Cancel Enable Banking connections for free users (#289)
## Summary - add month-end command to revoke old Enable Banking sessions for users without a paid plan while keeping their linked accounts as manual accounts - extract banking disconnect flow into a reusable action so manual disconnects and scheduled cleanup share same revoke and detach behavior - add feature coverage for free-user cleanup, paid-user skips, under-6-hour skips, non-Enable Banking skips, and revoke failure fallback ## Testing - vendor/bin/pint --dirty --format agent - php artisan test --compact tests/Feature/CancelFreeEnableBankingConnectionsCommandTest.php tests/Feature/OpenBanking/ConnectionControllerTest.php
This commit is contained in:
parent
319ca758e1
commit
2f583c0113
|
|
@ -0,0 +1,44 @@
|
|||
<?php
|
||||
|
||||
namespace App\Actions\OpenBanking;
|
||||
|
||||
use App\Contracts\BankingProviderInterface;
|
||||
use App\Enums\BankingConnectionStatus;
|
||||
use App\Models\BankingConnection;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
class DisconnectBankingConnection
|
||||
{
|
||||
public function __construct(private BankingProviderInterface $provider) {}
|
||||
|
||||
public function handle(BankingConnection $connection, bool $deleteAccounts = false): void
|
||||
{
|
||||
if ($connection->isEnableBanking() && $connection->session_id && $connection->isActive()) {
|
||||
try {
|
||||
$this->provider->revokeSession($connection->session_id);
|
||||
} catch (\Throwable $e) {
|
||||
Log::warning('Failed to revoke EnableBanking session', [
|
||||
'connection_id' => $connection->id,
|
||||
'session_id' => $connection->session_id,
|
||||
'error' => $e->getMessage(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
if ($deleteAccounts) {
|
||||
$connection->accounts->each(function ($account): void {
|
||||
$account->transactions()->delete();
|
||||
$account->balances()->delete();
|
||||
$account->delete();
|
||||
});
|
||||
} else {
|
||||
$connection->accounts()->update([
|
||||
'banking_connection_id' => null,
|
||||
'external_account_id' => null,
|
||||
]);
|
||||
}
|
||||
|
||||
$connection->update(['status' => BankingConnectionStatus::Revoked]);
|
||||
$connection->delete();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,70 @@
|
|||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Actions\OpenBanking\DisconnectBankingConnection;
|
||||
use App\Enums\BankingConnectionStatus;
|
||||
use App\Mail\EnableBankingConnectionsCancelledEmail;
|
||||
use App\Models\BankingConnection;
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Facades\Mail;
|
||||
|
||||
class CancelFreeEnableBankingConnectionsCommand extends Command
|
||||
{
|
||||
protected $signature = 'banking:cancel-free-enablebanking';
|
||||
|
||||
protected $description = 'Close Enable Banking connections for free users at month end';
|
||||
|
||||
public function handle(DisconnectBankingConnection $disconnectBankingConnection): int
|
||||
{
|
||||
$cutoff = now()->subHours(6);
|
||||
|
||||
$connections = BankingConnection::query()
|
||||
->with(['user', 'accounts'])
|
||||
->where('provider', 'enablebanking')
|
||||
->where('status', '!=', BankingConnectionStatus::Revoked)
|
||||
->where('created_at', '<=', $cutoff)
|
||||
->get();
|
||||
|
||||
$count = $connections->count();
|
||||
|
||||
if ($count === 0) {
|
||||
$this->info('No eligible Enable Banking connections found for free users.');
|
||||
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
|
||||
$revoked = 0;
|
||||
$skipped = 0;
|
||||
|
||||
$connections
|
||||
->groupBy('user_id')
|
||||
->each(function ($userConnections) use ($disconnectBankingConnection, &$revoked, &$skipped): void {
|
||||
$user = $userConnections->first()?->user;
|
||||
|
||||
if (! $user) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ($user->hasProPlan()) {
|
||||
$skipped += $userConnections->count();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
foreach ($userConnections as $connection) {
|
||||
$disconnectBankingConnection->handle($connection);
|
||||
$revoked++;
|
||||
}
|
||||
|
||||
Mail::to($user)->send(new EnableBankingConnectionsCancelledEmail(
|
||||
$user,
|
||||
$userConnections->count(),
|
||||
));
|
||||
});
|
||||
|
||||
$this->info("Revoked {$revoked} Enable Banking connection(s). Skipped paid users: {$skipped}.");
|
||||
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
}
|
||||
|
|
@ -2,18 +2,20 @@
|
|||
|
||||
namespace App\Http\Controllers\OpenBanking;
|
||||
|
||||
use App\Contracts\BankingProviderInterface;
|
||||
use App\Actions\OpenBanking\DisconnectBankingConnection;
|
||||
use App\Enums\BankingConnectionStatus;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\OpenBanking\DestroyConnectionRequest;
|
||||
use App\Http\Requests\OpenBanking\UpdateConnectionCredentialsRequest;
|
||||
use App\Jobs\SyncBankingConnectionJob;
|
||||
use App\Models\BankingConnection;
|
||||
use App\Models\User;
|
||||
use App\Services\Banking\BinanceClient;
|
||||
use App\Services\Banking\BitpandaClient;
|
||||
use App\Services\Banking\IndexaCapitalClient;
|
||||
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Inertia\Inertia;
|
||||
use Inertia\Response;
|
||||
|
|
@ -27,8 +29,10 @@ class ConnectionController extends Controller
|
|||
*/
|
||||
public function index(): Response
|
||||
{
|
||||
$connections = auth()->user()
|
||||
->bankingConnections()
|
||||
/** @var User $user */
|
||||
$user = Auth::user();
|
||||
|
||||
$connections = $user->bankingConnections()
|
||||
->withCount('accounts')
|
||||
->orderByDesc('created_at')
|
||||
->get()
|
||||
|
|
@ -46,7 +50,7 @@ class ConnectionController extends Controller
|
|||
*/
|
||||
public function sync(BankingConnection $connection): RedirectResponse
|
||||
{
|
||||
if ($connection->user_id !== auth()->id()) {
|
||||
if ($connection->user_id !== Auth::id()) {
|
||||
abort(403);
|
||||
}
|
||||
|
||||
|
|
@ -127,34 +131,9 @@ class ConnectionController extends Controller
|
|||
/**
|
||||
* Revoke and delete a banking connection.
|
||||
*/
|
||||
public function destroy(DestroyConnectionRequest $request, BankingConnection $connection, BankingProviderInterface $provider): RedirectResponse
|
||||
public function destroy(DestroyConnectionRequest $request, BankingConnection $connection, DisconnectBankingConnection $disconnectBankingConnection): RedirectResponse
|
||||
{
|
||||
if ($connection->isEnableBanking() && $connection->session_id && $connection->isActive()) {
|
||||
try {
|
||||
$provider->revokeSession($connection->session_id);
|
||||
} catch (\Throwable $e) {
|
||||
Log::warning('Failed to revoke EnableBanking session', [
|
||||
'session_id' => $connection->session_id,
|
||||
'error' => $e->getMessage(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
if ($request->boolean('delete_accounts')) {
|
||||
$connection->accounts->each(function ($account) {
|
||||
$account->transactions()->delete();
|
||||
$account->balances()->delete();
|
||||
$account->delete();
|
||||
});
|
||||
} else {
|
||||
$connection->accounts()->update([
|
||||
'banking_connection_id' => null,
|
||||
'external_account_id' => null,
|
||||
]);
|
||||
}
|
||||
|
||||
$connection->update(['status' => BankingConnectionStatus::Revoked]);
|
||||
$connection->delete();
|
||||
$disconnectBankingConnection->handle($connection, $request->boolean('delete_accounts'));
|
||||
|
||||
return redirect()->route('settings.connections.index')
|
||||
->with('success', 'Banking connection disconnected.');
|
||||
|
|
|
|||
|
|
@ -0,0 +1,63 @@
|
|||
<?php
|
||||
|
||||
namespace App\Mail;
|
||||
|
||||
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 EnableBankingConnectionsCancelledEmail extends Mailable implements ShouldQueue
|
||||
{
|
||||
use Queueable, SerializesModels;
|
||||
|
||||
/** @var int */
|
||||
public $tries = 5;
|
||||
|
||||
/** @var array<int, int> */
|
||||
public $backoff = [2, 5, 10, 30];
|
||||
|
||||
public function __construct(
|
||||
public User $user,
|
||||
public int $removedConnectionsCount,
|
||||
) {
|
||||
$this->onQueue('emails');
|
||||
}
|
||||
|
||||
public function envelope(): Envelope
|
||||
{
|
||||
return new Envelope(
|
||||
from: new Address(
|
||||
config('mail.from.address', 'no-reply@whisper.money'),
|
||||
config('mail.from.name', 'Whisper Money'),
|
||||
),
|
||||
subject: __('Your bank connections were disconnected'),
|
||||
);
|
||||
}
|
||||
|
||||
public function content(): Content
|
||||
{
|
||||
return new Content(
|
||||
markdown: 'mail.enable-banking-connections-cancelled',
|
||||
with: [
|
||||
'userName' => $this->user->name,
|
||||
'removedConnectionsCount' => $this->removedConnectionsCount,
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the middleware the job should pass through.
|
||||
*
|
||||
* @return array<int, object>
|
||||
*/
|
||||
public function middleware(): array
|
||||
{
|
||||
return [(new RateLimited('emails'))->releaseAfter(1)];
|
||||
}
|
||||
}
|
||||
|
|
@ -589,6 +589,7 @@
|
|||
"Get started at no cost. No bank connections included.": "Empieza sin coste. Sin conexiones bancarias incluidas.",
|
||||
"Get started quickly with your existing financial data.": "Comienza r\u00e1pidamente con tus datos financieros existentes.",
|
||||
"Github": "Github",
|
||||
"We disconnected your bank connection to keep your account on free access. Automatic bank sync is now paused, but all your accounts, transactions, and balances remain in Whisper Money.|We disconnected your :count bank connections to keep your account on free access. Automatic bank sync is now paused, but all your accounts, transactions, and balances remain in Whisper Money.": "Desconectamos tu conexi\u00f3n bancaria para mantener tu cuenta con acceso gratuito. La sincronizaci\u00f3n bancaria autom\u00e1tica est\u00e1 ahora en pausa, pero todas tus cuentas, transacciones y saldos permanecen en Whisper Money.|Desconectamos tus :count conexiones bancarias para mantener tu cuenta con acceso gratuito. La sincronizaci\u00f3n bancaria autom\u00e1tica est\u00e1 ahora en pausa, pero todas tus cuentas, transacciones y saldos permanecen en Whisper Money.",
|
||||
"Go to Dashboard": "Ir al Panel",
|
||||
"Go to your account's transaction history": "Ve al historial de transacciones de tu cuenta",
|
||||
"Go to your dashboard and click \"Import Transactions\". Select your CSV file and I'll map the columns automatically.": "Ve a tu panel y haz clic en \"Importar Transacciones\". Selecciona tu archivo CSV y mapear\u00e9 las columnas autom\u00e1ticamente.",
|
||||
|
|
@ -617,6 +618,8 @@
|
|||
"Hi! It's Victor and \u00c1lvaro, the founders of Whisper Money. We noticed you've completed your setup but haven't imported any transactions yet. Let us help you get started!": "\u00a1Hola! Somos V\u00edctor y \u00c1lvaro, los fundadores de Whisper Money. Notamos que completaste tu configuraci\u00f3n pero a\u00fan no has importado transacciones. \u00a1D\u00e9janos ayudarte a empezar!",
|
||||
"Hi! It's Victor and \u00c1lvaro, the founders of Whisper Money. We see you've already started importing your transactions - that's awesome! You're well on your way to taking control of your finances while keeping your data private.": "\u00a1Hola! Somos V\u00edctor y \u00c1lvaro, los fundadores de Whisper Money. Vemos que ya has empezado a importar tus transacciones, \u00a1genial! Est\u00e1s en camino de tomar el control de tus finanzas mientras mantienes tus datos privados.",
|
||||
"Hi! It's Victor here, the founder of Whisper Money. You've been using the app for a few days now, and I'd love to hear how it's working for you.": "\u00a1Hola! Soy V\u00edctor, el fundador de Whisper Money. Llevas unos d\u00edas usando la app y me encantar\u00eda saber c\u00f3mo te est\u00e1 funcionando.",
|
||||
"You can continue using the app on the free plan, and you can reconnect your bank later if you upgrade again.": "Puedes seguir usando la app con el plan gratuito y volver a conectar tu banco m\u00e1s adelante si actualizas de nuevo.",
|
||||
"Your bank connections were disconnected": "Tus conexiones bancarias fueron desconectadas",
|
||||
"Hi! It's Victor, the founder of Whisper Money. I noticed you signed up but haven't completed your setup yet. I wanted to check in personally and see if something went wrong or if you need any help.": "\u00a1Hola! Soy V\u00edctor, el fundador de Whisper Money. Not\u00e9 que te registraste pero a\u00fan no has completado tu configuraci\u00f3n. Quer\u00eda contactarte personalmente para ver si algo sali\u00f3 mal o si necesitas ayuda.",
|
||||
"Hi! It's Victor, the founder of Whisper Money. I noticed you've cancelled your subscription, and I wanted to reach out personally.": "\u00a1Hola! Soy V\u00edctor, el fundador de Whisper Money. Not\u00e9 que cancelaste tu suscripci\u00f3n y quer\u00eda contactarte personalmente.",
|
||||
"Hi! It's Victor, the founder of Whisper Money. I noticed you've completed your setup but haven't imported any transactions yet. Let me help you get started!": "\u00a1Hola! Soy V\u00edctor, el fundador de Whisper Money. Not\u00e9 que completaste tu configuraci\u00f3n pero a\u00fan no has importado transacciones. \u00a1D\u00e9jame ayudarte a empezar!",
|
||||
|
|
|
|||
|
|
@ -0,0 +1,17 @@
|
|||
<x-mail::message>
|
||||
# {{ __('Your bank connections were disconnected') }}
|
||||
|
||||
{{ __('Hi :name,', ['name' => $userName]) }}
|
||||
|
||||
{{ trans_choice('We disconnected your bank connection to keep your account on free access. Automatic bank sync is now paused, but all your accounts, transactions, and balances remain in Whisper Money.|We disconnected your :count bank connections to keep your account on free access. Automatic bank sync is now paused, but all your accounts, transactions, and balances remain in Whisper Money.', $removedConnectionsCount, ['count' => $removedConnectionsCount]) }}
|
||||
|
||||
{{ __('You can continue using the app on the free plan, and you can reconnect your bank later if you upgrade again.') }}
|
||||
|
||||
<x-mail::button :url="route('dashboard')">
|
||||
{{ __('Go to Dashboard') }}
|
||||
</x-mail::button>
|
||||
|
||||
{{ __('Best,') }}<br>
|
||||
{{ __('Álvaro & Víctor') }}<br>
|
||||
{{ __('Founders of Whisper Money') }}
|
||||
</x-mail::message>
|
||||
|
|
@ -7,6 +7,7 @@ Schedule::command('horizon:snapshot')->everyFiveMinutes();
|
|||
Schedule::command('budgets:generate-periods')->daily();
|
||||
Schedule::command('banking:sync')->everySixHours();
|
||||
Schedule::command('banks:check-logos')->weekly();
|
||||
Schedule::command('banking:cancel-free-enablebanking')->lastDayOfMonth('18:00');
|
||||
Schedule::command('real-estate:apply-revaluation')->monthlyOn(1, '00:00');
|
||||
Schedule::command('loans:generate-balances')->monthlyOn(1, '00:00');
|
||||
Schedule::command('resend:sync-leads')->dailyAt('03:00');
|
||||
|
|
|
|||
|
|
@ -0,0 +1,174 @@
|
|||
<?php
|
||||
|
||||
use App\Contracts\BankingProviderInterface;
|
||||
use App\Enums\BankingConnectionStatus;
|
||||
use App\Mail\EnableBankingConnectionsCancelledEmail;
|
||||
use App\Models\Account;
|
||||
use App\Models\BankingConnection;
|
||||
use App\Models\User;
|
||||
use Illuminate\Support\Facades\Mail;
|
||||
|
||||
use function Pest\Laravel\artisan;
|
||||
|
||||
test('revokes old enable banking connections for free users and keeps accounts manual', function () {
|
||||
config(['subscriptions.enabled' => true]);
|
||||
Mail::fake();
|
||||
|
||||
$user = User::factory()->create();
|
||||
$connection = BankingConnection::factory()->for($user)->create([
|
||||
'created_at' => now()->subHours(7),
|
||||
]);
|
||||
$account = Account::factory()->for($user)->create([
|
||||
'banking_connection_id' => $connection->id,
|
||||
'external_account_id' => 'ext-123',
|
||||
]);
|
||||
|
||||
$mockProvider = Mockery::mock(BankingProviderInterface::class);
|
||||
$mockProvider->shouldReceive('revokeSession')->once()->with($connection->session_id);
|
||||
app()->instance(BankingProviderInterface::class, $mockProvider);
|
||||
|
||||
artisan('banking:cancel-free-enablebanking')
|
||||
->expectsOutputToContain('Revoked 1 Enable Banking connection(s). Skipped paid users: 0.')
|
||||
->assertSuccessful();
|
||||
|
||||
$connection->refresh();
|
||||
expect($connection->status)->toBe(BankingConnectionStatus::Revoked);
|
||||
expect($connection->trashed())->toBeTrue();
|
||||
|
||||
$account->refresh();
|
||||
expect($account->banking_connection_id)->toBeNull();
|
||||
expect($account->external_account_id)->toBeNull();
|
||||
expect($account->trashed())->toBeFalse();
|
||||
|
||||
Mail::assertQueued(EnableBankingConnectionsCancelledEmail::class, function ($mail) use ($user) {
|
||||
return $mail->hasTo($user->email) && $mail->removedConnectionsCount === 1;
|
||||
});
|
||||
});
|
||||
|
||||
test('skips enable banking connections created less than six hours ago', function () {
|
||||
config(['subscriptions.enabled' => true]);
|
||||
Mail::fake();
|
||||
|
||||
$user = User::factory()->create();
|
||||
$connection = BankingConnection::factory()->for($user)->create([
|
||||
'created_at' => now()->subHours(5),
|
||||
]);
|
||||
|
||||
$mockProvider = Mockery::mock(BankingProviderInterface::class);
|
||||
$mockProvider->shouldNotReceive('revokeSession');
|
||||
app()->instance(BankingProviderInterface::class, $mockProvider);
|
||||
|
||||
artisan('banking:cancel-free-enablebanking')
|
||||
->expectsOutputToContain('No eligible Enable Banking connections found for free users.')
|
||||
->assertSuccessful();
|
||||
|
||||
expect($connection->fresh()->trashed())->toBeFalse();
|
||||
expect($connection->fresh()->status)->not->toBe(BankingConnectionStatus::Revoked);
|
||||
Mail::assertNothingOutgoing();
|
||||
});
|
||||
|
||||
test('skips subscribed users', function () {
|
||||
config(['subscriptions.enabled' => true]);
|
||||
Mail::fake();
|
||||
|
||||
$user = User::factory()->create();
|
||||
$user->subscriptions()->create([
|
||||
'type' => 'default',
|
||||
'stripe_id' => 'sub_test123',
|
||||
'stripe_status' => 'active',
|
||||
'stripe_price' => 'price_test123',
|
||||
]);
|
||||
|
||||
$connection = BankingConnection::factory()->for($user)->create([
|
||||
'created_at' => now()->subHours(7),
|
||||
]);
|
||||
|
||||
$mockProvider = Mockery::mock(BankingProviderInterface::class);
|
||||
$mockProvider->shouldNotReceive('revokeSession');
|
||||
app()->instance(BankingProviderInterface::class, $mockProvider);
|
||||
|
||||
artisan('banking:cancel-free-enablebanking')
|
||||
->expectsOutputToContain('Revoked 0 Enable Banking connection(s). Skipped paid users: 1.')
|
||||
->assertSuccessful();
|
||||
|
||||
expect($connection->fresh()->trashed())->toBeFalse();
|
||||
Mail::assertNothingOutgoing();
|
||||
});
|
||||
|
||||
test('skips non enable banking providers', function () {
|
||||
config(['subscriptions.enabled' => true]);
|
||||
Mail::fake();
|
||||
|
||||
$user = User::factory()->create();
|
||||
$connection = BankingConnection::factory()->for($user)->indexaCapital()->create([
|
||||
'created_at' => now()->subHours(7),
|
||||
]);
|
||||
|
||||
$mockProvider = Mockery::mock(BankingProviderInterface::class);
|
||||
$mockProvider->shouldNotReceive('revokeSession');
|
||||
app()->instance(BankingProviderInterface::class, $mockProvider);
|
||||
|
||||
artisan('banking:cancel-free-enablebanking')
|
||||
->expectsOutputToContain('No eligible Enable Banking connections found for free users.')
|
||||
->assertSuccessful();
|
||||
|
||||
expect($connection->fresh()->trashed())->toBeFalse();
|
||||
Mail::assertNothingOutgoing();
|
||||
});
|
||||
|
||||
test('continues disconnect when enable banking revoke fails', function () {
|
||||
config(['subscriptions.enabled' => true]);
|
||||
Mail::fake();
|
||||
|
||||
$user = User::factory()->create();
|
||||
$connection = BankingConnection::factory()->for($user)->create([
|
||||
'created_at' => now()->subHours(7),
|
||||
]);
|
||||
$account = Account::factory()->for($user)->create([
|
||||
'banking_connection_id' => $connection->id,
|
||||
'external_account_id' => 'ext-456',
|
||||
]);
|
||||
|
||||
$mockProvider = Mockery::mock(BankingProviderInterface::class);
|
||||
$mockProvider->shouldReceive('revokeSession')->once()->andThrow(new RuntimeException('API unavailable'));
|
||||
app()->instance(BankingProviderInterface::class, $mockProvider);
|
||||
|
||||
artisan('banking:cancel-free-enablebanking')->assertSuccessful();
|
||||
|
||||
expect($connection->fresh()->trashed())->toBeTrue();
|
||||
|
||||
$account->refresh();
|
||||
expect($account->banking_connection_id)->toBeNull();
|
||||
expect($account->external_account_id)->toBeNull();
|
||||
|
||||
Mail::assertQueued(EnableBankingConnectionsCancelledEmail::class, function ($mail) use ($user) {
|
||||
return $mail->hasTo($user->email) && $mail->removedConnectionsCount === 1;
|
||||
});
|
||||
});
|
||||
|
||||
test('sends one email per user when multiple connections are removed', function () {
|
||||
config(['subscriptions.enabled' => true]);
|
||||
Mail::fake();
|
||||
|
||||
$user = User::factory()->create();
|
||||
$firstConnection = BankingConnection::factory()->for($user)->create([
|
||||
'created_at' => now()->subHours(7),
|
||||
]);
|
||||
$secondConnection = BankingConnection::factory()->for($user)->create([
|
||||
'created_at' => now()->subHours(8),
|
||||
]);
|
||||
|
||||
$mockProvider = Mockery::mock(BankingProviderInterface::class);
|
||||
$mockProvider->shouldReceive('revokeSession')->once()->with($firstConnection->session_id);
|
||||
$mockProvider->shouldReceive('revokeSession')->once()->with($secondConnection->session_id);
|
||||
app()->instance(BankingProviderInterface::class, $mockProvider);
|
||||
|
||||
artisan('banking:cancel-free-enablebanking')
|
||||
->expectsOutputToContain('Revoked 2 Enable Banking connection(s). Skipped paid users: 0.')
|
||||
->assertSuccessful();
|
||||
|
||||
Mail::assertQueued(EnableBankingConnectionsCancelledEmail::class, 1);
|
||||
Mail::assertQueued(EnableBankingConnectionsCancelledEmail::class, function ($mail) use ($user) {
|
||||
return $mail->hasTo($user->email) && $mail->removedConnectionsCount === 2;
|
||||
});
|
||||
});
|
||||
|
|
@ -9,6 +9,7 @@ use App\Mail\Drip\OnboardingReminderEmail;
|
|||
use App\Mail\Drip\PromoCodeEmail;
|
||||
use App\Mail\Drip\SubscriptionCancelledEmail;
|
||||
use App\Mail\Drip\WelcomeEmail;
|
||||
use App\Mail\EnableBankingConnectionsCancelledEmail;
|
||||
use App\Mail\UpdateEmail;
|
||||
use App\Mail\WaitlistOvertaken;
|
||||
use App\Mail\WaitlistReferralNotification;
|
||||
|
|
@ -108,6 +109,7 @@ test('default sender is used for active non-drip mailables', function (string $m
|
|||
UpdateEmail::class => new UpdateEmail($user, 'test-update'),
|
||||
BankTransactionsSyncedEmail::class => new BankTransactionsSyncedEmail($user, 3, ['Test Bank' => 3]),
|
||||
BankingConnectionAuthFailedEmail::class => new BankingConnectionAuthFailedEmail($user, BankingConnection::factory()->for($user)->create(['aspsp_name' => 'Test Bank'])),
|
||||
EnableBankingConnectionsCancelledEmail::class => new EnableBankingConnectionsCancelledEmail($user, 2),
|
||||
BrokenBankLogosReportEmail::class => new BrokenBankLogosReportEmail([['id' => 'bank-1', 'name' => 'Test Bank', 'previous_logo' => 'https://example.com/logo.png']]),
|
||||
WaitlistWelcome::class => new WaitlistWelcome(UserLead::factory()->create()),
|
||||
WaitlistReferralNotification::class => new WaitlistReferralNotification(UserLead::factory()->create()),
|
||||
|
|
@ -124,6 +126,7 @@ test('default sender is used for active non-drip mailables', function (string $m
|
|||
UpdateEmail::class,
|
||||
BankTransactionsSyncedEmail::class,
|
||||
BankingConnectionAuthFailedEmail::class,
|
||||
EnableBankingConnectionsCancelledEmail::class,
|
||||
BrokenBankLogosReportEmail::class,
|
||||
WaitlistWelcome::class,
|
||||
WaitlistReferralNotification::class,
|
||||
|
|
@ -176,6 +179,7 @@ test('mail blade signatures use alvaro before victor', function () {
|
|||
'mail/bank-transactions-synced.blade.php',
|
||||
'mail/drip/feedback.blade.php',
|
||||
'mail/banking-connection-auth-failed.blade.php',
|
||||
'mail/enable-banking-connections-cancelled.blade.php',
|
||||
];
|
||||
|
||||
foreach ($mailViews as $mailView) {
|
||||
|
|
@ -186,3 +190,15 @@ test('mail blade signatures use alvaro before victor', function () {
|
|||
->not->toContain("{{ __('Víctor & Álvaro') }}<br>");
|
||||
}
|
||||
});
|
||||
|
||||
test('enable banking cancellation email includes dashboard access messaging', function () {
|
||||
$user = User::factory()->create(['name' => 'Test User']);
|
||||
|
||||
$mailable = new EnableBankingConnectionsCancelledEmail($user, 2);
|
||||
|
||||
$mailable->assertHasSubject('Your bank connections were disconnected');
|
||||
$mailable->assertSeeInHtml('Go to Dashboard');
|
||||
$mailable->assertSeeInHtml('free access');
|
||||
$mailable->assertSeeInHtml('accounts, transactions, and balances remain in Whisper Money');
|
||||
$mailable->assertSeeInHtml(route('dashboard'));
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in New Issue