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:
@@ -48,6 +48,7 @@ export const sq = {
|
||||
tariff: "Tarifa",
|
||||
tariffLab: "Lab Tarife",
|
||||
subscriptions: "Abonimet",
|
||||
plans: "Planet",
|
||||
site: "Park",
|
||||
users: "Përdoruesit",
|
||||
roles: "Rolet",
|
||||
@@ -393,9 +394,15 @@ export const sq = {
|
||||
cred: "kredencial",
|
||||
plates: "{{count}} targë(a)",
|
||||
noPrice: "pa çmim",
|
||||
perDay: "ditë",
|
||||
perWeek: "javë",
|
||||
perMonth: "muaj",
|
||||
monthlyPrice: "Çmimi mujor",
|
||||
pricePlaceholder: "p.sh. 10000",
|
||||
plan: "Plani",
|
||||
planNone: "— pa pagesë / falas —",
|
||||
planNoneAvail: "Asnjë plan i përcaktuar — admini duhet të krijojë një të parin.",
|
||||
quoting: "duke llogaritur…",
|
||||
quotePrompt: "zgjidh datën e mbarimit",
|
||||
quoteLine: "{{periods}} × {{unit}} · {{amount}} {{currency}}",
|
||||
tender: "Paguar me",
|
||||
tenderCash: "Para në dorë",
|
||||
tenderCard: "Kartë",
|
||||
@@ -415,11 +422,7 @@ export const sq = {
|
||||
limitCarsInAtOnce: "kufizo makinat brenda njëkohësisht",
|
||||
validFrom: "Vlen nga",
|
||||
validTo: "Vlen deri",
|
||||
months: "Muaj",
|
||||
monthsHint: "muaj të paguar",
|
||||
coverageHint: "deri më {{end}}",
|
||||
totalDue: "gjithsej {{total}}",
|
||||
validToOverride: "Vlen deri (manual)",
|
||||
validToEnd: "Vlen deri (mbarimi)",
|
||||
isoDateOptional: "Datë ISO (opsionale)",
|
||||
boundPlates: "Targat e lidhura",
|
||||
commaSeparatedOptional: "të ndara me presje (opsionale)",
|
||||
@@ -452,6 +455,29 @@ export const sq = {
|
||||
statusSuspended: "pezulluar",
|
||||
statusRevoked: "anuluar",
|
||||
},
|
||||
plans: {
|
||||
title: "Planet e abonimit",
|
||||
intro: "Planet i përcakton admini; operatori vetëm shet prej tyre — çmimi merret automatikisht, nuk shkruhet. Ndryshimi i një plani publikon një version të ri; shitjet e mëparshme ruajnë çmimin e tyre.",
|
||||
add: "+ Shto plan",
|
||||
noneYet: "Asnjë plan ende. Shto një që kabina të shesë abonime.",
|
||||
colName: "Emri",
|
||||
colPrice: "Çmimi",
|
||||
colEffective: "Vlen nga",
|
||||
inForce: "në fuqi",
|
||||
retired: "i tërhequr",
|
||||
newVersion: "Version i ri",
|
||||
retire: "Tërhiq",
|
||||
newTitle: "Plan i ri",
|
||||
newVersionTitle: "Publiko version të ri",
|
||||
newVersionHint: "Kjo publikon një version TË RI të planit — shitjet ekzistuese ruajnë çmimin origjinal.",
|
||||
period: "Periudha",
|
||||
pricePer: "Çmimi për periudhë",
|
||||
namePlaceholder: "p.sh. Hotel ditor",
|
||||
needName: "Emri i planit është i detyrueshëm.",
|
||||
needPrice: "Shkruaj një çmim më të madh se zero.",
|
||||
saved: "Plani u ruajt.",
|
||||
confirmRetire: "Të tërhiqet plani “{{name}}”? Nuk do të jetë më i shitshëm (historiku ruhet).",
|
||||
},
|
||||
site: {
|
||||
occupancy: "Prania:",
|
||||
noCapacitySet: "(pa kapacitet të caktuar)",
|
||||
@@ -460,7 +486,7 @@ export const sq = {
|
||||
capacityLabel: "Kapaciteti (bosh = pa kufi):",
|
||||
capacityPlaceholder: "p.sh. 120",
|
||||
printExitDefault: "Printo biletën e daljes si parazgjedhje",
|
||||
printExitHint: "(kabina larg daljes → klienti del vetë me biletë)",
|
||||
printExitHint: "(klienti skanon biletën në dalje)",
|
||||
parkDetails: "Të dhënat e parkimit (opsionale — shfaqen në bileta/fatura)",
|
||||
save: "Ruaj",
|
||||
saved: "U ruajt.",
|
||||
|
||||
Reference in New Issue
Block a user