完善借款、编号、付款与预算并发及验收(09-06-release-gate-integrity)
汇总已落盘的借款本金与还款、兼容历史编号的周期化编号、付款责任与回单核验、报销进度和排序、PWA/采购事务、错误原因与日期校验及ERP退役。 统一共享预算池锁序和锁后判断,恢复差旅原预算选择,补原因码固定夹具及反向完整性检查。 纳入配套迁移、测试、受管QA、分层验收合同、规约及任务记录;原始证据留在ignored artifacts。 关联任务:09-17-company-loan-principal-repayment、09-17-erp-exclusive-retirement、09-18-reimbursement-periodic-numbering、09-08-reimbursement-payment-closeout、09-18-local-concurrency-product-fixes、09-19-budget-pool-concurrency、09-05-error-explainability-payment-readiness。 预算补审由Claude定向验证2文件928项通过,原4446/28及r6结果仍仅对应各自历史输入;本轮未重跑测试。 完整新候选门禁、R1真实入口接入与12项公网义务仍开放。生产编号未启用、差旅仍冻结;不推送、不部署,不宣称发布资格。
这个提交包含在:
@@ -1,9 +1,12 @@
|
||||
import { mkdirSync } from "node:fs";
|
||||
import { mkdirSync, mkdtempSync } from "node:fs";
|
||||
import path from "node:path";
|
||||
import { expect, test, type Browser, type Page, type Route } from "@playwright/test";
|
||||
|
||||
const ROOT_DIR = path.resolve(process.cwd(), "..");
|
||||
const SCREENSHOT_DIR = path.join(ROOT_DIR, "output/playwright/r125-invoice-reupload-after-delete");
|
||||
const SCREENSHOT_ROOT = process.env.QA_VISUAL_OUTPUT_DIR || path.join(ROOT_DIR, "output/playwright/r125-invoice-reupload-after-delete");
|
||||
mkdirSync(SCREENSHOT_ROOT, { recursive: true });
|
||||
const SCREENSHOT_DIR = mkdtempSync(path.join(SCREENSHOT_ROOT, "r125-"));
|
||||
test.use({ serviceWorkers: "block" });
|
||||
const WECOM_UA =
|
||||
"Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148 wxwork/4.1.20 MicroMessenger/8.0.1 Language/zh_CN";
|
||||
|
||||
@@ -107,6 +110,8 @@ function buildState() {
|
||||
invoices: [] as ReturnType<typeof buildBoundInvoice>[],
|
||||
availableInvoices: [buildAvailableInvoice()],
|
||||
uploadCount: 0,
|
||||
archived: false,
|
||||
bindRequests: [] as Array<{ id: string; reimbursementId?: string; linkedItemId?: string }>,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -297,7 +302,8 @@ async function mockApis(page: Page, state: ReturnType<typeof buildState>) {
|
||||
|
||||
if (pathName === "/api/invoice-pool/upload") {
|
||||
state.uploadCount += 1;
|
||||
const invoicePoolId = state.uploadCount === 1 ? reusableInvoiceId : newInvoiceId;
|
||||
const invoicePoolId = state.archived ? newInvoiceId : reusableInvoiceId;
|
||||
state.availableInvoices = [...state.availableInvoices.filter((item) => item.id !== invoicePoolId), { ...buildAvailableInvoice(), id: invoicePoolId }];
|
||||
await fulfillJson(route, {
|
||||
invoicePoolId,
|
||||
status: "available",
|
||||
@@ -311,7 +317,10 @@ async function mockApis(page: Page, state: ReturnType<typeof buildState>) {
|
||||
|
||||
const bindMatch = pathName.match(/^\/api\/invoice-pool\/([^/]+)\/bind$/);
|
||||
if (bindMatch) {
|
||||
const payload = request.postDataJSON() as { reimbursementId?: string; linkedItemId?: string };
|
||||
state.bindRequests.push({ id: bindMatch[1], ...payload });
|
||||
state.invoices = [buildBoundInvoice(bindMatch[1])];
|
||||
state.availableInvoices = state.availableInvoices.filter((item) => item.id !== bindMatch[1]);
|
||||
await fulfillJson(route, {
|
||||
id: bindMatch[1],
|
||||
status: "bound",
|
||||
@@ -339,6 +348,7 @@ async function mockApis(page: Page, state: ReturnType<typeof buildState>) {
|
||||
|
||||
if (pathName === `/api/invoice-pool/${archivedInvoiceId}/archive`) {
|
||||
state.availableInvoices = [];
|
||||
state.archived = true;
|
||||
await fulfillJson(route, {
|
||||
id: archivedInvoiceId,
|
||||
status: "archived",
|
||||
@@ -385,29 +395,36 @@ test.describe("R125 发票删除后重传", () => {
|
||||
await page.setViewportSize({ width: 1440, height: 960 });
|
||||
await page.goto(`/reimbursements/${reimbursementId}`);
|
||||
await page.getByTestId("reimbursement-invoice-add").click();
|
||||
await page.getByLabel("新发票添加位置").selectOption("item-r125");
|
||||
await page.getByTestId("reimbursement-invoice-upload-file").setInputFiles({
|
||||
name: "北京悦海堂发票.pdf",
|
||||
mimeType: "application/pdf",
|
||||
buffer: Buffer.from("%PDF r125 invoice"),
|
||||
});
|
||||
await page.getByTestId("reimbursement-invoice-upload-submit").click();
|
||||
await expect(page.getByText("发票上传完成:1 张已添加")).toBeVisible({ timeout: 10_000 });
|
||||
await page.keyboard.press("Escape");
|
||||
await expect(page.getByText("北京悦海堂餐饮管理有限公司")).toBeVisible();
|
||||
await expect(page.getByText("发票上传完成:1 张已入我的发票,请选择后添加")).toBeVisible({ timeout: 10_000 });
|
||||
expect(state.bindRequests).toHaveLength(0);
|
||||
await page.getByRole("button", { name: "添加到报销单", exact: true }).click();
|
||||
await expect(page.locator(`#reimbursement-invoice-${reusableInvoiceId}`)).toBeVisible();
|
||||
expect(state.bindRequests).toEqual([{ id: reusableInvoiceId, reimbursementId, linkedItemId: "item-r125" }]);
|
||||
|
||||
await page.getByRole("button", { name: "移除" }).first().click();
|
||||
await page.locator(`#reimbursement-invoice-${reusableInvoiceId}`).getByRole("button", { name: "移除对应关系", exact: true }).click();
|
||||
await expect(page.getByText("北京悦海堂餐饮管理有限公司")).toHaveCount(0);
|
||||
|
||||
await page.getByTestId("reimbursement-invoice-add").click();
|
||||
await page.getByLabel("新发票添加位置").selectOption("item-r125");
|
||||
await page.getByTestId("reimbursement-invoice-upload-file").setInputFiles({
|
||||
name: "北京悦海堂发票.pdf",
|
||||
mimeType: "application/pdf",
|
||||
buffer: Buffer.from("%PDF r125 invoice"),
|
||||
});
|
||||
await page.getByTestId("reimbursement-invoice-upload-submit").click();
|
||||
await expect(page.getByText("发票上传完成:1 张已添加")).toBeVisible({ timeout: 10_000 });
|
||||
await page.keyboard.press("Escape");
|
||||
await expect(page.getByText("北京悦海堂餐饮管理有限公司")).toBeVisible();
|
||||
await expect(page.getByText("发票上传完成:1 张已入我的发票,请选择后添加")).toBeVisible({ timeout: 10_000 });
|
||||
expect(state.bindRequests).toHaveLength(1);
|
||||
await page.getByRole("button", { name: "添加到报销单", exact: true }).click();
|
||||
await expect(page.locator(`#reimbursement-invoice-${reusableInvoiceId}`)).toBeVisible();
|
||||
expect(state.bindRequests).toHaveLength(2);
|
||||
expect(state.bindRequests[1]).toEqual({ id: reusableInvoiceId, reimbursementId, linkedItemId: "item-r125" });
|
||||
await expectNoRawReimbursementCodes(page);
|
||||
await page.screenshot({ path: screenshotPath("pc-reupload-after-unbind.png"), fullPage: true });
|
||||
});
|
||||
@@ -431,16 +448,20 @@ test.describe("R125 发票删除后重传", () => {
|
||||
await expect(availableInvoiceCard).toHaveCount(0);
|
||||
|
||||
await invoiceDialog.getByRole("button", { name: "上传" }).click();
|
||||
await mobile.page.getByLabel("新发票添加位置").selectOption("item-r125");
|
||||
await mobile.page.getByTestId("reimbursement-invoice-upload-file").setInputFiles({
|
||||
name: "北京悦海堂发票.pdf",
|
||||
mimeType: "application/pdf",
|
||||
buffer: Buffer.from("%PDF r125 invoice"),
|
||||
});
|
||||
await mobile.page.getByTestId("reimbursement-invoice-upload-submit").click();
|
||||
await expect(
|
||||
mobile.page.getByTestId("reimbursement-invoice-dialog").getByText(/1 张已添加|已添加 1 张发票/),
|
||||
).toBeVisible({ timeout: 10_000 });
|
||||
await mobile.page.keyboard.press("Escape");
|
||||
await expect(invoiceDialog.getByRole("button", { name: "添加到报销单 1", exact: true })).toBeEnabled();
|
||||
expect(state.bindRequests).toHaveLength(0);
|
||||
await invoiceDialog.getByRole("button", { name: "添加到报销单 1", exact: true }).click();
|
||||
await expect(invoiceDialog.getByText("已添加 1 张发票", { exact: true })).toBeVisible();
|
||||
expect(state.archived).toBe(true);
|
||||
expect(state.bindRequests).toEqual([{ id: newInvoiceId, reimbursementId, linkedItemId: "item-r125" }]);
|
||||
await invoiceDialog.getByRole("button", { name: "返回报销单", exact: true }).click();
|
||||
await expect(mobile.page.getByText("北京悦海堂餐饮管理有限公司")).toBeVisible();
|
||||
await mobile.page.screenshot({ path: screenshotPath("mobile-reupload-after-archive.png"), fullPage: true });
|
||||
} finally {
|
||||
|
||||
在新工单中引用
屏蔽一个用户