# Mimari Kararlar (ADR)

*Son güncelleme: 24.07.2026. Her karar gerekçesiyle birlikte. Değişiklik gerekirse karar silinmez, "değiştirildi" notuyla güncellenir.*

## AD-01: Stack

**Karar:** Laravel 13 + PHP 8.5, PostgreSQL (mevcut standart yapı: pgbouncer, unaccent, TimescaleDB), Redis (Sentinel ile HA), Laravel Horizon.

**Gerekçe:** Proje CRUD/iş akışı-ağır, çok rollü, çok panelli B2B SaaS — Laravel'in olgun olduğu profil. Ekip yetkinliği PHP. Laravel 13 (Mart 2026) PHP 8.3–8.5 destekliyor. Redis ihtiyacı failover (Sentinel), sharding (Cluster) değil.

**Dikkat:** Proje kurulumunda ekosistem paketlerinin (Filament, spatie/laravel-permission, owen-it/laravel-auditing, tenancy) Laravel 13 uyumlu sürümleri pinlenip teyit edilecek.

## AD-02: Multi-tenancy modeli

**Karar:** Tek veritabanı, `tenant_id` kolonu. Tenant = müşteri firma. DB-per-tenant YOK.

**Gerekçe:** Müşteri sayısı yüzler mertebesinde; DB-per-tenant operasyonel yükü gereksiz. İzolasyon AD-03 ile iki katmanlı sağlanıyor.

## AD-03: İzolasyon — iki savunma hattı

**Karar:** (1) Uygulama katmanı: policy bazlı erişim + global scope'lar. (2) Veritabanı katmanı: **PostgreSQL Row Level Security (RLS)**. İzolasyon asla tek başına app-layer'a emanet edilmez.

**Kapsam:** eğitmen / planlamacı / satışçı / müşteri / katılımcı hiçbir şekilde birbirinin verisine erişemez. 3E personeli yetkisiz işlem yapamaz. Satışçı grupları müşteri bazlı scope'lanır (örn. "İzmit satış grubu" yalnız atandığı müşterileri görür).

## AD-04: pgbouncer + RLS uyumu (kritik tuzak)

**Karar:** Tenant context'i **her zaman transaction içinde `SET LOCAL`** ile set edilir; Laravel middleware'de tek noktadan uygulanır. Session-level `SET` YASAK.

**Gerekçe:** pgbouncer transaction pooling modunda session değişkenleri bağlantılar arasında karışır — A tenant'ının context'i B'nin sorgusuna sızabilir. `SET LOCAL` transaction sonunda öldüğü için pooling ile güvenli. Bu kural code review'da zorunlu kontrol maddesi.

## AD-05: Hassas veri şifrelemesi

**Karar:** Alan bazlı şifreleme (TCKN, kimlik, SGK verileri vb.) Laravel encrypted cast ile; müşteri ve katılımcı verisinde **tenant bazlı anahtar** altyapısı day-one tasarlanır (sonradan eklemek en pahalı kalem).

## AD-06: Audit log — bütünlük kanıtı *(değiştirildi: 26.07.2026)*

**Karar (güncel):** Append-only audit tablosu (TimescaleDB hypertable; retention policy Timescale'den). Bütünlük kanıtı **kendi kurduğumuz zincirdir**: batch başına **Merkle kökü** + batch'leri birbirine bağlayan **hash zinciri**; kök ve zincir hash'i periyodik olarak **dış tanığa** (public repository) yayımlanır. Log dosyaları S3-uyumlu depoda **Object Lock (WORM)** ile saklanır.

**Önceki karar:** batch hash'ine **KamuSM RFC 3161 zaman damgası**. **Değişiklik gerekçesi (Sets, 26.07.2026):** KamuSM veya benzeri bir ESHS'ye bağlanmıyoruz; damgayı küresel ölçekte geçerli olacak şekilde kendimiz kurguluyoruz. Bitcoin/OpenTimestamps türü bir anchor da kullanılmıyor — dış tanık olarak public repository yeterli görüldü.

**Ne kanıtlar:**

| Katman | Ne gösterir |
|---|---|
| Append-only trigger | Uygulama içinden yeniden yazma denemesi **hata verir**, sessizce yutulmaz |
| Merkle kökü | Batch'teki tek bir satır değişse kök değişir; tek satır, diğerlerini açmadan kanıtlanabilir (batch birden çok müşterinin kaydını taşır) |
| Hash zinciri | Bir satırın değişmesi sonraki **tüm** zincir hash'lerini bozar; tahrifat yerel kalamaz, mührü düzelterek de örtülemez |
| Dış tanık | Zincirin **ne zaman** var olduğunu gösterir — zincir tek başına bunu gösteremez, prensipte bugün baştan üretilmiş olabilir |
| Mühür tablosunun dondurulması | Satırları yeniden yazan birinin **yeniden mühürleyip** yerel zinciri temiz göstermesini engeller |
| WORM arşiv | Mühür kaydının uygulamanın da yeniden yazamayacağı bir kopyası |
| **Kurulum kimliği** | Hangi **sistemin** yazdığını söyler — kimliksiz bir mühür, veritabanı yeniden kurulduğunda önceki kurulumun mührüyle karışır *(16.08.2026, EGT-161)* |

### Kurulum kimliği — neden var *(16.08.2026, EGT-161)*

⛔ **Ölçülmüş arıza:** tanık deposu 26.07'de `from_id: 0 … 2` aralığını taşıyordu; 09.08 ve 10.08 dosyalarında **yine `from_id: 0`**'dan başlayan satırlar vardı. Veritabanı yeniden kurulmuştu — id'ler 1'den başlar — ve tanık, doğru biçimde, hiçbir şeyi unutmamıştı. Günlük doğrulama "aynı" aralık için iki farklı kök buldu ve **her gün** zinciri kırık ilan etti. Tek belirtisi log dosyasında bir satırdı.

⛔ **Tanık deposunu boşaltmak çözüm DEĞİLDİR** ve bu bilinçli bir karardır. İşimize gelmediğinde temizlediğimiz bir depo, temizlemediğimiz günler hakkında da hiçbir şey kanıtlamaz; üstelik silinen şey, önceki dönemin bağımsız kanıtıdır. Eksik olan, "bu satırı hangi sistem yazdı" sorusunun cevabıydı.

**Nasıl çalışır:** her kurulum, kendi migration'ında üretilen kısa bir kimlik alır (`installation` tablosu, tek satır). Mühür bu kimliği taşır; tanık deposunda satırlar `roots/<kimlik>/<tablo>/<gün>.jsonl` altında durur; doğrulama yalnız **kendi kurulumunun** satırlarına bakar. Mühür tablosundaki teklik kısıtı da kimliği içerir — `(kurulum, tablo, from_id)`.

⛔ Kimlik **veritabanında** durur, `.env`'de değil. Amacın tamamı, veritabanının yeniden kurulmasının YENİ bir kimlik üretmesidir; `.env` yeniden kurulumdan etkilenmediği için oraya yazılan bir değer, tam da tespit etmesi gereken olayı atlatırdı.

⚠ Yeniden kurulumdan sonra eski kurulumun satırları tanık deposunda **yerinde kalır** ve kimseyi rahatsız etmez. Geçiş için `php artisan audit:adopt-witness` vardır: mevcut veritabanının mühürlerine karşılık gelen eski satırları yeni klasöre kopyalar, hiçbir şeyi silmez.

⚠ Ayrı bir arıza — eşzamanlı iki yazıcının birbirinin commit'ini `reset --hard` ile silmesi — EGT-104'te kilitle kapatıldı; son başarısız mühür 11.08.2026 10:00'dur.

**Her yeni zincir halkası, oluşturulduğu anda yayımlanır (Sets, 26.07.2026).** Sıra: hesapla → yayımla → kaydet. Yayımlanmamış bir halka hâlâ bizim tarafımızda ve yeniden yazılabilir; yerel oluşturma zamanı ile tanığın kayıt zamanı arasındaki fark, bu yapıyı inceleyen herkesin soracağı ilk şeydir. Bu yüzden fark yoktur: ya ikisi birden oluşur ya hiçbiri. Sıra güvenlidir çünkü mühür deterministiktir — başarısız bir adımdan sonraki koşum birebir aynı halkayı üretir ve tanık onu zaten yayımlanmış olarak tanır.

**Dış tanığın çalışması üç şarta bağlıdır** (üçü olmazsa tanık değil yedek olur): repository **public**; `main` üzerinde **force-push ve silme kapalı**; periyodik **doğrulama işi** yerel zinciri yayımlananla karşılaştırır. Tanıklık değeri commit'in kendi tarihinden değil, **tanığın push'u ne zaman aldığını kendi tarafında kaydetmesinden** gelir.

**⚠ Delil sınıfı değişti — bilerek.** Bu kurgu **takdiri delildir**, kesin delil değil. 5070 sayılı kanunun güvenli elektronik imzaya tanıdığı statü BTK'ya kayıtlı bir ESHS'den gelir; kendi zincirimiz onu almaz. Bilirkişi zinciri yeniden hesaplayıp dış tanıkla karşılaştırarak doğrulayabilir ve kanıt gücü ciddidir, ancak itiraz halinde ispat yükü bizde kalır. **Hukuk teyidi açık kalemdir** (`docs/issues.md`); mevzuat sertifika/bildirim süreçlerinde nitelikli damga zorunlu kılarsa ESHS'ye veya bir kamu anchor'ına dönülür — veri modeli buna hazırdır, sabitlenen şey zaten mevcut zincir hash'idir.

**Bağımsız doğrulanabilirlik pazarlıksızdır.** Yalnız bizim kodumuzun yeniden hesaplayabildiği bir zincir delil değildir: yaprak sırası, hash fonksiyonu ve katlama kuralı yazılı olur, dışarıdan çalıştırılabilir bir doğrulama aracı ve prosedürü bulunur.

**Bunun bedeli: kural adlandırılır ve halkada taşınır.** Mührün `hash_algorithm` alanı hash fonksiyonunun değil **kuralın** adını tutar (bugün `sha256-utc-iso-v1`) — yaprağın nasıl yazıldığı dahil. Gerekçe 26.07.2026'da somutlaştı: yaprak, zaman damgasını oturumun `TimeZone`/`DateStyle` ayarına göre yazıyordu; aynı satır üç ayarda üç farklı yaprak üretti (canlı doğrulandı) ve dışarıdan doğrulayan bilirkişi zinciri **bozuk görürdü**. Kural UTC + sabit kalıba çekilerek düzeltildi. Buradaki ders kusurdan büyük: kural düzeltmesi, düzeltmeden önce mühürlenmiş her halkayı sessizce geçersizleştirir. Halka hangi kural altında üretildiğini kendi üzerinde söylemedikçe, zincirin ömrü kurallarının ömrüyle sınırlıdır.

**Kabul edilmiş artık risk — tek satırlık parti.** Yayımlanan Merkle kökü, partide **tek** satır varsa o satırın digest'idir. İçeriğin çoğunu bilen biri kalan küçük uzaylı bir alanı tahmin edip kökle karşılaştırarak doğrulayabilir. Bu, kök yayımlamanın doğasında vardır (Certificate Transparency dahil tüm şeffaflık günlükleri aynı özelliği taşır) ve tuzlamayla kapatılamaz: tuz yayımlanmazsa bağımsız doğrulama biter, yayımlanırsa koruma biter. Azaltıcı etken mühürlemenin **aralıkla** çalışması — partiler tipik olarak çok satırlıdır. Kabul ediliyor ve chunk2 §2.6 G-2 buraya atıf yapıyor; audit satırlarının kendisi kişisel veriyi zaten maskeli tutar (AD-16), yani en kötü durumda sızan şey maskelenmiş bir alanın tahmin doğrulamasıdır.

**Maliyet notu:** zincir tamamen kendi tarafımızda ve ek altyapı istemez — batch başına bir SHA-256 kümesi ve `audit_stamps`'te bir satır. Dış tanık ücretsiz. Anahtar yönetimi yoktur: zincirin kendisi kanıt yapısıdır, imza eklemek "biz öyle diyoruz"dan fazlasını katmaz.

## AD-07: Kimlik doğrulama

**Karar:** İlk günden çoklu yöntem: şifre, passcode, 2FA, magic link. Hangi kullanıcı türünün hangi yöntemleri kullanabileceği **admin panelden ayarlanır**. Finansal yetkili roller (teklif onayı, e-fatura) için MFA zorunlu.

**Not:** Magic link tek başına yoklama/katılım kanıtı olarak KULLANILAMAZ (paylaşılabilir) — yoklama kanıtı imzalı liste + eğitmen onayı (bkz. çatı dokümanı §13).

## AD-08: Hiçbir iş kuralı hardcoded değil

**Karar:** Eğitim kapsama kuralları ("bunu alan şunu almış sayılır"), asgari katılımcı sayıları, indirim limitleri (global + satışçı-özel override), masraf kuralları/limitleri, eğitmen ücret tarifeleri (global → eğitmen-özel → işlem-bazlı override) — tamamı admin panelden tanımlanabilir parametrik yapılar.

**Gerekçe:** Sistemi biz değil 3E besleyecek; mevzuat değişince kod değil tanım değişecek.

## AD-09: Paneller, ofis ve backoffice *(değiştirildi: 24.07.2026 · ofis/backoffice tanımı: 26.07.2026)*

**Karar (güncel):** 6 panel. Yalnız **backoffice Filament**'tir. Diğer beş panelin tamamı — **planlamacı, satışçı, müşteri, eğitmen, katılımcı** — özel tasarımdan beslenen **Inertia + Vue**, uçtan uca responsive/mobile-ready, **tek mode** (dark/light yok; tam beyaz olmayan, göz yormayan sakin palet — bkz. `docs/design/tasarim-brief.md`). Mobil uygulama şimdilik yok; gerekirse sonra.

**Önceki karar:** planlamacı + satışçı da Filament'teydi. **Değişiklik gerekçesi (Sets, 24.07.2026):** kullanıcı tarafındaki deneyim ürünün kendisi; planlamacı/satışçı ekranları da (dashboard, sorgulama, teklif) profesyonel özel tasarımla yapılacak.

### Ofis ≠ Backoffice *(Sets, 26.07.2026)*

Bu ayrım tekrar tekrar karıştırıldığı için tanım burada sabitlenir.

| | Ne yönetir | Host | Teknoloji |
|---|---|---|---|
| **Ofis** | **İşin yürütülmesi** — eğitim planlama, satış/teklif, eğitmen yönetimi, bakanlık bildirimi, müşteri operasyonu, mali takip | `ofis.*` | Inertia + Vue |
| **Backoffice** | **Sistemin çekirdeğinin yönetilmesi** — panelden seçilecek ya da kullanılacak her şeyin tanımı: eğitim türleri, mevzuatlar, sözleşmeler, evrak türleri, seçim listesi içerikleri, diller, bildirim şablonları, global parametreler | `backoffice.*` | Filament |

**Değişmez kural: backoffice üretir, ofis tüketir.** Ofis tarafında backoffice'ten tanımlanmamış bir eğitim açılamaz, tanımlanmamış bir mevzuat kullanılamaz, tanımlanmamış bir evrak türü seçilemez veya yüklenemez. Tanım tablolarına **yazma** yetkisi backoffice iznine bağlıdır; ofis policy'si bu tablolara yalnız **okuma** verir. Bu bir üslup tercihi değil, testle denetlenen bir sınırdır.

**Global parametre, dar ayar — genişletme onaya gider** *(revize: Sets, 28.07.2026)*. Global değer backoffice'te tanımlanır; **daraltma** ofiste (ve backoffice'te) doğrudan yapılabilir. Örnek: global azami indirim %20 iken satış yöneticisi bir satışçıyı %10 ile sınırlayabilir.

