# Fatura belgesi — implementasyon planı

*v1.1 · 20.10.2026 · EGT-222 · Tasarım: `design/39 Fatura Belgesi.dc.html` · Brief: `docs/design/fatura-belgesi-brief.md` · Kararlar: `proje-haritasi.md` §4.4 «Şubeler» → «Fatura belgesi (tasarım 39) için kararlar» (Sets, 20.10.2026) · Önceki: EGT-219 (3E şirketleri, sıralı numara, `issuer_snapshot` — canlıda)*

**Hedef:** kesilmiş ve iptal edilmiş faturanın A4 belgesi (ofis + müşteri portalı), tasarım 39'daki gibi; belgenin istediği üç veri eksiği kapatılır: müşterinin fatura bilgileri, kalem bazında KDV, kalemin ikinci satırı.

**Spec yok** (Sets: kararlar alınmış; kısa plan + heyet, sonra kod).

---

## Doğrulanmış mevcut durum (20.10.2026, koddan)

| Ne | Durum | Kanıt |
|---|---|---|
| Müşteri vergi bilgisi | `customers.tax_office`, `tax_number`, `national_id` (şifreli, gizli); **adres kolonu yok** — adres yalnız lokasyonlarda (`customer_locations.address`, il/ilçe option) | `Customer.php:45-89`, `CustomerLocation.php:23-30` |
| Müşteri formu | tek organizma `organisms/CustomerForm.vue` (ekleme + düzenleme + potansiyel müşteri dönüşümü); vergi alanları formun ortasında, bölümsüz | `CustomerForm.vue:188-240`, `CreateCustomerRequest`/`UpdateCustomerRequest` |
| KDV | **tek genel oran** (`Settings::vatRate`), teklife açılışta `quotes.vat_rate` olarak kopyalanır; `QuoteTotals::of(lines, discount…, vatRate)` tek oranla hesaplar | `QuoteTotals.php`, `CreateQuote.php`, 9 çağıran (`SalesQuotes`, `QuotePrintout`, `QuoteDocument`, `QuoteWorkingCopy`, `MyQuotes`, `TrainingRevenue`, `InvoiceableQuotes`, `OfficeUserCockpit`, `AddPackageToQuote`) |
| Fatura kalemi | `invoice_items(description, quantity, unit_price, line_total, vat_rate)`; `description` kesimde yalnız eğitim/paket adı; `vat_rate` hep faturanın tek oranı | `IssueInvoice.php:128-175` |
| Fatura kesen şirket | kesimde `issuer_snapshot` jsonb (unvan, vergi, adres, telefon, seri, banka, ibare, antet id) | EGT-219 A5 |
| Alıcı | faturada kopya **yok**; belge bugün olsa müşterinin güncel kaydını okurdu | — |
| İndirim | kalem + teklif indirimi faturanın `line_total`/`net_total`'ına işlenmiş; teklif çıktısı (37) toplamlarda tek «İndirim» satırı basar | `Quotes/Printout.vue:48-66` |
| Belge | yok; teklif çıktısı en yakın akraba (Inertia sayfası + tarayıcı yazdırma, antet route'tan) | `Office/Quotes/Printout`, `QuoteController::printout/letterhead` |

---

## Kararlar (teknik)

| # | Karar | Gerekçe |
|---|---|---|
| F-1 | **Müşteri fatura bilgileri** `customers` tablosuna: `billing_address` (text), `billing_city_option_value_id`, `billing_district_option_value_id` (lokasyonla aynı il/ilçe listeleri), `billing_postal_code` (5 hane, CHECK). Vergi dairesi/no ve T.C. kimlik no mevcut kolonlarıyla aynı bölüme taşınır. | Sets: «fatura bilgileri lokasyondan seçilmez, müşteri eklenirken bir bölüm olur». Ayrı tablo değil: bir firmanın bir fatura kimliği vardır. |
| F-2 | `CustomerForm.vue` içinde **«Fatura bilgileri»** başlıklı bölüm: vergi dairesi + vergi no (tüzel) / T.C. kimlik no (şahıs) + adres + il/ilçe (mevcut `CascadeSelect`) + posta kodu. Alanlar zorunlu değil; eksikse fatura belgesinde boş satır basılmaz, faturalar ekranı «fatura bilgisi eksik» uyarısı gösterir (kesimi engellemez). | Mevcut parçalarla; ekleme ve düzenleme aynı organizma. Engellememek: canlıdaki müşterilerin hiçbirinde adres yok. |
| F-3 | **Alıcı kopyası:** kesimde `invoices.recipient_snapshot` (şifreli cast, `encrypted:array`) — ad, hukuki tür, vergi dairesi/no, T.C. kimlik no, adres, il/ilçe adları, posta kodu. Belge kopyadan basar. Eski faturalarda null → belge müşterinin güncel kaydını okur ve bunu bilir. | Şirket kopyasıyla aynı ilke (spec §9.2: kesildiği gibi basılır). Şifreli: T.C. kimlik no içeriyor (AD-16). |
| F-4 | **KDV oranı tanım ve paket başına:** `training_definitions.vat_rate`, `content_packages.vat_rate` (nullable decimal(5,2), CHECK 0–100); boşsa genel oran. Backoffice formlarında alan. | Sets kararı. |
| F-5 | **Teklif kalemi oranı dondurur:** `quote_items.vat_rate` (nullable); `AddQuoteItem`/`AddPackageToQuote` kalem eklenirken tanımın/paketin oranını (yoksa teklifin `vat_rate`'ini) yazar. Mevcut kalemler null → teklifin oranı. | Tanımın oranı sonradan değişse de gönderilmiş teklif değişmez (M-35 gerekçesi). |
| F-6 | **`QuoteTotals` kalem bazında KDV:** satır dizisi isteğe bağlı `vat_rate` taşır (yoksa parametredeki oran); teklif indirimi kalemlere net tutarları oranında dağıtılır, KDV oran grubu başına hesaplanır; dönüşe `vat_by_rate` (oran → matrah, KDV) eklenir, `vat_total` onların toplamı. Tek oranlı teklifte sonuç bugünküyle **aynı** (test). Dokuz çağıran satıra `vat_rate` ekler. | Tek hesap yeri korunur; geriye dönük uyum. |
| F-7 | Teklif belgesi (37) ve müşteri portalı teklifi: toplamlarda oran başına KDV satırı (tek oranda bugünkü tek satır). Gönderilmiş sürümün dondurulmuş belgesi (`QuoteDocument`) oran satırlarını taşır. | ⛔ Sets, 20.10.2026 (heyet 824 F-R4 sorusu): «oran başına satır» — müşteri teklifle faturayı aynı hesapla görür. |
| F-8 | **Fatura:** kalem `vat_rate` teklif kaleminden (yoksa teklifin oranı); serbest kalem genel oran. `invoices.vat_total` oran gruplarının toplamı. | F-6'nın faturadaki karşılığı. |
| F-9 | **Kalemin ikinci satırı:** `invoice_items.detail` (nullable string) kesimde kopyalanır — eğitim kalemi: «gg–gg.aa.yyyy · il · fiyat türü»; paket kalemi: «içerik paketi · süre/sürüm» (paketin taşıdığı kadar); serbest kalem boş. | Tasarım 39 B4; Sets: «ihtiyacın varsa ekle». |
| F-10 | **İndirim belgede** teklif çıktısındaki gibi: toplamlarda «Liste tutarı · İndirim −₺ · Ara toplam (KDV hariç)»; indirim = Σ(miktar × birim fiyat) − net. İndirim yoksa satırlar basılmaz. | Sets: «37'deki gibi, sen ekle». Kalem satırında indirim sütunu yok (tasarımda yok). |
| F-11 | **Belge sayfası:** ofis `GET /muhasebe/faturalar/{invoice}/belge` (`InvoicePolicy::view`), portal `GET /faturalar/{invoice}/belge` — portalın mevcut kalıbıyla (`/teklifler/{quote}/yazdir`, `routes/panel.php:395`) ⚠ *uygulamada düzeltme (20.10.2026): o kalıp model binding değil, `OpaqueId` token'ı (`PortalScreen::fromToken`); belge de token'la açılır, liste `token` taşır*, `InvoicePolicy::view` müşteri dalı (kendi kiracısı + `customer.view_invoices` + taslak değil, `InvoicePolicy.php:41-48`); bağlanamayan ya da reddedilen 404. Yalnız `issued`/`cancelled`. Ortak sorgu `InvoiceDocument::for(Invoice)`; Inertia sayfası tarayıcı yazdırma yolu (teklif çıktısı gibi), antet görseli kopyadaki şablon id'sinden yetkili route ile. | Tasarım 39 «Ekranda açılış». |
| F-12 | **T.C. kimlik no** belgede açık; şahıs alıcının belgesi her açılışta erişim loguna yazılır (`AccessLogger`, ofiste kiracı = faturanın müşterisi; portalda kendi kiracısı). Ekranlarda maskeli kalır. | Spec §9.1, AD-21, kırmızı çizgi 13. |
| F-13 | **Hâller:** iptal — üstte kutu (tarih, gerekçe, varsa yerine kesilenin numarası) + saydam damga + ayakta «İPTAL»; yerine kesilmiş — başlıkta «… numaralı faturanın yerine düzenlenmiştir»; numarasız eski kayıt — «numarasız (eski kayıt)». İptal belgesi de not/banka/ibare dahil tam içerikle basılır. | Tasarım 39 Hâl 2, B2 varyantları. |
| F-14 | **Çok sayfa:** `thead` tekrar (`table-header-group`), satır bölünmez, 2. sayfadan itibaren ince devam başlığı ve her sayfada «Sayfa n / m» — `@page` kenar kutularıyla (Chrome); desteklenmeyen tarayıcıda devam başlığı yerine yalnız tekrar eden tablo başlığı kalır (bilinçli). | Tasarım 39 B4. ⚠ İlk doğrulanacak teknik nokta. |
| F-16 | **Antet yüklenmemişken** (kopyada `letterhead_template_id` null — `IssueInvoice::letterheadOf` null dönebilir): tasarım 39 B1 varyantı — görsel alanı çizilmez, logo uydurulmaz; unvan solda kalın, künye (vergi dairesi · VKN · adres · telefon) sağda; alt çizgi aynı. | Tasarım 39 «B1 — antet yüklenmemişken», teklif çıktısının (37) sade başlık kuralı. |
| F-15 | Düğmeler: ofis fatura detayında ve portal fatura satırında «Yazdır / PDF» (yeni sekme). Taslak fatura üreten yol bugün yok; düğmenin pasif hâli yine uygulanır. | Tasarım 39. |

---

## Veri modeli

| Tablo | Ek |
|---|---|
| `customers` | `billing_address` text, `billing_city_option_value_id` FK, `billing_district_option_value_id` FK, `billing_postal_code` char(5) CHECK `^[0-9]{5}$` |
| `training_definitions`, `content_packages` | `vat_rate` decimal(5,2) nullable, CHECK 0–100 |
| `quote_items` | `vat_rate` decimal(5,2) nullable |
| `invoice_items` | `detail` varchar(255) nullable |
| `invoices` | `recipient_snapshot` text nullable (şifreli JSON) |

⚠ Data-loss yok: yalnız ekleme. `customers` RLS'li; yeni kolonlar mevcut politikaya girer.

---

## Görevler

1. **Migration + modeller** (F-1, F-4, F-5, F-9, F-3 kolonları); fillable/cast; `Customer` fatura bilgisi okuyucusu.
2. **Müşteri fatura bilgileri** (F-2): request kuralları (ekleme, düzenleme, dönüşüm), `OnboardCustomer`/`UpdateCustomer`/`NewCustomer`, `CustomerForm.vue` bölümü, `CustomerDetail` + müşteri kartında gösterim, audit (mevcut müşteri güncelleme olayı).
3. **KDV** (F-4…F-8): backoffice tanım ve paket formlarında alan; `AddQuoteItem`/`AddPackageToQuote` dondurma; `QuoteTotals` + dokuz çağıran; teklif ekranı, çıktısı, portal teklifi ve `QuoteDocument` oran satırları; `IssueInvoice` kalem oranı.
4. **Alıcı kopyası ve ikinci satır** (F-3, F-9) `IssueInvoice` içinde.
5. **Belge** (F-10…F-15): `InvoiceDocument` sorgusu, ofis/portal route + controller + policy, `Invoices/Document.vue` (ortak), yazdırma CSS'i, erişim logu, düğmeler.
6. **Yardım + changelog + harita**; faturalar ekranında «fatura bilgisi eksik» uyarısı.

---

## Testler

- `QuoteTotals`: tek oran = bugünkü sonuç (mevcut testler + yeni eşitlik testi); iki oran + teklif indirimi dağılımı; yuvarlama toplamı tutar.
- Teklif kalemi tanımın/paketin oranını dondurur; tanım oranı sonradan değişince teklif değişmez; boş oran genel orana düşer.
- Fatura: kalem oranları tekliften; oran başına KDV ve genel toplam; serbest kalem genel oran; `detail` eğitim/paket/serbest; alıcı kopyası şifreli saklanır (ham kolonda T.C. no görünmez), sonradan müşteri adresi değişince belge eski adresi basar.
- Müşteri formu: fatura bilgileri ekleme/düzenleme/dönüşümde kaydedilir; posta kodu biçimi; il-ilçe tutarlılığı.
- Belge: antet yüklenmemiş şirketin belgesi sade başlıkla (görsel alanı yok); ofiste yetkili açar, yetkisiz/kapsam dışı 404; portalda kendi firmasının faturası açılır, başkasınınki 404; taslak 404; iptal hâli (kutu + yerine kesilen no); yerine kesilmiş satırı; numarasız eski kayıt; şahıs alıcıda erişim log satırı (ofis ve portal), firma alıcıda yok; antet route'u yetki ister.
- Tarayıcı spec'i: belge ofiste ve portalda açılır, konsol hatası yok, 375'te yatay kayar.

---

## Rollout
Migration ekleme; mevcut teklifler ve faturalar değişmez (oranlar null → genel oran). Canlı müşterilerin fatura bilgileri boş başlar; doldurmak satış/muhasebe işidir. Tanım ve paket oranları boş → genel oran (bugünkü davranış).

## Rollback
Kod `git revert`. Migration'lar yalnız ekleme ve `down()` taşır; ⚠ fatura kesildikten sonra `recipient_snapshot` ve `invoice_items.detail` dolduğu için şema geri alınırsa kopyalar kaybolur — o noktadan sonra yalnız kod geri alınır. `quote_items.vat_rate` geri alınırsa kalemler genel orana döner (teklif toplamları değişebilir); bu yüzden oran girildikten sonra şema geri alınmaz.

## Kapsam dışı
e-fatura/e-arşiv gönderimi (AD-12); fatura e-postayla belge eki; yasal ibare metni (şirket kaydında, boş olabilir).
