# Review — EGT-225 implementasyon (`f34c3693`) — 2026-10-21

> **Review type:** impl-review
> **Previous review:** `docs/reviews/review-egitim-belgesi-plan-re-review-2-2026-10-20.md` (plan v1.2 ✓, heyet 839)
> **Upstream document(s) read:** `docs/plans/egitim-belgesi-plan.md` v1.2 (tamamı), `design/52 Egitim Belgesi.dc.html` (tamamı)
> **Other cross-referenced documents:** `CLAUDE.md` (17 kırmızı çizgi), `docs/reviews/tasarim-52-kapsama.md`, `proje-haritasi.md` EGT-225 durum satırı; kod: 109 dosyalık commit'in çekirdeği satır satır okundu (aşağıda kanıtlarla)
> **Stage locks in effect:** Spec + Plan locked; tasarım 52 bağlayıcı

**Bağımsız doğrulama:** TeamCity full run **#1836 SUCCESS — 2794 test geçti** (bu incelemede tetiklendi ve beklendi). Commit mesajında AI attribution yok. Üç zorunlu tarama yapıldı: kırmızı çizgi (17 madde tek tek), wire (rotalar, üç tetikleyici, panel kayıtları), dil (yeni PHP/Vue/lang).

---

## Upstream Coverage Matrix

