import { Inject, Service } from 'typedi'; import Currencies from 'js-money/lib/currency'; import { ICurrencyEditDTO, ICurrencyDTO, ICurrenciesService, ICurrency, } from 'interfaces'; import { EventDispatcher, EventDispatcherInterface, } from 'decorators/eventDispatcher'; import { ServiceError } from 'exceptions'; import TenancyService from 'services/Tenancy/TenancyService'; 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' }; @Service() export default class CurrenciesService implements ICurrenciesService { @Inject('logger') logger: any; @EventDispatcher() eventDispatcher: EventDispatcherInterface; @Inject() tenancy: TenancyService; /** * Retrieve currency by given currency code or throw not found error. * @param {number} tenantId * @param {string} currencyCode * @param {number} currencyId */ private async validateCurrencyCodeUniquiness( tenantId: number, currencyCode: string, currencyId?: number ) { const { Currency } = this.tenancy.models(tenantId); this.logger.info( '[currencies] trying to validate currency code existance.', { tenantId, currencyCode } ); const foundCurrency = await Currency.query().onBuild((query) => { query.findOne('currency_code', currencyCode); if (currencyId) { query.whereNot('id', currencyId); } }); if (foundCurrency) { this.logger.info('[currencies] the currency code already exists.', { tenantId, currencyCode, }); throw new ServiceError(ERRORS.CURRENCY_CODE_EXISTS); } } /** * Retrieve currency by the given currency code or throw service error. * @param {number} tenantId * @param {string} currencyCode */ private async getCurrencyByCodeOrThrowError( tenantId: number, currencyCode: string ) { const { Currency } = this.tenancy.models(tenantId); this.logger.info( '[currencies] trying to validate currency code existance.', { tenantId, currencyCode } ); const foundCurrency = await Currency.query().findOne( 'currency_code', currencyCode ); if (!foundCurrency) { this.logger.info('[currencies] the given currency code not exists.', { tenantId, currencyCode, }); throw new ServiceError(ERRORS.CURRENCY_NOT_FOUND); } return foundCurrency; } /** * Retrieve currency by given id or throw not found error. * @param {number} tenantId * @param {number} currencyId */ private async getCurrencyIdOrThrowError( tenantId: number, currencyId: number ) { const { Currency } = this.tenancy.models(tenantId); this.logger.info( '[currencies] trying to validate currency by id existance.', { tenantId, currencyId } ); const foundCurrency = await Currency.query().findOne('id', currencyId); if (!foundCurrency) { this.logger.info('[currencies] the currency code not found.', { tenantId, currencyId, }); throw new ServiceError(ERRORS.CURRENCY_NOT_FOUND); } return foundCurrency; } /** * Creates a new currency. * @param {number} tenantId * @param {ICurrencyDTO} currencyDTO */ public async newCurrency(tenantId: number, currencyDTO: ICurrencyDTO) { const { Currency } = this.tenancy.models(tenantId); this.logger.info('[currencies] try to insert new currency.', { tenantId, currencyDTO, }); // Validate currency code uniquiness. await this.validateCurrencyCodeUniquiness( tenantId, currencyDTO.currencyCode ); await Currency.query().insert({ ...currencyDTO }); this.logger.info('[currencies] the currency inserted successfully.', { tenantId, currencyDTO, }); } /** * Edit details of the given currency. * @param {number} tenantId * @param {number} currencyId * @param {ICurrencyDTO} currencyDTO */ public async editCurrency( tenantId: number, currencyId: number, currencyDTO: ICurrencyEditDTO ): Promise { const { Currency } = this.tenancy.models(tenantId); this.logger.info('[currencies] try to edit currency.', { tenantId, currencyId, currencyDTO, }); await this.getCurrencyIdOrThrowError(tenantId, currencyId); const currency = await Currency.query().patchAndFetchById(currencyId, { ...currencyDTO, }); this.logger.info('[currencies] the currency edited successfully.', { tenantId, currencyDTO, }); return currency; } /** * Validate cannot delete base currency. * @param {number} tenantId * @param {string} currencyCode */ validateCannotDeleteBaseCurrency(tenantId: number, currencyCode: string) { const settings = this.tenancy.settings(tenantId); const baseCurrency = settings.get({ group: 'organization', key: 'base_currency', }); if (baseCurrency === currencyCode) { throw new ServiceError(ERRORS.CANNOT_DELETE_BASE_CURRENCY); } } /** * Delete the given currency code. * @param {number} tenantId * @param {string} currencyCode * @return {Promise<} */ public async deleteCurrency( tenantId: number, currencyCode: string ): Promise { const { Currency } = this.tenancy.models(tenantId); this.logger.info('[currencies] trying to delete the given currency.', { tenantId, currencyCode, }); await this.getCurrencyByCodeOrThrowError(tenantId, currencyCode); // Validate currency code not equals base currency. await this.validateCannotDeleteBaseCurrency(tenantId, currencyCode); await Currency.query().where('currency_code', currencyCode).delete(); this.logger.info('[currencies] the currency deleted successfully.', { tenantId, currencyCode, }); } /** * Listing currencies. * @param {number} tenantId * @return {Promise} */ public async listCurrencies(tenantId: number): Promise { const { Currency } = this.tenancy.models(tenantId); const settings = this.tenancy.settings(tenantId); const baseCurrency = settings.get({ group: 'organization', key: 'base_currency', }); const currencies = await Currency.query().onBuild((query) => { query.orderBy('createdAt', 'ASC'); }); const formattedCurrencies = currencies.map((currency) => ({ isBaseCurrency: baseCurrency === currency.currencyCode, ...currency, })); return formattedCurrencies; } /** * Seeds the given base currency to the currencies list. * @param {number} tenantId * @param {string} baseCurrency */ public async seedBaseCurrency(tenantId: number, baseCurrency: string) { const { Currency } = this.tenancy.models(tenantId); const currencyMeta = Currencies[baseCurrency]; const foundBaseCurrency = await Currency.query().findOne( 'currency_code', baseCurrency ); if (!foundBaseCurrency) { await Currency.query().insert({ currency_code: currencyMeta.code, currency_name: currencyMeta.name, }); } } }