125 lines
9.1 KiB
PL/PgSQL
125 lines
9.1 KiB
PL/PgSQL
-- Shared workflow capture is disabled until explicitly enabled by local configuration.
|
|
CREATE TABLE IF NOT EXISTS bmc_workflow_runtime (
|
|
id INTEGER PRIMARY KEY CHECK(id = 1),
|
|
enabled BOOLEAN NOT NULL DEFAULT FALSE,
|
|
mode TEXT NOT NULL DEFAULT 'shadow' CHECK(mode IN ('shadow','active')),
|
|
customer_ids JSONB NOT NULL DEFAULT '[]'::jsonb,
|
|
internal_enabled BOOLEAN NOT NULL DEFAULT FALSE,
|
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
|
);
|
|
ALTER TABLE bmc_workflow_runtime ADD COLUMN IF NOT EXISTS internal_enabled BOOLEAN NOT NULL DEFAULT FALSE;
|
|
INSERT INTO bmc_workflow_runtime(id) VALUES (1) ON CONFLICT DO NOTHING;
|
|
ALTER TABLE tmodule_times ADD COLUMN IF NOT EXISTS workflow_revision BIGINT NOT NULL DEFAULT 0;
|
|
ALTER TABLE tticket_prepaid_transactions ADD COLUMN IF NOT EXISTS workflow_time_entry_id INTEGER REFERENCES tmodule_times(id);
|
|
CREATE UNIQUE INDEX IF NOT EXISTS prepaid_workflow_entry_once ON tticket_prepaid_transactions(workflow_time_entry_id) WHERE workflow_time_entry_id IS NOT NULL;
|
|
CREATE TABLE IF NOT EXISTS bmc_workflow_settlements (
|
|
time_id INTEGER PRIMARY KEY REFERENCES tmodule_times(id),
|
|
method TEXT NOT NULL,
|
|
decision_id BIGINT,
|
|
draft_id INTEGER REFERENCES ordre_drafts(id),
|
|
transaction_id INTEGER REFERENCES tticket_prepaid_transactions(id),
|
|
agreement_id INTEGER REFERENCES customer_fixed_price_agreements(id),
|
|
hours NUMERIC(10,4) NOT NULL,
|
|
actor_id INTEGER,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
|
);
|
|
CREATE OR REPLACE FUNCTION bmc_capture_time_workflow() RETURNS trigger LANGUAGE plpgsql AS $$
|
|
BEGIN
|
|
IF TG_OP='UPDATE' AND (NEW.description,NEW.customer_id,NEW.sag_id,NEW.original_hours,NEW.worked_date,NEW.faktisk_tid_min,NEW.work_type)
|
|
IS NOT DISTINCT FROM (OLD.description,OLD.customer_id,OLD.sag_id,OLD.original_hours,OLD.worked_date,OLD.faktisk_tid_min,OLD.work_type) THEN
|
|
RETURN NEW;
|
|
END IF;
|
|
NEW.workflow_revision := CASE WHEN TG_OP='INSERT' THEN 1 ELSE OLD.workflow_revision+1 END;
|
|
IF EXISTS(SELECT 1 FROM bmc_workflow_runtime WHERE id=1 AND enabled) AND NEW.vtiger_id IS NULL THEN
|
|
INSERT INTO bmc_workflow_events(id,event_key,trigger,entity_type,entity_id,revision,context,chain)
|
|
VALUES(gen_random_uuid()::text,'time:'||NEW.id||':'||NEW.workflow_revision,
|
|
CASE WHEN TG_OP='INSERT' THEN 'time.created' ELSE 'time.updated' END,
|
|
'time_entry',NEW.id::text,NEW.workflow_revision,jsonb_build_object('time_id',NEW.id),COALESCE(NULLIF(current_setting('bmc.workflow_chain',true),'')::jsonb,'[]'::jsonb))
|
|
ON CONFLICT(event_key) DO NOTHING;
|
|
END IF;
|
|
RETURN NEW;
|
|
END $$;
|
|
DROP TRIGGER IF EXISTS bmc_time_workflow_capture ON tmodule_times;
|
|
CREATE TRIGGER bmc_time_workflow_capture BEFORE INSERT OR UPDATE ON tmodule_times FOR EACH ROW EXECUTE FUNCTION bmc_capture_time_workflow();
|
|
-- Case and tag changes invalidate affected un-settled registrations, using the same
|
|
-- transactional time trigger rather than a second queue or duplicate event publisher.
|
|
CREATE OR REPLACE FUNCTION bmc_invalidate_case_times() RETURNS trigger LANGUAGE plpgsql AS $$
|
|
BEGIN
|
|
IF EXISTS(SELECT 1 FROM bmc_workflow_runtime WHERE id=1 AND enabled) THEN
|
|
IF TG_OP='UPDATE' AND (NEW.customer_id,NEW.beskrivelse,NEW.status) IS DISTINCT FROM (OLD.customer_id,OLD.beskrivelse,OLD.status) THEN
|
|
UPDATE tmodule_times SET workflow_revision=workflow_revision+1 WHERE sag_id=NEW.id AND status<>'billed' AND economy_order_draft_id IS NULL;
|
|
INSERT INTO bmc_workflow_events(id,event_key,trigger,entity_type,entity_id,revision,context,chain)
|
|
SELECT gen_random_uuid()::text,'time:'||id||':'||workflow_revision,'time.updated','time_entry',id::text,workflow_revision,jsonb_build_object('time_id',id),COALESCE(NULLIF(current_setting('bmc.workflow_chain',true),'')::jsonb,'[]'::jsonb)
|
|
FROM tmodule_times WHERE sag_id=NEW.id AND status<>'billed' AND economy_order_draft_id IS NULL
|
|
ON CONFLICT(event_key) DO NOTHING;
|
|
END IF;
|
|
END IF;
|
|
RETURN NEW;
|
|
END $$;
|
|
DROP TRIGGER IF EXISTS bmc_case_workflow_capture ON sag_sager;
|
|
CREATE TRIGGER bmc_case_workflow_capture AFTER UPDATE ON sag_sager FOR EACH ROW EXECUTE FUNCTION bmc_invalidate_case_times();
|
|
CREATE OR REPLACE FUNCTION bmc_invalidate_tag_times() RETURNS trigger LANGUAGE plpgsql AS $$
|
|
DECLARE target_type TEXT; target_id INTEGER;
|
|
BEGIN
|
|
target_type:=CASE WHEN TG_OP='DELETE' THEN OLD.entity_type ELSE NEW.entity_type END;
|
|
target_id:=CASE WHEN TG_OP='DELETE' THEN OLD.entity_id ELSE NEW.entity_id END;
|
|
IF EXISTS(SELECT 1 FROM bmc_workflow_runtime WHERE id=1 AND enabled) AND target_type IN ('case','time_entry') THEN
|
|
UPDATE tmodule_times SET workflow_revision=workflow_revision+1
|
|
WHERE ((target_type='case' AND sag_id=target_id) OR (target_type='time_entry' AND id=target_id))
|
|
AND status<>'billed' AND economy_order_draft_id IS NULL;
|
|
INSERT INTO bmc_workflow_events(id,event_key,trigger,entity_type,entity_id,revision,context,chain)
|
|
SELECT gen_random_uuid()::text,'time:'||id||':'||workflow_revision,'time.updated','time_entry',id::text,workflow_revision,jsonb_build_object('time_id',id),COALESCE(NULLIF(current_setting('bmc.workflow_chain',true),'')::jsonb,'[]'::jsonb)
|
|
FROM tmodule_times WHERE ((target_type='case' AND sag_id=target_id) OR (target_type='time_entry' AND id=target_id))
|
|
AND status<>'billed' AND economy_order_draft_id IS NULL ON CONFLICT(event_key) DO NOTHING;
|
|
END IF;
|
|
IF TG_OP='DELETE' THEN RETURN OLD; ELSE RETURN NEW; END IF;
|
|
END $$;
|
|
DROP TRIGGER IF EXISTS bmc_tag_workflow_capture ON entity_tags;
|
|
CREATE TRIGGER bmc_tag_workflow_capture AFTER INSERT OR DELETE ON entity_tags FOR EACH ROW EXECUTE FUNCTION bmc_invalidate_tag_times();
|
|
-- Public event adapters reuse the same holdable queue across Hub modules.
|
|
CREATE OR REPLACE FUNCTION bmc_capture_module_workflow() RETURNS trigger LANGUAGE plpgsql AS $$
|
|
DECLARE payload JSONB; previous JSONB; etype TEXT; eid TEXT; event_trigger TEXT;
|
|
BEGIN
|
|
IF NOT EXISTS(SELECT 1 FROM bmc_workflow_runtime WHERE id=1 AND enabled) THEN
|
|
IF TG_OP='DELETE' THEN RETURN OLD; ELSE RETURN NEW; END IF;
|
|
END IF;
|
|
payload:=CASE WHEN TG_OP='DELETE' THEN to_jsonb(OLD) ELSE to_jsonb(NEW) END;
|
|
previous:=CASE WHEN TG_OP='UPDATE' THEN to_jsonb(OLD) ELSE '{}'::jsonb END;
|
|
etype:=TG_ARGV[0];
|
|
-- Match the mail module's outgoing classification. Sending, draft and
|
|
-- outbox records also represent authored mail, never incoming mail.
|
|
IF etype='mail' AND (
|
|
LOWER(COALESCE(payload->>'folder','')) LIKE 'sent%'
|
|
OR LOWER(COALESCE(payload->>'folder','')) LIKE 'outbox%'
|
|
OR LOWER(COALESCE(payload->>'folder','')) LIKE 'draft%'
|
|
OR LOWER(COALESCE(payload->>'status','')) IN ('sent','sending','draft')
|
|
OR LOWER(COALESCE(payload->>'direction','')) IN ('outbound','outgoing')
|
|
) THEN RETURN NEW; END IF;
|
|
eid:=payload->>'id';
|
|
event_trigger:=CASE WHEN TG_OP='INSERT' THEN TG_ARGV[1] WHEN TG_OP='UPDATE' THEN TG_ARGV[2] ELSE TG_ARGV[3] END;
|
|
IF event_trigger IS NULL OR event_trigger='' THEN
|
|
IF TG_OP='DELETE' THEN RETURN OLD; ELSE RETURN NEW; END IF;
|
|
END IF;
|
|
IF etype='case' AND TG_OP='UPDATE' AND payload->>'status'='Lukket' AND previous->>'status' IS DISTINCT FROM 'Lukket' THEN
|
|
event_trigger:='case.closed';
|
|
END IF;
|
|
IF TG_OP='UPDATE' AND payload-ARRAY['updated_at','last_synced_at'] IS NOT DISTINCT FROM previous-ARRAY['updated_at','last_synced_at'] THEN RETURN NEW; END IF;
|
|
IF etype='tag_assignment' THEN
|
|
etype:=payload->>'entity_type';eid:=payload->>'entity_id';
|
|
END IF;
|
|
INSERT INTO bmc_workflow_events(id,event_key,trigger,entity_type,entity_id,context,chain)
|
|
VALUES(gen_random_uuid()::text,event_trigger||':'||eid||':'||gen_random_uuid()::text,
|
|
event_trigger,etype,eid,payload,COALESCE(NULLIF(current_setting('bmc.workflow_chain',true),'')::jsonb,'[]'::jsonb));
|
|
IF TG_OP='DELETE' THEN RETURN OLD; ELSE RETURN NEW; END IF;
|
|
END $$;
|
|
DROP TRIGGER IF EXISTS bmc_case_generic_workflow ON sag_sager;
|
|
CREATE TRIGGER bmc_case_generic_workflow AFTER INSERT OR UPDATE ON sag_sager FOR EACH ROW EXECUTE FUNCTION bmc_capture_module_workflow('case','case.created','case.updated','');
|
|
DROP TRIGGER IF EXISTS bmc_tags_generic_workflow ON entity_tags;
|
|
CREATE TRIGGER bmc_tags_generic_workflow AFTER INSERT OR DELETE ON entity_tags FOR EACH ROW EXECUTE FUNCTION bmc_capture_module_workflow('tag_assignment','tags.added','','tags.removed');
|
|
DROP TRIGGER IF EXISTS bmc_customer_generic_workflow ON customers;
|
|
CREATE TRIGGER bmc_customer_generic_workflow AFTER INSERT OR UPDATE ON customers FOR EACH ROW EXECUTE FUNCTION bmc_capture_module_workflow('customer','customer.created','customer.updated','');
|
|
DROP TRIGGER IF EXISTS bmc_mail_generic_workflow ON email_messages;
|
|
CREATE TRIGGER bmc_mail_generic_workflow AFTER INSERT ON email_messages FOR EACH ROW EXECUTE FUNCTION bmc_capture_module_workflow('mail','mail.received','','');
|
|
DROP TRIGGER IF EXISTS bmc_task_generic_workflow ON sag_todo_steps;
|
|
CREATE TRIGGER bmc_task_generic_workflow AFTER INSERT OR UPDATE ON sag_todo_steps FOR EACH ROW EXECUTE FUNCTION bmc_capture_module_workflow('task','task.created','task.updated','');
|