# Review — Potansiyel müşteri implementasyonu (EGT-216) — 2026-10-20

> **Review type:** impl-review
> **Previous review:** `docs/reviews/review-potansiyel-musteri-plan-re-review-3-2026-10-20.md` (heyet 812, plan v1.3 APPROVED)
> **Upstream document(s) read:** `docs/plans/potansiyel-musteri-plan.md` v1.3 (tamamı — K-1…K-25, Görev 1–7, kabul kriterleri 1–20), `proje-haritasi.md` §4.4 (kilitli Sets kararları), `CLAUDE.md` (17 kırmızı çizgi tek tek tarandı).
> **Other cross-referenced documents / code:** commit aralığı `2cc21385..712b86bd` (95 dosya) — `git log`/`git diff` ile gezildi; aşağıdaki matristeki her satır dosyadan okunarak doğrulandı. TeamCity: `summary 1819` → **SUCCESS, Tests passed: 2624**; ayrıca `finished` listesinde ana süit **#1818 SUCCESS (5016 passed, 20 ignored)** — iki yapılandırma da yeşil, araçtan doğrulandı. ⚠ Build↔revizyon eşlemesini API alanı boş döndüğü için araçtan doğrulayamadım; yeşil sonuçlar son biten build'lerdir ve brief'teki teslimle örtüşür.
> **Stage locks in effect:** Spec yok (Sets kararı) + Plan v1.3 kilitli. Brief'teki kilitli Sets kararları (ayrı tablo, uyarı-engel-değil, telefon açık+loglu, forOffice değil açık parametre, Satış menüsü 6 kalem, tarayıcı spec'i salt okur, 4 tasarım öğesi → EGT-221, takvim kapısı → EGT-219, mevcut çağıranlar → EGT-220) tartışılmadı.
> ⚠ **Süreç notu (bulgu değil, brief bildirdi):** teslim review'dan önce production'a çıktı; ticket review için geri alındı. Review bu metinde bağımsız üretildi — "yayında ve CI yeşil" hiçbir satırda kanıt yerine kullanılmadı.

---

## Zorunlu üç tarama

