Files
fotospiel-app/app/Services/TenantAnnouncements/TenantAnnouncementService.php
Codex Agent 8f13465415
Some checks failed
linter / quality (push) Has been cancelled
tests / ci (push) Has been cancelled
tests / ui (push) Has been cancelled
Implement tenant announcements and audit log fixes
2026-01-02 14:19:46 +01:00

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 !== ''));
}
}