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"; /** * postAccountPayableDocument — post a REGISTERED vendor invoice to the general * ledger. * * Posting is the accounting step that "confirms" the invoice for payment: the * native command builds a balanced JournalEntry from the invoice's distribution * lines and the payable control account (an INVOICE debits the distribution * accounts and credits the payable control account; a CREDIT_MEMO reverses it), * posts it directly in financial-accounting under sourceDocumentType * ACCOUNT_PAYABLE_DOCUMENT, and moves the AP document REGISTERED -> POSTED in * the same transaction. Posting refuses to run while any active hold remains, * and requires the document currency to equal the company's base currency. * `id` is the AccountPayableDocument id. */ export default createResolver({ name: "postAccountPayableDocument", operation: "mutation", input: { id: t.string().description("Account payable document ID"), }, body: async (context) => { const ctx = createContext(context); const db = getDB("main-db"); return db .transaction() .execute(async (trx) => { const result = await apModulesExport.commands.postAccountPayableDocument( trx, { id: context.input.id }, 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 REGISTERED invoices can be posted: ${context.input.id}`); case "ACCOUNT_PAYABLE_AP_DOCUMENT_HAS_ACTIVE_HOLD": throw new DomainError( `Cannot post while an active hold remains; release all holds first.`, ); case "ACCOUNT_PAYABLE_AP_POSTING_CURRENCY_MISMATCH": throw new DomainError( `The invoice currency must equal the company base currency to post.`, ); case "ACCOUNT_PAYABLE_AP_ACCOUNTING_PERIOD_NOT_FOUND": throw new DomainError(`No accounting period covers the invoice posting date.`); case "ACCOUNT_PAYABLE_AP_LINE_TOTAL_MISMATCH": throw new DomainError( `Line/distribution totals do not reconcile with the document total.`, ); 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_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_COMPANY_NOT_FOUND": throw new DomainError(`The invoice company does not exist.`); case "ACCOUNT_PAYABLE_AP_JOURNAL_ENTRY_CREATE_FAILED": throw new DomainError(`Failed to create the journal entry for this invoice.`); case "ACCOUNT_PAYABLE_AP_JOURNAL_ENTRY_POST_FAILED": throw new DomainError(`Failed to post the journal entry for this invoice.`); case "ACCOUNT_PAYABLE_AP_ACQUISITION_COST_ADJUSTMENT_FAILED": throw new DomainError("Failed to adjust acquisition costs for this invoice."); 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 post the account payable document", { cause: err }); }); }, output: t .object({ id: t.string().description("Account payable document ID"), status: t.string().description("New document status (POSTED)"), }) .description("PostAccountPayableDocument response"), });