/* ========================================================= SYR CONVERTER 5 CURRENCIES KWD USD EUR SAR AED NO FIXED EXCHANGE RATES. ========================================================= */ const FRESH_CACHE_SECONDS = 300; const BACKUP_CACHE_SECONDS = 86400; /* * If LiraScope has not published a new * market rate for 4 hours, * display MARKET CLOSED. */ const MARKET_INACTIVE_MINUTES = 240; const REQUIRED_CURRENCIES = [ "USD", "EUR", "KWD", "SAR", "AED" ]; const LATEST_API = "https://lirascope.syria-cloud.sy/api/v1/rates/latest?currencies=USD,EUR,KWD,SAR,AED&lang=en"; const USD_BASED_API = "https://lirascope.syria-cloud.sy/api/v1/rates/usd-based?lang=en"; export async function onRequestGet( context ) { const { request, waitUntil } = context; const cache = caches.default; const origin = new URL( request.url ).origin; const freshKey = new Request( `${origin}/__syr_market_api_v11` ); const backupKey = new Request( `${origin}/__syr_market_backup_v11` ); /* ===================================================== 1. CLOUDFLARE SERVER CACHE ====================================================== */ const cached = await cache.match( freshKey ); if ( cached ) { const data = await cached.json(); return clientResponse({ ...data, serverCache: true }); } /* ===================================================== 2. GET LIVE SYRIAN MARKET DATA ====================================================== */ try { const latestResponse = await fetch( `${LATEST_API}&_=${Date.now()}`, { method: "GET", headers: { Accept: "application/json" } } ); if ( !latestResponse.ok ) { throw new Error( `LiraScope latest HTTP ${latestResponse.status}` ); } const latest = await latestResponse.json(); if ( !Array.isArray( latest.marketRates ) ) { throw new Error( "marketRates missing" ); } const rates = {}; /* ===================================================== DIRECT SYP MARKET RATES If LiraScope gives the currency directly, always use it. ====================================================== */ for ( const code of REQUIRED_CURRENCIES ) { const item = latest.marketRates.find( rate => String( rate.currency ) .toUpperCase() === code ); if ( !item ) { continue; } const buy = Number( item.buy ); const sell = Number( item.sell ); const mid = Number( item.mid ); if ( Number.isFinite(buy) && buy > 0 && Number.isFinite(sell) && sell > 0 ) { rates[ code ] = { buy: buy, sell: sell, mid: Number.isFinite(mid) && mid > 0 ? mid : ( buy + sell ) / 2, updated: item.timestampUtc || latest.timestampUtc || null }; } } /* ===================================================== USD IS REQUIRED AS OUR CROSS-RATE BASE ====================================================== */ if ( !rates.USD ) { throw new Error( "USD Syrian market rate unavailable" ); } /* ===================================================== FIND ANY MISSING CURRENCIES ====================================================== */ const missingCurrencies = REQUIRED_CURRENCIES.filter( code => !rates[ code ] ); /* ===================================================== USE LIVE USD-BASED FX FOR MISSING CURRENCIES Example: 1 USD = X SAR 1 USD = Y AED 1 USD = Z KWD combined with: 1 USD = N SYP gives SYP value dynamically. ====================================================== */ if ( missingCurrencies.length > 0 ) { const usdBasedResponse = await fetch( `${USD_BASED_API}&_=${Date.now()}`, { method: "GET", headers: { Accept: "application/json" } } ); if ( !usdBasedResponse.ok ) { throw new Error( `USD based API HTTP ${usdBasedResponse.status}` ); } const usdBased = await usdBasedResponse.json(); if ( !Array.isArray( usdBased.rates ) ) { throw new Error( "USD based rates missing" ); } for ( const code of missingCurrencies ) { /* * USD is already mandatory direct, * so no reason to derive USD. */ if ( code === "USD" ) { continue; } const reference = usdBased.rates.find( item => String( item.currency ) .toUpperCase() === code ); if ( !reference ) { throw new Error( `${code} USD reference unavailable` ); } const foreignBuy = Number( reference.buy ); const foreignSell = Number( reference.sell ); const foreignMid = Number( reference.mid ); if ( !Number.isFinite( foreignBuy ) || foreignBuy <= 0 || !Number.isFinite( foreignSell ) || foreignSell <= 0 ) { throw new Error( `Invalid ${code} USD reference` ); } /* * --------------------------------------------------- * Cross-rate * --------------------------------------------------- * * LiraScope usd-based: * * 1 USD = X foreign currency * * Syrian market: * * 1 USD = Y SYP * * Therefore: * * SYP per foreign currency * * Y / X * --------------------------------------------------- */ const buy = rates.USD.buy / foreignSell; const sell = rates.USD.sell / foreignBuy; const mid = Number.isFinite( foreignMid ) && foreignMid > 0 ? rates.USD.mid / foreignMid : ( buy + sell ) / 2; rates[ code ] = { buy: buy, sell: sell, mid: mid, updated: usdBased.timestampUtc || rates.USD.updated || latest.timestampUtc || null }; } } /* ===================================================== FINAL VALIDATION — ALL 5 REQUIRED ====================================================== */ for ( const code of REQUIRED_CURRENCIES ) { const rate = rates[ code ]; if ( !rate ) { throw new Error( `${code} final rate missing` ); } if ( !Number.isFinite( Number( rate.buy ) ) || Number( rate.buy ) <= 0 || !Number.isFinite( Number( rate.sell ) ) || Number( rate.sell ) <= 0 ) { throw new Error( `${code} final rate invalid` ); } } /* ===================================================== FIND LATEST MARKET TIMESTAMP ====================================================== */ const timestamps = Object.values( rates ) .map( item => new Date( item.updated ) .getTime() ) .filter( Number.isFinite ); const latestMarketTimestamp = timestamps.length ? Math.max( ...timestamps ) : Date.now(); const updated = new Date( latestMarketTimestamp ); const ageMinutes = Math.max( 0, ( Date.now() - updated.getTime() ) / 60000 ); /* ===================================================== MARKET STATUS ====================================================== */ const marketStatus = ageMinutes >= MARKET_INACTIVE_MINUTES ? "closed" : "open"; /* ===================================================== FINAL RESPONSE ====================================================== */ const payload = { success: true, source: "LiraScope Market API", stale: false, marketStatus: marketStatus, updated: updated.toISOString(), ageMinutes: Math.round( ageMinutes ), fetchedAt: new Date() .toISOString(), currencies: [ "KWD", "USD", "EUR", "SAR", "AED" ], rates: rates }; /* ===================================================== SAVE 5 MINUTE CACHE ====================================================== */ waitUntil( cache.put( freshKey, internalResponse( payload, FRESH_CACHE_SECONDS ) ) ); /* ===================================================== SAVE LAST SUCCESSFUL REAL API RESPONSE ====================================================== */ waitUntil( cache.put( backupKey, internalResponse( payload, BACKUP_CACHE_SECONDS ) ) ); return clientResponse( payload ); } catch (error) { console.error( "SYR API error:", error ); /* ===================================================== ONLY USE LAST REAL API RESPONSE NO FIXED RATE. ====================================================== */ const backup = await cache.match( backupKey ); if ( backup ) { const old = await backup.json(); const updated = old.updated ? new Date( old.updated ) : null; const ageMinutes = updated && Number.isFinite( updated.getTime() ) ? ( Date.now() - updated.getTime() ) / 60000 : Infinity; return clientResponse({ ...old, stale: true, marketStatus: ageMinutes >= MARKET_INACTIVE_MINUTES ? "closed" : old.marketStatus || "unknown", source: "Last LiraScope API Rate", error: "Live API temporarily unavailable" }); } /* ===================================================== NO API DATA AT ALL NO RATE IS INVENTED. ====================================================== */ return clientResponse( { success: false, stale: true, marketStatus: "unknown", currencies: [ "KWD", "USD", "EUR", "SAR", "AED" ], rates: null, error: "No valid API rate currently available" }, 503 ); } } /* ========================================================= CLIENT RESPONSE ========================================================= */ function clientResponse( data, status = 200 ) { return new Response( JSON.stringify( data ), { status: status, headers: { "Content-Type": "application/json; charset=utf-8", "Cache-Control": "no-store, no-cache, must-revalidate, max-age=0", "Pragma": "no-cache", "Expires": "0", "Access-Control-Allow-Origin": "*", "X-SYR-API": "v11" } } ); } /* ========================================================= INTERNAL CLOUDFLARE CACHE ========================================================= */ function internalResponse( data, seconds ) { return new Response( JSON.stringify( data ), { headers: { "Content-Type": "application/json; charset=utf-8", "Cache-Control": `public, max-age=${seconds}` } } ); }