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"; /** * updateIncomingPayment — edit a DRAFT incoming payment before it is posted. * * Applies a partial header patch (payment date / total / cash account) and any * combination of settlement edits — add new allocations, patch existing ones by * id, or remove them — in a single transaction. Only a DRAFT payment can be * edited (a POSTED payment must be reversed). After all edits are applied the * effective settlement set must not target the same due-schedule line twice. * `id` is the IncomingPayment id. */ export default createResolver({ name: "updateIncomingPayment", operation: "mutation", input: { id: t.string().description("Incoming payment ID (must be DRAFT)"), headerPatch: t .object( { paymentDate: t.string({ optional: true }).description("New payment date (YYYY-MM-DD)"), totalAmount: t .string({ optional: true }) .description("New payment total — positive magnitude (decimal string)"), paymentAccountId: t.string({ optional: true }).description("New cash/bank GL account"), }, { optional: true }, ) .description("Partial header update"), addSettlements: t .object( { accountReceivableDueScheduleLineId: t .string() .description("Target AR due-schedule line to settle"), settledAmount: t .string() .description("Amount allocated — positive magnitude (decimal string)"), }, { array: true, optional: true }, ) .description("New settlement allocations to add"), updateSettlements: t .object( { settlementId: t .string() .description("Existing settlement ID (must belong to this payment)"), settledAmount: t .string() .description("New allocated amount — positive magnitude (decimal string)"), }, { array: true, optional: true }, ) .description("Existing settlement allocations to patch"), removeSettlementIds: t .string({ array: true, optional: true }) .description("Settlement IDs to remove"), }, body: async (context) => { const ctx = createContext(context); const db = getDB("main-db"); return db .transaction() .execute(async (trx) => { const patch = context.input.headerPatch; const result = await arModulesExport.commands.updateIncomingPayment( trx, { id: context.input.id, headerPatch: patch ? { paymentDate: patch.paymentDate ? new Date(patch.paymentDate) : undefined, totalAmount: patch.totalAmount ?? undefined, paymentAccountId: patch.paymentAccountId ?? undefined, } : undefined, addSettlements: context.input.addSettlements?.map((s) => ({ accountReceivableDueScheduleLineId: s.accountReceivableDueScheduleLineId, settledAmount: s.settledAmount, })), updateSettlements: context.input.updateSettlements?.map((s) => ({ settlementId: s.settlementId, patch: { settledAmount: s.settledAmount }, })), removeSettlementIds: context.input.removeSettlementIds ?? undefined, }, 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 edited: ${context.input.id}`); case "ACCOUNT_RECEIVABLE_INCOMING_PAYMENT_INVALID_AMOUNT": throw new DomainError(`The payment amount must be a positive value.`); case "ACCOUNT_RECEIVABLE_ACCOUNT_NOT_FOUND": throw new DomainError(`Payment account not found.`); case "ACCOUNT_RECEIVABLE_ACCOUNT_INACTIVE": throw new DomainError(`Payment account is not ACTIVE.`); case "ACCOUNT_RECEIVABLE_ACCOUNT_COMPANY_MISMATCH": throw new DomainError(`The payment account belongs to a different company.`); case "ACCOUNT_RECEIVABLE_INCOMING_PAYMENT_SETTLEMENT_INVALID": throw new DomainError( `A settlement allocation is invalid (bad amount or duplicate target).`, ); case "ACCOUNT_RECEIVABLE_INCOMING_PAYMENT_SETTLEMENT_NOT_FOUND": throw new DomainError( `A settlement to patch or remove was not found on this payment.`, ); case "ACCOUNT_RECEIVABLE_INCOMING_PAYMENT_TARGET_INELIGIBLE": throw new DomainError( `A settlement target is not eligible (e.g. the invoice is not posted).`, ); 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 update the incoming payment", { cause: err }); }); }, output: t .object({ id: t.string().description("Updated incoming payment ID"), status: t.string().description("Lifecycle status (DRAFT)"), }) .description("Updated incoming payment"), });