PRD 06 — Pricing Plans (packages, memberships, promo codes, pricing groups)
Status: draft · Priority tier: P0 (packages, one-time memberships, promo core) / P1 (recurring, bulk codes) · Size: L · Depends on: PRD 01 (workspace, locations, currency), PRD 02 (classes), PRD 04 (appointments), PRD 07 (customers, tags), PRD 08 (transactions, payments)
1. Summary
Defines what a studio sells besides drop-in bookings: packages (credit bundles with validity), memberships (one-time or recurring access with credits), plan categories, promo codes (plan and drop-in, single and bulk sets) and pricing groups (customer groups with their own prices). Plans are the revenue core and the thing bookings consume credits from.
2. Problem & goals
- Owner can publish a package in ≤ 13 clicks plus typing (original: 13) and a one-time membership in ≤ 12.
- A plan, once purchased (online or manually, PRD 08), creates a customer entitlement with correct credits, start and end dates (100% of test cases in section 12).
- Promo codes apply deterministically: same cart + same code = same discount; limits are never exceeded under concurrent redemption.
- Non-goals (v1): recurring billing execution via a real gateway (see 08 open question), time-of-day "Time Pricing", organisation-linked packages, transferable packages, staff payroll linkage, store UI for buying plans (PRD for online store).
3. Users & roles
Owner/admin (full), manager with Pricing Plans permission (create/edit), front desk (view plans, sell via PRD 08, no edit), customer (sees public plans in store, redeems codes at checkout). Role matrix in section 11.
4. Scope
In (v1)
- must: Package CRUD with 4-step wizard (details, pricing, options, classes) and draft save
- must: Package status lifecycle (draft, active, inactive, scheduled, expired) with list filters and stats
- must: Membership wizard (about, pricing/credits, options), one-time payment, validity by duration or fixed range
- must: Credits: unlimited or limited, per service type (class, appointment, outlet access)
- must: Plan categories (create, rename, reorder, private, delete)
- must: Promo codes: plan and drop-in types, percent/fixed, once-only duration, active dates, list + deactivate/delete
- must: Pricing groups CRUD with colour, assignment (assignment UI lives in PRD 07), per-group plan price
- should: Activation on purchase vs first booking (packages); customer-chosen start (memberships)
- should: Location access (chosen set / customer picks 1), purchase eligibility (anyone / first plan / non-members / specific customers), purchase limit per customer
- should: Automations on purchase: assign pricing group, assign tag
- should: Private plan, scheduled availability window, duplicate, copy link, set inactive
- should: Promo limits (total uses, one per customer, min spend, discount cap), eligibility segments, export, mark as used, duplicate
- should: Credit rollover on memberships; credit rate per class in packages
-
should: Display options (custom vs auto description; online/onsite booking channel) Later (v2+)
-
should: Recurring memberships (cycle, end after N cycles, minimum commitment + early-cancel fee, joining fee) — needs payment provider (PRD 08 open question); data model reserved in v1
- should: Recurring promo duration (forever / N cycles) — depends on recurring memberships
- could: Bulk promo code sets (N unique codes under one title)
-
could: Day/time usage restrictions; per-class booking cap; trial plan flag; display layout preview Out
-
Transferable and organisation-linked packages: no evidence of behaviour beyond a toggle; revisit on demand.
- Time-of-day pricing (peak/non-peak, pro-rating): separate feature, not observed in detail.
- Pricing plan reports: belongs to reports PRD.
5. User stories
- US-06-01 As an owner, I want to create a 5-class package valid 1 month so that customers can prepay.
- US-06-02 As an owner, I want a package to start counting validity at first booking so that unused time is not lost.
- US-06-03 As an owner, I want different prices per pricing group (e.g. staff, student) so that I can reward segments.
- US-06-04 As an owner, I want to restrict a plan to new customers so that intro offers are not abused.
- US-06-05 As an owner, I want to group plans into categories and hide some from the store so that the catalogue stays tidy.
- US-06-06 As an owner, I want a membership with unlimited classes for a fixed duration so that regulars have a simple option.
- US-06-07 As an owner, I want buyers auto-tagged/grouped on purchase so that segments stay current.
- US-06-08 As an owner, I want promo codes with limits and dates so that campaigns cost me a known amount.
- US-06-09 As a front desk user, I want to see which plans a customer is eligible for so that I sell correctly.
- US-06-10 As an owner, I want to pause selling a plan without losing existing customers' credits.
6. Functional requirements
| ID | requirement | priority | evidence |
|---|---|---|---|
| FR-06-01 | Packages list grouped by category accordion; search; filter by status (inactive, active, draft, scheduled, expired) and category; card shows name, status chip, validity, price, total purchases, total sales | must | S45, S50, S194 |
| FR-06-02 | Package wizard step 1: name (≤100, required), description (rich text ≤1000, required), category (required, existing categories only), location access (admin-chosen set or customer picks 1), validity number + unit (days/weeks/months/years), activation (date of purchase or date of first booking) | must | S49, S190, F70 |
| FR-06-03 | Package step 2: price type paid/free; price per pricing group ("default" = everyone; each ticked group reveals its own price); credits count | must | S191 |
| FR-06-04 | Package step 3: who can buy (anyone / specific segment: first plan, non-members / specific customers); optional per-customer purchase limit | must | S192 |
| FR-06-05 | Package step 3 extras: automations (assign pricing group on purchase, tag on purchase); online/onsite booking channel; private plan; availability start (+ optional end) | should | S192 |
| FR-06-06 | Package step 3 extras: per-class booking cap, day/time restriction, trial flag | could | S192 |
| FR-06-07 | Package step 4: pick classes (select all, by category, search); free classes are shown disabled and cannot be included; credit rate per class (default 1) | must | S193 |
| FR-06-08 | Wizard shows progress, supports Save as draft at any step; Next disabled until step valid; final step publishes immediately (status active, no separate publish) | must | S49, S190-S194 |
| FR-06-09 | Package row menu: edit, duplicate, set inactive, copy link, delete (confirm; past purchases keep a retained record) | should | S194c, F20 |
| FR-06-10 | Membership wizard step 1: name (≤100), description (≤500), category | must | S57 |
| FR-06-11 | Membership step 2: default price + optional per-group prices; payment frequency one-time (v1); validity as duration or fixed date range; activation = purchase date or customer-chosen start within N days (default 30) | must | S58 |
| FR-06-12 | Membership recurring: every N days/weeks/months, optional end after N cycles, minimum commitment with early-cancel fee, joining fee. Publish blocked until payment provider connected | should (v2) | S58, S151 |
| FR-06-13 | Membership credits per service type (class, appointment, outlet access): unlimited or limited count, rollover toggle, selected services, location access | must | S59 |
| FR-06-14 | Membership options: restrict purchase (all / segment / specific customers), auto pricing group, auto tag, private plan, schedule availability; day/time restriction and display layout are could | should | S60 |
| FR-06-15 | Plan categories per type (package, membership): create inline (default name, editable), rename, set private, reorder by drag, delete; deleting moves items to an undeletable default "uncategorised" category | must | S47, F22 |
| FR-06-16 | Promo code create: code (≤20 chars, case-insensitive unique per business), type plan or drop-in, value percent or fixed, optionally restricted to specific plans / drop-in services, duration once | must | S52, S53, F23 |
| FR-06-17 | Promo limits: total uses, one use per customer, minimum purchase, cap on discount amount | should | S52 |
| FR-06-18 | Promo eligibility: all / segment (new customers, bought more than once, no active plan, holds specific plan, in specific pricing groups) / specific customers | should | S52 |
| FR-06-19 | Promo active dates: start (default now), optional end; status active/inactive/used/expired derived | must | S52 |
| FR-06-20 | Promo list: search, date range, type, status filters; table code, terms, type, start, end, qty (or unlimited), used, status; actions edit, mark as used, deactivate, delete (confirm); CSV export; duplicate from edit | should | S51, S56 |
| FR-06-21 | Form shows live summary of the promo rule in plain language | could | S52 |
| FR-06-22 | Bulk promo sets: title, N codes generated uniquely, same rules, limits apply per code | could | S54, S55 |
| FR-06-23 | Pricing groups: name, colour (hex), list with customer count linking to filtered customers, edit, assign customers, delete (confirm) | must | S61 |
| FR-06-24 | Price resolution for a customer: if customer is in one or more groups with a price on the plan, use the lowest; else default price; plan missing default and no matching group = not purchasable by that customer | must | S203 ("group price in customer"), assumption |
| FR-06-25 | Tax and discount order of operations defined in PRD 08 (discount first, then tax) | must | S205d |
7. Screens
| Our screen | Modelled on | States |
|---|---|---|
| Packages list | S45, S194 | empty, filled, filtered, error |
| Category row + inline edit | S47 | new, editing, private |
| Package wizard (4 steps) | S190-S193 | blank, invalid, filled, draft resumed |
| Memberships list + wizard (3 steps) | S48, S57-S60 | same |
| Plan service/credit drawer | S59 | unlimited, limited |
| Promo codes list | S51 | empty, filled, filtered |
| Promo form (plan / drop-in) with summary card | S52, S53, S56 | blank, invalid, filled |
| Bulk promo sets (later) | S54, S55 | empty, form |
| Pricing groups list + add/edit drawer | S61 | empty, filled, delete confirm |
| All screens need loading skeletons (original is slow) and inline error. |
8. Flows
- Publish package (target ≤ 13 clicks): Add package → details → pricing → options (description mode required) → classes → Add. Edge: empty category cannot be created by typing in the picker (fix in ours: allow "create category" there); draft resumes at last completed step.
- Create promo (target ≤ 6 clicks): + Promo code → choose type → code + value → Add. Edge: duplicate code rejected inline.
- Manage categories: create → rename → drag to reorder → delete (items fall back to default).
- Redeem at checkout: code entered in cart (PRD 08/store) → validate dates, status, eligibility, limits, scope, min spend → apply discount capped by cap → increment used count on payment success, not on entry. Edge cases: promo expires mid-checkout → reject at confirm; plan set inactive while in cart → reject; deleting a plan with active customer entitlements keeps entitlements.
9. Data model (✱ = guess)
- PlanCategory(id, type package|membership, name, is_private, sort_order)
- Package(id, name, description, category_id, location_access all-chosen|customer-picks-1, location_ids, validity_value, validity_unit, activation purchase|first_booking, credits, price_type paid|free, purchase_restrict, purchase_limit, auto_group_id, auto_tag_id, booking_channel both|online|onsite, is_private, available_from/to, status, is_trial✱, max_per_class✱, day_time_rules✱)
- PackagePrice(package_id, pricing_group_id null=default, amount)
- PackageClass(package_id, class_id, credit_rate)
- Membership(… as spec in recon: frequency, duration/fixed range, activation, recurring fields, services[], purchase_restrict, auto group/tag, private, availability, status); MembershipService(membership_id, service_type, credit_mode, credits, rollover, item_ids, location_access); MembershipPrice(like PackagePrice)
- CustomerPlan (entitlement; shared with PRD 07/08): customer_id, plan_type, plan_id, credits_total, credits_left, starts_at, ends_at, transaction_id, status active|expired|inactive|voided
- PromoCode(id, code, type, value_type, amount, scope plan_ids|service_ids, duration, cycles✱, total_limit, per_customer_limit, min_spend, cap_amount, eligibility, eligibility_group_ids, start_at, end_at, status, used_count, set_id null); PromoCodeSet(id, title, count)
- PricingGroup(id, name, colour) + CustomerPricingGroup(customer_id, group_id) Currency and tax config come from PRD 01/08; amounts stored as integer minor units.
10. Business rules & edge cases
- Activation = first booking: validity end computed at first credit use; until then the plan has no end date. Activation = purchase: ends_at = purchase time + validity.
- Free classes cannot be covered by package credits.
- Editing a plan never changes existing customer entitlements (price, credits, validity are snapshotted at purchase).
- Plans set inactive, scheduled (before start) or past their end are hidden from sale; existing entitlements remain usable.
- Private plans hidden from store, sellable by staff or direct link.
- Purchase limit counts non-voided purchases per customer; a void frees the count.
- "First plan" eligibility = customer has no prior non-voided plan purchase; "non-members" = lifecycle (PRD 07).
- Rollover (membership): unused credits carry to the next cycle; only meaningful when recurring.
- Promo
used_countincrements only on a paid or confirmed transaction; voiding restores one use. - Percent promos round to minor units (half-up ✱); discount never exceeds the line total; fixed amount never below zero total.
- Code match is case-insensitive; codes unique per business including those in sets.
- A pricing group that is deleted is removed from customers and plan prices; plans with no default price and no remaining group price become inactive ✱ (confirm).
11. Permissions
| Action | Owner | Manager | Front desk | Customer |
|---|---|---|---|---|
| View plans/promos/groups | yes | yes | plans only | public plans |
| Create/edit/delete plans, categories, groups | yes | yes (if "Pricing Plans" permission) | no | no |
| Create/edit promo codes | yes | yes (promo permission, a single leaf) | no | no |
| Permissions are view-only vs allow-editing per node (S120 matrix, see PRD 01). |
12. Acceptance criteria
- Given a draft package with missing category, When I click Next on step 1, Then Next is disabled and the field is flagged.
- Given a package with 2 credits, activation on purchase, validity 1 month, When it is sold on 10 Nov, Then the customer plan shows 2/2 credits and ends on 10 Dec.
- Given activation on first booking, When sold, Then ends_at is empty; When first booking is made, Then ends_at = that date + validity.
- Given group "Student" price 40 and default 50, When a student is sold the package, Then price 40; a non-group customer 50.
- Given a promo with limit 1 per customer, When the same customer uses it twice, Then the second is rejected with a clear message.
- Given two simultaneous redemptions of the last remaining use, Then exactly one succeeds.
- Given promo type drop-in restricted to class X, When applied to a package cart, Then rejected.
- Given a category containing 3 plans, When deleted, Then the plans move to the default category.
- Given a plan with active customers, When set to inactive, Then it disappears from store/POS and customers can still use credits.
- Given a free class, When picking classes for a package, Then its checkbox is disabled.
13. Dependencies & open questions
Depends on PRD 01 (location required before any plan can be published; original blocks publishing without one), 02/04 (services to include), 07, 08. Open questions:
- Recurring memberships: which payment gateway and who runs renewals (see PRD 08 Q1)? Ship one-time only in v1?
- Package wizard step 4 credit rate semantics (credits per class) observed only as a field; confirm.
- Per-group price rows for memberships not verified in recon.
- Pricing tier gating of plans/promos: pricing tier: TBD.
- Multi-group conflict rule (lowest price) is our assumption.
- Promo "mark as used" meaning (manual consume) inferred from list menu only.
- Tax-inclusive vs exclusive plan prices: see PRD 08.
- Not observed: promo applied in store checkout; plan purchase from store; refunds of plans.
14. Evidence
See evidence/evidence.md.
