Abo: Einmalprodukte bereits bei der Abo-Anlage + es/fr-Sprachsync

Einmalprodukte (Normalsortiment, show_on 3) können jetzt schon beim
Abo-Abschluss hinzugefügt werden – im Portal-Wizard (Abschnitt im
Upgrade-Schritt) und in allen drei Sales-Center-Abo-Strecken (eigene
Produktliste). Variante A: Zeilen laufen als Cart-Option abo_addon mit
durch den Checkout, werden sofort bezahlt und mit der Erstlieferung
verschickt (Rechnung/Lieferschein gruppieren automatisch).

- Checkout persistiert is_abo_addon (CheckoutRepository, PaymentHelper)
- createAboItems/ensureUserAboItemsFromLatestOrder überspringen Addons
  (sonst würden Einmalprodukte dauerhaft wiederkehren); amount ohne
  Addon-Anteil; aboHasBaseProduct ignoriert Addon-Zeilen
- CreateAboReplacementOrder propagiert is_abo_addon in Ersatzbestellungen
- iq-shopping-cart.js: zweite Datatable + abo_addon-Flag-Weitergabe
- i18n: neue abo.onetime_create_*-Keys (de/en/es/fr); zusätzlich es/fr
  komplett synchronisiert (abo-Kartenblock, navigation, payment,
  weborder inkl. Entfernung PAYONE-Altlasten, withdrawal.php neu)
- Tests: AboCreateOneTimeItemsTest, AboCreateOneTimeCartTest,
  AboPortalCreateOneTimeTest (17 neue, 122 Tests gesamt grün)

Plan: dev/2026-07-21-abo-einmalprodukte-anlage/

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Kevin 2026-07-21 14:36:40 +00:00
parent 438a1e72d8
commit 50d55f4b35
32 changed files with 1479 additions and 52 deletions

View file

