R78 A3-A5:跑通本地支付尾链回归并回填报告
这个提交包含在:
@@ -90,4 +90,30 @@ R70 初次试运行暂停项已在 2026-04-28 的 R71 定向修复后关闭。
|
||||
- 最新 batch 票据结果:`fb29cdd8-0c10-4254-8feb-d3c3c1be86c3`,销售方 `北京悦海堂餐饮管理有限公司`,总金额 `1683.00`,状态 `available`。
|
||||
- 试运行结论:
|
||||
- 员工侧报销 / 票据中心 / OCR 主链路 `上传 -> OCR -> available -> 绑定 -> 提交报销` 已具备本地试运行条件。
|
||||
- 本轮仍未覆盖真实企微 OAuth、HTTPS cookie secure、多角色并发协同,以及财务支付完成后的下游链路;这些项继续保留在外部或后续 focused regression。
|
||||
- 本轮仍未覆盖真实企微 OAuth、HTTPS cookie secure、多角色并发协同、真实 ERP 外发回执,以及高风险二次复核;这些项继续保留在外部或后续 focused regression。
|
||||
|
||||
## R78 财务支付尾链 focused regression
|
||||
|
||||
- 回归时间:`2026-04-29 10:37 - 10:52 CST`
|
||||
- 自动化环境:`http://127.0.0.1:3002`
|
||||
- 自动化数据库:`hyb_oa_manual_trialrun_20260429093000`
|
||||
- 本地 focused 开关:
|
||||
- 运行时 env:`PAYMENT_BATCH_ENABLED=true`、`ERP_SYNC_ENABLED=true`
|
||||
- DB feature flag:通过 `TRIALRUN_EXTRA_FORCE_ON_FLAGS='payment_batch_v1 erp_sync'` 重复执行 `seed:trialrun` 显式打开,仅用于本地 focused 财务回归,不改变默认 trialrun 上线建议。
|
||||
- 回归命令:
|
||||
- `cd server && ./scripts/regression-payment-batches.sh`
|
||||
- `cd client && E2E_BASE_URL=http://127.0.0.1:3002 npx playwright test tests/e2e/reimbursement-payment-batch-flow.spec.ts --workers=1 --reporter=list`
|
||||
- 结果:
|
||||
- server focused regression:`3 passed`
|
||||
- Playwright focused E2E:`1 passed (58.0s)`
|
||||
- 已验证链路:
|
||||
- employee:创建空白报销 -> 上传 mock 票据 -> 绑定 -> 提交
|
||||
- manager / finance:完成两级审批
|
||||
- finance:创建 reimbursement payment batch,并在复核后执行
|
||||
- finance_mgr:在 AP 工作台完成快捷复核
|
||||
- execute 后:`payment batch = executed`、`reimbursement = paid`、`invoice = archived`
|
||||
- ERP preview:可见最新 `pending` outbox
|
||||
- reviewer 结论:
|
||||
- `paymentBatches` 复核路由仍拦截 maker 自审;低风险批次维持 `finance_mgr` 复核、`finance` 执行的边界。
|
||||
- 失败路径单测确认不会错误归档已绑定发票,也不会误入 ERP / 通知 follow-up。
|
||||
- `PaymentBatchesPage` 与 `ApOperationsPage` 没有新增越权入口,按钮仍按 `payment.batch.manage` 与 `risk.review.manage` 分离。
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"Domain","Tier","Routes","Primary_Roles","Env_Gates","Feature_Flags","Coverage_Status","Evidence","Validated_Flows","Gaps","Next_Action"
|
||||
"认证与 Portal 导航","must_cover","/login|/portal|主/管/备导航","employee|manager|admin","APP_ORIGIN|COOKIE_SECURE|PORTAL_HOME_ENABLED|WECOM_ENTRY_ENABLED","portal_home_v1|wecom_entry_v1","done","artifacts/r69/acceptance-report.md|artifacts/r70/manual-trialrun-report.md|client/tests/e2e/login-portal-ux.spec.ts|client/tests/e2e/wecom-entry.spec.ts|client/tests/e2e/manual/r70-trialrun-assist.spec.ts","登录、Portal 首屏、员工/经理/管理员导航分组、普通浏览器与企微 WebView CTA 提示均有自动化或人工证据。","真实企微 OAuth 绑定与回跳未做端到端;HTTPS 部署态 cookie 行为未在本轮验证。","保持主链路 done;真实企微与 HTTPS 转 A5 外部专项。"
|
||||
"请假与审批","must_cover","/leave-requests|/workflow/tasks","employee|manager|admin","none","leave_holiday_calendar","done","artifacts/r69/acceptance-report.md|artifacts/r70/manual-trialrun-report.md|client/tests/e2e/leave-flow.spec.ts|client/tests/e2e/workflow-quick-approve.spec.ts|client/tests/e2e/leave-team-calendar.spec.ts","请假草稿、提交、撤回、经理快捷审批、团队日历权限边界与经理部门视角已自动化并在 R70 人工复测通过。","多审批人链路、代理审批、真实并发多人协同未在 trialrun 轮次验证。","维持 done;多人协同与代理审批并入 A5 或后续专项。"
|
||||
"报销、票据中心与 OCR","must_cover","/reimbursements|/reimbursements/:id|/invoice-pool","employee|manager|finance|admin","OCR_PROVIDER|TENCENT_SECRET_ID|TENCENT_SECRET_KEY","invoice_verification","partial_done","artifacts/r70/manual-trialrun-report.md|artifacts/r70/live-ocr-batch-report.md|issues/2026-04-29_08-39-22-r76-external-trialrun-closeout.csv|issues/2026-04-29_09-19-07-r77-ocr-pdf-first.csv|client/tests/e2e/reimbursement-workflow-progress.spec.ts|client/tests/e2e/invoice-ocr-flow.spec.ts|client/tests/e2e/manual/live-invoice-ocr.spec.ts|client/tests/e2e/manual/live-invoice-ocr-batch.spec.ts","报销提交后审批进度、票据中心缺字段修正、上传后自动 OCR 进入可绑定、绑定后提交已自动化;R70 人工链路也通过。R77 已在干净本地 trialrun DB 上用真实 PDF 发票跑通 `上传 -> OCR -> available`,并完成 live 单张 + batch Playwright 复测,runner 正常退出。","旧项目 `.env` 中一组腾讯凭证已在当前应用链路内验证可用;`.env.local` 仍是无效凭证。当前剩余缺口已不再是 OCR 鉴权、PDF 参数或 batch runner 收尾,而是财务支付完成后的下游链路尚未纳入本轮小团队 trialrun。","员工侧报销 / 票据 / OCR 主链路已具备本地试运行条件;如后续将财务支付后链路纳入试运行,再补一轮 focused regression。PDF 转图片 fallback 仅在新增真实票样兼容性问题出现时再评估。"
|
||||
"报销、票据中心与 OCR","must_cover","/reimbursements|/reimbursements/:id|/invoice-pool","employee|manager|finance|admin","OCR_PROVIDER|TENCENT_SECRET_ID|TENCENT_SECRET_KEY|PAYMENT_BATCH_ENABLED|ERP_SYNC_ENABLED","invoice_verification|payment_batch_v1|erp_sync","done","artifacts/r70/manual-trialrun-report.md|artifacts/r70/live-ocr-batch-report.md|issues/2026-04-29_08-39-22-r76-external-trialrun-closeout.csv|issues/2026-04-29_09-19-07-r77-ocr-pdf-first.csv|issues/2026-04-29_10-14-27-r78-reimbursement-payment-tail.csv|client/tests/e2e/reimbursement-workflow-progress.spec.ts|client/tests/e2e/invoice-ocr-flow.spec.ts|client/tests/e2e/reimbursement-payment-batch-flow.spec.ts|client/tests/e2e/manual/live-invoice-ocr.spec.ts|client/tests/e2e/manual/live-invoice-ocr-batch.spec.ts|server/scripts/regression-payment-batches.sh","报销提交后审批进度、票据中心缺字段修正、上传后自动 OCR 进入可绑定、绑定后提交已自动化;R70 人工链路也通过。R77 已在干净本地 trialrun DB 上用真实 PDF 发票跑通 `上传 -> OCR -> available`。R78 已补 employee->manager->finance->finance_mgr->payment batch->execute->reimbursement paid->invoice archived->ERP preview pending outbox focused regression。","真实 ERP 投递回执、高风险两轮复核、大额或混合付款批次仍未进入本轮本地试运行;腾讯 OCR 凭证仍依赖旧项目运行时注入。","维持 done;真实 ERP 与高风险付款转外部/专项回归,PDF 转图片 fallback 仅在新增票样兼容性问题出现时再评估。"
|
||||
"周报与待办","must_cover","/weekly-reports|/todos","employee|manager","WEEKLY_REPORT_ENABLED|TODO_BOARD_ENABLED","weekly_report_v1|todo_board_v1","done","artifacts/r70/auto-trialrun-deep-report.md|artifacts/r70/manual-trialrun-report.md|client/tests/e2e/weekly-todo-flow.spec.ts|client/tests/e2e/manual/r70-trialrun-deep-assist.spec.ts","周报保存、生成待办、提交周报、完成待办闭环已自动化且在真实 trialrun 数据上复核通过。","经理侧未提交周报聚合与催办策略未单独验证。","维持 done;如进入管理场景增强,再补经理视角聚合。"
|
||||
"公告、搜索与通知","must_cover","/announcements|/notifications|全局搜索","employee|manager|admin","ANNOUNCEMENTS_ENABLED|GLOBAL_SEARCH_ENABLED","announcements_v1|global_search_v1","done","artifacts/r70/auto-trialrun-deep-report.md|artifacts/r70/manual-trialrun-report.md|client/tests/e2e/announcements-presence.spec.ts|client/tests/e2e/global-search.spec.ts|client/tests/e2e/manual/r70-trialrun-deep-assist.spec.ts|client/tests/e2e/r75-automation-gap-fill.spec.ts","公告发布、员工可见、Portal 汇总、真实搜索命中公告并跳转,以及通知中心未读筛选、单条已读、全部已读与待办跳转均有自动化或人工证据。","无当前试运行阻断缺口;真实外发渠道投递与通知渠道配置验证另见系统治理和外部环境项。","站内通知维持 done;外发渠道留在 A5/治理专项。"
|
||||
"团队状态与考勤","must_cover","/team|/attendance","employee|manager|admin","PRESENCE_BOARD_ENABLED|ATTENDANCE_ENABLED","presence_board_v1|attendance_v1","done","artifacts/r70/manual-trialrun-report.md|client/tests/e2e/announcements-presence.spec.ts|issues/2026-04-09_r59-attendance-clockin.csv|client/tests/e2e/r75-automation-gap-fill.spec.ts","团队状态更新、Portal 汇总、经理查看团队状态,以及员工今日上班/下班打卡、月视图记录、管理员团队考勤补卡最小链路均已验证。","排班、节假日豁免、跨日班次等复杂考勤规则未做专项回归,但不构成本轮 trialrun 阻断。","维持 done;复杂考勤规则按需要独立专项。"
|
||||
@@ -10,7 +10,7 @@
|
||||
"采购","enabled_admin","/purchase-requests","employee|manager|finance","none","none","partial_done","artifacts/r69/acceptance-report.md|client/tests/e2e/procurement-flow.spec.ts|issues/2026-03-24_17-00-26-r33-3-transaction-timeout-tail-cleanup.csv","员工提单、经理审批、财务归档链路已自动化通过,且事务超时问题已收口。","未进入 R70 人工试运行,当前 trialrun DB 下没有业务体验反馈。","若采购进入试运行范围,先补一次人工 happy path;否则保留自动化证据即可。"
|
||||
"资产与盘点","enabled_admin","/assets|/inventory-checks|/mobile/assets/scan","employee|asset admin","none","none","partial_done","artifacts/r69/acceptance-report.md|client/tests/e2e/assets-flow.spec.ts|issues/2026-03-24_17-00-26-r33-3-transaction-timeout-tail-cleanup.csv","资产建卡、领用归还、调拨、盘点主链路已自动化通过。","未进入 R70 人工试运行;移动扫描真实设备与摄像头场景未验证。","保持自动化证据;真实移动扫描转 A5 或独立移动专项。"
|
||||
"差旅与移动审批","enabled_admin","/travel-requests|/mobile/approvals|/mobile/capture","employee|manager|finance","WECOM_ENTRY_ENABLED","wecom_entry_v1","partial_done","artifacts/r69/acceptance-report.md|client/tests/e2e/travel-payment-flow.spec.ts|client/tests/e2e/travel-reconciliation-flow.spec.ts|client/tests/e2e/mobile-approvals-offline.spec.ts|artifacts/r70/manual-trialrun-report.md","差旅申请、审批、支付、绑票、对账批次,以及移动审批 deep link 离线重放都有自动化证据;R70 人工仅确认移动入口可理解。","真实 IM deep link、真实移动设备拍照上传、多人并发审批未验证。","保留自动化为主;真实设备和 IM 深链并入 A5。"
|
||||
"预算、支付批次与 AP 运营","optional","/budgets|/payment-batches|/ap-operations","finance|admin","BUDGET_PRECHECK_ENABLED|PAYMENT_BATCH_ENABLED","budget_precheck_v1|payment_batch_v1","partial","issues/2026-03-29_08-07-14-p25-lite-budget-ap.csv|issues/2026-04-06_r52-ap-operations.csv","预算预检、支付批次 maker-checker、AP 工作台四 Tab 与快捷操作有实现、review 与浏览器级 QA 证据。","不属于当前小团队 trialrun 主路径,R69-R74 没有重新执行一轮本地场景回归。","若财务域纳入试运行,再补一轮 focused regression;本轮仅保留历史验证。"
|
||||
"预算、支付批次与 AP 运营","optional","/budgets|/payment-batches|/ap-operations","finance|admin","BUDGET_PRECHECK_ENABLED|PAYMENT_BATCH_ENABLED|ERP_SYNC_ENABLED","budget_precheck_v1|payment_batch_v1|erp_sync","partial_done","issues/2026-03-29_08-07-14-p25-lite-budget-ap.csv|issues/2026-04-06_r52-ap-operations.csv|issues/2026-04-29_10-14-27-r78-reimbursement-payment-tail.csv|client/tests/e2e/reimbursement-payment-batch-flow.spec.ts|server/scripts/regression-payment-batches.sh","支付批次 maker-checker、AP 待付款快捷复核、finance 执行报销付款批次,以及 reimbursement paid 后 ERP preview pending outbox 已在本地 focused regression 跑通;历史 budget/AP 视图与 review 证据仍保留。","预算预检页面级联动、高风险两轮复核、大额批次与非报销实体付款未在本轮 focused local 回归中重跑。","若财务域继续扩大试运行,再补 budget precheck 与高风险二次复核专项;当前 payment batch/AP happy path 已具备本地回归证据。"
|
||||
"ERP 集成与切换控制台","external","/system/integrations/erp|/system/cutover","integration admin|system.settings","ERP_SYNC_ENABLED|CUTOVER_CONSOLE_ENABLED","erp_sync|cutover_console_v1","partial_external","issues/2026-03-08_14-10-53-p19-erp-1-l5-masterdata-journal-bridge.csv|issues/2026-03-21_11-10-00-p29-replacement-readiness-refresh.csv|client/tests/e2e/live-smoke.spec.ts","ERP 管理页、mock/http 回归和只读 live smoke 有历史证据。","当前本地 trialrun 没有真实 ERP 出入站或 cutover 操作验证。","仅在外部环境专项下执行,不进入默认 trialrun gate。"
|
||||
"系统治理、功能开关与审计","enabled_admin","/system/workspace|/system/settings|/system/feature-flags|/system/workflow-definitions|/audit-logs|/system/notification-channels","admin","WORKFLOW_DEFINITION_ADMIN_ENABLED","workflow_definition_admin_v1","partial_done","artifacts/r70/manual-trialrun-report.md|client/tests/e2e/settings-governance.spec.ts|client/tests/e2e/live-smoke.spec.ts|server/src/scripts/smoke-trialrun-runtime.js|client/tests/e2e/r75-automation-gap-fill.spec.ts","工作区设置、系统设置、功能开关、审计日志打开与导航边界有自动化和人工证据;R74 本地门禁已检查 workspace/flag/navigation 基线,R75 已补功能开关备注保存并恢复写路径。","流程定义、通知渠道、工作区其他写操作和审计留痕写入效果仍未在当前 trialrun 做完整专项。","保留 partial_done;后续若治理域纳入试运行,再补通知渠道与流程定义写路径。"
|
||||
"企微、HTTPS、部署态与并发","external","/login(企微入口)|部署运行时|多账号并发","all roles","APP_ORIGIN|COOKIE_SECURE|WECOM_*|PUBLIC_SECURITY_HARDENING_ENABLED","wecom_entry_v1|public_security_hardening_v1","blocked_external","deploy/trialrun/runbook.md|client/tests/e2e/wecom-entry.spec.ts|client/tests/e2e/live-smoke.spec.ts|server/src/scripts/smoke-trialrun-runtime.js|issues/2026-04-29_08-39-22-r76-external-trialrun-closeout.csv","本地/只读 smoke、env contract、导航/入口提示与 local runtime gate 已就绪。","真实企微 OAuth、HTTPS cookie secure、多用户并发协同仍缺环境级成功证据。腾讯 OCR 鉴权已确认不是当前主阻塞点:旧项目 `.env` 中至少一组凭证可用,当前主阻塞已转为应用层未传 PDF 参数导致的 `ImageDecodeFailed`。","企微/HTTPS/并发仍按 A3/A4 外部专项推进;OCR 从“外部凭证阻塞”切换为 R77 内部修复项,修完后再回到 live trialrun。"
|
||||
|
||||
|
@@ -0,0 +1,190 @@
|
||||
import { test, expect } from "./helpers/fixtures";
|
||||
import {
|
||||
apiGetData,
|
||||
createAuthenticatedApiClient,
|
||||
EMPLOYEE,
|
||||
FINANCE,
|
||||
FINANCE_MGR,
|
||||
login,
|
||||
logout,
|
||||
MANAGER,
|
||||
} from "./helpers/auth";
|
||||
import { openNavLink } from "./helpers/navigation";
|
||||
import { TEST_IDS } from "./helpers/testIds";
|
||||
|
||||
function extractId(text: string, label: string) {
|
||||
const match = text.match(new RegExp(`${label}:([0-9a-f-]{36})`));
|
||||
if (!match?.[1]) {
|
||||
throw new Error(`Failed to parse ${label} from text: ${text}`);
|
||||
}
|
||||
return match[1];
|
||||
}
|
||||
|
||||
test("E2E 报销支付批次主链路:提交 -> 审批 -> 创建批次 -> 复核 -> 执行 -> 下游收口", async ({
|
||||
page,
|
||||
reimbursementDetail,
|
||||
workflowDetail,
|
||||
}) => {
|
||||
test.setTimeout(300_000);
|
||||
|
||||
const employeeApi = await createAuthenticatedApiClient(EMPLOYEE);
|
||||
const financeMgrApi = await createAuthenticatedApiClient(FINANCE_MGR);
|
||||
const paymentDate = new Date().toISOString().slice(0, 10);
|
||||
|
||||
try {
|
||||
await login(page, EMPLOYEE);
|
||||
|
||||
await (await openNavLink(page, "我的报销")).click();
|
||||
await expect(page).toHaveURL(/\/reimbursements/);
|
||||
const createResponsePromise = page.waitForResponse((response) => {
|
||||
const url = new URL(response.url());
|
||||
return response.request().method() === "POST" && url.pathname === "/api/reimbursements" && response.ok();
|
||||
});
|
||||
await page.getByTestId("reimbursements-create").click();
|
||||
await page.getByRole("menuitem", { name: "空白报销" }).click();
|
||||
const createPayload = (await (await createResponsePromise).json()) as { data?: { id?: string } };
|
||||
const reimbursementId = createPayload.data?.id;
|
||||
if (!reimbursementId) {
|
||||
throw new Error(`创建报销单后未返回 id: ${JSON.stringify(createPayload)}`);
|
||||
}
|
||||
|
||||
await page.goto(`/reimbursements/${reimbursementId}`);
|
||||
await reimbursementDetail.assertLoaded();
|
||||
|
||||
await (await openNavLink(page, "票据中心")).click();
|
||||
const marker = `R78_BATCH_${Date.now()}_${Math.random().toString(16).slice(2)}`;
|
||||
await page.getByTestId("invoice-upload-file").setInputFiles({
|
||||
name: `r78-${Date.now()}.png`,
|
||||
mimeType: "image/png",
|
||||
buffer: Buffer.from(marker, "utf8"),
|
||||
});
|
||||
await page.getByTestId("invoice-upload-submit").click();
|
||||
await expect(page.getByTestId("invoice-detail-panel")).toBeVisible();
|
||||
await expect(page.getByTestId("invoice-detail-panel")).toContainText("状态:available", { timeout: 90_000 });
|
||||
|
||||
const invoiceDetailText = await page.getByTestId("invoice-detail-panel").innerText();
|
||||
const invoiceId = extractId(invoiceDetailText, "票据ID");
|
||||
|
||||
await page.getByTestId("invoice-bind-invoice").selectOption(invoiceId);
|
||||
await page.getByTestId("invoice-bind-reimbursement").selectOption(reimbursementId);
|
||||
await page.getByTestId("invoice-bind-submit").click();
|
||||
await expect(page.getByText("绑定成功")).toBeVisible({ timeout: 30_000 });
|
||||
|
||||
await page.goto(`/reimbursements/${reimbursementId}`);
|
||||
await reimbursementDetail.assertLoaded();
|
||||
await reimbursementDetail.submitReimbursement();
|
||||
await expect(page.getByText(/状态:(reviewing|审批中)/)).toBeVisible();
|
||||
|
||||
await logout(page, EMPLOYEE.name);
|
||||
|
||||
await login(page, MANAGER);
|
||||
await (await openNavLink(page, "待我处理")).click();
|
||||
await expect(page.getByTestId(TEST_IDS.workflow.tasksTable)).toBeVisible();
|
||||
const managerTaskRow = page.getByTestId(TEST_IDS.workflow.taskRow).filter({ hasText: reimbursementId }).first();
|
||||
await expect(managerTaskRow).toBeVisible();
|
||||
await managerTaskRow.getByTestId(TEST_IDS.workflow.detailLink).click();
|
||||
await workflowDetail.assertLoaded();
|
||||
await workflowDetail.approve();
|
||||
await expect(workflowDetail.summary).toContainText(/状态:(completed|已完成)/);
|
||||
|
||||
await logout(page, MANAGER.name);
|
||||
|
||||
await login(page, FINANCE);
|
||||
await (await openNavLink(page, "待我处理")).click();
|
||||
await expect(page.getByTestId(TEST_IDS.workflow.tasksTable)).toBeVisible();
|
||||
const financeTaskRow = page.getByTestId(TEST_IDS.workflow.taskRow).filter({ hasText: reimbursementId }).first();
|
||||
await expect(financeTaskRow).toBeVisible();
|
||||
await financeTaskRow.getByTestId(TEST_IDS.workflow.detailLink).click();
|
||||
await workflowDetail.assertLoaded();
|
||||
await workflowDetail.approve();
|
||||
await expect(workflowDetail.summary).toContainText(/状态:(completed|已完成)/);
|
||||
|
||||
const batchName = `R78-报销付款批次-${Date.now()}`;
|
||||
await page.goto("/payment-batches");
|
||||
await expect(page).toHaveURL(/\/payment-batches/);
|
||||
|
||||
const createCard = page.locator(".card-std").filter({ hasText: "新建批次" }).first();
|
||||
const createBatchResponsePromise = page.waitForResponse((response) => {
|
||||
const url = new URL(response.url());
|
||||
return response.request().method() === "POST" && url.pathname === "/api/payment-batches" && response.ok();
|
||||
});
|
||||
await createCard.locator("select").nth(0).selectOption("reimbursement");
|
||||
await createCard.getByPlaceholder("例如:2026Q2-财务付款批次-01").fill(batchName);
|
||||
await createCard.getByPlaceholder("用逗号或换行分隔 reimbursement / travel_order ID").fill(reimbursementId);
|
||||
await createCard.locator('input[type="date"]').fill(paymentDate);
|
||||
await createCard.getByPlaceholder("例如 PBX").fill("R78PB");
|
||||
await createCard.getByRole("button", { name: "创建批次" }).click();
|
||||
|
||||
const createBatchPayload = (await (await createBatchResponsePromise).json()) as {
|
||||
data?: { item?: { id?: string } };
|
||||
};
|
||||
const batchId = createBatchPayload.data?.item?.id;
|
||||
if (!batchId) {
|
||||
throw new Error(`创建支付批次后未返回 id: ${JSON.stringify(createBatchPayload)}`);
|
||||
}
|
||||
await page.goto(`/payment-batches?id=${batchId}`);
|
||||
const batchDetailCard = page.locator(".card-std").filter({ hasText: batchName }).filter({ hasText: "复核进度:" }).last();
|
||||
await expect(batchDetailCard).toBeVisible();
|
||||
await expect(batchDetailCard).toContainText("状态:draft");
|
||||
await expect(batchDetailCard).toContainText("复核进度:0/1");
|
||||
|
||||
await logout(page, FINANCE.name);
|
||||
|
||||
await login(page, FINANCE_MGR);
|
||||
await page.goto("/ap-operations");
|
||||
await expect(page.getByRole("tab", { name: /待付款/ })).toBeVisible();
|
||||
const pendingRow = page.locator("tr").filter({ hasText: batchName }).first();
|
||||
await expect(pendingRow).toBeVisible();
|
||||
await pendingRow.getByRole("button", { name: "快捷复核" }).click();
|
||||
await expect(page.getByRole("dialog")).toContainText(batchName);
|
||||
await page.getByPlaceholder("请输入复核意见").fill("R78 focused regression review");
|
||||
await page.getByRole("button", { name: "提交复核" }).click();
|
||||
await expect(pendingRow).toContainText("复核 1/1");
|
||||
|
||||
await logout(page, FINANCE_MGR.name);
|
||||
|
||||
await login(page, FINANCE);
|
||||
await page.goto(`/payment-batches?id=${batchId}`);
|
||||
const executedBatchDetailCard = page.locator(".card-std").filter({ hasText: batchName }).filter({ hasText: "复核进度:" }).last();
|
||||
await expect(executedBatchDetailCard).toBeVisible();
|
||||
await expect(executedBatchDetailCard).toContainText("复核进度:1/1");
|
||||
await executedBatchDetailCard.getByRole("button", { name: "执行" }).click();
|
||||
await expect(executedBatchDetailCard).toContainText("状态:executed", { timeout: 30_000 });
|
||||
|
||||
await page.goto(`/reimbursements/${reimbursementId}`);
|
||||
await reimbursementDetail.assertLoaded();
|
||||
await expect(page.getByText(/状态:(paid|已支付)/)).toBeVisible({ timeout: 30_000 });
|
||||
|
||||
await page.goto("/ap-operations");
|
||||
await expect(page.getByRole("tab", { name: /待付款/ })).toBeVisible();
|
||||
await expect(page.getByText(batchName)).toHaveCount(0);
|
||||
|
||||
await logout(page, FINANCE.name);
|
||||
|
||||
const invoiceDetail = await apiGetData<{
|
||||
status: string;
|
||||
archivedAt: string | null;
|
||||
linkedReimbursementId: string | null;
|
||||
}>(
|
||||
employeeApi.api,
|
||||
`/api/invoice-pool/${invoiceId}`,
|
||||
);
|
||||
expect(invoiceDetail.status).toBe("archived");
|
||||
expect(invoiceDetail.archivedAt).toBeTruthy();
|
||||
expect(invoiceDetail.linkedReimbursementId).toBe(reimbursementId);
|
||||
|
||||
const erpPreview = await apiGetData<{
|
||||
exportState: {
|
||||
lastOutboxId: string | null;
|
||||
lastOutboxStatus: string | null;
|
||||
};
|
||||
}>(
|
||||
financeMgrApi.api,
|
||||
`/api/integrations/erp/journals/reimbursements/${reimbursementId}/preview`,
|
||||
);
|
||||
expect(erpPreview.exportState.lastOutboxId).toBeTruthy();
|
||||
expect(erpPreview.exportState.lastOutboxStatus).toBe("pending");
|
||||
} finally {
|
||||
await Promise.all([employeeApi.dispose(), financeMgrApi.dispose()]);
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,6 @@
|
||||
"ID","Title","Description","Acceptance","Test_Method","Tools","Dev_Status","Review1_Status","Regression_Status","Files","Dependencies","Notes"
|
||||
"A1","R78 执行合同建档与范围冻结","创建并校验 R78 合同,范围仅覆盖“报销 approved -> 创建支付批次 -> 复核 -> 执行 -> 报销 paid -> AP/支付批次页可见结果”本地 focused regression;不包含真实 ERP 出站、企微、HTTPS、高风险二次复核。","issues/2026-04-29_10-14-27-r78-reimbursement-payment-tail.csv 列完整并通过校验;A2-A5 依赖顺序清晰;Notes 明确本轮无 Prisma 迁移、无外部环境专项。","命令:`python3 /home/shunli/.codex/skills/plan/scripts/validate_issues_csv.py issues/2026-04-29_10-14-27-r78-reimbursement-payment-tail.csv`。检查:12 列完整、Dependencies 为 JSON 数组字符串、Files 为实际路径或哨兵值。","manual","DONE","DONE","DONE","issues/2026-04-29_10-14-27-r78-reimbursement-payment-tail.csv","[]","回滚路径:删除本 CSV;不影响业务运行。2026-04-29:合同已写入并通过 validate_issues_csv.py 校验。"
|
||||
"A2","支付批次报销付款后端回归补齐","围绕 `paymentBatches` 的 reimbursement execute 路径补 focused 后端测试,覆盖 approved 报销进入批次后执行支付、报销变 paid、已绑定发票归档、失败条件与 maker-checker 边界;必要时补最小修复与专用回归脚本。","新增或补强的后端测试能稳定覆盖 reimbursement payment batch happy path;执行后报销状态/付款字段/发票归档行为正确;不引入 schema 变更。","命令:`cd server && npm run prisma:validate && node --check src/routes/paymentBatches.js && node --check src/routes/reimbursements.js && npm test -- tests/unit/routes/paymentBatches.reimbursement.test.js`。业务:approved 报销进入 payment batch,execute 后 reimbursement=paid,关联发票 archived。","manual","DONE","DONE","DONE","server/src/routes/paymentBatches.js|server/src/routes/reimbursements.js|server/tests/unit/routes/paymentBatches.reimbursement.test.js|server/scripts/regression-payment-batches.sh","[""A1""]","涉及付款状态流与票据归档,回滚路径:回退 payment batch reimbursement 执行逻辑改动、删除新增测试与脚本。2026-04-29:已补 payment batch reimbursement route 回归 3 条用例,覆盖 happy path、maker-checker 边界与 invalid pay status 失败路径;并补 `server/scripts/regression-payment-batches.sh`。当前 reimbursement payment batch 成功路径会在支付后补 ERP outbox 与已付款通知。"
|
||||
"A3","报销支付批次 Playwright 主链路补齐","新增 focused E2E:员工创建并提交流程、经理/财务审批、finance 创建 reimbursement payment batch、finance_mgr 复核、finance 执行、验证 PaymentBatchesPage / ApOperationsPage / reimbursement detail 的下游结果一致;若本地 trialrun 默认种子未打开 payment batch / erp sync,则补最小可复现的本地开关引导。","新增 Playwright 用例通过;至少验证 1) 批次从 draft/reviewed 到 executed;2) 报销单状态变 paid;3) 支付后票据进入 archived;4) AP/支付批次页能看到执行结果;5) ERP preview 可见 pending outbox;6) 本地 focused 回归可通过显式开关重复启动,不依赖手工改库。","命令:`cd client && E2E_BASE_URL=http://127.0.0.1:3002 npx playwright test tests/e2e/reimbursement-payment-batch-flow.spec.ts --workers=1 --reporter=list`。业务:employee->manager->finance->finance_mgr 四角色完成低风险报销支付批次闭环。","playwright","DONE","DONE","DONE","client/tests/e2e/reimbursement-payment-batch-flow.spec.ts|client/tests/e2e/helpers/auth.ts|client/tests/e2e/helpers/navigation.ts|client/src/pages/PaymentBatchesPage.tsx|client/src/pages/ApOperationsPage.tsx|server/src/scripts/seed-trialrun.js","[""A2""]","优先复用现有 reimbursement / workflow / invoice helper;若现有 UI 标识不足,仅补最小测试可观测性。本地 focused finance 回归不改变默认试运行上线建议,只允许显式 opt-in 打开 `payment_batch_v1`、`erp_sync`。2026-04-29:已补 `reimbursement-payment-batch-flow.spec.ts`,并通过 `TRIALRUN_EXTRA_FORCE_ON_FLAGS='payment_batch_v1 erp_sync'` 重复 seed 在 `http://127.0.0.1:3002` 跑通 focused E2E;覆盖 draft/reviewed/executed、reimbursement=paid、invoice=archived、ERP preview pending outbox。"
|
||||
"A4","付款状态流与权限 reviewer 复核","对 A2-A3 做独立复核,重点检查 maker-checker、payment.batch.manage 与 risk.review.manage 权限边界、报销 paid 与发票 archived 状态流、以及 AP/批次页是否误放大入口。","review 结论覆盖:1) finance 不能自审自己创建的批次;2) finance_mgr 复核后 finance 可执行;3) 失败路径不会错误归档发票;4) AP/批次页未新增越权操作。","人工 review:对照 server/src/routes/paymentBatches.js、server/src/routes/reimbursements.js 与新增 Playwright/单测断言输出 reviewer 结论;若发现权限/状态回归,A5 前必须先修复。","manual","DONE","DONE","DONE","server/src/routes/paymentBatches.js|server/src/routes/reimbursements.js|client/tests/e2e/reimbursement-payment-batch-flow.spec.ts","[""A2"",""A3""]","本 Issue 只输出 review 结论,不扩展新需求。2026-04-29 reviewer 结论:`paymentBatches` 复核路由继续拦截 maker 自审与二次复核人冲突;`PaymentBatchesPage` / `ApOperationsPage` 仍按 `payment.batch.manage` 与 `risk.review.manage` 分离入口;focused 单测确认 invalid reimbursement status 不会错误归档发票,也不会误入 ERP / 通知 follow-up;本轮无阻塞问题。"
|
||||
"A5","R78 回归收口与覆盖矩阵回填","执行 R78 post-check,回填合同、coverage matrix 与试运行报告,明确‘报销/票据/OCR’域剩余缺口是否仅剩外部专项与高风险付款场景。","A2-A4 通过后,R78 CSV 状态回填完整;coverage matrix 与 manual trialrun report 结论一致;可明确说明本地试运行对财务付款后链路的覆盖边界。","命令:`cd server && ./scripts/regression-payment-batches.sh`;`cd client && E2E_BASE_URL=http://127.0.0.1:3002 npx playwright test tests/e2e/reimbursement-payment-batch-flow.spec.ts --workers=1 --reporter=list`。业务:汇总批次执行、报销 paid、AP/支付批次结果与报告回填。","manual|playwright","DONE","DONE","DONE","issues/2026-04-29_10-14-27-r78-reimbursement-payment-tail.csv|artifacts/r75/coverage-matrix.csv|artifacts/r70/manual-trialrun-report.md","[""A4""]","若 R78 通过,下一步再决定是否单开高风险二次复核专项;本轮不进入真实 ERP/企微/HTTPS。2026-04-29:`./scripts/regression-payment-batches.sh` 与 focused Playwright 均通过;coverage matrix 已将报销/票据/OCR 域提升为 done,并把 payment batch / AP happy path 收口到 partial_done;manual trialrun report 已补 R78 财务支付尾链结论。"
|
||||
|
@@ -6,6 +6,30 @@ const { createPrismaClient } = require("../prismaClient");
|
||||
const { hashPassword } = require("../utils/crypto");
|
||||
const { validatePassword } = require("../utils/passwordPolicy");
|
||||
|
||||
const DEFAULT_FORCE_ON_FLAGS = Object.freeze([
|
||||
"portal_home_v1",
|
||||
"announcements_v1",
|
||||
"global_search_v1",
|
||||
"presence_board_v1",
|
||||
"attendance_v1",
|
||||
"weekly_report_v1",
|
||||
"employee_lifecycle_v1",
|
||||
"todo_board_v1",
|
||||
"project_domain_v1",
|
||||
"project_linking_v1",
|
||||
"project_communication_log_v1",
|
||||
"project_closure_checklist_v1",
|
||||
"confidential_doc_refs_v1",
|
||||
]);
|
||||
|
||||
const OPTIONAL_FORCE_ON_FLAGS = new Set([
|
||||
"erp_sync",
|
||||
"budget_precheck_v1",
|
||||
"payment_batch_v1",
|
||||
"workflow_definition_admin_v1",
|
||||
"cutover_console_v1",
|
||||
]);
|
||||
|
||||
function requiredEnv(name) {
|
||||
const value = process.env[name];
|
||||
if (typeof value !== "string" || !value.trim()) {
|
||||
@@ -14,6 +38,29 @@ function requiredEnv(name) {
|
||||
return value.trim();
|
||||
}
|
||||
|
||||
function parseExtraForceOnFlags() {
|
||||
const raw = String(process.env.TRIALRUN_EXTRA_FORCE_ON_FLAGS ?? "").trim();
|
||||
if (!raw) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const parts = raw
|
||||
.split(/[\s,]+/)
|
||||
.map((item) => item.trim())
|
||||
.filter(Boolean);
|
||||
const unique = Array.from(new Set(parts));
|
||||
const invalid = unique.filter((flagKey) => !OPTIONAL_FORCE_ON_FLAGS.has(flagKey));
|
||||
if (invalid.length > 0) {
|
||||
throw new Error(
|
||||
`Unsupported TRIALRUN_EXTRA_FORCE_ON_FLAGS: ${invalid.join(", ")}. Allowed: ${Array.from(
|
||||
OPTIONAL_FORCE_ON_FLAGS,
|
||||
).join(", ")}`,
|
||||
);
|
||||
}
|
||||
|
||||
return unique;
|
||||
}
|
||||
|
||||
async function ensureDepartment(prisma, payload) {
|
||||
const now = new Date();
|
||||
const existing = await prisma.department.findFirst({
|
||||
@@ -255,21 +302,10 @@ async function main() {
|
||||
|
||||
await ensureWorkspaceSetting(prisma);
|
||||
|
||||
for (const flagKey of [
|
||||
"portal_home_v1",
|
||||
"announcements_v1",
|
||||
"global_search_v1",
|
||||
"presence_board_v1",
|
||||
"attendance_v1",
|
||||
"weekly_report_v1",
|
||||
"employee_lifecycle_v1",
|
||||
"todo_board_v1",
|
||||
"project_domain_v1",
|
||||
"project_linking_v1",
|
||||
"project_communication_log_v1",
|
||||
"project_closure_checklist_v1",
|
||||
"confidential_doc_refs_v1",
|
||||
]) {
|
||||
const extraForceOnFlags = parseExtraForceOnFlags();
|
||||
const forceOnFlags = [...DEFAULT_FORCE_ON_FLAGS, ...extraForceOnFlags];
|
||||
|
||||
for (const flagKey of forceOnFlags) {
|
||||
await ensureFeatureFlag(prisma, {
|
||||
flag_key: flagKey,
|
||||
override_mode: "force_on",
|
||||
@@ -287,6 +323,8 @@ async function main() {
|
||||
console.log(`- phone: ${adminPhone}`);
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(`- created: ${result.created}`);
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(`- force_on flags: ${forceOnFlags.join(", ")}`);
|
||||
if (result.password) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(`- one-time password: ${result.password}`);
|
||||
|
||||
在新工单中引用
屏蔽一个用户