Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 22 additions & 3 deletions docs/api/reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,9 @@ backend, where paths are relative to `/api` unless noted.

| Method | Path | Access | Behavior |
|---|---|---|---|
| `GET` | `/problems` | Optional user | Tag filter, user status, cursor/limit, max 200 |
| `GET` | `/problems/:slug` | Optional user | Detail/count; editorial only for Admin/accepted solver |
| `GET` | `/problems/:slug/practice` | Public | Active versioned browser-practice cases; `no-store` |
| `GET` | `/problems` | Optional user | Tag filter, user status, cursor/limit, max 200; annotates `accessTier` and `locked` |
| `GET` | `/problems/:slug` | Optional user | Detail/count; editorial only for Admin/accepted solver; enforced paid content returns `402 PLUS_REQUIRED` |
| `GET` | `/problems/:slug/practice` | Optional user | Active versioned browser-practice cases; enforced paid content returns `402 PLUS_REQUIRED`; `no-store` |
| `POST` | `/problems` | Admin | Create problem and bounded testcases |
| `PUT` | `/problems/:id` | Admin | Optimistic content and atomic-version testcase switch |
| `DELETE` | `/problems/:id` | Admin | Delete problem/testcases |
Expand All @@ -47,6 +47,25 @@ suite because browser execution is a non-adversarial practice mode, not ranked
judging. Testcase versions publish by a single Problem pointer switch, so
clients see either the old or new set and never a partial edit.

The launch free set is a stable server-owned allowlist of 60 problems. Paid
enforcement is independently controlled by `PAID_ENTITLEMENTS_ENFORCED`; while
it is false, access labels are visible but no problem is blocked.

## Billing and membership

| Method | Path | Access | Behavior |
|---|---|---|---|
| `GET` | `/billing/offers` | Public | Four immutable INR offer snapshots and safe runtime flags |
| `GET` | `/billing/summary` | User | Effective Free/Plus/Lumus entitlement and current billing state |
| `POST` | `/billing/checkouts` | User | Idempotent hosted Cashfree subscription/order session; requires Indian phone |
| `POST` | `/billing/subscriptions/:id/cancel` | Owner | Stop future renewals while preserving the paid period |
| `POST` | `/billing/webhooks/cashfree` | Signed provider | Raw-body HMAC, replay/amount/currency checks, fulfillment and full Lumus refund revocation |

Checkout accepts only an `offerKey`; price, currency and benefits are resolved
server-side. The return page is informational. Only a verified webhook creates
or revokes an entitlement. All billing, checkout, webhook processing and paid
enforcement flags default to false.

## Execution and submissions

