import { Repository, LessThanOrEqual } from 'typeorm'; import { AppDataSource } from '../../config/typeorm.js'; import { CurrencyRate, RateSource } from './entities/currency-rate.entity.js'; import { Currency } from './entities/currency.entity.js'; import { NotFoundError } from '../../shared/errors/index.js'; import { logger } from '../../shared/utils/logger.js'; export interface CreateCurrencyRateDto { tenantId?: string; fromCurrencyCode: string; toCurrencyCode: string; rate: number; rateDate: Date; source?: RateSource; createdBy?: string; } export interface CurrencyRateFilter { tenantId?: string; fromCurrencyCode?: string; toCurrencyCode?: string; dateFrom?: Date; dateTo?: Date; limit?: number; } export interface ConvertCurrencyDto { amount: number; fromCurrencyCode: string; toCurrencyCode: string; date?: Date; tenantId?: string; } class CurrencyRatesService { private repository: Repository; private currencyRepository: Repository; constructor() { this.repository = AppDataSource.getRepository(CurrencyRate); this.currencyRepository = AppDataSource.getRepository(Currency); } async findAll(filter: CurrencyRateFilter = {}): Promise { logger.debug('Finding currency rates', { filter }); const query = this.repository .createQueryBuilder('rate') .leftJoinAndSelect('rate.fromCurrency', 'fromCurrency') .leftJoinAndSelect('rate.toCurrency', 'toCurrency'); if (filter.tenantId) { query.andWhere('(rate.tenantId = :tenantId OR rate.tenantId IS NULL)', { tenantId: filter.tenantId, }); } if (filter.fromCurrencyCode) { query.andWhere('fromCurrency.code = :fromCode', { fromCode: filter.fromCurrencyCode.toUpperCase(), }); } if (filter.toCurrencyCode) { query.andWhere('toCurrency.code = :toCode', { toCode: filter.toCurrencyCode.toUpperCase(), }); } if (filter.dateFrom) { query.andWhere('rate.rateDate >= :dateFrom', { dateFrom: filter.dateFrom }); } if (filter.dateTo) { query.andWhere('rate.rateDate <= :dateTo', { dateTo: filter.dateTo }); } query.orderBy('rate.rateDate', 'DESC'); if (filter.limit) { query.take(filter.limit); } return query.getMany(); } async findById(id: string): Promise { logger.debug('Finding currency rate by id', { id }); const rate = await this.repository.findOne({ where: { id }, relations: ['fromCurrency', 'toCurrency'], }); if (!rate) { throw new NotFoundError('Tipo de cambio no encontrado'); } return rate; } async getRate( fromCurrencyCode: string, toCurrencyCode: string, date: Date = new Date(), tenantId?: string ): Promise { logger.debug('Getting currency rate', { fromCurrencyCode, toCurrencyCode, date, tenantId }); // Same currency = rate 1 if (fromCurrencyCode.toUpperCase() === toCurrencyCode.toUpperCase()) { return 1; } // Find direct rate const directRate = await this.repository .createQueryBuilder('rate') .leftJoin('rate.fromCurrency', 'fromCurrency') .leftJoin('rate.toCurrency', 'toCurrency') .where('fromCurrency.code = :fromCode', { fromCode: fromCurrencyCode.toUpperCase() }) .andWhere('toCurrency.code = :toCode', { toCode: toCurrencyCode.toUpperCase() }) .andWhere('rate.rateDate <= :date', { date }) .andWhere('(rate.tenantId = :tenantId OR rate.tenantId IS NULL)', { tenantId: tenantId || null }) .orderBy('rate.rateDate', 'DESC') .addOrderBy('rate.tenantId', 'DESC', 'NULLS LAST') .getOne(); if (directRate) { return Number(directRate.rate); } // Try inverse rate const inverseRate = await this.repository .createQueryBuilder('rate') .leftJoin('rate.fromCurrency', 'fromCurrency') .leftJoin('rate.toCurrency', 'toCurrency') .where('fromCurrency.code = :toCode', { toCode: toCurrencyCode.toUpperCase() }) .andWhere('toCurrency.code = :fromCode', { fromCode: fromCurrencyCode.toUpperCase() }) .andWhere('rate.rateDate <= :date', { date }) .andWhere('(rate.tenantId = :tenantId OR rate.tenantId IS NULL)', { tenantId: tenantId || null }) .orderBy('rate.rateDate', 'DESC') .addOrderBy('rate.tenantId', 'DESC', 'NULLS LAST') .getOne(); if (inverseRate) { return 1 / Number(inverseRate.rate); } return null; } async convert(dto: ConvertCurrencyDto): Promise<{ amount: number; rate: number } | null> { logger.debug('Converting currency', dto); const rate = await this.getRate( dto.fromCurrencyCode, dto.toCurrencyCode, dto.date || new Date(), dto.tenantId ); if (rate === null) { return null; } return { amount: dto.amount * rate, rate, }; } async create(dto: CreateCurrencyRateDto): Promise { logger.info('Creating currency rate', { dto }); // Get currency IDs const fromCurrency = await this.currencyRepository.findOne({ where: { code: dto.fromCurrencyCode.toUpperCase() }, }); if (!fromCurrency) { throw new NotFoundError(`Moneda ${dto.fromCurrencyCode} no encontrada`); } const toCurrency = await this.currencyRepository.findOne({ where: { code: dto.toCurrencyCode.toUpperCase() }, }); if (!toCurrency) { throw new NotFoundError(`Moneda ${dto.toCurrencyCode} no encontrada`); } // Check if rate already exists for this date const existing = await this.repository.findOne({ where: { tenantId: dto.tenantId || undefined, fromCurrencyId: fromCurrency.id, toCurrencyId: toCurrency.id, rateDate: dto.rateDate, }, }); if (existing) { // Update existing rate existing.rate = dto.rate; existing.source = dto.source || 'manual'; return this.repository.save(existing); } const rate = this.repository.create({ tenantId: dto.tenantId || null, fromCurrencyId: fromCurrency.id, toCurrencyId: toCurrency.id, rate: dto.rate, rateDate: dto.rateDate, source: dto.source || 'manual', createdBy: dto.createdBy || null, }); return this.repository.save(rate); } async delete(id: string): Promise { logger.info('Deleting currency rate', { id }); const rate = await this.findById(id); await this.repository.remove(rate); } async getHistory( fromCurrencyCode: string, toCurrencyCode: string, days: number = 30, tenantId?: string ): Promise { logger.debug('Getting rate history', { fromCurrencyCode, toCurrencyCode, days, tenantId }); const dateFrom = new Date(); dateFrom.setDate(dateFrom.getDate() - days); return this.findAll({ fromCurrencyCode, toCurrencyCode, dateFrom, tenantId, limit: days, }); } async getLatestRates(baseCurrencyCode: string = 'MXN', tenantId?: string): Promise> { logger.debug('Getting latest rates', { baseCurrencyCode, tenantId }); const rates = new Map(); const currencies = await this.currencyRepository.find({ where: { active: true } }); for (const currency of currencies) { if (currency.code === baseCurrencyCode.toUpperCase()) { rates.set(currency.code, 1); continue; } const rate = await this.getRate(baseCurrencyCode, currency.code, new Date(), tenantId); if (rate !== null) { rates.set(currency.code, rate); } } return rates; } } export const currencyRatesService = new CurrencyRatesService();