import { err, ok } from "@tailor-platform/erp-kit/core"; import { Decimal } from "decimal.js"; import { inventorySupplyPlanLifecycle } from "../db/inventorySupplyPlan.lifecycle.generated"; import type { InventorySupplyPlanSourceType } from "../generated/enums"; import type { Transaction } from "../generated/kysely-tailordb"; import { InventorySupplyPlanNotFoundError, InvalidSupplyPlanQuantitiesError, SupplyPlanNotOpenError, } from "../lib/errors.generated"; export interface InventorySupplyPlanQuantity { expectedQuantity: string; receivedQuantity: string; } export interface ConsumeInventorySupplyPlanInternalInput { id: string; consumedQuantity: string; } export interface ConsumeInboundSupplyPlansInternalInput { itemId: string; siteId: string; quantity: string; sourceDocumentType: string | null; sourceDocumentId: string | null; sourceLineId: string | null; } export function getOpenInventorySupplyPlanQuantity(supplyPlan: InventorySupplyPlanQuantity) { return Decimal.max( new Decimal(supplyPlan.expectedQuantity).minus(supplyPlan.receivedQuantity), 0, ); } function isInventorySupplyPlanSourceType( sourceType: string, ): sourceType is InventorySupplyPlanSourceType { return sourceType === "PURCHASE_ORDER" || sourceType === "TRANSFER_ORDER"; } export async function consumeInventorySupplyPlanInternal( db: Transaction, input: ConsumeInventorySupplyPlanInternalInput, ) { const consumedQuantity = new Decimal(input.consumedQuantity); if (consumedQuantity.lte(0)) { return err(new InvalidSupplyPlanQuantitiesError(input.consumedQuantity)); } const existing = await db .selectFrom("InventorySupplyPlan") .selectAll() .where("id", "=", input.id) .forUpdate() .executeTakeFirst(); if (!existing) { return err(new InventorySupplyPlanNotFoundError(input.id)); } if (consumedQuantity.gt(getOpenInventorySupplyPlanQuantity(existing))) { return err(new InvalidSupplyPlanQuantitiesError(input.consumedQuantity)); } const nextStatus = inventorySupplyPlanLifecycle.tryTransition(existing.status, "consume"); if (!nextStatus) { return err(new SupplyPlanNotOpenError(existing.id)); } const supplyPlan = await db .updateTable("InventorySupplyPlan") .set({ receivedQuantity: new Decimal(existing.receivedQuantity).plus(consumedQuantity).toString(), status: nextStatus, }) .where("id", "=", existing.id) .returningAll() .executeTakeFirstOrThrow(); return ok({ supplyPlan }); } export async function consumeInboundSupplyPlansInternal( db: Transaction, input: ConsumeInboundSupplyPlansInternalInput, ) { if (!input.sourceDocumentType || !input.sourceDocumentId || !input.sourceLineId) { return ok({ supplyPlans: [] }); } if (!isInventorySupplyPlanSourceType(input.sourceDocumentType)) { return ok({ supplyPlans: [] }); } const matchingSupplyPlans = await db .selectFrom("InventorySupplyPlan") .selectAll() .where("itemId", "=", input.itemId) .where("siteId", "=", input.siteId) .where("sourceType", "=", input.sourceDocumentType) .where("sourceId", "=", input.sourceDocumentId) .where("sourceLineId", "=", input.sourceLineId) .where("status", "=", "OPEN") .orderBy("expectedDate", "asc") .orderBy("id", "asc") .forUpdate() .execute(); if (matchingSupplyPlans.length === 0) { return ok({ supplyPlans: [] }); } const consumedSupplyPlans = []; let remainingQuantity = new Decimal(input.quantity); for (const supplyPlan of matchingSupplyPlans) { if (remainingQuantity.lte(0)) break; const consumedQuantity = Decimal.min( getOpenInventorySupplyPlanQuantity(supplyPlan), remainingQuantity, ); if (consumedQuantity.lte(0)) continue; const consumeResult = await consumeInventorySupplyPlanInternal(db, { id: supplyPlan.id, consumedQuantity: consumedQuantity.toString(), }); if (!consumeResult.ok) { return consumeResult; } consumedSupplyPlans.push(consumeResult.value.supplyPlan); remainingQuantity = remainingQuantity.minus(consumedQuantity); } return ok({ supplyPlans: consumedSupplyPlans }); }