terminaldz/laravel-yalidine

Laravel package for integrating with the Yalidine delivery API

Installs: 0

Dependents: 0

Suggesters: 0

Security: 0

Stars: 1

Watchers: 0

Forks: 0

Open Issues: 0

pkg:composer/terminaldz/laravel-yalidine

dev-main 2025-10-09 05:26 UTC

This package is auto-updated.

Last update: 2026-01-09 06:10:16 UTC


README

Latest Version on Packagist Total Downloads License

A Laravel package for integrating with the Yalidine delivery API. This package provides a clean, fluent interface for managing parcels, tracking shipments, retrieving location data, and calculating delivery fees through the Yalidine REST API.

Features

  • 🚀 Simple and intuitive API
  • 📦 Complete parcel management (create, retrieve, update, delete)
  • 📍 Location data retrieval (wilayas, communes, centers)
  • 📊 Delivery history tracking
  • 💰 Fee calculation with weight and dimension support
  • ⚡ Rate limit handling and monitoring
  • 🔄 Pagination support for large datasets
  • ✅ Comprehensive validation
  • 🧪 Testing utilities included
  • 📝 Full Laravel integration (Service Provider, Facade, DI)
  • 🔒 Secure credential management
  • 📋 Detailed error messages and exceptions

Requirements

  • PHP >= 8.1
  • Laravel >= 10.0
  • Guzzle >= 7.0

Installation

Install the package via Composer:

composer require terminaldz/laravel-yalidine

Publish Configuration

Publish the configuration file:

php artisan vendor:publish --provider="Terminaldz\LaravelYalidine\YalidineServiceProvider"

This will create a config/yalidine.php configuration file.

Environment Variables

Add your Yalidine API credentials to your .env file:

YALIDINE_API_ID=your_api_id_here
YALIDINE_API_TOKEN=your_api_token_here
YALIDINE_BASE_URL=https://api.yalidine.app/v1

You can obtain your API credentials from your Yalidine dashboard.

Configuration

The config/yalidine.php file contains all configuration options:

return [
    // API Credentials
    'api_id' => env('YALIDINE_API_ID'),
    'api_token' => env('YALIDINE_API_TOKEN'),
    'base_url' => env('YALIDINE_BASE_URL', 'https://api.yalidine.app/v1'),
    
    // HTTP Client Options
    'timeout' => env('YALIDINE_TIMEOUT', 30),
    'retry_times' => env('YALIDINE_RETRY_TIMES', 3),
    'retry_delay' => env('YALIDINE_RETRY_DELAY', 1000),
    
    // Rate Limit Settings
    'rate_limit' => [
        'log_warnings' => true,
        'warning_threshold' => 10,
    ],
    
    // Caching
    'cache' => [
        'enabled' => true,
        'ttl' => 3600,
        'prefix' => 'yalidine',
    ],
    
    // Logging
    'logging' => [
        'enabled' => env('YALIDINE_LOGGING', true),
        'channel' => env('YALIDINE_LOG_CHANNEL', 'stack'),
    ],
];

Basic Usage

Using the Facade

use Terminaldz\LaravelYalidine\Facades\Yalidine;

// Create a parcel
$result = Yalidine::parcels()->create([
    'order_id' => 'ORDER-12345',
    'from_wilaya_name' => 'Alger',
    'firstname' => 'Ahmed',
    'familyname' => 'Benali',
    'contact_phone' => '0550123456',
    'address' => 'Cité des Martyrs, Bâtiment A',
    'to_commune_name' => 'Bordj El Kiffan',
    'to_wilaya_name' => 'Alger',
    'product_list' => 'Electronics - Smartphone',
    'price' => 5000,
    'do_insurance' => true,
    'declared_value' => 5000,
    'length' => 30,
    'width' => 20,
    'height' => 10,
    'weight' => 2,
    'freeshipping' => false,
    'is_stopdesk' => false,
    'has_exchange' => false,
]);

