@tailor-platform/erp-kit 0.5.0 → 0.6.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 (262) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/dist/cli.mjs +139 -35
  3. package/package.json +1 -1
  4. package/skills/erp-kit-app-5-impl-backend/SKILL.md +10 -5
  5. package/skills/erp-kit-app-7-impl-review/SKILL.md +1 -1
  6. package/skills/erp-kit-module-6-impl-review/SKILL.md +39 -17
  7. package/src/commands/generate-doc.ts +1 -1
  8. package/src/commands/init-module.test.ts +17 -3
  9. package/src/commands/init-module.ts +0 -12
  10. package/src/commands/lib/discovery.test.ts +13 -3
  11. package/src/commands/lib/discovery.ts +10 -2
  12. package/src/commands/lib/paths.ts +4 -2
  13. package/src/commands/lib/sync-check-tests.test.ts +84 -6
  14. package/src/commands/lib/sync-check-tests.ts +63 -3
  15. package/src/commands/sync-check.ts +7 -3
  16. package/src/generator/generate-app-code.ts +51 -16
  17. package/src/generator/generate-code-boilerplate.test.ts +9 -1
  18. package/src/generator/generate-stubs.ts +4 -0
  19. package/src/generator/scaffold.ts +6 -2
  20. package/src/generator/stub-templates.test.ts +11 -0
  21. package/src/generator/stub-templates.ts +22 -1
  22. package/src/mdschema.ts +39 -3
  23. package/src/modules/inventory/docs/features/inventory-adjustment.md +2 -1
  24. package/src/modules/inventory/docs/features/scrap-management.md +39 -1
  25. package/src/modules/manufacturing/README.md +63 -0
  26. package/src/modules/manufacturing/command/activateBillOfMaterial.generated.ts +6 -0
  27. package/src/modules/manufacturing/command/activateBillOfMaterial.test.ts +166 -0
  28. package/src/modules/manufacturing/command/activateBillOfMaterial.ts +173 -0
  29. package/src/modules/manufacturing/command/activateRouting.generated.ts +6 -0
  30. package/src/modules/manufacturing/command/activateRouting.test.ts +152 -0
  31. package/src/modules/manufacturing/command/activateRouting.ts +92 -0
  32. package/src/modules/manufacturing/command/activateWorkCenter.generated.ts +6 -0
  33. package/src/modules/manufacturing/command/activateWorkCenter.test.ts +135 -0
  34. package/src/modules/manufacturing/command/activateWorkCenter.ts +91 -0
  35. package/src/modules/manufacturing/command/cancelProductionOrder.generated.ts +6 -0
  36. package/src/modules/manufacturing/command/cancelProductionOrder.test.ts +151 -0
  37. package/src/modules/manufacturing/command/cancelProductionOrder.ts +114 -0
  38. package/src/modules/manufacturing/command/closeProductionOrder.generated.ts +6 -0
  39. package/src/modules/manufacturing/command/closeProductionOrder.test.ts +126 -0
  40. package/src/modules/manufacturing/command/closeProductionOrder.ts +87 -0
  41. package/src/modules/manufacturing/command/completeProductionOrder.generated.ts +6 -0
  42. package/src/modules/manufacturing/command/completeProductionOrder.test.ts +132 -0
  43. package/src/modules/manufacturing/command/completeProductionOrder.ts +97 -0
  44. package/src/modules/manufacturing/command/completeWorkOrder.generated.ts +6 -0
  45. package/src/modules/manufacturing/command/completeWorkOrder.test.ts +369 -0
  46. package/src/modules/manufacturing/command/completeWorkOrder.ts +212 -0
  47. package/src/modules/manufacturing/command/createBillOfMaterial.generated.ts +6 -0
  48. package/src/modules/manufacturing/command/createBillOfMaterial.test.ts +210 -0
  49. package/src/modules/manufacturing/command/createBillOfMaterial.ts +176 -0
  50. package/src/modules/manufacturing/command/createProductionOrder.generated.ts +6 -0
  51. package/src/modules/manufacturing/command/createProductionOrder.test.ts +160 -0
  52. package/src/modules/manufacturing/command/createProductionOrder.ts +129 -0
  53. package/src/modules/manufacturing/command/createRouting.generated.ts +6 -0
  54. package/src/modules/manufacturing/command/createRouting.test.ts +168 -0
  55. package/src/modules/manufacturing/command/createRouting.ts +128 -0
  56. package/src/modules/manufacturing/command/createWorkCenter.generated.ts +6 -0
  57. package/src/modules/manufacturing/command/createWorkCenter.test.ts +148 -0
  58. package/src/modules/manufacturing/command/createWorkCenter.ts +131 -0
  59. package/src/modules/manufacturing/command/deactivateBillOfMaterial.generated.ts +6 -0
  60. package/src/modules/manufacturing/command/deactivateBillOfMaterial.test.ts +103 -0
  61. package/src/modules/manufacturing/command/deactivateBillOfMaterial.ts +78 -0
  62. package/src/modules/manufacturing/command/deactivateRouting.generated.ts +6 -0
  63. package/src/modules/manufacturing/command/deactivateRouting.test.ts +112 -0
  64. package/src/modules/manufacturing/command/deactivateRouting.ts +76 -0
  65. package/src/modules/manufacturing/command/deactivateWorkCenter.generated.ts +6 -0
  66. package/src/modules/manufacturing/command/deactivateWorkCenter.test.ts +113 -0
  67. package/src/modules/manufacturing/command/deactivateWorkCenter.ts +85 -0
  68. package/src/modules/manufacturing/command/pauseWorkOrder.generated.ts +6 -0
  69. package/src/modules/manufacturing/command/pauseWorkOrder.test.ts +118 -0
  70. package/src/modules/manufacturing/command/pauseWorkOrder.ts +82 -0
  71. package/src/modules/manufacturing/command/recordInventoryIssueOutcome.generated.ts +6 -0
  72. package/src/modules/manufacturing/command/recordInventoryIssueOutcome.test.ts +183 -0
  73. package/src/modules/manufacturing/command/recordInventoryIssueOutcome.ts +139 -0
  74. package/src/modules/manufacturing/command/recordManufacturingCostSettlementAcknowledgment.generated.ts +6 -0
  75. package/src/modules/manufacturing/command/recordManufacturingCostSettlementAcknowledgment.test.ts +120 -0
  76. package/src/modules/manufacturing/command/recordManufacturingCostSettlementAcknowledgment.ts +110 -0
  77. package/src/modules/manufacturing/command/releaseProductionOrder.generated.ts +6 -0
  78. package/src/modules/manufacturing/command/releaseProductionOrder.test.ts +220 -0
  79. package/src/modules/manufacturing/command/releaseProductionOrder.ts +450 -0
  80. package/src/modules/manufacturing/command/reopenProductionOrder.generated.ts +6 -0
  81. package/src/modules/manufacturing/command/reopenProductionOrder.test.ts +196 -0
  82. package/src/modules/manufacturing/command/reopenProductionOrder.ts +98 -0
  83. package/src/modules/manufacturing/command/reportWorkOrderProgress.generated.ts +6 -0
  84. package/src/modules/manufacturing/command/reportWorkOrderProgress.test.ts +204 -0
  85. package/src/modules/manufacturing/command/reportWorkOrderProgress.ts +129 -0
  86. package/src/modules/manufacturing/command/rescheduleProductionOrder.generated.ts +6 -0
  87. package/src/modules/manufacturing/command/rescheduleProductionOrder.test.ts +185 -0
  88. package/src/modules/manufacturing/command/rescheduleProductionOrder.ts +95 -0
  89. package/src/modules/manufacturing/command/resumeWorkOrder.generated.ts +6 -0
  90. package/src/modules/manufacturing/command/resumeWorkOrder.test.ts +122 -0
  91. package/src/modules/manufacturing/command/resumeWorkOrder.ts +94 -0
  92. package/src/modules/manufacturing/command/reviewManufacturingCostSummary.generated.ts +6 -0
  93. package/src/modules/manufacturing/command/reviewManufacturingCostSummary.test.ts +231 -0
  94. package/src/modules/manufacturing/command/reviewManufacturingCostSummary.ts +137 -0
  95. package/src/modules/manufacturing/command/startWorkOrder.generated.ts +6 -0
  96. package/src/modules/manufacturing/command/startWorkOrder.test.ts +118 -0
  97. package/src/modules/manufacturing/command/startWorkOrder.ts +126 -0
  98. package/src/modules/manufacturing/command/technicallyCompleteProductionOrder.generated.ts +6 -0
  99. package/src/modules/manufacturing/command/technicallyCompleteProductionOrder.test.ts +153 -0
  100. package/src/modules/manufacturing/command/technicallyCompleteProductionOrder.ts +106 -0
  101. package/src/modules/manufacturing/command/unreleaseProductionOrder.generated.ts +6 -0
  102. package/src/modules/manufacturing/command/unreleaseProductionOrder.test.ts +140 -0
  103. package/src/modules/manufacturing/command/unreleaseProductionOrder.ts +131 -0
  104. package/src/modules/manufacturing/command/updateBillOfMaterial.generated.ts +6 -0
  105. package/src/modules/manufacturing/command/updateBillOfMaterial.test.ts +149 -0
  106. package/src/modules/manufacturing/command/updateBillOfMaterial.ts +174 -0
  107. package/src/modules/manufacturing/command/updateProductionOrder.generated.ts +6 -0
  108. package/src/modules/manufacturing/command/updateProductionOrder.test.ts +112 -0
  109. package/src/modules/manufacturing/command/updateProductionOrder.ts +145 -0
  110. package/src/modules/manufacturing/command/updateRouting.generated.ts +6 -0
  111. package/src/modules/manufacturing/command/updateRouting.test.ts +211 -0
  112. package/src/modules/manufacturing/command/updateRouting.ts +124 -0
  113. package/src/modules/manufacturing/command/updateWorkCenter.generated.ts +6 -0
  114. package/src/modules/manufacturing/command/updateWorkCenter.test.ts +152 -0
  115. package/src/modules/manufacturing/command/updateWorkCenter.ts +137 -0
  116. package/src/modules/manufacturing/db/.gitkeep +0 -0
  117. package/src/modules/manufacturing/db/billOfMaterial.ts +70 -0
  118. package/src/modules/manufacturing/db/billOfMaterialLine.ts +49 -0
  119. package/src/modules/manufacturing/db/costVarianceLine.ts +53 -0
  120. package/src/modules/manufacturing/db/manufacturingCostLine.ts +35 -0
  121. package/src/modules/manufacturing/db/manufacturingCostSettlementRecord.ts +39 -0
  122. package/src/modules/manufacturing/db/manufacturingCostSummary.ts +59 -0
  123. package/src/modules/manufacturing/db/productionOrder.ts +83 -0
  124. package/src/modules/manufacturing/db/productionOrderBomSnapshot.ts +44 -0
  125. package/src/modules/manufacturing/db/productionOrderCostBaseline.ts +44 -0
  126. package/src/modules/manufacturing/db/productionOrderMaterialRequirement.ts +57 -0
  127. package/src/modules/manufacturing/db/productionOrderRoutingSnapshot.ts +43 -0
  128. package/src/modules/manufacturing/db/routing.ts +63 -0
  129. package/src/modules/manufacturing/db/routingOperation.ts +57 -0
  130. package/src/modules/manufacturing/db/workCenter.ts +87 -0
  131. package/src/modules/manufacturing/db/workOrder.ts +65 -0
  132. package/src/modules/manufacturing/db/workOrderExecutionEvent.ts +54 -0
  133. package/src/modules/manufacturing/docs/commands/ActivateBillOfMaterial.md +50 -0
  134. package/src/modules/manufacturing/docs/commands/ActivateRouting.md +48 -0
  135. package/src/modules/manufacturing/docs/commands/ActivateWorkCenter.md +49 -0
  136. package/src/modules/manufacturing/docs/commands/CancelProductionOrder.md +48 -0
  137. package/src/modules/manufacturing/docs/commands/CloseProductionOrder.md +46 -0
  138. package/src/modules/manufacturing/docs/commands/CompleteProductionOrder.md +48 -0
  139. package/src/modules/manufacturing/docs/commands/CompleteWorkOrder.md +66 -0
  140. package/src/modules/manufacturing/docs/commands/CreateBillOfMaterial.md +54 -0
  141. package/src/modules/manufacturing/docs/commands/CreateProductionOrder.md +49 -0
  142. package/src/modules/manufacturing/docs/commands/CreateRouting.md +50 -0
  143. package/src/modules/manufacturing/docs/commands/CreateWorkCenter.md +51 -0
  144. package/src/modules/manufacturing/docs/commands/DeactivateBillOfMaterial.md +45 -0
  145. package/src/modules/manufacturing/docs/commands/DeactivateRouting.md +45 -0
  146. package/src/modules/manufacturing/docs/commands/DeactivateWorkCenter.md +45 -0
  147. package/src/modules/manufacturing/docs/commands/PauseWorkOrder.md +44 -0
  148. package/src/modules/manufacturing/docs/commands/RecordInventoryIssueOutcome.md +59 -0
  149. package/src/modules/manufacturing/docs/commands/RecordManufacturingCostSettlementAcknowledgment.md +49 -0
  150. package/src/modules/manufacturing/docs/commands/ReleaseProductionOrder.md +57 -0
  151. package/src/modules/manufacturing/docs/commands/ReopenProductionOrder.md +54 -0
  152. package/src/modules/manufacturing/docs/commands/ReportWorkOrderProgress.md +53 -0
  153. package/src/modules/manufacturing/docs/commands/RescheduleProductionOrder.md +45 -0
  154. package/src/modules/manufacturing/docs/commands/ResumeWorkOrder.md +44 -0
  155. package/src/modules/manufacturing/docs/commands/ReviewManufacturingCostSummary.md +52 -0
  156. package/src/modules/manufacturing/docs/commands/StartWorkOrder.md +46 -0
  157. package/src/modules/manufacturing/docs/commands/TechnicallyCompleteProductionOrder.md +51 -0
  158. package/src/modules/manufacturing/docs/commands/UnreleaseProductionOrder.md +46 -0
  159. package/src/modules/manufacturing/docs/commands/UpdateBillOfMaterial.md +48 -0
  160. package/src/modules/manufacturing/docs/commands/UpdateProductionOrder.md +48 -0
  161. package/src/modules/manufacturing/docs/commands/UpdateRouting.md +52 -0
  162. package/src/modules/manufacturing/docs/commands/UpdateWorkCenter.md +48 -0
  163. package/src/modules/manufacturing/docs/features/bill-of-material-management.md +83 -0
  164. package/src/modules/manufacturing/docs/features/manufacturing-cost-and-variance.md +191 -0
  165. package/src/modules/manufacturing/docs/features/production-order-lifecycle.md +103 -0
  166. package/src/modules/manufacturing/docs/features/routing-and-work-center-definition.md +63 -0
  167. package/src/modules/manufacturing/docs/features/work-order-execution.md +115 -0
  168. package/src/modules/manufacturing/docs/models/BillOfMaterial.md +60 -0
  169. package/src/modules/manufacturing/docs/models/ManufacturingCostSummary.md +66 -0
  170. package/src/modules/manufacturing/docs/models/ProductionOrder.md +76 -0
  171. package/src/modules/manufacturing/docs/models/Routing.md +58 -0
  172. package/src/modules/manufacturing/docs/models/WorkCenter.md +56 -0
  173. package/src/modules/manufacturing/docs/models/WorkOrder.md +63 -0
  174. package/src/modules/manufacturing/docs/queries/DetectBillOfMaterialCircularReference.md +39 -0
  175. package/src/modules/manufacturing/docs/queries/ExplodeBillOfMaterial.md +56 -0
  176. package/src/modules/manufacturing/docs/queries/GetBillOfMaterial.md +37 -0
  177. package/src/modules/manufacturing/docs/queries/GetManufacturingCostSummary.md +39 -0
  178. package/src/modules/manufacturing/docs/queries/GetProductionOrder.md +37 -0
  179. package/src/modules/manufacturing/docs/queries/GetRouting.md +39 -0
  180. package/src/modules/manufacturing/docs/queries/GetWorkCenter.md +35 -0
  181. package/src/modules/manufacturing/docs/queries/GetWorkOrder.md +38 -0
  182. package/src/modules/manufacturing/docs/queries/ListBillOfMaterialsByItem.md +42 -0
  183. package/src/modules/manufacturing/docs/queries/ListManufacturingCostSummariesByStatus.md +41 -0
  184. package/src/modules/manufacturing/docs/queries/ListProductionOrdersByStatus.md +41 -0
  185. package/src/modules/manufacturing/docs/queries/ListRoutingsByItem.md +42 -0
  186. package/src/modules/manufacturing/docs/queries/ListWorkCentersBySite.md +38 -0
  187. package/src/modules/manufacturing/docs/queries/ListWorkOrdersByProductionOrder.md +39 -0
  188. package/src/modules/manufacturing/docs/queries/ListWorkOrdersByWorkCenter.md +43 -0
  189. package/src/modules/manufacturing/executor/.gitkeep +0 -0
  190. package/src/modules/manufacturing/generated/enums.ts +113 -0
  191. package/src/modules/manufacturing/generated/kysely-tailordb.ts +247 -0
  192. package/src/modules/manufacturing/index.ts +2 -0
  193. package/src/modules/manufacturing/lib/_db_deps.ts +22 -0
  194. package/src/modules/manufacturing/lib/errors.generated.ts +592 -0
  195. package/src/modules/manufacturing/lib/permissions.generated.ts +35 -0
  196. package/src/modules/manufacturing/lib/types.ts +111 -0
  197. package/src/modules/manufacturing/module.ts +226 -0
  198. package/src/modules/manufacturing/permissions.ts +3 -0
  199. package/src/modules/manufacturing/query/.gitkeep +0 -0
  200. package/src/modules/manufacturing/query/detectBillOfMaterialCircularReference.generated.ts +5 -0
  201. package/src/modules/manufacturing/query/detectBillOfMaterialCircularReference.test.ts +115 -0
  202. package/src/modules/manufacturing/query/detectBillOfMaterialCircularReference.ts +79 -0
  203. package/src/modules/manufacturing/query/explodeBillOfMaterial.generated.ts +5 -0
  204. package/src/modules/manufacturing/query/explodeBillOfMaterial.test.ts +445 -0
  205. package/src/modules/manufacturing/query/explodeBillOfMaterial.ts +306 -0
  206. package/src/modules/manufacturing/query/getBillOfMaterial.generated.ts +5 -0
  207. package/src/modules/manufacturing/query/getBillOfMaterial.test.ts +64 -0
  208. package/src/modules/manufacturing/query/getBillOfMaterial.ts +27 -0
  209. package/src/modules/manufacturing/query/getManufacturingCostSummary.generated.ts +5 -0
  210. package/src/modules/manufacturing/query/getManufacturingCostSummary.test.ts +147 -0
  211. package/src/modules/manufacturing/query/getManufacturingCostSummary.ts +46 -0
  212. package/src/modules/manufacturing/query/getProductionOrder.generated.ts +5 -0
  213. package/src/modules/manufacturing/query/getProductionOrder.test.ts +139 -0
  214. package/src/modules/manufacturing/query/getProductionOrder.ts +84 -0
  215. package/src/modules/manufacturing/query/getRouting.generated.ts +5 -0
  216. package/src/modules/manufacturing/query/getRouting.test.ts +71 -0
  217. package/src/modules/manufacturing/query/getRouting.ts +34 -0
  218. package/src/modules/manufacturing/query/getWorkCenter.generated.ts +5 -0
  219. package/src/modules/manufacturing/query/getWorkCenter.test.ts +37 -0
  220. package/src/modules/manufacturing/query/getWorkCenter.ts +21 -0
  221. package/src/modules/manufacturing/query/getWorkOrder.generated.ts +5 -0
  222. package/src/modules/manufacturing/query/getWorkOrder.test.ts +73 -0
  223. package/src/modules/manufacturing/query/getWorkOrder.ts +28 -0
  224. package/src/modules/manufacturing/query/listBillOfMaterialsByItem.generated.ts +5 -0
  225. package/src/modules/manufacturing/query/listBillOfMaterialsByItem.test.ts +107 -0
  226. package/src/modules/manufacturing/query/listBillOfMaterialsByItem.ts +58 -0
  227. package/src/modules/manufacturing/query/listManufacturingCostSummariesByStatus.generated.ts +5 -0
  228. package/src/modules/manufacturing/query/listManufacturingCostSummariesByStatus.test.ts +96 -0
  229. package/src/modules/manufacturing/query/listManufacturingCostSummariesByStatus.ts +77 -0
  230. package/src/modules/manufacturing/query/listProductionOrdersByStatus.generated.ts +5 -0
  231. package/src/modules/manufacturing/query/listProductionOrdersByStatus.test.ts +121 -0
  232. package/src/modules/manufacturing/query/listProductionOrdersByStatus.ts +83 -0
  233. package/src/modules/manufacturing/query/listRoutingsByItem.generated.ts +5 -0
  234. package/src/modules/manufacturing/query/listRoutingsByItem.test.ts +110 -0
  235. package/src/modules/manufacturing/query/listRoutingsByItem.ts +54 -0
  236. package/src/modules/manufacturing/query/listWorkCentersBySite.generated.ts +5 -0
  237. package/src/modules/manufacturing/query/listWorkCentersBySite.test.ts +81 -0
  238. package/src/modules/manufacturing/query/listWorkCentersBySite.ts +70 -0
  239. package/src/modules/manufacturing/query/listWorkOrdersByProductionOrder.generated.ts +5 -0
  240. package/src/modules/manufacturing/query/listWorkOrdersByProductionOrder.test.ts +102 -0
  241. package/src/modules/manufacturing/query/listWorkOrdersByProductionOrder.ts +53 -0
  242. package/src/modules/manufacturing/query/listWorkOrdersByWorkCenter.generated.ts +5 -0
  243. package/src/modules/manufacturing/query/listWorkOrdersByWorkCenter.test.ts +143 -0
  244. package/src/modules/manufacturing/query/listWorkOrdersByWorkCenter.ts +56 -0
  245. package/src/modules/manufacturing/seed/index.ts +19 -0
  246. package/src/modules/manufacturing/tailor.config.ts +13 -0
  247. package/src/modules/manufacturing/tailor.d.ts +13 -0
  248. package/src/modules/manufacturing/testing/commandTestUtils.ts +29 -0
  249. package/src/modules/manufacturing/testing/fixtures.ts +402 -0
  250. package/templates/scaffold/app/backend/package.json +9 -2
  251. package/templates/scaffold/app/backend/src/tests/utils/graphql-client.ts +66 -0
  252. package/templates/scaffold/app/backend/src/tests/utils/setup.ts +21 -0
  253. package/templates/scaffold/app/backend/tsconfig.json +9 -2
  254. package/templates/scaffold/app/backend/vitest.config.ts +35 -0
  255. package/templates/scaffold/app/frontend/package.json +2 -2
  256. package/templates/scaffold/module/__dot__gitignore +3 -0
  257. package/templates/scaffold/module/eslint.config.js +31 -0
  258. package/templates/scaffold/module/generated/kysely-tailordb.ts +3 -0
  259. package/templates/scaffold/module/lib/types.ts +1 -6
  260. package/templates/scaffold/module/package.json +26 -0
  261. package/templates/scaffold/module/tsconfig.json +16 -0
  262. /package/{templates/scaffold/module/generated → src/modules/manufacturing/command}/.gitkeep +0 -0