⚠ **Genişletme engellenmez, onay akışına düşer.** Önceki hâli *"genişletme denemesi hata verir"* diyordu; kaldırıldı. Sets'in gerekçesi: *"patronla konuştu, patron dedi ki tamam bizim maks. indirim %25 ama siz bu müşteriye %30 yapın. Yapamayacaklar mı? İş sürecini kilitleyecek bir hamle yapmayız, onaya tabi olur."* Sert kapı iki şey üretiyordu: gerçek hayatta verilmiş kararı sistemin dışına itmek (telefon, e-posta, kayıtsız) ve istisnanın hiçbir yerde iz bırakmaması. Onaya bağlamak istisnayı hem mümkün hem kayıtlı kılar. Onay mekanizması AD-23'ünkidir.

⚠ Bu revizyon **yalnız iş parametrelerinin genişletilmesinin sonucunu** değiştirir (hata → onay). *"İş parametresi"* sayısal olmak zorunda değildir: bir eşik (azami indirim, onay tutarı) kadar seçenekli bir politika değeri de (planlamacının onay kuralını `always`'ten `never`'a çekmesi) bu kapsamdadır. Sınır **iş parametresi ↔ yetki/tenant sınırı** ayrımıdır, sayı ↔ seçenek ayrımı değil. *"Backoffice üretir, ofis tüketir"*, tanım tablolarına yazmanın backoffice'e ait olması ve **tenant kapsamındaki** daraltma/genişletme semantiği (AD-02) aynen durur — tenant sınırı bir yetki sınırıdır, iş parametresi değil, ve onaya açılmaz.

⚠ **Bugünkü kod bu kuralı karşılamıyor:** `app/Support/Business/DiscountCeiling.php` genişletmeyi sessizce kabul ediyor (ne hata veriyor ne onaya gönderiyor). Kapanış adresi **EGT-58**.

**Mali sınır — güvenlik değil kapsam kararı.** Backoffice mali **tanım ve parametreyi** yapar (fiyat, e-fatura ayarları, indirim oranları); mali **durumu görüntülemez** — *"mali tablolarımız nasıl"* ofisin sorusudur, backoffice'in değil. Gerekçe yetki değildir (backoffice yetkisi zaten geniştir), **kapsam kaymasıdır**: backoffice'e operasyon ekranı girmeye başlarsa "şu da olsun, bu da olsun" gelir ve sonunda **iki ofis yazıp iki ofis maintain ederiz**. Backoffice'in yapabilirliği geniştir; kapsamı bilinçli olarak dardır.

**Backoffice'in kendi yetki hiyerarşisi vardır.** *(v1.11'de düzeltildi: bu paragraf eskiden `system.access_backoffice` iznini "kapı" olarak tarif ediyordu — o, backoffice'in ofis kimliği üzerinde bir izin olduğu **eski modelin** cümlesiydi ve aşağıdaki "ayrı kimlik alanı" hükmüyle aynı anda doğru olamazdı; heyet 141 F-5.)* **Kapı artık bir ofis izni değil, backoffice hesabının kendisidir:** `backoffice_users` tablosunda kaydı olmayan hiç kimse backoffice'e giremez. İçeride ayrı izin ailesi yaşar: süper admin her şeye erişir; ileride eklenecek roller (ör. destek görevlisi) yalnız kendi alanına erişir, loglara / kullanıcı yönetimine / core parametrelere erişmez. *"Backoffice'e giren her kullanıcı en geniş yetkiye sahiptir"* **yanlıştır** ve bulgu konusudur.

**Backoffice AYRI BİR KİMLİK ALANIDIR** *(Sets, 26.07.2026 — spec v1.11)*. `backoffice_users` kendi oturum, auth ayarı ve **kendi izin ailesiyle** yaşar; `users` ile hiçbir tabloyu paylaşmaz.

Gerekçe güvenlik değil **karmaşıklığın adresi**: ofis çok net bir yapıdır ve öyle kalmalıdır; backoffice ise ihtiyaca göre şekillenecek, farklı ve karmaşık yetki yapıları gerektirecektir. İleride kurulacak destek yapısı ofis paneline **hiçbir şekilde** girmeyecek ama işini yapmak için müşteri verisinin bir kısmına ticket bazında erişmek zorunda kalacaktır. Bunu normal `users` üzerinde **istisna** olarak tanımlamak ofisin standardını bozar; backoffice'in **normal rutini** olarak tanımlamak ofisi sade ve denetlenebilir bırakır. *(Önceki karar: tek `users` + `system.access_backoffice` izni. Değişiklik gerekçesi budur.)*

**Backoffice'in yetki tavanı yoktur** *(Sets, 26.07.2026 — karışıklık üçüncü kez çıktığı için burada kesin dille sabitlenir).* Backoffice gerektiğinde müşteri verisine erişir, eğitim verisine erişip eğitim oluşturur, ofis hesaplarını (operasyon direktörü dahil) açar ve yetkilerini yönetir. Ayrımın **sebebi** budur: sistemin çekirdeğine her türlü yetkiyle erişilebilen yer burasıdır. Bunun kapatılmayan karşılığı **denetimdir, kısıt değil** — backoffice'in tenant verisine her dokunuşu sert biçimde loglanır; log yazılamıyorsa erişim de olmaz.

Yukarıdaki "mali sınır" ve benzeri sınırlar **kapsam tercihidir, yetki sınırı değildir** ve *"şu an için"* geçerlidir; değiştirmek ekran kararıdır, yetki modeli değişikliği değil. Ayrıntı spec §3.3 (tavan) ve §3.4 (bugünkü kapsam).

**Kapılı erişim — dar yetkili backoffice rollerinin erişim biçimi.** Backoffice'in kendi hiyerarşisinde süper admin dışında kalan roller (ör. ileride kurulacak destek görevlisi) müşteri verisine ancak dört şartı **birden** taşıyarak erişir: gerekçeye bağlı (ticket) · gerekçenin işaret ettiği tenant ve kayıtla sınırlı · süreli · sert biçimde loglanmış. Ayrıntı spec §3.3.1. ⚠ Bu, **süper admin için bir tavan değildir**; rol tanımıdır. Ofis kimliğine ise genişletilemez — ofise "ticket bazlı geçici erişim" eklemek, ayrımın önlemek için yapıldığı şeyin ta kendisidir. Destek yapısının kendisi ayrı bir spec'in konusudur.

**Tehdit varsayımı:** backoffice **süper admini** pratikte şirket sahibidir — fiyatı, mevzuatı ve sistemi belirleyen kişi. Kullanıcı yönetiminin backoffice'te sınırsız olması bu varsayıma dayanır. ⚠ Varsayım **yalnız süper admin rolü** içindir; backoffice'e eklenecek diğer roller (destek görevlisi gibi) bu varsayımı taşımaz ve kullanıcı yönetimine erişmez — backoffice'in kendi yetki hiyerarşisi tam olarak bunun içindir.

**Host ayrımı güvenlik sınırı değildir** — yetki her zaman policy'den gelir.

## AD-10: Kuyruk ve zamanlanmış işler

**Karar:** Redis + Horizon. Süre takibi taramaları (sertifika/belge geçerliliği, 3 ay kuralı, kademeli bildirimler 90/60/45/30/15/7 gün), bildirim gönderimi, video yükleme, damgalama işleri kuyrukta.

## AD-11: Dosya/evrak depolama

**Karar:** S3-uyumlu object storage. Her evrak SHA-256 hash ile sürümlü; kritik kayıtlar (sertifika, imzalı yoklama, onaylı teklif snapshot'ı) silinemez — iptal/revizyon ile yönetilir.

## AD-12: e-Fatura

**Karar:** Provider-bağımsız adaptör mimarisi day-one hazır; entegratör belli olunca provider eklenir. Fatura hem serbest kalemle hem "teklifi faturalandır" ile oluşturulabilir; bağımsız katılımcıya veya firmaya kesilebilir.

## AD-13: Eğitim kayıtları (video) *(güncellendi: 25.07.2026)*

**Bağlam:** Eğitimler **fiziksel olarak, yüz yüze** veriliyor. Bakanlık kayıt zorunluluğu getirdiği için oturumlar **Zoom ile kaydediliyor**. Kaynak dolayısıyla Zoom'dur — sistemin kendi kayıt altyapısı yoktur.

**Karar:** Zoom kaydı indirilip **unlisted (liste dışı) YouTube hesabına** arşiv amaçlı yüklenir; YouTube burada yayın platformu değil, **depolama** olarak kullanılır. Bakanlık paylaşılabilir link istediği için unlisted seçildi — private link açılamıyor.

- **Kayıt hiçbir panelde gösterilmez** — ne müşteri, ne katılımcı, ne eğitmen. Link yalnızca İdare ile paylaşılır. (Tasarım handoff'undaki kural doğrudur; çatı §8 ve tasarım brief'indeki "müşteri panelinde video linki" ifadesi bu karara göre düzeltildi.)
- MVP'de kayıt manuel yüklenir. **Zoom Cloud Recording API otomasyonu backlog'dadır** (EGT-41); yapılırsa Zoom API contract'ı resmî dokümandan doğrulanacak (varsayım yasağı).
- Katılımcı görüntüsü/sesi kayıtta olduğundan katılımcı onboarding'inde KVKK aydınlatma + açık rıza adımı zorunlu; rıza kaydı sistemde tutulur.

**Önceki karar:** "Videolar sistemde saklanır + YouTube'a otomatik yüklenir." **Değişiklik gerekçesi:** kaynağın Zoom olduğu ve eğitimlerin yüz yüze verildiği netleşti (Sets, 25.07.2026); ayrıca videonun panellerde gösterilmemesi kesinleşti.

## AD-14: Teklif onayında delil

**Karar:** Müşteri teklifi panelden onaylar; onay anında teklif metni sürümünün hash'i + zaman damgası + onaylayan kullanıcı kaydedilir ("o metni onaylamadım" itirazına karşı).

## AD-15: Bakanlık entegrasyonu *(güncellendi: 25.07.2026 — tek model değil)*

**Karar:** Resmî API yok — otomatik gönderim yapılmaz. Kullanıcı e-Devlet/KUMS'a yönlendirilir, işlem kanıtı (bakanlık eğitim ID'si) sisteme girilir.

