feat(payments): chip wallet, store-compliance gate and benefit application
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Successful in 11s
CI / integration (pull_request) Successful in 22s
CI / ui (pull_request) Successful in 1m7s
CI / conformance (pull_request) Successful in 9s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m41s
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Successful in 11s
CI / integration (pull_request) Successful in 22s
CI / ui (pull_request) Successful in 1m7s
CI / conformance (pull_request) Successful in 9s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m41s
Stand up the internal chip/benefit mechanic behind the narrow payments interface: context-aware balances and benefits, an atomic chip spend, admin grants as zero-price value sales, the one-directional store-compliance gate (VK/TG same- origin only, web draws direct→vk→tg, VK-iOS frozen, untrusted fail-closed), and per-origin hint and no-ads application with term stacking. Reads are served from an in-process, account-keyed write-through cache (mirroring the suspension gate), so hot paths issue no query to the payments schema. Flip the online-game hint wallet and the ad-banner suppression from the deprecated accounts.hint_balance / paid_account columns to the payments benefit (a hint balance no longer suppresses the banner — only a no-ads benefit does), and fold chip segments and benefits by origin on account merge, inside the merge tx. Add the GET/POST /api/v1/user/wallet edge chain (REST → Connect → FlatBuffers) plus its codec unit test; no wallet UI yet. Bring the frozen owner decisions log into the repo at docs/PAYMENTS_DECISIONS_ru.md (it was untracked under .vscode) and reference it from PLAN.md; record the read-cache design and the present-sources interface in PLAN.md and docs/PAYMENTS.md (+ RU mirror).
This commit is contained in:
@@ -2,9 +2,12 @@
|
||||
|
||||
Technical, step-by-step implementation of the monetization domain. Business mechanics and
|
||||
the rationale for every rule live in [`docs/PAYMENTS.md`](docs/PAYMENTS.md) (RU mirror
|
||||
[`docs/PAYMENTS_ru.md`](docs/PAYMENTS_ru.md)); this file is the *how*. Each stage is written
|
||||
to be self-sufficient: returning to it gives full context — goal, exact touch-points,
|
||||
tests, done-criteria, and current status — without re-deriving decisions.
|
||||
[`docs/PAYMENTS_ru.md`](docs/PAYMENTS_ru.md)); the frozen owner agreements they both derive
|
||||
from (the `D1`–`D41` decisions log) live in
|
||||
[`docs/PAYMENTS_DECISIONS_ru.md`](docs/PAYMENTS_DECISIONS_ru.md) — the authority when a rule
|
||||
is disputed. This file is the *how*. Each stage is written to be self-sufficient: returning
|
||||
to it gives full context — goal, exact touch-points, tests, done-criteria, and current
|
||||
status — without re-deriving decisions.
|
||||
|
||||
## How to use this file
|
||||
|
||||
@@ -28,7 +31,7 @@ tests, done-criteria, and current status — without re-deriving decisions.
|
||||
|-------|-------|---------|--------|
|
||||
| E0 | Payments data foundation | 1 | DONE |
|
||||
| E1 | Trusted platform signal | 1 | DONE |
|
||||
| E2 | Currency + benefit core | 1 | TODO |
|
||||
| E2 | Currency + benefit core | 1 | DONE |
|
||||
| E3 | Wallet UI | 1 | TODO |
|
||||
| E4 | Durability (PITR) | 2 | TODO |
|
||||
| E5 | Payment intake | 2 | TODO |
|
||||
@@ -82,9 +85,21 @@ Read once; individual stages assume it.
|
||||
`registerRoutes` gated `if s.payments != nil`, admin section in `registerConsole` gated
|
||||
`if s.payments != nil`.
|
||||
- Game/other domains depend on payments only through a **narrow interface** (e.g.
|
||||
`AdFree(ctx, account, platform) bool`, `SpendHint(ctx, account, platform) error`,
|
||||
`Balances(ctx, account, platform) …`). Keep the surface small; this is the seam that lets
|
||||
payments become a separate process later without touching callers.
|
||||
`AdFree(ctx, account, platform, present) bool`, `SpendHint(ctx, account, platform, present)
|
||||
error`, `Balances(ctx, account, platform, present) …`), where `present` is the set of
|
||||
identity sources the account currently holds (`vk`→vk, `telegram`→telegram, `email`→direct;
|
||||
`robot` ignored), computed by the caller from `account.Identities` — payments carries no
|
||||
cross-schema identity knowledge, so unlink/re-link availability (D14) falls out live. Keep
|
||||
the surface small; this is the seam that lets payments become a separate process later
|
||||
without touching callers.
|
||||
- **Read model.** Hot reads (`AdFree`/`HintsAvailable`/`Balances` and the gate) are served
|
||||
from an in-process, account-keyed, write-through cache inside the payments package
|
||||
(mirroring `account/suspension.go`), invalidated on every payments mutation
|
||||
(spend/grant/fund/refund/merge); the payments DB is touched only on a cache miss, so the
|
||||
steady-state hot path issues no query to the `payments` schema. The D39 materialized
|
||||
`balances`/`benefits` tables stay the in-transaction write target the cache fronts.
|
||||
Single-instance, matching the deployment (a multi-instance backend would need a shared
|
||||
cache).
|
||||
|
||||
### Migrations & codegen
|
||||
|
||||
@@ -283,33 +298,40 @@ Release 2). TG/direct subtype is not cryptographically trusted — E2 must keep
|
||||
|
||||
## E2 — Currency + benefit core
|
||||
|
||||
**Status:** TODO · **Release 1** · depends on: E0, E1 · mechanics: PAYMENTS §2–§6, §11.
|
||||
**Status:** DONE · **Release 1** · depends on: E0, E1 · mechanics: PAYMENTS §2–§6, §11.
|
||||
|
||||
**Goal.** The full internal money mechanic, exercisable end-to-end **without real money**
|
||||
via `admin_grant`: chip balances, spend→benefit atomically, the compliance gate by context,
|
||||
benefit application (no-ads stacking, hints per-origin), the legacy migration, and the
|
||||
`SpendHint` rewrite. This is the heart.
|
||||
|
||||
**Payments service (Go).**
|
||||
**Payments service (Go).** Every read/gate method also takes `present` (the account's live
|
||||
identity sources, see *Domain boundary*) and is served from the read cache.
|
||||
|
||||
- `Balances(ctx, account, platform)` → the segments spendable in the platform's context
|
||||
(VK→vk; TG→tg; direct→direct+vk+tg; VK-iOS→frozen/view; untrusted→none).
|
||||
- `Spend(ctx, account, platform, productID)` → gate-checked purchase of a value with chips:
|
||||
resolve spendable segments by context, draw by priority (direct→vk→tg on web), write a
|
||||
`spend` ledger row + decrement balance + apply benefit (extend `ads_paid_until[origin]`
|
||||
from `max(now,end)` / set `ads_forever` / add hints) **in one tx**, stamping `origin` =
|
||||
platform context. Refuse if untrusted (fail-closed) or funds insufficient.
|
||||
- `Grant(ctx, account, origin, atoms)` → `admin_grant` ledger row (price 0, concrete values
|
||||
only, never chips), same benefit application; origin chosen by the admin (E7 UI; here the
|
||||
service method + an internal/admin entrypoint).
|
||||
- `AdFree(ctx, account, platform)` / `HintsAvailable(ctx, account, platform)` /
|
||||
`SpendHint(ctx, account, platform)` → apply the one-directional origin rule: in context P,
|
||||
usable origins = {P} plus, when P is web/native, {vk,tg} (relaxation outward); in VK only
|
||||
vk; in TG only tg.
|
||||
- `Merge` / `Unlink` hooks: extend `accountmerge` to merge segments+benefits by origin
|
||||
(same-origin add, different coexist), and make segment availability follow identity
|
||||
presence (unlink → sleep, re-link → wake). Warn-before-unlink is a UI concern (surface the
|
||||
balance via the interface).
|
||||
- `Balances(ctx, account, platform, present)` → per-segment `{source, chips, spendable}` for
|
||||
the context (VK→vk spendable, direct/tg hidden; TG→tg; direct→direct+vk+tg by priority;
|
||||
VK-iOS→vk shown as a **frozen** number, none spendable; untrusted→view-only, none spendable).
|
||||
- `Spend(ctx, account, platform, present, productID)` → gate-checked purchase of a **value**
|
||||
(CHIP price, `method=NULL`; never a chip pack) with chips: resolve spendable segments by
|
||||
context, draw by priority direct→vk→tg, write a `spend` ledger row (+ a catalog **snapshot**
|
||||
of atoms+price) + decrement balance + apply benefit (extend `ads_paid_until[origin]` from
|
||||
`max(now,end)` / set `ads_forever` / add hints) **in one tx**, stamping `origin` = platform
|
||||
context. Refuse if untrusted (fail-closed) or funds insufficient.
|
||||
- `Grant(ctx, account, origin, atoms)` → a **0-price sale of a value**: an `admin_grant`
|
||||
ledger row (`chips_delta=0`, + snapshot), same benefit application; concrete values only,
|
||||
never chips (no chips move — it neither requires nor touches a balance). Origin chosen by
|
||||
the admin. In E2 this is the Go service method only, exercised by tests; the admin grant UI
|
||||
+ catalog editor are E7.
|
||||
- `AdFree(ctx, account, platform, present)` / `HintsAvailable(…)` / `SpendHint(…)` → apply
|
||||
the one-directional origin rule: in context P, usable origins = {P} plus, when P is
|
||||
web/native, {vk,tg} (relaxation outward); in VK only vk; in TG only tg. Hint consumption
|
||||
draws by the same priority direct→vk→tg.
|
||||
- `Merge` / `Unlink` hooks: extend `accountmerge` to merge segments **and** benefits by origin
|
||||
(same-origin add — chips sum, terms extend per origin; different origins coexist) in the
|
||||
caller's tx (`MergeTx`), and make **both** segment *and* benefit availability follow
|
||||
identity presence — unlink sleeps the balance *and* the benefit, re-link wakes them (D14),
|
||||
resolved live from `present`. Warn-before-unlink surfaces the balance via the interface; the
|
||||
warning UI itself is not E2.
|
||||
|
||||
**Backend wiring & migration.**
|
||||
|
||||
@@ -328,18 +350,22 @@ methods, Connect methods + transcode + FBS. Admin grant is internal/admin (E7 UI
|
||||
|
||||
**Tests.**
|
||||
|
||||
- unit (Go): gate-by-context matrix (every row of PAYMENTS §4); priority draw; no-ads
|
||||
stacking (`+=` from max(now,end)) + forever override; per-origin hint applicability;
|
||||
merge/unlink segment math.
|
||||
- unit (Go): gate-by-context matrix (every row of PAYMENTS §4, incl. VK-iOS frozen +
|
||||
untrusted); priority draw; no-ads stacking (`+=` from max(now,end)) + forever override;
|
||||
per-origin hint applicability + direct→vk→tg draw; merge/unlink segment+benefit math;
|
||||
catalog snapshotting; read-cache invalidation on each mutation.
|
||||
- integration: atomic spend (ledger+balance+benefit in one tx; failure rolls all back);
|
||||
admin_grant credits values not chips; legacy migration zeroes and flips reads; merge/
|
||||
unlink over Postgres.
|
||||
- UI: none new (E3 builds the screen); wallet DTO covered by codec unit tests.
|
||||
|
||||
**Done-criteria.** With chips seeded via `admin_grant`, a user can buy no-ads/hints; the gate
|
||||
blocks cross-context spend and cross-origin application; ads gate reads the new benefit;
|
||||
`SpendHint` uses segments; all layers green; **compliance regression** (a `direct` benefit
|
||||
never activates inside VK/TG) is a named test.
|
||||
**Done-criteria.** `admin_grant` applies no-ads/hints directly (a 0-price sale of a value —
|
||||
no chips involved); the player chip-spend path (`Spend`) is proven by unit/integration tests
|
||||
(balance seeded in-test, since a legitimate chip source arrives only with Release 2); the gate
|
||||
blocks cross-context spend and cross-origin application; the ads gate reads the new benefit;
|
||||
`SpendHint` uses segments; the hot read paths hit the read cache, not the `payments` schema;
|
||||
all layers green; **compliance regression** (a `direct` benefit never activates inside VK/TG)
|
||||
is a named test.
|
||||
|
||||
**Notes/risks.** This is the high-blast-radius core (money semantics + a live path
|
||||
`SpendHint`/`ads.Eligible`). Minimize surface, keep the interface narrow, no mixed-in
|
||||
|
||||
Reference in New Issue
Block a user