# نظام التكييش (Contract Cashout System) - توثيق شامل

**آخر تحديث:** 3 أبريل 2026  
**النسخة:** 1.0  
**الحالة:** جاهز للنقل/التكامل

---

## 1. أنواع التكييش

### أ) التكييش العادي (Simple Cashout)
- **الوصف:** صاحب العقد يطلب تسديد العقد ويريد الخروج منه
- **نطاق الحساب:** الأقساط من تاريخ اليوم فصاعدًا فقط
- **المتأخرات:** لا تُضم (يختار العميل تجاهلها أو تسديتها لاحقًا)
- **الناتج:** مبلغ واحد = (متبقي المستقبل) - (خصم الربح من المستقبل)

### ب) تسوية المتأخرات + التكييش (Overdue Settlement + Cashout)
- **الوصف:** تسوية شاملة تغطي كل شيء (متأخرات + مستقبل)
- **نطاق الحساب:** المتأخرات كاملة + أقساط المستقبل مع خصم الربح
- **الناتج:** مبلغ واحد = (اجمالي المتبقي) - (خصم الربح من المستقبل فقط)

---

## 2. المعادلات الأساسية

### صيغة حساب الربح في القسط الواحد

```
ربح القسط الواحد = (سعر التقسيط - السعر الأصلي) / عدد الأقساط

مثال:
- سعر أصلي: 20,000 جنيه
- نسبة زيادة: 70%
- سعر التقسيط: 20,000 + (20,000 × 70%) = 34,000 جنيه
- عدد الأقساط: 10
- ربح القسط الواحد = (34,000 - 20,000) / 10 = 1,400 جنيه
```

### صيغة حساب أصل القسط الواحد

```
أصل القسط الواحد = السعر الأصلي / عدد الأقساط

في المثال:
أصل القسط = 20,000 / 10 = 2,000 جنيه
```

### صيغة قيمة التكييش (بدون متأخرات)

```
مبلغ التكييش = آجمالي المتبقي من أقساط (من اليوم فصاعدًا)
               - (عدد الأقساط المستقبلية × ربح القسط الواحد)

في المثال (بعد دفع 2 قسط):
- الأقساط المتبقية: 8
- المتبقي من أقساط المستقبل: 8 × 3,400 = 27,200 ج
- خصم الربح: 8 × 1,400 = 11,200 ج
- مبلغ التكييش = 27,200 - 11,200 = 16,000 ج
```

### صيغة التسوية النهائية (مع متأخرات)

```
التسوية النهائية = مبلغ التكييش (المستقبل فقط)
                  + المتأخرات غير المسددة

في المثال (إذا كان هناك 2 قسط متأخر لم يُسدد):
- المتأخرات = 2 × 3,400 = 6,800 ج (كاملة بدون خصم ربح)
- مبلغ التكييش (منقح) = 16,000 + 6,800 = 22,800 ج
```

---

## 3. قواعد الحساب الأساسية

| القاعدة | الوصف | التطبيق |
|--------|-------|---------|
| تصنيف التاريخ | تاريخ القسط < يوم اليوم = متأخر | استخدم `.startOfDay()` لتجنب مشاكل الوقت |
| الأقساط الجزئية | المتبقي = القيمة - المدفوع | احسبها على الجزء المتبقي فقط |
| لا توجد مستقبل | إذا لم تكن هناك أقساط مستقبلية | رفع استثناء: "لا يوجد مبلغ للتكييش" |
| الناتج الموجب | المبلغ النهائي ≥ 0 | استخدم `max(result, 0)` دائمًا |
| الدقة العددية | استخدم 2 عشري | round($value, 2) بعد كل عملية |
| الأقساط المدفوعة | is_paid = true أو paid_amount = value | استبعدها من الحساب تمامًا |

---

## 4. حالات العقد (Contract Status)

```
enum ContractStatus {
    ACTIVE = 'active'           // عقد نشط جاري السداد
    FINISHED = 'finished'       // عقد انتهى بشكل عادي (كل الأقساط سددت)
    SUSPENDED = 'suspended'     // عقد موقوف (توقف سداد مؤقت)
    CASHED = 'cashed'           // عقد مكيش (تم تكييشه + خروج نهائي)
}
```

**التحولات المسموحة:**
- `active` → `cashed` (عند التكييش)
- `active` → `suspended` (توقف مؤقت)
- `suspended` → `active` (إعادة تنشيط)
- `active` → `finished` (سداد طبيعي كامل للأقساط)
- **لا يمكن** العودة من `cashed` أو `finished`

