feat(报销): 收敛只读内容与审批上下文 [09-02-reimbursement-readonly-content-consistency]
这个提交包含在:
@@ -0,0 +1,149 @@
|
||||
import { readFileSync } from "node:fs";
|
||||
import path from "node:path";
|
||||
import { expect, test } from "@playwright/test";
|
||||
import { buildReimbursementApprovalEvidence } from "../../src/lib/reimbursementApprovalProjection";
|
||||
import {
|
||||
buildReimbursementAmountSummary,
|
||||
parseMoneyToMinorUnits,
|
||||
} from "../../src/lib/reimbursementWorkspace";
|
||||
|
||||
function source(relativePath: string) {
|
||||
return readFileSync(path.resolve(process.cwd(), relativePath), "utf8");
|
||||
}
|
||||
|
||||
test("报销金额使用整数分判断完整性且不产生展示用第二总额", () => {
|
||||
expect(parseMoneyToMinorUnits("0")).toBe(0n);
|
||||
expect(parseMoneyToMinorUnits("0.1")).toBe(10n);
|
||||
expect(parseMoneyToMinorUnits("1234.56")).toBe(123456n);
|
||||
expect(parseMoneyToMinorUnits("1,234.56")).toBeNull();
|
||||
expect(parseMoneyToMinorUnits("0.001")).toBeNull();
|
||||
expect(parseMoneyToMinorUnits("NaN")).toBeNull();
|
||||
expect(parseMoneyToMinorUnits(null)).toBeNull();
|
||||
|
||||
expect(buildReimbursementAmountSummary("0.30", [{ amount: "0.1" }, { amount: "0.20" }])).toEqual({
|
||||
authoritativeTotalAmount: "0.30",
|
||||
integrity: "ready",
|
||||
});
|
||||
expect(buildReimbursementAmountSummary("1000.00", [{ amount: "400" }, { amount: "600.00" }])).toEqual({
|
||||
authoritativeTotalAmount: "1000.00",
|
||||
integrity: "ready",
|
||||
});
|
||||
expect(buildReimbursementAmountSummary("100.00", [{ amount: "99.99" }])).toEqual({
|
||||
authoritativeTotalAmount: "100.00",
|
||||
integrity: "mismatch",
|
||||
});
|
||||
expect(buildReimbursementAmountSummary("100.00", [{ amount: "invalid" }])).toEqual({
|
||||
authoritativeTotalAmount: "100.00",
|
||||
integrity: "unavailable",
|
||||
});
|
||||
expect(buildReimbursementAmountSummary("0.00", [])).toEqual({
|
||||
authoritativeTotalAmount: "0.00",
|
||||
integrity: "ready",
|
||||
});
|
||||
});
|
||||
|
||||
test("PC 与移动明细只展示共享模型中的服务端权威合计", () => {
|
||||
const evidenceList = source("src/components/reimbursements/ReimbursementItemEvidenceList.tsx");
|
||||
const desktopDetail = source("src/pages/ReimbursementDetailPage.tsx");
|
||||
const mobileDetail = source("src/pages/MobileReimbursementDetailPage.tsx");
|
||||
const approvalAdapter = source("src/pages/reimbursement-workspace/ReimbursementApprovalAdapter.tsx");
|
||||
const desktopShell = source("src/components/reimbursements/ReimbursementDesktopWorkspaceShell.tsx");
|
||||
const mobileShell = source("src/components/reimbursements/ReimbursementMobileWorkspaceShell.tsx");
|
||||
|
||||
expect(evidenceList).toContain("<tfoot");
|
||||
expect(evidenceList).toContain("amountSummary.authoritativeTotalAmount");
|
||||
expect(evidenceList).not.toContain("items.reduce(");
|
||||
expect(desktopShell).toContain("model.amountSummary.authoritativeTotalAmount");
|
||||
expect(mobileShell).toContain("model.amountSummary.authoritativeTotalAmount");
|
||||
expect(desktopShell).not.toContain("model.header.totalAmount");
|
||||
expect(mobileShell).not.toContain("model.header.totalAmount");
|
||||
for (const consumer of [desktopDetail, mobileDetail, approvalAdapter]) {
|
||||
expect(consumer).toContain("buildReimbursementWorkspaceModel(");
|
||||
expect(consumer).not.toContain("buildReimbursementAmountSummary(");
|
||||
}
|
||||
});
|
||||
|
||||
test("PC 审批历史只进入右栏且移动历史保持正文末尾", () => {
|
||||
const desktopShell = source("src/components/reimbursements/ReimbursementDesktopWorkspaceShell.tsx");
|
||||
const mobileShell = source("src/components/reimbursements/ReimbursementMobileWorkspaceShell.tsx");
|
||||
const approvalAdapter = source("src/pages/reimbursement-workspace/ReimbursementApprovalAdapter.tsx");
|
||||
|
||||
expect(desktopShell).not.toContain("slots.timeline");
|
||||
expect(desktopShell.indexOf("slots.actionPanel")).toBeLessThan(desktopShell.indexOf("slots.asideContext"));
|
||||
expect(mobileShell.indexOf("slots.afterEvidence")).toBeLessThan(mobileShell.indexOf("slots.timeline"));
|
||||
expect(approvalAdapter).toContain("asideContext: timeline");
|
||||
expect(approvalAdapter.match(/^\s+timeline,$/gm)).toHaveLength(1);
|
||||
});
|
||||
|
||||
test("申请人进度与审批历史保持有意分工", () => {
|
||||
const desktopDetail = source("src/pages/ReimbursementDetailPage.tsx");
|
||||
const mobileDetail = source("src/pages/MobileReimbursementDetailPage.tsx");
|
||||
const approvalAdapter = source("src/pages/reimbursement-workspace/ReimbursementApprovalAdapter.tsx");
|
||||
|
||||
expect(desktopDetail).toContain("<ProcessingProgress");
|
||||
expect(mobileDetail).toContain("<ProcessingProgress");
|
||||
expect(desktopDetail.match(/targetLinkLabel="查看所在付款批次"/g)).toHaveLength(1);
|
||||
expect(mobileDetail.match(/targetLinkLabel="查看所在付款批次"/g)).toHaveLength(1);
|
||||
expect(approvalAdapter).toContain('data-testid="reimbursement-workflow-history"');
|
||||
expect(approvalAdapter).not.toContain("<ProcessingProgress");
|
||||
});
|
||||
|
||||
test("票据申报事实与系统金额证据状态分属正文和处理上下文", () => {
|
||||
const baseDetail = {
|
||||
items: [{
|
||||
id: "item-1",
|
||||
expenseTypeId: "type-1",
|
||||
expenseDate: "2026-09-01",
|
||||
amount: "100.00",
|
||||
description: "合成明细",
|
||||
assetPriorApprovalBasis: null,
|
||||
expenseType: { id: "type-1", code: "office_supplies", name: "办公用品", category: "office", enabled: true, requiresInvoice: true },
|
||||
}],
|
||||
attachments: [],
|
||||
};
|
||||
const buildDetail = (amountEvidenceStatus: "verified" | "manual_review") => ({
|
||||
...baseDetail,
|
||||
invoices: [{
|
||||
id: "invoice-1",
|
||||
status: "bound",
|
||||
invoiceCode: "CODE-1",
|
||||
invoiceNumber: "NUMBER-1",
|
||||
invoiceDate: "2026-09-01",
|
||||
sellerName: "合成供应商",
|
||||
buyerName: null,
|
||||
documentCategory: "invoice",
|
||||
documentSubtype: "vat",
|
||||
totalAmount: "100.00",
|
||||
originalName: "invoice.pdf",
|
||||
mimeType: "application/pdf",
|
||||
fileSize: 100,
|
||||
linkedItemId: "item-1",
|
||||
amountEvidenceStatus,
|
||||
}],
|
||||
});
|
||||
const snapshot = { standardViolations: [], budgetCheck: null, budgetCheckStatus: "not_applicable" as const, parseStatus: "ready" as const };
|
||||
const verified = buildReimbursementApprovalEvidence(buildDetail("verified"), snapshot).items[0].invoices[0];
|
||||
const manualReview = buildReimbursementApprovalEvidence(buildDetail("manual_review"), snapshot).items[0].invoices[0];
|
||||
const declaredFact = ({ amountEvidenceStatus: _context, ...fact }: typeof verified) => fact;
|
||||
|
||||
expect(declaredFact(verified)).toEqual(declaredFact(manualReview));
|
||||
expect(verified.amountEvidenceStatus).toBe("verified");
|
||||
expect(manualReview.amountEvidenceStatus).toBe("manual_review");
|
||||
});
|
||||
|
||||
test("打印页保持组合归档视图且归档元数据不进入交互工作台", () => {
|
||||
const printPage = source("src/pages/ReimbursementPrintPage.tsx");
|
||||
const desktopShell = source("src/components/reimbursements/ReimbursementDesktopWorkspaceShell.tsx");
|
||||
const mobileShell = source("src/components/reimbursements/ReimbursementMobileWorkspaceShell.tsx");
|
||||
|
||||
expect(printPage).toContain("const totalAmount = detail.totalAmount");
|
||||
expect(printPage).toContain("<QRCodeSVG");
|
||||
expect(printPage).toContain("打印时间:");
|
||||
expect(printPage).toContain("打印人:");
|
||||
expect(printPage).toContain("审批记录");
|
||||
for (const shell of [desktopShell, mobileShell]) {
|
||||
expect(shell).not.toContain("QRCodeSVG");
|
||||
expect(shell).not.toContain("打印时间:");
|
||||
expect(shell).not.toContain("打印人:");
|
||||
}
|
||||
});
|
||||
在新工单中引用
屏蔽一个用户