Compare commits

...

39 Commits

Author SHA1 Message Date
github-actions[bot]
71d39707d9 Merge pull request #10738 from firefly-iii/release-1754800979
🤖 Automatically merge the PR into the develop branch.
2025-08-10 06:43:10 +02:00
JC5
9ccb8ae692 🤖 Auto commit for release 'develop' on 2025-08-10 2025-08-10 06:42:59 +02:00
James Cole
8cd50bb5bd Merge branch 'develop' of github.com:firefly-iii/firefly-iii into develop 2025-08-10 06:38:34 +02:00
James Cole
ae9e1278e5 Expand some timers, fix reports. 2025-08-10 06:38:23 +02:00
github-actions[bot]
58c03797b2 Merge pull request #10737 from firefly-iii/release-1754766186
🤖 Automatically merge the PR into the develop branch.
2025-08-09 21:03:19 +02:00
JC5
7db38b4c6c 🤖 Auto commit for release 'develop' on 2025-08-09 2025-08-09 21:03:06 +02:00
James Cole
da6b447e64 Add some extra debug info. 2025-08-09 20:42:49 +02:00
github-actions[bot]
c19ac2b0f3 Merge pull request #10736 from firefly-iii/release-1754764141
🤖 Automatically merge the PR into the develop branch.
2025-08-09 20:29:09 +02:00
JC5
d5ca2171b3 🤖 Auto commit for release 'develop' on 2025-08-09 2025-08-09 20:29:01 +02:00
James Cole
20972cb29f Merge errors. 2025-08-09 20:24:45 +02:00
James Cole
7b714d0866 Add some time logs. 2025-08-09 20:24:15 +02:00
github-actions[bot]
240ae8fa57 Merge pull request #10735 from firefly-iii/release-1754763336
🤖 Automatically merge the PR into the develop branch.
2025-08-09 20:16:11 +02:00
JC5
5a2f6b2652 🤖 Auto commit for release 'develop' on 2025-08-09 2025-08-09 20:15:36 +02:00
James Cole
4196ce31f0 Fix account thing overview. 2025-08-09 20:10:40 +02:00
James Cole
be8ca5db50 Update changelog. 2025-08-09 19:58:59 +02:00
github-actions[bot]
30a417ea3c Merge pull request #10734 from firefly-iii/release-1754750646
🤖 Automatically merge the PR into the develop branch.
2025-08-09 16:44:16 +02:00
JC5
695ed940e0 🤖 Auto commit for release 'develop' on 2025-08-09 2025-08-09 16:44:06 +02:00
James Cole
1353554cf8 Remove expensive call from loop. 2025-08-09 16:38:32 +02:00
James Cole
e1ba2732af Remove non-optimized method. 2025-08-09 16:34:25 +02:00
James Cole
42b57c0e0e Migrate to optimized method. 2025-08-09 16:31:11 +02:00
James Cole
a6072753b2 Remove PR 2025-08-09 16:01:44 +02:00
github-actions[bot]
e92c224c39 Merge pull request #10733 from firefly-iii/release-1754748022
🤖 Automatically merge the PR into the develop branch.
2025-08-09 16:00:30 +02:00
JC5
a3ed7ec8f6 🤖 Auto commit for release 'develop' on 2025-08-09 2025-08-09 16:00:22 +02:00
James Cole
17a2f99dff Order and palce in changelog. 2025-08-09 15:55:29 +02:00
James Cole
c14971543c Update changelog. 2025-08-09 15:43:36 +02:00
James Cole
55f899608d Add multi currency to piggy overview. 2025-08-09 15:33:03 +02:00
github-actions[bot]
83be63f27e Merge pull request #10731 from firefly-iii/release-1754731244
🤖 Automatically merge the PR into the develop branch.
2025-08-09 11:20:53 +02:00
JC5
ed48d190e5 🤖 Auto commit for release 'develop' on 2025-08-09 2025-08-09 11:20:44 +02:00
James Cole
3c3b6615e6 Make message singular / plural 2025-08-09 11:16:46 +02:00
James Cole
e71e5a877b Fix count for overdue bills. 2025-08-09 11:13:03 +02:00
James Cole
b2a65dc660 Small change in text. 2025-08-09 08:48:25 +02:00
James Cole
d66dccd076 Fix bad slack URL 2025-08-09 08:47:22 +02:00
github-actions[bot]
c1128b28f2 Merge pull request #10730 from firefly-iii/release-1754721510
🤖 Automatically merge the PR into the develop branch.
2025-08-09 08:38:39 +02:00
JC5
da8e78c28d 🤖 Auto commit for release 'develop' on 2025-08-09 2025-08-09 08:38:30 +02:00
James Cole
f50aa6b0ce Fix spam whoopsie. 2025-08-09 08:34:18 +02:00
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
44 changed files with 859 additions and 302 deletions

View File

