mivita/app/Services/AboRetryPaymentService.php
Kevin 7daebf32eb Logs: Kanal userabo ersetzt, Log-Mails ueber die Queue, weniger Rauschen
Log-Analyse 26.08. (P1-1, P2-1, P2-7, P2-8):
- MyLog schrieb in den nicht definierten Kanal userabo (109 EMERGENCY-
  Eintraege, laravel.log rotierte nicht mehr); jetzt abo_order.
- MyLog verschickt die Benachrichtigung ueber die Queue, ein Mailfehler
  bricht den Zahllauf nicht mehr ab. MailLog sammelt Nutzer, User-Agent
  und Zeit beim Erzeugen ein, weil der Worker keinen Request hat.
- Abgelehnte OAuth-Anfragen (falscher grant_type) werden als Warnung
  geloggt statt als Fehler gemailt.
- payments:check-accounts loggt je Berater nur noch auf debug und schreibt
  am Ende eine Zusammenfassung (geprueft, versendet, uebersprungen).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-23 15:41:06 +00:00

251 lines
8.2 KiB
PHP

<?php
namespace App\Services;
use App\Cron\UserMakeOrder;
use App\Models\UserAbo;
use App\Models\UserAboOrder;
use App\Services\Abo\AboStatusExplainer;
use App\Services\Incentive\IncentiveTracker;
use App\Services\Payment\PaymentProviderManager;
use Illuminate\Support\Facades\DB;
class AboRetryPaymentService
{
/**
* @return array{success: bool, message: string, order_id?: int}
*/
public function retry(UserAbo $userAbo): array
{
if (app(PaymentProviderManager::class)->aboPaymentsFrozen()) {
return [
'success' => false,
'message' => __('payment.abo_payments_frozen'),
];
}
if ($userAbo->status !== 3) {
return [
'success' => false,
'message' => __('abo.retry_only_hold'),
];
}
if (! $userAbo->active) {
return [
'success' => false,
'message' => __('abo.retry_only_active'),
];
}
if ($this->alreadyPaidToday($userAbo)) {
return [
'success' => false,
'message' => __('abo.retry_already_paid_today'),
];
}
\Log::channel('abo_order')->info('AboRetryPaymentService: Starte erneuten Zahlungsversuch', [
'abo_id' => $userAbo->id,
'email' => $userAbo->email,
'payone_userid' => $userAbo->payone_userid,
]);
AboHelper::ensureUserAboItemsFromLatestOrder($userAbo);
$shoppingOrder = null;
$paymentAttemptRecorded = false;
$userOrder = new UserMakeOrder($userAbo);
try {
if (! $userOrder->createShoppingUser()) {
return [
'success' => false,
'message' => __('abo.retry_error_shopping_user'),
];
}
$shoppingOrder = $userOrder->makeShoppingOrder();
if (! $shoppingOrder) {
return [
'success' => false,
'message' => __('abo.retry_error_order'),
];
}
$response = $this->normalizePaymentResponse($userOrder->makePayment());
if (($response['status'] ?? null) === 'APPROVED') {
$this->markAboSuccess($userAbo, $shoppingOrder);
$paymentAttemptRecorded = true;
return [
'success' => true,
'message' => __('abo.retry_success', ['order' => $shoppingOrder->id]),
'order_id' => $shoppingOrder->id,
];
}
$this->logPaymentError($userAbo, $shoppingOrder, $response);
$this->markAboError($userAbo, $shoppingOrder, AboStatusExplainer::paymentErrorMessage($response));
$paymentAttemptRecorded = true;
$this->sendPaymentErrorMail($userOrder, $shoppingOrder, $response);
return [
'success' => false,
'message' => __('abo.retry_failed', [
'error' => $this->formatPaymentError($response),
'order' => $shoppingOrder->id,
]),
'order_id' => $shoppingOrder->id,
];
} catch (\Throwable $e) {
\Log::channel('abo_order')->error('AboRetryPaymentService: Exception beim erneuten Zahlungsversuch', [
'abo_id' => $userAbo->id,
'order_id' => $shoppingOrder?->id,
'error' => $e->getMessage(),
'trace' => $e->getTraceAsString(),
]);
if ($shoppingOrder && ! $paymentAttemptRecorded) {
$this->markAboError($userAbo, $shoppingOrder, mb_substr('Exception: '.$e->getMessage(), 0, 255));
}
return [
'success' => false,
'message' => __('abo.retry_exception', ['error' => $e->getMessage()]),
'order_id' => $shoppingOrder?->id,
];
}
}
private function alreadyPaidToday(UserAbo $userAbo): bool
{
return UserAboOrder::where('user_abo_id', $userAbo->id)
->whereDate('created_at', now()->toDateString())
->where('paid', true)
->exists();
}
/**
* @return array<string, mixed>
*/
private function normalizePaymentResponse(mixed $response): array
{
if (is_object($response)) {
return (array) $response;
}
return is_array($response) ? $response : [];
}
private function markAboSuccess(UserAbo $userAbo, mixed $shoppingOrder): void
{
DB::transaction(function () use ($userAbo, $shoppingOrder): void {
$userAbo->update([
'status' => 2,
'active' => true,
'next_date' => AboHelper::setNextDate(now(), $userAbo->abo_interval),
'last_date' => now(),
]);
UserAboOrder::create([
'user_abo_id' => $userAbo->id,
'shopping_order_id' => $shoppingOrder->id,
'status' => 1,
'paid' => true,
]);
});
try {
IncentiveTracker::trackAboActivated($shoppingOrder);
} catch (\Throwable $e) {
\Log::channel('abo_order')->error('AboRetryPaymentService: Incentive-Tracking nach erfolgreichem Retry fehlgeschlagen', [
'abo_id' => $userAbo->id,
'order_id' => $shoppingOrder->id,
'error' => $e->getMessage(),
]);
}
// Nur bei Erfolg: Einmal-Artikel entfernen und Comp-Produkte neu bewerten.
AboOneTimeService::purgeAfterExecution($userAbo);
}
private function markAboError(UserAbo $userAbo, mixed $shoppingOrder, ?string $errorMessage = null): void
{
DB::transaction(function () use ($userAbo, $shoppingOrder, $errorMessage): void {
$userAbo->update([
'status' => 3,
'last_date' => now(),
]);
UserAboOrder::create([
'user_abo_id' => $userAbo->id,
'shopping_order_id' => $shoppingOrder->id,
'status' => 3,
'error_message' => $errorMessage,
'paid' => false,
]);
});
}
/**
* @param array<string, mixed> $response
*/
private function logPaymentError(UserAbo $userAbo, mixed $shoppingOrder, array $response): void
{
\Log::channel('abo_order')->error('AboRetryPaymentService: Zahlungsfehler beim erneuten Versuch', [
'abo_id' => $userAbo->id,
'order_id' => $shoppingOrder->id,
'response' => $response,
]);
MyLog::writeLog(
'abo_order',
'error',
'Error:AboRetryPaymentService::retry / makePayment Error',
$response
);
}
/**
* @param array<string, mixed> $response
*/
private function sendPaymentErrorMail(UserMakeOrder $userOrder, mixed $shoppingOrder, array $response): void
{
$shoppingPayment = $userOrder->getShoppingPayment();
if (! $shoppingPayment) {
return;
}
try {
Payment::paymentStatusSendMail($shoppingOrder, $shoppingPayment, [
'mode' => $shoppingPayment->mode,
'txaction' => 'error',
'send_link' => false,
'payment_error' => $response,
]);
} catch (\Throwable $e) {
\Log::channel('abo_order')->error('AboRetryPaymentService: Fehlermail nach Zahlungsfehler konnte nicht gesendet werden', [
'order_id' => $shoppingOrder->id,
'payment_id' => $shoppingPayment->id,
'error' => $e->getMessage(),
]);
}
}
/**
* @param array<string, mixed> $response
*/
private function formatPaymentError(array $response): string
{
$errorCode = $response['errorcode'] ?? null;
$errorMessage = $response['errormessage'] ?? $response['customermessage'] ?? ($response['status'] ?? __('payment.unknown'));
if ($errorCode) {
return '['.$errorCode.'] '.$errorMessage;
}
return (string) $errorMessage;
}
}