100 lines
4.2 KiB
PHP
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,
|
|
]);
|
|
}
|
|
}
|