import { ok, type CommandContext } from "@tailor-platform/erp-kit/core"; import { Decimal } from "decimal.js"; import type { Selectable, Transaction } from "../generated/kysely-tailordb"; import { redistributeAcquisitionCost, resolveOrderReferenceLayers, } from "../internal/costing/adjustment"; import type { FinancialAccountingCommands, FinancialAccountingQueries } from "../module"; export interface PostAcquisitionCostAdjustmentInput { /** Document that declared the variances. */ sourceType: "ACCOUNT_PAYABLE_DOCUMENT" | "PURCHASE_ORDER_REVISION"; sourceId: string; /** Clearing account the declared amounts wash against. */ varianceKind: "INVOICE_PRICE" | "ORDER_PRICE"; effectiveDate: Date; lines: { /** Source-document line that declared this variance. */ sourceLineId: string | null; /** Purchase order line whose receipt layers absorb the variance. */ purchaseOrderId: string; purchaseOrderLineId: string; /** Signed amount this line adds to the order line's variance. */ amount: string; }[]; } /** * Function: postAcquisitionCostAdjustment * * Records an adjustment per non-zero line and redistributes every affected * order line's cumulative variance over its receipt cost layers. An order line * with no receipts yet is only recorded; the first receipt posting redistributes * it. */ export async function run( db: Transaction, input: PostAcquisitionCostAdjustmentInput, ctx: CommandContext, financialAccountingCommands: Pick< FinancialAccountingCommands, "createJournalEntry" | "postJournalEntry" >, financialAccountingQueries: Pick, ) { const values = input.lines .filter((line) => !new Decimal(line.amount).isZero()) .map((line) => ({ sourceType: input.sourceType, sourceId: input.sourceId, sourceLineId: line.sourceLineId, purchaseOrderId: line.purchaseOrderId, purchaseOrderLineId: line.purchaseOrderLineId, amount: new Decimal(line.amount).toString(), kind: input.varianceKind, effectiveDate: input.effectiveDate, })); if (values.length === 0) { return ok({ acquisitionCostAdjustments: [] }); } const adjustments = await db .insertInto("AcquisitionCostAdjustment") .values(values) .returningAll() .execute(); // Resolve each adjustment's layers before locking anything; an order line // with no receipts yet simply waits in the register. const pending: { adjustment: Selectable<"AcquisitionCostAdjustment">; itemId: string; companyId: string; }[] = []; for (const adjustment of adjustments) { const layers = await resolveOrderReferenceLayers( db, adjustment.purchaseOrderId, adjustment.purchaseOrderLineId, ); if (layers.length > 0) { pending.push({ adjustment, itemId: layers[0].itemId, companyId: layers[0].companyId }); } } // Redistribute in item-valuation-key order so concurrent documents acquire // the locks in the same order and cannot deadlock. pending.sort((a, b) => { const aKey = `${a.itemId}:${a.companyId}`; const bKey = `${b.itemId}:${b.companyId}`; return aKey < bKey ? -1 : aKey > bKey ? 1 : 0; }); for (const { adjustment, itemId, companyId } of pending) { const result = await redistributeAcquisitionCost( db, adjustment, itemId, companyId, ctx, financialAccountingCommands, financialAccountingQueries, ); if (!result.ok) return result; } return ok({ acquisitionCostAdjustments: adjustments }); }