fix: isolate public subscribe from integration job failures
Queue Weeztix/Mailwizz after the HTTP response and catch dispatch errors. Jobs log Mailwizz/Weeztix API failures without rethrowing so sync driver and terminating callbacks do not surface 500s after a successful create. Add JS fallback for non-JSON error responses, deployment note, and a regression test for failing Mailwizz under QUEUE_CONNECTION=sync. Made-with: Cursor
This commit is contained in:
@@ -52,27 +52,42 @@ final class IssueWeeztixCouponForSubscriber implements ShouldBeUnique, ShouldQue
|
|||||||
|
|
||||||
public function handle(): void
|
public function handle(): void
|
||||||
{
|
{
|
||||||
$subscriber = Subscriber::query()
|
try {
|
||||||
->with(['preregistrationPage.weeztixConfig', 'preregistrationPage.mailwizzConfig'])
|
$subscriber = Subscriber::query()
|
||||||
->find($this->subscriber->id);
|
->with(['preregistrationPage.weeztixConfig', 'preregistrationPage.mailwizzConfig'])
|
||||||
|
->find($this->subscriber->id);
|
||||||
|
|
||||||
if ($subscriber === null) {
|
if ($subscriber === null) {
|
||||||
return;
|
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
|
public function failed(?Throwable $exception): void
|
||||||
@@ -97,7 +112,14 @@ final class IssueWeeztixCouponForSubscriber implements ShouldBeUnique, ShouldQue
|
|||||||
$page?->loadMissing('mailwizzConfig');
|
$page?->loadMissing('mailwizzConfig');
|
||||||
|
|
||||||
if ($page?->mailwizzConfig !== null) {
|
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(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -47,6 +47,18 @@ class SyncSubscriberToMailwizz implements ShouldBeUnique, ShouldQueueAfterCommit
|
|||||||
}
|
}
|
||||||
|
|
||||||
public function handle(): void
|
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()
|
$subscriber = Subscriber::query()
|
||||||
->with(['preregistrationPage.mailwizzConfig'])
|
->with(['preregistrationPage.mailwizzConfig'])
|
||||||
|
|||||||
@@ -9,6 +9,8 @@ use App\Jobs\SyncSubscriberToMailwizz;
|
|||||||
use App\Models\PreregistrationPage;
|
use App\Models\PreregistrationPage;
|
||||||
use App\Models\Subscriber;
|
use App\Models\Subscriber;
|
||||||
use App\Models\WeeztixConfig;
|
use App\Models\WeeztixConfig;
|
||||||
|
use Illuminate\Support\Facades\Log;
|
||||||
|
use Throwable;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Orchestrates public registration: local persist first, then queue external integrations
|
* Orchestrates public registration: local persist first, then queue external integrations
|
||||||
@@ -26,10 +28,18 @@ final class RegisterSubscriberOnPage
|
|||||||
$page->loadMissing('weeztixConfig', 'mailwizzConfig');
|
$page->loadMissing('weeztixConfig', 'mailwizzConfig');
|
||||||
$weeztix = $page->weeztixConfig;
|
$weeztix = $page->weeztixConfig;
|
||||||
|
|
||||||
if ($this->weeztixCanIssueCodes($weeztix)) {
|
try {
|
||||||
IssueWeeztixCouponForSubscriber::dispatch($subscriber);
|
if ($this->weeztixCanIssueCodes($weeztix)) {
|
||||||
} elseif ($page->mailwizzConfig !== null) {
|
IssueWeeztixCouponForSubscriber::dispatch($subscriber)->afterResponse();
|
||||||
SyncSubscriberToMailwizz::dispatch($subscriber->fresh());
|
} 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;
|
return $subscriber;
|
||||||
|
|||||||
@@ -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.
|
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).
|
- **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:
|
In DirectAdmin → Cron Jobs, add:
|
||||||
|
|||||||
@@ -555,12 +555,21 @@ document.addEventListener('alpine:init', () => {
|
|||||||
this.startRedirectCountdownIfNeeded();
|
this.startRedirectCountdownIfNeeded();
|
||||||
return;
|
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;
|
this.formError = data.message;
|
||||||
}
|
}
|
||||||
if (data.errors !== undefined && data.errors !== null && typeof data.errors === 'object') {
|
if (hasFieldErrors) {
|
||||||
this.fieldErrors = data.errors;
|
this.fieldErrors = data.errors;
|
||||||
}
|
}
|
||||||
|
if (!res.ok && !hasServerMessage && !hasFieldErrors) {
|
||||||
|
this.formError = this.genericError;
|
||||||
|
}
|
||||||
} catch {
|
} catch {
|
||||||
this.formError = this.genericError;
|
this.formError = this.genericError;
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
@@ -19,6 +19,27 @@ class SyncSubscriberToMailwizzTest extends TestCase
|
|||||||
{
|
{
|
||||||
use RefreshDatabase;
|
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
|
public function test_subscribe_with_mailwizz_config_runs_sync_create_path_and_marks_synced(): void
|
||||||
{
|
{
|
||||||
Http::fake(function (Request $request) {
|
Http::fake(function (Request $request) {
|
||||||
|
|||||||
Reference in New Issue
Block a user