# Checkout Sistemi - Discount ve Treat Analizi

## Genel Bakış

Bu doküman, poscloud.apper projesindeki checkout sisteminde discount (indirim) ve treat (ikram) mekanizmalarının nasıl çalıştığını detaylı olarak açıklar.

---

## 1. Temel Kavramlar

### Treat (İkram)
- **Tanım**: Tüm satışın ücretsiz olduğu durum (tahsil edilecek tutar = 0)
- **Payment Type**: `PAYMENT_TYPE['treat']` olarak ayarlanır
- **Kullanım**: Müşteri hiçbir ödeme yapmaz, tüm sipariş ikram edilir
- **Sabee Entegrasyonu**: Toplam fiyat 0 olarak gönderilir

### Discount (İndirim)
- **Tanım**: Satış tutarının bir kısmının indirime tabi tutulması
- **Storage**: `sale.discount` ve `sale.discount_pct` alanlarında saklanır
- **Formül**: `discount = net_price - checkoutAmount`
- **Örnek**: Net fiyat 100 TL, tahsil edilen 80 TL → Discount = 20 TL

---

## 2. Veri Modeli Yapısı

### Order Seviyesi (orders tablosu)

| Alan | Açıklama | Discount/Treat İlişkisi |
|------|----------|------------------------|
| `payment_type` | Ödeme tipi | ✅ Treat flag'i burada tutulur (`PAYMENT_TYPE['treat']`) |
| `total_price` | Order toplam fiyatı | ✅ Her order kendi fiyatını korur |
| `price` | Birim fiyat | ✅ Product fiyatı + spec farkları |
| `vat` | KDV | ✅ Order bazında hesaplanır |
| `service_fee` | Servis ücreti | ✅ Order bazında hesaplanır |
| `discount` | ❌ YOK | Order seviyesinde discount bilgisi tutulmaz |

**Önemli**: Order tablosunda discount alanı yoktur. Her order kendi `total_price` değerini korur.

### Sale Seviyesi (sales tablosu)

| Alan | Açıklama | Hesaplama |
|------|----------|-----------|
| `gross_price` | Brüt toplam | Tüm order'ların `total_price` toplamı |
| `net_price` | Net toplam | `gross_price - discount` |
| `discount` | İndirim tutarı | `net_price - checkoutAmount` veya `%` bazlı |
| `discount_pct` | İndirim yüzdesi | Manuel set edilir, varsa otomatik hesaplanır |
| `treat` | İkram toplamı | Treat olan order'ların toplamı |
| `cancel` | İptal toplamı | Cancel olan order'ların toplamı |
| `num_orders` | Sipariş sayısı | Aktif order sayısı |
| `num_treats` | İkram sayısı | Treat order sayısı |
| `num_cancels` | İptal sayısı | Cancel order sayısı |

---

## 3. Hesaplama Mantığı

### Sale::updateTotalPrice() Metodu (Sale.php:159-217)

```php
public function updateTotalPrice($persist = false)
{
    $orders = $this->allOrders()->select([...])->get();
    
    // Değişkenleri sıfırla
    $grossPrice = $netPrice = $treat = $cancel = $vat = $serviceFee = 0;
    $numOrders = $numTreats = $numCancels = 0;

    foreach ($orders as $order) {
        // 1. Cancel order'ları işle
        if ($order->isCancelled()) {
            $cancel     += $order->total_price;
            $numCancels += $order->quantity;
            continue;  // Diğer kategorilere girmez
        }

        // 2. Gross price hesapla
        if ($order->isForGross()) {
            $grossPrice += $order->total_price;
            $numOrders  += $order->quantity;
        }

        // 3. Treat order'ları izle
        if ($order->isTreat()) {
            $treat     += $order->total_price;
            $numTreats += $order->quantity;
        }

        // 4. Net price hesapla
        if ($order->isForNet()) {
            $netPrice   += $order->total_price;
            $vat        += $order->vat * $order->quantity;
            $serviceFee += $order->service_fee * $order->quantity;
        }
    }

    // 5. Percentage-based discount varsa hesapla
    if (+$this->discount_pct) {
        $this->discount = $netPrice * ($this->discount_pct * .01);
    }

    // 6. Net price'dan discount'u düş
    $netPrice = $netPrice - $this->discount;

    // 7. Sale alanlarını güncelle
    $this->gross_price = currencyRound($grossPrice);
    $this->net_price   = currencyRound($netPrice);
    $this->num_orders  = ceil($numOrders);
    $this->vat         = currencyRound($vat);
    $this->service_fee = currencyRound($serviceFee);
    $this->treat       = currencyRound($treat);
    $this->cancel      = currencyRound($cancel);
    $this->num_treats  = ceil($numTreats);
    $this->num_cancels = ceil($numCancels);

    if ($persist) {
        $this->save();
    }
}
```

