# Şube yapısı — implementasyon planı

*v1.2 · 20.10.2026 · EGT-219 · Upstream: `docs/specs/sube-spec.md` v0.6 (heyet 815 → 816 → 817 ✓ → 818 → 819 ✓) · Kararlar: `proje-haritasi.md` §4.4 · Sets: "tek plan, aşama aşama" (20.10.2026)*

**Hedef:** spec'in 27 kabul kriteri. Tek merkez (3E Group) ve onun altında yetenek setiyle kurulan şubeler; şube üyesi yalnız kendi şubesinin işini görür ve yapar; fatura bir 3E şirketi adına, o şirketin sıralı numarasıyla kesilir; gelir ve gider şubeye yazılır.

**Mimari:** AD-23'ün dikey-önce ağacı ve tek kapsam çözümleyicisi **korunur**. Şube, ağaca `groups.branch_id` ile bağlanan ayrı bir kayıttır; yetenek tavanı izin kontrolüne ve kapsam çözümleyicisine aynı tek sınıftan uygulanır. Yeni tablolar 3E'nin kendi kayıtlarıdır, kiracıya bağlı değildir (EGT-216 K-3 deseni) ve audit'leri açık beyanla kiracısız yazılır (EGT-216 K-25).

**Aşamalar** — her biri ayrı commit ve ayrı teslim edilebilir; sıra bağımlılığı yukarıdan aşağıya:

