import { err, ok, type CommandContext } from "@tailor-platform/erp-kit/core"; import type { Transaction } from "../generated/kysely-tailordb"; import { BankReconciliationAccountingPeriodNotFoundError, BankReconciliationInvalidError, BankReconciliationJournalFailedError, BankReconciliationNotFoundError, BankReconciliationSourceNotFoundError, } from "../lib/errors.generated"; import type { FinancialAccountingCommands, FinancialAccountingQueries } from "../module"; export interface ReverseBankReconciliationInput { id: string; } export async function run( db: Transaction, input: ReverseBankReconciliationInput, ctx: CommandContext, financialAccountingCommands: Pick, financialAccountingQueries: Pick< FinancialAccountingQueries, "getJournalEntryBySource" | "getPeriodByDate" >, ) { const { id } = input; const reversalDate = new Date(); const original = await db .selectFrom("BankReconciliation") .selectAll() .where("id", "=", id) .forUpdate() .executeTakeFirst(); if (!original) return err(new BankReconciliationNotFoundError(id)); if (original.action !== "RECONCILE") { return err(new BankReconciliationInvalidError(id)); } const originalItems = await db .selectFrom("BankReconciliationItem") .selectAll() .where("bankReconciliationId", "=", original.id) .orderBy("id", "asc") .execute(); const statementLineIds: string[] = []; for (const item of originalItems.filter( (originalItem) => originalItem.sourceType === "BANK_STATEMENT_LINE", )) { const statementLine = await db .selectFrom("BankStatementLine") .select(["id", "reconciliationStatus", "currentBankReconciliationId"]) .where("id", "=", item.sourceId) .forUpdate() .executeTakeFirst(); if (!statementLine) { return err(new BankReconciliationSourceNotFoundError(item.sourceId)); } if ( statementLine.reconciliationStatus !== "RECONCILED" || statementLine.currentBankReconciliationId !== original.id ) { return err(new BankReconciliationInvalidError(item.sourceId)); } statementLineIds.push(statementLine.id); } const reversal = await db .insertInto("BankReconciliation") .values({ bankAccountId: original.bankAccountId, reconciliationDate: reversalDate, confirmedAt: new Date(), action: "UNRECONCILE", }) .returningAll() .executeTakeFirstOrThrow(); const reversalItems = await db .insertInto("BankReconciliationItem") .values( originalItems.map((item) => ({ bankReconciliationId: reversal.id, sourceType: item.sourceType, sourceId: item.sourceId, clearedAmount: item.clearedAmount, })), ) .returningAll() .execute(); for (const statementLineId of statementLineIds) { await db .updateTable("BankStatementLine") .set({ reconciliationStatus: "UNRECONCILED", currentBankReconciliationId: null, }) .where("id", "=", statementLineId) .execute(); } let journalEntry = null; const { journalEntry: originalJournalEntry } = ( await financialAccountingQueries.getJournalEntryBySource( db, { sourceDocumentType: "BANK_RECONCILIATION", sourceDocumentId: original.id, }, ctx, ) ).value; if (originalJournalEntry) { const { accountingPeriod } = ( await financialAccountingQueries.getPeriodByDate( db, { companyId: originalJournalEntry.companyId, date: reversalDate }, ctx, ) ).value; if (!accountingPeriod) { return err( new BankReconciliationAccountingPeriodNotFoundError( `${originalJournalEntry.companyId}:${reversalDate.toISOString()}`, ), ); } const reverseResult = await financialAccountingCommands.reverseJournalEntry( db, { id: originalJournalEntry.id, reversalPeriodId: accountingPeriod.id, }, ctx, ); if (!reverseResult.ok) return err(new BankReconciliationJournalFailedError(reversal.id)); journalEntry = reverseResult.value.reversalEntry; } return ok({ reversal, reversalItems, journalEntry }); }