# Kontör Bakiye Uyarısı ve E-Adisyon Gönderim Kapısı — Kalan İşler

**Tarih:** 09.09.2026 (plan) · **Uygulama:** 12.09.2026
**Durum:** ✅ Uygulandı (kod tamam, sunucu doğrulaması bekliyor)
**Öncelik:** Yüksek

> **Uygulama özeti:** 4 görevin tamamı kod tarafında bitti. Tüm dosyalar `php -l` ile
> doğrulandı, iki Blade şablonu derlendi, iki komut `schedule:list` içinde görünüyor.
> Kalan iş yalnızca **sunucuda** `config:clear && config:cache` ve gerçek bir test
> e-postası gönderimi (bkz. Bölüm 8).

---

## 1. Bağlam

İşletmeden iki şikayet geldi:

1. Bir işletmenin bakiyesi 100'ün altına düştü ancak bilgilendirme e-postası ulaşmadı.
2. İşletme bakiyesi tükenmesine rağmen e-adisyon oluşturulmaya devam ediyor.

Yapılan kod incelemesinde **6 ayrı neden** tespit edildi:

- **1A** — mail yapılandırılmamıştı → ✅ çözüldü
- **1B** — `> 0` koşulu tükenmiş bakiyeyi eliyor → Görev 1B
- **1C** — kontrol yalnızca tarayıcı açıkken çalışıyor → Görev 1C
- **1D** — gün içi cache bastırması → tasarım gereği, iş olarak ele alınmıyor (bkz. Bölüm 7)
- **2** — gönderimde kredi kapısı yok → Görev 2
- **3** — controller yanlış cache anahtarı okuyor → Görev 3

Bu dokümanın kapsamı kalan **4 görevdir** (1B, 1C, 2, 3).

---

## 2. Tamamlananlar

| #   | Bulgu                                              | Durum      |
| --- | -------------------------------------------------- | ---------- |
| 1A  | `.env` dosyası boştu, mail hiç yapılandırılmamıştı | ✅ Çözüldü |

**Yapılan:** `.env` hem yerelde hem sunucuda güncellendi. SMTP yapılandırması:

- `MAIL_MAILER=smtp`
- `MAIL_HOST=smtp.yandex.com`
- `MAIL_PORT=587`
- `MAIL_ENCRYPTION=tls`
- `MAIL_USERNAME` / `MAIL_PASSWORD` / `MAIL_FROM_ADDRESS` / `MAIL_FROM_NAME` tanımlı

> Kimlik bilgileri bu dokümana yazılmamıştır; yalnızca `.env` içinde tutulmalıdır.

**Yerelde config cache yok** (`bootstrap/cache/` içinde sadece `packages.php` ve `services.php` var), dolayısıyla değişiklikler anında etkili.

### Sunucuda zorunlu adım

Production'da config cache'lenmiş olabilir. Cache'lenmişse `.env` güncellemesi **hiç etkili olmaz**:

```bash
php artisan config:clear && php artisan config:cache
```

### Mail doğrulama testi

```bash
php artisan tinker --execute="Mail::to('thatugur@gmail.com')->send(new App\Mail\CreditBalanceAlert('Test Firması', 42));"
```

E-posta gelirse 1A kesin kapanmış demektir. Gelmezse sorun SMTP kimlik bilgilerinde (Yandex uygulama şifresi veya gönderici adresi yetkisi).

---

## 3. Kalan İşler

### Görev 1B — Sıfır bakiyede uyarı gönderilmiyor ✅ UYGULANDI

**Sorun**

`app/Livewire/CreditBalance.php` L77:

```php
if ($this->balance > 0 && $this->balance <= 100) {
```

`> 0` koşulu, **tamamen tükenmiş (0) bakiyeyi uyarı dışına atıyor.** Bakiye 100'ün üstünden doğrudan 0'a düştüyse — veya 1–100 bandındayken kontrol hiç çalışmadıysa — uyarı hiçbir zaman tetiklenmiyor. Şikayetle birebir örtüşen neden budur.

Ayrıca `> 0` koşulu negatif (aşım/overdraft) değerleri de eliyor.

**Neden bu koşul eklenmişti**

Orijinal amaç "API boş/hatalı döndüğünde gürültü olmasın" idi. Yani asıl ayrım _bakiyenin 0 olması_ değil, _API'nin anlamlı veri döndürmemesi_ olmalı.

**Önerilen çözüm**

0'ı **gerçek tükenme** olarak kabul edip uyarıya dahil etmek; gürültüyü ise yalnızca "veri yok" durumunda bastırmak:

1. `parseCreditData()` içinde bir `$hasCreditData` bayrağı üretilir:
    - API `data` dizisi döndürdüyse → `true` (0 değeri geçerli bir tükenme verisidir)
    - Fallback yoluna (`findCreditInArray`) düşüldüyse → `false` (anlamlı veri yok)
2. Uyarı koşulu şu hale gelir:

