refactor(drip): extract base mailable and job for the drip email family (#641)
## Why
The drip email family was near copy-paste. Each of the eight mailables
repeated the same constructor, `$tries`/`$backoff`, `drip_from`
envelope, `markdown` + `userName` content, and `RateLimited` middleware
— differing only in subject and template (plus a reply-to on one, an
extra view var on another). Each of the eight jobs repeated the
`canReceiveEmails` / `hasReceivedEmail` guards, the `Mail::send` call,
and the `UserMailLog::create` block — differing only in the email type,
the mailable, and a handful of per-email eligibility checks.
## What
Two abstract bases, so each subclass declares only what varies:
- **`DripMail`** — owns the shared mailable shape. Subclasses declare
`dripSubject()` and `template()`; optional `contentData()` (PromoCode's
`FOUNDER` var) and `repliesToSender()` (PaywallFollowUp) hooks. The
hooks are named `dripSubject()`/`template()` deliberately to avoid
colliding with `Illuminate\Mail\Mailable`'s existing public
`subject()`/`markdown()` methods.
- **`SendDripEmailJob`** — owns the shared `handle()` flow (shared
guards → send → `UserMailLog`). Subclasses declare `emailType()` and
`buildMail()`; an optional `shouldSend()` hook carries the per-email
eligibility rules (pro-plan, onboarding, transactions, bank connections,
AI consent).
Each mailable drops from ~68 to ~13 lines and each job from ~48 to ~18.
Net **−459 lines**, with the send/log logic and envelope construction
now in one place.
## Behavior
No functional change. Subjects, `from`/`reply-to`, queue name, rate
limiting, templates, view data, and every per-job eligibility guard are
preserved. Notably, passing `replyTo: []` for the seven non-reply mails
is identical to the previous omission — `Envelope`'s `replyTo` defaults
to `[]`. Verified: `pest --exclude-testsuite=Browser` **1872 passing, 0
failing**; `pint --test`, `phpstan` (level 5), `format`, `lint` clean.
## Review-driven follow-up commits
Two review agents (architecture/duplication/coverage, and
behavior/regressions) ran against the first commit. The behavior agent
verified all eight jobs' guard logic invert correctly (order +
negations) and that `replyTo: []` ≡ omitted — no regressions. The
coverage agent found the two hook-driven behaviors were untested;
applied, each as its own commit:
- Extended the drip-sender parity test to all eight mailables and
asserted `PaywallFollowUpEmail`'s reply-to (and that a default drip mail
sets none).
- Asserted `PromoCodeEmail` injects the `FOUNDER` promo code into its
view data.
## Deliberately out of scope
- Non-drip mailables (`Waitlist*`, `Banking*`, `UpdateEmail`, …) share
the same `RateLimited('emails')->releaseAfter(1)` middleware and could
later share a queued-mail base too — separate concern.
- **Pre-existing** (not introduced here, noted by review): the job sends
the mail before writing `UserMailLog`, so a failure between the two can
re-enqueue a duplicate; there is no lock/unique constraint on `(user_id,
email_type)`. Worth a separate hardening pass.
This commit is contained in:
parent
845f51abb5
commit
eccfaa5a7a
|
|
@ -4,45 +4,22 @@ namespace App\Jobs\Drip;
|
||||||
|
|
||||||
use App\Enums\DripEmailType;
|
use App\Enums\DripEmailType;
|
||||||
use App\Mail\Drip\AiConsentFollowUpEmail;
|
use App\Mail\Drip\AiConsentFollowUpEmail;
|
||||||
use App\Models\User;
|
use Illuminate\Mail\Mailable;
|
||||||
use App\Models\UserMailLog;
|
|
||||||
use Illuminate\Bus\Queueable;
|
|
||||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
|
||||||
use Illuminate\Foundation\Bus\Dispatchable;
|
|
||||||
use Illuminate\Queue\InteractsWithQueue;
|
|
||||||
use Illuminate\Queue\SerializesModels;
|
|
||||||
use Illuminate\Support\Facades\Mail;
|
|
||||||
|
|
||||||
class SendAiConsentFollowUpEmailJob implements ShouldQueue
|
class SendAiConsentFollowUpEmailJob extends SendDripEmailJob
|
||||||
{
|
{
|
||||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
protected function emailType(): DripEmailType
|
||||||
|
|
||||||
public function __construct(public User $user)
|
|
||||||
{
|
{
|
||||||
$this->onQueue('emails');
|
return DripEmailType::AiConsentFollowUp;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function handle(): void
|
protected function buildMail(): Mailable
|
||||||
{
|
{
|
||||||
if (! $this->user->canReceiveEmails()) {
|
return new AiConsentFollowUpEmail($this->user);
|
||||||
return;
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if ($this->user->hasReceivedEmail(DripEmailType::AiConsentFollowUp)) {
|
protected function shouldSend(): bool
|
||||||
return;
|
{
|
||||||
}
|
return $this->user->aiConsents()->active()->exists();
|
||||||
|
|
||||||
if (! $this->user->aiConsents()->active()->exists()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
Mail::to($this->user)->send(new AiConsentFollowUpEmail($this->user));
|
|
||||||
|
|
||||||
UserMailLog::create([
|
|
||||||
'user_id' => $this->user->id,
|
|
||||||
'email_type' => DripEmailType::AiConsentFollowUp,
|
|
||||||
'email_identifier' => DripEmailType::AiConsentFollowUp->value,
|
|
||||||
'sent_at' => now(),
|
|
||||||
]);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,61 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Jobs\Drip;
|
||||||
|
|
||||||
|
use App\Enums\DripEmailType;
|
||||||
|
use App\Models\User;
|
||||||
|
use App\Models\UserMailLog;
|
||||||
|
use Illuminate\Bus\Queueable;
|
||||||
|
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||||
|
use Illuminate\Foundation\Bus\Dispatchable;
|
||||||
|
use Illuminate\Mail\Mailable;
|
||||||
|
use Illuminate\Queue\InteractsWithQueue;
|
||||||
|
use Illuminate\Queue\SerializesModels;
|
||||||
|
use Illuminate\Support\Facades\Mail;
|
||||||
|
|
||||||
|
abstract class SendDripEmailJob implements ShouldQueue
|
||||||
|
{
|
||||||
|
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||||
|
|
||||||
|
public function __construct(public User $user)
|
||||||
|
{
|
||||||
|
$this->onQueue('emails');
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract protected function emailType(): DripEmailType;
|
||||||
|
|
||||||
|
abstract protected function buildMail(): Mailable;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Per-email eligibility checks beyond the shared "can receive" and
|
||||||
|
* "not already sent" guards.
|
||||||
|
*/
|
||||||
|
protected function shouldSend(): bool
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function handle(): void
|
||||||
|
{
|
||||||
|
if (! $this->user->canReceiveEmails()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($this->user->hasReceivedEmail($this->emailType())) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (! $this->shouldSend()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Mail::to($this->user)->send($this->buildMail());
|
||||||
|
|
||||||
|
UserMailLog::create([
|
||||||
|
'user_id' => $this->user->id,
|
||||||
|
'email_type' => $this->emailType(),
|
||||||
|
'email_identifier' => $this->emailType()->value,
|
||||||
|
'sent_at' => now(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -4,45 +4,22 @@ namespace App\Jobs\Drip;
|
||||||
|
|
||||||
use App\Enums\DripEmailType;
|
use App\Enums\DripEmailType;
|
||||||
use App\Mail\Drip\FeedbackEmail;
|
use App\Mail\Drip\FeedbackEmail;
|
||||||
use App\Models\User;
|
use Illuminate\Mail\Mailable;
|
||||||
use App\Models\UserMailLog;
|
|
||||||
use Illuminate\Bus\Queueable;
|
|
||||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
|
||||||
use Illuminate\Foundation\Bus\Dispatchable;
|
|
||||||
use Illuminate\Queue\InteractsWithQueue;
|
|
||||||
use Illuminate\Queue\SerializesModels;
|
|
||||||
use Illuminate\Support\Facades\Mail;
|
|
||||||
|
|
||||||
class SendFeedbackEmailJob implements ShouldQueue
|
class SendFeedbackEmailJob extends SendDripEmailJob
|
||||||
{
|
{
|
||||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
protected function emailType(): DripEmailType
|
||||||
|
|
||||||
public function __construct(public User $user)
|
|
||||||
{
|
{
|
||||||
$this->onQueue('emails');
|
return DripEmailType::Feedback;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function handle(): void
|
protected function buildMail(): Mailable
|
||||||
{
|
{
|
||||||
if (! $this->user->canReceiveEmails()) {
|
return new FeedbackEmail($this->user);
|
||||||
return;
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if ($this->user->hasReceivedEmail(DripEmailType::Feedback)) {
|
protected function shouldSend(): bool
|
||||||
return;
|
{
|
||||||
}
|
return ! $this->user->hasProPlan();
|
||||||
|
|
||||||
if ($this->user->hasProPlan()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
Mail::to($this->user)->send(new FeedbackEmail($this->user));
|
|
||||||
|
|
||||||
UserMailLog::create([
|
|
||||||
'user_id' => $this->user->id,
|
|
||||||
'email_type' => DripEmailType::Feedback,
|
|
||||||
'email_identifier' => DripEmailType::Feedback->value,
|
|
||||||
'sent_at' => now(),
|
|
||||||
]);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,53 +4,24 @@ namespace App\Jobs\Drip;
|
||||||
|
|
||||||
use App\Enums\DripEmailType;
|
use App\Enums\DripEmailType;
|
||||||
use App\Mail\Drip\ImportHelpEmail;
|
use App\Mail\Drip\ImportHelpEmail;
|
||||||
use App\Models\User;
|
use Illuminate\Mail\Mailable;
|
||||||
use App\Models\UserMailLog;
|
|
||||||
use Illuminate\Bus\Queueable;
|
|
||||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
|
||||||
use Illuminate\Foundation\Bus\Dispatchable;
|
|
||||||
use Illuminate\Queue\InteractsWithQueue;
|
|
||||||
use Illuminate\Queue\SerializesModels;
|
|
||||||
use Illuminate\Support\Facades\Mail;
|
|
||||||
|
|
||||||
class SendImportHelpEmailJob implements ShouldQueue
|
class SendImportHelpEmailJob extends SendDripEmailJob
|
||||||
{
|
{
|
||||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
protected function emailType(): DripEmailType
|
||||||
|
|
||||||
public function __construct(public User $user)
|
|
||||||
{
|
{
|
||||||
$this->onQueue('emails');
|
return DripEmailType::ImportHelp;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function handle(): void
|
protected function buildMail(): Mailable
|
||||||
{
|
{
|
||||||
if (! $this->user->canReceiveEmails()) {
|
return new ImportHelpEmail($this->user);
|
||||||
return;
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if ($this->user->hasReceivedEmail(DripEmailType::ImportHelp)) {
|
protected function shouldSend(): bool
|
||||||
return;
|
{
|
||||||
}
|
return $this->user->isOnboarded()
|
||||||
|
&& ! $this->user->transactions()->exists()
|
||||||
if (! $this->user->isOnboarded()) {
|
&& ! $this->user->hasProPlan();
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($this->user->transactions()->exists()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($this->user->hasProPlan()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
Mail::to($this->user)->send(new ImportHelpEmail($this->user));
|
|
||||||
|
|
||||||
UserMailLog::create([
|
|
||||||
'user_id' => $this->user->id,
|
|
||||||
'email_type' => DripEmailType::ImportHelp,
|
|
||||||
'email_identifier' => DripEmailType::ImportHelp->value,
|
|
||||||
'sent_at' => now(),
|
|
||||||
]);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,45 +4,22 @@ namespace App\Jobs\Drip;
|
||||||
|
|
||||||
use App\Enums\DripEmailType;
|
use App\Enums\DripEmailType;
|
||||||
use App\Mail\Drip\OnboardingReminderEmail;
|
use App\Mail\Drip\OnboardingReminderEmail;
|
||||||
use App\Models\User;
|
use Illuminate\Mail\Mailable;
|
||||||
use App\Models\UserMailLog;
|
|
||||||
use Illuminate\Bus\Queueable;
|
|
||||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
|
||||||
use Illuminate\Foundation\Bus\Dispatchable;
|
|
||||||
use Illuminate\Queue\InteractsWithQueue;
|
|
||||||
use Illuminate\Queue\SerializesModels;
|
|
||||||
use Illuminate\Support\Facades\Mail;
|
|
||||||
|
|
||||||
class SendOnboardingReminderEmailJob implements ShouldQueue
|
class SendOnboardingReminderEmailJob extends SendDripEmailJob
|
||||||
{
|
{
|
||||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
protected function emailType(): DripEmailType
|
||||||
|
|
||||||
public function __construct(public User $user)
|
|
||||||
{
|
{
|
||||||
$this->onQueue('emails');
|
return DripEmailType::OnboardingReminder;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function handle(): void
|
protected function buildMail(): Mailable
|
||||||
{
|
{
|
||||||
if (! $this->user->canReceiveEmails()) {
|
return new OnboardingReminderEmail($this->user);
|
||||||
return;
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if ($this->user->hasReceivedEmail(DripEmailType::OnboardingReminder)) {
|
protected function shouldSend(): bool
|
||||||
return;
|
{
|
||||||
}
|
return ! $this->user->isOnboarded();
|
||||||
|
|
||||||
if ($this->user->isOnboarded()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
Mail::to($this->user)->send(new OnboardingReminderEmail($this->user));
|
|
||||||
|
|
||||||
UserMailLog::create([
|
|
||||||
'user_id' => $this->user->id,
|
|
||||||
'email_type' => DripEmailType::OnboardingReminder,
|
|
||||||
'email_identifier' => DripEmailType::OnboardingReminder->value,
|
|
||||||
'sent_at' => now(),
|
|
||||||
]);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,49 +4,23 @@ namespace App\Jobs\Drip;
|
||||||
|
|
||||||
use App\Enums\DripEmailType;
|
use App\Enums\DripEmailType;
|
||||||
use App\Mail\Drip\PaywallFollowUpEmail;
|
use App\Mail\Drip\PaywallFollowUpEmail;
|
||||||
use App\Models\User;
|
use Illuminate\Mail\Mailable;
|
||||||
use App\Models\UserMailLog;
|
|
||||||
use Illuminate\Bus\Queueable;
|
|
||||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
|
||||||
use Illuminate\Foundation\Bus\Dispatchable;
|
|
||||||
use Illuminate\Queue\InteractsWithQueue;
|
|
||||||
use Illuminate\Queue\SerializesModels;
|
|
||||||
use Illuminate\Support\Facades\Mail;
|
|
||||||
|
|
||||||
class SendPaywallFollowUpEmailJob implements ShouldQueue
|
class SendPaywallFollowUpEmailJob extends SendDripEmailJob
|
||||||
{
|
{
|
||||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
protected function emailType(): DripEmailType
|
||||||
|
|
||||||
public function __construct(public User $user)
|
|
||||||
{
|
{
|
||||||
$this->onQueue('emails');
|
return DripEmailType::PaywallFollowUp;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function handle(): void
|
protected function buildMail(): Mailable
|
||||||
{
|
{
|
||||||
if (! $this->user->canReceiveEmails()) {
|
return new PaywallFollowUpEmail($this->user);
|
||||||
return;
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if ($this->user->hasReceivedEmail(DripEmailType::PaywallFollowUp)) {
|
protected function shouldSend(): bool
|
||||||
return;
|
{
|
||||||
}
|
return ! $this->user->hasProPlan()
|
||||||
|
&& $this->user->bankingConnections()->exists();
|
||||||
if ($this->user->hasProPlan()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (! $this->user->bankingConnections()->exists()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
Mail::to($this->user)->send(new PaywallFollowUpEmail($this->user));
|
|
||||||
|
|
||||||
UserMailLog::create([
|
|
||||||
'user_id' => $this->user->id,
|
|
||||||
'email_type' => DripEmailType::PaywallFollowUp,
|
|
||||||
'email_identifier' => DripEmailType::PaywallFollowUp->value,
|
|
||||||
'sent_at' => now(),
|
|
||||||
]);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,53 +4,24 @@ namespace App\Jobs\Drip;
|
||||||
|
|
||||||
use App\Enums\DripEmailType;
|
use App\Enums\DripEmailType;
|
||||||
use App\Mail\Drip\PromoCodeEmail;
|
use App\Mail\Drip\PromoCodeEmail;
|
||||||
use App\Models\User;
|
use Illuminate\Mail\Mailable;
|
||||||
use App\Models\UserMailLog;
|
|
||||||
use Illuminate\Bus\Queueable;
|
|
||||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
|
||||||
use Illuminate\Foundation\Bus\Dispatchable;
|
|
||||||
use Illuminate\Queue\InteractsWithQueue;
|
|
||||||
use Illuminate\Queue\SerializesModels;
|
|
||||||
use Illuminate\Support\Facades\Mail;
|
|
||||||
|
|
||||||
class SendPromoCodeEmailJob implements ShouldQueue
|
class SendPromoCodeEmailJob extends SendDripEmailJob
|
||||||
{
|
{
|
||||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
protected function emailType(): DripEmailType
|
||||||
|
|
||||||
public function __construct(public User $user)
|
|
||||||
{
|
{
|
||||||
$this->onQueue('emails');
|
return DripEmailType::PromoCode;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function handle(): void
|
protected function buildMail(): Mailable
|
||||||
{
|
{
|
||||||
if (! $this->user->canReceiveEmails()) {
|
return new PromoCodeEmail($this->user);
|
||||||
return;
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if ($this->user->hasReceivedEmail(DripEmailType::PromoCode)) {
|
protected function shouldSend(): bool
|
||||||
return;
|
{
|
||||||
}
|
return $this->user->isOnboarded()
|
||||||
|
&& $this->user->transactions()->exists()
|
||||||
if (! $this->user->isOnboarded()) {
|
&& ! $this->user->hasProPlan();
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (! $this->user->transactions()->exists()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($this->user->hasProPlan()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
Mail::to($this->user)->send(new PromoCodeEmail($this->user));
|
|
||||||
|
|
||||||
UserMailLog::create([
|
|
||||||
'user_id' => $this->user->id,
|
|
||||||
'email_type' => DripEmailType::PromoCode,
|
|
||||||
'email_identifier' => DripEmailType::PromoCode->value,
|
|
||||||
'sent_at' => now(),
|
|
||||||
]);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,41 +4,17 @@ namespace App\Jobs\Drip;
|
||||||
|
|
||||||
use App\Enums\DripEmailType;
|
use App\Enums\DripEmailType;
|
||||||
use App\Mail\Drip\SubscriptionCancelledEmail;
|
use App\Mail\Drip\SubscriptionCancelledEmail;
|
||||||
use App\Models\User;
|
use Illuminate\Mail\Mailable;
|
||||||
use App\Models\UserMailLog;
|
|
||||||
use Illuminate\Bus\Queueable;
|
|
||||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
|
||||||
use Illuminate\Foundation\Bus\Dispatchable;
|
|
||||||
use Illuminate\Queue\InteractsWithQueue;
|
|
||||||
use Illuminate\Queue\SerializesModels;
|
|
||||||
use Illuminate\Support\Facades\Mail;
|
|
||||||
|
|
||||||
class SendSubscriptionCancelledEmailJob implements ShouldQueue
|
class SendSubscriptionCancelledEmailJob extends SendDripEmailJob
|
||||||
{
|
{
|
||||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
protected function emailType(): DripEmailType
|
||||||
|
|
||||||
public function __construct(public User $user)
|
|
||||||
{
|
{
|
||||||
$this->onQueue('emails');
|
return DripEmailType::SubscriptionCancelled;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function handle(): void
|
protected function buildMail(): Mailable
|
||||||
{
|
{
|
||||||
if (! $this->user->canReceiveEmails()) {
|
return new SubscriptionCancelledEmail($this->user);
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($this->user->hasReceivedEmail(DripEmailType::SubscriptionCancelled)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
Mail::to($this->user)->send(new SubscriptionCancelledEmail($this->user));
|
|
||||||
|
|
||||||
UserMailLog::create([
|
|
||||||
'user_id' => $this->user->id,
|
|
||||||
'email_type' => DripEmailType::SubscriptionCancelled,
|
|
||||||
'email_identifier' => DripEmailType::SubscriptionCancelled->value,
|
|
||||||
'sent_at' => now(),
|
|
||||||
]);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,41 +4,17 @@ namespace App\Jobs\Drip;
|
||||||
|
|
||||||
use App\Enums\DripEmailType;
|
use App\Enums\DripEmailType;
|
||||||
use App\Mail\Drip\WelcomeEmail;
|
use App\Mail\Drip\WelcomeEmail;
|
||||||
use App\Models\User;
|
use Illuminate\Mail\Mailable;
|
||||||
use App\Models\UserMailLog;
|
|
||||||
use Illuminate\Bus\Queueable;
|
|
||||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
|
||||||
use Illuminate\Foundation\Bus\Dispatchable;
|
|
||||||
use Illuminate\Queue\InteractsWithQueue;
|
|
||||||
use Illuminate\Queue\SerializesModels;
|
|
||||||
use Illuminate\Support\Facades\Mail;
|
|
||||||
|
|
||||||
class SendWelcomeEmailJob implements ShouldQueue
|
class SendWelcomeEmailJob extends SendDripEmailJob
|
||||||
{
|
{
|
||||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
protected function emailType(): DripEmailType
|
||||||
|
|
||||||
public function __construct(public User $user)
|
|
||||||
{
|
{
|
||||||
$this->onQueue('emails');
|
return DripEmailType::Welcome;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function handle(): void
|
protected function buildMail(): Mailable
|
||||||
{
|
{
|
||||||
if (! $this->user->canReceiveEmails()) {
|
return new WelcomeEmail($this->user);
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($this->user->hasReceivedEmail(DripEmailType::Welcome)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
Mail::to($this->user)->send(new WelcomeEmail($this->user));
|
|
||||||
|
|
||||||
UserMailLog::create([
|
|
||||||
'user_id' => $this->user->id,
|
|
||||||
'email_type' => DripEmailType::Welcome,
|
|
||||||
'email_identifier' => DripEmailType::Welcome->value,
|
|
||||||
'sent_at' => now(),
|
|
||||||
]);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,67 +2,15 @@
|
||||||
|
|
||||||
namespace App\Mail\Drip;
|
namespace App\Mail\Drip;
|
||||||
|
|
||||||
use App\Models\User;
|
class AiConsentFollowUpEmail extends DripMail
|
||||||
use Illuminate\Bus\Queueable;
|
|
||||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
|
||||||
use Illuminate\Mail\Mailable;
|
|
||||||
use Illuminate\Mail\Mailables\Address;
|
|
||||||
use Illuminate\Mail\Mailables\Content;
|
|
||||||
use Illuminate\Mail\Mailables\Envelope;
|
|
||||||
use Illuminate\Queue\Middleware\RateLimited;
|
|
||||||
use Illuminate\Queue\SerializesModels;
|
|
||||||
|
|
||||||
class AiConsentFollowUpEmail extends Mailable implements ShouldQueue
|
|
||||||
{
|
{
|
||||||
use Queueable, SerializesModels;
|
protected function dripSubject(): string
|
||||||
|
|
||||||
/**
|
|
||||||
* The number of times the job may be attempted.
|
|
||||||
*
|
|
||||||
* @var int
|
|
||||||
*/
|
|
||||||
public $tries = 5;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The number of seconds to wait before retrying the job.
|
|
||||||
*
|
|
||||||
* @var array<int, int>
|
|
||||||
*/
|
|
||||||
public $backoff = [2, 5, 10, 30];
|
|
||||||
|
|
||||||
public function __construct(public User $user)
|
|
||||||
{
|
{
|
||||||
$this->onQueue('emails');
|
return __('Putting AI to work on your finances');
|
||||||
}
|
}
|
||||||
|
|
||||||
public function envelope(): Envelope
|
protected function template(): string
|
||||||
{
|
{
|
||||||
return new Envelope(
|
return 'mail.drip.ai-consent-follow-up';
|
||||||
from: new Address(
|
|
||||||
config('mail.drip_from.address', 'hi@whisper.money'),
|
|
||||||
config('mail.drip_from.name', 'Álvaro and Víctor'),
|
|
||||||
),
|
|
||||||
subject: __('Putting AI to work on your finances'),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function content(): Content
|
|
||||||
{
|
|
||||||
return new Content(
|
|
||||||
markdown: 'mail.drip.ai-consent-follow-up',
|
|
||||||
with: [
|
|
||||||
'userName' => $this->user->name,
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the middleware the job should pass through.
|
|
||||||
*
|
|
||||||
* @return array<int, object>
|
|
||||||
*/
|
|
||||||
public function middleware(): array
|
|
||||||
{
|
|
||||||
return [(new RateLimited('emails'))->releaseAfter(1)];
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,94 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Mail\Drip;
|
||||||
|
|
||||||
|
use App\Models\User;
|
||||||
|
use Illuminate\Bus\Queueable;
|
||||||
|
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||||
|
use Illuminate\Mail\Mailable;
|
||||||
|
use Illuminate\Mail\Mailables\Address;
|
||||||
|
use Illuminate\Mail\Mailables\Content;
|
||||||
|
use Illuminate\Mail\Mailables\Envelope;
|
||||||
|
use Illuminate\Queue\Middleware\RateLimited;
|
||||||
|
use Illuminate\Queue\SerializesModels;
|
||||||
|
|
||||||
|
abstract class DripMail extends Mailable implements ShouldQueue
|
||||||
|
{
|
||||||
|
use Queueable, SerializesModels;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The number of times the job may be attempted.
|
||||||
|
*
|
||||||
|
* @var int
|
||||||
|
*/
|
||||||
|
public $tries = 5;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The number of seconds to wait before retrying the job.
|
||||||
|
*
|
||||||
|
* @var array<int, int>
|
||||||
|
*/
|
||||||
|
public $backoff = [2, 5, 10, 30];
|
||||||
|
|
||||||
|
public function __construct(public User $user)
|
||||||
|
{
|
||||||
|
$this->onQueue('emails');
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract protected function dripSubject(): string;
|
||||||
|
|
||||||
|
abstract protected function template(): string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extra view data merged alongside the recipient's name.
|
||||||
|
*
|
||||||
|
* @return array<string, mixed>
|
||||||
|
*/
|
||||||
|
protected function contentData(): array
|
||||||
|
{
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether replies should route back to the drip sender address.
|
||||||
|
*/
|
||||||
|
protected function repliesToSender(): bool
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function envelope(): Envelope
|
||||||
|
{
|
||||||
|
$from = new Address(
|
||||||
|
config('mail.drip_from.address', 'hi@whisper.money'),
|
||||||
|
config('mail.drip_from.name', 'Álvaro and Víctor'),
|
||||||
|
);
|
||||||
|
|
||||||
|
return new Envelope(
|
||||||
|
from: $from,
|
||||||
|
replyTo: $this->repliesToSender() ? [$from] : [],
|
||||||
|
subject: $this->dripSubject(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function content(): Content
|
||||||
|
{
|
||||||
|
return new Content(
|
||||||
|
markdown: $this->template(),
|
||||||
|
with: [
|
||||||
|
'userName' => $this->user->name,
|
||||||
|
...$this->contentData(),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the middleware the job should pass through.
|
||||||
|
*
|
||||||
|
* @return array<int, object>
|
||||||
|
*/
|
||||||
|
public function middleware(): array
|
||||||
|
{
|
||||||
|
return [(new RateLimited('emails'))->releaseAfter(1)];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -2,67 +2,15 @@
|
||||||
|
|
||||||
namespace App\Mail\Drip;
|
namespace App\Mail\Drip;
|
||||||
|
|
||||||
use App\Models\User;
|
class FeedbackEmail extends DripMail
|
||||||
use Illuminate\Bus\Queueable;
|
|
||||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
|
||||||
use Illuminate\Mail\Mailable;
|
|
||||||
use Illuminate\Mail\Mailables\Address;
|
|
||||||
use Illuminate\Mail\Mailables\Content;
|
|
||||||
use Illuminate\Mail\Mailables\Envelope;
|
|
||||||
use Illuminate\Queue\Middleware\RateLimited;
|
|
||||||
use Illuminate\Queue\SerializesModels;
|
|
||||||
|
|
||||||
class FeedbackEmail extends Mailable implements ShouldQueue
|
|
||||||
{
|
{
|
||||||
use Queueable, SerializesModels;
|
protected function dripSubject(): string
|
||||||
|
|
||||||
/**
|
|
||||||
* The number of times the job may be attempted.
|
|
||||||
*
|
|
||||||
* @var int
|
|
||||||
*/
|
|
||||||
public $tries = 5;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The number of seconds to wait before retrying the job.
|
|
||||||
*
|
|
||||||
* @var array<int, int>
|
|
||||||
*/
|
|
||||||
public $backoff = [2, 5, 10, 30];
|
|
||||||
|
|
||||||
public function __construct(public User $user)
|
|
||||||
{
|
{
|
||||||
$this->onQueue('emails');
|
return __("How's Your Experience So Far?");
|
||||||
}
|
}
|
||||||
|
|
||||||
public function envelope(): Envelope
|
protected function template(): string
|
||||||
{
|
{
|
||||||
return new Envelope(
|
return 'mail.drip.feedback';
|
||||||
from: new Address(
|
|
||||||
config('mail.drip_from.address', 'hi@whisper.money'),
|
|
||||||
config('mail.drip_from.name', 'Álvaro and Víctor'),
|
|
||||||
),
|
|
||||||
subject: __("How's Your Experience So Far?"),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function content(): Content
|
|
||||||
{
|
|
||||||
return new Content(
|
|
||||||
markdown: 'mail.drip.feedback',
|
|
||||||
with: [
|
|
||||||
'userName' => $this->user->name,
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the middleware the job should pass through.
|
|
||||||
*
|
|
||||||
* @return array<int, object>
|
|
||||||
*/
|
|
||||||
public function middleware(): array
|
|
||||||
{
|
|
||||||
return [(new RateLimited('emails'))->releaseAfter(1)];
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,67 +2,15 @@
|
||||||
|
|
||||||
namespace App\Mail\Drip;
|
namespace App\Mail\Drip;
|
||||||
|
|
||||||
use App\Models\User;
|
class ImportHelpEmail extends DripMail
|
||||||
use Illuminate\Bus\Queueable;
|
|
||||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
|
||||||
use Illuminate\Mail\Mailable;
|
|
||||||
use Illuminate\Mail\Mailables\Address;
|
|
||||||
use Illuminate\Mail\Mailables\Content;
|
|
||||||
use Illuminate\Mail\Mailables\Envelope;
|
|
||||||
use Illuminate\Queue\Middleware\RateLimited;
|
|
||||||
use Illuminate\Queue\SerializesModels;
|
|
||||||
|
|
||||||
class ImportHelpEmail extends Mailable implements ShouldQueue
|
|
||||||
{
|
{
|
||||||
use Queueable, SerializesModels;
|
protected function dripSubject(): string
|
||||||
|
|
||||||
/**
|
|
||||||
* The number of times the job may be attempted.
|
|
||||||
*
|
|
||||||
* @var int
|
|
||||||
*/
|
|
||||||
public $tries = 5;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The number of seconds to wait before retrying the job.
|
|
||||||
*
|
|
||||||
* @var array<int, int>
|
|
||||||
*/
|
|
||||||
public $backoff = [2, 5, 10, 30];
|
|
||||||
|
|
||||||
public function __construct(public User $user)
|
|
||||||
{
|
{
|
||||||
$this->onQueue('emails');
|
return __("Let's Import Your Transactions");
|
||||||
}
|
}
|
||||||
|
|
||||||
public function envelope(): Envelope
|
protected function template(): string
|
||||||
{
|
{
|
||||||
return new Envelope(
|
return 'mail.drip.import-help';
|
||||||
from: new Address(
|
|
||||||
config('mail.drip_from.address', 'hi@whisper.money'),
|
|
||||||
config('mail.drip_from.name', 'Álvaro and Víctor'),
|
|
||||||
),
|
|
||||||
subject: __("Let's Import Your Transactions"),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function content(): Content
|
|
||||||
{
|
|
||||||
return new Content(
|
|
||||||
markdown: 'mail.drip.import-help',
|
|
||||||
with: [
|
|
||||||
'userName' => $this->user->name,
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the middleware the job should pass through.
|
|
||||||
*
|
|
||||||
* @return array<int, object>
|
|
||||||
*/
|
|
||||||
public function middleware(): array
|
|
||||||
{
|
|
||||||
return [(new RateLimited('emails'))->releaseAfter(1)];
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,67 +2,15 @@
|
||||||
|
|
||||||
namespace App\Mail\Drip;
|
namespace App\Mail\Drip;
|
||||||
|
|
||||||
use App\Models\User;
|
class OnboardingReminderEmail extends DripMail
|
||||||
use Illuminate\Bus\Queueable;
|
|
||||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
|
||||||
use Illuminate\Mail\Mailable;
|
|
||||||
use Illuminate\Mail\Mailables\Address;
|
|
||||||
use Illuminate\Mail\Mailables\Content;
|
|
||||||
use Illuminate\Mail\Mailables\Envelope;
|
|
||||||
use Illuminate\Queue\Middleware\RateLimited;
|
|
||||||
use Illuminate\Queue\SerializesModels;
|
|
||||||
|
|
||||||
class OnboardingReminderEmail extends Mailable implements ShouldQueue
|
|
||||||
{
|
{
|
||||||
use Queueable, SerializesModels;
|
protected function dripSubject(): string
|
||||||
|
|
||||||
/**
|
|
||||||
* The number of times the job may be attempted.
|
|
||||||
*
|
|
||||||
* @var int
|
|
||||||
*/
|
|
||||||
public $tries = 5;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The number of seconds to wait before retrying the job.
|
|
||||||
*
|
|
||||||
* @var array<int, int>
|
|
||||||
*/
|
|
||||||
public $backoff = [2, 5, 10, 30];
|
|
||||||
|
|
||||||
public function __construct(public User $user)
|
|
||||||
{
|
{
|
||||||
$this->onQueue('emails');
|
return __('Need Help Getting Started?');
|
||||||
}
|
}
|
||||||
|
|
||||||
public function envelope(): Envelope
|
protected function template(): string
|
||||||
{
|
{
|
||||||
return new Envelope(
|
return 'mail.drip.onboarding-reminder';
|
||||||
from: new Address(
|
|
||||||
config('mail.drip_from.address', 'hi@whisper.money'),
|
|
||||||
config('mail.drip_from.name', 'Álvaro and Víctor'),
|
|
||||||
),
|
|
||||||
subject: __('Need Help Getting Started?'),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function content(): Content
|
|
||||||
{
|
|
||||||
return new Content(
|
|
||||||
markdown: 'mail.drip.onboarding-reminder',
|
|
||||||
with: [
|
|
||||||
'userName' => $this->user->name,
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the middleware the job should pass through.
|
|
||||||
*
|
|
||||||
* @return array<int, object>
|
|
||||||
*/
|
|
||||||
public function middleware(): array
|
|
||||||
{
|
|
||||||
return [(new RateLimited('emails'))->releaseAfter(1)];
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,73 +2,20 @@
|
||||||
|
|
||||||
namespace App\Mail\Drip;
|
namespace App\Mail\Drip;
|
||||||
|
|
||||||
use App\Models\User;
|
class PaywallFollowUpEmail extends DripMail
|
||||||
use Illuminate\Bus\Queueable;
|
|
||||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
|
||||||
use Illuminate\Mail\Mailable;
|
|
||||||
use Illuminate\Mail\Mailables\Address;
|
|
||||||
use Illuminate\Mail\Mailables\Content;
|
|
||||||
use Illuminate\Mail\Mailables\Envelope;
|
|
||||||
use Illuminate\Queue\Middleware\RateLimited;
|
|
||||||
use Illuminate\Queue\SerializesModels;
|
|
||||||
|
|
||||||
class PaywallFollowUpEmail extends Mailable implements ShouldQueue
|
|
||||||
{
|
{
|
||||||
use Queueable, SerializesModels;
|
protected function dripSubject(): string
|
||||||
|
|
||||||
/**
|
|
||||||
* The number of times the job may be attempted.
|
|
||||||
*
|
|
||||||
* @var int
|
|
||||||
*/
|
|
||||||
public $tries = 5;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The number of seconds to wait before retrying the job.
|
|
||||||
*
|
|
||||||
* @var array<int, int>
|
|
||||||
*/
|
|
||||||
public $backoff = [2, 5, 10, 30];
|
|
||||||
|
|
||||||
public function __construct(public User $user)
|
|
||||||
{
|
{
|
||||||
$this->onQueue('emails');
|
return __('What stopped you from getting started?');
|
||||||
}
|
}
|
||||||
|
|
||||||
public function envelope(): Envelope
|
protected function template(): string
|
||||||
{
|
{
|
||||||
return new Envelope(
|
return 'mail.drip.paywall-follow-up';
|
||||||
from: new Address(
|
|
||||||
config('mail.drip_from.address', 'hi@whisper.money'),
|
|
||||||
config('mail.drip_from.name', 'Álvaro and Víctor'),
|
|
||||||
),
|
|
||||||
replyTo: [
|
|
||||||
new Address(
|
|
||||||
config('mail.drip_from.address', 'hi@whisper.money'),
|
|
||||||
config('mail.drip_from.name', 'Álvaro and Víctor'),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
subject: __('What stopped you from getting started?'),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function content(): Content
|
protected function repliesToSender(): bool
|
||||||
{
|
{
|
||||||
return new Content(
|
return true;
|
||||||
markdown: 'mail.drip.paywall-follow-up',
|
|
||||||
with: [
|
|
||||||
'userName' => $this->user->name,
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the middleware the job should pass through.
|
|
||||||
*
|
|
||||||
* @return array<int, object>
|
|
||||||
*/
|
|
||||||
public function middleware(): array
|
|
||||||
{
|
|
||||||
return [(new RateLimited('emails'))->releaseAfter(1)];
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,68 +2,20 @@
|
||||||
|
|
||||||
namespace App\Mail\Drip;
|
namespace App\Mail\Drip;
|
||||||
|
|
||||||
use App\Models\User;
|
class PromoCodeEmail extends DripMail
|
||||||
use Illuminate\Bus\Queueable;
|
|
||||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
|
||||||
use Illuminate\Mail\Mailable;
|
|
||||||
use Illuminate\Mail\Mailables\Address;
|
|
||||||
use Illuminate\Mail\Mailables\Content;
|
|
||||||
use Illuminate\Mail\Mailables\Envelope;
|
|
||||||
use Illuminate\Queue\Middleware\RateLimited;
|
|
||||||
use Illuminate\Queue\SerializesModels;
|
|
||||||
|
|
||||||
class PromoCodeEmail extends Mailable implements ShouldQueue
|
|
||||||
{
|
{
|
||||||
use Queueable, SerializesModels;
|
protected function dripSubject(): string
|
||||||
|
|
||||||
/**
|
|
||||||
* The number of times the job may be attempted.
|
|
||||||
*
|
|
||||||
* @var int
|
|
||||||
*/
|
|
||||||
public $tries = 5;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The number of seconds to wait before retrying the job.
|
|
||||||
*
|
|
||||||
* @var array<int, int>
|
|
||||||
*/
|
|
||||||
public $backoff = [2, 5, 10, 30];
|
|
||||||
|
|
||||||
public function __construct(public User $user)
|
|
||||||
{
|
{
|
||||||
$this->onQueue('emails');
|
return __('Your Founder Discount - 80% Off First Period');
|
||||||
}
|
}
|
||||||
|
|
||||||
public function envelope(): Envelope
|
protected function template(): string
|
||||||
{
|
{
|
||||||
return new Envelope(
|
return 'mail.drip.promo-code';
|
||||||
from: new Address(
|
|
||||||
config('mail.drip_from.address', 'hi@whisper.money'),
|
|
||||||
config('mail.drip_from.name', 'Álvaro and Víctor'),
|
|
||||||
),
|
|
||||||
subject: __('Your Founder Discount - 80% Off First Period')
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function content(): Content
|
protected function contentData(): array
|
||||||
{
|
{
|
||||||
return new Content(
|
return ['promoCode' => 'FOUNDER'];
|
||||||
markdown: 'mail.drip.promo-code',
|
|
||||||
with: [
|
|
||||||
'userName' => $this->user->name,
|
|
||||||
'promoCode' => 'FOUNDER',
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the middleware the job should pass through.
|
|
||||||
*
|
|
||||||
* @return array<int, object>
|
|
||||||
*/
|
|
||||||
public function middleware(): array
|
|
||||||
{
|
|
||||||
return [(new RateLimited('emails'))->releaseAfter(1)];
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,67 +2,15 @@
|
||||||
|
|
||||||
namespace App\Mail\Drip;
|
namespace App\Mail\Drip;
|
||||||
|
|
||||||
use App\Models\User;
|
class SubscriptionCancelledEmail extends DripMail
|
||||||
use Illuminate\Bus\Queueable;
|
|
||||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
|
||||||
use Illuminate\Mail\Mailable;
|
|
||||||
use Illuminate\Mail\Mailables\Address;
|
|
||||||
use Illuminate\Mail\Mailables\Content;
|
|
||||||
use Illuminate\Mail\Mailables\Envelope;
|
|
||||||
use Illuminate\Queue\Middleware\RateLimited;
|
|
||||||
use Illuminate\Queue\SerializesModels;
|
|
||||||
|
|
||||||
class SubscriptionCancelledEmail extends Mailable implements ShouldQueue
|
|
||||||
{
|
{
|
||||||
use Queueable, SerializesModels;
|
protected function dripSubject(): string
|
||||||
|
|
||||||
/**
|
|
||||||
* The number of times the job may be attempted.
|
|
||||||
*
|
|
||||||
* @var int
|
|
||||||
*/
|
|
||||||
public $tries = 5;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The number of seconds to wait before retrying the job.
|
|
||||||
*
|
|
||||||
* @var array<int, int>
|
|
||||||
*/
|
|
||||||
public $backoff = [2, 5, 10, 30];
|
|
||||||
|
|
||||||
public function __construct(public User $user)
|
|
||||||
{
|
{
|
||||||
$this->onQueue('emails');
|
return __("We're sorry to see you go");
|
||||||
}
|
}
|
||||||
|
|
||||||
public function envelope(): Envelope
|
protected function template(): string
|
||||||
{
|
{
|
||||||
return new Envelope(
|
return 'mail.drip.subscription-cancelled';
|
||||||
from: new Address(
|
|
||||||
config('mail.drip_from.address', 'hi@whisper.money'),
|
|
||||||
config('mail.drip_from.name', 'Álvaro and Víctor'),
|
|
||||||
),
|
|
||||||
subject: __("We're sorry to see you go"),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function content(): Content
|
|
||||||
{
|
|
||||||
return new Content(
|
|
||||||
markdown: 'mail.drip.subscription-cancelled',
|
|
||||||
with: [
|
|
||||||
'userName' => $this->user->name,
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the middleware the job should pass through.
|
|
||||||
*
|
|
||||||
* @return array<int, object>
|
|
||||||
*/
|
|
||||||
public function middleware(): array
|
|
||||||
{
|
|
||||||
return [(new RateLimited('emails'))->releaseAfter(1)];
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,67 +2,15 @@
|
||||||
|
|
||||||
namespace App\Mail\Drip;
|
namespace App\Mail\Drip;
|
||||||
|
|
||||||
use App\Models\User;
|
class WelcomeEmail extends DripMail
|
||||||
use Illuminate\Bus\Queueable;
|
|
||||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
|
||||||
use Illuminate\Mail\Mailable;
|
|
||||||
use Illuminate\Mail\Mailables\Address;
|
|
||||||
use Illuminate\Mail\Mailables\Content;
|
|
||||||
use Illuminate\Mail\Mailables\Envelope;
|
|
||||||
use Illuminate\Queue\Middleware\RateLimited;
|
|
||||||
use Illuminate\Queue\SerializesModels;
|
|
||||||
|
|
||||||
class WelcomeEmail extends Mailable implements ShouldQueue
|
|
||||||
{
|
{
|
||||||
use Queueable, SerializesModels;
|
protected function dripSubject(): string
|
||||||
|
|
||||||
/**
|
|
||||||
* The number of times the job may be attempted.
|
|
||||||
*
|
|
||||||
* @var int
|
|
||||||
*/
|
|
||||||
public $tries = 5;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The number of seconds to wait before retrying the job.
|
|
||||||
*
|
|
||||||
* @var array<int, int>
|
|
||||||
*/
|
|
||||||
public $backoff = [2, 5, 10, 30];
|
|
||||||
|
|
||||||
public function __construct(public User $user)
|
|
||||||
{
|
{
|
||||||
$this->onQueue('emails');
|
return __('Welcome to Whisper Money - Your Privacy-First Finance App');
|
||||||
}
|
}
|
||||||
|
|
||||||
public function envelope(): Envelope
|
protected function template(): string
|
||||||
{
|
{
|
||||||
return new Envelope(
|
return 'mail.drip.welcome';
|
||||||
from: new Address(
|
|
||||||
config('mail.drip_from.address', 'hi@whisper.money'),
|
|
||||||
config('mail.drip_from.name', 'Álvaro and Víctor'),
|
|
||||||
),
|
|
||||||
subject: __('Welcome to Whisper Money - Your Privacy-First Finance App'),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function content(): Content
|
|
||||||
{
|
|
||||||
return new Content(
|
|
||||||
markdown: 'mail.drip.welcome',
|
|
||||||
with: [
|
|
||||||
'userName' => $this->user->name,
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the middleware the job should pass through.
|
|
||||||
*
|
|
||||||
* @return array<int, object>
|
|
||||||
*/
|
|
||||||
public function middleware(): array
|
|
||||||
{
|
|
||||||
return [(new RateLimited('emails'))->releaseAfter(1)];
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,9 +3,11 @@
|
||||||
use App\Mail\BankingConnectionAuthFailedEmail;
|
use App\Mail\BankingConnectionAuthFailedEmail;
|
||||||
use App\Mail\BankTransactionsSyncedEmail;
|
use App\Mail\BankTransactionsSyncedEmail;
|
||||||
use App\Mail\BrokenBankLogosReportEmail;
|
use App\Mail\BrokenBankLogosReportEmail;
|
||||||
|
use App\Mail\Drip\AiConsentFollowUpEmail;
|
||||||
use App\Mail\Drip\FeedbackEmail;
|
use App\Mail\Drip\FeedbackEmail;
|
||||||
use App\Mail\Drip\ImportHelpEmail;
|
use App\Mail\Drip\ImportHelpEmail;
|
||||||
use App\Mail\Drip\OnboardingReminderEmail;
|
use App\Mail\Drip\OnboardingReminderEmail;
|
||||||
|
use App\Mail\Drip\PaywallFollowUpEmail;
|
||||||
use App\Mail\Drip\PromoCodeEmail;
|
use App\Mail\Drip\PromoCodeEmail;
|
||||||
use App\Mail\Drip\SubscriptionCancelledEmail;
|
use App\Mail\Drip\SubscriptionCancelledEmail;
|
||||||
use App\Mail\Drip\WelcomeEmail;
|
use App\Mail\Drip\WelcomeEmail;
|
||||||
|
|
@ -84,24 +86,44 @@ test('drip mailables use the drip sender', function (string $mailableClass) {
|
||||||
$user = User::factory()->create();
|
$user = User::factory()->create();
|
||||||
|
|
||||||
$mailable = match ($mailableClass) {
|
$mailable = match ($mailableClass) {
|
||||||
|
AiConsentFollowUpEmail::class => new AiConsentFollowUpEmail($user),
|
||||||
WelcomeEmail::class => new WelcomeEmail($user),
|
WelcomeEmail::class => new WelcomeEmail($user),
|
||||||
FeedbackEmail::class => new FeedbackEmail($user),
|
FeedbackEmail::class => new FeedbackEmail($user),
|
||||||
ImportHelpEmail::class => new ImportHelpEmail($user),
|
ImportHelpEmail::class => new ImportHelpEmail($user),
|
||||||
OnboardingReminderEmail::class => new OnboardingReminderEmail($user),
|
OnboardingReminderEmail::class => new OnboardingReminderEmail($user),
|
||||||
|
PaywallFollowUpEmail::class => new PaywallFollowUpEmail($user),
|
||||||
PromoCodeEmail::class => new PromoCodeEmail($user),
|
PromoCodeEmail::class => new PromoCodeEmail($user),
|
||||||
SubscriptionCancelledEmail::class => new SubscriptionCancelledEmail($user),
|
SubscriptionCancelledEmail::class => new SubscriptionCancelledEmail($user),
|
||||||
};
|
};
|
||||||
|
|
||||||
expect($mailable->envelope()->from)->toEqual(new Address('hi@whisper.money', 'Álvaro and Víctor'));
|
expect($mailable->envelope()->from)->toEqual(new Address('hi@whisper.money', 'Álvaro and Víctor'));
|
||||||
})->with([
|
})->with([
|
||||||
|
AiConsentFollowUpEmail::class,
|
||||||
WelcomeEmail::class,
|
WelcomeEmail::class,
|
||||||
FeedbackEmail::class,
|
FeedbackEmail::class,
|
||||||
ImportHelpEmail::class,
|
ImportHelpEmail::class,
|
||||||
OnboardingReminderEmail::class,
|
OnboardingReminderEmail::class,
|
||||||
|
PaywallFollowUpEmail::class,
|
||||||
PromoCodeEmail::class,
|
PromoCodeEmail::class,
|
||||||
SubscriptionCancelledEmail::class,
|
SubscriptionCancelledEmail::class,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
test('only the paywall follow-up sets a reply-to, back to the drip sender', function () {
|
||||||
|
$user = User::factory()->create();
|
||||||
|
|
||||||
|
expect((new PaywallFollowUpEmail($user))->envelope()->replyTo)
|
||||||
|
->toEqual([new Address('hi@whisper.money', 'Álvaro and Víctor')]);
|
||||||
|
|
||||||
|
expect((new WelcomeEmail($user))->envelope()->replyTo)->toBe([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('promo code email adds the founder promo code to its view data', function () {
|
||||||
|
$user = User::factory()->create();
|
||||||
|
|
||||||
|
expect((new PromoCodeEmail($user))->content()->with)
|
||||||
|
->toEqual(['userName' => $user->name, 'promoCode' => 'FOUNDER']);
|
||||||
|
});
|
||||||
|
|
||||||
test('default sender is used for active non-drip mailables', function (string $mailableClass) {
|
test('default sender is used for active non-drip mailables', function (string $mailableClass) {
|
||||||
$user = User::factory()->create();
|
$user = User::factory()->create();
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue