import { err, ok, type CommandContext } from "@tailor-platform/erp-kit/core"; import { Decimal } from "decimal.js"; import { stockReservationLifecycle } from "../db/stockReservation.lifecycle.generated"; import type { StockReservationSourceType } from "../generated/enums"; import type { Transaction } from "../generated/kysely-tailordb"; import { calculateSiteAvailableExcludingReservation, getOpenStockReservationQuantity, } from "../internal/stockReservation"; import { InsufficientAvailableStockError, InvalidReservationQuantitiesError, ItemNotFoundError, SiteNotFoundError, StockReservationNotFoundError, StockReservationNotOpenError, StorageLocationNotFoundError, } from "../lib/errors.generated"; import type { ItemManagementQueries, OrganizationQueries } from "../module"; export interface UpdateStockReservationInput { id: string; sourceType?: StockReservationSourceType; sourceLineId?: string; sourceId?: string; itemId?: string; siteId?: string; storageLocationId?: string | null; reservedQuantity?: string; requiredDate?: Date; } export async function run( db: Transaction, input: UpdateStockReservationInput, ctx: CommandContext, itemManagementQueries: Pick, organizationQueries: Pick, ) { const { sourceType, sourceLineId, sourceId, itemId, siteId, storageLocationId, reservedQuantity: inputReservedQuantity, requiredDate, } = input; const existing = await db .selectFrom("StockReservation") .selectAll() .where("id", "=", input.id) .forUpdate() .executeTakeFirst(); if (!existing) { return err(new StockReservationNotFoundError(input.id)); } const nextStatus = stockReservationLifecycle.tryTransition(existing.status, "update"); if (!nextStatus) { return err(new StockReservationNotOpenError(existing.id)); } const updateSet: Record = { status: nextStatus }; if (sourceId !== undefined) { updateSet.sourceId = sourceId; } if (sourceType !== undefined) { updateSet.sourceType = sourceType; } if (sourceLineId !== undefined) { updateSet.sourceLineId = sourceLineId; } const effectiveItemId = itemId ?? existing.itemId; if (itemId !== undefined) { const { item } = (await itemManagementQueries.getItem(db, { id: effectiveItemId }, ctx)).value; if (!item) { return err(new ItemNotFoundError(effectiveItemId)); } updateSet.itemId = itemId; } const effectiveSiteId = siteId ?? existing.siteId; if (siteId !== undefined) { const { site } = (await organizationQueries.getSite(db, { id: siteId }, ctx)).value; if (!site) { return err(new SiteNotFoundError(siteId)); } updateSet.siteId = siteId; } const effectiveStorageLocationId = storageLocationId !== undefined ? storageLocationId : existing.storageLocationId; if ((siteId !== undefined || storageLocationId !== undefined) && effectiveStorageLocationId) { const location = await db .selectFrom("StorageLocation") .selectAll() .where("id", "=", effectiveStorageLocationId) .where("siteId", "=", effectiveSiteId) .executeTakeFirst(); if (!location) { return err(new StorageLocationNotFoundError(effectiveStorageLocationId)); } } if (storageLocationId !== undefined) { updateSet.storageLocationId = storageLocationId; } const effectiveReservedQuantity = inputReservedQuantity ?? existing.reservedQuantity; const reservedQuantity = new Decimal(effectiveReservedQuantity); const consumedQuantity = new Decimal(existing.consumedQuantity); if (reservedQuantity.lt(0) || reservedQuantity.lt(consumedQuantity)) { return err(new InvalidReservationQuantitiesError(effectiveReservedQuantity)); } const openReservedQuantity = getOpenStockReservationQuantity({ reservedQuantity: effectiveReservedQuantity, consumedQuantity: existing.consumedQuantity, }); const available = await calculateSiteAvailableExcludingReservation( db, effectiveItemId, effectiveSiteId, existing.id, ); if (!available || available.lt(openReservedQuantity)) { return err(new InsufficientAvailableStockError(`${effectiveItemId}:${effectiveSiteId}`)); } if (inputReservedQuantity !== undefined) { updateSet.reservedQuantity = inputReservedQuantity; } if (requiredDate !== undefined) { updateSet.requiredDate = requiredDate; } const stockReservation = await db .updateTable("StockReservation") .set(updateSet) .where("id", "=", existing.id) .returningAll() .executeTakeFirstOrThrow(); return ok({ stockReservation }); }