Warenwirtschaft: Review-Fix — Stichtags-Schutz als unveränderliches Flag (sale_stock_exempt)
Der Schutz historischer Bestellungen hing an shipped_at, das der OrderStatusService beim Status-Toggle nullt und neu stempelt — Storno + erneutes Versenden einer Alt-Bestellung hätte rückwirkend vom heutigen Bestand abgezogen. Migration mit einmaligem Backfill (shipped_at vor 16.07.2026), Guard prüft nur noch das Flag. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
e6af987237
commit
3eb14762e7
6 changed files with 70 additions and 11 deletions
|
|
@ -205,6 +205,7 @@ class ShoppingOrder extends Model
|
|||
'shipped_at' => 'datetime',
|
||||
'user_white_label' => 'boolean',
|
||||
'shipped_status_locked' => 'boolean',
|
||||
'sale_stock_exempt' => 'boolean',
|
||||
];
|
||||
|
||||
public static $shippedTypes = [
|
||||
|
|
|
|||
|
|
@ -6,18 +6,10 @@ use App\Models\Product;
|
|||
use App\Models\Production;
|
||||
use App\Models\ProductStockMovement;
|
||||
use App\Models\ShoppingOrder;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class ProductStockService
|
||||
{
|
||||
/**
|
||||
* AP-13-Stichtag: Bestellungen, die vor Einführung des Verkaufs-Abzugs versendet
|
||||
* wurden, sind historisch — ein erneutes Speichern ihres Versandstatus darf nicht
|
||||
* rückwirkend vom heutigen Bestand abziehen.
|
||||
*/
|
||||
public const SALE_STOCK_SINCE = '2026-07-16';
|
||||
|
||||
/**
|
||||
* Aktueller Bestand je Produkt (Summe Eingänge − Summe Ausgänge).
|
||||
*
|
||||
|
|
@ -190,9 +182,11 @@ class ProductStockService
|
|||
*/
|
||||
public function syncSaleStock(ShoppingOrder $order, ?int $userId = null): void
|
||||
{
|
||||
// Historische Bestellungen (vor dem Stichtag versendet) nicht anfassen:
|
||||
// Historische Bestellungen (vor dem AP-13-Stichtag versendet) nicht anfassen:
|
||||
// vor dem Stichtag wurde nichts gebucht, also gibt es auch nichts abzugleichen.
|
||||
if ($order->shipped_at && $order->shipped_at->lt(Carbon::parse(self::SALE_STOCK_SINCE))) {
|
||||
// Das Flag wird per Migration einmalig gesetzt und ist bewusst unveränderlich —
|
||||
// es überlebt damit auch Status-Toggles, die shipped_at nullen und neu stempeln.
|
||||
if ($order->sale_stock_exempt) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,40 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* AP-13-Stichtag, unveränderlich (Review 20.07.2026): Der Schutz historischer
|
||||
* Bestellungen hing bisher an `shipped_at` — das nullt der OrderStatusService
|
||||
* aber bei jedem Wechsel weg von versendet/abgeschlossen und stempelt beim
|
||||
* Zurückwechseln neu. Ein Status-Toggle einer Alt-Bestellung hätte dadurch
|
||||
* rückwirkend vom heutigen Bestand abgezogen. Das Flag wird einmalig für alle
|
||||
* vor dem Stichtag versendeten Bestellungen gesetzt und danach nie verändert.
|
||||
*/
|
||||
private const SALE_STOCK_SINCE = '2026-07-16';
|
||||
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('shopping_orders', function (Blueprint $table) {
|
||||
$table->boolean('sale_stock_exempt')->default(false)->after('shipped_status_locked');
|
||||
});
|
||||
|
||||
// Backfill: shipped_at vor dem Stichtag ⇒ physisch vor Einführung versendet,
|
||||
// im Anfangsbestand also bereits nicht mehr enthalten — nie abziehen.
|
||||
DB::table('shopping_orders')
|
||||
->whereNotNull('shipped_at')
|
||||
->where('shipped_at', '<', self::SALE_STOCK_SINCE)
|
||||
->update(['sale_stock_exempt' => true]);
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('shopping_orders', function (Blueprint $table) {
|
||||
$table->dropColumn('sale_stock_exempt');
|
||||
});
|
||||
}
|
||||
};
|
||||
|
|
@ -274,6 +274,7 @@ Der Warenkorb kommt also **per Knopfdruck** (Fulfillment-Übergabe), nicht autom
|
|||
| 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** |
|
||||
| 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) |
|
||||
| _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 | – |
|
||||
|
||||
|
|
|
|||
|
|
@ -98,7 +98,10 @@
|
|||
Gilt für **alle Bestellquellen** (WooCommerce-Fulfillment, Whitelabel-Shop,
|
||||
direkt im Partnercenter angelegte Bestellungen). **Alt-Bestellungen**, die
|
||||
vor dem 16.07.2026 versendet wurden, sind geschützt: erneutes Speichern
|
||||
ihres Status zieht nicht rückwirkend vom heutigen Bestand ab.
|
||||
ihres Status zieht nicht rückwirkend vom heutigen Bestand ab. Der Schutz ist
|
||||
seit 20.07.2026 als **unveränderliches Merkmal** an der Bestellung hinterlegt
|
||||
(einmalig beim Update gesetzt) und übersteht damit auch Storno und erneutes
|
||||
Versenden einer Alt-Bestellung.
|
||||
- **API-Bestellungen (neu 16.07.2026):** Unterpunkt von **Bestellungen** in der
|
||||
Warenwirtschaft. Jede über die Partnercenter-API eingehende
|
||||
Bestell-Übertragung (WooCommerce-Fulfillment) wird protokolliert – auch
|
||||
|
|
|
|||
|
|
@ -249,9 +249,11 @@ test('service-abgleich ist auch direkt idempotent (cron-storno)', function () {
|
|||
|
||||
test('historische bestellung (vor stichtag versendet) bucht bei erneutem speichern nichts', function () {
|
||||
$creme = ap13MakeProduct('AP13 Historisch');
|
||||
// sale_stock_exempt entspricht dem Migrations-Backfill (shipped_at vor dem Stichtag)
|
||||
$order = ap13MakeOrder([['product' => $creme, 'qty' => 5]], [
|
||||
'shipped' => 3,
|
||||
'shipped_at' => '2025-12-12 14:05:26',
|
||||
'sale_stock_exempt' => true,
|
||||
]);
|
||||
|
||||
// Admin bestätigt den Status einer Alt-Bestellung erneut — darf nicht rückwirkend abziehen
|
||||
|
|
@ -261,6 +263,24 @@ test('historische bestellung (vor stichtag versendet) bucht bei erneutem speiche
|
|||
->and($order->fresh()->shipped_at?->format('Y-m-d'))->toBe('2025-12-12');
|
||||
});
|
||||
|
||||
test('stichtags-schutz ueberlebt storno und erneutes versenden (status-toggle)', function () {
|
||||
$creme = ap13MakeProduct('AP13 Historisch-Toggle');
|
||||
$order = ap13MakeOrder([['product' => $creme, 'qty' => 5]], [
|
||||
'shipped' => 3,
|
||||
'shipped_at' => '2025-12-12 14:05:26',
|
||||
'sale_stock_exempt' => true,
|
||||
]);
|
||||
|
||||
// Storno nullt shipped_at, erneutes Versenden stempelt es neu (nach Stichtag) —
|
||||
// das unveränderliche Flag muss den rückwirkenden Abzug trotzdem verhindern
|
||||
$admin = ap13MakeAdmin();
|
||||
ap13SetShipped($order, 10, $admin);
|
||||
ap13SetShipped($order, 2, $admin);
|
||||
|
||||
expect(ProductStockMovement::query()->where('product_id', $creme->id)->count())->toBe(0)
|
||||
->and($order->fresh()->sale_stock_exempt)->toBeTrue();
|
||||
});
|
||||
|
||||
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