refactor(nestjs): e2e test cases

This commit is contained in:
Ahmed Bouhuolia
2025-03-31 00:39:00 +02:00
parent caff6ce47c
commit ab717b96ac
28 changed files with 168 additions and 115 deletions

View File

@@ -2,6 +2,7 @@ import { Inject, Injectable } from '@nestjs/common';
import { pick } from 'lodash'; import { pick } from 'lodash';
import { Knex } from 'knex'; import { Knex } from 'knex';
import * as R from 'ramda'; import * as R from 'ramda';
import * as composeAsync from 'async/compose';
import { CASHFLOW_TRANSACTION_TYPE } from '../constants'; import { CASHFLOW_TRANSACTION_TYPE } from '../constants';
import { transformCashflowTransactionType } from '../utils'; import { transformCashflowTransactionType } from '../utils';
import { CommandBankTransactionValidator } from './CommandCasflowValidator.service'; import { CommandBankTransactionValidator } from './CommandCasflowValidator.service';
@@ -104,7 +105,7 @@ export class CreateBankTransactionService {
} }
: {}), : {}),
}; };
return R.compose(this.branchDTOTransform.transformDTO<BankTransaction>)( return composeAsync(this.branchDTOTransform.transformDTO<BankTransaction>)(
initialDTO, initialDTO,
) as BankTransaction; ) as BankTransaction;
}; };

View File

@@ -27,6 +27,7 @@ export class BillDTOTransformer {
@Inject(ItemEntry.name) @Inject(ItemEntry.name)
private itemEntryModel: TenantModelProxy<typeof ItemEntry>, private itemEntryModel: TenantModelProxy<typeof ItemEntry>,
@Inject(Item.name) private itemModel: TenantModelProxy<typeof Item>, @Inject(Item.name) private itemModel: TenantModelProxy<typeof Item>,
) {} ) {}
@@ -114,12 +115,16 @@ export class BillDTOTransformer {
}), }),
userId: authorizedUser.id, userId: authorizedUser.id,
}; };
const asyncDto = await composeAsync(
this.branchDTOTransform.transformDTO<Bill>,
this.warehouseDTOTransform.transformDTO<Bill>,
)(initialDTO);
return R.compose( return R.compose(
// Associates tax amount withheld to the model. // Associates tax amount withheld to the model.
this.taxDTOTransformer.assocTaxAmountWithheldFromEntries, this.taxDTOTransformer.assocTaxAmountWithheldFromEntries,
this.branchDTOTransform.transformDTO<Bill>, )(asyncDto) as Bill;
this.warehouseDTOTransform.transformDTO<Bill>,
)(initialDTO) as Bill;
} }
/** /**

View File

@@ -1,23 +1,30 @@
import { Injectable } from '@nestjs/common'; import { Inject, Injectable } from '@nestjs/common';
import { SettingsStore } from '../Settings/SettingsStore';
import { SETTINGS_PROVIDER } from '../Settings/Settings.types';
import { Features } from '@/common/types/Features';
@Injectable() @Injectable()
export class BranchesSettingsService { export class BranchesSettingsService {
constructor(
@Inject(SETTINGS_PROVIDER)
private readonly settingsStore: () => SettingsStore,
) {}
/** /**
* Marks multi-branches as activated. * Marks multi-branches as activated.
*/ */
public markMultiBranchesAsActivated = () => { public markMultiBranchesAsActivated = async () => {
// const settings = this.tenancy.settings(tenantId); const settingsStore = await this.settingsStore();
// settings.set({ group: 'features', key: Features.BRANCHES, value: 1 }); settingsStore.set({ group: 'features', key: Features.BRANCHES, value: 1 });
}; };
/** /**
* Retrieves whether multi-branches is active. * Retrieves whether multi-branches is active.
*/ */
public isMultiBranchesActive = () => { public isMultiBranchesActive = async () => {
// const settings = this.tenancy.settings(tenantId); const settingsStore = await this.settingsStore();
// return settings.get({ group: 'features', key: Features.BRANCHES }); return settingsStore.get({ group: 'features', key: Features.BRANCHES });
return false;
}; };
} }

View File

