From 270b421a6ceb90361e34c7ca6513228dd055fc09 Mon Sep 17 00:00:00 2001 From: Ahmed Bouhuolia Date: Sun, 12 Jan 2025 18:22:48 +0200 Subject: [PATCH] refactor: dynamic list to nestjs --- packages/server-nest/package.json | 2 + .../server-nest/src/common/types/Features.ts | 16 + packages/server-nest/src/interfaces/Model.ts | 5 +- .../modules/Accounts/Accounts.controller.ts | 10 +- .../src/modules/Accounts/Accounts.module.ts | 10 +- .../src/modules/Accounts/Accounts.types.ts | 13 +- .../Accounts/AccountsApplication.service.ts | 26 +- .../modules/Accounts/GetAccounts.service.ts | 121 +++--- .../BankingTransactions.controller.ts | 15 +- .../BankingTransactionsApplication.service.ts | 10 +- .../BankingTransactions/models/BankAccount.ts | 139 +++++++ .../queries/GetBankAccounts.service.ts | 103 +++-- .../src/modules/Bills/Bills.application.ts | 18 +- .../src/modules/Bills/Bills.controller.ts | 6 + .../src/modules/Bills/Bills.module.ts | 4 + .../src/modules/Bills/Bills.types.ts | 2 - .../commands/BillInventoryTransactions.ts | 141 ++++--- .../modules/Bills/queries/GetBills.service.ts | 65 ++++ .../src/modules/Bills/queries/GetBills.ts | 72 ---- ...illWriteInventoryTransactionsSubscriber.ts | 58 +++ .../CreditNotesInventoryTransactions.ts | 160 ++++---- ...editNoteInventoryTransactionsSubscriber.ts | 158 ++++---- .../CustomViews/CustomViewBaseModel.ts | 8 +- .../src/modules/Customers/Customers.module.ts | 3 +- .../Customers/CustomersApplication.service.ts | 2 +- .../Customers/commands/GetCustomers.ts | 76 ---- .../GetCustomer.service.ts | 2 +- .../Customers/queries/GetCustomers.service.ts | 61 +++ .../DynamicFilter/DynamicFilter.ts | 10 +- .../DynamicFilter/DynamicFilter.types.ts | 6 +- .../DynamicFilter/DynamicFilterAbstractor.ts | 4 +- .../DynamicFilterAdvancedFilter.ts | 2 - .../DynamicFilter/DynamicFilterFilterRoles.ts | 10 +- .../DynamicFilterRoleAbstractor.ts | 57 +-- .../DynamicFilter/DynamicFilterSearch.ts | 1 - .../DynamicFilter/DynamicFilterSortBy.ts | 7 +- .../DynamicFilter/DynamicFilterViews.ts | 6 +- .../DynamicListing/DynamicList.module.ts | 18 + ...cListService.ts => DynamicList.service.ts} | 24 +- .../DynamicListing/DynamicListAbstract.ts | 1 - ...ew.ts => DynamicListCustomView.service.ts} | 7 +- ...s.ts => DynamicListFilterRoles.service.ts} | 4 +- ...Search.ts => DynamicListSearch.service.ts} | 4 +- .../DynamicListServiceAbstract.ts | 1 + ...SortBy.ts => DynamicListSortBy.service.ts} | 4 +- .../models/CustomViewBaseModel.ts | 26 ++ .../DynamicListing/models/MetadataModel.ts | 93 +++++ .../models/SearchableBaseModel.ts | 24 ++ .../DynamicListing/types/DynamicList.types.ts | 11 + .../modules/Expenses/Expenses.controller.ts | 10 + .../src/modules/Expenses/Expenses.module.ts | 5 +- .../Expenses/ExpensesApplication.service.ts | 19 +- .../Expenses/queries/GetExpenses.service.ts | 137 +++---- .../InventoryAdjustments.module.ts | 4 + ...nventoryAdjustmentInventoryTransactions.ts | 70 ++++ ...justmentInventoryTransactionsSubscriber.ts | 56 +++ .../src/modules/InventoryCost/Inventory.ts | 366 ++++++++++++++++++ .../InventoryCost/InventoryAverageCost.ts | 254 ++++++++++++ .../InventoryCost/InventoryCost.module.ts | 21 + .../InventoryCost/InventoryCostApplication.ts | 28 ++ .../InventoryCostGLStorage.service.ts | 38 ++ .../InventoryCost/InventoryCostLotTracker.ts | 302 +++++++++++++++ .../InventoryCost/InventoryCostMethod.ts | 46 +++ .../InventoryCost/InventoryCosts.service.ts | 149 +++++++ .../InventoryItemsQuantitySync.service.ts | 94 +++++ .../models/InventoryCostLotTracker.ts | 135 +++++++ .../models/InventoryTransaction.ts | 165 ++++++++ .../InventoryCostGLBeforeWriteSubscriber.ts | 27 ++ .../types/InventoryCost.types.ts | 61 +++ .../src/modules/InventoryCost/utils.ts | 15 + .../ItemCategory.application.ts | 11 + .../queries/GetItemCategories.service.ts | 56 +++ .../src/modules/Items/GetItems.service.ts | 67 ++++ .../src/modules/Items/Item.controller.ts | 19 +- .../src/modules/Items/Items.module.ts | 5 +- .../modules/Items/ItemsApplication.service.ts | 11 + .../src/modules/Items/models/Item.ts | 16 +- .../src/modules/Items/types/Items.types.ts | 8 + .../queries/GetManualJournals.service.ts | 129 +++--- .../PaymentReceived.application.ts | 19 +- .../PaymentsReceived.controller.ts | 6 + .../queries/GetPaymentsReceived.service.ts | 66 ++++ .../queries/GetPaymentsReceived.ts | 79 ---- .../src/modules/Resource/Resource.module.ts | 8 + .../src/modules/Resource/ResourceService.ts | 170 ++++++++ .../src/modules/Resource/_utils.ts | 6 + .../Resource/models/ResourcableModel.ts | 12 + .../SaleEstimates.application.ts | 12 +- .../SaleEstimates/SaleEstimates.controller.ts | 8 +- .../queries/GetSaleEstimates.service.ts | 68 ++++ .../SaleEstimates/queries/GetSaleEstimates.ts | 79 ---- .../types/SaleEstimates.types.ts | 9 +- .../inventory/InvoiceInventoryTransactions.ts | 77 ++++ .../InvoiceWriteInventoryTransactions.ts | 68 ++++ .../SaleReceiptApplication.service.ts | 33 +- .../SaleReceipts/SaleReceipts.module.ts | 6 +- .../SaleReceiptInventoryTransactions.ts | 66 ++++ .../SaleReceiptWriteInventoryTransactions.ts | 69 ++++ .../queries/GetSaleReceipts.service.ts | 73 ++++ .../SaleReceipts/queries/GetSaleReceipts.ts | 84 ---- .../modules/TaxRates/TaxRate.application.ts | 10 +- .../modules/TaxRates/TaxRate.controller.ts | 11 +- .../TaxRates/queries/GetTaxRates.service.ts | 50 +-- .../VendorCredit/VendorCredits.module.ts | 6 + .../VendorCreditsApplication.service.ts | 11 + .../VendorCreditInventoryTransactions.ts | 162 ++++---- .../queries/GetVendorCredits.service.ts | 122 +++--- ...orCreditInventoryTransactionsSusbcriber.ts | 152 +++----- .../src/modules/Vendors/Vendors.controller.ts | 6 + .../Vendors/VendorsApplication.service.ts | 12 +- .../Vendors/queries/GetVendors.service.ts | 66 ++++ .../src/modules/Vendors/queries/GetVendors.ts | 80 ---- .../modules/Views/GetResourceViews.service.ts | 1 - .../server/src/models/CustomViewBaseModel.ts | 4 + .../DynamicListing/DynamicListing.types.ts | 31 ++ .../VendorCreditInventoryTransactions.ts | 30 +- pnpm-lock.yaml | 3 + 117 files changed, 4232 insertions(+), 1493 deletions(-) create mode 100644 packages/server-nest/src/common/types/Features.ts create mode 100644 packages/server-nest/src/modules/BankingTransactions/models/BankAccount.ts create mode 100644 packages/server-nest/src/modules/Bills/queries/GetBills.service.ts delete mode 100644 packages/server-nest/src/modules/Bills/queries/GetBills.ts create mode 100644 packages/server-nest/src/modules/Bills/subscribers/BillWriteInventoryTransactionsSubscriber.ts delete mode 100644 packages/server-nest/src/modules/Customers/commands/GetCustomers.ts rename packages/server-nest/src/modules/Customers/{commands => queries}/GetCustomer.service.ts (92%) create mode 100644 packages/server-nest/src/modules/Customers/queries/GetCustomers.service.ts create mode 100644 packages/server-nest/src/modules/DynamicListing/DynamicList.module.ts rename packages/server-nest/src/modules/DynamicListing/{DynamicListService.ts => DynamicList.service.ts} (83%) delete mode 100644 packages/server-nest/src/modules/DynamicListing/DynamicListAbstract.ts rename packages/server-nest/src/modules/DynamicListing/{DynamicListCustomView.ts => DynamicListCustomView.service.ts} (85%) rename packages/server-nest/src/modules/DynamicListing/{DynamicListFilterRoles.ts => DynamicListFilterRoles.service.ts} (94%) rename packages/server-nest/src/modules/DynamicListing/{DynamicListSearch.ts => DynamicListSearch.service.ts} (72%) create mode 100644 packages/server-nest/src/modules/DynamicListing/DynamicListServiceAbstract.ts rename packages/server-nest/src/modules/DynamicListing/{DynamicListSortBy.ts => DynamicListSortBy.service.ts} (87%) create mode 100644 packages/server-nest/src/modules/DynamicListing/models/CustomViewBaseModel.ts create mode 100644 packages/server-nest/src/modules/DynamicListing/models/MetadataModel.ts create mode 100644 packages/server-nest/src/modules/DynamicListing/models/SearchableBaseModel.ts create mode 100644 packages/server-nest/src/modules/DynamicListing/types/DynamicList.types.ts create mode 100644 packages/server-nest/src/modules/InventoryAdjutments/inventory/InventoryAdjustmentInventoryTransactions.ts create mode 100644 packages/server-nest/src/modules/InventoryAdjutments/inventory/InventoryAdjustmentInventoryTransactionsSubscriber.ts create mode 100644 packages/server-nest/src/modules/InventoryCost/Inventory.ts create mode 100644 packages/server-nest/src/modules/InventoryCost/InventoryAverageCost.ts create mode 100644 packages/server-nest/src/modules/InventoryCost/InventoryCost.module.ts create mode 100644 packages/server-nest/src/modules/InventoryCost/InventoryCostApplication.ts create mode 100644 packages/server-nest/src/modules/InventoryCost/InventoryCostGLStorage.service.ts create mode 100644 packages/server-nest/src/modules/InventoryCost/InventoryCostLotTracker.ts create mode 100644 packages/server-nest/src/modules/InventoryCost/InventoryCostMethod.ts create mode 100644 packages/server-nest/src/modules/InventoryCost/InventoryCosts.service.ts create mode 100644 packages/server-nest/src/modules/InventoryCost/InventoryItemsQuantitySync.service.ts create mode 100644 packages/server-nest/src/modules/InventoryCost/models/InventoryCostLotTracker.ts create mode 100644 packages/server-nest/src/modules/InventoryCost/models/InventoryTransaction.ts create mode 100644 packages/server-nest/src/modules/InventoryCost/subscribers/InventoryCostGLBeforeWriteSubscriber.ts create mode 100644 packages/server-nest/src/modules/InventoryCost/types/InventoryCost.types.ts create mode 100644 packages/server-nest/src/modules/InventoryCost/utils.ts create mode 100644 packages/server-nest/src/modules/ItemCategories/queries/GetItemCategories.service.ts create mode 100644 packages/server-nest/src/modules/Items/types/Items.types.ts create mode 100644 packages/server-nest/src/modules/PaymentReceived/queries/GetPaymentsReceived.service.ts delete mode 100644 packages/server-nest/src/modules/PaymentReceived/queries/GetPaymentsReceived.ts create mode 100644 packages/server-nest/src/modules/Resource/Resource.module.ts create mode 100644 packages/server-nest/src/modules/Resource/ResourceService.ts create mode 100644 packages/server-nest/src/modules/Resource/_utils.ts create mode 100644 packages/server-nest/src/modules/Resource/models/ResourcableModel.ts create mode 100644 packages/server-nest/src/modules/SaleEstimates/queries/GetSaleEstimates.service.ts delete mode 100644 packages/server-nest/src/modules/SaleEstimates/queries/GetSaleEstimates.ts create mode 100644 packages/server-nest/src/modules/SaleInvoices/commands/inventory/InvoiceInventoryTransactions.ts create mode 100644 packages/server-nest/src/modules/SaleInvoices/subscribers/InvoiceWriteInventoryTransactions.ts create mode 100644 packages/server-nest/src/modules/SaleReceipts/inventory/SaleReceiptInventoryTransactions.ts create mode 100644 packages/server-nest/src/modules/SaleReceipts/inventory/SaleReceiptWriteInventoryTransactions.ts create mode 100644 packages/server-nest/src/modules/SaleReceipts/queries/GetSaleReceipts.service.ts delete mode 100644 packages/server-nest/src/modules/SaleReceipts/queries/GetSaleReceipts.ts create mode 100644 packages/server-nest/src/modules/Vendors/queries/GetVendors.service.ts delete mode 100644 packages/server-nest/src/modules/Vendors/queries/GetVendors.ts create mode 100644 packages/server/src/services/DynamicListing/DynamicListing.types.ts diff --git a/packages/server-nest/package.json b/packages/server-nest/package.json index 5eb7c80ea..b2bcbfaf2 100644 --- a/packages/server-nest/package.json +++ b/packages/server-nest/package.json @@ -52,6 +52,7 @@ "form-data": "^4.0.0", "fp-ts": "^2.16.9", "js-money": "^0.6.3", + "is-my-json-valid": "^2.20.5", "knex": "^3.1.0", "lamda": "^0.4.1", "lodash": "^4.17.21", @@ -66,6 +67,7 @@ "passport-jwt": "^4.0.1", "passport-local": "^1.0.0", "plaid": "^10.3.0", + "pluralize": "^8.0.0", "posthog-node": "^4.3.2", "pug": "^3.0.2", "ramda": "^0.30.1", diff --git a/packages/server-nest/src/common/types/Features.ts b/packages/server-nest/src/common/types/Features.ts new file mode 100644 index 000000000..0d2e94237 --- /dev/null +++ b/packages/server-nest/src/common/types/Features.ts @@ -0,0 +1,16 @@ +export enum Features { + WAREHOUSES = 'warehouses', + BRANCHES = 'branches', + BankSyncing = 'BankSyncing', +} + +export interface IFeatureAllItem { + name: string; + isAccessible: boolean; + defaultAccessible: boolean; +} + +export interface IFeatureConfiugration { + name: string; + defaultValue?: boolean; +} diff --git a/packages/server-nest/src/interfaces/Model.ts b/packages/server-nest/src/interfaces/Model.ts index 2623ce836..7e0c1713d 100644 --- a/packages/server-nest/src/interfaces/Model.ts +++ b/packages/server-nest/src/interfaces/Model.ts @@ -140,8 +140,9 @@ export interface IModelMeta { print?: IModelPrintMeta; - fields: { [key: string]: IModelMetaField }; - columns: { [key: string]: IModelMetaColumn }; + fields: Record; + fields2: Record; + columns: Record; } // ---- diff --git a/packages/server-nest/src/modules/Accounts/Accounts.controller.ts b/packages/server-nest/src/modules/Accounts/Accounts.controller.ts index ecc4f7e50..ff3517a31 100644 --- a/packages/server-nest/src/modules/Accounts/Accounts.controller.ts +++ b/packages/server-nest/src/modules/Accounts/Accounts.controller.ts @@ -12,7 +12,7 @@ import { AccountsApplication } from './AccountsApplication.service'; import { CreateAccountDTO } from './CreateAccount.dto'; import { EditAccountDTO } from './EditAccount.dto'; import { PublicRoute } from '../Auth/Jwt.guard'; -import { IAccountsTransactionsFilter } from './Accounts.types'; +import { IAccountsFilter, IAccountsTransactionsFilter } from './Accounts.types'; // import { IAccountsFilter, IAccountsTransactionsFilter } from './Accounts.types'; // import { ZodValidationPipe } from '@/common/pipes/ZodValidation.pipe'; @@ -64,9 +64,9 @@ export class AccountsController { return this.accountsApplication.getAccount(id); } - // @Get() - // async getAccounts(@Query() filter: IAccountsFilter) { - // return this.accountsApplication.getAccounts(filter); - // } + @Get() + async getAccounts(@Query() filter: IAccountsFilter) { + return this.accountsApplication.getAccounts(filter); + } } diff --git a/packages/server-nest/src/modules/Accounts/Accounts.module.ts b/packages/server-nest/src/modules/Accounts/Accounts.module.ts index 2176a3fc0..f5d27c2f5 100644 --- a/packages/server-nest/src/modules/Accounts/Accounts.module.ts +++ b/packages/server-nest/src/modules/Accounts/Accounts.module.ts @@ -13,8 +13,12 @@ import { TransformerInjectable } from '../Transformer/TransformerInjectable.serv import { ActivateAccount } from './ActivateAccount.service'; import { GetAccountTypesService } from './GetAccountTypes.service'; import { GetAccountTransactionsService } from './GetAccountTransactions.service'; +import { RegisterTenancyModel } from '../Tenancy/TenancyModels/Tenancy.module'; +import { BankAccount } from '../BankingTransactions/models/BankAccount'; // import { GetAccountsService } from './GetAccounts.service'; +const models = [RegisterTenancyModel(BankAccount)]; + @Module({ imports: [TenancyDatabaseModule], controllers: [AccountsController], @@ -31,10 +35,8 @@ import { GetAccountTransactionsService } from './GetAccountTransactions.service' ActivateAccount, GetAccountTypesService, GetAccountTransactionsService, + ...models, ], - exports: [ - AccountRepository, - CreateAccountService, - ] + exports: [AccountRepository, CreateAccountService, ...models], }) export class AccountsModule {} diff --git a/packages/server-nest/src/modules/Accounts/Accounts.types.ts b/packages/server-nest/src/modules/Accounts/Accounts.types.ts index 66a4a1ebe..559f802f6 100644 --- a/packages/server-nest/src/modules/Accounts/Accounts.types.ts +++ b/packages/server-nest/src/modules/Accounts/Accounts.types.ts @@ -18,11 +18,11 @@ export enum IAccountsStructureType { } // export interface IAccountsFilter extends IDynamicListFilterDTO { -// stringifiedFilterRoles?: string; -// onlyInactive: boolean; -// structure?: IAccountsStructureType; // } -export interface IAccountsFilter {} +export interface IAccountsFilter { + onlyInactive: boolean; + structure?: IAccountsStructureType; +} export interface IAccountType { label: string; key: string; @@ -88,7 +88,4 @@ export interface CreateAccountParams { ignoreUniqueName: boolean; } - -export interface IGetAccountTransactionPOJO { - -} +export interface IGetAccountTransactionPOJO {} diff --git a/packages/server-nest/src/modules/Accounts/AccountsApplication.service.ts b/packages/server-nest/src/modules/Accounts/AccountsApplication.service.ts index 5fa4b31f3..003b4d3cf 100644 --- a/packages/server-nest/src/modules/Accounts/AccountsApplication.service.ts +++ b/packages/server-nest/src/modules/Accounts/AccountsApplication.service.ts @@ -11,9 +11,12 @@ import { ActivateAccount } from './ActivateAccount.service'; import { GetAccountTypesService } from './GetAccountTypes.service'; import { GetAccountTransactionsService } from './GetAccountTransactions.service'; import { + IAccountsFilter, IAccountsTransactionsFilter, IGetAccountTransactionPOJO, } from './Accounts.types'; +import { GetAccountsService } from './GetAccounts.service'; +import { IFilterMeta } from '@/interfaces/Model'; @Injectable() export class AccountsApplication { @@ -24,8 +27,8 @@ export class AccountsApplication { private readonly activateAccountService: ActivateAccount, private readonly getAccountTypesService: GetAccountTypesService, private readonly getAccountService: GetAccount, - // private readonly getAccountsService: GetAccounts, private readonly getAccountTransactionsService: GetAccountTransactionsService, + private readonly getAccountsService: GetAccountsService, ) {} /** @@ -96,17 +99,16 @@ export class AccountsApplication { return this.getAccountTypesService.getAccountsTypes(); }; - // /** - // * Retrieves the accounts list. - // * @param {number} tenantId - // * @param {IAccountsFilter} filterDTO - // * @returns {Promise<{ accounts: IAccountResponse[]; filterMeta: IFilterMeta }>} - // */ - // public getAccounts = ( - // filterDTO: IAccountsFilter, - // ): Promise<{ accounts: IAccountResponse[]; filterMeta: IFilterMeta }> => { - // return this.getAccountsService.getAccountsList(filterDTO); - // }; + /** + * Retrieves the accounts list. + * @param {IAccountsFilter} filterDTO - Filter DTO. + * @returns {Promise<{ accounts: IAccountResponse[]; filterMeta: IFilterMeta }>} + */ + public getAccounts = ( + filterDTO: IAccountsFilter, + ): Promise<{ accounts: Account[]; filterMeta: IFilterMeta }> => { + return this.getAccountsService.getAccountsList(filterDTO); + }; /** * Retrieves the given account transactions. diff --git a/packages/server-nest/src/modules/Accounts/GetAccounts.service.ts b/packages/server-nest/src/modules/Accounts/GetAccounts.service.ts index 9372d4a9b..9afe3ed9a 100644 --- a/packages/server-nest/src/modules/Accounts/GetAccounts.service.ts +++ b/packages/server-nest/src/modules/Accounts/GetAccounts.service.ts @@ -1,67 +1,66 @@ -// import { Injectable } from '@nestjs/common'; -// import * as R from 'ramda'; -// import { -// IAccountsFilter, -// IAccountResponse, -// IFilterMeta, -// } from './Accounts.types'; -// import { DynamicListService } from '../DynamicListing/DynamicListService'; -// import { AccountTransformer } from './Account.transformer'; -// import { TransformerInjectable } from '../Transformer/TransformerInjectable.service'; -// import { Account } from './models/Account.model'; -// import { AccountRepository } from './repositories/Account.repository'; +import { Inject, Injectable } from '@nestjs/common'; +import * as R from 'ramda'; +import { IAccountsFilter } from './Accounts.types'; +import { DynamicListService } from '../DynamicListing/DynamicList.service'; +import { AccountTransformer } from './Account.transformer'; +import { TransformerInjectable } from '../Transformer/TransformerInjectable.service'; +import { Account } from './models/Account.model'; +import { AccountRepository } from './repositories/Account.repository'; +import { IFilterMeta } from '@/interfaces/Model'; -// @Injectable() -// export class GetAccountsService { -// constructor( -// private readonly dynamicListService: DynamicListService, -// private readonly transformerService: TransformerInjectable, -// private readonly accountModel: typeof Account, -// private readonly accountRepository: AccountRepository, -// ) {} +@Injectable() +export class GetAccountsService { + constructor( + private readonly dynamicListService: DynamicListService, + private readonly transformerService: TransformerInjectable, -// /** -// * Retrieve accounts datatable list. -// * @param {IAccountsFilter} accountsFilter -// * @returns {Promise<{ accounts: IAccountResponse[]; filterMeta: IFilterMeta }>} -// */ -// public async getAccountsList( -// filterDTO: IAccountsFilter, -// ): Promise<{ accounts: IAccountResponse[]; filterMeta: IFilterMeta }> { -// // Parses the stringified filter roles. -// const filter = this.parseListFilterDTO(filterDTO); + @Inject(Account.name) + private readonly accountModel: typeof Account, + private readonly accountRepository: AccountRepository, + ) {} -// // Dynamic list service. -// const dynamicList = await this.dynamicListService.dynamicList( -// this.accountModel, -// filter, -// ); -// // Retrieve accounts model based on the given query. -// const accounts = await this.accountModel.query().onBuild((builder) => { -// dynamicList.buildQuery()(builder); -// builder.modify('inactiveMode', filter.inactiveMode); -// }); -// const accountsGraph = await this.accountRepository.getDependencyGraph(); + /** + * Retrieve accounts datatable list. + * @param {IAccountsFilter} accountsFilter + * @returns {Promise<{ accounts: IAccountResponse[]; filterMeta: IFilterMeta }>} + */ + public async getAccountsList( + filterDTO: IAccountsFilter, + ): Promise<{ accounts: Account[]; filterMeta: IFilterMeta }> { + // Parses the stringified filter roles. + const filter = this.parseListFilterDTO(filterDTO); -// // Retrieves the transformed accounts collection. -// const transformedAccounts = await this.transformerService.transform( -// accounts, -// new AccountTransformer(), -// { accountsGraph, structure: filterDTO.structure }, -// ); + // Dynamic list service. + const dynamicList = await this.dynamicListService.dynamicList( + this.accountModel, + filter, + ); + // Retrieve accounts model based on the given query. + const accounts = await this.accountModel.query().onBuild((builder) => { + dynamicList.buildQuery()(builder); + builder.modify('inactiveMode', filter.inactiveMode); + }); + const accountsGraph = await this.accountRepository.getDependencyGraph(); -// return { -// accounts: transformedAccounts, -// filterMeta: dynamicList.getResponseMeta(), -// }; -// } + // Retrieves the transformed accounts collection. + const transformedAccounts = await this.transformerService.transform( + accounts, + new AccountTransformer(), + { accountsGraph, structure: filterDTO.structure }, + ); -// /** -// * Parsees accounts list filter DTO. -// * @param filterDTO -// * @returns -// */ -// private parseListFilterDTO(filterDTO) { -// return R.compose(this.dynamicListService.parseStringifiedFilter)(filterDTO); -// } -// } + return { + accounts: transformedAccounts, + filterMeta: dynamicList.getResponseMeta(), + }; + } + + /** + * Parsees accounts list filter DTO. + * @param filterDTO + * @returns + */ + private parseListFilterDTO(filterDTO) { + return R.compose(this.dynamicListService.parseStringifiedFilter)(filterDTO); + } +} diff --git a/packages/server-nest/src/modules/BankingTransactions/BankingTransactions.controller.ts b/packages/server-nest/src/modules/BankingTransactions/BankingTransactions.controller.ts index 09a527a10..a4e03cfa1 100644 --- a/packages/server-nest/src/modules/BankingTransactions/BankingTransactions.controller.ts +++ b/packages/server-nest/src/modules/BankingTransactions/BankingTransactions.controller.ts @@ -1,4 +1,12 @@ -import { Body, Controller, Delete, Get, Param, Post } from '@nestjs/common'; +import { + Body, + Controller, + Delete, + Get, + Param, + Post, + Query, +} from '@nestjs/common'; import { BankingTransactionsApplication } from './BankingTransactionsApplication.service'; import { ICashflowNewCommandDTO } from './types/BankingTransactions.types'; import { PublicRoute } from '../Auth/Jwt.guard'; @@ -10,6 +18,11 @@ export class BankingTransactionsController { private readonly bankingTransactionsApplication: BankingTransactionsApplication, ) {} + @Get('') + async getBankAccounts(@Query() filterDTO: ICashflowAccountsFilter) { + return this.bankingTransactionsApplication.getBankAccounts(filterDTO); + } + @Post() async createTransaction(@Body() transactionDTO: ICashflowNewCommandDTO) { return this.bankingTransactionsApplication.createTransaction( diff --git a/packages/server-nest/src/modules/BankingTransactions/BankingTransactionsApplication.service.ts b/packages/server-nest/src/modules/BankingTransactions/BankingTransactionsApplication.service.ts index bb4fce4d9..1e724d2e0 100644 --- a/packages/server-nest/src/modules/BankingTransactions/BankingTransactionsApplication.service.ts +++ b/packages/server-nest/src/modules/BankingTransactions/BankingTransactionsApplication.service.ts @@ -4,6 +4,7 @@ import { CreateBankTransactionService } from './commands/CreateBankTransaction.s import { GetBankTransactionService } from './queries/GetBankTransaction.service'; import { ICashflowNewCommandDTO } from './types/BankingTransactions.types'; import { Injectable } from '@nestjs/common'; +import { GetBankAccountsService } from './queries/GetBankAccounts.service'; @Injectable() export class BankingTransactionsApplication { @@ -11,7 +12,7 @@ export class BankingTransactionsApplication { private readonly createTransactionService: CreateBankTransactionService, private readonly deleteTransactionService: DeleteCashflowTransaction, private readonly getCashflowTransactionService: GetBankTransactionService, - // private readonly getCashflowAccountsService: GetBankingAccountsServic, + private readonly getBankAccountsService: GetBankAccountsService, ) {} /** @@ -48,11 +49,8 @@ export class BankingTransactionsApplication { /** * Retrieves the cashflow accounts. * @param {ICashflowAccountsFilter} filterDTO - * @returns */ - public getCashflowAccounts( - // filterDTO: ICashflowAccountsFilter, - ) { - // return this.getCashflowAccountsService.getCashflowAccounts(filterDTO); + public getBankAccounts(filterDTO: ICashflowAccountsFilter) { + return this.getBankAccountsService.getBankAccounts(filterDTO); } } diff --git a/packages/server-nest/src/modules/BankingTransactions/models/BankAccount.ts b/packages/server-nest/src/modules/BankingTransactions/models/BankAccount.ts new file mode 100644 index 000000000..fa588320f --- /dev/null +++ b/packages/server-nest/src/modules/BankingTransactions/models/BankAccount.ts @@ -0,0 +1,139 @@ +/* eslint-disable global-require */ +import { mixin, Model } from 'objection'; +import { castArray } from 'lodash'; +import { BaseModel } from '@/models/Model'; +import { AccountTypesUtils } from '@/libs/accounts-utils/AccountTypesUtils'; +import { PlaidItem } from '@/modules/BankingPlaid/models/PlaidItem'; + +export class BankAccount extends BaseModel { + public name!: string; + public slug!: string; + public code!: string; + public index!: number; + public accountType!: string; + public predefined!: boolean; + public currencyCode!: string; + public active!: boolean; + public bankBalance!: number; + public lastFeedsUpdatedAt!: string | null; + public amount!: number; + public plaidItemId!: number; + + public plaidItem!: PlaidItem; + + /** + * Table name. + */ + static get tableName() { + return 'accounts'; + } + + /** + * Timestamps columns. + */ + static get timestamps() { + return ['createdAt', 'updatedAt']; + } + + /** + * Virtual attributes. + */ + static get virtualAttributes() { + return ['accountTypeLabel']; + } + + /** + * Retrieve account type label. + */ + get accountTypeLabel() { + return AccountTypesUtils.getType(this.accountType, 'label'); + } + + /** + * Allows to mark model as resourceable to viewable and filterable. + */ + static get resourceable() { + return true; + } + + /** + * Model modifiers. + */ + static get modifiers() { + return { + /** + * Inactive/Active mode. + */ + inactiveMode(query, active = false) { + query.where('accounts.active', !active); + }, + }; + } + + /** + * Relationship mapping. + */ + static get relationMappings() { + const AccountTransaction = require('models/AccountTransaction'); + + return { + /** + * Account model may has many transactions. + */ + transactions: { + relation: Model.HasManyRelation, + modelClass: AccountTransaction.default, + join: { + from: 'accounts.id', + to: 'accounts_transactions.accountId', + }, + }, + }; + } + + /** + * Detarmines whether the given type equals the account type. + * @param {string} accountType + * @return {boolean} + */ + isAccountType(accountType) { + const types = castArray(accountType); + return types.indexOf(this.accountType) !== -1; + } + + /** + * Detarmine whether the given parent type equals the account type. + * @param {string} parentType + * @return {boolean} + */ + isParentType(parentType) { + return AccountTypesUtils.isParentTypeEqualsKey( + this.accountType, + parentType + ); + } + + // /** + // * Model settings. + // */ + // static get meta() { + // return CashflowAccountSettings; + // } + + // /** + // * Retrieve the default custom views, roles and columns. + // */ + // static get defaultViews() { + // return DEFAULT_VIEWS; + // } + + /** + * Model search roles. + */ + static get searchRoles() { + return [ + { condition: 'or', fieldKey: 'name', comparator: 'contains' }, + { condition: 'or', fieldKey: 'code', comparator: 'like' }, + ]; + } +} diff --git a/packages/server-nest/src/modules/BankingTransactions/queries/GetBankAccounts.service.ts b/packages/server-nest/src/modules/BankingTransactions/queries/GetBankAccounts.service.ts index 40f934d59..19f1c9ea2 100644 --- a/packages/server-nest/src/modules/BankingTransactions/queries/GetBankAccounts.service.ts +++ b/packages/server-nest/src/modules/BankingTransactions/queries/GetBankAccounts.service.ts @@ -1,61 +1,52 @@ -// import { Service, Inject } from 'typedi'; -// import { ICashflowAccount, ICashflowAccountsFilter } from '@/interfaces'; -// import { CashflowAccountTransformer } from './queries/BankAccountTransformer'; -// import TenancyService from '@/services/Tenancy/TenancyService'; -// import DynamicListingService from '@/services/DynamicListing/DynamicListService'; -// import { TransformerInjectable } from '@/lib/Transformer/TransformerInjectable'; -// import { ACCOUNT_TYPE } from '@/data/AccountTypes'; +import { Injectable, Inject } from '@nestjs/common'; +import { BankAccount } from '../models/BankAccount'; +import { DynamicListService } from '@/modules/DynamicListing/DynamicList.service'; +import { TransformerInjectable } from '@/modules/Transformer/TransformerInjectable.service'; +import { CashflowAccountTransformer } from './BankAccountTransformer'; +import { ACCOUNT_TYPE } from '@/constants/accounts'; -// @Service() -// export default class GetCashflowAccountsService { -// @Inject() -// private tenancy: TenancyService; +@Injectable() +export class GetBankAccountsService { + constructor( + private readonly dynamicListService: DynamicListService, + private readonly transformer: TransformerInjectable, -// @Inject() -// private dynamicListService: DynamicListingService; + @Inject(BankAccount.name) + private readonly bankAccountModel: typeof BankAccount + ) {} -// @Inject() -// private transformer: TransformerInjectable; + /** + * Retrieve the cash flow accounts. + * @param {ICashflowAccountsFilter} filterDTO - Filter DTO. + * @returns {ICashflowAccount[]} + */ + public async getBankAccounts( + filterDTO: ICashflowAccountsFilter, + ): Promise { + // Parsees accounts list filter DTO. + const filter = this.dynamicListService.parseStringifiedFilter(filterDTO); -// /** -// * Retrieve the cash flow accounts. -// * @param {number} tenantId - Tenant id. -// * @param {ICashflowAccountsFilter} filterDTO - Filter DTO. -// * @returns {ICashflowAccount[]} -// */ -// public async getCashflowAccounts( -// tenantId: number, -// filterDTO: ICashflowAccountsFilter -// ): Promise<{ cashflowAccounts: ICashflowAccount[] }> { -// const { CashflowAccount } = this.tenancy.models(tenantId); + // Dynamic list service. + const dynamicList = await this.dynamicListService.dynamicList( + BankAccount, + filter, + ); + // Retrieve accounts model based on the given query. + const accounts = await this.bankAccountModel.query().onBuild((builder) => { + dynamicList.buildQuery()(builder); -// // Parsees accounts list filter DTO. -// const filter = this.dynamicListService.parseStringifiedFilter(filterDTO); - -// // Dynamic list service. -// const dynamicList = await this.dynamicListService.dynamicList( -// tenantId, -// CashflowAccount, -// filter -// ); -// // Retrieve accounts model based on the given query. -// const accounts = await CashflowAccount.query().onBuild((builder) => { -// dynamicList.buildQuery()(builder); - -// builder.whereIn('account_type', [ -// ACCOUNT_TYPE.BANK, -// ACCOUNT_TYPE.CASH, -// ACCOUNT_TYPE.CREDIT_CARD, -// ]); -// builder.modify('inactiveMode', filter.inactiveMode); -// }); -// // Retrieves the transformed accounts. -// const transformed = await this.transformer.transform( -// tenantId, -// accounts, -// new CashflowAccountTransformer() -// ); - -// return transformed; -// } -// } + builder.whereIn('account_type', [ + ACCOUNT_TYPE.BANK, + ACCOUNT_TYPE.CASH, + ACCOUNT_TYPE.CREDIT_CARD, + ]); + builder.modify('inactiveMode', filter.inactiveMode); + }); + // Retrieves the transformed accounts. + const transformed = await this.transformer.transform( + accounts, + new CashflowAccountTransformer(), + ); + return transformed; + } +} diff --git a/packages/server-nest/src/modules/Bills/Bills.application.ts b/packages/server-nest/src/modules/Bills/Bills.application.ts index 9bb3db083..c71b6e071 100644 --- a/packages/server-nest/src/modules/Bills/Bills.application.ts +++ b/packages/server-nest/src/modules/Bills/Bills.application.ts @@ -1,17 +1,14 @@ - import { CreateBill } from './commands/CreateBill.service'; import { EditBillService } from './commands/EditBill.service'; import { GetBill } from './queries/GetBill'; // import { GetBills } from './queries/GetBills'; import { DeleteBill } from './commands/DeleteBill.service'; -import { - IBillDTO, - IBillEditDTO, -} from './Bills.types'; +import { IBillDTO, IBillEditDTO } from './Bills.types'; import { GetDueBills } from './queries/GetDueBills.service'; import { OpenBillService } from './commands/OpenBill.service'; import { GetBillPayments } from './queries/GetBillPayments'; import { Injectable } from '@nestjs/common'; +import { GetBillsService } from './queries/GetBills.service'; @Injectable() export class BillsApplication { @@ -22,7 +19,7 @@ export class BillsApplication { private deleteBillService: DeleteBill, private getDueBillsService: GetDueBills, private openBillService: OpenBillService, - // private getBillsService: GetBills, + private getBillsService: GetBillsService, // private getBillPaymentsService: GetBillPayments, ) {} @@ -56,14 +53,11 @@ export class BillsApplication { /** * Retrieve bills data table list. - * @param {number} tenantId - * @param {IBillsFilter} billsFilter - */ - // public getBills( - // filterDTO: IBillsFilter, - // ) { - // return this.getBillsService.getBills(filterDTO); - // } + public getBills(filterDTO: IBillsFilter) { + return this.getBillsService.getBills(filterDTO); + } /** * Retrieves the given bill details. diff --git a/packages/server-nest/src/modules/Bills/Bills.controller.ts b/packages/server-nest/src/modules/Bills/Bills.controller.ts index 6c37a2fea..1d2533d1a 100644 --- a/packages/server-nest/src/modules/Bills/Bills.controller.ts +++ b/packages/server-nest/src/modules/Bills/Bills.controller.ts @@ -6,6 +6,7 @@ import { Param, Delete, Get, + Query, } from '@nestjs/common'; import { BillsApplication } from './Bills.application'; import { IBillDTO, IBillEditDTO } from './Bills.types'; @@ -31,6 +32,11 @@ export class BillsController { return this.billsApplication.deleteBill(billId); } + @Get() + getBills(@Query() filterDTO: IBillsFilter) { + return this.billsApplication.getBills(filterDTO); + } + @Get(':id') getBill(@Param('id') billId: number) { return this.billsApplication.getBill(billId); diff --git a/packages/server-nest/src/modules/Bills/Bills.module.ts b/packages/server-nest/src/modules/Bills/Bills.module.ts index 5866e8187..0219b0a87 100644 --- a/packages/server-nest/src/modules/Bills/Bills.module.ts +++ b/packages/server-nest/src/modules/Bills/Bills.module.ts @@ -21,6 +21,8 @@ import { BillGLEntriesSubscriber } from './subscribers/BillGLEntriesSubscriber'; import { BillGLEntries } from './commands/BillsGLEntries'; import { LedgerModule } from '../Ledger/Ledger.module'; import { AccountsModule } from '../Accounts/Accounts.module'; +import { BillWriteInventoryTransactionsSubscriber } from './subscribers/BillWriteInventoryTransactionsSubscriber'; +import { BillInventoryTransactions } from './commands/BillInventoryTransactions'; @Module({ imports: [BillLandedCostsModule, LedgerModule, AccountsModule], @@ -43,6 +45,8 @@ import { AccountsModule } from '../Accounts/Accounts.module'; BillGLEntries, ItemsEntriesService, BillGLEntriesSubscriber, + BillInventoryTransactions, + BillWriteInventoryTransactionsSubscriber, ], controllers: [BillsController], }) diff --git a/packages/server-nest/src/modules/Bills/Bills.types.ts b/packages/server-nest/src/modules/Bills/Bills.types.ts index 56f0bab21..481fb00da 100644 --- a/packages/server-nest/src/modules/Bills/Bills.types.ts +++ b/packages/server-nest/src/modules/Bills/Bills.types.ts @@ -70,8 +70,6 @@ export interface IBillEditingPayload { trx: Knex.Transaction; } export interface IBillEditedPayload { - // tenantId: number; - // billId: number; oldBill: Bill; bill: Bill; billDTO: IBillDTO; diff --git a/packages/server-nest/src/modules/Bills/commands/BillInventoryTransactions.ts b/packages/server-nest/src/modules/Bills/commands/BillInventoryTransactions.ts index e0d8df399..6ddeee4f9 100644 --- a/packages/server-nest/src/modules/Bills/commands/BillInventoryTransactions.ts +++ b/packages/server-nest/src/modules/Bills/commands/BillInventoryTransactions.ts @@ -1,82 +1,73 @@ -// import { Knex } from 'knex'; -// import { Inject, Service } from 'typedi'; -// import InventoryService from '@/services/Inventory/Inventory'; -// import ItemsEntriesService from '@/services/Items/ItemsEntriesService'; -// import HasTenancyService from '@/services/Tenancy/TenancyService'; +import { Knex } from 'knex'; +import { Bill } from '../models/Bill'; +import { Injectable } from '@nestjs/common'; +import { ItemsEntriesService } from '@/modules/Items/ItemsEntries.service'; +import { InventoryService } from '@/modules/InventoryCost/Inventory'; -// @Service() -// export class BillInventoryTransactions { -// @Inject() -// private tenancy: HasTenancyService; +@Injectable() +export class BillInventoryTransactions { + constructor( + private readonly itemsEntriesService: ItemsEntriesService, + private readonly inventoryService: InventoryService, -// @Inject() -// private itemsEntriesService: ItemsEntriesService; + private readonly bill: typeof Bill + ) {} -// @Inject() -// private inventoryService: InventoryService; + /** + * Records the inventory transactions from the given bill input. + * @param {number} billId - Bill id. + * @return {Promise} + */ + public async recordInventoryTransactions( + billId: number, + override?: boolean, + trx?: Knex.Transaction + ): Promise { + // Retireve bill with assocaited entries and allocated cost entries. + + const bill = await this.bill.query(trx) + .findById(billId) + .withGraphFetched('entries.allocatedCostEntries'); -// /** -// * Records the inventory transactions from the given bill input. -// * @param {Bill} bill - Bill model object. -// * @param {number} billId - Bill id. -// * @return {Promise} -// */ -// public async recordInventoryTransactions( -// tenantId: number, -// billId: number, -// override?: boolean, -// trx?: Knex.Transaction -// ): Promise { -// const { Bill } = this.tenancy.models(tenantId); + // Loads the inventory items entries of the given sale invoice. + const inventoryEntries = + await this.itemsEntriesService.filterInventoryEntries( + bill.entries + ); + const transaction = { + transactionId: bill.id, + transactionType: 'Bill', + exchangeRate: bill.exchangeRate, -// // Retireve bill with assocaited entries and allocated cost entries. -// const bill = await Bill.query(trx) -// .findById(billId) -// .withGraphFetched('entries.allocatedCostEntries'); + date: bill.billDate, + direction: 'IN', + entries: inventoryEntries, + createdAt: bill.createdAt, -// // Loads the inventory items entries of the given sale invoice. -// const inventoryEntries = -// await this.itemsEntriesService.filterInventoryEntries( -// tenantId, -// bill.entries -// ); -// const transaction = { -// transactionId: bill.id, -// transactionType: 'Bill', -// exchangeRate: bill.exchangeRate, + warehouseId: bill.warehouseId, + }; + await this.inventoryService.recordInventoryTransactionsFromItemsEntries( + transaction, + override, + trx + ); + } -// date: bill.billDate, -// direction: 'IN', -// entries: inventoryEntries, -// createdAt: bill.createdAt, - -// warehouseId: bill.warehouseId, -// }; -// await this.inventoryService.recordInventoryTransactionsFromItemsEntries( -// tenantId, -// transaction, -// override, -// trx -// ); -// } - -// /** -// * Reverts the inventory transactions of the given bill id. -// * @param {number} tenantId - Tenant id. -// * @param {number} billId - Bill id. -// * @return {Promise} -// */ -// public async revertInventoryTransactions( -// tenantId: number, -// billId: number, -// trx?: Knex.Transaction -// ) { -// // Deletes the inventory transactions by the given reference id and type. -// await this.inventoryService.deleteInventoryTransactions( -// tenantId, -// billId, -// 'Bill', -// trx -// ); -// } -// } + /** + * Reverts the inventory transactions of the given bill id. + * @param {number} tenantId - Tenant id. + * @param {number} billId - Bill id. + * @return {Promise} + */ + public async revertInventoryTransactions( + billId: number, + trx?: Knex.Transaction + ) { + // Deletes the inventory transactions by the given reference id and type. + await this.inventoryService.deleteInventoryTransactions( + billId, + 'Bill', + trx + ); + } +} diff --git a/packages/server-nest/src/modules/Bills/queries/GetBills.service.ts b/packages/server-nest/src/modules/Bills/queries/GetBills.service.ts new file mode 100644 index 000000000..4951a1b21 --- /dev/null +++ b/packages/server-nest/src/modules/Bills/queries/GetBills.service.ts @@ -0,0 +1,65 @@ +import { Injectable } from '@nestjs/common'; +import * as R from 'ramda'; +import { TransformerInjectable } from '@/modules/Transformer/TransformerInjectable.service'; +import { DynamicListService } from '@/modules/DynamicListing/DynamicList.service'; +import { Bill } from '../models/Bill'; +import { IFilterMeta, IPaginationMeta } from '@/interfaces/Model'; +import { BillTransformer } from './Bill.transformer'; + +@Injectable() +export class GetBillsService { + constructor( + private transformer: TransformerInjectable, + private dynamicListService: DynamicListService, + ) {} + + /** + * Retrieve bills data table list. + * @param {IBillsFilter} billsFilter - + */ + public async getBills( + filterDTO: IBillsFilter, + ): Promise<{ + bills: Bill; + pagination: IPaginationMeta; + filterMeta: IFilterMeta; + }> { + // Parses bills list filter DTO. + const filter = this.parseListFilterDTO(filterDTO); + + // Dynamic list service. + const dynamicFilter = await this.dynamicListService.dynamicList( + Bill, + filter, + ); + const { results, pagination } = await Bill.query() + .onBuild((builder) => { + builder.withGraphFetched('vendor'); + builder.withGraphFetched('entries.item'); + dynamicFilter.buildQuery()(builder); + + // Filter query. + filterDTO?.filterQuery && filterDTO?.filterQuery(builder); + }) + .pagination(filter.page - 1, filter.pageSize); + + // Tranform the bills to POJO. + const bills = await this.transformer.transform( + results, + new BillTransformer(), + ); + return { + bills, + pagination, + filterMeta: dynamicFilter.getResponseMeta(), + }; + } + + /** + * Parses bills list filter DTO. + * @param filterDTO - + */ + private parseListFilterDTO(filterDTO) { + return R.compose(this.dynamicListService.parseStringifiedFilter)(filterDTO); + } +} diff --git a/packages/server-nest/src/modules/Bills/queries/GetBills.ts b/packages/server-nest/src/modules/Bills/queries/GetBills.ts deleted file mode 100644 index 2fae14a85..000000000 --- a/packages/server-nest/src/modules/Bills/queries/GetBills.ts +++ /dev/null @@ -1,72 +0,0 @@ -// import { Injectable } from '@nestjs/common'; -// import * as R from 'ramda'; -// import { -// IBill, -// IBillsFilter, -// IFilterMeta, -// IPaginationMeta, -// } from '@/interfaces'; -// import { BillTransformer } from './Bill.transformer'; -// import { TransformerInjectable } from '@/modules/Transformer/TransformerInjectable.service'; -// // import { DynamicListingService } from '@/modules/DynamicListing/DynamicListService'; - -// @Injectable() -// export class GetBills { -// constructor( -// private transformer: TransformerInjectable, -// private dynamicListService: DynamicListingService, -// ) {} - -// /** -// * Retrieve bills data table list. -// * @param {number} tenantId - -// * @param {IBillsFilter} billsFilter - -// */ -// public async getBills( -// tenantId: number, -// filterDTO: IBillsFilter, -// ): Promise<{ -// bills: IBill; -// pagination: IPaginationMeta; -// filterMeta: IFilterMeta; -// }> { -// // Parses bills list filter DTO. -// const filter = this.parseListFilterDTO(filterDTO); - -// // Dynamic list service. -// const dynamicFilter = await this.dynamicListService.dynamicList( -// tenantId, -// Bill, -// filter, -// ); -// const { results, pagination } = await Bill.query() -// .onBuild((builder) => { -// builder.withGraphFetched('vendor'); -// builder.withGraphFetched('entries.item'); -// dynamicFilter.buildQuery()(builder); - -// // Filter query. -// filterDTO?.filterQuery && filterDTO?.filterQuery(builder); -// }) -// .pagination(filter.page - 1, filter.pageSize); - -// // Tranform the bills to POJO. -// const bills = await this.transformer.transform( -// results, -// new PurchaseInvoiceTransformer(), -// ); -// return { -// bills, -// pagination, -// filterMeta: dynamicFilter.getResponseMeta(), -// }; -// } - -// /** -// * Parses bills list filter DTO. -// * @param filterDTO - -// */ -// private parseListFilterDTO(filterDTO) { -// return R.compose(this.dynamicListService.parseStringifiedFilter)(filterDTO); -// } -// } diff --git a/packages/server-nest/src/modules/Bills/subscribers/BillWriteInventoryTransactionsSubscriber.ts b/packages/server-nest/src/modules/Bills/subscribers/BillWriteInventoryTransactionsSubscriber.ts new file mode 100644 index 000000000..740e8a385 --- /dev/null +++ b/packages/server-nest/src/modules/Bills/subscribers/BillWriteInventoryTransactionsSubscriber.ts @@ -0,0 +1,58 @@ +import { Injectable } from '@nestjs/common'; +import { + IBillCreatedPayload, + IBillEditedPayload, + IBIllEventDeletedPayload, + IBillOpenedPayload, +} from '../Bills.types'; +import { BillInventoryTransactions } from '../commands/BillInventoryTransactions'; +import { events } from '@/common/events/events'; +import { OnEvent } from '@nestjs/event-emitter'; + +@Injectable() +export class BillWriteInventoryTransactionsSubscriber { + constructor(private readonly billsInventory: BillInventoryTransactions) {} + + /** + * Handles writing the inventory transactions once bill created. + * @param {IBillCreatedPayload | IBillOpenedPayload} payload - + */ + @OnEvent(events.bill.onCreated) + @OnEvent(events.bill.onOpened) + public async handleWritingInventoryTransactions({ + bill, + trx, + }: IBillCreatedPayload | IBillOpenedPayload) { + // Can't continue if the bill is not opened yet. + if (!bill.openedAt) return null; + + await this.billsInventory.recordInventoryTransactions(bill.id, false, trx); + } + + /** + * Handles the overwriting the inventory transactions once bill edited. + * @param {IBillEditedPayload} payload - + */ + @OnEvent(events.bill.onEdited) + public async handleOverwritingInventoryTransactions({ + bill, + trx, + }: IBillEditedPayload) { + // Can't continue if the bill is not opened yet. + if (!bill.openedAt) return null; + + await this.billsInventory.recordInventoryTransactions(bill.id, true, trx); + }; + + /** + * Handles the reverting the inventory transactions once the bill deleted. + * @param {IBIllEventDeletedPayload} payload - + */ + @OnEvent(events.bill.onDeleted) + public async handleRevertInventoryTransactions({ + billId, + trx, + }: IBIllEventDeletedPayload) { + await this.billsInventory.revertInventoryTransactions(billId, trx); + } +} diff --git a/packages/server-nest/src/modules/CreditNotes/commands/CreditNotesInventoryTransactions.ts b/packages/server-nest/src/modules/CreditNotes/commands/CreditNotesInventoryTransactions.ts index ec7deda66..9d64d6706 100644 --- a/packages/server-nest/src/modules/CreditNotes/commands/CreditNotesInventoryTransactions.ts +++ b/packages/server-nest/src/modules/CreditNotes/commands/CreditNotesInventoryTransactions.ts @@ -1,90 +1,82 @@ -// import { Inject, Service } from 'typedi'; -// import { Knex } from 'knex'; -// import { ICreditNote } from '@/interfaces'; -// import InventoryService from '@/services/Inventory/Inventory'; -// import ItemsEntriesService from '@/services/Items/ItemsEntriesService'; +import { Injectable } from '@nestjs/common'; -// @Service() -// export default class CreditNoteInventoryTransactions { -// @Inject() -// inventoryService: InventoryService; +import { InventoryService } from '@/modules/InventoryCost/Inventory'; +import { ItemsEntriesService } from '@/modules/Items/ItemsEntries.service'; +import { CreditNote } from '../models/CreditNote'; +import { Knex } from 'knex'; +@Injectable() +export class CreditNoteInventoryTransactions { + constructor( + private readonly inventoryService: InventoryService, + private readonly itemsEntriesService: ItemsEntriesService, + ) {} -// @Inject() -// itemsEntriesService: ItemsEntriesService; + /** + * Creates credit note inventory transactions. + * @param {number} tenantId + * @param {ICreditNote} creditNote + */ + public createInventoryTransactions = async ( + creditNote: CreditNote, + trx?: Knex.Transaction, + ): Promise => { + // Loads the inventory items entries of the given sale invoice. + const inventoryEntries = + await this.itemsEntriesService.filterInventoryEntries(creditNote.entries); -// /** -// * Creates credit note inventory transactions. -// * @param {number} tenantId -// * @param {ICreditNote} creditNote -// */ -// public createInventoryTransactions = async ( -// tenantId: number, -// creditNote: ICreditNote, -// trx?: Knex.Transaction -// ): Promise => { -// // Loads the inventory items entries of the given sale invoice. -// const inventoryEntries = -// await this.itemsEntriesService.filterInventoryEntries( -// tenantId, -// creditNote.entries -// ); -// const transaction = { -// transactionId: creditNote.id, -// transactionType: 'CreditNote', -// transactionNumber: creditNote.creditNoteNumber, -// exchangeRate: creditNote.exchangeRate, -// date: creditNote.creditNoteDate, -// direction: 'IN', -// entries: inventoryEntries, -// createdAt: creditNote.createdAt, -// warehouseId: creditNote.warehouseId, -// }; -// // Writes inventory tranactions. -// await this.inventoryService.recordInventoryTransactionsFromItemsEntries( -// tenantId, -// transaction, -// false, -// trx -// ); -// }; + const transaction = { + transactionId: creditNote.id, + transactionType: 'CreditNote', + transactionNumber: creditNote.creditNoteNumber, + exchangeRate: creditNote.exchangeRate, + date: creditNote.creditNoteDate, + direction: 'IN', + entries: inventoryEntries, + createdAt: creditNote.createdAt, + warehouseId: creditNote.warehouseId, + }; + // Writes inventory tranactions. + await this.inventoryService.recordInventoryTransactionsFromItemsEntries( + transaction, + false, + trx, + ); + }; -// /** -// * Edits vendor credit associated inventory transactions. -// * @param {number} tenantId -// * @param {number} creditNoteId -// * @param {ICreditNote} creditNote -// * @param {Knex.Transactions} trx -// */ -// public editInventoryTransactions = async ( -// tenantId: number, -// creditNoteId: number, -// creditNote: ICreditNote, -// trx?: Knex.Transaction -// ): Promise => { -// // Deletes inventory transactions. -// await this.deleteInventoryTransactions(tenantId, creditNoteId, trx); + /** + * Edits vendor credit associated inventory transactions. + * @param {number} tenantId + * @param {number} creditNoteId + * @param {ICreditNote} creditNote + * @param {Knex.Transactions} trx + */ + public editInventoryTransactions = async ( + creditNoteId: number, + creditNote: CreditNote, + trx?: Knex.Transaction, + ): Promise => { + // Deletes inventory transactions. + await this.deleteInventoryTransactions(creditNoteId, trx); -// // Re-write inventory transactions. -// await this.createInventoryTransactions(tenantId, creditNote, trx); -// }; + // Re-write inventory transactions. + await this.createInventoryTransactions(creditNote, trx); + }; -// /** -// * Deletes credit note associated inventory transactions. -// * @param {number} tenantId - Tenant id. -// * @param {number} creditNoteId - Credit note id. -// * @param {Knex.Transaction} trx - -// */ -// public deleteInventoryTransactions = async ( -// tenantId: number, -// creditNoteId: number, -// trx?: Knex.Transaction -// ): Promise => { -// // Deletes the inventory transactions by the given reference id and type. -// await this.inventoryService.deleteInventoryTransactions( -// tenantId, -// creditNoteId, -// 'CreditNote', -// trx -// ); -// }; -// } + /** + * Deletes credit note associated inventory transactions. + * @param {number} tenantId - Tenant id. + * @param {number} creditNoteId - Credit note id. + * @param {Knex.Transaction} trx - + */ + public deleteInventoryTransactions = async ( + creditNoteId: number, + trx?: Knex.Transaction, + ): Promise => { + // Deletes the inventory transactions by the given reference id and type. + await this.inventoryService.deleteInventoryTransactions( + creditNoteId, + 'CreditNote', + trx, + ); + }; +} diff --git a/packages/server-nest/src/modules/CreditNotes/subscribers/CreditNoteInventoryTransactionsSubscriber.ts b/packages/server-nest/src/modules/CreditNotes/subscribers/CreditNoteInventoryTransactionsSubscriber.ts index 7abba2f5a..67552b01d 100644 --- a/packages/server-nest/src/modules/CreditNotes/subscribers/CreditNoteInventoryTransactionsSubscriber.ts +++ b/packages/server-nest/src/modules/CreditNotes/subscribers/CreditNoteInventoryTransactionsSubscriber.ts @@ -1,98 +1,74 @@ -// import { Service, Inject } from 'typedi'; -// import events from '@/subscribers/events'; -// import CreditNoteInventoryTransactions from '../commands/CreditNotesInventoryTransactions'; -// import { -// ICreditNoteCreatedPayload, -// ICreditNoteDeletedPayload, -// ICreditNoteEditedPayload, -// } from '@/interfaces'; +import { Injectable } from '@nestjs/common'; +import { + ICreditNoteCreatedPayload, + ICreditNoteDeletedPayload, + ICreditNoteEditedPayload, +} from '../types/CreditNotes.types'; +import { OnEvent } from '@nestjs/event-emitter'; +import { events } from '@/common/events/events'; +import { CreditNoteInventoryTransactions } from '../commands/CreditNotesInventoryTransactions'; -// @Service() -// export default class CreditNoteInventoryTransactionsSubscriber { -// @Inject() -// inventoryTransactions: CreditNoteInventoryTransactions; +@Injectable() +export class CreditNoteInventoryTransactionsSubscriber { + constructor( + private readonly inventoryTransactions: CreditNoteInventoryTransactions; + ) {} -// /** -// * Attaches events with publisher. -// */ -// public attach(bus) { -// bus.subscribe( -// events.creditNote.onCreated, -// this.writeInventoryTranscationsOnceCreated -// ); -// bus.subscribe( -// events.creditNote.onEdited, -// this.rewriteInventoryTransactionsOnceEdited -// ); -// bus.subscribe( -// events.creditNote.onDeleted, -// this.revertInventoryTransactionsOnceDeleted -// ); -// bus.subscribe( -// events.creditNote.onOpened, -// this.writeInventoryTranscationsOnceCreated -// ); -// } + /** + * Writes inventory transactions once credit note created. + * @param {ICreditNoteCreatedPayload} payload - + * @returns {Promise} + */ + @OnEvent(events.creditNote.onCreated) + @OnEvent(events.creditNote.onOpened) + public async writeInventoryTranscationsOnceCreated({ + creditNote, + trx, + }: ICreditNoteCreatedPayload) { + // Can't continue if the credit note is open yet. + if (!creditNote.isOpen) return; -// /** -// * Writes inventory transactions once credit note created. -// * @param {ICreditNoteCreatedPayload} payload - -// * @returns {Promise} -// */ -// public writeInventoryTranscationsOnceCreated = async ({ -// tenantId, -// creditNote, -// trx, -// }: ICreditNoteCreatedPayload) => { -// // Can't continue if the credit note is open yet. -// if (!creditNote.isOpen) return; + await this.inventoryTransactions.createInventoryTransactions( + creditNote, + trx + ); + }; -// await this.inventoryTransactions.createInventoryTransactions( -// tenantId, -// creditNote, -// trx -// ); -// }; + /** + * Rewrites inventory transactions once credit note edited. + * @param {ICreditNoteEditedPayload} payload - + * @returns {Promise} + */ + @OnEvent(events.creditNote.onEdited) + public async rewriteInventoryTransactionsOnceEdited({ + creditNote, + trx, + }: ICreditNoteEditedPayload) { + // Can't continue if the credit note is open yet. + if (!creditNote.isOpen) return; -// /** -// * Rewrites inventory transactions once credit note edited. -// * @param {ICreditNoteEditedPayload} payload - -// * @returns {Promise} -// */ -// public rewriteInventoryTransactionsOnceEdited = async ({ -// tenantId, -// creditNoteId, -// creditNote, -// trx, -// }: ICreditNoteEditedPayload) => { -// // Can't continue if the credit note is open yet. -// if (!creditNote.isOpen) return; + await this.inventoryTransactions.editInventoryTransactions( + creditNoteId, + creditNote, + trx + ); + }; -// await this.inventoryTransactions.editInventoryTransactions( -// tenantId, -// creditNoteId, -// creditNote, -// trx -// ); -// }; + /** + * Reverts inventory transactions once credit note deleted. + * @param {ICreditNoteDeletedPayload} payload - + */ + @OnEvent(events.creditNote.onDeleted) + public async revertInventoryTransactionsOnceDeleted({ + oldCreditNote, + trx, + }: ICreditNoteDeletedPayload) { + // Can't continue if the credit note is open yet. + if (!oldCreditNote.isOpen) return; -// /** -// * Reverts inventory transactions once credit note deleted. -// * @param {ICreditNoteDeletedPayload} payload - -// */ -// public revertInventoryTransactionsOnceDeleted = async ({ -// tenantId, -// creditNoteId, -// oldCreditNote, -// trx, -// }: ICreditNoteDeletedPayload) => { -// // Can't continue if the credit note is open yet. -// if (!oldCreditNote.isOpen) return; - -// await this.inventoryTransactions.deleteInventoryTransactions( -// tenantId, -// creditNoteId, -// trx -// ); -// }; -// } + await this.inventoryTransactions.deleteInventoryTransactions( + creditNoteId, + trx + ); + }; +} diff --git a/packages/server-nest/src/modules/CustomViews/CustomViewBaseModel.ts b/packages/server-nest/src/modules/CustomViews/CustomViewBaseModel.ts index f7f1eda63..024595e8a 100644 --- a/packages/server-nest/src/modules/CustomViews/CustomViewBaseModel.ts +++ b/packages/server-nest/src/modules/CustomViews/CustomViewBaseModel.ts @@ -1,7 +1,9 @@ -import { Model as ObjectionModel } from 'objection'; +import { BaseModel } from "@/models/Model"; +; +type GConstructor = new (...args: any[]) => T; -export const CustomViewBaseModel = (Model) => - class extends Model { +export const CustomViewBaseModelMixin = >(Model: T) => + class CustomViewBaseModel extends Model { /** * Retrieve the default custom views, roles and columns. */ diff --git a/packages/server-nest/src/modules/Customers/Customers.module.ts b/packages/server-nest/src/modules/Customers/Customers.module.ts index 0c22e425d..ab576eb0e 100644 --- a/packages/server-nest/src/modules/Customers/Customers.module.ts +++ b/packages/server-nest/src/modules/Customers/Customers.module.ts @@ -7,7 +7,7 @@ import { CreateCustomer } from './commands/CreateCustomer.service'; import { CustomerValidators } from './commands/CustomerValidators.service'; import { EditCustomer } from './commands/EditCustomer.service'; import { EditOpeningBalanceCustomer } from './commands/EditOpeningBalanceCustomer.service'; -import { GetCustomerService } from './commands/GetCustomer.service'; +import { GetCustomerService } from './queries/GetCustomer.service'; import { CreateEditCustomerDTO } from './commands/CreateEditCustomerDTO.service'; import { CustomersController } from './Customers.controller'; import { CustomersApplication } from './CustomersApplication.service'; @@ -29,6 +29,7 @@ import { DeleteCustomer } from './commands/DeleteCustomer.service'; DeleteCustomer, TenancyContext, TransformerInjectable, + GetCustomerService ], }) export class CustomersModule {} diff --git a/packages/server-nest/src/modules/Customers/CustomersApplication.service.ts b/packages/server-nest/src/modules/Customers/CustomersApplication.service.ts index 3ae351df1..332e6e686 100644 --- a/packages/server-nest/src/modules/Customers/CustomersApplication.service.ts +++ b/packages/server-nest/src/modules/Customers/CustomersApplication.service.ts @@ -1,5 +1,5 @@ import { Injectable } from '@nestjs/common'; -import { GetCustomerService } from './commands/GetCustomer.service'; +import { GetCustomerService } from './queries/GetCustomer.service'; import { CreateCustomer } from './commands/CreateCustomer.service'; import { EditCustomer } from './commands/EditCustomer.service'; import { DeleteCustomer } from './commands/DeleteCustomer.service'; diff --git a/packages/server-nest/src/modules/Customers/commands/GetCustomers.ts b/packages/server-nest/src/modules/Customers/commands/GetCustomers.ts deleted file mode 100644 index 4380a3fa3..000000000 --- a/packages/server-nest/src/modules/Customers/commands/GetCustomers.ts +++ /dev/null @@ -1,76 +0,0 @@ -// import { Inject, Service } from 'typedi'; -// import * as R from 'ramda'; -// import { -// ICustomer, -// ICustomersFilter, -// IFilterMeta, -// IPaginationMeta, -// } from '@/interfaces'; -// import HasTenancyService from '@/services/Tenancy/TenancyService'; -// import DynamicListingService from '@/services/DynamicListing/DynamicListService'; -// import CustomerTransfromer from '../queries/CustomerTransformer'; -// import { TransformerInjectable } from '@/lib/Transformer/TransformerInjectable'; - -// @Service() -// export class GetCustomers { -// @Inject() -// private tenancy: HasTenancyService; - -// @Inject() -// private dynamicListService: DynamicListingService; - -// @Inject() -// private transformer: TransformerInjectable; - -// /** -// * Parses customers list filter DTO. -// * @param filterDTO - -// */ -// private parseCustomersListFilterDTO(filterDTO) { -// return R.compose(this.dynamicListService.parseStringifiedFilter)(filterDTO); -// } - -// /** -// * Retrieve customers paginated list. -// * @param {number} tenantId - Tenant id. -// * @param {ICustomersFilter} filter - Cusotmers filter. -// */ -// public async getCustomersList( -// filterDTO: ICustomersFilter -// ): Promise<{ -// customers: ICustomer[]; -// pagination: IPaginationMeta; -// filterMeta: IFilterMeta; -// }> { -// const { Customer } = this.tenancy.models(tenantId); - -// // Parses customers list filter DTO. -// const filter = this.parseCustomersListFilterDTO(filterDTO); - -// // Dynamic list. -// const dynamicList = await this.dynamicListService.dynamicList( -// tenantId, -// Customer, -// filter -// ); -// // Customers. -// const { results, pagination } = await Customer.query() -// .onBuild((builder) => { -// dynamicList.buildQuery()(builder); -// builder.modify('inactiveMode', filter.inactiveMode); -// }) -// .pagination(filter.page - 1, filter.pageSize); - -// // Retrieves the transformed customers. -// const customers = await this.transformer.transform( -// tenantId, -// results, -// new CustomerTransfromer() -// ); -// return { -// customers, -// pagination, -// filterMeta: dynamicList.getResponseMeta(), -// }; -// } -// } diff --git a/packages/server-nest/src/modules/Customers/commands/GetCustomer.service.ts b/packages/server-nest/src/modules/Customers/queries/GetCustomer.service.ts similarity index 92% rename from packages/server-nest/src/modules/Customers/commands/GetCustomer.service.ts rename to packages/server-nest/src/modules/Customers/queries/GetCustomer.service.ts index 7bca8c420..64c30960e 100644 --- a/packages/server-nest/src/modules/Customers/commands/GetCustomer.service.ts +++ b/packages/server-nest/src/modules/Customers/queries/GetCustomer.service.ts @@ -1,6 +1,6 @@ import { Inject, Injectable } from '@nestjs/common'; -import { CustomerTransfromer } from '../queries/CustomerTransformer'; +import { CustomerTransfromer } from './CustomerTransformer'; import { TransformerInjectable } from '@/modules/Transformer/TransformerInjectable.service'; import { Customer } from '../models/Customer'; diff --git a/packages/server-nest/src/modules/Customers/queries/GetCustomers.service.ts b/packages/server-nest/src/modules/Customers/queries/GetCustomers.service.ts new file mode 100644 index 000000000..0e605a525 --- /dev/null +++ b/packages/server-nest/src/modules/Customers/queries/GetCustomers.service.ts @@ -0,0 +1,61 @@ +import { DynamicListService } from '@/modules/DynamicListing/DynamicList.service'; +import { TransformerInjectable } from '@/modules/Transformer/TransformerInjectable.service'; +import { Inject, Injectable } from '@nestjs/common'; +import * as R from 'ramda'; +import { Customer } from '../models/Customer'; +import { IFilterMeta, IPaginationMeta } from '@/interfaces/Model'; +import { CustomerTransfromer } from './CustomerTransformer'; + +@Injectable() +export class GetCustomers { + constructor( + private dynamicListService: DynamicListService, + private transformer: TransformerInjectable, + + @Inject(Customer.name) private customerModel: typeof Customer, + ) {} + + /** + * Parses customers list filter DTO. + * @param filterDTO - + */ + private parseCustomersListFilterDTO(filterDTO) { + return R.compose(this.dynamicListService.parseStringifiedFilter)(filterDTO); + } + + /** + * Retrieve customers paginated list. + * @param {ICustomersFilter} filter - Cusotmers filter. + */ + public async getCustomersList(filterDTO: ICustomersFilter): Promise<{ + customers: Customer[]; + pagination: IPaginationMeta; + filterMeta: IFilterMeta; + }> { + // Parses customers list filter DTO. + const filter = this.parseCustomersListFilterDTO(filterDTO); + + const dynamicList = await this.dynamicListService.dynamicList( + Customer, + filter, + ); + const { results, pagination } = await this.customerModel + .query() + .onBuild((builder) => { + dynamicList.buildQuery()(builder); + builder.modify('inactiveMode', filter.inactiveMode); + }) + .pagination(filter.page - 1, filter.pageSize); + + // Retrieves the transformed customers. + const customers = await this.transformer.transform( + results, + new CustomerTransfromer(), + ); + return { + customers, + pagination, + filterMeta: dynamicList.getResponseMeta(), + }; + } +} diff --git a/packages/server-nest/src/modules/DynamicListing/DynamicFilter/DynamicFilter.ts b/packages/server-nest/src/modules/DynamicListing/DynamicFilter/DynamicFilter.ts index ddbb31ce3..2bdf7b515 100644 --- a/packages/server-nest/src/modules/DynamicListing/DynamicFilter/DynamicFilter.ts +++ b/packages/server-nest/src/modules/DynamicListing/DynamicFilter/DynamicFilter.ts @@ -1,17 +1,15 @@ import { forEach } from 'lodash'; import { DynamicFilterAbstractor } from './DynamicFilterAbstractor'; -import { IDynamicFilter, IFilterRole, IModel } from '@/interfaces'; +import { IDynamicFilter, IFilterRole } from './DynamicFilter.types'; import { BaseModel } from '@/models/Model'; +import { DynamicFilterRoleAbstractor } from './DynamicFilterRoleAbstractor'; export class DynamicFilter extends DynamicFilterAbstractor { - private model: BaseModel; - private dynamicFilters: IDynamicFilter[]; - /** * Constructor. * @param {String} tableName - */ - constructor(model: BaseModel) { + constructor(model: typeof BaseModel) { super(); this.model = model; @@ -22,7 +20,7 @@ export class DynamicFilter extends DynamicFilterAbstractor { * Registers the given dynamic filter. * @param {IDynamicFilter} filterRole - Filter role. */ - public setFilter = (dynamicFilter: IDynamicFilter) => { + public setFilter = (dynamicFilter: DynamicFilterRoleAbstractor) => { dynamicFilter.setModel(this.model); dynamicFilter.onInitialize(); diff --git a/packages/server-nest/src/modules/DynamicListing/DynamicFilter/DynamicFilter.types.ts b/packages/server-nest/src/modules/DynamicListing/DynamicFilter/DynamicFilter.types.ts index 2329e2ebf..ac9162d66 100644 --- a/packages/server-nest/src/modules/DynamicListing/DynamicFilter/DynamicFilter.types.ts +++ b/packages/server-nest/src/modules/DynamicListing/DynamicFilter/DynamicFilter.types.ts @@ -1,10 +1,10 @@ import { BaseModel } from '@/models/Model'; -// import { IModel, ISortOrder } from "./Model"; export type ISortOrder = 'DESC' | 'ASC'; export interface IDynamicFilter { - setModel(model: BaseModel): void; + setModel(model: typeof BaseModel): void; + onInitialize(): void; buildQuery(): void; getResponseMeta(); } @@ -20,7 +20,7 @@ export interface IDynamicListFilter { filterRoles?: IFilterRole[]; columnSortBy: ISortOrder; sortOrder: string; - stringifiedFilterRoles: string; + stringifiedFilterRoles?: string; searchKeyword?: string; viewSlug?: string; } diff --git a/packages/server-nest/src/modules/DynamicListing/DynamicFilter/DynamicFilterAbstractor.ts b/packages/server-nest/src/modules/DynamicListing/DynamicFilter/DynamicFilterAbstractor.ts index 7acc8c170..79f23c86f 100644 --- a/packages/server-nest/src/modules/DynamicListing/DynamicFilter/DynamicFilterAbstractor.ts +++ b/packages/server-nest/src/modules/DynamicListing/DynamicFilter/DynamicFilterAbstractor.ts @@ -2,8 +2,8 @@ import { BaseModel } from '@/models/Model'; import { IDynamicFilter } from './DynamicFilter.types'; export class DynamicFilterAbstractor { - model: BaseModel; - dynamicFilters: IDynamicFilter[]; + public model: typeof BaseModel; + public dynamicFilters: IDynamicFilter[]; /** * Extract relation table name from relation. diff --git a/packages/server-nest/src/modules/DynamicListing/DynamicFilter/DynamicFilterAdvancedFilter.ts b/packages/server-nest/src/modules/DynamicListing/DynamicFilter/DynamicFilterAdvancedFilter.ts index e597d07b5..2ed4813e5 100644 --- a/packages/server-nest/src/modules/DynamicListing/DynamicFilter/DynamicFilterAdvancedFilter.ts +++ b/packages/server-nest/src/modules/DynamicListing/DynamicFilter/DynamicFilterAdvancedFilter.ts @@ -2,8 +2,6 @@ import { IFilterRole } from './DynamicFilter.types'; import { DynamicFilterFilterRoles } from './DynamicFilterFilterRoles'; export class DynamicFilterAdvancedFilter extends DynamicFilterFilterRoles { - private filterRoles: IFilterRole[]; - /** * Constructor method. * @param {Array} filterRoles - diff --git a/packages/server-nest/src/modules/DynamicListing/DynamicFilter/DynamicFilterFilterRoles.ts b/packages/server-nest/src/modules/DynamicListing/DynamicFilter/DynamicFilterFilterRoles.ts index 00b70b9db..d3964d9bc 100644 --- a/packages/server-nest/src/modules/DynamicListing/DynamicFilter/DynamicFilterFilterRoles.ts +++ b/packages/server-nest/src/modules/DynamicListing/DynamicFilter/DynamicFilterFilterRoles.ts @@ -1,8 +1,6 @@ -import { DynamicFilterAbstractor } from './DynamicFilterRoleAbstractor'; -import { IFilterRole } from '@/interfaces'; +import { DynamicFilterRoleAbstractor } from './DynamicFilterRoleAbstractor'; -export class DynamicFilterFilterRoles extends DynamicFilterAbstractor { - private filterRoles: IFilterRole[]; +export class DynamicFilterFilterRoles extends DynamicFilterRoleAbstractor { /** * On initialize filter roles. */ @@ -28,14 +26,14 @@ export class DynamicFilterFilterRoles extends DynamicFilterAbstractor { /** * Builds database query of view roles. */ - protected buildQuery() { + public buildQuery() { const logicExpression = this.buildLogicExpression(); return (builder) => { this.buildFilterQuery( this.model, this.filterRoles, - logicExpression + logicExpression, )(builder); }; } diff --git a/packages/server-nest/src/modules/DynamicListing/DynamicFilter/DynamicFilterRoleAbstractor.ts b/packages/server-nest/src/modules/DynamicListing/DynamicFilter/DynamicFilterRoleAbstractor.ts index 2eceaff33..890df56f4 100644 --- a/packages/server-nest/src/modules/DynamicListing/DynamicFilter/DynamicFilterRoleAbstractor.ts +++ b/packages/server-nest/src/modules/DynamicListing/DynamicFilter/DynamicFilterRoleAbstractor.ts @@ -1,18 +1,19 @@ import moment from 'moment'; import * as R from 'ramda'; -import { IFilterRole, IDynamicFilter, } from './DynamicFilter.types'; +import { IFilterRole, IDynamicFilter } from './DynamicFilter.types'; import Parser from '@/libs/logic-evaluation/Parser'; import { Lexer } from '@/libs/logic-evaluation/Lexer'; import DynamicFilterQueryParser from './DynamicFilterQueryParser'; import { COMPARATOR_TYPE, FIELD_TYPE } from './constants'; import { BaseModel } from '@/models/Model'; +import { IMetadataModel } from '../models/MetadataModel'; -export abstract class DynamicFilterAbstractor - implements IDynamicFilter -{ +type MetadataModel = typeof BaseModel & IMetadataModel; + +export abstract class DynamicFilterRoleAbstractor implements IDynamicFilter { protected filterRoles: IFilterRole[] = []; protected tableName: string; - protected model: BaseModel; + protected model: MetadataModel; protected responseMeta: { [key: string]: any } = {}; public relationFields = []; @@ -20,7 +21,7 @@ export abstract class DynamicFilterAbstractor * Sets model the dynamic filter service. * @param {IModel} model */ - public setModel(model: BaseModel) { + public setModel(model: MetadataModel) { this.model = model; this.tableName = model.tableName; } @@ -46,9 +47,9 @@ export abstract class DynamicFilterAbstractor * @return {Function} */ protected buildFilterRolesQuery = ( - model: IModel, + model: typeof BaseModel, roles: IFilterRole[], - logicExpression: string = '' + logicExpression: string = '', ) => { const rolesIndexSet = this.convertRolesMapByIndex(model, roles); @@ -67,7 +68,7 @@ export abstract class DynamicFilterAbstractor /** * Parses the logic expression to base expression. - * @param {string} logicExpression - + * @param {string} logicExpression - * @return {string} */ private parseLogicExpression(logicExpression: string): string { @@ -84,9 +85,9 @@ export abstract class DynamicFilterAbstractor * @param {String} logicExpression - Logic expression. */ protected buildFilterQuery = ( - model: IModel, + model: typeof BaseModel, roles: IFilterRole[], - logicExpression: string + logicExpression: string, ) => { const basicExpression = this.parseLogicExpression(logicExpression); @@ -98,7 +99,7 @@ export abstract class DynamicFilterAbstractor /** * Retrieve relation column of comparator fieldز */ - private getFieldComparatorRelationColumn(field) { + protected getFieldComparatorRelationColumn(field: any): string { const relation = this.model.relationMappings[field.relationKey]; if (relation) { @@ -128,7 +129,7 @@ export abstract class DynamicFilterAbstractor * @param {IModel} model - * @param {Object} role - */ - protected buildRoleQuery = (model: BaseModel, role: IFilterRole) => { + protected buildRoleQuery = (model: MetadataModel, role: IFilterRole) => { const field = model.getField(role.fieldKey); const comparatorColumn = this.getFieldComparatorColumn(field); @@ -160,7 +161,7 @@ export abstract class DynamicFilterAbstractor */ protected booleanRoleQueryBuilder = ( role: IFilterRole, - comparatorColumn: string + comparatorColumn: string, ) => { switch (role.comparator) { case COMPARATOR_TYPE.EQUALS: @@ -187,7 +188,7 @@ export abstract class DynamicFilterAbstractor */ protected numberRoleQueryBuilder = ( role: IFilterRole, - comparatorColumn: string + comparatorColumn: string, ) => { switch (role.comparator) { case COMPARATOR_TYPE.EQUALS: @@ -230,7 +231,7 @@ export abstract class DynamicFilterAbstractor */ protected textRoleQueryBuilder = ( role: IFilterRole, - comparatorColumn: string + comparatorColumn: string, ) => { switch (role.comparator) { case COMPARATOR_TYPE.EQUAL: @@ -266,7 +267,6 @@ export abstract class DynamicFilterAbstractor return (builder) => { builder.where(comparatorColumn, 'LIKE', `%${role.value}`); }; - } }; @@ -278,7 +278,7 @@ export abstract class DynamicFilterAbstractor */ protected dateQueryBuilder = ( role: IFilterRole, - comparatorColumn: string + comparatorColumn: string, ) => { switch (role.comparator) { case COMPARATOR_TYPE.AFTER: @@ -302,12 +302,12 @@ export abstract class DynamicFilterAbstractor protected dateQueryInComparator = ( role: IFilterRole, comparatorColumn: string, - builder + builder, ) => { const hasTimeFormat = moment( role.value, 'YYYY-MM-DD HH:MM', - true + true, ).isValid(); const dateFormat = 'YYYY-MM-DD HH:MM:SS'; @@ -332,13 +332,13 @@ export abstract class DynamicFilterAbstractor protected dateQueryAfterBeforeComparator = ( role: IFilterRole, comparatorColumn: string, - builder + builder, ) => { const comparator = role.comparator === COMPARATOR_TYPE.BEFORE ? '<' : '>'; const hasTimeFormat = moment( role.value, 'YYYY-MM-DD HH:MM', - true + true, ).isValid(); const targetDate = moment(role.value); const dateFormat = 'YYYY-MM-DD HH:MM:SS'; @@ -355,16 +355,14 @@ export abstract class DynamicFilterAbstractor }; /** - * Registers relation field if the given field was relation type - * and not registered. + * Registers relation field if the given field was relation type and not registered. * @param {string} fieldKey - Field key. */ protected setRelationIfRelationField = (fieldKey: string): void => { const field = this.model.getField(fieldKey); const isAlreadyRegistered = this.relationFields.some( - (field) => field === fieldKey + (field) => field === fieldKey, ); - if ( !isAlreadyRegistered && field && @@ -385,4 +383,11 @@ export abstract class DynamicFilterAbstractor * On initialize the registered dynamic filter. */ onInitialize() {} + + buildQuery(): void { + throw new Error('Method not implemented.'); + } + getResponseMeta() { + throw new Error('Method not implemented.'); + } } diff --git a/packages/server-nest/src/modules/DynamicListing/DynamicFilter/DynamicFilterSearch.ts b/packages/server-nest/src/modules/DynamicListing/DynamicFilter/DynamicFilterSearch.ts index 7ea70c29d..1f8975d01 100644 --- a/packages/server-nest/src/modules/DynamicListing/DynamicFilter/DynamicFilterSearch.ts +++ b/packages/server-nest/src/modules/DynamicListing/DynamicFilter/DynamicFilterSearch.ts @@ -3,7 +3,6 @@ import { DynamicFilterFilterRoles } from './DynamicFilterFilterRoles'; export class DynamicFilterSearch extends DynamicFilterFilterRoles { private searchKeyword: string; - private filterRoles: IFilterRole[]; /** * Constructor method. diff --git a/packages/server-nest/src/modules/DynamicListing/DynamicFilter/DynamicFilterSortBy.ts b/packages/server-nest/src/modules/DynamicListing/DynamicFilter/DynamicFilterSortBy.ts index 7fed63a65..c06eea2d1 100644 --- a/packages/server-nest/src/modules/DynamicListing/DynamicFilter/DynamicFilterSortBy.ts +++ b/packages/server-nest/src/modules/DynamicListing/DynamicFilter/DynamicFilterSortBy.ts @@ -1,12 +1,13 @@ import { FIELD_TYPE } from './constants'; import { DynamicFilterAbstractor } from './DynamicFilterAbstractor'; +import { DynamicFilterRoleAbstractor } from './DynamicFilterRoleAbstractor'; interface ISortRole { fieldKey: string; order: string; } -export class DynamicFilterSortBy extends DynamicFilterAbstractor { +export class DynamicFilterSortBy extends DynamicFilterRoleAbstractor { private sortRole: ISortRole = {}; /** @@ -36,7 +37,7 @@ export class DynamicFilterSortBy extends DynamicFilterAbstractor { * @param field * @returns {string} */ - private getFieldComparatorRelationColumn = (field): string => { + protected getFieldComparatorRelationColumn(field: any): string { const relation = this.model.relationMappings[field.relationKey]; if (relation) { @@ -46,7 +47,7 @@ export class DynamicFilterSortBy extends DynamicFilterAbstractor { return `${relationModel.tableName}.${relationField.column}`; } return ''; - }; + } /** * Retrieve the comparator field column. diff --git a/packages/server-nest/src/modules/DynamicListing/DynamicFilter/DynamicFilterViews.ts b/packages/server-nest/src/modules/DynamicListing/DynamicFilter/DynamicFilterViews.ts index 56c976a31..64edf06f5 100644 --- a/packages/server-nest/src/modules/DynamicListing/DynamicFilter/DynamicFilterViews.ts +++ b/packages/server-nest/src/modules/DynamicListing/DynamicFilter/DynamicFilterViews.ts @@ -1,11 +1,9 @@ import { omit } from 'lodash'; -import { IView, IViewRole } from '@/interfaces'; -import { DynamicFilterAbstractor } from './DynamicFilterAbstractor'; +import { DynamicFilterRoleAbstractor } from './DynamicFilterRoleAbstractor'; -export class DynamicFilterViews extends DynamicFilterAbstractor { +export class DynamicFilterViews extends DynamicFilterRoleAbstractor { private viewSlug: string; private logicExpression: string; - private filterRoles: IViewRole[]; private viewColumns = []; /** diff --git a/packages/server-nest/src/modules/DynamicListing/DynamicList.module.ts b/packages/server-nest/src/modules/DynamicListing/DynamicList.module.ts new file mode 100644 index 000000000..b8e8f658b --- /dev/null +++ b/packages/server-nest/src/modules/DynamicListing/DynamicList.module.ts @@ -0,0 +1,18 @@ +import { Module } from '@nestjs/common'; +import { DynamicListService } from './DynamicList.service'; +import { DynamicListCustomView } from './DynamicListCustomView.service'; +import { DynamicListSortBy } from './DynamicListSortBy.service'; +import { DynamicListSearch } from './DynamicListSearch.service'; +import { DynamicListFilterRoles } from './DynamicListFilterRoles.service'; + +@Module({ + providers: [ + DynamicListService, + DynamicListCustomView, + DynamicListSortBy, + DynamicListSearch, + DynamicListFilterRoles, + ], + exports: [DynamicListService], +}) +export class DynamicListModule {} diff --git a/packages/server-nest/src/modules/DynamicListing/DynamicListService.ts b/packages/server-nest/src/modules/DynamicListing/DynamicList.service.ts similarity index 83% rename from packages/server-nest/src/modules/DynamicListing/DynamicListService.ts rename to packages/server-nest/src/modules/DynamicListing/DynamicList.service.ts index e97f27229..bb8b112dc 100644 --- a/packages/server-nest/src/modules/DynamicListing/DynamicListService.ts +++ b/packages/server-nest/src/modules/DynamicListing/DynamicList.service.ts @@ -1,18 +1,15 @@ import { castArray, isEmpty } from 'lodash'; -import { - IDynamicListFilter, - IDynamicListService, -} from './DynamicFilter/DynamicFilter.types'; -import { DynamicListSortBy } from './DynamicListSortBy'; -import { DynamicListSearch } from './DynamicListSearch'; -import { DynamicListCustomView } from './DynamicListCustomView'; +import { IDynamicListFilter } from './DynamicFilter/DynamicFilter.types'; +import { DynamicListSortBy } from './DynamicListSortBy.service'; +import { DynamicListSearch } from './DynamicListSearch.service'; +import { DynamicListCustomView } from './DynamicListCustomView.service'; import { Injectable } from '@nestjs/common'; -import { DynamicListFilterRoles } from './DynamicListFilterRoles'; +import { DynamicListFilterRoles } from './DynamicListFilterRoles.service'; import { DynamicFilter } from './DynamicFilter'; import { BaseModel } from '@/models/Model'; @Injectable() -export class DynamicListService implements IDynamicListService { +export class DynamicListService { constructor( private dynamicListFilterRoles: DynamicListFilterRoles, private dynamicListSearch: DynamicListSearch, @@ -44,7 +41,10 @@ export class DynamicListService implements IDynamicListService { * @param {IModel} model - Model. * @param {IDynamicListFilter} filter - Dynamic filter DTO. */ - public dynamicList = async (model: BaseModel, filter: IDynamicListFilter) => { + public dynamicList = async ( + model: typeof BaseModel, + filter: IDynamicListFilter, + ) => { const dynamicFilter = new DynamicFilter(model); // Parses the filter object. @@ -90,7 +90,9 @@ export class DynamicListService implements IDynamicListService { * Parses stringified filter roles. * @param {string} stringifiedFilterRoles - Stringified filter roles. */ - public parseStringifiedFilter = (filterRoles: IDynamicListFilter) => { + public parseStringifiedFilter( + filterRoles: T, + ): T { return { ...filterRoles, filterRoles: filterRoles.stringifiedFilterRoles diff --git a/packages/server-nest/src/modules/DynamicListing/DynamicListAbstract.ts b/packages/server-nest/src/modules/DynamicListing/DynamicListAbstract.ts deleted file mode 100644 index 37a98d0b4..000000000 --- a/packages/server-nest/src/modules/DynamicListing/DynamicListAbstract.ts +++ /dev/null @@ -1 +0,0 @@ -export class DynamicListAbstract {} diff --git a/packages/server-nest/src/modules/DynamicListing/DynamicListCustomView.ts b/packages/server-nest/src/modules/DynamicListing/DynamicListCustomView.service.ts similarity index 85% rename from packages/server-nest/src/modules/DynamicListing/DynamicListCustomView.ts rename to packages/server-nest/src/modules/DynamicListing/DynamicListCustomView.service.ts index 5dcbdddd3..f8f04db1b 100644 --- a/packages/server-nest/src/modules/DynamicListing/DynamicListCustomView.ts +++ b/packages/server-nest/src/modules/DynamicListing/DynamicListCustomView.service.ts @@ -1,12 +1,12 @@ import { Injectable } from '@nestjs/common'; -import { DynamicListAbstract } from './DynamicListAbstract'; import { ERRORS } from './constants'; import { DynamicFilterViews } from './DynamicFilter'; import { ServiceError } from '../Items/ServiceError'; import { BaseModel } from '@/models/Model'; +import { DynamicListServiceAbstract } from './DynamicListServiceAbstract'; @Injectable() -export class DynamicListCustomView extends DynamicListAbstract { +export class DynamicListCustomView extends DynamicListServiceAbstract { /** * Retreive custom view or throws error not found. * @param {number} tenantId @@ -30,7 +30,7 @@ export class DynamicListCustomView extends DynamicListAbstract { * Dynamic list custom view. * @param {IModel} model * @param {number} customViewId - * @returns + * @returns {DynamicFilterRoleAbstractor} */ public dynamicListCustomView = async ( dynamicFilter: any, @@ -40,6 +40,7 @@ export class DynamicListCustomView extends DynamicListAbstract { // Retrieve the custom view or throw not found. const view = await this.getCustomViewOrThrowError(customViewSlug, model); + return new DynamicFilterViews(view); }; } diff --git a/packages/server-nest/src/modules/DynamicListing/DynamicListFilterRoles.ts b/packages/server-nest/src/modules/DynamicListing/DynamicListFilterRoles.service.ts similarity index 94% rename from packages/server-nest/src/modules/DynamicListing/DynamicListFilterRoles.ts rename to packages/server-nest/src/modules/DynamicListing/DynamicListFilterRoles.service.ts index 681179ebe..0058046eb 100644 --- a/packages/server-nest/src/modules/DynamicListing/DynamicListFilterRoles.ts +++ b/packages/server-nest/src/modules/DynamicListing/DynamicListFilterRoles.service.ts @@ -2,14 +2,14 @@ import * as R from 'ramda'; import { Injectable } from '@nestjs/common'; import validator from 'is-my-json-valid'; import { IFilterRole } from './DynamicFilter/DynamicFilter.types'; -import { DynamicListAbstract } from './DynamicListAbstract'; import { DynamicFilterAdvancedFilter } from './DynamicFilter/DynamicFilterAdvancedFilter'; import { ERRORS } from './constants'; import { ServiceError } from '../Items/ServiceError'; import { BaseModel } from '@/models/Model'; +import { DynamicFilterRoleAbstractor } from './DynamicFilter/DynamicFilterRoleAbstractor'; @Injectable() -export class DynamicListFilterRoles extends DynamicListAbstract { +export class DynamicListFilterRoles extends DynamicFilterRoleAbstractor { /** * Validates filter roles schema. * @param {IFilterRole[]} filterRoles - Filter roles. diff --git a/packages/server-nest/src/modules/DynamicListing/DynamicListSearch.ts b/packages/server-nest/src/modules/DynamicListing/DynamicListSearch.service.ts similarity index 72% rename from packages/server-nest/src/modules/DynamicListing/DynamicListSearch.ts rename to packages/server-nest/src/modules/DynamicListing/DynamicListSearch.service.ts index d4d620742..fac4ebe39 100644 --- a/packages/server-nest/src/modules/DynamicListing/DynamicListSearch.ts +++ b/packages/server-nest/src/modules/DynamicListing/DynamicListSearch.service.ts @@ -1,9 +1,9 @@ import { Injectable } from '@nestjs/common'; -import { DynamicListAbstract } from './DynamicListAbstract'; import { DynamicFilterSearch } from './DynamicFilter/DynamicFilterSearch'; +import { DynamicListServiceAbstract } from './DynamicListServiceAbstract'; @Injectable() -export class DynamicListSearch extends DynamicListAbstract { +export class DynamicListSearch extends DynamicListServiceAbstract { /** * Dynamic list filter roles. * @param {string} searchKeyword - Search keyword. diff --git a/packages/server-nest/src/modules/DynamicListing/DynamicListServiceAbstract.ts b/packages/server-nest/src/modules/DynamicListing/DynamicListServiceAbstract.ts new file mode 100644 index 000000000..3b83f6f19 --- /dev/null +++ b/packages/server-nest/src/modules/DynamicListing/DynamicListServiceAbstract.ts @@ -0,0 +1 @@ +export class DynamicListServiceAbstract {} diff --git a/packages/server-nest/src/modules/DynamicListing/DynamicListSortBy.ts b/packages/server-nest/src/modules/DynamicListing/DynamicListSortBy.service.ts similarity index 87% rename from packages/server-nest/src/modules/DynamicListing/DynamicListSortBy.ts rename to packages/server-nest/src/modules/DynamicListing/DynamicListSortBy.service.ts index 734238070..5bf5c3c1b 100644 --- a/packages/server-nest/src/modules/DynamicListing/DynamicListSortBy.ts +++ b/packages/server-nest/src/modules/DynamicListing/DynamicListSortBy.service.ts @@ -1,13 +1,13 @@ import { Injectable } from '@nestjs/common'; -import { DynamicListAbstract } from './DynamicListAbstract'; import { ISortOrder } from './DynamicFilter/DynamicFilter.types'; import { ERRORS } from './constants'; import { DynamicFilterSortBy } from './DynamicFilter'; import { ServiceError } from '../Items/ServiceError'; import { BaseModel } from '@/models/Model'; +import { DynamicFilterRoleAbstractor } from './DynamicFilter/DynamicFilterRoleAbstractor'; @Injectable() -export class DynamicListSortBy extends DynamicListAbstract { +export class DynamicListSortBy extends DynamicFilterRoleAbstractor { /** * Dynamic list sort by. * @param {BaseModel} model diff --git a/packages/server-nest/src/modules/DynamicListing/models/CustomViewBaseModel.ts b/packages/server-nest/src/modules/DynamicListing/models/CustomViewBaseModel.ts new file mode 100644 index 000000000..8c0b2b1e7 --- /dev/null +++ b/packages/server-nest/src/modules/DynamicListing/models/CustomViewBaseModel.ts @@ -0,0 +1,26 @@ +import { BaseModel } from '@/models/Model'; + +export const CustomViewBaseModel = (Model: typeof BaseModel) => + class extends Model { + /** + * Retrieve the default custom views, roles and columns. + */ + static get defaultViews() { + return []; + } + + /** + * Retrieve the default view by the given slug. + */ + static getDefaultViewBySlug(viewSlug) { + return this.defaultViews.find((view) => view.slug === viewSlug) || null; + } + + /** + * Retrieve the default views. + * @returns {IView[]} + */ + static getDefaultViews() { + return this.defaultViews; + } + }; diff --git a/packages/server-nest/src/modules/DynamicListing/models/MetadataModel.ts b/packages/server-nest/src/modules/DynamicListing/models/MetadataModel.ts new file mode 100644 index 000000000..754237aad --- /dev/null +++ b/packages/server-nest/src/modules/DynamicListing/models/MetadataModel.ts @@ -0,0 +1,93 @@ +import { get } from 'lodash'; +import { + IModelMeta, + IModelMetaField, + IModelMetaDefaultSort, +} from '@/interfaces/Model'; +import { BaseModel } from '@/models/Model'; + +const defaultModelMeta = { + fields: {}, + fields2: {}, +}; + +export interface IMetadataModel extends BaseModel { + meta: IModelMeta; + parsedMeta: IModelMeta; + fields: { [key: string]: IModelMetaField }; + defaultSort: IModelMetaDefaultSort; + defaultFilterField: string; + + getField(key: string, attribute?: string): IModelMetaField; + getMeta(key?: string): IModelMeta; +} + +type GConstructor = new (...args: any[]) => T; + +export const MetadataModelMixin = >( + Model: T, +) => + class ModelSettings extends Model { + /** + * Retrieve the model meta. + * @returns {IModelMeta} + */ + static get meta(): IModelMeta { + throw new Error(''); + } + + /** + * Parsed meta merged with default emta. + * @returns {IModelMeta} + */ + static get parsedMeta(): IModelMeta { + return { + ...defaultModelMeta, + ...this.meta, + }; + } + + /** + * Retrieve specific model field meta of the given field key. + * @param {string} key + * @returns {IModelMetaField} + */ + public static getField(key: string, attribute?: string): IModelMetaField { + const field = get(this.meta.fields, key); + + return attribute ? get(field, attribute) : field; + } + + /** + * Retrieves the specific model meta. + * @param {string} key + * @returns + */ + public static getMeta(key?: string) { + return key ? get(this.parsedMeta, key) : this.parsedMeta; + } + + /** + * Retrieve the model meta fields. + * @return {{ [key: string]: IModelMetaField }} + */ + public static get fields(): { [key: string]: IModelMetaField } { + return this.getMeta('fields'); + } + + /** + * Retrieve the model default sort settings. + * @return {IModelMetaDefaultSort} + */ + public static get defaultSort(): IModelMetaDefaultSort { + return this.getMeta('defaultSort'); + } + + /** + * Retrieve the default filter field key. + * @return {string} + */ + public static get defaultFilterField(): string { + return this.getMeta('defaultFilterField'); + } + }; diff --git a/packages/server-nest/src/modules/DynamicListing/models/SearchableBaseModel.ts b/packages/server-nest/src/modules/DynamicListing/models/SearchableBaseModel.ts new file mode 100644 index 000000000..d93d83f57 --- /dev/null +++ b/packages/server-nest/src/modules/DynamicListing/models/SearchableBaseModel.ts @@ -0,0 +1,24 @@ +import { BaseModel } from '@/models/Model'; +import { IModelMeta } from '@/interfaces/Model'; +import { ISearchRole } from '../DynamicFilter.types'; + +type GConstructor = new (...args: any[]) => T; + +export const SearchableBaseModelMixin = >( + Model: T, +) => + class SearchableBaseModel extends Model { + /** + * Searchable model. + */ + static get searchable(): IModelMeta { + throw true; + } + + /** + * Search roles. + */ + static get searchRoles(): ISearchRole[] { + return []; + } + }; diff --git a/packages/server-nest/src/modules/DynamicListing/types/DynamicList.types.ts b/packages/server-nest/src/modules/DynamicListing/types/DynamicList.types.ts new file mode 100644 index 000000000..ae4a255f6 --- /dev/null +++ b/packages/server-nest/src/modules/DynamicListing/types/DynamicList.types.ts @@ -0,0 +1,11 @@ +import { ISortOrder } from '@/interfaces/Model'; +import { IFilterRole } from '../DynamicFilter/DynamicFilter.types'; + +export interface IDynamicListFilter { + customViewId?: number; + filterRoles?: IFilterRole[]; + columnSortBy: ISortOrder; + sortOrder: string; + stringifiedFilterRoles: string; + searchKeyword?: string; +} \ No newline at end of file diff --git a/packages/server-nest/src/modules/Expenses/Expenses.controller.ts b/packages/server-nest/src/modules/Expenses/Expenses.controller.ts index c1c36b540..be5e0b725 100644 --- a/packages/server-nest/src/modules/Expenses/Expenses.controller.ts +++ b/packages/server-nest/src/modules/Expenses/Expenses.controller.ts @@ -6,6 +6,7 @@ import { Param, Post, Put, + Query, } from '@nestjs/common'; import { ExpensesApplication } from './ExpensesApplication.service'; import { @@ -13,6 +14,7 @@ import { IExpenseEditDTO, } from './interfaces/Expenses.interface'; import { PublicRoute } from '../Auth/Jwt.guard'; +import { IExpensesFilter } from './Expenses.types'; @Controller('expenses') @PublicRoute() @@ -59,6 +61,14 @@ export class ExpensesController { return this.expensesApplication.publishExpense(expenseId); } + /** + * Get the expense transaction details. + */ + @Get('') + public getExpenses(@Query() filterDTO: IExpensesFilter) { + return this.expensesApplication.getExpenses(filterDTO); + } + /** * Get the expense transaction details. * @param {number} expenseId diff --git a/packages/server-nest/src/modules/Expenses/Expenses.module.ts b/packages/server-nest/src/modules/Expenses/Expenses.module.ts index 69f8c59f2..d54e2c257 100644 --- a/packages/server-nest/src/modules/Expenses/Expenses.module.ts +++ b/packages/server-nest/src/modules/Expenses/Expenses.module.ts @@ -15,6 +15,7 @@ import { ExpenseGLEntriesStorageService } from './subscribers/ExpenseGLEntriesSt import { ExpenseGLEntriesService } from './subscribers/ExpenseGLEntries.service'; import { LedgerModule } from '../Ledger/Ledger.module'; import { BranchesModule } from '../Branches/Branches.module'; +import { GetExpensesService } from './queries/GetExpenses.service'; @Module({ imports: [LedgerModule, BranchesModule], @@ -35,7 +36,9 @@ import { BranchesModule } from '../Branches/Branches.module'; TransformerInjectable, ExpensesWriteGLSubscriber, ExpenseGLEntriesStorageService, - ExpenseGLEntriesService + ExpenseGLEntriesService, + GetExpensesService, ], }) export class ExpensesModule {} + diff --git a/packages/server-nest/src/modules/Expenses/ExpensesApplication.service.ts b/packages/server-nest/src/modules/Expenses/ExpensesApplication.service.ts index aea04f129..9d8b21487 100644 --- a/packages/server-nest/src/modules/Expenses/ExpensesApplication.service.ts +++ b/packages/server-nest/src/modules/Expenses/ExpensesApplication.service.ts @@ -7,7 +7,9 @@ import { GetExpenseService } from './queries/GetExpense.service'; import { IExpenseCreateDTO, IExpenseEditDTO, + IExpensesFilter, } from './interfaces/Expenses.interface'; +import { GetExpensesService } from './queries/GetExpenses.service'; @Injectable() export class ExpensesApplication { @@ -17,7 +19,7 @@ export class ExpensesApplication { private readonly deleteExpenseService: DeleteExpense, private readonly publishExpenseService: PublishExpense, private readonly getExpenseService: GetExpenseService, - // private readonly getExpensesService: GetExpenseService, + private readonly getExpensesService: GetExpensesService, ) {} /** @@ -66,12 +68,11 @@ export class ExpensesApplication { return this.getExpenseService.getExpense(expenseId); }; - // /** - // * Retrieve expenses paginated list. - // * @param {number} tenantId - // * @param {IExpensesFilter} expensesFilter - // */ - // public getExpenses = (tenantId: number, filterDTO: IExpensesFilter) => { - // return this.getExpensesService.getExpensesList(tenantId, filterDTO); - // }; + /** + * Retrieve expenses paginated list. + * @param {IExpensesFilter} expensesFilter + */ + public getExpenses = (filterDTO: IExpensesFilter) => { + return this.getExpensesService.getExpensesList(filterDTO); + }; } diff --git a/packages/server-nest/src/modules/Expenses/queries/GetExpenses.service.ts b/packages/server-nest/src/modules/Expenses/queries/GetExpenses.service.ts index b6e9872bc..81b1e680e 100644 --- a/packages/server-nest/src/modules/Expenses/queries/GetExpenses.service.ts +++ b/packages/server-nest/src/modules/Expenses/queries/GetExpenses.service.ts @@ -1,81 +1,70 @@ -// import { Service, Inject } from 'typedi'; -// import * as R from 'ramda'; -// import { -// IExpensesFilter, -// IExpense, -// IPaginationMeta, -// IFilterMeta, -// } from '@/interfaces'; -// import DynamicListingService from '@/services/DynamicListing/DynamicListService'; -// import HasTenancyService from '@/services/Tenancy/TenancyService'; -// import { ExpenseTransfromer } from './Expense.transformer'; -// import { TransformerInjectable } from '@/lib/Transformer/TransformerInjectable'; +import * as R from 'ramda'; +import { ExpenseTransfromer } from './Expense.transformer'; +import { DynamicListService } from '@/modules/DynamicListing/DynamicList.service'; +import { TransformerInjectable } from '@/modules/Transformer/TransformerInjectable.service'; +import { Inject, Injectable } from '@nestjs/common'; +import { IExpensesFilter, IPaginationMeta } from '../Expenses.types'; +import { Expense } from '../models/Expense.model'; +import { IFilterMeta } from '@/interfaces/Model'; -// @Service() -// export class GetExpenses { -// @Inject() -// private dynamicListService: DynamicListingService; +@Injectable() +export class GetExpensesService { + constructor( + private readonly transformer: TransformerInjectable, + private readonly dynamicListService: DynamicListService, -// @Inject() -// private tenancy: HasTenancyService; + @Inject(Expense.name) + private readonly expense: typeof Expense, + ) {} -// @Inject() -// private transformer: TransformerInjectable; + /** + * Retrieve expenses paginated list. + * @param {IExpensesFilter} expensesFilter + * @return {IExpense[]} + */ + public async getExpensesList( + filterDTO: IExpensesFilter + ): Promise<{ + expenses: Expense[]; + pagination: IPaginationMeta; + filterMeta: IFilterMeta; + }> { + // Parses list filter DTO. + const filter = this.parseListFilterDTO(filterDTO); -// /** -// * Retrieve expenses paginated list. -// * @param {number} tenantId -// * @param {IExpensesFilter} expensesFilter -// * @return {IExpense[]} -// */ -// public getExpensesList = async ( -// tenantId: number, -// filterDTO: IExpensesFilter -// ): Promise<{ -// expenses: IExpense[]; -// pagination: IPaginationMeta; -// filterMeta: IFilterMeta; -// }> => { -// const { Expense } = this.tenancy.models(tenantId); + // Dynamic list service. + const dynamicList = await this.dynamicListService.dynamicList( + this.expense, + filter + ); + // Retrieves the paginated results. + const { results, pagination } = await this.expense.query() + .onBuild((builder) => { + builder.withGraphFetched('paymentAccount'); + builder.withGraphFetched('categories.expenseAccount'); -// // Parses list filter DTO. -// const filter = this.parseListFilterDTO(filterDTO); + dynamicList.buildQuery()(builder); + filterDTO?.filterQuery && filterDTO?.filterQuery(builder); + }) + .pagination(filter.page - 1, filter.pageSize); -// // Dynamic list service. -// const dynamicList = await this.dynamicListService.dynamicList( -// tenantId, -// Expense, -// filter -// ); -// // Retrieves the paginated results. -// const { results, pagination } = await Expense.query() -// .onBuild((builder) => { -// builder.withGraphFetched('paymentAccount'); -// builder.withGraphFetched('categories.expenseAccount'); + // Transformes the expenses models to POJO. + const expenses = await this.transformer.transform( + results, + new ExpenseTransfromer() + ); + return { + expenses, + pagination, + filterMeta: dynamicList.getResponseMeta(), + }; + }; -// dynamicList.buildQuery()(builder); -// filterDTO?.filterQuery && filterDTO?.filterQuery(builder); -// }) -// .pagination(filter.page - 1, filter.pageSize); - -// // Transformes the expenses models to POJO. -// const expenses = await this.transformer.transform( -// tenantId, -// results, -// new ExpenseTransfromer() -// ); -// return { -// expenses, -// pagination, -// filterMeta: dynamicList.getResponseMeta(), -// }; -// }; - -// /** -// * Parses filter DTO of expenses list. -// * @param filterDTO - -// */ -// private parseListFilterDTO(filterDTO) { -// return R.compose(this.dynamicListService.parseStringifiedFilter)(filterDTO); -// } -// } + /** + * Parses filter DTO of expenses list. + * @param filterDTO - + */ + private parseListFilterDTO(filterDTO) { + return R.compose(this.dynamicListService.parseStringifiedFilter)(filterDTO); + } +} diff --git a/packages/server-nest/src/modules/InventoryAdjutments/InventoryAdjustments.module.ts b/packages/server-nest/src/modules/InventoryAdjutments/InventoryAdjustments.module.ts index 9c9dd1003..4cec5d892 100644 --- a/packages/server-nest/src/modules/InventoryAdjutments/InventoryAdjustments.module.ts +++ b/packages/server-nest/src/modules/InventoryAdjutments/InventoryAdjustments.module.ts @@ -15,6 +15,8 @@ import { InventoryAdjustmentsGLSubscriber } from './subscribers/InventoryAdjustm import { InventoryAdjustmentsGLEntries } from './commands/ledger/InventoryAdjustmentsGLEntries'; import { LedgerModule } from '../Ledger/Ledger.module'; import { TenancyContext } from '../Tenancy/TenancyContext.service'; +import { InventoryAdjustmentInventoryTransactionsSubscriber } from './inventory/InventoryAdjustmentInventoryTransactionsSubscriber'; +import { InventoryAdjustmentInventoryTransactions } from './inventory/InventoryAdjustmentInventoryTransactions'; const models = [ RegisterTenancyModel(InventoryAdjustment), @@ -34,6 +36,8 @@ const models = [ InventoryAdjustmentsGLSubscriber, InventoryAdjustmentsGLEntries, TenancyContext, + InventoryAdjustmentInventoryTransactionsSubscriber, + InventoryAdjustmentInventoryTransactions ], exports: [...models], }) diff --git a/packages/server-nest/src/modules/InventoryAdjutments/inventory/InventoryAdjustmentInventoryTransactions.ts b/packages/server-nest/src/modules/InventoryAdjutments/inventory/InventoryAdjustmentInventoryTransactions.ts new file mode 100644 index 000000000..d9c69cc6b --- /dev/null +++ b/packages/server-nest/src/modules/InventoryAdjutments/inventory/InventoryAdjustmentInventoryTransactions.ts @@ -0,0 +1,70 @@ +import { Knex } from "knex"; +import { InventoryAdjustment } from "../models/InventoryAdjustment"; +import { InventoryTransaction } from "@/modules/InventoryCost/models/InventoryTransaction"; +import { InventoryService } from "@/modules/InventoryCost/Inventory"; +import { Injectable } from "@nestjs/common"; + +@Injectable() +export class InventoryAdjustmentInventoryTransactions { + constructor( + private readonly inventoryService: InventoryService + ) {} + + /** + * Writes the inventory transactions from the inventory adjustment transaction. + * @param {number} tenantId - + * @param {IInventoryAdjustment} inventoryAdjustment - + * @param {boolean} override - + * @param {Knex.Transaction} trx - + * @return {Promise} + */ + public async writeInventoryTransactions( + inventoryAdjustment: InventoryAdjustment, + override: boolean = false, + trx?: Knex.Transaction + ): Promise { + const commonTransaction = { + direction: inventoryAdjustment.inventoryDirection, + date: inventoryAdjustment.date, + transactionType: 'InventoryAdjustment', + transactionId: inventoryAdjustment.id, + createdAt: inventoryAdjustment.createdAt, + costAccountId: inventoryAdjustment.adjustmentAccountId, + + branchId: inventoryAdjustment.branchId, + warehouseId: inventoryAdjustment.warehouseId, + }; + const inventoryTransactions = []; + + inventoryAdjustment.entries.forEach((entry) => { + inventoryTransactions.push({ + ...commonTransaction, + itemId: entry.itemId, + quantity: entry.quantity, + rate: entry.cost, + }); + }); + // Saves the given inventory transactions to the storage. + await this.inventoryService.recordInventoryTransactions( + inventoryTransactions, + override, + trx + ); + } + + /** + * Reverts the inventory transactions from the inventory adjustment transaction. + * @param {number} inventoryAdjustmentId + */ + async revertInventoryTransactions( + inventoryAdjustmentId: number, + trx?: Knex.Transaction + ): Promise<{ oldInventoryTransactions: InventoryTransaction[] }> { + return this.inventoryService.deleteInventoryTransactions( + inventoryAdjustmentId, + 'InventoryAdjustment', + trx + ); + } + +} \ No newline at end of file diff --git a/packages/server-nest/src/modules/InventoryAdjutments/inventory/InventoryAdjustmentInventoryTransactionsSubscriber.ts b/packages/server-nest/src/modules/InventoryAdjutments/inventory/InventoryAdjustmentInventoryTransactionsSubscriber.ts new file mode 100644 index 000000000..dfc7b64ff --- /dev/null +++ b/packages/server-nest/src/modules/InventoryAdjutments/inventory/InventoryAdjustmentInventoryTransactionsSubscriber.ts @@ -0,0 +1,56 @@ +import { Injectable } from '@nestjs/common'; +import { + IInventoryAdjustmentEventCreatedPayload, + IInventoryAdjustmentEventPublishedPayload, +} from '../types/InventoryAdjustments.types'; +import { IInventoryAdjustmentEventDeletedPayload } from '../types/InventoryAdjustments.types'; +import { InventoryAdjustmentInventoryTransactions } from './InventoryAdjustmentInventoryTransactions'; +import { events } from '@/common/events/events'; +import { OnEvent } from '@nestjs/event-emitter'; + +@Injectable() +export class InventoryAdjustmentInventoryTransactionsSubscriber { + constructor( + private readonly inventoryTransactions: InventoryAdjustmentInventoryTransactions, + ) {} + + /** + * Handles writing inventory transactions once the quick adjustment created. + * @param {IInventoryAdjustmentEventPublishedPayload} payload + * @param {IInventoryAdjustmentEventCreatedPayload} payload - + */ + @OnEvent(events.inventoryAdjustment.onQuickCreated) + public async handleWriteInventoryTransactionsOncePublished({ + inventoryAdjustment, + trx, + }: + | IInventoryAdjustmentEventPublishedPayload + | IInventoryAdjustmentEventCreatedPayload) { + await this.inventoryTransactions.writeInventoryTransactions( + inventoryAdjustment, + false, + trx, + ); + } + + /** + * Handles reverting invetory transactions once the inventory adjustment deleted. + * @param {IInventoryAdjustmentEventDeletedPayload} payload - + */ + @OnEvent(events.inventoryAdjustment.onDeleted) + public async handleRevertInventoryTransactionsOnceDeleted({ + inventoryAdjustmentId, + oldInventoryAdjustment, + trx, + }: IInventoryAdjustmentEventDeletedPayload) { + // Can't continue if the inventory adjustment is not published. + if (!oldInventoryAdjustment.isPublished) { + return; + } + // Reverts the inventory transactions of adjustment transaction. + await this.inventoryTransactions.revertInventoryTransactions( + inventoryAdjustmentId, + trx, + ); + } +} diff --git a/packages/server-nest/src/modules/InventoryCost/Inventory.ts b/packages/server-nest/src/modules/InventoryCost/Inventory.ts new file mode 100644 index 000000000..7b4352f08 --- /dev/null +++ b/packages/server-nest/src/modules/InventoryCost/Inventory.ts @@ -0,0 +1,366 @@ +import { pick } from 'lodash'; +import { Inject, Injectable } from '@nestjs/common'; +import { EventEmitter2 } from '@nestjs/event-emitter'; +import { Knex } from 'knex'; +import { + IInventoryLotCost, + IInventoryTransaction, + TInventoryTransactionDirection, + IItemEntry, + IItemEntryTransactionType, + IInventoryTransactionsCreatedPayload, + IInventoryTransactionsDeletedPayload, + IInventoryItemCostScheduledPayload, +} from '@/interfaces'; +import { InventoryAverageCostMethod } from './InventoryAverageCost'; +import { InventoryCostLotTracker } from './InventoryCostLotTracker'; +import { ItemsEntriesService } from '../Items/ItemsEntries.service'; +import { UnitOfWork } from '../Tenancy/TenancyDB/UnitOfWork.service'; +import { Item } from '../Items/models/Item'; +import { SETTINGS_PROVIDER } from '../Settings/Settings.types'; +import { SettingsStore } from '../Settings/SettingsStore'; +import { events } from '@/common/events/events'; +import { InventoryTransaction } from './models/InventoryTransaction'; +import InventoryCostMethod from './InventoryCostMethod'; + +@Injectable() +export class InventoryService { + constructor( + private readonly eventEmitter: EventEmitter2, + private readonly itemsEntriesService: ItemsEntriesService, + private readonly uow: UnitOfWork, + + @Inject(Item.name) + private readonly itemModel: typeof Item, + + @Inject(InventoryTransaction.name) + private readonly inventoryTransactionModel: typeof InventoryTransaction, + + @Inject(InventoryCostLotTracker.name) + private readonly inventoryCostLotTracker: typeof InventoryCostLotTracker, + + @Inject(SETTINGS_PROVIDER) + private readonly settings: SettingsStore, + ) {} + + /** + * Transforms the items entries to inventory transactions. + */ + transformItemEntriesToInventory(transaction: { + transactionId: number; + transactionType: IItemEntryTransactionType; + transactionNumber?: string; + + exchangeRate?: number; + + warehouseId: number | null; + + date: Date | string; + direction: TInventoryTransactionDirection; + entries: IItemEntry[]; + createdAt: Date; + }): IInventoryTransaction[] { + const exchangeRate = transaction.exchangeRate || 1; + + return transaction.entries.map((entry: IItemEntry) => ({ + ...pick(entry, ['itemId', 'quantity']), + rate: entry.rate * exchangeRate, + transactionType: transaction.transactionType, + transactionId: transaction.transactionId, + direction: transaction.direction, + date: transaction.date, + entryId: entry.id, + createdAt: transaction.createdAt, + costAccountId: entry.costAccountId, + + warehouseId: entry.warehouseId || transaction.warehouseId, + meta: { + transactionNumber: transaction.transactionNumber, + description: entry.description, + }, + })); + } + + async computeItemCost(fromDate: Date, itemId: number) { + return this.uow.withTransaction((trx: Knex.Transaction) => { + return this.computeInventoryItemCost(fromDate, itemId); + }); + } + + /** + * Computes the given item cost and records the inventory lots transactions + * and journal entries based on the cost method FIFO, LIFO or average cost rate. + * @param {Date} fromDate - From date. + * @param {number} itemId - Item id. + */ + async computeInventoryItemCost( + fromDate: Date, + itemId: number, + trx?: Knex.Transaction, + ) { + // Fetches the item with associated item category. + const item = await Item.query().findById(itemId); + + // Cannot continue if the given item was not inventory item. + if (item.type !== 'inventory') { + throw new Error('You could not compute item cost has no inventory type.'); + } + let costMethodComputer: InventoryCostMethod; + + // Switch between methods based on the item cost method. + switch ('AVG') { + case 'FIFO': + case 'LIFO': + costMethodComputer = new InventoryCostLotTracker( + tenantId, + fromDate, + itemId, + ); + break; + case 'AVG': + costMethodComputer = new InventoryAverageCostMethod( + fromDate, + itemId, + trx, + ); + break; + } + return costMethodComputer.computeItemCost(); + } + + /** + * Schedule item cost compute job. + * @param {number} tenantId + * @param {number} itemId + * @param {Date} startingDate + */ + async scheduleComputeItemCost( + tenantId: number, + itemId: number, + startingDate: Date | string, + ) { + const agenda = Container.get('agenda'); + + const commonJobsQuery = { + name: 'compute-item-cost', + lastRunAt: { $exists: false }, + 'data.tenantId': tenantId, + 'data.itemId': itemId, + }; + // Cancel any `compute-item-cost` in the queue has upper starting date + // with the same given item. + await agenda.cancel({ + ...commonJobsQuery, + 'data.startingDate': { $lte: startingDate }, + }); + // Retrieve any `compute-item-cost` in the queue has lower starting date + // with the same given item. + const dependsJobs = await agenda.jobs({ + ...commonJobsQuery, + 'data.startingDate': { $gte: startingDate }, + }); + // If the depends jobs cleared. + if (dependsJobs.length === 0) { + await agenda.schedule( + config.scheduleComputeItemCost, + 'compute-item-cost', + { + startingDate, + itemId, + tenantId, + }, + ); + // Triggers `onComputeItemCostJobScheduled` event. + await this.eventPublisher.emitAsync( + events.inventory.onComputeItemCostJobScheduled, + { + startingDate, + itemId, + tenantId, + } as IInventoryItemCostScheduledPayload, + ); + } else { + // Re-schedule the jobs that have higher date from current moment. + await Promise.all( + dependsJobs.map((job) => + job.schedule(config.scheduleComputeItemCost).save(), + ), + ); + } + } + + /** + * Records the inventory transactions. + * @param {number} tenantId - Tenant id. + * @param {Bill} bill - Bill model object. + * @param {number} billId - Bill id. + * @return {Promise} + */ + async recordInventoryTransactions( + transactions: IInventoryTransaction[], + override: boolean = false, + trx?: Knex.Transaction, + ): Promise { + const bulkInsertOpers = []; + + transactions.forEach((transaction: IInventoryTransaction) => { + const oper = this.recordInventoryTransaction(transaction, override, trx); + bulkInsertOpers.push(oper); + }); + const inventoryTransactions = await Promise.all(bulkInsertOpers); + + // Triggers `onInventoryTransactionsCreated` event. + await this.eventEmitter.emitAsync( + events.inventory.onInventoryTransactionsCreated, + { + inventoryTransactions, + trx, + } as IInventoryTransactionsCreatedPayload, + ); + } + + /** + * Writes the inventory transactiosn on the storage from the given + * inventory transactions entries. + * + * @param {number} tenantId - + * @param {IInventoryTransaction} inventoryEntry - + * @param {boolean} deleteOld - + */ + async recordInventoryTransaction( + inventoryEntry: IInventoryTransaction, + deleteOld: boolean = false, + trx: Knex.Transaction, + ): Promise { + if (deleteOld) { + await this.deleteInventoryTransactions( + inventoryEntry.transactionId, + inventoryEntry.transactionType, + trx, + ); + } + return this.inventoryTransactionModel.query(trx).insertGraph({ + ...inventoryEntry, + }); + } + + /** + * Records the inventory transactions from items entries that have (inventory) type. + * + * @param {number} tenantId + * @param {number} transactionId + * @param {string} transactionType + * @param {Date|string} transactionDate + * @param {boolean} override + */ + async recordInventoryTransactionsFromItemsEntries( + transaction: { + transactionId: number; + transactionType: IItemEntryTransactionType; + exchangeRate: number; + + date: Date | string; + direction: TInventoryTransactionDirection; + entries: IItemEntry[]; + createdAt: Date | string; + + warehouseId: number; + }, + override: boolean = false, + trx?: Knex.Transaction, + ): Promise { + // Can't continue if there is no entries has inventory items in the invoice. + if (transaction.entries.length <= 0) { + return; + } + // Inventory transactions. + const inventoryTranscations = + this.transformItemEntriesToInventory(transaction); + + // Records the inventory transactions of the given sale invoice. + await this.recordInventoryTransactions( + inventoryTranscations, + override, + trx, + ); + } + + /** + * Deletes the given inventory transactions. + * @param {number} tenantId - Tenant id. + * @param {string} transactionType + * @param {number} transactionId + * @return {Promise<{ + * oldInventoryTransactions: IInventoryTransaction[] + * }>} + */ + async deleteInventoryTransactions( + transactionId: number, + transactionType: string, + trx?: Knex.Transaction, + ): Promise<{ oldInventoryTransactions: InventoryTransaction[] }> { + // Retrieve the inventory transactions of the given sale invoice. + const oldInventoryTransactions = await this.inventoryTransactionModel + .query(trx) + .where({ transactionId, transactionType }); + + // Deletes the inventory transactions by the given transaction type and id. + await this.inventoryTransactionModel + .query(trx) + .where({ transactionType, transactionId }) + .delete(); + + // Triggers `onInventoryTransactionsDeleted` event. + await this.eventEmitter.emitAsync( + events.inventory.onInventoryTransactionsDeleted, + { + oldInventoryTransactions, + transactionId, + transactionType, + trx, + } as IInventoryTransactionsDeletedPayload, + ); + return { oldInventoryTransactions }; + } + + /** + * Records the inventory cost lot transaction. + * @param {number} tenantId + * @param {IInventoryLotCost} inventoryLotEntry + * @return {Promise} + */ + async recordInventoryCostLotTransaction( + tenantId: number, + inventoryLotEntry: IInventoryLotCost, + ): Promise { + return this.inventoryCostLotTracker.query().insert({ + ...inventoryLotEntry, + }); + } + + /** + * Mark item cost computing is running. + * @param {boolean} isRunning - + */ + async markItemsCostComputeRunning(isRunning: boolean = true) { + this.settings.set({ + key: 'cost_compute_running', + group: 'inventory', + value: isRunning, + }); + await this.settings.save(); + } + + /** + * Checks if the items cost compute is running. + * @returns {boolean} + */ + isItemsCostComputeRunning() { + return ( + this.settings.get({ + key: 'cost_compute_running', + group: 'inventory', + }) ?? false + ); + } +} diff --git a/packages/server-nest/src/modules/InventoryCost/InventoryAverageCost.ts b/packages/server-nest/src/modules/InventoryCost/InventoryAverageCost.ts new file mode 100644 index 000000000..e8524a370 --- /dev/null +++ b/packages/server-nest/src/modules/InventoryCost/InventoryAverageCost.ts @@ -0,0 +1,254 @@ +import { pick } from 'lodash'; +import { Knex } from 'knex'; +import InventoryCostMethod from './InventoryCostMethod'; +import { InventoryTransaction } from './models/InventoryTransaction'; + +export class InventoryAverageCostMethod extends InventoryCostMethod { + startingDate: Date; + itemId: number; + costTransactions: any[]; + trx: Knex.Transaction; + + /** + * Constructor method. + * @param {number} tenantId - The given tenant id. + * @param {Date} startingDate - + * @param {number} itemId - The given inventory item id. + */ + constructor( + tenantId: number, + startingDate: Date, + itemId: number, + trx?: Knex.Transaction, + ) { + super(tenantId, startingDate, itemId); + + this.trx = trx; + this.startingDate = startingDate; + this.itemId = itemId; + this.costTransactions = []; + } + + /** + * Computes items costs from the given date using average cost method. + * ---------- + * - Calculate the items average cost in the given date. + * - Remove the journal entries that associated to the inventory transacions + * after the given date. + * - Re-compute the inventory transactions and re-write the journal entries + * after the given date. + * ---------- + * @async + * @param {Date} startingDate + * @param {number} referenceId + * @param {string} referenceType + */ + public async computeItemCost() { + const { InventoryTransaction } = this.tenantModels; + const { averageCost, openingQuantity, openingCost } = + await this.getOpeningAverageCost(this.startingDate, this.itemId); + + const afterInvTransactions = + await InventoryTransaction.query() + .modify('filterDateRange', this.startingDate) + .orderBy('date', 'ASC') + .orderByRaw("FIELD(direction, 'IN', 'OUT')") + .orderBy('createdAt', 'ASC') + .where('item_id', this.itemId) + .withGraphFetched('item'); + + // Tracking inventroy transactions and retrieve cost transactions based on + // average rate cost method. + const costTransactions = this.trackingCostTransactions( + afterInvTransactions, + openingQuantity, + openingCost, + ); + // Revert the inveout out lots transactions + await this.revertTheInventoryOutLotTrans(); + + // Store inventory lots cost transactions. + await this.storeInventoryLotsCost(costTransactions); + } + + /** + * Get items Average cost from specific date from inventory transactions. + * @async + * @param {Date} closingDate + * @return {number} + */ + public async getOpeningAverageCost(closingDate: Date, itemId: number) { + const { InventoryCostLotTracker } = this.tenantModels; + + const commonBuilder = (builder: any) => { + if (closingDate) { + builder.where('date', '<', closingDate); + } + builder.where('item_id', itemId); + builder.sum('rate as rate'); + builder.sum('quantity as quantity'); + builder.sum('cost as cost'); + builder.first(); + }; + // Calculates the total inventory total quantity and rate `IN` transactions. + const inInvSumationOper: Promise = InventoryCostLotTracker.query() + .onBuild(commonBuilder) + .where('direction', 'IN'); + + // Calculates the total inventory total quantity and rate `OUT` transactions. + const outInvSumationOper: Promise = InventoryCostLotTracker.query() + .onBuild(commonBuilder) + .where('direction', 'OUT'); + + const [inInvSumation, outInvSumation] = await Promise.all([ + inInvSumationOper, + outInvSumationOper, + ]); + return this.computeItemAverageCost( + inInvSumation?.cost || 0, + inInvSumation?.quantity || 0, + outInvSumation?.cost || 0, + outInvSumation?.quantity || 0, + ); + } + + /** + * Computes the item average cost. + * @static + * @param {number} quantityIn + * @param {number} rateIn + * @param {number} quantityOut + * @param {number} rateOut + */ + public computeItemAverageCost( + totalCostIn: number, + totalQuantityIn: number, + + totalCostOut: number, + totalQuantityOut: number, + ) { + const openingCost = totalCostIn - totalCostOut; + const openingQuantity = totalQuantityIn - totalQuantityOut; + + const averageCost = openingQuantity ? openingCost / openingQuantity : 0; + + return { averageCost, openingCost, openingQuantity }; + } + + private getCost(rate: number, quantity: number) { + return quantity ? rate * quantity : rate; + } + + /** + * Records the journal entries from specific item inventory transactions. + * @param {IInventoryTransaction[]} invTransactions + * @param {number} openingAverageCost + * @param {string} referenceType + * @param {number} referenceId + * @param {JournalCommand} journalCommands + */ + public trackingCostTransactions( + invTransactions: InventoryTransaction[], + openingQuantity: number = 0, + openingCost: number = 0, + ) { + const costTransactions: any[] = []; + + // Cumulative item quantity and cost. This will decrement after + // each out transactions depends on its quantity and cost. + let accQuantity: number = openingQuantity; + let accCost: number = openingCost; + + invTransactions.forEach((invTransaction: InventoryTransaction) => { + const commonEntry = { + invTransId: invTransaction.id, + ...pick(invTransaction, [ + 'date', + 'direction', + 'itemId', + 'quantity', + 'rate', + 'entryId', + 'transactionId', + 'transactionType', + 'createdAt', + 'costAccountId', + 'branchId', + 'warehouseId', + ]), + inventoryTransactionId: invTransaction.id, + }; + switch (invTransaction.direction) { + case 'IN': + const inCost = this.getCost( + invTransaction.rate, + invTransaction.quantity, + ); + // Increases the quantity and cost in `IN` inventory transactions. + accQuantity += invTransaction.quantity; + accCost += inCost; + + costTransactions.push({ + ...commonEntry, + cost: inCost, + }); + break; + case 'OUT': + // Average cost = Total cost / Total quantity + const averageCost = accQuantity ? accCost / accQuantity : 0; + + const quantity = + accQuantity > 0 + ? Math.min(invTransaction.quantity, accQuantity) + : invTransaction.quantity; + + // Cost = the transaction quantity * Average cost. + const cost = this.getCost(averageCost, quantity); + + // Revenue = transaction quanity * rate. + // const revenue = quantity * invTransaction.rate; + costTransactions.push({ + ...commonEntry, + quantity, + cost, + }); + accQuantity = Math.max(accQuantity - quantity, 0); + accCost = Math.max(accCost - cost, 0); + + if (invTransaction.quantity > quantity) { + const remainingQuantity = Math.max( + invTransaction.quantity - quantity, + 0, + ); + const remainingIncome = remainingQuantity * invTransaction.rate; + + costTransactions.push({ + ...commonEntry, + quantity: remainingQuantity, + cost: 0, + }); + accQuantity = Math.max(accQuantity - remainingQuantity, 0); + accCost = Math.max(accCost - remainingIncome, 0); + } + break; + } + }); + return costTransactions; + } + + /** + * Reverts the inventory lots `OUT` transactions. + * @param {Date} openingDate - Opening date. + * @param {number} itemId - Item id. + * @returns {Promise} + */ + async revertTheInventoryOutLotTrans(): Promise { + const { InventoryCostLotTracker } = this.tenantModels; + + await InventoryCostLotTracker.query(this.trx) + .modify('filterDateRange', this.startingDate) + .orderBy('date', 'DESC') + .where('item_id', this.itemId) + .delete(); + } +} diff --git a/packages/server-nest/src/modules/InventoryCost/InventoryCost.module.ts b/packages/server-nest/src/modules/InventoryCost/InventoryCost.module.ts new file mode 100644 index 000000000..e31161bc6 --- /dev/null +++ b/packages/server-nest/src/modules/InventoryCost/InventoryCost.module.ts @@ -0,0 +1,21 @@ +import { Module } from '@nestjs/common'; +import { InventoryCostGLBeforeWriteSubscriber } from './subscribers/InventoryCostGLBeforeWriteSubscriber'; +import { InventoryCostGLStorage } from './InventoryCostGLStorage.service'; +import { RegisterTenancyModel } from '../Tenancy/TenancyModels/Tenancy.module'; +import { InventoryCostLotTracker } from './models/InventoryCostLotTracker'; +import { InventoryTransaction } from './models/InventoryTransaction'; + +const models = [ + RegisterTenancyModel(InventoryCostLotTracker), + RegisterTenancyModel(InventoryTransaction), +]; + +@Module({ + providers: [ + ...models, + InventoryCostGLBeforeWriteSubscriber, + InventoryCostGLStorage, + ], + exports: [...models], +}) +export class InventoryCostModule {} diff --git a/packages/server-nest/src/modules/InventoryCost/InventoryCostApplication.ts b/packages/server-nest/src/modules/InventoryCost/InventoryCostApplication.ts new file mode 100644 index 000000000..23e03f45f --- /dev/null +++ b/packages/server-nest/src/modules/InventoryCost/InventoryCostApplication.ts @@ -0,0 +1,28 @@ +import { IInventoryItemCostMeta } from '@/interfaces'; +import { Service, Inject } from 'typedi'; +import { InventoryItemCostService } from './InventoryCosts.service'; + +@Service() +export class InventoryCostApplication { + @Inject() + inventoryCost: InventoryItemCostService; + + /** + * Retrieves the items inventory valuation list. + * @param {number} tenantId + * @param {number[]} itemsId + * @param {Date} date + * @returns {Promise} + */ + public getItemsInventoryValuationList = async ( + itemsId: number[], + date: Date + ): Promise => { + const itemsMap = await this.inventoryCost.getItemsInventoryValuation( + tenantId, + itemsId, + date + ); + return [...itemsMap.values()]; + }; +} diff --git a/packages/server-nest/src/modules/InventoryCost/InventoryCostGLStorage.service.ts b/packages/server-nest/src/modules/InventoryCost/InventoryCostGLStorage.service.ts new file mode 100644 index 000000000..6e52b1157 --- /dev/null +++ b/packages/server-nest/src/modules/InventoryCost/InventoryCostGLStorage.service.ts @@ -0,0 +1,38 @@ +import { Knex } from 'knex'; +import { Inject, Injectable } from '@nestjs/common'; +import { LedgerStorageService } from '../Ledger/LedgerStorage.service'; +import { Ledger } from '../Ledger/Ledger'; +import { AccountTransaction } from '../Accounts/models/AccountTransaction.model'; + +@Injectable() +export class InventoryCostGLStorage { + constructor( + private readonly ledgerStorage: LedgerStorageService, + + @Inject(AccountTransaction.name) + private readonly accountTransactionModel: typeof AccountTransaction, + ) {} + + /** + * Reverts the inventory cost GL entries from the given starting date. + * @param {Date} startingDate - Starting date. + * @param {Knex.Transaction} trx - Transaction. + */ + public async revertInventoryCostGLEntries( + startingDate: Date, + trx?: Knex.Transaction, + ): Promise { + // Retrieve transactions from specific date range and costable transactions only. + const transactions = await this.accountTransactionModel + .query() + .where('costable', true) + .modify('filterDateRange', startingDate) + .withGraphFetched('account'); + + // Transform transaction to ledger entries and reverse them. + const reversedLedger = Ledger.fromTransactions(transactions).reverse(); + + // Deletes and reverts balances of the given ledger. + await this.ledgerStorage.delete(reversedLedger, trx); + } +} diff --git a/packages/server-nest/src/modules/InventoryCost/InventoryCostLotTracker.ts b/packages/server-nest/src/modules/InventoryCost/InventoryCostLotTracker.ts new file mode 100644 index 000000000..349321c68 --- /dev/null +++ b/packages/server-nest/src/modules/InventoryCost/InventoryCostLotTracker.ts @@ -0,0 +1,302 @@ +import { pick, chain } from 'lodash'; +import moment from 'moment'; +import { IInventoryLotCost, IInventoryTransaction } from "interfaces"; +import InventoryCostMethod from '@/services/Inventory/InventoryCostMethod'; + +type TCostMethod = 'FIFO' | 'LIFO'; + +export class InventoryCostLotTracker extends InventoryCostMethod { + startingDate: Date; + itemId: number; + costMethod: TCostMethod; + itemsById: Map; + inventoryINTrans: any; + inventoryByItem: any; + costLotsTransactions: IInventoryLotCost[]; + inTransactions: any[]; + outTransactions: IInventoryTransaction[]; + revertJEntriesTransactions: IInventoryTransaction[]; + + /** + * Constructor method. + * @param {Date} startingDate - + * @param {number} itemId - + * @param {string} costMethod - + */ + constructor( + tenantId: number, + startingDate: Date, + itemId: number, + costMethod: TCostMethod = 'FIFO' + ) { + super(tenantId, startingDate, itemId); + + this.startingDate = startingDate; + this.itemId = itemId; + this.costMethod = costMethod; + + // Collect cost lots transactions to insert them to the storage in bulk. + this.costLotsTransactions= []; + // Collect inventory transactions by item id. + this.inventoryByItem = {}; + // Collection `IN` inventory tranaction by transaction id. + this.inventoryINTrans = {}; + // Collects `IN` transactions. + this.inTransactions = []; + // Collects `OUT` transactions. + this.outTransactions = []; + } + + /** + * Computes items costs from the given date using FIFO or LIFO cost method. + * -------- + * - Revert the inventory lots after the given date. + * - Remove all the journal entries from the inventory transactions + * after the given date. + * - Re-tracking the inventory lots from inventory transactions. + * - Re-write the journal entries from the given inventory transactions. + * @async + * @return {void} + */ + public async computeItemCost(): Promise { + await this.revertInventoryLots(this.startingDate); + await this.fetchInvINTransactions(); + await this.fetchInvOUTTransactions(); + await this.fetchRevertInvJReferenceIds(); + await this.fetchItemsMapped(); + + this.trackingInventoryINLots(this.inTransactions); + this.trackingInventoryOUTLots(this.outTransactions); + + // Re-tracking the inventory `IN` and `OUT` lots costs. + const storedTrackedInvLotsOper = this.storeInventoryLotsCost( + this.costLotsTransactions, + ); + return Promise.all([ + storedTrackedInvLotsOper, + ]); + } + + /** + * Fetched inventory transactions that has date from the starting date and + * fetches available IN LOTs transactions that has remaining bigger than zero. + * @private + */ + private async fetchInvINTransactions() { + const { InventoryTransaction, InventoryLotCostTracker } = this.tenantModels; + + const commonBuilder = (builder: any) => { + builder.orderBy('date', (this.costMethod === 'LIFO') ? 'DESC': 'ASC'); + builder.where('item_id', this.itemId); + }; + const afterInvTransactions: IInventoryTransaction[] = + await InventoryTransaction.query() + .modify('filterDateRange', this.startingDate) + .orderByRaw("FIELD(direction, 'IN', 'OUT')") + .onBuild(commonBuilder) + .orderBy('lot_number', (this.costMethod === 'LIFO') ? 'DESC' : 'ASC') + .withGraphFetched('item'); + + const availableINLots: IInventoryLotCost[] = + await InventoryLotCostTracker.query() + .modify('filterDateRange', null, this.startingDate) + .orderBy('date', 'ASC') + .where('direction', 'IN') + .orderBy('lot_number', 'ASC') + .onBuild(commonBuilder) + .whereNot('remaining', 0); + + this.inTransactions = [ + ...availableINLots.map((trans) => ({ lotTransId: trans.id, ...trans })), + ...afterInvTransactions.map((trans) => ({ invTransId: trans.id, ...trans })), + ]; + } + + /** + * Fetches inventory OUT transactions that has date from the starting date. + * @private + */ + private async fetchInvOUTTransactions() { + const { InventoryTransaction } = this.tenantModels; + + const afterOUTTransactions: IInventoryTransaction[] = + await InventoryTransaction.query() + .modify('filterDateRange', this.startingDate) + .orderBy('date', 'ASC') + .orderBy('lot_number', 'ASC') + .where('item_id', this.itemId) + .where('direction', 'OUT') + .withGraphFetched('item'); + + this.outTransactions = [ ...afterOUTTransactions ]; + } + + private async fetchItemsMapped() { + const itemsIds = chain(this.inTransactions).map((e) => e.itemId).uniq().value(); + const { Item } = this.tenantModels; + const storedItems = await Item.query() + .where('type', 'inventory') + .whereIn('id', itemsIds); + + this.itemsById = new Map(storedItems.map((item: any) => [item.id, item])); + } + + /** + * Fetch the inventory transactions that should revert its journal entries. + * @private + */ + private async fetchRevertInvJReferenceIds() { + const { InventoryTransaction } = this.tenantModels; + const revertJEntriesTransactions: IInventoryTransaction[] = + await InventoryTransaction.query() + .select(['transactionId', 'transactionType']) + .modify('filterDateRange', this.startingDate) + .where('direction', 'OUT') + .where('item_id', this.itemId); + + this.revertJEntriesTransactions = revertJEntriesTransactions; + } + + /** + * Revert the inventory lots to the given date by removing the inventory lots + * transactions after the given date and increment the remaining that + * associate to lot number. + * @async + * @return {Promise} + */ + public async revertInventoryLots(startingDate: Date) { + const { InventoryLotCostTracker } = this.tenantModels; + const asyncOpers: any[] = []; + const inventoryLotsTrans = await InventoryLotCostTracker.query() + .modify('filterDateRange', this.startingDate) + .orderBy('date', 'DESC') + .where('item_id', this.itemId) + .where('direction', 'OUT'); + + const deleteInvLotsTrans = InventoryLotCostTracker.query() + .modify('filterDateRange', this.startingDate) + .where('item_id', this.itemId) + .delete(); + + inventoryLotsTrans.forEach((inventoryLot: IInventoryLotCost) => { + if (!inventoryLot.lotNumber) { return; } + + const incrementOper = InventoryLotCostTracker.query() + .where('lot_number', inventoryLot.lotNumber) + .where('direction', 'IN') + .increment('remaining', inventoryLot.quantity); + + asyncOpers.push(incrementOper); + }); + return Promise.all([deleteInvLotsTrans, ...asyncOpers]); + } + + /** + * Tracking inventory `IN` lots transactions. + * @public + * @param {IInventoryTransaction[]} inventoryTransactions - + * @return {void} + */ + public trackingInventoryINLots( + inventoryTransactions: IInventoryTransaction[], + ) { + inventoryTransactions.forEach((transaction: IInventoryTransaction) => { + const { itemId, id } = transaction; + (this.inventoryByItem[itemId] || (this.inventoryByItem[itemId] = [])); + + const commonLotTransaction: IInventoryLotCost = { + ...pick(transaction, [ + 'date', 'rate', 'itemId', 'quantity', 'invTransId', 'lotTransId', + 'direction', 'transactionType', 'transactionId', 'lotNumber', 'remaining' + ]), + }; + this.inventoryByItem[itemId].push(id); + this.inventoryINTrans[id] = { + ...commonLotTransaction, + decrement: 0, + remaining: commonLotTransaction.remaining || commonLotTransaction.quantity, + }; + this.costLotsTransactions.push(this.inventoryINTrans[id]); + }); + } + + /** + * Tracking inventory `OUT` lots transactions. + * @public + * @param {IInventoryTransaction[]} inventoryTransactions - + * @return {void} + */ + public trackingInventoryOUTLots( + inventoryTransactions: IInventoryTransaction[], + ) { + inventoryTransactions.forEach((transaction: IInventoryTransaction) => { + const { itemId, id } = transaction; + (this.inventoryByItem[itemId] || (this.inventoryByItem[itemId] = [])); + + const commonLotTransaction: IInventoryLotCost = { + ...pick(transaction, [ + 'date', 'rate', 'itemId', 'quantity', 'invTransId', 'lotTransId', 'entryId', + 'direction', 'transactionType', 'transactionId', 'lotNumber', 'remaining' + ]), + }; + let invRemaining = transaction.quantity; + const idsShouldDel: number[] = []; + + this.inventoryByItem?.[itemId]?.some((_invTransactionId: number) => { + const _invINTransaction = this.inventoryINTrans[_invTransactionId]; + + // Can't continue if the IN transaction remaining equals zero. + if (invRemaining <= 0) { return true; } + + // Can't continue if the IN transaction date is after the current transaction date. + if (moment(_invINTransaction.date).isAfter(transaction.date)) { + return true; + } + // Detarmines the 'OUT' lot tranasctions whether bigger than 'IN' remaining transaction. + const biggerThanRemaining = (_invINTransaction.remaining - transaction.quantity) > 0; + const decrement = (biggerThanRemaining) ? transaction.quantity : _invINTransaction.remaining; + const maxDecrement = Math.min(decrement, invRemaining); + const cost = maxDecrement * _invINTransaction.rate; + + _invINTransaction.decrement += maxDecrement; + _invINTransaction.remaining = Math.max( + _invINTransaction.remaining - maxDecrement, + 0, + ); + invRemaining = Math.max(invRemaining - maxDecrement, 0); + + this.costLotsTransactions.push({ + ...commonLotTransaction, + cost, + quantity: maxDecrement, + lotNumber: _invINTransaction.lotNumber, + }); + // Pop the 'IN' lots that has zero remaining. + if (_invINTransaction.remaining === 0) { + idsShouldDel.push(_invTransactionId); + } + return false; + }); + if (invRemaining > 0) { + this.costLotsTransactions.push({ + ...commonLotTransaction, + quantity: invRemaining, + }); + } + this.removeInventoryItems(itemId, idsShouldDel); + }); + } + + /** + * Remove inventory transactions for specific item id. + * @private + * @param {number} itemId + * @param {number[]} idsShouldDel + * @return {void} + */ + private removeInventoryItems(itemId: number, idsShouldDel: number[]) { + // Remove the IN transactions that has zero remaining amount. + this.inventoryByItem[itemId] = this.inventoryByItem?.[itemId] + ?.filter((transId: number) => idsShouldDel.indexOf(transId) === -1); + } +} \ No newline at end of file diff --git a/packages/server-nest/src/modules/InventoryCost/InventoryCostMethod.ts b/packages/server-nest/src/modules/InventoryCost/InventoryCostMethod.ts new file mode 100644 index 000000000..b90711db7 --- /dev/null +++ b/packages/server-nest/src/modules/InventoryCost/InventoryCostMethod.ts @@ -0,0 +1,46 @@ +import { omit } from 'lodash'; +import { Container } from 'typedi'; +import TenancyService from '@/services/Tenancy/TenancyService'; +import { IInventoryLotCost } from '@/interfaces'; + +export default class InventoryCostMethod { + tenancy: TenancyService; + tenantModels: any; + + /** + * Constructor method. + * @param {number} tenantId - The given tenant id. + */ + constructor(tenantId: number, startingDate: Date, itemId: number) { + const tenancyService = Container.get(TenancyService); + + this.tenantModels = tenancyService.models(tenantId); + } + + /** + * Stores the inventory lots costs transactions in bulk. + * @param {IInventoryLotCost[]} costLotsTransactions + * @return {Promise[]} + */ + public storeInventoryLotsCost( + costLotsTransactions: IInventoryLotCost[] + ): Promise { + const { InventoryCostLotTracker } = this.tenantModels; + const opers: any = []; + + costLotsTransactions.forEach((transaction: any) => { + if (transaction.lotTransId && transaction.decrement) { + const decrementOper = InventoryCostLotTracker.query(this.trx) + .where('id', transaction.lotTransId) + .decrement('remaining', transaction.decrement); + opers.push(decrementOper); + } else if (!transaction.lotTransId) { + const operation = InventoryCostLotTracker.query(this.trx).insert({ + ...omit(transaction, ['decrement', 'invTransId', 'lotTransId']), + }); + opers.push(operation); + } + }); + return Promise.all(opers); + } +} diff --git a/packages/server-nest/src/modules/InventoryCost/InventoryCosts.service.ts b/packages/server-nest/src/modules/InventoryCost/InventoryCosts.service.ts new file mode 100644 index 000000000..a0fc9a674 --- /dev/null +++ b/packages/server-nest/src/modules/InventoryCost/InventoryCosts.service.ts @@ -0,0 +1,149 @@ +import { keyBy, get } from 'lodash'; +import { Knex } from 'knex'; +import * as R from 'ramda'; +import { IInventoryItemCostMeta } from './types/InventoryCost.types'; +import { Inject, Injectable } from '@nestjs/common'; +import { InventoryTransaction } from './models/InventoryTransaction'; +import { InventoryCostLotTracker } from './models/InventoryCostLotTracker'; +import { Item } from '../Items/models/Item'; + +@Injectable() +export class InventoryItemCostService { + constructor( + @Inject(InventoryTransaction.name) + private readonly inventoryTransactionModel: typeof InventoryTransaction, + + @Inject(InventoryCostLotTracker.name) + private readonly inventoryCostLotTrackerModel: typeof InventoryCostLotTracker, + + @Inject(Item.name) + private readonly itemModel: typeof Item, + ) {} + + /** + * Common query of items inventory valuation. + * @param {number[]} itemsIds - + * @param {Date} date - + * @param {Knex.QueryBuilder} builder - + */ + private itemsInventoryValuationCommonQuery = R.curry( + (itemsIds: number[], date: Date, builder: Knex.QueryBuilder) => { + if (date) { + builder.where('date', '<', date); + } + builder.whereIn('item_id', itemsIds); + builder.sum('rate as rate'); + builder.sum('quantity as quantity'); + builder.sum('cost as cost'); + + builder.groupBy('item_id'); + builder.select(['item_id']); + } + ); + + /** + * + * @param {} INValuationMap - + * @param {} OUTValuationMap - + * @param {number} itemId + */ + private getItemInventoryMeta = R.curry( + ( + INValuationMap, + OUTValuationMap, + itemId: number + ): IInventoryItemCostMeta => { + const INCost = get(INValuationMap, `[${itemId}].cost`, 0); + const INQuantity = get(INValuationMap, `[${itemId}].quantity`, 0); + + const OUTCost = get(OUTValuationMap, `[${itemId}].cost`, 0); + const OUTQuantity = get(OUTValuationMap, `[${itemId}].quantity`, 0); + + const valuation = INCost - OUTCost; + const quantity = INQuantity - OUTQuantity; + const average = quantity ? valuation / quantity : 0; + + return { itemId, valuation, quantity, average }; + } + ); + + /** + * + * @param {number} tenantId + * @param {number} itemsId + * @param {Date} date + * @returns + */ + private getItemsInventoryINAndOutAggregated = ( + itemsId: number[], + date: Date + ): Promise => { + + const commonBuilder = this.itemsInventoryValuationCommonQuery( + itemsId, + date + ); + const INValuationOper = this.inventoryCostLotTrackerModel.query() + .onBuild(commonBuilder) + .where('direction', 'IN'); + + const OUTValuationOper = this.inventoryCostLotTrackerModel.query() + .onBuild(commonBuilder) + .where('direction', 'OUT'); + + return Promise.all([OUTValuationOper, INValuationOper]); + }; + + /** + * + * @param {number} tenantId - + * @param {number[]} itemsIds - + * @param {Date} date - + */ + private getItemsInventoryInOutMap = async ( + itemsId: number[], + date: Date + ) => { + const [OUTValuation, INValuation] = + await this.getItemsInventoryINAndOutAggregated(itemsId, date); + + const OUTValuationMap = keyBy(OUTValuation, 'itemId'); + const INValuationMap = keyBy(INValuation, 'itemId'); + + return [OUTValuationMap, INValuationMap]; + }; + + /** + * + * @param {number} tenantId + * @param {number} itemId + * @param {Date} date + * @returns {Promise>} + */ + public getItemsInventoryValuation = async ( + itemsId: number[], + date: Date + ): Promise> => { + // Retrieves the inventory items. + const items = await this.itemModel.query() + .whereIn('id', itemsId) + .where('type', 'inventory'); + + // Retrieves the inventory items ids. + const inventoryItemsIds: number[] = items.map((item) => item.id); + + // Retreives the items inventory IN/OUT map. + const [OUTValuationMap, INValuationMap] = + await this.getItemsInventoryInOutMap(itemsId, date); + + const getItemValuation = this.getItemInventoryMeta( + INValuationMap, + OUTValuationMap + ); + const itemsValuations = inventoryItemsIds.map(getItemValuation); + const itemsValuationsMap = new Map( + itemsValuations.map((i) => [i.itemId, i]) + ); + return itemsValuationsMap; + }; +} diff --git a/packages/server-nest/src/modules/InventoryCost/InventoryItemsQuantitySync.service.ts b/packages/server-nest/src/modules/InventoryCost/InventoryItemsQuantitySync.service.ts new file mode 100644 index 000000000..97ab23c51 --- /dev/null +++ b/packages/server-nest/src/modules/InventoryCost/InventoryItemsQuantitySync.service.ts @@ -0,0 +1,94 @@ +import { toSafeInteger } from 'lodash'; +import { IInventoryTransaction, IItemsQuantityChanges } from '@/interfaces'; +import { Knex } from 'knex'; +import { Inject } from '@nestjs/common'; +import { Item } from '../Items/models/Item'; +import { Injectable } from '@nestjs/common'; + +/** + * Syncs the inventory transactions with inventory items quantity. + */ +@Injectable() +export class InventoryItemsQuantitySyncService { + constructor( + @Inject(Item.name) + private readonly itemModel: typeof Item, + ) {} + + /** + * Reverse the given inventory transactions. + * @param {IInventoryTransaction[]} inventroyTransactions + * @return {IInventoryTransaction[]} + */ + public reverseInventoryTransactions( + inventroyTransactions: IInventoryTransaction[], + ): IInventoryTransaction[] { + return inventroyTransactions.map((transaction) => ({ + ...transaction, + direction: transaction.direction === 'OUT' ? 'IN' : 'OUT', + })); + } + + /** + * Reverses the inventory transactions. + * @param {IInventoryTransaction[]} inventroyTransactions - + * @return {IItemsQuantityChanges[]} + */ + public getReverseItemsQuantityChanges( + inventroyTransactions: IInventoryTransaction[], + ): IItemsQuantityChanges[] { + const reversedTransactions = this.reverseInventoryTransactions( + inventroyTransactions, + ); + return this.getItemsQuantityChanges(reversedTransactions); + } + + /** + * Retrieve the items quantity changes from the given inventory transactions. + * @param {IInventoryTransaction[]} inventroyTransactions - Inventory transactions. + * @return {IItemsQuantityChanges[]} + */ + public getItemsQuantityChanges( + inventroyTransactions: IInventoryTransaction[], + ): IItemsQuantityChanges[] { + const balanceMap: { [itemId: number]: number } = {}; + + inventroyTransactions.forEach( + (inventoryTransaction: IInventoryTransaction) => { + const { itemId, direction, quantity } = inventoryTransaction; + + if (!balanceMap[itemId]) { + balanceMap[itemId] = 0; + } + balanceMap[itemId] += direction === 'IN' ? quantity : 0; + balanceMap[itemId] -= direction === 'OUT' ? quantity : 0; + }, + ); + return Object.entries(balanceMap).map(([itemId, balanceChange]) => ({ + itemId: toSafeInteger(itemId), + balanceChange, + })); + } + + /** + * Changes the items quantity changes. + * @param {IItemsQuantityChanges[]} itemsQuantity - Items quantity changes. + * @return {Promise} + */ + public async changeItemsQuantity( + itemsQuantity: IItemsQuantityChanges[], + trx?: Knex.Transaction, + ): Promise { + const opers = []; + + itemsQuantity.forEach((itemQuantity: IItemsQuantityChanges) => { + const changeQuantityOper = this.itemModel + .query(trx) + .where({ id: itemQuantity.itemId, type: 'inventory' }) + .modify('quantityOnHand', itemQuantity.balanceChange); + + opers.push(changeQuantityOper); + }); + await Promise.all(opers); + } +} diff --git a/packages/server-nest/src/modules/InventoryCost/models/InventoryCostLotTracker.ts b/packages/server-nest/src/modules/InventoryCost/models/InventoryCostLotTracker.ts new file mode 100644 index 000000000..f5351434a --- /dev/null +++ b/packages/server-nest/src/modules/InventoryCost/models/InventoryCostLotTracker.ts @@ -0,0 +1,135 @@ +import { Model } from 'objection'; +import { castArray } from 'lodash'; +import moment from 'moment'; +import { SaleInvoice } from '@/modules/SaleInvoices/models/SaleInvoice'; +import { SaleReceipt } from '@/modules/SaleReceipts/models/SaleReceipt'; +import { Item } from '@/modules/Items/models/Item'; +import { BaseModel } from '@/models/Model'; + +export class InventoryCostLotTracker extends BaseModel { + date: Date; + direction: string; + itemId: number; + quantity: number; + rate: number; + remaining: number; + cost: number; + transactionType: string; + transactionId: number; + costAccountId: number; + entryId: number; + createdAt: Date; + + exchangeRate: number; + currencyCode: string; + + item?: Item; + invoice?: SaleInvoice; + receipt?: SaleReceipt; + + /** + * Table name + */ + static get tableName() { + return 'inventory_cost_lot_tracker'; + } + + /** + * Model timestamps. + */ + static get timestamps() { + return []; + } + + /** + * Model modifiers. + */ + static get modifiers() { + return { + groupedEntriesCost(query) { + query.select(['date', 'item_id', 'transaction_id', 'transaction_type']); + query.sum('cost as cost'); + + query.groupBy('transaction_id'); + query.groupBy('transaction_type'); + query.groupBy('date'); + query.groupBy('item_id'); + }, + filterDateRange(query, startDate, endDate, type = 'day') { + const dateFormat = 'YYYY-MM-DD'; + const fromDate = moment(startDate).startOf(type).format(dateFormat); + const toDate = moment(endDate).endOf(type).format(dateFormat); + + if (startDate) { + query.where('date', '>=', fromDate); + } + if (endDate) { + query.where('date', '<=', toDate); + } + }, + + /** + * Filters transactions by the given branches. + */ + filterByBranches(query, branchesIds) { + const formattedBranchesIds = castArray(branchesIds); + + query.whereIn('branchId', formattedBranchesIds); + }, + + /** + * Filters transactions by the given warehosues. + */ + filterByWarehouses(query, branchesIds) { + const formattedWarehousesIds = castArray(branchesIds); + + query.whereIn('warehouseId', formattedWarehousesIds); + }, + }; + } + + /** + * Relationship mapping. + */ + static get relationMappings() { + const Item = require('models/Item'); + const SaleInvoice = require('models/SaleInvoice'); + const ItemEntry = require('models/ItemEntry'); + const SaleReceipt = require('models/SaleReceipt'); + + return { + item: { + relation: Model.BelongsToOneRelation, + modelClass: Item.default, + join: { + from: 'inventory_cost_lot_tracker.itemId', + to: 'items.id', + }, + }, + invoice: { + relation: Model.BelongsToOneRelation, + modelClass: SaleInvoice.default, + join: { + from: 'inventory_cost_lot_tracker.transactionId', + to: 'sales_invoices.id', + }, + }, + itemEntry: { + relation: Model.BelongsToOneRelation, + modelClass: ItemEntry.default, + join: { + from: 'inventory_cost_lot_tracker.entryId', + to: 'items_entries.id', + }, + }, + receipt: { + relation: Model.BelongsToOneRelation, + modelClass: SaleReceipt.default, + join: { + from: 'inventory_cost_lot_tracker.transactionId', + to: 'sales_receipts.id', + }, + }, + }; + } +} diff --git a/packages/server-nest/src/modules/InventoryCost/models/InventoryTransaction.ts b/packages/server-nest/src/modules/InventoryCost/models/InventoryTransaction.ts new file mode 100644 index 000000000..27c3f4cf5 --- /dev/null +++ b/packages/server-nest/src/modules/InventoryCost/models/InventoryTransaction.ts @@ -0,0 +1,165 @@ +import { Model, raw } from 'objection'; +import { castArray } from 'lodash'; +import moment from 'moment'; +import { BaseModel } from '@/models/Model'; +import { getTransactionTypeLabel } from '@/modules/BankingTransactions/utils'; +import { TInventoryTransactionDirection } from '../types/InventoryCost.types'; + +export class InventoryTransaction extends BaseModel { + date: Date | string; + direction: TInventoryTransactionDirection; + itemId: number; + quantity: number | null; + rate: number; + transactionType: string; + transactionId: number; + costAccountId?: number; + entryId: number; + + createdAt?: Date; + updatedAt?: Date; + + warehouseId?: number; + + /** + * Table name + */ + static get tableName() { + return 'inventory_transactions'; + } + + /** + * Model timestamps. + */ + get timestamps() { + return ['createdAt', 'updatedAt']; + } + + /** + * Retrieve formatted reference type. + * @return {string} + */ + get transcationTypeFormatted() { + return getTransactionTypeLabel(this.transactionType); + } + + /** + * Model modifiers. + */ + static get modifiers() { + return { + filterDateRange(query, startDate, endDate, type = 'day') { + const dateFormat = 'YYYY-MM-DD'; + const fromDate = moment(startDate).startOf(type).format(dateFormat); + const toDate = moment(endDate).endOf(type).format(dateFormat); + + if (startDate) { + query.where('date', '>=', fromDate); + } + if (endDate) { + query.where('date', '<=', toDate); + } + }, + + itemsTotals(builder) { + builder.select('itemId'); + builder.sum('rate as rate'); + builder.sum('quantity as quantity'); + builder.select(raw('SUM(`QUANTITY` * `RATE`) as COST')); + builder.groupBy('itemId'); + }, + + INDirection(builder) { + builder.where('direction', 'IN'); + }, + + OUTDirection(builder) { + builder.where('direction', 'OUT'); + }, + + /** + * Filters transactions by the given branches. + */ + filterByBranches(query, branchesIds) { + const formattedBranchesIds = castArray(branchesIds); + + query.whereIn('branch_id', formattedBranchesIds); + }, + + /** + * Filters transactions by the given warehosues. + */ + filterByWarehouses(query, warehousesIds) { + const formattedWarehousesIds = castArray(warehousesIds); + + query.whereIn('warehouse_id', formattedWarehousesIds); + }, + }; + } + + /** + * Relationship mapping. + */ + static get relationMappings() { + const Item = require('models/Item'); + const ItemEntry = require('models/ItemEntry'); + const InventoryTransactionMeta = require('models/InventoryTransactionMeta'); + const InventoryCostLots = require('models/InventoryCostLotTracker'); + + return { + // Transaction meta. + meta: { + relation: Model.HasOneRelation, + modelClass: InventoryTransactionMeta.default, + join: { + from: 'inventory_transactions.id', + to: 'inventory_transaction_meta.inventoryTransactionId', + }, + }, + // Item cost aggregated. + itemCostAggregated: { + relation: Model.HasOneRelation, + modelClass: InventoryCostLots.default, + join: { + from: 'inventory_transactions.itemId', + to: 'inventory_cost_lot_tracker.itemId', + }, + filter(query) { + query.select('itemId'); + query.sum('cost as cost'); + query.sum('quantity as quantity'); + query.groupBy('itemId'); + }, + }, + costLotAggregated: { + relation: Model.HasOneRelation, + modelClass: InventoryCostLots.default, + join: { + from: 'inventory_transactions.id', + to: 'inventory_cost_lot_tracker.inventoryTransactionId', + }, + filter(query) { + query.sum('cost as cost'); + query.sum('quantity as quantity'); + query.groupBy('inventoryTransactionId'); + }, + }, + item: { + relation: Model.BelongsToOneRelation, + modelClass: Item.default, + join: { + from: 'inventory_transactions.itemId', + to: 'items.id', + }, + }, + itemEntry: { + relation: Model.BelongsToOneRelation, + modelClass: ItemEntry.default, + join: { + from: 'inventory_transactions.entryId', + to: 'items_entries.id', + }, + }, + }; + } +} diff --git a/packages/server-nest/src/modules/InventoryCost/subscribers/InventoryCostGLBeforeWriteSubscriber.ts b/packages/server-nest/src/modules/InventoryCost/subscribers/InventoryCostGLBeforeWriteSubscriber.ts new file mode 100644 index 000000000..e695a5fa1 --- /dev/null +++ b/packages/server-nest/src/modules/InventoryCost/subscribers/InventoryCostGLBeforeWriteSubscriber.ts @@ -0,0 +1,27 @@ +import { Injectable } from '@nestjs/common'; +import { OnEvent } from '@nestjs/event-emitter'; +import { events } from '@/common/events/events'; +import { IInventoryCostLotsGLEntriesWriteEvent } from '../types/InventoryCost.types'; +import { InventoryCostGLStorage } from '../InventoryCostGLStorage.service'; + +@Injectable() +export class InventoryCostGLBeforeWriteSubscriber { + constructor( + private readonly inventoryCostGLStorage: InventoryCostGLStorage, + ) {} + + /** + * Writes the receipts cost GL entries once the inventory cost lots be written. + * @param {IInventoryCostLotsGLEntriesWriteEvent} + */ + @OnEvent(events.inventory.onCostLotsGLEntriesBeforeWrite) + public async revertsInventoryCostGLEntries({ + trx, + startingDate, + }: IInventoryCostLotsGLEntriesWriteEvent) { + await this.inventoryCostGLStorage.revertInventoryCostGLEntries( + startingDate, + trx + ); + }; +} diff --git a/packages/server-nest/src/modules/InventoryCost/types/InventoryCost.types.ts b/packages/server-nest/src/modules/InventoryCost/types/InventoryCost.types.ts new file mode 100644 index 000000000..3120da8ea --- /dev/null +++ b/packages/server-nest/src/modules/InventoryCost/types/InventoryCost.types.ts @@ -0,0 +1,61 @@ +import { Knex } from "knex"; +import { InventoryTransaction } from "../models/InventoryTransaction"; + + +export interface IInventoryItemCostMeta { + itemId: number; + valuation: number; + quantity: number; + average: number; +} + +export interface IInventoryCostLotsGLEntriesWriteEvent { + startingDate: Date, + trx: Knex.Transaction +} + +export type TInventoryTransactionDirection = 'IN' | 'OUT'; + +export type TCostMethod = 'FIFO' | 'LIFO' | 'AVG'; + +export interface IInventoryTransactionMeta { + id?: number; + transactionNumber: string; + description: string; +} + +export interface IInventoryCostLotAggregated { + cost: number; + quantity: number; +} + +export interface IItemsQuantityChanges { + itemId: number; + balanceChange: number; +} + +export interface IInventoryTransactionsCreatedPayload { + inventoryTransactions: InventoryTransaction[]; + trx: Knex.Transaction; +} + +export interface IInventoryTransactionsDeletedPayload { + oldInventoryTransactions: InventoryTransaction[]; + transactionId: number; + transactionType: string; + trx: Knex.Transaction; +} + +export interface IInventoryItemCostScheduledPayload { + startingDate: Date | string; + itemId: number; +} + +export interface IComputeItemCostJobStartedPayload { + startingDate: Date | string; + itemId: number; +} +export interface IComputeItemCostJobCompletedPayload { + startingDate: Date | string; + itemId: number; +} diff --git a/packages/server-nest/src/modules/InventoryCost/utils.ts b/packages/server-nest/src/modules/InventoryCost/utils.ts new file mode 100644 index 000000000..f961694b1 --- /dev/null +++ b/packages/server-nest/src/modules/InventoryCost/utils.ts @@ -0,0 +1,15 @@ +import { chain } from 'lodash'; + +/** + * Grpups by transaction type and id the inventory transactions. + * @param {IInventoryTransaction} invTransactions + * @returns + */ +export function groupInventoryTransactionsByTypeId( + transactions: { transactionType: string; transactionId: number }[] +): { transactionType: string; transactionId: number }[][] { + return chain(transactions) + .groupBy((t) => `${t.transactionType}-${t.transactionId}`) + .values() + .value(); +} diff --git a/packages/server-nest/src/modules/ItemCategories/ItemCategory.application.ts b/packages/server-nest/src/modules/ItemCategories/ItemCategory.application.ts index d6ce6fec3..af155a863 100644 --- a/packages/server-nest/src/modules/ItemCategories/ItemCategory.application.ts +++ b/packages/server-nest/src/modules/ItemCategories/ItemCategory.application.ts @@ -4,6 +4,7 @@ import { CreateItemCategoryService } from './commands/CreateItemCategory.service import { DeleteItemCategoryService } from './commands/DeleteItemCategory.service'; import { EditItemCategoryService } from './commands/EditItemCategory.service'; import { GetItemCategoryService } from './queries/GetItemCategory.service'; +import { GetItemCategoriesService } from './queries/GetItemCategories.service'; @Injectable() export class ItemCategoryApplication { @@ -18,6 +19,7 @@ export class ItemCategoryApplication { private readonly editItemCategoryService: EditItemCategoryService, private readonly getItemCategoryService: GetItemCategoryService, private readonly deleteItemCategoryService: DeleteItemCategoryService, + private readonly getItemCategoriesService: GetItemCategoriesService, ) {} /** @@ -69,4 +71,13 @@ export class ItemCategoryApplication { public deleteItemCategory(itemCategoryId: number) { return this.deleteItemCategoryService.deleteItemCategory(itemCategoryId); } + + /** + * Retrieves the item categories list. + * @param {IItemCategoriesFilter} filterDTO - The item categories filter DTO. + * @returns {Promise} + */ + public getItemCategories(filterDTO: IItemCategoriesFilter) { + return this.getItemCategoriesService.getItemCategories(filterDTO); + } } diff --git a/packages/server-nest/src/modules/ItemCategories/queries/GetItemCategories.service.ts b/packages/server-nest/src/modules/ItemCategories/queries/GetItemCategories.service.ts new file mode 100644 index 000000000..558f652c0 --- /dev/null +++ b/packages/server-nest/src/modules/ItemCategories/queries/GetItemCategories.service.ts @@ -0,0 +1,56 @@ +import * as R from 'ramda'; +import { DynamicListService } from '@/modules/DynamicListing/DynamicList.service'; +import { ItemCategory } from '../models/ItemCategory.model'; +import { Inject } from '@nestjs/common'; + +export class GetItemCategoriesService { + constructor( + private readonly dynamicListService: DynamicListService, + + @Inject(ItemCategory.name) + private readonly itemCategoryModel: typeof ItemCategory, + ) {} + + /** + * Parses items categories filter DTO. + * @param {} filterDTO + * @returns + */ + private parsesListFilterDTO(filterDTO) { + return R.compose( + // Parses stringified filter roles. + this.dynamicListService.parseStringifiedFilter, + )(filterDTO); + } + + /** + * Retrieve item categories list. + * @param {number} tenantId + * @param filter + */ + public async getItemCategories( + filterDTO: IItemCategoriesFilter, + ): Promise<{ itemCategories: ItemCategory[]; filterMeta: IFilterMeta }> { + // Parses list filter DTO. + const filter = this.parsesListFilterDTO(filterDTO); + + // Dynamic list service. + const dynamicList = await this.dynamicListService.dynamicList( + this.itemCategoryModel, + filter, + ); + // Items categories. + const itemCategories = await this.itemCategoryModel + .query() + .onBuild((query) => { + // Subquery to calculate sumation of associated items to the item category. + query.select( + '*', + this.itemCategoryModel.relatedQuery('items').count().as('count'), + ); + + dynamicList.buildQuery()(query); + }); + return { itemCategories, filterMeta: dynamicList.getResponseMeta() }; + } +} diff --git a/packages/server-nest/src/modules/Items/GetItems.service.ts b/packages/server-nest/src/modules/Items/GetItems.service.ts index e69de29bb..1366b4322 100644 --- a/packages/server-nest/src/modules/Items/GetItems.service.ts +++ b/packages/server-nest/src/modules/Items/GetItems.service.ts @@ -0,0 +1,67 @@ +import * as R from 'ramda'; +import { Inject, Injectable } from '@nestjs/common'; +import { TransformerInjectable } from '../Transformer/TransformerInjectable.service'; +import { DynamicListService } from '../DynamicListing/DynamicList.service'; +import { Item } from './models/Item'; +import { IItemsFilter } from './types/Items.types'; +import { ItemTransformer } from './Item.transformer'; + +@Injectable() +export class GetItemsService { + constructor( + private readonly dynamicListService: DynamicListService, + private readonly transformer: TransformerInjectable, + + @Inject(Item.name) + private readonly itemModel: typeof Item, + ) {} + + /** + * Parses items list filter DTO. + * @param {} filterDTO - Filter DTO. + */ + private parseItemsListFilterDTO(filterDTO: IItemsFilter) { + return R.compose( + this.dynamicListService.parseStringifiedFilter, + )(filterDTO); + } + + /** + * Retrieves items datatable list. + * @param {IItemsFilter} itemsFilter - Items filter. + */ + public async getItems(filterDTO: IItemsFilter) { + // Parses items list filter DTO. + const filter = this.parseItemsListFilterDTO(filterDTO); + + // Dynamic list service. + const dynamicFilter = await this.dynamicListService.dynamicList( + Item, + filter, + ); + const { results: items, pagination } = await this.itemModel + .query() + .onBuild((builder) => { + builder.modify('inactiveMode', filter.inactiveMode); + + builder.withGraphFetched('inventoryAccount'); + builder.withGraphFetched('sellAccount'); + builder.withGraphFetched('costAccount'); + builder.withGraphFetched('category'); + + dynamicFilter.buildQuery()(builder); + }) + .pagination(filter.page - 1, filter.pageSize); + + // Retrieves the transformed items. + const transformedItems = await this.transformer.transform( + items, + new ItemTransformer(), + ); + return { + items: transformedItems, + pagination, + filterMeta: dynamicFilter.getResponseMeta(), + }; + } +} diff --git a/packages/server-nest/src/modules/Items/Item.controller.ts b/packages/server-nest/src/modules/Items/Item.controller.ts index 54fbd358d..9d79d1d1e 100644 --- a/packages/server-nest/src/modules/Items/Item.controller.ts +++ b/packages/server-nest/src/modules/Items/Item.controller.ts @@ -4,32 +4,21 @@ import { Delete, Param, Post, - UsePipes, UseGuards, Patch, Get, Put, } from '@nestjs/common'; -import { ZodValidationPipe } from '@/common/pipes/ZodValidation.pipe'; -import { createItemSchema } from './Item.schema'; -import { CreateItemService } from './CreateItem.service'; -import { DeleteItemService } from './DeleteItem.service'; import { TenantController } from '../Tenancy/Tenant.controller'; import { SubscriptionGuard } from '../Subscription/interceptors/Subscription.guard'; import { PublicRoute } from '../Auth/Jwt.guard'; -import { EditItemService } from './EditItem.service'; import { ItemsApplicationService } from './ItemsApplication.service'; @Controller('/items') @UseGuards(SubscriptionGuard) @PublicRoute() export class ItemsController extends TenantController { - constructor( - private readonly createItemService: CreateItemService, - private readonly deleteItemService: DeleteItemService, - private readonly editItemService: EditItemService, - private readonly itemsApplication: ItemsApplicationService, - ) { + constructor(private readonly itemsApplication: ItemsApplicationService) { super(); } @@ -46,7 +35,7 @@ export class ItemsController extends TenantController { @Body() editItemDto: any, ): Promise { const itemId = parseInt(id, 10); - return this.editItemService.editItem(itemId, editItemDto); + return this.itemsApplication.editItem(itemId, editItemDto); } /** @@ -57,7 +46,7 @@ export class ItemsController extends TenantController { @Post() // @UsePipes(new ZodValidationPipe(createItemSchema)) async createItem(@Body() createItemDto: any): Promise { - return this.createItemService.createItem(createItemDto); + return this.itemsApplication.createItem(createItemDto); } /** @@ -67,7 +56,7 @@ export class ItemsController extends TenantController { @Delete(':id') async deleteItem(@Param('id') id: string): Promise { const itemId = parseInt(id, 10); - return this.deleteItemService.deleteItem(itemId); + return this.itemsApplication.deleteItem(itemId); } /** diff --git a/packages/server-nest/src/modules/Items/Items.module.ts b/packages/server-nest/src/modules/Items/Items.module.ts index 649088ac6..e0856c08c 100644 --- a/packages/server-nest/src/modules/Items/Items.module.ts +++ b/packages/server-nest/src/modules/Items/Items.module.ts @@ -13,9 +13,11 @@ import { ItemTransactionsService } from './ItemTransactions.service'; import { GetItemService } from './GetItem.service'; import { TransformerInjectable } from '../Transformer/TransformerInjectable.service'; import { ItemsEntriesService } from './ItemsEntries.service'; +import { GetItemsService } from './GetItems.service'; +import { DynamicListModule } from '../DynamicListing/DynamicList.module'; @Module({ - imports: [TenancyDatabaseModule], + imports: [TenancyDatabaseModule, DynamicListModule], controllers: [ItemsController], providers: [ ItemsValidators, @@ -26,6 +28,7 @@ import { ItemsEntriesService } from './ItemsEntries.service'; DeleteItemService, ItemsApplicationService, GetItemService, + GetItemsService, ItemTransactionsService, TenancyContext, TransformerInjectable, diff --git a/packages/server-nest/src/modules/Items/ItemsApplication.service.ts b/packages/server-nest/src/modules/Items/ItemsApplication.service.ts index d88d54d04..709eb3e93 100644 --- a/packages/server-nest/src/modules/Items/ItemsApplication.service.ts +++ b/packages/server-nest/src/modules/Items/ItemsApplication.service.ts @@ -9,6 +9,8 @@ import { ActivateItemService } from './ActivateItem.service'; import { GetItemService } from './GetItem.service'; import { ItemTransactionsService } from './ItemTransactions.service'; import { Injectable } from '@nestjs/common'; +import { GetItemsService } from './GetItems.service'; +import { IItemsFilter } from './types/Items.types'; @Injectable() export class ItemsApplicationService { @@ -19,6 +21,7 @@ export class ItemsApplicationService { private readonly activateItemService: ActivateItemService, private readonly inactivateItemService: InactivateItem, private readonly getItemService: GetItemService, + private readonly getItemsService: GetItemsService, private readonly itemTransactionsService: ItemTransactionsService, ) {} @@ -86,6 +89,14 @@ export class ItemsApplicationService { return this.getItemService.getItem(itemId); } + /** + * Retrieves the paginated filterable items list. + * @param {IItemsFilter} filterDTO + */ + async getItems(filterDTO: IItemsFilter) { + return this.getItemsService.getItems(filterDTO) + } + /** * Retrieves the item associated invoices transactions. * @param {number} itemId diff --git a/packages/server-nest/src/modules/Items/models/Item.ts b/packages/server-nest/src/modules/Items/models/Item.ts index 93f9733e6..5b9c61e8f 100644 --- a/packages/server-nest/src/modules/Items/models/Item.ts +++ b/packages/server-nest/src/modules/Items/models/Item.ts @@ -1,13 +1,19 @@ -import * as F from 'fp-ts/function'; import * as R from 'ramda'; -import { SearchableModel } from '@/modules/Search/SearchableMdel'; import { BaseModel } from '@/models/Model'; import { Warehouse } from '@/modules/Warehouses/models/Warehouse.model'; -// import { TenantModel } from '@/modules/System/models/TenantModel'; +import { CustomViewBaseModelMixin } from '@/modules/CustomViews/CustomViewBaseModel'; +import { SearchableBaseModelMixin } from '@/modules/DynamicListing/models/SearchableBaseModel'; +import { ResourceableModelMixin } from '@/modules/Resource/models/ResourcableModel'; +import { MetadataModelMixin } from '@/modules/DynamicListing/models/MetadataModel'; -// const Extend = R.compose(SearchableModel)(TenantModel); +const ExtendedItem = R.pipe( + CustomViewBaseModelMixin, + SearchableBaseModelMixin, + ResourceableModelMixin, + MetadataModelMixin +)(BaseModel); -export class Item extends BaseModel { +export class Item extends ExtendedItem { public readonly quantityOnHand: number; public readonly name: string; public readonly active: boolean; diff --git a/packages/server-nest/src/modules/Items/types/Items.types.ts b/packages/server-nest/src/modules/Items/types/Items.types.ts new file mode 100644 index 000000000..2471464da --- /dev/null +++ b/packages/server-nest/src/modules/Items/types/Items.types.ts @@ -0,0 +1,8 @@ +import { IDynamicListFilter } from '@/modules/DynamicListing/DynamicFilter/DynamicFilter.types'; + +export interface IItemsFilter extends IDynamicListFilter { + page: number; + pageSize: number; + inactiveMode: boolean; + viewSlug?: string; +} diff --git a/packages/server-nest/src/modules/ManualJournals/queries/GetManualJournals.service.ts b/packages/server-nest/src/modules/ManualJournals/queries/GetManualJournals.service.ts index ac8987e42..2e480bd4e 100644 --- a/packages/server-nest/src/modules/ManualJournals/queries/GetManualJournals.service.ts +++ b/packages/server-nest/src/modules/ManualJournals/queries/GetManualJournals.service.ts @@ -1,77 +1,66 @@ -// 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'; +import * as R from 'ramda'; +import { ManualJournalTransfromer } from './ManualJournalTransformer'; +import { Inject, Injectable } from '@nestjs/common'; +import { TransformerInjectable } from '@/modules/Transformer/TransformerInjectable.service'; +import { DynamicListService } from '@/modules/DynamicListing/DynamicList.service'; +import { ManualJournal } from '../models/ManualJournal'; +import { IFilterMeta, IPaginationMeta } from '@/interfaces/Model'; -// @Service() -// export class GetManualJournals { -// @Inject() -// private tenancy: TenancyService; +@Injectable() +export class GetManualJournals { + constructor( + private readonly dynamicListService: DynamicListService, + private readonly transformer: TransformerInjectable, -// @Inject() -// private dynamicListService: DynamicListingService; + @Inject(ManualJournal.name) + private readonly manualJournalModel: typeof ManualJournal, + ) {} -// @Inject() -// private transformer: TransformerInjectable; + /** + * Parses filter DTO of the manual journals list. + * @param filterDTO + */ + private parseListFilterDTO = (filterDTO) => { + return R.compose(this.dynamicListService.parseStringifiedFilter)(filterDTO); + }; -// /** -// * 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 ( + filterDTO: IManualJournalsFilter, + ): Promise<{ + manualJournals: ManualJournal[]; + pagination: IPaginationMeta; + filterMeta: IFilterMeta; + }> => { + // Parses filter DTO. + const filter = this.parseListFilterDTO(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); + // Dynamic service. + const dynamicService = await this.dynamicListService.dynamicList( + ManualJournal, + filter, + ); + const { results, pagination } = await this.manualJournalModel.query() + .onBuild((builder) => { + dynamicService.buildQuery()(builder); + builder.withGraphFetched('entries.account'); + }) + .pagination(filter.page - 1, filter.pageSize); -// // Parses filter DTO. -// const filter = this.parseListFilterDTO(filterDTO); + // Transformes the manual journals models to POJO. + const manualJournals = await this.transformer.transform( + results, + new ManualJournalTransfromer(), + ); -// // 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(), -// }; -// }; -// } + return { + manualJournals, + pagination, + filterMeta: dynamicService.getResponseMeta(), + }; + }; +} diff --git a/packages/server-nest/src/modules/PaymentReceived/PaymentReceived.application.ts b/packages/server-nest/src/modules/PaymentReceived/PaymentReceived.application.ts index 2a6043c9c..8c4a80e64 100644 --- a/packages/server-nest/src/modules/PaymentReceived/PaymentReceived.application.ts +++ b/packages/server-nest/src/modules/PaymentReceived/PaymentReceived.application.ts @@ -17,6 +17,7 @@ import { GetPaymentReceivedInvoices } from './queries/GetPaymentReceivedInvoices import { GetPaymentReceivedPdfService } from './queries/GetPaymentReceivedPdf.service'; // import { SendPaymentReceiveMailNotification } from './PaymentReceivedMailNotification'; import { GetPaymentReceivedStateService } from './queries/GetPaymentReceivedState.service'; +import { GetPaymentsReceivedService } from './queries/GetPaymentsReceived.service'; @Injectable() export class PaymentReceivesApplication { @@ -24,7 +25,7 @@ export class PaymentReceivesApplication { private createPaymentReceivedService: CreatePaymentReceivedService, private editPaymentReceivedService: EditPaymentReceivedService, private deletePaymentReceivedService: DeletePaymentReceivedService, - // private getPaymentsReceivedService: GetPaymentReceives, + private getPaymentsReceivedService: GetPaymentsReceivedService, private getPaymentReceivedService: GetPaymentReceivedService, private getPaymentReceiveInvoicesService: GetPaymentReceivedInvoices, // private paymentSmsNotify: PaymentReceiveNotifyBySms, @@ -77,19 +78,9 @@ export class PaymentReceivesApplication { * @param {IPaymentsReceivedFilter} filterDTO * @returns */ - // public async getPaymentReceives( - // tenantId: number, - // filterDTO: IPaymentsReceivedFilter, - // ): Promise<{ - // paymentReceives: IPaymentReceived[]; - // pagination: IPaginationMeta; - // filterMeta: IFilterMeta; - // }> { - // return this.getPaymentsReceivedService.getPaymentReceives( - // tenantId, - // filterDTO, - // ); - // } + public async getPaymentsReceived(filterDTO: IPaymentsReceivedFilter) { + return this.getPaymentsReceivedService.getPaymentReceives(filterDTO); + } /** * Retrieves the given payment receive. diff --git a/packages/server-nest/src/modules/PaymentReceived/PaymentsReceived.controller.ts b/packages/server-nest/src/modules/PaymentReceived/PaymentsReceived.controller.ts index 885c3c2d9..6257b6cb7 100644 --- a/packages/server-nest/src/modules/PaymentReceived/PaymentsReceived.controller.ts +++ b/packages/server-nest/src/modules/PaymentReceived/PaymentsReceived.controller.ts @@ -7,6 +7,7 @@ import { ParseIntPipe, Post, Put, + Query, } from '@nestjs/common'; import { PaymentReceivesApplication } from './PaymentReceived.application'; import { @@ -49,6 +50,11 @@ export class PaymentReceivesController { ); } + @Get() + public getPaymentsReceived(@Query() filterDTO: IPaymentsReceivedFilter) { + return this.paymentReceivesApplication.getPaymentsReceived(filterDTO); + } + @Get('state') public getPaymentReceivedState() { return this.paymentReceivesApplication.getPaymentReceivedState(); diff --git a/packages/server-nest/src/modules/PaymentReceived/queries/GetPaymentsReceived.service.ts b/packages/server-nest/src/modules/PaymentReceived/queries/GetPaymentsReceived.service.ts new file mode 100644 index 000000000..d5e980cf4 --- /dev/null +++ b/packages/server-nest/src/modules/PaymentReceived/queries/GetPaymentsReceived.service.ts @@ -0,0 +1,66 @@ +import * as R from 'ramda'; +import { PaymentReceiveTransfromer } from './PaymentReceivedTransformer'; +import { Inject, Injectable } from '@nestjs/common'; +import { TransformerInjectable } from '@/modules/Transformer/TransformerInjectable.service'; +import { DynamicListService } from '@/modules/DynamicListing/DynamicList.service'; +import { PaymentReceived } from '../models/PaymentReceived'; +import { IFilterMeta, IPaginationMeta } from '@/interfaces/Model'; + +@Injectable() +export class GetPaymentsReceivedService { + constructor( + private readonly dynamicListService: DynamicListService, + private readonly transformer: TransformerInjectable, + + @Inject(PaymentReceived.name) + private readonly paymentReceivedModel: typeof PaymentReceived, + ) {} + + /** + * Retrieve payment receives paginated and filterable list. + * @param {IPaymentsReceivedFilter} paymentReceivesFilter + */ + public async getPaymentReceives(filterDTO: IPaymentsReceivedFilter): Promise<{ + paymentReceives: PaymentReceived[]; + pagination: IPaginationMeta; + filterMeta: IFilterMeta; + }> { + // Parses filter DTO. + const filter = this.parseListFilterDTO(filterDTO); + + // Dynamic list service. + const dynamicList = await this.dynamicListService.dynamicList( + PaymentReceive, + filter, + ); + const { results, pagination } = await this.paymentReceivedModel + .query() + .onBuild((builder) => { + builder.withGraphFetched('customer'); + builder.withGraphFetched('depositAccount'); + + dynamicList.buildQuery()(builder); + filterDTO?.filterQuery && filterDTO.filterQuery(builder); + }) + .pagination(filter.page - 1, filter.pageSize); + + // Transformer the payment receives models to POJO. + const transformedPayments = await this.transformer.transform( + results, + new PaymentReceiveTransfromer(), + ); + return { + paymentReceives: transformedPayments, + pagination, + filterMeta: dynamicList.getResponseMeta(), + }; + } + + /** + * Parses payments receive list filter DTO. + * @param filterDTO + */ + private parseListFilterDTO(filterDTO) { + return R.compose(this.dynamicListService.parseStringifiedFilter)(filterDTO); + } +} diff --git a/packages/server-nest/src/modules/PaymentReceived/queries/GetPaymentsReceived.ts b/packages/server-nest/src/modules/PaymentReceived/queries/GetPaymentsReceived.ts deleted file mode 100644 index 7c14f4ff1..000000000 --- a/packages/server-nest/src/modules/PaymentReceived/queries/GetPaymentsReceived.ts +++ /dev/null @@ -1,79 +0,0 @@ -// import { Inject, Service } from 'typedi'; -// import * as R from 'ramda'; -// import { -// IFilterMeta, -// IPaginationMeta, -// IPaymentReceived, -// IPaymentsReceivedFilter, -// } from '@/interfaces'; -// import { PaymentReceiveTransfromer } from './PaymentReceivedTransformer'; -// import HasTenancyService from '@/services/Tenancy/TenancyService'; -// import { TransformerInjectable } from '@/lib/Transformer/TransformerInjectable'; -// import DynamicListingService from '@/services/DynamicListing/DynamicListService'; - -// @Service() -// export class GetPaymentReceives { -// @Inject() -// private tenancy: HasTenancyService; - -// @Inject() -// private dynamicListService: DynamicListingService; - -// @Inject() -// private transformer: TransformerInjectable; - -// /** -// * Retrieve payment receives paginated and filterable list. -// * @param {number} tenantId -// * @param {IPaymentsReceivedFilter} paymentReceivesFilter -// */ -// public async getPaymentReceives( -// tenantId: number, -// filterDTO: IPaymentsReceivedFilter -// ): Promise<{ -// paymentReceives: IPaymentReceived[]; -// pagination: IPaginationMeta; -// filterMeta: IFilterMeta; -// }> { -// const { PaymentReceive } = this.tenancy.models(tenantId); - -// // Parses filter DTO. -// const filter = this.parseListFilterDTO(filterDTO); - -// // Dynamic list service. -// const dynamicList = await this.dynamicListService.dynamicList( -// tenantId, -// PaymentReceive, -// filter -// ); -// const { results, pagination } = await PaymentReceive.query() -// .onBuild((builder) => { -// builder.withGraphFetched('customer'); -// builder.withGraphFetched('depositAccount'); - -// dynamicList.buildQuery()(builder); -// filterDTO?.filterQuery && filterDTO.filterQuery(builder); -// }) -// .pagination(filter.page - 1, filter.pageSize); - -// // Transformer the payment receives models to POJO. -// const transformedPayments = await this.transformer.transform( -// tenantId, -// results, -// new PaymentReceiveTransfromer() -// ); -// return { -// paymentReceives: transformedPayments, -// pagination, -// filterMeta: dynamicList.getResponseMeta(), -// }; -// } - -// /** -// * Parses payments receive list filter DTO. -// * @param filterDTO -// */ -// private parseListFilterDTO(filterDTO) { -// return R.compose(this.dynamicListService.parseStringifiedFilter)(filterDTO); -// } -// } diff --git a/packages/server-nest/src/modules/Resource/Resource.module.ts b/packages/server-nest/src/modules/Resource/Resource.module.ts new file mode 100644 index 000000000..e4e3ae54f --- /dev/null +++ b/packages/server-nest/src/modules/Resource/Resource.module.ts @@ -0,0 +1,8 @@ +import { Module } from '@nestjs/common'; +import { ResourceService } from './ResourceService'; + +@Module({ + providers: [ResourceService], + exports: [ResourceService], +}) +export class ResourceModule {} diff --git a/packages/server-nest/src/modules/Resource/ResourceService.ts b/packages/server-nest/src/modules/Resource/ResourceService.ts new file mode 100644 index 000000000..a59fbb522 --- /dev/null +++ b/packages/server-nest/src/modules/Resource/ResourceService.ts @@ -0,0 +1,170 @@ +import { ModuleRef } from '@nestjs/core'; +import { pickBy } from 'lodash'; +import { WarehousesSettings } from '../Warehouses/WarehousesSettings'; +import { Injectable } from '@nestjs/common'; +import { BranchesSettingsService } from '../Branches/BranchesSettings'; +import { ServiceError } from '../Items/ServiceError'; +import { IModelMetaColumn, IModelMetaField2 } from '@/interfaces/Model'; +import { IModelMeta } from '@/interfaces/Model'; +import { IModelMetaField } from '@/interfaces/Model'; +import { Features } from '@/common/types/Features'; +import { resourceToModelName } from './_utils'; + +const ERRORS = { + RESOURCE_MODEL_NOT_FOUND: 'RESOURCE_MODEL_NOT_FOUND', +}; + +@Injectable() +export class ResourceService { + constructor( + private readonly branchesSettings: BranchesSettingsService, + private readonly warehousesSettings: WarehousesSettings, + private readonly moduleRef: ModuleRef, + ) {} + + /** + * Retrieve resource model object. + * @param {string} inputModelName - Input model name. + */ + public getResourceModel(inputModelName: string) { + const modelName = resourceToModelName(inputModelName); + const resourceModel = this.moduleRef.get(modelName); + + if (!resourceModel) { + throw new ServiceError(ERRORS.RESOURCE_MODEL_NOT_FOUND); + } + return resourceModel; + } + + /** + * Retrieve the resource meta. + * @param {string} modelName - Model name. + * @param {string} metakey - Meta key. + * @returns {IModelMeta} + */ + public getResourceMeta(modelName: string, metakey?: string): IModelMeta { + const resourceModel = this.getResourceModel(modelName); + + // Retrieve the resource meta. + const resourceMeta = resourceModel.getMeta(metakey); + + // Localization the fields names. + return resourceMeta; + } + + /** + * Retrieve the resource fields. + * @param {string} modelName + * @returns {IModelMetaField} + */ + public getResourceFields(modelName: string): { + [key: string]: IModelMetaField; + } { + const meta = this.getResourceMeta(modelName); + + return meta.fields; + } + + /** + * Filter the fields based on the features. + * @param {IModelMetaField2} fields + * @returns {IModelMetaField2} + */ + public filterSupportFeatures = ( + fields: Record, + ) => { + const isMultiFeaturesEnabled = + this.branchesSettings.isMultiBranchesActive(); + const isMultiWarehousesEnabled = + this.warehousesSettings.isMultiWarehousesActive(); + + return pickBy(fields, (field) => { + if ( + !isMultiWarehousesEnabled && + field.features?.includes(Features.WAREHOUSES) + ) { + return false; + } + if ( + !isMultiFeaturesEnabled && + field.features?.includes(Features.BRANCHES) + ) { + return false; + } + return true; + }); + }; + + /** + * Retrieve the resource fields. + * @param {string} modelName + * @returns {IModelMetaField2} + */ + public getResourceFields2(modelName: string): { + [key: string]: IModelMetaField2; + } { + const meta = this.getResourceMeta(modelName); + + return this.filterSupportFeatures(meta.fields2); + } + + /** + * Retrieve the resource columns. + * @param {string} modelName - The model name. + * @returns {IModelMetaColumn} + */ + public getResourceColumns(modelName: string) { + const meta = this.getResourceMeta(modelName); + + return this.filterSupportFeatures(meta.columns); + } + + /** + * Retrieve the resource importable fields. + * @param {string} modelName - The model name. + * @returns {IModelMetaField} + */ + public getResourceImportableFields(modelName: string): { + [key: string]: IModelMetaField; + } { + const fields = this.getResourceFields(modelName); + + return pickBy(fields, (field) => field.importable); + } + + /** + * Retrieve the resource meta localized based on the current user language. + */ + // public getResourceMetaLocalized(meta, tenantId) { + // const $enumerationType = (field) => + // field.fieldType === 'enumeration' ? field : undefined; + + // const $hasFields = (field) => + // 'undefined' !== typeof field.fields ? field : undefined; + + // const $ColumnHasColumns = (column) => + // 'undefined' !== typeof column.columns ? column : undefined; + + // const $hasColumns = (columns) => + // 'undefined' !== typeof columns ? columns : undefined; + + // const naviagations = [ + // ['fields', qim.$each, 'name'], + // ['fields', qim.$each, $enumerationType, 'options', qim.$each, 'label'], + // ['fields2', qim.$each, 'name'], + // ['fields2', qim.$each, $enumerationType, 'options', qim.$each, 'label'], + // ['fields2', qim.$each, $hasFields, 'fields', qim.$each, 'name'], + // ['columns', $hasColumns, qim.$each, 'name'], + // [ + // 'columns', + // $hasColumns, + // qim.$each, + // $ColumnHasColumns, + // 'columns', + // qim.$each, + // 'name', + // ], + // ]; + // return this.i18nService.i18nApply(naviagations, meta, tenantId); + // } +} diff --git a/packages/server-nest/src/modules/Resource/_utils.ts b/packages/server-nest/src/modules/Resource/_utils.ts new file mode 100644 index 000000000..bd88a6a1c --- /dev/null +++ b/packages/server-nest/src/modules/Resource/_utils.ts @@ -0,0 +1,6 @@ +import { camelCase, upperFirst } from 'lodash'; +import pluralize from 'pluralize'; + +export const resourceToModelName = (resourceName: string): string => { + return upperFirst(camelCase(pluralize.singular(resourceName))); +}; diff --git a/packages/server-nest/src/modules/Resource/models/ResourcableModel.ts b/packages/server-nest/src/modules/Resource/models/ResourcableModel.ts new file mode 100644 index 000000000..3c4b7fec0 --- /dev/null +++ b/packages/server-nest/src/modules/Resource/models/ResourcableModel.ts @@ -0,0 +1,12 @@ +import { BaseModel } from '@/models/Model'; + +type GConstructor = new (...args: any[]) => T; + +export const ResourceableModelMixin = >( + Model: T, +) => + class ResourceableModel extends Model { + static get resourceable() { + return true; + } + }; diff --git a/packages/server-nest/src/modules/SaleEstimates/SaleEstimates.application.ts b/packages/server-nest/src/modules/SaleEstimates/SaleEstimates.application.ts index 8c2fec49b..7a22cab5a 100644 --- a/packages/server-nest/src/modules/SaleEstimates/SaleEstimates.application.ts +++ b/packages/server-nest/src/modules/SaleEstimates/SaleEstimates.application.ts @@ -21,6 +21,7 @@ import { RejectSaleEstimateService } from './commands/RejectSaleEstimate.service import { GetSaleEstimateState } from './queries/GetSaleEstimateState.service'; import { Injectable } from '@nestjs/common'; import { IFilterMeta, IPaginationMeta } from '@/interfaces/Model'; +import { GetSaleEstimatesService } from './queries/GetSaleEstimates.service'; @Injectable() export class SaleEstimatesApplication { @@ -29,7 +30,7 @@ export class SaleEstimatesApplication { private readonly editSaleEstimateService: EditSaleEstimate, private readonly deleteSaleEstimateService: DeleteSaleEstimate, private readonly getSaleEstimateService: GetSaleEstimate, - // private readonly getSaleEstimatesService: GetSaleEstimates, + private readonly getSaleEstimatesService: GetSaleEstimatesService, private readonly deliverSaleEstimateService: DeliverSaleEstimateService, private readonly approveSaleEstimateService: ApproveSaleEstimateService, private readonly rejectSaleEstimateService: RejectSaleEstimateService, @@ -80,9 +81,9 @@ export class SaleEstimatesApplication { * @param {ISalesEstimatesFilter} filterDTO - Sales estimates filter DTO. * @returns */ - // public getSaleEstimates(filterDTO: ISalesEstimatesFilter) { - // return this.getSaleEstimatesService.getEstimates(filterDTO); - // } + public getSaleEstimates(filterDTO: ISalesEstimatesFilter) { + return this.getSaleEstimatesService.getEstimates(filterDTO); + } /** * Deliver the given sale estimate. @@ -148,8 +149,7 @@ export class SaleEstimatesApplication { * @param {number} saleEstimateId - Sale estimate ID. * @returns {Promise} */ - public sendSaleEstimateMail() // saleEstimateId: number, - // saleEstimateMailOpts: SaleEstimateMailOptionsDTO, + public sendSaleEstimateMail() // saleEstimateMailOpts: SaleEstimateMailOptionsDTO, // saleEstimateId: number, { // return this.sendEstimateMailService.triggerMail( // saleEstimateId, diff --git a/packages/server-nest/src/modules/SaleEstimates/SaleEstimates.controller.ts b/packages/server-nest/src/modules/SaleEstimates/SaleEstimates.controller.ts index 3b3db294a..d19a5e488 100644 --- a/packages/server-nest/src/modules/SaleEstimates/SaleEstimates.controller.ts +++ b/packages/server-nest/src/modules/SaleEstimates/SaleEstimates.controller.ts @@ -58,10 +58,10 @@ export class SaleEstimatesController { return this.saleEstimatesApplication.getSaleEstimateState(); } - // @Get() - // public getSaleEstimates(@Query() filterDTO: ISalesEstimatesFilter) { - // return this.saleEstimatesApplication.getSaleEstimates(filterDTO); - // } + @Get() + public getSaleEstimates(@Query() filterDTO: ISalesEstimatesFilter) { + return this.saleEstimatesApplication.getSaleEstimates(filterDTO); + } @Post(':id/deliver') public deliverSaleEstimate( diff --git a/packages/server-nest/src/modules/SaleEstimates/queries/GetSaleEstimates.service.ts b/packages/server-nest/src/modules/SaleEstimates/queries/GetSaleEstimates.service.ts new file mode 100644 index 000000000..589593442 --- /dev/null +++ b/packages/server-nest/src/modules/SaleEstimates/queries/GetSaleEstimates.service.ts @@ -0,0 +1,68 @@ +import * as R from 'ramda'; +import { SaleEstimateTransfromer } from './SaleEstimate.transformer'; +import { DynamicListService } from '@/modules/DynamicListing/DynamicList.service'; +import { SaleEstimate } from '../models/SaleEstimate'; +import { Inject, Injectable } from '@nestjs/common'; +import { TransformerInjectable } from '@/modules/Transformer/TransformerInjectable.service'; +import { IFilterMeta, IPaginationMeta } from '@/interfaces/Model'; +import { ISalesEstimatesFilter } from '../types/SaleEstimates.types'; + +@Injectable() +export class GetSaleEstimatesService { + constructor( + private readonly dynamicListService: DynamicListService, + private readonly transformer: TransformerInjectable, + + @Inject(SaleEstimate.name) + private readonly saleEstimateModel: typeof SaleEstimate, + ) {} + + /** + * Retrieves estimates filterable and paginated list. + * @param {IEstimatesFilter} estimatesFilter - + */ + public async getEstimates( + filterDTO: ISalesEstimatesFilter + ): Promise<{ + salesEstimates: SaleEstimate[]; + pagination: IPaginationMeta; + filterMeta: IFilterMeta; + }> { + // Parses filter DTO. + const filter = this.parseListFilterDTO(filterDTO); + + // Dynamic list service. + const dynamicFilter = await this.dynamicListService.dynamicList( + SaleEstimate, + filter + ); + const { results, pagination } = await this.saleEstimateModel.query() + .onBuild((builder) => { + builder.withGraphFetched('customer'); + builder.withGraphFetched('entries'); + builder.withGraphFetched('entries.item'); + + dynamicFilter.buildQuery()(builder); + filterDTO?.filterQuery && filterDTO?.filterQuery(builder); + }) + .pagination(filter.page - 1, filter.pageSize); + + const transformedEstimates = await this.transformer.transform( + results, + new SaleEstimateTransfromer() + ); + return { + salesEstimates: transformedEstimates, + pagination, + filterMeta: dynamicFilter.getResponseMeta(), + }; + } + + /** + * Parses the sale receipts list filter DTO. + * @param filterDTO + */ + private parseListFilterDTO(filterDTO) { + return R.compose(this.dynamicListService.parseStringifiedFilter)(filterDTO); + } +} diff --git a/packages/server-nest/src/modules/SaleEstimates/queries/GetSaleEstimates.ts b/packages/server-nest/src/modules/SaleEstimates/queries/GetSaleEstimates.ts deleted file mode 100644 index a29814130..000000000 --- a/packages/server-nest/src/modules/SaleEstimates/queries/GetSaleEstimates.ts +++ /dev/null @@ -1,79 +0,0 @@ -// import * as R from 'ramda'; -// import { Inject, Service } from 'typedi'; -// import { -// IFilterMeta, -// IPaginationMeta, -// ISaleEstimate, -// ISalesEstimatesFilter, -// } from '@/interfaces'; -// import HasTenancyService from '@/services/Tenancy/TenancyService'; -// import DynamicListingService from '@/services/DynamicListing/DynamicListService'; -// import { TransformerInjectable } from '@/lib/Transformer/TransformerInjectable'; -// import { SaleEstimateDTOTransformer } from '../commands/SaleEstimateDTOTransformer'; -// import { SaleEstimateTransfromer } from './SaleEstimate.transformer'; - -// @Service() -// export class GetSaleEstimates { -// @Inject() -// private tenancy: HasTenancyService; - -// @Inject() -// private dynamicListService: DynamicListingService; - -// @Inject() -// private transformer: TransformerInjectable; - -// /** -// * Retrieves estimates filterable and paginated list. -// * @param {number} tenantId - -// * @param {IEstimatesFilter} estimatesFilter - -// */ -// public async getEstimates( -// tenantId: number, -// filterDTO: ISalesEstimatesFilter -// ): Promise<{ -// salesEstimates: ISaleEstimate[]; -// pagination: IPaginationMeta; -// filterMeta: IFilterMeta; -// }> { -// const { SaleEstimate } = this.tenancy.models(tenantId); - -// // Parses filter DTO. -// const filter = this.parseListFilterDTO(filterDTO); - -// // Dynamic list service. -// const dynamicFilter = await this.dynamicListService.dynamicList( -// tenantId, -// SaleEstimate, -// filter -// ); -// const { results, pagination } = await SaleEstimate.query() -// .onBuild((builder) => { -// builder.withGraphFetched('customer'); -// builder.withGraphFetched('entries'); -// builder.withGraphFetched('entries.item'); -// dynamicFilter.buildQuery()(builder); -// filterDTO?.filterQuery && filterDTO?.filterQuery(builder); -// }) -// .pagination(filter.page - 1, filter.pageSize); - -// const transformedEstimates = await this.transformer.transform( -// tenantId, -// results, -// new SaleEstimateTransfromer() -// ); -// return { -// salesEstimates: transformedEstimates, -// pagination, -// filterMeta: dynamicFilter.getResponseMeta(), -// }; -// } - -// /** -// * Parses the sale receipts list filter DTO. -// * @param filterDTO -// */ -// private parseListFilterDTO(filterDTO) { -// return R.compose(this.dynamicListService.parseStringifiedFilter)(filterDTO); -// } -// } diff --git a/packages/server-nest/src/modules/SaleEstimates/types/SaleEstimates.types.ts b/packages/server-nest/src/modules/SaleEstimates/types/SaleEstimates.types.ts index a10b3eeca..11ce07ee5 100644 --- a/packages/server-nest/src/modules/SaleEstimates/types/SaleEstimates.types.ts +++ b/packages/server-nest/src/modules/SaleEstimates/types/SaleEstimates.types.ts @@ -4,6 +4,7 @@ import { Knex } from 'knex'; import { SaleEstimate } from '../models/SaleEstimate'; import { IItemEntryDTO } from '@/modules/TransactionItemEntry/ItemEntry.types'; import { AttachmentLinkDTO } from '@/modules/Attachments/Attachments.types'; +import { IDynamicListFilter } from '@/modules/DynamicListing/DynamicFilter/DynamicFilter.types'; export interface ISaleEstimateDTO { customerId: number; @@ -22,10 +23,10 @@ export interface ISaleEstimateDTO { attachments?: AttachmentLinkDTO[]; } -// export interface ISalesEstimatesFilter extends IDynamicListFilterDTO { -// stringifiedFilterRoles?: string; -// filterQuery?: (q: any) => void; -// } +export interface ISalesEstimatesFilter extends IDynamicListFilter { + stringifiedFilterRoles?: string; + filterQuery?: (q: any) => void; +} export interface ISaleEstimateCreatedPayload { // tenantId: number; diff --git a/packages/server-nest/src/modules/SaleInvoices/commands/inventory/InvoiceInventoryTransactions.ts b/packages/server-nest/src/modules/SaleInvoices/commands/inventory/InvoiceInventoryTransactions.ts new file mode 100644 index 000000000..c04413836 --- /dev/null +++ b/packages/server-nest/src/modules/SaleInvoices/commands/inventory/InvoiceInventoryTransactions.ts @@ -0,0 +1,77 @@ +import { Inject, Service } from 'typedi'; +import { Knex } from 'knex'; +import { ISaleInvoice } from '@/interfaces'; +import ItemsEntriesService from '@/services/Items/ItemsEntriesService'; +import InventoryService from '@/services/Inventory/Inventory'; + +@Service() +export class InvoiceInventoryTransactions { + @Inject() + private itemsEntriesService: ItemsEntriesService; + + @Inject() + private inventoryService: InventoryService; + + /** + * Records the inventory transactions of the given sale invoice in case + * the invoice has inventory entries only. + * + * @param {number} tenantId - Tenant id. + * @param {SaleInvoice} saleInvoice - Sale invoice DTO. + * @param {number} saleInvoiceId - Sale invoice id. + * @param {boolean} override - Allow to override old transactions. + * @return {Promise} + */ + public async recordInventoryTranscactions( + saleInvoice: ISaleInvoice, + override?: boolean, + trx?: Knex.Transaction + ): Promise { + // Loads the inventory items entries of the given sale invoice. + const inventoryEntries = + await this.itemsEntriesService.filterInventoryEntries( + tenantId, + saleInvoice.entries, + trx + ); + const transaction = { + transactionId: saleInvoice.id, + transactionType: 'SaleInvoice', + transactionNumber: saleInvoice.invoiceNo, + + exchangeRate: saleInvoice.exchangeRate, + warehouseId: saleInvoice.warehouseId, + + date: saleInvoice.invoiceDate, + direction: 'OUT', + entries: inventoryEntries, + createdAt: saleInvoice.createdAt, + }; + await this.inventoryService.recordInventoryTransactionsFromItemsEntries( + tenantId, + transaction, + override, + trx + ); + } + + /** + * Reverting the inventory transactions once the invoice deleted. + * @param {number} tenantId - Tenant id. + * @param {number} billId - Bill id. + * @return {Promise} + */ + public async revertInventoryTransactions( + saleInvoiceId: number, + trx?: Knex.Transaction + ): Promise { + // Delete the inventory transaction of the given sale invoice. + const { oldInventoryTransactions } = + await this.inventoryService.deleteInventoryTransactions( + tenantId, + saleInvoiceId, + 'SaleInvoice', + trx + ); + } +} diff --git a/packages/server-nest/src/modules/SaleInvoices/subscribers/InvoiceWriteInventoryTransactions.ts b/packages/server-nest/src/modules/SaleInvoices/subscribers/InvoiceWriteInventoryTransactions.ts new file mode 100644 index 000000000..8e3de8f7e --- /dev/null +++ b/packages/server-nest/src/modules/SaleInvoices/subscribers/InvoiceWriteInventoryTransactions.ts @@ -0,0 +1,68 @@ +import { Injectable } from '@nestjs/common'; +import { OnEvent } from '@nestjs/event-emitter'; +import { InvoiceInventoryTransactions } from '../commands/inventory/InvoiceInventoryTransactions'; +import { events } from '@/common/events/events'; +import { + ISaleInvoiceCreatedPayload, + ISaleInvoiceEditedPayload, + ISaleInvoiceDeletedPayload, + ISaleInvoiceEventDeliveredPayload, +} from '../SaleInvoice.types'; + +@Injectable() +export class SaleInvoiceWriteInventoryTransactionsSubscriber { + constructor( + private readonly saleInvoiceInventory: InvoiceInventoryTransactions, + ) {} + + /** + * Handles the writing inventory transactions once the invoice created. + * @param {ISaleInvoiceCreatedPayload} payload + */ + @OnEvent(events.saleInvoice.onCreated) + public async handleWritingInventoryTransactions({ + saleInvoice, + trx, + }: ISaleInvoiceCreatedPayload | ISaleInvoiceEventDeliveredPayload) { + // Can't continue if the sale invoice is not delivered yet. + if (!saleInvoice.deliveredAt) return null; + + await this.saleInvoiceInventory.recordInventoryTranscactions( + saleInvoice, + false, + trx, + ); + } + + /** + * Rewriting the inventory transactions once the sale invoice be edited. + * @param {ISaleInvoiceEditPayload} payload - + */ + @OnEvent(events.saleInvoice.onEdited) + public async handleRewritingInventoryTransactions({ + saleInvoice, + trx, + }: ISaleInvoiceEditedPayload) { + await this.saleInvoiceInventory.recordInventoryTranscactions( + saleInvoice, + true, + trx, + ); + } + + /** + * Handles deleting the inventory transactions once the invoice deleted. + * @param {ISaleInvoiceDeletedPayload} payload - + */ + @OnEvent(events.saleInvoice.onDeleted) + public async handleDeletingInventoryTransactions({ + saleInvoiceId, + oldSaleInvoice, + trx, + }: ISaleInvoiceDeletedPayload) { + await this.saleInvoiceInventory.revertInventoryTransactions( + saleInvoiceId, + trx, + ); + } +} diff --git a/packages/server-nest/src/modules/SaleReceipts/SaleReceiptApplication.service.ts b/packages/server-nest/src/modules/SaleReceipts/SaleReceiptApplication.service.ts index fad6b7153..2bd2a7d07 100644 --- a/packages/server-nest/src/modules/SaleReceipts/SaleReceiptApplication.service.ts +++ b/packages/server-nest/src/modules/SaleReceipts/SaleReceiptApplication.service.ts @@ -8,7 +8,14 @@ import { CloseSaleReceipt } from './commands/CloseSaleReceipt.service'; import { DeleteSaleReceipt } from './commands/DeleteSaleReceipt.service'; import { GetSaleReceipt } from './queries/GetSaleReceipt.service'; import { EditSaleReceipt } from './commands/EditSaleReceipt.service'; -import { ISaleReceiptDTO, ISaleReceiptState } from './types/SaleReceipts.types'; +import { + ISaleReceiptDTO, + ISaleReceiptState, + ISalesReceiptsFilter, +} from './types/SaleReceipts.types'; +import { GetSaleReceiptsService } from './queries/GetSaleReceipts.service'; +import { SaleReceipt } from './models/SaleReceipt'; +import { IPaginationMeta } from '@/interfaces/Model'; @Injectable() export class SaleReceiptApplication { @@ -17,7 +24,7 @@ export class SaleReceiptApplication { private editSaleReceiptService: EditSaleReceipt, private getSaleReceiptService: GetSaleReceipt, private deleteSaleReceiptService: DeleteSaleReceipt, - // private getSaleReceiptsService: GetSaleReceipts, + private getSaleReceiptsService: GetSaleReceiptsService, private closeSaleReceiptService: CloseSaleReceipt, private getSaleReceiptPdfService: SaleReceiptsPdfService, // private saleReceiptNotifyBySmsService: SaleReceiptNotifyBySms, @@ -74,20 +81,16 @@ export class SaleReceiptApplication { /** * Retrieve sales receipts paginated and filterable list. - * @param {number} tenantId * @param {ISalesReceiptsFilter} filterDTO * @returns */ - // public async getSaleReceipts( - // tenantId: number, - // filterDTO: ISalesReceiptsFilter, - // ): Promise<{ - // data: ISaleReceipt[]; - // pagination: IPaginationMeta; - // filterMeta: IFilterMeta; - // }> { - // return this.getSaleReceiptsService.getSaleReceipts(tenantId, filterDTO); - // } + public async getSaleReceipts(filterDTO: ISalesReceiptsFilter): Promise<{ + data: SaleReceipt[]; + pagination: IPaginationMeta; + filterMeta: IFilterMeta; + }> { + return this.getSaleReceiptsService.getSaleReceipts(filterDTO); + } /** * Closes the given sale receipt. @@ -106,9 +109,7 @@ export class SaleReceiptApplication { * @returns */ public getSaleReceiptPdf(tenantId: number, saleReceiptId: number) { - return this.getSaleReceiptPdfService.saleReceiptPdf( - saleReceiptId, - ); + return this.getSaleReceiptPdfService.saleReceiptPdf(saleReceiptId); } /** diff --git a/packages/server-nest/src/modules/SaleReceipts/SaleReceipts.module.ts b/packages/server-nest/src/modules/SaleReceipts/SaleReceipts.module.ts index 71040089d..735d76f11 100644 --- a/packages/server-nest/src/modules/SaleReceipts/SaleReceipts.module.ts +++ b/packages/server-nest/src/modules/SaleReceipts/SaleReceipts.module.ts @@ -24,6 +24,8 @@ import { SaleReceiptGLEntriesSubscriber } from './subscribers/SaleReceiptGLEntri import { SaleReceiptGLEntries } from './ledger/SaleReceiptGLEntries'; import { LedgerModule } from '../Ledger/Ledger.module'; import { AccountsModule } from '../Accounts/Accounts.module'; +import { SaleReceiptInventoryTransactionsSubscriber } from './inventory/SaleReceiptWriteInventoryTransactions'; +import { GetSaleReceiptsService } from './queries/GetSaleReceipts.service'; @Module({ controllers: [SaleReceiptsController], @@ -53,7 +55,9 @@ import { AccountsModule } from '../Accounts/Accounts.module'; SaleReceiptBrandingTemplate, SaleReceiptIncrement, SaleReceiptGLEntries, - SaleReceiptGLEntriesSubscriber + SaleReceiptGLEntriesSubscriber, + SaleReceiptInventoryTransactionsSubscriber, + GetSaleReceiptsService ], }) export class SaleReceiptsModule {} diff --git a/packages/server-nest/src/modules/SaleReceipts/inventory/SaleReceiptInventoryTransactions.ts b/packages/server-nest/src/modules/SaleReceipts/inventory/SaleReceiptInventoryTransactions.ts new file mode 100644 index 000000000..64cb2546f --- /dev/null +++ b/packages/server-nest/src/modules/SaleReceipts/inventory/SaleReceiptInventoryTransactions.ts @@ -0,0 +1,66 @@ +import { Injectable } from '@nestjs/common'; +import { Knex } from 'knex'; +import { InventoryService } from '@/modules/InventoryCost/Inventory'; +import { ItemsEntriesService } from '@/modules/Items/ItemsEntries.service'; +import { SaleReceipt } from '../models/SaleReceipt'; + +@Injectable() +export class SaleReceiptInventoryTransactions { + constructor( + private readonly itemsEntriesService: ItemsEntriesService, + private readonly inventoryService: InventoryService, + ) {} + + /** + * Records the inventory transactions from the given bill input. + * @param {Bill} bill - Bill model object. + * @param {number} billId - Bill id. + * @return {Promise} + */ + public async recordInventoryTransactions( + saleReceipt: SaleReceipt, + override?: boolean, + trx?: Knex.Transaction, + ): Promise { + // Loads the inventory items entries of the given sale invoice. + const inventoryEntries = + await this.itemsEntriesService.filterInventoryEntries( + saleReceipt.entries, + ); + const transaction = { + transactionId: saleReceipt.id, + transactionType: 'SaleReceipt', + transactionNumber: saleReceipt.receiptNumber, + exchangeRate: saleReceipt.exchangeRate, + + date: saleReceipt.receiptDate, + direction: 'OUT', + entries: inventoryEntries, + createdAt: saleReceipt.createdAt, + + warehouseId: saleReceipt.warehouseId, + }; + return this.inventoryService.recordInventoryTransactionsFromItemsEntries( + transaction, + override, + trx, + ); + } + + /** + * Reverts the inventory transactions of the given bill id. + * @param {number} tenantId - Tenant id. + * @param {number} billId - Bill id. + * @return {Promise} + */ + public async revertInventoryTransactions( + receiptId: number, + trx?: Knex.Transaction, + ) { + return this.inventoryService.deleteInventoryTransactions( + receiptId, + 'SaleReceipt', + trx, + ); + } +} diff --git a/packages/server-nest/src/modules/SaleReceipts/inventory/SaleReceiptWriteInventoryTransactions.ts b/packages/server-nest/src/modules/SaleReceipts/inventory/SaleReceiptWriteInventoryTransactions.ts new file mode 100644 index 000000000..8c1b10c09 --- /dev/null +++ b/packages/server-nest/src/modules/SaleReceipts/inventory/SaleReceiptWriteInventoryTransactions.ts @@ -0,0 +1,69 @@ +import { OnEvent } from '@nestjs/event-emitter'; +import { + ISaleReceiptCreatedPayload, + ISaleReceiptEditedPayload, + ISaleReceiptEventDeletedPayload, +} from '../types/SaleReceipts.types'; +import { Injectable } from '@nestjs/common'; +import { events } from '@/common/events/events'; +import { SaleReceiptInventoryTransactions } from './SaleReceiptInventoryTransactions'; + +@Injectable() +export class SaleReceiptInventoryTransactionsSubscriber { + constructor( + private readonly saleReceiptInventory: SaleReceiptInventoryTransactions + ) {} + + /** + * Handles the writing inventory transactions once the receipt created. + * @param {ISaleReceiptCreatedPayload} payload - + */ + @OnEvent(events.saleReceipt.onCreated) + public async handleWritingInventoryTransactions({ + saleReceipt, + trx, + }: ISaleReceiptCreatedPayload) { + // Can't continue if the sale receipt is not closed yet. + if (!saleReceipt.closedAt) return null; + + await this.saleReceiptInventory.recordInventoryTransactions( + saleReceipt, + false, + trx + ); + }; + + /** + * Rewriting the inventory transactions once the sale invoice be edited. + * @param {ISaleReceiptEditedPayload} payload - + */ + @OnEvent(events.saleReceipt.onEdited) + public async handleRewritingInventoryTransactions({ + saleReceipt, + trx, + }: ISaleReceiptEditedPayload) { + // Can't continue if the sale receipt is not closed yet. + if (!saleReceipt.closedAt) return null; + + await this.saleReceiptInventory.recordInventoryTransactions( + saleReceipt, + true, + trx + ); + }; + + /** + * Handles deleting the inventory transactions once the receipt deleted. + * @param {ISaleReceiptEventDeletedPayload} payload - + */ + @OnEvent(events.saleReceipt.onDeleted) + public async handleDeletingInventoryTransactions({ + saleReceiptId, + trx, + }: ISaleReceiptEventDeletedPayload) { + await this.saleReceiptInventory.revertInventoryTransactions( + saleReceiptId, + trx + ); + }; +} diff --git a/packages/server-nest/src/modules/SaleReceipts/queries/GetSaleReceipts.service.ts b/packages/server-nest/src/modules/SaleReceipts/queries/GetSaleReceipts.service.ts new file mode 100644 index 000000000..ee6014873 --- /dev/null +++ b/packages/server-nest/src/modules/SaleReceipts/queries/GetSaleReceipts.service.ts @@ -0,0 +1,73 @@ +import * as R from 'ramda'; +import { SaleReceiptTransformer } from './SaleReceiptTransformer'; +import { Inject, Injectable } from '@nestjs/common'; +import { TransformerInjectable } from '@/modules/Transformer/TransformerInjectable.service'; +import { DynamicListService } from '@/modules/DynamicListing/DynamicList.service'; +import { ISalesReceiptsFilter } from '../types/SaleReceipts.types'; +import { SaleReceipt } from '../models/SaleReceipt'; +import { IPaginationMeta } from '@/interfaces/Model'; + +interface GetSaleReceiptsSettings { + fetchEntriesGraph?: boolean; +} +@Injectable() +export class GetSaleReceiptsService { + constructor( + private readonly transformer: TransformerInjectable, + private readonly dynamicListService: DynamicListService, + + @Inject(SaleReceipt.name) + private readonly saleReceiptModel: typeof SaleReceipt, + ) {} + + /** + * Retrieve sales receipts paginated and filterable list. + * @param {number} tenantId + * @param {ISaleReceiptFilter} salesReceiptsFilter + */ + public async getSaleReceipts(filterDTO: ISalesReceiptsFilter): Promise<{ + data: SaleReceipt[]; + pagination: IPaginationMeta; + filterMeta: IFilterMeta; + }> { + // Parses the stringified filter roles. + const filter = this.parseListFilterDTO(filterDTO); + + // Dynamic list service. + const dynamicFilter = await this.dynamicListService.dynamicList( + SaleReceipt, + filter, + ); + const { results, pagination } = await this.saleReceiptModel + .query() + .onBuild((builder) => { + builder.withGraphFetched('depositAccount'); + builder.withGraphFetched('customer'); + builder.withGraphFetched('entries.item'); + + dynamicFilter.buildQuery()(builder); + filterDTO?.filterQuery && filterDTO?.filterQuery(builder); + }) + .pagination(filter.page - 1, filter.pageSize); + + // Transformes the estimates models to POJO. + const salesEstimates = await this.transformer.transform( + results, + new SaleReceiptTransformer(), + ); + return { + data: salesEstimates, + pagination, + filterMeta: dynamicFilter.getResponseMeta(), + }; + } + + /** + * Parses the sale invoice list filter DTO. + * @param filterDTO + * @returns + */ + private parseListFilterDTO(filterDTO) { + return R.compose(this.dynamicListService.parseStringifiedFilter)(filterDTO); + } +} diff --git a/packages/server-nest/src/modules/SaleReceipts/queries/GetSaleReceipts.ts b/packages/server-nest/src/modules/SaleReceipts/queries/GetSaleReceipts.ts deleted file mode 100644 index eaa0d38a2..000000000 --- a/packages/server-nest/src/modules/SaleReceipts/queries/GetSaleReceipts.ts +++ /dev/null @@ -1,84 +0,0 @@ -// import * as R from 'ramda'; -// import { -// IFilterMeta, -// IPaginationMeta, -// ISaleReceipt, -// ISalesReceiptsFilter, -// } from '@/interfaces'; -// import HasTenancyService from '@/services/Tenancy/TenancyService'; -// import { Inject, Service } from 'typedi'; -// import { SaleReceiptTransformer } from './SaleReceiptTransformer'; -// import { TransformerInjectable } from '@/lib/Transformer/TransformerInjectable'; -// import DynamicListingService from '@/services/DynamicListing/DynamicListService'; - -// interface GetSaleReceiptsSettings { -// fetchEntriesGraph?: boolean; -// } -// @Service() -// export class GetSaleReceipts { -// @Inject() -// private tenancy: HasTenancyService; - -// @Inject() -// private transformer: TransformerInjectable; - -// @Inject() -// private dynamicListService: DynamicListingService; - -// /** -// * Retrieve sales receipts paginated and filterable list. -// * @param {number} tenantId -// * @param {ISaleReceiptFilter} salesReceiptsFilter -// */ -// public async getSaleReceipts( -// tenantId: number, -// filterDTO: ISalesReceiptsFilter -// ): Promise<{ -// data: ISaleReceipt[]; -// pagination: IPaginationMeta; -// filterMeta: IFilterMeta; -// }> { -// const { SaleReceipt } = this.tenancy.models(tenantId); - -// // Parses the stringified filter roles. -// const filter = this.parseListFilterDTO(filterDTO); - -// // Dynamic list service. -// const dynamicFilter = await this.dynamicListService.dynamicList( -// tenantId, -// SaleReceipt, -// filter -// ); -// const { results, pagination } = await SaleReceipt.query() -// .onBuild((builder) => { -// builder.withGraphFetched('depositAccount'); -// builder.withGraphFetched('customer'); -// builder.withGraphFetched('entries.item'); - -// dynamicFilter.buildQuery()(builder); -// filterDTO?.filterQuery && filterDTO?.filterQuery(builder); -// }) -// .pagination(filter.page - 1, filter.pageSize); - -// // Transformes the estimates models to POJO. -// const salesEstimates = await this.transformer.transform( -// tenantId, -// results, -// new SaleReceiptTransformer() -// ); -// return { -// data: salesEstimates, -// pagination, -// filterMeta: dynamicFilter.getResponseMeta(), -// }; -// } - -// /** -// * Parses the sale invoice list filter DTO. -// * @param filterDTO -// * @returns -// */ -// private parseListFilterDTO(filterDTO) { -// return R.compose(this.dynamicListService.parseStringifiedFilter)(filterDTO); -// } -// } diff --git a/packages/server-nest/src/modules/TaxRates/TaxRate.application.ts b/packages/server-nest/src/modules/TaxRates/TaxRate.application.ts index 93a5c9a22..a10ca1832 100644 --- a/packages/server-nest/src/modules/TaxRates/TaxRate.application.ts +++ b/packages/server-nest/src/modules/TaxRates/TaxRate.application.ts @@ -7,6 +7,7 @@ import { ActivateTaxRateService } from './commands/ActivateTaxRate.service'; import { InactivateTaxRateService } from './commands/InactivateTaxRate'; import { Injectable } from '@nestjs/common'; import { ICreateTaxRateDTO, IEditTaxRateDTO } from './TaxRates.types'; +import { GetTaxRatesService } from './queries/GetTaxRates.service'; @Injectable() export class TaxRatesApplication { @@ -17,7 +18,7 @@ export class TaxRatesApplication { private readonly getTaxRateService: GetTaxRateService, private readonly activateTaxRateService: ActivateTaxRateService, private readonly inactivateTaxRateService: InactivateTaxRateService, - // private readonly getTaxRatesService: GetTaxRatesService, + private readonly getTaxRatesService: GetTaxRatesService, ) {} /** @@ -56,17 +57,16 @@ export class TaxRatesApplication { * @param {number} taxRateId * @returns {Promise} */ - public getTaxRate(tenantId: number, taxRateId: number) { + public getTaxRate(taxRateId: number) { return this.getTaxRateService.getTaxRate(taxRateId); } /** * Retrieves the tax rates list. - * @param {number} tenantId * @returns {Promise} */ - public getTaxRates(tenantId: number) { - // return this.getTaxRatesService.getTaxRates(tenantId); + public getTaxRates() { + return this.getTaxRatesService.getTaxRates(); } /** diff --git a/packages/server-nest/src/modules/TaxRates/TaxRate.controller.ts b/packages/server-nest/src/modules/TaxRates/TaxRate.controller.ts index 3b26d8da1..b4e0165c0 100644 --- a/packages/server-nest/src/modules/TaxRates/TaxRate.controller.ts +++ b/packages/server-nest/src/modules/TaxRates/TaxRate.controller.ts @@ -35,16 +35,13 @@ export class TaxRatesController { } @Get(':id') - public getTaxRate( - @Param('tenantId') tenantId: number, - @Param('id') taxRateId: number, - ) { - return this.taxRatesApplication.getTaxRate(tenantId, taxRateId); + public getTaxRate(@Param('id') taxRateId: number) { + return this.taxRatesApplication.getTaxRate(taxRateId); } @Get() - public getTaxRates(@Param('tenantId') tenantId: number) { - return this.taxRatesApplication.getTaxRates(tenantId); + public getTaxRates() { + return this.taxRatesApplication.getTaxRates(); } @Put(':id/activate') diff --git a/packages/server-nest/src/modules/TaxRates/queries/GetTaxRates.service.ts b/packages/server-nest/src/modules/TaxRates/queries/GetTaxRates.service.ts index a62432ed4..72c22bfe9 100644 --- a/packages/server-nest/src/modules/TaxRates/queries/GetTaxRates.service.ts +++ b/packages/server-nest/src/modules/TaxRates/queries/GetTaxRates.service.ts @@ -1,32 +1,24 @@ -// import { Inject, Service } from 'typedi'; -// import HasTenancyService from '../Tenancy/TenancyService'; -// import { TransformerInjectable } from '@/lib/Transformer/TransformerInjectable'; -// import { TaxRateTransformer } from './TaxRate.transformer'; +import { Inject, Injectable } from '@nestjs/common'; +import { TaxRateTransformer } from './TaxRate.transformer'; +import { TransformerInjectable } from '@/modules/Transformer/TransformerInjectable.service'; +import { TaxRateModel } from '../models/TaxRate.model'; -// @Service() -// export class GetTaxRatesService { -// @Inject() -// private tenancy: HasTenancyService; +@Injectable() +export class GetTaxRatesService { + constructor( + private transformer: TransformerInjectable, + @Inject(TaxRateModel.name) private taxRateModel: typeof TaxRateModel, + ) {} -// @Inject() -// private transformer: TransformerInjectable; + /** + * Retrieves the tax rates list. + * @returns {Promise} + */ + public async getTaxRates() { + // Retrieves the tax rates. + const taxRates = await this.taxRateModel.query().orderBy('name', 'ASC'); -// /** -// * Retrieves the tax rates list. -// * @param {number} tenantId -// * @returns {Promise} -// */ -// public async getTaxRates(tenantId: number) { -// const { TaxRate } = this.tenancy.models(tenantId); - -// // Retrieves the tax rates. -// const taxRates = await TaxRate.query().orderBy('name', 'ASC'); - -// // Transforms the tax rates. -// return this.transformer.transform( -// tenantId, -// taxRates, -// new TaxRateTransformer() -// ); -// } -// } + // Transforms the tax rates. + return this.transformer.transform(taxRates, new TaxRateTransformer()); + } +} diff --git a/packages/server-nest/src/modules/VendorCredit/VendorCredits.module.ts b/packages/server-nest/src/modules/VendorCredit/VendorCredits.module.ts index 00c7d79e3..6434bcdc7 100644 --- a/packages/server-nest/src/modules/VendorCredit/VendorCredits.module.ts +++ b/packages/server-nest/src/modules/VendorCredit/VendorCredits.module.ts @@ -20,6 +20,9 @@ import { VendorCreditGlEntriesSubscriber } from './subscribers/VendorCreditGLEnt import { VendorCreditGLEntries } from './commands/VendorCreditGLEntries'; import { LedgerModule } from '../Ledger/Ledger.module'; import { AccountsModule } from '../Accounts/Accounts.module'; +import VendorCreditInventoryTransactionsSubscriber from './subscribers/VendorCreditInventoryTransactionsSusbcriber'; +import { VendorCreditInventoryTransactions } from './commands/VendorCreditInventoryTransactions'; +import { GetVendorCreditsService } from './queries/GetVendorCredits.service'; @Module({ imports: [ @@ -41,10 +44,13 @@ import { AccountsModule } from '../Accounts/Accounts.module'; VendorCreditAutoIncrementService, GetRefundVendorCreditService, GetVendorCreditService, + GetVendorCreditsService, VendorCreditsApplicationService, OpenVendorCreditService, VendorCreditGLEntries, VendorCreditGlEntriesSubscriber, + VendorCreditInventoryTransactions, + VendorCreditInventoryTransactionsSubscriber ], exports: [ CreateVendorCreditService, diff --git a/packages/server-nest/src/modules/VendorCredit/VendorCreditsApplication.service.ts b/packages/server-nest/src/modules/VendorCredit/VendorCreditsApplication.service.ts index e994a4453..8d1aa2317 100644 --- a/packages/server-nest/src/modules/VendorCredit/VendorCreditsApplication.service.ts +++ b/packages/server-nest/src/modules/VendorCredit/VendorCreditsApplication.service.ts @@ -7,6 +7,7 @@ import { IVendorCreditEditDTO } from './types/VendorCredit.types'; import { IVendorCreditCreateDTO } from './types/VendorCredit.types'; import { Injectable } from '@nestjs/common'; import { OpenVendorCreditService } from './commands/OpenVendorCredit.service'; +import { GetVendorCreditsService } from './queries/GetVendorCredits.service'; @Injectable() export class VendorCreditsApplicationService { @@ -22,6 +23,7 @@ export class VendorCreditsApplicationService { private readonly deleteVendorCreditService: DeleteVendorCreditService, private readonly getVendorCreditService: GetVendorCreditService, private readonly openVendorCreditService: OpenVendorCreditService, + private readonly getVendorCreditsService: GetVendorCreditsService, ) {} /** @@ -78,4 +80,13 @@ export class VendorCreditsApplicationService { getVendorCredit(vendorCreditId: number, trx?: Knex.Transaction) { return this.getVendorCreditService.getVendorCredit(vendorCreditId, trx); } + + /** + * Retrieves the paginated filterable vendor credits list. + * @param {IVendorCreditsQueryDTO} query + * @returns {} + */ + getVendorCredits(query: IVendorCreditsQueryDTO) { + return this.getVendorCreditsService.getVendorCredits(query); + } } diff --git a/packages/server-nest/src/modules/VendorCredit/commands/VendorCreditInventoryTransactions.ts b/packages/server-nest/src/modules/VendorCredit/commands/VendorCreditInventoryTransactions.ts index f604bcd31..5527fcb61 100644 --- a/packages/server-nest/src/modules/VendorCredit/commands/VendorCreditInventoryTransactions.ts +++ b/packages/server-nest/src/modules/VendorCredit/commands/VendorCreditInventoryTransactions.ts @@ -1,92 +1,82 @@ -// import { Knex } from 'knex'; -// import { Service, Inject } from 'typedi'; -// import { IVendorCredit } from '@/interfaces'; -// import InventoryService from '@/services/Inventory/Inventory'; -// import ItemsEntriesService from '@/services/Items/ItemsEntriesService'; +import { Knex } from 'knex'; +import { Injectable } from '@nestjs/common'; +import { VendorCredit } from '../models/VendorCredit'; +import { InventoryService } from '@/modules/InventoryCost/Inventory'; +import { ItemsEntriesService } from '@/modules/Items/ItemsEntries.service'; -// @Service() -// export default class VendorCreditInventoryTransactions { -// @Inject() -// inventoryService: InventoryService; +@Injectable() +export class VendorCreditInventoryTransactions { + constructor( + private readonly inventoryService: InventoryService, + private readonly itemsEntriesService: ItemsEntriesService + ) {} -// @Inject() -// itemsEntriesService: ItemsEntriesService; + /** + * Creates vendor credit associated inventory transactions. + * @param {IVnedorCredit} vendorCredit + * @param {Knex.Transaction} trx + */ + public createInventoryTransactions = async ( + vendorCredit: VendorCredit, + trx: Knex.Transaction + ): Promise => { + // Loads the inventory items entries of the given sale invoice. + const inventoryEntries = + await this.itemsEntriesService.filterInventoryEntries( + vendorCredit.entries + ); -// /** -// * Creates vendor credit associated inventory transactions. -// * @param {number} tenantId -// * @param {IVnedorCredit} vendorCredit -// * @param {Knex.Transaction} trx -// */ -// public createInventoryTransactions = async ( -// tenantId: number, -// vendorCredit: IVendorCredit, -// trx: Knex.Transaction -// ): Promise => { -// // Loads the inventory items entries of the given sale invoice. -// const inventoryEntries = -// await this.itemsEntriesService.filterInventoryEntries( -// tenantId, -// vendorCredit.entries -// ); + const transaction = { + transactionId: vendorCredit.id, + transactionType: 'VendorCredit', + transactionNumber: vendorCredit.vendorCreditNumber, + exchangeRate: vendorCredit.exchangeRate, + date: vendorCredit.vendorCreditDate, + direction: 'OUT', + entries: inventoryEntries, + warehouseId: vendorCredit.warehouseId, + createdAt: vendorCredit.createdAt, + }; + // Writes inventory tranactions. + await this.inventoryService.recordInventoryTransactionsFromItemsEntries( + transaction, + false, + trx + ); + }; -// const transaction = { -// transactionId: vendorCredit.id, -// transactionType: 'VendorCredit', -// transactionNumber: vendorCredit.vendorCreditNumber, -// exchangeRate: vendorCredit.exchangeRate, -// date: vendorCredit.vendorCreditDate, -// direction: 'OUT', -// entries: inventoryEntries, -// warehouseId: vendorCredit.warehouseId, -// createdAt: vendorCredit.createdAt, -// }; -// // Writes inventory tranactions. -// await this.inventoryService.recordInventoryTransactionsFromItemsEntries( -// tenantId, -// transaction, -// false, -// trx -// ); -// }; + /** + * Edits vendor credit associated inventory transactions. + * @param {number} vendorCreditId - Vendor credit id. + * @param {IVendorCredit} vendorCredit - Vendor credit. + * @param {Knex.Transactions} trx - Knex transaction. + */ + public async editInventoryTransactions( + vendorCreditId: number, + vendorCredit: VendorCredit, + trx?: Knex.Transaction + ): Promise { + // Deletes inventory transactions. + await this.deleteInventoryTransactions(vendorCreditId, trx); -// /** -// * Edits vendor credit associated inventory transactions. -// * @param {number} tenantId -// * @param {number} creditNoteId -// * @param {ICreditNote} creditNote -// * @param {Knex.Transactions} trx -// */ -// public editInventoryTransactions = async ( -// tenantId: number, -// vendorCreditId: number, -// vendorCredit: IVendorCredit, -// trx?: Knex.Transaction -// ): Promise => { -// // Deletes inventory transactions. -// await this.deleteInventoryTransactions(tenantId, vendorCreditId, trx); + // Re-write inventory transactions. + await this.createInventoryTransactions(vendorCredit, trx); + }; -// // Re-write inventory transactions. -// await this.createInventoryTransactions(tenantId, vendorCredit, trx); -// }; - -// /** -// * Deletes credit note associated inventory transactions. -// * @param {number} tenantId - Tenant id. -// * @param {number} creditNoteId - Credit note id. -// * @param {Knex.Transaction} trx - -// */ -// public deleteInventoryTransactions = async ( -// tenantId: number, -// vendorCreditId: number, -// trx?: Knex.Transaction -// ): Promise => { -// // Deletes the inventory transactions by the given reference id and type. -// await this.inventoryService.deleteInventoryTransactions( -// tenantId, -// vendorCreditId, -// 'VendorCredit', -// trx -// ); -// }; -// } + /** + * Deletes credit note associated inventory transactions. + * @param {number} vendorCreditId - Vendor credit id. + * @param {Knex.Transaction} trx - Knex transaction. + */ + public async deleteInventoryTransactions( + vendorCreditId: number, + trx?: Knex.Transaction + ): Promise { + // Deletes the inventory transactions by the given reference id and type. + await this.inventoryService.deleteInventoryTransactions( + vendorCreditId, + 'VendorCredit', + trx + ); + }; +} diff --git a/packages/server-nest/src/modules/VendorCredit/queries/GetVendorCredits.service.ts b/packages/server-nest/src/modules/VendorCredit/queries/GetVendorCredits.service.ts index 854008fa7..8f776f47d 100644 --- a/packages/server-nest/src/modules/VendorCredit/queries/GetVendorCredits.service.ts +++ b/packages/server-nest/src/modules/VendorCredit/queries/GetVendorCredits.service.ts @@ -1,70 +1,66 @@ -// import * as R from 'ramda'; -// import { Service, Inject } from 'typedi'; -// import BaseVendorCredit from '../commands/BaseVendorCredit'; -// import DynamicListingService from '@/services/DynamicListing/DynamicListService'; -// import { IVendorCreditsQueryDTO } from '@/interfaces'; -// import { VendorCreditTransformer } from './VendorCreditTransformer'; -// import { TransformerInjectable } from '@/lib/Transformer/TransformerInjectable'; +import * as R from 'ramda'; +import { Inject, Injectable } from '@nestjs/common'; +import { VendorCreditTransformer } from './VendorCreditTransformer'; +import { DynamicListService } from '@/modules/DynamicListing/DynamicList.service'; +import { TransformerInjectable } from '@/modules/Transformer/TransformerInjectable.service'; +import { VendorCredit } from '../models/VendorCredit'; -// @Service() -// export default class ListVendorCredits extends BaseVendorCredit { -// @Inject() -// private dynamicListService: DynamicListingService; +@Injectable() +export class GetVendorCreditsService { + constructor( + private readonly dynamicListService: DynamicListService, + private readonly transformer: TransformerInjectable, -// @Inject() -// private transformer: TransformerInjectable; + @Inject(VendorCredit.name) + private readonly vendorCreditModel: typeof VendorCredit, + ) {} -// /** -// * Parses the sale invoice list filter DTO. -// * @param {IVendorCreditsQueryDTO} filterDTO -// * @returns -// */ -// private parseListFilterDTO = (filterDTO: IVendorCreditsQueryDTO) => { -// return R.compose(this.dynamicListService.parseStringifiedFilter)(filterDTO); -// }; + /** + * Parses the sale invoice list filter DTO. + * @param {IVendorCreditsQueryDTO} filterDTO + * @returns + */ + private parseListFilterDTO = (filterDTO: IVendorCreditsQueryDTO) => { + return R.compose(this.dynamicListService.parseStringifiedFilter)(filterDTO); + }; -// /** -// * Retrieve the vendor credits list. -// * @param {number} tenantId - Tenant id. -// * @param {IVendorCreditsQueryDTO} vendorCreditQuery - -// */ -// public getVendorCredits = async ( -// tenantId: number, -// vendorCreditQuery: IVendorCreditsQueryDTO -// ) => { -// const { VendorCredit } = this.tenancy.models(tenantId); + /** + * Retrieve the vendor credits list. + * @param {IVendorCreditsQueryDTO} vendorCreditQuery - + */ + public getVendorCredits = async ( + vendorCreditQuery: IVendorCreditsQueryDTO, + ) => { + // Parses stringified filter roles. + const filter = this.parseListFilterDTO(vendorCreditQuery); -// // Parses stringified filter roles. -// const filter = this.parseListFilterDTO(vendorCreditQuery); + // Dynamic list service. + const dynamicFilter = await this.dynamicListService.dynamicList( + VendorCredit, + filter, + ); + const { results, pagination } = await this.vendorCreditModel + .query() + .onBuild((builder) => { + builder.withGraphFetched('entries'); + builder.withGraphFetched('vendor'); + dynamicFilter.buildQuery()(builder); -// // Dynamic list service. -// const dynamicFilter = await this.dynamicListService.dynamicList( -// tenantId, -// VendorCredit, -// filter -// ); -// const { results, pagination } = await VendorCredit.query() -// .onBuild((builder) => { -// builder.withGraphFetched('entries'); -// builder.withGraphFetched('vendor'); -// dynamicFilter.buildQuery()(builder); + // Gives ability to inject custom query to filter results. + vendorCreditQuery?.filterQuery && + vendorCreditQuery?.filterQuery(builder); + }) + .pagination(filter.page - 1, filter.pageSize); -// // Gives ability to inject custom query to filter results. -// vendorCreditQuery?.filterQuery && -// vendorCreditQuery?.filterQuery(builder); -// }) -// .pagination(filter.page - 1, filter.pageSize); - -// // Transformes the vendor credits models to POJO. -// const vendorCredits = await this.transformer.transform( -// tenantId, -// results, -// new VendorCreditTransformer() -// ); -// return { -// vendorCredits, -// pagination, -// filterMeta: dynamicFilter.getResponseMeta(), -// }; -// }; -// } + // Transformes the vendor credits models to POJO. + const vendorCredits = await this.transformer.transform( + results, + new VendorCreditTransformer(), + ); + return { + vendorCredits, + pagination, + filterMeta: dynamicFilter.getResponseMeta(), + }; + }; +} diff --git a/packages/server-nest/src/modules/VendorCredit/subscribers/VendorCreditInventoryTransactionsSusbcriber.ts b/packages/server-nest/src/modules/VendorCredit/subscribers/VendorCreditInventoryTransactionsSusbcriber.ts index d1a797377..5fc2d4634 100644 --- a/packages/server-nest/src/modules/VendorCredit/subscribers/VendorCreditInventoryTransactionsSusbcriber.ts +++ b/packages/server-nest/src/modules/VendorCredit/subscribers/VendorCreditInventoryTransactionsSusbcriber.ts @@ -1,93 +1,69 @@ -// import { Inject, Service } from 'typedi'; -// import events from '@/subscribers/events'; -// import { -// IVendorCreditCreatedPayload, -// IVendorCreditDeletedPayload, -// IVendorCreditEditedPayload, -// } from '@/interfaces'; -// import VendorCreditInventoryTransactions from '../commands/VendorCreditInventoryTransactions'; +import { + IVendorCreditCreatedPayload, + IVendorCreditDeletedPayload, + IVendorCreditEditedPayload, +} from '../types/VendorCredit.types'; +import { VendorCreditInventoryTransactions } from '../commands/VendorCreditInventoryTransactions'; +import { OnEvent } from '@nestjs/event-emitter'; +import { Injectable } from '@nestjs/common'; +import { events } from '@/common/events/events'; -// @Service() -// export default class VendorCreditInventoryTransactionsSubscriber { -// @Inject() -// private inventoryTransactions: VendorCreditInventoryTransactions; +@Injectable() +export default class VendorCreditInventoryTransactionsSubscriber { + constructor( + private readonly inventoryTransactions: VendorCreditInventoryTransactions, + ) {} + /** + * Writes inventory transactions once vendor created created. + * @param {IVendorCreditCreatedPayload} payload - + */ + @OnEvent(events.vendorCredit.onCreated) + @OnEvent(events.vendorCredit.onOpened) + public async writeInventoryTransactionsOnceCreated({ + vendorCredit, + trx, + }: IVendorCreditCreatedPayload) { + // Can't continue if vendor credit is not opened. + if (!vendorCredit.openedAt) return null; -// /** -// * Attaches events with handlers. -// * @param bus -// */ -// attach(bus) { -// bus.subscribe( -// events.vendorCredit.onCreated, -// this.writeInventoryTransactionsOnceCreated -// ); -// bus.subscribe( -// events.vendorCredit.onOpened, -// this.writeInventoryTransactionsOnceCreated -// ); -// bus.subscribe( -// events.vendorCredit.onEdited, -// this.rewriteInventroyTransactionsOnceEdited -// ); -// bus.subscribe( -// events.vendorCredit.onDeleted, -// this.revertInventoryTransactionsOnceDeleted -// ); -// } + await this.inventoryTransactions.createInventoryTransactions( + vendorCredit, + trx, + ); + } -// /** -// * Writes inventory transactions once vendor created created. -// * @param {IVendorCreditCreatedPayload} payload - -// */ -// private writeInventoryTransactionsOnceCreated = async ({ -// tenantId, -// vendorCredit, -// trx, -// }: IVendorCreditCreatedPayload) => { -// // Can't continue if vendor credit is not opened. -// if (!vendorCredit.openedAt) return null; + /** + * Rewrites inventory transactions once vendor credit edited. + * @param {IVendorCreditEditedPayload} payload - + */ + @OnEvent(events.vendorCredit.onEdited) + public async rewriteInventroyTransactionsOnceEdited({ + vendorCreditId, + vendorCredit, + trx, + }: IVendorCreditEditedPayload) { + // Can't continue if vendor credit is not opened. + if (!vendorCredit.openedAt) return null; -// await this.inventoryTransactions.createInventoryTransactions( -// tenantId, -// vendorCredit, -// trx -// ); -// }; + await this.inventoryTransactions.editInventoryTransactions( + vendorCreditId, + vendorCredit, + trx, + ); + } -// /** -// * Rewrites inventory transactions once vendor credit edited. -// * @param {IVendorCreditEditedPayload} payload - -// */ -// private rewriteInventroyTransactionsOnceEdited = async ({ -// tenantId, -// vendorCreditId, -// vendorCredit, -// trx, -// }: IVendorCreditEditedPayload) => { -// // Can't continue if vendor credit is not opened. -// if (!vendorCredit.openedAt) return null; - -// await this.inventoryTransactions.editInventoryTransactions( -// tenantId, -// vendorCreditId, -// vendorCredit, -// trx -// ); -// }; - -// /** -// * Reverts inventory transactions once vendor credit deleted. -// * @param {IVendorCreditDeletedPayload} payload - -// */ -// private revertInventoryTransactionsOnceDeleted = async ({ -// tenantId, -// vendorCreditId, -// trx, -// }: IVendorCreditDeletedPayload) => { -// await this.inventoryTransactions.deleteInventoryTransactions( -// tenantId, -// vendorCreditId, -// trx -// ); -// }; -// } + /** + * Reverts inventory transactions once vendor credit deleted. + * @param {IVendorCreditDeletedPayload} payload - + */ + @OnEvent(events.vendorCredit.onDeleted) + public async revertInventoryTransactionsOnceDeleted({ + vendorCreditId, + trx, + }: IVendorCreditDeletedPayload) { + await this.inventoryTransactions.deleteInventoryTransactions( + vendorCreditId, + trx, + ); + } +} diff --git a/packages/server-nest/src/modules/Vendors/Vendors.controller.ts b/packages/server-nest/src/modules/Vendors/Vendors.controller.ts index 7d7bb68ee..045add1f0 100644 --- a/packages/server-nest/src/modules/Vendors/Vendors.controller.ts +++ b/packages/server-nest/src/modules/Vendors/Vendors.controller.ts @@ -6,6 +6,7 @@ import { Param, Post, Put, + Query, } from '@nestjs/common'; import { VendorsApplication } from './VendorsApplication.service'; import { @@ -20,6 +21,11 @@ import { PublicRoute } from '../Auth/Jwt.guard'; export class VendorsController { constructor(private vendorsApplication: VendorsApplication) {} + @Get() + getVendors(@Query() filterDTO: IVendorsFilter) { + return this.vendorsApplication.getVendors(filterDTO); + } + @Get(':id') getVendor(@Param('id') vendorId: number) { return this.vendorsApplication.getVendor(vendorId); diff --git a/packages/server-nest/src/modules/Vendors/VendorsApplication.service.ts b/packages/server-nest/src/modules/Vendors/VendorsApplication.service.ts index 04abcc68c..b5ba06cce 100644 --- a/packages/server-nest/src/modules/Vendors/VendorsApplication.service.ts +++ b/packages/server-nest/src/modules/Vendors/VendorsApplication.service.ts @@ -10,6 +10,7 @@ import { IVendorNewDTO, IVendorOpeningBalanceEditDTO, } from './types/Vendors.types'; +import { GetVendorsService } from './queries/GetVendors.service'; @Injectable() export class VendorsApplication { @@ -19,7 +20,7 @@ export class VendorsApplication { private deleteVendorService: DeleteVendorService, private editOpeningBalanceService: EditOpeningBalanceVendorService, private getVendorService: GetVendorService, - // private getVendorsService: GetVendors, + private getVendorsService: GetVendorsService, ) {} /** @@ -77,11 +78,10 @@ export class VendorsApplication { /** * Retrieves the vendors paginated list. - * @param {number} tenantId * @param {IVendorsFilter} filterDTO - * @returns + * @returns {Promise<{vendors: Vendor[], pagination: IPaginationMeta, filterMeta: IFilterMeta}>>} */ - // public getVendors = (tenantId: number, filterDTO: IVendorsFilter) => { - // return this.getVendorsService.getVendorsList(tenantId, filterDTO); - // }; + public getVendors = (filterDTO: IVendorsFilter) => { + return this.getVendorsService.getVendorsList(filterDTO); + }; } diff --git a/packages/server-nest/src/modules/Vendors/queries/GetVendors.service.ts b/packages/server-nest/src/modules/Vendors/queries/GetVendors.service.ts new file mode 100644 index 000000000..1c7f9de32 --- /dev/null +++ b/packages/server-nest/src/modules/Vendors/queries/GetVendors.service.ts @@ -0,0 +1,66 @@ +import * as R from 'ramda'; +import { Vendor } from '../models/Vendor'; +import { DynamicListService } from '@/modules/DynamicListing/DynamicList.service'; +import { TransformerInjectable } from '@/modules/Transformer/TransformerInjectable.service'; +import { Inject, Injectable } from '@nestjs/common'; +import { IFilterMeta, IPaginationMeta } from '@/interfaces/Model'; +import { VendorTransfromer } from './VendorTransformer'; + +@Injectable() +export class GetVendorsService { + constructor( + private dynamicListService: DynamicListService, + private transformer: TransformerInjectable, + + @Inject(Vendor.name) private vendorModel: typeof Vendor, + ) {} + + /** + * Retrieve vendors datatable list. + * @param {IVendorsFilter} vendorsFilter - Vendors filter. + */ + public async getVendorsList(filterDTO: IVendorsFilter): Promise<{ + vendors: Vendor[]; + pagination: IPaginationMeta; + filterMeta: IFilterMeta; + }> { + // Parses vendors list filter DTO. + const filter = this.parseVendorsListFilterDTO(filterDTO); + + // Dynamic list service. + const dynamicList = await this.dynamicListService.dynamicList( + this.vendorModel, + filter, + ); + // Vendors list. + const { results, pagination } = await this.vendorModel + .query() + .onBuild((builder) => { + dynamicList.buildQuery()(builder); + + // Switches between active/inactive modes. + builder.modify('inactiveMode', filter.inactiveMode); + }) + .pagination(filter.page - 1, filter.pageSize); + + // Transform the vendors. + const transformedVendors = await this.transformer.transform( + results, + new VendorTransfromer(), + ); + return { + vendors: transformedVendors, + pagination, + filterMeta: dynamicList.getResponseMeta(), + }; + } + + /** + * + * @param filterDTO + * @returns + */ + private parseVendorsListFilterDTO(filterDTO) { + return R.compose(this.dynamicListService.parseStringifiedFilter)(filterDTO); + } +} diff --git a/packages/server-nest/src/modules/Vendors/queries/GetVendors.ts b/packages/server-nest/src/modules/Vendors/queries/GetVendors.ts deleted file mode 100644 index 53bfd3f3f..000000000 --- a/packages/server-nest/src/modules/Vendors/queries/GetVendors.ts +++ /dev/null @@ -1,80 +0,0 @@ -// import * as R from 'ramda'; -// import { Service, Inject } from 'typedi'; -// import { -// IFilterMeta, -// IPaginationMeta, -// IVendor, -// IVendorsFilter, -// } from '@/interfaces'; -// import HasTenancyService from '@/services/Tenancy/TenancyService'; -// import DynamicListingService from '@/services/DynamicListing/DynamicListService'; -// import VendorTransfromer from '../VendorTransformer'; -// import { TransformerInjectable } from '@/lib/Transformer/TransformerInjectable'; - -// @Service() -// export class GetVendors { -// @Inject() -// private tenancy: HasTenancyService; - -// @Inject() -// private dynamicListService: DynamicListingService; - -// @Inject() -// private transformer: TransformerInjectable; - -// /** -// * Retrieve vendors datatable list. -// * @param {number} tenantId - Tenant id. -// * @param {IVendorsFilter} vendorsFilter - Vendors filter. -// */ -// public async getVendorsList( -// tenantId: number, -// filterDTO: IVendorsFilter -// ): Promise<{ -// vendors: IVendor[]; -// pagination: IPaginationMeta; -// filterMeta: IFilterMeta; -// }> { -// const { Vendor } = this.tenancy.models(tenantId); - -// // Parses vendors list filter DTO. -// const filter = this.parseVendorsListFilterDTO(filterDTO); - -// // Dynamic list service. -// const dynamicList = await this.dynamicListService.dynamicList( -// tenantId, -// Vendor, -// filter -// ); -// // Vendors list. -// const { results, pagination } = await Vendor.query() -// .onBuild((builder) => { -// dynamicList.buildQuery()(builder); - -// // Switches between active/inactive modes. -// builder.modify('inactiveMode', filter.inactiveMode); -// }) -// .pagination(filter.page - 1, filter.pageSize); - -// // Transform the vendors. -// const transformedVendors = await this.transformer.transform( -// tenantId, -// results, -// new VendorTransfromer() -// ); -// return { -// vendors: transformedVendors, -// pagination, -// filterMeta: dynamicList.getResponseMeta(), -// }; -// } - -// /** -// * -// * @param filterDTO -// * @returns -// */ -// private parseVendorsListFilterDTO(filterDTO) { -// return R.compose(this.dynamicListService.parseStringifiedFilter)(filterDTO); -// } -// } diff --git a/packages/server-nest/src/modules/Views/GetResourceViews.service.ts b/packages/server-nest/src/modules/Views/GetResourceViews.service.ts index d3827fb40..1c6135b79 100644 --- a/packages/server-nest/src/modules/Views/GetResourceViews.service.ts +++ b/packages/server-nest/src/modules/Views/GetResourceViews.service.ts @@ -1,5 +1,4 @@ import { Injectable } from '@nestjs/common'; -import ResourceService from '@/services/Resource/ResourceService'; import { BaseModel } from '@/models/Model'; import { View } from './models/View.model'; diff --git a/packages/server/src/models/CustomViewBaseModel.ts b/packages/server/src/models/CustomViewBaseModel.ts index a54520023..5c1dfec54 100644 --- a/packages/server/src/models/CustomViewBaseModel.ts +++ b/packages/server/src/models/CustomViewBaseModel.ts @@ -14,6 +14,10 @@ export default (Model) => return this.defaultViews.find((view) => view.slug === viewSlug) || null; } + /** + * Retrieve the default views. + * @returns {IView[]} + */ static getDefaultViews() { return this.defaultViews; } diff --git a/packages/server/src/services/DynamicListing/DynamicListing.types.ts b/packages/server/src/services/DynamicListing/DynamicListing.types.ts new file mode 100644 index 000000000..175d155c7 --- /dev/null +++ b/packages/server/src/services/DynamicListing/DynamicListing.types.ts @@ -0,0 +1,31 @@ +// import { IModel, ISortOrder } from "./Model"; + +import { ISortOrder } from "@/interfaces"; + +// export interface IDynamicFilter { +// setModel(model: IModel): void; +// buildQuery(): void; +// getResponseMeta(); +// } + +export interface IFilterRole { + fieldKey: string; + value: string; + condition?: string; + index?: number; + comparator?: string; +} +export interface IDynamicListFilter { + customViewId?: number; + filterRoles?: IFilterRole[]; + columnSortBy: ISortOrder; + sortOrder: string; + stringifiedFilterRoles: string; + searchKeyword?: string; +} + +// Search role. +export interface ISearchRole { + fieldKey: string; + comparator: string; +} \ No newline at end of file diff --git a/packages/server/src/services/Purchases/VendorCredits/VendorCreditInventoryTransactions.ts b/packages/server/src/services/Purchases/VendorCredits/VendorCreditInventoryTransactions.ts index 258bfe8d9..e7dfbcea8 100644 --- a/packages/server/src/services/Purchases/VendorCredits/VendorCreditInventoryTransactions.ts +++ b/packages/server/src/services/Purchases/VendorCredits/VendorCreditInventoryTransactions.ts @@ -19,14 +19,12 @@ export default class VendorCreditInventoryTransactions { * @param {Knex.Transaction} trx */ public createInventoryTransactions = async ( - tenantId: number, vendorCredit: IVendorCredit, trx: Knex.Transaction ): Promise => { // Loads the inventory items entries of the given sale invoice. const inventoryEntries = await this.itemsEntriesService.filterInventoryEntries( - tenantId, vendorCredit.entries ); @@ -43,7 +41,6 @@ export default class VendorCreditInventoryTransactions { }; // Writes inventory tranactions. await this.inventoryService.recordInventoryTransactionsFromItemsEntries( - tenantId, transaction, false, trx @@ -52,38 +49,33 @@ export default class VendorCreditInventoryTransactions { /** * Edits vendor credit associated inventory transactions. - * @param {number} tenantId - * @param {number} creditNoteId - * @param {ICreditNote} creditNote - * @param {Knex.Transactions} trx + * @param {number} vendorCreditId - Vendor credit id. + * @param {IVendorCredit} vendorCredit - Vendor credit. + * @param {Knex.Transactions} trx - Knex transaction. */ - public editInventoryTransactions = async ( - tenantId: number, + public async editInventoryTransactions( vendorCreditId: number, vendorCredit: IVendorCredit, trx?: Knex.Transaction - ): Promise => { + ): Promise { // Deletes inventory transactions. - await this.deleteInventoryTransactions(tenantId, vendorCreditId, trx); + await this.deleteInventoryTransactions(vendorCreditId, trx); // Re-write inventory transactions. - await this.createInventoryTransactions(tenantId, vendorCredit, trx); + await this.createInventoryTransactions(vendorCredit, trx); }; /** * Deletes credit note associated inventory transactions. - * @param {number} tenantId - Tenant id. - * @param {number} creditNoteId - Credit note id. - * @param {Knex.Transaction} trx - + * @param {number} vendorCreditId - Vendor credit id. + * @param {Knex.Transaction} trx - Knex transaction. */ - public deleteInventoryTransactions = async ( - tenantId: number, + public async deleteInventoryTransactions( vendorCreditId: number, trx?: Knex.Transaction - ): Promise => { + ): Promise { // Deletes the inventory transactions by the given reference id and type. await this.inventoryService.deleteInventoryTransactions( - tenantId, vendorCreditId, 'VendorCredit', trx diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index fa5655e8a..21bd339fa 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -622,6 +622,9 @@ importers: plaid: specifier: ^10.3.0 version: 10.9.0 + pluralize: + specifier: ^8.0.0 + version: 8.0.0 posthog-node: specifier: ^4.3.2 version: 4.3.2