fix: enforce subscription period end (#19)

Closes #3

## Summary
- enforce `currentPeriodEnd` as a hard access boundary for generation requests
- transition elapsed `active` and `past_due` subscriptions to `expired` during runtime reads
- stop showing active-cycle quota for non-active subscriptions and document the current lifecycle behavior
- add DB tests for post-expiry generation rejection and expired account-view normalization

## Testing
- built `infra/docker/web.Dockerfile`
- ran `pnpm --filter @nproxy/db test` inside the built container
- verified `@nproxy/db build` and `@nproxy/web build` during the image build

Co-authored-by: sirily <sirily@git.shararam.party>
Reviewed-on: #19
This commit was merged in pull request #19.
This commit is contained in:
2026-03-10 18:12:21 +03:00
parent 1b2a4a076a
commit 624c5809b6
7 changed files with 410 additions and 28 deletions

View File

@@ -11,6 +11,7 @@ The current payment system covers:
- user subscription creation at registration time
- invoice creation through a provider adapter
- 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
- quota-cycle reset on successful activation
The current payment system does not yet cover:
@@ -154,6 +155,14 @@ If the invoice does not exist, the store returns `invoice_not_found`.
- Approximate quota shown to the user is derived from `generation_success` entries since the current billing cycle start.
- A successful payment activation starts a new cycle by writing `cycle_reset` and moving the subscription window forward.
- Failed generations do not consume quota.
- When a subscription period has elapsed, user-facing quota is no longer shown as an active-cycle quota.
## Subscription period enforcement
- `currentPeriodEnd` is the hard end of paid access.
- At or after `currentPeriodEnd`, the runtime no longer treats the subscription as active.
- During generation access checks, an elapsed `active` subscription is transitioned to `expired` before access is denied.
- During account and billing reads, an elapsed `active` or `past_due` subscription is normalized to `expired` so the stored lifecycle is reflected consistently.
- There is no grace period after `currentPeriodEnd`.
## HTTP surface
- `POST /api/billing/invoices`
@@ -174,7 +183,7 @@ Current payment-specific errors surfaced by the web app:
- 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.
- Subscription lifecycle is still incomplete on the invoice side because provider-driven expiry, cancelation, and reconciliation are not implemented yet.
## Required future direction
- Add provider callbacks or polling-based reconciliation.