Simplify recipient emails and add live upload progress on create.
Deploy Ladill Transfer / deploy (push) Successful in 47s

Recipients always get the download link immediately when an email is set.
Large files upload in the background with per-file and overall progress bars.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
isaacclad
2026-06-08 13:25:43 +00:00
co-authored by Cursor
parent c53191a41b
commit 4b616a7f04
11 changed files with 287 additions and 227 deletions
@@ -4,96 +4,32 @@ namespace App\Services\Transfer;
use App\Models\Transfer;
use App\Notifications\TransferRecipientNotification;
use Carbon\CarbonInterface;
use Illuminate\Support\Facades\Notification;
/** Sends milestone emails to transfer recipients (share link + reminders). */
/** Sends the immediate download-link email to transfer recipients. */
class TransferRecipientMailService
{
/** @return list<string> */
public function allowedMilestones(): array
public function notifyRecipient(Transfer $transfer, string $milestone = 'created'): bool
{
return array_keys((array) config('transfer.recipient_email_milestones', []));
}
/** @return array<string, string> */
public function milestoneOptions(): array
{
return (array) config('transfer.recipient_email_milestones', []);
}
/** @param list<string> $milestones */
public function normalizeMilestones(array $milestones): array
{
$allowed = $this->allowedMilestones();
return array_values(array_unique(array_filter(
$milestones,
static fn ($milestone) => in_array((string) $milestone, $allowed, true),
)));
}
public function notifyRecipient(Transfer $transfer, string $milestone): bool
{
$email = trim((string) $transfer->recipient_email);
if ($email === '' || ! $transfer->wantsRecipientMilestone($milestone)) {
if ($milestone !== 'created') {
return false;
}
if ($transfer->recipientMilestoneSent($milestone)) {
$email = trim((string) $transfer->recipient_email);
if ($email === '') {
return false;
}
if ($transfer->recipientMilestoneSent('created')) {
return false;
}
Notification::route('mail', $email)->notify(
new TransferRecipientNotification($transfer->loadMissing(['files', 'qrCode', 'user']), $milestone),
new TransferRecipientNotification($transfer->loadMissing(['files', 'qrCode', 'user']), 'created'),
);
$transfer->markRecipientMilestoneSent($milestone);
$transfer->markRecipientMilestoneSent('created');
return true;
}
public function processDueMilestones(): int
{
$sent = 0;
Transfer::query()
->accessible()
->whereNotNull('recipient_email')
->whereNotNull('recipient_email_milestones')
->with(['files', 'qrCode', 'user'])
->chunkById(100, function ($transfers) use (&$sent) {
foreach ($transfers as $transfer) {
foreach ($this->dueDayMilestones($transfer) as $milestone) {
if ($this->notifyRecipient($transfer, $milestone)) {
$sent++;
}
}
}
});
return $sent;
}
/** @return list<string> */
private function dueDayMilestones(Transfer $transfer): array
{
if (! $transfer->paid_until instanceof CarbonInterface || $transfer->paid_until->isPast()) {
return [];
}
$daysRemaining = (int) now()->startOfDay()->diffInDays(
$transfer->paid_until->copy()->startOfDay(),
false,
);
$due = [];
foreach (['7', '3', '1'] as $milestone) {
if ($daysRemaining === (int) $milestone && $transfer->wantsRecipientMilestone($milestone)) {
$due[] = $milestone;
}
}
return $due;
}
}