Doku: Plaene, Kundenbriefe, Log-Analyse und Ledger-Plan versioniert, Stand 23.09.

- September-Plan: B2, E1 (Gleichstand, Zahlungsdatum), A07 und die
  Abo-Warnung nachgetragen; Audit-Stand A07 erledigt.
- Bisher nur im Arbeitsbaum: Entwicklungsplan und Kundenbrief vom 07.09.
  samt PDF, Tickets vom 08.09., Log-Analyse 26.08., Ledger-Plan,
  August-Anerkennungen. E-Mail-Adressen von Kunden und Beratern durch
  Platzhalter bzw. Namen ersetzt.

Nicht versioniert (personenbezogene Daten): Kundenliste CSV/XLSX und die
Produktions-Logs in dev/2026-08-26-logs/*.log.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Kevin 2026-09-23 15:45:10 +00:00
parent 452d1ac17f
commit aa52b02a2a
12 changed files with 2113 additions and 4 deletions

View file

@ -0,0 +1,476 @@
# Log-Analyse 26.08.2026
Auswertung von 94 Logdateien (`abo_order`, `cleanup`, `cron`, `dhl`, `laravel`, `order_controller`, `payment`, `stripe`) aus `dev/2026-08-26-logs/`.
Zeitraum: 05.04.2026 – 26.08.2026 (Schwerpunkt 27.07.–26.08.2026, `laravel.log` reicht ungedreht bis April zurück).
## Umsetzungsstand (26.08.2026)
Alle Code-Fixes sind umgesetzt. Der Testlauf zeigt unveraendert 89 Vorbestandsfehler
(BusinessPlan, InvoiceService, ConcurrentPayment – alle bereits vor diesen Aenderungen rot),
bestandene Tests stiegen von 893 auf 914: die 21 neuen Tests laufen, keine Regression.
| # | Punkt | Status |
| --- | --- | --- |
| P0-1 | `contains()` auf Array | erledigt |
| P0-2 | `V53PAK` → `V53WPAK` | erledigt |
| P0-3 | Zolldaten Schweiz | CH/GB/US/NO gesperrt, ExportDocument offen (eigenes Ticket) |
| P1-1 | Log-Channel `userabo` | erledigt |
| P1-2 | Portal-Alias `user_order_detail` | erledigt |
| P1-3 | `delete-`-E-Mail-Kollision | erledigt |
| P1-4 | Nicht-ASCII-E-Mails | erledigt |
| P1-5 | DHL-Felder normalisieren | erledigt |
| P2-1 | Mailversand in die Queue | erledigt |
| P2-2 | PAYONE-Fehlerbilder | keine Codeaenderung – Kartenablehnungen sind Geschaeftsvorfaelle, `923`/`950` treten nicht mehr auf |
| P2-3 | DHL-Tracking Retry | erledigt |
| P2-4 | Stripe-Webhook-Rauschen | erledigt |
| P2-5 | `KasController` Fatal | **offen: Serverseitig** – Datei ist im Branch bereits geloescht, Deploy-Stand pruefen |
| P2-6 | ImageMagick PDF-Policy | **offen: Serverseitig** – `policy.xml` auf dem Server anpassen |
| P2-7 | Passport Grant Type | Logging entschaerft; ob der `password`-Grant aktiviert werden soll, ist noch zu klaeren |
| P2-8 | Logrotation / Cron-Rauschen | erledigt (Rotation loest sich mit P1-1, Erinnerungs-Logs auf `debug` + Zusammenfassung) |
## Nach dem Deploy noch zu tun
1. `php artisan view:clear` – die kompilierte Blade-View des Abo-Checks liegt im Cache.
2. Benutzer 1212 einmalig manuell aufraeumen, damit `user:cleanup` sauber durchlaeuft.
3. Bestehende Umlaut-Adressen pruefen:
`SELECT id, email FROM users WHERE email <> CONVERT(email USING ASCII);`
und dasselbe fuer `shopping_users.billing_email`.
4. ENV auf dem Server: `DHL_ACCOUNT_NUMBER_V53PAK` → `DHL_ACCOUNT_NUMBER_V53WPAK`
(der alte Name wird als Fallback weiterhin gelesen, muss also nicht sofort umgestellt werden).
5. Die 16 im Log genannten Auslandsbestellungen brauchen neue Labels.
---
# Fehlerverteilung (aggregiert)
| Vorkommen | Meldung | Quelle |
| --- | --- | --- |
| 109 | `Unable to create configured logger` (Log `[userabo]` is not defined) | laravel |
| 89 | `UserMakeAboOrder::makeOrder / makePayment Error response` | laravel |
| 66 | DHL HTTP 400 – davon **33× „The product entered is unknown"** | dhl |
| 53 | `Call to a member function contains() on array` | laravel |
| 24 | `[DHL Service] Shipment creation failed (sync)` | laravel |
| 21 | `[DHL Tracking Service] Unified API returned unexpected status` (HTTP 500) | laravel |
| 18 | `AboRetryPaymentService::retry / makePayment Error` | laravel |
| 17 | `Stripe-Webhook ohne zuordenbare Zahlung` | stripe |
| 9 | `SOAP-ERROR: Parsing WSDL ... kasapi.kasserver.com` | laravel |
| 9 | `Route [user_order_detail] not defined.` | laravel |
| 7 | `deleteInactiveUsers failed ... Duplicate entry 'delete-…'` | cleanup |
| 5 | `The authorization grant type is not supported` (Passport) | laravel |
| 3 | `[DHL Controller] Failed to dispatch: shipping phone darf maximal 20 Zeichen haben` | laravel |
| 2 | Non-ASCII-Zeichen im local-part der E-Mail-Adresse | payment / abo_order |
---
# P0 – Muss sofort behoben werden
## P0-1 · Abo-Anlage im Portal wirft 500 (`contains()` on array)
**Belege:** 53×, davon 43× am 24.08. und 10× am 25.08. – der mit Abstand häufigste harte Fehler. Betroffen sind echte Kunden (`userId` 1039, 1174, …).
```
production.ERROR: Call to a member function contains() on array
at resources/views/portal/abo/_create_check.blade.php:128
```
**Ursache:** [_create_check.blade.php:128](resources/views/portal/abo/_create_check.blade.php#L128) ruft `->contains(...)` auf dem Rückgabewert von `getContentByOrder()` auf. [Yard.php:695-709](app/Services/Yard.php#L695-L709) liefert aber ein **PHP-Array**, keine Collection:
```php
public function getContentByOrder()
{
$ret = [];
// …
$ret = array_merge($ret, $comp);
return $ret; // ← Array, nicht Collection
}
```
Eingeschleppt mit Commit `50d55f4` („Abo: Einmalprodukte bereits bei der Abo-Anlage", 21.07.2026) – seitdem bricht die Prüf-/Bestätigungsseite der Abo-Anlage ab, sobald sie gerendert wird. **Der Kunde kann das Abo nicht abschließen.**
**Lösungsvorschlag (minimal-invasiv, keine Signaturänderung):**
```blade
@php($hasOneTimeRows = collect(Yard::instance('subscription')->getContentByOrder())
->contains(fn ($item) => (bool) ($item->options->abo_addon ?? false)))
```
Alternative (sauberer, aber breitere Wirkung): `getContentByOrder()` auf `Illuminate\Support\Collection` umstellen und Rückgabetyp deklarieren. Dann müssen die weiteren Aufrufer geprüft werden – [CheckoutRepository.php:134](app/Repositories/CheckoutRepository.php#L134), [AboController.php:784](app/Http/Controllers/Portal/AboController.php#L784), [OrderPaymentService.php:203](app/Services/OrderPaymentService.php#L203), [PaymentHelper.php:114](app/Services/PaymentHelper.php#L114), [UserMakeOrder.php:303](app/Cron/UserMakeOrder.php#L303) sowie `AboHelper::aboHasBaseProduct()`.
**Empfehlung:** Erst den Blade-Fix ausliefern (Hotfix), die Typvereinheitlichung separat mit Test.
**Test:** Feature-Test, der die Abo-Anlage bis zum Schritt „check" durchspielt.
---
## P0-2 · Kein einziges Auslandslabel wird erzeugt – falscher DHL-Produktcode
**Belege:** **33 von 33** Anfragen mit `"product":"V53PAK"` scheitern mit
```
"validationMessages":[{"property":"product","validationMessage":"The product entered is unknown."}]
```
Durchgehend seit mindestens 27.07.2026, betroffene Bestellungen im Log-Zeitraum:
59704, 59893, 59909, 60011, 60246, 60335, 60342, 60403, 60431, 60534, 60574, 60599, 60628, 60630, 60734, 60945.
Zielländer: 27× AUT, 4× CHE, 2× ESP.
**Ursache:** Der Produktcode `V53PAK` existiert in der DHL Parcel DE Shipping API v2 nicht. Korrekt ist **`V53WPAK`**. Das steht wörtlich in der im Repo abgelegten API-Spezifikation, [parcel-de-shipping-v2_2.yaml:1231](dev/2026-05-13-dhl-modul/parcel-de-shipping-v2_2.yaml#L1231):
```
* V01PAK: DHL PAKET;
* V53WPAK: DHL PAKET International;
enum: [V01PAK, V53WPAK, V54EPAK, V62WP, V62KP, V66WPI]
```
Der Rest der Anwendung verwendet bereits korrekt `V53WPAK` – nur der Versand-Pfad nicht. Das ist die eigentliche Inkonsistenz:
| Verwendet `V53WPAK` (korrekt) | Verwendet `V53PAK` (falsch) |
| --- | --- |
| [ModalController.php:288](app/Http/Controllers/ModalController.php#L288) | [DhlProductResolver.php:15](app/Services/DhlProductResolver.php#L15) `INTERNATIONAL_PRODUCT_CODE` |
| [admin/dhl/create.blade.php:172](resources/views/admin/dhl/create.blade.php#L172), :383 | [config/dhl.php:99](config/dhl.php#L99), :107 |
| `resources/lang/{de,en,es,fr}/dhl.php:27` | [SettingController.php:143](app/Http/Controllers/SettingController.php#L143), :152 |
| | [DhlShipmentWeightCalculator.php:16](app/Services/DhlShipmentWeightCalculator.php#L16) |
| | [DhlModalService.php:319-320](app/Services/DhlModalService.php#L319-L320), :335 |
**Nebenwirkung:** Weil die Admin-Maske `V53WPAK` sendet, der Resolver aber `V53PAK` erwartet, wirft [DhlProductResolver::resolveProductCode()](app/Services/DhlProductResolver.php#L74) für manuell angelegte Auslandssendungen zusätzlich
`"Produkt V53WPAK ist fuer DHL-Versand in das Zielland … nicht erlaubt."`
**Lösungsvorschlag:**
1. `DhlProductResolver::INTERNATIONAL_PRODUCT_CODE = 'V53WPAK'`.
2. Konfig-Keys in [config/dhl.php](config/dhl.php) (`account_numbers`, `dimensions`) auf `V53WPAK` umbenennen, ENV-Namen `DHL_ACCOUNT_NUMBER_V53WPAK` mit Fallback auf `DHL_ACCOUNT_NUMBER_V53PAK`.
3. In `normalizeProductCode()` einen Alias ergänzen, analog zum bestehenden `V62WP → V62KP` in [SettingController::normalizeDhlProductCode()](app/Http/Controllers/SettingController.php#L184-L187):
```php
$productCode = match ($productCode) {
'V53PAK' => 'V53WPAK',
'V62WP' => 'V62KP',
default => $productCode,
};
```
Damit bleiben Alt-Settings (`dhl_account_v53pak` in der `settings`-Tabelle) und gespeicherte Sendungen lauffähig.
4. `DhlShipmentWeightCalculator`, `DhlModalService` und `SettingController` nachziehen.
5. Tests anpassen: `tests/Unit/Dhl/DhlProductResolverTest.php`, `ShippingServiceProductCodeTest.php`, `DhlSanitizeLoggingTest.php`.
**Die Abrechnungsnummer `63144073555301` ist korrekt** (EKP `6314407355` + Produkt `53` + Teilnahme `01`) und muss nicht geändert werden.
---
## P0-3 · Auslandssendungen in die Schweiz brauchen zusätzlich Zolldaten
**Beleg:** 4 der gescheiterten Sendungen gehen nach `CHE`. Die Schweiz liegt außerhalb der EU-Zollunion.
**Ursache:** Für `V53WPAK` in Drittländer verlangt die DHL-API einen `customs`-Block (ExportDocument/CN23) – siehe [parcel-de-shipping-v2_2.yaml:1059](dev/2026-05-13-dhl-modul/parcel-de-shipping-v2_2.yaml#L1059). Im gesamten Code existiert **kein** `customs`/`exportDocument`-Aufbau (`grep customs app/ packages/` → nur VIES-VAT).
**Lösungsvorschlag:** Nach dem Fix aus P0-2 wird ein Teil der Sendungen weiterhin scheitern. Entweder
(a) `ExportDocument` in [ShippingService::buildPayload()](packages/acme-laravel-dhl/src/Services/ShippingService.php#L535-L555) ergänzen (Positionen aus den Bestellzeilen: Beschreibung, Zolltarifnummer, Menge, Wert, Gewicht, Ursprungsland), oder
(b) `CH` vorerst aus `DEFAULT_INTERNATIONAL_COUNTRIES` / dem Setting `dhl_international_countries` nehmen und dort manuell versenden.
**Empfehlung:** (b) als Sofortmaßnahme, (a) als eigenes Ticket – nicht mit dem Hotfix vermischen.
---
# P1 – Wichtig, sollte zeitnah behoben werden
## P1-1 · Log-Channel `userabo` existiert nicht → 109 EMERGENCY-Einträge
**Beleg:**
```
laravel.EMERGENCY: Unable to create configured logger. Using emergency logger.
InvalidArgumentException: Log [userabo] is not defined.
```
**Ursache:** `MyLog::writeLog('userabo', …)` wird an drei Stellen aufgerufen, aber [config/logging.php](config/logging.php) kennt den Channel nicht (definiert sind: `order_controller`, `abo_order`, `domain`, `payone`, `stripe`, `cleanup`, `payment`, `cron`, `dhl`).
- [UserMakeAboOrder.php:293](app/Console/Commands/UserMakeAboOrder.php#L293)
- [AboRetryPaymentService.php:202](app/Services/AboRetryPaymentService.php#L202)
- [RetryFailedPaypalAbos.php:209](app/Console/Commands/RetryFailedPaypalAbos.php#L209)
**Folge:** Jeder Zahlungsfehler erzeugt zwei Einträge statt einem, landet im Fallback-`laravel.log` statt in einer eigenen Datei und verschleiert die eigentliche Meldung.
**Lösungsvorschlag:** An allen drei Stellen wird direkt darüber bereits sauber geloggt:
```php
\Log::channel('abo_order')->error('UserMakeAboOrder: Zahlungsfehler', [...]); // existiert
MyLog::writeLog('userabo', 'error', '…', $response); // Channel fehlt
```
Der `MyLog`-Aufruf dient hier nur noch dem Mailversand. Sauberste Variante: **`'userabo'` durch `'abo_order'` ersetzen** – dann fällt die doppelte Datei weg und der Mailversand bleibt erhalten. Wer die Trennung will, ergänzt stattdessen einen `userabo`-Daily-Channel in `config/logging.php`.
---
## P1-2 · `Route [user_order_detail] not defined` im Portal
**Beleg:** 9× am 14.08.2026, `userId` 930.
Stack: `resources/views/portal/abo/my_abo.blade.php:136` → `resources/views/admin/abo/_executions.blade.php:68`.
**Ursache:** Multi-Tenant-Routing. `user_order_detail` ist nur in [routes/domains/crm.php:196](routes/domains/crm.php#L196) definiert. Die geteilte View `admin.abo._executions` wird aber auch auf der **Portal-Domain** gerendert, wo diese Route nicht geladen ist. In [routes/domains/portal.php](routes/domains/portal.php#L68-L72) steht sogar der passende Kommentar:
```php
// Routen-Aliase für shared Views (admin.abo._detail, admin.abo._executions)
Route::get('portal/my-data/edit/{id?}', …)->name('user_customer_edit');
Route::get('portal/my-orders/detail/{id}', …)->name('user_shop_order_detail');
// ← 'user_order_detail' fehlt
```
Der Alias für `is_for === 'ot'` wurde angelegt, der für `is_for === 'me'` vergessen.
**Lösungsvorschlag:** Alias in `routes/domains/portal.php` ergänzen:
```php
Route::get('portal/my-orders/own-detail/{id}', [OrderController::class, 'myOrderShow'])->name('user_order_detail');
```
Zusätzlich in `_executions.blade.php` defensiv absichern (`@if (Route::has('user_order_detail'))`), da die View domainübergreifend genutzt wird.
---
## P1-3 · `user:cleanup` scheitert seit 7 Tagen an derselben E-Mail-Kollision
**Beleg:** Täglich seit 20.08.2026, immer `user_id: 1212`:
```
SQLSTATE[23000]: Duplicate entry 'delete-kundin@beispiel.de'
for key 'users.users_email_unique'
```
**Ursache:** [UserUtil::deleteUser()](app/Services/UserUtil.php#L180) präfixt die E-Mail fest mit `delete-`:
```php
$user->email = 'delete-'.$user->email;
```
War dieselbe Adresse bereits einmal soft-deleted, kollidiert der zweite Lauf mit dem Unique-Index. Der Cleanup bricht für diesen User ab und wiederholt den Fehler jede Nacht – der Datensatz bleibt dauerhaft ungelöscht.
**Lösungsvorschlag:** Präfix eindeutig machen und das Zurücksetzen anpassen. `str_replace('delete-', '', …)` reicht dann nicht mehr:
```php
// UserUtil::deleteUser()
$user->email = 'delete-'.$user->id.'-'.$user->email;
// neuer Helper
public static function stripDeletePrefix(string $email): string
{
return preg_replace('/^delete-(\d+-)?/', '', $email);
}
```
Anzupassende Stellen: [UserUtil.php:211](app/Services/UserUtil.php#L211) (`checkEmailExists`), [UserUtil.php:229](app/Services/UserUtil.php#L229) (`restoreUser`), [AdminUserCleanupController.php:138](app/Http/Controllers/AdminUserCleanupController.php#L138), [AdminUserController.php:302](app/Http/Controllers/AdminUserController.php#L302).
Zusätzlich: Länge der `users.email`-Spalte prüfen, das Präfix wächst um bis zu 13 Zeichen.
Für User 1212 einmalig manuell aufräumen, damit der Cronlauf sauber durchläuft.
**Test:** Unit-Test „zweimaliges Löschen derselben Adresse".
---
## P1-4 · E-Mail-Adressen mit Umlauten im local-part brechen Mailversand und Abo-Erinnerung
**Belege:**
```
payment-2026-08-05.log:
ERROR: Invalid billing email at shopping_order 59690 {"billing_email":"rüdiger.beispiel@web.de"}
abo_order-2026-08-02.log:
ERROR: AboSendExecutionReminder: Versand fehlgeschlagen
{"abo_id":519,"error":"Invalid addresses: non-ASCII characters not supported in local-part of email."}
```
**Ursache:** Solche Adressen kommen ins System, weil bei der Registrierung/Bestellung nicht gegen ASCII validiert wird. `filter_var(FILTER_VALIDATE_EMAIL)` und Symfony Mailer lehnen sie später ab.
- [Payment.php:349-352](app/Services/Payment.php#L349-L352) fängt den Fall ab und schickt an `config('app.checkout_mail')` – **der Kunde erhält seine Zahlungsbestätigung nie.**
- `AboSendExecutionReminder` fängt ihn **nicht** ab, die Ausführungserinnerung fällt komplett aus.
**Lösungsvorschlag:**
1. Bei der Eingabe validieren – eigene Rule `AsciiEmail`, angewandt auf Registrierung, Checkout und Profil-Bearbeitung. Verhindert neue Fälle.
2. `AboSendExecutionReminder` genauso absichern wie `Payment::paymentStatusSendMail()` (try/catch + Log), damit ein Datensatz nicht den ganzen Lauf abbricht.
3. Bestand einmalig prüfen:
`SELECT id, email FROM users WHERE email <> CONVERT(email USING ASCII);`
und die Betroffenen anschreiben bzw. korrigieren.
---
## P1-5 · Versand scheitert an zu langen Feldern statt sie zu bereinigen
**Belege:**
```
3× [DHL Controller] Failed to dispatch shipment creation
{"error":"shipping phone darf maximal 20 Zeichen haben.","order_id":"59996"/"60027"/"60837"}
1× [DHL Service] Shipment creation failed (sync)
{"order_id":60496,"error":"consignee.name2 darf maximal 50 Zeichen haben."}
3× DHL: "Parameter refNo (Shipment reference number) must be between 8 and 35 characters long."
→ gesendet wurde refNo "NL" (2 Zeichen)
2× DHL: "Please enter a valid phone number."
→ gesendet wurde z. B. "03301717813888"
```
**Ursache:** Die Telefonnummer ist für DHL ein **optionales** Feld. Trotzdem bricht [DhlShipmentController.php:301](app/Http/Controllers/DhlShipmentController.php#L301) (`'shipping_phone' => 'nullable|string|max:20'`) die komplette Sendungserstellung ab, wenn der Kunde die Nummer mit Leerzeichen/Bindestrichen/Vorwahl-Klammern eingegeben hat. Bei `refNo` fehlt umgekehrt die Mindestlänge – ein manuell eingetragenes „NL" geht ungeprüft an die API.
**Lösungsvorschlag:** In [DhlAddressValidator](app/Services/DhlAddressValidator.php#L84) bzw. `DhlModalService::prepareAddressForApi()` normalisieren statt validieren:
- Telefon: Sonderzeichen entfernen (`preg_replace('/[^\d+]/', '', $phone)`), auf 20 Zeichen kürzen; ist das Ergebnis unplausibel (< 6 Ziffern), Feld **weglassen** statt die Sendung scheitern zu lassen.
- `name2`/`name1`: auf die API-Grenzen kürzen (`Str::limit(…, 50, '')`).
- `refNo`: Fallback `Order-{id}` verwenden, sobald die eingegebene Referenz kürzer als 8 Zeichen ist; `'reference' => 'nullable|string|min:8|max:35'` als Validierung ergänzen ([DhlShipmentController.php:289](app/Http/Controllers/DhlShipmentController.php#L289)).
Grundsatz: Ein schlecht formatiertes optionales Feld darf keinen Versand blockieren.
---
# P2 – Optimierungen / Log-Rauschen
## P2-1 · `MyLog::writeLog()` verschickt Mails synchron
[MyLog.php:31-33](app/Services/MyLog.php#L31-L33):
```php
if ($mail) {
Mail::to(config('app.exception_mail'))->send(new MailLog(...)); // blockierend
}
```
`MailLog` nutzt bereits das `Queueable`-Trait, wird aber mit `send()` statt `queue()` verschickt. Bei jedem Zahlungsfehler wartet der Cron auf den SMTP-Roundtrip.
**Sichtbare Folge:** `UserMakeAboOrder` läuft an Abrechnungstagen **250 s (20.08.)** bzw. **234 s (25.08.)**, an allen anderen Tagen ~50 ms.
Und am 25.08. um 04:00 kippte genau dort der SMTP-Server weg:
`Connection to "w017f6e4.kasserver.com:587" has been closed unexpectedly.`
**Lösungsvorschlag:** `Mail::to(...)->queue(new MailLog(...))` – Horizon läuft ohnehin. Zusätzlich den Aufruf in try/catch kapseln, damit ein Mailfehler nie den Zahllauf abbricht.
---
## P2-2 · PAYONE-Fehlerbilder aus den Zahlläufen
89 Fehlerantworten in `UserMakeAboOrder`, 18 in `AboRetryPaymentService`. Nach Ursache getrennt:
| Code | Anzahl | Bewertung |
| --- | --- | --- |
| `923` Payment type not available for this currency or card type | 39 | **Alle am 05.04.2026**, danach nie wieder → einmaliges Konfigurationsproblem, erledigt |
| `950` Desired status change not possible for this payment process | 20 | Letzter Fall **22.07.2026** → durch das PAYONE-Refactoring offenbar behoben; nach Deploy des aktuellen Branch beobachten |
| `5` / `4` / `43` / `56` / `132` Authorization declined, Card stolen … | 22 | Normale Kartenablehnungen, kein Bug |
| `cardexpiredate faulty` | 3 | Abgelaufene Karte – hier wäre eine **proaktive Erinnerung** vor Ablaufdatum sinnvoll |
| Stripe `card_declined` / `insufficient funds` | 4 | Normale Ablehnung |
**Optimierung:** Kartenablehnungen sind Geschäftsvorfälle, keine Anwendungsfehler. Sie sollten als `warning` in `abo_order` laufen und nicht per Mail an `exception_mail` gehen – sonst geht der Blick für echte Fehler verloren. Nur „technische" Codes (`923`, `950`, HTTP-/Timeout-Fehler) sollten alarmieren.
Zusätzlich: Ein Job, der Abos mit demnächst ablaufender Karte erkennt und den Kunden vorab zur Aktualisierung auffordert, würde die 3 `cardexpiredate`-Fälle und einen Teil der Wiederholungsläufe vermeiden.
---
## P2-3 · DHL-Tracking: 21 HTTP-500 der DHL-Unified-API
```
WARNING: [DHL Tracking Service] Unified API returned unexpected status
{"tracking_number":"003404350651331…","status_code":500,
"detail":"The server encountered an unexpected condition…"}
```
Fremdsystemfehler, kein eigener Bug. Aktuell wird pro Versuch geloggt, ohne Backoff.
**Lösungsvorschlag:** Retry mit exponentiellem Backoff (`Http::retry(3, 200)`), erst nach dem letzten Fehlversuch als `warning` loggen. Sendungen, die dauerhaft 500 liefern, in ein Feld `tracking_last_error` schreiben statt jeden Lauf neu zu protokollieren.
---
## P2-4 · Stripe-Webhooks ohne zuordenbare Zahlung
17× `checkout.session.expired`, ausnahmslos ohne `metadata.shopping_payment_id`.
[StripeWebhookHandler.php:181-188](app/Services/Payment/Stripe/StripeWebhookHandler.php#L181-L188) loggt das als `warning` und gibt korrekt `true` zurück (Event gilt als verarbeitet).
Abgelaufene Checkout-Sessions ohne zugehörige Zahlung sind der Normalfall bei abgebrochenen Bestellungen – kein Fehler.
**Lösungsvorschlag:** Für `checkout.session.expired` (und `checkout.session.async_payment_failed`) auf `info` herabstufen, `warning` nur für Eventtypen, bei denen eine Zahlung existieren *muss*.
---
## P2-5 · `KasController` läuft auf dem Server in einen Fatal Error
**Beleg:** 9× am 24./25.08.2026:
```
FatalError: SOAP-ERROR: Parsing WSDL: Couldn't load from
'https://kasapi.kasserver.com/soap/wsdl/KasAuth.wsdl'
at app/Http/Controllers/Api/KasController.php:75
```
**Zwei Punkte:**
1. Die Datei existiert im aktuellen Branch **nicht mehr** – sie wurde mit Commit `d4e9d10` entfernt und liegt nur noch unter `dev/app-bak/`. Auf dem Server ist sie also noch deployt. **Deploy-Stand prüfen.**
2. Der Code behandelt SOAP-Fehler mit `trigger_error(..., E_USER_ERROR)` ([dev/app-bak/.../KasController.php:60](dev/app-bak/Http/Controllers/Api/KasController.php#L60)) – das erzeugt einen nicht abfangbaren Fatal Error statt einer 5xx-Antwort. Falls der Controller wieder gebraucht wird: `SoapFault` per try/catch fangen und eine JSON-Fehlerantwort liefern.
**Kontext:** Alle Vorfälle liegen im Fenster 24.08. 23:35 – 25.08. 00:25, zeitgleich mit
`Connection could not be established with host "w017f6e4.kasserver.com:587"` und
`[payment:check-uptime] Outage-Incident #4 für payone` (24.08. 23:50).
→ Eine Netzwerkstörung beim Hoster, kein Anwendungsfehler. Nur die fehlende Fehlerbehandlung ist unser Problem.
---
## P2-6 · ImageMagick verbietet PDF-Verarbeitung
**Beleg:** 13.08.2026, 10:31:
```
ERROR: PDF Verarbeitung fehlgeschlagen: attempt to perform an operation
not allowed by the security policy `PDF' @ error/constitute.c/IsCoderAuthorized/426
ERROR: Thumbnail creation failed: …
```
**Ursache:** Serverkonfiguration, nicht Code. Die ImageMagick-Policy (`/etc/ImageMagick-6/policy.xml`) sperrt den PDF-Coder – Standard seit CVE-2018-16509.
**Lösungsvorschlag:** Auf dem Server das PDF-`rights="none"` in der Policy auf `read|write` setzen und Ghostscript aktuell halten. Im Code zusätzlich prüfen, ob die PDF-Verarbeitung verfügbar ist, und bei fehlender Berechtigung ein Platzhalter-Thumbnail liefern statt zu scheitern.
---
## P2-7 · Passport: nicht unterstützter Grant Type
5 Vorfälle (17.08., 22.08. 04:03 und 05:53, …):
`Laravel\Passport\Exceptions\OAuthServerException: The authorization grant type is not supported`.
Kein Stacktrace bis in Anwendungscode. Vermutlich ein API-Client, der einen nicht aktivierten Grant (z. B. `password`) anfordert, oder Scan-Traffic auf `/oauth/token`.
**Lösungsvorschlag:** In [AuthServiceProvider](app/Providers/AuthServiceProvider.php) prüfen, welche Grants freigeschaltet sind, und mit den API-Clients abgleichen. Falls es Fremdtraffic ist: `/oauth/token` als 4xx behandeln und nicht als `ERROR` loggen (Rate-Limit ergänzen).
---
## P2-8 · Logrotation und Log-Rauschen
**`laravel.log`** ist 572 KB groß und enthält Einträge von **05.04.2026 bis 25.08.2026** – die tägliche Rotation greift hier nicht, weil `MyLog` über nicht existierende Channels in den Fallback schreibt (siehe P1-1). Nach dem Fix von P1-1 löst sich das mit auf; zusätzlich `LOG_CHANNEL=daily` und `LOG_DAILY_DAYS` prüfen.
**`laravel-2026-08-24.log`** ist 972 KB bei nur **50 Fehlern** – 5.868 von 6.054 Zeilen sind Stacktraces. Die 43 identischen `contains()`-Traces machen den Großteil aus; mit P0-1 verschwindet das.
**Cron-Rauschen:** Die Zahlungserinnerungen loggen jeden geprüften Benutzer auf `INFO`:
| Anzahl | Meldung |
| --- | --- |
| 488 | `Prüfe Zahlungserinnerungen für Benutzer` |
| 429 | `Erinnerung bereits gesendet für Benutzer` |
| 189 | `Zweite Erinnerung für Benutzer` |
| 146 | `Inaktives Konto für Benutzer` |
Über 1.200 Zeilen pro Woche für ~59 tatsächlich versandte Mails.
**Lösungsvorschlag:** „geprüft"/„bereits gesendet"/„inaktiv" auf `debug` setzen, am Ende des Laufs eine Zusammenfassung auf `INFO` schreiben (`geprüft: X, versendet: Y, übersprungen: Z`). Tatsächlich versandte Erinnerungen bleiben auf `INFO`.
---
# Hinweise ohne Handlungsbedarf
- **`BusinessStoreOptimized: NOT EXECUTED - wrong day (expected: 1, today: N)`** – korrektes Verhalten, die Provisionsberechnung läuft nur am 1. des Monats. Der Eintrag könnte auf `debug`.
- **`User #… attempted to create customer abo but one is already initiated`** (16× in `order_controller`) – die Schutzlogik greift wie vorgesehen. Auffällig ist nur, dass mehrere User es zweimal versuchen; ggf. ist die Rückmeldung in der Oberfläche zu unauffällig.
- **`Shipping country not found for user #1056, country: ` (leer)** – 2×, Stammdaten ohne Land. Einzelfall, Datensatz korrigieren.
- **`SponsorBuyingPointsRepair: VP-Buchung liegt bei anderem Berater — manuell pruefen (Umhaengung?)`** (26.08., `shopping_order_id: 58797`, `buyer_id: 1426`, `current_sponsor_id: 1241`, `booked_sponsor_id: 6`) – bewusst als Warnung ausgelegt, verlangt fachliche Prüfung, keinen Code-Fix.
- **`payments:check-accounts`** läuft täglich in 0–2 s durch, ohne Fehler.
---
# Empfohlene Reihenfolge
| # | Maßnahme | Aufwand | Wirkung |
| --- | --- | --- | --- |
| 1 | P0-1 `collect()` im Blade | Minuten | Abo-Anlage im Portal funktioniert wieder |
| 2 | P0-2 `V53PAK` → `V53WPAK` | ~1 h inkl. Tests | Auslandsversand funktioniert wieder |
| 3 | P1-3 `delete-`-Kollision | ~1 h | Nächtlicher Cleanup läuft wieder durch |
| 4 | P1-1 Log-Channel `userabo` | Minuten | 109 EMERGENCY-Einträge weg, saubere Logs |
| 5 | P1-2 Portal-Alias `user_order_detail` | Minuten | Kein 500 mehr auf „Meine Abos" |
| 6 | P1-5 DHL-Felder normalisieren | ~2 h | Keine blockierten Sendungen durch Telefonnummern |
| 7 | P1-4 ASCII-Validierung E-Mail | ~2 h | Kunden erhalten ihre Bestätigungen |
| 8 | P2-1 Mailversand in die Queue | ~30 min | Zahllauf von 250 s auf Sekunden |
| 9 | P0-3 Zolldaten CH | eigenes Ticket | Schweiz-Versand |
| 10 | P2-3/4/8 Log-Level und Retries | ~2 h | Auswertbare Logs |