refactor(nestjs): currencies module

This commit is contained in:
Ahmed Bouhuolia
2025-05-17 12:14:02 +02:00
parent 4de1ef71ca
commit ce058b9416
16 changed files with 504 additions and 1 deletions

View File

@@ -85,6 +85,7 @@ import { ImportModule } from '../Import/Import.module';
import { CreditNotesApplyInvoiceModule } from '../CreditNotesApplyInvoice/CreditNotesApplyInvoice.module';
import { ResourceModule } from '../Resource/Resource.module';
import { ViewsModule } from '../Views/Views.module';
import { CurrenciesModule } from '../Currencies/Currencies.module';
@Module({
imports: [
@@ -204,7 +205,8 @@ import { ViewsModule } from '../Views/Views.module';
ExportModule,
ImportModule,
ResourceModule,
ViewsModule
ViewsModule,
CurrenciesModule
],
controllers: [AppController],
providers: [

View File

@@ -0,0 +1,17 @@
export const InitialCurrencies = [
'USD',
'CAD',
'EUR',
'LYD',
'GBP',
'CNY',
'AUD',
'INR',
];
export const ERRORS = {
CURRENCY_NOT_FOUND: 'currency_not_found',
CURRENCY_CODE_EXISTS: 'currency_code_exists',
BASE_CURRENCY_INVALID: 'BASE_CURRENCY_INVALID',
CANNOT_DELETE_BASE_CURRENCY: 'CANNOT_DELETE_BASE_CURRENCY',
};

View File

@@ -0,0 +1,79 @@
import {
Controller,
Post,
Put,
Delete,
Get,
Body,
Param,
} from '@nestjs/common';
import {
ApiTags,
ApiOperation,
ApiBody,
ApiCreatedResponse,
ApiBadRequestResponse,
ApiParam,
ApiOkResponse,
ApiNotFoundResponse,
} from '@nestjs/swagger';
import { CurrenciesApplication } from './CurrenciesApplication.service';
import { CreateCurrencyDto } from './dtos/CreateCurrency.dto';
import { EditCurrencyDto } from './dtos/EditCurrency.dto';
@ApiTags('currencies')
@Controller('/currencies')
export class CurrenciesController {
constructor(private readonly currenciesApp: CurrenciesApplication) {}
@Post()
@ApiOperation({ summary: 'Create a new currency' })
@ApiBody({ type: CreateCurrencyDto })
@ApiCreatedResponse({
description: 'The currency has been successfully created.',
})
@ApiBadRequestResponse({ description: 'Invalid input data.' })
create(@Body() dto: CreateCurrencyDto) {
return this.currenciesApp.createCurrency(dto);
}
@Put(':id')
@ApiOperation({ summary: 'Edit an existing currency' })
@ApiParam({ name: 'id', type: Number, description: 'Currency ID' })
@ApiBody({ type: EditCurrencyDto })
@ApiOkResponse({ description: 'The currency has been successfully updated.' })
@ApiNotFoundResponse({ description: 'Currency not found.' })
@ApiBadRequestResponse({ description: 'Invalid input data.' })
edit(@Param('id') id: number, @Body() dto: EditCurrencyDto) {
return this.currenciesApp.editCurrency(Number(id), dto);
}
@Delete(':code')
@ApiOperation({ summary: 'Delete a currency by code' })
@ApiParam({ name: 'code', type: String, description: 'Currency code' })
@ApiOkResponse({ description: 'The currency has been successfully deleted.' })
@ApiNotFoundResponse({ description: 'Currency not found.' })
delete(@Param('code') code: string) {
return this.currenciesApp.deleteCurrency(code);
}
@Get()
@ApiOperation({ summary: 'Get all currencies' })
@ApiOkResponse({ description: 'List of all currencies.' })
findAll() {
return this.currenciesApp.getCurrencies();
}
@Get(':currencyCode')
@ApiOperation({ summary: 'Get a currency by code' })
@ApiParam({
name: 'currencyCode',
type: String,
description: 'Currency code',
})
@ApiOkResponse({ description: 'The currency details.' })
@ApiNotFoundResponse({ description: 'Currency not found.' })
findOne(@Param('currencyCode') currencyCode: string) {
return this.currenciesApp.getCurrency(currencyCode);
}
}

View File

@@ -0,0 +1,32 @@
import { Module } from '@nestjs/common';
import { RegisterTenancyModel } from '../Tenancy/TenancyModels/Tenancy.module';
import { Currency } from './models/Currency.model';
import { CreateCurrencyService } from './commands/CreateCurrency.service';
import { EditCurrencyService } from './commands/EditCurrency.service';
import { DeleteCurrencyService } from './commands/DeleteCurrency.service';
import { SeedInitialCurrenciesOnSetupSubsriber } from './subscribers/SeedInitialCurrenciesOnSetup.subscriber';
import { InitialCurrenciesSeedService } from './commands/InitialCurrenciesSeed.service';
import { CurrenciesApplication } from './CurrenciesApplication.service';
import { CurrenciesController } from './Currencies.controller';
import { TenancyModule } from '../Tenancy/Tenancy.module';
import { GetCurrenciesService } from './queries/GetCurrencies.service';
import { GetCurrencyService } from './queries/GetCurrency.service';
const models = [RegisterTenancyModel(Currency)];
@Module({
imports: [...models, TenancyModule],
exports: [...models],
providers: [
CreateCurrencyService,
EditCurrencyService,
DeleteCurrencyService,
GetCurrenciesService,
GetCurrencyService,
CurrenciesApplication,
InitialCurrenciesSeedService,
SeedInitialCurrenciesOnSetupSubsriber
],
controllers: [CurrenciesController]
})
export class CurrenciesModule {}

View File

@@ -0,0 +1,54 @@
import { Injectable } from '@nestjs/common';
import { CreateCurrencyService } from './commands/CreateCurrency.service';
import { EditCurrencyService } from './commands/EditCurrency.service';
import { DeleteCurrencyService } from './commands/DeleteCurrency.service';
import { GetCurrenciesService } from './queries/GetCurrencies.service';
import { GetCurrencyService } from './queries/GetCurrency.service';
import { CreateCurrencyDto } from './dtos/CreateCurrency.dto';
import { EditCurrencyDto } from './dtos/EditCurrency.dto';
@Injectable()
export class CurrenciesApplication {
constructor(
private readonly createCurrencyService: CreateCurrencyService,
private readonly editCurrencyService: EditCurrencyService,
private readonly deleteCurrencyService: DeleteCurrencyService,
private readonly getCurrenciesService: GetCurrenciesService,
private readonly getCurrencyService: GetCurrencyService,
) {}
/**
* Creates a new currency.
*/
public createCurrency(currencyDTO: CreateCurrencyDto) {
return this.createCurrencyService.createCurrency(currencyDTO);
}
/**
* Edits an existing currency.
*/
public editCurrency(currencyId: number, currencyDTO: EditCurrencyDto) {
return this.editCurrencyService.editCurrency(currencyId, currencyDTO);
}
/**
* Deletes a currency by code.
*/
public deleteCurrency(currencyCode: string) {
return this.deleteCurrencyService.deleteCurrency(currencyCode);
}
/**
* Gets a list of all currencies.
*/
public getCurrencies() {
return this.getCurrenciesService.getCurrencies();
}
/**
* Gets a single currency by id or code (to be implemented in GetCurrencyService).
*/
public getCurrency(currencyCode: string) {
return this.getCurrencyService.getCurrency(currencyCode);
}
}

View File

@@ -0,0 +1,19 @@
import { Transformer } from "../Transformer/Transformer";
export class CurrencyTransformer extends Transformer {
/**
* Include these attributes to sale invoice object.
* @returns {Array}
*/
public includeAttributes = (): string[] => {
return ['isBaseCurrency'];
};
/**
* Detarmines whether the currency is base currency.
* @returns {boolean}
*/
public isBaseCurrency(currency): boolean {
return this.context.organization.baseCurrency === currency.currencyCode;
}
}

View File

@@ -0,0 +1,45 @@
import { Inject, Injectable } from '@nestjs/common';
import { TenantModelProxy } from '../../System/models/TenantBaseModel';
import { Currency } from '../models/Currency.model';
import { ServiceError } from '@/modules/Items/ServiceError';
import { ERRORS } from '../Currencies.constants';
import { CreateCurrencyDto } from '../dtos/CreateCurrency.dto';
@Injectable()
export class CreateCurrencyService {
constructor(
@Inject(Currency.name)
private readonly currencyModel: TenantModelProxy<typeof Currency>,
) {}
async createCurrency(currencyDTO: CreateCurrencyDto) {
// Validate currency code uniquiness.
await this.validateCurrencyCodeUniquiness(currencyDTO.currencyCode);
await this.currencyModel()
.query()
.insert({ ...currencyDTO });
}
/**
* Retrieve currency by given currency code or throw not found error.
* @param {string} currencyCode
* @param {number} currencyId
*/
private async validateCurrencyCodeUniquiness(
currencyCode: string,
currencyId?: number,
) {
const foundCurrency = await this.currencyModel()
.query()
.onBuild((query) => {
query.findOne('currency_code', currencyCode);
if (currencyId) {
query.whereNot('id', currencyId);
}
});
if (foundCurrency) {
throw new ServiceError(ERRORS.CURRENCY_CODE_EXISTS);
}
}
}

View File

@@ -0,0 +1,46 @@
import { Inject, Injectable } from '@nestjs/common';
import { TenantModelProxy } from '../../System/models/TenantBaseModel';
import { Currency } from '../models/Currency.model';
import { ServiceError } from '@/modules/Items/ServiceError';
import { TenancyContext } from '@/modules/Tenancy/TenancyContext.service';
import { ERRORS } from '../Currencies.constants';
@Injectable()
export class DeleteCurrencyService {
constructor(
@Inject(Currency.name)
private readonly currencyModel: TenantModelProxy<typeof Currency>,
private readonly tenancyContext: TenancyContext,
) {}
/**
* Delete the given currency code.
* @param {string} currencyCode
* @return {Promise<void>}
*/
public async deleteCurrency(currencyCode: string): Promise<void> {
const foundCurrency = await this.currencyModel().query()
.findOne('currency_code', currencyCode)
.throwIfNotFound();
// Validate currency code not equals base currency.
await this.validateCannotDeleteBaseCurrency(currencyCode);
await this.currencyModel()
.query()
.where('currency_code', currencyCode)
.delete();
}
/**
* Validate cannot delete base currency.
* @param {string} currencyCode
*/
private async validateCannotDeleteBaseCurrency(currencyCode: string) {
const tenant = await this.tenancyContext.getTenant(true);
if (tenant.metadata.baseCurrency === currencyCode) {
throw new ServiceError(ERRORS.CANNOT_DELETE_BASE_CURRENCY);
}
}
}

View File

@@ -0,0 +1,35 @@
import { Inject, Injectable } from '@nestjs/common';
import { Currency } from '../models/Currency.model';
import { TenantModelProxy } from '../../System/models/TenantBaseModel';
import { EditCurrencyDto } from '../dtos/EditCurrency.dto';
@Injectable()
export class EditCurrencyService {
constructor(
@Inject(Currency.name)
private readonly currencyModel: TenantModelProxy<typeof Currency>,
) {}
/**
* Edit details of the given currency.
* @param {number} tenantId
* @param {number} currencyId
* @param {ICurrencyDTO} currencyDTO
*/
public async editCurrency(
currencyId: number,
currencyDTO: EditCurrencyDto,
): Promise<Currency> {
const foundCurrency = await this.currencyModel()
.query()
.findOne('id', currencyId)
.throwIfNotFound();
const currency = await this.currencyModel()
.query()
.patchAndFetchById(currencyId, {
...currencyDTO,
});
return currency;
}
}

View File

@@ -0,0 +1,47 @@
import { Inject, Injectable } from '@nestjs/common';
import { uniq } from 'lodash';
import Currencies from 'js-money/lib/currency';
import { InitialCurrencies } from '../Currencies.constants';
import { TenantModelProxy } from '../../System/models/TenantBaseModel';
import { Currency } from '../models/Currency.model';
@Injectable()
export class InitialCurrenciesSeedService {
constructor(
@Inject(Currency.name)
private readonly currencyModel: TenantModelProxy<typeof Currency>,
) {}
/**
* Seeds the given base currency to the currencies list.
* @param {string} baseCurrency - Base currency code.
*/
public async seedCurrencyByCode(currencyCode: string): Promise<void> {
const currencyMeta = Currencies[currencyCode];
const foundBaseCurrency = await this.currencyModel()
.query()
.findOne('currency_code', currencyCode);
if (!foundBaseCurrency) {
await this.currencyModel().query().insert({
currencyCode: currencyMeta.code,
currencyName: currencyMeta.name,
currencySign: currencyMeta.symbol,
});
}
}
/**
* Seeds initial currencies to the organization.
* @param {string} baseCurrency - Base currency code.
*/
public async seedInitialCurrencies(baseCurrency: string): Promise<void> {
const initialCurrencies = uniq([...InitialCurrencies, baseCurrency]);
// Seed currency opers.
const seedCurrencyOpers = initialCurrencies.map((currencyCode) => {
return this.seedCurrencyByCode(currencyCode);
});
await Promise.all(seedCurrencyOpers);
}
}

View File

@@ -0,0 +1,12 @@
import { IsString } from 'class-validator';
export class CreateCurrencyDto {
@IsString()
currencyName: string;
@IsString()
currencyCode: string;
@IsString()
currencySign: string;
}

View File

@@ -0,0 +1,9 @@
import { IsString } from 'class-validator';
export class EditCurrencyDto {
@IsString()
currencyName: string;
@IsString()
currencySign: string;
}

View File

@@ -0,0 +1,25 @@
import { TenantModel } from "@/modules/System/models/TenantModel";
export class Currency extends TenantModel {
public readonly currencySign: string;
public readonly currencyName: string;
public readonly currencyCode: string;
/**
* Table name
*/
static get tableName() {
return 'currencies';
}
/**
* Timestamps columns.
*/
get timestamps() {
return ['createdAt', 'updatedAt'];
}
static get resourceable() {
return true;
}
}

View File

@@ -0,0 +1,29 @@
import { Inject, Injectable } from "@nestjs/common";
import { Currency } from "../models/Currency.model";
import { TenantModelProxy } from "../../System/models/TenantBaseModel";
import { TransformerInjectable } from "../../Transformer/TransformerInjectable.service";
import { CurrencyTransformer } from "../Currency.transformer";
@Injectable()
export class GetCurrenciesService {
constructor(
@Inject(Currency.name)
private readonly currencyModel: TenantModelProxy<typeof Currency>,
private readonly transformerInjectable: TransformerInjectable,
) {
}
/**
* Retrieves currencies list.
* @return {Promise<ICurrency[]>}
*/
public async getCurrencies(): Promise<Currency[]> {
const currencies = await this.currencyModel().query().onBuild((query) => {
query.orderBy('createdAt', 'ASC');
});
return this.transformerInjectable.transform(
currencies,
new CurrencyTransformer()
);
}
}

View File

@@ -0,0 +1,26 @@
import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel';
import { Currency } from '../models/Currency.model';
import { TransformerInjectable } from '@/modules/Transformer/TransformerInjectable.service';
import { CurrencyTransformer } from '../Currency.transformer';
import { Inject, Injectable } from '@nestjs/common';
@Injectable()
export class GetCurrencyService {
constructor(
@Inject(Currency.name)
private readonly currencyModel: TenantModelProxy<typeof Currency>,
private readonly transformInjectable: TransformerInjectable,
) {}
getCurrency(currencyCode: string) {
const currency = this.currencyModel()
.query()
.findOne('currencyCode', currencyCode)
.throwIfNotFound();
return this.transformInjectable.transform(
currency,
new CurrencyTransformer(),
);
}
}

View File

@@ -0,0 +1,26 @@
import { OnEvent } from '@nestjs/event-emitter';
import { Injectable } from '@nestjs/common';
import { InitialCurrenciesSeedService } from '../commands/InitialCurrenciesSeed.service';
import { IOrganizationBuildEventPayload } from '@/modules/Organization/Organization.types';
import { events } from '@/common/events/events';
@Injectable()
export class SeedInitialCurrenciesOnSetupSubsriber {
constructor(
private readonly seedInitialCurrencies: InitialCurrenciesSeedService,
) {}
/**
* Seed initial currencies once organization build.
* @param {IOrganizationBuildEventPayload}
*/
@OnEvent(events.organization.build)
async seedInitialCurrenciesOnBuild({
systemUser,
buildDTO,
}: IOrganizationBuildEventPayload) {
await this.seedInitialCurrencies.seedInitialCurrencies(
buildDTO.baseCurrency,
);
}
}