Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 21
0.00% covered (danger)
0.00%
0 / 5
CRAP
0.00% covered (danger)
0.00%
0 / 1
AbstractSendgridJob
0.00% covered (danger)
0.00%
0 / 21
0.00% covered (danger)
0.00%
0 / 5
42
0.00% covered (danger)
0.00%
0 / 1
 middleware
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 buildMail
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
0
 logLabel
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
0
 handle
0.00% covered (danger)
0.00%
0 / 13
0.00% covered (danger)
0.00%
0 / 1
20
 failed
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2
3namespace App\Jobs\Email;
4
5use App\Services\SendgridLogger;
6use Illuminate\Bus\Queueable;
7use Illuminate\Contracts\Queue\ShouldQueue;
8use Illuminate\Foundation\Bus\Dispatchable;
9use Illuminate\Queue\InteractsWithQueue;
10use Illuminate\Queue\Middleware\RateLimited;
11use Illuminate\Queue\SerializesModels;
12use Illuminate\Support\Facades\Log;
13use 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 */
31abstract 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}