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:
parent
1e7ae92d0c
commit
cdb121fde0
32 changed files with 1479 additions and 52 deletions
|
|
@ -18,6 +18,7 @@ use App\Services\AboOneTimeService;
|
|||
use App\Services\AboOrderCart;
|
||||
use App\Services\AboPaymentMethodService;
|
||||
use App\Services\AboRecoveryCheckoutService;
|
||||
use App\Services\ProductOrderContext;
|
||||
use App\Services\Shop;
|
||||
use App\Services\UserService;
|
||||
use App\Services\Util;
|
||||
|
|
@ -684,6 +685,10 @@ class AboController extends Controller
|
|||
->whereJsonContains('show_on', ['13'])
|
||||
->orderBy('pos', 'ASC')
|
||||
->get(),
|
||||
'onetime_products' => Product::where('active', true)
|
||||
->whereJsonContains('show_on', ProductOrderContext::oneTimeShowOnIds())
|
||||
->orderBy('pos', 'ASC')
|
||||
->get(),
|
||||
'step' => 0,
|
||||
];
|
||||
if (Request::get('action') == 'back') {
|
||||
|
|
@ -721,6 +726,7 @@ class AboController extends Controller
|
|||
UserService::setInstance($this->instance);
|
||||
UserService::initCustomerYard($shopping_user, 'abo-ot-customer');
|
||||
$this->upgradeProductToCart();
|
||||
$this->oneTimeProductsToCart();
|
||||
$data['step'] = 4;
|
||||
break;
|
||||
case 5:
|
||||
|
|
@ -814,11 +820,83 @@ class AboController extends Controller
|
|||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Übernimmt die im Upgrade-Schritt gewählten Einmalprodukte (Normalsortiment) als
|
||||
* getrennte Cart-Zeilen mit Option `abo_addon = true`: nur erste Lieferung, wird beim
|
||||
* Checkout als is_abo_addon persistiert und nicht Teil des wiederkehrenden Abos.
|
||||
*/
|
||||
private function oneTimeProductsToCart(): void
|
||||
{
|
||||
$data = Request::all();
|
||||
|
||||
if (! isset($data['onetime_product_qty']) || ! is_array($data['onetime_product_qty'])) {
|
||||
return;
|
||||
}
|
||||
|
||||
foreach ($data['onetime_product_qty'] as $product_id => $quantity) {
|
||||
$product = Product::find($product_id);
|
||||
|
||||
if (! $product || ! ProductOrderContext::productMatchesShowOn($product, ProductOrderContext::oneTimeShowOnIds())) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$this->addOneTimeProductToCart($product, (int) $quantity);
|
||||
}
|
||||
}
|
||||
|
||||
private function addOneTimeProductToCart(Product $product, int $quantity): void
|
||||
{
|
||||
$cartItems = $this->yard->search(function ($item) use ($product) {
|
||||
return $item->id === $product->id && ($item->options->abo_addon ?? false);
|
||||
});
|
||||
|
||||
if ($quantity <= 0) {
|
||||
foreach ($cartItems as $item) {
|
||||
$this->yard->remove($item->rowId);
|
||||
}
|
||||
$this->yard->reCalculateShippingPrice();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if ($cartItems->count() > 0) {
|
||||
$this->yard->update($cartItems->first()->rowId, $quantity);
|
||||
} else {
|
||||
$image = $product->images->first()->slug ?? '';
|
||||
$price = $product->getPriceWith(
|
||||
$this->yard->getUserTaxFree(),
|
||||
false,
|
||||
$this->yard->getUserCountry()
|
||||
);
|
||||
|
||||
$this->yard->add(
|
||||
$product->id,
|
||||
$product->getLang('name'),
|
||||
$quantity,
|
||||
$price,
|
||||
false,
|
||||
false,
|
||||
[
|
||||
'image' => $image,
|
||||
'slug' => $product->slug,
|
||||
'weight' => $product->weight,
|
||||
'points' => $product->points,
|
||||
'no_commission' => $product->no_commission,
|
||||
'no_free_shipping' => $product->no_free_shipping,
|
||||
'show_on' => $product->show_on,
|
||||
'abo_addon' => true,
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
$this->yard->reCalculateShippingPrice();
|
||||
}
|
||||
|
||||
private function addProductToCart($product, $quantity)
|
||||
{
|
||||
// Suche nach dem Produkt im Warenkorb
|
||||
// Suche nach dem Produkt im Warenkorb (Einmal-Zeilen haben eine eigene rowId und bleiben außen vor)
|
||||
$cartItems = $this->yard->search(function ($item) use ($product) {
|
||||
return $item->id === $product->id;
|
||||
return $item->id === $product->id && ! ($item->options->abo_addon ?? false);
|
||||
});
|
||||
|
||||
// Wenn die Menge 0 ist, entferne das Produkt
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue