Files
budget/app/ReminderService.php
T
Ty Clifford b7eaa81501 -
2026-06-14 15:25:31 -04:00

100 lines
4.2 KiB
PHP

<?php
declare(strict_types=1);
namespace Budget;
use DateTimeImmutable;
use PDO;
final class ReminderService
{
public function __construct(private PDO $pdo, private Settings $settings, private Mailgun $mailgun)
{
}
public function run(bool $force = false): array
{
if (!(bool) $this->settings->get('modules.alerts', true)) {
return ['sent' => 0, 'skipped' => 0, 'errors' => ['Alerts module is disabled.']];
}
$times = (array) $this->settings->get('alerts.times', ['09:00']);
if (!$force && !in_array(date('H:i'), $times, true)) {
return ['sent' => 0, 'skipped' => 0, 'errors' => []];
}
$recipient = (string) $this->settings->get('alerts.email', '');
$windows = array_map('intval', (array) $this->settings->get('alerts.windows', [7, 3, 1]));
$today = new DateTimeImmutable('today');
$sent = 0;
$skipped = 0;
$errors = [];
foreach ($windows as $window) {
$target = $today->modify('+' . max(0, $window) . ' days')->format('Y-m-d');
$statement = $this->pdo->prepare(
'SELECT bi.*, c.name AS category_name
FROM budget_items bi
LEFT JOIN categories c ON c.id = bi.category_id
WHERE bi.due_date = :due_date AND bi.status = :status'
);
$statement->execute(['due_date' => $target, 'status' => 'active']);
foreach ($statement->fetchAll() as $item) {
if ($this->alreadyLogged((int) $item['id'], $today->format('Y-m-d'), $window, $recipient)) {
$skipped++;
continue;
}
try {
$subject = sprintf('%s due in %d day%s', $item['name'], $window, $window === 1 ? '' : 's');
$html = sprintf(
'<h1>%s</h1><p>Your %s budget item is due on <strong>%s</strong>.</p><p>Planned amount: <strong>%s%.2f</strong></p>',
htmlspecialchars($subject, ENT_QUOTES),
htmlspecialchars((string) ($item['category_name'] ?: 'uncategorized'), ENT_QUOTES),
htmlspecialchars((string) $item['due_date'], ENT_QUOTES),
htmlspecialchars((string) $this->settings->get('app.currency_symbol', '$'), ENT_QUOTES),
(float) $item['planned_amount']
);
$response = $this->mailgun->send($recipient, $subject, $html);
$this->log((int) $item['id'], $today->format('Y-m-d'), $window, $recipient, 'sent', (string) $response['body']);
$sent++;
} catch (\Throwable $exception) {
$this->log((int) $item['id'], $today->format('Y-m-d'), $window, $recipient, 'failed', $exception->getMessage());
$errors[] = $item['name'] . ': ' . $exception->getMessage();
}
}
}
return ['sent' => $sent, 'skipped' => $skipped, 'errors' => $errors];
}
private function alreadyLogged(int $itemId, string $date, int $window, string $recipient): bool
{
$statement = $this->pdo->prepare(
'SELECT COUNT(*) FROM alert_logs
WHERE budget_item_id = :item_id AND alert_date = :alert_date
AND window_days = :window_days AND recipient = :recipient'
);
$statement->execute([
'item_id' => $itemId,
'alert_date' => $date,
'window_days' => $window,
'recipient' => $recipient,
]);
return (int) $statement->fetchColumn() > 0;
}
private function log(int $itemId, string $date, int $window, string $recipient, string $status, string $response): void
{
$statement = $this->pdo->prepare(
'INSERT INTO alert_logs (budget_item_id, alert_date, window_days, recipient, status, response)
VALUES (:item_id, :alert_date, :window_days, :recipient, :status, :response)'
);
$statement->execute([
'item_id' => $itemId,
'alert_date' => $date,
'window_days' => $window,
'recipient' => $recipient,
'status' => $status,
'response' => $response,
]);
}
}