@@ -31,6 +31,7 @@ use FireflyIII\Exceptions\FireflyException;
use FireflyIII\Models\Account;
use FireflyIII\Repositories\Account\AccountRepositoryInterface;
use FireflyIII\Support\Debug\Timer;
use FireflyIII\Support\Facades\Amount;
use FireflyIII\Support\Facades\Steam;
use FireflyIII\Support\Http\Api\AccountFilter;
use FireflyIII\User;
@@ -79,17 +80,20 @@ class AccountController extends Controller
*/
public function accounts(AutocompleteRequest $request): JsonResponse
{
$data = $request->getData();
$types = $data['types'];
$query = $data['query'];
$date = $data['date'] ?? today(config('app.timezone'));
$return = [];
Timer::start(sprintf('AC accounts "%s"', $query));
$result = $this->repository->searchAccount((string) $query, $types, $this->parameters->get('limit'));
$data = $request->getData();
$types = $data['types'];
$query = $data['query'];
$date = $data['date'] ?? today(config('app.timezone'));
$return = [];
$timer = Timer::getInstance();
$timer->start(sprintf('AC accounts "%s"', $query));
$result = $this->repository->searchAccount((string) $query, $types, $this->parameters->get('limit'));
// set date to subday + end-of-day for account balance. so it is at $date 23:59:59
$date->endOfDay();
$allBalances = Steam::accountsBalancesOptimized($result, $date, $this->primaryCurrency, $this->convertToPrimary);
/** @var Account $account */
foreach ($result as $account) {
$nameWithBalance = $account->name;
@@ -98,15 +102,11 @@ class AccountController extends Controller
if (in_array($account->accountType->type, $this->balanceTypes, true)) {
// this one is correct.
Log::debug(sprintf('accounts: Call finalAccountBalance with date/time "%s"', $date->toIso8601String()));
$balance = Steam::finalAccountBalance($account, $date);
$balance = $allBalances[$account->id] ?? [];
$key = $this->convertToPrimary && $currency->id !== $this->primaryCurrency->id ? 'pc_balance' : 'balance';
$useCurrency = $this->convertToPrimary && $currency->id !== $this->primaryCurrency->id ? $this->primaryCurrency : $currency;
$amount = $balance[$key] ?? '0';
$nameWithBalance = sprintf(
'%s (%s)',
$account->name,
app('amount')->formatAnything($useCurrency, $amount, false)
);
$nameWithBalance = sprintf('%s (%s)', $account->name, Amount::formatAnything($useCurrency, $amount, false));
}
$return[] = [
@@ -138,7 +138,7 @@ class AccountController extends Controller
return $posA - $posB;
}
);
Timer::stop(sprintf('AC accounts "%s"', $query));
$timer->stop(sprintf('AC accounts "%s"', $query));
return response()->api($return);
}

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\User;
use Illuminate\Queue\SerializesModels;
class WarnUserAboutOverdueSubscriptions extends Event
{
use SerializesModels;
public function __construct(public User $user, public array $overdue) {}
}

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,118 @@ 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\WarnUserAboutOverdueSubscriptions;
use FireflyIII\Models\Bill;
use FireflyIII\Notifications\User\BillReminder;
use FireflyIII\Notifications\User\SubscriptionsOverdueReminder;
use FireflyIII\Support\Facades\Preferences;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Notification;
/**
* Class BillEventHandler
*/
class BillEventHandler
{
public function warnAboutOverdueSubscriptions(WarnUserAboutOverdueSubscriptions $event): void
{
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($user, 'notification_bill_reminder', true)->data;
if (false === $sendNotification) {
Log::debug('User has disabled bill reminders.');
return;
}
Log::debug(sprintf('Will warn 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($toBeWarned));
} 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
{
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

@@ -77,8 +77,8 @@ class NetWorth implements NetWorthInterface
Log::debug(sprintf('Now in byAccounts("%s", "%s")', $ids, $date->format('Y-m-d H:i:s')));
$primary = Amount::getPrimaryCurrency();
$netWorth = [];
Log::debug(sprintf('NetWorth: finalAccountsBalance("%s")', $date->format('Y-m-d H:i:s')));
$balances = Steam::finalAccountsBalance($accounts, $date);
Log::debug(sprintf('NetWorth: accountsBalancesOptimized("%s")', $date->format('Y-m-d H:i:s')));
$balances = Steam::accountsBalancesOptimized($accounts, $date, null, $convertToPrimary);
/** @var Account $account */
foreach ($accounts as $account) {
@@ -143,8 +143,8 @@ class NetWorth implements NetWorthInterface
*/
$accounts = $this->getAccounts();
$return = [];
Log::debug(sprintf('SumNetWorth: finalAccountsBalance("%s")', $date->format('Y-m-d H:i:s')));
$balances = Steam::finalAccountsBalance($accounts, $date);
Log::debug(sprintf('SumNetWorth: accountsBalancesOptimized("%s")', $date->format('Y-m-d H:i:s')));
$balances = Steam::accountsBalancesOptimized($accounts, $date);
foreach ($accounts as $account) {
$currency = $this->accountRepository->getAccountCurrency($account);
$balance = $balances[$account->id]['balance'] ?? '0';

View File

@@ -93,10 +93,10 @@ class IndexController extends Controller
$start->subSecond();
$ids = $accounts->pluck('id')->toArray();
Log::debug(sprintf('inactive start: finalAccountsBalance("%s")', $start->format('Y-m-d H:i:s')));
Log::debug(sprintf('inactive end: finalAccountsBalance("%s")', $end->format('Y-m-d H:i:s')));
$startBalances = Steam::finalAccountsBalance($accounts, $start);
$endBalances = Steam::finalAccountsBalance($accounts, $end);
Log::debug(sprintf('inactive start: accountsBalancesOptimized("%s")', $start->format('Y-m-d H:i:s')));
Log::debug(sprintf('inactive end: accountsBalancesOptimized("%s")', $end->format('Y-m-d H:i:s')));
$startBalances = Steam::accountsBalancesOptimized($accounts, $start, $this->primaryCurrency, $this->convertToPrimary);
$endBalances = Steam::accountsBalancesOptimized($accounts, $end, $this->primaryCurrency, $this->convertToPrimary);
$activities = Steam::getLastActivities($ids);
@@ -170,10 +170,10 @@ class IndexController extends Controller
$start->subSecond();
$ids = $accounts->pluck('id')->toArray();
Log::debug(sprintf('index start: finalAccountsBalance("%s")', $start->format('Y-m-d H:i:s')));
Log::debug(sprintf('index end: finalAccountsBalance("%s")', $end->format('Y-m-d H:i:s')));
$startBalances = Steam::finalAccountsBalance($accounts, $start);
$endBalances = Steam::finalAccountsBalance($accounts, $end);
Log::debug(sprintf('index start: accountsBalancesOptimized("%s")', $start->format('Y-m-d H:i:s')));
Log::debug(sprintf('index end: accountsBalancesOptimized("%s")', $end->format('Y-m-d H:i:s')));
$startBalances = Steam::accountsBalancesOptimized($accounts, $start, $this->primaryCurrency, $this->convertToPrimary);
$endBalances = Steam::accountsBalancesOptimized($accounts, $end, $this->primaryCurrency, $this->convertToPrimary);
$activities = Steam::getLastActivities($ids);

View File

@@ -40,6 +40,7 @@ use Illuminate\Routing\Redirector;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Log;
use Illuminate\View\View;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
/**
* Class ShowController
@@ -81,7 +82,9 @@ class ShowController extends Controller
* */
public function show(Request $request, Account $account, ?Carbon $start = null, ?Carbon $end = null)
{
if (0 === $account->id) {
throw new NotFoundHttpException();
}
$objectType = config(sprintf('firefly.shortNamesByFullName.%s', $account->accountType->type));
if (!$this->isEditableAccount($account)) {
@@ -116,18 +119,19 @@ class ShowController extends Controller
$firstTransaction = $this->repository->oldestJournalDate($account) ?? $start;
Log::debug('Start period overview');
Timer::start('period-overview');
$timer = Timer::getInstance();
$timer->start('period-overview');
$periods = $this->getAccountPeriodOverview($account, $firstTransaction, $end);
Log::debug('End period overview');
Timer::stop('period-overview');
$timer->stop('period-overview');
// if layout = v2, overrule the page title.
if ('v1' !== config('view.layout')) {
$subTitle = (string) trans('firefly.all_journals_for_account', ['name' => $account->name]);
}
Log::debug('Collect transactions');
Timer::start('collection');
$timer->start('collection');
/** @var GroupCollectorInterface $collector */
$collector = app(GroupCollectorInterface::class);
@@ -146,7 +150,7 @@ class ShowController extends Controller
Log::debug('End collect transactions');
Timer::stop('collection');
$timer->stop('collection');
// enrich data in arrays.

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

@@ -114,10 +114,10 @@ class AccountController extends Controller
$accountNames = $this->extractNames($accounts);
// grab all balances
Log::debug(sprintf('expenseAccounts: finalAccountsBalance("%s")', $start->format('Y-m-d H:i:s')));
Log::debug(sprintf('expenseAccounts: finalAccountsBalance("%s")', $end->format('Y-m-d H:i:s')));
$startBalances = Steam::finalAccountsBalance($accounts, $start, $this->primaryCurrency, $this->convertToPrimary);
$endBalances = Steam::finalAccountsBalance($accounts, $end, $this->primaryCurrency, $this->convertToPrimary);
Log::debug(sprintf('expenseAccounts: accountsBalancesOptimized("%s")', $start->format('Y-m-d H:i:s')));
Log::debug(sprintf('expenseAccounts: accountsBalancesOptimized("%s")', $end->format('Y-m-d H:i:s')));
$startBalances = Steam::accountsBalancesOptimized($accounts, $start, $this->primaryCurrency, $this->convertToPrimary);
$endBalances = Steam::accountsBalancesOptimized($accounts, $end, $this->primaryCurrency, $this->convertToPrimary);
// loop the accounts, then check for balance and currency info.
foreach ($accounts as $account) {
@@ -654,10 +654,10 @@ class AccountController extends Controller
$accountNames = $this->extractNames($accounts);
// grab all balances
Log::debug(sprintf('revAccounts: finalAccountsBalance("%s")', $start->format('Y-m-d H:i:s')));
Log::debug(sprintf('revAccounts: finalAccountsBalance("%s")', $end->format('Y-m-d H:i:s')));
$startBalances = Steam::finalAccountsBalance($accounts, $start);
$endBalances = Steam::finalAccountsBalance($accounts, $end);
Log::debug(sprintf('revAccounts: accountsBalancesOptimized("%s")', $start->format('Y-m-d H:i:s')));
Log::debug(sprintf('revAccounts: accountsBalancesOptimized("%s")', $end->format('Y-m-d H:i:s')));
$startBalances = Steam::accountsBalancesOptimized($accounts, $start, $this->primaryCurrency, $this->convertToPrimary);
$endBalances = Steam::accountsBalancesOptimized($accounts, $end, $this->primaryCurrency, $this->convertToPrimary);
// loop the accounts, then check for balance and currency info.

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

@@ -188,16 +188,7 @@ class ReportController extends Controller
$start->endOfDay(); // end of day so the final balance is at the end of that day.
$end->endOfDay();
app('view')->share(
'subTitle',
trans(
'firefly.report_default',
[
'start' => $start->isoFormat($this->monthAndDayFormat),
'end' => $end->isoFormat($this->monthAndDayFormat),
]
)
);
app('view')->share('subTitle', trans('firefly.report_default', ['start' => $start->isoFormat($this->monthAndDayFormat), 'end' => $end->isoFormat($this->monthAndDayFormat)]));
$generator = ReportGeneratorFactory::reportGenerator('Standard', $start, $end);
$generator->setAccounts($accounts);
@@ -222,16 +213,7 @@ class ReportController extends Controller
$start->endOfDay(); // end of day so the final balance is at the end of that day.
$end->endOfDay();
app('view')->share(
'subTitle',
trans(
'firefly.report_double',
[
'start' => $start->isoFormat($this->monthAndDayFormat),
'end' => $end->isoFormat($this->monthAndDayFormat),
]
)
);
app('view')->share('subTitle', trans('firefly.report_double', ['start' => $start->isoFormat($this->monthAndDayFormat), 'end' => $end->isoFormat($this->monthAndDayFormat)]));
$generator = ReportGeneratorFactory::reportGenerator('Account', $start, $end);
$generator->setAccounts($accounts);

View File

@@ -25,13 +25,18 @@ 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\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;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Log;
/**
* Class WarnAboutBills
@@ -63,7 +68,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,36 +76,42 @@ 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')));
$bills = Bill::all();
Log::debug(sprintf('Now at start of WarnAboutBills() job for %s.', $this->date->format('D d M Y')));
foreach (User::all() as $user) {
$bills = $user->bills()->where('active', true)->get();
$overdue = [];
/** @var Bill $bill */
foreach ($bills as $bill) {
app('log')->debug(sprintf('Now checking bill #%d ("%s")', $bill->id, $bill->name));
if ($this->hasDateFields($bill)) {
if ($this->needsWarning($bill, 'end_date')) {
$this->sendWarning($bill, 'end_date');
/** @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);
}
app('log')->debug('Done with handle()');
Log::debug('Done with handle()');
// clear cache:
app('preferences')->mark();
}
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 +126,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 +139,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 +160,49 @@ 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 sendOverdueAlerts(User $user, array $overdue): void
{
if (count($overdue) > 0) {
Log::debug(sprintf('Will now send warning about overdue bill for user #%d.', $user->id));
event(new WarnUserAboutOverdueSubscriptions($user, $overdue));
}
}
}

View File

@@ -0,0 +1,113 @@
<?php
declare(strict_types=1);
namespace FireflyIII\Notifications\User;
use Carbon\Carbon;
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 SubscriptionsOverdueReminder extends Notification
{
use Queueable;
public function __construct(private array $overdue) {}
/**
* @SuppressWarnings("PHPMD.UnusedFormalParameter")
*/
public function toArray(User $notifiable): array
{
return [
];
}
/**
* @SuppressWarnings("PHPMD.UnusedFormalParameter")
*/
public function toMail(User $notifiable): MailMessage
{
// 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.subscriptions-overdue-warning', ['info' => $info, 'count' => $count])
->subject($this->getSubject())
;
}
private function getSubject(): string
{
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
{
$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
{
$url = route('bills.index');
return new SlackMessage()
->warning()
->attachment(static function ($attachment) use ($url): void {
$attachment->title((string)trans('firefly.visit_bills'), $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\WarnUserAboutOverdueSubscriptions;
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',
],
WarnUserAboutOverdueSubscriptions::class => [
'FireflyIII\Handlers\Events\BillEventHandler@warnAboutOverdueSubscriptions',
],
// 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

@@ -566,7 +566,7 @@ class AccountRepository implements AccountRepositoryInterface, UserGroupInterfac
'transaction_types.type',
'transaction_journals.transaction_currency_id',
'transactions.amount',
'transactions.native_amount',
'transactions.native_amount as pc_amount',
'transactions.foreign_amount',
])
->toArray()

View File

@@ -50,10 +50,10 @@ class AccountTasker implements AccountTaskerInterface, UserGroupInterface
$yesterday = clone $start;
$yesterday->subDay()->endOfDay(); // exactly up until $start but NOT including.
$end->endOfDay(); // needs to be end of day to be correct.
Log::debug(sprintf('getAccountReport: finalAccountsBalance("%s")', $yesterday->format('Y-m-d H:i:s')));
Log::debug(sprintf('getAccountReport: finalAccountsBalance("%s")', $end->format('Y-m-d H:i:s')));
$startSet = Steam::finalAccountsBalance($accounts, $yesterday);
$endSet = Steam::finalAccountsBalance($accounts, $end);
Log::debug(sprintf('getAccountReport: accountsBalancesOptimized("%s")', $yesterday->format('Y-m-d H:i:s')));
Log::debug(sprintf('getAccountReport: accountsBalancesOptimized("%s")', $end->format('Y-m-d H:i:s')));
$startSet = Steam::accountsBalancesOptimized($accounts, $yesterday);
$endSet = Steam::accountsBalancesOptimized($accounts, $end);
Log::debug('Start of accountreport');
/** @var AccountRepositoryInterface $repository */

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

@@ -28,19 +28,34 @@ use Illuminate\Support\Facades\Log;
class Timer
{
private static array $times = [];
private array $times = [];
private static ?Timer $instance = null;
public static function start(string $title): void
private function __construct()
{
self::$times[$title] = microtime(true);
// Private constructor to prevent direct instantiation.
}
public static function stop(string $title): void
public static function getInstance(): self
{
$start = self::$times[$title] ?? 0;
if (null === self::$instance) {
self::$instance = new self();
}
return self::$instance;
}
public function start(string $title): void
{
$this->times[$title] = microtime(true);
}
public function stop(string $title): void
{
$start = $this->times[$title] ?? 0;
$end = microtime(true);
$diff = $end - $start;
unset(self::$times[$title]);
unset($this->times[$title]);
Log::debug(sprintf('Timer "%s" took %f seconds', $title, $diff));
}
}

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

@@ -80,7 +80,8 @@ trait PeriodOverview
protected function getAccountPeriodOverview(Account $account, Carbon $start, Carbon $end): array
{
Log::debug('Now in getAccountPeriodOverview()');
Timer::start('account-period-total');
$timer = Timer::getInstance();
$timer->start('account-period-total');
$this->accountRepository = app(AccountRepositoryInterface::class);
$range = Navigation::getViewRange(true);
[$start, $end] = $end < $start ? [$end, $start] : [$start, $end];
@@ -92,23 +93,24 @@ trait PeriodOverview
$cache->addProperty('account-show-period-entries');
$cache->addProperty($account->id);
if ($cache->has()) {
// return $cache->get();
Log::debug('Return CACHED in getAccountPeriodOverview()');
return $cache->get();
}
/** @var array $dates */
$dates = Navigation::blockPeriods($start, $end, $range);
$entries = [];
$spent = [];
$earned = [];
$transferredAway = [];
$transferredIn = [];
// run a custom query because doing this with the collector is MEGA slow.
$timer->start('account-period-collect');
$transactions = $this->accountRepository->periodCollection($account, $start, $end);
$timer->stop('account-period-collect');
// loop dates
Log::debug(sprintf('Count of loops: %d', count($dates)));
$loops = 0;
// stop after 10 loops for memory reasons.
$timer->start('account-period-loop');
foreach ($dates as $currentDate) {
$title = Navigation::periodShow($currentDate['start'], $currentDate['period']);
[$transactions, $spent] = $this->filterTransactionsByType(TransactionTypeEnum::WITHDRAWAL, $transactions, $currentDate['start'], $currentDate['end']);
@@ -117,18 +119,19 @@ 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;
}
$timer->stop('account-period-loop');
$cache->store($entries);
Timer::stop('account-period-total');
$timer->stop('account-period-total');
Log::debug('End of getAccountPeriodOverview()');
return $entries;
@@ -168,7 +171,7 @@ trait PeriodOverview
* @var array $item
*/
foreach ($transactions as $index => $item) {
$date = Carbon::parse($item['date']);
$date = Carbon::parse($item['date']);
if ($date >= $start && $date <= $end) {
if ('away' === $direction && -1 === bccomp((string)$item['amount'], '0')) {
$result[] = $item;
@@ -180,8 +183,8 @@ trait PeriodOverview
continue;
}
$filtered[] = $item;
}
$filtered[] = $item;
}
return [$filtered, $result];
@@ -590,13 +593,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;
}

View File

@@ -321,6 +321,7 @@ class Steam
public function accountsBalancesOptimized(Collection $accounts, Carbon $date, ?TransactionCurrency $primary = null, ?bool $convertToPrimary = null): array
{
Log::debug(sprintf('accountsBalancesOptimized: Called with date/time "%s"', $date->toIso8601String()));
$result = [];
$convertToPrimary ??= Amount::convertToPrimary();
$primary ??= Amount::getPrimaryCurrency();
@@ -520,17 +521,6 @@ class Steam
return $total;
}
public function finalAccountsBalance(Collection $accounts, Carbon $date): array
{
Log::debug(sprintf('finalAccountsBalance: Call finalAccountBalance with date/time "%s"', $date->toIso8601String()));
$balances = [];
foreach ($accounts as $account) {
$balances[$account->id] = $this->finalAccountBalance($account, $date);
}
return $balances;
}
/**
* @throws FireflyException
*/

View File

@@ -3,6 +3,53 @@
All notable changes to this project will be documented in this file.
This project adheres to [Semantic Versioning](http://semver.org/).
## 6.3.0 - 2025-08-xx
> ⚠️ Firefly III v6.3.0 introduces a lot of API changes that deal with multi-currency support. Make sure your beloved apps are updated to support this.
### Added
- [Issue 6836](https://github.com/firefly-iii/firefly-iii/issues/6836) (Send email about coming/past-due bills) reported by @elgatho
- [Issue 9640](https://github.com/firefly-iii/firefly-iii/issues/9640) (UI Improvements for Rules) reported by @siriuspal
- [Issue 9650](https://github.com/firefly-iii/firefly-iii/issues/9650) (Extra line in bills overview) reported by @poudenes
- Add Arabic as language, translations follow.
### Changed
- [Issue 10071](https://github.com/firefly-iii/firefly-iii/issues/10071) (Allow toggling password field to text) reported by @ragul-engg
- Renamed all instances of "default" and "native" currency to "primary" currency. This influences translations and API endpoints. The database is not changed because that's difficult to do reliably.
### Removed
- Any API-field called `default_*` or `native_*`. Use `primary_*` instead.
- All v2 endpoints.
### Fixed
- [Issue 9849](https://github.com/firefly-iii/firefly-iii/issues/9849) ("Display native amounts" not taken into account in report's pie charts) reported by @polter-rnd
- [Issue 10565](https://github.com/firefly-iii/firefly-iii/issues/10565) (Unable to delete reconciliation transaction) reported by @berta24
- [Issue 10600](https://github.com/firefly-iii/firefly-iii/issues/10600) (Show attachmen iccon when listing tranactions) reported by @JcMinarro
- [Discussion 10618](https://github.com/orgs/firefly-iii/discussions/10618) (Starting balance includes transactions that occur at 00:00 on the 1st of month) started by @jteez
- [Issue 10646](https://github.com/firefly-iii/firefly-iii/issues/10646) (Webhooks fire even if disabled) reported by @lvu
- [Issue 10656](https://github.com/firefly-iii/firefly-iii/issues/10656) (spent info "per day" shows the period total) reported by @frank-bg
- [Issue 10678](https://github.com/firefly-iii/firefly-iii/issues/10678) (Transactions from asset to liability account do not appear on category reports.) reported by @slackspace-io
- [Issue 10687](https://github.com/firefly-iii/firefly-iii/issues/10687) (Creating new Piggy Bank via API fails (Unexpected empty currency)) reported by @Madnex
- [Issue 10700](https://github.com/firefly-iii/firefly-iii/issues/10700) (Setting financial year date is inconsistent due to timezone calculations) reported by @AgeManning
- [Issue 10702](https://github.com/firefly-iii/firefly-iii/issues/10702) (Wrong order of months in category report) reported by @kapuett
- [Issue 10703](https://github.com/firefly-iii/firefly-iii/issues/10703) (Fire more than 5 webhooks in batch or through the cron job, and document it.) reported by @JC5
- [Issue 10704](https://github.com/firefly-iii/firefly-iii/issues/10704) (Some triggers with rule automation seems to have an issue) reported by @Alienlog
- [Issue 10706](https://github.com/firefly-iii/firefly-iii/issues/10706) (Add KRW in Default Currency List) reported by @readingsnail
- [Issue 10708](https://github.com/firefly-iii/firefly-iii/issues/10708) (Incomplete display of a rule when a trigger negates "description caontains") reported by @dethegeek
- [Issue 10709](https://github.com/firefly-iii/firefly-iii/issues/10709) (has_any_external_id search parameter invalid) reported by @Alienlog
- Tag overview will no longer search for tags dated < 1970.
### API
- All remaining API v2 endpoints are deprecated and removed in favour of the API v1 endpoints.
- All API read endpoints now support multi-currency. Fields such as the balance and amount fields will also be available as `pc_*`-fields. Objects with currency information also come with new `primary_currency_*` fields.
- All API read endpoints are DB optimized and should be faster.
- All documentation should be in sync again.
- [More info in the docs](https://docs.firefly-iii.org/references/firefly-iii/api/).
## 6.2.21 - 2025-07-18
### Added

52
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",
@@ -11618,16 +11618,16 @@
},
{
"name": "phpunit/phpunit",
"version": "12.3.0",
"version": "12.3.1",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/phpunit.git",
"reference": "264da860d6fe0d00582355a6ecbbf7ae57b44895"
"reference": "5fd1b6e8ab560e0c62600591d438d22a8d978d68"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/264da860d6fe0d00582355a6ecbbf7ae57b44895",
"reference": "264da860d6fe0d00582355a6ecbbf7ae57b44895",
"url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/5fd1b6e8ab560e0c62600591d438d22a8d978d68",
"reference": "5fd1b6e8ab560e0c62600591d438d22a8d978d68",
"shasum": ""
},
"require": {
@@ -11637,7 +11637,7 @@
"ext-mbstring": "*",
"ext-xml": "*",
"ext-xmlwriter": "*",
"myclabs/deep-copy": "^1.13.3",
"myclabs/deep-copy": "^1.13.4",
"phar-io/manifest": "^2.0.4",
"phar-io/version": "^3.2.1",
"php": ">=8.3",
@@ -11653,7 +11653,7 @@
"sebastian/exporter": "^7.0.0",
"sebastian/global-state": "^8.0.0",
"sebastian/object-enumerator": "^7.0.0",
"sebastian/type": "^6.0.2",
"sebastian/type": "^6.0.3",
"sebastian/version": "^6.0.0",
"staabm/side-effects-detector": "^1.0.5"
},
@@ -11695,7 +11695,7 @@
"support": {
"issues": "https://github.com/sebastianbergmann/phpunit/issues",
"security": "https://github.com/sebastianbergmann/phpunit/security/policy",
"source": "https://github.com/sebastianbergmann/phpunit/tree/12.3.0"
"source": "https://github.com/sebastianbergmann/phpunit/tree/12.3.1"
},
"funding": [
{
@@ -11719,7 +11719,7 @@
"type": "tidelift"
}
],
"time": "2025-08-01T05:14:47+00:00"
"time": "2025-08-09T07:12:41+00:00"
},
{
"name": "rector/rector",
@@ -12509,16 +12509,16 @@
},
{
"name": "sebastian/type",
"version": "6.0.2",
"version": "6.0.3",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/type.git",
"reference": "1d7cd6e514384c36d7a390347f57c385d4be6069"
"reference": "e549163b9760b8f71f191651d22acf32d56d6d4d"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/sebastianbergmann/type/zipball/1d7cd6e514384c36d7a390347f57c385d4be6069",
"reference": "1d7cd6e514384c36d7a390347f57c385d4be6069",
"url": "https://api.github.com/repos/sebastianbergmann/type/zipball/e549163b9760b8f71f191651d22acf32d56d6d4d",
"reference": "e549163b9760b8f71f191651d22acf32d56d6d4d",
"shasum": ""
},
"require": {
@@ -12554,15 +12554,27 @@
"support": {
"issues": "https://github.com/sebastianbergmann/type/issues",
"security": "https://github.com/sebastianbergmann/type/security/policy",
"source": "https://github.com/sebastianbergmann/type/tree/6.0.2"
"source": "https://github.com/sebastianbergmann/type/tree/6.0.3"
},
"funding": [
{
"url": "https://github.com/sebastianbergmann",
"type": "github"
},
{
"url": "https://liberapay.com/sebastianbergmann",
"type": "liberapay"
},
{
"url": "https://thanks.dev/u/gh/sebastianbergmann",
"type": "thanks_dev"
},
{
"url": "https://tidelift.com/funding/github/packagist/sebastian/type",
"type": "tidelift"
}
],
"time": "2025-03-18T13:37:31+00:00"
"time": "2025-08-09T06:57:12+00:00"
},
{
"name": "sebastian/version",

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-10',
'build_time' => 1754800876,
'api_version' => '2.1.0', // field is no longer used.
'db_version' => 26,

10
package-lock.json generated
View File

@@ -4326,9 +4326,9 @@
}
},
"node_modules/browserslist": {
"version": "4.25.1",
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.25.1.tgz",
"integrity": "sha512-KGj0KoOMXLpSNkkEI6Z6mShmQy0bc1I+T7K9N81k4WWMrfz+6fQ6es80B/YLAeRoKvjYE1YSHHOW1qe9xIVzHw==",
"version": "4.25.2",
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.25.2.tgz",
"integrity": "sha512-0si2SJK3ooGzIawRu61ZdPCO1IncZwS8IzuX73sPZsXW6EQ/w/DAfPyKI8l1ETTCr2MnvqWitmlCUxgdul45jA==",
"dev": true,
"funding": [
{
@@ -4346,8 +4346,8 @@
],
"license": "MIT",
"dependencies": {
"caniuse-lite": "^1.0.30001726",
"electron-to-chromium": "^1.5.173",
"caniuse-lite": "^1.0.30001733",
"electron-to-chromium": "^1.5.199",
"node-releases": "^2.0.19",
"update-browserslist-db": "^1.1.3"
},

View File

@@ -138,6 +138,14 @@ 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.
'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_single' => 'You have one subscription that is overdue to be paid. At the following date(s) a payment was expected, but it has not yet arrived.',
'subscriptions_overdue_warning_intro_multi' => '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_single' => 'Perhaps you have simply not linked a transaction to this subscription. In that case, please do so. You will NOT get another warning about this overdue subscription. A new warning will be sent out for the NEXT due payment.',
'subscriptions_overdue_please_action_multi' => 'Perhaps you have simply not linked a transaction to these subscriptions. In that case, please do so. You will NOT get another warning about these overdue subscriptions. A new warning will be sent out for the NEXT due payments.',
'subscriptions_overdue_outro' => 'If you believe this message is wrong, please contact the Firefly III developer. Thank you for using Firefly III.',
// 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',
@@ -1874,6 +1875,7 @@ return [
'subscr_expected_x_times' => 'Expect to pay {{amount}} {{times}} times this period',
'not_or_not_yet' => 'Not (yet)',
'visit_bill' => 'Visit subscription ":name" at Firefly III',
'visit_bills' => 'Visit subscriptions at Firefly III',
'match_between_amounts' => 'Subscription matches transactions between :low and :high.',
'running_again_loss' => 'Previously linked transactions to this subscription may lose their connection, if they (no longer) match the rule(s).',
'bill_related_rules' => 'Rules related to this subscription',
@@ -2331,6 +2333,7 @@ return [
// reports:
'quick_link_needs_accounts' => 'In order to generate reports, you need to add at least one asset account to Firefly III.',
'report_default' => 'Default financial report between :start and :end',
'report_audit' => 'Transaction history overview between :start and :end',
'report_category' => 'Category report between :start and :end',

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,24 @@
@component('mail::message')
@if(1 === $count)
{{ trans('email.subscriptions_overdue_warning_intro_single') }}
@endif
@if(1 !== $count)
{{ trans('email.subscriptions_overdue_warning_intro_multi', ['count' => $count]) }}
@endif
@foreach($info as $row)
- {{ $row['bill']->name }}:
@foreach($row['pay_dates'] as $date)
- {{ $date }}
@endforeach
@endforeach
@if(1 === $count)
{{ trans('email.subscriptions_overdue_please_action_single') }}
@endif
@if(1 !== $count)
{{ trans('email.subscriptions_overdue_please_action_multi', ['count' => $count]) }}
@endif
{{ trans('email.subscriptions_overdue_outro') }}
@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

@@ -12,7 +12,6 @@
<td style="text-align: right;">{{ period.total_transactions }}</td>
</tr>
{% endif %}
{% for entry in period.spent %}
{% if entry.amount != 0 %}
<tr>

View File

@@ -46,8 +46,12 @@
{% endif %}
</td>
<td style="text-align: right;" class="piggySaved">
<span title="Saved so far"
style="text-align:right;">{{ formatAmountBySymbol(piggy.current_amount,piggy.currency_symbol,piggy.currency_decimal_places) }}</span>
<span title="Saved so far" style="text-align:right;">
{{ formatAmountBySymbol(piggy.current_amount,piggy.currency_symbol,piggy.currency_decimal_places) }}
{% if convertToPrimary and piggy.currency_id != primaryCurrency.id and null != piggy.pc_current_amount %}
({{ formatAmountBySymbol(piggy.pc_current_amount,primaryCurrency.symbol,primaryCurrency.decimal_places) }})
{% endif %}
</span>
</td>
<td class="hidden-sm hidden-xs" style="text-align:right;width:40px;">
{% if piggy.current_amount > 0 %}
@@ -86,16 +90,25 @@
<td class="hidden-sm hidden-xs" style="text-align:right;">
{% if null != piggy.target_amount and 0 != piggy.target_amount %}
<span title="{{ 'target_amount'|_ }}">{{ formatAmountBySymbol(piggy.target_amount,piggy.currency_symbol,piggy.currency_decimal_places) }}</span>
{% if convertToPrimary and piggy.currency_id != primaryCurrency.id and null != piggy.pc_target_amount %}
(<span title="{{ 'target_amount'|_ }}">{{ formatAmountBySymbol(piggy.pc_target_amount,primaryCurrency.symbol, primaryCurrency.decimal_places) }}</span>)
{% endif %}
{% endif %}
</td>
<td class="hidden-sm hidden-xs" style="text-align:right;">
{% if piggy.left_to_save > 0 %}
<span title="{{ 'left_to_save'|_ }}">{{ formatAmountBySymbol(piggy.left_to_save,piggy.currency_symbol,piggy.currency_decimal_places) }}</span>
{% if convertToPrimary and piggy.currency_id != primaryCurrency.id and null != piggy.pc_left_to_save %}
(<span title="{{ 'left_to_save'|_ }}">{{ formatAmountBySymbol(piggy.pc_left_to_save, primaryCurrency.symbol,primaryCurrency.decimal_places) }}</span>)
{% endif %}
{% endif %}
</td>
<td class="hidden-sm hidden-xs" style="text-align:right;">
{% if piggy.target_date and piggy.save_per_month %}
{{ formatAmountBySymbol(piggy.save_per_month, piggy.currency_symbol, piggy.currency_decimal_places) }}
{% if convertToPrimary and piggy.currency_id != primaryCurrency.id and null != piggy.pc_save_per_month %}
({{ formatAmountBySymbol(piggy.pc_save_per_month, primaryCurrency.symbol, primaryCurrency.decimal_places) }})
{% endif %}
{% endif %}
</td>
</tr>

View File

@@ -128,6 +128,12 @@
<h3 class="box-title">{{ 'quick_link_reports'|_ }}</h3>
</div>
<div class="box-body">
{% if '' == accountList %}
<p class="text-danger">
{{ 'quick_link_needs_accounts'|_ }}
</p>
{% endif %}
{% if '' != accountList %}
<p>
{{ 'quick_link_examples'|_ }}
</p>
@@ -172,6 +178,7 @@
<p>
<em>{{ 'reports_can_bookmark'|_ }}</em>
</p>
{% endif %}
</div>
</div>

View File

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