完善借款、编号、付款与预算并发及验收(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项公网义务仍开放。生产编号未启用、差旅仍冻结;不推送、不部署,不宣称发布资格。
这个提交包含在:
2026-09-19 13:11:04 +08:00
父节点 729d90576f
当前提交 1f8d5b658a
共修改 367 个文件,包含 21079 行新增和 12287 行删除
@@ -1,4 +1,4 @@
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";
import {
@@ -7,7 +7,9 @@ import {
} from "../../src/lib/notificationLinks";
const ROOT_DIR = path.resolve(process.cwd(), "..");
const SCREENSHOT_DIR = path.join(ROOT_DIR, "output/playwright/mobile-ia-flow-remediation");
const visualRoot = path.resolve(process.env.QA_VISUAL_OUTPUT_DIR?.trim() || path.join(process.cwd(), "../output/playwright"));
mkdirSync(visualRoot, { recursive: true });
const SCREENSHOT_DIR = mkdtempSync(path.join(visualRoot, "dg7-mobile-"));
const IOS_WECOM_UA =
"Mozilla/5.0 (iPhone; CPU iPhone OS 16_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148 wxwork/4.1.20 MicroMessenger/8.0.1 Language/zh_CN";
@@ -753,9 +755,8 @@ async function mockApis(
async function loginForMobile(page: Page, role: RolePreset) {
await page.goto("/login");
const accountLoginButton = page.getByRole("button", { name: "使用账号密码登录" });
if (await accountLoginButton.isVisible({ timeout: 1_000 }).catch(() => false)) {
await accountLoginButton.click();
}
// This fixture always enables the WeCom-first account entry; wait for config.
await accountLoginButton.click({ timeout: 30_000 });
await expect(page.getByTestId("login-identifier")).toBeVisible();
await page.getByTestId("login-identifier").fill(`${role}@hyinda.com`);
await page.getByTestId("login-password").fill("password");
@@ -890,55 +891,57 @@ test.describe("MIF7 移动信息架构整改", () => {
test.skip(process.env.E2E_MODE === "live", "MIF7 本地专项使用 mock 数据验证信息架构。");
test("首页常用入口不重复底部 Tab,员工进度页按状态分标签", async ({ browser }) => {
for (const width of [320, 360, 375, 390, 393, 414, 430]) {
const notifications = buildNotifications();
const mobile = await newMobilePage(browser, width);
try {
await mockApis(mobile.page, "employee", notifications);
await loginForMobile(mobile.page, "employee");
const mobile = await newMobilePage(browser, 320);
try {
await mockApis(mobile.page, "employee", buildNotifications());
await loginForMobile(mobile.page, "employee");
// Widths exercise the same employee/data; login is independent of width.
for (const width of [320, 360, 375, 390, 393, 414, 430]) {
await mobile.page.setViewportSize({ width, height: width < 360 ? 568 : 844 });
await test.step(`员工首页与进度 ${width}px`, async () => {
await mobile.page.goto("/mobile");
await expect(mobile.page.getByTestId("mobile-nav-approvals-label")).toHaveText("进度");
await expect(mobile.page.getByTestId("mobile-action-approvals-label")).toHaveCount(0);
await expect(mobile.page.getByTestId("mobile-pending-summary")).toHaveCount(0);
await expect(mobile.page.getByTestId("mobile-action-grid")).toBeVisible();
await expect(mobile.page.getByTestId("mobile-action-purchase-label")).toHaveText("采购");
await expect(mobile.page.getByTestId("mobile-action-asset-scan")).toHaveCount(0);
await expectCommonActionsDoNotDuplicateBottomTabs(mobile.page);
await expectActionTileGapStable(mobile.page);
await expectNoMobileShellRegression(mobile.page);
await mobile.page.screenshot({ path: screenshotPath(`home-employee-w${width}`), fullPage: true });
await mobile.page.goto("/mobile");
await expect(mobile.page.getByTestId("mobile-nav-approvals-label")).toHaveText("进度");
await expect(mobile.page.getByTestId("mobile-action-approvals-label")).toHaveCount(0);
await expect(mobile.page.getByTestId("mobile-pending-summary")).toHaveCount(0);
await expect(mobile.page.getByTestId("mobile-action-grid")).toBeVisible();
await expect(mobile.page.getByTestId("mobile-action-purchase-label")).toHaveText("采购");
await expect(mobile.page.getByTestId("mobile-action-asset-scan")).toHaveCount(0);
await expectCommonActionsDoNotDuplicateBottomTabs(mobile.page);
await expectActionTileGapStable(mobile.page);
await expectNoMobileShellRegression(mobile.page);
await mobile.page.screenshot({ path: screenshotPath(`home-employee-w${width}`), fullPage: true });
await mobile.page.getByTestId("mobile-nav-approvals").click();
await expect(mobile.page).toHaveURL(/\/mobile\/approvals$/);
await expect(mobile.page.getByTestId("mobile-approvals-progress")).toBeVisible();
await expect(mobile.page.getByTestId("mobile-progress-tab-draft")).toBeVisible();
await expect(mobile.page.getByTestId("mobile-progress-tab-reviewing")).toBeVisible();
await expect(mobile.page.getByTestId("mobile-progress-tab-approved")).toBeVisible();
await expect(mobile.page.getByTestId("mobile-progress-tab-returned")).toBeVisible();
await expect(mobile.page.getByTestId("mobile-approvals-list")).toHaveCount(0);
await expect(mobile.page.getByTestId("mobile-approvals-approve")).toHaveCount(0);
await expect(mobile.page.getByTestId("mobile-approval-progress-card")).toHaveCount(3);
await expectMobileApprovalStatusSlots(mobile.page);
await expectNoMobileShellRegression(mobile.page);
await mobile.page.screenshot({ path: screenshotPath(`employee-progress-w${width}`), fullPage: true });
await mobile.page.getByTestId("mobile-nav-approvals").click();
await expect(mobile.page).toHaveURL(/\/mobile\/approvals$/);
await expect(mobile.page.getByTestId("mobile-approvals-progress")).toBeVisible();
await expect(mobile.page.getByTestId("mobile-progress-tab-draft")).toBeVisible();
await expect(mobile.page.getByTestId("mobile-progress-tab-reviewing")).toBeVisible();
await expect(mobile.page.getByTestId("mobile-progress-tab-approved")).toBeVisible();
await expect(mobile.page.getByTestId("mobile-progress-tab-returned")).toBeVisible();
await expect(mobile.page.getByTestId("mobile-approvals-list")).toHaveCount(0);
await expect(mobile.page.getByTestId("mobile-approvals-approve")).toHaveCount(0);
await expect(mobile.page.getByTestId("mobile-approval-progress-card")).toHaveCount(3);
await expectMobileApprovalStatusSlots(mobile.page);
await expectNoMobileShellRegression(mobile.page);
await mobile.page.screenshot({ path: screenshotPath(`employee-progress-w${width}`), fullPage: true });
await mobile.page.goto("/mobile/leave?view=records&status=approved");
await expect(mobile.page.getByTestId("mobile-leave-view-records")).toHaveAttribute("data-state", "active");
await expect(mobile.page.getByTestId("mobile-leave-record-card")).toBeVisible();
await expect(mobile.page.getByTestId("mobile-leave-balances")).toBeVisible();
await expect(mobile.page.getByTestId("mobile-leave-balances")).toContainText("企业微信主办");
await expect(mobile.page.getByTestId("mobile-leave-balance-summary")).toContainText("4.0");
await expect(mobile.page.getByTestId("mobile-leave-form")).toHaveCount(0);
await mobile.page.goto("/mobile/leave?view=records&status=approved");
await expect(mobile.page.getByTestId("mobile-leave-view-records")).toHaveAttribute("data-state", "active");
await expect(mobile.page.getByTestId("mobile-leave-record-card")).toBeVisible();
await expect(mobile.page.getByTestId("mobile-leave-balances")).toBeVisible();
await expect(mobile.page.getByTestId("mobile-leave-balances")).toContainText("企业微信主办");
await expect(mobile.page.getByTestId("mobile-leave-balance-summary")).toContainText("4.0");
await expect(mobile.page.getByTestId("mobile-leave-form")).toHaveCount(0);
await mobile.page.goto("/mobile/purchase-requests?status=approved");
await expect(mobile.page.getByTestId("mobile-purchase-requests-list")).toBeVisible();
await expect(mobile.page.getByLabel("当前列表搜索")).toBeVisible();
await expect(mobile.page.getByTestId("mobile-purchase-request-card")).toBeVisible();
await expect(mobile.page.getByTestId("mobile-purchase-tab-approved")).toHaveAttribute("data-state", "active");
} finally {
await mobile.context.close();
await mobile.page.goto("/mobile/purchase-requests?status=approved");
await expect(mobile.page.getByTestId("mobile-purchase-requests-list")).toBeVisible();
await expect(mobile.page.getByLabel("当前列表搜索")).toBeVisible();
await expect(mobile.page.getByTestId("mobile-purchase-request-card")).toBeVisible();
await expect(mobile.page.getByTestId("mobile-purchase-tab-approved")).toHaveAttribute("data-state", "active");
});
}
} finally {
await mobile.context.close();
}
});
@@ -1184,3 +1187,53 @@ test.describe("MIF7 移动信息架构整改", () => {
}
});
});
test("编号兼容:PC 与手机报销列表、搜索和详情保留新旧单号", async ({ browser }) => {
test.skip(process.env.E2E_MODE === "live", "仅本地合成编号,不访问线上");
const forms = [
{ id: "reimbursement-numbering-old", formNumber: "BX-20260917-0198" },
{ id: "reimbursement-numbering-new", formNumber: "BX-202609-0002" },
];
for (const mobile of [false, true]) {
const context = await browser.newContext({
viewport: mobile ? { width: 390, height: 844 } : { width: 1440, height: 1000 },
isMobile: mobile, hasTouch: mobile, serviceWorkers: "block",
...(mobile ? { userAgent: IOS_WECOM_UA } : {}),
});
const page = await context.newPage();
try {
await mockApis(page, "employee", buildNotifications());
await page.route("**/api/auth/wecom/config", route => fulfillJson(route, { enabled: false, entryEnabled: false, oauthEnabled: false }));
await page.route(url => url.pathname === "/api/reimbursements", route => fulfillJson(route, {
page: 1, pageSize: 20, total: forms.length, scope: "own",
items: forms.map(form => ({ ...reimbursementListItem(), ...form })),
}));
for (const form of forms) {
await page.route(`**/api/reimbursements/${form.id}`, route => fulfillJson(route, { ...reimbursementDetail(), ...form }));
}
await page.goto("/login");
await page.getByTestId("login-identifier").fill("[email protected]");
await page.getByTestId("login-password").fill("password");
await page.getByTestId("login-submit").click();
await expect(page).toHaveURL(/\/(portal|mobile)$/);
const listPath = mobile ? "/mobile/reimbursements" : "/reimbursements";
for (const form of forms) {
await page.goto(`${listPath}?status=reviewing`);
const rows = page.getByTestId(mobile ? "mobile-reimbursement-card" : "reimbursement-row");
await expect(rows).toHaveCount(2);
for (const source of forms) await expect(rows.filter({ hasText: source.formNumber })).toBeVisible();
if (mobile) {
await page.getByLabel("当前列表搜索").fill(form.formNumber);
await expect(rows).toHaveCount(1);
}
await rows.filter({ hasText: form.formNumber }).getByRole("link").click();
await expect(page).toHaveURL(new RegExp(`${listPath}/${form.id}(?:\\?|$)`));
await expect(page.getByTestId(mobile ? "mobile-reimbursement-detail-header" : "reimbursement-desktop-workspace")).toBeVisible();
const numberLabel = mobile
? page.getByTestId("mobile-reimbursement-detail-header").getByText(form.formNumber, { exact: true })
: page.getByRole("heading", { name: `报销单 ${form.formNumber}`, exact: true });
await expect(numberLabel).toBeVisible();
}
} finally { await context.close(); }
}
});