diff --git a/artifacts/r70/manual-trialrun-report.md b/artifacts/r70/manual-trialrun-report.md index 7e1375d0..d2e7a488 100644 --- a/artifacts/r70/manual-trialrun-report.md +++ b/artifacts/r70/manual-trialrun-report.md @@ -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` 分离。 diff --git a/artifacts/r75/coverage-matrix.csv b/artifacts/r75/coverage-matrix.csv index 296d4b9d..5b727dfa 100644 --- a/artifacts/r75/coverage-matrix.csv +++ b/artifacts/r75/coverage-matrix.csv @@ -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。" diff --git a/client/tests/e2e/reimbursement-payment-batch-flow.spec.ts b/client/tests/e2e/reimbursement-payment-batch-flow.spec.ts new file mode 100644 index 00000000..56b29452 --- /dev/null +++ b/client/tests/e2e/reimbursement-payment-batch-flow.spec.ts @@ -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()]); + } +}); diff --git a/issues/2026-04-29_10-14-27-r78-reimbursement-payment-tail.csv b/issues/2026-04-29_10-14-27-r78-reimbursement-payment-tail.csv new file mode 100644 index 00000000..b2d99206 --- /dev/null +++ b/issues/2026-04-29_10-14-27-r78-reimbursement-payment-tail.csv @@ -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 财务支付尾链结论。" diff --git a/server/src/scripts/seed-trialrun.js b/server/src/scripts/seed-trialrun.js index 57c3b824..816f8add 100644 --- a/server/src/scripts/seed-trialrun.js +++ b/server/src/scripts/seed-trialrun.js @@ -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}`);