**Kritik: iki farklı model var, statü makinesi tek olamaz** (mevzuat taraması 25.07.2026, detay `docs/mevzuat-kurallari.md` §10):

| Model | Kural | Statü davranışı |
|---|---|---|
| **Bildirim** — IMDG Kod ve tehlikeli yük deniz gözetim eğitimleri | Eğitim başlama tarihinden en az **3 gün önce** bildirilir (Y-2016 md.13/g; YÖN-2019 md.6/3) | Bakanlık ID'si girilince statü ilerler; onay beklenmez |
| **Onay** — genel deniz gözetim (DGY) eğitimleri | Program içeriği, yeri, tarihi ve eğitici bilgileri en az **15 gün önce** bildirilir; **İdare onaylamadan eğitim verilemez**, onaysız eğitim **geçersiz sayılır** (DG-2020 md.14/8) | Onay kanıtı olmadan eğitim gerçekleştirilemez — gerçek kapı |

Bildirim tipi ve süresi **eğitim tanımının alanıdır**; mevzuat kaydı bunu bir **uyarı** olarak hatırlatır (AD-27, 27.07.2026 revizyonu). Statü makinesi eğitim tanımındaki bu alana göre davranır.

**Diğer kurallar:**
- Katılımcı listesi bildirildikten sonra yapılan her değişiklik **"KUMS güncellemesi gerekli"** bayrağı üretir; güncelleme kaydedilmeden eğitim "Devam Ediyor" statüsüne geçemez. (Yedek listeden terfi bu durumu sık üretecek.)
- Azami katılımcı mevzuattan gelir ve **hard block**'tur: IMDG 30 (Y-2016 md.13/ç), TY deniz gözetim 25 (TY-2019 md.17/1).
- Asgari katılımcı parametrik (başlangıç 3) — **hiçbir mevzuat metninde bulunamadı**, operasyon/bakanlık teyidi bekliyor.
- Terminoloji: Y-2016 "e-Devlet UDHB Eğitim Kurumları Hizmetleri", YÖN-2019 "KUMS" diyor. Sistemde KUMS esas; güncel servis adı operasyondan teyit edilecek.

## AD-16: Anonimleştirme/maskeleme katmanı

**Karar:** İki zorunlu uygulama: (1) **Audit log maskeleme** — hassas değerler (TCKN, telefon, e-posta, IBAN) audit'e yazılırken maskelenir; ham değer yalnız şifreli ana kayıtta (AD-05) durur. (2) **Dump anonimleştirme** — prod verisi ham haliyle prod dışına asla çıkmaz; staging/local-prod/dev kopyaları PostgreSQL Anonymizer (`anon` extension) ile maskelenmiş dump'tan beslenir. Pseudonym katmanı (raporlama) şimdilik yok, gerekirse eklenir.

**Gerekçe:** (1) için: AD-06 audit'i append-only + damgalı = silinemez; içine ham PII yazılırsa KVKK silme/anonimleştirme hakkıyla çelişir — maskeleme yazım anında yapılmak zorunda. (2) için: PII sızıntısının tipik yolu prod değil, ham prod kopyası taşınan test ortamlarıdır.

**Sınır:** Bu katman RLS (AD-03) ve şifrelemenin (AD-05) alternatifi değil — onlar prod runtime erişimini, bu katman logları ve prod-dışı kopyaları korur.

## AD-17: Mailing altyapısı — Postmark

**Karar:** Mail provider **Postmark**; Laravel'in native Postmark transport'u kullanılır (ek adaptör katmanı gereksiz — mailer swap Laravel config seviyesinde zaten mümkün). Bu projeye **ayrı Postmark server + kendi gönderim domain'i** açılır (puanbenim hesabıyla reputation/izolasyon ayrımı). DKIM/SPF/Return-Path DNS kayıtları domain işiyle birlikte kurulur.

**İki mesaj sınıfı — ayrı stream zorunlu:** (1) Transactional (hatırlatma, teklif/onay bildirimi, magic link, süre uyarıları) → transactional stream. (2) Promosyonel ("yeni eğitim açıldı, katılın" duyuruları) → **Broadcast stream**. Karıştırmak yasak — pazarlama şikayeti transactional deliverability'yi yakar. Bounce + spam-complaint webhook'ları day-one bağlanır → otomatik suppression listesi.

**İYS/6563 uyumu (karar verildi, 24.07.2026):** Promosyonel her gönderim alıcının **ticari ileti onayı** kontrolünden geçer; onay/ret kayıtları sistemde tutulur (tarih + kaynak + kanıt). Onaysız alıcıya promosyonel mail gönderilemez (hard gate). **İYS entegrasyonu day-one, İletiMerkezi'nin İYS paketi üzerinden** — onay alınır, İYS bildirimi İletiMerkezi API'siyle yapılır (SMS ile tek entegrasyon noktası, bkz. AD-18). Hatırlatma/hizmet mailleri bu kapsamda değildir. İletiMerkezi İYS API field/endpoint'leri implementasyonda resmî dokümandan doğrulanır (varsayım yasağı kural 3).

**Şablonlar:** Mail şablonları admin-tanımlı/düzenlenebilir (AD-08 ruhu); Postmark template'leri değil, uygulama içi şablonlar kullanılır (tek kaynak, sürümlenebilir, audit'lenebilir).

## AD-18: SMS altyapısı — İletiMerkezi

**Karar:** SMS provider **İletiMerkezi** (mevcut çalışılan sağlayıcı). Kapsam bilinçli olarak dar: **yalnız auth/işlem SMS'leri** (OTP/2FA, magic link passcode, kritik işlem doğrulaması) — transactional sınıf, İYS kapsamı dışında. Promosyonel SMS gönderilmez; ileride gerekirse İYS onay kontrolü (AD-17 ile aynı hard gate) eklenerek açılır.

**Gerekçe:** İletiMerkezi aynı zamanda İYS paketi satıyor → SMS + mail tarafının İYS bildirimi tek entegrasyonla çözülür (AD-17). API contract'ı implementasyonda resmî dokümandan doğrulanır.

## AD-19: Public referans kodları

**Karar:** Destek/iletişimde kullanılacak kayıtlara sistem otomatik, **rastgele** public referans kodu üretir. Format: `PREFIX-YYYY-XXXX` (örn. `EG-2026-7K4F`). Kurallar:

- **Ardışık numara YASAK** — sıra numarası iş hacmini sızdırır (teklif/eğitim adedi ticari bilgidir). Tasarımdaki `EGT-YYYY-NNNN` (sıralı) formatı bu karara göre düzeltildi.
- Rastgele kısım: 4 karakter, büyük harf, karışan karakterler (0/O/1/I/L) ve sesli harfler hariç. Bugünkü alfabe 28 karakterdir (`config/reference_codes.php`) → **yıl başına ~615K** kapasite. ⚠ Kapasite *tür başına değil, yıl başına ortaktır* — aşağıdaki benzersizlik kuralının sonucu. Çakışmada unique index + yeniden üretim.
- Prefix'ler 2 harf: `EG` (eğitim), `TK` (teklif); mekanizma generic — destek konusu olabilecek her yeni kayıt tipi (fatura, masraf vb.) aynı formatla eklenir (Sets onayı, 24.07.2026). Prefix'ler kodda tek bir merkezi kayıtta (enum/registry) tanımlanır — dağınık string tanımıyla prefix çakışması yaşanmaz. **3 harfli prefix kullanılmaz** — YouTrack ticket prefix'leriyle (EGT-123) karışır.
- Kod atandıktan sonra **değişmez ve yeniden kullanılmaz**; iptal edilen kaydın kodu boşa çıkmaz.
- **Tek istisna — planlamanın eğitime dönüşmesi** *(Sets, 28.07.2026)*: planlama aşaması kendi kodunu taşır (`PL-2026-7K4F`); plan onaylanıp eğitim oluştuğunda **rastgele son ek korunur, ön ek `EG` olur** (`EG-2026-7K4F`). Böylece bir eğitimin planlama geçmişi kod bakılarak bulunur. ⛔ **Eski kod ölmez, yönlendirir:** `pl-2026-7k4f@` adresine onay sonrası gelen bir acente cevabı aynı kayda düşer. Kuralın amacı zaten dışarıdaki bir tarafın elindeki kodun geçersizleşmemesidir; ön ek değişimi bunu bozmadığı sürece istisna güvenlidir. `PL` **rezerve edilir** — başka hiçbir kayıt tipine verilmez.
- **Benzersizlik ön ekten bağımsızdır** *(PL→EG istisnasının zorunlu sonucu)*: rastgele son ek `(yıl, son ek)` uzayında tekilleşir, `(ön ek, yıl, son ek)` uzayında değil. Gerekçe: dönüşümde son ek **korunmak zorundadır**, yeniden üretilemez — dolayısıyla `PL-2026-7K4F` üretildiği anda `EG-2026-7K4F` de fiilen rezerve olmuş olmalıdır. Aksi hâlde bağımsız üretilen bir eğitim kodu, sonradan onaylanacak bir planın hedef kodunu kapatır ve dönüşüm çakışır — çakışmanın tek çözümü kodu değiştirmek olurdu, ki kural tam olarak onu yasaklıyor. Bu kural, plandan doğmayan eğitimlerin bağımsız kod üretimini de sürdürülebilir kılar: iki yol aynı havuzdan çeker.
- **Program üyeliği koda girer** *(Sets, 31.07.2026 revizyonu)*: bir programın (paket) kodu `PR-2026-7K4F`; **o programdaki eğitimler programın son ekini + sırasını taşır** → `EG-2026-7K4F-01`, `EG-2026-7K4F-02`. Programa bağlı olmayan eğitim düz biçimi korur (`EG-2026-9M2X`). Gerekçe: eskiden `PK-2026-7K4F` ile `EG-2026-9M2X` arasında **görünür hiçbir bağ yoktu** — kodu eline alan "bu hangi haftanın eğitimi" diyemiyordu, ki bir eğitim kodu hakkında sorulan ilk soru budur.
  - ⚠ **Son ek rastgele kalır; yalnız sıra sıralıdır.** Sıralı bir sıra numarası iş hacmi sızdırmaz ve yürünemez: kardeşi bulmak yine programın rastgele son ekini gerektirir.
  - ⚠ **Ön ek `PK` → `PR`** *(Sets)*: herkesin kullandığı kelime "program"; kimsenin söylemediği bir kelimenin kısaltması, kodu sesli okurken çevrilecek fazladan bir şeydir.
  - ⛔ **Son ek "yıldan sonrası"dır, "son tireden sonrası" değil.** `EG-2026-7K4F-01`'de ikincisi `01` verir ve `(yıl, son ek)` tekil olduğu için **her programın birinci eğitimi birbiriyle çakışırdı** (`ReferenceCode::suffixOf`).
  - ⚠ **Eğitim taşıma diye bir şey yoktur** *(Sets)*: programdan çıkarılan eğitim çıkarılmıştır; başka programa konan eğitim yeni ve bağımsız bir kayıttır. Bu yüzden kodun program üyeliğini taşıması yalan söyleyemez.
- **Kod ≠ yetki:** kod yalnız arama referansıdır; kodla erişim her zaman policy + tenant kontrolünden geçer (IDOR koruması). Kod tahmin edilebilirliği bir güvenlik varsayımı DEĞİLDİR.
- UI: monospace, tek tık kopyalanır chip (tasarımcının deseni); tüm panellerde detay başlığında ve listelerde görünür; global arama koda göre bulur.

## AD-20: Kayıt yazışmaları — Postmark inbound ile içeride

**Revizyon (Sets, 28.07.2026) — yazışma teklife değil, kayda bağlanır.** Bu karar yalnız **teklif** yazışmasını tanımlıyordu. Planlamacı otel/acente/araç tedarikçileriyle yazıştığında ortada teklif yoktur; planlanan **eğitim** vardır (çatı §11: "teklif alınıp eğitime işlenir"). Kapsam genişletilir: yazışma bir **kayda** bağlanır, kayıt türü **eğitim** veya **teklif** olur; her kayıt türü kendi referans kodu ön ekini ve kendi inbound adresini taşır.

⚠ Bu bir *erişim* ayrımı değildir. Planlama süreci, maliyet ve satış — hepsi aynı eğitimin verisidir; kimin ne gördüğü policy katmanının işidir (çatı §178/1), veriyi başka tabloya taşımanın değil. Kayıt türü ayrımının tek sebebi **hangi konuşma olduğudur**: açık eğitimde tek eğitime N müşterinin N ayrı teklifi bağlıdır (çatı §5), ve gelen bir cevabın hangi teklife ait olduğu "eğitime ait" denerek belirlenemez. Detay: `docs/specs/egitmen-spec.md` §8.

