openvibers/openvibe.billing AGENTS.md

AGENTS.md

main 10.0 KB · 164 lines · markdown

1# AGENTS.md — OpenVibe.Billing
2
3## Project overview
4
5The isolated money ledger of the OpenVibe network: one append-only, balanced, double-entry journal
6for every MONEY and CREDIT flow, provider receipts, subscriptions and entitlements, refunds,
7cashouts, usage rating, promo allowances and Vibes Pay. Node.js/Express, PostgreSQL (`ov_billing`),
8CommonJS, **no build step**.
9
10- Service id `billing`, port `4600`, staff console `https://billing.openvibe.network`; the service API
11 `/api/v1` is reachable **only on loopback** (`127.0.0.1:4600`), never through the public vhost.
12- Authority since 2026-10-17: `BILLING_AUTHORITY=billing` in Billing and Live, PowerChat deliveries
13 settle here, and Live's old money tables are read-only. Every site's account menu reads a person's
14 Vibes from `GET /api/me/vibes`.
15- Economic classification is [ADR-012](https://github.com/OpenVibers/OpenVibe.Contracts/blob/main/docs/adr/ADR-012-economic-classification.md).
16
17## What it owns
18
19- The journal: `accounts`, `transactions`, `ledger_entries`, `account_balances` in `ov_billing`
20 ([migrations/](migrations/)); **append-only** — triggers refuse UPDATE/DELETE, and every
21 transaction sums to zero per currency.
22- Three ledgers that never mix: bought Vibes (`user_credit`), creator money (`creator_payable`,
23 `payouts_pending`, `platform_revenue`, `refunds`, `chargeback_loss`), promo allowance
24 (`promo_credit` against a dedicated `promo_reserve`, never `platform_revenue`). Credit becomes
25 money only by transfer; money leaves only by cashout; promo has no such path.
26- Provider webhooks (PowerChat, Stripe, PayPal, CCBill, NOWPayments) — they land here and nowhere
27 else; a receipt is stored before it is processed.
28- Subscriptions and entitlements, intents, cashouts, usage readings (`usage_records`), rating
29 (`server/ops/rating.js`), promo windows, the freeze, reconciliation/import runs and the staff audit.
30- Vibes Pay (D12): app charges, the project's `project_payable` wallet, its signed webhooks and
31 sandbox Vibes ([docs/vibes-pay.md](docs/vibes-pay.md)).
32- The `billing.*` events and the public billing policy page.
33
34Not owned: loyalty points (OpenCoins, channel points), cosmetics (Inventory), game state, legacy
35balances, or any product UI.
36
37## Who calls it, and whom it calls
38
39- Callers: every product that moves money or grants an entitlement — Live (checkouts, donations,
40 subscriptions, cashouts), Tips, VIP, Inventory, Exchange and the account menus of every site. They
41 use a Network **service token** audience `openvibe.billing` with the route's capability.
42- Depends on: Network (JWKS, service tokens, SSO+PKCE for the console, `identity.subject.resolve` for
43 the Live import); Events (SDK outbox, the `billing.*` events); the configured payment providers;
44 PostgreSQL 18 and Valkey 9 (per-person limit counters; without `VALKEY_URL` they count in process).
45
46## Repository map
47
48- **Entry:** [server/index.js](server/index.js) — config, app, background jobs (provider event retry,
49 hourly renewal sweep, hourly reconciliation, outbox relay; rating only when its interval is set).
50- **App:** [server/app.js](server/app.js) — `/webhooks`, `/internal/credits`, `/api/v1/me`, `/api/v1`,
51 `/api/me/vibes`, the staff console and machine endpoints.
52- **Config:** [server/config.js](server/config.js), [server/config-store.js](server/config-store.js)
53 (owner settings, D11), [server/internal-config.js](server/internal-config.js).
54- **Database:** [server/db.js](server/db.js); schema in [migrations/](migrations/) (`0001…0018`).
55- **Ledger:** [server/ledger.js](server/ledger.js), [server/money/](server/money/) (reasons),
56 [server/ops/](server/ops/) — `transfers.js`, `cashouts.js`, `subscriptions.js`, `promo.js`,
57 `rating.js`, `usage.js`, `intents.js`, `wallet.js`, `admin.js`.
58- **API:** [server/api/](server/api/) — `v1.js`, `internal.js`, `me.js`, `webhooks.js`,
59 `subject-assertion.js`, `idempotency.js`, `auth.js`, `actor-limits.js`.
60- **Providers:** [server/providers/](server/providers/) — `powerchat.js`, `stripe.js`, `paypal.js`,
61 `nowpayments.js`, `index.js`.
62- **Console:** [server/console/](server/console/) — server-rendered, Network SSO, staff only.
63- **Scripts:** [scripts/reconcile.js](scripts/reconcile.js), `freeze.js`, `import-live.js`,
64 `load-rate-cards.js`.
65- **Deploy:** [deploy/systemd/openvibe-billing.service](deploy/systemd/openvibe-billing.service),
66 [deploy/nginx/billing.openvibe.network.conf](deploy/nginx/billing.openvibe.network.conf).
67
68## Commands
69
70```bash
71npm ci
72cp .env.example .env
73npm run dev # http://localhost:4600
74npm test # test/run.js, PGlite, stub Network/Events/providers
75npm run test:pg # same suite on PostgreSQL
76npm run reconcile # reconciliation report; exit 1 on failure
77npm run freeze -- status|on "<reason>"|off
78npm run import-live # LIVE_DATABASE_URL=… (see README); --dry-run, --json
79ov test # the harness's sharded run of what the change reaches
80```
81
82Tests need no network, provider key or running Network: in-process stubs, temp databases, random
83ports. `npm test -- import webhooks` filters; `-- --strict` makes a skip fail.
84
85## Database and migrations
86
87PostgreSQL database `ov_billing` on the host's data role (ADR-035); development/tests use PGlite.
88Async reads/writes through `openvibe-sdk/db`; an INSERT that needs its id uses `RETURNING id`. Schema
89is `migrations/NNNN_*.sql`, applied at boot; **never edit a migration that has run** (its checksum is
90recorded) and never create a table at runtime. Money integrity is enforced in SQL: the append-only
91triggers, the zero-sum rule per currency and the balance checks. Do not weaken them for convenience.
92
93## Auth, idempotency and the freeze
94
95- `/api/v1` is loopback-only (nginx denies it publicly), needs a Network service token audience
96 `openvibe.billing` with the route's capability, and **every POST needs an `Idempotency-Key`**.
97 A replay with the same money fields returns the first row; different money fields are
98 `409 billing.usage_key_reused` / the matching reason.
99- The staff console is Network SSO with PKCE, limited to `BILLING_STAFF_SUBJECTS`; every staff action
100 is audited as `billing.staff.action`.
101- **The freeze** (`money_writes_frozen` / `npm run freeze`) stops every mutation: writes answer
102 `503 billing.frozen`, reads still work, provider webhooks are stored (202) and processed in arrival
103 order after unfreeze, and jobs pause. Rating and renewals never run while frozen.
104
105## Frontend loading model
106
107The only UI is the **staff console**, server-rendered HTML (no SPA, no build step, no bundler); there
108is no `public/` app. `/shared/*` is served from the pinned `openvibe-shared` package where used.
109Provider pages carry no product UI; a person's balance is read by each site from `/api/me/vibes`.
110
111## Deploy
112
113`sudo ovhost deploy billing` (strategy `git-checkout`): fetch, fast-forward `/opt/openvibe.billing`,
114install on a lockfile change, restart, wait for `/api/ready`, roll back on failure. Unit
115`openvibe-billing.service`, env `/etc/openvibe/billing.env` (0600), vhost
116[deploy/nginx/billing.openvibe.network.conf](deploy/nginx/billing.openvibe.network.conf). The release
117migrates `ov_billing` at boot; `sudo ovhost rollback billing --to <sha>`. Cutover, freeze and import
118runbooks are in the README and [docs/](docs/); follow them, do not improvise a money migration.
119
120## Tests
121
122`npm test` runs each `test/*.test.js` in its own process through `openvibe-shared/test-runner`
123(60 s/file), with stub Network/Events/providers. The suite pins the ledger invariants, the three
124separate ledgers and promo isolation, promo grant/consume/lapse, idempotency, webhook storage and
125ordering, the freeze, renewals/grace, reconciliation, capabilities, the public words and the
126nginx/vhost rules. `test/public-words.test.js` and `test/nginx-vhost.test.js` pin files, so a copy
127change may require the matching test update. `ov test` runs the same.
128
129## Conventions
130
131- CommonJS, 4-space indent, single quotes, semicolons; factories with injectable `now`, `fetchImpl`,
132 `log`, `keys`, `identity`, `adapters` and `limitsNow` so tests are deterministic.
133- Amounts are integer **vibes-bits**; never use floats for money. Reasons are enum values from
134 `openvibe-contracts` money reasons, not free text.
135- Errors are RFC 9457 problem+json with stable codes (`billing.insufficient_funds`,
136 `billing.promo_isolated`, `billing.frozen`, …).
137- Secrets (provider keys, webhook secrets, OAuth) come from the environment and are never returned,
138 logged or written to a file.
139
140## Pitfalls
141
142- **Never write money outside the ledger.** A product must call an operation, not `UPDATE` a balance.
143- **Promo is not credit.** A transfer/cashout from `promo_credit` is refused; an admin adjustment
144 mixing promo with another kind is `billing.promo_isolated`; reconciliation checks it.
145- **Every POST needs an Idempotency-Key**; without it the route refuses, and a reused key with
146 different money fields is a 409, not a second charge.
147- **Webhooks are stored before processing**; never process inline and lose the receipt on a crash.
148- **The freeze is global.** New mutations must check it; the relay and reconciliation are the allowed
149 exceptions.
150- **`/api/v1` is not public.** Do not add it to the vhost; only the console, `/webhooks/*` and
151 `/api/health` are exposed.
152- **Imports read Live's data, never write it.** `scripts/import-live.js` is the only bridge and needs
153 `LIVE_DATABASE_URL`.
154- **Tests pin copy and nginx.** Update the test with an intended change; never skip the assertion.
155
156## Documentation
157
158- [README.md](README.md) — the journal, routes, providers, operations and deploy runbook.
159- [docs/economic-inventory.md](docs/economic-inventory.md) — every value flow and its class.
160- [docs/wallet.md](docs/wallet.md), [docs/balance.md](docs/balance.md), [docs/vibes-pay.md](docs/vibes-pay.md).
161- [docs/live-cutover.md](docs/live-cutover.md), [docs/cutover-usage-rating.md](docs/cutover-usage-rating.md),
162 [docs/renewal-grace-cutover.md](docs/renewal-grace-cutover.md), [docs/live-mapping.md](docs/live-mapping.md).
163- [docs/sdk-outbox.md](docs/sdk-outbox.md) — the event outbox.
164