﻿# 📋 قائمة مهام المرحلة الخامسة — Apex Logistics Dashboard
# (النسخة الأعظم على الإطلاق — 7 تيكتات هندسية)

> **القاعدة الذهبية:** لا تبدأ تيكت جديدة حتى تُغلق التيكت السابقة بالكامل وتتأكد من نجاح كل نقاط الفحص فيها.
>
> **مسار المشروع:** `D:\Important Projects\Apex_Logistics\Dashboard`
>
> **الملفات المرجعية اللي لازم تفتحها قبل ما تشتغل:**
> - [DASHBOARD_ROADMAP.md](file:///D:/Important%20Projects/Apex_Logistics/Dashboard/DASHBOARD_ROADMAP.md)
> - [Order.php](file:///D:/Important%20Projects/Apex_Logistics/Dashboard/app/Models/Order.php)
> - [ErrandStopObserver.php](file:///D:/Important%20Projects/Apex_Logistics/Dashboard/app/Observers/ErrandStopObserver.php)
> - [OrderObserver.php](file:///D:/Important%20Projects/Apex_Logistics/Dashboard/app/Observers/OrderObserver.php)
> - [CreateOrder.php](file:///D:/Important%20Projects/Apex_Logistics/Dashboard/app/Filament/Resources/Orders/Pages/CreateOrder.php)
> - [EditOrder.php](file:///D:/Important%20Projects/Apex_Logistics/Dashboard/app/Filament/Resources/Orders/Pages/EditOrder.php)
> - [OrderForm.php](file:///D:/Important%20Projects/Apex_Logistics/Dashboard/app/Filament/Resources/Orders/Schemas/OrderForm.php)
> - [AppServiceProvider.php](file:///D:/Important%20Projects/Apex_Logistics/Dashboard/app/Providers/AppServiceProvider.php)

---

> [!CAUTION]
> ## 📝 قاعدة التوثيق الإلزامية (Mandatory Changelog Rule)
>
> **بعد إغلاق كل تيكت، لازم تحدّث ملف التوثيق:**
> **المسار:** `D:\Important Projects\Apex_Logistics\Dashboard\PHASE5_CHANGELOG.md`
>
> **كل تغيير بيتسجل بالحرف الواحد — بدون استثناء:**
> - ✏️ كل **ملف جديد** اتأنشأ → سجّل اسمه الكامل ومحتواه بالكامل.
> - 🔧 كل **سطر اتعدّل** في ملف موجود → سجّل الملف ورقم السطر والكود القديم والكود الجديد (بصيغة diff).
> - ➕ كل **سطر اتضاف** → سجّل الملف ورقم السطر والكود المضاف.
> - ➖ كل **سطر اتحذف** → سجّل الملف ورقم السطر والكود المحذوف.
> - 📦 كل **حزمة اتثبتت** → سجّل اسمها وأمر التثبيت.
> - 🗃️ كل **migration اتعملت** → سجّل اسم الملف وكل الأعمدة والقيود.
> - ⚙️ كل **أمر terminal اتشغّل** → سجّل الأمر ونتيجته.
>
> **الصيغة المطلوبة لكل تيكت في الـ Changelog:**
> ```markdown
> # 🎟️ التيكت [رقم]: [الاسم]
> **تاريخ البدء:** YYYY-MM-DD HH:MM
> **تاريخ الانتهاء:** YYYY-MM-DD HH:MM
>
> ## الملفات الجديدة (New Files)
> ### `path/to/file.php`
> ```php
> // المحتوى الكامل للملف
> ```
>
> ## الملفات المعدّلة (Modified Files)
> ### `path/to/existing_file.php`
> ```diff
> - الكود القديم (سطر XX)
> + الكود الجديد (سطر XX)
> ```
>
> ## الأوامر المُنفّذة (Commands Executed)
> | الأمر | النتيجة |
> |-------|--------|
> | `composer require xyz` | ✅ نجح |
>
> ## نتائج الفحص (Verification Results)
> | الفحص | النتيجة |
> |-------|--------|
> | فحص X | ✅ |
> ```
>
> **⚠️ ممنوع إغلاق أي تيكت بدون تحديث هذا الملف. هذا ليس اختياري.**


---
---

# 🎟️ التيكت الأولى: التأسيس المعماري (Core Foundation)

> **الهدف:** بناء كل الأساسيات اللي المشروع كله بيعتمد عليها. لا واجهات، لا شاشات — أساسات فقط.
> **الاعتماديات:** لا شيء. هذه التيكت مستقلة تماماً.
> **التقدير الزمني:** ساعتين.

---

## 1.1 — تثبيت الحزم المفقودة

### 1.1.1 تثبيت Laravel Sanctum
- [x] **المشكلة:** الملف `routes/api.php` بيستخدم `auth:sanctum` middleware لكن الحزمة مش مثبتة في `composer.json`. يعني الـ API هيرمي Error لو حد حاول يستخدمه.
- [x] **الأمر:**
  ```bash
  cd "D:\Important Projects\Apex_Logistics\Dashboard"
  composer require laravel/sanctum
  ```
- [x] **بعد التثبيت:** انشر ملفات الحزمة:
  ```bash
  php artisan vendor:publish --provider="Laravel\Sanctum\SanctumServiceProvider"
  ```
- [x] **بعد النشر:** شغّل الـ migration عشان ينشئ جدول `personal_access_tokens`:
  ```bash
  php artisan migrate
  ```
- [x] **⚠️ ممنوع:** تعديل أي كود في `routes/api.php` في هذه المرحلة. إحنا بنثبت الحزمة بس.
- [x] **✅ فحص:** افتح `config/sanctum.php` — لو موجود يبقى تمام.

### 1.1.2 تثبيت Spatie Laravel Settings
- [x] **الأمر:**
  ```bash
  composer require spatie/laravel-settings
  ```
- [x] **بعد التثبيت:** انشر الـ migration:
  ```bash
  php artisan vendor:publish --provider="Spatie\LaravelSettings\LaravelSettingsServiceProvider" --tag="migrations"
  ```
- [x] **بعد النشر:** شغّل الـ migration عشان ينشئ جدول `settings`:
  ```bash
  php artisan migrate
  ```
- [x] **✅ فحص:** افتح `database/migrations/` وابحث عن ملف اسمه فيه `create_settings_table` — لو موجود يبقى تمام.

### 1.1.3 تثبيت moneyphp/money
- [x] **الأمر:**
  ```bash
  composer require moneyphp/money
  ```
- [x] **✅ فحص:** افتح `composer.json` — لازم تلاقي الحزم الثلاثة (`laravel/sanctum`, `spatie/laravel-settings`, `moneyphp/money`) موجودة في `require`.

---

## 1.2 — إنشاء PHP Enums

> **ليه بنعمل Enums؟** حالياً كل الحالات (status, type, etc.) مكتوبة كـ String عادي. لو حد كتب `'deliverd'` بدل `'delivered'` بغلطة إملائية، مفيش حاجة في الكود هتمنعه ولا تحذره. الـ Enums بتحل المشكلة دي نهائياً.

### 1.2.1 إنشاء مجلد الـ Enums
- [x] أنشئ مجلد: `app/Enums/`

### 1.2.2 إنشاء `app/Enums/OrderStatus.php`
- [x] الملف الجديد:
  ```php
  <?php

  namespace App\Enums;

  enum OrderStatus: string
  {
      case Pending = 'pending';
      case Assigned = 'assigned';
      case PendingPricing = 'pending_pricing';
      case Priced = 'priced';
      case PickedUp = 'picked_up';
      case Delivered = 'delivered';
      case PartiallyDelivered = 'partially_delivered';
      case CancellationRequested = 'cancellation_requested';
      case Cancelled = 'cancelled';
      case Disputed = 'disputed';
  }
  ```
- [x] **⚠️ تأكد:** القيم (values) لازم تطابق بالضبط اللي موجود في `OrderForm.php` السطور 150–161. لو في اختلاف حرف واحد هيكسر الداتا القديمة.

### 1.2.3 إنشاء `app/Enums/FinancialStatus.php`
- [x] الملف الجديد:
  ```php
  <?php

  namespace App\Enums;

  enum FinancialStatus: string
  {
      case Unsettled = 'unsettled';
      case Settled = 'settled';
      case Refunded = 'refunded';
      case Frozen = 'frozen';
  }
  ```
- [x] **⚠️ ملاحظة:** القيم القديمة في الداتابيز (`unsettled`, `settled`) لازم تبقى موجودة. القيم الجديدة (`refunded`, `frozen`) ستُستخدم لاحقاً في الأزرار والقيود المالية.

### 1.2.4 إنشاء `app/Enums/OrderType.php`
- [x] الملف الجديد:
  ```php
  <?php

  namespace App\Enums;

  enum OrderType: string
  {
      case Standard = 'standard';
      case Errand = 'errand';
  }
  ```

### 1.2.5 إنشاء `app/Enums/PaymentMethod.php`
- [x] الملف الجديد:
  ```php
  <?php

  namespace App\Enums;

  enum PaymentMethod: string
  {
      case Cash = 'cash';
      case Visa = 'visa';
      case Transfer = 'transfer';
  }
  ```

### 1.2.6 إنشاء `app/Enums/StopStatus.php`
- [x] الملف الجديد:
  ```php
  <?php

  namespace App\Enums;

  enum StopStatus: string
  {
      case Pending = 'pending';
      case Purchased = 'purchased';
      case NotFound = 'not_found';
  }
  ```

### 1.2.7 إنشاء `app/Enums/BatchStatus.php`
- [x] الملف الجديد:
  ```php
  <?php

  namespace App\Enums;

  enum BatchStatus: string
  {
      case Active = 'active';
      case Completed = 'completed';
      case Cancelled = 'cancelled';
      case Abandoned = 'abandoned';
  }
  ```

### 1.2.8 إنشاء `app/Enums/LedgerType.php`
- [x] الملف الجديد:
  ```php
  <?php

  namespace App\Enums;

  enum LedgerType: string
  {
      case Credit = 'credit';
      case Debit = 'debit';
  }
  ```

- [x] **✅ فحص:** لازم يكون عندك 7 ملفات جوه `app/Enums/`. شغّل الأمر ده للتأكد:
  ```bash
  php artisan tinker --execute="echo App\Enums\OrderStatus::Delivered->value;"
  ```
  لازم يطبع `delivered`. لو طبع Error يبقى في مشكلة في الـ namespace أو اسم الملف.

---

## 1.3 — إنشاء OrderStateMachine

> **ليه بنعمله؟** حالياً أي حد يقدر يغيّر حالة الطلب من `pending` لـ `delivered` مباشرة بدون ما يمر بالحالات الوسيطة. ده كارثة أمنية ومالية. الـ State Machine بيمنع أي انتقال غير شرعي.

### 1.3.1 إنشاء Exception مخصص
- [x] أنشئ `app/Exceptions/InvalidStatusTransitionException.php`:
  ```php
  <?php

  namespace App\Exceptions;

  use App\Enums\OrderStatus;
  use Exception;

  class InvalidStatusTransitionException extends Exception
  {
      public function __construct(OrderStatus $from, OrderStatus $to)
      {
          parent::__construct(
              "انتقال غير مسموح: من [{$from->value}] إلى [{$to->value}]."
          );
      }
  }
  ```

### 1.3.2 إنشاء `app/Services/OrderStateMachine.php`
- [x] أنشئ الملف بالمحتوى التالي.
- [x] **المفتاح (key)** = الحالة الحالية. **القيمة (value)** = مصفوفة الحالات المسموح الانتقال إليها.
- [x] الـ Standard Orders لها مسار مختلف عن الـ Errand Orders (الطلب الحر عنده حالات إضافية زي `pending_pricing` و `priced`).
  ```php
  <?php

  namespace App\Services;

  use App\Enums\OrderStatus;
  use App\Enums\OrderType;
  use App\Exceptions\InvalidStatusTransitionException;
  use App\Models\Order;

  class OrderStateMachine
  {
      /**
       * خريطة الانتقالات المسموح بها لكل نوع طلب.
       * المفتاح = الحالة الحالية.
       * القيمة = مصفوفة الحالات اللي مسموح ننتقل ليها.
       */
      private static array $standardTransitions = [
          'pending'                 => ['assigned', 'cancelled'],
          'assigned'                => ['picked_up', 'cancellation_requested'],
          'picked_up'               => ['delivered'],
          'delivered'               => ['disputed'],
          'cancellation_requested'  => ['cancelled', 'pending'],   // pending = إعادة تعيين
          'cancelled'               => [],                          // حالة نهائية
          'disputed'                => ['delivered', 'cancelled'],  // حل النزاع
      ];

      private static array $errandTransitions = [
          'pending'                 => ['assigned', 'cancelled'],
          'assigned'                => ['pending_pricing', 'cancellation_requested'],
          'pending_pricing'         => ['priced', 'cancellation_requested'],
          'priced'                  => ['picked_up', 'cancellation_requested'],
          'picked_up'               => ['delivered', 'partially_delivered'],
          'delivered'               => ['disputed'],
          'partially_delivered'     => ['disputed'],
          'cancellation_requested'  => ['cancelled', 'pending'],
          'cancelled'               => [],
          'disputed'                => ['delivered', 'partially_delivered', 'cancelled'],
      ];

      /**
       * هل هذا الانتقال مسموح؟
       */
      public static function canTransition(OrderStatus $from, OrderStatus $to, OrderType $type): bool
      {
          $map = $type === OrderType::Standard
              ? self::$standardTransitions
              : self::$errandTransitions;

          $allowed = $map[$from->value] ?? [];

          return in_array($to->value, $allowed);
      }

      /**
       * نفّذ الانتقال أو ارمِ Exception.
       * ⚠️ هذه الدالة لا تحفظ الطلب — أنت المسؤول عن الحفظ بعدها.
       */
      public static function transition(Order $order, OrderStatus $to): void
      {
          $from = $order->status;

          // لو الحالة الحالية string عادي (قبل ربط الـ Enum)، حوّلها
          if (is_string($from)) {
              $from = OrderStatus::from($from);
          }

          if (!self::canTransition($from, $to, $order->order_type)) {
              throw new InvalidStatusTransitionException($from, $to);
          }

          $order->status = $to;
      }
  }
  ```
- [x] **⚠️ ملاحظة مهمة جداً:** الدالة `transition()` بتغيّر الـ `status` على الـ Model Object بس. هي **لا تحفظ** في الداتابيز. أنت لازم تنادي `$order->save()` أو `$order->saveQuietly()` بعدها بنفسك. ده عشان تقدر تغيّر حقول تانية قبل الحفظ.

- [x] **✅ فحص:** شغّل في tinker:
  ```bash
  php artisan tinker --execute="
    use App\Services\OrderStateMachine;
    use App\Enums\{OrderStatus, OrderType};
    echo OrderStateMachine::canTransition(OrderStatus::Pending, OrderStatus::Delivered, OrderType::Standard) ? 'FAIL' : 'PASS';
  "
  ```
  لازم يطبع `PASS` (يعني الانتقال المباشر ممنوع).

---

## 1.4 — إنشاء MoneyHelper

> **ليه بنعمله؟** PHP بيعمل أخطاء في الأرقام العشرية. مثلاً `0.1 + 0.2 = 0.30000000000000004`. لو ده حصل في حساب فاتورة، العميل هيتحاسب غلط. الحل: نخزن كل الفلوس كـ Integer بالقرش/الهللة، ونقسم على 100 لما نعرض بس.

### 1.4.1 إنشاء مجلد Support
- [x] أنشئ مجلد: `app/Support/` (لو مش موجود)

### 1.4.2 إنشاء `app/Support/MoneyHelper.php`
- [x] الملف الجديد:
  ```php
  <?php

  namespace App\Support;

  /**
   * كل الدوال هنا بتتعامل مع الفلوس كـ Integer (قرش/هللة).
   * 1 ريال = 100 هللة. 1 جنيه = 100 قرش.
   * الحفظ في الداتابيز: Integer (هللات).
   * العرض للمستخدم: مقسوم على 100.
   */
  class MoneyHelper
  {
      /**
       * تحويل من مبلغ عشري (Float) إلى Integer بالقرش.
       * يُستخدم فقط في الـ Migration لتحويل الداتا القديمة.
       * ⚠️ ممنوع استخدامه في الكود العادي — الكود العادي يتعامل مع Integer مباشرة.
       */
      public static function toCents(float $amount): int
      {
          return (int) round($amount * 100);
      }

      /**
       * تحويل من Integer (قرش) إلى String منسق للعرض.
       * مثال: fromCents(1550) → "15.50"
       */
      public static function fromCents(int $cents): string
      {
          return number_format($cents / 100, 2, '.', '');
      }

      /**
       * عرض منسق مع العملة.
       * مثال: display(1550, 'SAR') → "15.50 SAR"
       */
      public static function display(int $cents, string $currency = 'SAR'): string
      {
          return self::fromCents($cents) . ' ' . $currency;
      }

      /**
       * جمع آمن لمبلغين.
       */
      public static function addCents(int $a, int $b): int
      {
          return $a + $b;
      }

      /**
       * حساب الضريبة.
       * الـ $amountCents = المبلغ بالقرش.
       * الـ $vatPercentageTimes100 = النسبة × 100 (مثلاً 15% = 1500).
       * مثال: calculateVat(1000, 1500) → 150 (يعني 1.50 ريال ضريبة على 10 ريال)
       */
      public static function calculateVat(int $amountCents, int $vatPercentageTimes100): int
      {
          return (int) round($amountCents * $vatPercentageTimes100 / 10000);
      }
  }
  ```
- [x] **✅ فحص:** شغّل في tinker:
  ```bash
  php artisan tinker --execute="
    use App\Support\MoneyHelper;
    echo MoneyHelper::calculateVat(1000, 1500); // لازم يطبع 150
    echo ' | ';
    echo MoneyHelper::display(1550, 'SAR');       // لازم يطبع 15.50 SAR
  "
  ```

---

## 1.5 — إضافة Private Disk

> **ليه؟** عاوزين صور الفواتير تتخزن في مكان آمن مش متاح للعامة. الـ `local` disk الافتراضي في Laravel 12 بالفعل يشير إلى `storage/app/private`، لكن إحنا هنعمل disk اسمه `private` بشكل صريح عشان الكود يبقى واضح ولو غيّرنا لـ S3 مستقبلاً نغيّر سطر واحد.

### 1.5.1 تعديل `config/filesystems.php`
- [x] **الملف:** [config/filesystems.php](file:///D:/Important%20Projects/Apex_Logistics/Dashboard/config/filesystems.php)
- [x] **المطلوب:** إضافة disk جديد اسمه `private` بعد الـ `s3` disk (سطر 61) وقبل الـ `]` القافل (سطر 63).
- [x] **الكود اللي هيتضاف:**
  ```php
  'private' => [
      'driver' => env('PRIVATE_DISK_DRIVER', 'local'),
      'root'   => storage_path('app/private/uploads'),
      'serve'  => false,
      'throw'  => false,
      'visibility' => 'private',
  ],
  ```
- [x] **⚠️ لاحظ:** الـ `root` بيشير لمجلد فرعي `uploads` عشان ميتلخبطش مع الملفات الأصلية في `app/private`.
- [x] **⚠️ لاحظ:** الـ `driver` بيقرا من `.env` عشان لو عاوزين نحوّل لـ S3 نغيّر `PRIVATE_DISK_DRIVER=s3` في `.env` بس.
- [x] **✅ فحص:** شغّل:
  ```bash
  php artisan tinker --execute="echo Storage::disk('private')->path('test.txt');"
  ```
  لازم يطبع مسار ينتهي بـ `storage\app\private\uploads\test.txt`.

---

## ✅ فحص نهائي للتيكت الأولى
- [x] `composer.json` فيه: `laravel/sanctum`, `spatie/laravel-settings`, `moneyphp/money`.
- [x] `app/Enums/` فيه 7 ملفات.
- [x] `app/Services/OrderStateMachine.php` موجود وشغال.
- [x] `app/Support/MoneyHelper.php` موجود وشغال.
- [x] `config/filesystems.php` فيه disk `private`.
- [x] شغّل `php artisan serve` وتأكد إن الموقع مش كاسر (لا أخطاء).

## 📝 توثيق التيكت الأولى (إلزامي)
- [x] **حدّث `PHASE5_CHANGELOG.md`** بكل التفاصيل التالية:
  - [x] سجّل الحزم الثلاثة اللي اتثبتت + أوامر التثبيت + نتائجها.
  - [x] سجّل كل ملف Enum اتأنشأ (7 ملفات) بالمحتوى الكامل.
  - [x] سجّل ملف `OrderStateMachine.php` بالمحتوى الكامل + ملف `InvalidStatusTransitionException.php`.
  - [x] سجّل ملف `MoneyHelper.php` بالمحتوى الكامل.
  - [x] سجّل التعديل على `config/filesystems.php` بصيغة diff (الكود القديم vs الجديد).
  - [x] سجّل كل أوامر الفحص اللي اتشغلت ونتائجها.

---
---

# 🎟️ التيكت الثانية: صيانة قواعد البيانات (Database Overhaul)

> **الهدف:** تعديل الهيكل الحالي ليستوعب الفلوس كـ Integer، وتوسيع الـ Enums الضيقة، وإنشاء الجداول الجديدة.
> **الاعتماديات:** التيكت الأولى لازم تكون مغلقة.
> **التقدير الزمني:** ساعة ونص.
> **⚠️ خطورة عالية:** هذه التيكت بتعدّل بيانات موجودة فعلاً. **خد Backup قبل ما تبدأ!**

---

## 2.1 — توسيع `financial_status` من Enum إلى String

> **المشكلة:** حالياً العمود `financial_status` في جدول `orders` هو `enum('unsettled','settled')` على مستوى الداتابيز. محتاجين نضيف `refunded` و `frozen` لكن MySQL مش بيسمح بتعديل Enum بسهولة. الحل: نحوّله لـ `string` والـ PHP Enum اللي عملناه هو اللي يضمن الـ Type Safety.

### 2.1.1 إنشاء الـ Migration
- [x] **الأمر:**
  ```bash
  php artisan make:migration expand_financial_status_on_orders_table --table=orders
  ```
- [x] **محتوى الـ Migration:**
  ```php
  public function up(): void
  {
      Schema::table('orders', function (Blueprint $table) {
          $table->string('financial_status')->default('unsettled')->change();
      });
  }

  public function down(): void
  {
      // ⚠️ الرجوع للـ enum ممكن يفشل لو في قيم جديدة في الداتا
      Schema::table('orders', function (Blueprint $table) {
          $table->enum('financial_status', ['unsettled', 'settled'])->default('unsettled')->change();
      });
  }
  ```
- [x] **⚠️ مطلوب:** حزمة `doctrine/dbal` لتعديل الأعمدة. لو مش مثبتة:
  ```bash
  composer require doctrine/dbal
  ```
- [x] **شغّل:**
  ```bash
  php artisan migrate
  ```
- [x] **✅ فحص:** اعمل query في tinker:
  ```bash
  php artisan tinker --execute="echo \Schema::getColumnType('orders', 'financial_status');"
  ```
  لازم يطبع `string` (مش `enum`).

---

## 2.2 — تحويل الأعمدة المالية من Decimal إلى Integer

> **المشكلة:** كل أعمدة الفلوس حالياً `decimal(10,2)`. محتاجين نحوّلها لـ `integer` (بالقرش). الداتا القديمة لازم تتحوّل بأمان (القيمة القديمة × 100).
>
> **⚠️ خطوة حرجة جداً. خد Backup كامل للداتابيز قبل ما تبدأ!**

### 2.2.1 إنشاء الـ Migration
- [x] **الأمر:**
  ```bash
  php artisan make:migration convert_financial_columns_to_integer
  ```
- [x] **محتوى الـ Migration (نموذج مفصّل):**
  ```php
  public function up(): void
  {
      // === جدول orders ===
      // الخطوة 1: إضافة أعمدة مؤقتة
      Schema::table('orders', function (Blueprint $table) {
          $table->integer('total_amount_cents')->default(0)->after('total_amount');
          $table->integer('tax_cents')->default(0)->after('tax');
          $table->integer('shipping_cents')->default(0)->after('shipping');
          $table->integer('discount_cents')->default(0)->after('discount');
      });

      // الخطوة 2: نسخ الداتا (القيمة القديمة × 100)
      DB::statement('UPDATE orders SET total_amount_cents = ROUND(total_amount * 100)');
      DB::statement('UPDATE orders SET tax_cents = ROUND(tax * 100)');
      DB::statement('UPDATE orders SET shipping_cents = ROUND(shipping * 100)');
      DB::statement('UPDATE orders SET discount_cents = ROUND(discount * 100)');

      // الخطوة 3: حذف الأعمدة القديمة
      Schema::table('orders', function (Blueprint $table) {
          $table->dropColumn(['total_amount', 'tax', 'shipping', 'discount']);
      });

      // الخطوة 4: إعادة تسمية الأعمدة الجديدة
      Schema::table('orders', function (Blueprint $table) {
          $table->renameColumn('total_amount_cents', 'total_amount');
          $table->renameColumn('tax_cents', 'tax');
          $table->renameColumn('shipping_cents', 'shipping');
          $table->renameColumn('discount_cents', 'discount');
      });

      // === جدول errand_stops ===
      Schema::table('errand_stops', function (Blueprint $table) {
          $table->integer('receipt_amount_cents')->nullable()->after('receipt_amount');
      });
      DB::statement('UPDATE errand_stops SET receipt_amount_cents = ROUND(COALESCE(receipt_amount, 0) * 100)');
      Schema::table('errand_stops', function (Blueprint $table) {
          $table->dropColumn('receipt_amount');
      });
      Schema::table('errand_stops', function (Blueprint $table) {
          $table->renameColumn('receipt_amount_cents', 'receipt_amount');
      });

      // === جدول order_items ===
      Schema::table('order_items', function (Blueprint $table) {
          $table->integer('unit_price_cents')->default(0)->after('unit_price');
      });
      DB::statement('UPDATE order_items SET unit_price_cents = ROUND(unit_price * 100)');
      Schema::table('order_items', function (Blueprint $table) {
          $table->dropColumn('unit_price');
      });
      Schema::table('order_items', function (Blueprint $table) {
          $table->renameColumn('unit_price_cents', 'unit_price');
      });

      // === جدول products ===
      Schema::table('products', function (Blueprint $table) {
          $table->integer('price_cents')->default(0)->after('price');
      });
      DB::statement('UPDATE products SET price_cents = ROUND(price * 100)');
      Schema::table('products', function (Blueprint $table) {
          $table->dropColumn('price');
      });
      Schema::table('products', function (Blueprint $table) {
          $table->renameColumn('price_cents', 'price');
      });
  }
  ```
- [x] **⚠️ هام جداً:** لازم تضيف `use Illuminate\Support\Facades\DB;` في أعلى ملف الـ Migration.
- [x] **شغّل:**
  ```bash
  php artisan migrate
  ```
- [x] **✅ فحص:**
  ```bash
  php artisan tinker --execute="echo App\Models\Order::first()?->total_amount ?? 'No orders';"
  ```
  لو كان في أوردر قيمته `15.50`، لازم دلوقتي يطبع `1550`.

---

## 2.3 — إنشاء جداول `delivery_batches` و `financial_ledgers` وأعمدة Snapshot

### 2.3.1 Migration جدول `delivery_batches`
- [x] **الأمر:**
  ```bash
  php artisan make:migration create_delivery_batches_table
  ```
- [x] **المحتوى:**
  ```php
  public function up(): void
  {
      Schema::create('delivery_batches', function (Blueprint $table) {
          $table->id();
          $table->foreignId('driver_id')->constrained('drivers')->restrictOnDelete();
          $table->string('status')->default('active');  // يُحكم بـ BatchStatus Enum
          $table->timestamp('started_at')->nullable();
          $table->timestamp('completed_at')->nullable();
          $table->timestamp('last_activity_at')->nullable();
          $table->timestamp('warned_at')->nullable();
          $table->timestamps();

          $table->index(['status', 'last_activity_at']); // للبحث السريع عن الرحلات المتأخرة
      });
  }
  ```

### 2.3.2 Migration إضافة `batch_id` لجدول `orders`
- [x] **الأمر:**
  ```bash
  php artisan make:migration add_batch_id_to_orders_table --table=orders
  ```
- [x] **المحتوى:**
  ```php
  public function up(): void
  {
      Schema::table('orders', function (Blueprint $table) {
          $table->foreignId('batch_id')->nullable()->after('driver_id')
              ->constrained('delivery_batches')->nullOnDelete();
      });
  }
  ```

### 2.3.3 Migration أعمدة Price Snapshot + Version لجدول `orders`
- [x] **الأمر:**
  ```bash
  php artisan make:migration add_pricing_snapshot_and_version_to_orders_table --table=orders
  ```
- [x] **المحتوى:**
  ```php
  public function up(): void
  {
      Schema::table('orders', function (Blueprint $table) {
          $table->integer('agreed_delivery_fee')->nullable()->after('discount');
          $table->integer('applied_vat_percentage')->nullable()->after('agreed_delivery_fee');
          $table->boolean('applied_vat_enabled')->default(true)->after('applied_vat_percentage');
          $table->string('pricing_strategy_used')->nullable()->after('applied_vat_enabled');
          $table->unsignedInteger('version')->default(1)->after('pricing_strategy_used');
      });
  }
  ```
- [x] **⚠️ لاحظ:** `applied_vat_percentage` يُخزَّن × 100 (مثلاً 15% = 1500). `agreed_delivery_fee` يُخزَّن بالقرش.

### 2.3.4 Migration جدول `financial_ledgers`
- [x] **الأمر:**
  ```bash
  php artisan make:migration create_financial_ledgers_table
  ```
- [x] **المحتوى:**
  ```php
  public function up(): void
  {
      Schema::create('financial_ledgers', function (Blueprint $table) {
          $table->id();
          $table->foreignId('order_id')->constrained('orders')->restrictOnDelete();
          $table->foreignId('driver_id')->nullable()->constrained('drivers')->restrictOnDelete();
          $table->string('type');                  // LedgerType Enum (credit/debit)
          $table->integer('amount');               // بالقرش/الهللة
          $table->string('category');              // delivery_fee, receipt_collection, penalty, reversal, refund
          $table->string('description');
          $table->unsignedInteger('reference_version'); // version الطلب وقت القيد
          $table->string('idempotency_key')->unique(); // ⚠️ مفتاح منع التكرار
          $table->foreignId('created_by')->nullable()->constrained('users')->nullOnDelete();
          $table->timestamps();

          $table->index(['driver_id', 'type']);     // للتجميع السريع لأرصدة المناديب
          $table->index('order_id');
      });
  }
  ```

### 2.3.5 تشغيل كل الـ Migrations
- [x] **الأمر:**
  ```bash
  php artisan migrate
  ```
- [x] **✅ فحص:** شغّل:
  ```bash
  php artisan tinker --execute="echo Schema::hasTable('delivery_batches') && Schema::hasTable('financial_ledgers') ? 'ALL GOOD' : 'MISSING TABLES';"
  ```
  لازم يطبع `ALL GOOD`.

---

## ✅ فحص نهائي للتيكت الثانية
- [x] `financial_status` عمود `string` مش `enum` في الداتابيز.
- [x] كل الأعمدة المالية `integer` (مش `decimal`).
- [x] جدول `delivery_batches` موجود بالأعمدة الصحيحة.
- [x] جدول `financial_ledgers` موجود بعمود `idempotency_key` (unique).
- [x] جدول `orders` فيه: `batch_id`, `agreed_delivery_fee`, `applied_vat_percentage`, `applied_vat_enabled`, `pricing_strategy_used`, `version`.
- [x] `php artisan serve` شغال بدون أخطاء.

## 📝 توثيق التيكت الثانية (إلزامي)
- [x] **حدّث `PHASE5_CHANGELOG.md`** بكل التفاصيل التالية:
  - [x] سجّل كل ملف migration اتأنشأ باسمه الكامل ومحتواه الكامل.
  - [x] سجّل أمر `doctrine/dbal` لو اتثبت.
  - [x] سجّل أمر `php artisan migrate` ونتيجته.
  - [x] سجّل عينة مقارنة قبل/بعد تحويل الأعمدة المالية (قيمة قديمة decimal × 100 = قيمة جديدة integer).
  - [x] سجّل كل أوامر الفحص ونتائجها.

---
---

# 🎟️ التيكت الثالثة: الإعدادات المركزية ومحرك التسعير (Settings & Pricing)

> **الهدف:** بناء شاشة الإعدادات ومحرك التسعير الذكي مع Price Snapshotting.
> **الاعتماديات:** التيكتان الأولى والثانية.
> **التقدير الزمني:** 3 ساعات.

---

## 3.1 — إنشاء GeneralSettings Class

### 3.1.1 إنشاء `app/Settings/GeneralSettings.php`
- [x] أنشئ مجلد `app/Settings/`
- [x] الملف الجديد:
  ```php
  <?php

  namespace App\Settings;

  use Spatie\LaravelSettings\Settings;

  class GeneralSettings extends Settings
  {
      // ——— الإعدادات المالية ———
      public bool $enable_vat;
      public int $vat_percentage;            // × 100 (مثلاً 1500 = 15%)
      public string $currency;
      public string $delivery_fee_strategy;  // 'flat_fee' أو 'base_plus_stop'
      public int $base_delivery_fee;         // بالقرش
      public int $extra_stop_fee;            // بالقرش

      // ——— إعدادات المناديب ———
      public string $dispatch_mode;          // 'manual' أو 'auto'
      public bool $enable_batched_orders;
      public int $max_orders_per_driver;
      public string $batch_routing_mode;     // 'same_zone' أو 'strict_nearby'
      public int $batch_inactivity_timeout_minutes;
      public int $batch_grace_period_minutes;
      public bool $driver_can_cancel_directly;

      // ——— إعدادات الطلبات ———
      public bool $require_receipt_image;
      public array $available_payment_methods;

      public static function group(): string
      {
          return 'general';
      }
  }
  ```

### 3.1.2 إنشاء Settings Migration
- [x] **الأمر:**
  ```bash
  php artisan make:settings-migration CreateGeneralSettings
  ```
  - لو الأمر مش شغال، أنشئ الملف يدوياً في `database/settings/`.
- [x] **المحتوى:**
  ```php
  <?php

  use Spatie\LaravelSettings\Migrations\SettingsMigration;

  class CreateGeneralSettings extends SettingsMigration
  {
      public function up(): void
      {
          $this->migrator->add('general.enable_vat', true);
          $this->migrator->add('general.vat_percentage', 1500);          // 15%
          $this->migrator->add('general.currency', 'SAR');
          $this->migrator->add('general.delivery_fee_strategy', 'flat_fee');
          $this->migrator->add('general.base_delivery_fee', 1500);       // 15 ريال
          $this->migrator->add('general.extra_stop_fee', 500);           // 5 ريال
          $this->migrator->add('general.dispatch_mode', 'manual');
          $this->migrator->add('general.enable_batched_orders', false);
          $this->migrator->add('general.max_orders_per_driver', 3);
          $this->migrator->add('general.batch_routing_mode', 'same_zone');
          $this->migrator->add('general.batch_inactivity_timeout_minutes', 120);
          $this->migrator->add('general.batch_grace_period_minutes', 15);
          $this->migrator->add('general.driver_can_cancel_directly', false);
          $this->migrator->add('general.require_receipt_image', true);
          $this->migrator->add('general.available_payment_methods', ['cash', 'visa', 'transfer']);
      }
  }
  ```
- [x] **شغّل:**
  ```bash
  php artisan migrate
  ```
- [x] **✅ فحص:**
  ```bash
  php artisan tinker --execute="echo app(App\Settings\GeneralSettings::class)->currency;"
  ```
  لازم يطبع `SAR`.

---

## 3.2 — إنشاء صفحة الإعدادات في Filament

### 3.2.1 إنشاء `app/Filament/Pages/ManageGeneralSettings.php`
- [x] أنشئ الملف. الصفحة تستخدم `SettingsPage` الخاص بـ Filament + Spatie.
- [x] لازم تكون مقسمة لـ **3 Tabs** (المالية، المناديب، الطلبات).
- [x] أعمدة الفلوس (مثل `base_delivery_fee`) تُعرض مقسومة على 100 باستخدام `->formatStateUsing(fn($state) => $state / 100)` وتتحفظ مضروبة في 100 باستخدام `->dehydrateStateUsing(fn($state) => (int)($state * 100))`.
- [x] **⚠️ ملاحظة:** لازم تضيف حماية بصلاحيات `Shield`. المشرف (super_admin) بس هو اللي يقدر يفتح الصفحة دي.

---

## 3.3 — إنشاء محرك التسعير (Pricing Engine)

### 3.3.1 إنشاء `app/Services/Pricing/DeliveryFeeCalculator.php`
- [x] أنشئ مجلد `app/Services/Pricing/`
- [x] الملف (Interface):
  ```php
  <?php

  namespace App\Services\Pricing;

  use App\Models\Order;

  interface DeliveryFeeCalculator
  {
      /**
       * احسب رسوم التوصيل بالقرش.
       */
      public function calculate(Order $order): int;
  }
  ```

### 3.3.2 إنشاء `app/Services/Pricing/Strategies/FlatFeeStrategy.php`
- [x] أنشئ مجلد `app/Services/Pricing/Strategies/`
- [x] الملف:
  ```php
  <?php

  namespace App\Services\Pricing\Strategies;

  use App\Models\Order;
  use App\Services\Pricing\DeliveryFeeCalculator;
  use App\Settings\GeneralSettings;

  class FlatFeeStrategy implements DeliveryFeeCalculator
  {
      public function calculate(Order $order): int
      {
          return app(GeneralSettings::class)->base_delivery_fee;
      }
  }
  ```

### 3.3.3 إنشاء `app/Services/Pricing/Strategies/BasePlusStopStrategy.php`
- [x] الملف:
  ```php
  <?php

  namespace App\Services\Pricing\Strategies;

  use App\Models\Order;
  use App\Services\Pricing\DeliveryFeeCalculator;
  use App\Settings\GeneralSettings;

  class BasePlusStopStrategy implements DeliveryFeeCalculator
  {
      public function calculate(Order $order): int
      {
          $settings = app(GeneralSettings::class);
          $stopsCount = $order->errandStops()->count();

          return $settings->base_delivery_fee + ($stopsCount * $settings->extra_stop_fee);
      }
  }
  ```

### 3.3.4 إنشاء `app/Services/Pricing/PricingManager.php`
- [x] الملف:
  ```php
  <?php

  namespace App\Services\Pricing;

  use App\Models\Order;
  use App\Services\Pricing\Strategies\FlatFeeStrategy;
  use App\Services\Pricing\Strategies\BasePlusStopStrategy;
  use App\Settings\GeneralSettings;
  use App\Support\MoneyHelper;

  class PricingManager
  {
      /**
       * يأخذ لقطة (Snapshot) من الأسعار الحالية ويثبتها على الطلب.
       * ⚠️ يُستدعى مرة واحدة فقط عند الإنشاء (standard) أو عند اكتمال التسعير (errand).
       * ⚠️ لا يحفظ الطلب — أنت المسؤول عن saveQuietly() بعدها.
       */
      public function snapshot(Order $order): void
      {
          $settings = app(GeneralSettings::class);
          $strategy = $this->resolveStrategy($settings);

          // حساب رسوم التوصيل
          $deliveryFee = $strategy->calculate($order);

          // حساب الضريبة على رسوم التوصيل فقط
          $tax = 0;
          if ($settings->enable_vat) {
              $tax = MoneyHelper::calculateVat($deliveryFee, $settings->vat_percentage);
          }

          // تثبيت الـ Snapshot على الطلب
          $order->agreed_delivery_fee    = $deliveryFee;
          $order->applied_vat_percentage = $settings->vat_percentage;
          $order->applied_vat_enabled    = $settings->enable_vat;
          $order->pricing_strategy_used  = $settings->delivery_fee_strategy;
          $order->shipping               = $deliveryFee;
          $order->tax                    = $tax;
      }

      private function resolveStrategy(GeneralSettings $settings): DeliveryFeeCalculator
      {
          return match ($settings->delivery_fee_strategy) {
              'base_plus_stop' => new BasePlusStopStrategy(),
              default          => new FlatFeeStrategy(),
          };
      }
  }
  ```

- [x] **✅ فحص:** ابني Unit Test أو اختبر في tinker — أنشئ Order وهمي واستدعي `PricingManager->snapshot($order)` وشوف إن الحقول اتملت صح.

---

## 3.4 — تحديث Models بالـ Enums والأعمدة الجديدة

### 3.4.1 تحديث `app/Models/Order.php`
- [x] **الملف:** [Order.php](file:///D:/Important%20Projects/Apex_Logistics/Dashboard/app/Models/Order.php)
- [x] **المطلوب:**
  1. إضافة `use` للـ Enums في أعلى الملف.
  2. إضافة الأعمدة الجديدة لـ `$fillable`.
  3. تحديث `$casts` بالـ Enums.
  4. إضافة علاقة `batch()`.
- [x] **الـ `$fillable` الجديد** (ضيف الأعمدة الجديدة فقط، **متشيلش** القديمة):
  ```php
  // أضف هذه الحقول لمصفوفة $fillable الموجودة:
  'batch_id',
  'agreed_delivery_fee',
  'applied_vat_percentage',
  'applied_vat_enabled',
  'pricing_strategy_used',
  'version',
  ```
- [x] **الـ `$casts` الجديد:**
  ```php
  protected $casts = [
      'order_type'                => \App\Enums\OrderType::class,
      'status'                    => \App\Enums\OrderStatus::class,
      'financial_status'          => \App\Enums\FinancialStatus::class,
      'payment_method'            => \App\Enums\PaymentMethod::class,
      'cancellation_requested_at' => 'datetime',
      'applied_vat_enabled'       => 'boolean',
  ];
  ```
- [x] **إضافة علاقة `batch()`:**
  ```php
  public function batch(): BelongsTo
  {
      return $this->belongsTo(DeliveryBatch::class, 'batch_id');
  }
  ```
  وضيف `use App\Models\DeliveryBatch;` في أعلى الملف.
- [x] **⚠️ تحذير:** بعد إضافة الـ Enum Casts، كل مقارنة في الكود لازم تستخدم الـ Enum مش String. مثلاً في `OrderObserver.php` السطر 22 بيقول `$order->status === 'cancelled'` — ده هيكسر! لازم يبقى `$order->status === OrderStatus::Cancelled` أو `$order->status->value === 'cancelled'`. **بس متعدلش الـ Observers دلوقتي**. هنعدلهم في تيكت تانية.

### 3.4.2 تحديث `app/Models/ErrandStop.php`
- [x] **الملف:** [ErrandStop.php](file:///D:/Important%20Projects/Apex_Logistics/Dashboard/app/Models/ErrandStop.php)
- [x] **أضف `$casts`:**
  ```php
  protected $casts = [
      'status' => \App\Enums\StopStatus::class,
  ];
  ```

---

## ✅ فحص نهائي للتيكت الثالثة
- [x] `app(GeneralSettings::class)->currency` بيرجع `SAR`.
- [x] صفحة الإعدادات في الداشبورد شغالة وبتحفظ وبتقرا.
- [x] `PricingManager->snapshot($order)` بيملا الـ Snapshot fields.
- [x] الـ Enums مربوطة بالـ Models بدون أخطاء.
- [x] `php artisan serve` شغال.

## 📝 توثيق التيكت الثالثة (إلزامي)
- [x] **حدّث `PHASE5_CHANGELOG.md`** بكل التفاصيل التالية:
  - [x] سجّل ملف `GeneralSettings.php` بالمحتوى الكامل.
  - [x] سجّل ملف Settings Migration بالمحتوى الكامل.
  - [x] سجّل ملف `ManageGeneralSettings.php` (صفحة Filament) بالمحتوى الكامل.
  - [x] سجّل ملفات محرك التسعير الأربعة (Interface + 2 Strategies + Manager) بالمحتوى الكامل.
  - [x] سجّل التعديلات على `Order.php` بصيغة diff (الأعمدة الجديدة في `$fillable` + `$casts` + علاقة `batch()`).
  - [x] سجّل التعديلات على `ErrandStop.php` بصيغة diff (إضافة `$casts`).
  - [x] سجّل كل أوامر الفحص ونتائجها.

---
---

# 🎟️ التيكت الرابعة: الرحلات المجمعة والتوزيع (Batches & Dispatching)

> **الهدف:** بناء نظام إدارة الرحلات المجمعة، توزيع الطلبات بقيود ذكية، وحل مشكلة "المندوب النائم".
> **الاعتماديات:** التيكتات 1–3.
> **التقدير الزمني:** 3 ساعات.

---

## 4.1 — إنشاء `app/Models/DeliveryBatch.php`
- [x] الملف الجديد:
  ```php
  <?php

  namespace App\Models;

  use App\Enums\BatchStatus;
  use Illuminate\Database\Eloquent\Model;
  use Illuminate\Database\Eloquent\Relations\BelongsTo;
  use Illuminate\Database\Eloquent\Relations\HasMany;
  use Spatie\Activitylog\Traits\LogsActivity;
  use Spatie\Activitylog\LogOptions;

  class DeliveryBatch extends Model
  {
      use LogsActivity;

      protected $fillable = [
          'driver_id', 'status', 'started_at', 'completed_at',
          'last_activity_at', 'warned_at',
      ];

      protected $casts = [
          'status'           => BatchStatus::class,
          'started_at'       => 'datetime',
          'completed_at'     => 'datetime',
          'last_activity_at' => 'datetime',
          'warned_at'        => 'datetime',
      ];

      public function getActivitylogOptions(): LogOptions
      {
          return LogOptions::defaults()->logFillable()->logOnlyDirty();
      }

      public function driver(): BelongsTo
      {
          return $this->belongsTo(Driver::class);
      }

      public function orders(): HasMany
      {
          return $this->hasMany(Order::class, 'batch_id');
      }

      // === Scopes ===

      /** الرحلات النشطة */
      public function scopeActive($query)
      {
          return $query->where('status', BatchStatus::Active);
      }

      /** الرحلات اللي المندوب اختفى فيها (تجاوز مهلة النشاط ولم يُحذَّر بعد) */
      public function scopeStale($query, int $timeoutMinutes)
      {
          return $query->active()
              ->whereNull('warned_at')
              ->where('last_activity_at', '<=', now()->subMinutes($timeoutMinutes));
      }

      /** الرحلات اللي اتحذر صاحبها وانتهت فترة السماح */
      public function scopeExpiredGrace($query, int $graceMinutes)
      {
          return $query->active()
              ->whereNotNull('warned_at')
              ->where('warned_at', '<=', now()->subMinutes($graceMinutes))
              ->where(function ($q) {
                  // ولم يعد النشاط بعد التحذير
                  $q->whereColumn('last_activity_at', '<', 'warned_at')
                    ->orWhereNull('last_activity_at');
              });
      }
  }
  ```

---

## 4.2 — إنشاء `app/Services/BatchDispatcher.php`
- [x] الملف الجديد — هذا الـ Service هو اللي بيتحقق من كل القيود قبل تعيين طلب لمندوب:
  ```php
  <?php

  namespace App\Services;

  use App\Enums\BatchStatus;
  use App\Models\DeliveryBatch;
  use App\Models\Driver;
  use App\Models\Order;
  use App\Settings\GeneralSettings;
  use Illuminate\Validation\ValidationException;

  class BatchDispatcher
  {
      /**
       * حاول تعيين طلب لمندوب مع كل القيود.
       * لو نجح → يربط الطلب بالـ Batch ويرجع true.
       * لو فشل → يرمي ValidationException برسالة واضحة.
       */
      public function assignOrderToDriver(Order $order, Driver $driver): DeliveryBatch
      {
          $settings = app(GeneralSettings::class);

          // 1. جلب الـ Batch النشطة للمندوب (لو موجودة)
          $activeBatch = DeliveryBatch::where('driver_id', $driver->id)
              ->active()
              ->first();

          // 2. لو المندوب عنده batch نشطة وخاصية التعدد مقفولة → ارفض
          if ($activeBatch && !$settings->enable_batched_orders) {
              throw ValidationException::withMessages([
                  'driver_id' => 'المندوب لديه طلب حالي ولا يمكن تعيين طلب جديد له. خاصية تعدد الطلبات مغلقة.',
              ]);
          }

          // 3. لو المندوب وصل الحد الأقصى → ارفض
          if ($activeBatch) {
              $currentCount = $activeBatch->orders()->count();
              if ($currentCount >= $settings->max_orders_per_driver) {
                  throw ValidationException::withMessages([
                      'driver_id' => "المندوب وصل للحد الأقصى ({$settings->max_orders_per_driver} طلبات).",
                  ]);
              }
          }

          // 4. فحص المنطقة (same_zone)
          if ($settings->batch_routing_mode === 'same_zone') {
              $customer = $order->customer;
              if ($customer) {
                  $driverZoneIds = $driver->zones()->pluck('zones.id')->toArray();
                  // ⚠️ التحقق من المنطقة سيتم تطويره في المرحلة السادسة (جيو-فينسينج حقيقي)
                  // حالياً: نتحقق إن المندوب عنده مناطق مربوطة
                  if (empty($driverZoneIds)) {
                      throw ValidationException::withMessages([
                          'driver_id' => 'المندوب ليس لديه مناطق عمل محددة.',
                      ]);
                  }
              }
          }

          // 5. كل الشروط اتحققت — أنشئ Batch جديدة أو أضف للموجودة
          if (!$activeBatch) {
              $activeBatch = DeliveryBatch::create([
                  'driver_id'        => $driver->id,
                  'status'           => BatchStatus::Active,
                  'started_at'       => now(),
                  'last_activity_at' => now(),
              ]);
          }

          // ربط الطلب
          $order->batch_id  = $activeBatch->id;
          $order->driver_id = $driver->id;

          // تحديث وقت النشاط
          $activeBatch->update(['last_activity_at' => now()]);

          return $activeBatch;
      }
  }
  ```

---

## 4.3 — إنشاء `app/Observers/BatchObserver.php` (Auto-Completion)
- [x] الملف الجديد — يراقب الطلبات ويُغلق الـ Batch تلقائياً لما كلها تخلص:
  ```php
  <?php

  namespace App\Observers;

  use App\Enums\BatchStatus;
  use App\Enums\OrderStatus;
  use App\Models\Order;

  class BatchObserver
  {
      public function updated(Order $order): void
      {
          // لو الطلب مش في Batch → مش مهتمين
          if (!$order->batch_id) {
              return;
          }

          // لو الحالة لم تتغير → مش مهتمين
          if (!$order->isDirty('status')) {
              return;
          }

          // الحالات النهائية
          $terminalStatuses = [
              OrderStatus::Delivered,
              OrderStatus::PartiallyDelivered,
              OrderStatus::Cancelled,
          ];

          // لو الحالة الجديدة مش نهائية → مش مهتمين
          if (!in_array($order->status, $terminalStatuses)) {
              return;
          }

          // تحقق: هل كل الطلبات في الـ Batch وصلت لحالة نهائية؟
          $batch = $order->batch;
          if (!$batch) {
              return;
          }

          $terminalValues = array_map(fn($s) => $s->value, $terminalStatuses);

          $allDone = $batch->orders()
              ->whereNotIn('status', $terminalValues)
              ->doesntExist();

          if ($allDone) {
              $batch->update([
                  'status'       => BatchStatus::Completed,
                  'completed_at' => now(),
              ]);
          }
      }
  }
  ```
- [x] **⚠️ هام:** لازم تسجّل هذا الـ Observer في `AppServiceProvider`. **بس متعملش ده دلوقتي** — هنعمل كل التسجيلات مرة واحدة في التيكت السابعة.

---

## 4.4 — إنشاء `app/Filament/Resources/DeliveryBatchResource.php`
- [x] أنشئ Resource كامل لعرض الرحلات المجمعة:
  ```bash
  php artisan make:filament-resource DeliveryBatch --view
  ```
- [x] عدّل الجدول ليعرض: ID، اسم المندوب، الحالة (Badge)، عدد الطلبات، وقت البداية، آخر نشاط.
- [x] فلاتر: حسب الحالة والمندوب.

---

## 4.5 — إنشاء `app/Console/Commands/ReleaseInactiveBatches.php`
- [x] **الأمر:**
  ```bash
  php artisan make:command ReleaseInactiveBatches
  ```
- [x] **الـ Signature:** `batches:release-inactive`
- [x] **المنطق (مرحلتين):**
  1. **المرحلة الأولى (التحذير):** ابحث عن Batches بحالة `active` اللي `last_activity_at` تجاوز الـ timeout ولم يُحذَّر بعد (`warned_at = null`). سجّل `warned_at = now()`. أرسل Filament Notification للمشرفين.
  2. **المرحلة الثانية (السحب):** ابحث عن Batches اللي `warned_at` تجاوز فترة السماح ولم يتم تحديث `last_activity_at` بعد التحذير. حوّل الطلبات غير المسلمة لـ `pending`. زوّد `version` بـ 1. حوّل الـ Batch لـ `abandoned`. سجّل Activity Log.
- [x] **⚠️ استخدم `saveQuietly()` عند تغيير حالة الطلبات عشان الـ Observers متتنادیش وتعمل Side Effects غير متوقعة.**
- [x] **تسجيل في `routes/console.php`:**
  ```php
  Schedule::command('batches:release-inactive')->everyFiveMinutes();
  ```

---

## ✅ فحص نهائي للتيكت الرابعة
- [x] `DeliveryBatch` Model شغال مع الـ Scopes.
- [x] `BatchDispatcher` بيرفض تعيين طلب لمندوب وصل الحد الأقصى.
- [x] `BatchObserver` بيغلق الـ Batch تلقائياً لما كل الطلبات تخلص.
- [x] `DeliveryBatchResource` شغال في الداشبورد.
- [x] `ReleaseInactiveBatches` مُسجّل في الـ Scheduler.

## 📝 توثيق التيكت الرابعة (إلزامي)
- [x] **حدّث `PHASE5_CHANGELOG.md`** بكل التفاصيل التالية:
  - [x] سجّل ملف `DeliveryBatch.php` بالمحتوى الكامل.
  - [x] سجّل ملف `BatchDispatcher.php` بالمحتوى الكامل.
  - [x] سجّل ملف `BatchObserver.php` بالمحتوى الكامل.
  - [x] سجّل ملف `DeliveryBatchResource.php` بالمحتوى الكامل.
  - [x] سجّل ملف `ReleaseInactiveBatches.php` بالمحتوى الكامل.
  - [x] سجّل كل أوامر الفحص ونتائجها.

---
---

# 🎟️ التيكت الخامسة: القيود المالية الصارمة (Financial Ledgers)

> **الهدف:** بناء الدفاتر المالية بنظام Append-Only مع ضمان عدم تكرار القيود.
> **الاعتماديات:** التيكتات 1–3 (الجدول أُنشئ في التيكت 2).
> **التقدير الزمني:** ساعتين ونص.

---

## 5.1 — إنشاء `app/Models/FinancialLedger.php`
- [x] الملف الجديد:
  ```php
  <?php

  namespace App\Models;

  use App\Enums\LedgerType;
  use Illuminate\Database\Eloquent\Model;
  use Illuminate\Database\Eloquent\Relations\BelongsTo;
  use Spatie\Activitylog\Traits\LogsActivity;
  use Spatie\Activitylog\LogOptions;

  class FinancialLedger extends Model
  {
      use LogsActivity;

      protected $fillable = [
          'order_id', 'driver_id', 'type', 'amount', 'category',
          'description', 'reference_version', 'idempotency_key', 'created_by',
      ];

      protected $casts = [
          'type' => LedgerType::class,
      ];

      public function getActivitylogOptions(): LogOptions
      {
          return LogOptions::defaults()->logFillable()->logOnlyDirty();
      }

      // === العلاقات ===
      public function order(): BelongsTo { return $this->belongsTo(Order::class); }
      public function driver(): BelongsTo { return $this->belongsTo(Driver::class); }
      public function createdBy(): BelongsTo { return $this->belongsTo(User::class, 'created_by'); }

      // === Scopes ===
      public function scopeForDriver($query, int $driverId) { return $query->where('driver_id', $driverId); }
      public function scopeDebits($query) { return $query->where('type', LedgerType::Debit); }
      public function scopeCredits($query) { return $query->where('type', LedgerType::Credit); }
  }
  ```
- [x] **⚠️ ممنوع تماماً:** إضافة `update()` أو `delete()` أو أي دالة بتعدّل سجل موجود. **Append-Only يعني Insert بس.**

---

## 5.2 — إنشاء `app/Observers/FinancialObserver.php`
- [x] الملف الجديد:
  ```php
  <?php

  namespace App\Observers;

  use App\Enums\FinancialStatus;
  use App\Enums\LedgerType;
  use App\Enums\OrderStatus;
  use App\Models\FinancialLedger;
  use App\Models\Order;
  use Illuminate\Support\Facades\DB;

  class FinancialObserver
  {
      public function updated(Order $order): void
      {
          if (!$order->isDirty('status')) {
              return;
          }

          $newStatus = $order->status;

          if (in_array($newStatus, [OrderStatus::Delivered, OrderStatus::PartiallyDelivered])) {
              $this->handleDelivered($order);
          }

          if ($newStatus === OrderStatus::Cancelled && $order->getOriginal('financial_status') === FinancialStatus::Settled->value) {
              $this->handleCancelledAfterSettlement($order);
          }

          if ($newStatus === OrderStatus::Disputed) {
              $this->handleDisputed($order);
          }
      }

      private function handleDelivered(Order $order): void
      {
          // ⚡ مفتاح منع التكرار (Idempotency)
          $key = "delivery_{$order->id}_v{$order->version}";

          if (FinancialLedger::where('idempotency_key', $key)->exists()) {
              return; // القيد موجود فعلاً — لا تكرره
          }

          DB::transaction(function () use ($order, $key) {
              // Debit على المندوب (المبلغ اللي حصّله من العميل)
              FinancialLedger::create([
                  'order_id'          => $order->id,
                  'driver_id'         => $order->driver_id,
                  'type'              => LedgerType::Debit,
                  'amount'            => $order->total_amount,
                  'category'          => 'receipt_collection',
                  'description'       => "تحصيل مبلغ الطلب #{$order->id}",
                  'reference_version' => $order->version,
                  'idempotency_key'   => $key,
              ]);

              // Credit للمتجر (حصة التوصيل + الضريبة)
              FinancialLedger::create([
                  'order_id'          => $order->id,
                  'driver_id'         => $order->driver_id,
                  'type'              => LedgerType::Credit,
                  'amount'            => $order->shipping + $order->tax,
                  'category'          => 'delivery_fee',
                  'description'       => "حصة التوصيل من الطلب #{$order->id}",
                  'reference_version' => $order->version,
                  'idempotency_key'   => "{$key}_store",
              ]);

              $order->financial_status = FinancialStatus::Settled;
              $order->saveQuietly();
          });
      }

      private function handleCancelledAfterSettlement(Order $order): void
      {
          $key = "reversal_{$order->id}_v{$order->version}";

          if (FinancialLedger::where('idempotency_key', $key)->exists()) {
              return;
          }

          DB::transaction(function () use ($order, $key) {
              // عكس القيود السابقة
              FinancialLedger::create([
                  'order_id'          => $order->id,
                  'driver_id'         => $order->driver_id,
                  'type'              => LedgerType::Credit,  // عكس الـ Debit
                  'amount'            => $order->total_amount,
                  'category'          => 'reversal',
                  'description'       => "عكس تحصيل الطلب #{$order->id} (إلغاء)",
                  'reference_version' => $order->version,
                  'idempotency_key'   => $key,
              ]);

              FinancialLedger::create([
                  'order_id'          => $order->id,
                  'driver_id'         => $order->driver_id,
                  'type'              => LedgerType::Debit,   // عكس الـ Credit
                  'amount'            => $order->shipping + $order->tax,
                  'category'          => 'reversal',
                  'description'       => "عكس حصة التوصيل للطلب #{$order->id} (إلغاء)",
                  'reference_version' => $order->version,
                  'idempotency_key'   => "{$key}_store",
              ]);

              $order->financial_status = FinancialStatus::Refunded;
              $order->saveQuietly();
          });
      }

      private function handleDisputed(Order $order): void
      {
          $order->financial_status = FinancialStatus::Frozen;
          $order->saveQuietly();
      }
  }
  ```

---

## 5.3 — إنشاء `app/Console/Commands/AggregateDriverBalances.php`
- [x] **الأمر:**
  ```bash
  php artisan make:command AggregateDriverBalances
  ```
- [x] **الـ Signature:** `drivers:aggregate-balances`
- [x] **المنطق:** يحسب رصيد كل مندوب (`SUM debits - SUM credits`) ويخزنه في Redis key: `driver:balance:{id}`. مُغلّف بـ `try/catch` — لو Redis واقع، يتخطى بدون كسر.
- [x] **تسجيل:** أضف في `routes/console.php`:
  ```php
  Schedule::command('drivers:aggregate-balances')->everyFiveMinutes();
  ```

---

## ✅ فحص نهائي للتيكت الخامسة
- [x] `FinancialLedger` Model بدون أي `update` أو `delete`.
- [x] `FinancialObserver` بينشئ قيود عند `delivered` ومبيكررش لو اتنادى مرتين.
- [x] `AggregateDriverBalances` مسجل في الـ Scheduler.

## 📝 توثيق التيكت الخامسة (إلزامي)
- [x] **حدّث `PHASE5_CHANGELOG.md`** بكل التفاصيل التالية:
  - [x] سجّل ملف `FinancialLedger.php` بالمحتوى الكامل.
  - [x] سجّل ملف `FinancialObserver.php` بالمحتوى الكامل (مع شرح الـ Idempotency Keys).
  - [x] سجّل ملف `AggregateDriverBalances.php` بالمحتوى الكامل.
  - [x] سجّل كل أوامر الفحص ونتائجها (خصوصاً اختبار منع التكرار).

---
---

# 🎟️ التيكت السادسة: واجهات الموبايل والحماية (Driver API & Security)

> **الهدف:** بناء كل الـ API Endpoints للمندوب، مع الحماية الكاملة (Validation, Rate Limiting, Optimistic Locking, Magic Bytes).
> **الاعتماديات:** التيكتات 1–5.
> **التقدير الزمني:** 4 ساعات.

---

## 6.1 — إنشاء Form Requests
- [x] أنشئ مجلد `app/Http/Requests/Api/Driver/`
- [x] أنشئ 5 ملفات FormRequest. كل ملف لازم يحتوي على `authorize(): bool` (يرجع `true`) و `rules(): array`. ارجع لجدول الـ Validation Rules في الخطة التنفيذية.

## 6.2 — إنشاء API Resources
- [x] أنشئ مجلد `app/Http/Resources/`
- [x] أنشئ `OrderApiResource.php`, `StopApiResource.php`, `BatchApiResource.php`.
- [x] **⚠️ ممنوع عرض:** `customer_id`, `driver_id`, `cancellation_approved_by`, أي FK داخلي، أو أي بيانات مالية داخلية غير المبلغ الإجمالي.
- [x] الفلوس تتعرض بالريال (مقسومة على 100) باستخدام `MoneyHelper::fromCents()`.

## 6.3 — إنشاء الـ Controllers
- [x] أنشئ مجلد `app/Http/Controllers/Api/Driver/`
- [x] أنشئ `OrderController.php`, `StopController.php`, `LocationController.php`.
- [x] **كل Endpoint لازم:**
  1. يستخدم FormRequest للـ Validation.
  2. يتحقق من ملكية الطلب (`$order->driver_id === auth()->user()->driver->id`).
  3. يفحص `version` ويرمي `409 Conflict` لو اختلف.
  4. يمر عبر `OrderStateMachine::transition()` لتغيير الحالة.
  5. يُحدّث `batch.last_activity_at`.
  6. يُرجع `ApiResource`.

## 6.4 — إنشاء `app/Http/Controllers/SecureFileController.php`
- [x] Route: `GET /admin/secure-file/{path}` في `routes/web.php`.
- [x] محمي بـ `auth` middleware.
- [x] يقرأ الملف من `Storage::disk('private')`.
- [x] يرجع `response()->file()`.

## 6.5 — تحديث `routes/api.php`
- [x] **⚠️ لا تحذف المسارات الموجودة** (heartbeat + user). أضف المسارات الجديدة بعدها.

## 6.6 — تسجيل Rate Limiters في `AppServiceProvider`
- [x] **الملف:** [AppServiceProvider.php](file:///D:/Important%20Projects/Apex_Logistics/Dashboard/app/Providers/AppServiceProvider.php)
- [x] أضف في دالة `boot()`:
  ```php
  \Illuminate\Support\Facades\RateLimiter::for('driver-api', function ($request) {
      return \Illuminate\Cache\RateLimiting\Limit::perMinute(120)
          ->by($request->user()?->id ?: $request->ip());
  });

  \Illuminate\Support\Facades\RateLimiter::for('driver-location', function ($request) {
      return \Illuminate\Cache\RateLimiting\Limit::perMinute(60)
          ->by($request->user()?->id ?: $request->ip());
  });
  ```

---

## ✅ فحص نهائي للتيكت السادسة
- [x] كل Endpoint بيرجع JSON سليم.
- [x] الـ Rate Limiter بيمنع أكتر من 60 طلب/الدقيقة على `/location`.
- [x] رفع صورة بامتداد `.php` أو `.exe` بيترفض (Magic Bytes).
- [x] Optimistic Locking بيرمي 409 لما الـ version يكون قديم.
- [x] `SecureFileController` بيعرض الصور في المتصفح.

## 📝 توثيق التيكت السادسة (إلزامي)
- [x] **حدّث `PHASE5_CHANGELOG.md`** بكل التفاصيل التالية:
  - [x] سجّل كل ملفات FormRequest الخمسة بالمحتوى الكامل.
  - [x] سجّل كل ملفات API Resources الثلاثة بالمحتوى الكامل.
  - [x] سجّل كل ملفات Controllers الثلاثة بالمحتوى الكامل.
  - [x] سجّل ملف `SecureFileController.php` بالمحتوى الكامل.
  - [x] سجّل التعديلات على `routes/api.php` بصيغة diff.
  - [x] سجّل التعديلات على `routes/web.php` بصيغة diff (إضافة secure-file route).
  - [x] سجّل التعديلات على `AppServiceProvider.php` بصيغة diff (Rate Limiters).
  - [x] سجّل كل أوامر الفحص ونتائجها (خصوصاً اختبارات الـ API بـ curl/Postman).

---
---

# 🎟️ التيكت السابعة: أزرار الطوارئ وربط كل شيء (Custom Actions & Wiring)

> **الهدف:** بناء أزرار الإدارة في الداشبورد، تحديث الـ Observers القديمة، وتسجيل كل شيء.
> **الاعتماديات:** كل التيكتات السابقة.
> **التقدير الزمني:** 3 ساعات.

---

## 7.1 — أزرار Filament في EditOrder

### 7.1.1 تعديل [EditOrder.php](file:///D:/Important%20Projects/Apex_Logistics/Dashboard/app/Filament/Resources/Orders/Pages/EditOrder.php)
- [x] **أضف** الأزرار كـ `HeaderActions`:

**زر "موافقة على الإلغاء":**
- شرط الظهور: `$this->record->status === OrderStatus::CancellationRequested`
- عند الضغط: `OrderStateMachine::transition($order, OrderStatus::Cancelled)` → `cancellation_approved_by = auth()->id()` → `version++` → `save()`.

**زر "إعادة تعيين ومعاقبة":**
- شرط الظهور: `$this->record->status === OrderStatus::CancellationRequested`
- Modal: حقل سبب + Checkbox "مخالفة".
- عند الضغط: `driver_id = null`, `batch_id = null` → `OrderStateMachine::transition($order, OrderStatus::Pending)` → `version++` → لو مخالفة: إنشاء `FinancialLedger` بـ type `debit` و category `penalty`.

**زر "حل النزاع":**
- شرط الظهور: `$this->record->status === OrderStatus::Disputed`
- Modal: خيارين (رد مبلغ / تأكيد تسليم).
- عند الضغط: يطبّق القرار عبر State Machine + Financial Observer.

---

## 7.2 — تحديث الـ Observers القديمة لتتوافق مع الـ Enums

### 7.2.1 تحديث [OrderObserver.php](file:///D:/Important%20Projects/Apex_Logistics/Dashboard/app/Observers/OrderObserver.php)
- [x] **السطر 22:** غيّر `$order->status === 'cancelled'` إلى `$order->status === \App\Enums\OrderStatus::Cancelled`
- [x] **السطر 22:** غيّر `$order->order_type === 'standard'` إلى `$order->order_type === \App\Enums\OrderType::Standard`

### 7.2.2 تحديث [ErrandStopObserver.php](file:///D:/Important%20Projects/Apex_Logistics/Dashboard/app/Observers/ErrandStopObserver.php)
- [x] **السطر 15:** غيّر `$order->order_type !== 'errand'` إلى `$order->order_type !== \App\Enums\OrderType::Errand`
- [x] **السطور 22–33:** استبدل الحسابات العشرية بـ `MoneyHelper` وقراءة الـ Snapshot:
  ```php
  // بدل:  config('settings.vat_percentage', 15)
  // استخدم: $order->applied_vat_percentage  (من الـ Snapshot)

  // بدل:  $deliveryFee * ($vatPercentage / 100)
  // استخدم: MoneyHelper::calculateVat($order->shipping, $order->applied_vat_percentage)
  ```
- [x] **السطر 37:** غيّر `in_array($stop->status, ['purchased', 'not_found'])` إلى:
  ```php
  in_array($stop->status, [\App\Enums\StopStatus::Purchased, \App\Enums\StopStatus::NotFound])
  ```
- [x] **السطر 38:** غيّر `$order->status === 'pending_pricing'` إلى `$order->status === \App\Enums\OrderStatus::PendingPricing`
- [x] **السطر 39:** غيّر `$order->status = 'priced'` إلى `$order->status = \App\Enums\OrderStatus::Priced`
- [x] **أضف** بعد تغيير الحالة لـ `priced`: استدعاء `PricingManager->snapshot($order)` لتثبيت الأسعار.

---

## 7.3 — تحديث [OrderForm.php](file:///D:/Important%20Projects/Apex_Logistics/Dashboard/app/Filament/Resources/Orders/Schemas/OrderForm.php)
- [x] **السطور 31–36 (payment_method options):** استبدلها بقراءة ديناميكية من الإعدادات:
  ```php
  ->options(function () {
      $methods = app(\App\Settings\GeneralSettings::class)->available_payment_methods;
      return collect($methods)->mapWithKeys(fn ($m) => [$m => ucfirst($m)])->toArray();
  })
  ```
- [x] **أسعار المنتجات (السطر 72):** بعد ما الأعمدة بقت Integer، الـ `unit_price` هيبقى بالقرش. لازم الـ Form يعرضها مقسومة على 100 للمستخدم.

---

## 7.4 — تحديث [CreateOrder.php](file:///D:/Important%20Projects/Apex_Logistics/Dashboard/app/Filament/Resources/Orders/Pages/CreateOrder.php)
- [x] بعد ما الطلب يتكريت (سطر 41)، نادي `PricingManager->snapshot($order)`:
  ```php
  $order = static::getModel()::create($data);

  // 🆕 تثبيت الأسعار (Snapshot)
  app(\App\Services\Pricing\PricingManager::class)->snapshot($order);
  $order->saveQuietly();
  ```
- [x] **⚠️ ده للطلبات العادية فقط.** الطلبات الحرة (`errand`) الـ Snapshot بيتعمل في `ErrandStopObserver` لما الطلب يبقى `priced`.

---

## 7.5 — تسجيل كل الـ Observers الجديدة في `AppServiceProvider`

### 7.5.1 تعديل [AppServiceProvider.php](file:///D:/Important%20Projects/Apex_Logistics/Dashboard/app/Providers/AppServiceProvider.php)
- [x] **أضف** في دالة `boot()` بعد السطر 29:
  ```php
  \App\Models\Order::observe(\App\Observers\FinancialObserver::class);
  \App\Models\Order::observe(\App\Observers\BatchObserver::class);
  ```
- [x] **⚠️ الترتيب مهم:** `OrderObserver` أولاً (استرداد المخزون)، ثم `FinancialObserver` (القيود المالية)، ثم `BatchObserver` (إغلاق الرحلة). الترتيب الحالي في السطر 28 (`OrderObserver`) يبقى كما هو.

---

## 7.6 — تسجيل الـ Commands الجديدة في Scheduler

### 7.6.1 تعديل [routes/console.php](file:///D:/Important%20Projects/Apex_Logistics/Dashboard/routes/console.php)
- [x] **أضف** بعد السطر 11:
  ```php
  Schedule::command('batches:release-inactive')->everyFiveMinutes();
  Schedule::command('drivers:aggregate-balances')->everyFiveMinutes();
  ```

---

## ✅ الفحص النهائي الشامل (Final Acceptance Test)

- [x] `php artisan serve` شغال بدون أي أخطاء.
- [x] إنشاء طلب Standard من الداشبورد → التأكد من حفظ الـ Snapshot.
- [x] تغيير أسعار الإعدادات → التأكد من أن الطلب القديم لم يتأثر.
- [x] تعيين طلب لمندوب وصل الحد → التأكد من رفض التعيين.
- [x] استدعاء API بـ version قديم → التأكد من `409 Conflict`.
- [x] تسليم طلب → التأكد من وجود قيود مالية.
- [x] إعادة trigger الـ Observer → التأكد من عدم تكرار القيود (idempotency).
- [x] تسليم آخر طلب في Batch → التأكد من تحول الـ Batch لـ `completed`.
- [x] رفع صورة فاتورة → التأكد من عرضها في الداشبورد عبر `SecureFileController`.
- [x] الضغط على "موافقة على الإلغاء" → التأكد من عودة المخزون + تسجيل المشرف.
- [x] الضغط على "إعادة تعيين ومعاقبة" → التأكد من فصل الطلب + تسجيل الغرامة.

## 📝 توثيق التيكت السابعة (إلزامي)
- [x] **حدّث `PHASE5_CHANGELOG.md`** بكل التفاصيل التالية:
  - [x] سجّل التعديلات على `EditOrder.php` بصيغة diff (الأزرار الثلاثة: Approve, Reassign, Resolve).
  - [x] سجّل التعديلات على `OrderObserver.php` بصيغة diff (تحويل Strings لـ Enums).
  - [x] سجّل التعديلات على `ErrandStopObserver.php` بصيغة diff (MoneyHelper + Snapshot + Enums + PricingManager).
  - [x] سجّل التعديلات على `OrderForm.php` بصيغة diff (قراءة ديناميكية من Settings).
  - [x] سجّل التعديلات على `CreateOrder.php` بصيغة diff (استدعاء PricingManager).
  - [x] سجّل التعديلات على `AppServiceProvider.php` بصيغة diff (تسجيل Observers الجديدة).
  - [x] سجّل التعديلات على `routes/console.php` بصيغة diff (الـ Commands الجديدة).
  - [x] سجّل كل نتائج الفحص النهائي الشامل (كل سطر أعلاه ونتيجته).

---

## 📊 إحصائيات ملف التوثيق النهائي
- [x] **تأكد أن `PHASE5_CHANGELOG.md` يحتوي على 7 أقسام** (قسم لكل تيكت).
- [x] **تأكد أن كل قسم فيه:** ملفات جديدة + ملفات معدّلة + أوامر مُنفّذة + نتائج فحص.
- [x] **تأكد أن الملف قابل للقراءة** (مُنسّق بـ Markdown صحيح مع syntax highlighting).
