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
JavaScript
"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¤cies=${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¤cies=${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({