diff --git a/app/Http/Controllers/Admin/SubscriberController.php b/app/Http/Controllers/Admin/SubscriberController.php index d296b1e..1b9a63b 100644 --- a/app/Http/Controllers/Admin/SubscriberController.php +++ b/app/Http/Controllers/Admin/SubscriberController.php @@ -8,6 +8,8 @@ use App\Http\Controllers\Controller; use App\Http\Requests\Admin\DestroySubscriberRequest; use App\Http\Requests\Admin\IndexSubscriberRequest; use App\Http\Requests\Admin\QueueMailwizzSyncRequest; +use App\Http\Requests\Admin\SyncSubscriberMailwizzRequest; +use App\Jobs\SyncSubscriberToMailwizz; use App\Models\PreregistrationPage; use App\Models\Subscriber; use App\Services\CleanupSubscriberIntegrationsService; @@ -79,6 +81,26 @@ class SubscriberController extends Controller )); } + public function syncSubscriberMailwizz( + SyncSubscriberMailwizzRequest $request, + PreregistrationPage $page, + Subscriber $subscriber + ): RedirectResponse { + $page->loadMissing('mailwizzConfig'); + + if ($page->mailwizzConfig === null) { + return redirect() + ->route('admin.pages.subscribers.index', $page) + ->with('error', __('This page has no Mailwizz integration.')); + } + + SyncSubscriberToMailwizz::dispatch($subscriber->fresh()); + + return redirect() + ->route('admin.pages.subscribers.index', $page) + ->with('status', __('Mailwizz sync has been queued for this subscriber.')); + } + public function export(IndexSubscriberRequest $request, PreregistrationPage $page): StreamedResponse { $search = $request->validated('search'); diff --git a/app/Http/Requests/Admin/SyncSubscriberMailwizzRequest.php b/app/Http/Requests/Admin/SyncSubscriberMailwizzRequest.php new file mode 100644 index 0000000..18c9bf0 --- /dev/null +++ b/app/Http/Requests/Admin/SyncSubscriberMailwizzRequest.php @@ -0,0 +1,36 @@ +route('page'); + $subscriber = $this->route('subscriber'); + if (! $page instanceof PreregistrationPage || ! $subscriber instanceof Subscriber) { + return false; + } + + if ($subscriber->preregistration_page_id !== $page->id) { + return false; + } + + return $this->user()?->can('update', $page) ?? false; + } + + /** + * @return array> + */ + public function rules(): array + { + return []; + } +} diff --git a/lang/nl.json b/lang/nl.json index 5660fc7..8276025 100644 --- a/lang/nl.json +++ b/lang/nl.json @@ -24,6 +24,9 @@ "Subscriber removed.": "Abonnee verwijderd.", "Delete this subscriber? This cannot be undone.": "Deze abonnee verwijderen? Dit kan niet ongedaan worden gemaakt.", "Remove": "Verwijderen", + "Sync Mailwizz": "Mailwizz sync", + "Mailwizz sync has been queued for this subscriber.": "Mailwizz-synchronisatie is in de wachtrij gezet voor deze abonnee.", + "Queue a Mailwizz sync for this subscriber? The tag and coupon code will be sent when the queue worker runs.": "Mailwizz-synchronisatie voor deze abonnee in de wachtrij zetten? De tag en kortingscode worden verstuurd zodra de queue-worker draait.", "Actions": "Acties", "Fix background to viewport": "Achtergrond vastzetten op het scherm", "When enabled, the background image and overlay stay fixed while visitors scroll long content.": "Als dit aan staat, blijven de achtergrondafbeelding en de overlay stilstaan terwijl bezoekers door lange inhoud scrollen." diff --git a/resources/views/admin/subscribers/index.blade.php b/resources/views/admin/subscribers/index.blade.php index f50087f..aa577e9 100644 --- a/resources/views/admin/subscribers/index.blade.php +++ b/resources/views/admin/subscribers/index.blade.php @@ -82,21 +82,39 @@ @can('update', $page) -
- @csrf - @method('DELETE') - +
+ @endif +
- {{ __('Remove') }} - -
+ @csrf + @method('DELETE') + + + @endcan diff --git a/routes/web.php b/routes/web.php index b50a9f6..36937a0 100644 --- a/routes/web.php +++ b/routes/web.php @@ -38,6 +38,7 @@ Route::middleware(['auth', 'verified'])->prefix('admin')->name('admin.')->group( Route::get('pages/{page}/subscribers/export', [SubscriberController::class, 'export'])->name('pages.subscribers.export'); Route::delete('pages/{page}/subscribers/{subscriber}', [SubscriberController::class, 'destroy'])->name('pages.subscribers.destroy'); Route::post('pages/{page}/subscribers/queue-mailwizz-sync', [SubscriberController::class, 'queueMailwizzSync'])->name('pages.subscribers.queue-mailwizz-sync'); + Route::post('pages/{page}/subscribers/{subscriber}/sync-mailwizz', [SubscriberController::class, 'syncSubscriberMailwizz'])->name('pages.subscribers.sync-mailwizz'); Route::get('pages/{page}/subscribers', [SubscriberController::class, 'index'])->name('pages.subscribers.index'); // Mailwizz configuration (nested under pages) diff --git a/tests/Feature/QueueUnsyncedMailwizzSubscribersTest.php b/tests/Feature/QueueUnsyncedMailwizzSubscribersTest.php index b7df7e7..13a059c 100644 --- a/tests/Feature/QueueUnsyncedMailwizzSubscribersTest.php +++ b/tests/Feature/QueueUnsyncedMailwizzSubscribersTest.php @@ -4,6 +4,7 @@ declare(strict_types=1); namespace Tests\Feature; +use App\Jobs\SyncSubscriberToMailwizz; use App\Models\MailwizzConfig; use App\Models\PreregistrationPage; use App\Models\Subscriber; @@ -12,6 +13,7 @@ use Illuminate\Foundation\Testing\RefreshDatabase; use Illuminate\Http\Client\Request; use Illuminate\Support\Facades\Artisan; use Illuminate\Support\Facades\Http; +use Illuminate\Support\Facades\Queue; use Illuminate\Support\Str; use Tests\TestCase; @@ -80,6 +82,135 @@ class QueueUnsyncedMailwizzSubscribersTest extends TestCase $response->assertForbidden(); } + public function test_owner_can_queue_single_subscriber_mailwizz_sync(): void + { + Queue::fake(); + + $user = User::factory()->create(['role' => 'user']); + $page = $this->makePageWithMailwizzForUser($user); + $subscriber = Subscriber::query()->create([ + 'preregistration_page_id' => $page->id, + 'first_name' => 'One', + 'last_name' => 'Off', + 'email' => 'oneoff@example.com', + 'synced_to_mailwizz' => true, + ]); + + $response = $this->actingAs($user)->post( + route('admin.pages.subscribers.sync-mailwizz', [$page, $subscriber]) + ); + + $response->assertRedirect(route('admin.pages.subscribers.index', $page)); + $response->assertSessionHas('status'); + Queue::assertPushed(SyncSubscriberToMailwizz::class, function (SyncSubscriberToMailwizz $job) use ($subscriber): bool { + return $job->subscriberId === $subscriber->id; + }); + } + + public function test_other_user_cannot_queue_single_subscriber_mailwizz_sync(): void + { + Queue::fake(); + + $owner = User::factory()->create(['role' => 'user']); + $intruder = User::factory()->create(['role' => 'user']); + $page = $this->makePageWithMailwizzForUser($owner); + $subscriber = Subscriber::query()->create([ + 'preregistration_page_id' => $page->id, + 'first_name' => 'A', + 'last_name' => 'B', + 'email' => 'ab@example.com', + ]); + + $response = $this->actingAs($intruder)->post( + route('admin.pages.subscribers.sync-mailwizz', [$page, $subscriber]) + ); + + $response->assertForbidden(); + Queue::assertNothingPushed(); + } + + public function test_single_subscriber_mailwizz_sync_redirects_with_error_when_page_has_no_mailwizz(): void + { + Queue::fake(); + + $user = User::factory()->create(['role' => 'user']); + $page = PreregistrationPage::query()->create([ + 'slug' => (string) Str::uuid(), + 'user_id' => $user->id, + 'title' => 'Fest', + 'heading' => 'Join', + 'intro_text' => null, + 'thank_you_message' => null, + 'expired_message' => null, + 'ticketshop_url' => null, + 'start_date' => now()->subHour(), + 'end_date' => now()->addMonth(), + 'phone_enabled' => false, + 'is_active' => true, + ]); + $subscriber = Subscriber::query()->create([ + 'preregistration_page_id' => $page->id, + 'first_name' => 'A', + 'last_name' => 'B', + 'email' => 'nomw@example.com', + ]); + + $response = $this->actingAs($user)->post( + route('admin.pages.subscribers.sync-mailwizz', [$page, $subscriber]) + ); + + $response->assertRedirect(route('admin.pages.subscribers.index', $page)); + $response->assertSessionHas('error'); + Queue::assertNothingPushed(); + } + + public function test_cannot_queue_single_subscriber_mailwizz_sync_with_mismatched_page(): void + { + Queue::fake(); + + $user = User::factory()->create(['role' => 'user']); + $pageA = $this->makePageWithMailwizzForUser($user); + $pageB = PreregistrationPage::query()->create([ + 'slug' => (string) Str::uuid(), + 'user_id' => $user->id, + 'title' => 'Other', + 'heading' => 'Other', + 'intro_text' => null, + 'thank_you_message' => null, + 'expired_message' => null, + 'ticketshop_url' => null, + 'start_date' => now()->subHour(), + 'end_date' => now()->addMonth(), + 'phone_enabled' => false, + 'is_active' => true, + ]); + MailwizzConfig::query()->create([ + 'preregistration_page_id' => $pageB->id, + 'api_key' => 'fake-api-key', + 'list_uid' => 'list-uid-2', + 'list_name' => 'List B', + 'field_email' => 'EMAIL', + 'field_first_name' => 'FNAME', + 'field_last_name' => 'LNAME', + 'field_phone' => null, + 'tag_field' => 'TAGS', + 'tag_value' => 'b-source', + ]); + $subscriber = Subscriber::query()->create([ + 'preregistration_page_id' => $pageB->id, + 'first_name' => 'A', + 'last_name' => 'B', + 'email' => 'on-b@example.com', + ]); + + $response = $this->actingAs($user)->post( + route('admin.pages.subscribers.sync-mailwizz', [$pageA, $subscriber]) + ); + + $response->assertForbidden(); + Queue::assertNothingPushed(); + } + private function makePageWithMailwizz(): PreregistrationPage { $user = User::factory()->create(['role' => 'user']);