diff --git a/app/Jobs/Drip/SendAiConsentFollowUpEmailJob.php b/app/Jobs/Drip/SendAiConsentFollowUpEmailJob.php index 482c0332..3694221c 100644 --- a/app/Jobs/Drip/SendAiConsentFollowUpEmailJob.php +++ b/app/Jobs/Drip/SendAiConsentFollowUpEmailJob.php @@ -4,45 +4,22 @@ namespace App\Jobs\Drip; use App\Enums\DripEmailType; use App\Mail\Drip\AiConsentFollowUpEmail; -use App\Models\User; -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; +use Illuminate\Mail\Mailable; -class SendAiConsentFollowUpEmailJob implements ShouldQueue +class SendAiConsentFollowUpEmailJob extends SendDripEmailJob { - use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; - - public function __construct(public User $user) + protected function emailType(): DripEmailType { - $this->onQueue('emails'); + return DripEmailType::AiConsentFollowUp; } - public function handle(): void + protected function buildMail(): Mailable { - if (! $this->user->canReceiveEmails()) { - return; - } + return new AiConsentFollowUpEmail($this->user); + } - if ($this->user->hasReceivedEmail(DripEmailType::AiConsentFollowUp)) { - return; - } - - 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(), - ]); + protected function shouldSend(): bool + { + return $this->user->aiConsents()->active()->exists(); } } diff --git a/app/Jobs/Drip/SendDripEmailJob.php b/app/Jobs/Drip/SendDripEmailJob.php new file mode 100644 index 00000000..4d4af1c3 --- /dev/null +++ b/app/Jobs/Drip/SendDripEmailJob.php @@ -0,0 +1,61 @@ +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(), + ]); + } +} diff --git a/app/Jobs/Drip/SendFeedbackEmailJob.php b/app/Jobs/Drip/SendFeedbackEmailJob.php index 98baa741..ff520f7d 100644 --- a/app/Jobs/Drip/SendFeedbackEmailJob.php +++ b/app/Jobs/Drip/SendFeedbackEmailJob.php @@ -4,45 +4,22 @@ namespace App\Jobs\Drip; use App\Enums\DripEmailType; use App\Mail\Drip\FeedbackEmail; -use App\Models\User; -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; +use Illuminate\Mail\Mailable; -class SendFeedbackEmailJob implements ShouldQueue +class SendFeedbackEmailJob extends SendDripEmailJob { - use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; - - public function __construct(public User $user) + protected function emailType(): DripEmailType { - $this->onQueue('emails'); + return DripEmailType::Feedback; } - public function handle(): void + protected function buildMail(): Mailable { - if (! $this->user->canReceiveEmails()) { - return; - } + return new FeedbackEmail($this->user); + } - if ($this->user->hasReceivedEmail(DripEmailType::Feedback)) { - return; - } - - 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(), - ]); + protected function shouldSend(): bool + { + return ! $this->user->hasProPlan(); } } diff --git a/app/Jobs/Drip/SendImportHelpEmailJob.php b/app/Jobs/Drip/SendImportHelpEmailJob.php index 71708a2a..dd941ce4 100644 --- a/app/Jobs/Drip/SendImportHelpEmailJob.php +++ b/app/Jobs/Drip/SendImportHelpEmailJob.php @@ -4,53 +4,24 @@ namespace App\Jobs\Drip; use App\Enums\DripEmailType; use App\Mail\Drip\ImportHelpEmail; -use App\Models\User; -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; +use Illuminate\Mail\Mailable; -class SendImportHelpEmailJob implements ShouldQueue +class SendImportHelpEmailJob extends SendDripEmailJob { - use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; - - public function __construct(public User $user) + protected function emailType(): DripEmailType { - $this->onQueue('emails'); + return DripEmailType::ImportHelp; } - public function handle(): void + protected function buildMail(): Mailable { - if (! $this->user->canReceiveEmails()) { - return; - } + return new ImportHelpEmail($this->user); + } - if ($this->user->hasReceivedEmail(DripEmailType::ImportHelp)) { - return; - } - - if (! $this->user->isOnboarded()) { - 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(), - ]); + protected function shouldSend(): bool + { + return $this->user->isOnboarded() + && ! $this->user->transactions()->exists() + && ! $this->user->hasProPlan(); } } diff --git a/app/Jobs/Drip/SendOnboardingReminderEmailJob.php b/app/Jobs/Drip/SendOnboardingReminderEmailJob.php index 7b7c97d8..58bae473 100644 --- a/app/Jobs/Drip/SendOnboardingReminderEmailJob.php +++ b/app/Jobs/Drip/SendOnboardingReminderEmailJob.php @@ -4,45 +4,22 @@ namespace App\Jobs\Drip; use App\Enums\DripEmailType; use App\Mail\Drip\OnboardingReminderEmail; -use App\Models\User; -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; +use Illuminate\Mail\Mailable; -class SendOnboardingReminderEmailJob implements ShouldQueue +class SendOnboardingReminderEmailJob extends SendDripEmailJob { - use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; - - public function __construct(public User $user) + protected function emailType(): DripEmailType { - $this->onQueue('emails'); + return DripEmailType::OnboardingReminder; } - public function handle(): void + protected function buildMail(): Mailable { - if (! $this->user->canReceiveEmails()) { - return; - } + return new OnboardingReminderEmail($this->user); + } - if ($this->user->hasReceivedEmail(DripEmailType::OnboardingReminder)) { - return; - } - - 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(), - ]); + protected function shouldSend(): bool + { + return ! $this->user->isOnboarded(); } } diff --git a/app/Jobs/Drip/SendPaywallFollowUpEmailJob.php b/app/Jobs/Drip/SendPaywallFollowUpEmailJob.php index 52776e5c..f331f4e7 100644 --- a/app/Jobs/Drip/SendPaywallFollowUpEmailJob.php +++ b/app/Jobs/Drip/SendPaywallFollowUpEmailJob.php @@ -4,49 +4,23 @@ namespace App\Jobs\Drip; use App\Enums\DripEmailType; use App\Mail\Drip\PaywallFollowUpEmail; -use App\Models\User; -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; +use Illuminate\Mail\Mailable; -class SendPaywallFollowUpEmailJob implements ShouldQueue +class SendPaywallFollowUpEmailJob extends SendDripEmailJob { - use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; - - public function __construct(public User $user) + protected function emailType(): DripEmailType { - $this->onQueue('emails'); + return DripEmailType::PaywallFollowUp; } - public function handle(): void + protected function buildMail(): Mailable { - if (! $this->user->canReceiveEmails()) { - return; - } + return new PaywallFollowUpEmail($this->user); + } - if ($this->user->hasReceivedEmail(DripEmailType::PaywallFollowUp)) { - return; - } - - 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(), - ]); + protected function shouldSend(): bool + { + return ! $this->user->hasProPlan() + && $this->user->bankingConnections()->exists(); } } diff --git a/app/Jobs/Drip/SendPromoCodeEmailJob.php b/app/Jobs/Drip/SendPromoCodeEmailJob.php index 11d87dce..b62c7386 100644 --- a/app/Jobs/Drip/SendPromoCodeEmailJob.php +++ b/app/Jobs/Drip/SendPromoCodeEmailJob.php @@ -4,53 +4,24 @@ namespace App\Jobs\Drip; use App\Enums\DripEmailType; use App\Mail\Drip\PromoCodeEmail; -use App\Models\User; -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; +use Illuminate\Mail\Mailable; -class SendPromoCodeEmailJob implements ShouldQueue +class SendPromoCodeEmailJob extends SendDripEmailJob { - use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; - - public function __construct(public User $user) + protected function emailType(): DripEmailType { - $this->onQueue('emails'); + return DripEmailType::PromoCode; } - public function handle(): void + protected function buildMail(): Mailable { - if (! $this->user->canReceiveEmails()) { - return; - } + return new PromoCodeEmail($this->user); + } - if ($this->user->hasReceivedEmail(DripEmailType::PromoCode)) { - return; - } - - if (! $this->user->isOnboarded()) { - 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(), - ]); + protected function shouldSend(): bool + { + return $this->user->isOnboarded() + && $this->user->transactions()->exists() + && ! $this->user->hasProPlan(); } } diff --git a/app/Jobs/Drip/SendSubscriptionCancelledEmailJob.php b/app/Jobs/Drip/SendSubscriptionCancelledEmailJob.php index 1f9283ed..4c35381c 100644 --- a/app/Jobs/Drip/SendSubscriptionCancelledEmailJob.php +++ b/app/Jobs/Drip/SendSubscriptionCancelledEmailJob.php @@ -4,41 +4,17 @@ namespace App\Jobs\Drip; use App\Enums\DripEmailType; use App\Mail\Drip\SubscriptionCancelledEmail; -use App\Models\User; -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; +use Illuminate\Mail\Mailable; -class SendSubscriptionCancelledEmailJob implements ShouldQueue +class SendSubscriptionCancelledEmailJob extends SendDripEmailJob { - use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; - - public function __construct(public User $user) + protected function emailType(): DripEmailType { - $this->onQueue('emails'); + return DripEmailType::SubscriptionCancelled; } - public function handle(): void + protected function buildMail(): Mailable { - if (! $this->user->canReceiveEmails()) { - 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(), - ]); + return new SubscriptionCancelledEmail($this->user); } } diff --git a/app/Jobs/Drip/SendWelcomeEmailJob.php b/app/Jobs/Drip/SendWelcomeEmailJob.php index 6f2f0c9c..040ba7f6 100644 --- a/app/Jobs/Drip/SendWelcomeEmailJob.php +++ b/app/Jobs/Drip/SendWelcomeEmailJob.php @@ -4,41 +4,17 @@ namespace App\Jobs\Drip; use App\Enums\DripEmailType; use App\Mail\Drip\WelcomeEmail; -use App\Models\User; -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; +use Illuminate\Mail\Mailable; -class SendWelcomeEmailJob implements ShouldQueue +class SendWelcomeEmailJob extends SendDripEmailJob { - use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; - - public function __construct(public User $user) + protected function emailType(): DripEmailType { - $this->onQueue('emails'); + return DripEmailType::Welcome; } - public function handle(): void + protected function buildMail(): Mailable { - if (! $this->user->canReceiveEmails()) { - 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(), - ]); + return new WelcomeEmail($this->user); } } diff --git a/app/Mail/Drip/AiConsentFollowUpEmail.php b/app/Mail/Drip/AiConsentFollowUpEmail.php index 1a5470b9..dc801f83 100644 --- a/app/Mail/Drip/AiConsentFollowUpEmail.php +++ b/app/Mail/Drip/AiConsentFollowUpEmail.php @@ -2,67 +2,15 @@ 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; - -class AiConsentFollowUpEmail extends Mailable implements ShouldQueue +class AiConsentFollowUpEmail extends DripMail { - 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 - */ - public $backoff = [2, 5, 10, 30]; - - public function __construct(public User $user) + protected function dripSubject(): string { - $this->onQueue('emails'); + return __('Putting AI to work on your finances'); } - public function envelope(): Envelope + protected function template(): string { - return new Envelope( - 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 - */ - public function middleware(): array - { - return [(new RateLimited('emails'))->releaseAfter(1)]; + return 'mail.drip.ai-consent-follow-up'; } } diff --git a/app/Mail/Drip/DripMail.php b/app/Mail/Drip/DripMail.php new file mode 100644 index 00000000..b080e986 --- /dev/null +++ b/app/Mail/Drip/DripMail.php @@ -0,0 +1,94 @@ + + */ + 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 + */ + 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 + */ + public function middleware(): array + { + return [(new RateLimited('emails'))->releaseAfter(1)]; + } +} diff --git a/app/Mail/Drip/FeedbackEmail.php b/app/Mail/Drip/FeedbackEmail.php index 37c7f2ee..95e658b1 100644 --- a/app/Mail/Drip/FeedbackEmail.php +++ b/app/Mail/Drip/FeedbackEmail.php @@ -2,67 +2,15 @@ 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; - -class FeedbackEmail extends Mailable implements ShouldQueue +class FeedbackEmail extends DripMail { - 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 - */ - public $backoff = [2, 5, 10, 30]; - - public function __construct(public User $user) + protected function dripSubject(): string { - $this->onQueue('emails'); + return __("How's Your Experience So Far?"); } - public function envelope(): Envelope + protected function template(): string { - return new Envelope( - 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 - */ - public function middleware(): array - { - return [(new RateLimited('emails'))->releaseAfter(1)]; + return 'mail.drip.feedback'; } } diff --git a/app/Mail/Drip/ImportHelpEmail.php b/app/Mail/Drip/ImportHelpEmail.php index f4825f50..8ac3c4db 100644 --- a/app/Mail/Drip/ImportHelpEmail.php +++ b/app/Mail/Drip/ImportHelpEmail.php @@ -2,67 +2,15 @@ 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; - -class ImportHelpEmail extends Mailable implements ShouldQueue +class ImportHelpEmail extends DripMail { - 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 - */ - public $backoff = [2, 5, 10, 30]; - - public function __construct(public User $user) + protected function dripSubject(): string { - $this->onQueue('emails'); + return __("Let's Import Your Transactions"); } - public function envelope(): Envelope + protected function template(): string { - return new Envelope( - 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 - */ - public function middleware(): array - { - return [(new RateLimited('emails'))->releaseAfter(1)]; + return 'mail.drip.import-help'; } } diff --git a/app/Mail/Drip/OnboardingReminderEmail.php b/app/Mail/Drip/OnboardingReminderEmail.php index f444d891..b1f76cb8 100644 --- a/app/Mail/Drip/OnboardingReminderEmail.php +++ b/app/Mail/Drip/OnboardingReminderEmail.php @@ -2,67 +2,15 @@ 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; - -class OnboardingReminderEmail extends Mailable implements ShouldQueue +class OnboardingReminderEmail extends DripMail { - 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 - */ - public $backoff = [2, 5, 10, 30]; - - public function __construct(public User $user) + protected function dripSubject(): string { - $this->onQueue('emails'); + return __('Need Help Getting Started?'); } - public function envelope(): Envelope + protected function template(): string { - return new Envelope( - 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 - */ - public function middleware(): array - { - return [(new RateLimited('emails'))->releaseAfter(1)]; + return 'mail.drip.onboarding-reminder'; } } diff --git a/app/Mail/Drip/PaywallFollowUpEmail.php b/app/Mail/Drip/PaywallFollowUpEmail.php index ccbb53e4..dd87ab8e 100644 --- a/app/Mail/Drip/PaywallFollowUpEmail.php +++ b/app/Mail/Drip/PaywallFollowUpEmail.php @@ -2,73 +2,20 @@ 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; - -class PaywallFollowUpEmail extends Mailable implements ShouldQueue +class PaywallFollowUpEmail extends DripMail { - 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 - */ - public $backoff = [2, 5, 10, 30]; - - public function __construct(public User $user) + protected function dripSubject(): string { - $this->onQueue('emails'); + return __('What stopped you from getting started?'); } - public function envelope(): Envelope + protected function template(): string { - return new Envelope( - 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?'), - ); + return 'mail.drip.paywall-follow-up'; } - public function content(): Content + protected function repliesToSender(): bool { - return new Content( - markdown: 'mail.drip.paywall-follow-up', - with: [ - 'userName' => $this->user->name, - ], - ); - } - - /** - * Get the middleware the job should pass through. - * - * @return array - */ - public function middleware(): array - { - return [(new RateLimited('emails'))->releaseAfter(1)]; + return true; } } diff --git a/app/Mail/Drip/PromoCodeEmail.php b/app/Mail/Drip/PromoCodeEmail.php index e2090472..e5af4244 100644 --- a/app/Mail/Drip/PromoCodeEmail.php +++ b/app/Mail/Drip/PromoCodeEmail.php @@ -2,68 +2,20 @@ 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; - -class PromoCodeEmail extends Mailable implements ShouldQueue +class PromoCodeEmail extends DripMail { - 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 - */ - public $backoff = [2, 5, 10, 30]; - - public function __construct(public User $user) + protected function dripSubject(): string { - $this->onQueue('emails'); + return __('Your Founder Discount - 80% Off First Period'); } - public function envelope(): Envelope + protected function template(): string { - return new Envelope( - 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') - ); + return 'mail.drip.promo-code'; } - public function content(): Content + protected function contentData(): array { - return new Content( - markdown: 'mail.drip.promo-code', - with: [ - 'userName' => $this->user->name, - 'promoCode' => 'FOUNDER', - ], - ); - } - - /** - * Get the middleware the job should pass through. - * - * @return array - */ - public function middleware(): array - { - return [(new RateLimited('emails'))->releaseAfter(1)]; + return ['promoCode' => 'FOUNDER']; } } diff --git a/app/Mail/Drip/SubscriptionCancelledEmail.php b/app/Mail/Drip/SubscriptionCancelledEmail.php index d1637c63..22cf3d3d 100644 --- a/app/Mail/Drip/SubscriptionCancelledEmail.php +++ b/app/Mail/Drip/SubscriptionCancelledEmail.php @@ -2,67 +2,15 @@ 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; - -class SubscriptionCancelledEmail extends Mailable implements ShouldQueue +class SubscriptionCancelledEmail extends DripMail { - 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 - */ - public $backoff = [2, 5, 10, 30]; - - public function __construct(public User $user) + protected function dripSubject(): string { - $this->onQueue('emails'); + return __("We're sorry to see you go"); } - public function envelope(): Envelope + protected function template(): string { - return new Envelope( - 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 - */ - public function middleware(): array - { - return [(new RateLimited('emails'))->releaseAfter(1)]; + return 'mail.drip.subscription-cancelled'; } } diff --git a/app/Mail/Drip/WelcomeEmail.php b/app/Mail/Drip/WelcomeEmail.php index bea10763..423ce3c7 100644 --- a/app/Mail/Drip/WelcomeEmail.php +++ b/app/Mail/Drip/WelcomeEmail.php @@ -2,67 +2,15 @@ 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; - -class WelcomeEmail extends Mailable implements ShouldQueue +class WelcomeEmail extends DripMail { - 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 - */ - public $backoff = [2, 5, 10, 30]; - - public function __construct(public User $user) + protected function dripSubject(): string { - $this->onQueue('emails'); + return __('Welcome to Whisper Money - Your Privacy-First Finance App'); } - public function envelope(): Envelope + protected function template(): string { - return new Envelope( - 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 - */ - public function middleware(): array - { - return [(new RateLimited('emails'))->releaseAfter(1)]; + return 'mail.drip.welcome'; } } diff --git a/tests/Feature/MailSenderTest.php b/tests/Feature/MailSenderTest.php index c78351fb..788a2eff 100644 --- a/tests/Feature/MailSenderTest.php +++ b/tests/Feature/MailSenderTest.php @@ -3,9 +3,11 @@ use App\Mail\BankingConnectionAuthFailedEmail; use App\Mail\BankTransactionsSyncedEmail; use App\Mail\BrokenBankLogosReportEmail; +use App\Mail\Drip\AiConsentFollowUpEmail; use App\Mail\Drip\FeedbackEmail; use App\Mail\Drip\ImportHelpEmail; use App\Mail\Drip\OnboardingReminderEmail; +use App\Mail\Drip\PaywallFollowUpEmail; use App\Mail\Drip\PromoCodeEmail; use App\Mail\Drip\SubscriptionCancelledEmail; use App\Mail\Drip\WelcomeEmail; @@ -84,24 +86,44 @@ test('drip mailables use the drip sender', function (string $mailableClass) { $user = User::factory()->create(); $mailable = match ($mailableClass) { + AiConsentFollowUpEmail::class => new AiConsentFollowUpEmail($user), WelcomeEmail::class => new WelcomeEmail($user), FeedbackEmail::class => new FeedbackEmail($user), ImportHelpEmail::class => new ImportHelpEmail($user), OnboardingReminderEmail::class => new OnboardingReminderEmail($user), + PaywallFollowUpEmail::class => new PaywallFollowUpEmail($user), PromoCodeEmail::class => new PromoCodeEmail($user), SubscriptionCancelledEmail::class => new SubscriptionCancelledEmail($user), }; expect($mailable->envelope()->from)->toEqual(new Address('hi@whisper.money', 'Álvaro and Víctor')); })->with([ + AiConsentFollowUpEmail::class, WelcomeEmail::class, FeedbackEmail::class, ImportHelpEmail::class, OnboardingReminderEmail::class, + PaywallFollowUpEmail::class, PromoCodeEmail::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) { $user = User::factory()->create();