**Akış Diyagramı:**
```
Order Loop
  ├─ isCancelled()?
  │   ├─ EVET → cancel += total_price, continue
  │   └─ HAYIR ↓
  ├─ isForGross()?
  │   └─ EVET → grossPrice += total_price
  ├─ isTreat()?
  │   └─ EVET → treat += total_price
  └─ isForNet()?
      └─ EVET → netPrice += total_price, vat += ..., serviceFee += ...

Discount Hesaplama
  ├─ discount_pct var mı?
  │   ├─ EVET → discount = netPrice * (discount_pct / 100)
  │   └─ HAYIR → discount manuel set edilmiş olmalı
  └─ netPrice = netPrice - discount

Sale Update
  └─ gross_price, net_price, treat, cancel, vat, service_fee kaydet
```

---

## 4. Checkout İşlemi (CheckoutRepository.php)

### checkout() Metodu (Satır 254-379)

#### Adım 1: Sale Split (Kısmi Checkout)
```php
// Seçili order'lar varsa sale'i böl
if (count($selectedOrders)) {
    $sale = $sale->split($selectedOrders);
    $total_quantity = $sale->num_orders;
    $total_price    = $sale->net_price;
} 
// Fiyat bazlı kısmi checkout
else if ($isArbitraryPartialCheckout) {
    $sale = $sale->splitByPrice($checkoutAmount);
    $total_quantity = $sale->num_orders;
    $total_price    = $sale->net_price;
}
```

#### Adım 2: Payment Type Belirleme
```php
// checkoutAmount == 0 ise treat, değilse seçilen ödeme tipi
$paymentType = $checkoutAmount == 0 ? PAYMENT_TYPE['treat'] : PAYMENT_TYPE[$action];
```

#### Adım 3: Credit/Sabee İşlemleri
```php
// Credit ödeme
if ($action === 'credit') {
    $process = $this->createCreditRecord($sale, $checkoutAmount, $customerId);
    $payerId = $customerId;
} 
// Sabee entegrasyonu
else if ($action === 'sabee') {
    $process = $this->createSabeeRecord($sale, $checkoutAmount, $roomId, $reservationCode);
    $payerId = $roomId;
}
```

#### Adım 4: Kredi Bakiyesi Kullanımı
```php
if ($sale->payer && $sale->payer->creditBalance() > 0 && $creditBalanceAction) {
    if ($creditBalanceAction === 'add_to_sale') {
        // Bahşiş olarak ekle
        $creditBalance = $sale->payer->creditBalance();
        $this->addTipOrderToSale($sale, $creditBalance);
        $sale->payer->useCreditBalance($sale, $paymentTypeForBalance);
        // checkoutAmount'u güncelle (sale + tip)
        $checkoutAmount = $sale->fresh()->net_price;
    } elseif ($creditBalanceAction === 'refund') {
        // İade et
        $sale->payer->refundCreditBalance($sale, $paymentTypeForBalance);
    }
}
```

#### Adım 5: Order'ları Güncelle
```php
// Ödenemez order'ları soft delete et
$sale->orders()->unpayable()->update([
    'deleted_at' => Carbon::now(),
]);

// Ödenebilir order'ların payment_type'ını güncelle ve soft delete et
$sale->orders()->payable()->update([
    'payment_type' => $paymentType,  // 'treat' veya seçilen tip
    'deleted_at'   => Carbon::now(),
]);
```

