Audit item #12. The fan-out job ran an upfront Plan query plus a per-user tier-name comparison before checking canSendNow('whatsapp'). Both are already covered by canSendNow → canUseChannel + daily-limit count, so the parent was duplicating filtering work that the child DispatchUserNotificationJob would do anyway via channelsFor(). Now the parent does only the cheap pre-check (canSendNow) before dispatching the per-user child job. Iteration uses chunkById(500) to make the memory bound explicit. Each user remains its own queueable unit — independent retry, no shared failure mode across the cohort. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
54 lines
1.8 KiB
PHP
54 lines
1.8 KiB
PHP
<?php
|
|
|
|
namespace App\Jobs;
|
|
|
|
use App\Models\User;
|
|
use App\Models\UserNotificationPreference;
|
|
use App\Services\PlanFeatures;
|
|
use Illuminate\Contracts\Queue\ShouldQueue;
|
|
use Illuminate\Database\Eloquent\Collection;
|
|
use Illuminate\Foundation\Queue\Queueable;
|
|
|
|
/**
|
|
* Fan-out job for scheduled WhatsApp updates (morning / evening).
|
|
* Dispatches one DispatchUserNotificationJob per eligible user so each
|
|
* user's send is its own queueable unit (independent retry, no shared
|
|
* failure mode across the cohort).
|
|
*
|
|
* Scheduled at 07:30 (morning) and 18:00 (evening) via routes/console.php.
|
|
*/
|
|
final class SendScheduledWhatsAppJob implements ShouldQueue
|
|
{
|
|
use Queueable;
|
|
|
|
public function __construct(public readonly string $period)
|
|
{
|
|
//
|
|
}
|
|
|
|
public function handle(): void
|
|
{
|
|
$triggerType = $this->period === 'morning' ? 'scheduled_morning' : 'scheduled_evening';
|
|
|
|
// Candidates: users who have explicitly opted in to WhatsApp.
|
|
// Per-user tier + daily-limit + scheduled-updates checks happen via
|
|
// canSendNow('whatsapp'); that single call covers tier eligibility
|
|
// (canUseChannel) AND today's notification_log count.
|
|
$userIds = UserNotificationPreference::where('channel', 'whatsapp')
|
|
->where('enabled', true)
|
|
->distinct()
|
|
->pluck('user_id');
|
|
|
|
User::whereIn('id', $userIds)
|
|
->chunkById(500, function (Collection $users) use ($triggerType): void {
|
|
foreach ($users as $user) {
|
|
if (! PlanFeatures::for($user)->canSendNow('whatsapp')) {
|
|
continue;
|
|
}
|
|
|
|
DispatchUserNotificationJob::dispatch($user, $triggerType, fuelType: 'all');
|
|
}
|
|
});
|
|
}
|
|
}
|