@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,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);
|
|
@@ -0,0 +1,113 @@
|
|
|
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
|
+
WorkCenterNotDeactivatableError,
|
|
7
|
+
WorkCenterInUseError,
|
|
8
|
+
} from "../lib/errors.generated";
|
|
9
|
+
import { baseActiveWorkCenter, baseDraftWorkCenter } from "../testing/fixtures";
|
|
10
|
+
import { run } from "./deactivateWorkCenter";
|
|
11
|
+
import type { CommandContext } from "@tailor-platform/erp-kit/module";
|
|
12
|
+
|
|
13
|
+
describe("deactivateWorkCenter", () => {
|
|
14
|
+
const ctx: CommandContext = {
|
|
15
|
+
actorId: "test-actor",
|
|
16
|
+
permissions: ["manufacturing:deactivateWorkCenter"],
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
it("deactivates an active work center with no blocking dependencies", async () => {
|
|
20
|
+
const { db, spies } = createMockDb<Transaction>();
|
|
21
|
+
const deactivated = { ...baseActiveWorkCenter, status: "INACTIVE" as const };
|
|
22
|
+
|
|
23
|
+
spies.select.mockReturnValueOnce(baseActiveWorkCenter); // fetch work center
|
|
24
|
+
spies.select.mockReturnValueOnce(undefined); // no active routing dependency
|
|
25
|
+
spies.select.mockReturnValueOnce(undefined); // no open work orders
|
|
26
|
+
spies.update.mockReturnValue(deactivated);
|
|
27
|
+
|
|
28
|
+
const result = await run(db, { id: baseActiveWorkCenter.id }, ctx);
|
|
29
|
+
|
|
30
|
+
expect(result.ok).toBe(true);
|
|
31
|
+
if (result.ok) {
|
|
32
|
+
expect(result.value.workCenter.status).toBe("INACTIVE");
|
|
33
|
+
}
|
|
34
|
+
expect(spies.update).toHaveBeenCalled();
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it("returns error when the work center does not exist", async () => {
|
|
38
|
+
const { db, spies } = createMockDb<Transaction>();
|
|
39
|
+
spies.select.mockReturnValueOnce(undefined);
|
|
40
|
+
|
|
41
|
+
const result = await run(db, { id: "nonexistent" }, ctx);
|
|
42
|
+
|
|
43
|
+
expect(result.ok).toBe(false);
|
|
44
|
+
if (!result.ok) {
|
|
45
|
+
expect(result.error).toBeInstanceOf(WorkCenterNotFoundError);
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it("returns error when the work center is not active", async () => {
|
|
50
|
+
const { db, spies } = createMockDb<Transaction>();
|
|
51
|
+
spies.select.mockReturnValueOnce(baseDraftWorkCenter);
|
|
52
|
+
|
|
53
|
+
const result = await run(db, { id: baseDraftWorkCenter.id }, ctx);
|
|
54
|
+
|
|
55
|
+
expect(result.ok).toBe(false);
|
|
56
|
+
if (!result.ok) {
|
|
57
|
+
expect(result.error).toBeInstanceOf(WorkCenterNotDeactivatableError);
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it("returns error when open routing or released-work dependencies block deactivation", async () => {
|
|
62
|
+
const { db, spies } = createMockDb<Transaction>();
|
|
63
|
+
|
|
64
|
+
spies.select.mockReturnValueOnce(baseActiveWorkCenter); // fetch work center
|
|
65
|
+
spies.select.mockReturnValueOnce({ id: "operation-1" }); // active routing dependency found
|
|
66
|
+
|
|
67
|
+
const result = await run(db, { id: baseActiveWorkCenter.id }, ctx);
|
|
68
|
+
|
|
69
|
+
expect(result.ok).toBe(false);
|
|
70
|
+
if (!result.ok) {
|
|
71
|
+
expect(result.error).toBeInstanceOf(WorkCenterInUseError);
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it("returns error when open work orders block deactivation", async () => {
|
|
76
|
+
const { db, spies } = createMockDb<Transaction>();
|
|
77
|
+
|
|
78
|
+
spies.select.mockReturnValueOnce(baseActiveWorkCenter); // fetch work center
|
|
79
|
+
spies.select.mockReturnValueOnce(undefined); // no active routing dependency
|
|
80
|
+
spies.select.mockReturnValueOnce({ id: "work-order-1" }); // open work order found
|
|
81
|
+
|
|
82
|
+
const result = await run(db, { id: baseActiveWorkCenter.id }, ctx);
|
|
83
|
+
|
|
84
|
+
expect(result.ok).toBe(false);
|
|
85
|
+
if (!result.ok) {
|
|
86
|
+
expect(result.error).toBeInstanceOf(WorkCenterInUseError);
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it("preserves historical work-order and cost data after deactivation", async () => {
|
|
91
|
+
const { db, spies } = createMockDb<Transaction>();
|
|
92
|
+
const deactivated = {
|
|
93
|
+
...baseActiveWorkCenter,
|
|
94
|
+
status: "INACTIVE" as const,
|
|
95
|
+
updatedAt: new Date(),
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
spies.select.mockReturnValueOnce(baseActiveWorkCenter);
|
|
99
|
+
spies.select.mockReturnValueOnce(undefined); // no active routing dependency
|
|
100
|
+
spies.select.mockReturnValueOnce(undefined); // no open work orders
|
|
101
|
+
spies.update.mockReturnValue(deactivated);
|
|
102
|
+
|
|
103
|
+
const result = await run(db, { id: baseActiveWorkCenter.id }, ctx);
|
|
104
|
+
|
|
105
|
+
expect(result.ok).toBe(true);
|
|
106
|
+
if (result.ok) {
|
|
107
|
+
// The work center is deactivated but its identity and historical fields are preserved
|
|
108
|
+
expect(result.value.workCenter.id).toBe(baseActiveWorkCenter.id);
|
|
109
|
+
expect(result.value.workCenter.code).toBe(baseActiveWorkCenter.code);
|
|
110
|
+
expect(result.value.workCenter.status).toBe("INACTIVE");
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
});
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import type { Transaction } from "../generated/kysely-tailordb";
|
|
2
|
+
import {
|
|
3
|
+
WorkCenterNotFoundError,
|
|
4
|
+
WorkCenterNotDeactivatableError,
|
|
5
|
+
WorkCenterInUseError,
|
|
6
|
+
} from "../lib/errors.generated";
|
|
7
|
+
import { ok, err, type CommandContext } from "@tailor-platform/erp-kit/module";
|
|
8
|
+
|
|
9
|
+
export interface DeactivateWorkCenterInput {
|
|
10
|
+
id: string;
|
|
11
|
+
from?: string[];
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Function: deactivateWorkCenter
|
|
16
|
+
*
|
|
17
|
+
* Removes a work center from future routing and release selection.
|
|
18
|
+
* Preserves historical work-order and cost records while blocking
|
|
19
|
+
* new execution use.
|
|
20
|
+
*/
|
|
21
|
+
export async function run<CF extends Record<string, unknown>>(
|
|
22
|
+
db: Transaction,
|
|
23
|
+
input: DeactivateWorkCenterInput & CF,
|
|
24
|
+
_ctx: CommandContext,
|
|
25
|
+
) {
|
|
26
|
+
const { id, from, ...customFields } = input;
|
|
27
|
+
void customFields;
|
|
28
|
+
|
|
29
|
+
const allowedStatuses = from ?? ["ACTIVE"];
|
|
30
|
+
|
|
31
|
+
// 1. Fetch work center with lock
|
|
32
|
+
const workCenter = await db
|
|
33
|
+
.selectFrom("WorkCenter")
|
|
34
|
+
.selectAll()
|
|
35
|
+
.where("id", "=", id)
|
|
36
|
+
.forUpdate()
|
|
37
|
+
.executeTakeFirst();
|
|
38
|
+
|
|
39
|
+
if (!workCenter) {
|
|
40
|
+
return err(new WorkCenterNotFoundError(id));
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// 2. Validate status is deactivatable
|
|
44
|
+
if (!allowedStatuses.includes(workCenter.status)) {
|
|
45
|
+
return err(new WorkCenterNotDeactivatableError(workCenter.code));
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// 3. Check for blocking dependencies — active routings that reference this work center
|
|
49
|
+
const activeRoutingDependency = await db
|
|
50
|
+
.selectFrom("RoutingOperation")
|
|
51
|
+
.innerJoin("Routing", "Routing.id", "RoutingOperation.routingId")
|
|
52
|
+
.select("RoutingOperation.id")
|
|
53
|
+
.where("RoutingOperation.workCenterId", "=", id)
|
|
54
|
+
.where("Routing.status", "=", "ACTIVE")
|
|
55
|
+
.executeTakeFirst();
|
|
56
|
+
|
|
57
|
+
if (activeRoutingDependency) {
|
|
58
|
+
return err(new WorkCenterInUseError(workCenter.code));
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// 4. Check for open work orders that still reference this work center
|
|
62
|
+
const openWorkOrder = await db
|
|
63
|
+
.selectFrom("WorkOrder")
|
|
64
|
+
.select("WorkOrder.id")
|
|
65
|
+
.where("WorkOrder.workCenterId", "=", id)
|
|
66
|
+
.where("WorkOrder.status", "in", ["PENDING", "IN_PROGRESS", "PAUSED"])
|
|
67
|
+
.executeTakeFirst();
|
|
68
|
+
|
|
69
|
+
if (openWorkOrder) {
|
|
70
|
+
return err(new WorkCenterInUseError(workCenter.code));
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// 5. Set status to INACTIVE
|
|
74
|
+
const updatedWorkCenter = await db
|
|
75
|
+
.updateTable("WorkCenter")
|
|
76
|
+
.set({
|
|
77
|
+
status: "INACTIVE",
|
|
78
|
+
updatedAt: new Date(),
|
|
79
|
+
})
|
|
80
|
+
.where("id", "=", id)
|
|
81
|
+
.returningAll()
|
|
82
|
+
.executeTakeFirst();
|
|
83
|
+
|
|
84
|
+
return ok({ workCenter: updatedWorkCenter! });
|
|
85
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
// @generated — do not edit
|
|
2
|
+
import { permissions } from "../lib/permissions.generated";
|
|
3
|
+
import { run } from "./pauseWorkOrder";
|
|
4
|
+
import { defineCommand } from "@tailor-platform/erp-kit/module";
|
|
5
|
+
|
|
6
|
+
export const pauseWorkOrder = defineCommand(permissions.pauseWorkOrder, run);
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { createMockDb } from "../../../testing/index";
|
|
3
|
+
import type { Transaction } from "../generated/kysely-tailordb";
|
|
4
|
+
import {
|
|
5
|
+
WorkOrderNotFoundError,
|
|
6
|
+
WorkOrderNotPausableError,
|
|
7
|
+
PauseReasonRequiredError,
|
|
8
|
+
} from "../lib/errors.generated";
|
|
9
|
+
import { baseInProgressWorkOrder, basePendingWorkOrder } from "../testing/fixtures";
|
|
10
|
+
import { run } from "./pauseWorkOrder";
|
|
11
|
+
import type { CommandContext } from "@tailor-platform/erp-kit/module";
|
|
12
|
+
|
|
13
|
+
describe("pauseWorkOrder", () => {
|
|
14
|
+
const ctx: CommandContext = {
|
|
15
|
+
actorId: "test-actor",
|
|
16
|
+
permissions: ["manufacturing:pauseWorkOrder"],
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
it("pauses an in-progress work order", async () => {
|
|
20
|
+
const { db, spies } = createMockDb<Transaction>();
|
|
21
|
+
const pausedWorkOrder = {
|
|
22
|
+
...baseInProgressWorkOrder,
|
|
23
|
+
status: "PAUSED" as const,
|
|
24
|
+
pauseReason: "Machine maintenance",
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
// work order lookup
|
|
28
|
+
spies.select.mockReturnValueOnce(baseInProgressWorkOrder);
|
|
29
|
+
// update work order
|
|
30
|
+
spies.update.mockReturnValueOnce(pausedWorkOrder);
|
|
31
|
+
// insert execution event
|
|
32
|
+
spies.insert.mockReturnValueOnce(undefined);
|
|
33
|
+
|
|
34
|
+
const result = await run(
|
|
35
|
+
db,
|
|
36
|
+
{ id: baseInProgressWorkOrder.id, pauseReason: "Machine maintenance" },
|
|
37
|
+
ctx,
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
expect(result.ok).toBe(true);
|
|
41
|
+
if (result.ok) {
|
|
42
|
+
expect(result.value.workOrder.status).toBe("PAUSED");
|
|
43
|
+
expect(result.value.workOrder.pauseReason).toBe("Machine maintenance");
|
|
44
|
+
}
|
|
45
|
+
expect(spies.update).toHaveBeenCalled();
|
|
46
|
+
expect(spies.insert).toHaveBeenCalled();
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it("returns error when the work order does not exist", async () => {
|
|
50
|
+
const { db, spies } = createMockDb<Transaction>();
|
|
51
|
+
spies.select.mockReturnValueOnce(undefined);
|
|
52
|
+
|
|
53
|
+
const result = await run(db, { id: "nonexistent", pauseReason: "Some reason" }, ctx);
|
|
54
|
+
|
|
55
|
+
expect(result.ok).toBe(false);
|
|
56
|
+
if (!result.ok) {
|
|
57
|
+
expect(result.error).toBeInstanceOf(WorkOrderNotFoundError);
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it("returns error when the work order is not in progress", async () => {
|
|
62
|
+
const { db, spies } = createMockDb<Transaction>();
|
|
63
|
+
spies.select.mockReturnValueOnce(basePendingWorkOrder);
|
|
64
|
+
|
|
65
|
+
const result = await run(db, { id: basePendingWorkOrder.id, pauseReason: "Some reason" }, ctx);
|
|
66
|
+
|
|
67
|
+
expect(result.ok).toBe(false);
|
|
68
|
+
if (!result.ok) {
|
|
69
|
+
expect(result.error).toBeInstanceOf(WorkOrderNotPausableError);
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it("returns error when no pause reason is provided", async () => {
|
|
74
|
+
const { db } = createMockDb<Transaction>();
|
|
75
|
+
|
|
76
|
+
const result = await run(db, { id: baseInProgressWorkOrder.id, pauseReason: "" }, ctx);
|
|
77
|
+
|
|
78
|
+
expect(result.ok).toBe(false);
|
|
79
|
+
if (!result.ok) {
|
|
80
|
+
expect(result.error).toBeInstanceOf(PauseReasonRequiredError);
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it("preserves prior execution quantities and time after pause", async () => {
|
|
85
|
+
const { db, spies } = createMockDb<Transaction>();
|
|
86
|
+
const pausedWorkOrder = {
|
|
87
|
+
...baseInProgressWorkOrder,
|
|
88
|
+
status: "PAUSED" as const,
|
|
89
|
+
pauseReason: "Break time",
|
|
90
|
+
completedQuantity: baseInProgressWorkOrder.completedQuantity,
|
|
91
|
+
scrapQuantity: baseInProgressWorkOrder.scrapQuantity,
|
|
92
|
+
actualSetupTime: baseInProgressWorkOrder.actualSetupTime,
|
|
93
|
+
actualRunTime: baseInProgressWorkOrder.actualRunTime,
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
// work order lookup
|
|
97
|
+
spies.select.mockReturnValueOnce(baseInProgressWorkOrder);
|
|
98
|
+
// update work order
|
|
99
|
+
spies.update.mockReturnValueOnce(pausedWorkOrder);
|
|
100
|
+
// insert execution event
|
|
101
|
+
spies.insert.mockReturnValueOnce(undefined);
|
|
102
|
+
|
|
103
|
+
const result = await run(
|
|
104
|
+
db,
|
|
105
|
+
{ id: baseInProgressWorkOrder.id, pauseReason: "Break time" },
|
|
106
|
+
ctx,
|
|
107
|
+
);
|
|
108
|
+
|
|
109
|
+
expect(result.ok).toBe(true);
|
|
110
|
+
if (result.ok) {
|
|
111
|
+
expect(result.value.workOrder.completedQuantity).toBe(
|
|
112
|
+
baseInProgressWorkOrder.completedQuantity,
|
|
113
|
+
);
|
|
114
|
+
expect(result.value.workOrder.actualSetupTime).toBe(baseInProgressWorkOrder.actualSetupTime);
|
|
115
|
+
expect(result.value.workOrder.actualRunTime).toBe(baseInProgressWorkOrder.actualRunTime);
|
|
116
|
+
}
|
|
117
|
+
});
|
|
118
|
+
});
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import type { Transaction } from "../generated/kysely-tailordb";
|
|
2
|
+
import {
|
|
3
|
+
WorkOrderNotFoundError,
|
|
4
|
+
WorkOrderNotPausableError,
|
|
5
|
+
PauseReasonRequiredError,
|
|
6
|
+
} from "../lib/errors.generated";
|
|
7
|
+
import { ok, err, type CommandContext } from "@tailor-platform/erp-kit/module";
|
|
8
|
+
|
|
9
|
+
export interface PauseWorkOrderInput {
|
|
10
|
+
id: string;
|
|
11
|
+
pauseReason: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Function: pauseWorkOrder
|
|
16
|
+
*
|
|
17
|
+
* Temporarily halts an in-progress work order and records the reason for
|
|
18
|
+
* the interruption. Preserves accumulated execution history so the work
|
|
19
|
+
* order can later resume.
|
|
20
|
+
*/
|
|
21
|
+
export async function run<CF extends Record<string, unknown>>(
|
|
22
|
+
db: Transaction,
|
|
23
|
+
input: PauseWorkOrderInput & CF,
|
|
24
|
+
_ctx: CommandContext,
|
|
25
|
+
) {
|
|
26
|
+
const { id, pauseReason, ...customFields } = input;
|
|
27
|
+
void customFields;
|
|
28
|
+
|
|
29
|
+
// 1. Validate pause reason is provided
|
|
30
|
+
if (!pauseReason || pauseReason.trim() === "") {
|
|
31
|
+
return err(new PauseReasonRequiredError(id));
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// 2. Fetch work order with lock
|
|
35
|
+
const workOrder = await db
|
|
36
|
+
.selectFrom("WorkOrder")
|
|
37
|
+
.selectAll()
|
|
38
|
+
.where("id", "=", id)
|
|
39
|
+
.forUpdate()
|
|
40
|
+
.executeTakeFirst();
|
|
41
|
+
|
|
42
|
+
if (!workOrder) {
|
|
43
|
+
return err(new WorkOrderNotFoundError(id));
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// 3. Validate status is IN_PROGRESS
|
|
47
|
+
if (workOrder.status !== "IN_PROGRESS") {
|
|
48
|
+
return err(new WorkOrderNotPausableError(id));
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// 4. Record pause event and set PAUSED
|
|
52
|
+
const now = new Date();
|
|
53
|
+
|
|
54
|
+
const updatedWorkOrder = await db
|
|
55
|
+
.updateTable("WorkOrder")
|
|
56
|
+
.set({
|
|
57
|
+
status: "PAUSED",
|
|
58
|
+
pauseReason,
|
|
59
|
+
updatedAt: now,
|
|
60
|
+
})
|
|
61
|
+
.where("id", "=", id)
|
|
62
|
+
.returningAll()
|
|
63
|
+
.executeTakeFirstOrThrow();
|
|
64
|
+
|
|
65
|
+
// 5. Create PAUSED execution event
|
|
66
|
+
await db
|
|
67
|
+
.insertInto("WorkOrderExecutionEvent")
|
|
68
|
+
.values({
|
|
69
|
+
workOrderId: id,
|
|
70
|
+
eventType: "PAUSED",
|
|
71
|
+
timestamp: now,
|
|
72
|
+
quantity: null,
|
|
73
|
+
timeValue: null,
|
|
74
|
+
scrapValue: null,
|
|
75
|
+
notes: pauseReason,
|
|
76
|
+
createdAt: now,
|
|
77
|
+
updatedAt: null,
|
|
78
|
+
})
|
|
79
|
+
.execute();
|
|
80
|
+
|
|
81
|
+
return ok({ workOrder: updatedWorkOrder });
|
|
82
|
+
}
|