Skip to content

Commit 3a3f050

Browse files
author
Itamar Junior
committed
feat: Implement reminder notifications for core CRO obligations and enhance compliance tracking
1 parent 047c7e6 commit 3a3f050

14 files changed

Lines changed: 531 additions & 25 deletions
Lines changed: 218 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,218 @@
1+
<?php
2+
3+
namespace App\Console\Commands;
4+
5+
use App\Models\Business;
6+
use App\Models\User;
7+
use App\Models\UserNotificationSetting;
8+
use App\Notifications\CompanyObligationReminderNotification;
9+
use Illuminate\Console\Command;
10+
use Illuminate\Support\Carbon;
11+
use Illuminate\Support\Facades\DB;
12+
13+
class SendCompanyObligationReminders extends Command
14+
{
15+
protected $signature = 'compliance:send-reminders {--business= : Restrict to a business id}';
16+
17+
protected $description = 'Send countdown and overdue escalation reminders for core CRO obligations';
18+
19+
/**
20+
* @var array<int, string>
21+
*/
22+
protected array $coreCodes = ['B1', 'B10', 'B2', 'AGM'];
23+
24+
/**
25+
* Execute the console command.
26+
*/
27+
public function handle(): int
28+
{
29+
$today = now()->startOfDay();
30+
$businessId = $this->option('business');
31+
32+
$businesses = Business::query()
33+
->when($businessId, fn ($q) => $q->where('id', (int) $businessId))
34+
->get();
35+
36+
if ($businesses->isEmpty()) {
37+
$this->warn('No business found for reminder processing.');
38+
return self::SUCCESS;
39+
}
40+
41+
$sent = 0;
42+
43+
foreach ($businesses as $business) {
44+
$users = $business->users()->get();
45+
if ($users->isEmpty()) {
46+
continue;
47+
}
48+
49+
$obligations = DB::table('company_cro_document as p')
50+
->join('companies as c', 'c.id', '=', 'p.company_id')
51+
->join('cro_doc_definitions as d', 'd.id', '=', 'p.cro_doc_definition_id')
52+
->where('c.business_id', $business->id)
53+
->where(function ($q) {
54+
$q->where('c.active', true)->orWhereNull('c.active');
55+
})
56+
->whereIn('d.code', $this->coreCodes)
57+
->whereNotNull('p.due_date')
58+
->select([
59+
'c.id as company_id',
60+
'c.name as company_name',
61+
'c.company_number',
62+
'd.id as definition_id',
63+
'd.code as doc_code',
64+
'd.name as doc_name',
65+
'p.status',
66+
'p.risk_level',
67+
'p.due_date',
68+
])
69+
->get();
70+
71+
foreach ($obligations as $row) {
72+
$dueDate = Carbon::parse((string) $row->due_date)->startOfDay();
73+
$daysUntilDue = $today->diffInDays($dueDate, false);
74+
75+
$countdownDays = [60, 30, 7];
76+
foreach ($countdownDays as $days) {
77+
$triggerDate = $dueDate->copy()->subDays($days);
78+
79+
if ($today->lt($triggerDate) || $daysUntilDue < 0) {
80+
continue;
81+
}
82+
83+
foreach ($users as $user) {
84+
if (!$this->isPreferenceEnabled($user, 'document_deadlines')) {
85+
continue;
86+
}
87+
88+
$type = "countdown_{$days}";
89+
if ($this->wasReminderSent((int) $user->id, (int) $row->company_id, (int) $row->definition_id, $type, $triggerDate)) {
90+
continue;
91+
}
92+
93+
$payload = [
94+
'type' => $type,
95+
'company_id' => (int) $row->company_id,
96+
'company_name' => (string) $row->company_name,
97+
'company_number' => (string) $row->company_number,
98+
'doc_code' => (string) $row->doc_code,
99+
'doc_name' => (string) $row->doc_name,
100+
'due_date' => $dueDate->toDateString(),
101+
'days_remaining' => $daysUntilDue,
102+
'title' => "{$row->doc_code} deadline in {$daysUntilDue} days",
103+
'message' => "{$row->doc_code} for {$row->company_name} is due on {$dueDate->format('d M Y')}.",
104+
'risk_level' => (string) $row->risk_level,
105+
'status' => (string) $row->status,
106+
];
107+
108+
$user->notify(new CompanyObligationReminderNotification($payload));
109+
$this->storeSentReminder((int) $user->id, (int) $row->company_id, (int) $row->definition_id, $type, $triggerDate, $dueDate);
110+
$sent++;
111+
}
112+
}
113+
114+
if ($daysUntilDue >= 0) {
115+
continue;
116+
}
117+
118+
$overdueDays = abs($daysUntilDue);
119+
$escalationThresholds = [
120+
1 => 'overdue_1',
121+
7 => 'overdue_7',
122+
30 => 'overdue_30',
123+
];
124+
125+
foreach ($escalationThresholds as $threshold => $type) {
126+
if ($overdueDays < $threshold) {
127+
continue;
128+
}
129+
130+
$triggerDate = $dueDate->copy()->addDays($threshold);
131+
132+
foreach ($users as $user) {
133+
if (!$this->isPreferenceEnabled($user, 'overdue_escalation')) {
134+
continue;
135+
}
136+
137+
if ($this->wasReminderSent((int) $user->id, (int) $row->company_id, (int) $row->definition_id, $type, $triggerDate)) {
138+
continue;
139+
}
140+
141+
$payload = [
142+
'type' => $type,
143+
'company_id' => (int) $row->company_id,
144+
'company_name' => (string) $row->company_name,
145+
'company_number' => (string) $row->company_number,
146+
'doc_code' => (string) $row->doc_code,
147+
'doc_name' => (string) $row->doc_name,
148+
'due_date' => $dueDate->toDateString(),
149+
'days_overdue' => $overdueDays,
150+
'title' => "{$row->doc_code} overdue ({$overdueDays} days)",
151+
'message' => "{$row->doc_code} for {$row->company_name} is overdue by {$overdueDays} days.",
152+
'risk_level' => (string) $row->risk_level,
153+
'status' => (string) $row->status,
154+
];
155+
156+
$user->notify(new CompanyObligationReminderNotification($payload));
157+
$this->storeSentReminder((int) $user->id, (int) $row->company_id, (int) $row->definition_id, $type, $triggerDate, $dueDate);
158+
$sent++;
159+
}
160+
}
161+
}
162+
}
163+
164+
$this->info("Compliance reminders processed. Notifications sent: {$sent}");
165+
return self::SUCCESS;
166+
}
167+
168+
protected function isPreferenceEnabled(User $user, string $key): bool
169+
{
170+
$setting = UserNotificationSetting::firstOrCreate(
171+
['user_id' => $user->id, 'notification_key' => $key],
172+
['is_enabled' => true]
173+
);
174+
175+
return (bool) $setting->is_enabled;
176+
}
177+
178+
protected function wasReminderSent(
179+
int $userId,
180+
int $companyId,
181+
int $definitionId,
182+
string $type,
183+
Carbon $triggerDate
184+
): bool {
185+
return DB::table('company_obligation_reminders')
186+
->where('user_id', $userId)
187+
->where('company_id', $companyId)
188+
->where('cro_doc_definition_id', $definitionId)
189+
->where('reminder_type', $type)
190+
->whereDate('trigger_date', $triggerDate->toDateString())
191+
->exists();
192+
}
193+
194+
protected function storeSentReminder(
195+
int $userId,
196+
int $companyId,
197+
int $definitionId,
198+
string $type,
199+
Carbon $triggerDate,
200+
Carbon $dueDate
201+
): void {
202+
DB::table('company_obligation_reminders')->upsert(
203+
[[
204+
'user_id' => $userId,
205+
'company_id' => $companyId,
206+
'cro_doc_definition_id' => $definitionId,
207+
'reminder_type' => $type,
208+
'trigger_date' => $triggerDate->toDateString(),
209+
'due_date' => $dueDate->toDateString(),
210+
'sent_at' => now(),
211+
'created_at' => now(),
212+
'updated_at' => now(),
213+
]],
214+
['user_id', 'company_id', 'cro_doc_definition_id', 'reminder_type', 'trigger_date'],
215+
['due_date', 'sent_at', 'updated_at']
216+
);
217+
}
218+
}

0 commit comments

Comments
 (0)