import { err, ok, type CommandContext } from "@tailor-platform/erp-kit/core"; import { Decimal } from "decimal.js"; import type { BankStatementLineFlowDirection } from "../generated/enums"; import type { Transaction } from "../generated/kysely-tailordb"; import { BankAccountInactiveError, BankAccountNotFoundError, BankStatementInvalidError, } from "../lib/errors.generated"; export interface CreateBankStatementLineInput { lineNumber: number; bookingDate: Date; valueDate?: Date; flowDirection: BankStatementLineFlowDirection; amount: string; bankReference?: string; endToEndId?: string; counterpartyName?: string; counterpartyAccount?: string; description?: string; } export interface CreateBankStatementInput { bankAccountId: string; statementNumber: string; statementDate: Date; openingBalance: string; closingBalance: string; importedAt?: Date; lines: CreateBankStatementLineInput[]; } export async function run< HF extends Record = Record, LF extends Record = Record, >( db: Transaction, input: Omit & HF & { lines: (CreateBankStatementLineInput & LF)[]; }, _ctx: CommandContext, ) { const { bankAccountId, statementNumber, statementDate, openingBalance, closingBalance, importedAt, lines, ...headerCustomFields } = input; const bankAccount = await db .selectFrom("BankAccount") .selectAll() .where("id", "=", bankAccountId) .executeTakeFirst(); if (!bankAccount) return err(new BankAccountNotFoundError(bankAccountId)); if (bankAccount.status !== "ACTIVE") return err(new BankAccountInactiveError(bankAccountId)); if (statementNumber.trim() === "" || lines.length === 0) { return err(new BankStatementInvalidError(statementNumber)); } const seenLineNumbers = new Set(); let movement = new Decimal(0); for (const line of lines) { if ( !Number.isInteger(line.lineNumber) || line.lineNumber < 1 || seenLineNumbers.has(line.lineNumber) ) { return err(new BankStatementInvalidError(`${statementNumber}:${line.lineNumber}`)); } seenLineNumbers.add(line.lineNumber); const amount = new Decimal(line.amount); if (amount.lte(0)) return err(new BankStatementInvalidError(`${statementNumber}:${line.lineNumber}`)); movement = line.flowDirection === "CREDIT" ? movement.plus(amount) : movement.minus(amount); } if (!new Decimal(openingBalance).plus(movement).eq(closingBalance)) { return err(new BankStatementInvalidError(statementNumber)); } const bankStatement = await db .insertInto("BankStatement") .values({ ...(headerCustomFields as Record), bankAccountId, statementNumber, statementDate, openingBalance, closingBalance, importedAt: importedAt ?? new Date(), }) .returningAll() .executeTakeFirstOrThrow(); const bankStatementLines = await db .insertInto("BankStatementLine") .values( lines.map((line) => { const { lineNumber, bookingDate, valueDate, flowDirection, amount, bankReference, endToEndId, counterpartyName, counterpartyAccount, description, ...lineCustomFields } = line; return { ...(lineCustomFields as Record), bankStatementId: bankStatement.id, lineNumber, bookingDate, valueDate: valueDate ?? null, flowDirection, amount, reconciliationStatus: "UNRECONCILED", currentBankReconciliationId: null, bankReference: bankReference ?? null, endToEndId: endToEndId ?? null, counterpartyName: counterpartyName ?? null, counterpartyAccount: counterpartyAccount ?? null, description: description ?? null, }; }), ) .returningAll() .execute(); return ok({ bankStatement, bankStatementLines }); }