# ARCHITECTURE.md — Flovy Mimari Anayasası

Bu dosya Flovy projesinin **değişmez mimari kurallarını** tanımlar.

Yapay zeka değişse de, geliştirici değişse de, yeni modül eklense de — **kod bu dosyaya uygun yazılır.** Burada tanımlanan katmanlar, adlandırma kuralları, sorumluluk sınırları ve veri akışı hiçbir gerekçeyle ihlal edilemez. İhlal edilirse teknik borç birikir, spagetti kod oluşur.

Yeni bir özellik eklemeden önce bu dosyayı oku. Yeni bir AI instance başlatılırken bu dosya context'e verilir.

---

## 1. Katmanlı Mimari (Zorunlu)

Flovy dört katmandan oluşur. Her katmanın tek bir sorumluluğu vardır ve sadece bir alt katmanla konuşur.

```
┌─────────────────────────────────────────────────┐
│                  ROUTER LAYER                   │
│  app/routers/*.py                               │
│  • HTTP request parse                           │
│  • Auth/tenant middleware                       │
│  • Pydantic schema ile input validate           │
│  • Service çağır                                │
│  • Response formatla, dön                       │
│  ❌ İş mantığı YOK, DB sorgusu YOK             │
└─────────────────────┬───────────────────────────┘
                      │ sadece Service çağırır
                      ▼
┌─────────────────────────────────────────────────┐
│                 SERVICE LAYER                   │
│  app/services/**/*.py                           │
│  • İş mantığı burada                           │
│  • Kurallar, doğrulamalar, akış koordinasyonu   │
│  • Birden fazla Repository kullanabilir         │
│  • Event/hook tetikler                          │
│  ❌ HTTP request/response bilgisi YOK           │
│  ❌ Doğrudan DB Model sorgusu YOK              │
└─────────────────────┬───────────────────────────┘
                      │ sadece Repository çağırır
                      ▼
┌─────────────────────────────────────────────────┐
│              REPOSITORY LAYER                   │
│  app/repositories/*.py                          │
│  • DB okuma/yazma işlemleri burada              │
│  • SQLAlchemy ORM sorguları                     │
│  • Her metot tek bir iş yapar                   │
│  ❌ İş mantığı YOK                             │
│  ❌ HTTP bilgisi YOK                           │
└─────────────────────┬───────────────────────────┘
                      │ sadece Model okur/yazar
                      ▼
┌─────────────────────────────────────────────────┐
│                 MODEL LAYER                     │
│  app/models/*.py                                │
│  • SQLAlchemy ORM tanımları                     │
│  • Tablo şeması, ilişkiler, tipler              │
│  ❌ İş mantığı YOK                             │
│  ❌ Metot/hesaplama YOK (property hariç)       │
└─────────────────────────────────────────────────┘
```

### Kural: Katman Atlatma Yasak

```python
# ❌ YANLIŞ — Router doğrudan DB sorgusu yapıyor
@router.get("/products")
async def list_products(db: AsyncSession = Depends(get_db)):
    products = await db.scalars(select(Product).where(...))  # YASAK
    return products

# ✅ DOĞRU — Router sadece Service çağırır
@router.get("/products")
async def list_products(
    tenant: Tenant = Depends(get_current_tenant),
    service: ProductService = Depends(),
):
    return await service.list_products(tenant.id)
```

```python
# ❌ YANLIŞ — Service doğrudan SQLAlchemy kullanıyor
class ProductService:
    async def list_products(self, tenant_id: str):
        async with get_db() as db:
            return await db.scalars(select(Product).where(...))  # YASAK

# ✅ DOĞRU — Service Repository'ye delege eder
class ProductService:
    def __init__(self, repo: ProductRepository):
        self.repo = repo

    async def list_products(self, tenant_id: str) -> list[ProductSchema]:
        products = await self.repo.find_by_tenant(tenant_id)
        return [ProductSchema.model_validate(p) for p in products]
```

---

## 2. Dosya ve Dizin Yapısı (Zorunlu)

