mivita/tests/Unit/Dhl/DhlProductResolverTest.php
Kevin 8f85c0107c DHL: Auslandslabels mit V53WPAK, Feldlaengen und Tracking-Wiederholung
Log-Analyse 26.08. (P0-2, P1-5, P2-3):
- Produktcode fuer DHL Paket International ist V53WPAK; mit V53PAK wurde
  kein einziges Auslandslabel erzeugt. Alte Codes werden gemappt
  (LEGACY_PRODUCT_CODE_ALIASES), Abrechnungsnummer und Masse suchen auch
  unter den alten Setting-Schluesseln (dhl_account_v53pak) und Env-Namen.
- Zu lange Felder (Name, Stadt, Telefon, Referenz) werden auf die
  DHL-Grenzen gekuerzt bzw. bereinigt statt den Versand abzulehnen; refNo
  mindestens 8 Zeichen, sonst die Bestellreferenz.
- Tracking wiederholt bei HTTP 5xx der Unified-API bis zu dreimal.

Zolldaten fuer die Schweiz (P0-3) sind nicht Teil dieses Commits.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-23 15:41:49 +00:00

201 lines
7.2 KiB
PHP

<?php
use App\Models\Setting;
use App\Services\DhlProductResolver;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
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('V53WPAK');
})->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('V53WPAK')
->and($resolver->getProductSuggestionsByCountry())->toMatchArray([
'DE' => 'V01PAK',
'FR' => 'V53WPAK',
]);
} finally {
config([
'dhl.config_source' => $previousConfigSource,
'dhl.international_countries' => $previousCountries,
]);
}
});
it('uses saved DHL international countries even with env config priority', function () {
$createdSettingsTable = ensureDhlSettingsTableForResolverTest();
try {
config([
'dhl.config_source' => 'env',
'dhl.international_countries' => ['AT'],
]);
Setting::whereSlug('dhl_international_countries')->delete();
Setting::setContentBySlug('dhl_international_countries', ['FR'], 'object');
expect((new DhlProductResolver)->getSupportedInternationalCountries())->toBe(['FR']);
} finally {
Setting::whereSlug('dhl_international_countries')->delete();
if ($createdSettingsTable) {
Schema::drop('settings');
}
}
});
it('keeps an intentionally empty saved DHL international country list', function () {
$createdSettingsTable = ensureDhlSettingsTableForResolverTest();
try {
config([
'dhl.config_source' => 'env',
'dhl.international_countries' => ['AT'],
]);
Setting::whereSlug('dhl_international_countries')->delete();
Setting::setContentBySlug('dhl_international_countries', [], 'object');
expect((new DhlProductResolver)->getSupportedInternationalCountries())->toBe([]);
} finally {
Setting::whereSlug('dhl_international_countries')->delete();
if ($createdSettingsTable) {
Schema::drop('settings');
}
}
});
it('normalizes configurable international countries', function () {
expect(DhlProductResolver::normalizeCountryCodeList([' at ', 'DE', 'XX', 'FR', 'AT']))->toBe(['AT', 'FR']);
});
function ensureDhlSettingsTableForResolverTest(): bool
{
if (Schema::hasTable('settings')) {
return false;
}
Schema::create('settings', function (Blueprint $table): void {
$table->increments('id');
$table->string('slug')->index();
$table->string('type')->nullable();
$table->json('object')->nullable();
$table->text('full_text')->nullable();
$table->text('text')->nullable();
$table->integer('int')->nullable();
$table->timestamps();
});
return true;
}
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('V53WPAK'))->toBe('international')
->and($resolver->getProductScopeLabel('V53WPAK'))->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('V53WPAK', null);
})->throws(InvalidArgumentException::class, 'Keine DHL-Abrechnungsnummer fuer Produkt V53WPAK konfiguriert.');
it('maps legacy product codes onto the codes the DHL API accepts', function () {
$resolver = new DhlProductResolver;
expect($resolver->normalizeProductCode('V53PAK'))->toBe('V53WPAK')
->and($resolver->normalizeProductCode('v53pak'))->toBe('V53WPAK')
->and($resolver->normalizeProductCode('V01PAK'))->toBe('V01PAK')
->and($resolver->normalizeProductCode(null))->toBeNull();
});
it('accepts the legacy international code without rejecting the shipment', function () {
expect((new DhlProductResolver)->resolveProductCode('AT', 'V53PAK'))->toBe('V53WPAK')
->and((new DhlProductResolver)->getProductScope('V53PAK'))->toBe('international');
});
it('offers legacy setting slugs as fallback candidates for a product', function () {
$resolver = new DhlProductResolver;
expect($resolver->getProductCodeCandidates('V53WPAK'))->toBe(['V53WPAK', 'V53PAK'])
->and($resolver->getProductCodeCandidates('V53PAK'))->toBe(['V53WPAK', 'V53PAK'])
->and($resolver->getProductCodeCandidates('V01PAK'))->toBe(['V01PAK']);
});
it('blocks destinations outside the EU customs union with an actionable message', function () {
(new DhlProductResolver)->resolveProductCode('CH', null, 'V01PAK');
})->throws(
InvalidArgumentException::class,
'DHL-Versand in das Zielland CH benoetigt Zolldaten (ExportDocument/CN23), die das Modul noch nicht erzeugt.'
);
it('keeps customs-required countries out of the supported list even when configured', function () {
$previousConfigSource = config('dhl.config_source');
$previousCountries = config('dhl.international_countries');
config([
'dhl.config_source' => 'env',
'dhl.international_countries' => ['AT', 'CH', 'GB'],
]);
try {
$resolver = new DhlProductResolver;
expect($resolver->getSupportedInternationalCountries())->toBe(['AT'])
->and($resolver->getAllowedProductCodesForCountry('CH'))->toBe([]);
} finally {
config([
'dhl.config_source' => $previousConfigSource,
'dhl.international_countries' => $previousCountries,
]);
}
});