feat: add Weeztix OAuth, coupon codes, and Mailwizz mapping
Implement Weeztix integration per documentation: database config and subscriber coupon_code, OAuth redirect/callback, admin setup UI with company/coupon selection via AJAX, synchronous coupon creation on public subscribe with duplicate and rate-limit handling, Mailwizz field mapping for coupon codes, subscriber table and CSV export, and connection hint on the pages list. Made-with: Cursor
This commit is contained in:
@@ -28,6 +28,7 @@ class PageController extends Controller
|
||||
{
|
||||
$query = PreregistrationPage::query()
|
||||
->withCount('subscribers')
|
||||
->with('weeztixConfig')
|
||||
->orderByDesc('start_date');
|
||||
|
||||
if (! $request->user()?->isSuperadmin()) {
|
||||
|
||||
@@ -89,7 +89,7 @@ class SubscriberController extends Controller
|
||||
if ($phoneEnabled) {
|
||||
$headers[] = 'Phone';
|
||||
}
|
||||
$headers = array_merge($headers, ['Registered At', 'Synced to Mailwizz', 'Synced At']);
|
||||
$headers = array_merge($headers, ['Coupon Code', 'Registered At', 'Synced to Mailwizz', 'Synced At']);
|
||||
fputcsv($handle, $headers);
|
||||
|
||||
foreach ($subscribers as $sub) {
|
||||
@@ -97,6 +97,7 @@ class SubscriberController extends Controller
|
||||
if ($phoneEnabled) {
|
||||
$row[] = $sub->phoneDisplay() ?? '';
|
||||
}
|
||||
$row[] = $sub->coupon_code ?? '';
|
||||
$row[] = $sub->created_at?->toDateTimeString() ?? '';
|
||||
$row[] = $sub->synced_to_mailwizz ? 'Yes' : 'No';
|
||||
$row[] = $sub->synced_at?->toDateTimeString() ?? '';
|
||||
|
||||
111
app/Http/Controllers/Admin/WeeztixApiController.php
Normal file
111
app/Http/Controllers/Admin/WeeztixApiController.php
Normal file
@@ -0,0 +1,111 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Http\Controllers\Admin;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\PreregistrationPage;
|
||||
use App\Services\WeeztixService;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use RuntimeException;
|
||||
|
||||
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
|
||||
{
|
||||
$request->validate([
|
||||
'page_id' => ['required', 'integer', 'exists:preregistration_pages,id'],
|
||||
'company_guid' => ['required', 'string', 'max:255'],
|
||||
]);
|
||||
|
||||
$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);
|
||||
}
|
||||
|
||||
$companyGuid = $request->string('company_guid')->toString();
|
||||
$previousGuid = $config->company_guid;
|
||||
$config->setAttribute('company_guid', $companyGuid);
|
||||
|
||||
try {
|
||||
$raw = (new WeeztixService($config))->getCoupons();
|
||||
$coupons = $this->normalizeCouponsPayload($raw);
|
||||
|
||||
return response()->json(['coupons' => $coupons]);
|
||||
} catch (RuntimeException) {
|
||||
return response()->json([
|
||||
'message' => __('Kon kortingsbonnen niet laden.'),
|
||||
], 422);
|
||||
} finally {
|
||||
$config->setAttribute('company_guid', $previousGuid);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $raw
|
||||
* @return list<array{guid: string, name: string}>
|
||||
*/
|
||||
private function normalizeCouponsPayload(array $raw): array
|
||||
{
|
||||
$list = $raw;
|
||||
if (isset($raw['data']) && is_array($raw['data'])) {
|
||||
$list = $raw['data'];
|
||||
}
|
||||
|
||||
if (! is_array($list)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$out = [];
|
||||
foreach ($list as $row) {
|
||||
if (! is_array($row)) {
|
||||
continue;
|
||||
}
|
||||
$guid = data_get($row, 'guid') ?? data_get($row, 'id');
|
||||
if (! is_string($guid) || $guid === '') {
|
||||
continue;
|
||||
}
|
||||
$name = data_get($row, 'name') ?? data_get($row, 'title') ?? $guid;
|
||||
$out[] = [
|
||||
'guid' => $guid,
|
||||
'name' => is_string($name) ? $name : $guid,
|
||||
];
|
||||
}
|
||||
|
||||
return $out;
|
||||
}
|
||||
}
|
||||
59
app/Http/Controllers/Admin/WeeztixController.php
Normal file
59
app/Http/Controllers/Admin/WeeztixController.php
Normal file
@@ -0,0 +1,59 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Http\Controllers\Admin;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\Admin\UpdateWeeztixConfigRequest;
|
||||
use App\Models\PreregistrationPage;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\View\View;
|
||||
|
||||
class WeeztixController extends Controller
|
||||
{
|
||||
public function edit(PreregistrationPage $page): View
|
||||
{
|
||||
$this->authorize('update', $page);
|
||||
|
||||
$page->load('weeztixConfig');
|
||||
|
||||
return view('admin.weeztix.edit', compact('page'));
|
||||
}
|
||||
|
||||
public function update(UpdateWeeztixConfigRequest $request, PreregistrationPage $page): RedirectResponse
|
||||
{
|
||||
$validated = $request->validated();
|
||||
|
||||
foreach (['client_id', 'client_secret'] as $key) {
|
||||
if (array_key_exists($key, $validated) && $validated[$key] === '' && $page->weeztixConfig !== null) {
|
||||
unset($validated[$key]);
|
||||
}
|
||||
}
|
||||
|
||||
$validated['redirect_uri'] = route('admin.weeztix.callback', absolute: true);
|
||||
|
||||
DB::transaction(function () use ($page, $validated): void {
|
||||
$page->weeztixConfig()->updateOrCreate(
|
||||
['preregistration_page_id' => $page->id],
|
||||
array_merge($validated, ['preregistration_page_id' => $page->id])
|
||||
);
|
||||
});
|
||||
|
||||
return redirect()
|
||||
->route('admin.pages.weeztix.edit', $page)
|
||||
->with('status', __('Weeztix-configuratie opgeslagen.'));
|
||||
}
|
||||
|
||||
public function destroy(PreregistrationPage $page): RedirectResponse
|
||||
{
|
||||
$this->authorize('update', $page);
|
||||
|
||||
$page->weeztixConfig()?->delete();
|
||||
|
||||
return redirect()
|
||||
->route('admin.pages.weeztix.edit', $page)
|
||||
->with('status', __('Weeztix-integratie verwijderd.'));
|
||||
}
|
||||
}
|
||||
141
app/Http/Controllers/Admin/WeeztixOAuthController.php
Normal file
141
app/Http/Controllers/Admin/WeeztixOAuthController.php
Normal file
@@ -0,0 +1,141 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Http\Controllers\Admin;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\PreregistrationPage;
|
||||
use App\Services\WeeztixService;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Illuminate\Support\Str;
|
||||
use RuntimeException;
|
||||
|
||||
class WeeztixOAuthController extends Controller
|
||||
{
|
||||
public function redirect(PreregistrationPage $page): RedirectResponse
|
||||
{
|
||||
$this->authorize('update', $page);
|
||||
|
||||
$page->load('weeztixConfig');
|
||||
$config = $page->weeztixConfig;
|
||||
if ($config === null) {
|
||||
return redirect()
|
||||
->route('admin.pages.weeztix.edit', $page)
|
||||
->with('error', __('Sla eerst je client ID en client secret op.'));
|
||||
}
|
||||
|
||||
$clientId = $config->client_id;
|
||||
if (! is_string($clientId) || $clientId === '') {
|
||||
return redirect()
|
||||
->route('admin.pages.weeztix.edit', $page)
|
||||
->with('error', __('Vul een geldige Weeztix client ID in.'));
|
||||
}
|
||||
|
||||
$state = Str::random(40);
|
||||
session([
|
||||
'weeztix_oauth_state' => $state,
|
||||
'weeztix_page_id' => $page->id,
|
||||
]);
|
||||
|
||||
$redirectUri = $config->redirect_uri;
|
||||
if (! is_string($redirectUri) || $redirectUri === '') {
|
||||
$redirectUri = route('admin.weeztix.callback', absolute: true);
|
||||
}
|
||||
|
||||
$query = http_build_query([
|
||||
'client_id' => $clientId,
|
||||
'redirect_uri' => $redirectUri,
|
||||
'response_type' => 'code',
|
||||
'state' => $state,
|
||||
]);
|
||||
|
||||
$authorizeBase = rtrim(config('weeztix.auth_base_url'), '/').'/tokens/authorize';
|
||||
|
||||
return redirect()->away($authorizeBase.'?'.$query);
|
||||
}
|
||||
|
||||
public function callback(Request $request): RedirectResponse
|
||||
{
|
||||
if ($request->filled('error')) {
|
||||
Log::warning('Weeztix OAuth provider error', [
|
||||
'error' => $request->string('error')->toString(),
|
||||
'description' => $request->string('error_description')->toString(),
|
||||
]);
|
||||
|
||||
return $this->redirectToWeeztixEditWithSessionPage(__('Weeztix heeft de verbinding geweigerd. Probeer opnieuw.'));
|
||||
}
|
||||
|
||||
$request->validate([
|
||||
'state' => ['required', 'string'],
|
||||
'code' => ['required', 'string'],
|
||||
]);
|
||||
|
||||
$storedState = session('weeztix_oauth_state');
|
||||
$pageId = session('weeztix_page_id');
|
||||
if (! is_string($storedState) || $storedState === '' || ($pageId === null || (! is_int($pageId) && ! is_numeric($pageId)))) {
|
||||
return redirect()
|
||||
->route('admin.dashboard')
|
||||
->with('error', __('Ongeldige OAuth-sessie. Start opnieuw vanaf de Weeztix-pagina.'));
|
||||
}
|
||||
|
||||
if ($request->string('state')->toString() !== $storedState) {
|
||||
abort(403, 'Invalid OAuth state');
|
||||
}
|
||||
|
||||
$page = PreregistrationPage::query()->findOrFail((int) $pageId);
|
||||
$this->authorize('update', $page);
|
||||
|
||||
$config = $page->weeztixConfig;
|
||||
if ($config === null) {
|
||||
session()->forget(['weeztix_oauth_state', 'weeztix_page_id']);
|
||||
|
||||
return redirect()
|
||||
->route('admin.pages.weeztix.edit', $page)
|
||||
->with('error', __('Geen Weeztix-configuratie gevonden voor deze pagina.'));
|
||||
}
|
||||
|
||||
try {
|
||||
$service = new WeeztixService($config);
|
||||
$service->exchangeAuthorizationCode($request->string('code')->toString());
|
||||
} catch (RuntimeException $e) {
|
||||
Log::error('Weeztix OAuth callback failed', [
|
||||
'page_id' => $page->id,
|
||||
'message' => $e->getMessage(),
|
||||
]);
|
||||
|
||||
session()->forget(['weeztix_oauth_state', 'weeztix_page_id']);
|
||||
|
||||
return redirect()
|
||||
->route('admin.pages.weeztix.edit', $page)
|
||||
->with('error', __('Verbinden met Weeztix is mislukt. Controleer je gegevens en probeer opnieuw.'));
|
||||
}
|
||||
|
||||
session()->forget(['weeztix_oauth_state', 'weeztix_page_id']);
|
||||
|
||||
return redirect()
|
||||
->route('admin.pages.weeztix.edit', $page)
|
||||
->with('status', __('Succesvol verbonden met Weeztix.'));
|
||||
}
|
||||
|
||||
private function redirectToWeeztixEditWithSessionPage(string $message): RedirectResponse
|
||||
{
|
||||
$pageId = session('weeztix_page_id');
|
||||
session()->forget(['weeztix_oauth_state', 'weeztix_page_id']);
|
||||
|
||||
if (is_int($pageId) || is_numeric($pageId)) {
|
||||
$page = PreregistrationPage::query()->find((int) $pageId);
|
||||
if ($page !== null) {
|
||||
return redirect()
|
||||
->route('admin.pages.weeztix.edit', $page)
|
||||
->with('error', $message);
|
||||
}
|
||||
}
|
||||
|
||||
return redirect()
|
||||
->route('admin.dashboard')
|
||||
->with('error', $message);
|
||||
}
|
||||
}
|
||||
@@ -4,13 +4,19 @@ declare(strict_types=1);
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Exceptions\WeeztixCouponCodeConflictException;
|
||||
use App\Http\Requests\SubscribePublicPageRequest;
|
||||
use App\Jobs\SyncSubscriberToMailwizz;
|
||||
use App\Models\PageBlock;
|
||||
use App\Models\PreregistrationPage;
|
||||
use App\Models\Subscriber;
|
||||
use App\Models\WeeztixConfig;
|
||||
use App\Services\WeeztixService;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Illuminate\View\View;
|
||||
use Throwable;
|
||||
|
||||
class PublicPageController extends Controller
|
||||
{
|
||||
@@ -52,13 +58,70 @@ class PublicPageController extends Controller
|
||||
|
||||
$subscriber = $publicPage->subscribers()->create($validated);
|
||||
|
||||
$publicPage->loadMissing('weeztixConfig');
|
||||
$weeztix = $publicPage->weeztixConfig;
|
||||
if ($this->weeztixCanIssueCodes($weeztix)) {
|
||||
$this->tryAttachWeeztixCouponCode($subscriber, $weeztix);
|
||||
}
|
||||
|
||||
if ($publicPage->mailwizzConfig !== null) {
|
||||
SyncSubscriberToMailwizz::dispatch($subscriber);
|
||||
SyncSubscriberToMailwizz::dispatch($subscriber->fresh());
|
||||
}
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'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,
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
@@ -42,6 +42,7 @@ class UpdateMailwizzConfigRequest extends FormRequest
|
||||
'field_first_name' => ['required', 'string', 'max:255'],
|
||||
'field_last_name' => ['required', 'string', 'max:255'],
|
||||
'field_phone' => ['nullable', 'string', 'max:255'],
|
||||
'field_coupon_code' => ['nullable', 'string', 'max:255'],
|
||||
'tag_field' => ['required', 'string', 'max:255'],
|
||||
'tag_value' => ['required', 'string', 'max:255'],
|
||||
];
|
||||
|
||||
55
app/Http/Requests/Admin/UpdateWeeztixConfigRequest.php
Normal file
55
app/Http/Requests/Admin/UpdateWeeztixConfigRequest.php
Normal file
@@ -0,0 +1,55 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Http\Requests\Admin;
|
||||
|
||||
use App\Models\PreregistrationPage;
|
||||
use Illuminate\Contracts\Validation\ValidationRule;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Validation\Rule;
|
||||
|
||||
class UpdateWeeztixConfigRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
$page = $this->route('page');
|
||||
if (! $page instanceof PreregistrationPage) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $this->user()?->can('update', $page) ?? false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, array<int, ValidationRule|string>>
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
/** @var PreregistrationPage $page */
|
||||
$page = $this->route('page');
|
||||
|
||||
return [
|
||||
'client_id' => [
|
||||
'sometimes',
|
||||
Rule::requiredIf(fn (): bool => $page->weeztixConfig === null),
|
||||
'nullable',
|
||||
'string',
|
||||
'max:2048',
|
||||
],
|
||||
'client_secret' => [
|
||||
'sometimes',
|
||||
Rule::requiredIf(fn (): bool => $page->weeztixConfig === null),
|
||||
'nullable',
|
||||
'string',
|
||||
'max:2048',
|
||||
],
|
||||
'company_guid' => ['nullable', 'string', 'max:255'],
|
||||
'company_name' => ['nullable', 'string', 'max:255'],
|
||||
'coupon_guid' => ['nullable', 'string', 'max:255'],
|
||||
'coupon_name' => ['nullable', 'string', 'max:255'],
|
||||
'code_prefix' => ['nullable', 'string', 'max:32'],
|
||||
'usage_count' => ['nullable', 'integer', 'min:1', 'max:99999'],
|
||||
];
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user