mivita/app/Services/PaymentHelper.php
Kevin ccc6f85809 Payment Phase R: PAYONE-Integration entflechten (R1–R7 + Review)
Strangler-Refactoring der PAYONE-Zahlungslogik in eine testbare
Service-Schicht, ohne Verhalten in Checkout/Cron/IPN zu ändern.

Neue Services (app/Services/Payment/Payone):
- PayonePaymentService (+ PayonePaymentResult-DTO) für CC-/SEPA-Check
  und Abo-Abbuchung
- PayoneRequestBuilder, PaymentMethodResolver, PayoneMethodConfig
- PayoneTransactionRecorder: zentrale PaymentTransaction-Erstellung
  (API-Response, Invoice, Webhook) inkl. PCI-Redaction (cardpan/cardcvc2)
- PayoneWebhookHandler (+ PayoneWebhookResult): IPN/TSOK-Logik aus
  Api\PayoneController; response('TSOK') statt echo/exit
- PayoneTransportException + Handler-Mapping (403) statt abort() im Service

Hotfixes (R1): var_dump/exit & bare exit() entfernt, Log-Redaction.

Security (R-Review):
- PAYONE_KEY_HASH (MD5-Hash, kein Klartext) mit Boot-Hard-Fail; schließt
  zugleich die ''-Webhook-Bypass-Lücke. Artisan-Command payone:hash-key
  (verdeckte Eingabe, optional --write) zur Hash-Erzeugung.
- Webhook: txaction Pflichtfeld, mode-Fallback auf payment->mode.

Pay\PayoneController ist @deprecated-Fassade (614 → 328 Zeilen),
delegiert an Services via Container-Singleton.

Migration: fehlende Spalte shopping_payments.carddata nachgezogen.

Tests: 47 grün (PayonePaymentService, Recorder, Webhook, Resolver,
TransportException, hash-key). End-to-End im Test-System verifiziert
(siehe dev/2026-06-09-payments).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-10 13:42:45 +00:00

141 lines
7.4 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,
];
$shopping_order_item = ShoppingOrderItem::create($data);
}
}
$shopping_order->makeTaxSplit();
return $shopping_order;
}
}