diff --git a/.env.example b/.env.example index 78945ab91..f32107eb4 100644 --- a/.env.example +++ b/.env.example @@ -57,4 +57,10 @@ GOTENBERG_DOCS_URL=http://server:3000/public/ # Gotenberg API - (development) # GOTENBERG_URL=http://localhost:9000 -# GOTENBERG_DOCS_URL=http://host.docker.internal:3000/public/ \ No newline at end of file +# GOTENBERG_DOCS_URL=http://host.docker.internal:3000/public/ + +# Exchange Rate Service +EXCHANGE_RATE_SERVICE=open-exchange-rate + +# Open Exchange Rate +OPEN_EXCHANGE_RATE_APP_ID= \ No newline at end of file diff --git a/packages/server/src/api/controllers/ExchangeRates.ts b/packages/server/src/api/controllers/ExchangeRates.ts index 4b808e921..63c476bf9 100644 --- a/packages/server/src/api/controllers/ExchangeRates.ts +++ b/packages/server/src/api/controllers/ExchangeRates.ts @@ -1,19 +1,16 @@ import { Service, Inject } from 'typedi'; import { Router, Request, Response, NextFunction } from 'express'; -import { check, param, query } from 'express-validator'; +import { query, oneOf } from 'express-validator'; import asyncMiddleware from '@/api/middleware/asyncMiddleware'; import BaseController from './BaseController'; import { ServiceError } from '@/exceptions'; -import ExchangeRatesService from '@/services/ExchangeRates/ExchangeRatesService'; -import DynamicListingService from '@/services/DynamicListing/DynamicListService'; +import { EchangeRateErrors } from '@/lib/ExchangeRate/types'; +import { ExchangeRateApplication } from '@/services/ExchangeRates/ExchangeRateApplication'; @Service() export default class ExchangeRatesController extends BaseController { @Inject() - exchangeRatesService: ExchangeRatesService; - - @Inject() - dynamicListService: DynamicListingService; + private exchangeRatesApp: ExchangeRateApplication; /** * Constructor method. @@ -22,164 +19,40 @@ export default class ExchangeRatesController extends BaseController { const router = Router(); router.get( - '/', - [...this.exchangeRatesListSchema], + '/latest', + [ + oneOf([ + query('to_currency').exists().isString().isISO4217(), + query('from_currency').exists().isString().isISO4217(), + ]), + ], this.validationResult, - asyncMiddleware(this.exchangeRates.bind(this)), - this.dynamicListService.handlerErrorsToResponse, - this.handleServiceError, - ); - router.post( - '/', - [...this.exchangeRateDTOSchema], - this.validationResult, - asyncMiddleware(this.addExchangeRate.bind(this)), - this.handleServiceError - ); - router.post( - '/:id', - [...this.exchangeRateEditDTOSchema, ...this.exchangeRateIdSchema], - this.validationResult, - asyncMiddleware(this.editExchangeRate.bind(this)), - this.handleServiceError - ); - router.delete( - '/:id', - [...this.exchangeRateIdSchema], - this.validationResult, - asyncMiddleware(this.deleteExchangeRate.bind(this)), + asyncMiddleware(this.latestExchangeRate.bind(this)), this.handleServiceError ); return router; } - get exchangeRatesListSchema() { - return [ - query('page').optional().isNumeric().toInt(), - query('page_size').optional().isNumeric().toInt(), - - query('column_sort_by').optional(), - query('sort_order').optional().isIn(['desc', 'asc']), - ]; - } - - get exchangeRateDTOSchema() { - return [ - check('exchange_rate').exists().isNumeric().toFloat(), - check('currency_code').exists().trim().escape(), - check('date').exists().isISO8601(), - ]; - } - - get exchangeRateEditDTOSchema() { - return [check('exchange_rate').exists().isNumeric().toFloat()]; - } - - get exchangeRateIdSchema() { - return [param('id').isNumeric().toInt()]; - } - - get exchangeRatesIdsSchema() { - return [ - query('ids').isArray({ min: 2 }), - query('ids.*').isNumeric().toInt(), - ]; - } - /** * Retrieve exchange rates. * @param {Request} req * @param {Response} res * @param {NextFunction} next */ - async exchangeRates(req: Request, res: Response, next: NextFunction) { + private async latestExchangeRate( + req: Request, + res: Response, + next: NextFunction + ) { const { tenantId } = req; - const filter = { - page: 1, - pageSize: 12, - filterRoles: [], - columnSortBy: 'created_at', - sortOrder: 'asc', - ...this.matchedQueryData(req), - }; - if (filter.stringifiedFilterRoles) { - filter.filterRoles = JSON.parse(filter.stringifiedFilterRoles); - } - try { - const exchangeRates = await this.exchangeRatesService.listExchangeRates( - tenantId, - filter - ); - return res.status(200).send({ exchange_rates: exchangeRates }); - } catch (error) { - next(error); - } - } - - /** - * Adds a new exchange rate on the given date. - * @param {Request} req - * @param {Response} res - * @param {NextFunction} next - */ - async addExchangeRate(req: Request, res: Response, next: NextFunction) { - const { tenantId } = req; - const exchangeRateDTO = this.matchedBodyData(req); + const exchangeRateQuery = this.matchedQueryData(req); try { - const exchangeRate = await this.exchangeRatesService.newExchangeRate( + const exchangeRate = await this.exchangeRatesApp.latest( tenantId, - exchangeRateDTO + exchangeRateQuery ); - return res.status(200).send({ id: exchangeRate.id }); - } catch (error) { - next(error); - } - } - - /** - * Edit the given exchange rate. - * @param {Request} req - * @param {Response} res - * @param {NextFunction} next - */ - async editExchangeRate(req: Request, res: Response, next: NextFunction) { - const { tenantId } = req; - const { id: exchangeRateId } = req.params; - const exchangeRateDTO = this.matchedBodyData(req); - - try { - const exchangeRate = await this.exchangeRatesService.editExchangeRate( - tenantId, - exchangeRateId, - exchangeRateDTO - ); - - return res.status(200).send({ - id: exchangeRateId, - message: 'The exchange rate has been edited successfully.', - }); - } catch (error) { - next(error); - } - } - - /** - * Delete the given exchange rate from the storage. - * @param {Request} req - * @param {Response} res - * @param {NextFunction} next - */ - async deleteExchangeRate(req: Request, res: Response, next: NextFunction) { - const { tenantId } = req; - const { id: exchangeRateId } = req.params; - - try { - await this.exchangeRatesService.deleteExchangeRate( - tenantId, - exchangeRateId - ); - return res.status(200).send({ id: exchangeRateId }); + return res.status(200).send(exchangeRate); } catch (error) { next(error); } @@ -192,26 +65,56 @@ export default class ExchangeRatesController extends BaseController { * @param {Response} res * @param {NextFunction} next */ - handleServiceError( + private handleServiceError( error: Error, req: Request, res: Response, next: NextFunction ) { if (error instanceof ServiceError) { - if (error.errorType === 'EXCHANGE_RATE_NOT_FOUND') { - return res.status(404).send({ - errors: [{ type: 'EXCHANGE.RATE.NOT.FOUND', code: 200 }], - }); - } - if (error.errorType === 'NOT_FOUND_EXCHANGE_RATES') { + if (EchangeRateErrors.EX_RATE_INVALID_BASE_CURRENCY === error.errorType) { return res.status(400).send({ - errors: [{ type: 'EXCHANGE.RATES.IS.NOT.FOUND', code: 100 }], + errors: [ + { + type: EchangeRateErrors.EX_RATE_INVALID_BASE_CURRENCY, + code: 100, + message: 'The given base currency is invalid.', + }, + ], }); - } - if (error.errorType === 'EXCHANGE_RATE_PERIOD_EXISTS') { + } else if ( + EchangeRateErrors.EX_RATE_SERVICE_NOT_ALLOWED === error.errorType + ) { return res.status(400).send({ - errors: [{ type: 'EXCHANGE.RATE.PERIOD.EXISTS', code: 300 }], + errors: [ + { + type: EchangeRateErrors.EX_RATE_SERVICE_NOT_ALLOWED, + code: 200, + message: 'The service is not allowed', + }, + ], + }); + } else if ( + EchangeRateErrors.EX_RATE_SERVICE_API_KEY_REQUIRED === error.errorType + ) { + return res.status(400).send({ + errors: [ + { + type: EchangeRateErrors.EX_RATE_SERVICE_API_KEY_REQUIRED, + code: 300, + message: 'The API key is required', + }, + ], + }); + } else if (EchangeRateErrors.EX_RATE_LIMIT_EXCEEDED === error.errorType) { + return res.status(400).send({ + errors: [ + { + type: EchangeRateErrors.EX_RATE_LIMIT_EXCEEDED, + code: 400, + message: 'The API rate limit has been exceeded', + }, + ], }); } } diff --git a/packages/server/src/config/index.ts b/packages/server/src/config/index.ts index 0dc9d9676..4d096875a 100644 --- a/packages/server/src/config/index.ts +++ b/packages/server/src/config/index.ts @@ -169,4 +169,14 @@ module.exports = { * to application detarmines to upgrade. */ databaseBatch: 4, + + /** + * Exchange rate. + */ + exchangeRate: { + service: 'open-exchange-rate', + openExchangeRate: { + appId: process.env.OPEN_EXCHANGE_RATE_APP_ID, + } + } }; diff --git a/packages/server/src/interfaces/ExchangeRate.ts b/packages/server/src/interfaces/ExchangeRate.ts index fc3bd33e4..45080fc0f 100644 --- a/packages/server/src/interfaces/ExchangeRate.ts +++ b/packages/server/src/interfaces/ExchangeRate.ts @@ -1,36 +1,10 @@ -import { IFilterRole } from './DynamicFilter'; +export interface ExchangeRateLatestDTO { + toCurrency: string; + fromCurrency: string; +} -export interface IExchangeRate { - id: number, - currencyCode: string, - exchangeRate: number, - date: Date, - createdAt: Date, - updatedAt: Date, -}; - -export interface IExchangeRateDTO { - currencyCode: string, - exchangeRate: number, - date: Date, -}; - -export interface IExchangeRateEditDTO { - exchangeRate: number, -}; - -export interface IExchangeRateFilter { - page: number, - pageSize: number, - filterRoles?: IFilterRole[]; - columnSortBy: string; - sortOrder: string; -}; - -export interface IExchangeRatesService { - newExchangeRate(tenantId: number, exchangeRateDTO: IExchangeRateDTO): Promise; - editExchangeRate(tenantId: number, exchangeRateId: number, editExRateDTO: IExchangeRateEditDTO): Promise; - - deleteExchangeRate(tenantId: number, exchangeRateId: number): Promise; - listExchangeRates(tenantId: number, exchangeRateFilter: IExchangeRateFilter): Promise; -}; \ No newline at end of file +export interface EchangeRateLatestPOJO { + baseCurrency: string; + toCurrency: string; + exchangeRate: number; +} diff --git a/packages/server/src/lib/ExchangeRate/ExchangeRate.ts b/packages/server/src/lib/ExchangeRate/ExchangeRate.ts new file mode 100644 index 000000000..5bb84cff3 --- /dev/null +++ b/packages/server/src/lib/ExchangeRate/ExchangeRate.ts @@ -0,0 +1,45 @@ +import { OpenExchangeRate } from './OpenExchangeRate'; +import { ExchangeRateServiceType, IExchangeRateService } from './types'; + +export class ExchangeRate { + private exchangeRateService: IExchangeRateService; + private exchangeRateServiceType: ExchangeRateServiceType; + + /** + * Constructor method. + * @param {ExchangeRateServiceType} service + */ + constructor(service: ExchangeRateServiceType) { + this.exchangeRateServiceType = service; + this.initService(); + } + + /** + * Initialize the exchange rate service based on the service type. + */ + private initService() { + if ( + this.exchangeRateServiceType === ExchangeRateServiceType.OpenExchangeRate + ) { + this.setExchangeRateService(new OpenExchangeRate()); + } + } + + /** + * Sets the exchange rate service. + * @param {IExchangeRateService} service + */ + private setExchangeRateService(service: IExchangeRateService) { + this.exchangeRateService = service; + } + + /** + * Gets the latest exchange rate. + * @param {string} baseCurrency + * @param {string} toCurrency + * @returns {number} + */ + public latest(baseCurrency: string, toCurrency: string): Promise { + return this.exchangeRateService.latest(baseCurrency, toCurrency); + } +} diff --git a/packages/server/src/lib/ExchangeRate/OpenExchangeRate.ts b/packages/server/src/lib/ExchangeRate/OpenExchangeRate.ts new file mode 100644 index 000000000..221c5c5c5 --- /dev/null +++ b/packages/server/src/lib/ExchangeRate/OpenExchangeRate.ts @@ -0,0 +1,81 @@ +import Axios, { AxiosError } from 'axios'; +import { + EchangeRateErrors, + IExchangeRateService, + OPEN_EXCHANGE_RATE_LATEST_URL, +} from './types'; +import config from '@/config'; +import { ServiceError } from '@/exceptions'; + +export class OpenExchangeRate implements IExchangeRateService { + /** + * Gets the latest exchange rate. + * @param {string} baseCurrency + * @param {string} toCurrency + * @returns {Promise { + // Vaclidates the Open Exchange Rate api id early. + this.validateApiIdExistance(); + + try { + const result = await Axios.get(OPEN_EXCHANGE_RATE_LATEST_URL, { + params: { + app_id: config.exchangeRate.openExchangeRate.appId, + base: baseCurrency, + symbols: toCurrency, + }, + }); + return result.data.rates[toCurrency] || (1 as number); + } catch (error) { + this.handleLatestErrors(error); + } + } + + /** + * Validates the Open Exchange Rate api id. + * @throws {ServiceError} + */ + private validateApiIdExistance() { + const apiId = config.exchangeRate.openExchangeRate.appId; + + if (!apiId) { + throw new ServiceError( + EchangeRateErrors.EX_RATE_SERVICE_API_KEY_REQUIRED, + 'Invalid App ID provided. Please sign up at https://openexchangerates.org/signup, or contact support@openexchangerates.org.' + ); + } + } + + /** + * Handles the latest errors. + * @param {any} error + * @throws {ServiceError} + */ + private handleLatestErrors(error: any) { + if (error.response.data?.message === 'missing_app_id') { + throw new ServiceError( + EchangeRateErrors.EX_RATE_SERVICE_API_KEY_REQUIRED, + 'Invalid App ID provided. Please sign up at https://openexchangerates.org/signup, or contact support@openexchangerates.org.' + ); + } else if (error.response.data?.message === 'invalid_app_id') { + throw new ServiceError( + EchangeRateErrors.EX_RATE_SERVICE_API_KEY_REQUIRED, + 'Invalid App ID provided. Please sign up at https://openexchangerates.org/signup, or contact support@openexchangerates.org.' + ); + } else if (error.response.data?.message === 'not_allowed') { + throw new ServiceError( + EchangeRateErrors.EX_RATE_SERVICE_NOT_ALLOWED, + 'Getting the exchange rate from the given base currency to the given currency is not allowed.' + ); + } else if (error.response.data?.message === 'invalid_base') { + throw new ServiceError( + EchangeRateErrors.EX_RATE_INVALID_BASE_CURRENCY, + 'The given base currency is invalid.' + ); + } + } +} diff --git a/packages/server/src/lib/ExchangeRate/types.ts b/packages/server/src/lib/ExchangeRate/types.ts new file mode 100644 index 000000000..8b40125cd --- /dev/null +++ b/packages/server/src/lib/ExchangeRate/types.ts @@ -0,0 +1,17 @@ +export interface IExchangeRateService { + latest(baseCurrency: string, toCurrency: string): Promise; +} + +export enum ExchangeRateServiceType { + OpenExchangeRate = 'OpenExchangeRate', +} + +export enum EchangeRateErrors { + EX_RATE_SERVICE_NOT_ALLOWED = 'EX_RATE_SERVICE_NOT_ALLOWED', + EX_RATE_LIMIT_EXCEEDED = 'EX_RATE_LIMIT_EXCEEDED', + EX_RATE_SERVICE_API_KEY_REQUIRED = 'EX_RATE_SERVICE_API_KEY_REQUIRED', + EX_RATE_INVALID_BASE_CURRENCY = 'EX_RATE_INVALID_BASE_CURRENCY', +} + +export const OPEN_EXCHANGE_RATE_LATEST_URL = + 'https://openexchangerates.org/api/latest.json'; diff --git a/packages/server/src/services/ExchangeRates/ExchangeRateApplication.ts b/packages/server/src/services/ExchangeRates/ExchangeRateApplication.ts new file mode 100644 index 000000000..9e51c5d72 --- /dev/null +++ b/packages/server/src/services/ExchangeRates/ExchangeRateApplication.ts @@ -0,0 +1,21 @@ +import { Inject } from 'typedi'; +import { ExchangeRatesService } from './ExchangeRatesService'; +import { EchangeRateLatestPOJO, ExchangeRateLatestDTO } from '@/interfaces'; + +export class ExchangeRateApplication { + @Inject() + private exchangeRateService: ExchangeRatesService; + + /** + * Gets the latest exchange rate. + * @param {number} tenantId + * @param {ExchangeRateLatestDTO} exchangeRateLatestDTO + * @returns {Promise} + */ + public latest( + tenantId: number, + exchangeRateLatestDTO: ExchangeRateLatestDTO + ): Promise { + return this.exchangeRateService.latest(tenantId, exchangeRateLatestDTO); + } +} diff --git a/packages/server/src/services/ExchangeRates/ExchangeRatesService.ts b/packages/server/src/services/ExchangeRates/ExchangeRatesService.ts index 9bc63fbfd..4cde544ad 100644 --- a/packages/server/src/services/ExchangeRates/ExchangeRatesService.ts +++ b/packages/server/src/services/ExchangeRates/ExchangeRatesService.ts @@ -1,193 +1,37 @@ -import moment from 'moment'; -import { difference } from 'lodash'; -import { Service, Inject } from 'typedi'; -import { ServiceError } from '@/exceptions'; -import DynamicListingService from '@/services/DynamicListing/DynamicListService'; -import { - EventDispatcher, - EventDispatcherInterface, -} from 'decorators/eventDispatcher'; -import { - IExchangeRateDTO, - IExchangeRate, - IExchangeRatesService, - IExchangeRateEditDTO, - IExchangeRateFilter, -} from '@/interfaces'; -import TenancyService from '@/services/Tenancy/TenancyService'; - -const ERRORS = { - NOT_FOUND_EXCHANGE_RATES: 'NOT_FOUND_EXCHANGE_RATES', - EXCHANGE_RATE_PERIOD_EXISTS: 'EXCHANGE_RATE_PERIOD_EXISTS', - EXCHANGE_RATE_NOT_FOUND: 'EXCHANGE_RATE_NOT_FOUND', -}; +import { Service } from 'typedi'; +import { ExchangeRate } from '@/lib/ExchangeRate/ExchangeRate'; +import { ExchangeRateServiceType } from '@/lib/ExchangeRate/types'; +import { EchangeRateLatestPOJO, ExchangeRateLatestDTO } from '@/interfaces'; +import { TenantMetadata } from '@/system/models'; @Service() -export default class ExchangeRatesService implements IExchangeRatesService { - @Inject('logger') - logger: any; - - @EventDispatcher() - eventDispatcher: EventDispatcherInterface; - - @Inject() - tenancy: TenancyService; - - @Inject() - dynamicListService: DynamicListingService; - +export class ExchangeRatesService { /** - * Creates a new exchange rate. + * Gets the latest exchange rate. * @param {number} tenantId - * @param {IExchangeRateDTO} exchangeRateDTO - * @returns {Promise} + * @param {number} exchangeRateLatestDTO + * @returns {EchangeRateLatestPOJO} */ - public async newExchangeRate( + public async latest( tenantId: number, - exchangeRateDTO: IExchangeRateDTO - ): Promise { - const { ExchangeRate } = this.tenancy.models(tenantId); + exchangeRateLatestDTO: ExchangeRateLatestDTO + ): Promise { + const organization = await TenantMetadata.query().findOne({ tenantId }); - this.logger.info('[exchange_rates] trying to insert new exchange rate.', { - tenantId, - exchangeRateDTO, - }); - await this.validateExchangeRatePeriodExistance(tenantId, exchangeRateDTO); + // Assign the organization base currency as a default currency + // if no currency is provided + const fromCurrency = + exchangeRateLatestDTO.fromCurrency || organization.baseCurrency; + const toCurrency = + exchangeRateLatestDTO.toCurrency || organization.baseCurrency; - const exchangeRate = await ExchangeRate.query().insertAndFetch({ - ...exchangeRateDTO, - date: moment(exchangeRateDTO.date).format('YYYY-MM-DD'), - }); - this.logger.info('[exchange_rates] inserted successfully.', { - tenantId, - exchangeRateDTO, - }); - return exchangeRate; - } + const exchange = new ExchangeRate(ExchangeRateServiceType.OpenExchangeRate); + const exchangeRate = await exchange.latest(fromCurrency, toCurrency); - /** - * Edits the exchange rate details. - * @param {number} tenantId - Tenant id. - * @param {number} exchangeRateId - Exchange rate id. - * @param {IExchangeRateEditDTO} editExRateDTO - Edit exchange rate DTO. - */ - public async editExchangeRate( - tenantId: number, - exchangeRateId: number, - editExRateDTO: IExchangeRateEditDTO - ): Promise { - const { ExchangeRate } = this.tenancy.models(tenantId); - - this.logger.info('[exchange_rates] trying to edit exchange rate.', { - tenantId, - exchangeRateId, - editExRateDTO, - }); - await this.validateExchangeRateExistance(tenantId, exchangeRateId); - - await ExchangeRate.query() - .where('id', exchangeRateId) - .update({ ...editExRateDTO }); - this.logger.info('[exchange_rates] exchange rate edited successfully.', { - tenantId, - exchangeRateId, - editExRateDTO, - }); - } - - /** - * Deletes the given exchange rate. - * @param {number} tenantId - Tenant id. - * @param {number} exchangeRateId - Exchange rate id. - */ - public async deleteExchangeRate( - tenantId: number, - exchangeRateId: number - ): Promise { - const { ExchangeRate } = this.tenancy.models(tenantId); - await this.validateExchangeRateExistance(tenantId, exchangeRateId); - - await ExchangeRate.query().findById(exchangeRateId).delete(); - } - - /** - * Listing exchange rates details. - * @param {number} tenantId - Tenant id. - * @param {IExchangeRateFilter} exchangeRateFilter - Exchange rates list filter. - */ - public async listExchangeRates( - tenantId: number, - exchangeRateFilter: IExchangeRateFilter - ): Promise { - const { ExchangeRate } = this.tenancy.models(tenantId); - const dynamicFilter = await this.dynamicListService.dynamicList( - tenantId, - ExchangeRate, - exchangeRateFilter - ); - // Retrieve exchange rates by the given query. - const exchangeRates = await ExchangeRate.query() - .onBuild((query) => { - dynamicFilter.buildQuery()(query); - }) - .pagination(exchangeRateFilter.page - 1, exchangeRateFilter.pageSize); - - return exchangeRates; - } - - /** - * Validates period of the exchange rate existance. - * @param {number} tenantId - Tenant id. - * @param {IExchangeRateDTO} exchangeRateDTO - Exchange rate DTO. - * @return {Promise} - */ - private async validateExchangeRatePeriodExistance( - tenantId: number, - exchangeRateDTO: IExchangeRateDTO - ): Promise { - const { ExchangeRate } = this.tenancy.models(tenantId); - - this.logger.info('[exchange_rates] trying to validate period existance.', { - tenantId, - }); - const foundExchangeRate = await ExchangeRate.query() - .where('currency_code', exchangeRateDTO.currencyCode) - .where('date', exchangeRateDTO.date); - - if (foundExchangeRate.length > 0) { - this.logger.info('[exchange_rates] given exchange rate period exists.', { - tenantId, - }); - throw new ServiceError(ERRORS.EXCHANGE_RATE_PERIOD_EXISTS); - } - } - - /** - * Validate the given echange rate id existance. - * @param {number} tenantId - Tenant id. - * @param {number} exchangeRateId - Exchange rate id. - * @returns {Promise} - */ - private async validateExchangeRateExistance( - tenantId: number, - exchangeRateId: number - ) { - const { ExchangeRate } = this.tenancy.models(tenantId); - - this.logger.info( - '[exchange_rates] trying to validate exchange rate id existance.', - { tenantId, exchangeRateId } - ); - const foundExchangeRate = await ExchangeRate.query().findById( - exchangeRateId - ); - - if (!foundExchangeRate) { - this.logger.info('[exchange_rates] exchange rate not found.', { - tenantId, - exchangeRateId, - }); - throw new ServiceError(ERRORS.EXCHANGE_RATE_NOT_FOUND); - } + return { + baseCurrency: fromCurrency, + toCurrency: exchangeRateLatestDTO.toCurrency, + exchangeRate, + }; } } diff --git a/packages/server/src/system/models/TenantMetadata.ts b/packages/server/src/system/models/TenantMetadata.ts index 4664cfd6d..7040a6a68 100644 --- a/packages/server/src/system/models/TenantMetadata.ts +++ b/packages/server/src/system/models/TenantMetadata.ts @@ -1,6 +1,8 @@ import BaseModel from 'models/Model'; export default class TenantMetadata extends BaseModel { + baseCurrency: string; + /** * Table name. */ diff --git a/packages/webapp/src/containers/Entries/AutoExchangeProvider.tsx b/packages/webapp/src/containers/Entries/AutoExchangeProvider.tsx index 6554b85a1..c4b5ea1f9 100644 --- a/packages/webapp/src/containers/Entries/AutoExchangeProvider.tsx +++ b/packages/webapp/src/containers/Entries/AutoExchangeProvider.tsx @@ -1,6 +1,5 @@ -import { useExchangeRate } from '@/hooks/query'; -import { useCurrentOrganization } from '@/hooks/state'; import React from 'react'; +import { useLatestExchangeRate } from '@/hooks/query'; interface AutoExchangeRateProviderProps { children: React.ReactNode; @@ -18,16 +17,19 @@ const AutoExchangeRateContext = React.createContext( function AutoExchangeRateProvider({ children }: AutoExchangeRateProviderProps) { const [autoExRateCurrency, setAutoExRateCurrency] = React.useState(''); - const currentOrganization = useCurrentOrganization(); // Retrieves the exchange rate. const { data: autoExchangeRate, isLoading: isAutoExchangeRateLoading } = - useExchangeRate(autoExRateCurrency, currentOrganization.base_currency, { - enabled: Boolean(currentOrganization.base_currency && autoExRateCurrency), - refetchOnWindowFocus: false, - staleTime: 0, - cacheTime: 0, - }); + useLatestExchangeRate( + { fromCurrency: autoExRateCurrency }, + { + enabled: Boolean(autoExRateCurrency), + refetchOnWindowFocus: false, + staleTime: 0, + cacheTime: 0, + retry: 0, + }, + ); const value = { autoExRateCurrency, diff --git a/packages/webapp/src/containers/Entries/withExRateItemEntriesPriceRecalc.tsx b/packages/webapp/src/containers/Entries/withExRateItemEntriesPriceRecalc.tsx index 490ef20b5..a1fd7af39 100644 --- a/packages/webapp/src/containers/Entries/withExRateItemEntriesPriceRecalc.tsx +++ b/packages/webapp/src/containers/Entries/withExRateItemEntriesPriceRecalc.tsx @@ -98,24 +98,30 @@ interface UseSyncExRateToFormProps { */ export const useSyncExRateToForm = ({ onSynced }: UseSyncExRateToFormProps) => { const { setFieldValue, values } = useFormikContext(); - const { autoExRateCurrency, autoExchangeRate } = useAutoExRateContext(); + const { autoExRateCurrency, autoExchangeRate, isAutoExchangeRateLoading } = + useAutoExRateContext(); const updateEntriesOnExChange = useUpdateEntriesOnExchangeRateChange(); // Sync the fetched real-time exchanage rate to the form. useEffect(() => { - if (autoExchangeRate?.exchange_rate && autoExRateCurrency) { - setFieldValue('exchange_rate', autoExchangeRate?.exchange_rate + ''); + if (!isAutoExchangeRateLoading && autoExRateCurrency) { + // Sets a default ex. rate to 1 in case the exchange rate service wasn't configured. + // or returned an error from the server-side. + const exchangeRate = autoExchangeRate?.exchange_rate || 1; + + setFieldValue('exchange_rate', exchangeRate + ''); setFieldValue( 'entries', - updateEntriesOnExChange( - values.exchange_rate, - autoExchangeRate?.exchange_rate, - ), + updateEntriesOnExChange(values.exchange_rate, exchangeRate), ); onSynced?.(); } // eslint-disable-next-line react-hooks/exhaustive-deps - }, [autoExchangeRate?.exchange_rate, autoExRateCurrency]); + }, [ + autoExchangeRate?.exchange_rate, + autoExRateCurrency, + isAutoExchangeRateLoading, + ]); return null; }; diff --git a/packages/webapp/src/hooks/query/exchangeRates.tsx b/packages/webapp/src/hooks/query/exchangeRates.tsx index f56958040..36700276b 100644 --- a/packages/webapp/src/hooks/query/exchangeRates.tsx +++ b/packages/webapp/src/hooks/query/exchangeRates.tsx @@ -1,34 +1,36 @@ // @ts-nocheck import { useQuery } from 'react-query'; import QUERY_TYPES from './types'; +import useApiRequest from '../useRequest'; -function getRandomItemFromArray(arr) { - const randomIndex = Math.floor(Math.random() * arr.length); - return arr[randomIndex]; -} -function delay(t, val) { - return new Promise((resolve) => setTimeout(resolve, t, val)); +interface LatestExchangeRateQuery { + fromCurrency?: string; + toCurrency?: string; } + /** - * Retrieves tax rates. + * Retrieves latest exchange rate. * @param {number} customerId - Customer id. */ -export function useExchangeRate( - fromCurrency: string, - toCurrency: string, +export function useLatestExchangeRate( + { toCurrency, fromCurrency }: LatestExchangeRateQuery, props, ) { - return useQuery( - [QUERY_TYPES.EXCHANGE_RATE, fromCurrency, toCurrency], - async () => { - await delay(100); + const apiRequest = useApiRequest(); - return { - from_currency: fromCurrency, - to_currency: toCurrency, - exchange_rate: 1.00, - }; - }, + return useQuery( + [QUERY_TYPES.EXCHANGE_RATE, toCurrency, fromCurrency], + () => + apiRequest + .http({ + url: `/api/exchange_rates/latest`, + method: 'get', + params: { + to_currency: toCurrency, + from_currency: fromCurrency, + }, + }) + .then((res) => res.data), props, ); }