activateScheduled(); $archived = $this->archiveExpired(); $queued = $this->dispatchActiveEmails(); return [ 'activated' => $activated, 'archived' => $archived, 'queued' => $queued, ]; } public function visibleAnnouncementsForTenant(Tenant $tenant): Collection { $baseQuery = TenantAnnouncement::query()->active(); $all = (clone $baseQuery) ->where('audience', TenantAnnouncementAudience::ALL->value) ->get(); $tenants = (clone $baseQuery) ->where('audience', TenantAnnouncementAudience::TENANTS->value) ->whereHas('tenants', fn (Builder $query) => $query->where('tenant_id', $tenant->id)) ->get(); $segments = (clone $baseQuery) ->where('audience', TenantAnnouncementAudience::SEGMENTS->value) ->get() ->filter(function (TenantAnnouncement $announcement) use ($tenant): bool { $segments = $this->normalizeSegments($announcement->segments ?? []); return $announcement->matchesSegments($tenant, $segments); }); return $all ->merge($tenants) ->merge($segments) ->sortByDesc(fn (TenantAnnouncement $announcement) => $announcement->starts_at ?? $announcement->created_at) ->values(); } private function activateScheduled(): int { return TenantAnnouncement::query() ->where('status', TenantAnnouncementStatus::SCHEDULED->value) ->where(function (Builder $builder): void { $builder->whereNull('starts_at') ->orWhere('starts_at', '<=', Carbon::now()); }) ->update([ 'status' => TenantAnnouncementStatus::ACTIVE->value, 'updated_at' => now(), ]); } private function archiveExpired(): int { return TenantAnnouncement::query() ->where('status', TenantAnnouncementStatus::ACTIVE->value) ->whereNotNull('ends_at') ->where('ends_at', '<', Carbon::now()) ->update([ 'status' => TenantAnnouncementStatus::ARCHIVED->value, 'updated_at' => now(), ]); } private function dispatchActiveEmails(): int { $announcements = TenantAnnouncement::query() ->active() ->where('email_enabled', true) ->get(); $queued = 0; foreach ($announcements as $announcement) { $queued += $this->dispatchEmailsForAnnouncement($announcement); } return $queued; } private function dispatchEmailsForAnnouncement(TenantAnnouncement $announcement): int { $tenants = $this->targetTenants($announcement); if ($tenants->isEmpty()) { return 0; } $deliveredTenantIds = TenantAnnouncementDelivery::query() ->where('tenant_announcement_id', $announcement->id) ->where('channel', 'mail') ->pluck('tenant_id') ->all(); $queued = 0; foreach ($tenants as $tenant) { if (in_array($tenant->id, $deliveredTenantIds, true)) { continue; } $queued += $this->queueDelivery($announcement, $tenant); } return $queued; } private function queueDelivery(TenantAnnouncement $announcement, Tenant $tenant): int { $recipient = $this->resolveRecipient($tenant); if (! $recipient) { TenantAnnouncementDelivery::query()->create([ 'tenant_announcement_id' => $announcement->id, 'tenant_id' => $tenant->id, 'channel' => 'mail', 'status' => TenantAnnouncementDeliveryStatus::SKIPPED->value, 'failed_at' => now(), 'failure_reason' => 'missing_recipient', ]); return 0; } try { Notification::route('mail', $recipient) ->notify(new TenantAnnouncementNotification($announcement, $tenant)); TenantAnnouncementDelivery::query()->create([ 'tenant_announcement_id' => $announcement->id, 'tenant_id' => $tenant->id, 'channel' => 'mail', 'status' => TenantAnnouncementDeliveryStatus::QUEUED->value, 'sent_at' => now(), ]); return 1; } catch (Throwable $exception) { TenantAnnouncementDelivery::query()->create([ 'tenant_announcement_id' => $announcement->id, 'tenant_id' => $tenant->id, 'channel' => 'mail', 'status' => TenantAnnouncementDeliveryStatus::FAILED->value, 'failed_at' => now(), 'failure_reason' => class_basename($exception), ]); } return 0; } private function resolveRecipient(Tenant $tenant): ?string { $email = $tenant->contact_email ?: $tenant->user?->email; return $email ?: null; } private function targetTenants(TenantAnnouncement $announcement): Collection { $query = Tenant::query()->with('user'); if ($announcement->audience === TenantAnnouncementAudience::TENANTS) { $query->whereHas('announcements', fn (Builder $builder) => $builder->where('tenant_announcements.id', $announcement->id)); } if ($announcement->audience === TenantAnnouncementAudience::SEGMENTS) { $this->applySegmentFilters($query, $this->normalizeSegments($announcement->segments ?? [])); } return $query->get(); } /** * @param array $segments */ private function applySegmentFilters(Builder $query, array $segments): void { foreach ($segments as $segment) { $normalized = $segment instanceof TenantAnnouncementSegment ? $segment : TenantAnnouncementSegment::tryFrom((string) $segment); if (! $normalized) { continue; } match ($normalized) { TenantAnnouncementSegment::ACTIVE_PACKAGE => $query->whereHas('activeResellerPackage'), TenantAnnouncementSegment::ACTIVE_STATUS => $query ->where('is_active', true) ->where('is_suspended', false) ->whereNull('pending_deletion_at') ->whereNull('anonymized_at'), }; } } /** * @param array $segments * @return array */ private function normalizeSegments(array $segments): array { return array_values(array_filter($segments, fn ($segment) => $segment !== null && $segment !== '')); } }