# Ön Ödemeli Bakiye Sistemi - Teknik Dokümantasyon

## 1. Veritabanı Yapısı

### 1.1 Yeni Tablo: `credit_transactions`

```sql
CREATE TABLE credit_transactions (
    id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
    customer_id INT UNSIGNED NOT NULL,
    company_id INT UNSIGNED NOT NULL,
    sale_id BIGINT UNSIGNED NULL,
    amount DECIMAL(12, 2) NOT NULL,
    balance_after DECIMAL(12, 2) NOT NULL,
    transaction_type VARCHAR(50) NOT NULL,
    payment_type TINYINT DEFAULT 5,
    description TEXT NULL,
    created_by INT UNSIGNED NULL,
    created_at TIMESTAMP NULL,
    updated_at TIMESTAMP NULL,

    FOREIGN KEY (customer_id) REFERENCES pos_customers(id) ON DELETE CASCADE,
    FOREIGN KEY (company_id) REFERENCES pos_companies(id) ON DELETE CASCADE,
    INDEX idx_customer_company (customer_id, company_id),
    INDEX idx_customer_created (customer_id, created_at),
    INDEX idx_transaction_type (transaction_type),
    INDEX idx_sale (sale_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
```

**Alan Açıklamaları:**

- `customer_id`: İşlemin ait olduğu müşteri (pos_customers tablosu)
- `company_id`: İşlemin yapıldığı firma (NOT NULL - her işlem bir firmaya ait)
- `sale_id`: İlgili satış ID (sadece `order_payment` ve `refund` işlemlerinde, diğerlerinde NULL). Doğrudan FK — polimorfik `reference_type` kullanılmaz.
- `amount`: Tutar (+bakiye yükleme, -harcama)
- `balance_after`: İşlem sonrası güncel bakiye (materialized balance). Race condition koruması için: son satırın `balance_after` değeri lock-for-update ile okunur, yeni bakiye hesaplanır.
- `transaction_type`: İşlem tipi ('credit_load', 'order_payment', 'refund', 'adjustment')
- `payment_type`: Ödeme tipi (5 = CREDIT, POS PaymentTypes ile uyumlu)
- `description`: İşlem açıklaması
- `created_by`: İşlemi yapan kullanıcı (POS admin, sadece POS tarafında set edilir)

### 1.2 Transaction Tipleri

| Tip             | Açıklama        | Amount      | Kullanım Senaryosu                       |
| --------------- | --------------- | ----------- | ---------------------------------------- |
| `credit_load`   | Bakiye yükleme  | Pozitif (+) | POS'ta müşteriye bakiye yüklenirken      |
| `order_payment` | Sipariş ödemesi | Negatif (-) | Menüden sipariş verildiğinde             |
| `refund`        | İade            | Pozitif (+) | Sipariş iptal edildiğinde                |
| `adjustment`    | Manuel düzeltme | +/-         | Admin tarafından manuel bakiye düzenleme |

---

## 2. Model Güncellemeleri

### 2.1 PosCustomer Modeli

**Yeni Eklenecek Özellikler:**

1. **Relationship:**
    - `creditTransactions()` - HasMany relationship with CreditTransaction

2. **Helper Methods:**
    - `getBalance($companyId)` - Müşterinin belirli firmadaki güncel bakiyesini hesaplar (SUM(amount)). **NOT:** Eloquent accessor kullanılmaz çünkü bakiye `customer_id + company_id` ile tanımlı; accessor'a company_id parametresi geçilemez.
    - `addCredit($amount, $description, $companyId, $createdBy)` - Bakiye yükleme (sadece POS tarafı çağırmalı)
    - `deductCredit($amount, $saleId, $description, $companyId)` - Bakiye düşme (sadece menu.apper checkout sırasında çağırmalı)
    - `hasSufficientBalance($amount, $companyId)` - Yeterli bakiye kontrolü (firma bazlı)
    - `getBalanceHistory($companyId)` - Bakiye geçmişi (running total ile)

**Güncellenecek Alanlar:**

- `$fillable` array'ine yeni alan eklenmeyecek (balance ayrı tabloda)
- `$casts` array'ine yeni alan eklenmeyecek

### 2.2 Yeni Model: CreditTransaction

**Temel Özellikler:**

1. **Connection:** `mysql-remote`
2. **Table:** `credit_transactions`
3. **Relationships:**
    - `customer()` - BelongsTo PosCustomer
    - `company()` - BelongsTo PosCompany
    - `sale()` - BelongsTo PosSale (sadece sale_id NULL olmadığında)

4. **Scopes:**
    - `scopeCreditLoads()` - Sadece bakiye yüklemeleri
    - `scopeOrderPayments()` - Sadece sipariş ödemeleri
    - `scopeForCompany($companyId)` - Belirli firma için

5. **Casts:**
    - `amount` → `decimal:2`
    - `balance_after` → `decimal:2`
    - `payment_type` → `integer`
    - `sale_id` → `integer`
    - `created_at/updated_at` → `datetime`

### 2.3 PosSale Modeli

**Değişiklik Yok** - Mevcut yapı korunur, sadece `payment_type = 5` (CREDIT) kullanımı yaygınlaşır.

---

## 3. Migration Dosyaları

### 3.1 Create Credit Transactions Table

**Dosya:** `database/migrations/2026_04_XX_create_credit_transactions_table.php`

**İçerik:**

- Tablo oluşturma şeması (yukarıdaki SQL yapısına uygun)
- Foreign key constraints
- Indexes

---

## 4. Web Route Endpoints

**ÖNEMLİ:** Bu sistem mevcut Laravel web route mimarisine entegre edilmiştir. Ayrı bir API katmanı oluşturulmamıştır. Session-based authentication ve CSRF protection kullanılır.

### 4.1 Menü Tarafı (Public Routes - Session Based)

**ÖNEMLİ:** Mevcut menü sistemi session tabanlıdır. QR kod zaten `customerQr()` metodunda çözülüp session'a yazılır. Her request'te QR kod göndermek gerekmez ve güvenlik riski oluşturur.

**Session'da mevcut veriler:**

- `session('menu_customer_qr_id')` — QR kod ID
- `session('menu_sale_id')` — Aktif sale ID
- `session('selected_pos_service_id')` — Company ID
- `session('payment_collection')` — Tahsilat tipi ('cash_register' veya 'prepayment')

---

