@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,231 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { createMockDb } from "../../../testing/index";
|
|
3
|
+
import type { Transaction } from "../generated/kysely-tailordb";
|
|
4
|
+
import {
|
|
5
|
+
CostSummaryNotFoundError,
|
|
6
|
+
CostSummaryNotReviewableError,
|
|
7
|
+
ParentOrderNotTechnicallyCompleteError,
|
|
8
|
+
IncompleteVarianceBreakdownError,
|
|
9
|
+
ReviewerRequiredError,
|
|
10
|
+
VarianceCalculationFailedError,
|
|
11
|
+
} from "../lib/errors.generated";
|
|
12
|
+
import {
|
|
13
|
+
basePendingReviewCostSummary,
|
|
14
|
+
baseCollectingCostSummary,
|
|
15
|
+
baseTechCompleteProductionOrder,
|
|
16
|
+
baseInProgressProductionOrder,
|
|
17
|
+
} from "../testing/fixtures";
|
|
18
|
+
import { run } from "./reviewManufacturingCostSummary";
|
|
19
|
+
import type { VarianceBreakdownEntry } from "./reviewManufacturingCostSummary";
|
|
20
|
+
import type { CommandContext } from "@tailor-platform/erp-kit/module";
|
|
21
|
+
|
|
22
|
+
describe("reviewManufacturingCostSummary", () => {
|
|
23
|
+
const ctx: CommandContext = {
|
|
24
|
+
actorId: "test-actor",
|
|
25
|
+
permissions: ["manufacturing:reviewManufacturingCostSummary"],
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const fullVarianceBreakdown: VarianceBreakdownEntry[] = [
|
|
29
|
+
{
|
|
30
|
+
varianceType: "MATERIAL_PRICE",
|
|
31
|
+
amount: 100,
|
|
32
|
+
accountReference: "acct-5001",
|
|
33
|
+
variancePercentage: 5.0,
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
varianceType: "MATERIAL_USAGE",
|
|
37
|
+
amount: -20,
|
|
38
|
+
accountReference: "acct-5002",
|
|
39
|
+
variancePercentage: -1.0,
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
varianceType: "LABOR_RATE",
|
|
43
|
+
amount: 30,
|
|
44
|
+
accountReference: "acct-5003",
|
|
45
|
+
variancePercentage: 6.0,
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
varianceType: "LABOR_EFFICIENCY",
|
|
49
|
+
amount: -10,
|
|
50
|
+
accountReference: "acct-5004",
|
|
51
|
+
variancePercentage: -2.0,
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
varianceType: "MACHINE_RATE",
|
|
55
|
+
amount: 50,
|
|
56
|
+
accountReference: "acct-5005",
|
|
57
|
+
variancePercentage: 5.0,
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
varianceType: "MACHINE_EFFICIENCY",
|
|
61
|
+
amount: 0,
|
|
62
|
+
accountReference: "acct-5006",
|
|
63
|
+
variancePercentage: 0.0,
|
|
64
|
+
},
|
|
65
|
+
{ varianceType: "SCRAP", amount: 50, accountReference: "acct-5007", variancePercentage: 100.0 },
|
|
66
|
+
{ varianceType: "YIELD", amount: 0, accountReference: "acct-5008", variancePercentage: 0.0 },
|
|
67
|
+
];
|
|
68
|
+
|
|
69
|
+
const validInput = {
|
|
70
|
+
costSummaryId: "cost-summary-2",
|
|
71
|
+
reviewerId: "reviewer-1",
|
|
72
|
+
reviewerNotes: "Variances within acceptable range",
|
|
73
|
+
varianceBreakdown: fullVarianceBreakdown,
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
it("reviews a pending cost summary and freezes the variance breakdown", async () => {
|
|
77
|
+
const { db, spies } = createMockDb<Transaction>();
|
|
78
|
+
|
|
79
|
+
// select: cost summary, production order
|
|
80
|
+
spies.select.mockReturnValueOnce(basePendingReviewCostSummary);
|
|
81
|
+
spies.select.mockReturnValueOnce(baseTechCompleteProductionOrder);
|
|
82
|
+
|
|
83
|
+
const updatedSummary = {
|
|
84
|
+
...basePendingReviewCostSummary,
|
|
85
|
+
status: "VARIANCE_REVIEWED",
|
|
86
|
+
reviewerNotes: "Variances within acceptable range",
|
|
87
|
+
};
|
|
88
|
+
spies.insert.mockReturnValue({});
|
|
89
|
+
spies.update.mockReturnValue(updatedSummary);
|
|
90
|
+
|
|
91
|
+
const result = await run(db, validInput, ctx);
|
|
92
|
+
|
|
93
|
+
expect(result.ok).toBe(true);
|
|
94
|
+
if (result.ok) {
|
|
95
|
+
expect(result.value.costSummary.status).toBe("VARIANCE_REVIEWED");
|
|
96
|
+
}
|
|
97
|
+
// 8 variance lines inserted
|
|
98
|
+
expect(spies.insert).toHaveBeenCalledTimes(8);
|
|
99
|
+
expect(spies.update).toHaveBeenCalled();
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it("returns error when the summary does not exist", async () => {
|
|
103
|
+
const { db, spies } = createMockDb<Transaction>();
|
|
104
|
+
|
|
105
|
+
spies.select.mockReturnValueOnce(undefined);
|
|
106
|
+
|
|
107
|
+
const result = await run(db, validInput, ctx);
|
|
108
|
+
|
|
109
|
+
expect(result.ok).toBe(false);
|
|
110
|
+
if (!result.ok) {
|
|
111
|
+
expect(result.error).toBeInstanceOf(CostSummaryNotFoundError);
|
|
112
|
+
}
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it("returns error when the summary is not pending review", async () => {
|
|
116
|
+
const { db, spies } = createMockDb<Transaction>();
|
|
117
|
+
|
|
118
|
+
spies.select.mockReturnValueOnce(baseCollectingCostSummary); // COLLECTING, not PENDING_VARIANCE_REVIEW
|
|
119
|
+
|
|
120
|
+
const result = await run(db, validInput, ctx);
|
|
121
|
+
|
|
122
|
+
expect(result.ok).toBe(false);
|
|
123
|
+
if (!result.ok) {
|
|
124
|
+
expect(result.error).toBeInstanceOf(CostSummaryNotReviewableError);
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it("returns error when the parent order is not technically complete", async () => {
|
|
129
|
+
const { db, spies } = createMockDb<Transaction>();
|
|
130
|
+
|
|
131
|
+
spies.select.mockReturnValueOnce(basePendingReviewCostSummary);
|
|
132
|
+
spies.select.mockReturnValueOnce(baseInProgressProductionOrder); // not TECHNICALLY_COMPLETE
|
|
133
|
+
|
|
134
|
+
const result = await run(db, validInput, ctx);
|
|
135
|
+
|
|
136
|
+
expect(result.ok).toBe(false);
|
|
137
|
+
if (!result.ok) {
|
|
138
|
+
expect(result.error).toBeInstanceOf(ParentOrderNotTechnicallyCompleteError);
|
|
139
|
+
}
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it("returns error when the review result cannot represent all required variance types", async () => {
|
|
143
|
+
const { db, spies } = createMockDb<Transaction>();
|
|
144
|
+
|
|
145
|
+
spies.select.mockReturnValueOnce(basePendingReviewCostSummary);
|
|
146
|
+
spies.select.mockReturnValueOnce(baseTechCompleteProductionOrder);
|
|
147
|
+
|
|
148
|
+
// Missing YIELD from the breakdown
|
|
149
|
+
const incompleteBreakdown = fullVarianceBreakdown.filter((v) => v.varianceType !== "YIELD");
|
|
150
|
+
|
|
151
|
+
const result = await run(db, { ...validInput, varianceBreakdown: incompleteBreakdown }, ctx);
|
|
152
|
+
|
|
153
|
+
expect(result.ok).toBe(false);
|
|
154
|
+
if (!result.ok) {
|
|
155
|
+
expect(result.error).toBeInstanceOf(IncompleteVarianceBreakdownError);
|
|
156
|
+
}
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
it("returns error when reviewer identity is missing", async () => {
|
|
160
|
+
const { db } = createMockDb<Transaction>();
|
|
161
|
+
|
|
162
|
+
const result = await run(db, { ...validInput, reviewerId: null }, ctx);
|
|
163
|
+
|
|
164
|
+
expect(result.ok).toBe(false);
|
|
165
|
+
if (!result.ok) {
|
|
166
|
+
expect(result.error).toBeInstanceOf(ReviewerRequiredError);
|
|
167
|
+
}
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
it("keeps all required variance types distinct during review", async () => {
|
|
171
|
+
const { db, spies } = createMockDb<Transaction>();
|
|
172
|
+
|
|
173
|
+
spies.select.mockReturnValueOnce(basePendingReviewCostSummary);
|
|
174
|
+
spies.select.mockReturnValueOnce(baseTechCompleteProductionOrder);
|
|
175
|
+
|
|
176
|
+
const updatedSummary = {
|
|
177
|
+
...basePendingReviewCostSummary,
|
|
178
|
+
status: "VARIANCE_REVIEWED",
|
|
179
|
+
};
|
|
180
|
+
spies.insert.mockReturnValue({});
|
|
181
|
+
spies.update.mockReturnValue(updatedSummary);
|
|
182
|
+
|
|
183
|
+
const result = await run(db, validInput, ctx);
|
|
184
|
+
|
|
185
|
+
expect(result.ok).toBe(true);
|
|
186
|
+
// Verify all 8 distinct variance lines were inserted
|
|
187
|
+
expect(spies.insert).toHaveBeenCalledTimes(8);
|
|
188
|
+
|
|
189
|
+
// Verify each call used the correct variance type through the values spy
|
|
190
|
+
const insertedTypes = new Set<string>();
|
|
191
|
+
for (const call of spies.values.mock.calls) {
|
|
192
|
+
const values = call[0] as Record<string, unknown>;
|
|
193
|
+
if (values.varianceType) {
|
|
194
|
+
insertedTypes.add(values.varianceType as string);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
expect(insertedTypes.size).toBe(8);
|
|
198
|
+
expect(insertedTypes.has("MATERIAL_PRICE")).toBe(true);
|
|
199
|
+
expect(insertedTypes.has("MATERIAL_USAGE")).toBe(true);
|
|
200
|
+
expect(insertedTypes.has("LABOR_RATE")).toBe(true);
|
|
201
|
+
expect(insertedTypes.has("LABOR_EFFICIENCY")).toBe(true);
|
|
202
|
+
expect(insertedTypes.has("MACHINE_RATE")).toBe(true);
|
|
203
|
+
expect(insertedTypes.has("MACHINE_EFFICIENCY")).toBe(true);
|
|
204
|
+
expect(insertedTypes.has("SCRAP")).toBe(true);
|
|
205
|
+
expect(insertedTypes.has("YIELD")).toBe(true);
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
it("returns error when final variance cannot be recalculated consistently", async () => {
|
|
209
|
+
const { db, spies } = createMockDb<Transaction>();
|
|
210
|
+
|
|
211
|
+
spies.select.mockReturnValueOnce(basePendingReviewCostSummary);
|
|
212
|
+
spies.select.mockReturnValueOnce(baseTechCompleteProductionOrder);
|
|
213
|
+
|
|
214
|
+
const result = await run(
|
|
215
|
+
db,
|
|
216
|
+
{
|
|
217
|
+
...validInput,
|
|
218
|
+
varianceBreakdown: [
|
|
219
|
+
{ ...fullVarianceBreakdown[0], amount: Number.NaN },
|
|
220
|
+
...fullVarianceBreakdown.slice(1),
|
|
221
|
+
],
|
|
222
|
+
},
|
|
223
|
+
ctx,
|
|
224
|
+
);
|
|
225
|
+
|
|
226
|
+
expect(result.ok).toBe(false);
|
|
227
|
+
if (!result.ok) {
|
|
228
|
+
expect(result.error).toBeInstanceOf(VarianceCalculationFailedError);
|
|
229
|
+
}
|
|
230
|
+
});
|
|
231
|
+
});
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import type { Transaction } from "../generated/kysely-tailordb";
|
|
2
|
+
import {
|
|
3
|
+
CostSummaryNotFoundError,
|
|
4
|
+
CostSummaryNotReviewableError,
|
|
5
|
+
ParentOrderNotTechnicallyCompleteError,
|
|
6
|
+
IncompleteVarianceBreakdownError,
|
|
7
|
+
ReviewerRequiredError,
|
|
8
|
+
VarianceCalculationFailedError,
|
|
9
|
+
} from "../lib/errors.generated";
|
|
10
|
+
import { ok, err, type CommandContext } from "@tailor-platform/erp-kit/module";
|
|
11
|
+
|
|
12
|
+
const REQUIRED_VARIANCE_TYPES = [
|
|
13
|
+
"MATERIAL_PRICE",
|
|
14
|
+
"MATERIAL_USAGE",
|
|
15
|
+
"LABOR_RATE",
|
|
16
|
+
"LABOR_EFFICIENCY",
|
|
17
|
+
"MACHINE_RATE",
|
|
18
|
+
"MACHINE_EFFICIENCY",
|
|
19
|
+
"SCRAP",
|
|
20
|
+
"YIELD",
|
|
21
|
+
] as const;
|
|
22
|
+
|
|
23
|
+
type VarianceType = (typeof REQUIRED_VARIANCE_TYPES)[number];
|
|
24
|
+
|
|
25
|
+
export interface VarianceBreakdownEntry {
|
|
26
|
+
varianceType: VarianceType;
|
|
27
|
+
amount: number;
|
|
28
|
+
accountReference?: string | null;
|
|
29
|
+
variancePercentage?: number | null;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface ReviewManufacturingCostSummaryInput {
|
|
33
|
+
costSummaryId: string;
|
|
34
|
+
reviewerId?: string | null;
|
|
35
|
+
reviewerNotes?: string | null;
|
|
36
|
+
varianceBreakdown?: VarianceBreakdownEntry[] | null;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Function: reviewManufacturingCostSummary
|
|
41
|
+
*
|
|
42
|
+
* Recalculates the final planned-versus-actual result for a technically
|
|
43
|
+
* complete order and freezes the variance classification through an
|
|
44
|
+
* explicit reviewer approval step.
|
|
45
|
+
*/
|
|
46
|
+
export async function run<CF extends Record<string, unknown>>(
|
|
47
|
+
db: Transaction,
|
|
48
|
+
input: ReviewManufacturingCostSummaryInput & CF,
|
|
49
|
+
_ctx: CommandContext,
|
|
50
|
+
) {
|
|
51
|
+
const { costSummaryId, reviewerId, reviewerNotes, varianceBreakdown } = input;
|
|
52
|
+
|
|
53
|
+
// 1. Validate reviewer identity
|
|
54
|
+
if (!reviewerId) {
|
|
55
|
+
return err(new ReviewerRequiredError(costSummaryId));
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// 2. Resolve cost summary
|
|
59
|
+
const costSummary = await db
|
|
60
|
+
.selectFrom("ManufacturingCostSummary")
|
|
61
|
+
.selectAll()
|
|
62
|
+
.where("id", "=", costSummaryId)
|
|
63
|
+
.forUpdate()
|
|
64
|
+
.executeTakeFirst();
|
|
65
|
+
|
|
66
|
+
if (!costSummary) {
|
|
67
|
+
return err(new CostSummaryNotFoundError(costSummaryId));
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// 3. Summary must be PENDING_VARIANCE_REVIEW
|
|
71
|
+
if (costSummary.status !== "PENDING_VARIANCE_REVIEW") {
|
|
72
|
+
return err(new CostSummaryNotReviewableError(costSummaryId));
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// 4. Parent production order must be TECHNICALLY_COMPLETE
|
|
76
|
+
const productionOrder = await db
|
|
77
|
+
.selectFrom("ProductionOrder")
|
|
78
|
+
.selectAll()
|
|
79
|
+
.where("id", "=", costSummary.productionOrderId)
|
|
80
|
+
.forUpdate()
|
|
81
|
+
.executeTakeFirst();
|
|
82
|
+
|
|
83
|
+
if (productionOrder?.status !== "TECHNICALLY_COMPLETE") {
|
|
84
|
+
return err(new ParentOrderNotTechnicallyCompleteError(costSummaryId));
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// 5. Validate variance breakdown covers all required categories
|
|
88
|
+
if (!varianceBreakdown || !Array.isArray(varianceBreakdown)) {
|
|
89
|
+
return err(new IncompleteVarianceBreakdownError(costSummaryId));
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const providedTypes = new Set(varianceBreakdown.map((v) => v.varianceType));
|
|
93
|
+
const missingTypes = REQUIRED_VARIANCE_TYPES.filter((t) => !providedTypes.has(t));
|
|
94
|
+
|
|
95
|
+
if (missingTypes.length > 0) {
|
|
96
|
+
return err(new IncompleteVarianceBreakdownError(costSummaryId));
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// 6. Validate that variance amounts are consistent (no NaN or undefined)
|
|
100
|
+
for (const entry of varianceBreakdown) {
|
|
101
|
+
if (typeof entry.amount !== "number" || isNaN(entry.amount)) {
|
|
102
|
+
return err(new VarianceCalculationFailedError(costSummaryId));
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// 7. Create cost variance lines for each breakdown entry
|
|
107
|
+
for (const entry of varianceBreakdown) {
|
|
108
|
+
await db
|
|
109
|
+
.insertInto("CostVarianceLine")
|
|
110
|
+
.values({
|
|
111
|
+
costSummaryId: costSummary.id,
|
|
112
|
+
varianceType: entry.varianceType,
|
|
113
|
+
amount: entry.amount,
|
|
114
|
+
accountReference: entry.accountReference ?? null,
|
|
115
|
+
variancePercentage: entry.variancePercentage ?? null,
|
|
116
|
+
createdAt: new Date(),
|
|
117
|
+
updatedAt: null,
|
|
118
|
+
})
|
|
119
|
+
.returningAll()
|
|
120
|
+
.executeTakeFirst();
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// 8. Update summary to VARIANCE_REVIEWED
|
|
124
|
+
const updatedSummary = await db
|
|
125
|
+
.updateTable("ManufacturingCostSummary")
|
|
126
|
+
.set({
|
|
127
|
+
status: "VARIANCE_REVIEWED",
|
|
128
|
+
reviewedDate: new Date(),
|
|
129
|
+
reviewerNotes: reviewerNotes ?? null,
|
|
130
|
+
updatedAt: new Date(),
|
|
131
|
+
})
|
|
132
|
+
.where("id", "=", costSummary.id)
|
|
133
|
+
.returningAll()
|
|
134
|
+
.executeTakeFirst();
|
|
135
|
+
|
|
136
|
+
return ok({ costSummary: updatedSummary! });
|
|
137
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
// @generated — do not edit
|
|
2
|
+
import { permissions } from "../lib/permissions.generated";
|
|
3
|
+
import { run } from "./startWorkOrder";
|
|
4
|
+
import { defineCommand } from "@tailor-platform/erp-kit/module";
|
|
5
|
+
|
|
6
|
+
export const startWorkOrder = defineCommand(permissions.startWorkOrder, 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
|
+
WorkOrderNotStartableError,
|
|
7
|
+
ParentOrderNotExecutableError,
|
|
8
|
+
OperationSequenceBlockedError,
|
|
9
|
+
} from "../lib/errors.generated";
|
|
10
|
+
import {
|
|
11
|
+
basePendingWorkOrder,
|
|
12
|
+
baseInProgressWorkOrder,
|
|
13
|
+
baseReleasedProductionOrder,
|
|
14
|
+
baseDraftProductionOrder,
|
|
15
|
+
} from "../testing/fixtures";
|
|
16
|
+
import { run } from "./startWorkOrder";
|
|
17
|
+
import type { CommandContext } from "@tailor-platform/erp-kit/module";
|
|
18
|
+
|
|
19
|
+
describe("startWorkOrder", () => {
|
|
20
|
+
const ctx: CommandContext = {
|
|
21
|
+
actorId: "test-actor",
|
|
22
|
+
permissions: ["manufacturing:startWorkOrder"],
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
it("starts a pending work order", async () => {
|
|
26
|
+
const { db, spies } = createMockDb<Transaction>();
|
|
27
|
+
const startedWorkOrder = {
|
|
28
|
+
...basePendingWorkOrder,
|
|
29
|
+
status: "IN_PROGRESS" as const,
|
|
30
|
+
actualStartDate: new Date(),
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
// work order lookup
|
|
34
|
+
spies.select.mockReturnValueOnce(basePendingWorkOrder);
|
|
35
|
+
// parent production order lookup
|
|
36
|
+
spies.select.mockReturnValueOnce(baseReleasedProductionOrder);
|
|
37
|
+
// sibling work orders (no predecessors)
|
|
38
|
+
spies.select.mockReturnValueOnce([basePendingWorkOrder]);
|
|
39
|
+
// update work order
|
|
40
|
+
spies.update.mockReturnValueOnce(startedWorkOrder);
|
|
41
|
+
// insert execution event
|
|
42
|
+
spies.insert.mockReturnValueOnce(undefined);
|
|
43
|
+
// update parent production order to IN_PROGRESS
|
|
44
|
+
spies.update.mockReturnValueOnce(undefined);
|
|
45
|
+
|
|
46
|
+
const result = await run(db, { id: basePendingWorkOrder.id }, ctx);
|
|
47
|
+
|
|
48
|
+
expect(result.ok).toBe(true);
|
|
49
|
+
if (result.ok) {
|
|
50
|
+
expect(result.value.workOrder.status).toBe("IN_PROGRESS");
|
|
51
|
+
}
|
|
52
|
+
expect(spies.update).toHaveBeenCalled();
|
|
53
|
+
expect(spies.insert).toHaveBeenCalled();
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it("returns error when the work order does not exist", async () => {
|
|
57
|
+
const { db, spies } = createMockDb<Transaction>();
|
|
58
|
+
spies.select.mockReturnValueOnce(undefined);
|
|
59
|
+
|
|
60
|
+
const result = await run(db, { id: "nonexistent" }, ctx);
|
|
61
|
+
|
|
62
|
+
expect(result.ok).toBe(false);
|
|
63
|
+
if (!result.ok) {
|
|
64
|
+
expect(result.error).toBeInstanceOf(WorkOrderNotFoundError);
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it("returns error when the work order is not pending", async () => {
|
|
69
|
+
const { db, spies } = createMockDb<Transaction>();
|
|
70
|
+
spies.select.mockReturnValueOnce(baseInProgressWorkOrder);
|
|
71
|
+
|
|
72
|
+
const result = await run(db, { id: baseInProgressWorkOrder.id }, ctx);
|
|
73
|
+
|
|
74
|
+
expect(result.ok).toBe(false);
|
|
75
|
+
if (!result.ok) {
|
|
76
|
+
expect(result.error).toBeInstanceOf(WorkOrderNotStartableError);
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it("returns error when the parent production order is not executable", async () => {
|
|
81
|
+
const { db, spies } = createMockDb<Transaction>();
|
|
82
|
+
|
|
83
|
+
// work order lookup
|
|
84
|
+
spies.select.mockReturnValueOnce(basePendingWorkOrder);
|
|
85
|
+
// parent production order - DRAFT is not executable
|
|
86
|
+
spies.select.mockReturnValueOnce(baseDraftProductionOrder);
|
|
87
|
+
|
|
88
|
+
const result = await run(db, { id: basePendingWorkOrder.id }, ctx);
|
|
89
|
+
|
|
90
|
+
expect(result.ok).toBe(false);
|
|
91
|
+
if (!result.ok) {
|
|
92
|
+
expect(result.error).toBeInstanceOf(ParentOrderNotExecutableError);
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it("returns error when required predecessor work is incomplete", async () => {
|
|
97
|
+
const { db, spies } = createMockDb<Transaction>();
|
|
98
|
+
const secondWorkOrder = {
|
|
99
|
+
...basePendingWorkOrder,
|
|
100
|
+
id: "work-order-second",
|
|
101
|
+
routingOperationSequenceNumber: 20,
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
// work order lookup (second operation)
|
|
105
|
+
spies.select.mockReturnValueOnce(secondWorkOrder);
|
|
106
|
+
// parent production order
|
|
107
|
+
spies.select.mockReturnValueOnce(baseReleasedProductionOrder);
|
|
108
|
+
// sibling work orders - first is still PENDING
|
|
109
|
+
spies.select.mockReturnValueOnce([basePendingWorkOrder, secondWorkOrder]);
|
|
110
|
+
|
|
111
|
+
const result = await run(db, { id: secondWorkOrder.id }, ctx);
|
|
112
|
+
|
|
113
|
+
expect(result.ok).toBe(false);
|
|
114
|
+
if (!result.ok) {
|
|
115
|
+
expect(result.error).toBeInstanceOf(OperationSequenceBlockedError);
|
|
116
|
+
}
|
|
117
|
+
});
|
|
118
|
+
});
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import type { Transaction } from "../generated/kysely-tailordb";
|
|
2
|
+
import {
|
|
3
|
+
WorkOrderNotFoundError,
|
|
4
|
+
WorkOrderNotStartableError,
|
|
5
|
+
ParentOrderNotExecutableError,
|
|
6
|
+
OperationSequenceBlockedError,
|
|
7
|
+
} from "../lib/errors.generated";
|
|
8
|
+
import { ok, err, type CommandContext } from "@tailor-platform/erp-kit/module";
|
|
9
|
+
|
|
10
|
+
const EXECUTABLE_ORDER_STATUSES = ["RELEASED", "IN_PROGRESS"] as const;
|
|
11
|
+
|
|
12
|
+
export interface StartWorkOrderInput {
|
|
13
|
+
id: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Function: startWorkOrder
|
|
18
|
+
*
|
|
19
|
+
* Begins execution on a pending work order. Records the actual start timestamp,
|
|
20
|
+
* moves the work order to IN_PROGRESS, and transitions the parent production
|
|
21
|
+
* order to IN_PROGRESS if it was RELEASED.
|
|
22
|
+
*/
|
|
23
|
+
export async function run<CF extends Record<string, unknown>>(
|
|
24
|
+
db: Transaction,
|
|
25
|
+
input: StartWorkOrderInput & CF,
|
|
26
|
+
_ctx: CommandContext,
|
|
27
|
+
) {
|
|
28
|
+
const { id, ...customFields } = input;
|
|
29
|
+
void customFields;
|
|
30
|
+
|
|
31
|
+
// 1. Fetch work order with lock
|
|
32
|
+
const workOrder = await db
|
|
33
|
+
.selectFrom("WorkOrder")
|
|
34
|
+
.selectAll()
|
|
35
|
+
.where("id", "=", id)
|
|
36
|
+
.forUpdate()
|
|
37
|
+
.executeTakeFirst();
|
|
38
|
+
|
|
39
|
+
if (!workOrder) {
|
|
40
|
+
return err(new WorkOrderNotFoundError(id));
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// 2. Validate status is PENDING
|
|
44
|
+
if (workOrder.status !== "PENDING") {
|
|
45
|
+
return err(new WorkOrderNotStartableError(id));
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// 3. Check parent production order is in an execution-capable state
|
|
49
|
+
const parentOrder = await db
|
|
50
|
+
.selectFrom("ProductionOrder")
|
|
51
|
+
.selectAll()
|
|
52
|
+
.where("id", "=", workOrder.productionOrderId)
|
|
53
|
+
.forUpdate()
|
|
54
|
+
.executeTakeFirst();
|
|
55
|
+
|
|
56
|
+
if (
|
|
57
|
+
!parentOrder ||
|
|
58
|
+
!EXECUTABLE_ORDER_STATUSES.includes(
|
|
59
|
+
parentOrder.status as (typeof EXECUTABLE_ORDER_STATUSES)[number],
|
|
60
|
+
)
|
|
61
|
+
) {
|
|
62
|
+
return err(new ParentOrderNotExecutableError(id));
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// 4. Check preceding operations are complete (sequence order guard)
|
|
66
|
+
const siblingWorkOrders = await db
|
|
67
|
+
.selectFrom("WorkOrder")
|
|
68
|
+
.selectAll()
|
|
69
|
+
.where("productionOrderId", "=", workOrder.productionOrderId)
|
|
70
|
+
.execute();
|
|
71
|
+
|
|
72
|
+
const hasPendingPredecessor = siblingWorkOrders.some(
|
|
73
|
+
(wo) =>
|
|
74
|
+
wo.routingOperationSequenceNumber < workOrder.routingOperationSequenceNumber &&
|
|
75
|
+
wo.status !== "COMPLETE" &&
|
|
76
|
+
wo.status !== "CANCELLED",
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
if (hasPendingPredecessor) {
|
|
80
|
+
return err(new OperationSequenceBlockedError(id));
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// 5. Record actual start and set IN_PROGRESS
|
|
84
|
+
const now = new Date();
|
|
85
|
+
|
|
86
|
+
const updatedWorkOrder = await db
|
|
87
|
+
.updateTable("WorkOrder")
|
|
88
|
+
.set({
|
|
89
|
+
status: "IN_PROGRESS",
|
|
90
|
+
actualStartDate: now,
|
|
91
|
+
updatedAt: now,
|
|
92
|
+
})
|
|
93
|
+
.where("id", "=", id)
|
|
94
|
+
.returningAll()
|
|
95
|
+
.executeTakeFirstOrThrow();
|
|
96
|
+
|
|
97
|
+
// 6. Create STARTED execution event
|
|
98
|
+
await db
|
|
99
|
+
.insertInto("WorkOrderExecutionEvent")
|
|
100
|
+
.values({
|
|
101
|
+
workOrderId: id,
|
|
102
|
+
eventType: "STARTED",
|
|
103
|
+
timestamp: now,
|
|
104
|
+
quantity: null,
|
|
105
|
+
timeValue: null,
|
|
106
|
+
scrapValue: null,
|
|
107
|
+
notes: null,
|
|
108
|
+
createdAt: now,
|
|
109
|
+
updatedAt: null,
|
|
110
|
+
})
|
|
111
|
+
.execute();
|
|
112
|
+
|
|
113
|
+
// 7. If parent production order is RELEASED, transition to IN_PROGRESS
|
|
114
|
+
if (parentOrder.status === "RELEASED") {
|
|
115
|
+
await db
|
|
116
|
+
.updateTable("ProductionOrder")
|
|
117
|
+
.set({
|
|
118
|
+
status: "IN_PROGRESS",
|
|
119
|
+
updatedAt: now,
|
|
120
|
+
})
|
|
121
|
+
.where("id", "=", parentOrder.id)
|
|
122
|
+
.execute();
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return ok({ workOrder: updatedWorkOrder });
|
|
126
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
// @generated — do not edit
|
|
2
|
+
import { permissions } from "../lib/permissions.generated";
|
|
3
|
+
import { run } from "./technicallyCompleteProductionOrder";
|
|
4
|
+
import { defineCommand } from "@tailor-platform/erp-kit/module";
|
|
5
|
+
|
|
6
|
+
export const technicallyCompleteProductionOrder = defineCommand(permissions.technicallyCompleteProductionOrder, run);
|