import { ok, err, type CommandContext } from "@tailor-platform/erp-kit/core"; import type { Transaction } from "../generated/kysely-tailordb"; import { ItemNotFoundError, ItemValuationPolicyChangeNotAllowedError, SiteNotFoundError, ValuationPolicyCompanyMismatchError, ValuationPolicyNotFoundError, } from "../lib/errors.generated"; import type { ItemManagementQueries, OrganizationQueries } from "../module"; export interface SetItemValuationPolicyInput { itemId: string; companyId: string; valuationPolicyId: string; } /** * Function: setItemValuationPolicy * * Assigns a specific valuation policy to an item in a company by creating (or * re-pointing) its ItemValuation record, overriding the default that a first * movement would otherwise apply. The policy is frozen once the item has any * InventoryLedger history in the company. */ export async function run( db: Transaction, input: SetItemValuationPolicyInput, ctx: CommandContext, itemManagementQueries: Pick, organizationQueries: Pick, ) { const { itemId, companyId, valuationPolicyId } = input; const { item } = (await itemManagementQueries.getItem(db, { id: itemId }, ctx)).value; if (!item) { return err(new ItemNotFoundError(itemId)); } const policy = await db .selectFrom("ValuationPolicy") .selectAll() .where("id", "=", valuationPolicyId) .executeTakeFirst(); if (!policy) { return err(new ValuationPolicyNotFoundError(valuationPolicyId)); } // The policy carries its company's posting accounts; assigning it under // another company would post costing to the wrong ledger. if (policy.companyId !== companyId) { return err(new ValuationPolicyCompanyMismatchError(`${valuationPolicyId}:${companyId}`)); } const existing = await db .selectFrom("ItemValuation") .selectAll() .where("itemId", "=", itemId) .where("companyId", "=", companyId) .forUpdate() .executeTakeFirst(); if (!existing) { const created = await db .insertInto("ItemValuation") .values({ itemId, companyId, valuationPolicyId, }) .returningAll() .executeTakeFirstOrThrow(); return ok({ itemValuation: created }); } if (existing.valuationPolicyId === valuationPolicyId) { return ok({ itemValuation: existing }); } // The freeze is per company: history in another company's sites does not // constrain this assignment. const movedSites = await db .selectFrom("InventoryLedger") .innerJoin("StorageLocation", "StorageLocation.id", "InventoryLedger.storageLocationId") .select("StorageLocation.siteId as siteId") .where("InventoryLedger.itemId", "=", itemId) .groupBy("StorageLocation.siteId") .execute(); for (const row of movedSites) { const { site } = (await organizationQueries.getSite(db, { id: row.siteId }, ctx)).value; if (!site) { return err(new SiteNotFoundError(row.siteId)); } if (site.companyId === companyId) { return err(new ItemValuationPolicyChangeNotAllowedError(itemId)); } } const updated = await db .updateTable("ItemValuation") .set({ valuationPolicyId }) .where("id", "=", existing.id) .returningAll() .executeTakeFirstOrThrow(); return ok({ itemValuation: updated }); }