Implement tenant announcements and audit log fixes
This commit is contained in:
234
app/Services/TenantAnnouncements/TenantAnnouncementService.php
Normal file
234
app/Services/TenantAnnouncements/TenantAnnouncementService.php
Normal file
@@ -0,0 +1,234 @@
|
||||
<?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 !== ''));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user