Compare commits

...

4 Commits

Author SHA1 Message Date
github-actions[bot]
661e4e53e6 Merge pull request #10729 from firefly-iii/release-1754719442
🤖 Automatically merge the PR into the develop branch.
2025-08-09 08:04:11 +02:00
JC5
3eeda4a6aa 🤖 Auto commit for release 'develop' on 2025-08-09 2025-08-09 08:04:02 +02:00
James Cole
4dba9cea21 Fire warnings for bills and expand webhook message cron job (#10696 and #10703 and #6836) 2025-08-09 07:59:38 +02:00
James Cole
6aab5fab05 Fix #9650 2025-08-09 06:59:55 +02:00
29 changed files with 607 additions and 180 deletions

View File

@@ -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);
}

View File

@@ -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,26 @@ 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));
}
}
}

View File

@@ -0,0 +1,20 @@
<?php
declare(strict_types=1);
namespace FireflyIII\Events\Model\Bill;
use FireflyIII\Events\Event;
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) {}
}

View File

@@ -0,0 +1,17 @@
<?php
declare(strict_types=1);
namespace FireflyIII\Events\Model\Bill;
use FireflyIII\Events\Event;
use FireflyIII\Models\Bill;
use Illuminate\Queue\SerializesModels;
class WarnUserAboutOverdueSubscription extends Event
{
use SerializesModels;
public function __construct(public Bill $bill, public array $dates) {}
}

View File

