@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,168 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { createMockDb } from "../../../testing/index";
|
|
3
|
+
import type { Transaction } from "../generated/kysely-tailordb";
|
|
4
|
+
import {
|
|
5
|
+
RoutingItemNotFoundError,
|
|
6
|
+
OperationRequiredError,
|
|
7
|
+
DuplicateOperationSequenceError,
|
|
8
|
+
InvalidStandardTimeError,
|
|
9
|
+
WorkCenterNotFoundError,
|
|
10
|
+
CrossCompanyWorkCenterError,
|
|
11
|
+
} from "../lib/errors.generated";
|
|
12
|
+
import { baseDraftRouting, baseActiveWorkCenter } from "../testing/fixtures";
|
|
13
|
+
import { run } from "./createRouting";
|
|
14
|
+
import type { CommandContext } from "@tailor-platform/erp-kit/module";
|
|
15
|
+
|
|
16
|
+
describe("createRouting", () => {
|
|
17
|
+
const ctx: CommandContext = {
|
|
18
|
+
actorId: "test-actor",
|
|
19
|
+
permissions: ["manufacturing:createRouting"],
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
const validOperations = [
|
|
23
|
+
{
|
|
24
|
+
sequenceNumber: 10,
|
|
25
|
+
operationDescription: "Assembly step",
|
|
26
|
+
workCenterId: "work-center-2",
|
|
27
|
+
standardSetupTime: 30,
|
|
28
|
+
standardRunTime: 60,
|
|
29
|
+
operatorInstructions: null,
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
sequenceNumber: 20,
|
|
33
|
+
operationDescription: "Quality check",
|
|
34
|
+
workCenterId: "work-center-2",
|
|
35
|
+
standardSetupTime: 10,
|
|
36
|
+
standardRunTime: 15,
|
|
37
|
+
operatorInstructions: null,
|
|
38
|
+
},
|
|
39
|
+
];
|
|
40
|
+
|
|
41
|
+
const validInput = {
|
|
42
|
+
parentItemId: "item-1",
|
|
43
|
+
companyId: "company-1",
|
|
44
|
+
siteId: null,
|
|
45
|
+
revisionNumber: "R1",
|
|
46
|
+
operations: validOperations,
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
it("creates a draft routing with ordered operations", async () => {
|
|
50
|
+
const { db, spies } = createMockDb<Transaction>();
|
|
51
|
+
|
|
52
|
+
// select: Item lookup
|
|
53
|
+
spies.select.mockReturnValueOnce({ id: "item-1" });
|
|
54
|
+
// select: WorkCenter lookup for op 1
|
|
55
|
+
spies.select.mockReturnValueOnce(baseActiveWorkCenter);
|
|
56
|
+
// select: WorkCenter lookup for op 2
|
|
57
|
+
spies.select.mockReturnValueOnce(baseActiveWorkCenter);
|
|
58
|
+
// insert: Routing
|
|
59
|
+
spies.insert.mockReturnValue(baseDraftRouting);
|
|
60
|
+
|
|
61
|
+
const result = await run(db, validInput, ctx);
|
|
62
|
+
|
|
63
|
+
expect(result.ok).toBe(true);
|
|
64
|
+
if (result.ok) {
|
|
65
|
+
expect(result.value.routing.status).toBe("DRAFT");
|
|
66
|
+
expect(result.value.routing.parentItemId).toBe("item-1");
|
|
67
|
+
}
|
|
68
|
+
expect(spies.insert).toHaveBeenCalled();
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it("returns error when no operations are provided", async () => {
|
|
72
|
+
const { db, spies } = createMockDb<Transaction>();
|
|
73
|
+
|
|
74
|
+
// select: Item lookup
|
|
75
|
+
spies.select.mockReturnValueOnce({ id: "item-1" });
|
|
76
|
+
|
|
77
|
+
const result = await run(db, { ...validInput, operations: [] }, ctx);
|
|
78
|
+
|
|
79
|
+
expect(result.ok).toBe(false);
|
|
80
|
+
if (!result.ok) {
|
|
81
|
+
expect(result.error).toBeInstanceOf(OperationRequiredError);
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it("returns error when two operations share the same sequence", async () => {
|
|
86
|
+
const { db, spies } = createMockDb<Transaction>();
|
|
87
|
+
|
|
88
|
+
// select: Item lookup
|
|
89
|
+
spies.select.mockReturnValueOnce({ id: "item-1" });
|
|
90
|
+
|
|
91
|
+
const duplicateOps = [
|
|
92
|
+
{ ...validOperations[0], sequenceNumber: 10 },
|
|
93
|
+
{ ...validOperations[1], sequenceNumber: 10 },
|
|
94
|
+
];
|
|
95
|
+
|
|
96
|
+
const result = await run(db, { ...validInput, operations: duplicateOps }, ctx);
|
|
97
|
+
|
|
98
|
+
expect(result.ok).toBe(false);
|
|
99
|
+
if (!result.ok) {
|
|
100
|
+
expect(result.error).toBeInstanceOf(DuplicateOperationSequenceError);
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it("returns error when a standard time is negative", async () => {
|
|
105
|
+
const { db, spies } = createMockDb<Transaction>();
|
|
106
|
+
|
|
107
|
+
// select: Item lookup
|
|
108
|
+
spies.select.mockReturnValueOnce({ id: "item-1" });
|
|
109
|
+
|
|
110
|
+
const badOps = [{ ...validOperations[0], standardSetupTime: -5 }];
|
|
111
|
+
|
|
112
|
+
const result = await run(db, { ...validInput, operations: badOps }, ctx);
|
|
113
|
+
|
|
114
|
+
expect(result.ok).toBe(false);
|
|
115
|
+
if (!result.ok) {
|
|
116
|
+
expect(result.error).toBeInstanceOf(InvalidStandardTimeError);
|
|
117
|
+
}
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it("returns error when a referenced work center does not exist", async () => {
|
|
121
|
+
const { db, spies } = createMockDb<Transaction>();
|
|
122
|
+
|
|
123
|
+
// select: Item lookup
|
|
124
|
+
spies.select.mockReturnValueOnce({ id: "item-1" });
|
|
125
|
+
// select: WorkCenter lookup - not found
|
|
126
|
+
spies.select.mockReturnValueOnce(undefined);
|
|
127
|
+
|
|
128
|
+
const result = await run(db, validInput, ctx);
|
|
129
|
+
|
|
130
|
+
expect(result.ok).toBe(false);
|
|
131
|
+
if (!result.ok) {
|
|
132
|
+
expect(result.error).toBeInstanceOf(WorkCenterNotFoundError);
|
|
133
|
+
}
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it("returns error when a work center is outside the routing company", async () => {
|
|
137
|
+
const { db, spies } = createMockDb<Transaction>();
|
|
138
|
+
|
|
139
|
+
// select: Item lookup
|
|
140
|
+
spies.select.mockReturnValueOnce({ id: "item-1" });
|
|
141
|
+
// select: WorkCenter lookup - different company
|
|
142
|
+
spies.select.mockReturnValueOnce({
|
|
143
|
+
...baseActiveWorkCenter,
|
|
144
|
+
companyId: "other-company",
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
const result = await run(db, validInput, ctx);
|
|
148
|
+
|
|
149
|
+
expect(result.ok).toBe(false);
|
|
150
|
+
if (!result.ok) {
|
|
151
|
+
expect(result.error).toBeInstanceOf(CrossCompanyWorkCenterError);
|
|
152
|
+
}
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it("returns error when parent item does not exist", async () => {
|
|
156
|
+
const { db, spies } = createMockDb<Transaction>();
|
|
157
|
+
|
|
158
|
+
// select: Item lookup - not found
|
|
159
|
+
spies.select.mockReturnValueOnce(undefined);
|
|
160
|
+
|
|
161
|
+
const result = await run(db, validInput, ctx);
|
|
162
|
+
|
|
163
|
+
expect(result.ok).toBe(false);
|
|
164
|
+
if (!result.ok) {
|
|
165
|
+
expect(result.error).toBeInstanceOf(RoutingItemNotFoundError);
|
|
166
|
+
}
|
|
167
|
+
});
|
|
168
|
+
});
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import type { Transaction } from "../generated/kysely-tailordb";
|
|
2
|
+
import {
|
|
3
|
+
RoutingItemNotFoundError,
|
|
4
|
+
OperationRequiredError,
|
|
5
|
+
DuplicateOperationSequenceError,
|
|
6
|
+
InvalidStandardTimeError,
|
|
7
|
+
WorkCenterNotFoundError,
|
|
8
|
+
CrossCompanyWorkCenterError,
|
|
9
|
+
} from "../lib/errors.generated";
|
|
10
|
+
import { ok, err, type CommandContext } from "@tailor-platform/erp-kit/module";
|
|
11
|
+
|
|
12
|
+
export interface CreateRoutingOperationInput {
|
|
13
|
+
sequenceNumber: number;
|
|
14
|
+
operationDescription?: string | null;
|
|
15
|
+
workCenterId: string;
|
|
16
|
+
standardSetupTime: number;
|
|
17
|
+
standardRunTime: number;
|
|
18
|
+
operatorInstructions?: string | null;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface CreateRoutingInput {
|
|
22
|
+
parentItemId: string;
|
|
23
|
+
companyId: string;
|
|
24
|
+
siteId?: string | null;
|
|
25
|
+
revisionNumber?: string | null;
|
|
26
|
+
operations: CreateRoutingOperationInput[];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Function: createRouting
|
|
31
|
+
*
|
|
32
|
+
* Creates a new routing in DRAFT status with ordered operations.
|
|
33
|
+
* Validates parent item existence, operation sequence uniqueness,
|
|
34
|
+
* standard times, and work center references.
|
|
35
|
+
*/
|
|
36
|
+
export async function run<CF extends Record<string, unknown>>(
|
|
37
|
+
db: Transaction,
|
|
38
|
+
input: CreateRoutingInput & CF,
|
|
39
|
+
_ctx: CommandContext,
|
|
40
|
+
) {
|
|
41
|
+
const { parentItemId, companyId, siteId, revisionNumber, operations, ...customFields } = input;
|
|
42
|
+
|
|
43
|
+
// 1. Validate parent item exists
|
|
44
|
+
const parentItem = await db
|
|
45
|
+
.selectFrom("Item")
|
|
46
|
+
.selectAll()
|
|
47
|
+
.where("id", "=", parentItemId)
|
|
48
|
+
.executeTakeFirst();
|
|
49
|
+
|
|
50
|
+
if (!parentItem) {
|
|
51
|
+
return err(new RoutingItemNotFoundError(parentItemId));
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// 2. Validate at least one operation
|
|
55
|
+
if (!operations || operations.length === 0) {
|
|
56
|
+
return err(new OperationRequiredError(parentItemId));
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// 3. Validate unique sequence numbers
|
|
60
|
+
const seqNumbers = operations.map((op) => op.sequenceNumber);
|
|
61
|
+
const uniqueSeqs = new Set(seqNumbers);
|
|
62
|
+
if (uniqueSeqs.size !== seqNumbers.length) {
|
|
63
|
+
return err(new DuplicateOperationSequenceError(parentItemId));
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// 4. Validate standard times >= 0
|
|
67
|
+
for (const op of operations) {
|
|
68
|
+
if (op.standardSetupTime < 0 || op.standardRunTime < 0) {
|
|
69
|
+
return err(new InvalidStandardTimeError(parentItemId));
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// 5. Validate work centers exist and belong to same company
|
|
74
|
+
for (const op of operations) {
|
|
75
|
+
const workCenter = await db
|
|
76
|
+
.selectFrom("WorkCenter")
|
|
77
|
+
.selectAll()
|
|
78
|
+
.where("id", "=", op.workCenterId)
|
|
79
|
+
.executeTakeFirst();
|
|
80
|
+
|
|
81
|
+
if (!workCenter) {
|
|
82
|
+
return err(new WorkCenterNotFoundError(op.workCenterId));
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (workCenter.companyId !== companyId) {
|
|
86
|
+
return err(new CrossCompanyWorkCenterError(op.workCenterId));
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// 6. Create routing in DRAFT status
|
|
91
|
+
const routing = await db
|
|
92
|
+
.insertInto("Routing")
|
|
93
|
+
.values({
|
|
94
|
+
...(customFields as Record<string, unknown>),
|
|
95
|
+
parentItemId,
|
|
96
|
+
companyId,
|
|
97
|
+
siteId: siteId ?? null,
|
|
98
|
+
revisionNumber: revisionNumber ?? null,
|
|
99
|
+
status: "DRAFT",
|
|
100
|
+
createdAt: new Date(),
|
|
101
|
+
updatedAt: null,
|
|
102
|
+
})
|
|
103
|
+
.returningAll()
|
|
104
|
+
.executeTakeFirst();
|
|
105
|
+
|
|
106
|
+
// 7. Create routing operations
|
|
107
|
+
const createdOperations = [];
|
|
108
|
+
for (const op of operations) {
|
|
109
|
+
const operation = await db
|
|
110
|
+
.insertInto("RoutingOperation")
|
|
111
|
+
.values({
|
|
112
|
+
routingId: routing!.id,
|
|
113
|
+
sequenceNumber: op.sequenceNumber,
|
|
114
|
+
operationDescription: op.operationDescription ?? null,
|
|
115
|
+
workCenterId: op.workCenterId,
|
|
116
|
+
standardSetupTime: op.standardSetupTime,
|
|
117
|
+
standardRunTime: op.standardRunTime,
|
|
118
|
+
operatorInstructions: op.operatorInstructions ?? null,
|
|
119
|
+
createdAt: new Date(),
|
|
120
|
+
updatedAt: null,
|
|
121
|
+
})
|
|
122
|
+
.returningAll()
|
|
123
|
+
.executeTakeFirst();
|
|
124
|
+
createdOperations.push(operation!);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return ok({ routing: routing!, operations: createdOperations });
|
|
128
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
// @generated — do not edit
|
|
2
|
+
import { permissions } from "../lib/permissions.generated";
|
|
3
|
+
import { run } from "./createWorkCenter";
|
|
4
|
+
import { defineCommand } from "@tailor-platform/erp-kit/module";
|
|
5
|
+
|
|
6
|
+
export const createWorkCenter = defineCommand(permissions.createWorkCenter, run);
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { createMockDb } from "../../../testing/index";
|
|
3
|
+
import type { Transaction } from "../generated/kysely-tailordb";
|
|
4
|
+
import {
|
|
5
|
+
InvalidScopeError,
|
|
6
|
+
DuplicateWorkCenterCodeError,
|
|
7
|
+
InvalidCapacityError,
|
|
8
|
+
InvalidRateError,
|
|
9
|
+
InvalidOverheadMethodError,
|
|
10
|
+
OverheadCurrencyRequiredError,
|
|
11
|
+
} from "../lib/errors.generated";
|
|
12
|
+
import { baseDraftWorkCenter } from "../testing/fixtures";
|
|
13
|
+
import { run } from "./createWorkCenter";
|
|
14
|
+
import type { CommandContext } from "@tailor-platform/erp-kit/module";
|
|
15
|
+
|
|
16
|
+
describe("createWorkCenter", () => {
|
|
17
|
+
const ctx: CommandContext = {
|
|
18
|
+
actorId: "test-actor",
|
|
19
|
+
permissions: ["manufacturing:createWorkCenter"],
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
const validInput = {
|
|
23
|
+
code: "WC-NEW",
|
|
24
|
+
companyId: "company-1",
|
|
25
|
+
siteId: "site-1",
|
|
26
|
+
capacityAssumptions: 100,
|
|
27
|
+
laborRate: 25.0,
|
|
28
|
+
machineRate: 50.0,
|
|
29
|
+
calendarReference: "cal-standard",
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
it("creates a draft work center with positive capacity", async () => {
|
|
33
|
+
const { db, spies } = createMockDb<Transaction>();
|
|
34
|
+
const createdWorkCenter = {
|
|
35
|
+
...baseDraftWorkCenter,
|
|
36
|
+
id: "new-wc-id",
|
|
37
|
+
code: "WC-NEW",
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
spies.select.mockReturnValueOnce(undefined);
|
|
41
|
+
spies.insert.mockReturnValue(createdWorkCenter);
|
|
42
|
+
|
|
43
|
+
const result = await run(db, validInput, ctx);
|
|
44
|
+
|
|
45
|
+
expect(result.ok).toBe(true);
|
|
46
|
+
if (result.ok) {
|
|
47
|
+
expect(result.value.workCenter.status).toBe("DRAFT");
|
|
48
|
+
expect(result.value.workCenter.code).toBe("WC-NEW");
|
|
49
|
+
}
|
|
50
|
+
expect(spies.insert).toHaveBeenCalled();
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it("returns error when scope is missing", async () => {
|
|
54
|
+
const { db } = createMockDb<Transaction>();
|
|
55
|
+
|
|
56
|
+
const result = await run(db, { ...validInput, companyId: "" }, ctx);
|
|
57
|
+
|
|
58
|
+
expect(result.ok).toBe(false);
|
|
59
|
+
if (!result.ok) {
|
|
60
|
+
expect(result.error).toBeInstanceOf(InvalidScopeError);
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it("creates a draft work center without siteId when not required", async () => {
|
|
65
|
+
const { db, spies } = createMockDb<Transaction>();
|
|
66
|
+
const createdWorkCenter = {
|
|
67
|
+
...baseDraftWorkCenter,
|
|
68
|
+
id: "new-wc-id",
|
|
69
|
+
code: "WC-NEW",
|
|
70
|
+
siteId: null,
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
spies.select.mockReturnValueOnce(undefined);
|
|
74
|
+
spies.insert.mockReturnValue(createdWorkCenter);
|
|
75
|
+
|
|
76
|
+
const result = await run(db, { ...validInput, siteId: null }, ctx);
|
|
77
|
+
|
|
78
|
+
expect(result.ok).toBe(true);
|
|
79
|
+
if (result.ok) {
|
|
80
|
+
expect(result.value.workCenter.status).toBe("DRAFT");
|
|
81
|
+
}
|
|
82
|
+
expect(spies.insert).toHaveBeenCalled();
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it("returns error when the code already exists in scope", async () => {
|
|
86
|
+
const { db, spies } = createMockDb<Transaction>();
|
|
87
|
+
spies.select.mockReturnValueOnce(baseDraftWorkCenter);
|
|
88
|
+
|
|
89
|
+
const result = await run(db, validInput, ctx);
|
|
90
|
+
|
|
91
|
+
expect(result.ok).toBe(false);
|
|
92
|
+
if (!result.ok) {
|
|
93
|
+
expect(result.error).toBeInstanceOf(DuplicateWorkCenterCodeError);
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it("returns error when capacity is not positive", async () => {
|
|
98
|
+
const { db } = createMockDb<Transaction>();
|
|
99
|
+
|
|
100
|
+
const result = await run(db, { ...validInput, capacityAssumptions: 0 }, ctx);
|
|
101
|
+
|
|
102
|
+
expect(result.ok).toBe(false);
|
|
103
|
+
if (!result.ok) {
|
|
104
|
+
expect(result.error).toBeInstanceOf(InvalidCapacityError);
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it("returns error when a rate is negative", async () => {
|
|
109
|
+
const { db } = createMockDb<Transaction>();
|
|
110
|
+
|
|
111
|
+
const result = await run(db, { ...validInput, laborRate: -1 }, ctx);
|
|
112
|
+
|
|
113
|
+
expect(result.ok).toBe(false);
|
|
114
|
+
if (!result.ok) {
|
|
115
|
+
expect(result.error).toBeInstanceOf(InvalidRateError);
|
|
116
|
+
}
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it("returns error when overhead method is invalid", async () => {
|
|
120
|
+
const { db } = createMockDb<Transaction>();
|
|
121
|
+
|
|
122
|
+
const result = await run(db, { ...validInput, overheadAbsorptionMethod: "UNSUPPORTED" }, ctx);
|
|
123
|
+
|
|
124
|
+
expect(result.ok).toBe(false);
|
|
125
|
+
if (!result.ok) {
|
|
126
|
+
expect(result.error).toBeInstanceOf(InvalidOverheadMethodError);
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it("returns error when fixed-amount overhead omits currency", async () => {
|
|
131
|
+
const { db } = createMockDb<Transaction>();
|
|
132
|
+
|
|
133
|
+
const result = await run(
|
|
134
|
+
db,
|
|
135
|
+
{
|
|
136
|
+
...validInput,
|
|
137
|
+
overheadAbsorptionMethod: "FIXED_AMOUNT_PER_GOOD_UNIT",
|
|
138
|
+
overheadAbsorptionCurrency: null,
|
|
139
|
+
},
|
|
140
|
+
ctx,
|
|
141
|
+
);
|
|
142
|
+
|
|
143
|
+
expect(result.ok).toBe(false);
|
|
144
|
+
if (!result.ok) {
|
|
145
|
+
expect(result.error).toBeInstanceOf(OverheadCurrencyRequiredError);
|
|
146
|
+
}
|
|
147
|
+
});
|
|
148
|
+
});
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import type { Transaction } from "../generated/kysely-tailordb";
|
|
2
|
+
import {
|
|
3
|
+
InvalidScopeError,
|
|
4
|
+
DuplicateWorkCenterCodeError,
|
|
5
|
+
InvalidCapacityError,
|
|
6
|
+
InvalidRateError,
|
|
7
|
+
InvalidOverheadMethodError,
|
|
8
|
+
OverheadCurrencyRequiredError,
|
|
9
|
+
} from "../lib/errors.generated";
|
|
10
|
+
import { ok, err, type CommandContext } from "@tailor-platform/erp-kit/module";
|
|
11
|
+
|
|
12
|
+
const VALID_OVERHEAD_METHODS = [
|
|
13
|
+
"PERCENT_OF_LABOR_COST",
|
|
14
|
+
"PERCENT_OF_MACHINE_COST",
|
|
15
|
+
"FIXED_AMOUNT_PER_GOOD_UNIT",
|
|
16
|
+
] as const;
|
|
17
|
+
|
|
18
|
+
export interface CreateWorkCenterInput {
|
|
19
|
+
code: string;
|
|
20
|
+
companyId: string;
|
|
21
|
+
siteId?: string | null;
|
|
22
|
+
capacityAssumptions: number;
|
|
23
|
+
laborRate?: number | null;
|
|
24
|
+
machineRate?: number | null;
|
|
25
|
+
calendarReference?: string | null;
|
|
26
|
+
overheadAbsorptionMethod?: string | null;
|
|
27
|
+
overheadAbsorptionCurrency?: string | null;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Function: createWorkCenter
|
|
32
|
+
*
|
|
33
|
+
* Creates a new work center in DRAFT status with scope, capacity assumptions,
|
|
34
|
+
* rate context, and optional overhead-absorption policy.
|
|
35
|
+
*/
|
|
36
|
+
export async function run<CF extends Record<string, unknown>>(
|
|
37
|
+
db: Transaction,
|
|
38
|
+
input: CreateWorkCenterInput & CF,
|
|
39
|
+
_ctx: CommandContext,
|
|
40
|
+
) {
|
|
41
|
+
const {
|
|
42
|
+
code,
|
|
43
|
+
companyId,
|
|
44
|
+
siteId,
|
|
45
|
+
capacityAssumptions,
|
|
46
|
+
laborRate,
|
|
47
|
+
machineRate,
|
|
48
|
+
calendarReference,
|
|
49
|
+
overheadAbsorptionMethod,
|
|
50
|
+
overheadAbsorptionCurrency,
|
|
51
|
+
...customFields
|
|
52
|
+
} = input;
|
|
53
|
+
|
|
54
|
+
// 1. Validate scope — company is always required
|
|
55
|
+
if (!companyId) {
|
|
56
|
+
return err(new InvalidScopeError(code));
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// 2. Validate capacity > 0
|
|
60
|
+
if (capacityAssumptions <= 0) {
|
|
61
|
+
return err(new InvalidCapacityError(code));
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// 3. Validate rates >= 0
|
|
65
|
+
if (laborRate != null && laborRate < 0) {
|
|
66
|
+
return err(new InvalidRateError(code));
|
|
67
|
+
}
|
|
68
|
+
if (machineRate != null && machineRate < 0) {
|
|
69
|
+
return err(new InvalidRateError(code));
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// 4. Validate overhead method
|
|
73
|
+
if (
|
|
74
|
+
overheadAbsorptionMethod != null &&
|
|
75
|
+
!VALID_OVERHEAD_METHODS.includes(
|
|
76
|
+
overheadAbsorptionMethod as (typeof VALID_OVERHEAD_METHODS)[number],
|
|
77
|
+
)
|
|
78
|
+
) {
|
|
79
|
+
return err(new InvalidOverheadMethodError(code));
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// 5. Overhead currency required for FIXED_AMOUNT_PER_GOOD_UNIT
|
|
83
|
+
if (overheadAbsorptionMethod === "FIXED_AMOUNT_PER_GOOD_UNIT" && !overheadAbsorptionCurrency) {
|
|
84
|
+
return err(new OverheadCurrencyRequiredError(code));
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// 6. Check code uniqueness within company+site scope
|
|
88
|
+
let existingQuery = db
|
|
89
|
+
.selectFrom("WorkCenter")
|
|
90
|
+
.selectAll()
|
|
91
|
+
.where("code", "=", code)
|
|
92
|
+
.where("companyId", "=", companyId);
|
|
93
|
+
|
|
94
|
+
if (siteId) {
|
|
95
|
+
existingQuery = existingQuery.where("siteId", "=", siteId);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const existing = await existingQuery.forUpdate().executeTakeFirst();
|
|
99
|
+
|
|
100
|
+
if (existing) {
|
|
101
|
+
return err(new DuplicateWorkCenterCodeError(code));
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// 7. Create work center in DRAFT status
|
|
105
|
+
const workCenter = await db
|
|
106
|
+
.insertInto("WorkCenter")
|
|
107
|
+
.values({
|
|
108
|
+
...(customFields as Record<string, unknown>),
|
|
109
|
+
code,
|
|
110
|
+
companyId,
|
|
111
|
+
siteId: siteId ?? null,
|
|
112
|
+
capacityAssumptions,
|
|
113
|
+
laborRate: laborRate ?? null,
|
|
114
|
+
machineRate: machineRate ?? null,
|
|
115
|
+
calendarReference: calendarReference ?? null,
|
|
116
|
+
overheadAbsorptionMethod:
|
|
117
|
+
(overheadAbsorptionMethod as
|
|
118
|
+
| "PERCENT_OF_LABOR_COST"
|
|
119
|
+
| "PERCENT_OF_MACHINE_COST"
|
|
120
|
+
| "FIXED_AMOUNT_PER_GOOD_UNIT"
|
|
121
|
+
| null) ?? null,
|
|
122
|
+
overheadAbsorptionCurrency: overheadAbsorptionCurrency ?? null,
|
|
123
|
+
status: "DRAFT",
|
|
124
|
+
createdAt: new Date(),
|
|
125
|
+
updatedAt: null,
|
|
126
|
+
})
|
|
127
|
+
.returningAll()
|
|
128
|
+
.executeTakeFirst();
|
|
129
|
+
|
|
130
|
+
return ok({ workCenter: workCenter! });
|
|
131
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
// @generated — do not edit
|
|
2
|
+
import { permissions } from "../lib/permissions.generated";
|
|
3
|
+
import { run } from "./deactivateBillOfMaterial";
|
|
4
|
+
import { defineCommand } from "@tailor-platform/erp-kit/module";
|
|
5
|
+
|
|
6
|
+
export const deactivateBillOfMaterial = defineCommand(permissions.deactivateBillOfMaterial, run);
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { createMockDb } from "../../../testing/index";
|
|
3
|
+
import type { Transaction } from "../generated/kysely-tailordb";
|
|
4
|
+
import {
|
|
5
|
+
BomNotFoundError,
|
|
6
|
+
BomNotDeactivatableError,
|
|
7
|
+
ReplacementRequiredError,
|
|
8
|
+
} from "../lib/errors.generated";
|
|
9
|
+
import { baseDraftBom, baseActiveBom } from "../testing/fixtures";
|
|
10
|
+
import { run } from "./deactivateBillOfMaterial";
|
|
11
|
+
import type { CommandContext } from "@tailor-platform/erp-kit/module";
|
|
12
|
+
|
|
13
|
+
describe("deactivateBillOfMaterial", () => {
|
|
14
|
+
const ctx: CommandContext = {
|
|
15
|
+
actorId: "test-actor",
|
|
16
|
+
permissions: ["manufacturing:deactivateBillOfMaterial"],
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
it("deactivates an active BOM", async () => {
|
|
20
|
+
const { db, spies } = createMockDb<Transaction>();
|
|
21
|
+
const deactivatedBom = { ...baseActiveBom, status: "INACTIVE" as const };
|
|
22
|
+
|
|
23
|
+
// BOM exists and is ACTIVE, with defaultSelection = true
|
|
24
|
+
spies.select.mockReturnValueOnce(baseActiveBom);
|
|
25
|
+
// replacement BOM exists
|
|
26
|
+
spies.select.mockReturnValueOnce({ ...baseActiveBom, id: "bom-replacement" });
|
|
27
|
+
// update to INACTIVE
|
|
28
|
+
spies.update.mockReturnValue(deactivatedBom);
|
|
29
|
+
|
|
30
|
+
const result = await run(db, { id: "bom-2" }, ctx);
|
|
31
|
+
|
|
32
|
+
expect(result.ok).toBe(true);
|
|
33
|
+
if (result.ok) {
|
|
34
|
+
expect(result.value.billOfMaterial.status).toBe("INACTIVE");
|
|
35
|
+
}
|
|
36
|
+
expect(spies.update).toHaveBeenCalled();
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it("returns error when the BOM does not exist", async () => {
|
|
40
|
+
const { db, spies } = createMockDb<Transaction>();
|
|
41
|
+
|
|
42
|
+
spies.select.mockReturnValueOnce(undefined);
|
|
43
|
+
|
|
44
|
+
const result = await run(db, { id: "bom-nonexistent" }, ctx);
|
|
45
|
+
|
|
46
|
+
expect(result.ok).toBe(false);
|
|
47
|
+
if (!result.ok) {
|
|
48
|
+
expect(result.error).toBeInstanceOf(BomNotFoundError);
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it("returns error when the BOM is not active", async () => {
|
|
53
|
+
const { db, spies } = createMockDb<Transaction>();
|
|
54
|
+
|
|
55
|
+
spies.select.mockReturnValueOnce(baseDraftBom);
|
|
56
|
+
|
|
57
|
+
const result = await run(db, { id: "bom-1" }, ctx);
|
|
58
|
+
|
|
59
|
+
expect(result.ok).toBe(false);
|
|
60
|
+
if (!result.ok) {
|
|
61
|
+
expect(result.error).toBeInstanceOf(BomNotDeactivatableError);
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it("returns error when replacement policy blocks deactivation", async () => {
|
|
66
|
+
const { db, spies } = createMockDb<Transaction>();
|
|
67
|
+
|
|
68
|
+
// BOM exists and is ACTIVE with default selection
|
|
69
|
+
spies.select.mockReturnValueOnce(baseActiveBom);
|
|
70
|
+
// no replacement BOM
|
|
71
|
+
spies.select.mockReturnValueOnce(undefined);
|
|
72
|
+
|
|
73
|
+
const result = await run(db, { id: "bom-2" }, ctx);
|
|
74
|
+
|
|
75
|
+
expect(result.ok).toBe(false);
|
|
76
|
+
if (!result.ok) {
|
|
77
|
+
expect(result.error).toBeInstanceOf(ReplacementRequiredError);
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it("preserves released production-order snapshots after deactivation", async () => {
|
|
82
|
+
const { db, spies } = createMockDb<Transaction>();
|
|
83
|
+
const deactivatedBom = { ...baseActiveBom, status: "INACTIVE" as const };
|
|
84
|
+
|
|
85
|
+
// BOM exists and is ACTIVE with default selection
|
|
86
|
+
spies.select.mockReturnValueOnce(baseActiveBom);
|
|
87
|
+
// replacement BOM exists
|
|
88
|
+
spies.select.mockReturnValueOnce({ ...baseActiveBom, id: "bom-replacement" });
|
|
89
|
+
// update to INACTIVE
|
|
90
|
+
spies.update.mockReturnValue(deactivatedBom);
|
|
91
|
+
|
|
92
|
+
const result = await run(db, { id: "bom-2" }, ctx);
|
|
93
|
+
|
|
94
|
+
// Verify that the command only updates the BOM status —
|
|
95
|
+
// it does not touch ProductionOrderBomSnapshot at all
|
|
96
|
+
expect(result.ok).toBe(true);
|
|
97
|
+
if (result.ok) {
|
|
98
|
+
expect(result.value.billOfMaterial.status).toBe("INACTIVE");
|
|
99
|
+
}
|
|
100
|
+
// The update should only have been called once (for BOM status)
|
|
101
|
+
expect(spies.update).toHaveBeenCalledTimes(1);
|
|
102
|
+
});
|
|
103
|
+
});
|