Ruby Wallet Split Structure
Status
Approved
Date
2026-04-23
Owners
- Platform Backend
- Wallet Domain
Affected Services
wallet_servicerolling_servicepromotion_servicegame_servicegatewayadmin_service
Related ADRs
docs/adr/ADR-005-wallet-topology-bucket-ledger-model.md
Related Stable Docs
docs/services/wallet-service.mddocs/architecture/domain-ownership.mddocs/architecture/data-ownership.mddocs/architecture/event-catalog.md
Related Runbooks
docs/runbooks/wallet/ruby-wallet-topology-rollout.md
Goal
Replace the current legacy wallet shape with a clean Ruby Wallet split
structure before production launch. The new model must support configurable
wallet topology and independently configured sports and casino wallet policies
while keeping wallet_service as the only money writer.
This spec intentionally does not preserve legacy domain fields such as
balance, cash, rebate, coupon, sport_bonus, or live_slots_bonus.
External response compatibility can be handled separately if a caller still
needs a projection, but the domain source of truth must be the split wallet
model described here.
Scope
In scope:
- Configurable wallet topology with versioned bucket types and wallet groups.
- Sports wallet group and casino wallet group.
- Shared withdrawable wallet.
- Shared points wallet.
- Coupon grants with sports, casino, provider-only, and all-game scopes.
- Configurable bet funding mode for sports, live, and slots.
- Combined-balance betting and wallet-selection betting.
- Rolling attribution by provider type and source bucket type.
- Normal-wallet transfer between sports normal and casino normal.
- Points transfer into sports normal or casino normal.
- Promotion settlement crediting rewards into points.
- Admin configuration contracts for all policy knobs shown in the wallet UI.
Out of scope:
- Preserving old wallet columns as canonical fields.
- Adding compatibility aliases as the primary design.
- Changing provider settlement math outside the wallet funding and rolling attribution boundary.
- BO UI implementation details beyond the required API and policy fields.
Current State Findings
The current servers_v2 wallet model is not compatible with this structure:
- Wallet enums still model the legacy buckets: balance, sports bonus, live/slots bonus, coupon, rebate, and cash.
- Player balances are stored as direct player columns instead of wallet-owned bucket rows.
- Bet authorization uses a fixed priority of coupon, bonus, rebate, balance, and cash.
- Bet settlement currently pays winnings to cash by default.
- Promotion rewards credit rebate/cashback/lossback into a rebate-style wallet, not into a points wallet.
- Coupon money is stored as a shared coupon balance even though coupon grants have different usage scopes.
- Transfer endpoints allow legacy cash or rebate transfer flows that conflict with the split structure.
Because the system has not launched, the required direction is a clean rewrite of the wallet domain model instead of compatibility layering.
Domain Terms
Wallet Topology
Wallet topology is the active set of wallet groups, bucket types, display groups, transfer edges, and provider-type mappings. Ruby Wallet split is the first required topology, not a hard-coded database shape.
Rules:
- Wallet topology is versioned.
- Only one topology version is active for new transactions at a time.
- Existing transactions keep the topology and policy snapshot used when they were authorized or created.
- A topology can add bucket types without changing the
wallet_buckettable shape. - A topology can disable bucket types only after all active balances, coupon grants, rollings, unsettled bets, and transfers for those bucket types are resolved or migrated.
- A topology cannot delete historical bucket type definitions used by ledger rows.
The first active split topology is RUBY_SPLIT_V1. The supported unified
topology is RUBY_UNIFIED_V1, which collapses sports/live/slots playable
normal and bonus money into one unified group while keeping withdrawable and
points in the shared group.
Provider Type
provider_type is the deciding source for wallet group and reward category:
RUBY_SPLIT_V1:
| Provider type | Wallet group | Reward category |
|---|---|---|
sports | sports | sports rebate, sports lossback, sports cashback |
live | casino | casino rebate, casino cashback |
slots | casino | casino rebate, casino cashback |
RUBY_UNIFIED_V1:
| Provider type | Wallet group | Reward category |
|---|---|---|
sports | unified | sports rebate, sports lossback, sports cashback |
live | unified | casino rebate, casino cashback |
slots | unified | casino rebate, casino cashback |
The mapping must be configurable only through controlled admin policy, not by caller-provided free text. Wallet and promotion services must reject unknown provider types.
Wallet Group
Wallet groups are configured by topology. RUBY_SPLIT_V1 starts with:
| Group | Meaning |
|---|---|
sports | Sports-only bettable wallets and sports rolling policy. |
casino | Live and slots bettable wallets and casino rolling policy. |
shared | Wallets shared across sports and casino flows. |
Future topologies may split live and slots into separate groups, add a
provider-specific group, or collapse groups, as long as every money movement
stores the active topology version and policy snapshot.
Wallet Bucket Type
Wallet bucket types are configured by topology and persisted as rows in
wallet_bucket_type. They are not code-only enum values.
Each bucket type has:
codewallet_grouprolebettablewithdrawabletransferabledisplay_orderstatus
Valid built-in roles:
| Role | Meaning |
|---|---|
NORMAL | Deposit or points-converted playable principal. |
BONUS | Bonus campaign principal plus bonus while rolling is active. |
WITHDRAWABLE | Confirmed withdrawable balance. |
POINTS | Promotion reward accumulation before conversion. |
RUBY_SPLIT_V1 starts with these bucket types:
| Bucket code | Group | Bettable | Withdrawable | Purpose |
|---|---|---|---|---|
SPORTS_NORMAL | sports | yes | no | Deposits without sports bonus. |
SPORTS_BONUS | sports | yes | no | Sports deposit principal plus bonus while rolling is active. |
CASINO_NORMAL | casino | yes | no | Deposits without casino bonus. |
CASINO_BONUS | casino | yes | no | Casino deposit principal plus bonus while rolling is active. |
WITHDRAWABLE | shared | yes | yes | Completed, confirmed withdrawable balance. |
POINTS | shared | no | no | Rebate, cashback, and lossback accumulation. |
RUBY_UNIFIED_V1 starts with these bucket types:
| Bucket code | Group | Bettable | Withdrawable | Purpose |
|---|---|---|---|---|
UNIFIED_NORMAL | unified | yes | no | Deposits and points-converted principal for all provider types. |
UNIFIED_BONUS | unified | yes | no | Bonus campaign principal plus bonus while rolling is active. |
WITHDRAWABLE | shared | yes | yes | Completed, confirmed withdrawable balance. |
POINTS | shared | no | no | Rebate, cashback, and lossback accumulation. |
Coupon value must not be stored as one generic bucket because every coupon can
carry distinct scope, provider restrictions, max payout, expiry, and rolling
state. Coupon money is represented by wallet_coupon_grant records and
appears in snapshots as aggregated coupon groups.
Wallet Structure Requirement
When RUBY_SPLIT_V1 is active, the player wallet view must be derived from:
- sports normal bucket
- sports bonus bucket
- casino normal bucket
- casino bonus bucket
- withdrawable bucket
- points bucket
- active coupon grants grouped by scope
The member UI may show a single total balance, but internal authorization, settlement, rolling, and transfer logic must operate on the split wallets.
Future topology versions must provide their own display mapping and must not
require changing the canonical wallet_bucket and wallet_ledger table shape.
When RUBY_UNIFIED_V1 is active, legacy API requests that still name
SPORTS_NORMAL, CASINO_NORMAL, SPORTS_BONUS, or CASINO_BONUS must be
resolved server-side to the configured NORMAL or BONUS bucket in the active
topology. This preserves old callers during cutover while keeping topology
documents authoritative.
Configurable Policies
Screenshot-to-Policy Mapping
The wallet configuration UI in the product screenshots maps to these backend policy documents:
| UI area | Backend policy |
|---|---|
| Wallet structure split checkbox | wallet_structure_policy.structure_mode = SPLIT. |
| Sports normal and bonus wallet balances | wallet_bucket rows for SPORTS_NORMAL and SPORTS_BONUS. |
| Casino normal and bonus wallet balances | wallet_bucket rows for CASINO_NORMAL and CASINO_BONUS. |
| Withdrawable wallet | wallet_bucket.WITHDRAWABLE. |
| Points wallet | wallet_bucket.POINTS. |
| Sports-only, casino-only, provider-only, all-game coupons | wallet_coupon_grant.scope and wallet_coupon_grant.provider_ids. |
| Sports game betting type | bet_funding_policy where provider_type = sports. |
| Casino game betting type | bet_funding_policy where provider_type in (live, slots). |
| Combined balance radio | funding_mode = COMBINED_BALANCE. |
| Wallet selection radio | funding_mode = WALLET_SELECTION. |
| Include coupon checkbox | include_coupons_in_combined. |
| Withdrawable-funded bets do not accumulate rolling | withdrawable_betting_policy = NO_ROLLING. |
| Withdrawable-funded bets accumulate to matching active rolling | withdrawable_betting_policy = AUTO_BY_PROVIDER_TYPE. |
| Accumulate to normal wallet | withdrawable_betting_policy = TO_NORMAL. |
| Accumulate to bonus wallet | withdrawable_betting_policy = TO_BONUS. |
The withdrawable sub-options must be available for both combined-balance mode and wallet-selection mode because a bet can be funded by withdrawable in either mode.
Wallet Structure Policy
| Field | Type | Required | Description |
|---|---|---|---|
structure_mode | enum | yes | SPLIT for this spec. |
active_topology_code | string | yes | Initial value: RUBY_SPLIT_V1. |
active_topology_version | int | yes | Incremented for every topology change. |
sports_provider_types | list | yes | Provider types routed to sports wallet group. |
casino_provider_types | list | yes | Provider types routed to casino wallet group. |
display_mode | enum | yes | MERGED_TOTAL, GROUPED, or DETAILED. |
Bet Funding Policy
Sports, live, and slots must each resolve to a wallet group and then read the group's funding policy. Live and slots may share the same casino policy, but the configuration must allow them to be explicitly listed so future separation does not require rewriting wallet code.
| Field | Type | Required | Description |
|---|---|---|---|
provider_type | enum | yes | sports, live, or slots. |
wallet_group | enum | yes | sports or casino. |
funding_mode | enum | yes | COMBINED_BALANCE or WALLET_SELECTION. |
include_coupons_in_combined | bool | yes | Whether eligible coupons are auto-used in combined mode. |
deduction_order | list | yes | Ordered funding sources. Default: coupon, bonus, normal, withdrawable. |
proportional_rolling | bool | yes | Whether rolling is accumulated by actual deducted amount per source. |
insufficient_funds_behavior | enum | yes | REJECT only for wallet selection; combined mode may aggregate eligible sources. |
Combined-Balance Mode
For sports:
- eligible sports coupon grants
- sports bonus
- sports normal
- withdrawable
For casino:
- eligible casino coupon grants
- casino bonus
- casino normal
- withdrawable
Default deduction order:
- Eligible coupon grants.
- Group bonus bucket.
- Group normal bucket.
- Shared withdrawable bucket.
If a bet consumes more than one source, wallet must persist the exact funding breakdown and rolling attribution per source. Settlement and rollback must use that persisted breakdown, not recompute from current balances.
Wallet-Selection Mode
The caller must provide one selected funding source before bet authorization:
- Sports game allowed sources: sports coupon grant, sports bonus, sports normal, withdrawable.
- Casino game allowed sources: casino coupon grant, casino bonus, casino normal, withdrawable.
Rules:
- A single bet may debit only the selected source.
- If the selected source cannot cover the amount, authorization fails.
- Opposite-group wallets are never selectable.
- Selected coupon grants must pass scope, provider, expiry, and status checks.
- Rolling progress is attributed only to the selected source.
Coupon Policy
Coupon grants support these scopes:
| Scope | Eligibility |
|---|---|
SPORTS_ONLY | Only sports provider types. |
CASINO_ONLY | Live and slots provider types. |
PROVIDER_ONLY | Only configured provider IDs. |
ALL_GAMES | Sports, live, and slots unless provider is explicitly excluded. |
Coupon rules:
- Coupons cannot be transferred to bonus, normal, withdrawable, or points.
- Coupons may be consumed only through eligible betting.
- Same-scope coupon grants may be appended from the player perspective, but wallet must preserve grant-level expiry, provider restriction, max payout, and ledger trace.
- Rolling target for appended coupons is merged at the rolling-policy level while the source grant references remain auditable.
- Coupon payout caps and effective odds constraints are evaluated from the coupon grant snapshot used at bet authorization time.
Bonus Policy
Rules:
- Sports bonus and casino bonus may be active at the same time.
- A player cannot receive another bonus in the same group while that group's bonus rolling is in progress unless the policy explicitly allows stacking.
- Bonus principal and bonus amount belong to the same group bonus bucket while rolling is active.
- On rolling completion, the configured releasable amount moves to withdrawable.
- On rolling cancellation or expiry, wallet applies the policy snapshot stored on the rolling record.
Normal Wallet Policy
Sports normal and casino normal are separate buckets with independent policy:
| Field | Description |
|---|---|
default_rolling_multiplier | Rolling generated by normal deposits or points transfers. |
win_destination_before_rolling_complete | SAME_NORMAL, WITHDRAWABLE, or policy-based. |
win_destination_after_rolling_complete | Usually WITHDRAWABLE. |
valid_odds_rule | Sports status or casino game-result rule used for valid betting. |
Example default:
- Sports normal can use a zero rolling multiplier and pay winnings directly to withdrawable.
- Casino normal can use a one-times rolling multiplier and keep winnings in casino normal until rolling completes.
Withdrawable Betting Policy
Withdrawable is shared and bettable, but its rolling attribution is configured:
| Option | Behavior |
|---|---|
NO_ROLLING | Withdrawable bet amount does not increase any rolling. |
AUTO_BY_PROVIDER_TYPE | Sports bets apply to sports active rolling; live/slots apply to casino active rolling. |
TO_NORMAL | Sports bets apply to sports normal rolling; casino bets apply to casino normal rolling. |
TO_BONUS | Sports bets apply to sports bonus rolling; casino bets apply to casino bonus rolling. |
Winnings from withdrawable-funded bets default back to withdrawable. If a bet
has mixed funding, settlement splits payout by the authorization funding ratio:
the withdrawable-funded share returns to WITHDRAWABLE, while the
non-withdrawable share follows the provider group's configured win destination.
Normal-Wallet Transfer Policy
Allowed transfers:
- sports normal to casino normal
- casino normal to sports normal
Forbidden transfers:
- bonus to normal
- coupon to any wallet
- withdrawable to normal
- points to bonus
- points to withdrawable
Policy fields:
| Field | Description |
|---|---|
enabled | Enables or disables normal-wallet transfer. |
minimum_amount | Smallest allowed transfer. |
amount_unit | Transfer amount must be divisible by this unit. |
block_when_unsettled_bets_exist | Blocks transfer while the player has unsettled bets. |
Rolling inheritance:
transfer_ratio = transfer_amount / source_balance_before_transfertarget_inherited_rolling = source_remaining_rolling * transfer_ratiosource_remaining_rolling_after = source_remaining_rolling * (1 - transfer_ratio)
The inheritance calculation must be transactionally tied to the wallet transfer ledger entries.
Compatibility:
- Existing player calls to
/finance/transferWithdrawal/sportare interpreted as a full transfer fromCASINO_NORMALtoSPORTS_NORMAL. - Existing player calls to
/finance/transferWithdrawal/live&slotsor/finance/transferWithdrawal/live_slotsare interpreted as a full transfer fromSPORTS_NORMALtoCASINO_NORMAL. - These aliases must never debit
WITHDRAWABLEor credit bonus buckets.
Points Policy
Points bucket receives:
- rebate
- cashback
- lossback
Points cannot be used for betting or withdrawal. A player must transfer points to sports normal or casino normal first.
Policy fields:
| Field | Description |
|---|---|
minimum_transfer_amount | Minimum points transfer. |
amount_unit | Points transfer unit. |
target_bucket_type_codes | Target NORMAL bucket codes, for example SPORTS_NORMAL, CASINO_NORMAL, or UNIFIED_NORMAL. |
rolling_multiplier | Rolling generated on the target normal wallet. |
Admin Policy API Surface
admin_service should expose BO-facing routes and forward policy writes to
wallet_service; wallet_service remains the policy owner and validator.
The paths below are relative to the admin API prefix.
Implemented route families:
GET /wallet/topology/activeGET /wallet/topologies/{topology_code}?version={version}PUT /wallet/topologies/{topology_code}PUT /wallet/topologies/{topology_code}/activatePUT /wallet/policies/{policy_key}PUT /wallet/policies/{policy_key}/activate
The BO UI may render structure, bet-funding, normal-wallets, withdrawable-betting, transfers, and points as separate editing panels, but the current production API persists them as versioned topology/policy documents through the generic routes above. Any future fine-grained policy routes must compose back into the same wallet-owned document validation and activation flow.
For split/unified or other topology switches, BO must call
PUT /wallet/topologies/{topology_code}/activate with the target topology
document plus matching policy_key and policy_document. Wallet service
activates the topology and policy atomically so there is never a window where
the active topology lacks a matching active policy. PUT /wallet/policies/{policy_key}/activate is reserved for replacing the policy
on the currently active topology.
Validation requirements:
- topology changes cannot remove bucket types referenced by ledger rows.
- topology activation must define every bucket type referenced by active policies.
- topology activation must reject same-code semantic drift for live buckets:
role,wallet_group, group shared-ness,bettable,withdrawable,transferable, andstatusare immutable while that bucket code has live balance, unsettled bets, or pending rolling. - topology activation must define a provider-type mapping for every active provider type.
- policy activation must validate all bucket references in settlement destinations, points targets, selected sources, transfer edges, deduction order, and withdrawable rolling targets.
- sports provider policy cannot target casino wallets.
- live and slots policies cannot target sports wallets unless product changes the provider mapping in the structure policy.
- deduction order cannot contain a wallet source that is unavailable for the provider type.
- wallet-selection mode must define the allowed selected wallet sources.
- active policy changes create a new immutable policy version.
- admin audit logs must include operator, old policy version, new policy version, and normalized diff.
Data Model
wallet_account
One row per player wallet account.
Required fields:
idplayer_idaccountcurrencystatuscreated_atupdated_at
wallet_topology
Versioned wallet shape configuration.
Required fields:
idcodeversionstatusdocumentcreated_bycreated_atactivated_at
Rules:
- Only one topology version may be active.
- Topology documents must pass schema validation before activation.
- Ledger, authorization, transfer, and rolling records store the topology code and version used at execution time.
wallet_bucket_type
Topology-owned bucket type definitions.
Required fields:
idtopology_codetopology_versioncodewallet_grouprolebettablewithdrawabletransferabledisplay_orderstatusmetadatacreated_at
Unique key:
(topology_code, topology_version, code)
wallet_bucket
One row per wallet bucket.
Required fields:
idwallet_account_idplayer_idbucket_type_codetopology_codecreated_topology_versionwallet_groupbalanceversioncreated_atupdated_at
Unique key:
(player_id, topology_code, bucket_type_code)
Rules:
wallet_bucketstores the current balance for a bucket code and must not create a second current-balance row just because topology version changes.- Transactional tables such as ledger, authorization, transfer, and rolling store the execution topology version for audit and deterministic replay.
wallet_coupon_grant
Wallet-owned coupon money and constraints.
Required fields:
idplayer_idpromotion_coupon_idscopeprovider_idsoriginal_amountremaining_amountmax_payoutrolling_multipliereffective_odds_ruletopology_codetopology_versionpolicy_versionpolicy_snapshotstatusexpires_atcreated_atupdated_at
wallet_ledger
Append-only money movement record.
Required fields:
idplayer_idbucket_type_codecoupon_grant_iddirectionamountbefore_balanceafter_balancechange_typeprovider_typeprovider_idbet_idrolling_idtransfer_idpromotion_reference_idrequest_idtopology_codetopology_versionpolicy_versionpolicy_snapshotcreated_at
Rules:
- Every balance mutation must have a ledger row.
- Ledger rows are immutable after commit.
- Bet settlement and rollback must reference the authorization ledger or stored funding breakdown.
wallet_policy
Versioned policy document used by wallet authorization and transfers.
Required fields:
idpolicy_keyversiontopology_codetopology_versionstatusdocumentcreated_bycreated_at
Wallet must store policy_version and policy_snapshot on transactional
records that need deterministic rollback.
Policy document rules:
- Policy documents must be validated against a versioned schema before activation.
- Policy documents must be declarative data, not executable scripts.
- Bucket type references must resolve against the active topology version.
- Provider type references must resolve against the active provider-type registry.
- Policy activation must fail if a deduction order, transfer edge, points target, or settlement destination references a disabled bucket type.
wallet_bet_authorization
Stores the immutable funding decision for every accepted bet.
Required fields:
idrequest_idplayer_idbet_idprovider_typeprovider_idgame_idamountfunding_modeselected_wallet_sourcefunding_breakdowntopology_codetopology_versionpolicy_versionpolicy_snapshotstatuscreated_atsettled_atrolled_back_at
Rules:
funding_breakdownis the source of truth for settlement and rollback.- Settlement must fail loudly if no accepted authorization exists for the bet.
- Rollback must restore the exact bucket and coupon grant sources in the authorization record.
- Duplicate authorization request IDs must be idempotent only when the payload hash matches.
wallet_transfer
Records normal-wallet and points-transfer operations.
Required fields:
idplayer_idtransfer_typesource_bucket_type_codetarget_bucket_type_codeamountsource_rolling_beforesource_rolling_aftertarget_rolling_addedrequest_idtopology_codetopology_versionpolicy_versioncreated_at
Service Ownership
wallet_service
Owns:
- wallet account and bucket state
- coupon grant money state
- wallet ledger
- bet funding policy enforcement
- deposit credit target
- settlement destination
- rollback and idempotency
- points transfer
- normal-wallet transfer
- admin wallet policy storage
rolling_service
Owns:
- rolling lifecycle
- rolling progress
- rolling completion, cancellation, expiry, and force-completion state
It must not decide money movement destinations. It receives source bucket type, wallet group, provider type, topology version, and policy snapshot from wallet or admin policy.
promotion_service
Owns:
- reward eligibility
- rebate, cashback, and lossback calculation
- coupon template and campaign definition
- coupon grant orchestration
It must credit rewards to POINTS through wallet and must credit coupon grants
through wallet coupon grant commands.
game_service
Owns:
- provider callback parsing
- bet lifecycle normalization
- provider ID and provider type resolution
It must pass provider_type, provider_id, bet_id, and game identifiers to
wallet commands.
admin_service
Owns:
- BO-facing APIs for wallet policy CRUD
- policy validation before activation
- audit logging for policy changes
It does not write money.
Command Contracts
Authorize Bet
Request must include:
request_idplayer_idbet_idamountprovider_typeprovider_idgame_idselected_wallet_sourcewhen the active policy requiresWALLET_SELECTION
Callers must not provide authoritative policy fields such as funding_mode,
deduction_order, or wallet_group. Wallet resolves those values from the
active topology and policy version.
Response must include:
acceptedfunding_breakdownbalance_snapshottopology_codetopology_versionpolicy_version
funding_breakdown stores one row per debited bucket or coupon grant.
Settle Bet
Request must include:
request_idplayer_idbet_idwin_amountvalid_bet_amountprovider_typeprovider_id
Wallet must load the stored authorization breakdown and resolve win destination by source bucket type and policy snapshot.
Roll Back Bet
Rollback must restore the original funding breakdown. It must not recompute eligible wallets from current balances or current policy.
Credit Deposit
Admin or recon approval must specify:
- target
bucket_type_code - bonus amount if any
- rolling multiplier and policy snapshot
The target bucket type must be allowed by the active topology and deposit policy.
Credit Promotion
Promotion credit commands:
- rebate, cashback, and lossback credit
POINTS - coupon issue creates wallet coupon grants
- event reward must explicitly declare target bucket or be rejected
Transfer Normal Wallet
Request:
- source bucket type
- target bucket type
- amount
Wallet must enforce transfer policy and rolling inheritance. In
RUBY_SPLIT_V1, the only normal-wallet transfer edges are sports normal to
casino normal and casino normal to sports normal. In RUBY_UNIFIED_V1,
normal-wallet transfer is disabled because all provider types already use
UNIFIED_NORMAL; legacy transfer aliases should return a no-op response.
Transfer Points
Request:
- source: points
- target bucket type
- amount
Wallet must enforce points policy and create target rolling if configured. In
RUBY_SPLIT_V1, allowed targets are sports normal and casino normal. In
RUBY_UNIFIED_V1, the allowed target is UNIFIED_NORMAL; legacy target names
such as SPORTS_NORMAL and CASINO_NORMAL are mapped to UNIFIED_NORMAL by
the wallet service.
Withdrawable Debit And Refund
Request:
- player id
- amount
Wallet must debit the shared WITHDRAWABLE bucket when a withdrawal order is
created. If review or payment later declines, wallet must refund the same
WITHDRAWABLE bucket through an idempotent refund command and then update the
legacy player_withdraw.status record as a compatibility side effect.
Admin Adjustment
Request:
- player id
- bucket type code
- signed amount
- operator and note
Admin adjustments must target an explicit bucket type code. Legacy wallet-type
integers may be mapped only by compatibility adapters at the edge. The money
write itself must be represented as a bucket credit or debit plus a
wallet_ledger row with change_type = BO_ADJUST.
Balance Snapshot
The canonical snapshot is structured, not legacy flat fields:
total_display_balancetopology_codetopology_versiongroups.sports.normalgroups.sports.bonusgroups.sports.couponsgroups.casino.normalgroups.casino.bonusgroups.casino.couponsshared.withdrawableshared.pointscoupon_grants
total_display_balance is a presentation value only and must not be used as a
source for authorization.
Observability
Required logs:
- bet authorization decision with policy version and funding breakdown
- settlement destination decision
- rollback source breakdown
- coupon grant eligibility rejection reason
- points transfer and normal-wallet transfer policy decisions
Required metrics:
- wallet authorization success and failure by provider type
- insufficient funds by wallet group
- coupon rejection by reason
- transfer rejection by reason
- ledger write latency
- rolling creation failures after wallet credit
Required alerts:
- wallet ledger write failure
- wallet bucket negative-balance prevention triggered
- idempotency payload mismatch
- rolling creation repeatedly failing after a successful wallet credit
- settlement cannot find authorization breakdown
TDD Requirements
Pure policy tests:
- sports combined-balance deduction order.
- casino combined-balance deduction order.
- include-coupon off skips coupons.
- wallet-selection rejects opposite-group wallets.
- wallet-selection rejects insufficient selected source balance.
- coupon scope rejects wrong provider type.
- provider-only coupon rejects non-listed provider IDs.
- withdrawable policy
NO_ROLLING. - withdrawable policy
AUTO_BY_PROVIDER_TYPE. - points transfer policy.
- normal-wallet transfer rolling inheritance formula.
API tests:
- authorize bet persists exact funding breakdown.
- settle bet uses stored funding breakdown.
- rollback restores original sources.
- duplicate request ID is idempotent.
- idempotency payload mismatch is rejected.
- deposit approval requires explicit target bucket type.
- promotion reward credits points.
- coupon issue creates coupon grant instead of generic coupon balance.
Cross-service tests:
- game callback to wallet authorize and settle for sports.
- game callback to wallet authorize and settle for live.
- promotion settlement to points to target normal transfer to rolling.
- coupon issue to bet use to rolling completion.
- normal-wallet transfer with inherited rolling.
Docker acceptance tests:
- run the full local stack.
- initialize one player with split wallets.
- execute sports and casino end-to-end flows.
- verify ledger, bucket balances, rolling records, promotion records, and admin policy reads.
Acceptance Criteria
- ADR-005 is accepted as the durable wallet money model.
- No new code treats legacy flat wallet fields as the canonical source of truth.
- Wallet bucket shape can change through topology configuration without
changing the
wallet_bucketorwallet_ledgertable structure. - All wallet mutations are represented in
wallet_ledger. - Sports, live, and slots funding policies are configurable.
- Sports and casino balances cannot be mixed except through the allowed normal wallet transfer.
- Points cannot be bet or withdrawn directly.
- Coupon grant constraints are enforced at authorization time.
- Bet settlement and rollback are deterministic from stored breakdown and policy snapshot.
wallet_serviceremains the only money writer.