Warenwirtschaft: Logistik-Mail regeneriert auch fehlende Rechnungs-PDF

Nachtrag zum Lieferschein-Fix (7d77314): Fehlte die Rechnungs-PDF auf der
Platte (Altdaten), brach MailLogistic::build() still ab — die Mail ging ohne
Anhänge bzw. gar nicht raus, obwohl das UI Erfolg meldete. Jetzt wird die
Rechnung wie der Lieferschein mit bestehender Rechnungsnummer regeneriert.
Hinweise-Doku und Plan-Protokoll fortgeschrieben.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Kevin Adametz 2026-07-16 13:19:12 +00:00
parent 6051aa6ea1
commit a1f94ea427
4 changed files with 33 additions and 2 deletions

View file

@ -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;
}

View file

@ -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 | – |

View file

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

View file

@ -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');