- Karten-Update ohne Abbuchung via PAYONE creditcardcheck/pseudocardpan (AboPaymentMethodService, CardUpdateResult/CreditCardCheckData-DTOs) - Eingabe nur durch Abo-Inhaber: CRM- und Portal-Formular + zeitlich begrenzter Berater→Kunde-Link (AboCardUpdateLink + Mail) - Änderungshistorie in user_abo_card_changes, Anzeige im Admin-Abo-Detail - Admin: Abo löschen (destroy) inkl. Test - Portal-Fix: myOrderCreate wirft 404 statt TypeError bei ungültiger ID - VIP-Sichtbarkeitstests für Einmalartikel-Fenster ergänzt - Enthält bereits die Freeze-Guards für den Stripe-Notbetrieb (Folge-Commit) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
82 lines
2.2 KiB
PHP
82 lines
2.2 KiB
PHP
<?php
|
||
|
||
namespace App\Models;
|
||
|
||
use Illuminate\Database\Eloquent\Model;
|
||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||
use Illuminate\Support\Carbon;
|
||
use Illuminate\Support\Str;
|
||
|
||
/**
|
||
* One-time, time-limited link that lets an Abo owner (customer) update their credit card
|
||
* via PAYONE without logging in. The plaintext token lives only in the e-mail link; the
|
||
* database stores a SHA-256 hash for an indexed O(1) lookup (never bcrypt – that would
|
||
* forbid a direct lookup). Single-use via {@see $used_at}, TTL via {@see ttlHours()}.
|
||
*
|
||
* @property int $user_abo_id
|
||
* @property string $token_hash
|
||
* @property Carbon $expires_at
|
||
* @property Carbon|null $used_at
|
||
* @property int|null $created_by_user_id
|
||
*/
|
||
class AboCardUpdateLink extends Model
|
||
{
|
||
protected $table = 'abo_card_update_links';
|
||
|
||
protected $fillable = [
|
||
'user_abo_id',
|
||
'token_hash',
|
||
'expires_at',
|
||
'used_at',
|
||
'created_by_user_id',
|
||
];
|
||
|
||
protected $casts = [
|
||
'expires_at' => 'datetime',
|
||
'used_at' => 'datetime',
|
||
];
|
||
|
||
public const DEFAULT_TTL_HOURS = 72;
|
||
|
||
public function user_abo(): BelongsTo
|
||
{
|
||
return $this->belongsTo(UserAbo::class, 'user_abo_id');
|
||
}
|
||
|
||
public static function ttlHours(): int
|
||
{
|
||
$hours = (int) Setting::getContentBySlug('abo-card-update-link-ttl-hours');
|
||
|
||
return $hours > 0 ? $hours : self::DEFAULT_TTL_HOURS;
|
||
}
|
||
|
||
public static function hashToken(string $token): string
|
||
{
|
||
return hash('sha256', $token);
|
||
}
|
||
|
||
/**
|
||
* Creates a link and returns the model plus the plaintext token (only emitted here,
|
||
* never persisted).
|
||
*
|
||
* @return array{0: self, 1: string}
|
||
*/
|
||
public static function generateFor(UserAbo $abo, ?int $createdBy = null): array
|
||
{
|
||
$token = Str::random(48);
|
||
|
||
$link = self::create([
|
||
'user_abo_id' => $abo->id,
|
||
'token_hash' => self::hashToken($token),
|
||
'expires_at' => now()->addHours(self::ttlHours()),
|
||
'created_by_user_id' => $createdBy,
|
||
]);
|
||
|
||
return [$link, $token];
|
||
}
|
||
|
||
public function isRedeemable(): bool
|
||
{
|
||
return $this->used_at === null && $this->expires_at->isFuture();
|
||
}
|
||
}
|