From 736cedd63d823439685761bec9fe667a4b0e8047 Mon Sep 17 00:00:00 2001 From: Ahmed Bouhuolia Date: Thu, 26 Dec 2024 20:57:21 +0200 Subject: [PATCH] feat: migrate manual journal to nestjs --- .../server-nest/src/modules/App/App.module.ts | 2 + .../src/modules/Branches/Branches.module.ts | 10 +- .../ManualJournalDTOTransformer.service.ts | 37 +++ .../ManualJournalDTOTransformer.ts | 32 -- .../src/modules/Contacts/models/Contact.ts | 28 +- .../CommandExpenseValidator.service.ts | 6 +- .../src/modules/Import/ImportableResources.ts | 2 +- .../ManualJournals.controller.ts | 49 +++ .../ManualJournals/ManualJournals.module.ts | 34 ++ .../ManualJournalsApplication.service.ts | 89 ++++++ .../AutoIncrementManualJournal.service.ts | 42 +++ .../CommandManualJournalValidators.service.ts | 295 ++++++++++++++++++ .../commands/CreateManualJournal.service.ts | 159 ++++++++++ .../commands/DeleteManualJournal.service.ts | 71 +++++ .../commands/EditManualJournal.service.ts | 139 +++++++++ .../commands/ManualJournalExportable.ts | 30 ++ .../commands/ManualJournalGLEntries.ts | 161 ++++++++++ .../ManualJournalGLEntriesSubscriber.ts | 131 ++++++++ .../commands/ManualJournalsImport.ts | 60 ++++ .../commands/PublishManualJournal.service.ts | 73 +++++ .../src/modules/ManualJournals/constants.ts | 64 ++++ .../ManualJournals/models/ManualJournal.ts | 207 ++++++++++++ .../models/ManualJournalEntry.ts | 64 ++++ .../queries/GetManualJournal.service.ts | 36 +++ .../queries/GetManualJournals.service.ts | 77 +++++ .../queries/ManualJournalTransformer.ts | 66 ++++ .../types/ManualJournals.types.ts | 100 ++++++ .../System/models/TenantMetadataModel.ts | 25 +- .../src/modules/System/models/TenantModel.ts | 2 +- .../TemplateInjectable.service.ts | 3 +- .../Tenancy/TenancyModels/Tenancy.module.ts | 6 +- .../TransactionItemEntry/models/ItemEntry.ts | 1 - .../src/modules/Transformer/Transformer.ts | 2 +- .../test/manual-journal.e2e-spec.ts | 95 ++++++ 34 files changed, 2129 insertions(+), 69 deletions(-) create mode 100644 packages/server-nest/src/modules/Branches/integrations/ManualJournals/ManualJournalDTOTransformer.service.ts delete mode 100644 packages/server-nest/src/modules/Branches/integrations/ManualJournals/ManualJournalDTOTransformer.ts create mode 100644 packages/server-nest/src/modules/ManualJournals/ManualJournals.controller.ts create mode 100644 packages/server-nest/src/modules/ManualJournals/ManualJournals.module.ts create mode 100644 packages/server-nest/src/modules/ManualJournals/ManualJournalsApplication.service.ts create mode 100644 packages/server-nest/src/modules/ManualJournals/commands/AutoIncrementManualJournal.service.ts create mode 100644 packages/server-nest/src/modules/ManualJournals/commands/CommandManualJournalValidators.service.ts create mode 100644 packages/server-nest/src/modules/ManualJournals/commands/CreateManualJournal.service.ts create mode 100644 packages/server-nest/src/modules/ManualJournals/commands/DeleteManualJournal.service.ts create mode 100644 packages/server-nest/src/modules/ManualJournals/commands/EditManualJournal.service.ts create mode 100644 packages/server-nest/src/modules/ManualJournals/commands/ManualJournalExportable.ts create mode 100644 packages/server-nest/src/modules/ManualJournals/commands/ManualJournalGLEntries.ts create mode 100644 packages/server-nest/src/modules/ManualJournals/commands/ManualJournalGLEntriesSubscriber.ts create mode 100644 packages/server-nest/src/modules/ManualJournals/commands/ManualJournalsImport.ts create mode 100644 packages/server-nest/src/modules/ManualJournals/commands/PublishManualJournal.service.ts create mode 100644 packages/server-nest/src/modules/ManualJournals/constants.ts create mode 100644 packages/server-nest/src/modules/ManualJournals/models/ManualJournal.ts create mode 100644 packages/server-nest/src/modules/ManualJournals/models/ManualJournalEntry.ts create mode 100644 packages/server-nest/src/modules/ManualJournals/queries/GetManualJournal.service.ts create mode 100644 packages/server-nest/src/modules/ManualJournals/queries/GetManualJournals.service.ts create mode 100644 packages/server-nest/src/modules/ManualJournals/queries/ManualJournalTransformer.ts create mode 100644 packages/server-nest/src/modules/ManualJournals/types/ManualJournals.types.ts create mode 100644 packages/server-nest/test/manual-journal.e2e-spec.ts diff --git a/packages/server-nest/src/modules/App/App.module.ts b/packages/server-nest/src/modules/App/App.module.ts index 9fd739684..04014bcc8 100644 --- a/packages/server-nest/src/modules/App/App.module.ts +++ b/packages/server-nest/src/modules/App/App.module.ts @@ -45,6 +45,7 @@ import { SaleEstimatesModule } from '../SaleEstimates/SaleEstimates.module'; import { BillsModule } from '../Bills/Bills.module'; import { SaleInvoicesModule } from '../SaleInvoices/SaleInvoices.module'; import { SaleReceiptsModule } from '../SaleReceipts/SaleReceipts.module'; +import { ManualJournalsModule } from '../ManualJournals/ManualJournals.module'; // import { BillPaymentsModule } from '../BillPayments/BillPayments.module'; @Module({ @@ -115,6 +116,7 @@ import { SaleReceiptsModule } from '../SaleReceipts/SaleReceipts.module'; SaleEstimatesModule, SaleReceiptsModule, BillsModule, + ManualJournalsModule // BillPaymentsModule, ], controllers: [AppController], diff --git a/packages/server-nest/src/modules/Branches/Branches.module.ts b/packages/server-nest/src/modules/Branches/Branches.module.ts index 81dc1085a..8b779a792 100644 --- a/packages/server-nest/src/modules/Branches/Branches.module.ts +++ b/packages/server-nest/src/modules/Branches/Branches.module.ts @@ -14,6 +14,7 @@ import { BranchesApplication } from './BranchesApplication.service'; import { BranchesSettingsService } from './BranchesSettings'; import { BranchCommandValidator } from './commands/BranchCommandValidator.service'; import { BranchTransactionDTOTransformer } from './integrations/BranchTransactionDTOTransform'; +import { ManualJournalBranchesDTOTransformer } from './integrations/ManualJournals/ManualJournalDTOTransformer.service'; @Module({ imports: [TenancyDatabaseModule], @@ -31,8 +32,13 @@ import { BranchTransactionDTOTransformer } from './integrations/BranchTransactio TenancyContext, TransformerInjectable, BranchCommandValidator, - BranchTransactionDTOTransformer + BranchTransactionDTOTransformer, + ManualJournalBranchesDTOTransformer, + ], + exports: [ + BranchesSettingsService, + BranchTransactionDTOTransformer, + ManualJournalBranchesDTOTransformer, ], - exports: [BranchTransactionDTOTransformer], }) export class BranchesModule {} diff --git a/packages/server-nest/src/modules/Branches/integrations/ManualJournals/ManualJournalDTOTransformer.service.ts b/packages/server-nest/src/modules/Branches/integrations/ManualJournals/ManualJournalDTOTransformer.service.ts new file mode 100644 index 000000000..bc88fdd8d --- /dev/null +++ b/packages/server-nest/src/modules/Branches/integrations/ManualJournals/ManualJournalDTOTransformer.service.ts @@ -0,0 +1,37 @@ +import { Inject, Injectable } from '@nestjs/common'; +import { omit } from 'lodash'; +import { BranchesSettingsService } from '../../BranchesSettings'; +import { IManualJournalDTO } from '@/modules/ManualJournals/types/ManualJournals.types'; + +@Injectable() +export class ManualJournalBranchesDTOTransformer { + constructor( + @Inject() + private readonly branchesSettings: BranchesSettingsService, + ) {} + + /** + * + * @param DTO + * @returns + */ + private excludeDTOBranchIdWhenInactive = ( + DTO: IManualJournalDTO, + ): IManualJournalDTO => { + const isActive = this.branchesSettings.isMultiBranchesActive(); + + if (isActive) return DTO; + + return { + ...DTO, + entries: DTO.entries.map((e) => omit(e, ['branchId'])), + }; + }; + + /** + * + */ + public transformDTO = (DTO: IManualJournalDTO): IManualJournalDTO => { + return this.excludeDTOBranchIdWhenInactive(DTO); + }; +} diff --git a/packages/server-nest/src/modules/Branches/integrations/ManualJournals/ManualJournalDTOTransformer.ts b/packages/server-nest/src/modules/Branches/integrations/ManualJournals/ManualJournalDTOTransformer.ts deleted file mode 100644 index 37c89e722..000000000 --- a/packages/server-nest/src/modules/Branches/integrations/ManualJournals/ManualJournalDTOTransformer.ts +++ /dev/null @@ -1,32 +0,0 @@ -// import { omit } from 'lodash'; -// import { Inject, Service } from 'typedi'; -// import { IManualJournal } from '@/interfaces'; -// import { BranchesSettings } from '../../BranchesSettings'; - -// @Service() -// export class ManualJournalBranchesDTOTransformer { -// @Inject() -// branchesSettings: BranchesSettings; - -// private excludeDTOBranchIdWhenInactive = ( -// tenantId: number, -// DTO: IManualJournal -// ): IManualJournal => { -// const isActive = this.branchesSettings.isMultiBranchesActive(tenantId); - -// if (isActive) return DTO; - -// return { -// ...DTO, -// entries: DTO.entries.map((e) => omit(e, ['branchId'])), -// }; -// }; -// /** -// * -// */ -// public transformDTO = -// (tenantId: number) => -// (DTO: IManualJournal): IManualJournal => { -// return this.excludeDTOBranchIdWhenInactive(tenantId, DTO); -// }; -// } diff --git a/packages/server-nest/src/modules/Contacts/models/Contact.ts b/packages/server-nest/src/modules/Contacts/models/Contact.ts index 1e43af419..fc102bcf8 100644 --- a/packages/server-nest/src/modules/Contacts/models/Contact.ts +++ b/packages/server-nest/src/modules/Contacts/models/Contact.ts @@ -132,13 +132,13 @@ export class Contact extends BaseModel { * Relationship mapping. */ static get relationMappings() { - const SaleEstimate = require('models/SaleEstimate'); - const SaleReceipt = require('models/SaleReceipt'); - const SaleInvoice = require('models/SaleInvoice'); - const PaymentReceive = require('models/PaymentReceive'); - const Bill = require('models/Bill'); - const BillPayment = require('models/BillPayment'); - const AccountTransaction = require('models/AccountTransaction'); + const { SaleEstimate } = require('../../SaleEstimates/models/SaleEstimate'); + const { SaleReceipt } = require('../../SaleReceipts/models/SaleReceipt'); + const { SaleInvoice } = require('../../SaleInvoices/models/SaleInvoice'); + const { PaymentReceived } = require('../../PaymentReceived/models/PaymentReceived'); + const { Bill } = require('../../Bills/models/Bill'); + const { BillPayment } = require('../../BillPayments/models/BillPayment'); + const { AccountTransaction } = require('../../Accounts/models/AccountTransaction.model'); return { /** @@ -146,7 +146,7 @@ export class Contact extends BaseModel { */ salesInvoices: { relation: Model.HasManyRelation, - modelClass: SaleInvoice.default, + modelClass: SaleInvoice, join: { from: 'contacts.id', to: 'sales_invoices.customerId', @@ -158,7 +158,7 @@ export class Contact extends BaseModel { */ salesEstimates: { relation: Model.HasManyRelation, - modelClass: SaleEstimate.default, + modelClass: SaleEstimate, join: { from: 'contacts.id', to: 'sales_estimates.customerId', @@ -170,7 +170,7 @@ export class Contact extends BaseModel { */ salesReceipts: { relation: Model.HasManyRelation, - modelClass: SaleReceipt.default, + modelClass: SaleReceipt, join: { from: 'contacts.id', to: 'sales_receipts.customerId', @@ -182,7 +182,7 @@ export class Contact extends BaseModel { */ paymentReceives: { relation: Model.HasManyRelation, - modelClass: PaymentReceive.default, + modelClass: PaymentReceived, join: { from: 'contacts.id', to: 'payment_receives.customerId', @@ -194,7 +194,7 @@ export class Contact extends BaseModel { */ bills: { relation: Model.HasManyRelation, - modelClass: Bill.default, + modelClass: Bill, join: { from: 'contacts.id', to: 'bills.vendorId', @@ -206,7 +206,7 @@ export class Contact extends BaseModel { */ billPayments: { relation: Model.HasManyRelation, - modelClass: BillPayment.default, + modelClass: BillPayment, join: { from: 'contacts.id', to: 'bills_payments.vendorId', @@ -218,7 +218,7 @@ export class Contact extends BaseModel { */ accountsTransactions: { relation: Model.HasManyRelation, - modelClass: AccountTransaction.default, + modelClass: AccountTransaction, join: { from: 'contacts.id', to: 'accounts_transactions.contactId', diff --git a/packages/server-nest/src/modules/Expenses/commands/CommandExpenseValidator.service.ts b/packages/server-nest/src/modules/Expenses/commands/CommandExpenseValidator.service.ts index 20367d6b4..8710f7ab0 100644 --- a/packages/server-nest/src/modules/Expenses/commands/CommandExpenseValidator.service.ts +++ b/packages/server-nest/src/modules/Expenses/commands/CommandExpenseValidator.service.ts @@ -30,9 +30,9 @@ export class CommandExpenseValidator { /** * Retrieve expense accounts or throw error in case one of the given accounts * not found not the storage. - * @param {number} tenantId - * @param {number} expenseAccountsIds - * @throws {ServiceError} + * @param {Array} tenantId + * @param {number} expenseAccountsIds + * @throws {ServiceError} * @returns {Promise} */ public validateExpensesAccountsExistance( diff --git a/packages/server-nest/src/modules/Import/ImportableResources.ts b/packages/server-nest/src/modules/Import/ImportableResources.ts index f79ceb36f..d4170c843 100644 --- a/packages/server-nest/src/modules/Import/ImportableResources.ts +++ b/packages/server-nest/src/modules/Import/ImportableResources.ts @@ -6,7 +6,7 @@ import { CustomersImportable } from '../Contacts/Customers/CustomersImportable'; import { VendorsImportable } from '../Contacts/Vendors/VendorsImportable'; import { ItemsImportable } from '../Items/ItemsImportable'; import { ItemCategoriesImportable } from '../ItemCategories/ItemCategoriesImportable'; -import { ManualJournalImportable } from '../ManualJournals/ManualJournalsImport'; +import { ManualJournalImportable } from '../ManualJournals/commands/ManualJournalsImport'; import { BillsImportable } from '../Purchases/Bills/BillsImportable'; import { ExpensesImportable } from '../Expenses/ExpensesImportable'; import { SaleInvoicesImportable } from '../Sales/Invoices/SaleInvoicesImportable'; diff --git a/packages/server-nest/src/modules/ManualJournals/ManualJournals.controller.ts b/packages/server-nest/src/modules/ManualJournals/ManualJournals.controller.ts new file mode 100644 index 000000000..28b6342b6 --- /dev/null +++ b/packages/server-nest/src/modules/ManualJournals/ManualJournals.controller.ts @@ -0,0 +1,49 @@ +import { + Body, + Controller, + Delete, + Get, + Param, + Post, + Put, +} from '@nestjs/common'; +import { ManualJournalsApplication } from './ManualJournalsApplication.service'; +import { IManualJournalDTO } from './types/ManualJournals.types'; +import { PublicRoute } from '../Auth/Jwt.guard'; + +@Controller('manual-journals') +@PublicRoute() +export class ManualJournalsController { + constructor(private manualJournalsApplication: ManualJournalsApplication) {} + + @Post() + public createManualJournal(@Body() manualJournalDTO: IManualJournalDTO) { + return this.manualJournalsApplication.createManualJournal(manualJournalDTO); + } + + @Put(':id') + public editManualJournal( + @Param('id') manualJournalId: number, + @Body() manualJournalDTO: IManualJournalDTO, + ) { + return this.manualJournalsApplication.editManualJournal( + manualJournalId, + manualJournalDTO, + ); + } + + @Delete(':id') + public deleteManualJournal(@Param('id') manualJournalId: number) { + return this.manualJournalsApplication.deleteManualJournal(manualJournalId); + } + + @Post(':id/publish') + public publishManualJournal(@Param('id') manualJournalId: number) { + return this.manualJournalsApplication.publishManualJournal(manualJournalId); + } + + @Get(':id') + public getManualJournal(@Param('id') manualJournalId: number) { + return this.manualJournalsApplication.getManualJournal(manualJournalId); + } +} diff --git a/packages/server-nest/src/modules/ManualJournals/ManualJournals.module.ts b/packages/server-nest/src/modules/ManualJournals/ManualJournals.module.ts new file mode 100644 index 000000000..b094fdfe2 --- /dev/null +++ b/packages/server-nest/src/modules/ManualJournals/ManualJournals.module.ts @@ -0,0 +1,34 @@ +import { Module } from '@nestjs/common'; +import { CreateManualJournalService } from './commands/CreateManualJournal.service'; +import { EditManualJournal } from './commands/EditManualJournal.service'; +import { DeleteManualJournalService } from './commands/DeleteManualJournal.service'; +import { PublishManualJournal } from './commands/PublishManualJournal.service'; +import { CommandManualJournalValidators } from './commands/CommandManualJournalValidators.service'; +import { AutoIncrementManualJournal } from './commands/AutoIncrementManualJournal.service'; +import { ManualJournalBranchesDTOTransformer } from '../Branches/integrations/ManualJournals/ManualJournalDTOTransformer.service'; +import { TenancyContext } from '../Tenancy/TenancyContext.service'; +import { AutoIncrementOrdersService } from '../AutoIncrementOrders/AutoIncrementOrders.service'; +import { BranchesModule } from '../Branches/Branches.module'; +import { ManualJournalsController } from './ManualJournals.controller'; +import { ManualJournalsApplication } from './ManualJournalsApplication.service'; +import { GetManualJournal } from './queries/GetManualJournal.service'; + +@Module({ + imports: [BranchesModule], + controllers: [ManualJournalsController], + providers: [ + TenancyContext, + CreateManualJournalService, + EditManualJournal, + DeleteManualJournalService, + PublishManualJournal, + CommandManualJournalValidators, + AutoIncrementManualJournal, + CommandManualJournalValidators, + ManualJournalBranchesDTOTransformer, + AutoIncrementOrdersService, + ManualJournalsApplication, + GetManualJournal + ], +}) +export class ManualJournalsModule {} diff --git a/packages/server-nest/src/modules/ManualJournals/ManualJournalsApplication.service.ts b/packages/server-nest/src/modules/ManualJournals/ManualJournalsApplication.service.ts new file mode 100644 index 000000000..a727cc329 --- /dev/null +++ b/packages/server-nest/src/modules/ManualJournals/ManualJournalsApplication.service.ts @@ -0,0 +1,89 @@ +import { Injectable } from '@nestjs/common'; +import { CreateManualJournalService } from './commands/CreateManualJournal.service'; +import { EditManualJournal } from './commands/EditManualJournal.service'; +import { PublishManualJournal } from './commands/PublishManualJournal.service'; +import { GetManualJournal } from './queries/GetManualJournal.service'; +import { DeleteManualJournalService } from './commands/DeleteManualJournal.service'; +import { IManualJournalDTO, } from './types/ManualJournals.types'; +// import { GetManualJournals } from './queries/GetManualJournals'; + +@Injectable() +export class ManualJournalsApplication { + constructor( + private createManualJournalService: CreateManualJournalService, + private editManualJournalService: EditManualJournal, + private deleteManualJournalService: DeleteManualJournalService, + private publishManualJournalService: PublishManualJournal, + private getManualJournalService: GetManualJournal, + // private getManualJournalsService: GetManualJournals, + ) {} + + /** + * Make journal entries. + * @param {number} tenantId + * @param {IManualJournalDTO} manualJournalDTO + * @returns {Promise} + */ + public createManualJournal = (manualJournalDTO: IManualJournalDTO) => { + return this.createManualJournalService.makeJournalEntries(manualJournalDTO); + }; + + /** + * Edits jouranl entries. + * @param {number} manualJournalId + * @param {IMakeJournalDTO} manualJournalDTO + */ + public editManualJournal = ( + manualJournalId: number, + manualJournalDTO: IManualJournalDTO, + ) => { + return this.editManualJournalService.editJournalEntries( + manualJournalId, + manualJournalDTO, + ); + }; + + /** + * Deletes the given manual journal + * @param {number} manualJournalId + * @return {Promise} + */ + public deleteManualJournal = (manualJournalId: number) => { + return this.deleteManualJournalService.deleteManualJournal( + manualJournalId, + ); + }; + + /** + * Publish the given manual journal. + * @param {number} manualJournalId - Manual journal id. + */ + public publishManualJournal = (manualJournalId: number) => { + return this.publishManualJournalService.publishManualJournal( + manualJournalId, + ); + }; + + /** + * Retrieves the specific manual journal. + * @param {number} manualJournalId + * @returns + */ + public getManualJournal = (manualJournalId: number) => { + return this.getManualJournalService.getManualJournal( + manualJournalId, + ); + }; + + /** + * Retrieves the paginated manual journals. + * @param {number} tenantId + * @param {IManualJournalsFilter} filterDTO + * @returns + */ + // public getManualJournals = ( + // filterDTO: IManualJournalsFilter, + // ) => { + // // return this.getManualJournalsService.getManualJournals(filterDTO); + // }; +} diff --git a/packages/server-nest/src/modules/ManualJournals/commands/AutoIncrementManualJournal.service.ts b/packages/server-nest/src/modules/ManualJournals/commands/AutoIncrementManualJournal.service.ts new file mode 100644 index 000000000..d0a104c77 --- /dev/null +++ b/packages/server-nest/src/modules/ManualJournals/commands/AutoIncrementManualJournal.service.ts @@ -0,0 +1,42 @@ +import { Injectable } from '@nestjs/common'; +import { AutoIncrementOrdersService } from '@/modules/AutoIncrementOrders/AutoIncrementOrders.service'; + +@Injectable() +export class AutoIncrementManualJournal { + /** + * + * @param autoIncrementOrdersService + */ + constructor( + private readonly autoIncrementOrdersService: AutoIncrementOrdersService + ) {} + + /** + * + * @returns {boolean} + */ + public autoIncrementEnabled = () => { + return this.autoIncrementOrdersService.autoIncrementEnabled( + 'manual_journals' + ); + }; + + /** + * Retrieve the next journal number. + */ + public getNextJournalNumber = (): string => { + return this.autoIncrementOrdersService.getNextTransactionNumber( + 'manual_journals' + ); + }; + + /** + * Increment the manual journal number. + * @param {number} tenantId + */ + public incrementNextJournalNumber = () => { + return this.autoIncrementOrdersService.incrementSettingsNextNumber( + 'manual_journals' + ); + }; +} diff --git a/packages/server-nest/src/modules/ManualJournals/commands/CommandManualJournalValidators.service.ts b/packages/server-nest/src/modules/ManualJournals/commands/CommandManualJournalValidators.service.ts new file mode 100644 index 000000000..eda77d709 --- /dev/null +++ b/packages/server-nest/src/modules/ManualJournals/commands/CommandManualJournalValidators.service.ts @@ -0,0 +1,295 @@ +import { Inject, Injectable } from '@nestjs/common'; +import { difference, isEmpty, round, sumBy } from 'lodash'; +import { + IManualJournalDTO, + IManualJournalEntryDTO, +} from '../types/ManualJournals.types'; +import { ERRORS } from '../constants'; +import { ServiceError } from '@/modules/Items/ServiceError'; +import { Account } from '@/modules/Accounts/models/Account.model'; +import { ManualJournal } from '../models/ManualJournal'; +import { Contact } from '@/modules/Contacts/models/Contact'; + +@Injectable() +export class CommandManualJournalValidators { + constructor( + @Inject(Account.name) + private readonly accountModel: typeof Account, + + @Inject(ManualJournal.name) + private readonly manualJournalModel: typeof ManualJournal, + + @Inject(Contact.name) + private readonly contactModel: typeof Contact, + ) {} + + /** + * Validate manual journal credit and debit should be equal. + * @param {IManualJournalDTO} manualJournalDTO + */ + public valdiateCreditDebitTotalEquals(manualJournalDTO: IManualJournalDTO) { + const totalCredit = round( + sumBy(manualJournalDTO.entries, (entry) => entry.credit || 0), + 2, + ); + const totalDebit = round( + sumBy(manualJournalDTO.entries, (entry) => entry.debit || 0), + 2, + ); + if (totalCredit <= 0 || totalDebit <= 0) { + throw new ServiceError(ERRORS.CREDIT_DEBIT_NOT_EQUAL_ZERO); + } + if (totalCredit !== totalDebit) { + throw new ServiceError(ERRORS.CREDIT_DEBIT_NOT_EQUAL); + } + } + + /** + * Validate manual entries accounts existance on the storage. + * @param {number} tenantId - + * @param {IManualJournalDTO} manualJournalDTO - + */ + public async validateAccountsExistance(manualJournalDTO: IManualJournalDTO) { + const manualAccountsIds = manualJournalDTO.entries.map((e) => e.accountId); + const accounts = await this.accountModel + .query() + .whereIn('id', manualAccountsIds); + + const storedAccountsIds = accounts.map((account) => account.id); + + if (difference(manualAccountsIds, storedAccountsIds).length > 0) { + throw new ServiceError(ERRORS.ACCOUNTS_IDS_NOT_FOUND); + } + } + + /** + * Validate manual journal number unique. + * @param {number} tenantId + * @param {IManualJournalDTO} manualJournalDTO + */ + public async validateManualJournalNoUnique( + journalNumber: string, + notId?: number, + ) { + const journals = await this.manualJournalModel + .query() + .where('journal_number', journalNumber) + .onBuild((builder) => { + if (notId) { + builder.whereNot('id', notId); + } + }); + if (journals.length > 0) { + throw new ServiceError( + ERRORS.JOURNAL_NUMBER_EXISTS, + 'The journal number is already exist.', + ); + } + } + + /** + * Validate accounts with contact type. + * @param {number} tenantId + * @param {IManualJournalDTO} manualJournalDTO + * @param {string} accountBySlug + * @param {string} contactType + */ + public async validateAccountWithContactType( + entriesDTO: IManualJournalEntryDTO[], + accountBySlug: string, + contactType: string, + ): Promise { + // Retrieve account meta by the given account slug. + const account = await this.accountModel + .query() + .findOne('slug', accountBySlug); + + // Retrieve all stored contacts on the storage from contacts entries. + const storedContacts = await this.contactModel.query().whereIn( + 'id', + entriesDTO + .filter((entry) => entry.contactId) + .map((entry) => entry.contactId), + ); + // Converts the stored contacts to map with id as key and entry as value. + const storedContactsMap = new Map( + storedContacts.map((contact) => [contact.id, contact]), + ); + + // Filter all entries of the given account. + const accountEntries = entriesDTO.filter( + (entry) => entry.accountId === account.id, + ); + // Can't continue if there is no entry that associate to the given account. + if (accountEntries.length === 0) { + return; + } + // Filter entries that have no contact type or not equal the valid type. + const entriesNoContact = accountEntries.filter((entry) => { + const contact = storedContactsMap.get(entry.contactId); + return !contact || contact.contactService !== contactType; + }); + // Throw error in case one of entries that has invalid contact type. + if (entriesNoContact.length > 0) { + const indexes = entriesNoContact.map((e) => e.index); + + return new ServiceError(ERRORS.ENTRIES_SHOULD_ASSIGN_WITH_CONTACT, '', { + accountSlug: accountBySlug, + contactType, + indexes, + }); + } + } + + /** + * Dynamic validates accounts with contacts. + * @param {number} tenantId + * @param {IManualJournalDTO} manualJournalDTO + */ + public async dynamicValidateAccountsWithContactType( + entriesDTO: IManualJournalEntryDTO[], + ): Promise { + return Promise.all([ + this.validateAccountWithContactType( + entriesDTO, + 'accounts-receivable', + 'customer', + ), + this.validateAccountWithContactType( + entriesDTO, + 'accounts-payable', + 'vendor', + ), + ]).then((results) => { + const metadataErrors = results + .filter((result) => result instanceof ServiceError) + .map((result: ServiceError) => result.payload); + + if (metadataErrors.length > 0) { + throw new ServiceError( + ERRORS.ENTRIES_SHOULD_ASSIGN_WITH_CONTACT, + '', + metadataErrors, + ); + } + + return results; + }); + } + + /** + * Validate entries contacts existance. + * @param {number} tenantId - + * @param {IManualJournalDTO} manualJournalDTO + */ + public async validateContactsExistance(manualJournalDTO: IManualJournalDTO) { + // Filters the entries that have contact only. + const entriesContactPairs = manualJournalDTO.entries.filter( + (entry) => entry.contactId, + ); + + if (entriesContactPairs.length > 0) { + const entriesContactsIds = entriesContactPairs.map( + (entry) => entry.contactId, + ); + // Retrieve all stored contacts on the storage from contacts entries. + const storedContacts = await this.contactModel + .query() + .whereIn('id', entriesContactsIds); + + // Converts the stored contacts to map with id as key and entry as value. + const storedContactsMap = new Map( + storedContacts.map((contact) => [contact.id, contact]), + ); + const notFoundContactsIds = []; + + entriesContactPairs.forEach((contactEntry) => { + const storedContact = storedContactsMap.get(contactEntry.contactId); + + // in case the contact id not found. + if (!storedContact) { + notFoundContactsIds.push(storedContact); + } + }); + if (notFoundContactsIds.length > 0) { + throw new ServiceError(ERRORS.CONTACTS_NOT_FOUND, '', { + contactsIds: notFoundContactsIds, + }); + } + } + } + + /** + * Validates expenses is not already published before. + * @param {ManualJournal} manualJournal + */ + public validateManualJournalIsNotPublished(manualJournal: ManualJournal) { + if (manualJournal.publishedAt) { + throw new ServiceError(ERRORS.MANUAL_JOURNAL_ALREADY_PUBLISHED); + } + } + + /** + * Validates the manual journal number require. + * @param {string} journalNumber + * @throws {ServiceError(ERRORS.MANUAL_JOURNAL_NO_REQUIRED)} + */ + public validateJournalNoRequireWhenAutoNotEnabled = ( + journalNumber: string, + ) => { + if (isEmpty(journalNumber)) { + throw new ServiceError(ERRORS.MANUAL_JOURNAL_NO_REQUIRED); + } + }; + + /** + * Filters the not published manual jorunals. + * @param {IManualJournal[]} manualJournal - Manual journal. + * @return {IManualJournal[]} + */ + public getNonePublishedManualJournals( + manualJournals: ManualJournal[], + ): ManualJournal[] { + return manualJournals.filter((manualJournal) => !manualJournal.publishedAt); + } + + /** + * Filters the published manual journals. + * @param {IManualJournal[]} manualJournal - Manual journal. + * @return {IManualJournal[]} + */ + public getPublishedManualJournals( + manualJournals: ManualJournal[], + ): ManualJournal[] { + return manualJournals.filter((expense) => expense.publishedAt); + } + + /** + * + * @param {number} tenantId + * @param {IManualJournalDTO} manualJournalDTO + */ + public validateJournalCurrencyWithAccountsCurrency = async ( + manualJournalDTO: IManualJournalDTO, + baseCurrency: string, + ) => { + const accountsIds = manualJournalDTO.entries.map((e) => e.accountId); + const accounts = await this.accountModel.query().whereIn('id', accountsIds); + + // Filters the accounts that has no base currency or DTO currency. + const notSupportedCurrency = accounts.filter((account) => { + if ( + account.currencyCode === baseCurrency || + account.currencyCode === manualJournalDTO.currencyCode + ) { + return false; + } + return true; + }); + if (notSupportedCurrency.length > 0) { + throw new ServiceError( + ERRORS.COULD_NOT_ASSIGN_DIFFERENT_CURRENCY_TO_ACCOUNTS, + ); + } + }; +} diff --git a/packages/server-nest/src/modules/ManualJournals/commands/CreateManualJournal.service.ts b/packages/server-nest/src/modules/ManualJournals/commands/CreateManualJournal.service.ts new file mode 100644 index 000000000..00636f58b --- /dev/null +++ b/packages/server-nest/src/modules/ManualJournals/commands/CreateManualJournal.service.ts @@ -0,0 +1,159 @@ +import { sumBy, omit } from 'lodash'; +import * as moment from 'moment'; +import * as R from 'ramda'; +import { Knex } from 'knex'; +import { Inject, Injectable } from '@nestjs/common'; +import { EventEmitter2 } from '@nestjs/event-emitter'; +import { + IManualJournalDTO, + IManualJournalEventCreatedPayload, + IManualJournalCreatingPayload, +} from '../types/ManualJournals.types'; +import { CommandManualJournalValidators } from './CommandManualJournalValidators.service'; +import { AutoIncrementManualJournal } from './AutoIncrementManualJournal.service'; +import { UnitOfWork } from '@/modules/Tenancy/TenancyDB/UnitOfWork.service'; +import { events } from '@/common/events/events'; +import { ManualJournal } from '../models/ManualJournal'; +import { assocItemEntriesDefaultIndex } from '@/utils/associate-item-entries-index'; +import { TenancyContext } from '@/modules/Tenancy/TenancyContext.service'; +import { ManualJournalBranchesDTOTransformer } from '@/modules/Branches/integrations/ManualJournals/ManualJournalDTOTransformer.service'; + +@Injectable() +export class CreateManualJournalService { + constructor( + private tenancyContext: TenancyContext, + private eventPublisher: EventEmitter2, + private uow: UnitOfWork, + private validator: CommandManualJournalValidators, + private autoIncrement: AutoIncrementManualJournal, + private branchesDTOTransformer: ManualJournalBranchesDTOTransformer, + + @Inject(ManualJournal.name) + private manualJournalModel: typeof ManualJournal, + ) {} + + /** + * Transform the new manual journal DTO to upsert graph operation. + * @param {IManualJournalDTO} manualJournalDTO - Manual jorunal DTO. + * @returns {Promise} + */ + private async transformNewDTOToModel( + manualJournalDTO: IManualJournalDTO, + ): Promise { + const amount = sumBy(manualJournalDTO.entries, 'credit') || 0; + const date = moment(manualJournalDTO.date).format('YYYY-MM-DD'); + + // Retrieve the next manual journal number. + const autoNextNumber = this.autoIncrement.getNextJournalNumber(); + + // The manual or auto-increment journal number. + const journalNumber = manualJournalDTO.journalNumber || autoNextNumber; + + const tenant = await this.tenancyContext.getTenant(true); + const authorizedUser = await this.tenancyContext.getSystemUser(); + + const entries = R.compose( + // Associate the default index to each item entry. + assocItemEntriesDefaultIndex, + )(manualJournalDTO.entries); + + const initialDTO = { + ...omit(manualJournalDTO, ['publish', 'attachments']), + ...(manualJournalDTO.publish + ? { publishedAt: moment().toMySqlDateTime() } + : {}), + amount, + date, + currencyCode: + manualJournalDTO.currencyCode || tenant?.metadata?.baseCurrency, + exchangeRate: manualJournalDTO.exchangeRate || 1, + journalNumber, + entries, + userId: authorizedUser.id, + }; + return R.compose( + // Omits the `branchId` from entries if multiply branches feature not active. + this.branchesDTOTransformer.transformDTO, + )(initialDTO) as ManualJournal; + } + + /** + * Authorize the manual journal creating. + * @param {number} tenantId + * @param {IManualJournalDTO} manualJournalDTO + * @param {ISystemUser} authorizedUser + */ + private authorize = async (manualJournalDTO: IManualJournalDTO) => { + const tenant = await this.tenancyContext.getTenant(true); + + // Validate the total credit should equals debit. + this.validator.valdiateCreditDebitTotalEquals(manualJournalDTO); + + // Validate the contacts existance. + await this.validator.validateContactsExistance(manualJournalDTO); + + // Validate entries accounts existance. + await this.validator.validateAccountsExistance(manualJournalDTO); + + // Validate manual journal number require when auto-increment not enabled. + this.validator.validateJournalNoRequireWhenAutoNotEnabled( + manualJournalDTO.journalNumber, + ); + // Validate manual journal uniquiness on the storage. + if (manualJournalDTO.journalNumber) { + await this.validator.validateManualJournalNoUnique( + manualJournalDTO.journalNumber, + ); + } + // Validate accounts with contact type from the given config. + await this.validator.dynamicValidateAccountsWithContactType( + manualJournalDTO.entries, + ) + // Validates the accounts currency with journal currency. + await this.validator.validateJournalCurrencyWithAccountsCurrency( + manualJournalDTO, + tenant.metadata.baseCurrency, + ); + }; + + /** + * Make journal entries. + * @param {number} tenantId + * @param {IManualJournalDTO} manualJournalDTO + * @param {ISystemUser} authorizedUser + */ + public makeJournalEntries = async ( + manualJournalDTO: IManualJournalDTO, + trx?: Knex.Transaction, + ): Promise => { + // Authorize manual journal creating. + await this.authorize(manualJournalDTO); + // Transformes the next DTO to model. + const manualJournalObj = await this.transformNewDTOToModel(manualJournalDTO); + + // Creates a manual journal transactions with associated transactions + // under unit-of-work envirement. + return this.uow.withTransaction(async (trx: Knex.Transaction) => { + // Triggers `onManualJournalCreating` event. + await this.eventPublisher.emitAsync(events.manualJournals.onCreating, { + manualJournalDTO, + trx, + } as IManualJournalCreatingPayload); + + // Upsert the manual journal object. + const manualJournal = await this.manualJournalModel + .query(trx) + .upsertGraph({ + ...manualJournalObj, + }); + // Triggers `onManualJournalCreated` event. + await this.eventPublisher.emitAsync(events.manualJournals.onCreated, { + manualJournal, + manualJournalDTO, + trx, + } as IManualJournalEventCreatedPayload); + + return manualJournal; + }, trx); + }; +} diff --git a/packages/server-nest/src/modules/ManualJournals/commands/DeleteManualJournal.service.ts b/packages/server-nest/src/modules/ManualJournals/commands/DeleteManualJournal.service.ts new file mode 100644 index 000000000..f5aa6a1fa --- /dev/null +++ b/packages/server-nest/src/modules/ManualJournals/commands/DeleteManualJournal.service.ts @@ -0,0 +1,71 @@ +import { Inject, Injectable } from '@nestjs/common'; +import { Knex } from 'knex'; +import { EventEmitter2 } from '@nestjs/event-emitter'; +import { + IManualJournalEventDeletedPayload, + IManualJournalDeletingPayload, +} from '../types/ManualJournals.types'; +import { UnitOfWork } from '@/modules/Tenancy/TenancyDB/UnitOfWork.service'; +import { ManualJournal } from '../models/ManualJournal'; +import { ManualJournalEntry } from '../models/ManualJournalEntry'; +import { events } from '@/common/events/events'; + +@Injectable() +export class DeleteManualJournalService { + constructor( + private readonly eventPublisher: EventEmitter2, + private readonly uow: UnitOfWork, + + @Inject(ManualJournal.name) + private readonly manualJournalModel: typeof ManualJournal, + + @Inject(ManualJournalEntry.name) + private readonly manualJournalEntryModel: typeof ManualJournalEntry, + ) {} + + /** + * Deletes the given manual journal + * @param {number} manualJournalId + * @return {Promise} + */ + public deleteManualJournal = async ( + manualJournalId: number, + ): Promise<{ + oldManualJournal: ManualJournal; + }> => { + // Validate the manual journal exists on the storage. + const oldManualJournal = await this.manualJournalModel.query() + .findById(manualJournalId) + .throwIfNotFound(); + + // Deletes the manual journal with associated transactions under unit-of-work envirement. + return this.uow.withTransaction(async (trx: Knex.Transaction) => { + // Triggers `onManualJournalDeleting` event. + await this.eventPublisher.emitAsync(events.manualJournals.onDeleting, { + oldManualJournal, + trx, + } as IManualJournalDeletingPayload); + + // Deletes the manual journal entries. + await this.manualJournalEntryModel + .query(trx) + .where('manualJournalId', manualJournalId) + .delete(); + + // Deletes the manual journal transaction. + await this.manualJournalModel + .query(trx) + .findById(manualJournalId) + .delete(); + + // Triggers `onManualJournalDeleted` event. + await this.eventPublisher.emitAsync(events.manualJournals.onDeleted, { + manualJournalId, + oldManualJournal, + trx, + } as IManualJournalEventDeletedPayload); + + return { oldManualJournal }; + }); + }; +} diff --git a/packages/server-nest/src/modules/ManualJournals/commands/EditManualJournal.service.ts b/packages/server-nest/src/modules/ManualJournals/commands/EditManualJournal.service.ts new file mode 100644 index 000000000..cb34d55ca --- /dev/null +++ b/packages/server-nest/src/modules/ManualJournals/commands/EditManualJournal.service.ts @@ -0,0 +1,139 @@ +import { Knex } from 'knex'; +import { omit, sumBy } from 'lodash'; +import * as moment from 'moment'; +import { Inject, Injectable } from '@nestjs/common'; +import { + IManualJournalDTO, + IManualJournalEventEditedPayload, + IManualJournalEditingPayload, +} from '../types/ManualJournals.types'; +import { EventEmitter2 } from '@nestjs/event-emitter'; +import { UnitOfWork } from '@/modules/Tenancy/TenancyDB/UnitOfWork.service'; +import { CommandManualJournalValidators } from './CommandManualJournalValidators.service'; +import { events } from '@/common/events/events'; +import { ManualJournal } from '../models/ManualJournal'; + +@Injectable() +export class EditManualJournal { + constructor( + private eventPublisher: EventEmitter2, + private uow: UnitOfWork, + private validator: CommandManualJournalValidators, + + @Inject(ManualJournal.name) + private manualJournalModel: typeof ManualJournal, + ) {} + + /** + * Authorize the manual journal editing. + * @param {number} tenantId + * @param {number} manualJournalId + * @param {IManualJournalDTO} manualJournalDTO + */ + private authorize = async ( + manualJournalId: number, + manualJournalDTO: IManualJournalDTO, + ) => { + // Validates the total credit and debit to be equals. + this.validator.valdiateCreditDebitTotalEquals(manualJournalDTO); + + // Validate the contacts existance. + await this.validator.validateContactsExistance(manualJournalDTO); + + // Validates entries accounts existance. + await this.validator.validateAccountsExistance(manualJournalDTO); + + // Validates the manual journal number uniquiness. + if (manualJournalDTO.journalNumber) { + await this.validator.validateManualJournalNoUnique( + manualJournalDTO.journalNumber, + manualJournalId, + ); + } + // Validate accounts with contact type from the given config. + await this.validator.dynamicValidateAccountsWithContactType( + manualJournalDTO.entries, + ); + }; + + /** + * Transform the edit manual journal DTO to upsert graph operation. + * @param {IManualJournalDTO} manualJournalDTO - Manual jorunal DTO. + * @param {IManualJournal} oldManualJournal + */ + private transformEditDTOToModel = ( + manualJournalDTO: IManualJournalDTO, + oldManualJournal: ManualJournal, + ) => { + const amount = sumBy(manualJournalDTO.entries, 'credit') || 0; + const date = moment(manualJournalDTO.date).format('YYYY-MM-DD'); + + return { + id: oldManualJournal.id, + ...omit(manualJournalDTO, ['publish', 'attachments']), + ...(manualJournalDTO.publish && !oldManualJournal.publishedAt + ? { publishedAt: moment().toMySqlDateTime() } + : {}), + amount, + date, + }; + }; + + /** + * Edits jouranl entries. + * @param {number} tenantId + * @param {number} manualJournalId + * @param {IMakeJournalDTO} manualJournalDTO + * @param {ISystemUser} authorizedUser + */ + public async editJournalEntries( + manualJournalId: number, + manualJournalDTO: IManualJournalDTO, + ): Promise<{ + manualJournal: ManualJournal; + oldManualJournal: ManualJournal; + }> { + // Validates the manual journal existance on the storage. + const oldManualJournal = await ManualJournal.query() + .findById(manualJournalId) + .throwIfNotFound(); + + // Authorize manual journal editing. + await this.authorize(manualJournalId, manualJournalDTO); + + // Transform manual journal DTO to model. + const manualJournalObj = this.transformEditDTOToModel( + manualJournalDTO, + oldManualJournal, + ); + // Edits the manual journal transactions with associated transactions + // under unit-of-work envirement. + return this.uow.withTransaction(async (trx: Knex.Transaction) => { + // Triggers `onManualJournalEditing` event. + await this.eventPublisher.emitAsync(events.manualJournals.onEditing, { + manualJournalDTO, + oldManualJournal, + trx, + } as IManualJournalEditingPayload); + + // Upserts the manual journal graph to the storage. + await this.manualJournalModel.query(trx).upsertGraph({ + ...manualJournalObj, + }); + // Retrieve the given manual journal with associated entries after modifications. + const manualJournal = await this.manualJournalModel.query(trx) + .findById(manualJournalId) + .withGraphFetched('entries'); + + // Triggers `onManualJournalEdited` event. + await this.eventPublisher.emitAsync(events.manualJournals.onEdited, { + manualJournal, + oldManualJournal, + manualJournalDTO, + trx, + } as IManualJournalEventEditedPayload); + + return { manualJournal, oldManualJournal }; + }); + } +} diff --git a/packages/server-nest/src/modules/ManualJournals/commands/ManualJournalExportable.ts b/packages/server-nest/src/modules/ManualJournals/commands/ManualJournalExportable.ts new file mode 100644 index 000000000..a91eee5b9 --- /dev/null +++ b/packages/server-nest/src/modules/ManualJournals/commands/ManualJournalExportable.ts @@ -0,0 +1,30 @@ +// import { Inject, Service } from 'typedi'; +// import { IManualJournalsFilter } from '@/interfaces'; +// import { Exportable } from '../../Export/Exportable'; +// import { ManualJournalsApplication } from '../ManualJournalsApplication'; +// import { EXPORT_SIZE_LIMIT } from '../../Export/constants'; + +// @Service() +// export class ManualJournalsExportable extends Exportable { +// @Inject() +// private manualJournalsApplication: ManualJournalsApplication; + +// /** +// * Retrieves the manual journals data to exportable sheet. +// * @param {number} tenantId +// * @returns +// */ +// public exportable(tenantId: number, query: IManualJournalsFilter) { +// const parsedQuery = { +// sortOrder: 'desc', +// columnSortBy: 'created_at', +// ...query, +// page: 1, +// pageSize: EXPORT_SIZE_LIMIT, +// } as IManualJournalsFilter; + +// return this.manualJournalsApplication +// .getManualJournals(tenantId, parsedQuery) +// .then((output) => output.manualJournals); +// } +// } diff --git a/packages/server-nest/src/modules/ManualJournals/commands/ManualJournalGLEntries.ts b/packages/server-nest/src/modules/ManualJournals/commands/ManualJournalGLEntries.ts new file mode 100644 index 000000000..38f33e1cf --- /dev/null +++ b/packages/server-nest/src/modules/ManualJournals/commands/ManualJournalGLEntries.ts @@ -0,0 +1,161 @@ +// import { Service, Inject } from 'typedi'; +// import * as R from 'ramda'; +// import { +// IManualJournal, +// IManualJournalEntry, +// ILedgerEntry, +// } from '@/interfaces'; +// import { Knex } from 'knex'; +// import Ledger from '@/services/Accounting/Ledger'; +// import LedgerStorageService from '@/services/Accounting/LedgerStorageService'; +// import HasTenancyService from '@/services/Tenancy/TenancyService'; + +// @Service() +// export class ManualJournalGLEntries { +// @Inject() +// private ledgerStorage: LedgerStorageService; + +// @Inject() +// private tenancy: HasTenancyService; + +// /** +// * Create manual journal GL entries. +// * @param {number} tenantId +// * @param {number} manualJournalId +// * @param {Knex.Transaction} trx +// */ +// public createManualJournalGLEntries = async ( +// tenantId: number, +// manualJournalId: number, +// trx?: Knex.Transaction +// ) => { +// const { ManualJournal } = this.tenancy.models(tenantId); + +// // Retrieves the given manual journal with associated entries. +// const manualJournal = await ManualJournal.query(trx) +// .findById(manualJournalId) +// .withGraphFetched('entries.account'); + +// // Retrieves the ledger entries of the given manual journal. +// const ledger = this.getManualJournalGLedger(manualJournal); + +// // Commits the given ledger on the storage. +// await this.ledgerStorage.commit(tenantId, ledger, trx); +// }; + +// /** +// * Edits manual journal GL entries. +// * @param {number} tenantId +// * @param {number} manualJournalId +// * @param {Knex.Transaction} trx +// */ +// public editManualJournalGLEntries = async ( +// tenantId: number, +// manualJournalId: number, +// trx?: Knex.Transaction +// ) => { +// // Reverts the manual journal GL entries. +// await this.revertManualJournalGLEntries(tenantId, manualJournalId, trx); + +// // Write the manual journal GL entries. +// await this.createManualJournalGLEntries(tenantId, manualJournalId, trx); +// }; + +// /** +// * Deletes the manual journal GL entries. +// * @param {number} tenantId +// * @param {number} manualJournalId +// * @param {Knex.Transaction} trx +// */ +// public revertManualJournalGLEntries = async ( +// tenantId: number, +// manualJournalId: number, +// trx?: Knex.Transaction +// ): Promise => { +// return this.ledgerStorage.deleteByReference( +// tenantId, +// manualJournalId, +// 'Journal', +// trx +// ); +// }; + +// /** +// * Retrieves the ledger of the given manual journal. +// * @param {IManualJournal} manualJournal +// * @returns {Ledger} +// */ +// private getManualJournalGLedger = (manualJournal: IManualJournal) => { +// const entries = this.getManualJournalGLEntries(manualJournal); + +// return new Ledger(entries); +// }; + +// /** +// * Retrieves the common entry details of the manual journal +// * @param {IManualJournal} manualJournal +// * @returns {Partial} +// */ +// private getManualJournalCommonEntry = ( +// manualJournal: IManualJournal +// ): Partial => { +// return { +// transactionNumber: manualJournal.journalNumber, +// referenceNumber: manualJournal.reference, +// createdAt: manualJournal.createdAt, +// date: manualJournal.date, +// currencyCode: manualJournal.currencyCode, +// exchangeRate: manualJournal.exchangeRate, + +// transactionType: 'Journal', +// transactionId: manualJournal.id, + +// userId: manualJournal.userId, +// }; +// }; + +// /** +// * Retrieves the ledger entry of the given manual journal and +// * its associated entry. +// * @param {IManualJournal} manualJournal - +// * @param {IManualJournalEntry} entry - +// * @returns {ILedgerEntry} +// */ +// private getManualJournalEntry = R.curry( +// ( +// manualJournal: IManualJournal, +// entry: IManualJournalEntry +// ): ILedgerEntry => { +// const commonEntry = this.getManualJournalCommonEntry(manualJournal); + +// return { +// ...commonEntry, +// debit: entry.debit, +// credit: entry.credit, +// accountId: entry.accountId, + +// contactId: entry.contactId, +// note: entry.note, + +// index: entry.index, +// accountNormal: entry.account.accountNormal, + +// branchId: entry.branchId, +// projectId: entry.projectId, +// }; +// } +// ); + +// /** +// * Retrieves the ledger of the given manual journal. +// * @param {IManualJournal} manualJournal +// * @returns {ILedgerEntry[]} +// */ +// private getManualJournalGLEntries = ( +// manualJournal: IManualJournal +// ): ILedgerEntry[] => { +// const transformEntry = this.getManualJournalEntry(manualJournal); + +// return manualJournal.entries.map(transformEntry).flat(); +// }; +// } diff --git a/packages/server-nest/src/modules/ManualJournals/commands/ManualJournalGLEntriesSubscriber.ts b/packages/server-nest/src/modules/ManualJournals/commands/ManualJournalGLEntriesSubscriber.ts new file mode 100644 index 000000000..c3acb306b --- /dev/null +++ b/packages/server-nest/src/modules/ManualJournals/commands/ManualJournalGLEntriesSubscriber.ts @@ -0,0 +1,131 @@ +// import { Inject } from 'typedi'; +// import { EventSubscriber } from 'event-dispatch'; +// import { +// IManualJournalEventCreatedPayload, +// IManualJournalEventEditedPayload, +// IManualJournalEventPublishedPayload, +// IManualJournalEventDeletedPayload, +// } from '@/interfaces'; +// import events from '@/subscribers/events'; +// import { ManualJournalGLEntries } from './ManualJournalGLEntries'; +// import { AutoIncrementManualJournal } from './AutoIncrementManualJournal.service'; + +// @EventSubscriber() +// export class ManualJournalWriteGLSubscriber { +// @Inject() +// private manualJournalGLEntries: ManualJournalGLEntries; + +// @Inject() +// private manualJournalAutoIncrement: AutoIncrementManualJournal; + +// /** +// * Attaches events with handlers. +// * @param bus +// */ +// public attach(bus) { +// bus.subscribe( +// events.manualJournals.onCreated, +// this.handleWriteJournalEntriesOnCreated +// ); +// bus.subscribe( +// events.manualJournals.onCreated, +// this.handleJournalNumberIncrement +// ); +// bus.subscribe( +// events.manualJournals.onEdited, +// this.handleRewriteJournalEntriesOnEdited +// ); +// bus.subscribe( +// events.manualJournals.onPublished, +// this.handleWriteJournalEntriesOnPublished +// ); +// bus.subscribe( +// events.manualJournals.onDeleted, +// this.handleRevertJournalEntries +// ); +// } + +// /** +// * Handle manual journal created event. +// * @param {IManualJournalEventCreatedPayload} payload - +// * @returns {Promise} +// */ +// private handleWriteJournalEntriesOnCreated = async ({ +// tenantId, +// manualJournal, +// trx, +// }: IManualJournalEventCreatedPayload) => { +// // Ingore writing manual journal journal entries in case was not published. +// if (!manualJournal.publishedAt) return; + +// await this.manualJournalGLEntries.createManualJournalGLEntries( +// tenantId, +// manualJournal.id, +// trx +// ); +// }; + +// /** +// * Handles the manual journal next number increment once the journal be created. +// * @param {IManualJournalEventCreatedPayload} payload - +// * @return {Promise} +// */ +// private handleJournalNumberIncrement = async ({ +// tenantId, +// }: IManualJournalEventCreatedPayload) => { +// await this.manualJournalAutoIncrement.incrementNextJournalNumber(tenantId); +// }; + +// /** +// * Handle manual journal edited event. +// * @param {IManualJournalEventEditedPayload} +// * @return {Promise} +// */ +// private handleRewriteJournalEntriesOnEdited = async ({ +// tenantId, +// manualJournal, +// oldManualJournal, +// trx, +// }: IManualJournalEventEditedPayload) => { +// if (manualJournal.publishedAt) { +// await this.manualJournalGLEntries.editManualJournalGLEntries( +// tenantId, +// manualJournal.id, +// trx +// ); +// } +// }; + +// /** +// * Handles writing journal entries once the manula journal publish. +// * @param {IManualJournalEventPublishedPayload} payload - +// * @return {Promise} +// */ +// private handleWriteJournalEntriesOnPublished = async ({ +// tenantId, +// manualJournal, +// trx, +// }: IManualJournalEventPublishedPayload) => { +// await this.manualJournalGLEntries.createManualJournalGLEntries( +// tenantId, +// manualJournal.id, +// trx +// ); +// }; + +// /** +// * Handle manual journal deleted event. +// * @param {IManualJournalEventDeletedPayload} payload - +// */ +// private handleRevertJournalEntries = async ({ +// tenantId, +// manualJournalId, +// trx, +// }: IManualJournalEventDeletedPayload) => { +// await this.manualJournalGLEntries.revertManualJournalGLEntries( +// tenantId, +// manualJournalId, +// trx +// ); +// }; +// } diff --git a/packages/server-nest/src/modules/ManualJournals/commands/ManualJournalsImport.ts b/packages/server-nest/src/modules/ManualJournals/commands/ManualJournalsImport.ts new file mode 100644 index 000000000..377c3b2ae --- /dev/null +++ b/packages/server-nest/src/modules/ManualJournals/commands/ManualJournalsImport.ts @@ -0,0 +1,60 @@ +// import { Inject } from 'typedi'; +// import { Knex } from 'knex'; +// import * as Yup from 'yup'; +// import { Importable } from '../../Import/Importable'; +// import { CreateManualJournalService } from './CreateManualJournal.service'; +// import { IManualJournalDTO } from '@/interfaces'; +// import { ImportableContext } from '../../Import/interfaces'; +// import { ManualJournalsSampleData } from '../constants'; + +// export class ManualJournalImportable extends Importable { +// @Inject() +// private createManualJournalService: CreateManualJournalService; + +// /** +// * Importing to account service. +// * @param {number} tenantId +// * @param {IAccountCreateDTO} createAccountDTO +// * @returns +// */ +// public importable( +// tenantId: number, +// createJournalDTO: IManualJournalDTO, +// trx?: Knex.Transaction +// ) { +// return this.createManualJournalService.makeJournalEntries( +// tenantId, +// createJournalDTO, +// {}, +// trx +// ); +// } + +// /** +// * Transformes the DTO before passing it to importable and validation. +// * @param {Record} createDTO +// * @param {ImportableContext} context +// * @returns {Record} +// */ +// public transform(createDTO: Record, context: ImportableContext) { +// return createDTO; +// } + +// /** +// * Params validation schema. +// * @returns {ValidationSchema[]} +// */ +// public paramsValidationSchema() { +// return Yup.object().shape({ +// autoIncrement: Yup.boolean(), +// }); +// } + +// /** +// * Retrieves the sample data of manual journals that used to download sample sheet. +// * @returns {Record} +// */ +// public sampleData(): Record[] { +// return ManualJournalsSampleData; +// } +// } diff --git a/packages/server-nest/src/modules/ManualJournals/commands/PublishManualJournal.service.ts b/packages/server-nest/src/modules/ManualJournals/commands/PublishManualJournal.service.ts new file mode 100644 index 000000000..baa447d53 --- /dev/null +++ b/packages/server-nest/src/modules/ManualJournals/commands/PublishManualJournal.service.ts @@ -0,0 +1,73 @@ +import * as moment from 'moment'; +import { Knex } from 'knex'; +import { Inject, Injectable } from '@nestjs/common'; +import { EventEmitter2 } from '@nestjs/event-emitter'; +import { + IManualJournalEventPublishedPayload, + IManualJournalPublishingPayload, +} from '../types/ManualJournals.types'; +import { CommandManualJournalValidators } from './CommandManualJournalValidators.service'; +import { UnitOfWork } from '@/modules/Tenancy/TenancyDB/UnitOfWork.service'; +import { ManualJournal } from '../models/ManualJournal'; +import { events } from '@/common/events/events'; + +@Injectable() +export class PublishManualJournal { + constructor( + private eventPublisher: EventEmitter2, + private uow: UnitOfWork, + private validator: CommandManualJournalValidators, + + @Inject(ManualJournal.name) + private manualJournalModel: typeof ManualJournal, + ) {} + + /** + * Authorize the manual journal publishing. + * @param {number} manualJournalId - Manual journal id. + */ + private authorize = (oldManualJournal: ManualJournal) => { + // Validate the manual journal is not published. + this.validator.validateManualJournalIsNotPublished(oldManualJournal); + }; + + /** + * Publish the given manual journal. + * @param {number} manualJournalId - Manual journal id. + */ + public async publishManualJournal(manualJournalId: number): Promise { + // Find the old manual journal or throw not found error. + const oldManualJournal = await this.manualJournalModel + .query() + .findById(manualJournalId) + .throwIfNotFound(); + + // Authorize the manual journal publishing. + await this.authorize(oldManualJournal); + + // Publishes the manual journal with associated transactions. + return this.uow.withTransaction(async (trx: Knex.Transaction) => { + // Triggers `onManualJournalPublishing` event. + await this.eventPublisher.emitAsync(events.manualJournals.onPublishing, { + oldManualJournal, + trx, + } as IManualJournalPublishingPayload); + + // Mark the given manual journal as published. + await this.manualJournalModel.query(trx).findById(manualJournalId).patch({ + publishedAt: moment().toMySqlDateTime(), + }); + // Retrieve the manual journal with enrties after modification. + const manualJournal = await this.manualJournalModel.query() + .findById(manualJournalId) + .withGraphFetched('entries'); + + // Triggers `onManualJournalPublishedBulk` event. + await this.eventPublisher.emitAsync(events.manualJournals.onPublished, { + manualJournal, + oldManualJournal, + trx, + } as IManualJournalEventPublishedPayload); + }); + } +} diff --git a/packages/server-nest/src/modules/ManualJournals/constants.ts b/packages/server-nest/src/modules/ManualJournals/constants.ts new file mode 100644 index 000000000..e72cc11cf --- /dev/null +++ b/packages/server-nest/src/modules/ManualJournals/constants.ts @@ -0,0 +1,64 @@ +export const ERRORS = { + NOT_FOUND: 'manual_journal_not_found', + CREDIT_DEBIT_NOT_EQUAL_ZERO: 'credit_debit_not_equal_zero', + CREDIT_DEBIT_NOT_EQUAL: 'credit_debit_not_equal', + ACCOUNTS_IDS_NOT_FOUND: 'accounts_ids_not_found', + JOURNAL_NUMBER_EXISTS: 'journal_number_exists', + ENTRIES_SHOULD_ASSIGN_WITH_CONTACT: 'ENTRIES_SHOULD_ASSIGN_WITH_CONTACT', + CONTACTS_NOT_FOUND: 'contacts_not_found', + ENTRIES_CONTACTS_NOT_FOUND: 'ENTRIES_CONTACTS_NOT_FOUND', + MANUAL_JOURNAL_ALREADY_PUBLISHED: 'MANUAL_JOURNAL_ALREADY_PUBLISHED', + MANUAL_JOURNAL_NO_REQUIRED: 'MANUAL_JOURNAL_NO_REQUIRED', + COULD_NOT_ASSIGN_DIFFERENT_CURRENCY_TO_ACCOUNTS: + 'COULD_NOT_ASSIGN_DIFFERENT_CURRENCY_TO_ACCOUNTS', + MANUAL_JOURNAL_ENTRIES_HAVE_NO_BRANCH_ID: + 'MANUAL_JOURNAL_ENTRIES_HAVE_NO_BRANCH_ID', +}; + +export const CONTACTS_CONFIG = [ + { + accountBySlug: 'accounts-receivable', + contactService: 'customer', + assignRequired: true, + }, + { + accountBySlug: 'accounts-payable', + contactService: 'vendor', + assignRequired: true, + }, +]; + +export const DEFAULT_VIEWS = []; + +export const ManualJournalsSampleData = [ + { + Date: '2024-02-02', + 'Journal No': 'J-100022', + 'Reference No.': 'REF-10000', + 'Currency Code': '', + 'Exchange Rate': '', + 'Journal Type': '', + Description: 'Animi quasi qui itaque aut possimus illum est magnam enim.', + Credit: 1000, + Debit: 0, + Note: 'Qui reprehenderit voluptate.', + Account: 'Bank Account', + Contact: '', + Publish: 'T', + }, + { + Date: '2024-02-02', + 'Journal No': 'J-100022', + 'Reference No.': 'REF-10000', + 'Currency Code': '', + 'Exchange Rate': '', + 'Journal Type': '', + Description: 'In assumenda dicta autem non est corrupti non et.', + Credit: 0, + Debit: 1000, + Note: 'Omnis tempora qui fugiat neque dolor voluptatem aut repudiandae nihil.', + Account: 'Bank Account', + Contact: '', + Publish: 'T', + }, +]; diff --git a/packages/server-nest/src/modules/ManualJournals/models/ManualJournal.ts b/packages/server-nest/src/modules/ManualJournals/models/ManualJournal.ts new file mode 100644 index 000000000..41540d86f --- /dev/null +++ b/packages/server-nest/src/modules/ManualJournals/models/ManualJournal.ts @@ -0,0 +1,207 @@ +import { Model, mixin } from 'objection'; +// import TenantModel from 'models/TenantModel'; +// import { formatNumber } from 'utils'; +// import ModelSetting from './ModelSetting'; +// import ManualJournalSettings from './ManualJournal.Settings'; +// import CustomViewBaseModel from './CustomViewBaseModel'; +// import { DEFAULT_VIEWS } from '@/services/ManualJournals/constants'; +// import ModelSearchable from './ModelSearchable'; +import { ManualJournalEntry } from './ManualJournalEntry'; +import { BaseModel } from '@/models/Model'; +import { Document } from '@/modules/ChromiumlyTenancy/models/Document'; + +export class ManualJournal extends BaseModel { + date: Date; + journalNumber: string; + journalType: string; + reference: string; + amount: number; + currencyCode: string; + exchangeRate: number | null; + publishedAt: Date | string | null; + description: string; + userId?: number; + + createdAt?: Date; + updatedAt?: Date; + + entries!: ManualJournalEntry[]; + attachments!: Document[]; + + /** + * Table name. + */ + static get tableName() { + return 'manual_journals'; + } + + /** + * Model timestamps. + */ + get timestamps() { + return ['createdAt', 'updatedAt']; + } + + /** + * Virtual attributes. + */ + static get virtualAttributes() { + return ['isPublished', 'amountFormatted']; + } + + /** + * Retrieve the amount formatted value. + */ + // get amountFormatted() { + // return formatNumber(this.amount, { currencyCode: this.currencyCode }); + // } + + /** + * Detarmines whether the invoice is published. + * @return {boolean} + */ + get isPublished() { + return !!this.publishedAt; + } + + /** + * Model modifiers. + */ + static get modifiers() { + return { + /** + * Sort by status query. + */ + sortByStatus(query, order) { + query.orderByRaw(`PUBLISHED_AT IS NULL ${order}`); + }, + + /** + * Filter by draft status. + */ + filterByDraft(query) { + query.whereNull('publishedAt'); + }, + + /** + * Filter by published status. + */ + filterByPublished(query) { + query.whereNotNull('publishedAt'); + }, + + /** + * Filter by the given status. + */ + filterByStatus(query, filterType) { + switch (filterType) { + case 'draft': + query.modify('filterByDraft'); + break; + case 'published': + default: + query.modify('filterByPublished'); + break; + } + }, + }; + } + + /** + * Relationship mapping. + */ + static get relationMappings() { + const { AccountTransaction } = require('../../Accounts/models/AccountTransaction.model'); + const { ManualJournalEntry } = require('./ManualJournalEntry'); + const { Document } = require('../../ChromiumlyTenancy/models/Document'); + // const { MatchedBankTransaction } = require('models/MatchedBankTransaction'); + + return { + entries: { + relation: Model.HasManyRelation, + modelClass: ManualJournalEntry, + join: { + from: 'manual_journals.id', + to: 'manual_journals_entries.manualJournalId', + }, + filter(query) { + query.orderBy('index', 'ASC'); + }, + }, + transactions: { + relation: Model.HasManyRelation, + modelClass: AccountTransaction, + join: { + from: 'manual_journals.id', + to: 'accounts_transactions.referenceId', + }, + filter: (query) => { + query.where('referenceType', 'Journal'); + }, + }, + + /** + * Manual journal may has many attached attachments. + */ + attachments: { + relation: Model.ManyToManyRelation, + modelClass: Document, + join: { + from: 'manual_journals.id', + through: { + from: 'document_links.modelId', + to: 'document_links.documentId', + }, + to: 'documents.id', + }, + filter(query) { + query.where('model_ref', 'ManualJournal'); + }, + }, + + /** + * Manual journal may belongs to matched bank transaction. + */ + // matchedBankTransaction: { + // relation: Model.BelongsToOneRelation, + // modelClass: MatchedBankTransaction, + // join: { + // from: 'manual_journals.id', + // to: 'matched_bank_transactions.referenceId', + // }, + // filter(query) { + // query.where('reference_type', 'ManualJournal'); + // }, + // }, + }; + } + + // static get meta() { + // return ManualJournalSettings; + // } + + // /** + // * Retrieve the default custom views, roles and columns. + // */ + // static get defaultViews() { + // return DEFAULT_VIEWS; + // } + + /** + * Model search attributes. + */ + static get searchRoles() { + return [ + { fieldKey: 'journal_number', comparator: 'contains' }, + { condition: 'or', fieldKey: 'reference', comparator: 'contains' }, + { condition: 'or', fieldKey: 'amount', comparator: 'equals' }, + ]; + } + + /** + * Prevents mutate base currency since the model is not empty. + */ + static get preventMutateBaseCurrency() { + return true; + } +} diff --git a/packages/server-nest/src/modules/ManualJournals/models/ManualJournalEntry.ts b/packages/server-nest/src/modules/ManualJournals/models/ManualJournalEntry.ts new file mode 100644 index 000000000..89161c405 --- /dev/null +++ b/packages/server-nest/src/modules/ManualJournals/models/ManualJournalEntry.ts @@ -0,0 +1,64 @@ +import { Model } from 'objection'; +import { BaseModel } from '@/models/Model'; + +export class ManualJournalEntry extends BaseModel { + index: number; + credit: number; + debit: number; + accountId: number; + note: string; + contactId?: number; + + branchId!: number; + projectId?: number; + + /** + * Table name. + */ + static get tableName() { + return 'manual_journals_entries'; + } + + /** + * Model timestamps. + */ + get timestamps() { + return []; + } + + /** + * Relationship mapping. + */ + static get relationMappings() { + const { Account } = require('../../Accounts/models/Account.model'); + const { Contact } = require('../../Contacts/models/Contact'); + const { Branch } = require('../../Branches/models/Branch.model'); + + return { + account: { + relation: Model.BelongsToOneRelation, + modelClass: Account, + join: { + from: 'manual_journals_entries.accountId', + to: 'accounts.id', + }, + }, + contact: { + relation: Model.BelongsToOneRelation, + modelClass: Contact, + join: { + from: 'manual_journals_entries.contactId', + to: 'contacts.id', + }, + }, + branch: { + relation: Model.BelongsToOneRelation, + modelClass: Branch, + join: { + from: 'manual_journals_entries.branchId', + to: 'branches.id', + }, + }, + }; + } +} diff --git a/packages/server-nest/src/modules/ManualJournals/queries/GetManualJournal.service.ts b/packages/server-nest/src/modules/ManualJournals/queries/GetManualJournal.service.ts new file mode 100644 index 000000000..9fcafd22e --- /dev/null +++ b/packages/server-nest/src/modules/ManualJournals/queries/GetManualJournal.service.ts @@ -0,0 +1,36 @@ +import { Inject, Injectable } from '@nestjs/common'; +import { ManualJournal } from '../models/ManualJournal'; +import { TransformerInjectable } from '@/modules/Transformer/TransformerInjectable.service'; +import { ManualJournalTransfromer } from './ManualJournalTransformer'; + +@Injectable() +export class GetManualJournal { + constructor( + private readonly transformer: TransformerInjectable, + + @Inject(ManualJournal.name) + private readonly manualJournalModel: typeof ManualJournal, + ) {} + + /** + * Retrieve manual journal details with associated journal transactions. + * @param {number} tenantId + * @param {number} manualJournalId + */ + public getManualJournal = async (manualJournalId: number) => { + const manualJournal = await this.manualJournalModel + .query() + .findById(manualJournalId) + .withGraphFetched('entries.account') + .withGraphFetched('entries.contact') + .withGraphFetched('entries.branch') + .withGraphFetched('transactions') + .withGraphFetched('attachments') + .throwIfNotFound(); + + return this.transformer.transform( + manualJournal, + new ManualJournalTransfromer(), + ); + }; +} diff --git a/packages/server-nest/src/modules/ManualJournals/queries/GetManualJournals.service.ts b/packages/server-nest/src/modules/ManualJournals/queries/GetManualJournals.service.ts new file mode 100644 index 000000000..ac8987e42 --- /dev/null +++ b/packages/server-nest/src/modules/ManualJournals/queries/GetManualJournals.service.ts @@ -0,0 +1,77 @@ +// import { Service, Inject } from 'typedi'; +// import * as R from 'ramda'; +// import { +// IManualJournalsFilter, +// IManualJournal, +// IPaginationMeta, +// IFilterMeta, +// } from '@/interfaces'; +// import TenancyService from '@/services/Tenancy/TenancyService'; +// import DynamicListingService from '@/services/DynamicListing/DynamicListService'; +// import { ManualJournalTransfromer } from './ManualJournalTransformer'; +// import { TransformerInjectable } from '@/lib/Transformer/TransformerInjectable'; + +// @Service() +// export class GetManualJournals { +// @Inject() +// private tenancy: TenancyService; + +// @Inject() +// private dynamicListService: DynamicListingService; + +// @Inject() +// private transformer: TransformerInjectable; + +// /** +// * Parses filter DTO of the manual journals list. +// * @param filterDTO +// */ +// private parseListFilterDTO = (filterDTO) => { +// return R.compose(this.dynamicListService.parseStringifiedFilter)(filterDTO); +// }; + +// /** +// * Retrieve manual journals datatable list. +// * @param {number} tenantId - +// * @param {IManualJournalsFilter} filter - +// */ +// public getManualJournals = async ( +// tenantId: number, +// filterDTO: IManualJournalsFilter +// ): Promise<{ +// manualJournals: IManualJournal[]; +// pagination: IPaginationMeta; +// filterMeta: IFilterMeta; +// }> => { +// const { ManualJournal } = this.tenancy.models(tenantId); + +// // Parses filter DTO. +// const filter = this.parseListFilterDTO(filterDTO); + +// // Dynamic service. +// const dynamicService = await this.dynamicListService.dynamicList( +// tenantId, +// ManualJournal, +// filter +// ); +// const { results, pagination } = await ManualJournal.query() +// .onBuild((builder) => { +// dynamicService.buildQuery()(builder); +// builder.withGraphFetched('entries.account'); +// }) +// .pagination(filter.page - 1, filter.pageSize); + +// // Transformes the manual journals models to POJO. +// const manualJournals = await this.transformer.transform( +// tenantId, +// results, +// new ManualJournalTransfromer() +// ); + +// return { +// manualJournals, +// pagination, +// filterMeta: dynamicService.getResponseMeta(), +// }; +// }; +// } diff --git a/packages/server-nest/src/modules/ManualJournals/queries/ManualJournalTransformer.ts b/packages/server-nest/src/modules/ManualJournals/queries/ManualJournalTransformer.ts new file mode 100644 index 000000000..b67ccec2b --- /dev/null +++ b/packages/server-nest/src/modules/ManualJournals/queries/ManualJournalTransformer.ts @@ -0,0 +1,66 @@ +import { Transformer } from '@/modules/Transformer/Transformer'; +import { AttachmentTransformer } from '@/modules/Attachments/Attachment.transformer'; +import { ManualJournal } from '../models/ManualJournal'; + +export class ManualJournalTransfromer extends Transformer { + /** + * Include these attributes to expense object. + * @returns {Array} + */ + public includeAttributes = (): string[] => { + return [ + 'formattedAmount', + 'formattedDate', + 'formattedPublishedAt', + 'formattedCreatedAt', + 'attachments', + ]; + }; + + /** + * Retrieve formatted journal amount. + * @param {IManualJournal} manualJournal + * @returns {string} + */ + protected formattedAmount = (manualJorunal: ManualJournal): string => { + return this.formatNumber(manualJorunal.amount, { + currencyCode: manualJorunal.currencyCode, + }); + }; + + /** + * Retrieve formatted date. + * @param {ManualJournal} manualJournal + * @returns {string} + */ + protected formattedDate = (manualJorunal: ManualJournal): string => { + return this.formatDate(manualJorunal.date); + }; + + /** + * Retrieve formatted created at date. + * @param {ManualJournal} manualJournal + * @returns {string} + */ + protected formattedCreatedAt = (manualJorunal: ManualJournal): string => { + return this.formatDate(manualJorunal.createdAt); + }; + + /** + * Retrieve formatted published at date. + * @param {ManualJournal} manualJournal + * @returns {string} + */ + protected formattedPublishedAt = (manualJorunal: ManualJournal): string => { + return this.formatDate(manualJorunal.publishedAt); + }; + + /** + * Retrieves the manual journal attachments. + * @param {ManualJournal} manualJorunal + * @returns + */ + protected attachments = (manualJorunal: ManualJournal) => { + return this.item(manualJorunal.attachments, new AttachmentTransformer()); + }; +} diff --git a/packages/server-nest/src/modules/ManualJournals/types/ManualJournals.types.ts b/packages/server-nest/src/modules/ManualJournals/types/ManualJournals.types.ts new file mode 100644 index 000000000..f092e7db2 --- /dev/null +++ b/packages/server-nest/src/modules/ManualJournals/types/ManualJournals.types.ts @@ -0,0 +1,100 @@ +import { Knex } from 'knex'; +// import { IDynamicListFilterDTO } from './DynamicFilter'; +// import { ISystemUser } from './User'; +// import { IAccount } from './Account'; +// import { AttachmentLinkDTO } from './Attachments'; +import { ManualJournal } from '../models/ManualJournal'; +import { AttachmentLinkDTO } from '@/modules/Attachments/Attachments.types'; + +export interface IManualJournalEntryDTO { + index: number; + credit: number; + debit: number; + accountId: number; + note: string; + contactId?: number; + branchId?: number + projectId?: number; +} + +export interface IManualJournalDTO { + date: Date; + currencyCode?: string; + exchangeRate?: number; + journalNumber: string; + journalType: string; + reference?: string; + description?: string; + publish?: boolean; + branchId?: number; + entries: IManualJournalEntryDTO[]; + attachments?: AttachmentLinkDTO[]; +} + +// export interface IManualJournalsFilter extends IDynamicListFilterDTO { +// stringifiedFilterRoles?: string; +// page: number; +// pageSize: number; +// } + +export interface IManualJournalEventPublishedPayload { + // tenantId: number; + manualJournal: ManualJournal; + // manualJournalId: number; + oldManualJournal: ManualJournal; + trx: Knex.Transaction; +} + +export interface IManualJournalPublishingPayload { + oldManualJournal: ManualJournal; + trx: Knex.Transaction; + // tenantId: number; +} + +export interface IManualJournalEventDeletedPayload { + // tenantId: number; + manualJournalId: number; + oldManualJournal: ManualJournal; + trx?: Knex.Transaction; +} + +export interface IManualJournalDeletingPayload { + // tenantId: number; + oldManualJournal: ManualJournal; + trx: Knex.Transaction; +} + +export interface IManualJournalEventEditedPayload { + // tenantId: number; + manualJournal: ManualJournal; + oldManualJournal: ManualJournal; + manualJournalDTO: IManualJournalDTO; + trx: Knex.Transaction; +} +export interface IManualJournalEditingPayload { + // tenantId: number; + oldManualJournal: ManualJournal; + manualJournalDTO: IManualJournalDTO; + trx: Knex.Transaction; +} + +export interface IManualJournalCreatingPayload { + // tenantId: number; + manualJournalDTO: IManualJournalDTO; + trx: Knex.Transaction; +} + +export interface IManualJournalEventCreatedPayload { + // tenantId: number; + manualJournal: ManualJournal; + // manualJournalId: number; + manualJournalDTO: IManualJournalDTO; + trx: Knex.Transaction; +} + +export enum ManualJournalAction { + Create = 'Create', + View = 'View', + Edit = 'Edit', + Delete = 'Delete', +} diff --git a/packages/server-nest/src/modules/System/models/TenantMetadataModel.ts b/packages/server-nest/src/modules/System/models/TenantMetadataModel.ts index 91c23587e..22c0723f0 100644 --- a/packages/server-nest/src/modules/System/models/TenantMetadataModel.ts +++ b/packages/server-nest/src/modules/System/models/TenantMetadataModel.ts @@ -7,18 +7,19 @@ import { findByIsoCountryCode } from '@bigcapital/utils'; // import { getUploadedObjectUri } from '../../services/Attachments/utils'; export class TenantMetadata extends BaseModel { - baseCurrency!: string; - name!: string; - tenantId!: number; - industry!: string; - location!: string; - language!: string; - timezone!: string; - dateFormat!: string; - fiscalYear!: string; - primaryColor!: string; - logoKey!: string; - address!: Record; + public baseCurrency!: string; + public name!: string; + public tenantId!: number; + public industry!: string; + public location!: string; + public language!: string; + public timezone!: string; + public dateFormat!: string; + public fiscalYear!: string; + public primaryColor!: string; + public logoKey!: string; + public logoUri!: string; + public address!: Record; /** * Json schema. diff --git a/packages/server-nest/src/modules/System/models/TenantModel.ts b/packages/server-nest/src/modules/System/models/TenantModel.ts index 2b89bff2c..d42c68238 100644 --- a/packages/server-nest/src/modules/System/models/TenantModel.ts +++ b/packages/server-nest/src/modules/System/models/TenantModel.ts @@ -8,7 +8,7 @@ export class TenantModel extends BaseModel { public readonly initializedAt: string; public readonly seededAt: boolean; public readonly builtAt: string; - public readonly metadata: any; + public readonly metadata: TenantMetadata; /** * Table name. diff --git a/packages/server-nest/src/modules/TemplateInjectable/TemplateInjectable.service.ts b/packages/server-nest/src/modules/TemplateInjectable/TemplateInjectable.service.ts index 452a52d45..0806a4e67 100644 --- a/packages/server-nest/src/modules/TemplateInjectable/TemplateInjectable.service.ts +++ b/packages/server-nest/src/modules/TemplateInjectable/TemplateInjectable.service.ts @@ -21,7 +21,8 @@ export class TemplateInjectable { return templateRender(filename, { organizationName: organization.metadata.name, - organizationEmail: organization.metadata.email, + // @todo email + organizationEmail: '', __: this.i18n.t, ...options, }); diff --git a/packages/server-nest/src/modules/Tenancy/TenancyModels/Tenancy.module.ts b/packages/server-nest/src/modules/Tenancy/TenancyModels/Tenancy.module.ts index 086b84a1e..e3deb665a 100644 --- a/packages/server-nest/src/modules/Tenancy/TenancyModels/Tenancy.module.ts +++ b/packages/server-nest/src/modules/Tenancy/TenancyModels/Tenancy.module.ts @@ -32,6 +32,8 @@ import { CreditNoteAppliedInvoice } from '@/modules/CreditNotes/models/CreditNot import { CreditNote } from '@/modules/CreditNotes/models/CreditNote'; import { PaymentLink } from '@/modules/PaymentLinks/models/PaymentLink'; import { SaleReceipt } from '@/modules/SaleReceipts/models/SaleReceipt'; +import { ManualJournal } from '@/modules/ManualJournals/models/ManualJournal'; +import { ManualJournalEntry } from '@/modules/ManualJournals/models/ManualJournalEntry'; const models = [ Item, @@ -63,7 +65,9 @@ const models = [ CreditNoteAppliedInvoice, CreditNote, PaymentLink, - SaleReceipt + SaleReceipt, + ManualJournal, + ManualJournalEntry ]; const modelProviders = models.map((model) => { diff --git a/packages/server-nest/src/modules/TransactionItemEntry/models/ItemEntry.ts b/packages/server-nest/src/modules/TransactionItemEntry/models/ItemEntry.ts index 443df65f3..4589db44d 100644 --- a/packages/server-nest/src/modules/TransactionItemEntry/models/ItemEntry.ts +++ b/packages/server-nest/src/modules/TransactionItemEntry/models/ItemEntry.ts @@ -1,4 +1,3 @@ -import { Model } from 'objection'; import { BaseModel } from '@/models/Model'; import { getExlusiveTaxAmount, diff --git a/packages/server-nest/src/modules/Transformer/Transformer.ts b/packages/server-nest/src/modules/Transformer/Transformer.ts index c492a904c..79a4fe97b 100644 --- a/packages/server-nest/src/modules/Transformer/Transformer.ts +++ b/packages/server-nest/src/modules/Transformer/Transformer.ts @@ -1,4 +1,4 @@ -import moment from 'moment'; +import * as moment from 'moment'; import * as R from 'ramda'; import { includes, isFunction, isObject, isUndefined, omit } from 'lodash'; // import { EXPORT_DTE_FORMAT } from '@/services/Export/constants'; diff --git a/packages/server-nest/test/manual-journal.e2e-spec.ts b/packages/server-nest/test/manual-journal.e2e-spec.ts new file mode 100644 index 000000000..6aa8b6b8c --- /dev/null +++ b/packages/server-nest/test/manual-journal.e2e-spec.ts @@ -0,0 +1,95 @@ +import * as request from 'supertest'; +import { faker } from '@faker-js/faker'; +import { app } from './init-app-test'; + +const makeManualJournalRequest = () => ({ + date: '2022-06-01', + reference: faker.string.uuid(), + journalNumber: faker.string.uuid(), + publish: false, + entries: [ + { + index: 1, + credit: 1000, + debit: 0, + accountId: 1003, + }, + { + index: 2, + credit: 0, + debit: 1000, + accountId: 1004, + }, + ], +}); + +describe('Manual Journals (e2e)', () => { + it('/manual-journals (POST)', () => { + return request(app.getHttpServer()) + .post('/manual-journals') + .set('organization-id', '4064541lv40nhca') + .send(makeManualJournalRequest()) + .expect(201); + }); + + it('/manual-journals/:id (DELETE)', async () => { + const response = await request(app.getHttpServer()) + .post('/manual-journals') + .set('organization-id', '4064541lv40nhca') + .send(makeManualJournalRequest()); + + const journalId = response.body.id; + + return request(app.getHttpServer()) + .delete(`/manual-journals/${journalId}`) + .set('organization-id', '4064541lv40nhca') + .send() + .expect(200); + }); + + it('/manual-journals/:id (GET)', async () => { + const response = await request(app.getHttpServer()) + .post('/manual-journals') + .set('organization-id', '4064541lv40nhca') + .send(makeManualJournalRequest()); + + const journalId = response.body.id; + + return request(app.getHttpServer()) + .get(`/manual-journals/${journalId}`) + .set('organization-id', '4064541lv40nhca') + .send() + .expect(200); + }); + + it('/manual-journals/:id (PUT)', async () => { + const manualJournal = makeManualJournalRequest(); + const response = await request(app.getHttpServer()) + .post('/manual-journals') + .set('organization-id', '4064541lv40nhca') + .send(manualJournal); + + const journalId = response.body.id; + + return request(app.getHttpServer()) + .put(`/manual-journals/${journalId}`) + .set('organization-id', '4064541lv40nhca') + .send(manualJournal) + .expect(200); + }); + + it('/manual-journals/:id/publish (POST)', async () => { + const response = await request(app.getHttpServer()) + .post('/manual-journals') + .set('organization-id', '4064541lv40nhca') + .send(makeManualJournalRequest()); + + const journalId = response.body.id; + + return request(app.getHttpServer()) + .post(`/manual-journals/${journalId}/publish`) + .set('organization-id', '4064541lv40nhca') + .send() + .expect(200); + }); +});