source: backend/src/main/resources/db.migration/R__billing_integrity.sql@ 20d16ca

Last change on this file since 20d16ca was 20d16ca, checked in by MBK <marija.karapandzova@…>, 2 weeks ago

Implement advanced database queries

  • Property mode set to 100644
File size: 4.9 KB
Line 
1-- schema addition: billing had no "issued" date, only payment_date (populated only once
2-- paid), so there was no way to measure how long a still-PENDING bill has been outstanding.
3-- Existing rows will backfill to NOW() at ALTER time, which is not historically accurate —
4-- acceptable for this project, but worth noting.
5ALTER TABLE billing ADD COLUMN IF NOT EXISTS created_at TIMESTAMP DEFAULT NOW();
6
7CREATE TABLE IF NOT EXISTS billing_audit_log (
8 audit_id BIGSERIAL PRIMARY KEY,
9 bill_id BIGINT NOT NULL REFERENCES billing(bill_id),
10 old_amount DECIMAL(12,2),
11 new_amount DECIMAL(12,2),
12 old_status TEXT,
13 new_status TEXT,
14 change_type TEXT CHECK (change_type IN ('INSERT', 'UPDATE', 'LINE_ITEM_ADD', 'LINE_ITEM_REMOVE')),
15 changed_at TIMESTAMP DEFAULT NOW()
16 );
17
18-- Note: billing_procedures links to the catalog procedure_id, not to a specific
19-- performed_procedures row, so when the same procedure type has been performed on more
20-- than one patient there is no reliable way to verify a billed line item belongs to the
21-- same patient as the bill. That check is intentionally left out rather than implemented
22-- unreliably.
23
24CREATE OR REPLACE FUNCTION recalculate_billing_total(p_bill_id BIGINT, p_change_type TEXT)
25RETURNS VOID
26LANGUAGE plpgsql
27AS $$
28DECLARE
29v_procedure_total DECIMAL;
30 v_lab_total DECIMAL;
31 v_new_total DECIMAL;
32BEGIN
33SELECT COALESCE(SUM(p.cost), 0) INTO v_procedure_total
34FROM billing_procedures bp
35 JOIN procedures p ON p.procedure_id = bp.procedure_id
36WHERE bp.bill_id = p_bill_id;
37
38SELECT COALESCE(SUM(lt.cost), 0) INTO v_lab_total
39FROM billing_lab_tests blt
40 JOIN lab_tests lt ON lt.test_id = blt.test_id
41WHERE blt.bill_id = p_bill_id;
42
43v_new_total := v_procedure_total + v_lab_total;
44
45UPDATE billing SET total_cost = v_new_total WHERE bill_id = p_bill_id;
46
47INSERT INTO billing_audit_log (bill_id, new_amount, change_type)
48VALUES (p_bill_id, v_new_total, p_change_type);
49END;
50$$;
51
52-- one shared trigger function, reused for both billing_procedures and billing_lab_tests
53-- (both tables have a bill_id column, so NEW.bill_id / OLD.bill_id works either way)
54CREATE OR REPLACE FUNCTION t1_billing_line_item_changed()
55RETURNS TRIGGER
56LANGUAGE plpgsql
57AS $$
58BEGIN
59 IF TG_OP = 'DELETE' THEN
60 PERFORM recalculate_billing_total(OLD.bill_id, 'LINE_ITEM_REMOVE');
61RETURN OLD;
62ELSE
63 PERFORM recalculate_billing_total(NEW.bill_id, 'LINE_ITEM_ADD');
64RETURN NEW;
65END IF;
66END;
67$$;
68
69DROP TRIGGER IF EXISTS trg_billing_procedures_update_total ON billing_procedures;
70CREATE TRIGGER trg_billing_procedures_update_total
71 AFTER INSERT OR DELETE ON billing_procedures
72FOR EACH ROW
73EXECUTE FUNCTION t1_billing_line_item_changed();
74
75DROP TRIGGER IF EXISTS trg_billing_lab_tests_update_total ON billing_lab_tests;
76CREATE TRIGGER trg_billing_lab_tests_update_total
77 AFTER INSERT OR DELETE ON billing_lab_tests
78FOR EACH ROW
79EXECUTE FUNCTION t1_billing_line_item_changed();
80
81CREATE OR REPLACE FUNCTION is_valid_billing_transition(p_old TEXT, p_new TEXT)
82RETURNS BOOLEAN
83LANGUAGE sql
84IMMUTABLE
85AS $$
86SELECT CASE
87 WHEN p_old = p_new THEN TRUE
88 WHEN p_old = 'PENDING' AND p_new IN ('PAID', 'CANCELLED') THEN TRUE
89 ELSE FALSE
90 END;
91$$;
92
93CREATE OR REPLACE FUNCTION t2_billing_status_transition()
94RETURNS TRIGGER
95LANGUAGE plpgsql
96AS $$
97BEGIN
98 IF NOT is_valid_billing_transition(OLD.payment_status, NEW.payment_status) THEN
99 RAISE EXCEPTION 'Cannot transition billing status from % to %',
100 OLD.payment_status, NEW.payment_status;
101END IF;
102
103 IF OLD.payment_status <> NEW.payment_status THEN
104 INSERT INTO billing_audit_log (bill_id, old_status, new_status, change_type)
105 VALUES (NEW.bill_id, OLD.payment_status, NEW.payment_status, 'UPDATE');
106END IF;
107
108RETURN NEW;
109END;
110$$;
111
112DROP TRIGGER IF EXISTS trg_billing_status_transition ON billing;
113CREATE TRIGGER trg_billing_status_transition
114 BEFORE UPDATE ON billing
115 FOR EACH ROW
116 EXECUTE FUNCTION t2_billing_status_transition();
117
118CREATE OR REPLACE VIEW v_overdue_billings AS
119SELECT
120 b.bill_id,
121 p.patient_id, p.first_name, p.last_name,
122 b.total_cost, b.payment_status, b.created_at,
123 CURRENT_DATE - b.created_at::DATE AS days_outstanding,
124 CASE
125 WHEN CURRENT_DATE - b.created_at::DATE > 60 THEN 'CRITICAL'
126 WHEN CURRENT_DATE - b.created_at::DATE > 30 THEN 'OVERDUE'
127 ELSE 'PENDING'
128END AS urgency
129FROM billing b
130JOIN medical_records mr ON b.record_id = mr.record_id
131JOIN patients p ON mr.patient_id = p.patient_id
132WHERE b.payment_status = 'PENDING'
133 AND CURRENT_DATE - b.created_at::DATE >= 30
134ORDER BY days_outstanding DESC;
135
136CREATE OR REPLACE PROCEDURE job_billing_alerts()
137LANGUAGE plpgsql
138AS $$
139DECLARE
140 v_overdue_count INT;
141BEGIN
142 SELECT COUNT(*) INTO v_overdue_count FROM v_overdue_billings WHERE days_outstanding > 30;
143 RAISE NOTICE 'Found % overdue billing records requiring follow-up', v_overdue_count;
144END;
145$$;
Note: See TracBrowser for help on using the repository browser.