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>
255 lines
8.8 KiB
PHP
255 lines
8.8 KiB
PHP
<?php
|
|
|
|
namespace App\Services;
|
|
|
|
use App\Models\Setting;
|
|
use Illuminate\Support\Facades\Schema;
|
|
use InvalidArgumentException;
|
|
|
|
class DhlProductResolver
|
|
{
|
|
public const DOMESTIC_COUNTRY = 'DE';
|
|
|
|
public const DOMESTIC_PRODUCT_CODES = ['V01PAK', 'V62KP'];
|
|
|
|
public const INTERNATIONAL_PRODUCT_CODE = 'V53WPAK';
|
|
|
|
/**
|
|
* Historisch verwendete Produktcodes, die die DHL Parcel DE Shipping API v2
|
|
* nicht kennt. Werden auf den gueltigen Code abgebildet, damit Alt-Settings
|
|
* und gespeicherte Sendungen weiter funktionieren.
|
|
*
|
|
* @var array<string, string>
|
|
*/
|
|
public const LEGACY_PRODUCT_CODE_ALIASES = [
|
|
'V53PAK' => 'V53WPAK',
|
|
];
|
|
|
|
public const DEFAULT_INTERNATIONAL_COUNTRIES = ['AT', 'ES', 'NL', 'BE', 'FR'];
|
|
|
|
/**
|
|
* Zielländer ausserhalb der EU-Zollunion. DHL verlangt fuer sie einen
|
|
* customs-Block (ExportDocument/CN23), den wir noch nicht aufbauen -
|
|
* ohne den wird die Sendung von DHL abgelehnt. Sie bleiben deshalb
|
|
* gesperrt, auch wenn sie im Setting dhl_international_countries stehen.
|
|
*
|
|
* @var string[]
|
|
*/
|
|
public const CUSTOMS_REQUIRED_COUNTRIES = ['CH', 'GB', 'US', 'NO'];
|
|
|
|
public const DHL_COUNTRY_CODES = [
|
|
'DE' => 'DEU',
|
|
'AT' => 'AUT',
|
|
'ES' => 'ESP',
|
|
'CH' => 'CHE',
|
|
'US' => 'USA',
|
|
'GB' => 'GBR',
|
|
'FR' => 'FRA',
|
|
'IT' => 'ITA',
|
|
'NL' => 'NLD',
|
|
'BE' => 'BEL',
|
|
'PL' => 'POL',
|
|
'CZ' => 'CZE',
|
|
'DK' => 'DNK',
|
|
'SE' => 'SWE',
|
|
'NO' => 'NOR',
|
|
];
|
|
|
|
/**
|
|
* @return array{country_code: string, dhl_country_code: string, product_code: string}
|
|
*/
|
|
public function resolveForShipment(string $destinationCountryCode, ?string $requestedProductCode = null, ?string $defaultProductCode = null): array
|
|
{
|
|
$countryCode = $this->normalizeCountryCode($destinationCountryCode);
|
|
$productCode = $this->resolveProductCode($countryCode, $requestedProductCode, $defaultProductCode);
|
|
|
|
return [
|
|
'country_code' => $countryCode,
|
|
'dhl_country_code' => $this->toDhlCountryCode($countryCode),
|
|
'product_code' => $productCode,
|
|
];
|
|
}
|
|
|
|
public function resolveProductCode(string $destinationCountryCode, ?string $requestedProductCode = null, ?string $defaultProductCode = null): string
|
|
{
|
|
$countryCode = $this->normalizeCountryCode($destinationCountryCode);
|
|
$hasRequestedProduct = $requestedProductCode !== null && trim($requestedProductCode) !== '';
|
|
$productCode = $this->normalizeProductCode($requestedProductCode ?: $defaultProductCode);
|
|
|
|
if ($countryCode === self::DOMESTIC_COUNTRY) {
|
|
$productCode = $productCode ?: 'V01PAK';
|
|
|
|
if (! in_array($productCode, self::DOMESTIC_PRODUCT_CODES, true)) {
|
|
throw new InvalidArgumentException("Produkt {$productCode} ist fuer DHL-Versand nach Deutschland nicht erlaubt.");
|
|
}
|
|
|
|
return $productCode;
|
|
}
|
|
|
|
if (in_array($countryCode, self::CUSTOMS_REQUIRED_COUNTRIES, true)) {
|
|
throw new InvalidArgumentException("DHL-Versand in das Zielland {$countryCode} benoetigt Zolldaten (ExportDocument/CN23), die das Modul noch nicht erzeugt. Sendung bitte manuell ueber das DHL-Geschaeftskundenportal anlegen.");
|
|
}
|
|
|
|
if (! in_array($countryCode, $this->getSupportedInternationalCountries(), true)) {
|
|
throw new InvalidArgumentException("DHL-Versand in das Zielland {$countryCode} ist aktuell nicht freigegeben.");
|
|
}
|
|
|
|
if (! $productCode || (! $hasRequestedProduct && in_array($productCode, self::DOMESTIC_PRODUCT_CODES, true))) {
|
|
return self::INTERNATIONAL_PRODUCT_CODE;
|
|
}
|
|
|
|
if ($productCode !== self::INTERNATIONAL_PRODUCT_CODE) {
|
|
throw new InvalidArgumentException("Produkt {$productCode} ist fuer DHL-Versand in das Zielland {$countryCode} nicht erlaubt.");
|
|
}
|
|
|
|
return $productCode;
|
|
}
|
|
|
|
/**
|
|
* @return array<string, string>
|
|
*/
|
|
public function getProductSuggestionsByCountry(): array
|
|
{
|
|
return array_fill_keys($this->getSupportedInternationalCountries(), self::INTERNATIONAL_PRODUCT_CODE)
|
|
+ [self::DOMESTIC_COUNTRY => 'V01PAK'];
|
|
}
|
|
|
|
/**
|
|
* @return string[]
|
|
*/
|
|
public function getAllowedProductCodesForCountry(string $destinationCountryCode): array
|
|
{
|
|
$countryCode = $this->normalizeCountryCode($destinationCountryCode);
|
|
|
|
if ($countryCode === self::DOMESTIC_COUNTRY) {
|
|
return self::DOMESTIC_PRODUCT_CODES;
|
|
}
|
|
|
|
if (in_array($countryCode, $this->getSupportedInternationalCountries(), true)) {
|
|
return [self::INTERNATIONAL_PRODUCT_CODE];
|
|
}
|
|
|
|
return [];
|
|
}
|
|
|
|
public function toDhlCountryCode(string $countryCode): string
|
|
{
|
|
$countryCode = $this->normalizeCountryCode($countryCode);
|
|
|
|
return self::DHL_COUNTRY_CODES[$countryCode];
|
|
}
|
|
|
|
public function assertBillingNumber(string $productCode, ?string $billingNumber): string
|
|
{
|
|
if ($billingNumber === null || trim($billingNumber) === '') {
|
|
throw new InvalidArgumentException("Keine DHL-Abrechnungsnummer fuer Produkt {$productCode} konfiguriert.");
|
|
}
|
|
|
|
return $billingNumber;
|
|
}
|
|
|
|
/**
|
|
* Liefert den gueltigen Produktcode zusammen mit seinen historischen Schreibweisen,
|
|
* damit Aufrufer Settings-Slugs und Config-Keys aus beiden Generationen aufloesen koennen.
|
|
*
|
|
* @return string[]
|
|
*/
|
|
public function getProductCodeCandidates(string $productCode): array
|
|
{
|
|
$productCode = $this->normalizeProductCode($productCode) ?? '';
|
|
|
|
$legacyCodes = array_keys(array_filter(
|
|
self::LEGACY_PRODUCT_CODE_ALIASES,
|
|
static fn (string $target): bool => $target === $productCode
|
|
));
|
|
|
|
return array_values(array_unique(array_merge([$productCode], $legacyCodes)));
|
|
}
|
|
|
|
public function getProductScope(string $productCode): string
|
|
{
|
|
$productCode = $this->normalizeProductCode($productCode);
|
|
|
|
return $productCode === self::INTERNATIONAL_PRODUCT_CODE ? 'international' : 'national';
|
|
}
|
|
|
|
public function getProductScopeLabel(string $productCode): string
|
|
{
|
|
return $this->getProductScope($productCode) === 'international'
|
|
? 'DHL Paket International'
|
|
: 'DHL Paket National';
|
|
}
|
|
|
|
public function normalizeCountryCode(string $countryCode): string
|
|
{
|
|
$countryCode = strtoupper(trim($countryCode));
|
|
|
|
if ($countryCode === '') {
|
|
throw new InvalidArgumentException('DHL-Zielland fehlt.');
|
|
}
|
|
|
|
if (strlen($countryCode) === 3) {
|
|
$countryCode = array_search($countryCode, self::DHL_COUNTRY_CODES, true) ?: $countryCode;
|
|
}
|
|
|
|
if (! array_key_exists($countryCode, self::DHL_COUNTRY_CODES)) {
|
|
throw new InvalidArgumentException("DHL-Laendercode {$countryCode} wird nicht unterstuetzt.");
|
|
}
|
|
|
|
return $countryCode;
|
|
}
|
|
|
|
public function normalizeProductCode(?string $productCode): ?string
|
|
{
|
|
if ($productCode === null || trim($productCode) === '') {
|
|
return null;
|
|
}
|
|
|
|
$productCode = strtoupper(trim($productCode));
|
|
|
|
return self::LEGACY_PRODUCT_CODE_ALIASES[$productCode] ?? $productCode;
|
|
}
|
|
|
|
/**
|
|
* @return string[]
|
|
*/
|
|
public function getSupportedInternationalCountries(): array
|
|
{
|
|
$useEnvPriority = config('dhl.config_source') === 'env';
|
|
$configCountries = config('dhl.international_countries', self::DEFAULT_INTERNATIONAL_COUNTRIES);
|
|
$countries = $configCountries;
|
|
$storedCountries = Schema::hasTable('settings')
|
|
? Setting::getContentBySlug('dhl_international_countries')
|
|
: false;
|
|
|
|
if (is_array($storedCountries)) {
|
|
$countries = $storedCountries;
|
|
} elseif (! $useEnvPriority) {
|
|
$countries = $storedCountries ?: $configCountries;
|
|
}
|
|
|
|
// Zolldaten-pflichtige Laender werden hier hart herausgefiltert, damit ein
|
|
// gesetztes Setting sie nicht doch in den Versand laesst.
|
|
return array_values(array_diff(
|
|
self::normalizeCountryCodeList(is_array($countries) ? $countries : []),
|
|
self::CUSTOMS_REQUIRED_COUNTRIES
|
|
));
|
|
}
|
|
|
|
/**
|
|
* @return string[]
|
|
*/
|
|
public static function normalizeCountryCodeList(array $countryCodes): array
|
|
{
|
|
$countryCodes = array_map(
|
|
static fn ($countryCode): string => strtoupper(trim((string) $countryCode)),
|
|
$countryCodes
|
|
);
|
|
|
|
return array_values(array_filter(array_unique($countryCodes), static function (string $countryCode): bool {
|
|
return $countryCode !== ''
|
|
&& $countryCode !== self::DOMESTIC_COUNTRY
|
|
&& array_key_exists($countryCode, self::DHL_COUNTRY_CODES);
|
|
}));
|
|
}
|
|
}
|