mivita/app/Services/DhlAddressValidator.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;
}
}