@@ -1,38 +0,0 @@
<?php
/**
* DestroyedTransactionGroup.php
* Copyright (c) 2019 james@firefly-iii.org
*
* This file is part of Firefly III (https://github.com/firefly-iii).
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
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) {}
}

View File

@@ -24,48 +24,95 @@ 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.');
}
}

View File

@@ -57,7 +57,7 @@ class IndexController extends Controller
$this->middleware(
function ($request, $next) {
app('view')->share('title', (string) trans('firefly.bills'));
app('view')->share('title', (string)trans('firefly.bills'));
app('view')->share('mainTitleIcon', 'fa-calendar-o');
$this->repository = app(BillRepositoryInterface::class);
@@ -79,7 +79,6 @@ class IndexController extends Controller
$total = $collection->count();
$parameters = new ParameterBag();
// sub one day from temp start so the last paid date is one day before it should be.
$tempStart = clone $start;
@@ -112,7 +111,7 @@ class IndexController extends Controller
$bills = [
0 => [ // the index is the order, not the ID.
'object_group_id' => 0,
'object_group_title' => (string) trans('firefly.default_group_title_name'),
'object_group_title' => (string)trans('firefly.default_group_title_name'),
'bills' => [],
],
];
@@ -120,7 +119,7 @@ class IndexController extends Controller
/** @var Bill $bill */
foreach ($collection as $bill) {
$array = $transformer->transform($bill);
$groupOrder = (int) $array['object_group_order'];
$groupOrder = (int)$array['object_group_order'];
// make group array if necessary:
$bills[$groupOrder] ??= [
'object_group_id' => $array['object_group_id'],
@@ -173,16 +172,28 @@ class IndexController extends Controller
'currency_symbol' => $bill['currency_symbol'],
'currency_decimal_places' => $bill['currency_decimal_places'],
'avg' => '0',
'total_left_to_pay' => '0',
'period' => $range,
'per_period' => '0',
];
// only fill in avg when bill is active.
if (null !== $bill['next_expected_match']) {
$avg = bcdiv(bcadd((string) $bill['amount_min'], (string) $bill['amount_max']), '2');
$avg = bcmul($avg, (string) count($bill['pay_dates']));
$avg = bcdiv(bcadd((string)$bill['amount_min'], (string)$bill['amount_max']), '2');
$avg = bcmul($avg, (string)count($bill['pay_dates']));
$sums[$groupOrder][$currencyId]['avg'] = bcadd($sums[$groupOrder][$currencyId]['avg'], $avg);
}
// only fill in total_left_to_pay when bill is not yet paid.
if (count($bill['paid_dates']) < count($bill['pay_dates'])) {
$count = count($bill['pay_dates']) - count($bill['paid_dates']);
if ($count > 0) {
$avg = bcdiv(bcadd((string)$bill['amount_min'], (string)$bill['amount_max']), '2');
$avg = bcmul($avg, (string)$count);
$sums[$groupOrder][$currencyId]['total_left_to_pay'] = bcadd($sums[$groupOrder][$currencyId]['total_left_to_pay'], $avg);
}
}
// fill in per period regardless:
$sums[$groupOrder][$currencyId]['per_period'] = bcadd($sums[$groupOrder][$currencyId]['per_period'], $this->amountPerPeriod($bill, $range));
}
@@ -193,7 +204,7 @@ class IndexController extends Controller
private function amountPerPeriod(array $bill, string $range): string
{
$avg = bcdiv(bcadd((string) $bill['amount_min'], (string) $bill['amount_max']), '2');
$avg = bcdiv(bcadd((string)$bill['amount_min'], (string)$bill['amount_max']), '2');
app('log')->debug(sprintf('Amount per period for bill #%d "%s"', $bill['id'], $bill['name']));
app('log')->debug(sprintf('Average is %s', $avg));
@@ -206,8 +217,8 @@ class IndexController extends Controller
'weekly' => '52.17',
'daily' => '365.24',
];
$yearAmount = bcmul($avg, bcdiv($multiplies[$bill['repeat_freq']], (string) ($bill['skip'] + 1)));
app('log')->debug(sprintf('Amount per year is %s (%s * %s / %s)', $yearAmount, $avg, $multiplies[$bill['repeat_freq']], (string) ($bill['skip'] + 1)));
$yearAmount = bcmul($avg, bcdiv($multiplies[$bill['repeat_freq']], (string)($bill['skip'] + 1)));
app('log')->debug(sprintf('Amount per year is %s (%s * %s / %s)', $yearAmount, $avg, $multiplies[$bill['repeat_freq']], (string)($bill['skip'] + 1)));
// per period:
$division = [
@@ -258,8 +269,8 @@ class IndexController extends Controller
'period' => $entry['period'],
'per_period' => '0',
];
$totals[$currencyId]['avg'] = bcadd($totals[$currencyId]['avg'], (string) $entry['avg']);
$totals[$currencyId]['per_period'] = bcadd($totals[$currencyId]['per_period'], (string) $entry['per_period']);
$totals[$currencyId]['avg'] = bcadd($totals[$currencyId]['avg'], (string)$entry['avg']);
$totals[$currencyId]['per_period'] = bcadd($totals[$currencyId]['per_period'], (string)$entry['per_period']);
}
}
@@ -271,8 +282,8 @@ class IndexController extends Controller
*/
public function setOrder(Request $request, Bill $bill): JsonResponse
{
$objectGroupTitle = (string) $request->get('objectGroupTitle');
$newOrder = (int) $request->get('order');
$objectGroupTitle = (string)$request->get('objectGroupTitle');
$newOrder = (int)$request->get('order');
$this->repository->setOrder($bill, $newOrder);
if ('' !== $objectGroupTitle) {
$this->repository->setObjectGroup($bill, $objectGroupTitle);

View File

@@ -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);

View File

@@ -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
@@ -63,7 +67,7 @@ class WarnAboutBills implements ShouldQueue
$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,13 +136,13 @@ 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));
}
@@ -149,4 +157,47 @@ 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));
}
}

View File

@@ -0,0 +1,102 @@
<?php
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;
use Illuminate\Bus\Queueable;
use Illuminate\Notifications\Messages\MailMessage;
use Illuminate\Notifications\Messages\SlackMessage;
use Illuminate\Notifications\Notification;
use NotificationChannels\Pushover\PushoverMessage;
class SubscriptionOverdueReminder extends Notification
{
use Queueable;
public function __construct(private Bill $bill, private array $dates) {}
/**
* @SuppressWarnings("PHPMD.UnusedFormalParameter")
*/
public function toArray(User $notifiable): array
{
return [
];
}
/**
* @SuppressWarnings("PHPMD.UnusedFormalParameter")
*/
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']
);
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);
}
}

