# دليل التنفيذ الكامل — نظام البيع والشراء والمرتجعات

---

## أولاً: فاتورة البيع

### 1. الجداول المطلوبة

```sql
-- الفاتورة الرئيسية
sales:
  id, sale_number, customer_name, customer_phone,
  sale_date, vault_id, total_amount, notes, status,
  created_by, timestamps, softDeletes

-- أصناف الفاتورة
sale_items:
  id, sale_id (FK), product_id (FK),
  quantity, unit_price,
  subtotal,          -- qty * price (قبل الخصم)
  discount_amount,   -- الخصم على الصنف
  total,             -- subtotal - discount
  serials (JSON),    -- أرقام السيريال إذا المنتج له serial
  notes,
  timestamps
```

### 2. الـ Models

```php
// app/Models/Sale.php
public function items() {
    return $this->hasMany(SaleItem::class);
}

// app/Models/SaleItem.php
protected $fillable = [
    'sale_id', 'product_id', 'quantity', 'unit_price',
    'subtotal', 'discount_amount', 'total', 'serials', 'notes'
];
protected $casts = [
    'quantity'        => 'integer',
    'unit_price'      => 'decimal:2',
    'subtotal'        => 'decimal:2',
    'total'           => 'decimal:2',
    'serials'         => 'array',
];

public function sale()    { return $this->belongsTo(Sale::class); }
public function product() { return $this->belongsTo(Product::class); }
```

### 3. Observer — `app/Observers/SaleItemObserver.php`

```php
// يُحسب تلقائياً قبل الحفظ
public function creating(SaleItem $item): void {
    $item->subtotal = $item->quantity * $item->unit_price;
    $item->total    = $item->subtotal - ($item->discount_amount ?? 0);
}

public function updating(SaleItem $item): void {
    // نفس الـ creating
    $item->subtotal = $item->quantity * $item->unit_price;
    $item->total    = $item->subtotal - ($item->discount_amount ?? 0);
}

// بعد الحفظ: يخصم من المخزون
public function created(SaleItem $item): void {
    // التحقق أن الكمية متاحة
    // إنشاء StockMovement نوع 'out'
    // product->decrement('stock_quantity', qty)
    // تحديث serials إذا موجودة (status = 'sold')
}

// عند التعديل: يحسب الفرق ويعدل المخزون
public function updated(SaleItem $item): void {
    // إذا تغيرت الكمية: diff = جديد - قديم
    // إذا diff > 0 → decrement بالفرق
    // إذا diff < 0 → increment بالفرق
}

// عند الحذف: يرجع للمخزون
public function deleted(SaleItem $item): void {
    // حذف StockMovement المرتبط
    // product->increment('stock_quantity', qty)
}
```

### 4. الـ Resource — `SaleResource.php`

**التخطيط داخل الـ Repeater:**

```
شبكة 6 أعمدة:
├── المنتج (Select)       → columnSpan(3) — نصف العرض
├── الكمية (TextInput)    → columnSpan(1)
├── سعر الوحدة           → columnSpan(1)
└── الإجمالي (disabled)  → columnSpan(1)
السيريالات (متخفي)       → columnSpanFull (يظهر فقط إذا has_serial=true)
```

**الكود:**

```php
Repeater::make('items')
    ->relationship('items')      // ← يحفظ تلقائياً عبر العلاقة
    ->columns(6)
    ->defaultItems(1)
    ->addActionLabel('إضافة صنف')
    ->reorderableWithButtons()
    ->collapsible()
    ->cloneable()
    ->itemLabel(fn($state) => Product::find($state['product_id'])?->name ?? 'صنف جديد')
    ->schema([

        Select::make('product_id')
            ->columnSpan(3)
            ->reactive()
            ->afterStateUpdated(function($state, $set, $get) {
                $product = Product::find($state);
                $set('unit_price',      $product->sell_price);
                $set('has_serial',      $product->has_serial);
                $set('available_stock', $product->stock_quantity);
            }),

        TextInput::make('quantity')
            ->columnSpan(1)
            ->default(1)
            ->reactive()
            ->helperText(fn($get) => 'المتاح: ' . ($get('available_stock') ?? 0))
            ->afterStateUpdated(fn($state, $get, $set) =>
                $set('total_price', $state * $get('unit_price'))),

        TextInput::make('unit_price')
            ->columnSpan(1)
            ->reactive()
            ->afterStateUpdated(fn($state, $get, $set) =>
                $set('total_price', $state * $get('quantity'))),

        TextInput::make('total_price')
            ->columnSpan(1)
            ->disabled()
            ->dehydrated(false),   // ← لا يُحفظ (الـ Observer يحسبه)

        Select::make('serials')
            ->multiple()
            ->hidden(fn($get) => !$get('has_serial'))
            ->columnSpanFull(),

        Hidden::make('has_serial'),
        Hidden::make('available_stock'),
    ])
```

