@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.
- package/CHANGELOG.md +15 -0
- package/dist/cli.mjs +103 -23
- package/package.json +1 -1
- package/skills/erp-kit-app-5-impl-backend/SKILL.md +7 -4
- 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/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-stubs.ts +4 -0
- package/src/generator/stub-templates.test.ts +11 -0
- package/src/generator/stub-templates.ts +22 -1
- 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/.gitkeep +0 -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
|
@@ -0,0 +1,151 @@
|
|
|
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
|
+
ProductionOrderNotCancellableError,
|
|
7
|
+
ExecutionAlreadyStartedError,
|
|
8
|
+
InventoryHandoffExistsError,
|
|
9
|
+
} from "../lib/errors.generated";
|
|
10
|
+
import {
|
|
11
|
+
baseDraftProductionOrder,
|
|
12
|
+
baseReleasedProductionOrder,
|
|
13
|
+
baseInProgressProductionOrder,
|
|
14
|
+
basePendingWorkOrder,
|
|
15
|
+
baseInProgressWorkOrder,
|
|
16
|
+
baseCollectingCostSummary,
|
|
17
|
+
} from "../testing/fixtures";
|
|
18
|
+
import { run } from "./cancelProductionOrder";
|
|
19
|
+
import type { CommandContext } from "@tailor-platform/erp-kit/module";
|
|
20
|
+
|
|
21
|
+
describe("cancelProductionOrder", () => {
|
|
22
|
+
const ctx: CommandContext = {
|
|
23
|
+
actorId: "test-actor",
|
|
24
|
+
permissions: ["manufacturing:cancelProductionOrder"],
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
it("cancels a draft production order", async () => {
|
|
28
|
+
const { db, spies } = createMockDb<Transaction>();
|
|
29
|
+
const cancelled = { ...baseDraftProductionOrder, status: "CANCELLED" as const };
|
|
30
|
+
|
|
31
|
+
// order lookup
|
|
32
|
+
spies.select.mockReturnValueOnce(baseDraftProductionOrder);
|
|
33
|
+
// update order status
|
|
34
|
+
spies.update.mockReturnValue(cancelled);
|
|
35
|
+
|
|
36
|
+
const result = await run(db, { id: baseDraftProductionOrder.id }, ctx);
|
|
37
|
+
|
|
38
|
+
expect(result.ok).toBe(true);
|
|
39
|
+
if (result.ok) {
|
|
40
|
+
expect(result.value.productionOrder.status).toBe("CANCELLED");
|
|
41
|
+
}
|
|
42
|
+
expect(spies.update).toHaveBeenCalled();
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it("cancels a released order before execution starts", async () => {
|
|
46
|
+
const { db, spies } = createMockDb<Transaction>();
|
|
47
|
+
const cancelled = { ...baseReleasedProductionOrder, status: "CANCELLED" as const };
|
|
48
|
+
|
|
49
|
+
// order lookup
|
|
50
|
+
spies.select.mockReturnValueOnce(baseReleasedProductionOrder);
|
|
51
|
+
// work orders - all pending
|
|
52
|
+
spies.select.mockReturnValueOnce([basePendingWorkOrder]);
|
|
53
|
+
// cost summary - no actuals
|
|
54
|
+
spies.select.mockReturnValueOnce(baseCollectingCostSummary);
|
|
55
|
+
// cascade cancel work orders
|
|
56
|
+
spies.update.mockReturnValueOnce(undefined);
|
|
57
|
+
// update order status
|
|
58
|
+
spies.update.mockReturnValue(cancelled);
|
|
59
|
+
|
|
60
|
+
const result = await run(db, { id: baseReleasedProductionOrder.id }, ctx);
|
|
61
|
+
|
|
62
|
+
expect(result.ok).toBe(true);
|
|
63
|
+
if (result.ok) {
|
|
64
|
+
expect(result.value.productionOrder.status).toBe("CANCELLED");
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it("returns error when the order does not exist", async () => {
|
|
69
|
+
const { db, spies } = createMockDb<Transaction>();
|
|
70
|
+
spies.select.mockReturnValueOnce(undefined);
|
|
71
|
+
|
|
72
|
+
const result = await run(db, { id: "nonexistent" }, ctx);
|
|
73
|
+
|
|
74
|
+
expect(result.ok).toBe(false);
|
|
75
|
+
if (!result.ok) {
|
|
76
|
+
expect(result.error).toBeInstanceOf(ProductionOrderNotFoundError);
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it("returns error when the status does not allow cancellation", async () => {
|
|
81
|
+
const { db, spies } = createMockDb<Transaction>();
|
|
82
|
+
spies.select.mockReturnValueOnce(baseInProgressProductionOrder);
|
|
83
|
+
|
|
84
|
+
const result = await run(db, { id: baseInProgressProductionOrder.id }, ctx);
|
|
85
|
+
|
|
86
|
+
expect(result.ok).toBe(false);
|
|
87
|
+
if (!result.ok) {
|
|
88
|
+
expect(result.error).toBeInstanceOf(ProductionOrderNotCancellableError);
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it("returns error when execution evidence already exists", async () => {
|
|
93
|
+
const { db, spies } = createMockDb<Transaction>();
|
|
94
|
+
|
|
95
|
+
// order lookup
|
|
96
|
+
spies.select.mockReturnValueOnce(baseReleasedProductionOrder);
|
|
97
|
+
// work orders - one in progress
|
|
98
|
+
spies.select.mockReturnValueOnce([baseInProgressWorkOrder]);
|
|
99
|
+
|
|
100
|
+
const result = await run(db, { id: baseReleasedProductionOrder.id }, ctx);
|
|
101
|
+
|
|
102
|
+
expect(result.ok).toBe(false);
|
|
103
|
+
if (!result.ok) {
|
|
104
|
+
expect(result.error).toBeInstanceOf(ExecutionAlreadyStartedError);
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it("returns error when inventory handoff evidence already exists", async () => {
|
|
109
|
+
const { db, spies } = createMockDb<Transaction>();
|
|
110
|
+
const costSummaryWithActuals = {
|
|
111
|
+
...baseCollectingCostSummary,
|
|
112
|
+
actualMaterialCost: 500,
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
// order lookup
|
|
116
|
+
spies.select.mockReturnValueOnce(baseReleasedProductionOrder);
|
|
117
|
+
// work orders - all pending
|
|
118
|
+
spies.select.mockReturnValueOnce([basePendingWorkOrder]);
|
|
119
|
+
// cost summary with actual costs
|
|
120
|
+
spies.select.mockReturnValueOnce(costSummaryWithActuals);
|
|
121
|
+
|
|
122
|
+
const result = await run(db, { id: baseReleasedProductionOrder.id }, ctx);
|
|
123
|
+
|
|
124
|
+
expect(result.ok).toBe(false);
|
|
125
|
+
if (!result.ok) {
|
|
126
|
+
expect(result.error).toBeInstanceOf(InventoryHandoffExistsError);
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it("cascades cancellation to pending work orders", async () => {
|
|
131
|
+
const { db, spies } = createMockDb<Transaction>();
|
|
132
|
+
const cancelled = { ...baseReleasedProductionOrder, status: "CANCELLED" as const };
|
|
133
|
+
|
|
134
|
+
// order lookup
|
|
135
|
+
spies.select.mockReturnValueOnce(baseReleasedProductionOrder);
|
|
136
|
+
// work orders - all pending
|
|
137
|
+
spies.select.mockReturnValueOnce([basePendingWorkOrder]);
|
|
138
|
+
// cost summary - no actuals
|
|
139
|
+
spies.select.mockReturnValueOnce(baseCollectingCostSummary);
|
|
140
|
+
// cascade cancel work orders (first update call)
|
|
141
|
+
spies.update.mockReturnValueOnce(undefined);
|
|
142
|
+
// update order status (second update call)
|
|
143
|
+
spies.update.mockReturnValue(cancelled);
|
|
144
|
+
|
|
145
|
+
const result = await run(db, { id: baseReleasedProductionOrder.id }, ctx);
|
|
146
|
+
|
|
147
|
+
expect(result.ok).toBe(true);
|
|
148
|
+
// update should be called at least twice: once for work orders, once for order
|
|
149
|
+
expect(spies.update).toHaveBeenCalled();
|
|
150
|
+
});
|
|
151
|
+
});
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import type { Transaction } from "../generated/kysely-tailordb";
|
|
2
|
+
import {
|
|
3
|
+
ProductionOrderNotFoundError,
|
|
4
|
+
ProductionOrderNotCancellableError,
|
|
5
|
+
ExecutionAlreadyStartedError,
|
|
6
|
+
InventoryHandoffExistsError,
|
|
7
|
+
} from "../lib/errors.generated";
|
|
8
|
+
import { ok, err, type CommandContext } from "@tailor-platform/erp-kit/module";
|
|
9
|
+
|
|
10
|
+
const CANCELLABLE_STATUSES = ["DRAFT", "RELEASED"] as const;
|
|
11
|
+
|
|
12
|
+
export interface CancelProductionOrderInput {
|
|
13
|
+
id: string;
|
|
14
|
+
from?: string[];
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Function: cancelProductionOrder
|
|
19
|
+
*
|
|
20
|
+
* Abandons a draft or not-yet-started released order. Preserves audit
|
|
21
|
+
* history while blocking further execution updates and cascading
|
|
22
|
+
* cancellation to pending work orders.
|
|
23
|
+
*/
|
|
24
|
+
export async function run<CF extends Record<string, unknown>>(
|
|
25
|
+
db: Transaction,
|
|
26
|
+
input: CancelProductionOrderInput & CF,
|
|
27
|
+
_ctx: CommandContext,
|
|
28
|
+
) {
|
|
29
|
+
const { id, from, ...customFields } = input;
|
|
30
|
+
void customFields;
|
|
31
|
+
|
|
32
|
+
const allowedStatuses = from ?? [...CANCELLABLE_STATUSES];
|
|
33
|
+
|
|
34
|
+
// 1. Fetch production order with lock
|
|
35
|
+
const order = await db
|
|
36
|
+
.selectFrom("ProductionOrder")
|
|
37
|
+
.selectAll()
|
|
38
|
+
.where("id", "=", id)
|
|
39
|
+
.forUpdate()
|
|
40
|
+
.executeTakeFirst();
|
|
41
|
+
|
|
42
|
+
if (!order) {
|
|
43
|
+
return err(new ProductionOrderNotFoundError(id));
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// 2. Validate status is cancellable
|
|
47
|
+
if (!allowedStatuses.includes(order.status)) {
|
|
48
|
+
return err(new ProductionOrderNotCancellableError(id));
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// 3. For released orders, check execution and inventory evidence
|
|
52
|
+
if (order.status === "RELEASED") {
|
|
53
|
+
// Check work orders for execution evidence
|
|
54
|
+
const workOrders = await db
|
|
55
|
+
.selectFrom("WorkOrder")
|
|
56
|
+
.selectAll()
|
|
57
|
+
.where("productionOrderId", "=", id)
|
|
58
|
+
.execute();
|
|
59
|
+
|
|
60
|
+
const hasExecution = workOrders.some(
|
|
61
|
+
(wo) =>
|
|
62
|
+
wo.status === "IN_PROGRESS" ||
|
|
63
|
+
wo.status === "PAUSED" ||
|
|
64
|
+
wo.status === "COMPLETE" ||
|
|
65
|
+
wo.completedQuantity > 0 ||
|
|
66
|
+
wo.actualStartDate != null,
|
|
67
|
+
);
|
|
68
|
+
|
|
69
|
+
if (hasExecution) {
|
|
70
|
+
return err(new ExecutionAlreadyStartedError(id));
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Check for inventory handoff evidence
|
|
74
|
+
const costSummary = await db
|
|
75
|
+
.selectFrom("ManufacturingCostSummary")
|
|
76
|
+
.selectAll()
|
|
77
|
+
.where("productionOrderId", "=", id)
|
|
78
|
+
.executeTakeFirst();
|
|
79
|
+
|
|
80
|
+
if (
|
|
81
|
+
costSummary &&
|
|
82
|
+
(costSummary.actualMaterialCost > 0 ||
|
|
83
|
+
costSummary.actualLaborCost > 0 ||
|
|
84
|
+
costSummary.actualMachineCost > 0 ||
|
|
85
|
+
costSummary.actualOverheadCost > 0)
|
|
86
|
+
) {
|
|
87
|
+
return err(new InventoryHandoffExistsError(id));
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// 4. Cascade cancellation to pending work orders
|
|
91
|
+
await db
|
|
92
|
+
.updateTable("WorkOrder")
|
|
93
|
+
.set({
|
|
94
|
+
status: "CANCELLED",
|
|
95
|
+
updatedAt: new Date(),
|
|
96
|
+
})
|
|
97
|
+
.where("productionOrderId", "=", id)
|
|
98
|
+
.where("status", "=", "PENDING")
|
|
99
|
+
.execute();
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// 5. Set order status to CANCELLED
|
|
103
|
+
const cancelledOrder = await db
|
|
104
|
+
.updateTable("ProductionOrder")
|
|
105
|
+
.set({
|
|
106
|
+
status: "CANCELLED",
|
|
107
|
+
updatedAt: new Date(),
|
|
108
|
+
})
|
|
109
|
+
.where("id", "=", id)
|
|
110
|
+
.returningAll()
|
|
111
|
+
.executeTakeFirstOrThrow();
|
|
112
|
+
|
|
113
|
+
return ok({ productionOrder: cancelledOrder });
|
|
114
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
// @generated — do not edit
|
|
2
|
+
import { permissions } from "../lib/permissions.generated";
|
|
3
|
+
import { run } from "./closeProductionOrder";
|
|
4
|
+
import { defineCommand } from "@tailor-platform/erp-kit/module";
|
|
5
|
+
|
|
6
|
+
export const closeProductionOrder = defineCommand(permissions.closeProductionOrder, run);
|
|
@@ -0,0 +1,126 @@
|
|
|
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
|
+
ProductionOrderNotClosableError,
|
|
7
|
+
OpenWorkRemainsError,
|
|
8
|
+
CostSummaryNotSettledError,
|
|
9
|
+
} from "../lib/errors.generated";
|
|
10
|
+
import {
|
|
11
|
+
baseTechCompleteProductionOrder,
|
|
12
|
+
baseCompletedProductionOrder,
|
|
13
|
+
baseClosedProductionOrder,
|
|
14
|
+
baseCompleteWorkOrder,
|
|
15
|
+
baseCancelledWorkOrder,
|
|
16
|
+
basePendingWorkOrder,
|
|
17
|
+
baseSettledCostSummary,
|
|
18
|
+
basePendingReviewCostSummary,
|
|
19
|
+
} from "../testing/fixtures";
|
|
20
|
+
import { run } from "./closeProductionOrder";
|
|
21
|
+
import type { CommandContext } from "@tailor-platform/erp-kit/module";
|
|
22
|
+
|
|
23
|
+
describe("closeProductionOrder", () => {
|
|
24
|
+
const ctx: CommandContext = {
|
|
25
|
+
actorId: "test-actor",
|
|
26
|
+
permissions: ["manufacturing:closeProductionOrder"],
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
it("closes a technically complete order with a settled cost summary", async () => {
|
|
30
|
+
const { db, spies } = createMockDb<Transaction>();
|
|
31
|
+
const closed = { ...baseTechCompleteProductionOrder, status: "CLOSED" as const };
|
|
32
|
+
|
|
33
|
+
// order lookup
|
|
34
|
+
spies.select.mockReturnValueOnce(baseTechCompleteProductionOrder);
|
|
35
|
+
// work orders - all complete or cancelled
|
|
36
|
+
spies.select.mockReturnValueOnce([baseCompleteWorkOrder, baseCancelledWorkOrder]);
|
|
37
|
+
// cost summary in SETTLED
|
|
38
|
+
spies.select.mockReturnValueOnce({
|
|
39
|
+
...baseSettledCostSummary,
|
|
40
|
+
productionOrderId: baseTechCompleteProductionOrder.id,
|
|
41
|
+
});
|
|
42
|
+
// update order status
|
|
43
|
+
spies.update.mockReturnValue(closed);
|
|
44
|
+
|
|
45
|
+
const result = await run(db, { id: baseTechCompleteProductionOrder.id }, ctx);
|
|
46
|
+
|
|
47
|
+
expect(result.ok).toBe(true);
|
|
48
|
+
if (result.ok) {
|
|
49
|
+
expect(result.value.productionOrder.status).toBe("CLOSED");
|
|
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 technically complete", async () => {
|
|
67
|
+
const { db, spies } = createMockDb<Transaction>();
|
|
68
|
+
spies.select.mockReturnValueOnce(baseCompletedProductionOrder);
|
|
69
|
+
|
|
70
|
+
const result = await run(db, { id: baseCompletedProductionOrder.id }, ctx);
|
|
71
|
+
|
|
72
|
+
expect(result.ok).toBe(false);
|
|
73
|
+
if (!result.ok) {
|
|
74
|
+
expect(result.error).toBeInstanceOf(ProductionOrderNotClosableError);
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it("returns error when open work or execution exceptions remain", async () => {
|
|
79
|
+
const { db, spies } = createMockDb<Transaction>();
|
|
80
|
+
|
|
81
|
+
// order lookup
|
|
82
|
+
spies.select.mockReturnValueOnce(baseTechCompleteProductionOrder);
|
|
83
|
+
// work orders - one still pending
|
|
84
|
+
spies.select.mockReturnValueOnce([baseCompleteWorkOrder, basePendingWorkOrder]);
|
|
85
|
+
|
|
86
|
+
const result = await run(db, { id: baseTechCompleteProductionOrder.id }, ctx);
|
|
87
|
+
|
|
88
|
+
expect(result.ok).toBe(false);
|
|
89
|
+
if (!result.ok) {
|
|
90
|
+
expect(result.error).toBeInstanceOf(OpenWorkRemainsError);
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it("returns error when the linked cost summary is not settled", async () => {
|
|
95
|
+
const { db, spies } = createMockDb<Transaction>();
|
|
96
|
+
|
|
97
|
+
// order lookup
|
|
98
|
+
spies.select.mockReturnValueOnce(baseTechCompleteProductionOrder);
|
|
99
|
+
// work orders - all complete
|
|
100
|
+
spies.select.mockReturnValueOnce([baseCompleteWorkOrder]);
|
|
101
|
+
// cost summary not settled
|
|
102
|
+
spies.select.mockReturnValueOnce({
|
|
103
|
+
...basePendingReviewCostSummary,
|
|
104
|
+
productionOrderId: baseTechCompleteProductionOrder.id,
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
const result = await run(db, { id: baseTechCompleteProductionOrder.id }, ctx);
|
|
108
|
+
|
|
109
|
+
expect(result.ok).toBe(false);
|
|
110
|
+
if (!result.ok) {
|
|
111
|
+
expect(result.error).toBeInstanceOf(CostSummaryNotSettledError);
|
|
112
|
+
}
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it("blocks further execution commands after close", async () => {
|
|
116
|
+
const { db, spies } = createMockDb<Transaction>();
|
|
117
|
+
spies.select.mockReturnValueOnce(baseClosedProductionOrder);
|
|
118
|
+
|
|
119
|
+
const result = await run(db, { id: baseClosedProductionOrder.id }, ctx);
|
|
120
|
+
|
|
121
|
+
expect(result.ok).toBe(false);
|
|
122
|
+
if (!result.ok) {
|
|
123
|
+
expect(result.error).toBeInstanceOf(ProductionOrderNotClosableError);
|
|
124
|
+
}
|
|
125
|
+
});
|
|
126
|
+
});
|
|
@@ -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
|
+
});
|