echo "Tracking Number: " . $result->tracking;
echo "Label URL: " . $result->label;

Using Dependency Injection

use Terminaldz\LaravelYalidine\Client\YalidineClient;

class ShippingController extends Controller
{
    public function __construct(
        private YalidineClient $yalidine
    ) {}
    
    public function createShipment(Request $request)
    {
        $result = $this->yalidine->parcels()->create([
            'order_id' => $request->order_id,
            'firstname' => $request->firstname,
            'familyname' => $request->familyname,
            // ... other fields
        ]);
        
        return response()->json([
            'tracking' => $result->tracking,
            'label' => $result->label,
        ]);
    }
}

Usage Examples

Parcel Management

Create a Single Parcel

$result = Yalidine::parcels()->create([
    'order_id' => 'ORDER-12345',
    'from_wilaya_name' => 'Alger',
    'firstname' => 'Ahmed',
    'familyname' => 'Benali',
    'contact_phone' => '0550123456',
    'address' => 'Cité des Martyrs',
    'to_commune_name' => 'Bordj El Kiffan',
    'to_wilaya_name' => 'Alger',
    'product_list' => 'Electronics',
    'price' => 5000,
    'do_insurance' => true,
    'declared_value' => 5000,
    'length' => 30,
    'width' => 20,
    'height' => 10,
    'weight' => 2,
    'freeshipping' => false,
    'is_stopdesk' => false,
    'has_exchange' => false,
]);

Create Multiple Parcels (Batch)

$parcels = [
    [
        'order_id' => 'ORDER-001',
        'firstname' => 'Ahmed',
        // ... other fields
    ],
    [
        'order_id' => 'ORDER-002',
        'firstname' => 'Fatima',
        // ... other fields
    ],
];

$results = Yalidine::parcels()->createBatch($parcels);

Retrieve a Parcel

$parcel = Yalidine::parcels()->get('yal-123456');

echo $parcel->tracking;
echo $parcel->lastStatus;
echo $parcel->dateCreation->format('Y-m-d H:i');

List Parcels with Filters

$parcels = Yalidine::parcels()->list([
    'status' => 'Livré',
    'wilaya' => 16,
    'from_date' => '2025-01-01',
    'to_date' => '2025-01-31',
    'page' => 1,
    'page_size' => 50,
]);

foreach ($parcels->items() as $parcel) {
    echo $parcel->tracking . ': ' . $parcel->lastStatus . "\n";
}

if ($parcels->hasNextPage()) {
    $nextUrl = $parcels->getNextPageUrl();
}

Update a Parcel

// Only parcels with status "en préparation" can be updated
$parcel = Yalidine::parcels()->update('yal-123456', [
    'contact_phone' => '0551234567',
    'address' => 'New Address',
]);

Delete a Parcel

// Only parcels with status "en préparation" can be deleted
$deleted = Yalidine::parcels()->delete('yal-123456');

Location Data

Get All Wilayas

$wilayas = Yalidine::locations()->wilayas();

foreach ($wilayas->items() as $wilaya) {
    echo $wilaya->id . ': ' . $wilaya->name . "\n";
}

Get Communes for a Wilaya

$communes = Yalidine::locations()->communes([
    'wilaya_id' => 16,
]);

foreach ($communes->items() as $commune) {
    echo $commune->name . ' - Delivery time: ' . $commune->deliveryTimeParcel . ' days' . "\n";
}

Get Communes with Stop Desks

$stopDeskCommunes = Yalidine::locations()->communes([
    'wilaya_id' => 16,
    'has_stop_desk' => true,
]);

Get Centers

$centers = Yalidine::locations()->centers([
    'wilaya_id' => 16,
    'commune_id' => 1630,
]);

foreach ($centers->items() as $center) {
    echo $center->name . ' - ' . $center->address . "\n";
    echo 'GPS: ' . $center->getLatitude() . ', ' . $center->getLongitude() . "\n";
}