---

## ثانياً: فاتورة الشراء

### 1. الجداول

```sql
purchases:
  id, purchase_number, supplier_id (FK), purchase_date,
  vault_id (FK), total_amount, notes, created_by, timestamps

purchase_items:
  id, purchase_id (FK), product_id (FK),
  quantity, unit_price,
  sell_price,       -- سعر البيع المقترح (يُحدَّث على المنتج عند الحفظ)
  total_price,      -- quantity * unit_price
  serials (JSON),
  attachments (JSON),
  notes, timestamps
```

### 2. Observer — `app/Observers/PurchaseItemObserver.php`

```php
public function created(PurchaseItem $item): void {
    // StockMovement نوع 'in'
    // product->increment('stock_quantity', qty)
    // product->update(['purchase_price' => unit_price])
    // إذا sell_price > 0 → product->update(['sell_price' => sell_price])
    // إنشاء سجلات ProductSerial إذا موجودة (status = 'available')
}

public function updated(PurchaseItem $item): void {
    // حساب diff وتعديل المخزون
}

public function deleted(PurchaseItem $item): void {
    // product->decrement('stock_quantity', qty)
    // حذف السيريالات المرتبطة
}
```

### 3. الـ Resource — تخطيط الـ Repeater

```
شبكة 11 عموداً:
├── المنتج              → columnSpan(4)
├── الكمية              → columnSpan(1)
├── سعر الشراء          → columnSpan(2)
├── سعر البيع المقترح   → columnSpan(2)
└── الإجمالي            → columnSpan(2)
السيريالات              → columnSpanFull
المرفقات                → columnSpanFull
```

---

## ثالثاً: مرتجع البيع (الهيكل الجديد)

### المشكلة القديمة والحل

> **قبل:** `sale_returns` يحتوي على `product_id, quantity, unit_price` مباشرة ← فاتورة واحدة = صنف واحد فقط
> **بعد:** جدول منفصل للأصناف ← فاتورة واحدة = أصناف متعددة

### 1. الجداول

```sql
-- migration: 2026_04_08_000001_create_sale_return_items_table.php

-- sale_returns بعد التعديل (أُزيل منها product_id, quantity, unit_price)
sale_returns:
  id, sale_id (FK nullable), return_date, vault_id (FK nullable),
  total_amount,   -- يُحسب تلقائياً من مجموع الأصناف
  notes, created_by, timestamps, softDeletes

-- الجدول الجديد
sale_return_items:
  id, sale_return_id (FK cascade), product_id (FK cascade),
  quantity, unit_price, total_price,
  timestamps
  INDEX(sale_return_id, product_id)
```

**الـ Migration:**

```php
Schema::create('sale_return_items', function (Blueprint $table) {
    $table->id();
    $table->foreignId('sale_return_id')->constrained()->cascadeOnDelete();
    $table->foreignId('product_id')->constrained()->cascadeOnDelete();
    $table->integer('quantity');
    $table->decimal('unit_price', 10, 2);
    $table->decimal('total_price', 15, 2);
    $table->timestamps();
    $table->index(['sale_return_id', 'product_id']);
});

// نقل البيانات القديمة
DB::statement('
    INSERT INTO sale_return_items (sale_return_id, product_id, quantity, unit_price, total_price, ...)
    SELECT id, product_id, quantity, unit_price, total_amount, ...
    FROM sale_returns WHERE product_id IS NOT NULL
');

// حذف الأعمدة القديمة
Schema::table('sale_returns', function (Blueprint $table) {
    $table->dropForeign(['product_id']);
    $table->dropColumn(['product_id', 'quantity', 'unit_price']);
});
```

### 2. الـ Models

