@tailor-platform/erp-kit 0.6.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +14 -0
- package/README.md +10 -10
- package/dist/cli.mjs +407 -69
- package/package.json +1 -1
- package/skills/erp-kit-app-1-requirements/SKILL.md +33 -17
- package/skills/erp-kit-app-2-requirements-review/SKILL.md +12 -0
- package/skills/erp-kit-app-3-plan/SKILL.md +18 -4
- package/skills/erp-kit-app-3-plan/references/resolver-extraction.md +1 -1
- package/skills/erp-kit-app-3-plan/references/screen-extraction.md +1 -1
- package/skills/erp-kit-app-4-plan-review/SKILL.md +12 -0
- package/skills/erp-kit-app-5-impl-backend/SKILL.md +12 -0
- package/skills/erp-kit-app-6-impl-frontend/SKILL.md +12 -0
- package/skills/erp-kit-app-7-impl-review/SKILL.md +13 -1
- package/skills/erp-kit-app-shared/references/progress-protocol.md +77 -0
- package/skills/erp-kit-mock-scenario/SKILL.md +1 -1
- package/skills/erp-kit-module-1-requirements/SKILL.md +1 -1
- package/skills/erp-kit-module-3-plan/SKILL.md +3 -3
- package/skills/erp-kit-module-3-update-plan/SKILL.md +3 -3
- package/skills/erp-kit-module-5-impl/SKILL.md +1 -1
- package/src/commands/app/index.ts +2 -0
- package/src/commands/app/progress/git-context.ts +16 -0
- package/src/commands/app/progress/index.ts +45 -0
- package/src/commands/app/progress/log.ts +49 -0
- package/src/commands/app/progress/progress.test.ts +128 -0
- package/src/commands/app/progress/schema-cmd.ts +10 -0
- package/src/commands/check.test.ts +4 -4
- package/src/commands/lib/discovery.test.ts +5 -7
- package/src/commands/lib/discovery.ts +8 -8
- package/src/commands/lib/sync-check-source.test.ts +1 -1
- package/src/commands/lib/sync-check-source.ts +6 -1
- package/src/commands/lib/sync-check-tests.test.ts +43 -0
- package/src/commands/lib/sync-check-tests.ts +20 -2
- package/src/commands/sync-check.ts +3 -0
- package/src/generator/generate-app-code.test.ts +0 -6
- package/src/generator/generate-app-code.ts +3 -13
- package/src/generator/generate-code.test.ts +10 -40
- package/src/generator/generate-code.ts +6 -12
- package/src/generator/stub-templates.test.ts +0 -7
- package/src/generator/stub-templates.ts +0 -14
- package/src/modules/finance-ledger/README.md +50 -0
- package/src/modules/finance-ledger/command/.gitkeep +0 -0
- package/src/modules/finance-ledger/command/addJournalLine.generated.ts +6 -0
- package/src/modules/finance-ledger/command/addJournalLine.test.ts +438 -0
- package/src/modules/finance-ledger/command/addJournalLine.ts +122 -0
- package/src/modules/finance-ledger/command/approveAndLockPeriod.generated.ts +6 -0
- package/src/modules/finance-ledger/command/approveAndLockPeriod.test.ts +107 -0
- package/src/modules/finance-ledger/command/approveAndLockPeriod.ts +72 -0
- package/src/modules/finance-ledger/command/beginClose.generated.ts +6 -0
- package/src/modules/finance-ledger/command/beginClose.test.ts +106 -0
- package/src/modules/finance-ledger/command/beginClose.ts +58 -0
- package/src/modules/finance-ledger/command/closePeriod.generated.ts +6 -0
- package/src/modules/finance-ledger/command/closePeriod.test.ts +87 -0
- package/src/modules/finance-ledger/command/closePeriod.ts +44 -0
- package/src/modules/finance-ledger/command/createAccountingPeriod.generated.ts +6 -0
- package/src/modules/finance-ledger/command/createAccountingPeriod.test.ts +425 -0
- package/src/modules/finance-ledger/command/createAccountingPeriod.ts +133 -0
- package/src/modules/finance-ledger/command/createFiscalYear.generated.ts +6 -0
- package/src/modules/finance-ledger/command/createFiscalYear.test.ts +197 -0
- package/src/modules/finance-ledger/command/createFiscalYear.ts +70 -0
- package/src/modules/finance-ledger/command/createJournalEntry.generated.ts +6 -0
- package/src/modules/finance-ledger/command/createJournalEntry.test.ts +261 -0
- package/src/modules/finance-ledger/command/createJournalEntry.ts +121 -0
- package/src/modules/finance-ledger/command/deleteAccountingPeriod.generated.ts +6 -0
- package/src/modules/finance-ledger/command/deleteAccountingPeriod.test.ts +71 -0
- package/src/modules/finance-ledger/command/deleteAccountingPeriod.ts +55 -0
- package/src/modules/finance-ledger/command/deleteFiscalYear.generated.ts +6 -0
- package/src/modules/finance-ledger/command/deleteFiscalYear.test.ts +38 -0
- package/src/modules/finance-ledger/command/deleteFiscalYear.ts +34 -0
- package/src/modules/finance-ledger/command/deleteJournalEntry.generated.ts +6 -0
- package/src/modules/finance-ledger/command/deleteJournalEntry.test.ts +58 -0
- package/src/modules/finance-ledger/command/deleteJournalEntry.ts +43 -0
- package/src/modules/finance-ledger/command/executeYearEndClose.generated.ts +6 -0
- package/src/modules/finance-ledger/command/executeYearEndClose.test.ts +239 -0
- package/src/modules/finance-ledger/command/executeYearEndClose.ts +415 -0
- package/src/modules/finance-ledger/command/finalCloseAndLockPeriod.generated.ts +6 -0
- package/src/modules/finance-ledger/command/finalCloseAndLockPeriod.test.ts +102 -0
- package/src/modules/finance-ledger/command/finalCloseAndLockPeriod.ts +76 -0
- package/src/modules/finance-ledger/command/finalizeFinancialStatement.generated.ts +6 -0
- package/src/modules/finance-ledger/command/finalizeFinancialStatement.test.ts +73 -0
- package/src/modules/finance-ledger/command/finalizeFinancialStatement.ts +73 -0
- package/src/modules/finance-ledger/command/generateFinancialStatement.generated.ts +6 -0
- package/src/modules/finance-ledger/command/generateFinancialStatement.test.ts +311 -0
- package/src/modules/finance-ledger/command/generateFinancialStatement.ts +275 -0
- package/src/modules/finance-ledger/command/generatePreliminaryStatements.generated.ts +6 -0
- package/src/modules/finance-ledger/command/generatePreliminaryStatements.test.ts +152 -0
- package/src/modules/finance-ledger/command/generatePreliminaryStatements.ts +140 -0
- package/src/modules/finance-ledger/command/generateTrialBalance.generated.ts +6 -0
- package/src/modules/finance-ledger/command/generateTrialBalance.test.ts +439 -0
- package/src/modules/finance-ledger/command/generateTrialBalance.ts +268 -0
- package/src/modules/finance-ledger/command/initiatePeriodClose.generated.ts +6 -0
- package/src/modules/finance-ledger/command/initiatePeriodClose.test.ts +153 -0
- package/src/modules/finance-ledger/command/initiatePeriodClose.ts +84 -0
- package/src/modules/finance-ledger/command/openForAdvanceEntry.generated.ts +6 -0
- package/src/modules/finance-ledger/command/openForAdvanceEntry.test.ts +87 -0
- package/src/modules/finance-ledger/command/openForAdvanceEntry.ts +44 -0
- package/src/modules/finance-ledger/command/openPeriod.generated.ts +6 -0
- package/src/modules/finance-ledger/command/openPeriod.test.ts +90 -0
- package/src/modules/finance-ledger/command/openPeriod.ts +44 -0
- package/src/modules/finance-ledger/command/permanentlyClosePeriod.generated.ts +6 -0
- package/src/modules/finance-ledger/command/permanentlyClosePeriod.test.ts +87 -0
- package/src/modules/finance-ledger/command/permanentlyClosePeriod.ts +48 -0
- package/src/modules/finance-ledger/command/postAdjustingEntries.generated.ts +6 -0
- package/src/modules/finance-ledger/command/postAdjustingEntries.test.ts +392 -0
- package/src/modules/finance-ledger/command/postAdjustingEntries.ts +156 -0
- package/src/modules/finance-ledger/command/postJournalEntry.generated.ts +6 -0
- package/src/modules/finance-ledger/command/postJournalEntry.test.ts +346 -0
- package/src/modules/finance-ledger/command/postJournalEntry.ts +160 -0
- package/src/modules/finance-ledger/command/processInventoryHandoff.generated.ts +6 -0
- package/src/modules/finance-ledger/command/processInventoryHandoff.test.ts +211 -0
- package/src/modules/finance-ledger/command/processInventoryHandoff.ts +133 -0
- package/src/modules/finance-ledger/command/processManufacturingHandoff.generated.ts +6 -0
- package/src/modules/finance-ledger/command/processManufacturingHandoff.test.ts +221 -0
- package/src/modules/finance-ledger/command/processManufacturingHandoff.ts +133 -0
- package/src/modules/finance-ledger/command/processPurchaseHandoff.generated.ts +6 -0
- package/src/modules/finance-ledger/command/processPurchaseHandoff.test.ts +222 -0
- package/src/modules/finance-ledger/command/processPurchaseHandoff.ts +133 -0
- package/src/modules/finance-ledger/command/processSalesHandoff.generated.ts +6 -0
- package/src/modules/finance-ledger/command/processSalesHandoff.test.ts +257 -0
- package/src/modules/finance-ledger/command/processSalesHandoff.ts +135 -0
- package/src/modules/finance-ledger/command/regenerateFinancialStatement.generated.ts +6 -0
- package/src/modules/finance-ledger/command/regenerateFinancialStatement.test.ts +129 -0
- package/src/modules/finance-ledger/command/regenerateFinancialStatement.ts +186 -0
- package/src/modules/finance-ledger/command/removeJournalLine.generated.ts +6 -0
- package/src/modules/finance-ledger/command/removeJournalLine.test.ts +65 -0
- package/src/modules/finance-ledger/command/removeJournalLine.ts +39 -0
- package/src/modules/finance-ledger/command/reopenPeriod.generated.ts +6 -0
- package/src/modules/finance-ledger/command/reopenPeriod.test.ts +87 -0
- package/src/modules/finance-ledger/command/reopenPeriod.ts +44 -0
- package/src/modules/finance-ledger/command/reverseJournalEntry.generated.ts +6 -0
- package/src/modules/finance-ledger/command/reverseJournalEntry.test.ts +337 -0
- package/src/modules/finance-ledger/command/reverseJournalEntry.ts +140 -0
- package/src/modules/finance-ledger/command/revertSoftLock.generated.ts +6 -0
- package/src/modules/finance-ledger/command/revertSoftLock.test.ts +96 -0
- package/src/modules/finance-ledger/command/revertSoftLock.ts +67 -0
- package/src/modules/finance-ledger/command/updateFiscalYear.generated.ts +6 -0
- package/src/modules/finance-ledger/command/updateFiscalYear.test.ts +138 -0
- package/src/modules/finance-ledger/command/updateFiscalYear.ts +85 -0
- package/src/modules/finance-ledger/command/updateJournalEntry.generated.ts +6 -0
- package/src/modules/finance-ledger/command/updateJournalEntry.test.ts +195 -0
- package/src/modules/finance-ledger/command/updateJournalEntry.ts +86 -0
- package/src/modules/finance-ledger/command/updateJournalLine.generated.ts +6 -0
- package/src/modules/finance-ledger/command/updateJournalLine.test.ts +385 -0
- package/src/modules/finance-ledger/command/updateJournalLine.ts +155 -0
- package/src/modules/finance-ledger/command/verifySubledgerTransfers.generated.ts +6 -0
- package/src/modules/finance-ledger/command/verifySubledgerTransfers.test.ts +201 -0
- package/src/modules/finance-ledger/command/verifySubledgerTransfers.ts +113 -0
- package/src/modules/finance-ledger/command/verifyTrialBalance.generated.ts +6 -0
- package/src/modules/finance-ledger/command/verifyTrialBalance.test.ts +136 -0
- package/src/modules/finance-ledger/command/verifyTrialBalance.ts +97 -0
- package/src/modules/finance-ledger/db/.gitkeep +0 -0
- package/src/modules/finance-ledger/db/accountingPeriod.ts +58 -0
- package/src/modules/finance-ledger/db/financialStatement.ts +92 -0
- package/src/modules/finance-ledger/db/financialStatementLineItem.ts +76 -0
- package/src/modules/finance-ledger/db/fiscalYear.ts +41 -0
- package/src/modules/finance-ledger/db/journalEntry.ts +101 -0
- package/src/modules/finance-ledger/db/journalLine.ts +64 -0
- package/src/modules/finance-ledger/db/periodClose.ts +97 -0
- package/src/modules/finance-ledger/db/trialBalance.ts +63 -0
- package/src/modules/finance-ledger/db/trialBalanceLine.ts +63 -0
- package/src/modules/finance-ledger/docs/commands/AddJournalLine.md +74 -0
- package/src/modules/finance-ledger/docs/commands/ApproveAndLockPeriod.md +53 -0
- package/src/modules/finance-ledger/docs/commands/BeginClose.md +47 -0
- package/src/modules/finance-ledger/docs/commands/ClosePeriod.md +45 -0
- package/src/modules/finance-ledger/docs/commands/CreateAccountingPeriod.md +69 -0
- package/src/modules/finance-ledger/docs/commands/CreateFiscalYear.md +56 -0
- package/src/modules/finance-ledger/docs/commands/CreateJournalEntry.md +63 -0
- package/src/modules/finance-ledger/docs/commands/DeleteAccountingPeriod.md +46 -0
- package/src/modules/finance-ledger/docs/commands/DeleteFiscalYear.md +40 -0
- package/src/modules/finance-ledger/docs/commands/DeleteJournalEntry.md +44 -0
- package/src/modules/finance-ledger/docs/commands/ExecuteYearEndClose.md +81 -0
- package/src/modules/finance-ledger/docs/commands/FinalCloseAndLockPeriod.md +49 -0
- package/src/modules/finance-ledger/docs/commands/FinalizeFinancialStatement.md +43 -0
- package/src/modules/finance-ledger/docs/commands/GenerateFinancialStatement.md +86 -0
- package/src/modules/finance-ledger/docs/commands/GeneratePreliminaryStatements.md +53 -0
- package/src/modules/finance-ledger/docs/commands/GenerateTrialBalance.md +75 -0
- package/src/modules/finance-ledger/docs/commands/InitiatePeriodClose.md +58 -0
- package/src/modules/finance-ledger/docs/commands/OpenForAdvanceEntry.md +44 -0
- package/src/modules/finance-ledger/docs/commands/OpenPeriod.md +45 -0
- package/src/modules/finance-ledger/docs/commands/PermanentlyClosePeriod.md +45 -0
- package/src/modules/finance-ledger/docs/commands/PostAdjustingEntries.md +61 -0
- package/src/modules/finance-ledger/docs/commands/PostJournalEntry.md +81 -0
- package/src/modules/finance-ledger/docs/commands/ProcessInventoryHandoff.md +72 -0
- package/src/modules/finance-ledger/docs/commands/ProcessManufacturingHandoff.md +68 -0
- package/src/modules/finance-ledger/docs/commands/ProcessPurchaseHandoff.md +68 -0
- package/src/modules/finance-ledger/docs/commands/ProcessSalesHandoff.md +71 -0
- package/src/modules/finance-ledger/docs/commands/RegenerateFinancialStatement.md +60 -0
- package/src/modules/finance-ledger/docs/commands/RemoveJournalLine.md +42 -0
- package/src/modules/finance-ledger/docs/commands/ReopenPeriod.md +45 -0
- package/src/modules/finance-ledger/docs/commands/ReverseJournalEntry.md +62 -0
- package/src/modules/finance-ledger/docs/commands/RevertSoftLock.md +49 -0
- package/src/modules/finance-ledger/docs/commands/UpdateFiscalYear.md +60 -0
- package/src/modules/finance-ledger/docs/commands/UpdateJournalEntry.md +50 -0
- package/src/modules/finance-ledger/docs/commands/UpdateJournalLine.md +61 -0
- package/src/modules/finance-ledger/docs/commands/VerifySubledgerTransfers.md +59 -0
- package/src/modules/finance-ledger/docs/commands/VerifyTrialBalance.md +53 -0
- package/src/modules/finance-ledger/docs/features/accounting-period-management.md +110 -0
- package/src/modules/finance-ledger/docs/features/financial-statement-generation.md +115 -0
- package/src/modules/finance-ledger/docs/features/journal-entry-management.md +138 -0
- package/src/modules/finance-ledger/docs/features/period-end-close.md +102 -0
- package/src/modules/finance-ledger/docs/features/subledger-integration.md +141 -0
- package/src/modules/finance-ledger/docs/features/trial-balance.md +99 -0
- package/src/modules/finance-ledger/docs/features/year-end-close.md +84 -0
- package/src/modules/finance-ledger/docs/models/AccountingPeriod.md +71 -0
- package/src/modules/finance-ledger/docs/models/FinancialStatement.md +76 -0
- package/src/modules/finance-ledger/docs/models/FinancialStatementLineItem.md +41 -0
- package/src/modules/finance-ledger/docs/models/FiscalYear.md +41 -0
- package/src/modules/finance-ledger/docs/models/JournalEntry.md +80 -0
- package/src/modules/finance-ledger/docs/models/JournalLine.md +47 -0
- package/src/modules/finance-ledger/docs/models/PeriodClose.md +83 -0
- package/src/modules/finance-ledger/docs/models/TrialBalance.md +56 -0
- package/src/modules/finance-ledger/docs/models/TrialBalanceLine.md +37 -0
- package/src/modules/finance-ledger/docs/queries/GetAccountingPeriod.md +35 -0
- package/src/modules/finance-ledger/docs/queries/GetFinancialStatement.md +38 -0
- package/src/modules/finance-ledger/docs/queries/GetFiscalYear.md +35 -0
- package/src/modules/finance-ledger/docs/queries/GetJournalEntry.md +37 -0
- package/src/modules/finance-ledger/docs/queries/GetPeriodByDate.md +38 -0
- package/src/modules/finance-ledger/docs/queries/GetPeriodClose.md +36 -0
- package/src/modules/finance-ledger/docs/queries/GetSubledgerTransferStatus.md +45 -0
- package/src/modules/finance-ledger/docs/queries/GetTrialBalance.md +38 -0
- package/src/modules/finance-ledger/docs/queries/ListAccountingPeriods.md +46 -0
- package/src/modules/finance-ledger/docs/queries/ListFinancialStatements.md +46 -0
- package/src/modules/finance-ledger/docs/queries/ListFiscalYears.md +42 -0
- package/src/modules/finance-ledger/docs/queries/ListJournalEntries.md +48 -0
- package/src/modules/finance-ledger/docs/queries/ListPeriodCloses.md +46 -0
- package/src/modules/finance-ledger/docs/queries/ListTrialBalances.md +51 -0
- package/src/modules/finance-ledger/executor/.gitkeep +0 -0
- package/src/modules/finance-ledger/generated/enums.ts +109 -0
- package/src/modules/finance-ledger/generated/kysely-tailordb.ts +202 -0
- package/src/modules/finance-ledger/index.ts +2 -0
- package/src/modules/finance-ledger/lib/_db_deps.ts +56 -0
- package/src/modules/finance-ledger/lib/errors.generated.ts +332 -0
- package/src/modules/finance-ledger/lib/permissions.generated.ts +41 -0
- package/src/modules/finance-ledger/lib/types.ts +66 -0
- package/src/modules/finance-ledger/module.ts +262 -0
- package/src/modules/finance-ledger/package.json +26 -0
- package/src/modules/finance-ledger/permissions.ts +3 -0
- package/src/modules/finance-ledger/query/.gitkeep +0 -0
- package/src/modules/finance-ledger/query/getAccountingPeriod.generated.ts +5 -0
- package/src/modules/finance-ledger/query/getAccountingPeriod.test.ts +31 -0
- package/src/modules/finance-ledger/query/getAccountingPeriod.ts +21 -0
- package/src/modules/finance-ledger/query/getFinancialStatement.generated.ts +5 -0
- package/src/modules/finance-ledger/query/getFinancialStatement.test.ts +35 -0
- package/src/modules/finance-ledger/query/getFinancialStatement.ts +29 -0
- package/src/modules/finance-ledger/query/getFiscalYear.generated.ts +5 -0
- package/src/modules/finance-ledger/query/getFiscalYear.test.ts +31 -0
- package/src/modules/finance-ledger/query/getFiscalYear.ts +21 -0
- package/src/modules/finance-ledger/query/getJournalEntry.generated.ts +5 -0
- package/src/modules/finance-ledger/query/getJournalEntry.test.ts +35 -0
- package/src/modules/finance-ledger/query/getJournalEntry.ts +29 -0
- package/src/modules/finance-ledger/query/getPeriodByDate.generated.ts +5 -0
- package/src/modules/finance-ledger/query/getPeriodByDate.test.ts +53 -0
- package/src/modules/finance-ledger/query/getPeriodByDate.ts +27 -0
- package/src/modules/finance-ledger/query/getPeriodClose.generated.ts +5 -0
- package/src/modules/finance-ledger/query/getPeriodClose.test.ts +31 -0
- package/src/modules/finance-ledger/query/getPeriodClose.ts +21 -0
- package/src/modules/finance-ledger/query/getSubledgerTransferStatus.generated.ts +5 -0
- package/src/modules/finance-ledger/query/getSubledgerTransferStatus.test.ts +101 -0
- package/src/modules/finance-ledger/query/getSubledgerTransferStatus.ts +68 -0
- package/src/modules/finance-ledger/query/getTrialBalance.generated.ts +5 -0
- package/src/modules/finance-ledger/query/getTrialBalance.test.ts +33 -0
- package/src/modules/finance-ledger/query/getTrialBalance.ts +30 -0
- package/src/modules/finance-ledger/query/listAccountingPeriods.generated.ts +5 -0
- package/src/modules/finance-ledger/query/listAccountingPeriods.test.ts +81 -0
- package/src/modules/finance-ledger/query/listAccountingPeriods.ts +61 -0
- package/src/modules/finance-ledger/query/listFinancialStatements.generated.ts +5 -0
- package/src/modules/finance-ledger/query/listFinancialStatements.test.ts +76 -0
- package/src/modules/finance-ledger/query/listFinancialStatements.ts +62 -0
- package/src/modules/finance-ledger/query/listFiscalYears.generated.ts +5 -0
- package/src/modules/finance-ledger/query/listFiscalYears.test.ts +63 -0
- package/src/modules/finance-ledger/query/listFiscalYears.ts +45 -0
- package/src/modules/finance-ledger/query/listJournalEntries.generated.ts +5 -0
- package/src/modules/finance-ledger/query/listJournalEntries.test.ts +91 -0
- package/src/modules/finance-ledger/query/listJournalEntries.ts +64 -0
- package/src/modules/finance-ledger/query/listPeriodCloses.generated.ts +5 -0
- package/src/modules/finance-ledger/query/listPeriodCloses.test.ts +63 -0
- package/src/modules/finance-ledger/query/listPeriodCloses.ts +64 -0
- package/src/modules/finance-ledger/query/listTrialBalances.generated.ts +5 -0
- package/src/modules/finance-ledger/query/listTrialBalances.test.ts +78 -0
- package/src/modules/finance-ledger/query/listTrialBalances.ts +56 -0
- package/src/modules/finance-ledger/seed/index.ts +19 -0
- package/src/modules/finance-ledger/tailor.config.ts +13 -0
- package/src/modules/finance-ledger/tailor.d.ts +13 -0
- package/src/modules/finance-ledger/testing/commandTestUtils.ts +35 -0
- package/src/modules/finance-ledger/testing/fixtures.ts +382 -0
- package/src/modules/finance-ledger/tsconfig.json +16 -0
- package/src/progress/schema.test.ts +161 -0
- package/src/progress/schema.ts +316 -0
- package/templates/scaffold/app/backend/package.json +1 -3
- package/templates/scaffold/app/backend/vitest.config.ts +4 -21
- package/src/generator/generate-stubs.ts +0 -35
|
@@ -0,0 +1,415 @@
|
|
|
1
|
+
import { err, ok, type CommandContext } from "@tailor-platform/erp-kit/module";
|
|
2
|
+
import type { Transaction } from "../generated/kysely-tailordb";
|
|
3
|
+
import {
|
|
4
|
+
AccountingPeriodNotFoundError,
|
|
5
|
+
NotFinalPeriodError,
|
|
6
|
+
StandardCloseNotCompleteError,
|
|
7
|
+
DuplicateYearEndCloseError,
|
|
8
|
+
RetainedEarningsAccountNotFoundError,
|
|
9
|
+
NewFiscalYearNotDefinedError,
|
|
10
|
+
} from "../lib/errors.generated";
|
|
11
|
+
|
|
12
|
+
export interface ExecuteYearEndCloseInput {
|
|
13
|
+
accountingPeriodId: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
interface AuditEntry {
|
|
17
|
+
type: string;
|
|
18
|
+
fiscalYearId?: string;
|
|
19
|
+
closingEntryId?: string;
|
|
20
|
+
openingEntryId?: string;
|
|
21
|
+
netIncome?: number;
|
|
22
|
+
actorId?: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const TEMPORARY_CLASSIFICATIONS = ["REVENUE", "EXPENSE"];
|
|
26
|
+
const PERMANENT_CLASSIFICATIONS = ["ASSET", "LIABILITY", "EQUITY"];
|
|
27
|
+
const VALID_CLOSE_STATUSES = ["SOFT_LOCKED", "PERMANENTLY_CLOSED"];
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Function: executeYearEndClose
|
|
31
|
+
*
|
|
32
|
+
* Executes fiscal year-end close on the final period of a fiscal year.
|
|
33
|
+
* Generates a closing journal entry that zeros temporary accounts (REVENUE, EXPENSE)
|
|
34
|
+
* and transfers net income/loss to retained earnings. Then generates opening balance
|
|
35
|
+
* entries for the new fiscal year's permanent accounts.
|
|
36
|
+
*/
|
|
37
|
+
export async function run(db: Transaction, input: ExecuteYearEndCloseInput, ctx: CommandContext) {
|
|
38
|
+
const { accountingPeriodId } = input;
|
|
39
|
+
const auditTrail: AuditEntry[] = [];
|
|
40
|
+
|
|
41
|
+
// 1. Find AccountingPeriod
|
|
42
|
+
const period = await db
|
|
43
|
+
.selectFrom("AccountingPeriod")
|
|
44
|
+
.selectAll()
|
|
45
|
+
.where("id", "=", accountingPeriodId)
|
|
46
|
+
.executeTakeFirst();
|
|
47
|
+
|
|
48
|
+
if (!period) {
|
|
49
|
+
return err(new AccountingPeriodNotFoundError(accountingPeriodId));
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// 2. Find FiscalYear for period
|
|
53
|
+
const fiscalYear = await db
|
|
54
|
+
.selectFrom("FiscalYear")
|
|
55
|
+
.selectAll()
|
|
56
|
+
.where("id", "=", period.fiscalYearId)
|
|
57
|
+
.forUpdate()
|
|
58
|
+
.executeTakeFirst();
|
|
59
|
+
|
|
60
|
+
// 3. Validate this is the final period of the fiscal year
|
|
61
|
+
const periodEnd = new Date(period.endDate).getTime();
|
|
62
|
+
const fyEnd = new Date(fiscalYear!.endDate).getTime();
|
|
63
|
+
if (periodEnd !== fyEnd) {
|
|
64
|
+
return err(new NotFinalPeriodError(accountingPeriodId));
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// 4. Check PeriodClose exists and is in SOFT_LOCKED or PERMANENTLY_CLOSED status
|
|
68
|
+
const periodClose = await db
|
|
69
|
+
.selectFrom("PeriodClose")
|
|
70
|
+
.selectAll()
|
|
71
|
+
.where("accountingPeriodId", "=", accountingPeriodId)
|
|
72
|
+
.executeTakeFirst();
|
|
73
|
+
|
|
74
|
+
if (!periodClose || !VALID_CLOSE_STATUSES.includes(periodClose.status as string)) {
|
|
75
|
+
return err(new StandardCloseNotCompleteError(accountingPeriodId));
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// 5. Check fiscal year yearEndCloseExecuted flag
|
|
79
|
+
if (fiscalYear!.yearEndCloseExecuted) {
|
|
80
|
+
return err(new DuplicateYearEndCloseError(fiscalYear!.id));
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// 6. Find retained earnings account in active CoA
|
|
84
|
+
const _chartOfAccounts = await db
|
|
85
|
+
.selectFrom("ChartOfAccounts")
|
|
86
|
+
.selectAll()
|
|
87
|
+
.where("companyId", "=", period.companyId)
|
|
88
|
+
.where("status", "=", "ACTIVE")
|
|
89
|
+
.executeTakeFirst();
|
|
90
|
+
|
|
91
|
+
const retainedEarningsAccount = await db
|
|
92
|
+
.selectFrom("Account")
|
|
93
|
+
.selectAll()
|
|
94
|
+
.where("classification", "=", "RETAINED_EARNINGS")
|
|
95
|
+
.where("status", "=", "ACTIVE")
|
|
96
|
+
.executeTakeFirst();
|
|
97
|
+
|
|
98
|
+
if (!retainedEarningsAccount) {
|
|
99
|
+
return err(new RetainedEarningsAccountNotFoundError(period.companyId));
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// 7. Find all posted journal lines for temporary accounts in this fiscal year
|
|
103
|
+
// Get all accounting period IDs for this fiscal year
|
|
104
|
+
const allPeriods = await db
|
|
105
|
+
.selectFrom("AccountingPeriod")
|
|
106
|
+
.select("id")
|
|
107
|
+
.where("fiscalYearId", "=", fiscalYear!.id)
|
|
108
|
+
.execute();
|
|
109
|
+
|
|
110
|
+
const allPeriodIds = allPeriods.map((p: { id: string }) => p.id);
|
|
111
|
+
|
|
112
|
+
// Find all posted journal lines for temporary accounts across ALL periods in the fiscal year
|
|
113
|
+
const journalLines = await db
|
|
114
|
+
.selectFrom("JournalLine")
|
|
115
|
+
.innerJoin("JournalEntry", "JournalEntry.id", "JournalLine.journalEntryId")
|
|
116
|
+
.innerJoin("Account", "Account.id", "JournalLine.accountId")
|
|
117
|
+
.selectAll()
|
|
118
|
+
.where("JournalEntry.accountingPeriodId", "in", allPeriodIds)
|
|
119
|
+
.where("JournalEntry.status", "=", "POSTED")
|
|
120
|
+
.execute();
|
|
121
|
+
|
|
122
|
+
// Filter for temporary account lines
|
|
123
|
+
const temporaryLines = journalLines.filter((line: Record<string, unknown>) =>
|
|
124
|
+
TEMPORARY_CLASSIFICATIONS.includes(line.classification as string),
|
|
125
|
+
);
|
|
126
|
+
|
|
127
|
+
// 8. Calculate net income
|
|
128
|
+
const revenueLines = temporaryLines.filter(
|
|
129
|
+
(line: Record<string, unknown>) => line.classification === "REVENUE",
|
|
130
|
+
);
|
|
131
|
+
const expenseLines = temporaryLines.filter(
|
|
132
|
+
(line: Record<string, unknown>) => line.classification === "EXPENSE",
|
|
133
|
+
);
|
|
134
|
+
|
|
135
|
+
const totalRevenueCredits = revenueLines.reduce(
|
|
136
|
+
(sum: number, line: Record<string, unknown>) =>
|
|
137
|
+
sum + ((line.functionalCreditAmount as number) ?? 0),
|
|
138
|
+
0,
|
|
139
|
+
);
|
|
140
|
+
const totalRevenueDebits = revenueLines.reduce(
|
|
141
|
+
(sum: number, line: Record<string, unknown>) =>
|
|
142
|
+
sum + ((line.functionalDebitAmount as number) ?? 0),
|
|
143
|
+
0,
|
|
144
|
+
);
|
|
145
|
+
const totalExpenseDebits = expenseLines.reduce(
|
|
146
|
+
(sum: number, line: Record<string, unknown>) =>
|
|
147
|
+
sum + ((line.functionalDebitAmount as number) ?? 0),
|
|
148
|
+
0,
|
|
149
|
+
);
|
|
150
|
+
const totalExpenseCredits = expenseLines.reduce(
|
|
151
|
+
(sum: number, line: Record<string, unknown>) =>
|
|
152
|
+
sum + ((line.functionalCreditAmount as number) ?? 0),
|
|
153
|
+
0,
|
|
154
|
+
);
|
|
155
|
+
|
|
156
|
+
const netIncome =
|
|
157
|
+
totalRevenueCredits - totalRevenueDebits - (totalExpenseDebits - totalExpenseCredits);
|
|
158
|
+
|
|
159
|
+
// 9. Create CLOSING type journal entry
|
|
160
|
+
const now = new Date();
|
|
161
|
+
const closingEntry = await db
|
|
162
|
+
.insertInto("JournalEntry")
|
|
163
|
+
.values({
|
|
164
|
+
companyId: period.companyId,
|
|
165
|
+
accountingPeriodId,
|
|
166
|
+
entryDate: now,
|
|
167
|
+
journalType: "CLOSING",
|
|
168
|
+
referenceNumber: `CLOSE-${fiscalYear!.id}`,
|
|
169
|
+
status: "POSTED",
|
|
170
|
+
description: `Year-end closing entry for ${fiscalYear!.name}`,
|
|
171
|
+
sourceDocumentReference: null,
|
|
172
|
+
adjustingEntryType: null,
|
|
173
|
+
reversalOfId: null,
|
|
174
|
+
postedAt: now,
|
|
175
|
+
createdAt: now,
|
|
176
|
+
updatedAt: null,
|
|
177
|
+
})
|
|
178
|
+
.returningAll()
|
|
179
|
+
.executeTakeFirst();
|
|
180
|
+
|
|
181
|
+
// Create closing journal lines: debit revenue accounts, credit expense accounts, net to retained earnings
|
|
182
|
+
const closingLines: {
|
|
183
|
+
journalEntryId: string;
|
|
184
|
+
accountId: string;
|
|
185
|
+
debitAmount: number | null;
|
|
186
|
+
creditAmount: number | null;
|
|
187
|
+
description: string;
|
|
188
|
+
currencyCode: null;
|
|
189
|
+
exchangeRate: null;
|
|
190
|
+
functionalDebitAmount: number | null;
|
|
191
|
+
functionalCreditAmount: number | null;
|
|
192
|
+
createdAt: Date;
|
|
193
|
+
updatedAt: null;
|
|
194
|
+
}[] = [];
|
|
195
|
+
|
|
196
|
+
// Debit revenue accounts (to zero them out)
|
|
197
|
+
for (const line of revenueLines) {
|
|
198
|
+
closingLines.push({
|
|
199
|
+
journalEntryId: closingEntry!.id,
|
|
200
|
+
accountId: line.accountId,
|
|
201
|
+
debitAmount: line.functionalCreditAmount! ?? 0,
|
|
202
|
+
creditAmount: null,
|
|
203
|
+
description: "Close revenue account",
|
|
204
|
+
currencyCode: null,
|
|
205
|
+
exchangeRate: null,
|
|
206
|
+
functionalDebitAmount: line.functionalCreditAmount! ?? 0,
|
|
207
|
+
functionalCreditAmount: null,
|
|
208
|
+
createdAt: now,
|
|
209
|
+
updatedAt: null,
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// Credit expense accounts (to zero them out)
|
|
214
|
+
for (const line of expenseLines) {
|
|
215
|
+
closingLines.push({
|
|
216
|
+
journalEntryId: closingEntry!.id,
|
|
217
|
+
accountId: line.accountId,
|
|
218
|
+
debitAmount: null,
|
|
219
|
+
creditAmount: line.functionalDebitAmount! ?? 0,
|
|
220
|
+
description: "Close expense account",
|
|
221
|
+
currencyCode: null,
|
|
222
|
+
exchangeRate: null,
|
|
223
|
+
functionalDebitAmount: null,
|
|
224
|
+
functionalCreditAmount: line.functionalDebitAmount! ?? 0,
|
|
225
|
+
createdAt: now,
|
|
226
|
+
updatedAt: null,
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// Net income/loss to retained earnings
|
|
231
|
+
if (netIncome >= 0) {
|
|
232
|
+
closingLines.push({
|
|
233
|
+
journalEntryId: closingEntry!.id,
|
|
234
|
+
accountId: retainedEarningsAccount.id,
|
|
235
|
+
debitAmount: null,
|
|
236
|
+
creditAmount: netIncome,
|
|
237
|
+
description: "Transfer net income to retained earnings",
|
|
238
|
+
currencyCode: null,
|
|
239
|
+
exchangeRate: null,
|
|
240
|
+
functionalDebitAmount: null,
|
|
241
|
+
functionalCreditAmount: netIncome,
|
|
242
|
+
createdAt: now,
|
|
243
|
+
updatedAt: null,
|
|
244
|
+
});
|
|
245
|
+
} else {
|
|
246
|
+
closingLines.push({
|
|
247
|
+
journalEntryId: closingEntry!.id,
|
|
248
|
+
accountId: retainedEarningsAccount.id,
|
|
249
|
+
debitAmount: Math.abs(netIncome),
|
|
250
|
+
creditAmount: null,
|
|
251
|
+
description: "Transfer net loss to retained earnings",
|
|
252
|
+
currencyCode: null,
|
|
253
|
+
exchangeRate: null,
|
|
254
|
+
functionalDebitAmount: Math.abs(netIncome),
|
|
255
|
+
functionalCreditAmount: null,
|
|
256
|
+
createdAt: now,
|
|
257
|
+
updatedAt: null,
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
if (closingLines.length > 0) {
|
|
262
|
+
await db.insertInto("JournalLine").values(closingLines).returningAll().execute();
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// 10. Find next fiscal year
|
|
266
|
+
const nextFiscalYear = await db
|
|
267
|
+
.selectFrom("FiscalYear")
|
|
268
|
+
.selectAll()
|
|
269
|
+
.where("companyId", "=", period.companyId)
|
|
270
|
+
.where("startDate", ">", fiscalYear!.endDate)
|
|
271
|
+
.orderBy("startDate", "asc")
|
|
272
|
+
.limit(1)
|
|
273
|
+
.executeTakeFirst();
|
|
274
|
+
|
|
275
|
+
if (!nextFiscalYear) {
|
|
276
|
+
return err(new NewFiscalYearNotDefinedError(fiscalYear!.id));
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// 11. Generate OPENING type journal entry for permanent accounts in new fiscal year
|
|
280
|
+
const permanentLines = journalLines.filter((line: Record<string, unknown>) =>
|
|
281
|
+
PERMANENT_CLASSIFICATIONS.includes(line.classification as string),
|
|
282
|
+
);
|
|
283
|
+
|
|
284
|
+
// Find first period of new fiscal year
|
|
285
|
+
const newFirstPeriod = await db
|
|
286
|
+
.selectFrom("AccountingPeriod")
|
|
287
|
+
.selectAll()
|
|
288
|
+
.where("fiscalYearId", "=", nextFiscalYear.id)
|
|
289
|
+
.orderBy("startDate", "asc")
|
|
290
|
+
.limit(1)
|
|
291
|
+
.executeTakeFirst();
|
|
292
|
+
|
|
293
|
+
const openingEntry = await db
|
|
294
|
+
.insertInto("JournalEntry")
|
|
295
|
+
.values({
|
|
296
|
+
companyId: period.companyId,
|
|
297
|
+
accountingPeriodId: newFirstPeriod ? newFirstPeriod.id : nextFiscalYear.id,
|
|
298
|
+
entryDate: now,
|
|
299
|
+
journalType: "OPENING",
|
|
300
|
+
referenceNumber: `OPEN-${nextFiscalYear.id}`,
|
|
301
|
+
status: "POSTED",
|
|
302
|
+
description: `Opening balance entry for ${nextFiscalYear.name}`,
|
|
303
|
+
sourceDocumentReference: null,
|
|
304
|
+
adjustingEntryType: null,
|
|
305
|
+
reversalOfId: null,
|
|
306
|
+
postedAt: now,
|
|
307
|
+
createdAt: now,
|
|
308
|
+
updatedAt: null,
|
|
309
|
+
})
|
|
310
|
+
.returningAll()
|
|
311
|
+
.executeTakeFirst();
|
|
312
|
+
|
|
313
|
+
// Create opening journal lines for permanent accounts
|
|
314
|
+
const openingLines: {
|
|
315
|
+
journalEntryId: string;
|
|
316
|
+
accountId: string;
|
|
317
|
+
debitAmount: number | null;
|
|
318
|
+
creditAmount: number | null;
|
|
319
|
+
description: string;
|
|
320
|
+
currencyCode: null;
|
|
321
|
+
exchangeRate: null;
|
|
322
|
+
functionalDebitAmount: number | null;
|
|
323
|
+
functionalCreditAmount: number | null;
|
|
324
|
+
createdAt: Date;
|
|
325
|
+
updatedAt: null;
|
|
326
|
+
}[] = [];
|
|
327
|
+
for (const line of permanentLines) {
|
|
328
|
+
openingLines.push({
|
|
329
|
+
journalEntryId: openingEntry!.id,
|
|
330
|
+
accountId: line.accountId,
|
|
331
|
+
debitAmount: line.functionalDebitAmount! ?? null,
|
|
332
|
+
creditAmount: line.functionalCreditAmount! ?? null,
|
|
333
|
+
description: "Opening balance",
|
|
334
|
+
currencyCode: null,
|
|
335
|
+
exchangeRate: null,
|
|
336
|
+
functionalDebitAmount: line.functionalDebitAmount! ?? null,
|
|
337
|
+
functionalCreditAmount: line.functionalCreditAmount! ?? null,
|
|
338
|
+
createdAt: now,
|
|
339
|
+
updatedAt: null,
|
|
340
|
+
});
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// Include retained earnings in opening entry
|
|
344
|
+
if (netIncome >= 0) {
|
|
345
|
+
openingLines.push({
|
|
346
|
+
journalEntryId: openingEntry!.id,
|
|
347
|
+
accountId: retainedEarningsAccount.id,
|
|
348
|
+
debitAmount: null,
|
|
349
|
+
creditAmount: netIncome,
|
|
350
|
+
description: "Opening retained earnings balance",
|
|
351
|
+
currencyCode: null,
|
|
352
|
+
exchangeRate: null,
|
|
353
|
+
functionalDebitAmount: null,
|
|
354
|
+
functionalCreditAmount: netIncome,
|
|
355
|
+
createdAt: now,
|
|
356
|
+
updatedAt: null,
|
|
357
|
+
});
|
|
358
|
+
} else {
|
|
359
|
+
openingLines.push({
|
|
360
|
+
journalEntryId: openingEntry!.id,
|
|
361
|
+
accountId: retainedEarningsAccount.id,
|
|
362
|
+
debitAmount: Math.abs(netIncome),
|
|
363
|
+
creditAmount: null,
|
|
364
|
+
description: "Opening retained earnings balance",
|
|
365
|
+
currencyCode: null,
|
|
366
|
+
exchangeRate: null,
|
|
367
|
+
functionalDebitAmount: Math.abs(netIncome),
|
|
368
|
+
functionalCreditAmount: null,
|
|
369
|
+
createdAt: now,
|
|
370
|
+
updatedAt: null,
|
|
371
|
+
});
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
if (openingLines.length > 0) {
|
|
375
|
+
await db.insertInto("JournalLine").values(openingLines).returningAll().execute();
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
// 12. Update fiscal year yearEndCloseExecuted = true
|
|
379
|
+
await db
|
|
380
|
+
.updateTable("FiscalYear")
|
|
381
|
+
.set({
|
|
382
|
+
yearEndCloseExecuted: true,
|
|
383
|
+
updatedAt: now,
|
|
384
|
+
})
|
|
385
|
+
.where("id", "=", fiscalYear!.id)
|
|
386
|
+
.returningAll()
|
|
387
|
+
.executeTakeFirstOrThrow();
|
|
388
|
+
|
|
389
|
+
// 13. Update PeriodClose yearEndCloseExecuted = true
|
|
390
|
+
await db
|
|
391
|
+
.updateTable("PeriodClose")
|
|
392
|
+
.set({
|
|
393
|
+
yearEndCloseExecuted: true,
|
|
394
|
+
updatedAt: now,
|
|
395
|
+
})
|
|
396
|
+
.where("id", "=", periodClose.id)
|
|
397
|
+
.returningAll()
|
|
398
|
+
.executeTakeFirstOrThrow();
|
|
399
|
+
|
|
400
|
+
// 14. Record audit trail
|
|
401
|
+
auditTrail.push({
|
|
402
|
+
type: "YEAR_END_CLOSE",
|
|
403
|
+
fiscalYearId: fiscalYear!.id,
|
|
404
|
+
closingEntryId: closingEntry!.id,
|
|
405
|
+
openingEntryId: openingEntry!.id,
|
|
406
|
+
netIncome,
|
|
407
|
+
actorId: ctx.actorId,
|
|
408
|
+
});
|
|
409
|
+
|
|
410
|
+
return ok({
|
|
411
|
+
closingEntry: closingEntry!,
|
|
412
|
+
openingEntry: openingEntry!,
|
|
413
|
+
auditTrail,
|
|
414
|
+
});
|
|
415
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
// @generated — do not edit
|
|
2
|
+
import { defineCommand } from "@tailor-platform/erp-kit/module";
|
|
3
|
+
import { permissions } from "../lib/permissions.generated";
|
|
4
|
+
import { run } from "./finalCloseAndLockPeriod";
|
|
5
|
+
|
|
6
|
+
export const finalCloseAndLockPeriod = defineCommand(permissions.finalCloseAndLockPeriod, run);
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { createMockDb } from "../../../testing/index";
|
|
3
|
+
import type { Transaction } from "../generated/kysely-tailordb";
|
|
4
|
+
import { PeriodCloseNotFoundError, InvalidStatusTransitionError } from "../lib/errors.generated";
|
|
5
|
+
import {
|
|
6
|
+
softLockedPeriodClose,
|
|
7
|
+
underReviewPeriodClose,
|
|
8
|
+
inProgressPeriodClose,
|
|
9
|
+
} from "../testing/fixtures";
|
|
10
|
+
import { commandCtx, expectErr, expectOk } from "../testing/commandTestUtils";
|
|
11
|
+
import { run } from "./finalCloseAndLockPeriod";
|
|
12
|
+
|
|
13
|
+
describe("finalCloseAndLockPeriod", () => {
|
|
14
|
+
it("returns error when PeriodClose record does not exist", async () => {
|
|
15
|
+
const { db, spies } = createMockDb<Transaction>();
|
|
16
|
+
spies.select.mockReturnValueOnce(undefined);
|
|
17
|
+
|
|
18
|
+
const result = await run(db, { id: "nonexistent" }, commandCtx);
|
|
19
|
+
|
|
20
|
+
expectErr(result, PeriodCloseNotFoundError);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it("returns error when PeriodClose is in UNDER_REVIEW status", async () => {
|
|
24
|
+
const { db, spies } = createMockDb<Transaction>();
|
|
25
|
+
spies.select.mockReturnValueOnce(underReviewPeriodClose);
|
|
26
|
+
|
|
27
|
+
const result = await run(db, { id: underReviewPeriodClose.id }, commandCtx);
|
|
28
|
+
|
|
29
|
+
expectErr(result, InvalidStatusTransitionError);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it("returns error when PeriodClose is in IN_PROGRESS status", async () => {
|
|
33
|
+
const { db, spies } = createMockDb<Transaction>();
|
|
34
|
+
spies.select.mockReturnValueOnce(inProgressPeriodClose);
|
|
35
|
+
|
|
36
|
+
const result = await run(db, { id: inProgressPeriodClose.id }, commandCtx);
|
|
37
|
+
|
|
38
|
+
expectErr(result, InvalidStatusTransitionError);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it("transitions SOFT_LOCKED PeriodClose to PERMANENTLY_CLOSED with completionPercentage 100", async () => {
|
|
42
|
+
const updatedPeriodClose = {
|
|
43
|
+
...softLockedPeriodClose,
|
|
44
|
+
status: "PERMANENTLY_CLOSED",
|
|
45
|
+
completionPercentage: 100,
|
|
46
|
+
finalizedByUserId: commandCtx.actorId,
|
|
47
|
+
};
|
|
48
|
+
const { db, spies } = createMockDb<Transaction>();
|
|
49
|
+
spies.select.mockReturnValueOnce(softLockedPeriodClose);
|
|
50
|
+
spies.update.mockReturnValueOnce(updatedPeriodClose);
|
|
51
|
+
spies.update.mockReturnValueOnce({ status: "PERMANENTLY_CLOSED" });
|
|
52
|
+
|
|
53
|
+
const result = await run(db, { id: softLockedPeriodClose.id }, commandCtx);
|
|
54
|
+
|
|
55
|
+
const value = expectOk(result);
|
|
56
|
+
expect(value.periodClose.status).toBe("PERMANENTLY_CLOSED");
|
|
57
|
+
expect(value.periodClose.completionPercentage).toBe(100);
|
|
58
|
+
expect(value.periodClose.finalizedByUserId).toBe(commandCtx.actorId);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it("also updates the associated AccountingPeriod to PERMANENTLY_CLOSED", async () => {
|
|
62
|
+
const updatedPeriodClose = {
|
|
63
|
+
...softLockedPeriodClose,
|
|
64
|
+
status: "PERMANENTLY_CLOSED",
|
|
65
|
+
completionPercentage: 100,
|
|
66
|
+
};
|
|
67
|
+
const updatedAccountingPeriod = { status: "PERMANENTLY_CLOSED" };
|
|
68
|
+
const { db, spies } = createMockDb<Transaction>();
|
|
69
|
+
spies.select.mockReturnValueOnce(softLockedPeriodClose);
|
|
70
|
+
spies.update.mockReturnValueOnce(updatedPeriodClose);
|
|
71
|
+
spies.update.mockReturnValueOnce(updatedAccountingPeriod);
|
|
72
|
+
|
|
73
|
+
const result = await run(db, { id: softLockedPeriodClose.id }, commandCtx);
|
|
74
|
+
|
|
75
|
+
expectOk(result);
|
|
76
|
+
expect(spies.update).toHaveBeenCalledTimes(2);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it("emits audit event recording status transition from SOFT_LOCKED to PERMANENTLY_CLOSED", async () => {
|
|
80
|
+
const updatedPeriodClose = {
|
|
81
|
+
...softLockedPeriodClose,
|
|
82
|
+
status: "PERMANENTLY_CLOSED",
|
|
83
|
+
completionPercentage: 100,
|
|
84
|
+
};
|
|
85
|
+
const { db, spies } = createMockDb<Transaction>();
|
|
86
|
+
spies.select.mockReturnValueOnce(softLockedPeriodClose);
|
|
87
|
+
spies.update.mockReturnValueOnce(updatedPeriodClose);
|
|
88
|
+
spies.update.mockReturnValueOnce({ status: "PERMANENTLY_CLOSED" });
|
|
89
|
+
|
|
90
|
+
const result = await run(db, { id: softLockedPeriodClose.id }, commandCtx);
|
|
91
|
+
|
|
92
|
+
const value = expectOk(result);
|
|
93
|
+
expect(value.auditTrail).toContainEqual(
|
|
94
|
+
expect.objectContaining({
|
|
95
|
+
type: "STATUS_TRANSITION",
|
|
96
|
+
from: "SOFT_LOCKED",
|
|
97
|
+
to: "PERMANENTLY_CLOSED",
|
|
98
|
+
actorId: commandCtx.actorId,
|
|
99
|
+
}),
|
|
100
|
+
);
|
|
101
|
+
});
|
|
102
|
+
});
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { err, ok, type CommandContext } from "@tailor-platform/erp-kit/module";
|
|
2
|
+
import type { Transaction } from "../generated/kysely-tailordb";
|
|
3
|
+
import { PeriodCloseNotFoundError, InvalidStatusTransitionError } from "../lib/errors.generated";
|
|
4
|
+
|
|
5
|
+
export interface FinalCloseAndLockPeriodInput {
|
|
6
|
+
id: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Function: finalCloseAndLockPeriod
|
|
11
|
+
*
|
|
12
|
+
* Transitions a PeriodClose record from SOFT_LOCKED to PERMANENTLY_CLOSED status,
|
|
13
|
+
* making the period fully immutable — no further journal entries allowed regardless of role.
|
|
14
|
+
* Also transitions the underlying AccountingPeriod to PERMANENTLY_CLOSED.
|
|
15
|
+
* Sets completionPercentage to 100% and records the operator's userId.
|
|
16
|
+
*/
|
|
17
|
+
export async function run(
|
|
18
|
+
db: Transaction,
|
|
19
|
+
input: FinalCloseAndLockPeriodInput,
|
|
20
|
+
ctx: CommandContext,
|
|
21
|
+
) {
|
|
22
|
+
const { id } = input;
|
|
23
|
+
|
|
24
|
+
// 1. Find PeriodClose record
|
|
25
|
+
const periodClose = await db
|
|
26
|
+
.selectFrom("PeriodClose")
|
|
27
|
+
.selectAll()
|
|
28
|
+
.where("id", "=", id)
|
|
29
|
+
.forUpdate()
|
|
30
|
+
.executeTakeFirst();
|
|
31
|
+
|
|
32
|
+
if (!periodClose) {
|
|
33
|
+
return err(new PeriodCloseNotFoundError(id));
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// 2. Validate status is SOFT_LOCKED
|
|
37
|
+
if (periodClose.status !== "SOFT_LOCKED") {
|
|
38
|
+
return err(new InvalidStatusTransitionError(id));
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// 3. Transition PeriodClose to PERMANENTLY_CLOSED with completionPercentage 100
|
|
42
|
+
const updated = await db
|
|
43
|
+
.updateTable("PeriodClose")
|
|
44
|
+
.set({
|
|
45
|
+
status: "PERMANENTLY_CLOSED",
|
|
46
|
+
completionPercentage: 100,
|
|
47
|
+
finalizedByUserId: ctx.actorId,
|
|
48
|
+
updatedAt: new Date(),
|
|
49
|
+
})
|
|
50
|
+
.where("id", "=", id)
|
|
51
|
+
.returningAll()
|
|
52
|
+
.executeTakeFirstOrThrow();
|
|
53
|
+
|
|
54
|
+
// 4. Also transition the associated AccountingPeriod to PERMANENTLY_CLOSED
|
|
55
|
+
await db
|
|
56
|
+
.updateTable("AccountingPeriod")
|
|
57
|
+
.set({
|
|
58
|
+
status: "PERMANENTLY_CLOSED",
|
|
59
|
+
updatedAt: new Date(),
|
|
60
|
+
})
|
|
61
|
+
.where("id", "=", periodClose.accountingPeriodId)
|
|
62
|
+
.returningAll()
|
|
63
|
+
.executeTakeFirstOrThrow();
|
|
64
|
+
|
|
65
|
+
// 5. Emit audit event recording status transition and operator
|
|
66
|
+
const auditTrail = [
|
|
67
|
+
{
|
|
68
|
+
type: "STATUS_TRANSITION",
|
|
69
|
+
from: "SOFT_LOCKED",
|
|
70
|
+
to: "PERMANENTLY_CLOSED",
|
|
71
|
+
actorId: ctx.actorId,
|
|
72
|
+
},
|
|
73
|
+
];
|
|
74
|
+
|
|
75
|
+
return ok({ periodClose: updated, auditTrail });
|
|
76
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
// @generated — do not edit
|
|
2
|
+
import { defineCommand } from "@tailor-platform/erp-kit/module";
|
|
3
|
+
import { permissions } from "../lib/permissions.generated";
|
|
4
|
+
import { run } from "./finalizeFinancialStatement";
|
|
5
|
+
|
|
6
|
+
export const finalizeFinancialStatement = defineCommand(permissions.finalizeFinancialStatement, run);
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { createMockDb } from "../../../testing/index";
|
|
3
|
+
import type { Transaction } from "../generated/kysely-tailordb";
|
|
4
|
+
import {
|
|
5
|
+
FinancialStatementNotFoundError,
|
|
6
|
+
InvalidStatusTransitionError,
|
|
7
|
+
} from "../lib/errors.generated";
|
|
8
|
+
import { baseFinancialStatement, finalizedFinancialStatement } from "../testing/fixtures";
|
|
9
|
+
import { commandCtx, expectErr, expectOk } from "../testing/commandTestUtils";
|
|
10
|
+
import { run } from "./finalizeFinancialStatement";
|
|
11
|
+
|
|
12
|
+
describe("finalizeFinancialStatement", () => {
|
|
13
|
+
it("returns error when financial statement does not exist", async () => {
|
|
14
|
+
const { db, spies } = createMockDb<Transaction>();
|
|
15
|
+
spies.select.mockReturnValueOnce(undefined); // statement lookup
|
|
16
|
+
|
|
17
|
+
const result = await run(db, { id: "nonexistent" }, commandCtx);
|
|
18
|
+
|
|
19
|
+
expectErr(result, FinancialStatementNotFoundError);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it("returns error when financial statement is already FINALIZED", async () => {
|
|
23
|
+
const { db, spies } = createMockDb<Transaction>();
|
|
24
|
+
spies.select.mockReturnValueOnce(finalizedFinancialStatement); // statement lookup
|
|
25
|
+
|
|
26
|
+
const result = await run(db, { id: finalizedFinancialStatement.id }, commandCtx);
|
|
27
|
+
|
|
28
|
+
expectErr(result, InvalidStatusTransitionError);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it("transitions DRAFT statement to FINALIZED with finalizedAt set", async () => {
|
|
32
|
+
const { db, spies } = createMockDb<Transaction>();
|
|
33
|
+
const finalizedResult = {
|
|
34
|
+
...baseFinancialStatement,
|
|
35
|
+
status: "FINALIZED",
|
|
36
|
+
finalizedAt: new Date(),
|
|
37
|
+
updatedAt: new Date(),
|
|
38
|
+
};
|
|
39
|
+
spies.select.mockReturnValueOnce(baseFinancialStatement); // statement lookup
|
|
40
|
+
spies.update.mockReturnValueOnce(finalizedResult); // update result
|
|
41
|
+
|
|
42
|
+
const result = await run(db, { id: baseFinancialStatement.id }, commandCtx);
|
|
43
|
+
|
|
44
|
+
const value = expectOk(result);
|
|
45
|
+
expect(value.financialStatement.status).toBe("FINALIZED");
|
|
46
|
+
expect(value.financialStatement.finalizedAt).toBeDefined();
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it("emits audit event recording status transition from DRAFT to FINALIZED", async () => {
|
|
50
|
+
const { db, spies } = createMockDb<Transaction>();
|
|
51
|
+
const finalizedResult = {
|
|
52
|
+
...baseFinancialStatement,
|
|
53
|
+
status: "FINALIZED",
|
|
54
|
+
finalizedAt: new Date(),
|
|
55
|
+
updatedAt: new Date(),
|
|
56
|
+
};
|
|
57
|
+
spies.select.mockReturnValueOnce(baseFinancialStatement); // statement lookup
|
|
58
|
+
spies.update.mockReturnValueOnce(finalizedResult); // update result
|
|
59
|
+
|
|
60
|
+
const result = await run(db, { id: baseFinancialStatement.id }, commandCtx);
|
|
61
|
+
|
|
62
|
+
const value = expectOk(result);
|
|
63
|
+
expect(value.auditTrail).toBeDefined();
|
|
64
|
+
expect(value.auditTrail).toContainEqual(
|
|
65
|
+
expect.objectContaining({
|
|
66
|
+
type: "STATUS_TRANSITION",
|
|
67
|
+
from: "DRAFT",
|
|
68
|
+
to: "FINALIZED",
|
|
69
|
+
actorId: commandCtx.actorId,
|
|
70
|
+
}),
|
|
71
|
+
);
|
|
72
|
+
});
|
|
73
|
+
});
|