# Review — Müşteri portalı ve teklif dikeyi planı v1.0 — 2026-07-31

> **Review type:** plan-review
> **Previous review:** spec zinciri (171→173 APPROVED)
> **Upstream document(s) read:** `docs/specs/musteri-portali-spec.md` (45 kriter — bu oturumda üç tur okundu) · index + 4 chunk **bütün olarak** okundu
> **Other cross-referenced documents / kod:** `docs/arch/mimari-kararlar.md` AD-09 **tam metin** (84–105 dahil), AD-11, AD-20 · `app/Support/Business/DiscountCeiling.php` (varlık+konum), `app/Support/Database/TenantPolicy.php`, `app/Domain/Shared/ReferencePrefix.php`, `vite.config.js`, `database/migrations/2026_08_14_000500`, `app/Queries/Customer/PanelAccess.php`, `app/Models/CustomerUser.php`, `app/Policies/TrainingParticipantPolicy.php` · `docs/specs/egitmen-spec.md:417`
> **Stage locks in effect:** Spec kriterleri ve brainstorm kararları kilitli.

---

## Upstream Coverage Matrix (45 kriter — bağımsız tarandı)

45 kriterin 45'i chunk'lara dağıtılmış; chunk içi kapsama tabloları örneklemle doğrulandı (M-13/M-35/M-37/M-39/M-40/M-41/M-42/M-43/M-44/M-45 dahil). İki eşleme sorunu: **M-36** yanlış karşılığa bağlanmış (F-P6) ve **M-12'nin plan karşılığı AD-09'un güncel haliyle çelişiyor** (F-P2). M-33'ün iki yarısının iki chunk'a bilinçli bölünmesi işaretli ✓.

## Sorulara kısa cevaplar

1. **İki geçiş:** enrolments geçişi güvenli kurgulanmış (kırmızı çizgi 5 tetiklenmiyor: drop yok, taşıma idempotent, düşürme ayrı ticket) — ama **eski tablonun üç canlı okuyucusu planda yok** (F-P3). `training_requests` geçişinde **policy taslağı kendi negatif testiyle çelişiyor** (F-P1).
2. **P-4/P-5:** kurgu spec §10.1'i karşılıyor — policy'nin yokluğu gerekçeli-yorumlu, alan düzeyi query'de, policy-katmanı negatif testi chunk4'te var. Kalan risk F-P1'in yardımcı tabloları.
3. **Migration sırası:** bağımlılık hatası bulunamadı (4→5→6 zinciri, thread'ler kendilerine işaret eden kayıtlardan önce, `invoice_notifications.thread_id` chunk 3'ten sonra) ✓.
4. **Test stratejisi:** AD-26 ile uyumlu; eksik tek sınıf **audit** (F-P7).
5. **Plan sınırı:** spec kararlarını yeniden tartışan yer yok; ters yönde iki ihlal — `valid_until`/`expired` spec'te olmayan davranış (F-P4), `min()` daraltması güncel AD-09'la çelişen davranış (F-P2).
6. **Caller kuralı:** chunk 2 backbone'u kapanış adresiyle teslim ediyor (`41`/`50` sonraki tur), caller'lı parçası (`ComplianceSummary` → chunk 1 dashboard) açıkça işaretli — onay kuralı 4 istisnasına uygun ✓.

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