Delivery History

Get History for a Parcel

$history = Yalidine::histories()->get('yal-123456');

foreach ($history as $status) {
    echo $status->dateStatus->format('Y-m-d H:i') . ': ';
    echo $status->status;
    
    if ($status->reason) {
        echo ' (' . $status->reason . ')';
    }
    
    echo "\n";
}

List Histories with Filters

$histories = Yalidine::histories()->list([
    'tracking' => 'yal-123456',
    'status' => 'Livré',
    'from_date' => '2025-01-01',
    'to_date' => '2025-01-31',
]);

Fee Calculation

Calculate Fees Between Wilayas

$fees = Yalidine::fees()->calculate(
    fromWilayaId: 5,  // Batna
    toWilayaId: 16    // Alger
);

echo "Zone: " . $fees->zone . "\n";
echo "COD Percentage: " . $fees->codPercentage . "%\n";
echo "Insurance Percentage: " . $fees->insurancePercentage . "%\n";

// Get fee for a specific commune
$communeFee = $fees->getFeeForCommune(1630);
echo "Express Home: " . $communeFee->expressHome . " DA\n";
echo "Express Stop Desk: " . $communeFee->expressStopDesk . " DA\n";

Calculate Total Fee Including Weight

$total = Yalidine::fees()->calculateTotal([
    'from_wilaya_id' => 5,
    'to_wilaya_id' => 16,
    'commune_id' => 1630,
    'price' => 5000,
    'declared_value' => 5000,
    'weight' => 7,
    'length' => 30,
    'width' => 20,
    'height' => 10,
    'delivery_type' => 'express_home',
]);

echo "Total Fee: " . $total . " DA";

Rate Limit Monitoring

$rateLimits = Yalidine::getRateLimitInfo();

echo "Second quota left: " . $rateLimits['second'] . "\n";
echo "Minute quota left: " . $rateLimits['minute'] . "\n";
echo "Hour quota left: " . $rateLimits['hour'] . "\n";
echo "Day quota left: " . $rateLimits['day'] . "\n";

Error Handling

The package provides specific exceptions for different error scenarios:

use Terminaldz\LaravelYalidine\Exceptions\ValidationException;
use Terminaldz\LaravelYalidine\Exceptions\RateLimitExceededException;
use Terminaldz\LaravelYalidine\Exceptions\AuthenticationException;
use Terminaldz\LaravelYalidine\Exceptions\YalidineException;

try {
    $result = Yalidine::parcels()->create($parcelData);
} catch (ValidationException $e) {
    // Handle validation errors
    foreach ($e->getErrors() as $field => $errors) {
        echo "$field: " . implode(', ', $errors) . "\n";
    }
} catch (RateLimitExceededException $e) {
    // Handle rate limit
    $retryAfter = $e->getRetryAfter();
    Log::warning("Rate limit exceeded. Retry after {$retryAfter} seconds");
} catch (AuthenticationException $e) {
    // Handle authentication errors
    Log::error('Invalid API credentials: ' . $e->getMessage());
} catch (YalidineException $e) {
    // Handle other Yalidine errors
    Log::error('Yalidine API error: ' . $e->getMessage());
}

Testing

The package includes testing utilities to help you test your integration without making real API calls:

use Terminaldz\LaravelYalidine\Facades\Yalidine;
use Terminaldz\LaravelYalidine\DataTransferObjects\Parcel;

// In your test
Yalidine::fake([
    'parcels.create' => [
        'tracking' => 'yal-TEST123',
        'order_id' => 'ORDER-12345',
        'label' => 'https://example.com/label.pdf',
        'success' => true,
    ],
]);

// Your code that uses Yalidine
$result = Yalidine::parcels()->create([...]);

// Assert expectations
Yalidine::assertParcelCreated();
Yalidine::assertParcelCreated(function ($data) {
    return $data['order_id'] === 'ORDER-12345';
});

Advanced Usage

