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
This commit is contained in:
@@ -8,6 +8,8 @@ use App\Http\Controllers\Controller;
|
|||||||
use App\Http\Requests\Admin\DestroySubscriberRequest;
|
use App\Http\Requests\Admin\DestroySubscriberRequest;
|
||||||
use App\Http\Requests\Admin\IndexSubscriberRequest;
|
use App\Http\Requests\Admin\IndexSubscriberRequest;
|
||||||
use App\Http\Requests\Admin\QueueMailwizzSyncRequest;
|
use App\Http\Requests\Admin\QueueMailwizzSyncRequest;
|
||||||
|
use App\Http\Requests\Admin\SyncSubscriberMailwizzRequest;
|
||||||
|
use App\Jobs\SyncSubscriberToMailwizz;
|
||||||
use App\Models\PreregistrationPage;
|
use App\Models\PreregistrationPage;
|
||||||
use App\Models\Subscriber;
|
use App\Models\Subscriber;
|
||||||
use App\Services\CleanupSubscriberIntegrationsService;
|
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
|
public function export(IndexSubscriberRequest $request, PreregistrationPage $page): StreamedResponse
|
||||||
{
|
{
|
||||||
$search = $request->validated('search');
|
$search = $request->validated('search');
|
||||||
|
|||||||
36
app/Http/Requests/Admin/SyncSubscriberMailwizzRequest.php
Normal file
36
app/Http/Requests/Admin/SyncSubscriberMailwizzRequest.php
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Http\Requests\Admin;
|
||||||
|
|
||||||
|
use App\Models\PreregistrationPage;
|
||||||
|
use App\Models\Subscriber;
|
||||||
|
use Illuminate\Contracts\Validation\ValidationRule;
|
||||||
|
use Illuminate\Foundation\Http\FormRequest;
|
||||||
|
|
||||||
|
class SyncSubscriberMailwizzRequest extends FormRequest
|
||||||
|
{
|
||||||
|
public function authorize(): bool
|
||||||
|
{
|
||||||
|
$page = $this->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<string, array<int, ValidationRule|string>>
|
||||||
|
*/
|
||||||
|
public function rules(): array
|
||||||
|
{
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -24,6 +24,9 @@
|
|||||||
"Subscriber removed.": "Abonnee verwijderd.",
|
"Subscriber removed.": "Abonnee verwijderd.",
|
||||||
"Delete this subscriber? This cannot be undone.": "Deze abonnee verwijderen? Dit kan niet ongedaan worden gemaakt.",
|
"Delete this subscriber? This cannot be undone.": "Deze abonnee verwijderen? Dit kan niet ongedaan worden gemaakt.",
|
||||||
"Remove": "Verwijderen",
|
"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",
|
"Actions": "Acties",
|
||||||
"Fix background to viewport": "Achtergrond vastzetten op het scherm",
|
"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."
|
"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."
|
||||||
|
|||||||
@@ -82,21 +82,39 @@
|
|||||||
</td>
|
</td>
|
||||||
<td class="whitespace-nowrap px-4 py-3 text-right">
|
<td class="whitespace-nowrap px-4 py-3 text-right">
|
||||||
@can('update', $page)
|
@can('update', $page)
|
||||||
<form
|
<div class="inline-flex flex-wrap items-center justify-end gap-1.5">
|
||||||
method="post"
|
@if ($page->mailwizzConfig !== null)
|
||||||
action="{{ route('admin.pages.subscribers.destroy', [$page, $subscriber]) }}"
|
<form
|
||||||
class="inline"
|
method="post"
|
||||||
onsubmit="return confirm(@js(__('Delete this subscriber? This cannot be undone.')));"
|
action="{{ route('admin.pages.subscribers.sync-mailwizz', [$page, $subscriber]) }}"
|
||||||
>
|
class="inline"
|
||||||
@csrf
|
onsubmit="return confirm(@js(__('Queue a Mailwizz sync for this subscriber? The tag and coupon code will be sent when the queue worker runs.')));"
|
||||||
@method('DELETE')
|
>
|
||||||
<button
|
@csrf
|
||||||
type="submit"
|
<button
|
||||||
class="rounded-lg border border-red-200 bg-white px-2.5 py-1 text-xs font-semibold text-red-700 hover:bg-red-50"
|
type="submit"
|
||||||
|
class="rounded-lg border border-indigo-200 bg-white px-2.5 py-1 text-xs font-semibold text-indigo-700 hover:bg-indigo-50"
|
||||||
|
>
|
||||||
|
{{ __('Sync Mailwizz') }}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
@endif
|
||||||
|
<form
|
||||||
|
method="post"
|
||||||
|
action="{{ route('admin.pages.subscribers.destroy', [$page, $subscriber]) }}"
|
||||||
|
class="inline"
|
||||||
|
onsubmit="return confirm(@js(__('Delete this subscriber? This cannot be undone.')));"
|
||||||
>
|
>
|
||||||
{{ __('Remove') }}
|
@csrf
|
||||||
</button>
|
@method('DELETE')
|
||||||
</form>
|
<button
|
||||||
|
type="submit"
|
||||||
|
class="rounded-lg border border-red-200 bg-white px-2.5 py-1 text-xs font-semibold text-red-700 hover:bg-red-50"
|
||||||
|
>
|
||||||
|
{{ __('Remove') }}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
@endcan
|
@endcan
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|||||||
@@ -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::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::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/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');
|
Route::get('pages/{page}/subscribers', [SubscriberController::class, 'index'])->name('pages.subscribers.index');
|
||||||
|
|
||||||
// Mailwizz configuration (nested under pages)
|
// Mailwizz configuration (nested under pages)
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace Tests\Feature;
|
namespace Tests\Feature;
|
||||||
|
|
||||||
|
use App\Jobs\SyncSubscriberToMailwizz;
|
||||||
use App\Models\MailwizzConfig;
|
use App\Models\MailwizzConfig;
|
||||||
use App\Models\PreregistrationPage;
|
use App\Models\PreregistrationPage;
|
||||||
use App\Models\Subscriber;
|
use App\Models\Subscriber;
|
||||||
@@ -12,6 +13,7 @@ use Illuminate\Foundation\Testing\RefreshDatabase;
|
|||||||
use Illuminate\Http\Client\Request;
|
use Illuminate\Http\Client\Request;
|
||||||
use Illuminate\Support\Facades\Artisan;
|
use Illuminate\Support\Facades\Artisan;
|
||||||
use Illuminate\Support\Facades\Http;
|
use Illuminate\Support\Facades\Http;
|
||||||
|
use Illuminate\Support\Facades\Queue;
|
||||||
use Illuminate\Support\Str;
|
use Illuminate\Support\Str;
|
||||||
use Tests\TestCase;
|
use Tests\TestCase;
|
||||||
|
|
||||||
@@ -80,6 +82,135 @@ class QueueUnsyncedMailwizzSubscribersTest extends TestCase
|
|||||||
$response->assertForbidden();
|
$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
|
private function makePageWithMailwizz(): PreregistrationPage
|
||||||
{
|
{
|
||||||
$user = User::factory()->create(['role' => 'user']);
|
$user = User::factory()->create(['role' => 'user']);
|
||||||
|
|||||||
Reference in New Issue
Block a user