| # | Aşama | Spec kriterleri |
|---|---|---|
| A1 | Şube çekirdeği, yetenek tavanı, firmayı açanın ataması | 1, 2 (müşteri/kullanıcı/teklif/potansiyel/talep), 3, 4, 9, 20, 21, 22, 23 |
| A2 | Eğitim görünürlüğü ve genişletme onayı | 2 (eğitim), 5, 6, 11, 13 |
| A3 | Eğitmenlerin şubeye açılması ve aday listesi | 7, 14, 15, 24 |
| A4 | Takvimim ve Satış kokpiti kapısı | 12 |
| A5 | 3E şirketleri, sıralı fatura numarası, gelir şubesi, fatura kapsamı, teklif anteti | 16, 17, 18, 26 (+27'nin veri yarısı) |
| A6 | Gider şubesi, şube mali tablosu, merkez kırılımı | 8, 19 |
| A7 | Yetenek kaldırma ve şube kapatma akışı | 10, 25 |
| — | Fatura belgesi | 27 — ⚠ tasarım bekliyor (`docs/design/fatura-belgesi-brief.md`); veri A5'te hazırlanır, ekran tasarım gelince ayrı tur |

---

## Doğrulanmış mevcut durum

⚠ 20.10.2026'da koddan okundu (araştırma notları doğrulanarak). Satır numaları o günün ağacıdır.

| Ne | Durum | Kanıt |
|---|---|---|
| İzin kontrolü | `Gate::before` → `PermissionFamilies::allows()`; argümanlı kontrol politikaya düşer | `AuthServiceProvider.php:205-214`, `PermissionFamilies.php:87-107` |
| İzin kontrolü düğümü bilmez | yalnız kişi + yetki adı; kapsamlı sorular `ScopeResolver::widestHeld` üzerinden aynı kapıdan geçer | `PermissionFamilies.php:211-222`, `ScopeResolver.php:108-127` |
| Kapsam | `groupsFor` → `reachableGroups` = üyelik düğümleri + alt ağaç (`Group::subtreeIds`); seviye `group`'u daraltır | `ScopeResolver.php:44-70, 133-166`, `Group.php:88-104` |
| Yetki verme tavanı | `Ceiling::allowsGranting/widestGrantable` — yalnız **veren**e bakar; ekran hücreyi `grantable=false` ile kapatır | `Ceiling.php:42-106`, `OfficePermissionMatrix.php:187-197`, `organisms/PermissionMatrix.vue:163` |
| `AssignOfficePermissions` | `Ceiling`'i çağırmaz, aynı kontrolleri Gate ile yapar | `AssignOfficePermissions.php:94-98, 203-267` |
| Grup ağacı | `groups(parent_id, name, vertical, is_active)`, `vertical` ∈ operations/sales/planning/instructors (enum yok); backoffice `GroupResource` (oluştur/düzenle, silme yok) | `2026_08_13_000100_create_group_tree.php`, `GroupResource.php:64-174` |
| Şube kavramı | **yok** (yalnız yorumlarda "branch" = düğüm) | grep: app/, database/, config/, lang/ |
| Eğitim kapsamı | `training_scopes(training_id, scope_type all/group/user, group_id, user_id)`, yalnız `PlanTrainingRequest` yazar; kapsamsız = herkes | `2026_09_30_000100_…scope….php:49-84`, `PlanTrainingRequest.php:224-242`, `TrainingsInScope.php:50-102` |
| Kapsamı okuyan listeler | yalnız `VisibleTrainings`, `QuotableTrainings`, `SalesAgenda::sellableTrainings`; okumayanlar: `PlanningCalendar`, `PlanningOutlook`, `PlanningAgenda`, `OperationsAgenda`, `TrainingPolicy::view` | araştırma §3, doğrulandı |
| Doğrudan planlama | `PlanTrainings`/`PlanTraining` kapsam yazmaz; form (`Trainings/Plan.vue`) yalnız açık/kapalı seçer | `PlanTraining.php:66-142`, `Plan.vue:124,968-1095` |
| Onaylar | genel onay tablosu yok; her tür kendi satırında bekler, `PendingApprovals` birleştirir; «kendi işini onaylayamaz» action'da | `ApprovalKind.php`, `PendingApprovals.php:83-92`, `ApproveTrainingCost.php:61-71` |
| Eğitmen adayı | `EligibleInstructors`, `PlanningCandidates` planlamacıya göre süzmez; atama `training.assign_instructor` + `view` | `EligibleInstructors.php:45-111`, `TrainingPolicy.php:147-149` |
| Eğitmenin düğümü | eğitmen → `users` → `group_memberships` | `Instructor.php:76`, `PendingApprovals.php:730-752` |
| Fatura | `invoices(customer_id, quote_id, reference_code_id, status draft/issued/cancelled, replaces_invoice_id, issued_at, issued_by, totals…)`; şirket/şube/numara kolonu yok | `2026_09_01_000200_create_invoices.php:32-109` |
| Fatura «numarası» | `FT-2026-7K4M` **rastgele** referans kodu (R-2); sıralı numara yok | `ReferenceCode.php:69-78`, `create_reference_codes.php:12-15` |
| Fatura yetkisi | kapsamsız (`invoice.view/create/cancel/…`) | `InvoicePolicy.php:39-91` |
| Şirket kimliği | `Settings::companyLetterhead()` → ad/alt başlık/adres/telefon; **vergi no yok**; okuyanlar teklif çıktısı, çerçeve sözleşme, yoklama | `Settings.php:385-400`, `QuotePrintout.php:116`, `FrameworkContractPrintout.php:83`, `TrainingController.php:1001` |
| Antet görseli | `document_templates(kind QUOTE_LETTERHEAD/REGISTER_LETTERHEAD, is_current…)`, şirkete bağlı değil; teklif sürümü kendi antetini dondurur | `DocumentTemplate.php:35-65`, `SendQuote.php:108-114` |
| Fatura belgesi | **yok** | `pages/Office/Invoices/` = Index, Show |
| Mali tablo | `FinancialOverview::for` — gelir (fatura `gross_total`), gider (`InstructorEarning`, onaylı `InstructorExpense`, `TrainingCostItem`), marj; şube/şirket süzgeci yok; kapı `financial.view` | `FinancialOverview.php:78-313`, `AccountingController.php:49-93` |
| Takvim/kokpit kapısı | `Quote::viewAny` (menü + controller); bölümler kişinin kendi kaydıyla | `OfficeNavigation.php:216-217`, `SalesAgendaController.php:46,57,82` |
| Firmayı açan | `office_user_customers`'a yazılmıyor | `OnboardCustomer.php:45-63`, `CustomerController.php:127-157` |
| Production verisi | fatura **0**; grup ağacında şube düğümü yok | production, 20.10.2026 |

---

## Global kısıtlar

- **Dil:** kod İngilizce, arayüz Türkçe (`lang/tr`, `lang/en` eşi).
- **Kod yerleşimi (AD-25):** karar `app/Domain/Branches/`, eylem `app/Actions/{Office,Backoffice,Sales}/`, sorgu `app/Queries/…`, yetki `app/Policies/`, Filament şemaları `…/Schemas/`.
- **Kiracı:** yeni tablolar 3E'nin kaydıdır → `tenancy.exempt_tables` + `TenancyTest` sabit listesi; modeller `OfficeRecord` (EGT-216 K-25); fatura kiracı kuralı bugünkü gibi.
- **Audit:** her yazma, işlemle aynı transaction'da; şube/şirket/yetenek/onay olayları `lang/*/audit.php`'de cümleli (`EnumLabelTest`).
- **Silme yok:** şube, şirket, fatura, eğitim kapsamı silinmez; kapatılır/iptal edilir.
- **Test:** grup etiketi (`backoffice`, `planlama`, `satis`, `omurga`, `omurga-rls`); `FrozenClock`; factory. Dokunulan alanın testi `--filter`; tam koşu ticket kapanışında.
- **Tasarım:** ofiste yeni ekran yok (A3'ün eğitmen dosyası alanı mevcut parçalarla — Sets); backoffice Filament'tir (tasarım dışı); fatura belgesi tasarım bekler.
- **Geri dönük uyum:** şubesi olmayan (yalnız merkez üyeliği olan) her kullanıcının davranışı değişmez — kriter 9; her aşamada «merkez kullanıcısı aynı sonucu alır» testi.

---

## Kararlar (teknik — spec'in nasıl uygulanacağı)

| # | Karar | Gerekçe |
|---|---|---|
| P-1 | Şube = `branches` tablosu; düğüm bağı `groups.branch_id` (nullable) + `unique(branch_id, vertical)` | Spec §2.1. Bağ düğümde durur, çünkü kapsam düğüm üzerinden çalışır; şube ağacın kendisi değil, düğümlerinin sahibidir. |
| P-2 | Yetenek → yetki eşlemesi `config/authorization.php` `office_capabilities` (sales/planning/instructors → temel adlar) + arch testi: her ofis yetkisi ya bir yeteneğe ya «yeteneğe bağlı değil» listesine aittir | Spec §11.1. Eşleme yetki sözlüğünün parçasıdır (`PermissionMatrix` gibi), iş kuralı parametresi değil; yeni bir yetkinin sınıflandırılmadan eklenmesini arch testi engeller. |
| P-3 | Tavan **tek sınıf**: `App\Support\Authorization\BranchCeiling` — `permits(User, ability): bool` ve `reachableGroups(User, base): list<int>` | Spec §3, F-5. İki yerde uygulanır: (a) `PermissionFamilies::ask` içinde, kişinin **tavana uyan bir üyeliği yoksa** yetki reddedilir; (b) `ScopeResolver::reachableGroups`, yetkinin yeteneği kapalı şube düğümlerini alt ağaçtan önce eler. Merkez üyeliği (branch_id null düğümler) tavana takılmaz; hiç üyeliği olmayan kişi de (bugünkü davranış). |
| P-4 | Yetki ekranı tavanı **alıcıya** göre de çizer: `OfficePermissionMatrix::get($granter, ?$subject)`; hücre `grantable=false` + `blocked_reason='capability'` | Spec §3 madde 2. Bugün tavan yalnız verene bakıyor; alıcının şubesi ikinci bir koşul. `AssignOfficePermissions` aynı sınıfı çağırır (ekran ile eylem ayrışmasın). |
| P-5 | Eğitim kapsamına yeni tür: `scope_type='branch'` + `branch_id`; eşleşme: okuyanın tavanlı erişilen düğümlerinden biri o şubenin | Spec §7. Şubeye açmak her düğüm için ayrı `group` satırı yazmak demek olurdu; yeni yetenek eklenince eski eğitimler o düğüme görünmezdi. |
| P-6 | «Kapsamsız = herkes» kuralı «kapsamsız = merkez» olur: kapsam satırı olmayan eğitim, **yalnız şube üyeliği olan** okuyana görünmez | Spec §6 S-5. Merkez okuyanın sonucu aynı kalır (kriter 9); veri yeniden yazılmaz. |
| P-7 | Kapsamı **her** eğitim listesine ve `TrainingPolicy::view`'a uygulamak; liste envanteri arch testinde sabit | Doğrulanmış durum: beş liste ve detay kapsamı okumuyor. Okunmayan tek liste şube dışı eğitimi gösterir — EGT-216'nın `TrainingsInScope` gerekçesi. |
| P-8 | Genişletme izni yeni yetki `training.widen_visibility.{group,all}`; yetkisi olmayanın isteği `training_visibility_requests` satırına düşer, `ApprovalKind::VisibilityWidening`; onaylayan aynı yetkiyi kapsamı isteyeni kapsayarak taşıyan kişi | Spec §7.1: "Merkezin Planlama Direktörü onaylar". AD-23 *"direktör için özel `if` yoktur"*: onaylayan bir rol adı değil, yetki + kapsamdır; Planlama Direktörü bu yetkiyi `all` ile taşır. ⛔ Sets teyit etti (20.10.2026): onay yetkiden ve kapsamdan gelir; başkasına da verilebilir (ör. bir şube yöneticisine kendi şubesi için). |
| P-9 | Eğitmen açma `instructor_branch_access(instructor_id, branch_id)`; ofis eğitmen dosyasında `instructor.update` ile | Spec §8, S-9. |
| P-10 | Aday listesi süzgeci tek sorguda: şube planlamacısı → kendi şubesinin eğitmen düğümü üyeleri + o şubeye açılanlar; merkez planlamacısı → bugünkü havuz (şube eğitmenleri dahil, kapsamı yetiyorsa) | Spec §8, kriter 14, 24. |
| P-11 | Takvim/kokpit kapısı yeni yetenek kontrolü `SalesWorkspacePolicy::open` = `quote.view` **veya** `potential_customer.view`; teklif bölümleri `quote.view` yoksa boş | Spec §6 S-6. |
| P-12 | 3E şirketleri `issuing_companies` + `issuing_company_bank_accounts`; ilk kayıt bugünkü `company.*` ayarlarından migration'la; **varsayılan şirket** bayrağı (merkezin şirketi) | Spec §9.1. Bugünkü teklif çıktısı ve yoklama aynı kimliği okumaya devam eder (varsayılan şirket). |
| P-13 | Sıralı numara `invoice_number_sequences(issuing_company_id, year, last_number)`; `IssueInvoice` transaction'ında `SELECT … FOR UPDATE` | Spec §9.1, kriter 26: boşluksuz, tekrarsız, eşzamanlı güvenli. Referans kodu (`FT-…`) ayrıca kalır. |
| P-14 | Faturaya kesimde **kimlik kopyası**: `invoices.issuer_snapshot` jsonb (unvan, vergi dairesi/no, adres, telefon, banka hesapları, yasal ibare, antet şablon id'si) + `issuing_company_id` + `invoice_number` | Spec §9.2 (heyet 815 F-2): şirket sonradan değişse de belge kesildiği gibi basılır. |
| P-15 | `invoices.revenue_branch_id` (null = merkez); varsayılanlar spec §9.3; kesimden sonra değişmez (CHECK değil, action kuralı + test) | Spec §9.3. |
| P-16 | Fatura yetkisi kapsamlı: `invoice.view.{own,group,all}` (diğer `invoice.*` kapsamsız kalır); mevcut sahipler `.all`'a taşınır (`2026_08_13_000200_scope_permission_names` deseni) | Spec S-12, kriter 17; kimse ertesi sabah daha az görmez. |
| P-17 | Teklif şirketi `quotes.issuing_company_id` (varsayılan teklifi yazanın şubesinin şirketi); antet şirkete bağlı: `document_templates.issuing_company_id` (null = varsayılan şirket) | Spec §9.2. Teklif sürümü anteti bugün de dondurur (`SendQuote`); şirket o anda belirlenir. |
| P-18 | Gider şubesi `trainings.cost_branch_id` (null = merkez), varsayılan planlayanın şubesi | Spec §9.4. |
| P-19 | Şube mali tablosu: `FinancialOverview` okuyanın **şube kümesini** alır — yalnız şube üyeliği olan okuyan için kendi şube(ler)i, merkez okuyan için hepsi + yeni «şube kırılımı» bölümü | Spec §9, kriter 8. Yeni yetki yok: `financial.view` aynen; daralma okuyanın üyeliğinden. |
| P-20 | Yetenek kaldırma / kapatma = Filament çok adımlı eylem: (1) etkilenenleri ad ad listeler, (2) her kişi için hedef merkez düğümü ve her açık iş için yeni sahip/şube sorar, (3) hepsi cevaplanınca tek transaction'da taşır ve kapatır | Spec §4, §9.5, kriter 10, 25. |

---

## Veri modeli

### `branches` (yeni, A1)

⚠ Şirket bağı (`branches.issuing_company_id`) burada **yoktur**; A5'te eklenir (heyet 820 F-4).
`id`, `name` (unique), `has_sales` bool, `has_planning` bool, `has_instructors` bool (CHECK en az biri), `is_active` bool default true, `closed_at`, `closed_by` FK users nullable, `close_reason` text nullable (CHECK kapalıysa dolu), timestamps.

### `groups.branch_id` (ek, A1)
nullable FK `branches`, `restrictOnDelete`; `unique(branch_id, vertical)`; indeks.

### `training_scopes` (değişiklik, A2)
`branch_id` nullable FK; CHECK yeniden yazılır: `branch` türü yalnız `branch_id` taşır. ⚠ Kolon eklemek + CHECK'i düşürüp yeniden kurmak — veri kaybı yok, mevcut satırlar yeni CHECK'i sağlar (test eder).

### `training_visibility_requests` (yeni, A2)
`id`, `training_id` FK, `requested_scopes` jsonb, `requested_by` FK users, `status` (`pending/approved/refused/withdrawn`, CHECK), `decided_by`, `decided_at`, `reason` (CHECK reddedildiyse dolu), timestamps; indeks `(status, training_id)`.

### `instructor_branch_access` (yeni, A3)
`instructor_id` FK, `branch_id` FK, `granted_by` FK users, timestamps; PK `(instructor_id, branch_id)`.

### `issuing_companies` (yeni, A5)
`id`, `name` (unvan), `tax_office`, `tax_number`, `address`, `phone`, `series_prefix` char(3) unique (CHECK `^[A-Z0-9]{3}$`), `legal_notice` text nullable, `is_default` bool (partial unique: tek varsayılan), `is_active`, timestamps.

### `branches.issuing_company_id` (ek, A5)
FK `issuing_companies`. Aynı migration'da A1–A5 arasında kurulmuş şubelere **varsayılan şirket** yazılır (backfill), ardından kolon `NOT NULL` olur. Şube formunda seçim zorunlu.

### `issuing_company_bank_accounts` (yeni, A5)
`id`, `issuing_company_id` FK, `bank_name`, `branch_name` nullable, `iban` (CHECK `^TR[0-9]{24}$`), `position`, timestamps.

### `invoice_number_sequences` (yeni, A5)
`issuing_company_id` FK, `year` smallint, `last_number` bigint; PK `(issuing_company_id, year)`.

### `invoices` (ek, A5)
`issuing_company_id` FK nullable, `invoice_number` varchar(16) unique nullable, `issuer_snapshot` jsonb nullable, `revenue_branch_id` FK nullable. CHECK: `status='issued'` ve `issued_at >= <özellik tarihi>` ise numara dolu — ⚠ tarih sabiti yerine: numarası boş kesilmiş fatura yalnız migration öncesi var olabilir; kural action'da + test (spec v0.6 eski faturalar).

### `quotes` (ek, A5) · `document_templates` (ek, A5) · `trainings` (ek, A6)
`quotes.issuing_company_id` nullable; `document_templates.issuing_company_id` nullable; `trainings.cost_branch_id` nullable.

⚠ **Data-loss riski:** hiçbir kolon düşmüyor, yeniden adlanmıyor, tipi değişmiyor. Tek yapısal değişiklik `training_scopes` CHECK'inin yeniden kurulması (A2) ve izin adlarının `.all`'a taşınması (A5, P-16) — ikisi de veriyi koruyan dönüşüm, testli.

---

## Yetki değişiklikleri

| Yetki | Aşama | Yetenek (P-2) |
|---|---|---|
| `training.widen_visibility.{group,all}` | A2 | planlama |
| `invoice.view.{own,group,all}` (bugünkü `invoice.view` yerine) | A5 | bağlı değil |
| `issuer.choose` — teklif **ve** faturada şirketi değiştirmek, tek yetki (spec §9.2 "aynı yetkiyle", §11.1 "ayrı bir yetkidir ve yeteneğe bağlı değildir") | A5 | bağlı değil |

Backoffice: `branch.view`, `branch.manage`, `issuing_company.view`, `issuing_company.manage` (backoffice ailesi; süper admin taşır).

---

## A1 — Şube çekirdeği ve tavan

**Dosyalar:** migration `2026_11_01_000100_a_branch_owns_nodes_of_the_tree.php` · `app/Models/Branch.php` (`OfficeRecord`) · `app/Domain/Branches/Capability.php` (enum sales/planning/instructors + `forPermission(string)`) · `config/authorization.php` `office_capabilities` · `app/Support/Authorization/BranchCeiling.php` · değişen `PermissionFamilies::ask`, `ScopeResolver::reachableGroups`, `Ceiling` + `OfficePermissionMatrix` (alıcı), `AssignOfficePermissions` · backoffice `app/Filament/Backoffice/Resources/Branches/BranchResource.php` (+ Schemas) · `app/Actions/Backoffice/OpenBranch.php`, `AddBranchCapability.php` · `OnboardCustomer` (açanı ata) · `lang/*/branches.php`, `audit.php`, `permission.php`.

1. Migration + model + factory.
2. `OpenBranch(BackofficeUser, name, capabilities)`: şube + her yetenek için ilgili dikey kökün altına düğüm (`name` = şube adı, `vertical`, `branch_id`); audit `branch.opened`. `AddBranchCapability`: düğüm açar.
3. `BranchCeiling`: kişinin üyelik düğümlerini `branch_id` ile okur (istek başına bir sorgu, memoize); `permits(user, ability)` = yeteneği yoksa true; yoksa üyeliklerinden en az biri merkez düğümü ya da yeteneği açık şube düğümü.
4. `PermissionFamilies::ask` sonucu `&& BranchCeiling::permits`; `ScopeResolver::reachableGroups($user, $base)` tavanı eleyerek.
5. Yetki ekranı: `OfficePermissionMatrix::get($granter, $subject)`; `PermissionMatrix.vue` engelleme etiketi `blocked_reason='capability'` için yeni metin («Bu şubenin X yeteneği yok»). Filament `OfficeUserForm`: aynı tavan, kilitli hücre.
6. `OnboardCustomer`: açanı `office_user_customers`'a ekler (audit `user.customers_assigned`, `reason: created`), `customer.view.all` olsa da (zararsız, tekrarsız).
7. Arch testi: her ofis yetkisi sınıflandırılmış (P-2).

**Testler (`omurga` + `backoffice`):** şube açma düğümleri doğru dikeye kurar; yalnız İzmit satış üyesi planlama yetkisini tutsa bile `can('training.create')` false, merkez üyesi true (kriter 3, 9); iki şube üyeliği düğüm başına (kriter 23); `groupsFor` kapalı yetenek düğümünü döndürmez; yetki ekranı hücreyi gerekçeyle kapatır, eylem de reddeder; şube yöneticisi yalnız kendi şubesine kullanıcı açar, yetkisizse açamaz (kriter 4); şube üyesi kullanıcı listesinde şube dışını görmez (kriter 22); aynı yetkili merkez ve şube kullanıcısının menü kalemleri eşit (kriter 21); firmayı açan atanır (kriter 20); müşteri/teklif/potansiyel listeleri şubeye daralır; **talep kuyruğu** (`TrainingRequestQueue`) şube üyesine yalnız kapsamındaki ve atandığı firmaların taleplerini gösterir, merkez kullanıcısı için değişmez (kriter 2 — §6 tablosunun Talep satırı, heyet 820 F-5).

## A2 — Eğitim görünürlüğü

**Dosyalar:** migration (`training_scopes.branch_id`, CHECK; `training_visibility_requests`) · `TrainingScope::BRANCH` · `TrainingsInScope` (P-5, P-6) · `PlanningCalendar`, `PlanningOutlook`, `PlanningAgenda`, `OperationsAgenda`, `TrainingPolicy::view` kapsama bağlanır (P-7) · `NewTraining`/`PlanTrainingsRequest` kapsam alanı · `PlanTraining` kapsamı yazar · `Plan.vue` «Kimler görsün» seçimi (talep formundaki mevcut kapsam bileşeni) · `app/Actions/Office/RequestVisibilityWidening.php`, `DecideVisibilityWidening.php` · `ApprovalKind::VisibilityWidening`, `PendingApprovals` dalı, `ApprovalController::decide` dalı.

1. Kapsam seçimi: şube planlamacısında form **yalnız kendi şubesi** seçili açılır; merkez planlamacısında bugünkü varsayılan (kapsamsız = merkez). Açık ve kapalı eğitimin ikisinde de şube seçilir (kriter 6).
2. Şube dışına genişletme: `training.widen_visibility` varsa doğrudan; yoksa eğitim kendi şubesiyle kaydedilir ve istek onaya düşer (kriter 13). Onay, isteği kapsayan aynı yetki sahibince; kendi isteğini onaylayamaz.
3. Liste envanteri arch testi: `Training::query()` geçen her `app/Queries/Office` sınıfı ya `TrainingsInScope` uygular ya da gerekçeli muafiyet listesindedir.

**Testler (`planlama`):** şube planlamacısının eğitimi yalnız o şubeye (ve merkezde kapsamı yetene) görünür; merkezin şube seçmediği açık eğitim şubeye görünmez, seçtiğine görünür (kriter 6); kapsamsız eski eğitim merkeze görünür, şubeye görünmez (kriter 11); genişletme onayı + red + kendi isteği; beş listenin ve detayın her biri şube kapsamına uyar; merkez kullanıcısının liste sonuçları değişmez (kriter 9).

## A3 — Eğitmenler

**Dosyalar:** migration `instructor_branch_access` · `app/Actions/Office/OpenInstructorToBranches.php` · `InstructorController` + `Instructors/Show.vue` «Açık olduğu şubeler» alanı (mevcut çoklu seçim bileşeni) · `EligibleInstructors`, `PlanningCandidates` süzgeci (P-10) · `OfficeInstructors` şube üyesi için kendi eğitmen düğümüne daralır.

**Testler (`egitmen` + `planlama`):** eğitmen yeteneği olmayan şubenin planlamacısı yalnız açılanları aday görür, dosyayı açamaz (kriter 7, 24); eğitmen yeteneği olan şube kendi eğitmenlerini görür; merkez planlamacısı şube eğitmenini atar (kriter 14); açma işlemi audit'lenir.

## A4 — Takvimim ve kokpit

`SalesWorkspacePolicy` (P-11); `OfficeNavigation` iki kalem; `SalesAgendaController` üç `authorize`; `SalesAgenda`/`SalesCalendar` teklif bölümleri `quote.view` yoksa boş.

**Testler (`satis`):** yalnız potansiyel müşteri yetkisi olan satışçı iki ekranı açar, yalnız kendi görüşme/görevlerini görür, teklif bölümü boştur (kriter 12); teklif yetkilisinin ekranı değişmez.

## A5 — 3E şirketleri, numara, gelir şubesi, teklif anteti

**Dosyalar:** migration'lar (veri modeli A5) · `IssuingCompany`, `IssuingCompanyBankAccount` modelleri · `FrameworkContractPrintout`, `TrainingController::register` anteti, `Settings::companyLetterhead`, `BusinessSettings` (company alanları kaldırılır) · backoffice `IssuingCompanyResource` · `branches.issuing_company_id` zorunlu (şube formunda seçim) · `app/Domain/Invoicing/InvoiceNumber.php` (biçim: önek + yıl + 9 hane, saf) · `app/Actions/Sales/IssueInvoice` (şirket seçimi, numara, snapshot, gelir şubesi) · `InvoicePolicy` kapsamlı (P-16) + izin taşıma migration'ı · `InvoiceController`/`Invoices/Show.vue` şirket ve gelir şubesi alanları · `QuotePrintout` + `SendQuote` şirkete göre antet · `CreateQuote` varsayılan şirket.

1. **Tek kaynak** (heyet 820 F-2): varsayılan şirket migration'la `company.*` ayarlarından ilk kayıt olur ve şirket kimliğinin **tek kaynağıdır**. Okuyan dört yer ona bağlanır: `QuotePrintout`/`SendQuote` (teklifin şirketi), `FrameworkContractPrintout` (varsayılan şirket — çerçeve sözleşme 3E Group'un sözleşmesidir), yoklama anteti `TrainingController:1001` (varsayılan şirket — bugünkü davranış; yoklamanın şube şirketiyle basılması spec'te yoktur ve istenirse ayrı karardır), ve `Settings::companyLetterhead()` varsayılan şirketi okuyan bir ince sarmalayıcıya döner. Backoffice `BusinessSettings` sayfasındaki **kimlik** alanları (`company.name`, `company.subtitle`, `company.address`, `company.phone`) **kaldırılır**, yerine «3E şirketleri» kaynağına bağlantı; ayar satırları silinmez, okunmaz olur (geri dönüş için). ⚠ `company.letterhead_min_width_px` **kalır**: şirket kimliği değil, antet görselinin asgari genişlik kuralıdır (kendi docblock'u, kırmızı çizgi 9) ve yerinde düzenlenmeye devam eder (heyet 821 F-1). Arch testi: bu dört kimlik anahtarını `IssuingCompany` migration'ı dışında okuyan kod yok.
   Ayrıca: `series_prefix` ve `tax_number` **boş kalır** → `IssueInvoice` eksik kimlikli şirketle kesimi reddeder («şirketin vergi numarası ve seri öneki tanımlanmadan fatura kesilemez»). ⚠ Rollout adımı.
2. Numara: `invoice_number_sequences` satırı kilitlenir, `last_number+1`, `InvoiceNumber::format`. İptal numarayı korur; yerine kesilen yeni numara alır; eski numarasız faturalar dokunulmaz (spec v0.6).
3. Gelir şubesi varsayılanları (spec §9.3 tablosu); kesimden sonra değişmez.

**Testler (`satis` + `omurga-rls`):** numara biçimi; iki şirket ayrı sıra; eşzamanlı iki kesim farklı numara (iki bağlantı); iptal numarayı korur, yeniden kesim yeni numara; eski numarasız fatura etkilenmez (kriter 26); şirket değiştirme (teklifte ve faturada, tek yetki `issuer.choose`) yetkisiz reddedilir, yetkiliyle olur ve audit'lenir; snapshot kesimden sonra şirket değişse de aynı kalır (kriter 18); gelir şubesinin üç varsayılanı ve kesim sonrası değişmezliği (kriter 16); `invoice.view.own/group/all` (kriter 17); mevcut `invoice.view` sahibi `.all` ile aynı listeyi görür (kriter 9); teklif çıktısı şirketin antetiyle; çerçeve sözleşme ve yoklama anteti şirket kaydından (bayat `company.*` değeri basılmaz — eski ayar değiştirilip belgede görünmediği test edilir).

## A6 — Gider şubesi ve mali tablo

`trainings.cost_branch_id` + planlama formunda alan (planlama yetkisiyle değişir, audit); `FinancialOverview` okuyanın şube kümesiyle (P-19) — gelir `revenue_branch_id`, gider `cost_branch_id`; merkez için «Şube kırılımı» bölümü (Accounting sayfası, mevcut tablo bileşeni).

**Testler (`satis`):** şube üyesi yalnız kendi geliri/gideri/marjı; merkez kırılımı satırları toplamla tutar; gelir ve gideri farklı şubelerde olan eğitim iki şubede ayrı görünür (kriter 19); `financial.view` olmayan şube üyesi açamaz (kriter 8).

## A7 — Yetenek kaldırma ve şube kapatma

`app/Domain/Branches/Unwinding.php` (etkilenenleri listeler — spec §4'ün saydığı her tür: düğüm üyeleri; bu düğümlerin üyelerine ait **açık teklifler** ve **açık potansiyel müşteriler**; gelir şubesi bu şube olan kesilmemiş faturalar; gider şubesi bu şube olan açık eğitimler; kapsamı yalnız bu şube olan açık eğitimler) · `app/Actions/Backoffice/RemoveBranchCapability.php`, `CloseBranch.php` (cevaplar tamamsa tek transaction: kişileri hedef merkez düğümlerine yerleştir; açık teklif ve potansiyel müşteri için seçilen yeni sahibe devret — kişi merkeze taşınıyorsa «sahibinde kalsın» da bir cevaptır; fatura ve eğitim alanlarını yeniden ata; düğümü/şubeyi pasifleştir) · Filament çok adımlı eylem.

**Testler (`backoffice`):** doluyken uyarı + ad ad liste (kişi, açık teklif, açık potansiyel müşteri, kesilmemiş fatura, açık eğitim — her tür ayrı senaryo), eksik cevapla hiçbir şey taşınmaz; tam cevapla taşınır ve kapanır (kriter 10); kişi ve kayıt silinmez; kapanan şubenin kesilmiş faturaları şirket/numara/gelir şubesiyle aynı, şirket kaydı açık, geçmiş rapor okunur, iptal faturanın şirketiyle (kriter 25).

---

## Çapraz panel etkisi

| Panel | Etki |
|---|---|
| Ofis — satış, planlama, eğitmen, muhasebe | içerik şubeye daralır (A1–A6); merkez kullanıcısı için değişmez |
| Ofis — yetki ekranı (`64`) | alıcıya göre kapalı hücre (A1) |
| Backoffice | şube ve 3E şirketleri kaynakları; şube formunda şirket (A1, A5) |
| Müşteri portalı | faturada numara ve şirket bilgisi (A5); belge tasarımla |
| Katılımcı | etkisiz |

## Kuyruk / Horizon
Yeni job yok. A7 transaction'ı büyük olabilir (bir şubenin bütün işi) — tek şube ölçeğinde yüzler; zaman aşımı bugünkü HTTP sınırında kalır, ölçülür.

## Performans
`BranchCeiling` istek başına tek üyelik sorgusu (memoize). `TrainingsInScope` `branch` dalı `groups.branch_id` indeksini kullanır. Numara kilidi satır kilididir, şirket başına seri hâlinde.

## Rollout
1. Her aşama ayrı deploy; A1 tek başına davranış değiştirmez (şube yokken tavan boştur).
2. A5 sonrası **fatura kesilemez** ta ki backoffice'te varsayılan şirketin vergi numarası ve seri öneki girilene dek — production'da kesilmiş fatura 0, etki yok; deploy notunda Sets'e yazılır.
3. Şubelerin kurulması (İzmit, İzmir) ve kişilerin yerleştirilmesi Sets'in/Tayfun'un backoffice işidir; plan veri girmez.

## Rollback
Kod `git revert`. Migration'lar ekleme; `down()` var. ⚠ Şube açıldıktan / fatura numarası verildikten sonra şema geri alınmaz (numara ve şube kaydı kaybolur); yalnız kod geri alınır. `training_scopes` CHECK'i eski hâline `down()` ile döner, `branch` satırı yoksa.

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

| Konu | Adres |
|---|---|
| Fatura belgesi ekranı (ofis + portal) | tasarım gelince ayrı tur (brief `docs/design/fatura-belgesi-brief.md`); veri A5'te hazır |
| Yasal ibare metni | Sets verecek; `issuing_companies.legal_notice` alanı A5'te |
| e-fatura | AD-12 |
| EGT-220 (kiracısız erişim logu, mevcut dört okuma) | ayrı ticket |

## Revizyon geçmişi

| Sürüm | Tarih | Değişiklik |
|---|---|---|
| v1.0 | 20.10.2026 | İlk sürüm |
| v1.1 | 20.10.2026 | Heyet 820 (REJECTED): F-1 tek `issuer.choose` yetkisi; F-2 şirket kimliğinin tek kaynağı (dört okuyan + BusinessSettings alanları kaldırılır, arch testi); F-3 A7 açık teklif ve potansiyel müşteriyi listeler/sorar; F-4 `branches.issuing_company_id` A5'e taşındı + backfill; F-5 talep kuyruğu testi A1'de |
| v1.2 | 20.10.2026 | Heyet 821 F-1: `company.letterhead_min_width_px` kimlik değil, kapsamdan çıkarıldı; arch testi dört kimlik anahtarına daraltıldı |
