feat(notification): implement email notifications for user registration and purchase events
This commit is contained in:
@@ -2,8 +2,13 @@
|
||||
|
||||
namespace Tests\Feature\Auth;
|
||||
|
||||
use App\Domains\Attachable\Enums\AttachmentType;
|
||||
use App\Domains\Attachable\Models\Attachment;
|
||||
use App\Domains\Auth\Models\User;
|
||||
use App\Domains\Notification\Events\UserRegistered;
|
||||
use App\Domains\Tenant\Models\Tenant;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\Event;
|
||||
use Tests\TestCase;
|
||||
|
||||
class RegisterControllerTest extends TestCase
|
||||
@@ -15,8 +20,8 @@ class RegisterControllerTest extends TestCase
|
||||
$response = $this->postJson('/api/register', [
|
||||
'nombre_apellido' => 'Ada Lovelace',
|
||||
'email' => 'ada@example.com',
|
||||
'password' => 'secret123',
|
||||
'password_confirmation' => 'secret123',
|
||||
'password' => 'Secret!123',
|
||||
'password_confirmation' => 'Secret!123',
|
||||
'dni' => '12345678A',
|
||||
'telefono' => '+541122334455',
|
||||
]);
|
||||
@@ -38,7 +43,7 @@ class RegisterControllerTest extends TestCase
|
||||
|
||||
$user = User::query()->where('email', 'ada@example.com')->firstOrFail();
|
||||
|
||||
$this->assertNotSame('secret123', $user->password);
|
||||
$this->assertNotSame('Secret!123', $user->password);
|
||||
}
|
||||
|
||||
public function test_it_registers_a_user_without_optional_fields(): void
|
||||
@@ -46,8 +51,8 @@ class RegisterControllerTest extends TestCase
|
||||
$response = $this->postJson('/api/register', [
|
||||
'nombre_apellido' => 'Alan Turing',
|
||||
'email' => 'alan@example.com',
|
||||
'password' => 'secret123',
|
||||
'password_confirmation' => 'secret123',
|
||||
'password' => 'Secret!123',
|
||||
'password_confirmation' => 'Secret!123',
|
||||
]);
|
||||
|
||||
$response
|
||||
@@ -66,6 +71,50 @@ class RegisterControllerTest extends TestCase
|
||||
]);
|
||||
}
|
||||
|
||||
public function test_it_dispatches_the_welcome_notification_with_tenant_context(): void
|
||||
{
|
||||
Event::fake([UserRegistered::class]);
|
||||
$header = Attachment::query()->create([
|
||||
'path' => 'test/welcome-header.png',
|
||||
'filename' => 'header.png',
|
||||
'type' => AttachmentType::Image,
|
||||
'mime_type' => 'image/png',
|
||||
]);
|
||||
$footer = Attachment::query()->create([
|
||||
'path' => 'test/welcome-footer.png',
|
||||
'filename' => 'footer.png',
|
||||
'type' => AttachmentType::Image,
|
||||
'mime_type' => 'image/png',
|
||||
]);
|
||||
$tenant = Tenant::query()->create([
|
||||
'codigo' => 'welcome-tenant',
|
||||
'nombre' => 'Welcome Tenant',
|
||||
'dominio' => 'welcome.local',
|
||||
'primary_color' => '#000000',
|
||||
'secondary_color' => '#000000',
|
||||
'danger_color' => '#000000',
|
||||
'success_color' => '#000000',
|
||||
'header_bg_color' => '#000000',
|
||||
'footer_bg_color' => '#000000',
|
||||
'header_logo_id' => $header->id,
|
||||
'footer_logo_id' => $footer->id,
|
||||
]);
|
||||
|
||||
$this->postJson('/api/register', [
|
||||
'tenant_codigo' => $tenant->codigo,
|
||||
'nombre_apellido' => 'Grace Hopper',
|
||||
'email' => 'grace@example.com',
|
||||
'password' => 'Secret!123',
|
||||
'password_confirmation' => 'Secret!123',
|
||||
])->assertCreated();
|
||||
|
||||
Event::assertDispatched(
|
||||
UserRegistered::class,
|
||||
fn (UserRegistered $event): bool => $event->tenantCode === $tenant->codigo
|
||||
&& $event->user->email === 'grace@example.com',
|
||||
);
|
||||
}
|
||||
|
||||
public function test_it_validates_required_fields_and_unique_email(): void
|
||||
{
|
||||
User::factory()->create([
|
||||
|
||||
@@ -19,6 +19,7 @@ use App\Domains\Tenant\Models\Tenant;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Illuminate\Support\Facades\Queue;
|
||||
use Illuminate\Support\Str;
|
||||
use Tests\TestCase;
|
||||
|
||||
@@ -32,6 +33,7 @@ class TelepagosWebhookTest extends TestCase
|
||||
|
||||
config(['services.integrations.secret' => 'base64:'.base64_encode(random_bytes(32))]);
|
||||
Cache::flush();
|
||||
Queue::fake();
|
||||
}
|
||||
|
||||
public function test_transfer_payment_intent_requires_a_valid_transfer_payer_dni(): void
|
||||
|
||||
138
tests/Feature/Notification/NotificationMailServiceTest.php
Normal file
138
tests/Feature/Notification/NotificationMailServiceTest.php
Normal file
@@ -0,0 +1,138 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature\Notification;
|
||||
|
||||
use App\Domains\Attachable\Enums\AttachmentType;
|
||||
use App\Domains\Attachable\Models\Attachment;
|
||||
use App\Domains\Auth\Models\User;
|
||||
use App\Domains\Catalog\Models\CatalogItem;
|
||||
use App\Domains\Integration\Models\Integration;
|
||||
use App\Domains\Notification\Services\NotificationMailService;
|
||||
use App\Domains\Purchase\Models\Purchase;
|
||||
use App\Domains\Tenant\Models\Tenant;
|
||||
use App\Domains\Ticket\Models\Ticket;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Mail\Mailable;
|
||||
use Illuminate\Support\Facades\Mail;
|
||||
use Tests\TestCase;
|
||||
|
||||
class NotificationMailServiceTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
private Tenant $tenant;
|
||||
|
||||
private User $user;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
Mail::fake();
|
||||
Integration::query()->create([
|
||||
'integration_code' => 'email',
|
||||
'name' => 'Email',
|
||||
'url' => null,
|
||||
'requires_tenant_configuration' => false,
|
||||
'integration_data_schema' => [],
|
||||
]);
|
||||
$header = Attachment::query()->create([
|
||||
'path' => 'test/mail-header.png',
|
||||
'filename' => 'header.png',
|
||||
'type' => AttachmentType::Image,
|
||||
'mime_type' => 'image/png',
|
||||
]);
|
||||
$footer = Attachment::query()->create([
|
||||
'path' => 'test/mail-footer.png',
|
||||
'filename' => 'footer.png',
|
||||
'type' => AttachmentType::Image,
|
||||
'mime_type' => 'image/png',
|
||||
]);
|
||||
$this->tenant = Tenant::query()->create([
|
||||
'codigo' => 'mail-tenant',
|
||||
'nombre' => 'Mail Tenant',
|
||||
'dominio' => 'mail.local',
|
||||
'primary_color' => '#112233',
|
||||
'secondary_color' => '#000000',
|
||||
'danger_color' => '#000000',
|
||||
'success_color' => '#000000',
|
||||
'header_bg_color' => '#000000',
|
||||
'footer_bg_color' => '#000000',
|
||||
'header_logo_id' => $header->id,
|
||||
'footer_logo_id' => $footer->id,
|
||||
]);
|
||||
$this->user = User::factory()->create([
|
||||
'nombre_apellido' => 'Ada Lovelace',
|
||||
'email' => 'ada@example.com',
|
||||
]);
|
||||
}
|
||||
|
||||
public function test_it_sends_a_branded_welcome_email(): void
|
||||
{
|
||||
app(NotificationMailService::class)->sendWelcome($this->user->id, $this->tenant->codigo);
|
||||
|
||||
Mail::assertSent(Mailable::class, function (Mailable $mail): bool {
|
||||
$mail->assertTo('ada@example.com');
|
||||
$mail->assertHasSubject('Bienvenido a Mail Tenant');
|
||||
|
||||
return str_contains($mail->render(), 'Ada Lovelace')
|
||||
&& str_contains($mail->render(), 'Mail Tenant');
|
||||
});
|
||||
}
|
||||
|
||||
public function test_it_sends_purchase_and_ticket_emails_to_the_purchase_recipient(): void
|
||||
{
|
||||
$purchase = Purchase::query()->create([
|
||||
'tenant_codigo' => $this->tenant->codigo,
|
||||
'user_id' => $this->user->id,
|
||||
'status' => Purchase::STATUS_PAID,
|
||||
'payment_method' => 'transfer',
|
||||
'total' => 25,
|
||||
'email' => 'checkout@example.com',
|
||||
]);
|
||||
$catalogItem = CatalogItem::query()->create([
|
||||
'tenant_code' => $this->tenant->codigo,
|
||||
'slug' => 'entrada',
|
||||
'nombre' => 'Entrada',
|
||||
'descripcion' => 'Entrada general',
|
||||
'precio' => 25,
|
||||
'has_tickets' => true,
|
||||
]);
|
||||
$purchase->items()->create([
|
||||
'source_catalog_item_id' => $catalogItem->id,
|
||||
'nombre' => 'Entrada',
|
||||
'descripcion' => 'Entrada general',
|
||||
'slug' => 'entrada',
|
||||
'item_nombre' => 'Entrada general',
|
||||
'variant_attributes' => [],
|
||||
'cantidad' => 1,
|
||||
'precio_unitario' => 25,
|
||||
'total' => 25,
|
||||
]);
|
||||
$ticket = Ticket::query()->create([
|
||||
'tenant_code' => $this->tenant->codigo,
|
||||
'ticket' => fake()->uuid(),
|
||||
'name' => 'Entrada general',
|
||||
'description' => 'Entrada general',
|
||||
'user_id' => $this->user->id,
|
||||
]);
|
||||
$service = app(NotificationMailService::class);
|
||||
|
||||
$service->sendPurchasePaid($purchase->id);
|
||||
$service->sendTicketsAvailable($purchase->id, [$ticket->id]);
|
||||
|
||||
Mail::assertSent(Mailable::class, 2);
|
||||
Mail::assertSent(Mailable::class, function (Mailable $mail) use ($purchase): bool {
|
||||
$mail->assertTo('checkout@example.com');
|
||||
|
||||
return $mail->subject === "Pago confirmado - Compra #{$purchase->id}"
|
||||
&& str_contains($mail->render(), 'Total pagado');
|
||||
});
|
||||
Mail::assertSent(Mailable::class, function (Mailable $mail): bool {
|
||||
$mail->assertTo('checkout@example.com');
|
||||
|
||||
return $mail->subject === 'Tus tickets ya están disponibles'
|
||||
&& str_contains($mail->render(), 'Entrada general');
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -15,6 +15,7 @@ use App\Domains\Purchase\Models\Purchase;
|
||||
use App\Domains\Purchase\Services\CheckoutService;
|
||||
use App\Domains\Tenant\Models\Tenant;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\Queue;
|
||||
use Illuminate\Support\Str;
|
||||
use Tests\TestCase;
|
||||
|
||||
@@ -22,6 +23,13 @@ class StorePurchaseTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
Queue::fake();
|
||||
}
|
||||
|
||||
public function test_it_creates_a_purchase_from_cart_id_without_persisting_items_yet(): void
|
||||
{
|
||||
$tenant = $this->createTenant('sonder', 'Sonder', 'sonder.com.ar');
|
||||
|
||||
@@ -7,6 +7,7 @@ use App\Domains\Attachable\Models\Attachment;
|
||||
use App\Domains\Auth\Models\User;
|
||||
use App\Domains\Catalog\Enums\CatalogItemType;
|
||||
use App\Domains\Catalog\Models\CatalogItem;
|
||||
use App\Domains\Notification\Events\TicketsAvailable;
|
||||
use App\Domains\Purchase\Models\Purchase;
|
||||
use App\Domains\Tenant\Models\Tenant;
|
||||
use App\Domains\Ticket\Exceptions\TicketGenerationException;
|
||||
@@ -14,6 +15,8 @@ use App\Domains\Ticket\Services\TicketGeneratorService;
|
||||
use Illuminate\Database\Eloquent\Collection as EloquentCollection;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Carbon;
|
||||
use Illuminate\Support\Facades\Event;
|
||||
use Illuminate\Support\Facades\Queue;
|
||||
use Illuminate\Support\Str;
|
||||
use Tests\TestCase;
|
||||
|
||||
@@ -32,6 +35,7 @@ class TicketGeneratorServiceTest extends TestCase
|
||||
parent::setUp();
|
||||
|
||||
Carbon::setTestNow('2026-07-21 10:00:00');
|
||||
Queue::fake();
|
||||
$this->service = app(TicketGeneratorService::class);
|
||||
$this->tenant = $this->createTenant();
|
||||
$this->user = User::factory()->create();
|
||||
@@ -128,6 +132,7 @@ class TicketGeneratorServiceTest extends TestCase
|
||||
|
||||
public function test_marking_a_purchase_as_paid_generates_its_tickets_once(): void
|
||||
{
|
||||
Event::fake([TicketsAvailable::class]);
|
||||
$item = $this->createTicketableItem('paid-ticket');
|
||||
$purchase = $this->createPurchase($item, 2);
|
||||
$purchase->setRelation('items', new EloquentCollection);
|
||||
@@ -136,10 +141,12 @@ class TicketGeneratorServiceTest extends TestCase
|
||||
|
||||
$this->assertSame(Purchase::STATUS_PAID, $purchase->status);
|
||||
$this->assertDatabaseCount('tickets', 2);
|
||||
Event::assertDispatchedTimes(TicketsAvailable::class, 1);
|
||||
|
||||
$purchase->markAsPaid();
|
||||
|
||||
$this->assertDatabaseCount('tickets', 2);
|
||||
Event::assertDispatchedTimes(TicketsAvailable::class, 1);
|
||||
}
|
||||
|
||||
public function test_a_ticket_generated_from_a_purchase_keeps_its_source_ids(): void
|
||||
@@ -157,6 +164,7 @@ class TicketGeneratorServiceTest extends TestCase
|
||||
|
||||
public function test_marking_a_purchase_as_paid_ignores_items_without_tickets(): void
|
||||
{
|
||||
Event::fake([TicketsAvailable::class]);
|
||||
$item = $this->createTicketableItem('regular-product');
|
||||
$item->update(['has_tickets' => false]);
|
||||
$purchase = $this->createPurchase($item->fresh(), 1);
|
||||
@@ -165,6 +173,7 @@ class TicketGeneratorServiceTest extends TestCase
|
||||
|
||||
$this->assertSame(Purchase::STATUS_PAID, $purchase->status);
|
||||
$this->assertDatabaseCount('tickets', 0);
|
||||
Event::assertNotDispatched(TicketsAvailable::class);
|
||||
}
|
||||
|
||||
public function test_paid_status_is_rolled_back_when_ticket_generation_fails(): void
|
||||
|
||||
Reference in New Issue
Block a user