# Plan — Ofis T4: Kullanıcı Oluşturma, Grup Ağacı ve Eğitmen Kaydı

> **Uygulayıcı için:** Bu plan görev görev uygulanır. Adımlar `- [ ]` kutucuklarıyla takip edilir. Her görev kendi test döngüsünü taşır ve bağımsız olarak commit edilir.

*Sürüm 2.3 — 28.07.2026. Ticket: `EGT-59`. Heyet 159 (v1.0) REJECTED — 16 bulgu; heyet 160 (v2.0) REJECTED — 9 bulgu; heyet 161 (v2.1) REJECTED — 5 bulgu; heyet 162 (v2.2) REJECTED — 3 bulgu. Bu sürüm dördünü de karşılar.*

**Upstream:** `docs/arch/mimari-kararlar.md` **AD-23** (organizasyon ve yetki modeli) · **AD-09** (ofis/backoffice sınırı, provisioning muafiyeti) · **AD-24 Ek 2** (sahip başına evrak tablosu) · AD-05, AD-11, AD-21, AD-26 · `cati-dokumani.md` §174, §178 · `docs/specs/egitmen-spec.md` §1–§4 (heyet 158 APPROVED, **kilitli**) · `design/61 Yonetici Tanimlar.dc.html`, `design/63 Yonetici Kullanici Log.dc.html`.

⚠ **Bu turun ayrı bir spec'i yoktur** *(Sets, 28.07.2026)*. Upstream doğrudan AD'ler, çatı, kilitli `egitmen-spec` ve tasarımdır.

**Hedef:** Ofis kullanıcısı **oluşturulabilir** hale gelsin — grubuyla ve seviyesiyle; eğitmen ise mevzuat dosyası ve belgeleriyle. Parolayı kullanıcı kendisi belirlesin. Yetki, kullanıcı ilk girişini yaptıktan sonra yetkililerce tanımlansın. *"Bu kayıt kimin kapsamında"* sorusunun tek bir çözümleyicisi olsun.

**Mimari:** AD-23'ün tek grup ağacı `groups` + `group_memberships`; üyelik **seviye** taşır (`director` · `group_manager` · `staff`), yetki taşımaz. Kapsam seviyesi izin adının eki olur (`resource.action.own|group|all`) ve tek çözümleyiciden geçer. Kullanıcı oluşturma tek Action'dır, iki giriş noktası çağırır (ofis Inertia, backoffice Filament). Parola hiçbir yolda üçüncü kişi tarafından belirlenmez.

---

## v1.0'ın kök hatası — bu sürümün taşıdığı disiplin

⛔ v1.0 **repoyu okumadan** yazıldı: var olan bir kolonu ekliyordu, olmayan izinleri kapsamlandırıyordu, boş bir rol sözlüğüne dayanıyordu, daraltılmış bir tabloya yazıyordu ve mevcut bir kontrolün *"yapılmadığını"* iddia ediyordu. Beş bulgu doğrudan bundan doğdu.

**Bu sürümde her iddia dosyadan doğrulanmıştır ve kanıtı satır referansıyla yanındadır.** Doğrulanmamış satır bırakılmadı; bırakılmışsa bulgudur.

---

## Doğrulanmış mevcut durum

| Ne | Gerçek | Kanıt |
|---|---|---|
| Ofis izin sözlüğü | `user.view · user.create · user.update · user.deactivate · user.assign_permissions · user.assign_customers · customer.view_all · customer.create · customer.update · customer.deactivate · document.view · document.download · document.upload · document.view_special_category · audit.view · analytics.view · financial.view` | `config/authorization.php:25-80` |
| Ofis rol sözlüğü | **Boş.** İzinler kişiye doğrudan verilir | `LocalOfficeAccountSeeder:80-85` |
| Backoffice'in ofis kapısı | `office_user.view · office_user.create · office_user.update · office_user.assign_permissions` | `config/authorization.php:184-187` |
| `office_magic_links.purpose` | **Zaten var** — `string(32)`, default `sign_in` | `2026_07_28_040100:37` |
| Hesap durumu | `HasAccountStatus` + `isActive()` + `EnsureAccountIsActive` | `app/Models/User.php:44,93` · `app/Http/Middleware/EnsureAccountIsActive.php:30` |
| Devir kuralı | `AssignOfficePermissions` **alt-küme kontrolünü zaten yapıyor** (`beyondTheGranter`) | `app/Actions/Office/AssignOfficePermissions.php:51-58` |
| Evrak tabloları | `documents` → `customer_documents`'a **daraltıldı**, tenant-scoped; ofis kimliği yazamaz | `2026_08_06_000100:60-82` |
| Evrak şablonu | `document_types` — `uploadable_by` (`operations`/`owner`/`both`), `requires_approval`, `has_expiry`, `is_special_category` | `2026_08_04_000200:71-133` |
| Evrak kapsamı | `document_type_scopes.scope` | `2026_08_04_000200:145-156` |
| Bildirim şablonu | `notification_templates` — `key` + `version` + jsonb `subject`/`body` | `2026_07_28_070000:47-90` |
| Ayarlar | `settings` (key + jsonb) + `Settings` sınıfı | `2026_07_28_080000:36-44` · `app/Support/Business/Settings.php` |
| Yetki çağıranları | `UserPolicy:49,54,59,70,81` · `UserController:42,62,140,141` · `OfficeNavigation:47,48` | — |

---

## Global kısıtlar

Her görevin gereklilikleri bu bölümü **örtük olarak içerir**.

- **Dil:** kod tarafı İngilizce; arayüz Türkçe ve **yalnız** `lang/` üzerinden (`tests/Arch/LocaleTest.php`).
- **Katman sözleşmesi (AD-25):** Controller'da query builder ve `validate()` yok · Action tek public `__invoke` · sorgu `app/Queries/` · doğrulama `app/Http/Requests/` · `app/Support/` karar vermez.
- **Test grubu:** `tests/Feature/Omurga/` → `omurga` · `tests/Feature/OmurgaRls/` → `omurga-rls` · `tests/Feature/Backoffice/` → `backoffice`. Grupsuz test reddedilir (AD-26).
- **Testte çıplak `now()` yok**; saat `tests/Pest.php`'de donduruluyor.
- **Audit:** kritik işlem **aynı transaction'da** loglanır (kırmızı çizgi 13).
- **`env()` yalnız `config/` içinde.** İş kuralı sabiti yok (AD-08) — süreler, eşikler `settings`'ten.
- **Kritik kayıt silinmez** (AD-11). Kullanıcı **pasifleştirilir** (AD-23).
- **Şema yalnız migration ile** (kırmızı çizgi 6).
- **Commit:** ilk satır `#EGT-59 In Progress work <süre>`, ikinci satır conventional commit. AI attribution yasak.
- **Lokal kapı:** `pint --dirty` → `composer analyse` → ilgili grup → commit. Görev başına TeamCity yok.
- **Kapanış kapısı:** TeamCity `trigger` + `wait`, **yeşil**.

### Bu planın upstream'e eklediği türetmeler

**T-1 — Kapsam seviyesi izin adının eki olur** (`resource.action.own|group|all`). Dayanak `config/authorization.php:13-15`'in kendi notu. Ayrı `level` kolonu Spatie'nin isim-tabanlı önbelleğini ve `PermissionFamilies` yolunu bozardı.

