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:
@@ -17,6 +17,7 @@ import (
|
||||
"scrabble/backend/internal/engine"
|
||||
"scrabble/backend/internal/game"
|
||||
"scrabble/backend/internal/lobby"
|
||||
"scrabble/backend/internal/payments"
|
||||
"scrabble/backend/internal/robot"
|
||||
"scrabble/backend/internal/social"
|
||||
)
|
||||
@@ -42,9 +43,42 @@ func newGameService() *game.Service {
|
||||
zap.NewNop(),
|
||||
)
|
||||
svc.SetFirstMoveEntropy(seatZeroFirstMove)
|
||||
svc.SetHintWallet(newPaymentsService())
|
||||
return svc
|
||||
}
|
||||
|
||||
// newPaymentsService builds a payments service over the shared pool (its own in-process read
|
||||
// cache), for wiring the game hint wallet and the account-merge fold in the integration suite.
|
||||
func newPaymentsService() *payments.Service {
|
||||
return payments.NewService(payments.NewStore(testDB))
|
||||
}
|
||||
|
||||
// seedBenefit writes a payments benefit row for an account+origin directly (bypassing the
|
||||
// service), used to stand up wallet state a test then spends or merges. A non-zero hints count
|
||||
// and/or the forever flag are set; a caller wanting a no-ads term sets it via seedBenefitUntil.
|
||||
func seedBenefit(t *testing.T, id uuid.UUID, origin string, hints int, forever bool) {
|
||||
t.Helper()
|
||||
if _, err := testDB.ExecContext(context.Background(),
|
||||
`INSERT INTO payments.benefits (account_id, origin, hints, ads_forever) VALUES ($1,$2,$3,$4)
|
||||
ON CONFLICT (account_id, origin) DO UPDATE SET hints = EXCLUDED.hints, ads_forever = EXCLUDED.ads_forever`,
|
||||
id, origin, hints, forever); err != nil {
|
||||
t.Fatalf("seed benefit: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// readBenefit reads an account's benefit row for an origin: the hint count and the forever flag
|
||||
// (both zero/false when the row is absent).
|
||||
func readBenefit(t *testing.T, id uuid.UUID, origin string) (hints int, forever bool) {
|
||||
t.Helper()
|
||||
err := testDB.QueryRowContext(context.Background(),
|
||||
`SELECT hints, ads_forever FROM payments.benefits WHERE account_id=$1 AND origin=$2`, id, origin).
|
||||
Scan(&hints, &forever)
|
||||
if err != nil && !errors.Is(err, sql.ErrNoRows) {
|
||||
t.Fatalf("read benefit: %v", err)
|
||||
}
|
||||
return hints, forever
|
||||
}
|
||||
|
||||
// seatZeroFirstMove is a first-move-draw entropy factory that always elects the first
|
||||
// listed account as the leader, keeping the integration suite's turn order stable
|
||||
// despite the real draw's randomness: the first contender draws a blank — the best
|
||||
|
||||
Reference in New Issue
Block a user