14-04-2026

This commit is contained in:
Kevin Adametz 2026-04-14 18:07:45 +02:00
parent f58c709945
commit 0f82fea88a
72 changed files with 7414 additions and 148 deletions

View file

@ -0,0 +1,123 @@
<?php
namespace App\Models;
use App\User;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
/**
* @property int $id
* @property string $event checkout_visited|form_submitted|payment_initiated|payment_returned|payment_confirmed
* @property string|null $session_id
* @property string|null $domain
* @property int|null $shopping_user_id
* @property int|null $shopping_order_id
* @property int|null $shopping_payment_id
* @property int|null $consultant_user_id
* @property string|null $payment_method
* @property string|null $return_status
* @property int|null $amount_cents
* @property array|null $metadata
* @property \Illuminate\Support\Carbon $created_at
*/
class CheckoutFunnelEvent extends Model
{
protected $fillable = [
'event',
'session_id',
'domain',
'shopping_user_id',
'shopping_order_id',
'shopping_payment_id',
'consultant_user_id',
'payment_method',
'return_status',
'amount_cents',
'metadata',
];
protected function casts(): array
{
return [
'metadata' => 'array',
'amount_cents' => 'integer',
];
}
public function shopping_order(): BelongsTo
{
return $this->belongsTo(ShoppingOrder::class);
}
public function shopping_payment(): BelongsTo
{
return $this->belongsTo(ShoppingPayment::class);
}
public function consultant(): BelongsTo
{
return $this->belongsTo(User::class, 'consultant_user_id');
}
/** @return array<string, string> */
public static function eventLabels(): array
{
return [
'checkout_visited' => 'Checkout aufgerufen',
'form_submitted' => 'Formular abgeschickt',
'payment_initiated' => 'Zahlung gestartet',
'payment_returned' => 'Zurück von PAYONE',
'payment_confirmed' => 'PAYONE Callback',
];
}
public function getEventLabelAttribute(): string
{
return self::eventLabels()[$this->event] ?? $this->event;
}
/**
* Classifies the domain into a human-readable source channel.
*
* - my.mivita.* → Salescenter (Berater-Bereich)
* - in.mivita.* → Beraterzugang (in.mivita.care login)
* - *.mivita.care → Kundenshop (Live)
* - *.mivita.shop → Kundenshop (Live)
* - *.mivita.test → Testserver
*/
public function getSourceTypeAttribute(): string
{
$domain = strtolower($this->domain ?? '');
if (str_starts_with($domain, 'my.')) {
return 'salescenter';
}
if (str_starts_with($domain, 'in.')) {
return 'beraterzugang';
}
if (str_ends_with($domain, '.test') || str_contains($domain, '.mivita.test')) {
return 'testserver';
}
if (str_ends_with($domain, '.care') || str_ends_with($domain, '.shop')) {
return 'kundenshop';
}
return 'unbekannt';
}
/** @return array<string, string> */
public static function sourceLabels(): array
{
return [
'kundenshop' => 'Kundenshop (*.mivita.care / *.mivita.shop)',
'salescenter' => 'Salescenter (my.mivita.care)',
'beraterzugang' => 'Beraterzugang (in.mivita.care)',
'testserver' => 'Testserver (*.mivita.test)',
'unbekannt' => 'Unbekannt',
];
}
}

View file

@ -0,0 +1,46 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class IncidentActivity extends Model
{
protected $fillable = [
'incident_id',
'type',
'title',
'content',
'author',
];
public function incident(): BelongsTo
{
return $this->belongsTo(PaymentIncident::class, 'incident_id');
}
public function getTypeIconAttribute(): string
{
return match ($this->type) {
'email' => 'ion-md-mail',
'call' => 'ion-md-call',
'ticket' => 'ion-md-pricetag',
'status_change' => 'ion-md-swap',
'provider_response' => 'ion-md-return-left',
default => 'ion-md-create',
};
}
public function getTypeLabelAttribute(): string
{
return match ($this->type) {
'email' => 'E-Mail',
'call' => 'Telefonat',
'ticket' => 'Ticket',
'status_change' => 'Statusänderung',
'provider_response' => 'Anbieter-Antwort',
default => 'Notiz',
};
}
}

View file

@ -0,0 +1,156 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany;
class PaymentIncident extends Model
{
protected $fillable = [
'title',
'description',
'notes',
'provider',
'type',
'status',
'severity',
'affected_orders',
'affected_revenue',
'detected_at',
'resolved_at',
'ticket_number',
];
/** @var array<string, mixed> */
protected $attributes = [
'status' => 'open',
'severity' => 'medium',
'provider' => 'payone',
];
protected function casts(): array
{
return [
'detected_at' => 'datetime',
'resolved_at' => 'datetime',
'affected_revenue' => 'decimal:2',
];
}
public function activities(): HasMany
{
return $this->hasMany(IncidentActivity::class, 'incident_id')->orderBy('created_at');
}
public function scopeOpen(Builder $query): Builder
{
return $query->whereIn('status', ['open', 'in_progress', 'waiting_provider']);
}
public function scopePayone(Builder $query): Builder
{
return $query->where('provider', 'payone');
}
public function scopeLastDays(Builder $query, int $days): Builder
{
return $query->where('detected_at', '>=', now()->subDays($days));
}
public function getDurationAttribute(): string
{
$end = $this->resolved_at ?? now();
$diff = $this->detected_at->diff($end);
if ($diff->days > 0) {
return $diff->days.'d '.$diff->h.'h';
}
if ($diff->h > 0) {
return $diff->h.'h '.$diff->i.'min';
}
return $diff->i.' min';
}
public function getSeverityColorAttribute(): string
{
return match ($this->severity) {
'critical' => 'danger',
'high' => 'warning',
'medium' => 'info',
'low' => 'success',
default => 'secondary',
};
}
public function getSeverityLabelAttribute(): string
{
return match ($this->severity) {
'critical' => 'Kritisch',
'high' => 'Hoch',
'medium' => 'Mittel',
'low' => 'Niedrig',
default => $this->severity,
};
}
public function getStatusLabelAttribute(): string
{
return match ($this->status) {
'open' => 'Offen',
'in_progress' => 'In Bearbeitung',
'waiting_provider' => 'Wartet auf Anbieter',
'resolved' => 'Gelöst',
'closed' => 'Geschlossen',
default => $this->status,
};
}
public function getStatusColorAttribute(): string
{
return match ($this->status) {
'open' => 'danger',
'in_progress' => 'warning',
'waiting_provider' => 'info',
'resolved' => 'success',
'closed' => 'secondary',
default => 'secondary',
};
}
public function getTypeIconAttribute(): string
{
return match ($this->type) {
'outage' => 'ion-md-power',
'ipn_error' => 'ion-md-git-network',
'payment_failure' => 'ion-md-card',
'slow_response' => 'ion-md-timer',
default => 'ion-md-alert',
};
}
public function getTypeLabelAttribute(): string
{
return match ($this->type) {
'outage' => 'Ausfall',
'ipn_error' => 'IPN-Fehler',
'payment_failure' => 'Zahlungsfehler',
'slow_response' => 'Langsame Antwort',
default => 'Sonstiges',
};
}
public function getProviderLabelAttribute(): string
{
return match ($this->provider) {
'payone' => 'PAYONE',
// 'stripe' => 'Stripe', // aktuell nicht aktiv
'paypal' => 'PayPal',
// 'mollie' => 'Mollie', // aktuell nicht aktiv
default => 'Sonstige',
};
}
}

View file

@ -22,6 +22,7 @@ use Illuminate\Database\Eloquent\Model;
* @property \Illuminate\Support\Carbon|null $created_at
* @property \Illuminate\Support\Carbon|null $updated_at
* @property-read \App\Models\ShoppingPayment $shopping_payment
*
* @method static \Illuminate\Database\Eloquent\Builder|\App\Models\PaymentTransaction newModelQuery()
* @method static \Illuminate\Database\Eloquent\Builder|\App\Models\PaymentTransaction newQuery()
* @method static \Illuminate\Database\Eloquent\Builder|\App\Models\PaymentTransaction query()
@ -39,18 +40,20 @@ use Illuminate\Database\Eloquent\Model;
* @method static \Illuminate\Database\Eloquent\Builder|\App\Models\PaymentTransaction whereTxid($value)
* @method static \Illuminate\Database\Eloquent\Builder|\App\Models\PaymentTransaction whereUpdatedAt($value)
* @method static \Illuminate\Database\Eloquent\Builder|\App\Models\PaymentTransaction whereUserid($value)
*
* @property string|null $mode
*
* @method static \Illuminate\Database\Eloquent\Builder|\App\Models\PaymentTransaction whereMode($value)
*
* @mixin \Eloquent
*/
class PaymentTransaction extends Model
{
protected $table = 'payment_transactions';
protected $casts = [
'transmitted_data' => 'array'
];
'transmitted_data' => 'array',
];
protected $fillable = [
'shopping_payment_id',
@ -67,9 +70,52 @@ class PaymentTransaction extends Model
'mode',
];
public function shopping_payment()
{
return $this->belongsTo('App\Models\ShoppingPayment','shopping_payment_id');
return $this->belongsTo('App\Models\ShoppingPayment', 'shopping_payment_id');
}
/**
* Returns a human-readable description for known PAYONE error codes.
*
* @return array<int, string>
*/
public static function payoneErrorDescriptions(): array
{
return [
2 => 'Allgemeiner Fehler',
4 => 'Karte gesperrt',
5 => 'Zahlung abgelehnt',
12 => 'Ungültige Kartennummer',
14 => 'Ungültige Kartendaten',
33 => 'Karte abgelaufen',
34 => 'Karte gesperrt/gestohlen',
55 => 'Falsche PIN',
56 => 'Kunde hat abgebrochen',
58 => 'Transaktion nicht erlaubt',
62 => 'Karte für internationale Transaktionen nicht zugelassen',
104 => 'Karte für Online-Zahlungen nicht erlaubt',
105 => 'Karte ungültig',
120 => 'Falsche Prüfziffer (CVV)',
130 => 'Limit überschritten',
131 => 'Währung nicht unterstützt',
135 => 'Bank antwortet nicht',
136 => 'Bank nicht gefunden',
137 => 'Betrag stimmt nicht überein',
900 => '3D-Secure Authentifizierung fehlgeschlagen',
902 => 'Bank hat abgelehnt',
970 => 'Keine Antwort (Timeout)',
4218 => 'Karte unter Betrugsverdacht',
4219 => 'Transaktion von Kartenaussteller abgelehnt',
];
}
public function getErrorDescriptionAttribute(): ?string
{
if (! $this->errorcode) {
return null;
}
return self::payoneErrorDescriptions()[(int) $this->errorcode] ?? null;
}
}

View file

@ -0,0 +1,24 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class ProviderUptimeLog extends Model
{
protected $fillable = [
'provider',
'is_up',
'response_time_ms',
'error_message',
'checked_at',
];
protected function casts(): array
{
return [
'is_up' => 'boolean',
'checked_at' => 'datetime',
];
}
}