import { err, ok, type CommandContext } from "@tailor-platform/erp-kit/core"; import { Decimal } from "decimal.js"; import type { Transaction } from "../generated/kysely-tailordb"; import { ApAccrualAccountUnresolvedError, ApPurchaseOrderAccountMismatchError, } from "../lib/errors.generated"; import type { InventoryQueries, PurchaseQueries } from "../module"; export interface ReceiptDistributionLine { /** Net, not gross: the receipt never accrued the tax. */ netAmount: string; sourceType?: "PURCHASE_ORDER" | null; purchaseOrderLineId?: string | null; quantity?: string | null; correctionType?: "PRICE" | "QUANTITY" | "AMOUNT" | null; } export interface DerivedReceiptDistribution { accountId: string; /** Positive on ACCRUAL rows; signed on invoice price variance rows. */ amount: string; // Persisted on the row so later edits can find and recompute it. distributionType: "ACCRUAL" | "INVOICE_PRICE_VARIANCE"; } /** * Derives each receipt-required purchase-order line's accrual and invoice * price variance distributions, keyed by the line's index in the input. */ export async function deriveReceiptDistributions( db: Transaction, args: { companyId: string; supplierAccountId: string; lines: ReceiptDistributionLine[]; validationLines?: ReceiptDistributionLine[]; }, ctx: CommandContext, purchaseQueries: Pick, inventoryQueries: Pick, ) { const derived = new Map(); const purchaseOrderLineIds = [ ...new Set( (args.validationLines ?? args.lines) .filter((line) => line.sourceType === "PURCHASE_ORDER" && line.purchaseOrderLineId) .map((line) => line.purchaseOrderLineId as string), ), ]; if (purchaseOrderLineIds.length === 0) return ok(derived); const { purchaseOrderLines } = ( await purchaseQueries.listPurchaseOrderLinesForMatching(db, { purchaseOrderLineIds }, ctx) ).value; for (const line of purchaseOrderLines) { if (line.companyId !== args.companyId || line.supplierAccountId !== args.supplierAccountId) { return err(new ApPurchaseOrderAccountMismatchError(line.id)); } } const purchaseOrderLineById = new Map(purchaseOrderLines.map((line) => [line.id, line])); // A service line never reaches inventory, so it has no accrual to relieve. const receiptRequired = args.lines.flatMap((line, index) => { if (line.sourceType !== "PURCHASE_ORDER" || !line.purchaseOrderLineId) return []; const purchaseOrderLine = purchaseOrderLineById.get(line.purchaseOrderLineId); if (!purchaseOrderLine?.requiresPhysicalReceipt) return []; return [{ index, line, purchaseOrderLine }]; }); if (receiptRequired.length === 0) return ok(derived); // The same policy the inventory postings resolve, so the clearing accounts wash. const { resolutions } = ( await inventoryQueries.resolveItemValuationPolicies( db, { companyId: args.companyId, itemIds: receiptRequired.map((r) => r.purchaseOrderLine.itemId), }, ctx, ) ).value; const policyByItemId = new Map( resolutions.map((resolution) => [resolution.itemId, resolution.valuationPolicy]), ); for (const { index, line, purchaseOrderLine } of receiptRequired) { const policy = policyByItemId.get(purchaseOrderLine.itemId); if (!policy) { return err( new ApAccrualAccountUnresolvedError(`${args.companyId}:${purchaseOrderLine.itemId}`), ); } const rows: DerivedReceiptDistribution[] = []; if (line.correctionType === "AMOUNT") { // An AMOUNT correction declares nothing to inventory, so nothing may // land in the invoice price variance account. rows.push({ accountId: policy.accrualAccountId, amount: line.netAmount, distributionType: "ACCRUAL", }); } else if (line.correctionType === "PRICE") { // A PRICE correction reprices already-billed units: its whole net amount // is invoice price variance and no accrual quantity is relieved. rows.push({ accountId: policy.invoicePriceVarianceAccountId, amount: line.netAmount, distributionType: "INVOICE_PRICE_VARIANCE", }); } else { const accrual = new Decimal(line.quantity ?? 0).mul(purchaseOrderLine.unitPrice); const variance = new Decimal(line.netAmount).minus(accrual); if (!accrual.isZero()) { rows.push({ accountId: policy.accrualAccountId, amount: accrual.toString(), distributionType: "ACCRUAL", }); } if (!variance.isZero()) { rows.push({ accountId: policy.invoicePriceVarianceAccountId, amount: variance.toString(), distributionType: "INVOICE_PRICE_VARIANCE", }); } } derived.set(index, rows); } return ok(derived); }