# Potansiyel müşteri — implementasyon planı

*v1.3 · 20.10.2026 · EGT-216 · heyet 809 (6 bulgu), 810 (1 bulgu), 811 (APPROVED) · v1.3 uygulama başında bulunan audit kiracı genişlemesi · Spec turu yoktur (Sets, 19.10.2026: "tablo yapıları net, proje zaten çalışıyor; spec yazılabilecek değil, plan yazılabilecek netlikte") · Kararların kaynağı: `proje-haritasi.md` §4.4 «Potansiyel müşteri ayrı bir yapıdır» · İlgili: EGT-219 (şube yapısı)*

**Hedef:** satışçı, henüz müşterimiz olmayan bir firmayı **yetkili tanımlamadan** kaydedebilsin; arasın, görüşme notu ve görev tutsun, görüşme planlasın. Firma dönerse kayıt **müşteriye dönüştürülür**: yetkili elle girilir, firma kiracı olarak açılır, potansiyel dönemin notları/görevleri/görüşmeleri müşteride devam eder; potansiyel kayıt silinmez, «müşteriye dönüştü» olarak tarihçede kalır.

**Mimari:** iki yeni tablo (`potential_customers`, `potential_customer_contacts`), `sales_meetings`'e iki nullable kolon, bir `SECURITY DEFINER` fonksiyon (mükerrer vergi no sorusu). Potansiyel müşteri **kiracı değildir**: `quotes`, `opportunities`, `record_notes`, `sales_meetings` gibi 3E'nin kendi kaydıdır, satır güvenliği yoktur, erişim **policy + kapsam sorgusu** ile korunur. Müşteriye dönüşüm mevcut açılış zincirini (`OnboardCustomer` → `CreateCustomerPerson` → `InviteCustomerPerson`) **yeniden yazmadan** çağırır.

**Stack:** Laravel 13 · PHP 8.5 · PostgreSQL · Inertia + Vue 3 · Pest · Playwright

---

## Doğrulanmış mevcut durum

⚠ Bu tablo 19.10.2026'da **koddan okunarak** üretildi.

