From 9bcd30965f300a05fe0262a9075d7c249c4085a5 Mon Sep 17 00:00:00 2001 From: shunli Date: Mon, 9 Mar 2026 11:15:21 +0800 Subject: [PATCH] =?UTF-8?q?docs/test(P19ERP3-5,P19ERP3-6):=20=E6=94=B6?= =?UTF-8?q?=E5=8F=A3=E8=B5=84=E4=BA=A7ERP=E6=96=87=E6=A1=A3=E7=9F=A9?= =?UTF-8?q?=E9=98=B5=E4=B8=8E=E5=9B=9E=E5=BD=92=E8=84=9A=E6=9C=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/domain-events.md | 19 +- docs/feature-matrix.md | 16 +- docs/integration-boundaries.md | 18 +- scripts/validate-feature-matrix.py | 1 + server/scripts/regression-integrations.sh | 268 +++++++++++++++++++++- 5 files changed, 298 insertions(+), 24 deletions(-) diff --git a/docs/domain-events.md b/docs/domain-events.md index f7a30591..74987bbd 100644 --- a/docs/domain-events.md +++ b/docs/domain-events.md @@ -1,7 +1,7 @@ -# 领域事件清单(P19 / P19+IM-1 / P19+ERP-2) +# 领域事件清单(P19 / P19+IM-1 / P19+ERP-3) -**快照日期**:2026-03-08 -**目标**:冻结 `hyb-oa` 当前采购与资产域的标准领域事件名,并补充 `P19+IM-1` 已实际消费的 IM 通知类型清单,以及 `P19+ERP-1 / ERP-2` 已实际落地的 ERP 消费映射,作为后续 ERP / IM / AI 适配的事实源。 +**快照日期**:2026-03-09 +**目标**:冻结 `hyb-oa` 当前采购与资产域的标准领域事件名,并补充 `P19+IM-1` 已实际消费的 IM 通知类型清单,以及 `P19+ERP-1 / ERP-3` 已实际落地的 ERP 消费映射,作为后续 ERP / IM / AI 适配的事实源。 ## 1. 使用原则 - 本文档同时维护两类事实: @@ -22,7 +22,7 @@ | `asset.card.created` | 资产卡片创建成功后 | `asset_card` | `assetId`, `assetNo`, `name`, `categoryId`, `legalEntityId`, `costCenterId`, `purchaseRequestId`, `status` | ERP、IM、AI | 资产域不接 workflow | | `asset.assigned` | 资产领用成功后 | `asset_card` | `assetId`, `assetNo`, `employeeId`, `assignedAt`, `location`, `status` | IM、AI | 对应新增 open assignment | | `asset.returned` | 资产归还成功后 | `asset_card` | `assetId`, `assetNo`, `employeeId`, `returnedAt`, `location`, `status` | IM、AI | 对应关闭 open assignment | -| `asset.transferred` | 资产调拨成功后 | `asset_card` | `assetId`, `assetNo`, `fromEmployeeId`, `toEmployeeId`, `fromLocation`, `toLocation`, `transferredAt`, `status` | ERP、IM、AI | 可只改位置,不一定改归属人 | +| `asset.transferred` | 资产调拨成功后 | `asset_card` | `assetId`, `assetNo`, `fromEmployeeId`, `toEmployeeId`, `fromLocation`, `toLocation`, `transferredAt`, `status` | IM、AI | 可只改位置,不一定改归属人;ERP-3 一期不消费 | | `inventory_check.completed` | 盘点任务完成后 | `inventory_check` | `inventoryCheckId`, `checkNo`, `scopeType`, `scopeRefId`, `completedAt`, `itemCount`, `summary` | ERP、IM、AI | `summary` 建议包含 matched / missing / extra / damaged 计数 | ## 3. 事件名与运行时对象映射 @@ -63,7 +63,7 @@ ## 7. 推荐消费场景 - IM:采购审批结果、报销结果、资产领用归还、盘点完成提醒 -- ERP:采购审批 commitment、PO/合同归档 accrual / summary、资产卡片新增摘要、盘点完成摘要 +- ERP:采购审批 commitment、PO/合同归档 accrual / summary、资产卡片 ledger summary、盘点完成摘要 - AI:审批摘要、异常提示、主数据重复建议、盘点结果总结 ## 8. P19+IM-1 已消费通知类型 @@ -93,12 +93,15 @@ 这些事件名称仍保留在长期蓝图中,但不属于 `P19+IM-1` 的首批协同覆盖范围。 -## 10. P19+ERP-1 / ERP-2 已实现消费映射 +## 10. P19+ERP-1 / ERP-3 已实现消费映射 - `P19+ERP-1` 没有新增新的领域事件名;ERP 一期真正落地消费的既有业务事实是 `reimbursement.paid`,运行时会先构建标准化 `ReimbursementJournalSlice`,再通过 `erp.journal.export` 写入 `IntegrationOutbox` 异步派发。 - `ReimbursementJournalSlice.sourceEventName` 固定为 `reimbursement.paid`,用于标记该凭证切片来源于“报销付款完成”这一既有业务事实;历史 `erp.reimbursement.paid` outbox 仍继续兼容。 - `P19+ERP-2` 正式冻结新增领域事件名 `purchase_order.archived`,并消费两类采购事实: - `purchase_request.approved` → `PurchaseCommitmentJournalSlice` → `erp.journal.export` - `purchase_order.archived` → `PurchaseAccrualJournalSlice` + `purchase_order_archive` summary → `erp.journal.export` / `erp.procurement.export` - 采购 ERP 成功派发后,运行时统一把外部 ERP 编号回写到 `integration_bindings`,分别对应 `purchase_request_commitment_journal`、`purchase_order_accrual_journal`、`purchase_order_po`、`purchase_order_contract`。 -- ERP 主数据同步(`LegalEntity` / `CostCenter` / `Supplier`)仍属于主动 Pull,不作为新的领域事件名纳入本表;`IntegrationSyncState` 也继续只承载主数据资源级同步状态,而不承载采购单据轨道。 -- `asset.card.created`、`inventory_check.completed` 等事件继续只冻结名称与最小载荷;真实 ERP 导出留待后续 `P19+ERP-3`。 +- ERP 主数据同步(`LegalEntity` / `CostCenter` / `Supplier`)仍属于主动 Pull,不作为新的领域事件名纳入本表;`IntegrationSyncState` 也继续只承载主数据资源级同步状态,而不承载采购或资产单据轨道。 +- `P19+ERP-3` 一期已落地资产消费映射: + - `asset.card.created` → `AssetLedgerSummaryPayload` → `erp.asset.export` + - `PATCH /api/assets/:id` 导致 `legalEntityId / costCenterId / status(retired|archived)` 变化 → 资产 ERP 内部集成事实 → `erp.asset.export` +- 资产 ERP 成功派发后,运行时统一把外部 ERP 资产编号回写到 `integration_bindings(provider=erp, entity_type=asset_erp_card)`;领用 / 归还 / 位置调拨仍不进入 ERP 消费映射。 diff --git a/docs/feature-matrix.md b/docs/feature-matrix.md index 8cc1c392..8e5a8d53 100644 --- a/docs/feature-matrix.md +++ b/docs/feature-matrix.md @@ -1,8 +1,8 @@ -# hyb-oa 实现状态矩阵(P5-P19+IM-1+ERP-2) +# hyb-oa 实现状态矩阵(P5-P19+IM-1+ERP-3) -**快照日期**:2026-03-08 -**范围**:`F1-F42`(含 `F19b/F19c/F19d`)+ `R1-S2` + `P9A1-P9D1` + `P10E1-P10G1` + `G0-G6` + `H1` + `M1-M4` + `P13-0-P13-8` + `P14-0-P14-8` + `P15A-0-P15A-5` + `P15B-0-P15B-6` + `P16A-0-P16A-6` + `P16B-0-P16B-6` + `P16C-0-P16C-6` + `R9-0-R9-3` + `G2-0-G2-6` + `P17A-0-P17A-6` + `P17B-0-P17B-6` + `P18A-0-P18A-6` + `P18B-0-P18B-6` + `P19-0-P19-6` + `P19IM1-0-P19IM1-6` + `P19ERP1-0-P19ERP1-6`,共 206 项。 -**事实源**:`issues/2026-02-09_13-39-02-p5-blueprint-v3.csv` + `issues/2026-02-09_23-30-00-p6-p8-remaining-work-master.csv` + `issues/2026-02-10_09-42-31-p9-maintenance-performance-matrix.csv` + `issues/2026-02-10_10-35-30-p10-production-hardening.csv` + `issues/2026-02-11-p11-p12-governance-mobile.csv` + `issues/2026-02-12_01-09-41-p13-l5-governance.csv` + `issues/2026-02-13_00-11-56-p14-l5-operationalization.csv` + `issues/2026-03-06_14-30-00-p15a-l5-permission-decouple.csv` + `issues/2026-03-06_14-30-00-p15b-l5-leave-v1.csv` + `issues/2026-03-07_13-47-05-p16a-l5-master-data-core.csv` + `issues/2026-03-07_16-22-07-p16b-l5-holiday-calendar.csv` + `issues/2026-03-07_17-14-39-p16c-l5-suppliers.csv` + `issues/2026-03-07_18-15-03-r9-g2-p19-roadmap-refresh.csv` + `issues/2026-03-07_18-16-00-g2-l5-cross-domain-bridge.csv` + `issues/2026-03-07_19-33-09-p17a-l5-procurement-core.csv` + `issues/2026-03-07_21-00-37-p17b-l5-procurement-budget-archive.csv` + `issues/2026-03-07_22-02-09-p18a-l5-asset-registry-core.csv` + `issues/2026-03-07_22-34-03-p18b-l5-asset-movements-inventory.csv` + `issues/2026-03-07_23-22-00-p19-l4-integration-readiness.csv` + `issues/2026-03-08_12-18-10-p19-im-1-l5-collaboration-adapter.csv` + `issues/2026-03-08_14-10-53-p19-erp-1-l5-masterdata-journal-bridge.csv` + `issues/2026-03-08_22-46-51-p19-erp-2-l5-procurement-commitment-archive.csv`(执行合同) +**快照日期**:2026-03-09 +**范围**:`F1-F42`(含 `F19b/F19c/F19d`)+ `R1-S2` + `P9A1-P9D1` + `P10E1-P10G1` + `G0-G6` + `H1` + `M1-M4` + `P13-0-P13-8` + `P14-0-P14-8` + `P15A-0-P15A-5` + `P15B-0-P15B-6` + `P16A-0-P16A-6` + `P16B-0-P16B-6` + `P16C-0-P16C-6` + `R9-0-R9-3` + `G2-0-G2-6` + `P17A-0-P17A-6` + `P17B-0-P17B-6` + `P18A-0-P18A-6` + `P18B-0-P18B-6` + `P19-0-P19-6` + `P19IM1-0-P19IM1-6` + `P19ERP1-0-P19ERP1-6` + `P19ERP2-0-P19ERP2-6` + `P19ERP3-0-P19ERP3-6`,共 220 项。 +**事实源**:`issues/2026-02-09_13-39-02-p5-blueprint-v3.csv` + `issues/2026-02-09_23-30-00-p6-p8-remaining-work-master.csv` + `issues/2026-02-10_09-42-31-p9-maintenance-performance-matrix.csv` + `issues/2026-02-10_10-35-30-p10-production-hardening.csv` + `issues/2026-02-11-p11-p12-governance-mobile.csv` + `issues/2026-02-12_01-09-41-p13-l5-governance.csv` + `issues/2026-02-13_00-11-56-p14-l5-operationalization.csv` + `issues/2026-03-06_14-30-00-p15a-l5-permission-decouple.csv` + `issues/2026-03-06_14-30-00-p15b-l5-leave-v1.csv` + `issues/2026-03-07_13-47-05-p16a-l5-master-data-core.csv` + `issues/2026-03-07_16-22-07-p16b-l5-holiday-calendar.csv` + `issues/2026-03-07_17-14-39-p16c-l5-suppliers.csv` + `issues/2026-03-07_18-15-03-r9-g2-p19-roadmap-refresh.csv` + `issues/2026-03-07_18-16-00-g2-l5-cross-domain-bridge.csv` + `issues/2026-03-07_19-33-09-p17a-l5-procurement-core.csv` + `issues/2026-03-07_21-00-37-p17b-l5-procurement-budget-archive.csv` + `issues/2026-03-07_22-02-09-p18a-l5-asset-registry-core.csv` + `issues/2026-03-07_22-34-03-p18b-l5-asset-movements-inventory.csv` + `issues/2026-03-07_23-22-00-p19-l4-integration-readiness.csv` + `issues/2026-03-08_12-18-10-p19-im-1-l5-collaboration-adapter.csv` + `issues/2026-03-08_14-10-53-p19-erp-1-l5-masterdata-journal-bridge.csv` + `issues/2026-03-08_22-46-51-p19-erp-2-l5-procurement-commitment-archive.csv` + `issues/2026-03-09_00-10-00-p19-erp-3-l5-asset-ledger-summary.csv`(执行合同) **说明**: - 本矩阵用于全局读图(审计/交接/回归覆盖追踪),不替代 `issues/*.csv` 的执行合同职责。 @@ -247,3 +247,11 @@ | P19ERP2-5 | 文档事件与矩阵同步 | N/A | 2026-03-08_22-46-51-p19-erp-2-l5-procurement-commitment-archive.csv | 2026-03-08_22-46-51-p19-erp-2-l5-procurement-commitment-archive.csv | 2026-03-08 | DONE | validate-feature-matrix | 集成边界、领域事件、Feature Matrix 与校验器同步到 `P5-P19+IM-1+ERP-2`,并纳入 ERP-2 合同事实源 | | P19ERP2-6 | P19ERP2回归与验收收口 | N/A | 2026-03-08_22-46-51-p19-erp-2-l5-procurement-commitment-archive.csv | 2026-03-08_22-46-51-p19-erp-2-l5-procurement-commitment-archive.csv | 2026-03-08 | DONE | server:test + regression-integrations + regression-all + client:typecheck/build | 采购双阶段 journal、archive summary 覆盖同步、详情挂件、权限边界与既有 ERP / IM 主流程回归全部通过 | +| P19ERP3-0 | P19ERP3执行合同建档 | N/A | 2026-03-09_00-10-00-p19-erp-3-l5-asset-ledger-summary.csv | 2026-03-09_00-10-00-p19-erp-3-l5-asset-ledger-summary.csv | 2026-03-09 | DONE | CSV 校验 | 资产 ERP 台账摘要执行合同建档,冻结仅做 asset ledger summary export 与仓库事实对齐 | +| P19ERP3-1 | 资产ERP出站契约与provider扩展 | N/A | 2026-03-09_00-10-00-p19-erp-3-l5-asset-ledger-summary.csv | 2026-03-09_00-10-00-p19-erp-3-l5-asset-ledger-summary.csv | 2026-03-09 | DONE | server:test | 新增 `erp.asset.export` 契约、`exportAssetToErp` provider 与 dispatcher / binding 回写规则,不新增核心表 ERP 字段 | +| P19ERP3-2 | 资产建卡与财务维度变更自动出站 | N/A | 2026-03-09_00-10-00-p19-erp-3-l5-asset-ledger-summary.csv | 2026-03-09_00-10-00-p19-erp-3-l5-asset-ledger-summary.csv | 2026-03-09 | DONE | server:test + regression-integrations | `POST /api/assets` 与财务维度 patch 自动写入资产 ERP 摘要 outbox,gate 关闭时仅 skip 不阻断业务 | +| P19ERP3-3 | 资产ERP轨道查询与重试接口 | N/A | 2026-03-09_00-10-00-p19-erp-3-l5-asset-ledger-summary.csv | 2026-03-09_00-10-00-p19-erp-3-l5-asset-ledger-summary.csv | 2026-03-09 | DONE | server:test + regression-integrations | 新增资产 ERP `track / requeue` 接口,支持脱敏查看、历史补发与失败重试 | +| P19ERP3-4 | 资产详情ERP轨道挂件 | N/A | 2026-03-09_00-10-00-p19-erp-3-l5-asset-ledger-summary.csv | 2026-03-09_00-10-00-p19-erp-3-l5-asset-ledger-summary.csv | 2026-03-09 | DONE | client:typecheck/build | `AssetRegistryPage` 详情侧新增资产 ERP 同步轨道挂件,按权限区分状态查看与补发操作 | +| P19ERP3-5 | 文档事件与矩阵同步 | N/A | 2026-03-09_00-10-00-p19-erp-3-l5-asset-ledger-summary.csv | 2026-03-09_00-10-00-p19-erp-3-l5-asset-ledger-summary.csv | 2026-03-09 | DONE | validate-feature-matrix | 集成边界、领域事件、Feature Matrix 与校验器同步到 `P5-P19+IM-1+ERP-3` 并纳入 ERP-3 合同事实源 | +| P19ERP3-6 | P19ERP3回归与验收收口 | N/A | 2026-03-09_00-10-00-p19-erp-3-l5-asset-ledger-summary.csv | 2026-03-09_00-10-00-p19-erp-3-l5-asset-ledger-summary.csv | 2026-03-09 | DONE | server:test + regression-integrations + regression-all + client:typecheck/build | 资产 ERP 摘要出站、详情挂件、脱敏权限、同一 outbox 覆盖同步与既有采购 / 报销 / IM 主流程回归全部通过 | + diff --git a/docs/integration-boundaries.md b/docs/integration-boundaries.md index 4b29b9e6..ca976652 100644 --- a/docs/integration-boundaries.md +++ b/docs/integration-boundaries.md @@ -1,7 +1,7 @@ -# 集成边界冻结(P19 / P19+IM-1 / P19+ERP-2) +# 集成边界冻结(P19 / P19+IM-1 / P19+ERP-3) -**快照日期**:2026-03-08 -**当前基线**:`P19+IM-1`、`P19+ERP-1` 与 `P19+ERP-2` 已完成;当前运行时已落地企微群机器人 + 深链 + Outbox 异步派发的一期协同适配,以及 ERP 主数据 Pull、报销付款凭证切片、采购双阶段 journal 与归档摘要出站。 +**快照日期**:2026-03-09 +**当前基线**:`P19+IM-1`、`P19+ERP-1`、`P19+ERP-2` 与 `P19+ERP-3` 已完成;当前运行时已落地企微群机器人 + 深链 + Outbox 异步派发的一期协同适配,以及 ERP 主数据 Pull、报销付款凭证切片、采购双阶段 journal / 归档摘要出站、资产台账摘要出站。 **目标**:在保持核心域稳定的前提下,冻结 ERP / IM / AI / MCP 的长期边界,并记录 IM 一期与 ERP 两期的已实现范围、禁入规则和后续扩展位。 ## 1. 总体原则 @@ -28,7 +28,7 @@ ### 2.1 允许进入适配层的对象 - 主数据:`LegalEntity`、`CostCenter`、`Supplier` - 采购:`purchase_request` 审批结果、预算占用确认结果、PO / 合同归档摘要 -- 资产:`asset_card` 基础卡片信息、归属变更摘要、盘点完成摘要 +- 资产:`asset_card` 基础卡片信息、财务维度变更摘要、盘点完成摘要 ### 2.2 推荐适配方式 - **主数据同步**: @@ -49,7 +49,7 @@ ### 2.4 建议后续专项 - `P19+ERP-1`:主数据同步协议 - `P19+ERP-2`:采购双阶段 journal 与归档摘要出站 -- `P19+ERP-3`:资产台账摘要导出协议 +- `P19+ERP-3`:资产台账摘要导出协议(已完成一期) ### 2.5 P19+ERP-1 已实现范围 - 已新增 `IntegrationBinding` 与 `IntegrationSyncState` 两张集成层私有表,用于维护 ERP 映射与同步状态,不向核心事务表注入 ERP 私有字段。 @@ -66,6 +66,14 @@ - 已明确 `IntegrationSyncState` 继续只表示资源级主数据同步状态,不承载采购单据级轨道;采购详情轨道只允许由 `IntegrationOutbox + IntegrationBinding + 当前单据事实` 推导。 - 已将采购 ERP 轨道挂件固定在采购详情 / 归档区:所有可读用户仅查看阶段状态,`integration.manage` 才能查看 ERP 外部编号并执行手动补发 / 重试。 +### 2.7 P19+ERP-3 已实现范围 +- 已新增 `erp.asset.export` 单一资产 ERP 事件,固定用于 `asset_card` 的 ledger summary export,不进入 `erp.journal.export`,也不向核心资产表注入 ERP 私有字段。 +- 已冻结资产一期触发边界:仅 `POST /api/assets` 建卡成功,以及 `PATCH /api/assets/:id` 导致 `legalEntityId / costCenterId / status(retired|archived)` 发生变化时自动出站。 +- 已明确 `POST /api/assets/:id/assign`、`POST /api/assets/:id/return`、`POST /api/assets/:id/transfer` 与仅 `name / location / serialNo / activatedAt` 变化不触发 ERP,同步边界仅覆盖财务台账摘要。 +- 已为单资产固定 `asset_card:{assetId}:ledger_summary` dedupe key,并采用“同一条 outbox 覆盖同步”语义:payload 无变化时返回历史记录,payload 有变化或失败 / 死信时仅重置同一条记录。 +- 已在 `integration_bindings` 中固定资产映射 `asset_erp_card`;成功派发后回写 ERP 资产外部编号与最近同步时间。 +- 已将资产 ERP 轨道挂件固定在资产详情侧边栏:所有可读用户查看同步状态,`integration.manage` 才能查看 ERP 外部编号并执行补发 / 重试;`IntegrationSyncState` 继续不承载资产单据级轨道。 + ## 3. IM / 协同适配边界 ### 3.1 一期已实现能力(P19+IM-1) diff --git a/scripts/validate-feature-matrix.py b/scripts/validate-feature-matrix.py index 43a0cf53..a743defe 100755 --- a/scripts/validate-feature-matrix.py +++ b/scripts/validate-feature-matrix.py @@ -41,6 +41,7 @@ CSV_FACT_SOURCES = [ ROOT / "issues" / "2026-03-08_12-18-10-p19-im-1-l5-collaboration-adapter.csv", ROOT / "issues" / "2026-03-08_14-10-53-p19-erp-1-l5-masterdata-journal-bridge.csv", ROOT / "issues" / "2026-03-08_22-46-51-p19-erp-2-l5-procurement-commitment-archive.csv", + ROOT / "issues" / "2026-03-09_00-10-00-p19-erp-3-l5-asset-ledger-summary.csv", ] ID_PATTERN = re.compile(r"^[A-Z][0-9][0-9A-Za-z-]*$") diff --git a/server/scripts/regression-integrations.sh b/server/scripts/regression-integrations.sh index a4c6bd45..97277307 100755 --- a/server/scripts/regression-integrations.sh +++ b/server/scripts/regression-integrations.sh @@ -271,6 +271,54 @@ const server = http.createServer(async (req, res) => { }); } + if (req.method === 'POST' && url.pathname === '/erp/assets/export') { + const mode = url.searchParams.get('mode') || 'success'; + const chunks = []; + for await (const chunk of req) { + chunks.push(chunk); + } + + let parsedBody = null; + try { + parsedBody = JSON.parse(Buffer.concat(chunks).toString('utf8') || '{}'); + } catch { + parsedBody = null; + } + + if (mode === 'timeout') { + setTimeout(() => { + sendJson(res, 200, { + ok: true, + externalAssetId: `ERP-ASSET-${Date.now()}`, + externalAssetNo: `ERP-ASSET-${parsedBody?.assetNo || Date.now()}`, + }); + }, 1500); + return; + } + + if (mode === 'client') { + return sendJson(res, 422, { + ok: false, + code: 'invalid_payload', + message: 'mock asset client error', + }); + } + + if (mode === 'server') { + return sendJson(res, 503, { + ok: false, + code: 'service_unavailable', + message: 'mock asset server error', + }); + } + + return sendJson(res, 200, { + ok: true, + externalAssetId: `ERP-ASSET-${parsedBody?.assetNo || parsedBody?.entityId || Date.now()}`, + externalAssetNo: `ERP-ASSET-${parsedBody?.assetNo || parsedBody?.entityId || Date.now()}`, + }); + } + sendJson(res, 404, { ok: false, message: 'not_found' }); }); @@ -304,9 +352,11 @@ start_app_server() { ERP_MASTER_DATA_ENDPOINT="http://127.0.0.1:$MOCK_ERP_PORT/erp/master-data/pull?mode=success" \ ERP_JOURNAL_EXPORT_ENDPOINT="http://127.0.0.1:$MOCK_ERP_PORT/erp/journals/export?mode=success" \ ERP_PROCUREMENT_EXPORT_ENDPOINT="http://127.0.0.1:$MOCK_ERP_PORT/erp/procurement/export?mode=success" \ + ERP_ASSET_EXPORT_ENDPOINT="http://127.0.0.1:$MOCK_ERP_PORT/erp/assets/export?mode=success" \ ERP_SYNC_ENDPOINT="http://127.0.0.1:$MOCK_ERP_PORT/erp/reimbursements/paid?mode=success" \ ERP_SYNC_TIMEOUT_MS="$ERP_SYNC_TIMEOUT_MS" \ ERP_PROCUREMENT_TIMEOUT_MS="$ERP_SYNC_TIMEOUT_MS" \ + ERP_ASSET_TIMEOUT_MS="$ERP_SYNC_TIMEOUT_MS" \ ERP_MASTER_DATA_TIMEOUT_MS="$ERP_SYNC_TIMEOUT_MS" \ ERP_SYNC_AUTH_TOKEN="" \ node src/index.js >"$TMP_DIR/app.log" 2>&1 & @@ -404,6 +454,39 @@ const { createPrismaClient } = require('./src/prismaClient'); NODE } +reset_erp_integration_state() { + DATABASE_URL="$DATABASE_URL" node <<'NODE' +require('dotenv/config'); +const { createPrismaClient } = require('./src/prismaClient'); + +(async () => { + const { prisma, pool } = createPrismaClient(); + await prisma.$connect(); + try { + await prisma.integrationOutbox.deleteMany({ + where: { + event_type: { + in: ['erp.reimbursement.paid', 'erp.journal.export', 'erp.procurement.export', 'erp.asset.export'], + }, + }, + }); + await prisma.integrationBinding.deleteMany({ + where: { provider: 'erp' }, + }); + await prisma.integrationSyncState.deleteMany({ + where: { provider: 'erp' }, + }); + } finally { + await prisma.$disconnect(); + await pool.end(); + } +})().catch((error) => { + console.error(error); + process.exit(1); +}); +NODE +} + create_paid_reimbursement() { DATABASE_URL="$DATABASE_URL" RUN_SUFFIX="$RUN_SUFFIX" node <<'NODE' require('dotenv/config'); @@ -762,7 +845,8 @@ NODE run_outbox_once() { local journal_mode="$1" local procurement_mode="${2:-$journal_mode}" - DATABASE_URL="$DATABASE_URL" INTEGRATION_OUTBOX_BATCH_SIZE=500 ERP_SYNC_ENABLED=true ERP_SYNC_PROVIDER=http ERP_JOURNAL_EXPORT_ENDPOINT="http://127.0.0.1:$MOCK_ERP_PORT/erp/journals/export?mode=$journal_mode" ERP_PROCUREMENT_EXPORT_ENDPOINT="http://127.0.0.1:$MOCK_ERP_PORT/erp/procurement/export?mode=$procurement_mode" ERP_SYNC_ENDPOINT="http://127.0.0.1:$MOCK_ERP_PORT/erp/reimbursements/paid?mode=$journal_mode" ERP_SYNC_TIMEOUT_MS="$ERP_SYNC_TIMEOUT_MS" ERP_PROCUREMENT_TIMEOUT_MS="$ERP_SYNC_TIMEOUT_MS" ERP_SYNC_AUTH_TOKEN="" npm run --silent job:integration-outbox-once >"$TMP_DIR/outbox-once.log" + local asset_mode="${3:-$journal_mode}" + DATABASE_URL="$DATABASE_URL" INTEGRATION_OUTBOX_BATCH_SIZE=500 ERP_SYNC_ENABLED=true ERP_SYNC_PROVIDER=http ERP_JOURNAL_EXPORT_ENDPOINT="http://127.0.0.1:$MOCK_ERP_PORT/erp/journals/export?mode=$journal_mode" ERP_PROCUREMENT_EXPORT_ENDPOINT="http://127.0.0.1:$MOCK_ERP_PORT/erp/procurement/export?mode=$procurement_mode" ERP_ASSET_EXPORT_ENDPOINT="http://127.0.0.1:$MOCK_ERP_PORT/erp/assets/export?mode=$asset_mode" ERP_SYNC_ENDPOINT="http://127.0.0.1:$MOCK_ERP_PORT/erp/reimbursements/paid?mode=$journal_mode" ERP_SYNC_TIMEOUT_MS="$ERP_SYNC_TIMEOUT_MS" ERP_PROCUREMENT_TIMEOUT_MS="$ERP_SYNC_TIMEOUT_MS" ERP_ASSET_TIMEOUT_MS="$ERP_SYNC_TIMEOUT_MS" ERP_SYNC_AUTH_TOKEN="" npm run --silent job:integration-outbox-once >"$TMP_DIR/outbox-once.log" } assert_pull_mode() { @@ -863,6 +947,92 @@ archive_purchase_order() { jq -er '.data.archiveOrder.id' "$TMP_DIR/${tmp_prefix}-archive.json" } +resolve_asset_fixture_ids() { + DATABASE_URL="$DATABASE_URL" LEGAL_ENTITY_CODE="$LEGAL_ENTITY_CODE" COST_CENTER_CODE="$COST_CENTER_CODE" node - <<'NODE' +require('dotenv/config'); +const { createPrismaClient } = require('./src/prismaClient'); + +(async () => { + const { prisma, pool } = createPrismaClient(); + await prisma.$connect(); + try { + const employee = await prisma.employee.findUnique({ + where: { email: process.env.SEED_EMPLOYEE_EMAIL ?? 'employee@hyinda.com' }, + select: { id: true, legal_entity_id: true }, + }); + const primaryCostCenter = await prisma.costCenter.findFirst({ + where: { + enabled: true, + archived_at: null, + legal_entity_id: employee.legal_entity_id, + }, + orderBy: { created_at: 'asc' }, + select: { id: true }, + }); + const syncedLegalEntity = await prisma.legalEntity.findUnique({ + where: { code: process.env.LEGAL_ENTITY_CODE }, + select: { id: true }, + }); + const syncedCostCenter = await prisma.costCenter.findFirst({ + where: { + code: process.env.COST_CENTER_CODE, + legal_entity_id: syncedLegalEntity?.id ?? undefined, + }, + select: { id: true }, + }); + + process.stdout.write([ + employee.id, + employee.legal_entity_id, + primaryCostCenter?.id ?? '', + syncedLegalEntity?.id ?? '', + syncedCostCenter?.id ?? '', + ].join('\n')); + } finally { + await prisma.$disconnect(); + await pool.end(); + } +})().catch((error) => { + console.error(error); + process.exit(1); +}); +NODE +} + +create_asset_category() { + local token="$1" + local tmp_prefix="$2" + local category_code="AST-${RUN_SUFFIX_NUMERIC:0:8}-${tmp_prefix}" + local status + status="$(api_json_auth POST "/api/asset-categories" "$token" "$TMP_DIR/${tmp_prefix}-asset-category.json" "$(jq -nc --arg code "$category_code" --arg name "资产分类-${tmp_prefix}" '{code:$code, name:$name, enabled:true}')")" + assert_status "$status" "200" "create asset category ${tmp_prefix}" "$TMP_DIR/${tmp_prefix}-asset-category.json" + jq -er '.data.assetCategory.id' "$TMP_DIR/${tmp_prefix}-asset-category.json" +} + +create_asset_card() { + local token="$1" + local category_id="$2" + local legal_entity_id="$3" + local cost_center_id="$4" + local assignee_employee_id="$5" + local tmp_prefix="$6" + + local body + body="$(jq -nc \ + --arg name "资产ERP-${tmp_prefix}" \ + --arg categoryId "$category_id" \ + --arg legalEntityId "$legal_entity_id" \ + --arg costCenterId "$cost_center_id" \ + --arg assigneeEmployeeId "$assignee_employee_id" \ + --arg location "上海办公室-A区-${tmp_prefix}" \ + '{name:$name, categoryId:$categoryId, legalEntityId:$legalEntityId, costCenterId:$costCenterId, assigneeEmployeeId:($assigneeEmployeeId|select(length>0)), location:$location, activatedAt:"2026-03-09"}')" + + local status + status="$(api_json_auth POST "/api/assets" "$token" "$TMP_DIR/${tmp_prefix}-asset-create.json" "$body")" + assert_status "$status" "200" "create asset ${tmp_prefix}" "$TMP_DIR/${tmp_prefix}-asset-create.json" + jq -er '.data.asset.id' "$TMP_DIR/${tmp_prefix}-asset-create.json" +} + assert_export_mode_semantics() { local mode="$1" local state_json="$2" @@ -935,6 +1105,9 @@ run_export_mode_case() { echo "[0/9] Ensure ERP feature flag force_on" set_erp_flag "force_on" +echo "[0.5/9] Reset ERP integration artifacts" +reset_erp_integration_state + echo "[1/9] Start embedded mock ERP server" start_mock_erp_server @@ -1013,6 +1186,13 @@ SUPPLIER_ID="${FIXTURE_IDS[2]}" DOCUMENT_CATEGORY_ID="${FIXTURE_IDS[3]}" BUDGET_POOL_ID="${FIXTURE_IDS[4]}" +PROC_SUCCESS_PO="PO-P19ERP2-${RUN_SUFFIX_NUMERIC}-001" +PROC_SUCCESS_CONTRACT_1="CT-P19ERP2-${RUN_SUFFIX_NUMERIC}-001" +PROC_SUCCESS_CONTRACT_2="CT-P19ERP2-${RUN_SUFFIX_NUMERIC}-002" +PROC_SUCCESS_CONTRACT_3="CT-P19ERP2-${RUN_SUFFIX_NUMERIC}-003" +PROC_SKIPPED_PO="PO-P19ERP2-${RUN_SUFFIX_NUMERIC}-101" +PROC_SKIPPED_CONTRACT="CT-P19ERP2-${RUN_SUFFIX_NUMERIC}-101" + mapfile -t PROCUREMENT_APPROVED < <(create_and_approve_purchase "$EMPLOYEE_TOKEN" "$MANAGER_TOKEN" "procurement erp success" "proc-success") PURCHASE_ID_APPROVED="${PROCUREMENT_APPROVED[0]}" COMMITMENT_DEDUPE="purchase_request:${PURCHASE_ID_APPROVED}:commitment" @@ -1024,7 +1204,7 @@ jq -e '.status == "succeeded"' <<<"$commitment_state_done" >/dev/null commitment_binding="$(query_binding_by_key 'purchase_request_commitment_journal' "$PURCHASE_ID_APPROVED")" jq -e '.externalCode != null' <<<"$commitment_binding" >/dev/null -ARCHIVE_ID_APPROVED="$(archive_purchase_order "$FINANCE_MGR_TOKEN" "$PURCHASE_ID_APPROVED" 'PO-P19ERP2-001' 'CT-P19ERP2-001' 'proc-success')" +ARCHIVE_ID_APPROVED="$(archive_purchase_order "$FINANCE_MGR_TOKEN" "$PURCHASE_ID_APPROVED" "$PROC_SUCCESS_PO" "$PROC_SUCCESS_CONTRACT_1" 'proc-success')" ACCRUAL_DEDUPE="purchase_order_archive:${ARCHIVE_ID_APPROVED}:accrual" SUMMARY_DEDUPE="purchase_order_archive:${ARCHIVE_ID_APPROVED}:summary" accrual_state="$(query_outbox_by_key 'erp.journal.export' "$ACCRUAL_DEDUPE")" @@ -1044,14 +1224,14 @@ jq -e '.externalCode != null' <<<"$po_binding" >/dev/null jq -e '.externalCode != null' <<<"$contract_binding" >/dev/null echo "[10/13] Archive update only resets summary" -ARCHIVE_UPDATE_STATUS="$(api_json_auth POST "/api/purchase-requests/$PURCHASE_ID_APPROVED/archive-order" "$FINANCE_MGR_TOKEN" "$TMP_DIR/proc-success-archive-update.json" '{"poNo":"PO-P19ERP2-001","contractNo":"CT-P19ERP2-002","archivedFiles":[]}')" +ARCHIVE_UPDATE_STATUS="$(api_json_auth POST "/api/purchase-requests/$PURCHASE_ID_APPROVED/archive-order" "$FINANCE_MGR_TOKEN" "$TMP_DIR/proc-success-archive-update.json" "$(jq -nc --arg po "$PROC_SUCCESS_PO" --arg contract "$PROC_SUCCESS_CONTRACT_2" '{poNo:$po, contractNo:$contract, archivedFiles:[]}')")" assert_status "$ARCHIVE_UPDATE_STATUS" "200" "archive update" "$TMP_DIR/proc-success-archive-update.json" accrual_state_after_update="$(query_outbox_by_key 'erp.journal.export' "$ACCRUAL_DEDUPE")" summary_state_after_update="$(query_outbox_by_key 'erp.procurement.export' "$SUMMARY_DEDUPE")" ACCRUAL_OUTBOX_ID="$(jq -er '.id' <<<"$accrual_state_done")" SUMMARY_OUTBOX_ID="$(jq -er '.id' <<<"$summary_state_done")" jq -e --arg id "$ACCRUAL_OUTBOX_ID" '.id == $id and .status == "succeeded"' <<<"$accrual_state_after_update" >/dev/null -jq -e --arg id "$SUMMARY_OUTBOX_ID" '.id == $id and .status == "pending" and .payload.contractNo == "CT-P19ERP2-002"' <<<"$summary_state_after_update" >/dev/null +jq -e --arg id "$SUMMARY_OUTBOX_ID" --arg contract "$PROC_SUCCESS_CONTRACT_2" '.id == $id and .status == "pending" and .payload.contractNo == $contract' <<<"$summary_state_after_update" >/dev/null run_outbox_once success success contract_binding_updated="$(query_binding_by_key 'purchase_order_contract' "$ARCHIVE_ID_APPROVED")" jq -e '.externalCode != null' <<<"$contract_binding_updated" >/dev/null @@ -1062,7 +1242,7 @@ mapfile -t PROCUREMENT_SKIPPED < <(create_and_approve_purchase "$EMPLOYEE_TOKEN" PURCHASE_ID_SKIPPED="${PROCUREMENT_SKIPPED[0]}" COMMITMENT_DEDUPE_SKIPPED="purchase_request:${PURCHASE_ID_SKIPPED}:commitment" assert_outbox_absent_by_key 'erp.journal.export' "$COMMITMENT_DEDUPE_SKIPPED" -ARCHIVE_ID_SKIPPED="$(archive_purchase_order "$FINANCE_MGR_TOKEN" "$PURCHASE_ID_SKIPPED" 'PO-P19ERP2-101' 'CT-P19ERP2-101' 'proc-skipped')" +ARCHIVE_ID_SKIPPED="$(archive_purchase_order "$FINANCE_MGR_TOKEN" "$PURCHASE_ID_SKIPPED" "$PROC_SKIPPED_PO" "$PROC_SKIPPED_CONTRACT" 'proc-skipped')" ACCRUAL_DEDUPE_SKIPPED="purchase_order_archive:${ARCHIVE_ID_SKIPPED}:accrual" SUMMARY_DEDUPE_SKIPPED="purchase_order_archive:${ARCHIVE_ID_SKIPPED}:summary" assert_outbox_absent_by_key 'erp.journal.export' "$ACCRUAL_DEDUPE_SKIPPED" @@ -1077,7 +1257,7 @@ jq -e '.status == "succeeded"' <<<"$(query_outbox_by_key 'erp.journal.export' "$ jq -e '.status == "succeeded"' <<<"$(query_outbox_by_key 'erp.procurement.export' "$SUMMARY_DEDUPE_SKIPPED")" >/dev/null echo "[12/13] Archive summary dead-letter can requeue same outbox" -PROC_SUMMARY_FAIL_STATUS="$(api_json_auth POST "/api/purchase-requests/$PURCHASE_ID_APPROVED/archive-order" "$FINANCE_MGR_TOKEN" "$TMP_DIR/proc-summary-fail-archive.json" '{"poNo":"PO-P19ERP2-001","contractNo":"CT-P19ERP2-003","archivedFiles":[]}')" +PROC_SUMMARY_FAIL_STATUS="$(api_json_auth POST "/api/purchase-requests/$PURCHASE_ID_APPROVED/archive-order" "$FINANCE_MGR_TOKEN" "$TMP_DIR/proc-summary-fail-archive.json" "$(jq -nc --arg po "$PROC_SUCCESS_PO" --arg contract "$PROC_SUCCESS_CONTRACT_3" '{poNo:$po, contractNo:$contract, archivedFiles:[]}')")" assert_status "$PROC_SUMMARY_FAIL_STATUS" "200" "archive update for summary failure" "$TMP_DIR/proc-summary-fail-archive.json" run_outbox_once success client summary_dead_state="$(query_outbox_by_key 'erp.procurement.export' "$SUMMARY_DEDUPE")" @@ -1090,7 +1270,79 @@ run_outbox_once success success summary_requeue_state="$(query_outbox_by_key 'erp.procurement.export' "$SUMMARY_DEDUPE")" jq -e --arg id "$SUMMARY_DEAD_ID" '.id == $id and .status == "succeeded"' <<<"$summary_requeue_state" >/dev/null - echo "[13/13] ERP env gate off blocks sync" +echo "[13/18] Asset ledger summary auto export + binding" +mapfile -t ASSET_FIXTURE_IDS < <(resolve_asset_fixture_ids) +ASSET_ASSIGNEE_ID="${ASSET_FIXTURE_IDS[0]}" +ASSET_BASE_LEGAL_ENTITY_ID="${ASSET_FIXTURE_IDS[1]}" +ASSET_BASE_COST_CENTER_ID="${ASSET_FIXTURE_IDS[2]}" +ASSET_SYNCED_LEGAL_ENTITY_ID="${ASSET_FIXTURE_IDS[3]}" +ASSET_SYNCED_COST_CENTER_ID="${ASSET_FIXTURE_IDS[4]}" +ASSET_CATEGORY_ID="$(create_asset_category "$FINANCE_MGR_TOKEN" 'asset-success')" +ASSET_ID_APPROVED="$(create_asset_card "$FINANCE_MGR_TOKEN" "$ASSET_CATEGORY_ID" "$ASSET_BASE_LEGAL_ENTITY_ID" "$ASSET_BASE_COST_CENTER_ID" "$ASSET_ASSIGNEE_ID" 'asset-success')" +ASSET_DEDUPE="asset_card:${ASSET_ID_APPROVED}:ledger_summary" +asset_state_created="$(query_outbox_by_key 'erp.asset.export' "$ASSET_DEDUPE")" +jq -e '.status == "pending" and .payload.exportType == "asset_ledger_summary" and .payload.changeType == "created"' <<<"$asset_state_created" >/dev/null +run_outbox_once success success success +asset_state_done="$(query_outbox_by_key 'erp.asset.export' "$ASSET_DEDUPE")" +jq -e '.status == "succeeded"' <<<"$asset_state_done" >/dev/null +asset_binding="$(query_binding_by_key 'asset_erp_card' "$ASSET_ID_APPROVED")" +jq -e '.externalCode != null and .lastSyncedAt != null' <<<"$asset_binding" >/dev/null +ASSET_OUTBOX_ID="$(jq -er '.id' <<<"$asset_state_done")" +ASSET_TRACK_EMPLOYEE_CODE="$(api_json_auth GET "/api/integrations/erp/assets/$ASSET_ID_APPROVED/track" "$EMPLOYEE_TOKEN" "$TMP_DIR/asset-track-employee.json")" +assert_status "$ASSET_TRACK_EMPLOYEE_CODE" "200" "employee asset erp track" "$TMP_DIR/asset-track-employee.json" +jq -e '.data.stage.deliveryStatus == "succeeded" and .data.stage.binding.externalCode == null and .data.permissions.canRetry == false' "$TMP_DIR/asset-track-employee.json" >/dev/null +ASSET_REQUEUE_FORBIDDEN_CODE="$(api_json_auth POST "/api/integrations/erp/assets/$ASSET_ID_APPROVED/requeue?runNow=0" "$EMPLOYEE_TOKEN" "$TMP_DIR/asset-requeue-forbidden.json" '{}')" +assert_status "$ASSET_REQUEUE_FORBIDDEN_CODE" "403" "asset requeue forbidden" "$TMP_DIR/asset-requeue-forbidden.json" + +echo "[14/18] Non-financial patch does not reset asset outbox" +ASSET_PATCH_NON_FIN_CODE="$(api_json_auth PATCH "/api/assets/$ASSET_ID_APPROVED" "$FINANCE_MGR_TOKEN" "$TMP_DIR/asset-non-financial.json" '{"name":"资产ERP-asset-success-v2","location":"上海办公室-B区"}')" +assert_status "$ASSET_PATCH_NON_FIN_CODE" "200" "asset non financial patch" "$TMP_DIR/asset-non-financial.json" +asset_state_after_non_fin="$(query_outbox_by_key 'erp.asset.export' "$ASSET_DEDUPE")" +jq -e --arg id "$ASSET_OUTBOX_ID" '.id == $id and .status == "succeeded"' <<<"$asset_state_after_non_fin" >/dev/null + +echo "[15/18] Financial patch resets same asset outbox" +ASSET_PATCH_FIN_CODE="$(api_json_auth PATCH "/api/assets/$ASSET_ID_APPROVED" "$FINANCE_MGR_TOKEN" "$TMP_DIR/asset-financial.json" "$(jq -nc --arg le "$ASSET_SYNCED_LEGAL_ENTITY_ID" --arg cc "$ASSET_SYNCED_COST_CENTER_ID" '{legalEntityId:$le, costCenterId:$cc}')")" +assert_status "$ASSET_PATCH_FIN_CODE" "200" "asset financial patch" "$TMP_DIR/asset-financial.json" +asset_state_after_fin="$(query_outbox_by_key 'erp.asset.export' "$ASSET_DEDUPE")" +jq -e --arg id "$ASSET_OUTBOX_ID" --arg le "$ASSET_SYNCED_LEGAL_ENTITY_ID" --arg cc "$ASSET_SYNCED_COST_CENTER_ID" '.id == $id and .status == "pending" and .payload.changeType == "financial_dimensions_changed" and .payload.legalEntity.id == $le and .payload.costCenter.id == $cc' <<<"$asset_state_after_fin" >/dev/null +run_outbox_once success success success +asset_state_after_fin_done="$(query_outbox_by_key 'erp.asset.export' "$ASSET_DEDUPE")" +jq -e '.status == "succeeded"' <<<"$asset_state_after_fin_done" >/dev/null + +echo "[16/18] Retired asset dead-letter can requeue same outbox" +ASSET_PATCH_RETIRED_CODE="$(api_json_auth PATCH "/api/assets/$ASSET_ID_APPROVED" "$FINANCE_MGR_TOKEN" "$TMP_DIR/asset-retired.json" '{"status":"retired","retiredAt":"2026-03-09"}')" +assert_status "$ASSET_PATCH_RETIRED_CODE" "200" "asset retired patch" "$TMP_DIR/asset-retired.json" +asset_state_retired_pending="$(query_outbox_by_key 'erp.asset.export' "$ASSET_DEDUPE")" +jq -e --arg id "$ASSET_OUTBOX_ID" '.id == $id and .status == "pending" and .payload.changeType == "retired"' <<<"$asset_state_retired_pending" >/dev/null +run_outbox_once success success client +asset_state_dead="$(query_outbox_by_key 'erp.asset.export' "$ASSET_DEDUPE")" +jq -e '.status == "dead_letter" and .attempts == 1 and (.lastError | tostring | test("erp_asset_http_4xx|422"))' <<<"$asset_state_dead" >/dev/null +ASSET_DEAD_OUTBOX_ID="$(jq -er '.id' <<<"$asset_state_dead")" +ASSET_REQUEUE_CODE="$(api_json_auth POST "/api/integrations/erp/assets/$ASSET_ID_APPROVED/requeue?runNow=0" "$FINANCE_MGR_TOKEN" "$TMP_DIR/asset-requeue.json" '{}')" +assert_status "$ASSET_REQUEUE_CODE" "200" "asset requeue" "$TMP_DIR/asset-requeue.json" +jq -e --arg id "$ASSET_DEAD_OUTBOX_ID" '.data.outboxId == $id and .data.requeued == true' "$TMP_DIR/asset-requeue.json" >/dev/null +run_outbox_once success success success +asset_state_requeued_done="$(query_outbox_by_key 'erp.asset.export' "$ASSET_DEDUPE")" +jq -e --arg id "$ASSET_DEAD_OUTBOX_ID" '.id == $id and .status == "succeeded"' <<<"$asset_state_requeued_done" >/dev/null + +echo "[17/18] Force-off skips asset auto export and manual requeue backfills historical资产" +set_erp_flag "force_off" +ASSET_CATEGORY_SKIPPED_ID="$(create_asset_category "$FINANCE_MGR_TOKEN" 'asset-skipped')" +ASSET_ID_SKIPPED="$(create_asset_card "$FINANCE_MGR_TOKEN" "$ASSET_CATEGORY_SKIPPED_ID" "$ASSET_BASE_LEGAL_ENTITY_ID" "$ASSET_BASE_COST_CENTER_ID" "$ASSET_ASSIGNEE_ID" 'asset-skipped')" +ASSET_DEDUPE_SKIPPED="asset_card:${ASSET_ID_SKIPPED}:ledger_summary" +assert_outbox_absent_by_key 'erp.asset.export' "$ASSET_DEDUPE_SKIPPED" +ASSET_PATCH_SKIPPED_CODE="$(api_json_auth PATCH "/api/assets/$ASSET_ID_SKIPPED" "$FINANCE_MGR_TOKEN" "$TMP_DIR/asset-skipped-patch.json" "$(jq -nc --arg le "$ASSET_SYNCED_LEGAL_ENTITY_ID" --arg cc "$ASSET_SYNCED_COST_CENTER_ID" '{legalEntityId:$le, costCenterId:$cc}')")" +assert_status "$ASSET_PATCH_SKIPPED_CODE" "200" "asset skipped patch" "$TMP_DIR/asset-skipped-patch.json" +assert_outbox_absent_by_key 'erp.asset.export' "$ASSET_DEDUPE_SKIPPED" +set_erp_flag "force_on" +ASSET_BACKFILL_CODE="$(api_json_auth POST "/api/integrations/erp/assets/$ASSET_ID_SKIPPED/requeue?runNow=0" "$FINANCE_MGR_TOKEN" "$TMP_DIR/asset-backfill.json" '{}')" +assert_status "$ASSET_BACKFILL_CODE" "200" "asset backfill" "$TMP_DIR/asset-backfill.json" +jq -e '.data.duplicated == false and .data.status == "pending"' "$TMP_DIR/asset-backfill.json" >/dev/null +run_outbox_once success success success +asset_backfill_state="$(query_outbox_by_key 'erp.asset.export' "$ASSET_DEDUPE_SKIPPED")" +jq -e '.status == "succeeded"' <<<"$asset_backfill_state" >/dev/null + + echo "[18/18] ERP env gate off blocks sync" if kill -0 "$APP_SERVER_PID" >/dev/null 2>&1; then kill "$APP_SERVER_PID" >/dev/null 2>&1 || true wait "$APP_SERVER_PID" >/dev/null 2>&1 || true @@ -1105,8 +1357,10 @@ ERP_SYNC_PROVIDER=http \ ERP_MASTER_DATA_ENDPOINT="http://127.0.0.1:$MOCK_ERP_PORT/erp/master-data/pull?mode=success" \ ERP_JOURNAL_EXPORT_ENDPOINT="http://127.0.0.1:$MOCK_ERP_PORT/erp/journals/export?mode=success" \ ERP_PROCUREMENT_EXPORT_ENDPOINT="http://127.0.0.1:$MOCK_ERP_PORT/erp/procurement/export?mode=success" \ +ERP_ASSET_EXPORT_ENDPOINT="http://127.0.0.1:$MOCK_ERP_PORT/erp/assets/export?mode=success" \ ERP_SYNC_TIMEOUT_MS="$ERP_SYNC_TIMEOUT_MS" \ ERP_PROCUREMENT_TIMEOUT_MS="$ERP_SYNC_TIMEOUT_MS" \ +ERP_ASSET_TIMEOUT_MS="$ERP_SYNC_TIMEOUT_MS" \ ERP_MASTER_DATA_TIMEOUT_MS="$ERP_SYNC_TIMEOUT_MS" \ ERP_SYNC_AUTH_TOKEN="" \ node src/index.js >"$TMP_DIR/app-gate-off.log" 2>&1 &