---

## 5. البيانات المحفوظة بعد التكييش

### جدول/Model: ContractCashout (اختياري - للتدقيق)

```sql
CREATE TABLE contract_cashouts (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    contract_id BIGINT NOT NULL,
    cashout_type ENUM('simple', 'overdue_settlement') NOT NULL,
    
    -- المبالغ
    total_amount DECIMAL(15,2) NOT NULL,        -- المبلغ النهائي المسدد
    discount_profit DECIMAL(15,2) NOT NULL,    -- إجمالي خصم الربح
    overdue_amount DECIMAL(15,2) DEFAULT 0,    -- المتأخرات المسددة
    future_amount DECIMAL(15,2) NOT NULL,      -- المستقبل المسدد
    
    -- العدادات
    overdue_count INT DEFAULT 0,                -- عدد الأقساط المتأخرة
    future_count INT NOT NULL,                  -- عدد الأقساط المستقبلية
    
    -- البيانات الوصفية
    cashout_date DATE NOT NULL,
    executed_by BIGINT NOT NULL,                -- معرف المستخدم
    payment_id BIGINT NOT NULL,                 -- ربط مع جدول Payment
    notes TEXT,
    
    -- الطوابع الزمنية
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    
    FOREIGN KEY (contract_id) REFERENCES contracts(id),
    FOREIGN KEY (executed_by) REFERENCES users(id),
    FOREIGN KEY (payment_id) REFERENCES payments(id)
);
```

### تحديث جدول العقد

```sql
-- بعد التكييش مباشرة، يتم:
UPDATE contracts SET
    status = 'cashed',
    updated_at = NOW()
WHERE id = contract_id;
```

### تسجيل حركة السداد (في جدول Payment)

```
payment = {
    customer_id: contract.customer_id,
    contract_id: contract.id,
    vault_id: <اختيار المستخدم>,
    amount: snapshot.cashout_total,
    payment_date: <تاريخ التكييش>,
    payment_method: <نقدي/تحويل/شيك/بطاقة>,
    status: 'completed',
    receipt_number: 'CASHOUT-' + contract_id + '-' + timestamp,
    notes: 'تكييش العقد - خصم ربح X جنيه' 
           أو 
           'تسوية متأخرات + تكييش - خصم ربح X جنيه',
    created_by: auth_user_id
}
```

---

## 6. الأثر المالي والتدفقات النقدية

### تسلسل العملية

```
1. حساب الفترة: تجميع متبقي الأقساط من اليوم فصاعدًا
2. تحديد الربح: ضرب عدد الأقساط المستقبلية × ربح القسط
3. تخصيص الخصم: كل قسط مستقبلي ينخفض بمقدار ربحه
4. حساب الناتج: المتبقي (من المستقبل) - خصم الربح
5. إضافة المتأخرات: إذا تم اختيار السيناريو الثاني
6. إنشاء حركة سداد: دفعة واحدة بقيمة النهائية
7. تحديث الحالة: العقد → مكيش
8. تحديث الأقساط: تقليل قيمة كل قسط مستقبلي
```

### حركة الخزنة (Vault Transaction)

```
تلقائيًا عند السداد:
- النوع: in (دخول)
- المبلغ: snapshot.cashout_total
- الحالة: completed
- الملاحظة: "تكييش من عميل - " + contract_id
- الرصيد قبل: X
- الرصيد بعد: X + amount
```

---

## 7. واجهة المستخدم - العناصر المطلوبة

### أ) عرض قيم التكييش (في صفحة عرض العقد)

```
┌──────────────────────────────────────┐
│ قيمة التكييش (بدون متأخرات)          │
│ X,XXX.XX جنيه                        │
├──────────────────────────────────────┤
│ إجمالي خصم الربح                      │
│ X,XXX.XX جنيه                        │
├──────────────────────────────────────┤
│ تسوية المتأخرات + تكييش              │
│ X,XXX.XX جنيه                        │
└──────────────────────────────────────┘

تحديث تلقائي عند:
- تدخل المستخدم لشاشة العقد
- أو تغيير حالة السداد (دفعة جديدة)
```

### ب) زراحة الإجراء الأول: "تكييش العقد"

**الأيقونة:** 💰 أو `heroicon-o-banknotes`  
**اللون:** أخضر (success)  
**الحالة المسموحة:** active فقط (وليس suspended/cashed/finished)

**النموذج المنبثق:**

