Appendix F: Promotion Rules Reference
Competitive Intelligence Applied
This appendix documents the complete promotions system, inspired by Retail Pro’s sophisticated promotions engine - their most powerful feature after 35 years of retail software development.
Promotion Types
1. Percentage Discount
Reduces price by a percentage.
{
"type": "percentage",
"value": 25,
"appliesTo": "category",
"targetIds": ["cat_apparel"]
}
Examples:
- 25% off all apparel
- 10% off entire purchase
- 15% off clearance items
2. Fixed Amount Discount
Reduces price by a fixed dollar amount.
{
"type": "fixed_amount",
"value": 10.00,
"appliesTo": "item",
"targetIds": ["item_shirt_001"]
}
Examples:
- $10 off any shirt
- $5 off purchase over $50
- $25 off seasonal items
3. Buy X Get Y (BXGY)
Buy a quantity, get additional items free or discounted.
{
"type": "buy_x_get_y",
"buyQuantity": 2,
"getQuantity": 1,
"getDiscountPercent": 100,
"appliesTo": "category",
"targetIds": ["cat_socks"]
}
Examples:
- Buy 2 get 1 free
- Buy 3 get 1 50% off
- Buy 1 get 2nd 25% off
4. Bundle Pricing
Fixed price for a combination of items.
{
"type": "bundle",
"bundlePrice": 99.00,
"requiredItems": [
{ "categoryId": "cat_shirts", "quantity": 3 }
]
}
Examples:
- 3 shirts for $99
- Outfit bundle: shirt + pants + belt for $149
- Mix & match: any 5 items for $75
5. Threshold Discount
Spend X amount, save Y amount.
{
"type": "threshold",
"thresholds": [
{ "spend": 50, "save": 10 },
{ "spend": 100, "save": 25 },
{ "spend": 200, "save": 60 }
]
}
Examples:
- Spend $50 save $10
- Spend $100 save $25
- Tiered: Spend more, save more
6. BOGO (Buy One Get One)
Special case of BXGY, commonly used.
{
"type": "bogo",
"getDiscountPercent": 50,
"appliesTo": "all"
}
Examples:
- BOGO 50% off
- BOGO Free (getDiscountPercent: 100)
- BOGO $10 off
Condition Rules
Minimum Purchase Amount
{
"conditions": {
"minPurchaseAmount": 75.00
}
}
Only applies if cart subtotal >= $75.00
Minimum Quantity
{
"conditions": {
"minQuantity": 3
}
}
Only applies if qualifying items quantity >= 3
Customer Type Requirement
{
"conditions": {
"customerTypes": ["gold", "platinum", "employee"]
}
}
Only applies to customers in specified loyalty tiers.
Customer Types:
| Type | Description |
|---|---|
guest | No customer attached |
basic | Standard customer |
silver | Silver loyalty tier |
gold | Gold loyalty tier |
platinum | Platinum loyalty tier |
employee | Staff discount |
vip | VIP customers |
Excluded Items
{
"conditions": {
"excludedItems": ["item_giftcard", "item_clearance_001"],
"excludedCategories": ["cat_gift_cards", "cat_already_on_sale"]
}
}
Items in these lists are never eligible for this promotion.
First Purchase Only
{
"conditions": {
"firstPurchaseOnly": true
}
}
Only applies to customers with zero purchase history.
Scheduling
Date Range
{
"schedule": {
"startAt": "2025-12-20T00:00:00Z",
"endAt": "2025-12-26T23:59:59Z"
}
}
Promotion only active during this window.
Active Days
{
"schedule": {
"activeDays": ["friday", "saturday", "sunday"]
}
}
Only active on specified days of week.
Active Hours (Retail Pro Feature)
{
"schedule": {
"activeHours": {
"start": "10:00",
"end": "14:00"
}
}
}
Only active during specified hours (lunch special, happy hour, etc.)
Recurring Schedule
{
"schedule": {
"recurring": true,
"recurrence": {
"type": "weekly",
"days": ["tuesday"],
"startTime": "17:00",
"endTime": "20:00"
}
}
}
Repeats every Tuesday 5-8 PM (e.g., “Taco Tuesday” equivalent).
Usage Limits
Total Uses
{
"limits": {
"maxUsesTotal": 1000
}
}
Promotion ends after 1000 total uses across all customers.
Per Customer
{
"limits": {
"maxUsesPerCustomer": 3
}
}
Each customer can use this promotion max 3 times.
Per Day
{
"limits": {
"maxUsesPerDay": 100
}
}
Max 100 uses per day (flash sale protection).
Per Transaction
{
"limits": {
"maxUsesPerTransaction": 1
}
}
Can only be applied once per sale (e.g., one coupon per purchase).
Stacking Rules
Non-Stackable (Exclusive)
{
"stacking": {
"stackable": false,
"priority": 10
}
}
Cannot combine with other promotions. Higher priority wins.
Stackable with Restrictions
{
"stacking": {
"stackable": true,
"stackableWith": ["promo_loyalty", "promo_birthday"],
"excludeWithCodes": ["CLEARANCE", "EMPLOYEE"]
}
}
Can stack with specific promotions, excludes others.
Priority System
When multiple exclusive promotions apply:
| Priority | Wins |
|---|---|
| 1 | Lowest - applies last if stackable |
| 10 | Higher - wins over lower |
| 100 | Highest - always wins |
{
"stacking": {
"priority": 50
}
}
Location Restrictions
Specific Locations
{
"locationIds": ["loc_gm", "loc_hm"]
}
Only valid at Greenbrier Mall and Hampton locations.
Exclude Locations
{
"excludeLocationIds": ["loc_outlet"]
}
Valid everywhere except outlet store.
All Locations
{
"locationIds": null
}
null means valid at all locations.
Promotion Evaluation Algorithm
┌─────────────────────────────────────────────────────────────────────────┐
│ PROMOTION EVALUATION FLOW │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ 1. GATHER CONTEXT │
│ ├── Cart items (itemId, categoryId, quantity, price) │
│ ├── Customer info (customerId, loyaltyTier, purchaseHistory) │
│ ├── Location (locationId) │
│ └── Current time (for schedule checks) │
│ │
│ 2. FILTER ACTIVE PROMOTIONS │
│ ├── Is within startAt - endAt range? │
│ ├── Is active today (activeDays)? │
│ ├── Is active now (activeHours)? │
│ ├── Is location valid (locationIds)? │
│ └── Has uses remaining (limits)? │
│ │
│ 3. CHECK ELIGIBILITY PER PROMOTION │
│ ├── Does cart meet minPurchaseAmount? │
│ ├── Does cart meet minQuantity? │
│ ├── Is customer type eligible? │
│ ├── Are there non-excluded items that qualify? │
│ └── Is customer eligible (maxUsesPerCustomer)? │
│ │
│ 4. CALCULATE DISCOUNTS │
│ ├── For each eligible promotion: │
│ │ └── Calculate discount amount │
│ │ │
│ 5. RESOLVE STACKING │
│ ├── Group by stackable vs non-stackable │
│ ├── For non-stackable: keep highest priority │
│ ├── For stackable: apply all that stackableWith allows │
│ └── Exclude any excludeWithCodes conflicts │
│ │
│ 6. RETURN RESULT │
│ ├── applicablePromotions[] (will be applied) │
│ ├── autoAppliedPromotions[] (loyalty, etc.) │
│ ├── ineligiblePromotions[] (and reasons) │
│ └── totalDiscount, newSubtotal │
│ │
└─────────────────────────────────────────────────────────────────────────┘
Database Schema
-- Promotions table
CREATE TABLE promotions (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL REFERENCES shared.tenants(id),
name VARCHAR(255) NOT NULL,
code VARCHAR(50), -- Promo code (optional)
description TEXT,
type VARCHAR(50) NOT NULL, -- percentage, fixed_amount, etc.
value DECIMAL(10,2), -- Discount value
applies_to VARCHAR(50) NOT NULL, -- all, category, item, vendor
target_ids TEXT[], -- Array of category/item IDs
conditions JSONB DEFAULT '{}', -- Complex conditions
schedule JSONB DEFAULT '{}', -- Scheduling rules
limits JSONB DEFAULT '{}', -- Usage limits
stacking JSONB DEFAULT '{}', -- Stacking rules
location_ids UUID[], -- NULL = all locations
status VARCHAR(20) DEFAULT 'active', -- active, paused, expired
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW(),
created_by UUID REFERENCES employees(id)
);
-- Promotion usage tracking
CREATE TABLE promotion_usage (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
promotion_id UUID NOT NULL REFERENCES promotions(id),
sale_id UUID NOT NULL REFERENCES sales(id),
customer_id UUID REFERENCES customers(id),
location_id UUID NOT NULL,
discount_amount DECIMAL(10,2) NOT NULL,
used_at TIMESTAMPTZ DEFAULT NOW()
);
-- Indexes for performance
CREATE INDEX idx_promotions_tenant_status ON promotions(tenant_id, status);
CREATE INDEX idx_promotions_code ON promotions(code) WHERE code IS NOT NULL;
CREATE INDEX idx_promotion_usage_promo ON promotion_usage(promotion_id);
CREATE INDEX idx_promotion_usage_customer ON promotion_usage(customer_id);
CREATE INDEX idx_promotion_usage_date ON promotion_usage(used_at);
Example Promotions
1. Holiday Weekend Sale
{
"name": "Holiday Weekend Sale",
"code": "HOLIDAY25",
"type": "percentage",
"value": 25,
"appliesTo": "category",
"targetIds": ["cat_apparel", "cat_accessories"],
"conditions": {
"minPurchaseAmount": 50.00,
"excludedCategories": ["cat_gift_cards", "cat_clearance"]
},
"schedule": {
"startAt": "2025-12-20T00:00:00Z",
"endAt": "2025-12-26T23:59:59Z"
},
"limits": {
"maxUsesPerCustomer": 2
},
"stacking": {
"stackable": false,
"priority": 50
}
}
2. Gold Member Auto-Discount
{
"name": "Gold Member Discount",
"code": null,
"type": "percentage",
"value": 10,
"appliesTo": "all",
"conditions": {
"customerTypes": ["gold", "platinum"]
},
"schedule": {},
"limits": {},
"stacking": {
"stackable": true,
"priority": 5
}
}
Automatically applied, no code needed, stacks with other promos.
3. Buy 2 Get 1 Free Socks
{
"name": "Sock Sale",
"type": "buy_x_get_y",
"buyQuantity": 2,
"getQuantity": 1,
"getDiscountPercent": 100,
"appliesTo": "category",
"targetIds": ["cat_socks"],
"schedule": {
"activeDays": ["saturday", "sunday"]
}
}
Weekend only, auto-applies when 3+ socks in cart.
4. Employee Discount
{
"name": "Employee Discount",
"code": "STAFF",
"type": "percentage",
"value": 40,
"appliesTo": "all",
"conditions": {
"customerTypes": ["employee"],
"excludedCategories": ["cat_gift_cards"]
},
"stacking": {
"stackable": false,
"priority": 100
}
}
40% off for employees, highest priority, never stacks.
5. Flash Sale (Limited Quantity)
{
"name": "Flash Sale - First 50",
"type": "percentage",
"value": 50,
"appliesTo": "item",
"targetIds": ["item_bomber_jacket"],
"schedule": {
"startAt": "2025-01-15T12:00:00Z",
"endAt": "2025-01-15T14:00:00Z"
},
"limits": {
"maxUsesTotal": 50,
"maxUsesPerCustomer": 1
}
}
2-hour flash sale, first 50 customers only.
API Usage Examples
Create a Promotion
POST /api/v1/promotions
Authorization: Bearer {token}
{
"name": "Summer Sale",
"code": "SUMMER30",
"type": "percentage",
"value": 30,
"appliesTo": "category",
"targetIds": ["cat_swimwear", "cat_sandals"],
"schedule": {
"startAt": "2025-06-01T00:00:00Z",
"endAt": "2025-08-31T23:59:59Z"
}
}
Evaluate Cart for Promotions
POST /api/v1/promotions/evaluate
Authorization: Bearer {token}
{
"locationId": "loc_gm",
"customerId": "cust_jane",
"lineItems": [
{ "itemId": "item_001", "categoryId": "cat_swimwear", "quantity": 2, "unitPrice": 45.00 }
],
"subtotal": 90.00,
"appliedCodes": ["SUMMER30"]
}
Check Promotion Usage
GET /api/v1/promotions/{id}/usage?from=2025-06-01&to=2025-06-30
Authorization: Bearer {token}
Response:
{
"promotionId": "promo_summer30",
"period": { "from": "2025-06-01", "to": "2025-06-30" },
"totalUses": 342,
"totalDiscountGiven": 4567.89,
"uniqueCustomers": 287,
"byLocation": [
{ "locationId": "loc_gm", "uses": 145, "discount": 1890.50 },
{ "locationId": "loc_hm", "uses": 112, "discount": 1456.78 }
],
"byDay": [
{ "date": "2025-06-01", "uses": 23 },
{ "date": "2025-06-02", "uses": 31 }
]
}
Implementation Checklist
- Create
promotionstable with JSONB columns - Create
promotion_usagetracking table - Implement PromotionService with evaluation logic
- Add /api/v1/promotions endpoints
- Build promotion editor UI in Admin Portal
- Add promotion display to POS cart
- Implement auto-apply for loyalty promotions
- Add promotion analytics to Reports
- Test stacking logic thoroughly
- Load test evaluation endpoint
Competitive Advantage
| Feature | Our System | Retail Pro | Lightspeed |
|---|---|---|---|
| Minute-level scheduling | ✅ | ✅ | ❌ |
| Customer type targeting | ✅ | ✅ | ⚠️ Limited |
| Complex stacking rules | ✅ | ✅ | ❌ |
| Per-location restrictions | ✅ | ✅ | ✅ |
| Usage limits (total/customer/day) | ✅ | ✅ | ⚠️ Limited |
| Auto-apply (no code needed) | ✅ | ✅ | ⚠️ Limited |
| Real-time evaluation API | ✅ | ✅ | ✅ |
We match Retail Pro’s 35-year sophistication in a modern architecture.
Appendix F - Version 1.0 Based on competitive analysis of Retail Pro Prism promotions engine