Pharmacy Module

نظام إدارة الصيدلية والمبيعات - Event-Driven Integration مع Inventory & DrugCatalog

Ready Enhanced Event-Driven

نظرة عامة (Overview)

Pharmacy Module هو نظام متكامل لإدارة الصيدلية والمبيعات الدوائية. يوفر تكامل محكم مع Inventory Module لإدارة المخزون و DrugCatalog للأدوية المسجلة.

الميزات الرئيسية

  • Transaction Support: جميع العمليات محمية بـ DB transactions
  • Event-Driven: تكامل عبر Events مع Inventory
  • Stock Integration: Reserve → Deduct → Release pattern
  • Medication Dispensing: صرف الأدوية مع تتبع كامل
  • Returns Management: إدارة المرتجعات
  • Comprehensive Logging: تسجيل مفصل لجميع العمليات
  • Validation Layer: فحص شامل قبل أي عملية
  • Custom Exceptions: معالجة أخطاء مخصصة

الكيانات (Entities)

Entity PharmacySale

Path: Modules/Pharmacy/Entities/PharmacySale.php

الوصف: فاتورة مبيعات الصيدلية

الخصائص الرئيسية:

- id
- location_id (FK: business_locations)
- sale_no (auto-generated: PH-YYYYMM-XXX)
- patient_id (FK: patients)
- encounter_id (FK: care_encounters)
- prescription_id (FK: prescriptions)
- status_id (FK: statuses)
- sale_date
- total_amount
- tax_amount
- discount_amount
- net_amount
- payment_status
- notes

العلاقات (Relationships):

  • location() → BusinessLocation
  • patient() → Patient
  • encounter() → CareEncounter
  • status() → Status
  • lines() → PharmacySaleLine (hasMany)

Entity PharmacySaleLine

Path: Modules/Pharmacy/Entities/PharmacySaleLine.php

الوصف: سطر في فاتورة الصيدلية - الربط الأساسي مع Inventory

الخصائص الرئيسية:

- id
- pharmacy_sale_id (FK: pharmacy_sales)
- drug_id (FK: drugs) - من DrugCatalog
- inventory_product_id (FK: inventory_products)
- inventory_batch_id (FK: inventory_batches)
- unit_id (FK: inventory_units)
- location_id
- quantity
- cost (from inventory)
- price (selling price)
- discount
- tax
- total
- inventory_movement_uuid - تتبع حركة المخزون
- dispensed_at
- dispensed_by

المميزات:

  • Dual Reference: ربط مع Drug (catalog) و InventoryProduct
  • Movement Tracking: تتبع UUID حركة المخزون
  • Audit Trail: تسجيل من صرف ومتى

Entity PharmacyReturn

Path: Modules/Pharmacy/Entities/PharmacyReturn.php

الوصف: مرتجعات الصيدلية

الخصائص الرئيسية:

- id
- location_id
- return_no (auto-generated)
- pharmacy_sale_id (FK: pharmacy_sales)
- return_date
- return_type (customer, damaged, expired)
- total_amount
- refund_amount
- status_id
- reason
- notes

العلاقات:

  • sale() → PharmacySale
  • lines() → PharmacyReturnLine (hasMany)

الخدمات (Services)

Service PharmacyDispenseService

Path: Modules/Pharmacy/Application/Services/PharmacyDispenseService.php

الوصف: الخدمة الأساسية لصرف الأدوية - Enhanced مع Transaction Support

Main Methods:

// صرف دواء (مع transaction)
dispense(
    PharmacyDispenseDTO $dto,
    int $saleLineId
): StockMovementDTO

// إلغاء صرف (مع transaction)
cancel(
    PharmacyReturnDTO $dto,
    int $saleLineId
): bool

Dispense Flow:

التدفق الكامل داخل DB::transaction():
  1. Validation: التحقق من عدم الصرف المسبق
  2. Reserve Stock: حجز المخزون
  3. Deduct Stock: خصم المخزون فعلياً
  4. Update Sale Line: تحديث السطر بـ movement_uuid
  5. Dispatch Event: إرسال MedicationDispensedEvent

مثال الاستخدام:

use Modules\Pharmacy\Application\DTOs\PharmacyDispenseDTO;
use Modules\Pharmacy\Application\Services\PharmacyDispenseService;

$dispenseService = app(PharmacyDispenseService::class);

$dto = new PharmacyDispenseDTO(
    productId: 5,
    unitId: 2,
    quantity: 10.0,
    locationId: 1,
    batchId: null // FIFO auto-selection
);