**Karar:** Teklif mailleri sistemden gönderilir; cevaplar **Postmark inbound processing** ile içeri alınıp teklifin yazışma akışına bağlanır. Kurgu: inbound subdomain **`teklif.3egrp.com`** (MX → inbound.postmarkapp.com; catch-all — adresler önceden tanımlanmaz), Reply-To adresi referans kodunu taşır (`tk-2026-xxxx@teklif.3egrp.com`), webhook JSON'ı `To` local-part'ındaki koddan teklifi bulur (fallback: In-Reply-To/References eşlemesi), ekler S3'e hash'li kaydedilir (AD-11), satışçıya bildirim düşer. Satışçı cevabı panelden yazar → thread uçtan uca sistemde. Kod eşleşmeyen mail → triyaj kuyruğu; auto-reply/OOO filtrelenir; SpamStatus eşiği üstü karantina.

**Genişleme deseni:** her inbound kanal kendi subdomain'ini alır — ileride destek süreci `destek.3egrp.com`, başka kanal başka subdomain. Postmark kısıtı gereği (server başına 1 inbound stream, stream başına 1 domain) **her yeni subdomain = yeni Postmark server + ayrı webhook** olarak planlanır; mevcut stream'e eklenmez. Kök domain (3egrp.com) MX'ine ASLA dokunulmaz — şirket postası ayrı yaşar.

**Güvenlik sınırları (pazarlıksız):**
1. **Gelen mail = doğrulanmamış içerik.** Gönderen spoof edilebilir; mail cevabı hiçbir iş aksiyonunu otomatik tetiklemez. Müşterinin maildeki "onaylıyorum"u teklif onayı DEĞİLDİR — onay yalnız panelden (AD-14). Mail sadece yazışma kaydıdır.
2. Webhook endpoint'i secret/imza ile korunur; payload doğrulanmadan işlenmez.
3. Mail gövde/ekleri kişisel veri içerebilir → retention ve erişim politikası teklif spec'inde tanımlanır; erişim audit'lenir (AD-06).

**Bilinen sınır:** satışçı sistem dışından (kendi posta kutusundan) giden mail atarsa sistem göremez — teknik değil süreç kuralı: teklif yazışması sistemden yürütülür. DNS tarafı: inbound domain MX kaydı, Tayfun ile yapılacak DNS işine dahildir.

## AD-21: Audit — okuma dahil uçtan uca izleme

**Karar:** AD-06 yalnız **yazma** olaylarını kapsıyordu. Kapsam **görüntüleme/erişime** genişletilir; ancak hacim ve garanti farkı nedeniyle üç log sınıfı tanımlanır.

**Önce bir ayrım: aksiyon log değildir *(Sets, 26.07.2026)*.** Müşterinin teklifi onaylaması, revize talep etmesi, mesaj göndermesi; katılımcının eğitime katılması, notu, check-in'i — bunlar **iş verisidir**, denetim kaydı değil. İşin kendi kaydında yaşarlar ve yetkisi olan herkes bunları normal ekranlarda görür. Aşağıdaki üç sınıf **bunların üzerine** yazılan denetim katmanıdır.

Bunun pazarlıksız tasarım sonucu: **iş akışının ekrandaki zaman çizelgesi audit tablosundan beslenmez.** Teklifin geçmişi teklifin kendi verisinden gelir. Aksi halde denetim katmanının izolasyon, maskeleme ve saklama kuralları arayüze sızar; audit hem asıl işini yapamaz hem de UI'ın bağımlılığı olduğu için değiştirilemez hale gelir.

| Sınıf | Tablo | Kapsam | Damga | Saklama |
|---|---|---|---|---|
| **İş olayı audit'i** | `audit_writes` | Kullanıcının sisteme girdileri: giriş/çıkış, oluşturma, düzenleme, durum geçişi, onay, gönderim | Kendi zincirimiz: Merkle kökü + hash zinciri, dış tanığa yayımlanır (AD-06) | Silinmez |
| **Erişim logu** | `audit_access` | Hassas kayda dokunan her okuma: evrak görüntüleme/indirme, TCKN/SGK/IBAN görüntüleme, özel nitelikli veri, teklif metni okuma, sertifika kodu görüntüleme | Aynı zincir (AD-06) | Uzun — mevzuat saklama süreleriyle uyumlu |
| **Kullanım logu** | `audit_usage` | Liste/sayfa görüntülemeleri: kullanıcı, rota, uygulanan filtre, dönen kayıt sayısı | Yok | **Taban 2 yıl**; backoffice'te boyut göstergesi + **elle** temizleme seçenekleri (2/5 yıldan eski) |

⛔ **Sistem hiçbir zaman kendiliğinden veri silmez** *(Sets, 28.07.2026 — bu satır bir yanlış okumayı düzeltiyor)*. Yukarıdaki *"retention seçeneği"* ifadesi **otomatik bir saklama politikası değildir**: backoffice'te bir sayfa, kullanım logunun boyutunu gösterir ve *"2 yıldan eskileri sil" / "5 yıldan eskileri sil"* düğmelerini **elin altında bulundurur**. Karar vermeleri gerekirse araç hazırdır; kimse basmazsa hiçbir satır kaybolmaz.

