@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,173 @@
1
+ import type { Transaction } from "../generated/kysely-tailordb";
2
+ import {
3
+ BomNotFoundError,
4
+ BomNotActivatableError,
5
+ ComponentLineRequiredError,
6
+ ComponentItemInactiveError,
7
+ CircularBomReferenceError,
8
+ EffectivityConflictError,
9
+ } from "../lib/errors.generated";
10
+ import { ok, err, type CommandContext } from "@tailor-platform/erp-kit/module";
11
+
12
+ export interface ActivateBillOfMaterialInput {
13
+ id: string;
14
+ from?: string[];
15
+ }
16
+
17
+ /**
18
+ * Function: activateBillOfMaterial
19
+ *
20
+ * Validates a draft BOM version and makes it selectable for production-order
21
+ * release. Activation enforces circular-reference checks, effectivity
22
+ * conflicts, and component readiness.
23
+ */
24
+ export async function run<CF extends Record<string, unknown>>(
25
+ db: Transaction,
26
+ input: ActivateBillOfMaterialInput & CF,
27
+ _ctx: CommandContext,
28
+ ) {
29
+ const { id, from, ...customFields } = input;
30
+ void customFields;
31
+
32
+ const allowedStatuses = from ?? ["DRAFT"];
33
+
34
+ // 1. Fetch BOM with lock
35
+ const bom = await db
36
+ .selectFrom("BillOfMaterial")
37
+ .selectAll()
38
+ .where("id", "=", id)
39
+ .forUpdate()
40
+ .executeTakeFirst();
41
+
42
+ if (!bom) {
43
+ return err(new BomNotFoundError(id));
44
+ }
45
+
46
+ // 2. Validate status is activatable
47
+ if (!allowedStatuses.includes(bom.status)) {
48
+ return err(new BomNotActivatableError(id));
49
+ }
50
+
51
+ // 3. Validate at least one component line exists
52
+ const lines = await db
53
+ .selectFrom("BillOfMaterialLine")
54
+ .selectAll()
55
+ .where("billOfMaterialId", "=", id)
56
+ .execute();
57
+
58
+ if (!lines || lines.length === 0) {
59
+ return err(new ComponentLineRequiredError(id));
60
+ }
61
+
62
+ // 4. Validate all component items are active
63
+ for (const line of lines) {
64
+ const componentItem = await db
65
+ .selectFrom("Item")
66
+ .selectAll()
67
+ .where("id", "=", line.itemId)
68
+ .executeTakeFirst();
69
+
70
+ if (!componentItem) {
71
+ return err(new ComponentItemInactiveError(line.itemId));
72
+ }
73
+ }
74
+
75
+ // 5. Check for circular references using DFS traversal
76
+ const circularResult = await detectCircularReference(
77
+ db,
78
+ bom.parentItemId,
79
+ lines,
80
+ new Set<string>([bom.parentItemId]),
81
+ );
82
+ if (circularResult) {
83
+ return err(new CircularBomReferenceError(id));
84
+ }
85
+
86
+ // 6. Check effectivity conflicts — overlapping active effectivity windows
87
+ // for the same parent item and scope
88
+ const conflictingBom = await db
89
+ .selectFrom("BillOfMaterial")
90
+ .selectAll()
91
+ .where("parentItemId", "=", bom.parentItemId)
92
+ .where("companyId", "=", bom.companyId)
93
+ .where("id", "!=", id)
94
+ .where("status", "=", "ACTIVE")
95
+ .executeTakeFirst();
96
+
97
+ if (conflictingBom) {
98
+ // Check date overlap
99
+ const bomStart = bom.effectivityStartDate;
100
+ const bomEnd = bom.effectivityEndDate;
101
+ const conflictStart = conflictingBom.effectivityStartDate;
102
+ const conflictEnd = conflictingBom.effectivityEndDate;
103
+
104
+ const hasOverlap =
105
+ (!bomStart || !conflictEnd || bomStart <= conflictEnd) &&
106
+ (!bomEnd || !conflictStart || bomEnd >= conflictStart);
107
+
108
+ if (hasOverlap && bom.defaultSelection && conflictingBom.defaultSelection) {
109
+ return err(new EffectivityConflictError(id));
110
+ }
111
+ }
112
+
113
+ // 7. Set status to ACTIVE
114
+ const updatedBom = await db
115
+ .updateTable("BillOfMaterial")
116
+ .set({
117
+ status: "ACTIVE",
118
+ updatedAt: new Date(),
119
+ })
120
+ .where("id", "=", id)
121
+ .returningAll()
122
+ .executeTakeFirst();
123
+
124
+ return ok({ billOfMaterial: updatedBom! });
125
+ }
126
+
127
+ async function detectCircularReference(
128
+ db: Transaction,
129
+ rootParentItemId: string,
130
+ lines: { itemId: string; isSubassembly: boolean | null }[],
131
+ visitedItems: Set<string>,
132
+ ): Promise<boolean> {
133
+ for (const line of lines) {
134
+ // If this item has already been visited in the path, we have a cycle
135
+ if (visitedItems.has(line.itemId)) {
136
+ return true;
137
+ }
138
+
139
+ if (!line.isSubassembly) continue;
140
+
141
+ // Find ALL child BOMs for this item (multiple versions may exist)
142
+ const childBoms = await db
143
+ .selectFrom("BillOfMaterial")
144
+ .selectAll()
145
+ .where("parentItemId", "=", line.itemId)
146
+ .where("status", "in", ["DRAFT", "ACTIVE"])
147
+ .execute();
148
+
149
+ if (childBoms.length > 0) {
150
+ visitedItems.add(line.itemId);
151
+
152
+ for (const childBom of childBoms) {
153
+ const childLines = await db
154
+ .selectFrom("BillOfMaterialLine")
155
+ .selectAll()
156
+ .where("billOfMaterialId", "=", childBom.id)
157
+ .execute();
158
+
159
+ const hasCycle = await detectCircularReference(
160
+ db,
161
+ rootParentItemId,
162
+ childLines,
163
+ visitedItems,
164
+ );
165
+
166
+ if (hasCycle) return true;
167
+ }
168
+
169
+ visitedItems.delete(line.itemId);
170
+ }
171
+ }
172
+ return false;
173
+ }
@@ -0,0 +1,6 @@
1
+ // @generated — do not edit
2
+ import { permissions } from "../lib/permissions.generated";
3
+ import { run } from "./activateRouting";
4
+ import { defineCommand } from "@tailor-platform/erp-kit/module";
5
+
6
+ export const activateRouting = defineCommand(permissions.activateRouting, run);
@@ -0,0 +1,152 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { createMockDb } from "../../../testing/index";
3
+ import type { Transaction } from "../generated/kysely-tailordb";
4
+ import {
5
+ RoutingNotFoundError,
6
+ RoutingNotActivatableError,
7
+ OperationRequiredError,
8
+ DuplicateOperationSequenceError,
9
+ WorkCenterNotActiveError,
10
+ CrossCompanyWorkCenterError,
11
+ } from "../lib/errors.generated";
12
+ import {
13
+ baseDraftRouting,
14
+ baseActiveRouting,
15
+ baseRoutingOperation,
16
+ baseActiveWorkCenter,
17
+ baseInactiveWorkCenter,
18
+ } from "../testing/fixtures";
19
+ import { run } from "./activateRouting";
20
+ import type { CommandContext } from "@tailor-platform/erp-kit/module";
21
+
22
+ describe("activateRouting", () => {
23
+ const ctx: CommandContext = {
24
+ actorId: "test-actor",
25
+ permissions: ["manufacturing:activateRouting"],
26
+ };
27
+
28
+ const validInput = { id: "routing-1" };
29
+
30
+ it("activates a valid draft routing", async () => {
31
+ const { db, spies } = createMockDb<Transaction>();
32
+
33
+ // select: Routing lookup
34
+ spies.select.mockReturnValueOnce(baseDraftRouting);
35
+ // select: RoutingOperation list
36
+ spies.select.mockReturnValueOnce([baseRoutingOperation]);
37
+ // select: WorkCenter lookup for operation
38
+ spies.select.mockReturnValueOnce(baseActiveWorkCenter);
39
+ // update: status to ACTIVE
40
+ spies.update.mockReturnValue({ ...baseDraftRouting, status: "ACTIVE" });
41
+
42
+ const result = await run(db, validInput, ctx);
43
+
44
+ expect(result.ok).toBe(true);
45
+ if (result.ok) {
46
+ expect(result.value.routing.status).toBe("ACTIVE");
47
+ }
48
+ expect(spies.update).toHaveBeenCalled();
49
+ });
50
+
51
+ it("returns error when the routing does not exist", async () => {
52
+ const { db, spies } = createMockDb<Transaction>();
53
+
54
+ // select: Routing lookup - not found
55
+ spies.select.mockReturnValueOnce(undefined);
56
+
57
+ const result = await run(db, validInput, ctx);
58
+
59
+ expect(result.ok).toBe(false);
60
+ if (!result.ok) {
61
+ expect(result.error).toBeInstanceOf(RoutingNotFoundError);
62
+ }
63
+ });
64
+
65
+ it("returns error when the routing is not in DRAFT", async () => {
66
+ const { db, spies } = createMockDb<Transaction>();
67
+
68
+ // select: Routing lookup - ACTIVE
69
+ spies.select.mockReturnValueOnce(baseActiveRouting);
70
+
71
+ const result = await run(db, { id: "routing-2" }, ctx);
72
+
73
+ expect(result.ok).toBe(false);
74
+ if (!result.ok) {
75
+ expect(result.error).toBeInstanceOf(RoutingNotActivatableError);
76
+ }
77
+ });
78
+
79
+ it("returns error when the routing has no operations", async () => {
80
+ const { db, spies } = createMockDb<Transaction>();
81
+
82
+ // select: Routing lookup
83
+ spies.select.mockReturnValueOnce(baseDraftRouting);
84
+ // select: RoutingOperation list - empty
85
+ spies.select.mockReturnValueOnce([]);
86
+
87
+ const result = await run(db, validInput, ctx);
88
+
89
+ expect(result.ok).toBe(false);
90
+ if (!result.ok) {
91
+ expect(result.error).toBeInstanceOf(OperationRequiredError);
92
+ }
93
+ });
94
+
95
+ it("returns error when operation sequence numbers are duplicated", async () => {
96
+ const { db, spies } = createMockDb<Transaction>();
97
+
98
+ // select: Routing lookup
99
+ spies.select.mockReturnValueOnce(baseDraftRouting);
100
+ // select: RoutingOperation list with duplicate sequences
101
+ spies.select.mockReturnValueOnce([
102
+ baseRoutingOperation,
103
+ { ...baseRoutingOperation, id: "operation-dup", sequenceNumber: 10 },
104
+ ]);
105
+
106
+ const result = await run(db, validInput, ctx);
107
+
108
+ expect(result.ok).toBe(false);
109
+ if (!result.ok) {
110
+ expect(result.error).toBeInstanceOf(DuplicateOperationSequenceError);
111
+ }
112
+ });
113
+
114
+ it("returns error when a referenced work center is inactive", async () => {
115
+ const { db, spies } = createMockDb<Transaction>();
116
+
117
+ // select: Routing lookup
118
+ spies.select.mockReturnValueOnce(baseDraftRouting);
119
+ // select: RoutingOperation list
120
+ spies.select.mockReturnValueOnce([baseRoutingOperation]);
121
+ // select: WorkCenter lookup - INACTIVE
122
+ spies.select.mockReturnValueOnce(baseInactiveWorkCenter);
123
+
124
+ const result = await run(db, validInput, ctx);
125
+
126
+ expect(result.ok).toBe(false);
127
+ if (!result.ok) {
128
+ expect(result.error).toBeInstanceOf(WorkCenterNotActiveError);
129
+ }
130
+ });
131
+
132
+ it("returns error when a referenced work center is outside the routing company", async () => {
133
+ const { db, spies } = createMockDb<Transaction>();
134
+
135
+ // select: Routing lookup
136
+ spies.select.mockReturnValueOnce(baseDraftRouting);
137
+ // select: RoutingOperation list
138
+ spies.select.mockReturnValueOnce([baseRoutingOperation]);
139
+ // select: WorkCenter lookup - different company
140
+ spies.select.mockReturnValueOnce({
141
+ ...baseActiveWorkCenter,
142
+ companyId: "other-company",
143
+ });
144
+
145
+ const result = await run(db, validInput, ctx);
146
+
147
+ expect(result.ok).toBe(false);
148
+ if (!result.ok) {
149
+ expect(result.error).toBeInstanceOf(CrossCompanyWorkCenterError);
150
+ }
151
+ });
152
+ });
@@ -0,0 +1,92 @@
1
+ import type { Transaction } from "../generated/kysely-tailordb";
2
+ import {
3
+ RoutingNotFoundError,
4
+ RoutingNotActivatableError,
5
+ OperationRequiredError,
6
+ DuplicateOperationSequenceError,
7
+ WorkCenterNotActiveError,
8
+ CrossCompanyWorkCenterError,
9
+ } from "../lib/errors.generated";
10
+ import { ok, err, type CommandContext } from "@tailor-platform/erp-kit/module";
11
+
12
+ export interface ActivateRoutingInput {
13
+ id: string;
14
+ from?: string[];
15
+ }
16
+
17
+ /**
18
+ * Function: activateRouting
19
+ *
20
+ * Validates a draft routing and transitions it to ACTIVE status.
21
+ * Enforces operation completeness, sequence integrity, and active
22
+ * work-center readiness.
23
+ */
24
+ export async function run(db: Transaction, input: ActivateRoutingInput, _ctx: CommandContext) {
25
+ const { id, from = ["DRAFT"] } = input;
26
+
27
+ // 1. Fetch routing
28
+ const routing = await db
29
+ .selectFrom("Routing")
30
+ .selectAll()
31
+ .where("id", "=", id)
32
+ .forUpdate()
33
+ .executeTakeFirst();
34
+
35
+ if (!routing) {
36
+ return err(new RoutingNotFoundError(id));
37
+ }
38
+
39
+ // 2. Verify allowed source status
40
+ if (!from.includes(routing.status)) {
41
+ return err(new RoutingNotActivatableError(id));
42
+ }
43
+
44
+ // 3. Fetch operations
45
+ const operations = await db
46
+ .selectFrom("RoutingOperation")
47
+ .selectAll()
48
+ .where("routingId", "=", id)
49
+ .execute();
50
+
51
+ // 4. At least one operation required
52
+ if (operations.length === 0) {
53
+ return err(new OperationRequiredError(id));
54
+ }
55
+
56
+ // 5. Validate unique sequence numbers
57
+ const seqNumbers = operations.map((op) => op.sequenceNumber);
58
+ const uniqueSeqs = new Set(seqNumbers);
59
+ if (uniqueSeqs.size !== seqNumbers.length) {
60
+ return err(new DuplicateOperationSequenceError(id));
61
+ }
62
+
63
+ // 6. Validate work centers are active and in same company
64
+ for (const op of operations) {
65
+ const workCenter = await db
66
+ .selectFrom("WorkCenter")
67
+ .selectAll()
68
+ .where("id", "=", op.workCenterId)
69
+ .executeTakeFirst();
70
+
71
+ if (workCenter?.status !== "ACTIVE") {
72
+ return err(new WorkCenterNotActiveError(op.workCenterId));
73
+ }
74
+
75
+ if (workCenter.companyId !== routing.companyId) {
76
+ return err(new CrossCompanyWorkCenterError(op.workCenterId));
77
+ }
78
+ }
79
+
80
+ // 7. Set status to ACTIVE
81
+ const updatedRouting = await db
82
+ .updateTable("Routing")
83
+ .set({
84
+ status: "ACTIVE",
85
+ updatedAt: new Date(),
86
+ })
87
+ .where("id", "=", id)
88
+ .returningAll()
89
+ .executeTakeFirst();
90
+
91
+ return ok({ routing: updatedRouting! });
92
+ }
@@ -0,0 +1,6 @@
1
+ // @generated — do not edit
2
+ import { permissions } from "../lib/permissions.generated";
3
+ import { run } from "./activateWorkCenter";
4
+ import { defineCommand } from "@tailor-platform/erp-kit/module";
5
+
6
+ export const activateWorkCenter = defineCommand(permissions.activateWorkCenter, run);
@@ -0,0 +1,135 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { createMockDb } from "../../../testing/index";
3
+ import type { Transaction } from "../generated/kysely-tailordb";
4
+ import {
5
+ WorkCenterNotFoundError,
6
+ WorkCenterNotActivatableError,
7
+ MissingCalendarContextError,
8
+ InvalidCapacityError,
9
+ InvalidRateError,
10
+ OverheadCurrencyRequiredError,
11
+ } from "../lib/errors.generated";
12
+ import {
13
+ baseDraftWorkCenter,
14
+ baseActiveWorkCenter,
15
+ baseInactiveWorkCenter,
16
+ } from "../testing/fixtures";
17
+ import { run } from "./activateWorkCenter";
18
+ import type { CommandContext } from "@tailor-platform/erp-kit/module";
19
+
20
+ describe("activateWorkCenter", () => {
21
+ const ctx: CommandContext = {
22
+ actorId: "test-actor",
23
+ permissions: ["manufacturing:activateWorkCenter"],
24
+ };
25
+
26
+ it("activates a valid draft work center", async () => {
27
+ const { db, spies } = createMockDb<Transaction>();
28
+ const activated = { ...baseDraftWorkCenter, status: "ACTIVE" as const };
29
+
30
+ spies.select.mockReturnValueOnce(baseDraftWorkCenter);
31
+ spies.update.mockReturnValue(activated);
32
+
33
+ const result = await run(db, { id: baseDraftWorkCenter.id }, ctx);
34
+
35
+ expect(result.ok).toBe(true);
36
+ if (result.ok) {
37
+ expect(result.value.workCenter.status).toBe("ACTIVE");
38
+ }
39
+ expect(spies.update).toHaveBeenCalled();
40
+ });
41
+
42
+ it("reactivates a valid inactive work center", async () => {
43
+ const { db, spies } = createMockDb<Transaction>();
44
+ const reactivated = { ...baseInactiveWorkCenter, status: "ACTIVE" as const };
45
+
46
+ spies.select.mockReturnValueOnce(baseInactiveWorkCenter);
47
+ spies.update.mockReturnValue(reactivated);
48
+
49
+ const result = await run(db, { id: baseInactiveWorkCenter.id }, ctx);
50
+
51
+ expect(result.ok).toBe(true);
52
+ if (result.ok) {
53
+ expect(result.value.workCenter.status).toBe("ACTIVE");
54
+ }
55
+ expect(spies.update).toHaveBeenCalled();
56
+ });
57
+
58
+ it("returns error when the work center does not exist", async () => {
59
+ const { db, spies } = createMockDb<Transaction>();
60
+ spies.select.mockReturnValueOnce(undefined);
61
+
62
+ const result = await run(db, { id: "nonexistent" }, ctx);
63
+
64
+ expect(result.ok).toBe(false);
65
+ if (!result.ok) {
66
+ expect(result.error).toBeInstanceOf(WorkCenterNotFoundError);
67
+ }
68
+ });
69
+
70
+ it("returns error when the status does not allow activation", async () => {
71
+ const { db, spies } = createMockDb<Transaction>();
72
+ spies.select.mockReturnValueOnce(baseActiveWorkCenter);
73
+
74
+ const result = await run(db, { id: baseActiveWorkCenter.id }, ctx);
75
+
76
+ expect(result.ok).toBe(false);
77
+ if (!result.ok) {
78
+ expect(result.error).toBeInstanceOf(WorkCenterNotActivatableError);
79
+ }
80
+ });
81
+
82
+ it("returns error when required calendar context is missing", async () => {
83
+ const { db, spies } = createMockDb<Transaction>();
84
+ const noCalendar = { ...baseDraftWorkCenter, calendarReference: null };
85
+ spies.select.mockReturnValueOnce(noCalendar);
86
+
87
+ const result = await run(db, { id: baseDraftWorkCenter.id }, ctx);
88
+
89
+ expect(result.ok).toBe(false);
90
+ if (!result.ok) {
91
+ expect(result.error).toBeInstanceOf(MissingCalendarContextError);
92
+ }
93
+ });
94
+
95
+ it("returns error when capacity is not positive", async () => {
96
+ const { db, spies } = createMockDb<Transaction>();
97
+ const zeroCapacity = { ...baseDraftWorkCenter, capacityAssumptions: 0 };
98
+ spies.select.mockReturnValueOnce(zeroCapacity);
99
+
100
+ const result = await run(db, { id: baseDraftWorkCenter.id }, ctx);
101
+
102
+ expect(result.ok).toBe(false);
103
+ if (!result.ok) {
104
+ expect(result.error).toBeInstanceOf(InvalidCapacityError);
105
+ }
106
+ });
107
+
108
+ it("returns error when a configured rate is negative", async () => {
109
+ const { db, spies } = createMockDb<Transaction>();
110
+ spies.select.mockReturnValueOnce({ ...baseDraftWorkCenter, laborRate: -1 });
111
+
112
+ const result = await run(db, { id: baseDraftWorkCenter.id }, ctx);
113
+
114
+ expect(result.ok).toBe(false);
115
+ if (!result.ok) {
116
+ expect(result.error).toBeInstanceOf(InvalidRateError);
117
+ }
118
+ });
119
+
120
+ it("returns error when fixed overhead is missing its currency", async () => {
121
+ const { db, spies } = createMockDb<Transaction>();
122
+ spies.select.mockReturnValueOnce({
123
+ ...baseDraftWorkCenter,
124
+ overheadAbsorptionMethod: "FIXED_AMOUNT_PER_GOOD_UNIT",
125
+ overheadAbsorptionCurrency: null,
126
+ });
127
+
128
+ const result = await run(db, { id: baseDraftWorkCenter.id }, ctx);
129
+
130
+ expect(result.ok).toBe(false);
131
+ if (!result.ok) {
132
+ expect(result.error).toBeInstanceOf(OverheadCurrencyRequiredError);
133
+ }
134
+ });
135
+ });
@@ -0,0 +1,91 @@
1
+ import type { Transaction } from "../generated/kysely-tailordb";
2
+ import {
3
+ WorkCenterNotFoundError,
4
+ WorkCenterNotActivatableError,
5
+ MissingCalendarContextError,
6
+ InvalidCapacityError,
7
+ InvalidRateError,
8
+ OverheadCurrencyRequiredError,
9
+ } from "../lib/errors.generated";
10
+ import { ok, err, type CommandContext } from "@tailor-platform/erp-kit/module";
11
+
12
+ const ACTIVATABLE_STATUSES = ["DRAFT", "INACTIVE"] as const;
13
+
14
+ export interface ActivateWorkCenterInput {
15
+ id: string;
16
+ from?: string[];
17
+ }
18
+
19
+ /**
20
+ * Function: activateWorkCenter
21
+ *
22
+ * Validates that a draft or inactive work center has the execution data
23
+ * required by planning and costing, then marks it available for routing
24
+ * and work-order use.
25
+ */
26
+ export async function run<CF extends Record<string, unknown>>(
27
+ db: Transaction,
28
+ input: ActivateWorkCenterInput & CF,
29
+ _ctx: CommandContext,
30
+ ) {
31
+ const { id, from, ...customFields } = input;
32
+ void customFields;
33
+
34
+ const allowedStatuses = from ?? [...ACTIVATABLE_STATUSES];
35
+
36
+ // 1. Fetch work center with lock
37
+ const workCenter = await db
38
+ .selectFrom("WorkCenter")
39
+ .selectAll()
40
+ .where("id", "=", id)
41
+ .forUpdate()
42
+ .executeTakeFirst();
43
+
44
+ if (!workCenter) {
45
+ return err(new WorkCenterNotFoundError(id));
46
+ }
47
+
48
+ // 2. Validate status is activatable
49
+ if (!allowedStatuses.includes(workCenter.status)) {
50
+ return err(new WorkCenterNotActivatableError(workCenter.code));
51
+ }
52
+
53
+ // 3. Validate calendar context
54
+ if (!workCenter.calendarReference) {
55
+ return err(new MissingCalendarContextError(workCenter.code));
56
+ }
57
+
58
+ // 4. Validate capacity > 0
59
+ if (workCenter.capacityAssumptions <= 0) {
60
+ return err(new InvalidCapacityError(workCenter.code));
61
+ }
62
+
63
+ // 5. Validate rates >= 0
64
+ if (workCenter.laborRate != null && workCenter.laborRate < 0) {
65
+ return err(new InvalidRateError(workCenter.code));
66
+ }
67
+ if (workCenter.machineRate != null && workCenter.machineRate < 0) {
68
+ return err(new InvalidRateError(workCenter.code));
69
+ }
70
+
71
+ // 6. Validate overhead currency for FIXED_AMOUNT_PER_GOOD_UNIT
72
+ if (
73
+ workCenter.overheadAbsorptionMethod === "FIXED_AMOUNT_PER_GOOD_UNIT" &&
74
+ !workCenter.overheadAbsorptionCurrency
75
+ ) {
76
+ return err(new OverheadCurrencyRequiredError(workCenter.code));
77
+ }
78
+
79
+ // 7. Set status to ACTIVE
80
+ const updatedWorkCenter = await db
81
+ .updateTable("WorkCenter")
82
+ .set({
83
+ status: "ACTIVE",
84
+ updatedAt: new Date(),
85
+ })
86
+ .where("id", "=", id)
87
+ .returningAll()
88
+ .executeTakeFirst();
89
+
90
+ return ok({ workCenter: updatedWorkCenter! });
91
+ }
@@ -0,0 +1,6 @@
1
+ // @generated — do not edit
2
+ import { permissions } from "../lib/permissions.generated";
3
+ import { run } from "./cancelProductionOrder";
4
+ import { defineCommand } from "@tailor-platform/erp-kit/module";
5
+
6
+ export const cancelProductionOrder = defineCommand(permissions.cancelProductionOrder, run);