*/ /** * Anlaeufe fuer die Paid-Transaktion — wiederholt nur bei Deadlock / * Lock-Timeout (Ticket C2). */ private const PAID_TRANSACTION_ATTEMPTS = 3; private const TXACTION_PRIORITY = [ 'appointed' => 1, 'pending' => 2, 'failed' => 3, 'paid' => 10, ]; /** * Processes a PAYONE IPN/TSOK callback. Always returns a result whose `response` * is sent verbatim to PAYONE. A thrown exception (paid-path transaction failure) * propagates so PAYONE retries the callback. * * @param array $data */ public function handle(array $data): PayoneWebhookResult { if (! isset($data['key'], $data['param'], $data['userid'], $data['txid'], $data['reference'], $data['price'], $data['txaction'])) { MyLog::writeLog( 'payone', 'error', 'Error:2001 App\Http\Controllers\Api\PayoneController::paymentStatus parameter incomplete', $data ); return PayoneWebhookResult::rejected('2001'); } if ($data['key'] != config('payone.defaults.key')) { MyLog::writeLog( 'payone', 'error', 'Error:2002 App\Http\Controllers\Api\PayoneController::paymentStatus Key error', $data ); return PayoneWebhookResult::rejected('2002'); } $shopping_order = ShoppingOrder::find($data['param']); if (! $shopping_order) { MyLog::writeLog( 'payone', 'error', 'Error:2003 App\Http\Controllers\Api\PayoneController::paymentStatus ShoppingOrder not found:', $data ); return PayoneWebhookResult::rejected('2003'); } $shopping_payment = ShoppingPayment::where('reference', $data['reference'])->first(); if (! $shopping_payment) { MyLog::writeLog( 'payone', 'error', 'Error:2004 App\Http\Controllers\Api\PayoneController::paymentStatus ShoppingPayment not found', $data ); return PayoneWebhookResult::rejected('2004'); } if ($shopping_payment->shopping_order_id != $shopping_order->id) { MyLog::writeLog( 'payone', 'error', 'Error:2005 App\Http\Controllers\Api\PayoneController::paymentStatus ShoppingPayment no realation ShoppingOrder', $data ); return PayoneWebhookResult::rejected('2005'); } $price = number_format((round($data['price'], 2) * 100), 0, '.', ''); $price_amount = number_format($shopping_payment->amount, 0, '.', ''); if ($price_amount != $price) { $data['shopping_payment-amount'] = $price_amount; $data['price-amount'] = $price; MyLog::writeLog( 'payone', 'error', 'Error:2006 App\Http\Controllers\Api\PayoneController::paymentStatus Price error', $data ); return PayoneWebhookResult::rejected('2006'); } /* * Payone sendet dieselbe txaction oft mehrfach (v. a. "appointed"). War der Status * bereits auf ShoppingPayment gespeichert, ist das ein Duplikat: TSOK, keine Doppel-Verarbeitung. * Ausnahme: erneutes "paid", obwohl die Bestellung noch nicht als bezahlt gefuehrt wird (Recovery). */ if ($shopping_payment->txaction == $data['txaction']) { if ($data['txaction'] === 'paid' && $shopping_order->txaction === 'paid') { MyLog::writeLog( 'payone', 'notice', 'App\Http\Controllers\Api\PayoneController::paymentStatus duplicate callback ignored (already paid)', $data, false ); return PayoneWebhookResult::duplicateIgnored($data['txaction']); } if (in_array($data['txaction'], ['appointed', 'failed', 'pending'], true)) { MyLog::writeLog( 'payone', 'info', 'App\Http\Controllers\Api\PayoneController::paymentStatus duplicate callback ignored (same txaction)', [ 'reference' => $data['reference'] ?? null, 'param' => $data['param'] ?? null, 'txaction' => $data['txaction'], 'txid' => $data['txid'] ?? null, ], false ); return PayoneWebhookResult::duplicateIgnored($data['txaction']); } } // create transaction (centralised in the recorder, incl. PCI redaction + utf8ize) PayoneTransactionRecorder::recordWebhookCallback($shopping_payment, $data); $current_priority = self::TXACTION_PRIORITY[$shopping_order->txaction] ?? 0; $new_priority = self::TXACTION_PRIORITY[$data['txaction']] ?? 0; // Only update txaction if new priority is higher than current if ($new_priority > $current_priority) { $shopping_order->txaction = $data['txaction']; $shopping_order->save(); $shopping_payment->txaction = $data['txaction']; $shopping_payment->save(); } else { MyLog::writeLog( 'payone', 'info', 'App\Http\Controllers\Api\PayoneController::paymentStatus - txaction not updated (current: '.$shopping_order->txaction.' has higher/equal priority than new: '.$data['txaction'].')', $data, false ); } $send_link = false; $send_mail = true; if ($data['txaction'] === 'failed') { $shopping_order->setUserHistoryValue(['status' => 6]); Util::setInstanceStatusByPayment($shopping_payment, 5); } if ($data['txaction'] === 'appointed') { $shopping_order->setUserHistoryValue(['status' => 7]); ShoppingUserService::snycOrdersByShoppingOrder($shopping_order); Util::setInstanceStatusByPayment($shopping_payment, 4); } if ($data['txaction'] === 'paid') { // Transaktion mit Zeilensperre auf der Bestellung gegen doppelte // Verarbeitung; bis zu 3 Anlaeufe, weil die Punkte-Buchung den Berater // sperrt und zwei parallele Webhooks desselben Beraters sich in einem // Deadlock treffen koennen (Ticket C2) — der Verlierer wiederholt. try { [$send_link, $send_mail] = \DB::transaction(function () use ($shopping_order, $shopping_payment, $send_link, $send_mail) { $locked_order = ShoppingOrder::where('id', $shopping_order->id) ->lockForUpdate() ->first(); if ($locked_order->paid) { return [$send_link, false]; } return [Payment::paymentStatusPaidAction($locked_order, true, $shopping_payment), $send_mail]; }, self::PAID_TRANSACTION_ATTEMPTS); } catch (\Exception $e) { MyLog::writeLog( 'payone', 'error', 'Error:2008 App\Http\Controllers\Api\PayoneController::paymentStatus Transaction failed', ['error' => $e->getMessage(), 'data' => $data] ); throw $e; } CheckoutFunnelTracker::confirmedPayment( shoppingPaymentId: $shopping_payment->id, txaction: 'paid', metadata: ['mode' => $data['mode'] ?? null, 'txid' => $data['txid'] ?? null], ); } if ($data['txaction'] === 'appointed') { CheckoutFunnelTracker::confirmedPayment( shoppingPaymentId: $shopping_payment->id, txaction: 'appointed', metadata: ['mode' => $data['mode'] ?? null], ); } $data['send_link'] = $send_link; if ($send_mail) { Payment::paymentStatusSendMail($shopping_order, $shopping_payment, $data); } return PayoneWebhookResult::processed( txaction: $data['txaction'], paid: $data['txaction'] === 'paid', mailSent: $send_mail, sendLink: (bool) $send_link, ); } }