# 09 — Ziyaretçi Hafızası (Moat)

## Neden En Kritik Modül?

Rakipler her sohbeti sıfırdan başlatır. Flovy, aynı ziyaretçiyi tekrar tanır ve geçmişe dayalı kişiselleştirilmiş deneyim sunar. İşletme Flovy'den vazgeçince bu veriyi kaybeder. **Bu, switching cost'un temelidir.**

---

## Ziyaretçi Kimlik Tespiti — Sinyal Hiyerarşisi

```
Güven seviyesi (yüksekten düşüğe):

1. Email (form/checkout'tan) ────────────► %100 güven, kalıcı profil merge
2. Telefon (form'dan) ───────────────────► %100 güven, kalıcı profil merge
3. Authenticated UUID (LocalStorage) ────► %95 güven, 365 gün TTL
4. Browser fingerprint ──────────────────► %80 güven (canvas+audio+font)
5. IP + UserAgent kombinasyonu ──────────► %40 güven, yardımcı sinyal
```

### Browser Fingerprint Bileşenleri

Widget JS tarafında toplanan sinyaller:

```javascript
{
  canvas_hash: sha256(canvas.toDataURL()),
  audio_hash: sha256(audioContext fingerprint),
  fonts: ['Arial', 'Helvetica', ...],  // yüklü fontlar
  screen: "1920x1080",
  timezone: "Europe/Istanbul",
  language: "tr-TR",
  platform: "MacIntel",
  plugins_count: 3,
  webgl_vendor: "Apple Inc.",
}
// → SHA256 → tek hash
```

---

## Veri Modeli

```
flovy_visitors
├── id (UUID)
├── tenant_id (FK)
├── visitor_uuid          ← LocalStorage'dan — kalıcı anonim ID
├── fingerprint_hash      ← Browser sinyal hash'i
├── email                 ← Form/checkout'tan — varsa
├── phone                 ← Form'dan — varsa
├── name                  ← Form'dan — varsa
├── ip_address            ← Son bilinen IP
├── ip_history (JSON)     ← ["1.2.3.4", "5.6.7.8"]
├── device_type           ← mobile / desktop / tablet
├── browser               ← chrome / safari / firefox
├── language              ← tr / en
├── total_sessions (INT)
├── total_messages (INT)
├── last_session_at
├── last_intent           ← Son tespit edilen niyet
├── lifetime_value (DECIMAL) ← Toplam harcama (TL)
├── products_viewed (JSON)   ← [{"id":..., "title":..., "at":"..."}]
├── products_purchased (JSON) ← [{"id":..., "title":..., "amount":..., "at":"..."}]
├── cart_abandoned (JSON)    ← Son terk edilen sepet
├── interests (JSON)         ← Sık baktığı kategoriler
├── preferred_time_of_day    ← morning / afternoon / evening
├── is_returning (BOOL)
├── kvkk_consent_at
├── anonymized_at            ← PII silinince set edilir
├── created_at
└── updated_at

flovy_visitor_sessions
├── id (UUID)
├── visitor_id (FK → flovy_visitors)
├── tenant_id
├── chat_session_id
├── ip_address
├── page_url
├── referrer
├── utm_source, utm_medium, utm_campaign
├── started_at
└── ended_at
```

---

## Ziyaretçi Tanıma Akışı

```
Widget yüklendiğinde (her sayfa açılışında):

1. Widget JS:
   a. LocalStorage'dan visitor_uuid oku (yoksa yeni UUID üret, kaydet)
   b. Fingerprint hesapla (canvas + audio + font)
   c. POST /api/widget/{api_key}/visitor/identify {
       visitor_uuid, fingerprint, ip (server tarafı alır), page_url, referrer
   }

2. Backend (VisitorService.identify):
   a. visitor_uuid ile bul → varsa güncelle
   b. Yoksa fingerprint_hash ile bul → varsa merge
   c. Hiç yoksa yeni kayıt oluştur
   d. → visitor_id dön

3. Sohbet açılınca:
   - chat_sessions.visitor_id = visitor_id
   - MemoryBuilder.build(visitor_id) çağrılır
   - Geçmiş → prompt context'e inject edilir
```

