import { err, ok, type CommandContext } from "@tailor-platform/erp-kit/core"; import { Decimal } from "decimal.js"; import { accountReceivableDocumentLifecycle } from "../db/accountReceivableDocument.lifecycle.generated"; import type { Transaction } from "../generated/kysely-tailordb"; import { AccountInactiveError, AccountNotFoundError, AccountingPeriodNotFoundError, DocumentNotFoundError, InvalidDocumentStatusError, JournalEntryCreateFailedError, JournalEntryPostFailedError, LineTotalMismatchError, MinimumLinesNotMetError, PostingCurrencyMismatchError, CompanyNotFoundError, } from "../lib/errors.generated"; import type { CoaManagementQueries, FinancialAccountingCommands, FinancialAccountingQueries, OrganizationQueries, } from "../module"; export async function run( db: Transaction, input: { id: string }, ctx: CommandContext, organizationQueries: Pick, coaManagementQueries: Pick, financialAccountingCommands: Pick< FinancialAccountingCommands, "createJournalEntry" | "postJournalEntry" >, financialAccountingQueries: Pick, ) { const document = await db .selectFrom("AccountReceivableDocument") .selectAll() .where("id", "=", input.id) .forUpdate() .executeTakeFirst(); if (!document) return err(new DocumentNotFoundError(input.id)); const nextStatus = accountReceivableDocumentLifecycle.tryTransition(document.status, "post"); if (!nextStatus) return err(new InvalidDocumentStatusError(input.id)); const lines = await db .selectFrom("AccountReceivableDocumentLine") .selectAll() .where("accountReceivableDocumentId", "=", input.id) .execute(); if (lines.length === 0) return err(new MinimumLinesNotMetError(input.id)); const lineTotal = lines.reduce((sum, line) => sum.plus(line.grossAmount), new Decimal(0)); if (!lineTotal.eq(document.totalAmount)) return err(new LineTotalMismatchError(input.id)); const distributions = await db .selectFrom("AccountReceivableDistributionLine as distribution") .innerJoin( "AccountReceivableDocumentLine as line", "line.id", "distribution.accountReceivableDocumentLineId", ) .selectAll("distribution") .where("line.accountReceivableDocumentId", "=", input.id) .execute(); const distributionTotal = distributions.reduce( (sum, distribution) => sum.plus(distribution.amount), new Decimal(0), ); if (distributions.length === 0 || !distributionTotal.eq(document.totalAmount)) { return err(new LineTotalMismatchError(input.id)); } const { company } = (await organizationQueries.getCompany(db, { id: document.companyId }, ctx)) .value; if (!company) return err(new CompanyNotFoundError(document.companyId)); if (company.baseCurrencyId !== document.currencyId) { return err(new PostingCurrencyMismatchError(input.id)); } const accountIds = [ ...new Set([ document.receivableControlAccountId, ...distributions.map(({ accountId }) => accountId), ]), ]; const accounts = ( await coaManagementQueries.listAccounts( db, { companyId: document.companyId, accountIds, limit: accountIds.length }, ctx, ) ).value.items; const accountById = new Map(accounts.map((account) => [account.id, account])); for (const accountId of accountIds) { const account = accountById.get(accountId); if (!account) return err(new AccountNotFoundError(accountId)); if (account.status !== "ACTIVE") return err(new AccountInactiveError(accountId)); } const { accountingPeriod } = ( await financialAccountingQueries.getPeriodByDate( db, { companyId: document.companyId, date: document.postingDate }, ctx, ) ).value; if (!accountingPeriod) return err(new AccountingPeriodNotFoundError(input.id)); const isInvoice = document.documentType === "INVOICE"; const createResult = await financialAccountingCommands.createJournalEntry( db, { header: { companyId: document.companyId, accountingPeriodId: accountingPeriod.id, entryDate: document.postingDate, description: `${document.documentType} ${document.documentNumber}`, sourceDocumentType: "SALES_INVOICE", sourceDocumentId: document.id, }, lines: [ ...distributions.map((distribution) => ({ accountId: distribution.accountId, debitAmount: isInvoice ? null : distribution.amount, creditAmount: isInvoice ? distribution.amount : null, description: distribution.description, })), { accountId: document.receivableControlAccountId, debitAmount: isInvoice ? document.totalAmount : null, creditAmount: isInvoice ? null : document.totalAmount, description: "Accounts receivable control", }, ], }, ctx, ); if (!createResult.ok) return err(new JournalEntryCreateFailedError(input.id)); const postResult = await financialAccountingCommands.postJournalEntry( db, { id: createResult.value.journalEntry.id }, ctx, ); if (!postResult.ok) return err(new JournalEntryPostFailedError(input.id)); const postedDocument = await db .updateTable("AccountReceivableDocument") .set({ status: nextStatus, postedAt: new Date() }) .where("id", "=", input.id) .returningAll() .executeTakeFirstOrThrow(); return ok({ accountReceivableDocument: postedDocument, journalEntry: postResult.value.journalEntry, }); }