import { createContext, DomainError } from "@tailor-platform/erp-kit/app"; import { createResolver, t } from "@tailor-platform/sdk"; import { getDB } from "@/generated/kysely-tailordb"; import { apModulesExport } from "@/modules"; const compactPatch = >(patch: T): T => Object.fromEntries(Object.entries(patch).filter(([, value]) => value != null)) as T; export default createResolver({ name: "updateAccountPayableDocument", operation: "mutation", input: { id: t.string().description("Account payable document ID (must be DRAFT)"), headerPatch: t .object( { documentDate: t.date({ optional: true }).description("Document date"), externalDocumentNumber: t .string({ optional: true }) .description("Supplier's own invoice number"), description: t.string({ optional: true }).description("Invoice description"), payableControlAccountId: t .string({ optional: true }) .description("GL payable control account (LIABILITY)"), totalAmount: t .string({ optional: true }) .description("Recomputed document total (decimal string); required when lines change"), }, { optional: true }, ) .description("Header fields to patch"), addLines: t .object( { netAmount: t.string().description("Line net amount (decimal string)"), grossAmount: t.string().description("Line gross amount (decimal string)"), taxAmount: t.string({ optional: true }).description("Line tax amount (decimal string)"), description: t.string({ optional: true }).description("Line description"), distributions: t .object( { accountId: t.string().description("GL account the amount is booked to"), amount: t.string().description("Distribution amount (decimal string)"), description: t.string({ optional: true }).description("Distribution description"), }, { array: true }, ) .description("Line distributions (must sum to grossAmount)"), }, { array: true, optional: true }, ) .description("Amount-only lines to add"), updateLines: t .object( { lineId: t.string().description("Existing line to edit"), patch: t .object({ netAmount: t.string({ optional: true }).description("New net amount"), grossAmount: t.string({ optional: true }).description("New gross amount"), taxAmount: t.string({ optional: true }).description("New tax amount"), description: t.string({ optional: true }).description("New description"), }) .description("Line field patch"), addDistributions: t .object( { accountId: t.string().description("GL account the amount is booked to"), amount: t.string().description("Distribution amount (decimal string)"), description: t.string({ optional: true }).description("Distribution description"), }, { array: true, optional: true }, ) .description("Distributions to add to this line"), updateDistributions: t .object( { distributionLineId: t.string().description("Existing distribution line to edit"), patch: t .object({ accountId: t.string({ optional: true }).description("Re-point to a GL account"), amount: t.string({ optional: true }).description("New distribution amount"), description: t.string({ optional: true }).description("New description"), }) .description("Distribution field patch"), }, { array: true, optional: true }, ) .description("Distributions to edit on this line"), removeDistributionLineIds: t .string({ array: true, optional: true }) .description("Distributions to remove from this line"), }, { array: true, optional: true }, ) .description("Edits to existing lines"), removeLineIds: t .string({ array: true, optional: true }) .description("Existing lines to remove"), dueSchedule: t .object( { dueDate: t.date().description("Installment due date"), amount: t.string().description("Installment amount (decimal string)"), }, { array: true, optional: true }, ) .description("Replacement due schedule (omit to leave the current schedule unchanged)"), }, body: async (context) => { const ctx = createContext(context); const db = getDB("main-db"); return db .transaction() .execute(async (trx) => { const { headerPatch, addLines, updateLines, removeLineIds, dueSchedule } = context.input; const result = await apModulesExport.commands.updateAccountPayableDocument( trx, { id: context.input.id, headerPatch: headerPatch ? { ...headerPatch, payableControlAccountId: headerPatch.payableControlAccountId ?? undefined, totalAmount: headerPatch.totalAmount ?? undefined, documentDate: headerPatch.documentDate ? new Date(headerPatch.documentDate) : undefined, } : undefined, addLines: addLines ?? undefined, updateLines: updateLines?.map((edit) => ({ lineId: edit.lineId, patch: edit.patch ? compactPatch({ description: edit.patch.description ?? undefined, taxAmount: edit.patch.taxAmount ?? undefined, netAmount: edit.patch.netAmount ?? undefined, grossAmount: edit.patch.grossAmount ?? undefined, }) : undefined, addDistributions: edit.addDistributions ?? undefined, updateDistributions: edit.updateDistributions?.map((d) => ({ distributionLineId: d.distributionLineId, patch: compactPatch({ accountId: d.patch.accountId ?? undefined, amount: d.patch.amount ?? undefined, description: d.patch.description ?? undefined, }), })), removeDistributionLineIds: edit.removeDistributionLineIds ?? undefined, })), removeLineIds: removeLineIds ?? undefined, dueSchedule: dueSchedule?.map((d) => ({ ...d, dueDate: new Date(d.dueDate) })), }, ctx, ); if (!result.ok) { switch (result.error.code) { case "ACCOUNT_PAYABLE_AP_DOCUMENT_NOT_FOUND": throw new DomainError(`Account payable document not found: ${context.input.id}`); case "ACCOUNT_PAYABLE_AP_INVALID_DOCUMENT_STATUS": throw new DomainError(`Only DRAFT invoices can be edited: ${context.input.id}`); case "ACCOUNT_PAYABLE_ACCOUNT_NOT_FOUND": throw new DomainError("A referenced GL account does not exist."); case "ACCOUNT_PAYABLE_ACCOUNT_INACTIVE": throw new DomainError("A referenced GL account is not ACTIVE."); case "ACCOUNT_PAYABLE_ACCOUNT_COMPANY_MISMATCH": throw new DomainError("A referenced GL account belongs to a different company."); case "ACCOUNT_PAYABLE_AP_CURRENCY_NOT_FOUND": throw new DomainError("Referenced currency not found."); case "ACCOUNT_PAYABLE_AP_SUPPLIER_ACCOUNT_NOT_FOUND": throw new DomainError("Referenced supplier account does not exist."); case "ACCOUNT_PAYABLE_AP_INVALID_SUPPLIER_ACCOUNT": throw new DomainError( "The supplier account is unavailable for transactions or belongs to another company.", ); case "ACCOUNT_PAYABLE_AP_PURCHASE_ORDER_ACCOUNT_MISMATCH": throw new DomainError( "A referenced purchase order belongs to a different company or supplier account.", ); case "ACCOUNT_PAYABLE_AP_DERIVED_DISTRIBUTION_IMMUTABLE": throw new DomainError("System-derived distributions cannot be edited."); case "ACCOUNT_PAYABLE_AP_DOCUMENT_LINE_NOT_FOUND": throw new DomainError("A referenced document line does not exist."); case "ACCOUNT_PAYABLE_AP_INVALID_AMOUNT": throw new DomainError(`Invalid amount: ${result.error.message}`); case "ACCOUNT_PAYABLE_AP_LINE_EDIT_CONFLICT": throw new DomainError("Conflicting line edits were supplied."); case "ACCOUNT_PAYABLE_AP_LINE_TOTAL_MISMATCH": throw new DomainError( `Line/distribution totals do not reconcile: ${result.error.message}`, ); case "ACCOUNT_PAYABLE_AP_MINIMUM_LINES_NOT_MET": throw new DomainError( "The invoice must have at least one line with one distribution.", ); case "ACCOUNT_PAYABLE_AP_LINE_SOURCE_INCONSISTENT": throw new DomainError("Line source fields are inconsistent."); case "ACCOUNT_PAYABLE_AP_LINE_AMOUNT_MISMATCH": throw new DomainError("A line net amount does not equal quantity times unit price."); case "ACCOUNT_PAYABLE_AP_DUE_SCHEDULE_INVALID": throw new DomainError( "The due schedule is invalid or does not reconcile with the total.", ); case "ACCOUNT_PAYABLE_AP_ACCRUAL_ACCOUNT_UNRESOLVED": throw new DomainError( "Could not resolve an accrual account for a purchase-order sourced line.", ); 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.accountPayableDocument.id, status: result.value.accountPayableDocument.status, }; }) .catch((err: unknown) => { if (err instanceof DomainError) throw err; throw new Error("Failed to update the account payable invoice", { cause: err }); }); }, output: t .object({ id: t.string().description("Account payable document ID"), status: t.string().description("Document status (DRAFT)"), }) .description("UpdateAccountPayableDocument response"), });