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.
201 lines
6.7 KiB
Markdown
201 lines
6.7 KiB
Markdown
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 = today` for 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. |