---
tags: Promotions
title: Promotions
robots: noindex, nofollow
---
# Promo Codes
## Goal
Stand up a ***Promotions*** domain that oversees creation, update, storage, and redemption mechanisms for incentivized offers.
The MVP will revolve around ***text-based Promo Codes*** to be created/updated in the admin panel and redeemed by users in-app.
#### Backend Services
|Service|Responsibility|
|-|-|
Admin.Engines.Promotions|Responsible for creating + updating Promo Codes and potentially Campaigns, writing them directly to the PromoCodes db table, Campaigns db table
Engines.Promotions|Responsible for validating a Promo Code, creating a PromoCodeRedemption Record and writing a WalletTransactionCommand to the Wallet service bus queue
Engines.Wallet.Async|Responsible for adding site credit transaction associated with a promo code redemption to the internal wallet
Engines.Promotions.Data|Responsible for Promotions DB configuration and table defintions for PromoCodes, PromoCodeRedemptions, Campaigns, etc.
## Implementation
##### MVP
**Admin Side**: Units admin should have a dedicated Admin Panel tab tied to the *Admin.Engines.Promotions* service that allows creating and updating a promotional code with an eligible redemption window, redemption limit, etc.
- Consideration to also create Campaigns tab + Create/Update Campaign mutations
**Client Side**: Users should have a dedicated UI view tied to the *Engines.Promotions* service in which they can enter the promo code they have received in order to redeem on Units and boost their site credit amount.
##### Promo Code Redemption Flow
```mermaid
sequenceDiagram
participant user as User
participant ui as UI
participant pe as PromotionsEngine
participant pae as PromotionsAsyncEngine
participant wae as WalletAsyncEngine
participant sb as ServiceBus
participant pt as PromotionsDB
participant wt as WalletDB
user ->> ui: Enter Promo Code
ui ->> pe: RedeemPromoCode Mutation (via GQL Gateway)
pe ->> pt: Query PromoCodes Table
pt ->> pe: PromoCodeResponse
opt VALID PROMO CODE
par Wallet Transaction Step 1 + 2
pe->>pt: Write new PromoCodeRedemption record
pe->>pt: Update PromoCode record PromoCode.TotalRedeemed + 1
end
par Site Credit Wallet Transaction Step 3
pe--)sb: Send WalletTransactionCommand
wae ->> wae: Handle WalletTransactionCommand
wae->>wt: Write new Transactions record
end
pe ->> ui: PromoCode Redeemed Success Message
end
opt INVALID PROMO CODE
pe ->> ui: Promo Code Invalid Message
end
```
##### Promo Code Validation Logic
Client should hit RedeemPromoCode mutation in Engines.Promotions to check against eligibility before writing a PromoCode RedemptionCommand message and WalletTransaction command message to the Promotions and Wallet sb queues
These eligibility checks may include:
* Player eligible for promos - check PlayerProfile.PromoEnabled = true
* PromoCode string matches existing record in DB
* PromoCode is active - eligible start date has begun and/or bool field isActive = true
* PromoCode not expired - eligible end date has not passed
* PromoCode redemption limit has not been met
## Data Models
#### Proposed Tables:
- PromoCodes
- FK to Campaigns
- PromoCodeRedemptions
- FK to PromoCodes
- Association to Users table
- Association to Transactions table
- Campaigns
#### PromoCode Data Model
|Name|Data Type|Description|
|-|-|-|
ID|ID Int|NOT NULL
Code|String (24)|NOT NULL, UNIQUE
PromoAmount|Decimal|NOT NULL
CampaignId|String|NULLABLE, FK: Campaigns
RedemptionLimit|Int|NOT NULL
TotalRedeemed|Int|NOT NULL
IsActive|Bool|NOT NULL
CreatedAtUtc|Datetime|NOT NULL
StartedAtUtc|Datetime|NOT NULL
EndedAtUtc|Datetime|NOT NULL
UpdatedAtUtc|Datetime|NULLABLE
UpdatedBy|String|NULLABLE
#### CreatePromoCodeRequest
```
public class CreatePromoCodeRequest
{
public string Code { get; set; }
public decimal PromoAmount { get; set; }
public string? CampaignId { get; set; }
public int RedemptionLimit { get; set; }
public int TotalRedeemed { get; set; }
public DateTime StartedAtUtc { get; set; }
public DateTime EndedAtUtc { get; set; }
public bool IsActive { get; set; }
}
```
#### UpdatePromoCodeRequest
```
public class UpdatePromoCodeRequest
{
public int PromoCodeId { get; set; }
public int? RedemptionLimit { get; set; }
public int? CampaignId { get; set; }
public int? TotalRedeemed { get; set; }
public bool? IsActive { get; set; }
public DateTime? StartedAtUtc { get; set; }
public DateTime? EndedAtUtc { get; set; }
public DateTime? UpdatedAtUtc { get; set; }
public string? UpdatedBy { get; set; }
}
```
#### PromoCodeRedemption Data Model
|Name|Data Type|Description|
|-|-|-|
ID|ID Int|NOT NULL
PromoCodeId|ID Int|NOT NULL, FK: PromoCodes
CampaignId|ID Int|NULLABLE
UserId|String|NOT NULL
CreatedAtUtc|Datetime|NOT NULL
**Indexes:**
- Unique Index on UserId and PromoCodeId (UserId, PromoCodeId)
#### Campaign Data Model
|Name|Data Type|Description|
|-|-|-|
ID|ID Int|NOT NULL
Name|String (60)|NOT NULL
Description|String (200)|NOT NULL
CreatedAtUtc|Datetime|NOT NULL
UpdatedOnUtc|Datetime|NULLABLE
UpdatedBy|String|NULLABLE
#### CreateCampaignRequest
```
public class CreateCampaignRequest
{
public string Name { get; set; }
public string Description { get; set; }
}
```
#### UpdateCampaignRequest
```
public class UpdateCampaignRequest
{
public int ID { get; set; }
public string Name { get; set; }
public string Description { get; set; }
}
```
#### Entity Relationship Diagram
```mermaid
erDiagram
CAMPAIGN ||--o{ PROMOCODE : allows
CAMPAIGN {
int Id PK
string Name
string Description
datetime CreatedAt
}
PROMOCODE {
int Id PK
string Code
decimal PromoAmount
string CampaignId FK
int RedemptionLimit
int TotalRedeemed
bool IsActive
datetime CreatedAt
datetime StartedAt
datetime EndedAt
}
PROMOCODE ||--o{ PROMOCODEREDEMPTION : allows
USER }|..|{ PROMOCODEREDEMPTION : redeems
```
#### Considerations
- What should the max length of a PromoCode be? 12 characters?
- 24
- What fields should we allow to update on a PromoCode?
- TotalRedemptions
- IsActive
- Campaigns domain: should we create a Campaign data model and table within the Promotions DB context to tie PromoCodes to marketing campaigns?
- This will help in distinguishing between internal campaigns, marketing partnerships, etc.
- Will require Campaigns tab in Admin Panel as well to create/update Campaign
- Should we have start date - end date eligibility windows? Or just an isActive bool flag that we can update from the Admin Panel?
- Time window could give us historical record of when this campaign ran
- Should we have total redemption limits?
- 1 redemption per person, but do we want to cap redemptions?
- This gives us flexibility to create Campaigns in which there is urgency due to an expiring amount of Promo codes
- Should we store Category on the PromoCodes model?
- Category Ex: GENERAL_SITE_CREDIT vs. REFERRAL_SITE_CREDIT
- Should we store Type on the PromoCodes model?
- Type Ex: TEXT_ENTRY_CODE vs. QR_CODE
- Any reason to store the Code value on the PromoCodeRedemption record?
## Tickets
- Stand up new Admin.Engines.Promotions service (1)
- Provision Promotions DB resources and configure for Promotions DB access to write to PromoCodes table
- Standup Engines.Promotions.Data service (1)
- Create data models:
- Campaign
- PromoCode
- PromoCodeRedemption
- Create tables
- Campaigns
- PromoCodes
- PromoCodeRedemptions
- Admin-side logic for Promo Codes (3)
- Update relevant data packages
- Write mutations for Campaigns
- CreateCampaign
- UpdateCampaign
- Test Campaign mutations
- Write mutations for Promo Codes
- CreatePromoCode
- UpdatePromoCode
- Test Promo Code mutations
- Stand up dedicated Engines.Promotions service (5)
- Model off of existing Engines.Payments
- Make sure relevant packages are up to date
- Configure to Promotions DB, need to access PromoCodes table and PromoCodeRedemptions table
- Write mutation responsible for validating promo code
- check eligibilty etc.
- Write new record to PromoCodeRedemptions table
- Update TotalRedemptions value on PromoCode record in PromoCodes table
- Write a WalletTransactionCommand message to sb queue
- Add new services to GQL Gateways (1)
- Admin.Engines.Promotions => Admin GQL Gateway
- Engines.Promotions => GQL Gateway
## Future Reference
Eventual types and categories to consider for expanding this feature
##### Promotion Types:
- Text Code (MVP)
- QR Code
- Automated (DONE)
- Ex: First Time Deposit Match
- Link
- Ex: Referral Link
##### Promotion Categories:
- Site Credit Gift (Referral)
- Site Credit Gift (General)
- Deposit Match
- Entry Match
- Bet and Get
- Entry Multiplier Boost
- Risk free entry
- Risk free story/leg
- Unlock a discounted line
## Product Requirements
- Admin can create Campaign (1:many with promo codes)
- Admin can create Promo Code (can be associated with at most 1 campaign - does not have to)
- admin can disable promo codes at will
- promo codes can be updated (which fields TBD)
- User gets ahold of Promo Code (via twitter, email, etc)
- User can input Promo Code in app and submit
- BE validates Promo Code (date, redemption limit, user promo enabled, etc)
- If validated
- inserts to redemptions table
- increments redemption count in promo codes table
- if the above two steps are successful:
- kick off AddTransactionCommand
- return success
Questions we need to define:
- shape of campaign table and create request (AP)
- shape of promo code table and create request (AP)
- update promo code - what is allowed to be updated?
- shape of promo code redemption table
- shape of promo code redemption request (Client)
- promo code redemption validation logic
- different error code responses (product)
- promo code redemption logic (table inserts, updates)
- AddTransactionCommand metadata
Notes:
- For all timestamps, include UTC in field name
- Campaigns
- Campaign doesn't need start/end date
- Campaign doesn't need partner fields
- Campaign should be id/name/description/createdAt (request model to reflect this)
- Promo Codes
- Table Model
- Count of db.PromoCodeRedemptions records is source of truth when validating redemption limit, but we will stil maintain TotalRedeemed in PromoCode table for analytics purposes
- remove category, remove referral
- campaign is nullable
- rename amount => PromoAmount
- Code is Unique index
- increase VARCHAR for name to 24
- Create request is implied
- Update Request
- cannot change PromoAmount
- can change:
- start/end date
- redemptionlimit
- isActive
- Redemption Table
- remove IP
- unique index for userid X promocodeid
- remove transactionRefId
- add createdAtUTC, campaignid (nullable) and amount
- Redemption Logic
- validation implied
- the following actions exist in a transaction
- insert into redemptions table
- update count on promo code table
- send SB message to wallet
- AddTransactionCommand metadata - implied
- Ref field should include unique combo of userid x promocodeid