| Upstream madde | Durum | Kanıt |
|---|---|---|
| B-1 `generated` + yalnız 3E/marka (CHECK + form) | ✓ | migration `2026_11_05_000100:80-81`; form `TrainingDefinitionForm:359-365` (issuerKinds daraltması) |
| B-2 önek (3 krk `[A-Z0-9]`, CHECK, tekil), sayaç, kilitle aynı işlemde, boşluksuz, `InvoiceNumber::format` | ✓ | migration `:35-54`; `CertificateNumbers:29-52` (transaction guard + `FOR UPDATE`); `CertificateSeriesPrefixField` (regex `InvoiceNumber::PREFIX`, çapraz-tablo kuralı, numara alındıysa kilitli); test «gives a number back when the print rolls back» |
| B-3 şablon türü, tanım başına sürüm, indeks daraltma + iki kısmi indeks + CHECK, `certificate_imprint_position` | ✓ | migration `:56-78` plan metniyle birebir; `UploadCertificateTemplate:59-105` (sürüm, eski silinmez, audit) |
| B-4 PNG/tek sayfa PDF, A4 ±%2, `pdftoppm -r <dpi>`, poppler ön koşulu, açık hata | ✓ | `CertificateTemplateFile` (sıra tasarımın hata kartlarınca); `Integrations/Poppler/PdfRasterizer` (AD-25 ✓); `ortamlar.md` ön koşul satırı |
| B-5 üç tetikleyici + tek koşul + elle giriş kapalı | ✓ | `CompleteTraining` (TRIGGER_COMPLETION, işlem içinde), `RecordExamResult` (TRIGGER_EXAM — yeniden sınav), `UploadCertificateTemplate::printWaiting` (TRIGGER_TEMPLATE, firma başına kendi işlemi); `CertificateReadiness` tek kaynak; `RecordCertificate` `generated` tanımda reddediyor; testler 50/117/123/147/197 |
| B-6 durum kolonları, kısmi tekil indeks `status='valid'`, `printed` kopya şifreli | ✓ (bir sapma) | migration `:84-104`; kopya **`TenantEncryptedArray`** (plan: `encrypted:array`) → **F-1(a)**; kopyadan basım `CertificateDocument::for` + test «prints from the copy» |
| B-7 kiracısız doğrulama kaydı, kimlik no yok, aynı işlemde, muafiyet gerekçeli | ✓ | migration `:107-125`; `CertificatePrinter:143-156`; `config/tenancy.php` + `TenancyTest` gerekçeli; audit `printed`'ı dışlıyor (`CertificateRecord:61`, AD-16) |
| B-8 panel+ofis `/dogrula`, giriş zorunlu, üç hâl, her sorgu erişim kaydı, e-posta ayardan | ✓ (bir ekleme) | rotalar `panel.php`/`office.php`; `CertificateVerificationPage` (recordQuery → sahibin kiracısı; **bulunamadıysa okuyanın firması/ofis kaydı** — plan dışı karar → **F-1(d)**, `_000200` migration) |
| B-9 belgede yazanlar + kurallar (İçerik düşer, tek hücre, ad küçülmesi, süresiz satırı) | ✓ | `CertificatePrinter::copyFor`, `CertificateDocument::shape` (`issuer_is_provider`), `TrainingCertificate.vue` (nameSize, line-clamp); fixture ölçümleri (12 geçti) |
| B-10 ortak organizma, şablon arka plan, yetkili rota, iptal/düzeltme görünümü | ✓ | `TrainingCertificate.vue` + `certificate.css`; şablon akışı yetki arkasında (ofis `viewAny Training`, müşteri oturum, backoffice `training.view`) |
| B-11 üç yüzey + «Tümünü PDF» + her açılış erişim kaydı | ✓ | ofis `CertificateController` (FindTraining `view` kapısı), portal `staff` (`customer.manage_participants`), katılımcı `own` (yalnız kendi — test 80); `CertificateDocument::for` her açılışta `AccessLogger::record` (test «writes the access row every time») |
| B-12 şablon yok şeridi + backoffice uyarısı (form kaydedilir) | ✓ | `TrainingCertificates.vue:94`; form `Callout::warning` + test «saves without a template» |
| B-12b üretim audit'i aynı işlemde | ✓ | `CertificatePrinter:158-168` (`training.certificate_generated`: numara, düzenleyen, sürüm, tetikleyici); test satır 80/140 assert ediyor |
| B-13 `certificate.void`, gerekçe zorunlu, yeni numara, silinmez | ✓ | `CorrectCertificate`/`CancelCertificate` (policy `void`, AD-11, audit alan **adları** — değer yok, AD-16); PermissionMatrix + config + lang |
| B-14 backoffice formu (üç kart, önek·son no, geçerlilik, şablon, künye konumu, örnekle önizle) | ✓ | `TrainingDefinitionForm` «Belge» bölümü; önizleme `CertificateTemplateController::preview` (backoffice guard + `training.view`) |
| Veri modeli (7 satır) | ✓ | iki migration birebir; izin kayıtlı |
| Görev 1–5 | ✓ | yukarıdaki satırlar |
| Görev 6 (altyapı/yardım/changelog/harita) | ⚠ | `ortamlar.md` ✓, yardım metinleri ✓, changelog ✓, harita ✓; **canlıya `poppler-utils` kurulmadı, deploy yapılmadı** (bilinçli, açıklandı) → **F-5** |
| Test: önek / şablon indeksleri / numara / üretim / içerik / erişim / düzeltme / doğrulama | ✓ | 29 yeni Pest testi; adlar ve assert'ler plan satırlarıyla eşleşiyor; CI #1836 yeşil |
| Test: «iki eşzamanlı üretim aynı numarayı almaz (kilit)» | ❌ | eşzamanlılık testi yok (grep: concurrent/eşzamanlı boş); kilit kodda var, rollback testi var → **F-3** |
| Test: «pdftoppm yoksa açık hata» | ❌ | `pdf_tools_missing` dalı testsiz (kod dalı: `CertificateTemplateFile:115-117`) → **F-4** |
| Test: tarayıcı «belge (ofis + portal) açılır» | ⚠ | doğrulama + 375 ✓; **ofis belgesi testi atlandı** (`training-certificate.spec.js:58`, dev DB'de basılmış belge yok) → **F-6** |
| Tasarım 52 — eksiksiz uygulama | ⚠ | kapsama matrisi üretilmiş ve gerçek (örneklemle doğrulandı); **4 satır Sets cevabı bekliyor** (10, 12, 32, 38) → **F-2** |

**Kırmızı çizgi taraması (17/17):** ihlal yok. Tenant: portal okumaları kendi kapsamında, doğrulama tablosu bilinçli kiracısız ve TCKN'siz; `SET LOCAL` dışı tenant context yok; her uç yetkili (ofis `view`+`void`, portal izin/sahiplik, backoffice guard+izin); ham sorgular TenantContext içinde ve `status` süzgeçli; migration additive, `down` üretilmiş belgeyi reddediyor (AD-11); `env()` yok; credential yok; mevzuat/iş kuralı gömülü değil (dpi ayar, A4 kâğıt geometrisi, geçerlilik `renewal_months`'tan); test silinmedi; AI attribution yok; üretim/düzeltme/iptal/yükleme audit'li ve TCKN okuyan her açılış erişim kaydında; kod yerleşimi AD-25'e uygun (Poppler → `Integrations/`, FormRequest'ler, sorgular `Queries/`); test disiplini tam (grup, çıplak `now()` yok); analitik/kişi izleme yok. **Dil:** kod İngilizce (Türkçe yalnız `arch-allow` işaretli içerik ve mevcut konvansiyonlar: Türkçe rota yolları, `tests/Helpers/` adları, tarayıcı skip mesajları); UI tamamı `lang/` üzerinden (yeni organizmalarda gömülü metin yok).

## ✅ What works

- **Plan ↔ kod sadakati yüksek ve kanıtlı:** migration planın indeks/CHECK metnini harfiyen uyguluyor; numara boşluksuzluğu mimari olarak doğru (işlem içi `FOR UPDATE`, geri dönen numara testli); üç tetikleyici gerçekten bağlı ve her biri testli.
- **Tek karar kaynağı deseni:** `CertificateReadiness` hem eylemin hem ekran metinlerinin kaynağı — ekranın vaadiyle eylemin reddi ayrışamaz.
- **Önek alanı beklenenden sağlam:** çapraz-tablo tekillik kuralı ve «numara alındıysa önek kilitli» davranışı plan'daki açık soruyu (önek çakışması) form seviyesinde olabilecek en sıkı hâliyle kapatıyor.
- **Kapsam değişikliğinin (e) taraması tam:** `certificate_records` okuyan her nokta tarandı — ham dört sorgu süzüldü, kalan tüm okuyucular Eloquent scope'undan geçiyor (scope `qualifyColumn` ile join-güvenli yazılmış), tarih okuyucuları `allCertificates()` ile bilinçli.

## ⚠️ Findings & Risks

**F-1 — Plan kilitliyken beş uygulama kararı plandan ayrıldı; Sets onayı yok.** Hepsi kodda doğrulandı, hiçbiri güvenlik/tenant/mevzuat riski getirmiyor, ikisi planın gerçek açığını kapatıyor — ama plan bağlayıcıdır ve onay mercii Sets'tir (raporlanmış, henüz onaylanmamış): **(a)** `printed` kopyası `encrypted:array` değil `TenantEncryptedArray` — planın kendi ifadesi AD-05 ile çelişiyordu (`CustomerUser.national_id` zaten `TenantEncrypted`, APP_KEY müşteri kişisel verisi için kapalı); kod doğru tarafta, plan metni aşılmış durumda. **(b)** Asgari çözünürlük plan'da sabitti, kodda iş ayarı (`company.certificate_template_min_dpi`). **(d)** Bulunamayan numaranın erişim kaydı okuyanın firmasına/ofis kaydına yazılıyor; doğrulama satırına `owner_tenant_id` eklendi (`_000200`) — plan yalnız «sahibinin kiracısı» diyordu. **(e)** `VALID_ONLY` global scope planda yoktu; mevcut uyum/yenileme/portal okuyucularının davranışını değiştiren mimari bir karar. **(g)** İptal edilen belge yeniden basılmıyor (`CertificateReadiness::CANCELLED`) — planın «geçerli belgesi yok» koşulu harfiyen uygulansaydı iptal edilen belge bir sonraki tetikleyicide sessizce yeniden basılırdı; kod doğru, plan maddesi eksikti.

**F-2 — Tasarım kapsama matrisinde 4 cevapsız ⚠ satır (10, 12, 32, 38).** Yükleme kutusunun Filament alanı olması, hata kartları yerine alan altı metin, kaçırılan oturum adının kartta olmaması, portalda satır yerine kart. Sorular doğru biçimde yöneltilmiş; ancak tasarım sapması yalnız Sets onayıyla olur ve cevapsız matris kapanmaz (Onay Kuralı 6, Tasarım Uygulama Protokolü).

**F-3 — Planın test listesindeki «iki eşzamanlı üretim aynı numarayı almaz (kilit)» testi yazılmamış.** Kilit mekanizması kodda var ve rollback-boşluksuzluk testi mevcut; eşzamanlılığı fiilen iki bağlantıyla sınayan test yok.

**F-4 — Planın «pdftoppm yoksa açık hata» dalı testsiz.** Kod dalı var (`CertificateTemplateFile:115-117` → `pdf_tools_missing`); hiçbir test bu dalı tetiklemiyor — tam da «sessiz düşmesin» diye plana yazılmış davranış doğrulanmamış durumda.

**F-5 — Görev 6'nın canlı ayağı eksik: `poppler-utils` prod'a kurulmadı, deploy yapılmadı.** Bilinçli ve açıklanmış; ama plan rollout'u ön koşula bağlıyor ve bu yapılmadan iş «tamamlandı» sayılamaz (deploy, teslimin parçası — CLAUDE.md).

**F-6 — Tarayıcı kapsamında ofis belgesi doğrulanmadı.** `training-certificate.spec.js:30` dev veritabanında basılmış belge olmadığı için atlandı; planın tarayıcı satırı «belge (ofis + portal) açılır» der. Fixture ölçümleri organizmayı kapsıyor, ofis sayfası akışı tarayıcıda hiç açılmadı.

## ❓ Open Questions

1. **Yardım ekran görüntüleri:** üç makalenin metni yeni kartı/düğmeleri anlatıyor, görseller yenilenmedi (commit'te `public/images/yardim` değişikliği yok). Yeni öğeler koşullu (yalnız «şablondan üretilir» eğitimde) olduğundan «gerekiyorsa» kuralının kapsamına girip girmediği Sets'in kararı.
2. `company.certificate_contact_email` varsayılanı boş — canlıda doldurulmazsa doğrulama sayfası iletişim adresi basmaz; rollout notlarına girmeli mi?

## Previous Findings Status

Plan re-review 2'nin taşınan üç açık sorusundan ikisi implementasyonda kapandı: katılımcı oturumu `/dogrula`ya erişiyor (rota + yorum, Sets kararının metniyle uyumlu); «bulunamadı» sorgusunun kaydı çözüldü (F-1(d) olarak onaya bağlı). Önek çakışması form + çapraz-tablo kuralı + kilitleme ile kapatıldı.

## Verdict: ❌ REJECTED

Kodun kendisi sağlam: CI #1836 yeşil (2794), kırmızı çizgi ihlali yok, wire tam, plan maddelerinin tamamı kanıtla karşılanmış. RED'in sebebi kod hatası değil, kapanmamış onaylar ve eksik doğrulamadır: beş plan sapması ile dört tasarım sorusu Sets'in kararını bekliyor (F-1, F-2), planın adlandırdığı iki test yazılmamış (F-3, F-4), tarayıcıda ofis belgesi hiç açılmadı (F-6) ve rollout ön koşulu duruyor (F-5). Sets beş sapmayı ve dört tasarım satırını onaylar, iki test yazılır, ofis belgesi tarayıcıda bir kez gösterilir ve canlı ön koşul tamamlanırsa iş kapanabilir.