#### Adım 6: Discount Hesaplama ve Sale Güncelleme
```php
// Sale güncelleme verilerini hazırla
$updateData = [
    'payment_type' => $paymentType,
    'deleted_at'   => Carbon::now(),
];

// payer_id varsa ekle
if ($payerId !== null) {
    $updateData['payer_id'] = $payerId;
}

// channel bilgilerini koru
if ($sale->channel !== null) {
    $updateData['channel'] = $sale->channel;
}
if ($sale->channel_id !== null) {
    $updateData['channel_id'] = $sale->channel_id;
}

// financial_day_id yoksa ekle (QR menu gibi external sales için)
if (!$sale->financial_day_id) {
    $updateData['financial_day_id'] = FinancialDay::getCreateActiveEntry()->id;
}

$sale->update($updateData);

// Discount hesapla
$discount = $sale->net_price - $checkoutAmount;
if ($discount) {
    $sale->discount = $discount;
}

// Toplam fiyatları yeniden hesapla
$sale->updateTotalPrice(true);

// FinancialDay'i güncelle
FinancialDay::getCreateActiveEntry()->recalculate(true);
```

---

## 5. Sabee Entegrasyonunda Discount ve Treat

### createSabeeRecord() Metodu (Satır 419-492)

#### Treat Kontrolü
```php
$isTreat = $checkoutAmount == 0;
```

#### Room Bilgisi Ekleme
```php
$roomInfo = "Sabee - Oda: {$room->name}";
if ($reservationCode) {
    $roomInfo .= ", Rezervasyon: {$reservationCode}";
}

$existingNote = trim($sale->note ?? '');
$newNote = !empty($existingNote) 
    ? $existingNote . " | " . $roomInfo 
    : $roomInfo;

$sale->update(['note' => $newNote]);
```

#### Discount Dağıtımı (Order Gruplarına)
```php
// Orders'ları invoice_title'a göre grupla
$orderGroups = $sale->orders->map(function ($order) {
    $order->invoice_title = array_first($order->getCategoryInvoiceTitles());
    return $order;
})
->groupBy('invoice_title')
->map(function ($groupedOrders) use ($saleNetPrice, $discount, $isTreat) {
    
    if ($isTreat) {
        // Treat ise tüm gruplar 0
        $groupTotalPrice = 0;
        $discount = 0;
    } else {
        // Normal durumda oranlı dağıt
        $groupTotalPrice = $groupedOrders
            ->whereNotInStrict('payment_type', [PAYMENT_TYPE['treat'], PAYMENT_TYPE['cancel']])
            ->sum('total_price');
        
        // Discount'u oranlı dağıt
        $discount = currencyRound($discount * ($groupTotalPrice / $saleNetPrice));
    }

    return [
        'orders'    => $groupedOrders,
        'net_price' => $groupTotalPrice,
        'discount'  => $discount,
    ];
});
```

**Örnek Discount Dağıtımı:**
```
Toplam Satış: 100 TL
Discount: 20 TL

Grup 1 (Yemekler): 60 TL
  → Discount = 20 * (60/100) = 12 TL

Grup 2 (İçecekler): 40 TL
  → Discount = 20 * (40/100) = 8 TL
```

#### submitToSabee() Metodu (Satır 508-643)

##### Services Array Oluşturma
```php
foreach ($orders as $order) {
    if ($order->isCancelled()) continue;

    $price = 0;

    // Sync edilmiş product
    if ($order->product->sabee_id) {
        $price = $order->price + $order->vat + $order->service_fee;
    } 
    // Sync edilmemiş product (extra service fee ayrı gönderilir)
    else if (!$order->isTreat()) {
        $price = $order->price + $order->vat;
        $extraServiceFee += $order->service_fee * $order->quantity;
    }

    // Treat ise fiyat 0
    if ($order->isTreat() || $isTreat) {
        $price = 0;
    } else {
        $price = toSabeeCurrency($price);
    }

    $services[] = [
        "name"        => $order->product->name,
        "quantity"    => $order->quantity,
        "price"       => (float)$price,
        'vat'         => (float)$order->product->vat_pct,
        "submit_time" => $order->created_at->format('Y-m-d H:i:s'),
    ];

    if ($order->product->sabee_id) {
        $service['service_id'] = $order->product->sabee_id;
    }
}
```

##### Extra Service Fee Ekleme
```php
if ($extraServiceFee && !$isTreat) {
    $services[] = [
        "name"        => transUc('generic.financial.service_fee'),
        "quantity"    => 1,
        "price"       => toSabeeCurrency($extraServiceFee),
        'vat'         => 0,
        "submit_time" => Carbon::now()->format('Y-m-d H:i:s'),
    ];
}
```

##### Discount Ekleme (Negatif Servis Olarak)
```php
if ($discount && !$isTreat) {
    $services[] = [
        "name"        => transUc('generic.financial.discount'),
        "quantity"    => 1,
        "price"       => toSabeeCurrency($discount * -1),  // NEGATIF DEĞER
        'vat'         => 0,
        "submit_time" => Carbon::now()->format('Y-m-d H:i:s'),
    ];
}
```

##### Total Price Belirleme
```php
if ($isTreat) {
    $totalPrice = 0;  // Treat ise toplam 0
} else {
    $totalPrice = toSabeeCurrency($saleNetPrice);
}
```

##### Sabee API Çağrısı
```php
$sabeeClient->serviceSubmit([
    "reference_id"     => "bulut-adisyon-$saleId",
    "reservation_code" => $reservationCode,
    "group_id"         => (string)$saleId,
    "group_name"       => $groupName,
    "services"         => $services,
    "total_price"      => (float)$totalPrice,
    "user"             => user()->fullname,
]);
```

---

## 6. Örnek Senaryolar

### Senaryo 1: Normal Checkout (Discount Yok)

**Başlangıç:**
- Sale net_price: 100 TL
- Checkout amount: 100 TL
- Payment type: cash

**İşlem:**
```php
$discount = 100 - 100 = 0  // Discount yok
$paymentType = PAYMENT_TYPE['cash']
```

