# Review — docs/plans/sube-plan.md v1.0 — 20.10.2026

> **Review type:** plan-review
> **Previous review:** N/A (planın ilk review'ı; spec zinciri: 815 ✗ → 816 ✗ → 817 ✓ → 818 ✗ → 819 ✓)
> **Upstream document(s) read:** `docs/specs/sube-spec.md` v0.6 (heyet 819 APPROVED) — tamamı, §1–§12 + 27 kabul kriteri
> **Other cross-referenced documents:** `proje-haritasi.md` §4.4 · `CLAUDE.md` (17 kırmızı çizgi) · AD-04/06/08/09/11/12/21/23/25/26 · kod doğrulamaları (bu review'da koşuldu): `AuthServiceProvider.php:205-214` (Gate::before yalnız argümansız kontrolü `PermissionFamilies::allows`'a verir), `PermissionFamilies.php:87-107`, `ScopeResolver.php:44-70` (`groupsFor` → `reachableGroups`), `2026_08_13_000200_scope_permission_names.php` (P-16'nın deseni var), `2026_09_30_000100_…scope….php:81` (CHECK), `config/authorization.php:356` + `InvoicePolicy.php:39-50` (`invoice.view` kapsamsız), `Settings.php:385-399` + `QuotePrintout.php:116` / `FrameworkContractPrintout.php:83` / `TrainingController.php:1001` / `BusinessSettings.php:103` (şirket kimliğini okuyan dört yer; önceki turda doğrulanmıştı), production `Invoice::count()` = 0 (818 turunda canlıda doğrulandı)
> **Stage locks in effect:** **Spec locked** (v0.6, heyet 819). Spec kararları tartışılmadı; yalnız planın spec'e uyumu ve planın kendi getirdiği riskler değerlendirildi. Ek kilitli karar: P-8 (Sets teyidi — onay yetkiden ve kapsamdan gelir).

---

## Upstream Coverage Matrix

| Upstream § / madde | Plan'da durumu | Kanıt |
|---|---|---|
| §2.1 şube kaydı + dikey düğümler, AD-23 ağacı değişmez | ✓ | P-1 (`branches` + `groups.branch_id`, `unique(branch_id, vertical)`), A1 |
| §2.2 üç yetenek, kurulurken seçim | ✓ | P-1 (üç bool + CHECK en az biri), `OpenBranch`, A1 |
| §2.3 menüler değişmez (kriter 21) | ✓ | A1 test listesi "menü kalemleri eşit" |
| §3 yetenek tavanı: verilemez + verilmişse çalışmaz + düğüm başına (kriter 3, 23) | ✓ | P-3 (tek sınıf, iki uygulama noktası: `PermissionFamilies::ask` ve `ScopeResolver::reachableGroups`) + P-4 (alıcıya göre ekran, `blocked_reason='capability'`); Gate::before'un yalnız argümansız kontrolü karşıladığı ve argümanlı soruların `ScopeResolver` üzerinden geçtiği koddan doğrulandı — iki nokta birlikte her iki yolu kapatıyor |
| §3 merkez üyeliği tavana takılmaz (kriter 9) | ✓ | P-3 "branch_id null düğümler takılmaz; hiç üyeliği olmayan kişi de"; her aşamada «merkez kullanıcısı aynı sonucu alır» testi (global kısıt) |
| §4 kurulum backoffice'te; yaşam döngüsü audit'li | ✓ | A1 `BranchResource` + `OpenBranch`/`AddBranchCapability`, audit `branch.opened` |
| §4 yetenek kaldırma / kapatma: uyarı → atama soruları → taşı (kriter 10) | ⚠ | A7 `Unwinding` düğüm üyelerini, kesilmemiş faturaları ve açık eğitimleri listeliyor; **spec §4'ün açıkça saydığı «açık teklif … potansiyel müşteri kime» soruları listede yok** → F-3 |
| §5 kullanıcı açma + şube dışını görmeme (kriter 4, 22) | ✓ | A1 (mevcut `user.create.group` + şube düğümleri; test listesi) |
| §6 Müşteri satırı + açanın ataması (kriter 20) | ✓ | A1 adım 6 (`OnboardCustomer`, audit'li, `customer.view.all` olsa da zararsız) |
| §6 Kullanıcı / Potansiyel / Teklif satırları (kriter 2) | ✓ | A1 test listesi; teklif/potansiyel bugünkü kapsam makinesi + şube düğümleriyle doğal daralır |
| §6 Talep satırı (kriter 2 "her satırı ayrı test") | ⚠ | "bugünkü kural" — iş değişikliği gerekmiyor, doğru; ama kriter 2 her satır için ayrı test ister ve **talep satırının testi hiçbir aşamanın test listesinde yok** → F-5 |
| §6 kapsamsız eğitim yalnız merkeze (kriter 11) | ✓ | P-6 («kapsamsız = merkez», veri yeniden yazılmaz), A2 testleri |
| §6 Takvimim/kokpit (kriter 12) | ✓ | P-11 `SalesWorkspacePolicy::open` = `quote.view` ∨ `potential_customer.view`; A4 |
| §7.1 planlarken seçim, varsayılan kendi şubesi, genişletme onayı (kriter 5, 13) | ✓ | A2; P-8 (`training.widen_visibility.{group,all}` + `training_visibility_requests` + `ApprovalKind::VisibilityWidening`; kendi isteğini onaylayamaz testi) — Sets'in P-8 teyidi brief'te |
| §7.2 merkez eğitiminde şube seçimi (kriter 6) | ✓ | A2 adım 1 (açık ve kapalı ikisinde de) |
| §8 eğitmen açma, aday listesi, merkez ataması (kriter 7, 14, 15, 24) | ✓ | P-9 (`instructor_branch_access`, eğitmen dosyasında mevcut çoklu seçim), P-10, A3 |
| §9 mali: şube kendi rakamı, merkez kırılımı (kriter 8) | ✓ | P-19 (`FinancialOverview` okuyanın şube kümesi; yeni yetki yok), A6 |
| §9.1 3E şirketleri + sıralı numara + eski faturalar (kriter 26) | ✓ | P-12, P-13 (`SELECT … FOR UPDATE`, transaction içi — boşluksuz/tekrarsız/eşzamanlı güvenli), A5 adım 2 (iptal numarayı korur, eski numarasız dokunulmaz); A5 testlerinde eşzamanlı iki bağlantı |
| §9.1 banka hesapları + yasal ibare + TCKN/erişim logu (kriter 27'nin verisi) | ✓ | P-12 (`issuing_company_bank_accounts`), P-14 snapshot'a dahil; belge ekranı bilinçli dışarıda, adres yazılı (Sets kararı) |
| §9.2 kesimde sabitleme (kriter 18) | ✓ | P-14 (`issuer_snapshot` jsonb: unvan, vergi dairesi/no, adres, telefon, banka hesapları, ibare, antet şablon id) — şirket sonradan değişse de belge o anın kopyası |
| §9.2 şirket değiştirme yetkisi — **fatura ve teklifte AYNI yetki, yeteneğe bağlı değil** | ❌ | Plan **iki ayrı yetki** tanımlıyor (`invoice.choose_issuer` bağlı değil; `quote.choose_issuer` **satış yeteneğine bağlı**); spec §9.2 "teklifte şirket değişikliği de **aynı yetkiyle** yapılır", §11.1 "Fatura kesen şirketi değiştirme **ayrı bir yetkidir ve yeteneğe bağlı değildir**" (tekil) → F-1 |
| §9.2 teklif anteti şirkete göre | ⚠ | P-17 doğru; ama şirket kimliğini okuyan dört yerden yalnız ikisi (QuotePrintout, SendQuote) A5 dosya listesinde — **çerçeve sözleşme, yoklama anteti ve `BusinessSettings` `company.*` editörünün akıbeti tanımsız; iki kaynak riski** → F-2 |
| §9.3 gelir şubesi: değer kümesi, üç varsayılan, kesim sonrası değişmezlik (kriter 16) | ✓ | P-15 (`revenue_branch_id` null=merkez; action kuralı + test), A5 adım 3 |
| S-12 fatura kapsamı (kriter 17) + kimse daha az görmez (kriter 9) | ✓ | P-16 (`invoice.view.{own,group,all}`, mevcut sahipler `.all`'a — `scope_permission_names` deseni koddan doğrulandı) |
| §9.4 gider şubesi (kriter 19) | ✓ | P-18 (`trainings.cost_branch_id`, varsayılan planlayan, audit), A6 |
| §9.5 kapanışta mali kayıtlar (kriter 25) | ✓ | A7 testleri (şirket/numara/gelir şubesi aynen, şirket açık, geçmiş rapor okunur, iptal kendi şirketiyle) |
| §10 kriter 1–27 eşlemesi | ⚠ | Aşama tablosu 27 kriteri paylaştırıyor; F-1/F-3/F-5 dışında eksiksiz |
| §12 kapsam dışı adresleri | ✓ | Planın "kapanış adresi" tablosu (belge ekranı, ibare metni, e-fatura, EGT-220) |
| AD-04/RLS | ✓ | Yeni tablolar 3E kaydı → `tenancy.exempt_tables` + `TenancyTest` sabit listesi; EGT-216 K-3/K-25 deseni |
| AD-06 audit işlemle aynı transaction | ✓ | Global kısıt + A7 tek transaction |
| AD-08 / kırmızı çizgi 9 | ✓ | P-2 eşlemesi yetki sözlüğü (config + arch testi) — mevcut izin tanımlarının yaşadığı yer; iş parametresi değil |
| AD-11 silinmezlik | ✓ | Global kısıt "silme yok"; `restrictOnDelete`; A7 pasifleştirir |
| AD-25 kod yerleşimi | ✓ | Global kısıt + dosya adresleri (Domain/Branches, Actions, Queries, Policies) |
| AD-26 test disiplini | ✓ | Grup etiketleri, FrozenClock, factory; `omurga-rls` A5'te |
| Migration güvenliği / sıra | ❌ | Kolon düşmüyor/yeniden adlanmıyor (✓); **ama `branches.issuing_company_id` FK'sı A1'de tanımlı, referans verdiği `issuing_companies` tablosu A5'te kuruluyor — A1 migration'ı bu hâliyle koşamaz; A1–A5 arasında kurulmuş şubeler için nullable→zorunlu geçişinin backfill'i de tanımsız** → F-4 |
| Rollout / rollback | ✓ | Aşamalı deploy; adım 2 (eksik kimlikli şirketle kesim reddi) production'da fatura 0 olduğu için etkisiz — rakam 818 turunda canlıda bağımsız doğrulanmıştı; rollback dürüst (veri sonrası şema geri alınmaz) |

---

## ✅ What works

- **"Doğrulanmış mevcut durum" tablosu gerçekten doğru:** planın dayandığı yedi kritik kod iddiasını bu review'da yeniden koştum — Gate::before'un yalnız argümansız kontrolü `PermissionFamilies`'e vermesi, `ScopeResolver::groupsFor→reachableGroups` zinciri, `invoice.view`'ın kapsamsızlığı, izin taşıma migration deseninin varlığı, `training_scopes` CHECK'i, şirket kimliğini okuyan dört yer, production'da 0 fatura. Hepsi tuttu.
- **P-3'ün iki uygulama noktası doğru seçilmiş:** izin kontrolünün iki yolu var (argümansız → `Gate::before`/`ask`; argümanlı → policy → `ScopeResolver`) ve tavan tam bu iki boğaza konuyor; tek sınıf (`BranchCeiling`) ekran/eylem/sorgu ayrışmasını da önlüyor. Merkez üyeliğinin muafiyeti ve "hiç üyeliği olmayan kişi" davranışı kriter 9'u koruyor; her aşamada merkez-değişmezlik testi şart koşulmuş.
- **P-13/P-14 sağlam:** satır kilidi + aynı transaction = boşluksuz ve eşzamanlı güvenli numara; snapshot kesim anının tam kopyası (banka hesapları ve ibare dahil) ve belge turu gelmeden verisi hazır. Tarih sabitli CHECK yerine action kuralı tercihi gerekçeli.
- **P-6/P-7 dürüst ve mekanizmalı:** kapsam okumayan beş liste + detay tek tek sayılmış, envanter arch testiyle sabitleniyor — gelecekte eklenen bir liste sınıfının kapsamı atlaması derlenemez hâle geliyor.
- **P-8, AD-23'ün ruhuna uygun** (rol adı değil yetki+kapsam) ve Sets teyitli; dört göz kuralı test listesinde.
- Rollout adım 2 doğru bir emniyet: vergi numarasız/öneksiz şirketle kesim reddi, yanlış tüzel kimlikli belge basmaktan iyidir ve production'da 0 fatura olduğu için kesinti riski yok; deploy notu şartı yazılı.

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

**F-1 — Şirket değiştirme yetkisi spec'e aykırı bölünmüş.** Spec §9.2: *"teklifte şirket değişikliği de **aynı yetkiyle** yapılır"*; §11.1: *"Fatura kesen şirketi değiştirme (§9.2) **ayrı bir yetkidir ve yeteneğe bağlı değildir**"* — tekil bir yetki, yeteneksiz. Plan iki yetki tanımlıyor (`invoice.choose_issuer` → bağlı değil; `quote.choose_issuer` → **satış yeteneğine bağlı**) ve bölünmenin hiçbir gerekçesi yazılı değil. Sonuç spec'ten farklı davranır: satış yeteneği kapalı bir şubenin üyesi faturada şirket seçebilirken teklifte seçemez; spec ikisini aynı yetkiye bağlamıştı.

**F-2 — Şirket kimliğinin A5 sonrası iki kaynağı; eski okuyucuların akıbeti tanımsız.** Kimliği bugün dört yer okuyor (`QuotePrintout`, `FrameworkContractPrintout`, `TrainingController` yoklama anteti, `BusinessSettings` ekranı — hepsi `Settings::companyLetterhead()` / `company.*`). A5 dosya listesi yalnız `QuotePrintout` + `SendQuote`'u değiştiriyor; P-12'nin *"bugünkü teklif çıktısı ve yoklama aynı kimliği okumaya devam eder (varsayılan şirket)"* cümlesi hangi kaynaktan okuyacaklarını söylemiyor ve `BusinessSettings`'teki `company.*` editörünün akıbeti (kaldırılır mı, `issuing_companies` varsayılanına mı yönlenir, ikisi birden mi yaşar) planda yok. İki ayrı yerde düzenlenebilen tek kimlik, imzalanan yoklama kâğıdına ve çerçeve sözleşmeye bayat unvan/adres basılmasıyla sonuçlanır — spec §9.1 "bugünkü tek kimlik bu listenin **ilk kaydı olur**" derken tek kaynağa geçişi tarif ediyordu.

**F-3 — A7 `Unwinding` açık teklif ve potansiyel müşteriyi sormuyor.** Spec §4'ün atama soruları açıkça sayılı: *"kişi hangi merkez düğümüne, **açık teklif/eğitim/potansiyel müşteri** kime"*. Planın `Unwinding`'i düğüm üyelerini, kesilmemiş faturaları ve açık eğitimleri listeliyor; açık teklifler ve potansiyel müşteriler listede ve soru akışında yok. "Kişi taşınınca işi de taşınır" varsayımı spec'in akışını karşılamaz — spec kişinin nereye gideceğini ve işin kime devredileceğini **ayrı ayrı** sorduruyor (satış yeteneği kaldırılan şubenin üyesi planlama düğümüne taşınırken teklifleri başka bir satışçıya devredilebilmeli). Kriter 10'un testi de bu hâliyle eksik senaryoyla yazılır.

**F-4 — Migration sırası hatası: `branches.issuing_company_id` FK'sı A1'de, `issuing_companies` A5'te.** Veri modeli `branches` (A1) tanımına `issuing_company_id FK nullable (A5'te zorunlu hâle gelir)` koyuyor; FK'nın referans verdiği tablo dört aşama sonra kuruluyor — A1 migration'ı bu hâliyle koşamaz. Ayrıca A5'te kolonun zorunlu hâle gelmesi, A1–A5 arasında kurulmuş (şirketsiz) şubeler için backfill kuralı tanımlanmadan yapılamaz; plan bunu söylemiyor. (Kolonu A5'te eklemek ya da A1'de FK'sız bırakıp A5'te bağlamak gibi yollar var — hangisi olduğu planın kararı; bulgu, planın şu anki hâlinin uygulanamaz olmasıdır.)

**F-5 — Kriter 2'nin "her satırı ayrı test" şartında Talep satırı testsiz.** §6 tablosunun satırları aşamalara dağıtılmış (müşteri/kullanıcı/teklif/potansiyel A1, eğitim A2, eğitmen A3, takvim A4, mali A6) ama **Talep** (`TrainingRequestQueue`) hiçbir aşamanın test listesinde yok. "Bugünkü kural" doğru — iş değişikliği gerekmez — ama kriter 2 satırın şube üyesinde doğru daraldığının ayrı testini ister; davranışı koruyan şey de tam o testtir.

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

1. `invoice.view.{own,group}`'un fatura eşlemesi (own = `issued_by`, group = `revenue_branch_id` ∈ okuyanın şube kümesi, merkez faturası = merkez üyeliği) spec S-12'de tanımlı; planın `InvoicePolicy` değişikliği bu eşlemeyi P-19'un "şube kümesi" kavramıyla aynı yardımcıdan mı okuyacak? (Bulgu değil — implementasyonda iki ayrı küme tanımı doğarsa tutarsızlık riski; tek cümlelik netleştirme yeter.)

## ⚠️ Upstream Findings

Yok. (F-1 spec'in ihlalidir, spec'in hatası değil.)

## Verdict: ❌ REJECTED

Planın omurgası sağlam — tavanın iki uygulama noktası, numara kilidi, snapshot, liste envanteri ve rollout emniyeti doğru kurulmuş, dayandığı kod iddialarının tamamı bu review'da doğrulandı. Ama matrix'te 2 ❌ ve 3 ⚠ var: spec'in tekil "şirket değiştirme yetkisi" kararı ikiye bölünüp birine yetenek bağlanmış (F-1), şirket kimliği iki kaynaklı kalıyor (F-2), kapanış akışı spec'in saydığı iki iş türünü sormuyor (F-3), A1 migration'ı yazıldığı hâliyle koşamaz (F-4) ve kriter 2'nin bir satırı testsiz (F-5). Beşi de plan scope'unda; REJECTED.