```
app/
├── main.py                    ← FastAPI app, middleware, router register
├── config.py                  ← Pydantic Settings — TÜM config buradan
├── database.py                ← Engine, session factory, get_db dependency
│
├── models/                    ← SQLAlchemy ORM modelleri (sadece tablo tanımı)
│   ├── __init__.py
│   ├── tenant.py
│   ├── product.py
│   ├── chat.py
│   ├── visitor.py
│   ├── order.py
│   └── knowledge.py
│
├── schemas/                   ← Pydantic request/response şemaları
│   ├── __init__.py
│   ├── auth.py
│   ├── product.py
│   ├── chat.py
│   ├── visitor.py
│   └── order.py
│
├── repositories/              ← DB erişim katmanı
│   ├── __init__.py
│   ├── base.py                ← BaseRepository (get, create, update, delete)
│   ├── tenant_repo.py
│   ├── product_repo.py
│   ├── chat_repo.py
│   ├── visitor_repo.py
│   └── order_repo.py
│
├── services/                  ← İş mantığı katmanı
│   ├── __init__.py
│   ├── auth_service.py
│   ├── product_service.py
│   ├── chat_service.py
│   ├── visitor_service.py
│   ├── order_service.py
│   ├── ai/
│   │   ├── flovy_service.py   ← Orkestratör
│   │   ├── intent_router.py
│   │   ├── rule_matcher.py
│   │   ├── semantic_router.py
│   │   ├── gemma_client.py
│   │   ├── prompt_builder.py
│   │   └── response_parser.py
│   ├── rag/
│   │   ├── knowledge_indexer.py
│   │   ├── knowledge_retriever.py
│   │   ├── embedding_client.py
│   │   └── product_matcher.py
│   ├── tools/
│   │   ├── base.py            ← ToolBase, ToolResult
│   │   ├── registry.py
│   │   ├── search_products.py
│   │   ├── add_to_cart.py
│   │   ├── create_pay_link.py
│   │   ├── collect_lead.py
│   │   └── escalate_to_human.py
│   ├── payment/
│   │   └── paytr_service.py
│   └── efatura/
│       └── turkcell_service.py
│
├── routers/                   ← HTTP endpoint'leri
│   ├── __init__.py
│   ├── auth.py
│   ├── products.py
│   ├── widget.py
│   ├── chat.py
│   ├── knowledge.py
│   ├── orders.py
│   └── webhooks.py
│
├── auth/                      ← JWT, middleware, dependency'ler
│   ├── jwt.py
│   ├── dependencies.py        ← get_current_tenant, get_widget_by_key
│   └── middleware.py
│
├── tasks/                     ← Celery background job'ları
│   ├── celery_app.py
│   ├── index_knowledge.py
│   ├── issue_efatura.py
│   ├── warm_anchors.py
│   └── anonymize_pii.py
│
└── utils/
    ├── exceptions.py          ← Domain exception sınıfları
    ├── pagination.py          ← Cursor pagination yardımcısı
    └── security.py            ← Hash, token üretimi
```

### Adlandırma Kuralları

| Tür | Format | Örnek |
|---|---|---|
| Dosya | `snake_case.py` | `product_service.py` |
| Sınıf | `PascalCase` | `ProductService` |
| Fonksiyon | `snake_case` | `list_products` |
| Async fonksiyon | `async def snake_case` | `async def list_products` |
| Repository metodu | fiil + nesne | `find_by_tenant`, `create_product` |
| Service metodu | iş eylemi | `list_products`, `publish_product` |
| Router handler | HTTP fiil + kaynak | `get_products`, `create_product` |
| Schema (request) | `{İsim}Request` | `CreateProductRequest` |
| Schema (response) | `{İsim}Response` | `ProductResponse` |
| Celery task | `{eylem}_{nesne}` | `issue_efatura`, `index_knowledge` |

---

## 3. Dependency Injection (DI) Kuralı

FastAPI'nin built-in DI sistemi kullanılır. Hiçbir sınıf `__init__` içinde başka bir servis doğrudan örneklenmez.

