import { err, ok, type CommandContext } from "@tailor-platform/erp-kit/core"; import { assignUnitCosts, post, sumReceiptQuantitiesBySourceLine } from "../domain/inboundShipment"; import type { Transaction } from "../generated/kysely-tailordb"; import { InvalidStatusError, PurchaseOrderReceiptSyncFailedError } from "../lib/errors.generated"; import type { InventoryCommands, PurchaseCommands, PurchaseQueries } from "../module"; import { createInboundShipmentRepository, type InboundShipmentRepository, } from "../repository/inboundShipmentRepository"; export interface PostInboundShipmentInput { id: string; } export async function run( db: Transaction, input: PostInboundShipmentInput, ctx: CommandContext, inventoryCommands: Pick, purchaseCommands: Pick, purchaseQueries: Pick, repositoryFactory: ( db: Transaction, ) => InboundShipmentRepository = createInboundShipmentRepository, ) { // The FOR UPDATE lock plus the DRAFT-only transition guarantees one-shot posting. const repository = repositoryFactory(db); const shipment = await repository.findById(input.id, { forUpdate: true }); if (!shipment) { return err(new InvalidStatusError(input.id)); } const posted = post(shipment, new Date()); if (!posted.ok) { return posted; } // Feed this shipment's received quantities to purchase as a delta. Purchase adds // it under a row lock, so concurrent posts of the same PO line can't lost-update. const receipts = sumReceiptQuantitiesBySourceLine(posted.value); const receiptSyncResult = await purchaseCommands.recalculatePurchaseOrderReceiptStatus( db, { lineReceipts: receipts.map(({ sourceLineId, quantity }) => ({ purchaseOrderLineId: sourceLineId, receivedQuantityDelta: quantity, })), }, ctx, ); if (!receiptSyncResult.ok) { return err(new PurchaseOrderReceiptSyncFailedError(posted.value.id)); } // Read prices after the receipt sync so they are the posting-time values under purchase's lock. const { purchaseOrderLines } = ( await purchaseQueries.listPurchaseOrderLinesForMatching( db, { purchaseOrderLineIds: receipts.map(({ sourceLineId }) => sourceLineId) }, ctx, ) ).value; const priced = assignUnitCosts( posted.value, new Map(purchaseOrderLines.map((line) => [line.id, line.unitPrice])), ); const postingResult = await inventoryCommands.postInventoryLedger( db, { sourceType: "INBOUND_SHIPMENT", sourceId: priced.id, effectiveDate: priced.header.effectiveDate, lines: priced.lines.map((line) => ({ direction: "IN" as const, action: "QUANTITY_CHANGE" as const, itemId: line.itemId, storageLocationId: line.storageLocationId, stockType: line.stockType, quantity: line.primaryQuantity, unitCost: line.unitCost ?? undefined, sourceLineId: line.id, orderDocumentType: line.sourceDocumentType, orderDocumentId: line.sourceDocumentId, orderDocumentLineId: line.sourceLineId, })), }, ctx, ); if (!postingResult.ok) { return postingResult; } await repository.save(priced); return ok({ inboundShipmentId: priced.id }); }