Promotions, Loyalty, Vouchers & Change Money
Read Conventions first.
promotions
id,business_id,name,descriptionpromotion_typeNOT NULL - CHECK IN (‘item’,‘category’,‘brand’,‘supplier’,‘happy_hour’,‘invoice_tier’,‘membership_tier’,‘birthday’,‘payment_method’,‘quantity_break’,‘coupon’)is_active;start_date/end_dateNULLpriorityDEFAULT 100;is_exclusiveDEFAULT falsediscount_methodCHECK IN (‘percentage’,‘fixed’) NULL;discount_valueNULL;max_discount_amountNULLqty_break_buy_quantity,qty_break_free_quantityNULL;qty_break_discount_percentNULLcodeNULL - coupon code - UNIQUE (business_id, code)max_uses_total,max_uses_per_customerNULL;times_usedDEFAULT 0happy_hour_start_time,happy_hour_end_timeNULLmembership_conditionCHECK IN (‘any_member’,‘non_member’,‘specific_tier’) NULLbirthday_basisCHECK IN (‘birthdate’,‘loyalty_anniversary’) NULLmin_matched_spend_amount,min_matched_quantityNULLcreated_by,created_at,updated_at; index(business_id, is_active, promotion_type)
Promotion targeting tables
All composite-PK join tables, promotion_id CASCADE:
- promotion_target_items -
(promotion_id, item_id) - promotion_target_item_groups -
(promotion_id, item_group_id) - promotion_target_categories -
(promotion_id, item_category_id) - promotion_target_brands -
(promotion_id, brand_id) - promotion_target_suppliers -
(promotion_id, supplier_id)→ business_partners - promotion_target_loyalty_tiers -
(promotion_id, tier_id)→ loyalty_bonus_tiers CASCADE - promotion_target_payment_methods -
(promotion_id, payment_method_id)CASCADE - promotion_branches -
(promotion_id, branch_id)- where the promotion applies - promotion_happy_hour_days -
(promotion_id, day_of_week);day_of_weeksmallint CHECK BETWEEN 0 AND 6 (0 = Sunday)
promotion_tiers
Spend thresholds for an invoice_tier promotion.
id,promotion_idCASCADEmin_spend_amountNOT NULLdiscount_typeCHECK IN (‘percentage’,‘fixed’);discount_valueNOT NULL- Index
(promotion_id, min_spend_amount)
Promotion application & redemption
- ar_invoice_promotion_applications - what was actually discounted on an invoice.
id,ar_invoice_idCASCADE;line_indexinteger NULL - position within the request’s Lines[] slice, NOT a FK toar_invoice_lines.id; NULL = header-level (invoice_tier or coupon);promotion_idNOT NULL;promotion_namesnapshot;discount_amountNOT NULL,created_at; index(ar_invoice_id) - promotion_coupon_redemptions -
id,promotion_idNOT NULL,business_id,customer_idNULL;reference_typeDEFAULT ‘ar_invoice’;reference_idNOT NULL;discount_amount,created_by,redeemed_at; UNIQUE (promotion_id, reference_id) - one redemption per document
gift_vouchers
One-shot all-or-nothing lifecycle.
id,business_id,code- UNIQUE (business_id, code)amountNULL - CHECK (amount IS NULL OR amount > 0); NULL while unactivatedstatusDEFAULT ‘active’ - CHECK IN (‘unactivated’,‘active’,‘redeemed’,‘void’); ‘unactivated’ = pre-printed/registered, no value yetexpiry_dateNULL (auto-computed frombusiness_settings.gift_voucher_expiry_monthsat activation if not set)customer_idNULLissued_reference_typeNOT NULL,issued_reference_id,issued_byredeemed_reference_typeNULL,redeemed_reference_idNULL,redeemed_atcreated_at
Loyalty
- loyalty_point_transactions - append-only ledger;
business_partners.loyalty_points_balanceis the running total.id,business_id,customer_idNOT NULL;typeCHECK IN (‘earn’,‘redeem’);pointsNOT NULL;balance_afterNOT NULL;lkr_valueNOT NULL;spend_amountDEFAULT 0;reference_type,reference_idNULL;journal_entry_idNULL;created_by,created_at; indexes(customer_id, created_at DESC),(business_id, created_at DESC) - loyalty_bonus_tiers - monthly-spend thresholds that multiply earning.
id,business_id;min_monthly_spendCHECK >= 0 - UNIQUE index (business_id, min_monthly_spend);bonus_multiplierCHECK >= 0;created_at
change_money_transactions
Small-change credit kept on the customer’s account instead of handed over.
id,business_id,customer_idNOT NULLtypeCHECK IN (‘earn’,‘redeem’);amount;balance_afterreference_type,reference_id,journal_entry_idNULLcost_center_idcreated_by,created_at; indexes(customer_id, created_at DESC),(business_id, created_at DESC)