Compare commits
13 Commits
977e09d8ac
...
91caa16e70
| Author | SHA1 | Date | |
|---|---|---|---|
| 91caa16e70 | |||
| 1e7ee14540 | |||
| 627edbbb83 | |||
| 7eda51f52a | |||
| de83a6fb76 | |||
| d802ce2a7c | |||
| 7ed660ec55 | |||
| 9f8052f683 | |||
| 217e1d9afb | |||
| 89931b817d | |||
| e0de8a05fa | |||
| 55434ce086 | |||
| 6561bda30d |
@@ -45,6 +45,10 @@ SESSION_DOMAIN=null
|
|||||||
|
|
||||||
BROADCAST_CONNECTION=log
|
BROADCAST_CONNECTION=log
|
||||||
FILESYSTEM_DISK=local
|
FILESYSTEM_DISK=local
|
||||||
|
|
||||||
|
# Use "database" or "redis" in production and run `php artisan queue:work` (see documentation/DEPLOYMENT-STRATEGY.md).
|
||||||
|
# Avoid "sync" in production: Mailwizz (and Weeztix coupon) jobs would run inside the HTTP request; a thrown error
|
||||||
|
# can return 5xx to the visitor even though the subscriber row is already saved — confusing UX.
|
||||||
QUEUE_CONNECTION=database
|
QUEUE_CONNECTION=database
|
||||||
|
|
||||||
CACHE_STORE=database
|
CACHE_STORE=database
|
||||||
|
|||||||
@@ -8,18 +8,29 @@ use App\Http\Controllers\Controller;
|
|||||||
use App\Http\Requests\Admin\UpdateMailwizzConfigRequest;
|
use App\Http\Requests\Admin\UpdateMailwizzConfigRequest;
|
||||||
use App\Models\PreregistrationPage;
|
use App\Models\PreregistrationPage;
|
||||||
use Illuminate\Http\RedirectResponse;
|
use Illuminate\Http\RedirectResponse;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
use Illuminate\Support\Facades\DB;
|
use Illuminate\Support\Facades\DB;
|
||||||
use Illuminate\View\View;
|
use Illuminate\View\View;
|
||||||
|
|
||||||
class MailwizzController extends Controller
|
class MailwizzController extends Controller
|
||||||
{
|
{
|
||||||
public function edit(PreregistrationPage $page): View
|
public function edit(Request $request, PreregistrationPage $page): View|RedirectResponse
|
||||||
{
|
{
|
||||||
$this->authorize('update', $page);
|
$this->authorize('update', $page);
|
||||||
|
|
||||||
$page->load('mailwizzConfig');
|
$page->load(['mailwizzConfig', 'weeztixConfig']);
|
||||||
|
|
||||||
return view('admin.mailwizz.edit', compact('page'));
|
$config = $page->mailwizzConfig;
|
||||||
|
$showWizard = $config === null || $request->boolean('wizard');
|
||||||
|
if ($showWizard && $config === null) {
|
||||||
|
$requestedStep = min(4, max(1, (int) $request->query('step', 1)));
|
||||||
|
if ($requestedStep !== 1) {
|
||||||
|
return redirect()
|
||||||
|
->route('admin.pages.mailwizz.edit', ['page' => $page, 'wizard' => 1, 'step' => 1]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return view('admin.mailwizz.edit', compact('page', 'showWizard'));
|
||||||
}
|
}
|
||||||
|
|
||||||
public function update(UpdateMailwizzConfigRequest $request, PreregistrationPage $page): RedirectResponse
|
public function update(UpdateMailwizzConfigRequest $request, PreregistrationPage $page): RedirectResponse
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ class PageController extends Controller
|
|||||||
{
|
{
|
||||||
$query = PreregistrationPage::query()
|
$query = PreregistrationPage::query()
|
||||||
->withCount('subscribers')
|
->withCount('subscribers')
|
||||||
->with('weeztixConfig')
|
->with(['weeztixConfig', 'mailwizzConfig'])
|
||||||
->orderByDesc('start_date');
|
->orderByDesc('start_date');
|
||||||
|
|
||||||
if (! $request->user()?->isSuperadmin()) {
|
if (! $request->user()?->isSuperadmin()) {
|
||||||
@@ -86,7 +86,11 @@ class PageController extends Controller
|
|||||||
|
|
||||||
public function edit(PreregistrationPage $page): View
|
public function edit(PreregistrationPage $page): View
|
||||||
{
|
{
|
||||||
$page->load(['blocks' => fn ($q) => $q->orderBy('sort_order')]);
|
$page->load([
|
||||||
|
'blocks' => fn ($q) => $q->orderBy('sort_order'),
|
||||||
|
'mailwizzConfig',
|
||||||
|
'weeztixConfig',
|
||||||
|
]);
|
||||||
|
|
||||||
return view('admin.pages.edit', compact('page'));
|
return view('admin.pages.edit', compact('page'));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ use App\Http\Requests\Admin\IndexSubscriberRequest;
|
|||||||
use App\Http\Requests\Admin\QueueMailwizzSyncRequest;
|
use App\Http\Requests\Admin\QueueMailwizzSyncRequest;
|
||||||
use App\Models\PreregistrationPage;
|
use App\Models\PreregistrationPage;
|
||||||
use App\Models\Subscriber;
|
use App\Models\Subscriber;
|
||||||
|
use App\Services\CleanupSubscriberIntegrationsService;
|
||||||
use App\Services\DispatchUnsyncedMailwizzSyncJobsService;
|
use App\Services\DispatchUnsyncedMailwizzSyncJobsService;
|
||||||
use Illuminate\Http\RedirectResponse;
|
use Illuminate\Http\RedirectResponse;
|
||||||
use Illuminate\View\View;
|
use Illuminate\View\View;
|
||||||
@@ -34,8 +35,13 @@ class SubscriberController extends Controller
|
|||||||
return view('admin.subscribers.index', compact('page', 'subscribers', 'unsyncedMailwizzCount'));
|
return view('admin.subscribers.index', compact('page', 'subscribers', 'unsyncedMailwizzCount'));
|
||||||
}
|
}
|
||||||
|
|
||||||
public function destroy(DestroySubscriberRequest $request, PreregistrationPage $page, Subscriber $subscriber): RedirectResponse
|
public function destroy(
|
||||||
{
|
DestroySubscriberRequest $request,
|
||||||
|
PreregistrationPage $page,
|
||||||
|
Subscriber $subscriber,
|
||||||
|
CleanupSubscriberIntegrationsService $cleanupIntegrations
|
||||||
|
): RedirectResponse {
|
||||||
|
$cleanupIntegrations->runBeforeDelete($subscriber);
|
||||||
$subscriber->delete();
|
$subscriber->delete();
|
||||||
|
|
||||||
return redirect()
|
return redirect()
|
||||||
|
|||||||
@@ -13,38 +13,10 @@ use RuntimeException;
|
|||||||
|
|
||||||
class WeeztixApiController extends Controller
|
class WeeztixApiController extends Controller
|
||||||
{
|
{
|
||||||
public function companies(Request $request): JsonResponse
|
|
||||||
{
|
|
||||||
$request->validate([
|
|
||||||
'page_id' => ['required', 'integer', 'exists:preregistration_pages,id'],
|
|
||||||
]);
|
|
||||||
|
|
||||||
$page = PreregistrationPage::query()->findOrFail($request->integer('page_id'));
|
|
||||||
$this->authorize('update', $page);
|
|
||||||
|
|
||||||
$config = $page->weeztixConfig;
|
|
||||||
if ($config === null || ! $config->is_connected) {
|
|
||||||
return response()->json([
|
|
||||||
'message' => __('Niet verbonden met Weeztix.'),
|
|
||||||
], 422);
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
$companies = (new WeeztixService($config))->getCompanies();
|
|
||||||
|
|
||||||
return response()->json(['companies' => $companies]);
|
|
||||||
} catch (RuntimeException) {
|
|
||||||
return response()->json([
|
|
||||||
'message' => __('Kon bedrijven niet laden. Vernieuw de verbinding indien nodig.'),
|
|
||||||
], 422);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public function coupons(Request $request): JsonResponse
|
public function coupons(Request $request): JsonResponse
|
||||||
{
|
{
|
||||||
$request->validate([
|
$request->validate([
|
||||||
'page_id' => ['required', 'integer', 'exists:preregistration_pages,id'],
|
'page_id' => ['required', 'integer', 'exists:preregistration_pages,id'],
|
||||||
'company_guid' => ['required', 'string', 'max:255'],
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$page = PreregistrationPage::query()->findOrFail($request->integer('page_id'));
|
$page = PreregistrationPage::query()->findOrFail($request->integer('page_id'));
|
||||||
@@ -57,9 +29,12 @@ class WeeztixApiController extends Controller
|
|||||||
], 422);
|
], 422);
|
||||||
}
|
}
|
||||||
|
|
||||||
$companyGuid = $request->string('company_guid')->toString();
|
$companyGuid = $config->company_guid;
|
||||||
$previousGuid = $config->company_guid;
|
if (! is_string($companyGuid) || $companyGuid === '') {
|
||||||
$config->setAttribute('company_guid', $companyGuid);
|
return response()->json([
|
||||||
|
'message' => __('Geen Weeztix-bedrijf gekoppeld. Verbind opnieuw met Weeztix.'),
|
||||||
|
], 422);
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$raw = (new WeeztixService($config))->getCoupons();
|
$raw = (new WeeztixService($config))->getCoupons();
|
||||||
@@ -70,8 +45,6 @@ class WeeztixApiController extends Controller
|
|||||||
return response()->json([
|
return response()->json([
|
||||||
'message' => __('Kon kortingsbonnen niet laden.'),
|
'message' => __('Kon kortingsbonnen niet laden.'),
|
||||||
], 422);
|
], 422);
|
||||||
} finally {
|
|
||||||
$config->setAttribute('company_guid', $previousGuid);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -95,6 +68,9 @@ class WeeztixApiController extends Controller
|
|||||||
if (! is_array($row)) {
|
if (! is_array($row)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
if (! $this->couponRowHasEnabledStatus($row)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
$guid = data_get($row, 'guid') ?? data_get($row, 'id');
|
$guid = data_get($row, 'guid') ?? data_get($row, 'id');
|
||||||
if (! is_string($guid) || $guid === '') {
|
if (! is_string($guid) || $guid === '') {
|
||||||
continue;
|
continue;
|
||||||
@@ -108,4 +84,16 @@ class WeeztixApiController extends Controller
|
|||||||
|
|
||||||
return $out;
|
return $out;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Weeztix coupon list items expose a string status; only "enabled" should appear in the admin picker.
|
||||||
|
*
|
||||||
|
* @param array<string, mixed> $row
|
||||||
|
*/
|
||||||
|
private function couponRowHasEnabledStatus(array $row): bool
|
||||||
|
{
|
||||||
|
$status = data_get($row, 'status');
|
||||||
|
|
||||||
|
return is_string($status) && strcasecmp(trim($status), 'enabled') === 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,18 +8,34 @@ use App\Http\Controllers\Controller;
|
|||||||
use App\Http\Requests\Admin\UpdateWeeztixConfigRequest;
|
use App\Http\Requests\Admin\UpdateWeeztixConfigRequest;
|
||||||
use App\Models\PreregistrationPage;
|
use App\Models\PreregistrationPage;
|
||||||
use Illuminate\Http\RedirectResponse;
|
use Illuminate\Http\RedirectResponse;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
use Illuminate\Support\Facades\DB;
|
use Illuminate\Support\Facades\DB;
|
||||||
use Illuminate\View\View;
|
use Illuminate\View\View;
|
||||||
|
|
||||||
class WeeztixController extends Controller
|
class WeeztixController extends Controller
|
||||||
{
|
{
|
||||||
public function edit(PreregistrationPage $page): View
|
public function edit(Request $request, PreregistrationPage $page): View
|
||||||
{
|
{
|
||||||
$this->authorize('update', $page);
|
$this->authorize('update', $page);
|
||||||
|
|
||||||
$page->load('weeztixConfig');
|
$page->load('weeztixConfig');
|
||||||
|
|
||||||
return view('admin.weeztix.edit', compact('page'));
|
$showWizard = $page->weeztixConfig === null || $request->boolean('wizard');
|
||||||
|
$wizardStep = $showWizard ? min(3, max(1, (int) $request->query('step', 1))) : 1;
|
||||||
|
if ($showWizard && $page->weeztixConfig === null && $wizardStep !== 1) {
|
||||||
|
return redirect()
|
||||||
|
->route('admin.pages.weeztix.edit', ['page' => $page, 'wizard' => 1, 'step' => 1]);
|
||||||
|
}
|
||||||
|
$hasStoredCredentials = $page->weeztixConfig !== null
|
||||||
|
&& is_string($page->weeztixConfig->client_id)
|
||||||
|
&& $page->weeztixConfig->client_id !== '';
|
||||||
|
|
||||||
|
return view('admin.weeztix.edit', compact(
|
||||||
|
'page',
|
||||||
|
'showWizard',
|
||||||
|
'wizardStep',
|
||||||
|
'hasStoredCredentials'
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
public function update(UpdateWeeztixConfigRequest $request, PreregistrationPage $page): RedirectResponse
|
public function update(UpdateWeeztixConfigRequest $request, PreregistrationPage $page): RedirectResponse
|
||||||
@@ -41,8 +57,26 @@ class WeeztixController extends Controller
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
$page->load('weeztixConfig');
|
||||||
|
|
||||||
|
if ($request->boolean('wizard')) {
|
||||||
|
if ($request->boolean('wizard_coupon_save')) {
|
||||||
return redirect()
|
return redirect()
|
||||||
->route('admin.pages.weeztix.edit', $page)
|
->route('admin.pages.weeztix.edit', ['page' => $page])
|
||||||
|
->with('status', __('Weeztix-configuratie opgeslagen.'));
|
||||||
|
}
|
||||||
|
|
||||||
|
return redirect()
|
||||||
|
->route('admin.pages.weeztix.edit', [
|
||||||
|
'page' => $page,
|
||||||
|
'wizard' => 1,
|
||||||
|
'step' => 2,
|
||||||
|
])
|
||||||
|
->with('status', __('Weeztix-configuratie opgeslagen.'));
|
||||||
|
}
|
||||||
|
|
||||||
|
return redirect()
|
||||||
|
->route('admin.pages.weeztix.edit', ['page' => $page])
|
||||||
->with('status', __('Weeztix-configuratie opgeslagen.'));
|
->with('status', __('Weeztix-configuratie opgeslagen.'));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ use RuntimeException;
|
|||||||
|
|
||||||
class WeeztixOAuthController extends Controller
|
class WeeztixOAuthController extends Controller
|
||||||
{
|
{
|
||||||
public function redirect(PreregistrationPage $page): RedirectResponse
|
public function redirect(Request $request, PreregistrationPage $page): RedirectResponse
|
||||||
{
|
{
|
||||||
$this->authorize('update', $page);
|
$this->authorize('update', $page);
|
||||||
|
|
||||||
@@ -38,6 +38,7 @@ class WeeztixOAuthController extends Controller
|
|||||||
session([
|
session([
|
||||||
'weeztix_oauth_state' => $state,
|
'weeztix_oauth_state' => $state,
|
||||||
'weeztix_page_id' => $page->id,
|
'weeztix_page_id' => $page->id,
|
||||||
|
'weeztix_oauth_resume_wizard' => $request->boolean('wizard'),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$redirectUri = $config->redirect_uri;
|
$redirectUri = $config->redirect_uri;
|
||||||
@@ -90,7 +91,7 @@ class WeeztixOAuthController extends Controller
|
|||||||
|
|
||||||
$config = $page->weeztixConfig;
|
$config = $page->weeztixConfig;
|
||||||
if ($config === null) {
|
if ($config === null) {
|
||||||
session()->forget(['weeztix_oauth_state', 'weeztix_page_id']);
|
$this->forgetOauthSession();
|
||||||
|
|
||||||
return redirect()
|
return redirect()
|
||||||
->route('admin.pages.weeztix.edit', $page)
|
->route('admin.pages.weeztix.edit', $page)
|
||||||
@@ -100,36 +101,62 @@ class WeeztixOAuthController extends Controller
|
|||||||
try {
|
try {
|
||||||
$service = new WeeztixService($config);
|
$service = new WeeztixService($config);
|
||||||
$service->exchangeAuthorizationCode($request->string('code')->toString());
|
$service->exchangeAuthorizationCode($request->string('code')->toString());
|
||||||
|
$config = $config->fresh();
|
||||||
|
if ($config !== null) {
|
||||||
|
(new WeeztixService($config))->ensureCompanyStoredFromWeeztix();
|
||||||
|
}
|
||||||
} catch (RuntimeException $e) {
|
} catch (RuntimeException $e) {
|
||||||
Log::error('Weeztix OAuth callback failed', [
|
Log::error('Weeztix OAuth callback failed', [
|
||||||
'page_id' => $page->id,
|
'page_id' => $page->id,
|
||||||
'message' => $e->getMessage(),
|
'message' => $e->getMessage(),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
session()->forget(['weeztix_oauth_state', 'weeztix_page_id']);
|
$resumeWizard = $this->forgetOauthSession();
|
||||||
|
|
||||||
return redirect()
|
return redirect()
|
||||||
->route('admin.pages.weeztix.edit', $page)
|
->route('admin.pages.weeztix.edit', $this->weeztixEditParams($page, $resumeWizard, 2))
|
||||||
->with('error', __('Verbinden met Weeztix is mislukt. Controleer je gegevens en probeer opnieuw.'));
|
->with('error', __('Verbinden met Weeztix is mislukt. Controleer je gegevens en probeer opnieuw.'));
|
||||||
}
|
}
|
||||||
|
|
||||||
session()->forget(['weeztix_oauth_state', 'weeztix_page_id']);
|
$resumeWizard = $this->forgetOauthSession();
|
||||||
|
|
||||||
return redirect()
|
return redirect()
|
||||||
->route('admin.pages.weeztix.edit', $page)
|
->route('admin.pages.weeztix.edit', $this->weeztixEditParams($page, $resumeWizard, 3))
|
||||||
->with('status', __('Succesvol verbonden met Weeztix.'));
|
->with('status', __('Succesvol verbonden met Weeztix.'));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array{page: PreregistrationPage, wizard?: int, step?: int}
|
||||||
|
*/
|
||||||
|
private function weeztixEditParams(PreregistrationPage $page, bool $resumeWizard, int $step): array
|
||||||
|
{
|
||||||
|
$params = ['page' => $page];
|
||||||
|
if ($resumeWizard) {
|
||||||
|
$params['wizard'] = 1;
|
||||||
|
$params['step'] = $step;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $params;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function forgetOauthSession(): bool
|
||||||
|
{
|
||||||
|
$resumeWizard = (bool) session()->pull('weeztix_oauth_resume_wizard', false);
|
||||||
|
session()->forget(['weeztix_oauth_state', 'weeztix_page_id']);
|
||||||
|
|
||||||
|
return $resumeWizard;
|
||||||
|
}
|
||||||
|
|
||||||
private function redirectToWeeztixEditWithSessionPage(string $message): RedirectResponse
|
private function redirectToWeeztixEditWithSessionPage(string $message): RedirectResponse
|
||||||
{
|
{
|
||||||
$pageId = session('weeztix_page_id');
|
$pageId = session('weeztix_page_id');
|
||||||
session()->forget(['weeztix_oauth_state', 'weeztix_page_id']);
|
$resumeWizard = $this->forgetOauthSession();
|
||||||
|
|
||||||
if (is_int($pageId) || is_numeric($pageId)) {
|
if (is_int($pageId) || is_numeric($pageId)) {
|
||||||
$page = PreregistrationPage::query()->find((int) $pageId);
|
$page = PreregistrationPage::query()->find((int) $pageId);
|
||||||
if ($page !== null) {
|
if ($page !== null) {
|
||||||
return redirect()
|
return redirect()
|
||||||
->route('admin.pages.weeztix.edit', $page)
|
->route('admin.pages.weeztix.edit', $this->weeztixEditParams($page, $resumeWizard, 2))
|
||||||
->with('error', $message);
|
->with('error', $message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,22 +4,20 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace App\Http\Controllers;
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
use App\Exceptions\WeeztixCouponCodeConflictException;
|
|
||||||
use App\Http\Requests\SubscribePublicPageRequest;
|
use App\Http\Requests\SubscribePublicPageRequest;
|
||||||
use App\Jobs\SyncSubscriberToMailwizz;
|
|
||||||
use App\Models\PageBlock;
|
use App\Models\PageBlock;
|
||||||
use App\Models\PreregistrationPage;
|
use App\Models\PreregistrationPage;
|
||||||
use App\Models\Subscriber;
|
use App\Services\RegisterSubscriberOnPage;
|
||||||
use App\Models\WeeztixConfig;
|
|
||||||
use App\Services\WeeztixService;
|
|
||||||
use Illuminate\Http\JsonResponse;
|
use Illuminate\Http\JsonResponse;
|
||||||
use Illuminate\Support\Collection;
|
use Illuminate\Support\Collection;
|
||||||
use Illuminate\Support\Facades\Log;
|
|
||||||
use Illuminate\View\View;
|
use Illuminate\View\View;
|
||||||
use Throwable;
|
|
||||||
|
|
||||||
class PublicPageController extends Controller
|
class PublicPageController extends Controller
|
||||||
{
|
{
|
||||||
|
public function __construct(
|
||||||
|
private readonly RegisterSubscriberOnPage $registerSubscriberOnPage
|
||||||
|
) {}
|
||||||
|
|
||||||
public function show(PreregistrationPage $publicPage): View
|
public function show(PreregistrationPage $publicPage): View
|
||||||
{
|
{
|
||||||
$publicPage->load(['blocks' => fn ($q) => $q->orderBy('sort_order')]);
|
$publicPage->load(['blocks' => fn ($q) => $q->orderBy('sort_order')]);
|
||||||
@@ -56,72 +54,11 @@ class PublicPageController extends Controller
|
|||||||
], 422);
|
], 422);
|
||||||
}
|
}
|
||||||
|
|
||||||
$subscriber = $publicPage->subscribers()->create($validated);
|
$this->registerSubscriberOnPage->storeAndQueueIntegrations($publicPage, $validated);
|
||||||
|
|
||||||
$publicPage->loadMissing('weeztixConfig');
|
|
||||||
$weeztix = $publicPage->weeztixConfig;
|
|
||||||
if ($this->weeztixCanIssueCodes($weeztix)) {
|
|
||||||
$this->tryAttachWeeztixCouponCode($subscriber, $weeztix);
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($publicPage->mailwizzConfig !== null) {
|
|
||||||
SyncSubscriberToMailwizz::dispatch($subscriber->fresh());
|
|
||||||
}
|
|
||||||
|
|
||||||
return response()->json([
|
return response()->json([
|
||||||
'success' => true,
|
'success' => true,
|
||||||
'message' => $publicPage->thank_you_message ?? __('Thank you for registering!'),
|
'message' => $publicPage->thank_you_message ?? __('Thank you for registering!'),
|
||||||
'coupon_code' => $subscriber->fresh()?->coupon_code,
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
private function weeztixCanIssueCodes(?WeeztixConfig $config): bool
|
|
||||||
{
|
|
||||||
if ($config === null || ! $config->is_connected) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
$company = $config->company_guid;
|
|
||||||
$coupon = $config->coupon_guid;
|
|
||||||
|
|
||||||
return is_string($company) && $company !== '' && is_string($coupon) && $coupon !== '';
|
|
||||||
}
|
|
||||||
|
|
||||||
private function tryAttachWeeztixCouponCode(Subscriber $subscriber, WeeztixConfig $config): void
|
|
||||||
{
|
|
||||||
$freshConfig = $config->fresh();
|
|
||||||
if ($freshConfig === null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
$service = new WeeztixService($freshConfig);
|
|
||||||
$maxAttempts = 5;
|
|
||||||
|
|
||||||
for ($attempt = 0; $attempt < $maxAttempts; $attempt++) {
|
|
||||||
try {
|
|
||||||
$code = WeeztixService::generateUniqueCode(
|
|
||||||
is_string($freshConfig->code_prefix) && $freshConfig->code_prefix !== ''
|
|
||||||
? $freshConfig->code_prefix
|
|
||||||
: 'PREREG'
|
|
||||||
);
|
|
||||||
$service->createCouponCode($code);
|
|
||||||
$subscriber->update(['coupon_code' => $code]);
|
|
||||||
|
|
||||||
return;
|
|
||||||
} catch (WeeztixCouponCodeConflictException) {
|
|
||||||
continue;
|
|
||||||
} catch (Throwable $e) {
|
|
||||||
Log::error('Weeztix coupon creation failed', [
|
|
||||||
'subscriber_id' => $subscriber->id,
|
|
||||||
'message' => $e->getMessage(),
|
|
||||||
]);
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Log::warning('Weeztix coupon: exhausted duplicate retries', [
|
|
||||||
'subscriber_id' => $subscriber->id,
|
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -44,8 +44,6 @@ class UpdateWeeztixConfigRequest extends FormRequest
|
|||||||
'string',
|
'string',
|
||||||
'max:2048',
|
'max:2048',
|
||||||
],
|
],
|
||||||
'company_guid' => ['nullable', 'string', 'max:255'],
|
|
||||||
'company_name' => ['nullable', 'string', 'max:255'],
|
|
||||||
'coupon_guid' => ['nullable', 'string', 'max:255'],
|
'coupon_guid' => ['nullable', 'string', 'max:255'],
|
||||||
'coupon_name' => ['nullable', 'string', 'max:255'],
|
'coupon_name' => ['nullable', 'string', 'max:255'],
|
||||||
'code_prefix' => ['nullable', 'string', 'max:32'],
|
'code_prefix' => ['nullable', 'string', 'max:32'],
|
||||||
|
|||||||
175
app/Jobs/IssueWeeztixCouponForSubscriber.php
Normal file
175
app/Jobs/IssueWeeztixCouponForSubscriber.php
Normal file
@@ -0,0 +1,175 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Jobs;
|
||||||
|
|
||||||
|
use App\Exceptions\WeeztixCouponCodeConflictException;
|
||||||
|
use App\Models\Subscriber;
|
||||||
|
use App\Models\WeeztixConfig;
|
||||||
|
use App\Services\WeeztixService;
|
||||||
|
use Illuminate\Bus\Queueable;
|
||||||
|
use Illuminate\Contracts\Queue\ShouldBeUnique;
|
||||||
|
use Illuminate\Contracts\Queue\ShouldQueueAfterCommit;
|
||||||
|
use Illuminate\Foundation\Bus\Dispatchable;
|
||||||
|
use Illuminate\Queue\InteractsWithQueue;
|
||||||
|
use Illuminate\Queue\SerializesModels;
|
||||||
|
use Illuminate\Support\Facades\Log;
|
||||||
|
use Throwable;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates the Weeztix coupon code after the subscriber row exists, then queues Mailwizz sync
|
||||||
|
* so the external APIs never block the public HTTP response and Mailwizz runs after coupon_code is set when possible.
|
||||||
|
*/
|
||||||
|
final class IssueWeeztixCouponForSubscriber implements ShouldBeUnique, ShouldQueueAfterCommit
|
||||||
|
{
|
||||||
|
use Dispatchable;
|
||||||
|
use InteractsWithQueue;
|
||||||
|
use Queueable;
|
||||||
|
use SerializesModels;
|
||||||
|
|
||||||
|
public int $tries = 3;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var list<int>
|
||||||
|
*/
|
||||||
|
public array $backoff = [5, 15, 45];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Seconds before the unique lock expires if the worker dies before releasing it.
|
||||||
|
*/
|
||||||
|
public int $uniqueFor = 300;
|
||||||
|
|
||||||
|
public function __construct(public Subscriber $subscriber)
|
||||||
|
{
|
||||||
|
$this->onQueue('weeztix');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function uniqueId(): string
|
||||||
|
{
|
||||||
|
return 'weeztix-coupon-subscriber-'.$this->subscriber->getKey();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function handle(): void
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$subscriber = Subscriber::query()
|
||||||
|
->with(['preregistrationPage.weeztixConfig', 'preregistrationPage.mailwizzConfig'])
|
||||||
|
->find($this->subscriber->id);
|
||||||
|
|
||||||
|
if ($subscriber === null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$page = $subscriber->preregistrationPage;
|
||||||
|
if ($page === null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$config = $page->weeztixConfig;
|
||||||
|
$couponMissing = ! is_string($subscriber->coupon_code) || $subscriber->coupon_code === '';
|
||||||
|
|
||||||
|
if ($couponMissing && $this->weeztixCanIssueCodes($config)) {
|
||||||
|
$this->tryAttachWeeztixCouponCode($subscriber, $config);
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->dispatchMailwizzIfNeeded($subscriber);
|
||||||
|
} catch (Throwable $e) {
|
||||||
|
Log::error('IssueWeeztixCouponForSubscriber: handle failed', [
|
||||||
|
'subscriber_id' => $this->subscriber->id,
|
||||||
|
'message' => $e->getMessage(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
$subscriber = Subscriber::query()
|
||||||
|
->with(['preregistrationPage.mailwizzConfig'])
|
||||||
|
->find($this->subscriber->id);
|
||||||
|
|
||||||
|
if ($subscriber !== null) {
|
||||||
|
$this->dispatchMailwizzIfNeeded($subscriber);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function failed(?Throwable $exception): void
|
||||||
|
{
|
||||||
|
Log::error('IssueWeeztixCouponForSubscriber failed', [
|
||||||
|
'subscriber_id' => $this->subscriber->id,
|
||||||
|
'message' => $exception?->getMessage(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
$subscriber = Subscriber::query()
|
||||||
|
->with('preregistrationPage.mailwizzConfig')
|
||||||
|
->find($this->subscriber->id);
|
||||||
|
|
||||||
|
if ($subscriber !== null) {
|
||||||
|
$this->dispatchMailwizzIfNeeded($subscriber);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function dispatchMailwizzIfNeeded(Subscriber $subscriber): void
|
||||||
|
{
|
||||||
|
$page = $subscriber->preregistrationPage;
|
||||||
|
$page?->loadMissing('mailwizzConfig');
|
||||||
|
|
||||||
|
if ($page?->mailwizzConfig !== null) {
|
||||||
|
try {
|
||||||
|
SyncSubscriberToMailwizz::dispatch($subscriber->fresh());
|
||||||
|
} catch (Throwable $e) {
|
||||||
|
Log::error('IssueWeeztixCouponForSubscriber: could not queue Mailwizz sync', [
|
||||||
|
'subscriber_id' => $subscriber->id,
|
||||||
|
'message' => $e->getMessage(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function weeztixCanIssueCodes(?WeeztixConfig $config): bool
|
||||||
|
{
|
||||||
|
if ($config === null || ! $config->is_connected) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$company = $config->company_guid;
|
||||||
|
$coupon = $config->coupon_guid;
|
||||||
|
|
||||||
|
return is_string($company) && $company !== '' && is_string($coupon) && $coupon !== '';
|
||||||
|
}
|
||||||
|
|
||||||
|
private function tryAttachWeeztixCouponCode(Subscriber $subscriber, WeeztixConfig $config): void
|
||||||
|
{
|
||||||
|
$freshConfig = $config->fresh();
|
||||||
|
if ($freshConfig === null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$service = new WeeztixService($freshConfig);
|
||||||
|
$maxAttempts = 5;
|
||||||
|
|
||||||
|
for ($attempt = 0; $attempt < $maxAttempts; $attempt++) {
|
||||||
|
try {
|
||||||
|
$code = WeeztixService::generateUniqueCode(
|
||||||
|
is_string($freshConfig->code_prefix) && $freshConfig->code_prefix !== ''
|
||||||
|
? $freshConfig->code_prefix
|
||||||
|
: 'PREREG'
|
||||||
|
);
|
||||||
|
$service->createCouponCode($code);
|
||||||
|
$subscriber->update(['coupon_code' => $code]);
|
||||||
|
|
||||||
|
return;
|
||||||
|
} catch (WeeztixCouponCodeConflictException) {
|
||||||
|
continue;
|
||||||
|
} catch (Throwable $e) {
|
||||||
|
Log::error('Weeztix coupon creation failed', [
|
||||||
|
'subscriber_id' => $subscriber->id,
|
||||||
|
'message' => $e->getMessage(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Log::warning('Weeztix coupon: exhausted duplicate retries', [
|
||||||
|
'subscriber_id' => $subscriber->id,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,23 +6,23 @@ namespace App\Jobs;
|
|||||||
|
|
||||||
use App\Models\MailwizzConfig;
|
use App\Models\MailwizzConfig;
|
||||||
use App\Models\Subscriber;
|
use App\Models\Subscriber;
|
||||||
|
use App\Services\MailwizzCheckboxlistTags;
|
||||||
use App\Services\MailwizzService;
|
use App\Services\MailwizzService;
|
||||||
|
use App\Services\MailwizzSubscriberFormPayload;
|
||||||
use Illuminate\Bus\Queueable;
|
use Illuminate\Bus\Queueable;
|
||||||
use Illuminate\Contracts\Queue\ShouldBeUnique;
|
use Illuminate\Contracts\Queue\ShouldBeUnique;
|
||||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
use Illuminate\Contracts\Queue\ShouldQueueAfterCommit;
|
||||||
use Illuminate\Foundation\Bus\Dispatchable;
|
use Illuminate\Foundation\Bus\Dispatchable;
|
||||||
use Illuminate\Queue\InteractsWithQueue;
|
use Illuminate\Queue\InteractsWithQueue;
|
||||||
use Illuminate\Queue\SerializesModels;
|
|
||||||
use Illuminate\Support\Facades\Log;
|
use Illuminate\Support\Facades\Log;
|
||||||
use RuntimeException;
|
use RuntimeException;
|
||||||
use Throwable;
|
use Throwable;
|
||||||
|
|
||||||
class SyncSubscriberToMailwizz implements ShouldBeUnique, ShouldQueue
|
class SyncSubscriberToMailwizz implements ShouldBeUnique, ShouldQueueAfterCommit
|
||||||
{
|
{
|
||||||
use Dispatchable;
|
use Dispatchable;
|
||||||
use InteractsWithQueue;
|
use InteractsWithQueue;
|
||||||
use Queueable;
|
use Queueable;
|
||||||
use SerializesModels;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Seconds before the unique lock expires if the worker dies before releasing it.
|
* Seconds before the unique lock expires if the worker dies before releasing it.
|
||||||
@@ -36,21 +36,52 @@ class SyncSubscriberToMailwizz implements ShouldBeUnique, ShouldQueue
|
|||||||
*/
|
*/
|
||||||
public array $backoff = [10, 30, 60];
|
public array $backoff = [10, 30, 60];
|
||||||
|
|
||||||
public function __construct(public Subscriber $subscriber)
|
/**
|
||||||
|
* Set in the constructor for new jobs. Remains null when an old queue payload (pre–subscriber-id refactor) is unserialized.
|
||||||
|
*/
|
||||||
|
public ?int $subscriberId = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param Subscriber|int $subscriber Model is accepted when dispatching; only the id is serialized for the queue.
|
||||||
|
*/
|
||||||
|
public function __construct(Subscriber|int $subscriber)
|
||||||
{
|
{
|
||||||
|
$this->subscriberId = $subscriber instanceof Subscriber
|
||||||
|
? (int) $subscriber->getKey()
|
||||||
|
: $subscriber;
|
||||||
$this->onQueue('mailwizz');
|
$this->onQueue('mailwizz');
|
||||||
}
|
}
|
||||||
|
|
||||||
public function uniqueId(): string
|
public function uniqueId(): string
|
||||||
{
|
{
|
||||||
return (string) $this->subscriber->getKey();
|
return $this->subscriberId !== null
|
||||||
|
? (string) $this->subscriberId
|
||||||
|
: 'stale-mailwizz-sync-payload';
|
||||||
}
|
}
|
||||||
|
|
||||||
public function handle(): void
|
public function handle(): void
|
||||||
|
{
|
||||||
|
if ($this->subscriberId === null) {
|
||||||
|
Log::notice('SyncSubscriberToMailwizz: skipped job with missing subscriber id (stale queue payload). Clear the queue or re-dispatch sync jobs.');
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$this->runSync();
|
||||||
|
} catch (Throwable $e) {
|
||||||
|
Log::error('SyncSubscriberToMailwizz: integration failed; subscriber remains local (use admin resync if needed)', [
|
||||||
|
'subscriber_id' => $this->subscriberId,
|
||||||
|
'message' => $e->getMessage(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function runSync(): void
|
||||||
{
|
{
|
||||||
$subscriber = Subscriber::query()
|
$subscriber = Subscriber::query()
|
||||||
->with(['preregistrationPage.mailwizzConfig'])
|
->with(['preregistrationPage.mailwizzConfig'])
|
||||||
->find($this->subscriber->id);
|
->find($this->subscriberId);
|
||||||
|
|
||||||
if ($subscriber === null) {
|
if ($subscriber === null) {
|
||||||
return;
|
return;
|
||||||
@@ -101,7 +132,7 @@ class SyncSubscriberToMailwizz implements ShouldBeUnique, ShouldQueue
|
|||||||
public function failed(?Throwable $exception): void
|
public function failed(?Throwable $exception): void
|
||||||
{
|
{
|
||||||
Log::error('SyncSubscriberToMailwizz failed', [
|
Log::error('SyncSubscriberToMailwizz failed', [
|
||||||
'subscriber_id' => $this->subscriber->id,
|
'subscriber_id' => $this->subscriberId,
|
||||||
'message' => $exception?->getMessage(),
|
'message' => $exception?->getMessage(),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
@@ -119,29 +150,6 @@ class SyncSubscriberToMailwizz implements ShouldBeUnique, ShouldQueue
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private function buildBasePayload(Subscriber $subscriber, MailwizzConfig $config, bool $phoneEnabled): array
|
|
||||||
{
|
|
||||||
$data = [
|
|
||||||
$config->field_email => $subscriber->email,
|
|
||||||
$config->field_first_name => $subscriber->first_name,
|
|
||||||
$config->field_last_name => $subscriber->last_name,
|
|
||||||
];
|
|
||||||
|
|
||||||
if ($phoneEnabled && $config->field_phone !== null && $config->field_phone !== '') {
|
|
||||||
$phone = $subscriber->phoneDisplay();
|
|
||||||
if ($phone !== null && $phone !== '') {
|
|
||||||
$data[$config->field_phone] = $phone;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$couponField = $config->field_coupon_code;
|
|
||||||
if (is_string($couponField) && $couponField !== '' && $subscriber->coupon_code !== null && $subscriber->coupon_code !== '') {
|
|
||||||
$data[$couponField] = $subscriber->coupon_code;
|
|
||||||
}
|
|
||||||
|
|
||||||
return $data;
|
|
||||||
}
|
|
||||||
|
|
||||||
private function createInMailwizz(
|
private function createInMailwizz(
|
||||||
MailwizzService $service,
|
MailwizzService $service,
|
||||||
Subscriber $subscriber,
|
Subscriber $subscriber,
|
||||||
@@ -149,7 +157,7 @@ class SyncSubscriberToMailwizz implements ShouldBeUnique, ShouldQueue
|
|||||||
string $listUid
|
string $listUid
|
||||||
): void {
|
): void {
|
||||||
$page = $subscriber->preregistrationPage;
|
$page = $subscriber->preregistrationPage;
|
||||||
$data = $this->buildBasePayload($subscriber, $config, $page->isPhoneFieldEnabledForSubscribers());
|
$data = MailwizzSubscriberFormPayload::baseFields($subscriber, $config, $page->isPhoneFieldEnabledForSubscribers());
|
||||||
$tagField = $config->tag_field;
|
$tagField = $config->tag_field;
|
||||||
$tagValue = $config->tag_value;
|
$tagValue = $config->tag_value;
|
||||||
if ($tagField !== null && $tagField !== '' && $tagValue !== null && $tagValue !== '') {
|
if ($tagField !== null && $tagField !== '' && $tagValue !== null && $tagValue !== '') {
|
||||||
@@ -167,7 +175,7 @@ class SyncSubscriberToMailwizz implements ShouldBeUnique, ShouldQueue
|
|||||||
string $subscriberUid
|
string $subscriberUid
|
||||||
): void {
|
): void {
|
||||||
$page = $subscriber->preregistrationPage;
|
$page = $subscriber->preregistrationPage;
|
||||||
$data = $this->buildBasePayload($subscriber, $config, $page->isPhoneFieldEnabledForSubscribers());
|
$data = MailwizzSubscriberFormPayload::baseFields($subscriber, $config, $page->isPhoneFieldEnabledForSubscribers());
|
||||||
|
|
||||||
$tagField = $config->tag_field;
|
$tagField = $config->tag_field;
|
||||||
$tagValue = $config->tag_value;
|
$tagValue = $config->tag_value;
|
||||||
@@ -176,46 +184,11 @@ class SyncSubscriberToMailwizz implements ShouldBeUnique, ShouldQueue
|
|||||||
if ($full === null) {
|
if ($full === null) {
|
||||||
throw new RuntimeException('Mailwizz getSubscriber returned an empty payload.');
|
throw new RuntimeException('Mailwizz getSubscriber returned an empty payload.');
|
||||||
}
|
}
|
||||||
$existingCsv = $this->extractTagCsvFromResponse($full, $tagField);
|
$existingCsv = MailwizzCheckboxlistTags::extractCsvFromSubscriberResponse($full, $tagField);
|
||||||
$merged = $this->mergeCheckboxlistTags($existingCsv, $tagValue);
|
$merged = MailwizzCheckboxlistTags::mergeValueIntoCsv($existingCsv, $tagValue);
|
||||||
$data[$tagField] = $merged;
|
$data[$tagField] = $merged;
|
||||||
}
|
}
|
||||||
|
|
||||||
$service->updateSubscriber($listUid, $subscriberUid, $data);
|
$service->updateSubscriber($listUid, $subscriberUid, $data);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @param array<string, mixed> $apiResponse
|
|
||||||
*/
|
|
||||||
private function extractTagCsvFromResponse(array $apiResponse, string $tagField): string
|
|
||||||
{
|
|
||||||
$record = data_get($apiResponse, 'data.record');
|
|
||||||
if (! is_array($record)) {
|
|
||||||
$record = data_get($apiResponse, 'data');
|
|
||||||
}
|
|
||||||
if (! is_array($record)) {
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
|
|
||||||
$raw = $record[$tagField] ?? data_get($record, "fields.{$tagField}");
|
|
||||||
|
|
||||||
if (is_array($raw)) {
|
|
||||||
return implode(',', array_map(static fn (mixed $v): string => is_scalar($v) ? (string) $v : '', $raw));
|
|
||||||
}
|
|
||||||
|
|
||||||
return is_string($raw) ? $raw : '';
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return list<string>
|
|
||||||
*/
|
|
||||||
private function mergeCheckboxlistTags(string $existingCsv, string $newValue): array
|
|
||||||
{
|
|
||||||
$parts = array_filter(array_map('trim', explode(',', $existingCsv)), static fn (string $s): bool => $s !== '');
|
|
||||||
if (! in_array($newValue, $parts, true)) {
|
|
||||||
$parts[] = $newValue;
|
|
||||||
}
|
|
||||||
|
|
||||||
return array_values($parts);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -204,4 +204,56 @@ class PreregistrationPage extends Model
|
|||||||
|
|
||||||
return 'active';
|
return 'active';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mailwizz setup depth for admin UI (API key + list + email field = ready to sync).
|
||||||
|
*
|
||||||
|
* @return 'none'|'partial'|'ready'
|
||||||
|
*/
|
||||||
|
public function mailwizzIntegrationStatus(): string
|
||||||
|
{
|
||||||
|
$c = $this->mailwizzConfig;
|
||||||
|
if ($c === null) {
|
||||||
|
return 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
$key = $c->api_key;
|
||||||
|
if (! is_string($key) || $key === '') {
|
||||||
|
return 'partial';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (! is_string($c->list_uid) || $c->list_uid === '' || ! is_string($c->field_email) || $c->field_email === '') {
|
||||||
|
return 'partial';
|
||||||
|
}
|
||||||
|
|
||||||
|
return 'ready';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Weeztix setup depth for admin UI.
|
||||||
|
*
|
||||||
|
* @return 'none'|'credentials'|'connected'|'ready'
|
||||||
|
*/
|
||||||
|
public function weeztixIntegrationStatus(): string
|
||||||
|
{
|
||||||
|
$c = $this->weeztixConfig;
|
||||||
|
if ($c === null) {
|
||||||
|
return 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
$hasClient = is_string($c->client_id) && $c->client_id !== '';
|
||||||
|
if (! $hasClient) {
|
||||||
|
return 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (! $c->is_connected) {
|
||||||
|
return 'credentials';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (! is_string($c->coupon_guid) || $c->coupon_guid === '') {
|
||||||
|
return 'connected';
|
||||||
|
}
|
||||||
|
|
||||||
|
return 'ready';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
145
app/Services/CleanupSubscriberIntegrationsService.php
Normal file
145
app/Services/CleanupSubscriberIntegrationsService.php
Normal file
@@ -0,0 +1,145 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Services;
|
||||||
|
|
||||||
|
use App\Models\MailwizzConfig;
|
||||||
|
use App\Models\PreregistrationPage;
|
||||||
|
use App\Models\Subscriber;
|
||||||
|
use App\Models\WeeztixConfig;
|
||||||
|
use Illuminate\Support\Facades\Log;
|
||||||
|
use RuntimeException;
|
||||||
|
use Throwable;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Before a subscriber row is deleted, best-effort cleanup in Weeztix (coupon code) and Mailwizz (strip source tag, clear coupon field).
|
||||||
|
* Failures are logged only; local delete must still proceed.
|
||||||
|
*/
|
||||||
|
final class CleanupSubscriberIntegrationsService
|
||||||
|
{
|
||||||
|
public function runBeforeDelete(Subscriber $subscriber): void
|
||||||
|
{
|
||||||
|
$subscriber->loadMissing(['preregistrationPage.mailwizzConfig', 'preregistrationPage.weeztixConfig']);
|
||||||
|
$page = $subscriber->preregistrationPage;
|
||||||
|
if ($page === null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$this->cleanupWeeztixIfApplicable($subscriber, $page);
|
||||||
|
} catch (Throwable $e) {
|
||||||
|
Log::error('CleanupSubscriberIntegrations: Weeztix cleanup failed', [
|
||||||
|
'subscriber_id' => $subscriber->id,
|
||||||
|
'preregistration_page_id' => $page->id,
|
||||||
|
'message' => $e->getMessage(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$this->cleanupMailwizzIfApplicable($subscriber, $page);
|
||||||
|
} catch (Throwable $e) {
|
||||||
|
Log::error('CleanupSubscriberIntegrations: Mailwizz cleanup failed', [
|
||||||
|
'subscriber_id' => $subscriber->id,
|
||||||
|
'preregistration_page_id' => $page->id,
|
||||||
|
'message' => $e->getMessage(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function cleanupWeeztixIfApplicable(Subscriber $subscriber, PreregistrationPage $page): void
|
||||||
|
{
|
||||||
|
$config = $page->weeztixConfig;
|
||||||
|
if ($config === null || ! $config->is_connected) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$code = $subscriber->coupon_code;
|
||||||
|
if (! is_string($code) || trim($code) === '') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (! $this->weeztixCanManageCodes($config)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$fresh = $config->fresh();
|
||||||
|
if ($fresh === null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
(new WeeztixService($fresh))->deleteCouponCodeByCodeString($code);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function weeztixCanManageCodes(WeeztixConfig $config): bool
|
||||||
|
{
|
||||||
|
$company = $config->company_guid;
|
||||||
|
$coupon = $config->coupon_guid;
|
||||||
|
|
||||||
|
return is_string($company) && $company !== '' && is_string($coupon) && $coupon !== '';
|
||||||
|
}
|
||||||
|
|
||||||
|
private function cleanupMailwizzIfApplicable(Subscriber $subscriber, PreregistrationPage $page): void
|
||||||
|
{
|
||||||
|
$config = $page->mailwizzConfig;
|
||||||
|
if ($config === null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (! $this->mailwizzConfigAllowsUpdate($config)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$apiKey = $config->api_key;
|
||||||
|
if (! is_string($apiKey) || $apiKey === '') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$service = new MailwizzService($apiKey);
|
||||||
|
$listUid = $config->list_uid;
|
||||||
|
|
||||||
|
$search = $service->searchSubscriber($listUid, $subscriber->email);
|
||||||
|
if ($search === null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$subscriberUid = $search['subscriber_uid'];
|
||||||
|
/** @var array<string, mixed> $data */
|
||||||
|
$data = [];
|
||||||
|
|
||||||
|
$couponField = $config->field_coupon_code;
|
||||||
|
if (is_string($couponField) && $couponField !== '') {
|
||||||
|
$data[$couponField] = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
$tagField = $config->tag_field;
|
||||||
|
$tagValue = $config->tag_value;
|
||||||
|
if ($tagField !== null && $tagField !== '' && $tagValue !== null && $tagValue !== '') {
|
||||||
|
$full = $service->getSubscriber($listUid, $subscriberUid);
|
||||||
|
if ($full === null) {
|
||||||
|
throw new RuntimeException('Mailwizz getSubscriber returned an empty payload.');
|
||||||
|
}
|
||||||
|
$existingCsv = MailwizzCheckboxlistTags::extractCsvFromSubscriberResponse($full, $tagField);
|
||||||
|
$data[$tagField] = MailwizzCheckboxlistTags::removeValueFromCsv($existingCsv, $tagValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($data === []) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$service->updateSubscriber($listUid, $subscriberUid, $data);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function mailwizzConfigAllowsUpdate(MailwizzConfig $config): bool
|
||||||
|
{
|
||||||
|
if ($config->list_uid === '' || $config->field_email === '' || $config->field_first_name === '' || $config->field_last_name === '') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$hasTagWork = $config->tag_field !== null && $config->tag_field !== ''
|
||||||
|
&& $config->tag_value !== null && $config->tag_value !== '';
|
||||||
|
$hasCouponField = is_string($config->field_coupon_code) && $config->field_coupon_code !== '';
|
||||||
|
|
||||||
|
return $hasTagWork || $hasCouponField;
|
||||||
|
}
|
||||||
|
}
|
||||||
64
app/Services/MailwizzCheckboxlistTags.php
Normal file
64
app/Services/MailwizzCheckboxlistTags.php
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Services;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parses and mutates Mailwizz checkboxlist-style tag fields (comma-separated in API, array on write).
|
||||||
|
*/
|
||||||
|
final class MailwizzCheckboxlistTags
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @param array<string, mixed> $apiResponse getSubscriber JSON payload
|
||||||
|
*/
|
||||||
|
public static function extractCsvFromSubscriberResponse(array $apiResponse, string $tagField): string
|
||||||
|
{
|
||||||
|
$record = data_get($apiResponse, 'data.record');
|
||||||
|
if (! is_array($record)) {
|
||||||
|
$record = data_get($apiResponse, 'data');
|
||||||
|
}
|
||||||
|
if (! is_array($record)) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
$raw = $record[$tagField] ?? data_get($record, "fields.{$tagField}");
|
||||||
|
|
||||||
|
if (is_array($raw)) {
|
||||||
|
return implode(',', array_map(static fn (mixed $v): string => is_scalar($v) ? (string) $v : '', $raw));
|
||||||
|
}
|
||||||
|
|
||||||
|
return is_string($raw) ? $raw : '';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return list<string>
|
||||||
|
*/
|
||||||
|
public static function mergeValueIntoCsv(string $existingCsv, string $newValue): array
|
||||||
|
{
|
||||||
|
$parts = array_filter(array_map('trim', explode(',', $existingCsv)), static fn (string $s): bool => $s !== '');
|
||||||
|
if (! in_array($newValue, $parts, true)) {
|
||||||
|
$parts[] = $newValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
return array_values($parts);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return list<string>
|
||||||
|
*/
|
||||||
|
public static function removeValueFromCsv(string $existingCsv, string $valueToRemove): array
|
||||||
|
{
|
||||||
|
$needle = trim($valueToRemove);
|
||||||
|
$parts = array_filter(array_map('trim', explode(',', $existingCsv)), static fn (string $s): bool => $s !== '');
|
||||||
|
|
||||||
|
if ($needle === '') {
|
||||||
|
return array_values($parts);
|
||||||
|
}
|
||||||
|
|
||||||
|
return array_values(array_filter(
|
||||||
|
$parts,
|
||||||
|
static fn (string $s): bool => $s !== $needle
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -202,7 +202,8 @@ final class MailwizzService
|
|||||||
$out = [];
|
$out = [];
|
||||||
foreach ($data as $key => $value) {
|
foreach ($data as $key => $value) {
|
||||||
if (is_array($value)) {
|
if (is_array($value)) {
|
||||||
$out[$key] = $value;
|
// Empty arrays are omitted by Laravel's multipart encoder (no KEY[] parts), so Mailwizz never clears checkboxlist fields.
|
||||||
|
$out[$key] = $value === [] ? '' : $value;
|
||||||
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|||||||
40
app/Services/MailwizzSubscriberFormPayload.php
Normal file
40
app/Services/MailwizzSubscriberFormPayload.php
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Services;
|
||||||
|
|
||||||
|
use App\Models\MailwizzConfig;
|
||||||
|
use App\Models\Subscriber;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shared Mailwizz list subscriber field payload for create/update (excluding tag merge logic).
|
||||||
|
*/
|
||||||
|
final class MailwizzSubscriberFormPayload
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @return array<string, mixed>
|
||||||
|
*/
|
||||||
|
public static function baseFields(Subscriber $subscriber, MailwizzConfig $config, bool $phoneEnabled): array
|
||||||
|
{
|
||||||
|
$data = [
|
||||||
|
$config->field_email => $subscriber->email,
|
||||||
|
$config->field_first_name => $subscriber->first_name,
|
||||||
|
$config->field_last_name => $subscriber->last_name,
|
||||||
|
];
|
||||||
|
|
||||||
|
if ($phoneEnabled && $config->field_phone !== null && $config->field_phone !== '') {
|
||||||
|
$phone = $subscriber->phoneDisplay();
|
||||||
|
if ($phone !== null && $phone !== '') {
|
||||||
|
$data[$config->field_phone] = $phone;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$couponField = $config->field_coupon_code;
|
||||||
|
if (is_string($couponField) && $couponField !== '' && $subscriber->coupon_code !== null && $subscriber->coupon_code !== '') {
|
||||||
|
$data[$couponField] = $subscriber->coupon_code;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $data;
|
||||||
|
}
|
||||||
|
}
|
||||||
59
app/Services/RegisterSubscriberOnPage.php
Normal file
59
app/Services/RegisterSubscriberOnPage.php
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Services;
|
||||||
|
|
||||||
|
use App\Jobs\IssueWeeztixCouponForSubscriber;
|
||||||
|
use App\Jobs\SyncSubscriberToMailwizz;
|
||||||
|
use App\Models\PreregistrationPage;
|
||||||
|
use App\Models\Subscriber;
|
||||||
|
use App\Models\WeeztixConfig;
|
||||||
|
use Illuminate\Support\Facades\Log;
|
||||||
|
use Throwable;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Orchestrates public registration: local persist first, then queue external integrations
|
||||||
|
* so Weeztix/Mailwizz failures never prevent a subscriber row from being stored.
|
||||||
|
*/
|
||||||
|
final class RegisterSubscriberOnPage
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @param array<string, mixed> $validated
|
||||||
|
*/
|
||||||
|
public function storeAndQueueIntegrations(PreregistrationPage $page, array $validated): Subscriber
|
||||||
|
{
|
||||||
|
$subscriber = $page->subscribers()->create($validated);
|
||||||
|
|
||||||
|
$page->loadMissing('weeztixConfig', 'mailwizzConfig');
|
||||||
|
$weeztix = $page->weeztixConfig;
|
||||||
|
|
||||||
|
try {
|
||||||
|
if ($this->weeztixCanIssueCodes($weeztix)) {
|
||||||
|
IssueWeeztixCouponForSubscriber::dispatch($subscriber)->afterResponse();
|
||||||
|
} elseif ($page->mailwizzConfig !== null) {
|
||||||
|
SyncSubscriberToMailwizz::dispatch($subscriber->fresh())->afterResponse();
|
||||||
|
}
|
||||||
|
} catch (Throwable $e) {
|
||||||
|
Log::error('RegisterSubscriberOnPage: could not queue integration jobs', [
|
||||||
|
'subscriber_id' => $subscriber->id,
|
||||||
|
'preregistration_page_id' => $page->id,
|
||||||
|
'message' => $e->getMessage(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $subscriber;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function weeztixCanIssueCodes(?WeeztixConfig $config): bool
|
||||||
|
{
|
||||||
|
if ($config === null || ! $config->is_connected) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$company = $config->company_guid;
|
||||||
|
$coupon = $config->coupon_guid;
|
||||||
|
|
||||||
|
return is_string($company) && $company !== '' && is_string($coupon) && $coupon !== '';
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -13,6 +13,7 @@ use Illuminate\Support\Facades\Log;
|
|||||||
use InvalidArgumentException;
|
use InvalidArgumentException;
|
||||||
use LogicException;
|
use LogicException;
|
||||||
use RuntimeException;
|
use RuntimeException;
|
||||||
|
use Throwable;
|
||||||
|
|
||||||
final class WeeztixService
|
final class WeeztixService
|
||||||
{
|
{
|
||||||
@@ -84,6 +85,39 @@ final class WeeztixService
|
|||||||
return $this->normalizeCompaniesFromProfile($json);
|
return $this->normalizeCompaniesFromProfile($json);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sync company_guid/name from the Weeztix user profile for the current access token (after OAuth).
|
||||||
|
* Runs on every successful connect or reconnect so a different company chosen in Weeztix is stored here.
|
||||||
|
* Uses the first company returned from the profile when several are present.
|
||||||
|
*/
|
||||||
|
public function ensureCompanyStoredFromWeeztix(): void
|
||||||
|
{
|
||||||
|
$this->config->refresh();
|
||||||
|
|
||||||
|
try {
|
||||||
|
$companies = $this->getCompanies();
|
||||||
|
if ($companies === []) {
|
||||||
|
Log::warning('Weeztix: geen bedrijf uit profiel voor automatische koppeling.', [
|
||||||
|
'weeztix_config_id' => $this->config->id,
|
||||||
|
]);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$row = $companies[0];
|
||||||
|
$this->config->update([
|
||||||
|
'company_guid' => $row['guid'],
|
||||||
|
'company_name' => $row['name'],
|
||||||
|
]);
|
||||||
|
$this->config->refresh();
|
||||||
|
} catch (Throwable $e) {
|
||||||
|
Log::warning('Weeztix: automatisch bedrijf vastleggen mislukt', [
|
||||||
|
'weeztix_config_id' => $this->config->id,
|
||||||
|
'message' => $e->getMessage(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Exchange OAuth authorization code for tokens (admin callback).
|
* Exchange OAuth authorization code for tokens (admin callback).
|
||||||
*/
|
*/
|
||||||
@@ -216,6 +250,159 @@ final class WeeztixService
|
|||||||
throw new RuntimeException('Weeztix API rate limited after retries.');
|
throw new RuntimeException('Weeztix API rate limited after retries.');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Lists coupon codes for the coupon selected in config (GET /coupon/{guid}/codes).
|
||||||
|
*
|
||||||
|
* @return list<array{guid: string, code: string}>
|
||||||
|
*/
|
||||||
|
public function listCouponCodesForConfiguredCoupon(): array
|
||||||
|
{
|
||||||
|
$this->assertCompanyGuid();
|
||||||
|
$couponGuid = $this->config->coupon_guid;
|
||||||
|
if (! is_string($couponGuid) || $couponGuid === '') {
|
||||||
|
throw new LogicException('Weeztix coupon is not configured.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$url = config('weeztix.api_base_url').'/coupon/'.$couponGuid.'/codes';
|
||||||
|
$json = $this->apiRequest('get', $url, []);
|
||||||
|
|
||||||
|
return $this->normalizeCouponCodeListResponse($json);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Soft-deletes a coupon code in Weeztix by matching the human-readable code string.
|
||||||
|
*/
|
||||||
|
public function deleteCouponCodeByCodeString(string $code): void
|
||||||
|
{
|
||||||
|
$trimmed = trim($code);
|
||||||
|
if ($trimmed === '') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->assertCompanyGuid();
|
||||||
|
$couponGuid = $this->config->coupon_guid;
|
||||||
|
if (! is_string($couponGuid) || $couponGuid === '') {
|
||||||
|
throw new LogicException('Weeztix coupon is not configured.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$rows = $this->listCouponCodesForConfiguredCoupon();
|
||||||
|
$codeGuid = null;
|
||||||
|
foreach ($rows as $row) {
|
||||||
|
if (strcasecmp($row['code'], $trimmed) === 0) {
|
||||||
|
$codeGuid = $row['guid'];
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($codeGuid === null) {
|
||||||
|
Log::info('Weeztix: coupon code not found when deleting (already removed or unknown)', [
|
||||||
|
'code' => $trimmed,
|
||||||
|
]);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$url = config('weeztix.api_base_url').'/coupon/'.$couponGuid.'/codes/'.$codeGuid;
|
||||||
|
$token = $this->getValidAccessToken();
|
||||||
|
$response = $this->sendApiRequest('delete', $url, [], $token);
|
||||||
|
|
||||||
|
if ($response->status() === 401) {
|
||||||
|
$this->refreshAccessToken();
|
||||||
|
$this->config->refresh();
|
||||||
|
$response = $this->sendApiRequest('delete', $url, [], (string) $this->config->access_token);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($response->status() === 404) {
|
||||||
|
Log::info('Weeztix: coupon code already deleted remotely', [
|
||||||
|
'code' => $trimmed,
|
||||||
|
'code_guid' => $codeGuid,
|
||||||
|
]);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($response->failed()) {
|
||||||
|
$this->logFailedResponse('deleteCouponCodeByCodeString', $url, $response);
|
||||||
|
|
||||||
|
throw new RuntimeException('Weeztix API delete coupon code failed: '.$response->status());
|
||||||
|
}
|
||||||
|
|
||||||
|
Log::debug('Weeztix API', [
|
||||||
|
'action' => 'deleteCouponCodeByCodeString',
|
||||||
|
'url' => $url,
|
||||||
|
'http_status' => $response->status(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<string, mixed> $json
|
||||||
|
* @return list<array{guid: string, code: string}>
|
||||||
|
*/
|
||||||
|
private function normalizeCouponCodeListResponse(array $json): array
|
||||||
|
{
|
||||||
|
$candidates = [
|
||||||
|
data_get($json, 'data'),
|
||||||
|
data_get($json, 'data.codes'),
|
||||||
|
data_get($json, 'data.records'),
|
||||||
|
data_get($json, 'codes'),
|
||||||
|
$json,
|
||||||
|
];
|
||||||
|
|
||||||
|
foreach ($candidates as $raw) {
|
||||||
|
if (! is_array($raw)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($this->isListArray($raw)) {
|
||||||
|
$normalized = $this->normalizeCouponCodeRows($raw);
|
||||||
|
if ($normalized !== []) {
|
||||||
|
return $normalized;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<int, mixed> $rows
|
||||||
|
* @return list<array{guid: string, code: string}>
|
||||||
|
*/
|
||||||
|
private function normalizeCouponCodeRows(array $rows): array
|
||||||
|
{
|
||||||
|
$out = [];
|
||||||
|
foreach ($rows as $row) {
|
||||||
|
if (! is_array($row)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$guid = data_get($row, 'guid')
|
||||||
|
?? data_get($row, 'id')
|
||||||
|
?? data_get($row, 'coupon_code_guid');
|
||||||
|
$code = data_get($row, 'code')
|
||||||
|
?? data_get($row, 'coupon_code')
|
||||||
|
?? data_get($row, 'name');
|
||||||
|
if (! is_string($guid) || $guid === '' || ! is_string($code) || $code === '') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$out[] = ['guid' => $guid, 'code' => $code];
|
||||||
|
}
|
||||||
|
|
||||||
|
return $out;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<int, mixed> $arr
|
||||||
|
*/
|
||||||
|
private function isListArray(array $arr): bool
|
||||||
|
{
|
||||||
|
if ($arr === []) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return array_keys($arr) === range(0, count($arr) - 1);
|
||||||
|
}
|
||||||
|
|
||||||
public static function generateUniqueCode(string $prefix = 'PREREG', int $length = 6): string
|
public static function generateUniqueCode(string $prefix = 'PREREG', int $length = 6): string
|
||||||
{
|
{
|
||||||
$chars = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789';
|
$chars = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789';
|
||||||
|
|||||||
@@ -269,6 +269,12 @@ Laravel ships with a `public/.htaccess` that works with Apache. Verify `mod_rewr
|
|||||||
|
|
||||||
### 4.7 Set up cron for queue worker + scheduler
|
### 4.7 Set up cron for queue worker + scheduler
|
||||||
|
|
||||||
|
Public registration saves the subscriber in the database first, then queues **Weeztix** (coupon code) and **Mailwizz** sync jobs. Those jobs must be processed by a worker.
|
||||||
|
|
||||||
|
- **Visitor-facing behaviour:** the public subscribe endpoint returns **HTTP 200 with `success: true`** as soon as the subscriber row is stored. Failures in Mailwizz or Weeztix are **logged** (and visible via failed jobs when using a real queue); they do **not** change the JSON shown to the visitor. Use logs and admin resync to diagnose integration issues.
|
||||||
|
- **Production:** set `QUEUE_CONNECTION=database` (or `redis`) so retries and `queue:failed` work as intended. `sync` is acceptable for small installs but runs integration work in-process; still, the visitor should not see 5xx from a broken Mailwizz/Weeztix API after subscribe.
|
||||||
|
- **Queues:** coupon jobs use `weeztix`; Mailwizz uses `mailwizz`. The worker should listen to both (order below prioritises `weeztix` so coupon creation tends to run before sync when both are pending).
|
||||||
|
|
||||||
In DirectAdmin → Cron Jobs, add:
|
In DirectAdmin → Cron Jobs, add:
|
||||||
|
|
||||||
```
|
```
|
||||||
@@ -276,9 +282,11 @@ In DirectAdmin → Cron Jobs, add:
|
|||||||
* * * * * cd /home/username/preregister && php artisan schedule:run >> /dev/null 2>&1
|
* * * * * cd /home/username/preregister && php artisan schedule:run >> /dev/null 2>&1
|
||||||
|
|
||||||
# Queue worker - process one job per run (every minute)
|
# Queue worker - process one job per run (every minute)
|
||||||
* * * * * cd /home/username/preregister && php artisan queue:work --once --queue=mailwizz >> /dev/null 2>&1
|
* * * * * cd /home/username/preregister && php artisan queue:work --once --queue=weeztix,mailwizz,default >> /dev/null 2>&1
|
||||||
```
|
```
|
||||||
|
|
||||||
|
For higher throughput, use a persistent supervisor-managed worker instead of `--once` in cron; keep the same `--queue=weeztix,mailwizz,default` order.
|
||||||
|
|
||||||
### 4.8 Directory permissions
|
### 4.8 Directory permissions
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|||||||
@@ -14,6 +14,7 @@
|
|||||||
"Sending…": "Bezig met verzenden…",
|
"Sending…": "Bezig met verzenden…",
|
||||||
"Something went wrong. Please try again.": "Er ging iets mis. Probeer het opnieuw.",
|
"Something went wrong. Please try again.": "Er ging iets mis. Probeer het opnieuw.",
|
||||||
"This pre-registration period has ended.": "Deze preregistratieperiode is afgelopen.",
|
"This pre-registration period has ended.": "Deze preregistratieperiode is afgelopen.",
|
||||||
|
"You will be redirected in :seconds s…": "Je wordt over :seconds seconden doorgestuurd…",
|
||||||
"Visit ticket shop": "Ga naar de ticketshop",
|
"Visit ticket shop": "Ga naar de ticketshop",
|
||||||
"Thank you for registering!": "Bedankt voor je registratie!",
|
"Thank you for registering!": "Bedankt voor je registratie!",
|
||||||
"You are already registered for this event.": "Je bent al geregistreerd voor dit evenement.",
|
"You are already registered for this event.": "Je bent al geregistreerd voor dit evenement.",
|
||||||
|
|||||||
@@ -420,7 +420,6 @@ document.addEventListener('alpine:init', () => {
|
|||||||
redirectSecondsLeft: null,
|
redirectSecondsLeft: null,
|
||||||
redirectTimer: null,
|
redirectTimer: null,
|
||||||
strings: config.strings || {},
|
strings: config.strings || {},
|
||||||
couponCode: '',
|
|
||||||
|
|
||||||
copyPageLink() {
|
copyPageLink() {
|
||||||
const url = this.pageShareUrl;
|
const url = this.pageShareUrl;
|
||||||
@@ -435,19 +434,6 @@ document.addEventListener('alpine:init', () => {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
copyCouponCode() {
|
|
||||||
const code = this.couponCode;
|
|
||||||
if (!code) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
navigator.clipboard.writeText(code).then(() => {
|
|
||||||
this.copyFeedback = this.strings?.couponCopied || '';
|
|
||||||
setTimeout(() => {
|
|
||||||
this.copyFeedback = '';
|
|
||||||
}, 2500);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
init() {
|
init() {
|
||||||
if (this.phase === 'before') {
|
if (this.phase === 'before') {
|
||||||
this.tickCountdown();
|
this.tickCountdown();
|
||||||
@@ -566,17 +552,24 @@ document.addEventListener('alpine:init', () => {
|
|||||||
if (res.ok && data.success) {
|
if (res.ok && data.success) {
|
||||||
this.phase = 'thanks';
|
this.phase = 'thanks';
|
||||||
this.thankYouMessage = data.message ?? '';
|
this.thankYouMessage = data.message ?? '';
|
||||||
this.couponCode =
|
|
||||||
typeof data.coupon_code === 'string' && data.coupon_code !== '' ? data.coupon_code : '';
|
|
||||||
this.startRedirectCountdownIfNeeded();
|
this.startRedirectCountdownIfNeeded();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (typeof data.message === 'string' && data.message !== '') {
|
const hasServerMessage = typeof data.message === 'string' && data.message !== '';
|
||||||
|
const hasFieldErrors =
|
||||||
|
data.errors !== undefined &&
|
||||||
|
data.errors !== null &&
|
||||||
|
typeof data.errors === 'object' &&
|
||||||
|
Object.keys(data.errors).length > 0;
|
||||||
|
if (hasServerMessage) {
|
||||||
this.formError = data.message;
|
this.formError = data.message;
|
||||||
}
|
}
|
||||||
if (data.errors !== undefined && data.errors !== null && typeof data.errors === 'object') {
|
if (hasFieldErrors) {
|
||||||
this.fieldErrors = data.errors;
|
this.fieldErrors = data.errors;
|
||||||
}
|
}
|
||||||
|
if (!res.ok && !hasServerMessage && !hasFieldErrors) {
|
||||||
|
this.formError = this.genericError;
|
||||||
|
}
|
||||||
} catch {
|
} catch {
|
||||||
this.formError = this.genericError;
|
this.formError = this.genericError;
|
||||||
} finally {
|
} finally {
|
||||||
@@ -769,20 +762,17 @@ document.addEventListener('alpine:init', () => {
|
|||||||
|
|
||||||
Alpine.data('weeztixSetup', (cfg) => ({
|
Alpine.data('weeztixSetup', (cfg) => ({
|
||||||
pageId: cfg.pageId,
|
pageId: cfg.pageId,
|
||||||
companiesUrl: cfg.companiesUrl,
|
|
||||||
couponsUrl: cfg.couponsUrl,
|
couponsUrl: cfg.couponsUrl,
|
||||||
csrf: cfg.csrf,
|
csrf: cfg.csrf,
|
||||||
isConnected: cfg.isConnected === true,
|
isConnected: cfg.isConnected === true,
|
||||||
callbackUrl: cfg.callbackUrl,
|
callbackUrl: cfg.callbackUrl,
|
||||||
errorMessage: '',
|
errorMessage: '',
|
||||||
companies: [],
|
|
||||||
coupons: [],
|
coupons: [],
|
||||||
companyGuid: '',
|
|
||||||
companyName: '',
|
|
||||||
couponGuid: '',
|
couponGuid: '',
|
||||||
couponName: '',
|
couponName: '',
|
||||||
codePrefix: 'PREREG',
|
codePrefix: 'PREREG',
|
||||||
usageCount: 1,
|
usageCount: 1,
|
||||||
|
couponsRefreshing: false,
|
||||||
strings: cfg.strings || {},
|
strings: cfg.strings || {},
|
||||||
|
|
||||||
async init() {
|
async init() {
|
||||||
@@ -797,19 +787,12 @@ document.addEventListener('alpine:init', () => {
|
|||||||
} else {
|
} else {
|
||||||
this.usageCount = 1;
|
this.usageCount = 1;
|
||||||
}
|
}
|
||||||
this.companyGuid = cfg.existing.company_guid || '';
|
|
||||||
this.companyName = cfg.existing.company_name || '';
|
|
||||||
this.couponGuid = cfg.existing.coupon_guid || '';
|
this.couponGuid = cfg.existing.coupon_guid || '';
|
||||||
this.couponName = cfg.existing.coupon_name || '';
|
this.couponName = cfg.existing.coupon_name || '';
|
||||||
}
|
}
|
||||||
if (this.isConnected) {
|
if (this.isConnected) {
|
||||||
await this.loadCompanies();
|
await this.loadCoupons();
|
||||||
if (this.companyGuid) {
|
} else if (cfg.existing && cfg.existing.coupon_guid) {
|
||||||
await this.loadCouponsForGuid(this.companyGuid);
|
|
||||||
}
|
|
||||||
} else if (cfg.existing && (cfg.existing.company_guid || cfg.existing.coupon_guid)) {
|
|
||||||
// Show saved choices even when not connected (e.g. expired refresh); lists are from DB only.
|
|
||||||
this.ensureSelectedCompanyInList();
|
|
||||||
this.ensureSelectedCouponInList();
|
this.ensureSelectedCouponInList();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -829,18 +812,6 @@ document.addEventListener('alpine:init', () => {
|
|||||||
return { res, data };
|
return { res, data };
|
||||||
},
|
},
|
||||||
|
|
||||||
syncCompanyNameFromSelection() {
|
|
||||||
if (!this.companyGuid) {
|
|
||||||
this.companyName = '';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const c = this.companies.find((x) => x.guid === this.companyGuid);
|
|
||||||
if (c && typeof c.name === 'string' && c.name.trim() !== '') {
|
|
||||||
this.companyName = c.name.trim();
|
|
||||||
}
|
|
||||||
// If API row has no name or list is still loading, keep companyName from server (DB).
|
|
||||||
},
|
|
||||||
|
|
||||||
syncCouponName() {
|
syncCouponName() {
|
||||||
if (!this.couponGuid) {
|
if (!this.couponGuid) {
|
||||||
this.couponName = '';
|
this.couponName = '';
|
||||||
@@ -852,18 +823,6 @@ document.addEventListener('alpine:init', () => {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
ensureSelectedCompanyInList() {
|
|
||||||
const guid = this.companyGuid;
|
|
||||||
if (!guid || this.companies.some((x) => x.guid === guid)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const label =
|
|
||||||
typeof this.companyName === 'string' && this.companyName.trim() !== ''
|
|
||||||
? this.companyName.trim()
|
|
||||||
: guid;
|
|
||||||
this.companies = [{ guid, name: label }, ...this.companies];
|
|
||||||
},
|
|
||||||
|
|
||||||
ensureSelectedCouponInList() {
|
ensureSelectedCouponInList() {
|
||||||
const guid = this.couponGuid;
|
const guid = this.couponGuid;
|
||||||
if (!guid || this.coupons.some((x) => x.guid === guid)) {
|
if (!guid || this.coupons.some((x) => x.guid === guid)) {
|
||||||
@@ -876,36 +835,9 @@ document.addEventListener('alpine:init', () => {
|
|||||||
this.coupons = [{ guid, name: label }, ...this.coupons];
|
this.coupons = [{ guid, name: label }, ...this.coupons];
|
||||||
},
|
},
|
||||||
|
|
||||||
async loadCompanies() {
|
async loadCoupons() {
|
||||||
this.errorMessage = '';
|
this.errorMessage = '';
|
||||||
const { res, data } = await this.postJson(this.companiesUrl, { page_id: this.pageId });
|
const { res, data } = await this.postJson(this.couponsUrl, { page_id: this.pageId });
|
||||||
if (!res.ok) {
|
|
||||||
this.errorMessage = data.message || this.strings.genericError;
|
|
||||||
this.ensureSelectedCompanyInList();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.companies = Array.isArray(data.companies) ? data.companies : [];
|
|
||||||
this.ensureSelectedCompanyInList();
|
|
||||||
this.syncCompanyNameFromSelection();
|
|
||||||
},
|
|
||||||
|
|
||||||
async onCompanyChange() {
|
|
||||||
this.syncCompanyNameFromSelection();
|
|
||||||
this.couponGuid = '';
|
|
||||||
this.couponName = '';
|
|
||||||
this.coupons = [];
|
|
||||||
if (!this.companyGuid) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
await this.loadCouponsForGuid(this.companyGuid);
|
|
||||||
},
|
|
||||||
|
|
||||||
async loadCouponsForGuid(guid) {
|
|
||||||
this.errorMessage = '';
|
|
||||||
const { res, data } = await this.postJson(this.couponsUrl, {
|
|
||||||
page_id: this.pageId,
|
|
||||||
company_guid: guid,
|
|
||||||
});
|
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
this.errorMessage = data.message || this.strings.loadCouponsError;
|
this.errorMessage = data.message || this.strings.loadCouponsError;
|
||||||
this.ensureSelectedCouponInList();
|
this.ensureSelectedCouponInList();
|
||||||
@@ -916,35 +848,16 @@ document.addEventListener('alpine:init', () => {
|
|||||||
this.syncCouponName();
|
this.syncCouponName();
|
||||||
},
|
},
|
||||||
|
|
||||||
/** Prefer human-readable label; skip API "names" that are just the GUID / UUID. */
|
async refreshCoupons() {
|
||||||
companyLabel(c) {
|
if (!this.isConnected) {
|
||||||
if (!c || typeof c.guid !== 'string') {
|
return;
|
||||||
return '';
|
|
||||||
}
|
}
|
||||||
const g = c.guid;
|
this.couponsRefreshing = true;
|
||||||
const isBadLabel = (s) => {
|
try {
|
||||||
const t = typeof s === 'string' ? s.trim() : '';
|
await this.loadCoupons();
|
||||||
return (
|
} finally {
|
||||||
t === '' ||
|
this.couponsRefreshing = false;
|
||||||
t.toLowerCase() === g.toLowerCase() ||
|
|
||||||
this.stringLooksLikeUuid(t)
|
|
||||||
);
|
|
||||||
};
|
|
||||||
const fromApi = typeof c.name === 'string' ? c.name : '';
|
|
||||||
if (!isBadLabel(fromApi)) {
|
|
||||||
return fromApi.trim();
|
|
||||||
}
|
}
|
||||||
if (this.companyGuid === g && !isBadLabel(this.companyName)) {
|
|
||||||
return String(this.companyName).trim();
|
|
||||||
}
|
|
||||||
|
|
||||||
return g;
|
|
||||||
},
|
|
||||||
|
|
||||||
stringLooksLikeUuid(s) {
|
|
||||||
return /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-5][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}$/.test(
|
|
||||||
String(s),
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
$config = $page->mailwizzConfig;
|
$config = $page->mailwizzConfig;
|
||||||
$page->loadMissing('weeztixConfig');
|
$page->loadMissing('weeztixConfig');
|
||||||
$hasWeeztixForCouponMap = $page->weeztixConfig !== null && $page->weeztixConfig->is_connected;
|
$hasWeeztixForCouponMap = $page->weeztixConfig !== null && $page->weeztixConfig->is_connected;
|
||||||
|
$mailwizzStatus = $page->mailwizzIntegrationStatus();
|
||||||
$existing = $config !== null
|
$existing = $config !== null
|
||||||
? [
|
? [
|
||||||
'list_uid' => $config->list_uid,
|
'list_uid' => $config->list_uid,
|
||||||
@@ -24,7 +25,116 @@
|
|||||||
@section('mobile_title', __('Mailwizz'))
|
@section('mobile_title', __('Mailwizz'))
|
||||||
|
|
||||||
@section('content')
|
@section('content')
|
||||||
<div class="mx-auto max-w-3xl" x-data="mailwizzWizard(@js([
|
<div class="mx-auto max-w-3xl">
|
||||||
|
<div class="mb-8">
|
||||||
|
<a href="{{ route('admin.pages.edit', $page) }}" class="text-sm font-medium text-indigo-600 hover:text-indigo-500">← {{ __('Back to page') }}</a>
|
||||||
|
<h1 class="mt-4 text-2xl font-semibold text-slate-900">{{ __('Mailwizz') }}</h1>
|
||||||
|
<p class="mt-2 text-sm text-slate-600">{{ __('Page:') }} <span class="font-medium text-slate-800">{{ $page->title }}</span></p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@include('admin.pages._save_flash')
|
||||||
|
|
||||||
|
@if ($errors->any())
|
||||||
|
<div class="mb-6 rounded-lg border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-900" role="alert">
|
||||||
|
<p class="font-medium">{{ __('Please fix the following:') }}</p>
|
||||||
|
<ul class="mt-2 list-disc space-y-1 pl-5">
|
||||||
|
@foreach ($errors->all() as $message)
|
||||||
|
<li>{{ $message }}</li>
|
||||||
|
@endforeach
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
|
||||||
|
@if (! $showWizard && $config !== null)
|
||||||
|
@if ($mailwizzStatus !== 'ready')
|
||||||
|
<div class="mb-6 rounded-xl border border-amber-200 bg-amber-50 px-4 py-3 text-sm text-amber-950">
|
||||||
|
<p class="font-medium">{{ __('Setup incomplete') }}</p>
|
||||||
|
<p class="mt-1 text-amber-900">{{ __('Run the wizard again to finish Mailwizz (API key, list, and field mapping).') }}</p>
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
|
||||||
|
<div class="mb-6 flex flex-wrap items-center gap-3">
|
||||||
|
<a
|
||||||
|
href="{{ route('admin.pages.mailwizz.edit', ['page' => $page, 'wizard' => 1, 'step' => 1]) }}"
|
||||||
|
class="inline-flex rounded-lg bg-indigo-600 px-4 py-2.5 text-sm font-semibold text-white shadow-sm hover:bg-indigo-500"
|
||||||
|
>
|
||||||
|
{{ __('Change settings (wizard)') }}
|
||||||
|
</a>
|
||||||
|
<form action="{{ route('admin.pages.mailwizz.destroy', $page) }}" method="post" class="inline"
|
||||||
|
onsubmit="return confirm(@js(__('Remove Mailwizz integration for this page? Subscribers will stay in the database but will no longer sync.')));">
|
||||||
|
@csrf
|
||||||
|
@method('DELETE')
|
||||||
|
<button type="submit" class="rounded-lg border border-red-200 bg-white px-4 py-2.5 text-sm font-semibold text-red-700 shadow-sm hover:bg-red-50">
|
||||||
|
{{ __('Disconnect Mailwizz') }}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="rounded-xl border border-slate-200 bg-white p-6 shadow-sm sm:p-8">
|
||||||
|
<h2 class="text-lg font-semibold text-slate-900">{{ __('Current configuration') }}</h2>
|
||||||
|
<p class="mt-1 text-sm text-slate-600">{{ __('The API key is stored encrypted and is not shown here.') }}</p>
|
||||||
|
|
||||||
|
<dl class="mt-6 space-y-4 border-t border-slate-100 pt-6 text-sm">
|
||||||
|
<div class="flex flex-col gap-1 sm:flex-row sm:justify-between">
|
||||||
|
<dt class="font-medium text-slate-700">{{ __('Connection') }}</dt>
|
||||||
|
<dd>
|
||||||
|
@if ($mailwizzStatus === 'ready')
|
||||||
|
<span class="inline-flex rounded-full bg-emerald-100 px-2.5 py-0.5 text-xs font-medium text-emerald-800">{{ __('Ready to sync') }}</span>
|
||||||
|
@else
|
||||||
|
<span class="inline-flex rounded-full bg-amber-100 px-2.5 py-0.5 text-xs font-medium text-amber-900">{{ __('Incomplete') }}</span>
|
||||||
|
@endif
|
||||||
|
</dd>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col gap-1 sm:flex-row sm:justify-between">
|
||||||
|
<dt class="font-medium text-slate-700">{{ __('Mailing list') }}</dt>
|
||||||
|
<dd class="text-slate-800">{{ $config->list_name ?: '—' }}</dd>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col gap-1 sm:flex-row sm:justify-between">
|
||||||
|
<dt class="font-medium text-slate-700">{{ __('List UID') }}</dt>
|
||||||
|
<dd class="break-all font-mono text-xs text-slate-600">{{ $config->list_uid ?: '—' }}</dd>
|
||||||
|
</div>
|
||||||
|
</dl>
|
||||||
|
|
||||||
|
<h3 class="mt-8 border-t border-slate-100 pt-6 text-sm font-semibold text-slate-900">{{ __('Field mapping') }}</h3>
|
||||||
|
<p class="mt-1 text-xs text-slate-500">{{ __('Mailwizz custom fields are matched by tag.') }}</p>
|
||||||
|
<dl class="mt-4 space-y-4 text-sm">
|
||||||
|
<div class="flex flex-col gap-1 sm:flex-row sm:justify-between">
|
||||||
|
<dt class="font-medium text-slate-700">{{ __('Email') }}</dt>
|
||||||
|
<dd class="font-mono text-xs text-slate-800">{{ $config->field_email ?: '—' }}</dd>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col gap-1 sm:flex-row sm:justify-between">
|
||||||
|
<dt class="font-medium text-slate-700">{{ __('First name') }}</dt>
|
||||||
|
<dd class="font-mono text-xs text-slate-800">{{ $config->field_first_name ?: '—' }}</dd>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col gap-1 sm:flex-row sm:justify-between">
|
||||||
|
<dt class="font-medium text-slate-700">{{ __('Last name') }}</dt>
|
||||||
|
<dd class="font-mono text-xs text-slate-800">{{ $config->field_last_name ?: '—' }}</dd>
|
||||||
|
</div>
|
||||||
|
@if ($page->isPhoneFieldEnabledForSubscribers())
|
||||||
|
<div class="flex flex-col gap-1 sm:flex-row sm:justify-between">
|
||||||
|
<dt class="font-medium text-slate-700">{{ __('Phone') }}</dt>
|
||||||
|
<dd class="font-mono text-xs text-slate-800">{{ filled($config->field_phone) ? $config->field_phone : '—' }}</dd>
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
@if ($hasWeeztixForCouponMap)
|
||||||
|
<div class="flex flex-col gap-1 sm:flex-row sm:justify-between">
|
||||||
|
<dt class="font-medium text-slate-700">{{ __('Kortingscode (Weeztix)') }}</dt>
|
||||||
|
<dd class="font-mono text-xs text-slate-800">{{ filled($config->field_coupon_code) ? $config->field_coupon_code : '—' }}</dd>
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
<div class="flex flex-col gap-1 sm:flex-row sm:justify-between">
|
||||||
|
<dt class="font-medium text-slate-700">{{ __('Tag / source (checkbox list)') }}</dt>
|
||||||
|
<dd class="font-mono text-xs text-slate-800">{{ filled($config->tag_field) ? $config->tag_field : '—' }}</dd>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col gap-1 sm:flex-row sm:justify-between">
|
||||||
|
<dt class="font-medium text-slate-700">{{ __('Source tag option') }}</dt>
|
||||||
|
<dd class="font-mono text-xs text-slate-800">{{ filled($config->tag_value) ? $config->tag_value : '—' }}</dd>
|
||||||
|
</div>
|
||||||
|
</dl>
|
||||||
|
</div>
|
||||||
|
@else
|
||||||
|
<div
|
||||||
|
x-data="mailwizzWizard(@js([
|
||||||
'listsUrl' => route('admin.mailwizz.lists'),
|
'listsUrl' => route('admin.mailwizz.lists'),
|
||||||
'fieldsUrl' => route('admin.mailwizz.fields'),
|
'fieldsUrl' => route('admin.mailwizz.fields'),
|
||||||
'csrf' => csrf_token(),
|
'csrf' => csrf_token(),
|
||||||
@@ -41,50 +151,48 @@
|
|||||||
'tagFieldError' => __('Select a checkbox list field for source / tag tracking.'),
|
'tagFieldError' => __('Select a checkbox list field for source / tag tracking.'),
|
||||||
'tagValueError' => __('Select the tag option that identifies this pre-registration.'),
|
'tagValueError' => __('Select the tag option that identifies this pre-registration.'),
|
||||||
],
|
],
|
||||||
]))">
|
]))"
|
||||||
<div class="mb-8">
|
>
|
||||||
<a href="{{ route('admin.pages.edit', $page) }}" class="text-sm font-medium text-indigo-600 hover:text-indigo-500">← {{ __('Back to page') }}</a>
|
|
||||||
<h1 class="mt-4 text-2xl font-semibold text-slate-900">{{ __('Mailwizz') }}</h1>
|
|
||||||
<p class="mt-2 text-sm text-slate-600">{{ __('Page:') }} <span class="font-medium text-slate-800">{{ $page->title }}</span></p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
@if ($errors->any())
|
|
||||||
<div class="mb-6 rounded-lg border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-900" role="alert">
|
|
||||||
<p class="font-medium">{{ __('Please fix the following:') }}</p>
|
|
||||||
<ul class="mt-2 list-disc space-y-1 pl-5">
|
|
||||||
@foreach ($errors->all() as $message)
|
|
||||||
<li>{{ $message }}</li>
|
|
||||||
@endforeach
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
@endif
|
|
||||||
|
|
||||||
@if ($config !== null)
|
@if ($config !== null)
|
||||||
<div class="mb-8 rounded-xl border border-emerald-200 bg-emerald-50 px-4 py-3 text-sm text-emerald-900">
|
<div class="mb-6 flex flex-wrap items-center gap-3">
|
||||||
<p class="font-medium">{{ __('Integration active') }}</p>
|
<a href="{{ route('admin.pages.mailwizz.edit', $page) }}" class="text-sm font-medium text-slate-600 hover:text-slate-900">
|
||||||
<p class="mt-1 text-emerald-800">
|
{{ __('Cancel and return to overview') }}
|
||||||
{{ __('List:') }}
|
</a>
|
||||||
<span class="font-mono text-xs">{{ $config->list_name ?: $config->list_uid }}</span>
|
|
||||||
</p>
|
|
||||||
<form action="{{ route('admin.pages.mailwizz.destroy', $page) }}" method="post" class="mt-3"
|
|
||||||
onsubmit="return confirm(@js(__('Remove Mailwizz integration for this page? Subscribers will stay in the database but will no longer sync.')));">
|
|
||||||
@csrf
|
|
||||||
@method('DELETE')
|
|
||||||
<button type="submit" class="text-sm font-semibold text-red-700 underline hover:text-red-800">
|
|
||||||
{{ __('Remove integration') }}
|
|
||||||
</button>
|
|
||||||
</form>
|
|
||||||
</div>
|
</div>
|
||||||
@endif
|
@endif
|
||||||
|
|
||||||
<div class="mb-6 flex flex-wrap gap-2 text-xs font-medium text-slate-500">
|
<div class="mb-8 flex flex-wrap items-center gap-2" aria-label="{{ __('Wizard steps') }}">
|
||||||
<span :class="step >= 1 ? 'text-indigo-600' : ''">1. {{ __('API key') }}</span>
|
<span
|
||||||
<span aria-hidden="true">→</span>
|
class="inline-flex items-center gap-2 rounded-full border px-3 py-1 text-xs font-medium"
|
||||||
<span :class="step >= 2 ? 'text-indigo-600' : ''">2. {{ __('List') }}</span>
|
:class="step === 1 ? 'border-indigo-500 bg-indigo-50 text-indigo-900' : (step > 1 ? 'border-emerald-200 bg-emerald-50 text-emerald-900' : 'border-slate-200 bg-slate-50 text-slate-500')"
|
||||||
<span aria-hidden="true">→</span>
|
>
|
||||||
<span :class="step >= 3 ? 'text-indigo-600' : ''">3. {{ __('Field mapping') }}</span>
|
<span class="tabular-nums">1</span>
|
||||||
<span aria-hidden="true">→</span>
|
{{ __('API key') }}
|
||||||
<span :class="step >= 4 ? 'text-indigo-600' : ''">4. {{ __('Tag / source') }}</span>
|
</span>
|
||||||
|
<span class="text-slate-300" aria-hidden="true">→</span>
|
||||||
|
<span
|
||||||
|
class="inline-flex items-center gap-2 rounded-full border px-3 py-1 text-xs font-medium"
|
||||||
|
:class="step === 2 ? 'border-indigo-500 bg-indigo-50 text-indigo-900' : (step > 2 ? 'border-emerald-200 bg-emerald-50 text-emerald-900' : 'border-slate-200 bg-slate-50 text-slate-500')"
|
||||||
|
>
|
||||||
|
<span class="tabular-nums">2</span>
|
||||||
|
{{ __('List') }}
|
||||||
|
</span>
|
||||||
|
<span class="text-slate-300" aria-hidden="true">→</span>
|
||||||
|
<span
|
||||||
|
class="inline-flex items-center gap-2 rounded-full border px-3 py-1 text-xs font-medium"
|
||||||
|
:class="step === 3 ? 'border-indigo-500 bg-indigo-50 text-indigo-900' : (step > 3 ? 'border-emerald-200 bg-emerald-50 text-emerald-900' : 'border-slate-200 bg-slate-50 text-slate-500')"
|
||||||
|
>
|
||||||
|
<span class="tabular-nums">3</span>
|
||||||
|
{{ __('Field mapping') }}
|
||||||
|
</span>
|
||||||
|
<span class="text-slate-300" aria-hidden="true">→</span>
|
||||||
|
<span
|
||||||
|
class="inline-flex items-center gap-2 rounded-full border px-3 py-1 text-xs font-medium"
|
||||||
|
:class="step === 4 ? 'border-indigo-500 bg-indigo-50 text-indigo-900' : 'border-slate-200 bg-slate-50 text-slate-500'"
|
||||||
|
>
|
||||||
|
<span class="tabular-nums">4</span>
|
||||||
|
{{ __('Tag / source') }}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="rounded-xl border border-slate-200 bg-white p-6 shadow-sm sm:p-8">
|
<div class="rounded-xl border border-slate-200 bg-white p-6 shadow-sm sm:p-8">
|
||||||
@@ -92,6 +200,7 @@
|
|||||||
|
|
||||||
{{-- Step 1 --}}
|
{{-- Step 1 --}}
|
||||||
<div x-show="step === 1" x-cloak class="space-y-4">
|
<div x-show="step === 1" x-cloak class="space-y-4">
|
||||||
|
<h2 class="text-lg font-semibold text-slate-900">{{ __('Step 1: API key') }}</h2>
|
||||||
<p class="text-sm leading-relaxed text-slate-600">
|
<p class="text-sm leading-relaxed text-slate-600">
|
||||||
{{ __('First, create a mailing list in Mailwizz with the required custom fields. Add a custom field of type Checkbox List with an option value you will use to track this pre-registration source.') }}
|
{{ __('First, create a mailing list in Mailwizz with the required custom fields. Add a custom field of type Checkbox List with an option value you will use to track this pre-registration source.') }}
|
||||||
</p>
|
</p>
|
||||||
@@ -124,6 +233,7 @@
|
|||||||
|
|
||||||
{{-- Step 2 --}}
|
{{-- Step 2 --}}
|
||||||
<div x-show="step === 2" x-cloak class="space-y-4">
|
<div x-show="step === 2" x-cloak class="space-y-4">
|
||||||
|
<h2 class="text-lg font-semibold text-slate-900">{{ __('Step 2: Mailing list') }}</h2>
|
||||||
<div>
|
<div>
|
||||||
<label for="mailwizz_list" class="block text-sm font-medium text-slate-700">{{ __('Mailing list') }}</label>
|
<label for="mailwizz_list" class="block text-sm font-medium text-slate-700">{{ __('Mailing list') }}</label>
|
||||||
<select
|
<select
|
||||||
@@ -156,6 +266,7 @@
|
|||||||
|
|
||||||
{{-- Step 3 --}}
|
{{-- Step 3 --}}
|
||||||
<div x-show="step === 3" x-cloak class="space-y-5">
|
<div x-show="step === 3" x-cloak class="space-y-5">
|
||||||
|
<h2 class="text-lg font-semibold text-slate-900">{{ __('Step 3: Field mapping') }}</h2>
|
||||||
<p class="text-sm text-slate-600">{{ __('Map each local field to the matching Mailwizz custom field (by tag).') }}</p>
|
<p class="text-sm text-slate-600">{{ __('Map each local field to the matching Mailwizz custom field (by tag).') }}</p>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
@@ -233,6 +344,7 @@
|
|||||||
|
|
||||||
{{-- Step 4 --}}
|
{{-- Step 4 --}}
|
||||||
<div x-show="step === 4" x-cloak class="space-y-5">
|
<div x-show="step === 4" x-cloak class="space-y-5">
|
||||||
|
<h2 class="text-lg font-semibold text-slate-900">{{ __('Step 4: Tag / source') }}</h2>
|
||||||
<p class="text-sm text-slate-600">{{ __('Choose the checkbox option that marks subscribers from this pre-registration page.') }}</p>
|
<p class="text-sm text-slate-600">{{ __('Choose the checkbox option that marks subscribers from this pre-registration page.') }}</p>
|
||||||
|
|
||||||
<fieldset class="space-y-2">
|
<fieldset class="space-y-2">
|
||||||
@@ -274,4 +386,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
@endsection
|
@endsection
|
||||||
|
|||||||
55
resources/views/admin/pages/_integration_badges.blade.php
Normal file
55
resources/views/admin/pages/_integration_badges.blade.php
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
@php
|
||||||
|
$only = $only ?? null;
|
||||||
|
$integrationBadgeClass = $integrationBadgeClass ?? '';
|
||||||
|
if (! in_array($only, [null, 'mailwizz', 'weeztix'], true)) {
|
||||||
|
$only = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$mailwizz = $page->mailwizzIntegrationStatus();
|
||||||
|
$weeztix = $page->weeztixIntegrationStatus();
|
||||||
|
|
||||||
|
$mailwizzClasses = match ($mailwizz) {
|
||||||
|
'ready' => 'border-emerald-200 bg-emerald-50 text-emerald-900',
|
||||||
|
'partial' => 'border-amber-200 bg-amber-50 text-amber-950',
|
||||||
|
default => 'border-slate-200 bg-slate-50 text-slate-600',
|
||||||
|
};
|
||||||
|
$mailwizzLabel = match ($mailwizz) {
|
||||||
|
'ready' => __('Ready'),
|
||||||
|
'partial' => __('Incomplete'),
|
||||||
|
default => __('Off'),
|
||||||
|
};
|
||||||
|
|
||||||
|
$weeztixClasses = match ($weeztix) {
|
||||||
|
'ready' => 'border-emerald-200 bg-emerald-50 text-emerald-900',
|
||||||
|
'connected' => 'border-sky-200 bg-sky-50 text-sky-950',
|
||||||
|
'credentials' => 'border-amber-200 bg-amber-50 text-amber-950',
|
||||||
|
default => 'border-slate-200 bg-slate-50 text-slate-600',
|
||||||
|
};
|
||||||
|
$weeztixLabel = match ($weeztix) {
|
||||||
|
'ready' => __('Ready'),
|
||||||
|
'connected' => __('Connected'),
|
||||||
|
'credentials' => __('OAuth only'),
|
||||||
|
default => __('Off'),
|
||||||
|
};
|
||||||
|
|
||||||
|
$showMailwizz = $only === null || $only === 'mailwizz';
|
||||||
|
$showWeeztix = $only === null || $only === 'weeztix';
|
||||||
|
@endphp
|
||||||
|
<div class="flex flex-wrap items-center gap-1.5 {{ $integrationBadgeClass }}">
|
||||||
|
@if ($showMailwizz)
|
||||||
|
<span
|
||||||
|
class="inline-flex items-center rounded-full border px-2 py-0.5 text-xs font-medium {{ $mailwizzClasses }}"
|
||||||
|
title="{{ __('Mailwizz: :state', ['state' => $mailwizzLabel]) }}"
|
||||||
|
>
|
||||||
|
{{ __('MW') }} · {{ $mailwizzLabel }}
|
||||||
|
</span>
|
||||||
|
@endif
|
||||||
|
@if ($showWeeztix)
|
||||||
|
<span
|
||||||
|
class="inline-flex items-center rounded-full border px-2 py-0.5 text-xs font-medium {{ $weeztixClasses }}"
|
||||||
|
title="{{ __('Weeztix: :state', ['state' => $weeztixLabel]) }}"
|
||||||
|
>
|
||||||
|
{{ __('WZ') }} · {{ $weeztixLabel }}
|
||||||
|
</span>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
@@ -13,10 +13,30 @@
|
|||||||
{{ __('Public URL') }}: <a href="{{ route('public.page', ['publicPage' => $page]) }}" class="text-indigo-600 hover:underline" target="_blank" rel="noopener">{{ url('/r/'.$page->slug) }}</a>
|
{{ __('Public URL') }}: <a href="{{ route('public.page', ['publicPage' => $page]) }}" class="text-indigo-600 hover:underline" target="_blank" rel="noopener">{{ url('/r/'.$page->slug) }}</a>
|
||||||
</p>
|
</p>
|
||||||
@can('update', $page)
|
@can('update', $page)
|
||||||
<p class="mt-3 flex flex-wrap gap-x-4 gap-y-1">
|
<div class="mt-6 grid gap-4 sm:grid-cols-2">
|
||||||
<a href="{{ route('admin.pages.mailwizz.edit', $page) }}" class="text-sm font-medium text-indigo-600 hover:text-indigo-500">{{ __('Mailwizz integration') }} →</a>
|
<a
|
||||||
<a href="{{ route('admin.pages.weeztix.edit', $page) }}" class="text-sm font-medium text-indigo-600 hover:text-indigo-500">{{ __('Weeztix integration') }} →</a>
|
href="{{ route('admin.pages.mailwizz.edit', $page) }}"
|
||||||
</p>
|
class="group flex flex-col rounded-xl border border-slate-200 bg-white p-4 shadow-sm transition hover:border-indigo-200 hover:shadow-md"
|
||||||
|
>
|
||||||
|
<div class="flex items-start justify-between gap-3">
|
||||||
|
<h2 class="text-sm font-semibold text-slate-900">{{ __('Mailwizz') }}</h2>
|
||||||
|
@include('admin.pages._integration_badges', ['page' => $page, 'only' => 'mailwizz', 'integrationBadgeClass' => 'justify-end'])
|
||||||
|
</div>
|
||||||
|
<p class="mt-2 flex-1 text-xs text-slate-600">{{ __('Sync subscribers to your Mailwizz list and map fields.') }}</p>
|
||||||
|
<span class="mt-3 text-sm font-medium text-indigo-600 group-hover:text-indigo-500">{{ __('Open Mailwizz') }} →</span>
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
href="{{ route('admin.pages.weeztix.edit', $page) }}"
|
||||||
|
class="group flex flex-col rounded-xl border border-slate-200 bg-white p-4 shadow-sm transition hover:border-indigo-200 hover:shadow-md"
|
||||||
|
>
|
||||||
|
<div class="flex items-start justify-between gap-3">
|
||||||
|
<h2 class="text-sm font-semibold text-slate-900">{{ __('Weeztix') }}</h2>
|
||||||
|
@include('admin.pages._integration_badges', ['page' => $page, 'only' => 'weeztix', 'integrationBadgeClass' => 'justify-end'])
|
||||||
|
</div>
|
||||||
|
<p class="mt-2 flex-1 text-xs text-slate-600">{{ __('Issue unique discount codes via Weeztix when visitors sign up.') }}</p>
|
||||||
|
<span class="mt-3 text-sm font-medium text-indigo-600 group-hover:text-indigo-500">{{ __('Open Weeztix') }} →</span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
@endcan
|
@endcan
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -32,6 +32,7 @@
|
|||||||
<th scope="col" class="px-4 py-3 font-semibold text-slate-700">{{ __('Start') }}</th>
|
<th scope="col" class="px-4 py-3 font-semibold text-slate-700">{{ __('Start') }}</th>
|
||||||
<th scope="col" class="px-4 py-3 font-semibold text-slate-700">{{ __('End') }}</th>
|
<th scope="col" class="px-4 py-3 font-semibold text-slate-700">{{ __('End') }}</th>
|
||||||
<th scope="col" class="px-4 py-3 font-semibold text-slate-700">{{ __('Subscribers') }}</th>
|
<th scope="col" class="px-4 py-3 font-semibold text-slate-700">{{ __('Subscribers') }}</th>
|
||||||
|
<th scope="col" class="px-4 py-3 font-semibold text-slate-700">{{ __('Integrations') }}</th>
|
||||||
<th scope="col" class="px-4 py-3 text-right font-semibold text-slate-700">{{ __('Actions') }}</th>
|
<th scope="col" class="px-4 py-3 text-right font-semibold text-slate-700">{{ __('Actions') }}</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
@@ -64,6 +65,9 @@
|
|||||||
<td class="whitespace-nowrap px-4 py-3 text-slate-600">{{ $page->start_date->timezone(config('app.timezone'))->format('Y-m-d H:i') }}</td>
|
<td class="whitespace-nowrap px-4 py-3 text-slate-600">{{ $page->start_date->timezone(config('app.timezone'))->format('Y-m-d H:i') }}</td>
|
||||||
<td class="whitespace-nowrap px-4 py-3 text-slate-600">{{ $page->end_date->timezone(config('app.timezone'))->format('Y-m-d H:i') }}</td>
|
<td class="whitespace-nowrap px-4 py-3 text-slate-600">{{ $page->end_date->timezone(config('app.timezone'))->format('Y-m-d H:i') }}</td>
|
||||||
<td class="whitespace-nowrap px-4 py-3 tabular-nums text-slate-600">{{ number_format($page->subscribers_count) }}</td>
|
<td class="whitespace-nowrap px-4 py-3 tabular-nums text-slate-600">{{ number_format($page->subscribers_count) }}</td>
|
||||||
|
<td class="px-4 py-3">
|
||||||
|
@include('admin.pages._integration_badges', ['page' => $page])
|
||||||
|
</td>
|
||||||
<td class="whitespace-nowrap px-4 py-3 text-right">
|
<td class="whitespace-nowrap px-4 py-3 text-right">
|
||||||
<div class="flex flex-wrap items-center justify-end gap-2">
|
<div class="flex flex-wrap items-center justify-end gap-2">
|
||||||
@can('update', $page)
|
@can('update', $page)
|
||||||
@@ -77,9 +81,6 @@
|
|||||||
@endcan
|
@endcan
|
||||||
@can('update', $page)
|
@can('update', $page)
|
||||||
<a href="{{ route('admin.pages.weeztix.edit', $page) }}" class="text-indigo-600 hover:text-indigo-500">{{ __('Weeztix') }}</a>
|
<a href="{{ route('admin.pages.weeztix.edit', $page) }}" class="text-indigo-600 hover:text-indigo-500">{{ __('Weeztix') }}</a>
|
||||||
@if ($page->weeztixConfig?->is_connected)
|
|
||||||
<span class="text-xs font-medium text-emerald-600" title="{{ __('Weeztix verbonden') }}">●</span>
|
|
||||||
@endif
|
|
||||||
@endcan
|
@endcan
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@@ -103,7 +104,7 @@
|
|||||||
</tr>
|
</tr>
|
||||||
@empty
|
@empty
|
||||||
<tr>
|
<tr>
|
||||||
<td colspan="{{ auth()->user()->isSuperadmin() ? 7 : 6 }}" class="px-4 py-12 text-center text-slate-500">
|
<td colspan="{{ auth()->user()->isSuperadmin() ? 8 : 7 }}" class="px-4 py-12 text-center text-slate-500">
|
||||||
{{ __('No pages yet.') }}
|
{{ __('No pages yet.') }}
|
||||||
@can('create', \App\Models\PreregistrationPage::class)
|
@can('create', \App\Models\PreregistrationPage::class)
|
||||||
<a href="{{ route('admin.pages.create') }}" class="font-medium text-indigo-600 hover:text-indigo-500">{{ __('Create one') }}</a>
|
<a href="{{ route('admin.pages.create') }}" class="font-medium text-indigo-600 hover:text-indigo-500">{{ __('Create one') }}</a>
|
||||||
|
|||||||
@@ -3,14 +3,14 @@
|
|||||||
$wz = $page->weeztixConfig;
|
$wz = $page->weeztixConfig;
|
||||||
$existing = $wz !== null
|
$existing = $wz !== null
|
||||||
? [
|
? [
|
||||||
'company_guid' => $wz->company_guid,
|
|
||||||
'company_name' => $wz->company_name,
|
|
||||||
'coupon_guid' => $wz->coupon_guid,
|
'coupon_guid' => $wz->coupon_guid,
|
||||||
'coupon_name' => $wz->coupon_name,
|
'coupon_name' => $wz->coupon_name,
|
||||||
'code_prefix' => $wz->code_prefix,
|
'code_prefix' => $wz->code_prefix,
|
||||||
'usage_count' => $wz->usage_count,
|
'usage_count' => $wz->usage_count,
|
||||||
]
|
]
|
||||||
: null;
|
: null;
|
||||||
|
$credentialsEdit = ! $hasStoredCredentials || request()->query('credentials') === 'edit';
|
||||||
|
$oauthUrl = route('admin.pages.weeztix.oauth.redirect', ['page' => $page, 'wizard' => 1]);
|
||||||
@endphp
|
@endphp
|
||||||
|
|
||||||
@extends('layouts.admin')
|
@extends('layouts.admin')
|
||||||
@@ -20,24 +20,7 @@
|
|||||||
@section('mobile_title', __('Weeztix'))
|
@section('mobile_title', __('Weeztix'))
|
||||||
|
|
||||||
@section('content')
|
@section('content')
|
||||||
<div
|
<div class="mx-auto max-w-3xl">
|
||||||
class="mx-auto max-w-3xl"
|
|
||||||
x-data="weeztixSetup(@js([
|
|
||||||
'pageId' => $page->id,
|
|
||||||
'companiesUrl' => route('admin.weeztix.companies'),
|
|
||||||
'couponsUrl' => route('admin.weeztix.coupons'),
|
|
||||||
'csrf' => csrf_token(),
|
|
||||||
'isConnected' => $wz?->is_connected ?? false,
|
|
||||||
'tokenExpiresAt' => $wz?->token_expires_at instanceof Carbon ? $wz->token_expires_at->timezone(config('app.timezone'))->format('Y-m-d H:i') : null,
|
|
||||||
'callbackUrl' => route('admin.weeztix.callback', absolute: true),
|
|
||||||
'existing' => $existing,
|
|
||||||
'strings' => [
|
|
||||||
'genericError' => __('Er ging iets mis. Probeer het opnieuw.'),
|
|
||||||
'selectCompany' => __('Selecteer een bedrijf.'),
|
|
||||||
'loadCouponsError' => __('Kon kortingsbonnen niet laden.'),
|
|
||||||
],
|
|
||||||
]))"
|
|
||||||
>
|
|
||||||
<div class="mb-8">
|
<div class="mb-8">
|
||||||
<a href="{{ route('admin.pages.edit', $page) }}" class="text-sm font-medium text-indigo-600 hover:text-indigo-500">← {{ __('Terug naar pagina') }}</a>
|
<a href="{{ route('admin.pages.edit', $page) }}" class="text-sm font-medium text-indigo-600 hover:text-indigo-500">← {{ __('Terug naar pagina') }}</a>
|
||||||
<h1 class="mt-4 text-2xl font-semibold text-slate-900">{{ __('Weeztix') }}</h1>
|
<h1 class="mt-4 text-2xl font-semibold text-slate-900">{{ __('Weeztix') }}</h1>
|
||||||
@@ -57,53 +40,131 @@
|
|||||||
</div>
|
</div>
|
||||||
@endif
|
@endif
|
||||||
|
|
||||||
@if ($wz !== null && $wz->is_connected)
|
@if (! $showWizard && $wz !== null)
|
||||||
<div class="mb-8 rounded-xl border border-emerald-200 bg-emerald-50 px-4 py-3 text-sm text-emerald-900">
|
{{-- Summary (read-only): change only via wizard --}}
|
||||||
<p class="font-medium">{{ __('Verbonden met Weeztix') }}</p>
|
@if ($wz->is_connected && ($wz->company_guid === null || $wz->company_guid === ''))
|
||||||
@if ($wz->token_expires_at)
|
<div class="mb-6 rounded-xl border border-amber-200 bg-amber-50 px-4 py-3 text-sm text-amber-950">
|
||||||
<p class="mt-1 text-emerald-800">
|
<p class="font-medium">{{ __('Bedrijf nog niet vastgelegd') }}</p>
|
||||||
{{ __('Huidig toegangstoken tot:') }}
|
<p class="mt-1 text-amber-900">{{ __('Start de wizard en verbind opnieuw met Weeztix zodat het bedrijf automatisch wordt gekoppeld.') }}</p>
|
||||||
<span class="font-mono text-xs">{{ $wz->token_expires_at->timezone(config('app.timezone'))->format('Y-m-d H:i') }}</span>
|
|
||||||
</p>
|
|
||||||
<p class="mt-2 text-emerald-800/90">
|
|
||||||
{{ __('Het toegangstoken wordt automatisch vernieuwd bij API-gebruik (o.a. kortingscodes), zolang de refresh-token geldig is. Je hoeft niet opnieuw te verbinden.') }}
|
|
||||||
</p>
|
|
||||||
@endif
|
|
||||||
</div>
|
|
||||||
@elseif ($wz !== null && ! $wz->is_connected)
|
|
||||||
<div class="mb-8 rounded-xl border border-amber-200 bg-amber-50 px-4 py-3 text-sm text-amber-950">
|
|
||||||
<p class="font-medium">{{ __('Niet verbonden') }}</p>
|
|
||||||
<p class="mt-1 text-amber-900">{{ __('Je moet opnieuw verbinden om kortingscodes aan te maken. Gebruik de knop “Verbind met Weeztix”.') }}</p>
|
|
||||||
</div>
|
</div>
|
||||||
@endif
|
@endif
|
||||||
|
|
||||||
@if ($wz !== null)
|
<div class="mb-6 flex flex-wrap items-center gap-3">
|
||||||
<form action="{{ route('admin.pages.weeztix.destroy', $page) }}" method="post" class="mb-8"
|
<a
|
||||||
|
href="{{ route('admin.pages.weeztix.edit', ['page' => $page, 'wizard' => 1, 'step' => 1]) }}"
|
||||||
|
class="inline-flex rounded-lg bg-indigo-600 px-4 py-2.5 text-sm font-semibold text-white shadow-sm hover:bg-indigo-500"
|
||||||
|
>
|
||||||
|
{{ __('Instellingen wijzigen (wizard)') }}
|
||||||
|
</a>
|
||||||
|
<form action="{{ route('admin.pages.weeztix.destroy', $page) }}" method="post" class="inline"
|
||||||
onsubmit="return confirm(@js(__('Weeztix-integratie voor deze pagina verwijderen? Opgeslagen tokens worden gewist.')));">
|
onsubmit="return confirm(@js(__('Weeztix-integratie voor deze pagina verwijderen? Opgeslagen tokens worden gewist.')));">
|
||||||
@csrf
|
@csrf
|
||||||
@method('DELETE')
|
@method('DELETE')
|
||||||
<button type="submit" class="text-sm font-semibold text-red-700 underline hover:text-red-800">
|
<button type="submit" class="rounded-lg border border-red-200 bg-white px-4 py-2.5 text-sm font-semibold text-red-700 shadow-sm hover:bg-red-50">
|
||||||
{{ __('Weeztix loskoppelen') }}
|
{{ __('Weeztix loskoppelen') }}
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
@endif
|
</div>
|
||||||
|
|
||||||
<div class="rounded-xl border border-slate-200 bg-white p-6 shadow-sm sm:p-8">
|
<div class="rounded-xl border border-slate-200 bg-white p-6 shadow-sm sm:p-8">
|
||||||
<div x-show="errorMessage !== ''" x-cloak class="mb-6 rounded-lg border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-800" x-text="errorMessage"></div>
|
<h2 class="text-lg font-semibold text-slate-900">{{ __('Huidige configuratie') }}</h2>
|
||||||
|
<p class="mt-1 text-sm text-slate-600">{{ __('OAuth-gegevens zijn opgeslagen maar worden om veiligheidsredenen niet getoond.') }}</p>
|
||||||
|
|
||||||
<section class="space-y-4 border-b border-slate-100 pb-8">
|
<dl class="mt-6 space-y-4 border-t border-slate-100 pt-6 text-sm">
|
||||||
<h2 class="text-lg font-semibold text-slate-900">{{ __('Stap 1: OAuth-gegevens') }}</h2>
|
<div class="flex flex-col gap-1 sm:flex-row sm:justify-between">
|
||||||
<p class="text-sm leading-relaxed text-slate-600">
|
<dt class="font-medium text-slate-700">{{ __('Verbinding') }}</dt>
|
||||||
{{ __('Maak eerst een OAuth-client in het Weeztix-dashboard en stel de redirect-URI exact in op:') }}
|
<dd>
|
||||||
</p>
|
@if ($wz->is_connected)
|
||||||
<p class="rounded-lg bg-slate-50 px-3 py-2 font-mono text-xs text-slate-800 break-all" x-text="callbackUrl"></p>
|
<span class="inline-flex rounded-full bg-emerald-100 px-2.5 py-0.5 text-xs font-medium text-emerald-800">{{ __('Verbonden') }}</span>
|
||||||
<p class="text-sm text-slate-600">
|
@else
|
||||||
{{ __('Maak daarna een korting (coupon) in Weeztix; die kies je hierna in stap 2.') }}
|
<span class="inline-flex rounded-full bg-amber-100 px-2.5 py-0.5 text-xs font-medium text-amber-900">{{ __('Niet verbonden') }}</span>
|
||||||
</p>
|
@endif
|
||||||
|
</dd>
|
||||||
|
</div>
|
||||||
|
@if ($wz->is_connected && $wz->token_expires_at)
|
||||||
|
<div class="flex flex-col gap-1 sm:flex-row sm:justify-between">
|
||||||
|
<dt class="font-medium text-slate-700">{{ __('Toegangstoken tot') }}</dt>
|
||||||
|
<dd class="font-mono text-xs text-slate-800">{{ $wz->token_expires_at->timezone(config('app.timezone'))->format('Y-m-d H:i') }}</dd>
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
<div class="flex flex-col gap-1 sm:flex-row sm:justify-between">
|
||||||
|
<dt class="font-medium text-slate-700">{{ __('Callback-URL (in Weeztix-dashboard)') }}</dt>
|
||||||
|
<dd class="break-all font-mono text-xs text-slate-600">{{ route('admin.weeztix.callback', absolute: true) }}</dd>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col gap-1 sm:flex-row sm:justify-between">
|
||||||
|
<dt class="font-medium text-slate-700">{{ __('Coupon') }}</dt>
|
||||||
|
<dd class="text-slate-800">{{ $wz->coupon_name ?: ($wz->coupon_guid ? $wz->coupon_guid : '—') }}</dd>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col gap-1 sm:flex-row sm:justify-between">
|
||||||
|
<dt class="font-medium text-slate-700">{{ __('Codevoorvoegsel') }}</dt>
|
||||||
|
<dd class="font-mono text-slate-800">{{ $wz->code_prefix ?? '—' }}</dd>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col gap-1 sm:flex-row sm:justify-between">
|
||||||
|
<dt class="font-medium text-slate-700">{{ __('Gebruik per code') }}</dt>
|
||||||
|
<dd class="text-slate-800">{{ (int) ($wz->usage_count ?? 1) }}</dd>
|
||||||
|
</div>
|
||||||
|
</dl>
|
||||||
|
</div>
|
||||||
|
@else
|
||||||
|
{{-- Wizard --}}
|
||||||
|
<div class="mb-6 flex flex-wrap items-center justify-between gap-3">
|
||||||
|
@if ($wz !== null)
|
||||||
|
<a href="{{ route('admin.pages.weeztix.edit', ['page' => $page]) }}" class="text-sm font-medium text-slate-600 hover:text-slate-900">
|
||||||
|
{{ __('Annuleren en terug naar overzicht') }}
|
||||||
|
</a>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
|
||||||
<form method="post" action="{{ route('admin.pages.weeztix.update', $page) }}" class="space-y-4">
|
<div class="mb-8 flex flex-wrap items-center gap-2" aria-label="{{ __('Wizardstappen') }}">
|
||||||
|
@foreach ([1 => __('OAuth'), 2 => __('Verbinden'), 3 => __('Coupon')] as $num => $label)
|
||||||
|
@php
|
||||||
|
$active = $wizardStep === $num;
|
||||||
|
$done = $wizardStep > $num;
|
||||||
|
@endphp
|
||||||
|
<span class="inline-flex items-center gap-2 rounded-full border px-3 py-1 text-xs font-medium
|
||||||
|
{{ $active ? 'border-indigo-500 bg-indigo-50 text-indigo-900' : '' }}
|
||||||
|
{{ $done ? 'border-emerald-200 bg-emerald-50 text-emerald-900' : '' }}
|
||||||
|
{{ ! $active && ! $done ? 'border-slate-200 bg-slate-50 text-slate-500' : '' }}">
|
||||||
|
<span class="tabular-nums">{{ $num }}</span>
|
||||||
|
{{ $label }}
|
||||||
|
</span>
|
||||||
|
@if ($num < 3)
|
||||||
|
<span class="text-slate-300" aria-hidden="true">→</span>
|
||||||
|
@endif
|
||||||
|
@endforeach
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="rounded-xl border border-slate-200 bg-white p-6 shadow-sm sm:p-8 space-y-10">
|
||||||
|
@if ($wizardStep === 1)
|
||||||
|
<section class="space-y-4" aria-labelledby="wz-wizard-step1">
|
||||||
|
<h2 id="wz-wizard-step1" class="text-lg font-semibold text-slate-900">{{ __('Stap 1: OAuth-client') }}</h2>
|
||||||
|
|
||||||
|
@if ($hasStoredCredentials && ! $credentialsEdit)
|
||||||
|
<p class="text-sm text-slate-600">{{ __('Wil je Client ID, client secret of de callback-URI in Weeztix wijzigen? De callback-URL van deze applicatie is hieronder; die moet exact overeenkomen in het Weeztix-dashboard.') }}</p>
|
||||||
|
<p class="rounded-lg bg-slate-50 px-3 py-2 font-mono text-xs text-slate-800 break-all">{{ route('admin.weeztix.callback', absolute: true) }}</p>
|
||||||
|
<div class="flex flex-wrap gap-3 pt-2">
|
||||||
|
<a
|
||||||
|
href="{{ route('admin.pages.weeztix.edit', ['page' => $page, 'wizard' => 1, 'step' => 2]) }}"
|
||||||
|
class="inline-flex rounded-lg bg-indigo-600 px-4 py-2.5 text-sm font-semibold text-white shadow-sm hover:bg-indigo-500"
|
||||||
|
>
|
||||||
|
{{ __('Nee, huidige gegevens behouden') }}
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
href="{{ route('admin.pages.weeztix.edit', ['page' => $page, 'wizard' => 1, 'step' => 1, 'credentials' => 'edit']) }}"
|
||||||
|
class="inline-flex rounded-lg border border-slate-300 bg-white px-4 py-2.5 text-sm font-semibold text-slate-800 shadow-sm hover:bg-slate-50"
|
||||||
|
>
|
||||||
|
{{ __('Ja, Client ID en secret wijzigen') }}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
@else
|
||||||
|
<p class="text-sm text-slate-600">{{ __('Vul de OAuth-client uit het Weeztix-dashboard in. Zet de redirect-URI exact op de onderstaande URL.') }}</p>
|
||||||
|
<p class="rounded-lg bg-slate-50 px-3 py-2 font-mono text-xs text-slate-800 break-all">{{ route('admin.weeztix.callback', absolute: true) }}</p>
|
||||||
|
|
||||||
|
<form method="post" action="{{ route('admin.pages.weeztix.update', $page) }}" class="space-y-4 pt-2">
|
||||||
@csrf
|
@csrf
|
||||||
@method('PUT')
|
@method('PUT')
|
||||||
|
<input type="hidden" name="wizard" value="1">
|
||||||
|
<input type="hidden" name="wizard_credential_save" value="1">
|
||||||
<div>
|
<div>
|
||||||
<label for="weeztix_client_id" class="block text-sm font-medium text-slate-700">{{ __('Client ID') }}</label>
|
<label for="weeztix_client_id" class="block text-sm font-medium text-slate-700">{{ __('Client ID') }}</label>
|
||||||
<input
|
<input
|
||||||
@@ -132,54 +193,103 @@
|
|||||||
</div>
|
</div>
|
||||||
@if ($wz !== null)
|
@if ($wz !== null)
|
||||||
<p class="text-xs text-slate-500">
|
<p class="text-xs text-slate-500">
|
||||||
{{ __('Client ID en secret zijn opgeslagen maar worden niet opnieuw getoond. Laat de velden leeg om ze te behouden; vul ze alleen in als je ze wilt wijzigen.') }}
|
{{ __('Laat velden leeg om opgeslagen waarden te behouden.') }}
|
||||||
</p>
|
</p>
|
||||||
@endif
|
@endif
|
||||||
<button type="submit" class="rounded-lg bg-slate-800 px-4 py-2.5 text-sm font-semibold text-white shadow-sm hover:bg-slate-700">
|
<button type="submit" class="rounded-lg bg-slate-800 px-4 py-2.5 text-sm font-semibold text-white shadow-sm hover:bg-slate-700">
|
||||||
{{ __('Gegevens opslaan') }}
|
{{ __('Opslaan en verder naar verbinden') }}
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
@if ($wz !== null)
|
|
||||||
<div class="pt-2">
|
|
||||||
<a
|
|
||||||
href="{{ route('admin.pages.weeztix.oauth.redirect', $page) }}"
|
|
||||||
class="inline-flex rounded-lg bg-indigo-600 px-4 py-2.5 text-sm font-semibold text-white shadow-sm hover:bg-indigo-500"
|
|
||||||
>
|
|
||||||
{{ __('Verbind met Weeztix') }}
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
@endif
|
@endif
|
||||||
</section>
|
</section>
|
||||||
|
@endif
|
||||||
|
|
||||||
@if ($wz !== null)
|
@if ($wizardStep === 2)
|
||||||
<section class="space-y-4 pt-8">
|
<section class="space-y-4" aria-labelledby="wz-wizard-step2">
|
||||||
<h2 class="text-lg font-semibold text-slate-900">{{ __('Stap 2: Bedrijf en kortingsbon') }}</h2>
|
<h2 id="wz-wizard-step2" class="text-lg font-semibold text-slate-900">{{ __('Stap 2: Verbinden met Weeztix') }}</h2>
|
||||||
<p class="text-sm text-slate-600">{{ __('Na een geslaagde verbinding kun je een bedrijf en bestaande coupon uit Weeztix kiezen.') }}</p>
|
<p class="text-sm text-slate-600">{{ __('Log in bij Weeztix en keur de toegang goed. Daarna ga je automatisch verder naar de coupon.') }}</p>
|
||||||
|
<p class="rounded-lg bg-slate-50 px-3 py-2 font-mono text-xs text-slate-800 break-all">{{ route('admin.weeztix.callback', absolute: true) }}</p>
|
||||||
|
|
||||||
|
@if ($wz !== null && $wz->is_connected)
|
||||||
|
<div class="rounded-lg border border-emerald-200 bg-emerald-50 px-4 py-3 text-sm text-emerald-900">
|
||||||
|
{{ __('Je bent verbonden. Start opnieuw de Weeztix-login hieronder om een ander bedrijf te kiezen; het gekoppelde bedrijf wordt daarna automatisch bijgewerkt.') }}
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
|
||||||
|
<div class="flex flex-wrap items-center gap-3">
|
||||||
|
<a
|
||||||
|
href="{{ $oauthUrl }}"
|
||||||
|
class="inline-flex rounded-lg bg-indigo-600 px-4 py-2.5 text-sm font-semibold text-white shadow-sm hover:bg-indigo-500"
|
||||||
|
>
|
||||||
|
@if ($wz !== null && $wz->is_connected)
|
||||||
|
{{ __('Opnieuw verbinden met Weeztix') }}
|
||||||
|
@else
|
||||||
|
{{ __('Verbind met Weeztix') }}
|
||||||
|
@endif
|
||||||
|
</a>
|
||||||
|
@if ($wz !== null && $wz->is_connected)
|
||||||
|
<a
|
||||||
|
href="{{ route('admin.pages.weeztix.edit', ['page' => $page, 'wizard' => 1, 'step' => 3]) }}"
|
||||||
|
class="inline-flex rounded-lg border border-slate-300 bg-white px-4 py-2.5 text-sm font-semibold text-slate-800 shadow-sm hover:bg-slate-50"
|
||||||
|
>
|
||||||
|
{{ __('Naar stap 3: coupon') }}
|
||||||
|
</a>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p class="pt-4">
|
||||||
|
<a href="{{ route('admin.pages.weeztix.edit', ['page' => $page, 'wizard' => 1, 'step' => 1]) }}" class="text-sm font-medium text-slate-600 hover:text-slate-900">{{ __('← Terug naar stap 1') }}</a>
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
@endif
|
||||||
|
|
||||||
|
@if ($wizardStep === 3)
|
||||||
|
<div
|
||||||
|
x-data="weeztixSetup(@js([
|
||||||
|
'pageId' => $page->id,
|
||||||
|
'couponsUrl' => route('admin.weeztix.coupons'),
|
||||||
|
'csrf' => csrf_token(),
|
||||||
|
'isConnected' => $wz?->is_connected ?? false,
|
||||||
|
'callbackUrl' => route('admin.weeztix.callback', absolute: true),
|
||||||
|
'existing' => $existing,
|
||||||
|
'strings' => [
|
||||||
|
'genericError' => __('Er ging iets mis. Probeer het opnieuw.'),
|
||||||
|
'loadCouponsError' => __('Kon kortingsbonnen niet laden.'),
|
||||||
|
'refreshCoupons' => __('Vernieuwen'),
|
||||||
|
'refreshCouponsBusy' => __('Bezig…'),
|
||||||
|
'refreshCouponsTitle' => __('Couponlijst opnieuw ophalen van Weeztix'),
|
||||||
|
],
|
||||||
|
]))"
|
||||||
|
>
|
||||||
|
<div x-show="errorMessage !== ''" x-cloak class="mb-6 rounded-lg border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-800" x-text="errorMessage"></div>
|
||||||
|
|
||||||
|
<section class="space-y-4" aria-labelledby="wz-wizard-step3">
|
||||||
|
<h2 id="wz-wizard-step3" class="text-lg font-semibold text-slate-900">{{ __('Stap 3: Coupon en codes') }}</h2>
|
||||||
|
<p class="text-sm text-slate-600">{{ __('Kies een bestaande coupon in Weeztix en stel het voorvoegsel en aantal gebruiken per code in.') }}</p>
|
||||||
|
|
||||||
<form method="post" action="{{ route('admin.pages.weeztix.update', $page) }}" class="space-y-4">
|
<form method="post" action="{{ route('admin.pages.weeztix.update', $page) }}" class="space-y-4">
|
||||||
@csrf
|
@csrf
|
||||||
@method('PUT')
|
@method('PUT')
|
||||||
|
<input type="hidden" name="wizard" value="1">
|
||||||
|
<input type="hidden" name="wizard_coupon_save" value="1">
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label for="weeztix_company" class="block text-sm font-medium text-slate-700">{{ __('Bedrijf') }}</label>
|
<div class="flex flex-wrap items-end justify-between gap-2">
|
||||||
<select
|
|
||||||
id="weeztix_company"
|
|
||||||
name="company_guid"
|
|
||||||
x-model="companyGuid"
|
|
||||||
@change="onCompanyChange()"
|
|
||||||
class="mt-1 block w-full rounded-lg border border-slate-300 px-3 py-2 text-slate-900 shadow-sm focus:border-indigo-500 focus:outline-none focus:ring-1 focus:ring-indigo-500"
|
|
||||||
>
|
|
||||||
<option value="">{{ __('Selecteer een bedrijf…') }}</option>
|
|
||||||
<template x-for="c in companies" :key="c.guid">
|
|
||||||
<option :value="c.guid" x-text="companyLabel(c)"></option>
|
|
||||||
</template>
|
|
||||||
</select>
|
|
||||||
<input type="hidden" name="company_name" :value="companyName">
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label for="weeztix_coupon" class="block text-sm font-medium text-slate-700">{{ __('Coupon (kortingssjabloon)') }}</label>
|
<label for="weeztix_coupon" class="block text-sm font-medium text-slate-700">{{ __('Coupon (kortingssjabloon)') }}</label>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
x-show="isConnected"
|
||||||
|
x-cloak
|
||||||
|
@click="refreshCoupons()"
|
||||||
|
:disabled="couponsRefreshing"
|
||||||
|
:aria-busy="couponsRefreshing"
|
||||||
|
:title="strings.refreshCouponsTitle"
|
||||||
|
class="shrink-0 rounded-md border border-slate-300 bg-white px-2.5 py-1 text-xs font-medium text-slate-700 shadow-sm hover:bg-slate-50 disabled:cursor-not-allowed disabled:opacity-50"
|
||||||
|
>
|
||||||
|
<span x-show="!couponsRefreshing" x-text="strings.refreshCoupons"></span>
|
||||||
|
<span x-show="couponsRefreshing" x-cloak x-text="strings.refreshCouponsBusy"></span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
<select
|
<select
|
||||||
id="weeztix_coupon"
|
id="weeztix_coupon"
|
||||||
name="coupon_guid"
|
name="coupon_guid"
|
||||||
@@ -223,12 +333,19 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-wrap gap-3 pt-2">
|
||||||
<button type="submit" class="rounded-lg bg-indigo-600 px-4 py-2.5 text-sm font-semibold text-white shadow-sm hover:bg-indigo-500">
|
<button type="submit" class="rounded-lg bg-indigo-600 px-4 py-2.5 text-sm font-semibold text-white shadow-sm hover:bg-indigo-500">
|
||||||
{{ __('Configuratie opslaan') }}
|
{{ __('Opslaan en wizard afronden') }}
|
||||||
</button>
|
</button>
|
||||||
|
<a href="{{ route('admin.pages.weeztix.edit', ['page' => $page, 'wizard' => 1, 'step' => 2]) }}" class="inline-flex items-center rounded-lg border border-slate-300 bg-white px-4 py-2.5 text-sm font-semibold text-slate-700 shadow-sm hover:bg-slate-50">
|
||||||
|
{{ __('Terug naar stap 2') }}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</section>
|
</section>
|
||||||
|
</div>
|
||||||
@endif
|
@endif
|
||||||
</div>
|
</div>
|
||||||
|
@endif
|
||||||
</div>
|
</div>
|
||||||
@endsection
|
@endsection
|
||||||
|
|||||||
@@ -156,30 +156,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<p class="whitespace-pre-line text-center text-base leading-relaxed text-white/95 sm:text-lg" x-text="thankYouMessage"></p>
|
<p class="whitespace-pre-line text-center text-base leading-relaxed text-white/95 sm:text-lg" x-text="thankYouMessage"></p>
|
||||||
<template x-if="couponCode">
|
|
||||||
<div class="mt-6 rounded-xl border border-white/20 bg-white/10 p-6 backdrop-blur">
|
|
||||||
<p class="text-center text-sm text-white/70">{{ __('Jouw kortingscode') }}</p>
|
|
||||||
<div class="mt-3 flex flex-wrap items-center justify-center gap-3">
|
|
||||||
<span
|
|
||||||
class="font-mono text-2xl font-bold tracking-wider text-festival"
|
|
||||||
x-text="couponCode"
|
|
||||||
></span>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="rounded-lg border border-white/25 p-2 text-white/70 transition hover:border-white/40 hover:text-white"
|
|
||||||
@click="copyCouponCode()"
|
|
||||||
aria-label="{{ __('Kortingscode kopiëren') }}"
|
|
||||||
>
|
|
||||||
<svg class="h-6 w-6" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" aria-hidden="true">
|
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" d="M15.666 3.888A2.25 2.25 0 0013.5 2.25h-3c-1.03 0-1.9.693-2.166 1.638m7.332 0c.055.194.084.4.084.612v0a.75.75 0 01-.75.75H9a.75.75 0 01-.75-.75v0c0-.212.03-.418.084-.612m7.332 0c.646.049 1.288.11 1.927.184 1.1.128 1.907 1.077 1.907 2.185V19.5a2.25 2.25 0 01-2.25 2.25H6.75A2.25 2.25 0 014.5 19.5V6.257c0-1.108.806-2.057 1.907-2.185a48.208 48.208 0 011.927-.184" />
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<p class="mt-3 text-center text-xs text-white/50">
|
|
||||||
{{ __('Gebruik deze code bij het afrekenen in de ticketshop.') }}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<p
|
<p
|
||||||
x-show="redirectSecondsLeft !== null && redirectSecondsLeft > 0"
|
x-show="redirectSecondsLeft !== null && redirectSecondsLeft > 0"
|
||||||
x-cloak
|
x-cloak
|
||||||
|
|||||||
@@ -71,7 +71,6 @@
|
|||||||
'redirectUrl' => filled($redirectAfterSubmit) ? $redirectAfterSubmit : null,
|
'redirectUrl' => filled($redirectAfterSubmit) ? $redirectAfterSubmit : null,
|
||||||
'strings' => [
|
'strings' => [
|
||||||
'linkCopied' => __('Link gekopieerd!'),
|
'linkCopied' => __('Link gekopieerd!'),
|
||||||
'couponCopied' => __('Kortingscode gekopieerd!'),
|
|
||||||
'redirectCountdown' => __('You will be redirected in :seconds s…'),
|
'redirectCountdown' => __('You will be redirected in :seconds s…'),
|
||||||
],
|
],
|
||||||
]))"
|
]))"
|
||||||
|
|||||||
@@ -56,7 +56,6 @@ Route::middleware(['auth', 'verified'])->prefix('admin')->name('admin.')->group(
|
|||||||
Route::get('pages/{page}/weeztix/oauth/redirect', [WeeztixOAuthController::class, 'redirect'])->name('pages.weeztix.oauth.redirect');
|
Route::get('pages/{page}/weeztix/oauth/redirect', [WeeztixOAuthController::class, 'redirect'])->name('pages.weeztix.oauth.redirect');
|
||||||
Route::get('weeztix/callback', [WeeztixOAuthController::class, 'callback'])->name('weeztix.callback');
|
Route::get('weeztix/callback', [WeeztixOAuthController::class, 'callback'])->name('weeztix.callback');
|
||||||
|
|
||||||
Route::post('weeztix/companies', [WeeztixApiController::class, 'companies'])->name('weeztix.companies');
|
|
||||||
Route::post('weeztix/coupons', [WeeztixApiController::class, 'coupons'])->name('weeztix.coupons');
|
Route::post('weeztix/coupons', [WeeztixApiController::class, 'coupons'])->name('weeztix.coupons');
|
||||||
|
|
||||||
// User management (superadmin only)
|
// User management (superadmin only)
|
||||||
|
|||||||
@@ -4,10 +4,14 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace Tests\Feature;
|
namespace Tests\Feature;
|
||||||
|
|
||||||
|
use App\Models\MailwizzConfig;
|
||||||
use App\Models\PreregistrationPage;
|
use App\Models\PreregistrationPage;
|
||||||
use App\Models\Subscriber;
|
use App\Models\Subscriber;
|
||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
|
use App\Models\WeeztixConfig;
|
||||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||||
|
use Illuminate\Http\Client\Request;
|
||||||
|
use Illuminate\Support\Facades\Http;
|
||||||
use Illuminate\Support\Str;
|
use Illuminate\Support\Str;
|
||||||
use Tests\TestCase;
|
use Tests\TestCase;
|
||||||
|
|
||||||
@@ -128,4 +132,202 @@ class DestroySubscriberTest extends TestCase
|
|||||||
$response->assertForbidden();
|
$response->assertForbidden();
|
||||||
$this->assertDatabaseHas('subscribers', ['id' => $subscriber->id]);
|
$this->assertDatabaseHas('subscribers', ['id' => $subscriber->id]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function test_delete_strips_mailwizz_source_tag_and_clears_coupon_field(): void
|
||||||
|
{
|
||||||
|
Http::fake(function (Request $request) {
|
||||||
|
$url = $request->url();
|
||||||
|
if (str_contains($url, 'search-by-email')) {
|
||||||
|
return Http::response(['status' => 'success', 'data' => ['subscriber_uid' => 'sub-to-clean']]);
|
||||||
|
}
|
||||||
|
if ($request->method() === 'GET' && str_contains($url, '/subscribers/sub-to-clean') && ! str_contains($url, 'search-by-email')) {
|
||||||
|
return Http::response([
|
||||||
|
'status' => 'success',
|
||||||
|
'data' => [
|
||||||
|
'record' => [
|
||||||
|
'TAGS' => 'preregister-source,other-tag',
|
||||||
|
'COUPON' => 'PREREG-OLD',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
if ($request->method() === 'PUT' && str_contains($url, '/subscribers/sub-to-clean')) {
|
||||||
|
$body = $request->body();
|
||||||
|
$this->assertStringContainsString('other-tag', $body);
|
||||||
|
$this->assertStringNotContainsString('preregister-source', $body);
|
||||||
|
$this->assertStringContainsString('COUPON', $body);
|
||||||
|
|
||||||
|
return Http::response(['status' => 'success']);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Http::response(['status' => 'error'], 500);
|
||||||
|
});
|
||||||
|
|
||||||
|
$user = User::factory()->create(['role' => 'user']);
|
||||||
|
$page = $this->makePageForDestroyTest($user);
|
||||||
|
MailwizzConfig::query()->create([
|
||||||
|
'preregistration_page_id' => $page->id,
|
||||||
|
'api_key' => 'fake-api-key',
|
||||||
|
'list_uid' => 'list-uid-1',
|
||||||
|
'list_name' => 'Main list',
|
||||||
|
'field_email' => 'EMAIL',
|
||||||
|
'field_first_name' => 'FNAME',
|
||||||
|
'field_last_name' => 'LNAME',
|
||||||
|
'field_phone' => null,
|
||||||
|
'field_coupon_code' => 'COUPON',
|
||||||
|
'tag_field' => 'TAGS',
|
||||||
|
'tag_value' => 'preregister-source',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$subscriber = Subscriber::query()->create([
|
||||||
|
'preregistration_page_id' => $page->id,
|
||||||
|
'first_name' => 'Clean',
|
||||||
|
'last_name' => 'Up',
|
||||||
|
'email' => 'cleanup@example.com',
|
||||||
|
'coupon_code' => 'PREREG-LOCAL',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$response = $this->actingAs($user)->delete(route('admin.pages.subscribers.destroy', [$page, $subscriber]));
|
||||||
|
|
||||||
|
$response->assertRedirect(route('admin.pages.subscribers.index', $page));
|
||||||
|
$this->assertDatabaseMissing('subscribers', ['id' => $subscriber->id]);
|
||||||
|
Http::assertSentCount(3);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_delete_clears_mailwizz_checkboxlist_when_only_configured_tag_is_present(): void
|
||||||
|
{
|
||||||
|
Http::fake(function (Request $request) {
|
||||||
|
$url = $request->url();
|
||||||
|
if (str_contains($url, 'search-by-email')) {
|
||||||
|
return Http::response(['status' => 'success', 'data' => ['subscriber_uid' => 'sub-single-tag']]);
|
||||||
|
}
|
||||||
|
if ($request->method() === 'GET' && str_contains($url, '/subscribers/sub-single-tag') && ! str_contains($url, 'search-by-email')) {
|
||||||
|
return Http::response([
|
||||||
|
'status' => 'success',
|
||||||
|
'data' => [
|
||||||
|
'record' => [
|
||||||
|
'TAGS' => 'preregister-source',
|
||||||
|
'COUPON' => 'PREREG-OLD',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
if ($request->method() === 'PUT' && str_contains($url, '/subscribers/sub-single-tag')) {
|
||||||
|
$body = $request->body();
|
||||||
|
$this->assertStringContainsString('TAGS', $body);
|
||||||
|
$this->assertStringNotContainsString('preregister-source', $body);
|
||||||
|
$this->assertStringNotContainsString('TAGS[]', $body);
|
||||||
|
$this->assertStringContainsString('COUPON', $body);
|
||||||
|
|
||||||
|
return Http::response(['status' => 'success']);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Http::response(['status' => 'error'], 500);
|
||||||
|
});
|
||||||
|
|
||||||
|
$user = User::factory()->create(['role' => 'user']);
|
||||||
|
$page = $this->makePageForDestroyTest($user);
|
||||||
|
MailwizzConfig::query()->create([
|
||||||
|
'preregistration_page_id' => $page->id,
|
||||||
|
'api_key' => 'fake-api-key',
|
||||||
|
'list_uid' => 'list-uid-1',
|
||||||
|
'list_name' => 'Main list',
|
||||||
|
'field_email' => 'EMAIL',
|
||||||
|
'field_first_name' => 'FNAME',
|
||||||
|
'field_last_name' => 'LNAME',
|
||||||
|
'field_phone' => null,
|
||||||
|
'field_coupon_code' => 'COUPON',
|
||||||
|
'tag_field' => 'TAGS',
|
||||||
|
'tag_value' => 'preregister-source',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$subscriber = Subscriber::query()->create([
|
||||||
|
'preregistration_page_id' => $page->id,
|
||||||
|
'first_name' => 'Solo',
|
||||||
|
'last_name' => 'Tag',
|
||||||
|
'email' => 'solo-tag@example.com',
|
||||||
|
'coupon_code' => 'PREREG-LOCAL',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$response = $this->actingAs($user)->delete(route('admin.pages.subscribers.destroy', [$page, $subscriber]));
|
||||||
|
|
||||||
|
$response->assertRedirect(route('admin.pages.subscribers.index', $page));
|
||||||
|
$this->assertDatabaseMissing('subscribers', ['id' => $subscriber->id]);
|
||||||
|
Http::assertSentCount(3);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_delete_removes_coupon_code_in_weeztix_when_configured(): void
|
||||||
|
{
|
||||||
|
Http::fake(function (Request $request) {
|
||||||
|
$url = $request->url();
|
||||||
|
if ($request->method() === 'GET' && preg_match('#/coupon/coupon-guid-test/codes$#', $url) === 1) {
|
||||||
|
return Http::response([
|
||||||
|
'data' => [
|
||||||
|
['guid' => 'wzx-code-guid', 'code' => 'PREREG-DEL99'],
|
||||||
|
],
|
||||||
|
], 200);
|
||||||
|
}
|
||||||
|
if ($request->method() === 'DELETE' && str_contains($url, '/coupon/coupon-guid-test/codes/wzx-code-guid')) {
|
||||||
|
return Http::response(null, 204);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Http::response(['status' => 'error'], 500);
|
||||||
|
});
|
||||||
|
|
||||||
|
$user = User::factory()->create(['role' => 'user']);
|
||||||
|
$page = $this->makePageForDestroyTest($user);
|
||||||
|
WeeztixConfig::query()->create([
|
||||||
|
'preregistration_page_id' => $page->id,
|
||||||
|
'client_id' => 'client-id',
|
||||||
|
'client_secret' => 'client-secret',
|
||||||
|
'redirect_uri' => 'https://app.test/callback',
|
||||||
|
'access_token' => 'access-token',
|
||||||
|
'refresh_token' => 'refresh-token',
|
||||||
|
'token_expires_at' => now()->addHour(),
|
||||||
|
'refresh_token_expires_at' => now()->addMonth(),
|
||||||
|
'company_guid' => 'company-guid-test',
|
||||||
|
'company_name' => 'Test Co',
|
||||||
|
'coupon_guid' => 'coupon-guid-test',
|
||||||
|
'coupon_name' => 'PreReg',
|
||||||
|
'is_connected' => true,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$subscriber = Subscriber::query()->create([
|
||||||
|
'preregistration_page_id' => $page->id,
|
||||||
|
'first_name' => 'Weez',
|
||||||
|
'last_name' => 'Tix',
|
||||||
|
'email' => 'weez@example.com',
|
||||||
|
'coupon_code' => 'PREREG-DEL99',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$response = $this->actingAs($user)->delete(route('admin.pages.subscribers.destroy', [$page, $subscriber]));
|
||||||
|
|
||||||
|
$response->assertRedirect(route('admin.pages.subscribers.index', $page));
|
||||||
|
$this->assertDatabaseMissing('subscribers', ['id' => $subscriber->id]);
|
||||||
|
|
||||||
|
Http::assertSent(function (Request $request): bool {
|
||||||
|
return $request->method() === 'DELETE'
|
||||||
|
&& str_contains($request->url(), '/coupon/coupon-guid-test/codes/wzx-code-guid');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private function makePageForDestroyTest(User $user): PreregistrationPage
|
||||||
|
{
|
||||||
|
return PreregistrationPage::query()->create([
|
||||||
|
'slug' => (string) Str::uuid(),
|
||||||
|
'user_id' => $user->id,
|
||||||
|
'title' => 'Fest',
|
||||||
|
'heading' => 'Fest',
|
||||||
|
'intro_text' => null,
|
||||||
|
'thank_you_message' => null,
|
||||||
|
'expired_message' => null,
|
||||||
|
'ticketshop_url' => null,
|
||||||
|
'start_date' => now()->subDay(),
|
||||||
|
'end_date' => now()->addMonth(),
|
||||||
|
'phone_enabled' => false,
|
||||||
|
'background_image' => null,
|
||||||
|
'logo_image' => null,
|
||||||
|
'is_active' => true,
|
||||||
|
]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace Tests\Feature;
|
namespace Tests\Feature;
|
||||||
|
|
||||||
|
use App\Models\MailwizzConfig;
|
||||||
use App\Models\PreregistrationPage;
|
use App\Models\PreregistrationPage;
|
||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||||
@@ -36,6 +37,50 @@ class MailwizzConfigUiTest extends TestCase
|
|||||||
$response->assertForbidden();
|
$response->assertForbidden();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function test_connected_mailwizz_shows_overview_until_wizard_requested(): void
|
||||||
|
{
|
||||||
|
$user = User::factory()->create(['role' => 'user']);
|
||||||
|
$page = $this->makePageForUser($user);
|
||||||
|
MailwizzConfig::query()->create([
|
||||||
|
'preregistration_page_id' => $page->id,
|
||||||
|
'api_key' => 'test-key',
|
||||||
|
'list_uid' => 'list-uid-1',
|
||||||
|
'list_name' => 'Main list',
|
||||||
|
'field_email' => 'EMAIL',
|
||||||
|
'field_first_name' => 'FNAME',
|
||||||
|
'field_last_name' => 'LNAME',
|
||||||
|
'field_phone' => null,
|
||||||
|
'field_coupon_code' => null,
|
||||||
|
'tag_field' => 'TAGS',
|
||||||
|
'tag_value' => 'preregister-source',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$overview = $this->actingAs($user)->get(route('admin.pages.mailwizz.edit', $page));
|
||||||
|
$overview->assertOk();
|
||||||
|
$overview->assertSee('Current configuration', escape: false);
|
||||||
|
$overview->assertSee('Change settings (wizard)', escape: false);
|
||||||
|
$overview->assertDontSee('Step 1: API key', escape: false);
|
||||||
|
|
||||||
|
$wizard = $this->actingAs($user)->get(route('admin.pages.mailwizz.edit', [
|
||||||
|
'page' => $page,
|
||||||
|
'wizard' => 1,
|
||||||
|
'step' => 1,
|
||||||
|
]));
|
||||||
|
$wizard->assertOk();
|
||||||
|
$wizard->assertSee('Step 1: API key', escape: false);
|
||||||
|
$wizard->assertSee('Cancel and return to overview', escape: false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_mailwizz_wizard_redirects_to_step_one_when_no_config_and_step_gt_one(): void
|
||||||
|
{
|
||||||
|
$user = User::factory()->create(['role' => 'user']);
|
||||||
|
$page = $this->makePageForUser($user);
|
||||||
|
|
||||||
|
$this->actingAs($user)
|
||||||
|
->get(route('admin.pages.mailwizz.edit', ['page' => $page, 'wizard' => 1, 'step' => 3]))
|
||||||
|
->assertRedirect(route('admin.pages.mailwizz.edit', ['page' => $page, 'wizard' => 1, 'step' => 1]));
|
||||||
|
}
|
||||||
|
|
||||||
private function makePageForUser(User $user): PreregistrationPage
|
private function makePageForUser(User $user): PreregistrationPage
|
||||||
{
|
{
|
||||||
return PreregistrationPage::query()->create([
|
return PreregistrationPage::query()->create([
|
||||||
|
|||||||
@@ -19,6 +19,27 @@ class SyncSubscriberToMailwizzTest extends TestCase
|
|||||||
{
|
{
|
||||||
use RefreshDatabase;
|
use RefreshDatabase;
|
||||||
|
|
||||||
|
public function test_subscribe_returns_ok_when_mailwizz_api_fails_under_sync_queue(): void
|
||||||
|
{
|
||||||
|
Http::fake([
|
||||||
|
'*' => Http::response(['status' => 'error', 'message' => 'service unavailable'], 503),
|
||||||
|
]);
|
||||||
|
|
||||||
|
$page = $this->makePageWithMailwizz();
|
||||||
|
|
||||||
|
$this->postJson(route('public.subscribe', ['publicPage' => $page->slug]), [
|
||||||
|
'first_name' => 'Broken',
|
||||||
|
'last_name' => 'Mailwizz',
|
||||||
|
'email' => 'broken-mailwizz@example.com',
|
||||||
|
])
|
||||||
|
->assertOk()
|
||||||
|
->assertJson(['success' => true]);
|
||||||
|
|
||||||
|
$subscriber = Subscriber::query()->where('email', 'broken-mailwizz@example.com')->first();
|
||||||
|
$this->assertNotNull($subscriber);
|
||||||
|
$this->assertFalse($subscriber->synced_to_mailwizz);
|
||||||
|
}
|
||||||
|
|
||||||
public function test_subscribe_with_mailwizz_config_runs_sync_create_path_and_marks_synced(): void
|
public function test_subscribe_with_mailwizz_config_runs_sync_create_path_and_marks_synced(): void
|
||||||
{
|
{
|
||||||
Http::fake(function (Request $request) {
|
Http::fake(function (Request $request) {
|
||||||
|
|||||||
Reference in New Issue
Block a user