본문으로 건너뛰기

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/* requires X-Internal-Service-Token
  • gateway and admin_service are the intended synchronous callers

Dependencies

  • PostgreSQL
  • Redis
  • wallet_service
  • rolling_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_saga
  • promotion_inbox, which is claimed with INSERT ... ON CONFLICT DO NOTHING RETURNING before any bet-stat projection write. Duplicate Redis Stream deliveries that lose the claim skip side effects, so player_provider_stat_day / player_stat_day increments cannot be doubled by concurrent consumers.

Events

Emits:

  • no separately documented outbound Redis stream is currently required

Consumes:

  • wallet stream events including:
    • BET_SETTLED_CONFIRMED
    • BET_ROLLED_BACK_CONFIRMED

Health

  • API /health checks DB and Redis
  • promotion_worker readiness depends on:
    • worker supervision
    • settlement scheduler running
    • settlement job health staying green

Key Env Vars

  • DATABASE_URL
  • REDIS_URL
  • WALLET_SERVICE_URL
  • ROLLING_SERVICE_URL
  • MULTI_BRAND_ENFORCEMENT — required; one of off / observe / enforce. Production target is enforce post-Phase-16. Drives multi_brand_enforcement_mode{service="promotion_service"} gauge and the cross-brand reject decision in brand_check.py (promotion_cross_brand_rejected_total).
  • BRAND_SIGNING_KEYrequired in production; HMAC-SHA256 secret used to sign X-Brand-Signature on outbound brand-scoped wallet writes from coupon saga / settlement.
  • INTERNAL_SERVICE_TOKEN_PROMOTIONrequired in production; per-caller token presented to wallet/rolling when promotion calls them.
  • PER_CALLER_TOKEN_REQUIREDon is 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_CONSUMER
  • ENABLE_SETTLEMENT_SCHEDULER
  • ENABLE_SAGA_RECOVERY

Multi-Brand Constraints

Per ADR-009:

  • coupon usage rows, promotion configs, settlement projections, and promotion_coupon_saga rows carry brand_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_id ascending order, sequentially; cross-brand aggregation in settlement is forbidden
  • internal promotion routes require X-Brand-Id; mismatched-player-row behavior is gated by MULTI_BRAND_ENFORCEMENT (observe logs + counts and proceeds; enforce rejects)
  • coupon saga and points credit commands targeting wallet_service propagate brand_id and rely on wallet_service brand validation

Tests

  • cd servers_v2/promotion_service && uv run pytest
  • key suites:
    • tests/test_event_consumer.py
    • tests/test_settlement_tasks.py
    • tests/test_coupon_saga_recovery.py
    • tests/test_coupon_routes.py
    • tests/test_internal_auth.py