import { err, ok, type CommandContext } from "@tailor-platform/erp-kit/core"; import { Decimal } from "decimal.js"; import type { Selectable, Transaction } from "../../generated/kysely-tailordb"; import { AccountingPeriodNotFoundError, JournalEntryCreateFailedError, JournalEntryPostFailedError, } from "../../lib/errors.generated"; import type { FinancialAccountingCommands, FinancialAccountingQueries } from "../../module"; /** Costing role; maps to a ValuationPolicy account via ROLE_ACCOUNT_FIELDS. */ export type CostingRole = | "INVENTORY" | "ACCRUAL" | "INVOICE_PRICE_VARIANCE" | "COGS" | "PPV" | "ADJUSTMENT" | "STANDARD_COST_ADJUSTMENT" | "CONSUMED_PRICE_VARIANCE"; export interface CostingFact { role: CostingRole; dcIndicator: "DR" | "CR"; /** Positive amount. */ amount: string; } const ROLE_ACCOUNT_FIELDS = { INVENTORY: "inventoryAccountId", ACCRUAL: "accrualAccountId", INVOICE_PRICE_VARIANCE: "invoicePriceVarianceAccountId", COGS: "cogsAccountId", PPV: "ppvAccountId", ADJUSTMENT: "adjustmentAccountId", STANDARD_COST_ADJUSTMENT: "standardCostAdjustmentAccountId", CONSUMED_PRICE_VARIANCE: "consumedPriceVarianceAccountId", } as const; export function pushNonZero(facts: CostingFact[], fact: CostingFact) { if (!new Decimal(fact.amount).isZero()) { facts.push(fact); } } /** * Records costing facts as a balanced posted journal entry in the policy's * company ledger, resolving line accounts from the policy. */ export async function createCostingJournalEntry( db: Transaction, args: { entryDate: Date; description: string; sourceDocumentType: | "INVENTORY_LEDGER" | "STANDARD_COST_REVISION" | "ACQUISITION_COST_ADJUSTMENT"; sourceDocumentId: string; facts: CostingFact[]; policy: Selectable<"ValuationPolicy">; }, ctx: CommandContext, financialAccountingCommands: Pick< FinancialAccountingCommands, "createJournalEntry" | "postJournalEntry" >, financialAccountingQueries: Pick, ) { const { entryDate, description, sourceDocumentType, sourceDocumentId, facts, policy } = args; const { companyId } = policy; // Every posting account is required on the policy, so this cannot fail. const lines = facts.map((fact) => ({ accountId: policy[ROLE_ACCOUNT_FIELDS[fact.role]], debitAmount: fact.dcIndicator === "DR" ? fact.amount : null, creditAmount: fact.dcIndicator === "CR" ? fact.amount : null, description: fact.role, })); const { accountingPeriod } = ( await financialAccountingQueries.getPeriodByDate(db, { companyId, date: entryDate }, ctx) ).value; if (!accountingPeriod) { return err(new AccountingPeriodNotFoundError(`${companyId}:${entryDate.toISOString()}`)); } const createResult = await financialAccountingCommands.createJournalEntry( db, { header: { companyId, accountingPeriodId: accountingPeriod.id, entryDate, description, sourceDocumentType, sourceDocumentId, }, lines, }, ctx, ); if (!createResult.ok) { // Preserve the underlying reason so the failure is diagnosable; the wrapper // keeps inventory's error surface free of financial-accounting error types. return err( new JournalEntryCreateFailedError(`${sourceDocumentId} (${createResult.error.message})`), ); } const postResult = await financialAccountingCommands.postJournalEntry( db, { id: createResult.value.journalEntry.id }, ctx, ); if (!postResult.ok) { return err( new JournalEntryPostFailedError(`${sourceDocumentId} (${postResult.error.message})`), ); } return ok(undefined); }