status; if ($abo->isPendingInitialPayment() || $status === 0) { return $this->pendingPayment($abo); } return match ($status) { 3 => $this->paymentFailed($abo), 4 => $this->cancelled($abo), 5 => new AboStatusInfo(AboStatusInfo::STATE_FINISHED, __('abo.state_finished'), 'dark', __('abo.state_text_finished')), 6 => $this->deactivated($abo), 7 => new AboStatusInfo(AboStatusInfo::STATE_GRACE, __('abo.state_grace'), 'info', __('abo.state_text_grace')), 8 => new AboStatusInfo(AboStatusInfo::STATE_EXTERN_DONE, __('abo.state_extern_done'), 'info', __('abo.state_text_extern_done')), 2 => $abo->active ? $this->running($abo) : $this->deactivated($abo), default => new AboStatusInfo(AboStatusInfo::STATE_UNKNOWN, $abo->getStatusType() ?: __('abo.state_unknown'), $abo->getStatusColor(), ''), }; } /** * Kurzer Fehlertext aus einer Payone-/Stripe-Antwort fuer * `user_abo_orders.error_message` — null, wenn die Antwort nichts Lesbares * enthaelt. * * @param array|null $response */ public static function paymentErrorMessage(?array $response): ?string { if (! $response) { return null; } $message = $response['customermessage'] ?? $response['errormessage'] ?? $response['message'] ?? null; $code = $response['errorcode'] ?? $response['decline_code'] ?? $response['code'] ?? null; if (! is_string($message) || trim($message) === '') { return null; } $text = trim($message); if (is_scalar($code) && (string) $code !== '') { $text = '['.$code.'] '.$text; } return mb_substr($text, 0, 255); } private function running(UserAbo $abo): AboStatusInfo { $pausedUntil = $abo->pausedUntil(); if ($pausedUntil !== null && $pausedUntil->gte(Carbon::today())) { $actor = $this->actorFromReason($abo->status_reason, 'paused_by_') ?? $this->actorFromHistory($abo, UserAboStatusHistory::ACTION_PAUSED); $date = $abo->last_pause_date ? Carbon::parse($abo->getRawOriginal('last_pause_date')) : null; return new AboStatusInfo( AboStatusInfo::STATE_PAUSED, __('abo.state_paused'), 'warning', __('abo.state_text_paused', [ 'until' => $pausedUntil->format(\Util::formatDateDB()), 'actor' => $this->actorLabel($actor), 'date' => $date ? $date->format(\Util::formatDateDB()) : '-', ]), $date, $actor, ); } $last = $this->rawDate($abo, 'last_date'); $next = $this->rawDate($abo, 'next_date'); $text = $last ? __('abo.state_text_active', ['last' => $last->format(\Util::formatDateDB()), 'next' => $next?->format(\Util::formatDateDB()) ?? '-']) : __('abo.state_text_active_first', ['next' => $next?->format(\Util::formatDateDB()) ?? '-']); return new AboStatusInfo(AboStatusInfo::STATE_ACTIVE, __('abo.state_active'), 'success', $text, $next); } private function paymentFailed(UserAbo $abo): AboStatusInfo { $date = $this->rawDate($abo, 'last_date'); $error = $this->latestErrorMessage($abo); $text = __('abo.state_text_payment_failed', ['date' => $date ? $date->format(\Util::formatDateDB()) : '-']); if ($error !== null) { $text .= ' '.__('abo.state_text_payment_error', ['error' => $error]); } return new AboStatusInfo(AboStatusInfo::STATE_PAYMENT_FAILED, __('abo.state_payment_failed'), 'danger', $text, $date, null, $error); } private function cancelled(UserAbo $abo): AboStatusInfo { $actor = $this->actorFromReason($abo->status_reason, 'cancelled_by_') ?? $this->actorFromHistory($abo, UserAboStatusHistory::ACTION_CANCELLED); $date = $this->rawDate($abo, 'cancel_date') ?? $this->historyDate($abo, UserAboStatusHistory::ACTION_CANCELLED); return new AboStatusInfo( AboStatusInfo::STATE_CANCELLED, __('abo.state_cancelled'), 'dark', __('abo.state_text_cancelled', [ 'date' => $date ? $date->format(\Util::formatDateDB()) : '-', 'actor' => $this->actorLabel($actor), ]), $date, $actor, ); } private function deactivated(UserAbo $abo): AboStatusInfo { $actor = $this->actorFromReason($abo->status_reason, 'deactivated_by_') ?? $this->actorFromHistory($abo, UserAboStatusHistory::ACTION_DEACTIVATED); $date = $this->historyDate($abo, UserAboStatusHistory::ACTION_DEACTIVATED); $text = $date ? __('abo.state_text_deactivated', ['date' => $date->format(\Util::formatDateDB()), 'actor' => $this->actorLabel($actor)]) : __('abo.state_text_deactivated_no_date', ['actor' => $this->actorLabel($actor)]); return new AboStatusInfo(AboStatusInfo::STATE_DEACTIVATED, __('abo.state_deactivated'), 'secondary', $text, $date, $actor); } private function pendingPayment(UserAbo $abo): AboStatusInfo { if ($abo->hasFailedInitialPayment()) { $text = __('abo.pending_payment_failed_copy'); } elseif ($abo->hasInitialPaymentStatus()) { $text = __('abo.pending_payment_processing_copy'); } else { $text = __('abo.pending_payment_waiting_copy'); } return new AboStatusInfo(AboStatusInfo::STATE_PENDING_PAYMENT, __('abo.state_pending_payment'), 'warning', $text); } private function latestErrorMessage(UserAbo $abo): ?string { $order = $abo->relationLoaded('user_abo_orders') ? $abo->user_abo_orders->sortByDesc('id')->first(fn (UserAboOrder $order) => (int) $order->status === 3) : $abo->user_abo_orders()->where('status', 3)->orderByDesc('id')->first(); $message = $order?->error_message; return is_string($message) && trim($message) !== '' ? $message : null; } private function actorFromReason(?string $reason, string $prefix): ?string { if (! $reason || ! str_starts_with($reason, $prefix)) { return null; } return substr($reason, strlen($prefix)) ?: null; } private function actorFromHistory(UserAbo $abo, string $action): ?string { $entry = $this->latestHistory($abo, $action); return $entry ? UserAbo::reasonActorFromView($entry->view) : null; } private function historyDate(UserAbo $abo, string $action): ?Carbon { return $this->latestHistory($abo, $action)?->created_at?->copy(); } /** * Letzter Protokolleintrag je Aktion — pro Abo und Aktion nur einmal geladen, * aus der vorgeladenen Relation, wenn vorhanden (Listen: `with('status_histories')`). * * @var array */ private array $historyMemo = []; private function latestHistory(UserAbo $abo, string $action): ?UserAboStatusHistory { $key = $abo->id.':'.$action; if (array_key_exists($key, $this->historyMemo)) { return $this->historyMemo[$key]; } if ($abo->relationLoaded('status_histories')) { $entry = $abo->status_histories ->filter(fn (UserAboStatusHistory $history) => $history->action === $action) ->sortByDesc('id') ->first(); } else { $entry = $abo->status_histories()->where('action', $action)->orderByDesc('id')->first(); } return $this->historyMemo[$key] = $entry; } private function actorLabel(?string $actor): string { return __('abo.actor_by_'.($actor ?: UserAbo::ACTOR_SYSTEM)); } private function rawDate(UserAbo $abo, string $column): ?Carbon { $raw = $abo->getRawOriginal($column); return $raw ? Carbon::parse($raw) : null; } }