Bu makale, B2B bayi yonetim sistemi mimarisinin 4. asamasini ele almaktadir: Coklu Para Birimi (Dovizli Cari), Otomatik Merkez Bankasi (TCMB) Kur Entegrasyonu ve Kur Korumali Siparis Yonetimi. Asama 1 (temel panel), Asama 2 (stok & sepet) ve Asama 3 (e-Tahsilat & POS) analizlerinin devamidir. Bu asamada, kurlari ilk bakista basariyla ceviren ama yuksek kur oynakligi veya TCMB API kesintisi aninda sirketi buyuk zarara ugratacak 10 adet senior seviye hata analiz edilmektedir.
Senaryo: Urunler USD bazli, bayi carisi EUR veya TL bazli. Siparis aninda anlik doviz kuru Merkez Bankasi'ndan cekiliyor, kur sabitleniyor ve faturalandirma kuyruguna aktariliyor. B2B platformlarinda (ozellikle ithalat-ihracat veya yedek parca gibi kuresel sektorlerde) fiyatlar genellikle USD/EUR bazlidir ancak tahsilat TL veya farkli bir dovizle yapilabilir. Sistemlerin coktu anlar: kur dalgalanmasi sirasinda sepet onaylandiginda kur sabitleme hatasi, coklu para birimli carilerde kur donusum yuvarlamalari ve TCMB API'si coktugunde siparis akisinin durmasi.
| Kategori | Hata | Hata Numaralari | Risk |
|---|---|---|---|
| Guvenlik | 3 | #1 (Client FX), #5 (Trusting Client), #3 (Slippage) | Kritik |
| Finansal Tutarlilik | 5 | #3 (Slippage), #4 (Float), #8 (Spread), #9 (Wallet Race), #10 (Slippage Tol.) | Kritik |
| Performans & Hata Y. | 2 | #6 (TCMB SPOF), #7 (XML Blokaj) | Yuksek |
Doviz cevrimi dogrudan arayuzde yapiliyor. Bayi tarayici dev tools ile kurlari manipule edip (orn. dolar kurunu 1 TL yaparak) backend'e manipule edilmis tutar gonderebilir. Backend hicbir kur dogrulamasi yapmiyor.
Hizli para birimi degisimlerinde (TRY->EUR->USD) eski API yanitinin gelmesi yeni yanitin uzerine yazabilir. Kullanici EUR secti ama once USD yaniti gelir ve yanlis tutar gosterilir. AbortController veya request ID takibi yok.
Sepetteki fiyat backend'e gonderilirken kurun ne zaman alindigi bilgisi (timestamp) gonderilmiyor. Kullanici sepeti 3 saat acik birakip kur firladiktan sonra eski ucuz kurla siparis gecebilir. Kur sabitleme penceresi (time-window) yok.
Doviz cevriminde float veri tipi kullaniliyor: 5000.0 * 34.5542 = 172771.00000000003. Binlerce islemde bu kurus sapmalari birikerek muhasebe krizine donusur. Ayrica kur carpiminda floating point representation hatasi vardir.
Istemciden gelen cevrilmis tutar dogrudan kabul ediliyor. Backend, tutarin kendi USD baz fiyat ve guncel kur uzerinden yeniden hesaplanip dogrulanmasini yapmiyor. Bu, Aşama 2'deki 'frontend'den fiyat alma' hatasinin doviz versiyonudur.
TCMB API'si veya XML servisi coktugunde ya da yavasladiginda sistem hata firlatir ve tum B2B siparis akisi durur. Yedekli kur saglayici (Fallback FX Provider) veya DB/Redis tabanli son kararli kur (Stale Cache) mekanizmasi yok.
Buyuk TCMB XML verisini dogrudan asenkron akista islemek CPU kilitleyebilir. ET.fromstring() senkron bir CPU-bound islemdir ve event loop'u bloke eder. TCMB XML'i 50+ kur icerir ve her istekte yeniden ayristirilir.
TCMB ham kuru uzerinden dogrudan islem yapiliyor. Banka ve serbest piyasa makas araligi (spread) hesaba katilmiyor. TCMB kuru genellikle serbest piyasadan %1-3 daha dusuktur. Sirket bankadan doviz alirken kur makasindan zarar eder.
Bayinin carisinde ilgili para birimi cuzdan kontrol edilmeden ve kilitlenmeden dogrudan ekleme/cikarma yapiliyor. Iki eszamanli doviz islemi ayni cuzdani ayni anda degistirebilir. Redis lock veya DB transaction yok.
Istek basladigi andaki kur ile veritabanina yazildigi andaki kur arasinda buyuk bir dalgalanma (slippage) olursa, bir ust limit (max tolerans %0.5 gibi) kontrolu olmadigi icin islem zararla tamamlanabilir. Kur sabitleme ile gerceklesme kuru arasinda fark olabilir.
Asagidaki duzeltilmis React kodu, frontend hatalarini (#1, #2, #3) cozmektedir:
import React, { useState, useCallback, useRef, useEffect } from 'react';
// DÜZELTME 2: AbortController ile onceki istekleri iptal et
function useDebounce(value, delay = 500) {
const [debounced, setDebounced] = useState(value);
useEffect(() => {
const t = setTimeout(() => setDebounced(value), delay);
return () => clearTimeout(t);
}, [value, delay]);
return debounced;
}
export default function CurrencySelectorCart({ usdTotal, dealerId }) {
const [selectedCurrency, setSelectedCurrency] = useState('TRY');
const [displayTotal, setDisplayTotal] = useState(null);
const [fxLockId, setFxLockId] = useState(null); // DÜZELTME 3: Kur lock ID
const [submitting, setSubmitting] = useState(false);
const [error, setError] = useState(null);
const abortRef = useRef(null);
const debouncedCurrency = useDebounce(selectedCurrency, 500);
// DÜZELTME 1+2: Display amacli kur — backend hesaplamayi yapar
useEffect(() => {
if (abortRef.current) abortRef.current.abort();
const controller = new AbortController();
abortRef.current = controller;
fetch(`/api/v4/fx/display-rate?from=USD&to=${debouncedCurrency}`, {
signal: controller.signal,
credentials: 'include'
})
.then(res => res.json())
.then(data => {
// Sadece gosterim — backend siparis aninda kendi hesaplar
setDisplayTotal({
amount: data.display_amount,
rate: data.rate,
rate_source: data.source // tcmb / fallback / stale
});
})
.catch(err => {
if (err.name !== 'AbortError') setError('Kur alinamadi');
});
return () => controller.abort();
}, [debouncedCurrency]);
// DÜZELTME 3: Kur sabitleme backend'de yapilir
const lockFXRate = useCallback(async () => {
const res = await fetch('/api/v4/fx/lock', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Idempotency-Key': crypto.randomUUID()
},
credentials: 'include',
body: JSON.stringify({
from_currency: 'USD',
to_currency: selectedCurrency,
base_amount: usdTotal // USD baz fiyat
})
});
if (!res.ok) throw new Error('Kur sabitlenemedi');
const data = await res.json();
setFxLockId(data.lock_id);
return data;
}, [selectedCurrency, usdTotal]);
const submitFXOrder = useCallback(async () => {
if (submitting) return;
setSubmitting(true);
setError(null);
try {
// DÜZELTME 3: Once kuru backend'de sabitle
const lockData = await lockFXRate();
// DÜZELTME 5: Frontend tutar GONDERILMIYOR — sadece lock_id
const response = await fetch('/api/v4/orders/fx-submit', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Idempotency-Key': crypto.randomUUID()
},
credentials: 'include',
body: JSON.stringify({
fx_lock_id: lockData.lock_id, // Sadece lock ID
dealer_id: dealerId,
base_usd_amount: usdTotal
// amount GONDERILMIYOR! Backend hesaplar.
})
});
const result = await response.json();
if (!response.ok) throw new Error(result.detail);
alert(`Siparis alindi! Tutar: ${result.final_amount} ${selectedCurrency}`);
} catch (err) {
setError(err.message);
} finally {
setSubmitting(false);
}
}, [submitting, lockFXRate, selectedCurrency, dealerId, usdTotal]);
return (
{/* DÜZELTME 1: Display only — backend hesaplar */}
{displayTotal && (
Gosterim Tutari: {displayTotal.amount} {selectedCurrency}
Kur: {displayTotal.rate} (Kaynak: {displayTotal.rate_source})
)}
{error && {error}}
{/* DÜZELTME 3: Kur 5 dakika icinde sabitlenir, TTL dolduysa yenilenir */}
{fxLockId && Kur 5 dakika icin sabitlendi. Lock ID: {fxLockId}
}
);
}/api/v4/fx/lock ile kur 5 dakika TTL ile sabitlenir, sadece lock_id gonderiliramount gonderilmiyor — sadece fx_lock_id ve base_usd_amountAsagidaki duzeltilmis FastAPI kodu, backend hatalarini (#4, #5, #6, #7, #8, #9, #10) cozmektedir:
import asyncio
import json
import uuid
from datetime import datetime, timedelta
from decimal import Decimal, ROUND_HALF_UP # DÜZELTME 4: Decimal
from fastapi import FastAPI, HTTPException, Header, Depends
from pydantic import BaseModel
import httpx
import xml.etree.ElementTree as ET
import redis.asyncio as aioredis
app = FastAPI()
redis_client = aioredyis.from_url("redis://localhost:6379/0", decode_responses=True)
# DÜZELTME 8: Kur marji (spread) konfigurasyonu
SPREAD_CONFIG = {
"VIP": Decimal("0.005"), # %0.5
"STANDARD": Decimal("0.02"), # %2
"NEW": Decimal("0.03"), # %3
}
MAX_SLIPPAGE = Decimal("0.005") # DÜZELTME 10: %0.5 max slippage
FX_LOCK_TTL = 300 # 5 dakika
class FXOrderPayload(BaseModel):
fx_lock_id: str # DÜZELTME 3: Lock ID, ham tutar degil
dealer_id: str
base_usd_amount: Decimal # DÜZELTME 4: Decimal
# DÜZELTME 6: Cok katmanli kur saglayici
async def get_exchange_rate(from_cur: str, to_cur: str) -> dict:
# 1. Redis cache kontrol
cache_key = f"fx_rate:{from_cur}:{to_cur}"
cached = await redis_client.get(cache_key)
if cached:
data = json.loads(cached)
return {**data, "source": "redis_cache"}
# 2. TCMB API (primary) — circuit breaker ile
try:
rate = await fetch_from_tcmb(from_cur, to_cur)
if rate:
await redis_client.setex(cache_key, 3600, json.dumps({
"rate": str(rate), "fetched_at": datetime.utcnow().isoformat()
}))
return {"rate": rate, "source": "tcmb"}
except Exception as e:
app.logger.warning(f"TCMB failed: {e}")
# 3. Fallback API (Fixer.io vb.)
try:
rate = await fetch_from_fallback(from_cur, to_cur)
if rate:
await redis_client.setex(cache_key, 3600, json.dumps({
"rate": str(rate), "fetched_at": datetime.utcnow().isoformat()
}))
return {"rate": rate, "source": "fallback"}
except Exception as e:
app.logger.warning(f"Fallback FX failed: {e}")
# 4. Stale cache (son basarili kur — risk kabul edilmis)
stale = await redis_client.get(f"fx_stale:{from_cur}:{to_cur}")
if stale:
app.logger.critical(f"Using STALE rate for {from_cur}->{to_cur}")
return {"rate": Decimal(stale), "source": "stale_cache"}
raise HTTPException(503, "Kur saglayici hicbiri yanit vermiyor")
# DÜZELTME 7: XML ayristirma thread pool'da
async def fetch_from_tcmb(from_cur: str, to_cur: str) -> Decimal:
async with httpx.AsyncClient(timeout=10.0) as client:
response = await client.get("https://www.tcmb.gov.tr/kurlar/today.xml")
response.raise_for_status()
# DÜZELTME 7: CPU-bound XML ayristirma thread pool'a tasinir
root = await asyncio.to_thread(ET.fromstring, response.content)
# Kur bul
for currency in root.findall("Currency"):
code = currency.find("CurrencyCode").text
if code == from_cur:
rate = Decimal(currency.find("BanknoteSelling").text or "0")
return rate
return None
# DÜZELTME 3: Kur sabitleme endpoint'i
@app.post("/api/v4/fx/lock")
async def lock_fx_rate(
payload: dict,
dealer_id: str = Depends(get_current_dealer)
):
from_cur = payload["from_currency"]
to_cur = payload["to_currency"]
base_amount = Decimal(str(payload["base_amount"]))
# Guncel kuru al
rate_data = await get_exchange_rate(from_cur, to_cur)
rate = rate_data["rate"]
# DÜZELTME 8: Spread ekle
dealer_segment = await get_dealer_segment(dealer_id)
spread = SPREAD_CONFIG.get(dealer_segment, Decimal("0.02"))
effective_rate = (rate * (Decimal("1") + spread)).quantize(
Decimal("0.0001"), rounding=ROUND_HALF_UP
)
# Lock olustur
lock_id = str(uuid.uuid4())
lock_data = {
"lock_id": lock_id,
"from_currency": from_cur,
"to_currency": to_cur,
"base_amount": str(base_amount),
"locked_rate": str(effective_rate),
"locked_at": datetime.utcnow().isoformat(),
"expires_at": (datetime.utcnow() + timedelta(seconds=FX_LOCK_TTL)).isoformat(),
"source": rate_data["source"]
}
# DÜZELTME 3: Redis'te 5 dakika TTL ile sakla
await redis_client.setex(
f"fx_lock:{lock_id}",
FX_LOCK_TTL,
json.dumps(lock_data)
)
return {
"lock_id": lock_id,
"locked_rate": str(effective_rate),
"locked_amount": str((base_amount * effective_rate).quantize(
Decimal("0.01"), rounding=ROUND_HALF_UP
)),
"expires_in_seconds": FX_LOCK_TTL,
"source": rate_data["source"]
}
@app.post("/api/v4/orders/fx-submit")
async def process_fx_order(
payload: FXOrderPayload,
x_idempotency_key: str = Header(...),
dealer_id: str = Depends(get_current_dealer)
):
# DÜZELTME 5: Idempotency kontrol
idem_key = f"fx_order_idem:{x_idempotency_key}"
existing = await redis_client.get(idem_key)
if existing:
return {"status": "duplicate", "order_id": existing}
# DÜZELTME 3: Lock ID'den kur bilgisini al
lock_data = await redis_client.get(f"fx_lock:{payload.fx_lock_id}")
if not lock_data:
raise HTTPException(409, "Kur suresi dolmus. Lutfen kuru yeniden sabitleyin.")
lock = json.loads(lock_data)
locked_rate = Decimal(lock["locked_rate"])
locked_at = datetime.fromisoformat(lock["locked_at"])
base_amount = payload.base_usd_amount # DÜZELTME 4: Decimal
# DÜZELTME 5: Backend kendi hesaplar — frontend tutarini KULLANMA
calculated_amount = (base_amount * locked_rate).quantize(
Decimal("0.01"), rounding=ROUND_HALF_UP
)
# DÜZELTME 10: Slippage kontrol
current_rate_data = await get_exchange_rate(
lock["from_currency"], lock["to_currency"]
)
current_rate = current_rate_data["rate"]
slippage = abs(current_rate - locked_rate) / locked_rate
if slippage > MAX_SLIPPAGE:
raise HTTPException(
409,
f"Kur degisimi tolerans asti: "
f"Locked={locked_rate}, Current={current_rate}, "
f"Slippage={slippage*100}%, Max={MAX_SLIPPAGE*100}%"
)
# DÜZELTME 9: Wallet lock — race condition korumasi
wallet_lock_key = f"wallet_lock:{dealer_id}:{lock['to_currency']}"
wallet_lock_id = str(uuid.uuid4())
acquired = await redis_client.set(wallet_lock_key, wallet_lock_id, nx=True, ex=30)
if not acquired:
raise HTTPException(409, "Cuzdan isleniyor, bekleyin")
try:
# DB transaction — SELECT FOR UPDATE
async with db.begin():
wallet = await db.execute(
"SELECT balance FROM wallets WHERE dealer_id = :did AND currency = :cur FOR UPDATE",
{"did": dealer_id, "cur": lock["to_currency"]}
)
wallet_row = wallet.fetchone()
if not wallet_row:
# Cuzdan yoksa olustur
await db.execute(
"INSERT INTO wallets (dealer_id, currency, balance) VALUES (:did, :cur, 0)",
{"did": dealer_id, "cur": lock["to_currency"]}
)
current_balance = Decimal("0")
else:
current_balance = Decimal(str(wallet_row.balance))
# Bakiye kontrolu — yeterli mi?
if current_balance < calculated_amount:
raise HTTPException(
402,
f"Yetersiz bakiye: {current_balance} {lock['to_currency']}, "
f"Gerekli: {calculated_amount}"
)
# Atomic bakiye dusumu
await db.execute(
"UPDATE wallets SET balance = balance - :amt WHERE dealer_id = :did AND currency = :cur",
{"amt": str(calculated_amount), "did": dealer_id, "cur": lock["to_currency"]}
)
# Siparis olustur
order = await create_fx_order(
db, dealer_id, base_amount, calculated_amount,
locked_rate, lock["from_currency"], lock["to_currency"],
lock["source"], str(slippage)
)
# Lock'u sil
await redis_client.delete(f"fx_lock:{payload.fx_lock_id}")
# Idempotency kaydet
await redis_client.setex(idem_key, 86400, str(order.id))
return {
"status": "confirmed",
"order_id": order.id,
"final_amount": str(calculated_amount),
"currency": lock["to_currency"],
"locked_rate": str(locked_rate),
"current_rate": str(current_rate),
"slippage_pct": str(slippage * 100),
"rate_source": lock["source"]
}
finally:
# DÜZELTME 9: Wallet lock release
current = await redis_client.get(wallet_lock_key)
if current == wallet_lock_id:
await redis_client.delete(wallet_lock_key)Decimal sinifi — tum kur carpimlarinda finansal hassasiyetasyncio.to_thread(ET.fromstring, ...) ile XML ayristirma thread pool'a tasinir + Redis JSON cacheSPREAD_CONFIG — bayi segmentine gore kur marji (VIP %0.5, Standard %2, New %3)wallet_lock:dealer:currency — Redis SET NX + SELECT FOR UPDATE ile dovizli cari race condition korumasiMAX_SLIPPAGE = %0.5 — locked_rate ile current_rate arasinda %0.5'ten buyuk fark varsa 409 Conflict/api/v4/fx/lock — kur 5 dakika TTL ile Redis'te sabitlenir, slippage kontrol ile dogrulanirAsama 4 — Coklu Para Birimi, TCMB Kur Entegrasyonu ve Kur Korumali Siparis Yonetimi modulundeki 10 kritik hata, bir B2B e-ticaret sisteminin doviz/finansal en hassas noktalarini temsil etmektedir. Istemci tarafi doviz cevrimi ve guvenilmeyen istemci verisi guvenlik acisindan; float kullanimi, spread ihmali ve slippage tolerans eksikligi finansal tutarlilik acisindan en kritik sorunlardir. TCMB SPOF ise tum siparis akisini durdurabilen bir sistem krizidir.
Sunulan production-ready kod mimarisi — display-only frontend kur, backend FX lock (5dk TTL), 4 katmanli kur saglayici (TCMB -> Fallback -> Stale -> Daily), Decimal finansal hesaplama, spread marji (VIP/Standard/New), asyncio.to_thread XML ayristirma, wallet lock (Redis + SELECT FOR UPDATE), slippage tolerans (%0.5 max) ve idempotency key — bu sorunlarin tamamini cozmektedir. Dort asamanin (temel panel, stok/sepet, e-tahsilat, dovizli cari) birlesimi, enterprise-ready bir B2B bayi yonetim sistemi mimarisi olusturmaktadir.