39b6127bce
Закрыт пункт «Reminders ⏸ no-view» из AppLayout nav-tree. Schema-таблица
reminders уже была в v8.10 §17.5 — теперь работает целиком backend-side.
Backend:
- App\Models\Reminder — Eloquent с casts/relations + isCompleted/isOverdue.
- ReminderFactory с states overdue/completed/sent.
- App\Http\Controllers\Api\ReminderController под auth:sanctum:
GET ?filter=&deal_id=&limit= (active/today/upcoming/overdue/completed,
окно ±1 день, counts для UI badges);
POST {deal_id, text?, remind_at, assignee_id?} (FK guard на assignee);
PATCH {id} (при смене remind_at сбрасывает is_sent+sent_at для retrigger);
POST {id}/complete (idempotent);
DELETE {id}.
RLS-обёртка + defense-in-depth where('tenant_id').
- App\Mail\ReminderDueNotification + emails/reminder.blade.php (Forest,
TZ из recipient.timezone).
- NotificationService::notifyReminder(Reminder) — recipient = assignee_id
?? created_by (если active+!deleted). Каналы email+inapp по prefs.
payload {reminder_id, deal_id} для UI deep-link.
- App\Console\Commands\RemindersDispatchDue — cron reminders:dispatch-due
{--dry-run} {--limit=500}. По одному reminder в DB::transaction (SET
LOCAL app.current_tenant_id нельзя переключать). После notifyReminder
ставит is_sent=true даже если recipient deactivated (защита от retry-spam).
Pest +32 (347/347 за 41.21 сек, 1203 assertions):
- ReminderControllerTest 21: 401 / RLS / 5 filter'ов / counts / deal_id /
store + FK guard / update text+remind_at сбрасывает is_sent / complete
idempotent / delete + 404 чужой.
- RemindersDispatchDueTest 11: due → email+inapp / future skip / completed
skip / уже sent / assignee вместо created_by / deactivated user (is_sent
всё равно) / только inapp при email=false / --dry-run / --limit / RLS.
PHPStan baseline регенерирован. IDE-helper для всех моделей.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
55 lines
1.5 KiB
PHP
55 lines
1.5 KiB
PHP
<?php
|
||
|
||
declare(strict_types=1);
|
||
|
||
namespace App\Mail;
|
||
|
||
use App\Models\Reminder;
|
||
use App\Models\User;
|
||
use Illuminate\Bus\Queueable;
|
||
use Illuminate\Mail\Mailable;
|
||
use Illuminate\Mail\Mailables\Content;
|
||
use Illuminate\Mail\Mailables\Envelope;
|
||
use Illuminate\Queue\SerializesModels;
|
||
|
||
/**
|
||
* Email-уведомление о наступлении срока напоминания (ТЗ §18.5, событие reminder).
|
||
*
|
||
* Триггер: cron-команда `reminders:dispatch-due` находит rows с
|
||
* `is_sent=false AND completed_at IS NULL AND remind_at <= NOW()`,
|
||
* вызывает NotificationService::notifyReminder для каждой,
|
||
* затем ставит `is_sent=true, sent_at=NOW()`.
|
||
*/
|
||
class ReminderDueNotification extends Mailable
|
||
{
|
||
use Queueable;
|
||
use SerializesModels;
|
||
|
||
public function __construct(
|
||
public User $recipient,
|
||
public Reminder $reminder,
|
||
) {}
|
||
|
||
public function envelope(): Envelope
|
||
{
|
||
$shortText = $this->reminder->text
|
||
? mb_substr($this->reminder->text, 0, 60)
|
||
: 'Срок касания клиента';
|
||
|
||
return new Envelope(
|
||
subject: "Лидерра. Напоминание — {$shortText}",
|
||
);
|
||
}
|
||
|
||
public function content(): Content
|
||
{
|
||
return new Content(
|
||
view: 'emails.reminder',
|
||
with: [
|
||
'recipient' => $this->recipient,
|
||
'reminder' => $this->reminder,
|
||
],
|
||
);
|
||
}
|
||
}
|