Bu kural yalnız kullanım logu için değil, **sistemin tamamı için** geçerlidir: zamanlanmış bir iş, bir retention policy ya da bir cron hiçbir iş verisini silmez. (Laravel'in kendi log dosyaları, cache ve geçici dosyalar bu kapsamın dışındadır — onlar iş verisi değildir.)

⚠ Bu sayfa **yalnız `audit_usage`'a dokunur.** `audit_writes` silinmez (AD-06, kırmızı çizgi 12) ve `audit_access` mevzuat saklama süreleriyle bağlıdır; ikisine de bu sayfadan ulaşılamaz.

Bir liste ekranında 200 satır göründü diye 200 erişim satırı yazılmaz — "şu filtreyle 200 kayıt gördü" yazılır.

> **Adlandırma notu:** bu tablo daha önce **"aktivite logu" / `audit_activity`** adını taşıyordu. "Aktivite" kelimesi iş olaylarını (*yeni teklif oluşturdu, revize gönderdi*) anlatmak için kullanıldığından ad **kullanım logu / `audit_usage`** olarak değiştirildi. Arayüzde "aktivite" gördüğün yer `audit_writes`'tır; `audit_usage` kullanıcıya hiçbir zaman satır satır gösterilmez.

### Kim ne görür *(Sets, 26.07.2026)*

| Sınıf | Ofis | Backoffice | Müşteri / katılımcı paneli |
|---|---|---|---|
| **İş olayı audit'i** | Yalnız **ofis personelinin** olayları, yetki ve sorumluluk kapsamında (satış yöneticisi ekibinin adımlarını görür) | Tamamı | Görünmez |
| **Erişim logu** | Görünmez | Tamamı | Görünmez |
| **Kullanım logu** | Görünmez | Yalnız agregat (aşağıdaki k-anonimlik kuralı) | Görünmez |

**Müşteri kimliklerinin denetim izi ofiste görünmez.** Müşteri kullanıcısının giriş geçmişi, hangi ekranı açtığı, neyi görüntülediği ofis tarafında hiçbir ekranda yer almaz — yalnız backoffice'te. Bu, müşterinin **iş aksiyonlarını** kapsamaz: onay, revize talebi ve yazışma iş verisidir ve teklifin kendi ekranında görünür.

**Tek istisna — "son aktif" damgası *(Sets, 26.07.2026)*.** Müşteri kullanıcısının son aktif zaman damgası ofiste müşteri kartında görünür; gerekçesi operasyoneldir (*"paneli açmayan müşteri = churn sinyali"*, aşağıda). İstisna **tek bir türetilmiş alanla** sınırlıdır: damganın kendisi. Giriş geçmişi, oturum listesi, giriş sayısı veya geçirilen süre gösterilmez ve bu istisnaya başka alan eklenemez. Anlık "şu an aktif" rozetinin müşteri kullanıcılarında **bulunmaması** (aşağıdaki online göstergesi hükmü) bu istisnayla çelişmez: rozet sürekli bir gözetim yüzeyidir, damga ise tek bir zaman noktasıdır.

**Ofis personelinin kimlik doğrulama olayları ofis feed'inde görünür.** Giriş/çıkış `audit_writes`'ın kapsamındadır ve yöneticinin sorumluluk alanına girer. Sistem bu olaylardan **süre, oturum uzunluğu, aktiflik skoru veya sıralama üretmez** (kırmızı çizgi 17 ve aşağıdaki analitik rejimi). Giriş olayı zaten kullanım süresini göstermez — sabah giriş yapıp gün boyu panele bakmayan kişi de aynı satırı üretir; ölçtüğü şey oturum açma anıdır, kullanım değil.

**Backoffice çapraz-tenant okuma.** Backoffice tüm tenant'ların denetim kayıtlarını görür. Bu yeni bir yetki değil, **spec T-4'ün (sistem yöneticisinin çapraz-tenant erişimi) denetim kayıtlarına uygulanışıdır**; izolasyon (AD-03) kaldırılmaz, **kapsam tek bir okuma yolundan genişletilir**. Yol açıkça yetkilendirilir ve **kendisi de audit'lenir** ("X yöneticisi tüm tenant'ların kaydını sorguladı"). Dağıtılmış "backoffice zaten her şeyi görür" gerekçesiyle ikinci bir yol açılamaz; denetim dışı iş verisi bu yoldan okunmaz.

**İki pazarlıksız teknik kural:**

1. **Audit yazımı işlemle aynı transaction'da DB'ye gider** — kuyruğa değil. Redis düşerse log kaybolmaz; garanti: *log yazılamadıysa işlem de olmaz.* Damgalama ve WORM arşivleme async, kuyrukta.
2. **Audit opt-in bir trait olamaz.** Tenant-scoped her modelde ve her hassas okuma noktasında zorunlu geçiş; **arch testiyle denetlenir** — audit'i olmayan model testi kırar. "Eklemeyi unuttuk" senaryosu mümkün olmamalı.

**Ürün kullanım analitiği** aynı veriden türer, ayrı araç kullanılmaz:

- **Amaç: ürün iyileştirme, personel takibi değil** *(Sets, 28.07.2026)*. Hangi ekran kullanılıyor, hangisi kullanılmıyor, hangi özelliğe uzanılıyor. *"Kim kaç saat kullanmış"* bu ekranın sorusu değildir ve olmayacaktır. Personelin ne yaptığı sorulacaksa cevabı **işlem günlüğüdür** (`audit_writes`) — ayrı ekran, ayrı soru.
- Kaynak: kullanım logu (`audit_usage`), **düz gruplu sorgu** (`App\Queries\Audit\UsageAnalytics`). ⚠ **Revize 28.07.2026:** bu satır önce *"TimescaleDB continuous aggregate (saatlik/günlük rollup)"* diyordu. Rollup tek bir şey için gerekiyordu — `count(distinct user_id)`, ki continuous aggregate desteklemez — o da yalnızca aşağıdaki eşik için. Eşik ayara dönüşünce gerekçe ortadan kalktı ve **kurulmadı**. Tablo büyüyünce rollup girer; önce değil.
- Kırılım: müşteri tarafında `tenant × rol × ekran`, ofis tarafında `rol/kullanıcı grubu × ekran` (satışçılar/eğitmenler/planlamacılar hangi sayfaları çok veya hiç kullanmıyor). Amaç ürün geliştirme önceliklendirmesi.
- **Kişi kırılımı YOK. Toplam çevrimiçi süre / aktiflik skoru YOK.** Kişi bazında tutulan tek türetilmiş veri: **son aktif** zaman damgası (hem ofis hem müşteri kullanıcısı kartında). Gerekçesi operasyonel ("3 haftadır giriş yapmamış satışçı", "paneli açmayan müşteri = churn sinyali"), performans ölçümü değil.
- **Kişi eşiği: bir kırılım eşikten az kişi içeriyorsa rolü gizlenir**, görüntüleme sayısı kalır. ⚠ **Revize 28.07.2026:** önce *"en az 2 kişi… bir üst role yuvarlanır"* sabit kuraldı; artık **admin-tanımlı ayar**, varsayılan 2, 1 yapılınca kapanır (`business.usage.minimum_people`, parametreler ekranı). Sets'in gerekçesi: kişi bazlı rapor ekranı yok ve olmayacak, dolayısıyla katı bir kural gereksiz maliyet. Varsayılanın 2 kalmasının sebebi **kaza**: tek kişilik bir rolde rol adı o kişinin adıdır ve kimse öyle tasarlamasa da metrik bireysel hâle gelir. *"Bir üst role yuvarlama"* uygulanmadı — sistemde rol hiyerarşisi yok ve uydurmak, birinin sayılarını ait olmadığı başlığa koymak olurdu; satır **rolsüz kalana katlanır**, toplam korunur.
- Üçüncü parti analitik aracı (GA, Mixpanel vb.) kullanılmaz — müşteri verisi dış sisteme çıkmaz, KVKK tarafında ek yük doğmaz.
- **Uygulama garantisi kod düzeyinde:** `UsageAnalytics` kullanıcı argümanı almaz ve `TenantContextGuard::whileReadingUsageAggregate()` istisnası, SQL'in **kendisini** denetler — tek tablo, `group by` zorunlu, `user_id` yalnız `count(distinct …)` içinde. Kişi bazlı bir sorgu bu bayrağın arkasında da reddedilir; "çağıran agregat olduğunu söylüyor" yeterli sayılmaz.

**Online göstergesi:** anlık "şu an aktif" rozeti yalnız direktör + operasyon direktörü tarafından görülür (AD-22 ile besleniyor). Müşteri kullanıcılarında ve katılımcıda online göstergesi **yoktur** — başka bir tüzel kişinin çalışanını izlemiş oluruz.

## AD-22: Realtime — Laravel Reverb, dar kapsam

**Karar:** Reverb **day-one** kurulur, kapsam bilinçli olarak dardır.

**Gerekçe:** sonradan eklemek pahalı — Inertia+Vue tarafında Echo bağlantısı layout'a, bildirim state'i store'a girer ve bildirim üreten her nokta tek tek elden geçer. Şimdi kurulursa `ShouldBroadcast` eklemek tek satır. Gerçek kullanımlar mevcut: müşteri teklifi onayladı, Postmark inbound cevap geldi, limit aşımı onaya düştü, kontenjan teyidi gerekti.

> ⛔ **Revizyon — Sets kararı, 05.08.2026: yalnız OFİS tarafı.** Müşteri portalında soket **yoktur** ve bu bir ilk adım değil, kararın kendisidir.
>
> **Gerekçe 1 — kritik olayların hepsi ofiste.** Aynı dakika içinde düşmesi gereken her bildirimin alıcısı ofis: müşteri teklifi onayladı → *satışçı* bilmeli · maile cevap geldi · mail bounce etti · limit aşımı onaya düştü · kontenjan teyidi gerekti. Hiçbiri müşterinin kendi ekranıyla cevaplanmıyor.
>
> **Gerekçe 2 — karmaşa gerçekten yok oluyor.** Reverb'ün pahalı tek parçası kanal yetkilendirmesiydi; pahalı olma sebebi **üç guard**tı (varsayılan guard yok). Tek tarafa inince problem *"üç aileyi nasıl ayırt ederim"*den *"office guard'ı"*na düşüyor.
>
> **Gerekçe 3 — risk sınıfı ortadan kalkıyor.** Portal internete açık ve çok kiracılı olan taraf. Soket koymamak, kanal adında tenant id taşımayı, auth callback'inde kapsam çözmeyi ve bir müşterinin diğerinin kanalına abone olma yüzeyini **hiç var etmiyor**. Ofis kapalı, tanınan, sayısı bilinen bir nüfus.
>
> ⚠ **`in_app` asimetrisi — bu bir hata değildir.** Bildirim satırı iki taraf için de aynı şekilde yazılır; ofiste **canlı** görünür, müşteride **sayfa açılışında**. Yazılmazsa hata olarak raporlanır.
>
> ⚠ **Sonradan müşteri tarafı eklemek ucuz kalıyor** ve karar verilmeden önce kontrol edildi: yayın, in-app satırının kapatıldığı **tek yerde** yapılıyor ve orası iki taraf için ortak. İkinci bir dinleyici kitlesi yalnız kendi layout bootstrap'ını ister — layout'lar zaten ayrı dosya.
>
> **Kanal:** `office.user.{id}`, `guards: office`. `App.Models.User.{id}` **kullanılmıyor**: burada `User` ofis kimliğidir, `CustomerUser` başkasıdır; sınıf adını taşıyan bir kanal, üç kimliği olan bir sistemde *"the user"* diye okunur.
>
> **Oturum kapanışı — dört katman.** ⛔ Zorlama sunucuda zaten tamdı (`TerminatesSessions` session satırlarını anında siliyor). Eksik olan **açık ekranın haberi**. Pusher protokolünde sunucu tarafı *"bu istemciyi düşür"* komutu **yoktur**, dolayısıyla mekanizma işbirlikçidir — ve burada **yeterlidir**: mesajı yok sayan istemcinin elindeki soket salt okunur, HTTP ölü, ve `RecipientResolver` yalnız aktif hesapları adresliyor. (1) `session.ended` yayını · (2) `/broadcasting/auth` 403'ü — her bağlanışta bedava oturum yoklaması · (3) her Inertia yanıtındaki `auth` prop'u · (4) **yalnız soket kopukken** 60 sn'lik nabız. ⛔ Katman 4 **yalnız 401/403'te** çıkış yaptırır; ağ hatası oturum kapatma sebebi değildir.
>
> **Yetki değişikliği:** `UserPermissionsChanged` (yıllardır dinleyicisiz duruyordu) → panel kabuğunu tazeler. Hesap aktif kaldığı için session doğru şekilde sürer; değişen yalnız menünün ne teklif ettiğidir.

**Kapsam:**
- **Ofis** kullanıcısı başına **private kanal** (`office.user.{id}`) + bildirim omurgasının `in_app` satırı.
- **Presence channel KULLANILMAZ.** Üyelik listesi yayınlar, izolasyon açısından fazla bilgi sızdırır, ölçeklendiğinde en çok debug edilen bileşendir.
- "Bu eğitim için 1 satışçı daha teklif hazırlıyor" uyarısı presence ile değil, **TTL'li kısa ömürlü "çalışıyor" kaydı + broadcast** ile yapılır. Kim olduğu ve hangi müşteri için çalıştığı **gösterilmez**. Uyarı tavsiye niteliğindedir, hiçbir akışı bloklamaz; TTL dolunca kendiliğinden düşer (orphan lock kalmaz).
- Online göstergesi: son aktivite zaman damgasından türetilir (AD-21).
- Canlı ortak düzenleme, typing indicator vb. **yok**.

**Altyapı:** Reverb bir PHP process'tir — compose'a değil supervisor'a girer; dev'de `php artisan reverb:start`. Subdomain **`ws.infra.egitim.3egrp.com`**; `infra.*` core servisler için çatı subdomain'dir (ileride `cdn.infra…` gibi) — infra altında bir sorun görülürse çekirdek servis sorunu olduğu anlaşılır. Lokal karşılık `ws.infra.egitim.dev.sets.fyi`, local prod `ws.infra.egitim.devprod.sets.fyi`.

⚠ *Bu satır 09.08.2026'da düzeltildi ve iki olgu birden yanlıştı: host ailesi `akademi.*` yazıyordu (ürün `egitim.3egrp.com`'a taşındı, `akademi.3egrp.com` akademinin tanıtım sitesi ve posta gönderim domain'i olarak kaldı) ve lokal karşılık `.test` diyordu — `.test` hostları 27.07.2026'da kaldırılmıştı. Bir AD satırı yetki olarak okunur; iki yanlış olgu taşıması pahalıdır.*

**Test:** broadcast driver testte `null` + `Event::fake()` — flaky riski yaratmaz (AD-26).

## AD-23: Organizasyon ve yetki modeli

**Karar:** Tek grup ağacı + kapsam seviyeli yetkiler. Direktörlük ayrı bir mekanizma değil, **ağacın kökündeki yöneticiliktir**.

```
3E  ................................. Operasyon Direktörü (tüm dikeyler)
├── Satış  ......................... Satış Direktörü
│   ├── İzmit Satış  ............... Satış Yöneticisi → satışçılar
│   └── İstanbul Satış
├── Planlama  ...................... Planlama Direktörü
│   ├── İzmit Planlama
│   └── İstanbul Planlama
└── Eğitmen Kadrosu  ............... Eğitim Direktörü
    ├── İzmit Eğitmenleri
    └── İstanbul Eğitmenleri
```

Süper admin / admin ağacın **dışındadır** (sistem rolü, backoffice).

⚠ **Düzeltme (Sets, 28.07.2026):** ağaçta `Planlama` dikeyinin başı *"Eğitim Direktörü"* yazıyordu ve `Eğitmen Kadrosu` başsız duruyordu. İkisi ayrı dikeydir ve ayrı yöneticisi vardır. Mimari zaten aynı kişinin iki dikeyde birden direktör olmasını destekliyor (aşağıdaki *"bir kişi iki dikeyde direktör olabilir"* kuralı) — etiketleri birleştirmeye gerek yoktu.

**Üyelik seviyesi — rol yetki değil kapsam taşır** *(Sets, 28.07.2026)*. Üyelik üç seviyeden birini taşır: **`director` · `group_manager` · `staff`**. Ekrandaki ad bileşimden türer (`Satış` + `director` → *"Satış Direktörü"*); on bir sabit rol adı tutulmaz, çünkü dikey bilgisi zaten ağaçtadır ve yeni bir dikey açmak yeni rol tanımı gerektirmemelidir.

⛔ **Seviye yetki vermez, kapsam belirler.** `staff` yalnız kendisini, `group_manager` ve `director` kendi düğümü + altlarını kapsar — ikisi arasındaki farkı seviye değil **ağaçtaki yer** yaratır, ki bu kararın kendi cümlesidir (*"direktör için özel `if` yoktur"*). Yetkiler her hâlükârda havuzdan tek tek seçilir.

**Gerekçe:** "ağır hiyerarşi istemiyorum ama direktör olsun" ihtiyacı aslında aynı yapının iki seviyesidir. Tek bir kapsam çözümleyicisi yazılır ("kişinin üyeliklerinden erişilebilen düğümler + altları"); direktör için özel `if` yoktur. Bugün ağaç iki seviye kullanılır; yarın "Marmara Bölge" gerekirse araya düğüm eklenir, kod değişmez. Kullanılmayan seviye zaten yok — çöp yapı oluşmaz.

**Bileşenler:**
- **Yetki** = `kaynak.eylem` + **kapsam seviyesi** (`own` / `group` / `all`). *"Teklif görüntüleyebilir"* tek başına yetersizdir. Yetkiler kişiye **havuzdan tek tek** verilir.
- **Üyelik** = (kişi, grup, **seviye**) üçlüsü. Bir kişi birden fazla grupta **farklı seviyelerde** bulunabilir (ör. İzmit'te grup yöneticisi, İstanbul'da personel). Kapsamı üyeliklerinin birleşimidir.
- ⚠ **Revizyon (Sets, 28.07.2026):** üyelik eskiden `(kişi, grup, rol)` üçlüsüydü ve rol *"yetki seti"* diye tanımlıydı. Kaldırıldı: **rol bir yetki seti olsaydı, üyelik vermek yetki vermek olurdu** — oysa yeni kullanıcı sisteme yetkisiz girer ve yetkisi ilk girişinden sonra tanımlanır (`docs/plans/ofis-t4-plan.md` K-D). Üyelik artık **seviye** taşır; seviye kapsamı belirler, yetki vermez. Rol bir *yetki seti kısayolu* olarak ileride istenirse ayrı bir kalemdir.
- **Etkin kapsam dar olandır:** izin ekinin söylediği ile seviyenin tavanı çakışırsa dar olan geçerlidir (`staff` + `.group` → `own`). AD-09'un *"zincirin en dar halkası"* kuralının aynısı. ⚠ `all` yalnız izin ekiyle gelir ve ağaçtaki yerden bağımsızdır.
- **Direktörlük ≠ ek yetki alanı.** Direktör kendi dikeyinin en üstüdür, fazlası değil. Mali tabloları görmek **ayrı bir yetkidir** ve varsayılan olarak operasyon direktöründedir; admin dilerse bir direktöre açar. Bir kişi iki dikeyde direktör olabilir.
- **Org kapsamı ≠ nesne yetkisi.** Ağaç "hangi kayıtlar kapsamımda" sorusunu, yetki "bu kayda ne yapabilirim" sorusunu cevaplar. Dikeyler arası akış (satışçı planlanmış eğitimi görür) ağacı gevşeterek değil, nesne yetkisiyle çözülür — yoksa ağaç delik deşik olur.

**Güvenlik kuralları (pazarlıksız):**
1. **Ofis içinde kimse sahip olmadığı yetkiyi devredemez**; "yetki atama" ayrı bir yetkidir. Bu kuralın **tek ve kalıcı istisnası backoffice provisioning'idir** (aşağıda): adlandırılmış, ayrı bir izin ailesine bağlı ve tehdit varsayımı AD-09'da yazılı. Başka istisna yoktur; "benim durumum da özel" gerekçesi kabul edilmez.
2. **Kimse kendi oluşturduğu kaydı onaylayamaz** — otomatik bir üste çıkar (dört göz). Aynı kişinin bir grupta yönetici, başka grupta icracı olabildiği modelde bu senaryo kesinlikle çıkar.

**Kullanıcı oluşturma — iki yol, tek kural *(Sets, 26.07.2026)*:**

- **Ofis içinde kademeli ve açık izne bağlı.** Operasyon direktörü ofisin kökündedir: direktör de personel de oluşturabilir. Bir satış direktörü yeni satışçı oluşturabilir — **ancak** operasyon direktörü ona kullanıcı oluşturma yetkisini verdiyse ve yeni kullanıcı onun kapsamındaysa. Yani kural 1 ofis içinde aynen geçerlidir; yetki kapsamdan değil, **açık izinden** gelir.
- **Backoffice tarafında provisioning — kural 1'e kalıcı istisna.** Backoffice'in kullanıcı yönetimi aynı işlemleri kapsam sınırı olmadan yapabilir; backoffice yöneticisi kendisi ofis yetkisi taşımadığı halde ofis yetkisi atar. Sistem ilk açıldığında **yalnız backoffice kullanıcıları vardır** — ilk ofis kullanıcıları oradan doğar — ve bu yetki **kuruluştan sonra da** backoffice'te kalır. Yani bu bir "önce gelen adım" değil, **süreklilik arz eden bir muafiyettir** ve kural 1'de böyle adlandırılmıştır. Dayanağı AD-09'daki tehdit varsayımıdır (backoffice yöneticisi = şirket sahibi); varsayım değişirse muafiyet yeniden değerlendirilir.
- Her iki yolda da kullanıcı yönetimi işlemleri audit'lenir (AD-21) ve backoffice'te MFA zorunludur.

**Atamalar:**
- **Müşteri** gruba (zorunlu) + kişiye (opsiyonel) atanır; kişiye atanmamışlar grup havuzunda durur.
- **Eğitmen → planlamacı ataması** simetriktir: geçici havuz, **süreli veya süresiz**. Süre dolunca otomatik geri çekilir. Eğitmen geri çekildiğinde **mevcut planlanmış eğitimler korunur**, yalnız yeni planlama yapılamaz.
- Planlama ve eğitmen dikeylerinde müşteri ataması yoktur.

**Kullanıcı çıkışı:** kullanıcı **silinmez, pasifleştirilir**. Geçmiş kayıtlarda kimin ne yaptığı asla değişmez (denetim izi). Çıkışta **müşteri devri sihirbazı** çalışır: gruba ata / kişiye ata / havuza koy — yöneticinin kararı audit'e yazılır.

**Onay ve eskalasyon:**
**Ortak omurga, ayrı havuzlar** *(Sets, 28.07.2026)*. Onay mekanizması **tek** yerde yaşar; onay **listeleri** süreç başına ayrıdır.

| Ortak omurga (tek yerde yazılır) | Sürecin kendi (her süreçte ayrı) |
|---|---|
| Onay adımı kaydı: kim, ne zaman, hangi adım | Kayıt tipi, koşul, adım listesi |
| Dört göz kuralı | Yetki adı (`expense.approve_group`, `training.approve`, `quote.approve`…) |
| Üst yöneticinin onaylayabilmesi + silsile bildirim | Ekran, filtre, gösterilen tablo ve alanlar |
| 1 iş günü alarmı | Sürece özel zorunluluklar (ör. fiş fotoğrafı) |
| Audit (AD-06) | — |

⚠ **Ayrı havuz şart:** tür seçmeli tek bir onay listesi kullanılamaz hâle gelir; filtrelenebilirlik ve yetki kırılımı süreç başına ekran ister. ⚠ **Ortak omurga da şart:** dört göz, eskalasyon sayacı, silsile bildirimi ve audit üç kez yazılırsa üçü aynı gün doğru olur, altı ay sonra biri düzeltilir ikisi unutulur. Bir sürece alan eklemek diğerlerine dokunmaz — omurga yalnız *"2. adım, şu kişi, şu saatte"* bilir.

- Onay kuralı backoffice'te tanımlıdır: `(kayıt tipi, koşul, gereken onay adımları[])`. Örn. *teklif + indirim > %15 → [grup yöneticisi, direktör]*.
- **Global parametrenin genişletilmesi de bir onay konusudur** (AD-09 revizyonu, 28.07.2026): satışçı tavanının globalin üstüne çıkması, planlamacının global onay kuralını gevşetmesi — engellenmez, bu motora düşer.
- "Kendi kaydını onaylayabilir mi" **rol bazında** ayarlanır (personel hayır, satış yöneticisi evet gibi).
- **Kullanıcı bazında "ikinci imza gerekir" bayrağı**: yeni atanmış bir yöneticinin onayı tek başına yeterli sayılmaz, her zaman bir üst imzayla tamamlanır. Öğrenme süreci bitince bayrak kalkar, kural değişmez.
- Bir onay üst yöneticiler tarafından **görülebilir ve onaylanabilir** (yönetici izinliyken iş durmasın). Üst onaylarsa **silsile bildirim** gider (asıl onaycıya + kaydı oluşturana) ve audit'e "atlanan onaycı" yazılır — sessiz bypass olmaz.
- İki liste: **"benden onay bekleyenler"** ve **"genel onay bekleyenler"** (üst yöneticiler için).
- **1 gün** geçen onay alarm üretir. ⛔ **Takvim günü** *(Sets, 18.08.2026 — EGT-43 Won't fix)*: "iş günü" hesabı resmî tatil takvimi ister, öyle bir listeye ihtiyaç yok. *"Lojistik sektörü 7/24/365 çalışan bir sektör zaten; tatil günü ise o gün kendileri ayarlarlar iş akışlarını."* Kayıt olarak eski metin "1 iş günü" diyordu; kural değişmedi, **sayma biçimi karara bağlandı**.

### Şifre kimde vardır *(16.08.2026, EGT-159)*

⛔ **Panel ekranlarına erişen her hesabın şifresi vardır.** Ofis ve backoffice tarafında bu şemanın kendisiyle zorlanır (`users.password` ve `backoffice_users.password` NOT NULL); ölçüldü, 52 ofis ve 6 backoffice hesabının tamamı şifreli.

⚠ **Şifresiz hesap yalnız katılımcıdır** ve katılımcı da panele girer — aynı panele. Göremediği şey firma ekranlarıdır, çünkü izni yoktur; kendi eğitimlerini ve belgelerini görür *(Sets, 16.08.2026: «katılımcı panele girmez» cümlesi yanlıştır)*. `customer_users.password` bu yüzden nullable: bir katılımcıya hiç sorulmamış bir şifre uydurmak, kimsenin istemediği ve kimsenin döndürmediği bir kimlik bilgisi yaratmaktır.

⚠ **Sonucu şudur:** şifresiz bir kişi firma paneli ekranlarına zaten ulaşamaz. Girişi sihirli bağlantıyladır, o da `isStrong()` bakımından zayıftır ve `RequireStrongAuth` firma ekranlarına 403 der. Dolayısıyla kimlik penceresinin şifre sorması bir kısıt değildir: o pencereyi görebilen herkesin tanımı gereği şifresi vardır.

⛔ Bu, «yalnız passkey'i olan, şifresi olmayan hesap» diye bir şeyin **üretilemeyeceği** anlamına da gelir: passkey eklemek de aynı şifre penceresinin arkasındadır. Bir hesap passkey'e ancak şifresi varken kavuşur.

⚠ Ara durum kural ihlali değildir: daveti henüz kabul etmemiş bir hesabın şifresi yoktur ve olmamalıdır — şifre, davet kabul edilirken belirlenir. Ölçüldü (16.08.2026): panele erişebilen 16 müşteri kullanıcısından yalnız biri şifresiz ve o kişi hiç giriş yapmamış.

## AD-24: Evrak ve gereklilik motoru

**Karar:** Evrak yönetimi bir CRUD değil, **gereklilik tanımlama motorudur**. Alanlar tiplenmiş ve sabittir; dinamik olan **hangi bağlamda hangi evrakın zorunlu olduğudur**. EAV/JSON şema kullanılmaz.

> **Ek — Sets kararı, 27.07.2026 (kararı değiştirmez, eksiğini kapatır).** EAV yasağı *"yarın mevzuat bugün serbest metin girdiğimiz bir alanı sorgu alanı yaparsa ne olacak"* sorusunu cevapsız bırakıyordu. Cevap depolamada değil **söz dağarcığındaydı**: bir gün karara girebilecek her değer **yönetilen bir listeden seçilir**, serbest metin olarak yazılmaz. Sebebi migration değil onarılamazlıktır — aynı bölüm üç farklı yazımla girildiğinde hiçbir sorgu bunları birleştiremez ve hiçbir migration bunu geriye dönük düzeltemez. Bu ek sayesinde AD-24'ün kabul ettiği *"yılda birkaç migration"* bedeli gerçekten küçük kalır: yeni alan bir nullable kolon + bir liste bağıdır, geçmiş veri de doğru bulunur.
>
> **Ek 2 — sahip başına ayrı evrak tablosu (Sets kararı, 27.07.2026).** AD-24'ün *"aynı şablon beş yerde"* cümlesi **şablon** hakkındadır; chunk 4 bunu **satır** kararına genişletip tek polimorfik `documents` tablosu kurmuştu. Bu düzeltildi: **tanım ortak, satırlar sahibine göre ayrı tablolarda.**
>
> **Gerekçe — bağlam.** Eğitmenin diploması 3E'nin, katılımcının sağlık raporu müşterinin verisidir. RLS **tablo başına tekdüzedir** (chunk 2): bir tablo satırlarının bir kısmı için row-secured, kalanı için bağlamsız olamaz; koşullu politikayla korunan nullable `tenant_id` tam olarak chunk 2'nin yasakladığı şeydir. Emsal zaten kod tabanındadır — üç kimlik alanı hiçbir tablo paylaşmaz (spec kriter 31). **Policy, gate ve izin kontrolleri de bu sayede tek sahipli, tek bağlamlı ve tek izin ailesine bağlı kalır.**
>
> **Kural:** 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. Tekrar bilinçli bedeldir — "Kimlik" tek tanım olarak kalır, tek raporda toplanır, satırları ait olduğu yerde durur. ⚠ **Kapsam listesi ile tablo listesi kilitlidir:** bir sahip tablosu varsa o sahip için bir kapsam da olmalı, yoksa oraya hangi belge türünün gireceği söylenemez.
>
> **Sahip tablosu kendi modülüyle doğar.** Sahibi olmayan bir evrak tablosu yabancı anahtar taşıyamaz — bölmenin yarı sebebi budur. chunk 4'ün tablosu, zaten uyduğu sahibe daraltıldı: `customer_documents` (sahip = `tenant_id`, çünkü `customers` zaten tenant'tır).
>
> **Ve nitelik belgeden okunmaz.** *"Mühendislik fakültesinden mezun"* (DG-2020 md.14/6) bir belge alanı değil bir **yeterliliktir**: eğitmen ön şartı olarak tanımlanır, belge onun kanıtıdır, doğrulamayı insan yapar. Taranmış bir diploma zaten makine tarafından doğrulanamaz. **Serbest metindeki hiçbir değer karara girdi olamaz.**

**Gerekçe:** tam dinamik alan tanımı (EAV) maksimum esneklik verir ama alan bazlı şifreleme (AD-05), RLS, audit maskeleme (AD-16) ve raporlama bunun üstünde kötü çalışır. Hibrit model, "3E kendi kurallarını girer" vaadinin pratikte tamamını karşılar; yepyeni bir **veri tipi** (ör. "gemi adamı cüzdanı numarası") eklemek yılda birkaç migration'a mal olur — bu kabul edilebilir bir bedeldir.

**`evrak_turleri` (şablon):** ad · **kullanım kapsamı** (çoklu seçim: eğitmen · ofis kullanıcısı · müşteri firma · müşteri personeli/katılımcı · **eğitim tanımı**) · ön/arka yüz gerekli mi · alınma tarihi var mı · geçerlilik/expire tarihi var mı · özel ID alanı var mı (+etiketi, ör. "Bakanlık Eğitmen No") · açıklama alanı var mı · dosya tipi/boyut kısıtı · **kim yükleyebilir** (yalnız operasyon / yalnız sahibi / ikisi) · **onay gerekli mi** · **özel nitelikli mi**.

Kullanım kapsamı sayesinde "Kimlik" tek şablon olarak tanımlanır, beş yerde aynı davranır, aynı raporda toplanır. Gözetim/TMGD modülleri geldiğinde sıfır ek iş.

**`gereklilikler` (bağlam × evrak):** *"IMDG Kod eğitmeni → Gözetim Yetki Belgesi → zorunlu"*, *"IMDG görev 4 eğitimi → sınav dokümanı → zorunlu"*. Form her yerde şablondan render edilir, zorunluluk bağlamdan gelir.

**Özel nitelikli veri (KVKK m.6):** adli sicil kaydı, sağlık raporu vb. Bayrak işaretliyse sistem üç kuralı **otomatik** uygular: erişim rol bazında daraltılır · her görüntüleme audit'lenir (AD-21 erişim logu) · saklama süresi tanımlıdır. Uyum ürünü satarken kendi içimizde açık vermeyiz.

**Şablon sürümleme ve değişiklik kilidi:**
- Evrak türü sürümlenir; yüklenen her evrak hangi sürümle yüklendiğini taşır (AD-11 hash + sürüm).
- Değişiklik öncesi **etki analizi** gösterilir: *"Bu değişiklik 47 yüklü evrakı etkiliyor; 12'sinde expire tarihi boş kalacak."* İki yol sunulur: **(a) geçmişe uygula** — etkilenen kayıtlar "eksik alan" görevine döner ve tamamlanmadan o kişiye **yeni atama yapılamaz**; **(b) ileriye dönük** — eski sürüm geçerli kalır.
- **Kritik alan kaldırma / tip değiştirmede (a)/(b) seçeneği yoktur:** "önce şu 12 kaydı düzeltin" denir ve değişiklik **reddedilir**. Veri hiçbir zaman şablonuyla tutarsız kalmaz.

**Zorunluluğun geriye dönük etkisi:** yeni bir zorunluluk (ör. mevzuat değişip adli sicil zorunlu olunca) **yürürlük tarihiyle** tanımlanır. Geçmiş kayıtlar bloklanmaz, "eksik evrak" rozetiyle listelenir ve **yeni eğitim ataması yapılamaz**. Engel geçmişe değil ileriye dönük işler.

**Evrak talebi görevi (çekirdek servis):** hedef kişi (eğitmen / müşteri yetkilisi / katılımcı) · istenen evrak türü · son tarih · durum (bekliyor → yüklendi → onaylandı / reddedildi + gerekçe). **Yükleyen ≠ onaylayan** (dört göz). Yeni eğitmen eklenirken operasyoncu elindekileri yükler, kalanları tek tıkla eğitmene görev atar; eğitmen panelinde alert + görev görür, yükler, operasyon onaylar, alert kalkar. Aynı mekanizma müşteri personelinin SGK belgesi ve katılımcının yenileme sertifikası için de çalışır. **Eğitim tanımındaki eksik evraklar** (eğitim dokümanı, sınav dokümanı, yoklama dokümanı) operasyon yöneticisine görev olarak düşer.

## AD-25: Kod yerleşimi ve atomic design

**Karar:** Satır/dosya boyutu limiti **konmaz**. Bunun yerine **yerleşim sözleşmesi** uygulanır: her sorumluluğun tek bir doğru adresi vardır ve bu adres `docs/arch/kod-yerlesimi.md`'de yazılıdır.

**Gerekçe:** limit koyunca kod doğru yere değil, limitin altına sığacak yere gider ve gerçek ihtiyaç anında gerçekçi olmayan çözümler üretilir. Asıl sorun dosya uzunluğu değil, **gidecek başka adresi olmayan koddur** — geliştirici aynı dosyaya eklemeye devam eder çünkü "controller'a mı, servise mi, helper'a mı?" sorusu her seferinde yeniden cevaplanır. Adres sabitlenirse dosya kendiliğinden büyümez ve 1000–2000 satırlık blokları düzenlerken yaşanan bağlam sorunu doğmaz.

**Katman özeti** (detay ayrı dokümanda): `app/Domain/` framework'süz iş kuralı kararları · `app/Actions/` tek eylem, tek `__invoke` · `app/Queries/` sorgular · `app/Policies/` yetki · `app/Integrations/<Saglayici>/` dış servis adaptörleri · Form Request girdi doğrulama · `app/Data/` DTO · durum makineleri Domain altında.

**Zorlama:** Pest **arch testleri**. Domain'de `Illuminate\*` import edilemez, Controller'da `DB::`/`validate()` olamaz, Model'de HTTP çağrısı olamaz, Integrations dışında SDK import edilemez. İhlal anında build kırılır — satır saymaya gerek kalmaz.

**Atomic design:** Inertia + Vue tarafında **katı** uygulanır (`atoms / molecules / organisms / templates / pages`); sayfa iş mantığı barındırmaz. **Filament (backoffice) kapsam dışıdır** — kendi bileşen sistemi vardır, atomic hiyerarşi dayatmak ters teper; kural yalnızca "custom Blade view yazma, Filament primitive'lerini kullan" ve form/tablo şemasını Resource'tan ayır. Design token'ları **tek CSS değişken kaynağından** Filament dışındaki tüm ön yüzlere beslenir. *(28.07.2026'da netleştirildi: cümle önce "her iki tarafa" diyordu ve Filament'i tartışan bir paragrafın sonunda durduğu için iki ayrı okuyucu tarafından "Vue + Filament" diye okundu. Kast ofis + müşteri idi; ifade sayım yerine **dışlama** olarak yazıldı ki yarın doğacak yüzey — landing, katılımcı — dışarıda kalmasın. Uygulama: `docs/arch/kod-yerlesimi.md` §4.)*

## AD-26: Test stratejisi ve flaky önleme

**Karar:** Test disiplini **day-one** kurulur. Puanbenim deneyiminde bunların tamamı sonradan eklendi (PA-191 grup scoping, OPS-25 suite isolation, PGGSSENCMODE olayı) ve her biri haftalar yedi. Bu projede baştan konur.

**1. RLS testte gerçekten çalışsın.** PostgreSQL'de tablo sahibi ve superuser RLS'i **sessizce bypass eder**. Test bağlantısı **owner olmayan, superuser olmayan** bir rolle kurulur ve tenant tablolarında `FORCE ROW LEVEL SECURITY` açıktır. Aksi halde izolasyon testleri yemyeşil geçer, prod sızdırır — projenin 1 numaralı kırmızı çizgisi test edilemez hale gelir.

**2. `SET LOCAL` için ayrı suite.** `RefreshDatabase` her testi bir transaction'a sarar; tenant context'i de transaction içinde yaşar (AD-04). Sarmalanmış transaction içinde `SET LOCAL` semantiği prod davranışını yansıtmaz. Bu yüzden: normal testler `RefreshDatabase` + `--parallel` ile hızlı koşar, **`omurga-rls` grubu transaction sarmalaması olmadan (truncation ile)** koşar. Küçük bir gruptur, yavaşlığı sorun değildir.

**3. İş kuralları DB'ye dokunmaz.** Kapsama, muafiyet, fiyat/indirim, süre ve kontenjan hesapları `app/Domain/` altındaki saf sınıflarda yaşar (AD-25) → testleri unit ve milisaniye. Bu projede iş kuralı yoğunluğu çok yüksek; Feature testine kaydırılırsa suite dakikalarca sürer. **Tek başına en büyük hız kazancı budur.**

**4. Flaky'nin üç klasik kaynağı kapalı:**
- **Zaman:** `Carbon::setTestNow()` zorunlu; testte çıplak `now()` **arch testiyle** reddedilir. Süre takibi ağırlıklı bir üründe zaman flaky'nin 1 numaralı sebebidir.
- **Dış çağrı:** `Http::preventStrayRequests()` global açık. Postmark/İletiMerkezi/YouTube/KamuSM/Zoom'a kazara giden istek testi **anında** kırar, "bazen kırmızı" haline gelmez.
- **Paylaşılan state:** `CACHE_STORE=array`, `Storage::fake()`, mail array driver, broadcast `null` + `Event::fake()`; Redis'e yalnız Redis testleri dokunur ve parallel'de process başına ayrı DB numarası alır.

**5. Gruplama zorunlu, day-one.** Her test dosyası tam olarak bir gruba girer: `omurga` · `omurga-rls` · `backoffice` · `planlama` · `satis` · `egitmen` · `musteri` · `katilimci` · `entegrasyon` · `mevzuat`. Grupsuz test arch testiyle reddedilir. Geliştirme sırasında scoped koşum (`teamcity.sh trigger Egitim_Egitim main --group=satis`; script destekliyor) veya lokalde `composer test:satis`. **Merge öncesi full run ZORUNLU** — scoped run cross-cutting regression kaçırır.

**6. Retry yok, karantina var.** CI'da retry flaky'yi gizler ve borcu büyütür. Bunun yerine: **nightly `--order-by=random` full run** (sıra bağımlılığını yakalar) + iki kez tutarsız davranan test `karantina` grubuna alınır ve **otomatik YouTrack ticket'ı açılır** (Sentry botunun duplicate mantığıyla). Karantinada **5'ten fazla test birikirse yeni feature yasak**.

**Hız bütçesi:** grup başına < 60 sn, full suite < 5 dk. Aşıldığında otomatik "test hızlandırma" ticket'ı — yavaşlama fark edilmeden birikmez. `BCRYPT_ROUNDS=4`, config cache yasağı (test başlangıcında guard), migration sayısı arttıkça `schema:dump` ile squash.

**Zorunlu kapsamlar:** her tenant-scoped tablo için **negatif izolasyon testi** · her policy için **yetkisiz rol testi** · para/hesap · durum makinesi geçişleri · mevzuata dayalı kurallar. **Test edilmez:** framework davranışı, Filament'in kendi render'ı, getter/setter. "Eksiksiz test" kapsam sayısı değil **risk kapsaması** demektir.

## AD-27: Mevzuat kaydı — kurallar veriden gelir

**Karar:** Mevzuata dayalı hiçbir kural koda gömülmez. **Mevzuat** bir ana veri kaydıdır; eğitim tanımı ona bağlanır ve **uyarılarını** ekranda görür. Limit, eşik ve periyotların tamamı **eğitim tanımının alanıdır**. AD-08'in en ağır uygulaması budur.

> ⚠ **Revizyon — Sets kararı, 27.07.2026.** Bu AD'nin ilk hâli mevzuat kaydına **makine-okunur bir kısıt seti** koyuyor ve eğitim tanımının bunu **devralmasını** öngörüyordu (miras yalnız sıkılaştırır + lex specialis zinciri). Kaldırıldı. Kararın kaynağı bir yorum hatasıydı: brainstorm'da söylenen *"mevzuattan gelir"* ifadesi *"mevzuat kaydından gelir"* diye genişletildi ve sorulmadan bir veri modeline dönüştürüldü.
>
> **Teknik gerekçe — kısıt seti doğru doldurulamıyor.** Resmî metinler aynı kısıtı **eğitim başına** farklı yazıyor: YÖN-2019 md.8 iki yıl der, md.5/2 ve md.5/4 görev 11 ve 12 için beş yıl der; TY-2019 Ek-5/2 ise on satırlık bir tablodur ve içinde iki ayrı periyot vardır (IMDG/IMSBC 2 yıl, diğer sekizi 5 yıl). Mevzuat sürümünde tek bir `renewal_months` kolonu bunların hiçbirini yalan söylemeden ifade edemez.
>
> **Operasyonel gerekçe:** genelge veya değişiklik yayımlandığında türetilmiş her kısıtın elden geçirilmesi gerekir. Kayıt artık **çürümeyeni** tutar: metin, bağlı dokümanları ve okunması gereken uyarılar.
>
> **Kırmızı çizgi 14 özde korunur:** hiçbir sayı koda girmiyor, hepsi admin-tanımlı veri (AD-08). Değişen şey sayıyı **hangi kaydın** taşıdığıdır.

**Gerekçe (mevzuat taraması, 25.07.2026 — `docs/mevzuat-kurallari.md`):** üç eğitim ailesinin kuralları birbirinden farklı çıktı. Azami katılımcı IMDG'de 30, tehlikeli yük deniz gözetiminde 25, genel deniz gözetiminde düzenlenmemiş. Sınav IMDG'de zorunlu, DGY'de şart değil. Bildirim IMDG'de 3 gün, DGY'de 15 gün **ve onay**. Yenileme kimi eğitimde 2, kimisinde 5 yıl. Bunları koda gömmek mümkün değil; gömülürse her mevzuat değişikliği release gerektirir.

**Kaydın katmanları:**
1. **Künye** — ad, kısa kod, tür (yönetmelik/yönerge), RG veya Bakan Oluru tarih-sayı, resmî link.
2. **Bağlı dokümanlar** — ana metin, yönerge, **genelge**, değişiklik, ek. Her biri kendi tarihi, sayısı, resmî bağlantısı ve dosyasıyla. Bir ana mevzuattan sonra yayımlanan her belge buraya bağlanır; *"bu konuyu hangi belgeler yönetiyor"* sorusu tek ekrandan cevaplanır.
3. **Uyarılar** — eğitim tanımlanırken ekranda gösterilecek, **madde referanslı** hatırlatmalar. Metin yazılır, sayı zorlanmaz: *"Eğitim seminerleri 30 kişiyi geçmeyecek şekilde düzenlenir (md.13/ç)"* — okuyan kişi limiti eğitim tanımına kendisi girer.

**Zorlama yok, uyarı var.** Mevzuat kaydındaki hiçbir alan bir eğitim tanımını reddetmez. *"MEVZUATTA YOK"* satırları da bir uyarı olarak yazılır — çünkü alanı doldurmaya çalışan kişiye *"böyle bir kural yok"* demek, onu boş bırakmakla uydurmak arasındaki farkı kuran şeydir.

**Zincir (lex specialis) — 05.08.2026 revizyonu.** Kaydın **dördüncü** katmanı: bir metnin sessiz kaldığı konularda hangi metne bakılacağı, sırasıyla (`regulation_fallbacks`). TY-2019 md.17/4 bunu kendi cümlesiyle söylüyor: *"bu Yönergede yer almayan hususlarda Y-2016 ve YÖN-2019 hükümleri geçerlidir."* Ekran, seçilen mevzuatın **kendi uyarılarını, ardından zincirden gelenleri** gösterir; devralınan her satır geldiği metnin koduyla işaretlidir.

> ⛔ **Bu, 27.07.2026'nın "otomatik çözümlenen bir zincir tablosu yoktur" satırını tersine çevirir** *(Sets, 05.08.2026)*. Gerekçe: **"mevzuatlar birbirine zincirleme bağlı gidiyor, güncel çıkan sadece atıf ile bazı değişiklikler içerebiliyor."** Atıfı takip edemeyen bir kayıt, yürürlükte olanı gösteremez — kaydın var olma sebebi tam olarak budur.
>
> ⚠ **Kısıt seti geri gelmedi ve gelmeyecek.** Zincir **yalnız uyarı taşır**; hiçbir limit, eşik, süre veya periyot ondan türetilmez. 31.07.2026'nın gerekçesi (mevzuat başına tek sayı yalan söylüyor) aynen geçerlidir. Zincir *"bu kişi başka ne okumalı"* sorusunu cevaplar.
>
> ⚠ Günlük karşılığı: **bildirim süresi TY-2019'da hiç geçmez**; üç gün Y-2016 md.13/g'dedir. Yalnız özel metni okuyan planlamacı onu hiç görmezdi.
>
> ⚠ Döngü yasak: kendine atıf DB CHECK ile, A→B→A yazılmadan önce `FallbackChain::wouldLoop()` ile reddedilir.

**Genişleme:** yeni eğitim ailesi (TMGD, SRC…) eklemek = yeni mevzuat kaydı + uyarıları + dokümanları + **zinciri** + eğitim satırları. **Kod değişmez.**

## AD-28: Sertifika sınırı — belge Bakanlıkta, kod bizde

**Karar:** Sertifikayı **Bakanlık üretir** (KUMS üzerinden). 3E kendi sertifika belgesini üretmez, şablonlamaz, saklamaz.

**Gerekçe:** Bakanlık bir sertifikayı iptal edebilir; doğrulama entegrasyonumuz olmadığı için bundan haberdar olamayız. Elimizdeki kopya yanlış güvence üretir ve kullanıcı geçersiz belgeye dayanarak işlem yapar. Belgenin ve geçerliliğinin sorumluluğu Bakanlıktadır; bizim işimiz eğitimi vermektir.

**Tutulan:** Bakanlık **sertifika numarası + veriliş tarihi + geçerlilik tarihi**. Nedeni ikilidir — (a) denetimde *"bu eğitimi verdik, bildirdik, Bakanlık bu sertifikayı düzenledi"* kanıtı; (b) ileride Bakanlık doğrulama API'si açılırsa entegrasyonun anahtarı sertifika kodudur, iş bir provider eklemeye bakar (AD-12'deki adaptör mantığı).