Fluent Query Builder

$parcels = Yalidine::parcels()
    ->query()
    ->whereStatus('Livré')
    ->whereWilaya(16)
    ->whereDateBetween('2025-01-01', '2025-01-31')
    ->whereOrderId('ORDER-12345')
    ->paginate(50);

Caching Location Data

Location data is automatically cached based on your configuration. You can manually clear the cache:

// Clear all Yalidine cache
Cache::tags(['yalidine'])->flush();

// Or use the cache prefix from config
$prefix = config('yalidine.cache.prefix');
Cache::forget("{$prefix}:wilayas");

Common Use Cases

E-commerce Integration

// In your order processing logic
public function processOrder(Order $order)
{
    try {
        // Create parcel when order is confirmed
        $result = Yalidine::parcels()->create([
            'order_id' => $order->id,
            'from_wilaya_name' => config('shop.wilaya'),
            'firstname' => $order->customer->first_name,
            'familyname' => $order->customer->last_name,
            'contact_phone' => $order->customer->phone,
            'address' => $order->shipping_address,
            'to_commune_name' => $order->commune,
            'to_wilaya_name' => $order->wilaya,
            'product_list' => $order->items->pluck('name')->implode(', '),
            'price' => $order->total_with_shipping,
            'do_insurance' => $order->total > 10000,
            'declared_value' => $order->total,
            'freeshipping' => $order->has_free_shipping,
        ]);
        
        // Save tracking number
        $order->update([
            'tracking_number' => $result->tracking,
            'shipping_label_url' => $result->label,
        ]);
        
        // Notify customer
        $order->customer->notify(new ShipmentCreated($result->tracking));
        
    } catch (ValidationException $e) {
        Log::error('Invalid order data for Yalidine', [
            'order_id' => $order->id,
            'errors' => $e->getErrors(),
        ]);
        throw $e;
    }
}

Tracking Status Updates

// Create a scheduled job to update parcel statuses
public function handle()
{
    $pendingOrders = Order::whereIn('status', ['shipped', 'in_transit'])->get();
    
    foreach ($pendingOrders as $order) {
        try {
            $parcel = Yalidine::parcels()->get($order->tracking_number);
            
            // Update order status based on parcel status
            if ($parcel->lastStatus === 'Livré') {
                $order->markAsDelivered();
                $order->customer->notify(new OrderDelivered($order));
            } elseif (in_array($parcel->lastStatus, ['Échec', 'Retour'])) {
                $order->markAsFailed();
                $order->customer->notify(new DeliveryFailed($order, $parcel->lastStatus));
            }
            
        } catch (YalidineException $e) {
            Log::warning('Failed to update tracking for order', [
                'order_id' => $order->id,
                'error' => $e->getMessage(),
            ]);
        }
    }
}

Dynamic Fee Calculator

// Calculate shipping fees in real-time during checkout
public function calculateShipping(Request $request)
{
    $validated = $request->validate([
        'wilaya_id' => 'required|integer',
        'commune_id' => 'required|integer',
        'cart_total' => 'required|numeric',
    ]);
    
    try {
        $fees = Yalidine::fees()->calculate(
            fromWilayaId: config('shop.wilaya_id'),
            toWilayaId: $validated['wilaya_id']
        );
        
        $communeFee = $fees->getFeeForCommune($validated['commune_id']);
        
        return response()->json([
            'express_home' => $communeFee->expressHome,
            'express_stopdesk' => $communeFee->expressStopDesk,
            'classic_home' => $communeFee->classicHome,
            'classic_stopdesk' => $communeFee->classicStopDesk,
        ]);
        
    } catch (YalidineException $e) {
        return response()->json([
            'error' => 'Unable to calculate shipping fees',
        ], 500);
    }
}

Location Dropdown Population

