Metadata-Version: 2.4
Name: paytr-python
Version: 0.3.0
Summary: Async Python client for the PayTR payment APIs (iFrame, callback, refund, reporting, links, recurring).
License: Custom (MIT with Attribution Requirements)
Project-URL: Homepage, https://dev.paytr.com/en
Keywords: paytr,payment,iframe,async
Requires-Python: >=3.10
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: aiohttp>=3.9
Dynamic: license-file

# PayTR Python Client

[![PyPI Version](https://img.shields.io/pypi/v/paytr-python.svg)](https://pypi.org/project/paytr-python/)
[![Python Versions](https://img.shields.io/pypi/pyversions/paytr-python.svg)](https://pypi.org/project/paytr-python/)
[![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT)

[PayTR](https://dev.paytr.com/) API entegrasyonu için **hafif**, **tamamen asenkron** ve **framework bağımsız** bir Python kütüphanesi. Tek bağımlılığı `aiohttp`'dir; FastAPI, Django, Flask veya düz script'lerle aynı şekilde çalışır.

<!-- MARK: Desteklenen Özellikler -->
### Desteklenen Özellikler

| Özellik | PayTR Endpoint'i | İstemci Metodu |
| :--- | :--- | :--- |
| **iFrame Token** *(1. Adım — yeni tasarım v2, Kart & Havale/EFT)* | `/odeme/api/get-token` | `create_iframe_token()` |
| **Bildirim (Callback)** *(2. Adım — iFrame, EFT, Direct, Link)* | *Sizin Bildirim URL'niz* | `handle_callback()` / `parse_callback()` |
| **İade** *(Tam veya Kısmi)* | `/odeme/iade` | `refund()` |
| **Durum Sorgulama** | `/odeme/durum-sorgu` | `status()` |
| **İşlem Dökümü / Ödeme Özeti / Ödeme Detayı** | `/rapor/...` | `transaction_detail()` / `payment_statement()` / `payment_detail()` |
| **Ödeme Linki** *(Oluşturma & Silme)* | `/odeme/api/link/...` | `create_payment_link()` / `delete_payment_link()` |
| **BIN & Taksit Oranları** | `/odeme/api/bin-detail`, `/odeme/taksit-oranlari` | `bin_detail()` / `installment_rates()` |
| **Kayıtlı Kartlar** *(Listeleme & Silme)* | `/odeme/capi/...` | `list_cards()` / `delete_card()` |
| **Tekrarlayan Ödeme** *(Kayıtlı karttan, Non3D)* | `/odeme` | `recurring_payment()` |

Bildirim dışındaki tüm işlemler yetki gerektirir: bunları yalnızca kendi yetkilendirilmiş backend kodunuzdan çağırın, asla doğrudan dışarıya açmayın.

> [!NOTE]
> **Kapsam dışı:** Ön Provizyon (dökümanı herkese açık değil), Direct API ile ilk kart ödemesi (kart bilgileri sunucunuzdan geçmemeli; form tarayıcıdan doğrudan PayTR'ye gönderilir), pazaryeri platform transferi.

<!-- MARK: Kurulum -->
## 📦 Kurulum

```bash
pip install paytr-python
# VEYA
uv add paytr-python
```

`merchant_id`, `merchant_key` ve `merchant_salt` değerleri [PayTR Mağaza Paneli](https://www.paytr.com/magaza) > **Destek & Kurulum > Entegrasyon Bilgileri** sayfasındadır.

> [!WARNING]
> `merchant_key` ve `merchant_salt` gizlidir: frontend'e koymayın, Git'e commit'lemeyin, çevre değişkenlerinde (`.env`) tutun. İstemci bunları public attribute olarak tutmaz, loglara da yazmaz.

<!-- MARK: Kullanım -->
## 🚀 Kullanım

```python
from paytr import PayTRClient, get_client_ip

client = PayTRClient.from_env()  # PAYTR_MERCHANT_ID / _KEY / _SALT (+ _TEST_MODE=1, _DEBUG_ON=1)
# veya: PayTRClient(merchant_id="...", merchant_key="...", merchant_salt="...")
```

### 1. Adım — Ödeme başlatma (kendi yetkilendirilmiş endpoint'inizde)

Tutarı ve sepeti **sunucunuzda**, kendi fiyatlarınızdan hesaplayın; istemciden gelen fiyata asla güvenmeyin.

```python
result = await client.create_iframe_token(
    merchant_oid="SIPARIS123",         # Alfanümerik, en fazla 64 karakter
    email="alici@example.com",         # En fazla 100 karakter, Türkçe karakter olmadan
    payment_amount="34.56",            # TL (str, float veya Decimal), en az 1.00
    user_ip=get_client_ip(request.headers, request.client.host),
    user_basket=[("Ürün 1", "18.00", 1), ("Ürün 2", "16.56", 1)],  # (isim, birim fiyat, adet)
    merchant_ok_url="https://siteniz.com/basarili",
    merchant_fail_url="https://siteniz.com/hata",
    user_name="Ayşe Yılmaz", user_address="İstanbul", user_phone="05551112233",
    # Opsiyonel: no_installment=True, max_installment=6, timeout_limit=30 (dk),
    # dark_mode=True, payment_type="eft", lang="en", test_mode=True (sadece bu istek için)
)
result["iframe_url"]  # kart veya Havale/EFT için doğru adres
```

Sayfanıza gömmek için `paytr.iframe_html(token)` hazır HTML verir (yeni tasarımın `?v2` script'i ile). iFrame'in `id`'si `paytriframe` olmalıdır: PayTR'nin script'i bu id'yi sabit kullanır.

### 2. Adım — Bildirim (callback)

PayTR sonucu panelde tanımladığınız **Bildirim URL**'ye POST eder. `handle_callback` imzayı doğrular, işleyicinizi çalıştırır ve PayTR'ye dönmeniz gereken yanıtı verir — herhangi bir framework'te:

```python
from paytr import Callback

async def on_payment(data: Callback) -> bool | None:
    # Yalnızca imzası doğrulanmış bildirimler gelir. PayTR aynı bildirimi tekrar
    # gönderebilir: her merchant_oid'i yalnızca BİR KEZ işleyin (idempotent).
    if data.is_test and not expecting_test_orders:
        return  # test ödemesi gerçek siparişi onaylamasın
    if data.is_success:
        ...  # data.paid_minor_units'i siparişin beklenen tutarıyla (kuruş) karşılaştırın
    # False dönmek (veya exception) PayTR'nin bildirimi tekrar göndermesini sağlar.

# FastAPI
@app.post("/paytr/callback")
async def paytr_callback(request: Request):
    body, status = await client.handle_callback(await request.form(), on_payment)
    return PlainTextResponse(body, status_code=status)
```

| Durum | Yanıt |
| :--- | :--- |
| Geçersiz / eksik imza | `400` — `on_payment` çağrılmaz |
| `on_payment` exception fırlatır veya `False` döner | `500` — PayTR ~1 dk sonra tekrar dener |
| Diğer | `200 OK` — PayTR durur |

Kendi akışınızı kurmak isterseniz `client.parse_callback(form)` doğrulanmış bir `Callback` döner (geçersizse `PayTRSignatureError`). `Callback` alanları: `merchant_oid`, `status`, `total_amount` (kuruş), `is_success`, `is_test`, `paid_minor_units`, `failed_reason_code`, `error_message` ve diğer tüm alanlar için `raw`.

> [!IMPORTANT]
> İmza yalnızca `merchant_oid`, `status` ve `total_amount`'u kapsar. `test_mode`, `currency`, `payment_amount` gibi alanlar imzalı **değildir** — siparişin test mi gerçek mi olduğunu kendi kaydınızdan doğrulayın. Taksitli ödemede `total_amount`, `payment_amount`'tan büyük olabilir.

> [!NOTE]
> **user_ip ve IPv6:** PayTR, iFrame'i açan alıcının IP'sini `user_ip` ile karşılaştırır. Forum kayıtlarına göre PayTR IPv6 desteklemiyor; IPv6 bir `user_ip` gönderildiğinde kütüphane uyarı loglar. Cloudflare arkasındaysanız ödeme alt alan adını yalnızca IPv4 sunacak şekilde ayarlamak bu sorunu çözer.

### Diğer Backend Metotları

```python
# İade (TL). Her iadeye bir reference_no atanır; ağ hatasında iadenin PayTR'de
# kaydedilip kaydedilmediği status() ile kontrol edilir — körlemesine tekrar
# deneyip çift iade yapılmaz.
await client.refund(merchant_oid="SIPARIS123", return_amount="11.90")

await client.status("SIPARIS123")                      # tutar, iadeler (returns), kart bilgisi…

# Raporlar PayTR'nin ham yanıtını döner (veri yoksa {}).
await client.transaction_detail(start_date="2026-06-01 00:00:00", end_date="2026-06-03 23:59:59")  # en fazla 3 gün; dummy=True örnek veri
await client.payment_statement(start_date="2026-06-01", end_date="2026-06-30")
await client.payment_detail("2026-06-05")

link = await client.create_payment_link(name="Özel Tişört", price=14.45, min_count=1)
await client.delete_payment_link(link["id"])
# callback_link verirseniz callback_id zorunludur; bildirimi handle_callback doğrular.

await client.bin_detail("435508")
await client.installment_rates("req123")

cards = await client.list_cards("kullanici_tokeni")  # Non3D yetkisi gerekir
await client.recurring_payment(
    utoken="kullanici_tokeni", ctoken=cards[0]["ctoken"],
    merchant_oid="SIPARIS124", email="alici@example.com", payment_amount="34.56",
    user_ip="1.2.3.4", user_name="Ayşe", user_address="İstanbul", user_phone="0555...",
    user_basket=[("Ürün 1", "34.56", 1)],
    merchant_ok_url="https://siteniz.com/ok", merchant_fail_url="https://siteniz.com/fail",
)
```

> [!TIP]
> **Tutarlar:** Tüm metotlar tutarı TL olarak alır (`"34.56"`, `34.56`, `Decimal`); kütüphane her endpoint'in istediği biçime (kuruş tamsayı veya `"34.56"`) kendisi çevirir. Bildirimlerdeki `total_amount` ise her zaman **kuruştur**.

<!-- MARK: Hata Yönetimi -->
## ❌ Hata Yönetimi

| Exception | Ne zaman |
| :--- | :--- |
| `PayTRConfigError` | Eksik kimlik bilgisi veya PayTR'nin reddedeceği girdi (hatalı `merchant_oid`, e-posta, IP, 1 TL altı tutar, boş sepet) — istek hiç gönderilmez |
| `PayTRNetworkError` | Bağlantı hatası, zaman aşımı, geçersiz yanıt |
| `PayTRSignatureError` | Bildirimde eksik alan veya geçersiz imza |
| `PayTRAPIError` | PayTR hata döndü: `.message`, `.code` (`err_no`), `.payload` (ham yanıt) |

Hepsi `PayTRError`'dan türer. Hata kodlarını açıklamaya çevirmek için: `describe("payment", "10")`, `describe("refund", "009")`.

<!-- MARK: Loglama -->
## 📝 Loglama

Kütüphane varsayılan olarak **sessizdir**. Logları görmek için tek satır yeterli:

```python
import paytr
paytr.enable_logging()          # veya enable_logging("DEBUG")
```

```text
[PAYTR] INFO:     callback oid=SIPARIS123 status=success total=3456 test=False
[PAYTR] WARNING:  callback rejected: bad hash
```

Uygulamanız logging'i zaten kendisi yapılandırıyorsa (`basicConfig`, Sentry vb.) `enable_logging` çağırmayın; kütüphane standart `"paytr"` logger'ına (`paytr.logger`) yazdığı için loglar sizin yapılandırmanıza akar.

| Seviye | Olay |
| :--- | :--- |
| `DEBUG` | Her PayTR isteği (URL) |
| `INFO` | Doğrulanmış bildirim (oid, durum, tutar, test) |
| `WARNING` | Reddedilen bildirim, PayTR API hatası, IPv6 `user_ip`, ağ hatası sonrası doğrulanan iade |
| `ERROR` | Ağ hatası, `on_payment` exception'ı (stack trace ile) |

<!-- MARK: HTTP Oturumu -->
## 🔄 HTTP Oturumu

Varsayılan olarak ilk istekte 30 sn timeout'lu bir `aiohttp.ClientSession` açılır (`timeout=10` ile değiştirilebilir). Kendi oturumunuzu da verebilirsiniz: `PayTRClient(..., session=my_aiohttp_session)` — dışarıdan verilen oturum kütüphane tarafından kapatılmaz. Uygulama kapanırken `await client.aclose()` çağırın veya `async with PayTRClient(...) as client:` kullanın.

<!-- MARK: Demo Uygulama -->
## 🕹️ Demo Uygulama

`src/` altında FastAPI ile küçük bir demo var: `api/payment.py` callback rotası ve yalnızca test modunda çalışan bir `/pay` rotası içerir (tarayıcıdan gelen fiyata güvendiği için canlı modda reddeder).

```bash
uv sync
cp src/example.env .env        # PayTR bilgilerinizi girin, PAYTR_TEST_MODE=1
cd src && uv run main.py       # http://127.0.0.1:8000/paytr/
uv run pytest                  # ağ bağlantısı gerektirmez
```

### 🧪 Test Kartları
`test_mode` açıkken: Visa `4355084355084358`, Mastercard `5406675406675403`, Troy `9792030394440796` — son kullanma `12/30`, CVV `000`. iFrame'de test kartı otomatik doldurulur.
