feat: subscription plan catalog — config-defined pricing, dated spans, no typed amounts
Re-model subscription pricing from per-row, operator-typed prices into an admin-composed, versioned PLAN CATALOG (the tariff pattern). The operator now SELLS by picking a plan over a date span; the price is LOOKED UP, never typed — removing the fat-finger risk on a money field — and day/week/month periods make the hotel "guest stays 1–N days" case a daily plan over a check-in→check-out span. - Schema/migration 0010: new `subscription_plans` (immutable, effective-dated, keyed by a stable planId; period day/week/month + per-period price + active flag). `subscriptions` gains planId/planVersionId; period enum widened. Seeds a "Monthly" plan from the existing site default price (no data loss). - Pricing (pure, unit-tested in @parking/shared): periods = ceil(span / period), amount = periods × per-period price. Ceil = any started period is full (hotel practice). `resolvePlanVersion` picks the latest active version ≤ sale instant. - Backend: new admin-only plan CRUD (`subscription:plan` permission); reworked sell path derives the amount from the plan; `POST /api/subscriptions/quote` returns a server-computed quote so the operator can't override it. The signed-payment sale fix is unchanged — only the amount SOURCE moved; payload now carries planId/planVersionId/periods. Updates never re-sell (price frozen). - Frontend: SubscriptionManager sell form swaps the price field for a plan picker + start/end dates + a live quote line. New SubscriptionPlansManager (Setup tab) for the admin catalog. i18n (sq+en) for both. Verified on a copy of the live DB: 0010 applies (existing subs intact), a 3-night hotel sale prices to 2,400 ALL, appends one signed payment with planVersionId, chain verifies. Build+lint 12/12; 68 shared tests pass. Claude-Session: https://claude.ai/code/session_01Xcm6ikLgGoCxxHrxtjkk5V
This commit is contained in:
+21
@@ -1131,3 +1131,24 @@ UI: a "Takings so far" button on the shift control reveals a cyan X-report panel
|
||||
keeps the live drawer total. Verified on a copy of the live DB: matches drawerBalance(),
|
||||
drawer identity holds (expected = opening + cash + added − removed), 0 events appended, chain
|
||||
verifies. Build + lint 12/12. Resolves the X-report item flagged the same day in [[shift]].
|
||||
|
||||
## [2026-06-20] feat | Subscription plan catalog — config-defined, dated spans, no typed prices
|
||||
|
||||
Re-modelled subscription pricing from per-row operator-typed `priceMinor` + monthly-only `period`
|
||||
into an admin-composed, versioned PLAN CATALOG (the tariff pattern). Plans (`subscription_plans`,
|
||||
migration 0010) are immutable effective-dated versions keyed by a stable planId, with period ∈
|
||||
day/week/month + per-period price. The operator SELLS by selecting a plan over a date span (start
|
||||
defaults to today, end required); the price is LOOKED UP — periods = ceil(span / period), amount =
|
||||
periods × per-period price (ceil = any started period is full; hotel/parking practice). The hotel
|
||||
1–N day case is a daily plan over a check-in→check-out span. `POST /api/subscriptions/quote` gives a
|
||||
live server-computed quote so the operator can't override the amount. New `subscription:plan`
|
||||
permission (admin-only) composes the catalog; selling stays operator-grade `subscription:create`.
|
||||
The signed-`payment` sale fix is unchanged — only the amount SOURCE moved to the plan quote; payload
|
||||
now carries planId/planVersionId/periods. Pure span math lives + is unit-tested in @parking/shared
|
||||
(68 tests incl. ceil/Jan-31 clamp). New SubscriptionPlansManager screen (Setup tab) + reworked
|
||||
SubscriptionManager sell form (plan picker + dates + quote, no price field). Verified on a copy of
|
||||
the live DB: 0010 applies (existing subs intact, monthly plan seeds from site default), a 3-night
|
||||
hotel sale prices to 2,400 ALL, appends ONE signed payment with planVersionId, chain verifies.
|
||||
Build + lint 12/12. Updated [[subscription]] (plan catalog supersedes typed price; data model) +
|
||||
[[tariff]] (shared versioned-config pattern). The site default price column is kept only to seed the
|
||||
first plan.
|
||||
|
||||
Reference in New Issue
Block a user