feat(notification): implement email notifications for user registration and purchase events
This commit is contained in:
@@ -4,6 +4,7 @@ namespace App\Domains\Auth\Requests;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Validation\Rule;
|
||||
use Illuminate\Validation\Rules\Password;
|
||||
|
||||
class RegisterUserRequest extends FormRequest
|
||||
{
|
||||
@@ -18,9 +19,10 @@ class RegisterUserRequest extends FormRequest
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'tenant_codigo' => ['nullable', 'string', Rule::exists('tenants', 'codigo')],
|
||||
'nombre_apellido' => ['required', 'string', 'max:255'],
|
||||
'email' => ['required', 'string', 'email', 'max:255', Rule::unique('users', 'email')],
|
||||
'password' => ['required', 'string', 'confirmed', \Illuminate\Validation\Rules\Password::min(8)->mixedCase()->symbols()],
|
||||
'password' => ['required', 'string', 'confirmed', Password::min(8)->mixedCase()->symbols()],
|
||||
'dni' => ['nullable', 'string', 'max:255'],
|
||||
'telefono' => ['nullable', 'string', 'max:255'],
|
||||
];
|
||||
|
||||
@@ -3,20 +3,27 @@
|
||||
namespace App\Domains\Auth\Services;
|
||||
|
||||
use App\Domains\Auth\Models\User;
|
||||
use App\Domains\Notification\Events\UserRegistered;
|
||||
|
||||
class RegisterUserService
|
||||
{
|
||||
/**
|
||||
* @param array{nombre_apellido: string, email: string, password: string, dni?: string|null, telefono?: string|null} $data
|
||||
* @param array{tenant_codigo?: string|null, nombre_apellido: string, email: string, password: string, dni?: string|null, telefono?: string|null} $data
|
||||
*/
|
||||
public function register(array $data): User
|
||||
{
|
||||
return User::query()->create([
|
||||
$user = User::query()->create([
|
||||
'nombre_apellido' => $data['nombre_apellido'],
|
||||
'email' => $data['email'],
|
||||
'password' => $data['password'],
|
||||
'dni' => $data['dni'] ?? null,
|
||||
'telefono' => $data['telefono'] ?? null,
|
||||
]);
|
||||
|
||||
if (! empty($data['tenant_codigo'])) {
|
||||
UserRegistered::dispatch($user, $data['tenant_codigo']);
|
||||
}
|
||||
|
||||
return $user;
|
||||
}
|
||||
}
|
||||
|
||||
20
app/Domains/Notification/Events/TicketsAvailable.php
Normal file
20
app/Domains/Notification/Events/TicketsAvailable.php
Normal file
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domains\Notification\Events;
|
||||
|
||||
use App\Domains\Purchase\Models\Purchase;
|
||||
use Illuminate\Foundation\Events\Dispatchable;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
|
||||
class TicketsAvailable
|
||||
{
|
||||
use Dispatchable, SerializesModels;
|
||||
|
||||
/**
|
||||
* @param array<int, int> $ticketIds
|
||||
*/
|
||||
public function __construct(
|
||||
public readonly Purchase $purchase,
|
||||
public readonly array $ticketIds,
|
||||
) {}
|
||||
}
|
||||
17
app/Domains/Notification/Events/UserRegistered.php
Normal file
17
app/Domains/Notification/Events/UserRegistered.php
Normal file
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domains\Notification\Events;
|
||||
|
||||
use App\Domains\Auth\Models\User;
|
||||
use Illuminate\Foundation\Events\Dispatchable;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
|
||||
class UserRegistered
|
||||
{
|
||||
use Dispatchable, SerializesModels;
|
||||
|
||||
public function __construct(
|
||||
public readonly User $user,
|
||||
public readonly string $tenantCode,
|
||||
) {}
|
||||
}
|
||||
25
app/Domains/Notification/Listeners/SendPurchasePaidEmail.php
Normal file
25
app/Domains/Notification/Listeners/SendPurchasePaidEmail.php
Normal file
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domains\Notification\Listeners;
|
||||
|
||||
use App\Domains\Notification\Services\NotificationMailService;
|
||||
use App\Domains\Purchase\Events\PurchasePaid;
|
||||
use Illuminate\Contracts\Queue\ShouldQueueAfterCommit;
|
||||
use Illuminate\Queue\InteractsWithQueue;
|
||||
|
||||
class SendPurchasePaidEmail implements ShouldQueueAfterCommit
|
||||
{
|
||||
use InteractsWithQueue;
|
||||
|
||||
public string $queue = 'emails';
|
||||
|
||||
public int $tries = 3;
|
||||
|
||||
/** @var array<int, int> */
|
||||
public array $backoff = [30, 120, 300];
|
||||
|
||||
public function handle(PurchasePaid $event): void
|
||||
{
|
||||
app(NotificationMailService::class)->sendPurchasePaid($event->purchase->getKey());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domains\Notification\Listeners;
|
||||
|
||||
use App\Domains\Notification\Events\TicketsAvailable;
|
||||
use App\Domains\Notification\Services\NotificationMailService;
|
||||
use Illuminate\Contracts\Queue\ShouldQueueAfterCommit;
|
||||
use Illuminate\Queue\InteractsWithQueue;
|
||||
|
||||
class SendTicketsAvailableEmail implements ShouldQueueAfterCommit
|
||||
{
|
||||
use InteractsWithQueue;
|
||||
|
||||
public string $queue = 'emails';
|
||||
|
||||
public int $tries = 3;
|
||||
|
||||
/** @var array<int, int> */
|
||||
public array $backoff = [30, 120, 300];
|
||||
|
||||
public function handle(TicketsAvailable $event): void
|
||||
{
|
||||
app(NotificationMailService::class)->sendTicketsAvailable($event->purchase->getKey(), $event->ticketIds);
|
||||
}
|
||||
}
|
||||
25
app/Domains/Notification/Listeners/SendWelcomeEmail.php
Normal file
25
app/Domains/Notification/Listeners/SendWelcomeEmail.php
Normal file
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domains\Notification\Listeners;
|
||||
|
||||
use App\Domains\Notification\Events\UserRegistered;
|
||||
use App\Domains\Notification\Services\NotificationMailService;
|
||||
use Illuminate\Contracts\Queue\ShouldQueueAfterCommit;
|
||||
use Illuminate\Queue\InteractsWithQueue;
|
||||
|
||||
class SendWelcomeEmail implements ShouldQueueAfterCommit
|
||||
{
|
||||
use InteractsWithQueue;
|
||||
|
||||
public string $queue = 'emails';
|
||||
|
||||
public int $tries = 3;
|
||||
|
||||
/** @var array<int, int> */
|
||||
public array $backoff = [30, 120, 300];
|
||||
|
||||
public function handle(UserRegistered $event): void
|
||||
{
|
||||
app(NotificationMailService::class)->sendWelcome($event->user->getKey(), $event->tenantCode);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domains\Notification\Services;
|
||||
|
||||
use App\Domains\Auth\Models\User;
|
||||
use App\Domains\Integration\Services\MailService;
|
||||
use App\Domains\Purchase\Models\Purchase;
|
||||
use App\Domains\Tenant\Models\Tenant;
|
||||
use App\Domains\Ticket\Models\Ticket;
|
||||
use Illuminate\Support\Collection;
|
||||
|
||||
class NotificationMailService
|
||||
{
|
||||
public function __construct(
|
||||
private readonly MailService $mailService,
|
||||
) {}
|
||||
|
||||
public function sendWelcome(int $userId, string $tenantCode): void
|
||||
{
|
||||
$tenant = Tenant::query()->where('codigo', $tenantCode)->firstOrFail();
|
||||
$user = User::query()->findOrFail($userId);
|
||||
|
||||
$this->mailService
|
||||
->forTenant($tenantCode)
|
||||
->send(
|
||||
$user->email,
|
||||
"Bienvenido a {$tenant->nombre}",
|
||||
view('mail.notifications.welcome', compact('tenant', 'user'))->render(),
|
||||
);
|
||||
}
|
||||
|
||||
public function sendPurchasePaid(int $purchaseId): void
|
||||
{
|
||||
$purchase = Purchase::query()
|
||||
->with(['tenant', 'user', 'items'])
|
||||
->findOrFail($purchaseId);
|
||||
|
||||
$this->mailService
|
||||
->forTenant($purchase->tenant_codigo)
|
||||
->send(
|
||||
$this->recipientFor($purchase),
|
||||
"Pago confirmado - Compra #{$purchase->getKey()}",
|
||||
view('mail.notifications.purchase-paid', compact('purchase'))->render(),
|
||||
);
|
||||
}
|
||||
|
||||
/** @param array<int, int> $ticketIds */
|
||||
public function sendTicketsAvailable(int $purchaseId, array $ticketIds): void
|
||||
{
|
||||
$purchase = Purchase::query()->with(['tenant', 'user'])->findOrFail($purchaseId);
|
||||
/** @var Collection<int, Ticket> $tickets */
|
||||
$tickets = Ticket::query()
|
||||
->where('tenant_code', $purchase->tenant_codigo)
|
||||
->where('user_id', $purchase->user_id)
|
||||
->whereKey($ticketIds)
|
||||
->get();
|
||||
|
||||
if ($tickets->isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->mailService
|
||||
->forTenant($purchase->tenant_codigo)
|
||||
->send(
|
||||
$this->recipientFor($purchase),
|
||||
'Tus tickets ya están disponibles',
|
||||
view('mail.notifications.tickets-available', compact('purchase', 'tickets'))->render(),
|
||||
);
|
||||
}
|
||||
|
||||
private function recipientFor(Purchase $purchase): string
|
||||
{
|
||||
return (string) ($purchase->email ?: $purchase->user?->email);
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@
|
||||
namespace App\Domains\Ticket\Listeners;
|
||||
|
||||
use App\Domains\Catalog\Models\CatalogItem;
|
||||
use App\Domains\Notification\Events\TicketsAvailable;
|
||||
use App\Domains\Purchase\Events\PurchasePaid;
|
||||
use App\Domains\Ticket\Exceptions\TicketGenerationException;
|
||||
use App\Domains\Ticket\Services\TicketGeneratorService;
|
||||
@@ -20,6 +21,7 @@ class GenerateTicketsForPaidPurchase
|
||||
->with(['user', 'items'])
|
||||
->findOrFail($event->purchase->getKey());
|
||||
$user = $purchase->user;
|
||||
$ticketIds = [];
|
||||
|
||||
foreach ($purchase->items as $purchaseItem) {
|
||||
$catalogItem = CatalogItem::query()
|
||||
@@ -38,12 +40,18 @@ class GenerateTicketsForPaidPurchase
|
||||
throw TicketGenerationException::purchaseWithoutUser($purchase);
|
||||
}
|
||||
|
||||
$this->ticketGenerator->generate(
|
||||
$generatedTickets = $this->ticketGenerator->generate(
|
||||
$catalogItem,
|
||||
$user,
|
||||
$purchaseItem->cantidad,
|
||||
$purchaseItem->source_variant_id,
|
||||
);
|
||||
|
||||
array_push($ticketIds, ...$generatedTickets->pluck('id')->all());
|
||||
}
|
||||
|
||||
if ($ticketIds !== []) {
|
||||
TicketsAvailable::dispatch($purchase, $ticketIds);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,11 @@
|
||||
|
||||
namespace App\Providers;
|
||||
|
||||
use App\Domains\Notification\Events\TicketsAvailable;
|
||||
use App\Domains\Notification\Events\UserRegistered;
|
||||
use App\Domains\Notification\Listeners\SendPurchasePaidEmail;
|
||||
use App\Domains\Notification\Listeners\SendTicketsAvailableEmail;
|
||||
use App\Domains\Notification\Listeners\SendWelcomeEmail;
|
||||
use App\Domains\Purchase\Events\PurchasePaid;
|
||||
use App\Domains\Ticket\Listeners\GenerateTicketsForPaidPurchase;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
@@ -23,7 +28,10 @@ class AppServiceProvider extends ServiceProvider
|
||||
*/
|
||||
public function boot(): void
|
||||
{
|
||||
Event::listen(PurchasePaid::class, SendPurchasePaidEmail::class);
|
||||
Event::listen(PurchasePaid::class, GenerateTicketsForPaidPurchase::class);
|
||||
Event::listen(TicketsAvailable::class, SendTicketsAvailableEmail::class);
|
||||
Event::listen(UserRegistered::class, SendWelcomeEmail::class);
|
||||
|
||||
Builder::macro('paginateFromRequest', function (int $defaultPerPage = 15, int $maxPerPage = 100, ?int $page = null) {
|
||||
/** @var Builder $this */
|
||||
|
||||
@@ -44,7 +44,7 @@
|
||||
],
|
||||
"dev": [
|
||||
"Composer\\Config::disableProcessTimeout",
|
||||
"npx concurrently -c \"#93c5fd,#c4b5fd,#fb7185,#fdba74\" \"php artisan serve\" \"php artisan queue:listen --tries=1 --timeout=0\" \"php artisan pail --timeout=0\" \"npm run dev\" --names=server,queue,logs,vite --kill-others"
|
||||
"npx concurrently -c \"#93c5fd,#c4b5fd,#fb7185,#fdba74\" \"php artisan serve\" \"php artisan queue:listen --queue=emails,default --tries=1 --timeout=0\" \"php artisan pail --timeout=0\" \"npm run dev\" --names=server,queue,logs,vite --kill-others"
|
||||
],
|
||||
"test": [
|
||||
"@php artisan config:clear --ansi @no_additional_args",
|
||||
|
||||
12
resources/views/mail/notifications/purchase-paid.blade.php
Normal file
12
resources/views/mail/notifications/purchase-paid.blade.php
Normal file
@@ -0,0 +1,12 @@
|
||||
<h1 style="margin: 0 0 20px;">¡Recibimos tu pago!</h1>
|
||||
<p>La compra <strong>#{{ $purchase->id }}</strong> fue confirmada correctamente.</p>
|
||||
<table role="presentation" style="width: 100%; border-collapse: collapse; margin: 20px 0;">
|
||||
@foreach ($purchase->items as $item)
|
||||
<tr>
|
||||
<td style="padding: 8px 0; border-bottom: 1px solid #e2e8f0;">{{ $item->item_nombre }}</td>
|
||||
<td style="padding: 8px 0; border-bottom: 1px solid #e2e8f0; text-align: center;">× {{ $item->cantidad }}</td>
|
||||
<td style="padding: 8px 0; border-bottom: 1px solid #e2e8f0; text-align: right;">${{ number_format((float) $item->total, 2, ',', '.') }}</td>
|
||||
</tr>
|
||||
@endforeach
|
||||
</table>
|
||||
<p style="font-size: 18px;"><strong>Total pagado: ${{ number_format((float) $purchase->total, 2, ',', '.') }}</strong></p>
|
||||
@@ -0,0 +1,7 @@
|
||||
<h1 style="margin: 0 0 20px;">Tus tickets ya están disponibles</h1>
|
||||
<p>Generamos {{ $tickets->count() }} {{ $tickets->count() === 1 ? 'ticket' : 'tickets' }} para la compra <strong>#{{ $purchase->id }}</strong>.</p>
|
||||
<ul style="padding-left: 20px;">
|
||||
@foreach ($tickets as $ticket)
|
||||
<li style="margin-bottom: 8px;">{{ $ticket->name }}</li>
|
||||
@endforeach
|
||||
</ul>
|
||||
3
resources/views/mail/notifications/welcome.blade.php
Normal file
3
resources/views/mail/notifications/welcome.blade.php
Normal file
@@ -0,0 +1,3 @@
|
||||
<h1 style="margin: 0 0 20px;">¡Bienvenido a {{ $tenant->nombre }}!</h1>
|
||||
<p>Hola {{ $user->nombre_apellido }}, tu cuenta fue creada correctamente.</p>
|
||||
<p>Ya podés ingresar y comenzar a comprar.</p>
|
||||
@@ -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