diff --git a/.trellis/spec/hyb-oa/reimbursement-invoice-ocr.md b/.trellis/spec/hyb-oa/reimbursement-invoice-ocr.md index 57a53c34..4ae22146 100644 --- a/.trellis/spec/hyb-oa/reimbursement-invoice-ocr.md +++ b/.trellis/spec/hyb-oa/reimbursement-invoice-ocr.md @@ -383,6 +383,96 @@ node scripts/qa/check-ocr-provider-samples.mjs \ The caller records stdout before applying the exit-code gate; it never uses a `set -e` assignment whose non-zero status exits before the JSON is emitted. +## Scenario: Travel Allowance Expense-Type Boundary + +### 1. Scope / Trigger + +- Trigger: adding or changing travel reimbursement types, travel-request + transport mappings, allowance policies or expense standards. +- Purpose: keep intercity transport reimbursed by evidence separate from the + two daily lump-sum allowances, and avoid false over-limit results until the + reimbursement item model supports a day count. + +### 2. Signatures + +- Seed contract in `server/src/scripts/seed-config.js`: + - `travel_bus` -> `大巴车票`. + - `travel_meal` -> `伙食补助费`. + - `travel_local_transport_allowance` -> `市内交通补助费`. +- Travel conversion contract in `server/src/routes/reimbursements.js`: + `TRAVEL_TRANSPORT_EXPENSE_TYPE_CODES.bus === "travel_bus"`. +- Current item data boundary: `ReimbursementItem` stores expense type, date, + amount and description, but no quantity or eligible-day count. + +### 3. Contracts + +- `travel_bus` always means an intercity bus/coach ticket created from or used + with a travel segment. It must not represent local transport allowance. +- Meal allowance is CNY 100 per eligible person-day; local transport allowance + is CNY 80 per eligible person-day. Both are lump-sum items without an invoice. +- Employees create one item per allowance, set the amount to eligible days + multiplied by the rate, and write the date range, day count, rate and formula + in the description. Supervisor and finance review that calculation. +- Departure and return dates each count as one natural day; same-day return is + one day. Personal extensions do not count. Meals or vehicles paid directly + by the company or another organization exclude the corresponding allowance. +- Claiming meal allowance excludes separate personal meal reimbursement; + claiming local transport allowance excludes bus, metro, taxi and ride-hailing + reimbursement during the trip. Intercity transport, accommodation and other + approved necessary expenses remain evidence-based actual reimbursement. +- `ExpenseStandard.per_unit=day` currently does not multiply `max_amount` by a + day count. Do not seed or configure CNY 100/CNY 80 warn/block standards until + item quantity/day multiplication is implemented end to end. + +### 4. Validation & Error Matrix + +- Travel segment `bus` maps to an allowance type -> block the change; preserve + `travel_bus` and add/use the separate allowance code. +- CNY 100 or CNY 80 daily rate configured as the current item-level maximum -> + remove/disable it because a multi-day allowance would be falsely over limit. +- Allowance item lacks date range, eligible-day count, rate or formula -> return + for completion during supervisor/finance review; do not invent a day count. +- Company/other party directly bore the corresponding cost -> exclude those + days from the applicable allowance and record the reason. + +### 5. Good/Base/Bad Cases + +- Good: a three-day eligible trip uses separate meal and local-transport items + whose descriptions show `3 天 × 单价`, while rail/hotel items carry PDFs and + required payment evidence. +- Base: intercity coach travel uses `travel_bus` / `大巴车票` and remains an + actual-expense item with valid evidence. +- Bad: rename `travel_bus` to local transport allowance, or configure a CNY 80 + item maximum and thereby reject a valid multi-day allowance. + +### 6. Tests Required + +- Seed-default unit test asserts all three codes and Chinese names are distinct. +- Travel-request conversion test asserts `transport_type=bus` produces an item + whose expense type code is `travel_bus` and description identifies `大巴`. +- When automatic day calculation is later implemented, add service and route + tests for same-day, multi-day, personal-extension and directly-borne cases + before enabling any daily standard. +- Documentation checks must keep the employee guide, reimbursement fact source + and production expense-type names aligned. + +### 7. Wrong vs Correct + +#### Wrong + +```js +const TRAVEL_TRANSPORT_EXPENSE_TYPE_CODES = { bus: "travel_local_transport_allowance" }; +// Current fee control compares the whole multi-day item directly with 80. +``` + +#### Correct + +```js +const TRAVEL_TRANSPORT_EXPENSE_TYPE_CODES = { bus: "travel_bus" }; +// Local transport allowance is a separate manually calculated item until the +// model and fee-control service both support eligible-day multiplication. +``` + ## Scenario: Payment Batch Receipt Attention Count ### 1. Scope / Trigger diff --git a/.trellis/tasks/07-28-travel-reimbursement-guide/design.md b/.trellis/tasks/07-28-travel-reimbursement-guide/design.md index 65717d31..cfa5ffaf 100644 --- a/.trellis/tasks/07-28-travel-reimbursement-guide/design.md +++ b/.trellis/tasks/07-28-travel-reimbursement-guide/design.md @@ -2,7 +2,7 @@ ## 边界 -本任务不开发新的 OCR provider,也不开放图片发票或改变审批流。在现有腾讯 provider 内把统一入口从 `VatInvoiceOCR` 改为 `RecognizeGeneralInvoice`,增加票种感知的安全归一化和上传大小门禁,再完成生产配置恢复、真实链路验证及两份既有业务文档更新。 +本任务不开发新的 OCR provider,也不开放图片发票或改变审批流。在现有腾讯 provider 内把统一入口从 `VatInvoiceOCR` 改为 `RecognizeGeneralInvoice`,增加票种感知的安全归一化和上传大小门禁,再完成生产配置恢复、真实链路验证、差旅补助费用类型收口及两份既有业务文档更新。 生产配置写入、容器重建、腾讯 API 调用和真实样票上传均属于单独受控步骤;每一步只在前一步通过后继续。 @@ -60,6 +60,14 @@ - `docs/报销发票附件规则.md` 固化支付证明必交、敏感字段遮挡、退款/代付/优惠差异和无截图例外口径。 - OCR 结果仅辅助识别和查重;财务仍核对明细、PDF 原件和个人支付证明。 +## 差旅补助与费用类型 + +- `travel_bus` 已由差旅行程 `transport_type=bus` 自动映射,业务语义是城际“大巴”;将其显示名收口为“大巴车票”,不得复用为市内交通补助。 +- `travel_meal` 保留代码并将显示名收口为“伙食补助费”;新增 `travel_local_transport_allowance`,显示名为“市内交通补助费”。生产当前两类既有费用均无历史报销明细,种子脚本按代码幂等更新名称和新增类型。 +- 伙食补助费为 100 元/人/天,市内交通补助费为 80 元/人/天,均按自然日包干且不要求发票;直接承担、重复报销和个人延长天数按 PRD 规则处理。 +- 当前 `ReimbursementItem` 没有天数或数量字段,`ExpenseStandard.per_unit=day` 也不会在费用控制中乘以天数。因此本轮不创建 100/80 元 block 或 warn 标准,员工分别新增补助明细,以“天数 × 单价”人工算出总额并在说明写明计算式,由主管和财务复核。 +- 自动带出差旅起止日、计算符合条件的补助天数以及按天校验属于后续产品能力,不阻塞本次真实报销。 + ## 失败与回退 - adapter 聚焦测试、配置 smoke、腾讯调用或任一真实样票验证失败:停止开放真实票据,不继续制造 pending/retry 数据。 diff --git a/.trellis/tasks/07-28-travel-reimbursement-guide/implement.md b/.trellis/tasks/07-28-travel-reimbursement-guide/implement.md index dd294b3d..1f0bfd97 100644 --- a/.trellis/tasks/07-28-travel-reimbursement-guide/implement.md +++ b/.trellis/tasks/07-28-travel-reimbursement-guide/implement.md @@ -62,6 +62,14 @@ - [x] 更新 `docs/报销发票附件规则.md` 的支付截图、实报实销差异、退款/代付和人工复核规则。 - [x] 不新建平行员工指南,不写入真实票据、支付信息、账号或环境路径。 +## 5.1 差旅补助规定落地 + +- [x] 修正 `travel_bus` 显示名为“大巴车票”,修正 `travel_meal` 显示名为“伙食补助费”,新增 `travel_local_transport_allowance` / “市内交通补助费”。 +- [x] 增加聚焦测试,锁定城际大巴映射和两项补助费用类型不混用。 +- [x] 更新员工指南和报销事实源,写明 100 元/天伙食补助、80 元/天市内交通补助、自然日、包干、直接承担、个人延长和其他费用实报实销规则。 +- [x] 明确当前由员工按“天数 × 单价”人工填写总额;不配置会误判多日补助的单条费用标准。 +- [x] 重新生成并检查一页版差旅报销 PDF。 + ## 6. 验证与审查 - [x] 静态核对页面/API 上传格式、明细绑定和员工/财务字段边界。 @@ -115,6 +123,13 @@ - 仅在铁路通过后运行住宿样票并明确 `PASS`:票种为数电发票,号码、日期、金额、税额、价税合计、销售方和购买方字段命中,无缺失必填字段。 - 两次授权腾讯 OCR 调用已全部用完。输出仅保留匿名 alias、票种和字段命中布尔值;两次探针均未创建 OA 发票或 OCR 数据,最近 30 分钟对应新增记录为 `0/0`,无需业务数据清理。 +## 2026-07-29 差旅补助规定实施验证 + +- 已把城际大巴、伙食补助和市内交通补助收口为三个独立稳定代码;种子聚焦测试证明按代码幂等更新/创建,不配置 100/80 元单条费用标准。 +- 员工指南与报销事实源已写明两项补助单价、自然日、包干、直接承担不重复领取、个人延长不计补助和人工算式要求;单页 PDF 已重新生成并完成视觉检查。 +- 聚焦测试 3 个文件/17 项、后端全量 168 个文件/1077 项、Prisma validate、流程合同、文档审计、Trellis governance、敏感内容、task validate、Node 语法和 `git diff --check` 均通过;文档审计仅保留 22 个既有未注册文档 warning。 +- 本地实施不等于生产费用类型已变更。提交、推送、部署和生产 `seed:config` 仍等待对应授权;在生产只读复核三个费用类型且确认未新增错误费用标准前,不通知杨俊按补助新口径提交。 + ## 风险文件与操作 - 生产环境文件:包含 Secret,只允许用户现场静默输入;不得由补丁、聊天或命令参数传入。 diff --git a/.trellis/tasks/07-28-travel-reimbursement-guide/prd.md b/.trellis/tasks/07-28-travel-reimbursement-guide/prd.md index 98c29dbf..e03dc338 100644 --- a/.trellis/tasks/07-28-travel-reimbursement-guide/prd.md +++ b/.trellis/tasks/07-28-travel-reimbursement-guide/prd.md @@ -2,7 +2,7 @@ ## Goal -先把生产发票识别从 mock 切换到真实腾讯 OCR 并取得授权样票证据,再让杨俊及后续员工一次看懂差旅报销需要提交什么、分别上传到哪里,以及财务如何按“明细—发票—个人支付证明”复核。 +先把生产发票识别从 mock 切换到真实腾讯 OCR 并取得授权样票证据,再让杨俊及后续员工一次看懂差旅报销需要提交什么、分别上传到哪里、差旅补助如何计算,以及财务如何按“明细—发票—个人支付证明”复核。 ## Background @@ -26,6 +26,7 @@ - 更新 `docs/报销发票附件规则.md`,固化火车票/住宿费支付证明、实报实销和 OCR 关闭期间人工核对规则。 - 配置并正式启用生产腾讯 OCR,完成配置门禁、受控发布、公网检查和授权 PDF 样票验证。 - 在现有腾讯 provider 内改用通用票据识别高级版,补齐住宿电子普通发票和铁路电子客票的安全归一化、质量门禁、大小限制和测试。 +- 固化公司差旅补助规定,补齐不与城际大巴费用混淆的费用类型,并同步员工指南和报销事实源。 ## Requirements @@ -47,6 +48,12 @@ - 当前无独立 worker 且 server 自身运行 jobs,配置变更只重建实际加载配置的 server;不得额外启动第二个 OCR job executor。 - 真实能力必须使用已授权 PDF 样票验证;缺少授权样票只能证明配置齐全,不能宣称真实 OCR 已通过。 - 指南明确审批链:员工提交、直属主管审批、财务复核;财务付款和回单不由员工预填。 +- 伙食补助费按 100 元/人/天包干,市内交通补助费按 80 元/人/天包干;两项补助均不要求发票。 +- 补助按出差自然日计算,出发日和返回日均计 1 天,当天往返计 1 天;因个人原因延长的天数不计补助。 +- 领取伙食补助后个人餐费不得另报;领取市内交通补助后,出差期间公交、地铁、出租车、网约车等不得另报。 +- 公司或其他单位已直接承担餐食或车辆费用的,不重复领取相应补助;城际交通、住宿及其他经批准的必要差旅费用凭有效材料实报实销。 +- 当前报销明细没有天数字段,两项补助分别作为独立明细人工填写总额和计算式,例如“伙食补助费:3 天 × 100 元”;不得把现有 `per_unit=day` 费用标准直接配置为 100/80 元,因为当前校验不会按天数乘算,会误判多日补助。 +- `travel_bus` 保留为城际大巴车票费用;市内交通补助使用独立费用类型,不得共用代码或混同展示。 - 文档不放真实票据、支付截图、账号、订单号、银行信息或完整业务数据。 ## Acceptance Criteria @@ -59,6 +66,9 @@ - [ ] 生产实际 provider 为 `tencent`,只授予 `ocr:RecognizeGeneralInvoice`,两个凭证字段已配置但不泄露,external deps smoke、`/ready` 和运行 job 拓扑通过。 - [ ] 住宿电子普通发票和铁路电子客票各一份已授权 PDF 经真实腾讯 OCR 进入可解释终态,并证明没有调用 mock;样票和原始识别 payload 不进入仓库或聊天。 - [ ] 明确 OCR 只辅助字段识别/查重,财务仍执行明细—PDF 原件—支付证明复核。 +- [ ] 员工指南和报销事实源完整写明两项补助的单价、自然日口径、包干不另报、直接承担不重复领取和个人延长不计补助规则。 +- [ ] 生产种子数据中“伙食补助费”“市内交通补助费”均为独立 enabled 费用类型,`travel_bus` 仍明确表示城际大巴车票。 +- [ ] 未配置会把多日补助误判为超标的 100/80 元单条费用标准;指南要求员工按天数人工计算并在说明中写出算式。 - [ ] 在真实 OCR 验收完成前不得仅因指南已完成就通知杨俊可以提交。 - [ ] 全程不把真实票据、支付截图、完整账号、银行资料、Secret 或原始 payload 写入仓库、任务或聊天。 @@ -66,9 +76,9 @@ - 新增第二个 OCR provider、开放图片发票、自动验真或增加人工发票字段。 - 本轮不做通用发票平台或覆盖全部票种;仅支持现有住宿 VAT/电子普通发票和电子发票(铁路电子客票)主路径。 -- 修改报销页面、上传格式、审批规则、费用标准或费用类型。 +- 修改报销页面、上传格式、审批规则或自动按天计算;本轮只调整差旅相关费用类型名称并新增独立的市内交通补助类型,不配置 100/80 元费用标准。 - 代替杨俊上传票据、创建报销、提交审批,或代替主管/财务处理。 -- 制定覆盖所有费用类别的完整财务制度;本轮只收口首笔差旅的火车票和住宿费。 +- 制定覆盖所有费用类别的完整财务制度;本轮只收口首笔差旅的实报实销材料和两项包干补助。 ## Test Plan diff --git a/docs/员工快速上手.md b/docs/员工快速上手.md index cf41117e..0f41073c 100644 --- a/docs/员工快速上手.md +++ b/docs/员工快速上手.md @@ -49,18 +49,21 @@ - OAOPS1 期间请用真实业务数据完成报销、采购/费用、合同台账和项目关联;遇到提交、审批、付款、导出或权限阻断时,记录页面、步骤和影响等级,让管理员当天建修复合同或暂停对应流程。 - OALAUNCH1 收口期内,请把真实业务问题按“阻断 / 非阻断 / 建议”告诉管理员;不要只在聊天里说“系统有问题”。阻断包括无法提交、无法审批、付款/AP 无法闭环、越权可见、合同或资产敏感信息泄露、备份恢复或导出证据缺失。 -### 差旅报销一页指南(火车票 + 住宿费) +### 差旅报销一页指南 1. 从企业微信工作台进入海音达工作台,打开 `报销`,新建报销单。 -2. 把费用拆成两条明细,不要合并填写: +2. 把费用拆成独立明细,不要合并填写: - 火车票:费用类型选“火车票”,填写乘车日期、个人实际支付的合规金额和事由,例如“项目出差往返高铁”。 - 住宿费:费用类型选“住宿费”,填写实际住宿日期、个人实际支付的合规金额和事由,例如“项目出差住宿”。 + - 伙食补助费:按符合条件的出差自然日 `N 天 × 100 元` 计算总额,说明中写明天数、日期范围和算式,不要求发票。 + - 市内交通补助费:按符合条件的出差自然日 `N 天 × 80 元` 计算总额,说明中写明天数、日期范围和算式,不要求发票。 3. 在每条明细的“添加发票”入口上传对应的官方 PDF:火车电子票据 PDF 绑定火车票明细,住宿电子普通发票 PDF 绑定住宿费明细。每个 PDF 只放一张票据、只识别第一页,单个文件不能超过 7 MiB;图片、照片和支付截图不能当作正式发票上传。 4. 火车票和住宿费都必须另附个人支付截图。支付截图通过整张报销单的“上传附件 / 补充材料”入口上传,并在文件名或说明中写清对应“火车票”还是“住宿费”。 5. 支付截图应看得出商户、金额、支付时间和支付成功状态;银行卡号、支付账号、订单号等与复核无关的信息先遮挡。 -6. 明细金额填写个人实际支付的合规金额,原则上不得超过有效发票金额。使用优惠券、多人合并开票、部分自费或发票金额与实际支付金额不一致时,必须在明细说明中写清原因。 -7. 已退款、公司直接支付或他人承担的金额不能再次报销。确实无法提供支付截图时,上传情况说明,由财务决定退回还是例外接受,不得补造凭证。 -8. 提交前逐项检查:两条明细、两份对应 PDF、两类支付证明和金额说明都已齐全。提交后由直属主管审批、财务复核;实际付款和付款回单由财务处理,员工不预填。 +6. 补助按自然日计算:出发日、返回日各计 1 天,当天往返计 1 天;因个人原因延长的天数不计。公司或其他单位已直接承担餐食或车辆费用的,对应补助不得重复领取。 +7. 伙食补助和市内交通补助均为包干:领取伙食补助后个人餐费不得另报;领取市内交通补助后,出差期间公交、地铁、出租车、网约车等不得另报。城际交通、住宿及其他经批准的必要差旅费用凭有效材料实报实销。 +8. 实报实销明细填写个人实际支付的合规金额,原则上不得超过有效发票金额。优惠券、多人合并开票、部分自费或发票金额与实付金额不一致时,必须说明;已退款、公司直接支付或他人承担的金额不能再次报销。无法提供支付截图时上传情况说明,由财务决定是否例外接受,不得补造凭证。 +9. 提交前检查各项明细、对应 PDF、火车票和住宿费支付证明以及补助算式。提交后由直属主管审批、财务复核;实际付款和付款回单由财务处理,员工不预填。 OCR 会辅助识别发票字段和查重,但不代表系统已经确认费用真实合规。财务仍会同时核对明细金额、PDF 原件和个人支付证明;三者不一致时会退回补充或更正。 diff --git a/docs/报销发票附件规则.md b/docs/报销发票附件规则.md index b8c58818..4d73ef0a 100644 --- a/docs/报销发票附件规则.md +++ b/docs/报销发票附件规则.md @@ -133,7 +133,7 @@ R5 公网验收后确认:正式发票/OCR 上传入口只接受 PDF 发票文 ## 差旅实报实销与支付证明 -首笔差旅报销按“明细—正式发票—个人支付证明”三方对应复核,当前火车票和住宿费执行以下规则: +首笔差旅报销按“实报实销明细—正式票据—个人支付证明”和“包干补助—天数—单价”分别复核。 - 火车票和住宿费必须拆成不同明细,分别选择生产已有的“火车票”和“住宿费”费用类型,填写发生日期、个人实际支付的合规金额和事由。 - 火车电子票据、住宿电子普通发票等正式票据必须使用官方 PDF,从对应明细的发票入口上传并绑定;发票截图、支付截图或拍照图片不得冒充正式发票。 @@ -145,6 +145,16 @@ R5 公网验收后确认:正式发票/OCR 上传入口只接受 PDF 发票文 - OCR 只辅助识别发票字段、匹配和查重,不判断支付真实性、费用合理性或发票法律状态。财务复核时必须同时查看明细金额、PDF 原件和个人支付证明;三者不一致时退回更正或补充说明。 - 财务付款凭证号、付款日期、付款渠道和付款回单属于审批通过后的财务处理,不是员工提交报销时填写的个人支付证明。 +### 差旅补助规定 + +- 伙食补助费为 100 元/人/天,市内交通补助费为 80 元/人/天,均按符合条件的出差自然日包干发放,不要求发票。 +- 出发日和返回日均计 1 天,当天往返计 1 天;因个人原因延长的行程天数不计补助。 +- 领取伙食补助后,个人餐费不得另行报销;领取市内交通补助后,出差期间公交、地铁、出租车、网约车等市内交通费用不得另行报销。 +- 公司或其他单位已直接承担相应餐食或车辆费用的,不重复领取对应补助。 +- 城际交通、住宿以及其他经批准的必要差旅费用凭有效材料实报实销。城际大巴费用使用“大巴车票”费用类型,不得填写为“市内交通补助费”。 +- 当前系统不自动计算补助天数。员工应为两项补助分别建立明细,金额填写 `符合条件的天数 × 对应单价` 的总额,并在说明中写明日期范围、天数、单价、算式及不计补助的情况;主管和财务据此复核。 +- 在系统具备数量/天数乘算能力前,不配置 100 元或 80 元的单条费用上限。现有按日单位只作展示,直接配置会把多日补助误判为超标。 + ## PC 与手机体验 PC 端: diff --git a/server/src/scripts/seed-config.js b/server/src/scripts/seed-config.js index 1cdf61a8..d25a26e8 100644 --- a/server/src/scripts/seed-config.js +++ b/server/src/scripts/seed-config.js @@ -7,8 +7,13 @@ const ENABLED_EXPENSE_TYPES = [ { code: "travel_train", name: "火车票", category: "travel" }, { code: "travel_taxi", name: "打车费", category: "travel" }, { code: "travel_hotel", name: "住宿费", category: "travel" }, - { code: "travel_bus", name: "市内交通", category: "travel" }, - { code: "travel_meal", name: "差旅餐补", category: "travel" }, + { code: "travel_bus", name: "大巴车票", category: "travel" }, + { code: "travel_meal", name: "伙食补助费", category: "travel" }, + { + code: "travel_local_transport_allowance", + name: "市内交通补助费", + category: "travel", + }, { code: "travel_fuel", name: "油费", category: "travel" }, { code: "travel_toll", name: "过路费", category: "travel" }, { code: "travel_parking", name: "停车费", category: "travel" }, @@ -1088,5 +1093,7 @@ if (require.main === module) { module.exports = { DEFAULT_APPROVAL_RULES, + ENABLED_EXPENSE_TYPES, seedApprovalRules, + seedExpenseTypes, }; diff --git a/server/tests/unit/routes/reimbursementTemplates.test.js b/server/tests/unit/routes/reimbursementTemplates.test.js index dbb21aea..6856a7ff 100644 --- a/server/tests/unit/routes/reimbursementTemplates.test.js +++ b/server/tests/unit/routes/reimbursementTemplates.test.js @@ -21,7 +21,7 @@ function buildTemplate(overrides = {}) { return { id: TEMPLATE_ID, name: "国内差旅报销", - description: "机票、火车票与市内交通占位模板", + description: "机票、火车票与大巴车票占位模板", items: [ { expenseTypeId: EXPENSE_TYPE_ID, diff --git a/server/tests/unit/routes/reimbursements.create.test.js b/server/tests/unit/routes/reimbursements.create.test.js index d38f3394..08fdfe2d 100644 --- a/server/tests/unit/routes/reimbursements.create.test.js +++ b/server/tests/unit/routes/reimbursements.create.test.js @@ -60,7 +60,7 @@ function buildTemplate(overrides = {}) { }, { expenseTypeId: BUS_TYPE_ID, - description: "市内交通占位", + description: "大巴车票占位", }, ], ...overrides, @@ -127,7 +127,7 @@ async function buildApp(options = {}) { expenseTypes: options.expenseTypes ?? [ { id: AIRFARE_TYPE_ID, code: "travel_airfare", name: "机票", category: "travel", enabled: true }, { id: TRAIN_TYPE_ID, code: "travel_train", name: "火车票", category: "travel", enabled: true }, - { id: BUS_TYPE_ID, code: "travel_bus", name: "市内交通", category: "travel", enabled: true }, + { id: BUS_TYPE_ID, code: "travel_bus", name: "大巴车票", category: "travel", enabled: true }, { id: TAXI_TYPE_ID, code: "travel_taxi", name: "打车费", category: "travel", enabled: true }, { id: TRANSPORT_TYPE_ID, code: "transport", name: "legacy-交通费", category: "travel", enabled: true }, ], diff --git a/server/tests/unit/scripts/seed-config.defaults.test.js b/server/tests/unit/scripts/seed-config.defaults.test.js index 737a90f8..8c6b2073 100644 --- a/server/tests/unit/scripts/seed-config.defaults.test.js +++ b/server/tests/unit/scripts/seed-config.defaults.test.js @@ -1,8 +1,51 @@ const { DEFAULT_APPROVAL_RULES, + ENABLED_EXPENSE_TYPES, seedApprovalRules, + seedExpenseTypes, } = require("../../../src/scripts/seed-config"); +describe("default travel expense types", () => { + it("keeps intercity bus tickets separate from the two travel allowances", () => { + expect(ENABLED_EXPENSE_TYPES).toEqual( + expect.arrayContaining([ + expect.objectContaining({ code: "travel_bus", name: "大巴车票", category: "travel" }), + expect.objectContaining({ code: "travel_meal", name: "伙食补助费", category: "travel" }), + expect.objectContaining({ + code: "travel_local_transport_allowance", + name: "市内交通补助费", + category: "travel", + }), + ]), + ); + + const travelCodes = ENABLED_EXPENSE_TYPES.filter((item) => + ["travel_bus", "travel_meal", "travel_local_transport_allowance"].includes(item.code), + ); + expect(travelCodes).toHaveLength(3); + }); + + it("upserts the distinct travel types by stable code", async () => { + const upsert = vi.fn().mockResolvedValue({}); + + await seedExpenseTypes({ expenseType: { upsert } }); + + for (const [code, name] of [ + ["travel_bus", "大巴车票"], + ["travel_meal", "伙食补助费"], + ["travel_local_transport_allowance", "市内交通补助费"], + ]) { + expect(upsert).toHaveBeenCalledWith( + expect.objectContaining({ + where: { code }, + update: expect.objectContaining({ name, enabled: true }), + create: expect.objectContaining({ code, name, enabled: true }), + }), + ); + } + }); +}); + describe("small-team default reimbursement approval rules", () => { it("uses finance for every owner fallback and finance review node", () => { expect(DEFAULT_APPROVAL_RULES).toEqual([