#### GET `/menu/balance`

**Açıklama:** Session'daki müşteri bakiyesini getir (AJAX request için)

**Request:**

```
GET /menu/balance
Headers: X-CSRF-TOKEN: {token}
```

**Response (JSON):**

```json
{
    "success": true,
    "data": {
        "customer_id": 42,
        "customer_name": "Ahmet Yılmaz",
        "balance": 750.0,
        "currency": "TL"
    }
}
```

**Logic:**

1. Session'dan `menu_customer_qr_id` ve `selected_pos_service_id` al
2. CustomerQrCode ile müşteri bul
3. `$customer->getBalance($companyId)` ile bakiye hesapla
4. DB connection: `mysql-remote`

---

#### POST `/menu/check-balance`

**Açıklama:** Sepet tutarı için yeterli bakiye kontrolü (AJAX). Sepet tutarını session'daki `menu_cart`'tan hesaplar.

**Request:**

```
POST /menu/check-balance
Headers: X-CSRF-TOKEN: {token}
Body: (boş — sepet tutarı session'dan hesaplanır)
```

**Response (Yeterli Bakiye):**

```json
{
    "success": true,
    "data": {
        "sufficient": true,
        "current_balance": 750.0,
        "cart_total": 350.0,
        "remaining_balance": 400.0
    }
}
```

**Response (Yetersiz Bakiye):**

```json
{
    "success": false,
    "error": {
        "code": "INSUFFICIENT_BALANCE",
        "message": "Yetersiz bakiye. Lütfen bakiye yükleyiniz.",
        "current_balance": 200.0,
        "required_amount": 350.0,
        "shortage": 150.0
    }
}
```

---

#### POST `/menu/checkout` (Mevcut endpoint — genişletilecek)

**Açıklama:** Mevcut checkout endpoint'i `payment_collection` ayarına göre dallanacak. Ayrı bir `checkout-with-credit` endpoint'i oluşturulmaz.

**Prepayment akışı (session('payment_collection') === 'prepayment'):**

```
1. Mevcut validasyon ve müşteri işlemleri (aynı)
2. Order'ları oluştur (payment_type = 5 / CREDIT)
3. Sale güncelle (payment_type = 5 / CREDIT)
4. Bakiye kontrolü (lockForUpdate ile son credit_transaction'dan balance_after oku)
5. CreditTransaction oluştur (amount: -cartTotal, balance_after: kalan bakiye)
6. Cart temizle, print API çağır (aynı)
7. Redirect: /menu/orders
   Flash: "Siparişiniz alındı. Kalan bakiye: 400.00 TL"
```

**Cash register akışı (mevcut — değişiklik yok):**

```
1-6. Mevcut akış aynen devam eder (payment_type = 0 / UNPAID)
```

**Hata Durumları (Prepayment):**

- Yetersiz bakiye → Redirect back + flash error
- QR/session geçersiz → Redirect back + flash error

---

### 4.2 POS Tarafı (Authenticated Routes - Web Session)

#### GET `/customers/{id}/add-credit`

**Açıklama:** Bakiye yükleme formunu göster

**Auth Required:** ✅ (Web session - authenticated admin)
**Middleware:** `auth:sanctum`, `verified`, `check.user.access`

**Response:** Blade view (`customers.add-credit-form`)

---

#### POST `/customers/{id}/add-credit`

**Açıklama:** Müşteriye bakiye yükleme

**Auth Required:** ✅ (Web session - authenticated admin)

**Request (Form Data):**

```
amount: 1000.00
description: Nakit ödeme ile bakiye yükleme
payment_method: cash
_csrf_token: {token}
```

**Response (Başarılı):**

```
Redirect: /customers/show/{id}
Flash: success => "₺1000.00 bakiye yüklendi. Yeni bakiye: ₺1750.00"
```

**Response (Hata):**

```
Redirect: back()
Flash: error => "Bakiye yükleme başarısız: {error_message}"
Errors: validation errors
```

**Validation:**

- Amount > 0 ve <= 50000
- Authenticated user (web session)
- Customer exists
- DB connection: `mysql-remote`

---

#### GET `/customers/{id}/balance-history`

**Açıklama:** Müşteri bakiye geçmişi sayfası

**Auth Required:** ✅ (Web session)

**Response:** Blade view (`customers.balance-history`)

**View Data:**

```php
[
    'customer' => PosCustomer model,
    'transactions' => CreditTransaction collection (with running balance),
    'current_balance' => float
]
```

---

#### GET `/customers/search-by-phone`

**Açıklama:** Telefon ile müşteri arama (bakiye bilgisi ile) - AJAX

**Request:**

```
GET /customers/search-by-phone?phone=5551234567
Headers: X-CSRF-TOKEN: {token}
```

**Response (JSON):**

```json
{
    "success": true,
    "data": {
        "customers": [
            {
                "id": 42,
                "name": "Ahmet",
                "surname": "Yılmaz",
                "phone": "5551234567",
                "balance": 400.0,
                "last_transaction_date": "2026-04-26T15:45:00Z"
            }
        ]
    }
}
```

---

## 5. Controller Güncellemeleri

### 5.1 MenuController

**Yeni Metodlar:**

1. `getBalance(Request $request)`
    - Session'dan müşteri bilgisi al (`menu_customer_qr_id`, `selected_pos_service_id`)
    - Bakiye hesapla: `$customer->getBalance($companyId)` (DB::connection('mysql-remote'))
    - JSON response döndür (AJAX için)

2. `checkBalance(Request $request)`
    - Session'daki `menu_cart`'tan sepet tutarını hesapla
    - Session'daki müşteri bakiyesi ile karşılaştır
    - Yeterli/yetersiz durumu JSON olarak döndür

3. `processCreditPayment($sale, $cartTotal, $customerId, $companyId)` (private)
    - DB::connection('mysql-remote')->beginTransaction()
    - Son CreditTransaction'ı lockForUpdate ile oku → balance_after al
    - Bakiye kontrolü: balance_after >= cartTotal
    - Yeni CreditTransaction oluştur (amount: -cartTotal, balance_after: yeni bakiye)
    - Sale payment_type = 5 güncelle
    - Commit
    - Hata durumunda rollback + flash error

**Mevcut Metod Güncellemeleri:**

