@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,95 @@
|
|
|
1
|
+
import type { Transaction } from "../generated/kysely-tailordb";
|
|
2
|
+
import {
|
|
3
|
+
ProductionOrderNotFoundError,
|
|
4
|
+
ProductionOrderNotReschedulableError,
|
|
5
|
+
ExecutionAlreadyStartedError,
|
|
6
|
+
InvalidDateRangeError,
|
|
7
|
+
} from "../lib/errors.generated";
|
|
8
|
+
import { ok, err, type CommandContext } from "@tailor-platform/erp-kit/module";
|
|
9
|
+
|
|
10
|
+
export interface RescheduleProductionOrderInput {
|
|
11
|
+
id: string;
|
|
12
|
+
plannedStartDate: Date;
|
|
13
|
+
plannedEndDate: Date;
|
|
14
|
+
from?: string[];
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Function: rescheduleProductionOrder
|
|
19
|
+
*
|
|
20
|
+
* Changes the planned execution dates on a released order before execution
|
|
21
|
+
* starts. Preserves the released snapshots and keeps the change auditable.
|
|
22
|
+
*/
|
|
23
|
+
export async function run<CF extends Record<string, unknown>>(
|
|
24
|
+
db: Transaction,
|
|
25
|
+
input: RescheduleProductionOrderInput & CF,
|
|
26
|
+
_ctx: CommandContext,
|
|
27
|
+
) {
|
|
28
|
+
const { id, plannedStartDate, plannedEndDate, from, ...customFields } = input;
|
|
29
|
+
void customFields;
|
|
30
|
+
|
|
31
|
+
const allowedStatuses = from ?? ["RELEASED"];
|
|
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 is reschedulable
|
|
46
|
+
if (!allowedStatuses.includes(order.status)) {
|
|
47
|
+
return err(new ProductionOrderNotReschedulableError(id));
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// 3. Check no work order has execution evidence
|
|
51
|
+
const workOrders = await db
|
|
52
|
+
.selectFrom("WorkOrder")
|
|
53
|
+
.selectAll()
|
|
54
|
+
.where("productionOrderId", "=", id)
|
|
55
|
+
.execute();
|
|
56
|
+
|
|
57
|
+
const hasExecution = workOrders.some(
|
|
58
|
+
(wo) =>
|
|
59
|
+
wo.status === "IN_PROGRESS" ||
|
|
60
|
+
wo.status === "PAUSED" ||
|
|
61
|
+
wo.status === "COMPLETE" ||
|
|
62
|
+
wo.completedQuantity > 0 ||
|
|
63
|
+
wo.actualStartDate != null,
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
if (hasExecution) {
|
|
67
|
+
return err(new ExecutionAlreadyStartedError(id));
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// 4. Validate revised dates
|
|
71
|
+
if (!plannedStartDate || !plannedEndDate) {
|
|
72
|
+
return err(new InvalidDateRangeError(id));
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const start = new Date(plannedStartDate);
|
|
76
|
+
const end = new Date(plannedEndDate);
|
|
77
|
+
|
|
78
|
+
if (isNaN(start.getTime()) || isNaN(end.getTime()) || start >= end) {
|
|
79
|
+
return err(new InvalidDateRangeError(id));
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// 5. Persist scheduling update
|
|
83
|
+
const rescheduled = await db
|
|
84
|
+
.updateTable("ProductionOrder")
|
|
85
|
+
.set({
|
|
86
|
+
plannedStartDate,
|
|
87
|
+
plannedEndDate,
|
|
88
|
+
updatedAt: new Date(),
|
|
89
|
+
})
|
|
90
|
+
.where("id", "=", id)
|
|
91
|
+
.returningAll()
|
|
92
|
+
.executeTakeFirstOrThrow();
|
|
93
|
+
|
|
94
|
+
return ok({ productionOrder: rescheduled });
|
|
95
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
// @generated — do not edit
|
|
2
|
+
import { permissions } from "../lib/permissions.generated";
|
|
3
|
+
import { run } from "./resumeWorkOrder";
|
|
4
|
+
import { defineCommand } from "@tailor-platform/erp-kit/module";
|
|
5
|
+
|
|
6
|
+
export const resumeWorkOrder = defineCommand(permissions.resumeWorkOrder, run);
|
|
@@ -0,0 +1,122 @@
|
|
|
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
|
+
WorkOrderNotResumableError,
|
|
7
|
+
ParentOrderNotExecutableError,
|
|
8
|
+
} from "../lib/errors.generated";
|
|
9
|
+
import {
|
|
10
|
+
basePausedWorkOrder,
|
|
11
|
+
basePendingWorkOrder,
|
|
12
|
+
baseInProgressProductionOrder,
|
|
13
|
+
baseDraftProductionOrder,
|
|
14
|
+
} from "../testing/fixtures";
|
|
15
|
+
import { run } from "./resumeWorkOrder";
|
|
16
|
+
import type { CommandContext } from "@tailor-platform/erp-kit/module";
|
|
17
|
+
|
|
18
|
+
describe("resumeWorkOrder", () => {
|
|
19
|
+
const ctx: CommandContext = {
|
|
20
|
+
actorId: "test-actor",
|
|
21
|
+
permissions: ["manufacturing:resumeWorkOrder"],
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
it("resumes a paused work order", async () => {
|
|
25
|
+
const { db, spies } = createMockDb<Transaction>();
|
|
26
|
+
const resumedWorkOrder = {
|
|
27
|
+
...basePausedWorkOrder,
|
|
28
|
+
status: "IN_PROGRESS" as const,
|
|
29
|
+
pauseReason: null,
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
// work order lookup
|
|
33
|
+
spies.select.mockReturnValueOnce(basePausedWorkOrder);
|
|
34
|
+
// parent production order lookup
|
|
35
|
+
spies.select.mockReturnValueOnce(baseInProgressProductionOrder);
|
|
36
|
+
// update work order
|
|
37
|
+
spies.update.mockReturnValueOnce(resumedWorkOrder);
|
|
38
|
+
// insert execution event
|
|
39
|
+
spies.insert.mockReturnValueOnce(undefined);
|
|
40
|
+
|
|
41
|
+
const result = await run(db, { id: basePausedWorkOrder.id }, ctx);
|
|
42
|
+
|
|
43
|
+
expect(result.ok).toBe(true);
|
|
44
|
+
if (result.ok) {
|
|
45
|
+
expect(result.value.workOrder.status).toBe("IN_PROGRESS");
|
|
46
|
+
expect(result.value.workOrder.pauseReason).toBeNull();
|
|
47
|
+
}
|
|
48
|
+
expect(spies.update).toHaveBeenCalled();
|
|
49
|
+
expect(spies.insert).toHaveBeenCalled();
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it("returns error when the work order does not exist", async () => {
|
|
53
|
+
const { db, spies } = createMockDb<Transaction>();
|
|
54
|
+
spies.select.mockReturnValueOnce(undefined);
|
|
55
|
+
|
|
56
|
+
const result = await run(db, { id: "nonexistent" }, ctx);
|
|
57
|
+
|
|
58
|
+
expect(result.ok).toBe(false);
|
|
59
|
+
if (!result.ok) {
|
|
60
|
+
expect(result.error).toBeInstanceOf(WorkOrderNotFoundError);
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it("returns error when the work order is not paused", async () => {
|
|
65
|
+
const { db, spies } = createMockDb<Transaction>();
|
|
66
|
+
spies.select.mockReturnValueOnce(basePendingWorkOrder);
|
|
67
|
+
|
|
68
|
+
const result = await run(db, { id: basePendingWorkOrder.id }, ctx);
|
|
69
|
+
|
|
70
|
+
expect(result.ok).toBe(false);
|
|
71
|
+
if (!result.ok) {
|
|
72
|
+
expect(result.error).toBeInstanceOf(WorkOrderNotResumableError);
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it("returns error when the parent production order no longer allows execution", async () => {
|
|
77
|
+
const { db, spies } = createMockDb<Transaction>();
|
|
78
|
+
|
|
79
|
+
// work order lookup
|
|
80
|
+
spies.select.mockReturnValueOnce(basePausedWorkOrder);
|
|
81
|
+
// parent production order - DRAFT is not executable
|
|
82
|
+
spies.select.mockReturnValueOnce(baseDraftProductionOrder);
|
|
83
|
+
|
|
84
|
+
const result = await run(db, { id: basePausedWorkOrder.id }, ctx);
|
|
85
|
+
|
|
86
|
+
expect(result.ok).toBe(false);
|
|
87
|
+
if (!result.ok) {
|
|
88
|
+
expect(result.error).toBeInstanceOf(ParentOrderNotExecutableError);
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it("preserves prior execution history after resume", async () => {
|
|
93
|
+
const { db, spies } = createMockDb<Transaction>();
|
|
94
|
+
const resumedWorkOrder = {
|
|
95
|
+
...basePausedWorkOrder,
|
|
96
|
+
status: "IN_PROGRESS" as const,
|
|
97
|
+
pauseReason: null,
|
|
98
|
+
completedQuantity: basePausedWorkOrder.completedQuantity,
|
|
99
|
+
scrapQuantity: basePausedWorkOrder.scrapQuantity,
|
|
100
|
+
actualSetupTime: basePausedWorkOrder.actualSetupTime,
|
|
101
|
+
actualRunTime: basePausedWorkOrder.actualRunTime,
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
// work order lookup
|
|
105
|
+
spies.select.mockReturnValueOnce(basePausedWorkOrder);
|
|
106
|
+
// parent production order lookup
|
|
107
|
+
spies.select.mockReturnValueOnce(baseInProgressProductionOrder);
|
|
108
|
+
// update work order
|
|
109
|
+
spies.update.mockReturnValueOnce(resumedWorkOrder);
|
|
110
|
+
// insert execution event
|
|
111
|
+
spies.insert.mockReturnValueOnce(undefined);
|
|
112
|
+
|
|
113
|
+
const result = await run(db, { id: basePausedWorkOrder.id }, ctx);
|
|
114
|
+
|
|
115
|
+
expect(result.ok).toBe(true);
|
|
116
|
+
if (result.ok) {
|
|
117
|
+
expect(result.value.workOrder.completedQuantity).toBe(basePausedWorkOrder.completedQuantity);
|
|
118
|
+
expect(result.value.workOrder.actualSetupTime).toBe(basePausedWorkOrder.actualSetupTime);
|
|
119
|
+
expect(result.value.workOrder.actualRunTime).toBe(basePausedWorkOrder.actualRunTime);
|
|
120
|
+
}
|
|
121
|
+
});
|
|
122
|
+
});
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import type { Transaction } from "../generated/kysely-tailordb";
|
|
2
|
+
import {
|
|
3
|
+
WorkOrderNotFoundError,
|
|
4
|
+
WorkOrderNotResumableError,
|
|
5
|
+
ParentOrderNotExecutableError,
|
|
6
|
+
} from "../lib/errors.generated";
|
|
7
|
+
import { ok, err, type CommandContext } from "@tailor-platform/erp-kit/module";
|
|
8
|
+
|
|
9
|
+
const EXECUTABLE_ORDER_STATUSES = ["RELEASED", "IN_PROGRESS"] as const;
|
|
10
|
+
|
|
11
|
+
export interface ResumeWorkOrderInput {
|
|
12
|
+
id: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Function: resumeWorkOrder
|
|
17
|
+
*
|
|
18
|
+
* Restarts a paused work order without losing the accumulated execution
|
|
19
|
+
* history recorded before the interruption. Clears the pause reason.
|
|
20
|
+
*/
|
|
21
|
+
export async function run<CF extends Record<string, unknown>>(
|
|
22
|
+
db: Transaction,
|
|
23
|
+
input: ResumeWorkOrderInput & CF,
|
|
24
|
+
_ctx: CommandContext,
|
|
25
|
+
) {
|
|
26
|
+
const { id, ...customFields } = input;
|
|
27
|
+
void customFields;
|
|
28
|
+
|
|
29
|
+
// 1. Fetch work order with lock
|
|
30
|
+
const workOrder = await db
|
|
31
|
+
.selectFrom("WorkOrder")
|
|
32
|
+
.selectAll()
|
|
33
|
+
.where("id", "=", id)
|
|
34
|
+
.forUpdate()
|
|
35
|
+
.executeTakeFirst();
|
|
36
|
+
|
|
37
|
+
if (!workOrder) {
|
|
38
|
+
return err(new WorkOrderNotFoundError(id));
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// 2. Validate status is PAUSED
|
|
42
|
+
if (workOrder.status !== "PAUSED") {
|
|
43
|
+
return err(new WorkOrderNotResumableError(id));
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// 3. Check parent production order is still execution-capable
|
|
47
|
+
const parentOrder = await db
|
|
48
|
+
.selectFrom("ProductionOrder")
|
|
49
|
+
.selectAll()
|
|
50
|
+
.where("id", "=", workOrder.productionOrderId)
|
|
51
|
+
.forUpdate()
|
|
52
|
+
.executeTakeFirst();
|
|
53
|
+
|
|
54
|
+
if (
|
|
55
|
+
!parentOrder ||
|
|
56
|
+
!EXECUTABLE_ORDER_STATUSES.includes(
|
|
57
|
+
parentOrder.status as (typeof EXECUTABLE_ORDER_STATUSES)[number],
|
|
58
|
+
)
|
|
59
|
+
) {
|
|
60
|
+
return err(new ParentOrderNotExecutableError(id));
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// 4. Record resume event and set IN_PROGRESS, clear pause reason
|
|
64
|
+
const now = new Date();
|
|
65
|
+
|
|
66
|
+
const updatedWorkOrder = await db
|
|
67
|
+
.updateTable("WorkOrder")
|
|
68
|
+
.set({
|
|
69
|
+
status: "IN_PROGRESS",
|
|
70
|
+
pauseReason: null,
|
|
71
|
+
updatedAt: now,
|
|
72
|
+
})
|
|
73
|
+
.where("id", "=", id)
|
|
74
|
+
.returningAll()
|
|
75
|
+
.executeTakeFirstOrThrow();
|
|
76
|
+
|
|
77
|
+
// 5. Create RESUMED execution event
|
|
78
|
+
await db
|
|
79
|
+
.insertInto("WorkOrderExecutionEvent")
|
|
80
|
+
.values({
|
|
81
|
+
workOrderId: id,
|
|
82
|
+
eventType: "RESUMED",
|
|
83
|
+
timestamp: now,
|
|
84
|
+
quantity: null,
|
|
85
|
+
timeValue: null,
|
|
86
|
+
scrapValue: null,
|
|
87
|
+
notes: null,
|
|
88
|
+
createdAt: now,
|
|
89
|
+
updatedAt: null,
|
|
90
|
+
})
|
|
91
|
+
.execute();
|
|
92
|
+
|
|
93
|
+
return ok({ workOrder: updatedWorkOrder });
|
|
94
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
// @generated — do not edit
|
|
2
|
+
import { permissions } from "../lib/permissions.generated";
|
|
3
|
+
import { run } from "./reviewManufacturingCostSummary";
|
|
4
|
+
import { defineCommand } from "@tailor-platform/erp-kit/module";
|
|
5
|
+
|
|
6
|
+
export const reviewManufacturingCostSummary = defineCommand(permissions.reviewManufacturingCostSummary, run);
|
|
@@ -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);
|