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.