- `checkout()` — `payment_collection === 'prepayment'` kontrolü eklenecek. Ayrı endpoint değil, mevcut metod içinde dallanma:

    ```php
    // Order oluştururken:
    'payment_type' => session('payment_collection') === 'prepayment' ? 5 : 0,

    // Sale güncellemeden önce:
    if (session('payment_collection') === 'prepayment') {
        $this->processCreditPayment($sale, $cartTotal, $customerId, $companyId);
    }
    ```

- `cart()` — View'da bakiye gösterimi eklenecek
- `customerQr()` — QR girişinde `payment_collection` session'a yazılacak ve bakiye view'a pass edilecek

---

### 5.2 PosCustomerController

**Yeni Metodlar:**

1. `showAddCreditForm($id)`
    - Müşteri bilgilerini getir
    - Bakiye yükleme formunu göster (Blade view)

2. `addCredit(Request $request, $id)`
    - Validation
    - DB transaction başlat (DB::connection('mysql-remote'))
    - CreditTransaction oluştur
    - Transaction commit
    - Redirect + Flash message döndür

3. `balanceHistory($id)`
    - Müşteri transactions'larını getir
    - Running total hesapla
    - Blade view döndür

**Mevcut Metod Güncellemeleri:**

- `index()` - Liste görünümünde bakiye kolonu eklenecek
- `show()` - Müşteri detay sayfasında bakiye kartı ve son işlemler gösterilecek
- `searchByPhone()` - Response'a balance field'ı eklenecek (JSON)

---

## 6. Route Tanımları

### 6.1 routes/web.php Güncellemeleri

**Menu Routes (Prepayment Balance System):**

```php
Route::prefix('menu')->name('menu.')->group(function () {
    // ... existing routes ...

    // Prepayment balance system endpoints (session-based, QR kod gönderilmez)
    Route::get('/balance', [MenuController::class, 'getBalance'])
        ->name('balance');

    Route::post('/check-balance', [MenuController::class, 'checkBalance'])
        ->name('check-balance');

    // checkout-with-credit endpoint'i YOK
    // Mevcut POST /menu/checkout route'u payment_collection'a göre dallanır
});
```

**Customer Routes (Balance Management):**

```php
Route::prefix('customers')->name('customers.')->group(function () {
    // ... existing routes ...

    // Balance management
    Route::get('{id}/add-credit', [PosCustomerController::class, 'showAddCreditForm'])
        ->name('add-credit-form');

    Route::post('{id}/add-credit', [PosCustomerController::class, 'addCredit'])
        ->name('add-credit');

    Route::get('{id}/balance-history', [PosCustomerController::class, 'balanceHistory'])
        ->name('balance-history');
});
```

---

## 7. View Güncellemeleri

### 7.1 Menu Cart View (`resources/views/menu/cart.blade.php`)

**Değişiklikler:**

1. **Bakiye Gösterimi:**

    ```blade
    @if($paymentCollection === 'prepayment' && isset($customerBalance))
    <div class="p-4 mb-4 border border-green-200 rounded-lg bg-green-50">
        <div class="flex items-center justify-between">
            <span class="font-medium text-green-800">Mevcut Bakiye:</span>
            <span class="text-2xl font-bold text-green-600">{{ number_format($customerBalance, 2) }} TL</span>
        </div>
    </div>
    @endif
    ```

2. **Sepet Onay Butonu:**
    - Yetersiz bakiye durumunda buton disabled
    - Hover'da tooltip: "Yetersiz bakiye"

3. **Hata Mesajı:**
    ```blade
    @if(session('insufficient_balance'))
    <div class="p-4 mb-4 border border-red-200 rounded-lg bg-red-50">
        <p class="text-red-800">
            Yetersiz bakiye. Mevcut: {{ session('current_balance') }} TL,
            Gerekli: {{ session('required_amount') }} TL
        </p>
    </div>
    @endif
    ```

---

### 7.2 Menu Customer QR View (`resources/views/menu/customer-qr.blade.php`)

**Değişiklikler:**

1. **QR Girişinde Bakiye Gösterimi:**
    ```blade
    @if($paymentCollection === 'prepayment' && isset($customerBalance))
    <div class="p-4 mb-4 border border-blue-200 rounded-lg bg-blue-50">
        <div class="flex items-center justify-between">
            <span class="font-medium text-blue-800">Mevcut Bakiyeniz:</span>
            <span class="text-xl font-bold text-blue-600">{{ number_format($customerBalance, 2) }} TL</span>
        </div>
    </div>
    @endif
    ```

---

### 7.3 Menu Orders View (`resources/views/menu/orders.blade.php`)

**Değişiklikler:**

1. **Sipariş Listesinde Ödeme Tipi:**
    ```blade
    @if($order->payment_type == 5)
    <span class="text-green-800 bg-green-100 badge">Bakiye</span>
    @endif
    ```

---

### 7.4 POS Customer Views

**Yeni View: `resources/views/customers/add-credit-form.blade.php`**

- Bakiye yükleme formu (modal veya ayrı sayfa)
- Tutar input
- Açıklama textarea
- Ödeme yöntemi select (cash, card, transfer)
- Submit button

**Yeni View: `resources/views/customers/balance-history.blade.php`**

- Müşteri bakiye geçmişi tablosu
- Running balance gösterimi
- Filtreleme (tarih aralığı, işlem tipi)
- Export butonu (opsiyonel)

**Güncellenecek View: `resources/views/customers/index.blade.php`**

- Liste tablosuna "Bakiye" kolonu eklenecek
- Bakiye sıralama özelliği

**Güncellenecek View: `resources/views/customers/show.blade.php`**

- Müşteri detay sayfasında bakiye kartı (prominent display)
- Son 10 işlem listesi
- "Bakiye Yükle" butonu (redirect to add-credit-form)
- "Bakiye Geçmişi" linki

---

## 8. Business Logic Flow

### 8.1 Bakiye Yükleme Akışı (POS)

```
1. POS Admin → Müşteri seç
2. "Bakiye Yükle" butonuna tıkla
3. GET /customers/{id}/add-credit → Form sayfası açılır
4. Form doldurulur:
   - Tutar gir
   - Açıklama ekle
   - Ödeme yöntemi seç
5. Submit → POST /customers/{id}/add-credit
6. Backend (PosCustomerController):
   - Validation
   - DB::connection('mysql-remote')->beginTransaction()
   - CreditTransaction oluştur (amount: +1000)
   - Transaction commit
   - Redirect + Flash message
7. UI güncellenir:
   - Müşteri detay sayfasına yönlendir
   - Success message göster
   - Yeni bakiye görüntüle
```

