Skip to content

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

  1. 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.
  2. Free classes cannot be covered by package credits.
  3. Editing a plan never changes existing customer entitlements (price, credits, validity are snapshotted at purchase).
  4. Plans set inactive, scheduled (before start) or past their end are hidden from sale; existing entitlements remain usable.
  5. Private plans hidden from store, sellable by staff or direct link.
  6. Purchase limit counts non-voided purchases per customer; a void frees the count.
  7. "First plan" eligibility = customer has no prior non-voided plan purchase; "non-members" = lifecycle (PRD 07).
  8. Rollover (membership): unused credits carry to the next cycle; only meaningful when recurring.
  9. Promo used_count increments only on a paid or confirmed transaction; voiding restores one use.
  10. Percent promos round to minor units (half-up ✱); discount never exceeds the line total; fixed amount never below zero total.
  11. Code match is case-insensitive; codes unique per business including those in sets.
  12. 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:

  1. Recurring memberships: which payment gateway and who runs renewals (see PRD 08 Q1)? Ship one-time only in v1?
  2. Package wizard step 4 credit rate semantics (credits per class) observed only as a field; confirm.
  3. Per-group price rows for memberships not verified in recon.
  4. Pricing tier gating of plans/promos: pricing tier: TBD.
  5. Multi-group conflict rule (lowest price) is our assumption.
  6. Promo "mark as used" meaning (manual consume) inferred from list menu only.
  7. Tax-inclusive vs exclusive plan prices: see PRD 08.
  8. Not observed: promo applied in store checkout; plan purchase from store; refunds of plans.

14. Evidence

See evidence/evidence.md.