235 lines
7.5 KiB
PHP
235 lines
7.5 KiB
PHP
<?php
|
|
|
|
namespace App\Services\TenantAnnouncements;
|
|
|
|
use App\Enums\TenantAnnouncementAudience;
|
|
use App\Enums\TenantAnnouncementDeliveryStatus;
|
|
use App\Enums\TenantAnnouncementSegment;
|
|
use App\Enums\TenantAnnouncementStatus;
|
|
use App\Models\Tenant;
|
|
use App\Models\TenantAnnouncement;
|
|
use App\Models\TenantAnnouncementDelivery;
|
|
use App\Notifications\TenantAnnouncementNotification;
|
|
use Illuminate\Database\Eloquent\Builder;
|
|
use Illuminate\Support\Carbon;
|
|
use Illuminate\Support\Collection;
|
|
use Illuminate\Support\Facades\Notification;
|
|
use Throwable;
|
|
|
|
class TenantAnnouncementService
|
|
{
|
|
/**
|
|
* @return array{activated: int, archived: int, queued: int}
|
|
*/
|
|
public function process(): array
|
|
{
|
|
$activated = $this->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<int, TenantAnnouncementSegment|string> $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<int, mixed> $segments
|
|
* @return array<int, TenantAnnouncementSegment|string>
|
|
*/
|
|
private function normalizeSegments(array $segments): array
|
|
{
|
|
return array_values(array_filter($segments, fn ($segment) => $segment !== null && $segment !== ''));
|
|
}
|
|
}
|