@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,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
|
+
});
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import type { Transaction } from "../generated/kysely-tailordb";
|
|
2
|
+
import {
|
|
3
|
+
BomNotFoundError,
|
|
4
|
+
BomNotDeactivatableError,
|
|
5
|
+
ReplacementRequiredError,
|
|
6
|
+
} from "../lib/errors.generated";
|
|
7
|
+
import { ok, err, type CommandContext } from "@tailor-platform/erp-kit/module";
|
|
8
|
+
|
|
9
|
+
export interface DeactivateBillOfMaterialInput {
|
|
10
|
+
id: string;
|
|
11
|
+
from?: string[];
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Function: deactivateBillOfMaterial
|
|
16
|
+
*
|
|
17
|
+
* Removes an active BOM version from future selection. Preserves audit
|
|
18
|
+
* history and any released production-order snapshots that already depend
|
|
19
|
+
* on the version.
|
|
20
|
+
*/
|
|
21
|
+
export async function run<CF extends Record<string, unknown>>(
|
|
22
|
+
db: Transaction,
|
|
23
|
+
input: DeactivateBillOfMaterialInput & CF,
|
|
24
|
+
_ctx: CommandContext,
|
|
25
|
+
) {
|
|
26
|
+
const { id, from, ...customFields } = input;
|
|
27
|
+
void customFields;
|
|
28
|
+
|
|
29
|
+
const allowedStatuses = from ?? ["ACTIVE"];
|
|
30
|
+
|
|
31
|
+
// 1. Fetch BOM with lock
|
|
32
|
+
const bom = await db
|
|
33
|
+
.selectFrom("BillOfMaterial")
|
|
34
|
+
.selectAll()
|
|
35
|
+
.where("id", "=", id)
|
|
36
|
+
.forUpdate()
|
|
37
|
+
.executeTakeFirst();
|
|
38
|
+
|
|
39
|
+
if (!bom) {
|
|
40
|
+
return err(new BomNotFoundError(id));
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// 2. Validate status is deactivatable
|
|
44
|
+
if (!allowedStatuses.includes(bom.status)) {
|
|
45
|
+
return err(new BomNotDeactivatableError(id));
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// 3. Check replacement policy — if this is the only active BOM for this
|
|
49
|
+
// parent item in scope and it is the default selection, a replacement
|
|
50
|
+
// active version must exist before deactivation
|
|
51
|
+
if (bom.defaultSelection) {
|
|
52
|
+
const replacementBom = await db
|
|
53
|
+
.selectFrom("BillOfMaterial")
|
|
54
|
+
.selectAll()
|
|
55
|
+
.where("parentItemId", "=", bom.parentItemId)
|
|
56
|
+
.where("companyId", "=", bom.companyId)
|
|
57
|
+
.where("id", "!=", id)
|
|
58
|
+
.where("status", "=", "ACTIVE")
|
|
59
|
+
.executeTakeFirst();
|
|
60
|
+
|
|
61
|
+
if (!replacementBom) {
|
|
62
|
+
return err(new ReplacementRequiredError(id));
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// 4. Set status to INACTIVE
|
|
67
|
+
const updatedBom = await db
|
|
68
|
+
.updateTable("BillOfMaterial")
|
|
69
|
+
.set({
|
|
70
|
+
status: "INACTIVE",
|
|
71
|
+
updatedAt: new Date(),
|
|
72
|
+
})
|
|
73
|
+
.where("id", "=", id)
|
|
74
|
+
.returningAll()
|
|
75
|
+
.executeTakeFirst();
|
|
76
|
+
|
|
77
|
+
return ok({ billOfMaterial: updatedBom! });
|
|
78
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
// @generated — do not edit
|
|
2
|
+
import { permissions } from "../lib/permissions.generated";
|
|
3
|
+
import { run } from "./deactivateRouting";
|
|
4
|
+
import { defineCommand } from "@tailor-platform/erp-kit/module";
|
|
5
|
+
|
|
6
|
+
export const deactivateRouting = defineCommand(permissions.deactivateRouting, run);
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { createMockDb } from "../../../testing/index";
|
|
3
|
+
import type { Transaction } from "../generated/kysely-tailordb";
|
|
4
|
+
import {
|
|
5
|
+
RoutingNotFoundError,
|
|
6
|
+
RoutingNotDeactivatableError,
|
|
7
|
+
ReplacementRequiredError,
|
|
8
|
+
} from "../lib/errors.generated";
|
|
9
|
+
import {
|
|
10
|
+
baseActiveRouting,
|
|
11
|
+
baseDraftRouting,
|
|
12
|
+
baseReleasedProductionOrder,
|
|
13
|
+
} from "../testing/fixtures";
|
|
14
|
+
import { run } from "./deactivateRouting";
|
|
15
|
+
import type { CommandContext } from "@tailor-platform/erp-kit/module";
|
|
16
|
+
|
|
17
|
+
describe("deactivateRouting", () => {
|
|
18
|
+
const ctx: CommandContext = {
|
|
19
|
+
actorId: "test-actor",
|
|
20
|
+
permissions: ["manufacturing:deactivateRouting"],
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const validInput = { id: "routing-2" };
|
|
24
|
+
|
|
25
|
+
it("deactivates an active routing", async () => {
|
|
26
|
+
const { db, spies } = createMockDb<Transaction>();
|
|
27
|
+
|
|
28
|
+
// select: Routing lookup
|
|
29
|
+
spies.select.mockReturnValueOnce(baseActiveRouting);
|
|
30
|
+
// select: Replacement routing lookup - another active routing exists
|
|
31
|
+
spies.select.mockReturnValueOnce({ ...baseActiveRouting, id: "routing-4" });
|
|
32
|
+
// update: status to INACTIVE
|
|
33
|
+
spies.update.mockReturnValue({ ...baseActiveRouting, status: "INACTIVE" });
|
|
34
|
+
|
|
35
|
+
const result = await run(db, validInput, ctx);
|
|
36
|
+
|
|
37
|
+
expect(result.ok).toBe(true);
|
|
38
|
+
if (result.ok) {
|
|
39
|
+
expect(result.value.routing.status).toBe("INACTIVE");
|
|
40
|
+
}
|
|
41
|
+
expect(spies.update).toHaveBeenCalled();
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it("returns error when the routing does not exist", async () => {
|
|
45
|
+
const { db, spies } = createMockDb<Transaction>();
|
|
46
|
+
|
|
47
|
+
// select: Routing lookup - not found
|
|
48
|
+
spies.select.mockReturnValueOnce(undefined);
|
|
49
|
+
|
|
50
|
+
const result = await run(db, { id: "nonexistent" }, ctx);
|
|
51
|
+
|
|
52
|
+
expect(result.ok).toBe(false);
|
|
53
|
+
if (!result.ok) {
|
|
54
|
+
expect(result.error).toBeInstanceOf(RoutingNotFoundError);
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it("returns error when the routing is not active", async () => {
|
|
59
|
+
const { db, spies } = createMockDb<Transaction>();
|
|
60
|
+
|
|
61
|
+
// select: Routing lookup - DRAFT
|
|
62
|
+
spies.select.mockReturnValueOnce(baseDraftRouting);
|
|
63
|
+
|
|
64
|
+
const result = await run(db, { id: "routing-1" }, ctx);
|
|
65
|
+
|
|
66
|
+
expect(result.ok).toBe(false);
|
|
67
|
+
if (!result.ok) {
|
|
68
|
+
expect(result.error).toBeInstanceOf(RoutingNotDeactivatableError);
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it("returns error when replacement policy blocks deactivation", async () => {
|
|
73
|
+
const { db, spies } = createMockDb<Transaction>();
|
|
74
|
+
|
|
75
|
+
// select: Routing lookup
|
|
76
|
+
spies.select.mockReturnValueOnce(baseActiveRouting);
|
|
77
|
+
// select: Replacement routing lookup - no replacement found
|
|
78
|
+
spies.select.mockReturnValueOnce(undefined);
|
|
79
|
+
// select: Dependent production order - exists in RELEASED
|
|
80
|
+
spies.select.mockReturnValueOnce({
|
|
81
|
+
...baseReleasedProductionOrder,
|
|
82
|
+
selectedRoutingRevisionId: "routing-2",
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
const result = await run(db, validInput, ctx);
|
|
86
|
+
|
|
87
|
+
expect(result.ok).toBe(false);
|
|
88
|
+
if (!result.ok) {
|
|
89
|
+
expect(result.error).toBeInstanceOf(ReplacementRequiredError);
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it("preserves released operation snapshots after deactivation", async () => {
|
|
94
|
+
const { db, spies } = createMockDb<Transaction>();
|
|
95
|
+
|
|
96
|
+
// select: Routing lookup
|
|
97
|
+
spies.select.mockReturnValueOnce(baseActiveRouting);
|
|
98
|
+
// select: Replacement routing exists
|
|
99
|
+
spies.select.mockReturnValueOnce({ ...baseActiveRouting, id: "routing-4" });
|
|
100
|
+
// update: status to INACTIVE
|
|
101
|
+
spies.update.mockReturnValue({ ...baseActiveRouting, status: "INACTIVE" });
|
|
102
|
+
|
|
103
|
+
const result = await run(db, validInput, ctx);
|
|
104
|
+
|
|
105
|
+
// Deactivation only updates the Routing status,
|
|
106
|
+
// not ProductionOrderRoutingSnapshot, preserving frozen snapshots.
|
|
107
|
+
expect(result.ok).toBe(true);
|
|
108
|
+
if (result.ok) {
|
|
109
|
+
expect(result.value.routing.status).toBe("INACTIVE");
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
});
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import type { Transaction } from "../generated/kysely-tailordb";
|
|
2
|
+
import {
|
|
3
|
+
RoutingNotFoundError,
|
|
4
|
+
RoutingNotDeactivatableError,
|
|
5
|
+
ReplacementRequiredError,
|
|
6
|
+
} from "../lib/errors.generated";
|
|
7
|
+
import { ok, err, type CommandContext } from "@tailor-platform/erp-kit/module";
|
|
8
|
+
|
|
9
|
+
export interface DeactivateRoutingInput {
|
|
10
|
+
id: string;
|
|
11
|
+
from?: string[];
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Function: deactivateRouting
|
|
16
|
+
*
|
|
17
|
+
* Removes an active routing from future production-order selection.
|
|
18
|
+
* Released production orders keep their frozen operation plan.
|
|
19
|
+
*/
|
|
20
|
+
export async function run(db: Transaction, input: DeactivateRoutingInput, _ctx: CommandContext) {
|
|
21
|
+
const { id, from = ["ACTIVE"] } = input;
|
|
22
|
+
|
|
23
|
+
// 1. Fetch routing
|
|
24
|
+
const routing = await db
|
|
25
|
+
.selectFrom("Routing")
|
|
26
|
+
.selectAll()
|
|
27
|
+
.where("id", "=", id)
|
|
28
|
+
.forUpdate()
|
|
29
|
+
.executeTakeFirst();
|
|
30
|
+
|
|
31
|
+
if (!routing) {
|
|
32
|
+
return err(new RoutingNotFoundError(id));
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// 2. Verify allowed source status
|
|
36
|
+
if (!from.includes(routing.status)) {
|
|
37
|
+
return err(new RoutingNotDeactivatableError(id));
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// 3. Replacement policy: check if another active routing exists for same item+company
|
|
41
|
+
const replacement = await db
|
|
42
|
+
.selectFrom("Routing")
|
|
43
|
+
.selectAll()
|
|
44
|
+
.where("parentItemId", "=", routing.parentItemId)
|
|
45
|
+
.where("companyId", "=", routing.companyId)
|
|
46
|
+
.where("status", "=", "ACTIVE")
|
|
47
|
+
.where("id", "!=", id)
|
|
48
|
+
.executeTakeFirst();
|
|
49
|
+
|
|
50
|
+
if (!replacement) {
|
|
51
|
+
// Check if any production orders reference this routing in released/in-progress state
|
|
52
|
+
const dependentOrder = await db
|
|
53
|
+
.selectFrom("ProductionOrder")
|
|
54
|
+
.selectAll()
|
|
55
|
+
.where("selectedRoutingRevisionId", "=", id)
|
|
56
|
+
.where("status", "in", ["RELEASED", "IN_PROGRESS"])
|
|
57
|
+
.executeTakeFirst();
|
|
58
|
+
|
|
59
|
+
if (dependentOrder) {
|
|
60
|
+
return err(new ReplacementRequiredError(id));
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// 4. Set status to INACTIVE
|
|
65
|
+
const updatedRouting = await db
|
|
66
|
+
.updateTable("Routing")
|
|
67
|
+
.set({
|
|
68
|
+
status: "INACTIVE",
|
|
69
|
+
updatedAt: new Date(),
|
|
70
|
+
})
|
|
71
|
+
.where("id", "=", id)
|
|
72
|
+
.returningAll()
|
|
73
|
+
.executeTakeFirst();
|
|
74
|
+
|
|
75
|
+
return ok({ routing: updatedRouting! });
|
|
76
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
// @generated — do not edit
|
|
2
|
+
import { permissions } from "../lib/permissions.generated";
|
|
3
|
+
import { run } from "./deactivateWorkCenter";
|
|
4
|
+
import { defineCommand } from "@tailor-platform/erp-kit/module";
|
|
5
|
+
|
|
6
|
+
export const deactivateWorkCenter = defineCommand(permissions.deactivateWorkCenter, run);
|