```
┌─────────────────────────────────────────────┐
│ تكييش العقد                                 │
├─────────────────────────────────────────────┤
│ قيمة التكييش:        9,600 جنيه             │
│ خصم الربح:          4,200 جنيه             │
│ التفاصيل:                                  │
│   - أقساط متأخرة: 0                        │
│   - أقساط اليوم/المستقبل: 3              │
├─────────────────────────────────────────────┤
│ الخزنة:              [اختر خزنة ▼]          │
│ طريقة الدفع:         [نقدي ▼]             │
│ تاريخ الدفع:         [اليوم ▼]            │
│ ملاحظات:             [نص اختياري...]     │
├─────────────────────────────────────────────┤
│ [إلغاء]    [تنفيذ التكييش]                |
└─────────────────────────────────────────────┘
```

### ج) زر الإجراء الثاني: "تسوية المتأخرات + تكييش"

**الأيقونة:** 🧮 أو `heroicon-o-calculator`  
**اللون:** أزرق (info)  
**الحالة المسموحة:** active فقط (وليس suspended/cashed/finished)

**النموذج المنبثق:**

```
┌─────────────────────────────────────────────┐
│ تسوية المتأخرات + تكييش العقد             │
├─────────────────────────────────────────────┤
│ إجمالي الدفعة:      13,800 جنيه            │
│ خصم الربح:          4,200 جنيه             │
│ التفاصيل:                                  │
│   - متأخرات: 2                             │
│   - أقساط اليوم/المستقبل: 3              │
├─────────────────────────────────────────────┤
│ الخزنة:              [اختر خزنة ▼]          │
│ طريقة الدفع:         [نقدي ▼]             │
│ تاريخ الدفع:         [اليوم ▼]            │
│ ملاحظات:             [نص اختياري...]     │
├─────────────────────────────────────────────┤
│ [إلغاء]  [تنفيذ التسوية]                  |
└─────────────────────────────────────────────┘
```

### د) الحالات التي لا تسمح بالتكييش

```
إذا كان العقد:
- status = 'suspended'     → إظهار: "عقد موقوف لا يمكن تكييشه"
- status = 'finished'      → إظهار: "عقد منتهي بالفعل"
- status = 'cashed'        → إظهار: "عقد مكيش بالفعل"
- بدون أقساط مستقبلية    → تعطيل الزر مع رسالة: "لا توجد أقساط متبقية"
```

---

## 8. التقارير والإحصائيات

### أ) استبعاد العقود المكيشة من التقارير

جميع التقارير التالية **يجب أن تستبعد** العقود بحالة `cashed`:

1. **تقرير أقساط اليوم**
   ```where contract.status IN ['active', 'finished']```

2. **تقرير أقساط الأسبوع**
   ```where contract.status IN ['active', 'finished']```

3. **تقرير أقساط الشهر**
   ```where contract.status IN ['active', 'finished']```

4. **تقرير الأقساط المتأخرة**
   ```where contract.status IN ['active', 'finished']```

### ب) إضافة فلتر اختياري

**اسم الفلتر:** "استبعاد العقود المكيشة"  
**النوع:** Checkbox  
**القيمة الافتراضية:** مفعّل (checked)

```
إذا كان مفعلاً:
   where contract.status != 'cashed'
إذا كان معطلاً:
   بدون قيد (اعرض الكل)
```

### ج) تقرير جديد (اختياري): "عمليات التكييش"

```
Columns:
- رقم العقد
- اسم العميل
- تاريخ التكييش
- نوع التكييش (simple / overdue_settlement)
- المبلغ النهائي
- خصم الربح
- المتأخرات المسددة
- من نفذ العملية
```

---

## 9. الصلاحيات والتدقيق (Permissions & Audit)

### صلاحيات اقترحة

```
permission:
  - 'Execute:ContractCashout'       // لتنفيذ التكييش العادي
  - 'Execute:ContractSettleOverdue' // لتنفيذ تسوية المتأخرات
  - 'View:ContractCashoutHistory'   // لعرض سجل التكييشات
  - 'Export:ContractCashoutReport'  // لتصدير تقرير التكييشات
```

### ما يجب تسجيله (Audit Log)

```sql
INSERT INTO audit_logs (
    user_id,
    action,
    model_type,
    model_id,
    changes,
    ip_address,
    created_at
) VALUES (
    auth()->id(),
    'contract.cashout',
    'Contract',
    contract_id,
    JSON_OBJECT(
        'old_status', 'active',
        'new_status', 'cashed',
        'cashout_amount', snapshot.cashout_total,
        'profit_discount', snapshot.discount_total,
        'payment_id', payment.id
    ),
    request()->ip(),
    NOW()
);
```

