docs: clarify payment finality semantics

This commit is contained in:
sirily
2026-03-10 17:37:10 +03:00
parent 064f76b5c0
commit 336cb7f33e

View File

@@ -10,7 +10,7 @@ The current payment system covers:
- default subscription plan bootstrap - default subscription plan bootstrap
- user subscription creation at registration time - user subscription creation at registration time
- invoice creation through a provider adapter - 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 - quota-cycle reset on successful activation
The current payment system does not yet cover: The current payment system does not yet cover:
@@ -42,6 +42,8 @@ The current payment system does not yet cover:
### `PaymentInvoice` ### `PaymentInvoice`
- stores one provider-facing payment attempt for a subscription - 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: - important fields:
- local `id` - local `id`
- `subscriptionId` - `subscriptionId`
@@ -91,6 +93,13 @@ Current runtime note:
5. The returned provider invoice data is persisted as a new local `PaymentInvoice` in `pending`. 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. 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 ## Invoice listing flow
- `GET /api/billing/invoices` returns the user's invoices ordered by newest first. - `GET /api/billing/invoices` returns the user's invoices ordered by newest first.
- This is a read-only view over persisted `PaymentInvoice` rows. - 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`. 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. 2. The web app resolves the admin session and passes actor metadata into the billing store.
3. `markInvoicePaid` runs inside one database transaction. 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. If the invoice is `pending`, the store: 4. `markInvoicePaid` runs inside one database transaction.
5. If the invoice is `pending`, the store:
- updates the invoice to `paid` - updates the invoice to `paid`
- sets `paidAt` - sets `paidAt`
- updates the related subscription to `active` - updates the related subscription to `active`
@@ -111,7 +121,11 @@ The implemented activation path is manual and admin-driven.
- clears `canceledAt` - clears `canceledAt`
- writes a `UsageLedgerEntry` with `entryType = cycle_reset` - writes a `UsageLedgerEntry` with `entryType = cycle_reset`
- writes an `AdminAuditLog` entry `invoice_mark_paid` - 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 ## Idempotency and transition rules
`markInvoicePaid` is replay-safe. `markInvoicePaid` is replay-safe.
@@ -156,11 +170,17 @@ Current payment-specific errors surfaced by the web app:
## Current limitations ## Current limitations
- The system still depends on manual admin confirmation to activate access. - 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 provider callback or reconciliation job updates invoice state automatically.
- No runtime path currently moves invoices to `expired` or `canceled`. - No runtime path currently moves invoices to `expired` or `canceled`.
- The provider adapter does not yet verify external status or signatures. - The provider adapter does not yet verify external status or signatures.
- Subscription lifecycle beyond the current `mark-paid` path is still incomplete. - 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 ## Code references
- `packages/db/src/bootstrap.ts` - `packages/db/src/bootstrap.ts`
- `packages/db/src/auth-store.ts` - `packages/db/src/auth-store.ts`