跳到主要内容

Ruby Wallet Split Structure

Status

Approved

Date

2026-04-23

Owners

  • Platform Backend
  • Wallet Domain

Affected Services

  • wallet_service
  • rolling_service
  • promotion_service
  • game_service
  • gateway
  • admin_service
  • docs/adr/ADR-005-wallet-topology-bucket-ledger-model.md
  • docs/services/wallet-service.md
  • docs/architecture/domain-ownership.md
  • docs/architecture/data-ownership.md
  • docs/architecture/event-catalog.md
  • 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_bucket table 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 typeWallet groupReward category
sportssportssports rebate, sports lossback, sports cashback
livecasinocasino rebate, casino cashback
slotscasinocasino rebate, casino cashback

RUBY_UNIFIED_V1:

Provider typeWallet groupReward category
sportsunifiedsports rebate, sports lossback, sports cashback
liveunifiedcasino rebate, casino cashback
slotsunifiedcasino 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:

GroupMeaning
sportsSports-only bettable wallets and sports rolling policy.
casinoLive and slots bettable wallets and casino rolling policy.
sharedWallets 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:

  • code
  • wallet_group
  • role
  • bettable
  • withdrawable
  • transferable
  • display_order
  • status

Valid built-in roles:

RoleMeaning
NORMALDeposit or points-converted playable principal.
BONUSBonus campaign principal plus bonus while rolling is active.
WITHDRAWABLEConfirmed withdrawable balance.
POINTSPromotion reward accumulation before conversion.

RUBY_SPLIT_V1 starts with these bucket types:

Bucket codeGroupBettableWithdrawablePurpose
SPORTS_NORMALsportsyesnoDeposits without sports bonus.
SPORTS_BONUSsportsyesnoSports deposit principal plus bonus while rolling is active.
CASINO_NORMALcasinoyesnoDeposits without casino bonus.
CASINO_BONUScasinoyesnoCasino deposit principal plus bonus while rolling is active.
WITHDRAWABLEsharedyesyesCompleted, confirmed withdrawable balance.
POINTSsharednonoRebate, cashback, and lossback accumulation.

RUBY_UNIFIED_V1 starts with these bucket types:

Bucket codeGroupBettableWithdrawablePurpose
UNIFIED_NORMALunifiedyesnoDeposits and points-converted principal for all provider types.
UNIFIED_BONUSunifiedyesnoBonus campaign principal plus bonus while rolling is active.
WITHDRAWABLEsharedyesyesCompleted, confirmed withdrawable balance.
POINTSsharednonoRebate, 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 areaBackend policy
Wallet structure split checkboxwallet_structure_policy.structure_mode = SPLIT.
Sports normal and bonus wallet balanceswallet_bucket rows for SPORTS_NORMAL and SPORTS_BONUS.
Casino normal and bonus wallet balanceswallet_bucket rows for CASINO_NORMAL and CASINO_BONUS.
Withdrawable walletwallet_bucket.WITHDRAWABLE.
Points walletwallet_bucket.POINTS.
Sports-only, casino-only, provider-only, all-game couponswallet_coupon_grant.scope and wallet_coupon_grant.provider_ids.
Sports game betting typebet_funding_policy where provider_type = sports.
Casino game betting typebet_funding_policy where provider_type in (live, slots).
Combined balance radiofunding_mode = COMBINED_BALANCE.
Wallet selection radiofunding_mode = WALLET_SELECTION.
Include coupon checkboxinclude_coupons_in_combined.
Withdrawable-funded bets do not accumulate rollingwithdrawable_betting_policy = NO_ROLLING.
Withdrawable-funded bets accumulate to matching active rollingwithdrawable_betting_policy = AUTO_BY_PROVIDER_TYPE.
Accumulate to normal walletwithdrawable_betting_policy = TO_NORMAL.
Accumulate to bonus walletwithdrawable_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

FieldTypeRequiredDescription
structure_modeenumyesSPLIT for this spec.
active_topology_codestringyesInitial value: RUBY_SPLIT_V1.
active_topology_versionintyesIncremented for every topology change.
sports_provider_typeslistyesProvider types routed to sports wallet group.
casino_provider_typeslistyesProvider types routed to casino wallet group.
display_modeenumyesMERGED_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.

FieldTypeRequiredDescription
provider_typeenumyessports, live, or slots.
wallet_groupenumyessports or casino.
funding_modeenumyesCOMBINED_BALANCE or WALLET_SELECTION.
include_coupons_in_combinedboolyesWhether eligible coupons are auto-used in combined mode.
deduction_orderlistyesOrdered funding sources. Default: coupon, bonus, normal, withdrawable.
proportional_rollingboolyesWhether rolling is accumulated by actual deducted amount per source.
insufficient_funds_behaviorenumyesREJECT 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:

  1. Eligible coupon grants.
  2. Group bonus bucket.
  3. Group normal bucket.
  4. 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:

ScopeEligibility
SPORTS_ONLYOnly sports provider types.
CASINO_ONLYLive and slots provider types.
PROVIDER_ONLYOnly configured provider IDs.
ALL_GAMESSports, 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:

