10.April 2026
This commit is contained in:
parent
a00c42e770
commit
f58c709945
208 changed files with 19280 additions and 2914 deletions
127
app/Services/SyS/AboOrdersOverview.php
Normal file
127
app/Services/SyS/AboOrdersOverview.php
Normal file
|
|
@ -0,0 +1,127 @@
|
|||
<?php
|
||||
|
||||
namespace App\Services\SyS;
|
||||
|
||||
use App\Models\ShoppingOrder;
|
||||
use App\Models\UserAboOrder;
|
||||
|
||||
class AboOrdersOverview
|
||||
{
|
||||
/**
|
||||
* Payone-/Shop-Zahlungsstatus: tatsächlich eingezogen.
|
||||
*
|
||||
* @var list<string>
|
||||
*/
|
||||
private const SUCCESS_TXACTIONS = ['paid', 'extern_paid', 'invoice_paid'];
|
||||
|
||||
public static function show()
|
||||
{
|
||||
$filter = request('filter', 'all');
|
||||
|
||||
$aboOrders = UserAboOrder::with([
|
||||
'user_abo',
|
||||
'user_abo.user',
|
||||
'user_abo.user.account',
|
||||
'shopping_order',
|
||||
'shopping_order.shopping_user',
|
||||
'shopping_order.shopping_payments',
|
||||
])
|
||||
->whereHas('shopping_order')
|
||||
->when($filter === 'berater', fn ($q) => $q->whereHas('user_abo', fn ($q) => $q->where('is_for', 'me')))
|
||||
->when($filter === 'kunde', fn ($q) => $q->whereHas('user_abo', fn ($q) => $q->where('is_for', '!=', 'me')))
|
||||
->orderByDesc('created_at')
|
||||
->get();
|
||||
|
||||
$summary = [
|
||||
'total_orders' => $aboOrders->count(),
|
||||
'total_diff' => 0.0,
|
||||
'affected_orders' => 0,
|
||||
];
|
||||
|
||||
$rows = [];
|
||||
|
||||
foreach ($aboOrders as $aboOrder) {
|
||||
$order = $aboOrder->shopping_order;
|
||||
if (! $order) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$subtotalWs = (float) $order->subtotal_ws;
|
||||
$totalShipping = (float) $order->total_shipping;
|
||||
$tax = (float) $order->tax;
|
||||
|
||||
$expectedCents = (int) round($totalShipping * 100);
|
||||
$actualCents = self::actualChargedCentsFromPayments($order);
|
||||
|
||||
$actualEur = $actualCents !== null ? round($actualCents / 100, 2) : null;
|
||||
$diff = ($actualCents !== null)
|
||||
? round(($expectedCents - $actualCents) / 100, 2)
|
||||
: null;
|
||||
|
||||
if ($diff !== null && abs($diff) <= 0.01) {
|
||||
$diff = 0;
|
||||
}
|
||||
|
||||
$payments = $order->shopping_payments;
|
||||
$paymentTxSummary = $payments->isEmpty()
|
||||
? null
|
||||
: $payments->pluck('txaction')->filter()->unique()->implode(', ');
|
||||
|
||||
$user = $aboOrder->user_abo->user ?? null;
|
||||
|
||||
$rows[] = [
|
||||
'abo_order_id' => $aboOrder->id,
|
||||
'abo_id' => $aboOrder->user_abo_id,
|
||||
'order_id' => '<a href='.route('admin_sales_customers_detail', [$aboOrder->shopping_order_id]).'>'.$aboOrder->shopping_order_id.'</a>',
|
||||
'user_id' => $user->id ?? null,
|
||||
'user_name' => $aboOrder->shopping_order->shopping_user ? ($aboOrder->shopping_order->shopping_user->billing_firstname ?? '').' '.($aboOrder->shopping_order->shopping_user->billing_lastname ?? '') : '-',
|
||||
'user_email' => $aboOrder->shopping_order->shopping_user ? $aboOrder->shopping_order->shopping_user->billing_email ?? '-' : '-',
|
||||
'is_for' => $aboOrder->user_abo->is_for ?? '-',
|
||||
'subtotal_ws' => $subtotalWs,
|
||||
'tax' => $tax,
|
||||
'total_shipping' => $totalShipping,
|
||||
'actual_charged_eur' => $actualEur,
|
||||
'payment_count' => $payments->count(),
|
||||
'payment_txactions' => $paymentTxSummary,
|
||||
'diff' => $diff,
|
||||
'status' => $aboOrder->status,
|
||||
'paid' => $aboOrder->paid,
|
||||
'txaction' => $order->txaction,
|
||||
'created_at' => $aboOrder->created_at,
|
||||
];
|
||||
|
||||
if ($diff !== null && abs($diff) >= 0.01) {
|
||||
$summary['total_diff'] += $diff;
|
||||
$summary['affected_orders']++;
|
||||
}
|
||||
}
|
||||
|
||||
$summary['total_diff'] = round($summary['total_diff'], 2);
|
||||
|
||||
return view('sys.tools.abo-orders-overview', [
|
||||
'rows' => $rows,
|
||||
'summary' => $summary,
|
||||
'filter' => $filter,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Summiert erfolgreiche Abbuchungen aus `shopping_payments` (Cent).
|
||||
* Kein Treffer bei erfolgreichen Status → null (kein belastbarer Eingang).
|
||||
*/
|
||||
public static function actualChargedCentsFromPayments(ShoppingOrder $order): ?int
|
||||
{
|
||||
$payments = $order->shopping_payments;
|
||||
if ($payments === null || $payments->isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$successful = $payments->filter(
|
||||
fn ($p) => in_array($p->txaction, self::SUCCESS_TXACTIONS, true)
|
||||
);
|
||||
|
||||
$sum = (int) $successful->sum('amount');
|
||||
|
||||
return $sum > 0 ? $sum : null;
|
||||
}
|
||||
}
|
||||
536
app/Services/SyS/PayoneCallbackTestbench.php
Normal file
536
app/Services/SyS/PayoneCallbackTestbench.php
Normal file
|
|
@ -0,0 +1,536 @@
|
|||
<?php
|
||||
|
||||
namespace App\Services\SyS;
|
||||
|
||||
use App\Console\Commands\UserMakeAboOrder;
|
||||
use App\Models\PaymentTransaction;
|
||||
use App\Models\ShippingCountry;
|
||||
use App\Models\ShoppingOrder;
|
||||
use App\Models\ShoppingPayment;
|
||||
use App\Models\ShoppingUser;
|
||||
use App\Models\UserAbo;
|
||||
use App\Models\UserAboOrder;
|
||||
use App\Models\UserShop;
|
||||
use App\Services\AboHelper;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Console\Command as ConsoleCommand;
|
||||
use Illuminate\Console\OutputStyle;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Redirect;
|
||||
use Illuminate\Support\Str;
|
||||
use ReflectionMethod;
|
||||
use Symfony\Component\Console\Input\ArrayInput;
|
||||
use Symfony\Component\Console\Output\NullOutput;
|
||||
use Throwable;
|
||||
|
||||
class PayoneCallbackTestbench
|
||||
{
|
||||
public static function show()
|
||||
{
|
||||
self::ensureAllowed();
|
||||
|
||||
return view('sys.tools.payone-callback-testbench', [
|
||||
'fixture' => session('payone_testbench_fixture'),
|
||||
'simulateResult' => session('payone_testbench_simulate'),
|
||||
'checkoutSuccess' => session('payone_testbench_checkout_success'),
|
||||
'userAboId' => session('payone_testbench_user_abo_id'),
|
||||
'cronRenewal' => session('payone_testbench_cron_renewal'),
|
||||
'cronRenewalOrderId' => session('payone_testbench_cron_renewal_order_id'),
|
||||
]);
|
||||
}
|
||||
|
||||
public static function store()
|
||||
{
|
||||
self::ensureAllowed();
|
||||
|
||||
$action = request('action');
|
||||
|
||||
if ($action === 'create_fixture') {
|
||||
return self::createFixture();
|
||||
}
|
||||
|
||||
if ($action === 'simulate_paid') {
|
||||
return self::simulatePaidCallback();
|
||||
}
|
||||
|
||||
if ($action === 'simulate_checkout_success') {
|
||||
return self::simulateCheckoutSuccess();
|
||||
}
|
||||
|
||||
if ($action === 'simulate_cron_renewal') {
|
||||
return self::simulateCronRenewal();
|
||||
}
|
||||
|
||||
if ($action === 'clear_fixture') {
|
||||
session()->forget([
|
||||
'payone_testbench_fixture',
|
||||
'payone_testbench_simulate',
|
||||
'payone_testbench_checkout_success',
|
||||
'payone_testbench_user_abo_id',
|
||||
'payone_testbench_cron_renewal',
|
||||
'payone_testbench_cron_renewal_order_id',
|
||||
]);
|
||||
|
||||
return Redirect::route('sysadmin_tool', ['payone_callback_testbench']);
|
||||
}
|
||||
|
||||
return Redirect::route('sysadmin_tool', ['payone_callback_testbench'])
|
||||
->with('error', 'Unbekannte Aktion.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Payload wie Payone ihn an die Status-URL sendet (Route: api.{domain}/payment/status).
|
||||
*
|
||||
* @return array<string, string>
|
||||
*/
|
||||
public static function buildPayoneCallbackPayload(ShoppingOrder $order, ShoppingPayment $payment, ?int $txid = null): array
|
||||
{
|
||||
$txid = $txid ?? random_int(100_000_000, 999_999_999);
|
||||
$price = number_format(round($payment->amount / 100, 2), 2, '.', '');
|
||||
|
||||
return [
|
||||
'key' => (string) config('payone.defaults.key'),
|
||||
'param' => (string) $order->id,
|
||||
'userid' => '999999999',
|
||||
'txid' => (string) $txid,
|
||||
'reference' => $payment->reference,
|
||||
'price' => $price,
|
||||
'txaction' => 'paid',
|
||||
'mode' => $payment->mode ?? 'test',
|
||||
'clearingtype' => $payment->clearingtype,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Vollständige URL der Payone-Server-zu-Server-Route (z. B. http://api.mivita.test/payment/status).
|
||||
*/
|
||||
public static function paymentStatusUrl(): string
|
||||
{
|
||||
return route('api.payment_status', [], true);
|
||||
}
|
||||
|
||||
private static function ensureAllowed(): void
|
||||
{
|
||||
if (app()->isProduction()) {
|
||||
abort(403, 'Payone-Testbench ist in Production deaktiviert.');
|
||||
}
|
||||
}
|
||||
|
||||
private static function createFixture()
|
||||
{
|
||||
$validated = request()->validate([
|
||||
'amount_eur' => ['required', 'numeric', 'min:0.01', 'max:99999.99'],
|
||||
'consultant_user_id' => ['required', 'integer', 'exists:users,id'],
|
||||
'is_abo' => ['sometimes', 'boolean'],
|
||||
'is_for_ot' => ['sometimes', 'boolean'],
|
||||
]);
|
||||
|
||||
$amountEur = round((float) $validated['amount_eur'], 2);
|
||||
$amountCents = (int) round($amountEur * 100);
|
||||
$isAbo = request()->boolean('is_abo');
|
||||
$isFor = request()->boolean('is_for_ot') ? 'ot' : 'me';
|
||||
$consultantId = (int) $validated['consultant_user_id'];
|
||||
|
||||
session()->forget(['payone_testbench_simulate', 'payone_testbench_checkout_success']);
|
||||
|
||||
$country = ShippingCountry::query()->first();
|
||||
if (! $country) {
|
||||
abort(500, 'Kein Eintrag in shipping_countries – bitte Stammdaten anlegen.');
|
||||
}
|
||||
|
||||
$userShop = UserShop::query()->first();
|
||||
if (! $userShop) {
|
||||
abort(500, 'Kein user_shops Eintrag – bitte Shop anlegen.');
|
||||
}
|
||||
|
||||
$fixture = DB::transaction(function () use ($amountEur, $amountCents, $isAbo, $isFor, $country, $userShop, $consultantId) {
|
||||
$email = 'payone-bench-'.Str::lower(Str::random(8)).'@example.test';
|
||||
|
||||
if ($isFor === 'me') {
|
||||
$shoppingUserAttrs = [
|
||||
'billing_firstname' => 'Bench',
|
||||
'billing_lastname' => 'Payone',
|
||||
'billing_email' => $email,
|
||||
'billing_country_id' => $country->id,
|
||||
'shipping_country_id' => $country->id,
|
||||
'is_for' => $isFor,
|
||||
'is_from' => 'user_order',
|
||||
'auth_user_id' => $consultantId,
|
||||
'member_id' => null,
|
||||
];
|
||||
$orderAttrs = [
|
||||
'auth_user_id' => $consultantId,
|
||||
'member_id' => null,
|
||||
];
|
||||
} else {
|
||||
$shoppingUserAttrs = [
|
||||
'billing_firstname' => 'Bench',
|
||||
'billing_lastname' => 'Payone',
|
||||
'billing_email' => $email,
|
||||
'billing_country_id' => $country->id,
|
||||
'shipping_country_id' => $country->id,
|
||||
'is_for' => $isFor,
|
||||
'is_from' => 'user_order',
|
||||
'auth_user_id' => null,
|
||||
'member_id' => $consultantId,
|
||||
];
|
||||
$orderAttrs = [
|
||||
'auth_user_id' => null,
|
||||
'member_id' => $consultantId,
|
||||
];
|
||||
}
|
||||
|
||||
$shoppingUser = ShoppingUser::create($shoppingUserAttrs);
|
||||
|
||||
$order = ShoppingOrder::create(array_merge([
|
||||
'shopping_user_id' => $shoppingUser->id,
|
||||
'country_id' => $country->id,
|
||||
'language' => app()->getLocale(),
|
||||
'user_shop_id' => $userShop->id,
|
||||
'payment_for' => $shoppingUser->getOrderPaymentFor(),
|
||||
'total' => $amountEur,
|
||||
'subtotal' => $amountEur,
|
||||
'shipping' => 0,
|
||||
'shipping_net' => 0,
|
||||
'subtotal_ws' => $amountEur,
|
||||
'tax' => 0,
|
||||
'total_shipping' => $amountEur,
|
||||
'points' => 0,
|
||||
'weight' => 0,
|
||||
'paid' => false,
|
||||
'is_abo' => $isAbo,
|
||||
'abo_interval' => $isAbo ? 30 : 0,
|
||||
'txaction' => 'prev',
|
||||
'mode' => 'test',
|
||||
], $orderAttrs));
|
||||
|
||||
$reference = self::generatePaymentReference();
|
||||
|
||||
$payment = ShoppingPayment::create([
|
||||
'shopping_order_id' => $order->id,
|
||||
'clearingtype' => 'wlt',
|
||||
'wallettype' => 'PPE',
|
||||
'onlinebanktransfertype' => '',
|
||||
'reference' => $reference,
|
||||
'amount' => $amountCents,
|
||||
'currency' => 'EUR',
|
||||
'txaction' => null,
|
||||
'mode' => 'test',
|
||||
'is_abo' => $isAbo,
|
||||
'abo_interval' => $isAbo ? ($order->abo_interval ?? 0) : 0,
|
||||
]);
|
||||
|
||||
return [
|
||||
'shopping_order_id' => $order->id,
|
||||
'shopping_payment_id' => $payment->id,
|
||||
'reference' => $reference,
|
||||
'amount_eur' => $amountEur,
|
||||
'amount_cents' => $amountCents,
|
||||
'is_abo' => $isAbo,
|
||||
'is_for' => $isFor,
|
||||
'consultant_user_id' => $consultantId,
|
||||
'assignment_note' => $isFor === 'me'
|
||||
? 'Berater: auth_user_id = Berater-ID, member_id leer'
|
||||
: 'Kunde: auth_user_id leer, member_id = Berater-ID (Zuordnung / Provision)',
|
||||
'api_url' => self::paymentStatusUrl(),
|
||||
'curl' => self::buildCurlExample(self::paymentStatusUrl(), self::buildPayoneCallbackPayload($order->fresh(), $payment->fresh())),
|
||||
];
|
||||
});
|
||||
|
||||
session()->put('payone_testbench_fixture', $fixture);
|
||||
|
||||
return Redirect::route('sysadmin_tool', ['payone_callback_testbench']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Entspricht dem Browser-Erfolg nach Zahlung: {@see CheckoutController::handleSuccessfulTransaction}
|
||||
* und {@see CheckoutController::transactionApproved} → {@see AboHelper::createNewAbo}.
|
||||
*/
|
||||
private static function simulateCheckoutSuccess()
|
||||
{
|
||||
$validated = request()->validate([
|
||||
'shopping_order_id' => ['required', 'integer', 'exists:shopping_orders,id'],
|
||||
]);
|
||||
|
||||
$order = ShoppingOrder::query()
|
||||
->with(['shopping_order_items', 'shopping_user', 'shopping_payments'])
|
||||
->findOrFail($validated['shopping_order_id']);
|
||||
|
||||
$payment = $order->shopping_payments->last();
|
||||
|
||||
if (! $payment) {
|
||||
return Redirect::route('sysadmin_tool', ['payone_callback_testbench'])
|
||||
->with('error', 'Keine ShoppingPayment zu dieser Bestellung.');
|
||||
}
|
||||
|
||||
if (! $order->is_abo || (int) $order->abo_interval <= 0) {
|
||||
return Redirect::route('sysadmin_tool', ['payone_callback_testbench'])
|
||||
->with('error', 'Bestellung ist kein Abo (is_abo / abo_interval).');
|
||||
}
|
||||
|
||||
if (UserAboOrder::query()->where('shopping_order_id', $order->id)->exists()) {
|
||||
return Redirect::route('sysadmin_tool', ['payone_callback_testbench'])
|
||||
->with('error', 'Für diese Bestellung existiert bereits ein UserAboOrder – Abo wurde bereits angelegt.');
|
||||
}
|
||||
|
||||
$payment->load('payment_transactions');
|
||||
|
||||
if ($payment->payment_transactions->isEmpty()) {
|
||||
PaymentTransaction::create([
|
||||
'shopping_payment_id' => $payment->id,
|
||||
'request' => 'transaction',
|
||||
'txid' => random_int(1, 999_999_999),
|
||||
'userid' => 999_999_999,
|
||||
'status' => 'PAYONE',
|
||||
'key' => (string) config('payone.defaults.key'),
|
||||
'txaction' => 'paid',
|
||||
'transmitted_data' => [],
|
||||
'mode' => $payment->mode ?? 'test',
|
||||
]);
|
||||
}
|
||||
|
||||
try {
|
||||
AboHelper::createNewAbo($payment->fresh([
|
||||
'shopping_order.shopping_user',
|
||||
'shopping_order.shopping_order_items',
|
||||
'payment_transactions',
|
||||
]));
|
||||
} catch (Throwable $e) {
|
||||
return Redirect::route('sysadmin_tool', ['payone_callback_testbench'])
|
||||
->with('error', 'createNewAbo: '.$e->getMessage());
|
||||
}
|
||||
|
||||
$userAboOrder = UserAboOrder::query()
|
||||
->where('shopping_order_id', $order->id)
|
||||
->with('user_abo')
|
||||
->first();
|
||||
|
||||
session()->put('payone_testbench_checkout_success', [
|
||||
'user_abo_id' => $userAboOrder?->user_abo_id,
|
||||
'user_abo_order_id' => $userAboOrder?->id,
|
||||
'shopping_order_id' => $order->id,
|
||||
'order_paid_after' => (bool) $order->fresh()->paid,
|
||||
'hint' => 'Erstbestellung: Abo-Stammdaten wie nach Checkout-Redirect. Die Bestätigung (paid, setAboActive, Incentive) folgt im nächsten Schritt über die Payone-API.',
|
||||
]);
|
||||
|
||||
if ($userAboOrder?->user_abo_id) {
|
||||
session()->put('payone_testbench_user_abo_id', $userAboOrder->user_abo_id);
|
||||
}
|
||||
|
||||
return Redirect::route('sysadmin_tool', ['payone_callback_testbench']);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, string>
|
||||
*/
|
||||
private static function buildCurlExample(string $url, array $payload): string
|
||||
{
|
||||
$parts = [];
|
||||
foreach ($payload as $k => $v) {
|
||||
$parts[] = escapeshellarg($k).'='.escapeshellarg((string) $v);
|
||||
}
|
||||
|
||||
return 'curl -X POST '.escapeshellarg($url).' -d '.implode(' -d ', $parts);
|
||||
}
|
||||
|
||||
private static function generatePaymentReference(): string
|
||||
{
|
||||
return substr(str_replace('-', '', (string) Str::uuid()), 0, 16);
|
||||
}
|
||||
|
||||
private static function simulatePaidCallback()
|
||||
{
|
||||
$validated = request()->validate([
|
||||
'shopping_order_id' => ['required', 'integer', 'exists:shopping_orders,id'],
|
||||
]);
|
||||
|
||||
$order = ShoppingOrder::query()->with('shopping_payments')->findOrFail($validated['shopping_order_id']);
|
||||
$payment = $order->shopping_payments->last();
|
||||
|
||||
if (! $payment) {
|
||||
return Redirect::route('sysadmin_tool', ['payone_callback_testbench'])
|
||||
->with('error', 'Keine ShoppingPayment zu dieser Bestellung.');
|
||||
}
|
||||
|
||||
if ($order->is_abo && (int) $order->abo_interval > 0) {
|
||||
if (! UserAboOrder::query()->where('shopping_order_id', $order->id)->exists()) {
|
||||
return Redirect::route('sysadmin_tool', ['payone_callback_testbench'])
|
||||
->with('error', 'Bei einer Abo-Erstbestellung zuerst Schritt 2 (Checkout: createNewAbo) ausführen, danach erst die Payone-API (paid). So existiert ein UserAboOrder für setAboActive und trackAboActivated.');
|
||||
}
|
||||
}
|
||||
|
||||
$payload = self::buildPayoneCallbackPayload($order, $payment);
|
||||
|
||||
$request = Request::create(self::paymentStatusUrl(), 'POST', $payload);
|
||||
$response = app()->handle($request);
|
||||
|
||||
$order->refresh();
|
||||
|
||||
$result = [
|
||||
'http_status' => $response->getStatusCode(),
|
||||
'body' => $response->getContent(),
|
||||
'order_paid' => (bool) $order->paid,
|
||||
'order_txaction' => $order->txaction,
|
||||
'payload' => $payload,
|
||||
'hint' => 'Entspricht Payment::paymentStatusPaidAction: Abo bestätigen (setAboActive), ggf. Incentive trackAboActivated, Rechnung …',
|
||||
];
|
||||
|
||||
session()->put('payone_testbench_simulate', $result);
|
||||
|
||||
$userAbo = $order->getUserAbo();
|
||||
if ($userAbo) {
|
||||
session()->put('payone_testbench_user_abo_id', $userAbo->id);
|
||||
}
|
||||
|
||||
return Redirect::route('sysadmin_tool', ['payone_callback_testbench']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Wie {@see UserMakeAboOrder::checkAbosToOrder}: next_date = heute und keine doppelte Verarbeitung am selben Tag.
|
||||
* Nur fuer Testbench (nicht Production).
|
||||
*/
|
||||
public static function prepareUserAboForCronRun(UserAbo $userAbo): void
|
||||
{
|
||||
self::ensureAllowed();
|
||||
|
||||
$today = Carbon::today()->format('Y-m-d');
|
||||
|
||||
DB::transaction(function () use ($userAbo, $today) {
|
||||
UserAboOrder::query()
|
||||
->where('user_abo_id', $userAbo->id)
|
||||
->whereDate('created_at', $today)
|
||||
->delete();
|
||||
|
||||
$userAbo->update(['next_date' => $today]);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Einmaliger Cron-Lauf: {@see UserMakeAboOrder::makeOrder} (Bestellung + Payone-Zahlung).
|
||||
* Danach ggf. Schritt 5: Payone-API (paid) fuer die Verlaengerungs-Bestellung.
|
||||
*/
|
||||
private static function simulateCronRenewal()
|
||||
{
|
||||
self::ensureAllowed();
|
||||
|
||||
$validated = request()->validate([
|
||||
'user_abo_id' => ['nullable', 'integer', 'exists:user_abos,id'],
|
||||
]);
|
||||
|
||||
$userAboId = $validated['user_abo_id'] ?? session('payone_testbench_user_abo_id');
|
||||
if (! $userAboId) {
|
||||
return Redirect::route('sysadmin_tool', ['payone_callback_testbench'])
|
||||
->with('error', 'Kein user_abo_id – zuerst Abo-Erstkauf (Schritte 2–3) abschließen oder ID eintragen.');
|
||||
}
|
||||
|
||||
$userAbo = UserAbo::query()
|
||||
->with(['user_abo_items', 'shopping_user'])
|
||||
->findOrFail($userAboId);
|
||||
|
||||
AboHelper::ensureUserAboItemsFromLatestOrder($userAbo);
|
||||
$userAbo->refresh();
|
||||
$userAbo->load(['user_abo_items', 'shopping_user']);
|
||||
|
||||
if (! $userAbo->active || (int) $userAbo->status !== 2) {
|
||||
return Redirect::route('sysadmin_tool', ['payone_callback_testbench'])
|
||||
->with('error', 'UserAbo nicht aktiv oder nicht status 2 (abo_okay).');
|
||||
}
|
||||
if ($userAbo->user_abo_items->isEmpty()) {
|
||||
return Redirect::route('sysadmin_tool', ['payone_callback_testbench'])
|
||||
->with('error', 'Keine Abo-Artikel – Verlängerung nicht möglich.');
|
||||
}
|
||||
|
||||
self::prepareUserAboForCronRun($userAbo);
|
||||
$userAbo->refresh();
|
||||
|
||||
$command = new UserMakeAboOrder;
|
||||
self::bindNullConsoleOutput($command);
|
||||
|
||||
$makeOrder = new ReflectionMethod(UserMakeAboOrder::class, 'makeOrder');
|
||||
$makeOrder->setAccessible(true);
|
||||
|
||||
try {
|
||||
/** @var ShoppingOrder|null $shoppingOrder */
|
||||
$shoppingOrder = $makeOrder->invoke($command, $userAbo->fresh(['user_abo_items', 'shopping_user']));
|
||||
} catch (Throwable $e) {
|
||||
return Redirect::route('sysadmin_tool', ['payone_callback_testbench'])
|
||||
->with('error', 'Cron makeOrder: '.$e->getMessage());
|
||||
}
|
||||
|
||||
if (! $shoppingOrder) {
|
||||
session()->put('payone_testbench_cron_renewal', [
|
||||
'success' => false,
|
||||
'message' => 'makeOrder hat keine Bestellung zurückgegeben (typisch: makeShoppingOrder false, z. B. fehlende Referenz-Bestellung/user_shop_id, oder createShoppingUser liefert nichts).',
|
||||
'diagnosis' => self::cronRenewalDiagnosis($userAbo->fresh(['user_abo_items', 'shopping_user'])),
|
||||
]);
|
||||
session()->forget('payone_testbench_cron_renewal_order_id');
|
||||
|
||||
return Redirect::route('sysadmin_tool', ['payone_callback_testbench']);
|
||||
}
|
||||
|
||||
session()->put('payone_testbench_cron_renewal', [
|
||||
'success' => true,
|
||||
'shopping_order_id' => $shoppingOrder->id,
|
||||
'user_abo_id' => $userAbo->id,
|
||||
'hint' => 'Entspricht user:make_abo_order / makeOrder. Für Rechnung und Incentive-Umsatz wie in Produktion: Schritt 5 (Payone paid) für diese Verlängerungs-Bestellung ausführen.',
|
||||
]);
|
||||
session()->put('payone_testbench_cron_renewal_order_id', $shoppingOrder->id);
|
||||
|
||||
return Redirect::route('sysadmin_tool', ['payone_callback_testbench']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Ohne Log-Datei: Ursachen fuer fehlgeschlagenes makeOrder eingrenzen.
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
private static function cronRenewalDiagnosis(UserAbo $userAbo): array
|
||||
{
|
||||
$out = [
|
||||
'user_abo_items' => $userAbo->user_abo_items->count(),
|
||||
];
|
||||
|
||||
$su = $userAbo->shopping_user;
|
||||
if (! $su) {
|
||||
$out['shopping_user'] = 'fehlt (UserAbo.shopping_user_id)';
|
||||
|
||||
return $out;
|
||||
}
|
||||
|
||||
$out['shopping_user_id'] = $su->id;
|
||||
$out['shopping_orders_count'] = $su->shopping_orders()->count();
|
||||
$ref = $su->shopping_orders()->orderByDesc('id')->first();
|
||||
$out['reference_order_id'] = $ref?->id;
|
||||
$out['reference_user_shop_id'] = $ref?->user_shop_id;
|
||||
|
||||
if ($ref && $ref->user_shop_id) {
|
||||
$shop = UserShop::withTrashed()->find($ref->user_shop_id);
|
||||
$out['user_shop_row_exists'] = $shop !== null;
|
||||
$out['user_shop_row_trashed'] = $shop !== null && $shop->trashed();
|
||||
$out['user_shop_relation_loaded'] = $ref->user_shop !== null;
|
||||
}
|
||||
|
||||
return $out;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ohne Artisan-Kontext ist $command->output null – info()/error() wuerden crashen.
|
||||
*/
|
||||
private static function bindNullConsoleOutput(ConsoleCommand $command): void
|
||||
{
|
||||
$command->setLaravel(app());
|
||||
|
||||
$input = new ArrayInput([]);
|
||||
$nullOutput = new NullOutput;
|
||||
$outputStyle = app()->make(OutputStyle::class, [
|
||||
'input' => $input,
|
||||
'output' => $nullOutput,
|
||||
]);
|
||||
|
||||
$reflection = new \ReflectionClass($command);
|
||||
$property = $reflection->getProperty('output');
|
||||
$property->setAccessible(true);
|
||||
$property->setValue($command, $outputStyle);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue