mirror of
https://github.com/bigcapitalhq/bigcapital.git
synced 2026-02-16 12:50:38 +00:00
refactor(nestjs): currencies module
This commit is contained in:
@@ -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: [
|
||||
|
||||
@@ -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',
|
||||
};
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
32
packages/server/src/modules/Currencies/Currencies.module.ts
Normal file
32
packages/server/src/modules/Currencies/Currencies.module.ts
Normal 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 {}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import { IsString } from 'class-validator';
|
||||
|
||||
export class CreateCurrencyDto {
|
||||
@IsString()
|
||||
currencyName: string;
|
||||
|
||||
@IsString()
|
||||
currencyCode: string;
|
||||
|
||||
@IsString()
|
||||
currencySign: string;
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
import { IsString } from 'class-validator';
|
||||
|
||||
export class EditCurrencyDto {
|
||||
@IsString()
|
||||
currencyName: string;
|
||||
|
||||
@IsString()
|
||||
currencySign: string;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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(),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user