### Snapshot المسجل

```json
{
  "contract_id": 123,
  "timestamp": "2026-04-03T14:30:00Z",
  "calculation_date": "2026-04-03",
  "type": "simple",
  "values": {
    "original_price": 20000,
    "profit_percentage": 70,
    "installment_price": 34000,
    "installment_count": 10,
    "profit_per_installment": 1400,
    "paid_installments": 2,
    "unpaid_future_count": 8,
    "unpaid_overdue_count": 0,
    "total_cashout": 16000,
    "discount_total": 11200,
    "future_amount": 16000,
    "overdue_amount": 0
  },
  "executed_by_user": 5,
  "payment_method": "cash",
  "vault_id": 2
}
```

---

## 10. حالات الاختبار (Test Cases)

### Test 1: بدون متأخرات (معياري)

```
Setup:
- السعر الأصلي: 20,000
- نسبة الزيادة: 70% (= 34,000 السعر النهائي)
- عدد الأقساط: 10 (3,400 كل قسط)
- دفع: قسط #1 وقسط #2 (6,800)
- المتأخرات: 0

Expected:
- الأقساط المتبقية: 8
- خصم الربح: 8 × 1,400 = 11,200
- مبلغ التكييش = 27,200 - 11,200 = 16,000
- الحالة النهائية: cashed
```

### Test 2: مع متأخرات

```
Setup:
- نفس البيانات السابقة
- لكن: قسط #1 متأخر + قسط #2 متأخر + قسط #3 متأخر
- المدفوع: 0
- المتأخرات: 3 أقساط

Expected (بدون متأخرات):
- مبلغ التكييش: 28,200 - 10,800 = 17,400

Expected (مع متأخرات):
- المتأخرات: 3 × 3,400 = 10,200
- مبلغ التكييش: 17,400 + 10,200 = 27,600
```

### Test 3: قسط جزئي

```
Setup:
- نفس البيانات الأصلية
- قسط #1: مدفوع بالكامل (3,400)
- قسط #2: مدفوع جزئي (1,700 من 3,400)

Expected:
- المتبقي من قسط #2: 3,400 - 1,700 = 1,700
- إجمالي المتبقي: (1,700) + (8 × 3,400) = 1,700 + 27,200 = 28,900
- خصم الربح: 8 × 1,400 = 11,200
  (لاحظ: الخصم من الأقساط المستقبلية الكاملة فقط)
- مبلغ التكييش: 28,900 - 11,200 = 17,700
```

### Test 4: كل الأقساط مدفوعة

```
Setup:
- 10 أقساط بقيمة 3,400 لكل
- كل الأقساط: is_paid = true و paid_amount = 3,400

Expected:
- استثناء: "لا يوجد مبلغ متبقي للتكييش"
- رفع Exception والعودة للمستخدم بتنبيه واضح
```

### Test 5: عقد موقوف

```
Setup:
- العقد: status = 'suspended'
- محاولة تكييش

Expected:
- الزر معطل أو غير مرئي
- رسالة: "عقد موقوف لا يمكن تكييشه حالياً"
- التنفيذ: خطأ 403 (Forbidden)
```

### Test 6: عقد منتهي

```
Setup:
- العقد: status = 'finished'
- محاولة تكييش

Expected:
- الزر غير مرئي
- رسالة: "العقد منتهي بالفعل"
```

### Test 7: عقد مكيش بالفعل

```
Setup:
- العقد: status = 'cashed'
- محاولة تكييش مرة أخرى

Expected:
- الزر غير مرئي
- رسالة: "عقد مكيش بالفعل"
- التنفيذ: خطأ 403
```

### Test 8: بدون أقساط مستقبلية

```
Setup:
- العقد: كل الأقساط متأخرة أو اليوم
- لا توجد أقساط بتاريخ مستقبلي > اليوم

Expected (السيناريو الأول):
- استثناء: "لا توجد أقساط مستقبلية للتكييش"
- الزر معطل

Expected (السيناريو الثاني - مع متأخرات):
- تسوية المتأخرات = كل المتبقي (بدون خصم ربح)
- الحالة النهائية: cashed
```

---

## 11. معادلات سريعة قابلة للنسخ

### Python

