Warenwirtschaft: Review-Fix — Mahnwesen-Storno über OrderStatusService::setShipped
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 <noreply@anthropic.com>
This commit is contained in:
parent
3eb14762e7
commit
ddd6dde593
3 changed files with 59 additions and 4 deletions
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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 | – |
|
||||
|
||||
|
|
|
|||
|
|
@ -1,16 +1,21 @@
|
|||
<?php
|
||||
|
||||
use App\Models\Country;
|
||||
use App\Models\PaymentReminder;
|
||||
use App\Models\Product;
|
||||
use App\Models\ProductStockMovement;
|
||||
use App\Models\Shipping;
|
||||
use App\Models\ShippingCountry;
|
||||
use App\Models\ShoppingOrder;
|
||||
use App\Models\ShoppingOrderItem;
|
||||
use App\Models\ShoppingOrderMargin;
|
||||
use App\Models\ShoppingPayment;
|
||||
use App\Models\ShoppingUser;
|
||||
use App\Services\PaymentReminderService;
|
||||
use App\Services\ProductStockService;
|
||||
use App\User;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\Mail;
|
||||
use Tests\TestCase;
|
||||
|
||||
uses(TestCase::class, RefreshDatabase::class);
|
||||
|
|
@ -281,6 +286,53 @@ test('stichtags-schutz ueberlebt storno und erneutes versenden (status-toggle)',
|
|||
->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');
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue