6.0 KiB
6.0 KiB
Payment System
Purpose
Describe how billing and payment processing currently work in nproxy.
This document is about the implemented system, not the desired future state.
Scope
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
- quota-cycle reset on successful activation
The current payment system does not yet cover:
- provider webhooks
- polling-based reconciliation
- automatic
expiredorcanceledtransitions - recurring billing
Main records
SubscriptionPlan
- one default active plan is bootstrapped by
packages/db/src/bootstrap.ts - current default seed:
code:monthlydisplayName:MonthlymonthlyRequestLimit:100monthlyPriceUsd:9.99billingCurrency:USDT
Subscription
- created for a new user during registration if the default plan exists
- starts in
pending_activation - becomes
activeonly after successful invoice activation - stores:
activatedAtcurrentPeriodStartcurrentPeriodEndrenewsManually
PaymentInvoice
- stores one provider-facing payment attempt for a subscription
- important fields:
- local
id subscriptionIdproviderproviderInvoiceIdstatuscurrencyamountCryptoamountUsdpaymentAddressexpiresAtpaidAt
- local
UsageLedgerEntry
- records quota-affecting events
- on successful payment activation the system writes
cycle_reset - successful generations later write
generation_success
AdminAuditLog
- records state-changing admin actions
- the current admin
mark-paidflow writes:invoice_mark_paidinvoice_mark_paid_replayed
Provider boundary
Payment-provider code stays behind packages/providers/src/payments.ts.
Current provider contract:
createInvoice(input) -> providerInvoiceId, paymentAddress, amountCrypto, amountUsd, currency, expiresAt
Current runtime note:
- the provider adapter is still a placeholder adapter
- provider callbacks and status lookups are not implemented yet
- the rest of the payment flow is intentionally provider-agnostic
Registration flow
- User registers through
POST /api/auth/register. packages/db/src/auth-store.tscreates the user and session.- If the default plan exists, the store also creates a
Subscriptioninpending_activation. - At this point the user has an account and a plan assignment, but not an active paid cycle.
Invoice creation flow
- Authenticated user calls
POST /api/billing/invoices. packages/db/src/billing-store.tsloads the latest subscription for that user.- If there is already a non-expired
pendinginvoice for the same subscription, it is reused. - Otherwise the app calls the payment-provider adapter
createInvoice. - The returned provider invoice data is persisted as a new local
PaymentInvoiceinpending. - The API returns invoice details, including provider invoice id, amount, address, and expiry time.
Invoice listing flow
GET /api/billing/invoicesreturns the user's invoices ordered by newest first.- This is a read-only view over persisted
PaymentInvoicerows.
Current activation flow
The implemented activation path is manual and admin-driven.
- An authenticated admin calls
POST /api/admin/invoices/:id/mark-paid. - The web app resolves the admin session and passes actor metadata into the billing store.
markInvoicePaidruns inside one database transaction.- If the invoice is
pending, the store:- updates the invoice to
paid - sets
paidAt - updates the related subscription to
active - sets
activatedAtif it was not already set - sets
currentPeriodStart = paidAt - sets
currentPeriodEnd = paidAt + 30 days - clears
canceledAt - writes a
UsageLedgerEntrywithentryType = cycle_reset - writes an
AdminAuditLogentryinvoice_mark_paid
- updates the invoice to
- The API returns the updated invoice.
Idempotency and transition rules
markInvoicePaid is replay-safe.
Allowed cases
pending -> paidpaid -> paidas a no-op replay
Replay behavior
If the invoice is already paid:
- the subscription is not mutated again
- no extra
cycle_resetentry is created - an audit event
invoice_mark_paid_replayedis written
Rejected cases
If the invoice is already terminal:
expiredcanceled
the store rejects the request with invoice_transition_not_allowed.
If the invoice does not exist, the store returns invoice_not_found.
How quota ties to payment
- A user can create generation requests only with an active subscription.
- Approximate quota shown to the user is derived from
generation_successentries since the current billing cycle start. - A successful payment activation starts a new cycle by writing
cycle_resetand moving the subscription window forward. - Failed generations do not consume quota.
HTTP surface
POST /api/billing/invoices- create or reuse the current pending invoice for the authenticated user
GET /api/billing/invoices- list the authenticated user's invoices
POST /api/admin/invoices/:id/mark-paid- admin-only manual payment activation path
Error behavior
Current payment-specific errors surfaced by the web app:
invoice_not_found->404invoice_transition_not_allowed->409
Current limitations
- The system still depends on manual admin confirmation to activate access.
- No provider callback or reconciliation job updates invoice state automatically.
- No runtime path currently moves invoices to
expiredorcanceled. - The provider adapter does not yet verify external status or signatures.
- Subscription lifecycle beyond the current
mark-paidpath is still incomplete.
Code references
packages/db/src/bootstrap.tspackages/db/src/auth-store.tspackages/db/src/billing-store.tspackages/db/src/account-store.tspackages/providers/src/payments.tsapps/web/src/main.ts