feat(schedule): add schedule:bootstrap and minute-precise automation runs

Install per-module schedule:run cron from artisan, store sms/in_app send minutes, and match scheduled triggers at exact HH:mm Tehran time.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-08-24 22:13:06 +03:30
co-authored by Cursor
parent 9bb0fa3a1d
commit 1c8ab4b725
7 changed files with 214 additions and 17 deletions
+21 -11
View File
@@ -384,14 +384,25 @@ class AutomationEngine
private function hourMatches(Automation $auto, string $channel): bool
{
$h = $channel === AutomationCatalog::CHANNEL_SMS
? $auto->sms_send_hour
: $auto->in_app_send_hour;
if ($h === null) {
[$hour, $minute] = $this->scheduledTime($auto, $channel);
if ($hour === null) {
return true;
}
return (int) now('Asia/Tehran')->hour === (int) $h;
$now = now('Asia/Tehran');
return (int) $now->hour === (int) $hour
&& (int) $now->minute === (int) ($minute ?? 0);
}
/** @return array{0: int|null, 1: int|null} */
private function scheduledTime(Automation $auto, string $channel): array
{
if ($channel === AutomationCatalog::CHANNEL_SMS) {
return [$auto->sms_send_hour, $auto->sms_send_minute];
}
return [$auto->in_app_send_hour, $auto->in_app_send_minute];
}
private function alreadyRanThisHour(Automation $auto, string $channel): bool
@@ -419,17 +430,16 @@ class AutomationEngine
private function snapToHour(Automation $auto, string $channel, Carbon $after): Carbon
{
$h = $channel === AutomationCatalog::CHANNEL_SMS
? $auto->sms_send_hour
: $auto->in_app_send_hour;
if ($h === null) {
[$hour, $minute] = $this->scheduledTime($auto, $channel);
if ($hour === null) {
return $after;
}
$local = $after->copy()->timezone('Asia/Tehran');
if ((int) $local->hour === (int) $h) {
$targetMinute = (int) ($minute ?? 0);
if ((int) $local->hour === (int) $hour && (int) $local->minute === $targetMinute) {
return $after;
}
$target = $local->copy()->setTime((int) $h, 0, 0);
$target = $local->copy()->setTime((int) $hour, $targetMinute, 0);
if ($target->lt($local)) {
$target->addDay();
}