// Populate location dropdowns for checkout form
public function getLocations(Request $request)
{
    // Cache wilayas for 24 hours
    $wilayas = Cache::remember('yalidine:wilayas', 86400, function () {
        return Yalidine::locations()->wilayas()->items();
    });
    
    // Get communes for selected wilaya
    if ($request->has('wilaya_id')) {
        $communes = Cache::remember(
            "yalidine:communes:{$request->wilaya_id}",
            86400,
            function () use ($request) {
                return Yalidine::locations()->communes([
                    'wilaya_id' => $request->wilaya_id,
                ])->items();
            }
        );
        
        return response()->json([
            'wilayas' => $wilayas,
            'communes' => $communes,
        ]);
    }
    
    return response()->json(['wilayas' => $wilayas]);
}

Error Handling Patterns

Graceful Degradation

try {
    $result = Yalidine::parcels()->create($parcelData);
    return redirect()->route('orders.show', $order)->with('success', 'Shipment created');
} catch (ValidationException $e) {
    // Show validation errors to user
    return back()->withErrors($e->getErrors())->withInput();
} catch (RateLimitExceededException $e) {
    // Queue for retry
    CreateYalidineParcel::dispatch($order)->delay(now()->addSeconds($e->getRetryAfter()));
    return back()->with('warning', 'Shipment will be created shortly');
} catch (YalidineException $e) {
    // Log and show generic error
    Log::error('Yalidine API error', ['error' => $e->getMessage()]);
    return back()->with('error', 'Unable to create shipment. Please try again.');
}

Retry Logic with Queues

use Illuminate\Queue\SerializesModels;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Contracts\Queue\ShouldQueue;

class CreateYalidineParcel implements ShouldQueue
{
    use InteractsWithQueue, SerializesModels;
    
    public $tries = 3;
    public $backoff = [60, 300, 900]; // 1 min, 5 min, 15 min
    
    public function __construct(
        public Order $order
    ) {}
    
    public function handle()
    {
        try {
            $result = Yalidine::parcels()->create([
                'order_id' => $this->order->id,
                // ... other fields
            ]);
            
            $this->order->update([
                'tracking_number' => $result->tracking,
                'shipping_label_url' => $result->label,
            ]);
            
        } catch (RateLimitExceededException $e) {
            // Release back to queue with delay
            $this->release($e->getRetryAfter());
        } catch (ValidationException $e) {
            // Don't retry validation errors
            $this->fail($e);
        }
    }
}

Monitoring Rate Limits

// Create a middleware to monitor rate limits
class MonitorYalidineRateLimits
{
    public function handle($request, Closure $next)
    {
        $response = $next($request);
        
        // Check rate limits after each request
        $rateLimits = Yalidine::getRateLimitInfo();
        
        // Alert if any quota is running low
        foreach ($rateLimits as $period => $remaining) {
            if ($remaining < 10) {
                Log::warning("Yalidine {$period} quota running low", [
                    'remaining' => $remaining,
                ]);
                
                // Send alert to admin
                if ($remaining < 5) {
                    Notification::route('slack', config('services.slack.webhook'))
                        ->notify(new YalidineQuotaLow($period, $remaining));
                }
            }
        }
        
        return $response;
    }
}

Validation Rules

The package validates data before sending to the API. Here are the validation rules:

Phone Number Validation

  • Mobile: Must start with 0 followed by 9 digits (e.g., 0550123456)
  • Landline: Must start with 0 followed by 8 digits (e.g., 021123456)

Price and Value Validation

  • price: Must be between 0 and 150,000 DA
  • declared_value: Must be between 0 and 150,000 DA

Dimension Validation

  • length, width, height: Must be >= 0
  • weight: Must be >= 0

Conditional Validation

  • If is_stopdesk is true, stopdesk_id is required
  • If has_exchange is true, product_to_collect is required

Performance Tips

1. Cache Location Data

Location data (wilayas, communes, centers) rarely changes. Cache it aggressively:

// In your AppServiceProvider
public function boot()
{
    // Warm up cache on application boot
    Cache::remember('yalidine:wilayas', 86400, function () {
        return Yalidine::locations()->wilayas()->items();
    });
}

