@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,174 @@
1
+ import type { Transaction } from "../generated/kysely-tailordb";
2
+ import {
3
+ BomNotFoundError,
4
+ BomNotMutableError,
5
+ InvalidComponentQuantityError,
6
+ ComponentItemInactiveError,
7
+ AmbiguousEffectivityRuleError,
8
+ } from "../lib/errors.generated";
9
+ import { ok, err, type CommandContext } from "@tailor-platform/erp-kit/module";
10
+
11
+ export interface UpdateBillOfMaterialLineInput {
12
+ itemId: string;
13
+ requiredQuantity: number;
14
+ unitOfMeasure?: string | null;
15
+ scrapAssumption?: number | null;
16
+ isSubassembly?: boolean | null;
17
+ }
18
+
19
+ export interface UpdateBillOfMaterialInput {
20
+ id: string;
21
+ bomType?: string | null;
22
+ effectivityStartDate?: Date | null;
23
+ effectivityEndDate?: Date | null;
24
+ defaultSelection?: boolean | null;
25
+ revisionNumber?: string | null;
26
+ lines?: UpdateBillOfMaterialLineInput[];
27
+ }
28
+
29
+ /**
30
+ * Function: updateBillOfMaterial
31
+ *
32
+ * Revises mutable draft BOM content before activation. Supports changes to
33
+ * effectivity, default flags, bomType, and component lines while preserving
34
+ * released production-order snapshots.
35
+ */
36
+ export async function run<CF extends Record<string, unknown>>(
37
+ db: Transaction,
38
+ input: UpdateBillOfMaterialInput & CF,
39
+ _ctx: CommandContext,
40
+ ) {
41
+ const {
42
+ id,
43
+ bomType,
44
+ effectivityStartDate,
45
+ effectivityEndDate,
46
+ defaultSelection,
47
+ revisionNumber,
48
+ lines,
49
+ ...customFields
50
+ } = input;
51
+
52
+ // 1. Fetch BOM with lock
53
+ const bom = await db
54
+ .selectFrom("BillOfMaterial")
55
+ .selectAll()
56
+ .where("id", "=", id)
57
+ .forUpdate()
58
+ .executeTakeFirst();
59
+
60
+ if (!bom) {
61
+ return err(new BomNotFoundError(id));
62
+ }
63
+
64
+ // 2. Only DRAFT BOMs are mutable
65
+ if (bom.status !== "DRAFT") {
66
+ return err(new BomNotMutableError(id));
67
+ }
68
+
69
+ // 3. Validate revised component lines if provided
70
+ if (lines) {
71
+ for (const line of lines) {
72
+ if (line.requiredQuantity <= 0) {
73
+ return err(new InvalidComponentQuantityError(line.itemId));
74
+ }
75
+
76
+ // Check component item is not inactive
77
+ const componentItem = await db
78
+ .selectFrom("Item")
79
+ .selectAll()
80
+ .where("id", "=", line.itemId)
81
+ .executeTakeFirst();
82
+
83
+ if (!componentItem) {
84
+ return err(new ComponentItemInactiveError(line.itemId));
85
+ }
86
+ }
87
+ }
88
+
89
+ // 4. Check effectivity ambiguity — if effectivity or default selection changed,
90
+ // verify no ambiguous active-selection plan would result once activated
91
+ const effectiveDefaultSelection =
92
+ defaultSelection !== undefined ? defaultSelection : bom.defaultSelection;
93
+ const effectiveStartDate =
94
+ effectivityStartDate !== undefined ? effectivityStartDate : bom.effectivityStartDate;
95
+ const effectiveEndDate =
96
+ effectivityEndDate !== undefined ? effectivityEndDate : bom.effectivityEndDate;
97
+
98
+ if (
99
+ defaultSelection !== undefined ||
100
+ effectivityStartDate !== undefined ||
101
+ effectivityEndDate !== undefined
102
+ ) {
103
+ // Find other active BOMs for same parent item and scope that could conflict
104
+ const conflictingBom = await db
105
+ .selectFrom("BillOfMaterial")
106
+ .selectAll()
107
+ .where("parentItemId", "=", bom.parentItemId)
108
+ .where("companyId", "=", bom.companyId)
109
+ .where("id", "!=", id)
110
+ .where("status", "=", "ACTIVE")
111
+ .where("defaultSelection", "=", effectiveDefaultSelection ?? false)
112
+ .executeTakeFirst();
113
+
114
+ if (conflictingBom && effectiveDefaultSelection) {
115
+ // Check date overlap
116
+ const hasOverlap =
117
+ !effectiveStartDate ||
118
+ !conflictingBom.effectivityEndDate ||
119
+ effectiveStartDate <= conflictingBom.effectivityEndDate;
120
+ const hasOverlap2 =
121
+ !effectiveEndDate ||
122
+ !conflictingBom.effectivityStartDate ||
123
+ effectiveEndDate >= conflictingBom.effectivityStartDate;
124
+
125
+ if (hasOverlap && hasOverlap2) {
126
+ return err(new AmbiguousEffectivityRuleError(id));
127
+ }
128
+ }
129
+ }
130
+
131
+ // 5. Build update set for BOM header
132
+ const updateSet: Record<string, unknown> = {
133
+ ...customFields,
134
+ updatedAt: new Date(),
135
+ };
136
+
137
+ if (bomType !== undefined) updateSet.bomType = bomType;
138
+ if (effectivityStartDate !== undefined) updateSet.effectivityStartDate = effectivityStartDate;
139
+ if (effectivityEndDate !== undefined) updateSet.effectivityEndDate = effectivityEndDate;
140
+ if (defaultSelection !== undefined) updateSet.defaultSelection = defaultSelection;
141
+ if (revisionNumber !== undefined) updateSet.revisionNumber = revisionNumber;
142
+
143
+ // 6. Persist BOM header changes
144
+ const updatedBom = await db
145
+ .updateTable("BillOfMaterial")
146
+ .set(updateSet)
147
+ .where("id", "=", id)
148
+ .returningAll()
149
+ .executeTakeFirstOrThrow();
150
+
151
+ // 7. Replace lines if provided (delete old, insert new)
152
+ if (lines) {
153
+ await db.deleteFrom("BillOfMaterialLine").where("billOfMaterialId", "=", id).execute();
154
+
155
+ for (const line of lines) {
156
+ await db
157
+ .insertInto("BillOfMaterialLine")
158
+ .values({
159
+ billOfMaterialId: id,
160
+ itemId: line.itemId,
161
+ requiredQuantity: line.requiredQuantity,
162
+ unitOfMeasure: line.unitOfMeasure ?? null,
163
+ scrapAssumption: line.scrapAssumption ?? null,
164
+ isSubassembly: line.isSubassembly ?? null,
165
+ createdAt: new Date(),
166
+ updatedAt: null,
167
+ })
168
+ .returningAll()
169
+ .executeTakeFirst();
170
+ }
171
+ }
172
+
173
+ return ok({ billOfMaterial: updatedBom });
174
+ }
@@ -0,0 +1,6 @@
1
+ // @generated — do not edit
2
+ import { permissions } from "../lib/permissions.generated";
3
+ import { run } from "./updateProductionOrder";
4
+ import { defineCommand } from "@tailor-platform/erp-kit/module";
5
+
6
+ export const updateProductionOrder = defineCommand(permissions.updateProductionOrder, run);
@@ -0,0 +1,112 @@
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
+ ProductionOrderNotMutableError,
7
+ InvalidPlannedQuantityError,
8
+ ImmutableScopeFieldError,
9
+ CrossScopeMasterReferenceError,
10
+ } from "../lib/errors.generated";
11
+ import {
12
+ baseDraftProductionOrder,
13
+ baseReleasedProductionOrder,
14
+ baseActiveBom,
15
+ } from "../testing/fixtures";
16
+ import { run } from "./updateProductionOrder";
17
+ import type { CommandContext } from "@tailor-platform/erp-kit/module";
18
+
19
+ describe("updateProductionOrder", () => {
20
+ const ctx: CommandContext = {
21
+ actorId: "test-actor",
22
+ permissions: ["manufacturing:updateProductionOrder"],
23
+ };
24
+
25
+ it("updates draft planning fields on a production order", async () => {
26
+ const { db, spies } = createMockDb<Transaction>();
27
+ const updated = { ...baseDraftProductionOrder, plannedQuantity: 200 };
28
+
29
+ spies.select.mockReturnValueOnce(baseDraftProductionOrder);
30
+ spies.update.mockReturnValue(updated);
31
+
32
+ const result = await run(db, { id: baseDraftProductionOrder.id, plannedQuantity: 200 }, ctx);
33
+
34
+ expect(result.ok).toBe(true);
35
+ if (result.ok) {
36
+ expect(result.value.productionOrder.plannedQuantity).toBe(200);
37
+ }
38
+ expect(spies.update).toHaveBeenCalled();
39
+ });
40
+
41
+ it("returns error when the order does not exist", async () => {
42
+ const { db, spies } = createMockDb<Transaction>();
43
+ spies.select.mockReturnValueOnce(undefined);
44
+
45
+ const result = await run(db, { id: "nonexistent" }, ctx);
46
+
47
+ expect(result.ok).toBe(false);
48
+ if (!result.ok) {
49
+ expect(result.error).toBeInstanceOf(ProductionOrderNotFoundError);
50
+ }
51
+ });
52
+
53
+ it("returns error when the order is not in DRAFT", async () => {
54
+ const { db, spies } = createMockDb<Transaction>();
55
+ spies.select.mockReturnValueOnce(baseReleasedProductionOrder);
56
+
57
+ const result = await run(db, { id: baseReleasedProductionOrder.id, plannedQuantity: 200 }, ctx);
58
+
59
+ expect(result.ok).toBe(false);
60
+ if (!result.ok) {
61
+ expect(result.error).toBeInstanceOf(ProductionOrderNotMutableError);
62
+ }
63
+ });
64
+
65
+ it("returns error when planned quantity is not positive", async () => {
66
+ const { db, spies } = createMockDb<Transaction>();
67
+ spies.select.mockReturnValueOnce(baseDraftProductionOrder);
68
+
69
+ const result = await run(db, { id: baseDraftProductionOrder.id, plannedQuantity: 0 }, ctx);
70
+
71
+ expect(result.ok).toBe(false);
72
+ if (!result.ok) {
73
+ expect(result.error).toBeInstanceOf(InvalidPlannedQuantityError);
74
+ }
75
+ });
76
+
77
+ it("returns error when immutable scope identity is changed", async () => {
78
+ const { db, spies } = createMockDb<Transaction>();
79
+ spies.select.mockReturnValueOnce(baseDraftProductionOrder);
80
+
81
+ const result = await run(
82
+ db,
83
+ { id: baseDraftProductionOrder.id, orderedItemId: "different-item" },
84
+ ctx,
85
+ );
86
+
87
+ expect(result.ok).toBe(false);
88
+ if (!result.ok) {
89
+ expect(result.error).toBeInstanceOf(ImmutableScopeFieldError);
90
+ }
91
+ });
92
+
93
+ it("returns error when BOM or routing references are out of scope", async () => {
94
+ const { db, spies } = createMockDb<Transaction>();
95
+ const crossScopeBom = { ...baseActiveBom, companyId: "other-company" };
96
+
97
+ spies.select.mockReturnValueOnce(baseDraftProductionOrder);
98
+ // bom lookup
99
+ spies.select.mockReturnValueOnce(crossScopeBom);
100
+
101
+ const result = await run(
102
+ db,
103
+ { id: baseDraftProductionOrder.id, selectedBomVersionId: crossScopeBom.id },
104
+ ctx,
105
+ );
106
+
107
+ expect(result.ok).toBe(false);
108
+ if (!result.ok) {
109
+ expect(result.error).toBeInstanceOf(CrossScopeMasterReferenceError);
110
+ }
111
+ });
112
+ });
@@ -0,0 +1,145 @@
1
+ import type { Transaction } from "../generated/kysely-tailordb";
2
+ import {
3
+ ProductionOrderNotFoundError,
4
+ ProductionOrderNotMutableError,
5
+ InvalidPlannedQuantityError,
6
+ ImmutableScopeFieldError,
7
+ CrossScopeMasterReferenceError,
8
+ } from "../lib/errors.generated";
9
+ import { ok, err, type CommandContext } from "@tailor-platform/erp-kit/module";
10
+
11
+ export interface UpdateProductionOrderInput {
12
+ id: string;
13
+ plannedQuantity?: number | null;
14
+ plannedStartDate?: Date | null;
15
+ plannedEndDate?: Date | null;
16
+ selectedBomVersionId?: string | null;
17
+ selectedRoutingRevisionId?: string | null;
18
+ orderedItemId?: string | null;
19
+ companyId?: string | null;
20
+ siteId?: string | null;
21
+ }
22
+
23
+ /**
24
+ * Function: updateProductionOrder
25
+ *
26
+ * Revises mutable draft planning fields such as dates, quantity, and
27
+ * preferred BOM or routing before release freezes execution assumptions.
28
+ */
29
+ export async function run<CF extends Record<string, unknown>>(
30
+ db: Transaction,
31
+ input: UpdateProductionOrderInput & CF,
32
+ _ctx: CommandContext,
33
+ ) {
34
+ const {
35
+ id,
36
+ plannedQuantity,
37
+ plannedStartDate,
38
+ plannedEndDate,
39
+ selectedBomVersionId,
40
+ selectedRoutingRevisionId,
41
+ orderedItemId,
42
+ companyId,
43
+ siteId,
44
+ ...customFields
45
+ } = input;
46
+
47
+ // 1. Fetch production order with lock
48
+ const order = await db
49
+ .selectFrom("ProductionOrder")
50
+ .selectAll()
51
+ .where("id", "=", id)
52
+ .forUpdate()
53
+ .executeTakeFirst();
54
+
55
+ if (!order) {
56
+ return err(new ProductionOrderNotFoundError(id));
57
+ }
58
+
59
+ // 2. Only DRAFT orders are mutable
60
+ if (order.status !== "DRAFT") {
61
+ return err(new ProductionOrderNotMutableError(id));
62
+ }
63
+
64
+ // 3. Validate planned quantity if provided
65
+ if (plannedQuantity != null && plannedQuantity <= 0) {
66
+ return err(new InvalidPlannedQuantityError(id));
67
+ }
68
+
69
+ // 4. Immutable scope fields
70
+ if (
71
+ orderedItemId !== undefined &&
72
+ orderedItemId !== null &&
73
+ orderedItemId !== order.orderedItemId
74
+ ) {
75
+ return err(new ImmutableScopeFieldError(id));
76
+ }
77
+ if (companyId !== undefined && companyId !== null && companyId !== order.companyId) {
78
+ return err(new ImmutableScopeFieldError(id));
79
+ }
80
+ if (siteId !== undefined && siteId !== null && siteId !== order.siteId) {
81
+ return err(new ImmutableScopeFieldError(id));
82
+ }
83
+
84
+ // 5. Validate optional BOM reference scope
85
+ const effectiveBomId =
86
+ selectedBomVersionId !== undefined ? selectedBomVersionId : order.selectedBomVersionId;
87
+ if (effectiveBomId) {
88
+ const bom = await db
89
+ .selectFrom("BillOfMaterial")
90
+ .selectAll()
91
+ .where("id", "=", effectiveBomId)
92
+ .executeTakeFirst();
93
+
94
+ if (
95
+ bom &&
96
+ (bom.companyId !== order.companyId || (bom.siteId != null && bom.siteId !== order.siteId))
97
+ ) {
98
+ return err(new CrossScopeMasterReferenceError(id));
99
+ }
100
+ }
101
+
102
+ // 6. Validate optional routing reference scope
103
+ const effectiveRoutingId =
104
+ selectedRoutingRevisionId !== undefined
105
+ ? selectedRoutingRevisionId
106
+ : order.selectedRoutingRevisionId;
107
+ if (effectiveRoutingId) {
108
+ const routing = await db
109
+ .selectFrom("Routing")
110
+ .selectAll()
111
+ .where("id", "=", effectiveRoutingId)
112
+ .executeTakeFirst();
113
+
114
+ if (
115
+ routing &&
116
+ (routing.companyId !== order.companyId ||
117
+ (routing.siteId != null && routing.siteId !== order.siteId))
118
+ ) {
119
+ return err(new CrossScopeMasterReferenceError(id));
120
+ }
121
+ }
122
+
123
+ // 7. Build update set
124
+ const updateSet: Record<string, unknown> = {
125
+ ...customFields,
126
+ updatedAt: new Date(),
127
+ };
128
+
129
+ if (plannedQuantity !== undefined) updateSet.plannedQuantity = plannedQuantity;
130
+ if (plannedStartDate !== undefined) updateSet.plannedStartDate = plannedStartDate;
131
+ if (plannedEndDate !== undefined) updateSet.plannedEndDate = plannedEndDate;
132
+ if (selectedBomVersionId !== undefined) updateSet.selectedBomVersionId = selectedBomVersionId;
133
+ if (selectedRoutingRevisionId !== undefined)
134
+ updateSet.selectedRoutingRevisionId = selectedRoutingRevisionId;
135
+
136
+ // 8. Persist changes
137
+ const updated = await db
138
+ .updateTable("ProductionOrder")
139
+ .set(updateSet)
140
+ .where("id", "=", id)
141
+ .returningAll()
142
+ .executeTakeFirstOrThrow();
143
+
144
+ return ok({ productionOrder: updated });
145
+ }
@@ -0,0 +1,6 @@
1
+ // @generated — do not edit
2
+ import { permissions } from "../lib/permissions.generated";
3
+ import { run } from "./updateRouting";
4
+ import { defineCommand } from "@tailor-platform/erp-kit/module";
5
+
6
+ export const updateRouting = defineCommand(permissions.updateRouting, run);
@@ -0,0 +1,211 @@
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
+ RoutingNotMutableError,
7
+ DuplicateOperationSequenceError,
8
+ InvalidStandardTimeError,
9
+ WorkCenterNotFoundError,
10
+ CrossCompanyWorkCenterError,
11
+ } from "../lib/errors.generated";
12
+ import { baseDraftRouting, baseActiveRouting, baseActiveWorkCenter } from "../testing/fixtures";
13
+ import { run } from "./updateRouting";
14
+ import type { CommandContext } from "@tailor-platform/erp-kit/module";
15
+
16
+ describe("updateRouting", () => {
17
+ const ctx: CommandContext = {
18
+ actorId: "test-actor",
19
+ permissions: ["manufacturing:updateRouting"],
20
+ };
21
+
22
+ const validOperations = [
23
+ {
24
+ sequenceNumber: 10,
25
+ operationDescription: "Revised assembly",
26
+ workCenterId: "work-center-2",
27
+ standardSetupTime: 25,
28
+ standardRunTime: 55,
29
+ operatorInstructions: null,
30
+ },
31
+ {
32
+ sequenceNumber: 20,
33
+ operationDescription: "Revised QC",
34
+ workCenterId: "work-center-2",
35
+ standardSetupTime: 15,
36
+ standardRunTime: 20,
37
+ operatorInstructions: null,
38
+ },
39
+ ];
40
+
41
+ const validInput = {
42
+ id: "routing-1",
43
+ operations: validOperations,
44
+ };
45
+
46
+ it("updates draft routing operations", async () => {
47
+ const { db, spies } = createMockDb<Transaction>();
48
+
49
+ // select: Routing lookup
50
+ spies.select.mockReturnValueOnce(baseDraftRouting);
51
+ // select: WorkCenter lookup for op 1
52
+ spies.select.mockReturnValueOnce(baseActiveWorkCenter);
53
+ // select: WorkCenter lookup for op 2
54
+ spies.select.mockReturnValueOnce(baseActiveWorkCenter);
55
+ // update: Routing header
56
+ spies.update.mockReturnValue({ ...baseDraftRouting, updatedAt: new Date() });
57
+ // insert: new operations
58
+ spies.insert.mockReturnValue({
59
+ id: "op-new",
60
+ routingId: "routing-1",
61
+ sequenceNumber: 10,
62
+ operationDescription: "Revised assembly",
63
+ workCenterId: "work-center-2",
64
+ standardSetupTime: 25,
65
+ standardRunTime: 55,
66
+ operatorInstructions: null,
67
+ createdAt: new Date(),
68
+ updatedAt: null,
69
+ });
70
+
71
+ const result = await run(db, validInput, ctx);
72
+
73
+ expect(result.ok).toBe(true);
74
+ if (result.ok) {
75
+ expect(result.value.routing).toBeDefined();
76
+ }
77
+ expect(spies.update).toHaveBeenCalled();
78
+ });
79
+
80
+ it("returns error when the routing does not exist", async () => {
81
+ const { db, spies } = createMockDb<Transaction>();
82
+
83
+ // select: Routing lookup - not found
84
+ spies.select.mockReturnValueOnce(undefined);
85
+
86
+ const result = await run(db, validInput, ctx);
87
+
88
+ expect(result.ok).toBe(false);
89
+ if (!result.ok) {
90
+ expect(result.error).toBeInstanceOf(RoutingNotFoundError);
91
+ }
92
+ });
93
+
94
+ it("returns error when the routing is not in DRAFT", async () => {
95
+ const { db, spies } = createMockDb<Transaction>();
96
+
97
+ // select: Routing lookup - ACTIVE
98
+ spies.select.mockReturnValueOnce(baseActiveRouting);
99
+
100
+ const result = await run(db, { ...validInput, id: "routing-2" }, ctx);
101
+
102
+ expect(result.ok).toBe(false);
103
+ if (!result.ok) {
104
+ expect(result.error).toBeInstanceOf(RoutingNotMutableError);
105
+ }
106
+ });
107
+
108
+ it("returns error when revised sequence numbers conflict", async () => {
109
+ const { db, spies } = createMockDb<Transaction>();
110
+
111
+ // select: Routing lookup
112
+ spies.select.mockReturnValueOnce(baseDraftRouting);
113
+
114
+ const duplicateOps = [
115
+ { ...validOperations[0], sequenceNumber: 10 },
116
+ { ...validOperations[1], sequenceNumber: 10 },
117
+ ];
118
+
119
+ const result = await run(db, { ...validInput, operations: duplicateOps }, ctx);
120
+
121
+ expect(result.ok).toBe(false);
122
+ if (!result.ok) {
123
+ expect(result.error).toBeInstanceOf(DuplicateOperationSequenceError);
124
+ }
125
+ });
126
+
127
+ it("returns error when a revised standard time is negative", async () => {
128
+ const { db, spies } = createMockDb<Transaction>();
129
+
130
+ // select: Routing lookup
131
+ spies.select.mockReturnValueOnce(baseDraftRouting);
132
+
133
+ const badOps = [{ ...validOperations[0], standardRunTime: -1 }];
134
+
135
+ const result = await run(db, { ...validInput, operations: badOps }, ctx);
136
+
137
+ expect(result.ok).toBe(false);
138
+ if (!result.ok) {
139
+ expect(result.error).toBeInstanceOf(InvalidStandardTimeError);
140
+ }
141
+ });
142
+
143
+ it("returns error when a referenced work center does not exist", async () => {
144
+ const { db, spies } = createMockDb<Transaction>();
145
+
146
+ // select: Routing lookup
147
+ spies.select.mockReturnValueOnce(baseDraftRouting);
148
+ // select: WorkCenter lookup - not found
149
+ spies.select.mockReturnValueOnce(undefined);
150
+
151
+ const result = await run(db, validInput, ctx);
152
+
153
+ expect(result.ok).toBe(false);
154
+ if (!result.ok) {
155
+ expect(result.error).toBeInstanceOf(WorkCenterNotFoundError);
156
+ }
157
+ });
158
+
159
+ it("returns error when a referenced work center is outside the routing company", async () => {
160
+ const { db, spies } = createMockDb<Transaction>();
161
+
162
+ // select: Routing lookup
163
+ spies.select.mockReturnValueOnce(baseDraftRouting);
164
+ // select: WorkCenter lookup - different company
165
+ spies.select.mockReturnValueOnce({
166
+ ...baseActiveWorkCenter,
167
+ companyId: "other-company",
168
+ });
169
+
170
+ const result = await run(db, validInput, ctx);
171
+
172
+ expect(result.ok).toBe(false);
173
+ if (!result.ok) {
174
+ expect(result.error).toBeInstanceOf(CrossCompanyWorkCenterError);
175
+ }
176
+ });
177
+
178
+ it("preserves released routing snapshots after update", async () => {
179
+ const { db, spies } = createMockDb<Transaction>();
180
+
181
+ // select: Routing lookup - DRAFT (only DRAFT can be updated)
182
+ spies.select.mockReturnValueOnce(baseDraftRouting);
183
+ // select: WorkCenter lookup for op 1
184
+ spies.select.mockReturnValueOnce(baseActiveWorkCenter);
185
+ // update: Routing header
186
+ spies.update.mockReturnValue({ ...baseDraftRouting, updatedAt: new Date() });
187
+ // insert: new operations
188
+ spies.insert.mockReturnValue({
189
+ id: "op-new",
190
+ routingId: "routing-1",
191
+ sequenceNumber: 10,
192
+ operationDescription: "Revised assembly",
193
+ workCenterId: "work-center-2",
194
+ standardSetupTime: 25,
195
+ standardRunTime: 55,
196
+ operatorInstructions: null,
197
+ createdAt: new Date(),
198
+ updatedAt: null,
199
+ });
200
+
201
+ const singleOp = [validOperations[0]];
202
+ const result = await run(db, { ...validInput, operations: singleOp }, ctx);
203
+
204
+ // The update only touches the Routing and RoutingOperation tables,
205
+ // not ProductionOrderRoutingSnapshot, preserving frozen snapshots.
206
+ expect(result.ok).toBe(true);
207
+ if (result.ok) {
208
+ expect(result.value.routing.status).toBe("DRAFT");
209
+ }
210
+ });
211
+ });