docs: revise shop order-level discount design
Replace the ad-hoc manual order discount with a per-club catalogue of named OrderDiscountType presets snapshotted onto AppliedDiscount rows, with stacking, retirement, and an optional value override. Regenerate ARCHITECTURE.pdf. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -652,31 +652,44 @@ drops.*
|
|||||||
- Only applies when `today <= early_bird_deadline`; otherwise the line charges `list_price`.
|
- Only applies when `today <= early_bird_deadline`; otherwise the line charges `list_price`.
|
||||||
A `CheckConstraint` guarantees an enabled product has a deadline + type + value.
|
A `CheckConstraint` guarantees an enabled product has a deadline + type + value.
|
||||||
|
|
||||||
**B. Manual order-level discount — admin-applied, before finalize.**
|
**B. Order-level discount — selected from a club catalogue of presets.**
|
||||||
A `TREASURER`/`BOARD` (§3.2) applies an ad-hoc discount to a whole order — e.g. a multi-kid
|
Rather than typing a type + value + reason per order, each club **defines named presets once**
|
||||||
/ sibling discount — as a `PERCENT` or `AMOUNT` off the `subtotal`, with a required
|
as `OrderDiscountType` rows (e.g. *Sibling discount −15%*, *Volunteer −€25*, *Hardship*),
|
||||||
`manual_disc_reason` and audit stamp (`manual_disc_by` / `manual_disc_at`). It sits on the
|
managed under the club's shop settings. On a `pending` order a `TREASURER`/`BOARD` (§3.2)
|
||||||
`Order`, not on a product.
|
simply **toggles the applicable presets on** — each toggle creates an `AppliedDiscount` row.
|
||||||
- **Lifecycle refinement.** The manual discount forces the order to be *editable before it
|
No arithmetic is entered at order time; the treasurer picks from a list.
|
||||||
freezes*: checkout now creates the order as **`pending`**; a treasurer may set/clear the
|
- **Snapshot, like prices.** `AppliedDiscount` copies the preset's `label` / `disc_type` /
|
||||||
manual discount while `pending`; `finalize()` then locks the order, computes the final
|
`value` at apply time. Editing or retiring (`is_active=False`) an `OrderDiscountType` later
|
||||||
`total`, allocates the `Invoice.number`, and issues the PDF. **After `finalize` the order
|
never rewrites past orders — historical totals stay correct. `PROTECT` on the FK means a
|
||||||
and its discount are immutable** — a correction means a credit/refund, not an edit. The
|
used preset can't be hard-deleted; retire it instead.
|
||||||
access service gates this via `can_manage_shop(user, club)`.
|
- **Multiple discounts stack** — several presets can apply to one order (a preset toggles on
|
||||||
|
at most once, via `unique_together (order, discount_type)`). See stacking rule below.
|
||||||
|
- **Optional override.** The default flow enters *zero* numbers. If a club needs a one-off
|
||||||
|
amount (e.g. a bespoke hardship figure), allow the treasurer to override the snapshot
|
||||||
|
`value` on that `AppliedDiscount` — an opt-in escape hatch, not the primary path. A pure
|
||||||
|
ad-hoc discount is then just a generic "Custom" preset with an overridden value.
|
||||||
|
- **Lifecycle refinement.** Discounts force the order to be *editable before it freezes*:
|
||||||
|
checkout creates the order as **`pending`**; a treasurer toggles presets on/off while
|
||||||
|
`pending`; `finalize()` then locks the order + its `AppliedDiscount`s, computes the final
|
||||||
|
`total`, allocates the `Invoice.number`, and issues the PDF. **After `finalize` everything
|
||||||
|
is immutable** — a correction means a credit/refund, not an edit. Gated by
|
||||||
|
`can_manage_shop(user, club)`.
|
||||||
|
|
||||||
**Computation & rounding (both kinds).**
|
**Computation & rounding (both kinds).**
|
||||||
`total = subtotal − order_discount`, where `subtotal = Σ line_total` and each `line_total`
|
`total = subtotal − Σ applied_discounts`, where `subtotal = Σ line_total` and each
|
||||||
already reflects the early-bird price. Order of application: **line-level early-bird first,
|
`line_total` already reflects the early-bird price. Order of application: **line-level
|
||||||
then the order-level manual discount** on the resulting subtotal. Percentages compute on the
|
early-bird first, then all order-level presets**, each computed against the **same
|
||||||
base they apply to (unit price / subtotal), round **`ROUND_HALF_UP` to 2 decimals**, and are
|
`subtotal` base** (percentages don't compound on each other — predictable and order-
|
||||||
**clamped to `[0, base]`** so no line or order can go negative. Every discounted document
|
independent) and summed. Percentages compute on their base, round **`ROUND_HALF_UP` to 2
|
||||||
(order summary, invoice) shows list price, discount, and net so members see how the number
|
decimals**; the **summed** order discount is **clamped to `[0, subtotal]`** so an order can
|
||||||
was reached.
|
never go negative. Every discounted document (order summary, invoice) itemises each applied
|
||||||
|
discount by `label` plus the net so members see how the number was reached.
|
||||||
|
|
||||||
**Extension point.** Both are deliberately field-level, not a discount-row model — enough for
|
**Extension point.** `OrderDiscountType` is the reusable catalogue for the two required cases.
|
||||||
the two required cases. Coupon codes, stacked promotions, or per-member entitlements would
|
Coupon *codes* (member-entered), auto-applied promotions (rule-based, e.g. "3+ siblings"), or
|
||||||
warrant a first-class `Discount`/`Coupon` model + an M2M to orders; add it only when that need
|
per-member entitlements would extend this — add an eligibility rule / code field or an
|
||||||
is real.
|
auto-apply service on top of the same model when that need is real, rather than a parallel
|
||||||
|
mechanism.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -703,6 +716,7 @@ User 1───< Member (FK, unique per club) # User is GLOBAL — no clu
|
|||||||
└── (purchaser) ──< Order ───< OrderLine >── Product ──> Season
|
└── (purchaser) ──< Order ───< OrderLine >── Product ──> Season
|
||||||
│ └──> Member (beneficiary)
|
│ └──> Member (beneficiary)
|
||||||
├──< Payment
|
├──< Payment
|
||||||
|
├──< AppliedDiscount >── OrderDiscountType (club preset)
|
||||||
└─1:1─ Invoice (Cart ──< CartItem >── Product)
|
└─1:1─ Invoice (Cart ──< CartItem >── Product)
|
||||||
|
|
||||||
Season ──< Team, Event, ClubMembership, (membership/event) Product # all within one club
|
Season ──< Team, Event, ClubMembership, (membership/event) Product # all within one club
|
||||||
@@ -739,10 +753,12 @@ Legend: `───<` one-to-many, `>───<` many-to-many via a through model
|
|||||||
8. ✅ **Tenant resolution ≠ `django.contrib.sites`** — Sites evaluated and rejected as the
|
8. ✅ **Tenant resolution ≠ `django.contrib.sites`** — Sites evaluated and rejected as the
|
||||||
mechanism; `Club` stays the single tenant root, resolution in `ClubTenantMiddleware`
|
mechanism; `Club` stays the single tenant root, resolution in `ClubTenantMiddleware`
|
||||||
(§2.4). Sites optional only as a later bridge for Site-aware third parties.
|
(§2.4). Sites optional only as a later bridge for Site-aware third parties.
|
||||||
9. ✅ **Shop discounts** — two field-level mechanisms (§5.7.1): a per-`Product` early-bird
|
9. ✅ **Shop discounts** — two mechanisms (§5.7.1): a per-`Product` early-bird discount
|
||||||
discount (toggle + deadline + PERCENT/AMOUNT, frozen at checkout) and a manual order-level
|
(toggle + deadline + PERCENT/AMOUNT, frozen at checkout) and **order-level discounts
|
||||||
discount a treasurer applies to a `pending` order before `finalize()`. Adds an
|
selected from a club catalogue of `OrderDiscountType` presets** — a treasurer toggles
|
||||||
`Order.pending → finalized` step; no discount-row model yet.
|
presets on a `pending` order (each = a snapshotting `AppliedDiscount` row) before
|
||||||
|
`finalize()`, rather than typing values. Presets stack against the same subtotal base;
|
||||||
|
optional per-row value override for one-offs. Adds an `Order.pending → finalized` step.
|
||||||
|
|
||||||
Infrastructure/config for the above (media storage, dependencies + exact setup) is
|
Infrastructure/config for the above (media storage, dependencies + exact setup) is
|
||||||
specified in **§8**.
|
specified in **§8**.
|
||||||
|
|||||||
BIN
ARCHITECTURE.pdf
BIN
ARCHITECTURE.pdf
Binary file not shown.
Reference in New Issue
Block a user