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
710 lines
25 KiB
Markdown
710 lines
25 KiB
Markdown
# Cursor Prompt — Weeztix Coupon Code Integration
|
|
|
|
> Paste this into Cursor chat with `@Codebase` and `@PreRegister-Development-Prompt.md` as context.
|
|
|
|
---
|
|
|
|
## Role & Context
|
|
|
|
You are a senior full-stack developer and integration architect. You're working on the PreRegister Laravel 11 application (Blade + Tailwind CSS + Alpine.js). No React, no Vue, no Livewire.
|
|
|
|
The application already has a Mailwizz integration that syncs subscribers to an email marketing platform. You are now adding a **second integration: Weeztix** — a ticket sales platform. When a visitor pre-registers on a page, a unique coupon code is generated via the Weeztix API and assigned to the subscriber. This coupon code can then be forwarded to Mailwizz so the subscriber receives a personalized discount email.
|
|
|
|
---
|
|
|
|
## Part 1: Weeztix Platform Concepts
|
|
|
|
### Coupon vs CouponCode
|
|
|
|
Weeztix separates these into two resources:
|
|
|
|
- **Coupon** = a template/definition. Defines the discount type (percentage, fixed amount), value, what it applies to (orders or products). The Coupon is configured by the event organizer in Weeztix dashboard.
|
|
- **CouponCode** = the actual code a visitor enters in the ticket shop to get the discount. Each CouponCode belongs to a Coupon and inherits its discount settings. CouponCodes are unique strings like `PREREG-A7X9K2`.
|
|
|
|
**In our flow:** The user selects an existing Coupon in the backend. When a visitor registers, we create a unique CouponCode under that Coupon via the API.
|
|
|
|
### Authentication: OAuth2 Authorization Code Grant
|
|
|
|
Weeztix uses the OAuth2 Authorization Code flow. Key details:
|
|
|
|
**Endpoints:**
|
|
| Action | Method | URL |
|
|
|---|---|---|
|
|
| Authorize (redirect user) | GET | `https://auth.openticket.tech/tokens/authorize` |
|
|
| Exchange code for token | POST | `https://auth.openticket.tech/tokens` |
|
|
| Refresh token | POST | `https://auth.openticket.tech/tokens` |
|
|
| API requests | Various | `https://api.weeztix.com/...` |
|
|
|
|
**Authorization redirect parameters:**
|
|
```
|
|
https://auth.openticket.tech/tokens/authorize?
|
|
client_id={OAUTH_CLIENT_ID}
|
|
&redirect_uri={OAUTH_CLIENT_REDIRECT}
|
|
&response_type=code
|
|
&state={random_state}
|
|
```
|
|
|
|
**Token exchange (POST to `https://auth.openticket.tech/tokens`):**
|
|
```json
|
|
{
|
|
"grant_type": "authorization_code",
|
|
"client_id": "...",
|
|
"client_secret": "...",
|
|
"redirect_uri": "...",
|
|
"code": "..."
|
|
}
|
|
```
|
|
|
|
**Token response:**
|
|
```json
|
|
{
|
|
"token_type": "Bearer",
|
|
"expires_in": 259200,
|
|
"access_token": "THE_ACTUAL_TOKEN",
|
|
"refresh_token": "REFRESH_TOKEN",
|
|
"refresh_token_expires_in": 31535999
|
|
}
|
|
```
|
|
- `access_token` expires in ~3 days (259200 seconds)
|
|
- `refresh_token` expires in ~365 days, can only be used once
|
|
|
|
**Refresh token (POST to `https://auth.openticket.tech/tokens`):**
|
|
```json
|
|
{
|
|
"grant_type": "refresh_token",
|
|
"refresh_token": "...",
|
|
"client_id": "...",
|
|
"client_secret": "..."
|
|
}
|
|
```
|
|
Returns a new `access_token` and a new `refresh_token`.
|
|
|
|
**API requests require:**
|
|
- Header: `Authorization: Bearer {access_token}`
|
|
- Header: `Company: {company_guid}` (to scope requests to a specific company)
|
|
|
|
### API Endpoints We Need
|
|
|
|
| Action | Method | URL | Notes |
|
|
|---|---|---|---|
|
|
| Get coupons | GET | `https://api.weeztix.com/coupon` | Returns coupons for the company (Company header) |
|
|
| Add coupon codes | POST | `https://api.weeztix.com/coupon/{coupon_guid}/couponCode` | Creates one or more codes under a coupon |
|
|
|
|
**Add CouponCodes request body:**
|
|
```json
|
|
{
|
|
"usage_count": 1,
|
|
"applies_to_count": null,
|
|
"codes": [
|
|
{
|
|
"code": "PREREG-A7X9K2"
|
|
}
|
|
]
|
|
}
|
|
```
|
|
- `usage_count`: how many times the code can be used (1 = single use per subscriber)
|
|
- `applies_to_count`: null = unlimited items in the order can use the discount
|
|
- `codes`: array of code objects, each with a unique `code` string
|
|
|
|
**Important:** Duplicate CouponCodes cannot be added to a Coupon. Generate unique codes.
|
|
|
|
---
|
|
|
|
## Part 2: Database Changes
|
|
|
|
### New Migration: `weeztix_configs` table
|
|
|
|
```php
|
|
Schema::create('weeztix_configs', function (Blueprint $table) {
|
|
$table->id();
|
|
$table->foreignId('preregistration_page_id')->constrained()->cascadeOnDelete();
|
|
|
|
// OAuth credentials (encrypted at rest)
|
|
$table->text('client_id');
|
|
$table->text('client_secret');
|
|
$table->string('redirect_uri');
|
|
|
|
// OAuth tokens (encrypted at rest)
|
|
$table->text('access_token')->nullable();
|
|
$table->text('refresh_token')->nullable();
|
|
$table->timestamp('token_expires_at')->nullable();
|
|
$table->timestamp('refresh_token_expires_at')->nullable();
|
|
|
|
// Company context
|
|
$table->string('company_guid')->nullable();
|
|
$table->string('company_name')->nullable();
|
|
|
|
// Selected coupon
|
|
$table->string('coupon_guid')->nullable();
|
|
$table->string('coupon_name')->nullable();
|
|
|
|
// CouponCode settings
|
|
$table->string('code_prefix')->default('PREREG'); // prefix for generated codes
|
|
$table->integer('usage_count')->default(1); // how many times a code can be used
|
|
|
|
$table->boolean('is_connected')->default(false); // OAuth flow completed?
|
|
$table->timestamps();
|
|
});
|
|
```
|
|
|
|
### Modify `subscribers` table
|
|
|
|
Add a column to store the generated coupon code per subscriber:
|
|
|
|
```php
|
|
Schema::table('subscribers', function (Blueprint $table) {
|
|
$table->string('coupon_code')->nullable()->after('synced_at');
|
|
});
|
|
```
|
|
|
|
### Model: `WeeztixConfig`
|
|
|
|
```php
|
|
class WeeztixConfig extends Model
|
|
{
|
|
protected $fillable = [
|
|
'preregistration_page_id', 'client_id', 'client_secret', 'redirect_uri',
|
|
'access_token', 'refresh_token', 'token_expires_at', 'refresh_token_expires_at',
|
|
'company_guid', 'company_name', 'coupon_guid', 'coupon_name',
|
|
'code_prefix', 'usage_count', 'is_connected',
|
|
];
|
|
|
|
protected $casts = [
|
|
'client_id' => 'encrypted',
|
|
'client_secret' => 'encrypted',
|
|
'access_token' => 'encrypted',
|
|
'refresh_token' => 'encrypted',
|
|
'token_expires_at' => 'datetime',
|
|
'refresh_token_expires_at' => 'datetime',
|
|
'is_connected' => 'boolean',
|
|
];
|
|
|
|
public function preregistrationPage(): BelongsTo
|
|
{
|
|
return $this->belongsTo(PreregistrationPage::class);
|
|
}
|
|
|
|
public function isTokenExpired(): bool
|
|
{
|
|
return !$this->token_expires_at || $this->token_expires_at->isPast();
|
|
}
|
|
|
|
public function isRefreshTokenExpired(): bool
|
|
{
|
|
return !$this->refresh_token_expires_at || $this->refresh_token_expires_at->isPast();
|
|
}
|
|
}
|
|
```
|
|
|
|
### Update `PreregistrationPage` model
|
|
|
|
Add:
|
|
```php
|
|
public function weeztixConfig(): HasOne
|
|
{
|
|
return $this->hasOne(WeeztixConfig::class);
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## Part 3: WeeztixService
|
|
|
|
Create `app/Services/WeeztixService.php` — encapsulates all Weeztix API communication with automatic token refresh.
|
|
|
|
```php
|
|
class WeeztixService
|
|
{
|
|
private WeeztixConfig $config;
|
|
|
|
public function __construct(WeeztixConfig $config)
|
|
{
|
|
$this->config = $config;
|
|
}
|
|
|
|
/**
|
|
* Get a valid access token, refreshing if necessary.
|
|
* Updates the config model with new tokens.
|
|
*/
|
|
public function getValidAccessToken(): string
|
|
|
|
/**
|
|
* Refresh the access token using the refresh token.
|
|
* Stores the new tokens in the config.
|
|
* Throws exception if refresh token is also expired (re-auth needed).
|
|
*/
|
|
private function refreshAccessToken(): void
|
|
|
|
/**
|
|
* Make an authenticated API request.
|
|
* Automatically refreshes token if expired.
|
|
*/
|
|
private function apiRequest(string $method, string $url, array $data = []): array
|
|
|
|
/**
|
|
* Get all companies the token has access to.
|
|
* GET https://auth.weeztix.com/users/me (or similar endpoint)
|
|
*/
|
|
public function getCompanies(): array
|
|
|
|
/**
|
|
* Get all coupons for the configured company.
|
|
* GET https://api.weeztix.com/coupon
|
|
* Header: Company: {company_guid}
|
|
*/
|
|
public function getCoupons(): array
|
|
|
|
/**
|
|
* Create a unique coupon code under the configured coupon.
|
|
* POST https://api.weeztix.com/coupon/{coupon_guid}/couponCode
|
|
* Header: Company: {company_guid}
|
|
*/
|
|
public function createCouponCode(string $code): array
|
|
|
|
/**
|
|
* Generate a unique code string.
|
|
* Format: {prefix}-{random alphanumeric}
|
|
* Example: PREREG-A7X9K2
|
|
*/
|
|
public static function generateUniqueCode(string $prefix = 'PREREG', int $length = 6): string
|
|
}
|
|
```
|
|
|
|
### Key Implementation Details for WeeztixService
|
|
|
|
**Token refresh logic:**
|
|
```
|
|
1. Check if access_token exists and is not expired
|
|
2. If expired, check if refresh_token exists and is not expired
|
|
3. If refresh_token valid: POST to https://auth.openticket.tech/tokens with grant_type=refresh_token
|
|
4. Store new access_token, refresh_token, and their expiry timestamps
|
|
5. If refresh_token also expired: mark config as disconnected, throw exception (user must re-authorize)
|
|
```
|
|
|
|
**API request wrapper:**
|
|
```php
|
|
private function apiRequest(string $method, string $url, array $data = []): array
|
|
{
|
|
$token = $this->getValidAccessToken();
|
|
|
|
$response = Http::withHeaders([
|
|
'Authorization' => "Bearer {$token}",
|
|
'Company' => $this->config->company_guid,
|
|
])->{$method}($url, $data);
|
|
|
|
if ($response->status() === 401) {
|
|
// Token might have been revoked, try refresh once
|
|
$this->refreshAccessToken();
|
|
$token = $this->config->access_token;
|
|
|
|
$response = Http::withHeaders([
|
|
'Authorization' => "Bearer {$token}",
|
|
'Company' => $this->config->company_guid,
|
|
])->{$method}($url, $data);
|
|
}
|
|
|
|
if ($response->failed()) {
|
|
Log::error('Weeztix API request failed', [
|
|
'url' => $url,
|
|
'status' => $response->status(),
|
|
'body' => $response->json(),
|
|
]);
|
|
throw new \RuntimeException("Weeztix API request failed: {$response->status()}");
|
|
}
|
|
|
|
return $response->json();
|
|
}
|
|
```
|
|
|
|
**Unique code generation:**
|
|
```php
|
|
public static function generateUniqueCode(string $prefix = 'PREREG', int $length = 6): string
|
|
{
|
|
$chars = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789'; // no 0/O/1/I to avoid confusion
|
|
$code = '';
|
|
for ($i = 0; $i < $length; $i++) {
|
|
$code .= $chars[random_int(0, strlen($chars) - 1)];
|
|
}
|
|
return strtoupper($prefix) . '-' . $code;
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## Part 4: OAuth Flow — Routes & Controller
|
|
|
|
### Routes
|
|
|
|
```php
|
|
// Inside the admin auth middleware group:
|
|
|
|
// Weeztix configuration (per page)
|
|
Route::get('pages/{page}/weeztix', [WeeztixController::class, 'edit'])->name('pages.weeztix.edit');
|
|
Route::put('pages/{page}/weeztix', [WeeztixController::class, 'update'])->name('pages.weeztix.update');
|
|
Route::delete('pages/{page}/weeztix', [WeeztixController::class, 'destroy'])->name('pages.weeztix.destroy');
|
|
|
|
// OAuth callback (needs to be accessible during OAuth flow)
|
|
Route::get('weeztix/callback', [WeeztixOAuthController::class, 'callback'])->name('weeztix.callback');
|
|
|
|
// AJAX endpoints for dynamic loading
|
|
Route::post('weeztix/coupons', [WeeztixApiController::class, 'coupons'])->name('weeztix.coupons');
|
|
```
|
|
|
|
### OAuth Flow
|
|
|
|
**Step 1: User enters client_id and client_secret in the backend form.**
|
|
|
|
**Step 2: User clicks "Connect to Weeztix" button.**
|
|
|
|
The controller builds the authorization URL and redirects:
|
|
```php
|
|
public function redirect(PreregistrationPage $page)
|
|
{
|
|
$config = $page->weeztixConfig;
|
|
$state = Str::random(40);
|
|
|
|
// Store state + page ID in session for the callback
|
|
session(['weeztix_oauth_state' => $state, 'weeztix_page_id' => $page->id]);
|
|
|
|
$query = http_build_query([
|
|
'client_id' => $config->client_id,
|
|
'redirect_uri' => route('admin.weeztix.callback'),
|
|
'response_type' => 'code',
|
|
'state' => $state,
|
|
]);
|
|
|
|
return redirect("https://auth.openticket.tech/tokens/authorize?{$query}");
|
|
}
|
|
```
|
|
|
|
**Step 3: Weeztix redirects back to our callback URL with `code` and `state`.**
|
|
|
|
```php
|
|
public function callback(Request $request)
|
|
{
|
|
// Verify state
|
|
$storedState = session('weeztix_oauth_state');
|
|
$pageId = session('weeztix_page_id');
|
|
abort_if($request->state !== $storedState, 403, 'Invalid state');
|
|
|
|
// Exchange code for tokens
|
|
$response = Http::post('https://auth.openticket.tech/tokens', [
|
|
'grant_type' => 'authorization_code',
|
|
'client_id' => $config->client_id,
|
|
'client_secret' => $config->client_secret,
|
|
'redirect_uri' => route('admin.weeztix.callback'),
|
|
'code' => $request->code,
|
|
]);
|
|
|
|
// Store tokens in weeztix_configs
|
|
$config->update([
|
|
'access_token' => $data['access_token'],
|
|
'refresh_token' => $data['refresh_token'],
|
|
'token_expires_at' => now()->addSeconds($data['expires_in']),
|
|
'refresh_token_expires_at' => now()->addSeconds($data['refresh_token_expires_in']),
|
|
'is_connected' => true,
|
|
]);
|
|
|
|
// Redirect back to the Weeztix config page
|
|
return redirect()->route('admin.pages.weeztix.edit', $pageId)
|
|
->with('success', 'Successfully connected to Weeztix!');
|
|
}
|
|
```
|
|
|
|
**Step 4: After OAuth, the user selects a Company and Coupon (see Part 5).**
|
|
|
|
---
|
|
|
|
## Part 5: Backend Configuration UI
|
|
|
|
### Weeztix Configuration Page (tab/section within page edit)
|
|
|
|
Build a multi-step configuration interface similar to the Mailwizz config:
|
|
|
|
```
|
|
┌─── Weeztix Integration ────────────────────────────────┐
|
|
│ │
|
|
│ Step 1: OAuth Credentials │
|
|
│ ┌────────────────────────────────────────────────┐ │
|
|
│ │ Client ID: [________________________] │ │
|
|
│ │ Client Secret: [________________________] │ │
|
|
│ │ Redirect URI: https://preregister.crewli.nl │ │
|
|
│ │ /admin/weeztix/callback │ │
|
|
│ │ (auto-generated, read-only) │ │
|
|
│ │ │ │
|
|
│ │ [Connect to Weeztix] ← OAuth redirect button │ │
|
|
│ └────────────────────────────────────────────────┘ │
|
|
│ │
|
|
│ Step 2: Select Coupon (shown after OAuth success) │
|
|
│ ┌────────────────────────────────────────────────┐ │
|
|
│ │ Status: ✅ Connected │ │
|
|
│ │ Token expires: 2026-04-07 14:30 │ │
|
|
│ │ │ │
|
|
│ │ Coupon: [▼ Select a coupon ] │ │
|
|
│ │ - Early Bird 20% discount │ │
|
|
│ │ - Pre-register €5 korting │ │
|
|
│ │ │ │
|
|
│ │ Code Prefix: [PREREG ] │ │
|
|
│ │ Usage per Code: [1] │ │
|
|
│ │ │ │
|
|
│ │ [Save Configuration] │ │
|
|
│ └────────────────────────────────────────────────┘ │
|
|
│ │
|
|
│ [Disconnect Weeztix] │
|
|
└─────────────────────────────────────────────────────────┘
|
|
```
|
|
|
|
**Instructions to show the user:**
|
|
> "Before connecting, create an OAuth Client in your Weeztix dashboard and set the redirect URI to: `{route('admin.weeztix.callback')}`. Also create a Coupon with the desired discount settings. You will select this Coupon here after connecting."
|
|
|
|
**Loading Coupons (AJAX):**
|
|
After successful OAuth, use Alpine.js to fetch coupons via:
|
|
```
|
|
POST /admin/weeztix/coupons
|
|
Body: { page_id: ... }
|
|
```
|
|
The controller uses `WeeztixService::getCoupons()` and returns the list as JSON.
|
|
|
|
---
|
|
|
|
## Part 6: Frontend — Coupon Code Generation on Registration
|
|
|
|
### Updated Subscription Flow
|
|
|
|
When a visitor registers on a public page:
|
|
|
|
```
|
|
1. Validate form input
|
|
2. Check for duplicate email
|
|
3. Store subscriber in database
|
|
4. IF Weeztix is configured AND connected:
|
|
a. Generate a unique coupon code: PREREG-A7X9K2
|
|
b. Create the coupon code in Weeztix via API
|
|
c. Store the coupon code on the subscriber record
|
|
5. IF Mailwizz is configured:
|
|
a. Dispatch SyncSubscriberToMailwizz job
|
|
b. The job should include the coupon_code in the Mailwizz subscriber data
|
|
(if a Mailwizz field mapping for coupon_code is configured)
|
|
6. Return success with thank-you message
|
|
```
|
|
|
|
### Job: `CreateWeeztixCouponCode`
|
|
|
|
Create a queued job (or execute synchronously if speed is critical — the visitor should see the coupon code in the thank-you message):
|
|
|
|
```php
|
|
class CreateWeeztixCouponCode implements ShouldQueue
|
|
{
|
|
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
|
|
|
public function __construct(
|
|
private Subscriber $subscriber
|
|
) {}
|
|
|
|
public function handle(): void
|
|
{
|
|
$page = $this->subscriber->preregistrationPage;
|
|
$config = $page->weeztixConfig;
|
|
|
|
if (!$config || !$config->is_connected || !$config->coupon_guid) {
|
|
return;
|
|
}
|
|
|
|
$service = new WeeztixService($config);
|
|
$code = WeeztixService::generateUniqueCode($config->code_prefix);
|
|
|
|
try {
|
|
$service->createCouponCode($code);
|
|
|
|
$this->subscriber->update(['coupon_code' => $code]);
|
|
} catch (\Exception $e) {
|
|
Log::error('Failed to create Weeztix coupon code', [
|
|
'subscriber_id' => $this->subscriber->id,
|
|
'error' => $e->getMessage(),
|
|
]);
|
|
throw $e; // Let the job retry
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
**IMPORTANT DECISION: Sync vs Async**
|
|
|
|
If the coupon code should be shown in the thank-you message immediately after registration, the Weeztix API call must be **synchronous** (not queued). In that case, call the service directly in `PublicPageController@subscribe` instead of dispatching a job:
|
|
|
|
```php
|
|
// In PublicPageController@subscribe, after storing the subscriber:
|
|
if ($page->weeztixConfig && $page->weeztixConfig->is_connected) {
|
|
try {
|
|
$service = new WeeztixService($page->weeztixConfig);
|
|
$code = WeeztixService::generateUniqueCode($page->weeztixConfig->code_prefix);
|
|
$service->createCouponCode($code);
|
|
$subscriber->update(['coupon_code' => $code]);
|
|
} catch (\Exception $e) {
|
|
Log::error('Weeztix coupon creation failed', ['error' => $e->getMessage()]);
|
|
// Don't fail the registration — the subscriber is already saved
|
|
}
|
|
}
|
|
|
|
// Then dispatch the Mailwizz sync job (which includes the coupon_code)
|
|
if ($page->mailwizzConfig) {
|
|
SyncSubscriberToMailwizz::dispatch($subscriber->fresh());
|
|
}
|
|
|
|
return response()->json([
|
|
'success' => true,
|
|
'message' => $page->thank_you_message ?? 'Bedankt voor je registratie!',
|
|
'coupon_code' => $subscriber->coupon_code, // null if Weeztix not configured
|
|
]);
|
|
```
|
|
|
|
### Show Coupon Code in Thank-You State
|
|
|
|
Update the Alpine.js success state to display the coupon code if present:
|
|
|
|
```html
|
|
<div x-show="submitted" class="text-center text-white">
|
|
<p x-text="successMessage"></p>
|
|
|
|
<template x-if="couponCode">
|
|
<div class="mt-6 bg-white/10 backdrop-blur rounded-xl p-6 border border-white/20">
|
|
<p class="text-sm text-white/70 mb-2">Jouw kortingscode:</p>
|
|
<div class="flex items-center justify-center gap-3">
|
|
<span class="text-2xl font-mono font-bold tracking-wider text-orange-400"
|
|
x-text="couponCode"></span>
|
|
<button @click="copyCode()"
|
|
class="text-white/50 hover:text-white transition">
|
|
<!-- Copy icon (Heroicon clipboard) -->
|
|
<svg>...</svg>
|
|
</button>
|
|
</div>
|
|
<p class="text-xs text-white/50 mt-3">
|
|
Gebruik deze code bij het afrekenen in de ticketshop.
|
|
</p>
|
|
</div>
|
|
</template>
|
|
</div>
|
|
```
|
|
|
|
---
|
|
|
|
## Part 7: Mailwizz Integration — Forward Coupon Code
|
|
|
|
### Add Coupon Code Field Mapping to Mailwizz Config
|
|
|
|
Add a new field to `mailwizz_configs`:
|
|
|
|
```php
|
|
// New migration
|
|
Schema::table('mailwizz_configs', function (Blueprint $table) {
|
|
$table->string('field_coupon_code')->nullable()->after('field_phone');
|
|
// This maps to a Mailwizz custom field (e.g., 'COUPON' tag)
|
|
});
|
|
```
|
|
|
|
### Update Mailwizz Configuration Wizard
|
|
|
|
In the field mapping step, add an additional mapping:
|
|
- **Coupon Code** → show text fields from the Mailwizz list → let user select the field where the coupon code should be stored (e.g., a custom field with tag `COUPON`)
|
|
- This mapping is optional — only shown if Weeztix is also configured
|
|
|
|
### Update SyncSubscriberToMailwizz Job
|
|
|
|
When building the Mailwizz subscriber data array, include the coupon code if the mapping exists:
|
|
|
|
```php
|
|
// In the sync job, when building the data array:
|
|
if ($config->field_coupon_code && $subscriber->coupon_code) {
|
|
$data[$config->field_coupon_code] = $subscriber->coupon_code;
|
|
}
|
|
```
|
|
|
|
This allows the Mailwizz email template to include `[COUPON]` as a merge tag, so each subscriber receives their personal coupon code in the email.
|
|
|
|
---
|
|
|
|
## Part 8: Subscriber Management — Show Coupon Codes
|
|
|
|
### Update Subscribers Index
|
|
|
|
Add the coupon code column to the subscribers table in the backend:
|
|
|
|
| First Name | Last Name | Email | Phone | Coupon Code | Synced | Registered |
|
|
|---|---|---|---|---|---|---|
|
|
| Bert | Hausmans | bert@... | +316... | PREREG-A7X9K2 | ✅ | 2026-04-04 |
|
|
|
|
### Update CSV Export
|
|
|
|
Add the `coupon_code` column to the CSV export.
|
|
|
|
---
|
|
|
|
## Part 9: Implementation Order
|
|
|
|
### Step 1: Database
|
|
- Create `weeztix_configs` migration
|
|
- Add `coupon_code` to `subscribers` migration
|
|
- Add `field_coupon_code` to `mailwizz_configs` migration
|
|
- Create `WeeztixConfig` model
|
|
- Update `PreregistrationPage` model with `weeztixConfig` relationship
|
|
- Run migrations
|
|
|
|
### Step 2: WeeztixService
|
|
- Create `app/Services/WeeztixService.php`
|
|
- Implement token management (get valid token, refresh, detect expiry)
|
|
- Implement `getCoupons()` and `createCouponCode()`
|
|
- Add unique code generation
|
|
|
|
### Step 3: OAuth Flow
|
|
- Create `WeeztixOAuthController` (redirect + callback)
|
|
- Add routes for OAuth
|
|
- Handle state validation and token storage
|
|
|
|
### Step 4: Backend Configuration UI
|
|
- Create `WeeztixController` (edit, update, destroy)
|
|
- Create `WeeztixApiController` (coupons AJAX endpoint)
|
|
- Create Blade view `admin/weeztix/edit.blade.php`
|
|
- Build the multi-step form with Alpine.js
|
|
- Add link/tab in the page edit navigation
|
|
|
|
### Step 5: Frontend Coupon Generation
|
|
- Update `PublicPageController@subscribe` to generate coupon codes
|
|
- Update Alpine.js success state to show coupon code
|
|
- Add copy-to-clipboard functionality
|
|
|
|
### Step 6: Mailwizz Coupon Forwarding
|
|
- Update Mailwizz config migration and model
|
|
- Update Mailwizz configuration wizard (add coupon_code field mapping)
|
|
- Update `SyncSubscriberToMailwizz` job to include coupon code
|
|
|
|
### Step 7: Backend Updates
|
|
- Update subscribers index to show coupon codes
|
|
- Update CSV export to include coupon codes
|
|
- Add Weeztix connection status indicator on the pages index
|
|
|
|
### Step 8: Error Handling & Edge Cases
|
|
- Handle Weeztix API downtime (don't fail registration)
|
|
- Handle expired refresh tokens (show "Reconnect" button)
|
|
- Handle duplicate coupon codes (retry with new code)
|
|
- Handle Weeztix rate limiting
|
|
- Log all API interactions
|
|
|
|
---
|
|
|
|
## Important Rules
|
|
|
|
- **OAuth credentials must be encrypted** — use Laravel's `encrypted` cast
|
|
- **Token refresh must be automatic** — the user should never have to manually refresh
|
|
- **Registration must not fail** if Weeztix is down — coupon code is a bonus, not a requirement
|
|
- **Coupon codes must be unique** — use the unambiguous character set (no 0/O/1/I)
|
|
- **Blade + Alpine.js + Tailwind only** — no additional JS frameworks
|
|
- **All text in Dutch** — labels, messages, instructions
|
|
- **Don't break existing integrations** — Mailwizz sync must continue working, form blocks must remain functional
|
|
- **Refer to the Weeztix API documentation** for exact request/response formats:
|
|
- Authentication: https://docs.weeztix.com/docs/introduction/authentication/
|
|
- Request token: https://docs.weeztix.com/docs/introduction/authentication/request-token
|
|
- Refresh token: https://docs.weeztix.com/docs/introduction/authentication/refresh-token
|
|
- Get Coupons: https://docs.weeztix.com/api/dashboard/get-coupons
|
|
- Add CouponCodes: https://docs.weeztix.com/api/dashboard/add-coupon-codes
|
|
- Issuing requests (Company header): https://docs.weeztix.com/docs/introduction/issue-request/
|