@@ -0,0 +1,450 @@
1
+ import type { Transaction } from "../generated/kysely-tailordb";
2
+ import {
3
+ ProductionOrderNotFoundError,
4
+ ProductionOrderNotReleasableError,
5
+ BomNotResolvedError,
6
+ RoutingNotResolvedError,
7
+ CrossCompanyMasterReferenceError,
8
+ CrossSiteMasterReferenceError,
9
+ PlannedMaterialCostUnavailableError,
10
+ } from "../lib/errors.generated";
11
+ import { ok, err, type CommandContext } from "@tailor-platform/erp-kit/module";
12
+
13
+ export interface ReleaseProductionOrderInput {
14
+ id: string;
15
+ from?: string[];
16
+ }
17
+
18
+ interface ReleaseMaterialValuationSnapshot {
19
+ plannedUnitCost: number;
20
+ currencyCode: string;
21
+ valuationMethod: string;
22
+ sourceValuationReference: string | null;
23
+ }
24
+
25
+ function getOptionalNumber(record: Record<string, unknown>, key: string): number | null {
26
+ const value = record[key];
27
+ return typeof value === "number" && Number.isFinite(value) ? value : null;
28
+ }
29
+
30
+ function getOptionalString(record: Record<string, unknown>, key: string): string | null {
31
+ const value = record[key];
32
+ return typeof value === "string" && value.length > 0 ? value : null;
33
+ }
34
+
35
+ function resolveReleaseMaterialValuation(
36
+ itemRecord: Record<string, unknown> | undefined,
37
+ ): ReleaseMaterialValuationSnapshot | null {
38
+ if (!itemRecord) {
39
+ return null;
40
+ }
41
+
42
+ const valuationMethod =
43
+ getOptionalString(itemRecord, "valuationMethod") ??
44
+ getOptionalString(itemRecord, "costingMethod") ??
45
+ (getOptionalNumber(itemRecord, "standardCostRate") != null ? "STANDARD_COST" : null) ??
46
+ (getOptionalNumber(itemRecord, "costPerUnit") != null ? "AVCO" : null);
47
+
48
+ if (!valuationMethod) {
49
+ return null;
50
+ }
51
+
52
+ const plannedUnitCost =
53
+ valuationMethod === "STANDARD_COST"
54
+ ? (getOptionalNumber(itemRecord, "standardCostRate") ??
55
+ getOptionalNumber(itemRecord, "costPerUnit"))
56
+ : (getOptionalNumber(itemRecord, "costPerUnit") ??
57
+ getOptionalNumber(itemRecord, "standardCostRate"));
58
+
59
+ if (plannedUnitCost == null) {
60
+ return null;
61
+ }
62
+
63
+ return {
64
+ plannedUnitCost,
65
+ currencyCode:
66
+ getOptionalString(itemRecord, "currencyCode") ??
67
+ getOptionalString(itemRecord, "valuationCurrency") ??
68
+ "USD",
69
+ valuationMethod,
70
+ sourceValuationReference:
71
+ getOptionalString(itemRecord, "valuationReference") ??
72
+ getOptionalString(itemRecord, "valuationPolicyId") ??
73
+ getOptionalString(itemRecord, "itemValuationId"),
74
+ };
75
+ }
76
+
77
+ /**
78
+ * Function: releaseProductionOrder
79
+ *
80
+ * Turns a draft plan into executable work by resolving active BOM and routing
81
+ * content, freezing snapshots, creating work orders and material requirements,
82
+ * and opening manufacturing cost collection.
83
+ */
84
+ export async function run<CF extends Record<string, unknown>>(
85
+ db: Transaction,
86
+ input: ReleaseProductionOrderInput & CF,
87
+ _ctx: CommandContext,
88
+ ) {
89
+ const { id, from, ...customFields } = input;
90
+ void customFields;
91
+
92
+ const allowedStatuses = from ?? ["DRAFT"];
93
+ const now = new Date();
94
+
95
+ // 1. Fetch production order with lock
96
+ const order = await db
97
+ .selectFrom("ProductionOrder")
98
+ .selectAll()
99
+ .where("id", "=", id)
100
+ .forUpdate()
101
+ .executeTakeFirst();
102
+
103
+ if (!order) {
104
+ return err(new ProductionOrderNotFoundError(id));
105
+ }
106
+
107
+ // 2. Validate status is releasable
108
+ if (!allowedStatuses.includes(order.status)) {
109
+ return err(new ProductionOrderNotReleasableError(id));
110
+ }
111
+
112
+ // 3. Resolve active BOM
113
+ let bom;
114
+ if (order.selectedBomVersionId) {
115
+ bom = await db
116
+ .selectFrom("BillOfMaterial")
117
+ .selectAll()
118
+ .where("id", "=", order.selectedBomVersionId)
119
+ .where("status", "=", "ACTIVE")
120
+ .executeTakeFirst();
121
+ } else {
122
+ bom = await db
123
+ .selectFrom("BillOfMaterial")
124
+ .selectAll()
125
+ .where("parentItemId", "=", order.orderedItemId)
126
+ .where("companyId", "=", order.companyId)
127
+ .where("status", "=", "ACTIVE")
128
+ .executeTakeFirst();
129
+ }
130
+
131
+ if (!bom) {
132
+ return err(new BomNotResolvedError(id));
133
+ }
134
+
135
+ // 4. Validate BOM company scope
136
+ if (bom.companyId !== order.companyId) {
137
+ return err(new CrossCompanyMasterReferenceError(id));
138
+ }
139
+
140
+ // 5. Validate BOM site scope
141
+ if (bom.siteId != null && bom.siteId !== order.siteId) {
142
+ return err(new CrossSiteMasterReferenceError(id));
143
+ }
144
+
145
+ // 6. Resolve active routing
146
+ let routing;
147
+ if (order.selectedRoutingRevisionId) {
148
+ routing = await db
149
+ .selectFrom("Routing")
150
+ .selectAll()
151
+ .where("id", "=", order.selectedRoutingRevisionId)
152
+ .where("status", "=", "ACTIVE")
153
+ .executeTakeFirst();
154
+ } else {
155
+ routing = await db
156
+ .selectFrom("Routing")
157
+ .selectAll()
158
+ .where("parentItemId", "=", order.orderedItemId)
159
+ .where("companyId", "=", order.companyId)
160
+ .where("status", "=", "ACTIVE")
161
+ .executeTakeFirst();
162
+ }
163
+
164
+ if (!routing) {
165
+ return err(new RoutingNotResolvedError(id));
166
+ }
167
+
168
+ // 7. Validate routing company scope
169
+ if (routing.companyId !== order.companyId) {
170
+ return err(new CrossCompanyMasterReferenceError(id));
171
+ }
172
+
173
+ // 8. Validate routing site scope
174
+ if (routing.siteId != null && routing.siteId !== order.siteId) {
175
+ return err(new CrossSiteMasterReferenceError(id));
176
+ }
177
+
178
+ // 9. Fetch BOM lines for material requirements
179
+ const bomLines = await db
180
+ .selectFrom("BillOfMaterialLine")
181
+ .selectAll()
182
+ .where("billOfMaterialId", "=", bom.id)
183
+ .execute();
184
+
185
+ // 10. Resolve planned material cost for each component
186
+ let totalPlannedMaterialCost = 0;
187
+ let baselineCurrencyCode = "USD";
188
+ const materialRequirements: {
189
+ itemId: string;
190
+ requiredQuantity: number;
191
+ unitOfMeasure: string | null;
192
+ plannedUnitCost: number;
193
+ valuationMethod: string;
194
+ valuationCurrencyCode: string;
195
+ sourceValuationReference: string | null;
196
+ }[] = [];
197
+
198
+ for (const line of bomLines) {
199
+ // Resolve release-time valuation from inventory-owned item valuation context.
200
+ const itemValuationRecord = (await db
201
+ .selectFrom("Item")
202
+ .selectAll()
203
+ .where("id", "=", line.itemId)
204
+ .executeTakeFirst()) as Record<string, unknown> | undefined;
205
+ const valuation = resolveReleaseMaterialValuation(itemValuationRecord);
206
+
207
+ if (!valuation) {
208
+ return err(new PlannedMaterialCostUnavailableError(line.itemId));
209
+ }
210
+
211
+ const requiredQuantity = line.requiredQuantity * order.plannedQuantity;
212
+ totalPlannedMaterialCost += valuation.plannedUnitCost * requiredQuantity;
213
+ baselineCurrencyCode = valuation.currencyCode;
214
+
215
+ materialRequirements.push({
216
+ itemId: line.itemId,
217
+ requiredQuantity,
218
+ unitOfMeasure: line.unitOfMeasure ?? null,
219
+ plannedUnitCost: valuation.plannedUnitCost,
220
+ valuationMethod: valuation.valuationMethod,
221
+ valuationCurrencyCode: valuation.currencyCode,
222
+ sourceValuationReference: valuation.sourceValuationReference,
223
+ });
224
+ }
225
+
226
+ // 11. Fetch routing operations for work orders
227
+ const operations = await db
228
+ .selectFrom("RoutingOperation")
229
+ .selectAll()
230
+ .where("routingId", "=", routing.id)
231
+ .execute();
232
+
233
+ // 12. Calculate planned labor and machine costs from operations and work centers
234
+ let totalPlannedLaborCost = 0;
235
+ let totalPlannedMachineCost = 0;
236
+ let totalPlannedOverheadCost = 0;
237
+ const workCenterSnapshots = new Map<
238
+ string,
239
+ {
240
+ workCenterId: string;
241
+ code: string | null;
242
+ laborRate: number | null;
243
+ machineRate: number | null;
244
+ overheadAbsorptionMethod: string | null;
245
+ overheadAbsorptionRate: number | null;
246
+ overheadAbsorptionCurrency: string | null;
247
+ }
248
+ >();
249
+
250
+ for (const op of operations) {
251
+ const wc = await db
252
+ .selectFrom("WorkCenter")
253
+ .selectAll()
254
+ .where("id", "=", op.workCenterId)
255
+ .executeTakeFirst();
256
+
257
+ if (wc) {
258
+ const duration = op.standardSetupTime + op.standardRunTime * order.plannedQuantity;
259
+ const laborCost = (wc.laborRate ?? 0) * duration;
260
+ const machineCost = (wc.machineRate ?? 0) * duration;
261
+ totalPlannedLaborCost += laborCost;
262
+ totalPlannedMachineCost += machineCost;
263
+
264
+ const wcRecord = wc as Record<string, unknown>;
265
+ const overheadRate = getOptionalNumber(wcRecord, "overheadAbsorptionRate");
266
+ if (wc.overheadAbsorptionMethod === "PERCENT_OF_LABOR_COST" && overheadRate != null) {
267
+ totalPlannedOverheadCost += laborCost * (overheadRate / 100);
268
+ } else if (
269
+ wc.overheadAbsorptionMethod === "PERCENT_OF_MACHINE_COST" &&
270
+ overheadRate != null
271
+ ) {
272
+ totalPlannedOverheadCost += machineCost * (overheadRate / 100);
273
+ } else if (
274
+ wc.overheadAbsorptionMethod === "FIXED_AMOUNT_PER_GOOD_UNIT" &&
275
+ overheadRate != null
276
+ ) {
277
+ totalPlannedOverheadCost += order.plannedQuantity * overheadRate;
278
+ }
279
+
280
+ workCenterSnapshots.set(op.workCenterId, {
281
+ workCenterId: wc.id,
282
+ code: wc.code,
283
+ laborRate: wc.laborRate ?? null,
284
+ machineRate: wc.machineRate ?? null,
285
+ overheadAbsorptionMethod: wc.overheadAbsorptionMethod ?? null,
286
+ overheadAbsorptionRate: overheadRate,
287
+ overheadAbsorptionCurrency: wc.overheadAbsorptionCurrency ?? null,
288
+ });
289
+ }
290
+ }
291
+
292
+ // 13. Create BOM snapshot
293
+ await db
294
+ .insertInto("ProductionOrderBomSnapshot")
295
+ .values({
296
+ productionOrderId: order.id,
297
+ parentItemId: bom.parentItemId,
298
+ bomType: bom.bomType,
299
+ snapshotData: JSON.stringify({
300
+ bomId: bom.id,
301
+ revisionNumber: bom.revisionNumber,
302
+ companyId: bom.companyId,
303
+ siteId: bom.siteId,
304
+ effectivityStartDate: bom.effectivityStartDate,
305
+ effectivityEndDate: bom.effectivityEndDate,
306
+ defaultSelection: bom.defaultSelection,
307
+ lines: bomLines.map((line) => {
308
+ const requirement = materialRequirements.find((req) => req.itemId === line.itemId);
309
+ return {
310
+ itemId: line.itemId,
311
+ requiredQuantity: line.requiredQuantity,
312
+ unitOfMeasure: line.unitOfMeasure,
313
+ scrapAssumption: line.scrapAssumption,
314
+ isSubassembly: line.isSubassembly,
315
+ plannedUnitCost: requirement?.plannedUnitCost ?? null,
316
+ currencyCode: requirement?.valuationCurrencyCode ?? null,
317
+ valuationMethod: requirement?.valuationMethod ?? null,
318
+ sourceValuationReference: requirement?.sourceValuationReference ?? null,
319
+ };
320
+ }),
321
+ }),
322
+ createdAt: now,
323
+ updatedAt: null,
324
+ })
325
+ .returningAll()
326
+ .executeTakeFirst();
327
+
328
+ // 14. Create routing snapshot
329
+ await db
330
+ .insertInto("ProductionOrderRoutingSnapshot")
331
+ .values({
332
+ productionOrderId: order.id,
333
+ routingId: routing.id,
334
+ snapshotData: JSON.stringify({
335
+ routingId: routing.id,
336
+ revisionNumber: routing.revisionNumber,
337
+ companyId: routing.companyId,
338
+ siteId: routing.siteId,
339
+ operations: operations.map((op) => ({
340
+ id: op.id,
341
+ sequenceNumber: op.sequenceNumber,
342
+ operationDescription: op.operationDescription,
343
+ workCenterId: op.workCenterId,
344
+ standardSetupTime: op.standardSetupTime,
345
+ standardRunTime: op.standardRunTime,
346
+ operatorInstructions: op.operatorInstructions,
347
+ workCenterSnapshot: workCenterSnapshots.get(op.workCenterId) ?? null,
348
+ })),
349
+ }),
350
+ createdAt: now,
351
+ updatedAt: null,
352
+ })
353
+ .returningAll()
354
+ .executeTakeFirst();
355
+
356
+ // 15. Create cost baseline
357
+ await db
358
+ .insertInto("ProductionOrderCostBaseline")
359
+ .values({
360
+ productionOrderId: order.id,
361
+ plannedMaterialCost: totalPlannedMaterialCost,
362
+ plannedLaborCost: totalPlannedLaborCost,
363
+ plannedMachineCost: totalPlannedMachineCost,
364
+ plannedOverheadCost: totalPlannedOverheadCost,
365
+ currencyCode: baselineCurrencyCode,
366
+ createdAt: now,
367
+ updatedAt: null,
368
+ })
369
+ .returningAll()
370
+ .executeTakeFirst();
371
+
372
+ // 16. Create material requirements
373
+ for (const matReq of materialRequirements) {
374
+ await db
375
+ .insertInto("ProductionOrderMaterialRequirement")
376
+ .values({
377
+ productionOrderId: order.id,
378
+ itemId: matReq.itemId,
379
+ requiredQuantity: matReq.requiredQuantity,
380
+ unitOfMeasure: matReq.unitOfMeasure,
381
+ plannedUnitCost: matReq.plannedUnitCost,
382
+ createdAt: now,
383
+ updatedAt: null,
384
+ })
385
+ .returningAll()
386
+ .executeTakeFirst();
387
+ }
388
+
389
+ // 17. Create work orders from routing operations
390
+ for (const op of operations) {
391
+ await db
392
+ .insertInto("WorkOrder")
393
+ .values({
394
+ productionOrderId: order.id,
395
+ routingOperationSequenceNumber: op.sequenceNumber,
396
+ workCenterId: op.workCenterId,
397
+ plannedQuantity: order.plannedQuantity,
398
+ completedQuantity: 0,
399
+ scrapQuantity: 0,
400
+ actualSetupTime: 0,
401
+ actualRunTime: 0,
402
+ actualStartDate: null,
403
+ pauseReason: null,
404
+ executionNotes: null,
405
+ status: "PENDING",
406
+ createdAt: now,
407
+ updatedAt: null,
408
+ })
409
+ .returningAll()
410
+ .executeTakeFirst();
411
+ }
412
+
413
+ // 18. Create manufacturing cost summary in COLLECTING status
414
+ await db
415
+ .insertInto("ManufacturingCostSummary")
416
+ .values({
417
+ productionOrderId: order.id,
418
+ plannedMaterialCost: totalPlannedMaterialCost,
419
+ plannedLaborCost: totalPlannedLaborCost,
420
+ plannedMachineCost: totalPlannedMachineCost,
421
+ plannedOverheadCost: totalPlannedOverheadCost,
422
+ actualMaterialCost: 0,
423
+ actualLaborCost: 0,
424
+ actualMachineCost: 0,
425
+ actualOverheadCost: 0,
426
+ currencyCode: baselineCurrencyCode,
427
+ reviewedDate: null,
428
+ reviewerNotes: null,
429
+ status: "COLLECTING",
430
+ createdAt: now,
431
+ updatedAt: null,
432
+ })
433
+ .returningAll()
434
+ .executeTakeFirst();
435
+
436
+ // 19. Update order status to RELEASED with selected BOM/routing IDs
437
+ const releasedOrder = await db
438
+ .updateTable("ProductionOrder")
439
+ .set({
440
+ selectedBomVersionId: bom.id,
441
+ selectedRoutingRevisionId: routing.id,
442
+ status: "RELEASED",
443
+ updatedAt: now,
444
+ })
445
+ .where("id", "=", id)
446
+ .returningAll()
447
+ .executeTakeFirstOrThrow();
448
+
449
+ return ok({ productionOrder: releasedOrder });
450
+ }
@@ -0,0 +1,6 @@
1
+ // @generated — do not edit
2
+ import { permissions } from "../lib/permissions.generated";
3
+ import { run } from "./reopenProductionOrder";
4
+ import { defineCommand } from "@tailor-platform/erp-kit/module";
5
+
6
+ export const reopenProductionOrder = defineCommand(permissions.reopenProductionOrder, run);
@@ -0,0 +1,196 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { createMockDb } from "../../../testing/index";
3
+ import type { Transaction } from "../generated/kysely-tailordb";
4
+ import {
5
+ ProductionOrderNotFoundError,
6
+ ProductionOrderNotReopenableError,
7
+ ReopenReasonRequiredError,
8
+ CostSummaryNotReopenableError,
9
+ OrderAlreadyClosedError,
10
+ } from "../lib/errors.generated";
11
+ import {
12
+ baseTechCompleteProductionOrder,
13
+ baseCompletedProductionOrder,
14
+ baseClosedProductionOrder,
15
+ basePendingReviewCostSummary,
16
+ baseReviewedCostSummary,
17
+ baseSettledCostSummary,
18
+ } from "../testing/fixtures";
19
+ import { run } from "./reopenProductionOrder";
20
+ import type { CommandContext } from "@tailor-platform/erp-kit/module";
21
+
22
+ describe("reopenProductionOrder", () => {
23
+ const ctx: CommandContext = {
24
+ actorId: "test-actor",
25
+ permissions: ["manufacturing:reopenProductionOrder"],
26
+ };
27
+
28
+ const validInput = {
29
+ id: baseTechCompleteProductionOrder.id,
30
+ reason: "Additional rework needed for quality defects",
31
+ };
32
+
33
+ it("reopens a technically complete order for additional execution", async () => {
34
+ const { db, spies } = createMockDb<Transaction>();
35
+ const reopened = { ...baseTechCompleteProductionOrder, status: "IN_PROGRESS" as const };
36
+
37
+ // order lookup
38
+ spies.select.mockReturnValueOnce(baseTechCompleteProductionOrder);
39
+ // cost summary in PENDING_VARIANCE_REVIEW
40
+ spies.select.mockReturnValueOnce({
41
+ ...basePendingReviewCostSummary,
42
+ productionOrderId: baseTechCompleteProductionOrder.id,
43
+ });
44
+ // update cost summary
45
+ spies.update.mockReturnValueOnce(undefined);
46
+ // update order status
47
+ spies.update.mockReturnValue(reopened);
48
+
49
+ const result = await run(db, validInput, ctx);
50
+
51
+ expect(result.ok).toBe(true);
52
+ if (result.ok) {
53
+ expect(result.value.productionOrder.status).toBe("IN_PROGRESS");
54
+ }
55
+ expect(spies.update).toHaveBeenCalled();
56
+ });
57
+
58
+ it("returns error when the order does not exist", async () => {
59
+ const { db, spies } = createMockDb<Transaction>();
60
+ spies.select.mockReturnValueOnce(undefined);
61
+
62
+ const result = await run(db, { id: "nonexistent", reason: "rework" }, ctx);
63
+
64
+ expect(result.ok).toBe(false);
65
+ if (!result.ok) {
66
+ expect(result.error).toBeInstanceOf(ProductionOrderNotFoundError);
67
+ }
68
+ });
69
+
70
+ it("returns error when the order is not technically complete", async () => {
71
+ const { db, spies } = createMockDb<Transaction>();
72
+ spies.select.mockReturnValueOnce(baseCompletedProductionOrder);
73
+
74
+ const result = await run(db, { id: baseCompletedProductionOrder.id, reason: "rework" }, ctx);
75
+
76
+ expect(result.ok).toBe(false);
77
+ if (!result.ok) {
78
+ expect(result.error).toBeInstanceOf(ProductionOrderNotReopenableError);
79
+ }
80
+ });
81
+
82
+ it("returns error when no reopen reason is provided", async () => {
83
+ const { db, spies } = createMockDb<Transaction>();
84
+ spies.select.mockReturnValueOnce(baseTechCompleteProductionOrder);
85
+
86
+ const result = await run(db, { id: baseTechCompleteProductionOrder.id, reason: "" }, ctx);
87
+
88
+ expect(result.ok).toBe(false);
89
+ if (!result.ok) {
90
+ expect(result.error).toBeInstanceOf(ReopenReasonRequiredError);
91
+ }
92
+ });
93
+
94
+ it("reopens an order when the linked cost summary is PENDING_VARIANCE_REVIEW", async () => {
95
+ const { db, spies } = createMockDb<Transaction>();
96
+ const reopened = { ...baseTechCompleteProductionOrder, status: "IN_PROGRESS" as const };
97
+
98
+ // order lookup
99
+ spies.select.mockReturnValueOnce(baseTechCompleteProductionOrder);
100
+ // cost summary in PENDING_VARIANCE_REVIEW
101
+ spies.select.mockReturnValueOnce({
102
+ ...basePendingReviewCostSummary,
103
+ productionOrderId: baseTechCompleteProductionOrder.id,
104
+ });
105
+ // update cost summary
106
+ spies.update.mockReturnValueOnce(undefined);
107
+ // update order status
108
+ spies.update.mockReturnValue(reopened);
109
+
110
+ const result = await run(db, validInput, ctx);
111
+
112
+ expect(result.ok).toBe(true);
113
+ if (result.ok) {
114
+ expect(result.value.productionOrder.status).toBe("IN_PROGRESS");
115
+ }
116
+ });
117
+
118
+ it("reopens an order when the linked cost summary is VARIANCE_REVIEWED", async () => {
119
+ const { db, spies } = createMockDb<Transaction>();
120
+ const reopened = { ...baseTechCompleteProductionOrder, status: "IN_PROGRESS" as const };
121
+
122
+ // order lookup
123
+ spies.select.mockReturnValueOnce(baseTechCompleteProductionOrder);
124
+ // cost summary in VARIANCE_REVIEWED
125
+ spies.select.mockReturnValueOnce({
126
+ ...baseReviewedCostSummary,
127
+ productionOrderId: baseTechCompleteProductionOrder.id,
128
+ });
129
+ // update cost summary
130
+ spies.update.mockReturnValueOnce(undefined);
131
+ // update order status
132
+ spies.update.mockReturnValue(reopened);
133
+
134
+ const result = await run(db, validInput, ctx);
135
+
136
+ expect(result.ok).toBe(true);
137
+ if (result.ok) {
138
+ expect(result.value.productionOrder.status).toBe("IN_PROGRESS");
139
+ }
140
+ });
141
+
142
+ it("returns error when the linked cost summary is already SETTLED or otherwise not reopenable", async () => {
143
+ const { db, spies } = createMockDb<Transaction>();
144
+
145
+ // order lookup
146
+ spies.select.mockReturnValueOnce(baseTechCompleteProductionOrder);
147
+ // cost summary already SETTLED
148
+ spies.select.mockReturnValueOnce({
149
+ ...baseSettledCostSummary,
150
+ productionOrderId: baseTechCompleteProductionOrder.id,
151
+ });
152
+
153
+ const result = await run(db, validInput, ctx);
154
+
155
+ expect(result.ok).toBe(false);
156
+ if (!result.ok) {
157
+ expect(result.error).toBeInstanceOf(CostSummaryNotReopenableError);
158
+ }
159
+ });
160
+
161
+ it("returns error when the order is already closed", async () => {
162
+ const { db, spies } = createMockDb<Transaction>();
163
+ spies.select.mockReturnValueOnce(baseClosedProductionOrder);
164
+
165
+ const result = await run(db, { id: baseClosedProductionOrder.id, reason: "rework" }, ctx);
166
+
167
+ expect(result.ok).toBe(false);
168
+ if (!result.ok) {
169
+ expect(result.error).toBeInstanceOf(OrderAlreadyClosedError);
170
+ }
171
+ });
172
+
173
+ it("returns the linked cost summary to collecting on reopen", async () => {
174
+ const { db, spies } = createMockDb<Transaction>();
175
+ const reopened = { ...baseTechCompleteProductionOrder, status: "IN_PROGRESS" as const };
176
+
177
+ // order lookup
178
+ spies.select.mockReturnValueOnce(baseTechCompleteProductionOrder);
179
+ // cost summary in PENDING_VARIANCE_REVIEW
180
+ spies.select.mockReturnValueOnce({
181
+ ...basePendingReviewCostSummary,
182
+ productionOrderId: baseTechCompleteProductionOrder.id,
183
+ });
184
+ // update cost summary to COLLECTING
185
+ spies.update.mockReturnValueOnce(undefined);
186
+ // update order status
187
+ spies.update.mockReturnValue(reopened);
188
+
189
+ const result = await run(db, validInput, ctx);
190
+
191
+ expect(result.ok).toBe(true);
192
+ // The first update call should be for cost summary (back to COLLECTING)
193
+ // The second update call should be for order status (to IN_PROGRESS)
194
+ expect(spies.update).toHaveBeenCalledTimes(2);
195
+ });
196
+ });