Bug Fix: Order 181 - Automated Capture Issue

The Problem

Order 181 (and potentially others) failed to auto-capture properly, resulting in repeated failed capture attempts.

Symptoms:

Root Cause

Bug in app/models/order.rb line 197-201:

def has_uncaptured_authorization?
  payment_transactions
    .where(transaction_type: "auth_only", status: "approved")
    .exists?  # ❌ BUG: Doesn't check captured_at!
end

This method returned true for already-captured authorizations because:

  1. It only checked transaction_type: "auth_only" and status: "approved"
  2. It didn't check if captured_at was NULL
  3. When an authorization is captured, captured_at gets set, but status stays "approved"

The Timeline (Order 181)

Date Event What Happened
Oct 25 Order created TX 35: authorization successful
Oct 26 8:24am Manual capture TX 35 captured (capture_source: "manual_fix")
captured_at set to Oct 26
P1 marked as "paid"
❌ Order payment_status NOT updated
Nov 22 8am Auto-capture P2 has_uncaptured_authorization? returns true (WRONG!)
Tried to re-capture TX 35 → FAILED
Error: "Payment declined" (already captured)
Nov 22 9:29am Retry attempt Same failure
Nov 23 8am Retry attempt Same failure

The Fix

1. Fixed the has_uncaptured_authorization? Method

Before:

def has_uncaptured_authorization?
  payment_transactions
    .where(transaction_type: "auth_only", status: "approved")
    .exists?
end

After:

def has_uncaptured_authorization?
  payment_transactions
    .where(transaction_type: "auth_only", status: "approved")
    .where(captured_at: nil)  # ✅ Only truly uncaptured auths
    .exists?
end

2. Fixed Order 181's Payment Status

Updated payment_status from "failed" to "paid" to reflect that P1 is paid.

Order.find(181).update_column(:payment_status, "paid")

Impact

Before Fix:

After Fix:

Testing

Verification on Order 181:

o = Order.find(181)
o.has_uncaptured_authorization?  # Returns false (correct!)
# TX 35 has captured_at set, so it's excluded

What Happens Next:

Related Orders

This bug likely affected other orders where:

  1. P1 was manually captured (capture_source: "manual_fix")
  2. The automated job later tried to charge a subsequent period
  3. Capture attempts failed because old auth was already captured

To find potentially affected orders:

# Orders with manually-captured first period
affected = PaymentTransaction
  .where(capture_source: "manual_fix")
  .joins(:order)
  .where(orders: { is_recurring: true })
  .distinct
  .pluck(:order_id)

Order.where(id: affected).each do |order|
  puts "Order #{order.id} (#{order.order_number})"
  puts "  payment_status: #{order.payment_status}"
  puts "  has_uncaptured_authorization?: #{order.has_uncaptured_authorization?}"
end

Prevention

This fix prevents:

  1. Re-capturing already-captured authorizations
  2. Failed billing for recurring orders
  3. Customer confusion from failed payment emails

The system now correctly distinguishes between:

Files Modified

Deployment

This fix has been applied to production:

No other orders currently affected (verified via production check).