From ddd6dde593d706b829086100ba6fe5541590fa1e Mon Sep 17 00:00:00 2001 From: Kevin Adametz Date: Mon, 20 Jul 2026 09:47:20 +0000 Subject: [PATCH] =?UTF-8?q?Warenwirtschaft:=20Review-Fix=20=E2=80=94=20Mah?= =?UTF-8?q?nwesen-Storno=20=C3=BCber=20OrderStatusService::setShipped?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Der Auto-Storno der Zahlungserinnerung schrieb shipped=10 direkt und rief nur syncSaleStock() — Margen-Storno-Flag, pending_to/shipped_at-Reset und Promotions-/Guthaben-Folgen fehlten. Die Partnerprovision einer versendeten, nie bezahlten Bestellung wäre zahlbar geblieben. Co-Authored-By: Claude Fable 5 --- app/Services/PaymentReminderService.php | 10 ++-- ...ntwicklungsplan-aktualisiert-06-07-2026.md | 1 + tests/Feature/SaleStockDeductionTest.php | 52 +++++++++++++++++++ 3 files changed, 59 insertions(+), 4 deletions(-) diff --git a/app/Services/PaymentReminderService.php b/app/Services/PaymentReminderService.php index 33c624c..eeccacd 100644 --- a/app/Services/PaymentReminderService.php +++ b/app/Services/PaymentReminderService.php @@ -185,10 +185,12 @@ class PaymentReminderService // action ausführen if ($payment_reminder->action === 'set_order_status_cancelled') { $this->setNoNPayment($payment); - $payment->shopping_order->shipped = 10; - $payment->shopping_order->save(); - // AP-13: war die Bestellung bereits versendet, gebuchte Verkaufs-Abgänge zurückführen - app(ProductStockService::class)->syncSaleStock($payment->shopping_order); + // Review-Fix 20.07.2026: Storno über den zentralen Statuswechsel — der setzt + // neben dem AP-13-Bestandsabgleich auch das Storno-Flag der Marge, nullt + // pending_to/shipped_at und behandelt Promotions-/Guthaben-Folgen. Der + // bisherige Direkt-Write (shipped = 10 + syncSaleStock) ließ die Provision + // einer versendeten, nie bezahlten Bestellung zahlbar stehen. + app(OrderStatusService::class)->setShipped($payment->shopping_order->id, 10); $this->createLog( 'action_completed', "Action abgeschlossen: Bestellung auf 'Storniert' gesetzt, Payment auf 'non' gesetzt", diff --git a/dev/product management /entwicklungsplan-aktualisiert-06-07-2026.md b/dev/product management /entwicklungsplan-aktualisiert-06-07-2026.md index 64604fe..15ce35b 100644 --- a/dev/product management /entwicklungsplan-aktualisiert-06-07-2026.md +++ b/dev/product management /entwicklungsplan-aktualisiert-06-07-2026.md @@ -275,6 +275,7 @@ Der Warenkorb kommt also **per Knopfdruck** (Fulfillment-Übergabe), nicht autom | 17.07.2026 | **AP-32-Brückenlösung (DHL-Label aus WooCommerce übernehmen)** | Bis zur echten DHL-API-Anbindung: Das im Shop erstellte Label (Plugin „DHL for WooCommerce", Meta `_pr_shipment_dhl_label_tracking`) kommt **bei der Bestell-Übertragung mit**. `POST wp/order` akzeptiert `wp_label_url` + `wp_tracking_number` (String oder Array); das System lädt die Label-PDF als **lokale Kopie** in den privaten Storage (`wp-labels/`, Migration `2026_07_17_090000`: `shopping_orders.wp_label_url/wp_label_path`; Sendungsnummer in bestehender Spalte `tracking`) — Begründung: Shop-URL ist öffentlich + erratbar (Kundenadresse!), Kopie überlebt Löschung im Shop; Download-Fehler blockieren die Übertragung nie. **Neuer Endpoint `POST wp/label`** zum Nachreichen (Connector-Hook `pr_save_dhl_label_tracking`); `wp/status` liefert je Bestellung `tracking_number` zurück (Tracking-Link Endkunde). **Anzeige:** Abwicklungs-Board + Bestell-Details mit „Label"-Download (lokale Kopie über copyreader-Dokumentroute `type=label`, Fallback Shop-Link) + Sendungsnummer. Nötiges Connector-Update (WP-Seite) dokumentiert in `dev/product management /ap-32-brueckenloesung-connector-update.md`. Hinweise-Doku + Testleitfaden (§11) fortgeschrieben. Nebenbei: `pint.json` schließt `dev/` (Fremdcode-Referenzen) von der Formatierung aus. | Neuer `WpLabelTransferTest` (10 grün: Übertragung mit Label/Sendungsnr., Download-Fehler + Nicht-PDF blockieren nicht, Mehrpaket-Nummern, Nachreichen, unbekannte Nummer, Status-Poll, Board-Anzeige, Label-Download + 404). Volle Suite: **286 grün, 1052 Assertions** | | 17.07.2026 | **Kundenentscheid: Rohstoffbestand-Default „Nur aus Herstellerrezepturen"** | `RawMaterialStockController@index`: Default-Scope von `all` auf `recipe` gedreht (`?scope=all` zeigt weiterhin alles); Button-Gruppe entsprechend verlinkt, Hinweise-Doku angepasst. | `RawMaterialStockTest` angepasst (Default filtert, `scope=all` zeigt alle). Volle Suite: **286 grün, 1052 Assertions** | | 20.07.2026 | **Review-Fix: Stichtags-Schutz unveränderlich (`sale_stock_exempt`)** | Code-Review vor Live-Release: Der AP-13-Stichtags-Schutz hing an `shipped_at`, das `OrderStatusService::setShipped()` bei jedem Wechsel weg von versendet/abgeschlossen **nullt** und beim Zurückwechseln mit `now()` neu stempelt — Storno + erneutes Versenden einer Alt-Bestellung hätte rückwirkend vom heutigen Bestand abgezogen (Verletzung der dokumentierten Garantie, hinweise.md). Jetzt: Migration `2026_07_20_090000` ergänzt `shopping_orders.sale_stock_exempt` (bool) mit **einmaligem Backfill** (`shipped_at` < 16.07.2026 ⇒ exempt) — das Flag wird danach nie verändert; `syncSaleStock()` prüft nur noch das Flag, die Konstante `SALE_STOCK_SINCE` wandert in die Migration. | `SaleStockDeductionTest` +1: Toggle-Test storniert→versendet bucht nichts (12 grün) | +| 20.07.2026 | **Review-Fix: Mahnwesen-Storno über zentralen Statuswechsel** | Code-Review vor Live-Release: Der Auto-Storno der Zahlungserinnerung (`set_order_status_cancelled`) schrieb `shipped = 10` direkt und rief nur `syncSaleStock()` — **vorbei am in AP-33a extrahierten `OrderStatusService::setShipped()`**. Folge: Bestand kam zurück, aber `margin->cancellation` blieb false, `partner_commission_pending_to`/`shipped_at` blieben gesetzt und die Promotions-/Guthaben-Folgen fehlten — die **Partnerprovision einer versendeten, nie bezahlten Bestellung wäre nach Ablauf der Pending-Frist zahlbar geworden** (Rechnungs-Bestellungen werden vor Zahlungseingang versendet; die Mahn-Abfrage filtert nicht auf Versandstatus). Jetzt ruft der Mahn-Storno `OrderStatusService::setShipped($id, 10)`. | `SaleStockDeductionTest` +1: Ende-zu-Ende über `sendReminder()` — Rückbuchung + Margen-Storno + pending_to/shipped_at genullt (13 grün) | | _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. **Zwischenstand 17.07.2026: Brückenlösung aktiv** (Label aus dem Shop, s. Protokollzeile) — deckt den Versand-Flow ab, bis die DHL-Klärungen da sind | – | | _offen_ | AP-14/15/16/17 | Audit-Trail, Blockrechte, 2FA, WaWi-Einstellungen | – | diff --git a/tests/Feature/SaleStockDeductionTest.php b/tests/Feature/SaleStockDeductionTest.php index ea8eaaa..ccceee6 100644 --- a/tests/Feature/SaleStockDeductionTest.php +++ b/tests/Feature/SaleStockDeductionTest.php @@ -1,16 +1,21 @@ and($order->fresh()->sale_stock_exempt)->toBeTrue(); }); +test('mahnwesen-storno bucht bestand zurueck und storniert die marge', function () { + Mail::fake(); + + $creme = ap13MakeProduct('AP13 Mahn-Storno'); + $order = ap13MakeOrder([['product' => $creme, 'qty' => 2]], ['total_shipping' => 1000]); + $order->shopping_user->forceFill(['billing_email' => 'mahnung@test.example'])->save(); + $margin = ShoppingOrderMargin::query()->forceCreate([ + 'shopping_order_id' => $order->id, + 'user_id' => $order->auth_user_id, + 'net_partner_commission' => 500, + ]); + + // Rechnungs-Bestellung wird vor Zahlungseingang versendet (Abzug + Provision pending) + ap13SetShipped($order, 2); + expect(ap13Net($order, $creme))->toBe(2) + ->and($margin->fresh()->partner_commission_pending_to)->not->toBeNull(); + + $payment = ShoppingPayment::query()->forceCreate([ + 'shopping_order_id' => $order->id, + 'clearingtype' => 'fnc', + 'txaction' => 'open', + 'mode' => 'live', + 'amount' => 1000, + 'currency' => 'EUR', + 'reminder' => 0, + 'reference' => 'AP13-MAHN-1', + ]); + PaymentReminder::query()->forceCreate([ + 'active' => true, + 'clearingtype' => 'fnc', + 'interval' => 7, + 'action' => 'set_order_status_cancelled', + 'subject' => 'Letzte Mahnung {order_number}', + 'message' => 'Bestellung {order_number} wird storniert.', + ]); + + app(PaymentReminderService::class)->sendReminder($payment); + + // Der zentrale Statuswechsel muss greifen: Rückbuchung UND Margen-Storno + $order = $order->fresh(); + expect($order->shipped)->toBe(10) + ->and($order->shipped_at)->toBeNull() + ->and(ap13Net($order, $creme))->toBe(0) + ->and($margin->fresh()->cancellation)->toBeTruthy() + ->and($margin->fresh()->partner_commission_pending_to)->toBeNull(); +}); + test('verkaufs-abgaenge zaehlen im monatsverbrauch', function () { $service = app(ProductStockService::class); $creme = ap13MakeProduct('AP13 Verbrauch');