$user_abo_items * @property-read Collection $one_time_items * @property-read int|null $user_abo_items_count * @property-read int|null $user_abo_orders_count * * @method static \Illuminate\Database\Eloquent\Builder|UserAbo newModelQuery() * @method static \Illuminate\Database\Eloquent\Builder|UserAbo newQuery() * @method static \Illuminate\Database\Eloquent\Builder|UserAbo onlyTrashed() * @method static \Illuminate\Database\Eloquent\Builder|UserAbo query() * @method static \Illuminate\Database\Eloquent\Builder|UserAbo whereAboInterval($value) * @method static \Illuminate\Database\Eloquent\Builder|UserAbo whereActive($value) * @method static \Illuminate\Database\Eloquent\Builder|UserAbo whereAmount($value) * @method static \Illuminate\Database\Eloquent\Builder|UserAbo whereCancelDate($value) * @method static \Illuminate\Database\Eloquent\Builder|UserAbo whereCarddata($value) * @method static \Illuminate\Database\Eloquent\Builder|UserAbo whereClearingtype($value) * @method static \Illuminate\Database\Eloquent\Builder|UserAbo whereCreatedAt($value) * @method static \Illuminate\Database\Eloquent\Builder|UserAbo whereDeletedAt($value) * @method static \Illuminate\Database\Eloquent\Builder|UserAbo whereEmail($value) * @method static \Illuminate\Database\Eloquent\Builder|UserAbo whereId($value) * @method static \Illuminate\Database\Eloquent\Builder|UserAbo whereIsFor($value) * @method static \Illuminate\Database\Eloquent\Builder|UserAbo whereLastDate($value) * @method static \Illuminate\Database\Eloquent\Builder|UserAbo whereMemberId($value) * @method static \Illuminate\Database\Eloquent\Builder|UserAbo whereNextDate($value) * @method static \Illuminate\Database\Eloquent\Builder|UserAbo wherePayoneUserid($value) * @method static \Illuminate\Database\Eloquent\Builder|UserAbo whereShoppingUserId($value) * @method static \Illuminate\Database\Eloquent\Builder|UserAbo whereStartDate($value) * @method static \Illuminate\Database\Eloquent\Builder|UserAbo whereStatus($value) * @method static \Illuminate\Database\Eloquent\Builder|UserAbo whereUpdatedAt($value) * @method static \Illuminate\Database\Eloquent\Builder|UserAbo whereUserDeletedAt($value) * @method static \Illuminate\Database\Eloquent\Builder|UserAbo whereUserId($value) * @method static \Illuminate\Database\Eloquent\Builder|UserAbo whereWallettype($value) * @method static \Illuminate\Database\Eloquent\Builder|UserAbo withTrashed() * @method static \Illuminate\Database\Eloquent\Builder|UserAbo withoutTrashed() * * @mixin \Eloquent */ class UserAbo extends Model { use SoftDeletes; protected $table = 'user_abos'; protected $casts = [ 'user_id' => 'int', 'member_id' => 'int', 'shopping_user_id' => 'int', 'payone_userid' => 'int', 'active' => 'bool', 'status' => 'int', 'abo_interval' => 'int', 'amount' => 'int', 'start_date' => 'datetime', 'last_date' => 'datetime', 'next_date' => 'datetime', 'onetime_reminder_sent_for' => 'date', 'cancel_date' => 'datetime', 'user_deleted_at' => 'datetime', 'carddata' => 'array', 'card_update_pending_at' => 'datetime', ]; protected $fillable = [ 'user_id', 'member_id', 'shopping_user_id', 'is_for', 'email', 'payone_userid', 'payment_provider', 'stripe_customer_id', 'stripe_payment_method_id', 'clearingtype', 'wallettype', 'carddata', 'amount', 'active', 'status', 'abo_interval', 'start_date', 'last_date', 'next_date', 'onetime_reminder_sent_for', 'cancel_date', 'user_deleted_at', 'card_update_pending_at', ]; public static $aboDeliveryDays = [5, 10, 20, 25]; public static $statusTypes = [ 0 => 'abo_new', 1 => 'abo_new', 2 => 'abo_okay', 3 => 'abo_hold', 4 => 'abo_cancel', 5 => 'abo_finish', 6 => 'abo_inactive', 7 => 'abo_grace', 8 => 'abo_extern_done', ]; public static $statusColors = [ 0 => 'success', 1 => 'success', 2 => 'secondary', 3 => 'warning', 4 => 'danger', 5 => 'info', 6 => 'warning', 7 => 'danger', 8 => 'success', ]; /** * Aktive Abos (laufend): active=true und kein terminaler/inaktiver Status * (4 = storniert, 5 = beendet, 6 = inaktiv). Spiegelt die Definition aus * {@see BackofficeDashboardService}. */ public function scopeActiveAbo(Builder $query): Builder { return $query ->where('active', true) ->whereNotIn('status', [4, 5, 6]); } /** * Schränkt die Abfrage auf Abos ein, die im angegebenen Monat/Jahr hinzugekommen * sind (Eingangsmonat = start_date). Bei month=0 (oder außerhalb 1–12) bleibt die * Abfrage unverändert. */ public function scopeAddedInMonth(Builder $query, int $year, int $month): Builder { if ($month < 1 || $month > 12) { return $query; } return $query ->whereYear('start_date', $year) ->whereMonth('start_date', $month); } public function user() { return $this->belongsTo(User::class, 'user_id'); } public function member() { return $this->belongsTo(User::class, 'member_id'); } public function shopping_user() { return $this->belongsTo('App\Models\ShoppingUser', 'shopping_user_id'); } public function user_abo_orders() { return $this->hasMany(UserAboOrder::class); } public function initialUserAboOrder(): ?UserAboOrder { return $this->user_abo_orders() ->orderBy('id') ->first(); } public function initialShoppingOrder(): ?ShoppingOrder { return $this->initialUserAboOrder()?->shopping_order; } public function initialShoppingPayment(): ?ShoppingPayment { return $this->initialShoppingOrder()?->shopping_payments() ->orderBy('id') ->first(); } public function isPendingInitialPayment(): bool { return (int) $this->status === 1; } public function hasInitialPaymentStatus(): bool { $payment = $this->initialShoppingPayment(); $order = $this->initialShoppingOrder(); return filled($payment?->txaction) || filled($payment?->status) || filled($order?->txaction); } public function hasFailedInitialPayment(): bool { $payment = $this->initialShoppingPayment(); $order = $this->initialShoppingOrder(); return in_array($payment?->txaction, ['failed'], true) || in_array($payment?->status, ['error', 'cancel'], true) || in_array($order?->txaction, ['failed'], true); } public function canDeletePendingInitialPayment(): bool { return $this->isPendingInitialPayment() && $this->hasFailedInitialPayment(); } public function isCreditCard(): bool { return $this->clearingtype === 'cc'; } /** * Selbstbedienungs-Detailseite dieses Abos: Berater-eigenes Abo (is_for=me) → CRM, * Kunden-Abo → Kundenportal. Genutzt für Recovery-Links (Mail, Checkout-Zurück). */ public function selfServiceDetailUrl(): string { return $this->is_for === 'me' ? rtrim(Util::getMyMivitaUrl(), '/').'/user/abos/detail/me/'.$this->id : rtrim(Util::getMyMivitaPortalUrl(), '/').'/portal/my-subscriptions'; } /** * Maskierte Kartendarstellung für die Anzeige, z. B. „411111XXXXXX1111 (12/30)". * Nutzt ausschließlich Token-/Maskendaten aus carddata (nie PAN/CVC). */ public function maskedCard(): ?string { if (! $this->isCreditCard()) { return null; } return self::formatMaskedCard(is_array($this->carddata) ? $this->carddata : null); } /** * Baut die maskierte Kartendarstellung aus beliebigen carddata (Token-/Maskendaten). * * @param array|null $carddata */ public static function formatMaskedCard(?array $carddata): ?string { $pan = $carddata['truncatedcardpan'] ?? null; if (! $pan) { // Stripe-Summary (type/brand/last4/exp aus payment_method_details) if (! empty($carddata['last4'])) { $label = ! empty($carddata['brand']) ? ucfirst($carddata['brand']).' ' : ''; $exp = ! empty($carddata['exp']) ? ' ('.$carddata['exp'].')' : ''; return $label.'•••• '.$carddata['last4'].$exp; } if (! empty($carddata['type'])) { return ShoppingPayment::stripeLabelFromSummary($carddata); } return null; } $expire = (string) ($carddata['cardexpiredate'] ?? ''); if (strlen($expire) === 4) { return $pan.' ('.substr($expire, 2, 2).'/'.substr($expire, 0, 2).')'; } return $pan; } public function card_changes(): HasMany { return $this->hasMany(UserAboCardChange::class)->latest(); } /** * Ob der Kreditkarten-Token dieses Abos aktualisiert werden darf. Guard-agnostische * Invariante: nur CC-Abos, nicht gelöscht, kein terminaler/inaktiver Status * (4 = storniert, 5 = beendet, 6 = inaktiv). Die Eigentümer-/Guard-Prüfung erfolgt * zusätzlich im jeweiligen Controller (CRM bzw. Portal). */ public function canUpdatePaymentMethod(): bool { return $this->isCreditCard() && ! $this->trashed() && ! in_array((int) $this->status, [4, 5, 6], true); } /** * Zustands-Invariante ohne die "nur CC"-Einschränkung — für den * Stripe-Setup-Flow "Zahlungsart ändern" (Plan S5), der jede Zahlart * wechseln kann. Welcher Flow gilt, entscheidet * {@see AboPaymentMethodService::canStartChange()}. */ public function canChangePaymentMethodAnyType(): bool { return ! $this->trashed() && ! in_array((int) $this->status, [4, 5, 6], true); } public function user_abo_items() { return $this->hasMany(UserAboItem::class); } public function one_time_items() { return $this->hasMany(UserAboOneTimeItem::class); } public function user_abo_item_histories() { return $this->hasMany(UserAboItemHistory::class); } public function getInitialItems() { return $this->user_abo_item_histories() ->where('is_initial', true) ->where('comp', 0) ->orderBy('created_at') ->get(); } public function getInitialCompItems() { return $this->user_abo_item_histories() ->where('is_initial', true) ->where('comp', '>', 0) ->orderBy('comp') ->get(); } public function getChangeHistory() { return $this->user_abo_item_histories() ->where('is_initial', false) ->orderByDesc('created_at') ->get(); } public function getCountOrders() { // sind bezahlte Bestellungen return $this->user_abo_orders->where('status', '>=', 2)->count(); } public function getCountPaidOrders() { // Ausgefuehrte Abo-Lieferungen: bezahlte sowie extern erledigte (Status 8, // abo_extern_done) Bestellungen. Extern erledigt = Lieferung ausgefuehrt und // Punkte vergeben, daher zaehlt sie wie eine bezahlte Lieferung (z. B. fuer die // Mindestlaufzeit), obwohl paid=false bleibt (Inkasso laeuft ausserhalb). return $this->user_abo_orders ->where('status', '>=', 2) ->filter(fn (UserAboOrder $order) => $order->paid || (int) $order->status === 8) ->count(); } public function setStartDateAttribute($value) { $this->attributes['start_date'] = isset($value) ? (new Carbon($value))->format('Y-m-d') : null; } public function getStartDateAttribute() { return $this->attributes['start_date'] ? Carbon::parse($this->attributes['start_date'])->format(\Util::formatDateDB()) : ''; } public function setLastDateAttribute($value) { $this->attributes['last_date'] = isset($value) ? (new Carbon($value))->format('Y-m-d') : null; } public function getLastDateAttribute() { return $this->attributes['last_date'] ? Carbon::parse($this->attributes['last_date'])->format(\Util::formatDateDB()) : ''; } public function setNextDateAttribute($value) { $this->attributes['next_date'] = isset($value) ? (new Carbon($value))->format('Y-m-d') : null; } public function getNextDateAttribute() { return $this->attributes['next_date'] ? Carbon::parse($this->attributes['next_date'])->format(\Util::formatDateDB()) : ''; } public function setCancelDateAttribute($value) { $this->attributes['cancel_date'] = isset($value) ? (new Carbon($value))->format('Y-m-d') : null; } public function getCancelDateAttribute() { return $this->attributes['cancel_date'] ? Carbon::parse($this->attributes['cancel_date'])->format(\Util::formatDateDB()) : ''; } public function getFormattedAmount(): string { return isset($this->attributes['amount']) ? Util::formatNumber($this->attributes['amount'] / 100) : ''; } public function getTotalPoints(): float { return $this->user_abo_items ->where('comp', 0) ->sum(fn ($item) => ($item->product?->points ?? 0) * $item->qty); } public function getFormattedTotalPoints(): string { return Util::formatNumber($this->getTotalPoints()); } public function getIsForFormated() { return $this->attributes['is_for'] === 'me' ? ''.__('tables.adviser').'' : ''.__('tables.customer').''; } public function getStatusFormated() { return ''.$this->getStatusType().''; } public function getStatusType() { return isset(self::$statusTypes[$this->status]) ? __('abo.'.self::$statusTypes[$this->status]) : ''; } public function getStatusColor() { return isset(self::$statusColors[$this->status]) ? self::$statusColors[$this->status] : 'default'; } public function getPaymentType() { if ($this->payment_provider === PaymentProvider::Stripe->value) { return ShoppingPayment::stripeLabelFromSummary(is_array($this->carddata) ? $this->carddata : []); } return $this->clearingtype === 'wlt' ? __('payment.paypal') : __('payment.credit_card'); } }