Replace stripe_customer_id on account with account_processor_link table. Update account_payment_method to use processor enum + processor_payment_method_id instead of Stripe-specific fields. Supports multiple simultaneous processors for migration scenarios.
6.7 KiB
Music Store Management Platform
Domain Design: Payments & Billing
1. Overview
The Payments domain describes how the application integrates with Stripe for all payment flows. It covers one-time payments at POS, recurring subscription billing for lessons and rentals, webhook event handling, and the AIM migration payment transition strategy.
2. Processor Entity Model
Payment processor customer and payment method references are stored in processor-agnostic tables, not directly on the account. This supports running multiple processors simultaneously (e.g. during migration) and adding new processors without schema changes.
Processor Concept
Maps To
Notes
Customer (Stripe cus_xxx, GP customer token)
account_processor_link
One link per processor per account — multiple links allowed during migration
PaymentMethod (Stripe pm_xxx, GP payment token)
account_payment_method
Processor-agnostic — processor enum + processor_payment_method_id
Subscription
enrollment / rental
One per billing group or standalone
SubscriptionItem
enrollment / rental line
One per enrollment/rental within subscription
Invoice
monthly bill
Auto-generated by Stripe on billing date
PaymentIntent
transaction
One-time charges at POS
Reader
terminal device
Physical Stripe Terminal reader
3. Recurring Billing Flow
-
Account created in app → Stripe Customer created via API → stripe_customer_id stored
-
Enrollment or rental activated → Stripe Subscription created or line item added
-
Billing date arrives → Stripe automatically charges payment method
-
Stripe sends webhook: invoice.paid → app updates subscription status, records payment
-
Stripe sends webhook: invoice.payment_failed → app flags account, triggers follow-up
-
Stripe card updater silently updates expired/replaced cards → reduces failed payments
4. Webhook Events
Event
Action
invoice.paid
Record payment, update subscription status, generate receipt
invoice.payment_failed
Flag account, notify store staff, trigger retry logic
customer.subscription.deleted
Update enrollment/rental status to cancelled
payment_intent.succeeded
Confirm POS transaction, update inventory status
payment_intent.payment_failed
Revert cart, notify staff
charge.dispute.created
Alert store management, flag transaction
5. AIM Migration — Payment Transition
5.1 The Problem
Existing AIM customers on recurring billing (rentals and lessons) are on a legacy payment processor. Stripe tokens are processor-specific — they cannot be transferred. Customers must re-enter their card to move to Stripe.
5.2 Transition Strategy
-
New customers: go directly to Stripe from day one
-
Migrated customers: flagged requires_payment_update = true on payment method
-
At each monthly renewal point, system prompts staff to collect new card
-
Customer re-enters card via Stripe Elements — new Stripe subscription created
-
Old processor subscription cancelled after successful Stripe charge confirmed
-
Both processors run in parallel until all active recurring billing migrated
5.3 Old Processor Wind-Down
-
Do not close old processor account until 120 days after last transaction
-
Chargebacks can arrive up to 120 days after transaction — account must remain open
-
Keep old processor credentials in Secrets Manager until confirmed closed
-
All historical transaction IDs preserved in database with processor field
6. Payment Provider Billing Ownership
The two supported payment processors have fundamentally different recurring billing models. This difference is the most important architectural consideration in the payment provider abstraction.
6.1 Stripe — Provider-Managed Billing
Stripe owns the billing schedule. When a rental or enrollment is activated, the platform creates a Stripe Subscription object. Stripe automatically charges the customer's payment method on each billing cycle. The platform learns about charges via webhooks (invoice.paid, invoice.payment_failed).
- Platform creates/cancels subscriptions — Stripe handles the schedule and charging
- Stripe Card Updater silently refreshes expired cards — reduces failed payments
- Proration calculated by Stripe when billing dates change or subscriptions are modified mid-cycle — platform does not need its own proration logic for Stripe stores
- Platform is reactive — it responds to webhook events, not proactive billing
6.2 Global Payments — Platform-Managed Billing
Global Payments provides tokenized card storage and on-demand charge capability, but does not offer a subscription/recurring billing API. The platform must own the billing schedule entirely.
- Platform stores GP payment tokens on
account_payment_method - BullMQ cron job runs daily — queries all rentals/enrollments with
billing_anchor_day = todayfor GP stores - Job calls
provider.charge()for each due item - Failed charges retry with exponential backoff (3 attempts over 7 days)
- GP's Decline Minimizer helps reduce false declines but does not auto-update expired cards
- Proration and partial-month charges calculated by the platform's billing service, not the processor
- Mid-cycle additions, cancellations, and billing date changes all require the platform to compute prorated amounts and charge/credit accordingly
6.3 PaymentProvider Interface Impact
The PaymentProvider interface must account for this split:
managedSubscriptions: boolean— Stripe: true, GP: false- If true:
createSubscription(),addSubscriptionItem(),removeSubscriptionItem(),cancelSubscription(),changeBillingAnchor()— the platform delegates billing lifecycle to the provider - If false:
charge()is the only billing method — the platform's BullMQ scheduler triggers charges on anchor days, handles retries, and manages the billing lifecycle internally - Both providers share:
createCustomer(),charge(),refund(),collectPayment()(terminal),parseWebhookEvent()
6.4 Billing Service Routing
The billing service checks store.payment_processor to determine the flow:
- Stripe store — enrollment/rental activation calls
provider.createSubscription(). Billing is hands-off after that. - GP store — enrollment/rental activation records the billing anchor day. The daily BullMQ job picks it up on the correct day and calls
provider.charge(). - Both paths produce the same internal records (
rental_payment,lesson_payment) and trigger the same accounting journal entries.
7. Database Schema
6.1 stripe_webhook_event
id, event_id (Stripe's ID), event_type, payload (jsonb),processed_at, status (received|processed|failed), error_message, created_at
All incoming webhook events are stored before processing. This enables replay if processing fails.