@ -0,0 +1,244 @@
# Entwicklungsplan: Einmalprodukte bereits bei der Abo-Anlage hinzufügen
> **Status:** In Planung · **Erstellt:** 2026-07-21 · **Basis-Feature:** `dev/2026-06-05/ENTWICKLUNGSPLAN-ABO-EINMALPRODUKTE.md` (Einmalprodukte für laufende Abos, Phasen 1–7 umgesetzt)
## 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).
- **Alle Phasen, Hinweise und getroffenen Detail-Entscheidungen** werden sauber dokumentiert – auch Abweichungen vom Plan 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 --dirty`.
---
## 1. Ziel & Briefing
Bisher können Einmalprodukte (normales Bestellsortiment, `show_on = '3'`) nur zu **laufenden** Abos hinzugefügt werden – innerhalb des Zeitfensters vor der nächsten Ausführung. Sie gelten nur für die nächste Lieferung, erscheinen auf Rechnung + Lieferschein (eigene Gruppe „Einmalig hinzugefügte Artikel") und werden nach erfolgreicher Ausführung entfernt.
**Neu:** Auch beim **Abschluss** (Anlage) eines Abos sollen Einmalprodukte hinzugefügt werden können – also im Abo-Anlage-Prozess (Portal-Wizard und Sales-Center-Bestellstrecke), bevor das Abo existiert.
---
## 2. Ist-Zustand (verifiziert am 2026-07-21, alle 42 Einmalprodukte-Tests grün)
### 2.1 Bestehendes Einmalprodukte-Feature (laufende Abos)
| Baustein | Umsetzung | Schlüsseldateien |
| -------- | --------- | ---------------- |
| Datenmodell | Tabelle `user_abo_one_time_items` (Preis-Snapshot, `confirmed_qty`/`confirmed_at`, SoftDeletes); Flag `is_abo_addon` auf `shopping_order_items` | `app/Models/UserAboOneTimeItem.php`, `app/Models/UserAbo.php:358`, `app/Models/ShoppingOrderItem.php:89,110,119-131` |
| Zeitfenster | Setting `abo-onetime-window-days`, Default **4** Kalendertage (`DEFAULT_ONETIME_WINDOW_DAYS`); VIP (`admin>=1`) umgeht das Fenster | `app/Services/AboHelper.php:202-249` |
| Service | `AboOneTimeService::handleAction()` (add/update/remove/confirm/discard); nur **bestätigte** Items (`confirmed_at` gesetzt UND `confirmed_qty == qty`) fließen in die Ausführung | `app/Services/AboOneTimeService.php:19-160` |
| Ausführung | `UserMakeOrder`: `makeOrderYard()` (reine Abo-Produkte, setzt `user_abos.amount`) → `AboOrderCart::addOneTimeItemsToYard()` (Cart-Option `abo_addon=true`, Snapshot-Preis) → Order-Item bekommt `is_abo_addon` | `app/Cron/UserMakeOrder.php:220,224,315`, `app/Services/AboOrderCart.php:242-287` |
| Purge | Nach erfolgreicher Abbuchung `AboOneTimeService::purgeAfterExecution()` (Force-Delete + Comp-Rebalancing); auch nach Recovery-Zahlung | `app/Cron/UserMakeAboOrder.php` (via Command), `app/Services/AboHelper.php:480` |
| PDF | Rechnung + Lieferschein gruppieren nach `is_abo_addon` (Überschrift `pdf.abo_addon_items_heading`) | `resources/views/pdf/invoice-detail.blade.php:30-49`, `resources/views/pdf/delivery-detail.blade.php:14-32` |
| UI | Portal (`Portal/AboController@oneTime`) + Sales Center (`User/AboController@oneTime`), gemeinsame Partials `admin/abo/_order_onetime*.blade.php`, Produkt-Datatable gefiltert auf `show_on` enthält `'3'` | `routes/domains/portal.php:59-66`, `routes/domains/crm.php:164-174` |
| Reminder | `abo:send-execution-reminder` (06:00) + `abo:preview-reminder` | `app/Console/Commands/AboSendExecutionReminder.php` |
### 2.2 Abo-Anlage heute (zwei Strecken, Einmalprodukte spielen KEINE Rolle)
**A) Portal (Kunde, Self-Service):** `Portal/AboController@myAboCreate` – Step-Wizard (0–5): Basis-Produkte `show_on='12'`, Upgrade-Produkte `show_on='13'`, Yard-Instanz `subscription`-Cart mit `is_for='abo-ot-customer'`, dann `processCheckout()` → `ShoppingInstance` (`is_abo=true`, `abo_interval`) → Standard-Checkout. Datei: `app/Http/Controllers/Portal/AboController.php:658-905`.
**B) Sales Center (Berater):** `User/OrderController` Bestellstrecke mit `$for ∈ {abo-me, abo-ot-member, abo-ot-customer}` – Produktliste/Datatable auf `show_on 12/13` beschränkt (`OrderController.php:656,674`), Cart-Add prüft hart `ProductOrderContext::isProductAllowedInContext()` (`OrderController.php:827` → Ablehnung `cart_product_not_allowed_for_order_type`).
**Abo-Entstehung nach Zahlung:** `AboHelper::createNewAbo(ShoppingPayment)` (`AboHelper.php:302-387`) – aufgerufen aus `CheckoutController` (659, 702), `Payment.php:295`, Stripe-Webhook. Legt `UserAbo` an (`amount = shopping_payment->amount`, `next_date = getFirstAboDate()`) und ruft `createAboItems()` (`AboHelper.php:483`): **kopiert ALLE `shopping_order_items` 1:1 in `user_abo_items`** (= wiederkehrende Positionen).
### 2.3 Relevante Lücken (für dieses Feature entscheidend)
1. **`is_abo_addon` wird beim Checkout nicht persistiert.** Nur der Cron-Pfad (`UserMakeOrder.php:315`) schreibt das Flag. Die Checkout-Order-Item-Erzeugung in `CheckoutRepository.php:~180-204` und `PaymentHelper::makeShoppingOrder()` (`PaymentHelper.php:115-136`) kennt die Cart-Option `abo_addon` nicht.
2. **`createAboItems()` kopiert alles** – Einmalprodukte in der Erstbestellung würden zu **dauerhaften** Abo-Positionen. Gleiche Falle: `ensureUserAboItemsFromLatestOrder()` (`AboHelper.php:503-536`) und `RepairMissingAboFromOrders` (nutzt `createNewAbo` → derselbe Fix greift).
3. **Produktkontext blockiert:** `ProductOrderContext::allowedShowOnIds(true, …)` = `['12','13']` – Sortiment `'3'` ist im Abo-Cart heute verboten (gewollt, damit keine Normalprodukte zu Abo-Positionen werden).
4. **`user_abos.amount`** würde bei Anlage die Einmalprodukte enthalten (`= shopping_payment->amount`); korrigiert sich erst bei der ersten Cron-Ausführung durch `makeOrderYard()`. Bis dahin zeigt das Abo einen zu hohen Betrag.
---
## 3. Grundsatz-Entscheidung: Wann werden die Einmalprodukte geliefert?
| Variante | Beschreibung | Bewertung |
| -------- | ------------ | --------- |
| **A – Mit der Erstbestellung (EMPFOHLEN)** | Einmalprodukte kommen in den Anlage-Warenkorb (Cart-Option `abo_addon=true`), werden **sofort mit dem Checkout bezahlt** und mit der ersten Lieferung verschickt. Auf Erstrechnung + Lieferschein erscheinen sie als „Einmalig hinzugefügte Artikel" (Gruppierung existiert schon). Sie werden bei der Abo-Anlage **nicht** in `user_abo_items` übernommen → tauchen in keiner Folgelieferung auf. Kein Eintrag in `user_abo_one_time_items` nötig (nichts zu purgen). | Natürliche Semantik von „beim Abschluss hinzufügen": ein Bezahlvorgang, sofortige Lieferung. Technisch schlank. |
| B – Für die erste reguläre Ausführung | Bei Anlage werden die Produkte als **bestätigte** `user_abo_one_time_items` ans neue Abo gehängt; Lieferung + Abbuchung erst mit der ersten Cron-Ausführung (`next_date`, ~1 Monat später). | Getrennte spätere Abbuchung (Überraschungseffekt), Kunde wartet einen Monat, mehr Moving Parts (Snapshot ohne initialisierten Abo-Yard). Nur sinnvoll, wenn fachlich explizit gewünscht. |
> **Annahme A1:** Variante **A**. Bitte vor Umsetzungsbeginn bestätigen. (Variante B wäre als Zusatzoption später nachrüstbar, z. B. Checkbox „erst mit der nächsten regulären Lieferung".)
### Weitere Annahmen
| # | Thema | Annahme |
| - | ----- | ------- |
| A2 | Betroffene Strecken | Beide: Portal-Wizard **und** Sales-Center-Abo-Strecken (`abo-me`, `abo-ot-member`, `abo-ot-customer`). |
| A3 | Zeitfenster | Bei der Anlage **irrelevant** – das Fenster (`abo-onetime-window-days`) gilt nur für laufende Abos. Kein Gate bei Anlage. |
| A4 | Bestätigungs-Schritt | Entfällt – der Checkout selbst ist die verbindliche Bestätigung (kein `confirm`-Workflow wie im laufenden Abo). |
| A5 | Sortiment | Wie im Bestand: normales Bestellsortiment `show_on` enthält `'3'`; für `abo-me` analog Beraterbestellung `'2'` (klären, siehe Phase 3/4). |
| A6 | Basis-Produkt-Pflicht | `aboHasBaseProduct()`-Prüfung bleibt unverändert (Einmalprodukte zählen nicht als Basis-Produkt – Prüfung ist Whitelist auf Basis-Produkte, Zusatzitems stören nicht). |
---
## 4. Lösungsarchitektur (Variante A)
Kernidee: **Der Anlage-Warenkorb transportiert Einmalprodukte als normale Cart-Rows mit Option `abo_addon=true`** – exakt die Option, die der Cron-Pfad heute schon nutzt. Damit:
1. Checkout/Zahlung/Versand funktionieren unverändert (Position ist eine normale Warenkorbzeile: Gewicht, Versandkosten, Steuer, Punkte inklusive).
2. Die Order-Item-Erzeugung persistiert das Flag → `shopping_order_items.is_abo_addon = true` → **PDF-Gruppierung auf Rechnung/Lieferschein greift automatisch** (bestehende Logik + Tests).
3. `createAboItems()` überspringt `is_abo_addon`-Positionen → nichts wird dauerhaft.
Kein neues Datenmodell, keine Migration, kein Purge nötig.
---
## 5. Phasen
### Phase 1 — Backend-Fundament: Flag durch den Checkout tragen
**Status:** ERLEDIGT (2026-07-21)
**Umsetzung:**
- `app/Repositories/CheckoutRepository.php`: beide Order-Item-Zweige (Update ~Z.155, Create ~Z.193) schreiben `'is_abo_addon' => (bool)($item->options->abo_addon ?? false)`.
- `app/Services/PaymentHelper.php::makeShoppingOrder()`: identisch ergänzt.
- `Api/ShoppingUserController.php:700` bewusst NICHT angefasst (API-Bestellungen sind keine Abo-Anlage; Default `false` über DB-Spalte).
- Test: `tests/Feature/AboCreateOneTimeItemsTest.php` („persistiert die Cart-Option abo_addon…", via `PaymentHelper::makeShoppingOrder` mit echtem Yard).
- `app/Repositories/CheckoutRepository.php` (Order-Item-Erzeugung, beide Zweige Update+Create um `'is_abo_addon' => (bool)($item->options->abo_addon ?? false)` ergänzen).
- `app/Services/PaymentHelper.php::makeShoppingOrder()` analog.
- Prüfen: `app/Http/Controllers/Api/ShoppingUserController.php:700` (API-Order-Items) – vermutlich nicht abo-relevant, dokumentieren.
- **Test:** Feature-Test – Yard mit `abo_addon`-Row → ShoppingOrder → Order-Item trägt `is_abo_addon=true`; normale Rows bleiben `false`.
### Phase 2 — Abo-Anlage: Einmalprodukte NICHT in `user_abo_items` übernehmen
**Status:** ERLEDIGT (2026-07-21)
**Umsetzung:**
- `AboHelper::createAboItems()`: überspringt `is_abo_addon`-Positionen.
- `AboHelper::ensureUserAboItemsFromLatestOrder()`: ebenso (Repair-Pfad).
- `AboHelper::createNewAbo()`: `amount = shopping_payment->amount − Addon-Brutto (price × qty, in Cent)`, konsistent zur Definition von `makeOrderYard()` (Abo-Artikel + Versand). Versand bleibt der Erstbestellungs-Versand, wird bei der ersten Cron-Ausführung ohnehin neu berechnet.
- Tests: `tests/Feature/AboCreateOneTimeItemsTest.php` (5 Tests grün); Regression: `PaymentAboCallbackCreatesUserAboTest`, `AboHelperEnsureUserAboItemsFromLatestOrderTest`, `StripeAboTest`, `StripeCheckoutTest`, `AboMakeOrderOneTimeTest` (43 Tests grün).
- `AboHelper::createAboItems()` (`AboHelper.php:483`): Positionen mit `is_abo_addon=true` überspringen.
- `AboHelper::ensureUserAboItemsFromLatestOrder()` (`AboHelper.php:527`): ebenfalls überspringen (Repair-Pfad).
- `AboHelper::createNewAbo()`: `amount` **ohne** Addon-Anteil setzen (Summe der `aboOnly()`-Positionen konsistent zur Betragsdefinition von `makeOrderYard()`; genaue Formel bei Umsetzung an `makeOrderYard()` ausrichten und im Plan dokumentieren).
- **Tests:** Abo-Anlage aus Order mit Addon-Items → `user_abo_items` enthält nur Abo-Produkte; `amount` ohne Addons; Repair-Command-Pfad ebenso.
### Phase 3 — Sales Center: Einmalprodukte in der Abo-Bestellstrecke
**Status:** ERLEDIGT (2026-07-21)
**Umsetzung:**
- `ProductOrderContext::oneTimeShowOnIds()` neu (`['3']`, wie Bestandsfeature; gilt lt. A5 auch für `abo-me`).
- `User/OrderController@datatable`: Modus `abo_addon=1` (nur bei `is_abo`) → Einmal-Sortiment; `product`-Spalte sucht die Addon-Cart-Zeile per Yard-`search` (eigene rowId, da Optionen in die rowId einfließen), Inputs `onetime_qty_*` + `data-abo-addon="1"`.
- `User/OrderController@handleUpdateCart`: Request-Flag `abo_addon` → Sortiment-Prüfung gegen `oneTimeShowOnIds()`, Cart-Option `abo_addon=true` (getrennte Zeile). Gewichts-/Comp-Logik unverändert.
- Neues Partial `resources/views/user/order/_onetime_list.blade.php` (eigene Datatable `#datatables-onetime-list`), eingebunden in `list_me`/`list_ot-member`/`list_ot-customer` bei `$is_abo`; Preisspalten je Flow (me = EK, ot = VK).
- `yard_view_form.blade.php`: Addon-Zeilen mit Badge `abo.onetime_badge_first_delivery`; Mengen-Controls und Entfernen-Link tragen `data-abo-addon`.
- `public/js/iq-shopping-cart.js`: zweite Tabelle, Addon-Flag-Weitergabe (`abo_addon` im updateCart-Request), zielgenaue Input-/Zeilen-Auflösung via `data-abo-addon`.
- i18n: `abo.onetime_create_hl`, `abo.onetime_create_info`, `abo.onetime_badge_first_delivery` (de/en/es/fr).
- Tests: `tests/Feature/AboCreateOneTimeCartTest.php` (6 Tests grün) — Controller direkt aufgerufen (Domain-Routing hat Catch-All → kein HTTP-Roundtrip; etabliertes Muster „route() vermeiden").
- **Hinweis:** `datatable()`-Query nutzt `JSON_CONTAINS` (MySQL) → auf SQLite nicht testbar; Sortiment-Logik ist über `oneTimeShowOnIds()`-Test + Cart-Ablehnungstests abgedeckt.
- `User/OrderController@list` (Abo-Flows): zusätzlicher Bereich „Einmalprodukte (nur erste Lieferung)" – zweite Produktliste/Datatable auf Normalsortiment (`'3'`, bei `abo-me` ggf. `'2'`, siehe A5), UI-Muster der bestehenden Einmalprodukte-Ansicht (`modal_abo_onetime_products` / Datatable `OrderController@datatable` mit neuem Kontext-Parameter).
- `handleUpdateCart()` (`OrderController.php:816-885`): Request-Flag `abo_addon` akzeptieren; Kontextprüfung dann gegen Normalsortiment statt `['12','13']`; Cart-Add mit Option `abo_addon=true`. Gewichts-/Comp-Logik läuft unverändert über den Yard.
- `ProductOrderContext`: Hilfsmethode für den Addon-Kontext (z. B. `allowedOneTimeShowOnIds(string $for)`), keine Aufweichung der bestehenden Abo-Prüfung.
- Warenkorb-Ansicht (`user/order/yard_view_form` + Zusammenfassung): Addon-Zeilen visuell kennzeichnen/gruppieren („einmalig, nur erste Lieferung"), analog `AboOrderCart::getSplitSummary()`-Muster.
- **Tests:** Add erlaubt mit Flag + `show_on 3`; ohne Flag weiterhin abgelehnt; Abo-Produkt mit Flag abgelehnt.
### Phase 4 — Portal-Wizard: Einmalprodukte-Schritt
**Status:** ERLEDIGT (2026-07-21)
**Entscheidung:** Kein neuer Wizard-Schritt (Step-Nummerierung + back-Logik bleiben unangetastet) — Einmalprodukte sind ein **optionaler Abschnitt im Upgrade-Schritt** (Step 3-View), verarbeitet beim Submit zu Step 4.
**Umsetzung:**
- `Portal/AboController`: `onetime_products` (aktiv, `oneTimeShowOnIds()`) in `prepareAboCreateData()`; case 4 ruft zusätzlich `oneTimeProductsToCart()`; neue Methoden `oneTimeProductsToCart()`/`addOneTimeProductToCart()` (Sortiment-Prüfung, Cart-Option `abo_addon=true`, qty 0 = entfernen); `addProductToCart()` (Basis/Upgrade) schließt Einmal-Zeilen aus der Warenkorb-Suche aus.
- `AboHelper::aboHasBaseProduct()`: überspringt Einmal-Zeilen (ein Addon mit `show_on ['3','12']` zählte sonst fälschlich als Basis-Produkt).
- Views: neue Komponente `componenten/onetime-product-table.blade.php` (addon-bewusste Warenkorb-Suche, Inputs `onetime_product_qty[id]`); Abschnitt in `_create_upgrade_products.blade.php` (Überschrift + Info-Alert); `_create_check.blade.php` zeigt Addon-Zeilen mit Badge `abo.onetime_badge_first_delivery` + Info-Alert.
- i18n: `abo.onetime_product` (de/en/es/fr).
- Tests: `tests/Feature/AboPortalCreateOneTimeTest.php` (5 Tests grün, private Methoden via Reflection — Wizard braucht Customer-Guard + Session, HTTP-Roundtrip scheitert am Domain-Catch-All). Blade-Kompilierung via `view:cache` verifiziert, JS via `node --check`.
- `Portal/AboController::prepareAboCreateData()`: neuer optionaler Schritt zwischen Upgrade-Produkten und Zusammenfassung (Step-Nummerierung + `action=back`-Logik anpassen) ODER Abschnitt im bestehenden Upgrade-Schritt – Entscheidung nach UI-Sichtung von `my_abo_create.blade.php`.
- Produktquelle: `show_on='3'` (aktiv), Darstellung analog Basis-/Upgrade-Kacheln mit Mengenfeld (`onetime_product_qty[]`).
- `addProductToCart()`-Variante mit Option `abo_addon=true` (Preislogik identisch `abo-ot-customer`-Zweig; vgl. `buildOneTimeSnapshot()`-Rundung `round(price, 1)` – konsistent halten).
- Zusammenfassung (Step 4/5) + Checkout-Seite: getrennte Anzeige „Abo-Produkte" vs. „Einmalige Artikel (nur erste Lieferung)" inkl. Hinweistext, dass diese nicht Teil der wiederkehrenden Lieferung sind.
- **Tests:** Wizard-Flow mit Einmalprodukt → Yard enthält Row mit `abo_addon`; Basis-Produkt-Pflicht unverändert; Step-Navigation (back) intakt.
### Phase 5 — Rechnung/Lieferschein & E-Mails verifizieren
**Status:** ERLEDIGT (2026-07-21)
**Umsetzung:**
- Erwartung bestätigt: Gruppierung greift automatisch über `is_abo_addon` (bestehende PDF-Logik). Integrationstest in `AboCreateOneTimeItemsTest` („gruppiert Erstrechnung und Lieferschein…"): Anlage-Order → `pdf.invoice-detail` + `pdf.delivery-detail` zeigen beide Blöcke, Abo vor Einmal.
- Bestellbestätigungs-Mails: keine Änderung (Positionsreihenfolge folgt der Order; bewusst klein gehalten — bei Bedarf später nachschärfen).
- Erwartung: Erstrechnung + Lieferschein gruppieren automatisch (bestehende `is_abo_addon`-Logik). **Test** analog `AboOneTimeInvoiceGroupingTest` für eine Anlage-Order.
- Bestellbestätigungs-Mail(s) der Checkout-Strecke prüfen: Addon-Kennzeichnung sinnvoll? (Klein halten; ggf. nur Reihenfolge wie PDF.)
- Hinweis Test-System: Rechnungen entstehen nur in Produktion – auf `.test` mit `sales:preview-invoice` prüfen.
### Phase 6 — Folge-Logik & Rand-Pfade
**Status:** ERLEDIGT (2026-07-21)
**Prüfergebnisse:**
- `IncentiveTracker::trackAboActivated`: zählt das **Abo selbst** (`points_abo_onetime` je Incentive), keine Order-Punkte → von Addons unberührt.
- Provision/Punkte: Einmalprodukte zählen wie normale Positionen der Erstbestellung (Order-`points` inkl. Addons — gewollt, wie beim Bestandsfeature in der Folgelieferung); Abo-eigene Auswertungen (`user_abo_items`, `amount`) bleiben Addon-frei (P2).
- `RepairMissingAboFromOrders`: nutzt `createNewAbo` → P2-Fix greift automatisch.
- `CreateAboReplacementOrder`: übernahm das Flag nicht → `is_abo_addon` wird jetzt in `calculateReplacement()`/`createReplacementItems()` propagiert (Ersatzrechnung behält Gruppierung). Regressionstest grün.
- Stripe-Webhook-Pfad: selber `createNewAbo`-Code, `StripeAboTest`/`StripeCheckoutTest` grün.
- Recovery-Checkout: legt kein Abo an; Variante A schreibt nichts in `user_abo_one_time_items` → keine Interferenz mit `purgeAfterExecution`.
- `Api/ShoppingUserController` (API-Order-Items): keine Abo-Anlage, DB-Default `false` korrekt — unverändert.
- **Provision/Punkte:** Einmalprodukte der Erstbestellung zählen wie normale Verkaufs­positionen der Order (Punkte/Provision der Erstbestellung) – Verhalten dokumentieren; sicherstellen, dass keine Abo-spezifische Auswertung (`user_abo_items`, `amount`) sie mitzählt.
- `IncentiveTracker::trackAboActivated` prüfen (nutzt Order – zählt sie Order-Punkte? Falls ja: Entscheidung dokumentieren).
- `CreateAboReplacementOrder` (Ersatzbestellung aus `user_abo_items`): unberührt, da Addons dort nie ankommen – kurz verifizieren.
- Stripe-Webhook-Pfad (`createNewAbo` via Webhook) läuft durch dieselben Fixes – Test mit Stripe-Payment-Fake, falls vorhandene Test-Infrastruktur das hergibt.
- Recovery-Checkout (`advanceAboAfterRecoveryPayment`): kein neues Abo, keine Anlage-Addons – unberührt, verifizieren dass `purgeAfterExecution` keine Anlage-Interferenz hat (Variante A schreibt nichts in `user_abo_one_time_items`).
### Phase 7 — i18n, Pint, Gesamttest
**Status:** ERLEDIGT (2026-07-21)
- Neue Keys `abo.onetime_create_hl`, `abo.onetime_create_info`, `abo.onetime_badge_first_delivery`, `abo.onetime_product` in de/en/es/fr (de in du-Form).
- **Zusatzfund:** `abo.php` war in es/fr bereits VOR dieser Umsetzung nicht synchron — der komplette Kreditkarten-/Zahlartwechsel-Block (41 Keys, `card_*`/`payment_method_change_*`, aus dem CC-Update-/Stripe-Feature) fehlte. In es/fr nachübersetzt → `abo.php` jetzt synchron.
- **Nachtrag (2026-07-21, auf Kundenwunsch):** Auch die restlichen vorbestehenden Lücken aus dem Stripe-/Widerrufs-Workstream wurden synchronisiert: `navigation.php` (es/fr: `my_statistics`, `team_statistics`, `withdrawals`), `payment.php` (es/fr: 15 Stripe-/Wartungs-Keys), `weborder.php` (es/fr: 17 Stripe-Zahlart-Keys ergänzt, 4 PAYONE-Altlasten `payment_method_direct/sepa` entfernt), `withdrawal.php` (es/fr: komplett neu übersetzt, es = usted, fr = vous). `LocalizationSyncTest` + `FrenchLocalizationTest` + `LocalizationMiddlewareTest` grün (8 Tests, 286 Assertions).
- `./vendor/bin/pint --dirty` → pass; Blade-Kompilierung (`view:cache`) + `node --check` fürs JS grün.
- Gesamtlauf: 122 Tests grün (neue 16 + Einmalprodukte-Bestandssuite + Checkout-/Stripe-/Repair-Regressionen).
---
## 9. Umsetzungs-Zusammenfassung (2026-07-21)
Alle 7 Phasen umgesetzt. Kern-Mechanik: Cart-Option `abo_addon` → `shopping_order_items.is_abo_addon` (Checkout persistiert jetzt) → PDF-Gruppierung automatisch; `createAboItems`/`ensureUserAboItemsFromLatestOrder` überspringen Addons; `user_abos.amount` ohne Addon-Anteil. Kein neues Datenmodell, keine Migration. Neue Tests: `AboCreateOneTimeItemsTest` (6), `AboCreateOneTimeCartTest` (6), `AboPortalCreateOneTimeTest` (5).
---
## 6. Risiken & Stolperfallen
1. **Die Kopier-Falle ist der kritischste Punkt:** Ohne Phase 2 würden Einmalprodukte zu dauerhaften Abo-Positionen (und ab der ersten Cron-Ausführung monatlich berechnet). Phase 2 daher **vor** jeder UI-Phase umsetzen und testen.
2. **`user_abos.amount`**-Definition exakt an `makeOrderYard()` ausrichten, sonst springt der angezeigte Abo-Betrag bei der ersten Ausführung.
3. **Cart-Persistenz zwischen Steps:** Beide Strecken re-initialisieren den Yard pro Step (`initCustomerYard`) – Addon-Rows müssen Step-Wechsel und `action=back` überleben (Option hängt an der Row, sollte gegeben sein; testen).
4. **Kontextwechsel-Reset:** `OrderController@list:194-199` zerstört den Yard bei Sortimentswechsel (`allowedShowOnIds`-Vergleich) – neue Addon-Logik darf diesen Vergleich nicht verfälschen.
5. **Gewichtslimit/Versandkosten:** Addon-Rows zählen ins Gesamtgewicht (gewollt, wie im Bestand) – Max-Gewicht-Prüfung greift automatisch (`exceedsCartMaxWeight`).
6. **Keine Vermischung mit dem Bestandsfeature:** `user_abo_one_time_items`, Fenster-Logik, `confirm`-Workflow und Reminder bleiben unangetastet (gelten weiter nur für laufende Abos).
7. **Homeparty/Collection-Sonderzweige** in `CheckoutRepository` (setzen `product_id = null`): Abo-Anlage läuft nicht über diese Zweige, aber beim Ergänzen des Flags nicht hineinpfuschen.
---
## 7. Betroffene Dateien (Vorschau)
**Geändert:**
- `app/Repositories/CheckoutRepository.php`, `app/Services/PaymentHelper.php` (P1)
- `app/Services/AboHelper.php` (`createAboItems`, `ensureUserAboItemsFromLatestOrder`, `createNewAbo`/amount) (P2)
- `app/Http/Controllers/User/OrderController.php` (P3)
- `app/Services/ProductOrderContext.php` (P3)
- `resources/views/user/order/*` (P3)
- `app/Http/Controllers/Portal/AboController.php` (P4)
- `resources/views/portal/abo/my_abo_create.blade.php` (P4)
- `resources/lang/{de,en,es,fr}/…` (P7)
**Neu (Tests):**
- `tests/Feature/AboCreateOneTimeItemsTest.php` (Kern: Flag-Persistenz, createAboItems-Skip, amount)
- `tests/Feature/AboCreateOneTimeUiTest.php` (Strecken/Views), ggf. aufgeteilt
---
## 8. Umsetzungsreihenfolge
P1 + P2 (Backend-Fundament + Kopier-Falle, isoliert testbar) → P3 (Sales Center) → P4 (Portal) → P5 (PDF/Mail-Verifikation) → P6 (Rand-Pfade) → P7 (i18n/Abschluss).
Vor P1: **Annahme A1 (Variante A) und A2/A5 bestätigen lassen.**