**T-2 — Görev (task) kaydı geneldir.** Bu turda üç tüketicisi var: eğitmen belgesi (K-C), yetki bekleyen kullanıcı (K-D), ileride kadro değişikliği bildirimi. Sahibi **polimorfiktir** — ofis kullanıcısı da olabilir backoffice kullanıcısı da (K-E'nin zorunlu sonucu).

**T-3 — Davet, mevcut `purpose` kolonunun ikinci değeridir.** Yeni tablo da yeni kolon da açılmaz.

**T-4 — K-C'nin mekanizması `document_types.uploadable_by`'dır**, yeni bir alan değil (`2026_08_04_000200:122`). *"Yönetici yükler / kendi yüklesin"* seçimi, tür `both` ise kayıt anında yapılan bir tercihtir; tür `operations` ya da `owner` ise seçim yoktur ve ekran seçenek göstermez.

---

## Kararlar — Sets, 28.07.2026

| # | Karar | Görev |
|---|---|---|
| K-A | **Asla parola belirlemiyoruz.** Davet magic link'i gider, kişi kendi belirler. Davet **24 saat** geçerli (admin ayarı, açılış değeri). | 3 |
| K-B | **Kullanıcı yönetimi hem ofiste hem backoffice'te.** | 4, 10 |
| K-C | **Eğitmen belgelerini ya hesabı açan yükler, ya kişiye görev düşer**; durumu kullanıcı panelinden takip edilir. | 8, 9 |
| K-D | ⛔ **Yetki oluşturma anında verilmez.** Kullanıcı **ilk girişini yapana kadar** yetkisizdir; girişinden sonra kapsamdaki yetkililere görev düşer. *"Kimse `all` ya da başka bir yetkiyle sisteme girmez."* | 4, 6 |
| K-E | **Yetkili yoksa görev backoffice'e düşer** — ve bu bir bootstrap değil **kalıcı işletim modudur**: *"backoffice'ten operasyon direktörü oluşturulurken kullanıcı oluşturma yetkisi verilmezse bu yetki backoffice'te kalır."* | 5, 6 |
| K-F | **Üyelik seviyesi rol yerine geçer:** `director` · `group_manager` · `staff`. Dikey ağaçtan gelir, ekrandaki ad bileşimden türer. **Seviye yetki vermez, kapsam belirler**; yetkiler havuzdan tek tek seçilir. | 1, 2 |

---

## Dosya haritası

**Şema**

| Dosya | Sorumluluk |
|---|---|
| `…_create_group_tree.php` | `groups`, `group_memberships` (seviyeli) |
| `…_scope_permission_names.php` | Ofis izinlerini kapsamlandırır; `customer.view_all` → `customer.view.all` |
| `…_create_tasks.php` | Genel görev — **polimorfik sahip** |
| `…_create_instructors.php` | Eğitmen mevzuat dosyası |
| `…_create_instructor_documents.php` | AD-24 Ek 2 — `instructor_documents` + `instructor_document_versions` |
| `…_create_office_user_documents.php` | AD-24 Ek 2 — ofis kullanıcısının kendi belgeleri |

⛔ **`purpose` migration'ı YOKTUR** — kolon zaten var (`2026_07_28_040100:37`).

**Sunucu**

| Dosya | Sorumluluk |
|---|---|
| `app/Models/Group.php`, `GroupMembership.php`, `Instructor.php`, `Task.php`, `InstructorDocument.php` | Modeller |
| `app/Enums/MembershipLevel.php` | `director` · `group_manager` · `staff` (K-F) |
| `app/Support/Authorization/PermissionScope.php` | `own`/`group`/`all` ayrıştırma (T-1) |
| `app/Support/Authorization/ScopeResolver.php` | AD-23'ün tek kapsam çözümleyicisi |
| `app/Support/Authorization/ScopedGate.php` | Çıplak isimle sorulan kapsamlı izni çözer (F-4) |
| `app/Actions/Office/CreateOfficeUser.php` | Kullanıcı oluşturma — tek yol, iki çağıran |
| `app/Actions/Office/InviteOfficeUser.php` | Davet |
| `app/Actions/Auth/AcceptOfficeInvitation.php` | Daveti tüketir, parolayı belirler, görevi açar |
| `app/Actions/Office/CreateInstructorRecord.php` | Eğitmen dosyası |
| `app/Actions/Office/OpenTask.php`, `CompleteTask.php`, `ResolveTasks.php` | Görev yaşam döngüsü |
| `app/Actions/Office/UploadInstructorDocument.php` | Belge yükleme (iki yol) |
| `app/Queries/Office/GroupTree.php`, `AssignableGroups.php`, `GrantablePermissions.php` | Ağaç ve devir |
| `app/Queries/Office/UsersAwaitingPermissions.php`, `MyTasks.php`, `Instructors.php`, `InstructorDocumentStatus.php` | Listeler |
| `app/Http/Controllers/Office/UserController.php` | +`create` +`store` +`permissions` |
| `app/Http/Controllers/Office/InstructorController.php`, `TaskController.php` | Eğitmen ve görev ekranları |
| `app/Http/Controllers/Auth/OfficeInvitationController.php` | Davet kabul |
| `app/Filament/Backoffice/Resources/Groups/…`, `OfficeUsers/…`, `PendingOfficeTasks…` | Backoffice tarafı (K-B, K-E) |

**Ön yüz**

| Dosya | Sorumluluk |
|---|---|
| `pages/Office/Users/Create.vue`, `Permissions.vue` | Oluşturma ve yetki tanımlama |
| `pages/Office/Instructors/Index.vue`, `Create.vue` | Tasarım 61 |
| `pages/Office/Tasks/Index.vue` | Görev listesi (F-8) |
| `pages/Auth/AcceptInvitation.vue` | Davet kabul + parola |
| `organisms/GroupPicker.vue`, `PermissionChecklist.vue` · `molecules/DocumentRequirement.vue` | Bileşenler |

---

## Görev 1: Grup ağacı, üyelik ve seviye

**Dosyalar:** `…_create_group_tree.php` · `app/Models/Group.php`, `GroupMembership.php` · `app/Enums/MembershipLevel.php` · `database/factories/GroupFactory.php` · `tests/Feature/Omurga/GroupTreeTest.php`

- [ ] **Adım 1: Migration**

```php
Schema::create('groups', function (Blueprint $table): void {
    $table->bigIncrements('id');
    $table->foreignId('parent_id')->nullable()->constrained('groups')->restrictOnDelete();
    $table->string('name');
    $table->string('vertical', 32)->nullable();   // operations | sales | planning | instructors
    $table->boolean('is_active')->default(true);
    $table->timestampsTz(precision: 6);
    $table->unique(['parent_id', 'name']);
});

Schema::create('group_memberships', function (Blueprint $table): void {
    $table->bigIncrements('id');
    $table->foreignId('user_id')->constrained('users')->restrictOnDelete();
    $table->foreignId('group_id')->constrained('groups')->restrictOnDelete();
    $table->string('level', 16);                  // director | group_manager | staff
    $table->timestampsTz(precision: 6);
    $table->unique(['user_id', 'group_id']);
});
```

⛔ **`role_id` YOKTUR** (K-F). Ofis ailesinin rol sözlüğü boştur (`LocalOfficeAccountSeeder:80-85`) ve bu turda **doldurulmaz**: rol bir yetki seti olsaydı üyelikle birlikte yetki verilirdi, ki K-D bunu yasaklıyor. Üyelik **seviye** taşır, seviye kapsam belirler.

⚠ Tekillik `(user, group)` — aynı kişi aynı grupta iki seviyede olamaz. AD-23'ün *"bir kişi birden fazla grupta farklı rollerle bulunabilir"* kuralı bozulmuyor: farklı **gruplarda** farklı seviyeler serbest, ve aynı kişi iki dikeyde birden direktör olabilir (AD-23'ün 28.07.2026 düzeltmesi).

⚠ `restrictOnDelete` — grup silinmez, `is_active` ile pasifleşir.

- [ ] **Adım 2: `MembershipLevel` enum'u**

```php
enum MembershipLevel: string
{
    case Director = 'director';
    case GroupManager = 'group_manager';
    case Staff = 'staff';

    /** Kapsam yalnız `staff` için daralır; kalan ikisi düğüm + altlarıdır. */
    public function seesOnlyOwnRecords(): bool;
}
```

⚠ `director` ile `group_manager` arasındaki farkı **seviye değil ağaçtaki yer** yaratır — AD-23'ün *"direktör için özel `if` yoktur"* cümlesi. Seviye ekrandaki ad ve ileride onay silsilesi için taşınır.

- [ ] **Adım 3: Modeller** — `Group::descendants()` recursive CTE ile tek sorguda:

```sql
WITH RECURSIVE subtree AS (
    SELECT id FROM groups WHERE id = ANY(?)
    UNION ALL
    SELECT g.id FROM groups g JOIN subtree s ON g.parent_id = s.id
) SELECT id FROM subtree
```

