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"; /** * createIncomingPayment — create an editable (DRAFT) incoming payment that * settles open account-receivable due-schedule lines. * * A payment records who paid (`customerAccountId` — a CUSTOMER), into which * cash/bank GL account (`paymentAccountId`), on what date, for how much, and an * optional set of settlements. Each settlement allocates part of the payment to * an `AccountReceivableDueScheduleLine` — an invoice's due installment, or a * credit memo's negative installment (a payment may settle invoices and credit * memos together at their signed net). Creation does NOT post to the GL; that * happens at `postIncomingPayment`. `totalAmount` is a positive magnitude. This * is the AR mirror of `createOutgoingPayment`. */ export default createResolver({ name: "createIncomingPayment", operation: "mutation", input: { companyId: t.string().description("Owning company whose ledger the payment posts to"), customerAccountId: t.string().description("Customer that paid"), currencyId: t .string() .description("Payment currency (must equal the company base currency to post)"), paymentAccountId: t.string().description("Cash/bank GL account the payment is received into"), paymentDate: t.string().description("Payment date (YYYY-MM-DD)"), totalAmount: t.string().description("Payment total — positive magnitude (decimal string)"), settlements: t .object( { accountReceivableDueScheduleLineId: t .string() .description("Target AR due-schedule line to settle"), settledAmount: t .string() .description("Amount allocated to this line — positive magnitude (decimal string)"), }, { array: true }, ) .description("Settlement allocations (may be empty; editable until the payment is posted)"), }, body: async (context) => { const db = getDB("main-db"); return db .transaction() .execute(async (trx) => { const result = await arModulesExport.commands.createIncomingPayment( trx, { companyId: context.input.companyId, customerAccountId: context.input.customerAccountId, currencyId: context.input.currencyId, paymentAccountId: context.input.paymentAccountId, paymentDate: new Date(context.input.paymentDate), totalAmount: context.input.totalAmount, settlements: context.input.settlements.map((s) => ({ accountReceivableDueScheduleLineId: s.accountReceivableDueScheduleLineId, settledAmount: s.settledAmount, })), }, createContext(context), ); if (!result.ok) { switch (result.error.code) { case "ACCOUNT_RECEIVABLE_COMPANY_NOT_FOUND": throw new DomainError(`Company not found: ${context.input.companyId}`); case "ACCOUNT_RECEIVABLE_COMPANY_INACTIVE": throw new DomainError(`Company is not active: ${context.input.companyId}`); case "ACCOUNT_RECEIVABLE_CUSTOMER_ACCOUNT_NOT_FOUND": throw new DomainError(`Customer not found: ${context.input.customerAccountId}`); case "ACCOUNT_RECEIVABLE_INVALID_CUSTOMER_ACCOUNT": throw new DomainError( `The customer account is not valid for this company or is inactive: ${context.input.customerAccountId}`, ); case "ACCOUNT_RECEIVABLE_CURRENCY_NOT_FOUND": throw new DomainError(`Currency not found: ${context.input.currencyId}`); case "ACCOUNT_RECEIVABLE_ACCOUNT_NOT_FOUND": throw new DomainError(`Payment account not found: ${context.input.paymentAccountId}`); case "ACCOUNT_RECEIVABLE_ACCOUNT_INACTIVE": throw new DomainError( `Payment account is not ACTIVE: ${context.input.paymentAccountId}`, ); case "ACCOUNT_RECEIVABLE_ACCOUNT_COMPANY_MISMATCH": throw new DomainError(`The payment account belongs to a different company.`); case "ACCOUNT_RECEIVABLE_INCOMING_PAYMENT_INVALID_AMOUNT": throw new DomainError(`The payment amount must be a positive value.`); case "ACCOUNT_RECEIVABLE_INCOMING_PAYMENT_SETTLEMENT_INVALID": throw new DomainError( `A settlement allocation is invalid (bad amount or ineligible target).`, ); 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 create the incoming payment", { cause: err }); }); }, output: t .object({ id: t.string().description("Created incoming payment ID"), status: t.string().description("Lifecycle status (DRAFT on create)"), }) .description("Created incoming payment"), });