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. Stripe Entity Model Stripe Entity Maps To Notes Customer account One Stripe customer per billing account PaymentMethod account_payment_method pm_xxx stored as reference only 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.