- [ ] **Adım 4: Test** (`omurga`) — ağaç kurulur, torunlar gelir · aynı kişi iki grupta iki farklı seviyede olabilir · aynı grupta ikinci üyelik reddedilir · üyeliği olan grup silinemez · pasif grup ağaçta durur.

- [ ] **Adım 5–6:** kırmızı gör, yeşile al, kalite kapısı, commit.

---

## Görev 2: Kapsam seviyeleri, çözümleyici ve çağıranların taşınması

⛔ **Bu görev bir veri + kod taşımasıdır.** İzin adı değişiyor; onu soran her yer birlikte taşınmazsa yetki sessizce kaybolur.

**Dosyalar:** `…_scope_permission_names.php` · `app/Support/Authorization/PermissionScope.php`, `ScopeResolver.php`, `ScopedGate.php` · `config/authorization.php` · `app/Policies/UserPolicy.php` · `app/Http/Controllers/Office/UserController.php` · `app/Queries/Office/OfficeNavigation.php`, `OfficePermissionMatrix.php`, `OfficeUsers.php` · `tests/Feature/Omurga/ScopeResolverTest.php`, `ScopedPermissionMigrationTest.php`

- [ ] **Adım 1: Hangi izinler kapsamlanır — mevcut sözlükten seçilerek**

| İzin | Kapsamlanır mı | Gerekçe |
|---|---|---|
| `user.view` | ✓ `.own/.group/.all` | Kim kimi görebilir |
| `user.update` | ✓ | Kim kimi düzenleyebilir |
| `user.deactivate` | ✓ | Kim kimi pasifleştirebilir |
| `user.assign_permissions` | ✓ | Kim kime yetki verebilir |
| `user.create` | ✓ `.group/.all` | AD-23: yeni kullanıcı aktörün kapsamında olmalı. ⚠ `.own` anlamsızdır, **üretilmez** |
| `customer.view_all` | ✓ → **`customer.view.all`** (yalnız yeniden adlandırma) | Ad zaten kapsam söylüyordu. ⛔ `.group` ve `.own` sürümleri **üretilmez** — gerekçesi Adım 4'te, kriteri A-11c. Atama tabanlı kapsam izne bağlanmaz |
| `user.assign_customers`, `customer.create/update/deactivate`, `document.*`, `audit.view`, `analytics.view`, `financial.view` | ✗ | Kapsam sorusu taşımıyorlar; bu turda dokunulmaz |

⛔ **`quote.view` ve `training.view` YOKTUR** — v1.0 bunları kapsamlandırıyordu; ofis sözlüğünde böyle izinler yok (`config/authorization.php:25-80`). Kendi turlarında **doğarken kapsamlı doğarlar**.

⚠ `training.view` backoffice ailesindedir ve backoffice'in kapsam kavramı yoktur (AD-09: *"yetki tavanı yoktur"*) — migration **yalnız ofis guard'ını** filtreler.

- [ ] **Adım 2: `PermissionScope`**

```php
enum PermissionScope: string
{
    case Own = 'own';
    case Group = 'group';
    case All = 'all';

    /** @return array{0: string, 1: self|null} `user.view.group` → ['user.view', Group]; `audit.view` → ['audit.view', null] */
    public static function split(string $ability): array;
}
```

⚠ Kapsamsız isim `null` döner ve **`All` sayılmaz** — sessiz genişletme olurdu.

- [ ] **Adım 3: `ScopeResolver`**

```php
/**
 * @return list<int>|null Erişilebilir grup id'leri; null = sınırsız (`all`).
 * @throws LogicException Kapsam eki taşımayan bir izin adı verilirse.
 */
public function groupsFor(User $user, string $ability): ?array;
public function covers(User $user, string $ability, User $subject): bool;
```

AD-23 birebir: `all` → null · `group` → üyelik düğümleri + altları · `own` → yalnız kendisi. Kişi birden çok üyelik taşıyorsa **birleşim**.

⛔ **İki farklı `null` karıştırılmaz (R2-4).** `PermissionScope::split()` kapsam eki olmayan isim için `null` döner — *"bu izin kapsam sorusu taşımıyor"*. `groupsFor()` ise `null`'ı **sınırsız** anlamında kullanır. Kapsamsız bir isimle `groupsFor()` çağrılırsa **istisna atar**, sınırsız dönmez: aksi hâlde kapsam sorusu olmayan bir izin sessizce sınırsız kapsam kazanırdı. Bu bir programlama hatasıdır ve öyle davranır.

- [ ] **Adım 3b: Seviye × izin eki bileşimi — dar olan kazanır (R2-1)**

⛔ İki kaynak birden kapsam söylüyor: üyelik **seviyesi** (K-F) ve izin **eki** (T-1). Kural tektir:

```
etkin kapsam = min(izin eki, seviyenin izin verdiği en geniş kapsam)
```

⛔ **Seviye tavanı yalnız `.group` ekini daraltır.** `.all` ve `.own` seviyeden ve üyelikten **bağımsızdır**.

| İzin eki | Seviye | Etkin |
|---|---|---|
| `.all` | herhangi (üyeliksiz dahil) | **`all`** — ağaçtan bağımsız |
| `.group` | `director` / `group_manager` | Düğüm + altları |
| `.group` | `staff` | **`own`** — seviye daraltır |
| `.group` | üyeliği yok | **`own`** — kapsanacak düğüm yok |
| `.own` | herhangi | `own` |

⚠ Gerekçe AD-09'un cümlesi: *"etkin değer zincirin en dar halkasıdır"* — ama daraltma **yalnız grup ekseninde** anlamlıdır. `all`'ı seviyeyle daraltmak iki şeyi bozardı:

1. **`.all` ölü kapsam olurdu** — hiçbir seviyenin tavanı `all` olmadığından hiç kimse `all` alamazdı.
2. ⛔ **A-12 çökerdi:** bugünkü kullanıcıların **üyeliği yok** (ağaç bu turda doğuyor) ve migration onları `.all`'a taşıyor. *"Üyeliksiz → own"* kuralı uygulansaydı, taşıma ertesi sabah herkes yalnız kendini görürdü — *"kimse bugünkünden az görmez"* güvencesinin tam tersi.

⚠ Org ağacının dışına çıkmak **bilinçli bir yetkidir**; `.all` taşıyan kişi ağaçtaki yerinden bağımsız olarak her şeyi görür. Kriter A-13 `group_manager` + `user.view.group` bileşimiyle koşulur.

- [ ] **Adım 4: `ScopedGate` — çağıranlar kırılmasın (F-4)**

⛔ Mevcut çağıranların **tam envanteri** (grep ile doğrulanmış): `UserPolicy:49,54,59,70,81` · `UserController:42,62,140,141` · `OfficeNavigation:47,48` · **`app/Support/Tenancy/SessionScopeResolver.php:73`** · izin adlarının kayıt defteri **`app/Support/Authorization/PermissionMatrix.php`**. Rename sonrası bu çağrılar **hiçbir izni bulamaz** ve herkes yetkisiz kalır.

⛔ **`SessionScopeResolver:73` özel dikkat ister (R2-2):** bugün `allows('customer.view_all')` ile *"tüm müşteriler"* kapsamını açıyor — bu bir **kapsam kararıdır**, yetki kontrolü değil. `ScopedGate` bunu yakalayamaz: gate *"bir şekilde görebiliyor mu"* der, oysa burada sorulan *"hepsini mi görüyor"*dur.

⛔ **Değişiklik minimum tutulur: yalnız izin adı güncellenir.** Bugünkü iki kademeli davranış (`officeScope`, satır 70-86) aynen korunur:

| Koşul | Kapsam | Bugün | v2.1'den sonra |
|---|---|---|---|
| `customer.view.all` taşıyor | Tüm müşteriler | `customer.view_all` | Yeni ad |
| Taşımıyor | **`office_user_customers` atamaları** | Aynı | **Aynı** |

