'integer', 'expires_at' => 'datetime', 'paid_until' => 'datetime', 'grace_ends_at' => 'datetime', 'last_billed_at' => 'datetime', 'downloads_total' => 'integer', 'storage_bytes' => 'integer', ]; public function user(): BelongsTo { return $this->belongsTo(User::class); } public function qrCode(): BelongsTo { return $this->belongsTo(QrCode::class); } public function files(): HasMany { return $this->hasMany(TransferFile::class); } public function downloadEvents(): HasMany { return $this->hasMany(TransferDownloadEvent::class); } /** Files are available to recipients while paid or within the unpaid grace window. */ public function isAccessible(): bool { if ($this->status === self::STATUS_DELETED) { return false; } if ($this->isPaid()) { return true; } return $this->isInGracePeriod(); } /** @deprecated Use isAccessible() — kept for existing call sites. */ public function isActive(): bool { return $this->isAccessible(); } public function isPaid(): bool { if (! in_array($this->status, [self::STATUS_ACTIVE, self::STATUS_GRACE], true)) { return false; } return $this->paid_until instanceof CarbonInterface && $this->paid_until->isFuture(); } public function isInGracePeriod(): bool { if ($this->status !== self::STATUS_GRACE) { return false; } return $this->grace_ends_at instanceof CarbonInterface && $this->grace_ends_at->isFuture(); } public function isPastGracePeriod(): bool { return $this->grace_ends_at instanceof CarbonInterface && $this->grace_ends_at->isPast(); } public function billingPeriodEndsAt(): ?CarbonInterface { return $this->paid_until; } public function isPasswordProtected(): bool { return $this->password_hash !== null && $this->password_hash !== ''; } public function storageGb(): float { return round($this->storage_bytes / (1024 * 1024 * 1024), 3); } public function monthlyCostGhs(): float { return round($this->storageGb() * (float) config('transfer.price_per_gb_month', 0.30), 2); } public function billingStatusLabel(): string { if ($this->isPaid()) { return 'Active'; } if ($this->isInGracePeriod()) { return 'Payment due'; } return 'Unavailable'; } public function scopeAccessible(Builder $query): Builder { return $query ->whereIn('status', [self::STATUS_ACTIVE, self::STATUS_GRACE]) ->where(function (Builder $inner) { $inner->where('paid_until', '>', now()) ->orWhere('grace_ends_at', '>', now()); }); } }