feat: add invoice polling reconciliation
This commit is contained in:
@@ -12,15 +12,15 @@ The current payment system covers:
|
||||
- invoice creation through a provider adapter
|
||||
- automatic renewal-invoice creation `72 hours` before `currentPeriodEnd`
|
||||
- one renewal-invoice creation attempt per paid cycle unless the user explicitly creates a new one manually
|
||||
- worker-side polling reconciliation for `pending` invoices
|
||||
- manual admin activation after an operator verifies that the provider reported a final successful payment status
|
||||
- automatic expiry of elapsed subscription periods during account and generation access checks
|
||||
- automatic expiry of elapsed pending invoices
|
||||
- automatic provider-driven `expired` and `canceled` transitions for pending invoices
|
||||
- quota-cycle reset on successful activation
|
||||
|
||||
The current payment system does not yet cover:
|
||||
- provider webhooks
|
||||
- polling-based reconciliation
|
||||
- automatic `expired` or `canceled` transitions
|
||||
- recurring billing
|
||||
|
||||
## Main records
|
||||
@@ -77,10 +77,12 @@ Payment-provider code stays behind `packages/providers/src/payments.ts`.
|
||||
|
||||
Current provider contract:
|
||||
- `createInvoice(input) -> providerInvoiceId, paymentAddress, amountCrypto, amountUsd, currency, expiresAt`
|
||||
- `getInvoiceStatus(providerInvoiceId) -> pending|paid|expired|canceled + paidAt? + expiresAt?`
|
||||
|
||||
Current runtime note:
|
||||
- the provider adapter is still a placeholder adapter
|
||||
- provider callbacks and status lookups are not implemented yet
|
||||
- worker polling is implemented, but provider-specific HTTP/status mapping is still placeholder logic
|
||||
- provider callbacks and webhook signature verification are not implemented yet
|
||||
- the rest of the payment flow is intentionally provider-agnostic
|
||||
|
||||
## Registration flow
|
||||
@@ -113,7 +115,18 @@ Current rule:
|
||||
- `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.
|
||||
- Worker reconciliation now polls provider status for stored `pending` invoices.
|
||||
- If the provider reports final `paid`, the worker activates access from provider `paidAt` when that timestamp is available.
|
||||
- If the provider reports final `expired` or `canceled`, the worker finalizes the local invoice to the same terminal status.
|
||||
|
||||
## Worker reconciliation flow
|
||||
1. The worker loads a batch of `pending` invoices that have `providerInvoiceId`.
|
||||
2. For each invoice, it calls `paymentProviderAdapter.getInvoiceStatus(providerInvoiceId)`.
|
||||
3. If the provider still reports `pending`, the worker leaves the invoice unchanged.
|
||||
4. If the provider reports `paid`, the worker calls the same idempotent activation path as admin `mark-paid`.
|
||||
5. If the provider reports `expired` or `canceled`, the worker atomically moves the local invoice from `pending` to that terminal state.
|
||||
6. After provider polling, the worker also expires any still-pending invoices whose local `expiresAt` has elapsed.
|
||||
7. If a manual admin action or another worker already finalized the invoice, reconciliation degrades to replay/no-op behavior instead of duplicating side effects.
|
||||
|
||||
## Invoice listing flow
|
||||
- `GET /api/billing/invoices` returns the user's invoices ordered by newest first.
|
||||
@@ -121,13 +134,13 @@ Current rule:
|
||||
- The worker also marks `pending` invoices `expired` when `expiresAt` has passed.
|
||||
|
||||
## Current activation flow
|
||||
The implemented activation path is manual and admin-driven.
|
||||
The implemented activation paths are:
|
||||
- automatic worker reconciliation after provider final status
|
||||
- manual admin override after an operator verifies provider final status outside the app
|
||||
|
||||
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. 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:
|
||||
Shared activation behavior:
|
||||
- `markInvoicePaid` runs inside one database transaction.
|
||||
- If the invoice is `pending`, the store:
|
||||
- updates the invoice to `paid`
|
||||
- sets `paidAt`
|
||||
- updates the related subscription to `active`
|
||||
@@ -136,8 +149,13 @@ The implemented activation path is manual and admin-driven.
|
||||
- sets `currentPeriodEnd = paidAt + 30 days`
|
||||
- clears `canceledAt`
|
||||
- writes a `UsageLedgerEntry` with `entryType = cycle_reset`
|
||||
- writes an `AdminAuditLog` entry `invoice_mark_paid`
|
||||
6. The API returns the updated invoice.
|
||||
- writes an `AdminAuditLog` entry `invoice_mark_paid` when actor metadata is present
|
||||
|
||||
Manual admin path:
|
||||
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. This endpoint is intended to be used only after the operator has already verified that the provider reached a final successful payment state.
|
||||
4. The API returns the updated invoice.
|
||||
|
||||
Important constraint:
|
||||
- `mark-paid` is not evidence by itself that a `pending` invoice became payable.
|
||||
@@ -193,16 +211,14 @@ Current payment-specific errors surfaced by the web app:
|
||||
- `invoice_transition_not_allowed` -> `409`
|
||||
|
||||
## 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 is still incomplete on the invoice side because provider-driven expiry, cancelation, and reconciliation are not implemented yet.
|
||||
- The provider adapter still uses placeholder status lookups; real provider HTTP integration is not implemented yet.
|
||||
- No provider callback or webhook signature verification path exists yet.
|
||||
- Manual admin `mark-paid` still exists as an override, so operator judgment is still part of the system for exceptional cases.
|
||||
- The worker polls invoice status in batches; there is no provider push path yet.
|
||||
|
||||
## Required future direction
|
||||
- Add provider callbacks or polling-based reconciliation.
|
||||
- Persist provider-final status before activating access automatically.
|
||||
- Replace placeholder provider status lookups with real provider integration.
|
||||
- Add provider callbacks or webhook ingestion on top of polling where the chosen provider supports it.
|
||||
- Reduce or remove the need for operator judgment in the normal payment-success path.
|
||||
|
||||
## Code references
|
||||
|
||||
Reference in New Issue
Block a user