FieldDescription
default_rolling_multiplierRolling generated by normal deposits or points transfers.
win_destination_before_rolling_completeSAME_NORMAL, WITHDRAWABLE, or policy-based.
win_destination_after_rolling_completeUsually WITHDRAWABLE.
valid_odds_ruleSports 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:

OptionBehavior
NO_ROLLINGWithdrawable bet amount does not increase any rolling.
AUTO_BY_PROVIDER_TYPESports bets apply to sports active rolling; live/slots apply to casino active rolling.
TO_NORMALSports bets apply to sports normal rolling; casino bets apply to casino normal rolling.
TO_BONUSSports 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:

FieldDescription
enabledEnables or disables normal-wallet transfer.
minimum_amountSmallest allowed transfer.
amount_unitTransfer amount must be divisible by this unit.
block_when_unsettled_bets_existBlocks transfer while the player has unsettled bets.

Rolling inheritance:

  • transfer_ratio = transfer_amount / source_balance_before_transfer
  • target_inherited_rolling = source_remaining_rolling * transfer_ratio
  • source_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/sport are interpreted as a full transfer from CASINO_NORMAL to SPORTS_NORMAL.
  • Existing player calls to /finance/transferWithdrawal/live&slots or /finance/transferWithdrawal/live_slots are interpreted as a full transfer from SPORTS_NORMAL to CASINO_NORMAL.
  • These aliases must never debit WITHDRAWABLE or 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:

FieldDescription
minimum_transfer_amountMinimum points transfer.
amount_unitPoints transfer unit.
target_bucket_type_codesTarget NORMAL bucket codes, for example SPORTS_NORMAL, CASINO_NORMAL, or UNIFIED_NORMAL.
rolling_multiplierRolling 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/active
  • GET /wallet/topologies/{topology_code}?version={version}
  • PUT /wallet/topologies/{topology_code}
  • PUT /wallet/topologies/{topology_code}/activate
  • PUT /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, and status are 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:

  • id
  • player_id
  • account
  • currency
  • status
  • created_at
  • updated_at

wallet_topology

Versioned wallet shape configuration.

Required fields:

  • id
  • code
  • version
  • status
  • document
  • created_by
  • created_at
  • activated_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:

  • id
  • topology_code
  • topology_version
  • code
  • wallet_group
  • role
  • bettable
  • withdrawable
  • transferable
  • display_order
  • status
  • metadata
  • created_at

Unique key:

  • (topology_code, topology_version, code)

wallet_bucket

One row per wallet bucket.

Required fields:

  • id
  • wallet_account_id
  • player_id
  • bucket_type_code
  • topology_code
  • created_topology_version
  • wallet_group
  • balance
  • version
  • created_at
  • updated_at

Unique key:

  • (player_id, topology_code, bucket_type_code)

Rules:

  • wallet_bucket stores 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:

  • id
  • player_id
  • promotion_coupon_id
  • scope
  • provider_ids
  • original_amount
  • remaining_amount
  • max_payout
  • rolling_multiplier
  • effective_odds_rule
  • topology_code
  • topology_version
  • policy_version
  • policy_snapshot
  • status
  • expires_at
  • created_at
  • updated_at

wallet_ledger

Append-only money movement record.

Required fields:

  • id
  • player_id
  • bucket_type_code
  • coupon_grant_id
  • direction
  • amount
  • before_balance
  • after_balance
  • change_type
  • provider_type
  • provider_id
  • bet_id
  • rolling_id
  • transfer_id
  • promotion_reference_id
  • request_id
  • topology_code
  • topology_version
  • policy_version
  • policy_snapshot
  • created_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:

  • id
  • policy_key
  • version
  • topology_code
  • topology_version
  • status
  • document
  • created_by
  • created_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:

  • id
  • request_id
  • player_id
  • bet_id
  • provider_type
  • provider_id
  • game_id
  • amount
  • funding_mode
  • selected_wallet_source
  • funding_breakdown
  • topology_code
  • topology_version
  • policy_version
  • policy_snapshot
  • status
  • created_at
  • settled_at
  • rolled_back_at

Rules:

  • funding_breakdown is 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:

  • id
  • player_id
  • transfer_type
  • source_bucket_type_code
  • target_bucket_type_code
  • amount
  • source_rolling_before
  • source_rolling_after
  • target_rolling_added
  • request_id
  • topology_code
  • topology_version
  • policy_version
  • created_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_id
  • player_id
  • bet_id
  • amount
  • provider_type
  • provider_id
  • game_id
  • selected_wallet_source when the active policy requires WALLET_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:

  • accepted
  • funding_breakdown
  • balance_snapshot
  • topology_code
  • topology_version
  • policy_version

funding_breakdown stores one row per debited bucket or coupon grant.

Settle Bet

Request must include:

  • request_id
  • player_id
  • bet_id
  • win_amount
  • valid_bet_amount
  • provider_type
  • provider_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_balance
  • topology_code
  • topology_version
  • groups.sports.normal
  • groups.sports.bonus
  • groups.sports.coupons
  • groups.casino.normal
  • groups.casino.bonus
  • groups.casino.coupons
  • shared.withdrawable
  • shared.points
  • coupon_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_bucket or wallet_ledger table 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_service remains the only money writer.