# Review — Şube yapısı implementasyonu (A1–A7, EGT-219) — 20.10.2026

> **Review type:** impl-review
> **Previous review:** N/A (implementasyonun ilk review'ı; plan zinciri 820 ✗ → 821 ✗ → 822 ✓, spec zinciri 815…819 ✓)
> **Upstream document(s) read:** `docs/plans/sube-plan.md` v1.2 (heyet 822 APPROVED) — tamamı · `docs/specs/sube-spec.md` v0.6 (heyet 819 APPROVED) — kabul kriterleri 1–26 (+27'nin veri yarısı)
> **Other cross-referenced documents:** `CLAUDE.md` (17 kırmızı çizgi + yardım kuralı) · `proje-haritasi.md` §4.4 · AD-04/06/08/11/23/25/26
> **Stage locks in effect:** **Spec + Plan locked.** Plan kararları tartışılmadı; yalnız kodun plana uyumu ve yeni risk arandı.
> **Kod aralığı:** `6f40dd42..HEAD` — 11 feature commit + docs + **44362bba** (yardım ekran görüntüleri; brief yazıldıktan sonra eklenmiş, aralığın içinde). Working tree temiz.
> **Bu review'da bizzat koşulan doğrulamalar:** 12 yeni test dosyasından 76 test (`pest` hedefli, 76/76 geçti — BranchCeiling, BranchVisibility, BranchUnwinding, IssuingCompany, BranchFinance, InvoiceNumberRace, BranchInstructor, SalesWorkspace, Branch, CompanyIdentity, TrainingListInventory, Authorization arch) · `pint --test --dirty` temiz · `composer analyse` 0 hata · yenilenen `planlama-egitim-yeni.png` **açılıp bakıldı** («Kimler görsün» bloğu görünüyor). Geliştiricinin tam lokal koşusu (2320 + 378 RLS) rapor edildi; **TeamCity ve deploy brief gereği review sonrası** — teslimden önce yeşil CI şarttır.

---

## Üç zorunlu tarama

**1 · Kırmızı çizgi taraması (17/17):** ihlal yok. Öne çıkanlar: tenant — yeni tablolar gerekçeli muafiyetle `tenancy.exempt_tables`'ta (3E'nin öz kayıtları), fatura kuralı değişmedi, portal sorgusu (`MyInvoices`) müşterinin kendi faturası; `SET LOCAL` dışı session SET yok (yarış testi dahi `SET LOCAL lock_timeout` kullanıyor); her yeni uç yetki doğruluyor (dosya+satır matriste); raw SQL yalnız muaf tablolarda (`Group::branchesOf`, `InvoiceNumbers`); veri kaybı yok (yalnız ekleme; `training_scopes` CHECK yeniden kuruluşu eski satırları koruyor — migration'da test edilen dönüşüm); `env()` ve credential diff'te yok; iş kuralı hardcode'u yok (yetenek×yetki haritası plan P-2'yle onaylı yetki sözlüğü, arch testli); **test silinmedi** — `BusinessSettingsTest`'in letterhead testi plana uyan yeni davranışı kanıtlayacak şekilde yerinde yeniden yazıldı ve bunu kendi yorumunda beyan ediyor ("it changed with the decision, not by accident"); AI attribution yok; hard delete yok (kaynaklarda Delete action yok, "never deletes a branch" testi; `BranchUnwinder::seat`'teki tek satır silme, hedefte zaten var olan üyeliğin mükerrerini önleme — kişi ve iş kaydı silinmiyor, docblock beyan ediyor); kritik işlemler audit'li ve **işlemle aynı transaction'da** (OpenBranch/CloseBranch/RemoveBranchCapability/ChangeQuoteIssuer/ChooseCostBranch/OpenInstructorToBranches/IssueInvoice — hepsi `DB::transaction` içinde `AuditWriter::record`), kiracısız kayıtlar `officeRecord: true` beyanıyla; mevzuat değeri yok; AD-25 — `app/Domain/Branches/{Capability,Unwinding}` ve `app/Domain/Invoicing/InvoiceNumber` framework'süz, controller'larda `validate()` yok (yeni FormRequest'ler), SDK yok; AD-26 — grup etiketleri var, testlerde çıplak `now()` yok (`moment()`/`FrozenClock`), yarış testi RLS iddiası değil kilit iddiası test ediyor; hazır bileşen — yeni alanlar mevcut `RadioCardGroup` (EGT-65/74'ten) ve mevcut çoklu seçimle; kişi bazlı izleme yok.