⛔ **`customer.view.group` bu turda ÜRETİLMEZ (R3-2).** İki sebep: (a) müşteri → grup zorunlu ataması bu turda yok (kapanış tablosu, madde 6) — grup kapsamının müşteri tarafında karşılığı henüz tanımsızdır; (b) atama tabanlı ikinci kademe **izin gerektirmiyor** ve bugün öyle çalışan kullanıcılar var; araya izne bağlı bir kademe koymak onların kapsamını **sessizce sıfırlardı**.

Bu turda `customer.view` için üretilen tek isim **`customer.view.all`**'dır (mevcut `customer.view_all`'ın yeniden adlandırılmışı). `.group` ve `.own` sürümleri müşteri turunda, atama modeli netleştiğinde doğar.

⚠ `.own` da üretilmez: atama tabanlı kapsam **varsayılan davranıştır**, izinle ifade edilmez.

⚠ Bu satır atlanırsa müşteri kapsamı sessizce daralır — testi A-11b'dir.

Çözüm: `Gate::before` benzeri bir çözümleyici — çıplak isimle sorulan kapsamlı bir izin, kişinin taşıdığı **herhangi bir** kapsam sürümüyle karşılanır (*"bir şekilde görebiliyor mu"*). Kapsamın kendisi `ScopeResolver` ile ayrıca sorulur.

⚠ Bu, çağıranları tek tek değiştirmekten üstündür: *"görebilir mi"* ile *"neyi görebilir"* farklı sorulardır ve her çağıranın ikisini birden sorması gerekmez.

⛔ **Ama kapsam iki yerde birden uygulanmak zorundadır (R2-3):**

| Nerede | Nasıl |
|---|---|
| **Liste** — `OfficeUsers` sorgusu | `ScopeResolver::groupsFor()` ile filtre |
| **Tek kayıt** — `office.users.show`, `user.update`, `user.deactivate` | `ScopeResolver::covers($actor, $ability, $subject)` |

⚠ Yalnız listeyi filtrelemek kapsamı **süs** yapar: `user.view.group` taşıyan kişi listede kapsamındakileri görür ama detay rotasına id yazarak kapsam dışındaki herkesi açabilirdi. `UserPolicy`'nin `view`, `update`, `deactivate` metotları `covers()` çağırır.

- [ ] **Adım 5: Migration**

Ofis guard'ında, tek tek adlandırılmış izinler için: yeniden adlandırma + çoğaltma. **Mevcut atamalar `.all` sürümüne taşınır** — bugün o izni taşıyan bugünkü kadar görür.

⛔ `down()` yazılır: `.own`/`.group` satırları silinir, `.all` çıplak isme döner, atamalar geri taşınır.

⚠ Migration sonrası `AuthorizationSeeder` koşturulur (`docs/infra/ortamlar.md`).

- [ ] **Adım 6: Testler**