```python
# ❌ YANLIŞ — Hard-coded bağımlılık
class ProductService:
    def __init__(self):
        self.repo = ProductRepository()      # YASAK
        self.embedding = EmbeddingClient()  # YASAK

# ✅ DOĞRU — DI ile enjekte edilir
class ProductService:
    def __init__(
        self,
        repo: ProductRepository = Depends(),
        embedding: EmbeddingClient = Depends(),
    ):
        self.repo = repo
        self.embedding = embedding

# Router'da kullanım:
@router.post("/products")
async def create_product(
    body: CreateProductRequest,
    tenant: Tenant = Depends(get_current_tenant),
    service: ProductService = Depends(),
):
    return await service.create(tenant.id, body)
```

---

## 4. Tenant ve Visitor İzolasyonu (Mutlak Kural)

Her veri erişimi `tenant_id` ile filtrelenmelidir. Bu kuralın ihlali cross-tenant veri sızıntısıdır — güvenlik açığıdır.

```python
# ❌ YANLIŞ — tenant_id filtresi eksik
async def get_product(product_id: str) -> Product:
    return await db.get(Product, product_id)  # Başka tenant'ın ürünü gelebilir!

# ✅ DOĞRU — her zaman tenant_id ile birlikte sorgula
async def get_product(product_id: str, tenant_id: str) -> Product | None:
    result = await db.execute(
        select(Product).where(
            Product.id == product_id,
            Product.tenant_id == tenant_id,  # ZORUNLU
        )
    )
    return result.scalar_one_or_none()
```

Visitor sorguları da aynı kurala tabidir:

```python
# visitor_id her zaman tenant_id ile birlikte kullanılır
select(Visitor).where(
    Visitor.id == visitor_id,
    Visitor.tenant_id == tenant_id,   # ZORUNLU
)
```

Repository `BaseRepository`'deki her metot `tenant_id` parametresini zorunlu alır.

---

## 5. Async Kuralı (Mutlak Kural)

Tüm IO operasyonları async olmalıdır. Senkron IO çağrısı event loop'u bloke eder, tüm eşzamanlı istekler durur.

```python
# ❌ YANLIŞ — senkron IO
import requests
response = requests.get(url)         # YASAK — event loop bloke

import time
time.sleep(1)                        # YASAK — event loop bloke

# ✅ DOĞRU — async IO
import httpx
async with httpx.AsyncClient() as client:
    response = await client.get(url)

import asyncio
await asyncio.sleep(1)
```

| Kütüphane | Kullanım |
|---|---|
| `httpx.AsyncClient` | Dış API çağrıları (Gemini, PayTR, Turkcell) |
| `SQLAlchemy async` | DB sorguları |
| `aioredis` | Cache okuma/yazma |
| `aiofiles` | Dosya okuma/yazma |

---

## 6. Hata Yönetimi (Zorunlu Yaklaşım)

Domain hatalar özel exception sınıflarıyla temsil edilir. Router katmanında yakalanır ve HTTP'ye çevrilir. `try/except Exception` her yerde kullanılmaz.

```python
# app/utils/exceptions.py
class FlovyException(Exception):
    def __init__(self, code: str, message: str, status_code: int = 400):
        self.code = code
        self.message = message
        self.status_code = status_code

class NotFoundError(FlovyException):
    def __init__(self, resource: str):
        super().__init__("NOT_FOUND", f"{resource} bulunamadı.", 404)

class PermissionError(FlovyException):
    def __init__(self):
        super().__init__("FORBIDDEN", "Bu işlem için yetkiniz yok.", 403)

class QuotaExceededError(FlovyException):
    def __init__(self, quota_type: str):
        super().__init__("QUOTA_EXCEEDED", f"{quota_type} kotanız dolmuş.", 429)

class ValidationError(FlovyException):
    def __init__(self, message: str):
        super().__init__("VALIDATION_FAILED", message, 422)
```

