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
- 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`