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"; export default createResolver({ name: "createAccountPayableDocument", operation: "mutation", input: { header: t .object({ companyId: t.string().description("Company the invoice belongs to"), supplierAccountId: t.string().description("Supplier account the invoice is owed to"), currencyId: t.string().description("Transaction currency"), payableControlAccountId: t.string().description("GL payable control account (LIABILITY)"), documentType: t.string().description("INVOICE or CREDIT_MEMO"), externalDocumentNumber: t .string({ optional: true }) .description("Supplier's own invoice number"), description: t.string({ optional: true }).description("Invoice description"), documentDate: t.date().description("Invoice document date"), postingDate: t.date().description("GL posting date"), totalAmount: t.string().description("Document total (decimal string)"), }) .description("Invoice header"), lines: t .object( { netAmount: t.string().description("Line net amount (decimal string)"), grossAmount: t.string().description("Line gross amount (decimal string)"), taxAmount: t.string({ optional: true }).description("Line tax amount (decimal string)"), description: t.string({ optional: true }).description("Line description"), sourceType: t .string({ optional: true }) .description("PURCHASE_ORDER for PO-sourced lines"), purchaseOrderLineId: t .string({ optional: true }) .description("Backing purchase order line (PO-sourced)"), quantity: t.string({ optional: true }).description("Ordered quantity (PO-sourced)"), unitPrice: t.string({ optional: true }).description("Ordered unit price (PO-sourced)"), unitId: t.string({ optional: true }).description("Unit of measure (PO-sourced)"), distributions: t .object( { accountId: t.string().description("GL account the amount is booked to"), amount: t.string().description("Distribution amount (decimal string)"), description: t.string({ optional: true }).description("Distribution description"), }, { array: true }, ) .description( "Line distributions (empty for PO-sourced lines; the command derives them)", ), }, { array: true }, ) .description("Invoice lines"), dueSchedule: t .object( { dueDate: t.date().description("Installment due date"), amount: t.string().description("Installment amount (decimal string)"), }, { array: true, optional: true }, ) .description("Payment due schedule"), }, body: async (context) => { const ctx = createContext(context); const db = getDB("main-db"); const { header, lines, dueSchedule } = context.input; return db .transaction() .execute(async (trx) => { const result = await apModulesExport.commands.createAccountPayableDocument( trx, { header: { companyId: header.companyId, supplierAccountId: header.supplierAccountId, currencyId: header.currencyId, payableControlAccountId: header.payableControlAccountId, documentType: header.documentType as "INVOICE" | "CREDIT_MEMO", externalDocumentNumber: header.externalDocumentNumber ?? undefined, description: header.description ?? undefined, documentDate: new Date(header.documentDate), postingDate: new Date(header.postingDate), totalAmount: header.totalAmount, }, lines: lines.map((line) => ({ netAmount: line.netAmount, grossAmount: line.grossAmount, taxAmount: line.taxAmount ?? undefined, description: line.description ?? undefined, sourceType: (line.sourceType ?? undefined) as "PURCHASE_ORDER" | undefined, purchaseOrderLineId: line.purchaseOrderLineId ?? undefined, quantity: line.quantity ?? undefined, unitPrice: line.unitPrice ?? undefined, unitId: line.unitId ?? undefined, distributions: line.distributions.map((distribution) => ({ accountId: distribution.accountId, amount: distribution.amount, description: distribution.description ?? undefined, })), })), dueSchedule: dueSchedule?.map((installment) => ({ dueDate: new Date(installment.dueDate), amount: installment.amount, })), }, ctx, ); if (!result.ok) { switch (result.error.code) { case "ACCOUNT_PAYABLE_COMPANY_NOT_FOUND": throw new DomainError(`Company not found: ${header.companyId}`); case "ACCOUNT_PAYABLE_AP_SUPPLIER_ACCOUNT_NOT_FOUND": throw new DomainError(`Supplier account not found: ${header.supplierAccountId}`); case "ACCOUNT_PAYABLE_AP_INVALID_SUPPLIER_ACCOUNT": throw new DomainError( "The supplier account is not valid for this company or is inactive.", ); case "ACCOUNT_PAYABLE_AP_CURRENCY_NOT_FOUND": throw new DomainError(`Currency not found: ${header.currencyId}`); 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_AP_INVALID_AMOUNT": throw new DomainError(`Invalid amount: ${result.error.message}`); case "ACCOUNT_PAYABLE_AP_LINE_TOTAL_MISMATCH": throw new DomainError( `Line/distribution totals do not reconcile: ${result.error.message}`, ); case "ACCOUNT_PAYABLE_AP_LINE_AMOUNT_MISMATCH": throw new DomainError("A line net amount does not equal quantity times unit price."); case "ACCOUNT_PAYABLE_AP_LINE_SOURCE_INCONSISTENT": throw new DomainError("Line source fields are inconsistent."); case "ACCOUNT_PAYABLE_AP_DUE_SCHEDULE_INVALID": throw new DomainError("The due schedule is invalid or does not reconcile."); 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_AP_ACCRUAL_ACCOUNT_UNRESOLVED": throw new DomainError( "Could not resolve an accrual account for a purchase-order sourced line.", ); case "ACCOUNT_PAYABLE_AP_DOCUMENT_LINE_NOT_FOUND": throw new DomainError("A referenced document line does not exist."); case "ACCOUNT_PAYABLE_AP_PURCHASE_ORDER_ACCOUNT_MISMATCH": throw new DomainError( "The invoice and purchase order must belong to the same company and supplier account.", ); 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 create the account payable invoice", { cause: err }); }); }, output: t .object({ id: t.string().description("Account payable document ID"), status: t.string().description("Document status (DRAFT)"), }) .description("CreateAccountPayableDocument response"), });