# E-Adisyon Sistemi Çalışma Mantığı

Bu doküman, POS satışlarının e-arşiv/e-adisyon belgesine dönüştürülerek GİB'ye (Gelir İdaresi Başkanlığı) gönderilmesi sürecini açıklar.

---

## 1. Genel Bakış

Sistem, tamamlanmış POS satışlarını (`PosSale`) e-adisyon belgesine dönüştürür ve MYSOFT e-Belge API üzerinden GİB'ye (Gelir İdaresi Başkanlığı) iletir. Gönderim iki yoldan yapılır: manuel (route) ve otomatik (scheduler):

| Kanal           | Sağlayıcı          | İletişim Yöntemi | Route                             |
| --------------- | ------------------ | ---------------- | --------------------------------- |
| **MYSOFT**      | MYSOFT e-Belge API | REST / OAuth 2.0 | `POST /eadisyon/send-mysoft/{id}` |
| **Otomatik**    | MYSOFT e-Belge API | REST (Scheduler) | `php artisan eadisyon:auto-send`  |

Belge tipi: **e-Adisyon** (`credit_note_type_code = ADISYON`, `eadisyon` tablosunda saklanır).

---

## 2. Veri Akışı

```
PosSale (mysql-remote)
   │
   ├── Order kalemleri (mysql-remote) ──> Satır kalemleri
   │
   ▼
[buildEInvoiceDocument]  ──►  JSON belge yapısı
   │
   ├──► Manuel gönderim  (POST /eadisyon/send-mysoft/{id})
   │
   └──► Otomatik gönderim (eadisyon:auto-send, scheduler)
   │
   ▼
MYSOFT REST API  ──►  GİB
   │
   ▼
Eadisyon kaydı (mysql)  ──►  status: 1 (başarılı) / 0 (başarısız)
```

