mivita/dev/2026-01-22/next-steps.md
2026-02-20 17:55:06 +01:00

77 KiB

Development Backlog - 22.01.2026

Status-Legende

  • [x] Erledigt
  • [ ] Offen
  • [!] Hohe Priorität
  • [?] Klärungsbedarf

ERLEDIGTE AUFGABEN

[x] Produkt-Slugs anpassen

[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:

{
  "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:

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:

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:

ALTER TABLE products
  MODIFY points DECIMAL(10,2);

Weitere Models mit points:

  • App\Models\ShoppingOrderpoints
  • App\Models\ShoppingOrderItempoints
  • App\Models\ShoppingCollectOrderpoints
  • App\Models\HomepartyUserOrderItempoints

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:

// 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
  1. 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
  1. 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
  1. 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
  1. 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!

  1. 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!
  1. 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:

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):

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:

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:

// 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:

// 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:

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):

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

  • Stornorechnung erstellen für Berater-Bestellung
  • Stornorechnung erstellen für Shop-Bestellung
  • Stornorechnung erstellen für Homeparty
  • Stornorechnung erstellen für Sammelbestellung
  • PDF-Download DE, EN, ES
  • Punktekorrektur in user_sales_volumes prüfen
  • Monatssummen in Business Points prüfen
  • Upline-Punkte prüfen (MLM-Hierarchie)
  • Bestellstatus "Storniert" anzeigen
  • Versandstatus "Storniert" (nur bei offenen)
  • Doppel-Storno verhindert
  • 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:

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:

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

// 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:

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

// 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.

// 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:

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:

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:

// 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:

// Ü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:

// 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:

// 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:

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

# UserBusiness für betroffenen Monat neu berechnen
php artisan business:store-optimized 12 2025 --clear

Oder nur für User 1218:

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

// 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 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:

// 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):

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

$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:

$count1000 = UserBusiness::where('month', $month)
    ->where('year', $year)
    ->where('sales_volume_points_sum', '>=', 1000)
    ->whereIn('user_id', $teamUserIds)
    ->count();

Klickbare Details (AJAX Modal):

// 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 ?? '-'
    ])
]);

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):

'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):

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:

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

# 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


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:

$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:

$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:

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:

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

  • #1 News Links ERLEDIGT 22.01.2026
  • #3 Vorkasse TXID Hinweis ERLEDIGT 22.01.2026
  • #12 Bezahllink Status ERLEDIGT 28.01.2026

Phase 2: Kritische Bugs (Provisionen betroffen)

  • #9 Gutschriften Punkte Bug
  • #10 Nicht zugeordnete Zahlungen

Phase 3: Infrastruktur (DB-Änderungen)

  • #2 Points DECIMAL (benötigt Migration + Testing) ERLEDIGT 22.01.2026
  • #7 Stornorechnungen mit Punktekorrektur ERLEDIGT 06.02.2026

Phase 4: Features

  • #6 Mehrsprachigkeit PDFs
  • #14 DHL UI + Tracking-Mail ERLEDIGT 23.01.2026
  • #11 Monatsstatistik Erweiterungen

Phase 5: Langfristig

  • #4 Packstation/Postnummer ERLEDIGT 22.01.2026
  • #5 Set-Produkte Bundles ERLEDIGT 22.01.2026
  • #8 Französisch
  • #13 Steuerberater-Modul