Files
SGF/database/sql/industrial-costs.sql
T
2026-10-27 13:57:09 +02:00

107 lines
9.2 KiB
PL/PgSQL

CREATE TABLE ic_products (
id bigserial PRIMARY KEY,tenant_id bigint NOT NULL,company_id bigint NOT NULL,unit_id bigint NOT NULL,
reference text NOT NULL,name text NOT NULL,version integer NOT NULL CHECK(version=1),unit text NOT NULL CHECK(unit='UN'),
actor_id bigint NOT NULL REFERENCES users(id),created_at timestamptz NOT NULL,
UNIQUE(company_id,reference),FOREIGN KEY(tenant_id,company_id,unit_id) REFERENCES units(tenant_id,company_id,id)
);
CREATE TABLE ic_lots (
id bigserial PRIMARY KEY,tenant_id bigint NOT NULL,company_id bigint NOT NULL,unit_id bigint NOT NULL,
product_id bigint NOT NULL REFERENCES ic_products(id),reference text NOT NULL,centre text NOT NULL,
period_start date NOT NULL,period_end date NOT NULL CHECK(period_end>=period_start),currency text NOT NULL CHECK(currency='MZN'),
actor_id bigint NOT NULL REFERENCES users(id),created_at timestamptz NOT NULL,
UNIQUE(company_id,reference),FOREIGN KEY(tenant_id,company_id,unit_id) REFERENCES units(tenant_id,company_id,id)
);
CREATE TABLE ic_inputs (
id bigserial PRIMARY KEY,tenant_id bigint NOT NULL,company_id bigint NOT NULL,unit_id bigint NOT NULL,
lot_id bigint NOT NULL REFERENCES ic_lots(id),kind text NOT NULL CHECK(kind IN ('material','labour','pool','machine','production','material_rate','labour_rate','criterion')),
source_ref text NOT NULL CHECK(source_ref LIKE 'IND-TEST-%'),version integer NOT NULL CHECK(version>0),previous_id bigint UNIQUE REFERENCES ic_inputs(id),
data jsonb NOT NULL,evidence text NOT NULL CHECK(length(evidence)>=3),actor_id bigint NOT NULL REFERENCES users(id),created_at timestamptz NOT NULL,
UNIQUE(lot_id,kind,version),UNIQUE(company_id,source_ref,version),FOREIGN KEY(tenant_id,company_id,unit_id) REFERENCES units(tenant_id,company_id,id)
);
CREATE TABLE ic_results (
id bigserial PRIMARY KEY,tenant_id bigint NOT NULL,company_id bigint NOT NULL,unit_id bigint NOT NULL,
lot_id bigint NOT NULL REFERENCES ic_lots(id),version integer NOT NULL CHECK(version>0),previous_id bigint UNIQUE REFERENCES ic_results(id),
fingerprint text NOT NULL,snapshot jsonb NOT NULL,actor_id bigint NOT NULL REFERENCES users(id),created_at timestamptz NOT NULL,
UNIQUE(lot_id,version),FOREIGN KEY(tenant_id,company_id,unit_id) REFERENCES units(tenant_id,company_id,id)
);
CREATE TABLE ic_decisions (
id bigserial PRIMARY KEY,input_id bigint UNIQUE REFERENCES ic_inputs(id),result_id bigint UNIQUE REFERENCES ic_results(id),
version integer NOT NULL CHECK(version>0),decision text NOT NULL CHECK(decision IN ('approved','refused')),reason text NOT NULL CHECK(length(reason)>=3),
actor_id bigint NOT NULL REFERENCES users(id),created_at timestamptz NOT NULL,CHECK(num_nonnulls(input_id,result_id)=1)
);
CREATE TABLE ic_commands (
id bigserial PRIMARY KEY,tenant_id bigint NOT NULL,company_id bigint NOT NULL,unit_id bigint NOT NULL,
lot_id bigint NOT NULL REFERENCES ic_lots(id),actor_id bigint NOT NULL REFERENCES users(id),action text NOT NULL CHECK(action IN ('open','input','calculate','decide')),
command_key text NOT NULL,request_hash text NOT NULL,result jsonb NOT NULL,created_at timestamptz NOT NULL,
UNIQUE(company_id,unit_id,actor_id,action,command_key),FOREIGN KEY(tenant_id,company_id,unit_id) REFERENCES units(tenant_id,company_id,id)
);
CREATE TABLE ic_events(id bigserial PRIMARY KEY,command_id bigint NOT NULL UNIQUE REFERENCES ic_commands(id),schema_version integer NOT NULL CHECK(schema_version=1),payload jsonb NOT NULL,created_at timestamptz NOT NULL);
CREATE TABLE ic_outbox(event_id bigint PRIMARY KEY REFERENCES ic_events(id),state text NOT NULL CHECK(state='captured_no_stock_or_accounting'));
CREATE OR REPLACE FUNCTION sgf_ic_environment() RETURNS void LANGUAGE plpgsql AS $$
BEGIN IF current_database() IS DISTINCT FROM 'sgf_tempo_test' OR inet_server_port() IS DISTINCT FROM 55432 THEN RAISE EXCEPTION 'SGF_IC_TEST_DATABASE_ONLY'; END IF; END; $$;
CREATE OR REPLACE FUNCTION sgf_ic_test_only() RETURNS trigger LANGUAGE plpgsql AS $$
BEGIN PERFORM sgf_ic_environment(); RETURN NEW; END; $$;
CREATE OR REPLACE FUNCTION sgf_ic_input_current(iid bigint,lid bigint,ver integer) RETURNS void LANGUAGE plpgsql AS $$
BEGIN
IF NOT EXISTS(SELECT 1 FROM ic_inputs i JOIN ic_decisions d ON d.input_id=i.id AND d.version=i.version AND d.decision='approved' WHERE i.id=iid AND i.lot_id=lid AND i.version=ver)
OR EXISTS(SELECT 1 FROM ic_inputs WHERE previous_id=iid) THEN RAISE EXCEPTION 'SGF_IC_INPUT_NOT_CURRENT_APPROVED'; END IF;
END; $$;
CREATE OR REPLACE FUNCTION sgf_ic_guard() RETURNS trigger LANGUAGE plpgsql AS $$
DECLARE l ic_lots; p record; target record; i ic_inputs; v integer; lid bigint; ref jsonb;
BEGIN
PERFORM sgf_ic_environment();
IF TG_TABLE_NAME='ic_products' THEN PERFORM 1 FROM companies WHERE id=NEW.company_id FOR UPDATE; RETURN NEW; END IF;
IF TG_TABLE_NAME='ic_lots' THEN
PERFORM 1 FROM companies WHERE id=NEW.company_id FOR UPDATE;
IF NOT EXISTS(SELECT 1 FROM ic_products WHERE id=NEW.product_id AND ROW(tenant_id,company_id,unit_id)=ROW(NEW.tenant_id,NEW.company_id,NEW.unit_id)) THEN RAISE EXCEPTION 'SGF_IC_PRODUCT_SCOPE'; END IF; RETURN NEW;
END IF;
IF TG_TABLE_NAME='ic_decisions' THEN
IF NEW.input_id IS NOT NULL THEN SELECT lot_id INTO STRICT lid FROM ic_inputs WHERE id=NEW.input_id;
ELSE SELECT lot_id INTO STRICT lid FROM ic_results WHERE id=NEW.result_id; END IF;
ELSE lid=NEW.lot_id; END IF;
SELECT * INTO STRICT l FROM ic_lots WHERE id=lid; PERFORM 1 FROM companies WHERE id=l.company_id FOR UPDATE;
IF TG_TABLE_NAME='ic_decisions' THEN
IF NEW.input_id IS NOT NULL THEN
SELECT id,version,actor_id INTO STRICT target FROM ic_inputs WHERE id=NEW.input_id;
IF EXISTS(SELECT 1 FROM ic_inputs WHERE previous_id=target.id) THEN RAISE EXCEPTION 'SGF_IC_OLD_INPUT'; END IF;
ELSE
SELECT id,version,actor_id INTO STRICT target FROM ic_results WHERE id=NEW.result_id;
IF EXISTS(SELECT 1 FROM ic_results WHERE previous_id=target.id) THEN RAISE EXCEPTION 'SGF_IC_OLD_RESULT'; END IF;
FOR ref IN SELECT value FROM ic_results r,jsonb_each(r.snapshot->'refs') WHERE r.id=target.id LOOP
IF ref<>'null'::jsonb THEN PERFORM sgf_ic_input_current((ref->>'id')::bigint,lid,(ref->>'version')::integer); END IF;
END LOOP;
END IF;
IF target.actor_id=NEW.actor_id OR target.version<>NEW.version THEN RAISE EXCEPTION 'SGF_IC_SEPARATION_VERSION'; END IF; RETURN NEW;
END IF;
IF ROW(NEW.tenant_id,NEW.company_id,NEW.unit_id) IS DISTINCT FROM ROW(l.tenant_id,l.company_id,l.unit_id) THEN RAISE EXCEPTION 'SGF_IC_SCOPE'; END IF;
IF TG_TABLE_NAME='ic_commands' THEN RETURN NEW; END IF;
IF TG_TABLE_NAME='ic_inputs' THEN
SELECT coalesce(max(version),0) INTO v FROM ic_inputs WHERE lot_id=lid AND kind=NEW.kind;
IF NEW.version<>v+1 OR (v=0 AND NEW.previous_id IS NOT NULL) THEN RAISE EXCEPTION 'SGF_IC_VERSION'; END IF;
IF v>0 THEN SELECT * INTO STRICT p FROM ic_inputs WHERE id=NEW.previous_id AND lot_id=lid AND kind=NEW.kind AND version=v;
IF p.source_ref<>NEW.source_ref THEN RAISE EXCEPTION 'SGF_IC_ORIGIN_TRANSFER'; END IF;
END IF;
IF NEW.kind IN ('material','labour','machine','production') AND ((NEW.data->>'quantity')::numeric<=0 OR (NEW.data->>'quantity')::numeric<>trunc((NEW.data->>'quantity')::numeric,8)) THEN RAISE EXCEPTION 'SGF_IC_QUANTITY'; END IF;
IF NEW.kind='criterion' AND ((NEW.data->>'capacity')::numeric<=0 OR NEW.data->>'driver_unit' IS DISTINCT FROM 'HM' OR NEW.data->>'hypothesis' IS DISTINCT FROM 'synthetic_only') THEN RAISE EXCEPTION 'SGF_IC_CAPACITY'; END IF;
IF NEW.kind IN ('material_rate','labour_rate') THEN
SELECT * INTO STRICT i FROM ic_inputs WHERE id=(NEW.data->>'source_id')::bigint AND lot_id=lid AND version=(NEW.data->>'source_version')::integer;
IF i.kind<>replace(NEW.kind,'_rate','') OR (NEW.data->>'amount')::numeric<0 OR NEW.data->>'currency' IS DISTINCT FROM 'MZN' OR NEW.data->>'unit' IS DISTINCT FROM (CASE WHEN i.kind='material' THEN 'KG' ELSE 'H' END) THEN RAISE EXCEPTION 'SGF_IC_RATE'; END IF;
END IF;
ELSE
SELECT coalesce(max(version),0) INTO v FROM ic_results WHERE lot_id=lid;
IF NEW.version<>v+1 OR NEW.snapshot->>'policy' IS DISTINCT FROM 'INDUSTRIAL-COST-SIM-v1' OR NEW.snapshot->>'currency' IS DISTINCT FROM 'MZN' THEN RAISE EXCEPTION 'SGF_IC_VERSION_POLICY'; END IF;
IF v=0 THEN IF NEW.previous_id IS NOT NULL THEN RAISE EXCEPTION 'SGF_IC_PREVIOUS'; END IF;
ELSE PERFORM 1 FROM ic_results WHERE id=NEW.previous_id AND lot_id=lid AND version=v; IF NOT FOUND THEN RAISE EXCEPTION 'SGF_IC_PREVIOUS'; END IF; END IF;
FOR ref IN SELECT value FROM jsonb_each(NEW.snapshot->'refs') LOOP
IF ref<>'null'::jsonb THEN PERFORM sgf_ic_input_current((ref->>'id')::bigint,lid,(ref->>'version')::integer); END IF;
END LOOP;
END IF;
RETURN NEW;
EXCEPTION WHEN NO_DATA_FOUND THEN RAISE EXCEPTION 'SGF_IC_REFERENCE'; END; $$;
CREATE TRIGGER ic_product_integrity BEFORE INSERT ON ic_products FOR EACH ROW EXECUTE FUNCTION sgf_ic_guard();
CREATE TRIGGER ic_lot_integrity BEFORE INSERT ON ic_lots FOR EACH ROW EXECUTE FUNCTION sgf_ic_guard();
CREATE TRIGGER ic_input_integrity BEFORE INSERT ON ic_inputs FOR EACH ROW EXECUTE FUNCTION sgf_ic_guard();
CREATE TRIGGER ic_result_integrity BEFORE INSERT ON ic_results FOR EACH ROW EXECUTE FUNCTION sgf_ic_guard();
CREATE TRIGGER ic_decision_integrity BEFORE INSERT ON ic_decisions FOR EACH ROW EXECUTE FUNCTION sgf_ic_guard();
CREATE TRIGGER ic_command_integrity BEFORE INSERT ON ic_commands FOR EACH ROW EXECUTE FUNCTION sgf_ic_guard();