From 865acb63aa467eb332381c20b4660301327aeffe Mon Sep 17 00:00:00 2001 From: shunli Date: Sat, 7 Mar 2026 22:33:17 +0800 Subject: [PATCH] =?UTF-8?q?docs/test(P18A-5,P18A-6):=20=E6=94=B6=E5=8F=A3?= =?UTF-8?q?=E8=B5=84=E4=BA=A7=E7=9F=A9=E9=98=B5=E6=96=87=E6=A1=A3=E4=B8=8E?= =?UTF-8?q?=E5=9B=9E=E5=BD=92?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/feature-matrix.md | 14 +- ...7_22-02-09-p18a-l5-asset-registry-core.csv | 16 +- scripts/validate-feature-matrix.py | 1 + server/scripts/regression-all.sh | 21 +- server/scripts/regression-assets.sh | 283 ++++++++++++++++++ .../tests/unit/constants/permissions.test.js | 11 + .../unit/documentSequence.service.test.js | 23 ++ 7 files changed, 348 insertions(+), 21 deletions(-) create mode 100755 server/scripts/regression-assets.sh diff --git a/docs/feature-matrix.md b/docs/feature-matrix.md index a69dcdac..1fd271c6 100644 --- a/docs/feature-matrix.md +++ b/docs/feature-matrix.md @@ -1,8 +1,8 @@ -# hyb-oa 实现状态矩阵(P5-P17B) +# hyb-oa 实现状态矩阵(P5-P18A) **快照日期**:2026-03-07 -**范围**:`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`,共 164 项。 -**事实源**:`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`(执行合同) +**范围**:`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`,共 178 项。 +**事实源**:`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/*.csv` 的执行合同职责。 @@ -197,3 +197,11 @@ | P17B-4 | 前端归档入口与采购E2E | N/A | 2026-03-07_21-00-37-p17b-l5-procurement-budget-archive.csv | 2026-03-07_21-00-37-p17b-l5-procurement-budget-archive.csv | 2026-03-07 | DONE | client:typecheck/build + playwright | `PurchaseRequestsPage` 新增归档区块,采购主链路 E2E 覆盖员工提单、经理审批、财务归档 | | P17B-5 | 安全文档与接口说明 | N/A | 2026-03-07_21-00-37-p17b-l5-procurement-budget-archive.csv | 2026-03-07_21-00-37-p17b-l5-procurement-budget-archive.csv | 2026-03-07 | DONE | node --check + validate-feature-matrix | 采购预算闭环、归档权限边界与矩阵事实源同步完成,主数据读取越权缺口已补齐 | | P17B-6 | P17B回归与安全收口 | N/A | 2026-03-07_21-00-37-p17b-l5-procurement-budget-archive.csv | 2026-03-07_21-00-37-p17b-l5-procurement-budget-archive.csv | 2026-03-07 | DONE | regression-all + client:typecheck/build + playwright | 采购预算闭环、PO归档与全量回归通过,报销/请假/主数据/采购主流程零回归 | + +| P18A-0 | P18A执行合同建档 | N/A | 2026-03-07_22-02-09-p18a-l5-asset-registry-core.csv | 2026-03-07_22-02-09-p18a-l5-asset-registry-core.csv | 2026-03-07 | DONE | CSV 校验 | 固资台账核心执行合同建档并冻结仅做资产分类与资产卡片 | +| P18A-1 | 资产Prisma模型与迁移 | N/A | 2026-03-07_22-02-09-p18a-l5-asset-registry-core.csv | 2026-03-07_22-02-09-p18a-l5-asset-registry-core.csv | 2026-03-07 | DONE | prisma:validate + prisma:migrate + prisma:generate | `AssetCategory`、`AssetCard` 与法体/成本中心/采购来源关系落地,且不破坏既有主流程 | +| P18A-2 | 资产权限与系统种子 | N/A | 2026-03-07_22-02-09-p18a-l5-asset-registry-core.csv | 2026-03-07_22-02-09-p18a-l5-asset-registry-core.csv | 2026-03-07 | DONE | server:test + seed:config | `asset.*` 权限映射与 `ASSET_CARD_DEFAULT` 系统单据分类补齐完成 | +| P18A-3 | 资产分类与卡片后端API | N/A | 2026-03-07_22-02-09-p18a-l5-asset-registry-core.csv | 2026-03-07_22-02-09-p18a-l5-asset-registry-core.csv | 2026-03-07 | DONE | server:test + regression-assets | `/api/asset-categories` 与 `/api/assets` 列表详情创建更新接口可用,采购来源引用校验正确 | +| P18A-4 | 资产台账前端入口与页面 | N/A | 2026-03-07_22-02-09-p18a-l5-asset-registry-core.csv | 2026-03-07_22-02-09-p18a-l5-asset-registry-core.csv | 2026-03-07 | DONE | client:typecheck/build | `AssetRegistryPage`、菜单、路由与资产分类/卡片编辑界面可用 | +| P18A-5 | OpenAPI与矩阵同步 | N/A | 2026-03-07_22-02-09-p18a-l5-asset-registry-core.csv | 2026-03-07_22-02-09-p18a-l5-asset-registry-core.csv | 2026-03-07 | DONE | node --check + validate-feature-matrix | 资产接口文档标签、合同锚点与矩阵事实源同步完成 | +| P18A-6 | P18A回归与验收收口 | N/A | 2026-03-07_22-02-09-p18a-l5-asset-registry-core.csv | 2026-03-07_22-02-09-p18a-l5-asset-registry-core.csv | 2026-03-07 | DONE | regression-all + client:typecheck/build | 资产分类、资产卡片与既有报销/请假/采购/主数据回归全部通过 | diff --git a/issues/2026-03-07_22-02-09-p18a-l5-asset-registry-core.csv b/issues/2026-03-07_22-02-09-p18a-l5-asset-registry-core.csv index e4af6303..8295dea1 100644 --- a/issues/2026-03-07_22-02-09-p18a-l5-asset-registry-core.csv +++ b/issues/2026-03-07_22-02-09-p18a-l5-asset-registry-core.csv @@ -1,8 +1,8 @@ -ID,Title,Description,Acceptance,Test_Method,Tools,Dev_Status,Review1_Status,Regression_Status,Files,Dependencies,Notes -P18A-0,P18A执行合同建档,创建并校验P18A固资台账核心执行合同CSV,CSV通过校验且字段完整,python3 /home/shunli/.codex/skills/plan/scripts/validate_issues_csv.py issues/2026-03-07_22-02-09-p18a-l5-asset-registry-core.csv,manual,TODO,TODO,TODO,issues/2026-03-07_22-02-09-p18a-l5-asset-registry-core.csv,[],P18A仅交付资产分类与资产卡片不进入折旧凭证ERP联动 -P18A-1,资产Prisma模型与迁移,新增AssetCategory与AssetCard并补齐法体成本中心采购来源等关系,Prisma schema与迁移通过校验且不破坏报销请假采购主流程,cd server && npm run prisma:validate && npx prisma migrate dev --name p18a_asset_registry_core && npm run prisma:generate,manual,TODO,TODO,TODO,server/prisma/schema.prisma | server/prisma/migrations | server/src/prismaClient.js,"[""P18A-0""]",asset_no创建时生成 purchase_request_id仅允许引用approved采购单 -P18A-2,资产权限与系统种子,新增asset权限域角色映射并补齐系统单据分类默认配置ASSET_CARD_DEFAULT,默认角色权限与文档分类种子一致且不误开员工管理权限,cd server && npm run test,manual,TODO,TODO,TODO,server/src/constants/permissions.js | server/src/services/documentSequence.js | server/src/scripts/seed-config.js,"[""P18A-1""]",资产域不接workflow employee仅保留view_own -P18A-3,资产分类与卡片后端API,实现资产分类CRUD与资产卡片列表详情创建更新接口并补齐编号生成校验,资产分类和资产卡片接口可用且purchase_request引用校验正确,cd server && npm run test,manual,TODO,TODO,TODO,server/src/routes/assets.js | server/src/app.js | server/tests,"[""P18A-2""]",PATCH不允许修改asset_no与purchase_request_id -P18A-4,资产台账前端入口与页面,新增AssetRegistryPage API封装 菜单与路由并支持分类管理资产列表创建编辑,资产台账页面可用且菜单显隐与asset权限一致,cd client && npm run typecheck && npm run build,manual,TODO,TODO,TODO,client/src/pages/AssetRegistryPage.tsx | client/src/api/assets.ts | client/src/router/index.tsx | client/src/components/navigation/NavLinks.tsx,"[""P18A-3""]",页面包含分类管理区资产列表区创建编辑抽屉 -P18A-5,OpenAPI与矩阵同步,补齐资产接口文档 路由标签 合同锚点并同步Feature Matrix事实源,/api/docs/json可见资产接口且矩阵事实源与合同锚点一致,cd server && npm run test && python3 scripts/validate-feature-matrix.py,manual,TODO,TODO,TODO,server/src/routes/assets.js | docs/feature-matrix.md | issues/2026-03-07_22-02-09-p18a-l5-asset-registry-core.csv,"[""P18A-4""]",只冻结资产台账边界不提前进入流转盘点 -P18A-6,P18A回归与验收收口,执行后端前端回归并确认报销请假主数据采购零回归后再允许进入P18B,资产分类资产卡片可维护且既有报销请假采购主流程零回归,cd server && npm run test && OCR_PROVIDER=mock JOBS_ENABLED=false ./scripts/regression-all.sh && cd ../client && npm run typecheck && npm run build,manual,TODO,TODO,TODO,issues/2026-03-07_22-02-09-p18a-l5-asset-registry-core.csv | server/src/routes/assets.js | client/src/pages/AssetRegistryPage.tsx,"[""P18A-5""]",P18A不做资产审批流不做折旧凭证ERP过账 +ID,Title,Description,Acceptance,Test_Method,Tools,Dev_Status,Review1_Status,Regression_Status,Files,Dependencies,Notes +P18A-0,P18A执行合同建档,创建并校验P18A固资台账核心执行合同CSV,CSV通过校验且字段完整,python3 /home/shunli/.codex/skills/plan/scripts/validate_issues_csv.py issues/2026-03-07_22-02-09-p18a-l5-asset-registry-core.csv,manual,DONE,DONE,DONE,issues/2026-03-07_22-02-09-p18a-l5-asset-registry-core.csv,[],P18A仅交付资产分类与资产卡片不进入折旧凭证ERP联动 +P18A-1,资产Prisma模型与迁移,新增AssetCategory与AssetCard并补齐法体成本中心采购来源等关系,Prisma schema与迁移通过校验且不破坏报销请假采购主流程,cd server && npm run prisma:validate && npx prisma migrate dev --name p18a_asset_registry_core && npm run prisma:generate,manual,DONE,DONE,DONE,server/prisma/schema.prisma | server/prisma/migrations | server/src/prismaClient.js,"[""P18A-0""]",asset_no创建时生成 purchase_request_id仅允许引用approved采购单 +P18A-2,资产权限与系统种子,新增asset权限域角色映射并补齐系统单据分类默认配置ASSET_CARD_DEFAULT,默认角色权限与文档分类种子一致且不误开员工管理权限,cd server && npm run test,manual,DONE,DONE,DONE,server/src/constants/permissions.js | server/src/services/documentSequence.js | server/src/scripts/seed-config.js,"[""P18A-1""]",资产域不接workflow employee仅保留view_own +P18A-3,资产分类与卡片后端API,实现资产分类CRUD与资产卡片列表详情创建更新接口并补齐编号生成校验,资产分类和资产卡片接口可用且purchase_request引用校验正确,cd server && npm run test,manual,DONE,DONE,DONE,server/src/routes/assets.js | server/src/app.js | server/tests,"[""P18A-2""]",PATCH不允许修改asset_no与purchase_request_id +P18A-4,资产台账前端入口与页面,新增AssetRegistryPage API封装 菜单与路由并支持分类管理资产列表创建编辑,资产台账页面可用且菜单显隐与asset权限一致,cd client && npm run typecheck && npm run build,manual,DONE,DONE,DONE,client/src/pages/AssetRegistryPage.tsx | client/src/api/assets.ts | client/src/router/index.tsx | client/src/components/navigation/NavLinks.tsx,"[""P18A-3""]",页面包含分类管理区资产列表区创建编辑抽屉 +P18A-5,OpenAPI与矩阵同步,补齐资产接口文档 路由标签 合同锚点并同步Feature Matrix事实源,/api/docs/json可见资产接口且矩阵事实源与合同锚点一致,cd server && npm run test && python3 scripts/validate-feature-matrix.py,manual,DONE,DONE,DONE,server/src/routes/assets.js | docs/feature-matrix.md | issues/2026-03-07_22-02-09-p18a-l5-asset-registry-core.csv,"[""P18A-4""]",只冻结资产台账边界不提前进入流转盘点 +P18A-6,P18A回归与验收收口,执行后端前端回归并确认报销请假主数据采购零回归后再允许进入P18B,资产分类资产卡片可维护且既有报销请假采购主流程零回归,cd server && npm run test && OCR_PROVIDER=mock JOBS_ENABLED=false ./scripts/regression-all.sh && cd ../client && npm run typecheck && npm run build,manual,DONE,DONE,DONE,issues/2026-03-07_22-02-09-p18a-l5-asset-registry-core.csv | server/src/routes/assets.js | client/src/pages/AssetRegistryPage.tsx,"[""P18A-5""]",P18A不做资产审批流不做折旧凭证ERP过账 diff --git a/scripts/validate-feature-matrix.py b/scripts/validate-feature-matrix.py index 76cf5ad5..c6837f4e 100755 --- a/scripts/validate-feature-matrix.py +++ b/scripts/validate-feature-matrix.py @@ -35,6 +35,7 @@ CSV_FACT_SOURCES = [ ROOT / "issues" / "2026-03-07_18-16-00-g2-l5-cross-domain-bridge.csv", ROOT / "issues" / "2026-03-07_19-33-09-p17a-l5-procurement-core.csv", ROOT / "issues" / "2026-03-07_21-00-37-p17b-l5-procurement-budget-archive.csv", + ROOT / "issues" / "2026-03-07_22-02-09-p18a-l5-asset-registry-core.csv", ] ID_PATTERN = re.compile(r"^[A-Z][0-9][0-9A-Za-z-]*$") diff --git a/server/scripts/regression-all.sh b/server/scripts/regression-all.sh index 8af01ab4..f1e8833e 100755 --- a/server/scripts/regression-all.sh +++ b/server/scripts/regression-all.sh @@ -57,7 +57,7 @@ if [[ -n "$JSON_REPORT_PATH" ]]; then trap rr_cleanup EXIT fi -TOTAL_STEPS=13 +TOTAL_STEPS=14 PASSED_STEPS=0 FAILED_STEPS=0 SKIPPED_STEPS=0 @@ -125,15 +125,16 @@ run_step 1 "smoke" "Smoke Test" "1" '"$SCRIPT_DIR"/smoke-test.sh' run_step 2 "dedup" "Dedup Regression" "1" '"$SCRIPT_DIR"/regression-dedup.sh' run_step 3 "workflow_actions" "Workflow Actions Regression" "1" '"$SCRIPT_DIR"/regression-workflow-actions.sh' run_step 4 "procurement" "Procurement Regression" "1" '"$SCRIPT_DIR"/regression-procurement.sh' -run_step 5 "audit" "Audit Regression" "$RUN_AUDIT_REGRESSION" '"$SCRIPT_DIR"/regression-audit.sh' "RUN_AUDIT_REGRESSION=$RUN_AUDIT_REGRESSION" -run_step 6 "ocr" "OCR Regression" "$RUN_OCR_REGRESSION" '"$SCRIPT_DIR"/regression-ocr.sh' "RUN_OCR_REGRESSION=$RUN_OCR_REGRESSION" -run_step 7 "org_settings" "Org + Settings Regression" "$RUN_ORG_SETTINGS_REGRESSION" '"$SCRIPT_DIR"/regression-org-settings.sh' "RUN_ORG_SETTINGS_REGRESSION=$RUN_ORG_SETTINGS_REGRESSION" -run_step 8 "auth_security" "Auth Security Regression" "$RUN_AUTH_SECURITY_REGRESSION" '"$SCRIPT_DIR"/regression-auth-security.sh' "RUN_AUTH_SECURITY_REGRESSION=$RUN_AUTH_SECURITY_REGRESSION" -run_step 9 "verification" "Verification Regression" "$RUN_VERIFICATION_REGRESSION" '"$SCRIPT_DIR"/regression-verification.sh' "RUN_VERIFICATION_REGRESSION=$RUN_VERIFICATION_REGRESSION" -run_step 10 "dashboard" "Dashboard Regression" "$RUN_DASHBOARD_REGRESSION" '"$SCRIPT_DIR"/regression-dashboard.sh' "RUN_DASHBOARD_REGRESSION=$RUN_DASHBOARD_REGRESSION" -run_step 11 "budget" "Budget Regression" "$RUN_BUDGET_REGRESSION" '"$SCRIPT_DIR"/regression-budget.sh' "RUN_BUDGET_REGRESSION=$RUN_BUDGET_REGRESSION" -run_step 12 "integrations" "Integrations Regression" "$RUN_INTEGRATIONS_REGRESSION" '"$SCRIPT_DIR"/regression-integrations.sh' "RUN_INTEGRATIONS_REGRESSION=$RUN_INTEGRATIONS_REGRESSION" -run_step 13 "feature_flags" "Feature Flags Regression" "$RUN_FEATURE_FLAGS_REGRESSION" '"$SCRIPT_DIR"/regression-feature-flags.sh' "RUN_FEATURE_FLAGS_REGRESSION=$RUN_FEATURE_FLAGS_REGRESSION" +run_step 5 "asset" "Asset Regression" "1" '"$SCRIPT_DIR"/regression-assets.sh' +run_step 6 "audit" "Audit Regression" "$RUN_AUDIT_REGRESSION" '"$SCRIPT_DIR"/regression-audit.sh' "RUN_AUDIT_REGRESSION=$RUN_AUDIT_REGRESSION" +run_step 7 "ocr" "OCR Regression" "$RUN_OCR_REGRESSION" '"$SCRIPT_DIR"/regression-ocr.sh' "RUN_OCR_REGRESSION=$RUN_OCR_REGRESSION" +run_step 8 "org_settings" "Org + Settings Regression" "$RUN_ORG_SETTINGS_REGRESSION" '"$SCRIPT_DIR"/regression-org-settings.sh' "RUN_ORG_SETTINGS_REGRESSION=$RUN_ORG_SETTINGS_REGRESSION" +run_step 9 "auth_security" "Auth Security Regression" "$RUN_AUTH_SECURITY_REGRESSION" '"$SCRIPT_DIR"/regression-auth-security.sh' "RUN_AUTH_SECURITY_REGRESSION=$RUN_AUTH_SECURITY_REGRESSION" +run_step 10 "verification" "Verification Regression" "$RUN_VERIFICATION_REGRESSION" '"$SCRIPT_DIR"/regression-verification.sh' "RUN_VERIFICATION_REGRESSION=$RUN_VERIFICATION_REGRESSION" +run_step 11 "dashboard" "Dashboard Regression" "$RUN_DASHBOARD_REGRESSION" '"$SCRIPT_DIR"/regression-dashboard.sh' "RUN_DASHBOARD_REGRESSION=$RUN_DASHBOARD_REGRESSION" +run_step 12 "budget" "Budget Regression" "$RUN_BUDGET_REGRESSION" '"$SCRIPT_DIR"/regression-budget.sh' "RUN_BUDGET_REGRESSION=$RUN_BUDGET_REGRESSION" +run_step 13 "integrations" "Integrations Regression" "$RUN_INTEGRATIONS_REGRESSION" '"$SCRIPT_DIR"/regression-integrations.sh' "RUN_INTEGRATIONS_REGRESSION=$RUN_INTEGRATIONS_REGRESSION" +run_step 14 "feature_flags" "Feature Flags Regression" "$RUN_FEATURE_FLAGS_REGRESSION" '"$SCRIPT_DIR"/regression-feature-flags.sh' "RUN_FEATURE_FLAGS_REGRESSION=$RUN_FEATURE_FLAGS_REGRESSION" RUN_ENDED_MS="$(rr_now_ms)" OVERALL_DURATION_MS="$((RUN_ENDED_MS - RUN_STARTED_MS))" diff --git a/server/scripts/regression-assets.sh b/server/scripts/regression-assets.sh new file mode 100755 index 00000000..6f912df5 --- /dev/null +++ b/server/scripts/regression-assets.sh @@ -0,0 +1,283 @@ +#!/usr/bin/env bash +set -euo pipefail + +BASE_URL="${BASE_URL:-http://127.0.0.1:3000}" +DEVICE_FINGERPRINT="${DEVICE_FINGERPRINT:-regression-assets}" +EMPLOYEE_IDENTIFIER="${EMPLOYEE_IDENTIFIER:-employee@hyinda.com}" +EMPLOYEE_PASSWORD="${EMPLOYEE_PASSWORD:-EmployeePassword1234}" +FINANCE_MGR_IDENTIFIER="${FINANCE_MGR_IDENTIFIER:-finance_mgr@hyinda.com}" +FINANCE_MGR_PASSWORD="${FINANCE_MGR_PASSWORD:-FinanceMgrPassword1234}" + +if ! command -v curl >/dev/null 2>&1; then + echo "[ERROR] curl not found" >&2 + exit 1 +fi + +if ! command -v jq >/dev/null 2>&1; then + echo "[ERROR] jq not found" >&2 + exit 1 +fi + +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +cd "$ROOT_DIR" + +if [[ -z "${DATABASE_URL:-}" && -f ".env" ]]; then + set -a + # shellcheck disable=SC1091 + source .env + set +a +fi + +if [[ -z "${DATABASE_URL:-}" ]]; then + echo "[ERROR] DATABASE_URL is required (export it or provide server/.env)" >&2 + exit 1 +fi + +TMP_DIR="$(mktemp -d)" +cleanup() { + rm -rf "$TMP_DIR" +} +trap cleanup EXIT + +assert_status() { + local actual="$1" + local expected="$2" + local name="$3" + local body_file="${4:-}" + + if [[ "$actual" != "$expected" ]]; then + echo "[ERROR] $name expected HTTP $expected but got $actual" >&2 + [[ -n "$body_file" && -f "$body_file" ]] && cat "$body_file" >&2 + exit 1 + fi +} + +assert_jq() { + local file_path="$1" + local jq_expr="$2" + local name="$3" + if ! jq -e "$jq_expr" "$file_path" >/dev/null; then + echo "[ERROR] $name assertion failed: $jq_expr" >&2 + cat "$file_path" >&2 + exit 1 + fi +} + +login_user() { + local identifier="$1" + local password="$2" + local out_file="$3" + + local status + status="$(curl -sS -o "$out_file" -w "%{http_code}" \ + -X POST "$BASE_URL/api/auth/login" \ + -H "content-type: application/json" \ + -H "x-device-fingerprint: $DEVICE_FINGERPRINT" \ + -d "{\"identifier\":\"$identifier\",\"password\":\"$password\"}")" + assert_status "$status" "200" "login $identifier" "$out_file" + jq -er '.data.accessToken' "$out_file" +} + +api_json_auth() { + local method="$1" + local url="$2" + local token="$3" + local out_file="$4" + local body="${5:-}" + + if [[ -n "$body" ]]; then + curl -sS -o "$out_file" -w "%{http_code}" \ + -X "$method" "$url" \ + -H "authorization: Bearer $token" \ + -H "content-type: application/json" \ + -d "$body" + else + curl -sS -o "$out_file" -w "%{http_code}" \ + -X "$method" "$url" \ + -H "authorization: Bearer $token" + fi +} + +resolve_fixture_ids() { + DATABASE_URL="$DATABASE_URL" node - <<'NODE' +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 costCenter = await prisma.costCenter.findFirst({ + where: { + legal_entity_id: employee.legal_entity_id, + enabled: true, + archived_at: null, + }, + orderBy: { created_at: 'asc' }, + select: { id: true }, + }); + const documentCategory = await prisma.documentCategory.findFirst({ + where: { + entity_type: 'asset_card', + enabled: true, + archived_at: null, + code: 'ASSET_CARD_DEFAULT', + }, + orderBy: { created_at: 'asc' }, + select: { id: true }, + }) ?? await prisma.documentCategory.findFirst({ + where: { + entity_type: 'asset_card', + enabled: true, + archived_at: null, + }, + orderBy: { created_at: 'asc' }, + select: { id: true }, + }); + const approvedPurchase = await prisma.purchaseRequest.findFirst({ + where: { status: 'approved' }, + orderBy: { updated_at: 'desc' }, + select: { id: true }, + }); + + process.stdout.write([ + employee.id, + employee.legal_entity_id ?? '', + costCenter?.id ?? '', + documentCategory?.id ?? '', + approvedPurchase?.id ?? '', + ].join('\n')); + } finally { + await prisma.$disconnect(); + await pool.end(); + } +})().catch((error) => { + console.error(error); + process.exit(1); +}); +NODE +} + +assert_asset_state() { + local asset_id="$1" + local category_id="$2" + local employee_id="$3" + local purchase_request_id="$4" + + DATABASE_URL="$DATABASE_URL" node - "$asset_id" "$category_id" "$employee_id" "$purchase_request_id" <<'NODE' +const [assetId, categoryId, employeeId, purchaseRequestId] = process.argv.slice(2); +const { createPrismaClient } = require('./src/prismaClient'); + +(async () => { + const { prisma, pool } = createPrismaClient(); + await prisma.$connect(); + try { + const category = await prisma.assetCategory.findUnique({ + where: { id: categoryId }, + select: { name: true, document_category: { select: { code: true } } }, + }); + const asset = await prisma.assetCard.findUnique({ + where: { id: assetId }, + select: { + asset_no: true, + status: true, + assignee_employee_id: true, + purchase_request_id: true, + location: true, + }, + }); + + if (!category || category.name !== '回归资产分类-已更新' || category.document_category?.code !== 'ASSET_CARD_DEFAULT') { + console.error('[ERROR] asset category state mismatch', category); + process.exit(1); + } + + if (!asset || !asset.asset_no.startsWith('ZC-') || asset.status !== 'in_use' || asset.assignee_employee_id !== employeeId) { + console.error('[ERROR] asset card state mismatch', asset); + process.exit(1); + } + + if (purchaseRequestId && asset.purchase_request_id !== purchaseRequestId) { + console.error('[ERROR] asset purchase source mismatch', asset); + process.exit(1); + } + + if (asset.location !== '深圳办公室 A-01') { + console.error('[ERROR] asset location mismatch', asset); + process.exit(1); + } + } finally { + await prisma.$disconnect(); + await pool.end(); + } +})().catch((error) => { + console.error(error); + process.exit(1); +}); +NODE +} + +echo "[1/8] login finance manager and employee" +FINANCE_MGR_TOKEN="$(login_user "$FINANCE_MGR_IDENTIFIER" "$FINANCE_MGR_PASSWORD" "$TMP_DIR/finance-mgr-login.json")" +EMPLOYEE_TOKEN="$(login_user "$EMPLOYEE_IDENTIFIER" "$EMPLOYEE_PASSWORD" "$TMP_DIR/employee-login.json")" + +mapfile -t FIXTURE_IDS < <(resolve_fixture_ids) +EMPLOYEE_ID="${FIXTURE_IDS[0]}" +LEGAL_ENTITY_ID="${FIXTURE_IDS[1]}" +COST_CENTER_ID="${FIXTURE_IDS[2]}" +DOCUMENT_CATEGORY_ID="${FIXTURE_IDS[3]}" +APPROVED_PURCHASE_ID="${FIXTURE_IDS[4]}" + +if [[ -z "$LEGAL_ENTITY_ID" || -z "$COST_CENTER_ID" || -z "$DOCUMENT_CATEGORY_ID" ]]; then + echo "[ERROR] missing asset fixtures" >&2 + exit 1 +fi + +echo "[2/8] create asset category" +CATEGORY_CODE="ASSET_REG_$(date +%s)" +CREATE_CATEGORY_BODY="$(jq -nc --arg code "$CATEGORY_CODE" --arg dc "$DOCUMENT_CATEGORY_ID" '{code:$code,name:"回归资产分类",documentCategoryId:$dc,enabled:true}')" +create_category_status="$(api_json_auth POST "$BASE_URL/api/asset-categories" "$FINANCE_MGR_TOKEN" "$TMP_DIR/create-category.json" "$CREATE_CATEGORY_BODY")" +assert_status "$create_category_status" "200" "create asset category" "$TMP_DIR/create-category.json" +CATEGORY_ID="$(jq -er '.data.assetCategory.id' "$TMP_DIR/create-category.json")" +assert_jq "$TMP_DIR/create-category.json" '.data.assetCategory.documentCategory.code == "ASSET_CARD_DEFAULT"' "asset category document category" + +echo "[3/8] update asset category" +PATCH_CATEGORY_BODY='{"name":"回归资产分类-已更新","enabled":true}' +patch_category_status="$(api_json_auth PATCH "$BASE_URL/api/asset-categories/$CATEGORY_ID" "$FINANCE_MGR_TOKEN" "$TMP_DIR/patch-category.json" "$PATCH_CATEGORY_BODY")" +assert_status "$patch_category_status" "200" "patch asset category" "$TMP_DIR/patch-category.json" +assert_jq "$TMP_DIR/patch-category.json" '.data.assetCategory.name == "回归资产分类-已更新"' "patched asset category name" + +echo "[4/8] create asset card with approved purchase source" +CREATE_ASSET_BODY="$(jq -nc --arg name "回归资产卡片" --arg categoryId "$CATEGORY_ID" --arg legalEntityId "$LEGAL_ENTITY_ID" --arg costCenterId "$COST_CENTER_ID" --arg purchaseRequestId "$APPROVED_PURCHASE_ID" --arg assigneeEmployeeId "$EMPLOYEE_ID" '{name:$name,categoryId:$categoryId,legalEntityId:$legalEntityId,costCenterId:$costCenterId,purchaseRequestId:($purchaseRequestId|select(length>0)),status:"in_stock",assigneeEmployeeId:$assigneeEmployeeId}')" +create_asset_status="$(api_json_auth POST "$BASE_URL/api/assets" "$FINANCE_MGR_TOKEN" "$TMP_DIR/create-asset.json" "$CREATE_ASSET_BODY")" +assert_status "$create_asset_status" "200" "create asset" "$TMP_DIR/create-asset.json" +ASSET_ID="$(jq -er '.data.asset.id' "$TMP_DIR/create-asset.json")" +assert_jq "$TMP_DIR/create-asset.json" '.data.asset.assetNo | test("^ZC-")' "asset no generated" +assert_jq "$TMP_DIR/create-asset.json" '.data.asset.assignee.employeeNo == "EMP001" or .data.asset.assignee.name == "Employee"' "asset assignee set" + +echo "[5/8] employee can only see own assigned asset" +list_own_status="$(api_json_auth GET "$BASE_URL/api/assets?mine=1" "$EMPLOYEE_TOKEN" "$TMP_DIR/list-own.json")" +assert_status "$list_own_status" "200" "employee list own assets" "$TMP_DIR/list-own.json" +assert_jq "$TMP_DIR/list-own.json" ".data.mine == true and (.data.items | map(.id) | index(\"$ASSET_ID\") != null)" "employee owns asset" + +echo "[6/8] finance manager updates asset status and location" +PATCH_ASSET_BODY='{"status":"in_use","location":"深圳办公室 A-01"}' +patch_asset_status="$(api_json_auth PATCH "$BASE_URL/api/assets/$ASSET_ID" "$FINANCE_MGR_TOKEN" "$TMP_DIR/patch-asset.json" "$PATCH_ASSET_BODY")" +assert_status "$patch_asset_status" "200" "patch asset" "$TMP_DIR/patch-asset.json" +assert_jq "$TMP_DIR/patch-asset.json" '.data.asset.status == "in_use" and .data.asset.location == "深圳办公室 A-01"' "asset status/location updated" + +echo "[7/8] purchase source is immutable after create" +INVALID_PATCH_BODY="$(jq -nc --arg purchaseRequestId "$APPROVED_PURCHASE_ID" '{purchaseRequestId:$purchaseRequestId}')" +invalid_patch_status="$(api_json_auth PATCH "$BASE_URL/api/assets/$ASSET_ID" "$FINANCE_MGR_TOKEN" "$TMP_DIR/invalid-patch.json" "$INVALID_PATCH_BODY")" +assert_status "$invalid_patch_status" "400" "patch asset purchase source forbidden" "$TMP_DIR/invalid-patch.json" +assert_jq "$TMP_DIR/invalid-patch.json" '.error.code == "VALID_001"' "invalid purchase source patch code" + +echo "[8/8] verify category and asset db state" +assert_asset_state "$ASSET_ID" "$CATEGORY_ID" "$EMPLOYEE_ID" "$APPROVED_PURCHASE_ID" + +echo "[OK] Asset regression passed" +echo "- assetCategoryId: $CATEGORY_ID" +echo "- assetId: $ASSET_ID" diff --git a/server/tests/unit/constants/permissions.test.js b/server/tests/unit/constants/permissions.test.js index c1ce3c3a..b2a8d0f3 100644 --- a/server/tests/unit/constants/permissions.test.js +++ b/server/tests/unit/constants/permissions.test.js @@ -44,6 +44,17 @@ describe("Permission Constants", () => { expect(hasPermission(resolvePermissionsForRoles(["administration"]), "procurement.manage")).toBe(true); }); + + it("grants asset permissions to configured roles", () => { + expect(hasPermission(resolvePermissionsForRoles(["employee"]), "asset.view_own")).toBe(true); + expect(hasPermission(resolvePermissionsForRoles(["manager"]), "asset.view_all")).toBe(true); + expect(hasPermission(resolvePermissionsForRoles(["finance"]), "asset.view_all")).toBe(true); + expect(hasPermission(resolvePermissionsForRoles(["administration"]), "asset.manage")).toBe(true); + expect(hasPermission(resolvePermissionsForRoles(["administration"]), "asset.transfer")).toBe(true); + expect(hasPermission(resolvePermissionsForRoles(["finance_mgr"]), "asset.inventory")).toBe(true); + expect(hasPermission(resolvePermissionsForRoles(["employee"]), "asset.manage")).toBe(false); + }); + it("maps workflow entities to correct permission codes", () => { expect(resolveEntityPermission("reimbursement", "approve")).toBe("expense.approve"); expect(resolveEntityPermission("reimbursement", "transfer")).toBe("expense.transfer"); diff --git a/server/tests/unit/documentSequence.service.test.js b/server/tests/unit/documentSequence.service.test.js index df2eb770..0cef9b60 100644 --- a/server/tests/unit/documentSequence.service.test.js +++ b/server/tests/unit/documentSequence.service.test.js @@ -130,4 +130,27 @@ describe("documentSequence service", () => { expect(issued.config.source).toBe("document_category"); expect(issued.documentNumber).toMatch(/^CG-\d{8}-0001$/); }); + + + it("falls back to default asset category code for asset cards", async () => { + const prisma = buildPrisma({ + categoryMap: { + ASSET_CARD_DEFAULT: { + code: "ASSET_CARD_DEFAULT", + entity_type: "asset_card", + sequence_prefix: "ZC", + number_length: 4, + reset_period: "yearly", + enabled: true, + archived_at: null, + }, + }, + }); + + const config = await resolveDocumentSequenceConfig(prisma, { documentType: "asset_card" }); + + expect(config.source).toBe("document_category"); + expect(config.documentCategoryCode).toBe("ASSET_CARD_DEFAULT"); + expect(config.prefix).toBe("ZC"); + }); });