---

### 8.2 Sipariş ile Bakiye Düşme Akışı (Menü)

```
1. Müşteri QR kodu okutur → /menu/customer/{qr_code}
2. customerQr() metodu:
   a. payment_collection ayarını session'a yazar (index() ile paralel)
   b. payment_collection === 'prepayment' ise müşteri bakiyesini view'a pass eder
3. Menü açılır → Müşteri bakiyesini görür → Sepete ürün ekler
4. Sepet sayfasına gider → /menu/cart
   - Bakiye göstergesi: "Mevcut Bakiye: 750 TL"
   - Sepet toplamı: 350 TL
   - Kalan bakiye: 400 TL
5. "SİPARİŞİ GÖNDER" butonuna tıklar
   - Frontend: POST /menu/check-balance (AJAX, yetersizse buton disabled)
6. Checkout formu submit → POST /menu/checkout (MEVCUT endpoint)
7. Backend (MenuController@checkout - Transaction içinde):
   a. Mevcut validasyon ve müşteri işlemleri (aynı)
   b. session('payment_collection') === 'prepayment' kontrolü:
      - EVET → processCreditPayment() çağır:
        i.  DB::connection('mysql-remote')->beginTransaction()
        ii. Son CreditTransaction'ı lockForUpdate ile oku → balance_after = 750
        iii. Kontrol: 750 >= 350 ✓
        iv. CreditTransaction oluştur (amount: -350, balance_after: 400)
        v.  Sale payment_type = 5 güncelle
        vi. Order payment_type = 5 ile oluştur
        vii. Commit
      - HAYIR → Mevcut UNPAID akışı (payment_type = 0, değişiklik yok)
   c. Sale toplamlarını güncelle (aynı)
   d. Cart temizle (aynı)
   e. Print API çağır (aynı)
8. Response:
   - Redirect: /menu/orders
   - Flash: "Siparişiniz alındı. Kalan bakiye: 400.00 TL"
9. UI:
   - Sipariş onay ekranı göster
   - Ödeme tipi: "Bakiye ile ödendi" badge
```

---

### 8.3 Hata Yönetimi

**Senaryo 1: Yetersiz Bakiye**

```
1. Check balance (AJAX) → insufficient
2. JSON error response döndür
3. UI:
   - Hata mesajı göster (red alert box)
   - Checkout butonu disabled
   - Opsiyonel: "POS'tan bakiye yükleyin" bilgi mesajı
```

**Senaryo 2: Concurrent Requests (Race Condition)**

```
1. İki simultaneous request aynı anda checkout yapar
2. Database transaction + SELECT FOR UPDATE kullan
3. İlk request başarılı, ikinci request hata alır
4. İkinci request'e "Bakiye yetersiz" hatası döndür
5. Rollback yapılır
```

**Senaryo 3: QR Kod Süresi Dolmuş**

```
1. QR code inactive
2. Error: "Geçersiz QR kod"
3. Redirect to error page veya menu home
```

**Senaryo 4: Database Connection Hatası**

```
1. mysql-remote connection başarısız
2. Try-catch ile yakala
3. User-friendly error message
4. Log error for debugging
```

---

## 9. Settings Entegrasyonu

### 9.1 Firma Ayarları

**Settings Tablosu Kayıtları:**

```sql
-- Menü tipi
INSERT INTO settings (relation_type, relation_id, key, value)
VALUES ('company', 1, 'menu_type', 'dynamic_qr_verified');

-- Tahsilat tipi
INSERT INTO settings (relation_type, relation_id, key, value)
VALUES ('company', 1, 'payment_collection', 'prepayment');
```

### 9.2 MenuController Ayar Okuma

```php
// MenuController'da mevcut getMenuSetting() metodu TEK BİR DEĞER döndürür, array değil!
// Kullanım: getMenuSetting($companyId, $key, $default)

$paymentCollection = $this->getMenuSetting($companyId, 'payment_collection', 'cash_register');

if ($paymentCollection === 'prepayment') {
    // Bakiye sistemi devrede
    // Balance kontrolü yap
    // CreditTransaction kullan
    // DB::connection('mysql-remote') kullanmayı unutma!
} else {
    // Normal akış (UNPAID)
}
```

---

## 10. Güvenlik ve Validasyon

### 10.1 Authorization

**POS Tarafı:**

- Sadece authenticated admin kullanıcılar bakiye yükleyebilir
- Middleware: `auth:sanctum`, `verified`, `check.user.access`
- Web session authentication kullanılır

**Menü Tarafı:**

- QR kod doğrulama zorunlu
- Sadece kendi bakiyesini görebilir
- Sadece kendi bakiyesinden harcama yapabilir
- CSRF token validation aktif

### 10.2 Input Validation

**Bakiye Yükleme:**

```php
$validated = $request->validate([
    'amount' => 'required|numeric|min:1|max:50000',
    'description' => 'nullable|string|max:500',
    'payment_method' => 'required|in:cash,card,transfer',
]);
```

**Checkout (Prepayment):**

```php
// Prepayment modunda ek validasyon GEREKMİYOR
// Mevcut checkout validasyonu aynen kullanılır:
// - Tip B: name, surname, phone (gerekli)
// - Tip C: payer_id session'da zaten var
// Sepet içeriği ve bakiye SESSION'dAN okunur, request'ten değil
// Bu sayede client-side manipülasyon (fiyat değiştirme vb.) önlenir
```

### 10.3 Database Transaction

**ÖNEMLİ:** Tüm credit transaction işlemlerinde `mysql-remote` connection kullanılmalıdır!

