Bill transfer storage monthly with a 15-day grace period before deletion.
Deploy Ladill Transfer / deploy (push) Successful in 51s

Files stay available while wallet renewals succeed each month. Failed renewals
keep files for TRANSFER_GRACE_PERIOD_DAYS (default 15) before automatic cleanup.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
isaacclad
2026-06-08 12:28:33 +00:00
co-authored by Cursor
parent 65b634bb0b
commit 311b5b40bb
23 changed files with 502 additions and 56 deletions
+71 -3
View File
@@ -2,6 +2,8 @@
namespace App\Models;
use Carbon\CarbonInterface;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
@@ -10,6 +12,8 @@ class Transfer extends Model
{
public const STATUS_ACTIVE = 'active';
public const STATUS_GRACE = 'grace';
public const STATUS_EXPIRED = 'expired';
public const STATUS_DELETED = 'deleted';
@@ -22,6 +26,9 @@ class Transfer extends Model
'password_hash',
'retention_days',
'expires_at',
'paid_until',
'grace_ends_at',
'last_billed_at',
'status',
'downloads_total',
'storage_bytes',
@@ -30,6 +37,9 @@ class Transfer extends Model
protected $casts = [
'retention_days' => 'integer',
'expires_at' => 'datetime',
'paid_until' => 'datetime',
'grace_ends_at' => 'datetime',
'last_billed_at' => 'datetime',
'downloads_total' => 'integer',
'storage_bytes' => 'integer',
];
@@ -54,17 +64,52 @@ class Transfer extends Model
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
{
if ($this->status !== self::STATUS_ACTIVE) {
return $this->isAccessible();
}
public function isPaid(): bool
{
if (! in_array($this->status, [self::STATUS_ACTIVE, self::STATUS_GRACE], true)) {
return false;
}
if ($this->expires_at && $this->expires_at->isPast()) {
return $this->paid_until instanceof CarbonInterface && $this->paid_until->isFuture();
}
public function isInGracePeriod(): bool
{
if ($this->status !== self::STATUS_GRACE) {
return false;
}
return true;
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
@@ -81,4 +126,27 @@ class Transfer extends Model
{
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());
});
}
}