@tailor-platform/erp-kit 0.5.1 → 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 (251) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/dist/cli.mjs +103 -23
  3. package/package.json +1 -1
  4. package/skills/erp-kit-app-5-impl-backend/SKILL.md +7 -4
  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/lib/discovery.test.ts +13 -3
  9. package/src/commands/lib/discovery.ts +10 -2
  10. package/src/commands/lib/paths.ts +4 -2
  11. package/src/commands/lib/sync-check-tests.test.ts +84 -6
  12. package/src/commands/lib/sync-check-tests.ts +63 -3
  13. package/src/commands/sync-check.ts +7 -3
  14. package/src/generator/generate-app-code.ts +51 -16
  15. package/src/generator/generate-stubs.ts +4 -0
  16. package/src/generator/stub-templates.test.ts +11 -0
  17. package/src/generator/stub-templates.ts +22 -1
  18. package/src/modules/inventory/docs/features/inventory-adjustment.md +2 -1
  19. package/src/modules/inventory/docs/features/scrap-management.md +39 -1
  20. package/src/modules/manufacturing/README.md +63 -0
  21. package/src/modules/manufacturing/command/.gitkeep +0 -0
  22. package/src/modules/manufacturing/command/activateBillOfMaterial.generated.ts +6 -0
  23. package/src/modules/manufacturing/command/activateBillOfMaterial.test.ts +166 -0
  24. package/src/modules/manufacturing/command/activateBillOfMaterial.ts +173 -0
  25. package/src/modules/manufacturing/command/activateRouting.generated.ts +6 -0
  26. package/src/modules/manufacturing/command/activateRouting.test.ts +152 -0
  27. package/src/modules/manufacturing/command/activateRouting.ts +92 -0
  28. package/src/modules/manufacturing/command/activateWorkCenter.generated.ts +6 -0
  29. package/src/modules/manufacturing/command/activateWorkCenter.test.ts +135 -0
  30. package/src/modules/manufacturing/command/activateWorkCenter.ts +91 -0
  31. package/src/modules/manufacturing/command/cancelProductionOrder.generated.ts +6 -0
  32. package/src/modules/manufacturing/command/cancelProductionOrder.test.ts +151 -0
  33. package/src/modules/manufacturing/command/cancelProductionOrder.ts +114 -0
  34. package/src/modules/manufacturing/command/closeProductionOrder.generated.ts +6 -0
  35. package/src/modules/manufacturing/command/closeProductionOrder.test.ts +126 -0
  36. package/src/modules/manufacturing/command/closeProductionOrder.ts +87 -0
  37. package/src/modules/manufacturing/command/completeProductionOrder.generated.ts +6 -0
  38. package/src/modules/manufacturing/command/completeProductionOrder.test.ts +132 -0
  39. package/src/modules/manufacturing/command/completeProductionOrder.ts +97 -0
  40. package/src/modules/manufacturing/command/completeWorkOrder.generated.ts +6 -0
  41. package/src/modules/manufacturing/command/completeWorkOrder.test.ts +369 -0
  42. package/src/modules/manufacturing/command/completeWorkOrder.ts +212 -0
  43. package/src/modules/manufacturing/command/createBillOfMaterial.generated.ts +6 -0
  44. package/src/modules/manufacturing/command/createBillOfMaterial.test.ts +210 -0
  45. package/src/modules/manufacturing/command/createBillOfMaterial.ts +176 -0
  46. package/src/modules/manufacturing/command/createProductionOrder.generated.ts +6 -0
  47. package/src/modules/manufacturing/command/createProductionOrder.test.ts +160 -0
  48. package/src/modules/manufacturing/command/createProductionOrder.ts +129 -0
  49. package/src/modules/manufacturing/command/createRouting.generated.ts +6 -0
  50. package/src/modules/manufacturing/command/createRouting.test.ts +168 -0
  51. package/src/modules/manufacturing/command/createRouting.ts +128 -0
  52. package/src/modules/manufacturing/command/createWorkCenter.generated.ts +6 -0
  53. package/src/modules/manufacturing/command/createWorkCenter.test.ts +148 -0
  54. package/src/modules/manufacturing/command/createWorkCenter.ts +131 -0
  55. package/src/modules/manufacturing/command/deactivateBillOfMaterial.generated.ts +6 -0
  56. package/src/modules/manufacturing/command/deactivateBillOfMaterial.test.ts +103 -0
  57. package/src/modules/manufacturing/command/deactivateBillOfMaterial.ts +78 -0
  58. package/src/modules/manufacturing/command/deactivateRouting.generated.ts +6 -0
  59. package/src/modules/manufacturing/command/deactivateRouting.test.ts +112 -0
  60. package/src/modules/manufacturing/command/deactivateRouting.ts +76 -0
  61. package/src/modules/manufacturing/command/deactivateWorkCenter.generated.ts +6 -0
  62. package/src/modules/manufacturing/command/deactivateWorkCenter.test.ts +113 -0
  63. package/src/modules/manufacturing/command/deactivateWorkCenter.ts +85 -0
  64. package/src/modules/manufacturing/command/pauseWorkOrder.generated.ts +6 -0
  65. package/src/modules/manufacturing/command/pauseWorkOrder.test.ts +118 -0
  66. package/src/modules/manufacturing/command/pauseWorkOrder.ts +82 -0
  67. package/src/modules/manufacturing/command/recordInventoryIssueOutcome.generated.ts +6 -0
  68. package/src/modules/manufacturing/command/recordInventoryIssueOutcome.test.ts +183 -0
  69. package/src/modules/manufacturing/command/recordInventoryIssueOutcome.ts +139 -0
  70. package/src/modules/manufacturing/command/recordManufacturingCostSettlementAcknowledgment.generated.ts +6 -0
  71. package/src/modules/manufacturing/command/recordManufacturingCostSettlementAcknowledgment.test.ts +120 -0
  72. package/src/modules/manufacturing/command/recordManufacturingCostSettlementAcknowledgment.ts +110 -0
  73. package/src/modules/manufacturing/command/releaseProductionOrder.generated.ts +6 -0
  74. package/src/modules/manufacturing/command/releaseProductionOrder.test.ts +220 -0
  75. package/src/modules/manufacturing/command/releaseProductionOrder.ts +450 -0
  76. package/src/modules/manufacturing/command/reopenProductionOrder.generated.ts +6 -0
  77. package/src/modules/manufacturing/command/reopenProductionOrder.test.ts +196 -0
  78. package/src/modules/manufacturing/command/reopenProductionOrder.ts +98 -0
  79. package/src/modules/manufacturing/command/reportWorkOrderProgress.generated.ts +6 -0
  80. package/src/modules/manufacturing/command/reportWorkOrderProgress.test.ts +204 -0
  81. package/src/modules/manufacturing/command/reportWorkOrderProgress.ts +129 -0
  82. package/src/modules/manufacturing/command/rescheduleProductionOrder.generated.ts +6 -0
  83. package/src/modules/manufacturing/command/rescheduleProductionOrder.test.ts +185 -0
  84. package/src/modules/manufacturing/command/rescheduleProductionOrder.ts +95 -0
  85. package/src/modules/manufacturing/command/resumeWorkOrder.generated.ts +6 -0
  86. package/src/modules/manufacturing/command/resumeWorkOrder.test.ts +122 -0
  87. package/src/modules/manufacturing/command/resumeWorkOrder.ts +94 -0
  88. package/src/modules/manufacturing/command/reviewManufacturingCostSummary.generated.ts +6 -0
  89. package/src/modules/manufacturing/command/reviewManufacturingCostSummary.test.ts +231 -0
  90. package/src/modules/manufacturing/command/reviewManufacturingCostSummary.ts +137 -0
  91. package/src/modules/manufacturing/command/startWorkOrder.generated.ts +6 -0
  92. package/src/modules/manufacturing/command/startWorkOrder.test.ts +118 -0
  93. package/src/modules/manufacturing/command/startWorkOrder.ts +126 -0
  94. package/src/modules/manufacturing/command/technicallyCompleteProductionOrder.generated.ts +6 -0
  95. package/src/modules/manufacturing/command/technicallyCompleteProductionOrder.test.ts +153 -0
  96. package/src/modules/manufacturing/command/technicallyCompleteProductionOrder.ts +106 -0
  97. package/src/modules/manufacturing/command/unreleaseProductionOrder.generated.ts +6 -0
  98. package/src/modules/manufacturing/command/unreleaseProductionOrder.test.ts +140 -0
  99. package/src/modules/manufacturing/command/unreleaseProductionOrder.ts +131 -0
  100. package/src/modules/manufacturing/command/updateBillOfMaterial.generated.ts +6 -0
  101. package/src/modules/manufacturing/command/updateBillOfMaterial.test.ts +149 -0
  102. package/src/modules/manufacturing/command/updateBillOfMaterial.ts +174 -0
  103. package/src/modules/manufacturing/command/updateProductionOrder.generated.ts +6 -0
  104. package/src/modules/manufacturing/command/updateProductionOrder.test.ts +112 -0
  105. package/src/modules/manufacturing/command/updateProductionOrder.ts +145 -0
  106. package/src/modules/manufacturing/command/updateRouting.generated.ts +6 -0
  107. package/src/modules/manufacturing/command/updateRouting.test.ts +211 -0
  108. package/src/modules/manufacturing/command/updateRouting.ts +124 -0
  109. package/src/modules/manufacturing/command/updateWorkCenter.generated.ts +6 -0
  110. package/src/modules/manufacturing/command/updateWorkCenter.test.ts +152 -0
  111. package/src/modules/manufacturing/command/updateWorkCenter.ts +137 -0
  112. package/src/modules/manufacturing/db/.gitkeep +0 -0
  113. package/src/modules/manufacturing/db/billOfMaterial.ts +70 -0
  114. package/src/modules/manufacturing/db/billOfMaterialLine.ts +49 -0
  115. package/src/modules/manufacturing/db/costVarianceLine.ts +53 -0
  116. package/src/modules/manufacturing/db/manufacturingCostLine.ts +35 -0
  117. package/src/modules/manufacturing/db/manufacturingCostSettlementRecord.ts +39 -0
  118. package/src/modules/manufacturing/db/manufacturingCostSummary.ts +59 -0
  119. package/src/modules/manufacturing/db/productionOrder.ts +83 -0
  120. package/src/modules/manufacturing/db/productionOrderBomSnapshot.ts +44 -0
  121. package/src/modules/manufacturing/db/productionOrderCostBaseline.ts +44 -0
  122. package/src/modules/manufacturing/db/productionOrderMaterialRequirement.ts +57 -0
  123. package/src/modules/manufacturing/db/productionOrderRoutingSnapshot.ts +43 -0
  124. package/src/modules/manufacturing/db/routing.ts +63 -0
  125. package/src/modules/manufacturing/db/routingOperation.ts +57 -0
  126. package/src/modules/manufacturing/db/workCenter.ts +87 -0
  127. package/src/modules/manufacturing/db/workOrder.ts +65 -0
  128. package/src/modules/manufacturing/db/workOrderExecutionEvent.ts +54 -0
  129. package/src/modules/manufacturing/docs/commands/ActivateBillOfMaterial.md +50 -0
  130. package/src/modules/manufacturing/docs/commands/ActivateRouting.md +48 -0
  131. package/src/modules/manufacturing/docs/commands/ActivateWorkCenter.md +49 -0
  132. package/src/modules/manufacturing/docs/commands/CancelProductionOrder.md +48 -0
  133. package/src/modules/manufacturing/docs/commands/CloseProductionOrder.md +46 -0
  134. package/src/modules/manufacturing/docs/commands/CompleteProductionOrder.md +48 -0
  135. package/src/modules/manufacturing/docs/commands/CompleteWorkOrder.md +66 -0
  136. package/src/modules/manufacturing/docs/commands/CreateBillOfMaterial.md +54 -0
  137. package/src/modules/manufacturing/docs/commands/CreateProductionOrder.md +49 -0
  138. package/src/modules/manufacturing/docs/commands/CreateRouting.md +50 -0
  139. package/src/modules/manufacturing/docs/commands/CreateWorkCenter.md +51 -0
  140. package/src/modules/manufacturing/docs/commands/DeactivateBillOfMaterial.md +45 -0
  141. package/src/modules/manufacturing/docs/commands/DeactivateRouting.md +45 -0
  142. package/src/modules/manufacturing/docs/commands/DeactivateWorkCenter.md +45 -0
  143. package/src/modules/manufacturing/docs/commands/PauseWorkOrder.md +44 -0
  144. package/src/modules/manufacturing/docs/commands/RecordInventoryIssueOutcome.md +59 -0
  145. package/src/modules/manufacturing/docs/commands/RecordManufacturingCostSettlementAcknowledgment.md +49 -0
  146. package/src/modules/manufacturing/docs/commands/ReleaseProductionOrder.md +57 -0
  147. package/src/modules/manufacturing/docs/commands/ReopenProductionOrder.md +54 -0
  148. package/src/modules/manufacturing/docs/commands/ReportWorkOrderProgress.md +53 -0
  149. package/src/modules/manufacturing/docs/commands/RescheduleProductionOrder.md +45 -0
  150. package/src/modules/manufacturing/docs/commands/ResumeWorkOrder.md +44 -0
  151. package/src/modules/manufacturing/docs/commands/ReviewManufacturingCostSummary.md +52 -0
  152. package/src/modules/manufacturing/docs/commands/StartWorkOrder.md +46 -0
  153. package/src/modules/manufacturing/docs/commands/TechnicallyCompleteProductionOrder.md +51 -0
  154. package/src/modules/manufacturing/docs/commands/UnreleaseProductionOrder.md +46 -0
  155. package/src/modules/manufacturing/docs/commands/UpdateBillOfMaterial.md +48 -0
  156. package/src/modules/manufacturing/docs/commands/UpdateProductionOrder.md +48 -0
  157. package/src/modules/manufacturing/docs/commands/UpdateRouting.md +52 -0
  158. package/src/modules/manufacturing/docs/commands/UpdateWorkCenter.md +48 -0
  159. package/src/modules/manufacturing/docs/features/bill-of-material-management.md +83 -0
  160. package/src/modules/manufacturing/docs/features/manufacturing-cost-and-variance.md +191 -0
  161. package/src/modules/manufacturing/docs/features/production-order-lifecycle.md +103 -0
  162. package/src/modules/manufacturing/docs/features/routing-and-work-center-definition.md +63 -0
  163. package/src/modules/manufacturing/docs/features/work-order-execution.md +115 -0
  164. package/src/modules/manufacturing/docs/models/BillOfMaterial.md +60 -0
  165. package/src/modules/manufacturing/docs/models/ManufacturingCostSummary.md +66 -0
  166. package/src/modules/manufacturing/docs/models/ProductionOrder.md +76 -0
  167. package/src/modules/manufacturing/docs/models/Routing.md +58 -0
  168. package/src/modules/manufacturing/docs/models/WorkCenter.md +56 -0
  169. package/src/modules/manufacturing/docs/models/WorkOrder.md +63 -0
  170. package/src/modules/manufacturing/docs/queries/DetectBillOfMaterialCircularReference.md +39 -0
  171. package/src/modules/manufacturing/docs/queries/ExplodeBillOfMaterial.md +56 -0
  172. package/src/modules/manufacturing/docs/queries/GetBillOfMaterial.md +37 -0
  173. package/src/modules/manufacturing/docs/queries/GetManufacturingCostSummary.md +39 -0
  174. package/src/modules/manufacturing/docs/queries/GetProductionOrder.md +37 -0
  175. package/src/modules/manufacturing/docs/queries/GetRouting.md +39 -0
  176. package/src/modules/manufacturing/docs/queries/GetWorkCenter.md +35 -0
  177. package/src/modules/manufacturing/docs/queries/GetWorkOrder.md +38 -0
  178. package/src/modules/manufacturing/docs/queries/ListBillOfMaterialsByItem.md +42 -0
  179. package/src/modules/manufacturing/docs/queries/ListManufacturingCostSummariesByStatus.md +41 -0
  180. package/src/modules/manufacturing/docs/queries/ListProductionOrdersByStatus.md +41 -0
  181. package/src/modules/manufacturing/docs/queries/ListRoutingsByItem.md +42 -0
  182. package/src/modules/manufacturing/docs/queries/ListWorkCentersBySite.md +38 -0
  183. package/src/modules/manufacturing/docs/queries/ListWorkOrdersByProductionOrder.md +39 -0
  184. package/src/modules/manufacturing/docs/queries/ListWorkOrdersByWorkCenter.md +43 -0
  185. package/src/modules/manufacturing/executor/.gitkeep +0 -0
  186. package/src/modules/manufacturing/generated/enums.ts +113 -0
  187. package/src/modules/manufacturing/generated/kysely-tailordb.ts +247 -0
  188. package/src/modules/manufacturing/index.ts +2 -0
  189. package/src/modules/manufacturing/lib/_db_deps.ts +22 -0
  190. package/src/modules/manufacturing/lib/errors.generated.ts +592 -0
  191. package/src/modules/manufacturing/lib/permissions.generated.ts +35 -0
  192. package/src/modules/manufacturing/lib/types.ts +111 -0
  193. package/src/modules/manufacturing/module.ts +226 -0
  194. package/src/modules/manufacturing/permissions.ts +3 -0
  195. package/src/modules/manufacturing/query/.gitkeep +0 -0
  196. package/src/modules/manufacturing/query/detectBillOfMaterialCircularReference.generated.ts +5 -0
  197. package/src/modules/manufacturing/query/detectBillOfMaterialCircularReference.test.ts +115 -0
  198. package/src/modules/manufacturing/query/detectBillOfMaterialCircularReference.ts +79 -0
  199. package/src/modules/manufacturing/query/explodeBillOfMaterial.generated.ts +5 -0
  200. package/src/modules/manufacturing/query/explodeBillOfMaterial.test.ts +445 -0
  201. package/src/modules/manufacturing/query/explodeBillOfMaterial.ts +306 -0
  202. package/src/modules/manufacturing/query/getBillOfMaterial.generated.ts +5 -0
  203. package/src/modules/manufacturing/query/getBillOfMaterial.test.ts +64 -0
  204. package/src/modules/manufacturing/query/getBillOfMaterial.ts +27 -0
  205. package/src/modules/manufacturing/query/getManufacturingCostSummary.generated.ts +5 -0
  206. package/src/modules/manufacturing/query/getManufacturingCostSummary.test.ts +147 -0
  207. package/src/modules/manufacturing/query/getManufacturingCostSummary.ts +46 -0
  208. package/src/modules/manufacturing/query/getProductionOrder.generated.ts +5 -0
  209. package/src/modules/manufacturing/query/getProductionOrder.test.ts +139 -0
  210. package/src/modules/manufacturing/query/getProductionOrder.ts +84 -0
  211. package/src/modules/manufacturing/query/getRouting.generated.ts +5 -0
  212. package/src/modules/manufacturing/query/getRouting.test.ts +71 -0
  213. package/src/modules/manufacturing/query/getRouting.ts +34 -0
  214. package/src/modules/manufacturing/query/getWorkCenter.generated.ts +5 -0
  215. package/src/modules/manufacturing/query/getWorkCenter.test.ts +37 -0
  216. package/src/modules/manufacturing/query/getWorkCenter.ts +21 -0
  217. package/src/modules/manufacturing/query/getWorkOrder.generated.ts +5 -0
  218. package/src/modules/manufacturing/query/getWorkOrder.test.ts +73 -0
  219. package/src/modules/manufacturing/query/getWorkOrder.ts +28 -0
  220. package/src/modules/manufacturing/query/listBillOfMaterialsByItem.generated.ts +5 -0
  221. package/src/modules/manufacturing/query/listBillOfMaterialsByItem.test.ts +107 -0
  222. package/src/modules/manufacturing/query/listBillOfMaterialsByItem.ts +58 -0
  223. package/src/modules/manufacturing/query/listManufacturingCostSummariesByStatus.generated.ts +5 -0
  224. package/src/modules/manufacturing/query/listManufacturingCostSummariesByStatus.test.ts +96 -0
  225. package/src/modules/manufacturing/query/listManufacturingCostSummariesByStatus.ts +77 -0
  226. package/src/modules/manufacturing/query/listProductionOrdersByStatus.generated.ts +5 -0
  227. package/src/modules/manufacturing/query/listProductionOrdersByStatus.test.ts +121 -0
  228. package/src/modules/manufacturing/query/listProductionOrdersByStatus.ts +83 -0
  229. package/src/modules/manufacturing/query/listRoutingsByItem.generated.ts +5 -0
  230. package/src/modules/manufacturing/query/listRoutingsByItem.test.ts +110 -0
  231. package/src/modules/manufacturing/query/listRoutingsByItem.ts +54 -0
  232. package/src/modules/manufacturing/query/listWorkCentersBySite.generated.ts +5 -0
  233. package/src/modules/manufacturing/query/listWorkCentersBySite.test.ts +81 -0
  234. package/src/modules/manufacturing/query/listWorkCentersBySite.ts +70 -0
  235. package/src/modules/manufacturing/query/listWorkOrdersByProductionOrder.generated.ts +5 -0
  236. package/src/modules/manufacturing/query/listWorkOrdersByProductionOrder.test.ts +102 -0
  237. package/src/modules/manufacturing/query/listWorkOrdersByProductionOrder.ts +53 -0
  238. package/src/modules/manufacturing/query/listWorkOrdersByWorkCenter.generated.ts +5 -0
  239. package/src/modules/manufacturing/query/listWorkOrdersByWorkCenter.test.ts +143 -0
  240. package/src/modules/manufacturing/query/listWorkOrdersByWorkCenter.ts +56 -0
  241. package/src/modules/manufacturing/seed/index.ts +19 -0
  242. package/src/modules/manufacturing/tailor.config.ts +13 -0
  243. package/src/modules/manufacturing/tailor.d.ts +13 -0
  244. package/src/modules/manufacturing/testing/commandTestUtils.ts +29 -0
  245. package/src/modules/manufacturing/testing/fixtures.ts +402 -0
  246. package/templates/scaffold/app/backend/package.json +9 -2
  247. package/templates/scaffold/app/backend/src/tests/utils/graphql-client.ts +66 -0
  248. package/templates/scaffold/app/backend/src/tests/utils/setup.ts +21 -0
  249. package/templates/scaffold/app/backend/tsconfig.json +9 -2
  250. package/templates/scaffold/app/backend/vitest.config.ts +35 -0
  251. package/templates/scaffold/app/frontend/package.json +2 -2
@@ -0,0 +1,153 @@
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
+ ProductionOrderNotTechnicallyCompletableError,
7
+ ExecutionExceptionRemainsError,
8
+ PendingMaterialIssueRequestsError,
9
+ CostSummaryNotReadyError,
10
+ } from "../lib/errors.generated";
11
+ import {
12
+ baseCompletedProductionOrder,
13
+ baseInProgressProductionOrder,
14
+ baseCompleteWorkOrder,
15
+ baseCancelledWorkOrder,
16
+ baseInProgressWorkOrder,
17
+ basePausedWorkOrder,
18
+ basePendingWorkOrder,
19
+ baseCollectingCostSummary,
20
+ basePendingReviewCostSummary,
21
+ } from "../testing/fixtures";
22
+ import { run } from "./technicallyCompleteProductionOrder";
23
+ import type { CommandContext } from "@tailor-platform/erp-kit/module";
24
+
25
+ describe("technicallyCompleteProductionOrder", () => {
26
+ const ctx: CommandContext = {
27
+ actorId: "test-actor",
28
+ permissions: ["manufacturing:technicallyCompleteProductionOrder"],
29
+ };
30
+
31
+ it("technically completes a completed order and opens variance review", async () => {
32
+ const { db, spies } = createMockDb<Transaction>();
33
+ const techComplete = {
34
+ ...baseCompletedProductionOrder,
35
+ status: "TECHNICALLY_COMPLETE" as const,
36
+ };
37
+
38
+ // order lookup
39
+ spies.select.mockReturnValueOnce(baseCompletedProductionOrder);
40
+ // work orders - all complete or cancelled
41
+ spies.select.mockReturnValueOnce([baseCompleteWorkOrder, baseCancelledWorkOrder]);
42
+ // cost summary in COLLECTING
43
+ spies.select.mockReturnValueOnce({
44
+ ...baseCollectingCostSummary,
45
+ productionOrderId: baseCompletedProductionOrder.id,
46
+ });
47
+ // update cost summary
48
+ spies.update.mockReturnValueOnce(undefined);
49
+ // update order status
50
+ spies.update.mockReturnValue(techComplete);
51
+
52
+ const result = await run(db, { id: baseCompletedProductionOrder.id }, ctx);
53
+
54
+ expect(result.ok).toBe(true);
55
+ if (result.ok) {
56
+ expect(result.value.productionOrder.status).toBe("TECHNICALLY_COMPLETE");
57
+ }
58
+ expect(spies.update).toHaveBeenCalled();
59
+ });
60
+
61
+ it("returns error when the order does not exist", async () => {
62
+ const { db, spies } = createMockDb<Transaction>();
63
+ spies.select.mockReturnValueOnce(undefined);
64
+
65
+ const result = await run(db, { id: "nonexistent" }, ctx);
66
+
67
+ expect(result.ok).toBe(false);
68
+ if (!result.ok) {
69
+ expect(result.error).toBeInstanceOf(ProductionOrderNotFoundError);
70
+ }
71
+ });
72
+
73
+ it("returns error when the order is not in COMPLETED", async () => {
74
+ const { db, spies } = createMockDb<Transaction>();
75
+ spies.select.mockReturnValueOnce(baseInProgressProductionOrder);
76
+
77
+ const result = await run(db, { id: baseInProgressProductionOrder.id }, ctx);
78
+
79
+ expect(result.ok).toBe(false);
80
+ if (!result.ok) {
81
+ expect(result.error).toBeInstanceOf(ProductionOrderNotTechnicallyCompletableError);
82
+ }
83
+ });
84
+
85
+ it("returns error when unresolved execution exceptions remain", async () => {
86
+ const { db, spies } = createMockDb<Transaction>();
87
+
88
+ // order lookup
89
+ spies.select.mockReturnValueOnce(baseCompletedProductionOrder);
90
+ // work orders - one still in progress
91
+ spies.select.mockReturnValueOnce([baseInProgressWorkOrder]);
92
+
93
+ const result = await run(db, { id: baseCompletedProductionOrder.id }, ctx);
94
+
95
+ expect(result.ok).toBe(false);
96
+ if (!result.ok) {
97
+ expect(result.error).toBeInstanceOf(ExecutionExceptionRemainsError);
98
+ }
99
+ });
100
+
101
+ it("returns error when pending material issue requests or unresolved backflush intents remain", async () => {
102
+ const { db, spies } = createMockDb<Transaction>();
103
+
104
+ // order lookup
105
+ spies.select.mockReturnValueOnce(baseCompletedProductionOrder);
106
+ // work orders - one still pending
107
+ spies.select.mockReturnValueOnce([baseCompleteWorkOrder, basePendingWorkOrder]);
108
+
109
+ const result = await run(db, { id: baseCompletedProductionOrder.id }, ctx);
110
+
111
+ expect(result.ok).toBe(false);
112
+ if (!result.ok) {
113
+ expect(result.error).toBeInstanceOf(PendingMaterialIssueRequestsError);
114
+ }
115
+ });
116
+
117
+ it("returns error when the linked cost summary is not ready for review", async () => {
118
+ const { db, spies } = createMockDb<Transaction>();
119
+
120
+ // order lookup
121
+ spies.select.mockReturnValueOnce(baseCompletedProductionOrder);
122
+ // work orders - all complete
123
+ spies.select.mockReturnValueOnce([baseCompleteWorkOrder]);
124
+ // cost summary already in PENDING_VARIANCE_REVIEW (not COLLECTING)
125
+ spies.select.mockReturnValueOnce({
126
+ ...basePendingReviewCostSummary,
127
+ productionOrderId: baseCompletedProductionOrder.id,
128
+ });
129
+
130
+ const result = await run(db, { id: baseCompletedProductionOrder.id }, ctx);
131
+
132
+ expect(result.ok).toBe(false);
133
+ if (!result.ok) {
134
+ expect(result.error).toBeInstanceOf(CostSummaryNotReadyError);
135
+ }
136
+ });
137
+
138
+ it("blocks further shop-floor reporting after technical completion", async () => {
139
+ const { db, spies } = createMockDb<Transaction>();
140
+
141
+ // order lookup - already paused work order still open
142
+ spies.select.mockReturnValueOnce(baseCompletedProductionOrder);
143
+ // work orders - one paused (execution exception)
144
+ spies.select.mockReturnValueOnce([basePausedWorkOrder]);
145
+
146
+ const result = await run(db, { id: baseCompletedProductionOrder.id }, ctx);
147
+
148
+ expect(result.ok).toBe(false);
149
+ if (!result.ok) {
150
+ expect(result.error).toBeInstanceOf(ExecutionExceptionRemainsError);
151
+ }
152
+ });
153
+ });
@@ -0,0 +1,106 @@
1
+ import type { Transaction } from "../generated/kysely-tailordb";
2
+ import {
3
+ ProductionOrderNotFoundError,
4
+ ProductionOrderNotTechnicallyCompletableError,
5
+ ExecutionExceptionRemainsError,
6
+ PendingMaterialIssueRequestsError,
7
+ CostSummaryNotReadyError,
8
+ } from "../lib/errors.generated";
9
+ import { ok, err, type CommandContext } from "@tailor-platform/erp-kit/module";
10
+
11
+ export interface TechnicallyCompleteProductionOrderInput {
12
+ id: string;
13
+ from?: string[];
14
+ }
15
+
16
+ /**
17
+ * Function: technicallyCompleteProductionOrder
18
+ *
19
+ * Freezes the production order after physical completion and moves the linked
20
+ * manufacturing cost summary into variance review. Marks the point where no
21
+ * more normal execution or rescheduling is expected.
22
+ */
23
+ export async function run<CF extends Record<string, unknown>>(
24
+ db: Transaction,
25
+ input: TechnicallyCompleteProductionOrderInput & CF,
26
+ _ctx: CommandContext,
27
+ ) {
28
+ const { id, from, ...customFields } = input;
29
+ void customFields;
30
+
31
+ const allowedStatuses = from ?? ["COMPLETED"];
32
+
33
+ // 1. Fetch production order with lock
34
+ const order = await db
35
+ .selectFrom("ProductionOrder")
36
+ .selectAll()
37
+ .where("id", "=", id)
38
+ .forUpdate()
39
+ .executeTakeFirst();
40
+
41
+ if (!order) {
42
+ return err(new ProductionOrderNotFoundError(id));
43
+ }
44
+
45
+ // 2. Validate status
46
+ if (!allowedStatuses.includes(order.status)) {
47
+ return err(new ProductionOrderNotTechnicallyCompletableError(id));
48
+ }
49
+
50
+ // 3. Check for open execution exceptions (work orders not COMPLETE or CANCELLED)
51
+ const workOrders = await db
52
+ .selectFrom("WorkOrder")
53
+ .selectAll()
54
+ .where("productionOrderId", "=", id)
55
+ .execute();
56
+
57
+ const hasExecutionExceptions = workOrders.some(
58
+ (wo) => wo.status === "IN_PROGRESS" || wo.status === "PAUSED",
59
+ );
60
+
61
+ if (hasExecutionExceptions) {
62
+ return err(new ExecutionExceptionRemainsError(id));
63
+ }
64
+
65
+ // 4. Check for pending material issue requests (work orders still PENDING)
66
+ const hasPendingWork = workOrders.some((wo) => wo.status === "PENDING");
67
+
68
+ if (hasPendingWork) {
69
+ return err(new PendingMaterialIssueRequestsError(id));
70
+ }
71
+
72
+ // 5. Find and validate cost summary
73
+ const costSummary = await db
74
+ .selectFrom("ManufacturingCostSummary")
75
+ .selectAll()
76
+ .where("productionOrderId", "=", id)
77
+ .forUpdate()
78
+ .executeTakeFirst();
79
+
80
+ if (costSummary?.status !== "COLLECTING") {
81
+ return err(new CostSummaryNotReadyError(id));
82
+ }
83
+
84
+ // 6. Move cost summary to PENDING_VARIANCE_REVIEW
85
+ await db
86
+ .updateTable("ManufacturingCostSummary")
87
+ .set({
88
+ status: "PENDING_VARIANCE_REVIEW",
89
+ updatedAt: new Date(),
90
+ })
91
+ .where("id", "=", costSummary.id)
92
+ .execute();
93
+
94
+ // 7. Set order status to TECHNICALLY_COMPLETE
95
+ const techCompleteOrder = await db
96
+ .updateTable("ProductionOrder")
97
+ .set({
98
+ status: "TECHNICALLY_COMPLETE",
99
+ updatedAt: new Date(),
100
+ })
101
+ .where("id", "=", id)
102
+ .returningAll()
103
+ .executeTakeFirstOrThrow();
104
+
105
+ return ok({ productionOrder: techCompleteOrder });
106
+ }
@@ -0,0 +1,6 @@
1
+ // @generated — do not edit
2
+ import { permissions } from "../lib/permissions.generated";
3
+ import { run } from "./unreleaseProductionOrder";
4
+ import { defineCommand } from "@tailor-platform/erp-kit/module";
5
+
6
+ export const unreleaseProductionOrder = defineCommand(permissions.unreleaseProductionOrder, run);
@@ -0,0 +1,140 @@
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
+ ProductionOrderNotUnreleasableError,
7
+ ExecutionAlreadyStartedError,
8
+ InventoryHandoffExistsError,
9
+ } from "../lib/errors.generated";
10
+ import {
11
+ baseDraftProductionOrder,
12
+ baseReleasedProductionOrder,
13
+ basePendingWorkOrder,
14
+ baseInProgressWorkOrder,
15
+ baseMaterialRequirement,
16
+ baseCollectingCostSummary,
17
+ } from "../testing/fixtures";
18
+ import { run } from "./unreleaseProductionOrder";
19
+ import type { CommandContext } from "@tailor-platform/erp-kit/module";
20
+
21
+ describe("unreleaseProductionOrder", () => {
22
+ const ctx: CommandContext = {
23
+ actorId: "test-actor",
24
+ permissions: ["manufacturing:unreleaseProductionOrder"],
25
+ };
26
+
27
+ it("unreleases a released order with no execution evidence", async () => {
28
+ const { db, spies } = createMockDb<Transaction>();
29
+ const draftOrder = { ...baseReleasedProductionOrder, status: "DRAFT" as const };
30
+
31
+ // order lookup
32
+ spies.select.mockReturnValueOnce(baseReleasedProductionOrder);
33
+ // work orders - all pending
34
+ spies.select.mockReturnValueOnce([basePendingWorkOrder]);
35
+ // material requirements
36
+ spies.select.mockReturnValueOnce([baseMaterialRequirement]);
37
+ // cost summary - no actuals
38
+ spies.select.mockReturnValueOnce(baseCollectingCostSummary);
39
+ // delete operations return
40
+ spies.delete.mockReturnValue(undefined);
41
+ // update order status
42
+ spies.update.mockReturnValue(draftOrder);
43
+
44
+ const result = await run(db, { id: baseReleasedProductionOrder.id }, ctx);
45
+
46
+ expect(result.ok).toBe(true);
47
+ if (result.ok) {
48
+ expect(result.value.productionOrder.status).toBe("DRAFT");
49
+ }
50
+ expect(spies.update).toHaveBeenCalled();
51
+ });
52
+
53
+ it("returns error when the order does not exist", async () => {
54
+ const { db, spies } = createMockDb<Transaction>();
55
+ spies.select.mockReturnValueOnce(undefined);
56
+
57
+ const result = await run(db, { id: "nonexistent" }, ctx);
58
+
59
+ expect(result.ok).toBe(false);
60
+ if (!result.ok) {
61
+ expect(result.error).toBeInstanceOf(ProductionOrderNotFoundError);
62
+ }
63
+ });
64
+
65
+ it("returns error when the order is not in RELEASED", async () => {
66
+ const { db, spies } = createMockDb<Transaction>();
67
+ spies.select.mockReturnValueOnce(baseDraftProductionOrder);
68
+
69
+ const result = await run(db, { id: baseDraftProductionOrder.id }, ctx);
70
+
71
+ expect(result.ok).toBe(false);
72
+ if (!result.ok) {
73
+ expect(result.error).toBeInstanceOf(ProductionOrderNotUnreleasableError);
74
+ }
75
+ });
76
+
77
+ it("returns error when a work order has already started", async () => {
78
+ const { db, spies } = createMockDb<Transaction>();
79
+
80
+ // order lookup
81
+ spies.select.mockReturnValueOnce(baseReleasedProductionOrder);
82
+ // work orders - one in progress
83
+ spies.select.mockReturnValueOnce([baseInProgressWorkOrder]);
84
+
85
+ const result = await run(db, { id: baseReleasedProductionOrder.id }, ctx);
86
+
87
+ expect(result.ok).toBe(false);
88
+ if (!result.ok) {
89
+ expect(result.error).toBeInstanceOf(ExecutionAlreadyStartedError);
90
+ }
91
+ });
92
+
93
+ it("returns error when inventory handoff evidence already exists", async () => {
94
+ const { db, spies } = createMockDb<Transaction>();
95
+ const costSummaryWithActuals = {
96
+ ...baseCollectingCostSummary,
97
+ actualMaterialCost: 500,
98
+ };
99
+
100
+ // order lookup
101
+ spies.select.mockReturnValueOnce(baseReleasedProductionOrder);
102
+ // work orders - all pending
103
+ spies.select.mockReturnValueOnce([basePendingWorkOrder]);
104
+ // material requirements
105
+ spies.select.mockReturnValueOnce([baseMaterialRequirement]);
106
+ // cost summary with actual costs
107
+ spies.select.mockReturnValueOnce(costSummaryWithActuals);
108
+
109
+ const result = await run(db, { id: baseReleasedProductionOrder.id }, ctx);
110
+
111
+ expect(result.ok).toBe(false);
112
+ if (!result.ok) {
113
+ expect(result.error).toBeInstanceOf(InventoryHandoffExistsError);
114
+ }
115
+ });
116
+
117
+ it("removes work orders and material requirements when unrelease succeeds", async () => {
118
+ const { db, spies } = createMockDb<Transaction>();
119
+ const draftOrder = { ...baseReleasedProductionOrder, status: "DRAFT" as const };
120
+
121
+ // order lookup
122
+ spies.select.mockReturnValueOnce(baseReleasedProductionOrder);
123
+ // work orders - all pending
124
+ spies.select.mockReturnValueOnce([basePendingWorkOrder]);
125
+ // material requirements
126
+ spies.select.mockReturnValueOnce([]);
127
+ // cost summary
128
+ spies.select.mockReturnValueOnce(baseCollectingCostSummary);
129
+ // delete operations
130
+ spies.delete.mockReturnValue(undefined);
131
+ // update order
132
+ spies.update.mockReturnValue(draftOrder);
133
+
134
+ const result = await run(db, { id: baseReleasedProductionOrder.id }, ctx);
135
+
136
+ expect(result.ok).toBe(true);
137
+ expect(spies.delete).toHaveBeenCalled();
138
+ expect(spies.update).toHaveBeenCalled();
139
+ });
140
+ });
@@ -0,0 +1,131 @@
1
+ import type { Transaction } from "../generated/kysely-tailordb";
2
+ import {
3
+ ProductionOrderNotFoundError,
4
+ ProductionOrderNotUnreleasableError,
5
+ ExecutionAlreadyStartedError,
6
+ InventoryHandoffExistsError,
7
+ } from "../lib/errors.generated";
8
+ import { ok, err, type CommandContext } from "@tailor-platform/erp-kit/module";
9
+
10
+ export interface UnreleaseProductionOrderInput {
11
+ id: string;
12
+ from?: string[];
13
+ }
14
+
15
+ /**
16
+ * Function: unreleaseProductionOrder
17
+ *
18
+ * Returns a released order to DRAFT when execution has not meaningfully
19
+ * started. Removes release artifacts so the planner can safely revise
20
+ * the order.
21
+ */
22
+ export async function run<CF extends Record<string, unknown>>(
23
+ db: Transaction,
24
+ input: UnreleaseProductionOrderInput & CF,
25
+ _ctx: CommandContext,
26
+ ) {
27
+ const { id, from, ...customFields } = input;
28
+ void customFields;
29
+
30
+ const allowedStatuses = from ?? ["RELEASED"];
31
+
32
+ // 1. Fetch production order with lock
33
+ const order = await db
34
+ .selectFrom("ProductionOrder")
35
+ .selectAll()
36
+ .where("id", "=", id)
37
+ .forUpdate()
38
+ .executeTakeFirst();
39
+
40
+ if (!order) {
41
+ return err(new ProductionOrderNotFoundError(id));
42
+ }
43
+
44
+ // 2. Validate status is unreleasable
45
+ if (!allowedStatuses.includes(order.status)) {
46
+ return err(new ProductionOrderNotUnreleasableError(id));
47
+ }
48
+
49
+ // 3. Check no work order has execution evidence
50
+ const workOrders = await db
51
+ .selectFrom("WorkOrder")
52
+ .selectAll()
53
+ .where("productionOrderId", "=", id)
54
+ .execute();
55
+
56
+ const hasExecution = workOrders.some(
57
+ (wo) =>
58
+ wo.status === "IN_PROGRESS" ||
59
+ wo.status === "PAUSED" ||
60
+ wo.status === "COMPLETE" ||
61
+ wo.completedQuantity > 0 ||
62
+ wo.actualStartDate != null,
63
+ );
64
+
65
+ if (hasExecution) {
66
+ return err(new ExecutionAlreadyStartedError(id));
67
+ }
68
+
69
+ // 4. Check no irreversible inventory handoff
70
+ const _materialRequirements = await db
71
+ .selectFrom("ProductionOrderMaterialRequirement")
72
+ .selectAll()
73
+ .where("productionOrderId", "=", id)
74
+ .execute();
75
+
76
+ // Check if any cost summary has actual costs recorded (inventory handoff evidence)
77
+ const costSummary = await db
78
+ .selectFrom("ManufacturingCostSummary")
79
+ .selectAll()
80
+ .where("productionOrderId", "=", id)
81
+ .executeTakeFirst();
82
+
83
+ if (
84
+ costSummary &&
85
+ (costSummary.actualMaterialCost > 0 ||
86
+ costSummary.actualLaborCost > 0 ||
87
+ costSummary.actualMachineCost > 0 ||
88
+ costSummary.actualOverheadCost > 0)
89
+ ) {
90
+ return err(new InventoryHandoffExistsError(id));
91
+ }
92
+
93
+ // 5. Remove release artifacts - delete work orders
94
+ await db.deleteFrom("WorkOrder").where("productionOrderId", "=", id).execute();
95
+
96
+ // 6. Remove material requirements
97
+ await db
98
+ .deleteFrom("ProductionOrderMaterialRequirement")
99
+ .where("productionOrderId", "=", id)
100
+ .execute();
101
+
102
+ // 7. Remove BOM snapshot
103
+ await db.deleteFrom("ProductionOrderBomSnapshot").where("productionOrderId", "=", id).execute();
104
+
105
+ // 8. Remove routing snapshot
106
+ await db
107
+ .deleteFrom("ProductionOrderRoutingSnapshot")
108
+ .where("productionOrderId", "=", id)
109
+ .execute();
110
+
111
+ // 9. Remove cost baseline
112
+ await db.deleteFrom("ProductionOrderCostBaseline").where("productionOrderId", "=", id).execute();
113
+
114
+ // 10. Remove cost summary
115
+ await db.deleteFrom("ManufacturingCostSummary").where("productionOrderId", "=", id).execute();
116
+
117
+ // 11. Set status to DRAFT
118
+ const draftOrder = await db
119
+ .updateTable("ProductionOrder")
120
+ .set({
121
+ selectedBomVersionId: null,
122
+ selectedRoutingRevisionId: null,
123
+ status: "DRAFT",
124
+ updatedAt: new Date(),
125
+ })
126
+ .where("id", "=", id)
127
+ .returningAll()
128
+ .executeTakeFirstOrThrow();
129
+
130
+ return ok({ productionOrder: draftOrder });
131
+ }
@@ -0,0 +1,6 @@
1
+ // @generated — do not edit
2
+ import { permissions } from "../lib/permissions.generated";
3
+ import { run } from "./updateBillOfMaterial";
4
+ import { defineCommand } from "@tailor-platform/erp-kit/module";
5
+
6
+ export const updateBillOfMaterial = defineCommand(permissions.updateBillOfMaterial, run);
@@ -0,0 +1,149 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { createMockDb } from "../../../testing/index";
3
+ import type { Transaction } from "../generated/kysely-tailordb";
4
+ import {
5
+ BomNotFoundError,
6
+ BomNotMutableError,
7
+ InvalidComponentQuantityError,
8
+ ComponentItemInactiveError,
9
+ AmbiguousEffectivityRuleError,
10
+ } from "../lib/errors.generated";
11
+ import { baseDraftBom, baseActiveBom, baseBomLine } from "../testing/fixtures";
12
+ import { run } from "./updateBillOfMaterial";
13
+ import type { CommandContext } from "@tailor-platform/erp-kit/module";
14
+
15
+ describe("updateBillOfMaterial", () => {
16
+ const ctx: CommandContext = {
17
+ actorId: "test-actor",
18
+ permissions: ["manufacturing:updateBillOfMaterial"],
19
+ };
20
+
21
+ const validInput = {
22
+ id: "bom-1",
23
+ bomType: "PHANTOM" as const,
24
+ lines: [
25
+ {
26
+ itemId: "item-2",
27
+ requiredQuantity: 3.0,
28
+ unitOfMeasure: "EA",
29
+ },
30
+ ],
31
+ };
32
+
33
+ it("updates draft BOM metadata and component lines", async () => {
34
+ const { db, spies } = createMockDb<Transaction>();
35
+ const updatedBom = {
36
+ ...baseDraftBom,
37
+ bomType: "PHANTOM" as const,
38
+ };
39
+
40
+ // BOM exists and is DRAFT
41
+ spies.select.mockReturnValueOnce(baseDraftBom);
42
+ // component item exists
43
+ spies.select.mockReturnValueOnce({ id: "item-2" });
44
+ // update BOM header
45
+ spies.update.mockReturnValueOnce(updatedBom);
46
+ // delete old lines
47
+ spies.delete.mockReturnValueOnce(undefined);
48
+ // insert new line
49
+ spies.insert.mockReturnValueOnce({ ...baseBomLine, requiredQuantity: 3.0 });
50
+
51
+ const result = await run(db, validInput, ctx);
52
+
53
+ expect(result.ok).toBe(true);
54
+ if (result.ok) {
55
+ expect(result.value.billOfMaterial.bomType).toBe("PHANTOM");
56
+ }
57
+ expect(spies.update).toHaveBeenCalled();
58
+ });
59
+
60
+ it("returns error when the BOM does not exist", async () => {
61
+ const { db, spies } = createMockDb<Transaction>();
62
+
63
+ spies.select.mockReturnValueOnce(undefined);
64
+
65
+ const result = await run(db, validInput, ctx);
66
+
67
+ expect(result.ok).toBe(false);
68
+ if (!result.ok) {
69
+ expect(result.error).toBeInstanceOf(BomNotFoundError);
70
+ }
71
+ });
72
+
73
+ it("returns error when the BOM is not in DRAFT", async () => {
74
+ const { db, spies } = createMockDb<Transaction>();
75
+
76
+ spies.select.mockReturnValueOnce(baseActiveBom);
77
+
78
+ const result = await run(db, { ...validInput, id: "bom-2" }, ctx);
79
+
80
+ expect(result.ok).toBe(false);
81
+ if (!result.ok) {
82
+ expect(result.error).toBeInstanceOf(BomNotMutableError);
83
+ }
84
+ });
85
+
86
+ it("returns error when a revised component quantity is invalid", async () => {
87
+ const { db, spies } = createMockDb<Transaction>();
88
+
89
+ spies.select.mockReturnValueOnce(baseDraftBom);
90
+
91
+ const result = await run(
92
+ db,
93
+ {
94
+ ...validInput,
95
+ lines: [{ itemId: "item-2", requiredQuantity: -1, unitOfMeasure: "EA" }],
96
+ },
97
+ ctx,
98
+ );
99
+
100
+ expect(result.ok).toBe(false);
101
+ if (!result.ok) {
102
+ expect(result.error).toBeInstanceOf(InvalidComponentQuantityError);
103
+ }
104
+ });
105
+
106
+ it("returns error when a newly referenced component is inactive", async () => {
107
+ const { db, spies } = createMockDb<Transaction>();
108
+
109
+ // BOM exists and is DRAFT
110
+ spies.select.mockReturnValueOnce(baseDraftBom);
111
+ // component item not found (inactive)
112
+ spies.select.mockReturnValueOnce(undefined);
113
+
114
+ const result = await run(db, validInput, ctx);
115
+
116
+ expect(result.ok).toBe(false);
117
+ if (!result.ok) {
118
+ expect(result.error).toBeInstanceOf(ComponentItemInactiveError);
119
+ }
120
+ });
121
+
122
+ it("returns error when effectivity changes would create ambiguous selection", async () => {
123
+ const { db, spies } = createMockDb<Transaction>();
124
+
125
+ // BOM exists and is DRAFT
126
+ spies.select.mockReturnValueOnce(baseDraftBom);
127
+ // conflicting active BOM with same default selection
128
+ spies.select.mockReturnValueOnce({
129
+ ...baseActiveBom,
130
+ defaultSelection: true,
131
+ effectivityStartDate: null,
132
+ effectivityEndDate: null,
133
+ });
134
+
135
+ const result = await run(
136
+ db,
137
+ {
138
+ id: "bom-1",
139
+ defaultSelection: true,
140
+ },
141
+ ctx,
142
+ );
143
+
144
+ expect(result.ok).toBe(false);
145
+ if (!result.ok) {
146
+ expect(result.error).toBeInstanceOf(AmbiguousEffectivityRuleError);
147
+ }
148
+ });
149
+ });