```php
// app/Models/SaleReturn.php — بعد التعديل
// أُزيل من fillable: product_id, quantity, unit_price
// أُضيف:
public function items() {
    return $this->hasMany(SaleReturnItem::class);
}

// app/Models/SaleReturnItem.php — جديد
namespace App\Models;

class SaleReturnItem extends Model
{
    protected $fillable = ['sale_return_id', 'product_id', 'quantity', 'unit_price', 'total_price'];

    protected $casts = [
        'quantity'    => 'integer',
        'unit_price'  => 'decimal:2',
        'total_price' => 'decimal:2',
    ];

    public function saleReturn() { return $this->belongsTo(SaleReturn::class); }
    public function product()    { return $this->belongsTo(Product::class); }
}
```

### 3. Observer الأصناف — `SaleReturnItemObserver.php` (جديد)

```php
namespace App\Observers;

use App\Models\SaleReturnItem;
use App\Models\StockMovement;

class SaleReturnItemObserver
{
    // قبل الحفظ: يحسب الإجمالي
    public function creating(SaleReturnItem $item): void {
        $item->total_price = round($item->quantity * $item->unit_price, 2);
    }

    public function updating(SaleReturnItem $item): void {
        $item->total_price = round($item->quantity * $item->unit_price, 2);
    }

    // بعد الإنشاء: يُرجع الكمية للمخزون
    public function created(SaleReturnItem $item): void {
        DB::transaction(function () use ($item) {
            $product = $item->product;
            if (!$product) return;

            StockMovement::create([
                'product_id'      => $product->id,
                'type'            => 'in',      // ← ترجع بضاعة للمخزون
                'quantity'        => $item->quantity,
                'reference_type'  => 'sale_return',
                'reference_id'    => $item->sale_return_id,
                'before_quantity' => $product->stock_quantity,
                'after_quantity'  => $product->stock_quantity + $item->quantity,
                'notes'           => "مرتجع مبيعات - صنف #{$item->id}",
                'created_by'      => auth()->id(),
            ]);

            $product->increment('stock_quantity', $item->quantity);
            $this->syncReturnTotal($item);
        });
    }

    // بعد التعديل: يعدل فرق المخزون
    public function updated(SaleReturnItem $item): void {
        DB::transaction(function () use ($item) {
            $product = $item->product;
            if (!$product) return;

            $diff = $item->quantity - $item->getOriginal('quantity');

            if ($diff > 0) $product->increment('stock_quantity', $diff);
            elseif ($diff < 0) $product->decrement('stock_quantity', abs($diff));

            $this->syncReturnTotal($item);
        });
    }

    // بعد الحذف: يعكس الكمية
    public function deleted(SaleReturnItem $item): void {
        DB::transaction(function () use ($item) {
            $product = $item->product;
            if (!$product) return;

            StockMovement::where('reference_type', 'sale_return')
                ->where('reference_id', $item->sale_return_id)
                ->where('notes', 'like', "%صنف #{$item->id}%")
                ->delete();

            $product->decrement('stock_quantity', $item->quantity);
            $this->syncReturnTotal($item);
        });
    }

    // ← الدالة المشتركة — تجمع الأصناف وتحدث total_amount
    private function syncReturnTotal(SaleReturnItem $item): void {
        $total = $item->saleReturn?->items()->sum('total_price') ?? 0;
        $item->saleReturn?->updateQuietly(['total_amount' => $total]);
        // updateQuietly ← مهمة جداً: لا تُطلق حدث updated مرة ثانية
    }
}
```

### 4. Observer الفاتورة — `SaleReturnObserver.php` (معدَّل)

