Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Select an option

  • Save peterberkenbosch/d19243973e6782a6724318fa17e3b553 to your computer and use it in GitHub Desktop.

Select an option

Save peterberkenbosch/d19243973e6782a6724318fa17e3b553 to your computer and use it in GitHub Desktop.
Customer Pays Fees: Historical Bug Fixes and Issues

Customer Pays Fees: Historical Bug Fixes and Issues

This document contains historical information about bugs discovered and fixed during the implementation of the Customer Pays Fees feature. This is preserved for reference but is not part of the current operational documentation.

Timeline

April 2025: Initial Implementation

  • Only updated Payments::CalculateFee with fee rates per payable type
  • Stripe::CreatePaymentIntent and Payments::Create still hardcoded booking_fee
  • Root cause of subsequent bugs: Multiple fee calculation points with inconsistent rates

October 2025: Systematic Double-Fee Bug Discovery

  • Admin credit card payments for 6+ payable types were overcharging customers
  • Root cause: Multiple fee calculation points with inconsistent rates between services
  • Solution: Single source of truth in model layer with explicit fee_amount parameter
  • Added comprehensive controller specs to prevent regression

October 2025: A/R Balance Bug Discovery

  • Confirmation entries were debiting A/R for full amount including fees ($85.00)
  • Payment entries were crediting A/R for service amount only ($81.60)
  • Result: A/R balance showed fee amount ($3.40) instead of $0.00 after payment
  • Root cause: Confirmation services didn't exclude fee from A/R amounts
  • Solution: Added service_amount_for_ar helper, applied to both debits and credits
  • Fixed in EventSignups::RecordConfirmation and Bookings::RecordConfirmation

November 2025: Customer-Facing Double-Fee Bug Discovery

  • Customer-facing booking and voucher payments were double-charging fees
  • Affected controllers failed to pass fee_amount parameter to CreatePaymentIntent
  • Result: Service calculated fees again on already-fee-inclusive amounts

Bug Details

✅ FIXED: Customer Bookings and Vouchers Double-Fee Bug (November 11, 2025)

Status: FIXED

Affected Controllers:

  1. app/controllers/customer/bookings_controller.rb - paygo and regular booking payment flows
  2. app/controllers/customer/vouchers_controller.rb - voucher payment flow

Problem: Both controllers calculated amounts including fees but did NOT pass fee_amount parameter to CreatePaymentIntent. This caused CreatePaymentIntent to calculate and add fees AGAIN, resulting in double-charging.

Impact Before Fix:

  • Paygo bookings: Customers charged 5-10% extra (e.g., €105 becomes €110.25)
  • Regular bookings: Customers charged 5-10% extra on remaining balance
  • Voucher payments: Customers charged 5-10% extra
  • Affected ALL customer-facing facility bookings and vouchers when CUSTOMER_PAYS_FEES enabled

Fix Applied:

# customer/bookings_controller.rb - NOW FIXED
def payment_intent
  amount, fee_amount_cents = calculate_payment_amounts
  result = ::Stripe::CreatePaymentIntent.call!(@booking.customer, amount, description: @booking.payment_description, fee_amount: fee_amount_cents)
  render json: { clientSecret: result.payload.client_secret }, status: :created
end

private

def calculate_payment_amounts
  if params[:paygo].present?
    # Paygo: Calculate fee on initial total
    base_amount = @booking.initial_total
    amount = @booking.customer_pays_service_fee? ? base_amount * (1 + @booking.venue.booking_fee) : base_amount
    fee_amount_cents = @booking.customer_pays_service_fee? ? (base_amount * @booking.venue.booking_fee * 100).to_i : 0
  else
    # Regular payment: Calculate fee on amount due
    base_amount = @booking.amount_due
    amount = @booking.amount_due(for_stripe: true)
    fee_amount_cents = ((amount - base_amount) * 100).to_i
  end

  [amount, fee_amount_cents]
end

# customer/vouchers_controller.rb - NOW FIXED
def payment_intent
  voucher = current_customer.vouchers.find(params[:id])
  base_amount = voucher.amount_due
  amount = voucher.amount_due(for_stripe: true)
  fee_amount_cents = ((amount - base_amount) * 100).to_i

  result = ::Stripe::CreatePaymentIntent.call!(voucher.customer, amount, description: voucher.description, recipient: current_venue, fee_amount: fee_amount_cents)
  render json: { clientSecret: result.payload.client_secret }, status: :created
end

Test Coverage Added:

  • spec/controllers/customer/bookings_controller_spec.rb - Tests for paygo and regular payments, fees enabled/disabled
  • spec/controllers/customer/vouchers_controller_spec.rb - Tests for fees enabled/disabled
  • Both test suites verify correct fee_amount parameter is passed to CreatePaymentIntent

Related Work: This fix also corrected the admin booking overview to always display base amounts (not fee-inclusive amounts) everywhere except the payment modal fee banner. The fee banner only appears when paying with credit card via Stripe.

✅ FIXED: Admin Stripe Controller "amount_too_large" Error (November 11, 2025)

Status: FIXED

Affected Controller: app/controllers/admin/stripe_controller.rb

Problem: The controller had backwards logic for when to add fees:

  • It ONLY added fees for Invoices (Pattern 1)
  • It used base amount for Bookings (wrong - should add fees for all payables)

What Actually Happened:

  1. View passed: booking.amount_due (€30.00 base) + booking.service_fee (€1.50)
  2. Controller created PaymentIntent with: €30.00 (wrong - didn't add fee!)
  3. Payment record stored: €31.50 (correct)
  4. Stripe capture tried: €31.50 from €30.00 PaymentIntent → ERROR: "amount_too_large"

Fix Applied:

# app/controllers/admin/stripe_controller.rb
def payment_intent
  base_amount_param = params[:amount].to_d
  fee_amount_dollars = params[:fee_amount]&.to_d || 0

  # Add fees for ALL payables when customer_pays_fees is enabled
  amount = if @customer && current_venue.enabled?(Feature::CUSTOMER_PAYS_FEES) && fee_amount_dollars.positive?
             base_amount_param + fee_amount_dollars
           else
             base_amount_param
           end

  fee_amount_cents = (fee_amount_dollars * 100).to_i

  result = ::Stripe::CreatePaymentIntent.call!(
    @customer,
    amount,
    recipient: current_venue,
    description: params[:description],
    fee_amount: fee_amount_cents
  )
  render json: { clientSecret: result.payload.client_secret }, status: :created
end

Test Coverage Updated:

  • Updated spec/controllers/admin/stripe_controller_spec.rb to expect fee addition for all payables
  • Removed incorrect pattern-based tests
  • All admin payment flows now correctly tested

✅ FIXED: Booking Confirmation Email Amount Display (November 11, 2025)

Status: FIXED

Affected File: app/views/mailers/_booking_details.html.haml

Problem: The email template was using payable.subtotal_text to display the "Booking" amount in the fee breakdown. However, subtotal represents total - tax (not "total - fee"), resulting in incorrect amounts being displayed (€27.27 instead of €30.00).

Fix Applied: Changed line 57 from payable.subtotal_text to payable.total_text.

Result: Booking confirmation emails now correctly display:

  • Booking: €30.00 (base amount)
  • Transaction Fee: €1.50 (5% fee)
  • Total: €31.50 (base + fee)

⚠️ REMAINING: Customer Slots Controller (Requires Investigation)

Status: NOT FIXED - requires review

Controller: app/controllers/customer/slots_controller.rb

Potential Issue: Individual slot payment flow may have similar double-fee bug pattern.

Next Steps: Review customer/slots_controller.rb:payment_intent action and add specs if needed.

Lessons Learned

Root Causes

  1. Multiple Fee Calculation Points: Having fee logic in multiple places (services, controllers, models) led to inconsistency
  2. Implicit vs Explicit: Not explicitly passing fee_amount parameter caused services to recalculate fees
  3. Pattern Confusion: Trying to distinguish "Pattern 1 vs Pattern 2" added complexity that didn't match reality
  4. Accounting Distinction: Not clearly separating "service amount" from "total with fees" for A/R tracking

Solutions That Worked

  1. Single Source of Truth: Fee calculation in model layer (CustomerPayableFees concern)
  2. Explicit Parameters: Always pass fee_amount parameter to prevent recalculation
  3. Comprehensive Testing: Controller specs verify correct parameter passing
  4. Clear Separation: Accounting helper methods (service_amount_for_ar) for A/R vs revenue

Prevention Strategies

  1. Always pass fee_amount: When calling CreatePaymentIntent with fee-inclusive amounts
  2. Test both flows: Admin and customer-facing payment actions
  3. Check accounting: Verify A/R entries use service amount, not total with fees
  4. Document clearly: Don't over-complicate with "patterns" - just describe what happens

Impact Summary

Before Fixes:

  • Customer-facing bookings: Double-charged on fees (5-10% overcharge)
  • Customer-facing vouchers: Double-charged on fees (5-10% overcharge)
  • Admin bookings: Payments failed with "amount_too_large" error
  • Booking emails: Showed incorrect amounts
  • A/R balances: Incorrect balances after payment (showing fee amount instead of $0)

After Fixes:

  • ✅ All payment flows correctly calculate and charge fees once
  • ✅ Admin payments succeed with correct amounts
  • ✅ Emails display correct amounts
  • ✅ A/R balances correctly track service amounts
  • ✅ Comprehensive test coverage prevents regression

Test Coverage Summary

Added Specs:

  • spec/controllers/customer/bookings_controller_spec.rb (4 examples)
  • spec/controllers/customer/vouchers_controller_spec.rb (2 examples)
  • Updated spec/controllers/admin/stripe_controller_spec.rb (12 examples)

Existing Specs Verified:

  • Customer::InvoicesController (6 examples)
  • Customer::EventSignupsController (144 examples)
  • Customer::Memberships::BillingPeriodsController (111 examples)
  • All model specs with CustomerPayableFees concern

Total: 279+ examples covering fee calculation and payment intent creation across all payables.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment