# Development Backlog - 22.01.2026 ## Status-Legende - `[x]` Erledigt - `[ ]` Offen - `[!]` Hohe Priorität - `[?]` Klärungsbedarf --- ## ERLEDIGTE AUFGABEN ### [x] Produkt-Slugs anpassen - **Status:** Erledigt - **Beschreibung:** Slug kann direkt im Admin geändert werden - **URL:** https://gesundheit.mivita.care/produkte/black-friday-week ### [x] WWW-Redirect entfernen - **Status:** Erledigt - **Beschreibung:** Domain/Subdomain funktioniert ohne WWW-Prefix ### [x] Abo-Anpassungen (Protokoll Claudia) - **Status:** Erledigt - **Änderungen:** - Checkbox für AGB vor Abo-Abschluss - Änderungen erst nach 6 Ausführungen möglich - Nur Liefertag + Lieferadresse änderbar - Lieferadresse sync mit Benutzerdaten --- ## OFFENE AUFGABEN --- ### [X] 1. NEWS: Download-Center Verlinkung **Priorität:** Hoch ✅ **ERLEDIGT** **Bereich:** Dashboard / News **Problem:** Benutzer finden das Download-Center nicht. Nur Verweis reicht nicht. **Lösung implementiert:** ✅ Strukturiertes JSON-Feld `file_links` für Datei-Links pro Sprache ✅ Admin-Formular mit Select2-Dropdown zur Auswahl von DC-Dateien ✅ Mehrsprachige Unterstützung (DE, EN, ES) ✅ Schöne Button-Darstellung im Dashboard mit Icons ✅ Direkte Links zum Download-Center **Implementierte Dateien:** 1. **Migration:** `2026_01_23_120458_add_file_links_to_dashboard_news_table.php` - Neues JSON-Feld `file_links` in `dashboard_news` Tabelle 2. **Model:** `app/Models/DashboardNews.php` - `file_links` zu `$fillable` und `$casts` hinzugefügt - Neue Methoden: `getFileLinks($lang)`, `hasFileLinks($lang)` 3. **Admin-Formular:** `resources/views/admin/site/news/form.blade.php` - Datei-Link-Sektion für jede Sprache - Select2-Dropdown mit allen aktiven DC-Dateien - JavaScript zum Hinzufügen/Entfernen von Links - Dynamische Label-Eingabe 4. **Frontend-View:** `resources/views/dashboard/_news.blade.php` - Anzeige der Datei-Links als grüne Download-Buttons - **Direkter Download-Link** über `route('storage_file', [$file->id, 'dc_file', 'download'])` - Icons mit Ionicons - Responsive Darstellung 5. **Übersetzungen:** - DE: `resources/lang/de/backend.php` - EN: `resources/lang/en/backend.php` - ES: `resources/lang/es/backend.php` - Neue Keys: `file_links`, `file_links_hint`, `link_label`, `select_file`, `add_file_link` **JSON-Struktur:** ```json { "de": [ { "file_id": 123, "label": "Preisliste herunterladen" }, { "file_id": 456, "label": "Produktkatalog öffnen" } ], "en": [{ "file_id": 789, "label": "Download Price List" }] } ``` **Verwendung im Admin:** 1. News bearbeiten → Zum jeweiligen Sprach-Tab scrollen 2. "Datei-Link hinzufügen" klicken 3. Label eingeben (z.B. "Preisliste herunterladen") 4. Datei aus Dropdown auswählen 5. Speichern → Links erscheinen prominent im Dashboard --- ### [X] 2. Points mit Dezimalstellen (DECIMAL statt INT) **Priorität:** Hoch **Bereich:** Marketingplan / Provisionsberechnung **Problem:** Punkte werden aktuell als INT gespeichert. Kommazahlen bei Produkten werden falsch berechnet/gerundet. **Anforderung:** - Alle Punkte-Felder auf DECIMAL umstellen - Berechnung im gesamten Marketingplan anpassen **Punkte-Ursprung (Produkte):** | Parameter | Wert | |-----------|------| | Model | `App\Models\Product` | | Feld | `points` (INT) → `DECIMAL(10,2)` | | Zusätzlich | `sponsor_buying_points`, `sponsor_buying_points_amount` | **Punkte-Aggregation (Sales Volume):** | Parameter | Aktuell | Neu | |-----------|---------|-----| | Model | `App\Models\UserSalesVolume` | - | | Tabelle | `user_sales_volumes` | - | | Datentyp | `INT` | `DECIMAL(10,2)` | **Betroffene Spalten `user_sales_volumes`:** ```sql ALTER TABLE user_sales_volumes MODIFY points DECIMAL(10,2), MODIFY month_points DECIMAL(10,2), MODIFY month_KP_points DECIMAL(10,2), MODIFY month_TP_points DECIMAL(10,2), MODIFY month_shop_points DECIMAL(10,2); ``` **Betroffene Spalten `user_business`:** ```sql ALTER TABLE user_business MODIFY sales_volume_points DECIMAL(10,2), MODIFY sales_volume_points_shop DECIMAL(10,2), MODIFY sales_volume_points_sum DECIMAL(10,2); ``` **Betroffene Spalten `products`:** ```sql ALTER TABLE products MODIFY points DECIMAL(10,2); ``` **Weitere Models mit `points`:** - `App\Models\ShoppingOrder` → `points` - `App\Models\ShoppingOrderItem` → `points` - `App\Models\ShoppingCollectOrder` → `points` - `App\Models\HomepartyUserOrderItem` → `points` **Betroffene Services:** - `app/Services/BusinessPlan/TreeCalcBotOptimized.php` - Alle Berechnungen in `app/Services/BusinessPlan/` **Migration erforderlich:** Ja (mehrere ALTER TABLE) --- ### [X] 3. Vorkasse: Verwendungszweck deutlich machen **Priorität:** Hoch **Bereich:** Checkout / Payment / E-Mail **Problem:** Kunden geben falschen Verwendungszweck an. System kann Zahlung nicht zuordnen. **Anforderung:** - Payone TXID als Verwendungszweck deutlich hervorheben - Hinweis im Checkout, in E-Mail und im Kundenkonto **Technische Details:** | Parameter | Wert | |-----------|------| | Clearingtype | `vor` (Vorkasse) | | Controller | `App\Http\Controllers\Pay\PayoneController` | | Checkout-View | `resources/views/web/templates/checkout.blade.php:898` | | Mail-Template | `resources/views/emails/order_*.blade.php` | | Kundenkonto | `resources/views/user/order/*.blade.php` | **Verwendungszweck-Feld (KORRIGIERT):** | Falsch | Richtig | |--------|---------| | `shopping_payments.reference` | `payment_transactions.txid` | **Zugriff auf TXID:** ```php // Model: App\Models\PaymentTransaction $transaction = PaymentTransaction::where('shopping_payment_id', $payment->id)->first(); $verwendungszweck = $transaction->txid; // Oder via transmitted_data JSON $txid = $transaction->transmitted_data['txid'] ?? null; ``` **Umsetzung:** 1. **Checkout:** Alert-Box mit Verwendungszweck-Hinweis bei Vorkasse-Auswahl 2. **E-Mail:** Hervorgehobener Block mit Bankdaten + TXID als Verwendungszweck 3. **Kundenkonto:** Info-Box bei unbezahlten Vorkasse-Bestellungen mit TXID **Beispiel-Text:** ``` WICHTIG: Bitte geben Sie als Verwendungszweck ausschließlich folgende Nummer an: [TXID: 123456789] Nur so kann Ihre Zahlung automatisch zugeordnet werden. ``` --- ### [X] 4. Paketbox/Packstation Feld **Status:** ✅ ERLEDIGT **Priorität:** Mittel **Bereich:** Adressverwaltung / Checkout **Anforderung:** - Neues Feld "DHL Postnummer" nur bei Lieferadresse - Automatische Erkennung: Wenn Postnummer angegeben → Packstation-Modus **Technische Details:** | Parameter | Wert | |-----------|------| | Tabelle | `shopping_users` | | Model | `App\Models\ShoppingUser` | **Durchgeführte Implementierung:** 1. ✅ **Migration erstellt und ausgeführt** - Datei: `database/migrations/2026_01_22_181707_add_shipping_postnumber_to_shopping_users_table.php` - Spalte: `shipping_postnumber VARCHAR(20) NULLABLE` 2. ✅ **Model angepasst** (`app/Models/ShoppingUser.php`) - Feld im `$fillable` Array - Methode `hasPostnumber()` hinzugefügt 3. ✅ **Checkout-Formular** (`resources/views/web/templates/checkout.blade.php`) - Eingabefeld für Postnummer nach Telefon-Feld - Placeholder: "12345678" - JavaScript-Validierung für Packstation-Format 4. ✅ **DHL Modal** (`resources/views/admin/dhl/modal_in_order_shipment.blade.php`) - Postnummer-Feld hinzugefügt 5. ✅ **DHL Service** (`app/Services/DhlModalService.php`) - Postnummer wird korrekt an DHL API übergeben 6. ✅ **Kundendetail-Ansicht** (`resources/views/admin/customer/_customer_detail.blade.php`) - Postnummer wird mit Badge angezeigt - Hinweistext für Packstation-Lieferung 7. ✅ **Kunden-Bearbeitungsformular** (`resources/views/admin/customer/_edit.blade.php`) - Postnummer-Feld direkt nach Telefon-Feld - Mit Hinweistext und Placeholder - Wird in User-Bereich und Admin-Bereich verwendet 8. ✅ **Lieferschein-PDF** (`resources/views/pdf/delivery.blade.php`) - Postnummer wird fett gedruckt in der Lieferadresse angezeigt - Format: "DHL Postnummer: 12345678" 9. ✅ **Alert-Box im Formular** (`resources/views/admin/customer/_edit.blade.php`) - Deutliche gelbe Warning-Box erscheint, wenn Postnummer ausgefüllt wird - Erklärt klar, dass bei Packstation-Lieferung: - Straße/Nr. = "Packstation [Nummer]" (z.B. "Packstation 145") - PLZ/Ort = Standort der Packstation (nicht Wohnadresse!) - JavaScript-gesteuerte Ein-/Ausblendung bei Input - Dismissable (kann vom User geschlossen werden) 10. ✅ **User-Account Formular** (`resources/views/user/user_form.blade.php`) - Migration für `user_accounts` Tabelle erstellt und ausgeführt - Datei: `database/migrations/2026_01_23_102622_add_shipping_postnumber_to_user_accounts_table.php` - Model `UserAccount` im `$fillable` Array erweitert - Postnummer-Feld nach shipping_phone hinzugefügt - Identische Alert-Box wie im Admin-Formular - Identisches JavaScript für Ein-/Ausblendung 11. ✅ **Checkout-Formular** (`resources/views/web/templates/checkout.blade.php`) - Alte kleine Info-Box (alert-info) ersetzt durch große Warning-Box - Identische gelbe Alert-Box wie in allen anderen Formularen - JavaScript bereits vorhanden (togglePackstationHint) - Wird automatisch ein-/ausgeblendet bei Input 12. ✅ **CheckoutRepository Datenübertragung** (`app/Repositories/CheckoutRepository.php`) - **Problem behoben:** Postnummer wurde nicht von UserAccount zu ShoppingUser übertragen - `shoppingUserByAuthUser()` erweitert (Zeile 350): Übertragung für eingeloggte User - `shoppingUserAuthData()` erweitert (Zeile 418 + 430): Übertragung für Salescenter-Bestellungen - Postnummer wird jetzt korrekt im Checkout-Formular angezeigt 13. ✅ **Anzeige-Views erweitert** - Postnummer wird überall angezeigt: - ✅ `admin/sales/_detail.blade.php` - Admin Bestelldetails - ✅ `admin/sales/_detail_homparty_user.blade.php` - Homeparty Bestelldetails - ✅ `portal/order/_detail.blade.php` - Portal Bestelldetails - ✅ `emails/checkout_status.blade.php` - Bestellstatus E-Mail - ✅ `emails/checkout.blade.php` - Checkout E-Mail (2 Stellen) - ✅ `admin/modal/is_like_member.blade.php` - Kundenzuordnung Modal (2 Stellen) - **Format:** Badge mit Icon + Hinweistext in Web-Views - **Format:** Fett gedruckt "DHL Postnummer: XXX" in E-Mails 14. ✅ **Formular-Views erweitert** - Postnummer-Eingabe überall möglich: - ✅ `portal/customer/_edit_form.blade.php` - Portal Kundenformular - Postnummer-Feld nach shipping_phone - Alert-Box mit JavaScript (togglePackstationAlert) - Identisch zu anderen Formularen - ✅ `user/order/shipping_me.blade.php` - Bestellung für mich selbst - Hidden field `shipping_postnumber` hinzugefügt (2 Stellen) - Für `same_as_billing` true/false Szenarien ## 🔍 **TIEFENPRÜFUNG DURCHGEFÜHRT - Weitere kritische Lücken gefunden und geschlossen!** 15. ✅ **KRITISCHE CONTROLLER/SERVICES KORRIGIERT** - Datenübertragung sichergestellt: - ✅ `app/Services/UserUtil.php` (Zeile 101) - ShoppingUser-Erstellung aus UserAccount - `shipping_postnumber` fehlte komplett! - ✅ `app/Services/AboOrderCart.php` (Zeilen 277 + 289) - Abo-Bestellungen: ShoppingUser aus UserAccount - `shipping_postnumber` fehlte an 2 Stellen (same_as_billing true/false) - ✅ `app/Services/PaymentHelper.php` (Zeile 115) - Payment ShoppingUser Update - `shipping_postnumber` fehlte komplett! 16. ✅ **WEITERE FEHLENDE VIEWS ERGÄNZT**: - ✅ `user/homeparty/_address.blade.php` (2 Stellen) - Homeparty Adressanzeige (billing + shipping) - Fett: "DHL Postnummer: XXX" - ✅ `user/order/payment/custom_payment.blade.php` - Custom Payment Bestelldetails - Badge mit Info-Text - ✅ `emails/custom_payment.blade.php` - Custom Payment E-Mail - Fett: "DHL Postnummer: XXX" **Übersetzungen:** - DE: `payment.dhl_postnumber` = "DHL Postnummer" - EN: `payment.dhl_postnumber` = "DHL Post Number" - ES: `payment.dhl_postnumber` = "Número de correo DHL" - **Neue Alert-Box Übersetzungen** in `resources/lang/{de,en,es}/payment.php`: - `packstation_alert_title` - `packstation_alert_intro` - `packstation_alert_street` - `packstation_alert_street_example` - `packstation_alert_location` - `packstation_alert_not_home` - `packstation_alert_footer` **DHL API Integration:** ```php if ($user->shipping_postnumber) { $recipient['postNumber'] = $user->shipping_postnumber; // shipping_address enthält "Packstation 145" (3-stellige Nummer!) } ``` **Wichtige Hinweise zur Packstation-Nummer:** - ⚠️ **Packstation-NUMMER ist 3-stellig** (100-999, steht auf gelbem Schild) - 📱 **DHL Postnummer ist 6-10-stellig** (separate Kundennummer in DHL App) - 🚫 **Häufiger Fehler:** Postnummer wird als Packstation-Nummer eingegeben - ✅ **Richtig:** "Packstation 145" (nicht "Packstation 12345") - 📄 **Anleitung:** `/dev/22-01-2026/packstation-anleitung.md` **Verbesserte Fehlermeldungen:** - Detaillierte Fehlermeldung bei ungültiger Packstation-Nummer - Frontend-Hinweise in allen Formularen aktualisiert (DE, EN, ES) - Logging mit allen relevanten Daten für besseres Debugging --- ### [X] 5. Set/Kit Produkte: Inhalte auflisten **Priorität:** Mittel **Bereich:** Produkte / Rechnungen / Lieferscheine **Problem:** Bei Sets/Kits werden enthaltene Einzelprodukte nicht aufgelistet. **Anforderung:** - Alle enthaltenen Produkte unter dem Set auflisten - Auf Rechnung und Lieferschein ausweisen - Admin-UI: Dropdown + Liste (wie bei Inhaltsstoffen) **Referenz-Implementierung (Inhaltsstoffe):** | Parameter | Wert | |-----------|------| | Pivot-Tabelle | `product_ingredients` | | Model | `App\Models\ProductIngredient` | | Relation | `Product::p_ingredients()` (belongsToMany) | **Neue Tabellen-Struktur (analog zu Inhaltsstoffen):** ```sql CREATE TABLE product_bundles ( id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, product_id BIGINT UNSIGNED NOT NULL, -- Das Set/Kit (Parent) bundle_product_id BIGINT UNSIGNED NOT NULL, -- Enthaltenes Produkt (Child) quantity INT UNSIGNED DEFAULT 1, pos INT UNSIGNED DEFAULT 0, -- Sortierung created_at TIMESTAMP NULL, updated_at TIMESTAMP NULL, FOREIGN KEY (product_id) REFERENCES products(id) ON DELETE CASCADE, FOREIGN KEY (bundle_product_id) REFERENCES products(id) ON DELETE CASCADE, UNIQUE KEY unique_bundle (product_id, bundle_product_id) ); ``` **Neue Model-Relation in `Product.php`:** ```php public function bundleItems() { return $this->belongsToMany(Product::class, 'product_bundles', 'product_id', 'bundle_product_id') ->withPivot('quantity', 'pos') ->orderBy('pos'); } public function isBundle(): bool { return $this->bundleItems()->count() > 0; } ``` **Admin-UI (wie Inhaltsstoffe):** - Dropdown zur Produktauswahl - Listenansicht mit Menge und Sortierung - Vorlage: `resources/views/admin/product/` → Ingredients-Sektion kopieren **Betroffene Views:** - `resources/views/pdf/invoice.blade.php` → Bundle-Items unter Produkt auflisten - `resources/views/pdf/delivery.blade.php` → Bundle-Items auflisten - Shop-Produktdetailseiten --- ### [!] 6. Mehrsprachigkeit: Rechnungen, Provisionen, Lieferscheine **Priorität:** Hoch **Bereich:** PDF-Generierung / E-Mail **Anforderung:** - Deutsche Version bleibt primär (rechtlich bindend) - Zusätzliche Kopie in Landessprache (EN, ES, FR) - Sprache aus User-Einstellung (`users.locale`) **Technische Details:** | Parameter | Wert | |-----------|------| | PDF-Service | `App\Services\Invoice` | | Templates | `resources/views/pdf/*.blade.php` | | Mail | `App\Mail\MailInvoice` | | User-Sprache | `users.locale` (Spalte prüfen/anlegen) | **Betroffene Dokumente:** | Dokument | Datei | Status | |----------|-------|--------| | Rechnung | `invoice.blade.php` | [ ] | | Lieferschein | `delivery.blade.php` | [ ] | | Provisionsabrechnung | `credit_details.blade.php` | [ ] | | Stornorechnung | (neu) | [ ] | | Mitgliedschaftsverlängerung | E-Mail Template | [ ] | | Partnerantrag/Vertrag | PDF Template | [ ] | **Umsetzung:** ```php // Invoice Service erweitern public function generatePdf(Order $order, string $locale = 'de'): string { app()->setLocale($locale); // PDF generieren... } // Zwei PDFs generieren $pdfDE = $this->generatePdf($order, 'de'); $pdfUser = $this->generatePdf($order, $user->locale ?? 'de'); // Bei E-Mail beide anhängen (wenn unterschiedlich) if ($user->locale && $user->locale !== 'de') { $mail->attach($pdfDE, ['as' => 'Rechnung-DE.pdf']); $mail->attach($pdfUser, ['as' => 'Invoice-' . strtoupper($user->locale) . '.pdf']); } ``` **Speicherung:** - Beide PDFs im System speichern (Bestellungen-Ansicht) - Zusätzliche Spalten in `user_invoices`: `file_localized`, `locale` --- ### ✅ 7. Stornorechnungen mit Punktekorrektur **[ERLEDIGT 06.02.2026]** **Priorität:** Hoch **Bereich:** Admin / Rechnungswesen / Marketingplan **Problem:** - Storno-Button fehlt im Admin - Punkte werden bei Storno NICHT abgezogen (in gesamter MLM-Struktur) **Anforderung:** - Button "Stornorechnung erstellen" neben Rechnung - Negativbetrag im Rechnungskreis - Punkte in gesamter MLM-Struktur korrigieren (Upline!) - Mehrsprachigkeit beachten **Technische Details:** | Parameter | Wert | |-----------|------| | Model | `App\Models\UserInvoice` | | Storno-Felder | `cancellation`, `cancellation_id`, `cancellation_date` | | Punkte-Model | `App\Models\UserSalesVolume` | | Business-Model | `App\Models\UserBusiness` | | Admin-View | `resources/views/admin/order/*.blade.php` | **Punktekorrektur-Logik:** ```php // 1. Original-SalesVolume finden $salesVolume = UserSalesVolume::where('order_id', $order->id)->first(); // 2. Punkte negieren (neuer Eintrag mit negativen Werten) UserSalesVolume::create([ 'user_id' => $salesVolume->user_id, 'order_id' => $order->id, 'month' => $salesVolume->month, 'year' => $salesVolume->year, 'month_points' => -$salesVolume->month_points, 'month_KP_points' => -$salesVolume->month_KP_points, 'status' => 6, // Neuer Status: 'cancelled' ]); // 3. Upline-Struktur durchlaufen und korrigieren // → TreeCalcBotOptimized neu berechnen oder separater CancellationService ``` **Betroffene Tabellen:** ``` user_sales_volumes → Negativer Eintrag hinzufügen user_business → Monatsdaten neu berechnen (oder Neuberechnung triggern) ``` **Admin-Route:** ```php Route::post('/admin/invoice/{id}/cancel', [InvoiceController::class, 'cancel']); ``` --- ### ✅ **UMSETZUNG ABGESCHLOSSEN** (06.02.2026) **Status:** ✅ ERLEDIGT Die Stornorechnung-Funktionalität mit automatischer Punktekorrektur wurde vollständig implementiert und ist produktionsbereit. #### Implementierte Komponenten **1. Backend-Logik** (`app/Repositories/InvoiceRepository.php`) Neue Methode `createCancellation()`: - Erstellt neue Stornorechnung mit eindeutiger Rechnungsnummer - Generiert PDF-Dokumente (Rechnung + Lieferschein) mit negativen Werten - Markiert Originalrechnung als storniert (`cancellation=true`, `cancellation_id`, `cancellation_date`) - Setzt Bestellstatus: - `txaction = 'cancelled'` (Zahlungsstatus) - `shipped = 10` (Versandstatus), nur wenn noch nicht versendet (`shipped` < 2) - Ruft Punktekorrektur-Service auf - Optional: E-Mail-Versand der Stornorechnung - Vollständige DB-Transaktion mit Rollback bei Fehlern **2. Punktekorrektur-Service** (`app/Services/BusinessPlan/SalesPointsVolume.php`) Neue Methode `cancelSalesPointsVolume()`: - Erstellt `UserSalesVolume`-Eintrag mit **negativen** Punkten und Beträgen - Status 6 = `cancelled` (neu hinzugefügt) - Triggert automatische Neuberechnung der MLM-Struktur via `reCalculateSalesPointsVolume()` - Korrigiert Upline-Punkte und monatliche Summen (KP, TP, Shop-Punkte) - Vollständiges Logging für Nachvollziehbarkeit Erweiterte Methode `reCalculateSalesPointsVolume()`: - Case 6 hinzugefügt für Storno-Einträge - Negative Werte werden korrekt in monatliche Summen eingerechnet - Unterscheidet zwischen Shop- und Berater-Punkten (`status_turnover`) **3. Controller** (`app/Http/Controllers/SalesController.php`) Neue Methode `invoiceCancellation()`: - Validiert Anfrage und prüft Berechtigungen - Prüft, ob Originalrechnung existiert - Prüft, ob bereits storniert wurde (verhindert Doppel-Storno) - Ruft Repository-Methode auf - Fehlerbehandlung mit Flash-Messages - Redirect zurück zur Bestelldetailseite **4. Model-Relationen** (`app/Models/ShoppingOrder.php`) Überarbeitete Eloquent-Relations: - `user_invoice()`: Findet Original-Rechnung korrekt (auch nach Storno) - Logik: `cancellation=false` ODER `cancellation_id IS NOT NULL` - `user_cancellation_invoice()`: Findet Stornorechnung - Logik: `cancellation=true` UND `cancellation_id IS NULL` - `user_invoices()`: Alle Rechnungen (Original + Storno) Neue Hilfsmethoden: - `isCancellationInvoice()`: Prüft, ob Stornorechnung existiert - `getCancellationInvoice()`: Holt Stornorechnung-Entity **5. File-Controller** (`app/Http/Controllers/FileController.php`) Erweitert für `$from === 'cancellation'`: - Lädt korrekt die Stornorechnung-PDF (nicht die Originalrechnung) - Query mit `whereNull('cancellation_id')` für eindeutige Identifikation - Unterstützt mehrsprachige PDF-Versionen (DE, EN, ES) - Respektiert Berechtigungen und Disk-Konfiguration **6. PDF-Templates** (neu erstellt) - `resources/views/pdf/cancellation.blade.php` - Rotes Design mit "STORNORECHNUNG" Header - Referenz zur Originalrechnung - Negatives Vorzeichen bei allen Beträgen - `resources/views/pdf/cancellation-detail.blade.php` - Negativer Einzelpreis und Gesamtpreis - Kompatibel mit `ShoppingOrderItem`-Struktur - `resources/views/pdf/cancellation_delivery.blade.php` - Lieferschein für Stornolieferung - Negative Mengen - `resources/views/pdf/cancellation-detail-homeparty.blade.php` - Spezialversion für Homeparty-Bestellungen - Negative Punkte und Beträge - `resources/views/pdf/cancellation-detail-collection.blade.php` - Spezialversion für Sammelbestellungen - Negative Summen **7. Admin-UI** (`resources/views/admin/sales/_detail.blade.php`) Stornorechnung-Sektion: - Zeigt Badge mit Rechnungsnummer (rot) - Download/Stream-Buttons für Stornorechnung (rot) - Mehrsprachige PDF-Versionen (DE, EN, ES) - Button "Stornorechnung erstellen" (nur wenn noch nicht storniert) - Datum der Storno-Erstellung - Originalrechnung bleibt sichtbar nach Storno Modal für Stornorechnung: - `resources/views/admin/sales/_detail.blade.php` (Zeilen 781-836) - Datums-Auswahl für Stornodatum - Checkbox für E-Mail-Versand - Bestätigungs-Button **8. Business Points Admin** DataTable (`app/Http/Controllers/BusinessPointsController.php`): - Zeigt Storno-Einträge mit rotem Undo-Icon - Link zur betroffenen Bestellung Detail-Modal (`resources/views/admin/business/modal_edit_points.blade.php`): - Zeigt Stornorechnung mit rotem Badge - Link zu Stornorechnung-PDF (primär) - Link zu Originalrechnung (sekundär, klein) - Negative Werte werden rot dargestellt - Zeigt monatliche Summen und Syslog **9. Status-System** Neue Status-Werte: - `UserSalesVolume::$statusTypes[6] = 'cancelled'` - `UserSalesVolume::$statusColors[6] = 'danger'` (rot) - `Payment::$txaction_text['cancelled'] = 'cancelled'` - `Payment::$txaction_color['cancelled'] = 'danger'` (rot) **10. Übersetzungen** Ergänzt in `resources/lang/{de,en,es}/pdf.php`: - `cancellation_invoice`: "Stornorechnung" / "Cancellation Invoice" / "Factura de cancelación" - `cancellation_nr`: "Storno-Nr." / "Cancellation No." / "No. de cancelación" - `cancellation_for`: "Storno für Rechnung" / "Cancellation for Invoice" / "Cancelación de factura" - `cancelled`: "Storniert" / "Cancelled" / "Cancelado" - `cancellation_delivery`: "Storno-Lieferschein" / "Cancellation Delivery Note" / "Albarán de cancelación" Status in `resources/lang/{de,en,es}/payment.php`: - `cancelled`: "Storniert" / "Cancelled" / "Cancelado" (bereits vorhanden) #### Routing **CRM Domain** (`routes/domains/crm.php` Zeile 346-347): ```php Route::post('/admin/sales/invoice/cancellation', 'SalesController@invoiceCancellation') ->name('admin_sales_invoice_cancellation'); ``` #### Datenbank-Struktur **Bestehende Felder genutzt:** `user_invoices`: - `cancellation` (boolean): `true` bei Original UND Stornorechnung - `cancellation_id` (int): Nur bei Originalrechnung (zeigt auf Storno), `NULL` bei Stornorechnung - `cancellation_date` (string): Stornodatum **Unterscheidung:** - **Originalrechnung (nach Storno):** `cancellation=true`, `cancellation_id IS NOT NULL` - **Stornorechnung:** `cancellation=true`, `cancellation_id IS NULL` `user_sales_volumes`: - `status = 6` für Storno-Einträge - Negative `points` und `total_net` `shopping_orders`: - `txaction = 'cancelled'` (Zahlungsstatus) - `shipped = 10` (Versandstatus "storniert") #### Workflow 1. **Admin klickt "Stornorechnung erstellen"** 2. **Modal öffnet sich** → Datum auswählen, optional E-Mail aktivieren 3. **POST an `admin_sales_invoice_cancellation`** 4. **Controller validiert** → keine Doppel-Stornos 5. **Repository erstellt Storno:** - Neue Rechnungsnummer - PDF-Generierung (Rechnung + Lieferschein) - DB-Eintrag für Stornorechnung - Originalrechnung markieren - Bestellstatus aktualisieren 6. **Service korrigiert Punkte:** - Neuer `UserSalesVolume`-Eintrag (negativ) - MLM-Struktur neu berechnen - Monatssummen aktualisieren 7. **Redirect mit Success-Message** 8. **UI zeigt:** - Original- UND Stornorechnung - Roter Status "Storniert" - Punktekorrektur in Business-Points-Liste #### Besondere Features - ✅ **Atomare Transaktion**: Alles oder nichts (DB-Rollback bei Fehler) - ✅ **Mehrsprachigkeit**: DE, EN, ES für alle PDFs - ✅ **Upline-Korrektur**: Komplette MLM-Hierarchie wird neu berechnet - ✅ **Verhindert Doppel-Storno**: Check auf bestehende Stornorechnung - ✅ **Versand-Logik**: Nur offene Bestellungen werden versandstorniert - ✅ **Logging**: Vollständige Nachvollziehbarkeit via `syslog` und `\Log` - ✅ **Performance**: Memory-optimiert, nutzt `TreeCalcBotOptimized` - ✅ **UI-Konsistenz**: Roter Farbcode durchgängig (danger) #### Testing-Checkliste - [x] Stornorechnung erstellen für Berater-Bestellung - [x] Stornorechnung erstellen für Shop-Bestellung - [x] Stornorechnung erstellen für Homeparty - [x] Stornorechnung erstellen für Sammelbestellung - [x] PDF-Download DE, EN, ES - [x] Punktekorrektur in `user_sales_volumes` prüfen - [x] Monatssummen in Business Points prüfen - [x] Upline-Punkte prüfen (MLM-Hierarchie) - [x] Bestellstatus "Storniert" anzeigen - [x] Versandstatus "Storniert" (nur bei offenen) - [x] Doppel-Storno verhindert - [x] E-Mail-Versand optional #### Performance - **Memory:** Effizient durch Service-basierte Architektur - **DB-Queries:** Optimiert mit Eloquent Relations - **PDF-Generation:** Nutzt bestehende `InvoicePDF`-Klasse - **Recalculation:** Nutzt optimierte `TreeCalcBotOptimized` #### Geänderte Dateien **Backend:** - `app/Http/Controllers/SalesController.php` (Methode `invoiceCancellation()`) - `app/Http/Controllers/FileController.php` (Stornorechnung laden) - `app/Repositories/InvoiceRepository.php` (Methode `createCancellation()`) - `app/Services/BusinessPlan/SalesPointsVolume.php` (Status 6, `cancelSalesPointsVolume()`) - `app/Services/Invoice.php` (Hilfsmethoden für Dateinamen) - `app/Services/Payment.php` (txaction 'cancelled') - `app/Models/ShoppingOrder.php` (Relations überarbeitet) - `app/Models/UserSalesVolume.php` (Status 6 hinzugefügt) **Frontend:** - `resources/views/admin/sales/_detail.blade.php` (Storno-Sektion + Modal) - `resources/views/admin/business/points.blade.php` (DataTable) - `resources/views/admin/business/modal_edit_points.blade.php` (Detail-Modal) **PDF-Templates (neu):** - `resources/views/pdf/cancellation.blade.php` - `resources/views/pdf/cancellation-detail.blade.php` - `resources/views/pdf/cancellation_delivery.blade.php` - `resources/views/pdf/cancellation-detail-homeparty.blade.php` - `resources/views/pdf/cancellation-detail-collection.blade.php` **Übersetzungen:** - `resources/lang/de/pdf.php` - `resources/lang/en/pdf.php` - `resources/lang/es/pdf.php` - `resources/lang/de/payment.php` (bereits vorhanden) - `resources/lang/en/payment.php` (bereits vorhanden) - `resources/lang/es/payment.php` (bereits vorhanden) **Routing:** - `routes/domains/crm.php` (Route bereits vorhanden) #### Technische Highlights 1. **Saubere Architektur**: Controller → Repository → Service → Model 2. **Eloquent Relations**: Komplexe WHERE-Queries in Relations gekapselt 3. **DB-Transaktionen**: Atomare Operationen mit Rollback 4. **Service-Layer**: Business-Logik getrennt von Data-Access 5. **Code-Qualität**: Pint-formatiert, PSR-12 konform 6. **Fehlerbehandlung**: Try-Catch mit aussagekräftigen Fehlermeldungen 7. **Logging**: Strukturiertes Logging mit Kontext-Daten #### Dokumentation - Diese Datei: `dev/22-01-2026/next-steps.md` - Code-Kommentare in allen geänderten Dateien - Log-Messages für Debugging und Monitoring **Abgeschlossen am:** 06.02.2026 --- ### [ ] 8. Französisch hinzufügen **Priorität:** Mittel **Bereich:** Lokalisierung **Anforderung:** - Neue Sprachdateien: `resources/lang/fr/` - Monatsstatistik übersetzen - Vorkasse-Texte übersetzen **Technische Details:** | Parameter | Wert | |-----------|------| | Verzeichnis | `resources/lang/fr/` | | Vorlage | `resources/lang/de/` kopieren | **Dateien erstellen:** ``` resources/lang/fr/ ├── abo.php ├── backend.php ├── cal.php ├── customer.php ├── email.php ├── home.php ├── marketingplan.php ├── navigation.php ├── payment.php └── team.php ``` **Umsetzung:** ```bash cp -r resources/lang/de resources/lang/fr # Dann alle Dateien übersetzen ``` --- ### [!] 9. Gutschriften: Falsche Punkteberechnung **Priorität:** Hoch **Bereich:** Marketingplan / Team-Ansicht **Problem:** Gutschriften werden nicht korrekt zu Punkten addiert. Unterschiedliche Anzeige für Admin vs. User. **Beispiel:** - Monika Kunz: Admin sieht 625 Punkte, User sieht 1115 Punkte (Dezember) - Differenz: 490 Punkte → vermutlich Gutschrift nicht berücksichtigt **Technische Details:** | Parameter | Wert | |-----------|------| | Service | `App\Services\BusinessPlan\TreeCalcBotOptimized` | | Status | `UserSalesVolume.status = 4` (credit/Gutschrift) | | Model | `App\Models\UserSalesVolume` | **Status-Mapping:** ```php 0 => 'not_assigned' 1 => 'advisor_order' 2 => 'shoporder' 3 => 'shoporder_pending' 4 => 'credit' // ← Gutschrift 5 => 'registration' ``` **Debugging-Schritte:** 1. Query für User mit `status = 4` im betroffenen Monat prüfen: ```php UserSalesVolume::where('user_id', $userId) ->where('month', 12)->where('year', 2025) ->where('status', 4)->get(); ``` 2. Berechnung in `getPointsKPSum()` / `getPointsTPSum()` validieren 3. Team-View Query vs. Admin-View Query vergleichen 4. Prüfen ob `status_points` korrekt gesetzt ist **Beispiel-Fall:** - App\Models\UserSalesVolume user_id=1218 year=2025 month=12 -> 32758 ist eine Gutschrift und wird hier richtig summiert, in der Auswertung steht allerdings 545 month_KP_points also ohne die Gutschrift. - Die Auswertung erfolgt live über App\Services\BusinessPlan\TreeCalcBotOptimized und wird nach abgeschlossenem Monat über einen Job mit diesem Controller app/Console/Commands/BusinessStoreOptimized.php in der DB app/Models/UserBusiness.php gespeichert - Die Zusammenfassung für das Beispiel ist hier UserBusiness ID=21264 - es sind klar die falschen Werte zusammengerechnet worden --- ## 🔍 TECHNISCHE ANALYSE - Gutschriften-System (28.01.2026) ### System-Architektur **1. Wo werden Gutschriften erstellt?** | Stelle | Datei | Funktion | Typ | | ----------------------- | ------------------------------------------------- | ------------------------ | ------------------------------------ | | Manuell Admin | `app/Services/BusinessPlan/SalesPointsVolume.php` | `addSalesPointsVolume()` | Punkte-Gutschrift (status=4) | | Automatisch Provisionen | `app/Cron/UserPaymentCredits.php` | `addUserCreditItem()` | Zahlungs-Gutschrift (UserCreditItem) | | PDF-Gutschriften | `app/Repositories/CreditRepository.php` | `create()` | Zahlungs-PDFs | **2. Datenfluss Punkte-Berechnung** ``` UserSalesVolume (Tabelle) ├─ status = 1 (advisor_order) → Eigene Bestellung ├─ status = 2 (shoporder) → Shop-Bestellung ├─ status = 3 (shoporder_pending) → Shop ausstehend ├─ status = 4 (credit) → 🔥 GUTSCHRIFT └─ status = 5 (registration) → Registrierung │ ↓ Bei Änderung/Erstellung │ SalesPointsVolume::reCalculateSalesPointsVolume() ├─ Durchläuft ALLE Einträge eines Users (month/year) ├─ Sortierung: orderBy('id', 'ASC') ✅ Chronologisch ├─ Berechnet kumulative Summe: month_KP_points, month_TP_points ├─ Bei status=4: add_KP_TP_Points() wird aufgerufen └─ Speichert Werte in JEDEM Eintrag │ ↓ Letzter Eintrag enthält Gesamtsumme │ User::getUserSalesVolume($month, $year, 'first') ├─ Query: orderBy('id', 'DESC') → Gibt LETZTEN Eintrag └─ Gibt UserSalesVolume-Objekt zurück │ ↓ User::getUserSalesVolumeBy($month, $year, $field) ├─ Ruft getUserSalesVolume('first') auf └─ Bei 'sales_volume_points_KP_sum': getPointsKPSum() │ ↓ UserSalesVolume::getPointsKPSum() └─ return month_KP_points + month_shop_points │ ↓ Live-Berechnung │ BusinessUserItemOptimized::getUserSalesVolumeOptimized() └─ Ruft User::getUserSalesVolumeBy() auf │ ↓ Speicherung nach Monatsende │ BusinessStoreOptimized → UserBusiness (Tabelle) ``` ### 🔴 Identifizierte Fehlerquellen **HAUPTPROBLEM 1: Fehlende Neuberechnung** ```php // SalesPointsVolume::addSalesPointsVolume() - Zeile 219-257 // Wenn eine Gutschrift manuell hinzugefügt wird: $user_sales_volume = UserSalesVolume::create([ 'status' => 4, // Gutschrift 'status_points' => $data['status_points'], // ⚠️ Muss gesetzt sein! // ... ]); // ✅ Neuberechnung wird aufgerufen: self::reCalculateSalesPointsVolume($user_sales_volume->user_id, ...); ``` **Aber:** Wenn Gutschriften direkt über DB oder andere Wege erstellt werden, fehlt dieser Aufruf! --- **HAUPTPROBLEM 2: status_points nicht gesetzt** In `SalesPointsVolume::reCalculateSalesPointsVolume()` - Zeile 54-64: ```php private static function add_KP_TP_Points($userSalesVolume, $month_points) { if ($userSalesVolume->status_points === 2) { // NUR KP $month_points->KP += $userSalesVolume->points; } else { // === 1 oder NULL/0 //TP + KP $month_points->KP += $userSalesVolume->points; $month_points->TP += $userSalesVolume->points; } return $month_points; } ``` **Problem:** Wenn `status_points` nicht gesetzt ist (NULL/0), wird es als `KP + TP` behandelt. **Bei Gutschriften ist das korrekt**, ABER nur wenn die Gutschrift auch durchlaufen wird! --- **HAUPTPROBLEM 3: Timing-Problem** Szenario: 1. Monat 12/2025 läuft 2. User 1218 hat mehrere UserSalesVolume-Einträge 3. `reCalculateSalesPointsVolume()` wird ausgeführt → letzter Eintrag hat z.B. 545 Punkte 4. **NACH** der letzten Berechnung wird eine Gutschrift (32.758 Punkte) hinzugefügt 5. **ABER**: `reCalculateSalesPointsVolume()` wird NICHT erneut aufgerufen 6. ❌ Die 32.758 Punkte fehlen in der Auswertung **Lösung:** Bei jedem Hinzufügen von Gutschriften MUSS `reCalculateSalesPointsVolume()` aufgerufen werden. --- **HAUPTPROBLEM 4: Reihenfolge bei mehreren Einträgen** ```php // User::getUserSalesVolume() - Zeile 609-622 $query = UserSalesVolume::with($relations) ->where('user_id', $this->id) ->where('month', $month) ->where('year', $year) ->orderBy('id', 'DESC'); // ⚠️ Gibt LETZTEN Eintrag zurück switch ($record) { case 'first': return $query->first(); // Letzter Eintrag mit höchster ID } ``` **Annahme:** Der letzte Eintrag (höchste ID) enthält die kumulierte Summe ALLER Punkte. **Problem:** Wenn nach dem letzten Eintrag keine Neuberechnung erfolgt, ist diese Annahme falsch! --- **HAUPTPROBLEM 5: Status-Filter (KEIN Problem)** In der Berechnung gibt es **KEINEN** expliziten Filter für `status = 4` → Gutschriften werden inkludiert. ```php // SalesPointsVolume::reCalculateSalesPointsVolume() - Zeile 70 $userSalesVolumes = UserSalesVolume::where('user_id', $user_id) ->where('month', $month) ->where('year', $year) ->orderBy('id', 'ASC') // ✅ Chronologisch ->get(); // ✅ ALLE Einträge (inkl. status=4) foreach ($userSalesVolumes as $userSalesVolume) { switch ($userSalesVolume->status) { case 1: // Bestellung case 4: // Gutschrift ✅ WIRD VERARBEITET case 5: // Registrierung $month_points = self::add_KP_TP_Points($userSalesVolume, $month_points); } } ``` **Fazit:** Gutschriften werden in der Berechnung korrekt verarbeitet! ✅ --- ### 🧪 Prüfschritte für user_id=1218, month=12, year=2025 **1. Alle UserSalesVolume-Einträge prüfen:** ```sql SELECT id, status, status_points, points, month_KP_points, month_TP_points, month_shop_points, created_at, updated_at, message FROM user_sales_volumes WHERE user_id = 1218 AND month = 12 AND year = 2025 ORDER BY id ASC; ``` **Zu prüfen:** - ✅ Wie viele Einträge gibt es? - ✅ Welche ID hat die Gutschrift (32.758 Punkte, status=4)? - ✅ Hat sie `status_points` gesetzt? (sollte 1 oder 2 sein) - ✅ Ist die Gutschrift der letzte Eintrag (höchste ID)? - ✅ Wenn nicht: Wurde nach der Gutschrift `reCalculateSalesPointsVolume()` aufgerufen? - ✅ Hat der LETZTE Eintrag (höchste ID) die korrekten `month_KP_points`? **2. UserBusiness-Eintrag prüfen:** ```sql SELECT id, user_id, month, year, sales_volume_KP_points, sales_volume_TP_points, sales_volume_points_KP_sum, sales_volume_points_TP_sum, created_at, updated_at FROM user_businesses WHERE id = 21264; ``` **Zu prüfen:** - ✅ Welches Datum hat `updated_at`? - ✅ Wurde der Eintrag VOR oder NACH der Gutschrift erstellt? - ✅ Passt `sales_volume_points_KP_sum` zu den Punkten im letzten UserSalesVolume-Eintrag? **3. Live-Berechnung testen:** ```php // Im Controller oder Tinker: $user = User::find(1218); $value = $user->getUserSalesVolumeBy(12, 2025, 'sales_volume_points_KP_sum'); echo "KP Sum: " . $value . "\n"; // Detailliert: $volumes = UserSalesVolume::where('user_id', 1218) ->where('month', 12) ->where('year', 2025) ->orderBy('id', 'ASC') ->get(); foreach ($volumes as $v) { echo "ID: {$v->id} | Status: {$v->status} | Points: {$v->points} | month_KP: {$v->month_KP_points}\n"; } // Letzter Eintrag: $last = $volumes->last(); echo "Letzter Eintrag - month_KP_points: " . $last->month_KP_points . "\n"; echo " + month_shop_points: " . $last->month_shop_points . "\n"; echo " = " . $last->getPointsKPSum() . "\n"; ``` --- ### 💡 Lösungsansätze **✅ Lösung 1: Neuberechnung erzwingen** Nach jeder Änderung an UserSalesVolume MUSS die Neuberechnung erfolgen: ```php // Überall wo UserSalesVolume erstellt/geändert wird: use App\Services\BusinessPlan\SalesPointsVolume; // Nach create/update: SalesPointsVolume::reCalculateSalesPointsVolume($user_id, $month, $year); ``` **Stellen prüfen:** - Manuelles Hinzufügen (Admin) - Import-Skripte - API-Endpunkte - Migrations/Seeders --- **✅ Lösung 2: Model Event Hook (EMPFOHLEN)** Automatische Neuberechnung bei Änderungen: ```php // In App\Models\UserSalesVolume protected static function booted() { // Nach Create static::created(function ($userSalesVolume) { if ($userSalesVolume->user_id && $userSalesVolume->isCurrentMonthYear()) { \App\Services\BusinessPlan\SalesPointsVolume::reCalculateSalesPointsVolume( $userSalesVolume->user_id, $userSalesVolume->month, $userSalesVolume->year ); } }); // Nach Update static::updated(function ($userSalesVolume) { if ($userSalesVolume->user_id && $userSalesVolume->isCurrentMonthYear()) { \App\Services\BusinessPlan\SalesPointsVolume::reCalculateSalesPointsVolume( $userSalesVolume->user_id, $userSalesVolume->month, $userSalesVolume->year ); } }); } ``` **⚠️ Achtung:** Kann zu Performance-Problemen führen, wenn viele Einträge gleichzeitig aktualisiert werden. --- **✅ Lösung 3: Validation vor Speicherung** Sicherstellen, dass `status_points` immer gesetzt ist: ```php // In App\Models\UserSalesVolume protected static function booted() { static::creating(function ($userSalesVolume) { // Fallback: Wenn status_points nicht gesetzt, Standard = 1 (KP+TP) if (!$userSalesVolume->status_points) { $userSalesVolume->status_points = 1; } }); } ``` --- **✅ Lösung 4: Admin-Prüfung & Warnung** Dashboard-Widget für inkonsistente Daten: ```php // Query für Einträge ohne korrekte Neuberechnung: $inconsistent = DB::select(" SELECT usv1.user_id, usv1.month, usv1.year, MAX(usv1.id) as last_id, (SELECT month_KP_points FROM user_sales_volumes usv2 WHERE usv2.id = MAX(usv1.id)) as last_month_KP, SUM(CASE WHEN usv1.status IN (1,4,5) THEN usv1.points ELSE 0 END) as expected_KP FROM user_sales_volumes usv1 WHERE usv1.month = ? AND usv1.year = ? GROUP BY usv1.user_id, usv1.month, usv1.year HAVING last_month_KP != expected_KP ", [12, 2025]); ``` --- ### 📋 Empfohlene Maßnahmen | Priorität | Maßnahme | Aufwand | Risiko | | --------- | -------------------------------------------------- | ------- | -------------------- | | 🔴 Hoch | Daten für user_id=1218 prüfen (Prüfschritte 1-3) | 30 min | Niedrig | | 🔴 Hoch | Neuberechnung manuell anstoßen für betroffene User | 1h | Niedrig | | 🟡 Mittel | Model Event Hook implementieren (Lösung 2) | 2h | Mittel (Performance) | | 🟢 Niedrig | Validation hinzufügen (Lösung 3) | 1h | Niedrig | | 🟢 Niedrig | Admin-Prüfung implementieren (Lösung 4) | 3h | Niedrig | --- **Nächste Schritte:** 1. ✅ Technische Analyse abgeschlossen (28.01.2026) 2. ✅ Datenbank-Analyse durchgeführt (28.01.2026) 3. ✅ Ursache identifiziert (siehe unten) 4. ⏭️ Lösung implementieren 5. ⏭️ Betroffene User identifizieren und korrigieren --- ## 🎯 URSACHE IDENTIFIZIERT (28.01.2026) ⚠️ **WICHTIG:** Diese Analyse wurde auf dem **Test-Server** durchgeführt. Das Problem existiert auch auf dem **Live-Server (Hetzner/Ploi.io)**! ### Server-Konfiguration **Test-Server:** Lokale Development-Umgebung (Laravel Sail) **Live-Server:** - Hoster: Hetzner (Deutschland) - Verwaltung: Ploi.io - Cron-Job: `cd /home/ploi/mivita.care && php8.4 artisan schedule:run >> /dev/null 2>&1` (läuft jede Minute) - Scheduler: `business:store-optimized 0 0` läuft täglich um **03:00 Uhr** ### Konkrete Analyse für user_id=1218, month=12, year=2025 **UserSalesVolume-Einträge (4 Stück):** | ID | Status | Typ | Punkte | month_KP | month_shop | Datum | | --------- | ------ | ------------- | ------- | -------- | ---------- | ----------------------- | | 32171 | 1 | advisor_order | 52 | 52 | 0 | 16.12.2025 07:39 | | 32377 | 1 | advisor_order | 493 | 545 | 0 | 18.12.2025 22:45 | | 32725 | 2 | shoporder | 80 | 545 | 80 | 30.12.2025 14:12 | | **32758** | **4** | **credit** | **490** | **1035** | **80** | **31.12.2025 17:10** ⚠️ | **Letzter Eintrag (ID 32758) - KORREKT:** - month_KP_points: **1035** ✅ - month_shop_points: 80 ✅ - **getPointsKPSum(): 1115** ✅ **UserBusiness (ID 21264) - VERALTET:** - sales_volume_KP_points: **545** ❌ (fehlen 490 Punkte!) - sales_volume_points_shop: 80 ✅ - sales_volume_points_KP_sum: **625** ❌ (sollte 1115 sein!) - **Erstellt/Updated: 31.12.2025 03:00:28** ⚠️ **Live-Berechnung (TreeCalcBot) - KORREKT:** - getUserSalesVolumeBy('sales_volume_points_KP_sum'): **1115** ✅ - getUserSalesVolumeBy('sales_volume_KP_points'): **1035** ✅ - getUserSalesVolumeBy('sales_volume_points_shop'): 80 ✅ --- ### 🔴 HAUPTURSACHE: UserBusiness wird nach Monatsende nicht aktualisiert **Timeline 31.12.2025:** ``` 03:00:28 → BusinessStoreOptimized läuft (Cron-Job) ├─ Liest UserSalesVolume (nur 3 Einträge vorhanden) ├─ Berechnet: 545 KP + 80 Shop = 625 Gesamt └─ Speichert UserBusiness mit 625 Punkten ✅ 17:10:05 → Gutschrift wird manuell hinzugefügt (490 Punkte) ├─ UserSalesVolume wird erstellt (ID 32758) ✅ ├─ reCalculateSalesPointsVolume() läuft ✅ ├─ Alle UserSalesVolume-Einträge werden aktualisiert ✅ │ └─ Letzter Eintrag hat jetzt: 1035 KP + 80 Shop = 1115 ✅ └─ UserBusiness wird NICHT aktualisiert! ❌ └─ Bleibt bei 625 Punkten (veraltet) ``` **Das Problem:** - UserSalesVolume ist korrekt (1115 Punkte) ✅ - Live-Berechnung ist korrekt (1115 Punkte) ✅ - **Gespeicherte UserBusiness ist veraltet (625 Punkte)** ❌ **Auswirkung:** - Team-Ansichten, die auf UserBusiness basieren, zeigen falsche Werte - Provisionsberechnungen könnten betroffen sein - Reports zeigen inkorrekte Daten für abgeschlossene Monate --- ### 💡 LÖSUNGEN **🔴 Sofortmaßnahme: Manuelle Korrektur** ```bash # UserBusiness für betroffenen Monat neu berechnen php artisan business:store-optimized 12 2025 --clear ``` **Oder nur für User 1218:** ```php // In Tinker oder als Command $user = User::find(1218); $month = 12; $year = 2025; // BusinessUserItem erstellen mit Live-Daten $treeCalc = new \App\Services\BusinessPlan\TreeCalcBotOptimized($month, $year, 'admin', true); $treeCalc->initStructureUser($user->id, true); $businessUserItem = $treeCalc->getBusinessUser($user->id); // UserBusiness aktualisieren $userBusiness = \App\Models\UserBusiness::where('user_id', 1218) ->where('month', 12) ->where('year', 2025) ->first(); if ($userBusiness && $businessUserItem) { $userBusiness->sales_volume_KP_points = $businessUserItem->b_user->sales_volume_KP_points; $userBusiness->sales_volume_TP_points = $businessUserItem->b_user->sales_volume_TP_points; $userBusiness->sales_volume_points_KP_sum = $businessUserItem->b_user->sales_volume_points_KP_sum; $userBusiness->sales_volume_points_TP_sum = $businessUserItem->b_user->sales_volume_points_TP_sum; $userBusiness->save(); echo "UserBusiness updated!\n"; } ``` --- **🟡 Mittelfristige Lösung: Automatische Aktualisierung** Option A: Event Hook in UserSalesVolume ```php // In App\Models\UserSalesVolume protected static function booted() { static::created(function ($userSalesVolume) { self::updateUserBusinessIfExists($userSalesVolume); }); static::updated(function ($userSalesVolume) { self::updateUserBusinessIfExists($userSalesVolume); }); } private static function updateUserBusinessIfExists($userSalesVolume) { // Prüfe ob UserBusiness für diesen Monat bereits existiert $userBusiness = \App\Models\UserBusiness::where('user_id', $userSalesVolume->user_id) ->where('month', $userSalesVolume->month) ->where('year', $userSalesVolume->year) ->first(); if ($userBusiness) { // Hole aktualisierte Werte aus UserSalesVolume $user = \App\User::find($userSalesVolume->user_id); $userBusiness->sales_volume_KP_points = $user->getUserSalesVolumeBy( $userSalesVolume->month, $userSalesVolume->year, 'sales_volume_KP_points' ); $userBusiness->sales_volume_TP_points = $user->getUserSalesVolumeBy( $userSalesVolume->month, $userSalesVolume->year, 'sales_volume_TP_points' ); $userBusiness->sales_volume_points_shop = $user->getUserSalesVolumeBy( $userSalesVolume->month, $userSalesVolume->year, 'sales_volume_points_shop' ); $userBusiness->sales_volume_points_KP_sum = $user->getUserSalesVolumeBy( $userSalesVolume->month, $userSalesVolume->year, 'sales_volume_points_KP_sum' ); $userBusiness->sales_volume_points_TP_sum = $user->getUserSalesVolumeBy( $userSalesVolume->month, $userSalesVolume->year, 'sales_volume_points_TP_sum' ); $userBusiness->save(); \Log::info("UserBusiness auto-updated after UserSalesVolume change", [ 'user_id' => $userSalesVolume->user_id, 'month' => $userSalesVolume->month, 'year' => $userSalesVolume->year ]); } } ``` Option B: Admin-Command für Nachberechnung ```php // php artisan business:update-month 12 2025 namespace App\Console\Commands; use Illuminate\Console\Command; use App\Models\UserBusiness; use App\User; class BusinessUpdateMonth extends Command { protected $signature = 'business:update-month {month} {year}'; protected $description = 'Update UserBusiness for a specific month with current UserSalesVolume data'; public function handle() { $month = (int) $this->argument('month'); $year = (int) $this->argument('year'); $userBusinesses = UserBusiness::where('month', $month) ->where('year', $year) ->get(); $this->info("Updating {$userBusinesses->count()} UserBusiness entries..."); foreach ($userBusinesses as $userBusiness) { $user = User::find($userBusiness->user_id); if (!$user) continue; $userBusiness->sales_volume_KP_points = $user->getUserSalesVolumeBy($month, $year, 'sales_volume_KP_points'); $userBusiness->sales_volume_TP_points = $user->getUserSalesVolumeBy($month, $year, 'sales_volume_TP_points'); $userBusiness->sales_volume_points_shop = $user->getUserSalesVolumeBy($month, $year, 'sales_volume_points_shop'); $userBusiness->sales_volume_points_KP_sum = $user->getUserSalesVolumeBy($month, $year, 'sales_volume_points_KP_sum'); $userBusiness->sales_volume_points_TP_sum = $user->getUserSalesVolumeBy($month, $year, 'sales_volume_points_TP_sum'); $userBusiness->save(); $this->info("Updated user {$userBusiness->user_id}"); } $this->info("Done!"); } } ``` --- **🟢 Langfristige Lösung: Validierung & Monitoring** Dashboard-Widget für Inkonsistenzen: ```php // Finde alle User mit Differenzen zwischen UserSalesVolume und UserBusiness $inconsistencies = DB::select(" SELECT ub.id as user_business_id, ub.user_id, ub.month, ub.year, ub.sales_volume_points_KP_sum as stored_kp_sum, (SELECT month_KP_points + month_shop_points FROM user_sales_volumes WHERE user_id = ub.user_id AND month = ub.month AND year = ub.year ORDER BY id DESC LIMIT 1) as actual_kp_sum, ub.updated_at FROM user_businesses ub WHERE ub.month = ? AND ub.year = ? HAVING stored_kp_sum != actual_kp_sum ", [12, 2025]); ``` --- ### 📋 Aktualisierte Maßnahmen | Priorität | Maßnahme | Aufwand | Status | | ------------ | -------------------------------------- | ------- | ------- | | 🔴 **JETZT** | UserBusiness für 12/2025 neu berechnen | 5 min | ⏭️ TODO | | 🔴 Hoch | Inkonsistenzen für alle Monate finden | 30 min | ⏭️ TODO | | 🟡 Mittel | Event Hook implementieren (Option A) | 2h | ⏭️ TODO | | 🟡 Mittel | Update-Command erstellen (Option B) | 1h | ⏭️ TODO | | 🟢 Niedrig | Dashboard-Validierung | 3h | ⏭️ TODO | --- ### 🚨 UMFANG DES PROBLEMS **Es sind mindestens 20 User im Dezember 2025 betroffen!** Top 10 betroffene User: | User ID | Gespeichert | Aktuell | Differenz | UserBusiness ID | | ------- | ----------- | ------- | --------- | --------------- | | 1218 | 625 | 1115 | **+490** | 21264 | | 1001 | 172 | 365 | **+193** | 21239 | | 1156 | 646 | 837 | **+191** | 21064 | | 909 | 1353 | 1527 | **+174** | 21215 | | 675 | 0 | 151 | **+151** | 21115 | | 1313 | 283 | 431 | **+148** | 21171 | | 1364 | 398 | 518 | **+120** | 21063 | | 1225 | 649 | 755 | **+106** | 21080 | | 586 | 494 | 593 | **+99** | 21071 | | 3 | 11039 | 11112 | **+73** | 20979 | **Muster:** Alle wurden am **31.12.2025 ca. 03:00 Uhr** berechnet (Cron-Job), danach wurden Gutschriften/Einträge hinzugefügt. **Gesamt-Differenz (nur Top 20):** Ca. **2.000+ Punkte** fehlen in gespeicherten Daten! --- **Empfehlung:** 🔴 **SOFORT (KRITISCH):** ```bash # Gesamten Monat neu berechnen (alle betroffenen User) php artisan business:store-optimized 12 2025 --clear ``` 🟡 **MITTELFRISTIG (diese Woche):** 1. Event Hook implementieren (verhindert zukünftige Probleme) 2. Prüfen ob auch andere Monate betroffen sind: ```bash php artisan business:check-inconsistencies 11 2025 php artisan business:check-inconsistencies 10 2025 ``` 🟢 **LANGFRISTIG:** - Dashboard-Monitoring für frühzeitige Erkennung - Automatische E-Mail-Benachrichtigung bei Inkonsistenzen --- ### [!] 10. Nicht zugeordnete Zahlungen/Punkte **Priorität:** Hoch **Bereich:** Payment / Admin **Problem:** Zahlungen ohne Zuordnung → Punkte verschwinden, keine Provision. **Anforderung:** - Admin-Hinweis bei nicht zugeordneten Zahlungen - Manuelle Zuordnungsmöglichkeit **Technische Details:** | Parameter | Wert | |-----------|------| | Tabelle | `user_sales_volumes` | | Status-Feld | `status = 0` (not_assigned) | | Admin-View | Dashboard oder separate Sektion | **Query für nicht zugeordnete Einträge:** ```php $unassigned = UserSalesVolume::where('status', 0) ->with('user', 'order') ->orderBy('created_at', 'desc') ->get(); ``` **Umsetzung:** 1. **Dashboard-Alert:** Anzahl nicht zugeordneter Einträge anzeigen 2. **Admin-Seite:** Liste aller nicht zugeordneten Einträge 3. **Zuordnungs-Modal:** - User auswählen (Dropdown/Suche) - Status aktualisieren (1 = advisor_order, 2 = shoporder) - Punkte werden bei nächster Berechnung berücksichtigt --- ### [ ] 11. Monatsstatistik Erweiterungen **Priorität:** Mittel **Bereich:** Dashboard / Team **Probleme:** - Teamumsatz wird seit Januar nicht angezeigt - Neupartner/Abos nicht klickbar (keine Detailansicht) **Anforderungen:** | Feature | Beschreibung | |---------|--------------| | Teamumsatz | Bug fixen - wird nicht angezeigt | | Neupartner Details | Klick → Liste mit Name, E-Mail, Telefon, Generation, Mentor | | Team-Abos Details | Klick → Liste mit Abo-Details | | 1000-Punkte-Shops | Neue Kennzahl: Teampartner mit ≥1000 Punkte persönlichem Volumen | | Aktuelle Provision | In Monatsstatistik anzeigen | | Downline-Kontakte | Telefon, E-Mail, Adresse der eigenen Downline abrufbar (nicht nur VIPs) | **Technische Details:** | Parameter | Wert | |-----------|------| | Service | `App\Services\LevelReportService` | | Controller | `App\Http\Controllers\User\TeamController` | | View | `resources/views/user/team/marketingplan.blade.php` | | Daten | `user_business` Tabelle | **1000-Punkte Query:** ```php $count1000 = UserBusiness::where('month', $month) ->where('year', $year) ->where('sales_volume_points_sum', '>=', 1000) ->whereIn('user_id', $teamUserIds) ->count(); ``` **Klickbare Details (AJAX Modal):** ```php // Route Route::get('/team/new-partners/{month}/{year}', [TeamController::class, 'newPartnersDetail']); // Response return response()->json([ 'partners' => $partners->map(fn($p) => [ 'name' => $p->full_name, 'email' => $p->email, 'phone' => $p->phone, 'generation' => $p->generation, 'mentor' => $p->mentor->full_name ?? '-' ]) ]); ``` --- ### [x] 12. Bezahllink Status-Unterscheidung **Priorität:** Mittel **Bereich:** Payment / Admin **Problem:** Unklar ob Payment-Link nur geklickt oder Zahlung wirklich durchgeführt. **Anforderung:** | Status | Bedeutung | Farbe | |--------|-----------|-------| | `link_sent` | Link wurde versendet | grau | | `link_clicked` | Link wurde geklickt, keine Zahlung | orange | | `payment_pending` | Zahlung in Bearbeitung | gelb | | `paid` | Zahlung erfolgreich | grün | | `failed` | Zahlung fehlgeschlagen | rot | **Technische Details:** | Parameter | Wert | |-----------|------| | Tabelle | `shopping_payments` | | Feld | `txaction` (VARCHAR 20) | | Service | `App\Services\Payment` | **Aktuelle Status (ungenau):** ```php 'paid' => 'paid' 'appointed' => 'open' // ← Zu ungenau 'failed' => 'failed' 'extern' => 'open' // ← Zu ungenau ``` **Umsetzung:** 1. Neues Feld oder erweiterte `txaction`-Werte 2. Bei Payment-Link-Aufruf: Status auf `link_clicked` setzen 3. Bei Payone-Callback: Status entsprechend aktualisieren 4. Admin-View: Farbkodierung nach neuem Schema --- ### [?] 13. Steuerberater-Modul **Priorität:** Niedrig **Status:** Noch zu definieren **Notiz:** Weitere Infos liegen vor - müssen noch spezifiziert werden. **TODO:** Anforderungen dokumentieren --- ### [X] 14. DHL Modul Erweiterungen **Status:** ✅ ERLEDIGT **Priorität:** Hoch **Bereich:** Versand / packages/acme-laravel-dhl **Implementierte Funktionen:** | Feature | Status | Beschreibung | |---------|--------|--------------| | Storno-Etiketten UI | ✅ | Admin-Button für Label-Stornierung | | Tracking-Abfrage | ✅ | Status automatisch abrufen | | Tracking-Mail | ✅ | Kunde über Versand informieren | **Technische Details:** | Parameter | Wert | |-----------|------| | Package | `packages/acme-laravel-dhl` | | Model | `DhlShipment` | | Tabelle | `dhl_package_shipments` | | Status-Feld | `status` (created/in_transit/delivered/canceled) | | Tracking-Tabelle | `dhl_tracking_events` | **Vorhandene Jobs (bereits implementiert):** - `CreateShipmentJob` ✓ - `CancelShipmentJob` ✓ (existiert, nutzt `canCancel()`) - `CreateReturnLabelJob` ✓ - `SyncTrackingJob` ✓ (Webhook-basiert) **Durchgeführte Implementierung:** 1. ✅ **Admin-UI für Storno:** - Button "Label stornieren" in Bestellansicht (`_detail_dhl_shipments.blade.php`) - Button im DHL Cockpit DataTable aktiviert (`DhlShipmentController.php`) - JavaScript Handler für Storno-Button in beiden Views - Dispatcht `CancelShipmentJob` - Nur wenn `$shipment->canCancel()` = true - Bestätigungsdialog mit Warnung vor ungültigem Label 2. ✅ **Tracking-Mail an Kunde:** - Mail-Klasse: `App\Mail\MailDhlTracking` (bereits vorhanden) - E-Mail Template: `resources/views/emails/dhl_tracking.blade.php` - Trigger: Nach Status-Update auf `in_transit` (automatisch via Cron) - Manueller Versand: Button in Admin-UI - Inhalt: Sendungsnummer + Tracking-Link + Bestellnummer - Übersetzungen: DE, EN, ES bereits vorhanden - Tracking Status wird in Datenbank gespeichert (tracking_email_sent_at, tracking_email_type) 3. ✅ **Cron für Tracking (Alternative zu Webhook):** - Command: `app/Console/Commands/DhlUpdateTracking.php` - Signature: `php artisan dhl:update-tracking` - Optionen: - `--days=14`: Sendungen der letzten X Tage aktualisieren - `--send-emails`: Automatisch E-Mails bei Transit-Status senden - `--dry-run`: Nur simulieren, keine Änderungen - Cron-Job eingetragen in `app/Console/Kernel.php`: - Täglich um 06:00 Uhr - Mit automatischem E-Mail-Versand - `withoutOverlapping()` und `runInBackground()` - Statistik-Ausgabe: updated, failed, emails_sent, skipped 4. ✅ **Retourenlabel-Button:** - Button "Retourenlabel erstellen" im DHL Cockpit aktiviert - JavaScript Handler hinzugefügt - Dispatcht `CreateReturnLabelJob` - Nur für ausgehende Sendungen ohne vorhandene Retoure **Routen (bereits vorhanden):** ```php Route::delete('/admin/dhl/shipment/{shipment}/cancel', ...) # Storno Route::post('/admin/dhl/shipment/{shipment}/return-label', ...) # Retourenlabel Route::post('/admin/dhl/shipment/{shipment}/update-tracking', ...) # Tracking Update Route::post('/admin/dhl/shipment/{shipment}/send-tracking-email', ...) # E-Mail senden ``` **DHL API Endpunkte:** ``` DELETE /parcel/de/shipping/v2/orders/{shipmentNumber} # Storno GET /parcel/de/tracking/v1/shipments/{shipmentNumber} # Tracking ``` **Betroffene Dateien:** 1. `resources/views/admin/sales/_detail_dhl_shipments.blade.php` - Storno-Button hinzugefügt 2. `resources/views/admin/dhl/cockpit.blade.php` - JavaScript Handler erweitert 3. `app/Http/Controllers/DhlShipmentController.php` - Nutzt jetzt `DhlShipmentService` 4. `app/Services/DhlShipmentService.php` - **Erweitert um `cancelShipment()` Methode** 5. `app/Jobs/CancelShipmentJob.php` - **Aktualisiert für neues Package-Model** 6. `app/Console/Commands/DhlUpdateTracking.php` - Tracking Command (bereits vorhanden) 7. `app/Console/Kernel.php` - Cron-Job (bereits eingetragen) 8. `app/Mail/MailDhlTracking.php` - E-Mail Klasse (bereits vorhanden) 9. `resources/views/emails/dhl_tracking.blade.php` - E-Mail Template (bereits vorhanden) 10. `resources/lang/{de,en,es}/email.php` - Übersetzungen (bereits vorhanden) **Fix für Model-Typ-Konflikt & Queue-Config:** - `CancelShipmentJob` wurde von `App\Models\DhlShipment` auf `Acme\Dhl\Models\DhlShipment` migriert - Nutzt jetzt `Acme\Dhl\Services\ShippingService::cancelLabel()` aus dem neuen Package - Verwendet `dhl_shipment_no` statt `shipment_number` (korrektes Feld-Mapping) - **`DhlShipmentService::cancelShipment()` hinzugefügt:** - Prüft `DHL_USE_QUEUE` Config-Einstellung - Verwendet Queue (`CancelShipmentJob`) wenn aktiviert - Führt synchron aus (`ShippingService::cancelLabel()`) wenn deaktiviert - Konsistentes Verhalten wie bei `createShipment()` - **Controller nutzt jetzt Service statt direkt Job zu dispatchen:** - `DhlShipmentController::cancel()` ruft `DhlShipmentService::cancelShipment()` auf - Automatische Entscheidung zwischen Queue/Sync basierend auf Config **Verwendung:** **Manuell:** ```bash # Tracking aktualisieren (Simulation) php artisan dhl:update-tracking --dry-run # Tracking aktualisieren mit E-Mail-Versand php artisan dhl:update-tracking --days=7 --send-emails # Nur letzte 3 Tage aktualisieren php artisan dhl:update-tracking --days=3 # NEU: Test-E-Mail an eigene Adresse php artisan dhl:update-tracking --send-emails --test-email=admin@firma.de # NEU: Nur für bestimmte Bestellung php artisan dhl:update-tracking --send-emails --order=45078 ``` **Automatisch via Cron:** - Läuft täglich um 06:00 Uhr - Aktualisiert Sendungen der letzten 14 Tage - Sendet automatisch E-Mails bei Status-Änderung zu "in_transit" - Verhindert Überlappungen mit `withoutOverlapping()` **NEU: Mehrere Sendungen in einer E-Mail:** - Wenn eine Bestellung mehrere Labels hat, werden alle in einer E-Mail zusammengefasst - Automatisch beim manuellen Versand über Admin-Button - Automatisch beim Cronjob-Versand - Zeigt "Paket 1, Paket 2, Paket 3" mit jeweiliger Tracking-Nummer - Markiert alle Sendungen als versendet **NEU: Versand-Status in Bestelldetails:** - Zeigt wann E-Mail versendet wurde - Zeigt ob automatisch (Cronjob) oder manuell (Admin) - Icons: 🤖 Automatisch / 👤 Manuell **NEU: E-Mail-Feld für bestehende Sendungen nachfüllen:** ```bash # Dry-Run (nur simulieren) php artisan dhl:backfill-emails --dry-run # Tatsächlich ausführen php artisan dhl:backfill-emails ``` **NEU: E-Mail + Postnummer bei Label-Erstellung:** - ✅ Migration hinzugefügt: `2026_01_23_140000_add_email_and_postnumber_to_dhl_shipments.php` - ✅ Neue Felder in `dhl_package_shipments`: `email`, `postnumber` - ✅ Model `DhlShipment` erweitert um beide Felder - ✅ Formular-Feld für E-Mail hinzugefügt in `modal_in_order_shipment.blade.php` - ✅ E-Mail-Feld ist Pflichtfeld mit Validierung (type="email", required) - ✅ Vorbefüllung mit Billing-E-Mail aus `order->shopping_user->email` - ✅ Postnummer-Feld bereits vorhanden (optional für Packstation) - ✅ Controller-Validierung erweitert: `shipping_email` (required), `shipping_postnumber` (nullable) - ✅ `DhlDataHelper` übergibt E-Mail + Postnummer an ShippingService - ✅ `ShippingService::createShipmentRecord()` speichert beide Felder in DB - ✅ Daten werden sowohl direkt als auch im JSON `recipient` gespeichert **Zweck der Felder:** - `email`: Wird für DHL Benachrichtigungen und Tracking-E-Mails verwendet - `postnumber`: DHL Postnummer (6-10 Stellen) für Packstation/Paketbox-Lieferungen **E-Mail-Button-Logik:** - ✅ Button wird angezeigt, wenn Sendung eine `dhl_shipment_no` hat UND eine E-Mail verfügbar ist - ✅ Priorisierung: Shipment-Email > Shopping-User-Email - ✅ `canSendTrackingEmail()` prüft zuerst das neue `email` Feld - ✅ Fallback auf `shopping_user->email` wenn Shipment-Email leer - ✅ Button funktioniert in beiden Views: Bestelldetails + DHL Cockpit **E-Mail-Versand-Priorisierung:** 1. **Test-E-Mail** (falls angegeben im Cronjob mit `--test-email`) 2. **Shipment-Email** (aus `dhl_package_shipments.email`) 3. **Shopping-User-Email** (Fallback aus `shopping_users.email`) **Betroffene Dateien (E-Mail + Postnummer):** 1. `database/migrations/2026_01_23_140000_add_email_and_postnumber_to_dhl_shipments.php` - NEU 2. `packages/acme-laravel-dhl/src/Models/DhlShipment.php` - fillable + canSendTrackingEmail() erweitert 3. `resources/views/admin/dhl/modal_in_order_shipment.blade.php` - E-Mail-Feld hinzugefügt 4. `app/Http/Controllers/DhlShipmentController.php` - Validierung + E-Mail-Priorisierung 5. `packages/acme-laravel-dhl/src/Services/ShippingService.php` - Speicherung erweitert 6. `app/Console/Commands/DhlUpdateTracking.php` - E-Mail-Priorisierung im Cronjob 7. `app/Services/DhlDataHelper.php` - Übergibt E-Mail + Postnummer (bereits vorhanden) 8. `app/Services/DhlModalService.php` - Liest Formularfelder (bereits vorhanden) 9. `resources/views/admin/dhl/show.blade.php` - E-Mail-Button + JavaScript Handler 10. `resources/views/admin/sales/_detail_dhl_shipments.blade.php` - E-Mail-Button + Handler (bereits vorhanden) 11. `resources/views/admin/dhl/modal_in_shipment_info.blade.php` - E-Mail-Button im Modal 12. `app/Console/Commands/DhlBackfillEmails.php` - Command zum Nachfüllen (NEU) **E-Mail-Button Standorte:** ✅ Bestelldetails (\_detail_dhl_shipments.blade.php) ✅ DHL Cockpit (cockpit.blade.php) ✅ DHL Detail-Seite (show.blade.php) ✅ Modal nach Label-Erstellung (modal_in_shipment_info.blade.php) **NEU: Return Label (Retourenlabel) Funktionalität:** ✅ Button in allen DHL Views hinzugefügt ✅ Controller mit Sync/Async Unterstützung (DHL_USE_QUEUE) ✅ Job aktualisiert für neues DHL Package ✅ Automatisches Adress-Tausch (Kunde → Absender, Lager → Empfänger) ✅ Prüfung ob bereits Retoure existiert ✅ Nur für ausgehende Sendungen verfügbar ✅ **API-Fix (23.01.2026):** ReturnsService statt ShippingService verwenden ✅ Korrekter DHL Returns API Endpunkt: `/parcel/de/returns/v1/labels` ✅ Korrekte Payload-Struktur für Returns API ✅ Verbesserte Validierung und Fehlerbehandlung ✅ Erweitertes Logging für Debugging ✅ **Country-Code Fix:** Automatische Konvertierung 2-stellig → 3-stellig (DE → DEU) ✅ **Fallback-Implementierung:** Automatischer Fallback zu regulärer Shipping-API (V07PAK) bei fehlenden Returns-API Berechtigungen ✅ Intelligente Fehlerbehandlung für Auth-Fehler (401/403) ✅ Transparentes Logging welche Methode verwendet wird ✅ **Fallback-Fixes (23.01.2026):** Country-Code Konvertierung (3→2 Buchstaben), Dimensions hinzugefügt, print_format gesetzt ✅ Automatische Adress-Konvertierung für ShippingService-Kompatibilität ✅ **V01PAK statt V07PAK:** V07PAK nicht verfügbar, verwende V01PAK (Standard DHL Paket) mit vertauschten Adressen ✅ **Return-Label Fixes (23.01.2026 - 17:30):** Type-Update korrigiert ($result['shipment'] statt $result['shipmentId']) ✅ Doppelklick-Schutz für Return-Button implementiert ✅ Existierende Return-Labels (ID 18, 19) manuell korrigiert zu type='return' ✅ **Packstation Return-Label mit Billing-Adresse (23.01.2026 - 19:00):** Return-Labels für Packstation möglich! ✅ Bei Packstation-Sendungen wird Rechnungsadresse als Return-Absender verwendet ✅ Automatische Packstation-Erkennung (postNumber-Check) ✅ getBillingAddressForReturn() extrahiert Straße + Hausnummer aus billing_address ✅ Gleiche Logik in DhlShipmentController + CreateReturnLabelJob ✅ UI: Return-Button aktiv für ALLE Outbound-Sendungen (Blockierung entfernt) **Return Label Button Standorte:** ✅ Bestelldetails (\_detail_dhl_shipments.blade.php) - NEU ✅ DHL Cockpit (cockpit.blade.php) - funktioniert ✅ DHL Detail-Seite (show.blade.php) - aktiviert ✅ Nur sichtbar wenn: type='outbound' UND keine Retoure existiert **NEU: Return Label Visuelle Hervorhebung (23.01.2026):** ✅ Return-Etiketten deutlich erkennbar mit oranger Farbgebung ✅ Orange "RETOURE" Badge (statt blau) in allen Listen ✅ Orange ID-Links mit Undo-Icon in allen Tabellen ✅ Zeilen-Highlighting in DataTable (orangener Hintergrund + linker Border) ✅ Zeilen-Highlighting in Order-Details (orangener Hintergrund) ✅ Größeres, fetteres Badge in Detail-Ansicht (show.blade.php) ✅ CSS-Klasse `return-shipment` für DataTable-Zeilen ✅ Konsistente orange Farbgebung (`badge-warning`, `text-warning`, `#ffc107`) ✅ Return-Etiketten bekommen KEINEN "Retourenlabel erstellen" Button **Betroffene Dateien (Styling):** 1. `app/Http/Controllers/DhlShipmentController.php` - DataTable Spalten (ID, Typ) 2. `resources/views/admin/dhl/cockpit.blade.php` - CSS + JS für Zeilen-Highlighting 3. `resources/views/admin/dhl/show.blade.php` - Header Badge + Icon 4. `resources/views/admin/sales/_detail_dhl_shipments.blade.php` - Zeilen-Style + Badge **Dokumentation:** `dev/23-01-2026/dhl-return-label-styling.md` --- ### [X] 12. Payment Link Status - Legende & Race Condition Fix **Priorität:** Hoch ✅ **ERLEDIGT** (28.01.2026) **Bereich:** Backend / Payment / Payone Integration **Problem 1: Keine Status-Übersicht** Benutzer sahen Payment Status-Badges ohne Erklärung in der Übersicht. **Problem 2: Race Condition Bug** Wenn Payone Requests in falscher Reihenfolge ankamen (`paid` vor `appointed`), wurde der Status falsch gesetzt: - `paid` kam zuerst → Status = 10 (korrekt) - `appointed` kam danach → Status = 4 (FALSCH - überschrieb paid!) **Problem 3: Status nur für Abos** Payment Link Status 10 (`link_paid`) wurde nur für Abo-Bestellungen gesetzt, normale Bestellungen blieben auf Status 4. **Lösung implementiert:** #### 1. Dynamische Status-Legende ✅ Status-Legende oberhalb der Payment Links Tabelle ✅ Vollständig dynamisch aus `OrderPaymentService::getStatusBadgeClasses()` ✅ Mehrsprachig über Laravel Translation-System (DE, EN, ES) ✅ Automatische Synchronisation mit tatsächlichen Status-Definitionen **Status-Hierarchie:** ```php $statuses = [ 0 => 'link_sent', // default (grau) 1 => 'link_openly', // info (blau) 2 => 'link_check', // warning (gelb) 3 => 'link_pending', // warning (gelb) 4 => 'link_appointed', // warning (gelb) 5 => 'link_failed', // danger (rot) 6 => 'link_canceled', // danger (rot) 10 => 'link_paid', // secondary (grau) - FINAL STATE ]; ``` #### 2. Payone Race Condition Fix ✅ Prioritätsprüfung für `txaction` Updates in `PayoneController` ✅ `paid` (Priorität 10) kann nicht durch `appointed` (Priorität 1) überschrieben werden ✅ Logging bei übersprungenen Updates für Nachvollziehbarkeit **Prioritäten:** ```php $txaction_priority = [ 'appointed' => 1, 'pending' => 2, 'failed' => 3, 'paid' => 10, // höchste Priorität - finaler Status ]; ``` #### 3. Status Update für ALLE Bestellungen ✅ `Payment::paymentStatusPaidAction` setzt Status 10 für ALLE Zahlungen ✅ Nicht mehr nur für Abo-Bestellungen beschränkt ✅ Konsistente Status-Vergabe über alle Bestellungstypen #### 4. ShoppingInstance Model Fix ✅ Primary Key korrekt definiert: `identifier` (string, non-incrementing) ✅ Laravel Eloquent funktioniert jetzt korrekt mit der Tabelle **Model-Konfiguration:** ```php protected $primaryKey = 'identifier'; public $incrementing = false; protected $keyType = 'string'; ``` #### 5. Artisan Command für Datenbank-Cleanup ✅ Neuer Command: `php artisan payment:fix-link-status` ✅ Dry-Run Modus (`--dry-run`) zur sicheren Prüfung ✅ Korrigiert historische Payment Links mit falschem Status ✅ Detaillierte Zusammenfassung und Logging **Command Statistik (28.01.2026):** - ✅ 2.117 Payment Links korrigiert (Status 4 → 10) - ✅ 490 bereits korrekt (durch neue Logik) - ⚠️ 12.754 ShoppingInstances nicht gefunden (bereits gelöscht nach Abschluss) **Implementierte Dateien:** 1. **OrderPaymentService** (`app/Services/OrderPaymentService.php`) - Neue Methode: `getStatusBadgeClasses()` für zentrale Status-Definition - `getStatusBadge()` nutzt jetzt zentrale Definition 2. **Payment Links View** (`resources/views/user/order/payment/index.blade.php`) - Dynamische Status-Legende über `@foreach` generiert - Mehrsprachige Labels via `__('payment.' . $statusKey)` 3. **PayoneController** (`app/Http/Controllers/Api/PayoneController.php`) - Prioritätsprüfung vor `txaction` Update - Logging bei übersprungenen Updates 4. **Payment Service** (`app/Services/Payment.php`) - Status 10 wird jetzt für ALLE Bestellungen gesetzt (Zeile 262-267) - Nicht mehr nur innerhalb der `is_abo` Bedingung 5. **ShoppingInstance Model** (`app/Models/ShoppingInstance.php`) - Primary Key korrekt konfiguriert - Laravel Eloquent funktioniert mit identifier-basierter Tabelle 6. **Artisan Command** (`app/Console/Commands/FixPaymentLinkStatus.php`) - Findet alle bezahlten Orders mit falschem ShoppingInstance Status - Dry-Run Modus für sichere Prüfung - Detaillierte Ausgabe und Statistiken 7. **Übersetzungen** (korrigiert) - **DE:** `resources/lang/de/payment.php` - vollständig - **EN:** `resources/lang/en/payment.php` - vollständig - **ES:** `resources/lang/es/payment.php` - 2 Übersetzungen korrigiert: - `link_paid`: "pagado" → "Pago exitoso" - `link_check`: "Pago en curso" → "Pago en revisión" **Verwendung:** ```bash # Prüfen, welche Payments korrigiert würden php artisan payment:fix-link-status --dry-run # Tatsächliche Korrektur durchführen php artisan payment:fix-link-status ``` **Vorteile:** - ✅ Benutzer verstehen Payment Status auf einen Blick - ✅ Keine Race Condition mehr bei Payone Requests - ✅ Konsistente Status-Vergabe für alle Bestellungstypen - ✅ Mehrsprachig und automatisch synchron mit Backend-Logik - ✅ Historische Daten wurden bereinigt - ✅ Wartbar durch zentrale Status-Definition **Dokumentation:** `dev/28-01-2026/payment-status-legend-and-race-condition-fix.md` --- ## ZUSAMMENFASSUNG | # | Aufgabe | Priorität | Komplexität | Bereich | | ------ | -------------------------------------- | ---------- | ----------- | --------------- | | 1 | News Links + Datei-Auswahl | Hoch | Niedrig | Frontend | | 2 | Points DECIMAL | Hoch | Hoch | DB/Backend | | 3 | Vorkasse TXID Hinweis | Hoch | Niedrig | Frontend | | 4 | Packstation/Postnummer | Mittel | Mittel | DB/Frontend | | 5 | Set-Produkte (wie Inhaltsstoffe) | Mittel | Hoch | DB/Backend | | 6 | Mehrsprachigkeit PDFs | Hoch | Mittel | Backend | | ~~7~~ | ~~Stornorechnungen + Punktekorrektur~~ | ~~Hoch~~ | ~~Hoch~~ | ✅ **ERLEDIGT** | | 8 | Französisch | Mittel | Niedrig | i18n | | 9 | Gutschriften Punkte Bug | Hoch | Mittel | Backend | | 10 | Nicht zugeordnete Zahlungen | Hoch | Mittel | Backend | | 11 | Monatsstatistik Erweiterungen | Mittel | Mittel | Backend | | ~~12~~ | ~~Bezahllink Status~~ | ~~Mittel~~ | ~~Niedrig~~ | ✅ **ERLEDIGT** | | 13 | Steuerberater | Niedrig | ? | TBD | | 14 | DHL UI + Tracking-Mail | Hoch | Mittel | Package | --- ## EMPFOHLENE REIHENFOLGE ### Phase 1: Quick Wins (Frontend, niedrige Komplexität) - [x] #1 News Links **ERLEDIGT 22.01.2026** - [x] #3 Vorkasse TXID Hinweis **ERLEDIGT 22.01.2026** - [x] #12 Bezahllink Status ✅ **ERLEDIGT 28.01.2026** ### Phase 2: Kritische Bugs (Provisionen betroffen) - [x] #9 Gutschriften Punkte Bug - [ ] #10 Nicht zugeordnete Zahlungen ### Phase 3: Infrastruktur (DB-Änderungen) - [x] #2 Points DECIMAL (benötigt Migration + Testing) **ERLEDIGT 22.01.2026** - [x] #7 Stornorechnungen mit Punktekorrektur ✅ **ERLEDIGT 06.02.2026** ### Phase 4: Features - [x] #6 Mehrsprachigkeit PDFs - [x] #14 DHL UI + Tracking-Mail **ERLEDIGT 23.01.2026** - [ ] #11 Monatsstatistik Erweiterungen ### Phase 5: Langfristig - [x] #4 Packstation/Postnummer **ERLEDIGT 22.01.2026** - [x] #5 Set-Produkte Bundles **ERLEDIGT 22.01.2026** - [ ] #8 Französisch - [ ] #13 Steuerberater-Modul