**F-P1 · `training_requests` RLS taslağı kendi negatif testiyle çelişiyor — kırmızı çizgi 1 alanı.** Chunk3 §1.3'ün policy'si: `USING (tenant_id IS NULL OR tenant_id = ANY(app.tenant_ids()))`. Müşteri oturumu da `app.tenant_ids()` kurar (kendi tenant'ı) — bu policy'de **ilk koşul müşteri oturumuna da açıktır**: B firmasının kullanıcısı, RLS düzeyinde `tenant_id IS NULL` olan **tüm satış taleplerini okur**. Negatif test (c) *"müşteri kullanıcısı satış talebini hiç göremez"* diyor — taslak policy o testi **geçemez**; uygulama katmanı doğru süzse bile RLS'in son savunma hattı olması kuralı (kırmızı çizgi 1) delinmiş olur. Policy, kimlik alanını ayırt etmek zorunda (ofis bağlamında NULL satır görünür, müşteri bağlamında görünmez); mevcut `TenantPolicy` yalnız `app.tenant_ids()` bilir — bu ayrımın mekanizması planda tasarlanmalı. **Aynı bulgunun ikinci yarısı:** yardımcı tabloların (`training_request_definitions`, `training_request_invitees`, `training_request_scopes`) policy durumu hiç yazılmamış; `training_request_invitees` **davet edilen firmaların kimliklerini** taşır — bir müşteriye sızması doğrudan çapraz-tenant ticari sızıntıdır.

**F-P2 · `min(globalMax, sellerMax)` daraltması, AD-09'un 28.07.2026 revizesiyle çelişiyor — ve U-2 kaydı yanlış.** AD-09'un tam metni (satır 95–101) kuralı **yazıyor** ve spec'in/planın kullandığı halinden farklı: *"Genişletme engellenmez, onay akışına düşer"* — eski *"genişletme denemesi hata verir"* hali **kaldırılmış** (Sets: *"patron %25'ken bu müşteriye %30 dedi; yapamayacaklar mı?"*). EGT-58'in tarif ettiği hata da tam bu: `DiscountCeiling` genişletmeyi *"ne hata veriyor ne onaya gönderiyor"*. Chunk4'ün `min()` yaklaşımı üçüncü bir davranış üretiyor: tanımlanmış (belki onayla verilmiş) geniş limiti **sessizce ezip** globalin altına kırpar — onaylı istisna yok sayılır, EGT-58 "kapandı" denir ama AD-09'un istediği onay yolu yine kurulmamış olur. Zincirleme düzeltme gerekiyor: (a) plan `min()` yerine "genişletme onaya düşer" semantiği; (b) **upstream:** spec §5.2.1'in *"daraltılır, genişletilemez"* cümlesi ve §12 U-2 kaydı (*"AD-09'un metni yazmıyor"*) düzeltilmeli — metin yazıyor, revize haliyle. *(Süreç kabulü: round 2'de U-2'yi onaylayan bu review'du ve AD-09'u yalnız 78–86 arası okumuştu — eksik doğrulamanın üçüncü örneği, bu kez reviewer'da.)*

**F-P3 · Eski tablonun üç canlı okuyucusu geçiş planında yok.** "Kod yalnız yeni tabloyu okur" (index §4) hedefine rağmen `training_participants` bugün üç yerden okunuyor: `app/Queries/Customer/PanelAccess.php` (katılımcı yeteneğinin türetildiği yer — omurga U-1/U-2 kapısı), `app/Models/CustomerUser.php`, `app/Policies/TrainingParticipantPolicy.php`. Chunk2 bu okuyucuların `training_enrolments`'a geçirilmesini listelemiyor; geçirilmezse yeni kayıt alan katılımcılar **panel yeteneği alamaz** (PanelAccess eski, boş tabloya bakar). `TrainingParticipantPolicy`'nin kaderi (yeniden adlandırma/yeni policy) de yazılmalı. Index §7.3'ün taraması bunu sonda yakalar ama plan, bilinen okuyucuları şimdiden saymalı — tarama bilinmeyenler içindir.

**F-P4 · `valid_until` + `expired` spec'te yok — plan karar icat ediyor.** Chunk4 §1.1 teklife geçerlilik tarihi ve `expired` statüsü koyuyor. Spec'te (ve 31 kararda) teklif geçerlilik süresi diye bir davranış yok; brainstorm F-1 tartışmasında "rezervasyonun ömrü" sorusu K-20'ye bağlanmış ama süre/expiry hiçbir karara girmemişti. Kim expire eder (otomatik ise K-24'ün "sistem cevaba bakıp ilerletmez" felsefesiyle ve "sistem kendiliğinden veri silmez/karar vermez" çizgisiyle ilişkisi), süresi kim tanımlar (AD-08 — kodda sabit olamaz), dolunca kalemlerin `held` rezervasyonu düşer mi (kontenjan sayacını doğrudan etkiler) — bunlar spec kararı ister. Ya Sets'e sorulup spec'e eklenir ya kolonlar bu turdan çıkar; plan bu kararı sessizce veremez.

**F-P5 · `DiscountCeiling`'in konumu ve kaynağı yanlış.** Sınıf bugün `app/Support/Business/DiscountCeiling.php`'de (callers: `BusinessSettings`, `Settings`, `UpdateBusinessSettings`); chunk4 §2 onu `app/Domain/Sales/DiscountCeiling.php` diye yazıyor ve *"yeniden yazmaz, düzeltir"* diyor — ikisi aynı anda olmaz: ya mevcut dosya yerinde düzeltilir ya taşınır; taşınıyorsa üç caller'ın güncellenmesi plana girer. Kaynak atfı da yanlış: EGT-58 `docs/issues.md`'de değil, `egitmen-spec.md:417` ve AD-09:101'de kayıtlı.

**F-P6 · M-36 yanlış eşlenmiş — maliyet davranışının plan karşılığı yok.** Chunk4 kapsama tablosu M-36'yı ("satışçı maliyeti görür, değiştiremez") "quote_items + fiyat dondurma"ya bağlıyor; fiyat ≠ maliyettir. Maliyetin satışçı ekranına nereden geldiği (planlama verisi), salt-okunurluğunun nerede zorlandığı ve testi planın hiçbir yerinde yok. (Müşteriye maliyet sızmaması M-14'te var; satışçı tarafı eksik olan.)

**F-P7 · Audit'in test karşılığı yok.** Index §6.4 audit kapsamını doğru tanımlıyor ve chunk4 kapsaması M-32'yi "audit"e bağlıyor, ama dört chunk'ın test planlarının hiçbirinde audit senaryosu yok: kritik işlemin `audit_writes` satırı ürettiği, yazımın **aynı transaction'da** olduğu (AD-06) ve hassas okumaların `audit_access`'e düştüğü (AD-21 — chunk1'in TCKN açma eylemi dahil) hiçbir grupta assert edilmiyor. AD-26 test sınıfları arasında bu dikeyin en kritik eksiği bu.

## ❓ Open Questions

1. F-P4'ün spec sorusu: teklif geçerlilik süresi bir karar mı olacak (kim tanımlar, dolunca rezervasyon ve statü ne olur), yoksa bu turdan çıkacak mı?

## ⚠️ Upstream Findings

- **Spec §5.2.1 + §12 U-2 düzeltilmeli** (F-P2'nin upstream ayağı): AD-09'un güncel metni "genişletme onaya düşer" diyor; spec'in "genişletilemez" cümlesi ve "AD-09 metni yazmıyor" kaydı eski/eksik okumaya dayanıyor.

## Notlar (blocker değil)

- Zemin tablosundaki diğer iddialar doğrulandı: `vite.config.js` girişleri (app/office, portal yok) ✓, `ReferencePrefix` `TK`/`FT` ✓, `TenantPolicy::apply` imzası ✓, `consents` ✓, `AccountStatus` ✓.
- Chunk3'ün `party` kolonu tasarımı (mesaj düzeyi görünürlük) spec M-42'nin doğru mekanizması; `CorrespondenceThread` süzgecinin bileşende değil query'de olması kuralı da yerinde.
- Rollout bölümünün "geri alma kod ile, migration ile değil" kuralı kırmızı çizgi 5/AD-11 ile tutarlı.

## Verdict: ❌ REJECTED

Planın omurgası sağlam: 45 kriter dağıtılmış, migration sırası doğru, P-1…P-8 kararları gerekçeli, caller disiplini ve test grupları yerinde. Verdict'i düşüren şeylerin ikisi tek başına yeterli: **F-P1** — RLS taslağı kendi negatif testini geçemez ve yardımcı tablolar (davet edilen firma listesi dahil) policy'siz tanımlanmış, bu kırmızı çizgi 1'in tam ortasıdır; **F-P2** — indirim tavanının `min()` daraltması AD-09'un 28.07 revizesinin tam olarak kaldırdığı davranıştır ve EGT-58'i kapatmaz, yanlış kapatır (upstream'de spec §5.2.1/U-2 düzeltmesiyle birlikte). F-P3 ve F-P4 plan eksikliği; F-P5–F-P7 metin/eşleme işleri. Tamamı kapandığında plan implementasyona hazırdır.
