# Review — Potansiyel müşteri — implementasyon planı (v1.0) — 2026-10-19

> **Review type:** plan-review
> **Previous review:** N/A (ilk review)
> **Upstream document(s) read:** `proje-haritasi.md` §4.4 (iki karar bloğu: «Potansiyel müşteri ayrı bir yapıdır», «Şubeler: tek merkez 3E Group»), YouTrack EGT-216 (Tayfun'un açıklaması `youtrack.sh show` ile okundu), `docs/arch/mimari-kararlar.md` (AD-04, AD-05, AD-06, AD-08, AD-11, AD-16, AD-21, AD-23, AD-25, AD-26), `CLAUDE.md` (kırmızı çizgiler + Tasarım Uygulama Protokolü). Spec turu yoktur (Sets kararı, 19.10.2026; emsal `docs/plans/musteri-yonetimi-plan.md` — varlığı doğrulandı).
> **Other cross-referenced documents:** planın «Doğrulanmış mevcut durum» tablosundaki kanıtlar koddan tek tek açıldı: `TenantContext.php`, `TenantContextGuard.php`, `SessionScopeResolver.php`, `TenantScope.php`, `DefaultTenantConnection.php`, `CreateCustomer.php`, `OnboardCustomer.php`, `CreateCustomerPerson.php`, `CustomerController.php:126-154`, `CustomerPolicy.php`, `RecordNotePolicy.php`, `ScopeResolver.php`, `TrainingsInScope.php`, `ScheduleMeeting.php`, `AddCustomerNote.php`, `CloseMeeting.php`, `OpenCustomerTask.php`, `AuditWriter.php`, `Masker.php`, `AccessLogger.php`, `OpaqueId.php` (`app/Support/Http/`), `PermissionRows.php:84`, `PermissionMatrix.php`, `config/authorization.php`, `config/audit.php`, `config/tenancy.php`, migration'lar (`create_record_notes`, `a_seller_plans_their_day`, `create_tasks`, `create_customer_identity`, `create_customer_id_enumeration`), `tests/Arch/TenancyTest.php`, `tests/Feature/OmurgaRls/SchemaConformanceTest.php`, `tests/FrozenClock.php`, `design/02`, `design/32` (ızgara satırları 49 ve 82 doğrulandı), `OfficeNavigation.php`, `routes/office.php`, `SalesCalendar.php`, `SalesAgenda.php`, `CustomerCard.php`. Mevzuat bağımlılığı yok (CRM yüzeyi; bildirim/sertifika/yetkinlik kuralına dokunmuyor) — `mevzuat/` turu gerekmedi.
> **Stage locks in effect:** Sets'in plana bağlanan kararları kilitli (ayrı tablo, ayna değil, dönüşümde silinmez, yetkili elle, Takvimim/kokpit, teklif-fırsat-eğitim kapalı, yeni tasarım yok, Satış grubu, KVKK tarafı kapsam dışı). Bu kararlar tartışılmadı; yalnız planın bunlara ve mimariye uyumu değerlendirildi.

---

## Upstream Coverage Matrix

| Upstream § / madde | Reviewed doc'ta durumu | Kanıt |
|---|---|---|
| Harita §4.4: «Ayrı tablo, ayrı sayfa — customers'ta durum değil» | ✓ | plan K-1, veri modeli, `Pages/Office/PotentialCustomers/*`; 33 dosya ölçümü bağımsız grep ile tutuyor (33 dosya) |
| Harita §4.4: «Ayna tablo değil — yalnız arama sürecinin alanları» | ✓ | plan K-2; veri modeli yalnız arama alanları, dönüşüm formu eksikleri tamamlıyor (Görev 5 UI) |
| Harita §4.4: «Dönüşümde satır silinmez, 'dönüştü' durumuna geçer, müşteriyi gösterir» | ✓ | K-6, K-7; `converted_customer_id` + CHECK + unique; «Dönüştü» rozeti (K-17, Görev 5) |
| Harita §4.4: «Notlar ve görevler kopyalanmaz, yeniden bağlanır; aynı transaction + audit» | ✓ | K-9, Görev 5 adım 6-8; `record_notes`/`tasks` morph yapısı koddan doğrulandı; `CustomerCard::tasks()` subject ile okuyor → yeniden bağlanan görev kartta görünür (`CustomerCard.php:495-498`) |
| Harita §4.4: «Görünürlük kendi kapsamıyla; açan otomatik sahip; şube kapsamıyla çakışmaz» | ✓ | K-4, K-5, `PotentialCustomersInReach`; AD-23 semantiği (`ScopeResolver::groupsFor/covers`, seviye daraltması, `TrainingsInScope` aile-bağlama tuzağı) planda isabetli; EGT-219 şubeleri ek kod istemeden grup düğümüyle çalışır |
| Harita §4.4: «Mükerrer kontrolü iki tabloya da bakar (vergi no)» | ⚠ | K-13 + `app.customer_tax_number_taken` açma/düzenleme için eksiksiz; **dönüşümdeki ikinci kontrol kendi açık kaydıyla eşleşir ve istisna yazılmamış** → F-4 |
| Harita §4.4: «Kayıtta yapılabilenler: not, görev, planlı görüşme; görüşme Takvimim + kokpite düşer; yeni ekran yok» | ⚠ | K-15, Görev 4 planlama ve takvim/kokpit adaptasyonu var; **görüşmenin kapatılması/iptali (`CloseMeeting`) potansiyelde bugünkü kodla imkânsız ve dosya haritasında değişen dosya olarak yok** → F-2 |
| Harita §4.4: «Teklif, fırsat, eğitim ataması kapalı» | ✓ | K-16; `SchedulePotentialCustomerMeeting` `quote_id`/`opportunity_id` almıyor; kabul kriteri 2 ve 4 |
| Harita §4.4: «Dönüşümde yetkili ELLE; temaslar salt okunur tarihçe, hiçbiri firma kişisi olmaz; müşteri kartı geçmişe bağlantı verir» | ✓ | K-8, K-19; Görev 5 testi «customer_users sayısı 1»; `CustomerDetail.origin` |
| Harita §4.4: «Ekran: yeni tasarım yok; liste `02`, sayfa `32` alt kümesi; dört çizilmemiş öğe mevcut parçalarla; menüde Satış grubu» | ✓ | K-17, Görev 6; `design/32` ızgara iddiaları (`1fr 1fr 1.2fr`, `1fr 480px`) dosyadan doğrulandı; Tasarım Uygulama Protokolü (envanter + kapsama matrisi) Görev 6'da |
| Harita §4.4: «Kişisel veri tarafı ayrıca ele alınmaz» | ✓ | planda ayrı KVKK iş kalemi yok; K-14 yine AD-05 şifrelemesini koruyor |
| Harita §4.4 (şube bloğu): İzmit ayrı grup, yalnız kendine verilen işler | ✓ | K-4 ⚠ notu + `PotentialCustomerReachTest` İzmit/İstanbul fixture'ı; EGT-219 ile çakışma yok (kapsam `owner_user_id` üzerinden, `office_user_customers`'a dokunmuyor) |
| EGT-216: «yetkili eklemeden müşteri kayıt edilemiyor; notlar müşteri oluşturmadan kayıt edilemiyor» | ✓ | hedef cümlesi + kabul kriteri 1 ve 3; Görev 3 testinin ilk maddesi |
| AD-04 (SET LOCAL, PgBouncer) | ✓ | plan tenant context açmıyor (kiracı verisi yok); dönüşümde mevcut zincir kendi `forWrite`'ını kullanıyor; varsayım (a) doğrulama şartıyla işaretli — mekanik olarak tutuyor (aşağıda «What works») |
| AD-03/RLS karar çerçevesi — «tablo ya tenant-scoped ya exempt, üçüncü seçenek yok» | ❌ | K-3 kararı doğru gerekçeli; ama kararın bu kod tabanındaki **uygulama adresi** (`config/tenancy.php` `exempt_tables` + `tests/Arch/TenancyTest.php` + `SchemaConformanceTest`) planın hiçbir yerinde yok → F-1 |
| AD-05 (hassas veri şifreleme) | ✓ | K-14, `encrypted` cast (`Instructor.php:68` deseni doğrulandı); firma santrali bilinçli düz metin, gerekçeli |
| AD-06 / kırmızı çizgi 13 (audit aynı transaction, append-only) | ⚠ | Görev 3 «policy → transaction → yazma → audit aynı transaction» + istisna testi ✓; **Masker mekanizması yanlış aktarılmış** → F-3; **hassas alan okumasının erişim logu (AD-21) hiç ele alınmamış** → F-6 |
| AD-08 (iş kuralı hardcode edilmez) | ✓ | yetkiler havuzdan, paket taslağı backoffice'ten düzenlenir (K-20); sabitlenen tek şey durum makinesi adları — iş kuralı değil, yaşam döngüsü |
| AD-11 (silinmezlik) | ✓ | «Silme yok» global kısıtı, DELETE rotası yok, dönüşmüş kayıt salt okunur, rollback bölümü veri silen down()'u açıkça yasaklıyor |
| AD-16 (maskeleme) | ⚠ | niyet doğru, mekanizma iddiası yanlış → F-3 |
| AD-21 / kırmızı çizgi 17-18 | ⚠ | kişi bazlı analitik yok ✓; hassas okuma erişim logu kararı eksik → F-6 |
| AD-23 (grup ağacı, own/group/all, seviye daraltır) | ✓ | K-4/K-5 ve reach testleri AD-23'ün metniyle birebir (staff `group`→`own` testi dahil); `covers` ile hedef kapsaması `AssignCustomersToUser` benzeri |
| AD-25 (kod yerleşimi) | ✓ | dosya haritası adres sözleşmesine uygun: saf `app/Domain/Sales/`, okuma `Queries/Office`, yazma `Actions/Sales`, Controller ince, Rule/Request ayrı |
| AD-26 (test grupları, FrozenClock, RLS testi uygulama rolüyle) | ✓ | grup `satis` + fonksiyon testi `omurga-rls` uygulama rolüyle; `FrozenClock::INSTANT` mevcut (`tests/FrozenClock.php:16`); factory zorunlu |
| CLAUDE.md kırmızı çizgi taraması (17 madde, plan seviyesinde) | ⚠ | 1-2-4 (tenant) K-3 ile yapısal olarak konu dışı; 5 (data-loss) yok — yalnız ekleme; 7-8 planda temiz; 9 ✓; 12 ✓; 13 → F-3/F-6; 14 konu dışı (mevzuat kuralı yok); 15-16 ✓; 17 ✓ (PhoneInput/EmailInput global kısıtta) |
| CLAUDE.md Tasarım Uygulama Protokolü | ✓ | Görev 6 «önce 32 ve 02 baştan sona okunur, envanter», teslimden önce kapsama matrisi, ❌ satırlara K-16 gerekçesi |
| Rollout / rollback | ✓ | deploy yetki vermez (kapı policy), yetkiler ekrandan, Tayfun test hesabı ilk adım; rollback dürüst: veri girildikten sonra şema geri alınmaz, dönüşüm geri alınmaz (çare pasifleştirme) |
| Cross-panel etki + kuyruk/performans | ✓ | tablo eksiksiz (müşteri portalı/katılımcı/backoffice etkisiz gerekçeli); yeni job yok; «son temas» tek sorguda agregat |

