import { createContext, DomainError } from "@tailor-platform/erp-kit/app"; import { createResolver, t } from "@tailor-platform/sdk"; import { getDB } from "@/generated/kysely-tailordb"; import { invModulesExport } from "@/modules"; // Updates an existing valuation policy's name, default flag, and posting // accounts. The costing method is intentionally NOT updatable — changing it // after movements have posted would invalidate existing valuation; create a new // policy instead. Omitted fields are left unchanged. export default createResolver({ name: "updateValuationPolicy", operation: "mutation", input: { id: t.string().description("Valuation policy ID to update"), name: t.string({ optional: true }).description("New valuation policy name"), isDefault: t .bool({ optional: true }) .description("Mark (or unmark) as the company's default policy (at most one per company)"), inventoryAccountId: t .string({ optional: true }) .description("Posting account for the inventory asset side"), accrualAccountId: t .string({ optional: true }) .description("Posting account for the uninvoiced-receipt accrual (GR/IR clearing)"), invoicePriceVarianceAccountId: t .string({ optional: true }) .description("Posting account for the invoice-versus-order price difference (clearing)"), cogsAccountId: t .string({ optional: true }) .description("Posting account for cost of goods sold on issues"), ppvAccountId: t .string({ optional: true }) .description("Posting account for purchase price variance"), adjustmentAccountId: t .string({ optional: true }) .description("Posting account for inventory gain/loss adjustments and scrap write-offs"), standardCostAdjustmentAccountId: t .string({ optional: true }) .description("Posting account for standard cost revision revaluations"), consumedPriceVarianceAccountId: t .string({ optional: true }) .description( "Posting account for acquisition price variances that cannot be restated to their consumption", ), }, body: async (context) => { const ctx = createContext(context); const db = getDB("main-db"); return db .transaction() .execute(async (trx) => { // Promoting this policy to the company default: clear the company's // existing default first, in the SAME transaction. The default is unique // per company, so setting a new one while another exists would otherwise // be rejected. Doing both here (rather than as two client mutations) means // the company is never committed with zero defaults — the UI never sees a // momentary "no default" state between two round-trips. if (context.input.isDefault === true) { const target = await trx .selectFrom("ValuationPolicy") .select(["companyId"]) .where("id", "=", context.input.id) .executeTakeFirst(); if (!target) { throw new DomainError(`Valuation policy not found: ${context.input.id}`); } const existingDefault = await trx .selectFrom("ValuationPolicy") .select(["id"]) .where("defaultCompanyId", "=", target.companyId) .where("id", "!=", context.input.id) .executeTakeFirst(); if (existingDefault) { const cleared = await invModulesExport.commands.updateValuationPolicy( trx, { id: existingDefault.id, isDefault: false }, ctx, ); if (!cleared.ok) { throw cleared.error; } } } const result = await invModulesExport.commands.updateValuationPolicy( trx, { id: context.input.id, // Optional GraphQL inputs arrive as `T | null | undefined`; the command // treats `undefined` as "leave unchanged", so coalesce null → undefined. name: context.input.name ?? undefined, isDefault: context.input.isDefault ?? undefined, inventoryAccountId: context.input.inventoryAccountId ?? undefined, accrualAccountId: context.input.accrualAccountId ?? undefined, invoicePriceVarianceAccountId: context.input.invoicePriceVarianceAccountId ?? undefined, cogsAccountId: context.input.cogsAccountId ?? undefined, ppvAccountId: context.input.ppvAccountId ?? undefined, adjustmentAccountId: context.input.adjustmentAccountId ?? undefined, standardCostAdjustmentAccountId: context.input.standardCostAdjustmentAccountId ?? undefined, consumedPriceVarianceAccountId: context.input.consumedPriceVarianceAccountId ?? undefined, }, ctx, ); if (!result.ok) { switch (result.error.code) { case "INVENTORY_VALUATION_POLICY_NOT_FOUND": throw new DomainError(`Valuation policy not found: ${context.input.id}`); case "INVENTORY_ACCOUNT_NOT_FOUND": throw new DomainError( "A posting account does not exist in the company's chart of accounts", ); case "INVENTORY_DEFAULT_VALUATION_POLICY_ALREADY_EXISTS": throw new DomainError("The company already has a default valuation policy"); case "UNAUTHENTICATED": throw new DomainError("Authentication is required"); case "INSUFFICIENT_PERMISSION": throw new DomainError("You do not have permission to perform this action"); default: throw result.error satisfies never; } } return { id: result.value.valuationPolicy.id }; }) .catch((err: unknown) => { if (err instanceof DomainError) throw err; throw new Error("Failed to update the valuation policy", { cause: err }); }); }, output: t .object({ id: t.string().description("Updated valuation policy ID"), }) .description("Updated valuation policy"), });