258 lines
10 KiB
PHP
258 lines
10 KiB
PHP
<?php
|
|
|
|
namespace App\Services;
|
|
|
|
class DhlAddressValidator
|
|
{
|
|
private const DACH_COUNTRIES = ['DE', 'AT', 'CH'];
|
|
|
|
private const COUNTRY_SPECIFIC_POSTAL_PATTERNS = [
|
|
'DE' => ['pattern' => '/^\d{5}$/', 'message' => 'Deutsche Postleitzahl muss 5 Ziffern haben.'],
|
|
'AT' => ['pattern' => '/^\d{4}$/', 'message' => 'Oesterreichische Postleitzahl muss 4 Ziffern haben.'],
|
|
'CH' => ['pattern' => '/^\d{4}$/', 'message' => 'Schweizer Postleitzahl muss 4 Ziffern haben.'],
|
|
'ES' => ['pattern' => '/^\d{5}$/', 'message' => 'Spanische Postleitzahl muss 5 Ziffern haben.'],
|
|
];
|
|
|
|
/**
|
|
* @return array{status: string, can_create_label: bool, errors: array<int, string>, warnings: array<int, string>, normalized: array<string, mixed>, validation_available: bool, validation_level: string, validation_message: string}
|
|
*/
|
|
public function validate(array $address): array
|
|
{
|
|
$normalized = $this->normalize($address);
|
|
$errors = [];
|
|
$warnings = [];
|
|
$validationAvailable = $this->hasDachValidation($normalized['country_code']);
|
|
$validationLevel = $validationAvailable ? 'formal_dach' : 'basic';
|
|
$validationMessage = $validationAvailable
|
|
? 'Formale DACH-Pruefung aktiv: Pflichtfelder, PLZ-Format, Plausibilitaet und Packstation-Regeln werden geprueft. Eine echte Adressdatenbank-/DHL-Leitcodepruefung ist nicht angebunden.'
|
|
: 'Fuer dieses Zielland ist aktuell nur eine Basis-Adresspruefung verfuegbar.';
|
|
|
|
foreach ($this->requiredFields($normalized) as $field => $label) {
|
|
if ($normalized[$field] === '') {
|
|
$errors[] = "{$label} ist erforderlich.";
|
|
}
|
|
}
|
|
|
|
if ($normalized['firstname'] === '' && $normalized['lastname'] === '' && $normalized['company'] === '') {
|
|
$errors[] = 'Entweder Name oder Firmenname muss angegeben werden.';
|
|
}
|
|
|
|
$this->validateCountryAndPostalCode($normalized, $errors, $warnings);
|
|
$this->validateAddressPlausibility($normalized, $errors);
|
|
$this->validatePackstation($normalized, $errors, $warnings);
|
|
$this->validateWarnings($normalized, $warnings);
|
|
|
|
$status = 'valid';
|
|
if ($errors !== []) {
|
|
$status = 'error';
|
|
} elseif ($warnings !== []) {
|
|
$status = 'warning';
|
|
}
|
|
|
|
return [
|
|
'status' => $status,
|
|
'can_create_label' => $errors === [],
|
|
'errors' => array_values(array_unique($errors)),
|
|
'warnings' => array_values(array_unique($warnings)),
|
|
'normalized' => $normalized,
|
|
'validation_available' => $validationAvailable,
|
|
'validation_level' => $validationLevel,
|
|
'validation_message' => $validationMessage,
|
|
];
|
|
}
|
|
|
|
/**
|
|
* @return array<string, string>
|
|
*/
|
|
private function normalize(array $address): array
|
|
{
|
|
$countryCode = $address['country_code']
|
|
?? $address['shipping_country_code']
|
|
?? data_get($address, 'country.code')
|
|
?? '';
|
|
|
|
return [
|
|
'firstname' => trim((string) ($address['firstname'] ?? $address['shipping_firstname'] ?? '')),
|
|
'lastname' => trim((string) ($address['lastname'] ?? $address['shipping_lastname'] ?? '')),
|
|
'company' => trim((string) ($address['company'] ?? $address['shipping_company'] ?? '')),
|
|
'street' => trim((string) ($address['address'] ?? $address['street'] ?? $address['shipping_address'] ?? '')),
|
|
'house_number' => trim((string) ($address['houseNumber'] ?? $address['house_number'] ?? $address['shipping_houseNumber'] ?? '')),
|
|
'postal_code' => trim((string) ($address['zipcode'] ?? $address['postalCode'] ?? $address['postal_code'] ?? $address['shipping_zipcode'] ?? '')),
|
|
'city' => trim((string) ($address['city'] ?? $address['shipping_city'] ?? '')),
|
|
'country_code' => strtoupper(trim((string) $countryCode)),
|
|
'email' => trim((string) ($address['email'] ?? $address['shipping_email'] ?? '')),
|
|
'phone' => trim((string) ($address['phone'] ?? $address['shipping_phone'] ?? '')),
|
|
'postnumber' => trim((string) ($address['postnumber'] ?? $address['postNumber'] ?? $address['shipping_postnumber'] ?? '')),
|
|
];
|
|
}
|
|
|
|
/**
|
|
* @return array<string, string>
|
|
*/
|
|
private function requiredFields(array $address): array
|
|
{
|
|
$fields = [
|
|
'street' => 'Straße',
|
|
'postal_code' => 'Postleitzahl',
|
|
'city' => 'Ort',
|
|
'country_code' => 'Land',
|
|
];
|
|
|
|
if (! $this->isPackstation($address)) {
|
|
$fields['house_number'] = 'Hausnummer';
|
|
}
|
|
|
|
return $fields;
|
|
}
|
|
|
|
private function validateCountryAndPostalCode(array $address, array &$errors, array &$warnings): void
|
|
{
|
|
if ($address['country_code'] === '') {
|
|
return;
|
|
}
|
|
|
|
$resolver = new DhlProductResolver;
|
|
|
|
try {
|
|
if ($resolver->getAllowedProductCodesForCountry($address['country_code']) === []) {
|
|
$errors[] = "DHL-Versand in das Zielland {$address['country_code']} ist aktuell nicht freigegeben.";
|
|
}
|
|
} catch (\InvalidArgumentException $e) {
|
|
$errors[] = $e->getMessage();
|
|
|
|
return;
|
|
}
|
|
|
|
if (isset(self::COUNTRY_SPECIFIC_POSTAL_PATTERNS[$address['country_code']]) && $address['postal_code'] !== '') {
|
|
$rule = self::COUNTRY_SPECIFIC_POSTAL_PATTERNS[$address['country_code']];
|
|
if (! preg_match($rule['pattern'], $address['postal_code'])) {
|
|
$errors[] = $rule['message'];
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
if ($address['postal_code'] !== '') {
|
|
$warnings[] = 'Fuer dieses Zielland ist aktuell nur eine Basis-Adresspruefung verfuegbar. Bitte Adresse manuell pruefen.';
|
|
}
|
|
}
|
|
|
|
private function hasDachValidation(string $countryCode): bool
|
|
{
|
|
return in_array($countryCode, self::DACH_COUNTRIES, true);
|
|
}
|
|
|
|
private function validateAddressPlausibility(array $address, array &$errors): void
|
|
{
|
|
$isDachAddress = $this->hasDachValidation($address['country_code']);
|
|
|
|
if ($address['street'] !== '' && mb_strlen($address['street']) < 3) {
|
|
$errors[] = 'Straße ist zu kurz.';
|
|
}
|
|
|
|
if ($address['street'] !== '' && ! preg_match('/[a-zäöüß]/iu', $address['street'])) {
|
|
$errors[] = 'Straße muss Buchstaben enthalten.';
|
|
}
|
|
|
|
if ($address['city'] !== '' && mb_strlen($address['city']) < 2) {
|
|
$errors[] = 'Ort ist zu kurz.';
|
|
}
|
|
|
|
if ($address['city'] !== '' && ! preg_match('/[a-zäöüß]/iu', $address['city'])) {
|
|
$errors[] = 'Ort muss Buchstaben enthalten.';
|
|
}
|
|
|
|
if ($address['postal_code'] !== '' && ! preg_match('/^[A-Z0-9][A-Z0-9 -]{2,9}$/i', $address['postal_code'])) {
|
|
$errors[] = 'Postleitzahl enthaelt ungueltige Zeichen.';
|
|
}
|
|
|
|
if ($isDachAddress && ! $this->isPackstation($address) && ! preg_match('/\d/', $address['house_number'])) {
|
|
$errors[] = 'Hausnummer muss fuer DACH-Adressen eine Ziffer enthalten.';
|
|
}
|
|
|
|
if ($this->containsPlaceholderValue($address['street'])) {
|
|
$errors[] = 'Straße wirkt wie eine Test- oder Platzhalteradresse.';
|
|
}
|
|
|
|
if ($this->containsPlaceholderValue($address['city'])) {
|
|
$errors[] = 'Ort wirkt wie eine Test- oder Platzhalterangabe.';
|
|
}
|
|
}
|
|
|
|
private function validatePackstation(array $address, array &$errors, array &$warnings): void
|
|
{
|
|
if ($this->isPackstation($address) && $address['postnumber'] === '') {
|
|
$errors[] = 'DHL Postnummer ist fuer Packstation/Paketbox erforderlich.';
|
|
}
|
|
|
|
if ($address['postnumber'] === '') {
|
|
return;
|
|
}
|
|
|
|
if ($address['country_code'] !== 'DE') {
|
|
$errors[] = 'Packstation/Paketbox ist aktuell nur fuer Deutschland erlaubt.';
|
|
}
|
|
|
|
if (! preg_match('/^[0-9]{6,10}$/', $address['postnumber'])) {
|
|
$errors[] = 'DHL Postnummer muss 6-10 Ziffern enthalten.';
|
|
}
|
|
|
|
if (! $this->isPackstation($address)) {
|
|
$warnings[] = 'DHL Postnummer ist gesetzt. Bitte pruefen, ob Straße/Nr. eine Packstation oder Paketbox enthaelt.';
|
|
}
|
|
|
|
$lockerNumber = $this->extractLockerNumber($address);
|
|
if ($this->isPackstation($address) && $lockerNumber === null) {
|
|
$errors[] = 'Packstation-/Paketbox-Nummer fehlt.';
|
|
}
|
|
|
|
if ($lockerNumber !== null && ((int) $lockerNumber < 100 || (int) $lockerNumber > 999)) {
|
|
$errors[] = 'Packstation-/Paketbox-Nummer muss 3-stellig sein (100-999).';
|
|
}
|
|
}
|
|
|
|
private function validateWarnings(array $address, array &$warnings): void
|
|
{
|
|
if ($address['phone'] === '') {
|
|
$warnings[] = 'Telefonnummer fehlt. DHL kann Empfaenger bei Zustellproblemen eventuell nicht kontaktieren.';
|
|
}
|
|
|
|
if ($address['email'] === '') {
|
|
$warnings[] = 'E-Mail-Adresse fehlt. Tracking-Mails koennen nicht automatisch an diese Adresse gesendet werden.';
|
|
}
|
|
|
|
if (! $this->hasDachValidation($address['country_code']) && ! $this->isPackstation($address) && ! preg_match('/\d/', $address['house_number'])) {
|
|
$warnings[] = 'Hausnummer enthaelt keine Ziffer. Bitte Adresse manuell pruefen.';
|
|
}
|
|
}
|
|
|
|
private function containsPlaceholderValue(string $value): bool
|
|
{
|
|
$value = mb_strtolower(trim($value));
|
|
|
|
if ($value === '') {
|
|
return false;
|
|
}
|
|
|
|
return (bool) preg_match('/(test|fake|falsch|dummy|asdf|qwertz|qwerty|xxx|kein|ungueltig|invalid)/u', $value)
|
|
|| preg_match('/^(.)\1{2,}$/u', $value);
|
|
}
|
|
|
|
private function isPackstation(array $address): bool
|
|
{
|
|
return (bool) preg_match('/\b(packstation|paketbox)\b/i', $address['street'].' '.$address['house_number']);
|
|
}
|
|
|
|
private function extractLockerNumber(array $address): ?string
|
|
{
|
|
if (preg_match('/(?:packstation|paketbox)\s*(\d+)/i', $address['street'], $matches)) {
|
|
return $matches[1];
|
|
}
|
|
|
|
if ($this->isPackstation($address) && preg_match('/\d+/', $address['house_number'], $matches)) {
|
|
return $matches[0];
|
|
}
|
|
|
|
return null;
|
|
}
|
|
}
|