| Method | Path | Access | Behavior |
Expand Down
28 changes: 28 additions & 0 deletions docs/architecture/data-model.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ erDiagram
USER ||--o{ SESSION : owns
USER ||--o{ EVALUATION_JOB : queues
USER ||--o{ AUDIT_EVENT : acts
USER ||--o{ ENTITLEMENT_GRANT : receives
USER ||--o{ BILLING_SUBSCRIPTION : owns
USER ||--o{ BILLING_PURCHASE : owns
USER }o--o{ CONTEST : registers
USER ||--o{ LEADERBOARD : ranks
PROBLEM ||--o{ SUBMISSION : receives
Expand Down Expand Up @@ -63,6 +66,31 @@ erDiagram
date revokedAt
string replacedBy
}
ENTITLEMENT_GRANT {
ObjectId userId
string tier
stringArray benefits
string sourceType
ObjectId sourceId
date startsAt
date endsAt
string status
}
BILLING_SUBSCRIPTION {
ObjectId userId
string offerKey
string providerSubscriptionId
string status
date currentPeriodEnd
boolean cancelAtPeriodEnd
}
BILLING_PURCHASE {
ObjectId userId
string offerKey
string providerOrderId
string status
date capturedAt
}
EVALUATION_JOB {
ObjectId userId
ObjectId submissionId
Expand Down
18 changes: 9 additions & 9 deletions docs/architecture/decisions.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,15 @@ Use short-lived cookie access tokens and rotated, hashed, server-tracked refresh
Sessions. Route live browser API traffic through the same-origin Next.js BFF;
never persist or return production bearer credentials to browser JavaScript.

### ADR-010 — Provider-neutral billing and internal entitlements

Use Cashfree as the initial India-first adapter while keeping Katalume's
server-side entitlement ledger as the source of truth for access. Weekly,
monthly, yearly and Lumus lifetime offers are versioned independently of the
provider. Browser callbacks never grant access; signed, replay-safe webhooks
do. Checkout and enforcement use independent safe-default feature flags. See
[Subscription readiness](subscriptions.md).

## Required before launch

### ADR-008 — Controlled launch scope
Expand All @@ -57,12 +66,3 @@ traffic only after production-like evidence passes.
Promote the same immutable release from staging to production. Production
requires explicit approval, post-deploy smoke tests, observable health, and a
tested rollback.

### ADR-010 — Provider-neutral billing and internal entitlements

Keep billing disabled during the free beta. When commercial activation is
approved, treat the payment provider as the source of truth for money movement
and Katalume's server-side entitlement ledger as the source of truth for access.
All provider calls sit behind an adapter, browser callbacks never grant access,
and webhook processing is signed, idempotent, durable, ordered, and
reconcilable. See [Subscription readiness](subscriptions.md).
149 changes: 88 additions & 61 deletions docs/architecture/subscriptions.md
Original file line number Diff line number Diff line change
@@ -1,13 +1,42 @@
# Subscription readiness

**Status:** target-state design only
**Runtime state:** disabled
**Pricing state:** undecided
**Payment provider state:** unselected

Katalume must not display checkout, collect payment details, create mandates, or
grant paid access from a client-side callback until every activation gate in
this document is closed. The public beta remains free.
**Status:** implementation complete; commercial activation pending

**Runtime state:** disabled by safe-default feature flags

**Pricing state:** version 1 launch configuration

**Payment provider state:** Cashfree selected for the India-first adapter

Katalume may display its membership catalog while checkout is disabled, but it
must not create mandates or orders until every activation gate in this document
is closed. A browser redirect never grants paid access. The production beta
remains fully accessible until paid-entitlement enforcement is separately
enabled.

## Implemented membership model

- Free includes a stable, topic-balanced set of 60 problems: 30 Easy, 20 Medium
and 10 Hard.
- Plus includes every current and future problem, Interview Tracks, premium
Progress intelligence, and the premium Profile identity layer.
- Plus is offered weekly, monthly, or yearly.
- Lumus is the one-time lifetime tier and carries the same all-access benefits.
- Competitions remain Coming Soon and are outside the billing gate.
- Learn remains outside this rollout until its content owner completes the
learning experience and the benefit split is reviewed.

| Offer key | Display name | Price | Collection |
|---|---|---:|---|
| `plus_weekly_in_v1` | Plus Weekly | ₹79 weekly | Cashfree periodic subscription |
| `plus_monthly_in_v1` | Plus Monthly | ₹249 monthly | Cashfree periodic subscription |
| `plus_yearly_in_v1` | Plus Yearly | ₹1,999 yearly | Cashfree periodic subscription |
| `lumus_lifetime_in_v1` | Lumus Lifetime | ₹4,999 once | Cashfree one-time order |

These are immutable version 1 offer snapshots in code, not permission for the
production account to charge. Changing a live price requires a new offer key.
The amounts and tax display still require owner and professional review before
activation.

## Design goals

Expand All @@ -19,11 +48,10 @@ this document is closed. The public beta remains free.
later without rewriting product authorization.
- Preserve a useful free tier and avoid locking learning history behind payment.

## Explicit non-goals for this phase
## Explicit non-goals for activation

- No prices, discounts, trials, or paid feature limits are approved.
- No provider SDK, API key, webhook secret, checkout route, or billing UI is
added to production.
- No discount, trial, coupon, proration, upgrade/downgrade, or family plan is
included in version 1.
- No payment-method data is stored by Katalume.
- No claim is made that lifetime access means the service must operate forever.
- This document is engineering readiness, not tax or legal advice.
Expand Down Expand Up @@ -64,17 +92,20 @@ unit-economics review.
7. Manual support grants must be separate, attributed, expiring where
appropriate, and auditable.

## Target data model
## Production data model

All identifiers below are internal opaque IDs unless prefixed with
`provider`.
`provider`. The shipped version implements the customer, subscription,
purchase, webhook-event, and entitlement-grant subset. Payment/refund ledgers,
tax profiles, dead-letter automation, and support reconciliation remain
activation gates rather than claims about the current code.

### `BillingCustomer`

| Field | Purpose |
|---|---|
| `userId` | Unique Katalume user |
| `provider` | Adapter key, for example `razorpay` |
| `provider` | Adapter key, initially `cashfree` |
| `providerCustomerId` | Encrypted or access-restricted external reference |
| `billingEmail` | Invoice/contact address; separate from login changes |
| `taxProfile` | Optional legal name, country/state, postal code, GSTIN after validation |
Expand Down Expand Up @@ -176,36 +207,28 @@ during a short provider outage.

## Backend module boundary

Future billing code should live behind a provider-neutral boundary:
Billing code lives behind this provider-neutral boundary:

```text
src/billing/
billing.service
entitlement.service
offer.repository
webhook.service
reconciliation.service
billing.service.js
entitlement.service.js
providers/
provider.interface
razorpay.adapter
cashfree.adapter.js
```

The adapter contract should cover:
The current adapter covers:

```text
createCustomer
createCheckout
fetchSubscription
cancelSubscription
createRefund
verifyWebhook
normalizeWebhook
```

Controllers must not call a provider SDK directly. Provider objects must not
leak into user or authorization models.

## Planned API surface
## Implemented API surface

All mutating endpoints require an authenticated user, CSRF protection where
applicable, distributed rate limits, and an idempotency key.
Expand All @@ -215,10 +238,8 @@ applicable, distributed rate limits, and an idempotency key.
| `GET /api/billing/offers` | Return active, server-approved offer snapshots |
| `GET /api/billing/summary` | Return the user's normalized billing and entitlement state |
| `POST /api/billing/checkouts` | Create a provider-hosted checkout/mandate flow |
| `POST /api/billing/subscriptions/:id/cancel` | Cancel now or at period end |
| `POST /api/billing/subscriptions/:id/resume` | Resume if the provider permits it |
| `POST /api/billing/webhooks/:provider` | Receive signed provider events; no user auth |
| `POST /api/admin/billing/reconcile/:id` | Restricted, audited repair |
| `POST /api/billing/subscriptions/:id/cancel` | Cancel future renewals; retain the already-paid access window |
| `POST /api/billing/webhooks/cashfree` | Receive signed Cashfree events; no user auth |

The frontend should learn access through `/api/billing/summary`; it must not
infer paid status from query parameters, local storage, or a provider response.
Expand All @@ -230,7 +251,6 @@ sequenceDiagram
participant U as User browser
participant K as Katalume API
participant P as Payment provider
participant W as Billing worker

U->>K: Create checkout (offerKey + idempotency key)
K->>K: Validate active offer and snapshot terms
Expand All @@ -240,10 +260,10 @@ sequenceDiagram
U->>P: Complete provider-controlled payment flow
P-->>U: Return to informational status page
P->>K: Signed webhook
K->>K: Verify signature and persist event once
K-->>P: 2xx after durable receipt
W->>K: Normalize event and update billing state atomically
W->>K: Create/revoke entitlement grant
K->>K: Verify raw-body signature and acquire event once
K->>K: Validate amount/currency and update billing state
K->>K: Create or revoke entitlement grant
K-->>P: 2xx after processing
U->>K: Fetch billing summary
K-->>U: Verified entitlement
```
Expand All @@ -255,12 +275,12 @@ Webhook processing rules:
side effects.
3. Persist the unique event and payload hash before acknowledging it.
4. Return success for an identical replay.
5. Process asynchronously with bounded retries and dead-letter alerting.
6. Lock the billing aggregate and reject stale state transitions using provider
occurrence time plus provider sequence/version when available.
7. Apply billing state, payment state, and entitlement changes in one database
transaction where supported.
8. Reconcile uncertain events against the provider API.
5. Acquire the event with a compare-and-set processing lease so concurrent
deliveries do not execute twice; a stale lease may be retried.
6. Reject stale subscription-state transitions using provider occurrence time.
7. Validate the server-owned amount and currency before every grant.
8. Before live activation, add scheduled provider reconciliation and alerts for
uncertain multi-write failures.

## India-first payment requirements

Expand Down Expand Up @@ -302,6 +322,8 @@ transaction; do not recompute old invoices from current settings.
- Enforce server-side offer lookup; reject client-supplied amounts, currency,
tier, tax, or provider plan IDs.
- Bind checkout ownership to the authenticated Katalume user.
- Cancel renewable mandates before account deletion; anonymize billing contact
data retained for financial/legal records and remove product entitlements.
- Tokenize through the provider and keep Katalume out of raw card/UPI data.
- Redact provider payloads, email, phone, GSTIN, addresses, and failure detail
from ordinary logs and error reporting.
Expand All @@ -327,16 +349,19 @@ paid entitlements, and enabling entitlement enforcement must not expose
checkout. Unknown or missing configuration fails closed for new purchases while
preserving already-verified access.

Provider-specific configuration is added only after selection:
Cashfree-specific configuration:

```text
BILLING_<PROVIDER>_KEY_ID
BILLING_<PROVIDER>_KEY_SECRET
BILLING_<PROVIDER>_WEBHOOK_SECRET
BILLING_<PROVIDER>_ACCOUNT_ID
CASHFREE_CLIENT_ID
CASHFREE_CLIENT_SECRET
BILLING_WEBHOOK_URL
BILLING_ENVIRONMENT=sandbox|production
```

Do not create these variables or placeholder secrets in production yet.
Secrets belong only in Render's secret store. `BILLING_WEBHOOK_URL` must be the
direct public backend URL ending in `/api/billing/webhooks/cashfree`; do not
route it through the frontend BFF because signature verification uses the exact
raw request bytes and Cashfree headers.

## Testing strategy

Expand All @@ -354,11 +379,11 @@ Do not create these variables or placeholder secrets in production yet.

- checkout request retry with one idempotency key;
- duplicate/out-of-order webhook delivery;
- event persisted but worker interrupted before state update;
- event acquired but the API interrupted before state update;
- provider timeout after an unknown checkout result;
- successful payment with delayed webhook;
- failed renewal, recovery, cancellation, refund, chargeback, and dispute;
- database transaction failure at each write boundary;
- database failure at each write boundary and stale processing-lease recovery;
- secret rotation and test/live environment isolation.

### Production-like proof
Expand Down Expand Up @@ -403,17 +428,17 @@ Never overwrite provider A identifiers with provider B identifiers.

## Activation gates

- [ ] Free-versus-paid benefits approved from user research
- [ ] Weekly/monthly/yearly/lifetime prices and tax display approved
- [x] Free-versus-paid benefits implemented behind independent enforcement
- [ ] Weekly/monthly/yearly/lifetime prices and tax display approved for live sale
- [ ] Lifetime terms and business liability approved
- [ ] Provider selected after capability, onboarding, support, and total-cost review
- [x] Cashfree selected as the initial India-first adapter
- [ ] Legal entity, bank/settlement, KYC, GST, invoices, terms, privacy, refunds, and cancellation approved
- [ ] Backend models, adapter, APIs, workers, migrations, and indexes implemented
- [ ] Frontend pricing, checkout, manage-billing, invoice, and recovery UX implemented
- [ ] Webhook signature, idempotency, ordering, retry, and reconciliation tests green
- [x] Backend models, adapter, APIs, access checks, and indexes implemented
- [x] Frontend pricing, hosted checkout, membership, cancellation, and recovery UX implemented
- [x] Webhook signature, replay, amount verification, subscription, and lifetime tests green
- [ ] Test/live secrets and environments isolated
- [ ] Support and finance runbooks rehearsed
- [ ] Accessibility, mobile, localization, analytics-consent, and security review passed
- [ ] Accessibility, mobile, localization, analytics-consent, and security review passed in staging
- [ ] Staged test-mode and owner-only live canary passed
- [ ] `BILLING_ENABLED` separately approved for production

Expand All @@ -424,8 +449,10 @@ flags remain false.

- [NPCI: UPI AutoPay](https://www.npci.org.in/product/autopay)
- [Reserve Bank of India: e-mandate framework index/circular](https://www.rbi.org.in/scripts/bs_circularindexdisplay.aspx/Scripts/BS_CircularIndexDisplay.aspx?Id=12722)
- [Razorpay: subscription webhook events](https://razorpay.com/docs/payments/subscriptions/subscribe-to-webhooks/)
- [Razorpay: webhook processing overview](https://razorpay.com/docs/webhooks/)
- [Cashfree: subscription FAQ](https://www.cashfree.com/docs/payments/subscription/faq)
- [Cashfree: webhook signatures](https://www.cashfree.com/docs/payments/subscription/webhook-signature)
- [Cashfree: subscription API](https://www.cashfree.com/docs/api-reference/payments/latest/subscription/overview)
- [Cashfree: hosted web checkout](https://www.cashfree.com/docs/payments/online/web/redirect)
- [GST portal: e-invoicing glossary](https://tutorial.gst.gov.in/downloads/news/pamphlet_e_invoicing_glossary_updated_17_08_2023_approved_final.pdf)

These links are implementation inputs, not permanent constants. Re-check them
Expand Down
Loading