# Review — Fatura belgesi implementasyonu (EGT-222, commit `d3b82827`) — 2026-10-20

> **Review type:** impl-review
> **Previous review:** `docs/reviews/review-fatura-belgesi-plan-re-review-2026-10-20.md` (plan v1.1 ✅)
> **Upstream document(s) read:** `docs/plans/fatura-belgesi-plan.md` v1.1 (F-11'e uygulama sırasında eklenen düzeltme notu dahil), `proje-haritasi.md` §4.4 fatura belgesi kararları (3b dahil), `design/39 Fatura Belgesi.dc.html` (önceki turda tamamı; bu turda varyant satırları yeniden), `docs/reviews/tasarim-39-kapsama.md`
> **Other cross-referenced documents:** `CLAUDE.md`, EGT-219 kodu (`IssuingCompanies`, `issuer_snapshot`), `InvoicePolicy.php`, `PortalScreen::fromToken`, `AccessLogger.php`, `tests/Arch/LocaleTest.php` (arch-allow mekanizması)
> **Stage locks in effect:** Spec yok (Sets kararı) + Plan v1.1 locked

**Doğrulama koşuları (bu oturumda, benim tarafımdan):** TeamCity **#1823 SUCCESS — 2719 test geçti** (origin/main = `ab61553f`, incelenen kod dahil — push doğrulandı). Kuruş tutarlılığı iddiası `php -r` ile deterministik olarak yeniden üretildi (aşağıda F-1). Playwright 4/4 ve lokal tam koşular developer beyanıdır; browser spec'inin içeriği okundu, CI Playwright koşmaz.

---

## Upstream Coverage Matrix (plan F-1…F-16 + görevler/testler)

| Plan maddesi | Durum | Kanıt |
|---|---|---|
| F-1 müşteri fatura kolonları (adres, il/ilçe FK, posta kodu CHECK) | ✓ | migration `2026_11_02_000100` — kolonlar, FK'ler, `customers_billing_postal_code_shape` CHECK `^[0-9]{5}$`; yalnız ekleme, `down()` var |
| F-2 «Fatura bilgileri» bölümü + zorunlu değil + eksikse uyarı | ⚠ | `CustomerForm.vue` (CascadeSelect + TextareaInput, hazır bileşenler), `BillingFieldRules` (ilçe-il tutarlılığı `Rule::exists(...parent_id)`), `Customer::hasBillingDetails()`, faturalar ekranı `billing_complete` (`InvoiceController::index`) + test; **düzenleme/dönüşüm yollarının testi yok** → F-R3 |
| F-3 alıcı kopyası şifreli, eski faturada güncel kayıt | ✓ | `Invoice` cast `encrypted:array`; `InvoiceRecipient::of`; `IssueInvoice.php` kesimde kopya; eski fatura `recipientNow` `TenantContext::run` içinde; testler: ham kolonda VKN yok (167), adres değişince eski adres basılır (170-179), kopyasız fatura (182-192) |
| F-4 KDV tanım/paket başına, backoffice formları | ✓ | migration (decimal(5,2), CHECK 0–100 ×3 tablo); `TrainingDefinitionForm.php:149-156`, `ContentPackageResource.php:191-198`, etiketler `lang` üzerinden |
| F-5 teklif kalemi oranı dondurur | ✓ | `AddQuoteItem.php` (`$training->definition->vat_rate ?? $quote->vat_rate`), `AddPackageToQuote.php` aynı; `QuoteVatTest` + `InvoiceDocumentTest:119-139` (20/10 donuyor) |
| F-6 `QuoteTotals` oran başına, tek oranda aynı sonuç | ✓ | `QuoteTotals.php:127-157` — pay dağıtımı net oranında, **kalan kuruş son kaleme** (Σbase = net_total garantisi); `QuoteVatTest` eşitlik testi + dağılım + tasarımın kendi örneği (38.250/7.650, 12.000/1.200) |
| F-7 teklif çıktısı (37), portal teklifi, `QuoteDocument` oran satırları | ✓ | `Quotes/Printout.vue:66-78` (>1 oranda satır başına, tek oranda eski satır — Sets 3b birebir), `Quotes/Show.vue:230-237`, `QuoteDocument::summaryLines` dondurulmuş gövdeye oran satırları, `MyQuotes::grossTotalOf` kalem oranını geçiyor (portal ekranı zaten yalnız donmuş gövde + brüt gösterir) |
| F-8 fatura kalem oranı tekliften; serbest kalem genel oran; `vat_total` grupların toplamı | ✓ | `IssueInvoice.php` kalem `vat_rate`, serbest satır `settings->vatRate()`; kesimde `vat_total` = Σgrup ✓ test `InvoiceDocumentTest:138,151-158` |
| F-9 kalemin ikinci satırı kesimde kopya | ✓ | `IssueInvoice::detailOf` («tarih · il · fiyat türü» / paket; serbest boş), `invoice_items.detail`; test 141-158 |
| F-10 indirim 37'deki gibi toplamlarda | ⚠ | `InvoiceDocument.vue:254-271` Liste/İndirim/Ara toplam, indirim yoksa basılmaz ✓; **ancak oran satırları yeniden türetiliyor ve kuruş çelişkisi üretebiliyor** → F-R1 |
| F-11 ofis + portal rota, policy, 404, antet route'u yetkili | ✓ | `routes/office.php:713-717`, `routes/panel.php:503-507`; ofis `InvoiceController::document/letterhead` (policy view + printable, 404), portal `fromToken` + Gate + printable 404; testler: yetkisiz 404, taslak 404, yabancı firma 404, bozuk token 404, antet yetkisi (312-360) |
| F-12 TCKN açık + her açılışta erişim logu, kiracı = faturanın müşterisi | ⚠ | `InvoiceDocument.php:57-62` `TenantContext::run([customer_id])` içinde `AccessLogger::record(..., 'recipient_national_id')`; ofis testi tam (1 satır, 272-297), firma alıcıda yok (299-310); **portal açılışının log testi yok** → F-R2 |
| F-13 hâller (iptal kutu+damga+ayak, yerine kesilen, numarasız) | ✓ | `InvoiceDocument.vue:147-159, 182-184, 198, 314-316`; sorgu `replaced_by/replaces/cancelled_*`; testler 241-270 |
| F-14 çok sayfa (thead tekrar, satır bölünmez, devam başlığı, sayfa n/m) | ⚠ | kod var (`invoice.css:369,399-420`, `@page invoice` + `:first`, organizma 119-141); **çok sayfalı belgede hiç doğrulanmadı** — planın «ilk doğrulanacak teknik nokta» dediği şey → F-R4(a) |
| F-15 düğmeler (ofis detay, portal satır+kart, taslakta pasif + açıklama) | ✓ | `Invoices/Show.vue` (pasif + «yazdırılmaz» metni), `Portal/Invoices.vue` satır ve dar-ekran kartı token'lı bağlantı |
| F-16 antetsiz sade başlık | ✓ | `InvoiceDocument.vue:170-175`, `.invoice-head--plain`; test `has_letterhead=false` (222) ve antetli hâl (312-323) |
| Görev 6: yardım + changelog + harita + uyarı | ✓ | 4 yardım JSON + `musteri-faturalar.png` yeniden alınmış, `changelog/2026-10-20.json`, `proje-haritasi.md` durum satırı |
| Rollout (eski kayıtlar değişmez) | ✓ | null oranlar → teklifin/genel oran (`?? $vatRate`), kopyasız fatura güncel kaydı okur; `IssuingCompanyTest` ek |

**Zorunlu üç tarama:** (1) 17 kırmızı çizgi tek tek gezildi — ihlal yok: tenant testleri var, `SET LOCAL` yalnız `TenantContext` üzerinden, `env()` eklenmedi, credential yok, KDV oranları admin-tanımlı (AD-08 ✓), migration yalnız ekleme, hard delete yok, erişim logu okuma anında senkron, commit'te AI attribution yok, CascadeSelect/TextareaInput hazır bileşen, mevzuat kuralı yok (KDV ticari parametre). (2) Wire: rotalar kayıtlı, düğmeler iki panelde bağlı, uçtan uca feature testleri HTTP üzerinden geçiyor. (3) Dil: kod İngilizce; migration ve `detailOf`'taki Türkçe alıntılar `arch-allow: turkish-content` işaretli (mekanizma `LocaleTest.php:38`'de gerçek); kullanıcı metinleri `lang/tr+en` çiftiyle; `$t` iki girişte de kayıtlı (`office.js:55`, `portal.js:56`).

**Bilinçli sapma — bulgu DEĞİL:** plan F-11 portal rotasını «model binding» diye adlandırmıştı; planın örnek verdiği `/teklifler/{quote}/yazdir` rotasının kendisi `OpaqueId` token'lı çıktı (`QuoteController.php:84,216,362` → `PortalScreen::fromToken`). Uygulama planın bağlayıcı özünü («portalın mevcut kalıbı», `InvoicePolicy::view` müşteri dalı, 404, yalnız issued/cancelled) birebir uyguluyor; düzeltme notu plana (F-11 ⚠ notu) ve haritaya işlenmiş.

---

## ✅ What works

- **Finansal çekirdek sağlam ve test edilmiş:** `QuoteTotals`'ın pay dağıtımında kalan kuruşun son kaleme verilmesi Σmatrah = net garantisini kuruyor; tek oranlı teklifin eski sonuca eşitliği eski formülle bire bir test ediliyor; tasarımın kendi örneği (38.250→7.650, 12.000→1.200, 59.100) test sabiti.
- **Kesildiği gibi basılır iki yönde tamam:** issuer (EGT-219) + recipient (şifreli; ham kolonda VKN'nin görünmediği ve adres değişiminin belgeyi değiştirmediği ayrı ayrı assert ediliyor).
- **Erişim güvenliği dar ve 404'lü:** iki panelde de policy + printable; antet route'u da aynı kapıdan; yabancı firma / bozuk token / taslak / yetkisiz hepsi 404 testli. `invoices` row-secured olmadığı için policy'nin tek kapı olduğu yorumda da açık.
- **Kırmızı çizgi 13 doğru uygulanmış:** şahıs TCKN'si belgede açık (Q-3) ama her ofis açılışı alıcının kendi kiracısındaki erişim loguna tek satır düşüyor; firma alıcıda log yok — ikisi de testli.
- CI otoritesi yeşil: **#1823 SUCCESS, 2719 test** (bu review'da tetiklendi ve beklendi).

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

**F-R1 — Belgenin oran satırları kesim anındaki rakamlar değil; yeniden türetme kuruş çelişkisi üretiyor (ÇALIŞTIRILARAK DOĞRULANDI).** `InvoiceDocument::totalsOf` (`app/Queries/Office/InvoiceDocument.php:139-156`) matrah/KDV satırlarını saklanan `line_total`'lardan yeniden türetiyor; oysa `IssueInvoice::discountOn` kalem indirimini 2 haneye yuvarlanmış yüzdeden yeniden hesapladığı için Σ`line_total` ≠ `invoices.net_total` olabiliyor (tutar-tipi kalem indirimi; ürün bunu destekliyor). Yeniden üretilen örnek: 1.000,00 TL'lik kaleme 55,56 TL kalem indirimi, %20 KDV → kesimde `net_total` 944,44 / `vat_total` 188,89 / `gross_total` 1.133,33; belge ise «Ara toplam ₺944,44 · KDV %20 ₺188,88 · GENEL TOPLAM ₺1.133,33» basıyor — **944,44 + 188,88 = 1.133,32 ≠ 1.133,33**, çok oranlı durumda matrah satırı da Ara toplam'la çelişir. Sınıfın «give back exactly the shares the invoice was issued with» iddiası (`InvoiceDocument.php:30-32`) bu durumda yanlış; mevcut testler yalnız tam bölünebilen sayılarla geçiyor. Resmî belgenin kendi aritmetiğinin tutmaması, kodun başka yerde kendi deyişiyle «müşterinin muhasebecisinin telefon açtığı» hatadır.

**F-R2 — Plandaki «şahıs alıcıda erişim log satırı (ofis ve portal)» testinin portal yarısı yok.** `InvoiceDocumentTest.php:272-297` yalnız ofis açılışını doğruluyor; portal belge testi (334-360) firma alıcıyla. Şahıs alıcının kendi portal açılışında logun `CustomerUser` aktörüyle ve doğru kiracıda yazıldığı kanıtsız (plan test listesi, satır 80).

**F-R3 — Plandaki «fatura bilgileri ekleme/düzenleme/dönüşümde kaydedilir» testinin düzenleme ve dönüşüm yolları yok.** Tek test ekleme yolu (`InvoiceDocumentTest.php:362-397`). `UpdateCustomer` ve `ConvertPotentialCustomerRequest` kodda fatura alanlarını alıyor ama iki yolun da kanıtı yok (plan test listesi, satır 79).

**F-R4 — Tasarım kapsama matrisinin cevapsız ⚠/❌ satırları (Onay Kuralı 6: cevapsız matris kapanmaz; sapma yalnız Sets onayıyla).** (a) Çok sayfalı belgede devam başlığı ve her sayfada «Sayfa n / m» — planın F-14'te «ilk doğrulanacak teknik nokta» diye işaretlediği şey — yalnız **tek sayfalık** belgede doğrulandı (matris satır 27). (b) Tasarım 39:105'teki «Alıcı: faturayı kesen şirket» satırı basılmıyor; ilk yarısı belge içeriği olabilir (matris ❌, satır 33). (c) Baskıda yasal ibare tasarımın dibe sabitli ayağında değil, son içeriğin altında akıyor (satır 36). (d) İptal damgasında «yer yoksa yalnız üst kutu» davranışı yerine damga sonraki sayfaya taşıyor (satır 39). Dördü de developer tarafından dürüstçe işaretlenmiş ama Sets'in cevabı alınmadan kapanamaz.

*Not (bulgu değil, biçim):* `QuoteTotals.php:109-116`'daki docblock `percentageOf`'a aitken artık `vatByRate`'in üzerinde duruyor — iki bitişik docblock'tan ilki yanlış metodu anlatıyor.

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

1. F-R4'ün dört satırının cevabı Sets'te: devam başlığı çok sayfalı gerçek bir faturayla doğrulanacak mı; «Alıcı: …» satırı belge içeriği mi tasarımcı notu mu; baskıda yasal ibarenin yeri; damganın yer-yoksa davranışı.
2. F-R1'in çözüm yönü bir karar içeriyor (kesim anındaki oran kırılımının saklanması vs. türetmenin tutarlılaştırılması) — hangisi seçilirse seçilsin Σsatır = GENEL TOPLAM değişmezi testle kilitlenmeli (karar developer'ındır; burada yalnız değişmez şartı not edilir).

## ⚠️ Upstream Findings

Yok — plan ve kararlar sahada doğru çıktı; F-11'in «model binding» ifadesi fiilî kalıpla düzeltildi ve belgelendi (bulgu değil, kayıt).

## Verdict: ❌ REJECTED

Matriste üç ⚠ (F-10/F-12/F-14 üzerinden F-R1, F-R2, F-R4) ve plan test listesine karşı iki eksik (F-R2, F-R3) var; F-R1 çalıştırılarak doğrulanmış bir finansal tutarlılık hatasıdır ve tek başına red sebebidir. Geri kalan gövde — dondurma zinciri, şifreli kopya, erişim kapıları, erişim logu, hâller, dil ve kırmızı çizgiler — sağlam; CI #1823 yeşil (2719 test). Bulgular dar: biri hesap/saklama, ikisi test, biri Sets'e soru.