**Gösterim:** sertifika **kodu** müşteriye (kendi personelinin kodları) ve katılımcıya (kendi kodu) gösterilir; her gösterimde **zorunlu disclaimer**: *"Karar verici Bakanlıktır; güncel geçerlilik durumu Bakanlık sisteminden kontrol edilmelidir."* Belgenin kendisi tutulmaz ve gösterilmez.

**Sınır:** kendi eğitim kaydımız bundan ayrı bir şeydir ve **kalır** — hangi katılımcı, hangi eğitimi, ne zaman aldı, sınavdan kaç aldı, ne zamana kadar geçerli. Yenileme takibi, süre dolan personel dashboard'u ve satış fırsatı motoru tamamen buna dayanır; Bakanlık iptal riski bu kaydı etkilemez.

**Tasarım etkisi:** tasarımdaki sertifika kartı, QR doğrulama ve sertifika PDF şablonu **kapsam dışıdır**. Müşteri "Sertifikalar" ve katılımcı "Sertifikalarım" sayfaları "eğitim geçmişi / uyum durumu" olarak yeniden kurgulanır (EGT-10). Müşteri tanım dokümanındaki `FR-CER` kalemi de bu karara göre güncellenmelidir.

## AD-29: Ürünler arası yazışma — tek adres, ürün öneki, bağımsız geçit

