# 08 — Ödeme Modülü (PayTR)

## Ne İş Yapar?

Ziyaretçi sepetini onaylayınca PayTR üzerinden ödeme linki oluşturulur. Ziyaretçi ödemeyi tamamlayınca PayTR callback'i gelir, sipariş "ödendi" olarak işaretlenir ve e-Fatura kesimi tetiklenir. Sohbete "Ödemeniz alındı" mesajı düşer.

---

## Akış

```
1. Ziyaretçi "Ödemeye geç" der
2. AI: "₺299 ödeme linki oluşturayım mı?" (onay sorar)
3. Ziyaretçi: "Evet"
4. confirm-tool → CreatePayLinkTool.execute()
5. PayTRService.create_payment_link()
6. PayTR API → link_url döner
7. PaymentLink DB kaydı oluştur
8. Ziyaretçiye link gönderilir (UI: "Ödeme sayfası açılıyor")
9. Ziyaretçi PayTR sayfasında ödeme yapar
10. PayTR → POST /api/webhooks/paytr/callback
11. FastAPI: hash doğrula → Order.paid → Celery: issue_efatura
12. ChatSession'a AI mesajı: "✅ Ödemeniz alındı! Sipariş No: FLV-2026-00001"
```

---

## Veri Modeli

```
payment_links
├── id (UUID)
├── tenant_id
├── session_id (FK → chat_sessions)
├── order_id (FK → flovy_orders, nullable — checkout'ta set edilir)
├── paytr_link_id             ← PayTR'den dönen link ID
├── paytr_link_url            ← Ziyaretçiye gönderilen URL
├── callback_id               ← WDG{tenant_id}T{timestamp} format
├── amount (DECIMAL)
├── currency
├── status (created/paid/expired/cancelled)
├── customer_email
├── expires_at
├── paid_at
├── paytr_merchant_oid        ← Callback'te gelir
├── payment_type              ← Kredi kartı / havale
├── callback_response (JSON)  ← PayTR'den gelen ham veri
└── created_at
```

---

## PayTR Service

```python
class PayTRService:
    BASE_URL = "https://www.paytr.com/odeme/api/link"

    def __init__(self):
        self.merchant_id = settings.PAYTR_MERCHANT_ID
        self.merchant_key = settings.PAYTR_MERCHANT_KEY
        self.merchant_salt = settings.PAYTR_MERCHANT_SALT

    async def create_payment_link(
        self,
        tenant_id: str,
        session_id: str,
        amount: float,
        currency: str,
        email: str,
        description: str,
    ) -> dict:
        name = description[:200]
        price = int(round(amount * 100))
        currency_mapped = self._map_currency(currency)
        max_installment = "1"
        link_type = "collection"
        lang = "tr"
        callback_id = f"WDG{tenant_id[:8].replace('-','')}T{int(time.time())}"

        # HMAC token
        raw = name + str(price) + currency_mapped + max_installment + link_type + lang + email
        token = base64.b64encode(
            hmac.new(self.merchant_key.encode(), (raw + self.merchant_salt).encode(), hashlib.sha256).digest()
        ).decode()

        params = {
            "merchant_id": self.merchant_id,
            "name": name,
            "price": price,
            "currency": currency_mapped,
            "max_installment": max_installment,
            "link_type": link_type,
            "lang": lang,
            "email": email,
            "paytr_token": token,
            "callback_link": f"{settings.APP_BASE_URL}/api/webhooks/paytr/callback",
            "callback_id": callback_id,
            "expiry_date": (datetime.utcnow() + timedelta(days=30)).strftime("%Y-%m-%d %H:%M:%S"),
        }

        async with httpx.AsyncClient() as client:
            resp = await client.post(f"{self.BASE_URL}/create", data=params, timeout=30.0)

        result = resp.json()
        if result.get("status") == "success":
            return {
                "success": True,
                "link_id": result["id"],
                "link_url": result["link"],
                "callback_id": callback_id,
            }

        return {"success": False, "message": result.get("err_msg", "PayTR hatası")}

    def verify_callback_hash(self, data: dict, channel: str = "link") -> bool:
        merchant_oid = data.get("merchant_oid", "")
        status = data.get("status", "")
        total_amount = data.get("total_amount", "")
        received_hash = data.get("hash", "")
        callback_id = data.get("callback_id", "")

        if channel in ("link", "auto") and callback_id:
            raw = callback_id + merchant_oid + self.merchant_salt + status + total_amount
            expected = base64.b64encode(
                hmac.new(self.merchant_key.encode(), raw.encode(), hashlib.sha256).digest()
            ).decode()
            if hmac.compare_digest(expected, received_hash):
                return True

        return False

    def _map_currency(self, currency: str) -> str:
        return {"TRY": "TL", "USD": "USD", "EUR": "EUR"}.get(currency, "TL")
```

---

## PayTR Callback Handler

```python
@router.post("/api/webhooks/paytr/callback")
async def paytr_callback(request: Request, db: AsyncSession = Depends(get_db)):
    data = dict(await request.form())

    callback_id = data.get("callback_id", "")
    merchant_oid = data.get("merchant_oid", "")
    status = data.get("status", "")

    # Hash doğrula
    paytr = PayTRService()
    if not paytr.verify_callback_hash(data, channel="link"):
        logger.error(f"PayTR hash mismatch: callback_id={callback_id}")
        return PlainTextResponse("HASH MISMATCH", status_code=403)

    if status != "success":
        return PlainTextResponse("OK")

    # PaymentLink bul
    link = await PaymentLinkRepo.get_by_callback_id(callback_id, db)
    if not link:
        logger.error(f"PaymentLink bulunamadı: {callback_id}")
        return PlainTextResponse("OK")

    # Idempotent — lock ile
    async with db.begin():
        link = await db.execute(
            select(PaymentLink).where(PaymentLink.id == link.id).with_for_update()
        )
        link = link.scalar_one()

        if link.status == "paid":
            return PlainTextResponse("OK")

        total_paid = Decimal(data.get("total_amount", "0")) / 100

        link.status = "paid"
        link.paid_at = datetime.utcnow()
        link.paytr_merchant_oid = merchant_oid
        link.payment_type = data.get("payment_type")
        link.callback_response = data

        # Order güncelle
        if link.order_id:
            await OrderRepo.mark_paid(link.order_id, total_paid, db)

    # e-Fatura Celery'ye devret (transaction dışında)
    if link.order_id:
        issue_efatura.delay(str(link.order_id))

    # Sohbete "ödeme alındı" mesajı
    if link.session_id:
        asyncio.create_task(
            notify_payment_to_chat(link.session_id, link.order_id)
        )

    return PlainTextResponse("OK")  # PayTR her zaman "OK" bekler
```

---

## İş Kuralları

- Callback her zaman `"OK"` döner — aksi halde PayTR retry yapar, mükerrer işlem riski.
- Hash doğrulaması başarısız → `"HASH MISMATCH"` + 403 dön, loglama yap.
- `PaymentLink` kaydı olmadan ziyaretçiye URL verilmez — callback eşleşemaz.
- Idempotency: `status="paid"` ise tekrar işleme alma.
- `for_update()` lock: Aynı callback iki kez gelirse sadece biri işleme girer.
- e-Fatura ve sohbet bildirimi transaction dışında — fatura kesilmemesi ödemeyi etkilemez.
