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>
56 lines
1.5 KiB
PHP
56 lines
1.5 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace Database\Factories;
|
|
|
|
use App\Models\Reminder;
|
|
use App\Models\Tenant;
|
|
use App\Models\User;
|
|
use Illuminate\Database\Eloquent\Factories\Factory;
|
|
use Illuminate\Support\Carbon;
|
|
|
|
/**
|
|
* @extends Factory<Reminder>
|
|
*/
|
|
class ReminderFactory extends Factory
|
|
{
|
|
public function definition(): array
|
|
{
|
|
// remind_at +1 час по умолчанию (in future, неотправлено).
|
|
// Тесты для overdue/completed-flow явно ставят remind_at и completed_at.
|
|
$tenant = Tenant::factory();
|
|
|
|
return [
|
|
'tenant_id' => $tenant,
|
|
'deal_id' => fake()->numberBetween(1, 999999), // deal_id без FK
|
|
'text' => fake()->sentence(),
|
|
'remind_at' => Carbon::now()->addHour(),
|
|
'created_by' => User::factory()->state(fn (array $attrs, Reminder $r) => ['tenant_id' => $r->tenant_id]),
|
|
'assignee_id' => null,
|
|
'is_sent' => false,
|
|
];
|
|
}
|
|
|
|
public function overdue(): static
|
|
{
|
|
return $this->state(['remind_at' => Carbon::now()->subHours(2)]);
|
|
}
|
|
|
|
public function completed(): static
|
|
{
|
|
return $this->state([
|
|
'completed_at' => Carbon::now()->subMinutes(10),
|
|
'remind_at' => Carbon::now()->subHour(),
|
|
]);
|
|
}
|
|
|
|
public function sent(): static
|
|
{
|
|
return $this->state([
|
|
'is_sent' => true,
|
|
'sent_at' => Carbon::now()->subMinute(),
|
|
]);
|
|
}
|
|
}
|