1. **Kırmızı çizgi taraması (17 madde):** 1-tenant leak: yeni tablolar kiracı verisi taşımıyor, reach policy'de (aşağıda) ✓ · 2-SET LOCAL: yeni hiçbir session-level SET yok, her şey `TenantContext` üzerinden ✓ · 3-yetki: her action kendi gate'ini soruyor (aşağıda dosya+satır) ✓ · 4-raw DB bypass: `DB::select` yalnız SECURITY DEFINER fonksiyon ve `group_memberships` (muaf) üzerinde ✓ · 5-data-loss migration: yok (yalnız ekleme) ✓ · 6-elle DDL: yok ✓ · 7-`env()`: diff'te yok ✓ · 8-credential: yok ✓ · 9-hardcoded iş kuralı: yok (durumlar yaşam döngüsüdür, limit/eşik yok) ✓ · 10-test silme: diff'te silinen test yok ✓ · 11-AI attribution: commit gövdelerinde yok ✓ · 12-hard delete: DELETE rotası yok, FK'ler `restrictOnDelete` ✓ · 13-audit: her yazma transaction içinde audit'li, hassas okuma loglu (K-22/K-23), maskeli (K-24) ✓ · 14-mevzuat: bu işte mevzuat kuralı yok, N/A ✓ · 15-kod yerleşimi: `PotentialCustomerLifecycle` framework'süz (`app/Domain/Sales/`), controller'daki `::query()->find` lookuplar mevcut kod tabanı teamülü (`ApprovalController.php:406`, `ContentPackageController.php:53` aynı desen; arch testleri yeşil) ✓ · 16-test disiplini: 5 yeni test dosyasının tamamı gruplu (`satis`/`musteri`/`omurga-rls`), `FrozenClock::INSTANT` kullanılıyor, RLS testleri `OmurgaRls` dizininde ✓ · 17-hazır bileşen: `PotentialCustomerForm.vue:163-200` ve `PotentialContactForm.vue:73-77` `PhoneInput`/`EmailInput`, yalnız `v-model`+`id`(+`kind`) — veri taşınmıyor ✓.
2. **Wire taraması (FEATURE teslimi):** 13 rota kayıtlı (`routes/office.php:572+`, auth'lu `/satis` grubunun içinde) ✓ · menü kalemi `OfficeNavigation.php:225` (`sales` grubu, `sales_calendar`'dan sonra, kendi policy'si) ✓ · policy konvansiyonla çözülüyor ve reach testleri bunu kanıtlıyor ✓ · lang TR+EN (`lang/{tr,en}/potential_customers.php`) ✓ · yardım makalesi + 4 gerçek-ekran görüntüsü + changelog + harita güncel (`docs/yardim/satis-potansiyel-musteri.json`, `public/images/yardim/satis-potansiyel-musteri*.png`, `changelog/2026-10-20.json`, `proje-haritasi.md`) ✓. ⚠ Tek wire boşluğu bulgulara yazıldı: **dönüşüm rotasına hiçbir test POST atmıyor** (F-1).
3. **Dil taraması:** PHP eklerinde ve 7 yeni Vue bileşeninde `t()`/yorum dışında Türkçe karakter taraması **0 sonuç**; tablo/kolon/sınıf/rota adları İngilizce; rota *yolları* Türkçe (`/satis/...`) — mevcut proje teamülü; kullanıcı metinleri `lang/` üzerinden ✓. Browser spec'teki Türkçe assertion'lar `arch-allow: turkish-content` işaretli ✓.

---

## Upstream Coverage Matrix

| Upstream madde | Durum | Kanıt (dosya + satır) |
|---|---|---|
| K-1 ayrı tablo, K-2 ayna değil | ✓ | `2026_10_31_000100_a_company_we_have_not_won_yet.php:41-101` — yalnız arama alanları; `customers`'a dokunulmadı |
| K-3 RLS yok + exempt mekanizması | ✓ | `config/tenancy.php:236-241` iki tablo gerekçe yorumuyla; `tests/Arch/TenancyTest.php` pinli sayı 155 + iki tablo; `PotentialCustomerReachTest` bağlamsız koşuyor |
| K-4 kapsam sahip üzerinden (AD-23) | ✓ | `PotentialCustomersInReach.php:77-100` — `groupsFor` aile bağlaması içinde (`:79`), null=sınırsız, []=own, grup=üyeler+kendisi; testler: own/group/staff-daralır/all/yetkisiz (`PotentialCustomerReachTest:60-101`) |
| K-5 açan sahip + devir ayrı yetki, hedef kapsamda | ✓ | `OpenPotentialCustomer` sahip=aktör; `HandOverPotentialCustomer.php:32` `mayHandTo` + `:39` `assign` gate; `mayHandTo` (`PotentialCustomersInReach.php:65-70`) plandaki `ScopeResolver::covers` ile **aynı anlamsal sonucu** tek kaynaktan veriyor (own→yalnız kendisi, group→üyeler, all→herkes); test `PotentialCustomerReachTest:103` |
| K-6 yaşam döngüsü | ✓ | `app/Domain/Sales/PotentialCustomerLifecycle.php` saf PHP; unit test 5 senaryo (`PotentialCustomerLifecycleTest`) + DB CHECK'leri (`000100:103-119`) plandan da sıkı (çift yönlü `=`) |
| K-7 dönüşüm satırı silmez, salt okunur | ✓ | `converted_*` kolonları + CHECK; `PotentialCustomerPolicy` her yazma yeteneğinde `isWritable`/`allows`; test `PotentialCustomerTest:247` |
| K-8 yetkili elle, temaslar taşınmaz | ✓ | `ConvertPotentialCustomerRequest` `CreateCustomerRequest` kurallarını aynen okuyor (`:30`); `ConversionTest:170` «never turns a contact into a company person»; browser spec `:60-62` kişi alanları boş |
| K-9 notlar/görevler yeniden bağlanır, görüşmede iki kolon | ✓ | `ConvertPotentialCustomer::moveTheWork` (`:150-165`) model-bazlı (toplu UPDATE değil); `ConversionTest:144` iki kolonu birden doğruluyor |
| K-10 tek transaction, davet sonra | ⚠ | Kod doğru: `ConvertPotentialCustomer:72` tek `DB::transaction`, davet controller'da commit sonrası `allows('update')` kapısıyla (`PotentialCustomerController.php:281-284`); atomiklik testli (`ConversionTest:184` — müşteri satırı enumeration kapısından aranıyor, potansiyel `open` kalıyor). **Ama davet yolu hiçbir testte yok** → F-1 |
| K-11 convert = convert + customer.create + customer.update | ✓ | `PotentialCustomerPolicy::convert` (`:114-122`); test `ConversionTest:215` |
| K-12 sahip (+aktör) atanır, CreateCustomerPerson'dan ÖNCE | ✓ | `ConvertPotentialCustomer:89` (`keepTheRelationship`) `:91`'den önce; audit `user.customers_assigned {reason: conversion}` açık `tenantId` ile (`:135-140`); regresyon testi `ConversionTest:115` ve `:127` |
| K-13 vergi no uyarı + ad + kendisi hariç + onay audit'e | ✓ | `TaxNumberRegistry` yalnız ad döndürür, href yalnız kapsam içine (`:84-96`, `:116-118`); `TaxNumberMatchAcknowledged` after-hook, asla kalıcı engel değil; iki Request de `exceptProspectId` geçiyor; testler: kapsam dışı ad + bağlantısız (`ReachTest:113` — Vitsan iki ad), açık/kapalı/kendisi ayrımı (`ReachTest:135`), HTTP uyarı+onay+audit (`PotentialCustomerTest:108`), düzenleme (`:134`), dönüşümde kendini saymaz (`ConversionTest:225`) |
| K-14 temas telefon/e-posta şifreli | ✓ | `PotentialCustomerContact` cast'leri (`:49-50`); ham sütun testi (`PotentialCustomerTest:81`) |
| K-15 görüşme takvim/kokpit + kapatma/iptal dahil | ⚠ | `SalesCalendar`/`SalesAgenda` potansiyel adı+bağlantı (diff: `potentialCustomer:id,name` eager + `prospectHref`); `CloseMeeting` iki dal tek `stamp` metodu (`CloseMeeting.php:71-73`, `closeForAProspect:` view gate + not önce + bağlamsız + `officeRecord: true`); dönüşmüş görüşme müşteri yolundan ✓; tamamlama testli (`PotentialCustomerTest:295-327`). **İptal dalı testsiz** → F-2. Takvim kapısının Quote policy'de kalması Sets kararıyla EGT-219'a adresli (kilitli) |
| K-16 teklif/fırsat/eğitim kapalı | ✓ | `SchedulePotentialCustomerMeeting:50-58` `quote_id`/`opportunity_id` yazmıyor; yapısal ayrım (ayrı tablo); `PotentialCustomerTest:358` müşteri listesinde yok |
| K-17/tasarım: liste 02, sayfa 32 alt kümesi, 4 öğe, Satış grubu | ✓ | `Index.vue` DataTable+SlideOver+Banner; `Show.vue` 32'nin alt kümesi; 32'de olup sayfada olmayan 4 öğe **Sets kararıyla EGT-221'e** (kilitli sapma 7); menü `OfficeNavigation.php:225`; Satış 6 kalem istisnası adlandırılmış (`ReachableFeatureTest` diff: `$ceilings = ['sales' => 6]`, Sets 20.10.2026 yorumuyla) |
| K-18 ortak organizmalar iki sayfada | ✓ | `NoteForm`/`TaskForm`/`MeetingScheduleForm` — `Customers/Show.vue` (diff) ve `PotentialCustomers/Show.vue:294-296` ikisi de kullanıyor; adlar plandan farklı (kart değil form) — brief'te bildirilen, işlevi karşılayan sapma |
| K-19 müşteri kartında geçmiş bağlantısı | ✓ | `PotentialCustomerOrigin.php:28-42` (okuyanın reach'ine bağlı); `CustomerController.php:167,178`; browser spec `:82` bağlantıyı gerçek sayfada doğruluyor. (Plan `CustomerDetail` içinde diyordu; ayrı Query sınıfı — AD-25'e uygun eşdeğer) |
| K-20 paket taslağı | ✓ | `PermissionTemplateSeeder` diff: satışçı `view.own/create/update`, yönetici `view.group/assign/convert`(+direktör `view.all`); rollout "deploy yetki vermez" planda |
| K-21 not = view, quote.view sorulmaz | ✓ | `PotentialCustomerPolicy::note` (`:83-86`); rota `.notes.store` → `note`; test `PotentialCustomerTest:195` quote yetkisiz kullanıcı not yazıyor. ⚠ `note`/`follow` ek olarak `isWritable` ister — planın kendi içindeki çelişkinin ("note=view" ↔ "dönüşmüş kayıtta her yazma 403") sıkı yöne, kodda gerekçesiyle çözümü (`PotentialCustomerLifecycle:66-75` yorum: kapalı kayıt önce yeniden açılır); `reopen` yolu var, davranış testli (`:226`, `:247`) |
| K-22 temas telefonu açık + erişim logu (officeRecord) | ✓ | `PotentialCustomerCard.php:49` yalnız telefonlu temas varken, `officeRecord: true`; testler: tek-müşterili satışçıda `tenant_id` NULL (`PotentialCustomerTest:257-283` — `TenantScope` tek müşteriye pinlenerek), telefonsuzda satır yok (`:285`) |
| K-23 aynı kural müşteri kartına | ✓ | `CustomerPeople.php:84-97` sorgunun içinde, `TenantContext::run` bağlamında; `CustomerPeopleAccessLogTest` üç senaryo (kart, telefonsuz, teklif ekranı) `musteri` grubunda |
| K-25 kiracısız audit açık parametre + doğrulanan beyan | ✓ | `AuditWriter.php` diff (`officeRecord` + `tenantId` çakışmasında `LogicException` + bayraksız davranış değişmez), `AccessLogger.php` diff (aynı), `AuditDriver::tenantOf` diff (`OfficeRecord`+`belongsToNoCustomer` → `assertOfficeRecord` → NULL), `PersistsAuditRows::assertOfficeRecord` (subject şart, tablo exempt olmalı, `tenant_id` özniteliği taşımamalı); üç ortak model konuya bakıyor (`RecordNote:71`, `Task:106`, `SalesMeeting:122` — dönüşünce `customer_id` dolu→false); `TenantContext::forOffice` **kullanılmamış** ✓. Testler: `OfficeRecordAuditTest` 9 senaryo — fallback'in değiştirdiği davranış, üç yazıcıda beyanlı NULL, paylaşılan notun konuya göre cevabı (müşteri notu kiracısını korur), kiracılı konuya beyan → `LogicException` + satır yok, tenant'la çakışan beyan, subject'siz beyan, maskeli model izi; `ConversionTest:242` taşınan işin audit'i müşteri kiracısında + dönüşüm olayı NULL |
| K-24 maskeleme tam ad | ✓ | Tüm audit payload'larında anahtarlar tam `phone`/`email` (`AddPotentialCustomerContact:42-43`, `UpdatePotentialCustomerContact:32,40`); test ham değeri `audit_writes`'ta arıyor (`PotentialCustomerTest:81-106`, `OfficeRecordAuditTest:161`) |
| Görev 1 (şema/model/factory/durum) | ✓ | 3+1 migration (4.sü FK indeksleri — `ForeignKeyIndexTest` bulgusu, yalnız ekleme), factory'ler `closed()`/`converted()` state'li, CHECK testleri migration'da DB seviyesinde; dördüncü migration bildirilen sapma |
| Görev 2 (yetki/kapsam/fonksiyon) | ✓ | İzinler `config/authorization.php:318-324`; matris satırı + `decision` grubunda assign/convert (diff); fonksiyon envantere eklendi (`DatabaseInventoryTest` diff); `CustomersWithTaxNumberTest` 4 test: kapsam dışına ad, yalnız ad kolonu, btrim+boş sayı, PUBLIC çalıştıramaz — uygulama rolüyle, `omurga-rls` |
| Görev 3 (açma/düzenleme/temas/kapatma/devir) | ✓ | 8 action: policy→transaction→yazma→audit (`officeRecord: true`), hepsi grep ile satır satır doğrulandı; HTTP testleri: yetkisiz 404 her yazmada (`PotentialCustomerTest:145`), yetkisiz liste 403 (`:157`), kapat/yeniden aç (`:226`) |
| Görev 4 (not/görev/görüşme/takvim/CloseMeeting) | ⚠ | 1-4 ✓ (yukarıda); 5'in iptal dalı testsiz → F-2 |
| Görev 5 (dönüşüm) | ⚠ | Akış plandaki sırayla (`ConvertPotentialCustomer:72-117`); iki varsayım testle kanıtlı ((a) atomiklik `ConversionTest:184`, (b) `ConversionTest:115`); **davet + HTTP yolu testsiz** → F-1 |
| Görev 5b (erişim logları + K-25 testleri) | ✓ | Yukarıda K-22/K-23/K-25 satırları; `MyQuotes` quote.read'ine dokunulmadı (diff'te yok), mevcut testleri CI'da yeşil |
| Görev 6 (ekranlar) | ✓ | Liste/sayfa/organizmalar/menü yukarıda; browser spec salt-okur (Sets kararı — yazan planlı adımlar feature testlere taşındı, spec yorumunda gerekçeli `potential-customers.spec.js:8-13`), console error yakalama `:34-35`, 375 kart doğrulaması `:42-43` locator notu + iki proje |
| Görev 7 (yardım/changelog/harita) | ✓ | Dosyalar diff'te; ekran görüntüleri spec içinden gerçek ekrandan (1440) alınıyor (`shoot()`, `:23-29`) |
| Kabul 1-8, 11-16, 18-20 | ✓ | Her biri yukarıdaki satırlara eşlenik; 2 yapısal + `:358`; 5 reach testleri; 12 `ConversionTest:115`; 13 şifreli+maskeli; 14 atomiklik testi + tekdüze kod yapısı; 15 organizma çıkarımı sonrası CI'da mevcut müşteri testleri yeşil (#1818, 5016) + browser spec müşteri kartını açıyor (`:67-83`); 20 `TenancyTest` + bağlamsız sorgu |
| Kabul 9 («davet gider» dahil mutlu yol) | ⚠ | Dönüşüm action-seviyesinde eksiksiz testli; **davet** kanıtsız → F-1 |
| Kabul 10 (çift dönüşüm / kilit) | ✓ | Sıralı ikinci deneme testli (`ConversionTest:205` — bayat model örneğiyle, kilit altında yeniden okumayı zorluyor); **gerçek paralel test** `satis` grubunda koşamaz: `RefreshDatabase` transaction sarmalaması ikinci bağlantıya satırları göstermez — testin kendi yorumu aynı kısıtı belgeliyor (`ConversionTest:198-201`). Mekanizma kodda (`lockForUpdate`+`isOpen` yeniden okuma `:75-79`); planın paralel maddesinin test edilebilir karşılığı budur |
| Kabul 17 (tamamlanır VE iptal edilir) | ⚠ | Tamamlama testli; iptal testsiz → F-2 |