`ScopeResolverTest` (`omurga`): üç kapsam · birleşim · **kapsamsız isimde istisna** (Adım 3'ün sözleşmesi; `null` **dönmez**) · bileşim tablosunun beş satırı (Adım 3b).
`ScopedPermissionMigrationTest` (`omurga`): migration öncesi izne sahip kullanıcı sonrasında **aynı kümeyi** görür · `customer.view_all` taşıyan `customer.view.all` taşır · çıplak isimle `can()` hâlâ `true` (F-4) · `user.view.group` taşıyan kullanıcı listesinde yalnız kapsamındakileri görür · `down()` geri alır.

- [ ] **Adım 7–8:** yeşile al, kalite kapısı, commit.

---

## Görev 3: Davet — mevcut `purpose` kolonunun ikinci değeri

**Dosyalar:** `app/Models/OfficeMagicLink.php` · `app/Actions/Office/InviteOfficeUser.php` · `app/Actions/Auth/AcceptOfficeInvitation.php` · `app/Http/Controllers/Auth/OfficeInvitationController.php` · `app/Http/Requests/Office/AcceptInvitationRequest.php` · `pages/Auth/AcceptInvitation.vue` · `database/seeders/NotificationTemplateSeeder.php` · `app/Support/Business/Settings.php` · `tests/Feature/Omurga/OfficeInvitationTest.php`

⛔ **Migration yok.** `purpose` kolonu `2026_07_28_040100:37`'de var. Yeni değer: `invitation`.

- [ ] **Adım 1: `invited` hesap durumu**

`HasAccountStatus` ve `EnsureAccountIsActive` zaten var (`app/Models/User.php:44,93` · `EnsureAccountIsActive:30`). Bu adım `invited` değerini durum sözlüğüne ekler ve `isActive()`'in onu **aktif saymamasını** sağlar.

⚠ `invited` kullanıcı giriş **yapamaz**, yalnız daveti kabul edebilir — davet rotası `EnsureAccountIsActive` dışındadır.

- [ ] **Adım 2: TTL ayarı**

`Settings`'e `invitationValidHours(): int`; anahtar `auth.invitation_valid_hours`, **açılış değeri 24** (K-A), seeder ile yazılır. ⛔ Koda sabit yazılmaz (AD-08).

- [ ] **Adım 3: Bildirim şablonu**

`notification_templates`'e `office.invitation` anahtarlı satır seeder ile eklenir (`key` + `version` + jsonb, `2026_07_28_070000:47-90`). ⚠ Şablon **veri**dir; koda gömülü metin yasak.

- [ ] **Adım 4: İki amacın davranış farkı**

| | `sign_in` | `invitation` |
|---|---|---|
| Geçerlilik | Mevcut ayar | **24 saat** (ayar) |
| Tüketince | Oturum açar | **Parola belirletir**; parola yazılmadan oturum açılmaz |
| Hesap durumu | `active` olmalı | `invited` olabilir |

- [ ] **Adım 5: Action'lar**

`InviteOfficeUser`: link üretir, şablonla gönderir, audit'ler.
`AcceptOfficeInvitation`: doğrular → parolayı yazar → durumu `active` yapar → token'ı tüketir → **yetki görevini açar** (Görev 6) → audit'ler. Hepsi **tek transaction**.

⛔ Parola kuralı mevcut `ResetOfficePasswordRequest`'inkiyle **aynı**; ikinci bir güç kuralı yazılmaz.

- [ ] **Adım 6: Test** — davet oturum açmaz, parola belirletir · `invited` giriş yapamaz · kabul sonrası `active` · token tek kullanımlık · 24 saatten eski davet reddedilir (`travelTo`) · TTL ayardan okunur · kabul audit'lenir.

- [ ] **Adım 7–8:** yeşile al, kalite kapısı, commit.

---

## Görev 4: Kullanıcı oluşturma — tek Action, iki çağıran

**Dosyalar:** `app/Actions/Office/CreateOfficeUser.php` · `app/Http/Requests/Office/CreateUserRequest.php` · `app/Queries/Office/AssignableGroups.php` · `app/Http/Controllers/Office/UserController.php` · `routes/office.php` · `pages/Office/Users/Create.vue` · `organisms/GroupPicker.vue` · `tests/Feature/Omurga/CreateOfficeUserTest.php`

- [ ] **Adım 1: İmza**

```php
final class CreateOfficeUser
{
    /** @param list<array{group_id: int, level: MembershipLevel}> $memberships */
    public function __invoke(
        object $actor,                                // User (ofis) veya BackofficeUser
        NewOfficeUser $data,                          // ad, e-posta, telefon
        array $memberships,
        ?NewInstructorFile $instructorFile = null,    // dikey `instructors` ise (F-15)
    ): User;
}
```

⛔ **İmzada ne parola vardır ne izin** (K-A, K-D). Kural bir kontrolde değil, **imzada** yaşar.

- [ ] **Adım 2: Yetkilendirme — tipe değil izne bakılır (F-11)**

| Aktör | Kapı | Kapsam |
|---|---|---|
| Ofis | `user.create` (kapsamlı) | Hedef gruplar `ScopeResolver` kapsamında olmalı |
| Backoffice | **`office_user.create`** (`config/authorization.php:185`) | Kapsam sınırı yok (AD-23 muafiyeti) |

⛔ **Muafiyet aktör tipine bağlanmaz.** v1.0 `BackofficeUser` ise kapsam kontrolünü atlıyordu; AD-09'un tehdit varsayımı **yalnız süper admin** içindir ve ileride eklenecek dar backoffice rolleri (destek görevlisi) bu varsayımı taşımaz. Kapıdan geçmek yine izin ister.

- [ ] **Adım 3: Tek transaction** — kullanıcı + üyelikler + (varsa) eğitmen dosyası + audit + davet kaydı. **İzin yazılmaz** (K-D). Davet **maili** transaction dışında kuyruğa girer.

- [ ] **Adım 4: `AssignableGroups`** — ofis için `user.create` kapsamındaki gruplar, backoffice için tümü.

- [ ] **Adım 5: Rotalar**

```
GET  /yonetim/kullanicilar/yeni   → office.users.create
POST /yonetim/kullanicilar        → office.users.store
```

`auth:office` + `RequireStrongOfficeSession` + `user.create`.

- [ ] **Adım 6: Ekran** — ad soyad, e-posta, telefon, grup + seviye (birden çok, `GroupPicker`).

⛔ **Ne parola alanı vardır ne yetki listesi.** İki cümle durur (`lang/tr/`): *"Kullanıcı e-posta ile davet edilir ve parolasını kendisi belirler"* · *"Yetkiler, kullanıcı ilk girişini yaptıktan sonra tanımlanır."*

⚠ **Eğitmen alanları aynı ekranda, koşullu bölüm olarak (F-15):** seçilen grubun dikeyi `instructors` ise *"Eğitmen bilgileri"* açılır — bakanlık ID, kategori, belge geçerlilik tarihi, ücret (ops.), zorunlu belge satırları. **Sorumluluk sınırı:** `Users/Create` yeni kişiyi açar; `Instructors/*` **mevcut** eğitmenleri listeler ve dosyalarını düzenler.

- [ ] **Adım 7: Test**

- Oluşan kullanıcının **etkin izin kümesi boş** (K-D)
- Ofis aktörü kapsamı dışındaki gruba kullanıcı açamaz
- `office_user.create` **taşımayan** backoffice kullanıcısı açamaz (F-11)
- Taşıyan backoffice kullanıcısı kapsam sınırı olmadan açar
- Hesap `invited`, parolası yok
- Davet kuyruğa girer, kayıt yazılır
- Eğitmen dikeyinde eğitmen dosyası **aynı transaction'da** açılır (A-14)
- Audit yazılır · yetkisiz `403`

- [ ] **Adım 8–9:** yeşile al, kalite kapısı, commit.

---

## Görev 5: Genel görev (task) kaydı — polimorfik sahip

**Dosyalar:** `…_create_tasks.php` · `app/Models/Task.php` · `app/Actions/Office/OpenTask.php`, `CompleteTask.php`, `ResolveTasks.php` · `app/Queries/Office/MyTasks.php` · `tests/Feature/Omurga/TaskTest.php`

- [ ] **Adım 1: Migration**

```php
Schema::create('tasks', function (Blueprint $table): void {
    $table->bigIncrements('id');
    $table->morphs('assignee');          // User | BackofficeUser  (K-E)
    $table->nullableMorphs('opened_by');
    $table->string('kind', 48);
    $table->morphs('subject');
    $table->jsonb('context')->default('{}');
    $table->timestampTz('due_at', precision: 6)->nullable();
    $table->timestampTz('completed_at', precision: 6)->nullable();
    $table->nullableMorphs('completed_by');
    $table->timestampsTz(precision: 6);
    $table->index(['assignee_type', 'assignee_id', 'completed_at']);
});
```

⛔ **Sahip polimorfiktir** çünkü K-E kalıcıdır: yetki backoffice'te kalabilir, dolayısıyla görev de oraya düşebilmelidir — ilk kurulumda değil, **sürekli**.

⚠ `kind` kontrollü sözlükten (AD-08). Bu turun değerleri: `user.permissions_pending` · `instructor.document_upload`.

- [ ] **Adım 2: İki kapanış yolu (F-9)**

| Yol | Kim | Kural |
|---|---|---|
| `CompleteTask` | İnsan | ⛔ **Yalnız kendi görevini** tamamlayabilir |
| `ResolveTasks` | Sistem | Bir olay olunca `(kind, subject)` eşleşen **tüm** açık görevleri kapatır |

⚠ Çelişki değil: fan-out'ta görev kapsamdaki **her yetkiliye ayrı satır** olur; biri işi yapınca kardeş satırları **olay** kapatır, kimse başkasının görevine dokunmaz.

- [ ] **Adım 3: `MyTasks`** — sahibine göre, açık olanlar önce.

- [ ] **Adım 4: Test** — görev açılır · fan-out N satır · `CompleteTask` başkasınınkini reddeder · `ResolveTasks` kardeşleri kapatır · backoffice kullanıcısına görev düşebilir · tamamlama audit'lenir.

- [ ] **Adım 5–6:** yeşile al, kalite kapısı, commit.

---

## Görev 6: Yetki bekleyen kullanıcı

**Dosyalar:** `app/Actions/Auth/AcceptOfficeInvitation.php` · `app/Actions/Office/AssignOfficePermissions.php` · `app/Queries/Office/UsersAwaitingPermissions.php`, `GrantablePermissions.php` · `app/Http/Controllers/Office/UserController.php` · `pages/Office/Users/Permissions.vue` · `organisms/PermissionChecklist.vue`, `AppSidebar.vue` · `app/Filament/Backoffice/Resources/PendingOfficeTasks…` · `tests/Feature/Omurga/PermissionHandoverTest.php`

- [ ] **Adım 1: Yetkisiz giriş meşrudur**

⛔ Hiç izni olmayan kullanıcı **giriş yapabilir**; panel boş açılır: *"Yetkileriniz henüz tanımlanmadı. Yöneticiniz bilgilendirildi."*

⚠ Girişi reddetmek yanlış olurdu: kişi parolasını belirlemiş, MFA'sını kurmuş, hesabın sahibi olduğunu kanıtlamıştır. Ayrıca **giriş, uyarıyı tetikleyen olaydır** — engellenirse akış hiç başlamaz.

- [ ] **Adım 2: Tetikleyici — kullanıcının ilk girişi**

*(Sets teyidi: "kullanıcıyı oluşturduk, daveti gönderdik, davetle kullanıcı sisteme giriş yaptı, bu noktada yetkisiz kullanıcı var task'ı düşüyor")* — hesabın açıldığı an değil.

`AcceptOfficeInvitation` tamamlanınca fan-out:

```
kind:     user.permissions_pending
subject:  yeni kullanıcı
assignee: kapsamında bu kullanıcı olan ve `user.assign_permissions` taşıyan HER ofis kullanıcısı
```

⛔ **Küme boşsa görev backoffice'e düşer** (K-E) — `office_user.assign_permissions` taşıyanlara. Bu bir yedek değil kalıcı bir moddur: 3E operasyon direktörüne kullanıcı yetkisi hiç vermeyebilir.

⚠ Görev **oluşturana değil kapsama** düşer; oluşturan ayrılmış ya da yetkisi değişmiş olabilir.

- [ ] **Adım 3: Devir kuralını tamamla — yarısı zaten var (F-12)**

⛔ `AssignOfficePermissions:51-58` **alt-küme kontrolünü zaten yapıyor** (`beyondTheGranter`). v1.0 bunun yapılmadığını iddia ediyordu; yanlıştı.

Eksik **iki** şey eklenir:

1. **Kapsam seviyesi genişletilemez:** `user.view.group` taşıyan `user.view.all` veremez. Mevcut kontrol isim eşitliğine bakıyor; kapsam sıralaması (`own < group < all`) eklenir.
2. **Hedef kullanıcı verenin kapsamında olmalı** — `ScopeResolver::covers($actor, 'user.assign_permissions', $subject)`.

⚠ Action bugün `User $actor` tipli ve backoffice buradan geçmiyor. **Backoffice yolu eklenir** (K-B): imza `object $actor` olur ve backoffice `office_user.assign_permissions` ile geçer.

⛔ **Backoffice hem kapsam hem alt-küme kontrolünden muaftır (R2-5).** Yalnız kapsamdan muaf tutmak yetmez: `BackofficeUser` **ofis izin ailesinde hiçbir izin taşımaz** (aileler tablo paylaşmaz, AD-09) — dolayısıyla `beyondTheGranter` onun için **her izni** "sahip olmadığı" sayar ve hiçbir yetki veremez. Muafiyet AD-23'ün provisioning istisnasının zaten kapsadığı şeydir; iki kontrol de atlanır, kapı `office_user.assign_permissions` iznidir.

- [ ] **Adım 4: Görünürlük**

- Sidebar rozeti: *"Yetki bekleyen kullanıcı"* + sayı (yalnız yetkisi olan görür)
- `/yonetim/kullanicilar`'da ayrı filtre sekmesi (tasarım 63 deseni)
- Backoffice'te aynı kuyruğun Filament karşılığı (K-E)
- Yetki atanınca `ResolveTasks` kardeş görevleri kapatır

- [ ] **Adım 5: Test**

- İlk girişte fan-out görevi kapsamdaki **her** yetkiliye düşer
- Kapsamda yetkili yoksa görev **backoffice'e** düşer (K-E)
- İzinsiz kullanıcı giriş yapar (`200`), yetki isteyen rotada `403`
- Sahip olmadığı izni veremez (mevcut) · `group` taşıyan `all` veremez (yeni) · kapsamı dışındakine veremez (yeni)
- `office_user.assign_permissions` taşıyan backoffice verebilir; taşımayan veremez
- Atama sonrası kardeş görevler kapanır · audit yazılır

- [ ] **Adım 6–7:** yeşile al, kalite kapısı, commit.

---

## Görev 7: Eğitmen kaydı

**Dosyalar:** `…_create_instructors.php` · `app/Models/Instructor.php` · `database/factories/InstructorFactory.php` · `app/Actions/Office/CreateInstructorRecord.php` · **`app/Actions/Office/ChangeOfficeAccountStatus.php` (Değiştir — Adım 2b'nin ayrılış tetikleyicisi)** · `app/Support/Business/Settings.php` (Değiştir) · `app/Http/Requests/Office/CreateInstructorRequest.php` · `app/Queries/Office/Instructors.php` · `app/Http/Controllers/Office/InstructorController.php` · `pages/Office/Instructors/*.vue` · `tests/Feature/Omurga/InstructorRecordTest.php` · `tests/Feature/OmurgaRls/InstructorSensitiveDataTest.php`

- [ ] **Adım 1: Migration**

```php
Schema::create('instructors', function (Blueprint $table): void {
    $table->bigIncrements('id');
    $table->foreignId('user_id')->unique()->constrained('users')->restrictOnDelete();
    $table->string('category', 16);                    // permanent | contracted | project_based
    $table->string('ministry_instructor_id');          // egitmen-spec §2 — zorunlu
    $table->date('certificate_valid_until');           // Y-2016 md.7/3
    $table->timestampTz('certificate_revoked_at', precision: 6)->nullable();
    $table->unsignedTinyInteger('authorised_institutions')->default(1);  // Y-2016 md.7/5
    $table->date('last_taught_on')->nullable();        // Y-2016 md.8/1 — türetilir
    $table->string('rate_unit', 8)->nullable();        // hourly | daily
    $table->decimal('rate_amount', 12, 2)->nullable();
    $table->timestampsTz(precision: 6);
});
```

⛔ **Kullanıcı silinmez, dosya durur** (`egitmen-spec` §1) — `restrictOnDelete`.

- [ ] **Adım 2: İki kuruluş sınırı bir davranıştır, kolon değil (F-14)**

`authorised_institutions` sınırı **admin-tanımlı ayardır** (AD-27, kırmızı çizgi 14: mevzuat sayısı koda gömülmez): `Settings::maxAuthorisedInstitutions()`, anahtar `instructor.max_authorised_institutions`, **açılış değeri 2** (Y-2016 md.7/5), seeder ile.

⚠ Aşım **kaydı reddeder** (veri kuralı) ve mevzuat kaydı madde referanslı uyarı gösterir.

- [ ] **Adım 2b: Kadro değişikliği bildirim görevi — bu turda açılır (R2-8)**

⛔ **Ertelenemez.** `egitmen-spec` §1.2 ve Y-2016 md.13/j: eğitici kadrosundaki her değişiklik **15 gün içinde** İdareye bildirilir, aksi hâlde idari para cezası. **Tetikleyici olan işe alım bu turda canlıya çıkıyor** — erteleme, bu turda açılan eğitmenler için 15 günlük KUMS hatırlatmasını boşlukta bırakırdı. Altyapı (Görev 5) zaten hazır; eksik olan tek şey görev türüdür.

**İki tetikleyici, tek görev türü:**

| Olay | Nerede | Bu turda mı |
|---|---|---|
| **İşe alım** — eğitmen kaydı açılır | `CreateInstructorRecord` | ✓ |
| **Ayrılış** — eğitmen kullanıcısı pasifleşir | `ChangeOfficeAccountStatus` (üretimde) | ✓ **(R3-4)** |

⛔ **Ayrılış da bu turda bağlanır.** Pasifleştirme akışı `ChangeOfficeAccountStatus` ve `office.users.status` rotasıyla **zaten üretimdedir**; bu turda doğan eğitmenler doğar doğmaz pasifleştirilebilir. Y-2016 md.13/j *"her türlü değişiklik"* der — yalnız işe alımı bağlamak, ayrılışı 15 günlük yükümlülüğün dışında bırakırdı. Pasifleşen kullanıcının `instructor` kaydı varsa aynı görev açılır.

```
kind:     instructor.ministry_notification
subject:  eğitmen
context:  { change: hired | left }
assignee: kapsamında bu eğitmen olan ve `user.update` taşıyanlar; küme boşsa backoffice
          (`office_user.update`) — K-E
due_at:   olay tarihi + ayar
```

⚠ **Neden `user.update` (R3-3):** ofis sözlüğünde bakanlık bildirimine ait bir izin **yok** (`config/authorization.php:25-80`) ve bu turda uydurulmaz. Eğitmenin kaydını yönetebilen kişi, o kaydın doğurduğu mevzuat yükümlülüğünü de üstlenen kişidir; bugünkü sözlükte bunun karşılığı `user.update`'tir.

⛔ **Kapanış adresi:** bakanlık bildirimi turu kendi iznini (`ministry.notify` gibi) tanımladığında bu görevin `assignee` kuralı **oraya taşınır**; plan bunu şimdi uydurmuyor, bugünkü sözlükten en yakınını seçiyor ve seçimi gerekçesiyle kaydediyor.

⚠ **15 gün admin-tanımlı ayardır** (AD-08, `egitmen-spec` §1.2): `Settings::ministryNotificationDays()`, anahtar `instructor.ministry_notification_days`, açılış değeri 15.

⛔ **Görev açar, bildirim göndermez** — bildirim KUMS üzerinden insan eliyle yapılır (`egitmen-spec` §1.2, AD-15).

- [ ] **Adım 3: `last_taught_on` türetilir, girilmez** (`egitmen-spec` §2)

Bu turda **hiçbir eğitim kaydı yok**; alan `null` kalır. ⛔ **Kapanış adresi:** planlamacı turu, eğitim tamamlanma akışı. Bu turda elle doldurulabilir bir alan olarak **açılmaz** — açılırsa doğruluğu birinin hatırlamasına bağlı kalır.

- [ ] **Adım 4: Ücret** — boş alan eksik kayıt değildir (`egitmen-spec` §4.3).

⚠ **Kategori bazlı baz tarifenin tanımlandığı backoffice *Eğitmen ayarları* sayfası bu turda yoktur (F-14).** Kapanış adresi: `egitmen-spec` §9, eğitmen ücret/masraf turu. Bu turda eğitmen-özel ücret girilebilir; boş ücret **uyarı** üretir, hata değil.

- [ ] **Adım 5: Hassas veri — kendi tablosuna (F-6)**

⛔ TCKN/kimlik/ehliyet/SGK **`customer_documents`'a yazılamaz**: o tablo `2026_08_06_000100:60-82` ile tenant-scoped hale getirildi ve ofis kimliğinin tenant'ı yoktur. Koşullu `tenant_id`, chunk 2'nin yasakladığı şeydir.

Doğru adres AD-24 Ek 2'dir: **`instructor_documents`** (Görev 8). Kimlik numarası gibi **alanlar** `instructors` üzerinde `encrypted` cast ile durur (AD-05); okunması erişim logu yazar (AD-21).

- [ ] **Adım 6: Ekranlar** — `Instructors/Index` (tasarım 61: kategori, belgeler sütunları) · `Instructors/Show` mevcut eğitmenin dosyasını düzenler. Yeni kişi `Users/Create`'ten doğar (F-15).

- [ ] **Adım 7: Testler**

`omurga`: kayıt açılır · bakanlık ID'siz reddedilir · aynı kullanıcıya ikinci dosya açılamaz · sınırı aşan kuruluş sayısı reddedilir ve sınır **ayardan** okunur · kullanıcı pasifleşince dosya durur.
`omurga-rls`: şifreli alan maskeli döner, açık okuma erişim logu yazar — **owner rolüyle koşulmaz** (AD-26).

- [ ] **Adım 8–9:** yeşile al, kalite kapısı, commit.

---

## Görev 8: Eğitmen belgeleri — AD-24 Ek 2'nin sahip tablosu

⛔ **v1.0'da bu tablo hiç yoktu** ve belge akışı uygulanamazdı (F-7).

**Dosyalar:** `…_create_instructor_documents.php` · `…_create_office_user_documents.php` · `app/Models/InstructorDocument.php` · `app/Actions/Office/UploadInstructorDocument.php` · `app/Queries/Office/InstructorDocumentStatus.php` · `molecules/DocumentRequirement.vue` · `tests/Feature/Omurga/InstructorDocumentTest.php`

- [ ] **Adım 1: Migration — `customer_documents`'ın çekirdek kolonlarını tekrarla**

AD-24 Ek 2: *"her sahip varlık kendi `<sahip>_documents` ve `<sahip>_document_versions` tablosunu taşır; hepsi aynı çekirdek kolonları tekrarlar çünkü hepsi aynı `document_types` şablonundan üretilir."*

⚠ Kolonlar `customer_documents`'tan birebir kopyalanır (tekrar **bilinçli bedeldir**), sahip referansı `instructor_id` olur, `tenant_id` **yoktur** ve RLS politikası uygulanmaz — ofis verisi tenant-kapsamlı değildir.

⛔ **Kopyalanacak olan yalnız kolonlar değil, silinmezlik trigger'larıdır (R2-6).** `customer_documents` ve `customer_document_versions` üzerinde `app.deny_critical_document_delete()` ve `app.deny_critical_version_delete()` trigger'ları var (`2026_07_26_001000:262-331`); bunlar **kolon değil davranıştır** ve AD-11 ile kırmızı çizgi 12'nin veritabanı seviyesindeki karşılığıdır.

⚠ Yeni sahip tablolarında da kurulur ve **testle kanıtlanır** — kopyalanmazsa kritik bir eğitmen belgesi uygulamadan silinebilir ve koruma yalnız `customer_documents`'ta kalmış olur. Aynısı `office_user_documents` için de geçerlidir.

⚠ **Ofis kullanıcısının kendi belgeleri** için de aynısı: `office_user_documents`. AD-24'ün kapsam listesi *"eğitmen · ofis kullanıcısı · müşteri firma · müşteri personeli · eğitim tanımı"* diyor ve **kapsam listesi ile tablo listesi kilitlidir**.

- [ ] **Adım 2: `document_type_scopes`'a bağla**

Hangi belge türünün eğitmene uygulandığı `document_type_scopes` satırlarından okunur (`2026_08_04_000200:145-156`). ⛔ Zorunlu belge listesi **koda yazılmaz**.

⛔ **Kapsam değeri sabitten okunur, literal yazılmaz (R2-7):** `DocumentType::SCOPE_INSTRUCTOR` — ve değeri **`'egitmen'`**'dir (`app/Models/DocumentType.php:56`), `'instructor'` değil. Ofis kullanıcısı belgeleri için `SCOPE_OFFICE_USER = 'ofis_kullanicisi'`.

⚠ Literal yazılsaydı sorgu **boş dönerdi** ve hata vermezdi: zorunlu belgesi olmayan bir eğitmen gibi görünürdü. ⚠ Bu değerlerin Türkçe olması dil kuralıyla gerilim taşıyor ama **mevcut gerçektir**; değiştirmek bu turun işi değil, sabit üzerinden okumak zorunludur.

- [ ] **Adım 3: K-C'nin mekanizması `uploadable_by`'dır (T-4)**

`document_types.uploadable_by` (`2026_08_04_000200:122`) üç değer taşır:

| Tür değeri | Kayıt ekranında |
|---|---|
| `operations` | Yalnız yönetici yükler; seçenek gösterilmez |
| `owner` | Yalnız sahibi yükler; **görev otomatik açılır** |
| `both` | ⚠ **Seçim burada:** *"ben yükleyeyim"* / *"kendi yüklesin"* (K-C) |

⚠ *"Kendi yüklesin"* → `OpenTask(kind: instructor.document_upload)`, sahibi eğitmen.

- [ ] **Adım 4: Yükleme tek Action** — `UploadInstructorDocument`; yükleyen yönetici ya da sahibi olabilir, yetki `document.upload` + `uploadable_by` uyumu. Yükleme, görevi `ResolveTasks` ile kapatır.

- [ ] **Adım 5: Durum sorgusu** — hangi belge var, hangisi görevde, hangisi eksik; kullanıcı detayının *"geciken işleri"* bandını besler (tasarım 63).

⚠ Tasarım 61'in *"eksik zorunlu belgeyle kayıt engelli başlar"* ifadesi burada **türetilmiş durumdur**, kolon değil: eksik belgeli eğitmen `egitmen-spec` §3'ün uygunluk kapısından geçemez.

- [ ] **Adım 6: Test** — `both` türde seçim çalışır · `owner` türde görev otomatik açılır · `operations` türde seçenek yok · yükleme görevi kapatır · eksik belgeli eğitmen aday listesinde görünmez · özel nitelikli belge görüntülemesi erişim logu yazar (AD-21).

- [ ] **Adım 7–8:** yeşile al, kalite kapısı, commit.

---

## Görev 9: Görev ekranları — K-C'nin eğitmen tarafı

⛔ **v1.0'da `MyTasks` hiçbir ekrana bağlanmıyordu** (F-8): görev açılıyor, kimse göremiyordu.

**Dosyalar:** `app/Http/Controllers/Office/TaskController.php` · `pages/Office/Tasks/Index.vue` · `organisms/AppSidebar.vue` · `routes/office.php` · `tests/Feature/Omurga/TaskScreenTest.php`

- [ ] **Adım 1: `/yonetim/gorevlerim`** — kişinin açık görevleri; her satır kaynağına götürür.
- [ ] **Adım 2: Sidebar rozeti** — açık görev sayısı.
- [ ] **Adım 3: Eğitmenin belge yükleme yolu** — görevden dosyaya, `UploadInstructorDocument` ile.
- [ ] **Adım 4: Test** — eğitmen görevini görür ve tamamlar · başkasının görevini göremez · rozet sayısı doğru.
- [ ] **Adım 5–6:** yeşile al, kalite kapısı, commit.

---

## Görev 10: Backoffice tarafı

**Dosyalar:** `app/Filament/Backoffice/Resources/Groups/…` · `OfficeUsers/…` · `PendingOfficeTasks…` · `tests/Feature/Backoffice/GroupResourceTest.php`, `OfficeUserProvisioningTest.php`

- [ ] **Adım 1: Grup ağacı kaynağı** — düğüm ekleme, yeniden adlandırma, pasifleştirme (K-B: ofis de yapabilir; ikisi de aynı Action'ları çağırır).
- [ ] **Adım 2: Kullanıcı oluşturmayı `CreateOfficeUser`'a bağla** — ⛔ Filament kendi yaratma kodunu yazmaz.
- [ ] **Adım 3: Yetki bekleyen kuyruğu** (K-E) — kapsamda ofis yetkilisi yokken görevler buraya düşer; buradan yetki tanımlanır.
- [ ] **Adım 4: Test** — `office_user.create` taşımayan açamaz · taşıyan kapsamsız açar · MFA'sız backoffice kullanıcısı hiçbirini yapamaz · işlemler audit'lenir.
- [ ] **Adım 5–6:** yeşile al, kalite kapısı, commit.

---

## Görev 11: Kapanış

- [ ] **Adım 1: `AuthorizationSeeder`** — kapsamlı izinler veritabanına iner.
- [ ] **Adım 2: Ayar tohumları** — `auth.invitation_valid_hours = 24`, `instructor.max_authorised_institutions = 2`.
- [ ] **Adım 3: Wire-up doğrulaması (reverse-grep)** — `CreateOfficeUser` iki çağıranda · `ScopeResolver` `OfficeUsers` sorgusunda ve `AssignOfficePermissions`'ta · `OpenTask` iki akışta · `MyTasks` ekranda · davet rotası `route:list`'te.
- [ ] **Adım 4: Grup koşuları** — `omurga`, `omurga-rls`, `backoffice` ayrı ayrı.
- [ ] **Adım 5: TeamCity** yeşil → `EGT-59` `Fixed`.

---

## Kabul kriterleri

| # | Kriter |
|---|---|
| A-1 | Kullanıcı `/yonetim/kullanicilar/yeni`'den oluşturulur; ekranda **parola alanı ve yetki listesi yoktur** |
| A-2 | Hesap `invited` doğar, giriş yapamaz; **24 saat** geçerli davetle parolasını kendisi belirler |
| A-3 | Oluşan kullanıcının **etkin izin kümesi boştur** |
| A-4 | Ofis aktörü kapsamı dışındaki gruba kullanıcı açamaz |
| A-5 | `office_user.create` **taşımayan** backoffice kullanıcısı ofis kullanıcısı açamaz; taşıyan kapsamsız açar |
| A-6 | İlk girişte `user.permissions_pending` görevi kapsamdaki **her** yetkiliye düşer |
| A-7 | Kapsamda yetkili yoksa görev **backoffice'e** düşer ve oradan yetki verilebilir |
| A-8 | İzinsiz kullanıcı giriş yapar (`200`), yetki isteyen rotada `403` alır |
| A-9 | Yetki veren sahip olmadığı izni veremez; `group` taşıyan `all` veremez; kapsamı dışındakine veremez |
| A-9a | Backoffice **hem** alt-küme **hem** kapsam kuralından muaftır; kapı `office_user.assign_permissions` iznidir |
| A-10 | Yetki atanınca kardeş görevler **olayla** kapanır; kimse başkasının görevini elle tamamlayamaz |
| A-11 | `ScopeResolver` üç kapsamı AD-23'e göre çözer; kapsamsız isimde **istisna atar** (sınırsız dönmez) |
| A-11a | Seviye tavanı yalnız `.group` ekini daraltır: `staff` + `.group` → `own`; **`.all` her seviyede ve üyeliksiz de `all` kalır** |
| A-11b | `SessionScopeResolver` `customer.view.all` ile tüm müşterileri, taşımayanda **atama tabanlı** kapsamı döndürür; rename sonrası hiçbir kullanıcının kapsamı daralmaz |
| A-11c | Bu turda `customer.view.group` ve `customer.view.own` **üretilmez** |
| A-12 | Migration sonrası mevcut izin sahibi **aynı kümeyi** görür; çıplak isimle `can()` çalışmaya devam eder |
| A-13 | `group_manager` + `user.view.group` taşıyan kişi **listede** yalnız kapsamındakileri görür |
| A-13a | Aynı kişi kapsam dışındaki bir kullanıcının **detay/düzenleme/pasifleştirme** rotasında `403` alır |
| A-14 | Eğitmen dikeyinde kullanıcı açılınca eğitmen dosyası **aynı transaction'da** doğar; bakanlık ID'siz reddedilir |
| A-15 | Sınırı aşan yetkilendirilmiş kuruluş sayısı reddedilir; sınır **ayardan** okunur |
| A-15a | Eğitmen kaydı açılınca `instructor.ministry_notification` görevi açılır; süre **ayardan** gelir ve sistem bildirim **göndermez** |
| A-15b | Eğitmen kullanıcısı **pasifleştirilince** aynı görev `change: left` bağlamıyla açılır; eğitmen kaydı olmayan kullanıcıda açılmaz |
| A-16 | Eğitmen belgeleri `instructor_documents`'a yazılır; `customer_documents` bu akışta hiç kullanılmaz |
| A-16a | Kritik eğitmen belgesi ve sürümü **veritabanı seviyesinde** silinemez (trigger) |
| A-16b | Belge türü sorgusu `DocumentType::SCOPE_INSTRUCTOR` sabitini kullanır ve boş küme dönmez |
| A-17 | `uploadable_by = both` türde seçim çalışır; `owner` türde görev otomatik açılır; `operations` türde seçenek yoktur |
| A-18 | Eğitmen `/yonetim/gorevlerim`'de kendi görevini görür ve belgeyi oradan yükler; yükleme görevi kapatır |
| A-19 | Eksik zorunlu belgeli eğitmen aday listesinde görünmez |
| A-20 | Kullanıcı oluşturma, davet kabulü, yetki atama, belge yükleme, görev tamamlama **audit'lenir** |
| A-21 | Şifreli alanlar maskeli döner; açık okuma erişim logu yazar |
| A-22 | Grup silinemez (üyelik varsa); pasifleştirilir |
| A-23 | Kullanıcı yönetimi ofiste ve backoffice'te **aynı Action'ı** çağırır |

---

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

⚠ Onay kuralı 4'ün backbone istisnası kapsamında: aşağıdakiler bu turda **doğmaz**, ama nerede doğacağı yazılıdır.

| # | Ne | Kapanış adresi |
|---|---|---|
| 1 | `last_taught_on` türetimi | Planlamacı turu — eğitim tamamlanma akışı (Görev 7 Adım 3) |
| 2 | Eğitmen yetkinlik matrisi (CTU/fumigasyon kapısı) | Eğitim tanımı turu — `egitmen-spec` §11/2 |
| 3 | Kategori bazlı baz tarife (backoffice *Eğitmen ayarları*) | Eğitmen ücret/masraf turu — `egitmen-spec` §9 |
| 4 | **Eğitici belgesi süre-dolumu uyarısı** | ⚠ `certificate_valid_until` bu turda doğuyor ama süre yaklaşınca üretilecek **kademeli bildirim** (AD-10; çatı 90/60/45/30/15/7 deseni) bu turda **kurulmuyor**. Erteleme meşrudur — bildirim omurgası (zamanlanmış iş + kanal seçimi) bu turun kapsamı değil; ama adres yazılıdır: **süre takibi / kademeli bildirim turu**. ⛔ Uygunluk kapısının kendisi (`egitmen-spec` §3) bu bildirime **bağlı değildir**: süresi dolmuş eğitmen bildirim gitmese de aday listesinde görünmez |
| 5 | Eğitmen → planlamacı süreli havuz ataması (AD-23) | Planlamacı turu — `egitmen-spec` §6.3 aday listesi bunu tüketir |
| 6 | Müşteri → grup zorunlu ataması (AD-23) | Müşteri/satış turu |
| 7 | Onay ve eskalasyon omurgası (AD-23: dört göz, silsile, 1 iş günü) | ⚠ `egitmen-spec` §7.4 planlama onayı ve §7.8 masraf onayı bunu tüketiyor; omurga **onay turunda** kurulur. Bu turun `tasks` tablosu onun yerine geçmez — **görev bir hatırlatmadır, onay bir karardır** |
| 8 | Müşteri devri sihirbazı (AD-23, kullanıcı çıkışı) | Müşteri/satış turu |
| 9 | Ofis rol sözlüğü (yetki seti kısayolu) | ⚠ Bu turda **bilinçli olarak açılmıyor** (K-F): seviye kapsamı taşıyor, yetki havuzdan seçiliyor. Kısayol olarak istenirse ayrı kalem |
