Abo Einmalprodukte und Bestätigung abschließen
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
2bdc9ada3c
commit
2269ce031f
57 changed files with 3647 additions and 371 deletions
387
dev/2026-06-05/ENTWICKLUNGSPLAN-ABO-EINMALPRODUKTE.md
Normal file
387
dev/2026-06-05/ENTWICKLUNGSPLAN-ABO-EINMALPRODUKTE.md
Normal file
|
|
@ -0,0 +1,387 @@
|
|||
# Entwicklungsplan: Einmalige Produkte zum Abo-Versand hinzufügen
|
||||
|
||||
> **Status:** In Planung · **Erstellt:** 2026-06-05 · **Briefing:** `docs/salescenter/ABO - Bestellungen.md`
|
||||
|
||||
## Dokumentations-Hinweis (verbindlich)
|
||||
|
||||
Dieser Plan wird **Schritt für Schritt** abgearbeitet. Für **jede Phase** gilt:
|
||||
|
||||
- Der **Umsetzungsstatus** in diesem Dokument wird nach Abschluss eines Schritts auf `ERLEDIGT` gesetzt (mit Datum, betroffenen Dateien, Zeilenangaben und kurzer Begründung – analog zu `dev/2026-02-19/plan-wizard-starterpaket.md`).
|
||||
- **Alle Phasen, Hinweise und Optimierungen** (Abschnitt „Zu beachtende Punkte") werden sauber dokumentiert – auch Abweichungen vom Plan, getroffene Detail-Entscheidungen und Verworfenes (mit Begründung).
|
||||
- Jede Änderung wird **programmatisch getestet** (Pest), bevor die Phase als erledigt gilt.
|
||||
- Nach Code-Änderungen läuft `./vendor/bin/pint`.
|
||||
|
||||
---
|
||||
|
||||
## 1. Ziel & Briefing-Zusammenfassung
|
||||
|
||||
Berater und Endkunden sollen **kurz vor der nächsten Abo-Ausführung** (Zeitfenster X Tage, konfigurierbar) **einmalig** Produkte aus dem **normalen Bestellsortiment** (nicht dem Abo-Sortiment) zu ihrer anstehenden Abo-Lieferung hinzufügen können.
|
||||
|
||||
**Nutzen:** Versandkosten und einen zweiten Versandweg sparen (Nachhaltigkeit).
|
||||
|
||||
Kernpunkte:
|
||||
|
||||
- **Einmalig**, nicht dauerhaft – nach erfolgreicher Abo-Ausführung werden die Einmal-Artikel wieder entfernt; nur die reinen Abo-Produkte bleiben.
|
||||
- **Getrennte Sortimente:** Abo (`show_on` 12/13) vs. Bestellsortiment (`show_on` 2 = Berater, `show_on` 3 = Endkunde).
|
||||
- **Zwei Anlaufstellen:** Sales Center `my.mivita.care` (Berater) und Portal `in.mivita.care` (Endkunde).
|
||||
- **E-Mail-Vorlauf** mit zielgruppenspezifischem Link + Hinweis auf Deckung der Zahlungsweise und Nachhaltigkeit.
|
||||
- **UI:** Card über den Abo-Produkten → Modal mit Bestellsortiment (inkl. Steuer + Endpreis), `+`-Buttons, zweiter „Warenkorb" mit getrennten Zwischensummen (oben Einmal-Artikel, unten Abo), separate Gesamtsummen-Card (kombinierter Versand nach Gesamtgewicht), sticky Warenkorb-Seitenleiste.
|
||||
- **Rechnung & Lieferschein:** Eine Gesamtrechnung, Einmal-Artikel klar gekennzeichnet und unterhalb der Abo-Produkte abgegrenzt.
|
||||
|
||||
---
|
||||
|
||||
## 2. Geklärte Rahmenbedingungen (Stand 2026-06-05)
|
||||
|
||||
| # | Entscheidung |
|
||||
| -- | ------------ |
|
||||
| 1 | Zeitfenster-Setting Default = **4 Tage**; Fenster gilt von `next_date − X` bis Ausführung. |
|
||||
| 2 | **Bei Zahlungsfehler bleiben** Einmal-Artikel erhalten. User **und** Admin können Retry selbst anstoßen. User kann Artikel **jederzeit im Fenster** hinzufügen/entfernen. |
|
||||
| 3 | Einmal-Artikel **bleiben bestehen** bis zur nächsten **erfolgreichen** Ausführung oder bis sie der User manuell entfernt – auch bei Pause/Verschiebung. |
|
||||
| 4 | **Preis-Snapshot** beim Hinzufügen (Transparenz: hinzugefügter Preis gilt, nicht Tagespreis). |
|
||||
| 5 | Deckungsprüfung = **nur Text-Hinweis** (keine API-Abfrage möglich; Fehler kommt erst bei Abbuchung). |
|
||||
| 6 | Sortiment: **`show_on=2`** (Berater) / **`show_on=3`** (Endkunde), **keine** Min-/Höchstmenge (wie normale Bestellung). |
|
||||
| 7 | Komplette Versandlogik gilt: Gewicht ↑ → Versand ↑, ggf. **neues Kompensationsprodukt** nötig; im End-Warenkorb berücksichtigt und bei nächster Ausführung **zurückgesetzt**. |
|
||||
| 8 | Endkunden-Route passt; jeder Kunde hat **nur ein Abo** (kleine IDs). |
|
||||
|
||||
---
|
||||
|
||||
## 3. Ist-Zustand (Analyse-Ergebnis)
|
||||
|
||||
| Bereich | Status heute | Schlüsseldateien |
|
||||
| ------- | ------------ | ---------------- |
|
||||
| Abo-Datenmodell | `UserAbo` + `UserAboItem` (dauerhafte Positionen, `show_on` 12/13) + `UserAboOrder` (Ausführungs-Historie) | `app/Models/UserAbo.php`, `app/Models/UserAboItem.php`, `app/Models/UserAboOrder.php` |
|
||||
| Ausführung | Cron `user:make_abo_order` (täglich 04:00) → `UserMakeOrder` baut `ShoppingOrder` (`is_abo=1`) aus `user_abo_items`, zahlt via Payone | `app/Console/Commands/UserMakeAboOrder.php`, `app/Cron/UserMakeOrder.php` |
|
||||
| Cart | Kein eigenes Cart-System – `AboOrderCart` befüllt `Yard::instance('shopping')` aus Items, berechnet Versand nach Gewicht, schreibt `user_abos.amount` | `app/Services/AboOrderCart.php`, `app/Services/Yard.php` |
|
||||
| Sortiment-Trennung | Über `ProductOrderContext::allowedShowOnIds()` (Berater `2`/Abo `12,13`; Kunde `3`/Abo `12,13`) | `app/Services/ProductOrderContext.php`, `app/Http/Controllers/User/OrderController.php:598-639` |
|
||||
| „Produkt hinzufügen" (bestehend) | Erhöht **dauerhaft** das Abo-Sortiment (Action `addProduct`) – **nicht** das hier benötigte Verhalten | `app/Http/Controllers/User/AboController.php`, `Portal/AboController.php` |
|
||||
| Retry | Vorhanden im Admin per Knopfdruck (`AboRetryPaymentService::retry()`), nutzt `UserMakeOrder::makeShoppingOrder()` | `app/Services/AboRetryPaymentService.php`, `app/Http/Controllers/Admin/AboController.php:106` |
|
||||
| Rechnung/Lieferschein | Flache Iteration über `shopping_order_items` – **keine** Abo-/Einmal-Trennung | `resources/views/pdf/invoice-detail.blade.php`, `resources/views/pdf/delivery-detail.blade.php` |
|
||||
| Settings | Slug-basiertes `Setting`-Model; „Abo Einstellungen"-Card existiert (`abo-min-duration`) | `app/Models/Setting.php`, `resources/views/admin/settings/index.blade.php:63-75` |
|
||||
| E-Mail-Vorlauf | **Existiert nicht** | – |
|
||||
| Domains | `Util::getMyMivitaUrl()` (Berater) / `Util::getMyMivitaPortalUrl()` (Endkunde) | `app/Services/Util.php` |
|
||||
|
||||
---
|
||||
|
||||
## 4. Architektur-Entscheidungen
|
||||
|
||||
### 4.1 Eigene Tabelle `user_abo_one_time_items` (mit Preis-Snapshot)
|
||||
|
||||
Bewusst **getrennt** von `user_abo_items`, damit Einmal-Artikel nie in die dauerhafte Abo-Logik (Provision, Mindestlaufzeit, History, num_comp der Abo-Items) geraten und sauber gelöscht werden können.
|
||||
|
||||
**Spalten:** `id`, `user_abo_id` (FK, cascade), `product_id` (FK), `comp` (für ggf. auto. Kompensationsprodukte), `qty`, `price`, `price_net`, `tax_rate`, `tax`, `price_vk_net`, `discount`, `points`, `status`, `created_at`, `updated_at`, `deleted_at` (SoftDeletes).
|
||||
|
||||
Preis-Felder werden **beim Hinzufügen** als Snapshot befüllt (Entscheidung #4).
|
||||
|
||||
### 4.2 Kennzeichnung in `shopping_order_items`
|
||||
|
||||
Neue Spalte **`is_abo_addon`** (boolean, default `false`). Es bleibt **eine** Bestellung/Rechnung; die Trennung in Rechnung/Lieferschein erfolgt über dieses Flag (Abo-Produkte oben, Einmal-Artikel abgegrenzt darunter).
|
||||
|
||||
### 4.3 Kombinierter Versand, getrennte Zwischensummen
|
||||
|
||||
Einmal-Artikel + Abo-Items landen im **selben `Yard`-Cart** → Versand wird automatisch über das **Gesamtgewicht** berechnet (das ist der Nutzen des Features). Einmal-Artikel tragen im Cart die Option **`abo_addon => true`** → ermöglicht getrennte Zwischensummen-Berechnung in der Live-Ansicht ohne DB-Roundtrip.
|
||||
|
||||
### 4.4 `user_abos.amount` entkoppeln
|
||||
|
||||
`AboOrderCart::makeOrderYard()` überschreibt aktuell `user_abos.amount` bei **jedem** Aufruf (auch beim bloßen Ansehen). `amount` muss weiterhin nur den **reinen Abo-Betrag** enthalten. Der **kombinierte** Betrag (inkl. Einmal-Artikel) wird nur für die Abbuchung im Cron/Retry berechnet, nicht persistiert.
|
||||
|
||||
### 4.5 Zentrale Purge-Logik
|
||||
|
||||
Eine Methode „Einmal-Artikel des Abos löschen", aufgerufen **nur bei erfolgreicher** Ausführung – sowohl im Cron-Erfolgszweig (`UserMakeOrder`/`UserMakeAboOrder` → `updateAbo`) als auch in `AboRetryPaymentService::markAboSuccess()`. Bei Fehler: **behalten**.
|
||||
|
||||
### 4.6 Zeitfenster-Setting
|
||||
|
||||
Neuer Slug **`abo-onetime-window-days`** (Typ `int`, Default `4`) in der „Abo Einstellungen"-Card. Steuert Sichtbarkeit der Card/des Modals **und** den Versandzeitpunkt der Reminder-Mail. **Unabhängig** von den hartkodierten Lock-Fristen in `AboRepository` (siehe Punkt 9.2).
|
||||
|
||||
---
|
||||
|
||||
## 5. Betroffene Dateien (Übersicht)
|
||||
|
||||
| Datei | Änderung |
|
||||
| ----- | -------- |
|
||||
| `database/migrations/...create_user_abo_one_time_items_table.php` | **NEU** – Tabelle mit Snapshot-Feldern |
|
||||
| `database/migrations/...add_is_abo_addon_to_shopping_order_items_table.php` | **NEU** – Flag-Spalte |
|
||||
| `app/Models/UserAboOneTimeItem.php` | **NEU** – Model + Relationen |
|
||||
| `database/factories/UserAboOneTimeItemFactory.php` | **NEU** – Factory |
|
||||
| `app/Models/UserAbo.php` | Relation `one_time_items()` |
|
||||
| `app/Models/ShoppingOrderItem.php` | `is_abo_addon` fillable/cast, Scopes |
|
||||
| `app/Services/AboHelper.php` | `isOneTimeWindowOpen()`, Purge-Helper |
|
||||
| `app/Services/AboOrderCart.php` (oder neuer `AboOneTimeCart`) | Einmal-Artikel in Yard laden, getrennte Zwischensummen, num_comp inkl. Gesamtgewicht |
|
||||
| `app/Http/Controllers/User/AboController.php` | Actions `addOneTime` / `updateOneTime` / `removeOneTime` / User-Retry |
|
||||
| `app/Http/Controllers/Portal/AboController.php` | dieselben Actions für Endkunde |
|
||||
| `app/Http/Controllers/ModalController.php` | Modal-Branch für Einmal-Produkt-Auswahl |
|
||||
| `app/Http/Requests/...` | **NEU** – Form Requests für die Actions |
|
||||
| `app/Cron/UserMakeOrder.php` | Einmal-Artikel mit in Bestellung, `is_abo_addon=true`, kombinierter Betrag |
|
||||
| `app/Console/Commands/UserMakeAboOrder.php` | Purge nach Erfolg (zentral) |
|
||||
| `app/Services/AboRetryPaymentService.php` | Purge im `markAboSuccess()` |
|
||||
| `app/Console/Commands/AboSendExecutionReminder.php` | **NEU** – Reminder-Command |
|
||||
| `app/Console/Kernel.php` | Schedule-Eintrag Reminder |
|
||||
| `app/Mail/MailAboExecutionReminder.php` | **NEU** – Mailable |
|
||||
| `resources/views/emails/abo_execution_reminder.blade.php` | **NEU** – Mail-Template |
|
||||
| `resources/views/user/abo/detail.blade.php` | Card + zweite Liste + Summen + sticky Sidebar |
|
||||
| `resources/views/portal/abo/my_abo.blade.php` | dito für Endkunde |
|
||||
| `resources/views/admin/abo/_order_onetime*.blade.php` | **NEU** – Einmal-Artikel-Liste/Summen |
|
||||
| `resources/views/user/abo/modal_abo_onetime_products.blade.php` | **NEU** – Modal Bestellsortiment |
|
||||
| `public/js/iq-modal-cart.js` | JS für neue Actions |
|
||||
| `resources/views/pdf/invoice-detail.blade.php` | Gruppierung Abo / Einmal |
|
||||
| `resources/views/pdf/delivery-detail.blade.php` | Gruppierung Abo / Einmal |
|
||||
| `resources/views/admin/settings/index.blade.php` | Setting `abo-onetime-window-days` |
|
||||
| `resources/lang/{de,en,es,fr}/abo.php`, `email.php` | Übersetzungen |
|
||||
| `tests/Feature/...`, `tests/Unit/...` | **NEU** – Tests |
|
||||
|
||||
---
|
||||
|
||||
## 6. Phasenplan & Umsetzungsstatus
|
||||
|
||||
### Phase 1 – Datenmodell & Settings · `ERLEDIGT (2026-06-05)`
|
||||
|
||||
- [x] 1.1 Migration `user_abo_one_time_items` (inkl. Snapshot-Felder, FKs, SoftDeletes).
|
||||
- [x] 1.2 Model `UserAboOneTimeItem` (fillable, casts, Relationen `product()`, `user_abo()`) + Factory.
|
||||
- [x] 1.3 Migration `is_abo_addon` (boolean, default false) auf `shopping_order_items`.
|
||||
- [x] 1.4 `ShoppingOrderItem`: `is_abo_addon` in fillable/casts, Scopes `aboItems()` / `addonItems()`.
|
||||
- [x] 1.5 Relation `one_time_items()` (hasMany) auf `UserAbo`.
|
||||
- [x] 1.6 Setting `abo-onetime-window-days` (int, Default 4) in `admin/settings/index.blade.php` (Abo-Card).
|
||||
- [x] 1.7 `AboHelper::isOneTimeWindowOpen(UserAbo $abo): bool` (vergleicht `next_date` mit `now()` + Setting).
|
||||
- [x] 1.8 Tests: Window-Logik, Setting-Default/Custom, Model-Cast/Relation, Scopes.
|
||||
|
||||
**Doku (Stand 2026-06-05):**
|
||||
|
||||
- **1.1** `database/migrations/2026_06_05_120000_create_user_abo_one_time_items_table.php` – Tabelle mit Snapshot-Spalten (`price`, `price_net`, `tax_rate`, `tax`, `price_vk_net`, `discount`, `points`), `comp`, `qty`, `status`, `timestamps`, `softDeletes`. FK `user_abo_id` → `user_abos` (cascade), `product_id` → `products`. Spalten-Typen analog `shopping_order_items`.
|
||||
- **1.2** `app/Models/UserAboOneTimeItem.php` (HasFactory + SoftDeletes, Casts/Fillable, Relationen `product()`/`user_abo()`, `getFormattedPrice()`/`getFormattedTotalPrice()`). Factory `database/factories/UserAboOneTimeItemFactory.php`.
|
||||
- **1.3** `database/migrations/2026_06_05_120100_add_is_abo_addon_to_shopping_order_items_table.php` – `boolean is_abo_addon default(false) after('comp')`.
|
||||
- **1.4** `app/Models/ShoppingOrderItem.php` – `is_abo_addon` in fillable + Cast `bool`; Scopes `scopeAboItems()` (`is_abo_addon = false`) und `scopeAddonItems()` (`is_abo_addon = true`).
|
||||
- **1.5** `app/Models/UserAbo.php` – `one_time_items()` (hasMany) + Docblock `@property-read`.
|
||||
- **1.6** `resources/views/admin/settings/index.blade.php` – Feld „Zeitfenster für einmalige Produkte vor Ausführung (Tage)" (Slug `abo-onetime-window-days`, Typ `int`) in der Abo-Card, inkl. Hilfetext (Standard 4). Speicherung über bestehenden `SettingController@store`-Mechanismus.
|
||||
- **1.7** `app/Services/AboHelper.php` – Konstante `DEFAULT_ONETIME_WINDOW_DAYS = 4`, `getOneTimeWindowDays()` (liest Setting, Fallback Default bei 0/leer), `isOneTimeWindowOpen(UserAbo)` (rein zeitliche Prüfung: `next_date` vorhanden, nicht in der Vergangenheit, `diffInDays <= Fenster`). Abo-Zustand (active/status) prüfen die Aufrufer.
|
||||
- **1.8** `tests/Feature/AboOneTimeWindowTest.php` – 13 Tests, alle grün (Window-Grenzfälle Tag X / X+1 / Vergangenheit / kein `next_date`, Setting-Default/Custom, Model-Cast + Relation-Typ, Scope-SQL + bool-Cast).
|
||||
- **Pint:** ausgeführt (`--dirty`), `ShoppingOrderItem.php` formatiert.
|
||||
|
||||
**WICHTIG – Nebenbefund / Fix (siehe auch Abschnitt 9.7):** Die RefreshDatabase-Testsuite war auf SQLite **generell** kaputt: Migration `2026_05_12_113939_add_french_locale_to_trans_languages.php` setzt die Tabelle `trans_languages` voraus, die per Migration nie erzeugt wird (existiert nur in der echten MySQL-DB). Auch bestehende Tests (`AboRepositoryDateChangeTest`) schlugen dadurch fehl. Fix: `up()`/`down()` mit `Schema::hasTable('trans_languages')`-Guard versehen (produktionssicher, da Tabelle dort existiert; in Tests wird übersprungen). Dadurch läuft die gesamte RefreshDatabase-Suite wieder.
|
||||
|
||||
---
|
||||
|
||||
### Phase 2 – Backend-Logik (Berater + Endkunde) · `ERLEDIGT (2026-06-05)`
|
||||
|
||||
- [x] 2.1 Cart-Befüllung erweitern: Einmal-Artikel mit **Snapshot-Preis** + Option `abo_addon=true` zusätzlich in den Yard.
|
||||
- [x] 2.2 Getrennte Zwischensummen-Methode (Einmal vs. Abo).
|
||||
- [x] 2.3 Kompensationsprodukt-Logik auf **Gesamtgewicht inkl. Einmal-Artikel** (über Yard-Aufrufreihenfolge).
|
||||
- [x] 2.4 AJAX-Action `oneTime` (add/update/remove) in `User\AboController` **und** `Portal\AboController` – Zeitfenster-Guard, Sortiment-Filter, Snapshot, Berechtigungs-Check.
|
||||
- [x] 2.5 Form Request `AboOneTimeItemRequest` + Übersetzungen (DE/EN/ES/FR).
|
||||
- [x] 2.6 Tests (Service add/update/remove, Sortiment-Guard, Snapshot, getrennte Summen, FormRequest-Regeln).
|
||||
|
||||
**Doku (Stand 2026-06-05):**
|
||||
|
||||
- **2.1** `app/Services/AboOrderCart.php` → `addOneTimeItemsToYard(UserAbo)`: lädt `one_time_items` mit eingefrorenem Snapshot-Preis und Cart-Option `abo_addon=true` (+ `one_time_item_id`, `weight` live, `points`/`tax_rate` aus Snapshot) in `Yard::instance('shopping')`, danach `reCalculateShippingPrice()`. **Verändert `amount` nicht** (wird nach `makeOrderYard()` aufgerufen → 9.1 erfüllt).
|
||||
- **2.1/Snapshot** `AboOrderCart::buildOneTimeSnapshot(Product, UserAbo)`: berechnet `price` (Berater: `getPriceWith(tax_free,true,…)`, Kunde: `round(getPriceWith(tax_free,false,…),1)`), `price_net`, `tax_rate`, `tax`, `price_vk_net`, `discount` (Berater-Marge), `points` – konsistent zu `addProductToCart()`.
|
||||
- **2.2** `AboOrderCart::getSplitSummary()`: iteriert Yard-Content, trennt per `options->abo_addon` in `abo`/`one_time` (net/gross/tax) und liefert kombinierten Versand + `total_with_shipping` + `has_one_time`.
|
||||
- **2.3** Reihenfolge in der Action: `initYard → makeOrderYard → addOneTimeItemsToYard → checkNumOfCompProducts`. Da die Einmal-Artikel **vor** `checkNumOfCompProducts` im Yard liegen, berücksichtigt `getNumComp()` automatisch das Gesamtgewicht. Beim Ausführen (Phase 4) wird ohne Einmal-Artikel neu gerechnet → Reset.
|
||||
- **2.4** `User\AboController@oneTime()` + `Portal\AboController@oneTime()`: prüfen `isOneTimeWindowOpen()` (403 wenn zu), delegieren an gemeinsamen Service, bauen Yard neu auf und liefern JSON `{response, message, summary, one_time_items}`. Berechtigung via `checkPermissions()`/`checkPortalPermission()`.
|
||||
- **Service** `app/Services/AboOneTimeService.php`: gemeinsame `handleAction()` (add/update/remove) für beide Controller (DRY). Sortiment-Guard via `ProductOrderContext::allowedShowOnIds(false, me|ot-customer)` → `['2']`/`['3']`. Mengen-Clamp 1–100. Fremd-Item-Schutz über `user_abo_id`-Filter.
|
||||
- **Routen:** `user_abos_onetime` in `routes/domains/crm.php` (`POST /user/abos/onetime/{view}/{id}`) und `routes/domains/portal.php` (`POST portal/my-subscriptions/onetime/{view}/{id}`).
|
||||
- **2.5** `app/Http/Requests/Abo/AboOneTimeItemRequest.php` (array-Regeln, `required_if`-Logik, lokalisierte Messages). Neue Keys in `resources/lang/{de,en,es,fr}/abo.php`: `onetime_window_closed`, `onetime_product_not_allowed`, `onetime_action_required`, `onetime_action_invalid`, `onetime_qty_required`.
|
||||
- **2.6** `tests/Feature/AboOneTimeServiceTest.php` – 10 Tests grün (gesamt mit Phase 1: **23 grün**).
|
||||
- **Pint:** ausgeführt; ungenutzte Imports entfernt.
|
||||
|
||||
**Abweichungen / Entscheidungen (dokumentiert):**
|
||||
|
||||
1. **Gemeinsamer Service statt inline-Duplikat:** Add/Update/Remove liegen in `AboOneTimeService`, nicht doppelt in beiden Controllern (die bestehende `update()`-Multiplex-Logik wurde bewusst nicht angefasst).
|
||||
2. **Dedizierter Endpoint statt Multiplex-`update()`:** Damit der `FormRequest` sauber greift (Auto-Validation), bekommen die Einmal-Aktionen eine eigene Route/Methode `oneTime`, statt sie in die action-gemultiplexte `update()` zu zwängen (dort sind keine FormRequests im Einsatz).
|
||||
3. **Datatable + Modal für die Produktauswahl** (normales Sortiment `show_on` 2/3) sowie die JS-Anbindung sind **UI** und gehören zu **Phase 3** – hier bewusst noch nicht umgesetzt.
|
||||
4. **Teilabdeckung Tests:** Die voll integrierte kombinierte Versand-/`num_comp`-Berechnung (mit echtem Yard-Init für `me`/`ot`) wird in **Phase 4** (Ausführung) getestet; Phase 2 deckt Split-Mathematik (`getSplitSummary`) und Snapshot isoliert ab.
|
||||
|
||||
---
|
||||
|
||||
### Phase 3 – Frontend / Views · `ERLEDIGT (2026-06-05)`
|
||||
|
||||
- [x] 3.1 Neue Card oberhalb der Abo-Produkte in `user/abo/detail.blade.php` + `portal/abo/my_abo.blade.php` (nur wenn Fenster offen) inkl. Nachhaltigkeits-/Versand-Spar- und Deckungs-Hinweis (Text).
|
||||
- [x] 3.2 Modal `modal_abo_onetime_products.blade.php` (Yajra-Datatable Sortiment 2/3, Spalten Steuer + berechneter Endpreis, `+`-Button pro Zeile).
|
||||
- [x] 3.3 Übersicht: zweite Produktliste (Einmal) mit eigener Zwischensumme **oben**, Abo-Liste mit Zwischensumme **unten** (`split_mode`).
|
||||
- [x] 3.4 Separate Gesamtsummen-Card unter beiden Listen (Zwischensumme Einmal + Zwischensumme Abo + Versand nach Gesamtgewicht + Steuer + Gesamtsumme).
|
||||
- [x] 3.5 Sticky Warenkorb-Seitenleiste (Bootstrap 4 `sticky-top`) mit Detail-Zusammenfassung.
|
||||
- [x] 3.6 Eigenes JS-Modul `iq-abo-onetime.js` für die neuen Actions (Mengen +/-, Entfernen, Modal laden) – getrennt von `iq-modal-cart.js` (eigener Endpoint, eigene Holder).
|
||||
- [ ] 3.7 Visuelle Prüfung (Berater + Endkunde) – durch den User vorzunehmen; **kein** Asset-Build nötig (JS liegt in `public/js`, via `asset()` eingebunden).
|
||||
|
||||
**Doku (2026-06-05):**
|
||||
|
||||
- **Neue View-Partials** (`resources/views/admin/abo/`):
|
||||
- `_order_onetime.blade.php` – Card mit Überschrift, Info-/Deckungs-/Nachhaltigkeits-Hinweis, „Einmaliges Produkt hinzufügen"-Button (Modal-Trigger `data-action="abo-add-onetime"`), Holder `#insert_show_onetime_order`.
|
||||
- `_order_onetime_show.blade.php` – Einmal-Artikel-Liste (Bild, Name + „Einmalig"-Badge, Mengen-Stepper, Entfernen, Positionspreis aus Snapshot) + Zwischensumme Einmal aus `$summary`.
|
||||
- `_order_combined_summary.blade.php` – Gesamtsummen-Card: Zwischensumme Einmal + Zwischensumme Abo + Lieferland + Versand + (Netto/Steuer) + Gesamtsumme (alle aus `Yard::instance('shopping')`-Kombiwerten + `$summary`).
|
||||
- **Modal:** `resources/views/user/abo/modal_abo_onetime_products.blade.php` – DataTable über `user_abo_onetime_datatable`, `+`-Button-Spalte (`.add-onetime-product-basket`).
|
||||
- **`_order_abo_show.blade.php`:** Neuer optionaler Schalter `$split_mode` → zeigt im Split-Modus **nur** die Abo-Zwischensumme (aus `$summary['abo']['gross']`), Versand/Steuer/Gesamt entfallen (wandern in die Gesamtsummen-Card). Default `false` ⇒ bestehende Aufrufer unverändert. `_order_abo.blade.php` reicht `$split_mode`/`$summary` durch (mit Defaults).
|
||||
- **Nachtrag 2026-06-05:** Im Split-Modus zeigt `_order_abo_show.blade.php` zusätzlich zur Abo-Zwischensumme jetzt auch den **Gesamtbetrag der nächsten Lieferung** (`$summary['total_with_shipping']`) analog zur Einmalprodukt-Liste. Versand-/Steuerdetailzeilen bleiben weiterhin in der seitlichen Gesamtsummen-Card, damit die Abo-Liste nicht wieder den vollständigen Warenkorb dupliziert.
|
||||
- **Layout** `user/abo/detail.blade.php` + `portal/abo/my_abo.blade.php`: Bei offenem Fenster zweispaltiges `row` → links (`col-lg-8`) Einmal-Card + Abo-Card (split) + Comp-Produkte, rechts (`col-lg-4`) sticky Gesamtsummen-Card (`sticky-top`, `top:1rem`). Bei geschlossenem Fenster unveränderter Single-Column-Aufbau.
|
||||
- **JS** `public/js/iq-abo-onetime.js` (neu, eingebunden in beide Detail-Views, init `IqAboOneTime.init()`): postet an `data-onetime-route` (`user_abos_onetime`) mit `action add|update|remove`; rendert `#insert_show_onetime_order`, `#insert_show_products_order` (Abo-Split) und `#combined_summary_card` aus den HTML-Feldern der JSON-Antwort neu; Fehler-Handling über `#insert_onetime_error_message`.
|
||||
- **Controller (User + Portal):**
|
||||
- `oneTimeDatatable($id)` neu – Sortiment `show_on=2` (Berater/`me`) bzw. `show_on=3` (Endkunde/`ot`/Portal), Spalten inkl. Netto-/Brutto-Endpreis.
|
||||
- `oneTime()` liefert zusätzlich `html_onetime`, `html_abo` (split), `html_summary` für das Live-Re-Rendering.
|
||||
- `detail()`/`myAbo()` legen bei offenem Fenster die Einmal-Artikel mit in den Yard (`addOneTimeItemsToYard`) und übergeben `one_time_window_open` + `summary`.
|
||||
- `ModalController@load` (CRM) und `Portal\AboController@modalLoad` (Portal): Branch `abo-add-onetime` → Onetime-Modal.
|
||||
- **Routen:** `user_abo_onetime_datatable` (CRM `routes/domains/crm.php`, Portal `routes/domains/portal.php`).
|
||||
- **Übersetzungen** (`de/en/es/fr/abo.php`): `onetime_card_hl`, `onetime_card_info`, `onetime_important_hl`, `onetime_coverage_hint`, `onetime_reset_hint`, `add_onetime_product`, `onetime_subtotal`, `onetime_next_delivery_total`, `abo_subtotal`, `abo_next_delivery_total`, `onetime_empty`, `onetime_badge`, `combined_summary_hl`.
|
||||
- **Tests:** `tests/Feature/AboOneTimeViewTest.php` (4 Render-Tests: Einmal-Liste mit Position/Zwischensumme, Leer-Hinweis, kombinierte Gesamtsumme, Abo-Liste im Split-Modus). Gemeinsame Test-Helper (`makeShopEnv/makeMeAbo/makeProduct`) nach `tests/Pest.php` verschoben (guarded), Duplikate aus `AboOneTimeServiceTest.php` entfernt. **27 Tests grün** (Phase 1–3). Pint: pass.
|
||||
- **Sticky-Sidebar (Nachtrag 2026-06-05):** Bootstrap-`sticky-top` wurde durch eine **JS-gesteuerte Lösung** (`IqAboOneTime.initSticky/updateSticky` in `iq-abo-onetime.js`) ersetzt, da `position: sticky` in diesem Appwork-Layout durch Vorfahren-`overflow` unzuverlässig ist. Die Summen-Card (`#combined_summary_card` in `#combined_summary_col`) wird per `position: fixed` am oberen Rand angeheftet (Abstand 16 px) und **maximal bis zum Ende der Spalte** mitgeführt (Clamp `colRect.bottom − cardHeight`). Scroll-Listener im Capture-Modus (erfasst beliebige Scroll-Container), `requestAnimationFrame`-Throttle, Reset unter `lg` (einspaltig) und Re-Berechnung nach jedem Re-Render. Views nutzen statt `sticky-top` die Klasse `js-abo-sticky`.
|
||||
- **Bestätigung Einmalprodukte (Nachtrag 2026-06-05):** `UserAboOneTimeItem` wurde um `confirmed_qty`/`confirmed_at` erweitert. `AboOneTimeService` unterstützt `confirm`/`discard`: Bestätigen friert den aktuellen Stand kostenpflichtig ein (`status=1`), Verwerfen stellt den zuletzt bestätigten Stand wieder her oder entfernt unbestätigte neue Artikel endgültig. Die Einmalprodukt-Liste zeigt unten eine rechtliche Bestätigungsfläche mit Zwischensumme, Gesamtbetrag der nächsten Lieferung, AGB-/Widerrufs-Links und den Buttons „Änderungen verwerfen" / „Zahlungspflichtig aktualisieren". Nach Bestätigung verschwinden die Buttons und es erscheint ein grüner Erfolgshinweis; jede spätere Mengen-/Produktänderung setzt den Block wieder auf den ungespeicherten Zustand.
|
||||
- **Abo-Erhöhungsmodal (Nachtrag 2026-06-05):** Das bestehende Plus-Modal für dauerhafte Abo-Mengenerhöhungen wurde in `resources/views/admin/abo/_confirm_add_modal.blade.php` zentralisiert und rechtlich geschärft: Titel „Abo-Menge erhöhen", Info-Box, Artikel-/Mengenänderung, zusätzliche Kosten pro Lieferung, neuer Abo-Gesamtbetrag, Wirksamkeit zur nächsten Lieferung sowie AGB-/Widerrufs-Links. Primärer Button: „Zahlungspflichtig aktualisieren". Dieses Modal ist die aktuell umgesetzte aktive Zustimmung für dauerhafte Abo-Änderungen.
|
||||
- **Gesamtsummen-Card (Nachtrag 2026-06-05):** `_order_combined_summary.blade.php` wurde optisch hervorgehoben: Primary-Header mit weißem Titel, heller Card-Body und weiße Endsumme-Zeile. Die Card bleibt die maßgebliche Stelle für Versand, Steuer und Gesamtsumme der kombinierten nächsten Lieferung.
|
||||
- **Optimierung/Abweichung:** Statt `iq-modal-cart.js` zu überladen (Briefing 3.6) wurde ein **eigenes** JS-Modul gebaut – sauberere Trennung, da Einmal-Artikel einen eigenen Endpoint, eigene Holder und eine eigene Re-Render-Logik haben; Risiko für den bestehenden Abo-Warenkorb wird vermieden.
|
||||
- **Comp-Produkte (Nachtrag 2026-06-05):** Der Comp-Holder wird bei Einmal-AJAX-Aktionen mit aktualisiert; zusätzliche Kompensationsprodukte, die erst durch Einmal-/Zusatzprodukte nötig werden, erhalten einen sichtbaren Hinweis. Der Switch-Rebuild berücksichtigt Einmal-Artikel, damit eine Auswahl nicht mehr verschwindet.
|
||||
- **Tests (Nachtrag 2026-06-05):** `tests/Feature/AboOneTimeServiceTest.php`, `tests/Feature/AboOneTimeViewTest.php` und `tests/Feature/CartMaxWeightTest.php` wurden um Confirmation-State, Split-Summen, Modal-Inhalte, Comp-Hinweis und Maximalgewicht erweitert. Zuletzt geprüft: `./vendor/bin/pint --dirty --format agent` und `php artisan test --compact tests/Feature/AboOneTimeViewTest.php` (8 Tests / 28 Assertions grün).
|
||||
- **Offene Punkte für Phase 4/Folgearbeit:** Die technische Ausführung bleibt offen: Einmalprodukte in Cron/Retry übernehmen, `is_abo_addon` beim Schreiben der `ShoppingOrderItem` setzen, Purge nur nach erfolgreicher Zahlung, Rechnung/Lieferschein-Gruppierung und Reminder-Mail.
|
||||
|
||||
---
|
||||
|
||||
### Phase 4 – Ausführung, Retry & Aufräumen · `OFFEN`
|
||||
|
||||
- [ ] 4.1 `UserMakeOrder::makeShoppingOrder()`: Einmal-Artikel mit in den Yard; beim Anlegen der `ShoppingOrderItem` `is_abo_addon=true` setzen; Zahlbetrag = kombiniert.
|
||||
- [ ] 4.2 Zentrale Purge-Methode bei **Erfolg** aufrufen (Cron-Erfolg **und** `AboRetryPaymentService::markAboSuccess()`); bei Fehler **behalten** (Entscheidung #2/#3).
|
||||
- [ ] 4.3 `user_abos.amount` entkoppeln – reiner Abo-Betrag bleibt unverfälscht (Punkt 9.1).
|
||||
- [ ] 4.4 User-seitiges Retry: Route + Action im Sales Center (`User\AboController`) und Portal (`Portal\AboController`) → `AboRetryPaymentService::retry()` mit Berechtigungs-Check; Admin-Retry bleibt bestehen.
|
||||
- [ ] 4.5 num_comp bei Ausführung zurücksetzen (Einmal-Artikel raus → Kompensationsprodukte neu/zurück).
|
||||
- [ ] 4.6 Tests: Cron setzt `is_abo_addon` korrekt; Purge nur bei Erfolg; Behalten bei Fehler; User-Retry inkl. Einmal-Artikel; `amount` unverfälscht.
|
||||
|
||||
**Doku:** _(nach Umsetzung ausfüllen)_
|
||||
|
||||
---
|
||||
|
||||
### Phase 5 – Rechnung & Lieferschein · `OFFEN`
|
||||
|
||||
- [ ] 5.1 `invoice-detail.blade.php`: Positionen nach `is_abo_addon` gruppieren – Abo oben, abgegrenzter Block „Einmalig hinzugefügte Artikel" darunter.
|
||||
- [ ] 5.2 `delivery-detail.blade.php`: gleiche Gruppierung/Abgrenzung.
|
||||
- [ ] 5.3 Übersetzungen DE/EN/ES/FR für Block-Überschriften.
|
||||
- [ ] 5.4 Tests/Sichtprüfung: PDF mit gemischter Bestellung (Abo + Einmal) korrekt gruppiert.
|
||||
|
||||
**Doku:** _(nach Umsetzung ausfüllen)_
|
||||
|
||||
---
|
||||
|
||||
### Phase 6 – Reminder-E-Mail · `OFFEN`
|
||||
|
||||
- [ ] 6.1 Mailable `MailAboExecutionReminder` + Template (DE/EN/ES/FR): Ausführung in X Tagen, Deckung prüfen, jetzt Produkte mitnehmen / Nachhaltigkeit.
|
||||
- [ ] 6.2 Link je `is_for`: `me` → `getMyMivitaUrl()` + `user/abos/detail/me/{id}`; `ot` → `getMyMivitaPortalUrl()` + Portal-Abo-Route.
|
||||
- [ ] 6.3 Command `abo:send-execution-reminder`: selektiert Abos mit `next_date = heute + abo-onetime-window-days`, `active`, `status=2`; Duplikat-Schutz pro Zyklus.
|
||||
- [ ] 6.4 Schedule-Eintrag in `app/Console/Kernel.php`.
|
||||
- [ ] 6.5 Tests: Selektion, Link-Wahl Berater/Endkunde, Duplikat-Schutz.
|
||||
|
||||
**Doku:** _(nach Umsetzung ausfüllen)_
|
||||
|
||||
---
|
||||
|
||||
### Phase 7 – Gesamttests & Abschluss · `OFFEN`
|
||||
|
||||
- [ ] 7.1 End-to-End-Feature-Tests über alle Phasen.
|
||||
- [ ] 7.2 `./vendor/bin/pint` über alle geänderten Dateien.
|
||||
- [ ] 7.3 Vollständige Testsuite (nach Rücksprache) laufen lassen.
|
||||
- [ ] 7.4 Abschluss-Dokumentation in diesem File aktualisieren.
|
||||
|
||||
**Doku:** _(nach Umsetzung ausfüllen)_
|
||||
|
||||
---
|
||||
|
||||
## 7. Datenmodell-Detail (Referenz)
|
||||
|
||||
`user_abo_one_time_items` (Snapshot-Felder wie `shopping_order_items`, vgl. `database/migrations/2019_02_23_163724_create_shopping_order_items_table.php`):
|
||||
|
||||
```
|
||||
id BIGINT/INT PK
|
||||
user_abo_id INT FK -> user_abos (cascade)
|
||||
product_id INT FK -> products
|
||||
comp TINYINT nullable (auto. Kompensationsprodukte)
|
||||
qty INT
|
||||
price DECIMAL(8,2) nullable (Snapshot)
|
||||
price_net DECIMAL(8,3) nullable (Snapshot)
|
||||
tax_rate DECIMAL(5,2) nullable (Snapshot)
|
||||
tax DECIMAL(8,3) nullable (Snapshot)
|
||||
price_vk_net DECIMAL(8,3) nullable (Snapshot)
|
||||
discount DECIMAL(5,2) nullable (Snapshot)
|
||||
points INT nullable (Snapshot)
|
||||
status TINYINT default 0
|
||||
created_at / updated_at / deleted_at
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 8. Test-Strategie
|
||||
|
||||
- **Pest Feature-Tests** als Schwerpunkt (Boost-/Projekt-Regeln). Pro Phase mindestens Happy-Path + relevante Fehler-/Grenzpfade.
|
||||
- Factories für `UserAbo`, `UserAboItem`, `UserAboOneTimeItem`, `Product` (mit `show_on`, `weight`).
|
||||
- Payone/Zahlung in Ausführungs-/Retry-Tests **mocken**.
|
||||
- Zeit-/Datums-Tests mit `Carbon::setTestNow()` für das Fenster.
|
||||
|
||||
---
|
||||
|
||||
## 9. Zu beachtende Punkte / Ungereimtheiten / Optimierungen
|
||||
|
||||
> Diese Punkte sind **verbindlich** zu berücksichtigen und im jeweiligen Phasen-Doku-Abschnitt zu dokumentieren.
|
||||
|
||||
**9.1 `user_abos.amount` wird in `AboOrderCart::makeOrderYard()` bei jedem Aufruf überschrieben.**
|
||||
Beim Einbinden der Einmal-Artikel in denselben Yard würde `amount` verfälscht. → `amount` weiterhin nur aus reinen Abo-Items; kombinierter Betrag separat für die Abbuchung berechnen (Architektur 4.4).
|
||||
|
||||
**9.2 Konflikt mit Lock-Fristen in `AboRepository`.**
|
||||
`LOCK_DAYS_CHANGE = 10` / `LOCK_DAYS_PAUSE_CANCEL = 3` (hartkodiert) sperren Abo-Änderungen im Zeitfenster. Einmal-Artikel sind eine **eigene** Aktion und dürfen davon **nicht** betroffen sein → eigener Guard nur über `abo-onetime-window-days`.
|
||||
|
||||
**9.3 Kompensationsprodukte (`num_comp`) sind gewichts-/versandabhängig.**
|
||||
Mehr Gewicht durch Einmal-Artikel kann ein zusätzliches Kompensationsprodukt nötig machen (Entscheidung #7). Berechnung muss Gesamtgewicht einbeziehen **und** bei nächster Ausführung zurückgesetzt werden.
|
||||
|
||||
**9.4 Rechnung folgt asynchron über Payone-Callback, nicht im Cron.**
|
||||
Da `is_abo_addon` bereits beim Anlegen der `shopping_order_items` gesetzt wird und die Snapshot-Daten dauerhaft in der `ShoppingOrderItem` liegen, bleibt die Rechnung korrekt – auch nachdem `user_abo_one_time_items` gelöscht wurden (Reihenfolge Purge vs. Rechnung unkritisch).
|
||||
|
||||
**9.5 Cart-Option `abo_addon` zusätzlich zum DB-Flag.**
|
||||
Für getrennte Zwischensummen in der Live-Ansicht ohne DB-Roundtrip.
|
||||
|
||||
**9.6 Versandkostenfrei-Logik bleibt gültig** (`no_free_shipping`, `free_shipping_consultant`) – Einmal-Artikel verhalten sich wie normale Bestellpositionen (Entscheidung #7).
|
||||
|
||||
**9.7 Test-Umgebung: `trans_languages`-Migration (in Phase 1 gefixt).**
|
||||
Migration `2026_05_12_113939_add_french_locale_to_trans_languages.php` setzte eine nur in MySQL existierende Tabelle voraus → jeder RefreshDatabase-Test (SQLite `:memory:`) schlug fehl. Behoben durch `Schema::hasTable()`-Guard. Produktionssicher. Falls künftig weitere Migrationen Daten in nicht per Migration erzeugte Tabellen schreiben, gleiches Muster anwenden.
|
||||
|
||||
**9.8 Eigene Warenkorb-Instanz für den Abo-Flow (`AboOrderCart::INSTANCE = 'abo'`).**
|
||||
Zuvor nutzten der reguläre Bestell-Flow (`OrderController`) **und** der Abo-Detail-/Bearbeitungs-Flow (`AboOrderCart`) dieselbe Session-Instanz `Yard::instance('shopping')`. Bei zwei offenen Fenstern (Abo + Bestellung) vermischten sich die Warenkörbe, da der Cart pro Session nur unter dem Instanz-Namen gespeichert wird. → Abo-Flow auf dedizierte Instanz `'abo'` umgestellt (getrennt von `'shopping'` und der Abo-Anlage-Instanz `'subscription'`). Betroffen: `AboOrderCart` (inkl. `UserService::setInstance`), Cron `UserMakeOrder`, `User\AboController`/`Portal\AboController` (Datatables + `comp_product`-Render via `cart_instance`), `UserAboItem::getPrice()`, alle Abo-Views/Partials (`detail`, `my_abo`, `admin/abo/detail`, `_order_abo_show`, `_order_combined_summary`) sowie das geteilte Partial `comp_product` (jetzt mit `$cart_instance`-Parameter, Default `'shopping'`). `Yard::setTax()` arbeitet auf der zuletzt via `instance()` gesetzten Instanz und folgt der Umstellung automatisch.
|
||||
|
||||
**9.9 Versandkosten-Anzeige bei steuerbefreiten Kunden (Anzeige-Konsistenz).**
|
||||
In den Abo-Übersichten (`_order_abo_show`, `_order_combined_summary`) wurde die Versandzeile per `shipping()` (**brutto**) ausgegeben, obwohl steuerbefreite Kunden laut `Yard::totalWithShipping()` den **Netto**-Versand zahlen → Versandzeile stimmte nicht mit der (Netto-)Gesamtsumme überein. Fix: bei `getUserTaxFree()` jetzt `shippingNet()` anzeigen, sonst weiterhin `shipping()`. Der Bestell-Warenkorb (`yard_view_form`) war bereits korrekt (durchgängig netto). Abrechnung unverändert (steuerbefreit = Netto-Versand).
|
||||
|
||||
**9.10 Maximalgewicht-Sperre für alle Warenkörbe.**
|
||||
Die Versandkosten sind in Gewichtsstufen (`shipping_prices.weight_from`/`weight_to`) definiert. Bisher fiel die Berechnung bei Überschreitung der höchsten Stufe auf `shipping_prices->first()` (= **günstigste** Stufe) zurück – fachlich falsch. → Zwei Korrekturen:
|
||||
1. **Fallback-Fix** in `Yard::calculateShippingPrice()`: bei Gewicht über der obersten Stufe wird nun die **teuerste** Stufe (`sortByDesc('weight_to')->first()`) genutzt statt der günstigsten.
|
||||
2. **Zentrale Sperre** beim Hinzufügen/Hochsetzen: Neue Yard-Helfer `getMaxWeight()` (höchstes `weight_to` des Versandlandes) und `exceedsMaxWeight($additionalWeight)`. Würde eine Mengenerhöhung das Maximalgewicht überschreiten, wird das Produkt **nicht** hinzugefügt und ein Hinweis (`msg.cart_max_weight_reached`, DE/EN/FR/ES) angezeigt. Mengenreduktionen sind immer erlaubt.
|
||||
|
||||
Eingebaut in alle interaktiven Warenkörbe: regulärer Bestell-Warenkorb (`User\OrderController::handleUpdateCart`, Helfer `exceedsCartMaxWeight()`), Webshop (`Web\CardController` add/post/`updateCard`), Portal-Nachbestellung (`Portal\OrderController::addToCart`), Abo-Einmal-Artikel (`AboOneTimeService::add/update`) **und** permanente Abo-Artikel (`User\AboController`/`Portal\AboController` `addProduct`/`updateCart`). Für den Abo-Flow berechnet `AboOrderCart::combinedWeight()` das Gesamtgewicht aus regulären Abo- **und** Einmal-Artikeln; `AboOrderCart::exceedsMaxWeight()` prüft gegen das Maximalgewicht des (über `initYard()` gesetzten) Versandlandes. Programmatische Sammel-/Kombi-Bestellungen (Gewicht 0) bleiben unberührt.
|
||||
|
||||
**9.11 Berater-Bestell-Warenkorb: Plus/Minus-Buttons + Inline-Hinweis statt Alert-Popup.**
|
||||
Der Bestell-Warenkorb (`yard_view_form`) nutzte zuvor ein Zahlen-Input mit Spinner; bei einer Mengenüberschreitung (z. B. Maximalgewicht) erschien ein `alert()`-Popup, **der Zähler lief jedoch weiter hoch**. → Angeglichen an den Abo-Stil: Mengensteuerung jetzt über `−`/`+`-Buttons (`cart-remove-event`/`cart-add-event`, `input-group`/`input-extra`). Die `−`/`+`-Handler in `iq-shopping-cart.js` (`change_cart_qty`) erhöhen die Anzeige **nicht** optimistisch, sondern erst nach Server-Bestätigung per Re-Render. Wird das Update serverseitig abgelehnt, liefert `OrderController::cartUpdateRejected()` den **unveränderten** Warenkorb (`response: true`, `html_card` mit aktueller Menge) plus `error_message` zurück → der Zähler bleibt stehen und der Hinweis erscheint als **Inline-Alert** (`#insert_show_error_message`) über dem Warenkorb (kein Popup). JS-Refresh (`showCartError`) ersetzt das frühere `alert()` durch dieselbe Inline-Meldung. Betrifft Gewichts- **und** Sortiments-Sperre (`cart_max_weight_reached`, `cart_product_not_allowed_for_order_type`).
|
||||
|
||||
**9.12 Kompensationsprodukte bei Versandstufenwechsel sauber synchronisieren.**
|
||||
Wenn Einmal-Artikel oder reguläre Abo-Änderungen das Gesamtgewicht in eine andere Versandstufe verschieben, kann sich `num_comp` erhöhen oder wieder auf `0` fallen. Die alte Logik in `AboOrderCart::checkNumOfCompProducts()` legte durch eine Off-by-one-Schleife zu viele Comp-Produkte an und entfernte bestehende Comp-Produkte nicht, wenn die neue Versandstufe keine mehr benötigt. Fix: Es werden exakt fehlende Comp-Positionen `1..num_comp` angelegt, überschüssige Positionen konsequent aus DB und Yard entfernt, auch bei `num_comp = 0`. Zusätzlich wird im Sales-Center-Detail beim Laden offener Einmal-Artikel direkt `checkNumOfCompProducts()` ausgeführt (Portal hatte dies bereits), und AJAX-Re-Render respektiert wieder den tatsächlichen `add_only_mode`.
|
||||
|
||||
Nachtrag: Der normale Abo-Update-Endpunkt (`updateCompProduct`, reguläre Mengenänderungen) baut den Yard nun bei offenem Einmal-Fenster ebenfalls wieder **inkl. Einmal-Artikeln** auf, bevor `checkNumOfCompProducts()` läuft. Dadurch verschwindet ein nur durch Einmal-/Zusatzprodukte benötigtes Kompensationsprodukt beim Wechsel des Switches nicht mehr. Der Comp-Bereich wird als leerer Holder immer gerendert und bei Einmal-AJAX-Aktionen mit aktualisiert; Comp-Positionen oberhalb des reinen Abo-Bedarfs zeigen einen Hinweis, dass sie durch zusätzlich hinzugefügte Produkte nötig sind.
|
||||
|
||||
**9.13 Aktive Zustimmung bei dauerhaften Abo-Änderungen (umgesetzt über Modal).**
|
||||
Dauerhafte Abo-Mengenerhöhungen werden über das zentrale Bestätigungsmodal `_confirm_add_modal.blade.php` abgesichert: Vor der Änderung sieht der User Titel, Hinweistext, Artikel-/Mengenänderung, zusätzliche Kosten, neuen Abo-Gesamtbetrag, Wirksamkeitsdatum und AGB-/Widerrufs-Links. Erst der Klick auf „Zahlungspflichtig aktualisieren" bestätigt die kostenpflichtige Änderung. Ein separates Pending-Datenmodell wird für diesen Stand **nicht** umgesetzt, da die aktive Zustimmung fachlich über das Modal erfolgt. Einmal-Artikel bleiben davon getrennt und haben ihren eigenen Confirm-/Discard-State.
|
||||
|
||||
---
|
||||
|
||||
## 10. Offene Entscheidung (zu klären, blockiert NICHT Phase 1–7)
|
||||
|
||||
**Automatisiertes Retry** (du erwähntest: „muss noch etwas mehr automatisiert werden"):
|
||||
Der bestehende Retry ist Admin-manuell (`AboRetryPaymentService`). Eine **automatische** Wiederholung für `status=3`-Abos (geplanter Command, Intervall/Anzahl Versuche, Abbruchkriterien, Benachrichtigungen) ist sinnvoll, aber ein **eigenständiges Arbeitspaket**.
|
||||
|
||||
→ **Empfehlung:** Als separates Folge-Ticket/Phase außerhalb dieses Features umsetzen, um Scope und Risiko klein zu halten. **Entscheidung ausstehend** (in diesem Feature: nur **user-seitiges manuelles** Retry, Phase 4.4).
|
||||
|
||||
#Ja genau das ist ein eigenständiges Arbeitspaket. Erst mal nur den Retry wie in Phase 4.4
|
||||
---
|
||||
|
||||
## 11. Änderungs- / Fortschritts-Log
|
||||
|
||||
| Datum | Phase | Beschreibung | Status |
|
||||
| ----- | ----- | ------------ | ------ |
|
||||
| 2026-06-05 | – | Plan erstellt, Rahmenbedingungen geklärt | Plan finalisiert |
|
||||
| 2026-06-05 | 1 | Datenmodell (`user_abo_one_time_items`, `is_abo_addon`), Model + Factory, Relation, Setting, `isOneTimeWindowOpen()`, 13 Tests grün; Nebenfix `trans_languages`-Migration-Guard | ERLEDIGT |
|
||||
| 2026-06-05 | 2 | Backend: `addOneTimeItemsToYard`/`buildOneTimeSnapshot`/`getSplitSummary`, `AboOneTimeService`, `oneTime`-Endpoints (User+Portal) + Routen, `AboOneTimeItemRequest` + Übersetzungen; 10 Tests grün (23 gesamt) | ERLEDIGT |
|
||||
| 2026-06-05 | 3 | Frontend: View-Partials (`_order_onetime`, `_order_onetime_show`, `_order_combined_summary`), `split_mode` in `_order_abo_show`, Onetime-Modal + `oneTimeDatatable` (User+Portal), 2-spaltiges Layout mit sticky Summen-Card (detail/my_abo), JS `iq-abo-onetime.js`, Modal-Branch `abo-add-onetime`, Routen, 10 Übersetzungs-Keys; 4 Render-Tests grün (27 gesamt) | ERLEDIGT |
|
||||
| 2026-06-05 | Bugfix | Warenkorb-Trennung: Abo-Flow auf eigene Yard-Instanz `'abo'` (9.8), behebt Vermischung mit dem Bestell-Warenkorb über mehrere Fenster | ERLEDIGT |
|
||||
| 2026-06-05 | Bugfix | Versandkosten-Anzeige bei steuerbefreit in Abo-Übersichten auf Netto angeglichen (9.9) | ERLEDIGT |
|
||||
| 2026-06-05 | Bugfix | Maximalgewicht-Sperre für alle Warenkörbe + Fallback-Fix (teuerste statt günstigste Stufe) (9.10); `Yard::getMaxWeight()`/`exceedsMaxWeight()`, `AboOrderCart::combinedWeight()`/`exceedsMaxWeight()`, 4 Sprach-Keys; 17 Tests grün | ERLEDIGT |
|
||||
| 2026-06-05 | Bugfix | Berater-Bestell-Warenkorb: `−`/`+`-Buttons (Abo-Stil) statt Spinner, Inline-Hinweis statt Alert-Popup, Zähler läuft bei Stop nicht mehr hoch (9.11); `cartUpdateRejected()` + JS `change_cart_qty`/`showCartError` | ERLEDIGT |
|
||||
| 2026-06-05 | Bugfix | Kompensationsprodukte bei Versandstufenwechsel korrigiert (9.12): exakte Anlage statt Off-by-one, Entfernen auch bei `num_comp=0`, CRM-Detail synchronisiert Comp-Produkte nach Einmal-Artikeln, AJAX-Re-Render respektiert `add_only_mode`; Nachtrag: Comp-Switch-Rebuild berücksichtigt Einmal-Artikel, Comp-Hinweis für Zusatzprodukt-bedingte Positionen, Live-Render des Comp-Holders; 37 Tests grün | ERLEDIGT |
|
||||
| 2026-06-05 | UI/Consent | Rechtssicheres Modal für dauerhafte Abo-Mengenerhöhungen zentralisiert (`_confirm_add_modal`): Artikel-/Mengenänderung, Zusatzkosten, neuer Gesamtbetrag, Lieferdatum, AGB/Widerruf, Button „Zahlungspflichtig aktualisieren" (9.13) | ERLEDIGT |
|
||||
| 2026-06-05 | UI/Onetime | Confirm-/Discard-State für Einmalprodukte: `confirmed_qty`/`confirmed_at`, Aktionen `confirm`/`discard`, rechtliche Bestätigungsfläche, Erfolgshinweis nach Speichern und Rücksprung auf ungespeichert bei späteren Änderungen | ERLEDIGT |
|
||||
| 2026-06-05 | UI/Summen | Abo-Split-Liste zeigt neben der Abo-Zwischensumme auch die Endsumme der nächsten Lieferung; kombinierte Summen-Card optisch hervorgehoben; letzte Prüfung: 8 View-Tests / 28 Assertions grün | ERLEDIGT |
|
||||
Loading…
Add table
Add a link
Reference in a new issue