Encode empty checkboxlist arrays as an empty scalar so multipart requests include the field. On delete, PUT only coupon and tag fields to Mailwizz after merging the tag CSV from getSubscriber. Made-with: Cursor
313 lines
9.4 KiB
PHP
313 lines
9.4 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Services;
|
|
|
|
use Illuminate\Http\Client\PendingRequest;
|
|
use Illuminate\Http\Client\Response;
|
|
use Illuminate\Support\Facades\Http;
|
|
use Illuminate\Support\Facades\Log;
|
|
use RuntimeException;
|
|
|
|
final class MailwizzService
|
|
{
|
|
public function __construct(
|
|
private readonly string $apiKey,
|
|
private readonly string $baseUrl = 'https://www.mailwizz.nl/api'
|
|
) {}
|
|
|
|
private function client(): PendingRequest
|
|
{
|
|
return Http::withHeaders(['X-Api-Key' => $this->apiKey])->asForm();
|
|
}
|
|
|
|
private function logInteraction(string $action, string $url, Response $response): void
|
|
{
|
|
Log::debug('Mailwizz API', [
|
|
'action' => $action,
|
|
'url' => $url,
|
|
'http_status' => $response->status(),
|
|
'response_preview' => mb_substr($response->body(), 0, 2000),
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* @return list<array{list_uid: string, name: string}>
|
|
*/
|
|
public function getLists(): array
|
|
{
|
|
$url = "{$this->baseUrl}/lists";
|
|
$response = $this->client()->get($url);
|
|
$this->logInteraction('getLists', $url, $response);
|
|
|
|
if ($response->failed()) {
|
|
throw new RuntimeException(__('Invalid API key or connection failed.'));
|
|
}
|
|
|
|
$json = $response->json();
|
|
if (! is_array($json)) {
|
|
throw new RuntimeException(__('Invalid API key or connection failed.'));
|
|
}
|
|
|
|
if (($json['status'] ?? null) === 'error') {
|
|
throw new RuntimeException(__('Invalid API key or connection failed.'));
|
|
}
|
|
|
|
return $this->normalizeListsPayload($json);
|
|
}
|
|
|
|
/**
|
|
* @return list<array{tag: string, label: string, type_identifier: string, options: array<string, string>}>
|
|
*/
|
|
public function getListFields(string $listUid): array
|
|
{
|
|
$url = "{$this->baseUrl}/lists/{$listUid}/fields";
|
|
$response = $this->client()->get($url);
|
|
$this->logInteraction('getListFields', $url, $response);
|
|
|
|
if ($response->failed()) {
|
|
throw new RuntimeException(__('Failed to fetch list fields.'));
|
|
}
|
|
|
|
$json = $response->json();
|
|
if (! is_array($json)) {
|
|
throw new RuntimeException(__('Failed to fetch list fields.'));
|
|
}
|
|
|
|
if (($json['status'] ?? null) === 'error') {
|
|
throw new RuntimeException(__('Failed to fetch list fields.'));
|
|
}
|
|
|
|
return $this->normalizeFieldsPayload($json);
|
|
}
|
|
|
|
/**
|
|
* Search by email. Mailwizz uses query param EMAIL regardless of list field mapping.
|
|
*
|
|
* @return array{subscriber_uid: string}|null
|
|
*/
|
|
public function searchSubscriber(string $listUid, string $email): ?array
|
|
{
|
|
$url = "{$this->baseUrl}/lists/{$listUid}/subscribers/search-by-email";
|
|
$response = $this->client()->get($url, [
|
|
'EMAIL' => $email,
|
|
]);
|
|
$this->logInteraction('searchSubscriber', $url, $response);
|
|
|
|
if ($response->failed()) {
|
|
if ($response->status() === 404) {
|
|
return null;
|
|
}
|
|
|
|
throw new RuntimeException(__('Mailwizz subscriber search failed.'));
|
|
}
|
|
|
|
$json = $response->json();
|
|
if (! is_array($json)) {
|
|
return null;
|
|
}
|
|
|
|
if (($json['status'] ?? null) !== 'success') {
|
|
return null;
|
|
}
|
|
|
|
$uid = data_get($json, 'data.subscriber_uid')
|
|
?? data_get($json, 'data.record.subscriber_uid');
|
|
|
|
if (! is_string($uid) || $uid === '') {
|
|
return null;
|
|
}
|
|
|
|
return ['subscriber_uid' => $uid];
|
|
}
|
|
|
|
/**
|
|
* Full subscriber payload from Mailwizz (structure varies; callers inspect data.record).
|
|
*
|
|
* @return array<string, mixed>|null
|
|
*/
|
|
public function getSubscriber(string $listUid, string $subscriberUid): ?array
|
|
{
|
|
$url = "{$this->baseUrl}/lists/{$listUid}/subscribers/{$subscriberUid}";
|
|
$response = $this->client()->get($url);
|
|
$this->logInteraction('getSubscriber', $url, $response);
|
|
|
|
if ($response->failed()) {
|
|
throw new RuntimeException(__('Mailwizz could not load subscriber.'));
|
|
}
|
|
|
|
$json = $response->json();
|
|
if (! is_array($json) || ($json['status'] ?? null) !== 'success') {
|
|
return null;
|
|
}
|
|
|
|
return $json;
|
|
}
|
|
|
|
/**
|
|
* @param array<string, mixed> $data Form fields; checkboxlist values as string[] per field tag
|
|
* @return array<string, mixed>
|
|
*/
|
|
public function createSubscriber(string $listUid, array $data): array
|
|
{
|
|
$url = "{$this->baseUrl}/lists/{$listUid}/subscribers";
|
|
$response = $this->client()->post($url, $this->encodeFormPayload($data));
|
|
$this->logInteraction('createSubscriber', $url, $response);
|
|
|
|
if ($response->failed()) {
|
|
throw new RuntimeException(__('Mailwizz rejected subscriber creation.'));
|
|
}
|
|
|
|
$json = $response->json();
|
|
if (! is_array($json) || ($json['status'] ?? null) !== 'success') {
|
|
$msg = is_array($json) ? (string) ($json['error'] ?? $json['message'] ?? '') : '';
|
|
|
|
throw new RuntimeException($msg !== '' ? $msg : __('Mailwizz rejected subscriber creation.'));
|
|
}
|
|
|
|
return $json;
|
|
}
|
|
|
|
/**
|
|
* @param array<string, mixed> $data
|
|
* @return array<string, mixed>
|
|
*/
|
|
public function updateSubscriber(string $listUid, string $subscriberUid, array $data): array
|
|
{
|
|
$url = "{$this->baseUrl}/lists/{$listUid}/subscribers/{$subscriberUid}";
|
|
$response = $this->client()->put($url, $this->encodeFormPayload($data));
|
|
$this->logInteraction('updateSubscriber', $url, $response);
|
|
|
|
if ($response->failed()) {
|
|
throw new RuntimeException(__('Mailwizz rejected subscriber update.'));
|
|
}
|
|
|
|
$json = $response->json();
|
|
if (! is_array($json) || ($json['status'] ?? null) !== 'success') {
|
|
$msg = is_array($json) ? (string) ($json['error'] ?? $json['message'] ?? '') : '';
|
|
|
|
throw new RuntimeException($msg !== '' ? $msg : __('Mailwizz rejected subscriber update.'));
|
|
}
|
|
|
|
return $json;
|
|
}
|
|
|
|
/**
|
|
* @param array<string, mixed> $data
|
|
* @return array<string, mixed>
|
|
*/
|
|
private function encodeFormPayload(array $data): array
|
|
{
|
|
$out = [];
|
|
foreach ($data as $key => $value) {
|
|
if (is_array($value)) {
|
|
// Empty arrays are omitted by Laravel's multipart encoder (no KEY[] parts), so Mailwizz never clears checkboxlist fields.
|
|
$out[$key] = $value === [] ? '' : $value;
|
|
|
|
continue;
|
|
}
|
|
if ($value === null) {
|
|
continue;
|
|
}
|
|
$out[$key] = $value;
|
|
}
|
|
|
|
return $out;
|
|
}
|
|
|
|
/**
|
|
* @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;
|
|
}
|
|
}
|