```php
use Illuminate\Support\Facades\DB;

DB::connection('mysql-remote')->beginTransaction();
try {
    // 1. Son CreditTransaction'ı kilitle (balance_after oku)
    $lastTransaction = CreditTransaction::on('mysql-remote')
        ->where('customer_id', $customerId)
        ->where('company_id', $companyId)
        ->lockForUpdate()
        ->orderBy('id', 'desc')
        ->first();

    $currentBalance = $lastTransaction ? (float) $lastTransaction->balance_after : 0;

    // 2. Bakiye kontrolü
    if ($currentBalance < $totalAmount) {
        throw new \Exception('Yetersiz bakiye. Mevcut: ' . $currentBalance . ', Gerekli: ' . $totalAmount);
    }

    // 3. Order'ları ekle (payment_type = 5)
    foreach ($cart as $productId => $item) {
        Order::create([
            // ... mevcut alanlar ...
            'payment_type' => 5, // CREDIT
        ]);
    }

    // 4. Sale payment_type güncelle
    $sale->payment_type = 5;
    $sale->save();

    // 5. CreditTransaction oluştur (materialized balance)
    $newBalance = $currentBalance - $totalAmount;
    CreditTransaction::on('mysql-remote')->create([
        'customer_id' => $customerId,
        'company_id' => $companyId,
        'sale_id' => $sale->id,
        'amount' => -$totalAmount,
        'balance_after' => $newBalance,
        'transaction_type' => 'order_payment',
        'payment_type' => 5,
        'description' => "Sipariş #{$sale->id} ödemesi",
    ]);

    // 6. Commit
    DB::connection('mysql-remote')->commit();
} catch (\Exception $e) {
    DB::connection('mysql-remote')->rollBack();
    \Log::error('Credit transaction failed: ' . $e->getMessage());
    throw $e;
}
```

---

## 11. Test Senaryoları

### 11.1 Unit Tests

1. ✅ PosCustomer::addCredit() - Bakiye yükleme
2. ✅ PosCustomer::deductCredit() - Bakiye düşme
3. ✅ PosCustomer::hasSufficientBalance() - Bakiye kontrolü (company_id ile)
4. ✅ PosCustomer::getBalance() - Firma bazlı bakiye hesaplama
5. ✅ CreditTransaction scopes - Filtreleme
6. ✅ CreditTransaction::balance_after - Materialized balance doğruluğu

### 11.2 Feature Tests

1. ✅ POST /menu/check-balance - Yeterli bakiye
2. ✅ POST /menu/check-balance - Yetersiz bakiye
3. ✅ POST /menu/checkout - Prepayment başarılı sipariş (payment_type = 5)
4. ✅ POST /menu/checkout - Prepayment yetersiz bakiye (flash error)
5. ✅ POST /menu/checkout - Cash register akışı etkilenmez (payment_type = 0)
6. ✅ POST /customers/{id}/add-credit - Auth required
7. ✅ GET /menu/balance - Session-based doğru response
8. ✅ GET /customers/{id}/balance-history - Auth required

### 11.3 Integration Tests

1. ✅ Full flow: Bakiye yükle → Sipariş ver → Bakiye düş → Kalan bakiye doğrula
2. ✅ Concurrent requests - Race condition handling (lockForUpdate ile)
3. ✅ QR code expiration - İnaktif QR ile sipariş engelleme
4. ✅ Transaction rollback - Hata durumunda geri alma (balance_after değişmemeli)
5. ✅ Tip A ve Tip B - Prepayment değişikliklerinden etkilenmemeli

---

## 12. Mevcut Kod Tabanında Gereken Güncellemeler

### 12.1 MenuController@customerQr() — payment_collection session eklentisi

**Sorun:** `index()` (Tip B) session'a `payment_collection` yazar ama `customerQr()` (Tip C) yazmaz. Bu olmadan prepayment akışı hiç tetiklenmez.

**Gerekli değişiklik:**

```php
// customerQr() metoduna eklenecek (index() ile paralel):
$paymentCollection = $this->getMenuSetting($customerQr->company_id, 'payment_collection', 'cash_register');
session(['payment_collection' => $paymentCollection]);
```

---

### 12.2 MenuController@customerQr() — Bakiye view'a pass edilmesi

**Gerekli değişiklik:**

```php
// customerQr() metodunda, return view() öncesine:
$customerBalance = null;
if ($paymentCollection === 'prepayment' && $customer) {
    $customerBalance = $customer->getBalance($customerQr->company_id);
}

// return view() içine eklenecek:
'customerBalance' => $customerBalance,
'paymentCollection' => $paymentCollection,
```

---

### 12.3 MenuController@cart() — Bakiye view'a pass edilmesi

**Gerekli değişiklik:**

```php
// cart() metoduna eklenecek:
$paymentCollection = session('payment_collection');
$customerBalance = null;
if ($paymentCollection === 'prepayment' && $saleId) {
    $sale = PosSale::withoutGlobalScope('completed_sales_only')->find($saleId);
    if ($sale && $sale->payer_id) {
        $customer = PosCustomer::find($sale->payer_id);
        if ($customer) {
            $customerBalance = $customer->getBalance($companyId);
        }
    }
}

// return view() içine eklenecek:
'paymentCollection' => $paymentCollection,
'customerBalance' => $customerBalance,
```

---

### 12.4 PosSale::createMenuSale() — payment_type parametresi

**Mevcut:** `payment_type => 0` hardcoded (satır 189)

**Çözüm:** Checkout'ta güncelleme yapılabilir (createMenuSale'a parametre eklemeye gerek yok):

```php
// checkout() metodunda, processCreditPayment() içinde:
$sale->payment_type = 5; // CREDIT
$sale->save();
```

---

### 12.5 Dashboard — "Ön Ödeme" radio butonu aktifleştirme

**Dosya:** `resources/views/dashboard.blade.php`

**Mevcut durum:** "Ön Ödeme" radio butonu `disabled` ve `opacity-60` class'ına sahip (satır 305-320).

**Gerekli değişiklikler:**

1. `disabled` attribute'unu kaldır
2. `opacity-60` class'ını kaldır
3. "YAKINDA" etiketini kaldır
4. `value="prepayment"` olarak güncelle
5. Livewire/JS ile `payment_collection` setting kaydetme fonksiyonu ekle
6. MenuSettings Livewire bileşenine `payment_collection` yönetimi ekle

---

## 13. Deployment Checklist

### 13.1 Pre-Deployment

