27-05-2026 Update DHL Modul v2.0
This commit is contained in:
parent
53bdba33cd
commit
036595be94
41 changed files with 3346 additions and 310 deletions
|
|
@ -3,4 +3,5 @@
|
|||
uses(Tests\TestCase::class)->in('Feature/Incentive');
|
||||
uses(Tests\TestCase::class)->in('Feature/Sys');
|
||||
uses(Tests\TestCase::class)->in('Unit/Incentive');
|
||||
uses(Tests\TestCase::class)->in('Unit/Dhl');
|
||||
uses(Tests\TestCase::class)->in('Feature/PaymentDashboard');
|
||||
|
|
|
|||
174
tests/Unit/Dhl/DhlAddressValidatorTest.php
Normal file
174
tests/Unit/Dhl/DhlAddressValidatorTest.php
Normal file
|
|
@ -0,0 +1,174 @@
|
|||
<?php
|
||||
|
||||
use App\Services\DhlAddressValidator;
|
||||
|
||||
function validDhlAddress(array $overrides = []): array
|
||||
{
|
||||
return array_merge([
|
||||
'firstname' => 'Max',
|
||||
'lastname' => 'Mustermann',
|
||||
'street' => 'Hauptstrasse',
|
||||
'house_number' => '5',
|
||||
'postal_code' => '10115',
|
||||
'city' => 'Berlin',
|
||||
'country_code' => 'DE',
|
||||
'email' => 'max@example.com',
|
||||
'phone' => '+4930123456',
|
||||
], $overrides);
|
||||
}
|
||||
|
||||
beforeEach(function () {
|
||||
config([
|
||||
'dhl.config_source' => 'env',
|
||||
'dhl.international_countries' => ['AT', 'ES'],
|
||||
]);
|
||||
});
|
||||
|
||||
it('marks a complete supported address as valid', function () {
|
||||
$result = (new DhlAddressValidator)->validate(validDhlAddress());
|
||||
|
||||
expect($result['status'])->toBe('valid')
|
||||
->and($result['can_create_label'])->toBeTrue()
|
||||
->and($result['errors'])->toBeEmpty()
|
||||
->and($result['warnings'])->toBeEmpty()
|
||||
->and($result['validation_available'])->toBeTrue()
|
||||
->and($result['validation_level'])->toBe('formal_dach')
|
||||
->and($result['validation_message'])->toBe('Formale DACH-Pruefung aktiv: Pflichtfelder, PLZ-Format, Plausibilitaet und Packstation-Regeln werden geprueft. Eine echte Adressdatenbank-/DHL-Leitcodepruefung ist nicht angebunden.');
|
||||
});
|
||||
|
||||
it('blocks unsupported destination countries', function () {
|
||||
$result = (new DhlAddressValidator)->validate(validDhlAddress([
|
||||
'country_code' => 'FR',
|
||||
'postal_code' => '75001',
|
||||
]));
|
||||
|
||||
expect($result['status'])->toBe('error')
|
||||
->and($result['can_create_label'])->toBeFalse()
|
||||
->and($result['errors'])->toContain('DHL-Versand in das Zielland FR ist aktuell nicht freigegeben.');
|
||||
});
|
||||
|
||||
it('blocks invalid postal code formats for enabled countries', function () {
|
||||
$result = (new DhlAddressValidator)->validate(validDhlAddress([
|
||||
'country_code' => 'AT',
|
||||
'postal_code' => '10115',
|
||||
]));
|
||||
|
||||
expect($result['status'])->toBe('error')
|
||||
->and($result['errors'])->toContain('Oesterreichische Postleitzahl muss 4 Ziffern haben.');
|
||||
});
|
||||
|
||||
it('validates swiss postal codes when switzerland is enabled', function () {
|
||||
config([
|
||||
'dhl.config_source' => 'env',
|
||||
'dhl.international_countries' => ['AT', 'CH'],
|
||||
]);
|
||||
|
||||
$result = (new DhlAddressValidator)->validate(validDhlAddress([
|
||||
'country_code' => 'CH',
|
||||
'postal_code' => '8000',
|
||||
]));
|
||||
|
||||
expect($result['validation_available'])->toBeTrue()
|
||||
->and($result['validation_level'])->toBe('formal_dach')
|
||||
->and($result['errors'])->not->toContain('Schweizer Postleitzahl muss 4 Ziffern haben.');
|
||||
});
|
||||
|
||||
it('marks supported countries without country specific validation as basic checks', function () {
|
||||
config([
|
||||
'dhl.config_source' => 'env',
|
||||
'dhl.international_countries' => ['FR'],
|
||||
]);
|
||||
|
||||
$result = (new DhlAddressValidator)->validate(validDhlAddress([
|
||||
'country_code' => 'FR',
|
||||
'postal_code' => '75001',
|
||||
]));
|
||||
|
||||
expect($result['status'])->toBe('warning')
|
||||
->and($result['can_create_label'])->toBeTrue()
|
||||
->and($result['validation_available'])->toBeFalse()
|
||||
->and($result['validation_level'])->toBe('basic')
|
||||
->and($result['validation_message'])->toBe('Fuer dieses Zielland ist aktuell nur eine Basis-Adresspruefung verfuegbar.')
|
||||
->and($result['warnings'])->toContain('Fuer dieses Zielland ist aktuell nur eine Basis-Adresspruefung verfuegbar. Bitte Adresse manuell pruefen.');
|
||||
});
|
||||
|
||||
it('blocks implausible delivery address fields', function () {
|
||||
$result = (new DhlAddressValidator)->validate(validDhlAddress([
|
||||
'street' => '12',
|
||||
'postal_code' => '12@@@',
|
||||
'city' => '1',
|
||||
]));
|
||||
|
||||
expect($result['status'])->toBe('error')
|
||||
->and($result['can_create_label'])->toBeFalse()
|
||||
->and($result['errors'])->toContain('Straße ist zu kurz.')
|
||||
->and($result['errors'])->toContain('Straße muss Buchstaben enthalten.')
|
||||
->and($result['errors'])->toContain('Ort ist zu kurz.')
|
||||
->and($result['errors'])->toContain('Ort muss Buchstaben enthalten.')
|
||||
->and($result['errors'])->toContain('Postleitzahl enthaelt ungueltige Zeichen.');
|
||||
});
|
||||
|
||||
it('blocks placeholder delivery addresses', function () {
|
||||
$result = (new DhlAddressValidator)->validate(validDhlAddress([
|
||||
'street' => 'Teststrasse',
|
||||
'city' => 'Fakeort',
|
||||
]));
|
||||
|
||||
expect($result['status'])->toBe('error')
|
||||
->and($result['can_create_label'])->toBeFalse()
|
||||
->and($result['errors'])->toContain('Straße wirkt wie eine Test- oder Platzhalteradresse.')
|
||||
->and($result['errors'])->toContain('Ort wirkt wie eine Test- oder Platzhalterangabe.');
|
||||
});
|
||||
|
||||
it('blocks DACH addresses with house numbers without digits', function () {
|
||||
$result = (new DhlAddressValidator)->validate(validDhlAddress([
|
||||
'house_number' => 'links',
|
||||
]));
|
||||
|
||||
expect($result['status'])->toBe('error')
|
||||
->and($result['can_create_label'])->toBeFalse()
|
||||
->and($result['errors'])->toContain('Hausnummer muss fuer DACH-Adressen eine Ziffer enthalten.');
|
||||
});
|
||||
|
||||
it('allows warning-only addresses but marks them for review', function () {
|
||||
$result = (new DhlAddressValidator)->validate(validDhlAddress([
|
||||
'phone' => '',
|
||||
]));
|
||||
|
||||
expect($result['status'])->toBe('warning')
|
||||
->and($result['can_create_label'])->toBeTrue()
|
||||
->and($result['warnings'])->toContain('Telefonnummer fehlt. DHL kann Empfaenger bei Zustellproblemen eventuell nicht kontaktieren.');
|
||||
});
|
||||
|
||||
it('validates German packstation addresses', function () {
|
||||
$result = (new DhlAddressValidator)->validate(validDhlAddress([
|
||||
'street' => 'Packstation',
|
||||
'house_number' => '145',
|
||||
'postnumber' => '12345678',
|
||||
]));
|
||||
|
||||
expect($result['status'])->toBe('valid')
|
||||
->and($result['can_create_label'])->toBeTrue();
|
||||
});
|
||||
|
||||
it('blocks invalid packstation postnumbers', function () {
|
||||
$result = (new DhlAddressValidator)->validate(validDhlAddress([
|
||||
'street' => 'Packstation',
|
||||
'house_number' => '145',
|
||||
'postnumber' => 'abc',
|
||||
]));
|
||||
|
||||
expect($result['status'])->toBe('error')
|
||||
->and($result['errors'])->toContain('DHL Postnummer muss 6-10 Ziffern enthalten.');
|
||||
});
|
||||
|
||||
it('requires postnumber and locker number for packstation addresses', function () {
|
||||
$result = (new DhlAddressValidator)->validate(validDhlAddress([
|
||||
'street' => 'Packstation',
|
||||
'house_number' => '',
|
||||
'postnumber' => '',
|
||||
]));
|
||||
|
||||
expect($result['status'])->toBe('error')
|
||||
->and($result['errors'])->toContain('DHL Postnummer ist fuer Packstation/Paketbox erforderlich.');
|
||||
});
|
||||
100
tests/Unit/Dhl/DhlDataHelperReferenceTest.php
Normal file
100
tests/Unit/Dhl/DhlDataHelperReferenceTest.php
Normal file
|
|
@ -0,0 +1,100 @@
|
|||
<?php
|
||||
|
||||
use App\Models\ShoppingOrder;
|
||||
use App\Services\DhlDataHelper;
|
||||
|
||||
function dhlReferenceOrder(): ShoppingOrder
|
||||
{
|
||||
$order = new ShoppingOrder;
|
||||
$order->id = 98765;
|
||||
|
||||
return $order;
|
||||
}
|
||||
|
||||
function dhlReferenceOptions(array $overrides = []): array
|
||||
{
|
||||
return array_merge([
|
||||
'product_code' => 'V01PAK',
|
||||
'shipping_address' => [
|
||||
'firstname' => 'Max',
|
||||
'lastname' => 'Mustermann',
|
||||
'company' => '',
|
||||
'address' => 'Hauptstrasse',
|
||||
'houseNumber' => '5',
|
||||
'zipcode' => '10115',
|
||||
'city' => 'Berlin',
|
||||
'country' => (object) ['code' => 'DE'],
|
||||
'email' => 'max@example.com',
|
||||
'phone' => '+4930123456',
|
||||
'postnumber' => null,
|
||||
],
|
||||
], $overrides);
|
||||
}
|
||||
|
||||
function dhlReferenceConfig(): array
|
||||
{
|
||||
return [
|
||||
'default_product' => 'V01PAK',
|
||||
'label_format' => 'PDF',
|
||||
'dimensions' => [
|
||||
'V01PAK' => ['length' => 30, 'width' => 20, 'height' => 10],
|
||||
'default' => ['length' => 30, 'width' => 20, 'height' => 10],
|
||||
],
|
||||
'sender' => [
|
||||
'company' => 'mivita care gmbh',
|
||||
'name' => '',
|
||||
'street' => 'Leinfeld',
|
||||
'house_number' => '2',
|
||||
'postalCode' => '87755',
|
||||
'city' => 'Kirchhaslach',
|
||||
'country' => 'DE',
|
||||
'email' => 'versand@example.com',
|
||||
'phone' => '+4987654321',
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
it('uses the admin shipment reference for DHL order data', function () {
|
||||
$orderData = DhlDataHelper::prepareOrderData(
|
||||
dhlReferenceOrder(),
|
||||
1.2,
|
||||
dhlReferenceOptions(['reference' => 'Nachlieferung Mai']),
|
||||
dhlReferenceConfig()
|
||||
);
|
||||
|
||||
expect($orderData['reference'])->toBe('Nachlieferung Mai');
|
||||
});
|
||||
|
||||
it('falls back to the order reference when no admin reference is given', function () {
|
||||
$orderData = DhlDataHelper::prepareOrderData(
|
||||
dhlReferenceOrder(),
|
||||
1.2,
|
||||
dhlReferenceOptions(['reference' => '']),
|
||||
dhlReferenceConfig()
|
||||
);
|
||||
|
||||
expect($orderData['reference'])->toBe('Order-98765');
|
||||
});
|
||||
|
||||
it('normalizes the DHL reference to the API length limit', function () {
|
||||
$orderData = DhlDataHelper::prepareOrderData(
|
||||
dhlReferenceOrder(),
|
||||
1.2,
|
||||
dhlReferenceOptions(['reference' => 'Sehr lange interne Referenz fuer DHL Label Nachlieferung']),
|
||||
dhlReferenceConfig()
|
||||
);
|
||||
|
||||
expect(strlen($orderData['reference']))->toBe(35)
|
||||
->and($orderData['reference'])->toBe('Sehr lange interne Referenz fuer DH');
|
||||
});
|
||||
|
||||
it('passes the DHL mustEncode option into order data', function () {
|
||||
$orderData = DhlDataHelper::prepareOrderData(
|
||||
dhlReferenceOrder(),
|
||||
1.2,
|
||||
dhlReferenceOptions(['print_only_if_codeable' => false]),
|
||||
dhlReferenceConfig()
|
||||
);
|
||||
|
||||
expect($orderData['print_only_if_codeable'])->toBeFalse();
|
||||
});
|
||||
83
tests/Unit/Dhl/DhlProductResolverTest.php
Normal file
83
tests/Unit/Dhl/DhlProductResolverTest.php
Normal file
|
|
@ -0,0 +1,83 @@
|
|||
<?php
|
||||
|
||||
use App\Services\DhlProductResolver;
|
||||
|
||||
beforeEach(function () {
|
||||
config([
|
||||
'dhl.config_source' => 'env',
|
||||
'dhl.international_countries' => ['AT', 'ES'],
|
||||
]);
|
||||
});
|
||||
|
||||
it('resolves domestic DHL products for Germany', function () {
|
||||
$resolver = new DhlProductResolver;
|
||||
|
||||
expect($resolver->resolveForShipment('DE', 'V62KP'))->toMatchArray([
|
||||
'country_code' => 'DE',
|
||||
'dhl_country_code' => 'DEU',
|
||||
'product_code' => 'V62KP',
|
||||
]);
|
||||
});
|
||||
|
||||
it('suggests international parcel for Austria and Spain', function (string $countryCode) {
|
||||
$resolver = new DhlProductResolver;
|
||||
|
||||
expect($resolver->resolveProductCode($countryCode, null, 'V01PAK'))->toBe('V53PAK');
|
||||
})->with([
|
||||
'austria' => 'AT',
|
||||
'spain' => 'ES',
|
||||
]);
|
||||
|
||||
it('rejects unsupported destination countries', function () {
|
||||
(new DhlProductResolver)->resolveProductCode('FR', null, 'V01PAK');
|
||||
})->throws(InvalidArgumentException::class, 'DHL-Versand in das Zielland FR ist aktuell nicht freigegeben.');
|
||||
|
||||
it('uses configured international destination countries', function () {
|
||||
$previousConfigSource = config('dhl.config_source');
|
||||
$previousCountries = config('dhl.international_countries');
|
||||
|
||||
config([
|
||||
'dhl.config_source' => 'env',
|
||||
'dhl.international_countries' => ['AT', 'FR'],
|
||||
]);
|
||||
|
||||
try {
|
||||
$resolver = new DhlProductResolver;
|
||||
|
||||
expect($resolver->resolveProductCode('FR', null, 'V01PAK'))->toBe('V53PAK')
|
||||
->and($resolver->getProductSuggestionsByCountry())->toMatchArray([
|
||||
'DE' => 'V01PAK',
|
||||
'FR' => 'V53PAK',
|
||||
]);
|
||||
} finally {
|
||||
config([
|
||||
'dhl.config_source' => $previousConfigSource,
|
||||
'dhl.international_countries' => $previousCountries,
|
||||
]);
|
||||
}
|
||||
});
|
||||
|
||||
it('normalizes configurable international countries', function () {
|
||||
expect(DhlProductResolver::normalizeCountryCodeList([' at ', 'DE', 'XX', 'FR', 'AT']))->toBe(['AT', 'FR']);
|
||||
});
|
||||
|
||||
it('describes DHL product scope for preflight checks', function () {
|
||||
$resolver = new DhlProductResolver;
|
||||
|
||||
expect($resolver->getProductScope('V01PAK'))->toBe('national')
|
||||
->and($resolver->getProductScopeLabel('V01PAK'))->toBe('DHL Paket National')
|
||||
->and($resolver->getProductScope('V53PAK'))->toBe('international')
|
||||
->and($resolver->getProductScopeLabel('V53PAK'))->toBe('DHL Paket International');
|
||||
});
|
||||
|
||||
it('rejects domestic products for international shipments when explicitly selected', function () {
|
||||
(new DhlProductResolver)->resolveProductCode('AT', 'V01PAK');
|
||||
})->throws(InvalidArgumentException::class, 'Produkt V01PAK ist fuer DHL-Versand in das Zielland AT nicht erlaubt.');
|
||||
|
||||
it('does not fallback unknown countries to Germany', function () {
|
||||
(new DhlProductResolver)->toDhlCountryCode('XX');
|
||||
})->throws(InvalidArgumentException::class, 'DHL-Laendercode XX wird nicht unterstuetzt.');
|
||||
|
||||
it('requires a billing number for the resolved product', function () {
|
||||
(new DhlProductResolver)->assertBillingNumber('V53PAK', null);
|
||||
})->throws(InvalidArgumentException::class, 'Keine DHL-Abrechnungsnummer fuer Produkt V53PAK konfiguriert.');
|
||||
96
tests/Unit/Dhl/DhlShipmentStatusTest.php
Normal file
96
tests/Unit/Dhl/DhlShipmentStatusTest.php
Normal file
|
|
@ -0,0 +1,96 @@
|
|||
<?php
|
||||
|
||||
use Acme\Dhl\Models\DhlShipment;
|
||||
use App\Models\ShoppingOrder;
|
||||
use App\Services\DhlTrackingService;
|
||||
|
||||
it('normalizes legacy cancelled status to internal canceled status', function () {
|
||||
expect(DhlShipment::normalizeStatus('cancelled'))->toBe('canceled')
|
||||
->and(DhlShipment::normalizeStatus('canceled'))->toBe('canceled');
|
||||
});
|
||||
|
||||
it('translates canceled and legacy cancelled shipments consistently', function () {
|
||||
app()->setLocale('de');
|
||||
|
||||
$canceledShipment = new DhlShipment(['status' => 'canceled']);
|
||||
$legacyShipment = new DhlShipment(['status' => 'cancelled']);
|
||||
|
||||
expect($canceledShipment->getStatusTranslation())->toBe('Storniert')
|
||||
->and($legacyShipment->getStatusTranslation())->toBe('Storniert')
|
||||
->and($canceledShipment->getStatusBadgeClass())->toBe('warning')
|
||||
->and($legacyShipment->getStatusBadgeClass())->toBe('warning');
|
||||
});
|
||||
|
||||
it('returns tracking email history with latest entries first', function () {
|
||||
$shipment = new DhlShipment([
|
||||
'api_response_data' => [
|
||||
'tracking_email_history' => [
|
||||
[
|
||||
'sent_at' => '2026-05-27T08:00:00+00:00',
|
||||
'type' => 'auto',
|
||||
'status' => 'in_transit',
|
||||
'recipient_email' => 'first@example.test',
|
||||
],
|
||||
[
|
||||
'sent_at' => '2026-05-27T09:00:00+00:00',
|
||||
'type' => 'manual',
|
||||
'status' => 'out_for_delivery',
|
||||
'recipient_email' => 'second@example.test',
|
||||
],
|
||||
],
|
||||
],
|
||||
]);
|
||||
|
||||
$history = $shipment->getTrackingEmailHistory();
|
||||
|
||||
expect($history)->toHaveCount(2)
|
||||
->and($history[0]['recipient_email'])->toBe('second@example.test')
|
||||
->and($history[0]['status'])->toBe('out_for_delivery')
|
||||
->and(DhlShipment::getStatusBadgeClassFor($history[0]['status']))->toBe('primary');
|
||||
});
|
||||
|
||||
it('returns empty tracking email history for legacy shipments without history', function () {
|
||||
$shipment = new DhlShipment(['api_response_data' => ['items' => []]]);
|
||||
|
||||
expect($shipment->getTrackingEmailHistory())->toBe([]);
|
||||
});
|
||||
|
||||
it('triggers tracking emails for relevant status changes only once', function () {
|
||||
$order = new ShoppingOrder;
|
||||
|
||||
$shipment = new DhlShipment([
|
||||
'status' => 'out_for_delivery',
|
||||
'dhl_shipment_no' => '00340435065133',
|
||||
'email' => 'customer@example.test',
|
||||
]);
|
||||
$shipment->setRelation('shoppingOrder', $order);
|
||||
|
||||
expect($shipment->shouldTriggerTrackingEmail('created'))->toBeTrue()
|
||||
->and($shipment->shouldTriggerTrackingEmail('out_for_delivery'))->toBeFalse();
|
||||
|
||||
$shipment->tracking_email_sent_at = now();
|
||||
|
||||
expect($shipment->shouldTriggerTrackingEmail('created'))->toBeFalse();
|
||||
});
|
||||
|
||||
it('does not trigger tracking emails for terminal delivered status', function () {
|
||||
$order = new ShoppingOrder;
|
||||
|
||||
$shipment = new DhlShipment([
|
||||
'status' => 'delivered',
|
||||
'dhl_shipment_no' => '00340435065133',
|
||||
'email' => 'customer@example.test',
|
||||
]);
|
||||
$shipment->setRelation('shoppingOrder', $order);
|
||||
|
||||
expect($shipment->shouldTriggerTrackingEmail('in_transit'))->toBeFalse();
|
||||
});
|
||||
|
||||
it('maps DHL status variants to internal tracking statuses', function (string $dhlStatus, string $internalStatus) {
|
||||
expect(DhlTrackingService::mapDhlStatusToInternal($dhlStatus))->toBe($internalStatus);
|
||||
})->with([
|
||||
'transit' => ['transit', 'in_transit'],
|
||||
'in-transit' => ['in-transit', 'in_transit'],
|
||||
'out-for-delivery' => ['out-for-delivery', 'out_for_delivery'],
|
||||
'out_for_delivery' => ['out_for_delivery', 'out_for_delivery'],
|
||||
]);
|
||||
60
tests/Unit/Dhl/DhlShipmentWeightCalculatorTest.php
Normal file
60
tests/Unit/Dhl/DhlShipmentWeightCalculatorTest.php
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
<?php
|
||||
|
||||
use App\Models\Product;
|
||||
use App\Models\ShoppingOrder;
|
||||
use App\Models\ShoppingOrderItem;
|
||||
use App\Services\DhlShipmentWeightCalculator;
|
||||
use Illuminate\Database\Eloquent\Collection;
|
||||
|
||||
function dhlWeightOrder(int $baseWeightGrams, array $items = []): ShoppingOrder
|
||||
{
|
||||
$order = new ShoppingOrder;
|
||||
$order->weight = $baseWeightGrams;
|
||||
$order->setRelation('shopping_order_items', new Collection($items));
|
||||
|
||||
return $order;
|
||||
}
|
||||
|
||||
function dhlWeightItem(int $comp, int $qty, int $productWeightGrams): ShoppingOrderItem
|
||||
{
|
||||
$product = new Product;
|
||||
$product->weight = $productWeightGrams;
|
||||
|
||||
$item = new ShoppingOrderItem;
|
||||
$item->comp = $comp;
|
||||
$item->qty = $qty;
|
||||
$item->setRelation('product', $product);
|
||||
|
||||
return $item;
|
||||
}
|
||||
|
||||
it('uses the shopping order weight as DHL base weight', function () {
|
||||
$weight = (new DhlShipmentWeightCalculator)->calculate(dhlWeightOrder(1250));
|
||||
|
||||
expect($weight)->toBe(1.25);
|
||||
});
|
||||
|
||||
it('adds compensation product weights to the DHL weight', function () {
|
||||
$weight = (new DhlShipmentWeightCalculator)->calculate(dhlWeightOrder(500, [
|
||||
dhlWeightItem(comp: 1, qty: 2, productWeightGrams: 250),
|
||||
dhlWeightItem(comp: 0, qty: 10, productWeightGrams: 1000),
|
||||
]));
|
||||
|
||||
expect($weight)->toBe(1.0);
|
||||
});
|
||||
|
||||
it('falls back to a safe default DHL weight when no weight exists', function () {
|
||||
$weight = (new DhlShipmentWeightCalculator)->calculate(dhlWeightOrder(0));
|
||||
|
||||
expect($weight)->toBe(1.0);
|
||||
});
|
||||
|
||||
it('validates product specific DHL weight limits', function () {
|
||||
(new DhlShipmentWeightCalculator)->assertWithinProductLimit(1.001, 'V62KP');
|
||||
})->throws(InvalidArgumentException::class, 'Gewicht 1.001 kg ueberschreitet das DHL-Maximalgewicht fuer V62KP (1.0 kg).');
|
||||
|
||||
it('allows regular parcel products up to the DHL parcel limit', function () {
|
||||
(new DhlShipmentWeightCalculator)->assertWithinProductLimit(31.5, 'V01PAK');
|
||||
|
||||
expect(true)->toBeTrue();
|
||||
});
|
||||
130
tests/Unit/Dhl/ShippingServiceProductCodeTest.php
Normal file
130
tests/Unit/Dhl/ShippingServiceProductCodeTest.php
Normal file
|
|
@ -0,0 +1,130 @@
|
|||
<?php
|
||||
|
||||
use Acme\Dhl\Exceptions\DhlAddressValidationException;
|
||||
use Acme\Dhl\Services\ShippingService;
|
||||
use Acme\Dhl\Support\DhlClient;
|
||||
|
||||
function makeDhlPhaseOneShippingService(): ShippingService
|
||||
{
|
||||
return new ShippingService(new DhlClient('https://api-sandbox.dhl.com', null, null, null));
|
||||
}
|
||||
|
||||
function validDhlPhaseOneOrderData(string $productCode, string $countryCode = 'DE'): array
|
||||
{
|
||||
return [
|
||||
'order_id' => 123456,
|
||||
'weight_kg' => 0.5,
|
||||
'product_code' => $productCode,
|
||||
'label_format' => 'PDF',
|
||||
'shipper' => [
|
||||
'name' => 'mivita care gmbh',
|
||||
'street' => 'Leinfeld',
|
||||
'houseNumber' => '2',
|
||||
'postalCode' => '87755',
|
||||
'city' => 'Kirchhaslach',
|
||||
'country' => 'DE',
|
||||
],
|
||||
'consignee' => [
|
||||
'name' => 'Max Mustermann',
|
||||
'street' => 'Hauptstrasse',
|
||||
'houseNumber' => '5',
|
||||
'postalCode' => '10115',
|
||||
'city' => 'Berlin',
|
||||
'country' => $countryCode,
|
||||
],
|
||||
'reference' => 'ORDER-123456',
|
||||
];
|
||||
}
|
||||
|
||||
it('accepts DHL Kleinpaket as a product code', function () {
|
||||
$service = makeDhlPhaseOneShippingService();
|
||||
$method = new ReflectionMethod($service, 'validateOrderData');
|
||||
$method->setAccessible(true);
|
||||
|
||||
$validated = $method->invoke($service, validDhlPhaseOneOrderData('V62KP'));
|
||||
|
||||
expect($validated['product_code'])->toBe('V62KP');
|
||||
});
|
||||
|
||||
it('builds an international DHL parcel payload for Austria', function () {
|
||||
config([
|
||||
'dhl.account_numbers.V53PAK' => '63144073555301',
|
||||
'dhl.legacy.sandbox' => false,
|
||||
'dhl.legacy.test_mode' => false,
|
||||
]);
|
||||
|
||||
$service = makeDhlPhaseOneShippingService();
|
||||
$method = new ReflectionMethod($service, 'buildShipmentPayload');
|
||||
$method->setAccessible(true);
|
||||
|
||||
$payload = $method->invoke($service, validDhlPhaseOneOrderData('V53PAK', 'AT'));
|
||||
|
||||
expect($payload['shipments'][0]['product'])->toBe('V53PAK')
|
||||
->and($payload['shipments'][0]['billingNumber'])->toBe('63144073555301')
|
||||
->and($payload['shipments'][0]['consignee']['country'])->toBe('AUT');
|
||||
});
|
||||
|
||||
it('rejects unsupported countries instead of falling back to Germany', function () {
|
||||
$service = makeDhlPhaseOneShippingService();
|
||||
$method = new ReflectionMethod($service, 'buildShipmentPayload');
|
||||
$method->setAccessible(true);
|
||||
|
||||
$method->invoke($service, validDhlPhaseOneOrderData('V53PAK', 'FR'));
|
||||
})->throws(InvalidArgumentException::class);
|
||||
|
||||
it('rejects legacy Warenpost for new labels', function () {
|
||||
$service = makeDhlPhaseOneShippingService();
|
||||
$method = new ReflectionMethod($service, 'validateOrderData');
|
||||
$method->setAccessible(true);
|
||||
|
||||
$method->invoke($service, validDhlPhaseOneOrderData('V62WP'));
|
||||
})->throws(InvalidArgumentException::class);
|
||||
|
||||
it('builds a DHL Kleinpaket payload', function () {
|
||||
config([
|
||||
'dhl.account_numbers.V62KP' => '63144073556201',
|
||||
'dhl.legacy.sandbox' => false,
|
||||
'dhl.legacy.test_mode' => false,
|
||||
]);
|
||||
|
||||
$service = makeDhlPhaseOneShippingService();
|
||||
$method = new ReflectionMethod($service, 'buildShipmentPayload');
|
||||
$method->setAccessible(true);
|
||||
|
||||
$payload = $method->invoke($service, validDhlPhaseOneOrderData('V62KP'));
|
||||
|
||||
expect($payload['shipments'][0]['product'])->toBe('V62KP')
|
||||
->and($payload['shipments'][0]['billingNumber'])->toBe('63144073556201')
|
||||
->and($payload['shipments'][0]['refNo'])->toBe('ORDER-123456');
|
||||
});
|
||||
|
||||
it('uses mustEncode only for German consignee addresses', function () {
|
||||
$service = makeDhlPhaseOneShippingService();
|
||||
$method = new ReflectionMethod($service, 'shouldUseMustEncode');
|
||||
$method->setAccessible(true);
|
||||
|
||||
expect($method->invoke($service, validDhlPhaseOneOrderData('V01PAK', 'DE') + ['print_only_if_codeable' => true]))->toBeTrue()
|
||||
->and($method->invoke($service, validDhlPhaseOneOrderData('V53PAK', 'AT') + ['print_only_if_codeable' => true]))->toBeFalse()
|
||||
->and($method->invoke($service, validDhlPhaseOneOrderData('V01PAK', 'DE') + ['print_only_if_codeable' => false]))->toBeFalse();
|
||||
});
|
||||
|
||||
it('turns non-codeable DHL responses into address validation errors', function () {
|
||||
$service = makeDhlPhaseOneShippingService();
|
||||
$method = new ReflectionMethod($service, 'assertSuccessfulShipmentResponse');
|
||||
$method->setAccessible(true);
|
||||
|
||||
$method->invoke($service, [
|
||||
'status' => [
|
||||
'statusCode' => 200,
|
||||
],
|
||||
'items' => [[
|
||||
'sstatus' => [
|
||||
'statusCode' => 400,
|
||||
'detail' => 'Consignee address is not encodable.',
|
||||
],
|
||||
'validationMessages' => [[
|
||||
'validationMessage' => 'Address is not encodable.',
|
||||
]],
|
||||
]],
|
||||
], true);
|
||||
})->throws(DhlAddressValidationException::class, 'DHL kann diese Adresse nicht leitcodieren.');
|
||||
35
tests/Unit/Dhl/TrackShipmentJobTest.php
Normal file
35
tests/Unit/Dhl/TrackShipmentJobTest.php
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
<?php
|
||||
|
||||
use Acme\Dhl\Models\DhlShipment;
|
||||
use App\Jobs\TrackShipmentJob;
|
||||
use App\Services\DhlTrackingService;
|
||||
use Mockery\MockInterface;
|
||||
|
||||
afterEach(function () {
|
||||
Mockery::close();
|
||||
});
|
||||
|
||||
it('uses the current DHL tracking service for queued tracking updates', function () {
|
||||
$shipment = new DhlShipment([
|
||||
'id' => 123,
|
||||
'dhl_shipment_no' => '00340434161094000001',
|
||||
'status' => 'created',
|
||||
]);
|
||||
|
||||
$options = ['auto_retrack' => false];
|
||||
$job = new TrackShipmentJob($shipment, $options);
|
||||
|
||||
/** @var DhlTrackingService&MockInterface $trackingService */
|
||||
$trackingService = Mockery::mock(DhlTrackingService::class);
|
||||
$trackingService
|
||||
->shouldReceive('updateTrackingNow')
|
||||
->once()
|
||||
->with($shipment, $options)
|
||||
->andReturn([
|
||||
'success' => true,
|
||||
'tracking_status' => 'transit',
|
||||
'tracking_completed' => false,
|
||||
]);
|
||||
|
||||
$job->handle($trackingService);
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue