feat: Phase 4 - Mailwizz integration with subscriber sync and retry

This commit is contained in:
2026-04-03 22:03:53 +02:00
parent a1d570254e
commit 83e2158383
13 changed files with 983 additions and 133 deletions

View File

@@ -5,9 +5,10 @@ declare(strict_types=1);
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use App\Services\MailwizzService;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Http;
use RuntimeException;
class MailwizzApiController extends Controller
{
@@ -15,22 +16,15 @@ class MailwizzApiController extends Controller
{
$request->validate(['api_key' => ['required', 'string']]);
$response = Http::withHeaders([
'X-Api-Key' => $request->string('api_key')->toString(),
])->get('https://www.mailwizz.nl/api/lists');
try {
$service = new MailwizzService($request->string('api_key')->toString());
if ($response->failed()) {
return response()->json(['message' => __('Invalid API key or connection failed.')], 422);
return response()->json([
'lists' => $service->getLists(),
]);
} catch (RuntimeException $e) {
return response()->json(['message' => $e->getMessage()], 422);
}
$json = $response->json();
if (is_array($json) && ($json['status'] ?? null) === 'error') {
return response()->json(['message' => __('Invalid API key or connection failed.')], 422);
}
return response()->json([
'lists' => $this->normalizeListsPayload(is_array($json) ? $json : []),
]);
}
public function fields(Request $request): JsonResponse
@@ -40,116 +34,14 @@ class MailwizzApiController extends Controller
'list_uid' => ['required', 'string'],
]);
$listUid = $request->string('list_uid')->toString();
$response = Http::withHeaders([
'X-Api-Key' => $request->string('api_key')->toString(),
])->get("https://www.mailwizz.nl/api/lists/{$listUid}/fields");
try {
$service = new MailwizzService($request->string('api_key')->toString());
if ($response->failed()) {
return response()->json(['message' => __('Failed to fetch list fields.')], 422);
return response()->json([
'fields' => $service->getListFields($request->string('list_uid')->toString()),
]);
} catch (RuntimeException $e) {
return response()->json(['message' => $e->getMessage()], 422);
}
$json = $response->json();
if (is_array($json) && ($json['status'] ?? null) === 'error') {
return response()->json(['message' => __('Failed to fetch list fields.')], 422);
}
return response()->json([
'fields' => $this->normalizeFieldsPayload(is_array($json) ? $json : []),
]);
}
/**
* @return list<array{list_uid: string, name: string}>
*/
private function normalizeListsPayload(array $json): array
{
$out = [];
$records = data_get($json, 'data.records');
if (! is_array($records)) {
return $out;
}
foreach ($records as $row) {
if (! is_array($row)) {
continue;
}
$uid = data_get($row, 'general.list_uid') ?? data_get($row, 'list_uid');
$name = data_get($row, 'general.name') ?? data_get($row, 'name');
if (is_string($uid) && $uid !== '' && is_string($name) && $name !== '') {
$out[] = ['list_uid' => $uid, 'name' => $name];
}
}
return $out;
}
/**
* @return list<array{tag: string, label: string, type_identifier: string, options: array<string, string>}>
*/
private function normalizeFieldsPayload(array $json): array
{
$out = [];
$records = data_get($json, 'data.records');
if (! is_array($records)) {
return $out;
}
foreach ($records as $row) {
if (! is_array($row)) {
continue;
}
$tag = data_get($row, 'tag');
$label = data_get($row, 'label');
$typeId = data_get($row, 'type.identifier');
if (! is_string($tag) || $tag === '' || ! is_string($label)) {
continue;
}
$typeIdentifier = is_string($typeId) ? $typeId : '';
$rawOptions = data_get($row, 'options');
$options = $this->normalizeFieldOptions($rawOptions);
$out[] = [
'tag' => $tag,
'label' => $label,
'type_identifier' => $typeIdentifier,
'options' => $options,
];
}
return $out;
}
/**
* @return array<string, string>
*/
private function normalizeFieldOptions(mixed $rawOptions): array
{
if ($rawOptions === null || $rawOptions === '') {
return [];
}
if (is_string($rawOptions)) {
$decoded = json_decode($rawOptions, true);
if (is_array($decoded)) {
$rawOptions = $decoded;
} else {
return [];
}
}
if (! is_array($rawOptions)) {
return [];
}
$out = [];
foreach ($rawOptions as $key => $value) {
if (is_string($key) || is_int($key)) {
$k = (string) $key;
$out[$k] = is_scalar($value) ? (string) $value : '';
}
}
return $out;
}
}

