import { createContext, DomainError } from "@tailor-platform/erp-kit/app"; import { createResolver, t } from "@tailor-platform/sdk"; import { getDB } from "@/generated/kysely-tailordb"; import { finModulesExport } from "@/modules"; /** * createJournalEntry — raise a DRAFT journal entry (仕訳) in the general ledger. * * Thin wrapper over the native financial-accounting `createJournalEntry`. The * caller picks the accounting period; the company is derived from it so the UI * only needs one scope selector. Each line carries exactly one of a debit or a * credit amount (decimal strings); the native command enforces the double-entry * rules (≥2 lines, debit XOR credit, positive amounts). Balance (Σdebit=Σcredit) * is enforced later at post time. */ export default createResolver({ name: "createJournalEntry", operation: "mutation", input: { accountingPeriodId: t.string().description("Accounting period the entry posts to"), entryDate: t .string({ optional: true }) .description("Entry date (YYYY-MM-DD); defaults to today"), description: t.string({ optional: true }).description("Journal entry description"), lines: t .object( { accountId: t.string().description("GL account (coa-management) ID"), debit: t.string({ optional: true }).description("Debit amount (decimal string)"), credit: t.string({ optional: true }).description("Credit amount (decimal string)"), description: t.string({ optional: true }).description("Line description"), }, { array: true }, ) .description("Journal lines (at least two; each carries a debit or a credit)"), }, body: async (context) => { const ctx = createContext(context); const db = getDB("main-db"); return db .transaction() .execute(async (trx) => { if (context.input.lines.length < 2) { throw new DomainError("A journal entry requires at least two lines."); } const period = await trx .selectFrom("AccountingPeriod") .select(["companyId"]) .where("id", "=", context.input.accountingPeriodId) .executeTakeFirst(); if (!period) { throw new DomainError(`Accounting period not found: ${context.input.accountingPeriodId}`); } const result = await finModulesExport.commands.createJournalEntry( trx, { header: { companyId: period.companyId, accountingPeriodId: context.input.accountingPeriodId, entryDate: context.input.entryDate ? new Date(context.input.entryDate) : new Date(), description: context.input.description ?? undefined, }, lines: context.input.lines.map((line) => ({ accountId: line.accountId, debitAmount: line.debit?.trim() ? line.debit.trim() : null, creditAmount: line.credit?.trim() ? line.credit.trim() : null, description: line.description?.trim() ? line.description.trim() : null, })), }, ctx, ); if (!result.ok) { switch (result.error.code) { case "FINANCIAL_ACCOUNTING_COMPANY_NOT_FOUND": throw new DomainError("The company for this accounting period no longer exists."); case "FINANCIAL_ACCOUNTING_ACCOUNTING_PERIOD_NOT_FOUND": throw new DomainError( `Accounting period not found: ${context.input.accountingPeriodId}`, ); case "FINANCIAL_ACCOUNTING_ACCOUNTING_PERIOD_COMPANY_MISMATCH": throw new DomainError("The accounting period belongs to a different company."); case "FINANCIAL_ACCOUNTING_INVALID_ENTRY_DATE": throw new DomainError( "The entry date is invalid for the selected accounting period.", ); case "FINANCIAL_ACCOUNTING_MINIMUM_LINES_NOT_MET": throw new DomainError("A journal entry requires at least two lines."); case "FINANCIAL_ACCOUNTING_ACCOUNT_NOT_FOUND": throw new DomainError( `A referenced GL account does not exist: ${result.error.message}`, ); case "FINANCIAL_ACCOUNTING_ACCOUNT_INACTIVE": throw new DomainError(`A referenced GL account is inactive: ${result.error.message}`); case "FINANCIAL_ACCOUNTING_INVALID_DEBIT_CREDIT": throw new DomainError( "Each line must carry exactly one positive debit or credit amount.", ); case "UNAUTHENTICATED": throw new DomainError("Authentication is required"); case "INSUFFICIENT_PERMISSION": throw new DomainError("You do not have permission to perform this action"); default: throw result.error satisfies never; } } return { id: result.value.journalEntry.id, status: result.value.journalEntry.status, }; }) .catch((err: unknown) => { if (err instanceof DomainError) throw err; throw new Error("Failed to create the journal entry", { cause: err }); }); }, output: t .object({ id: t.string().description("Journal entry ID"), status: t.string().description("Document status (DRAFT)"), }) .description("CreateJournalEntry response"), });