mivita/app/Services/PaymentHelper.php
Kevin cdb121fde0 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>
2026-07-21 14:36:40 +00:00

142 lines
7.5 KiB
PHP

<?php
namespace App\Services;
use App\Http\Controllers\Pay\PayoneController;
use App\Models\ShoppingOrder;
use App\Models\ShoppingOrderItem;
use App\Models\ShoppingUser;
use App\Models\UserHistory;
use App\Services\Payment\Payone\PayoneTransactionRecorder;
use Yard;
class PaymentHelper
{
public function setProduct($product)
{
Yard::instance('shopping')->destroy();
Yard::instance('shopping')->add($product->id, $product->getLang('name'), 1, $product->price, false, false, ['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]);
}
public function initELVPayment($user)
{
$shopping_user = $this->makeShoppingUser($user);
$shopping_order = $this->makeShoppingOrder($user, $shopping_user);
$pay = new PayoneController;
$pay->init($shopping_user, $shopping_order);
$amount = Yard::instance('shopping')->totalWithShipping(2, '.', '') * 100;
$payment_method = 'elv';
$ret['elv']['mandate_identification'] = isset($user->account->payment_data['mandate_identification']) ? $user->account->payment_data['mandate_identification'] : '';
$ret['elv']['creditor_identifier'] = isset($user->account->payment_data['creditor_identifier']) ? $user->account->payment_data['creditor_identifier'] : '';
$ret['elv']['iban'] = isset($user->account->payment_data['iban']) ? $user->account->payment_data['iban'] : '';
$ret['elv']['bic'] = isset($user->account->payment_data['bic']) ? $user->account->payment_data['bic'] : '';
$ret['elv']['bankaccountholder'] = isset($user->account->payment_data['bankaccountholder']) ? $user->account->payment_data['bankaccountholder'] : '';
$reference = $pay->setPrePayment($payment_method, $amount, 'EUR', $ret);
$pay->setPersonalData();
$response = $pay->onlyPaymentResponse();
$shopping_payment = $pay->getShoppingPayment();
if (in_array($response['status'] ?? '', ['ERROR', 'REDIRECT', 'APPROVED'], true)) {
$statusMap = ['ERROR' => 3, 'REDIRECT' => 4, 'APPROVED' => 5];
$payT = PayoneTransactionRecorder::recordFromResponse($shopping_payment, 'authorization', $response);
UserHistory::create(['user_id' => $user->id, 'shopping_order_id' => $shopping_order->id, 'action' => 'abo_open_payment', 'referenz' => $payT->id, 'identifier' => $user->payment_account, 'status' => $statusMap[$response['status']]]);
}
}
public function makeShoppingUser($user, $is_from = 'membership', $is_for = 'me')
{
$shopping_user = new ShoppingUser;
$shopping_user->auth_user_id = $user->id;
$shopping_user->mode = 'prev';
$shopping_user->language = $user->getLocale();
$shopping_user->billing_salutation = $user->account->salutation;
$shopping_user->billing_company = $user->account->company;
$shopping_user->billing_firstname = $user->account->first_name;
$shopping_user->billing_lastname = $user->account->last_name;
$shopping_user->billing_address = $user->account->address;
$shopping_user->billing_address_2 = $user->account->address_2;
$shopping_user->billing_zipcode = $user->account->zipcode;
$shopping_user->billing_city = $user->account->city;
$shopping_user->billing_country_id = $user->account->country_id;
$shopping_user->billing_phone = $user->account->phone;
$shopping_user->billing_email = $user->email;
$shopping_user->language = $user->getLocale();
$shopping_user->faker_mail = false;
$shopping_user->shipping_email = $user->email;
$shopping_user->accepted_data_checkbox = 1;
$shopping_user->is_for = $is_for;
$shopping_user->is_from = $is_from;
$shopping_user->homeparty_id = null;
$shopping_user->same_as_billing = $user->account->same_as_billing ? false : true;
$shopping_user->shipping_salutation = $user->account->shipping_salutation;
$shopping_user->shipping_company = $user->account->shipping_company;
$shopping_user->shipping_firstname = $user->account->shipping_firstname;
$shopping_user->shipping_lastname = $user->account->shipping_lastname;
$shopping_user->shipping_address = $user->account->shipping_address;
$shopping_user->shipping_address_2 = $user->account->shipping_address_2;
$shopping_user->shipping_zipcode = $user->account->shipping_zipcode;
$shopping_user->shipping_city = $user->account->shipping_city;
$shopping_user->shipping_country_id = $user->account->shipping_country_id;
$shopping_user->shipping_phone = $user->account->shipping_phone;
$shopping_user->shipping_postnumber = $user->account->shipping_postnumber;
$shopping_user->save();
return $shopping_user;
}
public function makeShoppingOrder($user, $shopping_user)
{
$data = [
'shopping_user_id' => $shopping_user->id,
'auth_user_id' => $shopping_user->auth_user_id,
'country_id' => Yard::instance('shopping')->getShippingCountryId(),
'language' => \App::getLocale(),
'user_shop_id' => 1,
'payment_for' => $shopping_user->getOrderPaymentFor(),
'total' => Yard::instance('shopping')->total(2, '.', ''),
'subtotal' => Yard::instance('shopping')->subtotal(2, '.', ''),
'shipping' => Yard::instance('shopping')->shipping(2, '.', ','),
'shipping_net' => Yard::instance('shopping')->shippingNet(2, '.', ''),
'subtotal_ws' => Yard::instance('shopping')->subtotalWithShipping(2, '.', ''),
'tax' => Yard::instance('shopping')->taxWithShipping(2, '.', ''),
'total_shipping' => Yard::instance('shopping')->totalWithShipping(2, '.', ''),
'points' => Yard::instance('shopping')->points(),
'weight' => Yard::instance('shopping')->weight(),
'txaction' => 'prev',
'mode' => $user->test_mode ? 'test' : 'live',
];
$shopping_order = ShoppingOrder::create($data);
$items = Yard::instance('shopping')->getContentByOrder();
foreach ($items as $item) {
if (! ShoppingOrderItem::where('shopping_order_id', $shopping_order->id)->where('row_id', $item->rowId)->count()) {
$price_net = Yard::instance('shopping')->rowPriceNet($item, 2, '.', '');
$tax = $item->price - $price_net;
$data = [
'shopping_order_id' => $shopping_order->id,
'row_id' => $item->rowId,
'product_id' => $item->id,
'comp' => $item->options->comp,
'qty' => $item->qty,
'price' => $item->price,
'price_net' => $price_net,
'tax_rate' => $item->taxRate,
'tax' => $tax,
'price_vk_net' => $shopping_order->getPriceVkNetBy($item->id),
'discount' => $item->options->no_commission ? 0 : $shopping_order->getUserDiscount(),
'points' => $item->options->points,
'slug' => $item->options->slug,
'is_abo_addon' => (bool) ($item->options->abo_addon ?? false),
];
$shopping_order_item = ShoppingOrderItem::create($data);
}
}
$shopping_order->makeTaxSplit();
return $shopping_order;
}
}