@tailor-platform/erp-kit 0.5.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +24 -0
- package/dist/cli.mjs +139 -35
- package/package.json +1 -1
- package/skills/erp-kit-app-5-impl-backend/SKILL.md +10 -5
- package/skills/erp-kit-app-7-impl-review/SKILL.md +1 -1
- package/skills/erp-kit-module-6-impl-review/SKILL.md +39 -17
- package/src/commands/generate-doc.ts +1 -1
- package/src/commands/init-module.test.ts +17 -3
- package/src/commands/init-module.ts +0 -12
- package/src/commands/lib/discovery.test.ts +13 -3
- package/src/commands/lib/discovery.ts +10 -2
- package/src/commands/lib/paths.ts +4 -2
- package/src/commands/lib/sync-check-tests.test.ts +84 -6
- package/src/commands/lib/sync-check-tests.ts +63 -3
- package/src/commands/sync-check.ts +7 -3
- package/src/generator/generate-app-code.ts +51 -16
- package/src/generator/generate-code-boilerplate.test.ts +9 -1
- package/src/generator/generate-stubs.ts +4 -0
- package/src/generator/scaffold.ts +6 -2
- package/src/generator/stub-templates.test.ts +11 -0
- package/src/generator/stub-templates.ts +22 -1
- package/src/mdschema.ts +39 -3
- package/src/modules/inventory/docs/features/inventory-adjustment.md +2 -1
- package/src/modules/inventory/docs/features/scrap-management.md +39 -1
- package/src/modules/manufacturing/README.md +63 -0
- package/src/modules/manufacturing/command/activateBillOfMaterial.generated.ts +6 -0
- package/src/modules/manufacturing/command/activateBillOfMaterial.test.ts +166 -0
- package/src/modules/manufacturing/command/activateBillOfMaterial.ts +173 -0
- package/src/modules/manufacturing/command/activateRouting.generated.ts +6 -0
- package/src/modules/manufacturing/command/activateRouting.test.ts +152 -0
- package/src/modules/manufacturing/command/activateRouting.ts +92 -0
- package/src/modules/manufacturing/command/activateWorkCenter.generated.ts +6 -0
- package/src/modules/manufacturing/command/activateWorkCenter.test.ts +135 -0
- package/src/modules/manufacturing/command/activateWorkCenter.ts +91 -0
- package/src/modules/manufacturing/command/cancelProductionOrder.generated.ts +6 -0
- package/src/modules/manufacturing/command/cancelProductionOrder.test.ts +151 -0
- package/src/modules/manufacturing/command/cancelProductionOrder.ts +114 -0
- package/src/modules/manufacturing/command/closeProductionOrder.generated.ts +6 -0
- package/src/modules/manufacturing/command/closeProductionOrder.test.ts +126 -0
- package/src/modules/manufacturing/command/closeProductionOrder.ts +87 -0
- package/src/modules/manufacturing/command/completeProductionOrder.generated.ts +6 -0
- package/src/modules/manufacturing/command/completeProductionOrder.test.ts +132 -0
- package/src/modules/manufacturing/command/completeProductionOrder.ts +97 -0
- package/src/modules/manufacturing/command/completeWorkOrder.generated.ts +6 -0
- package/src/modules/manufacturing/command/completeWorkOrder.test.ts +369 -0
- package/src/modules/manufacturing/command/completeWorkOrder.ts +212 -0
- package/src/modules/manufacturing/command/createBillOfMaterial.generated.ts +6 -0
- package/src/modules/manufacturing/command/createBillOfMaterial.test.ts +210 -0
- package/src/modules/manufacturing/command/createBillOfMaterial.ts +176 -0
- package/src/modules/manufacturing/command/createProductionOrder.generated.ts +6 -0
- package/src/modules/manufacturing/command/createProductionOrder.test.ts +160 -0
- package/src/modules/manufacturing/command/createProductionOrder.ts +129 -0
- package/src/modules/manufacturing/command/createRouting.generated.ts +6 -0
- package/src/modules/manufacturing/command/createRouting.test.ts +168 -0
- package/src/modules/manufacturing/command/createRouting.ts +128 -0
- package/src/modules/manufacturing/command/createWorkCenter.generated.ts +6 -0
- package/src/modules/manufacturing/command/createWorkCenter.test.ts +148 -0
- package/src/modules/manufacturing/command/createWorkCenter.ts +131 -0
- package/src/modules/manufacturing/command/deactivateBillOfMaterial.generated.ts +6 -0
- package/src/modules/manufacturing/command/deactivateBillOfMaterial.test.ts +103 -0
- package/src/modules/manufacturing/command/deactivateBillOfMaterial.ts +78 -0
- package/src/modules/manufacturing/command/deactivateRouting.generated.ts +6 -0
- package/src/modules/manufacturing/command/deactivateRouting.test.ts +112 -0
- package/src/modules/manufacturing/command/deactivateRouting.ts +76 -0
- package/src/modules/manufacturing/command/deactivateWorkCenter.generated.ts +6 -0
- package/src/modules/manufacturing/command/deactivateWorkCenter.test.ts +113 -0
- package/src/modules/manufacturing/command/deactivateWorkCenter.ts +85 -0
- package/src/modules/manufacturing/command/pauseWorkOrder.generated.ts +6 -0
- package/src/modules/manufacturing/command/pauseWorkOrder.test.ts +118 -0
- package/src/modules/manufacturing/command/pauseWorkOrder.ts +82 -0
- package/src/modules/manufacturing/command/recordInventoryIssueOutcome.generated.ts +6 -0
- package/src/modules/manufacturing/command/recordInventoryIssueOutcome.test.ts +183 -0
- package/src/modules/manufacturing/command/recordInventoryIssueOutcome.ts +139 -0
- package/src/modules/manufacturing/command/recordManufacturingCostSettlementAcknowledgment.generated.ts +6 -0
- package/src/modules/manufacturing/command/recordManufacturingCostSettlementAcknowledgment.test.ts +120 -0
- package/src/modules/manufacturing/command/recordManufacturingCostSettlementAcknowledgment.ts +110 -0
- package/src/modules/manufacturing/command/releaseProductionOrder.generated.ts +6 -0
- package/src/modules/manufacturing/command/releaseProductionOrder.test.ts +220 -0
- package/src/modules/manufacturing/command/releaseProductionOrder.ts +450 -0
- package/src/modules/manufacturing/command/reopenProductionOrder.generated.ts +6 -0
- package/src/modules/manufacturing/command/reopenProductionOrder.test.ts +196 -0
- package/src/modules/manufacturing/command/reopenProductionOrder.ts +98 -0
- package/src/modules/manufacturing/command/reportWorkOrderProgress.generated.ts +6 -0
- package/src/modules/manufacturing/command/reportWorkOrderProgress.test.ts +204 -0
- package/src/modules/manufacturing/command/reportWorkOrderProgress.ts +129 -0
- package/src/modules/manufacturing/command/rescheduleProductionOrder.generated.ts +6 -0
- package/src/modules/manufacturing/command/rescheduleProductionOrder.test.ts +185 -0
- package/src/modules/manufacturing/command/rescheduleProductionOrder.ts +95 -0
- package/src/modules/manufacturing/command/resumeWorkOrder.generated.ts +6 -0
- package/src/modules/manufacturing/command/resumeWorkOrder.test.ts +122 -0
- package/src/modules/manufacturing/command/resumeWorkOrder.ts +94 -0
- package/src/modules/manufacturing/command/reviewManufacturingCostSummary.generated.ts +6 -0
- package/src/modules/manufacturing/command/reviewManufacturingCostSummary.test.ts +231 -0
- package/src/modules/manufacturing/command/reviewManufacturingCostSummary.ts +137 -0
- package/src/modules/manufacturing/command/startWorkOrder.generated.ts +6 -0
- package/src/modules/manufacturing/command/startWorkOrder.test.ts +118 -0
- package/src/modules/manufacturing/command/startWorkOrder.ts +126 -0
- package/src/modules/manufacturing/command/technicallyCompleteProductionOrder.generated.ts +6 -0
- package/src/modules/manufacturing/command/technicallyCompleteProductionOrder.test.ts +153 -0
- package/src/modules/manufacturing/command/technicallyCompleteProductionOrder.ts +106 -0
- package/src/modules/manufacturing/command/unreleaseProductionOrder.generated.ts +6 -0
- package/src/modules/manufacturing/command/unreleaseProductionOrder.test.ts +140 -0
- package/src/modules/manufacturing/command/unreleaseProductionOrder.ts +131 -0
- package/src/modules/manufacturing/command/updateBillOfMaterial.generated.ts +6 -0
- package/src/modules/manufacturing/command/updateBillOfMaterial.test.ts +149 -0
- package/src/modules/manufacturing/command/updateBillOfMaterial.ts +174 -0
- package/src/modules/manufacturing/command/updateProductionOrder.generated.ts +6 -0
- package/src/modules/manufacturing/command/updateProductionOrder.test.ts +112 -0
- package/src/modules/manufacturing/command/updateProductionOrder.ts +145 -0
- package/src/modules/manufacturing/command/updateRouting.generated.ts +6 -0
- package/src/modules/manufacturing/command/updateRouting.test.ts +211 -0
- package/src/modules/manufacturing/command/updateRouting.ts +124 -0
- package/src/modules/manufacturing/command/updateWorkCenter.generated.ts +6 -0
- package/src/modules/manufacturing/command/updateWorkCenter.test.ts +152 -0
- package/src/modules/manufacturing/command/updateWorkCenter.ts +137 -0
- package/src/modules/manufacturing/db/.gitkeep +0 -0
- package/src/modules/manufacturing/db/billOfMaterial.ts +70 -0
- package/src/modules/manufacturing/db/billOfMaterialLine.ts +49 -0
- package/src/modules/manufacturing/db/costVarianceLine.ts +53 -0
- package/src/modules/manufacturing/db/manufacturingCostLine.ts +35 -0
- package/src/modules/manufacturing/db/manufacturingCostSettlementRecord.ts +39 -0
- package/src/modules/manufacturing/db/manufacturingCostSummary.ts +59 -0
- package/src/modules/manufacturing/db/productionOrder.ts +83 -0
- package/src/modules/manufacturing/db/productionOrderBomSnapshot.ts +44 -0
- package/src/modules/manufacturing/db/productionOrderCostBaseline.ts +44 -0
- package/src/modules/manufacturing/db/productionOrderMaterialRequirement.ts +57 -0
- package/src/modules/manufacturing/db/productionOrderRoutingSnapshot.ts +43 -0
- package/src/modules/manufacturing/db/routing.ts +63 -0
- package/src/modules/manufacturing/db/routingOperation.ts +57 -0
- package/src/modules/manufacturing/db/workCenter.ts +87 -0
- package/src/modules/manufacturing/db/workOrder.ts +65 -0
- package/src/modules/manufacturing/db/workOrderExecutionEvent.ts +54 -0
- package/src/modules/manufacturing/docs/commands/ActivateBillOfMaterial.md +50 -0
- package/src/modules/manufacturing/docs/commands/ActivateRouting.md +48 -0
- package/src/modules/manufacturing/docs/commands/ActivateWorkCenter.md +49 -0
- package/src/modules/manufacturing/docs/commands/CancelProductionOrder.md +48 -0
- package/src/modules/manufacturing/docs/commands/CloseProductionOrder.md +46 -0
- package/src/modules/manufacturing/docs/commands/CompleteProductionOrder.md +48 -0
- package/src/modules/manufacturing/docs/commands/CompleteWorkOrder.md +66 -0
- package/src/modules/manufacturing/docs/commands/CreateBillOfMaterial.md +54 -0
- package/src/modules/manufacturing/docs/commands/CreateProductionOrder.md +49 -0
- package/src/modules/manufacturing/docs/commands/CreateRouting.md +50 -0
- package/src/modules/manufacturing/docs/commands/CreateWorkCenter.md +51 -0
- package/src/modules/manufacturing/docs/commands/DeactivateBillOfMaterial.md +45 -0
- package/src/modules/manufacturing/docs/commands/DeactivateRouting.md +45 -0
- package/src/modules/manufacturing/docs/commands/DeactivateWorkCenter.md +45 -0
- package/src/modules/manufacturing/docs/commands/PauseWorkOrder.md +44 -0
- package/src/modules/manufacturing/docs/commands/RecordInventoryIssueOutcome.md +59 -0
- package/src/modules/manufacturing/docs/commands/RecordManufacturingCostSettlementAcknowledgment.md +49 -0
- package/src/modules/manufacturing/docs/commands/ReleaseProductionOrder.md +57 -0
- package/src/modules/manufacturing/docs/commands/ReopenProductionOrder.md +54 -0
- package/src/modules/manufacturing/docs/commands/ReportWorkOrderProgress.md +53 -0
- package/src/modules/manufacturing/docs/commands/RescheduleProductionOrder.md +45 -0
- package/src/modules/manufacturing/docs/commands/ResumeWorkOrder.md +44 -0
- package/src/modules/manufacturing/docs/commands/ReviewManufacturingCostSummary.md +52 -0
- package/src/modules/manufacturing/docs/commands/StartWorkOrder.md +46 -0
- package/src/modules/manufacturing/docs/commands/TechnicallyCompleteProductionOrder.md +51 -0
- package/src/modules/manufacturing/docs/commands/UnreleaseProductionOrder.md +46 -0
- package/src/modules/manufacturing/docs/commands/UpdateBillOfMaterial.md +48 -0
- package/src/modules/manufacturing/docs/commands/UpdateProductionOrder.md +48 -0
- package/src/modules/manufacturing/docs/commands/UpdateRouting.md +52 -0
- package/src/modules/manufacturing/docs/commands/UpdateWorkCenter.md +48 -0
- package/src/modules/manufacturing/docs/features/bill-of-material-management.md +83 -0
- package/src/modules/manufacturing/docs/features/manufacturing-cost-and-variance.md +191 -0
- package/src/modules/manufacturing/docs/features/production-order-lifecycle.md +103 -0
- package/src/modules/manufacturing/docs/features/routing-and-work-center-definition.md +63 -0
- package/src/modules/manufacturing/docs/features/work-order-execution.md +115 -0
- package/src/modules/manufacturing/docs/models/BillOfMaterial.md +60 -0
- package/src/modules/manufacturing/docs/models/ManufacturingCostSummary.md +66 -0
- package/src/modules/manufacturing/docs/models/ProductionOrder.md +76 -0
- package/src/modules/manufacturing/docs/models/Routing.md +58 -0
- package/src/modules/manufacturing/docs/models/WorkCenter.md +56 -0
- package/src/modules/manufacturing/docs/models/WorkOrder.md +63 -0
- package/src/modules/manufacturing/docs/queries/DetectBillOfMaterialCircularReference.md +39 -0
- package/src/modules/manufacturing/docs/queries/ExplodeBillOfMaterial.md +56 -0
- package/src/modules/manufacturing/docs/queries/GetBillOfMaterial.md +37 -0
- package/src/modules/manufacturing/docs/queries/GetManufacturingCostSummary.md +39 -0
- package/src/modules/manufacturing/docs/queries/GetProductionOrder.md +37 -0
- package/src/modules/manufacturing/docs/queries/GetRouting.md +39 -0
- package/src/modules/manufacturing/docs/queries/GetWorkCenter.md +35 -0
- package/src/modules/manufacturing/docs/queries/GetWorkOrder.md +38 -0
- package/src/modules/manufacturing/docs/queries/ListBillOfMaterialsByItem.md +42 -0
- package/src/modules/manufacturing/docs/queries/ListManufacturingCostSummariesByStatus.md +41 -0
- package/src/modules/manufacturing/docs/queries/ListProductionOrdersByStatus.md +41 -0
- package/src/modules/manufacturing/docs/queries/ListRoutingsByItem.md +42 -0
- package/src/modules/manufacturing/docs/queries/ListWorkCentersBySite.md +38 -0
- package/src/modules/manufacturing/docs/queries/ListWorkOrdersByProductionOrder.md +39 -0
- package/src/modules/manufacturing/docs/queries/ListWorkOrdersByWorkCenter.md +43 -0
- package/src/modules/manufacturing/executor/.gitkeep +0 -0
- package/src/modules/manufacturing/generated/enums.ts +113 -0
- package/src/modules/manufacturing/generated/kysely-tailordb.ts +247 -0
- package/src/modules/manufacturing/index.ts +2 -0
- package/src/modules/manufacturing/lib/_db_deps.ts +22 -0
- package/src/modules/manufacturing/lib/errors.generated.ts +592 -0
- package/src/modules/manufacturing/lib/permissions.generated.ts +35 -0
- package/src/modules/manufacturing/lib/types.ts +111 -0
- package/src/modules/manufacturing/module.ts +226 -0
- package/src/modules/manufacturing/permissions.ts +3 -0
- package/src/modules/manufacturing/query/.gitkeep +0 -0
- package/src/modules/manufacturing/query/detectBillOfMaterialCircularReference.generated.ts +5 -0
- package/src/modules/manufacturing/query/detectBillOfMaterialCircularReference.test.ts +115 -0
- package/src/modules/manufacturing/query/detectBillOfMaterialCircularReference.ts +79 -0
- package/src/modules/manufacturing/query/explodeBillOfMaterial.generated.ts +5 -0
- package/src/modules/manufacturing/query/explodeBillOfMaterial.test.ts +445 -0
- package/src/modules/manufacturing/query/explodeBillOfMaterial.ts +306 -0
- package/src/modules/manufacturing/query/getBillOfMaterial.generated.ts +5 -0
- package/src/modules/manufacturing/query/getBillOfMaterial.test.ts +64 -0
- package/src/modules/manufacturing/query/getBillOfMaterial.ts +27 -0
- package/src/modules/manufacturing/query/getManufacturingCostSummary.generated.ts +5 -0
- package/src/modules/manufacturing/query/getManufacturingCostSummary.test.ts +147 -0
- package/src/modules/manufacturing/query/getManufacturingCostSummary.ts +46 -0
- package/src/modules/manufacturing/query/getProductionOrder.generated.ts +5 -0
- package/src/modules/manufacturing/query/getProductionOrder.test.ts +139 -0
- package/src/modules/manufacturing/query/getProductionOrder.ts +84 -0
- package/src/modules/manufacturing/query/getRouting.generated.ts +5 -0
- package/src/modules/manufacturing/query/getRouting.test.ts +71 -0
- package/src/modules/manufacturing/query/getRouting.ts +34 -0
- package/src/modules/manufacturing/query/getWorkCenter.generated.ts +5 -0
- package/src/modules/manufacturing/query/getWorkCenter.test.ts +37 -0
- package/src/modules/manufacturing/query/getWorkCenter.ts +21 -0
- package/src/modules/manufacturing/query/getWorkOrder.generated.ts +5 -0
- package/src/modules/manufacturing/query/getWorkOrder.test.ts +73 -0
- package/src/modules/manufacturing/query/getWorkOrder.ts +28 -0
- package/src/modules/manufacturing/query/listBillOfMaterialsByItem.generated.ts +5 -0
- package/src/modules/manufacturing/query/listBillOfMaterialsByItem.test.ts +107 -0
- package/src/modules/manufacturing/query/listBillOfMaterialsByItem.ts +58 -0
- package/src/modules/manufacturing/query/listManufacturingCostSummariesByStatus.generated.ts +5 -0
- package/src/modules/manufacturing/query/listManufacturingCostSummariesByStatus.test.ts +96 -0
- package/src/modules/manufacturing/query/listManufacturingCostSummariesByStatus.ts +77 -0
- package/src/modules/manufacturing/query/listProductionOrdersByStatus.generated.ts +5 -0
- package/src/modules/manufacturing/query/listProductionOrdersByStatus.test.ts +121 -0
- package/src/modules/manufacturing/query/listProductionOrdersByStatus.ts +83 -0
- package/src/modules/manufacturing/query/listRoutingsByItem.generated.ts +5 -0
- package/src/modules/manufacturing/query/listRoutingsByItem.test.ts +110 -0
- package/src/modules/manufacturing/query/listRoutingsByItem.ts +54 -0
- package/src/modules/manufacturing/query/listWorkCentersBySite.generated.ts +5 -0
- package/src/modules/manufacturing/query/listWorkCentersBySite.test.ts +81 -0
- package/src/modules/manufacturing/query/listWorkCentersBySite.ts +70 -0
- package/src/modules/manufacturing/query/listWorkOrdersByProductionOrder.generated.ts +5 -0
- package/src/modules/manufacturing/query/listWorkOrdersByProductionOrder.test.ts +102 -0
- package/src/modules/manufacturing/query/listWorkOrdersByProductionOrder.ts +53 -0
- package/src/modules/manufacturing/query/listWorkOrdersByWorkCenter.generated.ts +5 -0
- package/src/modules/manufacturing/query/listWorkOrdersByWorkCenter.test.ts +143 -0
- package/src/modules/manufacturing/query/listWorkOrdersByWorkCenter.ts +56 -0
- package/src/modules/manufacturing/seed/index.ts +19 -0
- package/src/modules/manufacturing/tailor.config.ts +13 -0
- package/src/modules/manufacturing/tailor.d.ts +13 -0
- package/src/modules/manufacturing/testing/commandTestUtils.ts +29 -0
- package/src/modules/manufacturing/testing/fixtures.ts +402 -0
- package/templates/scaffold/app/backend/package.json +9 -2
- package/templates/scaffold/app/backend/src/tests/utils/graphql-client.ts +66 -0
- package/templates/scaffold/app/backend/src/tests/utils/setup.ts +21 -0
- package/templates/scaffold/app/backend/tsconfig.json +9 -2
- package/templates/scaffold/app/backend/vitest.config.ts +35 -0
- package/templates/scaffold/app/frontend/package.json +2 -2
- package/templates/scaffold/module/__dot__gitignore +3 -0
- package/templates/scaffold/module/eslint.config.js +31 -0
- package/templates/scaffold/module/generated/kysely-tailordb.ts +3 -0
- package/templates/scaffold/module/lib/types.ts +1 -6
- package/templates/scaffold/module/package.json +26 -0
- package/templates/scaffold/module/tsconfig.json +16 -0
- /package/{templates/scaffold/module/generated → src/modules/manufacturing/command}/.gitkeep +0 -0
|
@@ -0,0 +1,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);
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { createMockDb } from "../../../testing/index";
|
|
3
|
+
import type { Transaction } from "../generated/kysely-tailordb";
|
|
4
|
+
import {
|
|
5
|
+
ProductionOrderNotFoundError,
|
|
6
|
+
ProductionOrderNotTechnicallyCompletableError,
|
|
7
|
+
ExecutionExceptionRemainsError,
|
|
8
|
+
PendingMaterialIssueRequestsError,
|
|
9
|
+
CostSummaryNotReadyError,
|
|
10
|
+
} from "../lib/errors.generated";
|
|
11
|
+
import {
|
|
12
|
+
baseCompletedProductionOrder,
|
|
13
|
+
baseInProgressProductionOrder,
|
|
14
|
+
baseCompleteWorkOrder,
|
|
15
|
+
baseCancelledWorkOrder,
|
|
16
|
+
baseInProgressWorkOrder,
|
|
17
|
+
basePausedWorkOrder,
|
|
18
|
+
basePendingWorkOrder,
|
|
19
|
+
baseCollectingCostSummary,
|
|
20
|
+
basePendingReviewCostSummary,
|
|
21
|
+
} from "../testing/fixtures";
|
|
22
|
+
import { run } from "./technicallyCompleteProductionOrder";
|
|
23
|
+
import type { CommandContext } from "@tailor-platform/erp-kit/module";
|
|
24
|
+
|
|
25
|
+
describe("technicallyCompleteProductionOrder", () => {
|
|
26
|
+
const ctx: CommandContext = {
|
|
27
|
+
actorId: "test-actor",
|
|
28
|
+
permissions: ["manufacturing:technicallyCompleteProductionOrder"],
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
it("technically completes a completed order and opens variance review", async () => {
|
|
32
|
+
const { db, spies } = createMockDb<Transaction>();
|
|
33
|
+
const techComplete = {
|
|
34
|
+
...baseCompletedProductionOrder,
|
|
35
|
+
status: "TECHNICALLY_COMPLETE" as const,
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
// order lookup
|
|
39
|
+
spies.select.mockReturnValueOnce(baseCompletedProductionOrder);
|
|
40
|
+
// work orders - all complete or cancelled
|
|
41
|
+
spies.select.mockReturnValueOnce([baseCompleteWorkOrder, baseCancelledWorkOrder]);
|
|
42
|
+
// cost summary in COLLECTING
|
|
43
|
+
spies.select.mockReturnValueOnce({
|
|
44
|
+
...baseCollectingCostSummary,
|
|
45
|
+
productionOrderId: baseCompletedProductionOrder.id,
|
|
46
|
+
});
|
|
47
|
+
// update cost summary
|
|
48
|
+
spies.update.mockReturnValueOnce(undefined);
|
|
49
|
+
// update order status
|
|
50
|
+
spies.update.mockReturnValue(techComplete);
|
|
51
|
+
|
|
52
|
+
const result = await run(db, { id: baseCompletedProductionOrder.id }, ctx);
|
|
53
|
+
|
|
54
|
+
expect(result.ok).toBe(true);
|
|
55
|
+
if (result.ok) {
|
|
56
|
+
expect(result.value.productionOrder.status).toBe("TECHNICALLY_COMPLETE");
|
|
57
|
+
}
|
|
58
|
+
expect(spies.update).toHaveBeenCalled();
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it("returns error when the order does not exist", async () => {
|
|
62
|
+
const { db, spies } = createMockDb<Transaction>();
|
|
63
|
+
spies.select.mockReturnValueOnce(undefined);
|
|
64
|
+
|
|
65
|
+
const result = await run(db, { id: "nonexistent" }, ctx);
|
|
66
|
+
|
|
67
|
+
expect(result.ok).toBe(false);
|
|
68
|
+
if (!result.ok) {
|
|
69
|
+
expect(result.error).toBeInstanceOf(ProductionOrderNotFoundError);
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it("returns error when the order is not in COMPLETED", async () => {
|
|
74
|
+
const { db, spies } = createMockDb<Transaction>();
|
|
75
|
+
spies.select.mockReturnValueOnce(baseInProgressProductionOrder);
|
|
76
|
+
|
|
77
|
+
const result = await run(db, { id: baseInProgressProductionOrder.id }, ctx);
|
|
78
|
+
|
|
79
|
+
expect(result.ok).toBe(false);
|
|
80
|
+
if (!result.ok) {
|
|
81
|
+
expect(result.error).toBeInstanceOf(ProductionOrderNotTechnicallyCompletableError);
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it("returns error when unresolved execution exceptions remain", async () => {
|
|
86
|
+
const { db, spies } = createMockDb<Transaction>();
|
|
87
|
+
|
|
88
|
+
// order lookup
|
|
89
|
+
spies.select.mockReturnValueOnce(baseCompletedProductionOrder);
|
|
90
|
+
// work orders - one still in progress
|
|
91
|
+
spies.select.mockReturnValueOnce([baseInProgressWorkOrder]);
|
|
92
|
+
|
|
93
|
+
const result = await run(db, { id: baseCompletedProductionOrder.id }, ctx);
|
|
94
|
+
|
|
95
|
+
expect(result.ok).toBe(false);
|
|
96
|
+
if (!result.ok) {
|
|
97
|
+
expect(result.error).toBeInstanceOf(ExecutionExceptionRemainsError);
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it("returns error when pending material issue requests or unresolved backflush intents remain", async () => {
|
|
102
|
+
const { db, spies } = createMockDb<Transaction>();
|
|
103
|
+
|
|
104
|
+
// order lookup
|
|
105
|
+
spies.select.mockReturnValueOnce(baseCompletedProductionOrder);
|
|
106
|
+
// work orders - one still pending
|
|
107
|
+
spies.select.mockReturnValueOnce([baseCompleteWorkOrder, basePendingWorkOrder]);
|
|
108
|
+
|
|
109
|
+
const result = await run(db, { id: baseCompletedProductionOrder.id }, ctx);
|
|
110
|
+
|
|
111
|
+
expect(result.ok).toBe(false);
|
|
112
|
+
if (!result.ok) {
|
|
113
|
+
expect(result.error).toBeInstanceOf(PendingMaterialIssueRequestsError);
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it("returns error when the linked cost summary is not ready for review", async () => {
|
|
118
|
+
const { db, spies } = createMockDb<Transaction>();
|
|
119
|
+
|
|
120
|
+
// order lookup
|
|
121
|
+
spies.select.mockReturnValueOnce(baseCompletedProductionOrder);
|
|
122
|
+
// work orders - all complete
|
|
123
|
+
spies.select.mockReturnValueOnce([baseCompleteWorkOrder]);
|
|
124
|
+
// cost summary already in PENDING_VARIANCE_REVIEW (not COLLECTING)
|
|
125
|
+
spies.select.mockReturnValueOnce({
|
|
126
|
+
...basePendingReviewCostSummary,
|
|
127
|
+
productionOrderId: baseCompletedProductionOrder.id,
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
const result = await run(db, { id: baseCompletedProductionOrder.id }, ctx);
|
|
131
|
+
|
|
132
|
+
expect(result.ok).toBe(false);
|
|
133
|
+
if (!result.ok) {
|
|
134
|
+
expect(result.error).toBeInstanceOf(CostSummaryNotReadyError);
|
|
135
|
+
}
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it("blocks further shop-floor reporting after technical completion", async () => {
|
|
139
|
+
const { db, spies } = createMockDb<Transaction>();
|
|
140
|
+
|
|
141
|
+
// order lookup - already paused work order still open
|
|
142
|
+
spies.select.mockReturnValueOnce(baseCompletedProductionOrder);
|
|
143
|
+
// work orders - one paused (execution exception)
|
|
144
|
+
spies.select.mockReturnValueOnce([basePausedWorkOrder]);
|
|
145
|
+
|
|
146
|
+
const result = await run(db, { id: baseCompletedProductionOrder.id }, ctx);
|
|
147
|
+
|
|
148
|
+
expect(result.ok).toBe(false);
|
|
149
|
+
if (!result.ok) {
|
|
150
|
+
expect(result.error).toBeInstanceOf(ExecutionExceptionRemainsError);
|
|
151
|
+
}
|
|
152
|
+
});
|
|
153
|
+
});
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import type { Transaction } from "../generated/kysely-tailordb";
|
|
2
|
+
import {
|
|
3
|
+
ProductionOrderNotFoundError,
|
|
4
|
+
ProductionOrderNotTechnicallyCompletableError,
|
|
5
|
+
ExecutionExceptionRemainsError,
|
|
6
|
+
PendingMaterialIssueRequestsError,
|
|
7
|
+
CostSummaryNotReadyError,
|
|
8
|
+
} from "../lib/errors.generated";
|
|
9
|
+
import { ok, err, type CommandContext } from "@tailor-platform/erp-kit/module";
|
|
10
|
+
|
|
11
|
+
export interface TechnicallyCompleteProductionOrderInput {
|
|
12
|
+
id: string;
|
|
13
|
+
from?: string[];
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Function: technicallyCompleteProductionOrder
|
|
18
|
+
*
|
|
19
|
+
* Freezes the production order after physical completion and moves the linked
|
|
20
|
+
* manufacturing cost summary into variance review. Marks the point where no
|
|
21
|
+
* more normal execution or rescheduling is expected.
|
|
22
|
+
*/
|
|
23
|
+
export async function run<CF extends Record<string, unknown>>(
|
|
24
|
+
db: Transaction,
|
|
25
|
+
input: TechnicallyCompleteProductionOrderInput & CF,
|
|
26
|
+
_ctx: CommandContext,
|
|
27
|
+
) {
|
|
28
|
+
const { id, from, ...customFields } = input;
|
|
29
|
+
void customFields;
|
|
30
|
+
|
|
31
|
+
const allowedStatuses = from ?? ["COMPLETED"];
|
|
32
|
+
|
|
33
|
+
// 1. Fetch production order with lock
|
|
34
|
+
const order = await db
|
|
35
|
+
.selectFrom("ProductionOrder")
|
|
36
|
+
.selectAll()
|
|
37
|
+
.where("id", "=", id)
|
|
38
|
+
.forUpdate()
|
|
39
|
+
.executeTakeFirst();
|
|
40
|
+
|
|
41
|
+
if (!order) {
|
|
42
|
+
return err(new ProductionOrderNotFoundError(id));
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// 2. Validate status
|
|
46
|
+
if (!allowedStatuses.includes(order.status)) {
|
|
47
|
+
return err(new ProductionOrderNotTechnicallyCompletableError(id));
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// 3. Check for open execution exceptions (work orders not COMPLETE or CANCELLED)
|
|
51
|
+
const workOrders = await db
|
|
52
|
+
.selectFrom("WorkOrder")
|
|
53
|
+
.selectAll()
|
|
54
|
+
.where("productionOrderId", "=", id)
|
|
55
|
+
.execute();
|
|
56
|
+
|
|
57
|
+
const hasExecutionExceptions = workOrders.some(
|
|
58
|
+
(wo) => wo.status === "IN_PROGRESS" || wo.status === "PAUSED",
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
if (hasExecutionExceptions) {
|
|
62
|
+
return err(new ExecutionExceptionRemainsError(id));
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// 4. Check for pending material issue requests (work orders still PENDING)
|
|
66
|
+
const hasPendingWork = workOrders.some((wo) => wo.status === "PENDING");
|
|
67
|
+
|
|
68
|
+
if (hasPendingWork) {
|
|
69
|
+
return err(new PendingMaterialIssueRequestsError(id));
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// 5. Find and validate cost summary
|
|
73
|
+
const costSummary = await db
|
|
74
|
+
.selectFrom("ManufacturingCostSummary")
|
|
75
|
+
.selectAll()
|
|
76
|
+
.where("productionOrderId", "=", id)
|
|
77
|
+
.forUpdate()
|
|
78
|
+
.executeTakeFirst();
|
|
79
|
+
|
|
80
|
+
if (costSummary?.status !== "COLLECTING") {
|
|
81
|
+
return err(new CostSummaryNotReadyError(id));
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// 6. Move cost summary to PENDING_VARIANCE_REVIEW
|
|
85
|
+
await db
|
|
86
|
+
.updateTable("ManufacturingCostSummary")
|
|
87
|
+
.set({
|
|
88
|
+
status: "PENDING_VARIANCE_REVIEW",
|
|
89
|
+
updatedAt: new Date(),
|
|
90
|
+
})
|
|
91
|
+
.where("id", "=", costSummary.id)
|
|
92
|
+
.execute();
|
|
93
|
+
|
|
94
|
+
// 7. Set order status to TECHNICALLY_COMPLETE
|
|
95
|
+
const techCompleteOrder = await db
|
|
96
|
+
.updateTable("ProductionOrder")
|
|
97
|
+
.set({
|
|
98
|
+
status: "TECHNICALLY_COMPLETE",
|
|
99
|
+
updatedAt: new Date(),
|
|
100
|
+
})
|
|
101
|
+
.where("id", "=", id)
|
|
102
|
+
.returningAll()
|
|
103
|
+
.executeTakeFirstOrThrow();
|
|
104
|
+
|
|
105
|
+
return ok({ productionOrder: techCompleteOrder });
|
|
106
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
// @generated — do not edit
|
|
2
|
+
import { permissions } from "../lib/permissions.generated";
|
|
3
|
+
import { run } from "./unreleaseProductionOrder";
|
|
4
|
+
import { defineCommand } from "@tailor-platform/erp-kit/module";
|
|
5
|
+
|
|
6
|
+
export const unreleaseProductionOrder = defineCommand(permissions.unreleaseProductionOrder, run);
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { createMockDb } from "../../../testing/index";
|
|
3
|
+
import type { Transaction } from "../generated/kysely-tailordb";
|
|
4
|
+
import {
|
|
5
|
+
ProductionOrderNotFoundError,
|
|
6
|
+
ProductionOrderNotUnreleasableError,
|
|
7
|
+
ExecutionAlreadyStartedError,
|
|
8
|
+
InventoryHandoffExistsError,
|
|
9
|
+
} from "../lib/errors.generated";
|
|
10
|
+
import {
|
|
11
|
+
baseDraftProductionOrder,
|
|
12
|
+
baseReleasedProductionOrder,
|
|
13
|
+
basePendingWorkOrder,
|
|
14
|
+
baseInProgressWorkOrder,
|
|
15
|
+
baseMaterialRequirement,
|
|
16
|
+
baseCollectingCostSummary,
|
|
17
|
+
} from "../testing/fixtures";
|
|
18
|
+
import { run } from "./unreleaseProductionOrder";
|
|
19
|
+
import type { CommandContext } from "@tailor-platform/erp-kit/module";
|
|
20
|
+
|
|
21
|
+
describe("unreleaseProductionOrder", () => {
|
|
22
|
+
const ctx: CommandContext = {
|
|
23
|
+
actorId: "test-actor",
|
|
24
|
+
permissions: ["manufacturing:unreleaseProductionOrder"],
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
it("unreleases a released order with no execution evidence", async () => {
|
|
28
|
+
const { db, spies } = createMockDb<Transaction>();
|
|
29
|
+
const draftOrder = { ...baseReleasedProductionOrder, status: "DRAFT" as const };
|
|
30
|
+
|
|
31
|
+
// order lookup
|
|
32
|
+
spies.select.mockReturnValueOnce(baseReleasedProductionOrder);
|
|
33
|
+
// work orders - all pending
|
|
34
|
+
spies.select.mockReturnValueOnce([basePendingWorkOrder]);
|
|
35
|
+
// material requirements
|
|
36
|
+
spies.select.mockReturnValueOnce([baseMaterialRequirement]);
|
|
37
|
+
// cost summary - no actuals
|
|
38
|
+
spies.select.mockReturnValueOnce(baseCollectingCostSummary);
|
|
39
|
+
// delete operations return
|
|
40
|
+
spies.delete.mockReturnValue(undefined);
|
|
41
|
+
// update order status
|
|
42
|
+
spies.update.mockReturnValue(draftOrder);
|
|
43
|
+
|
|
44
|
+
const result = await run(db, { id: baseReleasedProductionOrder.id }, ctx);
|
|
45
|
+
|
|
46
|
+
expect(result.ok).toBe(true);
|
|
47
|
+
if (result.ok) {
|
|
48
|
+
expect(result.value.productionOrder.status).toBe("DRAFT");
|
|
49
|
+
}
|
|
50
|
+
expect(spies.update).toHaveBeenCalled();
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it("returns error when the order does not exist", async () => {
|
|
54
|
+
const { db, spies } = createMockDb<Transaction>();
|
|
55
|
+
spies.select.mockReturnValueOnce(undefined);
|
|
56
|
+
|
|
57
|
+
const result = await run(db, { id: "nonexistent" }, ctx);
|
|
58
|
+
|
|
59
|
+
expect(result.ok).toBe(false);
|
|
60
|
+
if (!result.ok) {
|
|
61
|
+
expect(result.error).toBeInstanceOf(ProductionOrderNotFoundError);
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it("returns error when the order is not in RELEASED", async () => {
|
|
66
|
+
const { db, spies } = createMockDb<Transaction>();
|
|
67
|
+
spies.select.mockReturnValueOnce(baseDraftProductionOrder);
|
|
68
|
+
|
|
69
|
+
const result = await run(db, { id: baseDraftProductionOrder.id }, ctx);
|
|
70
|
+
|
|
71
|
+
expect(result.ok).toBe(false);
|
|
72
|
+
if (!result.ok) {
|
|
73
|
+
expect(result.error).toBeInstanceOf(ProductionOrderNotUnreleasableError);
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it("returns error when a work order has already started", async () => {
|
|
78
|
+
const { db, spies } = createMockDb<Transaction>();
|
|
79
|
+
|
|
80
|
+
// order lookup
|
|
81
|
+
spies.select.mockReturnValueOnce(baseReleasedProductionOrder);
|
|
82
|
+
// work orders - one in progress
|
|
83
|
+
spies.select.mockReturnValueOnce([baseInProgressWorkOrder]);
|
|
84
|
+
|
|
85
|
+
const result = await run(db, { id: baseReleasedProductionOrder.id }, ctx);
|
|
86
|
+
|
|
87
|
+
expect(result.ok).toBe(false);
|
|
88
|
+
if (!result.ok) {
|
|
89
|
+
expect(result.error).toBeInstanceOf(ExecutionAlreadyStartedError);
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it("returns error when inventory handoff evidence already exists", async () => {
|
|
94
|
+
const { db, spies } = createMockDb<Transaction>();
|
|
95
|
+
const costSummaryWithActuals = {
|
|
96
|
+
...baseCollectingCostSummary,
|
|
97
|
+
actualMaterialCost: 500,
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
// order lookup
|
|
101
|
+
spies.select.mockReturnValueOnce(baseReleasedProductionOrder);
|
|
102
|
+
// work orders - all pending
|
|
103
|
+
spies.select.mockReturnValueOnce([basePendingWorkOrder]);
|
|
104
|
+
// material requirements
|
|
105
|
+
spies.select.mockReturnValueOnce([baseMaterialRequirement]);
|
|
106
|
+
// cost summary with actual costs
|
|
107
|
+
spies.select.mockReturnValueOnce(costSummaryWithActuals);
|
|
108
|
+
|
|
109
|
+
const result = await run(db, { id: baseReleasedProductionOrder.id }, ctx);
|
|
110
|
+
|
|
111
|
+
expect(result.ok).toBe(false);
|
|
112
|
+
if (!result.ok) {
|
|
113
|
+
expect(result.error).toBeInstanceOf(InventoryHandoffExistsError);
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it("removes work orders and material requirements when unrelease succeeds", async () => {
|
|
118
|
+
const { db, spies } = createMockDb<Transaction>();
|
|
119
|
+
const draftOrder = { ...baseReleasedProductionOrder, status: "DRAFT" as const };
|
|
120
|
+
|
|
121
|
+
// order lookup
|
|
122
|
+
spies.select.mockReturnValueOnce(baseReleasedProductionOrder);
|
|
123
|
+
// work orders - all pending
|
|
124
|
+
spies.select.mockReturnValueOnce([basePendingWorkOrder]);
|
|
125
|
+
// material requirements
|
|
126
|
+
spies.select.mockReturnValueOnce([]);
|
|
127
|
+
// cost summary
|
|
128
|
+
spies.select.mockReturnValueOnce(baseCollectingCostSummary);
|
|
129
|
+
// delete operations
|
|
130
|
+
spies.delete.mockReturnValue(undefined);
|
|
131
|
+
// update order
|
|
132
|
+
spies.update.mockReturnValue(draftOrder);
|
|
133
|
+
|
|
134
|
+
const result = await run(db, { id: baseReleasedProductionOrder.id }, ctx);
|
|
135
|
+
|
|
136
|
+
expect(result.ok).toBe(true);
|
|
137
|
+
expect(spies.delete).toHaveBeenCalled();
|
|
138
|
+
expect(spies.update).toHaveBeenCalled();
|
|
139
|
+
});
|
|
140
|
+
});
|