feat(import): Mailable ImportCompletedNotification

Task 8 — email-уведомление пользователю по завершении CSV-импорта
исторических лидов (ТЗ §6.6). Два исхода: done (счётчики строк) /
failed (сообщение об ошибке). Blade-шаблон markdown.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Дмитрий
2026-05-16 19:23:00 +03:00
parent 95be14559a
commit 4f6739aef1
4 changed files with 143 additions and 0 deletions
@@ -0,0 +1,49 @@
<?php
declare(strict_types=1);
namespace App\Mail;
use App\Models\ImportLog;
use Illuminate\Bus\Queueable;
use Illuminate\Mail\Mailable;
use Illuminate\Mail\Mailables\Content;
use Illuminate\Mail\Mailables\Envelope;
use Illuminate\Queue\SerializesModels;
/**
* Уведомление о завершении CSV-импорта исторических лидов (ТЗ §6.6).
*/
class ImportCompletedNotification extends Mailable
{
use Queueable;
use SerializesModels;
/**
* @param string $outcome 'done' | 'failed'
*/
public function __construct(
public ImportLog $log,
public string $outcome,
) {}
public function envelope(): Envelope
{
$subject = $this->outcome === 'done'
? 'Импорт данных завершён — Лидерра'
: 'Импорт данных не удался — Лидерра';
return new Envelope(subject: $subject);
}
public function content(): Content
{
return new Content(
markdown: 'mail.import-completed',
with: [
'log' => $this->log,
'outcome' => $this->outcome,
],
);
}
}
+12
View File
@@ -1026,6 +1026,18 @@ parameters:
count: 18
path: tests/Feature/Import/HistoricalImportServiceTest.php
-
message: '#^Access to an undefined property Pest\\PendingCalls\\TestCall\:\:\$tenant\.$#'
identifier: property.notFound
count: 3
path: tests/Feature/Import/ImportCompletedNotificationTest.php
-
message: '#^Access to an undefined property Pest\\PendingCalls\\TestCall\:\:\$user\.$#'
identifier: property.notFound
count: 3
path: tests/Feature/Import/ImportCompletedNotificationTest.php
-
message: '#^Access to an undefined property Pest\\PendingCalls\\TestCall\:\:\$tenant\.$#'
identifier: property.notFound
@@ -0,0 +1,33 @@
<x-mail::message>
@if ($outcome === 'done')
# Импорт завершён
Импорт файла **{{ $log->filename }}** успешно завершён.
| Показатель | Значение |
|:-----------|---------:|
| Добавлено сделок | {{ $log->rows_added }} |
| Обновлено сделок | {{ $log->rows_updated }} |
| Пропущено строк | {{ $log->rows_skipped }} |
| Неизвестных статусов | {{ $log->unknown_statuses_count }} |
@if ($log->unknown_statuses_count > 0)
Обнаружены неизвестные статусы воронки замапьте их вручную на экране «Импорт данных».
@endif
@else
# Импорт не удался
Импорт файла **{{ $log->filename }}** завершился ошибкой:
> {{ $log->error_message }}
Проверьте формат файла и повторите загрузку на экране «Импорт данных».
@endif
<x-mail::button :url="config('app.url').'/import'">
Открыть «Импорт данных»
</x-mail::button>
С уважением,<br>
Лидерра
</x-mail::message>
@@ -0,0 +1,49 @@
<?php
declare(strict_types=1);
use App\Mail\ImportCompletedNotification;
use App\Models\ImportLog;
use App\Models\Tenant;
use App\Models\User;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Illuminate\Support\Facades\DB;
uses(DatabaseTransactions::class);
beforeEach(function (): void {
$this->tenant = Tenant::factory()->create();
$this->user = User::factory()->for($this->tenant)->create();
DB::statement('SET app.current_tenant_id = '.$this->tenant->id);
});
test('письмо об успешном импорте содержит счётчики', function (): void {
$log = ImportLog::factory()->create([
'tenant_id' => $this->tenant->id,
'user_id' => $this->user->id,
'status' => 'done',
'rows_added' => 120,
'rows_updated' => 8,
'rows_skipped' => 2,
]);
$rendered = (new ImportCompletedNotification($log, 'done'))->render();
expect($rendered)->toContain('120')
->and($rendered)->toContain('Импорт завершён');
});
test('письмо о неуспешном импорте сообщает об ошибке', function (): void {
$log = ImportLog::factory()->create([
'tenant_id' => $this->tenant->id,
'user_id' => $this->user->id,
'status' => 'failed',
'error_message' => 'Файл повреждён',
]);
$mailable = new ImportCompletedNotification($log, 'failed');
$rendered = $mailable->render();
expect($rendered)->toContain('Файл повреждён')
->and($mailable->envelope()->subject)->toContain('не удался');
});