**2 · Wire taraması (feature teslimi):** rotalar kayıtlı (`office.quotes.issuer`, `office.trainings.cost-branch` — `routes/office.php:605,771`); `SalesWorkspacePolicy` `Gate::policy` ile elle bağlı (`AuthServiceProvider.php:73`, model olmadığı için discovery bulamazdı — gerekçeli); `BranchCeiling` scoped + ağaç yazımlarında `forget` (`AuthServiceProvider::forgetBranchCeilingOnTreeChanges`); onay kuyruğu dalı `PendingApprovals::visibilityWidenings` + `ApprovalController::decide` match kolu + `subjectOf`; menü kalemleri `OfficeNavigation`'da yeni policy'ye bağlandı; Filament kaynakları testlerle sayfa düzeyinde çalıştırıldı (BranchTest, IssuingCompanyResourceTest, BranchUnwindingTest "offers the flow on the branch screen"); liste daraltmaları gerçek sorgulara bağlı (`VisibleTrainings:87`, `QuotableTrainings:60`, `PlanningCalendar:189,280`, `PlanningAgenda` beş nokta, `TrainingPolicy::view`); fatura listesi+detayı `InvoicesInReach` (controller:42 + policy:55); portal faturada numara+şirket (`Portal/Invoices.vue`, `MyInvoices`).

**3 · Dil taraması:** kod tarafında Türkçe tanımlayıcı/yorum yok (diff genelinde tarandı); `lang/en` + `lang/tr` çiftler hâlinde; rota ADLARI İngilizce (URL segmentlerinin Türkçe olması deponun yerleşik sözleşmesi); dokümantasyon blokları İngilizce, Türkçe alıntılar `arch-allow: turkish-content` ile işaretli (mevcut desen).

---

## Upstream Coverage Matrix

### Plan kararları (P-1…P-20)

