@tailor-platform/erp-kit 0.5.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (262) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/dist/cli.mjs +139 -35
  3. package/package.json +1 -1
  4. package/skills/erp-kit-app-5-impl-backend/SKILL.md +10 -5
  5. package/skills/erp-kit-app-7-impl-review/SKILL.md +1 -1
  6. package/skills/erp-kit-module-6-impl-review/SKILL.md +39 -17
  7. package/src/commands/generate-doc.ts +1 -1
  8. package/src/commands/init-module.test.ts +17 -3
  9. package/src/commands/init-module.ts +0 -12
  10. package/src/commands/lib/discovery.test.ts +13 -3
  11. package/src/commands/lib/discovery.ts +10 -2
  12. package/src/commands/lib/paths.ts +4 -2
  13. package/src/commands/lib/sync-check-tests.test.ts +84 -6
  14. package/src/commands/lib/sync-check-tests.ts +63 -3
  15. package/src/commands/sync-check.ts +7 -3
  16. package/src/generator/generate-app-code.ts +51 -16
  17. package/src/generator/generate-code-boilerplate.test.ts +9 -1
  18. package/src/generator/generate-stubs.ts +4 -0
  19. package/src/generator/scaffold.ts +6 -2
  20. package/src/generator/stub-templates.test.ts +11 -0
  21. package/src/generator/stub-templates.ts +22 -1
  22. package/src/mdschema.ts +39 -3
  23. package/src/modules/inventory/docs/features/inventory-adjustment.md +2 -1
  24. package/src/modules/inventory/docs/features/scrap-management.md +39 -1
  25. package/src/modules/manufacturing/README.md +63 -0
  26. package/src/modules/manufacturing/command/activateBillOfMaterial.generated.ts +6 -0
  27. package/src/modules/manufacturing/command/activateBillOfMaterial.test.ts +166 -0
  28. package/src/modules/manufacturing/command/activateBillOfMaterial.ts +173 -0
  29. package/src/modules/manufacturing/command/activateRouting.generated.ts +6 -0
  30. package/src/modules/manufacturing/command/activateRouting.test.ts +152 -0
  31. package/src/modules/manufacturing/command/activateRouting.ts +92 -0
  32. package/src/modules/manufacturing/command/activateWorkCenter.generated.ts +6 -0
  33. package/src/modules/manufacturing/command/activateWorkCenter.test.ts +135 -0
  34. package/src/modules/manufacturing/command/activateWorkCenter.ts +91 -0
  35. package/src/modules/manufacturing/command/cancelProductionOrder.generated.ts +6 -0
  36. package/src/modules/manufacturing/command/cancelProductionOrder.test.ts +151 -0
  37. package/src/modules/manufacturing/command/cancelProductionOrder.ts +114 -0
  38. package/src/modules/manufacturing/command/closeProductionOrder.generated.ts +6 -0
  39. package/src/modules/manufacturing/command/closeProductionOrder.test.ts +126 -0
  40. package/src/modules/manufacturing/command/closeProductionOrder.ts +87 -0
  41. package/src/modules/manufacturing/command/completeProductionOrder.generated.ts +6 -0
  42. package/src/modules/manufacturing/command/completeProductionOrder.test.ts +132 -0
  43. package/src/modules/manufacturing/command/completeProductionOrder.ts +97 -0
  44. package/src/modules/manufacturing/command/completeWorkOrder.generated.ts +6 -0
  45. package/src/modules/manufacturing/command/completeWorkOrder.test.ts +369 -0
  46. package/src/modules/manufacturing/command/completeWorkOrder.ts +212 -0
  47. package/src/modules/manufacturing/command/createBillOfMaterial.generated.ts +6 -0
  48. package/src/modules/manufacturing/command/createBillOfMaterial.test.ts +210 -0
  49. package/src/modules/manufacturing/command/createBillOfMaterial.ts +176 -0
  50. package/src/modules/manufacturing/command/createProductionOrder.generated.ts +6 -0
  51. package/src/modules/manufacturing/command/createProductionOrder.test.ts +160 -0
  52. package/src/modules/manufacturing/command/createProductionOrder.ts +129 -0
  53. package/src/modules/manufacturing/command/createRouting.generated.ts +6 -0
  54. package/src/modules/manufacturing/command/createRouting.test.ts +168 -0
  55. package/src/modules/manufacturing/command/createRouting.ts +128 -0
  56. package/src/modules/manufacturing/command/createWorkCenter.generated.ts +6 -0
  57. package/src/modules/manufacturing/command/createWorkCenter.test.ts +148 -0
  58. package/src/modules/manufacturing/command/createWorkCenter.ts +131 -0
  59. package/src/modules/manufacturing/command/deactivateBillOfMaterial.generated.ts +6 -0
  60. package/src/modules/manufacturing/command/deactivateBillOfMaterial.test.ts +103 -0
  61. package/src/modules/manufacturing/command/deactivateBillOfMaterial.ts +78 -0
  62. package/src/modules/manufacturing/command/deactivateRouting.generated.ts +6 -0
  63. package/src/modules/manufacturing/command/deactivateRouting.test.ts +112 -0
  64. package/src/modules/manufacturing/command/deactivateRouting.ts +76 -0
  65. package/src/modules/manufacturing/command/deactivateWorkCenter.generated.ts +6 -0
  66. package/src/modules/manufacturing/command/deactivateWorkCenter.test.ts +113 -0
  67. package/src/modules/manufacturing/command/deactivateWorkCenter.ts +85 -0
  68. package/src/modules/manufacturing/command/pauseWorkOrder.generated.ts +6 -0
  69. package/src/modules/manufacturing/command/pauseWorkOrder.test.ts +118 -0
  70. package/src/modules/manufacturing/command/pauseWorkOrder.ts +82 -0
  71. package/src/modules/manufacturing/command/recordInventoryIssueOutcome.generated.ts +6 -0
  72. package/src/modules/manufacturing/command/recordInventoryIssueOutcome.test.ts +183 -0
  73. package/src/modules/manufacturing/command/recordInventoryIssueOutcome.ts +139 -0
  74. package/src/modules/manufacturing/command/recordManufacturingCostSettlementAcknowledgment.generated.ts +6 -0
  75. package/src/modules/manufacturing/command/recordManufacturingCostSettlementAcknowledgment.test.ts +120 -0
  76. package/src/modules/manufacturing/command/recordManufacturingCostSettlementAcknowledgment.ts +110 -0
  77. package/src/modules/manufacturing/command/releaseProductionOrder.generated.ts +6 -0
  78. package/src/modules/manufacturing/command/releaseProductionOrder.test.ts +220 -0
  79. package/src/modules/manufacturing/command/releaseProductionOrder.ts +450 -0
  80. package/src/modules/manufacturing/command/reopenProductionOrder.generated.ts +6 -0
  81. package/src/modules/manufacturing/command/reopenProductionOrder.test.ts +196 -0
  82. package/src/modules/manufacturing/command/reopenProductionOrder.ts +98 -0
  83. package/src/modules/manufacturing/command/reportWorkOrderProgress.generated.ts +6 -0
  84. package/src/modules/manufacturing/command/reportWorkOrderProgress.test.ts +204 -0
  85. package/src/modules/manufacturing/command/reportWorkOrderProgress.ts +129 -0
  86. package/src/modules/manufacturing/command/rescheduleProductionOrder.generated.ts +6 -0
  87. package/src/modules/manufacturing/command/rescheduleProductionOrder.test.ts +185 -0
  88. package/src/modules/manufacturing/command/rescheduleProductionOrder.ts +95 -0
  89. package/src/modules/manufacturing/command/resumeWorkOrder.generated.ts +6 -0
  90. package/src/modules/manufacturing/command/resumeWorkOrder.test.ts +122 -0
  91. package/src/modules/manufacturing/command/resumeWorkOrder.ts +94 -0
  92. package/src/modules/manufacturing/command/reviewManufacturingCostSummary.generated.ts +6 -0
  93. package/src/modules/manufacturing/command/reviewManufacturingCostSummary.test.ts +231 -0
  94. package/src/modules/manufacturing/command/reviewManufacturingCostSummary.ts +137 -0
  95. package/src/modules/manufacturing/command/startWorkOrder.generated.ts +6 -0
  96. package/src/modules/manufacturing/command/startWorkOrder.test.ts +118 -0
  97. package/src/modules/manufacturing/command/startWorkOrder.ts +126 -0
  98. package/src/modules/manufacturing/command/technicallyCompleteProductionOrder.generated.ts +6 -0
  99. package/src/modules/manufacturing/command/technicallyCompleteProductionOrder.test.ts +153 -0
  100. package/src/modules/manufacturing/command/technicallyCompleteProductionOrder.ts +106 -0
  101. package/src/modules/manufacturing/command/unreleaseProductionOrder.generated.ts +6 -0
  102. package/src/modules/manufacturing/command/unreleaseProductionOrder.test.ts +140 -0
  103. package/src/modules/manufacturing/command/unreleaseProductionOrder.ts +131 -0
  104. package/src/modules/manufacturing/command/updateBillOfMaterial.generated.ts +6 -0
  105. package/src/modules/manufacturing/command/updateBillOfMaterial.test.ts +149 -0
  106. package/src/modules/manufacturing/command/updateBillOfMaterial.ts +174 -0
  107. package/src/modules/manufacturing/command/updateProductionOrder.generated.ts +6 -0
  108. package/src/modules/manufacturing/command/updateProductionOrder.test.ts +112 -0
  109. package/src/modules/manufacturing/command/updateProductionOrder.ts +145 -0
  110. package/src/modules/manufacturing/command/updateRouting.generated.ts +6 -0
  111. package/src/modules/manufacturing/command/updateRouting.test.ts +211 -0
  112. package/src/modules/manufacturing/command/updateRouting.ts +124 -0
  113. package/src/modules/manufacturing/command/updateWorkCenter.generated.ts +6 -0
  114. package/src/modules/manufacturing/command/updateWorkCenter.test.ts +152 -0
  115. package/src/modules/manufacturing/command/updateWorkCenter.ts +137 -0
  116. package/src/modules/manufacturing/db/.gitkeep +0 -0
  117. package/src/modules/manufacturing/db/billOfMaterial.ts +70 -0
  118. package/src/modules/manufacturing/db/billOfMaterialLine.ts +49 -0
  119. package/src/modules/manufacturing/db/costVarianceLine.ts +53 -0
  120. package/src/modules/manufacturing/db/manufacturingCostLine.ts +35 -0
  121. package/src/modules/manufacturing/db/manufacturingCostSettlementRecord.ts +39 -0
  122. package/src/modules/manufacturing/db/manufacturingCostSummary.ts +59 -0
  123. package/src/modules/manufacturing/db/productionOrder.ts +83 -0
  124. package/src/modules/manufacturing/db/productionOrderBomSnapshot.ts +44 -0
  125. package/src/modules/manufacturing/db/productionOrderCostBaseline.ts +44 -0
  126. package/src/modules/manufacturing/db/productionOrderMaterialRequirement.ts +57 -0
  127. package/src/modules/manufacturing/db/productionOrderRoutingSnapshot.ts +43 -0
  128. package/src/modules/manufacturing/db/routing.ts +63 -0
  129. package/src/modules/manufacturing/db/routingOperation.ts +57 -0
  130. package/src/modules/manufacturing/db/workCenter.ts +87 -0
  131. package/src/modules/manufacturing/db/workOrder.ts +65 -0
  132. package/src/modules/manufacturing/db/workOrderExecutionEvent.ts +54 -0
  133. package/src/modules/manufacturing/docs/commands/ActivateBillOfMaterial.md +50 -0
  134. package/src/modules/manufacturing/docs/commands/ActivateRouting.md +48 -0
  135. package/src/modules/manufacturing/docs/commands/ActivateWorkCenter.md +49 -0
  136. package/src/modules/manufacturing/docs/commands/CancelProductionOrder.md +48 -0
  137. package/src/modules/manufacturing/docs/commands/CloseProductionOrder.md +46 -0
  138. package/src/modules/manufacturing/docs/commands/CompleteProductionOrder.md +48 -0
  139. package/src/modules/manufacturing/docs/commands/CompleteWorkOrder.md +66 -0
  140. package/src/modules/manufacturing/docs/commands/CreateBillOfMaterial.md +54 -0
  141. package/src/modules/manufacturing/docs/commands/CreateProductionOrder.md +49 -0
  142. package/src/modules/manufacturing/docs/commands/CreateRouting.md +50 -0
  143. package/src/modules/manufacturing/docs/commands/CreateWorkCenter.md +51 -0
  144. package/src/modules/manufacturing/docs/commands/DeactivateBillOfMaterial.md +45 -0
  145. package/src/modules/manufacturing/docs/commands/DeactivateRouting.md +45 -0
  146. package/src/modules/manufacturing/docs/commands/DeactivateWorkCenter.md +45 -0
  147. package/src/modules/manufacturing/docs/commands/PauseWorkOrder.md +44 -0
  148. package/src/modules/manufacturing/docs/commands/RecordInventoryIssueOutcome.md +59 -0
  149. package/src/modules/manufacturing/docs/commands/RecordManufacturingCostSettlementAcknowledgment.md +49 -0
  150. package/src/modules/manufacturing/docs/commands/ReleaseProductionOrder.md +57 -0
  151. package/src/modules/manufacturing/docs/commands/ReopenProductionOrder.md +54 -0
  152. package/src/modules/manufacturing/docs/commands/ReportWorkOrderProgress.md +53 -0
  153. package/src/modules/manufacturing/docs/commands/RescheduleProductionOrder.md +45 -0
  154. package/src/modules/manufacturing/docs/commands/ResumeWorkOrder.md +44 -0
  155. package/src/modules/manufacturing/docs/commands/ReviewManufacturingCostSummary.md +52 -0
  156. package/src/modules/manufacturing/docs/commands/StartWorkOrder.md +46 -0
  157. package/src/modules/manufacturing/docs/commands/TechnicallyCompleteProductionOrder.md +51 -0
  158. package/src/modules/manufacturing/docs/commands/UnreleaseProductionOrder.md +46 -0
  159. package/src/modules/manufacturing/docs/commands/UpdateBillOfMaterial.md +48 -0
  160. package/src/modules/manufacturing/docs/commands/UpdateProductionOrder.md +48 -0
  161. package/src/modules/manufacturing/docs/commands/UpdateRouting.md +52 -0
  162. package/src/modules/manufacturing/docs/commands/UpdateWorkCenter.md +48 -0
  163. package/src/modules/manufacturing/docs/features/bill-of-material-management.md +83 -0
  164. package/src/modules/manufacturing/docs/features/manufacturing-cost-and-variance.md +191 -0
  165. package/src/modules/manufacturing/docs/features/production-order-lifecycle.md +103 -0
  166. package/src/modules/manufacturing/docs/features/routing-and-work-center-definition.md +63 -0
  167. package/src/modules/manufacturing/docs/features/work-order-execution.md +115 -0
  168. package/src/modules/manufacturing/docs/models/BillOfMaterial.md +60 -0
  169. package/src/modules/manufacturing/docs/models/ManufacturingCostSummary.md +66 -0
  170. package/src/modules/manufacturing/docs/models/ProductionOrder.md +76 -0
  171. package/src/modules/manufacturing/docs/models/Routing.md +58 -0
  172. package/src/modules/manufacturing/docs/models/WorkCenter.md +56 -0
  173. package/src/modules/manufacturing/docs/models/WorkOrder.md +63 -0
  174. package/src/modules/manufacturing/docs/queries/DetectBillOfMaterialCircularReference.md +39 -0
  175. package/src/modules/manufacturing/docs/queries/ExplodeBillOfMaterial.md +56 -0
  176. package/src/modules/manufacturing/docs/queries/GetBillOfMaterial.md +37 -0
  177. package/src/modules/manufacturing/docs/queries/GetManufacturingCostSummary.md +39 -0
  178. package/src/modules/manufacturing/docs/queries/GetProductionOrder.md +37 -0
  179. package/src/modules/manufacturing/docs/queries/GetRouting.md +39 -0
  180. package/src/modules/manufacturing/docs/queries/GetWorkCenter.md +35 -0
  181. package/src/modules/manufacturing/docs/queries/GetWorkOrder.md +38 -0
  182. package/src/modules/manufacturing/docs/queries/ListBillOfMaterialsByItem.md +42 -0
  183. package/src/modules/manufacturing/docs/queries/ListManufacturingCostSummariesByStatus.md +41 -0
  184. package/src/modules/manufacturing/docs/queries/ListProductionOrdersByStatus.md +41 -0
  185. package/src/modules/manufacturing/docs/queries/ListRoutingsByItem.md +42 -0
  186. package/src/modules/manufacturing/docs/queries/ListWorkCentersBySite.md +38 -0
  187. package/src/modules/manufacturing/docs/queries/ListWorkOrdersByProductionOrder.md +39 -0
  188. package/src/modules/manufacturing/docs/queries/ListWorkOrdersByWorkCenter.md +43 -0
  189. package/src/modules/manufacturing/executor/.gitkeep +0 -0
  190. package/src/modules/manufacturing/generated/enums.ts +113 -0
  191. package/src/modules/manufacturing/generated/kysely-tailordb.ts +247 -0
  192. package/src/modules/manufacturing/index.ts +2 -0
  193. package/src/modules/manufacturing/lib/_db_deps.ts +22 -0
  194. package/src/modules/manufacturing/lib/errors.generated.ts +592 -0
  195. package/src/modules/manufacturing/lib/permissions.generated.ts +35 -0
  196. package/src/modules/manufacturing/lib/types.ts +111 -0
  197. package/src/modules/manufacturing/module.ts +226 -0
  198. package/src/modules/manufacturing/permissions.ts +3 -0
  199. package/src/modules/manufacturing/query/.gitkeep +0 -0
  200. package/src/modules/manufacturing/query/detectBillOfMaterialCircularReference.generated.ts +5 -0
  201. package/src/modules/manufacturing/query/detectBillOfMaterialCircularReference.test.ts +115 -0
  202. package/src/modules/manufacturing/query/detectBillOfMaterialCircularReference.ts +79 -0
  203. package/src/modules/manufacturing/query/explodeBillOfMaterial.generated.ts +5 -0
  204. package/src/modules/manufacturing/query/explodeBillOfMaterial.test.ts +445 -0
  205. package/src/modules/manufacturing/query/explodeBillOfMaterial.ts +306 -0
  206. package/src/modules/manufacturing/query/getBillOfMaterial.generated.ts +5 -0
  207. package/src/modules/manufacturing/query/getBillOfMaterial.test.ts +64 -0
  208. package/src/modules/manufacturing/query/getBillOfMaterial.ts +27 -0
  209. package/src/modules/manufacturing/query/getManufacturingCostSummary.generated.ts +5 -0
  210. package/src/modules/manufacturing/query/getManufacturingCostSummary.test.ts +147 -0
  211. package/src/modules/manufacturing/query/getManufacturingCostSummary.ts +46 -0
  212. package/src/modules/manufacturing/query/getProductionOrder.generated.ts +5 -0
  213. package/src/modules/manufacturing/query/getProductionOrder.test.ts +139 -0
  214. package/src/modules/manufacturing/query/getProductionOrder.ts +84 -0
  215. package/src/modules/manufacturing/query/getRouting.generated.ts +5 -0
  216. package/src/modules/manufacturing/query/getRouting.test.ts +71 -0
  217. package/src/modules/manufacturing/query/getRouting.ts +34 -0
  218. package/src/modules/manufacturing/query/getWorkCenter.generated.ts +5 -0
  219. package/src/modules/manufacturing/query/getWorkCenter.test.ts +37 -0
  220. package/src/modules/manufacturing/query/getWorkCenter.ts +21 -0
  221. package/src/modules/manufacturing/query/getWorkOrder.generated.ts +5 -0
  222. package/src/modules/manufacturing/query/getWorkOrder.test.ts +73 -0
  223. package/src/modules/manufacturing/query/getWorkOrder.ts +28 -0
  224. package/src/modules/manufacturing/query/listBillOfMaterialsByItem.generated.ts +5 -0
  225. package/src/modules/manufacturing/query/listBillOfMaterialsByItem.test.ts +107 -0
  226. package/src/modules/manufacturing/query/listBillOfMaterialsByItem.ts +58 -0
  227. package/src/modules/manufacturing/query/listManufacturingCostSummariesByStatus.generated.ts +5 -0
  228. package/src/modules/manufacturing/query/listManufacturingCostSummariesByStatus.test.ts +96 -0
  229. package/src/modules/manufacturing/query/listManufacturingCostSummariesByStatus.ts +77 -0
  230. package/src/modules/manufacturing/query/listProductionOrdersByStatus.generated.ts +5 -0
  231. package/src/modules/manufacturing/query/listProductionOrdersByStatus.test.ts +121 -0
  232. package/src/modules/manufacturing/query/listProductionOrdersByStatus.ts +83 -0
  233. package/src/modules/manufacturing/query/listRoutingsByItem.generated.ts +5 -0
  234. package/src/modules/manufacturing/query/listRoutingsByItem.test.ts +110 -0
  235. package/src/modules/manufacturing/query/listRoutingsByItem.ts +54 -0
  236. package/src/modules/manufacturing/query/listWorkCentersBySite.generated.ts +5 -0
  237. package/src/modules/manufacturing/query/listWorkCentersBySite.test.ts +81 -0
  238. package/src/modules/manufacturing/query/listWorkCentersBySite.ts +70 -0
  239. package/src/modules/manufacturing/query/listWorkOrdersByProductionOrder.generated.ts +5 -0
  240. package/src/modules/manufacturing/query/listWorkOrdersByProductionOrder.test.ts +102 -0
  241. package/src/modules/manufacturing/query/listWorkOrdersByProductionOrder.ts +53 -0
  242. package/src/modules/manufacturing/query/listWorkOrdersByWorkCenter.generated.ts +5 -0
  243. package/src/modules/manufacturing/query/listWorkOrdersByWorkCenter.test.ts +143 -0
  244. package/src/modules/manufacturing/query/listWorkOrdersByWorkCenter.ts +56 -0
  245. package/src/modules/manufacturing/seed/index.ts +19 -0
  246. package/src/modules/manufacturing/tailor.config.ts +13 -0
  247. package/src/modules/manufacturing/tailor.d.ts +13 -0
  248. package/src/modules/manufacturing/testing/commandTestUtils.ts +29 -0
  249. package/src/modules/manufacturing/testing/fixtures.ts +402 -0
  250. package/templates/scaffold/app/backend/package.json +9 -2
  251. package/templates/scaffold/app/backend/src/tests/utils/graphql-client.ts +66 -0
  252. package/templates/scaffold/app/backend/src/tests/utils/setup.ts +21 -0
  253. package/templates/scaffold/app/backend/tsconfig.json +9 -2
  254. package/templates/scaffold/app/backend/vitest.config.ts +35 -0
  255. package/templates/scaffold/app/frontend/package.json +2 -2
  256. package/templates/scaffold/module/__dot__gitignore +3 -0
  257. package/templates/scaffold/module/eslint.config.js +31 -0
  258. package/templates/scaffold/module/generated/kysely-tailordb.ts +3 -0
  259. package/templates/scaffold/module/lib/types.ts +1 -6
  260. package/templates/scaffold/module/package.json +26 -0
  261. package/templates/scaffold/module/tsconfig.json +16 -0
  262. /package/{templates/scaffold/module/generated → src/modules/manufacturing/command}/.gitkeep +0 -0
