From 8110e7df9ceba5867791805ef6b16f8fa2349d6b Mon Sep 17 00:00:00 2001 From: shunli Date: Sat, 9 May 2026 15:28:39 +0800 Subject: [PATCH] =?UTF-8?q?A1-A7=20=E6=94=B6=E5=8F=A3OA=E7=BB=9F=E4=B8=80?= =?UTF-8?q?=E5=AE=A1=E6=89=B9=E4=B8=AD=E5=BF=83=E4=B8=8E=E4=BC=81=E5=BE=AE?= =?UTF-8?q?=E5=9F=BA=E7=A1=80=E8=AE=BE=E6=96=BD=E8=B7=AF=E7=BA=BF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client/src/features/navigation/schema.ts | 8 +++---- .../tests/e2e/r110-mobile-pc-routing.spec.ts | 22 ++++++++++++++---- docs/admin-handbook.md | 23 ++++++++++++------- docs/employee-quickstart.md | 12 ++++++---- docs/product-decisions.md | 16 +++++++++++++ docs/reimbursement-invoice-rules.md | 1 + ...-01-34-oa-unified-approval-wecom-infra.csv | 8 +++++++ server/scripts/regression-audit.sh | 20 ++++++++++------ server/scripts/regression-workflow-actions.sh | 4 ++++ server/scripts/smoke-test.sh | 21 +++++++++++++---- 10 files changed, 103 insertions(+), 32 deletions(-) create mode 100644 issues/2026-05-09_14-01-34-oa-unified-approval-wecom-infra.csv diff --git a/client/src/features/navigation/schema.ts b/client/src/features/navigation/schema.ts index 2c81afb9..bee3ccc1 100644 --- a/client/src/features/navigation/schema.ts +++ b/client/src/features/navigation/schema.ts @@ -1,6 +1,6 @@ -export const MAIN_NAV_ORDER = ["/portal", "/team", "/leave-requests", "/reimbursements", "/assets", "/weekly-reports", "/todos", "/announcements", "/projects"]; +export const MAIN_NAV_ORDER = ["/portal", "/workflow/tasks", "/team", "/leave-requests", "/reimbursements", "/assets", "/weekly-reports", "/todos", "/announcements", "/projects"]; export const ADMIN_NAV_ORDER = ["/system/workspace", "/people", "/organization", "/system/workflow-definitions", "/system/settings", "/system/feature-flags", "/audit-logs"]; -export const BACKUP_NAV_ORDER = ["/attendance", "/workflow/tasks", "/contracts-registry", "/purchase-requests", "/travel-requests", "/budgets", "/payment-batches", "/ap-operations", "/invoice-pool", "/inventory-checks", "/system/cutover", "/system/integrations/erp", "/dashboard", "/settings/profile"]; +export const BACKUP_NAV_ORDER = ["/attendance", "/contracts-registry", "/purchase-requests", "/travel-requests", "/budgets", "/payment-batches", "/ap-operations", "/invoice-pool", "/inventory-checks", "/system/cutover", "/system/integrations/erp", "/dashboard", "/settings/profile"]; const PAGE_TITLES: Array<{ path: string; title: string; group: string }> = [ { path: "/portal", title: "工作台", group: "工作台" }, @@ -22,8 +22,8 @@ const PAGE_TITLES: Array<{ path: string; title: string; group: string }> = [ { path: "/reimbursements", title: "报销", group: "主导航" }, { path: "/leave-requests", title: "请假", group: "主导航" }, { path: "/assets", title: "资产", group: "主导航" }, - { path: "/workflow/tasks/", title: "审批详情", group: "备用模块" }, - { path: "/workflow/tasks", title: "审批待办", group: "备用模块" }, + { path: "/workflow/tasks/", title: "审批详情", group: "主导航" }, + { path: "/workflow/tasks", title: "审批待办", group: "主导航" }, { path: "/mobile/assets/scan", title: "移动资产扫描", group: "移动工作台" }, { path: "/mobile/approvals", title: "移动审批", group: "移动工作台" }, { path: "/mobile/capture", title: "移动拍照上传", group: "移动工作台" }, diff --git a/client/tests/e2e/r110-mobile-pc-routing.spec.ts b/client/tests/e2e/r110-mobile-pc-routing.spec.ts index 675bd1d8..02d35be7 100644 --- a/client/tests/e2e/r110-mobile-pc-routing.spec.ts +++ b/client/tests/e2e/r110-mobile-pc-routing.spec.ts @@ -19,10 +19,24 @@ async function login(page: Page, user: E2EUser) { await page.goto("/login"); const credentialForm = page.locator("#login-identifier, input[autocomplete='username'], input[placeholder*='邮箱']").first(); const accountLoginButton = page.getByRole("button", { name: "使用账号密码登录" }); - const credentialFormVisible = await credentialForm.isVisible({ timeout: 1_000 }).catch(() => false); - const accountLoginVisible = await accountLoginButton.isVisible().catch(() => false); - if (!credentialFormVisible && accountLoginVisible) { - await accountLoginButton.click({ timeout: 5_000 }); + for (let attempt = 0; attempt < 3; attempt += 1) { + const credentialFormVisible = await credentialForm.isVisible({ timeout: 1_000 }).catch(() => false); + if (credentialFormVisible) { + break; + } + + const accountLoginVisible = await accountLoginButton.isVisible({ timeout: 3_000 }).catch(() => false); + if (!accountLoginVisible) { + break; + } + + try { + await accountLoginButton.click({ timeout: 5_000 }); + } catch (error) { + if (attempt >= 2) { + throw error; + } + } } const identifierByTestId = page.getByTestId("login-identifier"); diff --git a/docs/admin-handbook.md b/docs/admin-handbook.md index dc4b4a99..5fbfb9e0 100644 --- a/docs/admin-handbook.md +++ b/docs/admin-handbook.md @@ -3,7 +3,7 @@ ## 1. 适用范围 - 适用于 `small_team_internal` 工作区模式。 - 适用于单公司、少部门、全员账号密码登录的小团队。 -- 目标不是替代飞书/钉钉等 SaaS,而是把内部高频管理动作收口到一个系统。 +- 目标是把内部高频管理动作收口到 `hyb-oa`;企业微信只作为入口、身份、通知和打卡底座。 ## 2. 管理员每周固定动作 - 周一:查看 `Portal` 的团队状态、上周周报汇总、入离职待办、资产待归还。 @@ -12,8 +12,8 @@ ## 3. 首次上线最小配置 1. 进入 `工作区设置`,确认产品模式为 `small_team_internal`。 -2. 仅保留主导航:`Portal`、`团队`、`请假`、`报销`、`资产`、`周报`、`待办`、`公告`、`项目`。 -3. 将 `考勤`、`采购`、`差旅`、`预算`、`支付批次`、`AP 运营`、`ERP`、`AI` 等模块降级为隐藏或管理员备用。 +2. 仅保留主导航:`Portal`、`审批待办`、`团队`、`请假`、`报销`、`资产`、`周报`、`待办`、`公告`、`项目`。 +3. 将 `考勤`、`采购`、`差旅`、`预算`、`支付批次`、`AP 运营`、`ERP`、`AI` 等模块按真实启用情况保留为主路径或管理员备用;审批动作统一留在 OA。 4. 在 `用户管理` 中按推荐预设配置:老板 / 管理员 / 主管 / 员工。 5. 检查功能开关: - 建议开启:`portal_home_v1`、`announcements_v1`、`presence_board_v1`、`weekly_report_v1`、`employee_lifecycle_v1`、`todo_board_v1`、`global_search_v1` @@ -36,10 +36,12 @@ 2. 若员工仍有名下资产或待处理审批任务,系统不会自动完成离职。 3. checklist 完成且阻塞清零后,账号会被停用,主业务入口对该账号不可再用。 -## 5. 团队状态管理 +## 5. 团队状态与考勤口径 - 员工每天可在 `Portal` 或 `团队` 页更新状态:在岗 / 居家 / 外出 / 出差。 - 已批准请假会自动覆盖当天状态为 `请假`。 -- 管理员只需要看当天分布,不做打卡、迟到、排班和月度统计。 +- 企业微信打卡只作为原始采集工具;定位、人脸、Wi-Fi 和排班规则不在 OA 内重造。 +- OA 后续同步企业微信打卡数据后,应结合 OA 请假、补卡、外出、加班审批形成正式考勤口径。 +- 请假、补卡、外出、加班等审批仍在 OA 内完成,不使用企业微信原生审批。 ## 6. 周报与待办 - 周报固定三段:本周完成 / 当前阻塞 / 下周计划。 @@ -84,13 +86,15 @@ 4. 连续 4 周都达到预期后,再将 R66 试点验收关单并形成最终验收结论。 ## 12. 企业微信入口与登录策略 -- 建议把企业微信定位为 `入口 + 提醒 + 可选免登`,不要在企业微信里再维护第二套审批或项目录入路径。 +- 企业微信定位为 `入口 + 免登 + 通知 + 打卡原始采集`,不承载公司正式审批流。 +- 公司只维护一个企业微信自建应用,建议命名为 `海音达工作台`。 - 默认工作台入口在企业微信手机端落到 `/mobile`;桌面浏览器和账号密码登录仍进入 `/portal`。员工真正操作仍在 `hyb-oa` 内完成。 +- 所有报销、采购、费用、合同、请假、补卡、外出、加班、出差审批都在 OA 内提交和审批;企业微信消息只负责提醒并 deeplink 回 OA。 - 企微免登建议先完成账号绑定,再逐步在试点部门开启;任何时候都保留账号密码回退。 - 若 OA 部署到公网,生产建议同时满足:`HTTPS`、`COOKIE_SECURE=true`、登录失败锁定、限流、审计开启。 ### 正式接入最小步骤 -1. 在企业微信管理后台创建或编辑自建应用。 +1. 在企业微信管理后台创建或编辑唯一自建应用:`海音达工作台`。 2. 将工作台入口配置为:`https://oa.hyinda.com/api/auth/wecom/login?returnTo=%2Fmobile`。 3. 将 OAuth 回调地址配置为:`https://oa.hyinda.com/api/auth/wecom/callback`。 4. 在 `deploy/trialrun/.env` 中补齐并开启: @@ -103,12 +107,14 @@ - `WECOM_REDIRECT_URI=https://oa.hyinda.com/api/auth/wecom/callback` - `WECOM_ALLOWED_RETURN_HOSTS=oa.hyinda.com` 5. 先让管理员用账号密码登录,在 `个人设置` 完成一次绑定,再测试企业微信工作台直达;已绑定用户从手机企业微信进入时应直接看到移动工作台。 +6. 不配置企业微信原生审批模板作为正式流程;审批提醒使用企业微信消息,审批动作回到 OA 页面完成。 ### 员工会看到的典型状态 - 未绑定:登录页会提示“当前企业微信账号尚未绑定”,回退到账号密码登录。 - 已绑定:从企业微信工作台进入后直接落到 `/mobile`,桌面或普通浏览器场景仍可进入 `Portal`。 - 入口未开放:个人设置仍可绑定,但登录页不会把企业微信作为正式入口展示。 - 配置不完整:个人设置会明确提示缺少 `CorpID / AgentID / 回调地址` 等原因。 +- 收到审批提醒:点击后进入 OA 审批页,在 OA 中同意、驳回、退回或转审。 ## 13. 轻项目管理使用建议 - 项目主档只做轻量管理:成员、里程碑、任务、风险、交付记录、合同台账与资料引用。 @@ -124,6 +130,7 @@ - `hyb-oa` 只记录涉密合同或资料的元数据:编号、责任人、密级、归档位置、离线档案号。 - 涉密正文、涉密附件、详细报价/成本/毛利、敏感客户资料不进入公网 OA。 - 企业微信通知不得发送涉密正文,只保留最小必要信息和 deeplink。 +- 企业微信审批不作为涉密或财务审批载体,正式审批记录以 OA 审批历史和审计日志为准。 - 如项目被标记为涉密,管理员应要求正文继续保存在内网文件盘、NAS 或离线档案中。 ## 15. 初始化与导入建议 @@ -176,7 +183,7 @@ ## 19. R29 设计与工作台治理口径 - 首页不再做“模块超市”,而是先告诉员工今天该做什么。 - 管理员不要把备用模块重新平铺到主导航,避免系统再次变成模块拼盘。 -- 员工入口默认认知应保持为:`企业微信进入 -> Portal 查看今日动作 -> 进入具体模块处理`。 +- 员工入口默认认知应保持为:`企业微信进入 -> OA 移动工作台 -> 进入审批/报销/请假等模块处理`;PC 用户可直接登录 OA。 - 如果未来新增页面,请优先复用统一骨架: - 首页:`WorkbenchHero + MetricCard + SectionPanel` - 列表页:`PageHeader + FilterBar + 主列表` diff --git a/docs/employee-quickstart.md b/docs/employee-quickstart.md index 6eff4345..b5973dda 100644 --- a/docs/employee-quickstart.md +++ b/docs/employee-quickstart.md @@ -2,6 +2,7 @@ ## 1. 你每周主要会用到什么 - `Portal`:看公告、我的待办、我的资产、本周周报入口。 +- `审批`:处理分配给你的报销、请假、采购、差旅等审批。 - `团队`:更新今天是在岗、居家、外出还是出差。 - `请假`:提交并查看请假记录。 - `报销`:继续提交和跟踪报销。 @@ -32,8 +33,9 @@ - 可填写优先级和截止日期。 - 完成后及时点完成,避免 Portal 一直堆积。 -## 6. 请假和报销 -- 继续按现有流程提交。 +## 6. 审批、请假和报销 +- 公司正式审批都在海音达工作台内完成,不在企业微信审批里处理。 +- 请假、补卡、外出、加班、报销、采购、差旅等申请都按 OA 页面提交。 - 批准后的请假会自动反映到团队状态。 - 报销状态仍可在系统内查看,不需要额外问行政。 @@ -46,14 +48,16 @@ - 每周五提交周报 - 状态有变化时及时更新 `团队` - 有通知先看站内消息,IM 只是补充提醒 +- 收到企业微信审批提醒时,点开后回到 OA 页面处理。 ## 9. 从企业微信进入 - 如果公司启用了企业微信工作台入口,优先从企业微信打开海音达工作台。 -- 企业微信只是入口和提醒,真正填写和修改仍在海音达工作台页面内完成。 +- 企业微信只是入口、免登和提醒,真正填写、审批和修改仍在海音达工作台页面内完成。 +- 企业微信打卡仍按公司要求使用;请假、补卡、外出、加班等修正申请在 OA 内提交。 - 如果企业微信免登失败,直接回到账号密码登录即可。 - 如果你第一次从企业微信进入但看到“尚未绑定”,请不要反复重试,先用账号密码登录,再到 `个人设置 -> 企业微信绑定` 完成绑定。 - 如果看到“登录已失效 / 已超时”,重新从企业微信工作台进入即可。 -- 日常建议:在企业微信里打开海音达工作台,看 `Portal` 和通知;复杂填写、项目更新、报销与周报仍在工作台页面里完成。 +- 日常建议:手机从企业微信打开海音达工作台,PC 直接登录 OA;审批、项目更新、报销与周报都在工作台页面里完成。 ## 10. 项目怎么用 - 如果你是项目成员,优先在 `项目` 页面查看自己的里程碑、任务、风险和交付记录。 diff --git a/docs/product-decisions.md b/docs/product-decisions.md index 386927dd..b424d60c 100644 --- a/docs/product-decisions.md +++ b/docs/product-decisions.md @@ -5,6 +5,22 @@ --- +## 平台边界 + +### P0-1 OA 唯一审批中心 +- `hyb-oa` 是公司唯一业务主系统和唯一审批中心。 +- 报销、采购、费用、合同、请假、补卡、外出、加班、出差等正式审批,都必须在 OA 内提交、审批、留痕、归档。 +- 企业微信原生审批不作为公司正式审批入口;不得出现一部分审批在企业微信、一部分审批在 OA 的割裂口径。 +- 企业微信只作为基础设施:手机工作台入口、免登/绑定、通讯录身份、消息通知、打卡原始采集。 + +### P0-2 企业微信打卡与 OA 考勤口径 +- 企业微信打卡用于原始采集,保留其定位、人脸、Wi-Fi、规则与设备能力。 +- OA 后续可同步企业微信打卡原始记录,并结合 OA 内的请假、补卡、外出、加班审批形成正式考勤口径。 +- 请假、补卡、外出、加班等考勤修正审批仍在 OA 完成,不走企业微信审批。 +- 第一阶段不新增打卡 API 同步实现,先冻结边界,避免在审批中心收口时扩大战线。 + +--- + ## 请假模块 ### L-1 团队日历可见性 diff --git a/docs/reimbursement-invoice-rules.md b/docs/reimbursement-invoice-rules.md index e22a9ec1..07ef85d7 100644 --- a/docs/reimbursement-invoice-rules.md +++ b/docs/reimbursement-invoice-rules.md @@ -9,6 +9,7 @@ - 发票是报销凭证,附件是补充材料,二者不能混用。 - 已经报销、他人已报销、重复上传的发票不能加入新的报销单。 - 服务端必须强约束,前端只负责友好提示。 +- 报销审批必须在 OA 内完成,企业微信只发送提醒和 deeplink,不作为报销审批流或财务附件主库。 ## 发票状态 diff --git a/issues/2026-05-09_14-01-34-oa-unified-approval-wecom-infra.csv b/issues/2026-05-09_14-01-34-oa-unified-approval-wecom-infra.csv new file mode 100644 index 00000000..7d781969 --- /dev/null +++ b/issues/2026-05-09_14-01-34-oa-unified-approval-wecom-infra.csv @@ -0,0 +1,8 @@ +ID,Title,Description,Acceptance,Test_Method,Tools,Dev_Status,Review1_Status,Regression_Status,Files,Dependencies,Notes +A1,废弃飞书方案与清理工作区,"废弃飞书转向基线,删除未跟踪飞书文件并回退本轮飞书相关 tracked 改动,不使用 git reset --hard。","1) git status 不再包含 feishu/lark 相关新增文件;2) 不保留 FEISHU_* 配置、飞书路由、飞书 Prisma 表、飞书前端入口和飞书测试;3) 企业微信现有入口、绑定、通知能力不被删除。","命令:git status --short && rg -n ""FEISHU_|feishu|Feishu|飞书|lark|Lark"" server/src client/src server/prisma server/tests client/tests;业务流程:管理员确认飞书不再作为主平台或接入路线,系统仍保留企业微信工作台入口。",git|ripgrep,DONE,DONE,DONE,server|client|server/prisma|issues,"[]","回滚路径:如后续重新启用飞书,重新创建独立 Issue CSV 和迁移,不复用本轮废弃实现。" +A2,OA 唯一审批中心事实源,"固定 hyb-oa 是唯一业务主系统和唯一审批中心,企业微信原生审批不作为正式审批入口。","1) 产品/管理员/员工文档明确所有审批在 OA 内提交、审批、留痕、归档;2) 报销、采购、费用、合同、请假、补卡、外出、加班、出差均以 OA 审批为准;3) 企业微信只做入口、身份、通知和打卡原始采集。","业务流程:老板/管理员阅读事实源后能确认正式审批只认 OA;步骤:人工检查 docs/product-decisions.md、docs/admin-handbook.md、docs/employee-quickstart.md。",manual,DONE,DONE,DONE,docs/product-decisions.md|docs/admin-handbook.md|docs/employee-quickstart.md,"[""A1""]","不引入企业微信审批 API;不开发复杂 BPM。" +A3,企业微信入口免登通知边界固化,"保留一个企业微信自建应用作为海音达工作台入口,继续使用企业微信免登和消息通知,但审批动作必须回到 OA。","1) 管理员文档明确只建一个企业微信自建应用;2) 工作台入口默认 /mobile,PC 直接访问 OA;3) 企业微信消息只发送脱敏提醒和 deeplink;4) 不新增企业微信审批模板或审批回调依赖。","命令:cd client && npx playwright test tests/e2e/wecom-entry.spec.ts --workers=1 --reporter=list;业务流程:员工从企业微信工作台进入 OA 手机端,审批人从企微提醒点击进入 OA 审批页。",playwright|manual,DONE,DONE,DONE,docs/admin-handbook.md|docs/employee-quickstart.md|client/src/lib/runtimeContext.ts|client/src/pages/LoginPage.tsx,"[""A2""]","真实企业微信后台配置需人工复核;本轮不修改生产后台。" +A4,审批待办导航与移动入口收口,"把审批待办定位为 OA 核心入口而非边缘备用能力,PC 和移动端均应清晰可达。","1) PC 导航或工作台中审批待办入口清晰可达;2) 移动底部导航保留审批入口;3) 企业微信 UA 不误入 PC 页面;4) 不改变既有审批 API 契约。","命令:cd client && npm run typecheck && npm run build;cd client && npx playwright test tests/e2e/wecom-entry.spec.ts tests/e2e/workflow-quick-approve.spec.ts --workers=1 --reporter=list;业务流程:审批人从 PC 或手机进入待办并完成 OA 审批。",typescript|playwright,DONE,DONE,DONE,client/src/components/navigation|client/src/features/navigation.ts|client/src/features/mobile|client/tests/e2e,"[""A2"",""A3""]","若现有入口已满足,仅更新文案/事实源并用测试证明。" +A5,打卡与考勤边界文档化,"明确企业微信负责打卡原始采集,OA 后续同步并结合 OA 审批形成正式考勤口径。","1) 文档明确企业微信打卡不是审批中心;2) OA 请假、补卡、外出、加班审批是考勤修正依据;3) 本轮不新增打卡 API 同步实现;4) 形成后续 backlog 边界。","业务流程:管理员能据文档理解员工在企业微信打卡、在 OA 提交请假/补卡/外出/加班审批,最终考勤口径由 OA 汇总;步骤:人工检查 docs/admin-handbook.md 和 docs/product-decisions.md。",manual,DONE,DONE,DONE,docs/product-decisions.md|docs/admin-handbook.md,"[""A2""]","后续如接入企业微信打卡 API,另立 Issue CSV 和只读同步/补偿策略。" +A6,审批主流程回归,"验证报销、采购、请假等现有 OA 审批主流程不因飞书清理和入口收口被破坏。","1) workflow 语法检查通过;2) workflow actions 回归通过;3) 报销提交/审批/状态更新仍通过;4) 采购和请假审批相关单测或回归通过。","命令:cd server && npm run prisma:validate && node --check src/routes/workflow.js && node --check src/routes/reimbursements.js && ./scripts/regression-workflow-actions.sh;业务流程:报销提交->OA 审批->状态更新;采购提交->OA 审批;请假提交->OA 审批。",node|prisma|bash,DONE,DONE,DONE,server/src/routes/workflow.js|server/src/routes/reimbursements.js|server/scripts/regression-workflow-actions.sh,"[""A1"",""A4""]","涉及 workflow 状态机,完成后需 reviewer/回归重点关注状态跳转和权限。" +A7,部署公网与企业微信真机复核说明,"收口本轮验证结果并明确是否已部署、是否可在公网/企业微信真机测试。","1) 本地 server/client 检查通过或明确阻塞;2) git diff --check 通过;3) 最终回复明确未部署或已部署;4) 若未部署,不提示用户去企业微信真机测试。","命令:git diff --check;如部署则 curl /ready 和企业微信入口 smoke;业务流程:管理员根据最终报告判断是否可进入公网或企业微信工作台验收。",git|curl|manual,DONE,DONE,DONE,server|client|docs|issues|deploy,"[""A1"",""A2"",""A3"",""A4"",""A5"",""A6""]","本轮默认不主动改生产配置;部署需单独确认。" diff --git a/server/scripts/regression-audit.sh b/server/scripts/regression-audit.sh index aa8f8d83..9be9bc1b 100755 --- a/server/scripts/regression-audit.sh +++ b/server/scripts/regression-audit.sh @@ -417,17 +417,23 @@ echo "[8/10] duplicate override audit" RID_OVERRIDE_A="$(create_reimbursement "$EMPLOYEE_TOKEN" "$TMP_DIR/override-reim-a.json")" RID_OVERRIDE_B="$(create_reimbursement "$EMPLOYEE_TOKEN" "$TMP_DIR/override-reim-b.json")" -OV_FILE="$TMP_DIR/override-$RUN_SUFFIX.pdf" -printf '%%PDF-1.4\nMOCK_DUP:%s\n' "$RUN_SUFFIX" > "$OV_FILE" +OV_CODE="AUDDUP$RUN_SUFFIX" +OV_NUMBER="AUDNO$RUN_SUFFIX" +OV_FILE_A="$TMP_DIR/override-a-$RUN_SUFFIX.pdf" +OV_FILE_B="$TMP_DIR/override-b-$RUN_SUFFIX.pdf" +printf '%%PDF-1.4\nMOCK_DUP_A:%s\n' "$RUN_SUFFIX" > "$OV_FILE_A" +printf '%%PDF-1.4\nMOCK_DUP_B:%s\n' "$RUN_SUFFIX" > "$OV_FILE_B" -upload_status="$(upload_invoice "$EMPLOYEE_TOKEN" "$OV_FILE" "$TMP_DIR/override-upload-a.json")" +upload_status="$(upload_invoice "$EMPLOYEE_TOKEN" "$OV_FILE_A" "$TMP_DIR/override-upload-a.json")" assert_status "$upload_status" "200" "upload override invoice A" "$TMP_DIR/override-upload-a.json" IID_OVERRIDE_A="$(jq -er '.data.invoicePoolId' "$TMP_DIR/override-upload-a.json")" +patch_invoice "$EMPLOYEE_TOKEN" "$IID_OVERRIDE_A" "{\"totalAmount\":\"188.80\",\"invoiceCode\":\"$OV_CODE\",\"invoiceNumber\":\"$OV_NUMBER\"}" "$TMP_DIR/override-patch-a.json" bind_invoice "$EMPLOYEE_TOKEN" "$IID_OVERRIDE_A" "$RID_OVERRIDE_A" "$TMP_DIR/override-bind-a.json" -upload_status="$(upload_invoice "$EMPLOYEE_TOKEN" "$OV_FILE" "$TMP_DIR/override-upload-b.json")" +upload_status="$(upload_invoice "$EMPLOYEE_TOKEN" "$OV_FILE_B" "$TMP_DIR/override-upload-b.json")" assert_status "$upload_status" "200" "upload override invoice B" "$TMP_DIR/override-upload-b.json" IID_OVERRIDE_B="$(jq -er '.data.invoicePoolId' "$TMP_DIR/override-upload-b.json")" +patch_invoice "$EMPLOYEE_TOKEN" "$IID_OVERRIDE_B" "{\"totalAmount\":\"188.80\",\"invoiceCode\":\"$OV_CODE\",\"invoiceNumber\":\"$OV_NUMBER\"}" "$TMP_DIR/override-patch-b.json" bind_status="$(api_json_auth POST "$BASE_URL/api/invoice-pool/$IID_OVERRIDE_B/bind" "$EMPLOYEE_TOKEN" "$TMP_DIR/override-bind-b.json" "{\"reimbursementId\":\"$RID_OVERRIDE_B\"}")" assert_status "$bind_status" "409" "bind duplicate before override" "$TMP_DIR/override-bind-b.json" @@ -446,17 +452,17 @@ echo "[9/10] OCR retry audit + VALID_006/VALID_007" NOW_ISO="$(date -u +"%Y-%m-%dT%H:%M:%S.000Z")" OLD_ISO="$(date -u -d "-2 minutes" +"%Y-%m-%dT%H:%M:%S.000Z")" -INVOICE_FREQ="$(create_invoice_record "$EMPLOYEE_IDENTIFIER" "MOCK_OK:$RUN_SUFFIX" "ocr_failed" "0" "$NOW_ISO")" +INVOICE_FREQ="$(create_invoice_record "$EMPLOYEE_IDENTIFIER" "MOCK_OK:FREQ:$RUN_SUFFIX" "ocr_failed" "0" "$NOW_ISO")" retry_freq_status="$(retry_ocr "$EMPLOYEE_TOKEN" "$INVOICE_FREQ" "$TMP_DIR/retry-freq.json")" assert_status "$retry_freq_status" "409" "retry too frequent" "$TMP_DIR/retry-freq.json" assert_error_code "$TMP_DIR/retry-freq.json" "VALID_007" "retry too frequent" -INVOICE_EXCEEDED="$(create_invoice_record "$EMPLOYEE_IDENTIFIER" "MOCK_OK:$RUN_SUFFIX" "ocr_failed" "3" "$OLD_ISO")" +INVOICE_EXCEEDED="$(create_invoice_record "$EMPLOYEE_IDENTIFIER" "MOCK_OK:EXCEEDED:$RUN_SUFFIX" "ocr_failed" "3" "$OLD_ISO")" retry_exceeded_status="$(retry_ocr "$EMPLOYEE_TOKEN" "$INVOICE_EXCEEDED" "$TMP_DIR/retry-exceeded.json")" assert_status "$retry_exceeded_status" "409" "retry attempts exceeded" "$TMP_DIR/retry-exceeded.json" assert_error_code "$TMP_DIR/retry-exceeded.json" "VALID_006" "retry attempts exceeded" -INVOICE_RETRY_OK="$(create_invoice_record "$EMPLOYEE_IDENTIFIER" "MOCK_OK:$RUN_SUFFIX" "ocr_failed" "1" "$OLD_ISO")" +INVOICE_RETRY_OK="$(create_invoice_record "$EMPLOYEE_IDENTIFIER" "MOCK_OK:RETRY:$RUN_SUFFIX" "ocr_failed" "1" "$OLD_ISO")" retry_ok_status="$(retry_ocr "$EMPLOYEE_TOKEN" "$INVOICE_RETRY_OK" "$TMP_DIR/retry-ok.json")" assert_status "$retry_ok_status" "200" "retry ok" "$TMP_DIR/retry-ok.json" assert_jq "$TMP_DIR/retry-ok.json" '.data.status == "pending_ocr"' "retry ok status" diff --git a/server/scripts/regression-workflow-actions.sh b/server/scripts/regression-workflow-actions.sh index f87460af..03cf1aff 100755 --- a/server/scripts/regression-workflow-actions.sh +++ b/server/scripts/regression-workflow-actions.sh @@ -242,6 +242,7 @@ create_and_bind_invoice() { local file_path="$TMP_DIR/${label}-${RUN_SUFFIX}-${RANDOM}.pdf" local upload_body="$TMP_DIR/${label}-upload.json" + local patch_body="$TMP_DIR/${label}-patch.json" local bind_body="$TMP_DIR/${label}-bind.json" printf '%%PDF-1.4\n%s\n' "$label-$RUN_SUFFIX-$(date +%s%N)-$RANDOM" > "$file_path" @@ -253,6 +254,9 @@ create_and_bind_invoice() { local invoice_id invoice_id="$(jq -er '.data.invoicePoolId' "$upload_body")" + status="$(api_json_auth PATCH "$BASE_URL/api/invoice-pool/$invoice_id" "$token" "$patch_body" '{"totalAmount":"188.80"}')" + assert_status "$status" "200" "patch invoice total amount $label" "$patch_body" + status="$(api_json_auth POST "$BASE_URL/api/invoice-pool/$invoice_id/bind" "$token" "$bind_body" "{\"reimbursementId\":\"$reimbursement_id\"}")" assert_status "$status" "200" "bind invoice $label" "$bind_body" diff --git a/server/scripts/smoke-test.sh b/server/scripts/smoke-test.sh index 3825afeb..b860dc12 100755 --- a/server/scripts/smoke-test.sh +++ b/server/scripts/smoke-test.sh @@ -21,17 +21,19 @@ if ! command -v jq >/dev/null 2>&1; then exit 1 fi -if [[ ! -f "$SAMPLE_FILE" ]]; then - echo "[ERROR] Sample file not found: $SAMPLE_FILE" >&2 - exit 1 -fi - TMP_DIR="$(mktemp -d)" cleanup() { rm -rf "$TMP_DIR" } trap cleanup EXIT +if [[ ! -f "$SAMPLE_FILE" ]]; then + REQUESTED_SAMPLE_FILE="$SAMPLE_FILE" + SAMPLE_FILE="$TMP_DIR/hyb-oa-sample-invoice.pdf" + printf '%%PDF-1.4\nhyb-oa generated smoke invoice\n' > "$SAMPLE_FILE" + echo "[WARN] Sample file not found: $REQUESTED_SAMPLE_FILE; using generated smoke fixture" >&2 +fi + RUN_UPLOAD_FILE="$TMP_DIR/upload-$(basename "$SAMPLE_FILE")" cp "$SAMPLE_FILE" "$RUN_UPLOAD_FILE" printf '\n%%RUN-%s\n' "$(date +%s%N)" >> "$RUN_UPLOAD_FILE" @@ -104,6 +106,15 @@ upload_status="$(curl -sS -o "$upload_body" -w "%{http_code}" \ assert_status "$upload_status" "200" "upload invoice" INVOICE_ID="$(extract_json_field "$upload_body" '.data.invoicePoolId')" +echo "[5.1/13] Patch invoice total amount" +patch_invoice_body="$TMP_DIR/patch-invoice.json" +patch_invoice_status="$(curl -sS -o "$patch_invoice_body" -w "%{http_code}" \ + -X PATCH "$BASE_URL/api/invoice-pool/$INVOICE_ID" \ + -H "authorization: Bearer $EMPLOYEE_TOKEN" \ + -H "content-type: application/json" \ + -d '{"totalAmount":"188.80"}')" +assert_status "$patch_invoice_status" "200" "patch invoice total amount" + echo "[6/13] Bind invoice" bind_body="$TMP_DIR/bind.json" bind_status="$(curl -sS -o "$bind_body" -w "%{http_code}" \