| Ne | Durum | Kanıt |
|---|---|---|
| `customers` kolonları | `name`, `legal_type`, `kind_option_value_id`, `tax_office`, `tax_number`, `national_id` (şifreli), `default_locale`, `timezone`, `compliance_warning_days`, `status`, `price_group_id` — **adres/telefon/il yok** | `2026_07_27_000200_create_customer_identity.php:73-109`, `2026_07_28_060000_create_pricing.php:148`, `2026_10_12_000100_a_company_has_a_kind_of_its_own.php:42` |
| `customers` = kiracı | açılışta yetki satırları kurulur (`Tenancy\CreateCustomer` → `ProvisionTenantAuthorization`), RLS kendi `id`'si üzerinden | `app/Actions/Office/OnboardCustomer.php`, `musteri-yonetimi-plan.md` «Doğrulanmış mevcut durum» |
| `customers`'ı okuyan kod | `app/` içinde 33 dosya, 46 sorgu | `grep -rn "Customer::query()\|from('customers')\|table('customers')\|all_customer_ids" app` |
| Müşteri açma | `CustomerController::store`: firma + **zorunlu** yetkili (`person_name`, `person_email` required) + davet; üç adım **tek transaction değil** | `CustomerController.php:126-154`, `CreateCustomerRequest.php` |
| Müşteri durumları | yalnız `active` / `inactive` | `ChangeCustomerStatus.php:30` |
| Vergi no tekilliği | **yok** — ne index ne doğrulama | `CreateCustomerRequest.php`, `create_customer_identity.php` (yalnız `customer_users` e-posta hash'i unique) |
| Açanın atanması | **yok** — `OnboardCustomer` kimseyi `office_user_customers`'a yazmaz | `OnboardCustomer.php`; `SessionScopeResolver.php` (`customer.view.all` yoksa yalnız atamalar) |
| Atama action'ı | `AssignCustomersToUser` → `user.assign_customers` ister | `AssignCustomersToUser.php:40` |
| Kişi ekleme | `CreateCustomerPerson` → `update` policy'si (`customer.update` + kapsam) | `CreateCustomerPerson.php:90` |
| Görüşme notları | `record_notes` — çok biçimli (`subject_type`/`subject_id`), **kiracıya bağlı değil**, türler `phone/visit/internal/note` | `2026_09_03_000100_create_record_notes.php` |
| Görevler | `tasks` — çok biçimli `subject`, tür `TaskKind` (`customer.follow_up` var) | `2026_08_13_000300_create_tasks.php`, `TaskKind.php:200`, `OpenCustomerTask.php` |
| Planlı görüşme | `sales_meetings` — `customer_id` **nullable**, `customer_user_id`, `owner_user_id`, `kind` (`call/visit/online` CHECK), kiracıya bağlı değil | `2026_09_22_000200_a_seller_plans_their_day.php` |
| Takvim ve kokpit | `SalesCalendar` ve `SalesAgenda` görüşmeleri okur, firma adını `customer_id` üzerinden çözer | `SalesCalendar.php:140-147`, `SalesAgenda.php:461-501` |
| Müşteri kartı aktivitesi | `CustomerCard::activityOf()` — not, yazışma, teklif | `CustomerController::activity`, `app/Queries/Office/CustomerCard.php` |
| Grup kapsamı | `Support\Authorization\ScopeResolver::groupsFor/covers` — `own`/`group`/`all`, seviye `group`'u daraltır | `app/Support/Authorization/ScopeResolver.php` |
| Kapsam okuma deseni | tek sınıf, her listede aynı kural: `TrainingsInScope::apply()` (aile bağlaması içinde) | `app/Queries/Office/TrainingsInScope.php` |
| Yetki satırları | `PermissionRows` talep anında `findOrCreate` eder; paketler `PermissionTemplateSeeder`'da **taslak** | `app/Support/Authorization/PermissionRows.php:84`, `database/seeders/PermissionTemplateSeeder.php` |
| Şifreleme (ofis tarafı) | Laravel `encrypted` cast — `Instructor::national_id` | `app/Models/Instructor.php:68` |
| `SECURITY DEFINER` deseni | `app.all_customer_ids()` — `REVOKE … FROM PUBLIC`, `GRANT EXECUTE` uygulama rollerine | `2026_07_28_000200_create_customer_id_enumeration.php` |
| Menü | `OfficeNavigation`: `customers` → `management` grubu; satış grubu `sales_agenda`, `sales_calendar`, `quotes`, `packages`, `invoices` | `OfficeNavigation.php:215-262` |
| Satış rotaları | `/satis/...` (`/satis/teklifler`) | `routes/office.php:567` |
| Liste deseni | `Customers/Index.vue`: `DataTable` + `SlideOver` + `CustomerForm` (post `/musteriler`) | `Index.vue`, `organisms/CustomerForm.vue:140-145` |
| Müşteri kartı | `Customers/Show.vue` 1264 satır, not/görev/görüşme bölümleri **sayfa içinde** | `Show.vue:448-830` |
| İl listesi | `OptionList::PROVINCE` (`IL`), `ActiveValueOfOptionList` kuralı | `OptionList.php:39`, `CreateCustomerRequest.php` |
| Yardım / changelog | `docs/yardim/*.json` (43 dosya, `satis-musteri-karti.json` dahil), `changelog/*.json` | `ls docs/yardim`, `app/Queries/Office/Changelog.php:60` |
| Son migration | `2026_10_30_000100_an_answered_offer_says_who_answered_it.php` | `ls database/migrations` |
| Kiracısız tablolar | `config('tenancy.exempt_tables')` listesinde — `opportunities`, `sales_meetings`, `record_notes` dahil; listede olmayan tabloya bağlamsız sorgu `TenantContextGuard::inspect` tarafından **reddedilir**; `tests/Arch/TenancyTest.php:32` her modelin ya kiracılı ya muaf olmasını ister | `config/tenancy.php:90-253`, `TenantContextGuard.php:160-175` |
| Görüşme kapatma | `CloseMeeting` müşteri varsayar: `customer_id` boşsa `meeting_has_no_customer`, kapanış notu `AddCustomerNote` ile müşteriye, yazma `forWrite(customer)` içinde | `CloseMeeting.php:60-110` |
| Audit maskelemesi | `Masker::apply` yalnız `config('audit.masked_fields')` listesindeki **tam ada** uyan üst seviye anahtarı maskeler (`national_id`, `phone`, `email`, `iban`…); `str_contains` yalnız maskenin biçimini seçer | `Masker.php:28-40`, `config/audit.php:141-144` |
| Hassas okuma | `audit.sensitive_fields` = `national_id`, `social_security_no`, `iban`, `phone`; okuma noktası `AccessLogger::record($subject, $fields, $context)` ile **elle** işaretlenir | `config/audit.php:146-156`, `AccessLogger.php:34` |
| Firma kişilerinin telefonu | `CustomerPeople::for()` telefonu açık döndürüyor, **erişim logu yok**; okuyanlar müşteri kartı (`CustomerDetail`) ve teklif ekranı (`QuoteController`) | `CustomerPeople.php:59-95` |
| Not yazma yetkisi | `AddCustomerNote`: `view(customer)` + `create(RecordNote)`; `RecordNotePolicy::create` = `quote.view` | `AddCustomerNote.php:40-41`, `RecordNotePolicy.php:41-44` |

---

## Global kısıtlar

Her görevin gereksinimleri bunları **kapsar**; görev metinlerinde tekrarlanmaz.

- **Dil:** kod İngilizce, arayüz Türkçe (`lang/tr/`, `lang/en/` eşi). Koda gömülü kullanıcı metni yok.
- **Test grubu:** `satis` (AD-26). Çıplak `now()` yok — `Tests\FrozenClock::INSTANT`. Factory zorunlu.
- **Kod yerleşimi (AD-25):** okuma `app/Queries/Office/`, yazma `app/Actions/Sales/`, doğrulama `app/Http/Requests/Office/`, yetki `app/Policies/`, durum geçişi `app/Domain/Sales/`. Controller ince.
- **Tarayıcıya id gitmez** — `OpaqueId::encode('potential_customer' | 'potential_customer_contact', id)`; tip adı serbest metindir, kayıt listesi yoktur (`OpaqueId.php:77`).
- **Audit (AD-06, kırmızı çizgi 13):** her yazma işlemi append-only kayıt üretir, **işlemle aynı transaction'da**; potansiyel kayda ait her `AuditWriter`/`AccessLogger` çağrısı `officeRecord: true` taşır (K-25); telefon/e-posta audit'e **maskeli** girer (AD-16).
- **Silme yok (AD-11 ruhu):** potansiyel müşteri ve temas kişisi silinmez; kapatılır / dönüştürülür. Hiçbir rota `DELETE` açmaz.
- **Hazır bileşen (kırmızı çizgi 17):** telefon `PhoneInput`, e-posta `EmailInput`; bileşene veri taşınmaz.
- **Tasarım:** liste `design/02` deseni, sayfa `design/32`'nin alt kümesi. Kod yazılmadan önce `32` baştan sona okunur ve envanter çıkarılır (CLAUDE.md «Tasarım Uygulama Protokolü»); teslimden önce kapsama matrisi.
- **Kalite kapısı:** `pint --dirty` → `composer analyse` → dokunulan testler (`--filter`) → commit. Tam koşu yalnız ticket kapanışında.

---

## Kararlar

| # | Karar | Gerekçe |
|---|---|---|
| K-1 | **Ayrı tablo**, `customers`'ta durum değil | Sets, 19.10.2026. `customers` kiracıdır; 46 sorgunun her birine "potansiyelleri hariç tut" yazmak gerekirdi, unutulan tek yer potansiyel firmayı teklif seçicisine sokardı. Ayrı tablo bunu yapısal olarak imkânsız kılar. |
| K-2 | **Ayna tablo değil** — yalnız arama sürecinin alanları | Ayna, `customers`'a yapılan her şema değişikliğinin ikinci kez yapılması demektir; yapılmadığı gün dönüşüm sessizce veri kaybeder. Dönüşümde eksik zorunlu alanlar formda tamamlanır. |
| K-3 | Potansiyel müşteri **kiracı değildir, RLS yoktur** | Kiracı firmanın kendi verisidir ve portalı vardır; potansiyel müşteri 3E'nin kendi satış kaydıdır (`opportunities`, `sales_meetings`, `record_notes` ile aynı sınıf, üçü de RLS'siz). Erişimi policy + `PotentialCustomersInReach` korur. ⛔ Bu, kırmızı çizgi 1'in istisnası değildir: tabloda müşteri (kiracı) verisi yoktur. **Mekanizması:** iki tablo `config/tenancy.php` `exempt_tables`'a, `sales_meetings` bloğunun yanına, gerekçe yorumuyla eklenir — aksi hâlde `TenantContextGuard` her sorguyu reddeder ve `TenancyTest` modeli sınıflandırılmamış bulur (heyet 809 F-1). |
| K-4 | Kapsam **sahip üzerinden** okunur: `potential_customer.view.{own,group,all}` | AD-23'ün kendi deseni. `own` = `owner_user_id` benim; `group` = sahibi `ScopeResolver::groupsFor(…, 'potential_customer.view.group')` düğümlerinden birinde üye; `all` = herkes. ⚠ EGT-219'un şubeleri bu sayede **ek kod istemeden** çalışır: İzmit personeli `group` + İzmit düğümüyle yalnız İzmit'in kayıtlarını görür. |
| K-5 | Açan kişi **otomatik sahiptir**; sahip değişikliği ayrı yetkidir (`potential_customer.assign`) | Bir önceki ölçümde müşteri tarafında bulunan "açan kişi açtığını göremez" hatası burada baştan önlenir. Kaydı başkasına vermek, şube yapısında kapsam değiştiren bir eylemdir; düzenleme yetkisine katlanmaz. Hedef kişi de verenin kapsamında olmalıdır (`ScopeResolver::covers`). |
| K-6 | Durumlar: `open` → `converted` (son) · `open` ⇄ `closed` | `closed` = «ilgilenmiyor», gerekçe zorunlu, yeniden açılabilir. Dönüşüm geri alınmaz: kiracı açılmıştır. Geçiş kuralları tek yerde: `app/Domain/Sales/PotentialCustomerLifecycle.php`. |
| K-7 | Dönüşüm **satırı silmez**; `converted_customer_id`, `converted_at`, `converted_by` yazılır | Sets: dönüşüm metrikleri ve tarihçe. Dönüşmüş kayıt salt okunurdur. |
| K-8 | Dönüşümde yetkili **elle** girilir; temas kişileri önerilmez ve taşınmaz | Sets, 19.10.2026 — satış temasıyla yetkili aynı kişi olmak zorunda değil; yanlış kişiye panel erişimi verilmesin. Temaslar potansiyel kayıtta salt okunur tarihçe olarak kalır. |
| K-9 | Notlar ve görevler **yeniden bağlanır** (`subject` müşteriye çevrilir); görüşmelere `customer_id` yazılır, `potential_customer_id` **korunur** | Kopya yok (Sets onayı). Notun potansiyel döneme ait olduğu `created_at < converted_at` ile türetilebilir; görüşmede iki kolon birlikte kalır, "dönüşmeden önce kaç arama" sorusu cevaplanabilir. |
| K-10 | Dönüşüm **tek transaction**; davet transaction'dan **sonra** | Bugünkü `store` üç adımı ayrı yapıyor; dönüşümde yarım kalan iş "firma açıldı ama potansiyel hâlâ açık" demektir ve ikinci tık ikinci firmayı açar. Davet bir e-postadır, commit'ten önce gitmez. Davet kuralı `store` ile aynı: aktör `update` yetkisi taşıyorsa gönderilir. |
| K-11 | Dönüşüm yetkisi: `potential_customer.convert` **ve** `customer.create` + `customer.update` | Kiracı açmak `customer.create`'in kararıdır, yetkili eklemek `customer.update`'in (`CreateCustomerPerson:90`). Dönüşüm bunları devralmaz, ister. |
| K-12 | Dönüşümde sahip (ve farklıysa dönüştüren) yeni müşteriye **atanır** — `office_user_customers`'a doğrudan, audit'li | `AssignCustomersToUser` `user.assign_customers` ister; dönüştüren satışçıda bu yetki olmayabilir. Bu atama "başkasına yetki vermek" değil, kişinin zaten yürüttüğü ilişkiyi kaybetmemesidir — gerekçe audit kaydına `reason: conversion` olarak yazılır. ⚠ Atama, `CreateCustomerPerson` çağrılmadan **önce** yazılır; aksi hâlde `customer.view.all`'u olmayan dönüştüren kişi `update` policy'sinde düşer. |
| K-13 | Vergi no eşleşmesi **uyarıdır, engel değildir** *(v1.1 — Sets, 19.10.2026)*. Eşleşme **müşterilerde** (her durum) ve **açık potansiyellerde** aranır, **kaydın kendisi hariç**. İlk gönderimde eşleşme varsa kayıt yapılmaz, uyarı döner; kullanıcı «Yine de kaydet» ile `acknowledged_tax_number_match=1` gönderirse kayıt açılır ve onay audit'e (`tax_number_match_acknowledged: [adlar]`) yazılır. **Uyarı firmaların adını yazar**, okuyanın kapsamı dışında olsa bile; başka bilgi ve bağlantı yalnız kapsam içindekilere. | Sets: *"Vitsan — ödemeyi yapan tek çatı firma, altında 3 firma; vergi numarasında katı yaklaşmadık"*; *"firma adı gizli bilgi değil, müşteri ekleme ekranını açabilen kişiler adı görebilir; içeride konuşma şansları olur."* Müşteriler RLS arkasında olduğu için ad `app.customers_with_tax_number(text) RETURNS SETOF text` (`SECURITY DEFINER`) ile sorulur — **yalnız ad** döner, id/durum/başka kolon dönmez. Kendini hariç tutma hem düzenlemede hem dönüşümde geçerlidir: dönüşümde potansiyelin kendisi `open`'dır ve dışlanmazsa her dönüşüm kendine uyarı verirdi (heyet 809 F-4). Vergi no boşsa kontrol yok; ad benzerliği kapsam dışı. |
| K-14 | Temas kişisinin telefonu ve e-postası `encrypted` cast ile saklanır | AD-05; ofis tarafı tablo, kiracı anahtarı yok (`Instructor` deseni). Arama bu kolonlar üzerinden yapılmaz. |
| K-15 | Planlı görüşme potansiyel müşteriye bağlanabilir ve **mevcut Takvimim / Satış kokpiti**'ne düşer — **kapatma ve iptal dahil** | Sets, 19.10.2026. Yeni ekran açılmaz; takvim ve kokpit firma adını potansiyel kayıttan da çözer, tıklama potansiyel sayfasına gider. ⚠ `CloseMeeting` müşteri varsayıyordu (heyet 809 F-2): görüşmenin **konusu** artık müşteri ya da potansiyeldir; kapanış notu konuya göre `AddCustomerNote` ya da `AddPotentialCustomerNote` ile yazılır, potansiyel görüşmede tenant bağlamı açılmaz. Dönüşmüş potansiyelin görüşmesi `customer_id` taşıdığı için müşteri yolundan kapanır. |
| K-16 | Fırsat, teklif, eğitim ataması **kapalı** | Sets, 19.10.2026. Ayrı tablo bunu yapısal olarak sağlar: bu akışların hepsi `Customer` alır. |
| K-17 | Ekran: menüde **Satış** grubu; liste `02`, sayfa `32`'nin alt kümesi; çizilmemiş dört öğe mevcut parçalarla | Sets, 19.10.2026. Dört öğe: menü kalemi, «Müşteriye dönüştür» yan paneli, «Dönüştü» rozeti, mükerrer uyarısı (`Banner`). |
| K-18 | Not/görev/görüşme bölümleri `Customers/Show.vue`'dan **organizmaya çıkarılır** ve iki sayfada kullanılır | Kod yerleşimi §3: iki sayfada kullanılan parça organizmadır. Kopya, iki sayfanın ilk düzeltmede ayrışması demektir. Müşteri kartının davranışı değişmez — mevcut testler ve tarayıcı spec'i korur. |
| K-19 | Müşteri kartına «Potansiyel müşteri geçmişi» bağlantısı | Sets onayı (K-8 ile birlikte): temasların durduğu yere giden yol. `32`'de çizili değil; başlıktaki bilgi satırına tek bağlantı olarak eklenir. |
| K-21 | Potansiyel müşteriye not yazmak **yalnız potansiyelin `view`'ını** ister; `RecordNotePolicy::create` (`quote.view`) burada sorulmaz | Heyet 809 F-5. Not, kaydı görebilenin "onunla konuştum" demesidir (`AddCustomerNote:36-39`'un kendi gerekçesi); `quote.view` teklif dünyasının kapısıdır ve teklif potansiyel kayıtta yoktur (K-16). Şube paketi alan, teklif yetkisi olmayan bir satışçı da not yazabilmelidir. ⚠ Müşteri ve teklif notlarının kuralı **değişmez**. Yetki tek yerde: `PotentialCustomerPolicy::note`. |
| K-22 | Temas telefonu **açık** gösterilir; sayfa açılışı **erişim loguna** yazılır — `AccessLogger::record($prospect, ['phone'], ['contacts' => n], officeRecord: true)` (K-25), yalnız telefonu olan temas varsa | Sets, 19.10.2026 (heyet 809 F-6). `phone` hassas alandır, kırmızı çizgi 13 okumayı loglatır; satışçının her aramada tıklaması istenmedi. Okuma noktası tektir: `PotentialCustomerCard`. E-posta `sensitive_fields`'te değildir, loglanmaz. |
| K-23 | **Aynı kural müşteri kartına uygulanır**: `CustomerPeople::for()` telefonu olan kişi döndürdüğünde `AccessLogger::record($customer, ['phone'], ['people' => n])` | Sets, 19.10.2026: *"bu işte düzelt"*. Bugün firma kişilerinin telefonu logsuz gösteriliyor (`CustomerPeople.php:88`). Log, iki okuyanı (müşteri kartı, teklif ekranı) birden kapsamak için **sorgunun içinde** yazılır, controller'da değil. |
| K-25 | Kiracısı olmayan kaydın audit'i ve erişim logu **açık bir beyanla** kiracısız yazılır — üç yazıcının üçü de *(v1.3 — Sets, 20.10.2026: "null yazılabilir hâle getirmek zafiyet oluşturur; yazması gereken yerde yazmazsa kaçırırız. Yeni parametreli bir yapı daha doğru.")*. (1) `AuditWriter::record(…, officeRecord: true)` ve (2) `AccessLogger::record(…, officeRecord: true)` — çağıran kaydın ofise ait olduğunu **parametreyle** söyler; (3) model seviyesindeki audit (`AuditDriver`) parametre alamadığı için model `App\Support\Audit\OfficeRecord` sözleşmesini uygular — **bir soru**, işaret değil: `belongsToNoCustomer(): bool`; `tenantOf` yalnız `true` cevabında kiracısız yazar. `PotentialCustomer` ve `PotentialCustomerContact` her zaman `true`; **ortak modeller konularına bakar**: `RecordNote` ve `Task` konuları `PotentialCustomer` ise `true`, `SalesMeeting` `customer_id` boş ve `potential_customer_id` doluysa `true` — üçü de `implements Auditable` ve bugün müşteri bağlamında yazıldıkları için doğru kiracıyı alıyorlar; potansiyel kayıtta bağlam yoktur ve varsayılan yanlış müşteriye düşerdi. Müşteriye ait not/görev/görüşme `false` döner, davranışları değişmez. ⛔ **Beyan doğrulanır, güvenilmez:** `PersistsAuditRows::assertOfficeRecord(Model)` — konunun tablosu `tenancy.exempt_tables`'ta değilse **ya da** konuda `tenant_id` özniteliği varsa `LogicException`; NULL yazılmaz, işlem (transaction'la birlikte) düşer. Yani bayrak kiracılı bir kayda yanlışlıkla verilirse sessiz NULL değil, hata çıkar. ⚠ Örtük blok (`TenantContext::forOffice`) bu yüzden kullanılmaz: blok içindeki **her** yazmayı kiracısız yapar, kiracılı yazılması gereken bir satırı da. ⚠ Bayraksız çağrıların davranışı **değişmez** (`MyQuotes`'ın `quote.read`'i müşterinin bağlamından doğru kiracıyı alır); mevcut ofis çağıranları EGT-220. | Heyet 810 F-7 + v1.3'te bulunan genişleme: aynı varsayılan (`subject.tenant_id ?? CurrentActor::tenantId()`) yalnız `AccessLogger`'da değil, `AuditWriter.php:63` ve `AuditDriver.php:83-92`'de de var; tek müşteriye atanmış satışçının potansiyel kayda her yazması o müşterinin denetim izine düşerdi (`CurrentActor.php:78-86`). Tablo append-only, geri alınamaz. |
| K-24 | Audit anahtarları maskelenecek değer için **tam olarak** `phone` / `email` adını taşır; temas bilgisi audit'e iç içe dizi olarak konmaz | Heyet 809 F-3. `Masker` yalnız üst seviye tam adı maskeler; `contact_phone` ya da `['contact' => ['phone' => …]]` append-only tabloya **ham** yazılır ve geri alınamaz. |
| K-20 | Varsayılan paket taslağı: satışçı paketine `view.own`, `create`, `update`; satış yöneticisi paketine `view.group`, `assign`, `convert` | `PermissionTemplateSeeder` zaten taslaktır ve backoffice'ten düzenlenir (AD-08). ⚠ Mevcut kullanıcılara yetki **deploy ile verilmez** — rollout adımı. |

---

## Veri modeli

### `potential_customers` (yeni)

| Kolon | Tip | Not |
|---|---|---|
| `id` | bigIncrements | |
| `name` | string(255) | zorunlu |
| `legal_type` | string(20) nullable | `legal_entity`/`natural_person`; dönüşümde zorunlu olur, burada değil |
| `kind_option_value_id` | FK `option_list_values` nullable, `nullOnDelete` | `FIRMA-TURU` — EGT-217'nin süzgeci için şimdiden |
| `tax_office` | string(255) nullable | |
| `tax_number` | string(32) nullable, index | K-13 |
| `province_option_value_id` | FK `option_list_values` nullable | `IL` — kimi aradığını şehre göre süzmek |
| `phone` | string(32) nullable | firmanın santrali — kişisel veri değil, şifrelenmez |
| `email` | string(255) nullable | firmanın genel adresi |
| `website` | string(255) nullable | |
| `address` | text nullable | |
| `owner_user_id` | FK `users`, **not null**, `restrictOnDelete` | K-5 (kullanıcı silinmez, pasifleşir — AD-23) |
| `status` | string(16) default `open` | CHECK `open/closed/converted` |
| `closed_at`, `close_reason` | timestampTz / text nullable | CHECK `status <> 'closed' OR close_reason IS NOT NULL` |
| `converted_customer_id` | FK `customers` nullable, `restrictOnDelete` | CHECK `(status = 'converted') = (converted_customer_id IS NOT NULL)` |
| `converted_at`, `converted_by` | timestampTz nullable / FK `users` nullable | |
| `created_by` | FK `users` | |
| timestampsTz(6) | | |

İndeksler: `(owner_user_id, status)`, `(status, name)`, `tax_number`, `converted_customer_id` **unique** (bir müşteri tek potansiyelden doğar).

### `potential_customer_contacts` (yeni)

`id`, `potential_customer_id` FK `restrictOnDelete`, `name` string, `title` string nullable, `phone` text nullable (`encrypted`), `email` text nullable (`encrypted`), `note` text nullable, `timestampsTz`. İndeks `potential_customer_id`.

### `sales_meetings` (ek, nullable)

`potential_customer_id` FK nullable `restrictOnDelete`, `potential_customer_contact_id` FK nullable `nullOnDelete`; indeks `(potential_customer_id, scheduled_at)`. CHECK: `customer_user_id IS NULL OR customer_id IS NOT NULL` zaten anlamca geçerli; yeni CHECK `potential_customer_contact_id IS NULL OR potential_customer_id IS NOT NULL`.

⚠ **Data-loss riski yok:** iki yeni tablo, bir tabloya iki nullable kolon, bir fonksiyon. Hiçbir kolon düşmüyor, yeniden adlanmıyor, tipi değişmiyor. `down()` tabloları/kolonları/fonksiyonu kaldırır — **canlıda veri girildikten sonra rollback o veriyi siler**; rollback planına bakın.

### `app.customers_with_tax_number(text)` (yeni fonksiyon)

`RETURNS SETOF text`, `LANGUAGE sql STABLE SECURITY DEFINER SET search_path = public, app`, gövde `SELECT name FROM customers WHERE tax_number = btrim($1) ORDER BY name` — **yalnız ad** (K-13). Açık potansiyeller RLS'siz olduğu için normal sorguyla okunur (`TaxNumberRegistry`), kaydın kendisi `id <> ?` ile dışlanır. `REVOKE ALL … FROM PUBLIC`, `GRANT EXECUTE` → `config('tenancy.roles')`. Desen `app.all_customer_ids()` ile birebir.

⚠ Karşılaştırma `btrim` ile yapılır; bugün `customers.tax_number` normalleştirilmeden yazılıyor (doğrulanmış durum tablosu). Mevcut kayıtlarda boşluk/biçim farkı varsa eşleşme kaçabilir — Görev 2'de canlı verinin biçimi **okunarak** ölçülür (yalnız `SELECT`), sonuç ticket'a yazılır.

---

## Yetki

`config/authorization.php` office ailesi, yorumuyla birlikte:

```
potential_customer.view.own
potential_customer.view.group
potential_customer.view.all
potential_customer.create
potential_customer.update      // kapsam view'dan gelir (customer.update deseni)
potential_customer.assign      // sahibi değiştirmek — K-5
potential_customer.convert     // + customer.create + customer.update — K-11
```

`PermissionMatrix`: `potential_customer` kaynak satırı (view kapsamlı, create, update); `assign` ve `convert` **standalone `decision`** grubuna (iş akışı yetkisi, bir kaydın fiili değil — mevcut `training.assign_instructor` gerekçesiyle aynı).

`PotentialCustomerPolicy`:

| Yetenek | Kural |
|---|---|
| `viewAny` | `view.*`'ın herhangi biri **veya** `create` |
| `view` | kayıt `PotentialCustomersInReach` içinde |
| `create` | `potential_customer.create` |
| `update`, `close`, `reopen` | `update` + `view` + durum `open` (reopen: `closed`) |
| `assign` | `assign` + `view` + durum `open`; hedef kişi `ScopeResolver::covers($actor, 'potential_customer.view.group', $target)` |
| `convert` | `convert` + `customer.create` + `customer.update` + `view` + durum `open` |

| `note` | `view` — K-21; `RecordNotePolicy::create` sorulmaz |

Görev ve görüşme yazmak `view` ister — müşteri tarafıyla aynı kural (`ScheduleMeeting:41`). Not için K-21.

`PotentialCustomersInReach::apply(Builder, User)` — `TrainingsInScope` deseni, `PermissionFamilies::using(Office, …)` bağlaması **içinde** (`TrainingsInScope.php`'deki sessiz "grup yok" tuzağı). `null` → sınırsız; `[]` + `own` → `owner_user_id = reader`; gruplar → `owner_user_id IN (SELECT user_id FROM group_memberships WHERE group_id IN (…))` **veya** kendisi.

---

## Dosya haritası

**Yeni**

| Dosya | Sorumluluk |
|---|---|
| `database/migrations/2026_10_31_000100_a_company_we_have_not_won_yet.php` | iki tablo, CHECK'ler, indeksler |
| `database/migrations/2026_10_31_000200_a_meeting_may_be_with_a_company_we_have_not_won.php` | `sales_meetings` ekleri |
| `database/migrations/2026_10_31_000300_which_customers_carry_a_tax_number.php` | fonksiyon + grant |
| `app/Models/PotentialCustomer.php`, `app/Models/PotentialCustomerContact.php` | ilişkiler, cast'ler (`encrypted`), sabitler |
| `database/factories/PotentialCustomerFactory.php`, `PotentialCustomerContactFactory.php` | durum state'leri: `closed()`, `converted(Customer)` |
| `app/Domain/Sales/PotentialCustomerLifecycle.php` | izinli geçişler (saf PHP) |
| `app/Policies/PotentialCustomerPolicy.php` | yukarıdaki tablo |
| `app/Queries/Office/PotentialCustomersInReach.php` | kapsam (tek yer) |
| `app/Queries/Office/PotentialCustomerList.php` | liste satırları (ad, tür, il, sahip, son temas, açık görev, durum) |
| `app/Queries/Office/PotentialCustomerCard.php` | sayfa: bilgiler, temaslar, aktivite (not + görüşme), görevler |
| `app/Support/Sales/TaxNumberRegistry.php` | fonksiyonu ve açık potansiyel sorgusunu çağıran yardımcı; karar vermez, `matchesFor(string $taxNumber, ?int $exceptProspectId): list<array{name, kind: customer\|prospect, href: ?string}>` döner (`href` yalnız okuyanın kapsamındakilere) |
| `app/Rules/TaxNumberMatchAcknowledged.php` | doğrulama kuralı: eşleşme var ve `acknowledged_tax_number_match` yoksa hata + eşleşen adlar; onay varsa geçer (K-13) |
| `app/Actions/Sales/OpenPotentialCustomer.php` | açma; sahip = aktör |
| `app/Actions/Sales/UpdatePotentialCustomer.php` | düzenleme |
| `app/Actions/Sales/ClosePotentialCustomer.php`, `ReopenPotentialCustomer.php` | K-6 |
| `app/Actions/Sales/HandOverPotentialCustomer.php` | sahip değişikliği (K-5) |
| `app/Actions/Sales/AddPotentialCustomerContact.php`, `UpdatePotentialCustomerContact.php` | temaslar |
| `app/Actions/Sales/AddPotentialCustomerNote.php` | `record_notes`'a yazar |
| `app/Actions/Sales/OpenPotentialCustomerTask.php` | `OpenTask` üzerinden, `TaskKind::PotentialCustomerFollowUp` |
| `app/Actions/Sales/SchedulePotentialCustomerMeeting.php` | `sales_meetings`'e `potential_customer_id` ile |
| `app/Actions/Sales/ConvertPotentialCustomer.php` | K-9…K-12 |
| `app/Data/Office/NewPotentialCustomer.php`, `NewPotentialCustomerContact.php` | DTO |
| `app/Http/Requests/Office/PotentialCustomerRequest.php`, `PotentialCustomerContactRequest.php`, `ClosePotentialCustomerRequest.php`, `HandOverPotentialCustomerRequest.php`, `ConvertPotentialCustomerRequest.php` | doğrulama |
| `app/Http/Controllers/Office/PotentialCustomerController.php` | ince |
| `resources/js/Pages/Office/PotentialCustomers/Index.vue`, `Show.vue` | sayfalar |
| `resources/js/organisms/PotentialCustomerForm.vue`, `PotentialContactForm.vue` | formlar |
| `resources/js/organisms/RecordNotesCard.vue`, `RecordTasksCard.vue`, `MeetingScheduleForm.vue` | K-18 |
| `lang/tr/potential_customers.php`, `lang/en/potential_customers.php` | metinler |
| `docs/yardim/satis-potansiyel-musteri.json` + `public/images/yardim/satis-potansiyel-musteri*.png` | yardım makalesi |
| `tests/Feature/Satis/PotentialCustomerTest.php`, `PotentialCustomerReachTest.php`, `PotentialCustomerConversionTest.php` | |
| `tests/Feature/OmurgaRls/CustomersWithTaxNumberTest.php` | fonksiyon, uygulama rolüyle |
| `tests/Feature/OmurgaRls/OfficeRecordAuditTest.php` | K-25: üç yazıcıda beyanlı NULL kiracı + INSERT politikası (uygulama rolüyle); kiracılı konuya bayrak → `LogicException`, satır yok |
| `app/Support/Audit/OfficeRecord.php` | model sözleşmesi `belongsToNoCustomer()` (K-25) |
| `tests/Browser/potential-customers.spec.js` | |

**Değişen**

| Dosya | Değişiklik |
|---|---|
| `config/authorization.php` | yedi izin, gerekçe yorumuyla |
| `config/tenancy.php` | `potential_customers`, `potential_customer_contacts` → `exempt_tables` (K-3) |
| `app/Actions/Sales/CloseMeeting.php` | konu müşteri ya da potansiyel; not ve bağlam konuya göre (K-15) |
| `app/Queries/Office/CustomerPeople.php` | telefon okuması erişim loguna (K-23) |
| `app/Support/Audit/AccessLogger.php`, `AuditWriter.php` | `officeRecord` parametresi (varsayılan `false`, davranış aynı) — K-25 |
| `app/Support/Audit/AuditDriver.php` | `tenantOf`: `OfficeRecord` modeli → doğrulanmış NULL — K-25 |
| `app/Support/Audit/Concerns/PersistsAuditRows.php` | `assertOfficeRecord` — K-25 |
| `app/Support/Authorization/PermissionMatrix.php` | satır + standalone `decision` |
| `database/seeders/PermissionTemplateSeeder.php` | K-20 |
| `app/Support/Tasks/TaskKind.php` | `PotentialCustomerFollowUp = 'potential_customer.follow_up'` |
| `app/Models/SalesMeeting.php` | iki ilişki, fillable, `OfficeRecord` (K-25) |
| `app/Models/RecordNote.php`, `app/Models/Task.php` | `OfficeRecord` — konu potansiyelse `true` (K-25) |
| `app/Queries/Office/SalesCalendar.php`, `SalesAgenda.php` | potansiyel görüşmede ad + bağlantı (K-15) |
| `app/Queries/Office/OfficeNavigation.php` | `potential_customers` → `sales` grubu, `policy => PotentialCustomer::class`, `sales_calendar`'dan sonra |
| `app/Queries/Office/CustomerDetail.php` | `origin` (potansiyel kaydın opak id'si, varsa) — K-19 |
| `resources/js/Pages/Office/Customers/Show.vue` | üç bölüm organizmaya; başlığa `origin` bağlantısı |
| `resources/js/organisms/CustomerForm.vue` | `endpoint` ve `initial` prop'ları (varsayılan davranış değişmez); dönüşüm modunda kişi alanları boş ve zorunlu |
| `routes/office.php` | `/satis/potansiyel-musteriler` grubu |
| `lang/tr/office.php`, `lang/en/office.php` | menü etiketi «Potansiyel müşteriler» |
| `lang/tr/tasks.php`, `lang/en/tasks.php` | yeni görev türü |
| `docs/yardim/satis-musteri-karti.json` | K-19 bağlantısı adımı + ekran görüntüsü |
| `changelog/2026-10-19.json` (ya da teslim günü) | kayıt |
| `proje-haritasi.md` §4.4 | durum, veri, ekran satırları |

---

## Rotalar

`auth:office` + `RequireStrongOfficeSession` + `RequireLegalConsent` grubu, sayfa başına policy:

| Yöntem | Yol | Ad | Yetenek |
|---|---|---|---|
| GET | `/satis/potansiyel-musteriler` | `office.potential-customers.index` | `viewAny` |
| POST | `/satis/potansiyel-musteriler` | `.store` | `create` |
| GET | `/satis/potansiyel-musteriler/{potentialCustomer}` | `.show` | `view` |
| PUT | `…/{potentialCustomer}` | `.update` | `update` |
| POST | `…/{potentialCustomer}/kapat` | `.close` | `close` |
| POST | `…/{potentialCustomer}/yeniden-ac` | `.reopen` | `reopen` |
| PUT | `…/{potentialCustomer}/sahip` | `.hand-over` | `assign` |
| POST | `…/{potentialCustomer}/kisiler` | `.contacts.store` | `update` |
| PUT | `…/{potentialCustomer}/kisiler/{contact}` | `.contacts.update` | `update` |
| POST | `…/{potentialCustomer}/notlar` | `.notes.store` | `note` |
| POST | `…/{potentialCustomer}/gorevler` | `.tasks.store` | `view` |
| POST | `…/{potentialCustomer}/gorusmeler` | `.meetings.store` | `view` |
| POST | `…/{potentialCustomer}/musteriye-donustur` | `.convert` | `convert` |

⚠ Bağlama: opak id `FindPotentialCustomer` sorgusuyla çözülür, kapsam dışı kayıt **404** (`NotFoundOnDenied` deseni) — varlığı sızdırmaz.

---

## Görev 1 — Şema, modeller, factory, durum makinesi

1. Üç migration (veri modeli bölümü). Fonksiyon migration'ı `all_customer_ids` dosyasının iskeletini izler.
2. Modeller: `PotentialCustomer` (`owner`, `contacts`, `convertedCustomer`, `notes` morphMany `RecordNote`, `meetings` hasMany `SalesMeeting`), `PotentialCustomerContact` (`encrypted` cast'ler, `$hidden` yok — maskeleme sunumda).
3. `PotentialCustomerLifecycle::canMove(string $from, string $to): bool` + `assertMove` — saf PHP, `app/Domain/Sales/`.
4. Factory'ler, `closed()` ve `converted()` state'leri.

**Testler:** `PotentialCustomerLifecycleTest` (unit, `satis`) — izinli/izinsiz her geçiş; `converted`'dan çıkış yok. Migration: CHECK'lerin reddettiği üç satır (kapalı ama gerekçesiz, dönüşmüş ama müşterisiz, müşterili ama açık) — DB seviyesinde.

## Görev 2 — Yetki ve kapsam

1. İzinler, matris, paket taslağı (K-20).
2. `PotentialCustomersInReach`, `PotentialCustomerPolicy`, `FindPotentialCustomer`.
3. `TaxNumberRegistry` + `TaxNumberMatchAcknowledged`.
4. `config/tenancy.php` `exempt_tables` (K-3).
5. Canlı vergi no biçim ölçümü: local prod'da `SELECT tax_number FROM customers WHERE tax_number !~ '^[0-9]{10,11}$'` (yalnız okuma) — sonuç ticket'a yazılır; `btrim` yetmiyorsa normalleştirme kararı **Sets'e sorulur**, kendiliğinden genişletilmez.

**Testler (`PotentialCustomerReachTest`):** grup ağacı fixture'ı — `Satış` altında `İzmit Satış` ve `İstanbul Satış`, her birinde bir `staff` ve bir `group_manager`.
- `view.own`: yalnız kendi kayıtları; başkasınınkine `show` → 404.
- `view.group` + `group_manager` (İzmit): İzmit'in tüm kayıtları, İstanbul'unkiler değil.
- `view.group` + `staff`: `own`'a daralır (AD-23).
- `view.all`: herkes.
- Aile bağlaması dışında çağrı tuzağı: kapsam sorgusu bağlamasız çağrıldığında boş dönmediği (regresyon).
- `assign`: kapsam dışındaki kişiye devir reddedilir.
- `CustomersWithTaxNumberTest` (`omurga-rls`, uygulama rolüyle): kapsamında hiç müşteri olmayan kimlik için fonksiyon eşleşen firmanın **adını** döner; dönen kayıtta ad dışında kolon yoktur; `PUBLIC` çalıştıramaz.
- `TenancyTest` (mevcut arch testi) iki yeni modeli muaf olarak sınıflandırır; bağlamsız `PotentialCustomer::query()->get()` `TenantContextMissingException` fırlatmaz.

## Görev 3 — Açma, düzenleme, temaslar, kapatma, devir

Action'lar ve request'ler. Her action: policy → transaction → yazma → `AuditWriter::record` (aynı transaction).

Audit olayları: `potential_customer.opened`, `.updated` (eski/yeni değerler), `.closed` (gerekçe), `.reopened`, `.handed_over` (eski/yeni sahip), `.contact_added`, `.contact_updated` — telefon/e-posta **maskeli**: `AuditWriter` değerleri yazarken `Support\Masking\Masker::apply` ile maskeler, ama yalnız `config('audit.masked_fields')` listesindeki **tam ada** uyan **üst seviye** anahtarı (`AuditWriter.php:54-57`, `Masker.php:28-40`). ⚠ Bu yüzden anahtarlar tam olarak `phone` ve `email` olur (K-24) — `contact_phone`, `tel` ya da iç içe `['contact' => […]]` maskelenmeden yazılır; test bunu kanıtlar.

**Testler (`PotentialCustomerTest`):**
- Yetkili/temas olmadan açılır (EGT-216'nın asıl isteği); sahibi açan kişidir ve listede görür.
- `name` zorunlu.
- Vergi no bir **müşteride** kayıtlıysa — müşteri okuyanın kapsamı **dışında** olsa bile — ilk gönderim kaydetmez, uyarı firmanın **adını** taşır, bağlantı taşımaz; `acknowledged_tax_number_match=1` ile ikinci gönderim kaydı açar ve audit satırında onay + adlar vardır. Kapsam içindeki eşleşmede bağlantı vardır.
- Açık bir potansiyelde kayıtlıysa aynı uyarı; **kapalı/dönüşmüş** potansiyel uyarı üretmez (dönüşmüş olan müşteri tarafında yakalanır).
- Vitsan senaryosu: aynı vergi numarasıyla iki müşteri varken uyarı iki adı birden yazar.
- Düzenleme kaydın kendi vergi nosunu eşleşme saymaz.
- Audit satırında temas telefonu/e-postası **maskelidir** — ham değer `audit_writes`'ta aranır ve bulunmaz (K-24).
- Kapatma gerekçesiz reddedilir; kapalı kayıt düzenlenemez; yeniden açılır.
- Dönüşmüş kayıtta her yazma 403.
- Temas e-postası/telefonu veritabanında düz metin değildir (ham sütun okunur).
- Audit satırı işlemle aynı transaction'da: action içinde istisna fırlatılınca audit satırı da yoktur.
- Her rota için yetkisiz kullanıcı (izin yok) → 403/404.

## Görev 4 — Not, görev, görüşme

1. `AddPotentialCustomerNote` — `record_notes`, `subject` = potansiyel, türler mevcut CHECK'ten; tenant bağlamı **açılmaz** (kiracı yok, K-3); yetki `PotentialCustomerPolicy::note` (K-21).
2. `TaskKind::PotentialCustomerFollowUp` + `OpenPotentialCustomerTask` (`OpenCustomerTask`'ın birebir deseni, `distinctBy: ['title']`).
3. `SchedulePotentialCustomerMeeting` — `ScheduleMeeting`'in doğrulamaları (tür, konu); temas kişisi bu potansiyele ait olmalı; `quote_id`/`opportunity_id` yok (K-16).
4. `SalesCalendar` / `SalesAgenda`: `customer_id` boş, `potential_customer_id` dolu görüşme firma adını potansiyel kayıttan alır; satıra `potential: true` ve sayfa bağlantısı.
5. `CloseMeeting` (K-15, heyet 809 F-2): `customer_id` doluysa bugünkü yol **aynen** (dönüşmüş potansiyelin görüşmeleri dahil). Değilse ve `potential_customer_id` doluysa: yetki `view(prospect)`, kapanış notu `AddPotentialCustomerNote` ile (önce, ayrı action — bugünkü "not kaybolursa görüşme açık kalsın" sırası korunur), yazma tenant bağlamı **olmadan** `DB::transaction` içinde, aynı `meeting.held`/`meeting.cancelled` audit'i. İkisi de boşsa bugünkü `meeting_has_no_customer` hatası. Konu çözümü tek özel metotta; iki dal ayrı kopyalanmaz.

**Testler:** not yazıldığında kartta görünür; **`quote.view`'u olmayan**, yalnız potansiyel izinleri olan kullanıcı not yazabilir (K-21); görev "Görevlerim"de türüyle görünür; görüşme Takvimim ve kokpitte potansiyelin adıyla görünür; potansiyel görüşmesi kokpitten **tamamlanır** (kapanış notu potansiyelin aktivitesine düşer) ve **iptal edilir** (gerekçeyle); müşteri görüşmesini kapatan mevcut test değişmeden geçer; kapsam dışı potansiyele not/görev/görüşme ve kapsam dışı potansiyelin görüşmesini kapatma → 404; başka potansiyelin temasıyla görüşme reddedilir.

## Görev 5 — Müşteriye dönüşüm

`ConvertPotentialCustomer(User $actor, PotentialCustomer $prospect, NewCustomer $company, NewCustomerPerson $person): Customer`

```
authorize('convert', $prospect)
DB::transaction:
  1. PotentialCustomer::lockForUpdate()->find(id); status 'open' değilse ValidationException  (çift tık)
  2. TaxNumberMatchAcknowledged — dönüşüm formunun vergi nosuyla, POTANSİYELİN KENDİSİ HARİÇ (K-13, F-4);
     eşleşme var ve onay yoksa ValidationException (uyarı + adlar), onay varsa audit'e yazılır
  3. $customer = OnboardCustomer($actor, $company)                 // customer.create + customer.created audit
  4. office_user_customers ← sahip (+ aktör farklıysa)             // K-12, CreateCustomerPerson'dan ÖNCE
     audit customer.salesperson_assigned {reason: conversion}
  5. $person = CreateCustomerPerson($actor, $customer, $person)    // update policy — 4. adım sayesinde geçer
  6. TenantContext::forWrite(customer) İÇİNDE — artık o müşterinin kayıtları, audit'leri onun kiracısıyla:
     record_notes / tasks: subject → customer (model üzerinden, toplu UPDATE değil — model audit'i yazılsın)
     sales_meetings: customer_id ← customer (potential_customer_id korunur; belongsToNoCustomer artık false)
  7. prospect: status converted, converted_customer_id, converted_at, converted_by      // OfficeRecord → kiracısız
  8. AuditWriter::record('potential_customer.converted', $prospect, …, officeRecord: true) {customer_id, sayılar}
return $customer
controller, commit SONRASI: allows('update', $customer) ise InviteCustomerPerson($actor, $person)   // K-10
```

⚠ **Görev başında doğrulanacak iki varsayım** (kod okunarak, test ile kanıtlanarak; tutmazsa DUR ve sor):
- (a) `OnboardCustomer` ve `CreateCustomerPerson`'ın kendi `DB::transaction` çağrıları dış transaction içinde savepoint olarak çalışır ve `TenantContext::forWrite`'ın `SET LOCAL`'ı dış transaction'ı bozmaz (AD-04).
- (b) `SessionScopeResolver::accessibleTenantIds()` aynı istekte memoize edilmiyor — 4. adımda yazılan atama 5. adımın policy kontrolünde görünüyor. (`SessionScopeResolver` sınıfında önbellek yok; `EnsureTenantContext`'in istek başında ne hesapladığı ayrıca okunur.)

UI: `Show.vue`'da «Müşteriye dönüştür» düğmesi (yalnız `can.convert`) → `SlideOver` içinde `CustomerForm` (`endpoint` = dönüşüm rotası, `initial` = potansiyelden ad/tür/vergi/hukuki tip; **kişi alanları boş**, K-8). Başarıda yeni müşteri kartına yönlendirilir, flash «X müşteriye dönüştü».

Müşteri kartı: `CustomerDetail.origin` doluysa başlık bilgi satırında «Potansiyel müşteri geçmişi →» (K-19). Potansiyel sayfası dönüşmüşse salt okunur, üstte «Dönüştü» rozeti ve müşteri kartına bağlantı.

**Testler (`PotentialCustomerConversionTest`):**
- Vergi numarası dolu potansiyel, başka eşleşme yokken **uyarısız** dönüşür (kendini eşleşme saymaz — F-4); vergi numarası başka bir müşteride varken uyarı verir, onayla dönüşür.
- Mutlu yol: müşteri açılır, yetkili elle girilen kişidir, davet gider (`Mail::fake` / bildirim fake'i mevcut davet testindeki gibi), potansiyel `converted`, notlar/görevler müşteride, görüşmelerde iki kolon dolu.
- **Temas kişileri müşteri kişisi olmaz** — `customer_users` sayısı 1.
- `customer.view.all`'u **olmayan** satışçı dönüştürür → müşteri kartını açabilir (K-12'nin regresyonu).
- Yetkili e-postası geçersiz → hiçbir şey açılmaz: müşteri yok, atama yok, notlar potansiyelde, potansiyel `open`, audit satırı yok.
- İkinci dönüşüm denemesi reddedilir; iki paralel istek tek müşteri üretir (kilit).
- `customer.create` yoksa → 403, `convert` olsa bile.
- Dönüşmüş potansiyel hiçbir müşteri listesinde değil, oluşan müşteri `OfficeCustomers` ve teklif seçicisinde görünür.
- Müşteri kartı `origin` bağlantısını yalnız dönüşmüş müşteride verir.

## Görev 5b — Firma kişilerinin telefonu erişim loguna (K-23)

`CustomerPeople::for()`: dönen kişilerden en az birinin telefonu varsa, `TenantContext::run` içinde (audit satırı müşterinin kiracısıyla yazılır) `AccessLogger::record($customer, ['phone'], ['people' => n])`. Tek okuma noktası, iki okuyan (müşteri kartı, teklif ekranı).

**Testler** (`musteri` grubu, mevcut `CustomerManagementTest` yanına): telefonu olan kişisi bulunan müşterinin kartı açılınca `audit_access`'e bir satır düşer (`fields` = `phone`, okuyan = aktör); telefonsuz firmada satır düşmez; teklif ekranı da satır yazar. Potansiyel tarafı için aynı üç senaryo `PotentialCustomerTest`'te (K-22), artı K-25: **tam bir müşteriye atanmış** satışçı potansiyel sayfasını açınca `audit_access` satırının `tenant_id`'si **NULL**'dır, atanmış müşterinin id'si değildir; aynı satışçının potansiyel açma, düzenleme, not, görev, görüşme ve kapatma işlemlerinin **hem** `AuditWriter` olay satırları **hem** model audit satırları NULL kiracılıdır. `OfficeRecordAuditTest` (uygulama rolüyle): üç yazıcının beyanlı NULL yazdığı ve INSERT politikasının kabul ettiği; kiracılı bir konuya (`CustomerUser`) `officeRecord: true` verildiğinde `LogicException` ve **satır yazılmadığı**; müşteriye ait `RecordNote`/`Task`/`SalesMeeting`'in `belongsToNoCustomer() === false` döndüğü ve audit'lerinin müşterinin kiracısını taşıdığı. Dönüşüm testi: yeniden bağlanan not/görevlerin ve görüşmelerin dönüşüm audit satırları **yeni müşterinin** kiracısını taşır. Mevcut `MyQuotes` `quote.read` testi değişmeden geçer. Mevcut `MyQuotes` `quote.read` testi değişmeden geçer.

## Görev 6 — Ekranlar

**Önce:** `design/32 Satisci Musteri Karti.dc.html` ve `design/02` baştan sona okunur, envanter çıkarılır (ızgara `1fr 1fr 1.2fr` üst bant, `1fr 480px` gövde — `32:49`, `32:82`).

- **Liste** (`Index.vue`): `DataTable` — kolonlar ad (+ hukuki tip), tür, il, sahip, son temas, durum; dilimler Tümü / Açık / Kapalı / Dönüştü (sayılarıyla); arama ad + vergi no; «Potansiyel müşteri ekle» → `SlideOver` + `PotentialCustomerForm` (temas bölümü **isteğe bağlı**, ilk temas aynı formda eklenebilir). Vergi no eşleşmesi formun üstünde amber `Banner`: eşleşen firma adları (kapsam içindekiler bağlantılı) + «Yine de kaydet» düğmesi; aynı uyarı dönüşüm panelinde. Dar ekran kart şablonu.
- **Sayfa** (`Show.vue`): `32`'nin alt kümesi — başlık (ad, tür rozeti, durum rozeti, il · sahip), hızlı eylemler (＋ Görüşme notu, ＋ Görev, ＋ Görüşme planla, Müşteriye dönüştür, menüde Kapat / Sahibi değiştir), **Kişiler** kartı (telefon/e-posta açık — satışçının arayacağı numara; sayfa açılışı erişim loguna yazılır, K-22; maskeleme yalnız audit'te), **Aktivite** (not + görüşme), **Görevler**. `32`'deki ilişki özeti, uyum sinyali, açık teklifler, geçmiş eğitimler **yoktur** (K-16).
- **Organizmalar** (K-18): `RecordNotesCard`, `RecordTasksCard`, `MeetingScheduleForm` — `Customers/Show.vue`'dan çıkarılır, iki sayfa da kullanır.
- **Menü:** Satış grubu, Takvimim'den sonra.
- Kapsama matrisi (`32` öğesi | ✓/⚠/❌ | dosya+satır) teslimden önce üretilir; ❌ satırların K-16 ile kapsam dışı olduğu yazılır.

**Testler:** `tests/Browser/potential-customers.spec.js` — açma (yetkilisiz), not ekleme, görüşme planlayıp Takvimim'de görme, dönüşüm, müşteri kartında geçmiş bağlantısı; 375px'de liste kartı. Mevcut müşteri kartı spec'i (organizma çıkarımının regresyonu) yalnız o dosya koşulur.

## Görev 7 — Yardım, changelog, harita

- `docs/yardim/satis-potansiyel-musteri.json` — adımlar: ekle, ara/not, görüşme planla, kapat, dönüştür (yetkili elle girilir, temaslar taşınmaz — **sebebiyle**). Ekran görüntüleri Playwright 1440px. `satis-musteri-karti.json`'a K-19 adımı. `HelpArticleSeeder`.
- `changelog/<teslim günü>.json`.
- `proje-haritasi.md` §4.4: durum / veri / ekran / ticket satırları.

---

## Çapraz panel etkisi

| Panel / ekran | Etki |
|---|---|
| Satış: Takvimim, Satış kokpiti | potansiyel görüşmeler görünür (K-15) |
| Satış/Yönetim: Müşteriler, müşteri kartı | kart bölümleri organizmaya taşınır (davranış aynı), başlığa geçmiş bağlantısı |
| Görevlerim | yeni görev türü |
| Yetki ekranı (`64`) | yeni satır + `decision` grubunda iki yetki |
| Planlamacı, eğitmen | etkisiz |
| Müşteri portalı, katılımcı | **etkisiz** — potansiyel kayıt portalda hiçbir sorguda yok; dönüşümden sonra firma normal müşteri gibi davet edilir |
| Backoffice | etkisiz (müşteri verisi konmaz kuralı) |

## Kuyruk / Horizon

Yeni job yok. Davet e-postası mevcut `InviteCustomerPerson` yolundan gider (bugünkü kuyruk davranışı değişmez).

## Performans

Liste satışçının kendi kaydı ölçeğinde (yüzler); `(owner_user_id, status)` indeksi kapsam sorgusunu karşılar. "Son temas" kolonu not ve görüşmelerden **tek sorguda** agregat (`max(created_at)`), satır başına sorgu yok (Sentry N+1 dersi, `OfficeInstructors`).

## Rollout

1. Yeşil CI → prod deploy (migration'lar `deploy.sh` içinde koşar).
2. ⚠ Deploy kimseye yetki vermez: menü yetkisi olmayan herkese **görünmez** (kapı policy'dir). Yetkiler deploy sonrası **yetki ekranından** verilir — kime ne verileceği Sets'in kararı; Tayfun'un test hesabına verilmesi test prosedürünün ilk adımıdır.
3. Canlıda doğrulama: menü kalemi, bir potansiyel aç → not → dönüştür → müşteri kartı.

## Rollback

- Kod: `git revert` + deploy. Menü kalemi kaybolur; tablolar yerinde kalır, veri kaybolmaz.
- Şema: `migrate:rollback` **yalnız canlıda henüz veri yokken**. Veri girildikten sonra `down()` potansiyel kayıtları ve temasları siler ve dönüşmüş müşterilerin `sales_meetings.potential_customer_id` izini kaldırır — bu durumda rollback kod revert'üyle sınırlıdır; şema geri alınmaz. ⛔ Kırmızı çizgi 3 (proje kuralları): veri silen komut sorulmadan koşulmaz.
- Dönüşüm geri alınamaz (kiracı açılmıştır); bu bilinçli (K-6). Yanlış dönüşümün çaresi müşteriyi pasifleştirmektir.

---

## Kabul kriterleri

1. Yetkili/temas girmeden potansiyel müşteri açılır; açan kişi sahibidir ve listede görür.
2. Potansiyel müşteri hiçbir müşteri listesinde, teklif seçicisinde, eğitim firması seçiminde görünmez.
3. Not, görev, planlı görüşme yazılır; görüşme Takvimim ve Satış kokpitinde potansiyelin adıyla görünür.
4. Teklif, fırsat, eğitim ataması potansiyel kayıtta yoktur.
5. `view.own` / `view.group` / `view.all` AD-23'e göre çalışır; İzmit grubu İstanbul'un kayıtlarını görmez; kapsam dışı kayıt 404.
6. Sahip değişikliği ayrı yetkidir ve hedef kapsam içinde olmalıdır.
7. Vergi no bir müşteride veya açık potansiyelde kayıtlıysa açma/düzenleme/dönüşüm **uyarır**, firma adlarını yazar; «yine de kaydet» ile devam edilir ve onay audit'lenir; kayıt kendisiyle eşleşmez.
8. Kapatma gerekçe ister, yeniden açılabilir; dönüşmüş kayıt salt okunurdur.
9. Dönüşümde yetkili elle girilir; temas kişileri müşteri kişisi olmaz ve potansiyel kayıtta kalır.
10. Dönüşüm tek transaction'dır: başarısızlıkta hiçbir şey açılmaz; çift dönüşüm imkânsızdır.
11. Dönüşümden sonra notlar/görevler müşteri kartında, görüşmeler müşteride; potansiyel kayıt «Dönüştü» olarak kalır ve müşteri kartı ona bağlantı verir.
12. `customer.view.all`'u olmayan satışçı dönüştürdüğü müşteriyi görür.
13. Temas telefonu/e-postası veritabanında şifreli, audit'te maskelidir.
14. Her yazma işlemi aynı transaction'da audit'lenir.
15. Müşteri kartının mevcut davranışı değişmez (organizma çıkarımı).
16. Yardım makalesi ve changelog güncel; ekran görüntüleri yeni ekranı gösterir.
17. Potansiyel müşterinin görüşmesi Takvimim/kokpitten tamamlanır ve iptal edilir; kapanış notu potansiyelin aktivitesine düşer.
18. Teklif yetkisi olmayan, potansiyel izinleri olan kullanıcı not yazabilir.
19. Temas telefonu gösteren potansiyel sayfası ve firma kişisi telefonu gösteren müşteri kartı/teklif ekranı erişim loguna yazar.
20. İki yeni tablo `exempt_tables`'tadır; bağlamsız sorgu reddedilmez, arch testi geçer.

## Kapanış adresi verilen, bu turda yapılmayanlar

| Konu | Adres |
|---|---|
| Normal müşteri açılışında açan kişinin atanması (bugünkü hata) | EGT-219 madde 5 |
| `customers.tax_number` tekilliği / normalleştirmesi | Görev 2'deki ölçümün sonucuyla Sets'e |
| Potansiyel müşteri kaynağı (fuar, tavsiye, geçmiş veri…) ve dönüşüm metrikleri ekranı | ikinci aşama — `proje-haritasi.md` §4.1 geçmiş veri BI notu |
| Toplu içe aktarma (Excel'den aranacak firma listesi) | istenmedi; ihtiyaç doğarsa ayrı ticket |
| Dört mevcut ofis okumasının kiracısız erişim logu (K-25'in diğer çağıranları) | EGT-220 |

## Revizyon geçmişi

| Sürüm | Tarih | Değişiklik |
|---|---|---|
| v1.0 | 19.10.2026 | İlk sürüm |
| v1.1 | 19.10.2026 | Heyet 809 (REJECTED): F-1 `exempt_tables` mekanizması K-3'e ve Görev 2'ye; F-2 `CloseMeeting` potansiyel konusu (K-15, Görev 4.5); F-3 maskeleme tam ad kuralı (K-24, doğrulanmış durum düzeltildi — v1.0'daki "adında geçmesi yeterli" cümlesi yanlıştı); F-4 dönüşümde kendini dışlama (K-13, Görev 5); F-5 not yetkisi (K-21); F-6 erişim logu (K-22, Sets kararı). Sets kararları: vergi no **uyarı**dır, engel değil, firma adı gösterilir (K-13 yeniden yazıldı, fonksiyon ad döner); müşteri kartındaki logsuz telefon okuması bu işte düzeltilir (K-23, Görev 5b). ⚠ F-1'deki `SchemaConformanceTest` adında bir test dosyası yok (yalnız `TenancyTest.php:42` yorumunda geçiyor); bulgunun özü `TenancyTest.php:32` ile doğru. |
| v1.2 | 19.10.2026 | Heyet 810 (REJECTED, F-7): kiracısız kaydın erişim logu yanlış müşteriye yazılabiliyordu — K-25 `AccessLogger::recordOfficeRead`, K-22 bu yola bağlandı, Görev 5b'ye test. ⚠ Aynı kod yolu bugün dört ofis okumasında da var (eğitmen belgesi, eğitmen kimlik no, kurum belgesi, yoklama defteri) — Sets kararıyla ayrı ticket: EGT-220. |
| v1.3 | 20.10.2026 | Uygulama başında, onaylı plan gerçeğe uymadı: F-7'nin varsayılanı yalnız `AccessLogger`'da değil `AuditWriter.php:63` ve `AuditDriver.php:83-92`'de de var; potansiyel kayda **her yazma** yanlış müşteriye audit'lenirdi. Sets'in kararıyla (20.10.2026) örtük `TenantContext::forOffice` bloğu değil **açık parametre**: K-25 yeniden yazıldı — `officeRecord: true` (iki yazıcı), `OfficeRecord::belongsToNoCustomer()` (model audit'i, ortak modeller konuya bakar), beyanın doğrulanması (`assertOfficeRecord`, kiracılı konuda `LogicException`). `recordOfficeRead` kaldırıldı. Dönüşümde yeniden bağlama adımları `forWrite(müşteri)` içine alındı. |
