Files
app/src/services/exchange-rates.ts
BOHA 4f4b12f039 security: fix all Medium findings from FLAWS_REPORT audit
- Auth: TOTP replay protection with counter tracking, constant-time
  backup code comparison, atomic lockout increment, per-token logout
- Invoices/PDFs: net-based VAT calculation, dangerous URL scheme
  stripping in cleanQuillHtml, orders-pdf error handling
- Orders: reject item changes on status transition, cascading
  delete cleanup, take:1 with orderBy
- Projects: atomic rename collision handling, MIME/extension
  validation, empty customer name rejection
- Attendance: Czech public holiday awareness in frontend fund
  calculation, leave_hours 0 handling, invalid date NaN guard,
  bounded per-month queries in workfund
- Users/Admin: profile audit logging + password validation, session
  revocation guard, session ID validation, dashboard DB aggregation,
  soft-deleted record protection in scope templates
- Frontend: FormField label linkage, Pagination ARIA, error
  handling in OrderConfirmationModal, 401 propagation, GPS emoji
  hidden from screen readers, table sort state fix, geolocation
  race/abort cleanup, Leaflet popup DOM safety, Vehicles toggleActive
  minimal body, CompanySettings ref mutation fix, OfferDetail unlock
  abort, AttendanceBalances combined fetches
- Utils: env validation, Puppeteer concurrency mutex, invoice alert
  cron cleanup on shutdown, body limit alignment, TOTP error logging,
  trustProxy from env, symlink rejection, rate cache Map usage

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-24 08:24:14 +02:00

68 lines
1.9 KiB
TypeScript

/**
* Czech National Bank (ČNB) exchange rate service.
* Fetches daily rates and caches them.
* API: https://api.cnb.cz/cnbapi/exrates/daily
*/
interface CnbRate {
currencyCode: string;
rate: number;
amount: number;
}
const rateCache = new Map<string, Record<string, number>>();
async function fetchRatesForDate(
date?: string,
): Promise<Record<string, number>> {
const key = date || "today";
if (rateCache.has(key)) return rateCache.get(key)!;
try {
let url = "https://api.cnb.cz/cnbapi/exrates/daily?lang=EN";
if (date) url += `&date=${date}`;
const response = await fetch(url);
if (!response.ok) throw new Error(`CNB API: ${response.status}`);
const data = (await response.json()) as { rates: CnbRate[] };
const rates: Record<string, number> = { CZK: 1 };
for (const r of data.rates) {
rates[r.currencyCode] = r.rate / r.amount;
}
rateCache.set(key, rates);
return rates;
} catch (err) {
console.error("Failed to fetch CNB exchange rates:", err);
if (rateCache.has("today")) return rateCache.get("today")!;
throw new Error("Nepodařilo se získat aktuální kurzy z ČNB");
}
}
/** Convert an amount from a given currency to CZK using CNB rates */
export async function toCzk(
amount: number,
currency: string,
date?: string,
): Promise<number> {
if (currency === "CZK") return amount;
const rates = await fetchRatesForDate(date);
const rate = rates[currency];
if (!rate) throw new Error(`Neznámá měna: ${currency}`);
return Math.round(amount * rate * 100) / 100;
}
/** Get CNB rate for a currency (CZK per 1 unit), optionally for a specific date */
export async function getRate(
currency: string,
date?: string,
): Promise<number> {
if (currency === "CZK") return 1;
const rates = await fetchRatesForDate(date);
const rate = rates[currency];
if (!rate) throw new Error(`Neznámá měna: ${currency}`);
return rate;
}