```python
# app/main.py — Global exception handler
@app.exception_handler(FlovyException)
async def flovy_exception_handler(request: Request, exc: FlovyException):
    return JSONResponse(
        status_code=exc.status_code,
        content={"success": False, "error": {"code": exc.code, "message": exc.message}},
    )

@app.exception_handler(Exception)
async def generic_exception_handler(request: Request, exc: Exception):
    logger.exception(f"Unhandled exception: {exc}")
    return JSONResponse(
        status_code=500,
        content={"success": False, "error": {"code": "SERVER_ERROR", "message": "Sunucu hatası."}},
    )
```

```python
# Service'de kullanım
class ProductService:
    async def get_product(self, product_id: str, tenant_id: str) -> ProductResponse:
        product = await self.repo.get_product(product_id, tenant_id)
        if not product:
            raise NotFoundError("Ürün")    # ← domain exception, message sızdırmaz
        return ProductResponse.model_validate(product)
```

### Kural: Dış Servis Hatalarını Sarmala

```python
# ❌ YANLIŞ — ham exception leak
async def call_gemini(prompt):
    resp = await client.post(url, json=prompt)
    return resp.json()   # Gemini 500 verirse ham hata client'a gider

# ✅ DOĞRU — sarmala ve anlamlı hata ver
async def call_gemini(prompt):
    try:
        resp = await client.post(url, json=prompt, timeout=30.0)
        resp.raise_for_status()
        return resp.json()
    except httpx.TimeoutException:
        raise FlovyException("AI_TIMEOUT", "AI yanıt vermedi, lütfen tekrar deneyin.", 503)
    except httpx.HTTPStatusError as e:
        logger.error(f"Gemini API hata: {e.response.status_code}")
        raise FlovyException("AI_ERROR", "AI servisi geçici olarak kullanılamıyor.", 503)
```

---

## 7. Response Formatı (Zorunlu)

Tüm API yanıtları aynı zarfı kullanır:

```python
# Başarılı
{
  "success": true,
  "data": { ... },        # tek nesne
  "meta": {               # opsiyonel — pagination için
    "next_cursor": "...",
    "has_more": true,
    "limit": 20
  }
}

# Hatalı
{
  "success": false,
  "error": {
    "code": "NOT_FOUND",         # makine okur
    "message": "Ürün bulunamadı."  # insan okur
  }
}
```

```python
# app/utils/responses.py
def ok(data: dict | list, meta: dict = None, status: int = 200) -> JSONResponse:
    payload = {"success": True, "data": data}
    if meta:
        payload["meta"] = meta
    return JSONResponse(content=payload, status_code=status)

def created(data: dict) -> JSONResponse:
    return ok(data, status=201)

def no_content() -> Response:
    return Response(status_code=204)
```

---

## 8. Background Job Kuralı

**HTTP request içinde asla uzun süren iş yapılmaz.** Uzun iş = Celery task.

| İşlem | Nerede? |
|---|---|
| Embedding indexleme | Celery |
| Site scraping | Celery |
| e-Fatura gönderimi | Celery |
| PII anonimleştirme | Celery beat |
| Anchor ısıtma | Celery beat |
| Mesaj DB'ye kaydetme | Inline (hızlı) |
| Visitor profil güncelleme | Inline (hızlı) |
| PayTR link oluşturma | Inline (sync, dış API) |

```python
# ❌ YANLIŞ — HTTP request içinde embedding hesaplanıyor
@router.post("/products")
async def create_product(body: CreateProductRequest, ...):
    product = await service.create(...)
    embedding = await embed(product.description)  # 500ms+ YASAK
    await save_embedding(embedding)
    return product

# ✅ DOĞRU — Celery'ye devret
@router.post("/products")
async def create_product(body: CreateProductRequest, ...):
    product = await service.create(...)
    index_knowledge.delay(str(product.id), "product")  # fire-and-forget
    return created(ProductResponse.model_validate(product).model_dump())
```

---

## 9. Konfigürasyon Kuralı

`env()` hiçbir yerde doğrudan çağrılmaz. Tüm config `app/config.py` üzerinden okunur.