- [ ] Migration dosyası hazır (remote DB'ye uygulanacak)
- [ ] Modeller oluşturuldu (CreditTransaction, PosCustomer güncelleme)
- [ ] Web routes tanımlandı (routes/web.php — balance ve check-balance)
- [ ] Controller metodları yazıldı (MenuController: getBalance, checkBalance, processCreditPayment)
- [ ] Mevcut checkout() metoduna payment_collection dallanması eklendi
- [ ] customerQr() metoduna payment_collection session yazımı eklendi
- [ ] cart() metoduna bakiye view verisi eklendi
- [ ] View dosyaları güncellendi (cart, customer_qr, orders, dashboard)
- [ ] Dashboard "Ön Ödeme" radio butonu aktifleştirildi
- [ ] MenuSettings Livewire bileşenine payment_collection yönetimi eklendi
- [ ] Validation kuralları eklendi
- [ ] Error handling tamamlandı
- [ ] DB::connection('mysql-remote') tüm yerlerde kullanıldı
- [ ] Tip A ve Tip B akışlarının etkilenmediği doğrulandı
- [ ] Unit tests yazıldı
- [ ] Feature tests yazıldı

### 13.2 Deployment Steps

1. **Backup:**

    ```bash
    mysqldump -u user -p poscloud_db > backup_$(date +%Y%m%d).sql
    ```

2. **Migration:**

    ```bash
    php artisan migrate --path=database/migrations/2026_04_XX_create_credit_transactions_table.php
    ```

    **Not:** Migration remote database'e uygulanmalı!

3. **Cache Clear:**

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

4. **Test:**
    - POS'ta bakiye yükleme test et
    - Menüden sipariş verme test et
    - Bakiye geçmişi görüntüle

### 13.3 Post-Deployment

- [ ] Monitoring: Error logs izle
- [ ] Performance: Query performance kontrol et
- [ ] User Feedback: İlk kullanıcı geri bildirimlerini topla

---

## 14. Gelecek İyileştirmeler

### 14.1 Kısa Vadeli (1-2 Hafta)

- [ ] Bakiye uyarıları (düşük bakiye bildirimi)
- [ ] Otomatik bakiye yenileme (auto-reload)
- [ ] Bakiye limiti (maksimum bakiye sınırı)

### 14.2 Orta Vadeli (1-2 Ay)

- [ ] Bakiye bonus sistemi (1000 TL yükle → 50 TL bonus)
- [ ] Sadakat puanları entegrasyonu
- [ ] Aylık bakiye raporu (email)

### 14.3 Uzun Vadeli (3-6 Ay)

- [ ] Aile hesabı (ana hesap + alt hesaplar)
- [ ] Kurumsal hesaplar (şirket bakiyesi)
- [ ] Bakiye transferi (müşteriler arası)

---

## 15. Örnek Kullanım Senaryosu

### Senaryo: Lakeside Hotel — Dinamik QR ile Ön Ödemeli Sipariş

**Firma:** Lakeside Hotel (company_id: 3, slug: `lakeside-hotel`)
**Konfigürasyon:** Menü Tipi C + Tahsilat Tipi 2 (prepayment)
**Müşteri:** Ahmet Yılmaz (customer_id: 42, phone: 5551234567)

---

#### Adım 1: POS — Firma Ayarları

```
POS Admin → Dashboard → Menü Modu: "Dinamik QR ile Doğrulamalı Sipariş" seçili
POS Admin → Dashboard → Tahsilat Tipi: "Ön Ödeme" seçili (radio button aktif)
```

**Settings tablosu:**

```sql
-- company_id = 3 için
settings: { key: 'digital_menu_enabled', value: '1' }
settings: { key: 'payment_collection', value: 'prepayment' }
```

---

#### Adım 2: POS — Müşteri Oluşturma + Bakiye Yükleme

```
POS Admin → Müşteriler → Yeni Müşteri
  İsim: Ahmet
  Soyisim: Yılmaz
  Telefon: 5551234567
  → Kaydet (customer_id: 42 oluşturuldu)
```

```
POS Admin → Müşteriler → Ahmet Yılmaz → Bakiye Yükle
  Tutar: 1000.00 TL
  Açıklama: "Girişte nakit bakiye yükleme"
  Ödeme Yöntemi: Nakit
  → Yükle
```

**credit_transactions tablosu:**

| id  | customer_id | company_id | sale_id | amount   | balance_after | transaction_type | payment_type | description                  |
| --- | ----------- | ---------- | ------- | -------- | ------------- | ---------------- | ------------ | ---------------------------- |
| 1   | 42          | 3          | NULL    | +1000.00 | 1000.00       | credit_load      | 5            | Girişte nakit bakiye yükleme |

**UI:** Flash message → "₺1000.00 bakiye yüklendi. Yeni bakiye: ₺1000.00"

---

#### Adım 3: POS — QR Kod Oluşturma

```
POS Admin → Müşteriler → Ahmet Yılmaz → QR Kod Oluştur
  → QR kod üretilir (qr_code: "a7f3b2c1-4d5e-6f7a-8b9c-0d1e2f3a4b5c")
  → QR yazdırılır (PDF)
```

**customer_qr_codes tablosu:**

| id  | customer_id | company_id | qr_code      | is_active | table_id |
| --- | ----------- | ---------- | ------------ | --------- | -------- |
| 15  | 42          | 3          | a7f3b2c1-... | true      | NULL     |

**QR kod URL'si:** `https://menu.apper.com.tr/menu/customer/a7f3b2c1-4d5e-6f7a-8b9c-0d1e2f3a4b5c`

---

#### Adım 4: Müşteri — QR Kod Tarama

```
Ahmet telefondan QR kodu tarar
→ GET /menu/customer/a7f3b2c1-4d5e-6f7a-8b9c-0d1e2f3a4b5c
→ MenuController@customerQr()
```

**Backend akışı:**

1. `CustomerQrCode::findActiveByCode('a7f3b2c1-...')` → bulundu, aktif ✓
2. Müşteri: Ahmet Yılmaz bulundu
3. `payment_collection = 'prepayment'` session'a yazıldı
4. Bakiye hesaplandı: `$customer->getBalance(3)` → **1000.00 TL**
5. Aktif sale yok → masa seçimi gerekli

**Session:**

```
selected_pos_service_id = 3
menu_customer_qr_id = 15
menu_type = 'customer_qr'
payment_collection = 'prepayment'
```

**View (customer_qr.blade.php):**

```
┌──────────────────────────────────┐
│  🏨 Lakeside Hotel               │
│  www.lakeside-hotel.com          │
├──────────────────────────────────┤
│  👤 Ahmet Yılmaz                 │
│  📱 5551234567                   │
│  💰 Mevcut Bakiyeniz: 1.000 TL   │  ← MAVİ KART (sadece prepayment)
├──────────────────────────────────┤
│  🪑 [ MASA SEÇ ]                 │  ← Buton (masa atanmamış)
├──────────────────────────────────┤
│  ▼ İçecekler                     │
│    Çay           40,00 TL  [+]   │
│    Türk Kahvesi  60,00 TL  [+]   │
│    Soda          30,00 TL  [+]   │
│  ▼ Ana Yemekler                  │
│    Köfte         280,00 TL [+]   │
│    Tavuk Şiş     250,00 TL [+]   │
│  ▼ Tatlılar                      │
│    Künefe        180,00 TL [+]   │
│    Baklava       150,00 TL [+]   │
└──────────────────────────────────┘
│ 🛒 Sepet (0)                     │
```

---

#### Adım 5: Müşteri — Masa Seçimi

```
"MASA SEÇ" butonuna tıklar
→ Masa seçim modal açılır
→ "Masa 5" seçilir
→ POST /menu/assign-table { table_id: 12 }
```

**Backend:**

- `PosSale::createMenuSale(3, 12, 'table')` → sale_id: **87**
- `sale->payer_id = 42` (Ahmet Yılmaz)
- Session: `menu_sale_id = 87`, `menu_table_id = 12`

---

#### Adım 6: Müşteri — Ürünleri Sepete Ekleme

```
Ahmet menüden seçim yapar:
  → Köfte (+1) → POST /menu/add-to-cart
  → Türk Kahvesi (+1) → POST /menu/add-to-cart
  → Künefe (+1) → POST /menu/add-to-cart
```

**Session (menu_cart):**

```json
{
    "101": { "name": "Köfte", "price": 280, "quantity": 1 },
    "103": { "name": "Türk Kahvesi", "price": 60, "quantity": 1 },
    "107": { "name": "Künefe", "price": 180, "quantity": 1 }
}
```

**Sepet tutarı:** 280 + 60 + 180 = **520 TL**

---

#### Adım 7: Müşteri — Sepet Sayfası

```
Sepet ikonuna tıklar → GET /menu/cart
```

**View (cart.blade.php):**

```
┌──────────────────────────────────┐
│  SEPET                    ✕      │
│  Masa: Masa 5                    │
├──────────────────────────────────┤
│  💰 Mevcut Bakiye: 1.000 TL      │  ← YEŞİL KART (prepayment)
├──────────────────────────────────┤
│  Köfte              280 TL       │
│  [−] 1 [+]                      │
│  Türk Kahvesi        60 TL       │
│  [−] 1 [+]                      │
│  Künefe             180 TL       │
│  [−] 1 [+]                      │
├──────────────────────────────────┤
│  Toplam Tutar:     520 TL       │
│  Kalan Bakiye:     480 TL       │  ← Sadece prepayment
├──────────────────────────────────┤
│  [Sipariş notu...]               │
│  [ SİPARİŞİ GÖNDER ]            │
└──────────────────────────────────┘
```

---

#### Adım 8: Müşteri — Checkout

```
"SİPARİŞİ GÖNDER" tıklar → Checkout modal açılır
→ Müşteri bilgisi read-only (Tip C):
   Ahmet Yılmaz / 5551234567
→ Masa: Masa 5
→ "ONAYLA" tıklar → POST /menu/checkout
```

**Backend (MenuController@checkout):**

```php
$menuType = session('menu_type');           // 'customer_qr'
$paymentCollection = session('payment_collection'); // 'prepayment'

// Mevcut validasyon + müşteri işlemleri (Tip C, payer_id zaten var)

// payment_type belirleme:
$paymentType = $paymentCollection === 'prepayment' ? 5 : 0; // = 5

// Order oluşturma (3 order):
Order::create([
    'sale_id' => 87, 'product_id' => 101, 'price' => 280,
    'total_price' => 280, 'quantity' => 1,
    'payment_type' => 5,  // CREDIT
    'channel' => 'menu', 'status' => 'new',
]);
Order::create([
    'sale_id' => 87, 'product_id' => 103, 'price' => 60,
    'total_price' => 60, 'quantity' => 1,
    'payment_type' => 5,  // CREDIT
    'channel' => 'menu', 'status' => 'new',
]);
Order::create([
    'sale_id' => 87, 'product_id' => 107, 'price' => 180,
    'total_price' => 180, 'quantity' => 1,
    'payment_type' => 5,  // CREDIT
    'channel' => 'menu', 'status' => 'new',
]);

// Prepayment: processCreditPayment()
DB::connection('mysql-remote')->beginTransaction();
$lastTx = CreditTransaction::where('customer_id', 42)
    ->where('company_id', 3)
    ->lockForUpdate()->orderBy('id', 'desc')->first();
// balance_after = 1000.00 ✓ (1000 >= 520)

CreditTransaction::create([
    'customer_id' => 42,
    'company_id' => 3,
    'sale_id' => 87,
    'amount' => -520.00,
    'balance_after' => 480.00,      // 1000 - 520
    'transaction_type' => 'order_payment',
    'payment_type' => 5,
    'description' => 'Sipariş #87 ödemesi',
]);

$sale->payment_type = 5;
$sale->save();

DB::connection('mysql-remote')->commit();

// Sale toplamları güncelle, cart temizle, print API
```

**credit_transactions tablosu (güncel):**

| id  | customer_id | company_id | sale_id | amount   | balance_after | transaction_type |
| --- | ----------- | ---------- | ------- | -------- | ------------- | ---------------- |
| 1   | 42          | 3          | NULL    | +1000.00 | 1000.00       | credit_load      |
| 2   | 42          | 3          | 87      | -520.00  | 480.00        | order_payment    |

**sales tablosu (güncel):**

| id  | company_id | destination_id | payer_id | payment_type | channel | gross_price |
| --- | ---------- | -------------- | -------- | ------------ | ------- | ----------- |
| 87  | 3          | 12             | 42       | **5**        | menu    | 520.00      |

---

#### Adım 9: Müşteri — Sipariş Onay Sayfası

```
→ Redirect: /menu/orders
→ Flash: "Siparişiniz alındı. Kalan bakiye: 480.00 TL"
```

**View (orders.blade.php):**

```
┌──────────────────────────────────┐
│  SİPARİŞLER                      │
│  Masa: Masa 5                    │
├──────────────────────────────────┤
│  Köfte            280 TL  [Bakiye]│ ← Yeşil badge
│  Türk Kahvesi      60 TL  [Bakiye]│
│  Künefe           180 TL  [Bakiye]│
├──────────────────────────────────┤
│  Toplam: 520 TL                  │
│  💰 Kalan Bakiye: 480 TL         │
└──────────────────────────────────┘
```

---

#### Adım 10: İkinci Sipariş (Başarılı) + Üçüncü Sipariş (Bakiye Yetersiz)

**İkinci sipariş:**

```
Ahmet menüye döner, yeni ürün ekler:
  → Tavuk Şiş (+1) → 250 TL
  → Baklava (+1) → 150 TL
  → Sepet tutarı: 400 TL
  → Kalan bakiye: 480 TL → 480 >= 400 ✓ YETERLİ
  → Sipariş başarılı → balance_after = 80.00 TL
```

**credit_transactions tablosu (güncel):**

| id  | customer_id | company_id | sale_id | amount   | balance_after | transaction_type |
| --- | ----------- | ---------- | ------- | -------- | ------------- | ---------------- |
| 1   | 42          | 3          | NULL    | +1000.00 | 1000.00       | credit_load      |
| 2   | 42          | 3          | 87      | -520.00  | 480.00        | order_payment    |
| 3   | 42          | 3          | 88      | -400.00  | 80.00         | order_payment    |

**Üçüncü sipariş denemesi:**

```
  → Künefe (+1) → 180 TL
  → Sepet tutarı: 180 TL
  → Bakiye: 80 TL → 80 < 180 ✗ YETERSİZ
  → "SİPARİŞİ GÖNDER" butonu DISABLED
  → Kırmızı uyarı: "Yetersiz bakiye. Mevcut: 80 TL, Gerekli: 180 TL, Eksik: 100 TL"
```

**cart.blade.php (yetersiz bakiye durumu):**

```
┌──────────────────────────────────┐
│  SEPET                           │
│  Masa: Masa 5                    │
├──────────────────────────────────┤
│  💰 Mevcut Bakiye: 80 TL         │  ← KIRMIZI KART
├──────────────────────────────────┤
│  Künefe             180 TL       │
│  [−] 1 [+]                      │
├──────────────────────────────────┤
│  Toplam Tutar:     180 TL       │
│  ❌ Yetersiz bakiye!             │
│  Mevcut: 80 TL / Gerekli: 180 TL│
│  Eksik: 100 TL                   │
├──────────────────────────────────┤
│  [ SİPARİŞİ GÖNDER ] (disabled) │  ← Gri buton
└──────────────────────────────────┘
```

---

#### Adım 11: Masa Kapatma

```
POS Admin → Adisyonlar → Sale #87 → Masa Kapat
→ sale.deleted_at = now()
→ Customer QR deaktivasyonu (is_active = false)
→ Bakiye KORUNUR (80 TL hâlâ mevcut, müşteri tekrar bakiye yükleyebilir)
```

---

#### Veri Akış Özeti

```mermaid
graph TD
    A[POS: Müşteri Oluştur] --> B[POS: Bakiye Yükle +1000]
    B --> C[POS: QR Kod Üret]
    C --> D[Müşteri: QR Tara]
    D --> E[Session: payment_collection=prepayment]
    E --> F[Masa Seç]
    F --> G[Ürünleri Sepete Ekle]
    G --> H[Sepet: Bakiye Göster]
    H --> I{Bakiye Yeterli?}
    I -->|Evet| J[Checkout: processCreditPayment]
    I -->|Hayır| K[Buton Disabled + Uyarı]
    J --> L[CreditTransaction: -520]
    L --> M[Sale: payment_type=5]
    M --> N[Orders: payment_type=5]
    N --> O[Sipariş Onay: Kalan 480 TL]
```

---

## Özet

Bu dokümantasyon, `credit_transactions` tablosu tabanlı ön ödemeli bakiye sisteminin tüm teknik detaylarını içermektedir. Sistem mevcut Laravel web route mimarisine tam entegre edilmiştir:

✅ **Web Route Entegrasyonu** — Ayrı API katmanı yok, mevcut structure kullanılıyor  
✅ **Session-Based Auth** — Web session authentication ve CSRF protection  
✅ **Mevcut Checkout Entegrasyonu** — Ayrı endpoint değil, mevcut `checkout()` metoduna dallanma  
✅ **Remote Database** — Tüm işlemlerde `DB::connection('mysql-remote')` kullanımı  
✅ **Materialized Balance** — `balance_after` sütunu ile race condition koruması  
✅ **Güvenli** — lockForUpdate, transaction, authorization ve validation  
✅ **Ölçeklenebilir** — Indexes ve optimized queries  
✅ **Test Edilebilir** — Comprehensive test suite  
✅ **Geriye Dönük Uyumlu** — Tip A ve Tip B etkilenmez

**Kritik Notlar:**

1. Tüm database işlemlerinde `DB::connection('mysql-remote')` kullanılmalı
2. `company_id` NOT NULL olmalı — bakiye her zaman müşteri+firma bazlı
3. `sale_id` doğrudan FK, polimorfik `reference_type` kullanılmaz
4. Accessor kullanılmaz, `getBalance($companyId)` method kullanılır
5. `checkout-with-credit` ayrı endpoint YOK — mevcut `checkout()` içine entegre
6. QR kod her request'te gönderilmez, session'dan okunur
7. `balance_after` ile materialized balance — race condition koruması
8. `customerQr()` metoduna `payment_collection` session yazımı ZORUNLU
9. Dashboard "Ön Ödeme" radio butonu aktifleştirilmeli

---

**Son Güncelleme:** 2026-04-26  
**Versiyon:** 3.1  
**Durum:** Planlama Tamamlandı - Implementasyon Bekliyor  
**v3.1 Değişiklikler:** Bölüm 15 eklendi — uçtan uca örnek kullanım senaryosu (Tip C + Ön Ödeme: QR tarama → bakiye yükleme → sipariş → yetersiz bakiye senaryosu → masa kapatma)  
**v3.0 Değişiklikler:** checkout-with-credit ayrı endpoint kaldırıldı → mevcut checkout() entegrasyonu, reference_type → sale_id doğrudan FK, balance_after materialized balance eklendi, accessor kaldırıldı → getBalance() method, session-based yaklaşım (QR kod request'te gönderilmez), lockForUpdate doğru hedef (son CreditTransaction), customerQr() session eksikliği belgelendi, dashboard radio aktifleştirme eklendi