| Plan maddesi | Durum | Kanıt (dosya+satır) |
|---|---|---|
| P-1 `branches` + `groups.branch_id`, `unique(branch_id, vertical)` | ✓ | `2026_11_01_000100…:35-70`; alt düğüm şubeyi atasından alır: `Group::branchesOf` özyinelemeli CTE (`Group.php:116-135`) — brief sapma (2), P-1'in "yalnız şubenin kendi düğümü id taşır" cümlesinin doğru uygulaması |
| P-2 yetenek→yetki haritası config + arch testi | ✓ | `config/authorization.php` `office_capabilities` (spec §11.1 ile satır satır eşleşiyor; `potential_customer.*` somut adlara açılmış); `tests/Arch/AuthorizationTest.php:225-254` — tam-bir-listede + mükerrer + bayat ad kontrolü |
| P-3 tek sınıf tavan, iki uygulama noktası | ✓ | `BranchCeiling.php` (tam okundu); (a) `PermissionFamilies::allows:106-118` — yalnız Office ailesi + User; (b) `ScopeResolver::reachableGroups:164-169` `ceiling->nodesFor` ile; ek olarak `afterLevel:136-146` da tavanlı düğümlerle süzülüyor (yeteneksiz şubedeki yöneticilik seviyesi kapsamı genişletemez) |
| P-3 merkez/üyeliksiz kişi tavana takılmaz (kriter 9) | ✓ | `BranchCeiling::permits:49-66` — capability null → true; üyelik yok → true; merkez düğümü (`branch=null`) → `nodeMay` true. BranchCeilingTest:79 'leaves the centre exactly as it was' — **koştum, geçti** |
| P-4 yetki ekranı alıcıya göre + `AssignOfficePermissions` aynı tavan | ✓ | `OfficePermissionMatrix` (granter+subject), `PermissionMatrix.vue` `blocked_reason='capability'` metni, `OfficeUserForm`; BranchCeilingTest:137,160 (ekran kapatır + eylem reddeder + verilmiş yetki çalışmaz) |
| P-5 `scope_type='branch'` + eşleşme tavanlı erişilen düğüm | ✓ | `2026_11_01_000200…` CHECK dört tür; `TrainingsInScope::matchGroupOrBranch` + `branchesReachedBy` |
| P-6 kapsamsız = merkez; merkez okuyucu aynı | ✓ | `TrainingsInScope::apply` — merkez dalında `whereNotExists` korunur, `narrowForBranch` kapsamsızı hiç eşlemez; BranchVisibilityTest:161 (kriter 11), :264 (kriter 9) |
| P-7 her listeye + detaya kapsam; envanter arch testte | ✓ | `TrainingListInventoryTest.php` — gerekçeli muafiyet listesi + bayat muafiyet kontrolü; `TrainingPolicy::view:52-57` → `lets()`; beş planlama yüzeyi bağlı (wire taraması) |
| P-8 `training.widen_visibility.{group,all}`, istek tablosu, yetki+kapsam onayı, dört göz | ✓ | config:189-191; `TrainingVisibilityRequestPolicy::decide:35-50` (önce kendi-isteği reddi, sonra yetki + `scope->covers`); BranchVisibilityTest:213,225,238 — sonradan yetki verilse bile kendi isteğine karar yok |
| P-9 `instructor_branch_access`, eğitmen dosyasında `instructor.update` | ✓ | `2026_11_01_000300…`; `OpenInstructorToBranches:10` authorize + audit; `Instructors/Show.vue` mevcut çoklu seçim |
| P-10 aday listesi tek süzgeç | ✓ | `InstructorReach::poolFor` (şube eğitmen düğümü üyeleri + açılanlar; merkez → null=bugünkü havuz); `EligibleInstructors`, `PlanningCandidates`, `OfficeInstructors` bağlandı; BranchInstructorTest 4 senaryo — **koştum, geçti** |
| P-11 `SalesWorkspacePolicy::open` = quote ∨ potential_customer; teklif bölümleri boş | ✓ | policy + `OfficeNavigation:217-221` + `SalesAgendaController` 3 authorize (announce uçları `training.announce`'ta kaldı); SalesWorkspaceTest 3 senaryo |
| P-12 `issuing_companies` + banka hesapları; ilk kayıt `company.*`'dan; varsayılan bayrağı | ✓ | `2026_11_01_000400…:38-96` — partial unique tek varsayılan, `identity()` ayar+config fallback'iyle okuyor (arch testinin tek muafı: migration) |
| P-13 sıralı numara, `SELECT … FOR UPDATE`, transaction içi | ✓ | `InvoiceNumbers::take` — transaction-level guard + `ON CONFLICT DO NOTHING` + satır kilidi; `InvoiceNumberRaceTest` iki bağlantı + `lock_timeout` ile beklemeyi KANITLIYOR — **koştum, geçti** (`YRS2026000000001` → `…002`) |
| P-14 kesimde kimlik kopyası | ✓ | `IssuingCompanies::snapshot` — unvan/vergi dairesi+no/adres/telefon/**banka hesapları**/**yasal ibare**/antet şablon id + `series_prefix`; `invoices_number_names_its_company` CHECK (numara varsa şirket+snapshot zorunlu); IssuingCompanyTest:117 kopyanın sonradan değişmediğini assert ediyor |
| P-15 `revenue_branch_id` (null=merkez), üç varsayılan, kesim sonrası değişmez | ✓ | `IssueInvoice::revenueBranch` — seçilmişse doğrula, yoksa teklif yazanın / kesenin `HomeBranch`'i; kesim sonrası yazan hiçbir yol yok (tek yazıcı IssueInvoice; A7 yalnız DRAFT'ları taşır — `BranchUnwindingInventory::invoices` status=DRAFT) |
| P-16 `invoice.view.{own,group,all}`, mevcutlar `.all`'a | ✓ | `2026_11_01_000500…` — satır ADI değiştiği için grant'lar kendiliğinden taşınır, şablon kalemleri de (brief sapma 9); IssuingCompanyTest:184 — **koştum, geçti** |
| P-17 teklif şirketi + şirkete bağlı antet | ✓ | `CreateQuote:71-77` (yazanın şubesinin şirketi, merkez→varsayılan), `ChangeQuoteIssuer` (`update` + `issuer.choose`, yalnız taslak/iç inceleme, audit), `QuotePrintout::currentLetterheadOf` + `SendQuote:107` aynı satırı donduruyor; `document_templates` tekillik indeksleri `COALESCE(issuing_company_id, 0)` ile şirket başına (brief sapma 11) |
| P-18 `trainings.cost_branch_id`, varsayılan planlayan | ✓ | `2026_11_01_000600…`; `PlanTraining:147-152` (`HomeBranch`), `ChooseCostBranch` (`update` yetkisi + audit); BranchFinanceTest:41,100,118 |
| P-19 mali tablo okuyanın şube kümesi + merkez kırılımı | ✓ | `FinancialOverview::for:89-171` — gelir `revenue_branch_id`, gider `cost_branch_id`, `branch_breakdown` yalnız merkeze; eğitmen borcu/bakiyeleri şube okuyucusuna gösterilmiyor (brief sapma 12, kodda gerekçeli); `CustomerLedger`, `OperationsAgenda` vadesi geçmiş kartı da daraltıldı |
| P-20 çok adımlı kaldırma/kapatma, tek transaction | ✓ | `Unwinding::missing` (cevapsız varsa `ValidationException`, envanter transaction İÇİNDE yeniden hesaplanıyor), `BranchUnwinder::apply`, `CloseBranch`/`RemoveBranchCapability` (son yetenek kaldırılamaz → kapatma akışı); BranchUnwindingTest:90 'moves nothing while a question is unanswered' — **koştum, geçti** |

