@yss123/cage-cli 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (235) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +18 -2
  3. package/bin/cage.js +6 -0
  4. package/bin/create-cage-app.js +6 -0
  5. package/dist/analyze.d.ts +18 -0
  6. package/dist/analyze.js +26 -0
  7. package/dist/analyze.js.map +1 -0
  8. package/dist/baseline.d.ts +5 -0
  9. package/dist/baseline.js +50 -0
  10. package/dist/baseline.js.map +1 -0
  11. package/dist/budget.d.ts +7 -0
  12. package/dist/budget.js +52 -0
  13. package/dist/budget.js.map +1 -0
  14. package/dist/config.d.ts +30 -0
  15. package/dist/config.js +47 -0
  16. package/dist/config.js.map +1 -0
  17. package/dist/create.d.ts +1 -0
  18. package/dist/create.js +47 -0
  19. package/dist/create.js.map +1 -0
  20. package/dist/database.d.ts +5 -0
  21. package/dist/database.js +70 -0
  22. package/dist/database.js.map +1 -0
  23. package/dist/derive.d.ts +14 -0
  24. package/dist/derive.js +163 -0
  25. package/dist/derive.js.map +1 -0
  26. package/dist/dev.d.ts +2 -0
  27. package/dist/dev.js +155 -0
  28. package/dist/dev.js.map +1 -0
  29. package/dist/env.d.ts +1 -0
  30. package/dist/env.js +15 -0
  31. package/dist/env.js.map +1 -0
  32. package/dist/findings.d.ts +11 -0
  33. package/dist/findings.js +13 -0
  34. package/dist/findings.js.map +1 -0
  35. package/dist/gen/contracts.d.ts +6 -0
  36. package/dist/gen/contracts.js +167 -0
  37. package/dist/gen/contracts.js.map +1 -0
  38. package/dist/gen/index.d.ts +19 -0
  39. package/dist/gen/index.js +137 -0
  40. package/dist/gen/index.js.map +1 -0
  41. package/dist/gen/zod-code.d.ts +7 -0
  42. package/dist/gen/zod-code.js +161 -0
  43. package/dist/gen/zod-code.js.map +1 -0
  44. package/dist/graph-snapshot.d.ts +13 -0
  45. package/dist/graph-snapshot.js +46 -0
  46. package/dist/graph-snapshot.js.map +1 -0
  47. package/dist/graph.d.ts +11 -0
  48. package/dist/graph.js +87 -0
  49. package/dist/graph.js.map +1 -0
  50. package/dist/impact.d.ts +28 -0
  51. package/dist/impact.js +223 -0
  52. package/dist/impact.js.map +1 -0
  53. package/dist/index.d.ts +16 -0
  54. package/dist/index.js +18 -0
  55. package/dist/index.js.map +1 -0
  56. package/dist/load.d.ts +6 -0
  57. package/dist/load.js +17 -0
  58. package/dist/load.js.map +1 -0
  59. package/dist/main.d.ts +1 -0
  60. package/dist/main.js +176 -0
  61. package/dist/main.js.map +1 -0
  62. package/dist/migrate.d.ts +7 -0
  63. package/dist/migrate.js +69 -0
  64. package/dist/migrate.js.map +1 -0
  65. package/dist/ontology.d.ts +41 -0
  66. package/dist/ontology.js +270 -0
  67. package/dist/ontology.js.map +1 -0
  68. package/dist/process.d.ts +6 -0
  69. package/dist/process.js +39 -0
  70. package/dist/process.js.map +1 -0
  71. package/dist/rules.d.ts +23 -0
  72. package/dist/rules.js +251 -0
  73. package/dist/rules.js.map +1 -0
  74. package/dist/static-check.d.ts +23 -0
  75. package/dist/static-check.js +225 -0
  76. package/dist/static-check.js.map +1 -0
  77. package/dist/sync.d.ts +5 -0
  78. package/dist/sync.js +46 -0
  79. package/dist/sync.js.map +1 -0
  80. package/dist/upgrade.d.ts +10 -0
  81. package/dist/upgrade.js +81 -0
  82. package/dist/upgrade.js.map +1 -0
  83. package/dist/verify.d.ts +7 -0
  84. package/dist/verify.js +88 -0
  85. package/dist/verify.js.map +1 -0
  86. package/dist/version.d.ts +4 -0
  87. package/dist/version.js +6 -0
  88. package/dist/version.js.map +1 -0
  89. package/graph-ui/app.js +1065 -0
  90. package/graph-ui/index.html +197 -0
  91. package/graph-ui/model.js +141 -0
  92. package/graph-ui/relations.js +125 -0
  93. package/graph-ui/structure.js +74 -0
  94. package/graph-ui/style.css +1698 -0
  95. package/managed/errors/CAGE-APPR-001.md +32 -0
  96. package/managed/errors/CAGE-APPR-002.md +32 -0
  97. package/managed/errors/CAGE-APPR-003.md +32 -0
  98. package/managed/errors/CAGE-APPR-004.md +32 -0
  99. package/managed/errors/CAGE-APPR-005.md +32 -0
  100. package/managed/errors/CAGE-APPR-006.md +32 -0
  101. package/managed/errors/CAGE-APPR-007.md +32 -0
  102. package/managed/errors/CAGE-APPR-010.md +32 -0
  103. package/managed/errors/CAGE-AUTH-001.md +32 -0
  104. package/managed/errors/CAGE-BUDGET-001.md +32 -0
  105. package/managed/errors/CAGE-BUDGET-002.md +32 -0
  106. package/managed/errors/CAGE-BUDGET-003.md +32 -0
  107. package/managed/errors/CAGE-CMD-002.md +32 -0
  108. package/managed/errors/CAGE-CMD-003.md +34 -0
  109. package/managed/errors/CAGE-CONN-001.md +32 -0
  110. package/managed/errors/CAGE-CONN-002.md +32 -0
  111. package/managed/errors/CAGE-CONN-003.md +32 -0
  112. package/managed/errors/CAGE-CONN-004.md +32 -0
  113. package/managed/errors/CAGE-CONN-005.md +32 -0
  114. package/managed/errors/CAGE-CONN-006.md +32 -0
  115. package/managed/errors/CAGE-CONN-007.md +32 -0
  116. package/managed/errors/CAGE-CONN-008.md +32 -0
  117. package/managed/errors/CAGE-CONN-009.md +32 -0
  118. package/managed/errors/CAGE-CONN-010.md +32 -0
  119. package/managed/errors/CAGE-CONN-011.md +32 -0
  120. package/managed/errors/CAGE-CONN-012.md +32 -0
  121. package/managed/errors/CAGE-CREATE-001.md +32 -0
  122. package/managed/errors/CAGE-DECL-001.md +34 -0
  123. package/managed/errors/CAGE-DECL-002.md +34 -0
  124. package/managed/errors/CAGE-DECL-003.md +34 -0
  125. package/managed/errors/CAGE-DECL-004.md +34 -0
  126. package/managed/errors/CAGE-DEP-001.md +34 -0
  127. package/managed/errors/CAGE-DEP-002.md +34 -0
  128. package/managed/errors/CAGE-DEP-003.md +34 -0
  129. package/managed/errors/CAGE-DEP-004.md +34 -0
  130. package/managed/errors/CAGE-DEP-005.md +34 -0
  131. package/managed/errors/CAGE-DLQ-001.md +32 -0
  132. package/managed/errors/CAGE-ENV-001.md +32 -0
  133. package/managed/errors/CAGE-EVENT-001.md +32 -0
  134. package/managed/errors/CAGE-EVENT-002.md +32 -0
  135. package/managed/errors/CAGE-FIELD-001.md +32 -0
  136. package/managed/errors/CAGE-FIELD-002.md +32 -0
  137. package/managed/errors/CAGE-FIELD-003.md +32 -0
  138. package/managed/errors/CAGE-FIELD-004.md +32 -0
  139. package/managed/errors/CAGE-GEN-001.md +33 -0
  140. package/managed/errors/CAGE-GEN-002.md +33 -0
  141. package/managed/errors/CAGE-GRANT-001.md +32 -0
  142. package/managed/errors/CAGE-GRANT-002.md +32 -0
  143. package/managed/errors/CAGE-GRANT-003.md +32 -0
  144. package/managed/errors/CAGE-IMPACT-001.md +32 -0
  145. package/managed/errors/CAGE-JOB-001.md +32 -0
  146. package/managed/errors/CAGE-JOB-002.md +32 -0
  147. package/managed/errors/CAGE-JOB-003.md +32 -0
  148. package/managed/errors/CAGE-LINT-001.md +32 -0
  149. package/managed/errors/CAGE-MIG-001.md +32 -0
  150. package/managed/errors/CAGE-MIG-002.md +32 -0
  151. package/managed/errors/CAGE-MIG-003.md +32 -0
  152. package/managed/errors/CAGE-MIG-004.md +32 -0
  153. package/managed/errors/CAGE-MIG-005.md +32 -0
  154. package/managed/errors/CAGE-NAME-001.md +32 -0
  155. package/managed/errors/CAGE-NAME-002.md +32 -0
  156. package/managed/errors/CAGE-NAME-003.md +32 -0
  157. package/managed/errors/CAGE-NAME-004.md +32 -0
  158. package/managed/errors/CAGE-NAME-005.md +32 -0
  159. package/managed/errors/CAGE-NAME-006.md +32 -0
  160. package/managed/errors/CAGE-ORG-001.md +32 -0
  161. package/managed/errors/CAGE-PAIR-002.md +33 -0
  162. package/managed/errors/CAGE-QUERY-001.md +32 -0
  163. package/managed/errors/CAGE-QUERY-002.md +32 -0
  164. package/managed/errors/CAGE-QUERY-003.md +32 -0
  165. package/managed/errors/CAGE-REACT-001.md +32 -0
  166. package/managed/errors/CAGE-REACT-002.md +32 -0
  167. package/managed/errors/CAGE-READS-001.md +32 -0
  168. package/managed/errors/CAGE-REG-001.md +32 -0
  169. package/managed/errors/CAGE-REG-002.md +32 -0
  170. package/managed/errors/CAGE-REG-003.md +32 -0
  171. package/managed/errors/CAGE-ROUTE-001.md +32 -0
  172. package/managed/errors/CAGE-SCOPE-001.md +32 -0
  173. package/managed/errors/CAGE-SCOPE-002.md +32 -0
  174. package/managed/errors/CAGE-SECRET-001.md +32 -0
  175. package/managed/errors/CAGE-SECRET-002.md +32 -0
  176. package/managed/errors/CAGE-SENS-001.md +32 -0
  177. package/managed/errors/CAGE-SQL-001.md +32 -0
  178. package/managed/errors/CAGE-SQL-002.md +32 -0
  179. package/managed/errors/CAGE-STATE-001.md +32 -0
  180. package/managed/errors/CAGE-STATE-002.md +32 -0
  181. package/managed/errors/CAGE-STATE-003.md +32 -0
  182. package/managed/errors/CAGE-STATE-004.md +32 -0
  183. package/managed/errors/CAGE-STATE-005.md +32 -0
  184. package/managed/errors/CAGE-STATE-010.md +32 -0
  185. package/managed/errors/CAGE-STATE-011.md +32 -0
  186. package/managed/errors/CAGE-SYNC-001.md +35 -0
  187. package/managed/errors/CAGE-TEST-001.md +33 -0
  188. package/managed/errors/CAGE-UNSAFE-001.md +32 -0
  189. package/managed/errors/CAGE-UPGRADE-001.md +32 -0
  190. package/managed/errors/README.md +99 -0
  191. package/package.json +60 -3
  192. package/templates/app/.dockerignore +10 -0
  193. package/templates/app/.env.example +17 -0
  194. package/templates/app/.github/workflows/cage.yml +44 -0
  195. package/templates/app/AGENTS.md +13 -0
  196. package/templates/app/Dockerfile +23 -0
  197. package/templates/app/README.md +58 -0
  198. package/templates/app/apps/api/package.json +16 -0
  199. package/templates/app/apps/api/src/main.ts +10 -0
  200. package/templates/app/apps/api/src/worker.ts +14 -0
  201. package/templates/app/apps/api/tsconfig.json +5 -0
  202. package/templates/app/apps/web/index.html +13 -0
  203. package/templates/app/apps/web/package.json +22 -0
  204. package/templates/app/apps/web/src/main.tsx +552 -0
  205. package/templates/app/apps/web/src/style.css +663 -0
  206. package/templates/app/apps/web/tsconfig.json +11 -0
  207. package/templates/app/apps/web/vite.config.ts +16 -0
  208. package/templates/app/cage.config.json +114 -0
  209. package/templates/app/compose.yaml +39 -0
  210. package/templates/app/db/migrations/0001_init.sql +112 -0
  211. package/templates/app/package.json +25 -0
  212. package/templates/app/packages/contracts/package.json +16 -0
  213. package/templates/app/packages/contracts/src/dept.ts +74 -0
  214. package/templates/app/packages/contracts/src/index.ts +81 -0
  215. package/templates/app/packages/contracts/src/inventory-lock.ts +52 -0
  216. package/templates/app/packages/contracts/src/inventory.ts +57 -0
  217. package/templates/app/packages/contracts/src/purchase-order.ts +72 -0
  218. package/templates/app/packages/contracts/src/queries.ts +17 -0
  219. package/templates/app/packages/contracts/src/role.ts +85 -0
  220. package/templates/app/packages/contracts/src/user.ts +89 -0
  221. package/templates/app/packages/contracts/tsconfig.json +5 -0
  222. package/templates/app/packages/domain/package.json +23 -0
  223. package/templates/app/packages/domain/src/approvals.ts +18 -0
  224. package/templates/app/packages/domain/src/erp.ts +36 -0
  225. package/templates/app/packages/domain/src/index.ts +9 -0
  226. package/templates/app/packages/domain/src/inventory.ts +43 -0
  227. package/templates/app/packages/domain/src/jobs.ts +13 -0
  228. package/templates/app/packages/domain/src/purchase-order.test.ts +137 -0
  229. package/templates/app/packages/domain/src/purchase-order.ts +122 -0
  230. package/templates/app/packages/domain/src/queries.ts +10 -0
  231. package/templates/app/packages/domain/src/reactions.ts +24 -0
  232. package/templates/app/packages/domain/tsconfig.json +5 -0
  233. package/templates/app/pnpm-workspace.yaml +10 -0
  234. package/templates/app/renovate.json +5 -0
  235. package/templates/app/tsconfig.base.json +8 -0
