feat реклама за показы: письмо клиенту и колокольчик на ответ Яндекса
This commit is contained in:
@@ -0,0 +1,44 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Mail;
|
||||
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Mail\Mailable;
|
||||
use Illuminate\Mail\Mailables\Content;
|
||||
use Illuminate\Mail\Mailables\Envelope;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
|
||||
/**
|
||||
* Письмо клиенту: по его рекламной кампании пришёл ответ Яндекса.
|
||||
* Получателей ставит вызывающий код через Mail::to($email)->queue(new ...).
|
||||
*/
|
||||
final class AdModerationMessageMail extends Mailable
|
||||
{
|
||||
use Queueable;
|
||||
use SerializesModels;
|
||||
|
||||
public function __construct(
|
||||
public readonly string $campaignName,
|
||||
public readonly int $campaignId,
|
||||
public readonly string $body,
|
||||
) {}
|
||||
|
||||
public function envelope(): Envelope
|
||||
{
|
||||
return new Envelope(subject: 'Ответ Яндекса по рекламной кампании «'.$this->campaignName.'»');
|
||||
}
|
||||
|
||||
public function content(): Content
|
||||
{
|
||||
return new Content(
|
||||
view: 'mail.ad-moderation-message',
|
||||
with: [
|
||||
'campaignName' => $this->campaignName,
|
||||
'campaignId' => $this->campaignId,
|
||||
'body' => $this->body,
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -4,8 +4,14 @@ declare(strict_types=1);
|
||||
|
||||
namespace App\Services\Advertising;
|
||||
|
||||
use App\Mail\AdModerationMessageMail;
|
||||
use App\Models\AdCampaign;
|
||||
use App\Models\AdCampaignMessage;
|
||||
use App\Models\User;
|
||||
use App\Services\NotificationService;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Illuminate\Support\Facades\Mail;
|
||||
use Throwable;
|
||||
|
||||
/**
|
||||
* Единственное место, где создаются сообщения ленты кампании. Здесь же защита от дублей
|
||||
@@ -14,6 +20,8 @@ use App\Models\AdCampaignMessage;
|
||||
*/
|
||||
class CampaignMessageService
|
||||
{
|
||||
public function __construct(private readonly NotificationService $notifications) {}
|
||||
|
||||
/**
|
||||
* Слова Яндекса. Дубль по паре «кампания + баннер» не пишем: опрос модерации бежит
|
||||
* по расписанию и до перемены решения присылает одну и ту же причину при каждом
|
||||
@@ -55,6 +63,64 @@ class CampaignMessageService
|
||||
$message->setConnection($connection);
|
||||
$message->save();
|
||||
|
||||
$this->notify($campaign, $body);
|
||||
|
||||
return $message;
|
||||
}
|
||||
|
||||
/** Служебная отметка портала: расписка, а не новость — клиента ею не дёргаем. */
|
||||
public function postSystem(AdCampaign $campaign, string $body): ?AdCampaignMessage
|
||||
{
|
||||
$body = trim($body);
|
||||
if ($body === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
$message = new AdCampaignMessage([
|
||||
'tenant_id' => (int) $campaign->tenant_id,
|
||||
'campaign_id' => (int) $campaign->id,
|
||||
'author' => AdCampaignMessage::AUTHOR_SYSTEM,
|
||||
'body' => $body,
|
||||
]);
|
||||
$message->setConnection($campaign->getConnectionName());
|
||||
$message->save();
|
||||
|
||||
return $message;
|
||||
}
|
||||
|
||||
/**
|
||||
* Письмо и колокольчик всем живым пользователям тенанта. Внутри всё под Throwable:
|
||||
* упавшая почта не должна стирать саму новость — сообщение уже в ленте, клиент
|
||||
* увидит его, когда зайдёт.
|
||||
*/
|
||||
private function notify(AdCampaign $campaign, string $body): void
|
||||
{
|
||||
try {
|
||||
$users = User::query()
|
||||
->where('tenant_id', $campaign->tenant_id)
|
||||
->where('is_active', true)
|
||||
->whereNull('deleted_at')
|
||||
->get();
|
||||
|
||||
foreach ($users as $user) {
|
||||
$this->notifications->notifyInApp(
|
||||
$user,
|
||||
'ad_moderation',
|
||||
'Ответ Яндекса по рекламе',
|
||||
mb_substr($body, 0, 500),
|
||||
['campaign_id' => (int) $campaign->id],
|
||||
);
|
||||
|
||||
if (is_string($user->email) && $user->email !== '') {
|
||||
Mail::to($user->email)->queue(new AdModerationMessageMail(
|
||||
(string) $campaign->name, (int) $campaign->id, $body,
|
||||
));
|
||||
}
|
||||
}
|
||||
} catch (Throwable $e) {
|
||||
Log::warning('Не смогли уведомить клиента об ответе Яндекса: '.$e->getMessage(), [
|
||||
'campaign' => $campaign->id,
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
<p>По вашей рекламной кампании «{{ $campaignName }}» пришёл ответ Яндекса:</p>
|
||||
|
||||
<blockquote style="border-left: 3px solid #0F6E56; padding-left: 12px; margin: 16px 0;">
|
||||
{{ $body }}
|
||||
</blockquote>
|
||||
|
||||
<p>Открыть кампанию в личном кабинете и ответить: раздел «Реклама» → кампания «{{ $campaignName }}».</p>
|
||||
|
||||
<p style="color: #666; font-size: 13px;">Это письмо отправлено автоматически, отвечать на него не нужно.</p>
|
||||
@@ -0,0 +1,64 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use App\Mail\AdModerationMessageMail;
|
||||
use App\Models\AdCampaign;
|
||||
use App\Models\InAppNotification;
|
||||
use App\Models\Tenant;
|
||||
use App\Models\User;
|
||||
use App\Services\Advertising\CampaignMessageService;
|
||||
use Illuminate\Support\Facades\Mail;
|
||||
|
||||
function notifyCampaign(): array
|
||||
{
|
||||
$tenant = Tenant::factory()->create();
|
||||
$user = User::factory()->create(['tenant_id' => $tenant->id, 'is_active' => true]);
|
||||
$campaign = AdCampaign::create([
|
||||
'tenant_id' => $tenant->id, 'name' => 'C', 'audience_days' => 10, 'use_uploaded_list' => false,
|
||||
]);
|
||||
|
||||
return [$tenant, $user, $campaign];
|
||||
}
|
||||
|
||||
it('на слова Яндекса клиенту уходит письмо и загорается колокольчик', function () {
|
||||
Mail::fake();
|
||||
|
||||
[$tenant, $user, $campaign] = notifyCampaign();
|
||||
|
||||
app(CampaignMessageService::class)->postFromYandex($campaign, null, 'Изображение не подошло');
|
||||
|
||||
Mail::assertQueued(AdModerationMessageMail::class, fn ($mail) => $mail->hasTo($user->email));
|
||||
|
||||
expect(InAppNotification::where('tenant_id', $tenant->id)->where('user_id', $user->id)->count())->toBe(1);
|
||||
});
|
||||
|
||||
/**
|
||||
* Служебная отметка «документ отправлен в Яндекс» — расписка, а не новость.
|
||||
* Дёргать ею клиента незачем.
|
||||
*/
|
||||
it('на служебную отметку портала письмо не уходит', function () {
|
||||
Mail::fake();
|
||||
|
||||
[$tenant, , $campaign] = notifyCampaign();
|
||||
|
||||
app(CampaignMessageService::class)->postSystem($campaign, 'Документ отправлен в Яндекс');
|
||||
|
||||
Mail::assertNothingQueued();
|
||||
expect(InAppNotification::where('tenant_id', $tenant->id)->count())->toBe(0);
|
||||
});
|
||||
|
||||
/**
|
||||
* Почта — вещь ненадёжная. Если письмо не ушло, сообщение всё равно обязано остаться
|
||||
* в ленте: клиент увидит его, когда зайдёт. Иначе один сбой почты стирает саму новость.
|
||||
*/
|
||||
it('упавшая почта не мешает сообщению лечь в ленту', function () {
|
||||
[, , $campaign] = notifyCampaign();
|
||||
|
||||
Mail::shouldReceive('to')->andThrow(new RuntimeException('почта легла'));
|
||||
|
||||
$message = app(CampaignMessageService::class)->postFromYandex($campaign, null, 'Изображение не подошло');
|
||||
|
||||
expect($message)->not->toBeNull()
|
||||
->and($message->exists)->toBeTrue();
|
||||
});
|
||||
Reference in New Issue
Block a user