Otomatik kanal (ayrı akış — manuel route'dan bağımsız, bkz. [4.2](#42-otomatik-gönderim-scheduler)):

```
Scheduler (gece 03:00-06:00 saat başı)
   └──► [eadisyon:auto-send]  ──►  MYSOFT API  ──►  Eadisyon kaydı
```

---

## 3. Modeller ve Veritabanı

### `PosSale` (`mysql-remote` → `sales` tablosu)

- **Global Scope 1 (`selected_pos`):** `company_id = session('selected_pos_service_id')` — sadece seçili POS şirketinin satışlarını getirir.
- **Global Scope 2 (`completed_sales_only`):** `deleted_at IS NOT NULL` — sadece tamamlanmış satışlar (bu sistemde `deleted_at` = satış kapanış tarihi, standart Laravel soft-delete mantığıyla **ters** çalışır).

> ⚠️ **Önemli:** Satışlar `user_id` veya `current_team_id` ile filtrelenmez. Filtreleme **session tabanlı** `company_id` ile yapılır. Detaylar için [Erişim ve Filtreleme](#5-erişim-ve-filtreleme) bölümüne bakınız.

### `Eadisyon` (`mysql` → `eadisyon` tablosu)

- Global scope **yoktur** — tüm sorgular sistem geneli çalışır.
- `pos_sale_id` üzerinden `PosSale` ile birebir ilişki (`hasOne` / `belongsTo`).
- **Durum (`status`) alanı:**
    - `0` = Başarısız / Beklemede
    - `1` = Başarılı (GİB'ye iletildi)
- **`sent_at` alanı:** son deneme zamanı. Otomatik gönderimde başarısız kayıtlar için 6 saatlik cool-down hesabında kullanılır (bkz. [4.2](#42-otomatik-gönderim-scheduler)).

### `Order` (`mysql-remote`)

- E-adisyon satır kalemleri `Order` kayıtlarından üretilir.
- `withTrashed()` ile silinmiş kalemler de dahil edilir (satışın orijinal içeriği korunur).

---

## 4. Entegrasyon Kanalları

### 4.1. MYSOFT REST API

**Dosya:** `app/Http/Controllers/EadisyonController.php` + `app/Services/MysoftEInvoiceService.php`
**Route:** `POST /eadisyon/send-mysoft/{id}` → `sendViaMysoft()`

**Akış:**

1. `Integration` tablosundan `type='mysoft'` VE `is_active=true` kayıt bulunur.
2. `MysoftEInvoiceService->setIntegration()` — ortam (test/live) ayarlanır:
    - **Live:** `https://edocumentapi.mysoft.com.tr/api`
    - **Test:** `https://edocumentapi.mytest.tr/api`
3. `getAccessToken()` — OAuth 2.0 `password` grant ile token alınır, **23 saat** (`82800s`) cache'lenir.
4. `buildEInvoiceDocument($posSale)` — satış verisinden JSON belge yapısı üretilir:
    - `payment_type == 2` → `OKC_SERI_NO` (Yazarkasa), aksi → `ETTN` (e-Fatura/e-Arşiv)
    - VKN `Address::getOfficialAddress(team_id)` üzerinden alınır.
5. `createEInvoice($invoiceData)` — `POST /BillDocument/billDocumentOutbox` ile gönderilir.
6. Başarı koşulu: `success=true` AND `data.data.ettn` var AND `data.succeed=true`.
7. `Eadisyon::updateOrCreate()` ile DB kaydı yapılır.

---

### 4.2. Otomatik Gönderim (Scheduler)

**Dosyalar:** `app/Console/Commands/SendAutoEadisyon.php`, `app/Console/Kernel.php`
**Komut:** `php artisan eadisyon:auto-send {--days=7}`

Manuel route'lardan bağımsız olarak, Laravel Scheduler üzerinden **MYSOFT kanalıyla** otomatik gönderim yapılır:

```php
// app/Console/Kernel.php
$schedule->command('eadisyon:auto-send')
    ->hourly()
    ->between('03:00', '06:00')   // 03:00, 04:00, 05:00, 06:00
    ->timezone('Europe/Istanbul')
    ->withoutOverlapping(60)      // Çakışan run'lar aynı satışı GİB'ye iki kez göndermesin
    ->onOneServer();              // Çok sunuculu ortamda tek kopya çalışsın
```

#### Filtreler

1. **Firma:** `Integration` tablosunda `slug='e-files'` + `is_active=true` + `data.eadisyon_auto_send_enabled === true` olan firmalar (`account_id` → `company_id` eşlemesi). Anahtar `app/Livewire/AutoSendToggle.php` bileşeni ile açılıp kapanır.
2. **Tarih penceresi (kayan):** Varsayılan son **7 gün** — `deleted_at ∈ [today()-7gün, today())`. Pencere yalnızca "dünü" içermediği için, scheduler'ın kaçtığı bir gece ya da bütçe/limit nedeniyle işlenemeyen kayıtlar pencere içinde kaldığı sürece sonraki çalışmalarda **otomatik telafi edilir** (self-healing).
3. **Harici tutulanlar:**
    - `Eadisyon.status = 1` kaydı olan satışlar → çift belge gönderimi önlenir.
    - Son **6 saat** içinde denenyip başarısız olan satışlar (`sent_at` bazlı **cool-down**) → geçici hatalar cool-down sonrası tekrar denenir; kalıcı hatalılar her batch'in başını işgal edip kuyruğu tıkamaz (starvation önleme).

#### Kuyruk Bitene Kadar İşleme (Batch Döngüsü)

- Run başına sabit kayıt limiti **yoktur**; **150 kayıtlık batch'ler** halinde kuyruk boşalana kadar işlenir.
- Güvenlik sınırları: run başına en fazla **30 batch (~4.500 kayıt)** ve **45 dakika** zaman bütçesi. Sınıra takılan kalantı, sonraki saatlik çalışmada kaldığı yerden devam eder.
- **İmleç (`id > cursorId`):** batch'ler `id` artan sırayla çekilir; bu run'da işlenen (başarılı veya başarısız) kayıtlar aynı run içinde tekrar çekilmez. Böylece başarısız kayıtların sorguda kalıp döngüye takılması (sonsuz döngü) engellenir.
- Satışlar arası `usleep(500000)` ile **0,5 sn** rate-limit uygulanır.
- Run sonunda özet hem konsola yazılır hem `Log::info('[AUTO-EADISYON] Özet', ...)` ile loglanır.

#### Manuel Backfill (Geçmiş Kayıtları Toplama)

```bash
php artisan eadisyon:auto-send --days=30    # son 30 günü tara
```

> ⚠️ GİB/e-belge sağlayıcıları çok eski tarihli belgeleri reddedebilir; geniş `--days` değerlerini önce küçük bir aralıkla test edin.

> ⚠️ Zamanlamanın tetiklenmesi için sunucuda standart Laravel cron'u kurulu olmalıdır:
> `* * * * * cd /proje-yolu && php artisan schedule:run >> /dev/null 2>&1`

---

## 5. Erişim ve Filtreleme

### Route Koruma

Tüm e-adisyon route'ları şu middleware grubu altındadır:

```php
Route::middleware([
    'auth:sanctum',
    config('jetstream.auth_session'),
    'verified',
    'check.user.access',   // ← Kritik
])->group(...)
```

**`CheckUserAccess` middleware'i** kullanıcının erişim yetkisini kontrol eder:

```php
UserAccess::where('current_team_id', $teamId)
    ->where('user_id', $user->id)
    ->where('type', 'posmanager')
    ->exists();
```

Bu kontrolü geçemeyen kullanıcılar **403** hatası alır ve sayfaya giremez.

### Satış Listesi Filtreleme (`index()` metodu)

`index()` metodundaki `PosSale` sorguları **iki global scope** ile filtrelenir:

| Scope                  | Koşul                                             | Kaynak                                    |
| ---------------------- | ------------------------------------------------- | ----------------------------------------- |
| `selected_pos`         | `company_id = session('selected_pos_service_id')` | Sadece `/dashboard` route'unda set edilir |
| `completed_sales_only` | `deleted_at IS NOT NULL`                          | Otomatik                                  |

> **Session değeri nereden gelir?** `/dashboard` route'unda, kullanıcının `user_accesses` tablosundaki `type='pos'` kayıtlarından ilk geçerli `service_id` session'a yazılır:
>
> ```php
> session(['selected_pos_service_id' => $default_pos_access->service_id]);
> ```

### Takım Arkadaşının Görememesi Sorunu

Aynı `current_team_id`'ye sahip iki kullanıcı farklı sonuçlar görebilir çünkü:

1. **`CheckUserAccess` her kullanıcı için ayrı çalışır** — takım üyeliği yetki vermez, her kullanıcının kendi `type='posmanager'` kaydı olmalı.
2. **Session per-user'dır** — `selected_pos_service_id` kullanıcının kendi `type='pos'` `user_accesses` kaydından gelir.
3. **Eadisyon sorgusu global scope'suzdur** — `Eadisyon::where('status', 0)->count()` tüm sistemi sayar (potansiyel bug).

**Çözüm:** Takım arkadaşının da kendi `user_accesses` kayıtları olmalı (hem `posmanager` hem `pos` tiplerinde, doğru `service_id` ile).

---

## 6. Diğer Endpoint'ler

| Route                             | Metot             | Açıklama                                                    |
| --------------------------------- | ----------------- | ----------------------------------------------------------- |
| `GET /integrations/eadisyon`      | `index()`         | Satış listesi + e-adisyon durumları + kredi/paket bilgileri |
| `POST /eadisyon/send-mysoft/{id}` | `sendViaMysoft()` | MYSOFT REST API ile gönder                                  |
| `GET /eadisyon/print/{id}`        | `printReceipt()`  | Fiziksel fiş yazdırma görünümü                              |

---

## 7. Kredi Fiyatlama Mantığı

`index()` metodunda, son 365 günde yapılan satış sayısına göre dinamik kredi fiyatı hesaplanır:

```
creditBasePrice = 0.70 × 1.2 = 0.84 TL  (kredi taban fiyatı)

Satış sayısı ≥ 10.000  →  0.84 + 2.00 = 2.84 TL  (min hizmet bedeli)
Satış sayısı ≤ 1.500   →  0.84 + 5.00 = 5.84 TL  (max hizmet bedeli)
Aralıkta               →  Lineer interpolasyon
```

Paket seçimi gösterimi: `e-files` entegrasyonunda kredi bilgisi yoksa veya eşiğin altındaysa (`remainingCredit < salesLast365Days × 0.2`) gösterilir.

---

## 8. Dosya Referansları

| Dosya                                                           | Rolü                                                       |
| --------------------------------------------------------------- | ---------------------------------------------------------- |
| `app/Http/Controllers/EadisyonController.php`                   | Ana controller — MYSOFT gönderim, belge görüntüleme, print |
| `app/Services/MysoftEInvoiceService.php`                        | MYSOFT REST API servisi — OAuth, belge oluşturma, gönderim |
| `app/Models/Eadisyon.php`                                       | E-adisyon veritabanı modeli (mysql)                        |
| `app/Models/PosSale.php`                                        | POS satış modeli (mysql-remote), global scope'lar          |
| `app/Models/Order.php`                                          | Satış satır kalemleri                                      |
| `app/Http/Middleware/CheckUserAccess.php`                       | Erişim yetki kontrolü (`posmanager` tipi)                  |
| `resources/views/integrations/eadisyon/index.blade.php`         | Liste/gönderim arayüzü                                     |
| `resources/views/integrations/eadisyon/print-receipt.blade.php` | Fiş yazdırma görünümü                                      |
| `routes/web.php` (satır 288-299)                                | Route tanımları                                            |
| `app/Console/Commands/SendAutoEadisyon.php`                     | Otomatik gönderim komutu — kayan pencere, batch döngüsü    |
| `app/Console/Kernel.php`                                        | Scheduler tanımı (03:00–06:00 saat başı)                   |
| `app/Livewire/AutoSendToggle.php`                               | Otomatik gönderim aç/kapa anahtarı (entegrasyon `data`'sı) |

---

## 9. Bilinen Sorunlar ve Dikkat Edilecekler

1. **`Eadisyon::where('status', 0)->count()`** — global scope olmadığı için **sistem geneli** sayar. Takım/şirket bazlı filtre eklenmeli.
2. **Session bağımlılığı** — `selected_pos_service_id` session değeri sadece `/dashboard`'da set edilir. Eadisyon sayfasına direkt girişte session boş olabilir → filtre çalışmaz → tüm şirketlerin satışları görünebilir.
3. **Scheduler cron bağımlılığı** — `eadisyon:auto-send` yalnızca `php artisan schedule:run` cron'u çalışıyorsa tetiklenir. Sunucuda cron kurulu değilse otomatik gönderim hiç çalışmaz.
4. **Otomatik gönderimde deneme sayacı yok** — Kalıcı hatalı kayıtlar, 6 saatlik cool-down ile sınırlı biçimde pencere boyunca tekrar tekrar denenir. `eadisyon` tablosuna `attempts` sütunu eklenip deneme üst sınırı konulabilir.
5. **Geç tarihli backfill riski** — `--days` ile çok eski kayıtlar taranırken GİB/e-belge sağlayıcısının geç tarih reddetme ihtimali göz önünde bulundurulmalı.
6. **Controller'da eski SOAP kodu duruyor** — `sendEadisyon()`, `retryFailedEadisyon()`, `checkOutgoingDocStatus()` ve `generateEadisyonXml()` ile bunlara bağlı route'lar (`/eadisyon/send/{id}`, `/eadisyon/retry/{id}`) kullanılmayan eski kanala aittir; kod tabanından ve `routes/web.php`'den temizlenmeleri gerekir.
