import { err, ok, type CommandContext } from "@tailor-platform/erp-kit/core"; import { amendOrdered, deriveAcquisitionCostAdjustments, deriveSupplyPlanChanges, validateOrderItem, type ItemSnapshot, type DesiredSupplyPlan, } from "../domain/purchaseOrder"; import type { Transaction } from "../generated/kysely-tailordb"; import { PoNotFoundError, ReceivingSiteNotFoundError } from "../lib/errors.generated"; import type { InventoryCommands, InventoryQueries, ItemManagementQueries, OrganizationQueries, } from "../module"; import { createPurchaseItemRepository, type PurchaseItemRepository, } from "../repository/purchaseItemRepository"; import { createPurchaseOrderRepository, type PurchaseOrderRepository, } from "../repository/purchaseOrderRepository"; import type { PurchaseOrderLineInput } from "./createPurchaseOrder"; export interface AmendPurchaseOrderHeaderPatch { orderDate?: Date; receivingSiteId?: string | null; externalSupplierOrderReference?: string | null; } export interface AmendPurchaseOrderLinePatch { quantity?: string; unitPrice?: string; receivingSiteId?: string | null; } export interface AmendPurchaseOrderLineEdit { lineId: string; linePatch: AmendPurchaseOrderLinePatch; } export interface AmendOrderedPurchaseOrderInput { id: string; reason?: string; headerPatch?: AmendPurchaseOrderHeaderPatch; addLines?: PurchaseOrderLineInput[]; updateLines?: AmendPurchaseOrderLineEdit[]; removeLineIds?: string[]; } export async function run< CF extends Record = Record, LCF extends Record = Record, >( db: Transaction, input: Omit & { headerPatch?: AmendPurchaseOrderHeaderPatch & Partial; addLines?: (PurchaseOrderLineInput & LCF)[]; updateLines?: { lineId: string; linePatch: AmendPurchaseOrderLinePatch & Partial }[]; }, ctx: CommandContext, itemManagementQueries: Pick, inventoryCommands: Pick< InventoryCommands, | "createInventorySupplyPlan" | "updateInventorySupplyPlan" | "closeInventorySupplyPlan" | "postAcquisitionCostAdjustment" >, inventoryQueries: Pick, organizationQueries: Pick, repositoryFactory: (db: Transaction) => PurchaseOrderRepository = createPurchaseOrderRepository, purchaseItemRepositoryFactory: ( db: Transaction, ) => PurchaseItemRepository = createPurchaseItemRepository, ) { const { id, reason, headerPatch, addLines = [], updateLines = [], removeLineIds = [] } = input; const { orderDate, receivingSiteId, externalSupplierOrderReference, ...headerCustomFields } = headerPatch ?? {}; const repository = repositoryFactory(db); const order = await repository.findById(id, { forUpdate: true }); if (!order) { return err(new PoNotFoundError(id)); } // Items are validated only where they are chosen: added lines. const purchaseItemByItemId = await purchaseItemRepositoryFactory(db).findByItemIds( addLines.map((line) => line.itemId), ); const itemSnapshotByItemId = new Map(); for (const line of addLines) { if (itemSnapshotByItemId.has(line.itemId)) { continue; } const { item } = (await itemManagementQueries.getItem(db, { id: line.itemId }, ctx)).value; const snapshot = validateOrderItem(line.itemId, { item, purchaseItem: purchaseItemByItemId.get(line.itemId) ?? null, }); if (!snapshot.ok) { return snapshot; } itemSnapshotByItemId.set(line.itemId, snapshot.value); } // Every added itemId was validated above, so this lookup never misses. // eslint-disable-next-line @typescript-eslint/no-non-null-assertion const itemSnapshotFor = (itemId: string) => itemSnapshotByItemId.get(itemId)!; // Receiving sites — every referenced site must exist. const siteIdsToValidate = new Set(); if (receivingSiteId != null) { siteIdsToValidate.add(receivingSiteId); } for (const line of addLines) { if (line.receivingSiteId != null) { siteIdsToValidate.add(line.receivingSiteId); } } for (const { linePatch } of updateLines) { if (linePatch.receivingSiteId != null) { siteIdsToValidate.add(linePatch.receivingSiteId); } } for (const siteId of siteIdsToValidate) { const { site } = (await organizationQueries.getSite(db, { id: siteId }, ctx)).value; if (!site) { return err(new ReceivingSiteNotFoundError(siteId)); } } const amended = amendOrdered(order, { headerPatch: { orderDate, receivingSiteId, externalSupplierOrderReference, customFields: headerCustomFields, }, addLines: addLines.map((line) => { const { itemId, quantity, unitPrice, unitId, receivingSiteId, ...customFields } = line; return { item: itemSnapshotFor(itemId), quantity, unitPrice, unitId, receivingSiteId, customFields, }; }), updateLines: updateLines.map(({ lineId, linePatch }) => { const { quantity, unitPrice, receivingSiteId, ...customFields } = linePatch; return { lineId, patch: { quantity, unitPrice, receivingSiteId, customFields } }; }), removeLineIds, reason, amendedByUserId: ctx.actorId, }); if (!amended.ok) { return amended; } const { order: amendedOrder, revisionId } = amended.value; await repository.save(amendedOrder); // Declare the acquisition-cost impact of price changes. const priceAdjustments = deriveAcquisitionCostAdjustments(order, amendedOrder); if (priceAdjustments.length > 0) { const adjustmentResult = await inventoryCommands.postAcquisitionCostAdjustment( db, { sourceType: "PURCHASE_ORDER_REVISION", sourceId: revisionId, varianceKind: "ORDER_PRICE", effectiveDate: new Date(), lines: priceAdjustments.map((adjustment) => ({ sourceLineId: adjustment.purchaseOrderLineId, purchaseOrderId: id, purchaseOrderLineId: adjustment.purchaseOrderLineId, amount: adjustment.amount, })), }, ctx, ); if (!adjustmentResult.ok) { return adjustmentResult; } } // Propagate the amendment to the inventory supply plans. const supplyPlanChanges = deriveSupplyPlanChanges(order, amendedOrder); if (supplyPlanChanges.closeLineIds.length > 0) { const closeResult = await inventoryCommands.closeInventorySupplyPlan( db, { target: "SOURCE_LINES", sourceType: "PURCHASE_ORDER", sourceLineIds: supplyPlanChanges.closeLineIds, }, ctx, ); if (!closeResult.ok) { return closeResult; } } // Whether an upsert creates or updates depends on whether inventory already holds a plan. const supplyPlansToCreate: DesiredSupplyPlan[] = []; for (const desiredPlan of supplyPlanChanges.upserts) { const supplyPlanResult = await inventoryQueries.getInventorySupplyPlan( db, { sourceType: "PURCHASE_ORDER", sourceLineId: desiredPlan.sourceLineId }, ctx, ); const existingSupplyPlan = supplyPlanResult.value.supplyPlan; if (existingSupplyPlan) { const updateResult = await inventoryCommands.updateInventorySupplyPlan( db, { id: existingSupplyPlan.id, sourceType: "PURCHASE_ORDER", sourceId: id, sourceLineId: desiredPlan.sourceLineId, itemId: desiredPlan.itemId, siteId: desiredPlan.siteId, expectedQuantity: desiredPlan.expectedQuantity, unitId: desiredPlan.unitId, expectedDate: desiredPlan.expectedDate, }, ctx, ); if (!updateResult.ok) { return updateResult; } continue; } supplyPlansToCreate.push(desiredPlan); } if (supplyPlansToCreate.length > 0) { const createResult = await inventoryCommands.createInventorySupplyPlan( db, { sourceType: "PURCHASE_ORDER", sourceId: id, supplyPlans: supplyPlansToCreate }, ctx, ); if (!createResult.ok) { return createResult; } } return ok({ purchaseOrderId: id }); }