# 🔄 دليل نظام إعادة توزيع الأقساط التلقائي

## 📋 جدول المحتويات
1. [نظرة عامة](#نظرة-عامة)
2. [المشكلة الأساسية](#المشكلة-الأساسية)
3. [الحل المُطبق](#الحل-المطبق)
4. [كيف يعمل النظام](#كيف-يعمل-النظام)
5. [الملفات المعدلة](#الملفات-المعدلة)
6. [التوصيات البرمجية](#التوصيات-البرمجية)
7. [تطبيق الحل في نظام آخر](#تطبيق-الحل-في-نظام-آخر)

---

## 🎯 نظرة عامة

تم تطوير نظام **إعادة التوزيع التلقائي للمدفوعات على الأقساط** لحل مشكلة عدم تحديث الأقساط عند تعديل أو حذف المدفوعات.

### المبدأ الأساسي:
```
المدفوعات (Payments) ← توزع تلقائياً → الأقساط (Installments)
```

- **المصدر الوحيد للحقيقة**: جدول `payments` فقط
- **الأقساط**: تُحدث تلقائياً بناءً على المدفوعات
- **لا توجد إدخالات يدوية**: كل شيء يتم عبر Observer

---

## ❌ المشكلة الأساسية

### المشكلة 1: الحساب المكرر
```php
// ❌ خطأ: كان يحسب المدفوعات مرتين
$totalCollected = $totalPaidAmount + $totalPayments;

// حيث:
// $totalPaidAmount = مجموع paid_amount من الأقساط
// $totalPayments = مجموع amount من المدفوعات
```

**النتيجة**: رصيد خاطئ - العميل يظهر أنه دفع أكثر من المطلوب!

### المشكلة 2: عدم التحديث عند التعديل
```php
// سيناريو:
1. إضافة دفعة: 2,450 ج ← توزيع على الأقساط ✅
2. تعديل الدفعة إلى: 2,500 ج ← الأقساط لم تُحدث ❌
3. النتيجة: الأقساط تعكس المبلغ القديم
```

### المشكلة 3: عدم التحديث عند الحذف
```php
// سيناريو:
1. حذف دفعة 1,000 ج
2. الأقساط ما زالت تحتوي على المبلغ ❌
3. النتيجة: رصيد خاطئ
```

---

## ✅ الحل المُطبق

### الفلسفة:
> **"عند أي تغيير في المدفوعات → أعد توزيع كل شيء من الصفر"**

### المكونات الأساسية:

#### 1. PaymentObserver
يراقب جميع التغييرات على جدول `payments`:
- ✅ `created()` - عند إنشاء دفعة جديدة
- ✅ `updated()` - عند تعديل دفعة
- ✅ `deleting()` - قبل حذف دفعة

#### 2. Redistribution Logic
```php
private function redistributeAllPaymentsForContract(int $contractId): void
{
    // 1. مسح الأقساط (Reset)
    Installment::where('contract_id', $contractId)
        ->update([
            'paid_amount' => 0,
            'is_paid' => false,
            'payment_date' => null
        ]);

    // 2. الحصول على جميع المدفوعات
    $payments = Payment::where('contract_id', $contractId)
        ->where('status', 'completed')
        ->orderBy('payment_date', 'asc')
        ->get();

    // 3. إعادة التوزيع من الصفر
    foreach ($payments as $payment) {
        $this->distributePaymentToInstallments($payment);
    }
}
```

---

## 🔧 كيف يعمل النظام

### السيناريو الكامل:

#### مثال عملي:
```
عقد به 12 قسط، كل قسط = 1,700 ج
```

#### الخطوات:

**1. إضافة أول دفعة: 1,700 ج (30/01/2026)**
```
القسط 1: 1,700 ج ← مدفوع كامل ✅
```

**2. إضافة ثاني دفعة: 1,000 ج (15/02/2026)**
```
القسط 1: 1,700 ج ← مدفوع كامل ✅
القسط 2: 1,000 ج ← مدفوع جزئياً ⚠️
```

**3. إضافة ثالث دفعة: 2,450 ج (14/03/2026)**
```
القسط 1: 1,700 ج ← مدفوع كامل ✅
القسط 2: 1,700 ج ← مدفوع كامل ✅
القسط 3: 1,700 ج ← مدفوع كامل ✅
القسط 4: 50 ج ← مدفوع جزئياً ⚠️
```

**4. تعديل الدفعة الثالثة إلى: 2,500 ج**
```
→ النظام يعيد توزيع كل شيء من الصفر:

الخطوة 1: مسح جميع الأقساط
القسط 1-12: 0 ج ← غير مدفوع

الخطوة 2: إعادة التوزيع
دفعة 1 (1,700 ج):
  القسط 1: 1,700 ج ✅

دفعة 2 (1,000 ج):
  القسط 2: 1,000 ج ⚠️

دفعة 3 (2,500 ج):
  القسط 2: +700 ج = 1,700 ج ✅
  القسط 3: 1,700 ج ✅
  القسط 4: 100 ج ⚠️

النتيجة النهائية:
القسط 1: 1,700 ج ✅
القسط 2: 1,700 ج ✅
القسط 3: 1,700 ج ✅
القسط 4: 100 ج ⚠️
إجمالي: 5,200 ج (صحيح!)
```

---

## 📁 الملفات المعدلة

### 1. `app/Observers/PaymentObserver.php` ⭐
**الدور**: المراقب الرئيسي لجميع العمليات على المدفوعات

```php
<?php

namespace App\Observers;

use App\Models\Payment;
use App\Models\Installment;

class PaymentObserver
{
    /**
     * عند إنشاء دفعة جديدة
     */
    public function created(Payment $payment): void
    {
        if ($payment->status === 'completed' && $payment->contract_id) {
            $this->distributePaymentToInstallments($payment);
        }
    }

    /**
     * عند تعديل دفعة
     * ⚠️ الأهم: يُعيد توزيع كل المدفوعات من الصفر
     */
    public function updated(Payment $payment): void
    {
        if ($payment->contract_id && 
            ($payment->wasChanged('amount') || 
             $payment->wasChanged('status') || 
             $payment->wasChanged('payment_date'))) {
            $this->redistributeAllPaymentsForContract($payment->contract_id);
        }
    }

    /**
     * عند حذف دفعة
     * ⚠️ يُعيد توزيع المدفوعات المتبقية
     */
    public function deleting(Payment $payment): void
    {
        if ($payment->contract_id) {
            $this->redistributeAllPaymentsForContract($payment->contract_id);
        }
    }

    /**
     * ⭐ القلب النابض: إعادة توزيع كل المدفوعات من الصفر
     */
    private function redistributeAllPaymentsForContract(int $contractId): void
    {
        // 1. Reset: مسح جميع الأقساط
        Installment::where('contract_id', $contractId)
            ->update([
                'paid_amount' => 0,
                'is_paid' => false,
                'payment_date' => null
            ]);

        // 2. جلب جميع المدفوعات المكتملة بالترتيب الزمني
        $payments = Payment::where('contract_id', $contractId)
            ->where('status', 'completed')
            ->orderBy('payment_date', 'asc')
            ->get();

        // 3. إعادة توزيع كل دفعة
        foreach ($payments as $payment) {
            $this->distributePaymentToInstallments($payment);
        }
    }

    /**
     * توزيع دفعة واحدة على الأقساط بالترتيب
     */
    private function distributePaymentToInstallments(Payment $payment): void
    {
        // جلب الأقساط غير المدفوعة بالكامل
        $installments = Installment::where('contract_id', $payment->contract_id)
            ->where('is_paid', false)
            ->orderBy('due_date', 'asc')
            ->get();

        $remainingAmount = (float) $payment->amount;

        foreach ($installments as $installment) {
            if ($remainingAmount <= 0) break;

            // حساب المتبقي من القسط
            $installmentRemaining = (float) $installment->value - (float) $installment->paid_amount;

            if ($installmentRemaining <= 0) continue;

            // المبلغ المُضاف لهذا القسط
            $amountToPay = min($remainingAmount, $installmentRemaining);

            // تحديث القسط
            $installment->paid_amount = (float) $installment->paid_amount + $amountToPay;
            
            // هل اكتمل القسط؟
            if ($installment->paid_amount >= $installment->value) {
                $installment->is_paid = true;
                $installment->payment_date = $payment->payment_date;
            }
            
            $installment->save();

            $remainingAmount -= $amountToPay;
        }
    }
}
```

### 2. `app/Providers/AppServiceProvider.php`
**تسجيل الـ Observer**

```php
use App\Models\Payment;
use App\Observers\PaymentObserver;

public function boot(): void
{
    // تسجيل PaymentObserver
    Payment::observe(PaymentObserver::class);
}
```

### 3. `app/Models/Contract.php`
**تصحيح حساب المحصل**

```php
/**
 * ⚠️ التغيير الأهم: المصدر الوحيد للحقيقة
 */
public function getTotalCollectedAttribute()
{
    // ✅ صحيح: من المدفوعات فقط
    return $this->total_paid;
    
    // ❌ خطأ (قديم): كان يجمع مرتين
    // return $this->total_installments_paid;
}

public function getTotalPaidAttribute()
{
    return $this->payments()->completed()->sum('amount');
}

public function getTotalInstallmentsPaidAttribute()
{
    // للعرض التوضيحي فقط - ليس للحسابات
    return $this->installments()->sum('paid_amount');
}
```

### 4. `resources/views/filament/pages/customer-account-report.blade.php`
**تصحيح عرض التقرير**

```php
// ❌ خطأ (قديم)
$totalCollected = $totalPaidAmount + $totalPayments;

// ✅ صحيح (جديد)
$totalCollected = $totalPayments;  // من المدفوعات فقط
```

### 5. `app/Models/Installment.php`
**قيم افتراضية للأقساط**

```php
protected $attributes = [
    'is_paid' => false,
    'paid_amount' => 0,
];
```

---

## 💡 التوصيات البرمجية

### 1. ⭐ المبدأ الذهبي: Single Source of Truth
```php
/**
 * التوصية الأولى:
 * جدول payments هو المصدر الوحيد للحقيقة
 * جدول installments هو للعرض والتتبع فقط
 */

// ✅ صحيح
$totalCollected = Payment::where('contract_id', $id)->sum('amount');

// ❌ خطأ
$totalCollected = Installment::where('contract_id', $id)->sum('paid_amount');
```

### 2. 🔄 استخدم Observers دائماً
```php
/**
 * التوصية الثانية:
 * لا تعدل الأقساط يدوياً أبداً
 * دع Observer يتعامل مع كل شيء
 */

// ✅ صحيح
$payment = Payment::create([...]);
// Observer سيوزع تلقائياً

// ❌ خطأ
$installment->paid_amount = 100;
$installment->save();
// هذا سيفقد المزامنة
```

### 3. 🔒 الأمان أولاً
```php
/**
 * التوصية الثالثة:
 * استخدم Transactions للعمليات المهمة
 */

use Illuminate\Support\Facades\DB;

DB::transaction(function () use ($contractId) {
    // Reset installments
    Installment::where('contract_id', $contractId)->update([...]);
    
    // Redistribute payments
    $payments = Payment::where('contract_id', $contractId)->get();
    foreach ($payments as $payment) {
        // distribute...
    }
});
```

### 4. 📊 التحقق الدوري
```php
/**
 * التوصية الرابعة:
 * أنشئ Command للتحقق من التزامن
 */

// php artisan installments:verify-sync

use Illuminate\Console\Command;

class VerifyInstallmentsSyncCommand extends Command
{
    protected $signature = 'installments:verify-sync {--fix}';

    public function handle()
    {
        $contracts = Contract::all();
        
        foreach ($contracts as $contract) {
            $totalPayments = $contract->payments()
                ->where('status', 'completed')
                ->sum('amount');
                
            $totalOnInstallments = $contract->installments()
                ->sum('paid_amount');
            
            if (abs($totalPayments - $totalOnInstallments) > 0.01) {
                $this->error("Contract #{$contract->id}: Mismatch!");
                
                if ($this->option('fix')) {
                    $this->info("Fixing...");
                    // إعادة التوزيع
                    event(new ForceRedistributePayments($contract));
                }
            }
        }
    }
}
```

### 5. 🧪 اختبارات تلقائية
```php
/**
 * التوصية الخامسة:
 * اكتب Tests لجميع السيناريوهات
 */

class PaymentDistributionTest extends TestCase
{
    /** @test */
    public function it_redistributes_when_payment_amount_changed()
    {
        $contract = Contract::factory()->create();
        $installments = Installment::factory(3)->create([
            'contract_id' => $contract->id,
            'value' => 1000,
        ]);
        
        $payment = Payment::create([
            'contract_id' => $contract->id,
            'amount' => 1500,
            'status' => 'completed',
        ]);
        
        // التحقق من التوزيع الأولي
        $this->assertEquals(1000, $installments[0]->fresh()->paid_amount);
        $this->assertEquals(500, $installments[1]->fresh()->paid_amount);
        
        // تعديل المبلغ
        $payment->update(['amount' => 2500]);
        
        // التحقق من إعادة التوزيع
        $this->assertEquals(1000, $installments[0]->fresh()->paid_amount);
        $this->assertEquals(1000, $installments[1]->fresh()->paid_amount);
        $this->assertEquals(500, $installments[2]->fresh()->paid_amount);
    }
}
```

---

## 🚀 تطبيق الحل في نظام آخر

### الخطوات الكاملة:

#### **الخطوة 1: إنشاء Observer**
```bash
php artisan make:observer PaymentObserver --model=Payment
```

انسخ الكود من الأعلى إلى الملف المُنشأ.

#### **الخطوة 2: تسجيل Observer**
في `app/Providers/AppServiceProvider.php`:
```php
use App\Models\Payment;
use App\Observers\PaymentObserver;

public function boot(): void
{
    Payment::observe(PaymentObserver::class);
}
```

#### **الخطوة 3: تحديث Model**
في `app/Models/Contract.php`:
```php
public function getTotalCollectedAttribute()
{
    return $this->total_paid;
}

public function getTotalPaidAttribute()
{
    return $this->payments()->where('status', 'completed')->sum('amount');
}
```

في `app/Models/Installment.php`:
```php
protected $attributes = [
    'is_paid' => false,
    'paid_amount' => 0,
];
```

#### **الخطوة 4: تحديث Views**
في جميع التقارير، استخدم:
```php
// ✅ صحيح
$totalCollected = $payments->sum('amount');

// ❌ خطأ
$totalCollected = $installments->sum('paid_amount') + $payments->sum('amount');
```

#### **الخطوة 5: Migration للبيانات الموجودة**
```php
// database/migrations/xxxx_fix_existing_installments.php

public function up()
{
    $contracts = Contract::all();
    
    foreach ($contracts as $contract) {
        // Reset installments
        $contract->installments()->update([
            'paid_amount' => 0,
            'is_paid' => false,
            'payment_date' => null,
        ]);
        
        // Redistribute
        $payments = $contract->payments()
            ->where('status', 'completed')
            ->orderBy('payment_date')
            ->get();
        
        foreach ($payments as $payment) {
            // استخدم نفس منطق التوزيع
        }
    }
}
```

#### **الخطوة 6: الاختبار**
```bash
# 1. اختبر إنشاء دفعة جديدة
php artisan tinker
> $payment = Payment::create([...])

# 2. اختبر تعديل مبلغ
> $payment->update(['amount' => 2500])

# 3. اختبر الحذف
> $payment->delete()

# 4. تحقق من الأقساط في كل حالة
> Installment::where('contract_id', $contractId)->get()
```

---

## ⚠️ تنبيهات مهمة

### 1. الأداء (Performance)
```php
/**
 * ⚠️ تحذير:
 * إعادة التوزيع قد تكون بطيئة للعقود الكبيرة
 */

// الحل: استخدم Queues للعقود الكبيرة
dispatch(new RedistributePaymentsJob($contractId));
```

### 2. Race Conditions
```php
/**
 * ⚠️ تحذير:
 * تعديلان في نفس الوقت قد يسببان مشاكل
 */

// الحل: استخدم Locks
use Illuminate\Support\Facades\Cache;

Cache::lock("redistribute-{$contractId}", 10)->block(5, function () {
    // إعادة التوزيع هنا
});
```

### 3. النسخ الاحتياطي
```php
/**
 * ⚠️ تحذير:
 * احتفظ بنسخة احتياطية قبل Migration
 */

// قبل التطبيق:
mysqldump -u user -p database > backup_before_redistribution.sql
```

---

## 📊 مقارنة: قبل وبعد

### قبل التعديل ❌
```
إضافة دفعة → توزيع على الأقساط ✅
تعديل دفعة → لا يحدث شيء ❌
حذف دفعة → لا يحدث شيء ❌
التقارير → رصيد خاطئ (حساب مكرر) ❌
```

### بعد التعديل ✅
```
إضافة دفعة → توزيع على الأقساط ✅
تعديل دفعة → إعادة توزيع كامل ✅
حذف دفعة → إعادة توزيع للمتبقي ✅
التقارير → رصيد دقيق 100% ✅
```

---

## 🎓 الدروس المستفادة

1. **Single Source of Truth**: جدول واحد فقط هو المرجع
2. **Observers قوية**: استخدمها للمنطق التلقائي
3. **Reset & Rebuild**: أحياناً إعادة البناء أسهل من التتبع
4. **Tests ضرورية**: اختبر كل السيناريوهات
5. **التوثيق مهم**: للمطورين المستقبليين

---

## 📞 للاستفسارات

هذا النظام تم تطويره وتوثيقه بشكل كامل.
للأسئلة أو التحسينات، راجع:
- `PaymentObserver.php` - المنطق الأساسي
- `test_observer_update.php` - سكربت الاختبار
- `fix_customer_71.php` - مثال عملي للتطبيق

---

**تاريخ الإنشاء**: مارس 2026  
**الإصدار**: 2.0  
**الحالة**: مُطبق ومُختبر ✅
