-- ============================================================ -- SOFT DELETE & AUDIT LOG EXTENSION -- ============================================================ -- Add soft delete columns to core tables -- ============================================================ ALTER TABLE loads ADD COLUMN deleted_at TIMESTAMPTZ; ALTER TABLE shippers ADD COLUMN deleted_at TIMESTAMPTZ; ALTER TABLE vehicles ADD COLUMN deleted_at TIMESTAMPTZ; ALTER TABLE payments ADD COLUMN deleted_at TIMESTAMPTZ; ALTER TABLE portal_users ADD COLUMN deleted_at TIMESTAMPTZ; -- ============================================================ -- AUDIT LOG TABLE -- ============================================================ CREATE TABLE audit_logs ( id BIGSERIAL PRIMARY KEY, action TEXT NOT NULL, -- 'INSERT', 'UPDATE', 'DELETE' table_name TEXT NOT NULL, -- e.g., 'loads', 'shippers' row_id UUID, -- UUID of the affected row (if applicable) before_json JSONB, -- state before change after_json JSONB, -- state after change created_at TIMESTAMPTZ DEFAULT NOW(), user_id UUID, -- references admin who made change (nullable) notes TEXT ); -- Indexes for fast lookup CREATE INDEX idx_audit_logs_table ON audit_logs(table_name); CREATE INDEX idx_audit_logs_user ON audit_logs(user_id); CREATE INDEX idx_audit_logs_time ON audit_logs(created_at DESC); -- ============================================================ -- TRIGGERS FOR SOFT DELETE & AUDIT LOGGING -- ============================================================ -- Function to perform soft delete and log action CREATE OR REPLACE FUNCTION soft_delete_and_audit() RETURNS TRIGGER AS $$ DECLARE v_user_id UUID; BEGIN -- Determine which table triggered the trigger IF TG_TABLE_NAME = 'loads' THEN IF OLD.deleted_at IS NOT NULL THEN RETURN OLD; -- already soft-deleted END IF; UPDATE loads SET deleted_at = NOW() WHERE id = OLD.id; IF TG_OP = 'DELETE' THEN INSERT INTO audit_logs(action, table_name, row_id, before_json, created_at, user_id) VALUES ('DELETE', TG_TABLE_NAME, OLD.id::UUID, to_jsonb(OLD), NOW(), current_setting('user_id')::UUID); ELSIF TG_OP = 'UPDATE' THEN INSERT INTO audit_logs(action, table_name, row_id, before_json, after_json, created_at, user_id) VALUES ('UPDATE', TG_TABLE_NAME, NEW.id::UUID, to_jsonb(OLD), to_jsonb(NEW), NOW(), current_setting('user_id')::UUID); ELSIF TG_OP = 'INSERT' THEN INSERT INTO audit_logs(action, table_name, row_id, after_json, created_at, user_id) VALUES ('INSERT', TG_TABLE_NAME, NEW.id::UUID, NULL, NOW(), current_setting('user_id')::UUID); END IF; ELSIF TG_TABLE_NAME = 'shippers' THEN IF OLD.deleted_at IS NOT NULL THEN RETURN OLD; END IF; UPDATE shippers SET deleted_at = NOW() WHERE id = OLD.id; IF TG_OP = 'DELETE' THEN INSERT INTO audit_logs(action, table_name, row_id, before_json, created_at, user_id) VALUES ('DELETE', TG_TABLE_NAME, OLD.id::UUID, to_jsonb(OLD), NOW(), current_setting('user_id')::UUID); ELSIF TG_OP = 'UPDATE' THEN INSERT INTO audit_logs(action, table_name, row_id, before_json, after_json, created_at, user_id) VALUES ('UPDATE', TG_TABLE_NAME, NEW.id::UUID, to_jsonb(OLD), to_jsonb(NEW), NOW(), current_setting('user_id')::UUID); ELSIF TG_OP = 'INSERT' THEN INSERT INTO audit_logs(action, table_name, row_id, after_json, created_at, user_id) VALUES ('INSERT', TG_TABLE_NAME, NEW.id::UUID, to_jsonb(NEW), NOW(), current_setting('user_id')::UUID); END IF; ELSIF TG_TABLE_NAME = 'vehicles' THEN IF OLD.deleted_at IS NOT NULL THEN RETURN OLD; END IF; UPDATE vehicles SET deleted_at = NOW() WHERE id = OLD.id; IF TG_OP = 'DELETE' THEN INSERT INTO audit_logs(action, table_name, row_id, before_json, created_at, user_id) VALUES ('DELETE', TG_TABLE_NAME, OLD.id::UUID, to_jsonb(OLD), NOW(), current_setting('user_id')::UUID); ELSIF TG_OP = 'UPDATE' THEN INSERT INTO audit_logs(action, table_name, row_id, before_json, after_json, created_at, user_id) VALUES ('UPDATE', TG_TABLE_NAME, NEW.id::UUID, to_jsonb(OLD), to_jsonb(NEW), NOW(), current_setting('user_id')::UUID); ELSIF TG_OP = 'INSERT' THEN INSERT INTO audit_logs(action, table_name, row_id, after_json, created_at, user_id) VALUES ('INSERT', TG_TABLE_NAME, NEW.id::UUID, to_jsonb(NEW), NOW(), current_setting('user_id')::UUID); END IF; ELSIF TG_TABLE_NAME = 'payments' THEN IF OLD.deleted_at IS NOT NULL THEN RETURN OLD; END IF; UPDATE payments SET deleted_at = NOW() WHERE id = OLD.id; IF TG_OP = 'DELETE' THEN INSERT INTO audit_logs(action, table_name, row_id, before_json, created_at, user_id) VALUES ('DELETE', TG_TABLE_NAME, OLD.id::UUID, to_jsonb(OLD), NOW(), current_setting('user_id')::UUID); ELSIF TG_OP = 'UPDATE' THEN INSERT INTO audit_logs(action, table_name, row_id, before_json, after_json, created_at, user_id) VALUES ('UPDATE', TG_TABLE_NAME, NEW.id::UUID, to_jsonb(OLD), to_jsonb(NEW), NOW(), current_setting('user_id')::UUID); ELSIF TG_OP = 'INSERT' THEN INSERT INTO audit_logs(action, table_name, row_id, after_json, created_at, user_id) VALUES ('INSERT', TG_TABLE_NAME, NEW.id::UUID, to_jsonb(NEW), NOW(), current_setting('user_id')::UUID); END IF; END IF; RETURN COALESCE(NEW, OLD); END; $$ LANGUAGE plpgsql; -- Create triggers for soft delete and audit logging on each table CREATE TRIGGER trg_loads_soft_delete BEFORE UPDATE OR DELETE ON loads FOR EACH ROW EXECUTE FUNCTION soft_delete_and_audit(); CREATE TRIGGER trg_loads_soft_delete_INSERT BEFORE INSERT ON loads FOR EACH ROW EXECUTE FUNCTION soft_delete_and_audit(); CREATE TRIGGER trg_shippers_soft_delete BEFORE UPDATE OR DELETE ON shippers FOR EACH ROW EXECUTE FUNCTION soft_delete_and_audit(); CREATE TRIGGER trg_shippers_soft_delete_INSERT BEFORE INSERT ON shippers FOR EACH ROW EXECUTE FUNCTION soft_delete_and_audit(); CREATE TRIGGER trg_vehicles_soft_delete BEFORE UPDATE OR DELETE ON vehicles FOR EACH ROW EXECUTE FUNCTION soft_delete_and_audit(); CREATE TRIGGER trg_vehicles_soft_delete_INSERT BEFORE INSERT ON vehicles FOR EACH ROW EXECUTE FUNCTION soft_delete_and_audit(); CREATE TRIGGER trg_payments_soft_delete BEFORE UPDATE OR DELETE ON payments FOR EACH ROW EXECUTE FUNCTION soft_delete_and_audit(); CREATE TRIGGER trg_payments_soft_delete_INSERT BEFORE INSERT ON payments FOR EACH ROW EXECUTE FUNCTION soft_delete_and_audit(); -- ============================================================ -- AUDIT SETUP FUNCTION TO CAPTURE USER ID FROM SESSION CONTEXT -- ============================================================ -- Note: PostgreSQL cannot directly access Express session, so we pass user_id -- via SET user_id = '' before triggering audits. -- Application must set this variable before performing DB operations.