@@ -49,8 +49,8 @@ export class ActivateBranches {
* Activate multi-branches feature. * Activate multi-branches feature.
* @returns {Promise<void>} * @returns {Promise<void>}
*/ */
public activateBranches = (): Promise<void> => { public activateBranches = async (): Promise<void> => {
const isActivated = this.branchesSettings.isMultiBranchesActive(); const isActivated = await this.branchesSettings.isMultiBranchesActive();
// Throw error if mutli-branches is already activated. // Throw error if mutli-branches is already activated.
this.throwIfMultiBranchesActivated(isActivated); this.throwIfMultiBranchesActivated(isActivated);

View File

@@ -10,22 +10,24 @@ export class BranchTransactionDTOTransformer {
* Excludes DTO branch id when mutli-warehouses feature is inactive. * Excludes DTO branch id when mutli-warehouses feature is inactive.
* @returns {any} * @returns {any}
*/ */
private excludeDTOBranchIdWhenInactive = <T extends { branchId?: number }>( private excludeDTOBranchIdWhenInactive = async <
T extends { branchId?: number },
>(
DTO: T, DTO: T,
): Omit<T, 'branchId'> | T => { ): Promise<Omit<T, 'branchId'> | T> => {
const isActive = this.branchesSettings.isMultiBranchesActive(); const isActive = await this.branchesSettings.isMultiBranchesActive();
return !isActive ? omit(DTO, ['branchId']) : DTO; return !isActive ? omit(DTO, ['branchId']) : DTO;
}; };
/** /**
* Transformes the input DTO for branches feature. * Transforms the input DTO for branches feature.
* @param {T} DTO - * @param {T} DTO -
* @returns {Omit<T, 'branchId'> | T} * @returns {Omit<T, 'branchId'> | T}
*/ */
public transformDTO = <T extends { branchId?: number }>( public transformDTO = async <T extends { branchId?: number }>(
DTO: T, DTO: T,
): Omit<T, 'branchId'> | T => { ): Promise<Omit<T, 'branchId'> | T> => {
return this.excludeDTOBranchIdWhenInactive<T>(DTO); return this.excludeDTOBranchIdWhenInactive<T>(DTO);
}; };
} }

View File

@@ -11,17 +11,18 @@ export class ManualJournalBranchesDTOTransformer {
) {} ) {}
/** /**
* *
* @param DTO * @param DTO
* @returns * @returns
*/ */
private excludeDTOBranchIdWhenInactive = ( private excludeDTOBranchIdWhenInactive = async (
DTO: IManualJournalDTO, DTO: IManualJournalDTO,
): IManualJournalDTO => { ): Promise<IManualJournalDTO> => {
const isActive = this.branchesSettings.isMultiBranchesActive(); const isActive = await this.branchesSettings.isMultiBranchesActive();
if (isActive) return DTO;
if (isActive) {
return DTO;
}
return { return {
...DTO, ...DTO,
entries: DTO.entries.map((e) => omit(e, ['branchId'])), entries: DTO.entries.map((e) => omit(e, ['branchId'])),
@@ -29,9 +30,11 @@ export class ManualJournalBranchesDTOTransformer {
}; };
/** /**
* *
*/ */
public transformDTO = (DTO: IManualJournalDTO): IManualJournalDTO => { public transformDTO = async (
return this.excludeDTOBranchIdWhenInactive(DTO); DTO: IManualJournalDTO,
}; ): Promise<IManualJournalDTO> => {
return this.excludeDTOBranchIdWhenInactive(DTO);
};
} }

View File

@@ -17,7 +17,11 @@ import { BrandingTemplateDTOTransformer } from '../../PdfTemplate/BrandingTempla
import { assocItemEntriesDefaultIndex } from '@/utils/associate-item-entries-index'; import { assocItemEntriesDefaultIndex } from '@/utils/associate-item-entries-index';
import { CreditNoteAutoIncrementService } from './CreditNoteAutoIncrement.service'; import { CreditNoteAutoIncrementService } from './CreditNoteAutoIncrement.service';
import { CreditNote } from '../models/CreditNote'; import { CreditNote } from '../models/CreditNote';
import { CreateCreditNoteDto, CreditNoteEntryDto, EditCreditNoteDto } from '../dtos/CreditNote.dto'; import {
CreateCreditNoteDto,
CreditNoteEntryDto,
EditCreditNoteDto,
} from '../dtos/CreditNote.dto';
@Injectable() @Injectable()
export class CommandCreditNoteDTOTransform { export class CommandCreditNoteDTOTransform {
@@ -33,11 +37,11 @@ export class CommandCreditNoteDTOTransform {
private readonly branchDTOTransform: BranchTransactionDTOTransformer, private readonly branchDTOTransform: BranchTransactionDTOTransformer,
private readonly warehouseDTOTransform: WarehouseTransactionDTOTransform, private readonly warehouseDTOTransform: WarehouseTransactionDTOTransform,
private readonly brandingTemplatesTransformer: BrandingTemplateDTOTransformer, private readonly brandingTemplatesTransformer: BrandingTemplateDTOTransformer,
private readonly creditNoteAutoIncrement: CreditNoteAutoIncrementService private readonly creditNoteAutoIncrement: CreditNoteAutoIncrementService,
) {} ) {}
/** /**
* Transformes the credit/edit DTO to model. * Transforms the credit/edit DTO to model.
* @param {ICreditNoteNewDTO | ICreditNoteEditDTO} creditNoteDTO * @param {ICreditNoteNewDTO | ICreditNoteEditDTO} creditNoteDTO
* @param {string} customerCurrencyCode - * @param {string} customerCurrencyCode -
*/ */
@@ -61,10 +65,10 @@ export class CommandCreditNoteDTOTransform {
})), })),
)(creditNoteDTO.entries); )(creditNoteDTO.entries);
// Retreive the next credit note number. // Retrieves the next credit note number.
const autoNextNumber = this.creditNoteAutoIncrement.getNextCreditNumber(); const autoNextNumber = this.creditNoteAutoIncrement.getNextCreditNumber();
// Detarmines the credit note number. // Determines the credit note number.
const creditNoteNumber = const creditNoteNumber =
creditNoteDTO.creditNoteNumber || creditNoteDTO.creditNoteNumber ||
oldCreditNote?.creditNoteNumber || oldCreditNote?.creditNoteNumber ||
@@ -84,17 +88,17 @@ export class CommandCreditNoteDTOTransform {
refundedAmount: 0, refundedAmount: 0,
invoicesAmount: 0, invoicesAmount: 0,
}; };
const initialAsyncDTO = await composeAsync( const asyncDto = (await composeAsync(
this.branchDTOTransform.transformDTO<CreditNote>,
this.warehouseDTOTransform.transformDTO<CreditNote>,
// Assigns the default branding template id to the invoice DTO. // Assigns the default branding template id to the invoice DTO.
this.brandingTemplatesTransformer.assocDefaultBrandingTemplate( this.brandingTemplatesTransformer.assocDefaultBrandingTemplate(
'CreditNote', 'CreditNote',
), ),
)(initialDTO); )(initialDTO)) as CreditNote;
return R.compose( return asyncDto;
this.branchDTOTransform.transformDTO<CreditNote>,
this.warehouseDTOTransform.transformDTO<CreditNote>,
)(initialAsyncDTO) as CreditNote;
}; };
/** /**

View File

@@ -2,6 +2,7 @@ import { ItemEntryDto } from '@/modules/TransactionItemEntry/dto/ItemEntry.dto';
import { ApiProperty } from '@nestjs/swagger'; import { ApiProperty } from '@nestjs/swagger';
import { Type } from 'class-transformer'; import { Type } from 'class-transformer';
import { import {
ArrayMinSize,
IsArray, IsArray,
IsBoolean, IsBoolean,
IsDate, IsDate,
@@ -88,7 +89,7 @@ export class CommandCreditNoteDto {
@IsArray() @IsArray()
@ValidateNested({ each: true }) @ValidateNested({ each: true })
@Type(() => CreditNoteEntryDto) @Type(() => CreditNoteEntryDto)
@Min(1) @ArrayMinSize(1)
@ApiProperty({ @ApiProperty({
example: [ example: [
{ {

View File

@@ -2,6 +2,7 @@ import { Injectable } from '@nestjs/common';
import { omit, sumBy } from 'lodash'; import { omit, sumBy } from 'lodash';
import * as moment from 'moment'; import * as moment from 'moment';
import * as R from 'ramda'; import * as R from 'ramda';
import * as composeAsync from 'async/compose';
import { BranchTransactionDTOTransformer } from '@/modules/Branches/integrations/BranchTransactionDTOTransform'; import { BranchTransactionDTOTransformer } from '@/modules/Branches/integrations/BranchTransactionDTOTransform';
import { Expense } from '../models/Expense.model'; import { Expense } from '../models/Expense.model';
import { assocItemEntriesDefaultIndex } from '@/utils/associate-item-entries-index'; import { assocItemEntriesDefaultIndex } from '@/utils/associate-item-entries-index';
@@ -48,9 +49,9 @@ export class ExpenseDTOTransformer {
* @param {ISystemUser} authorizedUser * @param {ISystemUser} authorizedUser
* @return {IExpense} * @return {IExpense}
*/ */
private expenseDTOToModel( private async expenseDTOToModel(
expenseDTO: CreateExpenseDto | EditExpenseDto, expenseDTO: CreateExpenseDto | EditExpenseDto,
): Expense { ): Promise<Expense> {
const landedCostAmount = this.getExpenseLandedCostAmount(expenseDTO); const landedCostAmount = this.getExpenseLandedCostAmount(expenseDTO);
const totalAmount = this.getExpenseCategoriesTotal(expenseDTO.categories); const totalAmount = this.getExpenseCategoriesTotal(expenseDTO.categories);
@@ -71,20 +72,22 @@ export class ExpenseDTOTransformer {
} }
: {}), : {}),
}; };
return R.compose(this.branchDTOTransform.transformDTO<Expense>)( const asyncDto = await composeAsync(
initialDTO, this.branchDTOTransform.transformDTO<Expense>,
) as Expense; )(initialDTO);
return asyncDto as Expense;
} }
/** /**
* Transformes the expense create DTO. * Transforms the expense create DTO.
* @param {IExpenseCreateDTO} expenseDTO * @param {IExpenseCreateDTO} expenseDTO
* @returns {Promise<Expense>} * @returns {Promise<Expense>}
*/ */
public expenseCreateDTO = async ( public expenseCreateDTO = async (
expenseDTO: CreateExpenseDto | EditExpenseDto, expenseDTO: CreateExpenseDto | EditExpenseDto,
): Promise<Partial<Expense>> => { ): Promise<Partial<Expense>> => {
const initialDTO = this.expenseDTOToModel(expenseDTO); const initialDTO = await this.expenseDTOToModel(expenseDTO);
const tenant = await this.tenancyContext.getTenant(true); const tenant = await this.tenancyContext.getTenant(true);
return { return {

View File

@@ -1,7 +1,7 @@
import { Knex } from 'knex'; import { Knex } from 'knex';
import { Inject, Injectable } from '@nestjs/common'; import { Inject, Injectable } from '@nestjs/common';
import * as R from 'ramda';
import * as moment from 'moment'; import * as moment from 'moment';
import * as composeAsync from 'async/compose';
import { omit } from 'lodash'; import { omit } from 'lodash';
import { events } from '@/common/events/events'; import { events } from '@/common/events/events';
import { InventoryAdjustment } from '../models/InventoryAdjustment'; import { InventoryAdjustment } from '../models/InventoryAdjustment';
@@ -79,7 +79,7 @@ export class CreateQuickInventoryAdjustmentService {
: {}), : {}),
entries, entries,
}; };
return R.compose( return composeAsync(
this.warehouseDTOTransform.transformDTO<InventoryAdjustment>, this.warehouseDTOTransform.transformDTO<InventoryAdjustment>,
this.branchDTOTransform.transformDTO<InventoryAdjustment>, this.branchDTOTransform.transformDTO<InventoryAdjustment>,
)(initialDTO) as InventoryAdjustment; )(initialDTO) as InventoryAdjustment;

View File

@@ -3,6 +3,7 @@ import * as moment from 'moment';
import * as R from 'ramda'; import * as R from 'ramda';
import { Knex } from 'knex'; import { Knex } from 'knex';
import { Inject, Injectable } from '@nestjs/common'; import { Inject, Injectable } from '@nestjs/common';
import * as composeAsync from 'async/compose';
import { EventEmitter2 } from '@nestjs/event-emitter'; import { EventEmitter2 } from '@nestjs/event-emitter';
import { import {
IManualJournalDTO, IManualJournalDTO,
@@ -73,7 +74,7 @@ export class CreateManualJournalService {
entries, entries,
userId: authorizedUser.id, userId: authorizedUser.id,
}; };
return R.compose( return composeAsync(
// Omits the `branchId` from entries if multiply branches feature not active. // Omits the `branchId` from entries if multiply branches feature not active.
this.branchesDTOTransformer.transformDTO, this.branchesDTOTransformer.transformDTO,
)(initialDTO) as ManualJournal; )(initialDTO) as ManualJournal;

View File

@@ -71,15 +71,15 @@ export class PaymentReceiveDTOTransformer {
exchangeRate: paymentReceiveDTO.exchangeRate || 1, exchangeRate: paymentReceiveDTO.exchangeRate || 1,
entries, entries,
}; };
const initialAsyncDTO = await composeAsync( const asyncDto = await composeAsync(
this.branchDTOTransform.transformDTO<PaymentReceived>,
// Assigns the default branding template id to the invoice DTO. // Assigns the default branding template id to the invoice DTO.
this.brandingTemplatesTransformer.assocDefaultBrandingTemplate( this.brandingTemplatesTransformer.assocDefaultBrandingTemplate(
'SaleInvoice', 'SaleInvoice',
), ),
)(initialDTO); )(initialDTO);
return R.compose(this.branchDTOTransform.transformDTO<PaymentReceived>)( return asyncDto;
initialAsyncDTO,
) as PaymentReceived;
} }
} }