```python
# app/config.py
from pydantic_settings import BaseSettings

class Settings(BaseSettings):
    db_host: str = "localhost"
    db_name: str = "tekyerden_flovy"
    db_user: str
    db_password: str
    redis_url: str = "redis://localhost:6379/1"
    gemini_api_key: str
    paytr_merchant_id: str
    paytr_merchant_key: str
    paytr_merchant_salt: str
    turkcell_api_key: str
    jwt_secret: str
    app_base_url: str = "https://flovy.tekyerden.co"
    stream_secret: str
    stream_port: int = 8090

    class Config:
        env_file = ".env"

settings = Settings()

# ✅ Doğru kullanım
from app.config import settings
api_key = settings.gemini_api_key

# ❌ Yasak
import os
api_key = os.environ.get("GEMINI_API_KEY")  # YASAK
api_key = env("GEMINI_API_KEY")             # YASAK
```

---

## 10. Repository Pattern (Zorunlu Yapı)

```python
# app/repositories/base.py
class BaseRepository(Generic[T]):
    model: type[T]

    def __init__(self, db: AsyncSession = Depends(get_db)):
        self.db = db

    async def get(self, id: str, tenant_id: str) -> T | None:
        result = await self.db.execute(
            select(self.model).where(
                self.model.id == id,
                self.model.tenant_id == tenant_id,
            )
        )
        return result.scalar_one_or_none()

    async def create(self, **kwargs) -> T:
        obj = self.model(**kwargs)
        self.db.add(obj)
        await self.db.commit()
        await self.db.refresh(obj)
        return obj

    async def update(self, obj: T, **kwargs) -> T:
        for key, value in kwargs.items():
            setattr(obj, key, value)
        await self.db.commit()
        await self.db.refresh(obj)
        return obj

    async def soft_delete(self, obj: T) -> None:
        obj.deleted_at = datetime.utcnow()
        await self.db.commit()
```

Her repository sadece kendi modeline ait sorgular içerir. Başka modele ait tablo sorgusu yapılmaz.

---

## 11. Veri Akışı — Tam Resim

```
HTTP Request
    │
    ▼
Router (app/routers/*.py)
    • Pydantic ile body/query parse
    • Auth dependency (tenant/widget çek)
    • Service çağır
    │
    ▼
Service (app/services/**/*.py)
    • İş kuralları
    • Repository çağır
    • Gerekirse Celery task tetikle
    • Domain exception fırlat
    │
    ▼
Repository (app/repositories/*.py)
    • SQLAlchemy async sorgu
    • tenant_id filtresi zorunlu
    │
    ▼
Model (app/models/*.py)
    • SQLAlchemy tablo tanımı
    │
    ▼
MySQL (tekyerden_flovy)

─────── Geri dönüş ───────

Model → Repository → Service
    • Service, Model → Pydantic Schema dönüşümü yapar
    │
    ▼
Router
    • ok() / created() / no_content() ile sar
    │
    ▼
HTTP Response {"success": true, "data": {...}}
```

---

## 12. Yeni Modül Ekleme Protokolü

Yeni bir özellik eklenirken sıra şöyle olmalı:

```
1. docs/{N}-YENI-MODUL.md yaz
   → Modülün ne iş yaptığı, veri modeli, endpoint listesi, iş kuralları

2. app/models/yeni_model.py
   → SQLAlchemy ORM tanımı (sadece tablo şeması)

3. Alembic migration
   → alembic revision --autogenerate -m "add_yeni_model"
   → alembic upgrade head

4. app/schemas/yeni_model.py
   → Pydantic request/response şemaları

5. app/repositories/yeni_repo.py
   → BaseRepository'den miras, CRUD metotları

6. app/services/yeni_service.py
   → İş mantığı, Repository'ye delege

7. app/routers/yeni_router.py
   → Thin handler'lar, Service çağrısı

8. app/main.py
   → Router register et

9. tests/test_yeni_modul.py
   → En az: create, read, tenant isolation testi
```

**Bu sıra ihlal edilemez.** Model olmadan Service yazılmaz. Service olmadan Router yazılmaz.

