diff --git a/app/Mail/AccountWillExpire.php b/app/Mail/AccountWillExpire.php index 42a165699..7bab9e54a 100644 --- a/app/Mail/AccountWillExpire.php +++ b/app/Mail/AccountWillExpire.php @@ -24,16 +24,31 @@ class AccountWillExpire extends Mailable public ?string $preheader; + public bool $hasPendingRole; + + public ?string $pendingRoleName; + + public ?string $pendingRoleStartDate; + private string $siteEmail; public function __construct(User $user, int $days) { + $roleExpiryInfo = $user->getRoleExpiryInfo(); + $pendingRole = $roleExpiryInfo['pending_role']; + $pendingStart = $roleExpiryInfo['pending_start']; + $this->days = $days; $this->username = (string) $user->username; $this->account = (string) ($user->role->name ?? 'User'); + $this->hasPendingRole = (bool) $roleExpiryInfo['has_pending_role'] && $pendingRole !== null && $pendingStart !== null; + $this->pendingRoleName = $this->hasPendingRole ? (string) $pendingRole->name : null; + $this->pendingRoleStartDate = $this->hasPendingRole ? $pendingStart->toFormattedDateString() : null; $this->siteEmail = (string) config('mail.from.address'); $this->site = (string) config('app.name'); - $this->preheader = "Your {$this->account} role expires in {$this->days} day(s)."; + $this->preheader = $this->hasPendingRole + ? "Your {$this->account} role expires in {$this->days} day(s), then {$this->pendingRoleName} is scheduled." + : "Your {$this->account} role expires in {$this->days} day(s)."; } public function build(): static @@ -44,6 +59,9 @@ class AccountWillExpire extends Mailable 'username' => $this->username, 'account' => $this->account, 'days' => $this->days, + 'hasPendingRole' => $this->hasPendingRole, + 'pendingRoleName' => $this->pendingRoleName, + 'pendingRoleStartDate' => $this->pendingRoleStartDate, 'site' => $this->site, 'preheader' => $this->preheader, ]); diff --git a/app/Models/User.php b/app/Models/User.php index 454b4708d..bc1b1535a 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -109,6 +109,7 @@ use Spatie\Permission\Traits\HasRoles; * @property-read Collection $roleHistory * @property-read Collection $passkeys * @property-read Role|null $role + * @property-read Role|null $pendingRole * @property-read PasswordSecurity|null $passwordSecurity * * @method static Builder|User whereUsername(string $value) @@ -228,6 +229,14 @@ final class User extends Authenticatable implements HasPasskeys, MustVerifyEmail return $this->belongsTo(Role::class, 'roles_id'); } + /** + * @return BelongsTo + */ + public function pendingRole(): BelongsTo + { + return $this->belongsTo(Role::class, 'pending_roles_id'); + } + /** * @return HasMany */ @@ -649,7 +658,7 @@ final class User extends Authenticatable implements HasPasskeys, MustVerifyEmail return null; } - return Role::find($this->pending_roles_id); + return $this->pendingRole; } /** diff --git a/resources/views/emails/markdown/accountWillExpire.blade.php b/resources/views/emails/markdown/accountWillExpire.blade.php index df69c51fb..da6469218 100644 --- a/resources/views/emails/markdown/accountWillExpire.blade.php +++ b/resources/views/emails/markdown/accountWillExpire.blade.php @@ -7,7 +7,13 @@ Dear {{ $username }}, **Heads up:** Your **{{ $account }}** role expires in less than **{{ $days }} day(s)**. @endcomponent +@if($hasPendingRole) +Your **{{ $pendingRoleName }}** role is already scheduled to take effect on **{{ $pendingRoleStartDate }}** after your current role expires. + +No renewal action is needed for that pending role. If these details do not look right, please reach out before the expiry date. +@else To continue enjoying uninterrupted access to all your current features and benefits, please take action before your subscription expires. +@endif If you have any questions about renewing your account, please don't hesitate to reach out. diff --git a/tests/Feature/Mail/MailRenderingTest.php b/tests/Feature/Mail/MailRenderingTest.php index a63f87efd..1c702ee33 100644 --- a/tests/Feature/Mail/MailRenderingTest.php +++ b/tests/Feature/Mail/MailRenderingTest.php @@ -27,6 +27,7 @@ use Illuminate\Mail\Mailables\Content; use Illuminate\Mail\Markdown; use Illuminate\Support\Carbon; use PHPUnit\Framework\Attributes\DataProvider; +use Spatie\Permission\Models\Role; use Tests\TestCase; /** @@ -122,6 +123,31 @@ class MailRenderingTest extends TestCase $this->assertStringContainsStringIgnoringCase('color: #ffffff !important', $html); } + public function test_account_will_expire_includes_pending_role_details_when_present(): void + { + $currentRole = new Role; + $currentRole->name = 'Silver'; + + $pendingRole = new Role; + $pendingRole->name = 'Gold'; + + $user = new User; + $user->username = 'tester'; + $user->email = 'tester@example.test'; + $user->pending_roles_id = 2; + $user->pending_role_start_date = Carbon::create(2026, 6, 4, 12); + $user->setRelation('role', $currentRole); + $user->setRelation('pendingRole', $pendingRole); + + $mailable = new AccountWillExpire($user, 5); + + $mailable->assertSeeInHtml('Silver'); + $mailable->assertSeeInHtml('Gold'); + $mailable->assertSeeInHtml('Jun 4, 2026'); + $mailable->assertSeeInHtml('No renewal action is needed'); + $mailable->assertDontSeeInHtml('please take action before your subscription expires'); + } + public function test_invitation_mail_renders_with_branded_subject_and_brand_palette(): void { $inviter = new User;