import { err, ok, type CommandContext } from "@tailor-platform/erp-kit/core"; import { post, sumFulfillmentQuantitiesBySalesOrderLine } from "../domain/outboundShipment"; import type { Transaction } from "../generated/kysely-tailordb"; import { InvalidStatusError, SalesOrderFulfillmentSyncFailedError } from "../lib/errors.generated"; import type { InventoryCommands, SalesCommands } from "../module"; import { createOutboundShipmentRepository, type OutboundShipmentRepository, } from "../repository/outboundShipmentRepository"; export interface PostOutboundShipmentInput { id: string; } export async function run( db: Transaction, input: PostOutboundShipmentInput, ctx: CommandContext, inventoryCommands: Pick, salesCommands: Pick, repositoryFactory: ( db: Transaction, ) => OutboundShipmentRepository = createOutboundShipmentRepository, ) { // 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; } const postingResult = await inventoryCommands.postInventoryLedger( db, { sourceType: "OUTBOUND_SHIPMENT", sourceId: posted.value.id, effectiveDate: posted.value.header.effectiveDate, lines: posted.value.lines.map((line) => ({ direction: "OUT" as const, action: "QUANTITY_CHANGE" as const, itemId: line.itemId, storageLocationId: line.storageLocationId, stockType: line.stockType, quantity: line.primaryQuantity, sourceLineId: line.id, orderDocumentType: line.sourceDocumentType, orderDocumentId: line.sourceDocumentId, orderDocumentLineId: line.sourceLineId, })), }, ctx, ); if (!postingResult.ok) { return postingResult; } // Feed this shipment's shipped quantities to sales as a delta. Sales adds it // under a row lock, so concurrent posts of the same order line can't lost-update. const fulfillments = sumFulfillmentQuantitiesBySalesOrderLine(posted.value); if (fulfillments.length > 0) { const fulfillmentSyncResult = await salesCommands.recalculateSalesOrderFulfillmentStatus( db, { lineFulfillments: fulfillments.map(({ salesOrderLineId, quantity }) => ({ salesOrderLineId, fulfilledQuantityDelta: quantity, })), }, ctx, ); if (!fulfillmentSyncResult.ok) { return err(new SalesOrderFulfillmentSyncFailedError(posted.value.id)); } } await repository.save(posted.value); return ok({ outboundShipmentId: posted.value.id }); }