import { err, ok, type CommandContext } from "@tailor-platform/erp-kit/core"; import { Decimal } from "decimal.js"; import type { Selectable, Transaction } from "../generated/kysely-tailordb"; import { ApAcquisitionCostAdjustmentFailedError } from "../lib/errors.generated"; import type { InventoryCommands, PurchaseQueries } from "../module"; type AccountPayableDocument = Selectable<"AccountPayableDocument">; type AccountPayableDocumentLine = Selectable<"AccountPayableDocumentLine">; /** * Declares each purchase-order sourced line's price variance against the order * to inventory, which adjusts the acquisition cost of the received stock. */ export async function postAcquisitionCostAdjustment( db: Transaction, input: { document: AccountPayableDocument; lines: AccountPayableDocumentLine[]; }, ctx: CommandContext, purchaseQueries: Pick, inventoryCommands: Pick, ) { const { document } = input; const adjustmentLines = input.lines.flatMap((line) => { if ( line.sourceType !== "PURCHASE_ORDER" || line.purchaseOrderLineId === null || line.quantity === null || line.unitPrice === null ) { return []; } // An AMOUNT correction declares money that is not per-unit goods cost // (charges, lump-sum credits), so it stays in AP and never adjusts inventory. if (line.correctionType === "AMOUNT") return []; return [ { id: line.id, purchaseOrderLineId: line.purchaseOrderLineId, quantity: line.quantity, unitPrice: line.unitPrice, matchedPurchaseOrderUnitPrice: line.matchedPurchaseOrderUnitPrice, correctionType: line.correctionType, }, ]; }); if (adjustmentLines.length === 0) return ok(undefined); const purchaseOrderLineIds = [ ...new Set(adjustmentLines.map((line) => line.purchaseOrderLineId)), ]; const { purchaseOrderLines } = ( await purchaseQueries.listPurchaseOrderLinesForMatching(db, { purchaseOrderLineIds }, ctx) ).value; const purchaseOrderLineById = new Map( purchaseOrderLines.map((purchaseOrderLine) => [purchaseOrderLine.id, purchaseOrderLine]), ); const isInvoice = document.documentType === "INVOICE"; const varianceLines = []; for (const line of adjustmentLines) { const purchaseOrderLine = purchaseOrderLineById.get(line.purchaseOrderLineId); if (!purchaseOrderLine) return err(new ApAcquisitionCostAdjustmentFailedError(line.id)); // A PRICE correction's unitPrice is already the per-unit change; other lines // compare against the matched price, since a later change declares its own. let perUnit: Decimal; if (line.correctionType === "PRICE") { perUnit = new Decimal(line.unitPrice); } else { if (line.matchedPurchaseOrderUnitPrice === null) { return err(new ApAcquisitionCostAdjustmentFailedError(line.id)); } perUnit = new Decimal(line.unitPrice).minus(line.matchedPurchaseOrderUnitPrice); } // Line values are positive; the document type carries the direction. const amount = perUnit.mul(line.quantity).mul(isInvoice ? 1 : -1); if (amount.isZero()) continue; varianceLines.push({ sourceLineId: line.id, purchaseOrderId: purchaseOrderLine.purchaseOrderId, purchaseOrderLineId: line.purchaseOrderLineId, amount: amount.toString(), }); } if (varianceLines.length === 0) return ok(undefined); const adjustmentResult = await inventoryCommands.postAcquisitionCostAdjustment( db, { sourceType: "ACCOUNT_PAYABLE_DOCUMENT", sourceId: document.id, varianceKind: "INVOICE_PRICE", effectiveDate: document.postingDate, lines: varianceLines, }, ctx, ); if (!adjustmentResult.ok) return err(new ApAcquisitionCostAdjustmentFailedError(document.id)); return ok(undefined); } /** * Declares the matched-versus-current price difference on the quantity a * cancelled REGISTERED document returns to the unbilled pool. */ export async function postCancellationCostCompensation( db: Transaction, input: { document: AccountPayableDocument; lines: AccountPayableDocumentLine[]; }, ctx: CommandContext, purchaseQueries: Pick, inventoryCommands: Pick, ) { const { document } = input; // PRICE and AMOUNT corrections carry no billed quantity, so their // cancellation moves nothing between the pools. const matchedLines = input.lines.flatMap((line) => { if ( line.sourceType !== "PURCHASE_ORDER" || line.purchaseOrderLineId === null || line.quantity === null ) { return []; } if (line.correctionType === "PRICE" || line.correctionType === "AMOUNT") return []; return [ { id: line.id, purchaseOrderLineId: line.purchaseOrderLineId, quantity: line.quantity, matchedPurchaseOrderUnitPrice: line.matchedPurchaseOrderUnitPrice, }, ]; }); if (matchedLines.length === 0) return ok(undefined); const purchaseOrderLineIds = [...new Set(matchedLines.map((line) => line.purchaseOrderLineId))]; const { purchaseOrderLines } = ( await purchaseQueries.listPurchaseOrderLinesForMatching(db, { purchaseOrderLineIds }, ctx) ).value; const purchaseOrderLineById = new Map( purchaseOrderLines.map((purchaseOrderLine) => [purchaseOrderLine.id, purchaseOrderLine]), ); const isCreditMemo = document.documentType === "CREDIT_MEMO"; const compensationLines = []; for (const line of matchedLines) { const purchaseOrderLine = purchaseOrderLineById.get(line.purchaseOrderLineId); if (!purchaseOrderLine) return err(new ApAcquisitionCostAdjustmentFailedError(line.id)); if (line.matchedPurchaseOrderUnitPrice === null) { return err(new ApAcquisitionCostAdjustmentFailedError(line.id)); } const amount = new Decimal(purchaseOrderLine.unitPrice) .minus(line.matchedPurchaseOrderUnitPrice) .mul(line.quantity) .mul(isCreditMemo ? -1 : 1); if (amount.isZero()) continue; compensationLines.push({ sourceLineId: line.id, purchaseOrderId: purchaseOrderLine.purchaseOrderId, purchaseOrderLineId: line.purchaseOrderLineId, amount: amount.toString(), }); } if (compensationLines.length === 0) return ok(undefined); const adjustmentResult = await inventoryCommands.postAcquisitionCostAdjustment( db, { sourceType: "ACCOUNT_PAYABLE_DOCUMENT", sourceId: document.id, varianceKind: "ORDER_PRICE", effectiveDate: new Date(), lines: compensationLines, }, ctx, ); if (!adjustmentResult.ok) return err(new ApAcquisitionCostAdjustmentFailedError(document.id)); return ok(undefined); }