*(Sets, 16.08.2026 — gözetim yazılımı da teklif gönderecek ve aynı takip sistemini kullanacak)*

**Karar:** 3E'nin bütün ürünleri **tek bir gelen posta alan adını** paylaşır — bugünkü `teklif.3egrp.com`. Bir mektubun hangi ürüne ait olduğu **referans kodundaki ürün önekinden** okunur. Gelen postayı **eğitim uygulaması alır ve saklar**; başka bir ürüne ait mektubu **o ürün kendisi çeker** ve işlediğini ayrı bir çağrıyla onaylar.

*(İki revizyon, 16.08.2026: ilk hâli «bağımsız bir posta geçidi servisi» diyordu, ikincisi «eğitim iletir». İkisinin de gerekçesi aşağıdaki ölçümlerle değişti.)*

**Ölçülmüş kısıt (bunun için var):** Postmark'ta gelen posta alan adı **tüm Postmark genelinde benzersizdir ve bir sunucuya/akışa bağlıdır** — *"Inbound domains are unique across Postmark and are Stream-specific"* (resmî doküman, 15.08.2026'da okundu). Bir sunucunun tek gelen alan adı ve tek kancası olur. Yani **tek adres = tek kanca**: adres paylaşılıyorsa mektubu ilk alan yer tektir ve o yerin ne olduğu bir karardır.

