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
25 KiB
Cursor Prompt — Weeztix Coupon Code Integration
Paste this into Cursor chat with
@Codebaseand@PreRegister-Development-Prompt.mdas 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):
{
"grant_type": "authorization_code",
"client_id": "...",
"client_secret": "...",
"redirect_uri": "...",
"code": "..."
}
Token response:
{
"token_type": "Bearer",
"expires_in": 259200,
"access_token": "THE_ACTUAL_TOKEN",
"refresh_token": "REFRESH_TOKEN",
"refresh_token_expires_in": 31535999
}
access_tokenexpires in ~3 days (259200 seconds)refresh_tokenexpires in ~365 days, can only be used once
Refresh token (POST to https://auth.openticket.tech/tokens):
{
"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:
{
"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 discountcodes: array of code objects, each with a uniquecodestring
Important: Duplicate CouponCodes cannot be added to a Coupon. Generate unique codes.
Part 2: Database Changes
New Migration: weeztix_configs table
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:
Schema::table('subscribers', function (Blueprint $table) {
$table->string('coupon_code')->nullable()->after('synced_at');
});
Model: WeeztixConfig
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:
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.
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:
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:
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
// 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:
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.
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):
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:
// 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:
<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:
// 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:
// 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 | 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_configsmigration - Add
coupon_codetosubscribersmigration - Add
field_coupon_codetomailwizz_configsmigration - Create
WeeztixConfigmodel - Update
PreregistrationPagemodel withweeztixConfigrelationship - Run migrations
Step 2: WeeztixService
- Create
app/Services/WeeztixService.php - Implement token management (get valid token, refresh, detect expiry)
- Implement
getCoupons()andcreateCouponCode() - 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@subscribeto 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
SyncSubscriberToMailwizzjob 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
encryptedcast - 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/