From 845665c8be45315b5609ede8c804778c8c72bf3d Mon Sep 17 00:00:00 2001 From: "bert.hausmans" Date: Sun, 5 Apr 2026 13:45:30 +0200 Subject: [PATCH] feat: per-subscriber Mailwizz sync button on admin list Add POST route and form request to queue SyncSubscriberToMailwizz for one subscriber when the page has Mailwizz configured. Include Dutch strings and feature tests for auth and edge cases. Made-with: Cursor --- .../Admin/SubscriberController.php | 22 +++ .../Admin/SyncSubscriberMailwizzRequest.php | 36 +++++ lang/nl.json | 3 + .../views/admin/subscribers/index.blade.php | 46 ++++-- routes/web.php | 1 + .../QueueUnsyncedMailwizzSubscribersTest.php | 131 ++++++++++++++++++ 6 files changed, 225 insertions(+), 14 deletions(-) create mode 100644 app/Http/Requests/Admin/SyncSubscriberMailwizzRequest.php 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']);