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 1e7ae92d0c
commit cdb121fde0
32 changed files with 1479 additions and 52 deletions

View file

@ -1,6 +1,8 @@
var IqShoppingCart = {
table: "#datatables-order-list",
// Zweite Produktliste (Abo-Anlage): Einmalprodukte, deren Cart-Zeilen die Option abo_addon tragen
table_onetime: "#datatables-onetime-list",
btn_add: '.add-product-basket',
btn_remove: '.remove-product-basket',
card_holder: '#holder_html_view_card',
@ -39,17 +41,25 @@ var IqShoppingCart = {
},
reInit: function (){
var _self = this;
$(_self.table).find(_self.btn_add).on('click', function(){
_self.add_product($(this))
});
[_self.table, _self.table_onetime].forEach(function(tableSel){
$(tableSel).find(_self.btn_add).off('click').on('click', function(){
_self.add_product($(this), tableSel)
});
$(_self.table).find(_self.btn_remove).on('click', function(){
_self.remove_product($(this))
});
$(_self.table).find(_self.table_input).off('change').on('change', function(){
_self.update_input_table($(this));
$(tableSel).find(_self.btn_remove).off('click').on('click', function(){
_self.remove_product($(this), tableSel)
});
$(tableSel).find(_self.table_input).off('change').on('change', function(){
_self.update_input_table($(this), tableSel);
});
});
},
isAddonElement: function (_obj){
return _obj.data('abo-addon') == 1;
},
qtyInputName: function (product_id, isAddon){
return (isAddon ? 'onetime_qty_' : 'product_qty_') + product_id;
},
showInit: function (){
var _self = this;
$(_self.btn_clear).on('click', function (){
@ -67,7 +77,7 @@ var IqShoppingCart = {
});
$(_self.remove_item).on('click', function(event){
event.preventDefault();
_self.update_cart_database($(this).data('product-id'), 0);
_self.update_cart_database($(this).data('product-id'), 0, _self.isAddonElement($(this)));
});
if(_self.is_for === 'me' || _self.is_for === 'abo-me'){
$('input[name^="'+_self.comp_products+'"]').on('change', function(){
@ -82,19 +92,19 @@ var IqShoppingCart = {
_self.performRequest({shipping_country_id: id, shipping_is_for: is_for, action: 'updateShippingCountry'})
.done(_self.refreshItemsAndView);
},
update_input_table: function (_obj){
update_input_table: function (_obj, tableSel){
var _self = this;
var qty = parseInt(_obj.val());
qty = _self.checkNumber(qty);
_obj.val(qty);
_self.update_cart(_obj.data('product-id'), qty);
_self.update_cart(_obj.data('product-id'), qty, _self.isAddonElement(_obj));
},
update_input_cart: function (_obj){
var _self = this;
var qty = parseInt(_obj.val());
qty = _self.checkNumber(qty);
_obj.val(qty);
_self.update_cart_database(_obj.data('product-id'), qty);
_self.update_cart_database(_obj.data('product-id'), qty, _self.isAddonElement(_obj));
},
/**
* Plus-/Minus-Buttons im Warenkorb: Menge nicht optimistisch erhöhen, sondern
@ -104,41 +114,46 @@ var IqShoppingCart = {
change_cart_qty: function (_obj, delta){
var _self = this;
var productId = _obj.data('product-id');
var input = $(_self.card_holder).find('.cart-input-event-onchange[data-product-id="'+productId+'"]');
var isAddon = _self.isAddonElement(_obj);
var input = $(_self.card_holder).find('.cart-input-event-onchange[data-product-id="'+productId+'"]').filter(function(){
return IqShoppingCart.isAddonElement($(this)) === isAddon;
});
var qty = _self.checkNumber(parseInt(input.val(), 10) + delta);
_self.update_cart_database(productId, qty);
_self.update_cart_database(productId, qty, isAddon);
},
update_comp_product: function (_obj){
var _self = this;
_self.performRequest({comp_product_id: _obj.val(), comp_num: _obj.data('comp_num'), count_comp_products: $('input[name="'+_self.count_comp_products+'"]').val(), action: 'updateCompProduct'})
.done(_self.refreshItemsAndView);
},
add_product: function (_obj){
add_product: function (_obj, tableSel){
var _self = this;
var input = $(_self.table).find('input[name="product_qty_'+_obj.data('product-id')+'"]');
var isAddon = _self.isAddonElement(_obj);
var input = $(tableSel || _self.table).find('input[name="'+_self.qtyInputName(_obj.data('product-id'), isAddon)+'"]');
var qty = parseInt(input.val()) + 1;
qty = _self.checkNumber(qty);
input.val(qty);
_self.update_cart(_obj.data('product-id'), qty);
_self.update_cart(_obj.data('product-id'), qty, isAddon);
},
remove_product: function (_obj){
remove_product: function (_obj, tableSel){
var _self = this;
var input = $(_self.table).find('input[name="product_qty_'+_obj.data('product-id')+'"]');
var isAddon = _self.isAddonElement(_obj);
var input = $(tableSel || _self.table).find('input[name="'+_self.qtyInputName(_obj.data('product-id'), isAddon)+'"]');
var qty = parseInt(input.val()) - 1;
if(qty < 0){
qty = 0;
}
input.val(qty);
_self.update_cart(_obj.data('product-id'), qty);
_self.update_cart(_obj.data('product-id'), qty, isAddon);
},
update_cart_database: function (product_id, qty){
update_cart_database: function (product_id, qty, isAddon){
var _self = this;
_self.performRequest({product_id: product_id, qty: qty, action: 'updateCart'})
_self.performRequest({product_id: product_id, qty: qty, abo_addon: isAddon ? 1 : 0, action: 'updateCart'})
.done(_self.refreshDatabaseAndView);
},
update_cart: function (product_id, qty){
update_cart: function (product_id, qty, isAddon){
var _self = this;
_self.performRequest({product_id: product_id, qty: qty, action: 'updateCart'})
_self.performRequest({product_id: product_id, qty: qty, abo_addon: isAddon ? 1 : 0, action: 'updateCart'})
.done(_self.refreshItemsAndView);
},
/**
@ -225,7 +240,9 @@ var IqShoppingCart = {
$(_self.card_holder).html(data.html_card);
$(_self.comp_holder).html(data.html_comp);
var input = $(_self.table).find('input[name="product_qty_'+data.data.product_id+'"]');
var isAddon = data.data && data.data.abo_addon == 1;
var tableSel = isAddon ? _self.table_onetime : _self.table;
var input = $(tableSel).find('input[name="'+_self.qtyInputName(data.data.product_id, isAddon)+'"]');
input.val(data.data.qty);
_self.showInit();
_self.updateAboIntervalWarning();