View File

@@ -81,17 +81,17 @@ export class SaleEstimateDTOTransformer {
deliveredAt: moment().toMySqlDateTime(), deliveredAt: moment().toMySqlDateTime(),
}), }),
}; };
const initialAsyncDTO = await composeAsync( const asyncDto = await composeAsync(
this.branchDTOTransform.transformDTO<SaleEstimate>,
this.warehouseDTOTransform.transformDTO<SaleEstimate>,
// Assigns the default branding template id to the invoice DTO. // Assigns the default branding template id to the invoice DTO.
this.brandingTemplatesTransformer.assocDefaultBrandingTemplate( this.brandingTemplatesTransformer.assocDefaultBrandingTemplate(
'SaleEstimate', 'SaleEstimate',
), ),
)(initialDTO); )(initialDTO);
return R.compose( return asyncDto;
this.branchDTOTransform.transformDTO<SaleEstimate>,
this.warehouseDTOTransform.transformDTO<SaleEstimate>,
)(initialAsyncDTO);
} }
/** /**

View File

@@ -2,6 +2,7 @@ import { ItemEntryDto } from '@/modules/TransactionItemEntry/dto/ItemEntry.dto';
import { ApiProperty } from '@nestjs/swagger'; import { ApiProperty } from '@nestjs/swagger';
import { Type } from 'class-transformer'; import { Type } from 'class-transformer';
import { import {
ArrayMinSize,
IsArray, IsArray,
IsBoolean, IsBoolean,
IsDate, IsDate,
@@ -92,7 +93,7 @@ export class CommandSaleEstimateDto {
branchId?: number; branchId?: number;
@IsArray() @IsArray()
@MinLength(1) @ArrayMinSize(1)
@ValidateNested({ each: true }) @ValidateNested({ each: true })
@Type(() => SaleEstimateEntryDto) @Type(() => SaleEstimateEntryDto)
@ApiProperty({ @ApiProperty({

View File

@@ -121,17 +121,18 @@ export class CommandSaleInvoiceDTOTransformer {
} as SaleInvoice; } as SaleInvoice;
const initialAsyncDTO = await composeAsync( const initialAsyncDTO = await composeAsync(
this.branchDTOTransform.transformDTO<SaleInvoice>,
this.warehouseDTOTransform.transformDTO<SaleInvoice>,
// Assigns the default branding template id to the invoice DTO. // Assigns the default branding template id to the invoice DTO.
this.brandingTemplatesTransformer.assocDefaultBrandingTemplate( this.brandingTemplatesTransformer.assocDefaultBrandingTemplate(
'SaleInvoice', 'SaleInvoice',
), ),
)(initialDTO); )(initialDTO);
return R.compose( return R.compose(this.taxDTOTransformer.assocTaxAmountWithheldFromEntries)(
this.taxDTOTransformer.assocTaxAmountWithheldFromEntries, initialAsyncDTO,
this.branchDTOTransform.transformDTO<SaleInvoice>, );
this.warehouseDTOTransform.transformDTO<SaleInvoice>,
)(initialAsyncDTO);
} }
/** /**

View File

@@ -2,7 +2,6 @@ import { Inject, Injectable } from '@nestjs/common';
import { EventEmitter2 } from '@nestjs/event-emitter'; import { EventEmitter2 } from '@nestjs/event-emitter';
import { Knex } from 'knex'; import { Knex } from 'knex';
import { import {
ISaleInvoiceCreateDTO,
ISaleInvoiceCreatedPayload, ISaleInvoiceCreatedPayload,
ISaleInvoiceCreatingPaylaod, ISaleInvoiceCreatingPaylaod,
} from '../SaleInvoice.types'; } from '../SaleInvoice.types';

View File

@@ -15,7 +15,10 @@ import { assocItemEntriesDefaultIndex } from '@/utils/associate-item-entries-ind
import { SaleReceipt } from '../models/SaleReceipt'; import { SaleReceipt } from '../models/SaleReceipt';
import { Customer } from '@/modules/Customers/models/Customer'; import { Customer } from '@/modules/Customers/models/Customer';
import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel'; import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel';
import { CreateSaleReceiptDto, EditSaleReceiptDto } from '../dtos/SaleReceipt.dto'; import {
CreateSaleReceiptDto,
EditSaleReceiptDto,
} from '../dtos/SaleReceipt.dto';
@Injectable() @Injectable()
export class SaleReceiptDTOTransformer { export class SaleReceiptDTOTransformer {
@@ -96,16 +99,16 @@ export class SaleReceiptDTOTransformer {
}), }),
entries, entries,
}; };
const initialAsyncDTO = await composeAsync( const asyncDto = await composeAsync(
this.branchDTOTransform.transformDTO<SaleReceipt>,
this.warehouseDTOTransform.transformDTO<SaleReceipt>,
// Assigns the default branding template id to the invoice DTO. // Assigns the default branding template id to the invoice DTO.
this.brandingTemplatesTransformer.assocDefaultBrandingTemplate( this.brandingTemplatesTransformer.assocDefaultBrandingTemplate(
'SaleReceipt', 'SaleReceipt',
), ),
)(initialDTO); )(initialDTO);
return R.compose( return asyncDto;
this.branchDTOTransform.transformDTO<SaleReceipt>,
this.warehouseDTOTransform.transformDTO<SaleReceipt>,
)(initialAsyncDTO) as SaleReceipt;
} }
} }

View File

@@ -24,6 +24,7 @@ import { SaleReceipt } from '../models/SaleReceipt';
import { MailTransporter } from '@/modules/Mail/MailTransporter.service'; import { MailTransporter } from '@/modules/Mail/MailTransporter.service';
import { Mail } from '@/modules/Mail/Mail'; import { Mail } from '@/modules/Mail/Mail';
import { TenancyContext } from '@/modules/Tenancy/TenancyContext.service'; import { TenancyContext } from '@/modules/Tenancy/TenancyContext.service';
import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel';
@Injectable() @Injectable()
export class SaleReceiptMailNotification { export class SaleReceiptMailNotification {
@@ -43,7 +44,7 @@ export class SaleReceiptMailNotification {
private readonly tenancyContext: TenancyContext, private readonly tenancyContext: TenancyContext,
@Inject(SaleReceipt.name) @Inject(SaleReceipt.name)
private readonly saleReceiptModel: typeof SaleReceipt, private readonly saleReceiptModel: TenantModelProxy<typeof SaleReceipt>,
@InjectQueue(SendSaleReceiptMailQueue) @InjectQueue(SendSaleReceiptMailQueue)
private readonly sendSaleReceiptMailProcess: Queue, private readonly sendSaleReceiptMailProcess: Queue,
@@ -74,7 +75,6 @@ export class SaleReceiptMailNotification {
await this.sendSaleReceiptMailProcess.add(SendSaleReceiptMailJob, { await this.sendSaleReceiptMailProcess.add(SendSaleReceiptMailJob, {
...payload, ...payload,
}); });
// Triggers the event `onSaleReceiptPreMailSend`. // Triggers the event `onSaleReceiptPreMailSend`.
await this.eventEmitter.emitAsync(events.saleReceipt.onPreMailSend, { await this.eventEmitter.emitAsync(events.saleReceipt.onPreMailSend, {
saleReceiptId, saleReceiptId,
@@ -90,7 +90,7 @@ export class SaleReceiptMailNotification {
public async getMailOptions( public async getMailOptions(
saleReceiptId: number, saleReceiptId: number,
): Promise<SaleReceiptMailOpts> { ): Promise<SaleReceiptMailOpts> {
const saleReceipt = await this.saleReceiptModel const saleReceipt = await this.saleReceiptModel()
.query() .query()
.findById(saleReceiptId) .findById(saleReceiptId)
.throwIfNotFound(); .throwIfNotFound();

View File

@@ -1,4 +1,5 @@
import knex from 'knex'; import knex from 'knex';
import * as LRUCache from 'lru-cache';
import { Global, Module } from '@nestjs/common'; import { Global, Module } from '@nestjs/common';
import { knexSnakeCaseMappers } from 'objection'; import { knexSnakeCaseMappers } from 'objection';
import { ClsModule, ClsService } from 'nestjs-cls'; import { ClsModule, ClsService } from 'nestjs-cls';
@@ -6,6 +7,8 @@ import { ConfigService } from '@nestjs/config';
import { TENANCY_DB_CONNECTION } from './TenancyDB.constants'; import { TENANCY_DB_CONNECTION } from './TenancyDB.constants';
import { UnitOfWork } from './UnitOfWork.service'; import { UnitOfWork } from './UnitOfWork.service';
const lruCache = new LRUCache();
export const TenancyDatabaseProxyProvider = ClsModule.forFeatureAsync({ export const TenancyDatabaseProxyProvider = ClsModule.forFeatureAsync({
provide: TENANCY_DB_CONNECTION, provide: TENANCY_DB_CONNECTION,
global: true, global: true,
@@ -13,14 +16,19 @@ export const TenancyDatabaseProxyProvider = ClsModule.forFeatureAsync({
inject: [ConfigService, ClsService], inject: [ConfigService, ClsService],
useFactory: async (configService: ConfigService, cls: ClsService) => () => { useFactory: async (configService: ConfigService, cls: ClsService) => () => {
const organizationId = cls.get('organizationId'); const organizationId = cls.get('organizationId');
const database = `bigcapital_tenant_${organizationId}`;
const cachedInstance = lruCache.get(database);
return knex({ if (cachedInstance) {
return cachedInstance;
}
const knexInstance = knex({
client: configService.get('tenantDatabase.client'), client: configService.get('tenantDatabase.client'),
connection: { connection: {
host: configService.get('tenantDatabase.host'), host: configService.get('tenantDatabase.host'),
user: configService.get('tenantDatabase.user'), user: configService.get('tenantDatabase.user'),
password: configService.get('tenantDatabase.password'), password: configService.get('tenantDatabase.password'),
database: `bigcapital_tenant_${organizationId}`, database,
charset: 'utf8', charset: 'utf8',
}, },
migrations: { migrations: {
@@ -32,6 +40,9 @@ export const TenancyDatabaseProxyProvider = ClsModule.forFeatureAsync({
pool: { min: 0, max: 7 }, pool: { min: 0, max: 7 },
...knexSnakeCaseMappers({ upperCase: true }), ...knexSnakeCaseMappers({ upperCase: true }),
}); });
lruCache.set(database, knexInstance);
return knexInstance;
}, },
type: 'function', type: 'function',
}); });

View File

@@ -91,9 +91,10 @@ export function RegisterTenancyModel(model: typeof Model) {
provide: model.name, provide: model.name,
inject: [TENANCY_DB_CONNECTION], inject: [TENANCY_DB_CONNECTION],
global: true, global: true,
useFactory: async (tenantKnex: () => Knex) => () => { useFactory: (tenantKnex: () => Knex) => () => {
return model.bindKnex(tenantKnex()); return model.bindKnex(tenantKnex());
}, },
strict: true,
type: 'function', type: 'function',
}); });
} }

View File

@@ -1,6 +1,7 @@
import * as moment from 'moment'; import * as moment from 'moment';
import { omit } from 'lodash'; import { omit } from 'lodash';
import * as R from 'ramda'; import * as R from 'ramda';
import * as composeAsync from 'async/compose';
import { ERRORS } from '../constants'; import { ERRORS } from '../constants';
import { ItemsEntriesService } from '@/modules/Items/ItemsEntries.service'; import { ItemsEntriesService } from '@/modules/Items/ItemsEntries.service';
import { BranchTransactionDTOTransformer } from '@/modules/Branches/integrations/BranchTransactionDTOTransform'; import { BranchTransactionDTOTransformer } from '@/modules/Branches/integrations/BranchTransactionDTOTransform';
@@ -32,7 +33,7 @@ export class VendorCreditDTOTransformService {
) {} ) {}
/** /**
* Transformes the credit/edit vendor credit DTO to model. * Transforms the credit/edit vendor credit DTO to model.
* @param {IVendorCreditCreateDTO | IVendorCreditEditDTO} vendorCreditDTO * @param {IVendorCreditCreateDTO | IVendorCreditEditDTO} vendorCreditDTO
* @param {string} vendorCurrencyCode - * @param {string} vendorCurrencyCode -
* @param {IVendorCredit} oldVendorCredit - * @param {IVendorCredit} oldVendorCredit -
@@ -80,7 +81,7 @@ export class VendorCreditDTOTransformService {
openedAt: moment().toMySqlDateTime(), openedAt: moment().toMySqlDateTime(),
}), }),
}; };
return R.compose( return composeAsync(
this.branchDTOTransform.transformDTO<VendorCredit>, this.branchDTOTransform.transformDTO<VendorCredit>,
this.warehouseDTOTransform.transformDTO<VendorCredit>, this.warehouseDTOTransform.transformDTO<VendorCredit>,
)(initialDTO) as VendorCredit; )(initialDTO) as VendorCredit;

View File

@@ -1,6 +1,7 @@
import { Inject, Injectable } from '@nestjs/common'; import { Inject, Injectable } from '@nestjs/common';
import { Knex } from 'knex'; import { Knex } from 'knex';
import * as R from 'ramda'; import * as R from 'ramda';
import * as composeAsync from 'async/compose';
import { EventEmitter2 } from '@nestjs/event-emitter'; import { EventEmitter2 } from '@nestjs/event-emitter';
import { import {
IRefundVendorCreditCreatedPayload, IRefundVendorCreditCreatedPayload,
@@ -81,7 +82,7 @@ export class CreateRefundVendorCredit {
refundVendorCreditDTO, refundVendorCreditDTO,
} as IVendorCreditCreatePayload); } as IVendorCreditCreatePayload);
const refundCreditObj = this.transformDTOToModel( const refundCreditObj = await this.transformDTOToModel(
vendorCredit, vendorCredit,
refundVendorCreditDTO, refundVendorCreditDTO,
); );
@@ -119,7 +120,7 @@ export class CreateRefundVendorCredit {
* @param {RefundVendorCreditDto} vendorCreditDTO * @param {RefundVendorCreditDto} vendorCreditDTO
* @returns {IRefundVendorCredit} * @returns {IRefundVendorCredit}
*/ */
public transformDTOToModel = ( public transformDTOToModel = async (
vendorCredit: VendorCredit, vendorCredit: VendorCredit,
vendorCreditDTO: RefundVendorCreditDto, vendorCreditDTO: RefundVendorCreditDto,
) => { ) => {
@@ -129,7 +130,7 @@ export class CreateRefundVendorCredit {
currencyCode: vendorCredit.currencyCode, currencyCode: vendorCredit.currencyCode,
exchangeRate: vendorCreditDTO.exchangeRate || 1, exchangeRate: vendorCreditDTO.exchangeRate || 1,
}; };
return R.compose(this.branchDTOTransform.transformDTO)(initialDTO); return this.branchDTOTransform.transformDTO(initialDTO);
}; };
/** /**

View File

@@ -4,34 +4,32 @@ import { WarehousesSettings } from '../WarehousesSettings';
@Injectable() @Injectable()
export class WarehouseTransactionDTOTransform { export class WarehouseTransactionDTOTransform {
constructor( constructor(private readonly warehousesSettings: WarehousesSettings) {}
private readonly warehousesSettings: WarehousesSettings,
) {}
/** /**
* Excludes DTO warehouse id when mutli-warehouses feature is inactive. * Excludes DTO warehouse id when mutli-warehouses feature is inactive.
* @param {number} tenantId * @param {number} tenantId
* @returns {Promise<Omit<T, 'warehouseId'> | T>} * @returns {Promise<Omit<T, 'warehouseId'> | T>}
*/ */
private excludeDTOWarehouseIdWhenInactive = < private excludeDTOWarehouseIdWhenInactive = async <
T extends { warehouseId?: number } T extends { warehouseId?: number },
>( >(
DTO: T DTO: T,
): Omit<T, 'warehouseId'> | T => { ): Promise<Omit<T, 'warehouseId'> | T> => {
const isActive = this.warehousesSettings.isMultiWarehousesActive(); const isActive = await this.warehousesSettings.isMultiWarehousesActive();
return !isActive ? omit(DTO, ['warehouseId']) : DTO; return !isActive ? omit(DTO, ['warehouseId']) : DTO;
}; };
/** /**
* *
* @param {number} tenantId * @param {number} tenantId
* @param {T} DTO - * @param {T} DTO -
* @returns {Omit<T, 'warehouseId'> | T} * @returns {Omit<T, 'warehouseId'> | T}
*/ */
public transformDTO = public transformDTO = async <T extends { warehouseId?: number }>(
<T extends { warehouseId?: number }>(DTO: T): Omit<T, 'warehouseId'> | T => { DTO: T,
return this.excludeDTOWarehouseIdWhenInactive<T>(DTO); ): Promise<Omit<T, 'warehouseId'> | T> => {
}; return this.excludeDTOWarehouseIdWhenInactive<T>(DTO);
};
} }