```php
// لم يعد يتحكم في المخزون (ده شغل SaleReturnItemObserver)
// يتحكم فقط في الخزينة

public function created(SaleReturn $saleReturn): void {
    // لا شيء — الخزينة تُعالَج عند تغير total_amount
}

// يتفعل تلقائياً عندما syncReturnTotal يغير total_amount
public function updated(SaleReturn $saleReturn): void {
    if (!$saleReturn->isDirty('total_amount')) return;

    DB::transaction(function () use ($saleReturn) {
        $vault = $saleReturn->vault;
        if (!$vault) return;

        $oldAmount = (float) ($saleReturn->getOriginal('total_amount') ?? 0);
        $newAmount = (float) $saleReturn->total_amount;

        // حذف المعاملة القديمة وإنشاء جديدة
        VaultTransaction::where('reference_type', 'sale_return')
            ->where('reference_id', $saleReturn->id)
            ->delete();

        if ($oldAmount > 0) {
            $vault->decrement('balance', $oldAmount);
            $vault->refresh();
        }

        if ($newAmount > 0) {
            VaultTransaction::create([
                'vault_id'       => $vault->id,
                'type'           => 'out',   // ← خصم من الخزينة لرد مبلغ للعميل
                'amount'         => $newAmount,
                'reference_type' => 'sale_return',
                'reference_id'   => $saleReturn->id,
                'balance_before' => $vault->balance,
                'balance_after'  => $vault->balance - $newAmount,
                'notes'          => "رد مبلغ للعميل - مرتجع #{$saleReturn->id}",
                'created_by'     => auth()->id(),
            ]);
            $vault->decrement('balance', $newAmount);
        }
    });
}

public function deleted(SaleReturn $saleReturn): void {
    // حذف معاملة الخزينة + عكس الرصيد
    VaultTransaction::where('reference_type', 'sale_return')
        ->where('reference_id', $saleReturn->id)->delete();
    $vault->increment('balance', (float) $saleReturn->total_amount);
}
```

### 5. الـ Resource — `SaleReturnResource.php` (معدَّل)

```php
form:
  Section 'بيانات المرتجع' (columns 2)
  ├── sale_id     (Select → فاتورة البيع)
  ├── return_date (DatePicker)
  ├── vault_id    (Select → الخزينة لرد المبلغ)
  └── notes       (Textarea)

  Section 'أصناف المرتجع'
  └── Repeater::make('items')->relationship('items') ->columns(6)
      ├── product_id  columnSpan(3) → يملأ unit_price تلقائياً من sell_price
      ├── quantity    columnSpan(1)
      ├── unit_price  columnSpan(1)
      └── total_price columnSpan(1) disabled dehydrated(false)

  Section 'الإجمالي'
  └── Placeholder يحسب live: sum(qty * price)
```

---

## رابعاً: مرتجع الشراء (نفس النمط)

### 1. الجداول

```sql
-- migration: 2026_04_08_000002_create_purchase_return_items_table.php

-- purchase_returns بعد التعديل
purchase_returns:
  id, purchase_id (FK nullable), supplier_id (FK nullable),
  return_date, vault_id, total_amount, notes, created_by, timestamps

-- الجدول الجديد
purchase_return_items:
  id, purchase_return_id (FK cascade), product_id (FK cascade),
  quantity, unit_cost, total_price,
  timestamps
```

### 2. Observer الأصناف — `PurchaseReturnItemObserver.php` (جديد)

```php
public function created(PurchaseReturnItem $item): void {
    // التحقق: quantity <= product->stock_quantity (وإلا ValidationException)
    // StockMovement نوع 'out'  ← خصم من المخزون (بضاعة ترجع للمورد)
    // product->decrement('stock_quantity', qty)
    // syncReturnTotal()
}

public function updated(PurchaseReturnItem $item): void {
    // diff = جديد - قديم
    // إذا diff > 0 → decrement بالفرق (خصم أكثر)
    // إذا diff < 0 → increment بالفرق (رجع أقل)
    // syncReturnTotal()
}

public function deleted(PurchaseReturnItem $item): void {
    // product->increment('stock_quantity', qty)  ← عكس الخصم
    // syncReturnTotal()
}

private function syncReturnTotal(PurchaseReturnItem $item): void {
    $total = $item->purchaseReturn?->items()->sum('total_price') ?? 0;
    $item->purchaseReturn?->updateQuietly(['total_amount' => $total]);
}
```

### 3. Observer الفاتورة — `PurchaseReturnObserver.php` (معدَّل)

```php
public function updated(PurchaseReturn $purchaseReturn): void {
    if (!$purchaseReturn->isDirty('total_amount')) return;

    // --- الخزينة ---
    // VaultTransaction نوع 'in' ← استرداد مبلغ من المورد
    // vault->increment('balance', newAmount)

    // --- رصيد المورد ---
    // supplier->balance -= (newAmount - oldAmount)
}

public function deleted(PurchaseReturn $purchaseReturn): void {
    // عكس الخزينة: vault->decrement('balance', total_amount)
    // عكس المورد:  supplier->increment('balance', total_amount)
}
```

### 4. الـ Resource — تخطيط الـ Repeater

```
شبكة 9 أعمدة:
├── المنتج        → columnSpan(4)
├── الكمية        → columnSpan(1)
├── تكلفة الوحدة → columnSpan(2)
└── الإجمالي      → columnSpan(2)
```

