Accept isolated WORKSHOP-RESOURCE-RECORD-v1 delivery
Record explicit workshop acceptance and preserve subsequent synthetic work without accepting receivables. Keep 080000/070000 unapplied, real workshop grants pending and operational issuance blocks intact. Exclude local credentials and real-data preservation reports.
This commit is contained in:
@@ -0,0 +1,136 @@
|
||||
CREATE TABLE sr_records (
|
||||
id bigserial PRIMARY KEY, tenant_id bigint NOT NULL, company_id bigint NOT NULL, unit_id bigint NOT NULL,
|
||||
invoice_id bigint NOT NULL REFERENCES si_invoices(id), client_id bigint NOT NULL REFERENCES a_clients(id),
|
||||
obligation_id bigint REFERENCES sr_records(id), receipt_id bigint REFERENCES sr_records(id),
|
||||
kind text NOT NULL CHECK(kind IN ('obligation','receipt','observation','correction')),
|
||||
currency text NOT NULL CHECK(currency='MZN'), amount numeric NOT NULL CHECK(amount>=0 AND amount<10000000000000000 AND amount=trunc(amount,2)),
|
||||
source text NOT NULL, external_id text NOT NULL, data jsonb NOT NULL,
|
||||
version integer NOT NULL DEFAULT 1 CHECK(version=1), actor_id bigint NOT NULL REFERENCES users(id), created_at timestamptz NOT NULL,
|
||||
UNIQUE(company_id,unit_id,kind,source,external_id),
|
||||
FOREIGN KEY(tenant_id,company_id,unit_id) REFERENCES units(tenant_id,company_id,id)
|
||||
);
|
||||
CREATE UNIQUE INDEX sr_one_obligation ON sr_records(invoice_id) WHERE kind='obligation';
|
||||
CREATE UNIQUE INDEX sr_reduction_document ON sr_records(company_id,unit_id,(data->>'document_ref')) WHERE kind='correction' AND data->>'action'='reduction';
|
||||
CREATE TABLE sr_effects (
|
||||
id bigserial PRIMARY KEY, record_id bigint NOT NULL REFERENCES sr_records(id),
|
||||
obligation_id bigint NOT NULL REFERENCES sr_records(id), receipt_id bigint REFERENCES sr_records(id),
|
||||
kind text NOT NULL CHECK(kind IN ('open','receipt_confirm','refused','allocate','reconcile','divergence','reverse_allocation','reverse_reconcile','reverse_receipt','reduction','due')),
|
||||
amount numeric NOT NULL CHECK(amount>=0 AND amount<10000000000000000 AND amount=trunc(amount,2)), target_effect_id bigint REFERENCES sr_effects(id),
|
||||
revision integer NOT NULL CHECK(revision>=0), data jsonb NOT NULL,
|
||||
actor_id bigint NOT NULL REFERENCES users(id), created_at timestamptz NOT NULL
|
||||
);
|
||||
CREATE UNIQUE INDEX sr_terminal_decision ON sr_effects(record_id) WHERE kind NOT IN ('allocate','divergence');
|
||||
CREATE UNIQUE INDEX sr_reverse_once ON sr_effects(target_effect_id) WHERE target_effect_id IS NOT NULL;
|
||||
CREATE UNIQUE INDEX sr_divergence_once ON sr_effects(record_id) WHERE kind='divergence';
|
||||
CREATE INDEX sr_obligation_effects ON sr_effects(obligation_id,id);
|
||||
CREATE TABLE sr_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), action text NOT NULL, command_key varchar(100) NOT NULL,
|
||||
request_hash varchar(64) 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 sr_events (
|
||||
id bigserial PRIMARY KEY, command_id bigint NOT NULL UNIQUE REFERENCES sr_commands(id),
|
||||
type text NOT NULL, schema_version integer NOT NULL CHECK(schema_version=1), payload jsonb NOT NULL, created_at timestamptz NOT NULL
|
||||
);
|
||||
CREATE TABLE sr_outbox (event_id bigint PRIMARY KEY REFERENCES sr_events(id), state text NOT NULL CHECK(state='captured_simulation_only'));
|
||||
CREATE TABLE sr_deliveries (event_id bigint PRIMARY KEY REFERENCES sr_events(id), consumer text NOT NULL CHECK(consumer='simulation_integrity_v1'), created_at timestamptz NOT NULL);
|
||||
|
||||
CREATE OR REPLACE FUNCTION sgf_sr_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_SR_TEST_DATABASE_ONLY'; END IF;
|
||||
END; $$;
|
||||
|
||||
CREATE OR REPLACE FUNCTION sgf_sr_record_guard() RETURNS trigger LANGUAGE plpgsql AS $$
|
||||
DECLARE inv si_invoices; ob sr_records; rc sr_records; cid bigint; expected numeric;
|
||||
BEGIN
|
||||
PERFORM sgf_sr_environment();
|
||||
PERFORM 1 FROM companies WHERE id=NEW.company_id FOR UPDATE;
|
||||
SELECT * INTO STRICT inv FROM si_invoices WHERE id=NEW.invoice_id;
|
||||
SELECT (snapshot->>'client_id')::bigint INTO STRICT cid FROM a_history
|
||||
WHERE area='billing_drafts' AND record_id=inv.draft_id AND version=(inv.snapshot->'document'->>'draft_version')::integer
|
||||
AND tenant_id=inv.tenant_id AND company_id=inv.company_id AND unit_id=inv.unit_id;
|
||||
IF ROW(inv.tenant_id,inv.company_id,inv.unit_id,cid) IS DISTINCT FROM ROW(NEW.tenant_id,NEW.company_id,NEW.unit_id,NEW.client_id)
|
||||
OR inv.snapshot->>'notice' IS DISTINCT FROM 'TESTE — SEM VALIDADE FISCAL'
|
||||
OR inv.snapshot->>'simulation_only' IS DISTINCT FROM 'true'
|
||||
OR inv.snapshot->'document'->'data'->>'currency' IS DISTINCT FROM NEW.currency
|
||||
THEN RAISE EXCEPTION 'SGF_SR_DOCUMENT_SCOPE'; END IF;
|
||||
IF NEW.data->>'policy' IS DISTINCT FROM 'SGF-RECEIVABLES-SIM-v1' THEN RAISE EXCEPTION 'SGF_SR_POLICY'; END IF;
|
||||
IF NEW.kind='obligation' THEN
|
||||
expected=(inv.snapshot->'calculation'->>'total_document_proposed')::numeric;
|
||||
IF NEW.obligation_id IS NOT NULL OR NEW.receipt_id IS NOT NULL OR NEW.amount<>expected OR expected<=0 OR trunc(expected,2)<>expected THEN RAISE EXCEPTION 'SGF_SR_ORIGIN_AMOUNT'; END IF;
|
||||
ELSE
|
||||
SELECT * INTO STRICT ob FROM sr_records WHERE id=NEW.obligation_id AND kind='obligation';
|
||||
IF ROW(ob.tenant_id,ob.company_id,ob.unit_id,ob.invoice_id,ob.client_id,ob.currency) IS DISTINCT FROM ROW(NEW.tenant_id,NEW.company_id,NEW.unit_id,NEW.invoice_id,NEW.client_id,NEW.currency)
|
||||
OR NOT EXISTS(SELECT 1 FROM sr_effects WHERE record_id=ob.id AND kind='open') THEN RAISE EXCEPTION 'SGF_SR_OBLIGATION_SCOPE'; END IF;
|
||||
IF NEW.kind='receipt' AND (NEW.amount<=0 OR NEW.receipt_id IS NOT NULL OR coalesce(NEW.data->>'evidence','')='' OR coalesce(NEW.data->>'date','')='') THEN RAISE EXCEPTION 'SGF_SR_RECEIPT'; END IF;
|
||||
IF NEW.receipt_id IS NOT NULL THEN
|
||||
SELECT * INTO STRICT rc FROM sr_records WHERE id=NEW.receipt_id AND kind='receipt';
|
||||
IF rc.obligation_id<>ob.id THEN RAISE EXCEPTION 'SGF_SR_RECEIPT_SCOPE'; END IF;
|
||||
END IF;
|
||||
IF NEW.kind='observation' AND (NEW.receipt_id IS NULL OR coalesce(NEW.data->>'date','')='') THEN RAISE EXCEPTION 'SGF_SR_OBSERVATION'; END IF;
|
||||
IF NEW.kind='correction' AND (NEW.data->>'action' NOT IN ('reverse_allocation','reverse_reconcile','reverse_receipt','reduction','due') OR coalesce(NEW.data->>'reason','')='') THEN RAISE EXCEPTION 'SGF_SR_CORRECTION'; END IF;
|
||||
END IF;
|
||||
RETURN NEW;
|
||||
EXCEPTION WHEN NO_DATA_FOUND THEN RAISE EXCEPTION 'SGF_SR_INVALID_REFERENCE';
|
||||
END; $$;
|
||||
CREATE TRIGGER sr_record_guard BEFORE INSERT ON sr_records FOR EACH ROW EXECUTE FUNCTION sgf_sr_record_guard();
|
||||
|
||||
CREATE OR REPLACE FUNCTION sgf_sr_effect_guard() RETURNS trigger LANGUAGE plpgsql AS $$
|
||||
DECLARE r sr_records; ob sr_records; rc sr_records; target sr_effects; balance numeric; available numeric; n integer; a text;
|
||||
BEGIN
|
||||
PERFORM sgf_sr_environment();
|
||||
SELECT * INTO STRICT r FROM sr_records WHERE id=NEW.record_id;
|
||||
PERFORM 1 FROM companies WHERE id=r.company_id FOR UPDATE;
|
||||
SELECT * INTO STRICT ob FROM sr_records WHERE id=NEW.obligation_id AND kind='obligation';
|
||||
IF (CASE WHEN r.kind='obligation' THEN r.id ELSE r.obligation_id END)<>ob.id THEN RAISE EXCEPTION 'SGF_SR_EFFECT_SCOPE'; END IF;
|
||||
SELECT count(*) INTO n FROM sr_effects WHERE obligation_id=ob.id;
|
||||
IF NEW.revision<>n THEN RAISE EXCEPTION 'SGF_SR_STALE_REVISION'; END IF;
|
||||
IF NEW.kind IN ('open','receipt_confirm','refused','reconcile','divergence','reverse_allocation','reverse_reconcile','reverse_receipt','reduction','due') AND NEW.actor_id=r.actor_id THEN RAISE EXCEPTION 'SGF_SR_SELF_APPROVAL'; END IF;
|
||||
IF NEW.kind='open' AND (r.kind<>'obligation' OR NEW.amount<>r.amount) THEN RAISE EXCEPTION 'SGF_SR_OPEN'; END IF;
|
||||
IF NEW.kind='refused' AND (r.kind NOT IN ('obligation','receipt','correction') OR NEW.amount<>0) THEN RAISE EXCEPTION 'SGF_SR_REFUSE'; END IF;
|
||||
IF NEW.kind='receipt_confirm' AND (r.kind<>'receipt' OR NEW.amount<>r.amount) THEN RAISE EXCEPTION 'SGF_SR_CONFIRM'; END IF;
|
||||
IF NEW.receipt_id IS NOT NULL THEN
|
||||
SELECT * INTO STRICT rc FROM sr_records WHERE id=NEW.receipt_id AND kind='receipt';
|
||||
IF rc.obligation_id<>ob.id OR NOT EXISTS(SELECT 1 FROM sr_effects WHERE record_id=rc.id AND kind='receipt_confirm')
|
||||
OR EXISTS(SELECT 1 FROM sr_effects WHERE receipt_id=rc.id AND kind='reverse_receipt') THEN RAISE EXCEPTION 'SGF_SR_RECEIPT_UNAVAILABLE'; END IF;
|
||||
END IF;
|
||||
IF NEW.kind='receipt_confirm' AND NEW.receipt_id IS NOT NULL THEN RAISE EXCEPTION 'SGF_SR_CONFIRM_LINK'; END IF;
|
||||
IF NEW.kind='receipt_confirm' AND NEW.amount+(SELECT coalesce(sum(CASE WHEN kind='receipt_confirm' THEN amount WHEN kind='reverse_receipt' THEN -amount ELSE 0 END),0) FROM sr_effects WHERE obligation_id=ob.id)>=10000000000000000 THEN RAISE EXCEPTION 'SGF_SR_TOTAL_OVERFLOW'; END IF;
|
||||
IF NEW.kind NOT IN ('reverse_allocation','reverse_reconcile') AND NEW.target_effect_id IS NOT NULL THEN RAISE EXCEPTION 'SGF_SR_UNEXPECTED_TARGET'; END IF;
|
||||
IF NEW.kind NOT IN ('open','refused') AND NOT EXISTS(SELECT 1 FROM sr_effects WHERE record_id=ob.id AND kind='open') THEN RAISE EXCEPTION 'SGF_SR_NOT_OPEN'; END IF;
|
||||
SELECT ob.amount-coalesce(sum(CASE WHEN kind IN ('allocate','reduction') THEN amount WHEN kind='reverse_allocation' THEN -amount ELSE 0 END),0) INTO balance FROM sr_effects WHERE obligation_id=ob.id;
|
||||
IF NEW.kind='allocate' THEN
|
||||
IF r.kind<>'receipt' OR NEW.receipt_id IS DISTINCT FROM r.id OR NEW.amount<=0 THEN RAISE EXCEPTION 'SGF_SR_ALLOCATION'; END IF;
|
||||
SELECT rc.amount-coalesce(sum(CASE WHEN kind='allocate' THEN amount WHEN kind='reverse_allocation' THEN -amount ELSE 0 END),0) INTO available FROM sr_effects WHERE receipt_id=rc.id;
|
||||
IF NEW.amount>balance OR NEW.amount>available THEN RAISE EXCEPTION 'SGF_SR_OVERALLOCATION'; END IF;
|
||||
END IF;
|
||||
IF NEW.kind IN ('reconcile','divergence') THEN
|
||||
IF r.kind<>'observation' OR r.receipt_id IS DISTINCT FROM NEW.receipt_id THEN RAISE EXCEPTION 'SGF_SR_RECONCILE_SCOPE'; END IF;
|
||||
IF NEW.kind='reconcile' AND (r.amount<>rc.amount OR NEW.amount<>rc.amount) THEN RAISE EXCEPTION 'SGF_SR_RECONCILE_AMOUNT'; END IF;
|
||||
IF NEW.kind='divergence' AND (r.amount=rc.amount OR NEW.amount<>abs(r.amount-rc.amount)) THEN RAISE EXCEPTION 'SGF_SR_DIVERGENCE'; END IF;
|
||||
IF EXISTS(SELECT 1 FROM sr_effects e WHERE e.kind='reconcile' AND (e.receipt_id=rc.id OR e.record_id=r.id) AND NOT EXISTS(SELECT 1 FROM sr_effects v WHERE v.target_effect_id=e.id)) THEN RAISE EXCEPTION 'SGF_SR_OBSERVATION_REUSED'; END IF;
|
||||
END IF;
|
||||
IF NEW.kind IN ('reverse_allocation','reverse_reconcile','reverse_receipt','reduction','due') THEN
|
||||
IF r.kind<>'correction' OR r.data->>'action' IS DISTINCT FROM NEW.kind OR (r.data->>'revision')::integer IS DISTINCT FROM n THEN RAISE EXCEPTION 'SGF_SR_CORRECTION_VERSION'; END IF;
|
||||
IF NEW.kind IN ('reverse_allocation','reverse_reconcile') THEN
|
||||
SELECT * INTO STRICT target FROM sr_effects WHERE id=NEW.target_effect_id AND obligation_id=ob.id;
|
||||
IF target.kind<>(CASE WHEN NEW.kind='reverse_allocation' THEN 'allocate' ELSE 'reconcile' END) OR NEW.amount<>target.amount OR NEW.receipt_id IS DISTINCT FROM target.receipt_id OR (r.data->>'target_effect_id')::bigint IS DISTINCT FROM target.id THEN RAISE EXCEPTION 'SGF_SR_REVERSAL_TARGET'; END IF;
|
||||
ELSIF NEW.target_effect_id IS NOT NULL THEN RAISE EXCEPTION 'SGF_SR_UNEXPECTED_TARGET'; END IF;
|
||||
IF NEW.kind='reverse_receipt' THEN
|
||||
IF r.receipt_id IS DISTINCT FROM NEW.receipt_id OR rc.id IS NULL OR NEW.amount<>rc.amount THEN RAISE EXCEPTION 'SGF_SR_REVERSAL_RECEIPT'; END IF;
|
||||
SELECT coalesce(sum(CASE WHEN kind='allocate' THEN amount WHEN kind='reverse_allocation' THEN -amount ELSE 0 END),0) INTO available FROM sr_effects WHERE receipt_id=rc.id;
|
||||
IF available<>0 OR EXISTS(SELECT 1 FROM sr_effects e WHERE e.receipt_id=rc.id AND e.kind='reconcile' AND NOT EXISTS(SELECT 1 FROM sr_effects v WHERE v.target_effect_id=e.id)) THEN RAISE EXCEPTION 'SGF_SR_REVERSAL_DEPENDENCIES'; END IF;
|
||||
END IF;
|
||||
IF NEW.kind='reduction' AND (NEW.amount<=0 OR NEW.amount<>r.amount OR NEW.amount>balance OR coalesce(r.data->>'document_ref','') NOT LIKE 'TEST-REDUCTION-%' OR r.data->>'line' IS DISTINCT FROM '1') THEN RAISE EXCEPTION 'SGF_SR_REDUCTION'; END IF;
|
||||
IF NEW.kind='due' AND (NEW.amount<>0 OR NEW.data->>'due_date' IS DISTINCT FROM r.data->>'due_date') THEN RAISE EXCEPTION 'SGF_SR_DUE'; END IF;
|
||||
END IF;
|
||||
IF NEW.kind IN ('open','refused','receipt_confirm','reduction','due') AND NEW.receipt_id IS NOT NULL THEN RAISE EXCEPTION 'SGF_SR_UNEXPECTED_RECEIPT'; END IF;
|
||||
RETURN NEW;
|
||||
EXCEPTION WHEN NO_DATA_FOUND THEN RAISE EXCEPTION 'SGF_SR_INVALID_REFERENCE';
|
||||
END; $$;
|
||||
CREATE TRIGGER sr_effect_guard BEFORE INSERT ON sr_effects FOR EACH ROW EXECUTE FUNCTION sgf_sr_effect_guard();
|
||||
|
||||
CREATE OR REPLACE FUNCTION sgf_sr_test_insert() RETURNS trigger LANGUAGE plpgsql AS $$
|
||||
BEGIN PERFORM sgf_sr_environment(); RETURN NEW; END; $$;
|
||||
@@ -0,0 +1,162 @@
|
||||
CREATE TABLE wo_equipment (
|
||||
id bigserial PRIMARY KEY, tenant_id bigint NOT NULL, company_id bigint NOT NULL, client_id bigint NOT NULL REFERENCES a_clients(id),
|
||||
identifier text NOT NULL, description text NOT NULL, serial text, actor_id bigint NOT NULL REFERENCES users(id), created_at timestamptz NOT NULL,
|
||||
UNIQUE(company_id,identifier), FOREIGN KEY(tenant_id,company_id) REFERENCES companies(tenant_id,id)
|
||||
);
|
||||
CREATE TABLE wo_orders (
|
||||
id bigserial PRIMARY KEY, tenant_id bigint NOT NULL, company_id bigint NOT NULL, unit_id bigint NOT NULL,
|
||||
client_id bigint NOT NULL REFERENCES a_clients(id), equipment_id bigint NOT NULL REFERENCES wo_equipment(id),
|
||||
reference text NOT NULL, request text NOT NULL, location text NOT NULL, evidence text NOT NULL,
|
||||
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 wo_entries (
|
||||
id bigserial PRIMARY KEY, order_id bigint NOT NULL REFERENCES wo_orders(id),
|
||||
kind text NOT NULL CHECK(kind IN ('assignment','task','authorization','start','time','material')),
|
||||
origin text NOT NULL, version integer NOT NULL CHECK(version>0), previous_id bigint UNIQUE REFERENCES wo_entries(id),
|
||||
task_id bigint REFERENCES wo_entries(id), authorization_id bigint REFERENCES wo_entries(id), start_id bigint REFERENCES wo_entries(id),
|
||||
technician_id bigint REFERENCES users(id), starts_at timestamptz, ends_at timestamptz, minutes bigint,
|
||||
quantity numeric CHECK(quantity>0 AND quantity<10000000000000000 AND quantity=trunc(quantity,8)),
|
||||
outside_authority boolean NOT NULL DEFAULT false, revision integer NOT NULL CHECK(revision>=0), data jsonb NOT NULL,
|
||||
actor_id bigint NOT NULL REFERENCES users(id), created_at timestamptz NOT NULL,
|
||||
UNIQUE(order_id,kind,origin,version)
|
||||
);
|
||||
CREATE UNIQUE INDEX wo_start_completion ON wo_entries(start_id) WHERE start_id IS NOT NULL;
|
||||
CREATE INDEX wo_order_entries ON wo_entries(order_id,id);
|
||||
CREATE TABLE wo_decisions (
|
||||
id bigserial PRIMARY KEY, entry_id bigint NOT NULL UNIQUE REFERENCES wo_entries(id),
|
||||
entry_version integer NOT NULL, revision integer NOT NULL, decision text NOT NULL CHECK(decision IN ('verified','refused','acknowledged')),
|
||||
reason text NOT NULL, actor_id bigint NOT NULL REFERENCES users(id), created_at timestamptz NOT NULL
|
||||
);
|
||||
CREATE TABLE wo_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), action text NOT NULL, command_key varchar(100) NOT NULL, request_hash varchar(64) NOT NULL,
|
||||
order_id bigint NOT NULL REFERENCES wo_orders(id), 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 wo_events (
|
||||
id bigserial PRIMARY KEY, command_id bigint NOT NULL UNIQUE REFERENCES wo_commands(id), type text NOT NULL,
|
||||
schema_version integer NOT NULL CHECK(schema_version=1), payload jsonb NOT NULL, created_at timestamptz NOT NULL
|
||||
);
|
||||
CREATE TABLE wo_outbox (event_id bigint PRIMARY KEY REFERENCES wo_events(id), state text NOT NULL CHECK(state='captured_no_cost_stock_or_billing'));
|
||||
|
||||
CREATE OR REPLACE FUNCTION sgf_wo_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_WO_TEST_DATABASE_ONLY'; END IF;
|
||||
END; $$;
|
||||
CREATE OR REPLACE FUNCTION sgf_wo_test_only() RETURNS trigger LANGUAGE plpgsql AS $$
|
||||
BEGIN PERFORM sgf_wo_environment(); RETURN NEW; END; $$;
|
||||
CREATE OR REPLACE FUNCTION sgf_wo_revision(oid bigint) RETURNS integer LANGUAGE sql VOLATILE AS $$
|
||||
SELECT (SELECT count(*) FROM wo_entries WHERE order_id=oid)::integer+(SELECT count(*) FROM wo_decisions d JOIN wo_entries e ON e.id=d.entry_id WHERE e.order_id=oid)::integer;
|
||||
$$;
|
||||
CREATE OR REPLACE FUNCTION sgf_wo_parent_guard() RETURNS trigger LANGUAGE plpgsql AS $$
|
||||
DECLARE eq wo_equipment;
|
||||
BEGIN
|
||||
PERFORM sgf_wo_environment();
|
||||
PERFORM 1 FROM companies WHERE id=NEW.company_id FOR UPDATE;
|
||||
IF 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_WO_CLIENT_SCOPE'; END IF;
|
||||
IF TG_TABLE_NAME='wo_orders' THEN
|
||||
SELECT * INTO STRICT eq FROM wo_equipment WHERE id=NEW.equipment_id;
|
||||
IF ROW(eq.tenant_id,eq.company_id,eq.client_id) IS DISTINCT FROM ROW(NEW.tenant_id,NEW.company_id,NEW.client_id) THEN RAISE EXCEPTION 'SGF_WO_EQUIPMENT_SCOPE'; END IF;
|
||||
END IF;
|
||||
RETURN NEW;
|
||||
EXCEPTION WHEN NO_DATA_FOUND THEN RAISE EXCEPTION 'SGF_WO_INVALID_REFERENCE';
|
||||
END; $$;
|
||||
CREATE TRIGGER wo_equipment_scope BEFORE INSERT ON wo_equipment FOR EACH ROW EXECUTE FUNCTION sgf_wo_parent_guard();
|
||||
CREATE TRIGGER wo_order_scope BEFORE INSERT ON wo_orders FOR EACH ROW EXECUTE FUNCTION sgf_wo_parent_guard();
|
||||
|
||||
CREATE OR REPLACE FUNCTION sgf_wo_entry_guard() RETURNS trigger LANGUAGE plpgsql AS $$
|
||||
DECLARE o wo_orders; p wo_entries; t wo_entries; au wo_entries; st wo_entries; x wo_entries; assigned bigint; pause jsonb; last_end timestamptz; pause_seconds numeric:=0; duration numeric; item text; used numeric; provided numeric; event_kind text;
|
||||
BEGIN
|
||||
PERFORM sgf_wo_environment();
|
||||
SELECT * INTO STRICT o FROM wo_orders WHERE id=NEW.order_id;
|
||||
PERFORM 1 FROM companies WHERE id=o.company_id FOR UPDATE;
|
||||
IF NEW.revision<>sgf_wo_revision(o.id) OR NEW.data->>'policy' IS DISTINCT FROM 'WORKSHOP-RESOURCE-RECORD-v1' THEN RAISE EXCEPTION 'SGF_WO_STALE_REVISION'; END IF;
|
||||
IF NEW.previous_id IS NOT NULL THEN
|
||||
SELECT * INTO STRICT p FROM wo_entries WHERE id=NEW.previous_id;
|
||||
IF ROW(p.order_id,p.kind,p.origin) IS DISTINCT FROM ROW(NEW.order_id,NEW.kind,NEW.origin) OR NEW.version<>p.version+1
|
||||
OR EXISTS(SELECT 1 FROM wo_entries WHERE previous_id=p.id) OR coalesce(NEW.data->>'correction_reason','')='' THEN RAISE EXCEPTION 'SGF_WO_VERSION_CHAIN'; END IF;
|
||||
IF NEW.kind IN ('task','time','material') AND p.actor_id<>NEW.actor_id THEN RAISE EXCEPTION 'SGF_WO_ORIGIN_AUTHOR'; END IF;
|
||||
ELSE
|
||||
IF NEW.version<>1 THEN RAISE EXCEPTION 'SGF_WO_VERSION'; END IF;
|
||||
END IF;
|
||||
SELECT technician_id INTO assigned FROM wo_entries WHERE order_id=o.id AND kind='assignment' ORDER BY id DESC LIMIT 1;
|
||||
IF NEW.kind='assignment' THEN
|
||||
IF NEW.technician_id IS NULL OR NOT EXISTS(SELECT 1 FROM permission_grants WHERE user_id=NEW.technician_id AND tenant_id=o.tenant_id AND company_id=o.company_id AND unit_id=o.unit_id AND permission='workshop.record')
|
||||
OR NOT EXISTS(SELECT 1 FROM company_memberships WHERE user_id=NEW.technician_id AND company_id=o.company_id AND tenant_id=o.tenant_id) THEN RAISE EXCEPTION 'SGF_WO_ASSIGNEE'; END IF;
|
||||
END IF;
|
||||
IF NEW.kind IN ('task','start','time','material') AND (assigned IS DISTINCT FROM NEW.actor_id OR NEW.technician_id IS DISTINCT FROM NEW.actor_id) THEN RAISE EXCEPTION 'SGF_WO_ASSIGNED_TECHNICIAN'; END IF;
|
||||
IF NEW.kind='task' THEN
|
||||
IF coalesce(NEW.data->>'description','')='' THEN RAISE EXCEPTION 'SGF_WO_TASK'; END IF;
|
||||
IF NEW.data->>'service_id' IS NOT NULL AND NOT EXISTS(SELECT 1 FROM a_services WHERE id=(NEW.data->>'service_id')::bigint AND company_id=o.company_id AND tenant_id=o.tenant_id) THEN RAISE EXCEPTION 'SGF_WO_SERVICE_SCOPE'; END IF;
|
||||
END IF;
|
||||
IF NEW.kind='authorization' THEN
|
||||
IF NEW.task_id IS NULL OR coalesce(NEW.data->>'authorized_by','')='' OR coalesce(NEW.data->>'capacity','')='' OR coalesce(NEW.data->>'authorized_at','')='' OR coalesce(NEW.data->>'channel','')='' OR coalesce(NEW.data->>'scope','')='' OR coalesce(NEW.data->>'evidence','')='' THEN RAISE EXCEPTION 'SGF_WO_AUTH_EVIDENCE'; END IF;
|
||||
IF (NEW.data->>'authorized_at')::timestamptz>NEW.created_at THEN RAISE EXCEPTION 'SGF_WO_FUTURE_AUTH'; END IF;
|
||||
END IF;
|
||||
IF NEW.kind IN ('authorization','start','time','material') THEN
|
||||
SELECT * INTO STRICT t FROM wo_entries WHERE id=NEW.task_id AND order_id=o.id AND kind='task';
|
||||
IF EXISTS(SELECT 1 FROM wo_entries WHERE previous_id=t.id) AND NEW.kind IN ('authorization','start') THEN RAISE EXCEPTION 'SGF_WO_STALE_TASK'; END IF;
|
||||
END IF;
|
||||
IF NEW.kind IN ('start','time','material') THEN
|
||||
NEW.outside_authority:=true;
|
||||
IF NEW.authorization_id IS NOT NULL THEN
|
||||
SELECT * INTO STRICT au FROM wo_entries WHERE id=NEW.authorization_id AND order_id=o.id AND kind='authorization' AND task_id=t.id;
|
||||
IF au.created_at<=coalesce(NEW.starts_at,NEW.created_at) AND (au.data->>'authorized_at')::timestamptz<=coalesce(NEW.starts_at,NEW.created_at)
|
||||
AND NOT EXISTS(SELECT 1 FROM wo_entries z WHERE z.previous_id=au.id AND z.created_at<=coalesce(NEW.ends_at,NEW.starts_at,NEW.created_at))
|
||||
AND NOT EXISTS(SELECT 1 FROM wo_entries z WHERE z.previous_id=t.id AND z.created_at<=coalesce(NEW.ends_at,NEW.starts_at,NEW.created_at)) THEN NEW.outside_authority:=false; END IF;
|
||||
END IF;
|
||||
IF coalesce(NEW.data->>'evidence','')='' THEN RAISE EXCEPTION 'SGF_WO_SOURCE_EVIDENCE'; END IF;
|
||||
IF NEW.previous_id IS NOT NULL AND p.outside_authority THEN NEW.outside_authority:=true; END IF;
|
||||
END IF;
|
||||
IF NEW.kind='start' THEN
|
||||
IF NEW.outside_authority OR NEW.starts_at IS NULL OR NEW.starts_at<>NEW.created_at THEN RAISE EXCEPTION 'SGF_WO_START_NOT_AUTHORIZED'; END IF;
|
||||
IF EXISTS(SELECT 1 FROM wo_entries e JOIN wo_orders w ON w.id=e.order_id WHERE w.company_id=o.company_id AND e.technician_id=NEW.technician_id AND e.kind='start' AND NOT EXISTS(SELECT 1 FROM wo_entries done WHERE done.start_id=e.id)) THEN RAISE EXCEPTION 'SGF_WO_ACTIVE_START'; END IF;
|
||||
END IF;
|
||||
IF NEW.kind='time' THEN
|
||||
IF coalesce(NEW.data->>'time_type','') NOT IN ('work','travel') OR jsonb_typeof(NEW.data->'pauses') IS DISTINCT FROM 'array' THEN RAISE EXCEPTION 'SGF_WO_TIME_TYPE'; END IF;
|
||||
IF NEW.starts_at IS NULL OR NEW.ends_at IS NULL OR NEW.ends_at<=NEW.starts_at OR NEW.ends_at>NEW.created_at THEN RAISE EXCEPTION 'SGF_WO_TIME_RANGE'; END IF;
|
||||
IF NEW.start_id IS NOT NULL THEN
|
||||
SELECT * INTO STRICT st FROM wo_entries WHERE id=NEW.start_id AND order_id=o.id AND kind='start' AND technician_id=NEW.technician_id;
|
||||
IF st.task_id<>NEW.task_id OR st.authorization_id IS DISTINCT FROM NEW.authorization_id OR st.starts_at<>NEW.starts_at THEN RAISE EXCEPTION 'SGF_WO_START_LINK'; END IF;
|
||||
ELSIF NEW.data->>'mode' IS DISTINCT FROM 'reported' THEN RAISE EXCEPTION 'SGF_WO_START_REQUIRED'; END IF;
|
||||
last_end:=NEW.starts_at;
|
||||
FOR pause IN SELECT value FROM jsonb_array_elements(NEW.data->'pauses') LOOP
|
||||
IF (pause->>'start')::timestamptz<last_end OR (pause->>'end')::timestamptz<=(pause->>'start')::timestamptz OR (pause->>'end')::timestamptz>NEW.ends_at THEN RAISE EXCEPTION 'SGF_WO_PAUSES'; END IF;
|
||||
pause_seconds:=pause_seconds+extract(epoch FROM ((pause->>'end')::timestamptz-(pause->>'start')::timestamptz)); last_end:=(pause->>'end')::timestamptz;
|
||||
END LOOP;
|
||||
duration:=extract(epoch FROM (NEW.ends_at-NEW.starts_at))-pause_seconds;
|
||||
IF duration IS NULL OR NEW.minutes IS NULL OR duration<=0 OR mod(duration,60)<>0 OR NEW.minutes IS DISTINCT FROM (duration/60)::bigint THEN RAISE EXCEPTION 'SGF_WO_EFFECTIVE_MINUTES'; END IF;
|
||||
-- Conservative occupied interval, half-open: pauses do not permit a second simultaneous task.
|
||||
IF EXISTS(SELECT 1 FROM wo_entries e JOIN wo_orders w ON w.id=e.order_id WHERE e.kind='time' AND w.company_id=o.company_id AND e.technician_id=NEW.technician_id
|
||||
AND NOT EXISTS(SELECT 1 FROM wo_entries z WHERE z.previous_id=e.id) AND e.id IS DISTINCT FROM NEW.previous_id
|
||||
AND tstzrange(e.starts_at,e.ends_at,'[)') && tstzrange(NEW.starts_at,NEW.ends_at,'[)')) THEN RAISE EXCEPTION 'SGF_WO_TIME_OVERLAP'; END IF;
|
||||
END IF;
|
||||
IF NEW.kind='material' THEN
|
||||
IF NEW.quantity IS NULL OR NEW.starts_at IS NULL OR NEW.starts_at>NEW.created_at OR coalesce(NEW.data->>'event','') NOT IN ('required','delivered','consumed','returned') OR coalesce(NEW.data->>'owner','') NOT IN ('company','client') OR coalesce(NEW.data->>'resource','')='' OR coalesce(NEW.data->>'unit','') NOT IN ('UN','KG','M','L') THEN RAISE EXCEPTION 'SGF_WO_MATERIAL'; END IF;
|
||||
-- resource identity, owner and UOM cannot change silently for one material reference.
|
||||
IF EXISTS(SELECT 1 FROM wo_entries e WHERE e.order_id=o.id AND e.kind='material' AND e.data->>'resource'=NEW.data->>'resource'
|
||||
AND ROW(e.data->>'owner',e.data->>'unit') IS DISTINCT FROM ROW(NEW.data->>'owner',NEW.data->>'unit')) THEN RAISE EXCEPTION 'SGF_WO_MATERIAL_UNIT_OWNER'; END IF;
|
||||
IF NEW.previous_id IS NOT NULL AND ROW(p.data->>'resource',p.data->>'owner',p.data->>'unit',p.data->>'event') IS DISTINCT FROM ROW(NEW.data->>'resource',NEW.data->>'owner',NEW.data->>'unit',NEW.data->>'event') THEN RAISE EXCEPTION 'SGF_WO_MATERIAL_ORIGIN'; END IF;
|
||||
IF NEW.data->>'event'<>'consumed' AND NOT coalesce(p.outside_authority,false) THEN NEW.outside_authority:=false; END IF;
|
||||
END IF;
|
||||
RETURN NEW;
|
||||
EXCEPTION WHEN NO_DATA_FOUND THEN RAISE EXCEPTION 'SGF_WO_INVALID_REFERENCE';
|
||||
END; $$;
|
||||
CREATE TRIGGER wo_entry_guard BEFORE INSERT ON wo_entries FOR EACH ROW EXECUTE FUNCTION sgf_wo_entry_guard();
|
||||
|
||||
CREATE OR REPLACE FUNCTION sgf_wo_decision_guard() RETURNS trigger LANGUAGE plpgsql AS $$
|
||||
DECLARE e wo_entries; o wo_orders;
|
||||
BEGIN
|
||||
PERFORM sgf_wo_environment();
|
||||
SELECT * INTO STRICT e FROM wo_entries WHERE id=NEW.entry_id;
|
||||
SELECT * INTO STRICT o FROM wo_orders WHERE id=e.order_id;
|
||||
PERFORM 1 FROM companies WHERE id=o.company_id FOR UPDATE;
|
||||
IF e.kind NOT IN ('task','time','material') OR NEW.actor_id=e.actor_id THEN RAISE EXCEPTION 'SGF_WO_SELF_VERIFICATION'; END IF;
|
||||
IF NEW.entry_version<>e.version OR EXISTS(SELECT 1 FROM wo_entries WHERE previous_id=e.id) OR NEW.revision<>sgf_wo_revision(o.id) THEN RAISE EXCEPTION 'SGF_WO_STALE_DECISION'; END IF;
|
||||
IF e.outside_authority AND NEW.decision='verified' THEN RAISE EXCEPTION 'SGF_WO_OUTSIDE_AUTHORITY'; END IF;
|
||||
IF NEW.decision='acknowledged' AND NOT e.outside_authority THEN RAISE EXCEPTION 'SGF_WO_ACK_ONLY_EXCEPTION'; END IF;
|
||||
RETURN NEW;
|
||||
EXCEPTION WHEN NO_DATA_FOUND THEN RAISE EXCEPTION 'SGF_WO_INVALID_REFERENCE';
|
||||
END; $$;
|
||||
CREATE TRIGGER wo_decision_guard BEFORE INSERT ON wo_decisions FOR EACH ROW EXECUTE FUNCTION sgf_wo_decision_guard();
|
||||
Reference in New Issue
Block a user