Payment Method Safeguards

Problem This Solves

Previously, it was possible to create an invoice for a customer/order while the order was still configured for automatic credit card charging. This created a double-billing scenario where:

  1. The RecurringCardChargeJob would auto-charge the credit card daily
  2. The customer would also receive an invoice for the same order
  3. The customer could end up being billed twice for the same service

Solution: Validation-Based Prevention

We now block attempts to add credit card orders to invoices, forcing administrators to make an explicit decision about payment method.

How It Works

1. Model-Level Validation

When attempting to add an order to an invoice, the system checks:

# app/models/invoice.rb - add_order method
if order.payment_method == "credit_card" && order.is_recurring?
  raise Invoice::InvalidPaymentMethodError
end

2. Controller-Level Error Handling

The controller catches this exception and shows a clear error message:

Cannot add Order #O2025110308562201BA to invoice because it's configured
for automatic credit card charging. This would result in double-billing
(auto-charge + invoice).

To add this order to an invoice, first switch it to Purchase Order payment using:
bin/rails runner scripts/switch_order_to_po.rb 223

3. Explicit Admin Action Required

Admins must consciously decide to:

Error Message Locations

The validation triggers in two places:

  1. Creating New Invoice (admin/invoices#create)

  2. Adding Orders to Existing Invoice (admin/invoices#add_orders)

Finding Existing Mismatches

Check for Problem Orders

# In Rails console or script
mismatched_orders = Order.payment_method_mismatch

puts "Found #{mismatched_orders.count} orders with payment method mismatches:"
mismatched_orders.each do |order|
  puts "  Order #{order.id} (#{order.order_number}): #{order.customer_name}"
  puts "    payment_method: #{order.payment_method}"
  puts "    invoice_id: #{order.invoice_id}"
  puts "    next_billing_date: #{order.next_billing_date}"
  puts ""
end

Fix Existing Mismatches

For each mismatched order, decide:

Option A: Customer is switching to PO (invoice is correct)

bin/rails runner scripts/switch_order_to_po.rb ORDER_ID net_30

Option B: Invoice was created in error (remove from invoice)

order = Order.find(ORDER_ID)
order.update!(invoice_id: nil)

Production Check

# On production
ssh root@5.78.141.175 'docker exec $(docker ps --filter label=service=quarry_rentals --filter label=role=web --format "{{.Names}}" | head -1) bin/rails runner "
puts \"Checking for payment method mismatches...\"
mismatched = Order.payment_method_mismatch
puts \"Found #{mismatched.count} mismatched orders\"
mismatched.each do |o|
  puts \"  - Order #{o.order_number}: #{o.customer_name} (ID: #{o.id})\"
end
"'

How to Switch an Order to PO

Method 1: Using the Script (Recommended)

# Local
bin/rails runner scripts/switch_order_to_po.rb ORDER_ID net_30

# Production
ssh root@5.78.141.175 'docker exec $(docker ps --filter label=service=quarry_rentals --filter label=role=web --format "{{.Names}}" | head -1) bin/rails runner scripts/switch_order_to_po.rb ORDER_ID net_30'

Method 2: Manual Commands

ssh root@5.78.141.175 'docker exec $(docker ps --filter label=service=quarry_rentals --filter label=role=web --format "{{.Names}}" | head -1) bin/rails runner "
o = Order.find(ORDER_ID)
o.payment_method = \"purchase_order\"
o.payment_terms = \"net_30\"
o.payment_profile_id = nil
o.payment_status = \"unpaid\"
o.save!
"'

Workflow Example

Scenario: Customer calls to switch from CC to invoicing

  1. Customer calls: "We want invoices instead of auto-charging"
  2. Admin creates invoice in admin panel
  3. System blocks with error: "Cannot add Order #... because it's configured for auto-charging"
  4. Admin runs switch script: bin/rails runner scripts/switch_order_to_po.rb 223 net_30
  5. Admin tries again: Successfully adds order to invoice
  6. Result: Order now on PO, invoice sent, no more auto-charges

Benefits

Prevents double-billing - Can't happen anymore ✅ Forces conscious decision - Admin must think about payment method ✅ Clear error messages - Tells you exactly what to do ✅ Audit trail - Script logs when switches happen ✅ No silent changes - System doesn't assume what you want

Technical Details

Exception Class

class Invoice::InvalidPaymentMethodError < StandardError; end

Validation Logic

Scope for Finding Issues

# app/models/order.rb
scope :payment_method_mismatch, -> {
  recurring
    .where(payment_method: "credit_card")
    .where.not(invoice_id: nil)
    .where.not(status: ["cancelled", "completed"])
}

Related Documentation

Maintenance

Monthly Check (Recommended)

Run this check monthly to ensure no mismatches exist:

# Production
ssh root@5.78.141.175 'docker exec $(docker ps --filter label=service=quarry_rentals --filter label=role=web --format "{{.Names}}" | head -1) bin/rails runner "
mismatched = Order.payment_method_mismatch
if mismatched.any?
  puts \"⚠️  WARNING: #{mismatched.count} orders with payment method mismatches\"
  mismatched.each { |o| puts \"  - #{o.order_number}\" }
else
  puts \"✓ No payment method mismatches found\"
end
"'

If any are found, investigate why the safeguard didn't catch them (possibly created before this safeguard was added).