try {
    $movement = $dispenseService->dispense($dto, $saleLineId);

    // Success
    Log::info("Medication dispensed", [
        'movement_uuid' => $movement->movementUuid
    ]);

} catch (DispenseException $e) {
    // معالجة الخطأ
    Log::error("Dispense failed: " . $e->getMessage());
}

Features:

  • DB Transaction: ضمان تكامل البيانات
  • Validation: فحص شامل قبل الصرف
  • Rollback: إلغاء تلقائي عند الخطأ
  • Logging: تسجيل مفصل لكل خطوة
  • Events: إرسال أحداث للتكامل

Custom Exceptions

Exception DispenseException

Path: Modules/Pharmacy/Exceptions/DispenseException.php

الوصف: Exception مخصص لأخطاء الصرف مع Factory Methods

Factory Methods:

// مخزون غير كافي
DispenseException::insufficientStock(
    int $productId,
    float $required,
    float $available
)

// تم الصرف مسبقاً
DispenseException::alreadyDispensed(int $saleLineId)

// لم يتم الصرف
DispenseException::notDispensed(int $saleLineId)

// السطر غير موجود
DispenseException::saleLineNotFound(int $saleLineId)

مثال الاستخدام:

if ($availableStock < $requiredQuantity) {
    throw DispenseException::insufficientStock(
        $productId,
        $requiredQuantity,
        $availableStock
    );
}

if ($saleLine->inventory_movement_uuid) {
    throw DispenseException::alreadyDispensed($saleLineId);
}

Events & Listeners

Event MedicationDispensedEvent

Path: Modules/Pharmacy/Domain/Events/MedicationDispensedEvent.php

متى يُرسل: عند صرف دواء بنجاح

Event Data:

class MedicationDispensedEvent
{
    public function __construct(
        public int $saleLineId,
        public StockMovementDTO $movement,
        public array $metadata = []
    ) {}
}

Listeners:

  • CreateStockMovementListener (Inventory) - تسجيل في Inventory logs
  • UpdatePatientMedicationHistoryListener - تحديث سجل المريض

Event MedicationDispenseCancelledEvent

Path: Modules/Pharmacy/Domain/Events/MedicationDispenseCancelledEvent.php

متى يُرسل: عند إلغاء صرف دواء

Listeners:

  • ReverseStockMovementListener (Inventory) - تسجيل العكس في Inventory

API Endpoints

Base URL: /api/pharmacy

Sales Management

GET /sales

قائمة فواتير الصيدلية

Query Parameters
  • patient_id - فلترة حسب المريض
  • status_id - فلترة حسب الحالة
  • date_from - من تاريخ
  • date_to - إلى تاريخ
  • include - patient,lines,status
POST /sales

إنشاء فاتورة صيدلية جديدة

Request Body
{
  "patient_id": 123,
  "encounter_id": 456,
  "sale_date": "2025-12-06",
  "lines": [
    {
      "drug_id": 1,
      "inventory_product_id": 5,
      "inventory_batch_id": 10,
      "unit_id": 2,
      "quantity": 10,
      "price": 25.50
    }
  ],
  "notes": "Evening medication"
}
GET /sales/{id}

تفاصيل فاتورة محددة

Medication Dispensing

POST /sales/{id}/dispense

صرف الأدوية من الفاتورة

Request Body
{
  "lines": [
    {
      "sale_line_id": 1,
      "inventory_product_id": 5,
      "unit_id": 2,
      "quantity": 10,
      "batch_id": null
    }
  ]
}
Response Example
{
  "success": true,
  "message": "Medication dispensed successfully",
  "data": {
    "dispensed_lines": 1,
    "movements": [
      {
        "sale_line_id": 1,
        "movement_uuid": "abc-123-def",
        "quantity": 10,
        "dispensed_at": "2025-12-06T10:30:00Z"
      }
    ]
  }
}
POST /sales/{id}/cancel-dispense

إلغاء صرف دواء

Returns Management

GET /returns

قائمة المرتجعات

POST /returns

إنشاء مرتجع جديد

Request Body
{
  "pharmacy_sale_id": 123,
  "return_type": "customer",
  "reason": "Wrong medication",
  "lines": [
    {
      "sale_line_id": 1,
      "quantity": 5,
      "refund_amount": 125.00
    }
  ]
}

Integration Architecture

Integration مع Inventory Module

