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