14 — Payment Plans
What it is: Lets a fan pay for an order in installments over time instead of all at once. Two
tables: a per-program plan definition (tbl_payment_plans) the admin configures, and the
per-order installment schedule (tbl_payment_plan_installments) created when a fan checks out on a
plan. See the narrative walkthrough in Chapter 26 — Payment Plans.
Tables
tbl_payment_plans — the plan definition
One row per configured plan, scoped to a program. This is what the admin sets up; it does not belong to any order.
| Field | Notes |
|---|---|
paymentPlanId | PK |
programId | the Flex program this plan belongs to |
title / description | admin-facing label (title ≤ 100 chars) |
packIds / packageIds | JSONB arrays — which game-packs / preset-packages the plan applies to (null = not restricted by pack) |
accountGroupIds | JSONB array — gate the plan to specific account groups (see Account Groups & Ledgers) |
minEventCount | min events in the cart for the plan to be offered (nullable) |
minTotalAmount / maxTotalAmount | order-total bounds for eligibility (nullable) |
scheduleType | fixed or dynamic |
scheduleConfig | JSONB, discriminated by scheduleType — fixed: { dates: ['YYYY-MM-DD', …] }; dynamic: { cadence, intervalDays? } where cadence ∈ daily · weekly · biweekly · monthly · monthly_first · monthly_last · every_n_days (every_n_days also needs intervalDays) |
totalInstallments / endDate | dynamic bounding — at least one is required when scheduleType = 'dynamic' |
installmentAmount | fixed per-installment amount (nullable — otherwise the total is split evenly) |
firstInstallmentAmount | optional override for the first charge (e.g. a larger deposit) |
daysPriorToCharge | charge this many days before each due date (default 0) |
isDynamicShrink | default true — whether the schedule recomputes if the order total changes |
isActive | default true — only active plans are offered |
displayOrder | sort order among plans (default 0) |
tbl_payment_plan_installments — the per-order schedule
One row per scheduled charge, created at checkout when a fan picks a plan.
| Field | Notes |
|---|---|
installmentId | PK |
orderId | FK to tbl_orders — the order being paid off |
userId | the fan |
paymentPlanId | FK to the plan definition above |
installmentNumber / totalInstallments | "charge 2 of 4" |
dueDate | when this installment is due |
amount | scheduled amount |
amountActuallyCharged | what actually went through (nullable until charged) |
status | pending · processing · charged · failed · cancelled (the first installment is charged at checkout; the rest start pending) |
vendorInvoiceId | vendor reference for the charge (nullable) |
chargedAt | timestamp of the successful charge |
attemptCount / lastAttemptAt | retry tracking for failed charges |
reminderSentAt | when the upcoming-charge reminder was sent |
lastFailureCode / lastFailureReason / failureHistory | failure diagnostics (failureHistory is a JSONB log of attempts) |
Key rules
- A plan definition is per-program config; the installments are per-order and reference both the order and the plan.
- Eligibility for a plan is decided by the cart: pack/package match, account group, event count, and order-total bounds must all pass.
- The first installment is charged at checkout (
status = charged); subsequent ones are scheduledpendingand chargeddaysPriorToChargedays before theirdueDate. scheduleConfigshape depends onscheduleType— don't readdateson adynamicplan orcadenceon afixedone.
See Also
- Chapter 26 — Payment Plans — the narrative version
- Cart & Orders — where the order an installment pays off is created
- Account Groups & Ledgers — plan gating