fix: 增加回单核对与付款登记门禁(09-08-reimbursement-payment-closeout)
ci-e2e / e2e (push) Canceled after 0s
ci-minimal / check-minimal (push) Canceled after 0s

这个提交包含在:
2026-09-15 07:21:55 +08:00
父节点 229867f5fd
当前提交 8d08d46d16
共修改 33 个文件,包含 3691 行新增和 11 行删除
+9
查看文件
@@ -70,6 +70,15 @@ export const ERROR_REASON_MESSAGES: Record<string, string> = {
"payment_batch.invalid_identifier": "付款记录标识已失效,请刷新后重新选择。",
"payment_batch.invalid_input": "付款操作内容不完整或格式不正确,请核对页面必填项后重试。",
"payment_batch.bank_receipt_required": "银行付款登记前必须上传有效银行回单,请先补充回单。",
"payment_batch.receipt_retry_unavailable": "回单状态已变化或仍在识别,请刷新后查看;处理中超过15分钟仍未完成时可重试。",
"payment_batch.receipt_independent_review_required": "回单人工核验需要独立审批人复核;制单人、核验人及报销归属人或提交人不能自审。",
"payment_batch.receipt_transaction_reused": "回单交易已被使用或存在重复,请核对原回单;不能通过修改参考号重复登记。",
"payment_batch.receipt_manual_state_changed": "回单或核验版本已变化,请刷新后重新查看当前状态。",
"payment_batch.receipt_manual_verification_required": "回单信息尚不能确认与付款一致,请核对全部页、交易状态、金额、币种及收付款账号;不要直接登记付款。",
"payment_batch.receipt_key_unavailable": "回单核验密钥不可用,请联系管理员修复;不要重复识别或登记付款。",
"payment_batch.receipt_key_rotation_required": "回单交易记录使用的密钥版本不一致,请管理员完成受控密钥迁移;人工核验不能绕过。",
"payment_batch.receipt_snapshot_unsupported": "付款业务或银行快照不支持回单核验,请联系管理员核实;不要重复识别。",
"payment_batch.receipt_identity_unresolved": "无法完整确认报销参与人身份,请管理员核实人员关系后再复核;不能自行批准。",
"payment_batch.department_unavailable": "当前单据所属部门暂不能使用付款批次,请联系管理员确认处理方式。",
"payment_batch.source_not_found": "部分来源单据已不存在或不可用,请刷新后重新选择。",
"payment_batch.daily_sequence_exhausted": "今日付款批次编号已用完,请联系管理员处理,勿重复提交。",
+68
查看文件
@@ -118,6 +118,12 @@ export type PaymentBatchCandidatesResponse = {
};
export type PaymentBatchReceiptAttachment = {
ocrState?: "legacy" | "queued" | "processing" | "ocr_failed" | "needs_review" | "matched"
| "pending_review" | "association_ready" | "manual_approved" | "manual_rejected"
| "system_failed" | "key_failed" | "snapshot_failed";
ocrVersion?: number;
ocrStartedAt?: string | null;
ocrRetryAvailable?: boolean;
id: string;
originalName: string;
mimeType: string;
@@ -276,3 +282,65 @@ export async function voidPaymentBatchReceiptAttachment(batchId: string, attachm
{ method: "POST", body: JSON.stringify({ reason }) },
);
}
export async function retryPaymentBatchReceiptOcr(batchId: string, attachmentId: string, expectedVersion: number) {
return apiRequestData<{ item: { id: string; ocrState: string; ocrVersion: number } }>(
`/api/payment-batches/${batchId}/receipt-attachments/${attachmentId}/ocr-retry`,
{ method: "POST", body: JSON.stringify({ expectedVersion }) },
);
}
export type ReceiptVerificationPage = {
pageNumber: number;
amount: string | null;
currency: string | null;
payerAccountMasked: string | null;
payeeAccountMasked: string | null;
transactionStatus: string | null;
reference: string | null;
itemId?: string;
};
export type ReceiptVerificationDetail = {
canReview?: boolean;
version: number;
state: string;
pagesExpected: number;
pages: ReceiptVerificationPage[];
proposal: null | {
id: string; status: string; proposedBy: string; reviewedBy: string | null; reason: string;
changedFields: { pageNumber: number; fields: string[] }[];
pages: ReceiptVerificationPage[];
};
};
export function fetchReceiptVerification(batchId: string, attachmentId: string, signal?: AbortSignal) {
return apiRequestData<{ item: ReceiptVerificationDetail }>(
`/api/payment-batches/${batchId}/receipt-attachments/${attachmentId}/manual-verification`, { signal },
);
}
export function fetchReceiptReadiness(batchId: string, signal?: AbortSignal) {
return apiRequestData<{ item: { ready: boolean } }>(
`/api/payment-batches/${batchId}/receipt-readiness`, { signal },
);
}
export function saveReceiptVerification(batchId: string, attachmentId: string, body: {
expectedVersion: number; reason?: string;
pages: { pageNumber: number; itemId: string; fields?: { Name: string; Value: string }[] }[];
}) {
return apiRequestData<{ item: { id: string; status: string; version: number } }>(
`/api/payment-batches/${batchId}/receipt-attachments/${attachmentId}/manual-verification`,
{ method: "POST", body: JSON.stringify(body) },
);
}
export function reviewReceiptVerification(batchId: string, attachmentId: string, proposalId: string, body: {
expectedVersion: number; decision: "approve" | "reject"; reason: string;
}) {
return apiRequestData<{ item: { id: string; status: string; version: number } }>(
`/api/payment-batches/${batchId}/receipt-attachments/${attachmentId}/manual-verification/${proposalId}/review`,
{ method: "POST", body: JSON.stringify(body) },
);
}