diff --git a/app/Mail/MailLogistic.php b/app/Mail/MailLogistic.php index 690ee25..ad31ebe 100644 --- a/app/Mail/MailLogistic.php +++ b/app/Mail/MailLogistic.php @@ -52,6 +52,10 @@ class MailLogistic extends Mailable $filename = Invoice::getFilename($this->shopping_order); $path = Invoice::getDownloadPath($this->shopping_order); + if (! Storage::disk('public')->exists($path)) { + // Datei fehlt auf der Platte (Altdaten) — mit bestehender Nummer neu erzeugen + (new InvoiceRepository($this->shopping_order))->update([]); + } if (! Storage::disk('public')->exists($path)) { return; } diff --git a/dev/product management /entwicklungsplan-aktualisiert-06-07-2026.md b/dev/product management /entwicklungsplan-aktualisiert-06-07-2026.md index 2d2688c..1c1c163 100644 --- a/dev/product management /entwicklungsplan-aktualisiert-06-07-2026.md +++ b/dev/product management /entwicklungsplan-aktualisiert-06-07-2026.md @@ -269,7 +269,7 @@ Der Warenkorb kommt also **per Knopfdruck** (Fulfillment-Übergabe), nicht autom | 16.07.2026 | **AP-33a (Bestell-Abwicklung: Nav „Bestellungen" + Fulfillment-Board)** | Neuer Sidenav-Punkt **„Bestellungen"** (copyreader) mit Unterpunkten **Abwicklung** (neu) und **API-Bestellungen** (umgezogen). Board `admin/inventory/orders` (`OrderFulfillmentController`): 4 Filter-Kacheln (heute eingegangen / zu konfektionieren / versandfertig / heute versendet), je Bestellung Packliste aufklappbar (Sets mit Komponenten), Gewicht, Zahlstatus-Pill, Quelle-Badge (API/Shop/Promo), Suche. **Versandfertig-Regel** (Kundenentscheid): `ShoppingOrder::isReadyToShip()` — Rechnung (fnc) sofort, Vorkasse (vor) erst bei `paid`, API (`extern`) immer, `non`/`failed` nie. **Dokumente:** Erstellen (Rechnung + Lieferschein, ohne Mailversand) über bestehendes `InvoiceRepository::create()`; Downloads (einzeln/kombiniert via `PDFMerger`) über eigene copyreader-Routen, da `storage_file` admin ≥ 7 verlangt; **API-Bestellungen bekommen keine Rechnung** — externer Link (`wp_invoice_path`). **Statuswechsel:** Logik aus `SalesController::store()` in neuen **`OrderStatusService::setShipped()`** extrahiert (Transaktion, Lock, Margin-/Promotion-Folgen, AP-13-Abzug) — Verkäufe-Modal und Board nutzen dieselbe Implementierung; Board nur vorwärts (1/2), nur aus Status 0/1. **Sperr-Flag** `shopping_orders.shipped_status_locked` (Migration `2026_07_16_130000`, Kundenentscheid Punkt 3): vom Board gesetzte Status werden vom Auto-Wechsel der Detailseiten (offen → in Bearbeitung, 3 Stellen) nicht mehr überschrieben. Commit `a367d85`. | Neuer `OrderFulfillmentTest` (10 grün: Render/Packliste, Rechte, Versandfertig-Regel, Statuswechsel + Lock + Bestandsabzug, nur-vorwärts, Alt-Status unangetastet, Detailseiten-Sperre, Dokumente ohne Mail, API ohne Rechnung). Volle Suite: **268 grün, 987 Assertions** | | 16.07.2026 | **AP-13-Nachtrag: Stichtags-Schutz für Alt-Bestellungen** | Beim Blick auf Bestellung 1687 (versendet 12/2025) entdeckt: erneutes Speichern des Versandstatus einer **vor AP-13 versendeten** Bestellung hätte rückwirkend vom heutigen Bestand abgezogen. `syncSaleStock()` überspringt jetzt Bestellungen mit `shipped_at` vor dem Stichtag `ProductStockService::SALE_STOCK_SINCE` (16.07.2026). Kundenentscheid dokumentiert: Self-Shipping ohne Abzug bestätigt. | `SaleStockDeductionTest` +1 (11 grün). Suite: **258 grün, 944 Assertions** | | 16.07.2026 | **AP-13 (API-Bestellungen — Protokoll + SKU-Fehlermarkierung, Kundenanforderung 16.07.)** | Neue Tabelle **`api_order_logs`** (Migration `2026_07_16_090000`) + Model `ApiOrderLog`; `ShoppingUserController` protokolliert **jede** `wp/order`-Übertragung — erfolgreiche inkl. Zuordnungsergebnis je Position (SKU, Produkt, Menge, Preis, API-Meldungen), abgelehnte mit Grund (Kunde unbekannt, Bestellung existiert bereits, Speicherfehler). Positionen ohne Produktzuordnung ⇒ `has_errors` + Zusammenfassung („x von y Positionen ohne Produktzuordnung"). **Neuer Menüpunkt „API-Bestellungen"** (copyreader, `admin/inventory/api-orders`): Liste (Limit 500/`?all=1`, Filter „nur Fehler"), rote Markierung, **Modal „Ansehen"** mit Positionstabelle (Meldungen deutsch übersetzt, Roh-Payload bei abgelehnten). Dazu SKU-Konsistenz (Plan §4 „mitzulösen"): Warnkarte **doppelte WP-Nummern** aktiver Produkte, aufklappbare Liste **aktive Produkte ohne WP-Nummer**. Commit `71d5591`. | Neuer `ApiOrderLogTest` (8 grün, Ende-zu-Ende über `wp/order` mit `Passport::actingAs`). Suite: **257 grün, 940 Assertions** | -| 16.07.2026 | **Bugfix Logistik-Mail (Kundenmeldung)** | Fehlversand an die Rechnungsadresse: die Versand-Mail (`MailLogistic`) hängte den **Lieferschein nur bei White-Label-Bestellungen** an. Jetzt wird er **immer** mitgesendet (unabhängig von Logo/White-Label); fehlt die PDF-Datei auf der Platte, wird sie mit bestehender Rechnungsnummer regeneriert. Dazu **tote Duplikat-Klasse `app/Mail/MailLogitic.php` entfernt** (Tippfehler-Dateiname, definierte `MailLogistic` in einer Altversion ohne Lieferschein-Anhang — Kollisionsrisiko im Classmap-Autoload). Commit `7d77314`. | Neuer `LogisticMailTest` (3 grün: Anhang ohne White-Label, Regeneration fehlender Datei, Versand-Button). Volle Suite: **275 grün, 1012 Assertions** | +| 16.07.2026 | **Bugfix Logistik-Mail (Kundenmeldung)** | Fehlversand an die Rechnungsadresse: die Versand-Mail (`MailLogistic`) hängte den **Lieferschein nur bei White-Label-Bestellungen** an. Jetzt wird er **immer** mitgesendet (unabhängig von Logo/White-Label); fehlt die PDF-Datei auf der Platte, wird sie mit bestehender Rechnungsnummer regeneriert. Dazu **tote Duplikat-Klasse `app/Mail/MailLogitic.php` entfernt** (Tippfehler-Dateiname, definierte `MailLogistic` in einer Altversion ohne Lieferschein-Anhang — Kollisionsrisiko im Classmap-Autoload). Commit `7d77314`. **Nachtrag:** gleiche Regeneration auch für die **Rechnungs**-PDF — vorher brach `build()` bei fehlender Rechnungsdatei still ab (Mail ohne Anhänge trotz Erfolgsmeldung im UI). | Neuer `LogisticMailTest` (4 grün: Anhang ohne White-Label, Regeneration Lieferschein + Rechnung, Versand-Button). Volle Suite: **276 grün, 1016 Assertions** | | _offen_ | AP-32 | Versand im System: DHL-Etiketten per API — **neu aufgenommen + Ansatz dokumentiert (§4, 15.07.2026)**; Klärungspunkte (DHL-Konto, Versandprodukte, Druck-Setup) für nächste Anforderungsrunde notiert | – | | _offen_ | AP-14/15/16/17 | Audit-Trail, Blockrechte, 2FA, WaWi-Einstellungen | – | diff --git a/resources/docs/hinweise.md b/resources/docs/hinweise.md index 29b425e..f82c6fc 100644 --- a/resources/docs/hinweise.md +++ b/resources/docs/hinweise.md @@ -189,7 +189,9 @@ Rechnung **und** Lieferschein an — unabhängig davon, ob die Bestellung White-Label-/Logo-Produkte enthält. Vorher fehlte der Lieferschein bei Bestellungen ohne eigenes Etikett, was zu Fehlversänden an die - Rechnungsadresse führen konnte. + Rechnungsadresse führen konnte. Fehlt eine der beiden PDF-Dateien auf dem + Server (Altdaten), wird sie beim Mailversand automatisch mit der + bestehenden Rechnungsnummer neu erzeugt. - **Status-Vorrang der Warenwirtschaft (Kundenentscheid 16.07.2026 — wichtig):** Das Öffnen einer Bestell-Detailseite setzt eine offene Bestellung bislang automatisch auf „in Bearbeitung". **Sobald der Status einmal über die diff --git a/tests/Feature/LogisticMailTest.php b/tests/Feature/LogisticMailTest.php index b83df24..0ffd238 100644 --- a/tests/Feature/LogisticMailTest.php +++ b/tests/Feature/LogisticMailTest.php @@ -143,6 +143,31 @@ test('fehlende lieferschein-datei wird fuer die mail regeneriert', function () { expect($names->first(fn ($n) => str_starts_with((string) $n, 'Lieferschein-')))->not->toBeNull(); }); +test('fehlende rechnungs-datei wird fuer die mail regeneriert', function () { + Storage::fake('public'); + $order = lmMakeOrder(); + lmCreateDocuments($order); + $order->refresh(); + + $invoicePath = Invoice::getDownloadPath($order); + Storage::disk('public')->delete($invoicePath); + expect(Storage::disk('public')->exists($invoicePath))->toBeFalse(); + + $cwd = getcwd(); + chdir(public_path()); + try { + $mailable = new MailLogistic($order); + $mailable->build(); + } finally { + chdir($cwd); + } + + expect(Storage::disk('public')->exists($invoicePath))->toBeTrue(); + $names = collect($mailable->attachments)->pluck('options.as'); + expect($names)->toHaveCount(2) + ->and($names->first(fn ($n) => str_starts_with((string) $n, 'Rechnung-')))->not->toBeNull(); +}); + test('versand-button verschickt die logistik-mail', function () { Mail::fake(); Storage::fake('public');