Coupons & Promo Codes

The atomic discount unit on ABc. An owner (or platform admin) generates a code, configures who/when/how-much, and the system applies it deterministically at checkout — on the public site or in a manual booking. Every redemption is tracked, capped and audit-logged.

Discount types

Percent off

e.g. 15 % off the room subtotal. Can carry a max-cap (max ₹ deduction).

Most common

Flat amount off

₹500 off any booking over ₹3,000. Predictable for finance.

Free nights

"Book 3, stay 4." Cheapest night becomes free. Min-stay enforced automatically.

V1.1

Free upgrade

Book Standard, get Deluxe if available at check-in. Otherwise refund the upgrade fee.

V1.1

Configurable rules — every coupon carries these

FieldTypeExample
Codetext, 4–16 chars, uppercaseSUMMER25, FIRSTSTAY
Internal nametext"Summer 2026 — Manali properties"
Discount typeenumpercent · flat · free_nights · free_upgrade
Discount valuenumeric15 (for %) · 500 (for ₹) · 1 (for nights)
Max discount cap₹ (nullable)Cap a % discount at ₹2,000
Valid from / untildateBooking window. 2026-06-012026-08-31
Stay window from / untildate (nullable)Check-in must be in this range. Often used with seasonal sales.
Property scopeall · selected · by_tag"All Manali properties" or specific list
Room-type scopeall · selected"Only Deluxe + Family Suite"
Channelsmulti-selectDirect · Manual · (OTAs always excluded)
Min booking value₹ (nullable)Min ₹3,000
Min nightsint (nullable)Min 2 nights
Max total usesint (nullable)100 redemptions total, then auto-expire
Max uses per guestint1 per email/phone, default
First-time guest onlyboolTrue for acquisition codes (no prior bookings on file)
StackableboolDefault false — one coupon per booking
Auto-applyboolTrue = applies without code entry if rules match (member discount)
Statusenumdraft · scheduled · active · paused · expired · exhausted

Lifecycle states

draftowner creating
schedulednot yet started
activeaccepting redemptions
paused
expireddate past — terminal
exhausted

Coupons list UI

app.abc.com/marketing/coupons
Coupons
5 active · 2 scheduled · ₹68,400 discount given (30d)
CodeTypeScopeUsedRevenue impactStatus
SUMMER25
Summer 2026 sale
25 % off
capped at ₹2,000
All Manali · all rooms 42 / 100
until 31 Aug
+₹1,82,400 booked
−₹38,200 discount
Active
FIRSTSTAY
New guest discount
₹500 flat All · min ₹3,000 118 / ∞
first-time only
+₹6,42,100 booked
−₹59,000 discount
Active
DIWALI500
Festive — auto-apply
₹500 flat
auto-apply if eligible
All · stay 20–28 Oct 0 / 200 Scheduled · Sep 15
RETURN20
Loyalty
20 % off All · repeat guests 61 / ∞
+₹3,12,000 booked
Active
MONSOON15
Low-occupancy push
15 % off Hillside · Jul–Sep stays 100 / 100
+₹2,18,000 booked
Exhausted
WEEKDAY10 10 % off All · check-in Mon–Thu 8 / ∞ Paused

Create / edit coupon — UI

app.abc.com/marketing/coupons/new
New coupon
Code is what guests type. Internal name is for you.
Live preview · checkout view
Sample 3-night booking · ₹14,000 subtotal

Subtotal₹14,000
SUMMER25 (−25 %)−₹2,000 · capped
Taxes & fees₹1,440

Total₹13,440
Guest saves ₹2,000 · code applied automatically? No, manual entry

Where coupons get applied

Public checkout

An expander "Have a code?" sits above the totals. Live validation: invalid codes get a friendly error; valid codes update the total in place.

Manual booking (desk)

Front-desk agent has an "Apply coupon" picker that lists only coupons matching the current property + dates + room type.

Auto-apply

If auto_apply = true and rules match (e.g. logged-in returning guest), the discount is silently applied with a green note.

Public checkout — code entry

abc.com/checkout
Order summary
3 nights × ₹4,200₹12,600
PROMO CODE
Applied: 25 % off (capped at ₹2,000). You save ₹2,000.
SUMMER25 discount−₹2,000
GST & city tax₹1,272

Total₹11,872