@@ -0,0 +1,72 @@
1
+ // 生成物:由 cage gen 从 domain 声明生成,不要手改。
2
+ /* eslint-disable */
3
+
4
+ import { z } from 'zod';
5
+ import { client, useCageList, useCageQuery } from '@yss123/cage-client';
6
+
7
+ /** PurchaseOrder:读侧快照(敏感字段是按权限掩码后的值) */
8
+ export const PurchaseOrderSchema = z.object({
9
+ id: z.string().uuid(),
10
+ deptId: z.string().uuid(),
11
+ createdBy: z.string().uuid(),
12
+ supplierPhone: z.string().nullable().optional(),
13
+ amount: z.string(),
14
+ items: z.array(z.object({ sku: z.string(), qty: z.number().int() })),
15
+ erpNo: z.string().nullable().optional(),
16
+ status: z.enum(["draft", "pending_approval", "approved", "received", "rejected", "cancelled"]),
17
+ version: z.number().int(),
18
+ createdAt: z.string(),
19
+ updatedAt: z.string(),
20
+ });
21
+ export type PurchaseOrder = z.infer<typeof PurchaseOrderSchema>;
22
+ export type PurchaseOrderStatus = PurchaseOrder['status'];
23
+ export const PurchaseOrderStates = ['draft', 'pending_approval', 'approved', 'received', 'rejected', 'cancelled'] as const;
24
+
25
+ /** 命令输入:和后端命令的输入校验完全一致 */
26
+ export const PurchaseOrderInputs = {
27
+ cancel: z.object({ id: z.string().uuid() }),
28
+ create: z.object({ amount: z.string(), items: z.array(z.object({ sku: z.string(), qty: z.number().int().max(9007199254740991).gt(0) })).min(1), supplierPhone: z.string().optional(), deptId: z.string().uuid().optional() }),
29
+ submit: z.object({ id: z.string().uuid() }),
30
+ };
31
+
32
+ /** PurchaseOrder.cancel 可能返回的错误码 */
33
+ export type PurchaseOrderCancelError = 'CONFLICT' | 'DOMAIN_ERROR' | 'FORBIDDEN' | 'INVALID_INPUT' | 'INVALID_TRANSITION' | 'INVARIANT_VIOLATED' | 'NOT_FOUND';
34
+ /** PurchaseOrder.create 可能返回的错误码 */
35
+ export type PurchaseOrderCreateError = 'CONFLICT' | 'CONSTRAINT_VIOLATED' | 'DOMAIN_ERROR' | 'FORBIDDEN' | 'INVALID_INPUT' | 'INVARIANT_VIOLATED';
36
+ /** PurchaseOrder.submit 可能返回的错误码;业务码(domainCode):NO_ITEMS */
37
+ export type PurchaseOrderSubmitError = 'CONFLICT' | 'DOMAIN_ERROR' | 'FORBIDDEN' | 'INVALID_INPUT' | 'INVALID_TRANSITION' | 'INVARIANT_VIOLATED' | 'NOT_FOUND';
38
+ export type PurchaseOrderSubmitDomainCode = 'NO_ITEMS';
39
+
40
+ export type PurchaseOrderSortKey = 'createdAt' | 'updatedAt' | 'deptId' | 'createdBy' | 'supplierPhone' | 'amount' | 'erpNo';
41
+ export interface PurchaseOrderListParams {
42
+ page?: number;
43
+ pageSize?: number;
44
+ /** 排序:字段名,前面加 - 表示倒序 */
45
+ sort?: PurchaseOrderSortKey | `-${PurchaseOrderSortKey}`;
46
+ /** 可以逗号分隔多个状态 */
47
+ status?: string;
48
+ createdAfter?: string;
49
+ createdBefore?: string;
50
+ deptId?: string | number | boolean;
51
+ createdBy?: string | number | boolean;
52
+ amount?: string | number | boolean;
53
+ erpNo?: string | number | boolean;
54
+ }
55
+
56
+ /** 调用方法:每个命令一个;client 自动生成并附带幂等键和 CSRF 令牌 */
57
+ export const purchaseOrder = {
58
+ cancel: (id: string, expectedVersion?: number) => client.post<PurchaseOrder, PurchaseOrderCancelError>(`/api/purchase-order/${encodeURIComponent(id)}/cancel`, { expectedVersion }),
59
+ create: (input: z.input<typeof PurchaseOrderInputs.create>) => client.post<PurchaseOrder, PurchaseOrderCreateError>('/api/purchase-order/create', input),
60
+ submit: (id: string, expectedVersion?: number) => client.post<PurchaseOrder, PurchaseOrderSubmitError>(`/api/purchase-order/${encodeURIComponent(id)}/submit`, { expectedVersion }),
61
+ /** 导出 CSV:需要 PurchaseOrder.export 能力 */
62
+ export: (params: PurchaseOrderListParams = {}) => client.post<string, never>('/api/purchase-order/export', params),
63
+ get: (id: string) => client.get<PurchaseOrder>(`/api/purchase-order/${encodeURIComponent(id)}`),
64
+ list: (params: PurchaseOrderListParams = {}) => client.get<{ items: PurchaseOrder[]; total: number; page: number; pageSize: number }>('/api/purchase-order', params as Record<string, unknown>),
65
+ };
66
+
67
+ /** 详情查询 hook:响应也用 schema 校验;范围外返回 NOT_FOUND */
68
+ export const usePurchaseOrder = (id: string) => useCageQuery(['purchase-order', id], `/api/purchase-order/${encodeURIComponent(id)}`, PurchaseOrderSchema, { enabled: !!id });
69
+ /** 列表查询 hook:走读侧账号与安全视图 */
70
+ export const usePurchaseOrderList = (params: PurchaseOrderListParams = {}) => useCageList(['purchase-order'], '/api/purchase-order', params as Record<string, unknown>, PurchaseOrderSchema);
71
+ /** 查询缓存键前缀:命令成功后让它失效 */
72
+ export const purchaseOrderKeys = { all: ['purchase-order'] as const, one: (id: string) => ['purchase-order', id] as const };
@@ -0,0 +1,17 @@
1
+ // 生成物:由 cage gen 从 domain 声明生成,不要手改。
2
+ /* eslint-disable */
3
+
4
+ import { z } from 'zod';
5
+ import { client, useCageQuery } from '@yss123/cage-client';
6
+
7
+ /** 查询输入 */
8
+ export const QueryInputs = {
9
+ poByStatus: z.object({ status: z.string() }),
10
+ };
11
+
12
+ export const queries = {
13
+ /** poByStatus(需要 PurchaseOrder.read) */
14
+ poByStatus: (input: z.input<typeof QueryInputs.poByStatus> = {} as any) => client.get<Record<string, unknown>[]>('/api/queries/po-by-status', input as Record<string, unknown>),
15
+ };
16
+
17
+ export const usePoByStatus = (input: z.input<typeof QueryInputs.poByStatus> = {} as any) => useCageQuery<Record<string, unknown>[]>(['query', 'poByStatus'], '/api/queries/po-by-status', undefined, { query: input as Record<string, unknown> });
@@ -0,0 +1,85 @@
1
+ // 生成物:由 cage gen 从 domain 声明生成,不要手改。
2
+ /* eslint-disable */
3
+
4
+ import { z } from 'zod';
5
+ import { client, useCageList, useCageQuery } from '@yss123/cage-client';
6
+
7
+ /** 角色:读侧快照(敏感字段是按权限掩码后的值) */
8
+ export const RoleSchema = z.object({
9
+ id: z.string().uuid(),
10
+ code: z.string(),
11
+ name: z.string(),
12
+ capabilities: z.array(z.object({ capability: z.string(), params: z.record(z.string(), z.unknown()).nullable().optional() })),
13
+ scopes: z.array(z.object({ aggregate: z.string(), combinator: z.enum(["all", "self", "dept", "deptSubtree", "deptSet"]), deptIds: z.array(z.string().uuid()).nullable().optional() })),
14
+ status: z.enum(["active", "archived"]),
15
+ version: z.number().int(),
16
+ createdAt: z.string(),
17
+ updatedAt: z.string(),
18
+ });
19
+ export type Role = z.infer<typeof RoleSchema>;
20
+ export type RoleStatus = Role['status'];
21
+ export const RoleStates = ['active', 'archived'] as const;
22
+
23
+ /** 命令输入:和后端命令的输入校验完全一致 */
24
+ export const RoleInputs = {
25
+ archive: z.object({ id: z.string().uuid() }),
26
+ create: z.object({ code: z.string().min(1).max(64).regex(new RegExp("^[a-z][a-z0-9_]*$")), name: z.string().min(1).max(100) }),
27
+ grantCapability: z.object({ id: z.string().uuid(), capability: z.string().min(1).max(200), params: z.record(z.string(), z.unknown()).optional() }),
28
+ rename: z.object({ id: z.string().uuid(), name: z.string().min(1).max(100) }),
29
+ revokeCapability: z.object({ id: z.string().uuid(), capability: z.string().min(1).max(200) }),
30
+ setScope: z.object({ id: z.string().uuid(), aggregate: z.string().min(1).max(100), combinator: z.enum(["all", "self", "dept", "deptSubtree", "deptSet"]).nullable(), deptIds: z.array(z.string().uuid()).optional() }),
31
+ };
32
+
33
+ /** Role.archive 可能返回的错误码 */
34
+ export type RoleArchiveError = 'CONFLICT' | 'DOMAIN_ERROR' | 'FORBIDDEN' | 'INVALID_INPUT' | 'INVALID_TRANSITION' | 'NOT_FOUND';
35
+ /** Role.create 可能返回的错误码 */
36
+ export type RoleCreateError = 'CONFLICT' | 'CONSTRAINT_VIOLATED' | 'DOMAIN_ERROR' | 'FORBIDDEN' | 'INVALID_INPUT';
37
+ /** Role.grantCapability 可能返回的错误码 */
38
+ export type RoleGrantCapabilityError = 'CONFLICT' | 'CONSTRAINT_VIOLATED' | 'DOMAIN_ERROR' | 'FORBIDDEN' | 'INVALID_INPUT' | 'INVALID_TRANSITION' | 'NOT_FOUND';
39
+ /** Role.rename 可能返回的错误码 */
40
+ export type RoleRenameError = 'CONFLICT' | 'DOMAIN_ERROR' | 'FORBIDDEN' | 'INVALID_INPUT' | 'INVALID_TRANSITION' | 'NOT_FOUND';
41
+ /** Role.revokeCapability 可能返回的错误码 */
42
+ export type RoleRevokeCapabilityError = 'CONFLICT' | 'DOMAIN_ERROR' | 'FORBIDDEN' | 'INVALID_INPUT' | 'INVALID_TRANSITION' | 'NOT_FOUND';
43
+ /** Role.setScope 可能返回的错误码 */
44
+ export type RoleSetScopeError = 'CONFLICT' | 'CONSTRAINT_VIOLATED' | 'DOMAIN_ERROR' | 'FORBIDDEN' | 'INVALID_INPUT' | 'INVALID_TRANSITION' | 'NOT_FOUND';
45
+
46
+ export type RoleSortKey = 'createdAt' | 'updatedAt' | 'code' | 'name';
47
+ export interface RoleListParams {
48
+ page?: number;
49
+ pageSize?: number;
50
+ /** 排序:字段名,前面加 - 表示倒序 */
51
+ sort?: RoleSortKey | `-${RoleSortKey}`;
52
+ /** 可以逗号分隔多个状态 */
53
+ status?: string;
54
+ createdAfter?: string;
55
+ createdBefore?: string;
56
+ code?: string | number | boolean;
57
+ name?: string | number | boolean;
58
+ }
59
+
60
+ /** 调用方法:每个命令一个;client 自动生成并附带幂等键和 CSRF 令牌 */
61
+ export const role = {
62
+ /** 停用角色 */
63
+ archive: (id: string, expectedVersion?: number) => client.post<Role, RoleArchiveError>(`/api/role/${encodeURIComponent(id)}/archive`, { expectedVersion }),
64
+ /** 新建角色(默认没有任何能力) */
65
+ create: (input: z.input<typeof RoleInputs.create>) => client.post<Role, RoleCreateError>('/api/role/create', input),
66
+ /** 给角色授予能力点(不能授出自己没有的能力点) */
67
+ grantCapability: (id: string, input: Omit<z.input<typeof RoleInputs.grantCapability>, 'id'>, expectedVersion?: number) => client.post<Role, RoleGrantCapabilityError>(`/api/role/${encodeURIComponent(id)}/grant-capability`, { ...input, expectedVersion }),
68
+ /** 修改角色名称 */
69
+ rename: (id: string, input: Omit<z.input<typeof RoleInputs.rename>, 'id'>, expectedVersion?: number) => client.post<Role, RoleRenameError>(`/api/role/${encodeURIComponent(id)}/rename`, { ...input, expectedVersion }),
70
+ /** 收回角色的能力点 */
71
+ revokeCapability: (id: string, input: Omit<z.input<typeof RoleInputs.revokeCapability>, 'id'>, expectedVersion?: number) => client.post<Role, RoleRevokeCapabilityError>(`/api/role/${encodeURIComponent(id)}/revoke-capability`, { ...input, expectedVersion }),
72
+ /** 设置角色在某个聚合上的数据范围(不能授出比自己更宽的范围) */
73
+ setScope: (id: string, input: Omit<z.input<typeof RoleInputs.setScope>, 'id'>, expectedVersion?: number) => client.post<Role, RoleSetScopeError>(`/api/role/${encodeURIComponent(id)}/set-scope`, { ...input, expectedVersion }),
74
+ /** 导出 CSV:需要 Role.export 能力 */
75
+ export: (params: RoleListParams = {}) => client.post<string, never>('/api/role/export', params),
76
+ get: (id: string) => client.get<Role>(`/api/role/${encodeURIComponent(id)}`),
77
+ list: (params: RoleListParams = {}) => client.get<{ items: Role[]; total: number; page: number; pageSize: number }>('/api/role', params as Record<string, unknown>),
78
+ };
79
+
80
+ /** 详情查询 hook:响应也用 schema 校验;范围外返回 NOT_FOUND */
81
+ export const useRole = (id: string) => useCageQuery(['role', id], `/api/role/${encodeURIComponent(id)}`, RoleSchema, { enabled: !!id });
82
+ /** 列表查询 hook:走读侧账号与安全视图 */
83
+ export const useRoleList = (params: RoleListParams = {}) => useCageList(['role'], '/api/role', params as Record<string, unknown>, RoleSchema);
84
+ /** 查询缓存键前缀:命令成功后让它失效 */
85
+ export const roleKeys = { all: ['role'] as const, one: (id: string) => ['role', id] as const };
@@ -0,0 +1,89 @@
1
+ // 生成物:由 cage gen 从 domain 声明生成,不要手改。
2
+ /* eslint-disable */
3
+
4
+ import { z } from 'zod';
5
+ import { client, useCageList, useCageQuery } from '@yss123/cage-client';
6
+
7
+ /** 用户:读侧快照(敏感字段是按权限掩码后的值) */
8
+ export const UserSchema = z.object({
9
+ id: z.string().uuid(),
10
+ username: z.string(),
11
+ name: z.string(),
12
+ deptId: z.string().uuid(),
13
+ roleIds: z.array(z.string().uuid()),
14
+ phone: z.string().nullable().optional(),
15
+ email: z.string().nullable().optional(),
16
+ ssoSubject: z.string().nullable().optional(),
17
+ status: z.enum(["active", "disabled"]),
18
+ version: z.number().int(),
19
+ createdAt: z.string(),
20
+ updatedAt: z.string(),
21
+ });
22
+ export type User = z.infer<typeof UserSchema>;
23
+ export type UserStatus = User['status'];
24
+ export const UserStates = ['active', 'disabled'] as const;
25
+
26
+ /** 命令输入:和后端命令的输入校验完全一致 */
27
+ export const UserInputs = {
28
+ assignRoles: z.object({ id: z.string().uuid(), roleIds: z.array(z.string().uuid()).max(50) }),
29
+ create: z.object({ username: z.string().min(2).max(64).regex(new RegExp("^[A-Za-z0-9_.@-]+$")), name: z.string().min(1).max(100), deptId: z.string().uuid(), phone: z.string().max(32).nullable().optional(), email: z.string().email().max(200).nullable().optional(), ssoSubject: z.string().max(200).nullable().optional() }),
30
+ disable: z.object({ id: z.string().uuid() }),
31
+ enable: z.object({ id: z.string().uuid() }),
32
+ setPassword: z.object({ id: z.string().uuid(), password: z.string().min(8).max(200) }),
33
+ update: z.object({ id: z.string().uuid(), name: z.string().min(1).max(100).optional(), deptId: z.string().uuid().optional(), phone: z.string().max(32).nullable().optional(), email: z.string().email().max(200).nullable().optional() }),
34
+ };
35
+
36
+ /** User.assignRoles 可能返回的错误码 */
37
+ export type UserAssignRolesError = 'CONFLICT' | 'CONSTRAINT_VIOLATED' | 'DOMAIN_ERROR' | 'FORBIDDEN' | 'INVALID_INPUT' | 'INVALID_TRANSITION' | 'NOT_FOUND';
38
+ /** User.create 可能返回的错误码 */
39
+ export type UserCreateError = 'CONFLICT' | 'CONSTRAINT_VIOLATED' | 'DOMAIN_ERROR' | 'FORBIDDEN' | 'INVALID_INPUT';
40
+ /** User.disable 可能返回的错误码 */
41
+ export type UserDisableError = 'CONFLICT' | 'DOMAIN_ERROR' | 'FORBIDDEN' | 'INVALID_INPUT' | 'INVALID_TRANSITION' | 'NOT_FOUND';
42
+ /** User.enable 可能返回的错误码 */
43
+ export type UserEnableError = 'CONFLICT' | 'DOMAIN_ERROR' | 'FORBIDDEN' | 'INVALID_INPUT' | 'INVALID_TRANSITION' | 'NOT_FOUND';
44
+ /** User.setPassword 可能返回的错误码 */
45
+ export type UserSetPasswordError = 'CONFLICT' | 'DOMAIN_ERROR' | 'FORBIDDEN' | 'INVALID_INPUT' | 'INVALID_TRANSITION' | 'NOT_FOUND';
46
+ /** User.update 可能返回的错误码 */
47
+ export type UserUpdateError = 'CONFLICT' | 'DOMAIN_ERROR' | 'FORBIDDEN' | 'INVALID_INPUT' | 'INVALID_TRANSITION' | 'NOT_FOUND';
48
+
49
+ export type UserSortKey = 'createdAt' | 'updatedAt' | 'username' | 'name' | 'deptId' | 'phone' | 'email' | 'ssoSubject';
50
+ export interface UserListParams {
51
+ page?: number;
52
+ pageSize?: number;
53
+ /** 排序:字段名,前面加 - 表示倒序 */
54
+ sort?: UserSortKey | `-${UserSortKey}`;
55
+ /** 可以逗号分隔多个状态 */
56
+ status?: string;
57
+ createdAfter?: string;
58
+ createdBefore?: string;
59
+ username?: string | number | boolean;
60
+ deptId?: string | number | boolean;
61
+ ssoSubject?: string | number | boolean;
62
+ }
63
+
64
+ /** 调用方法:每个命令一个;client 自动生成并附带幂等键和 CSRF 令牌 */
65
+ export const user = {
66
+ /** 设置用户的角色(不能分配包含自己没有的能力的角色) */
67
+ assignRoles: (id: string, input: Omit<z.input<typeof UserInputs.assignRoles>, 'id'>, expectedVersion?: number) => client.post<User, UserAssignRolesError>(`/api/user/${encodeURIComponent(id)}/assign-roles`, { ...input, expectedVersion }),
68
+ /** 新建用户(默认没有任何角色) */
69
+ create: (input: z.input<typeof UserInputs.create>) => client.post<User, UserCreateError>('/api/user/create', input),
70
+ /** 停用用户 */
71
+ disable: (id: string, expectedVersion?: number) => client.post<User, UserDisableError>(`/api/user/${encodeURIComponent(id)}/disable`, { expectedVersion }),
72
+ /** 启用用户 */
73
+ enable: (id: string, expectedVersion?: number) => client.post<User, UserEnableError>(`/api/user/${encodeURIComponent(id)}/enable`, { expectedVersion }),
74
+ /** 重置密码 */
75
+ setPassword: (id: string, input: Omit<z.input<typeof UserInputs.setPassword>, 'id'>, expectedVersion?: number) => client.post<User, UserSetPasswordError>(`/api/user/${encodeURIComponent(id)}/set-password`, { ...input, expectedVersion }),
76
+ /** 修改用户信息 */
77
+ update: (id: string, input: Omit<z.input<typeof UserInputs.update>, 'id'>, expectedVersion?: number) => client.post<User, UserUpdateError>(`/api/user/${encodeURIComponent(id)}/update`, { ...input, expectedVersion }),
78
+ /** 导出 CSV:需要 User.export 能力 */
79
+ export: (params: UserListParams = {}) => client.post<string, never>('/api/user/export', params),
80
+ get: (id: string) => client.get<User>(`/api/user/${encodeURIComponent(id)}`),
81
+ list: (params: UserListParams = {}) => client.get<{ items: User[]; total: number; page: number; pageSize: number }>('/api/user', params as Record<string, unknown>),
82
+ };
83
+
84
+ /** 详情查询 hook:响应也用 schema 校验;范围外返回 NOT_FOUND */
85
+ export const useUser = (id: string) => useCageQuery(['user', id], `/api/user/${encodeURIComponent(id)}`, UserSchema, { enabled: !!id });
86
+ /** 列表查询 hook:走读侧账号与安全视图 */
87
+ export const useUserList = (params: UserListParams = {}) => useCageList(['user'], '/api/user', params as Record<string, unknown>, UserSchema);
88
+ /** 查询缓存键前缀:命令成功后让它失效 */
89
+ export const userKeys = { all: ['user'] as const, one: (id: string) => ['user', id] as const };
@@ -0,0 +1,5 @@
1
+ {
2
+ "extends": "../../tsconfig.base.json",
3
+ "compilerOptions": { "module": "ESNext", "moduleResolution": "Bundler", "outDir": "dist", "rootDir": "src" },
4
+ "include": ["src"]
5
+ }
@@ -0,0 +1,23 @@
1
+ {
2
+ "name": "@app/domain",
3
+ "private": true,
4
+ "type": "module",
5
+ "exports": {
6
+ ".": {
7
+ "types": "./dist/index.d.ts",
8
+ "development": "./src/index.ts",
9
+ "default": "./dist/index.js"
10
+ }
11
+ },
12
+ "scripts": {
13
+ "build": "tsc -p tsconfig.json",
14
+ "typecheck": "tsc -p tsconfig.json --noEmit"
15
+ },
16
+ "dependencies": {
17
+ "@yss123/cage-kernel": "0.1.0",
18
+ "@yss123/cage-admin": "0.1.0",
19
+ "@yss123/cage-approval": "0.1.0",
20
+ "@yss123/cage-connectors": "0.1.0",
21
+ "zod": "^4.1.5"
22
+ }
23
+ }
@@ -0,0 +1,18 @@
1
+ // 路由只决定由谁审批;最终结论仍调用领域命令,执行职责分离和额度约束。
2
+ import { approval } from '@yss123/cage-kernel';
3
+ import { approver, gt, route, step, when } from '@yss123/cage-approval';
4
+ import { PurchaseOrder, OrderSubmitted, OrderCancelled, approve, reject } from './purchase-order.js';
5
+
6
+ export const poApproval = approval(PurchaseOrder, {
7
+ process: 'po-approval',
8
+ start: OrderSubmitted,
9
+ cancelOn: [OrderCancelled],
10
+ routable: ['amount', 'deptId'],
11
+ onApproved: approve,
12
+ onRejected: reject,
13
+ });
14
+
15
+ export const poRoute = route('po-approval', [
16
+ step('部门经理审批', approver.deptLeader('deptId')),
17
+ when(gt('amount', 50000), step('财务审批', approver.role('finance'), { mode: 'any' })),
18
+ ]);
@@ -0,0 +1,36 @@
1
+ // ERP 的两个边界:出站只发送声明允许的字段,入站回调必须通过 HMAC 验签。
2
+ // 本地由 worker 注册 fake;这里始终只写声明,不发送请求或读取环境变量。
3
+ import { z } from 'zod';
4
+ import { connector, hmacSha256, inbound, op, outbound } from '@yss123/cage-connectors';
5
+ import { OrderApproved, recordErpNo, markReceived } from './purchase-order.js';
6
+
7
+ export const erp = connector('erp', {
8
+ hosts: { test: 'erp-sandbox.example.com', prod: 'erp.example.com' },
9
+ secret: 'ERP_API_TOKEN',
10
+ timeoutMs: 5000,
11
+ operations: {
12
+ pushOrder: op({
13
+ effect: 'retryableWrite',
14
+ method: 'POST',
15
+ path: '/api/purchase-orders',
16
+ request: z.object({
17
+ externalId: z.string(),
18
+ lines: z.array(z.object({ sku: z.string(), qty: z.number() })),
19
+ }),
20
+ response: z.object({ erpNo: z.string() }),
21
+ }),
22
+ },
23
+ });
24
+
25
+ export const pushToErp = outbound(OrderApproved, erp.operations.pushOrder, {
26
+ idempotencyKey: (e) => `po-${e.orderId}`,
27
+ map: (e) => ({ externalId: e.orderId, lines: e.items.map((i) => ({ sku: i.sku, qty: i.qty })) }),
28
+ onSuccess: { command: recordErpNo, map: (res, e) => ({ id: e.orderId, erpNo: res.erpNo }) },
29
+ });
30
+
31
+ export const erpReceived = inbound(erp, 'goodsReceived', {
32
+ verify: hmacSha256('ERP_WEBHOOK_SECRET'),
33
+ payload: z.object({ externalId: z.string().uuid() }),
34
+ command: markReceived,
35
+ map: (p) => ({ id: p.externalId }),
36
+ });
@@ -0,0 +1,9 @@
1
+ // 单一导出入口:CLI 抽取器、API 和 worker 都加载同一套声明。
2
+ export * from '@yss123/cage-admin/domain';
3
+ export * from './inventory.js';
4
+ export * from './purchase-order.js';
5
+ export * from './reactions.js';
6
+ export * from './approvals.js';
7
+ export * from './erp.js';
8
+ export * from './queries.js';
9
+ export * from './jobs.js';
@@ -0,0 +1,43 @@
1
+ // 最简聚合示例:Inventory 维护物料台账,InventoryLock 记录订单的资源占用。
2
+ // 此范例验证锁的生命周期;available 是登记数量,不包含仓库出入库核算。
3
+ import { aggregate, command, id, int, list, scope, states, text } from '@yss123/cage-kernel';
4
+ import { z } from 'zod';
5
+
6
+ export const Inventory = aggregate('Inventory', {
7
+ fields: { sku: text(64).unique(), available: int() },
8
+ scope: scope.none(),
9
+ });
10
+
11
+ export const createInventory = command(Inventory, {
12
+ name: 'create',
13
+ from: 'new',
14
+ to: 'active',
15
+ input: z.object({ sku: z.string(), available: z.number().int() }),
16
+ handler: (o) => o,
17
+ });
18
+
19
+ export const InventoryLock = aggregate('InventoryLock', {
20
+ fields: { orderId: id(), items: list({ sku: text(64), qty: int() }) },
21
+ states: states('held', 'released', { terminal: ['released'] }),
22
+ scope: scope.none(),
23
+ });
24
+
25
+ // 锁以订单 id 为主键,同一事务反应确保审批和建锁一起成功或回滚。
26
+ const itemsInput = z.array(z.object({ sku: z.string(), qty: z.number().int().positive() })).min(1);
27
+
28
+ export const acquireLock = command(InventoryLock, {
29
+ name: 'acquire',
30
+ from: 'new',
31
+ to: 'held',
32
+ invokedBy: 'process',
33
+ input: z.object({ id: z.string().uuid(), orderId: z.string().uuid(), items: itemsInput }),
34
+ handler: (o) => o,
35
+ });
36
+
37
+ export const releaseLock = command(InventoryLock, {
38
+ name: 'release',
39
+ from: 'held',
40
+ to: 'released',
41
+ invokedBy: 'process',
42
+ handler: (o) => o,
43
+ });
@@ -0,0 +1,13 @@
1
+ // 定时任务只调用显式列出的命令;取消操作和用户操作共用内核及审计。
2
+ import { daysAgo, job } from '@yss123/cage-kernel';
3
+ import { PurchaseOrder, cancel } from './purchase-order.js';
4
+
5
+ export const expireStaleOrders = job('expireStaleOrders', {
6
+ cron: '0 2 * * *',
7
+ reads: [PurchaseOrder],
8
+ calls: [cancel],
9
+ run: async ({ query, call }) => {
10
+ const stale = await query.list(PurchaseOrder, { status: 'draft', createdBefore: daysAgo(30) });
11
+ for (const po of stale) await call(cancel, { id: po.id });
12
+ },
13
+ });
@@ -0,0 +1,137 @@
1
+ // 定向业务测试补足通用造数无法准备的库存、审批人和角色参数。
2
+ // 使用真实 PostgreSQL 及示例迁移,确保示例不是仅能通过抽取的静态声明。
3
+ import { afterAll, beforeAll, expect, it } from 'vitest';
4
+ import { createTestEnv, type TestEnv, type TestUser } from '@yss123/cage-testkit';
5
+ import { readMigrations } from '@yss123/cage-kernel/db';
6
+ import { systemCtx } from '@yss123/cage-kernel';
7
+ import { fileURLToPath } from 'node:url';
8
+ import * as domain from './index.js';
9
+
10
+ let env: TestEnv;
11
+ let buyer: TestUser;
12
+ let manager: TestUser;
13
+ beforeAll(async () => {
14
+ env = await createTestEnv({
15
+ domain,
16
+ migrations: readMigrations(fileURLToPath(new URL('../../../db/migrations/', import.meta.url))),
17
+ });
18
+ buyer = await env.user({
19
+ caps: ['PurchaseOrder.create', 'PurchaseOrder.submit', 'PurchaseOrder.cancel', 'PurchaseOrder.read'],
20
+ });
21
+ manager = await env.user({
22
+ caps: ['PurchaseOrder.approve'],
23
+ params: { 'PurchaseOrder.approve': { ceiling: 50000 } },
24
+ });
25
+ await env.kernel.execute('Dept.update', { id: env.rootDeptId, leaderId: manager.id }, env.system());
26
+ await env.kernel.execute('Inventory.create', { sku: 'A-01', available: 100 }, env.system());
27
+ });
28
+ afterAll(() => env?.close());
29
+
30
+ async function submitted() {
31
+ const ctx = await buyer.ctx();
32
+ const order = await env.kernel.execute(
33
+ 'PurchaseOrder.create',
34
+ { amount: '1200.00', items: [{ sku: 'A-01', qty: 2 }] },
35
+ ctx,
36
+ );
37
+ return env.kernel.execute('PurchaseOrder.submit', { id: order.id }, ctx);
38
+ }
39
+
40
+ it('审批成功同事务建锁;取消后 worker 释放锁,且每步都有审计', async () => {
41
+ const order = await submitted();
42
+ const process = env.kernel.processCtx({
43
+ process: 'po-approval',
44
+ tenantId: env.tenantId,
45
+ onBehalfOf: manager.id,
46
+ capabilities: ['PurchaseOrder.approve'],
47
+ });
48
+ await env.kernel.execute('PurchaseOrder.approve', { id: order.id }, process);
49
+ expect(
50
+ (await env.query('select status from core.inventory_lock where id = $1', [order.id]))[0].status,
51
+ ).toBe('held');
52
+ // 先完成审批事件及 ERP 回写,再验证后续取消的资源释放。
53
+ // 同一单据的 outbox 严格按顺序执行,不能越过尚未成功的前序事件。
54
+ await env.drain();
55
+ await env.kernel.execute('PurchaseOrder.cancel', { id: order.id }, await buyer.ctx());
56
+ await env.drain();
57
+ expect(
58
+ (await env.query('select status from core.inventory_lock where id = $1', [order.id]))[0].status,
59
+ ).toBe('released');
60
+ expect(
61
+ await env.audit({ aggregateId: order.id, command: 'PurchaseOrder.approve', kind: 'write' }),
62
+ ).toHaveLength(1);
63
+ });
64
+
65
+ it('提单人不能借流程身份审批自己的采购单', async () => {
66
+ const order = await submitted();
67
+ const process = env.kernel.processCtx({
68
+ process: 'po-approval',
69
+ tenantId: env.tenantId,
70
+ onBehalfOf: buyer.id,
71
+ capabilities: ['PurchaseOrder.approve'],
72
+ });
73
+ expect((await env.callAs(process, 'PurchaseOrder.approve', { id: order.id })).code).toBe(
74
+ 'CONSTRAINT_VIOLATED',
75
+ );
76
+ });
77
+
78
+ it.each(['cancelled', 'received'] as const)(
79
+ '采购单进入 %s 后,延迟的 ERP 回写保留终态且不会阻塞解锁',
80
+ async (status) => {
81
+ const order = await submitted();
82
+ const process = env.kernel.processCtx({
83
+ process: 'po-approval',
84
+ tenantId: env.tenantId,
85
+ onBehalfOf: manager.id,
86
+ capabilities: ['PurchaseOrder.approve'],
87
+ });
88
+ await env.kernel.execute('PurchaseOrder.approve', { id: order.id }, process);
89
+ // 有意在处理 outbox 之前取消:数据库命令与第三方响应之间没有完成顺序保证。
90
+ // 断言最终业务状态和资源状态,避免仅验证命令返回成功而漏掉死信阻塞。
91
+ if (status === 'cancelled') {
92
+ await env.kernel.execute('PurchaseOrder.cancel', { id: order.id }, await buyer.ctx());
93
+ } else {
94
+ const external = systemCtx({
95
+ kind: 'external',
96
+ name: 'erp',
97
+ tenantId: env.tenantId,
98
+ capabilities: ['PurchaseOrder.markReceived'],
99
+ });
100
+ await env.kernel.execute('PurchaseOrder.markReceived', { id: order.id }, external);
101
+ }
102
+ await env.drain();
103
+ const saved = (
104
+ await env.query('select status, erp_no, version from core.purchase_order where id = $1', [order.id])
105
+ )[0];
106
+ expect(saved.status).toBe(status);
107
+ expect(saved.erp_no).toBeTruthy();
108
+ expect(saved.version).toBe(5);
109
+ expect(
110
+ (await env.query('select status from core.inventory_lock where id = $1', [order.id]))[0].status,
111
+ ).toBe('released');
112
+ expect(
113
+ await env.query('select id from sys.dead_letter where aggregate_id = $1', [order.id]),
114
+ ).toHaveLength(0);
115
+ expect(
116
+ await env.audit({ aggregateId: order.id, command: 'PurchaseOrder.recordErpNo', kind: 'write' }),
117
+ ).toHaveLength(1);
118
+ },
119
+ );
120
+
121
+ it('审批额度不够时保留待审批状态,不创建库存锁', async () => {
122
+ const order = await submitted();
123
+ const lowLimit = await env.user({
124
+ caps: ['PurchaseOrder.approve'],
125
+ params: { 'PurchaseOrder.approve': { ceiling: 500 } },
126
+ });
127
+ const process = env.kernel.processCtx({
128
+ process: 'po-approval',
129
+ tenantId: env.tenantId,
130
+ onBehalfOf: lowLimit.id,
131
+ capabilities: ['PurchaseOrder.approve'],
132
+ });
133
+ expect((await env.callAs(process, 'PurchaseOrder.approve', { id: order.id })).code).toBe(
134
+ 'CONSTRAINT_VIOLATED',
135
+ );
136
+ expect(await env.query('select id from core.inventory_lock where id = $1', [order.id])).toHaveLength(0);
137
+ });