import { createContext, DomainError } from "@tailor-platform/erp-kit/app"; import { createResolver, t } from "@tailor-platform/sdk"; import { getDB } from "@/generated/kysely-tailordb"; import { arModulesExport } from "@/modules"; /** * postIncomingPayment — post a DRAFT incoming payment to the general ledger. * * Posting books the balanced cash-receipt entry (Dr Cash / Cr Accounts * Receivable for the settled amount, direction-aware for credit-memo * settlements) under `sourceDocumentType = INCOMING_PAYMENT`, applies the * settlement allocations against the targeted AR due-schedule lines, and moves * the payment DRAFT -> POSTED in the same transaction. It rejects * over-settlement (receiving more than an open item's balance), a total that * does not reconcile with the settlements, ineligible targets (e.g. an unposted * invoice), and a payment currency that differs from the company base currency. * `id` is the IncomingPayment id. This is the AR mirror of `postOutgoingPayment`. */ export default createResolver({ name: "postIncomingPayment", operation: "mutation", input: { id: t.string().description("Incoming payment ID"), }, body: async (context) => { const ctx = createContext(context); const db = getDB("main-db"); return db .transaction() .execute(async (trx) => { const result = await arModulesExport.commands.postIncomingPayment( trx, { id: context.input.id }, ctx, ); if (!result.ok) { switch (result.error.code) { case "ACCOUNT_RECEIVABLE_INCOMING_PAYMENT_NOT_FOUND": throw new DomainError(`Incoming payment not found: ${context.input.id}`); case "ACCOUNT_RECEIVABLE_INCOMING_PAYMENT_INVALID_STATUS": throw new DomainError(`Only a DRAFT payment can be posted: ${context.input.id}`); case "ACCOUNT_RECEIVABLE_INCOMING_PAYMENT_OVER_SETTLEMENT": throw new DomainError(`A settlement exceeds the open balance of its target line.`); case "ACCOUNT_RECEIVABLE_INCOMING_PAYMENT_TOTAL_MISMATCH": throw new DomainError(`The payment total does not reconcile with its settlements.`); case "ACCOUNT_RECEIVABLE_INCOMING_PAYMENT_TARGET_INELIGIBLE": throw new DomainError( `A settlement target is not eligible (e.g. the invoice is not posted).`, ); case "ACCOUNT_RECEIVABLE_INCOMING_PAYMENT_SETTLEMENT_INVALID": throw new DomainError(`A settlement allocation is invalid.`); case "ACCOUNT_RECEIVABLE_INCOMING_PAYMENT_INVALID_AMOUNT": throw new DomainError(`The payment amount must be a positive value.`); case "ACCOUNT_RECEIVABLE_INCOMING_PAYMENT_ACCOUNTING_PERIOD_NOT_FOUND": throw new DomainError(`No accounting period covers the payment date.`); case "ACCOUNT_RECEIVABLE_INCOMING_PAYMENT_JOURNAL_ENTRY_CREATE_FAILED": throw new DomainError(`Failed to create the journal entry for this payment.`); case "ACCOUNT_RECEIVABLE_INCOMING_PAYMENT_JOURNAL_ENTRY_POST_FAILED": throw new DomainError(`Failed to post the journal entry for this payment.`); case "ACCOUNT_RECEIVABLE_COMPANY_NOT_FOUND": throw new DomainError("The payment company does not exist."); case "ACCOUNT_RECEIVABLE_COMPANY_INACTIVE": throw new DomainError("The payment company is not active."); case "ACCOUNT_RECEIVABLE_CUSTOMER_ACCOUNT_NOT_FOUND": throw new DomainError("The payment customer does not exist."); case "ACCOUNT_RECEIVABLE_INVALID_CUSTOMER_ACCOUNT": throw new DomainError( "The customer account is not valid for this company or is inactive.", ); case "ACCOUNT_RECEIVABLE_CURRENCY_NOT_FOUND": throw new DomainError("The payment currency is not configured."); case "ACCOUNT_RECEIVABLE_ACCOUNT_NOT_FOUND": throw new DomainError("A referenced GL account does not exist."); case "ACCOUNT_RECEIVABLE_ACCOUNT_INACTIVE": throw new DomainError("A referenced GL account is not ACTIVE."); case "ACCOUNT_RECEIVABLE_ACCOUNT_COMPANY_MISMATCH": throw new DomainError("A referenced GL account belongs to a different company."); 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.incomingPayment.id, status: result.value.incomingPayment.status, }; }) .catch((err: unknown) => { if (err instanceof DomainError) throw err; throw new Error("Failed to post the incoming payment", { cause: err }); }); }, output: t .object({ id: t.string().description("Posted incoming payment ID"), status: t.string().description("Lifecycle status (POSTED after posting)"), }) .description("Posted incoming payment"), });