---

## 13. Test Kuralları

```python
# Her service için en az 3 test:
# 1. Happy path
# 2. Not found → NotFoundError
# 3. Tenant isolation — başka tenant'ın verisine erişim → NotFoundError

@pytest.mark.asyncio
async def test_get_product_wrong_tenant(product_service, tenant_a, tenant_b):
    product = await create_test_product(tenant_id=tenant_a.id)
    with pytest.raises(NotFoundError):
        await product_service.get_product(product.id, tenant_id=tenant_b.id)
```

Mock yasakları:
- DB mock'lamak yasak — gerçek test DB kullanılır
- Dış API mock'ları: httpx `respx` ile (sadece Gemini, PayTR, Turkcell)

---

## 14. AI Servis Katmanı Mimarisi

AI servisleri (`app/services/ai/`) diğer servislerden farklı bir pattern izler: **orkestratör + strateji**.

```
FlovyService (Orkestratör)
    │
    ├── IntentRouter.decide(message, ctx)
    │       ├── RuleMatcher.match(message)       → RULE_MATCH
    │       ├── SemanticRouter.match(message)    → SEMANTIC_MATCH
    │       └── → LLM (Gemini)
    │
    ├── KnowledgeRetriever.top_k(...)           → RAG context
    ├── MemoryBuilder.build(visitor)             → Geçmiş context
    ├── PromptBuilder.build(...)                 → Final prompt
    ├── GemmaClient.chat(prompt)                 → LLM yanıt
    ├── ResponseParser.parse(response)           → Tool call'lar
    └── ToolRegistry.execute(tool_name, args)    → Tool çalıştır
```

**Kural:** `FlovyService` dışında hiçbir servis `GemmaClient`'ı doğrudan çağıramaz. AI çağrıları merkezi orkestratörden geçer.

**Kural:** `GemmaClient` bir singleton'dır — persistent HTTP connection pool için. DI ile enjekte edilir, her request'te `new GemmaClient()` çağrılmaz.

---

## 15. Streaming Mimarisi

Node.js streaming köprüsü (`/home/tekyerden/flovy-stream/`) FastAPI'den bağımsızdır. Aralarındaki sözleşme:

```
FastAPI → Node.js: POST /api/stream/resolve
  Request:  { stream_token: string }
  Response: { prompt: {...}, tools: [...], model: string, tenant_id: string }

Node.js → FastAPI: POST /api/stream/done
  Header:   X-Flovy-Stream-Secret: {STREAM_SECRET}
  Request:  { stream_token, content, tool_calls, tokens_in, tokens_out }
  Response: "OK"
```

**Kural:** Stream token Redis'te 5 dakika TTL ile saklanır. Token expire olmuşsa `done` callback reddedilir.

**Kural:** `X-Flovy-Stream-Secret` header doğrulanmadan hiçbir stream endpoint'i işleme alınmaz.

---

## Özet: Tek Bakışta Kurallar

| Kural | Özet |
|---|---|
| Katman atlatma | ❌ Router → DB yasak. Router → Service → Repo → Model. |
| Tenant izolasyonu | ❌ `tenant_id` filtresi olmayan sorgu yasak. |
| Senkron IO | ❌ `requests`, `time.sleep`, senkron dosya okuma yasak. |
| `env()` direkt | ❌ `os.environ`, `env()` direkt kullanımı yasak. `settings.*` kullan. |
| Exception leak | ❌ DB/dış servis ham hatası client'a verilmez. Sarmala. |
| HTTP request'te uzun iş | ❌ Embedding, scraping, e-fatura HTTP içinde çalışmaz. Celery. |
| `GemmaClient` direkt | ❌ AI katmanı dışından `GemmaClient` çağrısı yasak. |
| Mock DB test | ❌ Test'te gerçek DB kullanılır, mock yasak. |
| Response format | ✅ Her yanıt `{"success": bool, "data": ...}` zarfında. |
| Yeni modül sırası | ✅ docs → model → migration → schema → repo → service → router → test. |
