DHL Modul v0.5 Shipping Label ok
This commit is contained in:
parent
480fdc65ed
commit
8fdaa0ba1d
122 changed files with 17938 additions and 2239 deletions
199
packages/acme-laravel-dhl/README.md
Normal file
199
packages/acme-laravel-dhl/README.md
Normal file
|
|
@ -0,0 +1,199 @@
|
|||
# DHL Laravel Package
|
||||
|
||||
A comprehensive Laravel package for DHL shipping, tracking, and returns functionality with direct API integration.
|
||||
|
||||
## Features
|
||||
|
||||
- **Direct DHL API Integration**: No SDK dependencies, full control over API calls
|
||||
- **German Address Parsing**: Automatic extraction of house numbers from German addresses
|
||||
- **Queue Support**: Optional asynchronous processing for high-volume operations
|
||||
- **Comprehensive Tracking**: Full tracking event history and status management
|
||||
- **Return Label Generation**: Easy return shipment creation
|
||||
- **Retry Mechanisms**: Built-in retry logic for API calls and file operations
|
||||
- **Laravel Integration**: Native service providers, facades, and Eloquent models
|
||||
|
||||
## Installation
|
||||
|
||||
### 1. Add to composer.json
|
||||
|
||||
```json
|
||||
{
|
||||
"repositories": [
|
||||
{
|
||||
"type": "path",
|
||||
"url": "./packages/acme-laravel-dhl"
|
||||
}
|
||||
],
|
||||
"require": {
|
||||
"acme/laravel-dhl": "*"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Install the package
|
||||
|
||||
```bash
|
||||
composer update acme/laravel-dhl
|
||||
```
|
||||
|
||||
### 3. Publish configuration and run migrations
|
||||
|
||||
```bash
|
||||
php artisan vendor:publish --provider="Acme\Dhl\DhlServiceProvider" --tag="config"
|
||||
php artisan migrate
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
Add the following environment variables to your `.env` file:
|
||||
|
||||
```env
|
||||
# DHL API Configuration
|
||||
DHL_BASE_URL=https://api-eu.dhl.com
|
||||
DHL_API_KEY=your_api_key_here
|
||||
DHL_USERNAME=your_username
|
||||
DHL_PASSWORD=your_password
|
||||
DHL_BILLING_NUMBER=your_billing_number
|
||||
|
||||
# DHL Default Settings
|
||||
DHL_PRODUCT=V01PAK
|
||||
DHL_LABEL_FORMAT=PDF
|
||||
DHL_PROFILE=STANDARD_GRUPPENPROFIL
|
||||
|
||||
# Optional Queue Settings
|
||||
DHL_USE_QUEUE=false
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
### Creating a Shipment Label
|
||||
|
||||
```php
|
||||
use DHL;
|
||||
|
||||
$orderData = [
|
||||
'order_id' => 12345,
|
||||
'weight_kg' => 2.5,
|
||||
'shipper' => [
|
||||
'name' => 'Your Company',
|
||||
'street' => 'Musterstraße 123', // House number will be auto-parsed
|
||||
'postalCode' => '12345',
|
||||
'city' => 'Berlin',
|
||||
'country' => 'DE'
|
||||
],
|
||||
'consignee' => [
|
||||
'name' => 'Customer Name',
|
||||
'street' => 'Kundenstraße 456', // House number will be auto-parsed
|
||||
'postalCode' => '54321',
|
||||
'city' => 'Munich',
|
||||
'country' => 'DE'
|
||||
]
|
||||
];
|
||||
|
||||
$result = DHL::createLabel($orderData);
|
||||
// Returns: ['shipmentNumber' => '123...', 'label_path' => 'dhl/labels/...']
|
||||
```
|
||||
|
||||
### German Address Parsing
|
||||
|
||||
The package automatically handles German address formats by extracting house numbers from the street field:
|
||||
|
||||
```php
|
||||
// Input: "Musterstraße 123a"
|
||||
// Parsed to: street="Musterstraße", houseNumber="123a"
|
||||
|
||||
// Supported formats:
|
||||
"Musterstraße 123" -> street: "Musterstraße", number: "123"
|
||||
"Am Markt 7" -> street: "Am Markt", number: "7"
|
||||
"Karl-Marx-Straße 156" -> street: "Karl-Marx-Straße", number: "156"
|
||||
"Lindenstraße 1-3" -> street: "Lindenstraße", number: "1-3"
|
||||
"Muster Str. 99" -> street: "Muster Str.", number: "99"
|
||||
```
|
||||
|
||||
### Tracking Shipments
|
||||
|
||||
```php
|
||||
use DHL;
|
||||
|
||||
// Get current tracking status
|
||||
$status = DHL::track('1234567890123');
|
||||
|
||||
// Returns detailed tracking information including events
|
||||
```
|
||||
|
||||
### Creating Return Labels
|
||||
|
||||
```php
|
||||
use DHL;
|
||||
|
||||
$returnData = [
|
||||
'original_shipment_id' => 1,
|
||||
'return_reason' => 'Customer return',
|
||||
// ... address data
|
||||
];
|
||||
|
||||
$return = DHL::createReturn($returnData);
|
||||
```
|
||||
|
||||
### Using Services Directly
|
||||
|
||||
```php
|
||||
use Acme\Dhl\Services\ShippingService;
|
||||
use Acme\Dhl\Services\TrackingService;
|
||||
|
||||
// Dependency injection in controllers
|
||||
public function createLabel(ShippingService $shipping, Request $request)
|
||||
{
|
||||
$result = $shipping->createLabel($request->validated());
|
||||
return response()->json($result);
|
||||
}
|
||||
```
|
||||
|
||||
## Database Schema
|
||||
|
||||
The package creates the following tables:
|
||||
|
||||
- `dhl_shipments` - Main shipment records (outbound and returns)
|
||||
- `dhl_tracking_events` - Tracking event history
|
||||
|
||||
## Queue Configuration
|
||||
|
||||
For high-volume applications, enable queue processing:
|
||||
|
||||
```env
|
||||
DHL_USE_QUEUE=true
|
||||
```
|
||||
|
||||
When enabled, label creation and tracking updates will be processed asynchronously.
|
||||
|
||||
## Error Handling
|
||||
|
||||
The package includes comprehensive error handling:
|
||||
|
||||
- `DhlApiException` - General API errors
|
||||
- `DhlAuthenticationException` - Authentication failures
|
||||
- `DhlValidationException` - Data validation errors
|
||||
|
||||
## Testing
|
||||
|
||||
Run the included tests:
|
||||
|
||||
```bash
|
||||
vendor/bin/phpunit packages/acme-laravel-dhl/tests/
|
||||
```
|
||||
|
||||
## Address Parsing Tests
|
||||
|
||||
The package includes comprehensive tests for German address parsing:
|
||||
|
||||
```bash
|
||||
vendor/bin/phpunit packages/acme-laravel-dhl/tests/Unit/AddressParserTest.php
|
||||
```
|
||||
|
||||
## Logging
|
||||
|
||||
All API calls, address parsing, and errors are logged using Laravel's logging system. Check your logs for debugging information.
|
||||
|
||||
## License
|
||||
|
||||
MIT License
|
||||
30
packages/acme-laravel-dhl/composer.json
Normal file
30
packages/acme-laravel-dhl/composer.json
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
{
|
||||
"name": "acme/laravel-dhl",
|
||||
"description": "DHL (Post & Paket DE) Shipping, Returns & Tracking for Laravel.",
|
||||
"type": "library",
|
||||
"license": "MIT",
|
||||
"require": {
|
||||
"php": "^8.1",
|
||||
"illuminate/support": "^10.0|^11.0",
|
||||
"illuminate/http": "^10.0|^11.0",
|
||||
"illuminate/database": "^10.0|^11.0",
|
||||
"illuminate/queue": "^10.0|^11.0",
|
||||
"illuminate/events": "^10.0|^11.0",
|
||||
"guzzlehttp/guzzle": "^7.0"
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Acme\\Dhl\\": "src/"
|
||||
}
|
||||
},
|
||||
"extra": {
|
||||
"laravel": {
|
||||
"providers": [
|
||||
"Acme\\Dhl\\DhlServiceProvider"
|
||||
],
|
||||
"aliases": {
|
||||
"DHL": "Acme\\Dhl\\Facades\\DHL"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
13
packages/acme-laravel-dhl/config/dhl.php
Normal file
13
packages/acme-laravel-dhl/config/dhl.php
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
<?php
|
||||
return [
|
||||
'base_url' => env('DHL_BASE_URL', 'https://api-eu.dhl.com'),
|
||||
'api_key' => env('DHL_API_KEY'),
|
||||
'username' => env('DHL_USERNAME'),
|
||||
'password' => env('DHL_PASSWORD'),
|
||||
'billing_number' => env('DHL_BILLING_NUMBER'),
|
||||
'default_product' => env('DHL_PRODUCT', 'V01PAK'),
|
||||
'label_format' => env('DHL_LABEL_FORMAT', 'PDF'),
|
||||
'profile' => env('DHL_PROFILE', 'STANDARD_GRUPPENPROFIL'),
|
||||
'webhook' => ['enabled' => env('DHL_WEBHOOK_ENABLED', false), 'secret' => env('DHL_WEBHOOK_SECRET'), 'route' => env('DHL_WEBHOOK_ROUTE', 'dhl/webhooks/tracking')],
|
||||
'use_queue' => env('DHL_USE_QUEUE', false), // Set to true to enable queue jobs for async processing
|
||||
];
|
||||
|
|
@ -0,0 +1,53 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
// Anonymous class for migration (Laravel standard since 8.x)
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('dhl_package_shipments', function (Blueprint $table) {
|
||||
$table->id();
|
||||
|
||||
// Basic shipment info
|
||||
$table->unsignedBigInteger('order_id')->nullable()->index();
|
||||
$table->string('dhl_shipment_no')->nullable()->index();
|
||||
$table->enum('type', ['outbound', 'return'])->default('outbound');
|
||||
$table->unsignedBigInteger('related_shipment_id')->nullable()->index(); // For returns
|
||||
|
||||
// Product and billing
|
||||
$table->string('product_code')->default('V01PAK');
|
||||
$table->string('billing_number')->nullable();
|
||||
$table->decimal('weight_kg', 8, 3)->nullable();
|
||||
|
||||
// Label information
|
||||
$table->string('label_format')->default('PDF');
|
||||
$table->string('label_path')->nullable();
|
||||
|
||||
// Status tracking (simplified)
|
||||
$table->string('status')->default('created'); // created, in_transit, delivered, canceled, etc.
|
||||
$table->string('tracking_status')->nullable(); // Last DHL status text
|
||||
$table->timestamp('last_tracked_at')->nullable();
|
||||
|
||||
// API response data
|
||||
$table->json('api_response_data')->nullable();
|
||||
|
||||
$table->timestamps();
|
||||
|
||||
// Foreign key constraints
|
||||
$table->foreign('related_shipment_id')->references('id')->on('dhl_package_shipments')->onDelete('set null');
|
||||
|
||||
// Indexes for performance
|
||||
$table->index(['order_id', 'type']);
|
||||
$table->index(['status', 'created_at']);
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('dhl_package_shipments');
|
||||
}
|
||||
};
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration {
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('shipments', function (Blueprint $t) {
|
||||
$t->id();
|
||||
$t->unsignedBigInteger('order_id')->nullable();
|
||||
$t->string('carrier')->default('dhl');
|
||||
$t->string('dhl_shipment_no')->nullable()->index();
|
||||
$t->string('product_code')->nullable();
|
||||
$t->string('billing_number')->nullable();
|
||||
$t->decimal('weight_kg', 8, 3)->nullable();
|
||||
$t->string('status')->default('created');
|
||||
$t->string('label_format')->nullable();
|
||||
$t->string('label_path')->nullable();
|
||||
$t->json('meta')->nullable();
|
||||
$t->timestamps();
|
||||
});
|
||||
}
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('shipments');
|
||||
}
|
||||
};
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration {
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('shipment_labels', function (Blueprint $t) {
|
||||
$t->id();
|
||||
$t->foreignId('shipment_id')->constrained('shipments')->cascadeOnDelete();
|
||||
$t->string('format')->default('PDF');
|
||||
$t->string('path');
|
||||
$t->timestamps();
|
||||
});
|
||||
}
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('shipment_labels');
|
||||
}
|
||||
};
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('dhl_package_tracking_events', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->unsignedBigInteger('shipment_id')->index();
|
||||
$table->string('status_code')->nullable();
|
||||
$table->string('status_text')->nullable();
|
||||
$table->string('location')->nullable();
|
||||
$table->timestamp('event_time')->nullable();
|
||||
$table->json('raw')->nullable(); // Full event data from API
|
||||
$table->timestamps();
|
||||
|
||||
$table->foreign('shipment_id')->references('id')->on('dhl_package_shipments')->onDelete('cascade');
|
||||
$table->index(['shipment_id', 'event_time']);
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('dhl_package_tracking_events');
|
||||
}
|
||||
};
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration {
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('tracking_events', function (Blueprint $t) {
|
||||
$t->id();
|
||||
$t->foreignId('shipment_id')->constrained('shipments')->cascadeOnDelete();
|
||||
$t->string('status_code')->nullable();
|
||||
$t->string('status_text')->nullable();
|
||||
$t->string('location')->nullable();
|
||||
$t->timestamp('event_time')->nullable();
|
||||
$t->json('raw')->nullable();
|
||||
});
|
||||
}
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('tracking_events');
|
||||
}
|
||||
};
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration {
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('return_labels', function (Blueprint $t) {
|
||||
$t->id();
|
||||
$t->unsignedBigInteger('order_id')->nullable()->index();
|
||||
$t->string('dhl_return_no')->nullable()->index();
|
||||
$t->string('label_path')->nullable();
|
||||
$t->timestamps();
|
||||
});
|
||||
}
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('return_labels');
|
||||
}
|
||||
};
|
||||
6
packages/acme-laravel-dhl/routes/api.php
Normal file
6
packages/acme-laravel-dhl/routes/api.php
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Support\Facades\Route;
|
||||
use Acme\Dhl\Http\Controllers\TrackingWebhookController;
|
||||
|
||||
Route::post(config('dhl.webhook.route', 'dhl/webhooks/tracking'), [TrackingWebhookController::class, 'handle'])->name('dhl.webhook.tracking');
|
||||
40
packages/acme-laravel-dhl/src/DhlManager.php
Normal file
40
packages/acme-laravel-dhl/src/DhlManager.php
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
<?php
|
||||
|
||||
namespace Acme\Dhl;
|
||||
|
||||
class DhlManager
|
||||
{
|
||||
public function __construct(protected Support\DhlClient $client, protected Services\ShippingService $shipping, protected Services\TrackingService $tracking, protected Services\ReturnsService $returns) {}
|
||||
/**
|
||||
* @param array $orderData
|
||||
* @return array
|
||||
*/
|
||||
public function createLabel(array $orderData): array
|
||||
{
|
||||
return $this->shipping->createLabel($orderData);
|
||||
}
|
||||
/**
|
||||
* @param string $shipmentNumber
|
||||
* @return bool
|
||||
*/
|
||||
public function cancelLabel(string $shipmentNumber): bool
|
||||
{
|
||||
return $this->shipping->cancelLabel($shipmentNumber);
|
||||
}
|
||||
/**
|
||||
* @param string $trackingNumber
|
||||
* @return array
|
||||
*/
|
||||
public function track(string $trackingNumber): array
|
||||
{
|
||||
return $this->tracking->fetchStatus($trackingNumber);
|
||||
}
|
||||
/**
|
||||
* @param array $returnData
|
||||
* @return array
|
||||
*/
|
||||
public function createReturn(array $returnData): array
|
||||
{
|
||||
return $this->returns->createReturn($returnData);
|
||||
}
|
||||
}
|
||||
55
packages/acme-laravel-dhl/src/DhlServiceProvider.php
Normal file
55
packages/acme-laravel-dhl/src/DhlServiceProvider.php
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
<?php
|
||||
|
||||
namespace Acme\Dhl;
|
||||
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
|
||||
class DhlServiceProvider extends ServiceProvider
|
||||
{
|
||||
public function register(): void
|
||||
{
|
||||
$this->mergeConfigFrom(__DIR__ . '/../config/dhl.php', 'dhl');
|
||||
|
||||
// Bind DhlClient as a singleton
|
||||
$this->app->singleton(Support\DhlClient::class, function ($app) {
|
||||
return new Support\DhlClient(
|
||||
config('dhl.base_url'),
|
||||
config('dhl.api_key'),
|
||||
config('dhl.username'),
|
||||
config('dhl.password')
|
||||
);
|
||||
});
|
||||
|
||||
// Bind services as singletons
|
||||
$this->app->singleton(Services\ShippingService::class, function ($app) {
|
||||
return new Services\ShippingService($app->make(Support\DhlClient::class));
|
||||
});
|
||||
|
||||
$this->app->singleton(Services\TrackingService::class, function ($app) {
|
||||
return new Services\TrackingService($app->make(Support\DhlClient::class));
|
||||
});
|
||||
|
||||
$this->app->singleton(Services\ReturnsService::class, function ($app) {
|
||||
return new Services\ReturnsService($app->make(Support\DhlClient::class));
|
||||
});
|
||||
|
||||
// Bind the main manager class
|
||||
$this->app->singleton(DhlManager::class, function ($app) {
|
||||
return new DhlManager(
|
||||
$app->make(Support\DhlClient::class),
|
||||
$app->make(Services\ShippingService::class),
|
||||
$app->make(Services\TrackingService::class),
|
||||
$app->make(Services\ReturnsService::class)
|
||||
);
|
||||
});
|
||||
}
|
||||
public function boot(): void
|
||||
{
|
||||
$this->publishes([__DIR__ . '/../config/dhl.php' => config_path('dhl.php')], 'dhl-config');
|
||||
$this->publishes([__DIR__ . '/../database/migrations/' => database_path('migrations')], 'dhl-migrations');
|
||||
$this->loadMigrationsFrom(__DIR__ . '/../database/migrations');
|
||||
if (config('dhl.webhook.enabled')) {
|
||||
$this->loadRoutesFrom(__DIR__ . '/../routes/api.php');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
<?php
|
||||
|
||||
namespace Acme\Dhl\Exceptions;
|
||||
|
||||
class DhlApiException extends \Exception
|
||||
{
|
||||
//
|
||||
}
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
<?php
|
||||
|
||||
namespace Acme\Dhl\Exceptions;
|
||||
|
||||
class DhlAuthenticationException extends DhlApiException
|
||||
{
|
||||
//
|
||||
}
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
<?php
|
||||
|
||||
namespace Acme\Dhl\Exceptions;
|
||||
|
||||
class DhlValidationException extends DhlApiException
|
||||
{
|
||||
//
|
||||
}
|
||||
13
packages/acme-laravel-dhl/src/Facades/DHL.php
Normal file
13
packages/acme-laravel-dhl/src/Facades/DHL.php
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
<?php
|
||||
|
||||
namespace Acme\Dhl\Facades;
|
||||
|
||||
use Illuminate\Support\Facades\Facade;
|
||||
|
||||
class DHL extends Facade
|
||||
{
|
||||
protected static function getFacadeAccessor()
|
||||
{
|
||||
return \Acme\Dhl\DhlManager::class;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,43 @@
|
|||
<?php
|
||||
|
||||
namespace Acme\Dhl\Http\Controllers;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Routing\Controller;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Acme\Dhl\Models\DhlShipment;
|
||||
use Acme\Dhl\Models\DhlTrackingEvent;
|
||||
|
||||
class TrackingWebhookController extends Controller
|
||||
{
|
||||
public function handle(Request $r)
|
||||
{
|
||||
$secret = config('dhl.webhook.secret');
|
||||
if ($secret && $r->header('X-DHL-Secret') !== $secret) {
|
||||
abort(401, 'Invalid webhook secret');
|
||||
}
|
||||
$payload = $r->all();
|
||||
Log::info('DHL webhook received', ['payload' => $payload]);
|
||||
$events = data_get($payload, 'events', []);
|
||||
foreach ($events as $e) {
|
||||
$tracking = $e['trackingNumber'] ?? null;
|
||||
if (!$tracking) continue;
|
||||
$s = DhlShipment::firstOrCreate(['dhl_shipment_no' => $tracking], ['carrier' => 'dhl', 'status' => 'unknown',]);
|
||||
DhlTrackingEvent::create(['shipment_id' => $s->id, 'status_code' => $e['statusCode'] ?? null, 'status_text' => $e['status'] ?? null, 'location' => data_get($e, 'location'), 'event_time' => $e['timestamp'] ?? now(), 'raw' => $e,]);
|
||||
$statusCode = $e['statusCode'] ?? '';
|
||||
$newStatus = match (strtolower($statusCode)) {
|
||||
'delivered' => 'delivered',
|
||||
'returned' => 'returned',
|
||||
'exception', 'failed' => 'failed',
|
||||
'in_transit', 'transit' => 'in_transit',
|
||||
default => $s->status
|
||||
};
|
||||
if ($newStatus !== $s->status) {
|
||||
$s->status = $newStatus;
|
||||
$s->save();
|
||||
Log::info('Webhook updated shipment status', ['tracking' => $tracking, 'newStatus' => $newStatus]);
|
||||
}
|
||||
}
|
||||
return response()->json(['ok' => true]);
|
||||
}
|
||||
}
|
||||
29
packages/acme-laravel-dhl/src/Jobs/CreateReturnLabelJob.php
Normal file
29
packages/acme-laravel-dhl/src/Jobs/CreateReturnLabelJob.php
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
<?php
|
||||
|
||||
namespace Acme\Dhl\Jobs;
|
||||
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Foundation\Bus\Dispatchable;
|
||||
use Illuminate\Queue\InteractsWithQueue;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use Acme\Dhl\Services\ReturnsService;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
class CreateReturnLabelJob implements ShouldQueue
|
||||
{
|
||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||
|
||||
public function __construct(public array $returnData) {}
|
||||
|
||||
public function handle(ReturnsService $service): void
|
||||
{
|
||||
try {
|
||||
$service->createReturn($this->returnData);
|
||||
Log::info('Queued return label creation successful');
|
||||
} catch (\Exception $e) {
|
||||
Log::error('Queued return label creation failed', ['error' => $e->getMessage()]);
|
||||
$this->fail($e);
|
||||
}
|
||||
}
|
||||
}
|
||||
29
packages/acme-laravel-dhl/src/Jobs/CreateShipmentJob.php
Normal file
29
packages/acme-laravel-dhl/src/Jobs/CreateShipmentJob.php
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
<?php
|
||||
|
||||
namespace Acme\Dhl\Jobs;
|
||||
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Foundation\Bus\Dispatchable;
|
||||
use Illuminate\Queue\InteractsWithQueue;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use Acme\Dhl\Services\ShippingService;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
class CreateShipmentJob implements ShouldQueue
|
||||
{
|
||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||
|
||||
public function __construct(public array $orderData) {}
|
||||
|
||||
public function handle(ShippingService $service): void
|
||||
{
|
||||
try {
|
||||
$service->createLabel($this->orderData);
|
||||
Log::info('Queued shipment creation successful');
|
||||
} catch (\Exception $e) {
|
||||
Log::error('Queued shipment creation failed', ['error' => $e->getMessage()]);
|
||||
$this->fail($e);
|
||||
}
|
||||
}
|
||||
}
|
||||
29
packages/acme-laravel-dhl/src/Jobs/SyncTrackingJob.php
Normal file
29
packages/acme-laravel-dhl/src/Jobs/SyncTrackingJob.php
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
<?php
|
||||
|
||||
namespace Acme\Dhl\Jobs;
|
||||
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Foundation\Bus\Dispatchable;
|
||||
use Illuminate\Queue\InteractsWithQueue;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use Acme\Dhl\Services\TrackingService;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
class SyncTrackingJob implements ShouldQueue
|
||||
{
|
||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||
public function __construct(public array $trackingNumbers) {}
|
||||
public function handle(TrackingService $t): void
|
||||
{
|
||||
foreach ($this->trackingNumbers as $trackingNumber) {
|
||||
try {
|
||||
$t->fetchStatus($trackingNumber);
|
||||
Log::info('Synced tracking', ['trackingNumber' => $trackingNumber]);
|
||||
} catch (\Exception $e) {
|
||||
Log::error('Tracking sync failed', ['trackingNumber' => $trackingNumber, 'error' => $e->getMessage()]);
|
||||
$this->fail($e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
149
packages/acme-laravel-dhl/src/Models/DhlShipment.php
Normal file
149
packages/acme-laravel-dhl/src/Models/DhlShipment.php
Normal file
|
|
@ -0,0 +1,149 @@
|
|||
<?php
|
||||
|
||||
namespace Acme\Dhl\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use App\Models\ShoppingOrder;
|
||||
|
||||
/**
|
||||
* DHL Shipment Model for both outbound shipments and returns
|
||||
*/
|
||||
class DhlShipment extends Model
|
||||
{
|
||||
protected $table = 'dhl_package_shipments';
|
||||
|
||||
protected $fillable = [
|
||||
'order_id',
|
||||
'dhl_shipment_no',
|
||||
'routing_code',
|
||||
'type',
|
||||
'related_shipment_id',
|
||||
'product_code',
|
||||
'billing_number',
|
||||
'weight_kg',
|
||||
'label_format',
|
||||
'label_path',
|
||||
'status',
|
||||
'tracking_status',
|
||||
'last_tracked_at',
|
||||
'api_response_data'
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'api_response_data' => 'array',
|
||||
'last_tracked_at' => 'datetime',
|
||||
'weight_kg' => 'decimal:3'
|
||||
];
|
||||
|
||||
public const STATUS_MAP = [
|
||||
'pre-transit' => 'created',
|
||||
'transit' => 'in_transit',
|
||||
'out_for_delivery' => 'out_for_delivery',
|
||||
'delivered' => 'delivered',
|
||||
'exception' => 'exception',
|
||||
'returned' => 'returned',
|
||||
'failed' => 'failed',
|
||||
'unknown' => 'unknown'
|
||||
];
|
||||
|
||||
/**
|
||||
* Get the tracking events for this shipment
|
||||
*/
|
||||
public function trackingEvents(): HasMany
|
||||
{
|
||||
return $this->hasMany(DhlTrackingEvent::class, 'shipment_id');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the related shopping order for this shipment
|
||||
*/
|
||||
public function shoppingOrder(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(ShoppingOrder::class, 'order_id');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the related shipment (for returns)
|
||||
*/
|
||||
public function relatedShipment(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(self::class, 'related_shipment_id');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get returns for this shipment (if this is an outbound shipment)
|
||||
*/
|
||||
public function returns(): HasMany
|
||||
{
|
||||
return $this->hasMany(self::class, 'related_shipment_id');
|
||||
}
|
||||
|
||||
/**
|
||||
* Scope for outbound shipments
|
||||
*/
|
||||
public function scopeOutbound($query)
|
||||
{
|
||||
return $query->where('type', 'outbound');
|
||||
}
|
||||
|
||||
/**
|
||||
* Scope for return shipments
|
||||
*/
|
||||
public function scopeReturns($query)
|
||||
{
|
||||
return $query->where('type', 'return');
|
||||
}
|
||||
|
||||
/**
|
||||
* Scope for shipments by order
|
||||
*/
|
||||
public function scopeForOrder($query, int $orderId)
|
||||
{
|
||||
return $query->where('order_id', $orderId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if this is a return shipment
|
||||
*/
|
||||
public function isReturn(): bool
|
||||
{
|
||||
return $this->type === 'return';
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if this is an outbound shipment
|
||||
*/
|
||||
public function isOutbound(): bool
|
||||
{
|
||||
return $this->type === 'outbound';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the latest tracking status
|
||||
*/
|
||||
public function getLatestStatus(): ?string
|
||||
{
|
||||
return $this->trackingEvents()
|
||||
->orderBy('event_time', 'desc')
|
||||
->first()
|
||||
?->status_text;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if shipment can be canceled
|
||||
*/
|
||||
public function canCancel(): bool
|
||||
{
|
||||
return in_array($this->status, ['created', 'pending']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if shipment is delivered
|
||||
*/
|
||||
public function isDelivered(): bool
|
||||
{
|
||||
return $this->status === 'delivered';
|
||||
}
|
||||
}
|
||||
52
packages/acme-laravel-dhl/src/Models/DhlTrackingEvent.php
Normal file
52
packages/acme-laravel-dhl/src/Models/DhlTrackingEvent.php
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
<?php
|
||||
|
||||
namespace Acme\Dhl\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
/**
|
||||
* DHL Tracking Event Model for detailed tracking history
|
||||
*/
|
||||
class DhlTrackingEvent extends Model
|
||||
{
|
||||
protected $table = 'dhl_package_tracking_events';
|
||||
|
||||
protected $fillable = [
|
||||
'shipment_id',
|
||||
'status_code',
|
||||
'status_text',
|
||||
'location',
|
||||
'event_time',
|
||||
'raw'
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'raw' => 'array',
|
||||
'event_time' => 'datetime'
|
||||
];
|
||||
|
||||
/**
|
||||
* Get the shipment this event belongs to
|
||||
*/
|
||||
public function shipment(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(DhlShipment::class, 'shipment_id');
|
||||
}
|
||||
|
||||
/**
|
||||
* Scope for events ordered by time
|
||||
*/
|
||||
public function scopeOrderedByTime($query, string $direction = 'desc')
|
||||
{
|
||||
return $query->orderBy('event_time', $direction);
|
||||
}
|
||||
|
||||
/**
|
||||
* Scope for recent events
|
||||
*/
|
||||
public function scopeRecent($query, int $days = 30)
|
||||
{
|
||||
return $query->where('event_time', '>=', now()->subDays($days));
|
||||
}
|
||||
}
|
||||
10
packages/acme-laravel-dhl/src/Models/ReturnLabel.php
Normal file
10
packages/acme-laravel-dhl/src/Models/ReturnLabel.php
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
<?php
|
||||
|
||||
namespace Acme\Dhl\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class ReturnLabel extends Model
|
||||
{
|
||||
protected $fillable = ['order_id', 'dhl_return_no', 'label_path'];
|
||||
}
|
||||
19
packages/acme-laravel-dhl/src/Models/Shipment.php
Normal file
19
packages/acme-laravel-dhl/src/Models/Shipment.php
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
<?php
|
||||
|
||||
namespace Acme\Dhl\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class Shipment extends Model
|
||||
{
|
||||
protected $fillable = ['order_id', 'carrier', 'dhl_shipment_no', 'product_code', 'billing_number', 'weight_kg', 'status', 'label_format', 'label_path', 'meta'];
|
||||
protected $casts = ['meta' => 'array',];
|
||||
public function labels()
|
||||
{
|
||||
return $this->hasMany(ShipmentLabel::class);
|
||||
}
|
||||
public function events()
|
||||
{
|
||||
return $this->hasMany(TrackingEvent::class);
|
||||
}
|
||||
}
|
||||
14
packages/acme-laravel-dhl/src/Models/ShipmentLabel.php
Normal file
14
packages/acme-laravel-dhl/src/Models/ShipmentLabel.php
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
<?php
|
||||
|
||||
namespace Acme\Dhl\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class ShipmentLabel extends Model
|
||||
{
|
||||
protected $fillable = ['shipment_id', 'format', 'path'];
|
||||
public function shipment()
|
||||
{
|
||||
return $this->belongsTo(Shipment::class);
|
||||
}
|
||||
}
|
||||
16
packages/acme-laravel-dhl/src/Models/TrackingEvent.php
Normal file
16
packages/acme-laravel-dhl/src/Models/TrackingEvent.php
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
<?php
|
||||
|
||||
namespace Acme\Dhl\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class TrackingEvent extends Model
|
||||
{
|
||||
public $timestamps = false;
|
||||
protected $fillable = ['shipment_id', 'status_code', 'status_text', 'location', 'event_time', 'raw'];
|
||||
protected $casts = ['raw' => 'array', 'event_time' => 'datetime',];
|
||||
public function shipment()
|
||||
{
|
||||
return $this->belongsTo(Shipment::class);
|
||||
}
|
||||
}
|
||||
180
packages/acme-laravel-dhl/src/Services/ReturnsService.php
Normal file
180
packages/acme-laravel-dhl/src/Services/ReturnsService.php
Normal file
|
|
@ -0,0 +1,180 @@
|
|||
<?php
|
||||
|
||||
namespace Acme\Dhl\Services;
|
||||
|
||||
use Acme\Dhl\Support\DhlClient;
|
||||
use Acme\Dhl\Models\DhlShipment;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use InvalidArgumentException;
|
||||
use Exception;
|
||||
use Acme\Dhl\Jobs\CreateReturnLabelJob;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
|
||||
/**
|
||||
* DHL Returns Service for creating and managing return labels
|
||||
*/
|
||||
class ReturnsService
|
||||
{
|
||||
public function __construct(protected DhlClient $client) {}
|
||||
|
||||
/**
|
||||
* Create a return label for a shipment
|
||||
*
|
||||
* @param array $returnData Return shipment data
|
||||
* @return array Return label details including number and path
|
||||
* @throws InvalidArgumentException When required data is missing
|
||||
* @throws Exception When API request fails
|
||||
*/
|
||||
public function createReturn(array $returnData): array
|
||||
{
|
||||
$validatedData = $this->validateReturnData($returnData);
|
||||
if (config('dhl.use_queue')) {
|
||||
CreateReturnLabelJob::dispatch($validatedData);
|
||||
return ['queued' => true];
|
||||
}
|
||||
|
||||
$payload = $this->buildReturnPayload($returnData);
|
||||
$response = $this->client->request('post', '/parcel/de/returns/v1/labels', $payload);
|
||||
|
||||
$returnNumber = $this->extractReturnNumber($response);
|
||||
$labelBase64 = $this->extractLabelData($response);
|
||||
|
||||
$labelPath = $this->saveLabelFile($returnNumber, $labelBase64, $payload['labelFormat']);
|
||||
$returnShipment = $this->createReturnRecord($returnData, $returnNumber, $labelPath, $response);
|
||||
|
||||
Log::info('Created return label', ['returnNumber' => $returnNumber]);
|
||||
|
||||
return [
|
||||
'returnNumber' => $returnNumber,
|
||||
'label_path' => $labelPath,
|
||||
'returnShipment' => $returnShipment,
|
||||
'raw' => $response
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get return shipment by return number
|
||||
*
|
||||
* @param string $returnNumber DHL return number
|
||||
* @return DhlShipment|null Return shipment model or null if not found
|
||||
*/
|
||||
public function getReturnShipment(string $returnNumber): ?DhlShipment
|
||||
{
|
||||
return DhlShipment::where('dhl_shipment_no', $returnNumber)
|
||||
->where('type', 'return')
|
||||
->first();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all return shipments for an order
|
||||
*
|
||||
* @param int $orderId Order ID
|
||||
* @return \Illuminate\Database\Eloquent\Collection Return shipments collection
|
||||
*/
|
||||
public function getOrderReturns(int $orderId): \Illuminate\Database\Eloquent\Collection
|
||||
{
|
||||
return DhlShipment::where('order_id', $orderId)
|
||||
->where('type', 'return')
|
||||
->get();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get returns for a specific outbound shipment
|
||||
*
|
||||
* @param int $shipmentId Original outbound shipment ID
|
||||
* @return \Illuminate\Database\Eloquent\Collection Related return shipments
|
||||
*/
|
||||
public function getShipmentReturns(int $shipmentId): \Illuminate\Database\Eloquent\Collection
|
||||
{
|
||||
return DhlShipment::where('related_shipment_id', $shipmentId)
|
||||
->where('type', 'return')
|
||||
->get();
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate required return data
|
||||
*/
|
||||
private function validateReturnData(array $data): array
|
||||
{
|
||||
$validator = Validator::make($data, [
|
||||
'original_shipment_id' => 'nullable|integer|exists:dhl_shipments,id',
|
||||
'weight_kg' => 'nullable|numeric|min:0.1',
|
||||
'shipper' => 'required|array',
|
||||
'shipper.name' => 'required|string|max:50',
|
||||
// Add similar rules as in ShippingService
|
||||
'consignee' => 'required|array',
|
||||
'consignee.name' => 'required|string|max:50',
|
||||
// ... more fields
|
||||
]);
|
||||
if ($validator->fails()) {
|
||||
throw new InvalidArgumentException($validator->errors()->first());
|
||||
}
|
||||
if (empty(config('dhl.billing_number'))) {
|
||||
throw new InvalidArgumentException('DHL billing number must be configured');
|
||||
}
|
||||
return $validator->validated();
|
||||
}
|
||||
|
||||
/**
|
||||
* Build DHL return API payload
|
||||
*/
|
||||
private function buildReturnPayload(array $returnData): array
|
||||
{
|
||||
return [
|
||||
'labelFormat' => $returnData['label_format'] ?? 'PDF',
|
||||
'shipper' => $returnData['shipper'],
|
||||
'consignee' => $returnData['consignee'],
|
||||
'billingNumber' => config('dhl.billing_number')
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract return number from API response
|
||||
*/
|
||||
private function extractReturnNumber(array $response): ?string
|
||||
{
|
||||
return data_get($response, 'returnShipmentNo')
|
||||
?? data_get($response, 'shipments.0.returnShipmentNo');
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract base64 label data from API response
|
||||
*/
|
||||
private function extractLabelData(array $response): ?string
|
||||
{
|
||||
return data_get($response, 'label');
|
||||
}
|
||||
|
||||
/**
|
||||
* Save return label file to storage
|
||||
*/
|
||||
private function saveLabelFile(?string $returnNumber, ?string $labelBase64, string $format): ?string
|
||||
{
|
||||
if (!$labelBase64 || !$returnNumber) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$path = 'dhl/returns/' . $returnNumber . '.' . strtolower($format);
|
||||
Storage::disk('local')->put($path, base64_decode($labelBase64));
|
||||
|
||||
return $path;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create return shipment database record
|
||||
*/
|
||||
private function createReturnRecord(array $returnData, ?string $returnNumber, ?string $labelPath, array $response): DhlShipment
|
||||
{
|
||||
return DhlShipment::create([
|
||||
'order_id' => $returnData['order_id'] ?? null,
|
||||
'dhl_shipment_no' => $returnNumber,
|
||||
'type' => 'return',
|
||||
'related_shipment_id' => $returnData['original_shipment_id'] ?? null,
|
||||
'label_format' => $returnData['label_format'] ?? 'PDF',
|
||||
'label_path' => $labelPath,
|
||||
'status' => 'created',
|
||||
'api_response_data' => $response
|
||||
]);
|
||||
}
|
||||
}
|
||||
480
packages/acme-laravel-dhl/src/Services/ShippingService.php
Normal file
480
packages/acme-laravel-dhl/src/Services/ShippingService.php
Normal file
|
|
@ -0,0 +1,480 @@
|
|||
<?php
|
||||
|
||||
namespace Acme\Dhl\Services;
|
||||
|
||||
use Acme\Dhl\Support\DhlClient;
|
||||
use Acme\Dhl\Models\DhlShipment;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
use InvalidArgumentException;
|
||||
use Exception;
|
||||
use Acme\Dhl\Jobs\CreateShipmentJob;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
/**
|
||||
* DHL Shipping Service for creating and managing shipment labels
|
||||
*/
|
||||
class ShippingService
|
||||
{
|
||||
public function __construct(protected DhlClient $client) {}
|
||||
|
||||
/**
|
||||
* Create a new DHL shipment label
|
||||
*
|
||||
* @param array $orderData Order and shipping data
|
||||
* @return array Shipment details including number and label path
|
||||
* @throws InvalidArgumentException When required data is missing
|
||||
* @throws Exception When API request fails
|
||||
*/
|
||||
public function createLabel(array $orderData): array
|
||||
{
|
||||
Log::info('createLabel', $orderData);
|
||||
$validatedData = $this->validateOrderData($orderData);
|
||||
if (config('dhl.use_queue')) {
|
||||
CreateShipmentJob::dispatch($validatedData);
|
||||
return ['queued' => true];
|
||||
}
|
||||
|
||||
return DB::transaction(function () use ($validatedData) {
|
||||
$payload = $this->buildShipmentPayload($validatedData);
|
||||
|
||||
// Debug logging: Log the exact payload being sent to DHL API
|
||||
Log::info('[DHL API] Sending payload to DHL', [
|
||||
'endpoint' => '/parcel/de/shipping/v2/orders',
|
||||
'payload' => $payload,
|
||||
'payload_json' => json_encode($payload, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE)
|
||||
]);
|
||||
|
||||
try {
|
||||
// Build query parameters for print format
|
||||
$query = array_filter([
|
||||
'printFormat' => $validatedData['print_format'] ?? null,
|
||||
'retourePrintFormat' => $validatedData['retoure_print_format'] ?? null,
|
||||
]);
|
||||
|
||||
$response = $this->client->request('post', '/parcel/de/shipping/v2/orders', $payload, $query);
|
||||
|
||||
Log::info('[DHL API] Response received', [
|
||||
'response' => $response
|
||||
]);
|
||||
} catch (Exception $e) {
|
||||
Log::error('[DHL API] Request failed', [
|
||||
'error' => $e->getMessage(),
|
||||
'payload' => $payload
|
||||
]);
|
||||
throw $e;
|
||||
}
|
||||
|
||||
$shipmentNumber = $this->extractShipmentNumber($response);
|
||||
$labelBase64 = $this->extractLabelData($response);
|
||||
|
||||
$shipment = $this->createShipmentRecord($validatedData, $payload, $response, $shipmentNumber);
|
||||
$labelPath = $this->saveLabelFile($shipment, $labelBase64, $payload['shipments'][0]['print']['format']);
|
||||
|
||||
Log::info('Created shipment label', ['shipmentNumber' => $shipmentNumber]);
|
||||
|
||||
return [
|
||||
'shipmentNumber' => $shipmentNumber,
|
||||
'label_path' => $labelPath,
|
||||
'shipment' => $shipment,
|
||||
'raw' => $response
|
||||
];
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancel an existing DHL shipment
|
||||
*
|
||||
* @param string $shipmentNumber DHL shipment number
|
||||
* @return bool Success status
|
||||
* @throws Exception When cancellation fails
|
||||
*/
|
||||
public function cancelLabel(string $shipmentNumber): bool
|
||||
{
|
||||
if (empty($shipmentNumber)) {
|
||||
throw new InvalidArgumentException('Shipment number is required');
|
||||
}
|
||||
$shipment = DhlShipment::where('dhl_shipment_no', $shipmentNumber)->first();
|
||||
if (!$shipment || !$shipment->canCancel()) {
|
||||
throw new InvalidArgumentException('Shipment cannot be canceled');
|
||||
}
|
||||
$this->client->request('delete', "/parcel/de/shipping/v2/orders/{$shipmentNumber}");
|
||||
$shipment->update(['status' => 'canceled']);
|
||||
Log::info('Canceled shipment', ['shipmentNumber' => $shipmentNumber]);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate required order data according to DHL API v2 specification
|
||||
*/
|
||||
private function validateOrderData(array $data): array
|
||||
{
|
||||
// Pre-process German addresses before validation
|
||||
$data = $this->preprocessAddresses($data);
|
||||
|
||||
$validator = Validator::make($data, [
|
||||
'order_id' => 'nullable|integer',
|
||||
'weight_kg' => 'required|numeric|min:0.1|max:31.5', // DHL weight limit
|
||||
'product_code' => 'nullable|string|in:V01PAK,V53PAK,V62WP,V07PAK',
|
||||
'label_format' => 'nullable|string|in:PDF,ZPL',
|
||||
'print_format' => 'nullable|string', // Values like A4, 910-300-700 etc.
|
||||
'retoure_print_format' => 'nullable|string',
|
||||
|
||||
// Shipper validation (sender)
|
||||
'shipper' => 'required|array',
|
||||
'shipper.name' => 'required|string|max:50',
|
||||
'shipper.name2' => 'nullable|string|max:50',
|
||||
'shipper.street' => 'required|string|max:50',
|
||||
'shipper.houseNumber' => 'required|string|max:10',
|
||||
'shipper.postalCode' => 'required|string|max:10',
|
||||
'shipper.city' => 'required|string|max:50',
|
||||
'shipper.country' => 'required|string|size:2', // ISO 3166-1 alpha-2
|
||||
'shipper.email' => 'nullable|email|max:100',
|
||||
'shipper.phone' => 'nullable|string|max:20',
|
||||
|
||||
// Consignee validation (recipient)
|
||||
'consignee' => 'required|array',
|
||||
'consignee.name' => 'required|string|max:50',
|
||||
'consignee.name2' => 'nullable|string|max:50',
|
||||
'consignee.street' => 'required|string|max:50',
|
||||
'consignee.houseNumber' => 'required|string|max:10',
|
||||
'consignee.postalCode' => 'required|string|max:10',
|
||||
'consignee.city' => 'required|string|max:50',
|
||||
'consignee.country' => 'required|string|size:2',
|
||||
'consignee.email' => 'nullable|email|max:100',
|
||||
'consignee.phone' => 'nullable|string|max:20',
|
||||
|
||||
// Optional dimensions
|
||||
'dimensions' => 'nullable|array',
|
||||
'dimensions.length' => 'nullable|numeric|min:1|max:120',
|
||||
'dimensions.width' => 'nullable|numeric|min:1|max:60',
|
||||
'dimensions.height' => 'nullable|numeric|min:1|max:60',
|
||||
|
||||
// Optional services and reference
|
||||
'services' => 'nullable|array',
|
||||
'reference' => 'nullable|string|max:35', // DHL reference field limit
|
||||
]);
|
||||
|
||||
if ($validator->fails()) {
|
||||
throw new InvalidArgumentException($validator->errors()->first());
|
||||
}
|
||||
|
||||
return $validator->validated();
|
||||
}
|
||||
|
||||
/**
|
||||
* Preprocess addresses to extract house numbers from street field
|
||||
*/
|
||||
private function preprocessAddresses(array $data): array
|
||||
{
|
||||
// Process shipper address
|
||||
if (isset($data['shipper'])) {
|
||||
$data['shipper'] = $this->parseAddressFields($data['shipper']);
|
||||
}
|
||||
|
||||
// Process consignee address
|
||||
if (isset($data['consignee'])) {
|
||||
$data['consignee'] = $this->parseAddressFields($data['consignee']);
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse German address to extract street and house number
|
||||
*/
|
||||
private function parseAddressFields(array $addressData): array
|
||||
{
|
||||
// If houseNumber is already provided, use it
|
||||
if (!empty($addressData['houseNumber'])) {
|
||||
return $addressData;
|
||||
}
|
||||
|
||||
// If no houseNumber provided, try to parse from street
|
||||
if (empty($addressData['street'])) {
|
||||
return $addressData;
|
||||
}
|
||||
|
||||
$street = trim($addressData['street']);
|
||||
$parsed = $this->parseGermanAddress($street);
|
||||
|
||||
// Only update if we successfully parsed both parts
|
||||
if ($parsed['street'] && $parsed['houseNumber']) {
|
||||
$addressData['street'] = $parsed['street'];
|
||||
$addressData['houseNumber'] = $parsed['houseNumber'];
|
||||
|
||||
Log::info('Parsed German address', [
|
||||
'original' => $street,
|
||||
'parsed_street' => $parsed['street'],
|
||||
'parsed_houseNumber' => $parsed['houseNumber']
|
||||
]);
|
||||
} elseif (!$parsed['houseNumber']) {
|
||||
// If we can't parse house number, use a default
|
||||
$addressData['houseNumber'] = '1';
|
||||
|
||||
Log::warning('Could not parse house number from address, using default', [
|
||||
'street' => $street
|
||||
]);
|
||||
}
|
||||
|
||||
return $addressData;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse German address string to extract street name and house number
|
||||
* Handles formats like: "Musterstraße 123", "Muster Str. 123a", "Am Markt 1-3"
|
||||
*/
|
||||
private function parseGermanAddress(string $address): array
|
||||
{
|
||||
$address = trim($address);
|
||||
|
||||
// Pattern to match German addresses
|
||||
// Captures everything before the last word that contains numbers
|
||||
$patterns = [
|
||||
// "Musterstraße 123a" -> street: "Musterstraße", number: "123a"
|
||||
'/^(.+?)\s+([0-9]+[a-zA-Z]?)\s*$/',
|
||||
// "Am Markt 1-3" -> street: "Am Markt", number: "1-3"
|
||||
'/^(.+?)\s+([0-9]+[-\/][0-9]+[a-zA-Z]?)\s*$/',
|
||||
// "Muster Str. 123" -> street: "Muster Str.", number: "123"
|
||||
'/^(.+?)\s+([0-9]+)\s*$/',
|
||||
];
|
||||
|
||||
foreach ($patterns as $pattern) {
|
||||
if (preg_match($pattern, $address, $matches)) {
|
||||
return [
|
||||
'street' => trim($matches[1]),
|
||||
'houseNumber' => trim($matches[2])
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
// If no pattern matches, return original street with empty house number
|
||||
return [
|
||||
'street' => $address,
|
||||
'houseNumber' => null
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Build DHL API v2 payload from order data
|
||||
*
|
||||
* Structure follows official DHL API v2 createOrders specification:
|
||||
* https://developer.dhl.com/api-reference/parcel-de-shipping-post-parcel-germany-v2
|
||||
*/
|
||||
private function buildShipmentPayload(array $orderData): array
|
||||
{
|
||||
$productCode = $orderData['product_code'] ?? config('dhl.default_product', 'V01PAK');
|
||||
$billingNumber = $this->getBillingNumberForProduct($productCode);
|
||||
|
||||
$payload = [
|
||||
'profile' => config('dhl.profile', 'STANDARD_GRUPPENPROFIL'),
|
||||
'shipments' => [[
|
||||
'product' => $productCode,
|
||||
'billingNumber' => $billingNumber,
|
||||
|
||||
// Shipper information (sender) - separate street and house number as per official spec
|
||||
'shipper' => array_filter([
|
||||
'name1' => $orderData['shipper']['name'] ?? '',
|
||||
'name2' => !empty($orderData['shipper']['name2']) ? $orderData['shipper']['name2'] : null,
|
||||
'addressStreet' => $orderData['shipper']['street'] ?? '',
|
||||
'addressHouse' => $orderData['shipper']['houseNumber'] ?? null,
|
||||
'postalCode' => $orderData['shipper']['postalCode'] ?? '',
|
||||
'city' => $orderData['shipper']['city'] ?? '',
|
||||
'country' => $this->convertCountryCode($orderData['shipper']['country'] ?? 'DE'),
|
||||
'email' => !empty($orderData['shipper']['email']) ? $orderData['shipper']['email'] : null,
|
||||
'phone' => !empty($orderData['shipper']['phone']) ? $orderData['shipper']['phone'] : null,
|
||||
], function ($value) {
|
||||
return $value !== null;
|
||||
}),
|
||||
|
||||
// Consignee information (recipient) - separate street and house number as per official spec
|
||||
'consignee' => array_filter([
|
||||
'name1' => $orderData['consignee']['name'] ?? '',
|
||||
'name2' => !empty($orderData['consignee']['name2']) ? $orderData['consignee']['name2'] : null,
|
||||
'addressStreet' => $orderData['consignee']['street'] ?? '',
|
||||
'addressHouse' => $orderData['consignee']['houseNumber'] ?? null,
|
||||
'postalCode' => $orderData['consignee']['postalCode'] ?? '',
|
||||
'city' => $orderData['consignee']['city'] ?? '',
|
||||
'country' => $this->convertCountryCode($orderData['consignee']['country'] ?? 'DE'),
|
||||
'email' => !empty($orderData['consignee']['email']) ? $orderData['consignee']['email'] : null,
|
||||
'phone' => !empty($orderData['consignee']['phone']) ? $orderData['consignee']['phone'] : null,
|
||||
], function ($value) {
|
||||
return $value !== null;
|
||||
}),
|
||||
|
||||
'details' => [
|
||||
'weight' => [
|
||||
'value' => ($orderData['weight_kg'] ?? 1.0) * 1000, // Convert kg to grams
|
||||
'uom' => 'g'
|
||||
]
|
||||
],
|
||||
|
||||
'print' => [
|
||||
'format' => $orderData['label_format'] ?? config('dhl.label_format', 'PDF')
|
||||
]
|
||||
]]
|
||||
];
|
||||
|
||||
// Add dimensions if provided (convert cm to mm)
|
||||
if (!empty($orderData['dimensions'])) {
|
||||
$payload['shipments'][0]['details']['dim'] = [
|
||||
'uom' => 'mm',
|
||||
'length' => ($orderData['dimensions']['length'] ?? 30) * 10, // cm to mm
|
||||
'width' => ($orderData['dimensions']['width'] ?? 25) * 10, // cm to mm
|
||||
'height' => ($orderData['dimensions']['height'] ?? 10) * 10, // cm to mm
|
||||
];
|
||||
}
|
||||
|
||||
// Add custom reference if provided
|
||||
if (!empty($orderData['reference'])) {
|
||||
$payload['shipments'][0]['refNo'] = $orderData['reference'];
|
||||
}
|
||||
|
||||
return $payload;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert 2-letter country code to 3-letter country code for DHL API
|
||||
*/
|
||||
private function convertCountryCode(string $countryCode): string
|
||||
{
|
||||
$countryMap = [
|
||||
'DE' => 'DEU',
|
||||
'AT' => 'AUT',
|
||||
'CH' => 'CHE',
|
||||
'US' => 'USA',
|
||||
'GB' => 'GBR',
|
||||
'FR' => 'FRA',
|
||||
'IT' => 'ITA',
|
||||
'ES' => 'ESP',
|
||||
'NL' => 'NLD',
|
||||
'BE' => 'BEL',
|
||||
'PL' => 'POL',
|
||||
'CZ' => 'CZE',
|
||||
'DK' => 'DNK',
|
||||
'SE' => 'SWE',
|
||||
'NO' => 'NOR',
|
||||
];
|
||||
|
||||
return $countryMap[strtoupper($countryCode)] ?? 'DEU';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the correct billing number for the given product code
|
||||
*/
|
||||
private function getBillingNumberForProduct(string $productCode): string
|
||||
{
|
||||
// Try to get account number from config by product code
|
||||
$accountNumber = config("dhl.account_numbers.{$productCode}");
|
||||
|
||||
if ($accountNumber) {
|
||||
return $accountNumber;
|
||||
}
|
||||
|
||||
// Try to get from admin settings via Setting model
|
||||
try {
|
||||
$settingKey = 'dhl_account_' . strtolower($productCode);
|
||||
$accountNumber = \App\Models\Setting::getContentBySlug($settingKey);
|
||||
if ($accountNumber) {
|
||||
return $accountNumber;
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
Log::warning('Could not load DHL account number from settings', [
|
||||
'product_code' => $productCode,
|
||||
'setting_key' => $settingKey,
|
||||
'error' => $e->getMessage()
|
||||
]);
|
||||
}
|
||||
|
||||
// Fallback to default billing number
|
||||
$defaultBillingNumber = config('dhl.billing_number') ?: config('dhl.account_numbers.default');
|
||||
|
||||
Log::warning('Using default billing number for product code', [
|
||||
'product_code' => $productCode,
|
||||
'billing_number' => $defaultBillingNumber
|
||||
]);
|
||||
|
||||
return $defaultBillingNumber;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract shipment number from API response
|
||||
*/
|
||||
private function extractShipmentNumber(array $response): ?string
|
||||
{
|
||||
return data_get($response, 'items.0.shipmentNo')
|
||||
?? data_get($response, 'shipments.0.shipmentNo')
|
||||
?? data_get($response, 'shipmentNo')
|
||||
?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract base64 label data from API response
|
||||
*/
|
||||
private function extractLabelData(array $response): ?string
|
||||
{
|
||||
return data_get($response, 'items.0.label.b64')
|
||||
?? data_get($response, 'shipments.0.label.b64')
|
||||
?? data_get($response, 'shipments.0.label')
|
||||
?? data_get($response, 'label');
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract routing code from API response
|
||||
*/
|
||||
private function extractRoutingCode(array $response): ?string
|
||||
{
|
||||
return data_get($response, 'items.0.routingCode')
|
||||
?? data_get($response, 'shipments.0.routingCode')
|
||||
?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create shipment database record
|
||||
*/
|
||||
private function createShipmentRecord(array $orderData, array $payload, array $response, ?string $shipmentNumber): DhlShipment
|
||||
{
|
||||
return DhlShipment::create([
|
||||
'order_id' => $orderData['order_id'] ?? null,
|
||||
'dhl_shipment_no' => $shipmentNumber,
|
||||
'routing_code' => $this->extractRoutingCode($response),
|
||||
'type' => 'outbound',
|
||||
'product_code' => $payload['shipments'][0]['product'],
|
||||
'billing_number' => $payload['shipments'][0]['billingNumber'],
|
||||
'weight_kg' => $payload['shipments'][0]['details']['weight']['value'] / 1000,
|
||||
'status' => 'created',
|
||||
'label_format' => $payload['shipments'][0]['print']['format'],
|
||||
'label_path' => null,
|
||||
'api_response_data' => $response
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Save label file to storage and create label record
|
||||
*/
|
||||
private function saveLabelFile(DhlShipment $shipment, ?string $labelBase64, string $format): ?string
|
||||
{
|
||||
if (!$labelBase64) {
|
||||
Log::warning('No label data received for shipment', ['shipmentId' => $shipment->id]);
|
||||
return null;
|
||||
}
|
||||
$path = 'dhl/labels/' . $shipment->dhl_shipment_no . '.' . strtolower($format);
|
||||
$success = false;
|
||||
for ($attempt = 1; $attempt <= 3; $attempt++) {
|
||||
try {
|
||||
Storage::disk('local')->put($path, base64_decode($labelBase64));
|
||||
$success = true;
|
||||
break;
|
||||
} catch (\Exception $e) {
|
||||
Log::warning('Storage put failed, retrying', ['attempt' => $attempt, 'error' => $e->getMessage()]);
|
||||
usleep(1000000); // 1 second in microseconds
|
||||
}
|
||||
}
|
||||
if (!$success) {
|
||||
throw new \Exception('Failed to save label after 3 attempts');
|
||||
}
|
||||
$shipment->update(['label_path' => $path]);
|
||||
return $path;
|
||||
}
|
||||
}
|
||||
180
packages/acme-laravel-dhl/src/Services/TrackingService.php
Normal file
180
packages/acme-laravel-dhl/src/Services/TrackingService.php
Normal file
|
|
@ -0,0 +1,180 @@
|
|||
<?php
|
||||
|
||||
namespace Acme\Dhl\Services;
|
||||
|
||||
use Acme\Dhl\Support\DhlClient;
|
||||
use Acme\Dhl\Models\DhlShipment;
|
||||
use Acme\Dhl\Models\DhlTrackingEvent;
|
||||
use InvalidArgumentException;
|
||||
use Exception;
|
||||
use Acme\Dhl\Jobs\SyncTrackingJob;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
/**
|
||||
* DHL Tracking Service for fetching and managing tracking information
|
||||
*/
|
||||
class TrackingService
|
||||
{
|
||||
public function __construct(protected DhlClient $client) {}
|
||||
|
||||
/**
|
||||
* Fetch tracking status for a shipment
|
||||
*
|
||||
* @param string $trackingNumber DHL tracking number
|
||||
* @return array Tracking data from DHL API
|
||||
* @throws InvalidArgumentException When tracking number is empty
|
||||
* @throws Exception When API request fails
|
||||
*/
|
||||
public function fetchStatus(string $trackingNumber): array
|
||||
{
|
||||
if (empty($trackingNumber)) {
|
||||
throw new InvalidArgumentException('Tracking number is required');
|
||||
}
|
||||
if (config('dhl.use_queue')) {
|
||||
SyncTrackingJob::dispatch($trackingNumber);
|
||||
return ['queued' => true];
|
||||
}
|
||||
|
||||
$response = $this->client->request('get', '/post-tracking/api/shipments', [], [
|
||||
'trackingNumber' => $trackingNumber
|
||||
]);
|
||||
Log::info('Fetched tracking status', ['trackingNumber' => $trackingNumber]);
|
||||
|
||||
$events = data_get($response, 'shipments.0.events', []);
|
||||
$shipment = $this->findOrCreateShipment($trackingNumber);
|
||||
|
||||
$this->updateTrackingEvents($shipment, $events);
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update tracking status for multiple shipments
|
||||
*
|
||||
* @param array $trackingNumbers Array of tracking numbers to update
|
||||
* @return array Results for each tracking number
|
||||
*/
|
||||
public function updateMultipleStatus(array $trackingNumbers): array
|
||||
{
|
||||
$results = [];
|
||||
|
||||
foreach ($trackingNumbers as $trackingNumber) {
|
||||
try {
|
||||
$results[$trackingNumber] = $this->fetchStatus($trackingNumber);
|
||||
} catch (Exception $e) {
|
||||
Log::error('Tracking update failed', ['trackingNumber' => $trackingNumber, 'error' => $e->getMessage()]);
|
||||
$results[$trackingNumber] = ['error' => $e->getMessage()];
|
||||
}
|
||||
}
|
||||
|
||||
return $results;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get latest tracking status for a shipment
|
||||
*
|
||||
* @param string $trackingNumber DHL tracking number
|
||||
* @return array|null Latest tracking event or null if not found
|
||||
*/
|
||||
public function getLatestStatus(string $trackingNumber): ?array
|
||||
{
|
||||
$shipment = DhlShipment::where('dhl_shipment_no', $trackingNumber)->first();
|
||||
|
||||
if (!$shipment) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$latestEvent = $shipment->trackingEvents()
|
||||
->orderBy('event_time', 'desc')
|
||||
->first();
|
||||
|
||||
return $latestEvent ? $latestEvent->toArray() : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find existing shipment or create new one
|
||||
*/
|
||||
private function findOrCreateShipment(string $trackingNumber): DhlShipment
|
||||
{
|
||||
return DB::transaction(function () use ($trackingNumber) {
|
||||
return DhlShipment::firstOrCreate(
|
||||
['dhl_shipment_no' => $trackingNumber],
|
||||
[
|
||||
'type' => 'outbound',
|
||||
'status' => 'unknown'
|
||||
]
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Update tracking events for a shipment
|
||||
*/
|
||||
private function updateTrackingEvents(DhlShipment $shipment, array $events): void
|
||||
{
|
||||
foreach ($events as $eventData) {
|
||||
$this->createOrUpdateTrackingEvent($shipment, $eventData);
|
||||
}
|
||||
|
||||
// Update shipment status based on latest event
|
||||
$this->updateShipmentStatus($shipment, $events);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create or update a tracking event
|
||||
*/
|
||||
private function createOrUpdateTrackingEvent(DhlShipment $shipment, array $eventData): void
|
||||
{
|
||||
DhlTrackingEvent::updateOrCreate(
|
||||
[
|
||||
'shipment_id' => $shipment->id,
|
||||
'status_code' => $eventData['statusCode'] ?? null,
|
||||
'event_time' => $eventData['timestamp'] ?? null
|
||||
],
|
||||
[
|
||||
'status_text' => $eventData['status'] ?? null,
|
||||
'location' => $this->extractLocation($eventData),
|
||||
'raw' => $eventData
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract location from event data
|
||||
*/
|
||||
private function extractLocation(array $eventData): ?string
|
||||
{
|
||||
return data_get($eventData, 'location.address.addressLocality')
|
||||
?? data_get($eventData, 'location')
|
||||
?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update shipment status based on latest tracking event
|
||||
*/
|
||||
private function updateShipmentStatus(DhlShipment $shipment, array $events): void
|
||||
{
|
||||
if (empty($events)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Sort events by timestamp to get the latest
|
||||
usort($events, function ($a, $b) {
|
||||
return strtotime($b['timestamp'] ?? '0') <=> strtotime($a['timestamp'] ?? '0');
|
||||
});
|
||||
|
||||
$latestEvent = $events[0];
|
||||
$statusCode = $latestEvent['statusCode'] ?? null;
|
||||
|
||||
// Map DHL status codes to our internal status
|
||||
$status = DhlShipment::STATUS_MAP[$statusCode] ?? 'unknown';
|
||||
|
||||
$shipment->update([
|
||||
'status' => $status,
|
||||
'tracking_status' => $latestEvent['status'] ?? null,
|
||||
'last_tracked_at' => now()
|
||||
]);
|
||||
Log::info('Updated shipment status', ['shipmentId' => $shipment->id, 'newStatus' => $status]);
|
||||
}
|
||||
}
|
||||
198
packages/acme-laravel-dhl/src/Support/DhlClient.php
Normal file
198
packages/acme-laravel-dhl/src/Support/DhlClient.php
Normal file
|
|
@ -0,0 +1,198 @@
|
|||
<?php
|
||||
|
||||
namespace Acme\Dhl\Support;
|
||||
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Illuminate\Http\Client\RequestException;
|
||||
use Illuminate\Http\Client\ConnectionException;
|
||||
use Acme\Dhl\Exceptions\DhlApiException;
|
||||
use Acme\Dhl\Exceptions\DhlAuthenticationException;
|
||||
use Acme\Dhl\Exceptions\DhlValidationException;
|
||||
use Exception;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
/**
|
||||
* DHL API Client for handling HTTP requests to DHL services
|
||||
*/
|
||||
class DhlClient
|
||||
{
|
||||
public function __construct(
|
||||
protected string $baseUrl,
|
||||
protected ?string $apiKey,
|
||||
protected ?string $username,
|
||||
protected ?string $password
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Make HTTP request to DHL API
|
||||
*
|
||||
* @param string $method HTTP method (get, post, put, delete)
|
||||
* @param string $uri API endpoint URI
|
||||
* @param array $payload Request body data
|
||||
* @param array $query Query parameters
|
||||
* @return array Response data as array
|
||||
* @throws Exception When API request fails or returns error
|
||||
*/
|
||||
public function request(string $method, string $uri, array $payload = [], array $query = []): array
|
||||
{
|
||||
try {
|
||||
$request = Http::baseUrl($this->baseUrl)
|
||||
->withHeaders($this->buildHeaders())
|
||||
->timeout(30)
|
||||
->retry(3, 300, function ($exception, $attempt) {
|
||||
if ($exception instanceof RequestException && $exception->response->status() === 429) {
|
||||
$delay = min(1000000 * $attempt, 10000000); // Max 10 seconds
|
||||
usleep($delay); // Microseconds
|
||||
return true;
|
||||
}
|
||||
return $exception instanceof ConnectionException ||
|
||||
($exception instanceof RequestException && in_array($exception->response->status(), [500, 502, 503, 504]));
|
||||
}, false);
|
||||
|
||||
// Add authentication if provided
|
||||
if ($this->username && $this->password) {
|
||||
$request = $request->withBasicAuth($this->username, $this->password);
|
||||
}
|
||||
|
||||
// Make the request
|
||||
$response = match (strtolower($method)) {
|
||||
'get' => $request->get($uri, $query),
|
||||
'post' => $request->post($uri . '?' . http_build_query($query), $payload),
|
||||
'put' => $request->put($uri, $payload),
|
||||
'delete' => $request->delete($uri),
|
||||
default => throw new Exception("Unsupported HTTP method: {$method}")
|
||||
};
|
||||
|
||||
// Handle response
|
||||
if ($response->failed()) {
|
||||
// Log additional debug info for 400 errors
|
||||
if ($response->status() === 400) {
|
||||
Log::error('[DHL API] HTTP 400 Bad Request Details', [
|
||||
'method' => $method,
|
||||
'uri' => $uri,
|
||||
'request_payload' => $payload,
|
||||
'response_body' => $response->body(),
|
||||
'response_json' => $response->json(),
|
||||
'status_code' => $response->status(),
|
||||
'headers' => $response->headers()
|
||||
]);
|
||||
}
|
||||
|
||||
$this->handleErrorResponse($response, $method, $uri);
|
||||
}
|
||||
|
||||
return $response->json() ?? [];
|
||||
} catch (RequestException $e) {
|
||||
Log::error('DHL API request failed', ['error' => $e->getMessage(), 'method' => $method, 'uri' => $uri]);
|
||||
throw new DhlApiException("DHL API request failed: {$e->getMessage()}", $e->getCode(), $e);
|
||||
} catch (Exception $e) {
|
||||
Log::error('DHL API error', ['error' => $e->getMessage()]);
|
||||
// Re-throw our own exceptions
|
||||
if ($e instanceof DhlApiException) {
|
||||
throw $e;
|
||||
}
|
||||
throw new DhlApiException("DHL API error: {$e->getMessage()}", $e->getCode(), $e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Build HTTP headers for DHL API requests
|
||||
*/
|
||||
private function buildHeaders(): array
|
||||
{
|
||||
$headers = [
|
||||
'Accept' => 'application/json',
|
||||
'Content-Type' => 'application/json',
|
||||
'User-Agent' => 'acme-laravel-dhl/1.0'
|
||||
];
|
||||
|
||||
if ($this->apiKey) {
|
||||
$headers['dhl-api-key'] = $this->apiKey;
|
||||
}
|
||||
|
||||
return $headers;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle error responses from DHL API
|
||||
*/
|
||||
private function handleErrorResponse($response, string $method, string $uri): void
|
||||
{
|
||||
$status = $response->status();
|
||||
$body = $response->json();
|
||||
|
||||
$errorMessage = $this->extractErrorMessage($body) ?? "HTTP {$status} error";
|
||||
|
||||
match (true) {
|
||||
$status === 401 => throw new DhlAuthenticationException("DHL API authentication failed: {$errorMessage}"),
|
||||
$status === 403 => throw new DhlAuthenticationException("DHL API access forbidden: {$errorMessage}"),
|
||||
$status === 404 => throw new DhlApiException("DHL API endpoint not found: {$method} {$uri}"),
|
||||
$status === 422 => throw new DhlValidationException("DHL API validation error: {$errorMessage}"),
|
||||
$status === 429 => throw new DhlApiException("DHL API rate limit exceeded. Please try again later."),
|
||||
$status >= 500 => throw new DhlApiException("DHL API server error: {$errorMessage}"),
|
||||
default => throw new DhlApiException("DHL API error ({$status}): {$errorMessage}")
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract error message from DHL API response
|
||||
*/
|
||||
private function extractErrorMessage(?array $body): ?string
|
||||
{
|
||||
if (!$body) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Try different possible error message fields
|
||||
return $body['message']
|
||||
?? $body['error']
|
||||
?? $body['detail']
|
||||
?? data_get($body, 'errors.0.message')
|
||||
?? data_get($body, 'error.message')
|
||||
?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Test connection to DHL API
|
||||
*
|
||||
* @return bool True if connection successful
|
||||
*/
|
||||
public function testConnection(): bool
|
||||
{
|
||||
try {
|
||||
// Check basic connectivity and authentication
|
||||
// Use the simplest possible endpoint to minimize permission issues
|
||||
$response = Http::baseUrl($this->baseUrl)
|
||||
->withHeaders($this->buildHeaders())
|
||||
->withBasicAuth($this->username, $this->password)
|
||||
->timeout(10)
|
||||
->get('/');
|
||||
|
||||
// If we get any response (even 404), the connection and auth are working
|
||||
if ($response->status() === 401) {
|
||||
Log::error('DHL API authentication failed: Invalid username/password');
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($response->status() === 403 && str_contains($response->body(), 'api-key')) {
|
||||
Log::error('DHL API authentication failed: Invalid API key');
|
||||
return false;
|
||||
}
|
||||
|
||||
// Any other response code (including 404, 403 for endpoint access) means connection works
|
||||
Log::info('DHL API connection test successful', [
|
||||
'status' => $response->status(),
|
||||
'has_api_key' => !empty($this->apiKey),
|
||||
'has_credentials' => !empty($this->username) && !empty($this->password)
|
||||
]);
|
||||
|
||||
return true;
|
||||
} catch (Exception $e) {
|
||||
Log::error('DHL API connection test failed', [
|
||||
'error' => $e->getMessage(),
|
||||
'base_url' => $this->baseUrl
|
||||
]);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
114
packages/acme-laravel-dhl/tests/Unit/AddressParserTest.php
Normal file
114
packages/acme-laravel-dhl/tests/Unit/AddressParserTest.php
Normal file
|
|
@ -0,0 +1,114 @@
|
|||
<?php
|
||||
|
||||
namespace Tests\Unit;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Acme\Dhl\Services\ShippingService;
|
||||
use ReflectionClass;
|
||||
use ReflectionMethod;
|
||||
|
||||
/**
|
||||
* Test cases for German address parsing functionality
|
||||
*/
|
||||
class AddressParserTest extends TestCase
|
||||
{
|
||||
private ShippingService $shippingService;
|
||||
private ReflectionMethod $parseMethod;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
// Mock DhlClient for testing
|
||||
$mockClient = $this->createMock(\Acme\Dhl\Support\DhlClient::class);
|
||||
$this->shippingService = new ShippingService($mockClient);
|
||||
|
||||
// Make private method accessible for testing
|
||||
$reflection = new ReflectionClass($this->shippingService);
|
||||
$this->parseMethod = $reflection->getMethod('parseGermanAddress');
|
||||
$this->parseMethod->setAccessible(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test standard German address formats
|
||||
*/
|
||||
public function testStandardGermanAddresses(): void
|
||||
{
|
||||
$testCases = [
|
||||
// Basic format: Street + Number
|
||||
'Musterstraße 123' => ['street' => 'Musterstraße', 'houseNumber' => '123'],
|
||||
'Hauptstraße 1' => ['street' => 'Hauptstraße', 'houseNumber' => '1'],
|
||||
|
||||
// With letter suffix
|
||||
'Bahnhofstraße 45a' => ['street' => 'Bahnhofstraße', 'houseNumber' => '45a'],
|
||||
'Kirchgasse 12B' => ['street' => 'Kirchgasse', 'houseNumber' => '12B'],
|
||||
|
||||
// Multi-word street names
|
||||
'Am Markt 7' => ['street' => 'Am Markt', 'houseNumber' => '7'],
|
||||
'An der Kirche 23' => ['street' => 'An der Kirche', 'houseNumber' => '23'],
|
||||
'Karl-Marx-Straße 156' => ['street' => 'Karl-Marx-Straße', 'houseNumber' => '156'],
|
||||
|
||||
// Range numbers
|
||||
'Lindenstraße 1-3' => ['street' => 'Lindenstraße', 'houseNumber' => '1-3'],
|
||||
'Gartenweg 12/14' => ['street' => 'Gartenweg', 'houseNumber' => '12/14'],
|
||||
|
||||
// With abbreviations
|
||||
'Muster Str. 99' => ['street' => 'Muster Str.', 'houseNumber' => '99'],
|
||||
'Berliner Pl. 5' => ['street' => 'Berliner Pl.', 'houseNumber' => '5'],
|
||||
];
|
||||
|
||||
foreach ($testCases as $input => $expected) {
|
||||
$result = $this->parseMethod->invoke($this->shippingService, $input);
|
||||
|
||||
$this->assertEquals($expected['street'], $result['street'], "Failed parsing street for: {$input}");
|
||||
$this->assertEquals($expected['houseNumber'], $result['houseNumber'], "Failed parsing house number for: {$input}");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Test edge cases and problematic addresses
|
||||
*/
|
||||
public function testEdgeCases(): void
|
||||
{
|
||||
$edgeCases = [
|
||||
// No house number - should return null for houseNumber
|
||||
'Musterstraße' => ['street' => 'Musterstraße', 'houseNumber' => null],
|
||||
'Am Markt' => ['street' => 'Am Markt', 'houseNumber' => null],
|
||||
|
||||
// Empty string
|
||||
'' => ['street' => '', 'houseNumber' => null],
|
||||
|
||||
// Only number
|
||||
'123' => ['street' => '123', 'houseNumber' => null],
|
||||
|
||||
// Multiple spaces
|
||||
'Muster Straße 123' => ['street' => 'Muster Straße', 'houseNumber' => '123'],
|
||||
];
|
||||
|
||||
foreach ($edgeCases as $input => $expected) {
|
||||
$result = $this->parseMethod->invoke($this->shippingService, $input);
|
||||
|
||||
$this->assertEquals($expected['street'], $result['street'], "Failed parsing street for edge case: {$input}");
|
||||
$this->assertEquals($expected['houseNumber'], $result['houseNumber'], "Failed parsing house number for edge case: {$input}");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Test international addresses (should not be parsed)
|
||||
*/
|
||||
public function testInternationalAddresses(): void
|
||||
{
|
||||
$internationalCases = [
|
||||
// English-style addresses
|
||||
'123 Main Street' => ['street' => '123 Main Street', 'houseNumber' => null],
|
||||
'456 Oak Avenue' => ['street' => '456 Oak Avenue', 'houseNumber' => null],
|
||||
];
|
||||
|
||||
foreach ($internationalCases as $input => $expected) {
|
||||
$result = $this->parseMethod->invoke($this->shippingService, $input);
|
||||
|
||||
$this->assertEquals($expected['street'], $result['street'], "International address should not be parsed: {$input}");
|
||||
$this->assertEquals($expected['houseNumber'], $result['houseNumber'], "International address should have null house number: {$input}");
|
||||
}
|
||||
}
|
||||
}
|
||||
15
packages/laravel-collective-spatie-html-parser/.editorconfig
Normal file
15
packages/laravel-collective-spatie-html-parser/.editorconfig
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
root = true
|
||||
|
||||
[*]
|
||||
charset = utf-8
|
||||
end_of_line = lf
|
||||
insert_final_newline = true
|
||||
indent_style = space
|
||||
indent_size = 4
|
||||
trim_trailing_whitespace = true
|
||||
|
||||
[*.md]
|
||||
trim_trailing_whitespace = false
|
||||
|
||||
[*.yml]
|
||||
indent_size = 2
|
||||
1
packages/laravel-collective-spatie-html-parser/.gitignore
vendored
Normal file
1
packages/laravel-collective-spatie-html-parser/.gitignore
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
/vendor
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
# Contributing to laravel-collective-spatie-html-parser
|
||||
|
||||
Thank you for considering contributing to this project! Your contributions help maintain and improve this Laravel compatibility layer, ensuring a smooth transition between the obsolete laravel-collective library to the maintained laravel-spatie-html. This library also helps to that old proyects to be updated to the new versions of Laravel.
|
||||
|
||||
## How to Contribute
|
||||
|
||||
### 1. Submitting a Pull Request (PR)
|
||||
- Always create your PR against the `develop` branch.
|
||||
- Use a **descriptive name** and provide a **clear description** of what the PR does (bug fix, new feature, improvement, etc.).
|
||||
- Once reviewed and approved, I will merge it into `master` and handle the deployment.
|
||||
|
||||
### 2. Reporting Issues
|
||||
If you encounter a bug but are unable to fix it yourself, you can still help by reporting it:
|
||||
- Open a new **Issue** in the repository.
|
||||
- Provide a **detailed description** of the issue.
|
||||
- Include **steps to reproduce the problem** so others can verify and fix it.
|
||||
|
||||
## Code Style Guidelines
|
||||
- Follow Laravel’s coding standards and best practices.
|
||||
- Keep the code clean and well-documented.
|
||||
- Ensure that new features or fixes do not break existing functionality.
|
||||
|
||||
Thank you for your support and contributions!
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
The MIT License (MIT)
|
||||
|
||||
Copyright © 2024 Christian Albán
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
66
packages/laravel-collective-spatie-html-parser/README.md
Normal file
66
packages/laravel-collective-spatie-html-parser/README.md
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
# Laravel Collective to Spatie Laravel HTML Adapter
|
||||
|
||||
This package serves as an adapter to help projects that depend on the obsolete `laravel-collective/html` library. It provides an interface that uses the same syntax as the `Form` class of `laravel-collective/html` to create HTML elements. Under the hood, it utilizes the `spatie/laravel-html` library, which is actively maintained, to generate the HTML elements. This allows projects to update to newer Laravel versions without changing the HTML creation syntax.
|
||||
|
||||
## Features
|
||||
- Zero configuration needed, works out of the box.
|
||||
- Uses the same syntax as `laravel-collective/html`.
|
||||
- Leverages the actively maintained `spatie/laravel-html` library.
|
||||
|
||||
## Available Methods
|
||||
The following methods can be used and are located in the `src/FormAdapter` directory:
|
||||
|
||||
- `checkbox($name, $value = 1, $checked = null, $options = [])`
|
||||
- `open(array $options = [])`
|
||||
- `label($name, $value = null, $options = [], $escape_html = true)`
|
||||
- `text($name, $value = null, $options = [])`
|
||||
- `password($name, $options = [])`
|
||||
- `select($name, $list = [], $selected = null, array $selectAttributes = [], array $optionsAttributes = [], array $optgroupsAttributes = [])`
|
||||
- `radio($name, $value = null, $checked = null, $options = [])`
|
||||
- `submit($value = null, $options = [])`
|
||||
- `close()`
|
||||
- `input($type, $name, $value = null, $options = [])`
|
||||
- `search($name, $value = null, $options = [])`
|
||||
- `model($model, array $options = [])`
|
||||
- `hidden($name, $value = null, $options = [])`
|
||||
- `email($name, $value = null, $options = [])`
|
||||
- `tel($name, $value = null, $options = [])`
|
||||
- `number($name, $value = null, $options = [])`
|
||||
- `date($name, $value = null, $options = [])`
|
||||
- `datetime($name, $value = null, $options = [])`
|
||||
- `datetimeLocal($name, $value = null, $options = [])`
|
||||
- `time($name, $value = null, $options = [])`
|
||||
- `url($name, $value = null, $options = [])`
|
||||
- `file($name, $options = [])`
|
||||
- `textarea($name, $value = null, $options = [])`
|
||||
- `reset($value, $attributes = [])`
|
||||
- `image($url, $name = null, $attributes = [])`
|
||||
- `color($name, $value = null, $options = [])`
|
||||
- `button($value = null, $options = [])`
|
||||
|
||||
## Installation
|
||||
|
||||
To install the package, use composer:
|
||||
|
||||
```sh
|
||||
composer require alban/laravel-collective-spatie-html-parser
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
The methods listed above can be used in the same way as you would use the Form class from laravel-collective. Here is an example in a Blade template:
|
||||
|
||||
```php
|
||||
{{-- Using the FormAdapter class in a Blade template --}}
|
||||
{!! Form::text('relationship', $item->client->agent_relationship, ['required', 'class' => 'form-control input-sm']) !!}
|
||||
```
|
||||
|
||||
For more examples, please refer to the source code in the `src/FormAdapter.php` class file.
|
||||
|
||||
## License
|
||||
|
||||
This package is open-sourced software licensed under the [MIT license](https://opensource.org/licenses/MIT).
|
||||
|
||||
## Contributing
|
||||
|
||||
You are welcome to contribute to this project. Please refer to the [contributing guidelines](CONTRIBUTING.md) for more information.
|
||||
39
packages/laravel-collective-spatie-html-parser/composer.json
Normal file
39
packages/laravel-collective-spatie-html-parser/composer.json
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
{
|
||||
"name": "alban/laravel-collective-spatie-html-parser",
|
||||
"description": "Adapter class that allows use spatie/laravel-html for old projects that were using the abanondated laravelcollective/html package, and allow to update to new versions of laravel",
|
||||
"type": "library",
|
||||
"require": {
|
||||
"php": "^8.3 | ^8.2 | ^8.0 | ^7.4",
|
||||
"spatie/laravel-html": "^3.12",
|
||||
"illuminate/support": "^8.0|^9.0|^10.0|^11.0|^12.0"
|
||||
},
|
||||
"license": "MIT",
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Alban\\LaravelCollectiveSpatieHtmlParser\\": "src/"
|
||||
}
|
||||
},
|
||||
"extra": {
|
||||
"laravel": {
|
||||
"providers": [
|
||||
"Alban\\LaravelCollectiveSpatieHtmlParser\\ServiceProvider"
|
||||
],
|
||||
"aliases": {
|
||||
"Form": "Alban\\LaravelCollectiveSpatieHtmlParser\\FormFacade"
|
||||
}
|
||||
}
|
||||
},
|
||||
"authors": [
|
||||
{
|
||||
"name": "Christian Albán",
|
||||
"email": "christianalbanctdb1d@gmail.com"
|
||||
}
|
||||
],
|
||||
|
||||
"minimum-stability": "stable",
|
||||
"config": {
|
||||
"allow-plugins": {
|
||||
"kylekatarnls/update-helper": true
|
||||
}
|
||||
}
|
||||
}
|
||||
3375
packages/laravel-collective-spatie-html-parser/composer.lock
generated
Normal file
3375
packages/laravel-collective-spatie-html-parser/composer.lock
generated
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -0,0 +1,218 @@
|
|||
<?php
|
||||
|
||||
namespace Alban\LaravelCollectiveSpatieHtmlParser;
|
||||
|
||||
use Alban\LaravelCollectiveSpatieHtmlParser\Traits\AttributesUtils;
|
||||
|
||||
class FormAdapter
|
||||
{
|
||||
use AttributesUtils;
|
||||
|
||||
public function checkbox($name, $value = 1, $checked = null, $options = [])
|
||||
{
|
||||
$element = \Html::checkbox($name, $checked, $value);
|
||||
|
||||
return $this->mergeOptions($element, $options);
|
||||
}
|
||||
|
||||
public function open(array $options = [])
|
||||
{
|
||||
$method = array_key_exists('method', $options) ? $options['method'] : 'POST';
|
||||
$route = array_key_exists('route', $options) ? $options['route'] : '';
|
||||
$files = array_key_exists('files', $options) ? $options['files'] : false;
|
||||
|
||||
unset($options['method'], $options['route'], $options['files']);
|
||||
|
||||
$form = \Html::getFacadeRoot();
|
||||
|
||||
if (is_array($route) && count($route)) {
|
||||
$action = array_shift($route);
|
||||
$form = $form->form($method, route($action, $route));
|
||||
} elseif ($route != null && $route != [] && $route != '') {
|
||||
$form = $form->form($method, route($route));
|
||||
} else {
|
||||
$form = $form->form($method);
|
||||
}
|
||||
|
||||
if ($files) {
|
||||
$form = $form->acceptsFiles();
|
||||
}
|
||||
|
||||
return $this->mergeOptions($form, $options)->open();
|
||||
}
|
||||
|
||||
public function label($name, $value = null, $options = [], $escape_html = true)
|
||||
{
|
||||
$element = \Html::label($value, $name);
|
||||
|
||||
return $this->mergeOptions($element, $options);
|
||||
}
|
||||
|
||||
public function text($name, $value = null, $options = [])
|
||||
{
|
||||
$element = \Html::text($name, $value);
|
||||
|
||||
return $this->mergeOptions($element, $options);
|
||||
}
|
||||
|
||||
public function password($name, $options = [])
|
||||
{
|
||||
$element = \Html::password($name);
|
||||
|
||||
return $this->mergeOptions($element, $options);
|
||||
}
|
||||
|
||||
public function select(
|
||||
$name,
|
||||
$list = [],
|
||||
$selected = null,
|
||||
array $selectAttributes = [],
|
||||
array $optionsAttributes = [],
|
||||
array $optgroupsAttributes = []
|
||||
) {
|
||||
if (isset($selectAttributes['multiple']) || in_array('multiple', $selectAttributes)) {
|
||||
$element = \Html::select($name, $list, $selected)->multiple();
|
||||
} else {
|
||||
$element = \Html::select($name, $list, $selected);
|
||||
}
|
||||
// $element = \Html::select($name, $list, $selected);
|
||||
|
||||
return $this->mergeOptions($element, $selectAttributes);
|
||||
}
|
||||
|
||||
|
||||
public function radio($name, $value = null, $checked = null, $options = [])
|
||||
{
|
||||
$element = \Html::radio($name, $checked, $value);
|
||||
|
||||
return $this->mergeOptions($element, $options);
|
||||
}
|
||||
|
||||
public function submit($value = null, $options = [])
|
||||
{
|
||||
$element = \Html::submit($value);
|
||||
|
||||
return $this->mergeOptions($element, $options);
|
||||
}
|
||||
|
||||
public function close()
|
||||
{
|
||||
\Html::endModel();
|
||||
|
||||
return \Html::form()->close();
|
||||
}
|
||||
|
||||
public function input($type, $name, $value = null, $options = [])
|
||||
{
|
||||
$element = \Html::input($type, $name, $value);
|
||||
|
||||
return $this->mergeOptions($element, $options);
|
||||
}
|
||||
|
||||
public function search($name, $value = null, $options = [])
|
||||
{
|
||||
return $this->input('search', $name, $value, $options);
|
||||
}
|
||||
|
||||
public function model($model, array $options = [])
|
||||
{
|
||||
\Html::model($model);
|
||||
|
||||
return $this->open($options);
|
||||
}
|
||||
|
||||
public function hidden($name, $value = null, $options = [])
|
||||
{
|
||||
$element = \Html::hidden($name, $value);
|
||||
|
||||
return $this->mergeOptions($element, $options);
|
||||
}
|
||||
|
||||
public function email($name, $value = null, $options = [])
|
||||
{
|
||||
$element = \Html::email($name, $value);
|
||||
|
||||
return $this->mergeOptions($element, $options);
|
||||
}
|
||||
|
||||
public function tel($name, $value = null, $options = [])
|
||||
{
|
||||
$element = \Html::tel($name, $value);
|
||||
|
||||
return $this->mergeOptions($element, $options);
|
||||
}
|
||||
|
||||
public function number($name, $value = null, $options = [])
|
||||
{
|
||||
return $this->input('number', $name, $value, $options);
|
||||
}
|
||||
|
||||
public function date($name, $value = null, $options = [])
|
||||
{
|
||||
$element = \Html::date($name, $value);
|
||||
|
||||
return $this->mergeOptions($element, $options);
|
||||
}
|
||||
|
||||
public function datetime($name, $value = null, $options = [])
|
||||
{
|
||||
return $this->input('datetime', $name, $value, $options);
|
||||
}
|
||||
|
||||
public function datetimeLocal($name, $value = null, $options = [])
|
||||
{
|
||||
return $this->input('datetime-local', $name, $value, $options);
|
||||
}
|
||||
|
||||
public function time($name, $value = null, $options = [])
|
||||
{
|
||||
$element = \Html::time($name, $value);
|
||||
|
||||
return $this->mergeOptions($element, $options);
|
||||
}
|
||||
|
||||
public function url($name, $value = null, $options = [])
|
||||
{
|
||||
return $this->input('url', $name, $value, $options);
|
||||
}
|
||||
|
||||
public function file($name, $options = [])
|
||||
{
|
||||
$element = \Html::file($name);
|
||||
|
||||
return $this->mergeOptions($element, $options);
|
||||
}
|
||||
|
||||
public function textarea($name, $value = null, $options = [])
|
||||
{
|
||||
$element = \Html::textarea($name, $value);
|
||||
|
||||
return $this->mergeOptions($element, $options);
|
||||
}
|
||||
|
||||
public function reset($value, $attributes = [])
|
||||
{
|
||||
$element = \Html::reset($value);
|
||||
|
||||
return $this->mergeOptions($element, $attributes);
|
||||
}
|
||||
|
||||
public function image($url, $name = null, $attributes = [])
|
||||
{
|
||||
$element = \Html::img($url, $name);
|
||||
|
||||
return $this->mergeOptions($element, $attributes);
|
||||
}
|
||||
|
||||
public function color($name, $value = null, $options = [])
|
||||
{
|
||||
return $this->input('color', $name, $value, $options);
|
||||
}
|
||||
|
||||
public function button($value = null, $options = [])
|
||||
{
|
||||
$element = \Html::button($value);
|
||||
|
||||
return $this->mergeOptions($element, $options);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
<?php
|
||||
|
||||
namespace Alban\LaravelCollectiveSpatieHtmlParser;
|
||||
|
||||
use Illuminate\Support\Facades\Facade;
|
||||
|
||||
/**
|
||||
* @see \Collective\Html\HtmlBuilder
|
||||
*/
|
||||
class FormFacade extends Facade
|
||||
{
|
||||
|
||||
/**
|
||||
* Get the registered name of the component.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected static function getFacadeAccessor()
|
||||
{
|
||||
return 'form';
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
<?php
|
||||
|
||||
namespace Alban\LaravelCollectiveSpatieHtmlParser;
|
||||
|
||||
use Spatie\Html\Html;
|
||||
|
||||
class HtmlAdapter extends Html {}
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
<?php
|
||||
|
||||
namespace Alban\LaravelCollectiveSpatieHtmlParser;
|
||||
|
||||
use Illuminate\Support\Facades\Facade;
|
||||
|
||||
class HtmlFacade extends Facade
|
||||
{
|
||||
/**
|
||||
* Get the registered name of the component.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected static function getFacadeAccessor(): string
|
||||
{
|
||||
return 'html';
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,46 @@
|
|||
<?php
|
||||
|
||||
namespace Alban\LaravelCollectiveSpatieHtmlParser;
|
||||
|
||||
use Illuminate\Support\ServiceProvider as BaseServiceProvider;
|
||||
|
||||
class ServiceProvider extends BaseServiceProvider
|
||||
{
|
||||
/**
|
||||
* Register bindings in the container.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function register()
|
||||
{
|
||||
$this->registerFormAdpater();
|
||||
$this->registerHtmlAdapter();
|
||||
|
||||
$this->app->alias('form', FormAdapter::class);
|
||||
$this->app->alias('html', HtmlAdapter::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Register the HTML adapter instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function registerFormAdpater()
|
||||
{
|
||||
$this->app->singleton('form', function () {
|
||||
return new FormAdapter();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Register the HTML adapter instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function registerHtmlAdapter()
|
||||
{
|
||||
$this->app->singleton('html', function () {
|
||||
return new HtmlAdapter();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
<?php
|
||||
|
||||
namespace Alban\LaravelCollectiveSpatieHtmlParser\Traits;
|
||||
|
||||
trait AttributesUtils
|
||||
{
|
||||
public function mergeOptions($element, $options = [])
|
||||
{
|
||||
$newElement = $element;
|
||||
|
||||
if (isset($options['class'])) {
|
||||
$newElement = $newElement->addClass($options['class']);
|
||||
unset($options['class']);
|
||||
}
|
||||
|
||||
if (isset($options['placeholder'])) {
|
||||
$newElement = $newElement->placeholder($options['placeholder']);
|
||||
unset($options['placeholder']);
|
||||
}
|
||||
|
||||
foreach ($options as $key => $value) {
|
||||
if (!$value) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (is_numeric($key)) {
|
||||
$newElement = $newElement->attribute($value, '');
|
||||
continue;
|
||||
}
|
||||
|
||||
$newElement = $newElement->attribute($key, $value);
|
||||
}
|
||||
return $newElement;
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue