Files
portal/app/app/Models/Reminder.php
T
Дмитрий 39b6127bce phase2(reminders-backend): CRUD + cron-диспетчер + email/inapp (P0 этап 4)
Закрыт пункт «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>
2026-05-09 12:30:38 +03:00

89 lines
2.5 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
declare(strict_types=1);
namespace App\Models;
use Database\Factories\ReminderFactory;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
/**
* Напоминание на сделке (schema v8.10 §17.5).
*
* Tenant-aware модель с RLS. Композитные индексы:
* - idx_reminders_due (remind_at) WHERE is_sent=FALSE AND completed_at IS NULL — cron;
* - idx_reminders_deal (deal_id) — UI карточки сделки;
* - idx_reminders_tenant_user_active — дашборд «today/last/future».
*
* deal_id БЕЗ FK (deals партиционирована, FK на partitioned-родительскую
* таблицу не поддерживается без partition-key в составе).
*
* MVP: assignee_id всегда NULL — паритет с histories[].to оригинала. Поле
* зарезервировано для Post-MVP (multi-assignee).
*
* @mixin IdeHelperReminder
*/
class Reminder extends Model
{
/** @use HasFactory<ReminderFactory> */
use HasFactory;
protected $fillable = [
'tenant_id',
'deal_id',
'text',
'remind_at',
'created_by',
'assignee_id',
'completed_at',
'is_sent',
'sent_at',
];
protected function casts(): array
{
return [
'tenant_id' => 'integer',
'deal_id' => 'integer',
'created_by' => 'integer',
'assignee_id' => 'integer',
'is_sent' => 'boolean',
'remind_at' => 'datetime',
'completed_at' => 'datetime',
'sent_at' => 'datetime',
'created_at' => 'datetime',
'updated_at' => 'datetime',
];
}
/** @return BelongsTo<Tenant, $this> */
public function tenant(): BelongsTo
{
return $this->belongsTo(Tenant::class);
}
/** @return BelongsTo<User, $this> */
public function creator(): BelongsTo
{
return $this->belongsTo(User::class, 'created_by');
}
/** @return BelongsTo<User, $this> */
public function assignee(): BelongsTo
{
return $this->belongsTo(User::class, 'assignee_id');
}
public function isCompleted(): bool
{
return $this->completed_at !== null;
}
public function isOverdue(): bool
{
return $this->completed_at === null && $this->remind_at < now();
}
}