---

## ✅ What works

- **K-25 implementasyonu planın harfiyen karşılığı ve testi örnek nitelikte:** üç yazıcıda bayraksız davranış bit-bit korunmuş (`$officeRecord ? null : (eski ifade)`), beyan üç ayrı açıdan doğrulanıyor (subject şart / tablo exempt / tenant'sız), ve `OfficeRecordAuditTest:60` önce **düzelttiği hatayı gösterip** sonra düzeltmeyi kanıtlıyor. Paylaşılan not testinin müşteri dalını da pinlemesi (`tenants === [null, $customerId]`) F-7 ailesinin regresyonunu kapatıyor.
- **Dönüşümün atomiklik testi** (`ConversionTest:184`) iddialı bir tuzağı da çözmüş: müşteri satırının yokluğunu kendi transaction'ının göremeyeceği bağlantıdan değil, SECURITY DEFINER enumeration kapısından soruyor — ve bunu yorumuyla açıklıyor.
- **Kapsam dışına sızıntı yok:** fonksiyon yalnız ad döndürüyor (test: «returns a name and no other column»), href iki kayıt türünde de okuyanın reach'ine bağlı, boş/boşluklu numara güvenli (`btrim(candidate) <> ''` fonksiyonun kendi gövdesinde).
- **Bilinen 8 sapmanın 8'i de ya plan ruhuna eşdeğer ya Sets kararıyla adresli** ve hiçbiri sessiz değil: arch testlerine adlandırılmış istisna olarak işlenmiş (menü 6 kalem), kapanış adresleri (EGT-219/220/221) harita ve plana yazılmış.
- **Tarayıcı spec'i kilitli "yalnız okur" kuralını kendi yorumunda gerekçelendiriyor** ve dönüşüm panelini açıp **göndermeden** kapatıyor; yardım görüntüleri gerçek ekrandan.

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

**F-1 — Dönüşümün HTTP yolu ve commit-sonrası davet hiçbir testte yok; planın «davet gider (Mail::fake …)» test maddesi yazılmamış.** [CONFIRMED] Tüm dönüşüm testleri action'ı doğrudan çağırıyor (`app(ConvertPotentialCustomer::class)(…)`); depoda `musteriye-donustur` rotasına POST atan tek test yok (grep: tests/ içinde 0 sonuç) ve `tests/Feature/Satis/` içinde `Mail::fake`/`Notification::fake` yalnız `QuoteTest`'te. Kapsam dışı kalan gerçek yüzey: (a) `ConvertPotentialCustomerRequest`'in kural yayılımı ve `after()` hook'unun opak-id çözümü, (b) `PotentialCustomerController::convert`'in DTO alan eşlemesi (`:263-279`), (c) **davetin kendisi** — `:281-284`'teki `Gate::allows('update', $customer)` kapısı commit sonrası, taze scope çözümüne dayanıyor; tam da bu teslimde iki kez (810 F-7, v1.3) yakalanan "istek kapsamı yanlış cevap verir" hata sınıfının yaşadığı yer. Bu satırlar sessizce kırılırsa CI yeşil kalır, yetkili davetsiz kalır ve bunu ilk fark eden müşteri olur. Kabul kriteri 9'un davet yarısı ve Görev 5 mutlu yol maddesinin `Mail::fake` şartı karşılanmamış.

**F-2 — Kabul kriteri 17'nin «iptal edilir (gerekçeyle)» yarısı testsiz: `CloseMeeting::closeForAProspect`'in iptal dalı hiçbir test tarafından koşulmuyor.** [CONFIRMED] `PotentialCustomerTest:295-327` yalnız tamamlama + kapanış notunu doğruluyor; `tests/Feature/Satis/` ve `OfficeRecordAuditTest` içinde potansiyel görüşme için `cancelled_at`/`cancelled_reason` geçen tek assertion yok (grep: 0 sonuç). İptal dalı tamamlamadan farklı girdiler taşıyor (gerekçe zorunluluğu, not yazılmaması) ve `meeting.cancelled` audit'ini `officeRecord: true` ile yazıyor — bu kombinasyon hiç çalıştırılmamış kod yolu olarak üretimde duruyor. Görev 4 test listesi («iptal edilir (gerekçeyle)») ve kabul 17 açıkça istiyordu.

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

1. Kabul 10'un «iki paralel istek» maddesinin gerçek iki-bağlantılı kilit testi `satis` grubunda yapısal olarak koşamıyor (RefreshDatabase izolasyonu — testin kendi yorumu `ConversionTest:198-201` aynı kısıtı belgeliyor). Matris bunu bayat-model proxy'siyle ✓ saydı; Sets isterse `omurga-rls` grubunda iki bağlantılı bir kilit testi ayrı iş olarak açılabilir — karar Sets'in.

## Verdict: ❌ REJECTED

Gövde sağlam: K-1…K-25'in tamamı koda birebir inmiş, üç zorunlu tarama temiz, sekiz bilinen sapmanın hepsi gerekçeli/adresli, K-25 ve atomiklik testleri örnek seviyede; CI iki yapılandırmada yeşil (#1819: 2624, #1818: 5016 — araçtan doğrulandı). Ancak matriste iki ⚠ kaldı ve ikisi de planın **açıkça istediği** testlerin yokluğu: dönüşümün HTTP yolu + commit-sonrası davet kapısı tamamen test dışı (F-1 — özelliğin ana kullanıcı yolculuğunun kablosu kanıtsız) ve potansiyel görüşme iptali hiç koşulmamış kod yolu (F-2 — kabul kriteri 17'nin yarısı). İkisi de dar, eklemeli düzeltmelerdir; kapatılıp re-review istendiğinde gövdeye dokunmadan onaylanabilir.

```
docs/reviews/review-potansiyel-musteri-impl-2026-10-20.md
```
