@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,306 @@
1
+ import type { DB } from "../generated/kysely-tailordb";
2
+ import {
3
+ BomReferenceRequiredError,
4
+ EffectivityDateRequiredError,
5
+ BomNotFoundError,
6
+ BomNotEffectiveOnDateError,
7
+ ChildBomNotResolvedError,
8
+ BomCircularReferenceDetectedError,
9
+ } from "../lib/errors.generated";
10
+ import { ok, err, type ReadonlyDB } from "@tailor-platform/erp-kit/module";
11
+
12
+ export interface ExplodeBillOfMaterialInput {
13
+ bomId: string;
14
+ effectivityDate: Date;
15
+ }
16
+
17
+ interface ExplodedComponent {
18
+ itemId: string;
19
+ requiredQuantity: number;
20
+ unitOfMeasure: string | null;
21
+ depth: number;
22
+ bomPath: string[];
23
+ isSubassembly: boolean;
24
+ }
25
+
26
+ /**
27
+ * Function: explodeBillOfMaterial
28
+ *
29
+ * Expands one BOM revision into recursive component requirements using the
30
+ * requested effectivity date and site-applicable child BOM scope.
31
+ */
32
+ export async function run(db: ReadonlyDB<DB>, input: ExplodeBillOfMaterialInput) {
33
+ if (!input.bomId) {
34
+ return err(new BomReferenceRequiredError("bomId"));
35
+ }
36
+
37
+ if (!input.effectivityDate) {
38
+ return err(new EffectivityDateRequiredError("effectivityDate"));
39
+ }
40
+
41
+ const rootBom = await db
42
+ .selectFrom("BillOfMaterial")
43
+ .selectAll()
44
+ .where("id", "=", input.bomId)
45
+ .executeTakeFirst();
46
+
47
+ if (!rootBom) {
48
+ return err(new BomNotFoundError(input.bomId));
49
+ }
50
+
51
+ if (!isEffectiveOnDate(rootBom, input.effectivityDate)) {
52
+ return err(new BomNotEffectiveOnDateError(input.bomId));
53
+ }
54
+
55
+ const components: ExplodedComponent[] = [];
56
+ const visitedItems = new Set<string>();
57
+
58
+ const result = await expandBom(
59
+ db,
60
+ rootBom.id,
61
+ rootBom.companyId,
62
+ rootBom.siteId,
63
+ rootBom.bomType as string,
64
+ input.effectivityDate,
65
+ 1, // multiplier
66
+ 0, // depth
67
+ [rootBom.id],
68
+ visitedItems,
69
+ components,
70
+ );
71
+
72
+ if (result) {
73
+ return result; // error result
74
+ }
75
+
76
+ return ok({ components });
77
+ }
78
+
79
+ function isEffectiveOnDate(
80
+ bom: { effectivityStartDate: unknown; effectivityEndDate: unknown },
81
+ date: Date,
82
+ ): boolean {
83
+ const dateMs = date.getTime();
84
+
85
+ if (bom.effectivityStartDate) {
86
+ const start = new Date(bom.effectivityStartDate as string);
87
+ if (dateMs < start.getTime()) {
88
+ return false;
89
+ }
90
+ }
91
+
92
+ if (bom.effectivityEndDate) {
93
+ const end = new Date(bom.effectivityEndDate as string);
94
+ if (dateMs > end.getTime()) {
95
+ return false;
96
+ }
97
+ }
98
+
99
+ return true;
100
+ }
101
+
102
+ function resolveApplicableChildBom(
103
+ childBoms: {
104
+ id: string;
105
+ siteId?: string | null;
106
+ bomType: string;
107
+ effectivityStartDate: unknown;
108
+ effectivityEndDate: unknown;
109
+ }[],
110
+ effectivityDate: Date,
111
+ applicableSiteId: string | null,
112
+ ) {
113
+ const effectiveBoms = childBoms.filter((bom) => {
114
+ if (!isEffectiveOnDate(bom, effectivityDate)) {
115
+ return false;
116
+ }
117
+
118
+ if (applicableSiteId == null) {
119
+ return bom.siteId == null;
120
+ }
121
+
122
+ return bom.siteId == null || bom.siteId === applicableSiteId;
123
+ });
124
+
125
+ return (
126
+ effectiveBoms.find((bom) => bom.siteId === applicableSiteId) ??
127
+ effectiveBoms.find((bom) => bom.siteId == null)
128
+ );
129
+ }
130
+
131
+ async function expandBom(
132
+ db: ReadonlyDB<DB>,
133
+ bomId: string,
134
+ companyId: string,
135
+ applicableSiteId: string | null,
136
+ bomType: string,
137
+ effectivityDate: Date,
138
+ parentQuantityMultiplier: number,
139
+ depth: number,
140
+ bomPath: string[],
141
+ visitedItems: Set<string>,
142
+ components: ExplodedComponent[],
143
+ ): Promise<{ ok: false; error: Error } | undefined> {
144
+ const lines = await db
145
+ .selectFrom("BillOfMaterialLine")
146
+ .selectAll()
147
+ .where("billOfMaterialId", "=", bomId)
148
+ .execute();
149
+
150
+ for (const line of lines) {
151
+ const requiredQuantity = line.requiredQuantity * parentQuantityMultiplier;
152
+ const isSubassembly = line.isSubassembly === true;
153
+
154
+ // KIT: issue-only component demand — no recursive expansion
155
+ if (bomType === "KIT") {
156
+ components.push({
157
+ itemId: line.itemId,
158
+ requiredQuantity,
159
+ unitOfMeasure: line.unitOfMeasure,
160
+ depth: depth + 1,
161
+ bomPath: [...bomPath],
162
+ isSubassembly: false, // KIT components are always issue-only
163
+ });
164
+ continue;
165
+ }
166
+
167
+ // PHANTOM: flatten lines into parent path
168
+ if (bomType === "PHANTOM") {
169
+ if (isSubassembly) {
170
+ // Resolve child BOM and expand at SAME depth (no depth increment)
171
+ if (visitedItems.has(line.itemId)) {
172
+ return err(new BomCircularReferenceDetectedError(line.itemId));
173
+ }
174
+ visitedItems.add(line.itemId);
175
+
176
+ const childBoms = await db
177
+ .selectFrom("BillOfMaterial")
178
+ .selectAll()
179
+ .where("parentItemId", "=", line.itemId)
180
+ .where("companyId", "=", companyId)
181
+ .where("status", "=", "ACTIVE")
182
+ .execute();
183
+ const effectiveChildBom = resolveApplicableChildBom(
184
+ childBoms,
185
+ effectivityDate,
186
+ applicableSiteId,
187
+ );
188
+ if (!effectiveChildBom) {
189
+ return err(new ChildBomNotResolvedError(line.itemId));
190
+ }
191
+
192
+ const childResult = await expandBom(
193
+ db,
194
+ effectiveChildBom.id,
195
+ companyId,
196
+ applicableSiteId,
197
+ effectiveChildBom.bomType,
198
+ effectivityDate,
199
+ requiredQuantity,
200
+ depth, // same depth — flattening
201
+ [...bomPath, effectiveChildBom.id],
202
+ visitedItems,
203
+ components,
204
+ );
205
+ if (childResult) return childResult;
206
+ visitedItems.delete(line.itemId);
207
+ } else {
208
+ // Non-subassembly in phantom: push at current depth (flattened)
209
+ components.push({
210
+ itemId: line.itemId,
211
+ requiredQuantity,
212
+ unitOfMeasure: line.unitOfMeasure,
213
+ depth: depth + 1,
214
+ bomPath: [...bomPath],
215
+ isSubassembly: false,
216
+ });
217
+ }
218
+ continue;
219
+ }
220
+
221
+ // MANUFACTURE (default): add the component and recurse if subassembly
222
+ if (isSubassembly) {
223
+ if (visitedItems.has(line.itemId)) {
224
+ return err(new BomCircularReferenceDetectedError(line.itemId));
225
+ }
226
+ visitedItems.add(line.itemId);
227
+
228
+ // Find an active child BOM for this item effective on the date
229
+ const childBoms = await db
230
+ .selectFrom("BillOfMaterial")
231
+ .selectAll()
232
+ .where("parentItemId", "=", line.itemId)
233
+ .where("companyId", "=", companyId)
234
+ .where("status", "=", "ACTIVE")
235
+ .execute();
236
+
237
+ const effectiveChildBom = resolveApplicableChildBom(
238
+ childBoms,
239
+ effectivityDate,
240
+ applicableSiteId,
241
+ );
242
+
243
+ if (!effectiveChildBom) {
244
+ return err(new ChildBomNotResolvedError(line.itemId));
245
+ }
246
+
247
+ const childBomType = effectiveChildBom.bomType;
248
+
249
+ if (childBomType === "PHANTOM") {
250
+ // PHANTOM child: skip the phantom item, flatten its children to current depth
251
+ const childResult = await expandBom(
252
+ db,
253
+ effectiveChildBom.id,
254
+ companyId,
255
+ applicableSiteId,
256
+ childBomType,
257
+ effectivityDate,
258
+ requiredQuantity,
259
+ depth, // same depth — flattening
260
+ [...bomPath, effectiveChildBom.id],
261
+ visitedItems,
262
+ components,
263
+ );
264
+ if (childResult) return childResult;
265
+ } else {
266
+ // Non-phantom child: push the subassembly and recurse deeper
267
+ components.push({
268
+ itemId: line.itemId,
269
+ requiredQuantity,
270
+ unitOfMeasure: line.unitOfMeasure,
271
+ depth: depth + 1,
272
+ bomPath: [...bomPath],
273
+ isSubassembly: true,
274
+ });
275
+
276
+ const childResult = await expandBom(
277
+ db,
278
+ effectiveChildBom.id,
279
+ companyId,
280
+ applicableSiteId,
281
+ childBomType,
282
+ effectivityDate,
283
+ requiredQuantity,
284
+ depth + 1,
285
+ [...bomPath, effectiveChildBom.id],
286
+ visitedItems,
287
+ components,
288
+ );
289
+ if (childResult) return childResult;
290
+ }
291
+
292
+ visitedItems.delete(line.itemId);
293
+ } else {
294
+ components.push({
295
+ itemId: line.itemId,
296
+ requiredQuantity,
297
+ unitOfMeasure: line.unitOfMeasure,
298
+ depth: depth + 1,
299
+ bomPath: [...bomPath],
300
+ isSubassembly: false,
301
+ });
302
+ }
303
+ }
304
+
305
+ return undefined; // success, no error
306
+ }
@@ -0,0 +1,5 @@
1
+ // @generated — do not edit
2
+ import { run } from "./getBillOfMaterial";
3
+ import { defineQuery } from "@tailor-platform/erp-kit/module";
4
+
5
+ export const getBillOfMaterial = defineQuery(run);
@@ -0,0 +1,64 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { createMockDb } from "../../../testing/index";
3
+ import type { DB } from "../generated/kysely-tailordb";
4
+ import {
5
+ baseDraftBom,
6
+ baseActiveBom,
7
+ baseInactiveBom,
8
+ baseBomLine,
9
+ baseBomLine2,
10
+ } from "../testing/fixtures";
11
+ import { expectOk, expectErr } from "../testing/commandTestUtils";
12
+ import { BomNotFoundError } from "../lib/errors.generated";
13
+ import { run } from "./getBillOfMaterial";
14
+
15
+ describe("getBillOfMaterial", () => {
16
+ it("returns a BOM with component lines when found", async () => {
17
+ const { db, spies } = createMockDb<DB>();
18
+ spies.select.mockReturnValueOnce(baseActiveBom);
19
+ spies.select.mockReturnValueOnce([baseBomLine, baseBomLine2]);
20
+
21
+ const result = await run(db, { id: baseActiveBom.id });
22
+ const { billOfMaterial, lines } = expectOk<{
23
+ billOfMaterial: unknown;
24
+ lines: unknown[];
25
+ }>(result as never);
26
+
27
+ expect(billOfMaterial).toMatchObject({ id: baseActiveBom.id });
28
+ expect(lines).toHaveLength(2);
29
+ });
30
+
31
+ it("returns draft BOM versions when requested", async () => {
32
+ const { db, spies } = createMockDb<DB>();
33
+ spies.select.mockReturnValueOnce(baseDraftBom);
34
+ spies.select.mockReturnValueOnce([baseBomLine]);
35
+
36
+ const result = await run(db, { id: baseDraftBom.id });
37
+ const { billOfMaterial } = expectOk<{
38
+ billOfMaterial: { status: string };
39
+ }>(result as never);
40
+
41
+ expect(billOfMaterial.status).toBe("DRAFT");
42
+ });
43
+
44
+ it("returns inactive BOM versions when requested", async () => {
45
+ const { db, spies } = createMockDb<DB>();
46
+ spies.select.mockReturnValueOnce(baseInactiveBom);
47
+ spies.select.mockReturnValueOnce([]);
48
+
49
+ const result = await run(db, { id: baseInactiveBom.id });
50
+ const { billOfMaterial } = expectOk<{
51
+ billOfMaterial: { status: string };
52
+ }>(result as never);
53
+
54
+ expect(billOfMaterial.status).toBe("INACTIVE");
55
+ });
56
+
57
+ it("returns error when the BOM does not exist", async () => {
58
+ const { db, spies } = createMockDb<DB>();
59
+ spies.select.mockReturnValueOnce(undefined);
60
+
61
+ const result = await run(db, { id: "nonexistent-id" });
62
+ expectErr(result as never, BomNotFoundError);
63
+ });
64
+ });
@@ -0,0 +1,27 @@
1
+ import type { DB } from "../generated/kysely-tailordb";
2
+ import { BomNotFoundError } from "../lib/errors.generated";
3
+ import { ok, err, type ReadonlyDB } from "@tailor-platform/erp-kit/module";
4
+
5
+ export interface GetBillOfMaterialInput {
6
+ id: string;
7
+ }
8
+
9
+ export async function run(db: ReadonlyDB<DB>, input: GetBillOfMaterialInput) {
10
+ const billOfMaterial = await db
11
+ .selectFrom("BillOfMaterial")
12
+ .selectAll()
13
+ .where("id", "=", input.id)
14
+ .executeTakeFirst();
15
+
16
+ if (!billOfMaterial) {
17
+ return err(new BomNotFoundError(input.id));
18
+ }
19
+
20
+ const lines = await db
21
+ .selectFrom("BillOfMaterialLine")
22
+ .selectAll()
23
+ .where("billOfMaterialId", "=", billOfMaterial.id)
24
+ .execute();
25
+
26
+ return ok({ billOfMaterial, lines });
27
+ }
@@ -0,0 +1,5 @@
1
+ // @generated — do not edit
2
+ import { run } from "./getManufacturingCostSummary";
3
+ import { defineQuery } from "@tailor-platform/erp-kit/module";
4
+
5
+ export const getManufacturingCostSummary = defineQuery(run);
@@ -0,0 +1,147 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { createMockDb } from "../../../testing/index";
3
+ import type { DB } from "../generated/kysely-tailordb";
4
+ import { expectOk, expectErr } from "../testing/commandTestUtils";
5
+ import {
6
+ baseCollectingCostSummary,
7
+ baseReviewedCostSummary,
8
+ baseSettledCostSummary,
9
+ baseCostLine,
10
+ baseVarianceLine,
11
+ baseSettlementRecord,
12
+ } from "../testing/fixtures";
13
+ import { CostSummaryNotFoundError } from "../lib/errors.generated";
14
+ import { run } from "./getManufacturingCostSummary";
15
+
16
+ describe("getManufacturingCostSummary", () => {
17
+ it("returns a collecting cost summary with planned and actual totals", async () => {
18
+ const { db, spies } = createMockDb<DB>();
19
+ const costLine = { ...baseCostLine, costSummaryId: "cost-summary-1" };
20
+ // 1st select: costSummary
21
+ spies.select.mockReturnValueOnce(baseCollectingCostSummary);
22
+ // 2nd select: costLines
23
+ spies.select.mockReturnValueOnce([costLine]);
24
+ // 3rd select: varianceLines
25
+ spies.select.mockReturnValueOnce([]);
26
+ // 4th select: settlementRecords
27
+ spies.select.mockReturnValueOnce([]);
28
+
29
+ const result = await run(db, { id: "cost-summary-1" });
30
+ const { costSummary, costLines } = expectOk<{
31
+ costSummary: {
32
+ status: string;
33
+ plannedMaterialCost: number;
34
+ actualMaterialCost: number;
35
+ };
36
+ costLines: unknown[];
37
+ varianceLines: unknown[];
38
+ settlementRecords: unknown[];
39
+ }>(result as never);
40
+
41
+ expect(costSummary.status).toBe("COLLECTING");
42
+ expect(costSummary.plannedMaterialCost).toBe(2000);
43
+ expect(costSummary.actualMaterialCost).toBe(0);
44
+ expect(costLines).toHaveLength(1);
45
+ });
46
+
47
+ it("returns a reviewed cost summary with variance lines", async () => {
48
+ const { db, spies } = createMockDb<DB>();
49
+ const varianceLine = {
50
+ ...baseVarianceLine,
51
+ costSummaryId: "cost-summary-3",
52
+ };
53
+ // 1st select: costSummary
54
+ spies.select.mockReturnValueOnce(baseReviewedCostSummary);
55
+ // 2nd select: costLines
56
+ spies.select.mockReturnValueOnce([]);
57
+ // 3rd select: varianceLines
58
+ spies.select.mockReturnValueOnce([varianceLine]);
59
+ // 4th select: settlementRecords
60
+ spies.select.mockReturnValueOnce([]);
61
+
62
+ const result = await run(db, { id: "cost-summary-3" });
63
+ const { costSummary, varianceLines } = expectOk<{
64
+ costSummary: { status: string; reviewedDate: Date | null };
65
+ costLines: unknown[];
66
+ varianceLines: { varianceType: string; amount: number }[];
67
+ settlementRecords: unknown[];
68
+ }>(result as never);
69
+
70
+ expect(costSummary.status).toBe("VARIANCE_REVIEWED");
71
+ expect(costSummary.reviewedDate).toEqual(new Date("2024-04-01T00:00:00.000Z"));
72
+ expect(varianceLines).toHaveLength(1);
73
+ expect(varianceLines[0].varianceType).toBe("MATERIAL_PRICE");
74
+ expect(varianceLines[0].amount).toBe(100);
75
+ });
76
+
77
+ it("returns a settled cost summary with acknowledgment traceability", async () => {
78
+ const { db, spies } = createMockDb<DB>();
79
+ const settlement = {
80
+ ...baseSettlementRecord,
81
+ costSummaryId: "cost-summary-4",
82
+ };
83
+ // 1st select: costSummary
84
+ spies.select.mockReturnValueOnce(baseSettledCostSummary);
85
+ // 2nd select: costLines
86
+ spies.select.mockReturnValueOnce([]);
87
+ // 3rd select: varianceLines
88
+ spies.select.mockReturnValueOnce([]);
89
+ // 4th select: settlementRecords
90
+ spies.select.mockReturnValueOnce([settlement]);
91
+
92
+ const result = await run(db, { id: "cost-summary-4" });
93
+ const { costSummary, settlementRecords } = expectOk<{
94
+ costSummary: { status: string };
95
+ costLines: unknown[];
96
+ varianceLines: unknown[];
97
+ settlementRecords: {
98
+ settlementReference: string;
99
+ acknowledgedDate: Date;
100
+ }[];
101
+ }>(result as never);
102
+
103
+ expect(costSummary.status).toBe("SETTLED");
104
+ expect(settlementRecords).toHaveLength(1);
105
+ expect(settlementRecords[0].settlementReference).toBe("SETTLE-2024-001");
106
+ expect(settlementRecords[0].acknowledgedDate).toEqual(new Date("2024-04-15T00:00:00.000Z"));
107
+ });
108
+
109
+ it("returns variance lines with downstream account references", async () => {
110
+ const { db, spies } = createMockDb<DB>();
111
+ const varianceLine = {
112
+ ...baseVarianceLine,
113
+ costSummaryId: "cost-summary-3",
114
+ };
115
+ // 1st select: costSummary
116
+ spies.select.mockReturnValueOnce(baseReviewedCostSummary);
117
+ // 2nd select: costLines
118
+ spies.select.mockReturnValueOnce([]);
119
+ // 3rd select: varianceLines
120
+ spies.select.mockReturnValueOnce([varianceLine]);
121
+ // 4th select: settlementRecords
122
+ spies.select.mockReturnValueOnce([]);
123
+
124
+ const result = await run(db, { id: "cost-summary-3" });
125
+ const { varianceLines } = expectOk<{
126
+ costSummary: unknown;
127
+ costLines: unknown[];
128
+ varianceLines: {
129
+ accountReference: string;
130
+ variancePercentage: number;
131
+ }[];
132
+ settlementRecords: unknown[];
133
+ }>(result as never);
134
+
135
+ expect(varianceLines[0].accountReference).toBe("acct-5001");
136
+ expect(varianceLines[0].variancePercentage).toBe(5.0);
137
+ });
138
+
139
+ it("returns error when the cost summary does not exist", async () => {
140
+ const { db, spies } = createMockDb<DB>();
141
+ // 1st select: costSummary returns undefined
142
+ spies.select.mockReturnValueOnce(undefined);
143
+
144
+ const result = await run(db, { id: "nonexistent-id" });
145
+ expectErr(result as never, CostSummaryNotFoundError);
146
+ });
147
+ });
@@ -0,0 +1,46 @@
1
+ import type { DB } from "../generated/kysely-tailordb";
2
+ import { CostSummaryNotFoundError } from "../lib/errors.generated";
3
+ import { ok, err, type ReadonlyDB } from "@tailor-platform/erp-kit/module";
4
+
5
+ export interface GetManufacturingCostSummaryInput {
6
+ id?: string;
7
+ productionOrderId?: string;
8
+ }
9
+
10
+ export async function run(db: ReadonlyDB<DB>, input: GetManufacturingCostSummaryInput) {
11
+ let query = db.selectFrom("ManufacturingCostSummary").selectAll();
12
+
13
+ if (input.id) {
14
+ query = query.where("id", "=", input.id);
15
+ } else if (input.productionOrderId) {
16
+ query = query.where("productionOrderId", "=", input.productionOrderId);
17
+ } else {
18
+ return err(new CostSummaryNotFoundError("no id or productionOrderId provided"));
19
+ }
20
+
21
+ const costSummary = await query.executeTakeFirst();
22
+
23
+ if (!costSummary) {
24
+ return err(new CostSummaryNotFoundError(input.id ?? input.productionOrderId ?? "unknown"));
25
+ }
26
+
27
+ const costLines = await db
28
+ .selectFrom("ManufacturingCostLine")
29
+ .selectAll()
30
+ .where("costSummaryId", "=", costSummary.id)
31
+ .execute();
32
+
33
+ const varianceLines = await db
34
+ .selectFrom("CostVarianceLine")
35
+ .selectAll()
36
+ .where("costSummaryId", "=", costSummary.id)
37
+ .execute();
38
+
39
+ const settlementRecords = await db
40
+ .selectFrom("ManufacturingCostSettlementRecord")
41
+ .selectAll()
42
+ .where("costSummaryId", "=", costSummary.id)
43
+ .execute();
44
+
45
+ return ok({ costSummary, costLines, varianceLines, settlementRecords });
46
+ }
@@ -0,0 +1,5 @@
1
+ // @generated — do not edit
2
+ import { run } from "./getProductionOrder";
3
+ import { defineQuery } from "@tailor-platform/erp-kit/module";
4
+
5
+ export const getProductionOrder = defineQuery(run);