View File

@@ -19,7 +19,9 @@ export class WarehousesDTOValidators {
* Validates the warehouse existance of sale invoice transaction. * Validates the warehouse existance of sale invoice transaction.
* @param {IWarehouseTransactionDTO} DTO * @param {IWarehouseTransactionDTO} DTO
*/ */
public validateDTOWarehouseExistance = async (DTO: IWarehouseTransactionDTO) => { public validateDTOWarehouseExistance = async (
DTO: IWarehouseTransactionDTO,
) => {
// Validates the sale invoice warehouse id existance. // Validates the sale invoice warehouse id existance.
this.validateWarehouseExistanceService.validateWarehouseIdExistance( this.validateWarehouseExistanceService.validateWarehouseIdExistance(
DTO, DTO,
@@ -47,7 +49,7 @@ export class WarehousesDTOValidators {
public validateDTOWarehouseWhenActive = async ( public validateDTOWarehouseWhenActive = async (
DTO: IWarehouseTransactionDTO, DTO: IWarehouseTransactionDTO,
): Promise<void> => { ): Promise<void> => {
const isActive = this.warehousesSettings.isMultiWarehousesActive(); const isActive = await this.warehousesSettings.isMultiWarehousesActive();
// Can't continue if the multi-warehouses feature is inactive. // Can't continue if the multi-warehouses feature is inactive.
if (!isActive) return; if (!isActive) return;

View File

@@ -24,7 +24,7 @@ export class WarehousesItemsQuantitySyncSubscriber {
inventoryTransactions, inventoryTransactions,
trx, trx,
}: IInventoryTransactionsCreatedPayload) { }: IInventoryTransactionsCreatedPayload) {
const isActive = this.warehousesSettings.isMultiWarehousesActive(); const isActive = await this.warehousesSettings.isMultiWarehousesActive();
// Can't continue if the warehouses features is not active. // Can't continue if the warehouses features is not active.
if (!isActive) return; if (!isActive) return;
@@ -44,7 +44,7 @@ export class WarehousesItemsQuantitySyncSubscriber {
oldInventoryTransactions, oldInventoryTransactions,
trx, trx,
}: IInventoryTransactionsDeletedPayload) { }: IInventoryTransactionsDeletedPayload) {
const isActive = this.warehousesSettings.isMultiWarehousesActive(); const isActive = await this.warehousesSettings.isMultiWarehousesActive();
// Can't continue if the warehouses feature is not active yet. // Can't continue if the warehouses feature is not active yet.
if (!isActive) return; if (!isActive) return;

View File

@@ -1,25 +1,32 @@
import { Injectable } from '@nestjs/common'; import { Inject, Injectable } from '@nestjs/common';
import { SettingsStore } from '../Settings/SettingsStore';
import { SETTINGS_PROVIDER } from '../Settings/Settings.types';
import { Features } from '@/common/types/Features';
@Injectable() @Injectable()
export class WarehousesSettings { export class WarehousesSettings {
constructor(
@Inject(SETTINGS_PROVIDER)
private readonly settingsStore: () => SettingsStore,
) {}
/** /**
* Marks multi-warehouses as activated. * Marks multi-warehouses as activated.
*/ */
public markMutliwarehoussAsActivated = () => { public markMutliwarehoussAsActivated = async () => {
// const settings = this.tenancy.settings(tenantId); const settings = await this.settingsStore();
// settings.set({ group: 'features', key: Features.WAREHOUSES, value: 1 }); settings.set({ group: 'features', key: Features.WAREHOUSES, value: 1 });
}; };
/** /**
* Detarmines multi-warehouses is active. * Determines multi-warehouses is active.
* @param {number} tenantId * @param {number} tenantId
* @returns {boolean} * @returns {boolean}
*/ */
public isMultiWarehousesActive = () => { public isMultiWarehousesActive = async () => {
// const settings = this.tenancy.settings(tenantId); const settings = await this.settingsStore();
// return settings.get({ group: 'features', key: Features.WAREHOUSES }); return settings.get({ group: 'features', key: Features.WAREHOUSES });
return true;
}; };
} }

View File

@@ -40,7 +40,7 @@ export class ActivateWarehousesService {
* - Mutate inventory transactions with the primary warehouse. * - Mutate inventory transactions with the primary warehouse.
*/ */
public async activateWarehouses(): Promise<void> { public async activateWarehouses(): Promise<void> {
const isActivated = this.settings.isMultiWarehousesActive(); const isActivated = await this.settings.isMultiWarehousesActive();
this.throwIfWarehousesActivated(isActivated); this.throwIfWarehousesActivated(isActivated);
return this.uow.withTransaction(async (trx: Knex.Transaction) => { return this.uow.withTransaction(async (trx: Knex.Transaction) => {

View File

@@ -39,6 +39,7 @@ describe('Sale Estimates (e2e)', () => {
.set('organization-id', orgainzationId) .set('organization-id', orgainzationId)
.send({ .send({
name: faker.commerce.productName(), name: faker.commerce.productName(),
type: 'inventory',
sellable: true, sellable: true,
purchasable: true, purchasable: true,
sellAccountId: 1026, sellAccountId: 1026,