**Neden ayrı bir servis DEĞİL** *(revizyon, 16.08.2026)*: ilk karar «eğitim bakımdayken gözetimin postası birikir» gerekçesiyle bağımsız bir servis istiyordu. Ölçüldü ve gerekçe zayıf çıktı: **Postmark, 200 almadığı bir gelen posta kancasını yeniden dener** — 1 dk, 5 dk, 10 dk (üç kez), 15 dk, 30 dk, 1 sa, 2 sa, 6 sa; yaklaşık on saat, ve mektup kaybolmaz. Deploy penceremiz dakikalarla ölçülüyor. Yani birikme yoktu, en fazla bir dakikalık gecikme vardı — bunun için ayrı bir depo, ayrı bir deploy hattı ve ayrı bir izleme yükü almak, olmayan bir soruna karşılık gerçek bir maliyet ödemekti *(Sets: «bence eğitim yapsın bu işi»)*.

**Neden «her ürün kendi etiketine baksın» değil:** çünkü mümkün değil. Ölçüldü: **bir gelen posta akışında yalnız TEK kanca adresi olabilir** (Postmark dokümanı: *"You can only have one inbound webhook URL per Inbound Message Stream"*). Postayı alan tek bir yer vardır; orası kendine ait olmayanı görmezden gelirse, o mektup hiçbir yere ulaşmaz.

**Neden İTMEK değil, ÇEKMEK** *(Sets, 16.08.2026)*: itme modelinde eğitim, hedefin sağlığından sorumlu olur — yeniden deneme, geri çekilme, ölü mektup kutusu, «gözetim üç saattir cevap vermiyor» alarmı, hepsi eğitimin sırtına biner. Çekme modelinde eğitimin tek yükümlülüğü **ucu açık tutmak**; hedefin ayakta olup olmadığını bilmesi gereken tek taraf, çeken taraftır.

⛔ **Alma ile işleme ayrıdır.** Tüketici mektubu çeker, kendi tarafına yazar ve ANCAK ONDAN SONRA ikinci bir çağrıyla «aldım, işledim, kaydettim» der. Onay gelene kadar mektup bekliyor kalır: hat koparsa, tüketici çökerse ya da kaydı yazarken hata alırsa mektup kaybolmaz, sonraki çekişte yine gelir.

⛔ **Her tüketici yalnız KENDİ önekini görür.** Uç, çağıranın kimliğini doğrular ve o kimliğe bağlı öneki uygular. Bu bir filtre değil bir yetki sorusudur: aksi hâlde gözetim, compliance'ın müşteri yazışmasını okur.

⚠ **Aynı mektup iki kez gelebilir** ve bu bir kusur değil, «en az bir kez» teslimin doğal sonucudur: onay çağrısı kaybolursa mektup bekliyor kalır ve tekrar verilir. Tüketici, mektup kimliğine bakıp tekrar yazmayacak şekilde kurulur — Postmark'ın gelen posta dokümanı da kendi kancası için aynı şeyi söyler.

⚠ Kabul edilen bedel: eğitim, diğer ürünlerin postasını **saklayan** taraf olur. Onaylanmamış mektuplar orada birikir ve bir tüketici uzun süre gelmezse bunu görecek olan da eğitimdir.

**Neden ayrı alt alan adı değil:** teknik olarak daha basitti (her ürün kendi sunucusu, hiç temas yok) ve Sets tek adresi seçti: takip sistemi tek olsun, müşteri hangi üründen gelirse gelsin aynı adresle yazışsın.

**Sonuçları — hepsi bağlayıcı:**

1. ⛔ **Referans önekleri ürünü söylemek zorundadır.** Bugünkü sözlük söylemiyor: `PL` `PR` `EG` `TK` `TLP` `FT` `GV` `MS` (`app/Domain/Shared/ReferencePrefix.php`). İki ürün de teklif için `TK` kullanırsa `TK-2026-7K4F` iki ayrı kayıt olur ve **geçit hangi ürüne ileteceğini bilemez**. Önek şeması, ikinci ürün ilk mektubunu göndermeden önce kararlaştırılır.
2. ⛔ Geçit **hiçbir mektubu düşürmez**. Tanımadığı bir önek, ulaşılamayan bir ürün, biçimi bozuk bir kod — hepsi saklanır ve yeniden denenir. AD-20'nin "hiçbir şey atılmaz" kuralı geçitte de geçerlidir; bir webhook'un sessizce attığı mektup, görülemeyen tek arıza türüdür.

   ⛔ Mektup, **onaylanana kadar** bekliyor sayılır. Eğitim hiçbir şeyi «gönderdim» diye kapatmaz; kapatan, işlediğini söyleyen taraftır.
3. ⚠ Geçit **karar vermez**: yalnız iletir. AD-20 §1 aynen geçerli — gelen posta hiçbir iş kuralını tetiklemez.
4. ⚠ Tek Postmark sunucusu demek, **teslim istatistiklerinin ve gönderen itibarının ortak** olması demektir. Bir ürünün ölü adreslere gönderimi, diğerinin davet ve şifre sıfırlama postalarını da yavaşlatır. `mail.undeliverable_domains` koruması (EGT-162) bu yüzden ürün başına değil, ortak kuraldır.
5. ⚠ Gönderen adresi de aynı alan adındandır: her ürün kendi kayıt kodunun adresinden gönderir (`TK-2026-7K4F@teklif.3egrp.com`). Alan adı doğrulaması bütün adresleri kapsar, ürün başına kurulum gerekmez (EGT-163'te ölçüldü).

**Kapanış adresi — kapandı (EGT-168, 16.08.2026):** uç yazıldı. Üç yol imzalı istek arkasında: `GET /api/posta/bekleyenler`, `GET /api/posta/{mektup}/ek/{ek}`, `POST /api/posta/{mektup}/islendi`. Kimlik bir HMAC imzasıyla kanıtlanır (başlıkta, adreste değil), zaman damgası penceresi tekrar oynatmayı keser, karşılaştırma sabit zamanlıdır ve her sorgu **imzanın çözdüğü tüketiciyle** daraltılır. Kod: `app/Support/Handoff/*`, `app/Http/Middleware/VerifyHandoffSignature.php`, `app/Http/Controllers/Handoff/InboundMailHandoffController.php`, `app/Actions/Correspondence/{ListWaitingHandoffs,AcknowledgeHandoff,FindHandoffAttachment}.php`. Testler: `tests/Feature/Entegrasyon/InboundMailHandoffTest.php`.

⛔ **Tüketici tarafını yazan referans dokümanı okur: `docs/reference/urunler-arasi-posta-api.md`.** İmzalanan metin, hata kodları, kurulacak çekme döngüsü, sır yönetimi ve asgari test listesi oradadır — bellekten yazılan bir istemci `401` alır ve sunucu nedenini bilerek söylemez.

Geriye kalan iki iş, ikinci ürün ilk teklifini göndermeden önce yapılır: **önek şemasını kararlaştırmak** (bugünkü sözlük ürünü söylemiyor) ve tüketici tarafında **çekme döngüsünü kurmak** (GZT-1).

⚠ Aynı desen üçüncü ürün için de geçerlidir (compliance, 16.08.2026'da konuşuldu): yeni ürün eklemek, ona bir kimlik ve bir önek vermek demektir — eğitimde yazılacak yeni bir kod yolu değil.
