@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,173 @@
|
|
|
1
|
+
import type { Transaction } from "../generated/kysely-tailordb";
|
|
2
|
+
import {
|
|
3
|
+
BomNotFoundError,
|
|
4
|
+
BomNotActivatableError,
|
|
5
|
+
ComponentLineRequiredError,
|
|
6
|
+
ComponentItemInactiveError,
|
|
7
|
+
CircularBomReferenceError,
|
|
8
|
+
EffectivityConflictError,
|
|
9
|
+
} from "../lib/errors.generated";
|
|
10
|
+
import { ok, err, type CommandContext } from "@tailor-platform/erp-kit/module";
|
|
11
|
+
|
|
12
|
+
export interface ActivateBillOfMaterialInput {
|
|
13
|
+
id: string;
|
|
14
|
+
from?: string[];
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Function: activateBillOfMaterial
|
|
19
|
+
*
|
|
20
|
+
* Validates a draft BOM version and makes it selectable for production-order
|
|
21
|
+
* release. Activation enforces circular-reference checks, effectivity
|
|
22
|
+
* conflicts, and component readiness.
|
|
23
|
+
*/
|
|
24
|
+
export async function run<CF extends Record<string, unknown>>(
|
|
25
|
+
db: Transaction,
|
|
26
|
+
input: ActivateBillOfMaterialInput & CF,
|
|
27
|
+
_ctx: CommandContext,
|
|
28
|
+
) {
|
|
29
|
+
const { id, from, ...customFields } = input;
|
|
30
|
+
void customFields;
|
|
31
|
+
|
|
32
|
+
const allowedStatuses = from ?? ["DRAFT"];
|
|
33
|
+
|
|
34
|
+
// 1. Fetch BOM with lock
|
|
35
|
+
const bom = await db
|
|
36
|
+
.selectFrom("BillOfMaterial")
|
|
37
|
+
.selectAll()
|
|
38
|
+
.where("id", "=", id)
|
|
39
|
+
.forUpdate()
|
|
40
|
+
.executeTakeFirst();
|
|
41
|
+
|
|
42
|
+
if (!bom) {
|
|
43
|
+
return err(new BomNotFoundError(id));
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// 2. Validate status is activatable
|
|
47
|
+
if (!allowedStatuses.includes(bom.status)) {
|
|
48
|
+
return err(new BomNotActivatableError(id));
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// 3. Validate at least one component line exists
|
|
52
|
+
const lines = await db
|
|
53
|
+
.selectFrom("BillOfMaterialLine")
|
|
54
|
+
.selectAll()
|
|
55
|
+
.where("billOfMaterialId", "=", id)
|
|
56
|
+
.execute();
|
|
57
|
+
|
|
58
|
+
if (!lines || lines.length === 0) {
|
|
59
|
+
return err(new ComponentLineRequiredError(id));
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// 4. Validate all component items are active
|
|
63
|
+
for (const line of lines) {
|
|
64
|
+
const componentItem = await db
|
|
65
|
+
.selectFrom("Item")
|
|
66
|
+
.selectAll()
|
|
67
|
+
.where("id", "=", line.itemId)
|
|
68
|
+
.executeTakeFirst();
|
|
69
|
+
|
|
70
|
+
if (!componentItem) {
|
|
71
|
+
return err(new ComponentItemInactiveError(line.itemId));
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// 5. Check for circular references using DFS traversal
|
|
76
|
+
const circularResult = await detectCircularReference(
|
|
77
|
+
db,
|
|
78
|
+
bom.parentItemId,
|
|
79
|
+
lines,
|
|
80
|
+
new Set<string>([bom.parentItemId]),
|
|
81
|
+
);
|
|
82
|
+
if (circularResult) {
|
|
83
|
+
return err(new CircularBomReferenceError(id));
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// 6. Check effectivity conflicts — overlapping active effectivity windows
|
|
87
|
+
// for the same parent item and scope
|
|
88
|
+
const conflictingBom = await db
|
|
89
|
+
.selectFrom("BillOfMaterial")
|
|
90
|
+
.selectAll()
|
|
91
|
+
.where("parentItemId", "=", bom.parentItemId)
|
|
92
|
+
.where("companyId", "=", bom.companyId)
|
|
93
|
+
.where("id", "!=", id)
|
|
94
|
+
.where("status", "=", "ACTIVE")
|
|
95
|
+
.executeTakeFirst();
|
|
96
|
+
|
|
97
|
+
if (conflictingBom) {
|
|
98
|
+
// Check date overlap
|
|
99
|
+
const bomStart = bom.effectivityStartDate;
|
|
100
|
+
const bomEnd = bom.effectivityEndDate;
|
|
101
|
+
const conflictStart = conflictingBom.effectivityStartDate;
|
|
102
|
+
const conflictEnd = conflictingBom.effectivityEndDate;
|
|
103
|
+
|
|
104
|
+
const hasOverlap =
|
|
105
|
+
(!bomStart || !conflictEnd || bomStart <= conflictEnd) &&
|
|
106
|
+
(!bomEnd || !conflictStart || bomEnd >= conflictStart);
|
|
107
|
+
|
|
108
|
+
if (hasOverlap && bom.defaultSelection && conflictingBom.defaultSelection) {
|
|
109
|
+
return err(new EffectivityConflictError(id));
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// 7. Set status to ACTIVE
|
|
114
|
+
const updatedBom = await db
|
|
115
|
+
.updateTable("BillOfMaterial")
|
|
116
|
+
.set({
|
|
117
|
+
status: "ACTIVE",
|
|
118
|
+
updatedAt: new Date(),
|
|
119
|
+
})
|
|
120
|
+
.where("id", "=", id)
|
|
121
|
+
.returningAll()
|
|
122
|
+
.executeTakeFirst();
|
|
123
|
+
|
|
124
|
+
return ok({ billOfMaterial: updatedBom! });
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
async function detectCircularReference(
|
|
128
|
+
db: Transaction,
|
|
129
|
+
rootParentItemId: string,
|
|
130
|
+
lines: { itemId: string; isSubassembly: boolean | null }[],
|
|
131
|
+
visitedItems: Set<string>,
|
|
132
|
+
): Promise<boolean> {
|
|
133
|
+
for (const line of lines) {
|
|
134
|
+
// If this item has already been visited in the path, we have a cycle
|
|
135
|
+
if (visitedItems.has(line.itemId)) {
|
|
136
|
+
return true;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if (!line.isSubassembly) continue;
|
|
140
|
+
|
|
141
|
+
// Find ALL child BOMs for this item (multiple versions may exist)
|
|
142
|
+
const childBoms = await db
|
|
143
|
+
.selectFrom("BillOfMaterial")
|
|
144
|
+
.selectAll()
|
|
145
|
+
.where("parentItemId", "=", line.itemId)
|
|
146
|
+
.where("status", "in", ["DRAFT", "ACTIVE"])
|
|
147
|
+
.execute();
|
|
148
|
+
|
|
149
|
+
if (childBoms.length > 0) {
|
|
150
|
+
visitedItems.add(line.itemId);
|
|
151
|
+
|
|
152
|
+
for (const childBom of childBoms) {
|
|
153
|
+
const childLines = await db
|
|
154
|
+
.selectFrom("BillOfMaterialLine")
|
|
155
|
+
.selectAll()
|
|
156
|
+
.where("billOfMaterialId", "=", childBom.id)
|
|
157
|
+
.execute();
|
|
158
|
+
|
|
159
|
+
const hasCycle = await detectCircularReference(
|
|
160
|
+
db,
|
|
161
|
+
rootParentItemId,
|
|
162
|
+
childLines,
|
|
163
|
+
visitedItems,
|
|
164
|
+
);
|
|
165
|
+
|
|
166
|
+
if (hasCycle) return true;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
visitedItems.delete(line.itemId);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
return false;
|
|
173
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
// @generated — do not edit
|
|
2
|
+
import { permissions } from "../lib/permissions.generated";
|
|
3
|
+
import { run } from "./activateRouting";
|
|
4
|
+
import { defineCommand } from "@tailor-platform/erp-kit/module";
|
|
5
|
+
|
|
6
|
+
export const activateRouting = defineCommand(permissions.activateRouting, run);
|
|
@@ -0,0 +1,152 @@
|
|
|
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
|
+
RoutingNotActivatableError,
|
|
7
|
+
OperationRequiredError,
|
|
8
|
+
DuplicateOperationSequenceError,
|
|
9
|
+
WorkCenterNotActiveError,
|
|
10
|
+
CrossCompanyWorkCenterError,
|
|
11
|
+
} from "../lib/errors.generated";
|
|
12
|
+
import {
|
|
13
|
+
baseDraftRouting,
|
|
14
|
+
baseActiveRouting,
|
|
15
|
+
baseRoutingOperation,
|
|
16
|
+
baseActiveWorkCenter,
|
|
17
|
+
baseInactiveWorkCenter,
|
|
18
|
+
} from "../testing/fixtures";
|
|
19
|
+
import { run } from "./activateRouting";
|
|
20
|
+
import type { CommandContext } from "@tailor-platform/erp-kit/module";
|
|
21
|
+
|
|
22
|
+
describe("activateRouting", () => {
|
|
23
|
+
const ctx: CommandContext = {
|
|
24
|
+
actorId: "test-actor",
|
|
25
|
+
permissions: ["manufacturing:activateRouting"],
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const validInput = { id: "routing-1" };
|
|
29
|
+
|
|
30
|
+
it("activates a valid draft routing", async () => {
|
|
31
|
+
const { db, spies } = createMockDb<Transaction>();
|
|
32
|
+
|
|
33
|
+
// select: Routing lookup
|
|
34
|
+
spies.select.mockReturnValueOnce(baseDraftRouting);
|
|
35
|
+
// select: RoutingOperation list
|
|
36
|
+
spies.select.mockReturnValueOnce([baseRoutingOperation]);
|
|
37
|
+
// select: WorkCenter lookup for operation
|
|
38
|
+
spies.select.mockReturnValueOnce(baseActiveWorkCenter);
|
|
39
|
+
// update: status to ACTIVE
|
|
40
|
+
spies.update.mockReturnValue({ ...baseDraftRouting, status: "ACTIVE" });
|
|
41
|
+
|
|
42
|
+
const result = await run(db, validInput, ctx);
|
|
43
|
+
|
|
44
|
+
expect(result.ok).toBe(true);
|
|
45
|
+
if (result.ok) {
|
|
46
|
+
expect(result.value.routing.status).toBe("ACTIVE");
|
|
47
|
+
}
|
|
48
|
+
expect(spies.update).toHaveBeenCalled();
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it("returns error when the routing does not exist", async () => {
|
|
52
|
+
const { db, spies } = createMockDb<Transaction>();
|
|
53
|
+
|
|
54
|
+
// select: Routing lookup - not found
|
|
55
|
+
spies.select.mockReturnValueOnce(undefined);
|
|
56
|
+
|
|
57
|
+
const result = await run(db, validInput, ctx);
|
|
58
|
+
|
|
59
|
+
expect(result.ok).toBe(false);
|
|
60
|
+
if (!result.ok) {
|
|
61
|
+
expect(result.error).toBeInstanceOf(RoutingNotFoundError);
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it("returns error when the routing is not in DRAFT", async () => {
|
|
66
|
+
const { db, spies } = createMockDb<Transaction>();
|
|
67
|
+
|
|
68
|
+
// select: Routing lookup - ACTIVE
|
|
69
|
+
spies.select.mockReturnValueOnce(baseActiveRouting);
|
|
70
|
+
|
|
71
|
+
const result = await run(db, { id: "routing-2" }, ctx);
|
|
72
|
+
|
|
73
|
+
expect(result.ok).toBe(false);
|
|
74
|
+
if (!result.ok) {
|
|
75
|
+
expect(result.error).toBeInstanceOf(RoutingNotActivatableError);
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it("returns error when the routing has no operations", async () => {
|
|
80
|
+
const { db, spies } = createMockDb<Transaction>();
|
|
81
|
+
|
|
82
|
+
// select: Routing lookup
|
|
83
|
+
spies.select.mockReturnValueOnce(baseDraftRouting);
|
|
84
|
+
// select: RoutingOperation list - empty
|
|
85
|
+
spies.select.mockReturnValueOnce([]);
|
|
86
|
+
|
|
87
|
+
const result = await run(db, validInput, ctx);
|
|
88
|
+
|
|
89
|
+
expect(result.ok).toBe(false);
|
|
90
|
+
if (!result.ok) {
|
|
91
|
+
expect(result.error).toBeInstanceOf(OperationRequiredError);
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it("returns error when operation sequence numbers are duplicated", async () => {
|
|
96
|
+
const { db, spies } = createMockDb<Transaction>();
|
|
97
|
+
|
|
98
|
+
// select: Routing lookup
|
|
99
|
+
spies.select.mockReturnValueOnce(baseDraftRouting);
|
|
100
|
+
// select: RoutingOperation list with duplicate sequences
|
|
101
|
+
spies.select.mockReturnValueOnce([
|
|
102
|
+
baseRoutingOperation,
|
|
103
|
+
{ ...baseRoutingOperation, id: "operation-dup", sequenceNumber: 10 },
|
|
104
|
+
]);
|
|
105
|
+
|
|
106
|
+
const result = await run(db, validInput, ctx);
|
|
107
|
+
|
|
108
|
+
expect(result.ok).toBe(false);
|
|
109
|
+
if (!result.ok) {
|
|
110
|
+
expect(result.error).toBeInstanceOf(DuplicateOperationSequenceError);
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it("returns error when a referenced work center is inactive", async () => {
|
|
115
|
+
const { db, spies } = createMockDb<Transaction>();
|
|
116
|
+
|
|
117
|
+
// select: Routing lookup
|
|
118
|
+
spies.select.mockReturnValueOnce(baseDraftRouting);
|
|
119
|
+
// select: RoutingOperation list
|
|
120
|
+
spies.select.mockReturnValueOnce([baseRoutingOperation]);
|
|
121
|
+
// select: WorkCenter lookup - INACTIVE
|
|
122
|
+
spies.select.mockReturnValueOnce(baseInactiveWorkCenter);
|
|
123
|
+
|
|
124
|
+
const result = await run(db, validInput, ctx);
|
|
125
|
+
|
|
126
|
+
expect(result.ok).toBe(false);
|
|
127
|
+
if (!result.ok) {
|
|
128
|
+
expect(result.error).toBeInstanceOf(WorkCenterNotActiveError);
|
|
129
|
+
}
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it("returns error when a referenced work center is outside the routing company", async () => {
|
|
133
|
+
const { db, spies } = createMockDb<Transaction>();
|
|
134
|
+
|
|
135
|
+
// select: Routing lookup
|
|
136
|
+
spies.select.mockReturnValueOnce(baseDraftRouting);
|
|
137
|
+
// select: RoutingOperation list
|
|
138
|
+
spies.select.mockReturnValueOnce([baseRoutingOperation]);
|
|
139
|
+
// select: WorkCenter lookup - different company
|
|
140
|
+
spies.select.mockReturnValueOnce({
|
|
141
|
+
...baseActiveWorkCenter,
|
|
142
|
+
companyId: "other-company",
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
const result = await run(db, validInput, ctx);
|
|
146
|
+
|
|
147
|
+
expect(result.ok).toBe(false);
|
|
148
|
+
if (!result.ok) {
|
|
149
|
+
expect(result.error).toBeInstanceOf(CrossCompanyWorkCenterError);
|
|
150
|
+
}
|
|
151
|
+
});
|
|
152
|
+
});
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import type { Transaction } from "../generated/kysely-tailordb";
|
|
2
|
+
import {
|
|
3
|
+
RoutingNotFoundError,
|
|
4
|
+
RoutingNotActivatableError,
|
|
5
|
+
OperationRequiredError,
|
|
6
|
+
DuplicateOperationSequenceError,
|
|
7
|
+
WorkCenterNotActiveError,
|
|
8
|
+
CrossCompanyWorkCenterError,
|
|
9
|
+
} from "../lib/errors.generated";
|
|
10
|
+
import { ok, err, type CommandContext } from "@tailor-platform/erp-kit/module";
|
|
11
|
+
|
|
12
|
+
export interface ActivateRoutingInput {
|
|
13
|
+
id: string;
|
|
14
|
+
from?: string[];
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Function: activateRouting
|
|
19
|
+
*
|
|
20
|
+
* Validates a draft routing and transitions it to ACTIVE status.
|
|
21
|
+
* Enforces operation completeness, sequence integrity, and active
|
|
22
|
+
* work-center readiness.
|
|
23
|
+
*/
|
|
24
|
+
export async function run(db: Transaction, input: ActivateRoutingInput, _ctx: CommandContext) {
|
|
25
|
+
const { id, from = ["DRAFT"] } = input;
|
|
26
|
+
|
|
27
|
+
// 1. Fetch routing
|
|
28
|
+
const routing = await db
|
|
29
|
+
.selectFrom("Routing")
|
|
30
|
+
.selectAll()
|
|
31
|
+
.where("id", "=", id)
|
|
32
|
+
.forUpdate()
|
|
33
|
+
.executeTakeFirst();
|
|
34
|
+
|
|
35
|
+
if (!routing) {
|
|
36
|
+
return err(new RoutingNotFoundError(id));
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// 2. Verify allowed source status
|
|
40
|
+
if (!from.includes(routing.status)) {
|
|
41
|
+
return err(new RoutingNotActivatableError(id));
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// 3. Fetch operations
|
|
45
|
+
const operations = await db
|
|
46
|
+
.selectFrom("RoutingOperation")
|
|
47
|
+
.selectAll()
|
|
48
|
+
.where("routingId", "=", id)
|
|
49
|
+
.execute();
|
|
50
|
+
|
|
51
|
+
// 4. At least one operation required
|
|
52
|
+
if (operations.length === 0) {
|
|
53
|
+
return err(new OperationRequiredError(id));
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// 5. Validate unique sequence numbers
|
|
57
|
+
const seqNumbers = operations.map((op) => op.sequenceNumber);
|
|
58
|
+
const uniqueSeqs = new Set(seqNumbers);
|
|
59
|
+
if (uniqueSeqs.size !== seqNumbers.length) {
|
|
60
|
+
return err(new DuplicateOperationSequenceError(id));
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// 6. Validate work centers are active and in same company
|
|
64
|
+
for (const op of operations) {
|
|
65
|
+
const workCenter = await db
|
|
66
|
+
.selectFrom("WorkCenter")
|
|
67
|
+
.selectAll()
|
|
68
|
+
.where("id", "=", op.workCenterId)
|
|
69
|
+
.executeTakeFirst();
|
|
70
|
+
|
|
71
|
+
if (workCenter?.status !== "ACTIVE") {
|
|
72
|
+
return err(new WorkCenterNotActiveError(op.workCenterId));
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (workCenter.companyId !== routing.companyId) {
|
|
76
|
+
return err(new CrossCompanyWorkCenterError(op.workCenterId));
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// 7. Set status to ACTIVE
|
|
81
|
+
const updatedRouting = await db
|
|
82
|
+
.updateTable("Routing")
|
|
83
|
+
.set({
|
|
84
|
+
status: "ACTIVE",
|
|
85
|
+
updatedAt: new Date(),
|
|
86
|
+
})
|
|
87
|
+
.where("id", "=", id)
|
|
88
|
+
.returningAll()
|
|
89
|
+
.executeTakeFirst();
|
|
90
|
+
|
|
91
|
+
return ok({ routing: updatedRouting! });
|
|
92
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
// @generated — do not edit
|
|
2
|
+
import { permissions } from "../lib/permissions.generated";
|
|
3
|
+
import { run } from "./activateWorkCenter";
|
|
4
|
+
import { defineCommand } from "@tailor-platform/erp-kit/module";
|
|
5
|
+
|
|
6
|
+
export const activateWorkCenter = defineCommand(permissions.activateWorkCenter, run);
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { createMockDb } from "../../../testing/index";
|
|
3
|
+
import type { Transaction } from "../generated/kysely-tailordb";
|
|
4
|
+
import {
|
|
5
|
+
WorkCenterNotFoundError,
|
|
6
|
+
WorkCenterNotActivatableError,
|
|
7
|
+
MissingCalendarContextError,
|
|
8
|
+
InvalidCapacityError,
|
|
9
|
+
InvalidRateError,
|
|
10
|
+
OverheadCurrencyRequiredError,
|
|
11
|
+
} from "../lib/errors.generated";
|
|
12
|
+
import {
|
|
13
|
+
baseDraftWorkCenter,
|
|
14
|
+
baseActiveWorkCenter,
|
|
15
|
+
baseInactiveWorkCenter,
|
|
16
|
+
} from "../testing/fixtures";
|
|
17
|
+
import { run } from "./activateWorkCenter";
|
|
18
|
+
import type { CommandContext } from "@tailor-platform/erp-kit/module";
|
|
19
|
+
|
|
20
|
+
describe("activateWorkCenter", () => {
|
|
21
|
+
const ctx: CommandContext = {
|
|
22
|
+
actorId: "test-actor",
|
|
23
|
+
permissions: ["manufacturing:activateWorkCenter"],
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
it("activates a valid draft work center", async () => {
|
|
27
|
+
const { db, spies } = createMockDb<Transaction>();
|
|
28
|
+
const activated = { ...baseDraftWorkCenter, status: "ACTIVE" as const };
|
|
29
|
+
|
|
30
|
+
spies.select.mockReturnValueOnce(baseDraftWorkCenter);
|
|
31
|
+
spies.update.mockReturnValue(activated);
|
|
32
|
+
|
|
33
|
+
const result = await run(db, { id: baseDraftWorkCenter.id }, ctx);
|
|
34
|
+
|
|
35
|
+
expect(result.ok).toBe(true);
|
|
36
|
+
if (result.ok) {
|
|
37
|
+
expect(result.value.workCenter.status).toBe("ACTIVE");
|
|
38
|
+
}
|
|
39
|
+
expect(spies.update).toHaveBeenCalled();
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it("reactivates a valid inactive work center", async () => {
|
|
43
|
+
const { db, spies } = createMockDb<Transaction>();
|
|
44
|
+
const reactivated = { ...baseInactiveWorkCenter, status: "ACTIVE" as const };
|
|
45
|
+
|
|
46
|
+
spies.select.mockReturnValueOnce(baseInactiveWorkCenter);
|
|
47
|
+
spies.update.mockReturnValue(reactivated);
|
|
48
|
+
|
|
49
|
+
const result = await run(db, { id: baseInactiveWorkCenter.id }, ctx);
|
|
50
|
+
|
|
51
|
+
expect(result.ok).toBe(true);
|
|
52
|
+
if (result.ok) {
|
|
53
|
+
expect(result.value.workCenter.status).toBe("ACTIVE");
|
|
54
|
+
}
|
|
55
|
+
expect(spies.update).toHaveBeenCalled();
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it("returns error when the work center does not exist", async () => {
|
|
59
|
+
const { db, spies } = createMockDb<Transaction>();
|
|
60
|
+
spies.select.mockReturnValueOnce(undefined);
|
|
61
|
+
|
|
62
|
+
const result = await run(db, { id: "nonexistent" }, ctx);
|
|
63
|
+
|
|
64
|
+
expect(result.ok).toBe(false);
|
|
65
|
+
if (!result.ok) {
|
|
66
|
+
expect(result.error).toBeInstanceOf(WorkCenterNotFoundError);
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it("returns error when the status does not allow activation", async () => {
|
|
71
|
+
const { db, spies } = createMockDb<Transaction>();
|
|
72
|
+
spies.select.mockReturnValueOnce(baseActiveWorkCenter);
|
|
73
|
+
|
|
74
|
+
const result = await run(db, { id: baseActiveWorkCenter.id }, ctx);
|
|
75
|
+
|
|
76
|
+
expect(result.ok).toBe(false);
|
|
77
|
+
if (!result.ok) {
|
|
78
|
+
expect(result.error).toBeInstanceOf(WorkCenterNotActivatableError);
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it("returns error when required calendar context is missing", async () => {
|
|
83
|
+
const { db, spies } = createMockDb<Transaction>();
|
|
84
|
+
const noCalendar = { ...baseDraftWorkCenter, calendarReference: null };
|
|
85
|
+
spies.select.mockReturnValueOnce(noCalendar);
|
|
86
|
+
|
|
87
|
+
const result = await run(db, { id: baseDraftWorkCenter.id }, ctx);
|
|
88
|
+
|
|
89
|
+
expect(result.ok).toBe(false);
|
|
90
|
+
if (!result.ok) {
|
|
91
|
+
expect(result.error).toBeInstanceOf(MissingCalendarContextError);
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it("returns error when capacity is not positive", async () => {
|
|
96
|
+
const { db, spies } = createMockDb<Transaction>();
|
|
97
|
+
const zeroCapacity = { ...baseDraftWorkCenter, capacityAssumptions: 0 };
|
|
98
|
+
spies.select.mockReturnValueOnce(zeroCapacity);
|
|
99
|
+
|
|
100
|
+
const result = await run(db, { id: baseDraftWorkCenter.id }, 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 configured rate is negative", async () => {
|
|
109
|
+
const { db, spies } = createMockDb<Transaction>();
|
|
110
|
+
spies.select.mockReturnValueOnce({ ...baseDraftWorkCenter, laborRate: -1 });
|
|
111
|
+
|
|
112
|
+
const result = await run(db, { id: baseDraftWorkCenter.id }, ctx);
|
|
113
|
+
|
|
114
|
+
expect(result.ok).toBe(false);
|
|
115
|
+
if (!result.ok) {
|
|
116
|
+
expect(result.error).toBeInstanceOf(InvalidRateError);
|
|
117
|
+
}
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it("returns error when fixed overhead is missing its currency", async () => {
|
|
121
|
+
const { db, spies } = createMockDb<Transaction>();
|
|
122
|
+
spies.select.mockReturnValueOnce({
|
|
123
|
+
...baseDraftWorkCenter,
|
|
124
|
+
overheadAbsorptionMethod: "FIXED_AMOUNT_PER_GOOD_UNIT",
|
|
125
|
+
overheadAbsorptionCurrency: null,
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
const result = await run(db, { id: baseDraftWorkCenter.id }, ctx);
|
|
129
|
+
|
|
130
|
+
expect(result.ok).toBe(false);
|
|
131
|
+
if (!result.ok) {
|
|
132
|
+
expect(result.error).toBeInstanceOf(OverheadCurrencyRequiredError);
|
|
133
|
+
}
|
|
134
|
+
});
|
|
135
|
+
});
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import type { Transaction } from "../generated/kysely-tailordb";
|
|
2
|
+
import {
|
|
3
|
+
WorkCenterNotFoundError,
|
|
4
|
+
WorkCenterNotActivatableError,
|
|
5
|
+
MissingCalendarContextError,
|
|
6
|
+
InvalidCapacityError,
|
|
7
|
+
InvalidRateError,
|
|
8
|
+
OverheadCurrencyRequiredError,
|
|
9
|
+
} from "../lib/errors.generated";
|
|
10
|
+
import { ok, err, type CommandContext } from "@tailor-platform/erp-kit/module";
|
|
11
|
+
|
|
12
|
+
const ACTIVATABLE_STATUSES = ["DRAFT", "INACTIVE"] as const;
|
|
13
|
+
|
|
14
|
+
export interface ActivateWorkCenterInput {
|
|
15
|
+
id: string;
|
|
16
|
+
from?: string[];
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Function: activateWorkCenter
|
|
21
|
+
*
|
|
22
|
+
* Validates that a draft or inactive work center has the execution data
|
|
23
|
+
* required by planning and costing, then marks it available for routing
|
|
24
|
+
* and work-order use.
|
|
25
|
+
*/
|
|
26
|
+
export async function run<CF extends Record<string, unknown>>(
|
|
27
|
+
db: Transaction,
|
|
28
|
+
input: ActivateWorkCenterInput & CF,
|
|
29
|
+
_ctx: CommandContext,
|
|
30
|
+
) {
|
|
31
|
+
const { id, from, ...customFields } = input;
|
|
32
|
+
void customFields;
|
|
33
|
+
|
|
34
|
+
const allowedStatuses = from ?? [...ACTIVATABLE_STATUSES];
|
|
35
|
+
|
|
36
|
+
// 1. Fetch work center with lock
|
|
37
|
+
const workCenter = await db
|
|
38
|
+
.selectFrom("WorkCenter")
|
|
39
|
+
.selectAll()
|
|
40
|
+
.where("id", "=", id)
|
|
41
|
+
.forUpdate()
|
|
42
|
+
.executeTakeFirst();
|
|
43
|
+
|
|
44
|
+
if (!workCenter) {
|
|
45
|
+
return err(new WorkCenterNotFoundError(id));
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// 2. Validate status is activatable
|
|
49
|
+
if (!allowedStatuses.includes(workCenter.status)) {
|
|
50
|
+
return err(new WorkCenterNotActivatableError(workCenter.code));
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// 3. Validate calendar context
|
|
54
|
+
if (!workCenter.calendarReference) {
|
|
55
|
+
return err(new MissingCalendarContextError(workCenter.code));
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// 4. Validate capacity > 0
|
|
59
|
+
if (workCenter.capacityAssumptions <= 0) {
|
|
60
|
+
return err(new InvalidCapacityError(workCenter.code));
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// 5. Validate rates >= 0
|
|
64
|
+
if (workCenter.laborRate != null && workCenter.laborRate < 0) {
|
|
65
|
+
return err(new InvalidRateError(workCenter.code));
|
|
66
|
+
}
|
|
67
|
+
if (workCenter.machineRate != null && workCenter.machineRate < 0) {
|
|
68
|
+
return err(new InvalidRateError(workCenter.code));
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// 6. Validate overhead currency for FIXED_AMOUNT_PER_GOOD_UNIT
|
|
72
|
+
if (
|
|
73
|
+
workCenter.overheadAbsorptionMethod === "FIXED_AMOUNT_PER_GOOD_UNIT" &&
|
|
74
|
+
!workCenter.overheadAbsorptionCurrency
|
|
75
|
+
) {
|
|
76
|
+
return err(new OverheadCurrencyRequiredError(workCenter.code));
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// 7. Set status to ACTIVE
|
|
80
|
+
const updatedWorkCenter = await db
|
|
81
|
+
.updateTable("WorkCenter")
|
|
82
|
+
.set({
|
|
83
|
+
status: "ACTIVE",
|
|
84
|
+
updatedAt: new Date(),
|
|
85
|
+
})
|
|
86
|
+
.where("id", "=", id)
|
|
87
|
+
.returningAll()
|
|
88
|
+
.executeTakeFirst();
|
|
89
|
+
|
|
90
|
+
return ok({ workCenter: updatedWorkCenter! });
|
|
91
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
// @generated — do not edit
|
|
2
|
+
import { permissions } from "../lib/permissions.generated";
|
|
3
|
+
import { run } from "./cancelProductionOrder";
|
|
4
|
+
import { defineCommand } from "@tailor-platform/erp-kit/module";
|
|
5
|
+
|
|
6
|
+
export const cancelProductionOrder = defineCommand(permissions.cancelProductionOrder, run);
|