diff --git a/app/Api/V1/Controllers/System/CronController.php b/app/Api/V1/Controllers/System/CronController.php index 359237b16c..422b5c5ccc 100644 --- a/app/Api/V1/Controllers/System/CronController.php +++ b/app/Api/V1/Controllers/System/CronController.php @@ -54,6 +54,7 @@ class CronController extends Controller $return['exchange_rates'] = $this->exchangeRatesCronJob($config['force'], $config['date']); } $return['bill_notifications'] = $this->billWarningCronJob($config['force'], $config['date']); + $return['webhooks'] = $this->webhookCronJob($config['force'], $config['date']); return response()->api($return); } diff --git a/app/Console/Commands/Tools/Cron.php b/app/Console/Commands/Tools/Cron.php index 25b073801a..7a5e729016 100644 --- a/app/Console/Commands/Tools/Cron.php +++ b/app/Console/Commands/Tools/Cron.php @@ -32,6 +32,7 @@ use FireflyIII\Support\Cronjobs\BillWarningCronjob; use FireflyIII\Support\Cronjobs\ExchangeRatesCronjob; use FireflyIII\Support\Cronjobs\RecurringCronjob; use FireflyIII\Support\Cronjobs\UpdateCheckCronjob; +use FireflyIII\Support\Cronjobs\WebhookCronjob; use Illuminate\Console\Command; use Illuminate\Support\Facades\Log; use InvalidArgumentException; @@ -50,6 +51,7 @@ class Cron extends Command {--create-recurring : Create recurring transactions. Other tasks will be skipped unless also requested.} {--create-auto-budgets : Create auto budgets. Other tasks will be skipped unless also requested.} {--send-bill-warnings : Send bill warnings. Other tasks will be skipped unless also requested.} + {--send-webhook-messages : Sends any stray webhook messages (with a maximum of 5).} '; public function handle(): int @@ -58,7 +60,8 @@ class Cron extends Command && !$this->option('create-recurring') && !$this->option('create-auto-budgets') && !$this->option('send-bill-warnings') - && !$this->option('check-version'); + && !$this->option('check-version') + && !$this->option('send-webhook-messages'); $date = null; try { @@ -122,6 +125,16 @@ class Cron extends Command $this->friendlyError($e->getMessage()); } } + // Fire webhook messages cron job. + if ($doAll || $this->option('send-webhook-messages')) { + try { + $this->webhookCronJob($force, $date); + } catch (FireflyException $e) { + app('log')->error($e->getMessage()); + app('log')->error($e->getTraceAsString()); + $this->friendlyError($e->getMessage()); + } + } $this->friendlyInfo('More feedback on the cron jobs can be found in the log files.'); @@ -239,4 +252,25 @@ class Cron extends Command $this->friendlyPositive(sprintf('"Send bill warnings" cron ran with success: %s', $autoBudget->message)); } } + private function webhookCronJob(bool $force, ?Carbon $date): void + { + $webhook = new WebhookCronjob(); + $webhook->setForce($force); + // set date in cron job: + if ($date instanceof Carbon) { + $webhook->setDate($date); + } + + $webhook->fire(); + + if ($webhook->jobErrored) { + $this->friendlyError(sprintf('Error in "webhook" cron: %s', $webhook->message)); + } + if ($webhook->jobFired) { + $this->friendlyInfo(sprintf('"Webhook" cron fired: %s', $webhook->message)); + } + if ($webhook->jobSucceeded) { + $this->friendlyPositive(sprintf('"Webhook" cron ran with success: %s', $webhook->message)); + } + } } diff --git a/app/Events/Model/Bill/WarnUserAboutBill.php b/app/Events/Model/Bill/WarnUserAboutBill.php new file mode 100644 index 0000000000..ceb23cae49 --- /dev/null +++ b/app/Events/Model/Bill/WarnUserAboutBill.php @@ -0,0 +1,20 @@ +. - */ - -declare(strict_types=1); - -namespace FireflyIII\Events; - -use FireflyIII\Models\Bill; -use Illuminate\Queue\SerializesModels; - -/** - * Class WarnUserAboutBill. - */ -class WarnUserAboutBill extends Event -{ - use SerializesModels; - - public function __construct(public Bill $bill, public string $field, public int $diff) {} -} diff --git a/app/Handlers/Events/BillEventHandler.php b/app/Handlers/Events/BillEventHandler.php index 8ecb2fab82..4f2f2e5b85 100644 --- a/app/Handlers/Events/BillEventHandler.php +++ b/app/Handlers/Events/BillEventHandler.php @@ -24,48 +24,91 @@ declare(strict_types=1); namespace FireflyIII\Handlers\Events; -use FireflyIII\Events\WarnUserAboutBill; -use FireflyIII\Notifications\User\BillReminder; -use Illuminate\Support\Facades\Notification; use Exception; +use FireflyIII\Events\Model\Bill\WarnUserAboutBill; +use FireflyIII\Events\Model\Bill\WarnUserAboutOverdueSubscription; +use FireflyIII\Notifications\User\BillReminder; +use FireflyIII\Notifications\User\SubscriptionOverdueReminder; +use FireflyIII\Support\Facades\Preferences; +use Illuminate\Support\Facades\Log; +use Illuminate\Support\Facades\Notification; /** * Class BillEventHandler */ class BillEventHandler { + public function warnAboutOverdueSubscription(WarnUserAboutOverdueSubscription $event): void + { + $bill = $event->bill; + $dates = $event->dates; + + $key = sprintf('bill_overdue_%s_%s', $bill->id, substr(hash('sha256', json_encode($dates['pay_dates'], JSON_THROW_ON_ERROR)), 0, 10)); + $pref = Preferences::getForUser($bill->user, $key, false); + if (true === $pref->data) { + Log::debug(sprintf('User %s has already been warned about overdue subscription %s.', $bill->user->id, $bill->id)); + return; + } + /** @var bool $sendNotification */ + $sendNotification = Preferences::getForUser($bill->user, 'notification_bill_reminder', true)->data; + + if (true === $sendNotification) { + Log::debug('Will warning about overdue subscription.'); + Preferences::setForUser($bill->user, $key, true); + + try { + Notification::send($bill->user, new SubscriptionOverdueReminder($bill, $dates)); + } catch (Exception $e) { + $message = $e->getMessage(); + if (str_contains($message, 'Bcc')) { + Log::warning('[Bcc] Could not send notification. Please validate your email settings, use the .env.example file as a guide.'); + + return; + } + if (str_contains($message, 'RFC 2822')) { + Log::warning('[RFC] Could not send notification. Please validate your email settings, use the .env.example file as a guide.'); + + return; + } + Log::error($e->getMessage()); + Log::error($e->getTraceAsString()); + } + return; + } + Log::debug('User has disabled bill reminders.'); + } + public function warnAboutBill(WarnUserAboutBill $event): void { - app('log')->debug(sprintf('Now in %s', __METHOD__)); + Log::debug(sprintf('Now in %s', __METHOD__)); - $bill = $event->bill; + $bill = $event->bill; /** @var bool $preference */ - $preference = app('preferences')->getForUser($bill->user, 'notification_bill_reminder', true)->data; + Preferences::getForUser($bill->user, 'notification_bill_reminder', true)->data; if (true === $preference) { - app('log')->debug('Bill reminder is true!'); + Log::debug('Bill reminder is true!'); try { Notification::send($bill->user, new BillReminder($bill, $event->field, $event->diff)); } catch (Exception $e) { $message = $e->getMessage(); if (str_contains($message, 'Bcc')) { - app('log')->warning('[Bcc] Could not send notification. Please validate your email settings, use the .env.example file as a guide.'); + Log::warning('[Bcc] Could not send notification. Please validate your email settings, use the .env.example file as a guide.'); return; } if (str_contains($message, 'RFC 2822')) { - app('log')->warning('[RFC] Could not send notification. Please validate your email settings, use the .env.example file as a guide.'); + Log::warning('[RFC] Could not send notification. Please validate your email settings, use the .env.example file as a guide.'); return; } - app('log')->error($e->getMessage()); - app('log')->error($e->getTraceAsString()); + Log::error($e->getMessage()); + Log::error($e->getTraceAsString()); } + return; } - if (false === $preference) { - app('log')->debug('User has disabled bill reminders.'); - } + Log::debug('User has disabled bill reminders.'); } } diff --git a/app/Handlers/Events/WebhookEventHandler.php b/app/Handlers/Events/WebhookEventHandler.php index f48adef9df..cd561302e7 100644 --- a/app/Handlers/Events/WebhookEventHandler.php +++ b/app/Handlers/Events/WebhookEventHandler.php @@ -50,8 +50,7 @@ class WebhookEventHandler ->get(['webhook_messages.*']) ->filter( static fn (WebhookMessage $message) => $message->webhookAttempts()->count() <= 2 - )->splice(0, 5) - ; + )->splice(0, 5); Log::debug(sprintf('Found %d webhook message(s) ready to be send.', $messages->count())); foreach ($messages as $message) { if (false === $message->sent) { diff --git a/app/Http/Controllers/Controller.php b/app/Http/Controllers/Controller.php index 01b05dacb2..57da16d540 100644 --- a/app/Http/Controllers/Controller.php +++ b/app/Http/Controllers/Controller.php @@ -23,6 +23,7 @@ declare(strict_types=1); namespace FireflyIII\Http\Controllers; +use FireflyIII\Events\RequestedSendWebhookMessages; use FireflyIII\Models\TransactionCurrency; use FireflyIII\Support\Facades\Amount; use FireflyIII\Support\Facades\Steam; @@ -33,6 +34,7 @@ use Illuminate\Foundation\Bus\DispatchesJobs; use Illuminate\Foundation\Validation\ValidatesRequests; use Illuminate\Routing\Controller as BaseController; use Illuminate\Support\Facades\Config; +use Illuminate\Support\Facades\Log; use Illuminate\Support\Facades\View; use Illuminate\Support\Facades\Route; @@ -141,6 +143,14 @@ abstract class Controller extends BaseController View::share('shownDemo', $shownDemo); View::share('current_route_name', $page); View::share('original_route_name', Route::currentRouteName()); + + // lottery to send any remaining webhooks: + if(7 === random_int(1, 10)) { + // trigger event to send them: + Log::debug('send event RequestedSendWebhookMessages through lottery'); + event(new RequestedSendWebhookMessages()); + } + } View::share('darkMode', $darkMode); diff --git a/app/Jobs/WarnAboutBills.php b/app/Jobs/WarnAboutBills.php index bf8d8ae6d2..4bb57098dc 100644 --- a/app/Jobs/WarnAboutBills.php +++ b/app/Jobs/WarnAboutBills.php @@ -25,13 +25,17 @@ declare(strict_types=1); namespace FireflyIII\Jobs; use Carbon\Carbon; -use FireflyIII\Events\WarnUserAboutBill; +use FireflyIII\Events\Model\Bill\WarnUserAboutBill; +use FireflyIII\Events\Model\Bill\WarnUserAboutOverdueSubscription; use FireflyIII\Models\Bill; +use FireflyIII\Support\Facades\Navigation; +use FireflyIII\Support\JsonApi\Enrichments\SubscriptionEnrichment; use Illuminate\Bus\Queueable; use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Foundation\Bus\Dispatchable; use Illuminate\Queue\InteractsWithQueue; use Illuminate\Queue\SerializesModels; +use Illuminate\Support\Facades\Log; /** * Class WarnAboutBills @@ -51,19 +55,19 @@ class WarnAboutBills implements ShouldQueue */ public function __construct(?Carbon $date) { - $newDate = new Carbon(); + $newDate = new Carbon(); $newDate->startOfDay(); - $this->date = $newDate; + $this->date = $newDate; if ($date instanceof Carbon) { - $newDate = clone $date; + $newDate = clone $date; $newDate->startOfDay(); $this->date = $newDate; } $this->force = false; - app('log')->debug(sprintf('Created new WarnAboutBills("%s")', $this->date->format('Y-m-d'))); + Log::debug(sprintf('Created new WarnAboutBills("%s")', $this->date->format('Y-m-d'))); } /** @@ -71,12 +75,16 @@ class WarnAboutBills implements ShouldQueue */ public function handle(): void { - app('log')->debug(sprintf('Now at start of WarnAboutBills() job for %s.', $this->date->format('D d M Y'))); + Log::debug(sprintf('Now at start of WarnAboutBills() job for %s.', $this->date->format('D d M Y'))); $bills = Bill::all(); /** @var Bill $bill */ foreach ($bills as $bill) { - app('log')->debug(sprintf('Now checking bill #%d ("%s")', $bill->id, $bill->name)); + Log::debug(sprintf('Now checking bill #%d ("%s")', $bill->id, $bill->name)); + $dates = $this->getDates($bill); + if ($this->needsOverdueAlert($dates)) { + $this->sendOverdueAlert($bill, $dates); + } if ($this->hasDateFields($bill)) { if ($this->needsWarning($bill, 'end_date')) { $this->sendWarning($bill, 'end_date'); @@ -86,7 +94,7 @@ class WarnAboutBills implements ShouldQueue } } } - app('log')->debug('Done with handle()'); + Log::debug('Done with handle()'); // clear cache: app('preferences')->mark(); @@ -95,12 +103,12 @@ class WarnAboutBills implements ShouldQueue private function hasDateFields(Bill $bill): bool { if (false === $bill->active) { - app('log')->debug('Bill is not active.'); + Log::debug('Bill is not active.'); return false; } if (null === $bill->end_date && null === $bill->extension_date) { - app('log')->debug('Bill has no date fields.'); + Log::debug('Bill has no date fields.'); return false; } @@ -115,7 +123,7 @@ class WarnAboutBills implements ShouldQueue } $diff = $this->getDiff($bill, $field); $list = config('firefly.bill_reminder_periods'); - app('log')->debug(sprintf('Difference in days for field "%s" ("%s") is %d day(s)', $field, $bill->{$field}->format('Y-m-d'), $diff)); + Log::debug(sprintf('Difference in days for field "%s" ("%s") is %d day(s)', $field, $bill->{$field}->format('Y-m-d'), $diff)); if (in_array($diff, $list, true)) { return true; } @@ -128,19 +136,19 @@ class WarnAboutBills implements ShouldQueue $today = clone $this->date; $carbon = clone $bill->{$field}; - return (int) $today->diffInDays($carbon); + return (int)$today->diffInDays($carbon); } private function sendWarning(Bill $bill, string $field): void { $diff = $this->getDiff($bill, $field); - app('log')->debug('Will now send warning!'); + Log::debug('Will now send warning!'); event(new WarnUserAboutBill($bill, $field, $diff)); } public function setDate(Carbon $date): void { - $newDate = clone $date; + $newDate = clone $date; $newDate->startOfDay(); $this->date = $newDate; } @@ -149,4 +157,45 @@ class WarnAboutBills implements ShouldQueue { $this->force = $force; } + + private function getDates(Bill $bill): array + { + $start = clone $this->date; + $start = Navigation::startOfPeriod($start, $bill->repeat_freq); + $end = clone $start; + $end = Navigation::endOfPeriod($end, $bill->repeat_freq); + $enrichment = new SubscriptionEnrichment(); + $enrichment->setUser($bill->user); + $enrichment->setStart($start); + $enrichment->setEnd($end); + $single = $enrichment->enrichSingle($bill); + return [ + 'pay_dates' => $single->meta['pay_dates'] ?? [], + 'paid_dates' => $single->meta['paid_dates'] ?? [], + ]; + } + + private function needsOverdueAlert(array $dates): bool + { + $count = count($dates['pay_dates']) - count($dates['paid_dates']); + if (0 === $count || 0 === count($dates['pay_dates'])) { + return false; + } + // the earliest date in the list of pay dates must be 48hrs or more ago. + $earliest = new Carbon($dates['pay_dates'][0]); + $earliest->startOfDay(); + Log::debug(sprintf('Earliest expected pay date is %s' , $earliest->toAtomString())); + $diff = $earliest->diffInDays($this->date); + Log::debug(sprintf('Difference in days is %s', $diff)); + if ($diff < 2) { + return false; + } + return true; + } + + private function sendOverdueAlert(Bill $bill, array $dates): void + { + Log::debug('Will now send warning about overdue bill.'); + event(new WarnUserAboutOverdueSubscription($bill, $dates)); + } } diff --git a/app/Notifications/User/SubscriptionOverdueReminder.php b/app/Notifications/User/SubscriptionOverdueReminder.php new file mode 100644 index 0000000000..cdec41e3a3 --- /dev/null +++ b/app/Notifications/User/SubscriptionOverdueReminder.php @@ -0,0 +1,101 @@ +dates['pay_dates'] = array_map( + static function (string $date): string { + return new Carbon($date)->isoFormat((string) trans('config.month_and_day_moment_js')); + }, + $this->dates['pay_dates'] + ); + + return new MailMessage() + ->markdown('emails.subscription-overdue-warning', ['bill' => $this->bill,'dates' => $this->dates]) + ->subject($this->getSubject()) + ; + } + + private function getSubject(): string + { + return (string) trans('email.subscription_overdue_subject', ['name' => $this->bill->name]); + } + + public function toNtfy(User $notifiable): Message + { + $settings = ReturnsSettings::getSettings('ntfy', 'user', $notifiable); + $message = new Message(); + $message->topic($settings['ntfy_topic']); + $message->title($this->getSubject()); + $message->body((string) trans('email.bill_warning_please_action')); + + return $message; + } + + /** + * @SuppressWarnings("PHPMD.UnusedFormalParameter") + */ + public function toPushover(User $notifiable): PushoverMessage + { + return PushoverMessage::create((string) trans('email.bill_warning_please_action')) + ->title($this->getSubject()) + ; + } + + /** + * @SuppressWarnings("PHPMD.UnusedFormalParameter") + */ + public function toSlack(User $notifiable): SlackMessage + { + $bill = $this->bill; + $url = route('bills.show', [$bill->id]); + + return new SlackMessage() + ->warning() + ->attachment(static function ($attachment) use ($bill, $url): void { + $attachment->title((string) trans('firefly.visit_bill', ['name' => $bill->name]), $url); + }) + ->content($this->getSubject()) + ; + } + + /** + * @SuppressWarnings("PHPMD.UnusedFormalParameter") + */ + public function via(User $notifiable): array + { + return ReturnsAvailableChannels::returnChannels('user', $notifiable); + } + +} diff --git a/app/Providers/EventServiceProvider.php b/app/Providers/EventServiceProvider.php index 09ec135daa..b4ca4cb0f7 100644 --- a/app/Providers/EventServiceProvider.php +++ b/app/Providers/EventServiceProvider.php @@ -27,6 +27,8 @@ use FireflyIII\Events\ActuallyLoggedIn; use FireflyIII\Events\Admin\InvitationCreated; use FireflyIII\Events\DestroyedTransactionGroup; use FireflyIII\Events\DetectedNewIPAddress; +use FireflyIII\Events\Model\Bill\WarnUserAboutBill; +use FireflyIII\Events\Model\Bill\WarnUserAboutOverdueSubscription; use FireflyIII\Events\Model\BudgetLimit\Created; use FireflyIII\Events\Model\BudgetLimit\Deleted; use FireflyIII\Events\Model\BudgetLimit\Updated; @@ -58,7 +60,6 @@ use FireflyIII\Events\TriggeredAuditLog; use FireflyIII\Events\UpdatedAccount; use FireflyIII\Events\UpdatedTransactionGroup; use FireflyIII\Events\UserChangedEmail; -use FireflyIII\Events\WarnUserAboutBill; use FireflyIII\Handlers\Observer\AccountObserver; use FireflyIII\Handlers\Observer\AttachmentObserver; use FireflyIII\Handlers\Observer\AutoBudgetObserver; @@ -202,6 +203,9 @@ class EventServiceProvider extends ServiceProvider WarnUserAboutBill::class => [ 'FireflyIII\Handlers\Events\BillEventHandler@warnAboutBill', ], + WarnUserAboutOverdueSubscription::class => [ + 'FireflyIII\Handlers\Events\BillEventHandler@warnAboutOverdueSubscription', + ], // audit log events: TriggeredAuditLog::class => [ diff --git a/app/Services/Webhook/StandardWebhookSender.php b/app/Services/Webhook/StandardWebhookSender.php index 0962d538aa..ab9a82108a 100644 --- a/app/Services/Webhook/StandardWebhookSender.php +++ b/app/Services/Webhook/StandardWebhookSender.php @@ -32,6 +32,7 @@ use GuzzleHttp\Client; use GuzzleHttp\Exception\ConnectException; use GuzzleHttp\Exception\GuzzleException; use GuzzleHttp\Exception\RequestException; +use Illuminate\Support\Facades\Log; use JsonException; use function Safe\json_encode; @@ -65,9 +66,9 @@ class StandardWebhookSender implements WebhookSenderInterface try { $signature = $signatureGenerator->generate($this->message); } catch (FireflyException $e) { - app('log')->error('Did not send message because of a Firefly III Exception.'); - app('log')->error($e->getMessage()); - app('log')->error($e->getTraceAsString()); + Log::error('Did not send message because of a Firefly III Exception.'); + Log::error($e->getMessage()); + Log::error($e->getTraceAsString()); $attempt = new WebhookAttempt(); $attempt->webhookMessage()->associate($this->message); $attempt->status_code = 0; @@ -80,14 +81,14 @@ class StandardWebhookSender implements WebhookSenderInterface return; } - app('log')->debug(sprintf('Trying to send webhook message #%d', $this->message->id)); + Log::debug(sprintf('Trying to send webhook message #%d', $this->message->id)); try { $json = json_encode($this->message->message, JSON_THROW_ON_ERROR); } catch (JsonException $e) { - app('log')->error('Did not send message because of a JSON error.'); - app('log')->error($e->getMessage()); - app('log')->error($e->getTraceAsString()); + Log::error('Did not send message because of a JSON error.'); + Log::error($e->getMessage()); + Log::error($e->getTraceAsString()); $attempt = new WebhookAttempt(); $attempt->webhookMessage()->associate($this->message); $attempt->status_code = 0; @@ -115,9 +116,9 @@ class StandardWebhookSender implements WebhookSenderInterface try { $res = $client->request('POST', $this->message->webhook->url, $options); } catch (ConnectException|RequestException $e) { - app('log')->error('The webhook could NOT be submitted but Firefly III caught the error below.'); - app('log')->error($e->getMessage()); - app('log')->error($e->getTraceAsString()); + Log::error('The webhook could NOT be submitted but Firefly III caught the error below.'); + Log::error($e->getMessage()); + Log::error($e->getTraceAsString()); $logs = sprintf("%s\n%s", $e->getMessage(), $e->getTraceAsString()); @@ -130,9 +131,9 @@ class StandardWebhookSender implements WebhookSenderInterface $attempt->status_code = 0; if (method_exists($e, 'hasResponse') && method_exists($e, 'getResponse')) { $attempt->status_code = $e->hasResponse() ? $e->getResponse()->getStatusCode() : 0; - app('log')->error(sprintf('The status code of the error response is: %d', $attempt->status_code)); + Log::error(sprintf('The status code of the error response is: %d', $attempt->status_code)); $body = (string) ($e->hasResponse() ? $e->getResponse()->getBody() : ''); - app('log')->error(sprintf('The body of the error response is: %s', $body)); + Log::error(sprintf('The body of the error response is: %s', $body)); } $attempt->logs = $logs; $attempt->save(); @@ -142,9 +143,9 @@ class StandardWebhookSender implements WebhookSenderInterface $this->message->sent = true; $this->message->save(); - app('log')->debug(sprintf('Webhook message #%d was sent. Status code %d', $this->message->id, $res->getStatusCode())); - app('log')->debug(sprintf('Webhook request body size: %d bytes', strlen($json))); - app('log')->debug(sprintf('Response body: %s', $res->getBody())); + Log::debug(sprintf('Webhook message #%d was sent. Status code %d', $this->message->id, $res->getStatusCode())); + Log::debug(sprintf('Webhook request body size: %d bytes', strlen($json))); + Log::debug(sprintf('Response body: %s', $res->getBody())); } public function setMessage(WebhookMessage $message): void diff --git a/app/Support/Cronjobs/AutoBudgetCronjob.php b/app/Support/Cronjobs/AutoBudgetCronjob.php index 148f8a3e27..f71a4a4da1 100644 --- a/app/Support/Cronjobs/AutoBudgetCronjob.php +++ b/app/Support/Cronjobs/AutoBudgetCronjob.php @@ -28,6 +28,7 @@ use Carbon\Carbon; use FireflyIII\Jobs\CreateAutoBudgetLimits; use FireflyIII\Models\Configuration; use FireflyIII\Support\Facades\FireflyConfig; +use Illuminate\Support\Facades\Log; /** * Class AutoBudgetCronjob @@ -42,22 +43,22 @@ class AutoBudgetCronjob extends AbstractCronjob $diff = Carbon::now()->getTimestamp() - $lastTime; $diffForHumans = today(config('app.timezone'))->diffForHumans(Carbon::createFromTimestamp($lastTime), null, true); if (0 === $lastTime) { - app('log')->info('Auto budget cron-job has never fired before.'); + Log::info('Auto budget cron-job has never fired before.'); } // less than half a day ago: if ($lastTime > 0 && $diff <= 43200) { - app('log')->info(sprintf('It has been %s since the auto budget cron-job has fired.', $diffForHumans)); + Log::info(sprintf('It has been %s since the auto budget cron-job has fired.', $diffForHumans)); if (false === $this->force) { - app('log')->info('The auto budget cron-job will not fire now.'); + Log::info('The auto budget cron-job will not fire now.'); $this->message = sprintf('It has been %s since the auto budget cron-job has fired. It will not fire now.', $diffForHumans); return; } - app('log')->info('Execution of the auto budget cron-job has been FORCED.'); + Log::info('Execution of the auto budget cron-job has been FORCED.'); } if ($lastTime > 0 && $diff > 43200) { - app('log')->info(sprintf('It has been %s since the auto budget cron-job has fired. It will fire now!', $diffForHumans)); + Log::info(sprintf('It has been %s since the auto budget cron-job has fired. It will fire now!', $diffForHumans)); } $this->fireAutoBudget(); @@ -66,7 +67,7 @@ class AutoBudgetCronjob extends AbstractCronjob private function fireAutoBudget(): void { - app('log')->info(sprintf('Will now fire auto budget cron job task for date "%s".', $this->date->format('Y-m-d'))); + Log::info(sprintf('Will now fire auto budget cron job task for date "%s".', $this->date->format('Y-m-d'))); /** @var CreateAutoBudgetLimits $job */ $job = app(CreateAutoBudgetLimits::class, [$this->date]); @@ -80,6 +81,6 @@ class AutoBudgetCronjob extends AbstractCronjob $this->message = 'Auto-budget cron job fired successfully.'; FireflyConfig::set('last_ab_job', (int) $this->date->format('U')); - app('log')->info('Done with auto budget cron job task.'); + Log::info('Done with auto budget cron job task.'); } } diff --git a/app/Support/Cronjobs/BillWarningCronjob.php b/app/Support/Cronjobs/BillWarningCronjob.php index f3861fb787..aece4d403f 100644 --- a/app/Support/Cronjobs/BillWarningCronjob.php +++ b/app/Support/Cronjobs/BillWarningCronjob.php @@ -28,6 +28,8 @@ use Carbon\Carbon; use FireflyIII\Exceptions\FireflyException; use FireflyIII\Jobs\WarnAboutBills; use FireflyIII\Models\Configuration; +use FireflyIII\Support\Facades\FireflyConfig; +use Illuminate\Support\Facades\Log; /** * Class BillWarningCronjob @@ -39,22 +41,22 @@ class BillWarningCronjob extends AbstractCronjob */ public function fire(): void { - app('log')->debug(sprintf('Now in %s', __METHOD__)); + Log::debug(sprintf('Now in %s', __METHOD__)); /** @var Configuration $config */ - $config = app('fireflyconfig')->get('last_bw_job', 0); + $config = FireflyConfig::get('last_bw_job', 0); $lastTime = (int) $config->data; $diff = Carbon::now()->getTimestamp() - $lastTime; $diffForHumans = today(config('app.timezone'))->diffForHumans(Carbon::createFromTimestamp($lastTime), null, true); if (0 === $lastTime) { - app('log')->info('The bill notification cron-job has never fired before.'); + Log::info('The bill notification cron-job has never fired before.'); } // less than half a day ago: if ($lastTime > 0 && $diff <= 43200) { - app('log')->info(sprintf('It has been %s since the bill notification cron-job has fired.', $diffForHumans)); + Log::info(sprintf('It has been %s since the bill notification cron-job has fired.', $diffForHumans)); if (false === $this->force) { - app('log')->info('The cron-job will not fire now.'); + Log::info('The cron-job will not fire now.'); $this->message = sprintf('It has been %s since the bill notification cron-job has fired. It will not fire now.', $diffForHumans); $this->jobFired = false; $this->jobErrored = false; @@ -63,11 +65,11 @@ class BillWarningCronjob extends AbstractCronjob return; } - app('log')->info('Execution of the bill notification cron-job has been FORCED.'); + Log::info('Execution of the bill notification cron-job has been FORCED.'); } if ($lastTime > 0 && $diff > 43200) { - app('log')->info(sprintf('It has been %s since the bill notification cron-job has fired. It will fire now!', $diffForHumans)); + Log::info(sprintf('It has been %s since the bill notification cron-job has fired. It will fire now!', $diffForHumans)); } $this->fireWarnings(); @@ -77,7 +79,7 @@ class BillWarningCronjob extends AbstractCronjob private function fireWarnings(): void { - app('log')->info(sprintf('Will now fire bill notification job task for date "%s".', $this->date->format('Y-m-d H:i:s'))); + Log::info(sprintf('Will now fire bill notification job task for date "%s".', $this->date->format('Y-m-d H:i:s'))); /** @var WarnAboutBills $job */ $job = app(WarnAboutBills::class); @@ -91,8 +93,8 @@ class BillWarningCronjob extends AbstractCronjob $this->jobSucceeded = true; $this->message = 'Bill notification cron job fired successfully.'; - app('fireflyconfig')->set('last_bw_job', (int) $this->date->format('U')); - app('log')->info(sprintf('Marked the last time this job has run as "%s" (%d)', $this->date->format('Y-m-d H:i:s'), (int) $this->date->format('U'))); - app('log')->info('Done with bill notification cron job task.'); + FireflyConfig::set('last_bw_job', (int) $this->date->format('U')); + Log::info(sprintf('Marked the last time this job has run as "%s" (%d)', $this->date->format('Y-m-d H:i:s'), (int) $this->date->format('U'))); + Log::info('Done with bill notification cron job task.'); } } diff --git a/app/Support/Cronjobs/ExchangeRatesCronjob.php b/app/Support/Cronjobs/ExchangeRatesCronjob.php index b54c76a242..f7d80b8033 100644 --- a/app/Support/Cronjobs/ExchangeRatesCronjob.php +++ b/app/Support/Cronjobs/ExchangeRatesCronjob.php @@ -27,6 +27,7 @@ namespace FireflyIII\Support\Cronjobs; use Carbon\Carbon; use FireflyIII\Jobs\DownloadExchangeRates; use FireflyIII\Models\Configuration; +use FireflyIII\Support\Facades\FireflyConfig; use Illuminate\Support\Facades\Log; /** @@ -37,7 +38,7 @@ class ExchangeRatesCronjob extends AbstractCronjob public function fire(): void { /** @var Configuration $config */ - $config = app('fireflyconfig')->get('last_cer_job', 0); + $config = FireflyConfig::get('last_cer_job', 0); $lastTime = (int) $config->data; $diff = Carbon::now()->getTimestamp() - $lastTime; $diffForHumans = today(config('app.timezone'))->diffForHumans(Carbon::createFromTimestamp($lastTime), null, true); @@ -80,7 +81,7 @@ class ExchangeRatesCronjob extends AbstractCronjob $this->jobSucceeded = true; $this->message = 'Exchange rates cron job fired successfully.'; - app('fireflyconfig')->set('last_cer_job', (int) $this->date->format('U')); + FireflyConfig::set('last_cer_job', (int) $this->date->format('U')); Log::info('Done with exchange rates job task.'); } } diff --git a/app/Support/Cronjobs/UpdateCheckCronjob.php b/app/Support/Cronjobs/UpdateCheckCronjob.php index 15e3eb5221..6d3cea13ab 100644 --- a/app/Support/Cronjobs/UpdateCheckCronjob.php +++ b/app/Support/Cronjobs/UpdateCheckCronjob.php @@ -41,7 +41,7 @@ class UpdateCheckCronjob extends AbstractCronjob Log::debug('Now in checkForUpdates()'); // should not check for updates: - $permission = app('fireflyconfig')->get('permission_update_check', -1); + $permission = FireflyConfig::get('permission_update_check', -1); $value = (int) $permission->data; if (1 !== $value) { Log::debug('Update check is not enabled.'); diff --git a/app/Support/Cronjobs/WebhookCronjob.php b/app/Support/Cronjobs/WebhookCronjob.php new file mode 100644 index 0000000000..174acd62f4 --- /dev/null +++ b/app/Support/Cronjobs/WebhookCronjob.php @@ -0,0 +1,98 @@ +. + */ + +declare(strict_types=1); + +namespace FireflyIII\Support\Cronjobs; + +use Carbon\Carbon; +use FireflyIII\Events\RequestedSendWebhookMessages; +use FireflyIII\Exceptions\FireflyException; +use FireflyIII\Jobs\WarnAboutBills; +use FireflyIII\Models\Configuration; +use FireflyIII\Support\Facades\FireflyConfig; +use Illuminate\Support\Facades\Log; + +/** + * Class WebhookCronjob + */ +class WebhookCronjob extends AbstractCronjob +{ + /** + * @throws FireflyException + */ + public function fire(): void + { + Log::debug(sprintf('Now in %s', __METHOD__)); + + /** @var Configuration $config */ + $config = FireflyConfig::get('last_webhook_job', 0); + $lastTime = (int) $config->data; + $diff = Carbon::now()->getTimestamp() - $lastTime; + $diffForHumans = today(config('app.timezone'))->diffForHumans(Carbon::createFromTimestamp($lastTime), null, true); + + if (0 === $lastTime) { + Log::info('The webhook cron-job has never fired before.'); + } + // less than ten minutes ago. + if ($lastTime > 0 && $diff <= 600) { + Log::info(sprintf('It has been %s since the webhook cron-job has fired.', $diffForHumans)); + if (false === $this->force) { + Log::info('The cron-job will not fire now.'); + $this->message = sprintf('It has been %s since the webhook cron-job has fired. It will not fire now.', $diffForHumans); + $this->jobFired = false; + $this->jobErrored = false; + $this->jobSucceeded = false; + + return; + } + + Log::info('Execution of the webhook cron-job has been FORCED.'); + } + + if ($lastTime > 0 && $diff > 600) { + Log::info(sprintf('It has been %s since the webhook cron-job has fired. It will fire now!', $diffForHumans)); + } + + $this->fireWebhookmessages(); + + app('preferences')->mark(); + } + + private function fireWebhookmessages(): void + { + Log::info(sprintf('Will now send webhook messages for date "%s".', $this->date->format('Y-m-d H:i:s'))); + + Log::debug('send event RequestedSendWebhookMessages through cron job.'); + event(new RequestedSendWebhookMessages()); + + // get stuff from job: + $this->jobFired = true; + $this->jobErrored = false; + $this->jobSucceeded = true; + $this->message = 'Send webhook messages cron job fired successfully.'; + + FireflyConfig::set('last_webhook_job', (int) $this->date->format('U')); + Log::info(sprintf('Marked the last time this job has run as "%s" (%d)', $this->date->format('Y-m-d H:i:s'), (int) $this->date->format('U'))); + Log::info('Done with webhook cron job task.'); + } +} diff --git a/app/Support/Http/Controllers/CronRunner.php b/app/Support/Http/Controllers/CronRunner.php index c3b99bad25..c8cf03cfd6 100644 --- a/app/Support/Http/Controllers/CronRunner.php +++ b/app/Support/Http/Controllers/CronRunner.php @@ -30,6 +30,7 @@ use FireflyIII\Support\Cronjobs\AutoBudgetCronjob; use FireflyIII\Support\Cronjobs\BillWarningCronjob; use FireflyIII\Support\Cronjobs\ExchangeRatesCronjob; use FireflyIII\Support\Cronjobs\RecurringCronjob; +use FireflyIII\Support\Cronjobs\WebhookCronjob; /** * Trait CronRunner @@ -62,6 +63,32 @@ trait CronRunner ]; } + protected function webhookCronJob(bool $force, Carbon $date): array + { + /** @var WebhookCronjob $webhook */ + $webhook = app(WebhookCronjob::class); + $webhook->setForce($force); + $webhook->setDate($date); + + try { + $webhook->fire(); + } catch (FireflyException $e) { + return [ + 'job_fired' => false, + 'job_succeeded' => false, + 'job_errored' => true, + 'message' => $e->getMessage(), + ]; + } + + return [ + 'job_fired' => $webhook->jobFired, + 'job_succeeded' => $webhook->jobSucceeded, + 'job_errored' => $webhook->jobErrored, + 'message' => $webhook->message, + ]; + } + protected function exchangeRatesCronJob(bool $force, Carbon $date): array { /** @var ExchangeRatesCronjob $exchangeRates */ diff --git a/resources/lang/en_US/email.php b/resources/lang/en_US/email.php index ddc329f6fd..5531caac6d 100644 --- a/resources/lang/en_US/email.php +++ b/resources/lang/en_US/email.php @@ -138,6 +138,11 @@ return [ 'new_journals_subject' => 'Firefly III has created a new transaction|Firefly III has created :count new transactions', 'new_journals_header' => 'Firefly III has created a transaction for you. You can find it in your Firefly III installation:|Firefly III has created :count transactions for you. You can find them in your Firefly III installation:', + // subscription is overdue. + 'subscription_overdue_subject' => 'Your subscription ":name" is overdue to be paid', + 'subscription_overdue_warning_intro' => 'Your subscription ":name" is overdue to be paid. At the following date(s) a payment was expected, but it has not yet arrived.', + 'subscription_overdue_please_action' => 'Perhaps you have simply not linked the transaction to subscription ":name". In that case, please do so. You will NOT get another warning about this overdue bill.', + 'subscription_overdue_outro' => 'If you believe this message is wrong, please contact the Firefly III developer.', // bill warning 'bill_warning_subject_end_date' => 'Your subscription ":name" is due to end in :diff days', 'bill_warning_subject_now_end_date' => 'Your subscription ":name" is due to end TODAY', diff --git a/resources/views/emails/subscription-overdue-warning.blade.php b/resources/views/emails/subscription-overdue-warning.blade.php new file mode 100644 index 0000000000..228ccc843f --- /dev/null +++ b/resources/views/emails/subscription-overdue-warning.blade.php @@ -0,0 +1,10 @@ +@component('mail::message') +{{ trans('email.subscription_overdue_warning_intro', ['name' => $bill->name]) }} + +@foreach($dates['pay_dates'] as $date) + - {{ $date }} +@endforeach + +{{ trans('email.subscription_overdue_please_action', ['name' => $bill->name]) }} + +@endcomponent diff --git a/resources/views/vendor/mail/text/message.blade.php b/resources/views/vendor/mail/text/message.blade.php index 1ae9ed8f1b..a6ee0ae0ea 100644 --- a/resources/views/vendor/mail/text/message.blade.php +++ b/resources/views/vendor/mail/text/message.blade.php @@ -6,8 +6,14 @@ @endcomponent @endslot - {{-- Body --}} - {{ $slot }} +{{-- Body --}} +{{ trans('email.greeting') }} + +{{ $slot }} + +{{ trans('email.closing') }} + +{{ trans('email.signature')}} {{-- Subcopy --}} @isset($subcopy)