View File

@@ -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;
@@ -114,150 +115,153 @@ class EventServiceProvider extends ServiceProvider
protected $listen
= [
// is a User related event.
RegisteredUser::class => [
RegisteredUser::class => [
'FireflyIII\Handlers\Events\UserEventHandler@sendRegistrationMail',
'FireflyIII\Handlers\Events\UserEventHandler@sendAdminRegistrationNotification',
'FireflyIII\Handlers\Events\UserEventHandler@attachUserRole',
'FireflyIII\Handlers\Events\UserEventHandler@createGroupMembership',
'FireflyIII\Handlers\Events\UserEventHandler@createExchangeRates',
],
UserAttemptedLogin::class => [
UserAttemptedLogin::class => [
'FireflyIII\Handlers\Events\UserEventHandler@sendLoginAttemptNotification',
],
// is a User related event.
Login::class => [
Login::class => [
'FireflyIII\Handlers\Events\UserEventHandler@checkSingleUserIsAdmin',
'FireflyIII\Handlers\Events\UserEventHandler@demoUserBackToEnglish',
],
ActuallyLoggedIn::class => [
ActuallyLoggedIn::class => [
'FireflyIII\Handlers\Events\UserEventHandler@storeUserIPAddress',
],
DetectedNewIPAddress::class => [
DetectedNewIPAddress::class => [
'FireflyIII\Handlers\Events\UserEventHandler@notifyNewIPAddress',
],
RequestedVersionCheckStatus::class => [
RequestedVersionCheckStatus::class => [
'FireflyIII\Handlers\Events\VersionCheckEventHandler@checkForUpdates',
],
RequestedReportOnJournals::class => [
RequestedReportOnJournals::class => [
'FireflyIII\Handlers\Events\AutomationHandler@reportJournals',
],
// is a User related event.
RequestedNewPassword::class => [
RequestedNewPassword::class => [
'FireflyIII\Handlers\Events\UserEventHandler@sendNewPassword',
],
UserTestNotificationChannel::class => [
UserTestNotificationChannel::class => [
'FireflyIII\Handlers\Events\UserEventHandler@sendTestNotification',
],
// is a User related event.
UserChangedEmail::class => [
UserChangedEmail::class => [
'FireflyIII\Handlers\Events\UserEventHandler@sendEmailChangeConfirmMail',
'FireflyIII\Handlers\Events\UserEventHandler@sendEmailChangeUndoMail',
],
// admin related
OwnerTestNotificationChannel::class => [
OwnerTestNotificationChannel::class => [
'FireflyIII\Handlers\Events\AdminEventHandler@sendTestNotification',
],
NewVersionAvailable::class => [
NewVersionAvailable::class => [
'FireflyIII\Handlers\Events\AdminEventHandler@sendNewVersion',
],
InvitationCreated::class => [
InvitationCreated::class => [
'FireflyIII\Handlers\Events\AdminEventHandler@sendInvitationNotification',
'FireflyIII\Handlers\Events\UserEventHandler@sendRegistrationInvite',
],
UnknownUserAttemptedLogin::class => [
UnknownUserAttemptedLogin::class => [
'FireflyIII\Handlers\Events\AdminEventHandler@sendLoginAttemptNotification',
],
// is a Transaction Journal related event.
StoredTransactionGroup::class => [
StoredTransactionGroup::class => [
'FireflyIII\Handlers\Events\StoredGroupEventHandler@runAllHandlers',
],
// is a Transaction Journal related event.
UpdatedTransactionGroup::class => [
UpdatedTransactionGroup::class => [
'FireflyIII\Handlers\Events\UpdatedGroupEventHandler@runAllHandlers',
],
DestroyedTransactionGroup::class => [
DestroyedTransactionGroup::class => [
'FireflyIII\Handlers\Events\DestroyedGroupEventHandler@runAllHandlers',
],
// API related events:
AccessTokenCreated::class => [
AccessTokenCreated::class => [
'FireflyIII\Handlers\Events\APIEventHandler@accessTokenCreated',
],
// Webhook related event:
RequestedSendWebhookMessages::class => [
RequestedSendWebhookMessages::class => [
'FireflyIII\Handlers\Events\WebhookEventHandler@sendWebhookMessages',
],
// account related events:
StoredAccount::class => [
StoredAccount::class => [
'FireflyIII\Handlers\Events\StoredAccountEventHandler@recalculateCredit',
],
UpdatedAccount::class => [
UpdatedAccount::class => [
'FireflyIII\Handlers\Events\UpdatedAccountEventHandler@recalculateCredit',
],
// bill related events:
WarnUserAboutBill::class => [
WarnUserAboutBill::class => [
'FireflyIII\Handlers\Events\BillEventHandler@warnAboutBill',
],
WarnUserAboutOverdueSubscription::class => [
'FireflyIII\Handlers\Events\BillEventHandler@warnAboutOverdueSubscription',
],
// audit log events:
TriggeredAuditLog::class => [
TriggeredAuditLog::class => [
'FireflyIII\Handlers\Events\AuditEventHandler@storeAuditEvent',
],
// piggy bank related events:
ChangedAmount::class => [
ChangedAmount::class => [
'FireflyIII\Handlers\Events\Model\PiggyBankEventHandler@changePiggyAmount',
],
ChangedName::class => [
ChangedName::class => [
'FireflyIII\Handlers\Events\Model\PiggyBankEventHandler@changedPiggyBankName',
],
// budget related events: CRUD budget limit
Created::class => [
Created::class => [
'FireflyIII\Handlers\Events\Model\BudgetLimitHandler@created',
],
Updated::class => [
Updated::class => [
'FireflyIII\Handlers\Events\Model\BudgetLimitHandler@updated',
],
Deleted::class => [
Deleted::class => [
'FireflyIII\Handlers\Events\Model\BudgetLimitHandler@deleted',
],
// rule actions
RuleActionFailedOnArray::class => [
RuleActionFailedOnArray::class => [
'FireflyIII\Handlers\Events\Model\RuleHandler@ruleActionFailedOnArray',
],
RuleActionFailedOnObject::class => [
RuleActionFailedOnObject::class => [
'FireflyIII\Handlers\Events\Model\RuleHandler@ruleActionFailedOnObject',
],
// security related
EnabledMFA::class => [
EnabledMFA::class => [
'FireflyIII\Handlers\Events\Security\MFAHandler@sendMFAEnabledMail',
],
DisabledMFA::class => [
DisabledMFA::class => [
'FireflyIII\Handlers\Events\Security\MFAHandler@sendMFADisabledMail',
],
MFANewBackupCodes::class => [
MFANewBackupCodes::class => [
'FireflyIII\Handlers\Events\Security\MFAHandler@sendNewMFABackupCodesMail',
],
MFAUsedBackupCode::class => [
MFAUsedBackupCode::class => [
'FireflyIII\Handlers\Events\Security\MFAHandler@sendUsedBackupCodeMail',
],
MFABackupFewLeft::class => [
MFABackupFewLeft::class => [
'FireflyIII\Handlers\Events\Security\MFAHandler@sendBackupFewLeftMail',
],
MFABackupNoLeft::class => [
MFABackupNoLeft::class => [
'FireflyIII\Handlers\Events\Security\MFAHandler@sendBackupNoLeftMail',
],
MFAManyFailedAttempts::class => [
MFAManyFailedAttempts::class => [
'FireflyIII\Handlers\Events\Security\MFAHandler@sendMFAFailedAttemptsMail',
],
// preferences
UserGroupChangedPrimaryCurrency::class => [
UserGroupChangedPrimaryCurrency::class => [
'FireflyIII\Handlers\Events\PreferencesEventHandler@resetPrimaryCurrencyAmounts',
],
];

View File

@@ -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

View File

@@ -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.');
}
}

View File

@@ -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.');
}
}

View File

@@ -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.');
}
}

View File

@@ -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.');

View File

@@ -0,0 +1,97 @@
<?php
/**
* RecurringCronjob.php
* Copyright (c) 2019 james@firefly-iii.org
*
* This file is part of Firefly III (https://github.com/firefly-iii).
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
declare(strict_types=1);
namespace FireflyIII\Support\Cronjobs;
use Carbon\Carbon;
use FireflyIII\Events\RequestedSendWebhookMessages;
use FireflyIII\Exceptions\FireflyException;
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.');
}
}

View File

@@ -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 */

View File

@@ -117,14 +117,14 @@ trait PeriodOverview
[$transactions, $transferredIn] = $this->filterTransfers('in', $transactions, $currentDate['start'], $currentDate['end']);
$entries[]
= [
'title' => $title,
'route' => route('accounts.show', [$account->id, $currentDate['start']->format('Y-m-d'), $currentDate['end']->format('Y-m-d')]),
'total_transactions' => count($spent) + count($earned) + count($transferredAway) + count($transferredIn),
'spent' => $this->groupByCurrency($spent),
'earned' => $this->groupByCurrency($earned),
'transferred_away' => $this->groupByCurrency($transferredAway),
'transferred_in' => $this->groupByCurrency($transferredIn),
];
'title' => $title,
'route' => route('accounts.show', [$account->id, $currentDate['start']->format('Y-m-d'), $currentDate['end']->format('Y-m-d')]),
'total_transactions' => count($spent) + count($earned) + count($transferredAway) + count($transferredIn),
'spent' => $this->groupByCurrency($spent),
'earned' => $this->groupByCurrency($earned),
'transferred_away' => $this->groupByCurrency($transferredAway),
'transferred_in' => $this->groupByCurrency($transferredIn),
];
++$loops;
}
$cache->store($entries);
@@ -590,13 +590,13 @@ trait PeriodOverview
}
$entries[]
= [
'title' => $title,
'route' => route('transactions.index', [$transactionType, $currentDate['start']->format('Y-m-d'), $currentDate['end']->format('Y-m-d')]),
'total_transactions' => count($spent) + count($earned) + count($transferred),
'spent' => $this->groupByCurrency($spent),
'earned' => $this->groupByCurrency($earned),
'transferred' => $this->groupByCurrency($transferred),
];
'title' => $title,
'route' => route('transactions.index', [$transactionType, $currentDate['start']->format('Y-m-d'), $currentDate['end']->format('Y-m-d')]),
'total_transactions' => count($spent) + count($earned) + count($transferred),
'spent' => $this->groupByCurrency($spent),
'earned' => $this->groupByCurrency($earned),
'transferred' => $this->groupByCurrency($transferred),
];
++$loops;
}

View File

@@ -56,6 +56,7 @@ class SubscriptionEnrichment implements EnrichmentInterface
$this->collectPaidDates();
$this->collectPayDates();
// TODO clean me up.
$notes = $this->notes;

View File

@@ -110,8 +110,8 @@ class BillDateCalculator
$currentStart = clone $nextExpectedMatch;
++$loop;
if ($loop > 12) {
Log::debug('Loop is more than 12, so we break.');
if ($loop > 31) {
Log::debug('Loop is more than 31, so we break.');
break;
}

12
composer.lock generated
View File

@@ -10347,16 +10347,16 @@
},
{
"name": "driftingly/rector-laravel",
"version": "2.0.5",
"version": "2.0.6",
"source": {
"type": "git",
"url": "https://github.com/driftingly/rector-laravel.git",
"reference": "ac61de4f267c23249d175d7fc9149fd01528567d"
"reference": "5be95811801fc06126dd844beaeb6a41721ba3d3"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/driftingly/rector-laravel/zipball/ac61de4f267c23249d175d7fc9149fd01528567d",
"reference": "ac61de4f267c23249d175d7fc9149fd01528567d",
"url": "https://api.github.com/repos/driftingly/rector-laravel/zipball/5be95811801fc06126dd844beaeb6a41721ba3d3",
"reference": "5be95811801fc06126dd844beaeb6a41721ba3d3",
"shasum": ""
},
"require": {
@@ -10376,9 +10376,9 @@
"description": "Rector upgrades rules for Laravel Framework",
"support": {
"issues": "https://github.com/driftingly/rector-laravel/issues",
"source": "https://github.com/driftingly/rector-laravel/tree/2.0.5"
"source": "https://github.com/driftingly/rector-laravel/tree/2.0.6"
},
"time": "2025-05-14T17:30:41+00:00"
"time": "2025-08-08T22:10:01+00:00"
},
{
"name": "fakerphp/faker",

View File

@@ -78,8 +78,8 @@ return [
'running_balance_column' => env('USE_RUNNING_BALANCE', false),
// see cer.php for exchange rates feature flag.
],
'version' => 'develop/2025-08-08',
'build_time' => 1754679717,
'version' => 'develop/2025-08-09',
'build_time' => 1754719342,
'api_version' => '2.1.0', // field is no longer used.
'db_version' => 26,

View File

@@ -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',

View File

@@ -1864,6 +1864,7 @@ return [
'remove_budgeted_amount' => 'Remove budgeted amount in :currency',
// bills:
'left_to_pay_active_bills' => 'active, expected and not yet paid subscriptions',
'left_to_pay_lc' => 'left to pay',
'less_than_expected' => 'less than expected',
'more_than_expected' => 'more than expected',

View File

@@ -158,8 +158,8 @@
<td style="width:33%;">{{ 'amount'|_ }}</td>
<td>
{{ formatAmountBySymbol(limit.amount, limit.transactionCurrency.symbol, limit.transactionCurrency.decimal_places) }}
{% if convertToPrimary and 0 != limit.pc_amount %}
({{ formatAmountBySymbol(limit.pc_amount, primaryCurrency.symbol, primaryCurrency.decimal_places) }})
{% if convertToPrimary and null != limit.native_amount %}
({{ formatAmountBySymbol(limit.native_amount, primaryCurrency.symbol, primaryCurrency.decimal_places) }})
{% endif %}
</td>
</tr>

View File

@@ -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

View File

@@ -189,6 +189,21 @@
<td class="hidden-sm hidden-xs">&nbsp;</td><!-- repeats -->
</tr>
{% endif %}
{% if '0' != sum.total_left_to_pay %}
<tr>
<td class="hidden-sm hidden-xs">&nbsp;</td> <!-- handle -->
<td class="hidden-sm hidden-xs">&nbsp;</td> <!-- buttons -->
<td colspan="2" style="text-align: right;"> <!-- title -->
<small>{{ 'sum'|_ }} ({{ sum.currency_name }}) ({{ 'left_to_pay_active_bills'|_ }})</small>
</td>
<td style="text-align: right;"> <!-- amount -->
{{ formatAmountBySymbol(sum.total_left_to_pay, sum.currency_symbol, sum.currency_decimal_places) }}
</td>
<td>&nbsp;</td> <!-- paid in period -->
<td class="hidden-sm hidden-xs">&nbsp;</td> <!-- next expected match -->
<td class="hidden-sm hidden-xs">&nbsp;</td><!-- repeats -->
</tr>
{% endif %}
{% if '0' != sum.per_period %}
<tr>
<td class="hidden-sm hidden-xs">&nbsp;</td> <!-- handle -->

View File

@@ -6,8 +6,14 @@
@endcomponent
@endslot
{{-- Body --}}
{{ $slot }}
{{-- Body --}}
{{ trans('email.greeting') }}
{{ $slot }}
{{ trans('email.closing') }}
{{ trans('email.signature')}}
{{-- Subcopy --}}
@isset($subcopy)