@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,210 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { createMockDb } from "../../../testing/index";
|
|
3
|
+
import type { Transaction } from "../generated/kysely-tailordb";
|
|
4
|
+
import {
|
|
5
|
+
ParentItemNotFoundError,
|
|
6
|
+
ParentItemNotManufacturedError,
|
|
7
|
+
InvalidBomTypeError,
|
|
8
|
+
ComponentLineRequiredError,
|
|
9
|
+
InvalidComponentQuantityError,
|
|
10
|
+
ComponentItemInactiveError,
|
|
11
|
+
BomVersionConflictError,
|
|
12
|
+
} from "../lib/errors.generated";
|
|
13
|
+
import { baseDraftBom, baseBomLine } from "../testing/fixtures";
|
|
14
|
+
import { run } from "./createBillOfMaterial";
|
|
15
|
+
import type { CommandContext } from "@tailor-platform/erp-kit/module";
|
|
16
|
+
|
|
17
|
+
describe("createBillOfMaterial", () => {
|
|
18
|
+
const ctx: CommandContext = {
|
|
19
|
+
actorId: "test-actor",
|
|
20
|
+
permissions: ["manufacturing:createBillOfMaterial"],
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const validInput = {
|
|
24
|
+
parentItemId: "item-1",
|
|
25
|
+
companyId: "company-1",
|
|
26
|
+
siteId: null,
|
|
27
|
+
bomType: "MANUFACTURE",
|
|
28
|
+
effectivityStartDate: null,
|
|
29
|
+
effectivityEndDate: null,
|
|
30
|
+
defaultSelection: true,
|
|
31
|
+
revisionNumber: "V1",
|
|
32
|
+
lines: [
|
|
33
|
+
{
|
|
34
|
+
itemId: "item-2",
|
|
35
|
+
requiredQuantity: 2.0,
|
|
36
|
+
unitOfMeasure: "EA",
|
|
37
|
+
scrapAssumption: 5.0,
|
|
38
|
+
isSubassembly: false,
|
|
39
|
+
},
|
|
40
|
+
],
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
it("creates a draft BOM for an active manufactured item", async () => {
|
|
44
|
+
const { db, spies } = createMockDb<Transaction>();
|
|
45
|
+
const createdBom = {
|
|
46
|
+
...baseDraftBom,
|
|
47
|
+
id: "new-bom-id",
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
// parent item exists
|
|
51
|
+
spies.select.mockReturnValueOnce({ id: "item-1", status: "ACTIVE", isManufacturable: true });
|
|
52
|
+
// component item exists
|
|
53
|
+
spies.select.mockReturnValueOnce({ id: "item-2", status: "ACTIVE" });
|
|
54
|
+
// no existing BOM with same version identity
|
|
55
|
+
spies.select.mockReturnValueOnce(undefined);
|
|
56
|
+
// insert BOM
|
|
57
|
+
spies.insert.mockReturnValueOnce(createdBom);
|
|
58
|
+
// insert BOM line
|
|
59
|
+
spies.insert.mockReturnValueOnce({ ...baseBomLine, billOfMaterialId: "new-bom-id" });
|
|
60
|
+
|
|
61
|
+
const result = await run(db, validInput, ctx);
|
|
62
|
+
|
|
63
|
+
expect(result.ok).toBe(true);
|
|
64
|
+
if (result.ok) {
|
|
65
|
+
expect(result.value.billOfMaterial.status).toBe("DRAFT");
|
|
66
|
+
expect(result.value.billOfMaterial.id).toBe("new-bom-id");
|
|
67
|
+
}
|
|
68
|
+
expect(spies.insert).toHaveBeenCalled();
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it("returns error when the parent item does not exist", async () => {
|
|
72
|
+
const { db, spies } = createMockDb<Transaction>();
|
|
73
|
+
|
|
74
|
+
// parent item not found
|
|
75
|
+
spies.select.mockReturnValueOnce(undefined);
|
|
76
|
+
|
|
77
|
+
const result = await run(db, validInput, ctx);
|
|
78
|
+
|
|
79
|
+
expect(result.ok).toBe(false);
|
|
80
|
+
if (!result.ok) {
|
|
81
|
+
expect(result.error).toBeInstanceOf(ParentItemNotFoundError);
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it("returns error when the parent item is not manufacturable", async () => {
|
|
86
|
+
const { db, spies } = createMockDb<Transaction>();
|
|
87
|
+
|
|
88
|
+
// parent item exists but not manufacturable
|
|
89
|
+
spies.select.mockReturnValueOnce({ id: "item-1", status: "ACTIVE", isManufacturable: false });
|
|
90
|
+
|
|
91
|
+
const result = await run(db, validInput, ctx);
|
|
92
|
+
|
|
93
|
+
expect(result.ok).toBe(false);
|
|
94
|
+
if (!result.ok) {
|
|
95
|
+
expect(result.error).toBeInstanceOf(ParentItemNotManufacturedError);
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it("returns error when bomType is invalid", async () => {
|
|
100
|
+
const { db, spies } = createMockDb<Transaction>();
|
|
101
|
+
|
|
102
|
+
// parent item exists
|
|
103
|
+
spies.select.mockReturnValueOnce({ id: "item-1", status: "ACTIVE", isManufacturable: true });
|
|
104
|
+
|
|
105
|
+
const result = await run(db, { ...validInput, bomType: "UNSUPPORTED" }, ctx);
|
|
106
|
+
|
|
107
|
+
expect(result.ok).toBe(false);
|
|
108
|
+
if (!result.ok) {
|
|
109
|
+
expect(result.error).toBeInstanceOf(InvalidBomTypeError);
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it("returns error when no component lines are provided", async () => {
|
|
114
|
+
const { db, spies } = createMockDb<Transaction>();
|
|
115
|
+
|
|
116
|
+
// parent item exists
|
|
117
|
+
spies.select.mockReturnValueOnce({ id: "item-1", status: "ACTIVE", isManufacturable: true });
|
|
118
|
+
|
|
119
|
+
const result = await run(db, { ...validInput, lines: [] }, ctx);
|
|
120
|
+
|
|
121
|
+
expect(result.ok).toBe(false);
|
|
122
|
+
if (!result.ok) {
|
|
123
|
+
expect(result.error).toBeInstanceOf(ComponentLineRequiredError);
|
|
124
|
+
}
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it("returns error when a component quantity is not positive", async () => {
|
|
128
|
+
const { db, spies } = createMockDb<Transaction>();
|
|
129
|
+
|
|
130
|
+
// parent item exists
|
|
131
|
+
spies.select.mockReturnValueOnce({ id: "item-1", status: "ACTIVE", isManufacturable: true });
|
|
132
|
+
|
|
133
|
+
const result = await run(
|
|
134
|
+
db,
|
|
135
|
+
{
|
|
136
|
+
...validInput,
|
|
137
|
+
lines: [{ itemId: "item-2", requiredQuantity: 0, unitOfMeasure: "EA" }],
|
|
138
|
+
},
|
|
139
|
+
ctx,
|
|
140
|
+
);
|
|
141
|
+
|
|
142
|
+
expect(result.ok).toBe(false);
|
|
143
|
+
if (!result.ok) {
|
|
144
|
+
expect(result.error).toBeInstanceOf(InvalidComponentQuantityError);
|
|
145
|
+
}
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
it("returns error when a component item is inactive", async () => {
|
|
149
|
+
const { db, spies } = createMockDb<Transaction>();
|
|
150
|
+
|
|
151
|
+
// parent item exists
|
|
152
|
+
spies.select.mockReturnValueOnce({ id: "item-1", status: "ACTIVE", isManufacturable: true });
|
|
153
|
+
// component item not found (treated as inactive)
|
|
154
|
+
spies.select.mockReturnValueOnce(undefined);
|
|
155
|
+
|
|
156
|
+
const result = await run(db, validInput, ctx);
|
|
157
|
+
|
|
158
|
+
expect(result.ok).toBe(false);
|
|
159
|
+
if (!result.ok) {
|
|
160
|
+
expect(result.error).toBeInstanceOf(ComponentItemInactiveError);
|
|
161
|
+
}
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
it("returns error when a component item exists but is inactive", async () => {
|
|
165
|
+
const { db, spies } = createMockDb<Transaction>();
|
|
166
|
+
|
|
167
|
+
// parent item exists
|
|
168
|
+
spies.select.mockReturnValueOnce({ id: "item-1", status: "ACTIVE", isManufacturable: true });
|
|
169
|
+
// component item exists but is inactive
|
|
170
|
+
spies.select.mockReturnValueOnce({ id: "item-2", isActive: false });
|
|
171
|
+
|
|
172
|
+
const result = await run(db, validInput, ctx);
|
|
173
|
+
|
|
174
|
+
expect(result.ok).toBe(false);
|
|
175
|
+
if (!result.ok) {
|
|
176
|
+
expect(result.error).toBeInstanceOf(ComponentItemInactiveError);
|
|
177
|
+
}
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
it("returns error when the version identity already exists in scope", async () => {
|
|
181
|
+
const { db, spies } = createMockDb<Transaction>();
|
|
182
|
+
|
|
183
|
+
// parent item exists
|
|
184
|
+
spies.select.mockReturnValueOnce({ id: "item-1", status: "ACTIVE", isManufacturable: true });
|
|
185
|
+
// component item exists
|
|
186
|
+
spies.select.mockReturnValueOnce({ id: "item-2", status: "ACTIVE" });
|
|
187
|
+
// existing BOM with same version identity
|
|
188
|
+
spies.select.mockReturnValueOnce(baseDraftBom);
|
|
189
|
+
|
|
190
|
+
const result = await run(db, validInput, ctx);
|
|
191
|
+
|
|
192
|
+
expect(result.ok).toBe(false);
|
|
193
|
+
if (!result.ok) {
|
|
194
|
+
expect(result.error).toBeInstanceOf(BomVersionConflictError);
|
|
195
|
+
}
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
it("returns error when the parent item is inactive", async () => {
|
|
199
|
+
const { db, spies } = createMockDb<Transaction>();
|
|
200
|
+
|
|
201
|
+
spies.select.mockReturnValueOnce({ id: "item-1", status: "INACTIVE", isManufacturable: true });
|
|
202
|
+
|
|
203
|
+
const result = await run(db, validInput, ctx);
|
|
204
|
+
|
|
205
|
+
expect(result.ok).toBe(false);
|
|
206
|
+
if (!result.ok) {
|
|
207
|
+
expect(result.error).toBeInstanceOf(ParentItemNotManufacturedError);
|
|
208
|
+
}
|
|
209
|
+
});
|
|
210
|
+
});
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
import type { Transaction } from "../generated/kysely-tailordb";
|
|
2
|
+
import {
|
|
3
|
+
ParentItemNotFoundError,
|
|
4
|
+
ParentItemNotManufacturedError,
|
|
5
|
+
InvalidBomTypeError,
|
|
6
|
+
ComponentLineRequiredError,
|
|
7
|
+
InvalidComponentQuantityError,
|
|
8
|
+
ComponentItemInactiveError,
|
|
9
|
+
BomVersionConflictError,
|
|
10
|
+
} from "../lib/errors.generated";
|
|
11
|
+
import { ok, err, type CommandContext } from "@tailor-platform/erp-kit/module";
|
|
12
|
+
|
|
13
|
+
const VALID_BOM_TYPES = ["MANUFACTURE", "PHANTOM", "KIT"] as const;
|
|
14
|
+
|
|
15
|
+
export interface CreateBillOfMaterialLineInput {
|
|
16
|
+
itemId: string;
|
|
17
|
+
requiredQuantity: number;
|
|
18
|
+
unitOfMeasure?: string | null;
|
|
19
|
+
scrapAssumption?: number | null;
|
|
20
|
+
isSubassembly?: boolean | null;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface CreateBillOfMaterialInput {
|
|
24
|
+
parentItemId: string;
|
|
25
|
+
companyId: string;
|
|
26
|
+
siteId?: string | null;
|
|
27
|
+
bomType: string;
|
|
28
|
+
effectivityStartDate?: Date | null;
|
|
29
|
+
effectivityEndDate?: Date | null;
|
|
30
|
+
defaultSelection?: boolean | null;
|
|
31
|
+
revisionNumber?: string | null;
|
|
32
|
+
lines: CreateBillOfMaterialLineInput[];
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Function: createBillOfMaterial
|
|
37
|
+
*
|
|
38
|
+
* Creates a draft BOM version for one manufactured parent item at company or
|
|
39
|
+
* site scope. Establishes the initial bomType, effectivity window,
|
|
40
|
+
* default-selection intent, and component-line payload.
|
|
41
|
+
*/
|
|
42
|
+
export async function run<CF extends Record<string, unknown>>(
|
|
43
|
+
db: Transaction,
|
|
44
|
+
input: CreateBillOfMaterialInput & CF,
|
|
45
|
+
_ctx: CommandContext,
|
|
46
|
+
) {
|
|
47
|
+
const {
|
|
48
|
+
parentItemId,
|
|
49
|
+
companyId,
|
|
50
|
+
siteId,
|
|
51
|
+
bomType,
|
|
52
|
+
effectivityStartDate,
|
|
53
|
+
effectivityEndDate,
|
|
54
|
+
defaultSelection,
|
|
55
|
+
revisionNumber,
|
|
56
|
+
lines,
|
|
57
|
+
...customFields
|
|
58
|
+
} = input;
|
|
59
|
+
|
|
60
|
+
// 1. Validate parent item exists
|
|
61
|
+
const parentItem = await db
|
|
62
|
+
.selectFrom("Item")
|
|
63
|
+
.selectAll()
|
|
64
|
+
.where("id", "=", parentItemId)
|
|
65
|
+
.executeTakeFirst();
|
|
66
|
+
|
|
67
|
+
if (!parentItem) {
|
|
68
|
+
return err(new ParentItemNotFoundError(parentItemId));
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// 2. Validate parent item is active and manufacturable.
|
|
72
|
+
const parentItemRecord = parentItem as Record<string, unknown>;
|
|
73
|
+
const isInactive =
|
|
74
|
+
("status" in parentItemRecord && parentItemRecord.status !== "ACTIVE") ||
|
|
75
|
+
("isActive" in parentItemRecord && parentItemRecord.isActive === false);
|
|
76
|
+
const isNotManufacturable =
|
|
77
|
+
"isManufacturable" in parentItemRecord && parentItemRecord.isManufacturable === false;
|
|
78
|
+
|
|
79
|
+
if (isInactive || isNotManufacturable) {
|
|
80
|
+
return err(new ParentItemNotManufacturedError(parentItemId));
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// 3. Validate bomType
|
|
84
|
+
if (!VALID_BOM_TYPES.includes(bomType as (typeof VALID_BOM_TYPES)[number])) {
|
|
85
|
+
return err(new InvalidBomTypeError(parentItemId));
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// 4. Validate at least one component line
|
|
89
|
+
if (!lines || lines.length === 0) {
|
|
90
|
+
return err(new ComponentLineRequiredError(parentItemId));
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// 5. Validate each component line
|
|
94
|
+
for (const line of lines) {
|
|
95
|
+
if (line.requiredQuantity <= 0) {
|
|
96
|
+
return err(new InvalidComponentQuantityError(line.itemId));
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Check component item is not inactive (exists in Item table)
|
|
100
|
+
const componentItem = await db
|
|
101
|
+
.selectFrom("Item")
|
|
102
|
+
.selectAll()
|
|
103
|
+
.where("id", "=", line.itemId)
|
|
104
|
+
.executeTakeFirst();
|
|
105
|
+
|
|
106
|
+
if (!componentItem) {
|
|
107
|
+
return err(new ComponentItemInactiveError(line.itemId));
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Check component item is active
|
|
111
|
+
if ("isActive" in componentItem && componentItem.isActive === false) {
|
|
112
|
+
return err(new ComponentItemInactiveError(line.itemId));
|
|
113
|
+
}
|
|
114
|
+
if ("status" in componentItem && componentItem.status === "INACTIVE") {
|
|
115
|
+
return err(new ComponentItemInactiveError(line.itemId));
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// 6. Check version identity uniqueness within same parent item and scope
|
|
120
|
+
const existingBomQuery = db
|
|
121
|
+
.selectFrom("BillOfMaterial")
|
|
122
|
+
.selectAll()
|
|
123
|
+
.where("parentItemId", "=", parentItemId)
|
|
124
|
+
.where("companyId", "=", companyId);
|
|
125
|
+
|
|
126
|
+
const existingBom = await (
|
|
127
|
+
siteId ? existingBomQuery.where("siteId", "=", siteId) : existingBomQuery
|
|
128
|
+
)
|
|
129
|
+
.where("revisionNumber", "=", revisionNumber ?? null)
|
|
130
|
+
.forUpdate()
|
|
131
|
+
.executeTakeFirst();
|
|
132
|
+
|
|
133
|
+
if (existingBom) {
|
|
134
|
+
return err(new BomVersionConflictError(parentItemId));
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// 7. Create draft BOM
|
|
138
|
+
const billOfMaterial = await db
|
|
139
|
+
.insertInto("BillOfMaterial")
|
|
140
|
+
.values({
|
|
141
|
+
...(customFields as Record<string, unknown>),
|
|
142
|
+
parentItemId,
|
|
143
|
+
companyId,
|
|
144
|
+
siteId: siteId ?? null,
|
|
145
|
+
bomType: bomType as "MANUFACTURE" | "PHANTOM" | "KIT",
|
|
146
|
+
effectivityStartDate: effectivityStartDate ?? null,
|
|
147
|
+
effectivityEndDate: effectivityEndDate ?? null,
|
|
148
|
+
defaultSelection: defaultSelection ?? null,
|
|
149
|
+
revisionNumber: revisionNumber ?? null,
|
|
150
|
+
status: "DRAFT",
|
|
151
|
+
createdAt: new Date(),
|
|
152
|
+
updatedAt: null,
|
|
153
|
+
})
|
|
154
|
+
.returningAll()
|
|
155
|
+
.executeTakeFirst();
|
|
156
|
+
|
|
157
|
+
// 8. Create component lines
|
|
158
|
+
for (const line of lines) {
|
|
159
|
+
await db
|
|
160
|
+
.insertInto("BillOfMaterialLine")
|
|
161
|
+
.values({
|
|
162
|
+
billOfMaterialId: billOfMaterial!.id,
|
|
163
|
+
itemId: line.itemId,
|
|
164
|
+
requiredQuantity: line.requiredQuantity,
|
|
165
|
+
unitOfMeasure: line.unitOfMeasure ?? null,
|
|
166
|
+
scrapAssumption: line.scrapAssumption ?? null,
|
|
167
|
+
isSubassembly: line.isSubassembly ?? null,
|
|
168
|
+
createdAt: new Date(),
|
|
169
|
+
updatedAt: null,
|
|
170
|
+
})
|
|
171
|
+
.returningAll()
|
|
172
|
+
.executeTakeFirst();
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
return ok({ billOfMaterial: billOfMaterial! });
|
|
176
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
// @generated — do not edit
|
|
2
|
+
import { permissions } from "../lib/permissions.generated";
|
|
3
|
+
import { run } from "./createProductionOrder";
|
|
4
|
+
import { defineCommand } from "@tailor-platform/erp-kit/module";
|
|
5
|
+
|
|
6
|
+
export const createProductionOrder = defineCommand(permissions.createProductionOrder, run);
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { createMockDb } from "../../../testing/index";
|
|
3
|
+
import type { Transaction } from "../generated/kysely-tailordb";
|
|
4
|
+
import {
|
|
5
|
+
ProductionItemNotFoundError,
|
|
6
|
+
ProductionItemNotManufacturableError,
|
|
7
|
+
InvalidPlannedQuantityError,
|
|
8
|
+
InvalidSiteScopeError,
|
|
9
|
+
CrossScopeMasterReferenceError,
|
|
10
|
+
} from "../lib/errors.generated";
|
|
11
|
+
import { baseDraftProductionOrder, baseActiveBom, baseActiveRouting } from "../testing/fixtures";
|
|
12
|
+
import { run } from "./createProductionOrder";
|
|
13
|
+
import type { CommandContext } from "@tailor-platform/erp-kit/module";
|
|
14
|
+
|
|
15
|
+
describe("createProductionOrder", () => {
|
|
16
|
+
const ctx: CommandContext = {
|
|
17
|
+
actorId: "test-actor",
|
|
18
|
+
permissions: ["manufacturing:createProductionOrder"],
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
const validInput = {
|
|
22
|
+
orderedItemId: "item-1",
|
|
23
|
+
companyId: "company-1",
|
|
24
|
+
siteId: "site-1",
|
|
25
|
+
plannedQuantity: 100,
|
|
26
|
+
plannedStartDate: new Date("2024-03-01T00:00:00.000Z"),
|
|
27
|
+
plannedEndDate: new Date("2024-03-15T00:00:00.000Z"),
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
it("creates a draft production order for a manufacturable item", async () => {
|
|
31
|
+
const { db, spies } = createMockDb<Transaction>();
|
|
32
|
+
const createdOrder = {
|
|
33
|
+
...baseDraftProductionOrder,
|
|
34
|
+
id: "new-order-id",
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
// item lookup
|
|
38
|
+
spies.select.mockReturnValueOnce({ id: "item-1" });
|
|
39
|
+
// site lookup
|
|
40
|
+
spies.select.mockReturnValueOnce({ id: "site-1" });
|
|
41
|
+
// insert
|
|
42
|
+
spies.insert.mockReturnValue(createdOrder);
|
|
43
|
+
|
|
44
|
+
const result = await run(db, validInput, ctx);
|
|
45
|
+
|
|
46
|
+
expect(result.ok).toBe(true);
|
|
47
|
+
if (result.ok) {
|
|
48
|
+
expect(result.value.productionOrder.status).toBe("DRAFT");
|
|
49
|
+
}
|
|
50
|
+
expect(spies.insert).toHaveBeenCalled();
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it("returns error when the produced item does not exist", async () => {
|
|
54
|
+
const { db, spies } = createMockDb<Transaction>();
|
|
55
|
+
|
|
56
|
+
// item lookup returns undefined
|
|
57
|
+
spies.select.mockReturnValueOnce(undefined);
|
|
58
|
+
|
|
59
|
+
const result = await run(db, validInput, ctx);
|
|
60
|
+
|
|
61
|
+
expect(result.ok).toBe(false);
|
|
62
|
+
if (!result.ok) {
|
|
63
|
+
expect(result.error).toBeInstanceOf(ProductionItemNotFoundError);
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it("returns error when the produced item is not manufacturable", async () => {
|
|
68
|
+
const { db, spies } = createMockDb<Transaction>();
|
|
69
|
+
|
|
70
|
+
// item exists but not manufacturable
|
|
71
|
+
spies.select.mockReturnValueOnce({ id: "item-1", isManufacturable: false });
|
|
72
|
+
|
|
73
|
+
const result = await run(db, validInput, ctx);
|
|
74
|
+
|
|
75
|
+
expect(result.ok).toBe(false);
|
|
76
|
+
if (!result.ok) {
|
|
77
|
+
expect(result.error).toBeInstanceOf(ProductionItemNotManufacturableError);
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it("returns error when planned quantity is not positive", async () => {
|
|
82
|
+
const { db } = createMockDb<Transaction>();
|
|
83
|
+
|
|
84
|
+
const result = await run(db, { ...validInput, plannedQuantity: 0 }, ctx);
|
|
85
|
+
|
|
86
|
+
expect(result.ok).toBe(false);
|
|
87
|
+
if (!result.ok) {
|
|
88
|
+
expect(result.error).toBeInstanceOf(InvalidPlannedQuantityError);
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it("returns error when site scope is inconsistent with company", async () => {
|
|
93
|
+
const { db } = createMockDb<Transaction>();
|
|
94
|
+
|
|
95
|
+
const result = await run(db, { ...validInput, siteId: "" }, ctx);
|
|
96
|
+
|
|
97
|
+
expect(result.ok).toBe(false);
|
|
98
|
+
if (!result.ok) {
|
|
99
|
+
expect(result.error).toBeInstanceOf(InvalidSiteScopeError);
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it("returns error when site does not exist", async () => {
|
|
104
|
+
const { db, spies } = createMockDb<Transaction>();
|
|
105
|
+
|
|
106
|
+
// item lookup
|
|
107
|
+
spies.select.mockReturnValueOnce({ id: "item-1" });
|
|
108
|
+
// site lookup returns undefined
|
|
109
|
+
spies.select.mockReturnValueOnce(undefined);
|
|
110
|
+
|
|
111
|
+
const result = await run(db, validInput, ctx);
|
|
112
|
+
|
|
113
|
+
expect(result.ok).toBe(false);
|
|
114
|
+
if (!result.ok) {
|
|
115
|
+
expect(result.error).toBeInstanceOf(InvalidSiteScopeError);
|
|
116
|
+
}
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it("returns error when an optional BOM reference is outside scope", async () => {
|
|
120
|
+
const { db, spies } = createMockDb<Transaction>();
|
|
121
|
+
const crossScopeBom = { ...baseActiveBom, companyId: "other-company" };
|
|
122
|
+
|
|
123
|
+
// item lookup
|
|
124
|
+
spies.select.mockReturnValueOnce({ id: "item-1" });
|
|
125
|
+
// site lookup
|
|
126
|
+
spies.select.mockReturnValueOnce({ id: "site-1" });
|
|
127
|
+
// bom lookup
|
|
128
|
+
spies.select.mockReturnValueOnce(crossScopeBom);
|
|
129
|
+
|
|
130
|
+
const result = await run(db, { ...validInput, selectedBomVersionId: crossScopeBom.id }, ctx);
|
|
131
|
+
|
|
132
|
+
expect(result.ok).toBe(false);
|
|
133
|
+
if (!result.ok) {
|
|
134
|
+
expect(result.error).toBeInstanceOf(CrossScopeMasterReferenceError);
|
|
135
|
+
}
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it("returns error when an optional routing reference is outside scope", async () => {
|
|
139
|
+
const { db, spies } = createMockDb<Transaction>();
|
|
140
|
+
const crossScopeRouting = { ...baseActiveRouting, companyId: "other-company" };
|
|
141
|
+
|
|
142
|
+
// item lookup
|
|
143
|
+
spies.select.mockReturnValueOnce({ id: "item-1" });
|
|
144
|
+
// site lookup
|
|
145
|
+
spies.select.mockReturnValueOnce({ id: "site-1" });
|
|
146
|
+
// routing lookup
|
|
147
|
+
spies.select.mockReturnValueOnce(crossScopeRouting);
|
|
148
|
+
|
|
149
|
+
const result = await run(
|
|
150
|
+
db,
|
|
151
|
+
{ ...validInput, selectedRoutingRevisionId: crossScopeRouting.id },
|
|
152
|
+
ctx,
|
|
153
|
+
);
|
|
154
|
+
|
|
155
|
+
expect(result.ok).toBe(false);
|
|
156
|
+
if (!result.ok) {
|
|
157
|
+
expect(result.error).toBeInstanceOf(CrossScopeMasterReferenceError);
|
|
158
|
+
}
|
|
159
|
+
});
|
|
160
|
+
});
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import type { Transaction } from "../generated/kysely-tailordb";
|
|
2
|
+
import {
|
|
3
|
+
ProductionItemNotFoundError,
|
|
4
|
+
ProductionItemNotManufacturableError,
|
|
5
|
+
InvalidPlannedQuantityError,
|
|
6
|
+
InvalidSiteScopeError,
|
|
7
|
+
CrossScopeMasterReferenceError,
|
|
8
|
+
} from "../lib/errors.generated";
|
|
9
|
+
import { ok, err, type CommandContext } from "@tailor-platform/erp-kit/module";
|
|
10
|
+
|
|
11
|
+
export interface CreateProductionOrderInput {
|
|
12
|
+
orderedItemId: string;
|
|
13
|
+
companyId: string;
|
|
14
|
+
siteId: string;
|
|
15
|
+
plannedQuantity: number;
|
|
16
|
+
plannedStartDate: Date;
|
|
17
|
+
plannedEndDate: Date;
|
|
18
|
+
selectedBomVersionId?: string | null;
|
|
19
|
+
selectedRoutingRevisionId?: string | null;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Function: createProductionOrder
|
|
24
|
+
*
|
|
25
|
+
* Creates a draft manufacturing request with produced item, quantity, site,
|
|
26
|
+
* and planned dates. Records planner intent without freezing BOM, routing,
|
|
27
|
+
* work orders, or cost assumptions.
|
|
28
|
+
*/
|
|
29
|
+
export async function run<CF extends Record<string, unknown>>(
|
|
30
|
+
db: Transaction,
|
|
31
|
+
input: CreateProductionOrderInput & CF,
|
|
32
|
+
_ctx: CommandContext,
|
|
33
|
+
) {
|
|
34
|
+
const {
|
|
35
|
+
orderedItemId,
|
|
36
|
+
companyId,
|
|
37
|
+
siteId,
|
|
38
|
+
plannedQuantity,
|
|
39
|
+
plannedStartDate,
|
|
40
|
+
plannedEndDate,
|
|
41
|
+
selectedBomVersionId,
|
|
42
|
+
selectedRoutingRevisionId,
|
|
43
|
+
...customFields
|
|
44
|
+
} = input;
|
|
45
|
+
|
|
46
|
+
// 1. Validate planned quantity > 0
|
|
47
|
+
if (plannedQuantity <= 0) {
|
|
48
|
+
return err(new InvalidPlannedQuantityError(orderedItemId));
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// 2. Validate scope
|
|
52
|
+
if (!companyId || !siteId) {
|
|
53
|
+
return err(new InvalidSiteScopeError(orderedItemId));
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// 3. Validate produced item exists
|
|
57
|
+
const item = await db
|
|
58
|
+
.selectFrom("Item")
|
|
59
|
+
.selectAll()
|
|
60
|
+
.where("id", "=", orderedItemId)
|
|
61
|
+
.executeTakeFirst();
|
|
62
|
+
|
|
63
|
+
if (!item) {
|
|
64
|
+
return err(new ProductionItemNotFoundError(orderedItemId));
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// 4. Validate item is manufacturable
|
|
68
|
+
if (item && "isManufacturable" in item && item.isManufacturable === false) {
|
|
69
|
+
return err(new ProductionItemNotManufacturableError(orderedItemId));
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// 5. Validate site belongs to company scope
|
|
73
|
+
const site = await db.selectFrom("Site").selectAll().where("id", "=", siteId).executeTakeFirst();
|
|
74
|
+
|
|
75
|
+
if (!site) {
|
|
76
|
+
return err(new InvalidSiteScopeError(orderedItemId));
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// 5. Validate optional BOM reference scope
|
|
80
|
+
if (selectedBomVersionId) {
|
|
81
|
+
const bom = await db
|
|
82
|
+
.selectFrom("BillOfMaterial")
|
|
83
|
+
.selectAll()
|
|
84
|
+
.where("id", "=", selectedBomVersionId)
|
|
85
|
+
.executeTakeFirst();
|
|
86
|
+
|
|
87
|
+
if (bom && (bom.companyId !== companyId || (bom.siteId != null && bom.siteId !== siteId))) {
|
|
88
|
+
return err(new CrossScopeMasterReferenceError(orderedItemId));
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// 6. Validate optional routing reference scope
|
|
93
|
+
if (selectedRoutingRevisionId) {
|
|
94
|
+
const routing = await db
|
|
95
|
+
.selectFrom("Routing")
|
|
96
|
+
.selectAll()
|
|
97
|
+
.where("id", "=", selectedRoutingRevisionId)
|
|
98
|
+
.executeTakeFirst();
|
|
99
|
+
|
|
100
|
+
if (
|
|
101
|
+
routing &&
|
|
102
|
+
(routing.companyId !== companyId || (routing.siteId != null && routing.siteId !== siteId))
|
|
103
|
+
) {
|
|
104
|
+
return err(new CrossScopeMasterReferenceError(orderedItemId));
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// 7. Create draft production order
|
|
109
|
+
const productionOrder = await db
|
|
110
|
+
.insertInto("ProductionOrder")
|
|
111
|
+
.values({
|
|
112
|
+
...(customFields as Record<string, unknown>),
|
|
113
|
+
orderedItemId,
|
|
114
|
+
companyId,
|
|
115
|
+
siteId,
|
|
116
|
+
plannedQuantity,
|
|
117
|
+
plannedStartDate,
|
|
118
|
+
plannedEndDate,
|
|
119
|
+
selectedBomVersionId: selectedBomVersionId ?? null,
|
|
120
|
+
selectedRoutingRevisionId: selectedRoutingRevisionId ?? null,
|
|
121
|
+
status: "DRAFT",
|
|
122
|
+
createdAt: new Date(),
|
|
123
|
+
updatedAt: null,
|
|
124
|
+
})
|
|
125
|
+
.returningAll()
|
|
126
|
+
.executeTakeFirst();
|
|
127
|
+
|
|
128
|
+
return ok({ productionOrder: productionOrder! });
|
|
129
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
// @generated — do not edit
|
|
2
|
+
import { permissions } from "../lib/permissions.generated";
|
|
3
|
+
import { run } from "./createRouting";
|
|
4
|
+
import { defineCommand } from "@tailor-platform/erp-kit/module";
|
|
5
|
+
|
|
6
|
+
export const createRouting = defineCommand(permissions.createRouting, run);
|