CREATE TABLE IF NOT EXISTS audit_log ( id bigserial PRIMARY KEY, table_name text NOT NULL, op text NOT NULL, pk jsonb, old_data jsonb, new_data jsonb, actor_id bigint, ip text, ua text, changed_at timestamptz NOT NULL DEFAULT now() ); CREATE INDEX IF NOT EXISTS idx_audit_log_table_time ON audit_log (table_name, changed_at DESC); CREATE OR REPLACE FUNCTION audit_row_change() RETURNS trigger LANGUAGE plpgsql AS $$ DECLARE v_actor_id bigint; v_ip text; v_ua text; v_pk jsonb; BEGIN v_actor_id := NULLIF(current_setting('app.current_user_id', true), '')::bigint; v_ip := NULLIF(current_setting('app.current_ip', true), ''); v_ua := NULLIF(current_setting('app.current_ua', true), ''); IF TG_OP = 'INSERT' THEN v_pk := jsonb_build_object('id', NEW.id); INSERT INTO audit_log(table_name, op, pk, old_data, new_data, actor_id, ip, ua) VALUES (TG_TABLE_NAME, TG_OP, v_pk, NULL, to_jsonb(NEW), v_actor_id, v_ip, v_ua); RETURN NEW; ELSIF TG_OP = 'UPDATE' THEN v_pk := jsonb_build_object('id', NEW.id); INSERT INTO audit_log(table_name, op, pk, old_data, new_data, actor_id, ip, ua) VALUES (TG_TABLE_NAME, TG_OP, v_pk, to_jsonb(OLD), to_jsonb(NEW), v_actor_id, v_ip, v_ua); RETURN NEW; ELSE v_pk := jsonb_build_object('id', OLD.id); INSERT INTO audit_log(table_name, op, pk, old_data, new_data, actor_id, ip, ua) VALUES (TG_TABLE_NAME, TG_OP, v_pk, to_jsonb(OLD), NULL, v_actor_id, v_ip, v_ua); RETURN OLD; END IF; END; $$; DROP TRIGGER IF EXISTS audit_sales_orders ON sales_orders; CREATE TRIGGER audit_sales_orders AFTER INSERT OR UPDATE OR DELETE ON sales_orders FOR EACH ROW EXECUTE FUNCTION audit_row_change(); DROP TRIGGER IF EXISTS audit_purchase_orders ON purchase_orders; CREATE TRIGGER audit_purchase_orders AFTER INSERT OR UPDATE OR DELETE ON purchase_orders FOR EACH ROW EXECUTE FUNCTION audit_row_change(); DROP TRIGGER IF EXISTS audit_stock_movements ON stock_movements; CREATE TRIGGER audit_stock_movements AFTER INSERT OR UPDATE OR DELETE ON stock_movements FOR EACH ROW EXECUTE FUNCTION audit_row_change();