2. Use Batch Operations

When creating multiple parcels, use batch creation:

// Instead of this:
foreach ($orders as $order) {
    Yalidine::parcels()->create($order->toParcelData());
}

// Do this:
$parcelsData = $orders->map->toParcelData()->toArray();
Yalidine::parcels()->createBatch($parcelsData);

3. Queue Heavy Operations

Queue operations that aren't time-sensitive:

// Queue parcel creation
CreateYalidineParcel::dispatch($order);

// Queue status updates
UpdateParcelStatuses::dispatch()->everyFiveMinutes();

4. Optimize Pagination

Use appropriate page sizes based on your needs:

// For UI display (smaller pages)
$parcels = Yalidine::parcels()->list(['page_size' => 20]);

// For batch processing (larger pages)
$parcels = Yalidine::parcels()->list(['page_size' => 500]);

Troubleshooting

Authentication Errors

Problem: Getting 401 Unauthorized errors

Solution:

  1. Verify your credentials in .env file
  2. Ensure credentials are published to config: php artisan config:clear
  3. Check that credentials are correct in Yalidine dashboard

Rate Limit Errors

Problem: Getting 429 Too Many Requests errors

Solution:

  1. Implement retry logic with exponential backoff
  2. Monitor rate limits using getRateLimitInfo()
  3. Use queues to spread requests over time
  4. Contact Yalidine to increase your quota

Validation Errors

Problem: Getting validation errors when creating parcels

Solution:

  1. Check phone number format (0 + 9 digits for mobile)
  2. Verify price is within 0-150,000 range
  3. Ensure required fields are present
  4. Check conditional requirements (stopdesk_id, product_to_collect)

Connection Timeouts

Problem: Requests timing out

Solution:

  1. Increase timeout in config: 'timeout' => 60
  2. Check your network connection
  3. Verify Yalidine API is accessible
  4. Use queues for non-critical operations

API Reference

Parcel Resource

create(array $parcelData): ParcelCreationResult

Create a single parcel.

createBatch(array $parcels): array

Create multiple parcels in one request.

get(string $tracking): Parcel

Retrieve a parcel by tracking number.

list(array $filters = []): PaginatedResponse

List parcels with optional filters.

update(string $tracking, array $data): Parcel

Update a parcel (only if status is "en préparation").

delete(string $tracking): bool

Delete a parcel (only if status is "en préparation").

query(): ParcelQueryBuilder

Get a fluent query builder for parcels.

Location Resource

wilayas(array $filters = []): PaginatedResponse

Get all wilayas.

getWilaya(int $id): Wilaya

Get a specific wilaya by ID.

communes(array $filters = []): PaginatedResponse

Get communes with optional filters (wilaya_id, has_stop_desk).

getCommune(int $id): Commune

Get a specific commune by ID.

centers(array $filters = []): PaginatedResponse

Get centers with optional filters (wilaya_id, commune_id, center_id).

getCenter(int $centerId): Center

Get a specific center by ID.

History Resource

get(string $tracking): array

Get delivery history for a specific tracking number.

list(array $filters = []): PaginatedResponse

List histories with optional filters.

query(): HistoryQueryBuilder

Get a fluent query builder for histories.

Fee Resource

calculate(int $fromWilayaId, int $toWilayaId): FeeCalculation

Calculate fees between two wilayas.

calculateTotal(array $parcelSpecs): float

Calculate total fee including weight, insurance, and COD.

Documentation

External Resources

Contributing

Please see CONTRIBUTING.md for details on how to contribute to this package.

Changelog

Please see CHANGELOG.md for recent changes.

Security

If you discover any security-related issues, please email boukemoucheidriss@gmail.com instead of using the issue tracker.

Credits

Support

For issues, questions, or contributions, please visit our GitHub repository.

License

This package is open-sourced software licensed under the MIT license.