# 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

``` --- ## 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/