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"; /** * reverseOutgoingPayment — reverse a POSTED outgoing payment. * * Reversal posts an exact reversing journal entry (dated `reversalDate`), * releases the settlement allocations so the previously-paid AP due-schedule * lines become open again, and moves the payment POSTED -> REVERSED. Use this to * undo a payment that was posted in error while keeping a full audit trail (the * original entry is not deleted). A DRAFT payment cannot be reversed (cancel it * instead), and an already-reversed payment is rejected. `id` is the * OutgoingPayment id. */ export default createResolver({ name: "reverseOutgoingPayment", operation: "mutation", input: { id: t.string().description("Outgoing payment ID"), reversalDate: t.string().description("Reversal posting date (YYYY-MM-DD)"), }, body: async (context) => { const ctx = createContext(context); const db = getDB("main-db"); return db .transaction() .execute(async (trx) => { const result = await apModulesExport.commands.reverseOutgoingPayment( trx, { id: context.input.id, reversalDate: new Date(context.input.reversalDate) }, ctx, ); if (!result.ok) { switch (result.error.code) { case "ACCOUNT_PAYABLE_OUTGOING_PAYMENT_NOT_FOUND": throw new DomainError(`Outgoing payment not found: ${context.input.id}`); case "ACCOUNT_PAYABLE_OUTGOING_PAYMENT_INVALID_STATUS": throw new DomainError(`Only a POSTED payment can be reversed: ${context.input.id}`); case "ACCOUNT_PAYABLE_OUTGOING_PAYMENT_ALREADY_REVERSED": throw new DomainError(`This payment has already been reversed: ${context.input.id}`); case "ACCOUNT_PAYABLE_OUTGOING_PAYMENT_REVERSAL_NOT_ALLOWED": throw new DomainError(`This payment cannot be reversed in its current state.`); case "ACCOUNT_PAYABLE_OUTGOING_PAYMENT_ACCOUNTING_PERIOD_NOT_FOUND": throw new DomainError(`No accounting period covers the reversal date.`); case "ACCOUNT_PAYABLE_OUTGOING_PAYMENT_JOURNAL_ENTRY_CREATE_FAILED": throw new DomainError(`Failed to create the reversing journal entry.`); case "ACCOUNT_PAYABLE_OUTGOING_PAYMENT_JOURNAL_ENTRY_POST_FAILED": throw new DomainError(`Failed to post the reversing journal entry.`); 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.outgoingPayment.id, status: result.value.outgoingPayment.status, }; }) .catch((err: unknown) => { if (err instanceof DomainError) throw err; throw new Error("Failed to reverse the outgoing payment", { cause: err }); }); }, output: t .object({ id: t.string().description("Reversed outgoing payment ID"), status: t.string().description("Lifecycle status (REVERSED after reversal)"), }) .description("Reversed outgoing payment"), });