diff --git a/.ci/php-cs-fixer/composer.lock b/.ci/php-cs-fixer/composer.lock index f686a86fb2..9b686b8fee 100644 --- a/.ci/php-cs-fixer/composer.lock +++ b/.ci/php-cs-fixer/composer.lock @@ -402,16 +402,16 @@ }, { "name": "friendsofphp/php-cs-fixer", - "version": "v3.92.4", + "version": "v3.92.5", "source": { "type": "git", "url": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer.git", - "reference": "9e7488b19403423e02e8403cc1eb596baf4673b0" + "reference": "260cc8c4a1d2f6d2f22cd4f9c70aa72e55ebac58" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/PHP-CS-Fixer/PHP-CS-Fixer/zipball/9e7488b19403423e02e8403cc1eb596baf4673b0", - "reference": "9e7488b19403423e02e8403cc1eb596baf4673b0", + "url": "https://api.github.com/repos/PHP-CS-Fixer/PHP-CS-Fixer/zipball/260cc8c4a1d2f6d2f22cd4f9c70aa72e55ebac58", + "reference": "260cc8c4a1d2f6d2f22cd4f9c70aa72e55ebac58", "shasum": "" }, "require": { @@ -494,7 +494,7 @@ ], "support": { "issues": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/issues", - "source": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/tree/v3.92.4" + "source": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/tree/v3.92.5" }, "funding": [ { @@ -502,7 +502,7 @@ "type": "github" } ], - "time": "2026-01-04T00:38:52+00:00" + "time": "2026-01-08T21:57:37+00:00" }, { "name": "psr/container", diff --git a/THANKS.md b/THANKS.md index 2c2bdfd8d3..b3aaf8ba96 100755 --- a/THANKS.md +++ b/THANKS.md @@ -3,6 +3,9 @@ Over time, many people have contributed to Firefly III. Their efforts are not always visible, but always remembered and appreciated. Please find below all the people who contributed to the Firefly III code. Their names are mentioned in the year of their first contribution. +## 2026 +- embedded + ## 2025 - Diego Algorta - Jihad diff --git a/app/Helpers/Collector/Extensions/MetaCollection.php b/app/Helpers/Collector/Extensions/MetaCollection.php index 128fbe9f2c..bcfc737002 100644 --- a/app/Helpers/Collector/Extensions/MetaCollection.php +++ b/app/Helpers/Collector/Extensions/MetaCollection.php @@ -784,20 +784,21 @@ trait MetaCollection $filter = static function (array $object) use ($list): bool { Log::debug(sprintf('Now in setTags(%s) filter', implode(', ', $list))); foreach ($object['transactions'] as $transaction) { - $total = count($transaction['tags']); + $total = count($transaction['tags']); $matched = 0; foreach ($transaction['tags'] as $tag) { Log::debug(sprintf('"%s" versus', strtolower((string) $tag['name'])), $list); if (in_array(strtolower((string) $tag['name']), $list, true)) { Log::debug(sprintf('Transaction has tag "%s" so return true.', $tag['name'])); - $matched++; - if(1 === count($list)) { + ++$matched; + if (1 === count($list)) { return true; } } } - if(count($list) > 1 && $total === $matched && $matched === count($list)) { + if (count($list) > 1 && $total === $matched && $matched === count($list)) { Log::debug(sprintf('All %d searched tags are present.', $total)); + return true; } } diff --git a/app/Http/Controllers/PreferencesController.php b/app/Http/Controllers/PreferencesController.php index 02a1ce20b6..415e27e622 100644 --- a/app/Http/Controllers/PreferencesController.php +++ b/app/Http/Controllers/PreferencesController.php @@ -45,6 +45,7 @@ use Illuminate\Support\Facades\Log; use Illuminate\View\View; use JsonException; use Safe\Exceptions\FilesystemException; + use function Safe\file_get_contents; use function Safe\json_decode; @@ -78,16 +79,16 @@ class PreferencesController extends Controller * @throws FireflyException * @throws FilesystemException */ - public function index(AccountRepositoryInterface $repository): Factory | \Illuminate\Contracts\View\View + public function index(AccountRepositoryInterface $repository): Factory|\Illuminate\Contracts\View\View { - $accounts = $repository->getAccountsByType([AccountTypeEnum::DEFAULT->value, AccountTypeEnum::ASSET->value, AccountTypeEnum::LOAN->value, AccountTypeEnum::DEBT->value, AccountTypeEnum::MORTGAGE->value]); - $isDocker = env('IS_DOCKER', false); // @phpstan-ignore-line - $groupedAccounts = []; + $accounts = $repository->getAccountsByType([AccountTypeEnum::DEFAULT->value, AccountTypeEnum::ASSET->value, AccountTypeEnum::LOAN->value, AccountTypeEnum::DEBT->value, AccountTypeEnum::MORTGAGE->value]); + $isDocker = env('IS_DOCKER', false); // @phpstan-ignore-line + $groupedAccounts = []; /** @var Account $account */ foreach ($accounts as $account) { - $type = $account->accountType->type; - $role = sprintf('opt_group_%s', $repository->getMetaValue($account, 'account_role')); + $type = $account->accountType->type; + $role = sprintf('opt_group_%s', $repository->getMetaValue($account, 'account_role')); if (in_array($type, [AccountTypeEnum::MORTGAGE->value, AccountTypeEnum::DEBT->value, AccountTypeEnum::LOAN->value], true)) { $role = sprintf('opt_group_l_%s', $type); @@ -101,50 +102,50 @@ class PreferencesController extends Controller ksort($groupedAccounts); /** @var array $accountIds */ - $accountIds = $accounts->pluck('id')->toArray(); - $viewRange = Navigation::getViewRange(false); - $frontpageAccountsPref = Preferences::get('frontpageAccounts', $accountIds); - $frontpageAccounts = $frontpageAccountsPref->data; + $accountIds = $accounts->pluck('id')->toArray(); + $viewRange = Navigation::getViewRange(false); + $frontpageAccountsPref = Preferences::get('frontpageAccounts', $accountIds); + $frontpageAccounts = $frontpageAccountsPref->data; if (!is_array($frontpageAccounts)) { $frontpageAccounts = $accountIds; } - $language = Steam::getLanguage(); - $languages = config('firefly.languages'); - $locale = Preferences::get('locale', config('firefly.default_locale', 'equal'))->data; - $listPageSize = Preferences::get('listPageSize', 50)->data; - $darkMode = Preferences::get('darkMode', 'browser')->data; - $customFiscalYear = Preferences::get('customFiscalYear', 0)->data; - $fiscalYearStartStr = Preferences::get('fiscalYearStart', '01-01')->data; - $convertToPrimary = $this->convertToPrimary; + $language = Steam::getLanguage(); + $languages = config('firefly.languages'); + $locale = Preferences::get('locale', config('firefly.default_locale', 'equal'))->data; + $listPageSize = Preferences::get('listPageSize', 50)->data; + $darkMode = Preferences::get('darkMode', 'browser')->data; + $customFiscalYear = Preferences::get('customFiscalYear', 0)->data; + $fiscalYearStartStr = Preferences::get('fiscalYearStart', '01-01')->data; + $convertToPrimary = $this->convertToPrimary; if (is_array($fiscalYearStartStr)) { $fiscalYearStartStr = '01-01'; } - $fiscalYearStart = sprintf('%s-%s', Carbon::now()->format('Y'), (string)$fiscalYearStartStr); - $tjOptionalFields = Preferences::get('transaction_journal_optional_fields', [])->data; - $availableDarkModes = config('firefly.available_dark_modes'); + $fiscalYearStart = sprintf('%s-%s', Carbon::now()->format('Y'), (string)$fiscalYearStartStr); + $tjOptionalFields = Preferences::get('transaction_journal_optional_fields', [])->data; + $availableDarkModes = config('firefly.available_dark_modes'); // notifications settings - $slackUrl = Preferences::getEncrypted('slack_webhook_url', '')->data; - $pushoverAppToken = (string)Preferences::getEncrypted('pushover_app_token', '')->data; - $pushoverUserToken = (string)Preferences::getEncrypted('pushover_user_token', '')->data; - $ntfyServer = Preferences::getEncrypted('ntfy_server', 'https://ntfy.sh')->data; - $ntfyTopic = (string)Preferences::getEncrypted('ntfy_topic', '')->data; - $ntfyAuth = '1' === Preferences::get('ntfy_auth', false)->data; - $ntfyUser = Preferences::getEncrypted('ntfy_user', '')->data; - $ntfyPass = (string)Preferences::getEncrypted('ntfy_pass', '')->data; - $channels = config('notifications.channels'); - $forcedAvailability = []; - $anonymous = Steam::anonymous(); + $slackUrl = Preferences::getEncrypted('slack_webhook_url', '')->data; + $pushoverAppToken = (string)Preferences::getEncrypted('pushover_app_token', '')->data; + $pushoverUserToken = (string)Preferences::getEncrypted('pushover_user_token', '')->data; + $ntfyServer = Preferences::getEncrypted('ntfy_server', 'https://ntfy.sh')->data; + $ntfyTopic = (string)Preferences::getEncrypted('ntfy_topic', '')->data; + $ntfyAuth = '1' === Preferences::get('ntfy_auth', false)->data; + $ntfyUser = Preferences::getEncrypted('ntfy_user', '')->data; + $ntfyPass = (string)Preferences::getEncrypted('ntfy_pass', '')->data; + $channels = config('notifications.channels'); + $forcedAvailability = []; + $anonymous = Steam::anonymous(); // notification preferences - $notifications = []; + $notifications = []; foreach (config('notifications.notifications.user') as $key => $info) { if (true === $info['enabled']) { $notifications[$key] = [ - 'enabled' => true === Preferences::get(sprintf('notification_%s', $key), true)->data, - 'configurable' => $info['configurable'], - ]; + 'enabled' => true === Preferences::get(sprintf('notification_%s', $key), true)->data, + 'configurable' => $info['configurable'], + ]; } } // loop all channels to see if they are available. @@ -164,7 +165,7 @@ class PreferencesController extends Controller Log::error($e->getMessage()); $locales = []; } - $locales = ['equal' => (string)trans('firefly.equal_to_language')] + $locales; + $locales = ['equal' => (string)trans('firefly.equal_to_language')] + $locales; // an important fallback is that the frontPageAccount array gets refilled automatically // when it turns up empty. if (0 === count($frontpageAccounts)) { @@ -195,7 +196,7 @@ class PreferencesController extends Controller * @SuppressWarnings("PHPMD.ExcessiveMethodLength") * @SuppressWarnings("PHPMD.NPathComplexity") */ - public function postIndex(PreferencesRequest $request): Redirector | RedirectResponse + public function postIndex(PreferencesRequest $request): Redirector|RedirectResponse { Log::debug('postIndex for preferences.'); // front page accounts @@ -209,7 +210,7 @@ class PreferencesController extends Controller } // extract notifications: - $all = $request->all(); + $all = $request->all(); foreach (config('notifications.notifications.user') as $key => $info) { $key = sprintf('notification_%s', $key); if (array_key_exists($key, $all)) { @@ -246,7 +247,7 @@ class PreferencesController extends Controller } // convert primary - $convertToPrimary = 1 === (int)$request->get('convertToPrimary'); + $convertToPrimary = 1 === (int)$request->get('convertToPrimary'); if ($convertToPrimary && !$this->convertToPrimary) { // set to true! Log::debug('User sets convertToPrimary to true.'); @@ -258,9 +259,9 @@ class PreferencesController extends Controller Preferences::set('convert_to_primary', $convertToPrimary); // custom fiscal year - $customFiscalYear = 1 === (int)$request->get('customFiscalYear'); + $customFiscalYear = 1 === (int)$request->get('customFiscalYear'); Preferences::set('customFiscalYear', $customFiscalYear); - $fiscalYearString = (string)$request->get('fiscalYearStart'); + $fiscalYearString = (string)$request->get('fiscalYearStart'); if ('' !== $fiscalYearString) { $fiscalYearStart = Carbon::parse($fiscalYearString, config('app.timezone'))->format('m-d'); Preferences::set('fiscalYearStart', $fiscalYearStart); @@ -268,15 +269,15 @@ class PreferencesController extends Controller // save page size: Preferences::set('listPageSize', 50); - $listPageSize = (int)$request->get('listPageSize'); + $listPageSize = (int)$request->get('listPageSize'); if ($listPageSize > 0 && $listPageSize < 1337) { Preferences::set('listPageSize', $listPageSize); } // language: /** @var Preference $currentLang */ - $currentLang = Preferences::get('language', 'en_US'); - $lang = $request->get('language'); + $currentLang = Preferences::get('language', 'en_US'); + $lang = $request->get('language'); if (array_key_exists($lang, config('firefly.languages'))) { Preferences::set('language', $lang); } @@ -293,8 +294,8 @@ class PreferencesController extends Controller } // optional fields for transactions: - $setOptions = $request->get('tj') ?? []; - $optionalTj = [ + $setOptions = $request->get('tj') ?? []; + $optionalTj = [ 'interest_date' => array_key_exists('interest_date', $setOptions), 'book_date' => array_key_exists('book_date', $setOptions), 'process_date' => array_key_exists('process_date', $setOptions), @@ -311,13 +312,13 @@ class PreferencesController extends Controller Preferences::set('transaction_journal_optional_fields', $optionalTj); // dark mode - $darkMode = $request->get('darkMode') ?? 'browser'; + $darkMode = $request->get('darkMode') ?? 'browser'; if (in_array($darkMode, config('firefly.available_dark_modes'), true)) { Preferences::set('darkMode', $darkMode); } // anonymous amounts? - $anonymous = '1' === $request->get('anonymous'); + $anonymous = '1' === $request->get('anonymous'); Preferences::set('anonymous', $anonymous); // save and continue diff --git a/app/Http/Middleware/AcceptHeaders.php b/app/Http/Middleware/AcceptHeaders.php index 0d3dcc3b1a..8a7528864a 100644 --- a/app/Http/Middleware/AcceptHeaders.php +++ b/app/Http/Middleware/AcceptHeaders.php @@ -58,7 +58,7 @@ class AcceptHeaders // some routes are exempt from this. $exempt = [ 'api.v1.data.bulk.transactions', - 'api.v1.attachments.upload' + 'api.v1.attachments.upload', ]; if (('POST' === $method || 'PUT' === $method) && !$request->hasHeader('Content-Type') && !in_array($request->route()->getName(), $exempt, true)) { diff --git a/app/Repositories/Bill/BillRepository.php b/app/Repositories/Bill/BillRepository.php index 24edd2b85c..4e0eea396f 100644 --- a/app/Repositories/Bill/BillRepository.php +++ b/app/Repositories/Bill/BillRepository.php @@ -50,6 +50,7 @@ use Illuminate\Support\Collection; use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Log; use Illuminate\Support\Facades\Storage; +use Override; /** * Class BillRepository. @@ -66,7 +67,8 @@ class BillRepository implements BillRepositoryInterface, UserGroupInterface $search->whereLike('name', sprintf('%%%s', $query)); } $search->orderBy('name', 'ASC') - ->where('active', true); + ->where('active', true) + ; return $search->take($limit)->get(); } @@ -78,7 +80,8 @@ class BillRepository implements BillRepositoryInterface, UserGroupInterface $search->whereLike('name', sprintf('%s%%', $query)); } $search->orderBy('name', 'ASC') - ->where('active', true); + ->where('active', true) + ; return $search->take($limit)->get(); } @@ -180,9 +183,10 @@ class BillRepository implements BillRepositoryInterface, UserGroupInterface public function getBills(): Collection { return $this->user->bills() - ->orderBy('order', 'ASC') - ->orderBy('active', 'DESC') - ->orderBy('name', 'ASC')->get(); + ->orderBy('order', 'ASC') + ->orderBy('active', 'DESC') + ->orderBy('name', 'ASC')->get() + ; } public function getBillsForAccounts(Collection $accounts): Collection @@ -206,24 +210,25 @@ class BillRepository implements BillRepositoryInterface, UserGroupInterface $ids = $accounts->pluck('id')->toArray(); return $this->user->bills() - ->leftJoin( - 'transaction_journals', - static function (JoinClause $join): void { - $join->on('transaction_journals.bill_id', '=', 'bills.id')->whereNull('transaction_journals.deleted_at'); - } - ) - ->leftJoin( - 'transactions', - static function (JoinClause $join): void { - $join->on('transaction_journals.id', '=', 'transactions.transaction_journal_id')->where('transactions.amount', '<', 0); - } - ) - ->whereIn('transactions.account_id', $ids) - ->whereNull('transaction_journals.deleted_at') - ->orderBy('bills.active', 'DESC') - ->orderBy('bills.name', 'ASC') - ->groupBy($fields) - ->get($fields); + ->leftJoin( + 'transaction_journals', + static function (JoinClause $join): void { + $join->on('transaction_journals.bill_id', '=', 'bills.id')->whereNull('transaction_journals.deleted_at'); + } + ) + ->leftJoin( + 'transactions', + static function (JoinClause $join): void { + $join->on('transaction_journals.id', '=', 'transactions.transaction_journal_id')->where('transactions.amount', '<', 0); + } + ) + ->whereIn('transactions.account_id', $ids) + ->whereNull('transaction_journals.deleted_at') + ->orderBy('bills.active', 'DESC') + ->orderBy('bills.name', 'ASC') + ->groupBy($fields) + ->get($fields) + ; } /** @@ -248,7 +253,7 @@ class BillRepository implements BillRepositoryInterface, UserGroupInterface public function getOverallAverage(Bill $bill): array { /** @var JournalRepositoryInterface $repos */ - $repos = app(JournalRepositoryInterface::class); + $repos = app(JournalRepositoryInterface::class); $repos->setUser($this->user); // get and sort on currency @@ -261,7 +266,7 @@ class BillRepository implements BillRepositoryInterface, UserGroupInterface $transaction = $journal->transactions()->where('amount', '<', 0)->first(); $currencyId = (int)$journal->transaction_currency_id; $currency = $journal->transactionCurrency; - $result[$currencyId] ??= [ + $result[$currencyId] ??= [ 'sum' => '0', 'pc_sum' => '0', 'count' => 0, @@ -296,8 +301,9 @@ class BillRepository implements BillRepositoryInterface, UserGroupInterface public function getPaginator(int $size): LengthAwarePaginator { return $this->user->bills() - ->orderBy('active', 'DESC') - ->orderBy('name', 'ASC')->paginate($size); + ->orderBy('active', 'DESC') + ->orderBy('name', 'ASC')->paginate($size) + ; } /** @@ -310,11 +316,11 @@ class BillRepository implements BillRepositoryInterface, UserGroupInterface Log::debug(sprintf('Search for linked journals between %s and %s', $start->toW3cString(), $end->toW3cString())); return $bill->transactionJournals() - ->leftJoin('transactions', 'transactions.transaction_journal_id', '=', 'transaction_journals.id') - ->leftJoin('transaction_currencies AS currency', 'currency.id', '=', 'transactions.transaction_currency_id') - ->leftJoin('transaction_currencies AS foreign_currency', 'foreign_currency.id', '=', 'transactions.foreign_currency_id') - ->where('transactions.amount', '>', 0) - ->before($end)->after($start)->get( + ->leftJoin('transactions', 'transactions.transaction_journal_id', '=', 'transaction_journals.id') + ->leftJoin('transaction_currencies AS currency', 'currency.id', '=', 'transactions.transaction_currency_id') + ->leftJoin('transaction_currencies AS foreign_currency', 'foreign_currency.id', '=', 'transactions.foreign_currency_id') + ->where('transactions.amount', '>', 0) + ->before($end)->after($start)->get( [ 'transaction_journals.id', 'transaction_journals.date', @@ -328,7 +334,8 @@ class BillRepository implements BillRepositoryInterface, UserGroupInterface 'transactions.amount', 'transactions.foreign_amount', ] - ); + ) + ; } /** @@ -337,10 +344,11 @@ class BillRepository implements BillRepositoryInterface, UserGroupInterface public function getRulesForBill(Bill $bill): Collection { return $this->user->rules() - ->leftJoin('rule_actions', 'rule_actions.rule_id', '=', 'rules.id') - ->where('rule_actions.action_type', 'link_to_bill') - ->where('rule_actions.action_value', $bill->name) - ->get(['rules.*']); + ->leftJoin('rule_actions', 'rule_actions.rule_id', '=', 'rules.id') + ->where('rule_actions.action_type', 'link_to_bill') + ->where('rule_actions.action_value', $bill->name) + ->get(['rules.*']) + ; } /** @@ -351,15 +359,16 @@ class BillRepository implements BillRepositoryInterface, UserGroupInterface */ public function getRulesForBills(Collection $collection): array { - $rules = $this->user->rules() - ->leftJoin('rule_actions', 'rule_actions.rule_id', '=', 'rules.id') - ->where('rule_actions.action_type', 'link_to_bill') - ->get(['rules.id', 'rules.title', 'rule_actions.action_value', 'rules.active']); - $array = []; + $rules = $this->user->rules() + ->leftJoin('rule_actions', 'rule_actions.rule_id', '=', 'rules.id') + ->where('rule_actions.action_type', 'link_to_bill') + ->get(['rules.id', 'rules.title', 'rule_actions.action_value', 'rules.active']) + ; + $array = []; /** @var Rule $rule */ foreach ($rules as $rule) { - $array[$rule->action_value] ??= []; + $array[$rule->action_value] ??= []; $array[$rule->action_value][] = ['id' => $rule->id, 'title' => $rule->title, 'active' => $rule->active]; } $return = []; @@ -373,27 +382,28 @@ class BillRepository implements BillRepositoryInterface, UserGroupInterface public function getYearAverage(Bill $bill, Carbon $date): array { /** @var JournalRepositoryInterface $repos */ - $repos = app(JournalRepositoryInterface::class); + $repos = app(JournalRepositoryInterface::class); $repos->setUser($this->user); // get and sort on currency - $result = []; + $result = []; $journals = $bill->transactionJournals() - ->where('date', '>=', $date->year . '-01-01 00:00:00') - ->where('date', '<=', $date->year . '-12-31 23:59:59') - ->get(); + ->where('date', '>=', $date->year.'-01-01 00:00:00') + ->where('date', '<=', $date->year.'-12-31 23:59:59') + ->get() + ; /** @var TransactionJournal $journal */ foreach ($journals as $journal) { /** @var null|Transaction $transaction */ - $transaction = $journal->transactions()->where('amount', '<', 0)->first(); + $transaction = $journal->transactions()->where('amount', '<', 0)->first(); if (null === $transaction) { continue; } $currencyId = (int)$journal->transaction_currency_id; $currency = $journal->transactionCurrency; - $result[$currencyId] ??= [ + $result[$currencyId] ??= [ 'sum' => '0', 'pc_sum' => '0', 'count' => 0, @@ -443,7 +453,7 @@ class BillRepository implements BillRepositoryInterface, UserGroupInterface */ public function nextExpectedMatch(Bill $bill, Carbon $date): Carbon { - $cache = new CacheProperties(); + $cache = new CacheProperties(); $cache->addProperty($bill->id); $cache->addProperty('nextExpectedMatch'); $cache->addProperty($date); @@ -451,17 +461,17 @@ class BillRepository implements BillRepositoryInterface, UserGroupInterface return $cache->get(); } // find the most recent date for this bill NOT in the future. Cache this date: - $start = clone $bill->date; + $start = clone $bill->date; $start->startOfDay(); - Log::debug('nextExpectedMatch: Start is ' . $start->format('Y-m-d')); + Log::debug('nextExpectedMatch: Start is '.$start->format('Y-m-d')); while ($start < $date) { Log::debug(sprintf('$start (%s) < $date (%s)', $start->format('Y-m-d H:i:s'), $date->format('Y-m-d H:i:s'))); $start = Navigation::addPeriod($start, $bill->repeat_freq, $bill->skip); - Log::debug('Start is now ' . $start->format('Y-m-d H:i:s')); + Log::debug('Start is now '.$start->format('Y-m-d H:i:s')); } - $end = Navigation::addPeriod($start, $bill->repeat_freq, $bill->skip); + $end = Navigation::addPeriod($start, $bill->repeat_freq, $bill->skip); $end->endOfDay(); // see if the bill was paid in this period. @@ -473,8 +483,8 @@ class BillRepository implements BillRepositoryInterface, UserGroupInterface $start = clone $end; $end = Navigation::addPeriod($start, $bill->repeat_freq, $bill->skip); } - Log::debug('nextExpectedMatch: Final start is ' . $start->format('Y-m-d')); - Log::debug('nextExpectedMatch: Matching end is ' . $end->format('Y-m-d')); + Log::debug('nextExpectedMatch: Final start is '.$start->format('Y-m-d')); + Log::debug('nextExpectedMatch: Matching end is '.$end->format('Y-m-d')); $cache->store($start); @@ -535,8 +545,8 @@ class BillRepository implements BillRepositoryInterface, UserGroupInterface foreach ($bills as $bill) { /** @var Collection $set */ - $set = $bill->transactionJournals()->after($start)->before($end)->get(['transaction_journals.*']); - $currency = $convertToPrimary && $bill->transactionCurrency->id !== $primary->id ? $primary : $bill->transactionCurrency; + $set = $bill->transactionJournals()->after($start)->before($end)->get(['transaction_journals.*']); + $currency = $convertToPrimary && $bill->transactionCurrency->id !== $primary->id ? $primary : $bill->transactionCurrency; $return[(int)$currency->id] ??= [ 'id' => (string)$currency->id, 'name' => $currency->name, @@ -545,12 +555,12 @@ class BillRepository implements BillRepositoryInterface, UserGroupInterface 'decimal_places' => $currency->decimal_places, 'sum' => '0', ]; - $setAmount = '0'; + $setAmount = '0'; /** @var TransactionJournal $transactionJournal */ foreach ($set as $transactionJournal) { // grab currency from transaction. - $transactionCurrency = $transactionJournal->transactionCurrency; + $transactionCurrency = $transactionJournal->transactionCurrency; $return[(int)$transactionCurrency->id] ??= [ 'id' => (string)$transactionCurrency->id, 'name' => $transactionCurrency->name, @@ -569,7 +579,7 @@ class BillRepository implements BillRepositoryInterface, UserGroupInterface // Log::debug(sprintf('Total sum is now %s', $return[$currency->id]['sum'])); } // remove empty sets - $final = []; + $final = []; foreach ($return as $entry) { if (0 === bccomp($entry['sum'], '0')) { continue; @@ -583,10 +593,10 @@ class BillRepository implements BillRepositoryInterface, UserGroupInterface public function getActiveBills(): Collection { return $this->user->bills() - ->where('active', true) - ->orderBy('bills.name', 'ASC') - ->get(['bills.*', DB::raw('((bills.amount_min + bills.amount_max) / 2) AS expectedAmount')]) // @phpstan-ignore-line - ; + ->where('active', true) + ->orderBy('bills.name', 'ASC') + ->get(['bills.*', DB::raw('((bills.amount_min + bills.amount_max) / 2) AS expectedAmount')]) // @phpstan-ignore-line + ; } public function sumUnpaidInRange(Carbon $start, Carbon $end): array @@ -600,9 +610,9 @@ class BillRepository implements BillRepositoryInterface, UserGroupInterface /** @var Bill $bill */ foreach ($bills as $bill) { // \Illuminate\Support\Facades\Log::debug(sprintf('Processing bill #%d ("%s")', $bill->id, $bill->name)); - $dates = $this->getPayDatesInRange($bill, $start, $end); - $count = $bill->transactionJournals()->after($start)->before($end)->count(); - $total = $dates->count() - $count; + $dates = $this->getPayDatesInRange($bill, $start, $end); + $count = $bill->transactionJournals()->after($start)->before($end)->count(); + $total = $dates->count() - $count; // \Illuminate\Support\Facades\Log::debug(sprintf('Pay dates: %d, count: %d, left: %d', $dates->count(), $count, $total)); // \Illuminate\Support\Facades\Log::debug('dates', $dates->toArray()); @@ -611,10 +621,10 @@ class BillRepository implements BillRepositoryInterface, UserGroupInterface // Log::debug(sprintf('min field is %s, max field is %s', $minField, $maxField)); if ($total > 0) { - $currency = $convertToPrimary && $bill->transactionCurrency->id !== $primary->id ? $primary : $bill->transactionCurrency; - $average = bcdiv(bcadd($bill->{$maxField} ?? '0', $bill->{$minField} ?? '0'), '2'); + $currency = $convertToPrimary && $bill->transactionCurrency->id !== $primary->id ? $primary : $bill->transactionCurrency; + $average = bcdiv(bcadd($bill->{$maxField} ?? '0', $bill->{$minField} ?? '0'), '2'); Log::debug(sprintf('Amount to pay is %s %s (%d times)', $currency->code, $average, $total)); - $return[$currency->id] ??= [ + $return[$currency->id] ??= [ 'id' => (string)$currency->id, 'name' => $currency->name, 'symbol' => $currency->symbol, @@ -652,7 +662,7 @@ class BillRepository implements BillRepositoryInterface, UserGroupInterface // \Illuminate\Support\Facades\Log::debug(sprintf('Currentstart (%s) has become %s.', $currentStart->format('Y-m-d'), $nextExpectedMatch->format('Y-m-d'))); - $currentStart = clone $nextExpectedMatch; + $currentStart = clone $nextExpectedMatch; } return $set; @@ -698,7 +708,7 @@ class BillRepository implements BillRepositoryInterface, UserGroupInterface return $service->update($bill, $data); } - #[\Override] + #[Override] public function correctTransfers(): void { /** @var null|TransactionType $withdrawal */ diff --git a/app/Repositories/Bill/BillRepositoryInterface.php b/app/Repositories/Bill/BillRepositoryInterface.php index 8051e1ceb5..4c021b9684 100644 --- a/app/Repositories/Bill/BillRepositoryInterface.php +++ b/app/Repositories/Bill/BillRepositoryInterface.php @@ -53,6 +53,7 @@ interface BillRepositoryInterface * Add correct order to bills. */ public function correctOrder(): void; + public function correctTransfers(): void; public function destroy(Bill $bill): bool; diff --git a/app/Support/Http/Controllers/RenderPartialViews.php b/app/Support/Http/Controllers/RenderPartialViews.php index fe021bb736..ff7cb1c416 100644 --- a/app/Support/Http/Controllers/RenderPartialViews.php +++ b/app/Support/Http/Controllers/RenderPartialViews.php @@ -417,10 +417,11 @@ trait RenderPartialViews $repository = app(TagRepositoryInterface::class); $tags = $repository->get(); - $grouped = []; + $grouped = []; + /** @var Tag $tag */ - foreach($tags as $tag) { - $year = (int) $tag->date?->year; + foreach ($tags as $tag) { + $year = (int) $tag->date?->year; $grouped[$year] ??= [ 'tags' => [], 'year' => 0 === $year ? trans('firefly.no_date') : $year, @@ -428,6 +429,7 @@ trait RenderPartialViews $grouped[$year]['tags'][] = $tag; } ksort($grouped); + try { $result = view('reports.options.tag', ['tags' => $grouped])->render(); } catch (Throwable $e) { diff --git a/app/TransactionRules/Actions/LinkToBill.php b/app/TransactionRules/Actions/LinkToBill.php index 02d691616e..636c1c6c49 100644 --- a/app/TransactionRules/Actions/LinkToBill.php +++ b/app/TransactionRules/Actions/LinkToBill.php @@ -46,19 +46,20 @@ class LinkToBill implements ActionInterface public function actOnArray(array $journal): bool { /** @var User $user */ - $user = User::find($journal['user_id']); + $user = User::find($journal['user_id']); /** @var BillRepositoryInterface $repository */ $repository = app(BillRepositoryInterface::class); $repository->setUser($user); - $billName = $this->action->getValue($journal); - $bill = $repository->findByName($billName); + $billName = $this->action->getValue($journal); + $bill = $repository->findByName($billName); + /** @var TransactionJournal $object */ - $object = TransactionJournal::with('transactionType')->find($journal['transaction_journal_id']); - $type = $object->transactionType->type; + $object = TransactionJournal::with('transactionType')->find($journal['transaction_journal_id']); + $type = $object->transactionType->type; if (null !== $bill && TransactionTypeEnum::WITHDRAWAL->value === $type) { - $count = DB::table('transaction_journals')->where('id', '=', $journal['transaction_journal_id'])->where('bill_id', $bill->id)->count(); + $count = DB::table('transaction_journals')->where('id', '=', $journal['transaction_journal_id'])->where('bill_id', $bill->id)->count(); if (0 !== $count) { Log::error(sprintf('RuleAction LinkToBill could not set the bill of journal #%d to bill "%s": already set.', $journal['transaction_journal_id'], $billName)); // event(new RuleActionFailedOnArray($this->action, $journal, trans('rules.already_linked_to_subscription', ['name' => $billName]))); diff --git a/app/TransactionRules/Expressions/ActionExpression.php b/app/TransactionRules/Expressions/ActionExpression.php index a23fee70e1..429d4f1495 100644 --- a/app/TransactionRules/Expressions/ActionExpression.php +++ b/app/TransactionRules/Expressions/ActionExpression.php @@ -143,8 +143,9 @@ class ActionExpression private function evaluateExpression(string $expr, array $journal): string { $result = $this->expressionLanguage->evaluate($expr, $journal); - if(is_array($result)) { + if (is_array($result)) { Log::error('Result of evaluating the expression is an array, please investigate', $result); + throw new FireflyException('Result of evaluating the expression is an array, please open a GitHub issue about this and include the error logs.'); } diff --git a/app/Validation/FireflyValidator.php b/app/Validation/FireflyValidator.php index aad2afc795..2130af3a7d 100644 --- a/app/Validation/FireflyValidator.php +++ b/app/Validation/FireflyValidator.php @@ -457,9 +457,9 @@ class FireflyValidator extends Validator * * @SuppressWarnings("PHPMD.UnusedFormalParameter") */ - public function validateSecurePassword($attribute, string|null $value): bool + public function validateSecurePassword($attribute, ?string $value): bool { - $value = (string)$value; + $value = (string)$value; $verify = false; if (array_key_exists('verify_password', $this->data)) { $verify = 1 === (int) $this->data['verify_password']; @@ -468,7 +468,7 @@ class FireflyValidator extends Validator /** @var Verifier $service */ $service = app(Verifier::class); - return $service->validPassword( $value); + return $service->validPassword($value); } return true; diff --git a/composer.lock b/composer.lock index 8b28675e06..44d0eaa3dc 100644 --- a/composer.lock +++ b/composer.lock @@ -1878,16 +1878,16 @@ }, { "name": "laravel/framework", - "version": "v12.45.0", + "version": "v12.46.0", "source": { "type": "git", "url": "https://github.com/laravel/framework.git", - "reference": "9dfd2afc48f2519bfdbe6862dfb9849491c673ad" + "reference": "9dcff48d25a632c1fadb713024c952fec489c4ae" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/framework/zipball/9dfd2afc48f2519bfdbe6862dfb9849491c673ad", - "reference": "9dfd2afc48f2519bfdbe6862dfb9849491c673ad", + "url": "https://api.github.com/repos/laravel/framework/zipball/9dcff48d25a632c1fadb713024c952fec489c4ae", + "reference": "9dcff48d25a632c1fadb713024c952fec489c4ae", "shasum": "" }, "require": { @@ -2096,7 +2096,7 @@ "issues": "https://github.com/laravel/framework/issues", "source": "https://github.com/laravel/framework" }, - "time": "2026-01-06T15:24:52+00:00" + "time": "2026-01-07T23:26:53+00:00" }, { "name": "laravel/passport", @@ -2235,16 +2235,16 @@ }, { "name": "laravel/sanctum", - "version": "v4.2.1", + "version": "v4.2.2", "source": { "type": "git", "url": "https://github.com/laravel/sanctum.git", - "reference": "f5fb373be39a246c74a060f2cf2ae2c2145b3664" + "reference": "fd447754d2d3f56950d53b930128af2e3b617de9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/sanctum/zipball/f5fb373be39a246c74a060f2cf2ae2c2145b3664", - "reference": "f5fb373be39a246c74a060f2cf2ae2c2145b3664", + "url": "https://api.github.com/repos/laravel/sanctum/zipball/fd447754d2d3f56950d53b930128af2e3b617de9", + "reference": "fd447754d2d3f56950d53b930128af2e3b617de9", "shasum": "" }, "require": { @@ -2294,7 +2294,7 @@ "issues": "https://github.com/laravel/sanctum/issues", "source": "https://github.com/laravel/sanctum" }, - "time": "2025-11-21T13:59:03+00:00" + "time": "2026-01-06T23:11:51+00:00" }, { "name": "laravel/serializable-closure", diff --git a/config/firefly.php b/config/firefly.php index ea32253719..a11a8dea8f 100644 --- a/config/firefly.php +++ b/config/firefly.php @@ -78,8 +78,8 @@ return [ 'running_balance_column' => (bool)envNonEmpty('USE_RUNNING_BALANCE', true), // this is only the default value, is not used. // see cer.php for exchange rates feature flag. ], - 'version' => '6.4.15', - 'build_time' => 1767729818, + 'version' => 'develop/2026-01-10', + 'build_time' => 1768053746, 'api_version' => '2.1.0', // field is no longer used. 'db_version' => 28, // field is no longer used. diff --git a/package-lock.json b/package-lock.json index 6a81fd44a4..efb2a399e7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3092,9 +3092,9 @@ } }, "node_modules/@types/express-serve-static-core": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.1.0.tgz", - "integrity": "sha512-jnHMsrd0Mwa9Cf4IdOzbz543y4XJepXrbia2T4b6+spXC2We3t1y6K44D3mR8XMFSXMCf3/l7rCgddfx7UNVBA==", + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.1.1.tgz", + "integrity": "sha512-v4zIMr/cX7/d2BpAEX3KNKL/JrT1s43s96lLvvdTmza1oEvDudCqK9aF/djc/SWgy8Yh0h30TZx5VpzqFCxk5A==", "dev": true, "license": "MIT", "dependencies": { @@ -3105,9 +3105,9 @@ } }, "node_modules/@types/express/node_modules/@types/express-serve-static-core": { - "version": "4.19.7", - "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.7.tgz", - "integrity": "sha512-FvPtiIf1LfhzsaIXhv/PHan/2FeQBbtBDtfX2QfvPxdUelMDEckK08SM6nqo1MIZY3RUlfA+HV8+hFUSio78qg==", + "version": "4.19.8", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.8.tgz", + "integrity": "sha512-02S5fmqeoKzVZCHPZid4b8JH2eM5HzQLZWN2FohQEy/0eXTq8VXZfSN6Pcr3F6N9R/vNrj7cpgbhjie6m/1tCA==", "dev": true, "license": "MIT", "dependencies": { @@ -3218,9 +3218,9 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "25.0.3", - "resolved": "https://registry.npmjs.org/@types/node/-/node-25.0.3.tgz", - "integrity": "sha512-W609buLVRVmeW693xKfzHeIV6nJGGz98uCPfeXI1ELMLXVeKYZ9m15fAMSaUPBHYLGFsVRcMmSCksQOrZV9BYA==", + "version": "25.0.5", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.0.5.tgz", + "integrity": "sha512-FuLxeLuSVOqHPxSN1fkcD8DLU21gAP7nCKqGRJ/FglbCUBs0NYN6TpHcdmyLeh8C0KwGIaZQJSv+OYG+KZz+Gw==", "dev": true, "license": "MIT", "dependencies": { @@ -4117,9 +4117,9 @@ "license": "MIT" }, "node_modules/baseline-browser-mapping": { - "version": "2.9.11", - "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.11.tgz", - "integrity": "sha512-Sg0xJUNDU1sJNGdfGWhVHX0kkZ+HWcvmVymJbj6NSgZZmW/8S9Y2HQ5euytnIgakgxN6papOAWiwDo1ctFDcoQ==", + "version": "2.9.14", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.14.tgz", + "integrity": "sha512-B0xUquLkiGLgHhpPBqvl7GWegWBUNuujQ6kXd/r1U38ElPT6Ok8KZ8e+FpUGEc2ZoRQUzq/aUnaKFc/svWUGSg==", "dev": true, "license": "Apache-2.0", "bin": { @@ -4550,9 +4550,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001762", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001762.tgz", - "integrity": "sha512-PxZwGNvH7Ak8WX5iXzoK1KPZttBXNPuaOvI2ZYU7NrlM+d9Ov+TUvlLOBNGzVXAntMSMMlJPd+jY6ovrVjSmUw==", + "version": "1.0.30001763", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001763.tgz", + "integrity": "sha512-mh/dGtq56uN98LlNX9qdbKnzINhX0QzhiWBFEkFfsFO4QyCvL8YegrJAazCwXIeqkIob8BlZPGM3xdnY+sgmvQ==", "dev": true, "funding": [ { @@ -7093,9 +7093,9 @@ } }, "node_modules/i18next": { - "version": "25.7.3", - "resolved": "https://registry.npmjs.org/i18next/-/i18next-25.7.3.tgz", - "integrity": "sha512-2XaT+HpYGuc2uTExq9TVRhLsso+Dxym6PWaKpn36wfBmTI779OQ7iP/XaZHzrnGyzU4SHpFrTYLKfVyBfAhVNA==", + "version": "25.7.4", + "resolved": "https://registry.npmjs.org/i18next/-/i18next-25.7.4.tgz", + "integrity": "sha512-hRkpEblXXcXSNbw8mBNq9042OEetgyB/ahc/X17uV/khPwzV+uB8RHceHh3qavyrkPJvmXFKXME2Sy1E0KjAfw==", "funding": [ { "type": "individual", @@ -11462,9 +11462,9 @@ } }, "node_modules/vite": { - "version": "7.3.0", - "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.0.tgz", - "integrity": "sha512-dZwN5L1VlUBewiP6H9s2+B3e3Jg96D0vzN+Ry73sOefebhYr9f94wwkMNN/9ouoU8pV1BqA1d1zGk8928cx0rg==", + "version": "7.3.1", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz", + "integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==", "dev": true, "license": "MIT", "dependencies": {