import { ok, err, type CommandContext } from "@tailor-platform/erp-kit/core"; import { stockAdjustmentLifecycle } from "../db/stockAdjustment.lifecycle.generated"; import type { Selectable, Transaction } from "../generated/kysely-tailordb"; import { postInventoryLedgerInternal, type PostInventoryMovementLine, } from "../internal/inventoryPosting"; import { InvalidStatusError, StockAdjustmentNotFoundError } from "../lib/errors.generated"; import type { FinancialAccountingCommands, FinancialAccountingQueries, ItemManagementQueries, OrganizationQueries, } from "../module"; export interface ConfirmStockAdjustmentInput { id: string; } type AdjustmentEffect = { direction: "IN" | "OUT"; action: "QUANTITY_CHANGE" | "STOCK_TYPE_CHANGE"; stockType: "AVAILABLE" | "BLOCKED" | "IN_TRANSIT"; }; function getAdjustmentEffects( adjustment: Selectable<"StockAdjustment">, line: Selectable<"StockAdjustmentLine">, ): AdjustmentEffect[] { if (adjustment.adjustmentType === "CORRECTION") { return [ { direction: line.adjustmentDirection === "INCREASE" ? "IN" : "OUT", action: "QUANTITY_CHANGE", stockType: "AVAILABLE", }, ]; } if (adjustment.adjustmentType === "SCRAP") { return [ { direction: "OUT", action: "QUANTITY_CHANGE", stockType: line.fromStockCategory === "BLOCKED" ? "BLOCKED" : "AVAILABLE", }, ]; } if (adjustment.adjustmentType === "BLOCK") { return [ { direction: "OUT", action: "STOCK_TYPE_CHANGE", stockType: "AVAILABLE" }, { direction: "IN", action: "STOCK_TYPE_CHANGE", stockType: "BLOCKED" }, ]; } return [ { direction: "IN", action: "STOCK_TYPE_CHANGE", stockType: "AVAILABLE" }, { direction: "OUT", action: "STOCK_TYPE_CHANGE", stockType: "BLOCKED" }, ]; } /** * Function: confirmStockAdjustment * * Approves and executes a submitted stock adjustment, posting InventoryLedger * entries and stock effects through the inventory posting engine. * Combines approval and execution into a single step. */ export async function run( db: Transaction, input: ConfirmStockAdjustmentInput, ctx: CommandContext, itemManagementQueries: Pick, organizationQueries: Pick, financialAccountingCommands: Pick< FinancialAccountingCommands, "createJournalEntry" | "postJournalEntry" >, financialAccountingQueries: Pick, ) { const { id } = input; const stockAdjustment = await db .selectFrom("StockAdjustment") .selectAll() .where("id", "=", id) .forUpdate() .executeTakeFirst(); if (!stockAdjustment) { return err(new StockAdjustmentNotFoundError(id)); } const nextAdjustmentStatus = stockAdjustmentLifecycle.tryTransition( stockAdjustment.status, "confirm", ); if (!nextAdjustmentStatus) { return err(new InvalidStatusError(id)); } const lines = await db .selectFrom("StockAdjustmentLine") .selectAll() .where("stockAdjustmentId", "=", id) .execute(); const effectLines = lines.flatMap((line) => getAdjustmentEffects(stockAdjustment, line).map((effect) => ({ line, effect })), ); const movementLines: PostInventoryMovementLine[] = effectLines.map(({ line, effect }) => ({ direction: effect.direction, action: effect.action, itemId: line.itemId, storageLocationId: line.storageLocationId, stockType: effect.stockType, quantity: line.quantity, unitCost: line.unitCost ?? undefined, sourceLineId: line.id, })); let inventoryLedgerEntries: Selectable<"InventoryLedger">[] = []; if (movementLines.length > 0) { const postingResult = await postInventoryLedgerInternal( db, { sourceType: "STOCK_ADJUSTMENT", sourceId: stockAdjustment.id, effectiveDate: stockAdjustment.adjustmentDate, lines: movementLines, }, ctx, itemManagementQueries, organizationQueries, financialAccountingCommands, financialAccountingQueries, ); if (!postingResult.ok) { return postingResult; } inventoryLedgerEntries = postingResult.value.inventoryLedgerEntries; } const updatedAdjustment = await db .updateTable("StockAdjustment") .set({ status: nextAdjustmentStatus, }) .where("id", "=", id) .returningAll() .executeTakeFirstOrThrow(); return ok({ stockAdjustment: updatedAdjustment, inventoryLedgerEntries, }); }