UNPKG

somali-exchange-rates

Version:

πŸ‡ΈπŸ‡΄ Comprehensive Somali Exchange Rates platform with real-time rates, transfer fees, alerts, multi-language support, and advanced financial tools

1,546 lines (1,529 loc) β€’ 66.6 kB
"use strict"; var __create = Object.create; var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __getProtoOf = Object.getPrototypeOf; var __hasOwnProp = Object.prototype.hasOwnProperty; var __export = (target, all) => { for (var name in all) __defProp(target, name, { get: all[name], enumerable: true }); }; var __copyProps = (to, from, except, desc) => { if (from && typeof from === "object" || typeof from === "function") { for (let key of __getOwnPropNames(from)) if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); } return to; }; var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps( // If the importer is in node compatibility mode or this is not an ESM // file that has been converted to a CommonJS file using a Babel- // compatible transform (i.e. "__esModule" has not been set), then set // "default" to the CommonJS "module.exports" for node compatibility. isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target, mod )); var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); // src/index.ts var index_exports = {}; __export(index_exports, { AlertManager: () => AlertManager, ConfigManager: () => ConfigManager, ExportService: () => ExportService, HistoricalRateService: () => HistoricalRateService, LocalizationService: () => LocalizationService, MarketAnalyzer: () => MarketAnalyzer, ProviderManager: () => ProviderManager, RateStreamClient: () => RateStreamClient, RateStreamServer: () => RateStreamServer, SomaliaMarketService: () => SomaliaMarketService, analyzeMarket: () => analyzeMarket, calculateTransferFee: () => calculateTransferFee, compareTransferOptions: () => compareTransferOptions, convert: () => convert, createProvider: () => createProvider, createRateStream: () => createRateStream, detectAnomalies: () => detectAnomalies, exportAnalysisReport: () => exportAnalysisReport, exportRates: () => exportRates, exportToCSV: () => exportToCSV, exportToExcel: () => exportToExcel, formatCurrency: () => formatCurrency, formatLocalizedCurrency: () => formatLocalizedCurrency, formatLocalizedQuote: () => formatLocalizedQuote, formatSOS: () => formatSOS, formatTransferResult: () => formatTransferResult, getAlertManager: () => getAlertManager, getBestTransferOption: () => getBestTransferOption, getConfigManager: () => getConfigManager, getHistoricalRates: () => getHistoricalRates, getLocalizationService: () => getLocalizationService, getRate: () => getRate, getRateHistory: () => getRateHistory, getRates: () => getRates, getSomaliaMarketData: () => getSomaliaMarketData, getUserConfig: () => getUserConfig, getVolatility: () => getVolatility, listRateAlerts: () => listRateAlerts, loadUserConfig: () => loadUserConfig, quote: () => quote, removeRateAlert: () => removeRateAlert, runConfigWizard: () => runConfigWizard, saveUserConfig: () => saveUserConfig, setLanguage: () => setLanguage, setLocale: () => setLocale, setRateAlert: () => setRateAlert, setUserDefaultCurrencies: () => setUserDefaultCurrencies, setUserLanguage: () => setUserLanguage, setUserNotifications: () => setUserNotifications, startAlertMonitoring: () => startAlertMonitoring, stopAlertMonitoring: () => stopAlertMonitoring, translate: () => translate }); module.exports = __toCommonJS(index_exports); var import_node_os4 = __toESM(require("os")); var import_node_path5 = __toESM(require("path")); // src/utils.ts var import_promises = __toESM(require("fs/promises")); var import_node_path = __toESM(require("path")); async function tryReadJSON(p) { try { const raw = await import_promises.default.readFile(p, "utf8"); return JSON.parse(raw); } catch { return null; } } async function tryWriteJSON(p, data) { try { await import_promises.default.mkdir(import_node_path.default.dirname(p), { recursive: true }); await import_promises.default.writeFile(p, JSON.stringify(data, null, 2), "utf8"); } catch { } } function invert(baseToOthers, sosPerBase) { const out = {}; for (const [k, v] of Object.entries(baseToOthers)) { out[k] = v / sosPerBase; } return out; } function nice(n) { return Number(n.toFixed(n < 1 ? 4 : 2)); } // src/providers/exchangeratehost.ts var API = "https://api.exchangerate.host/latest"; var ExchangerateHostProvider = class { name = "exchangerate.host"; async fetchRatesSOS() { const symbols = ["SOS", "EUR", "GBP", "KES", "ETB", "AED", "SAR", "TRY", "CNY", "USD"].join(","); const url = `${API}?base=USD&symbols=${symbols}`; const res = await fetch(url); if (!res.ok) throw new Error(`Provider error ${res.status}`); const data = await res.json(); const usdTo = data.rates; const sosPerUsd = usdTo["SOS"]; if (!sosPerUsd) throw new Error("Provider returned no SOS rate"); return invert(usdTo, sosPerUsd); } }; // src/data/seed.json var seed_default = { USD: 175e-5, EUR: 16e-4, GBP: 135e-5, KES: 0.225, ETB: 0.102, AED: 64e-4, SAR: 66e-4, TRY: 0.056, CNY: 0.012 }; // src/cache.ts var memoryCache = null; function getMemoryCache() { return memoryCache; } function setMemoryCache(c) { memoryCache = c; } // src/providers/fixer.ts var API_BASE = "https://api.fixer.io/v1"; var FixerProvider = class { constructor(apiKey) { this.apiKey = apiKey; } name = "fixer.io"; priority = 2; timeout = 5e3; async fetchRatesSOS() { const symbols = ["SOS", "EUR", "GBP", "KES", "ETB", "AED", "SAR", "TRY", "CNY", "USD"].join(","); const url = `${API_BASE}/latest?access_key=${this.apiKey}&base=USD&symbols=${symbols}`; const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), this.timeout); try { const res = await fetch(url, { signal: controller.signal }); clearTimeout(timeoutId); if (!res.ok) throw new Error(`Fixer API error ${res.status}`); const data = await res.json(); if (!data.success) throw new Error(`Fixer API error: ${data.error?.info || "Unknown error"}`); const usdTo = data.rates; const sosPerUsd = usdTo["SOS"]; if (!sosPerUsd) throw new Error("Fixer returned no SOS rate"); return invert(usdTo, sosPerUsd); } catch (error) { clearTimeout(timeoutId); throw error; } } async fetchHistoricalRates(date) { const symbols = ["SOS", "EUR", "GBP", "KES", "ETB", "AED", "SAR", "TRY", "CNY", "USD"].join(","); const url = `${API_BASE}/${date}?access_key=${this.apiKey}&base=USD&symbols=${symbols}`; const res = await fetch(url); if (!res.ok) throw new Error(`Fixer historical API error ${res.status}`); const data = await res.json(); if (!data.success) throw new Error(`Fixer API error: ${data.error?.info || "Unknown error"}`); const usdTo = data.rates; const sosPerUsd = usdTo["SOS"]; if (!sosPerUsd) throw new Error("Fixer returned no SOS rate for date"); return invert(usdTo, sosPerUsd); } }; // src/providers/currencyapi.ts var API_BASE2 = "https://api.currencyapi.com/v3"; var CurrencyAPIProvider = class { constructor(apiKey) { this.apiKey = apiKey; } name = "currencyapi.com"; priority = 3; timeout = 5e3; async fetchRatesSOS() { const currencies = ["SOS", "EUR", "GBP", "KES", "ETB", "AED", "SAR", "TRY", "CNY", "USD"].join(","); const url = `${API_BASE2}/latest?apikey=${this.apiKey}&base_currency=USD&currencies=${currencies}`; const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), this.timeout); try { const res = await fetch(url, { signal: controller.signal }); clearTimeout(timeoutId); if (!res.ok) throw new Error(`CurrencyAPI error ${res.status}`); const data = await res.json(); if (data.errors) throw new Error(`CurrencyAPI error: ${JSON.stringify(data.errors)}`); const rates = {}; for (const [currency, info] of Object.entries(data.data)) { rates[currency] = info.value; } const sosPerUsd = rates["SOS"]; if (!sosPerUsd) throw new Error("CurrencyAPI returned no SOS rate"); return invert(rates, sosPerUsd); } catch (error) { clearTimeout(timeoutId); throw error; } } async fetchHistoricalRates(date) { const currencies = ["SOS", "EUR", "GBP", "KES", "ETB", "AED", "SAR", "TRY", "CNY", "USD"].join(","); const url = `${API_BASE2}/historical?apikey=${this.apiKey}&base_currency=USD&currencies=${currencies}&date=${date}`; const res = await fetch(url); if (!res.ok) throw new Error(`CurrencyAPI historical error ${res.status}`); const data = await res.json(); if (data.errors) throw new Error(`CurrencyAPI error: ${JSON.stringify(data.errors)}`); const rates = {}; for (const [currency, info] of Object.entries(data.data)) { rates[currency] = info.value; } const sosPerUsd = rates["SOS"]; if (!sosPerUsd) throw new Error("CurrencyAPI returned no SOS rate for date"); return invert(rates, sosPerUsd); } }; // src/providers/manager.ts var ProviderManager = class { config; constructor(config) { this.config = { primary: new ExchangerateHostProvider(), fallbacks: [], timeout: 1e4, maxRetries: 3, ...config }; } async fetchRates() { const providers = [this.config.primary, ...this.config.fallbacks]; let lastError = null; for (const provider of providers) { for (let attempt = 1; attempt <= this.config.maxRetries; attempt++) { try { console.log(`Attempting to fetch rates from ${provider.name} (attempt ${attempt})`); const rates = await this.fetchWithTimeout(provider, this.config.timeout); console.log(`Successfully fetched rates from ${provider.name}`); return rates; } catch (error) { lastError = error; console.warn(`Failed to fetch from ${provider.name} (attempt ${attempt}):`, error); if (attempt < this.config.maxRetries) { const delay = Math.pow(2, attempt - 1) * 1e3; await new Promise((resolve) => setTimeout(resolve, delay)); } } } } throw new Error(`All providers failed. Last error: ${lastError?.message}`); } async fetchHistoricalRates(date) { const providers = [this.config.primary, ...this.config.fallbacks].filter((p) => p.fetchHistoricalRates); let lastError = null; for (const provider of providers) { try { if (provider.fetchHistoricalRates) { console.log(`Fetching historical rates from ${provider.name} for ${date}`); const rates = await provider.fetchHistoricalRates(date); console.log(`Successfully fetched historical rates from ${provider.name}`); return rates; } } catch (error) { lastError = error; console.warn(`Failed to fetch historical rates from ${provider.name}:`, error); } } throw new Error(`All providers failed for historical data. Last error: ${lastError?.message}`); } async fetchWithTimeout(provider, timeout) { return new Promise((resolve, reject) => { const timer = setTimeout(() => { reject(new Error(`Provider ${provider.name} timed out after ${timeout}ms`)); }, timeout); provider.fetchRatesSOS().then((rates) => { clearTimeout(timer); resolve(rates); }).catch((error) => { clearTimeout(timer); reject(error); }); }); } addFallbackProvider(provider) { this.config.fallbacks.push(provider); this.config.fallbacks.sort((a, b) => (a.priority || 999) - (b.priority || 999)); } setPrimaryProvider(provider) { this.config.primary = provider; } getProviderStatus() { const providers = [this.config.primary, ...this.config.fallbacks]; return providers.map((provider) => ({ name: provider.name, priority: provider.priority || 999, available: true // Could implement health checks here })); } }; function createProvider(name, apiKey) { switch (name.toLowerCase()) { case "exchangerate-host": return new ExchangerateHostProvider(); case "fixer": if (!apiKey) throw new Error("Fixer provider requires API key"); return new FixerProvider(apiKey); case "currencyapi": if (!apiKey) throw new Error("CurrencyAPI provider requires API key"); return new CurrencyAPIProvider(apiKey); default: throw new Error(`Unknown provider: ${name}`); } } // src/historical.ts var import_node_path2 = __toESM(require("path")); var import_node_os = __toESM(require("os")); var HistoricalRateService = class { providerManager; cachePath; constructor(providerManager) { this.providerManager = providerManager || new ProviderManager(); this.cachePath = import_node_path2.default.join(import_node_os.default.homedir(), ".sosx", "historical-cache.json"); } async getHistoricalRates(date) { const cached = await this.getCachedRates(); if (cached[date]) { console.log(`Using cached historical rates for ${date}`); return cached[date]; } try { const rates = await this.providerManager.fetchHistoricalRates(date); await this.cacheRates(date, rates); return rates; } catch (error) { console.warn(`Failed to fetch historical rates for ${date}:`, error); throw error; } } async getRateHistory(options) { const { from, to, currency, baseCurrency = "SOS" } = options; const startDate = new Date(from); const endDate = new Date(to); const results = []; const currentDate = new Date(startDate); while (currentDate <= endDate) { const dateStr = currentDate.toISOString().split("T")[0]; try { const rates = await this.getHistoricalRates(dateStr); const rate = baseCurrency === "SOS" ? rates[currency] : 1 / rates[currency]; results.push({ date: dateStr, rate }); } catch (error) { console.warn(`Skipping ${dateStr} due to error:`, error); } currentDate.setDate(currentDate.getDate() + 1); } return results; } async getVolatility(currency, period = "30d") { const days = parseInt(period.replace("d", "")); const endDate = /* @__PURE__ */ new Date(); const startDate = /* @__PURE__ */ new Date(); startDate.setDate(startDate.getDate() - days); const history = await this.getRateHistory({ from: startDate.toISOString().split("T")[0], to: endDate.toISOString().split("T")[0], currency }); if (history.length < 2) return 0; const returns = []; for (let i = 1; i < history.length; i++) { const dailyReturn = (history[i].rate - history[i - 1].rate) / history[i - 1].rate; returns.push(dailyReturn); } const mean = returns.reduce((sum, ret) => sum + ret, 0) / returns.length; const variance = returns.reduce((sum, ret) => sum + Math.pow(ret - mean, 2), 0) / returns.length; return Math.sqrt(variance) * Math.sqrt(365); } async getCachedRates() { const cached = await tryReadJSON(this.cachePath); return cached || {}; } async cacheRates(date, rates) { const cached = await this.getCachedRates(); cached[date] = rates; const cutoffDate = /* @__PURE__ */ new Date(); cutoffDate.setDate(cutoffDate.getDate() - 90); const cutoffStr = cutoffDate.toISOString().split("T")[0]; Object.keys(cached).forEach((date2) => { if (date2 < cutoffStr) { delete cached[date2]; } }); await tryWriteJSON(this.cachePath, cached); } }; var historicalService; async function getHistoricalRates(date, provider) { if (!historicalService) { const providerManager = provider ? new ProviderManager({ primary: provider }) : void 0; historicalService = new HistoricalRateService(providerManager); } return historicalService.getHistoricalRates(date); } async function getRateHistory(currency, from, to) { if (!historicalService) { historicalService = new HistoricalRateService(); } return historicalService.getRateHistory({ currency, from, to }); } async function getVolatility(currency, period = "30d") { if (!historicalService) { historicalService = new HistoricalRateService(); } return historicalService.getVolatility(currency, period); } // src/alerts.ts var cron = __toESM(require("node-cron")); var nodemailer = __toESM(require("nodemailer")); var import_node_path3 = __toESM(require("path")); var import_node_os2 = __toESM(require("os")); var AlertManager = class { alertsPath; webhookConfigs = []; emailTransporter; monitoringTask; constructor() { this.alertsPath = import_node_path3.default.join(import_node_os2.default.homedir(), ".sosx", "alerts.json"); } async createAlert(alert) { const newAlert = { ...alert, id: this.generateId(), createdAt: /* @__PURE__ */ new Date(), active: true }; const alerts = await this.getAlerts(); alerts.push(newAlert); await this.saveAlerts(alerts); console.log(`Created alert: ${newAlert.from}/${newAlert.to} ${newAlert.direction} ${newAlert.threshold}`); this.startMonitoring(); return newAlert.id; } async updateAlert(id, updates) { const alerts = await this.getAlerts(); const index = alerts.findIndex((alert) => alert.id === id); if (index === -1) { throw new Error(`Alert with id ${id} not found`); } alerts[index] = { ...alerts[index], ...updates }; await this.saveAlerts(alerts); } async deleteAlert(id) { const alerts = await this.getAlerts(); const filtered = alerts.filter((alert) => alert.id !== id); if (filtered.length === alerts.length) { throw new Error(`Alert with id ${id} not found`); } await this.saveAlerts(filtered); console.log(`Deleted alert ${id}`); } async getAlerts() { const alerts = await tryReadJSON(this.alertsPath); return alerts || []; } async checkAlerts() { const alerts = await this.getAlerts(); const activeAlerts = alerts.filter((alert) => alert.active); if (activeAlerts.length === 0) return; try { const rates = await getRates(); for (const alert of activeAlerts) { const currentRate = rates[alert.to] / rates[alert.from]; const shouldTrigger = this.shouldTriggerAlert(alert, currentRate); if (shouldTrigger) { await this.triggerAlert(alert, currentRate); } } } catch (error) { console.error("Error checking alerts:", error); } } shouldTriggerAlert(alert, currentRate) { if (alert.direction === "above") { return currentRate > alert.threshold; } else { return currentRate < alert.threshold; } } async triggerAlert(alert, currentRate) { const message = `Alert triggered: ${alert.from}/${alert.to} is ${currentRate.toFixed(6)} (${alert.direction} ${alert.threshold})`; console.log(message); if (alert.webhook) { await this.sendWebhook(alert.webhook, { type: "alert-triggered", alert, currentRate, message, timestamp: (/* @__PURE__ */ new Date()).toISOString() }); } if (alert.email && this.emailTransporter) { await this.sendEmail(alert.email, "Rate Alert Triggered", message); } for (const config of this.webhookConfigs) { if (config.events.includes("alert-triggered") && config.currencies.includes(alert.from) && config.currencies.includes(alert.to)) { await this.sendWebhook(config.url, { type: "alert-triggered", alert, currentRate, message, timestamp: (/* @__PURE__ */ new Date()).toISOString() }); } } } async sendWebhook(url, payload) { try { const response = await fetch(url, { method: "POST", headers: { "Content-Type": "application/json", "User-Agent": "Somali-Exchange-Rates/1.0" }, body: JSON.stringify(payload) }); if (!response.ok) { console.warn(`Webhook failed: ${response.status} ${response.statusText}`); } } catch (error) { console.error("Webhook error:", error); } } async sendEmail(to, subject, text) { if (!this.emailTransporter) return; try { await this.emailTransporter.sendMail({ from: process.env.SMTP_FROM || "alerts@sosx.com", to, subject, text }); } catch (error) { console.error("Email error:", error); } } setupEmailTransporter(config) { this.emailTransporter = nodemailer.createTransporter(config); } addWebhookConfig(config) { this.webhookConfigs.push(config); } startMonitoring(interval = "*/5 * * * *") { if (this.monitoringTask) { this.monitoringTask.stop(); } this.monitoringTask = cron.schedule(interval, async () => { await this.checkAlerts(); }, { scheduled: false }); this.monitoringTask.start(); console.log(`Started alert monitoring with interval: ${interval}`); } stopMonitoring() { if (this.monitoringTask) { this.monitoringTask.stop(); this.monitoringTask = void 0; console.log("Stopped alert monitoring"); } } async saveAlerts(alerts) { await tryWriteJSON(this.alertsPath, alerts); } generateId() { return Math.random().toString(36).substr(2, 9); } }; var alertManager; function getAlertManager() { if (!alertManager) { alertManager = new AlertManager(); } return alertManager; } async function setRateAlert(from, to, threshold, direction, options = {}) { const manager = getAlertManager(); return manager.createAlert({ from, to, threshold, direction, webhook: options.webhook, email: options.email }); } async function removeRateAlert(id) { const manager = getAlertManager(); return manager.deleteAlert(id); } async function listRateAlerts() { const manager = getAlertManager(); return manager.getAlerts(); } function startAlertMonitoring(interval) { const manager = getAlertManager(); manager.startMonitoring(interval); } function stopAlertMonitoring() { const manager = getAlertManager(); manager.stopMonitoring(); } // src/transfer-fees.ts var PROVIDER_FEES = { "western-union": { "bank-transfer": { fixedFee: 5, percentageFee: 0.015, // 1.5% exchangeRateMargin: 0.02, // 2% margin on exchange rate estimatedTime: "1-3 business days", minimumFee: 5, maximumFee: 50 }, "cash-pickup": { fixedFee: 8, percentageFee: 0.02, // 2% exchangeRateMargin: 0.025, // 2.5% margin estimatedTime: "Within minutes", minimumFee: 8, maximumFee: 75 }, "mobile-money": { fixedFee: 3, percentageFee: 0.01, // 1% exchangeRateMargin: 0.015, // 1.5% margin estimatedTime: "Within minutes", minimumFee: 3, maximumFee: 25 } }, "remitly": { "bank-transfer": { fixedFee: 3.99, percentageFee: 0.01, // 1% exchangeRateMargin: 0.015, // 1.5% margin estimatedTime: "1-2 business days", minimumFee: 3.99, maximumFee: 30 }, "cash-pickup": { fixedFee: 4.99, percentageFee: 0.015, // 1.5% exchangeRateMargin: 0.02, // 2% margin estimatedTime: "Within minutes", minimumFee: 4.99, maximumFee: 40 }, "mobile-money": { fixedFee: 1.99, percentageFee: 5e-3, // 0.5% exchangeRateMargin: 0.01, // 1% margin estimatedTime: "Within minutes", minimumFee: 1.99, maximumFee: 15 } }, "worldremit": { "bank-transfer": { fixedFee: 2.99, percentageFee: 0.012, // 1.2% exchangeRateMargin: 0.018, // 1.8% margin estimatedTime: "1-2 business days", minimumFee: 2.99, maximumFee: 35 }, "cash-pickup": { fixedFee: 5.99, percentageFee: 0.018, // 1.8% exchangeRateMargin: 0.022, // 2.2% margin estimatedTime: "Within minutes", minimumFee: 5.99, maximumFee: 45 }, "mobile-money": { fixedFee: 2.49, percentageFee: 8e-3, // 0.8% exchangeRateMargin: 0.012, // 1.2% margin estimatedTime: "Within minutes", minimumFee: 2.49, maximumFee: 20 } }, "wise": { "bank-transfer": { fixedFee: 1.5, percentageFee: 5e-3, // 0.5% exchangeRateMargin: 5e-3, // 0.5% margin (Wise uses mid-market rate) estimatedTime: "1-2 business days", minimumFee: 1.5, maximumFee: 15 }, "cash-pickup": { fixedFee: 0, // Not available percentageFee: 0, exchangeRateMargin: 0, estimatedTime: "Not available", minimumFee: 0, maximumFee: 0 }, "mobile-money": { fixedFee: 2, percentageFee: 7e-3, // 0.7% exchangeRateMargin: 8e-3, // 0.8% margin estimatedTime: "Within hours", minimumFee: 2, maximumFee: 12 } } }; async function calculateTransferFee(options) { const { amount, from, to, provider, method } = options; const providerFees = PROVIDER_FEES[provider]; if (!providerFees) { throw new Error(`Unsupported provider: ${provider}`); } const feeStructure = providerFees[method]; if (!feeStructure) { throw new Error(`Method ${method} not available for ${provider}`); } if (feeStructure.fixedFee === 0 && feeStructure.percentageFee === 0) { throw new Error(`${method} is not available for ${provider}`); } const midMarketRate = await convert(1, from, to); const providerRate = midMarketRate * (1 - feeStructure.exchangeRateMargin); const percentageFee = amount * feeStructure.percentageFee; let totalFee = feeStructure.fixedFee + percentageFee; if (feeStructure.minimumFee && totalFee < feeStructure.minimumFee) { totalFee = feeStructure.minimumFee; } if (feeStructure.maximumFee && totalFee > feeStructure.maximumFee) { totalFee = feeStructure.maximumFee; } const totalCost = amount + totalFee; const recipientAmount = amount * providerRate; return { fee: totalFee, exchangeRate: providerRate, totalCost, recipientAmount, provider, estimatedTime: feeStructure.estimatedTime }; } async function compareTransferOptions(amount, from, to, method) { const providers = ["western-union", "remitly", "worldremit", "wise"]; const results = []; for (const provider of providers) { try { const result = await calculateTransferFee({ amount, from, to, provider, method }); results.push(result); } catch (error) { console.warn(`Failed to calculate fees for ${provider}:`, error); } } return results.sort((a, b) => a.totalCost - b.totalCost); } async function getBestTransferOption(amount, from, to) { const methods = ["bank-transfer", "cash-pickup", "mobile-money"]; let bestOption = null; for (const method of methods) { try { const options = await compareTransferOptions(amount, from, to, method); if (options.length > 0) { const best = options[0]; if (!bestOption || best.totalCost < bestOption.result.totalCost) { bestOption = { method, result: best }; } } } catch (error) { console.warn(`Failed to compare options for ${method}:`, error); } } if (!bestOption) { throw new Error("No transfer options available"); } return bestOption; } function formatTransferResult(result) { return ` Provider: ${result.provider} Fee: $${result.fee.toFixed(2)} Exchange Rate: ${result.exchangeRate.toFixed(6)} Total Cost: $${result.totalCost.toFixed(2)} Recipient Gets: ${result.recipientAmount.toFixed(2)} Estimated Time: ${result.estimatedTime} `.trim(); } // src/analysis.ts var MarketAnalyzer = class { async analyzeMarket(from, to, period = "30d") { const days = parseInt(period.replace("d", "")); const endDate = /* @__PURE__ */ new Date(); const startDate = /* @__PURE__ */ new Date(); startDate.setDate(startDate.getDate() - days); const history = await getRateHistory( to, startDate.toISOString().split("T")[0], endDate.toISOString().split("T")[0] ); if (history.length < 14) { throw new Error("Insufficient data for analysis (minimum 14 days required)"); } const rates = history.map((h) => h.rate); const volatility = await getVolatility(to, period); return { volatility, trend: this.calculateTrend(rates), support: this.calculateSupport(rates), resistance: this.calculateResistance(rates), rsi: this.calculateRSI(rates), sma: this.calculateSMA(rates, [7, 14, 30]), ema: this.calculateEMA(rates, [7, 14, 30]) }; } calculateTrend(rates) { if (rates.length < 2) return "neutral"; const recentRates = rates.slice(-7); const olderRates = rates.slice(-14, -7); const recentAvg = recentRates.reduce((sum, rate) => sum + rate, 0) / recentRates.length; const olderAvg = olderRates.reduce((sum, rate) => sum + rate, 0) / olderRates.length; const change = (recentAvg - olderAvg) / olderAvg; if (change > 0.02) return "bullish"; if (change < -0.02) return "bearish"; return "neutral"; } calculateSupport(rates) { const recentRates = rates.slice(-30); return Math.min(...recentRates); } calculateResistance(rates) { const recentRates = rates.slice(-30); return Math.max(...recentRates); } calculateRSI(rates, period = 14) { if (rates.length < period + 1) return 50; const changes = []; for (let i = 1; i < rates.length; i++) { changes.push(rates[i] - rates[i - 1]); } const recentChanges = changes.slice(-period); const gains = recentChanges.filter((change) => change > 0); const losses = recentChanges.filter((change) => change < 0).map((loss) => Math.abs(loss)); const avgGain = gains.length > 0 ? gains.reduce((sum, gain) => sum + gain, 0) / gains.length : 0; const avgLoss = losses.length > 0 ? losses.reduce((sum, loss) => sum + loss, 0) / losses.length : 0; if (avgLoss === 0) return 100; const rs = avgGain / avgLoss; return 100 - 100 / (1 + rs); } calculateSMA(rates, periods) { return periods.map((period) => { if (rates.length < period) return rates[rates.length - 1] || 0; const recentRates = rates.slice(-period); return recentRates.reduce((sum, rate) => sum + rate, 0) / recentRates.length; }); } calculateEMA(rates, periods) { return periods.map((period) => { if (rates.length < period) return rates[rates.length - 1] || 0; const multiplier = 2 / (period + 1); let ema = rates[0]; for (let i = 1; i < rates.length; i++) { ema = rates[i] * multiplier + ema * (1 - multiplier); } return ema; }); } }; var SomaliaMarketService = class { async getSomaliaMarketData() { const baseRate = 570; return { regions: { mogadishu: { officialRate: baseRate, blackMarketRate: baseRate * 1.05, // 5% premium spread: 0.05, volume: 1e6, // Daily volume in USD lastUpdated: /* @__PURE__ */ new Date() }, hargeisa: { officialRate: baseRate, blackMarketRate: baseRate * 1.08, // 8% premium spread: 0.08, volume: 5e5, lastUpdated: /* @__PURE__ */ new Date() }, bosaso: { officialRate: baseRate, blackMarketRate: baseRate * 1.12, // 12% premium spread: 0.12, volume: 2e5, lastUpdated: /* @__PURE__ */ new Date() }, kismayo: { officialRate: baseRate, blackMarketRate: baseRate * 1.15, // 15% premium spread: 0.15, volume: 15e4, lastUpdated: /* @__PURE__ */ new Date() }, garowe: { officialRate: baseRate, blackMarketRate: baseRate * 1.1, // 10% premium spread: 0.1, volume: 1e5, lastUpdated: /* @__PURE__ */ new Date() } } }; } async getRegionalSpread(region) { const marketData = await this.getSomaliaMarketData(); const regionData = marketData.regions[region.toLowerCase()]; if (!regionData) { throw new Error(`Unknown region: ${region}`); } return regionData.spread; } async getBestRegionalRate(amount) { const marketData = await this.getSomaliaMarketData(); let bestRegion = ""; let bestRate = 0; let bestAmount = 0; for (const [region, data] of Object.entries(marketData.regions)) { const rate = data.blackMarketRate || data.officialRate; const convertedAmount = amount * rate; if (convertedAmount > bestAmount) { bestRegion = region; bestRate = rate; bestAmount = convertedAmount; } } const worstAmount = Math.min(...Object.values(marketData.regions).map( (data) => amount * (data.blackMarketRate || data.officialRate) )); const savings = bestAmount - worstAmount; return { region: bestRegion, rate: bestRate, savings }; } }; var marketAnalyzer; var somaliaMarketService; async function analyzeMarket(from, to, period) { if (!marketAnalyzer) { marketAnalyzer = new MarketAnalyzer(); } return marketAnalyzer.analyzeMarket(from, to, period); } async function getSomaliaMarketData() { if (!somaliaMarketService) { somaliaMarketService = new SomaliaMarketService(); } return somaliaMarketService.getSomaliaMarketData(); } async function detectAnomalies(from, to, options) { const { threshold, timeWindow } = options; const days = parseInt(timeWindow.replace(/[^\d]/g, "")); const endDate = /* @__PURE__ */ new Date(); const startDate = /* @__PURE__ */ new Date(); startDate.setDate(startDate.getDate() - days); const history = await getRateHistory( to, startDate.toISOString().split("T")[0], endDate.toISOString().split("T")[0] ); if (history.length < 2) { return { anomaly: false, deviation: 0, message: "Insufficient data" }; } const currentRate = history[history.length - 1].rate; const previousRate = history[history.length - 2].rate; const change = Math.abs((currentRate - previousRate) / previousRate); const anomaly = change > threshold; const message = anomaly ? `Anomaly detected: ${(change * 100).toFixed(2)}% change in ${from}/${to} rate` : "No anomaly detected"; return { anomaly, deviation: change, message }; } // src/localization.ts var LOCALIZED_STRINGS = { // Currency names "currency.SOS": { en: "Somali Shilling", so: "Shilin Soomaali", ar: "\u0634\u0644\u0646 \u0635\u0648\u0645\u0627\u0644\u064A" }, "currency.USD": { en: "US Dollar", so: "Doolar Maraykan", ar: "\u062F\u0648\u0644\u0627\u0631 \u0623\u0645\u0631\u064A\u0643\u064A" }, "currency.EUR": { en: "Euro", so: "Yuuroo", ar: "\u064A\u0648\u0631\u0648" }, "currency.GBP": { en: "British Pound", so: "Bownd Biritish", ar: "\u062C\u0646\u064A\u0647 \u0625\u0633\u062A\u0631\u0644\u064A\u0646\u064A" }, "currency.KES": { en: "Kenyan Shilling", so: "Shilin Kiiniya", ar: "\u0634\u0644\u0646 \u0643\u064A\u0646\u064A" }, "currency.ETB": { en: "Ethiopian Birr", so: "Bir Itoobiya", ar: "\u0628\u064A\u0631 \u0625\u062B\u064A\u0648\u0628\u064A" }, "currency.AED": { en: "UAE Dirham", so: "Dirham Imaaraadka", ar: "\u062F\u0631\u0647\u0645 \u0625\u0645\u0627\u0631\u0627\u062A\u064A" }, "currency.SAR": { en: "Saudi Riyal", so: "Riyaal Sacuudi", ar: "\u0631\u064A\u0627\u0644 \u0633\u0639\u0648\u062F\u064A" }, "currency.TRY": { en: "Turkish Lira", so: "Lira Turki", ar: "\u0644\u064A\u0631\u0629 \u062A\u0631\u0643\u064A\u0629" }, "currency.CNY": { en: "Chinese Yuan", so: "Yuan Shiinaha", ar: "\u064A\u0648\u0627\u0646 \u0635\u064A\u0646\u064A" }, // Currency symbols "symbol.SOS": { en: "Sh", so: "Sh", ar: "\u0634.\u0635" }, // Common phrases "exchange_rate": { en: "Exchange Rate", so: "Qiimaha Sarifka", ar: "\u0633\u0639\u0631 \u0627\u0644\u0635\u0631\u0641" }, "conversion": { en: "Conversion", so: "Beddelka", ar: "\u0627\u0644\u062A\u062D\u0648\u064A\u0644" }, "amount": { en: "Amount", so: "Qadarka", ar: "\u0627\u0644\u0645\u0628\u0644\u063A" }, "from": { en: "From", so: "Ka", ar: "\u0645\u0646" }, "to": { en: "To", so: "Ilaa", ar: "\u0625\u0644\u0649" }, "equals": { en: "equals", so: "le'eg yahay", ar: "\u064A\u0633\u0627\u0648\u064A" }, "rate_updated": { en: "Rate updated", so: "Qiimaha waa la cusbooneysiiyay", ar: "\u062A\u0645 \u062A\u062D\u062F\u064A\u062B \u0627\u0644\u0633\u0639\u0631" }, "offline_mode": { en: "Offline mode", so: "Hab aan internetka lahayn", ar: "\u0648\u0636\u0639 \u0639\u062F\u0645 \u0627\u0644\u0627\u062A\u0635\u0627\u0644" }, "cache_used": { en: "Using cached data", so: "Isticmaalka xogta kaydsan", ar: "\u0627\u0633\u062A\u062E\u062F\u0627\u0645 \u0627\u0644\u0628\u064A\u0627\u0646\u0627\u062A \u0627\u0644\u0645\u062E\u0632\u0646\u0629" }, // Time periods "daily": { en: "Daily", so: "Maalin kasta", ar: "\u064A\u0648\u0645\u064A" }, "weekly": { en: "Weekly", so: "Toddobaad kasta", ar: "\u0623\u0633\u0628\u0648\u0639\u064A" }, "monthly": { en: "Monthly", so: "Bil kasta", ar: "\u0634\u0647\u0631\u064A" }, // Market analysis "trend.bullish": { en: "Bullish", so: "Kor u socda", ar: "\u0635\u0627\u0639\u062F" }, "trend.bearish": { en: "Bearish", so: "Hoos u socda", ar: "\u0647\u0627\u0628\u0637" }, "trend.neutral": { en: "Neutral", so: "Dhexdhexaad", ar: "\u0645\u062D\u0627\u064A\u062F" }, "volatility": { en: "Volatility", so: "Doorsooma", ar: "\u0627\u0644\u062A\u0642\u0644\u0628" }, "support": { en: "Support", so: "Taageero", ar: "\u0627\u0644\u062F\u0639\u0645" }, "resistance": { en: "Resistance", so: "Iska caabin", ar: "\u0627\u0644\u0645\u0642\u0627\u0648\u0645\u0629" } }; var LocalizationService = class { currentLanguage = "en"; currentLocale = "en-US"; setLanguage(language) { this.currentLanguage = language; } setLocale(locale) { this.currentLocale = locale; if (locale.startsWith("so")) { this.currentLanguage = "so"; } else if (locale.startsWith("ar")) { this.currentLanguage = "ar"; } else { this.currentLanguage = "en"; } } translate(key, language) { const lang = language || this.currentLanguage; const strings = LOCALIZED_STRINGS[key]; if (!strings) { console.warn(`Translation key not found: ${key}`); return key; } return strings[lang] || strings.en || key; } getCurrencyName(currency, language) { return this.translate(`currency.${currency}`, language); } getCurrencySymbol(currency, language) { const symbol = this.translate(`symbol.${currency}`, language); return symbol !== `symbol.${currency}` ? symbol : this.getDefaultSymbol(currency); } formatCurrency(amount, currency, options = {}) { const lang = options.language || this.currentLanguage; const locale = this.getLocaleForLanguage(lang); try { let formatted = new Intl.NumberFormat(locale, { style: "decimal", minimumFractionDigits: currency === "SOS" ? 0 : 2, maximumFractionDigits: currency === "SOS" ? 0 : 6 }).format(amount); if (options.showSymbol !== false) { const symbol = this.getCurrencySymbol(currency, lang); formatted = `${symbol} ${formatted}`; } if (options.showCode) { formatted = `${formatted} ${currency}`; } return formatted; } catch (error) { const symbol = options.showSymbol !== false ? this.getCurrencySymbol(currency, lang) : ""; const code = options.showCode ? ` ${currency}` : ""; return `${symbol} ${amount.toLocaleString()}${code}`.trim(); } } formatQuote(amount, fromCurrency, toCurrency, convertedAmount, options = {}) { const lang = options.language || this.currentLanguage; const fromFormatted = this.formatCurrency(amount, fromCurrency, { language: lang }); const toFormatted = this.formatCurrency(convertedAmount, toCurrency, { language: lang }); const equals = this.translate("equals", lang); return `${fromFormatted} ${equals} ${toFormatted}`; } formatTrend(trend, language) { return this.translate(`trend.${trend}`, language); } getAvailableLanguages() { return [ { code: "en", name: "English", nativeName: "English" }, { code: "so", name: "Somali", nativeName: "Soomaali" }, { code: "ar", name: "Arabic", nativeName: "\u0627\u0644\u0639\u0631\u0628\u064A\u0629" } ]; } getLocaleForLanguage(language) { switch (language) { case "so": return "so-SO"; case "ar": return "ar-SA"; default: return "en-US"; } } getDefaultSymbol(currency) { const symbols = { SOS: "Sh", USD: "$", EUR: "\u20AC", GBP: "\xA3", KES: "KSh", ETB: "Br", AED: "\u062F.\u0625", SAR: "\uFDFC", TRY: "\u20BA", CNY: "\xA5" }; return symbols[currency] || currency; } }; var localizationService; function getLocalizationService() { if (!localizationService) { localizationService = new LocalizationService(); } return localizationService; } function setLanguage(language) { getLocalizationService().setLanguage(language); } function setLocale(locale) { getLocalizationService().setLocale(locale); } function translate(key, language) { return getLocalizationService().translate(key, language); } function formatLocalizedCurrency(amount, currency, options) { return getLocalizationService().formatCurrency(amount, currency, options); } function formatLocalizedQuote(amount, fromCurrency, toCurrency, convertedAmount, options) { return getLocalizationService().formatQuote(amount, fromCurrency, toCurrency, convertedAmount, options); } // src/export.ts var XLSX = __toESM(require("xlsx")); var import_csv_writer = require("csv-writer"); var import_node_fs = require("fs"); var ExportService = class { async exportRates(options) { const { format, period, currencies, output } = options; const days = parseInt(period.replace(/[^\d]/g, "")); const endDate = /* @__PURE__ */ new Date(); const startDate = /* @__PURE__ */ new Date(); startDate.setDate(startDate.getDate() - days); const data = []; for (const currency of currencies) { if (currency === "SOS") continue; try { const history = await getRateHistory( currency, startDate.toISOString().split("T")[0], endDate.toISOString().split("T")[0] ); history.forEach((record) => { data.push({ date: record.date, currency, rate: record.rate, baseCurrency: "SOS" }); }); } catch (error) { console.warn(`Failed to get history for ${currency}:`, error); } } data.sort((a, b) => new Date(a.date).getTime() - new Date(b.date).getTime()); const filename = output || this.generateFilename(format, period, currencies); switch (format) { case "csv": return this.exportToCSV(data, filename); case "xlsx": return this.exportToXLSX(data, filename); case "json": return this.exportToJSON(data, filename); case "pdf": return this.exportToPDF(data, filename); default: throw new Error(`Unsupported export format: ${format}`); } } async exportAnalysisReport(currencies, period, output) { const analysisData = []; for (const currency of currencies) { if (currency === "SOS") continue; try { const analysis = await analyzeMarket("SOS", currency, period); analysisData.push({ currency, baseCurrency: "SOS", period, volatility: analysis.volatility, trend: analysis.trend, support: analysis.support, resistance: analysis.resistance, rsi: analysis.rsi, sma7: analysis.sma[0], sma14: analysis.sma[1], sma30: analysis.sma[2], ema7: analysis.ema[0], ema14: analysis.ema[1], ema30: analysis.ema[2], generatedAt: (/* @__PURE__ */ new Date()).toISOString() }); } catch (error) { console.warn(`Failed to analyze ${currency}:`, error); } } const filename = output || `analysis-report-${period}-${Date.now()}.xlsx`; return this.exportToXLSX(analysisData, filename); } async exportToCSV(data, filename) { if (data.length === 0) { throw new Error("No data to export"); } const csvWriter = (0, import_csv_writer.createObjectCsvWriter)({ path: filename, header: Object.keys(data[0]).map((key) => ({ id: key, title: key })) }); await csvWriter.writeRecords(data); console.log(`Exported ${data.length} records to ${filename}`); return filename; } async exportToXLSX(data, filename) { if (data.length === 0) { throw new Error("No data to export"); } const worksheet = XLSX.utils.json_to_sheet(data); const workbook = XLSX.utils.book_new(); XLSX.utils.book_append_sheet(workbook, worksheet, "Exchange Rates"); const range = XLSX.utils.decode_range(worksheet["!ref"] || "A1"); const colWidths = []; for (let col = range.s.c; col <= range.e.c; col++) { let maxWidth = 10; for (let row = range.s.r; row <= range.e.r; row++) { const cellAddress = XLSX.utils.encode_cell({ r: row, c: col }); const cell = worksheet[cellAddress]; if (cell && cell.v) { const cellLength = cell.v.toString().length; maxWidth = Math.max(maxWidth, cellLength); } } colWidths.push({ wch: Math.min(maxWidth + 2, 50) }); } worksheet["!cols"] = colWidths; XLSX.writeFile(workbook, filename); console.log(`Exported ${data.length} records to ${filename}`); return filename; } async exportToJSON(data, filename) { const jsonData = { exportedAt: (/* @__PURE__ */ new Date()).toISOString(), recordCount: data.length, data }; await import_node_fs.promises.writeFile(filename, JSON.stringify(jsonData, null, 2)); console.log(`Exported ${data.length} records to ${filename}`); return filename; } async exportToPDF(data, filename) { const html = this.generateHTMLReport(data); const htmlFilename = filename.replace(".pdf", ".html"); await import_node_fs.promises.writeFile(htmlFilename, html); console.log(`Exported HTML report to ${htmlFilename} (PDF conversion requires additional library)`); return htmlFilename; } generateHTMLReport(data) { const headers = data.length > 0 ? Object.keys(data[0]) : []; return ` <!DOCTYPE html> <html> <head> <title>Somali Exchange Rates Report</title> <style> body { font-family: Arial, sans-serif; margin: 20px; } table { border-collapse: collapse; width: 100%; } th, td { border: 1px solid #ddd; padding: 8px; text-align: left; } th { background-color: #f2f2f2; } .header { margin-bottom: 20px; } .summary { margin-bottom: 20px; padding: 10px; background-color: #f9f9f9; } </style> </head> <body> <div class="header"> <h1>Somali Exchange Rates Report</h1> <p>Generated on: ${(/* @__PURE__ */ new Date()).toLocaleString()}</p> </div> <div class="summary"> <h2>Summary</h2> <p>Total Records: ${data.length}</p> <p>Currencies: ${[...new Set(data.map((d) => d.currency))].join(", ")}</p> <p>Date Range: ${data.length > 0 ? `${data[0].date} to ${data[data.length - 1].date}` : "N/A"}</p> </div> <table> <thead> <tr> ${headers.map((header) => `<th>${header}</th>`).join("")} </tr> </thead> <tbody> ${data.map((row) => ` <tr> ${headers.map((header) => `<td>${row[header] || ""}</td>`).join("")} </tr> `).join("")} </tbody> </table> </body> </html>`; } generateFilename(format, period, currencies) { const timestamp = (/* @__PURE__ */ new Date()).toISOString().split("T")[0]; const currencyList = currencies.slice(0, 3).join("-"); return `exchange-rates-${currencyList}-${period}-${timestamp}.${format}`; } }; var exportService; async function exportRates(options) { if (!exportService) { exportService = new ExportService(); } return exportService.exportRates(options); } async function exportAnalysisReport(currencies, period, output) { if (!exportService) { exportService = new ExportService(); } return exportService.exportAnalysisReport(currencies, period, output); } async function exportToCSV(currencies, period, output) { return exportRates({ format: "csv", currencies, period, output }); } async function exportToExcel(currencies, period, output) { return exportRates({