feat(payments): add the payments schema, currency domain and money type
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Successful in 10s
CI / integration (pull_request) Successful in 20s
CI / ui (pull_request) Has been skipped
CI / conformance (pull_request) Successful in 10s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m45s

Stand up the payments data foundation: a self-contained `payments` Postgres
schema for the in-game currency, wallets, benefits, catalog, orders and the
append-only operations ledger, behind a domain package — nothing wired to real
money yet.

- Migration 00010: the `payments` schema and a NOLOGIN confinement role (ALL on
  payments.*, nothing on backend); the ledger with a BEFORE UPDATE/DELETE
  append-only trigger and a partial idempotency index; the materialised
  balances/benefits; the catalog (atoms seeded) + products + per-method prices;
  the typed single-row config; orders and payment_events. There is no
  cross-schema foreign key — account_id is a plain uuid kept consistent in code,
  which keeps the domain extractable. Expand-contract and reversible.
- Money is a bigint in the currency's minor units carried by a `Money` value
  type (exact, math/big): no float ever touches an amount, and a whole-unit
  currency cannot hold a fraction.
- Extend jetgen to generate the payments schema; construct the service in the
  composition root behind a narrow interface with a boot reachability check.
- Tests: integration (role confinement via SET ROLE, the append-only trigger,
  CHECK constraints, the idempotency index, and a forward+backward migration),
  Money unit tests, and an import-boundary test keeping the payments jet code
  private to the domain.
- Docs: PLAN.md, docs/PAYMENTS.md (+ _ru mirror) updated to the built model.
This commit is contained in:
Ilia Denisov
2026-07-08 01:07:56 +02:00
parent d9bc7596c6
commit ce8b5026c1
34 changed files with 2243 additions and 78 deletions
+11 -6
View File
@@ -49,7 +49,8 @@ The chip balance is **segmented by `source`** — the platform where the chips w
| `direct` | Robokassa purchase (web / native)|
A single account holds all three segments simultaneously: `balance = (account_id, source)`,
exactly three rows. Segmentation is **not** per-identity — see §6.
up to three rows — a row is materialised lazily on the segment's first funding, and an absent
segment reads as zero. Segmentation is **not** per-identity — see §6.
Why segmented, not one pooled balance: store rules forbid activating value that was paid
for outside the store's own cash desk. Chips funded inside VK (Votes) may only be spent in
@@ -302,8 +303,11 @@ confirms the exact НПД scheme with a tax advisor.)
The payments domain lives in its **own schema `payments`** in the shared Postgres instance,
with its own DB role (rights limited to `payments`) and a domain package behind a hard
interface. Cross-schema FKs to `backend.accounts` keep the chip↔benefit spend atomic in one
transaction. Durability is **PITR** (continuous WAL archive), independent of DB topology.
interface. There is **no cross-schema foreign key** to `backend.accounts` — an account id is a
plain value here, kept consistent in code and joined to the tombstoned account /
retained-identities dossier by the stable id. That keeps the chip↔benefit spend atomic
**within `payments`** and the domain extractable into its own database. Durability is **PITR**
(continuous WAL archive), independent of DB topology.
Core tables (final names/columns fixed in `PLAN.md`):
@@ -318,9 +322,10 @@ Core tables (final names/columns fixed in `PLAN.md`):
(pending/paid/expired).
- **payment_events** — succeeded/failed/refunded for the dispatcher.
Legacy `accounts.hint_balance` and `accounts.paid_account` are **deprecated** and dropped
(expand-contract) in favour of the segmented model; neither was ever set in prod (no purchase
flow existed), so legacy values are zeroed.
Legacy `accounts.hint_balance` and `accounts.paid_account` are **deprecated** in favour of the
segmented model and dropped in a later **contract-phase** migration (expand-contract, after the
currency core flips reads and Release 2 — image rollback stays DB-safe); neither was ever set in
prod (no purchase flow existed), so legacy values are zeroed.
## 15. Glossary