fd4608a8f1
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
37 lines
1.9 KiB
SQL
37 lines
1.9 KiB
SQL
CREATE TABLE `subscription_plans` (
|
|
`id` text PRIMARY KEY NOT NULL,
|
|
`plan_id` text NOT NULL,
|
|
`name` text NOT NULL,
|
|
`period` text NOT NULL,
|
|
`price_per_period_minor` integer NOT NULL,
|
|
`currency` text NOT NULL,
|
|
`effective_from` text NOT NULL,
|
|
`active` integer DEFAULT true NOT NULL,
|
|
`created_by` text,
|
|
`created_at` text DEFAULT (current_timestamp) NOT NULL
|
|
);
|
|
--> statement-breakpoint
|
|
CREATE INDEX `subscription_plans_plan_id_idx` ON `subscription_plans` (`plan_id`);--> statement-breakpoint
|
|
-- A subscription now records WHICH plan + WHICH immutable plan version priced it, so
|
|
-- the sale reprices identically later (like a payment's tariff_version_id). Null for
|
|
-- legacy/comp rows. SQLite ALTER ADD COLUMN is in-place and safe for existing rows.
|
|
ALTER TABLE `subscriptions` ADD `plan_id` text;--> statement-breakpoint
|
|
ALTER TABLE `subscriptions` ADD `plan_version_id` text;--> statement-breakpoint
|
|
-- Seed a "Monthly" plan from the existing site default price so sites that already set
|
|
-- one keep their monthly plan with no data loss. period='month'; effective at epoch so
|
|
-- it always resolves. Skipped when no site default is set (no priced plan to seed).
|
|
INSERT INTO `subscription_plans`
|
|
(`id`, `plan_id`, `name`, `period`, `price_per_period_minor`, `currency`, `effective_from`, `active`)
|
|
SELECT
|
|
'plan-monthly-seed', 'monthly', 'Monthly', 'month',
|
|
`subscription_monthly_price_minor`,
|
|
'ALL',
|
|
'1970-01-01T00:00:00.000Z', 1
|
|
FROM `site_config`
|
|
WHERE `subscription_monthly_price_minor` IS NOT NULL
|
|
LIMIT 1;--> statement-breakpoint
|
|
-- Grant the new subscription:plan permission to the built-in admin role (admin is
|
|
-- runtime-special-cased to ALL permissions, but the Roles UI lists the grid from these
|
|
-- rows — keep it in sync). INSERT OR IGNORE: harmless if the row already exists.
|
|
INSERT OR IGNORE INTO `role_permissions` (`role_id`, `permission`) VALUES ('admin','subscription:plan');
|