diff --git a/app/Jobs/IssueWeeztixCouponForSubscriber.php b/app/Jobs/IssueWeeztixCouponForSubscriber.php index 6c6d1f5..dbe2c5d 100644 --- a/app/Jobs/IssueWeeztixCouponForSubscriber.php +++ b/app/Jobs/IssueWeeztixCouponForSubscriber.php @@ -52,27 +52,42 @@ final class IssueWeeztixCouponForSubscriber implements ShouldBeUnique, ShouldQue public function handle(): void { - $subscriber = Subscriber::query() - ->with(['preregistrationPage.weeztixConfig', 'preregistrationPage.mailwizzConfig']) - ->find($this->subscriber->id); + try { + $subscriber = Subscriber::query() + ->with(['preregistrationPage.weeztixConfig', 'preregistrationPage.mailwizzConfig']) + ->find($this->subscriber->id); - if ($subscriber === null) { - return; + 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); + } } - - $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); } public function failed(?Throwable $exception): void @@ -97,7 +112,14 @@ final class IssueWeeztixCouponForSubscriber implements ShouldBeUnique, ShouldQue $page?->loadMissing('mailwizzConfig'); if ($page?->mailwizzConfig !== null) { - SyncSubscriberToMailwizz::dispatch($subscriber->fresh()); + try { + SyncSubscriberToMailwizz::dispatch($subscriber->fresh()); + } catch (Throwable $e) { + Log::error('IssueWeeztixCouponForSubscriber: could not queue Mailwizz sync', [ + 'subscriber_id' => $subscriber->id, + 'message' => $e->getMessage(), + ]); + } } } diff --git a/app/Jobs/SyncSubscriberToMailwizz.php b/app/Jobs/SyncSubscriberToMailwizz.php index 79c7a3e..93df6a0 100644 --- a/app/Jobs/SyncSubscriberToMailwizz.php +++ b/app/Jobs/SyncSubscriberToMailwizz.php @@ -47,6 +47,18 @@ class SyncSubscriberToMailwizz implements ShouldBeUnique, ShouldQueueAfterCommit } public function handle(): void + { + try { + $this->runSync(); + } catch (Throwable $e) { + Log::error('SyncSubscriberToMailwizz: integration failed; subscriber remains local (use admin resync if needed)', [ + 'subscriber_id' => $this->subscriber->id, + 'message' => $e->getMessage(), + ]); + } + } + + private function runSync(): void { $subscriber = Subscriber::query() ->with(['preregistrationPage.mailwizzConfig']) diff --git a/app/Services/RegisterSubscriberOnPage.php b/app/Services/RegisterSubscriberOnPage.php index 7ff93e0..1154014 100644 --- a/app/Services/RegisterSubscriberOnPage.php +++ b/app/Services/RegisterSubscriberOnPage.php @@ -9,6 +9,8 @@ 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 @@ -26,10 +28,18 @@ final class RegisterSubscriberOnPage $page->loadMissing('weeztixConfig', 'mailwizzConfig'); $weeztix = $page->weeztixConfig; - if ($this->weeztixCanIssueCodes($weeztix)) { - IssueWeeztixCouponForSubscriber::dispatch($subscriber); - } elseif ($page->mailwizzConfig !== null) { - SyncSubscriberToMailwizz::dispatch($subscriber->fresh()); + 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; diff --git a/documentation/DEPLOYMENT-STRATEGY.md b/documentation/DEPLOYMENT-STRATEGY.md index f50ce09..bce73d1 100644 --- a/documentation/DEPLOYMENT-STRATEGY.md +++ b/documentation/DEPLOYMENT-STRATEGY.md @@ -271,7 +271,8 @@ Laravel ships with a `public/.htaccess` that works with Apache. Verify `mod_rewr 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. -- **Production:** set `QUEUE_CONNECTION=database` (or `redis`) — never rely on `sync`, or a Mailwizz/Weeztix failure can return HTTP 5xx to the visitor while the subscriber is already stored (confusing UX). +- **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: diff --git a/resources/js/app.js b/resources/js/app.js index 73f4dd3..e1c59c4 100644 --- a/resources/js/app.js +++ b/resources/js/app.js @@ -555,12 +555,21 @@ document.addEventListener('alpine:init', () => { this.startRedirectCountdownIfNeeded(); 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; } - if (data.errors !== undefined && data.errors !== null && typeof data.errors === 'object') { + if (hasFieldErrors) { this.fieldErrors = data.errors; } + if (!res.ok && !hasServerMessage && !hasFieldErrors) { + this.formError = this.genericError; + } } catch { this.formError = this.genericError; } finally { diff --git a/tests/Feature/SyncSubscriberToMailwizzTest.php b/tests/Feature/SyncSubscriberToMailwizzTest.php index 7d57620..f9b85eb 100644 --- a/tests/Feature/SyncSubscriberToMailwizzTest.php +++ b/tests/Feature/SyncSubscriberToMailwizzTest.php @@ -19,6 +19,27 @@ class SyncSubscriberToMailwizzTest extends TestCase { 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 { Http::fake(function (Request $request) {