feat(subscribe): queue Weeztix coupon, then Mailwizz; document queues

- RegisterSubscriberOnPage: persist subscriber then dispatch integrations
- IssueWeeztixCouponForSubscriber on weeztix queue; dispatches Mailwizz after
  coupon attempt (idempotent if coupon_code already set); failed() fallback
- SyncSubscriberToMailwizz implements ShouldQueueAfterCommit
- Deployment: worker listens weeztix,mailwizz,default; warn against sync in prod
- .env.example: QUEUE_CONNECTION notes for subscribe UX

Made-with: Cursor
This commit is contained in:
2026-04-05 11:34:01 +02:00
parent 7ed660ec55
commit d802ce2a7c
6 changed files with 222 additions and 71 deletions

View File

@@ -0,0 +1,153 @@
<?php
declare(strict_types=1);
namespace App\Jobs;
use App\Exceptions\WeeztixCouponCodeConflictException;
use App\Models\Subscriber;
use App\Models\WeeztixConfig;
use App\Services\WeeztixService;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldBeUnique;
use Illuminate\Contracts\Queue\ShouldQueueAfterCommit;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Log;
use Throwable;
/**
* Creates the Weeztix coupon code after the subscriber row exists, then queues Mailwizz sync
* so the external APIs never block the public HTTP response and Mailwizz runs after coupon_code is set when possible.
*/
final class IssueWeeztixCouponForSubscriber implements ShouldBeUnique, ShouldQueueAfterCommit
{
use Dispatchable;
use InteractsWithQueue;
use Queueable;
use SerializesModels;
public int $tries = 3;
/**
* @var list<int>
*/
public array $backoff = [5, 15, 45];
/**
* Seconds before the unique lock expires if the worker dies before releasing it.
*/
public int $uniqueFor = 300;
public function __construct(public Subscriber $subscriber)
{
$this->onQueue('weeztix');
}
public function uniqueId(): string
{
return 'weeztix-coupon-subscriber-'.$this->subscriber->getKey();
}
public function handle(): void
{
$subscriber = Subscriber::query()
->with(['preregistrationPage.weeztixConfig', 'preregistrationPage.mailwizzConfig'])
->find($this->subscriber->id);
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);
}
public function failed(?Throwable $exception): void
{
Log::error('IssueWeeztixCouponForSubscriber failed', [
'subscriber_id' => $this->subscriber->id,
'message' => $exception?->getMessage(),
]);
$subscriber = Subscriber::query()
->with('preregistrationPage.mailwizzConfig')
->find($this->subscriber->id);
if ($subscriber !== null) {
$this->dispatchMailwizzIfNeeded($subscriber);
}
}
private function dispatchMailwizzIfNeeded(Subscriber $subscriber): void
{
$page = $subscriber->preregistrationPage;
$page?->loadMissing('mailwizzConfig');
if ($page?->mailwizzConfig !== null) {
SyncSubscriberToMailwizz::dispatch($subscriber->fresh());
}
}
private function weeztixCanIssueCodes(?WeeztixConfig $config): bool
{
if ($config === null || ! $config->is_connected) {
return false;
}
$company = $config->company_guid;
$coupon = $config->coupon_guid;
return is_string($company) && $company !== '' && is_string($coupon) && $coupon !== '';
}
private function tryAttachWeeztixCouponCode(Subscriber $subscriber, WeeztixConfig $config): void
{
$freshConfig = $config->fresh();
if ($freshConfig === null) {
return;
}
$service = new WeeztixService($freshConfig);
$maxAttempts = 5;
for ($attempt = 0; $attempt < $maxAttempts; $attempt++) {
try {
$code = WeeztixService::generateUniqueCode(
is_string($freshConfig->code_prefix) && $freshConfig->code_prefix !== ''
? $freshConfig->code_prefix
: 'PREREG'
);
$service->createCouponCode($code);
$subscriber->update(['coupon_code' => $code]);
return;
} catch (WeeztixCouponCodeConflictException) {
continue;
} catch (Throwable $e) {
Log::error('Weeztix coupon creation failed', [
'subscriber_id' => $subscriber->id,
'message' => $e->getMessage(),
]);
return;
}
}
Log::warning('Weeztix coupon: exhausted duplicate retries', [
'subscriber_id' => $subscriber->id,
]);
}
}

View File

@@ -9,7 +9,7 @@ use App\Models\Subscriber;
use App\Services\MailwizzService;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldBeUnique;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Contracts\Queue\ShouldQueueAfterCommit;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
@@ -17,7 +17,7 @@ use Illuminate\Support\Facades\Log;
use RuntimeException;
use Throwable;
class SyncSubscriberToMailwizz implements ShouldBeUnique, ShouldQueue
class SyncSubscriberToMailwizz implements ShouldBeUnique, ShouldQueueAfterCommit
{
use Dispatchable;
use InteractsWithQueue;