文件
hyb-oa/server/prisma/migrations/20261007020000_loan_failure_attribution/migration.sql
T
wangsli和Claude Opus 5.5 db28706177 借款对账 S1、S2:导出与银行提交分开登记、确认失败收紧及失败回单识别口径(09-17-company-loan-principal-repayment)
S1(迁移 20261007010000)
- 借款导出逐次记录;存量批次保留已知导出次数为历史汇总,不视为真实导出,存在即拦截
- 银行提交登记(只能撤回一次)与提交清单确认;独立复核,复核后任何登记、撤回、导出即失效
- 确认失败只计未作废附件;作废附件无法证明为明确失败或确属他笔付款即阻断
- S3 前临时拦截:多次导出、多次提交、清单未复核或历史汇总均不能确认失败
- 确认失败改按付款后规则;确认人不得兼任导出、提交登记、清单确认

S2(迁移 20261007020000)
- 存在同金额候选时须有已复核、覆盖提交清单的失败归属;结算与失败归属分槽

失败回单识别口径(用户同意并入)
- 新增原始状态措辞字段(多字段原文不一致即为空);失败事实严格判定:单页完整、
  仅“交易失败/转账失败/付款失败/支付失败”、唯一问题为交易失败、字段齐全;
  已撤销、已冲正、已退汇不接受;顶层识别状态与成功匹配不变
- 测试改走真实识别与归一化链,仅替换云端 OCR 响应

验证:借款 HTTP 41 组、浏览器 7 组、76 迁移;server 4463;共享回单测试 237;
三轮同模型只读复核的阻断项均已修复。

注意:界面未完成,不能单独部署;真实回单待验。未推送、未部署。

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-07 11:47:24 +08:00

46 行
3.2 KiB
PL/PgSQL

-- 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 $$;