Validation logic — order of checks

  1. Code exists & is active

    Otherwise: "That code isn't valid."

  2. Within booking window

    Today must be between valid_from and valid_until. Otherwise: "This code has expired."

  3. Stay window (if set)

    Booking's check-in date must be in stay_from..stay_until. Otherwise: "Not valid for these dates."

  4. Property + room-type scope

    This booking's property and room type must match scope. Otherwise: "Not valid for this property."

  5. Channel scope

    Booking source must be in channels. OTA bookings always rejected. Otherwise: "Not valid for OTA bookings."

  6. Min booking value & min nights

    Otherwise: "Spend ₹X more to use this code."

  7. Per-guest cap

    How many times has this email/phone already used this code? Otherwise: "You've already used this code."

  8. Global cap

    max_total_uses not yet reached. Otherwise: "This code is fully redeemed."

  9. First-time-only check

    If first_time_only, the guest must have zero prior confirmed bookings. Otherwise: "Only for new guests."

  10. Compute discount

    Apply value, then cap. Return discount amount + line label.

Data model

coupons
  • iduuid
  • codetext uniq
  • nametext
  • owner_id→ users · nullable
  • typeenum
  • valuenumeric
  • max_discount_capmoney · nullable
  • valid_fromtimestamptz
  • valid_untiltimestamptz
  • stay_fromdate · nullable
  • stay_untildate · nullable
  • property_scopejsonb
  • room_type_scopejsonb
  • channelstext[]
  • min_booking_valuemoney · nullable
  • min_nightsint · nullable
  • max_total_usesint · nullable
  • max_per_guestint
  • first_time_onlybool
  • stackablebool
  • auto_applybool
  • statusenum
  • campaign_id→ campaigns · nullable
coupon_redemptions
  • iduuid
  • coupon_id→ coupons
  • booking_id→ bookings
  • guest_id→ guests
  • discount_amountmoney
  • statusapplied · voided
  • redeemed_attimestamp
  • voided_attimestamp · nullable

API contract

POST /api/coupons/validate
// Request — sent during checkout when a user enters a code
{
  "code": "SUMMER25",
  "booking_draft": {
    "property_id": "prp_…",
    "room_type_id": "rt_…",
    "check_in": "2026-07-12",
    "check_out": "2026-07-15",
    "subtotal": 12600,
    "channel": "direct"
  },
  "guest": { "email": "priya@...", "phone": "+91…" }
}

// Response — 200 valid
{
  "valid": true,
  "coupon_id": "cpn_01HX…",
  "label": "SUMMER25 — 25 % off (capped at ₹2,000)",
  "discount_amount": 2000,
  "new_subtotal": 10600
}

// Response — 422 invalid (one example)
{
  "valid": false,
  "reason": "channel_excluded",
  "message": "This code isn't valid for OTA bookings."
}

Cancellation & the cap

Cancellation voids the redemption.

When a booking with an applied coupon is cancelled, the coupon's redemption row is marked voided. This decrements used_count so another guest can still use it within the cap. If the coupon's cap is already past, the cancelled slot stays voided but the cap is not reopened (anti-gaming).

Anti-abuse

  • Velocity limits — max 5 code-validation attempts per IP per minute (prevents brute-force guessing).
  • Code complexity — system-generated codes are 8 random chars (≈ 2.8 × 10¹² combos); owner-typed codes must be ≥ 4 chars.
  • Per-guest cap — enforced by verified email AND verified phone (anti same-person-two-accounts).
  • First-time check — runs against all confirmed bookings for this phone OR email, across all owners.
  • Audit log — every coupon create/edit/disable + every redemption + every void written to audit_log.
  • Suspicious-burst alert — if a single coupon hits 50 redemptions in <1 hour, owner gets an alert.

Integration with other modules

ModuleTouch point
Pricing EngineCoupon discount is applied after the rate engine returns subtotal. Coupon never alters the per-night rate — only adds a discount line item.
BookingEach booking stores its applied coupon code + discount amount + redemption ID. Visible in detail.
RefundsCancellation voids redemption + refund includes the discount-adjusted total (not the gross).
EmailsConfirmation email shows the coupon line. New template variant for "coupon-driven booking".
AnalyticsNew report: revenue with-vs-without coupons, top codes by redemption.
CampaignsA coupon can be linked to a parent campaign — that's how campaign-level attribution works.

MVP vs later

MVP (Phase 1)

Percent + flat. Code entry at public checkout + manual booking. Per-guest + per-total caps. Cancellation void.

Phase 1

V1.1

Auto-apply rules. Free-nights & free-upgrade types. First-time-only enforcement. Email blast of a code.

Phase 2

V2

Stackable rules engine. Tiered codes (e.g. "10 % up to ₹500, 15 % up to ₹2000"). Referral codes that pay the referrer.

Phase 3