'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(); } }