diff --git a/docs/ops/payment-system.md b/docs/ops/payment-system.md index 7d9e5e2..76a18e4 100644 --- a/docs/ops/payment-system.md +++ b/docs/ops/payment-system.md @@ -10,7 +10,7 @@ The current payment system covers: - default subscription plan bootstrap - user subscription creation at registration time - invoice creation through a provider adapter -- manual admin activation when an invoice is confirmed as paid +- manual admin activation after an operator verifies that the provider reported a final successful payment status - quota-cycle reset on successful activation The current payment system does not yet cover: @@ -42,6 +42,8 @@ The current payment system does not yet cover: ### `PaymentInvoice` - stores one provider-facing payment attempt for a subscription +- `pending` means the invoice exists, but the payment is not yet considered final or accepted +- `paid` means the payment is considered final and safe to activate against - important fields: - local `id` - `subscriptionId` @@ -91,6 +93,13 @@ Current runtime note: 5. The returned provider invoice data is persisted as a new local `PaymentInvoice` in `pending`. 6. The API returns invoice details, including provider invoice id, amount, address, and expiry time. +## Payment status semantics +- `pending` does not count as paid. +- `pending` does not activate the subscription. +- `pending` does not reset quota. +- The system must treat an invoice as `paid` only after the payment provider reports a final successful status, meaning the funds are accepted strongly enough for access activation. +- The current implementation does not fetch or verify that provider-final status automatically yet. + ## Invoice listing flow - `GET /api/billing/invoices` returns the user's invoices ordered by newest first. - This is a read-only view over persisted `PaymentInvoice` rows. @@ -100,8 +109,9 @@ The implemented activation path is manual and admin-driven. 1. An authenticated admin calls `POST /api/admin/invoices/:id/mark-paid`. 2. The web app resolves the admin session and passes actor metadata into the billing store. -3. `markInvoicePaid` runs inside one database transaction. -4. If the invoice is `pending`, the store: +3. This endpoint is intended to be used only after the operator has already verified that the provider reached a final successful payment state. +4. `markInvoicePaid` runs inside one database transaction. +5. If the invoice is `pending`, the store: - updates the invoice to `paid` - sets `paidAt` - updates the related subscription to `active` @@ -111,7 +121,11 @@ The implemented activation path is manual and admin-driven. - clears `canceledAt` - writes a `UsageLedgerEntry` with `entryType = cycle_reset` - writes an `AdminAuditLog` entry `invoice_mark_paid` -5. The API returns the updated invoice. +6. The API returns the updated invoice. + +Important constraint: +- `mark-paid` is not evidence by itself that a `pending` invoice became payable. +- It is only the current manual mechanism for recording that the provider has already given final confirmation outside the app. ## Idempotency and transition rules `markInvoicePaid` is replay-safe. @@ -156,11 +170,17 @@ Current payment-specific errors surfaced by the web app: ## Current limitations - The system still depends on manual admin confirmation to activate access. +- Because provider-final status is not ingested automatically yet, the app currently relies on operator judgment when calling `mark-paid`. - No provider callback or reconciliation job updates invoice state automatically. - No runtime path currently moves invoices to `expired` or `canceled`. - The provider adapter does not yet verify external status or signatures. - Subscription lifecycle beyond the current `mark-paid` path is still incomplete. +## Required future direction +- Add provider callbacks or polling-based reconciliation. +- Persist provider-final status before activating access automatically. +- Reduce or remove the need for operator judgment in the normal payment-success path. + ## Code references - `packages/db/src/bootstrap.ts` - `packages/db/src/auth-store.ts`