**Sonuç:**
- Sale.discount = 0
- Sale.payment_type = 'cash'
- Order.payment_type = 'cash' (tüm order'lar)

---

### Senaryo 2: Partial Checkout (20 TL Discount)

**Başlangıç:**
- Sale net_price: 100 TL
- Checkout amount: 80 TL
- Payment type: cash

**İşlem:**
```php
$discount = 100 - 80 = 20  // 20 TL discount
$paymentType = PAYMENT_TYPE['cash']
```

**Sonuç:**
- Sale.discount = 20
- Sale.net_price = 80 (updateTotalPrice sonrası)
- Order.payment_type = 'cash'

---

### Senaryo 3: Treat (Tam İkram)

**Başlangıç:**
- Sale net_price: 100 TL
- Checkout amount: 0 TL
- Payment type: treat

**İşlem:**
```php
$paymentType = PAYMENT_TYPE['treat']  // checkoutAmount == 0
$discount = 100 - 0 = 100  // Ama treat olduğu için discount uygulanmaz
```

**Sonuç:**
- Sale.discount = 0 (treat durumunda discount set edilmez)
- Sale.payment_type = 'treat'
- Sale.treat = 100 (updateTotalPrice sonrası)
- Order.payment_type = 'treat' (tüm order'lar)

---

### Senaryo 4: Sabee Entegrasyonu ile Discount

**Başlangıç:**
- Sale net_price: 100 TL
- Checkout amount: 80 TL
- Discount: 20 TL
- Order Groups:
  - Yemekler: 60 TL
  - İçecekler: 40 TL

**Sabee'ye Gönderim:**

**Grup 1 - Yemekler:**
```json
{
  "services": [
    {"name": "Pizza", "quantity": 2, "price": 30.0, "vat": 8},
    {"name": "Burger", "quantity": 1, "price": 18.0, "vat": 8},
    {"name": "İndirim", "quantity": 1, "price": -12.0, "vat": 0}
  ],
  "total_price": 60.0
}
```

**Grup 2 - İçecekler:**
```json
{
  "services": [
    {"name": "Cola", "quantity": 2, "price": 10.0, "vat": 8},
    {"name": "Ayran", "quantity": 1, "price": 5.0, "vat": 8},
    {"name": "İndirim", "quantity": 1, "price": -8.0, "vat": 0}
  ],
  "total_price": 40.0
}
```

---

### Senaryo 5: Kredi Bakiyesi ile Bahşiş

**Başlangıç:**
- Sale net_price: 100 TL
- Customer credit balance: 10 TL
- Credit balance action: 'add_to_sale'

**İşlem:**
```php
// 1. Bahşiş order'ı ekle
$this->addTipOrderToSale($sale, 10);
// Sale'e yeni order eklenir: Tip ürünü, 10 TL

// 2. Kredi bakiyesini kullan
$sale->payer->useCreditBalance($sale, $paymentTypeForBalance);

// 3. checkoutAmount'u güncelle
$checkoutAmount = $sale->fresh()->net_price;  // 110 TL
```

**Sonuç:**
- Sale.net_price = 110 TL (100 + 10 bahşiş)
- Sale.orders_count artar (yeni tip order'ı)
- Customer.credit_balance azalır (10 TL)

---

## 7. Özet Karşılaştırma Tablosu

| Özellik | Order Seviyesi | Sale Seviyesi | Açıklama |
|---------|----------------|---------------|----------|
| **Treat Flag** | ✅ `payment_type` | ✅ `treat` (aggregate) | Order'da flag, Sale'de toplam tutar |
| **Discount Amount** | ❌ Yok | ✅ `discount` | Sadece Sale'de tutulur |
| **Discount Percentage** | ❌ Yok | ✅ `discount_pct` | Sadece Sale'de tutulur |
| **Cancel Flag** | ✅ `payment_type` | ✅ `cancel` (aggregate) | Order'da flag, Sale'de toplam tutar |
| **Net Price** | ❌ Yok | ✅ `net_price` | Sale'de hesaplanır (gross - discount) |
| **Gross Price** | ✅ `total_price` | ✅ `gross_price` (aggregate) | Order'da bireysel, Sale'de toplam |
| **VAT** | ✅ `vat` | ✅ `vat` (aggregate) | Her ikisinde de tutulur |
| **Service Fee** | ✅ `service_fee` | ✅ `service_fee` (aggregate) | Her ikisinde de tutulur |

---

## 8. Kritik Noktalar

### ✅ Doğru Kullanım
1. **Order'lar kendi fiyatlarını korur**: Discount order'lara yansıtılmaz
2. **Discount sadece Sale seviyesinde**: `sale.discount` alanında tutulur
3. **Treat order'lar işaretlenir**: `payment_type = PAYMENT_TYPE['treat']`
4. **Sabee'ye discount negatif servis olarak gider**: `"price": -20.0`
5. **Treat durumunda discount gönderilmez**: Toplam zaten 0

### ⚠️ Dikkat Edilmesi Gerekenler
1. **updateTotalPrice() her zaman çağrılmalı**: Order eklendiğinde/silindiğinde/güncellendiğinde
2. **Soft delete edilen order'lar allOrders() ile alınmalı**: `withTrashed()` gerekir
3. **Sabee discount dağıtımı oranlı yapılır**: Her kategori grubuna proportionally
4. **Kredi bakiyesi checkoutAmount'u değiştirir**: Bahşiş eklenirse tutar artar
5. **Percentage discount otomatik hesaplanır**: `discount_pct` set edilirse

### 🔄 Akış Özeti
```
1. Checkout başlatılır
   ↓
2. Gerekirse sale split edilir (partial checkout)
   ↓
3. checkoutAmount belirlenir
   ↓
4. checkoutAmount == 0 mı?
   ├─ EVET → payment_type = 'treat'
   └─ HAYIR → payment_type = seçilen ödeme tipi
   ↓
5. Credit/Sabee işlemleri (varsa)
   ↓
6. Kredi bakiyesi bahşiş olarak eklenebilir
   ↓
7. Order'ların payment_type güncellenir ve soft delete edilir
   ↓
8. Discount = net_price - checkoutAmount hesaplanır
   ↓
9. Sale güncellenir (payment_type, discount, deleted_at)
   ↓
10. updateTotalPrice() çağrılır (aggregate değerler hesaplanır)
   ↓
11. FinancialDay recalculated
   ↓
12. Activity log oluşturulur
```

---

## 9. İlgili Dosyalar

- **app/Models/Sale.php**: Sale modeli, updateTotalPrice() metodu
- **app/Models/Order.php**: Order modeli, split() metodu
- **app/Repositories/CheckoutRepository.php**: Checkout işlemi, discount/treat mantığı
- **app/Traits/PayableTrait.php**: isTreat(), isCancelled(), isForGross(), isForNet() metodları
- **app/Types/PaymentTypes.php**: Payment type sabitleri

---

## 10. SQL Schema Reference

### orders tablosu
```sql
CREATE TABLE `orders` (
  `id` int unsigned NOT NULL AUTO_INCREMENT,
  `company_id` int unsigned NOT NULL,
  `user_id` int unsigned NOT NULL,
  `product_id` int unsigned NOT NULL,
  `category_id` int unsigned NOT NULL,
  `sale_id` int unsigned NOT NULL,
  `tracking_section_id` int unsigned DEFAULT NULL,
  `status` varchar(255) NOT NULL DEFAULT 'new',
  `quantity` decimal(10,5) NOT NULL DEFAULT '1.00000',
  `payment_type` tinyint NOT NULL DEFAULT '0',
  `price` decimal(10,2) NOT NULL DEFAULT '0.00',
  `vat` decimal(10,2) NOT NULL DEFAULT '0.00',
  `service_fee` decimal(10,2) NOT NULL DEFAULT '0.00',
  `total_price` decimal(10,2) NOT NULL DEFAULT '0.00',
  `specs` text,
  `cancel_reason` varchar(255) DEFAULT NULL,
  `canceller_id` int unsigned DEFAULT NULL,
  `created_at` timestamp NULL DEFAULT NULL,
  `updated_at` timestamp NULL DEFAULT NULL,
  `deleted_at` timestamp NULL DEFAULT NULL,
  PRIMARY KEY (`id`),
  KEY `orders_sale_id_foreign` (`sale_id`),
  KEY `orders_product_id_foreign` (`product_id`)
);
```

### sales tablosu
```sql
CREATE TABLE `sales` (
  `id` int unsigned NOT NULL AUTO_INCREMENT,
  `company_id` int unsigned NOT NULL,
  `user_id` int unsigned NOT NULL,
  `payer_id` int unsigned DEFAULT NULL,
  `destination_type` varchar(255) DEFAULT NULL,
  `destination_id` int unsigned DEFAULT NULL,
  `delivery_type` varchar(255) DEFAULT NULL,
  `channel` varchar(255) DEFAULT NULL,
  `channel_id` varchar(255) DEFAULT NULL,
  `status` varchar(255) NOT NULL DEFAULT 'new',
  `payment_type` tinyint NOT NULL DEFAULT '0',
  `gross_price` decimal(10,2) NOT NULL DEFAULT '0.00',
  `net_price` decimal(10,2) NOT NULL DEFAULT '0.00',
  `discount` decimal(10,2) NOT NULL DEFAULT '0.00',
  `discount_pct` decimal(5,2) DEFAULT '0.00',
  `vat` decimal(10,2) NOT NULL DEFAULT '0.00',
  `service_fee` decimal(10,2) NOT NULL DEFAULT '0.00',
  `treat` decimal(10,2) NOT NULL DEFAULT '0.00',
  `cancel` decimal(10,2) NOT NULL DEFAULT '0.00',
  `num_orders` int NOT NULL DEFAULT '0',
  `num_treats` int NOT NULL DEFAULT '0',
  `num_cancels` int NOT NULL DEFAULT '0',
  `note` text,
  `data` text,
  `financial_day_id` int unsigned DEFAULT NULL,
  `started_at` timestamp NULL DEFAULT NULL,
  `scheduled_to` timestamp NULL DEFAULT NULL,
  `created_at` timestamp NULL DEFAULT NULL,
  `updated_at` timestamp NULL DEFAULT NULL,
  `deleted_at` timestamp NULL DEFAULT NULL,
  PRIMARY KEY (`id`),
  KEY `sales_company_id_foreign` (`company_id`),
  KEY `sales_payer_id_foreign` (`payer_id`),
  KEY `sales_financial_day_id_foreign` (`financial_day_id`)
);
```

---

**Son Güncelleme**: 2026-05-15  
**Yazar**: AI Assistant  
**Versiyon**: 1.0
