import { err, ok, type CommandContext } from "@tailor-platform/erp-kit/core"; import { Decimal } from "decimal.js"; import type { Transaction } from "../generated/kysely-tailordb"; import { validateIncomingPaymentSettlementTargets } from "../internal/incomingPaymentSettlementValidation"; import { AccountCompanyMismatchError, AccountInactiveError, AccountNotFoundError, CustomerAccountNotFoundError, CurrencyNotFoundError, InvalidCustomerAccountError, CompanyInactiveError, CompanyNotFoundError, IncomingPaymentInvalidAmountError, IncomingPaymentSettlementInvalidError, } from "../lib/errors.generated"; import type { BusinessPartnerQueries, CoaManagementQueries, OrganizationQueries, PrimitivesQueries, } from "../module"; export interface IncomingPaymentSettlementInput { accountReceivableDueScheduleLineId: string; settledAmount: string; } export interface CreateIncomingPaymentInput { companyId: string; customerAccountId: string; currencyId: string; paymentAccountId: string; paymentDate: Date; totalAmount: string; settlements?: IncomingPaymentSettlementInput[]; } /** Function: createIncomingPayment * Creates an editable incoming payment with optional initial settlements. */ export async function run< CF extends Record = Record, SCF extends Record = Record, >( db: Transaction, input: Omit & CF & { settlements?: (IncomingPaymentSettlementInput & SCF)[] }, ctx: CommandContext, organizationQueries: Pick, customerAccountQueries: Pick, primitivesQueries: Pick, coaManagementQueries: Pick, ) { const { companyId, customerAccountId, currencyId, paymentAccountId, paymentDate, totalAmount, settlements = [], ...paymentCustomFields } = input; if (new Decimal(totalAmount).lte(0)) { return err(new IncomingPaymentInvalidAmountError("IncomingPayment")); } for (const settlement of settlements) { if (new Decimal(settlement.settledAmount).lte(0)) { return err( new IncomingPaymentInvalidAmountError(settlement.accountReceivableDueScheduleLineId), ); } } const { company } = (await organizationQueries.getCompany(db, { id: companyId }, ctx)).value; if (!company) return err(new CompanyNotFoundError(companyId)); if (company.status !== "ACTIVE") return err(new CompanyInactiveError(companyId)); const { account: customerAccount } = ( await customerAccountQueries.getCustomerAccount(db, { customerAccountId }, ctx) ).value; if (!customerAccount) return err(new CustomerAccountNotFoundError(customerAccountId)); if (customerAccount.accountStatus !== "ACTIVE" || customerAccount.companyId !== companyId) { return err(new InvalidCustomerAccountError(customerAccountId)); } const { currency } = (await primitivesQueries.getCurrency(db, { id: currencyId }, ctx)).value; if (!currency) return err(new CurrencyNotFoundError(currencyId)); const { account } = (await coaManagementQueries.getAccount(db, { id: paymentAccountId }, ctx)) .value; if (!account) return err(new AccountNotFoundError(paymentAccountId)); if (account.companyId !== companyId) { return err(new AccountCompanyMismatchError(paymentAccountId)); } if (account.status !== "ACTIVE") return err(new AccountInactiveError(paymentAccountId)); const dueScheduleLineIds = settlements.map( (settlement) => settlement.accountReceivableDueScheduleLineId, ); if (new Set(dueScheduleLineIds).size !== dueScheduleLineIds.length) { return err(new IncomingPaymentSettlementInvalidError("IncomingPayment")); } const targetResult = await validateIncomingPaymentSettlementTargets( db, { companyId, customerAccountId, currencyId }, settlements, ); if (!targetResult.ok) return targetResult; const incomingPayment = await db .insertInto("IncomingPayment") .values({ ...(paymentCustomFields as Record), companyId, customerAccountId, currencyId, paymentAccountId, paymentDate, totalAmount, status: "DRAFT", postedAt: null, reversalDate: null, reversedAt: null, cancelledAt: null, }) .returningAll() .executeTakeFirstOrThrow(); if (settlements.length > 0) { await db .insertInto("AccountReceivableSettlement") .values( settlements.map((settlement) => { const { accountReceivableDueScheduleLineId, settledAmount, ...settlementCustomFields } = settlement; return { ...(settlementCustomFields as Record), sourceType: "INCOMING_PAYMENT" as const, incomingPaymentId: incomingPayment.id, accountReceivableDueScheduleLineId, settledAmount, reversalOfSettlementId: null, }; }), ) .execute(); } return ok({ incomingPayment }); }