```python
def calculate_cashout(contract, include_overdue=False):
    profit_total = max(contract.installment_price - contract.original_price, 0)
    installment_count = max(contract.installment_count, 1)
    profit_per = round(profit_total / installment_count, 2)
    
    overdue_amt = 0.0
    future_amt = 0.0
    discount_amt = 0.0
    future_count = 0
    overdue_count = 0
    
    today = datetime.now().date()
    
    for inst in contract.installments.filter(is_paid=False):
        remaining = max(float(inst.value) - float(inst.paid_amount or 0), 0)
        if remaining <= 0:
            continue
        
        is_future = inst.due_date and inst.due_date >= today
        
        if is_future:
            current_discount = min(profit_per, remaining)
            future_amt += remaining - current_discount
            discount_amt += current_discount
            future_count += 1
        else:
            if include_overdue:
                overdue_amt += remaining
            overdue_count += 1
    
    return {
        'cashout_total': round(overdue_amt + future_amt, 2),
        'discount_total': round(discount_amt, 2),
        'future_amount': round(future_amt, 2),
        'overdue_amount': round(overdue_amt, 2),
        'future_count': future_count,
        'overdue_count': overdue_count,
    }
```

### SQL

```sql
SELECT 
    c.id as contract_id,
    c.customer_id,
    (c.installment_price - c.original_price) / c.installment_count as profit_per_installment,
    COUNT(DISTINCT CASE WHEN i.due_date >= CURDATE() AND i.is_paid = 0 THEN i.id END) as future_count,
    COUNT(DISTINCT CASE WHEN i.due_date < CURDATE() AND i.is_paid = 0 THEN i.id END) as overdue_count,
    SUM(CASE 
        WHEN i.due_date >= CURDATE() AND i.is_paid = 0 
        THEN (i.value - COALESCE(i.paid_amount, 0))
        ELSE 0
    END) as future_remaining,
    SUM(CASE 
        WHEN i.due_date < CURDATE() AND i.is_paid = 0 
        THEN (i.value - COALESCE(i.paid_amount, 0))
        ELSE 0
    END) as overdue_remaining
FROM contracts c
LEFT JOIN installments i ON c.id = i.contract_id
WHERE c.id = ?
GROUP BY c.id;
```

### JavaScript/Node.js

```javascript
function calculateCashout(contract, includeOverdue = false) {
    const profitTotal = Math.max(contract.installment_price - contract.original_price, 0);
    const installmentCount = Math.max(contract.installment_count, 1);
    const profitPer = Math.round((profitTotal / installmentCount) * 100) / 100;
    
    let overdueAmt = 0, futureAmt = 0, discountAmt = 0;
    let futureCount = 0, overdueCount = 0;
    
    const today = new Date().toISOString().split('T')[0];
    
    contract.installments.forEach(inst => {
        if (inst.is_paid) return;
        
        const remaining = Math.max(inst.value - (inst.paid_amount || 0), 0);
        if (remaining <= 0) return;
        
        const isFuture = inst.due_date >= today;
        
        if (isFuture) {
            const currentDiscount = Math.min(profitPer, remaining);
            futureAmt += remaining - currentDiscount;
            discountAmt += currentDiscount;
            futureCount++;
        } else {
            if (includeOverdue) overdueAmt += remaining;
            overdueCount++;
        }
    });
    
    return {
        cashout_total: Math.round((overdueAmt + futureAmt) * 100) / 100,
        discount_total: Math.round(discountAmt * 100) / 100,
        future_amount: Math.round(futureAmt * 100) / 100,
        overdue_amount: Math.round(overdueAmt * 100) / 100,
        future_count: futureCount,
        overdue_count: overdueCount,
    };
}
```

---

## ملخص سريع

| العنصر | الوصف |
|--------|-------|
| **الهدف** | تسديد العقد قبل الأوان مع خصم ربح الأقساط المستقبلية |
| **دقة الحساب** | عشريين بعد الفاصلة (2 decimal places) |
| **المعادلة الأساسية** | Cashout = Future Remaining - (Future Count × Profit Per) |
| **الحالة النهائية** | cashed |
| **حفظ البيانات** | Payment + تحديث Contract Status |
| **الاستبعاد من التقارير** | العقود cashed لا تظهر في تقارير الأقساط |
| **الصلاحيات** | يجب تقييد التنفيذ لمستخدمين محددين |

---

**إعداد:** نظام التقسيط المتطور  
**التاريخ:** 3 أبريل 2026  
**النسخة:** 1.0  
**الحالة:** توثيق كامل وجاهز للنقل
