-- 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;