diff --git a/app/Events/Model/Bill/WarnUserAboutOverdueSubscription.php b/app/Events/Model/Bill/WarnUserAboutOverdueSubscriptions.php similarity index 57% rename from app/Events/Model/Bill/WarnUserAboutOverdueSubscription.php rename to app/Events/Model/Bill/WarnUserAboutOverdueSubscriptions.php index 92d584750c..d7a4248778 100644 --- a/app/Events/Model/Bill/WarnUserAboutOverdueSubscription.php +++ b/app/Events/Model/Bill/WarnUserAboutOverdueSubscriptions.php @@ -6,12 +6,13 @@ namespace FireflyIII\Events\Model\Bill; use FireflyIII\Events\Event; use FireflyIII\Models\Bill; +use FireflyIII\User; use Illuminate\Queue\SerializesModels; -class WarnUserAboutOverdueSubscription extends Event +class WarnUserAboutOverdueSubscriptions extends Event { use SerializesModels; - public function __construct(public Bill $bill, public array $dates) {} + public function __construct(public User $user, public array $overdue) {} } diff --git a/app/Handlers/Events/BillEventHandler.php b/app/Handlers/Events/BillEventHandler.php index 4a5445af96..37210c1f9a 100644 --- a/app/Handlers/Events/BillEventHandler.php +++ b/app/Handlers/Events/BillEventHandler.php @@ -26,9 +26,10 @@ namespace FireflyIII\Handlers\Events; use Exception; use FireflyIII\Events\Model\Bill\WarnUserAboutBill; -use FireflyIII\Events\Model\Bill\WarnUserAboutOverdueSubscription; +use FireflyIII\Events\Model\Bill\WarnUserAboutOverdueSubscriptions; +use FireflyIII\Models\Bill; use FireflyIII\Notifications\User\BillReminder; -use FireflyIII\Notifications\User\SubscriptionOverdueReminder; +use FireflyIII\Notifications\User\SubscriptionsOverdueReminder; use FireflyIII\Support\Facades\Preferences; use Illuminate\Support\Facades\Log; use Illuminate\Support\Facades\Notification; @@ -38,47 +39,65 @@ use Illuminate\Support\Facades\Notification; */ class BillEventHandler { - public function warnAboutOverdueSubscription(WarnUserAboutOverdueSubscription $event): void + public function warnAboutOverdueSubscriptions(WarnUserAboutOverdueSubscriptions $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; + Log::debug(sprintf('Now in %s', __METHOD__)); + // make sure user does not get the warning twice. + $overdue = $event->overdue; + $user = $event->user; + $toBeWarned = []; + Log::debug(sprintf('%d bills to warn about.', count($overdue))); + foreach ($overdue as $item) { + /** @var Bill $bill */ + $bill = $item['bill']; + $key = sprintf('bill_overdue_%s_%s', $bill->id, substr(hash('sha256', json_encode($item['dates']['pay_dates'], JSON_THROW_ON_ERROR)), 0, 10)); + $pref = Preferences::getForUser($bill->user, $key, false); + if (true === $pref->data) { + Log::debug(sprintf('User #%d has already been warned about overdue subscription #%d.', $bill->user->id, $bill->id)); + continue; + } + $toBeWarned[] = $item; } + unset($bill); + Log::debug(sprintf('Now %d bills to warn about.', count($toBeWarned))); /** @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()); - } - + $sendNotification = Preferences::getForUser($user, 'notification_bill_reminder', true)->data; + if (false === $sendNotification) { + Log::debug('User has disabled bill reminders.'); return; } - Log::debug('User has disabled bill reminders.'); + Log::debug(sprintf('Will warning about %d overdue subscription(s).', count($toBeWarned))); + if (0 === count($toBeWarned)) { + Log::debug('No overdue subscriptions to warn about.'); + return; + } + foreach ($toBeWarned as $item) { + /** @var Bill $bill */ + $bill = $item['bill']; + $key = sprintf('bill_overdue_%s_%s', $bill->id, substr(hash('sha256', json_encode($item['dates']['pay_dates'], JSON_THROW_ON_ERROR)), 0, 10)); + Preferences::setForUser($bill->user, $key, true); + } + Log::warning('should hit this ONCE'); + try { + Notification::send($user, new SubscriptionsOverdueReminder($overdue)); + } 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()); + } + + } public function warnAboutBill(WarnUserAboutBill $event): void diff --git a/app/Jobs/WarnAboutBills.php b/app/Jobs/WarnAboutBills.php index ee5655b028..20c6fd090e 100644 --- a/app/Jobs/WarnAboutBills.php +++ b/app/Jobs/WarnAboutBills.php @@ -26,10 +26,11 @@ namespace FireflyIII\Jobs; use Carbon\Carbon; use FireflyIII\Events\Model\Bill\WarnUserAboutBill; -use FireflyIII\Events\Model\Bill\WarnUserAboutOverdueSubscription; +use FireflyIII\Events\Model\Bill\WarnUserAboutOverdueSubscriptions; use FireflyIII\Models\Bill; use FireflyIII\Support\Facades\Navigation; use FireflyIII\Support\JsonApi\Enrichments\SubscriptionEnrichment; +use FireflyIII\User; use Illuminate\Bus\Queueable; use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Foundation\Bus\Dispatchable; @@ -55,12 +56,12 @@ 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; } @@ -76,28 +77,29 @@ class WarnAboutBills implements ShouldQueue public function handle(): void { 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) { - 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'); + foreach (User::all() as $user) { + $bills = $user->bills()->where('active', true)->get(); + $overdue = []; + /** @var Bill $bill */ + foreach ($bills as $bill) { + Log::debug(sprintf('Now checking bill #%d ("%s")', $bill->id, $bill->name)); + $dates = $this->getDates($bill); + if ($this->needsOverdueAlert($dates)) { + $overdue[] = ['bill' => $bill, 'dates' => $dates]; } - if ($this->needsWarning($bill, 'extension_date')) { - $this->sendWarning($bill, 'extension_date'); + if ($this->hasDateFields($bill)) { + if ($this->needsWarning($bill, 'end_date')) { + $this->sendWarning($bill, 'end_date'); + } + if ($this->needsWarning($bill, 'extension_date')) { + $this->sendWarning($bill, 'extension_date'); + } } } + $this->sendOverdueAlerts($user, $overdue); } Log::debug('Done with handle()'); - // clear cache: - app('preferences')->mark(); } private function hasDateFields(Bill $bill): bool @@ -148,7 +150,7 @@ class WarnAboutBills implements ShouldQueue public function setDate(Carbon $date): void { - $newDate = clone $date; + $newDate = clone $date; $newDate->startOfDay(); $this->date = $newDate; } @@ -168,7 +170,7 @@ class WarnAboutBills implements ShouldQueue $enrichment->setUser($bill->user); $enrichment->setStart($start); $enrichment->setEnd($end); - $single = $enrichment->enrichSingle($bill); + $single = $enrichment->enrichSingle($bill); return [ 'pay_dates' => $single->meta['pay_dates'] ?? [], @@ -178,7 +180,7 @@ class WarnAboutBills implements ShouldQueue private function needsOverdueAlert(array $dates): bool { - $count = count($dates['pay_dates']) - count($dates['paid_dates']); + $count = count($dates['pay_dates']) - count($dates['paid_dates']); if (0 === $count || 0 === count($dates['pay_dates'])) { return false; } @@ -186,7 +188,7 @@ class WarnAboutBills implements ShouldQueue $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); + $diff = $earliest->diffInDays($this->date); Log::debug(sprintf('Difference in days is %s', $diff)); if ($diff < 2) { return false; @@ -195,9 +197,11 @@ class WarnAboutBills implements ShouldQueue return true; } - private function sendOverdueAlert(Bill $bill, array $dates): void + private function sendOverdueAlerts(User $user, array $overdue): void { - Log::debug('Will now send warning about overdue bill.'); - event(new WarnUserAboutOverdueSubscription($bill, $dates)); + if (count($overdue) > 0) { + Log::debug(sprintf('Will now send warning about overdue bill for user #%d.', $user->id)); + event(new WarnUserAboutOverdueSubscriptions($user, $overdue)); + } } } diff --git a/app/Notifications/User/SubscriptionOverdueReminder.php b/app/Notifications/User/SubscriptionsOverdueReminder.php similarity index 57% rename from app/Notifications/User/SubscriptionOverdueReminder.php rename to app/Notifications/User/SubscriptionsOverdueReminder.php index 80843ea8e8..5684e5ef28 100644 --- a/app/Notifications/User/SubscriptionOverdueReminder.php +++ b/app/Notifications/User/SubscriptionsOverdueReminder.php @@ -5,7 +5,6 @@ declare(strict_types=1); namespace FireflyIII\Notifications\User; use Carbon\Carbon; -use FireflyIII\Models\Bill; use FireflyIII\Notifications\ReturnsAvailableChannels; use FireflyIII\Notifications\ReturnsSettings; use FireflyIII\User; @@ -15,11 +14,13 @@ use Illuminate\Notifications\Messages\SlackMessage; use Illuminate\Notifications\Notification; use NotificationChannels\Pushover\PushoverMessage; -class SubscriptionOverdueReminder extends Notification +class SubscriptionsOverdueReminder extends Notification { use Queueable; - public function __construct(private Bill $bill, private array $dates) {} + public function __construct(private array $overdue) + { + } /** * @SuppressWarnings("PHPMD.UnusedFormalParameter") @@ -35,23 +36,31 @@ class SubscriptionOverdueReminder extends Notification */ public function toMail(User $notifiable): MailMessage { - // format the dates in a human-readable way - $this->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'] - ); - + // format the data + $info = []; + $count = 0; + foreach ($this->overdue as $item) { + $current = [ + 'bill' => $item['bill'], + ]; + $current['pay_dates'] = array_map( + static function (string $date): string { + return new Carbon($date)->isoFormat((string)trans('config.month_and_day_moment_js')); + }, $item['dates']['pay_dates']); + $info[] = $current; + $count++; + } return new MailMessage() - ->markdown('emails.subscription-overdue-warning', ['bill' => $this->bill, 'dates' => $this->dates]) - ->subject($this->getSubject()) - ; + ->markdown('emails.subscriptions-overdue-warning', ['info' => $info,'count' => $count]) + ->subject($this->getSubject()); } private function getSubject(): string { - return (string) trans('email.subscription_overdue_subject', ['name' => $this->bill->name]); + if (count($this->overdue) > 1) { + return (string)trans('email.subscriptions_overdue_subject_multi', ['count' => count($this->overdue)]); + } + return (string)trans('email.subscriptions_overdue_subject_single'); } public function toNtfy(User $notifiable): Message @@ -60,7 +69,7 @@ class SubscriptionOverdueReminder extends Notification $message = new Message(); $message->topic($settings['ntfy_topic']); $message->title($this->getSubject()); - $message->body((string) trans('email.bill_warning_please_action')); + $message->body((string)trans('email.bill_warning_please_action')); return $message; } @@ -70,9 +79,8 @@ class SubscriptionOverdueReminder extends Notification */ public function toPushover(User $notifiable): PushoverMessage { - return PushoverMessage::create((string) trans('email.bill_warning_please_action')) - ->title($this->getSubject()) - ; + return PushoverMessage::create((string)trans('email.bill_warning_please_action')) + ->title($this->getSubject()); } /** @@ -86,10 +94,9 @@ class SubscriptionOverdueReminder extends Notification return new SlackMessage() ->warning() ->attachment(static function ($attachment) use ($bill, $url): void { - $attachment->title((string) trans('firefly.visit_bill', ['name' => $bill->name]), $url); + $attachment->title((string)trans('firefly.visit_bill', ['name' => $bill->name]), $url); }) - ->content($this->getSubject()) - ; + ->content($this->getSubject()); } /** diff --git a/app/Providers/EventServiceProvider.php b/app/Providers/EventServiceProvider.php index 862658ca3a..ede84b5ea4 100644 --- a/app/Providers/EventServiceProvider.php +++ b/app/Providers/EventServiceProvider.php @@ -28,7 +28,7 @@ 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\Bill\WarnUserAboutOverdueSubscriptions; use FireflyIII\Events\Model\BudgetLimit\Created; use FireflyIII\Events\Model\BudgetLimit\Deleted; use FireflyIII\Events\Model\BudgetLimit\Updated; @@ -203,8 +203,8 @@ class EventServiceProvider extends ServiceProvider WarnUserAboutBill::class => [ 'FireflyIII\Handlers\Events\BillEventHandler@warnAboutBill', ], - WarnUserAboutOverdueSubscription::class => [ - 'FireflyIII\Handlers\Events\BillEventHandler@warnAboutOverdueSubscription', + WarnUserAboutOverdueSubscriptions::class => [ + 'FireflyIII\Handlers\Events\BillEventHandler@warnAboutOverdueSubscriptions', ], // audit log events: diff --git a/resources/lang/en_US/email.php b/resources/lang/en_US/email.php index 5531caac6d..bdf8a2ce0f 100644 --- a/resources/lang/en_US/email.php +++ b/resources/lang/en_US/email.php @@ -139,10 +139,11 @@ return [ '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.', + 'subscriptions_overdue_subject_multi' => 'You have :count subscriptions that are overdue to be paid', + 'subscriptions_overdue_subject_single' => 'You have a subscription that is overdue to be paid', + 'subscriptions_overdue_warning_intro' => 'You have :count subscription(s) that are overdue to be paid. At the following date(s) a payment was expected, but it has not yet arrived.', + 'subscriptions_overdue_please_action' => 'Perhaps you have simply not linked a transaction to these subscription(s). In that case, please do so. You will NOT get another warning about these overdue bill(s).', + 'subscriptions_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 deleted file mode 100644 index 228ccc843f..0000000000 --- a/resources/views/emails/subscription-overdue-warning.blade.php +++ /dev/null @@ -1,10 +0,0 @@ -@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/emails/subscriptions-overdue-warning.blade.php b/resources/views/emails/subscriptions-overdue-warning.blade.php new file mode 100644 index 0000000000..afdef5e172 --- /dev/null +++ b/resources/views/emails/subscriptions-overdue-warning.blade.php @@ -0,0 +1,17 @@ +@component('mail::message') +{{ trans('email.subscriptions_overdue_warning_intro', ['count' => $count]) }} + +@foreach($info as $row) +- {{ $row['bill']->name }}: + @foreach($row['pay_dates'] as $date) + - {{ $date }} +@endforeach +@endforeach + +{{ trans('email.subscriptions_overdue_please_action') }} + +{{ trans('email.subscriptions_overdue_outro') }} + + + +@endcomponent