['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, warnings: array, normalized: array, 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 */ 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 */ 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; } }