feat: automate guest notification triggers
This commit is contained in:
64
app/Console/Commands/SendGuestFeedbackReminders.php
Normal file
64
app/Console/Commands/SendGuestFeedbackReminders.php
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Console\Commands;
|
||||||
|
|
||||||
|
use App\Enums\GuestNotificationAudience;
|
||||||
|
use App\Enums\GuestNotificationType;
|
||||||
|
use App\Models\Event;
|
||||||
|
use App\Services\GuestNotificationService;
|
||||||
|
use Illuminate\Console\Command;
|
||||||
|
|
||||||
|
class SendGuestFeedbackReminders extends Command
|
||||||
|
{
|
||||||
|
protected $signature = 'guest:feedback-reminders {--hours=4 : Minimum hours after event date before reminding} {--limit=50 : Maximum events to process}';
|
||||||
|
|
||||||
|
protected $description = 'Send feedback reminder notifications to guests of recently finished events';
|
||||||
|
|
||||||
|
public function __construct(private readonly GuestNotificationService $notifications)
|
||||||
|
{
|
||||||
|
parent::__construct();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function handle(): int
|
||||||
|
{
|
||||||
|
$hours = max(0, (int) $this->option('hours'));
|
||||||
|
$limit = max(1, (int) $this->option('limit'));
|
||||||
|
$cutoff = now()->subHours($hours);
|
||||||
|
|
||||||
|
$events = Event::query()
|
||||||
|
->where('status', 'published')
|
||||||
|
->where('is_active', true)
|
||||||
|
->whereNotNull('date')
|
||||||
|
->where('date', '<=', $cutoff)
|
||||||
|
->whereDoesntHave('guestNotifications', function ($query) {
|
||||||
|
$query->where('type', GuestNotificationType::FEEDBACK_REQUEST->value);
|
||||||
|
})
|
||||||
|
->orderBy('date')
|
||||||
|
->limit($limit)
|
||||||
|
->get();
|
||||||
|
|
||||||
|
$count = 0;
|
||||||
|
|
||||||
|
foreach ($events as $event) {
|
||||||
|
$title = 'Danke fürs Mitmachen – wie war dein Erlebnis?';
|
||||||
|
$body = 'Teile dein kurzes Feedback mit dem Gastgeber oder lade deine Lieblingsmomente noch einmal hoch.';
|
||||||
|
|
||||||
|
$this->notifications->createNotification(
|
||||||
|
$event,
|
||||||
|
GuestNotificationType::FEEDBACK_REQUEST,
|
||||||
|
$title,
|
||||||
|
$body,
|
||||||
|
[
|
||||||
|
'audience_scope' => GuestNotificationAudience::ALL,
|
||||||
|
'expires_at' => now()->addHours(12),
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
$count++;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->info(sprintf('Feedback reminders dispatched for %d event(s).', $count));
|
||||||
|
|
||||||
|
return Command::SUCCESS;
|
||||||
|
}
|
||||||
|
}
|
||||||
22
app/Events/GuestPhotoUploaded.php
Normal file
22
app/Events/GuestPhotoUploaded.php
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Events;
|
||||||
|
|
||||||
|
use App\Models\Event;
|
||||||
|
use Illuminate\Broadcasting\InteractsWithSockets;
|
||||||
|
use Illuminate\Foundation\Events\Dispatchable;
|
||||||
|
use Illuminate\Queue\SerializesModels;
|
||||||
|
|
||||||
|
class GuestPhotoUploaded
|
||||||
|
{
|
||||||
|
use Dispatchable;
|
||||||
|
use InteractsWithSockets;
|
||||||
|
use SerializesModels;
|
||||||
|
|
||||||
|
public function __construct(
|
||||||
|
public Event $event,
|
||||||
|
public int $photoId,
|
||||||
|
public string $guestIdentifier,
|
||||||
|
public ?string $guestName = null,
|
||||||
|
) {}
|
||||||
|
}
|
||||||
@@ -5,6 +5,8 @@ namespace App\Http\Controllers\Api;
|
|||||||
use App\Enums\GuestNotificationAudience;
|
use App\Enums\GuestNotificationAudience;
|
||||||
use App\Enums\GuestNotificationDeliveryStatus;
|
use App\Enums\GuestNotificationDeliveryStatus;
|
||||||
use App\Enums\GuestNotificationState;
|
use App\Enums\GuestNotificationState;
|
||||||
|
use App\Enums\GuestNotificationType;
|
||||||
|
use App\Events\GuestPhotoUploaded;
|
||||||
use App\Models\Event;
|
use App\Models\Event;
|
||||||
use App\Models\EventJoinToken;
|
use App\Models\EventJoinToken;
|
||||||
use App\Models\EventMediaAsset;
|
use App\Models\EventMediaAsset;
|
||||||
@@ -1765,6 +1767,62 @@ class EventPublicController extends BaseController
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function notifyDeviceUploadLimit(Event $event, string $guestIdentifier, string $token): void
|
||||||
|
{
|
||||||
|
$title = 'Upload-Limit erreicht';
|
||||||
|
$body = 'Du hast bereits 50 Fotos hochgeladen. Wir speichern deine Warteschlange – bitte lösche zuerst alte Uploads.';
|
||||||
|
|
||||||
|
$this->guestNotificationService->createNotification(
|
||||||
|
$event,
|
||||||
|
GuestNotificationType::UPLOAD_ALERT,
|
||||||
|
$title,
|
||||||
|
$body,
|
||||||
|
[
|
||||||
|
'audience_scope' => GuestNotificationAudience::GUEST,
|
||||||
|
'target_identifier' => $guestIdentifier,
|
||||||
|
'payload' => [
|
||||||
|
'cta' => [
|
||||||
|
'label' => 'Warteschlange öffnen',
|
||||||
|
'href' => sprintf('/e/%s/queue', urlencode($token)),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
'expires_at' => now()->addHours(6),
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function notifyPhotoLimitReached(Event $event): void
|
||||||
|
{
|
||||||
|
$title = 'Uploads pausiert – Event-Limit erreicht';
|
||||||
|
|
||||||
|
if ($this->eventHasActiveNotification($event, GuestNotificationType::UPLOAD_ALERT, $title)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$body = 'Es können aktuell keine neuen Fotos hochgeladen werden. Wir informieren den Host – bleib kurz dran!';
|
||||||
|
|
||||||
|
$this->guestNotificationService->createNotification(
|
||||||
|
$event,
|
||||||
|
GuestNotificationType::UPLOAD_ALERT,
|
||||||
|
$title,
|
||||||
|
$body,
|
||||||
|
[
|
||||||
|
'audience_scope' => GuestNotificationAudience::ALL,
|
||||||
|
'expires_at' => now()->addHours(3),
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function eventHasActiveNotification(Event $event, GuestNotificationType $type, string $title): bool
|
||||||
|
{
|
||||||
|
return GuestNotification::query()
|
||||||
|
->where('event_id', $event->id)
|
||||||
|
->where('type', $type->value)
|
||||||
|
->where('title', $title)
|
||||||
|
->where('status', GuestNotificationState::ACTIVE->value)
|
||||||
|
->exists();
|
||||||
|
}
|
||||||
|
|
||||||
private function resolveNotificationIdentifier(Request $request): string
|
private function resolveNotificationIdentifier(Request $request): string
|
||||||
{
|
{
|
||||||
$identifier = $this->determineGuestIdentifier($request);
|
$identifier = $this->determineGuestIdentifier($request);
|
||||||
@@ -2142,6 +2200,10 @@ class EventPublicController extends BaseController
|
|||||||
|
|
||||||
$violation = $this->packageLimitEvaluator->assessPhotoUpload($tenantModel, $eventId, $eventModel);
|
$violation = $this->packageLimitEvaluator->assessPhotoUpload($tenantModel, $eventId, $eventModel);
|
||||||
if ($violation !== null) {
|
if ($violation !== null) {
|
||||||
|
if (($violation['code'] ?? null) === 'photo_limit_exceeded') {
|
||||||
|
$this->notifyPhotoLimitReached($eventModel);
|
||||||
|
}
|
||||||
|
|
||||||
return ApiError::response(
|
return ApiError::response(
|
||||||
$violation['code'],
|
$violation['code'],
|
||||||
$violation['title'],
|
$violation['title'],
|
||||||
@@ -2160,6 +2222,8 @@ class EventPublicController extends BaseController
|
|||||||
// Per-device cap per event (MVP: 50)
|
// Per-device cap per event (MVP: 50)
|
||||||
$deviceCount = DB::table('photos')->where('event_id', $eventId)->where('guest_name', $deviceId)->count();
|
$deviceCount = DB::table('photos')->where('event_id', $eventId)->where('guest_name', $deviceId)->count();
|
||||||
if ($deviceCount >= 50) {
|
if ($deviceCount >= 50) {
|
||||||
|
$this->notifyDeviceUploadLimit($eventModel, $deviceId, $token);
|
||||||
|
|
||||||
$this->recordTokenEvent(
|
$this->recordTokenEvent(
|
||||||
$joinToken,
|
$joinToken,
|
||||||
$request,
|
$request,
|
||||||
@@ -2282,6 +2346,13 @@ class EventPublicController extends BaseController
|
|||||||
Response::HTTP_CREATED
|
Response::HTTP_CREATED
|
||||||
);
|
);
|
||||||
|
|
||||||
|
event(new GuestPhotoUploaded(
|
||||||
|
$eventModel,
|
||||||
|
$photoId,
|
||||||
|
$deviceId,
|
||||||
|
$validated['guest_name'] ?? null,
|
||||||
|
));
|
||||||
|
|
||||||
return $response;
|
return $response;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,88 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Listeners\GuestNotifications;
|
||||||
|
|
||||||
|
use App\Enums\GuestNotificationAudience;
|
||||||
|
use App\Enums\GuestNotificationType;
|
||||||
|
use App\Events\GuestPhotoUploaded;
|
||||||
|
use App\Models\Photo;
|
||||||
|
use App\Services\GuestNotificationService;
|
||||||
|
|
||||||
|
class SendPhotoUploadedNotification
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @param int[] $milestones
|
||||||
|
*/
|
||||||
|
public function __construct(
|
||||||
|
private readonly GuestNotificationService $notifications,
|
||||||
|
private readonly array $milestones = [5, 10, 20]
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public function handle(GuestPhotoUploaded $event): void
|
||||||
|
{
|
||||||
|
$guestLabel = $this->resolveGuestLabel($event->guestName, $event->guestIdentifier);
|
||||||
|
$title = $guestLabel
|
||||||
|
? sprintf('%s hat gerade ein Foto gemacht 🎉', $guestLabel)
|
||||||
|
: 'Es gibt neue Fotos!';
|
||||||
|
|
||||||
|
$this->notifications->createNotification(
|
||||||
|
$event->event,
|
||||||
|
GuestNotificationType::PHOTO_ACTIVITY,
|
||||||
|
$title,
|
||||||
|
null,
|
||||||
|
[
|
||||||
|
'audience_scope' => GuestNotificationAudience::ALL,
|
||||||
|
'payload' => [
|
||||||
|
'photo_id' => $event->photoId,
|
||||||
|
],
|
||||||
|
'expires_at' => now()->addHours(3),
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->maybeCreateMilestoneNotification($event, $guestLabel);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function maybeCreateMilestoneNotification(GuestPhotoUploaded $event, ?string $guestLabel): void
|
||||||
|
{
|
||||||
|
if (! $guestLabel) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$count = Photo::where('event_id', $event->event->id)
|
||||||
|
->where('guest_name', $event->guestIdentifier)
|
||||||
|
->count();
|
||||||
|
|
||||||
|
if (! in_array($count, $this->milestones, true)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$title = sprintf('%s hat ein neues Achievement erreicht 🏅', $guestLabel);
|
||||||
|
$body = sprintf('%s hat jetzt %d Fotos beigetragen – weiter so!', $guestLabel, $count);
|
||||||
|
|
||||||
|
$this->notifications->createNotification(
|
||||||
|
$event->event,
|
||||||
|
GuestNotificationType::ACHIEVEMENT_MAJOR,
|
||||||
|
$title,
|
||||||
|
$body,
|
||||||
|
[
|
||||||
|
'audience_scope' => GuestNotificationAudience::ALL,
|
||||||
|
'expires_at' => now()->addDay(),
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function resolveGuestLabel(?string $guestName, string $guestIdentifier): ?string
|
||||||
|
{
|
||||||
|
$name = $guestName && trim($guestName) !== '' ? trim($guestName) : null;
|
||||||
|
|
||||||
|
if ($name) {
|
||||||
|
return $name;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($guestIdentifier === 'anon') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $guestIdentifier;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
namespace App\Providers;
|
namespace App\Providers;
|
||||||
|
|
||||||
|
use App\Events\GuestPhotoUploaded;
|
||||||
use App\Events\Packages\EventPackageGalleryExpired;
|
use App\Events\Packages\EventPackageGalleryExpired;
|
||||||
use App\Events\Packages\EventPackageGalleryExpiring;
|
use App\Events\Packages\EventPackageGalleryExpiring;
|
||||||
use App\Events\Packages\EventPackageGuestLimitReached;
|
use App\Events\Packages\EventPackageGuestLimitReached;
|
||||||
@@ -13,6 +14,7 @@ use App\Events\Packages\TenantPackageEventLimitReached;
|
|||||||
use App\Events\Packages\TenantPackageEventThresholdReached;
|
use App\Events\Packages\TenantPackageEventThresholdReached;
|
||||||
use App\Events\Packages\TenantPackageExpired;
|
use App\Events\Packages\TenantPackageExpired;
|
||||||
use App\Events\Packages\TenantPackageExpiring;
|
use App\Events\Packages\TenantPackageExpiring;
|
||||||
|
use App\Listeners\GuestNotifications\SendPhotoUploadedNotification;
|
||||||
use App\Listeners\Packages\QueueGalleryExpiredNotification;
|
use App\Listeners\Packages\QueueGalleryExpiredNotification;
|
||||||
use App\Listeners\Packages\QueueGalleryWarningNotification;
|
use App\Listeners\Packages\QueueGalleryWarningNotification;
|
||||||
use App\Listeners\Packages\QueueGuestLimitNotification;
|
use App\Listeners\Packages\QueueGuestLimitNotification;
|
||||||
@@ -118,6 +120,11 @@ class AppServiceProvider extends ServiceProvider
|
|||||||
[QueueTenantCreditsLowNotification::class, 'handle']
|
[QueueTenantCreditsLowNotification::class, 'handle']
|
||||||
);
|
);
|
||||||
|
|
||||||
|
EventFacade::listen(
|
||||||
|
GuestPhotoUploaded::class,
|
||||||
|
[SendPhotoUploadedNotification::class, 'handle']
|
||||||
|
);
|
||||||
|
|
||||||
RateLimiter::for('tenant-api', function (Request $request) {
|
RateLimiter::for('tenant-api', function (Request $request) {
|
||||||
$tenantId = $request->attributes->get('tenant_id')
|
$tenantId = $request->attributes->get('tenant_id')
|
||||||
?? $request->user()?->tenant_id
|
?? $request->user()?->tenant_id
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ return Application::configure(basePath: dirname(__DIR__))
|
|||||||
\App\Console\Commands\CheckUploadQueuesCommand::class,
|
\App\Console\Commands\CheckUploadQueuesCommand::class,
|
||||||
\App\Console\Commands\PurgeExpiredDataExports::class,
|
\App\Console\Commands\PurgeExpiredDataExports::class,
|
||||||
\App\Console\Commands\ProcessTenantRetention::class,
|
\App\Console\Commands\ProcessTenantRetention::class,
|
||||||
|
\App\Console\Commands\SendGuestFeedbackReminders::class,
|
||||||
])
|
])
|
||||||
->withSchedule(function (\Illuminate\Console\Scheduling\Schedule $schedule) {
|
->withSchedule(function (\Illuminate\Console\Scheduling\Schedule $schedule) {
|
||||||
$schedule->command('package:check-status')->dailyAt('06:00');
|
$schedule->command('package:check-status')->dailyAt('06:00');
|
||||||
@@ -36,6 +37,7 @@ return Application::configure(basePath: dirname(__DIR__))
|
|||||||
$schedule->command('photobooth:ingest')->everyFiveMinutes()->withoutOverlapping();
|
$schedule->command('photobooth:ingest')->everyFiveMinutes()->withoutOverlapping();
|
||||||
$schedule->command('exports:purge')->dailyAt('02:00');
|
$schedule->command('exports:purge')->dailyAt('02:00');
|
||||||
$schedule->command('tenants:retention-scan')->dailyAt('03:00');
|
$schedule->command('tenants:retention-scan')->dailyAt('03:00');
|
||||||
|
$schedule->command('guest:feedback-reminders')->dailyAt('22:00');
|
||||||
})
|
})
|
||||||
->withMiddleware(function (Middleware $middleware) {
|
->withMiddleware(function (Middleware $middleware) {
|
||||||
$middleware->alias([
|
$middleware->alias([
|
||||||
|
|||||||
@@ -9,8 +9,10 @@ use App\Models\Event;
|
|||||||
use App\Models\EventPackage;
|
use App\Models\EventPackage;
|
||||||
use App\Models\MediaStorageTarget;
|
use App\Models\MediaStorageTarget;
|
||||||
use App\Models\Package;
|
use App\Models\Package;
|
||||||
|
use App\Models\Photo;
|
||||||
use App\Models\Tenant;
|
use App\Models\Tenant;
|
||||||
use App\Services\EventJoinTokenService;
|
use App\Services\EventJoinTokenService;
|
||||||
|
use App\Services\Packages\PackageLimitEvaluator;
|
||||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||||
use Illuminate\Http\UploadedFile;
|
use Illuminate\Http\UploadedFile;
|
||||||
use Illuminate\Support\Facades\Bus;
|
use Illuminate\Support\Facades\Bus;
|
||||||
@@ -82,6 +84,12 @@ class EventGuestUploadLimitTest extends TestCase
|
|||||||
$response->assertJsonPath('error.code', 'photo_limit_exceeded');
|
$response->assertJsonPath('error.code', 'photo_limit_exceeded');
|
||||||
|
|
||||||
Bus::assertNothingDispatched();
|
Bus::assertNothingDispatched();
|
||||||
|
|
||||||
|
$this->assertDatabaseHas('guest_notifications', [
|
||||||
|
'event_id' => $event->id,
|
||||||
|
'type' => 'upload_alert',
|
||||||
|
'audience_scope' => 'all',
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function test_guest_upload_increments_usage_and_succeeds(): void
|
public function test_guest_upload_increments_usage_and_succeeds(): void
|
||||||
@@ -167,4 +175,81 @@ class EventGuestUploadLimitTest extends TestCase
|
|||||||
$response->assertJsonPath('limits.gallery.state', 'warning');
|
$response->assertJsonPath('limits.gallery.state', 'warning');
|
||||||
$response->assertJsonPath('limits.can_upload_photos', true);
|
$response->assertJsonPath('limits.can_upload_photos', true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function test_device_limit_creates_targeted_notification(): void
|
||||||
|
{
|
||||||
|
$tenant = Tenant::factory()->create();
|
||||||
|
$event = Event::factory()->for($tenant)->create([
|
||||||
|
'status' => 'published',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$mock = \Mockery::mock(PackageLimitEvaluator::class);
|
||||||
|
$mock->shouldReceive('assessPhotoUpload')->andReturn(null);
|
||||||
|
$mock->shouldReceive('resolveEventPackageForPhotoUpload')->andReturn(null);
|
||||||
|
$this->instance(PackageLimitEvaluator::class, $mock);
|
||||||
|
|
||||||
|
Photo::factory()->count(50)->for($event)->create([
|
||||||
|
'guest_name' => 'device-abc',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$emotion = Emotion::factory()->create();
|
||||||
|
$emotion->eventTypes()->attach($event->event_type_id);
|
||||||
|
|
||||||
|
$token = app(EventJoinTokenService::class)->createToken($event)->plain_token;
|
||||||
|
|
||||||
|
$response = $this->post("/api/v1/events/{$token}/upload", [
|
||||||
|
'photo' => UploadedFile::fake()->image('limit-device.jpg', 800, 600),
|
||||||
|
], [
|
||||||
|
'X-Device-Id' => 'device-abc',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$response->assertStatus(429);
|
||||||
|
|
||||||
|
$this->assertDatabaseHas('guest_notifications', [
|
||||||
|
'event_id' => $event->id,
|
||||||
|
'type' => 'upload_alert',
|
||||||
|
'target_identifier' => 'device-abc',
|
||||||
|
'audience_scope' => 'guest',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_photo_limit_violation_creates_notification(): void
|
||||||
|
{
|
||||||
|
$tenant = Tenant::factory()->create();
|
||||||
|
$event = Event::factory()->for($tenant)->create([
|
||||||
|
'status' => 'published',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$violation = [
|
||||||
|
'code' => 'photo_limit_exceeded',
|
||||||
|
'title' => 'Photo upload limit reached',
|
||||||
|
'message' => 'Limit reached',
|
||||||
|
'status' => 402,
|
||||||
|
'meta' => ['scope' => 'photos'],
|
||||||
|
];
|
||||||
|
|
||||||
|
$mock = \Mockery::mock(PackageLimitEvaluator::class);
|
||||||
|
$mock->shouldReceive('assessPhotoUpload')->andReturn($violation);
|
||||||
|
$mock->shouldReceive('resolveEventPackageForPhotoUpload')->andReturn(null);
|
||||||
|
$this->instance(PackageLimitEvaluator::class, $mock);
|
||||||
|
|
||||||
|
$emotion = Emotion::factory()->create();
|
||||||
|
$emotion->eventTypes()->attach($event->event_type_id);
|
||||||
|
|
||||||
|
$token = app(EventJoinTokenService::class)->createToken($event)->plain_token;
|
||||||
|
|
||||||
|
$response = $this->post("/api/v1/events/{$token}/upload", [
|
||||||
|
'photo' => UploadedFile::fake()->image('limit.jpg', 800, 600),
|
||||||
|
], [
|
||||||
|
'X-Device-Id' => 'device-xyz',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$response->assertStatus(402);
|
||||||
|
|
||||||
|
$this->assertDatabaseHas('guest_notifications', [
|
||||||
|
'event_id' => $event->id,
|
||||||
|
'type' => 'upload_alert',
|
||||||
|
'audience_scope' => 'all',
|
||||||
|
]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
35
tests/Feature/Console/SendGuestFeedbackRemindersTest.php
Normal file
35
tests/Feature/Console/SendGuestFeedbackRemindersTest.php
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Tests\Feature\Console;
|
||||||
|
|
||||||
|
use App\Models\Event;
|
||||||
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||||
|
use Tests\TestCase;
|
||||||
|
|
||||||
|
class SendGuestFeedbackRemindersTest extends TestCase
|
||||||
|
{
|
||||||
|
use RefreshDatabase;
|
||||||
|
|
||||||
|
public function test_command_creates_feedback_notifications_for_past_events(): void
|
||||||
|
{
|
||||||
|
$event = Event::factory()->create([
|
||||||
|
'status' => 'published',
|
||||||
|
'is_active' => true,
|
||||||
|
'date' => now()->subDay(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->artisan('guest:feedback-reminders', ['--hours' => 0])
|
||||||
|
->assertExitCode(0);
|
||||||
|
|
||||||
|
$this->assertDatabaseHas('guest_notifications', [
|
||||||
|
'event_id' => $event->id,
|
||||||
|
'type' => 'feedback_request',
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Running again should not duplicate notifications
|
||||||
|
$this->artisan('guest:feedback-reminders', ['--hours' => 0])
|
||||||
|
->assertExitCode(0);
|
||||||
|
|
||||||
|
$this->assertDatabaseCount('guest_notifications', 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Tests\Feature\Listeners;
|
||||||
|
|
||||||
|
use App\Enums\GuestNotificationType;
|
||||||
|
use App\Events\GuestPhotoUploaded;
|
||||||
|
use App\Models\Event;
|
||||||
|
use App\Models\Photo;
|
||||||
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||||
|
use Tests\TestCase;
|
||||||
|
|
||||||
|
class SendPhotoUploadedNotificationTest extends TestCase
|
||||||
|
{
|
||||||
|
use RefreshDatabase;
|
||||||
|
|
||||||
|
public function test_photo_upload_event_creates_notification(): void
|
||||||
|
{
|
||||||
|
$event = Event::factory()->create(['status' => 'published']);
|
||||||
|
$photo = Photo::factory()->for($event)->create(['guest_name' => 'Anne']);
|
||||||
|
|
||||||
|
event(new GuestPhotoUploaded($event->refresh(), $photo->id, 'Anne', 'Anne'));
|
||||||
|
|
||||||
|
$this->assertDatabaseHas('guest_notifications', [
|
||||||
|
'event_id' => $event->id,
|
||||||
|
'type' => GuestNotificationType::PHOTO_ACTIVITY->value,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_milestone_creates_achievement_notification(): void
|
||||||
|
{
|
||||||
|
$event = Event::factory()->create(['status' => 'published']);
|
||||||
|
Photo::factory()->count(4)->for($event)->create(['guest_name' => 'Mia']);
|
||||||
|
$photo = Photo::factory()->for($event)->create(['guest_name' => 'Mia']);
|
||||||
|
|
||||||
|
event(new GuestPhotoUploaded($event->refresh(), $photo->id, 'Mia', 'Mia'));
|
||||||
|
|
||||||
|
$this->assertDatabaseHas('guest_notifications', [
|
||||||
|
'event_id' => $event->id,
|
||||||
|
'type' => GuestNotificationType::ACHIEVEMENT_MAJOR->value,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user