mivita/dev/2026-08-26-logs/ANALYSE.md
Kevin aa52b02a2a 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>
2026-09-23 15:45:10 +00:00

476 lines
26 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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 |