```php
if ($this->hasCreditData && $this->balance <= 100) {
```

Böylece:

| Bakiye             | `hasCreditData`        | E-posta        |
| ------------------ | ---------------------- | -------------- |
| 0 (gerçek tükenme) | true                   | ✅ Gönderilir  |
| Negatif (aşım)     | true                   | ✅ Gönderilir  |
| 1–100              | true                   | ✅ Gönderilir  |
| > 100              | true                   | ❌ Gönderilmez |
| Herhangi           | false (API boş/hatalı) | ❌ Gönderilmez |

3. E-posta şablonuna (`resources/views/emails/credit-balance-alert.blade.php`) bakiye 0 veya negatifse ayrı bir vurgu eklenir: "Bakiye tükendi" / "Bakiye aşıldı".

**Uygulanan değişiklik**

- `app/Livewire/CreditBalance.php`
    - Yeni `public $hasCreditData = false;` özelliği eklendi; `loadCreditBalance()` başında sıfırlanıyor (bir önceki polling'den bayat `true` kalmasın diye)
    - `parseCreditData()` artık `MysoftEInvoiceService::hasCreditData()` ve `sumRemainingCredit()` kullanıyor — yerel toplam döngüsü kaldırıldı
    - Uyarı koşulu `if ($this->hasCreditData && $this->balance <= 100)` oldu
- `app/Mail/CreditBalanceAlert.php` — yeni `statusLabel()` metodu; konu önem derecesine göre değişiyor:
    - negatif → `Kontör Bakiyesi Aşıldı - {firma}`
    - sıfır → `Kontör Bakiyesi Tükendi - {firma}`
    - düşük → `Kontör Bakiye Uyarısı - {firma}`
- `resources/views/emails/credit-balance-alert.blade.php` — şablona `@php` önem derecesi bloğu eklendi; tükenmiş/aşılmış bakiyede palet amber'dan kırmızıya dönüyor, mesaj metni duruma göre değişiyor, "Durum" satırı eklendi

**Kabul kriterleri**

- [x] Bakiye 0 olduğunda e-posta gidiyor (kod yolu doğrulandı, gerçek gönderim Bölüm 8'de)
- [x] API veri döndürmediğinde e-posta gitmiyor (`hasCreditData` false → koşul false)
- [x] Negatif bakiyede e-posta gidiyor (`<= 100` negatifi kapsar)
- [x] Gün içi tekrar engeli (cache) korundu — anahtar değişmedi

---

### Görev 1C — Kontrol yalnızca tarayıcı açıkken çalışıyor ✅ UYGULANDI

**Sorun**

`@livewire('credit-balance')` projede **yalnızca bir yerde** kullanılıyor:

- `resources/views/integrations/eadisyon/index.blade.php` L70

`wire:poll.60s` yalnızca o sekme tarayıcıda açıkken tetiklenir. `app/Console/Kernel.php`'de planlanmış tek komut `eadisyon:auto-send` — **bakiye kontrolü yapan hiçbir zamanlanmış görev yok.**

Sonuç: o sayfayı kimse açmadıysa bakiye hiç sorgulanmıyor, eşik kontrolü hiç yapılmıyor, e-posta hiç gönderilmiyor. Uyarı sisteminin çalışması bir kullanıcının tarayıcı davranışına bağlı — bu yapısal bir kusur.

**Önerilen çözüm**

Yeni bir zamanlanmış komut: `app/Console/Commands/CheckCreditBalance.php`

1. İmza: `eadisyon:check-credit {--threshold=100}`
2. Tüm aktif `e-files` entegrasyonlarını tarar (`Integration::withoutGlobalScopes()`, `is_active = true`)
3. Her firma için:
    - `current_team_id` ile `Address::getOfficialAddress()` çağrılır → `vat_number` ve `service_name` alınır
    - `MysoftEInvoiceService::getCreditInfo()` ile bakiye çekilir
    - Bakiye ≤ eşik ve veri geçerliyse `CreditBalanceAlert` gönderilir
4. Tekrar engeli için mevcut cache anahtarı deseni kullanılır: `credit_balance_alert_{team_id}_{Y-m-d}` (aynı anahtar → Livewire bileşeni ile komut birbirini bastırmaz, aynı gün tek e-posta)
5. `Kernel.php`'ye planlama eklenir — günde 2 kez:

```php
$schedule->command('eadisyon:check-credit')
    ->twiceDaily(9, 18)
    ->timezone('Europe/Istanbul')
    ->withoutOverlapping(60)
    ->onOneServer();
```

6. Bakiye bilgisi `Integration->data['credit_info']` alanına zaten yazılıyor (`MysoftEInvoiceService.php` L1726); komut bunu da tazelemiş olur.

**Not:** Livewire bileşeni kaldırılmaz — dashboard'daki canlı gösterim için kalır. Komut, bileşenden **bağımsız** çalışır.

**Uygulanan değişiklik**

- **Yeni dosya:** `app/Console/Commands/CheckCreditBalance.php` (232 satır)
    - İmza: `eadisyon:check-credit {--threshold=100} {--force}`
    - `Integration::withoutGlobalScopes()->where('slug','e-files')->where('is_active',true)->whereNotNull('account_id')` — master credential kayıtları (`type='mysoft-efiles-master'`, `account_id=NULL`) bilinçli olarak hariç, çünkü onlar izlenecek bir firma değil
    - Her firma için: `Address::getOfficialAddress($integration->current_team_id)` → VKN + `service_name`, ardından `setIntegration()` + `getCreditInfo()`
    - `hasCreditData()` false ise **susuyor** (güvenilmez bakiye için yanlış "0 kontör" uyarısı göndermiyor)
    - Alıcı: `Integration->data['credit_alert_email']` varsa o, yoksa `DEFAULT_ALERT_EMAIL` sabiti (`thatugur@gmail.com`)
    - Özet çıktı + `Log::info('[CHECK-CREDIT] Özet')`: taranan / okunan / eşik altı / gönderilen / cache atlanan / VKN eksik / sorgu hatası
    - `--force` günlük cache engelini yok sayıyor (test için)
    - Gönderim **başarısız olursa cache yazılmıyor** — bir sonraki turda yeniden denenir
- `app/Console/Kernel.php` — `twiceDaily(9, 18)` + `Europe/Istanbul` + `withoutOverlapping(60)` + `onOneServer()`

**Doğrulama:** `php artisan schedule:list` çıktısı

```
  0 *    * * *  php artisan eadisyon:auto-send ..... Next Due: 13 dakika sonra
  0 9,18 * * *  php artisan eadisyon:check-credit .... Next Due: 11 saat sonra
```

**Kabul kriterleri**

- [x] Hiçbir tarayıcı açık değilken komut çalışıyor ( Livewire'a bağımlılığı yok)
- [x] Birden fazla firma varsa her biri ayrı değerlendiriliyor (entegrasyon döngüsü)
- [x] Aynı gün aynı firma için yalnızca 1 e-posta — cache anahtarı Livewire bileşeniyle **ortak** (`credit_balance_alert_{team_id}_{Y-m-d}`), böylece iki kanal aynı gün çift göndermiyor
- [x] `php artisan schedule:list` çıktısında komut görünüyor

---

### Görev 2 — E-adisyon gönderiminde kredi kapısı yok (ANA SORUN) ✅ UYGULANDI

**Sorun**

`app/Console/Commands/SendAutoEadisyon.php` içinde **hiçbir kredi kontrolü yok.** Komutun filtreleri yalnızca şunlar:

| Satır    | Filtre                                                |
| -------- | ----------------------------------------------------- |
| L24-27   | `slug = 'e-files'` ve `is_active = true`              |
| L38-41   | `data['eadisyon_auto_send_enabled'] === true`         |
| L86-88   | Daha önce başarılı gönderilenler (`status = 1`) hariç |
| L93-96   | Son 6 saatte başarısız olanlar hariç (cool-down)      |
| L112-119 | Tarih penceresi + firma filtresi                      |

Ardından L174-177'de doğrudan belge oluşturulup gönderiliyor:

```php
$invoiceData = $mysoftService->buildEInvoiceDocument($sale);
$result = $mysoftService->createEInvoice($invoiceData);
```

`getCreditInfo()` bu komutta **asla çağrılmıyor.** Tüm projede bu metodu çağıran tek yer `app/Livewire/CreditBalance.php` L71.

**Oluşan kısır döngü**

1. Kredi yokken MYSOFT her gönderimi reddediyor → `$isSuccess = false`
2. L221-231 `Eadisyon` kaydını `status = 0` ile yazıyor
3. L93-96 cool-down kaydı 6 saat dışarıda tutuyor
4. L22-27 zamanlama her gün 03:00–06:00 arası saat başı çalıştırıyor, pencere son 7 günü kapsıyor (L77-79)
5. Aynı kayıtlar **7 gün boyunca sonsuz kez** yeniden deneniyor

Etkileri: gereksiz API çağrısı, `eadisyons` tablosunda hata satırı birikmesi, tekrarlayan error log'ları, ve gönderimin hiç durmaması.

MYSOFT belgeyi kredisiz kabul ediyorsa (overdraft) firma farkında olmadan borçlanıyor — "oluşturulmaya devam ediyor" şikayeti bu durumu da kapsıyor.

**Önerilen çözüm**

`SendAutoEadisyon::handle()` içine firma bazında kredi kapısı eklenir:

1. Adım 4'ten (servis başlatma) sonra, döngüden **önce** her firma için bakiye çözülür:
    - Firma → entegrasyon → `current_team_id`
    - `Address::getOfficialAddress($teamId)` → `vat_number`, `service_name`
    - `getCreditInfo($vatNumber)` → `remainingCreditQty` toplamı
2. Sonuç `$creditByCompany[$companyId]` dizisinde önbelleğe alınır (her satış için tekrar API çağrısı yapılmaz — performans)
3. Satış döngüsünde (L153) gönderimden önce kontrol:

```php
if (($creditByCompany[$sale->company_id] ?? null) !== null
    && $creditByCompany[$sale->company_id] < $creditFloor) {
    // gönderimi atla, Eadisyon satırı YAZMA, sayacı skippedCredit++ yap
    continue;
}
```

> Önemli: atlanan satışlar için `Eadisyon` kaydı **yazılmamalı.** Yazılırsa `status = 0` cool-down'a girer ve kredi yüklendikten sonra gecikmeli işlenir. Kayıt yazılmayınca kredi geldiğinde bir sonraki çalışmada otomatik devam eder (self-healing korunur).

4. Bakiye eşik altındaysa **bir kez** yöneticiye bildirim gider (firma başına, günlük cache ile): yeni bir `CreditExhaustedAlert` Mailable veya mevcut `CreditBalanceAlert`'in farklı konusuyla
5. Komut çıktısına ve `Log::info('[AUTO-EADISYON] Özet')` bloğuna `kredi_nedeniyle_atlanan` sayısı eklenir
6. Yeni seçenek: `{--credit-floor=0 : Bu değerin altında gönderim durdurulur}`

**Karar — eşik değeri: A seçeneği (`0`)**

İki seçenek değerlendirildi:

| Seçenek  | Eşik      | Avantaj                                                   | Dezavantaj                                    |
| -------- | --------- | --------------------------------------------------------- | --------------------------------------------- |
| **A** ✅ | `0`       | Bakiye tam bitene kadar gönderim sürer, belge kaybı olmaz | Son belgeler reddedilebilir                   |
| B        | örn. `50` | Güvenli tampon, toplu gönderimde yarıda kesilmez          | Bakiye varken erken durur, yanıltıcı olabilir |

**Seçilen: A (`--credit-floor=0`).** Gerekçe: bakiye varken gönderimi durdurmak yanıltıcı olur; belge kaybı riski, erken durdurma riskinden daha önemli. Eşik `--credit-floor` seçeneğiyle çalıştırma bazında değiştirilebilir kaldı, dolayısıyla ileride tampon istenirse kod değişikliği gerekmez:

```bash
php artisan eadisyon:auto-send --credit-floor=50
```

**Bildirim kararı: ayrı şablon.** Mevcut `CreditBalanceAlert` yerine yeni `CreditExhaustedAlert` — "bakiye azaldı" ile "gönderim durduruldu" farklı eylemler gerektiren farklı durumlar ve alıcının ayırt edebilmesi gerekiyor.

**Uygulanan değişiklik**

- **Yeni dosya:** `app/Mail/CreditExhaustedAlert.php` — `$companyName`, `$remainingCredit`, `$pendingCount` taşır; konu `E-Adisyon Gönderimi Durduruldu - {firma}`
- **Yeni dosya:** `resources/views/emails/credit-exhausted-alert.blade.php` — kırmızı tema, bekleyen belge sayısı kutusu ve "Ne yapmalısınız?" adımları (kontör yükle → otomatik devam eder → hata kaydı yazılmaz)
- `app/Console/Commands/SendAutoEadisyon.php`
    - İmzaya `{--credit-floor=0}` eklendi, `DEFAULT_ALERT_EMAIL` sabiti tanımlandı
    - **Yeni adım 6c (kredi kapısı):** `$enabledIntegrations` döngüsünde firma başına bakiye çözülür → `$creditByCompany`, `$companyNames`, `$alertEmails`, `$blockedCompanyIds` haritaları. `balance <= $creditFloor` ise firma engellenir
    - **FAIL-OPEN:** VKN eksik / API hatası / `hasCreditData()` false ise firma **engellenmez**, yalnızca uyarı loglanır. İzleme servisindeki aksaklık tüm belge gönderimini durdurmamalı
    - **Yeni adım 7a:** engellenen firmaların bekleyen belge sayısı sayılır (`$blockedByCompany`, `$totalBlocked`) ve `notifyBlockedCompanies()` çağrılır
    - `$pendingCount` ve batch sorgularına `->whereNotIn('company_id', $blockedCompanyIds)` eklendi — engellenen firmaların satışları **hiç çekilmiyor**, dolayısıyla `createEInvoice()` çağrılmıyor ve `Eadisyon` satırı yazılmıyor
    - `$pendingCount === 0` erken dönüşü, kredi nedeniyle bekleyen belge varsa yanıltıcı "bekleyen satış yok" mesajı yerine uyarı veriyor
    - Özet çıktıya ve `Log::info('[AUTO-EADISYON] Özet')` bloğuna `kredi_esigi`, `kredi_nedeniyle_bekleyen`, `kredi_nedeniyle_duran_firma` eklendi
    - Yeni `notifyBlockedCompanies()` metodu: firma başına günde 1 e-posta (`credit_exhausted_alert_{company_id}_{Y-m-d}`), gönderim başarısızsa cache yazılmaz

> **Neden döngü içi `continue` yerine sorgu seviyesinde `whereNotIn`:** plandaki ilk öneri satış döngüsünde atlamaktı. Sorgu seviyesinde engellemek daha doğru — gereksiz satır çekilmiyor, özet sayıları gerçeği yansıtıyor ve `$cursorId` imleci engellenen kayıtlarla ilerlemiyor.

**Kabul kriterleri**

- [x] Bakiye eşik altındayken `createEInvoice()` hiç çağrılmıyor (sorgudan çıkarılıyor)
- [x] Atlanan satışlar için `eadisyons` tablosuna satır yazılmıyor → cool-down'a takılmıyor
- [x] Kredi yüklendikten sonra bir sonraki çalışmada otomatik işleniyor (self-healing korundu)
- [x] Yöneticiye "gönderim durduruldu" bildirimi gidiyor (ayrı şablon, bekleyen belge sayısıyla)
- [x] Özet log'da atlanan kayıt sayısı görünüyor
- [x] Çok firmalı ortamda bakiyesi olan firma etkilenmiyor (kapı firma bazında, `company_id` ile)

---

### Görev 3 — EadisyonController yanlış cache anahtarı okuyor ✅ UYGULANDI

**Sorun**

`app/Http/Controllers/EadisyonController.php` L811-814:

```php
if (isset($efiles_integration->data['remainingCreditQty'])) {
    $remainingCredit = $efiles_integration->data['remainingCreditQty'];
```

Ancak servisin yazdığı anahtar bu değil. `app/Services/MysoftEInvoiceService.php` L1726:

```php
$existingData['credit_info'] = $data;
```

Yani `data` dizisinin **üst seviyesinde `remainingCreditQty` anahtarı hiçbir zaman yazılmıyor** → `isset()` daima `false` → `$showPackageSelection` hiç ayarlanmıyor → paket seçim uyarısı asla tetiklenmiyor. Üçüncü bir sessiz hata.

**Önerilen çözüm**

1. Okuma yolu API'nin gerçek yapısına göre düzeltilir (`docs/MYSOFT_CREDIT.md` L36-53'te belgelenmiş format):

```php
$creditItems = $efiles_integration->data['credit_info']['data'] ?? [];
$remainingCredit = array_sum(array_column($creditItems, 'remainingCreditQty'));
```

2. Eşiğin hesaplanması korunur: `$threshold = $salesLast365Days * 0.2;`
3. `$creditItems` boşsa (henüz hiç sorgulanmamış) uyarı gösterilmez — varsayılan davranış korunur
4. Ortak mantık (bakiye toplamı) tekrar etmemesi için bir yardımcıya taşınabilir — örn. `MysoftEInvoiceService` üzerine `sumRemainingCredit(array $apiResponse): float`. Bu metod hem `CreditBalance.php`, hem `CheckCreditBalance.php`, hem `SendAutoEadisyon.php`, hem de `EadisyonController.php` tarafından kullanılır → **tek doğruluk kaynağı**

**Uygulanan değişiklik**

- `app/Services/MysoftEInvoiceService.php` — sınıfın sonuna iki **static** yardımcı eklendi (statik olmaları, servis örneği/entegrasyon kurulumu gerektirmeden controller'dan da çağrılabilmelerini sağlıyor):
    - `hasCreditData(array $apiResponse): bool` — `data` dizisi var ve dolu mu
    - `sumRemainingCredit(array $apiResponse): float` — tüm paketlerin `remainingCreditQty` toplamı; `hasCreditData()` false ise `0.0`
- `app/Http/Controllers/EadisyonController.php` — okuma yolu `data['credit_info']` olarak düzeltildi, `null` `data` için `?? []` koruması eklendi:

```php
$integrationData = $efiles_integration->data ?? [];
$creditInfo = $integrationData['credit_info'] ?? [];

if (MysoftEInvoiceService::hasCreditData($creditInfo)) {
    $remainingCredit = MysoftEInvoiceService::sumRemainingCredit($creditInfo);
    $threshold = $salesLast365Days * 0.2;
    $showPackageSelection = $remainingCredit < $threshold;
} else {
    $showPackageSelection = true;
}
```

**Tek doğruluk kaynağı:** bakiye hesaplama mantığı artık yalnızca `MysoftEInvoiceService::sumRemainingCredit()` içinde. Dört tüketici de bunu kullanıyor: `CreditBalance` (Livewire), `CheckCreditBalance` (yeni komut), `SendAutoEadisyon` (kredi kapısı), `EadisyonController` (paket uyarısı). `CreditBalance::findCreditInArray()` yalnızca `hasCreditData()` false olan yedek yolda kaldı.

**Kabul kriterleri**

- [x] Bakiye hesaplama mantığı tek bir yerde tanımlı
- [x] Hiç kredi verisi yokken hata oluşmuyor (`?? []` + `hasCreditData()` koruması)
- [x] Controller artık doğru anahtarı okuyor
- [ ] Kontör azaldığında paket seçim uyarısının arayüzde görünmesi — **sunucuda görsel doğrulama gerekli** (lokalde DB erişimi yok)

---

## 4. Uygulama Sırası — ✅ TAMAMLANDI

| Sıra | Görev                                              | Bağımlılık            | Durum |
| ---- | -------------------------------------------------- | --------------------- | ----- |
| 1    | Görev 3 (ortak `sumRemainingCredit` yardımcısı)    | Yok                   | ✅    |
| 2    | Görev 1B (`hasCreditData` bayrağı)                 | Görev 3               | ✅    |
| 3    | Görev 1C (`CheckCreditBalance` komutu + zamanlama) | Görev 3               | ✅    |
| 4    | Görev 2 (gönderim kapısı)                          | Görev 3 + eşik kararı | ✅    |

Planlanan sıra uygulandı: Görev 3 önce yapıldı çünkü diğer üç görev de aynı bakiye hesaplama mantığına ihtiyaç duyuyordu. Böylece dört tüketici tek bir `sumRemainingCredit()` çağrısını paylaşıyor, tekrar eden toplam döngüsü yazılmadı.

---

## 5. Genel Kabul Kriterleri

**Kod tarafında doğrulananlar:**

- [x] Bakiye 0 olan firma için uyarı e-postası gönderiliyor (`hasCreditData && balance <= 100`)
- [x] Hiçbir tarayıcı açık değilken zamanlanmış kontrol çalışıyor (`eadisyon:check-credit`, `schedule:list`'te görünüyor)
- [x] Bakiye tükenmiş firmanın e-adisyon gönderimi duruyor (`whereNotIn('company_id', $blockedCompanyIds)`)
- [x] Kredi yüklendikten sonra gönderim kendiliğinden devam ediyor (atlanan satışlar için `Eadisyon` satırı yazılmıyor → cool-down'a takılmıyor)
- [x] Aynı gün aynı firma için tekrarlayan e-posta gitmiyor (iki ayrı cache anahtarı, günlük TTL)
- [x] `eadisyons` tablosunda gereksiz `status = 0` satırları birikmiyor (engellenen firmalar sorgudan çıkarılıyor)
- [x] Tüm dosyalar `php -l` ile doğrulandı (8 dosya, sıfır hata)
- [x] İki e-posta şablonu Blade derleyicisinden geçti

**Sunucuda doğrulanması gerekenler:**

- [ ] `php artisan config:clear && php artisan config:cache` çalıştırıldı
- [ ] Test e-postası `thatugur@gmail.com` adresine ulaştı
- [ ] `php artisan eadisyon:check-credit` elle çalıştırılıp özet çıktı görüldü
- [ ] Paket seçim uyarısı arayüzde görünüyor (Görev 3)
- [ ] Gerçek bir firmanın bakiyesi eşik altındayken gönderimin durduğu loglardan teyit edildi

---

## 6. İlgili Dosyalar

**Yeni oluşturulanlar:**

| Dosya                                                     | Rol                                      |
| --------------------------------------------------------- | ---------------------------------------- |
| `app/Console/Commands/CheckCreditBalance.php`             | Zamanlanmış bakiye kontrolü (Görev 1C)   |
| `app/Mail/CreditExhaustedAlert.php`                       | "Gönderim durduruldu" Mailable (Görev 2) |
| `resources/views/emails/credit-exhausted-alert.blade.php` | Durdurma bildirimi şablonu (Görev 2)     |

**Değiştirilenler:**

| Dosya                                                   | Rol                                                            |
| ------------------------------------------------------- | -------------------------------------------------------------- |
| `app/Services/MysoftEInvoiceService.php`                | `hasCreditData()` + `sumRemainingCredit()` static yardımcıları |
| `app/Livewire/CreditBalance.php`                        | `hasCreditData` bayrağı, yeni uyarı koşulu (Görev 1B)          |
| `app/Mail/CreditBalanceAlert.php`                       | `statusLabel()` ile dinamik konu (Görev 1B)                    |
| `resources/views/emails/credit-balance-alert.blade.php` | Önem derecesine göre renk/metin (Görev 1B)                     |
| `app/Console/Commands/SendAutoEadisyon.php`             | Kredi kapısı + `notifyBlockedCompanies()` (Görev 2)            |
| `app/Console/Kernel.php`                                | `eadisyon:check-credit` zamanlaması (Görev 1C)                 |
| `app/Http/Controllers/EadisyonController.php`           | `credit_info` okuma düzeltmesi (Görev 3)                       |

**Değiştirilmeyen referanslar:**

| Dosya                                                   | Rol                                                     |
| ------------------------------------------------------- | ------------------------------------------------------- |
| `resources/views/integrations/eadisyon/index.blade.php` | Livewire bileşeninin kullanıldığı tek yer (L70) — kaldı |
| `app/Models/Address.php`                                | `getOfficialAddress()` L85 — VKN ve firma adı kaynağı   |
| `app/Models/Integration.php`                            | `efilesMaster()` L74 — master/per-company ayrımı        |
| `docs/MYSOFT_CREDIT.md`                                 | API yanıt formatı referansı                             |
| `docs/EADISYON_SYSTEM.md`                               | E-adisyon sistem mimarisi                               |

---

## 7. Notlar

- **Firma adı kaynağı:** `Integration->companyDisplayName(?Address)` tek ortak çözücüdür; Livewire bileşeni, `eadisyon:check-credit` ve kredi kapısı aynı sırayı kullanır. Sıra: `Address->service_name` → `Company->name` (`account_id`, sonra resmî adresin `company_id`'si) → `Address->title` → `Team->name` → `Integration->title` → `Firma #{id}`.
  - **Düzeltilen hata:** ilk uygulama yalnızca `Address->service_name` okuyordu. Bu alan `Address::$fillable` içinde tanımlı olmasına rağmen projede **hiçbir kod yolu tarafından yazılmıyor** — tüm `getOfficialAddress()` tüketicileri sadece `vat_number` kullanıyor. Sonuç: e-posta konusu `Bilinmeyen Şirket` oluyordu. `Company->name` uygulamanın genel gösterim adıdır (`EadisyonController` L498, `EadisyonSalesTable` L567/L572).
- **Vergi numarası:** `Address::getOfficialAddress($teamId)` ile team bazlı çözülür. `Address` modeli `currentTeamScope` **taşır** (L79), ancak bu kapsam `auth()->check()` false olduğunda — yani console/zamanlanmış bağlamda — **etkisizdir**. Ayrıca HTTP bağlamında kapsam `auth()->user()->currentTeam`'i kullanır, hedef team'i değil. Bu yüzden team filtresinin metodun içinde açıkça uygulanması şarttır; `getOfficialAddress()` bunu yapar ve her üç kredi yolu da bu metodu kullanır.
- **Master/per-company ayrımı:** credential'lar (`api_key`, token) yalnızca master kayıtta (`type='mysoft-efiles-master'`, `account_id=NULL`); firma bazlı ayarlar (`okc_serial`, `eadisyon_auto_send_enabled`) per-company kayıtlarda. Yeni komut `whereNotNull('account_id')` ile master'ları hariç tutuyor; `setIntegration()` environment'ı per-company `status`'tan, credential'ı `efilesMaster()`'dan kendisi çözüyor.
- **İki ayrı cache anahtarı:**
    - `credit_balance_alert_{team_id}_{Y-m-d}` → düşük bakiye uyarısı. Livewire bileşeni **ve** `eadisyon:check-credit` aynı anahtarı paylaşır; böylece iki kanal aynı gün aynı firma için çift e-posta göndermez.
    - `credit_exhausted_alert_{company_id}_{Y-m-d}` → gönderim durduruldu bildirimi. Bilinçli olarak **ayrı** anahtar; bir firma aynı gün hem "bakiye azaldı" hem "gönderim durdu" bilgisini alabilmeli.
    - Her ikisinde de gönderim **başarısız olursa cache yazılmaz** — bir sonraki turda yeniden denenir.
- **Bildirim alıcısı:** artık firma bazında ayarlanabilir. `Integration->data['credit_alert_email']` doluysa o adres, değilse `DEFAULT_ALERT_EMAIL` sabiti (`thatugur@gmail.com`). Her iki komut da aynı deseni kullanıyor. Henüz bunun için bir arayüz yok; şimdilik `data` JSON'una elle yazılabilir.
- **Kontör negatif olabilir:** kod tarafında negatif değere karşı clamp yok; MYSOFT aşım durumunda negatif döndürebilir. Negatif değer artık **bilinçli olarak uyarı üretiyor** (`<= 100` koşulu kapsar) ve e-postada "Bakiye Aşıldı" olarak kırmızı gösteriliyor. Kredi kapısında da negatif bakiye `<= 0` eşiğinin altında kaldığı için gönderimi durduruyor.
- **Widget durum göstergesi:** `resources/views/livewire/credit-balance.blade.php` tek bir `$state`'ten türetilir: `loading` / `error` / `nodata` / `overdrawn` / `depleted` / `low` / `ok`. Her durum renk (slate/rose/amber/emerald), ikon, rozet etiketi ve bir bilgilendirme satırı taşır. Eşik değeri view'a `render()` üzerinden geçirilir (`ALERT_THRESHOLD` sabiti ile e-posta koşulu aynı kaynak), böylece ekranda yazılan eşik ile gerçek koşul asla ayrışmaz.
  - Sayı yalnızca `hasCreditData` true ise basılır; hata/veri-yok durumunda `— KONTÖR` gösterilir. Önceden error dalında sabit `0 KONTÖR` yazılıyordu, yani bilinmeyen bir bakiye tükenmiş gibi görünüyordu.
  - `alertSentToday`, `credit_balance_alert_{team_id}_{Y-m-d}` cache anahtarının doluluğundan türetilir; "bugün uyarıldı" ile "eşik altında ama henüz uyarılmadı" ayrımı ekranda görünür.
- **FAIL-OPEN politikası:** kredi kapısı yalnızca bakiyesi *tükenmiş olduğu bilinen* firmayı durdurur. VKN eksikse, API hata verirse veya anlamlı kredi kaydı dönmezse firma engellenmez — yalnızca uyarı loglanır. Gerekçe: izleme servisindeki bir aksaklık tüm yasal belge gönderimini durdurmamalı.

---

## 8. Dağıtım ve Doğrulama Adımları

Sunucuda sırayla:

```bash
# 0. Frontend yeniden derle — credit-balance widget'ı yeni Tailwind sınıfları
#    kullanıyor (emerald/rose/slate durum renkleri, rozet). public/build .gitignore
#    içinde olduğu için sunucuda derlenmeli; aksi halde durum renkleri görünmez.
npm run build

# 1. Config cache'i tazele — .env değişikliğinin etkili olması için zorunlu
php artisan config:clear && php artisan config:cache

# 2. Yeni komutun göründüğünü doğrula
php artisan schedule:list
php artisan eadisyon:check-credit --help

# 3. Mail'in gerçekten çalıştığını uçtan uca test et
php artisan tinker --execute="Mail::to('thatugur@gmail.com')->send(new App\Mail\CreditBalanceAlert('Test Firması', 42));"

# 4. Bakiye kontrolünü elle çalıştır (e-posta göndermez, sadece raporlar)
#    Eşik altı firma yoksa hiçbir e-posta gitmez — güvenli
php artisan eadisyon:check-credit

# 5. Günlük cache engelini yok sayarak zorla test etmek gerekirse
php artisan eadisyon:check-credit --force

# 6. Kredi kapısını gönderim YAPMADAN gözlemlemek için eşik yükseltilebilir:
#    tüm firmalar "engellenmiş" sayılır, ama bu yalnızca kapı davranışını gösterir.
#    DİKKAT: bu gerçekten gönderimi durdurur ve durdurma e-postası gönderir.
php artisan eadisyon:auto-send --credit-floor=999999
```

**Beklenen log anahtarları** (`storage/logs/laravel.log`):

| Anahtar                                                                                              | Kaynak                                   |
| ---------------------------------------------------------------------------------------------------- | ---------------------------------------- |
| `[CHECK-CREDIT] Özet`                                                                                | `eadisyon:check-credit` her çalışmasında |
| `[CHECK-CREDIT] Uyarı e-postası gönderildi`                                                          | eşik altı firma için                     |
| `[CHECK-CREDIT] Bakiye sorgusu başarısız` / `exception` / `Vergi numarası eksik` / `Kredi kaydı yok` | fail-open durumları                      |
| `[AUTO-EADISYON] Kredi kapısı: ...`                                                                  | kapının neden engellemediği/engellediği  |
| `[AUTO-EADISYON] Gönderim kredi nedeniyle durduruldu`                                                | durdurma bildirimi gönderildiğinde       |
| `[AUTO-EADISYON] Özet` → `kredi_nedeniyle_bekleyen`                                                  | engellenen belge sayısı                  |
| `Credit balance alert email sent`                                                                    | Livewire bileşeni üzerinden gönderimde   |

---

## 9. Uygulama Sırasında Tespit Edilen Ayrı Hata (kapsam dışı)

`app/Http/Controllers/EadisyonController.php` **L834** — `getStatus()` metodu:

```php
$result = $this->checkOutgoingDocStatus($ettn);
```

Ancak metodun imzası iki parametre istiyor (L35):

```php
public function checkOutgoingDocStatus($uuid, $document_no)
```

Diğer iki çağrı noktası (L435 ve L1056) iki argümanla çağırıyor, yalnızca `getStatus()` tek argümanla çağırıyor. PHP'de eksik argüman `ArgumentCountError` fırlatır — yani **bu endpoint çağrıldığında 500 hatası verir.**

Bu hata bu görevden **bağımsız ve önceden de mevcuttu**; yapılan düzenlemeler yalnızca satır numarasını L830'dan L834'e kaydırdı. Kapsam dışı bırakıldı çünkü düzeltme, route'un yalnızca `$ettn` aldığı bir yerde `$document_no` değerinin nereden bulunacağının kararını gerektiriyor (muhtemelen `Eadisyon` kaydından `document_no` çekilmeli). Ayrı bir iş olarak ele alınmalı.