### Veri modeli, yetki değişiklikleri, global kısıtlar

| Plan maddesi | Durum | Kanıt |
|---|---|---|
| Veri modeli A1/A2/A3/A5/A6 kolonları ve CHECK'ler | ✓ | Migration'lar yukarıda; tüm an kolonları `timestampTz/timestampsTz(precision: 6)`; `branches.issuing_company_id` A5'te + backfill + `SET NOT NULL` (820 F-4 / 822 onayına uygun) |
| ⚠ `branches.closed_by` FK | ⚠→✓ | Plan "FK users" yazıyordu; kod `backoffice_users`'a bağlıyor (`…000100…:43`). **Plan metninden sapma**, ama planın kendi aksiyon imzası (`CloseBranch(BackofficeUser,…)`) ve üç kimlik alanının ayrılığı (spec §3 / kırmızı çizgi komşusu) yalnız bunu tutarlı kılar — plan satırı kendi içinde çelişiyordu, kod güvenli tarafı seçti. Bulgu değil; Sets görsün diye yazıldı |
| Yetki tablosu: `issuer.choose` TEK yetki, yeteneksiz | ✓ | config:357-359 + `ChangeQuoteIssuer` ve `IssueInvoice::issuer` aynı adı kullanıyor (820 F-1'in kapanışı kodda); IssuingCompanyTest:101,207 |
| Backoffice `branch.*`, `issuing_company.*` | ✓ | config:671-688 — `view/create/update/deactivate` (plan `view/manage` demişti; brief sapma 1 — matrisin sabit eylem sözlüğü, `group.*` ailesiyle aynı desen) |
| Global: dil, AD-25, kiracı, audit, silme yok, test disiplini, tasarım (yeni ekran yok), kriter 9 testi her aşamada | ✓ | Taramalar yukarıda; yeni ofis ekranı yok — yeni alanlar mevcut form/sayfalara mevcut bileşenlerle; her aşamanın test listesinde merkez-değişmezlik senaryosu var ve koşuldu |

### Spec kabul kriterleri (1–26)

| Kriter | Durum | Kanıt (test adı — 76/76 geçti, bizzat koşuldu / tam listede) |
|---|---|---|
| 1 | ✓ | BranchTest:45 'opens a branch with a node under each chosen vertical' |
| 2 | ✓ | BranchCeilingTest:230 (müşteri/teklif/potansiyel), :199 (kullanıcı), :273 (**talep** — 820 F-5 kapanışı), BranchVisibility (eğitim), BranchInstructor (eğitmen), SalesWorkspace (takvim), BranchFinance (mali) |
| 3 | ✓ | BranchCeilingTest:67,137,160 (verilemez + ekran gerekçe yazar + verilmişse çalışmaz) |
| 4 | ✓ | BranchCeilingTest:173 |
| 5, 6 | ✓ | BranchVisibilityTest:113,127,140,303 (açıkta da kapalıda da şube seçimi) |
| 7 | ✓ | BranchInstructorTest:71 |
| 8 | ✓ | BranchFinanceTest:64,126 |
| 9 | ✓ | Yapısal (BranchCeiling muafiyetleri) + her alanda ayrı test: BranchCeilingTest:79, BranchVisibilityTest:264,271, BranchInstructorTest:132, IssuingCompanyTest:154,184, SalesWorkspaceTest:81 |
| 10 | ✓ | BranchUnwindingTest:78,90,101,131 (ad ad liste · eksik cevapla hiçbir şey taşınmaz · kimse silinmez) |
| 11 | ✓ | BranchVisibilityTest:161 |
| 12 | ✓ | SalesWorkspaceTest:60,90 |
| 13 | ✓ | BranchVisibilityTest:113,176 (onaylanana kadar yalnız şube) |
| 14 | ✓ | BranchInstructorTest:132 |
| 15 | ✓ | BranchInstructorTest:71,109,145 |
| 16 | ✓ | IssuingCompanyTest:127 (üç varsayılan; kesim sonrası tek yazıcı yok + kriter 25 testi) |
| 17 | ✓ | IssuingCompanyTest:154 (own/group/all) |
| 18 | ✓ | IssuingCompanyTest:101,117,256 + migration CHECK |
| 19 | ✓ | BranchFinanceTest:41,64 (gelir≠gider şubesi ayrı görünür) |
| 20 | ✓ | `OnboardCustomer:52-67` + BranchCeilingTest:263 |
| 21 | ✓ | BranchCeilingTest:218 |
| 22 | ✓ | BranchCeilingTest:199 |
| 23 | ✓ | BranchCeilingTest:89,106 (alt düğüm şubenindir dahil) |
| 24 | ✓ | BranchInstructorTest:95 (dosya kapalı, açılana bile) |
| 25 | ✓ | BranchUnwindingTest:131 (kesilmiş fatura şirket/numara/gelirle aynen; şirket açık; iptal kendi şirketiyle) |
| 26 | ✓ | IssuingCompanyTest:54,60,67,82 + InvoiceNumberRaceTest (eşzamanlı — kilit bekletmesi kanıtlı) |
| 27 (veri yarısı) | ✓ | Snapshot banka hesapları + ibare taşıyor; belge ekranı plan gereği tasarım bekliyor, adresi yazılı; portalda numara+şirket şimdiden görünür (IssuingCompanyTest:276) |

### Brief'teki 16 sapma/yorum

(1) backoffice eylem adları — mevcut sözlük deseni, ✓ · (2) ata-şube özyinelemesi — P-1 metnine uygun, CTE doğrulandı ✓ · (3) `all` şube okuyucusuna görünmez + `training.view.all` genişletmez — **ilk yarısı kriter 6/11'in zorunlu sonucu** (aksi "seçilmeyen şube görmez"i bozar); ikinci yarısı yorum, kodda beyanlı, **Sets'e sorulacak** (aşağıda açık soru) ✓/❓ · (4) iki liste türü — kriter 9'u koruyan doğru ayrım ✓ · (5)-(7) planlama yüzeyleri — kodda gerekçeli, merkez değişmedi ✓ · (6) plan dışı ek daraltmalar (CustomerCard vb.) — kriter 2'nin "her listede" şartının gereği, plan envanterinin genişletilmesi; TrainingListInventory arch testi bunu kalıcılaştırıyor ✓ · (8) talep kuyruğu bugünkü kural ✓ (testli) · (9) ayrı izin migration'ı + şablonlar ✓ · (10) boş kimlikle doğan varsayılan şirket + kesim reddi ✓ (rollout testi) · (11) `COALESCE` indeksleri + çerçeve sözleşme varsayılan şirket ✓ · (12) eğitmen borcu şubeye gösterilmez — pay hesaplanmadan pay göstermemek finansal doğruluğun kendisi ✓ · (13) `branch` satırı silinmek yerine `all`'a çevrilir — "kapsam silinmez" kısıtının tek tutarlı çözümü ✓; taslak fatura üreten yol bugün yok, envanter savunmacı ✓ · (14) `HomeBranch` tek kaynak ✓ · (15) ekran görüntüleri — **brief'ten sonra kapanmış**: commit `44362bba` iki PNG'yi yeniledi; `planlama-egitim-yeni.png` açıldı, «Kimler görsün» bloğu görünüyor ✓ · (16) audit fallback EGT-220 kapsamı, yenilerde `officeRecord: true` ✓

---

## ✅ What works

- **Tavan mimarisi plandaki gibi tek sınıftan iki boğaza:** izin kontrolü ve kapsam çözümleyicisi aynı `BranchCeiling`'e soruyor; merkez/üyeliksiz muafiyetleri yapısal — kriter 9 testle değil tasarımla korunuyor, testler de üstüne var.
- **Numara disiplini kanıtlı:** boşluksuzluk transaction'a, tekrarsızlık unique'e, eşzamanlılık satır kilidine bağlanmış; yarış testi gerçekten iki bağlantıyla bekletmeyi gösteriyor ve bunu kendim koştum.
- **İki arch testi geleceği kilitliyor:** sınıflandırılmamış yetki ve kapsam okumayan eğitim listesi artık derlenemez (bayat-kayıt kontrolleri dahil — muafiyet listesi çürüyemez).
- **820/821'in altı plan bulgusunun kodda karşılığı birebir:** tek `issuer.choose`, tek kimlik kaynağı (dört okuyucu bağlandı, `letterhead_min_width_px` yerinde ve düzenlenebilir — `BusinessSettings.php:473-480`), A7 teklif+potansiyel soruları, FK A5'te backfill'le, talep testi.
- Dürüst kayıt: silinen değil dönüştürülen test kendi gerekçesini taşıyor; yardım metinleri beş makalede güncellendi ve iki ekran görüntüsü yeniden alındı.

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

Yok.

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

1. **Sapma (3)'ün ikinci yarısı — brief'in kendisi de "Sets'e ayrıca sorulacak" diyor:** `training.view.all` taşıyan bir şube üyesi kapsam seçiminin dışına genişlemiyor (`TrainingsInScope::narrowForBranch` null-reach'i şube ağacına indiriyor). Kriter 6/11 ile tutarlı, ama S-12'nin "all = tümü" genel semantiğiyle gerilimi var; Sets'in cevabı teslimden önce kayda girmeli.
2. **Teslim kapıları (bulgu değil, sıra):** TeamCity full run yeşil → deploy → `HelpArticleSeeder` koşulması → In Review + Tayfun. Brief gereği bunlar review'dan sonra; bu review lokalde hedefli 76 test + pint + phpstan'ı bizzat doğruladı, tam koşunun otoritesi CI'dır.
3. **İki belgelenmiş mikro-sapma Sets'in gözüne girsin** (ikisi de gerekçeli, bulgu değil): (a) `branches.closed_by` → `backoffice_users` (plan metni `users` diyordu; planın kendi aksiyon imzasıyla tutarlı olan bu); (b) teklifli faturada şirket varsayılanı = **teklifin şirketi** ?? gelir şubesinin şirketi (spec §9.2'nin ilk maddesi gelir şubesini söyler; teklifin şirketi yetkiyle değiştirilmişse ancak bu öncelik "müşteri teklifle faturayı aynı şirketten görür" cümlesini tutar; sapma hâli `issuer_differs_from_quote` ile audit'leniyor).

## ⚠️ Upstream Findings

Yok.

## Verdict: ✅ APPROVED

Matrix'te ❌/⚠ satır yok (tek ⚠, gerekçesiyle ✓'ya bağlandı ve açık soru 3a'da Sets'e sunuldu). Plan P-1…P-20'nin ve 26 kabul kriterinin tamamının kodda, dosya+satır kanıtıyla karşılığı var; 17 kırmızı çizgi taraması temiz; wire taraması uçtan uca bağlı; 76 hedefli test + pint + phpstan bu review'da bizzat koşuldu ve geçti. Teslim, yeşil TeamCity + deploy + seeder şartına ve açık soru 1'in (training.view.all yorumu) Sets cevabına bağlıdır — ikisi de bu kodun değil sürecin kalan adımlarıdır.
