Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
| Total | |
0.00% |
0 / 21 |
|
0.00% |
0 / 5 |
CRAP | |
0.00% |
0 / 1 |
| AbstractSendgridJob | |
0.00% |
0 / 21 |
|
0.00% |
0 / 5 |
42 | |
0.00% |
0 / 1 |
| middleware | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
| buildMail | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
0 | |||
| logLabel | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
0 | |||
| handle | |
0.00% |
0 / 13 |
|
0.00% |
0 / 1 |
20 | |||
| failed | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
2 | |||
| 1 | <?php |
| 2 | |
| 3 | namespace App\Jobs\Email; |
| 4 | |
| 5 | use App\Services\SendgridLogger; |
| 6 | use Illuminate\Bus\Queueable; |
| 7 | use Illuminate\Contracts\Queue\ShouldQueue; |
| 8 | use Illuminate\Foundation\Bus\Dispatchable; |
| 9 | use Illuminate\Queue\InteractsWithQueue; |
| 10 | use Illuminate\Queue\Middleware\RateLimited; |
| 11 | use Illuminate\Queue\SerializesModels; |
| 12 | use Illuminate\Support\Facades\Log; |
| 13 | use SendGrid\Mail\Mail; |
| 14 | |
| 15 | /** |
| 16 | * FIRE-1147: base class for all SendGrid sends that used to run synchronously |
| 17 | * inside HTTP request handlers (approve/reject/follow-up email). |
| 18 | * |
| 19 | * Subclasses implement buildMail() — the same template-construction code that |
| 20 | * used to live inline in Quotations.php / Notifications.php — and this base |
| 21 | * handles the boilerplate: client instantiation, response logging via |
| 22 | * SendgridLogger (preserving the existing tbl_sendgrid_outbound_log audit |
| 23 | * trail), retry policy, and dead-letter logging. |
| 24 | * |
| 25 | * Retries: 5 attempts with exponential backoff. Final failure lands in |
| 26 | * `failed_jobs` (Laravel built-in) plus an explicit error log line. |
| 27 | * |
| 28 | * Rate limit: shared 'sendgrid' bucket across all subclasses so we don't |
| 29 | * burst past SendGrid's per-account limit during peak approval traffic. |
| 30 | */ |
| 31 | abstract class AbstractSendgridJob implements ShouldQueue |
| 32 | { |
| 33 | use Dispatchable; |
| 34 | use InteractsWithQueue; |
| 35 | use Queueable; |
| 36 | use SerializesModels; |
| 37 | |
| 38 | public int $tries = 5; |
| 39 | |
| 40 | /** Exponential backoff in seconds: 10s, 30s, 1m, 5m, 15m. */ |
| 41 | public array $backoff = [10, 30, 60, 300, 900]; |
| 42 | |
| 43 | public int $timeout = 60; |
| 44 | |
| 45 | public function middleware(): array |
| 46 | { |
| 47 | return [new RateLimited('sendgrid')]; |
| 48 | } |
| 49 | |
| 50 | /** |
| 51 | * Subclasses build and return the SendGrid Mail message. |
| 52 | * Return null to skip the send entirely (e.g. no recipients to notify). |
| 53 | */ |
| 54 | abstract protected function buildMail(): ?Mail; |
| 55 | |
| 56 | /** |
| 57 | * A human-readable label included in SendgridLogger entries so the |
| 58 | * audit log shows which controller path originally triggered the send. |
| 59 | */ |
| 60 | abstract protected function logLabel(): string; |
| 61 | |
| 62 | public function handle(): void |
| 63 | { |
| 64 | $email = $this->buildMail(); |
| 65 | if ($email === null) { |
| 66 | return; |
| 67 | } |
| 68 | |
| 69 | $sendgrid = new \SendGrid(config('services.sendgrid.api_key')); |
| 70 | |
| 71 | try { |
| 72 | $response = $sendgrid->send($email); |
| 73 | SendgridLogger::log($email, $response, $this->logLabel()); |
| 74 | |
| 75 | // SendGrid 4xx/5xx — let the queue retry per $tries / $backoff. |
| 76 | if ($response->statusCode() >= 400) { |
| 77 | throw new \RuntimeException( |
| 78 | 'SendGrid '.$response->statusCode().': '.$response->body() |
| 79 | ); |
| 80 | } |
| 81 | } catch (\Throwable $e) { |
| 82 | // Best-effort exception log to tbl_sendgrid_outbound_log so the |
| 83 | // retry/exhaustion trail is fully recoverable from the audit table. |
| 84 | SendgridLogger::logException($email, $e, $this->logLabel()); |
| 85 | throw $e; |
| 86 | } |
| 87 | } |
| 88 | |
| 89 | public function failed(\Throwable $e): void |
| 90 | { |
| 91 | Log::channel('email_log')->error(static::class.' exhausted retries', [ |
| 92 | 'job' => static::class, |
| 93 | 'label' => $this->logLabel(), |
| 94 | 'error' => $e->getMessage(), |
| 95 | ]); |
| 96 | } |
| 97 | } |