Draft migrations rewritten in place (PD-16, PD-18), with 0004 for the licence tables (PD-20) and rule tests in fuel-db/tests. Dashboards' column list regenerated; coverage_detail selects operator_uid. Checks: tests/run.sh 106 of 106; 18 views, 686 rows, 0 differences against a reference from the same corpus. Codex's fixes to its inspection are not re-inspected. Bands: persistence, Superset. Rules: 6, 8. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
92 lines
4.6 KiB
PL/PgSQL
92 lines
4.6 KiB
PL/PgSQL
-- 0004_licence.sql: customer licences (PD-20) and plan prices (PD-19), in schema `licence`.
|
|
-- DRAFT, not applied. Applied after 0003 by a loader acting as fuel_owner (README in this folder).
|
|
-- fuel_ro has no access: its default privileges cover `public` only. The server gets its own role
|
|
-- when it starts checking licences. No customer's licence is seeded here; our admins write those.
|
|
BEGIN;
|
|
CREATE SCHEMA licence AUTHORIZATION fuel_owner;
|
|
SET LOCAL search_path = licence, fm, pg_catalog, public; -- fm holds btree_gist
|
|
|
|
CREATE TABLE licence.plan (
|
|
plan_code text PRIMARY KEY,
|
|
name text NOT NULL
|
|
);
|
|
CREATE TABLE licence.plan_price ( -- per sensor per month (PD-19); a change is a new dated row
|
|
plan_code text NOT NULL REFERENCES licence.plan,
|
|
payment_term text NOT NULL CHECK (payment_term IN ('annual','monthly')),
|
|
currency text NOT NULL CHECK (currency ~ '^[A-Z]{3}$'),
|
|
price numeric(8,2) NOT NULL CHECK (price >= 0),
|
|
valid_from date NOT NULL, valid_to date, -- NULL to = still valid
|
|
EXCLUDE USING gist (plan_code WITH =, payment_term WITH =, currency WITH =,
|
|
daterange(valid_from, valid_to, '[)') WITH &&)
|
|
);
|
|
CREATE TABLE licence.plan_entitlement (
|
|
plan_code text NOT NULL REFERENCES licence.plan,
|
|
feature text NOT NULL,
|
|
limit_value integer, -- NULL: no limit
|
|
PRIMARY KEY (plan_code, feature)
|
|
);
|
|
CREATE TABLE licence.licence ( -- a renewal is a new row
|
|
licence_id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
|
|
operator_uid text NOT NULL REFERENCES fm.operator,
|
|
plan_code text NOT NULL REFERENCES licence.plan,
|
|
payment_term text NOT NULL CHECK (payment_term IN ('annual','monthly')),
|
|
valid_from date NOT NULL,
|
|
valid_until date NOT NULL, -- last valid day
|
|
grace_until date NOT NULL, -- last day of grace; alarms stop after it
|
|
status text NOT NULL CHECK (status IN ('active','suspended')),
|
|
contract_ref text NOT NULL, -- the signed agreement or amendment the dates come from
|
|
created_by text NOT NULL, -- a role, never a person's name
|
|
created_at timestamptz NOT NULL DEFAULT now(),
|
|
CHECK (valid_until >= valid_from),
|
|
CHECK (grace_until >= valid_until),
|
|
EXCLUDE USING gist (operator_uid WITH =, daterange(valid_from, valid_until, '[]') WITH &&)
|
|
);
|
|
CREATE TABLE licence.licence_override (
|
|
licence_id bigint NOT NULL REFERENCES licence.licence,
|
|
feature text NOT NULL,
|
|
limit_value integer, -- NULL: no limit
|
|
PRIMARY KEY (licence_id, feature)
|
|
);
|
|
|
|
-- Prices decided on 30 Oct 2026 (PD-19). The pilot plan costs nothing on either term.
|
|
INSERT INTO licence.plan (plan_code, name) VALUES ('pilot', 'Pilot'), ('standard', 'Standard');
|
|
INSERT INTO licence.plan_price (plan_code, payment_term, currency, price, valid_from) VALUES
|
|
('pilot', 'annual', 'USD', 0.00, '2026-01-01'),
|
|
('pilot', 'monthly', 'USD', 0.00, '2026-01-01'),
|
|
('standard', 'annual', 'USD', 15.00, '2026-10-30'),
|
|
('standard', 'monthly', 'USD', 17.00, '2026-10-30');
|
|
|
|
-- Every change is logged (PD-20), and the log is never changed.
|
|
CREATE TABLE licence.licence_event (
|
|
event_id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
|
|
table_name text NOT NULL,
|
|
action text NOT NULL CHECK (action IN ('INSERT','UPDATE','DELETE')),
|
|
old_row jsonb, new_row jsonb,
|
|
changed_by text NOT NULL DEFAULT current_user,
|
|
changed_at timestamptz NOT NULL DEFAULT now()
|
|
);
|
|
CREATE FUNCTION licence.tg_log_change() RETURNS trigger LANGUAGE plpgsql AS $$
|
|
BEGIN
|
|
INSERT INTO licence.licence_event (table_name, action, old_row, new_row)
|
|
VALUES (TG_TABLE_NAME, TG_OP,
|
|
CASE WHEN TG_OP <> 'INSERT' THEN to_jsonb(OLD) END,
|
|
CASE WHEN TG_OP <> 'DELETE' THEN to_jsonb(NEW) END);
|
|
RETURN NULL;
|
|
END $$;
|
|
DO $$
|
|
DECLARE t text;
|
|
BEGIN
|
|
FOREACH t IN ARRAY ARRAY['plan','plan_price','plan_entitlement','licence','licence_override'] LOOP
|
|
EXECUTE format('CREATE TRIGGER log_change AFTER INSERT OR UPDATE OR DELETE ON licence.%I
|
|
FOR EACH ROW EXECUTE FUNCTION licence.tg_log_change()', t);
|
|
END LOOP;
|
|
END $$;
|
|
CREATE FUNCTION licence.tg_event_immutable() RETURNS trigger LANGUAGE plpgsql AS $$
|
|
BEGIN
|
|
RAISE EXCEPTION 'licence_event: the log is never changed';
|
|
END $$;
|
|
CREATE TRIGGER event_immutable BEFORE UPDATE OR DELETE ON licence.licence_event
|
|
FOR EACH ROW EXECUTE FUNCTION licence.tg_event_immutable();
|
|
|
|
COMMIT;
|