---

## ✅ What works

- **«Doğrulanmış mevcut durum» tablosu gerçekten koddan okunmuş.** Spot-check edilen her satır tuttu: `customers` kolonları, `store`'un üç adımının tek transaction olmadığı (`CustomerController.php:132-150`), vergi no tekilliğinin yokluğu, `SessionScopeResolver`'da önbellek olmadığı, `sales_meetings.customer_id`'nin nullable olduğu, «33 dosya» ölçümü (bağımsız grep ile 33 dosya), `OpaqueId::encode`'un `app/Support/Http/OpaqueId.php:77`'de olduğu, `design/32`'nin ızgara değerleri.
- **K-3 (RLS yok) gerekçesi yeterli.** `record_notes`, `sales_meetings`, `opportunities`, `tasks` migration'ları aynı gerekçeyi kendi yorumlarında taşıyor («3E'nin kendi kaydı, customer_id bir işaretçi») ve dördü de bugün RLS'siz. Potansiyel müşteri kiracı değildir; kırmızı çizgi 1'in istisnası değil, kapsamı dışıdır. (Uygulama adresi eksikliği ayrı — F-1.)
- **Görev 5'in iki varsayımı mekanik olarak tutuyor** (plan «test ile kanıtlanacak» diyor, bu doğru duruş): (a) tüm taraflar aynı bağlantıda — `DB_CONNECTION=pgsql`, `DefaultTenantConnection` da `'pgsql'` — yani iç `DB::transaction` ve `TenantContext`'in açtığı transaction savepoint'e dönüşür; `TenantContext::execute` dış context varken ikinci transaction açmıyor, `withScope` finally ile kapsamı geri sarıyor (`TenantContext.php:148-187`). (b) `SessionScopeResolver` her çağrıda `office_user_customers`'ı yeniden okuyor, `CustomerPolicy::update` de `TenantScope` singleton'ını değil bu resolver'ı kullanıyor (`CustomerPolicy.php:72-80`) — K-12'nin «atama `CreateCustomerPerson`'dan önce» sıralaması gerçekten gerekli ve yeterli.
- **K-12'nin kendisi iyi görülmüş bir tuzak:** `AssignCustomersToUser` `user.assign_customers` istiyor (`AssignCustomersToUser.php:40`), dönüştüren satışçıda olmayabilir; doğrudan yazım + `reason: conversion` audit'i gerekçeli.
- **K-13 fonksiyonu `app.all_customer_ids()` deseninin birebir izdüşümü** (`2026_07_28_000200_create_customer_id_enumeration.php`: SECURITY DEFINER, search_path sabit, REVOKE PUBLIC, GRANT `config('tenancy.roles')`), yalnız boolean döndürüyor, hata mesajı kapsam dışı kaydın adını/sahibini vermiyor; `btrim` sınırı ölçümle kapatılıyor ve normalleştirme kararı Sets'e bırakılmış. Kapsam dışı veri sızdırma yok.
- **Dönüşüm tasarımı bugünkü `store`'un bilinen zafiyetini kapatıyor:** tek transaction, `lockForUpdate` ile çift tık, davet commit sonrası, başarısızlıkta hiçbir iz kalmaması testle şart koşulmuş.
- **Rollback bölümü dürüst:** `down()`'un canlı veriyi sileceği açıkça yazılı, veri varken şema geri alınmaz, dönüşümün geri alınamazlığı bilinçli ve çaresi adlandırılmış.
- Kapanış adresleri tablosu (açan kişinin atanması → EGT-219, tax_number tekilliği → ölçüm sonucuyla Sets'e) backbone kuralına uygun.

## ⚠️ Findings & Risks (in-scope for this review type)

**F-1 — K-3'ün uygulama mekanizması planda yok: `config/tenancy.php` `exempt_tables` ve ona bağlı iki test.** Bu kod tabanında «RLS'siz tablo» kendiliğinden olmaz: `TenantContextGuard::inspect` (`TenantContextGuard.php:160-204`) tenant context'i yokken koşan her sorguyu `config('tenancy.exempt_tables')` listesine bakarak reddeder — `potential_customers` ve `potential_customer_contacts` listede olmadan planın bütün action ve query'leri `TenantContextMissingException` fırlatır. Ayrıca `tests/Feature/OmurgaRls/SchemaConformanceTest.php:49-67` exempt olmayan her tabloda FORCE RLS arar (iki yeni tablo onu kırar) ve `tests/Arch/TenancyTest.php:85+` exempt listesini «plan değişikliği» olarak sabitler — yorumunda her ekleme plandan gerekçe ister. Plan K-3'ü karar olarak veriyor ama ne «Değişen» dosya haritasında `config/tenancy.php` var, ne `tests/Arch/TenancyTest.php` var, ne de Görev 1-2'de bu adım anılıyor. (Emsal: `legacy_*` tabloları aynı listeye gerekçe yorumuyla eklendi, `config/tenancy.php:90-119`.)

**F-2 — K-15 «kokpite düşer» vaadi, görüşmenin yaşam döngüsünü kapsamıyor: `CloseMeeting` potansiyel görüşmede çalışamaz ve dosya haritasında yok.** `CloseMeeting.php:68-77`: görüşmenin müşterisi `TenantContext::run([(int) $meeting->customer_id], …)` ile okunur — `customer_id` NULL olan potansiyel görüşmede bu `[0]` kapsamıyla boş döner ve `meeting_has_no_customer` ValidationException'ı fırlar; kapanış notu `AddCustomerNote`'a (müşteri ister) gider, yazma `TenantContext::forWrite($customer…)` içindedir. Yani kokpite düşen potansiyel görüşme ne «yapıldı» ne «iptal» edilebilir. Plan Görev 4'te «`CloseMeeting`'in müşteri varsayımı okunarak doğrulanır» diyor — doğrulama burada yapıldı, varsayım tutmuyor — ama «Değişen» listesinde `CloseMeeting.php` yok, potansiyel görüşmenin kapanış notunun nereye yazılacağı (hangi subject, hangi action) tanımsız ve test listesinde potansiyel görüşme kapatma senaryosu yok.

**F-3 — Maskeleme mekanizması yanlış aktarılmış; planın kendi kuralı planın verdiği örnekle çelişiyor.** Plan (Görev 3): «alan adında `phone`/`email` geçmesi yeterlidir (`Masker.php:53-56`)». Gerçekte `Masker::apply` (`Masker.php:28-40`) yalnız `config('audit.masked_fields')` listesindeki **tam ada** maskeler (`in_array … strict: true`; liste: `config/audit.php:141-144`); `str_contains` yalnız maske **şeklini** seçen `value()`'dadır ve `apply` kapısını geçmiş alanlara uygulanır. Sonuç: planın cümlesine güvenen bir implementasyonda `contact_phone` ya da `person_email` gibi bir audit anahtarı maskesiz, append-only tabloya ham yazılır ve AD-16'nın kendi cümlesiyle «raw value written once stays raw». Planın zorunlu kıldığı test tam anahtar adlarıyla yazılırsa yeşil kalır ve yanlış adlı üçüncü bir anahtarı yakalamaz.

**F-4 — Dönüşümdeki ikinci mükerrerlik kontrolü kendi kaydını istisna tutmuyor.** K-13: vergi no **açık bir potansiyelde** kayıtlıysa engellenir. Görev 5 adım 2: «`TaxNumberNotTaken` yeniden (form ile commit arası yarış)». Dönüşen kayıt o anda `open` durumundadır ve vergi numarası doluysa kural **kendisiyle eşleşir** — her dönüşüm reddedilir. Düzenleme için self-exclusion hem karar hem test olarak yazılmış («Düzenleme kendi vergi nosunu 'mükerrer' saymaz»), dönüşüm için ne kural ne test var; mutlu yol testi vergi numarasız fixture ile yeşil geçebilir ve boşluk canlıda çıkar.

**F-5 — Not yazma yetkisi iki yerde farklı tarif edilmiş.** Rota tablosu `.notes.store` için yeteneği `view` olarak verirken, dayanak gösterilen müşteri tarafı iki kapılıdır: `AddCustomerNote.php:40-41` `view`(customer) **ve** `create`(RecordNote) ister; `RecordNotePolicy::create` ise `quote.view`'a bağlıdır. Plan «müşteri tarafıyla aynı kural» diyor ama aynı kuralın ikinci kapısını yazmıyor. `quote.view` verilmemiş, yalnız `potential_customer.*` paketi almış bir kullanıcı için davranış tanımsız: rota tablosuna göre not yazar, «aynı kural»a göre 403 alır. Authorization yerleşimi bu noktada belirsiz (aynı belirsizlik `record_notes.kind` üzerinden görev/görüşme satırlarına da yayılmıyor — onlar net; yalnız not).

**F-6 — AD-05 ile şifrelenen temas telefon/e-postasının ekranda açık gösterimi için AD-21 erişim-logu kararı planda yok.** K-14 bu alanları «hassas» sınıfına koyuyor (`encrypted` cast); `phone` `config('audit.sensitive_fields')` içindedir (`config/audit.php:154-156`) ve kırmızı çizgi 13 «hassas kayda dokunan her OKUMA da loglanır» der; `AccessLogger` okuma noktalarının elle işaretlendiğini yazar (`AccessLogger.php:20-22`). Görev 6 Kişiler kartında telefon/e-postayı açık gösteriyor («maskeleme yalnız audit'te») ama ne bir erişim-logu noktası planlıyor ne de loglanmamasının bilinçli olduğunu söylüyor. Mevcut kodda emsal iki yöne de çekilebilir: `CustomerPeople.php:88` müşteri kişisinin telefonunu logsuz veriyor, buna karşılık belge/kimlik okumaları elle loglanıyor (`InstructorController.php:466`, `RevealInstructorIdentifier`). Plan bu çelişkide hangi tarafta durduğunu yazmalı; karar Sets'indir (CLAUDE.md «Çelişki/Belirsizlik»).

## ❓ Open Questions (to be answered before next stage)

1. **Varsayım (a)'nın doğrulama kapsamı:** PostgreSQL'de savepoint içinde verilen `SET LOCAL`, savepoint RELEASE edildikten sonra **dış transaction'ın sonuna kadar yaşar**. Dönüşümde adım 5'ten (`CreateCustomerPerson` → `forWrite`) sonra adım 6-8, `app.tenant_ids` yeni müşteriye set edilmiş hâlde koşar — bugünkü adımlar yalnız RLS'siz tablolara ve audit'e dokunduğu için zararsız, ama varsayım (a) testinin bu kalıntı kapsamı da görünür kılması bekleniyor mu? (Planın «test ile kanıtlanır» cümlesi bunu kapsıyorsa sorun yok; kapsamı netleştirin.)
2. K-20 paket taslağında satışçıya `convert` verilmiyor (yalnız satış yöneticisinde) — dönüşümü hep yönetici mi yapacak? Taslak backoffice'ten düzenlenebilir olduğu için karar deploy'u bağlamıyor; yine de Tayfun'un test prosedürü hangi hesapla dönüşüm deneyecekse rollout adımında adlandırılmalı.

## ⚠️ Upstream Findings

Yok. (F-1…F-6'nın hiçbiri üst karar bloklarını sorgulamıyor; hepsi planın kendi eksiği.)

## Verdict: ❌ REJECTED

Plan, bu projede gördüğüm en sağlam «koddan okunmuş mevcut durum» tablolarından birine oturuyor ve Sets'in dokuz kararının dokuzunu da doğru taşıyor; K-3 gerekçesi, K-12 sıralaması ve K-13 fonksiyon tasarımı özellikle isabetli. Ancak matrix'te bir ❌ (F-1 — exempt listesi olmadan planın hiçbir sorgusu çalışmaz, iki test kırılır) ve dört ⚠ var: potansiyel görüşmenin kapatılamaması (F-2) K-15'in vaadini yarım bırakıyor; F-3 ve F-6 kırmızı çizgi 13'ün iki yarısına (maskeli yazma, loglu okuma) dokunuyor; F-4 dönüşümün mutlu yolunu vergi numaralı her kayıtta kilitleyebilir; F-5 yetki yerleşimini iki yerde farklı anlatıyor. Bulgular çözülüp re-review istendiğinde plan onaylanabilir durumda.