@@ -0,0 +1,87 @@
1
+ import type { Transaction } from "../generated/kysely-tailordb";
2
+ import {
3
+ ProductionOrderNotFoundError,
4
+ ProductionOrderNotClosableError,
5
+ OpenWorkRemainsError,
6
+ CostSummaryNotSettledError,
7
+ } from "../lib/errors.generated";
8
+ import { ok, err, type CommandContext } from "@tailor-platform/erp-kit/module";
9
+
10
+ export interface CloseProductionOrderInput {
11
+ id: string;
12
+ from?: string[];
13
+ }
14
+
15
+ /**
16
+ * Function: closeProductionOrder
17
+ *
18
+ * Performs the final administrative close after technical completion and
19
+ * downstream cost settlement are done. The command is the last lifecycle
20
+ * step for the production order.
21
+ */
22
+ export async function run<CF extends Record<string, unknown>>(
23
+ db: Transaction,
24
+ input: CloseProductionOrderInput & CF,
25
+ _ctx: CommandContext,
26
+ ) {
27
+ const { id, from, ...customFields } = input;
28
+ void customFields;
29
+
30
+ const allowedStatuses = from ?? ["TECHNICALLY_COMPLETE"];
31
+
32
+ // 1. Fetch production order with lock
33
+ const order = await db
34
+ .selectFrom("ProductionOrder")
35
+ .selectAll()
36
+ .where("id", "=", id)
37
+ .forUpdate()
38
+ .executeTakeFirst();
39
+
40
+ if (!order) {
41
+ return err(new ProductionOrderNotFoundError(id));
42
+ }
43
+
44
+ // 2. Validate status is closable
45
+ if (!allowedStatuses.includes(order.status)) {
46
+ return err(new ProductionOrderNotClosableError(id));
47
+ }
48
+
49
+ // 3. Check all work orders are resolved (COMPLETE or CANCELLED)
50
+ const workOrders = await db
51
+ .selectFrom("WorkOrder")
52
+ .selectAll()
53
+ .where("productionOrderId", "=", id)
54
+ .execute();
55
+
56
+ const hasOpenWork = workOrders.some(
57
+ (wo) => wo.status !== "COMPLETE" && wo.status !== "CANCELLED",
58
+ );
59
+
60
+ if (hasOpenWork) {
61
+ return err(new OpenWorkRemainsError(id));
62
+ }
63
+
64
+ // 4. Check cost summary is SETTLED
65
+ const costSummary = await db
66
+ .selectFrom("ManufacturingCostSummary")
67
+ .selectAll()
68
+ .where("productionOrderId", "=", id)
69
+ .executeTakeFirst();
70
+
71
+ if (costSummary?.status !== "SETTLED") {
72
+ return err(new CostSummaryNotSettledError(id));
73
+ }
74
+
75
+ // 5. Set order status to CLOSED
76
+ const closedOrder = await db
77
+ .updateTable("ProductionOrder")
78
+ .set({
79
+ status: "CLOSED",
80
+ updatedAt: new Date(),
81
+ })
82
+ .where("id", "=", id)
83
+ .returningAll()
84
+ .executeTakeFirstOrThrow();
85
+
86
+ return ok({ productionOrder: closedOrder });
87
+ }
@@ -0,0 +1,6 @@
1
+ // @generated — do not edit
2
+ import { permissions } from "../lib/permissions.generated";
3
+ import { run } from "./completeProductionOrder";
4
+ import { defineCommand } from "@tailor-platform/erp-kit/module";
5
+
6
+ export const completeProductionOrder = defineCommand(permissions.completeProductionOrder, run);
@@ -0,0 +1,132 @@
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
+ ProductionOrderNotCompletableError,
7
+ OpenWorkOrderRemainsError,
8
+ FinalOutputRequiredError,
9
+ FinalReceiptRequiredError,
10
+ } from "../lib/errors.generated";
11
+ import {
12
+ baseInProgressProductionOrder,
13
+ baseDraftProductionOrder,
14
+ baseCompleteWorkOrder,
15
+ baseCancelledWorkOrder,
16
+ baseInProgressWorkOrder,
17
+ baseCollectingCostSummary,
18
+ } from "../testing/fixtures";
19
+ import { run } from "./completeProductionOrder";
20
+ import type { CommandContext } from "@tailor-platform/erp-kit/module";
21
+
22
+ describe("completeProductionOrder", () => {
23
+ const ctx: CommandContext = {
24
+ actorId: "test-actor",
25
+ permissions: ["manufacturing:completeProductionOrder"],
26
+ };
27
+
28
+ it("completes an in-progress order after all required work orders finish", async () => {
29
+ const { db, spies } = createMockDb<Transaction>();
30
+ const completed = { ...baseInProgressProductionOrder, status: "COMPLETED" as const };
31
+
32
+ // order lookup
33
+ spies.select.mockReturnValueOnce(baseInProgressProductionOrder);
34
+ // work orders - all complete or cancelled
35
+ spies.select.mockReturnValueOnce([baseCompleteWorkOrder, baseCancelledWorkOrder]);
36
+ // cost summary with actual costs
37
+ spies.select.mockReturnValueOnce({
38
+ ...baseCollectingCostSummary,
39
+ productionOrderId: baseInProgressProductionOrder.id,
40
+ actualMaterialCost: 500,
41
+ });
42
+ // update order status
43
+ spies.update.mockReturnValue(completed);
44
+
45
+ const result = await run(db, { id: baseInProgressProductionOrder.id }, ctx);
46
+
47
+ expect(result.ok).toBe(true);
48
+ if (result.ok) {
49
+ expect(result.value.productionOrder.status).toBe("COMPLETED");
50
+ }
51
+ expect(spies.update).toHaveBeenCalled();
52
+ });
53
+
54
+ it("returns error when the order does not exist", async () => {
55
+ const { db, spies } = createMockDb<Transaction>();
56
+ spies.select.mockReturnValueOnce(undefined);
57
+
58
+ const result = await run(db, { id: "nonexistent" }, ctx);
59
+
60
+ expect(result.ok).toBe(false);
61
+ if (!result.ok) {
62
+ expect(result.error).toBeInstanceOf(ProductionOrderNotFoundError);
63
+ }
64
+ });
65
+
66
+ it("returns error when the order is not in IN_PROGRESS", async () => {
67
+ const { db, spies } = createMockDb<Transaction>();
68
+ spies.select.mockReturnValueOnce(baseDraftProductionOrder);
69
+
70
+ const result = await run(db, { id: baseDraftProductionOrder.id }, ctx);
71
+
72
+ expect(result.ok).toBe(false);
73
+ if (!result.ok) {
74
+ expect(result.error).toBeInstanceOf(ProductionOrderNotCompletableError);
75
+ }
76
+ });
77
+
78
+ it("returns error when required work orders remain open", async () => {
79
+ const { db, spies } = createMockDb<Transaction>();
80
+
81
+ // order lookup
82
+ spies.select.mockReturnValueOnce(baseInProgressProductionOrder);
83
+ // work orders - one still in progress
84
+ spies.select.mockReturnValueOnce([baseInProgressWorkOrder, baseCompleteWorkOrder]);
85
+
86
+ const result = await run(db, { id: baseInProgressProductionOrder.id }, ctx);
87
+
88
+ expect(result.ok).toBe(false);
89
+ if (!result.ok) {
90
+ expect(result.error).toBeInstanceOf(OpenWorkOrderRemainsError);
91
+ }
92
+ });
93
+
94
+ it("returns error when final output reporting is incomplete", async () => {
95
+ const { db, spies } = createMockDb<Transaction>();
96
+ const completedWithZero = { ...baseCompleteWorkOrder, completedQuantity: 0 };
97
+
98
+ // order lookup
99
+ spies.select.mockReturnValueOnce(baseInProgressProductionOrder);
100
+ // work orders - all cancelled (no completed output)
101
+ spies.select.mockReturnValueOnce([baseCancelledWorkOrder, completedWithZero]);
102
+
103
+ const result = await run(db, { id: baseInProgressProductionOrder.id }, ctx);
104
+
105
+ expect(result.ok).toBe(false);
106
+ if (!result.ok) {
107
+ expect(result.error).toBeInstanceOf(FinalOutputRequiredError);
108
+ }
109
+ });
110
+
111
+ it("returns error when required receipt handoff evidence is missing", async () => {
112
+ const { db, spies } = createMockDb<Transaction>();
113
+
114
+ // order lookup
115
+ spies.select.mockReturnValueOnce(baseInProgressProductionOrder);
116
+ // work orders - all complete with output
117
+ spies.select.mockReturnValueOnce([baseCompleteWorkOrder]);
118
+ // cost summary with no actual costs
119
+ spies.select.mockReturnValueOnce({
120
+ ...baseCollectingCostSummary,
121
+ productionOrderId: baseInProgressProductionOrder.id,
122
+ actualMaterialCost: 0,
123
+ });
124
+
125
+ const result = await run(db, { id: baseInProgressProductionOrder.id }, ctx);
126
+
127
+ expect(result.ok).toBe(false);
128
+ if (!result.ok) {
129
+ expect(result.error).toBeInstanceOf(FinalReceiptRequiredError);
130
+ }
131
+ });
132
+ });
@@ -0,0 +1,97 @@
1
+ import type { Transaction } from "../generated/kysely-tailordb";
2
+ import {
3
+ ProductionOrderNotFoundError,
4
+ ProductionOrderNotCompletableError,
5
+ OpenWorkOrderRemainsError,
6
+ FinalOutputRequiredError,
7
+ FinalReceiptRequiredError,
8
+ } from "../lib/errors.generated";
9
+ import { ok, err, type CommandContext } from "@tailor-platform/erp-kit/module";
10
+
11
+ export interface CompleteProductionOrderInput {
12
+ id: string;
13
+ from?: string[];
14
+ }
15
+
16
+ /**
17
+ * Function: completeProductionOrder
18
+ *
19
+ * Marks physical production complete once required work orders are finished
20
+ * and final receipt obligations are satisfied. The command freezes production
21
+ * execution while still allowing later technical completion and review.
22
+ */
23
+ export async function run<CF extends Record<string, unknown>>(
24
+ db: Transaction,
25
+ input: CompleteProductionOrderInput & CF,
26
+ _ctx: CommandContext,
27
+ ) {
28
+ const { id, from, ...customFields } = input;
29
+ void customFields;
30
+
31
+ const allowedStatuses = from ?? ["IN_PROGRESS"];
32
+
33
+ // 1. Fetch production order with lock
34
+ const order = await db
35
+ .selectFrom("ProductionOrder")
36
+ .selectAll()
37
+ .where("id", "=", id)
38
+ .forUpdate()
39
+ .executeTakeFirst();
40
+
41
+ if (!order) {
42
+ return err(new ProductionOrderNotFoundError(id));
43
+ }
44
+
45
+ // 2. Validate status is completable
46
+ if (!allowedStatuses.includes(order.status)) {
47
+ return err(new ProductionOrderNotCompletableError(id));
48
+ }
49
+
50
+ // 3. Check all required work orders are COMPLETE or CANCELLED
51
+ const workOrders = await db
52
+ .selectFrom("WorkOrder")
53
+ .selectAll()
54
+ .where("productionOrderId", "=", id)
55
+ .execute();
56
+
57
+ const hasOpenWorkOrders = workOrders.some(
58
+ (wo) => wo.status !== "COMPLETE" && wo.status !== "CANCELLED",
59
+ );
60
+
61
+ if (hasOpenWorkOrders) {
62
+ return err(new OpenWorkOrderRemainsError(id));
63
+ }
64
+
65
+ // 4. Check final output has been reported
66
+ const hasCompletedOutput = workOrders.some(
67
+ (wo) => wo.status === "COMPLETE" && wo.completedQuantity > 0,
68
+ );
69
+
70
+ if (!hasCompletedOutput) {
71
+ return err(new FinalOutputRequiredError(id));
72
+ }
73
+
74
+ // 5. Check receipt handoff evidence exists
75
+ const costSummary = await db
76
+ .selectFrom("ManufacturingCostSummary")
77
+ .selectAll()
78
+ .where("productionOrderId", "=", id)
79
+ .executeTakeFirst();
80
+
81
+ if (!costSummary || costSummary.actualMaterialCost <= 0) {
82
+ return err(new FinalReceiptRequiredError(id));
83
+ }
84
+
85
+ // 6. Set status to COMPLETED
86
+ const completedOrder = await db
87
+ .updateTable("ProductionOrder")
88
+ .set({
89
+ status: "COMPLETED",
90
+ updatedAt: new Date(),
91
+ })
92
+ .where("id", "=", id)
93
+ .returningAll()
94
+ .executeTakeFirstOrThrow();
95
+
96
+ return ok({ productionOrder: completedOrder });
97
+ }
@@ -0,0 +1,6 @@
1
+ // @generated — do not edit
2
+ import { permissions } from "../lib/permissions.generated";
3
+ import { run } from "./completeWorkOrder";
4
+ import { defineCommand } from "@tailor-platform/erp-kit/module";
5
+
6
+ export const completeWorkOrder = defineCommand(permissions.completeWorkOrder, run);
@@ -0,0 +1,369 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { createMockDb } from "../../../testing/index";
3
+ import type { Transaction } from "../generated/kysely-tailordb";
4
+ import {
5
+ WorkOrderNotFoundError,
6
+ WorkOrderNotCompletableError,
7
+ WorkOrderNotStartedError,
8
+ InvalidCompletionQuantityError,
9
+ DuplicateBackflushRiskError,
10
+ ReceiptHandoffRequiredError,
11
+ LotReferenceRequiredError,
12
+ SerialReferenceRequiredError,
13
+ } from "../lib/errors.generated";
14
+ import {
15
+ baseInProgressWorkOrder,
16
+ basePendingWorkOrder,
17
+ baseCompleteWorkOrder,
18
+ } from "../testing/fixtures";
19
+ import { run } from "./completeWorkOrder";
20
+ import type { CommandContext } from "@tailor-platform/erp-kit/module";
21
+
22
+ describe("completeWorkOrder", () => {
23
+ const ctx: CommandContext = {
24
+ actorId: "test-actor",
25
+ permissions: ["manufacturing:completeWorkOrder"],
26
+ };
27
+
28
+ const baseInput = {
29
+ id: baseInProgressWorkOrder.id,
30
+ completedQuantity: 50,
31
+ backflushRequired: false,
32
+ receiptRequired: false,
33
+ notes: "Final completion",
34
+ };
35
+
36
+ it("completes an in-progress work order with final quantity reporting", async () => {
37
+ const { db, spies } = createMockDb<Transaction>();
38
+ const completedWorkOrder = {
39
+ ...baseInProgressWorkOrder,
40
+ status: "COMPLETE" as const,
41
+ completedQuantity: 100,
42
+ };
43
+
44
+ // work order lookup
45
+ spies.select.mockReturnValueOnce(baseInProgressWorkOrder);
46
+ // update work order
47
+ spies.update.mockReturnValueOnce(completedWorkOrder);
48
+ // insert execution event
49
+ spies.insert.mockReturnValueOnce(undefined);
50
+ // sibling work orders (all complete after this one)
51
+ spies.select.mockReturnValueOnce([
52
+ { ...baseInProgressWorkOrder, id: baseInProgressWorkOrder.id, status: "IN_PROGRESS" },
53
+ ]);
54
+ // update parent production order
55
+ spies.update.mockReturnValueOnce(undefined);
56
+
57
+ const result = await run(db, baseInput, ctx);
58
+
59
+ expect(result.ok).toBe(true);
60
+ if (result.ok) {
61
+ expect(result.value.workOrder.status).toBe("COMPLETE");
62
+ expect(result.value.workOrder.completedQuantity).toBe(100);
63
+ }
64
+ expect(spies.update).toHaveBeenCalled();
65
+ expect(spies.insert).toHaveBeenCalled();
66
+ });
67
+
68
+ it("returns error when the work order does not exist", async () => {
69
+ const { db, spies } = createMockDb<Transaction>();
70
+ spies.select.mockReturnValueOnce(undefined);
71
+
72
+ const result = await run(db, { ...baseInput, id: "nonexistent" }, ctx);
73
+
74
+ expect(result.ok).toBe(false);
75
+ if (!result.ok) {
76
+ expect(result.error).toBeInstanceOf(WorkOrderNotFoundError);
77
+ }
78
+ });
79
+
80
+ it("returns error when the work order is not in progress", async () => {
81
+ const { db, spies } = createMockDb<Transaction>();
82
+ spies.select.mockReturnValueOnce(basePendingWorkOrder);
83
+
84
+ const result = await run(db, { ...baseInput, id: basePendingWorkOrder.id }, ctx);
85
+
86
+ expect(result.ok).toBe(false);
87
+ if (!result.ok) {
88
+ expect(result.error).toBeInstanceOf(WorkOrderNotCompletableError);
89
+ }
90
+ });
91
+
92
+ it("returns error when the work order was never started", async () => {
93
+ const { db, spies } = createMockDb<Transaction>();
94
+ const unstartedWorkOrder = {
95
+ ...baseInProgressWorkOrder,
96
+ actualStartDate: null,
97
+ };
98
+ spies.select.mockReturnValueOnce(unstartedWorkOrder);
99
+
100
+ const result = await run(db, baseInput, ctx);
101
+
102
+ expect(result.ok).toBe(false);
103
+ if (!result.ok) {
104
+ expect(result.error).toBeInstanceOf(WorkOrderNotStartedError);
105
+ }
106
+ });
107
+
108
+ it("returns error when completion quantity is invalid", async () => {
109
+ const { db, spies } = createMockDb<Transaction>();
110
+ spies.select.mockReturnValueOnce(baseInProgressWorkOrder);
111
+
112
+ const result = await run(db, { ...baseInput, completedQuantity: 0 }, ctx);
113
+
114
+ expect(result.ok).toBe(false);
115
+ if (!result.ok) {
116
+ expect(result.error).toBeInstanceOf(InvalidCompletionQuantityError);
117
+ }
118
+ });
119
+
120
+ it("returns error when backflush would duplicate manual issue", async () => {
121
+ const { db, spies } = createMockDb<Transaction>();
122
+ spies.select.mockReturnValueOnce(baseInProgressWorkOrder);
123
+
124
+ const result = await run(
125
+ db,
126
+ {
127
+ ...baseInput,
128
+ backflushRequired: true,
129
+ manuallyIssuedQuantity: 10,
130
+ },
131
+ ctx,
132
+ );
133
+
134
+ expect(result.ok).toBe(false);
135
+ if (!result.ok) {
136
+ expect(result.error).toBeInstanceOf(DuplicateBackflushRiskError);
137
+ }
138
+ });
139
+
140
+ it("emits receipt handoff when output receipt is required", async () => {
141
+ const { db, spies } = createMockDb<Transaction>();
142
+ const completedWorkOrder = {
143
+ ...baseInProgressWorkOrder,
144
+ status: "COMPLETE" as const,
145
+ completedQuantity: 100,
146
+ };
147
+
148
+ // work order lookup
149
+ spies.select.mockReturnValueOnce(baseInProgressWorkOrder);
150
+ // update work order
151
+ spies.update.mockReturnValueOnce(completedWorkOrder);
152
+ // insert execution event
153
+ spies.insert.mockReturnValueOnce(undefined);
154
+ // sibling work orders
155
+ spies.select.mockReturnValueOnce([
156
+ { ...baseInProgressWorkOrder, id: baseInProgressWorkOrder.id, status: "IN_PROGRESS" },
157
+ ]);
158
+ // update parent production order
159
+ spies.update.mockReturnValueOnce(undefined);
160
+
161
+ const result = await run(
162
+ db,
163
+ {
164
+ ...baseInput,
165
+ receiptRequired: true,
166
+ receiptData: {
167
+ itemReference: "item-fg-1",
168
+ unitOfMeasure: "EA",
169
+ siteReference: "site-1",
170
+ postingDate: new Date("2024-03-01T12:00:00.000Z"),
171
+ lotTracked: false,
172
+ serialTracked: false,
173
+ },
174
+ },
175
+ ctx,
176
+ );
177
+
178
+ expect(result.ok).toBe(true);
179
+ if (result.ok) {
180
+ expect(result.value.workOrder.status).toBe("COMPLETE");
181
+ expect(result.value.receiptHandoff?.itemReference).toBe("item-fg-1");
182
+ }
183
+ });
184
+
185
+ it("returns error when receipt is required but receipt data is missing", async () => {
186
+ const { db, spies } = createMockDb<Transaction>();
187
+ spies.select.mockReturnValueOnce(baseInProgressWorkOrder);
188
+
189
+ const result = await run(
190
+ db,
191
+ {
192
+ ...baseInput,
193
+ receiptRequired: true,
194
+ receiptData: null,
195
+ },
196
+ ctx,
197
+ );
198
+
199
+ expect(result.ok).toBe(false);
200
+ if (!result.ok) {
201
+ expect(result.error).toBeInstanceOf(ReceiptHandoffRequiredError);
202
+ }
203
+ });
204
+
205
+ it("returns error when a lot-tracked receipt omits finishedGoodLotReference", async () => {
206
+ const { db, spies } = createMockDb<Transaction>();
207
+ spies.select.mockReturnValueOnce(baseInProgressWorkOrder);
208
+
209
+ const result = await run(
210
+ db,
211
+ {
212
+ ...baseInput,
213
+ receiptRequired: true,
214
+ receiptData: {
215
+ itemReference: "item-fg-1",
216
+ unitOfMeasure: "EA",
217
+ siteReference: "site-1",
218
+ postingDate: new Date("2024-03-01T12:00:00.000Z"),
219
+ lotTracked: true,
220
+ serialTracked: false,
221
+ finishedGoodLotReference: null,
222
+ },
223
+ },
224
+ ctx,
225
+ );
226
+
227
+ expect(result.ok).toBe(false);
228
+ if (!result.ok) {
229
+ expect(result.error).toBeInstanceOf(LotReferenceRequiredError);
230
+ }
231
+ });
232
+
233
+ it("returns error when a serial-tracked receipt omits serialReferences", async () => {
234
+ const { db, spies } = createMockDb<Transaction>();
235
+ spies.select.mockReturnValueOnce(baseInProgressWorkOrder);
236
+
237
+ const result = await run(
238
+ db,
239
+ {
240
+ ...baseInput,
241
+ receiptRequired: true,
242
+ receiptData: {
243
+ itemReference: "item-fg-1",
244
+ unitOfMeasure: "EA",
245
+ siteReference: "site-1",
246
+ postingDate: new Date("2024-03-01T12:00:00.000Z"),
247
+ lotTracked: false,
248
+ serialTracked: true,
249
+ serialReferences: null,
250
+ },
251
+ },
252
+ ctx,
253
+ );
254
+
255
+ expect(result.ok).toBe(false);
256
+ if (!result.ok) {
257
+ expect(result.error).toBeInstanceOf(SerialReferenceRequiredError);
258
+ }
259
+ });
260
+
261
+ it("rolls up completion to the parent order", async () => {
262
+ const { db, spies } = createMockDb<Transaction>();
263
+ const completedWorkOrder = {
264
+ ...baseInProgressWorkOrder,
265
+ status: "COMPLETE" as const,
266
+ completedQuantity: 100,
267
+ };
268
+
269
+ // work order lookup
270
+ spies.select.mockReturnValueOnce(baseInProgressWorkOrder);
271
+ // update work order
272
+ spies.update.mockReturnValueOnce(completedWorkOrder);
273
+ // insert execution event
274
+ spies.insert.mockReturnValueOnce(undefined);
275
+ // sibling work orders - all siblings already complete/cancelled
276
+ spies.select.mockReturnValueOnce([
277
+ { ...baseInProgressWorkOrder, id: baseInProgressWorkOrder.id, status: "IN_PROGRESS" },
278
+ { ...baseCompleteWorkOrder, id: "work-order-sibling", status: "COMPLETE" },
279
+ ]);
280
+ // update parent production order to COMPLETED
281
+ spies.update.mockReturnValueOnce(undefined);
282
+
283
+ const result = await run(db, baseInput, ctx);
284
+
285
+ expect(result.ok).toBe(true);
286
+ // The update spy should have been called twice: once for the work order, once for the parent order
287
+ expect(spies.update).toHaveBeenCalledTimes(2);
288
+ });
289
+
290
+ it("allows zero-quantity completion only under an explicit bypass policy", async () => {
291
+ const { db, spies } = createMockDb<Transaction>();
292
+ const completedWorkOrder = {
293
+ ...baseInProgressWorkOrder,
294
+ status: "COMPLETE" as const,
295
+ completedQuantity: baseInProgressWorkOrder.completedQuantity,
296
+ };
297
+
298
+ spies.select.mockReturnValueOnce(baseInProgressWorkOrder);
299
+ spies.update.mockReturnValueOnce(completedWorkOrder);
300
+ spies.insert.mockReturnValueOnce(undefined);
301
+ spies.select.mockReturnValueOnce([
302
+ { ...baseInProgressWorkOrder, id: baseInProgressWorkOrder.id, status: "IN_PROGRESS" },
303
+ ]);
304
+ spies.update.mockReturnValueOnce(undefined);
305
+
306
+ const result = await run(
307
+ db,
308
+ {
309
+ ...baseInput,
310
+ completedQuantity: 0,
311
+ zeroQuantityBypassPolicy: {
312
+ allowZeroCompletion: true,
313
+ reasonCode: "QUALITY_HOLD",
314
+ },
315
+ receiptRequired: true,
316
+ receiptData: {
317
+ itemReference: "item-fg-1",
318
+ unitOfMeasure: "EA",
319
+ siteReference: "site-1",
320
+ postingDate: new Date("2024-03-01T12:00:00.000Z"),
321
+ lotTracked: false,
322
+ serialTracked: false,
323
+ },
324
+ },
325
+ ctx,
326
+ );
327
+
328
+ expect(result.ok).toBe(true);
329
+ if (result.ok) {
330
+ expect(result.value.backflushHandoff).toBeNull();
331
+ expect(result.value.receiptHandoff?.quantity).toBe(0);
332
+ }
333
+ });
334
+
335
+ it("emits a backflush handoff when completion requires backflush consumption", async () => {
336
+ const { db, spies } = createMockDb<Transaction>();
337
+ const completedWorkOrder = {
338
+ ...baseInProgressWorkOrder,
339
+ status: "COMPLETE" as const,
340
+ completedQuantity: 100,
341
+ };
342
+
343
+ spies.select.mockReturnValueOnce(baseInProgressWorkOrder);
344
+ spies.update.mockReturnValueOnce(completedWorkOrder);
345
+ spies.insert.mockReturnValueOnce(undefined);
346
+ spies.select.mockReturnValueOnce([
347
+ { ...baseInProgressWorkOrder, id: baseInProgressWorkOrder.id, status: "IN_PROGRESS" },
348
+ ]);
349
+ spies.update.mockReturnValueOnce(undefined);
350
+
351
+ const result = await run(
352
+ db,
353
+ {
354
+ ...baseInput,
355
+ backflushRequired: true,
356
+ manuallyIssuedQuantity: 0,
357
+ },
358
+ ctx,
359
+ );
360
+
361
+ expect(result.ok).toBe(true);
362
+ if (result.ok) {
363
+ expect(result.value.backflushHandoff?.productionOrderReference).toBe(
364
+ baseInProgressWorkOrder.productionOrderId,
365
+ );
366
+ expect(result.value.backflushHandoff?.completedQuantity).toBe(50);
367
+ }
368
+ });
369
+ });