import { err, ok, type CommandContext } from "@tailor-platform/erp-kit/core"; import { Decimal } from "decimal.js"; import type { Transaction, Updateable } from "../generated/kysely-tailordb"; import { validateOutgoingPaymentSettlementTargets } from "../internal/outgoingPaymentSettlementValidation"; import { AccountCompanyMismatchError, AccountInactiveError, AccountNotFoundError, OutgoingPaymentInvalidAmountError, OutgoingPaymentInvalidStatusError, OutgoingPaymentNotFoundError, OutgoingPaymentSettlementInvalidError, OutgoingPaymentSettlementNotFoundError, } from "../lib/errors.generated"; import type { CoaManagementQueries } from "../module"; export interface OutgoingPaymentHeaderPatch { paymentDate?: Date; totalAmount?: string; paymentAccountId?: string; } export interface OutgoingPaymentSettlementInput { accountPayableDueScheduleLineId: string; settledAmount: string; } export interface OutgoingPaymentSettlementPatch { settledAmount?: string; } export interface OutgoingPaymentSettlementEdit { settlementId: string; patch: OutgoingPaymentSettlementPatch; } export interface UpdateOutgoingPaymentInput { id: string; headerPatch?: OutgoingPaymentHeaderPatch; addSettlements?: OutgoingPaymentSettlementInput[]; updateSettlements?: OutgoingPaymentSettlementEdit[]; removeSettlementIds?: string[]; } function hasDuplicate(values: string[]) { return new Set(values).size !== values.length; } function isPositive(amount: string) { return new Decimal(amount).gt(0); } /** Function: updateOutgoingPayment * Incrementally updates a draft outgoing payment and its settlements. */ export async function run< HCF extends Record = Record, SCF extends Record = Record, >( db: Transaction, input: Omit< UpdateOutgoingPaymentInput, "headerPatch" | "addSettlements" | "updateSettlements" > & { headerPatch?: OutgoingPaymentHeaderPatch & Partial; addSettlements?: (OutgoingPaymentSettlementInput & SCF)[]; updateSettlements?: (Omit & { patch: OutgoingPaymentSettlementPatch & Partial; })[]; }, ctx: CommandContext, coaManagementQueries: Pick, ) { const { id, headerPatch, addSettlements = [], updateSettlements = [], removeSettlementIds = [], } = input; const payment = await db .selectFrom("OutgoingPayment") .selectAll() .where("id", "=", id) .forUpdate() .executeTakeFirst(); if (!payment) return err(new OutgoingPaymentNotFoundError(id)); if (payment.status !== "DRAFT") return err(new OutgoingPaymentInvalidStatusError(id)); const effectiveTotalAmount = headerPatch?.totalAmount ?? payment.totalAmount; if (!isPositive(effectiveTotalAmount)) { return err(new OutgoingPaymentInvalidAmountError(id)); } const effectivePaymentAccountId = headerPatch?.paymentAccountId ?? payment.paymentAccountId; const { account } = ( await coaManagementQueries.getAccount(db, { id: effectivePaymentAccountId }, ctx) ).value; if (!account) return err(new AccountNotFoundError(effectivePaymentAccountId)); if (account.status !== "ACTIVE") return err(new AccountInactiveError(effectivePaymentAccountId)); if (account.companyId !== payment.companyId) { return err(new AccountCompanyMismatchError(effectivePaymentAccountId)); } const currentSettlements = await db .selectFrom("AccountPayableSettlement") .selectAll() .where("sourceType", "=", "OUTGOING_PAYMENT") .where("outgoingPaymentId", "=", id) .execute(); const currentSettlementById = new Map( currentSettlements.map((settlement) => [settlement.id, settlement]), ); const updateIds = updateSettlements.map(({ settlementId }) => settlementId); if ( hasDuplicate(updateIds) || hasDuplicate(removeSettlementIds) || updateIds.some((settlementId) => removeSettlementIds.includes(settlementId)) ) { return err(new OutgoingPaymentSettlementInvalidError(id)); } for (const settlementId of [...updateIds, ...removeSettlementIds]) { if (!currentSettlementById.has(settlementId)) { return err(new OutgoingPaymentSettlementNotFoundError(settlementId)); } } for (const settlement of addSettlements) { if (!isPositive(settlement.settledAmount)) { return err(new OutgoingPaymentInvalidAmountError(settlement.accountPayableDueScheduleLineId)); } } for (const { settlementId, patch } of updateSettlements) { if (patch.settledAmount !== undefined && !isPositive(patch.settledAmount)) { return err(new OutgoingPaymentInvalidAmountError(settlementId)); } } const removeIdSet = new Set(removeSettlementIds); const updateById = new Map(updateSettlements.map((edit) => [edit.settlementId, edit.patch])); const effectiveSettlements = [ ...currentSettlements .filter((settlement) => !removeIdSet.has(settlement.id)) .map((settlement) => ({ ...settlement, ...updateById.get(settlement.id), })), ...addSettlements, ]; const effectiveDueScheduleIds = effectiveSettlements.map( (settlement) => settlement.accountPayableDueScheduleLineId, ); if (hasDuplicate(effectiveDueScheduleIds)) { return err(new OutgoingPaymentSettlementInvalidError(id)); } const targetResult = await validateOutgoingPaymentSettlementTargets( db, { companyId: payment.companyId, supplierAccountId: payment.supplierAccountId, currencyId: payment.currencyId, }, effectiveSettlements, ); if (!targetResult.ok) return targetResult; const { paymentDate, totalAmount, paymentAccountId, ...headerCustomFields } = headerPatch ?? {}; const paymentUpdate: Updateable<"OutgoingPayment"> = { ...(headerCustomFields as Updateable<"OutgoingPayment">), }; if (paymentDate !== undefined) paymentUpdate.paymentDate = paymentDate; if (totalAmount !== undefined) paymentUpdate.totalAmount = totalAmount; if (paymentAccountId !== undefined) paymentUpdate.paymentAccountId = paymentAccountId; const updatedPayment = Object.keys(paymentUpdate).length === 0 ? payment : await db .updateTable("OutgoingPayment") .set(paymentUpdate) .where("id", "=", id) .returningAll() .executeTakeFirstOrThrow(); for (const { settlementId, patch } of updateSettlements) { const { settledAmount, ...settlementCustomFields } = patch; const settlementUpdate: Updateable<"AccountPayableSettlement"> = { ...(settlementCustomFields as Updateable<"AccountPayableSettlement">), }; if (settledAmount !== undefined) settlementUpdate.settledAmount = settledAmount; if (Object.keys(settlementUpdate).length === 0) continue; await db .updateTable("AccountPayableSettlement") .set(settlementUpdate) .where("id", "=", settlementId) .where("sourceType", "=", "OUTGOING_PAYMENT") .where("outgoingPaymentId", "=", id) .execute(); } if (removeSettlementIds.length > 0) { await db .deleteFrom("AccountPayableSettlement") .where("sourceType", "=", "OUTGOING_PAYMENT") .where("outgoingPaymentId", "=", id) .where("id", "in", removeSettlementIds) .execute(); } if (addSettlements.length > 0) { await db .insertInto("AccountPayableSettlement") .values( addSettlements.map((settlement) => { const { accountPayableDueScheduleLineId, settledAmount, ...settlementCustomFields } = settlement; return { ...(settlementCustomFields as Record), sourceType: "OUTGOING_PAYMENT" as const, outgoingPaymentId: id, accountPayableDueScheduleLineId, settledAmount, reversalOfSettlementId: null, }; }), ) .execute(); } return ok({ outgoingPayment: updatedPayment }); }