---

## Memory Builder — Prompt Injection

Her AI yanıtından önce ziyaretçinin geçmişi sistem prompt'una eklenir:

```python
class MemoryBuilder:
    def build(self, visitor: Visitor) -> str:
        if not visitor or not visitor.is_returning:
            return ""

        parts = []

        if visitor.name:
            parts.append(f"Ziyaretçinin adı: {visitor.name}")

        if visitor.products_viewed:
            recent = visitor.products_viewed[-3:]
            titles = [p['title'] for p in recent]
            parts.append(f"Son baktığı ürünler: {', '.join(titles)}")

        if visitor.products_purchased:
            titles = [p['title'] for p in visitor.products_purchased]
            parts.append(f"Daha önce satın aldıkları: {', '.join(titles)}")

        if visitor.cart_abandoned:
            cart = visitor.cart_abandoned
            parts.append(
                f"Geçen seferde sepetini terk etti: {cart['title']} "
                f"({cart['amount']} TL) — bu ziyarette hatırlatabilirsin."
            )

        if visitor.last_intent:
            parts.append(f"Son sohbetteki niyeti: {visitor.last_intent}")

        if not parts:
            return ""

        return (
            "=== ZİYARETÇİ GEÇMİŞİ (sadece sen görüyorsun, doğal kullan) ===\n"
            + "\n".join(f"- {p}" for p in parts)
            + "\n==="
        )
```

---

## Email/Telefon Geldiğinde Merge

Ziyaretçi checkout veya lead formunu doldurunca:

```python
async def merge_visitor_identity(
    visitor: Visitor, email: str = None, phone: str = None, name: str = None
):
    # Aynı email/telefon başka anonim profile bağlı mı?
    existing = await db.query(Visitor).filter(
        Visitor.tenant_id == visitor.tenant_id,
        or_(Visitor.email == email, Visitor.phone == phone)
    ).first()

    if existing and existing.id != visitor.id:
        # Eski profili yeni ile merge et (total_sessions, purchased, viewed birleştir)
        await merge_profiles(source=existing, target=visitor)
        await db.delete(existing)

    visitor.email = email or visitor.email
    visitor.phone = phone or visitor.phone
    visitor.name = name or visitor.name
    await db.commit()
```

---

## Analitik Değer (Tenant'a Sunulan)

```
Bu ay Flovy:
- 847 ziyaretçi konuştu
- 234'ü daha önce gelmiş, tanındı (%27 dönüş oranı)
- Tanınan ziyaretçilerin dönüşüm oranı: %18 (yabancılar: %6)
- En çok terk edilen sepet ürünü: Collagen Plus (23 kez)
- Geri dönen ziyaretçiye "geçen sefer baktığın ürün" hatırlatması → 12 satış

Sonuç: Ziyaretçi tanıma sistemi bu ay ₺4.200 ek gelir sağladı.
```

---

## KVKK ve PII Yönetimi

- İlk sohbette: `"Sohbet geçmişiniz kişisel öneri sunmak için 90 gün saklanır. [Aydınlatma Metni]"` + onay checkbox.
- `kvkk_consent_at` set edilmeden visitor profili saklanmaz (sadece anonim session tutulur).
- Ziyaretçi "verimi sil" derse: email, phone, name → NULL; fingerprint_hash → rastgele string; products_viewed, purchased → [].
- 90 gün sonra otomatik anonimleştirme (Celery `anonymize_pii` job).
- Tenant başka tenant'ın visitor'ına erişemez — her sorgu `tenant_id` filtreli.

---

## Gotcha'lar

- Fingerprint, VPN değişiminde veya tarayıcı güncellenmesinde değişebilir → `visitor_uuid` (LocalStorage) ana identifier, fingerprint yardımcı.
- IP tek başına güvenilir değil — NAT arkasında birden fazla ziyaretçi aynı IP'den gelebilir. Hiçbir zaman tek başına profil merge tetiklemez.
- `kvkk_consent_at` olmadan LLM'e visitor geçmişi inject edilmez.
