@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.
- package/CHANGELOG.md +24 -0
- package/dist/cli.mjs +139 -35
- package/package.json +1 -1
- package/skills/erp-kit-app-5-impl-backend/SKILL.md +10 -5
- package/skills/erp-kit-app-7-impl-review/SKILL.md +1 -1
- package/skills/erp-kit-module-6-impl-review/SKILL.md +39 -17
- package/src/commands/generate-doc.ts +1 -1
- package/src/commands/init-module.test.ts +17 -3
- package/src/commands/init-module.ts +0 -12
- package/src/commands/lib/discovery.test.ts +13 -3
- package/src/commands/lib/discovery.ts +10 -2
- package/src/commands/lib/paths.ts +4 -2
- package/src/commands/lib/sync-check-tests.test.ts +84 -6
- package/src/commands/lib/sync-check-tests.ts +63 -3
- package/src/commands/sync-check.ts +7 -3
- package/src/generator/generate-app-code.ts +51 -16
- package/src/generator/generate-code-boilerplate.test.ts +9 -1
- package/src/generator/generate-stubs.ts +4 -0
- package/src/generator/scaffold.ts +6 -2
- package/src/generator/stub-templates.test.ts +11 -0
- package/src/generator/stub-templates.ts +22 -1
- package/src/mdschema.ts +39 -3
- package/src/modules/inventory/docs/features/inventory-adjustment.md +2 -1
- package/src/modules/inventory/docs/features/scrap-management.md +39 -1
- package/src/modules/manufacturing/README.md +63 -0
- package/src/modules/manufacturing/command/activateBillOfMaterial.generated.ts +6 -0
- package/src/modules/manufacturing/command/activateBillOfMaterial.test.ts +166 -0
- package/src/modules/manufacturing/command/activateBillOfMaterial.ts +173 -0
- package/src/modules/manufacturing/command/activateRouting.generated.ts +6 -0
- package/src/modules/manufacturing/command/activateRouting.test.ts +152 -0
- package/src/modules/manufacturing/command/activateRouting.ts +92 -0
- package/src/modules/manufacturing/command/activateWorkCenter.generated.ts +6 -0
- package/src/modules/manufacturing/command/activateWorkCenter.test.ts +135 -0
- package/src/modules/manufacturing/command/activateWorkCenter.ts +91 -0
- package/src/modules/manufacturing/command/cancelProductionOrder.generated.ts +6 -0
- package/src/modules/manufacturing/command/cancelProductionOrder.test.ts +151 -0
- package/src/modules/manufacturing/command/cancelProductionOrder.ts +114 -0
- package/src/modules/manufacturing/command/closeProductionOrder.generated.ts +6 -0
- package/src/modules/manufacturing/command/closeProductionOrder.test.ts +126 -0
- package/src/modules/manufacturing/command/closeProductionOrder.ts +87 -0
- package/src/modules/manufacturing/command/completeProductionOrder.generated.ts +6 -0
- package/src/modules/manufacturing/command/completeProductionOrder.test.ts +132 -0
- package/src/modules/manufacturing/command/completeProductionOrder.ts +97 -0
- package/src/modules/manufacturing/command/completeWorkOrder.generated.ts +6 -0
- package/src/modules/manufacturing/command/completeWorkOrder.test.ts +369 -0
- package/src/modules/manufacturing/command/completeWorkOrder.ts +212 -0
- package/src/modules/manufacturing/command/createBillOfMaterial.generated.ts +6 -0
- package/src/modules/manufacturing/command/createBillOfMaterial.test.ts +210 -0
- package/src/modules/manufacturing/command/createBillOfMaterial.ts +176 -0
- package/src/modules/manufacturing/command/createProductionOrder.generated.ts +6 -0
- package/src/modules/manufacturing/command/createProductionOrder.test.ts +160 -0
- package/src/modules/manufacturing/command/createProductionOrder.ts +129 -0
- package/src/modules/manufacturing/command/createRouting.generated.ts +6 -0
- package/src/modules/manufacturing/command/createRouting.test.ts +168 -0
- package/src/modules/manufacturing/command/createRouting.ts +128 -0
- package/src/modules/manufacturing/command/createWorkCenter.generated.ts +6 -0
- package/src/modules/manufacturing/command/createWorkCenter.test.ts +148 -0
- package/src/modules/manufacturing/command/createWorkCenter.ts +131 -0
- package/src/modules/manufacturing/command/deactivateBillOfMaterial.generated.ts +6 -0
- package/src/modules/manufacturing/command/deactivateBillOfMaterial.test.ts +103 -0
- package/src/modules/manufacturing/command/deactivateBillOfMaterial.ts +78 -0
- package/src/modules/manufacturing/command/deactivateRouting.generated.ts +6 -0
- package/src/modules/manufacturing/command/deactivateRouting.test.ts +112 -0
- package/src/modules/manufacturing/command/deactivateRouting.ts +76 -0
- package/src/modules/manufacturing/command/deactivateWorkCenter.generated.ts +6 -0
- package/src/modules/manufacturing/command/deactivateWorkCenter.test.ts +113 -0
- package/src/modules/manufacturing/command/deactivateWorkCenter.ts +85 -0
- package/src/modules/manufacturing/command/pauseWorkOrder.generated.ts +6 -0
- package/src/modules/manufacturing/command/pauseWorkOrder.test.ts +118 -0
- package/src/modules/manufacturing/command/pauseWorkOrder.ts +82 -0
- package/src/modules/manufacturing/command/recordInventoryIssueOutcome.generated.ts +6 -0
- package/src/modules/manufacturing/command/recordInventoryIssueOutcome.test.ts +183 -0
- package/src/modules/manufacturing/command/recordInventoryIssueOutcome.ts +139 -0
- package/src/modules/manufacturing/command/recordManufacturingCostSettlementAcknowledgment.generated.ts +6 -0
- package/src/modules/manufacturing/command/recordManufacturingCostSettlementAcknowledgment.test.ts +120 -0
- package/src/modules/manufacturing/command/recordManufacturingCostSettlementAcknowledgment.ts +110 -0
- package/src/modules/manufacturing/command/releaseProductionOrder.generated.ts +6 -0
- package/src/modules/manufacturing/command/releaseProductionOrder.test.ts +220 -0
- package/src/modules/manufacturing/command/releaseProductionOrder.ts +450 -0
- package/src/modules/manufacturing/command/reopenProductionOrder.generated.ts +6 -0
- package/src/modules/manufacturing/command/reopenProductionOrder.test.ts +196 -0
- package/src/modules/manufacturing/command/reopenProductionOrder.ts +98 -0
- package/src/modules/manufacturing/command/reportWorkOrderProgress.generated.ts +6 -0
- package/src/modules/manufacturing/command/reportWorkOrderProgress.test.ts +204 -0
- package/src/modules/manufacturing/command/reportWorkOrderProgress.ts +129 -0
- package/src/modules/manufacturing/command/rescheduleProductionOrder.generated.ts +6 -0
- package/src/modules/manufacturing/command/rescheduleProductionOrder.test.ts +185 -0
- package/src/modules/manufacturing/command/rescheduleProductionOrder.ts +95 -0
- package/src/modules/manufacturing/command/resumeWorkOrder.generated.ts +6 -0
- package/src/modules/manufacturing/command/resumeWorkOrder.test.ts +122 -0
- package/src/modules/manufacturing/command/resumeWorkOrder.ts +94 -0
- package/src/modules/manufacturing/command/reviewManufacturingCostSummary.generated.ts +6 -0
- package/src/modules/manufacturing/command/reviewManufacturingCostSummary.test.ts +231 -0
- package/src/modules/manufacturing/command/reviewManufacturingCostSummary.ts +137 -0
- package/src/modules/manufacturing/command/startWorkOrder.generated.ts +6 -0
- package/src/modules/manufacturing/command/startWorkOrder.test.ts +118 -0
- package/src/modules/manufacturing/command/startWorkOrder.ts +126 -0
- package/src/modules/manufacturing/command/technicallyCompleteProductionOrder.generated.ts +6 -0
- package/src/modules/manufacturing/command/technicallyCompleteProductionOrder.test.ts +153 -0
- package/src/modules/manufacturing/command/technicallyCompleteProductionOrder.ts +106 -0
- package/src/modules/manufacturing/command/unreleaseProductionOrder.generated.ts +6 -0
- package/src/modules/manufacturing/command/unreleaseProductionOrder.test.ts +140 -0
- package/src/modules/manufacturing/command/unreleaseProductionOrder.ts +131 -0
- package/src/modules/manufacturing/command/updateBillOfMaterial.generated.ts +6 -0
- package/src/modules/manufacturing/command/updateBillOfMaterial.test.ts +149 -0
- package/src/modules/manufacturing/command/updateBillOfMaterial.ts +174 -0
- package/src/modules/manufacturing/command/updateProductionOrder.generated.ts +6 -0
- package/src/modules/manufacturing/command/updateProductionOrder.test.ts +112 -0
- package/src/modules/manufacturing/command/updateProductionOrder.ts +145 -0
- package/src/modules/manufacturing/command/updateRouting.generated.ts +6 -0
- package/src/modules/manufacturing/command/updateRouting.test.ts +211 -0
- package/src/modules/manufacturing/command/updateRouting.ts +124 -0
- package/src/modules/manufacturing/command/updateWorkCenter.generated.ts +6 -0
- package/src/modules/manufacturing/command/updateWorkCenter.test.ts +152 -0
- package/src/modules/manufacturing/command/updateWorkCenter.ts +137 -0
- package/src/modules/manufacturing/db/.gitkeep +0 -0
- package/src/modules/manufacturing/db/billOfMaterial.ts +70 -0
- package/src/modules/manufacturing/db/billOfMaterialLine.ts +49 -0
- package/src/modules/manufacturing/db/costVarianceLine.ts +53 -0
- package/src/modules/manufacturing/db/manufacturingCostLine.ts +35 -0
- package/src/modules/manufacturing/db/manufacturingCostSettlementRecord.ts +39 -0
- package/src/modules/manufacturing/db/manufacturingCostSummary.ts +59 -0
- package/src/modules/manufacturing/db/productionOrder.ts +83 -0
- package/src/modules/manufacturing/db/productionOrderBomSnapshot.ts +44 -0
- package/src/modules/manufacturing/db/productionOrderCostBaseline.ts +44 -0
- package/src/modules/manufacturing/db/productionOrderMaterialRequirement.ts +57 -0
- package/src/modules/manufacturing/db/productionOrderRoutingSnapshot.ts +43 -0
- package/src/modules/manufacturing/db/routing.ts +63 -0
- package/src/modules/manufacturing/db/routingOperation.ts +57 -0
- package/src/modules/manufacturing/db/workCenter.ts +87 -0
- package/src/modules/manufacturing/db/workOrder.ts +65 -0
- package/src/modules/manufacturing/db/workOrderExecutionEvent.ts +54 -0
- package/src/modules/manufacturing/docs/commands/ActivateBillOfMaterial.md +50 -0
- package/src/modules/manufacturing/docs/commands/ActivateRouting.md +48 -0
- package/src/modules/manufacturing/docs/commands/ActivateWorkCenter.md +49 -0
- package/src/modules/manufacturing/docs/commands/CancelProductionOrder.md +48 -0
- package/src/modules/manufacturing/docs/commands/CloseProductionOrder.md +46 -0
- package/src/modules/manufacturing/docs/commands/CompleteProductionOrder.md +48 -0
- package/src/modules/manufacturing/docs/commands/CompleteWorkOrder.md +66 -0
- package/src/modules/manufacturing/docs/commands/CreateBillOfMaterial.md +54 -0
- package/src/modules/manufacturing/docs/commands/CreateProductionOrder.md +49 -0
- package/src/modules/manufacturing/docs/commands/CreateRouting.md +50 -0
- package/src/modules/manufacturing/docs/commands/CreateWorkCenter.md +51 -0
- package/src/modules/manufacturing/docs/commands/DeactivateBillOfMaterial.md +45 -0
- package/src/modules/manufacturing/docs/commands/DeactivateRouting.md +45 -0
- package/src/modules/manufacturing/docs/commands/DeactivateWorkCenter.md +45 -0
- package/src/modules/manufacturing/docs/commands/PauseWorkOrder.md +44 -0
- package/src/modules/manufacturing/docs/commands/RecordInventoryIssueOutcome.md +59 -0
- package/src/modules/manufacturing/docs/commands/RecordManufacturingCostSettlementAcknowledgment.md +49 -0
- package/src/modules/manufacturing/docs/commands/ReleaseProductionOrder.md +57 -0
- package/src/modules/manufacturing/docs/commands/ReopenProductionOrder.md +54 -0
- package/src/modules/manufacturing/docs/commands/ReportWorkOrderProgress.md +53 -0
- package/src/modules/manufacturing/docs/commands/RescheduleProductionOrder.md +45 -0
- package/src/modules/manufacturing/docs/commands/ResumeWorkOrder.md +44 -0
- package/src/modules/manufacturing/docs/commands/ReviewManufacturingCostSummary.md +52 -0
- package/src/modules/manufacturing/docs/commands/StartWorkOrder.md +46 -0
- package/src/modules/manufacturing/docs/commands/TechnicallyCompleteProductionOrder.md +51 -0
- package/src/modules/manufacturing/docs/commands/UnreleaseProductionOrder.md +46 -0
- package/src/modules/manufacturing/docs/commands/UpdateBillOfMaterial.md +48 -0
- package/src/modules/manufacturing/docs/commands/UpdateProductionOrder.md +48 -0
- package/src/modules/manufacturing/docs/commands/UpdateRouting.md +52 -0
- package/src/modules/manufacturing/docs/commands/UpdateWorkCenter.md +48 -0
- package/src/modules/manufacturing/docs/features/bill-of-material-management.md +83 -0
- package/src/modules/manufacturing/docs/features/manufacturing-cost-and-variance.md +191 -0
- package/src/modules/manufacturing/docs/features/production-order-lifecycle.md +103 -0
- package/src/modules/manufacturing/docs/features/routing-and-work-center-definition.md +63 -0
- package/src/modules/manufacturing/docs/features/work-order-execution.md +115 -0
- package/src/modules/manufacturing/docs/models/BillOfMaterial.md +60 -0
- package/src/modules/manufacturing/docs/models/ManufacturingCostSummary.md +66 -0
- package/src/modules/manufacturing/docs/models/ProductionOrder.md +76 -0
- package/src/modules/manufacturing/docs/models/Routing.md +58 -0
- package/src/modules/manufacturing/docs/models/WorkCenter.md +56 -0
- package/src/modules/manufacturing/docs/models/WorkOrder.md +63 -0
- package/src/modules/manufacturing/docs/queries/DetectBillOfMaterialCircularReference.md +39 -0
- package/src/modules/manufacturing/docs/queries/ExplodeBillOfMaterial.md +56 -0
- package/src/modules/manufacturing/docs/queries/GetBillOfMaterial.md +37 -0
- package/src/modules/manufacturing/docs/queries/GetManufacturingCostSummary.md +39 -0
- package/src/modules/manufacturing/docs/queries/GetProductionOrder.md +37 -0
- package/src/modules/manufacturing/docs/queries/GetRouting.md +39 -0
- package/src/modules/manufacturing/docs/queries/GetWorkCenter.md +35 -0
- package/src/modules/manufacturing/docs/queries/GetWorkOrder.md +38 -0
- package/src/modules/manufacturing/docs/queries/ListBillOfMaterialsByItem.md +42 -0
- package/src/modules/manufacturing/docs/queries/ListManufacturingCostSummariesByStatus.md +41 -0
- package/src/modules/manufacturing/docs/queries/ListProductionOrdersByStatus.md +41 -0
- package/src/modules/manufacturing/docs/queries/ListRoutingsByItem.md +42 -0
- package/src/modules/manufacturing/docs/queries/ListWorkCentersBySite.md +38 -0
- package/src/modules/manufacturing/docs/queries/ListWorkOrdersByProductionOrder.md +39 -0
- package/src/modules/manufacturing/docs/queries/ListWorkOrdersByWorkCenter.md +43 -0
- package/src/modules/manufacturing/executor/.gitkeep +0 -0
- package/src/modules/manufacturing/generated/enums.ts +113 -0
- package/src/modules/manufacturing/generated/kysely-tailordb.ts +247 -0
- package/src/modules/manufacturing/index.ts +2 -0
- package/src/modules/manufacturing/lib/_db_deps.ts +22 -0
- package/src/modules/manufacturing/lib/errors.generated.ts +592 -0
- package/src/modules/manufacturing/lib/permissions.generated.ts +35 -0
- package/src/modules/manufacturing/lib/types.ts +111 -0
- package/src/modules/manufacturing/module.ts +226 -0
- package/src/modules/manufacturing/permissions.ts +3 -0
- package/src/modules/manufacturing/query/.gitkeep +0 -0
- package/src/modules/manufacturing/query/detectBillOfMaterialCircularReference.generated.ts +5 -0
- package/src/modules/manufacturing/query/detectBillOfMaterialCircularReference.test.ts +115 -0
- package/src/modules/manufacturing/query/detectBillOfMaterialCircularReference.ts +79 -0
- package/src/modules/manufacturing/query/explodeBillOfMaterial.generated.ts +5 -0
- package/src/modules/manufacturing/query/explodeBillOfMaterial.test.ts +445 -0
- package/src/modules/manufacturing/query/explodeBillOfMaterial.ts +306 -0
- package/src/modules/manufacturing/query/getBillOfMaterial.generated.ts +5 -0
- package/src/modules/manufacturing/query/getBillOfMaterial.test.ts +64 -0
- package/src/modules/manufacturing/query/getBillOfMaterial.ts +27 -0
- package/src/modules/manufacturing/query/getManufacturingCostSummary.generated.ts +5 -0
- package/src/modules/manufacturing/query/getManufacturingCostSummary.test.ts +147 -0
- package/src/modules/manufacturing/query/getManufacturingCostSummary.ts +46 -0
- package/src/modules/manufacturing/query/getProductionOrder.generated.ts +5 -0
- package/src/modules/manufacturing/query/getProductionOrder.test.ts +139 -0
- package/src/modules/manufacturing/query/getProductionOrder.ts +84 -0
- package/src/modules/manufacturing/query/getRouting.generated.ts +5 -0
- package/src/modules/manufacturing/query/getRouting.test.ts +71 -0
- package/src/modules/manufacturing/query/getRouting.ts +34 -0
- package/src/modules/manufacturing/query/getWorkCenter.generated.ts +5 -0
- package/src/modules/manufacturing/query/getWorkCenter.test.ts +37 -0
- package/src/modules/manufacturing/query/getWorkCenter.ts +21 -0
- package/src/modules/manufacturing/query/getWorkOrder.generated.ts +5 -0
- package/src/modules/manufacturing/query/getWorkOrder.test.ts +73 -0
- package/src/modules/manufacturing/query/getWorkOrder.ts +28 -0
- package/src/modules/manufacturing/query/listBillOfMaterialsByItem.generated.ts +5 -0
- package/src/modules/manufacturing/query/listBillOfMaterialsByItem.test.ts +107 -0
- package/src/modules/manufacturing/query/listBillOfMaterialsByItem.ts +58 -0
- package/src/modules/manufacturing/query/listManufacturingCostSummariesByStatus.generated.ts +5 -0
- package/src/modules/manufacturing/query/listManufacturingCostSummariesByStatus.test.ts +96 -0
- package/src/modules/manufacturing/query/listManufacturingCostSummariesByStatus.ts +77 -0
- package/src/modules/manufacturing/query/listProductionOrdersByStatus.generated.ts +5 -0
- package/src/modules/manufacturing/query/listProductionOrdersByStatus.test.ts +121 -0
- package/src/modules/manufacturing/query/listProductionOrdersByStatus.ts +83 -0
- package/src/modules/manufacturing/query/listRoutingsByItem.generated.ts +5 -0
- package/src/modules/manufacturing/query/listRoutingsByItem.test.ts +110 -0
- package/src/modules/manufacturing/query/listRoutingsByItem.ts +54 -0
- package/src/modules/manufacturing/query/listWorkCentersBySite.generated.ts +5 -0
- package/src/modules/manufacturing/query/listWorkCentersBySite.test.ts +81 -0
- package/src/modules/manufacturing/query/listWorkCentersBySite.ts +70 -0
- package/src/modules/manufacturing/query/listWorkOrdersByProductionOrder.generated.ts +5 -0
- package/src/modules/manufacturing/query/listWorkOrdersByProductionOrder.test.ts +102 -0
- package/src/modules/manufacturing/query/listWorkOrdersByProductionOrder.ts +53 -0
- package/src/modules/manufacturing/query/listWorkOrdersByWorkCenter.generated.ts +5 -0
- package/src/modules/manufacturing/query/listWorkOrdersByWorkCenter.test.ts +143 -0
- package/src/modules/manufacturing/query/listWorkOrdersByWorkCenter.ts +56 -0
- package/src/modules/manufacturing/seed/index.ts +19 -0
- package/src/modules/manufacturing/tailor.config.ts +13 -0
- package/src/modules/manufacturing/tailor.d.ts +13 -0
- package/src/modules/manufacturing/testing/commandTestUtils.ts +29 -0
- package/src/modules/manufacturing/testing/fixtures.ts +402 -0
- package/templates/scaffold/app/backend/package.json +9 -2
- package/templates/scaffold/app/backend/src/tests/utils/graphql-client.ts +66 -0
- package/templates/scaffold/app/backend/src/tests/utils/setup.ts +21 -0
- package/templates/scaffold/app/backend/tsconfig.json +9 -2
- package/templates/scaffold/app/backend/vitest.config.ts +35 -0
- package/templates/scaffold/app/frontend/package.json +2 -2
- package/templates/scaffold/module/__dot__gitignore +3 -0
- package/templates/scaffold/module/eslint.config.js +31 -0
- package/templates/scaffold/module/generated/kysely-tailordb.ts +3 -0
- package/templates/scaffold/module/lib/types.ts +1 -6
- package/templates/scaffold/module/package.json +26 -0
- package/templates/scaffold/module/tsconfig.json +16 -0
- /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
|
+
});
|