View File

@@ -6,7 +6,10 @@ namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use App\Http\Requests\Admin\IndexSubscriberRequest;
use App\Http\Requests\Admin\QueueMailwizzSyncRequest;
use App\Models\PreregistrationPage;
use App\Services\DispatchUnsyncedMailwizzSyncJobsService;
use Illuminate\Http\RedirectResponse;
use Illuminate\View\View;
use Symfony\Component\HttpFoundation\StreamedResponse;
@@ -21,7 +24,42 @@ class SubscriberController extends Controller
->paginate(25)
->withQueryString();
return view('admin.subscribers.index', compact('page', 'subscribers'));
$page->loadMissing('mailwizzConfig');
$unsyncedMailwizzCount = $page->mailwizzConfig !== null
? (int) $page->subscribers()->where('synced_to_mailwizz', false)->count()
: 0;
return view('admin.subscribers.index', compact('page', 'subscribers', 'unsyncedMailwizzCount'));
}
public function queueMailwizzSync(
QueueMailwizzSyncRequest $request,
PreregistrationPage $page,
DispatchUnsyncedMailwizzSyncJobsService $dispatcher
): RedirectResponse {
$page->loadMissing('mailwizzConfig');
if ($page->mailwizzConfig === null) {
return redirect()
->route('admin.pages.subscribers.index', $page)
->with('error', __('This page has no Mailwizz integration.'));
}
$count = $dispatcher->dispatch($page->id);
if ($count === 0) {
return redirect()
->route('admin.pages.subscribers.index', $page)
->with('status', __('There are no unsynced subscribers to queue for this page.'));
}
return redirect()
->route('admin.pages.subscribers.index', $page)
->with('status', trans_choice(
'Queued Mailwizz sync for :count subscriber.|Queued Mailwizz sync for :count subscribers.',
$count,
['count' => $count]
));
}
public function export(IndexSubscriberRequest $request, PreregistrationPage $page): StreamedResponse

View File

@@ -5,6 +5,7 @@ declare(strict_types=1);
namespace App\Http\Controllers;
use App\Http\Requests\SubscribePublicPageRequest;
use App\Jobs\SyncSubscriberToMailwizz;
use App\Models\PreregistrationPage;
use Illuminate\Http\JsonResponse;
use Illuminate\View\View;
@@ -33,11 +34,10 @@ class PublicPageController extends Controller
], 422);
}
$publicPage->subscribers()->create($validated);
$subscriber = $publicPage->subscribers()->create($validated);
// Mailwizz sync will be wired up in Phase 4
if ($publicPage->mailwizzConfig) {
// SyncSubscriberToMailwizz::dispatch($subscriber);
if ($publicPage->mailwizzConfig !== null) {
SyncSubscriberToMailwizz::dispatch($subscriber);
}
return response()->json([

View File

@@ -0,0 +1,29 @@
<?php
declare(strict_types=1);
namespace App\Http\Requests\Admin;
use App\Models\PreregistrationPage;
use Illuminate\Foundation\Http\FormRequest;
class QueueMailwizzSyncRequest extends FormRequest
{
public function authorize(): bool
{
$page = $this->route('page');
if (! $page instanceof PreregistrationPage) {
return false;
}
return $this->user()?->can('update', $page) ?? false;
}
/**
* @return array<string, array<int, string>>
*/
public function rules(): array
{
return [];
}
}