-- S2 of the loan bank-result reconciliation design (design.md 3.1): a failed -- receipt on a batch with same-lender, same-amount competitors needs an -- independently reviewed attribution that names the bank submissions it covers. -- AlterTable ALTER TABLE "loan_receipt_attributions" ADD COLUMN "kind" VARCHAR(16) NOT NULL DEFAULT 'settlement', ADD COLUMN "covered_submission_ids" JSONB NOT NULL DEFAULT '[]'; ALTER TABLE "loan_receipt_attributions" ADD CONSTRAINT "loan_receipt_attributions_kind_check" CHECK ("kind" IN ('settlement', 'failure')); ALTER TABLE "loan_receipt_attributions" ADD CONSTRAINT "loan_receipt_attributions_covered_check" CHECK ( jsonb_typeof("covered_submission_ids") = 'array' AND ("kind" <> 'failure' OR (jsonb_array_length("covered_submission_ids") > 0 AND jsonb_array_length("verification_ids") = 1))); ALTER TABLE "loan_receipt_attributions" DROP CONSTRAINT "loan_receipt_attributions_basis_kind_check"; ALTER TABLE "loan_receipt_attributions" ADD CONSTRAINT "loan_receipt_attributions_basis_kind_check" CHECK ("basis_kind" IN ('customer_serial', 'bank_memo', 'bank_written_confirmation', 'competitors_resolved', 'bank_instruction_ref')); -- One open/approved attribution per batch and kind. DROP INDEX "loan_receipt_attributions_one_active_per_batch"; CREATE UNIQUE INDEX "loan_receipt_attributions_one_active_per_batch_kind" ON "loan_receipt_attributions"("payment_batch_id", "kind") WHERE "status" IN ('proposed', 'approved'); -- The proposal (now including kind and covered submissions) stays immutable. CREATE OR REPLACE FUNCTION guard_loan_receipt_attribution_history() RETURNS trigger LANGUAGE plpgsql AS $$ BEGIN IF TG_OP = 'DELETE' THEN RAISE EXCEPTION 'loan receipt attribution history is immutable'; END IF; IF OLD."status" IN ('rejected', 'superseded') THEN RAISE EXCEPTION 'closed loan receipt attribution is immutable'; END IF; IF OLD."status" = 'approved' AND (NEW."status" <> 'superseded' OR NEW."reviewed_by" IS DISTINCT FROM OLD."reviewed_by" OR NEW."reviewed_at" IS DISTINCT FROM OLD."reviewed_at" OR NEW."review_note" IS DISTINCT FROM OLD."review_note") THEN RAISE EXCEPTION 'approved loan receipt attribution can only be superseded'; END IF; IF OLD."status" = 'proposed' AND NEW."status" IN ('proposed', 'superseded') AND (NEW."reviewed_by" IS NOT NULL OR NEW."reviewed_at" IS NOT NULL OR NEW."review_note" IS NOT NULL) THEN RAISE EXCEPTION 'unreviewed loan receipt attribution cannot carry review fields'; END IF; IF NEW."payment_batch_id" <> OLD."payment_batch_id" OR NEW."fingerprint" <> OLD."fingerprint" OR NEW."key_id" <> OLD."key_id" OR NEW."verification_ids" <> OLD."verification_ids" OR NEW."manual_proposal_ids" <> OLD."manual_proposal_ids" OR NEW."competitor_batch_ids" <> OLD."competitor_batch_ids" OR NEW."basis_kind" <> OLD."basis_kind" OR NEW."basis_note" <> OLD."basis_note" OR NEW."exclusion_note" <> OLD."exclusion_note" OR NEW."proposed_by" <> OLD."proposed_by" OR NEW."proposed_at" <> OLD."proposed_at" OR NEW."kind" <> OLD."kind" OR NEW."covered_submission_ids" <> OLD."covered_submission_ids" THEN RAISE EXCEPTION 'loan receipt attribution proposal is immutable'; END IF; RETURN NEW; END $$;