# 07 — e-Fatura Otomasyonu

## Ne İş Yapar?

Ziyaretçi ödeme yaptıktan sonra Turkcell/iSIM360 API'ye otomatik olarak e-Fatura veya e-Arşiv faturası kesilir. Tenant manuel müdahale etmek zorunda kalmaz.

Panel.tekyerden.co'daki TurkcellEFaturaService mantığının Python'a portudur. Aynı REST/JSON API, aynı `x-api-key` auth.

---

## Akış

```
PayTR Callback (ödeme başarılı)
    │
    ▼
PayTR Webhook Handler
    │
    ├── PaymentLink.status = paid
    ├── Order.status = paid
    │
    ▼
Celery Task: issue_efatura.delay(order_id)
    │
    ▼
TurkcellEFaturaService
    │
    ├── Ziyaretçi GIB mükellefi mi? (efatura_mukellef_cache tablosu)
    │   ├── Evet → e-Fatura (GIB'e gönderilir)
    │   └── Hayır → e-Arşiv
    │
    ├── Fatura XML hazırla
    ├── Turkcell API'ye POST
    ├── ETTN al
    └── orders.efatura_status = sent
```

---

## Veri Modeli

```
flovy_orders
├── id (UUID)
├── tenant_id
├── session_id (FK → chat_sessions)
├── visitor_id (FK → flovy_visitors)
├── order_number              ← FLV-2026-00001 formatı
├── status (pending/paid/cancelled/refunded)
├── payment_status (pending/paid/failed)
├── items (JSON)              ← [{ product_id, title, qty, unit_price, tax_rate }]
├── subtotal (DECIMAL)
├── tax_total (DECIMAL)
├── total (DECIMAL)
├── currency (default: TRY)
├── customer_name
├── customer_email
├── customer_phone
├── customer_address
├── customer_tax_number       ← Şirket ise — e-Fatura için
├── customer_tax_office
├── payment_link_id (FK)
├── paytr_merchant_oid
├── paid_at
├── efatura_status (pending/sent/failed/not_required)
├── efatura_ettn              ← Turkcell'den dönen ETTN
├── efatura_type (efatura/earsiv)
├── efatura_number            ← Fatura numarası
├── efatura_sent_at
├── efatura_error
├── created_at
└── updated_at

efatura_mukellef_cache         ← GIB mükellef listesi (günlük ZIP güncelleme)
├── vkn_tckn                  ← VKN veya TCKN
├── unvan
├── is_mukellef (BOOL)
└── updated_at
```

---

## TurkcellEFaturaService

```python
class TurkcellEFaturaService:
    BASE_URL = settings.TURKCELL_API_URL

    def __init__(self):
        self.headers = {
            "x-api-key": settings.TURKCELL_API_KEY,
            "Content-Type": "application/json",
        }

    async def issue_invoice(self, order: Order) -> dict:
        # 1. Mükellefiyet kontrolü
        is_mukellef = await self.check_mukellef(order.customer_tax_number)
        invoice_type = "EARSIV" if not is_mukellef else "EFATURA"

        # 2. Fatura payload hazırla
        payload = self.build_payload(order, invoice_type)

        # 3. API çağrısı
        async with httpx.AsyncClient() as client:
            resp = await client.post(
                f"{self.BASE_URL}/invoices",
                json=payload,
                headers=self.headers,
                timeout=30.0,
            )

        result = resp.json()
        if result.get("success"):
            return {
                "success": True,
                "ettn": result["ettn"],
                "invoice_number": result["invoiceNumber"],
                "efatura_type": invoice_type,
            }

        return {"success": False, "message": result.get("message", "Bilinmeyen hata")}

    async def check_mukellef(self, tax_number: str | None) -> bool:
        if not tax_number:
            return False
        row = await db.get(EfaturaMukellefCache, tax_number)
        return bool(row and row.is_mukellef)

    def build_payload(self, order: Order, invoice_type: str) -> dict:
        return {
            "invoiceType": invoice_type,
            "invoiceDate": order.paid_at.strftime("%Y-%m-%d"),
            "buyer": {
                "name": order.customer_name,
                "email": order.customer_email,
                "taxNumber": order.customer_tax_number or "",
                "taxOffice": order.customer_tax_office or "",
                "address": order.customer_address or "",
            },
            "lines": [
                {
                    "description": item["title"],
                    "quantity": item["qty"],
                    "unitPrice": float(item["unit_price"]),
                    "vatRate": float(item["tax_rate"]),
                }
                for item in order.items
            ],
        }
```

---

## Celery Task

```python
@celery_app.task(bind=True, max_retries=3, default_retry_delay=300)
async def issue_efatura(self, order_id: str):
    order = await OrderRepo.get(order_id)
    if not order or order.efatura_status in ("sent",):
        return  # Idempotent

    try:
        svc = TurkcellEFaturaService()
        result = await svc.issue_invoice(order)

        if result["success"]:
            await OrderRepo.update(order_id, {
                "efatura_status": "sent",
                "efatura_ettn": result["ettn"],
                "efatura_number": result["invoice_number"],
                "efatura_type": result["efatura_type"],
                "efatura_sent_at": datetime.utcnow(),
            })
        else:
            await OrderRepo.update(order_id, {
                "efatura_status": "failed",
                "efatura_error": result["message"],
            })
            # 3 kez retry, sonra admin log
            raise self.retry(exc=Exception(result["message"]))

    except Exception as exc:
        logger.critical(f"e-Fatura gönderilemedi: order_id={order_id}, error={exc}")
        raise
```

---

## GIB Mükellef Cache Güncelleme

Günlük Celery beat job:

```python
@celery_app.task
async def refresh_mukellef_cache():
    # Turkcell'den günlük ZIP'i çek
    # ZIP içindeki CSV'yi parse et
    # efatura_mukellef_cache tablosunu upsert et
    ...
```

---

## API Endpoint'leri (Panel)

```
GET  /api/orders                       ← Sipariş listesi
GET  /api/orders/{id}                  ← Sipariş detayı
POST /api/orders/{id}/efatura/retry    ← Başarısız faturayı yeniden dene
GET  /api/orders/{id}/efatura/pdf      ← Fatura PDF (Turkcell'den çek)
```

---

## İş Kuralları

- Ödeme başarılı olduğunda e-Fatura kesimi Celery task'a devredilir — HTTP callback'i bloke etmez.
- Fatura kesilmese bile ödeme geçerlidir. Başarısız fatura admin log'a düşer, tenant panelinde "Yeniden Dene" butonu çıkar.
- `efatura_status` idempotent: "sent" ise task tekrar çalışsa da atlar.
- Tenant'ın Turkcell credentials'ı yoksa: Flovy'nin global credentials'ı kullanılır (ilk faz). İlerleyen fazda per-tenant credentials.
- `customer_tax_number` boşsa: e-Arşiv, bireysel satış varsayımı.