Pattern: Event-Driven Integration

Flow Diagram:

PharmacyDispenseService
    ↓
1. Validate (validateNotDispensed)
    ↓
2. Reserve Stock (StockReservationService)
    ↓
3. Deduct Stock (StockDeductionService)
    ↓
4. Update Sale Line (inventory_movement_uuid)
    ↓
5. Dispatch Event (MedicationDispensedEvent)
    ↓
[Inventory Module Listener]
    ↓
6. Log Movement (CreateStockMovementListener)

Services المستخدمة من Inventory:

  • StockReservationService - حجز المخزون
  • StockDeductionService - خصم المخزون
  • StockReleaseService - تحرير الحجز

Integration مع DrugCatalog Module

الربط مع DrugCatalog من خلال drug_id في PharmacySaleLine:

// الحصول على معلومات الدواء
$saleLine = PharmacySaleLine::with('drug')->find($id);

echo $saleLine->drug->trade_name;
echo $saleLine->drug->dosageForm->name;
echo $saleLine->drug->manufacturer->name;

Integration مع Care Module

ربط فواتير الصيدلية مع Encounters والـ Prescriptions:

// إنشاء فاتورة من وصفة طبية
$prescription = Prescription::with('lines')->find($id);

$sale = PharmacySale::create([
    'patient_id' => $prescription->patient_id,
    'encounter_id' => $prescription->encounter_id,
    'prescription_id' => $prescription->id,
    // ...
]);

// نسخ الأدوية من الوصفة
foreach ($prescription->lines as $prescLine) {
    $sale->lines()->create([
        'drug_id' => $prescLine->drug_id,
        'quantity' => $prescLine->quantity,
        // ...
    ]);
}

Best Practices

التوصيات

  • استخدم PharmacyDispenseService دائماً - لا تخصم المخزون يدوياً
  • جميع العمليات محمية بـ DB::transaction() - لا داعي لإضافة transaction آخر
  • استخدم DispenseException لمعالجة الأخطاء
  • اترك batchId null للسماح بـ FIFO auto-selection
  • راقب الـ Events للـ audit logging
  • استخدم inventory_movement_uuid لتتبع الحركات

تحذيرات

  • لا تعدل inventory_movement_uuid يدوياً
  • لا تصرف نفس السطر مرتين - سيفشل بـ DispenseException
  • تأكد من وجود inventory_product_id قبل الصرف
  • عند الإلغاء، تأكد من استخدام cancel() method

مثال كامل

use Modules\Pharmacy\Application\Services\PharmacyDispenseService;
use Modules\Pharmacy\Application\DTOs\PharmacyDispenseDTO;
use Modules\Pharmacy\Exceptions\DispenseException;

$dispenseService = app(PharmacyDispenseService::class);

try {
    DB::transaction(function () use ($dispenseService, $saleLines) {
        foreach ($saleLines as $line) {
            $dto = new PharmacyDispenseDTO(
                productId: $line->inventory_product_id,
                unitId: $line->unit_id,
                quantity: $line->quantity,
                locationId: $line->location_id,
                batchId: null // FIFO
            );

            $movement = $dispenseService->dispense($dto, $line->id);

            Log::info('Line dispensed', [
                'line_id' => $line->id,
                'movement_uuid' => $movement->movementUuid
            ]);
        }
    });

    return response()->json([
        'success' => true,
        'message' => 'All medications dispensed successfully'
    ]);

} catch (DispenseException $e) {
    Log::error('Dispense failed', [
        'error' => $e->getMessage(),
        'sale_id' => $saleId
    ]);

    return response()->json([
        'success' => false,
        'message' => $e->getMessage()
    ], 400);
}

Testing

Integration Tests

File: Modules/Pharmacy/tests/Feature/PharmacyInventoryIntegrationTest.php

Test Cases:

  • ✅ Complete dispense flow with stock deduction
  • ✅ Dispense fails with insufficient stock
  • ✅ Dispense prevents double dispensing
  • ✅ Cancel dispense releases stock
  • ✅ Cancel fails if not dispensed
  • ✅ Transaction rollback on error

Run Tests:

# All Pharmacy tests
php artisan test Modules/Pharmacy/tests

# Integration tests only
php artisan test --filter=PharmacyInventoryIntegrationTest

# Specific test
php artisan test --filter=test_complete_dispense_flow_with_stock_deduction

إحصائيات Module

6+
Entities
5+
Services
25+
API Endpoints
50+
Tests