commit melhorias sgf

This commit is contained in:
2026-11-11 09:00:17 +02:00
parent 0b05a1192c
commit 1d11a5e57b
736 changed files with 40746 additions and 210 deletions
+31
View File
@@ -0,0 +1,31 @@
CREATE TABLE br_statements(id bigserial PRIMARY KEY,tenant_id bigint NOT NULL,company_id bigint NOT NULL,unit_id bigint NOT NULL,statement_key uuid NOT NULL,account_ref text NOT NULL,currency text NOT NULL CHECK(currency='MZN'),period text NOT NULL,csv_sha256 char(64) NOT NULL,csv_bytes text NOT NULL,opening_minor bigint NOT NULL,closing_minor bigint NOT NULL,entries jsonb NOT NULL,source_hash char(64) NOT NULL,actor_id bigint NOT NULL REFERENCES users(id),reason text NOT NULL CHECK(length(reason)>=10),created_at timestamptz NOT NULL DEFAULT now(),UNIQUE(id,tenant_id,company_id,unit_id),UNIQUE(tenant_id,company_id,unit_id,statement_key),UNIQUE(tenant_id,company_id,unit_id,account_ref,csv_sha256),FOREIGN KEY(tenant_id,company_id,unit_id) REFERENCES units(tenant_id,company_id,id));
CREATE TABLE br_events(id bigserial PRIMARY KEY,tenant_id bigint NOT NULL,company_id bigint NOT NULL,unit_id bigint NOT NULL,statement_id bigint NOT NULL,version bigint NOT NULL CHECK(version>0),previous_id bigint UNIQUE REFERENCES br_events(id),proposal_id bigint UNIQUE REFERENCES br_events(id),action text NOT NULL CHECK(action IN('import','review_statement','prepare_match','review_match','finalize')),state text NOT NULL CHECK(state IN('imported','statement_reviewed','statement_refused','match_prepared','match_reviewed','match_refused','finalized')),prepared_by bigint NOT NULL REFERENCES users(id),actor_id bigint NOT NULL REFERENCES users(id),data jsonb NOT NULL,reason text NOT NULL CHECK(length(reason)>=10),created_at timestamptz NOT NULL DEFAULT now(),UNIQUE(statement_id,version),FOREIGN KEY(statement_id,tenant_id,company_id,unit_id) REFERENCES br_statements(id,tenant_id,company_id,unit_id));
CREATE UNIQUE INDEX br_once_ledger_line ON br_events(tenant_id,company_id,unit_id,((data->>'ledger_line_id')::bigint)) WHERE state='match_reviewed';
CREATE UNIQUE INDEX br_once_statement_row ON br_events(statement_id,(data->>'row_reference')) WHERE state='match_reviewed';
CREATE TABLE br_commands(id bigserial PRIMARY KEY,tenant_id bigint NOT NULL,company_id bigint NOT NULL,unit_id bigint NOT NULL,actor_id bigint NOT NULL REFERENCES users(id),command_key uuid NOT NULL,request_hash char(64) NOT NULL,result jsonb NOT NULL,created_at timestamptz NOT NULL DEFAULT now(),UNIQUE(tenant_id,company_id,unit_id,actor_id,command_key));
CREATE FUNCTION sgf_br_guard() RETURNS trigger LANGUAGE plpgsql AS $$
DECLARE permission_name text;lastrow br_events;p br_events;header br_statements;
BEGIN
PERFORM sgf_wo_environment();PERFORM 1 FROM companies WHERE tenant_id=NEW.tenant_id AND id=NEW.company_id FOR UPDATE;
IF TG_TABLE_NAME='br_statements' THEN permission_name:='bank_reconciliation.import';ELSE permission_name:='bank_reconciliation.'||CASE NEW.action WHEN 'import' THEN 'import' WHEN 'prepare_match' THEN 'prepare' ELSE 'review' END;END IF;
IF NOT EXISTS(SELECT 1 FROM company_memberships WHERE user_id=NEW.actor_id AND tenant_id=NEW.tenant_id AND company_id=NEW.company_id) OR NOT EXISTS(SELECT 1 FROM permission_grants WHERE user_id=NEW.actor_id AND tenant_id=NEW.tenant_id AND company_id=NEW.company_id AND unit_id=NEW.unit_id AND permission=permission_name) THEN RAISE EXCEPTION 'SGF_BR_ACCESS';END IF;
IF TG_TABLE_NAME='br_statements' THEN RETURN NEW;END IF;
SELECT * INTO header FROM br_statements WHERE id=NEW.statement_id AND (tenant_id,company_id,unit_id)=(NEW.tenant_id,NEW.company_id,NEW.unit_id);IF header.id IS NULL THEN RAISE EXCEPTION 'SGF_BR_SCOPE';END IF;
SELECT * INTO lastrow FROM br_events WHERE statement_id=NEW.statement_id ORDER BY version DESC LIMIT 1;IF NEW.version<>coalesce(lastrow.version,0)+1 OR NEW.previous_id IS DISTINCT FROM lastrow.id OR EXISTS(SELECT 1 FROM br_events WHERE statement_id=NEW.statement_id AND state='finalized') THEN RAISE EXCEPTION 'SGF_BR_STALE_OR_FINAL';END IF;
IF NEW.action='import' THEN IF lastrow.id IS NOT NULL OR NEW.state<>'imported' OR NEW.actor_id<>header.actor_id THEN RAISE EXCEPTION 'SGF_BR_IMPORT';END IF;
ELSIF NEW.action='review_statement' THEN IF NEW.actor_id=header.actor_id OR NEW.prepared_by<>header.actor_id OR NEW.state NOT IN('statement_reviewed','statement_refused') OR EXISTS(SELECT 1 FROM br_events WHERE statement_id=NEW.statement_id AND action='review_statement') THEN RAISE EXCEPTION 'SGF_BR_REVIEW';END IF;
ELSE IF NOT EXISTS(SELECT 1 FROM br_events WHERE statement_id=NEW.statement_id AND state='statement_reviewed') THEN RAISE EXCEPTION 'SGF_BR_NOT_REVIEWED';END IF;
IF NEW.action='prepare_match' THEN IF NEW.actor_id<>NEW.prepared_by OR NEW.state<>'match_prepared' THEN RAISE EXCEPTION 'SGF_BR_PREPARE';END IF;
ELSIF NEW.action='review_match' THEN SELECT * INTO p FROM br_events WHERE id=NEW.proposal_id AND statement_id=NEW.statement_id AND state='match_prepared';IF p.id IS NULL OR NEW.actor_id=p.actor_id OR NEW.prepared_by<>p.prepared_by OR NEW.data IS DISTINCT FROM p.data OR NEW.state NOT IN('match_reviewed','match_refused') THEN RAISE EXCEPTION 'SGF_BR_MATCH_REVIEW';END IF;
ELSIF NEW.action='finalize' THEN IF NEW.actor_id=header.actor_id OR NEW.state<>'finalized' OR (SELECT count(*) FROM br_events WHERE statement_id=NEW.statement_id AND state='match_reviewed')<>jsonb_array_length(header.entries) OR EXISTS(SELECT 1 FROM br_events proposed WHERE proposed.statement_id=NEW.statement_id AND proposed.state='match_prepared' AND NOT EXISTS(SELECT 1 FROM br_events r WHERE r.proposal_id=proposed.id)) THEN RAISE EXCEPTION 'SGF_BR_INCOMPLETE';END IF;END IF;END IF;
RETURN NEW;
END; $$;
CREATE TRIGGER br_statement_guard BEFORE INSERT ON br_statements FOR EACH ROW EXECUTE FUNCTION sgf_br_guard();
CREATE TRIGGER br_event_guard BEFORE INSERT ON br_events FOR EACH ROW EXECUTE FUNCTION sgf_br_guard();
CREATE TRIGGER br_statements_immutable BEFORE UPDATE OR DELETE ON br_statements FOR EACH ROW EXECUTE FUNCTION sgf_append_only_audit();
CREATE TRIGGER br_events_immutable BEFORE UPDATE OR DELETE ON br_events FOR EACH ROW EXECUTE FUNCTION sgf_append_only_audit();
CREATE TRIGGER br_commands_immutable BEFORE UPDATE OR DELETE ON br_commands FOR EACH ROW EXECUTE FUNCTION sgf_append_only_audit();
CREATE TRIGGER br_commands_test BEFORE INSERT ON br_commands FOR EACH ROW EXECUTE FUNCTION sgf_wo_test_only();
REVOKE ALL ON br_statements,br_events,br_commands FROM PUBLIC;
REVOKE ALL ON SEQUENCE br_statements_id_seq,br_events_id_seq,br_commands_id_seq FROM PUBLIC;
REVOKE ALL ON FUNCTION sgf_br_guard() FROM PUBLIC;
+24
View File
@@ -0,0 +1,24 @@
CREATE TABLE cash_events(id bigserial PRIMARY KEY,tenant_id bigint NOT NULL,company_id bigint NOT NULL,unit_id bigint NOT NULL,session_key uuid NOT NULL,version bigint NOT NULL CHECK(version>0),action text NOT NULL CHECK(action IN('prepare','review','close')),kind text NOT NULL CHECK(kind IN('open','receipt','expense','count')),state text NOT NULL CHECK(state IN('proposed','approved','refused','closed')),proposal_id bigint REFERENCES cash_events(id),source_receipt_id bigint REFERENCES sr_records(id),actor_id bigint NOT NULL REFERENCES users(id),data jsonb NOT NULL,dependencies jsonb NOT NULL,reason text NOT NULL CHECK(length(reason)>=10),created_at timestamptz NOT NULL DEFAULT now(),UNIQUE(tenant_id,company_id,unit_id,session_key,version),FOREIGN KEY(tenant_id,company_id,unit_id) REFERENCES units(tenant_id,company_id,id));
CREATE UNIQUE INDEX cash_one_review ON cash_events(proposal_id) WHERE action='review';
CREATE UNIQUE INDEX cash_one_receipt ON cash_events(source_receipt_id) WHERE action='review' AND kind='receipt' AND state='approved';
CREATE UNIQUE INDEX cash_one_close ON cash_events(tenant_id,company_id,unit_id,session_key) WHERE action='close';
CREATE UNIQUE INDEX cash_one_open ON cash_events(tenant_id,company_id,unit_id,session_key) WHERE action='review' AND kind='open' AND state='approved';
CREATE TABLE cash_commands(id bigserial PRIMARY KEY,tenant_id bigint NOT NULL,company_id bigint NOT NULL,unit_id bigint NOT NULL,actor_id bigint NOT NULL REFERENCES users(id),command_key uuid NOT NULL,request_hash char(64) NOT NULL,result jsonb NOT NULL,created_at timestamptz NOT NULL DEFAULT now(),UNIQUE(tenant_id,company_id,unit_id,actor_id,command_key));
CREATE FUNCTION sgf_cash_guard() RETURNS trigger LANGUAGE plpgsql AS $$
DECLARE latest bigint;p cash_events;permission_name text;
BEGIN
PERFORM sgf_wo_environment();PERFORM 1 FROM companies WHERE id=NEW.company_id AND tenant_id=NEW.tenant_id FOR UPDATE;
permission_name:='cash.'||CASE NEW.action WHEN 'prepare' THEN 'prepare' WHEN 'review' THEN 'review' ELSE 'close' END;
IF NOT EXISTS(SELECT 1 FROM permission_grants WHERE user_id=NEW.actor_id AND tenant_id=NEW.tenant_id AND company_id=NEW.company_id AND unit_id=NEW.unit_id AND permission=permission_name) THEN RAISE EXCEPTION 'SGF_CASH_ACCESS'; END IF;
SELECT coalesce(max(version),0) INTO latest FROM cash_events WHERE (tenant_id,company_id,unit_id,session_key)=(NEW.tenant_id,NEW.company_id,NEW.unit_id,NEW.session_key);
IF NEW.version<>latest+1 OR EXISTS(SELECT 1 FROM cash_events WHERE (tenant_id,company_id,unit_id,session_key)=(NEW.tenant_id,NEW.company_id,NEW.unit_id,NEW.session_key) AND state='closed') THEN RAISE EXCEPTION 'SGF_CASH_STALE_OR_CLOSED'; END IF;
IF NEW.action='review' THEN SELECT * INTO STRICT p FROM cash_events WHERE id=NEW.proposal_id AND action='prepare';IF ROW(p.tenant_id,p.company_id,p.unit_id,p.session_key,p.kind) IS DISTINCT FROM ROW(NEW.tenant_id,NEW.company_id,NEW.unit_id,NEW.session_key,NEW.kind) OR p.actor_id=NEW.actor_id OR NEW.data IS DISTINCT FROM p.data OR NEW.source_receipt_id IS DISTINCT FROM p.source_receipt_id THEN RAISE EXCEPTION 'SGF_CASH_SELF_OR_SOURCE'; END IF; END IF;
RETURN NEW;
END; $$;
CREATE TRIGGER cash_guard BEFORE INSERT ON cash_events FOR EACH ROW EXECUTE FUNCTION sgf_cash_guard();
CREATE TRIGGER cash_events_immutable BEFORE UPDATE OR DELETE ON cash_events FOR EACH ROW EXECUTE FUNCTION sgf_append_only_audit();
CREATE TRIGGER cash_commands_immutable BEFORE UPDATE OR DELETE ON cash_commands FOR EACH ROW EXECUTE FUNCTION sgf_append_only_audit();
CREATE TRIGGER cash_commands_test BEFORE INSERT ON cash_commands FOR EACH ROW EXECUTE FUNCTION sgf_wo_test_only();
REVOKE ALL ON cash_events,cash_commands FROM PUBLIC;
REVOKE ALL ON SEQUENCE cash_events_id_seq,cash_commands_id_seq FROM PUBLIC;
REVOKE ALL ON FUNCTION sgf_cash_guard() FROM PUBLIC;
+26
View File
@@ -0,0 +1,26 @@
CREATE TABLE cq_versions(
id bigserial PRIMARY KEY,tenant_id bigint NOT NULL,company_id bigint NOT NULL,unit_id bigint NOT NULL,
quote_key uuid NOT NULL,version int NOT NULL CHECK(version>0),previous_id bigint UNIQUE REFERENCES cq_versions(id),
action text NOT NULL CHECK(action IN('prepare','review','customer','convert')),state text NOT NULL CHECK(state IN('draft','approved','rejected','customer_accepted','customer_declined','converted')),
client_id bigint NOT NULL REFERENCES a_clients(id),source_order_id bigint REFERENCES wo_orders(id),prepared_by bigint NOT NULL REFERENCES users(id),actor_id bigint NOT NULL REFERENCES users(id),
data jsonb NOT NULL,dependencies jsonb NOT NULL,reason text NOT NULL CHECK(length(reason)>=10),created_at timestamptz NOT NULL DEFAULT now(),
UNIQUE(tenant_id,company_id,unit_id,quote_key,version),FOREIGN KEY(tenant_id,company_id,unit_id) REFERENCES units(tenant_id,company_id,id)
);
CREATE TABLE cq_commands(id bigserial PRIMARY KEY,tenant_id bigint NOT NULL,company_id bigint NOT NULL,unit_id bigint NOT NULL,actor_id bigint NOT NULL REFERENCES users(id),command_key uuid NOT NULL,request_hash char(64) NOT NULL,result jsonb NOT NULL,created_at timestamptz NOT NULL DEFAULT now(),UNIQUE(tenant_id,company_id,unit_id,actor_id,command_key));
CREATE TABLE cq_conversions(id bigserial PRIMARY KEY,tenant_id bigint NOT NULL,company_id bigint NOT NULL,unit_id bigint NOT NULL,quote_key uuid NOT NULL,quote_version_id bigint NOT NULL UNIQUE REFERENCES cq_versions(id),service_draft_id bigint NOT NULL UNIQUE REFERENCES a_service_drafts(id),billing_draft_id bigint NOT NULL UNIQUE REFERENCES a_billing_drafts(id),source_order_id bigint UNIQUE REFERENCES wo_orders(id),actor_id bigint NOT NULL REFERENCES users(id),created_at timestamptz NOT NULL DEFAULT now(),UNIQUE(tenant_id,company_id,unit_id,quote_key));
CREATE FUNCTION sgf_cq_guard() RETURNS trigger LANGUAGE plpgsql AS $$
DECLARE p cq_versions; right_name text;
BEGIN
PERFORM sgf_wo_environment();PERFORM 1 FROM companies WHERE id=NEW.company_id FOR UPDATE;
right_name:=CASE NEW.action WHEN 'review' THEN 'commercial_quotes.review' WHEN 'convert' THEN 'commercial_quotes.convert' ELSE 'commercial_quotes.prepare' END;
IF NOT EXISTS(SELECT 1 FROM permission_grants WHERE user_id=NEW.actor_id AND tenant_id=NEW.tenant_id AND company_id=NEW.company_id AND unit_id=NEW.unit_id AND permission=right_name)
OR NOT EXISTS(SELECT 1 FROM a_clients WHERE id=NEW.client_id AND tenant_id=NEW.tenant_id AND company_id=NEW.company_id)
THEN RAISE EXCEPTION 'SGF_CQ_AUTHORITY_OR_SCOPE'; END IF;
SELECT * INTO p FROM cq_versions WHERE (tenant_id,company_id,unit_id,quote_key)=(NEW.tenant_id,NEW.company_id,NEW.unit_id,NEW.quote_key) ORDER BY version DESC LIMIT 1;
IF (p.id IS NULL AND (NEW.version<>1 OR NEW.previous_id IS NOT NULL OR NEW.action<>'prepare')) OR (p.id IS NOT NULL AND (NEW.previous_id IS DISTINCT FROM p.id OR NEW.version<>p.version+1 OR p.state='converted')) THEN RAISE EXCEPTION 'SGF_CQ_STALE_OR_CONVERTED'; END IF;
IF NEW.action='review' AND (p.state<>'draft' OR NEW.actor_id=NEW.prepared_by) THEN RAISE EXCEPTION 'SGF_CQ_SELF_OR_STALE_REVIEW'; END IF;
IF NEW.action='customer' AND p.state<>'approved' THEN RAISE EXCEPTION 'SGF_CQ_CUSTOMER_BEFORE_REVIEW'; END IF;
IF NEW.action='convert' AND (p.state<>'customer_accepted' OR (NEW.data->>'valid_until')::date<(now() AT TIME ZONE 'Africa/Maputo')::date) THEN RAISE EXCEPTION 'SGF_CQ_NOT_CONVERTIBLE'; END IF;
RETURN NEW;
END; $$;
CREATE TRIGGER sgf_cq_guard BEFORE INSERT ON cq_versions FOR EACH ROW EXECUTE FUNCTION sgf_cq_guard();
+25
View File
@@ -0,0 +1,25 @@
CREATE TABLE an_criteria(id bigserial PRIMARY KEY,tenant_id bigint NOT NULL,company_id bigint NOT NULL,unit_id bigint NOT NULL,metric text NOT NULL CHECK(metric IN('cash_coverage_single_payable','cash_expense_share')),version bigint NOT NULL CHECK(version>0),previous_id bigint UNIQUE REFERENCES an_criteria(id),action text NOT NULL CHECK(action IN('prepare','review')),state text NOT NULL CHECK(state IN('prepared','reviewed','refused')),prepared_by bigint NOT NULL REFERENCES users(id),actor_id bigint NOT NULL REFERENCES users(id),definition jsonb NOT NULL,source_hash char(64) NOT NULL,reason text NOT NULL CHECK(length(reason)>=10),created_at timestamptz NOT NULL DEFAULT now(),UNIQUE(tenant_id,company_id,unit_id,metric,version),FOREIGN KEY(tenant_id,company_id,unit_id) REFERENCES units(tenant_id,company_id,id));
CREATE TABLE an_results(id bigserial PRIMARY KEY,tenant_id bigint NOT NULL,company_id bigint NOT NULL,unit_id bigint NOT NULL,actor_id bigint NOT NULL REFERENCES users(id),criterion_id bigint REFERENCES an_criteria(id),input jsonb NOT NULL,result jsonb NOT NULL,source_snapshot jsonb NOT NULL,reason text NOT NULL CHECK(length(reason)>=10),created_at timestamptz NOT NULL DEFAULT now(),FOREIGN KEY(tenant_id,company_id,unit_id) REFERENCES units(tenant_id,company_id,id));
CREATE TABLE an_commands(id bigserial PRIMARY KEY,tenant_id bigint NOT NULL,company_id bigint NOT NULL,unit_id bigint NOT NULL,actor_id bigint NOT NULL REFERENCES users(id),command_key uuid NOT NULL,request_hash char(64) NOT NULL,result jsonb NOT NULL,created_at timestamptz NOT NULL DEFAULT now(),UNIQUE(tenant_id,company_id,unit_id,actor_id,command_key));
CREATE FUNCTION sgf_an_guard() RETURNS trigger LANGUAGE plpgsql AS $$
DECLARE p an_criteria;permission_name text;
BEGIN
PERFORM sgf_wo_environment();PERFORM 1 FROM companies WHERE tenant_id=NEW.tenant_id AND id=NEW.company_id FOR UPDATE;
IF TG_TABLE_NAME='an_results' THEN permission_name:='financial_analysis.prepare';ELSE permission_name:='financial_analysis.'||NEW.action;END IF;
IF NOT EXISTS(SELECT 1 FROM company_memberships WHERE user_id=NEW.actor_id AND tenant_id=NEW.tenant_id AND company_id=NEW.company_id) OR NOT EXISTS(SELECT 1 FROM permission_grants WHERE user_id=NEW.actor_id AND tenant_id=NEW.tenant_id AND company_id=NEW.company_id AND unit_id=NEW.unit_id AND permission=permission_name) THEN RAISE EXCEPTION 'SGF_AN_ACCESS';END IF;
IF TG_TABLE_NAME='an_results' THEN RETURN NEW;END IF;
SELECT * INTO p FROM an_criteria WHERE (tenant_id,company_id,unit_id,metric)=(NEW.tenant_id,NEW.company_id,NEW.unit_id,NEW.metric) ORDER BY version DESC LIMIT 1;
IF NEW.version<>coalesce(p.version,0)+1 OR NEW.previous_id IS DISTINCT FROM p.id THEN RAISE EXCEPTION 'SGF_AN_STALE';END IF;
IF NEW.action='prepare' THEN IF NEW.state<>'prepared' OR NEW.prepared_by<>NEW.actor_id THEN RAISE EXCEPTION 'SGF_AN_PREPARE';END IF;
ELSE IF p.id IS NULL OR p.state<>'prepared' OR NEW.prepared_by<>p.prepared_by OR NEW.actor_id=p.prepared_by OR NEW.definition IS DISTINCT FROM p.definition OR NEW.source_hash<>p.source_hash OR NEW.state NOT IN('reviewed','refused') THEN RAISE EXCEPTION 'SGF_AN_REVIEW';END IF;END IF;
RETURN NEW;
END; $$;
CREATE TRIGGER an_criterion_guard BEFORE INSERT ON an_criteria FOR EACH ROW EXECUTE FUNCTION sgf_an_guard();
CREATE TRIGGER an_result_guard BEFORE INSERT ON an_results FOR EACH ROW EXECUTE FUNCTION sgf_an_guard();
CREATE TRIGGER an_criteria_immutable BEFORE UPDATE OR DELETE ON an_criteria FOR EACH ROW EXECUTE FUNCTION sgf_append_only_audit();
CREATE TRIGGER an_results_immutable BEFORE UPDATE OR DELETE ON an_results FOR EACH ROW EXECUTE FUNCTION sgf_append_only_audit();
CREATE TRIGGER an_commands_immutable BEFORE UPDATE OR DELETE ON an_commands FOR EACH ROW EXECUTE FUNCTION sgf_append_only_audit();
CREATE TRIGGER an_commands_test BEFORE INSERT ON an_commands FOR EACH ROW EXECUTE FUNCTION sgf_wo_test_only();
REVOKE ALL ON an_criteria,an_results,an_commands FROM PUBLIC;
REVOKE ALL ON SEQUENCE an_criteria_id_seq,an_results_id_seq,an_commands_id_seq FROM PUBLIC;
REVOKE ALL ON FUNCTION sgf_an_guard() FROM PUBLIC;
+25
View File
@@ -0,0 +1,25 @@
CREATE TABLE fx_versions(id bigserial PRIMARY KEY,tenant_id bigint NOT NULL,company_id bigint NOT NULL,unit_id bigint NOT NULL,process_key uuid NOT NULL,kind text NOT NULL CHECK(kind IN('assessment','form')),version bigint NOT NULL CHECK(version>0),previous_id bigint UNIQUE REFERENCES fx_versions(id),action text NOT NULL CHECK(action IN('prepare','review')),state text NOT NULL CHECK(state IN('prepared','reviewed','refused')),prepared_by bigint NOT NULL REFERENCES users(id),actor_id bigint NOT NULL REFERENCES users(id),data jsonb NOT NULL,dependencies jsonb NOT NULL,reason text NOT NULL CHECK(length(reason)>=10),created_at timestamptz NOT NULL DEFAULT now(),UNIQUE(tenant_id,company_id,unit_id,process_key,kind,version),FOREIGN KEY(tenant_id,company_id,unit_id) REFERENCES units(tenant_id,company_id,id));
CREATE TABLE fx_commands(id bigserial PRIMARY KEY,tenant_id bigint NOT NULL,company_id bigint NOT NULL,unit_id bigint NOT NULL,actor_id bigint NOT NULL REFERENCES users(id),command_key uuid NOT NULL,request_hash char(64) NOT NULL,result jsonb NOT NULL,created_at timestamptz NOT NULL DEFAULT now(),UNIQUE(tenant_id,company_id,unit_id,actor_id,command_key));
CREATE FUNCTION sgf_fx_guard() RETURNS trigger LANGUAGE plpgsql AS $$
DECLARE p fx_versions;latest bigint;prefix text;
BEGIN
PERFORM sgf_wo_environment();PERFORM 1 FROM companies WHERE tenant_id=NEW.tenant_id AND id=NEW.company_id FOR UPDATE;
prefix:=CASE NEW.kind WHEN 'assessment' THEN 'fiscal_assessments.' ELSE 'fiscal_forms.' END;
IF NOT EXISTS(SELECT 1 FROM permission_grants WHERE user_id=NEW.actor_id AND tenant_id=NEW.tenant_id AND company_id=NEW.company_id AND unit_id=NEW.unit_id AND permission=prefix||NEW.action) THEN RAISE EXCEPTION 'SGF_FX_ACCESS'; END IF;
IF NOT EXISTS(SELECT 1 FROM company_memberships WHERE user_id=NEW.actor_id AND tenant_id=NEW.tenant_id AND company_id=NEW.company_id) THEN RAISE EXCEPTION 'SGF_FX_MEMBER'; END IF;
SELECT coalesce(max(version),0) INTO latest FROM fx_versions WHERE (tenant_id,company_id,unit_id,process_key,kind)=(NEW.tenant_id,NEW.company_id,NEW.unit_id,NEW.process_key,NEW.kind);
IF NEW.version<>latest+1 THEN RAISE EXCEPTION 'SGF_FX_STALE'; END IF;
IF latest=0 THEN IF NEW.previous_id IS NOT NULL OR NEW.action<>'prepare' THEN RAISE EXCEPTION 'SGF_FX_FIRST'; END IF;
ELSE SELECT * INTO p FROM fx_versions WHERE (tenant_id,company_id,unit_id,process_key,kind,version)=(NEW.tenant_id,NEW.company_id,NEW.unit_id,NEW.process_key,NEW.kind,latest);
IF NEW.previous_id IS DISTINCT FROM p.id THEN RAISE EXCEPTION 'SGF_FX_PARENT'; END IF; END IF;
IF NEW.action='review' THEN IF p.state<>'prepared' OR NEW.prepared_by<>p.prepared_by OR NEW.actor_id=p.prepared_by OR NEW.state NOT IN('reviewed','refused') OR NEW.data IS DISTINCT FROM p.data OR NEW.dependencies IS DISTINCT FROM p.dependencies THEN RAISE EXCEPTION 'SGF_FX_REVIEW'; END IF;
ELSE IF NEW.prepared_by<>NEW.actor_id OR NEW.state<>'prepared' THEN RAISE EXCEPTION 'SGF_FX_PREPARE'; END IF; END IF;
RETURN NEW;
END; $$;
CREATE TRIGGER fx_guard BEFORE INSERT ON fx_versions FOR EACH ROW EXECUTE FUNCTION sgf_fx_guard();
CREATE TRIGGER fx_versions_immutable BEFORE UPDATE OR DELETE ON fx_versions FOR EACH ROW EXECUTE FUNCTION sgf_append_only_audit();
CREATE TRIGGER fx_commands_immutable BEFORE UPDATE OR DELETE ON fx_commands FOR EACH ROW EXECUTE FUNCTION sgf_append_only_audit();
CREATE TRIGGER fx_commands_test BEFORE INSERT ON fx_commands FOR EACH ROW EXECUTE FUNCTION sgf_wo_test_only();
REVOKE ALL ON fx_versions,fx_commands FROM PUBLIC;
REVOKE ALL ON SEQUENCE fx_versions_id_seq,fx_commands_id_seq FROM PUBLIC;
REVOKE ALL ON FUNCTION sgf_fx_guard() FROM PUBLIC;
+25
View File
@@ -0,0 +1,25 @@
CREATE TABLE fo_meta(policy text PRIMARY KEY,composition text NOT NULL,source_hash char(64) NOT NULL);
CREATE TABLE fo_versions(
id bigserial PRIMARY KEY,tenant_id bigint NOT NULL,company_id bigint NOT NULL,unit_id bigint NOT NULL,
record_key uuid NOT NULL,version bigint NOT NULL CHECK(version>0),previous_id bigint UNIQUE REFERENCES fo_versions(id),
action text NOT NULL CHECK(action IN('prepare','review')),state text NOT NULL CHECK(state IN('prepared','approved','refused')),
prepared_by bigint NOT NULL REFERENCES users(id),actor_id bigint NOT NULL REFERENCES users(id),
preparer_identity char(64) NOT NULL,data jsonb NOT NULL,source_basis char(64) NOT NULL,
impact jsonb NOT NULL,reason text NOT NULL CHECK(length(reason)>=10),created_at timestamptz NOT NULL,
UNIQUE(tenant_id,company_id,unit_id,record_key,version),
FOREIGN KEY(tenant_id,company_id,unit_id) REFERENCES units(tenant_id,company_id,id),
CHECK(jsonb_typeof(data)='object'),CHECK(jsonb_typeof(impact)='object'),
CHECK((action='prepare' AND state='prepared') OR (action='review' AND state IN('approved','refused'))),
CHECK(action<>'review' OR prepared_by<>actor_id)
);
CREATE TABLE fo_commands(
id bigserial PRIMARY KEY,tenant_id bigint NOT NULL,company_id bigint NOT NULL,unit_id bigint NOT NULL,
actor_id bigint NOT NULL REFERENCES users(id),command_key uuid NOT NULL,request_hash char(64) NOT NULL,
result jsonb NOT NULL,input jsonb NOT NULL,created_at timestamptz NOT NULL,
UNIQUE(tenant_id,company_id,unit_id,actor_id,command_key)
);
CREATE FUNCTION fo_append_only() RETURNS trigger LANGUAGE plpgsql AS $$
BEGIN RAISE EXCEPTION 'SGF_FISCAL_OBLIGATIONS_APPEND_ONLY'; END; $$;
CREATE TRIGGER fo_meta_immutable BEFORE UPDATE OR DELETE ON fo_meta FOR EACH ROW EXECUTE FUNCTION fo_append_only();
CREATE TRIGGER fo_versions_immutable BEFORE UPDATE OR DELETE ON fo_versions FOR EACH ROW EXECUTE FUNCTION fo_append_only();
CREATE TRIGGER fo_commands_immutable BEFORE UPDATE OR DELETE ON fo_commands FOR EACH ROW EXECUTE FUNCTION fo_append_only();
+27
View File
@@ -0,0 +1,27 @@
CREATE TABLE fr_events(id bigserial PRIMARY KEY,tenant_id bigint NOT NULL,company_id bigint NOT NULL,unit_id bigint NOT NULL,rule_key text NOT NULL CHECK(rule_key ~ '^[A-Z0-9_-]{3,80}$'),revision bigint NOT NULL CHECK(revision>0),rule_version integer NOT NULL CHECK(rule_version>0),previous_id bigint UNIQUE REFERENCES fr_events(id),target_id bigint REFERENCES fr_events(id),action text NOT NULL CHECK(action IN('prepare','review','withdraw')),state text NOT NULL CHECK(state IN('prepared','reviewed','refused','withdrawn')),prepared_by bigint NOT NULL REFERENCES users(id),actor_id bigint NOT NULL REFERENCES users(id),definition jsonb NOT NULL,reason text NOT NULL CHECK(length(reason)>=10),created_at timestamptz NOT NULL DEFAULT now(),UNIQUE(tenant_id,company_id,unit_id,rule_key,revision),FOREIGN KEY(tenant_id,company_id,unit_id) REFERENCES units(tenant_id,company_id,id));
CREATE TABLE fr_calculations(id bigserial PRIMARY KEY,tenant_id bigint NOT NULL,company_id bigint NOT NULL,unit_id bigint NOT NULL,actor_id bigint NOT NULL REFERENCES users(id),rule_event_id bigint REFERENCES fr_events(id),input jsonb NOT NULL,result jsonb NOT NULL,dependencies jsonb NOT NULL,reason text NOT NULL CHECK(length(reason)>=10),created_at timestamptz NOT NULL DEFAULT now(),FOREIGN KEY(tenant_id,company_id,unit_id) REFERENCES units(tenant_id,company_id,id));
CREATE TABLE fr_commands(id bigserial PRIMARY KEY,tenant_id bigint NOT NULL,company_id bigint NOT NULL,unit_id bigint NOT NULL,actor_id bigint NOT NULL REFERENCES users(id),command_key uuid NOT NULL,request_hash char(64) NOT NULL,result jsonb NOT NULL,created_at timestamptz NOT NULL DEFAULT now(),UNIQUE(tenant_id,company_id,unit_id,actor_id,command_key));
CREATE FUNCTION sgf_fr_guard() RETURNS trigger LANGUAGE plpgsql AS $$
DECLARE lastrow fr_events;t fr_events;v bigint;required_right text;
BEGIN
PERFORM sgf_wo_environment();PERFORM 1 FROM companies WHERE tenant_id=NEW.tenant_id AND id=NEW.company_id FOR UPDATE;
IF TG_TABLE_NAME='fr_calculations' THEN required_right:='fiscal_rules.prepare'; ELSE required_right:='fiscal_rules.'||CASE NEW.action WHEN 'prepare' THEN 'prepare' ELSE 'review' END; END IF;
IF NOT EXISTS(SELECT 1 FROM company_memberships WHERE user_id=NEW.actor_id AND tenant_id=NEW.tenant_id AND company_id=NEW.company_id) OR NOT EXISTS(SELECT 1 FROM permission_grants WHERE user_id=NEW.actor_id AND tenant_id=NEW.tenant_id AND company_id=NEW.company_id AND unit_id=NEW.unit_id AND permission=required_right) THEN RAISE EXCEPTION 'SGF_FR_ACCESS'; END IF;
IF TG_TABLE_NAME='fr_calculations' THEN RETURN NEW; END IF;
SELECT * INTO lastrow FROM fr_events WHERE (tenant_id,company_id,unit_id,rule_key)=(NEW.tenant_id,NEW.company_id,NEW.unit_id,NEW.rule_key) ORDER BY revision DESC LIMIT 1;
IF NEW.revision<>coalesce(lastrow.revision,0)+1 OR NEW.previous_id IS DISTINCT FROM lastrow.id THEN RAISE EXCEPTION 'SGF_FR_STALE'; END IF;
IF NEW.action='prepare' THEN SELECT coalesce(max(rule_version),0)+1 INTO v FROM fr_events WHERE (tenant_id,company_id,unit_id,rule_key)=(NEW.tenant_id,NEW.company_id,NEW.unit_id,NEW.rule_key); IF NEW.rule_version<>v OR NEW.state<>'prepared' OR NEW.prepared_by<>NEW.actor_id OR NEW.target_id IS NOT NULL THEN RAISE EXCEPTION 'SGF_FR_PREPARE'; END IF;
ELSE SELECT * INTO t FROM fr_events WHERE id=NEW.target_id AND (tenant_id,company_id,unit_id,rule_key)=(NEW.tenant_id,NEW.company_id,NEW.unit_id,NEW.rule_key);
IF t.id IS NULL OR EXISTS(SELECT 1 FROM fr_events WHERE (tenant_id,company_id,unit_id,rule_key,rule_version)=(t.tenant_id,t.company_id,t.unit_id,t.rule_key,t.rule_version) AND revision>t.revision) OR NEW.rule_version<>t.rule_version OR NEW.prepared_by<>t.prepared_by OR NEW.actor_id=t.prepared_by OR NEW.definition IS DISTINCT FROM t.definition THEN RAISE EXCEPTION 'SGF_FR_REVIEW'; END IF;
IF (NEW.action='review' AND (t.state<>'prepared' OR NEW.state NOT IN('reviewed','refused'))) OR (NEW.action='withdraw' AND (t.state<>'reviewed' OR NEW.state<>'withdrawn')) THEN RAISE EXCEPTION 'SGF_FR_STATE'; END IF; END IF;
RETURN NEW;
END; $$;
CREATE TRIGGER fr_event_guard BEFORE INSERT ON fr_events FOR EACH ROW EXECUTE FUNCTION sgf_fr_guard();
CREATE TRIGGER fr_calculation_guard BEFORE INSERT ON fr_calculations FOR EACH ROW EXECUTE FUNCTION sgf_fr_guard();
CREATE TRIGGER fr_events_immutable BEFORE UPDATE OR DELETE ON fr_events FOR EACH ROW EXECUTE FUNCTION sgf_append_only_audit();
CREATE TRIGGER fr_calculations_immutable BEFORE UPDATE OR DELETE ON fr_calculations FOR EACH ROW EXECUTE FUNCTION sgf_append_only_audit();
CREATE TRIGGER fr_commands_immutable BEFORE UPDATE OR DELETE ON fr_commands FOR EACH ROW EXECUTE FUNCTION sgf_append_only_audit();
CREATE TRIGGER fr_commands_test BEFORE INSERT ON fr_commands FOR EACH ROW EXECUTE FUNCTION sgf_wo_test_only();
REVOKE ALL ON fr_events,fr_calculations,fr_commands FROM PUBLIC;
REVOKE ALL ON SEQUENCE fr_events_id_seq,fr_calculations_id_seq,fr_commands_id_seq FROM PUBLIC;
REVOKE ALL ON FUNCTION sgf_fr_guard() FROM PUBLIC;
+28
View File
@@ -0,0 +1,28 @@
CREATE TABLE hp_meta(policy text PRIMARY KEY,composition text NOT NULL,source_hash char(64) NOT NULL);
CREATE TABLE hp_versions(
id bigserial PRIMARY KEY,tenant_id bigint NOT NULL,company_id bigint NOT NULL,unit_id bigint NOT NULL,
kind text NOT NULL CHECK(kind IN('contract','day','entitlement','leave','report')),record_key uuid NOT NULL,version bigint NOT NULL CHECK(version>0),previous_id bigint UNIQUE REFERENCES hp_versions(id),
action text NOT NULL CHECK(action IN('prepare','review')),state text NOT NULL CHECK(state IN('prepared','approved','refused')),
prepared_by bigint NOT NULL REFERENCES users(id),actor_id bigint NOT NULL REFERENCES users(id),
preparer_identity char(64) NOT NULL,data jsonb NOT NULL,source_basis char(64) NOT NULL,
impact jsonb NOT NULL,reason text NOT NULL CHECK(length(reason)>=10),created_at timestamptz NOT NULL,
UNIQUE(tenant_id,company_id,unit_id,record_key,version),
FOREIGN KEY(tenant_id,company_id,unit_id) REFERENCES units(tenant_id,company_id,id),
CHECK(jsonb_typeof(data)='object'),CHECK(jsonb_typeof(impact)='object'),
CHECK((action='prepare' AND state='prepared') OR (action='review' AND state IN('approved','refused'))),
CHECK(action<>'review' OR prepared_by<>actor_id)
);
CREATE TABLE hp_commands(
id bigserial PRIMARY KEY,tenant_id bigint NOT NULL,company_id bigint NOT NULL,unit_id bigint NOT NULL,
actor_id bigint NOT NULL REFERENCES users(id),command_key uuid NOT NULL,request_hash char(64) NOT NULL,
result jsonb NOT NULL,input jsonb NOT NULL,created_at timestamptz NOT NULL,
UNIQUE(tenant_id,company_id,unit_id,actor_id,command_key)
);
CREATE FUNCTION hp_append_only() RETURNS trigger LANGUAGE plpgsql AS $$
BEGIN RAISE EXCEPTION 'SGF_HR_PERSONNEL_APPEND_ONLY'; END; $$;
CREATE TRIGGER hp_meta_immutable BEFORE UPDATE OR DELETE ON hp_meta FOR EACH ROW EXECUTE FUNCTION hp_append_only();
CREATE TRIGGER hp_versions_immutable BEFORE UPDATE OR DELETE ON hp_versions FOR EACH ROW EXECUTE FUNCTION hp_append_only();
CREATE TRIGGER hp_commands_immutable BEFORE UPDATE OR DELETE ON hp_commands FOR EACH ROW EXECUTE FUNCTION hp_append_only();
CREATE INDEX hp_versions_scope_latest ON hp_versions(tenant_id,company_id,unit_id,record_key,version DESC);
CREATE INDEX hp_versions_kind ON hp_versions(tenant_id,company_id,unit_id,kind,id DESC);
+39
View File
@@ -0,0 +1,39 @@
CREATE TABLE fs_meta(policy text PRIMARY KEY,composition text NOT NULL,source_hash char(64) NOT NULL);
CREATE TABLE fs_versions(
id bigserial PRIMARY KEY,tenant_id bigint NOT NULL,company_id bigint NOT NULL,unit_id bigint NOT NULL,
kind text NOT NULL CHECK(kind IN('mapping','result')),version bigint NOT NULL CHECK(version>0),
previous_id bigint UNIQUE REFERENCES fs_versions(id),action text NOT NULL CHECK(action IN('prepare','review')),
state text NOT NULL CHECK(state IN('prepared','approved','refused')),prepared_host_id bigint NOT NULL,
prepared_native_id bigint NOT NULL REFERENCES users(id),prepared_person text NOT NULL,preparer_identity char(64) NOT NULL,
host_actor_id bigint NOT NULL,actor_id bigint NOT NULL REFERENCES users(id),data jsonb NOT NULL,
source_basis char(64) NOT NULL,reason text NOT NULL CHECK(length(reason)>=10),created_at timestamptz NOT NULL,
UNIQUE(tenant_id,company_id,unit_id,kind,version),CHECK(jsonb_typeof(data)='object'),
CHECK((action='prepare' AND state='prepared') OR (action='review' AND state IN('approved','refused'))),
CHECK(action<>'review' OR (prepared_host_id<>host_actor_id AND prepared_native_id<>actor_id))
);
CREATE TABLE fs_results(
tenant_id bigint NOT NULL,company_id bigint NOT NULL,unit_id bigint NOT NULL,snapshot_key uuid NOT NULL,
data jsonb NOT NULL,csv_sha256 char(64) NOT NULL,source_basis char(64) NOT NULL,created_at timestamptz NOT NULL,
PRIMARY KEY(tenant_id,company_id,unit_id,snapshot_key)
);
CREATE TABLE fs_result_lines(
tenant_id bigint NOT NULL,company_id bigint NOT NULL,unit_id bigint NOT NULL,snapshot_key uuid NOT NULL,
account_id bigint NOT NULL REFERENCES gl_accounts(id),code text NOT NULL,label text NOT NULL,currency text NOT NULL,
category text NOT NULL CHECK(category IN('asset','liability','income','expense','unclassified')),
opening_net_minor numeric(40,0) NOT NULL,debit_minor numeric(40,0) NOT NULL,credit_minor numeric(40,0) NOT NULL,closing_net_minor numeric(40,0) NOT NULL,
PRIMARY KEY(tenant_id,company_id,unit_id,snapshot_key,account_id),
FOREIGN KEY(tenant_id,company_id,unit_id,snapshot_key) REFERENCES fs_results(tenant_id,company_id,unit_id,snapshot_key) DEFERRABLE INITIALLY DEFERRED
);
CREATE TABLE fs_commands(
id bigserial PRIMARY KEY,tenant_id bigint NOT NULL,company_id bigint NOT NULL,unit_id bigint NOT NULL,
host_actor_id bigint NOT NULL,actor_id bigint NOT NULL REFERENCES users(id),command_key uuid NOT NULL,
request_hash char(64) NOT NULL,binding_hash char(64) NOT NULL,input jsonb NOT NULL,result jsonb NOT NULL,created_at timestamptz NOT NULL,
UNIQUE(tenant_id,company_id,unit_id,host_actor_id,command_key)
);
CREATE FUNCTION fs_append_only() RETURNS trigger LANGUAGE plpgsql AS $$
BEGIN RAISE EXCEPTION 'SGF_INTERNAL_STATEMENTS_APPEND_ONLY'; END; $$;
CREATE TRIGGER fs_meta_immutable BEFORE UPDATE OR DELETE ON fs_meta FOR EACH ROW EXECUTE FUNCTION fs_append_only();
CREATE TRIGGER fs_versions_immutable BEFORE UPDATE OR DELETE ON fs_versions FOR EACH ROW EXECUTE FUNCTION fs_append_only();
CREATE TRIGGER fs_results_immutable BEFORE UPDATE OR DELETE ON fs_results FOR EACH ROW EXECUTE FUNCTION fs_append_only();
CREATE TRIGGER fs_lines_immutable BEFORE UPDATE OR DELETE ON fs_result_lines FOR EACH ROW EXECUTE FUNCTION fs_append_only();
CREATE TRIGGER fs_commands_immutable BEFORE UPDATE OR DELETE ON fs_commands FOR EACH ROW EXECUTE FUNCTION fs_append_only();
+25
View File
@@ -0,0 +1,25 @@
CREATE TABLE pd_due_meta(policy text PRIMARY KEY, composition text NOT NULL, source_hash char(64) NOT NULL);
CREATE TABLE pd_due_versions(
id bigserial PRIMARY KEY, tenant_id bigint NOT NULL, company_id bigint NOT NULL, unit_id bigint NOT NULL,
obligation_id bigint NOT NULL REFERENCES pa_obligations(id), version bigint NOT NULL CHECK(version>0),
previous_id bigint UNIQUE REFERENCES pd_due_versions(id), action text NOT NULL CHECK(action IN('prepare','review')),
state text NOT NULL CHECK(state IN('prepared','approved','refused')), source_basis char(64) NOT NULL,
prepared_host_id bigint NOT NULL, prepared_native_id bigint NOT NULL REFERENCES users(id),
host_actor_id bigint NOT NULL, actor_id bigint NOT NULL REFERENCES users(id), data jsonb NOT NULL,
reason text NOT NULL CHECK(length(reason)>=10), created_at timestamptz NOT NULL,
UNIQUE(tenant_id,company_id,unit_id,obligation_id,version),
CHECK(jsonb_typeof(data)='object'),
CHECK((action='prepare' AND state='prepared') OR (action='review' AND state IN('approved','refused'))),
CHECK(action<>'review' OR (host_actor_id<>prepared_host_id AND actor_id<>prepared_native_id))
);
CREATE TABLE pd_due_commands(
id bigserial PRIMARY KEY, tenant_id bigint NOT NULL, company_id bigint NOT NULL, unit_id bigint NOT NULL,
host_actor_id bigint NOT NULL, actor_id bigint NOT NULL REFERENCES users(id), command_key uuid NOT NULL,
request_hash char(64) NOT NULL, binding_hash char(64) NOT NULL, input jsonb NOT NULL, result jsonb NOT NULL,
created_at timestamptz NOT NULL, UNIQUE(tenant_id,company_id,unit_id,host_actor_id,command_key)
);
CREATE FUNCTION pd_due_append_only() RETURNS trigger LANGUAGE plpgsql AS $$
BEGIN RAISE EXCEPTION 'SGF_PAYABLE_DUE_APPEND_ONLY'; END; $$;
CREATE TRIGGER pd_due_meta_immutable BEFORE UPDATE OR DELETE ON pd_due_meta FOR EACH ROW EXECUTE FUNCTION pd_due_append_only();
CREATE TRIGGER pd_due_versions_immutable BEFORE UPDATE OR DELETE ON pd_due_versions FOR EACH ROW EXECUTE FUNCTION pd_due_append_only();
CREATE TRIGGER pd_due_commands_immutable BEFORE UPDATE OR DELETE ON pd_due_commands FOR EACH ROW EXECUTE FUNCTION pd_due_append_only();
+28
View File
@@ -0,0 +1,28 @@
CREATE TABLE rm_meta(policy text PRIMARY KEY,composition text NOT NULL,source_hash char(64) NOT NULL);
CREATE TABLE rm_versions(
id bigserial PRIMARY KEY,tenant_id bigint NOT NULL,company_id bigint NOT NULL,unit_id bigint NOT NULL,
kind text NOT NULL CHECK(kind IN('worker','rules','payroll')),record_key uuid NOT NULL,version bigint NOT NULL CHECK(version>0),previous_id bigint UNIQUE REFERENCES rm_versions(id),
action text NOT NULL CHECK(action IN('prepare','review')),state text NOT NULL CHECK(state IN('prepared','approved','refused')),
prepared_by bigint NOT NULL REFERENCES users(id),actor_id bigint NOT NULL REFERENCES users(id),
preparer_identity char(64) NOT NULL,data jsonb NOT NULL,source_basis char(64) NOT NULL,
impact jsonb NOT NULL,reason text NOT NULL CHECK(length(reason)>=10),created_at timestamptz NOT NULL,
UNIQUE(tenant_id,company_id,unit_id,record_key,version),
FOREIGN KEY(tenant_id,company_id,unit_id) REFERENCES units(tenant_id,company_id,id),
CHECK(jsonb_typeof(data)='object'),CHECK(jsonb_typeof(impact)='object'),
CHECK((action='prepare' AND state='prepared') OR (action='review' AND state IN('approved','refused'))),
CHECK(action<>'review' OR prepared_by<>actor_id)
);
CREATE TABLE rm_commands(
id bigserial PRIMARY KEY,tenant_id bigint NOT NULL,company_id bigint NOT NULL,unit_id bigint NOT NULL,
actor_id bigint NOT NULL REFERENCES users(id),command_key uuid NOT NULL,request_hash char(64) NOT NULL,
result jsonb NOT NULL,input jsonb NOT NULL,created_at timestamptz NOT NULL,
UNIQUE(tenant_id,company_id,unit_id,actor_id,command_key)
);
CREATE FUNCTION rm_append_only() RETURNS trigger LANGUAGE plpgsql AS $$
BEGIN RAISE EXCEPTION 'SGF_REMUNERATIONS_APPEND_ONLY'; END; $$;
CREATE TRIGGER rm_meta_immutable BEFORE UPDATE OR DELETE ON rm_meta FOR EACH ROW EXECUTE FUNCTION rm_append_only();
CREATE TRIGGER rm_versions_immutable BEFORE UPDATE OR DELETE ON rm_versions FOR EACH ROW EXECUTE FUNCTION rm_append_only();
CREATE TRIGGER rm_commands_immutable BEFORE UPDATE OR DELETE ON rm_commands FOR EACH ROW EXECUTE FUNCTION rm_append_only();
CREATE INDEX rm_versions_scope_latest ON rm_versions(tenant_id,company_id,unit_id,record_key,version DESC);
CREATE INDEX rm_versions_kind ON rm_versions(tenant_id,company_id,unit_id,kind,id DESC);
+35
View File
@@ -0,0 +1,35 @@
CREATE TABLE sd_versions(
id bigserial PRIMARY KEY,tenant_id bigint NOT NULL,company_id bigint NOT NULL,unit_id bigint NOT NULL,
document_key uuid NOT NULL,version integer NOT NULL CHECK(version>0),previous_id bigint UNIQUE REFERENCES sd_versions(id),
kind text NOT NULL CHECK(kind IN('receipt','credit_note','invoice_receipt')),
action text NOT NULL CHECK(action IN('prepare','review','issue')),state text NOT NULL CHECK(state IN('draft','approved','refused','issued')),
prepared_by bigint NOT NULL REFERENCES users(id),actor_id bigint NOT NULL REFERENCES users(id),data jsonb NOT NULL,dependencies jsonb NOT NULL,
reason text NOT NULL CHECK(length(reason)>=10),created_at timestamptz NOT NULL DEFAULT now(),
UNIQUE(tenant_id,company_id,unit_id,document_key,version),FOREIGN KEY(tenant_id,company_id,unit_id) REFERENCES units(tenant_id,company_id,id)
);
CREATE TABLE sd_outputs(id bigserial PRIMARY KEY,tenant_id bigint NOT NULL,company_id bigint NOT NULL,unit_id bigint NOT NULL,document_key uuid NOT NULL UNIQUE,version_id bigint NOT NULL UNIQUE REFERENCES sd_versions(id),kind text NOT NULL,number bigint NOT NULL,invoice_id bigint NOT NULL REFERENCES si_invoices(id),effect_id bigint REFERENCES sr_effects(id),snapshot jsonb NOT NULL,pdf_base64 text NOT NULL,pdf_sha256 char(64) NOT NULL,actor_id bigint NOT NULL REFERENCES users(id),created_at timestamptz NOT NULL DEFAULT now(),UNIQUE(tenant_id,company_id,unit_id,kind,number));
CREATE UNIQUE INDEX sd_receipt_allocation_once ON sd_outputs(effect_id) WHERE kind='receipt';
CREATE UNIQUE INDEX sd_credit_effect_once ON sd_outputs(effect_id) WHERE kind='credit_note';
CREATE UNIQUE INDEX sd_invoice_receipt_once ON sd_outputs(invoice_id) WHERE kind='invoice_receipt';
CREATE TABLE sd_commands(id bigserial PRIMARY KEY,tenant_id bigint NOT NULL,company_id bigint NOT NULL,unit_id bigint NOT NULL,actor_id bigint NOT NULL REFERENCES users(id),command_key uuid NOT NULL,request_hash char(64) NOT NULL,result jsonb NOT NULL,created_at timestamptz NOT NULL DEFAULT now(),UNIQUE(tenant_id,company_id,unit_id,actor_id,command_key));
CREATE FUNCTION sgf_sd_guard() RETURNS trigger LANGUAGE plpgsql AS $$
DECLARE p sd_versions; permission_name text;
BEGIN
PERFORM sgf_wo_environment();PERFORM 1 FROM companies WHERE id=NEW.company_id FOR UPDATE;
permission_name:='service_documents.'||CASE NEW.action WHEN 'review' THEN 'review' WHEN 'issue' THEN 'issue' ELSE 'prepare' END;
IF NOT EXISTS(SELECT 1 FROM permission_grants WHERE user_id=NEW.actor_id AND tenant_id=NEW.tenant_id AND company_id=NEW.company_id AND unit_id=NEW.unit_id AND permission=permission_name) THEN RAISE EXCEPTION 'SGF_SD_ACCESS'; END IF;
SELECT * INTO p FROM sd_versions WHERE (tenant_id,company_id,unit_id,document_key)=(NEW.tenant_id,NEW.company_id,NEW.unit_id,NEW.document_key) ORDER BY version DESC LIMIT 1;
IF (p.id IS NULL AND (NEW.version<>1 OR NEW.action<>'prepare' OR NEW.previous_id IS NOT NULL OR NEW.prepared_by<>NEW.actor_id)) OR(p.id IS NOT NULL AND(NEW.previous_id IS DISTINCT FROM p.id OR NEW.version<>p.version+1 OR NEW.kind<>p.kind OR NEW.prepared_by<>p.prepared_by OR p.state='issued')) THEN RAISE EXCEPTION 'SGF_SD_VERSION'; END IF;
IF NEW.action='review' AND(p.state<>'draft' OR NEW.actor_id=NEW.prepared_by) THEN RAISE EXCEPTION 'SGF_SD_SELF_REVIEW'; END IF;
IF NEW.action='issue' AND p.state<>'approved' THEN RAISE EXCEPTION 'SGF_SD_UNREVIEWED'; END IF;
RETURN NEW;
END; $$;
CREATE TRIGGER sd_guard BEFORE INSERT ON sd_versions FOR EACH ROW EXECUTE FUNCTION sgf_sd_guard();
CREATE TRIGGER sd_versions_immutable BEFORE UPDATE OR DELETE ON sd_versions FOR EACH ROW EXECUTE FUNCTION sgf_append_only_audit();
CREATE TRIGGER sd_outputs_immutable BEFORE UPDATE OR DELETE ON sd_outputs FOR EACH ROW EXECUTE FUNCTION sgf_append_only_audit();
CREATE TRIGGER sd_commands_immutable BEFORE UPDATE OR DELETE ON sd_commands FOR EACH ROW EXECUTE FUNCTION sgf_append_only_audit();
REVOKE ALL ON sd_versions,sd_outputs,sd_commands FROM PUBLIC;
REVOKE ALL ON FUNCTION sgf_sd_guard() FROM PUBLIC;
CREATE TRIGGER sd_outputs_test BEFORE INSERT ON sd_outputs FOR EACH ROW EXECUTE FUNCTION sgf_wo_test_only();
CREATE TRIGGER sd_commands_test BEFORE INSERT ON sd_commands FOR EACH ROW EXECUTE FUNCTION sgf_wo_test_only();
+22
View File
@@ -0,0 +1,22 @@
CREATE TABLE sc_catalogue_meta(policy text PRIMARY KEY, composition text NOT NULL, source_hash char(64) NOT NULL);
CREATE TABLE sc_catalogue_versions(
id bigserial PRIMARY KEY, tenant_id bigint NOT NULL, company_id bigint NOT NULL, unit_id bigint NOT NULL,
record_key uuid NOT NULL, version bigint NOT NULL CHECK(version>0), previous_id bigint UNIQUE REFERENCES sc_catalogue_versions(id),
action text NOT NULL CHECK(action IN('prepare','review')), state text NOT NULL CHECK(state IN('prepared','approved','refused')),
prepared_host_id bigint NOT NULL, prepared_native_id bigint NOT NULL REFERENCES users(id), host_actor_id bigint NOT NULL, actor_id bigint NOT NULL REFERENCES users(id),
stock_item_id bigint REFERENCES stock_items(id), data jsonb NOT NULL CHECK(jsonb_typeof(data)='object'),
reason text NOT NULL CHECK(length(reason)>=10), created_at timestamptz NOT NULL,
UNIQUE(tenant_id,company_id,unit_id,record_key,version), CHECK((action='prepare' AND state='prepared') OR (action='review' AND state IN('approved','refused'))),
CHECK(action<>'review' OR (host_actor_id<>prepared_host_id AND actor_id<>prepared_native_id))
);
CREATE TABLE sc_catalogue_commands(
id bigserial PRIMARY KEY, tenant_id bigint NOT NULL, company_id bigint NOT NULL, unit_id bigint NOT NULL,
host_actor_id bigint NOT NULL, actor_id bigint NOT NULL REFERENCES users(id), command_key uuid NOT NULL,
request_hash char(64) NOT NULL, binding_hash char(64) NOT NULL, input jsonb NOT NULL, result jsonb NOT NULL, created_at timestamptz NOT NULL,
UNIQUE(tenant_id,company_id,unit_id,host_actor_id,command_key)
);
CREATE FUNCTION sc_catalogue_append_only() RETURNS trigger LANGUAGE plpgsql AS $$
BEGIN RAISE EXCEPTION 'SGF_CATALOGUE_APPEND_ONLY'; END; $$;
CREATE TRIGGER sc_catalogue_versions_immutable BEFORE UPDATE OR DELETE ON sc_catalogue_versions FOR EACH ROW EXECUTE FUNCTION sc_catalogue_append_only();
CREATE TRIGGER sc_catalogue_commands_immutable BEFORE UPDATE OR DELETE ON sc_catalogue_commands FOR EACH ROW EXECUTE FUNCTION sc_catalogue_append_only();
CREATE TRIGGER sc_catalogue_meta_immutable BEFORE UPDATE OR DELETE ON sc_catalogue_meta FOR EACH ROW EXECUTE FUNCTION sc_catalogue_append_only();
+32
View File
@@ -0,0 +1,32 @@
CREATE TABLE wo_schedule_events (
id bigserial PRIMARY KEY, tenant_id bigint NOT NULL, company_id bigint NOT NULL, unit_id bigint NOT NULL,
booking_key uuid NOT NULL, version integer NOT NULL CHECK(version>0), previous_id bigint UNIQUE REFERENCES wo_schedule_events(id),
order_id bigint NOT NULL REFERENCES wo_orders(id), equipment_id bigint NOT NULL REFERENCES wo_equipment(id), technician_id bigint NOT NULL REFERENCES users(id),
starts_at timestamptz NOT NULL, ends_at timestamptz NOT NULL, state text NOT NULL CHECK(state IN('scheduled','cancelled')),
reason text NOT NULL CHECK(length(reason)>=10), actor_id bigint NOT NULL REFERENCES users(id), command_key uuid NOT NULL,
request_hash char(64) NOT NULL, source_revision char(64) NOT NULL, snapshot jsonb NOT NULL, created_at timestamptz NOT NULL DEFAULT now(),
CHECK(ends_at>starts_at AND ends_at<=starts_at+interval '7 days'),
UNIQUE(tenant_id,company_id,unit_id,booking_key,version), UNIQUE(tenant_id,company_id,unit_id,actor_id,command_key),
FOREIGN KEY(tenant_id,company_id,unit_id) REFERENCES units(tenant_id,company_id,id)
);
CREATE FUNCTION sgf_schedule_guard() RETURNS trigger LANGUAGE plpgsql AS $$
DECLARE o wo_orders; p wo_schedule_events;
BEGIN
PERFORM sgf_wo_environment();
IF TG_OP<>'INSERT' THEN RAISE EXCEPTION 'SGF_SCHEDULE_APPEND_ONLY'; END IF;
PERFORM 1 FROM companies WHERE id=NEW.company_id FOR UPDATE;
SELECT * INTO STRICT o FROM wo_orders WHERE id=NEW.order_id;
IF ROW(o.tenant_id,o.company_id,o.unit_id,o.equipment_id) IS DISTINCT FROM ROW(NEW.tenant_id,NEW.company_id,NEW.unit_id,NEW.equipment_id)
OR NOT EXISTS(SELECT 1 FROM permission_grants WHERE user_id=NEW.actor_id AND tenant_id=NEW.tenant_id AND company_id=NEW.company_id AND unit_id=NEW.unit_id AND permission='workshop.assign')
OR (NEW.state='scheduled' AND (NOT EXISTS(SELECT 1 FROM permission_grants WHERE user_id=NEW.technician_id AND tenant_id=NEW.tenant_id AND company_id=NEW.company_id AND unit_id=NEW.unit_id AND permission='workshop.record') OR NEW.technician_id IS DISTINCT FROM (SELECT technician_id FROM wo_entries WHERE order_id=NEW.order_id AND kind='assignment' ORDER BY id DESC LIMIT 1)))
THEN RAISE EXCEPTION 'SGF_SCHEDULE_SCOPE_OR_AUTHORITY'; END IF;
SELECT * INTO p FROM wo_schedule_events WHERE (tenant_id,company_id,unit_id,booking_key)=(NEW.tenant_id,NEW.company_id,NEW.unit_id,NEW.booking_key) ORDER BY version DESC LIMIT 1;
IF (p.id IS NULL AND (NEW.version<>1 OR NEW.previous_id IS NOT NULL OR NEW.state<>'scheduled')) OR
(p.id IS NOT NULL AND (NEW.previous_id IS DISTINCT FROM p.id OR NEW.version<>p.version+1 OR NEW.order_id<>p.order_id OR p.state='cancelled')) THEN RAISE EXCEPTION 'SGF_SCHEDULE_STALE_VERSION'; END IF;
IF NEW.state='scheduled' AND EXISTS(SELECT 1 FROM wo_schedule_events e WHERE e.tenant_id=NEW.tenant_id AND e.company_id=NEW.company_id AND e.unit_id=NEW.unit_id AND e.booking_key<>NEW.booking_key AND e.state='scheduled'
AND NOT EXISTS(SELECT 1 FROM wo_schedule_events n WHERE n.previous_id=e.id)
AND (e.technician_id=NEW.technician_id OR e.equipment_id=NEW.equipment_id)
AND e.starts_at<NEW.ends_at AND e.ends_at>NEW.starts_at) THEN RAISE EXCEPTION 'SGF_SCHEDULE_RESOURCE_CONFLICT' USING ERRCODE='23514'; END IF;
RETURN NEW;
END; $$;
CREATE TRIGGER sgf_schedule_guard BEFORE INSERT OR UPDATE OR DELETE ON wo_schedule_events FOR EACH ROW EXECUTE FUNCTION sgf_schedule_guard();