Promotions
Discounts with rails. No oops, no overshoot.
Run a summer promo, a back-in-stock code, a referral discount, a quiet-Tuesday flash sale. Each one comes with caps that hold and an audit row per use so the math reconciles every time.
Built for salons that have run a promo before and learned the hard way how a code without a cap turns into a six-month giveaway. Caps first, copy second.
01/
Time-windowed, code-gated, or both.
Pick a start date and an end date and the promo auto-applies inside the window. Add a code and the discount unlocks only when the client types it at the review step. Some promos run both: live for everyone for a weekend, code-gated for repeat clients the rest of the month. Codes are stored uppercase and case-insensitive on input.
Auto-apply in the window.
Strikethrough pricing shows on the service picker for every visitor in the date range. No code needed.
Code-gated only.
The price stays full until the client types the code. Useful for VIPs, partner referrals, lapsed-client win-back.
Combined.
Both behaviors on one promo: live for a weekend, code-gated the rest of the run.
One promo per visit.
Multiple matching promos? The resolver picks the largest discount; no stacking. Spelled out in the audit row so you know which promo won.
02/
Percentage or flat amount, your call.
Percentage off in the 1 to 100 range. Flat amount off in your business currency. The percentage shape protects the salon when service prices move (a 15% promo on a $50 service is $7.50, a 15% promo on a $200 service is $30, both make sense). Flat amount is cleaner copy when the promo is "$10 off your next visit".
03/
Scope the promo so it covers what you meant.
Pick All services, a single bundle, a service category, or a hand-picked list of specific services. Specific-service scope means the promo only matches when the client books one of the listed services; the discount magnitude stays restricted to those items even on a multi-service basket. If the basket has nothing matching, the promo politely declines.
All services.
The classic. Everything qualifies; cap-only restrictions still apply.
Specific services.
A hand-picked list of services that qualify. A haircut + brow-wax basket where only Haircut is listed discounts the haircut, not the wax.
Category.
All services in a category (Colour, Lashes, Treatments). Add a service to the category later and it's automatically eligible.
Bundle.
Apply the promo to a specific service bundle. Useful for "15% off the wedding-prep bundle this month".
04/
Caps that hold the line.
Three caps available on every promo: total uses (the promo expires at 100), per-client (each client can use it once), and minimum order (the basket must clear $50 before the promo applies). Caps run server-side at booking time; the audit row writes inside the same transaction so a race condition cannot blow past the cap.
05/
Admin surface that tells the truth.
The /promotions page lists every promo with a Live, Scheduled, Expired, or Paused badge and a per-promo use counter. Kebab actions: Pause (stops new uses, leaves audit history), Resume, Delete. Promos with usage history can't be deleted (the audit row would orphan); pause them instead.
Status at a glance.
Live (in window, active), Scheduled (window hasn't started), Expired (window passed), Paused (active but stopped accepting uses).
Use counter.
Per-promo number that ticks up on every successful booking that applied the promo. Reconciles with the audit table.
Pause then resume.
Quiet-Tuesday flash sale that got too hot? Pause, breathe, resume when the queue clears.
Audit row per use.
Every applied promo writes a promotion_uses row with the appointment, the client, the discount amount, and the timestamp. The data feeds the cap math; you can also query it later for marketing.
06/
What the client sees.
On the public booking page, the service picker renders strikethrough pricing on every qualifying service the moment an auto-apply promo is live. The summary rail shows a code input that's always visible (single-service mode and multi-item mode both). Package-only baskets hide the code input; a typed code in that case returns a polite "Promo codes don't apply to package-only visits" message. Confirmation pages itemize subtotal, promo (with its name), and total.
Common questions
Honest answers, including the ones we don't love.
Can a client stack two promos?
No. The resolver evaluates every matching promo on the basket and picks the one with the largest discount. The audit row records which promo won so the math is traceable. Stacking creates support tickets; the single-winner rule prevents them.
Do promos apply on admin manual bookings?
v1 promos resolve on the public booking page (the place codes get typed). Admin manual bookings use the existing per-appointment discount field, which is more flexible (any amount, any reason). The two paths share the same Reports surface so revenue nets honestly.
What happens when the promo expires mid-basket?
The resolver runs at submission, not on the picker render. If a client started a basket while a promo was live and the window expired before they hit Confirm, the price reverts to full and the rail updates before the booking commits. No surprise charges.
Can I refund a promo-discounted booking?
Yes. The refund operates on the actual amount paid (subtotal minus promo), not on the gross. The promotion_uses row stays for the audit; refund logic doesn't reverse the use counter (the promo was used, then refunded; both facts are true).
Can two clients use a per-client-capped code in the same minute?
Yes. Per-client means each individual client can use the code once. Two different clients each using it once is fine. The cap math runs server-side on the audit row, so the race is honest at the database level.
Pairs well with
Fourteen days. No card.
Try Flowesce on a real Saturday.
No card required, no auto-charge at the end. If Flowesce isn't for you, export everything in one click and walk.