17 domain design docs covering architecture, accounts, inventory, rentals, lessons, repairs, POS, payments, batch repairs, delivery, billing, accounting, deployment, licensing, installer, and backend tech architecture. Plus implementation roadmap (doc 18) and personnel management (doc 19). Key design decisions documented: - company/location model (multi-tenant + multi-location) - member entity (renamed from student to support multiple adults) - Stripe vs Global Payments billing ownership differences - User/location/terminal licensing model - Valkey 8 instead of Redis
199 lines
6.2 KiB
Markdown
199 lines
6.2 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. 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. |