Skip to content

Promo codes

Purpose

The Promocodes screen creates, lists, and deactivates promotional codes. Codes can be one-off (single-use), batch-generated by template, or first-deposit-bonus codes (which get a separate game-whitelist sub-config — covered in deposit-bonuses.md). Players redeem via POST /promo/<code> (player-facing).

Audience

Marketing (primary), customer support (issue replacement codes), risk (audit promo abuse).

Path in admin-fe

Screen URL Page
Promocodes list /promocodes ebit-admin-fe/src/app/(dashboard)/promocodes/page.tsx
Promo detail /promocodes/[id] ebit-admin-fe/src/app/(dashboard)/promocodes/[id]/page.tsx
Deposit-bonus subset (first-deposit) /deposit-bonuses covered in deposit-bonuses.md

Backing API endpoints

Endpoint Source
GET /admin/promo (list) apps/api/src/promo/controllers/admin-promo.controller.ts:46
GET /admin/promo/:id apps/api/src/promo/controllers/admin-promo.controller.ts:74
PUT /admin/promo (create) apps/api/src/promo/controllers/admin-promo.controller.ts:147
PATCH /admin/promo/:id (update) apps/api/src/promo/controllers/admin-promo.controller.ts:157
DELETE /admin/promo/:id (deactivate; restorable) apps/api/src/promo/controllers/admin-promo.controller.ts:168
PUT /admin/promo/many (batch by template) apps/api/src/promo/controllers/admin-promo.controller.ts:81
GET /admin/user-promo (list claims) apps/api/src/promo/controllers/admin-promo.controller.ts:56
DELETE /admin/user-promo/:id (cancel a claim) apps/api/src/promo/controllers/admin-promo.controller.ts:66

Frontend wiring: ebit-admin-fe/src/queries/promocodes/.

Key actions

Action Required permission API call DB tables touched Audit-logged?
List codes promo.code.view GET /admin/promo PromoCode yes
View one code promo.code.view GET /admin/promo/:id PromoCode, UserPromoCode (claims) yes
Create code promo.code.edit PUT /admin/promo PromoCode yes
Update code promo.code.edit PATCH /admin/promo/:id PromoCode yes
Deactivate code (restorable) promo.code.edit DELETE /admin/promo/:id PromoCode.deactivatedAt yes
Batch-create from template promo.code.edit PUT /admin/promo/many PromoCode (n inserts) yes
List claims promo.code.view GET /admin/user-promo UserPromoCode yes
Cancel a claim (refund / void) promo.code.edit DELETE /admin/user-promo/:id UserPromoCode, Transaction yes

Filters and views

  • Status — active / deactivated / expired / fully claimed.
  • Typecash, bonus, free-spins, first-deposit-match.
  • Date rangecreatedAt or expiresAt.
  • Search — by code (case-insensitive substring).
  • Claims drill-in — paginated list of users who used this code.

Common workflows

  1. Single-use makegood code for a player. CS opens /promocodes, clicks Create. Sets code = "MAKEGOOD-2025-1234" (unique), value = 50 USD, expires in 7 days, single-use. Saves. Tells player to redeem via player site.
  2. Campaign batch. Marketing wants 5,000 unique codes for a partner campaign. Click Batch Create, configure template (PARTNER-XXXXXX), count, value, expiry. Backend writes 5k rows. Admin downloads the list (CSV via player UI or DB).
  3. Promo code abuse audit. Risk filters /admin/user-promo by dateFrom=last 24h. Sorts by userId to find one user redeeming many. Cross-check with bets-history.md for matching bonus-arbitrage wagering pattern.
  4. Cancel a fraudulent claim. Risk opens the claim row in /admin/user-promo, clicks Cancel. Backend voids the claim, refunds platform side via reverse Transaction. Logs in AdminActionLog.
  5. Deactivate a leaked code. A code got posted to a public Telegram group. Marketing finds the code, clicks Deactivate. Backend sets PromoCode.deactivatedAt. New claims rejected. Existing claims preserved (still valid).

Edge cases / gotchas

  • Codes are case-insensitive on lookup. Backend stores lowercase; UI shows uppercase.
  • Deactivate is soft. A deactivatedAt-set code can be restored by PATCH /admin/promo/:id { deactivatedAt: null }.
  • Single-use is enforced via UserPromoCode unique on (promoCodeId, userId) — but batch codes typically have maxClaims=1 per code with multiple codes. The two patterns are easy to confuse.
  • First-deposit-bonus codes carry a game-whitelist. Separate endpoints (/admin/promo/deposit/:id/games). See deposit-bonuses.md.
  • No expiry sweeper in the admin panel. Expired codes are filtered out at redeem time (promo-validator.service.ts). They remain in the DB.
  • Cancelling a claim does not revoke the player's bets placed using the bonus. To clawback bets, you must refund manually via user-profile.md deduct.
  • PUT /admin/promo/many with > 10k entries can timeout. Frontend chunks into batches; backend doesn't paginate the create itself.
  • Bots are excluded from claims by User.isBot=true check in promo-validator.service.ts.

Sequence — creating a single-use makegood code

sequenceDiagram
    actor cs as CS Operator
    participant admin-fe
    participant api
    participant pg as Postgres
    cs->>admin-fe: open /promocodes, click Create, fill {code, value, expiresAt, maxClaims=1}
    admin-fe->>api: PUT /admin/promo { ... }
    api->>api: PermissionGuard('promo.code.edit')
    api->>pg: INSERT PromoCode (code=lower, value, expiresAt, maxClaims=1)
    api->>pg: INSERT AdminActionLog
    api-->>admin-fe: PromoCodeDto
    admin-fe-->>cs: code added to list, copy-to-clipboard helper
    Note over cs: shares code with player via Zendesk
    Note over admin-fe,pg: player redeems via POST /promo/<code> (player-facing)