Promotion Service
Status
Active
Date
2026-04-28
Owners
- Platform Backend
Last Verified Commit
56362a7a
Runtime
API + standalone worker
Purpose
promotion_service owns coupon orchestration, promotion records, and settlement
logic for rebate, cashback, and lossback style promotional flows.
Primary Entry Points
/internal/promotions/rebate/*/internal/promotions/lossback/*/internal/promotions/coupons/*/internal/promotions/*record routes
Protection model:
/internal/promotions/*requiresX-Internal-Service-Tokengatewayandadmin_serviceare the intended synchronous callers
Dependencies
- PostgreSQL
- Redis
wallet_servicerolling_service
Background Work
promotion_worker runs three important categories of background work:
- wallet-event consumption
- settlement scheduling
- coupon saga recovery
Owned Data
- promotion config state
- coupon usage and coupon orchestration state
- settlement projections and summaries
promotion_coupon_sagapromotion_inbox, which is claimed withINSERT ... ON CONFLICT DO NOTHING RETURNINGbefore any bet-stat projection write. Duplicate Redis Stream deliveries that lose the claim skip side effects, soplayer_provider_stat_day/player_stat_dayincrements cannot be doubled by concurrent consumers.
Events
Emits:
- no separately documented outbound Redis stream is currently required
Consumes:
- wallet stream events including:
BET_SETTLED_CONFIRMEDBET_ROLLED_BACK_CONFIRMED
Health
- API
/healthchecks DB and Redis promotion_workerreadiness depends on:- worker supervision
- settlement scheduler running
- settlement job health staying green
Key Env Vars
DATABASE_URLREDIS_URLWALLET_SERVICE_URLROLLING_SERVICE_URLMULTI_BRAND_ENFORCEMENT— required; one ofoff/observe/enforce. Production target isenforcepost-Phase-16. Drivesmulti_brand_enforcement_mode{service="promotion_service"}gauge and the cross-brand reject decision inbrand_check.py(promotion_cross_brand_rejected_total).BRAND_SIGNING_KEY— required in production; HMAC-SHA256 secret used to signX-Brand-Signatureon outbound brand-scoped wallet writes from coupon saga / settlement.INTERNAL_SERVICE_TOKEN_PROMOTION— required in production; per-caller token presented to wallet/rolling when promotion calls them.PER_CALLER_TOKEN_REQUIRED—onis the Phase 16 target; activates the legacy-token hard-reject (T4-D-I2) on inbound/internal/promotions/*calls.INTERNAL_SERVICE_TOKEN— legacy single-shared-token; deprecated. Phase 16 release gate requires the bare variant to be absent.ENABLE_EVENT_CONSUMERENABLE_SETTLEMENT_SCHEDULERENABLE_SAGA_RECOVERY
Multi-Brand Constraints
Per ADR-009:
- coupon usage rows, promotion configs, settlement projections, and
promotion_coupon_sagarows carrybrand_id - coupon definitions, event configs, and rebate/cashback/lossback rates are per-brand; resolution falls back to documented global defaults only when a brand has no explicit value
- settlement schedulers iterate brand by brand in
brand_idascending order, sequentially; cross-brand aggregation in settlement is forbidden - internal promotion routes require
X-Brand-Id; mismatched-player-row behavior is gated byMULTI_BRAND_ENFORCEMENT(observelogs + counts and proceeds;enforcerejects) - coupon saga and points credit commands targeting
wallet_servicepropagatebrand_idand rely onwallet_servicebrand validation
Tests
cd servers_v2/promotion_service && uv run pytest- key suites:
tests/test_event_consumer.pytests/test_settlement_tasks.pytests/test_coupon_saga_recovery.pytests/test_coupon_routes.pytests/test_internal_auth.py