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
176 lines
5.4 KiB
PHP
176 lines
5.4 KiB
PHP
<?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
|
|
{
|
|
try {
|
|
$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);
|
|
} 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);
|
|
}
|
|
}
|
|
}
|
|
|
|
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) {
|
|
try {
|
|
SyncSubscriberToMailwizz::dispatch($subscriber->fresh());
|
|
} catch (Throwable $e) {
|
|
Log::error('IssueWeeztixCouponForSubscriber: could not queue Mailwizz sync', [
|
|
'subscriber_id' => $subscriber->id,
|
|
'message' => $e->getMessage(),
|
|
]);
|
|
}
|
|
}
|
|
}
|
|
|
|
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,
|
|
]);
|
|
}
|
|
}
|