---

## خامساً: التسجيل في `AppServiceProvider.php`

```php
// app/Providers/AppServiceProvider.php

// الـ Models
use App\Models\{SaleItem, SaleReturn, SaleReturnItem,
                PurchaseItem, PurchaseReturn, PurchaseReturnItem};

// الـ Observers
use App\Observers\{SaleItemObserver, SaleReturnObserver, SaleReturnItemObserver,
                   PurchaseItemObserver, PurchaseReturnObserver, PurchaseReturnItemObserver};

public function boot(): void {
    SaleItem::observe(SaleItemObserver::class);
    SaleReturn::observe(SaleReturnObserver::class);
    SaleReturnItem::observe(SaleReturnItemObserver::class);        // جديد

    PurchaseItem::observe(PurchaseItemObserver::class);
    PurchaseReturn::observe(PurchaseReturnObserver::class);
    PurchaseReturnItem::observe(PurchaseReturnItemObserver::class); // جديد
}
```

---

## سادساً: الملفات الجديدة والمعدَّلة (جدول مرجعي)

| الملف | الحالة | الوصف |
|-------|--------|-------|
| `database/migrations/2026_04_08_000001_create_sale_return_items_table.php` | **جديد** | ينشئ جدول الأصناف ويحذف الأعمدة القديمة مع نقل البيانات |
| `database/migrations/2026_04_08_000002_create_purchase_return_items_table.php` | **جديد** | نفس الفكرة لمرتجعات الشراء |
| `app/Models/SaleReturnItem.php` | **جديد** | Model أصناف مرتجع البيع |
| `app/Models/PurchaseReturnItem.php` | **جديد** | Model أصناف مرتجع الشراء |
| `app/Models/SaleReturn.php` | **معدَّل** | أُزيلت أعمدة الصنف — أُضيف `items()` |
| `app/Models/PurchaseReturn.php` | **معدَّل** | نفس الفكرة |
| `app/Observers/SaleReturnItemObserver.php` | **جديد** | المخزون + syncReturnTotal |
| `app/Observers/PurchaseReturnItemObserver.php` | **جديد** | المخزون + syncReturnTotal |
| `app/Observers/SaleReturnObserver.php` | **معدَّل** | الخزينة فقط |
| `app/Observers/PurchaseReturnObserver.php` | **معدَّل** | الخزينة + رصيد المورد |
| `app/Providers/AppServiceProvider.php` | **معدَّل** | تسجيل الـ Observers الجديدة |
| `SaleReturnResource.php` | **معدَّل** | Repeater بدل حقل صنف واحد |
| `PurchaseReturnResource.php` | **معدَّل** | Repeater بدل حقل صنف واحد |
| `SaleResource.php` | **معدَّل** | تخطيط `columns(6)` مع `columnSpan` |
| `PurchaseResource.php` | **معدَّل** | تخطيط `columns(11)` مع `columnSpan` |

---

## سابعاً: ترتيب التنفيذ في سيستم جديد

```bash
# 1. تشغيل المايجريشن
php artisan migrate

# 2. إنشاء الـ Models الجديدة ونسخ الكود المعدَّل للقديمة

# 3. إنشاء الـ Observers الجديدة ونسخ الكود المعدَّل للقديمة

# 4. تسجيل كل شيء في AppServiceProvider

# 5. تعديل الـ Resources (Repeater + columnSpan)

# 6. مسح الكاش
php artisan optimize:clear
```

---

## ثامناً: قواعد عامة مهمة

| القاعدة | السبب |
|---------|-------|
| `updateQuietly()` دائماً في `syncReturnTotal` | لو استخدمت `update()` ستُطلق observer.updated مرة ثانية وتحدث حلقة لا نهائية |
| `dehydrated(false)` على حقل الإجمالي في الـ Repeater | الإجمالي يُحسب في الـ Observer — لا نريد Filament يحفظ القيمة المعروضة |
| `DB::transaction()` في كل observer | إذا فشل أي جزء يُتراجع عن كل شيء |
| فحص `isDirty('total_amount')` في observer الفاتورة | لا نريد تشغيل منطق الخزينة عند أي تعديل آخر على الفاتورة |
| `->relationship('items')` في الـ Repeater | Filament يتولى الحفظ والتحديث والحذف تلقائياً |
