Compare commits

...

54 Commits

Author SHA1 Message Date
github-actions[bot]
c589360ad9 Merge pull request #11516 from firefly-iii/release-1768333294
🤖 Automatically merge the PR into the develop branch.
2026-01-13 20:41:40 +01:00
JC5
d4687fb34f 🤖 Auto commit for release 'develop' on 2026-01-13 2026-01-13 20:41:34 +01:00
github-actions[bot]
9c4159ca3d Merge pull request #11515 from firefly-iii/release-1768333066
🤖 Automatically merge the PR into the develop branch.
2026-01-13 20:37:53 +01:00
JC5
93507c8b96 🤖 Auto commit for release 'develop' on 2026-01-13 2026-01-13 20:37:46 +01:00
James Cole
23d49a4194 Code clean up and restoration using Mago. 2026-01-13 20:37:41 +01:00
James Cole
858c44fbce Reset fix. 2026-01-13 20:32:42 +01:00
James Cole
95a6543a94 Fix config 2026-01-13 05:26:12 +01:00
James Cole
855d40cf2f Update lock file 2026-01-13 05:25:11 +01:00
James Cole
7d0fec6326 Mago config 2026-01-13 05:25:01 +01:00
James Cole
ebd7dca6a9 Do some code cleanup courtesy of Mago. 2026-01-13 05:14:58 +01:00
James Cole
1d41fc9845 Do some code cleanup courtesy of Mago. 2026-01-13 05:13:01 +01:00
github-actions[bot]
fe9ae9c810 Merge pull request #11505 from firefly-iii/release-1768204215
🤖 Automatically merge the PR into the develop branch.
2026-01-12 08:50:22 +01:00
JC5
84600c5208 🤖 Auto commit for release 'develop' on 2026-01-12 2026-01-12 08:50:15 +01:00
James Cole
f7e89cab0a Fix #11502 2026-01-11 17:18:31 +01:00
James Cole
8195630a6e Fix #11501 2026-01-11 10:12:48 +01:00
github-actions[bot]
d5bf80a0cb Merge pull request #11498 from firefly-iii/release-1768068342
🤖 Automatically merge the PR into the develop branch.
2026-01-10 19:05:49 +01:00
JC5
ef1c64096d 🤖 Auto commit for release 'develop' on 2026-01-10 2026-01-10 19:05:42 +01:00
James Cole
ef35eaffb4 Merge branch 'develop' of github.com:firefly-iii/firefly-iii into develop 2026-01-10 18:28:21 +01:00
James Cole
869ee7c735 Add info 2026-01-10 18:28:10 +01:00
James Cole
6c534f01eb Catch null pointer. 2026-01-10 18:26:58 +01:00
github-actions[bot]
3a9d89b53d Merge pull request #11497 from firefly-iii/release-1768064652
🤖 Automatically merge the PR into the develop branch.
2026-01-10 18:04:21 +01:00
JC5
badff64cfd 🤖 Auto commit for release 'develop' on 2026-01-10 2026-01-10 18:04:12 +01:00
James Cole
abacfa212e Throw a 410. Don't report it. 2026-01-10 18:00:18 +01:00
github-actions[bot]
add2e859c4 Merge pull request #11496 from firefly-iii/release-1768064331
🤖 Automatically merge the PR into the develop branch.
2026-01-10 17:58:57 +01:00
JC5
92f6421fc4 🤖 Auto commit for release 'develop' on 2026-01-10 2026-01-10 17:58:51 +01:00
James Cole
f350c19ec1 Add some debug info. 2026-01-10 17:52:45 +01:00
James Cole
8170804d74 No error, just 404. 2026-01-10 17:50:54 +01:00
James Cole
4400f6217d Add debug logging. 2026-01-10 17:50:47 +01:00
James Cole
e223cea74e Add more feedback. 2026-01-10 17:42:51 +01:00
James Cole
2f62e11338 Add some info on the user's input. 2026-01-10 17:38:03 +01:00
github-actions[bot]
d96c7931d6 Merge pull request #11494 from firefly-iii/release-1768053858
🤖 Automatically merge the PR into the develop branch.
2026-01-10 15:04:28 +01:00
JC5
2ab105a902 🤖 Auto commit for release 'develop' on 2026-01-10 2026-01-10 15:04:18 +01:00
James Cole
7ced1f8cf3 Add debug logging for https://github.com/orgs/firefly-iii/discussions/11431 2026-01-10 14:35:14 +01:00
James Cole
03364d9530 More strict check on transaction journal type. 2026-01-10 08:19:10 +01:00
James Cole
1f75612741 Merge branch 'develop' of github.com:firefly-iii/firefly-iii into develop 2026-01-10 08:16:47 +01:00
James Cole
a141cf6e67 Remove bills from transfers in more places. 2026-01-10 08:16:41 +01:00
James Cole
c97fb07e8d Group tags by date. 2026-01-10 07:37:22 +01:00
James Cole
9833dd49a9 Merge pull request #11483 from pilipovicn/add-rsd-currency 2026-01-09 11:59:26 +01:00
embedded
b76f4fe7b9 Add Serbian Dinar to Currency Seeder 2026-01-09 11:32:04 +01:00
James Cole
6c114e2ffc Throw better error 2026-01-09 06:07:50 +01:00
James Cole
bd396673ed Fix bad header exception. 2026-01-09 05:58:05 +01:00
James Cole
ad72bc1722 Fix #11479 2026-01-09 05:57:55 +01:00
James Cole
466b42200d Fix #11473 2026-01-07 20:53:44 +01:00
github-actions[bot]
067112904e Merge pull request #11467 from firefly-iii/develop
🤖 Automatically merge the PR into the main branch.
2026-01-06 21:05:43 +01:00
github-actions[bot]
fc371e27b7 Merge pull request #11466 from firefly-iii/release-1767729927
🤖 Automatically merge the PR into the develop branch.
2026-01-06 21:05:36 +01:00
JC5
52b14b46a2 🤖 Auto commit for release 'v6.4.15' on 2026-01-06 2026-01-06 21:05:27 +01:00
James Cole
5260b770bb Clean up changelog. 2026-01-06 21:01:07 +01:00
github-actions[bot]
226c4c8f8e Merge pull request #11464 from firefly-iii/release-1767729551
🤖 Automatically merge the PR into the develop branch.
2026-01-06 20:59:17 +01:00
JC5
9ebafe64f1 🤖 Auto commit for release 'develop' on 2026-01-06 2026-01-06 20:59:11 +01:00
James Cole
ffa618101d Update changelog. 2026-01-06 20:55:08 +01:00
James Cole
280e531a76 Fix #11396 2026-01-06 20:43:11 +01:00
James Cole
f542a3fd88 Merge branch 'develop' of github.com:firefly-iii/firefly-iii into develop 2026-01-06 20:38:46 +01:00
James Cole
581d67a92c Fix #11399 2026-01-06 20:38:41 +01:00
github-actions[bot]
637d8e050a Merge pull request #11376 from firefly-iii/develop
🤖 Automatically merge the PR into the main branch.
2025-12-16 06:42:25 +01:00
92 changed files with 1296 additions and 1038 deletions

View File

@@ -40,7 +40,8 @@ return $config->setRules(
[
// rule sets
'@PHP83Migration' => true,
'@PHP8x3Migration' => true,
'@PHP8x4Migration' => true,
'@PhpCsFixer' => true,
'@PhpCsFixer:risky' => true,
'@PSR12' => true,

View File

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

View File

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

View File

@@ -24,7 +24,6 @@ declare(strict_types=1);
namespace FireflyIII\Api\V1\Controllers\Autocomplete;
use Illuminate\Http\Request;
use FireflyIII\Api\V1\Controllers\Controller;
use FireflyIII\Api\V1\Requests\Autocomplete\AutocompleteApiRequest;
use FireflyIII\Enums\AccountTypeEnum;
@@ -37,6 +36,7 @@ use FireflyIII\Support\Facades\Amount;
use FireflyIII\Support\Facades\Steam;
use FireflyIII\Support\Http\Api\AccountFilter;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Log;
/**
@@ -51,7 +51,7 @@ class AccountController extends Controller
protected array $acceptedRoles = [UserRoleEnum::READ_ONLY];
/** @var array<int, string> */
private array $balanceTypes;
private array $balanceTypes;
private AccountRepositoryInterface $repository;
/**
@@ -60,16 +60,14 @@ class AccountController extends Controller
public function __construct()
{
parent::__construct();
$this->middleware(
function (Request $request, $next) {
$this->validateUserGroup($request);
$this->repository = app(AccountRepositoryInterface::class);
$this->repository->setUser($this->user);
$this->repository->setUserGroup($this->userGroup);
$this->middleware(function (Request $request, $next) {
$this->validateUserGroup($request);
$this->repository = app(AccountRepositoryInterface::class);
$this->repository->setUser($this->user);
$this->repository->setUserGroup($this->userGroup);
return $next($request);
}
);
return $next($request);
});
$this->balanceTypes = [AccountTypeEnum::ASSET->value, AccountTypeEnum::LOAN->value, AccountTypeEnum::DEBT->value, AccountTypeEnum::MORTGAGE->value];
}
@@ -83,24 +81,18 @@ class AccountController extends Controller
public function accounts(AutocompleteApiRequest $request): JsonResponse
{
Log::debug('Before All.');
[
'types' => $types,
'query' => $query,
'date' => $date,
'limit' => $limit,
]
= $request->attributes->all();
['types' => $types, 'query' => $query, 'date' => $date, 'limit' => $limit] = $request->attributes->all();
$date ??= today(config('app.timezone'));
// set date to end-of-day for account balance. so it is at $date 23:59:59
$date->endOfDay();
$return = [];
$timer = Timer::getInstance();
$return = [];
$timer = Timer::getInstance();
$timer->start(sprintf('AC accounts "%s"', $query));
$result = $this->repository->searchAccount((string)$query, $types, $limit);
$allBalances = Steam::accountsBalancesOptimized($result, $date, $this->primaryCurrency, $this->convertToPrimary);
$result = $this->repository->searchAccount((string) $query, $types, $limit);
$allBalances = Steam::accountsBalancesOptimized($result, $date, $this->primaryCurrency, $this->convertToPrimary);
/** @var Account $account */
foreach ($result as $account) {
@@ -118,17 +110,17 @@ class AccountController extends Controller
}
$return[] = [
'id' => (string)$account->id,
'id' => (string) $account->id,
'name' => $account->name,
'name_with_balance' => $nameWithBalance,
'active' => $account->active,
'type' => $account->accountType->type,
'currency_id' => (string)$useCurrency->id,
'currency_id' => (string) $useCurrency->id,
'currency_name' => $useCurrency->name,
'currency_code' => $useCurrency->code,
'currency_symbol' => $useCurrency->symbol,
'currency_decimal_places' => $useCurrency->decimal_places,
'account_currency_id' => (string)$currency->id,
'account_currency_id' => (string) $currency->id,
'account_currency_name' => $currency->name,
'account_currency_code' => $currency->code,
'account_currency_symbol' => $currency->symbol,
@@ -137,16 +129,13 @@ class AccountController extends Controller
}
// custom order.
usort(
$return,
static function (array $left, array $right): int {
$order = [AccountTypeEnum::ASSET->value, AccountTypeEnum::REVENUE->value, AccountTypeEnum::EXPENSE->value];
$posA = (int)array_search($left['type'], $order, true);
$posB = (int)array_search($right['type'], $order, true);
usort($return, static function (array $left, array $right): int {
$order = [AccountTypeEnum::ASSET->value, AccountTypeEnum::REVENUE->value, AccountTypeEnum::EXPENSE->value];
$posA = (int) array_search($left['type'], $order, true);
$posB = (int) array_search($right['type'], $order, true);
return $posA - $posB;
}
);
return $posA - $posB;
});
$timer->stop(sprintf('AC accounts "%s"', $query));
return response()->api($return);

View File

@@ -24,13 +24,13 @@ declare(strict_types=1);
namespace FireflyIII\Api\V1\Controllers\Autocomplete;
use Illuminate\Http\Request;
use FireflyIII\Api\V1\Controllers\Controller;
use FireflyIII\Api\V1\Requests\Autocomplete\AutocompleteApiRequest;
use FireflyIII\Enums\UserRoleEnum;
use FireflyIII\Models\Bill;
use FireflyIII\Repositories\Bill\BillRepositoryInterface;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
/**
* Class BillController
@@ -46,16 +46,14 @@ class BillController extends Controller
public function __construct()
{
parent::__construct();
$this->middleware(
function (Request $request, $next) {
$this->validateUserGroup($request);
$this->repository = app(BillRepositoryInterface::class);
$this->repository->setUser($this->user);
$this->repository->setUserGroup($this->userGroup);
$this->middleware(function (Request $request, $next) {
$this->validateUserGroup($request);
$this->repository = app(BillRepositoryInterface::class);
$this->repository->setUser($this->user);
$this->repository->setUserGroup($this->userGroup);
return $next($request);
}
);
return $next($request);
});
}
/**
@@ -65,13 +63,7 @@ class BillController extends Controller
public function bills(AutocompleteApiRequest $request): JsonResponse
{
$result = $this->repository->searchBill($request->attributes->get('query'), $request->attributes->get('limit'));
$filtered = $result->map(
static fn (Bill $item): array => [
'id' => (string) $item->id,
'name' => $item->name,
'active' => $item->active,
]
);
$filtered = $result->map(static fn (Bill $item): array => ['id' => (string) $item->id, 'name' => $item->name, 'active' => $item->active]);
return response()->api($filtered->toArray());
}

View File

@@ -24,13 +24,13 @@ declare(strict_types=1);
namespace FireflyIII\Api\V1\Controllers\Autocomplete;
use Illuminate\Http\Request;
use FireflyIII\Api\V1\Controllers\Controller;
use FireflyIII\Api\V1\Requests\Autocomplete\AutocompleteApiRequest;
use FireflyIII\Enums\UserRoleEnum;
use FireflyIII\Models\Budget;
use FireflyIII\Repositories\Budget\BudgetRepositoryInterface;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
/**
* Class BudgetController
@@ -46,16 +46,14 @@ class BudgetController extends Controller
public function __construct()
{
parent::__construct();
$this->middleware(
function (Request $request, $next) {
$this->validateUserGroup($request);
$this->repository = app(BudgetRepositoryInterface::class);
$this->repository->setUser($this->user);
$this->repository->setUserGroup($this->userGroup);
$this->middleware(function (Request $request, $next) {
$this->validateUserGroup($request);
$this->repository = app(BudgetRepositoryInterface::class);
$this->repository->setUser($this->user);
$this->repository->setUserGroup($this->userGroup);
return $next($request);
}
);
return $next($request);
});
}
/**
@@ -65,13 +63,7 @@ class BudgetController extends Controller
public function budgets(AutocompleteApiRequest $request): JsonResponse
{
$result = $this->repository->searchBudget($request->attributes->get('query'), $request->attributes->get('limit'));
$filtered = $result->map(
static fn (Budget $item): array => [
'id' => (string) $item->id,
'name' => $item->name,
'active' => $item->active,
]
);
$filtered = $result->map(static fn (Budget $item): array => ['id' => (string) $item->id, 'name' => $item->name, 'active' => $item->active]);
return response()->api($filtered->toArray());
}

View File

@@ -24,13 +24,13 @@ declare(strict_types=1);
namespace FireflyIII\Api\V1\Controllers\Autocomplete;
use Illuminate\Http\Request;
use FireflyIII\Api\V1\Controllers\Controller;
use FireflyIII\Api\V1\Requests\Autocomplete\AutocompleteApiRequest;
use FireflyIII\Enums\UserRoleEnum;
use FireflyIII\Models\Category;
use FireflyIII\Repositories\Category\CategoryRepositoryInterface;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
/**
* Class CategoryController
@@ -46,16 +46,14 @@ class CategoryController extends Controller
public function __construct()
{
parent::__construct();
$this->middleware(
function (Request $request, $next) {
$this->validateUserGroup($request);
$this->repository = app(CategoryRepositoryInterface::class);
$this->repository->setUser($this->user);
$this->repository->setUserGroup($this->userGroup);
$this->middleware(function (Request $request, $next) {
$this->validateUserGroup($request);
$this->repository = app(CategoryRepositoryInterface::class);
$this->repository->setUser($this->user);
$this->repository->setUserGroup($this->userGroup);
return $next($request);
}
);
return $next($request);
});
}
/**
@@ -65,12 +63,7 @@ class CategoryController extends Controller
public function categories(AutocompleteApiRequest $request): JsonResponse
{
$result = $this->repository->searchCategory($request->attributes->get('query'), $request->attributes->get('limit'));
$filtered = $result->map(
static fn (Category $item): array => [
'id' => (string) $item->id,
'name' => $item->name,
]
);
$filtered = $result->map(static fn (Category $item): array => ['id' => (string) $item->id, 'name' => $item->name]);
return response()->api($filtered->toArray());
}

View File

@@ -24,7 +24,6 @@ declare(strict_types=1);
namespace FireflyIII\Api\V1\Controllers\Autocomplete;
use Illuminate\Http\Request;
use Deprecated;
use FireflyIII\Api\V1\Controllers\Controller;
use FireflyIII\Api\V1\Requests\Autocomplete\AutocompleteApiRequest;
@@ -33,6 +32,7 @@ use FireflyIII\Enums\UserRoleEnum;
use FireflyIII\Models\TransactionCurrency;
use FireflyIII\Repositories\Currency\CurrencyRepositoryInterface;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
/**
* Class CurrencyController
@@ -48,16 +48,14 @@ class CurrencyController extends Controller
public function __construct()
{
parent::__construct();
$this->middleware(
function (Request $request, $next) {
$this->validateUserGroup($request);
$this->repository = app(CurrencyRepositoryInterface::class);
$this->repository->setUser($this->user);
$this->repository->setUserGroup($this->userGroup);
$this->middleware(function (Request $request, $next) {
$this->validateUserGroup($request);
$this->repository = app(CurrencyRepositoryInterface::class);
$this->repository->setUser($this->user);
$this->repository->setUserGroup($this->userGroup);
return $next($request);
}
);
return $next($request);
});
}
/**

View File

@@ -24,13 +24,13 @@ declare(strict_types=1);
namespace FireflyIII\Api\V1\Controllers\Autocomplete;
use Illuminate\Http\Request;
use FireflyIII\Api\V1\Controllers\Controller;
use FireflyIII\Api\V1\Requests\Autocomplete\AutocompleteApiRequest;
use FireflyIII\Enums\UserRoleEnum;
use FireflyIII\Models\ObjectGroup;
use FireflyIII\Repositories\ObjectGroup\ObjectGroupRepositoryInterface;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
/**
* Class ObjectGroupController
@@ -46,16 +46,14 @@ class ObjectGroupController extends Controller
public function __construct()
{
parent::__construct();
$this->middleware(
function (Request $request, $next) {
$this->validateUserGroup($request);
$this->repository = app(ObjectGroupRepositoryInterface::class);
$this->repository->setUser($this->user);
$this->repository->setUserGroup($this->userGroup);
$this->middleware(function (Request $request, $next) {
$this->validateUserGroup($request);
$this->repository = app(ObjectGroupRepositoryInterface::class);
$this->repository->setUser($this->user);
$this->repository->setUserGroup($this->userGroup);
return $next($request);
}
);
return $next($request);
});
}
/**
@@ -69,11 +67,7 @@ class ObjectGroupController extends Controller
/** @var ObjectGroup $objectGroup */
foreach ($result as $objectGroup) {
$return[] = [
'id' => (string) $objectGroup->id,
'name' => $objectGroup->title,
'title' => $objectGroup->title,
];
$return[] = ['id' => (string) $objectGroup->id, 'name' => $objectGroup->title, 'title' => $objectGroup->title];
}
return response()->api($return);

View File

@@ -24,7 +24,6 @@ declare(strict_types=1);
namespace FireflyIII\Api\V1\Controllers\Autocomplete;
use Illuminate\Http\Request;
use FireflyIII\Api\V1\Controllers\Controller;
use FireflyIII\Api\V1\Requests\Autocomplete\AutocompleteApiRequest;
use FireflyIII\Enums\UserRoleEnum;
@@ -34,13 +33,14 @@ use FireflyIII\Repositories\Account\AccountRepositoryInterface;
use FireflyIII\Repositories\PiggyBank\PiggyBankRepositoryInterface;
use FireflyIII\Support\Facades\Amount;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
/**
* Class PiggyBankController
*/
class PiggyBankController extends Controller
{
private AccountRepositoryInterface $accountRepository;
private AccountRepositoryInterface $accountRepository;
private PiggyBankRepositoryInterface $piggyRepository;
protected array $acceptedRoles = [UserRoleEnum::READ_PIGGY_BANKS];
@@ -50,19 +50,17 @@ class PiggyBankController extends Controller
public function __construct()
{
parent::__construct();
$this->middleware(
function (Request $request, $next) {
$this->validateUserGroup($request);
$this->piggyRepository = app(PiggyBankRepositoryInterface::class);
$this->accountRepository = app(AccountRepositoryInterface::class);
$this->piggyRepository->setUser($this->user);
$this->piggyRepository->setUserGroup($this->userGroup);
$this->accountRepository->setUser($this->user);
$this->accountRepository->setUserGroup($this->userGroup);
$this->middleware(function (Request $request, $next) {
$this->validateUserGroup($request);
$this->piggyRepository = app(PiggyBankRepositoryInterface::class);
$this->accountRepository = app(AccountRepositoryInterface::class);
$this->piggyRepository->setUser($this->user);
$this->piggyRepository->setUserGroup($this->userGroup);
$this->accountRepository->setUser($this->user);
$this->accountRepository->setUserGroup($this->userGroup);
return $next($request);
}
);
return $next($request);
});
}
public function piggyBanks(AutocompleteApiRequest $request): JsonResponse
@@ -108,7 +106,7 @@ class PiggyBankController extends Controller
'%s (%s / %s)',
$piggy->name,
Amount::formatAnything($currency, $currentAmount, false),
Amount::formatAnything($currency, $piggy->target_amount, false),
Amount::formatAnything($currency, $piggy->target_amount, false)
),
'currency_id' => (string) $currency->id,
'currency_name' => $currency->name,

View File

@@ -24,13 +24,13 @@ declare(strict_types=1);
namespace FireflyIII\Api\V1\Controllers\Autocomplete;
use Illuminate\Http\Request;
use FireflyIII\Api\V1\Controllers\Controller;
use FireflyIII\Api\V1\Requests\Autocomplete\AutocompleteApiRequest;
use FireflyIII\Enums\UserRoleEnum;
use FireflyIII\Models\Recurrence;
use FireflyIII\Repositories\Recurring\RecurringRepositoryInterface;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
/**
* Class RecurrenceController
@@ -46,16 +46,14 @@ class RecurrenceController extends Controller
public function __construct()
{
parent::__construct();
$this->middleware(
function (Request $request, $next) {
$this->validateUserGroup($request);
$this->repository = app(RecurringRepositoryInterface::class);
$this->repository->setUser($this->user);
$this->repository->setUserGroup($this->userGroup);
$this->middleware(function (Request $request, $next) {
$this->validateUserGroup($request);
$this->repository = app(RecurringRepositoryInterface::class);
$this->repository->setUser($this->user);
$this->repository->setUserGroup($this->userGroup);
return $next($request);
}
);
return $next($request);
});
}
public function recurring(AutocompleteApiRequest $request): JsonResponse

View File

@@ -24,13 +24,13 @@ declare(strict_types=1);
namespace FireflyIII\Api\V1\Controllers\Autocomplete;
use Illuminate\Http\Request;
use FireflyIII\Api\V1\Controllers\Controller;
use FireflyIII\Api\V1\Requests\Autocomplete\AutocompleteApiRequest;
use FireflyIII\Enums\UserRoleEnum;
use FireflyIII\Models\Rule;
use FireflyIII\Repositories\Rule\RuleRepositoryInterface;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
/**
* Class RuleController
@@ -38,7 +38,7 @@ use Illuminate\Http\JsonResponse;
class RuleController extends Controller
{
private RuleRepositoryInterface $repository;
protected array $acceptedRoles = [UserRoleEnum::READ_RULES];
protected array $acceptedRoles = [UserRoleEnum::READ_RULES];
/**
* RuleController constructor.
@@ -46,16 +46,14 @@ class RuleController extends Controller
public function __construct()
{
parent::__construct();
$this->middleware(
function (Request $request, $next) {
$this->validateUserGroup($request);
$this->repository = app(RuleRepositoryInterface::class);
$this->repository->setUser($this->user);
$this->repository->setUserGroup($this->userGroup);
$this->middleware(function (Request $request, $next) {
$this->validateUserGroup($request);
$this->repository = app(RuleRepositoryInterface::class);
$this->repository->setUser($this->user);
$this->repository->setUserGroup($this->userGroup);
return $next($request);
}
);
return $next($request);
});
}
public function rules(AutocompleteApiRequest $request): JsonResponse
@@ -66,7 +64,7 @@ class RuleController extends Controller
/** @var Rule $rule */
foreach ($rules as $rule) {
$response[] = [
'id' => (string)$rule->id,
'id' => (string) $rule->id,
'name' => $rule->title,
'description' => $rule->description,
'active' => $rule->active,

View File

@@ -24,13 +24,13 @@ declare(strict_types=1);
namespace FireflyIII\Api\V1\Controllers\Autocomplete;
use Illuminate\Http\Request;
use FireflyIII\Api\V1\Controllers\Controller;
use FireflyIII\Api\V1\Requests\Autocomplete\AutocompleteApiRequest;
use FireflyIII\Enums\UserRoleEnum;
use FireflyIII\Models\RuleGroup;
use FireflyIII\Repositories\RuleGroup\RuleGroupRepositoryInterface;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
/**
* Class RuleGroupController
@@ -46,16 +46,14 @@ class RuleGroupController extends Controller
public function __construct()
{
parent::__construct();
$this->middleware(
function (Request $request, $next) {
$this->validateUserGroup($request);
$this->repository = app(RuleGroupRepositoryInterface::class);
$this->repository->setUser($this->user);
$this->repository->setUserGroup($this->userGroup);
$this->middleware(function (Request $request, $next) {
$this->validateUserGroup($request);
$this->repository = app(RuleGroupRepositoryInterface::class);
$this->repository->setUser($this->user);
$this->repository->setUserGroup($this->userGroup);
return $next($request);
}
);
return $next($request);
});
}
public function ruleGroups(AutocompleteApiRequest $request): JsonResponse

View File

@@ -24,13 +24,13 @@ declare(strict_types=1);
namespace FireflyIII\Api\V1\Controllers\Autocomplete;
use Illuminate\Http\Request;
use FireflyIII\Api\V1\Controllers\Controller;
use FireflyIII\Api\V1\Requests\Autocomplete\AutocompleteApiRequest;
use FireflyIII\Enums\UserRoleEnum;
use FireflyIII\Models\Tag;
use FireflyIII\Repositories\Tag\TagRepositoryInterface;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
/**
* Class TagController
@@ -46,16 +46,14 @@ class TagController extends Controller
public function __construct()
{
parent::__construct();
$this->middleware(
function (Request $request, $next) {
$this->validateUserGroup($request);
$this->repository = app(TagRepositoryInterface::class);
$this->repository->setUser($this->user);
$this->repository->setUserGroup($this->userGroup);
$this->middleware(function (Request $request, $next) {
$this->validateUserGroup($request);
$this->repository = app(TagRepositoryInterface::class);
$this->repository->setUser($this->user);
$this->repository->setUserGroup($this->userGroup);
return $next($request);
}
);
return $next($request);
});
}
public function tags(AutocompleteApiRequest $request): JsonResponse
@@ -65,11 +63,7 @@ class TagController extends Controller
/** @var Tag $tag */
foreach ($result as $tag) {
$array[] = [
'id' => (string) $tag->id,
'name' => $tag->tag,
'tag' => $tag->tag,
];
$array[] = ['id' => (string) $tag->id, 'name' => $tag->tag, 'tag' => $tag->tag];
}
return response()->api($array);

View File

@@ -24,7 +24,6 @@ declare(strict_types=1);
namespace FireflyIII\Api\V1\Controllers\Autocomplete;
use Illuminate\Http\Request;
use FireflyIII\Api\V1\Controllers\Controller;
use FireflyIII\Api\V1\Requests\Autocomplete\AutocompleteApiRequest;
use FireflyIII\Api\V1\Requests\Autocomplete\AutocompleteTransactionApiRequest;
@@ -34,6 +33,7 @@ use FireflyIII\Models\TransactionJournal;
use FireflyIII\Repositories\Journal\JournalRepositoryInterface;
use FireflyIII\Repositories\TransactionGroup\TransactionGroupRepositoryInterface;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Collection;
/**
@@ -43,7 +43,7 @@ class TransactionController extends Controller
{
protected array $acceptedRoles = [UserRoleEnum::READ_ONLY];
private TransactionGroupRepositoryInterface $groupRepository;
private JournalRepositoryInterface $repository;
private JournalRepositoryInterface $repository;
/**
* TransactionController constructor.
@@ -51,19 +51,17 @@ class TransactionController extends Controller
public function __construct()
{
parent::__construct();
$this->middleware(
function (Request $request, $next) {
$this->validateUserGroup($request);
$this->repository = app(JournalRepositoryInterface::class);
$this->groupRepository = app(TransactionGroupRepositoryInterface::class);
$this->repository->setUser($this->user);
$this->repository->setUserGroup($this->userGroup);
$this->groupRepository->setUser($this->user);
$this->groupRepository->setUserGroup($this->userGroup);
$this->middleware(function (Request $request, $next) {
$this->validateUserGroup($request);
$this->repository = app(JournalRepositoryInterface::class);
$this->groupRepository = app(TransactionGroupRepositoryInterface::class);
$this->repository->setUser($this->user);
$this->repository->setUserGroup($this->userGroup);
$this->groupRepository->setUser($this->user);
$this->groupRepository->setUserGroup($this->userGroup);
return $next($request);
}
);
return $next($request);
});
}
public function transactions(AutocompleteTransactionApiRequest $request): JsonResponse

View File

@@ -24,13 +24,13 @@ declare(strict_types=1);
namespace FireflyIII\Api\V1\Controllers\Autocomplete;
use Illuminate\Http\Request;
use FireflyIII\Api\V1\Controllers\Controller;
use FireflyIII\Api\V1\Requests\Autocomplete\AutocompleteApiRequest;
use FireflyIII\Enums\UserRoleEnum;
use FireflyIII\Models\TransactionType;
use FireflyIII\Repositories\TransactionType\TransactionTypeRepositoryInterface;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
/**
* Class TransactionTypeController
@@ -46,14 +46,12 @@ class TransactionTypeController extends Controller
public function __construct()
{
parent::__construct();
$this->middleware(
function (Request $request, $next) {
$this->validateUserGroup($request);
$this->repository = app(TransactionTypeRepositoryInterface::class);
$this->middleware(function (Request $request, $next) {
$this->validateUserGroup($request);
$this->repository = app(TransactionTypeRepositoryInterface::class);
return $next($request);
}
);
return $next($request);
});
}
public function transactionTypes(AutocompleteApiRequest $request): JsonResponse
@@ -64,11 +62,7 @@ class TransactionTypeController extends Controller
/** @var TransactionType $type */
foreach ($types as $type) {
// different key for consistency.
$array[] = [
'id' => (string) $type->id,
'name' => $type->type,
'type' => $type->type,
];
$array[] = ['id' => (string) $type->id, 'name' => $type->type, 'type' => $type->type];
}
return response()->api($array);

View File

@@ -49,9 +49,9 @@ class AccountController extends Controller
use CleansChartData;
use CollectsAccountsFromFilter;
protected array $acceptedRoles = [UserRoleEnum::READ_ONLY];
protected array $acceptedRoles = [UserRoleEnum::READ_ONLY];
private array $chartData = [];
private array $chartData = [];
private AccountRepositoryInterface $repository;
/**
@@ -60,16 +60,14 @@ class AccountController extends Controller
public function __construct()
{
parent::__construct();
$this->middleware(
function (Request $request, $next) {
$this->repository = app(AccountRepositoryInterface::class);
$this->validateUserGroup($request);
$this->repository->setUserGroup($this->userGroup);
$this->repository->setUser($this->user);
$this->middleware(function (Request $request, $next) {
$this->repository = app(AccountRepositoryInterface::class);
$this->validateUserGroup($request);
$this->repository->setUserGroup($this->userGroup);
$this->repository->setUser($this->user);
return $next($request);
}
);
return $next($request);
});
}
/**
@@ -115,14 +113,14 @@ class AccountController extends Controller
'label' => $account->name,
// the currency that belongs to the account.
'currency_id' => (string)$currency->id,
'currency_id' => (string) $currency->id,
'currency_name' => $currency->name,
'currency_code' => $currency->code,
'currency_symbol' => $currency->symbol,
'currency_decimal_places' => $currency->decimal_places,
// the primary currency
'primary_currency_id' => (string)$this->primaryCurrency->id,
'primary_currency_id' => (string) $this->primaryCurrency->id,
// the default currency of the user (could be the same!)
'date' => $params['start']->toAtomString(),
@@ -136,7 +134,7 @@ class AccountController extends Controller
];
if ($this->convertToPrimary) {
$currentSet['pc_entries'] = [];
$currentSet['primary_currency_id'] = (string)$this->primaryCurrency->id;
$currentSet['primary_currency_id'] = (string) $this->primaryCurrency->id;
$currentSet['primary_currency_code'] = $this->primaryCurrency->code;
$currentSet['primary_currency_symbol'] = $this->primaryCurrency->symbol;
$currentSet['primary_currency_decimal_places'] = $this->primaryCurrency->decimal_places;
@@ -151,7 +149,6 @@ class AccountController extends Controller
$previous = $balance;
$currentSet['entries'][$label] = $balance;
// do the same for the primary currency balance, if relevant:
$pcBalance = null;
if ($this->convertToPrimary) {
@@ -160,6 +157,7 @@ class AccountController extends Controller
$currentSet['pc_entries'][$label] = $pcBalance;
}
$currentStart = Navigation::addPeriod($currentStart, $period);
// $currentStart->addDay();
}
$this->chartData[] = $currentSet;

View File

@@ -1,6 +1,5 @@
<?php
/*
* BalanceController.php
* Copyright (c) 2025 james@firefly-iii.org
@@ -25,7 +24,6 @@ declare(strict_types=1);
namespace FireflyIII\Api\V1\Controllers\Chart;
use Illuminate\Http\Request;
use FireflyIII\Api\V1\Controllers\Controller;
use FireflyIII\Api\V1\Requests\Chart\ChartRequest;
use FireflyIII\Enums\TransactionTypeEnum;
@@ -37,6 +35,7 @@ use FireflyIII\Support\Http\Api\AccountBalanceGrouped;
use FireflyIII\Support\Http\Api\CleansChartData;
use FireflyIII\Support\Http\Api\CollectsAccountsFromFilter;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
/**
* Class BalanceController
@@ -45,10 +44,11 @@ class BalanceController extends Controller
{
use CleansChartData;
use CollectsAccountsFromFilter;
protected array $acceptedRoles = [UserRoleEnum::READ_ONLY];
private array $chartData = [];
private GroupCollectorInterface $collector;
protected array $acceptedRoles = [UserRoleEnum::READ_ONLY];
private array $chartData = [];
private GroupCollectorInterface $collector;
private AccountRepositoryInterface $repository;
// private TransactionCurrency $default;
@@ -56,19 +56,17 @@ class BalanceController extends Controller
public function __construct()
{
parent::__construct();
$this->middleware(
function (Request $request, $next) {
$this->validateUserGroup($request);
$this->repository = app(AccountRepositoryInterface::class);
$this->collector = app(GroupCollectorInterface::class);
$this->repository->setUserGroup($this->userGroup);
$this->collector->setUserGroup($this->userGroup);
$this->repository->setUser($this->user);
$this->collector->setUser($this->user);
$this->middleware(function (Request $request, $next) {
$this->validateUserGroup($request);
$this->repository = app(AccountRepositoryInterface::class);
$this->collector = app(GroupCollectorInterface::class);
$this->repository->setUserGroup($this->userGroup);
$this->collector->setUserGroup($this->userGroup);
$this->repository->setUser($this->user);
$this->collector->setUser($this->user);
return $next($request);
}
);
return $next($request);
});
}
/**
@@ -89,10 +87,16 @@ class BalanceController extends Controller
// get journals for entire period:
$this->collector->setRange($queryParameters['start'], $queryParameters['end'])
$this->collector
->setRange($queryParameters['start'], $queryParameters['end'])
->withAccountInformation()
->setXorAccounts($accounts)
->setTypes([TransactionTypeEnum::WITHDRAWAL->value, TransactionTypeEnum::DEPOSIT->value, TransactionTypeEnum::RECONCILIATION->value, TransactionTypeEnum::TRANSFER->value])
->setTypes([
TransactionTypeEnum::WITHDRAWAL->value,
TransactionTypeEnum::DEPOSIT->value,
TransactionTypeEnum::RECONCILIATION->value,
TransactionTypeEnum::TRANSFER->value,
])
;
$journals = $this->collector->getExtractedJournals();

View File

@@ -24,7 +24,6 @@ declare(strict_types=1);
namespace FireflyIII\Api\V1\Controllers\Chart;
use Illuminate\Http\Request;
use Carbon\Carbon;
use FireflyIII\Api\V1\Controllers\Controller;
use FireflyIII\Api\V1\Requests\DateRangeRequest;
@@ -36,13 +35,14 @@ use FireflyIII\Repositories\Budget\BudgetLimitRepositoryInterface;
use FireflyIII\Repositories\Budget\BudgetRepositoryInterface;
use FireflyIII\Repositories\Budget\OperationsRepositoryInterface;
use FireflyIII\Support\Facades\Amount;
use FireflyIII\Support\Facades\Steam;
use FireflyIII\Support\Http\Api\CleansChartData;
use FireflyIII\Support\Http\Api\ExchangeRateConverter;
use FireflyIII\Support\Http\Api\ValidatesUserGroupTrait;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Log;
use FireflyIII\Support\Facades\Steam;
/**
* Class BudgetController
@@ -52,32 +52,30 @@ class BudgetController extends Controller
use CleansChartData;
use ValidatesUserGroupTrait;
protected array $acceptedRoles = [UserRoleEnum::READ_ONLY];
protected array $acceptedRoles = [UserRoleEnum::READ_ONLY];
protected OperationsRepositoryInterface $opsRepository;
private BudgetLimitRepositoryInterface $blRepository;
private array $currencies = [];
private BudgetRepositoryInterface $repository;
private BudgetLimitRepositoryInterface $blRepository;
private array $currencies = [];
private BudgetRepositoryInterface $repository;
public function __construct()
{
parent::__construct();
$this->middleware(
function (Request $request, $next) {
$this->validateUserGroup($request);
$this->repository = app(BudgetRepositoryInterface::class);
$this->blRepository = app(BudgetLimitRepositoryInterface::class);
$this->opsRepository = app(OperationsRepositoryInterface::class);
$this->repository->setUserGroup($this->userGroup);
$this->opsRepository->setUserGroup($this->userGroup);
$this->blRepository->setUserGroup($this->userGroup);
$this->repository->setUser($this->user);
$this->opsRepository->setUser($this->user);
$this->blRepository->setUser($this->user);
$this->middleware(function (Request $request, $next) {
$this->validateUserGroup($request);
$this->repository = app(BudgetRepositoryInterface::class);
$this->blRepository = app(BudgetLimitRepositoryInterface::class);
$this->opsRepository = app(OperationsRepositoryInterface::class);
$this->repository->setUserGroup($this->userGroup);
$this->opsRepository->setUserGroup($this->userGroup);
$this->blRepository->setUserGroup($this->userGroup);
$this->repository->setUser($this->user);
$this->opsRepository->setUser($this->user);
$this->blRepository->setUser($this->user);
return $next($request);
}
);
return $next($request);
});
}
/**
@@ -158,18 +156,17 @@ class BudgetController extends Controller
$rows[] = $row;
}
// is always an array
$return = [];
foreach ($rows as $row) {
$current = [
'label' => $budget->name,
'currency_id' => (string)$row['currency_id'],
'currency_id' => (string) $row['currency_id'],
'currency_name' => $row['currency_name'],
'currency_code' => $row['currency_code'],
'currency_decimal_places' => $row['currency_decimal_places'],
'primary_currency_id' => (string)$this->primaryCurrency->id,
'primary_currency_id' => (string) $this->primaryCurrency->id,
'primary_currency_name' => $this->primaryCurrency->name,
'primary_currency_code' => $this->primaryCurrency->code,
'primary_currency_symbol' => $this->primaryCurrency->symbol,
@@ -181,12 +178,7 @@ class BudgetController extends Controller
'end_date' => $row['end'],
'yAxisID' => 0,
'type' => 'bar',
'entries' => [
'budgeted' => $row['budgeted'],
'spent' => $row['spent'],
'left' => $row['left'],
'overspent' => $row['overspent'],
],
'entries' => ['budgeted' => $row['budgeted'], 'spent' => $row['spent'], 'left' => $row['left'], 'overspent' => $row['overspent']],
'pc_entries' => [
'budgeted' => $row['pc_budgeted'],
'spent' => $row['pc_spent'],
@@ -233,11 +225,11 @@ class BudgetController extends Controller
foreach ($spent as $currencyId => $block) {
$this->currencies[$currencyId] ??= Amount::getTransactionCurrencyById($currencyId);
$return[$currencyId] ??= [
'currency_id' => (string)$currencyId,
'currency_id' => (string) $currencyId,
'currency_code' => $block['currency_code'],
'currency_name' => $block['currency_name'],
'currency_symbol' => $block['currency_symbol'],
'currency_decimal_places' => (int)$block['currency_decimal_places'],
'currency_decimal_places' => (int) $block['currency_decimal_places'],
'start' => $start->toAtomString(),
'end' => $end->toAtomString(),
'budgeted' => '0',
@@ -251,7 +243,7 @@ class BudgetController extends Controller
/** @var array $journal */
foreach ($currentBudgetArray['transaction_journals'] as $journal) {
/** @var numeric-string $amount */
$amount = (string)$journal['amount'];
$amount = (string) $journal['amount'];
$return[$currencyId]['spent'] = bcadd($return[$currencyId]['spent'], $amount);
}
}
@@ -270,14 +262,21 @@ class BudgetController extends Controller
if ($this->convertToPrimary) {
if ($current->transaction_currency_id === $this->primaryCurrency->id) {
// simply add it.
$amount = bcadd($amount, (string)$current->amount);
$amount = bcadd($amount, (string) $current->amount);
Log::debug(sprintf('Set amount in limit to %s', $amount));
}
if ($current->transaction_currency_id !== $this->primaryCurrency->id) {
// convert and then add it.
$converted = $converter->convert($current->transactionCurrency, $this->primaryCurrency, $current->start_date, $current->amount);
$amount = bcadd($amount, $converted);
Log::debug(sprintf('Budgeted in limit #%d: %s %s, converted to %s %s', $current->id, $current->transactionCurrency->code, $current->amount, $this->primaryCurrency->code, $converted));
Log::debug(sprintf(
'Budgeted in limit #%d: %s %s, converted to %s %s',
$current->id,
$current->transactionCurrency->code,
$current->amount,
$this->primaryCurrency->code,
$converted
));
Log::debug(sprintf('Set amount in limit to %s', $amount));
}
}

View File

@@ -24,7 +24,6 @@ declare(strict_types=1);
namespace FireflyIII\Api\V1\Controllers\Chart;
use Illuminate\Http\Request;
use Carbon\Carbon;
use FireflyIII\Api\V1\Controllers\Controller;
use FireflyIII\Api\V1\Requests\DateRangeRequest;
@@ -40,6 +39,7 @@ use FireflyIII\Support\Http\Api\CleansChartData;
use FireflyIII\Support\Http\Api\ExchangeRateConverter;
use FireflyIII\Support\Http\Api\ValidatesUserGroupTrait;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Log;
/**
@@ -52,25 +52,23 @@ class CategoryController extends Controller
protected array $acceptedRoles = [UserRoleEnum::READ_ONLY];
private AccountRepositoryInterface $accountRepos;
private AccountRepositoryInterface $accountRepos;
private CurrencyRepositoryInterface $currencyRepos;
public function __construct()
{
parent::__construct();
$this->middleware(
function (Request $request, $next) {
$this->validateUserGroup($request);
$this->accountRepos = app(AccountRepositoryInterface::class);
$this->currencyRepos = app(CurrencyRepositoryInterface::class);
$this->accountRepos->setUserGroup($this->userGroup);
$this->currencyRepos->setUserGroup($this->userGroup);
$this->accountRepos->setUser($this->user);
$this->currencyRepos->setUser($this->user);
$this->middleware(function (Request $request, $next) {
$this->validateUserGroup($request);
$this->accountRepos = app(AccountRepositoryInterface::class);
$this->currencyRepos = app(CurrencyRepositoryInterface::class);
$this->accountRepos->setUserGroup($this->userGroup);
$this->currencyRepos->setUserGroup($this->userGroup);
$this->accountRepos->setUser($this->user);
$this->currencyRepos->setUser($this->user);
return $next($request);
}
);
return $next($request);
});
}
/**
@@ -88,7 +86,12 @@ class CategoryController extends Controller
/** @var Carbon $end */
$end = $request->attributes->get('end');
$accounts = $this->accountRepos->getAccountsByType([AccountTypeEnum::DEBT->value, AccountTypeEnum::LOAN->value, AccountTypeEnum::MORTGAGE->value, AccountTypeEnum::ASSET->value]);
$accounts = $this->accountRepos->getAccountsByType([
AccountTypeEnum::DEBT->value,
AccountTypeEnum::LOAN->value,
AccountTypeEnum::MORTGAGE->value,
AccountTypeEnum::ASSET->value,
]);
$currencies = [];
$return = [];
$converter = new ExchangeRateConverter();
@@ -104,7 +107,7 @@ class CategoryController extends Controller
/** @var array $journal */
foreach ($journals as $journal) {
// find journal:
$journalCurrencyId = (int)$journal['currency_id'];
$journalCurrencyId = (int) $journal['currency_id'];
$type = $journal['transaction_type_type'];
$currency = $currencies[$journalCurrencyId] ?? $this->currencyRepos->find($journalCurrencyId);
$currencies[$journalCurrencyId] = $currency;
@@ -113,7 +116,7 @@ class CategoryController extends Controller
$currencyCode = $currency->code;
$currencySymbol = $currency->symbol;
$currencyDecimalPlaces = $currency->decimal_places;
$amount = Steam::positive((string)$journal['amount']);
$amount = Steam::positive((string) $journal['amount']);
$pcAmount = null;
// overrule if necessary:
@@ -130,18 +133,17 @@ class CategoryController extends Controller
Log::debug(sprintf('Converted %s %s to %s %s', $journal['currency_code'], $amount, $this->primaryCurrency->code, $pcAmount));
}
$categoryName = $journal['category_name'] ?? (string)trans('firefly.no_category');
$categoryName = $journal['category_name'] ?? (string) trans('firefly.no_category');
$key = sprintf('%s-%s', $categoryName, $currencyCode);
// create arrays
$return[$key] ??= [
'label' => $categoryName,
'currency_id' => (string)$currencyId,
'currency_id' => (string) $currencyId,
'currency_name' => $currencyName,
'currency_code' => $currencyCode,
'currency_symbol' => $currencySymbol,
'currency_decimal_places' => $currencyDecimalPlaces,
'primary_currency_id' => (string)$this->primaryCurrency->id,
'primary_currency_id' => (string) $this->primaryCurrency->id,
'primary_currency_name' => $this->primaryCurrency->name,
'primary_currency_code' => $this->primaryCurrency->code,
'primary_currency_symbol' => $this->primaryCurrency->symbol,
@@ -151,14 +153,8 @@ class CategoryController extends Controller
'end_date' => $end->toAtomString(),
'yAxisID' => 0,
'type' => 'bar',
'entries' => [
'spent' => '0',
'earned' => '0',
],
'pc_entries' => [
'spent' => '0',
'earned' => '0',
],
'entries' => ['spent' => '0', 'earned' => '0'],
'pc_entries' => ['spent' => '0', 'earned' => '0'],
];
// add monies
@@ -182,7 +178,10 @@ class CategoryController extends Controller
$return = array_values($return);
// order by amount
usort($return, static fn (array $a, array $b): int => ((float)$a['entries']['spent'] + (float)$a['entries']['earned']) < ((float)$b['entries']['spent'] + (float)$b['entries']['earned']) ? 1 : -1);
usort($return, static fn (array $a, array $b): int => ((float) $a['entries']['spent'] + (float) $a['entries']['earned'])
< ((float) $b['entries']['spent'] + (float) $b['entries']['earned'])
? 1
: -1);
return response()->json($this->clean($return));
}

View File

@@ -24,9 +24,9 @@ declare(strict_types=1);
namespace FireflyIII\Api\V1\Controllers;
use Deprecated;
use Carbon\Carbon;
use Carbon\Exceptions\InvalidFormatException;
use Deprecated;
use FireflyIII\Exceptions\BadHttpHeaderException;
use FireflyIII\Models\TransactionCurrency;
use FireflyIII\Support\Facades\Amount;
@@ -63,15 +63,16 @@ abstract class Controller extends BaseController
use ValidatesRequests;
use ValidatesUserGroupTrait;
protected const string CONTENT_TYPE = 'application/vnd.api+json';
protected const string JSON_CONTENT_TYPE = 'application/json';
protected array $accepts = ['application/json', 'application/vnd.api+json'];
protected const string CONTENT_TYPE = 'application/vnd.api+json';
protected const string JSON_CONTENT_TYPE = 'application/json';
protected bool $convertToPrimary = false;
protected array $accepts = ['application/json', 'application/vnd.api+json'];
protected bool $convertToPrimary = false;
protected TransactionCurrency $primaryCurrency;
/** @deprecated use Request classes */
protected ParameterBag $parameters;
protected ParameterBag $parameters;
/**
* Controller constructor.
@@ -79,26 +80,22 @@ abstract class Controller extends BaseController
public function __construct()
{
// get global parameters
$this->middleware(
function ($request, $next) {
$this->parameters = $this->getParameters();
if (auth()->check()) {
$language = Steam::getLanguage();
$this->convertToPrimary = Amount::convertToPrimary();
$this->primaryCurrency = Amount::getPrimaryCurrency();
app()->setLocale($language);
}
// filter down what this endpoint accepts.
if (!$request->accepts($this->accepts)) {
throw new BadHttpHeaderException(sprintf('Sorry, Accept header "%s" is not something this endpoint can provide.', $request->header('Accept')));
}
return $next($request);
$this->middleware(function ($request, $next) {
$this->parameters = $this->getParameters();
if (auth()->check()) {
$language = Steam::getLanguage();
$this->convertToPrimary = Amount::convertToPrimary();
$this->primaryCurrency = Amount::getPrimaryCurrency();
app()->setLocale($language);
}
);
// filter down what this endpoint accepts.
if (!$request->accepts($this->accepts)) {
throw new BadHttpHeaderException(sprintf('Sorry, Accept header "%s" is not something this endpoint can provide.', $request->header('Accept')));
}
return $next($request);
});
}
#[Deprecated(message: <<<'TXT'
@@ -108,7 +105,7 @@ abstract class Controller extends BaseController
private function getParameters(): ParameterBag
{
$bag = new ParameterBag();
$page = (int)request()->get('page');
$page = (int) request()->get('page');
$page = min(max(1, $page), 2 ** 16);
$bag->set('page', $page);
@@ -127,10 +124,10 @@ abstract class Controller extends BaseController
$obj = null;
if (null !== $date) {
try {
$obj = Carbon::parse((string)$date, config('app.timezone'));
$obj = Carbon::parse((string) $date, config('app.timezone'));
} catch (InvalidFormatException $e) {
// don't care
Log::warning(sprintf('Ignored invalid date "%s" in API controller parameter check: %s', substr((string)$date, 0, 20), $e->getMessage()));
Log::warning(sprintf('Ignored invalid date "%s" in API controller parameter check: %s', substr((string) $date, 0, 20), $e->getMessage()));
}
}
if ($obj instanceof Carbon) {
@@ -150,24 +147,27 @@ abstract class Controller extends BaseController
$value = null;
}
if (null !== $value) {
$value = (int)$value;
$value = (int) $value;
$value = min(max(1, $value), 2 ** 16);
$bag->set($integer, $value);
}
if (null === $value
if (
null === $value
&& 'limit' === $integer // @phpstan-ignore-line
&& auth()->check()) {
&& auth()->check()
) {
// set default for user:
/** @var User $user */
$user = auth()->user();
$pageSize = (int)Preferences::getForUser($user, 'listPageSize', 50)->data;
$pageSize = (int) Preferences::getForUser($user, 'listPageSize', 50)->data;
$bag->set($integer, $pageSize);
}
}
// sort fields:
return $bag;
// return $this->getSortParameters($bag);
}

View File

@@ -24,7 +24,6 @@ declare(strict_types=1);
namespace FireflyIII\Api\V1\Controllers\Data;
use Illuminate\Http\Request;
use FireflyIII\Api\V1\Controllers\Controller;
use FireflyIII\Api\V1\Requests\Data\DestroyRequest;
use FireflyIII\Enums\AccountTypeEnum;
@@ -49,6 +48,7 @@ use FireflyIII\Services\Internal\Destroy\AccountDestroyService;
use FireflyIII\Services\Internal\Destroy\JournalDestroyService;
use FireflyIII\Support\Facades\Preferences;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Log;
/**
@@ -63,13 +63,11 @@ class DestroyController extends Controller
public function __construct()
{
parent::__construct();
$this->middleware(
function (Request $request, $next) {
$this->validateUserGroup($request);
$this->middleware(function (Request $request, $next) {
$this->validateUserGroup($request);
return $next($request);
}
);
return $next($request);
});
}
public function destroy(DestroyRequest $request): JsonResponse
@@ -77,10 +75,40 @@ class DestroyController extends Controller
$objects = $request->getObjects();
$this->unused = $request->boolean('unused');
$allExceptAssets = [AccountTypeEnum::BENEFICIARY->value, AccountTypeEnum::CASH->value, AccountTypeEnum::CREDITCARD->value, AccountTypeEnum::DEFAULT->value, AccountTypeEnum::EXPENSE->value, AccountTypeEnum::IMPORT->value, AccountTypeEnum::INITIAL_BALANCE->value, AccountTypeEnum::LIABILITY_CREDIT->value, AccountTypeEnum::RECONCILIATION->value, AccountTypeEnum::REVENUE->value];
$all = [AccountTypeEnum::ASSET->value, AccountTypeEnum::BENEFICIARY->value, AccountTypeEnum::CASH->value, AccountTypeEnum::CREDITCARD->value, AccountTypeEnum::DEBT->value, AccountTypeEnum::DEFAULT->value, AccountTypeEnum::EXPENSE->value, AccountTypeEnum::IMPORT->value, AccountTypeEnum::INITIAL_BALANCE->value, AccountTypeEnum::LIABILITY_CREDIT->value, AccountTypeEnum::LOAN->value, AccountTypeEnum::MORTGAGE->value, AccountTypeEnum::RECONCILIATION->value];
$allExceptAssets = [
AccountTypeEnum::BENEFICIARY->value,
AccountTypeEnum::CASH->value,
AccountTypeEnum::CREDITCARD->value,
AccountTypeEnum::DEFAULT->value,
AccountTypeEnum::EXPENSE->value,
AccountTypeEnum::IMPORT->value,
AccountTypeEnum::INITIAL_BALANCE->value,
AccountTypeEnum::LIABILITY_CREDIT->value,
AccountTypeEnum::RECONCILIATION->value,
AccountTypeEnum::REVENUE->value,
];
$all = [
AccountTypeEnum::ASSET->value,
AccountTypeEnum::BENEFICIARY->value,
AccountTypeEnum::CASH->value,
AccountTypeEnum::CREDITCARD->value,
AccountTypeEnum::DEBT->value,
AccountTypeEnum::DEFAULT->value,
AccountTypeEnum::EXPENSE->value,
AccountTypeEnum::IMPORT->value,
AccountTypeEnum::INITIAL_BALANCE->value,
AccountTypeEnum::LIABILITY_CREDIT->value,
AccountTypeEnum::LOAN->value,
AccountTypeEnum::MORTGAGE->value,
AccountTypeEnum::RECONCILIATION->value,
];
$liabilities = [AccountTypeEnum::DEBT->value, AccountTypeEnum::LOAN->value, AccountTypeEnum::MORTGAGE->value, AccountTypeEnum::CREDITCARD->value];
$transactions = [TransactionTypeEnum::WITHDRAWAL->value, TransactionTypeEnum::DEPOSIT->value, TransactionTypeEnum::TRANSFER->value, TransactionTypeEnum::RECONCILIATION->value];
$transactions = [
TransactionTypeEnum::WITHDRAWAL->value,
TransactionTypeEnum::DEPOSIT->value,
TransactionTypeEnum::TRANSFER->value,
TransactionTypeEnum::RECONCILIATION->value,
];
match ($objects) {
'budgets' => $this->destroyBudgets(),
@@ -101,7 +129,7 @@ class DestroyController extends Controller
'withdrawals' => $this->destroyTransactions([TransactionTypeEnum::WITHDRAWAL->value]),
'deposits' => $this->destroyTransactions([TransactionTypeEnum::DEPOSIT->value]),
'transfers' => $this->destroyTransactions([TransactionTypeEnum::TRANSFER->value]),
default => throw new FireflyException(sprintf('200033: This endpoint can\'t handle object "%s"', $objects)),
default => throw new FireflyException(sprintf('200033: This endpoint can\'t handle object "%s"', $objects))
};
Preferences::mark();

View File

@@ -24,7 +24,6 @@ declare(strict_types=1);
namespace FireflyIII\Api\V1\Controllers\Data;
use Illuminate\Http\Request;
use FireflyIII\Api\V1\Controllers\Controller;
use FireflyIII\Enums\UserRoleEnum;
use FireflyIII\Models\Account;
@@ -40,6 +39,7 @@ use FireflyIII\Models\TransactionJournal;
use FireflyIII\Repositories\PiggyBank\PiggyBankRepositoryInterface;
use FireflyIII\User;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
/**
* Class PurgeController
@@ -51,13 +51,11 @@ class PurgeController extends Controller
public function __construct()
{
parent::__construct();
$this->middleware(
function (Request $request, $next) {
$this->validateUserGroup($request);
$this->middleware(function (Request $request, $next) {
$this->validateUserGroup($request);
return $next($request);
}
);
return $next($request);
});
}
/**

View File

@@ -44,6 +44,8 @@ use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Validator;
use Illuminate\Validation\ValidationException;
use League\Fractal\Resource\Item;
use Symfony\Component\HttpKernel\Exception\GoneHttpException;
use Symfony\Component\HttpKernel\Exception\HttpException;
/**
* Class StoreController
@@ -82,7 +84,7 @@ class StoreController extends Controller
*
* Store a new transaction.
*
* @throws FireflyException|ValidationException
* @throws FireflyException|GoneHttpException|ValidationException
*/
public function store(StoreRequest $request): JsonResponse
{
@@ -133,7 +135,7 @@ class StoreController extends Controller
$selectedGroup = $collector->getGroups()->first();
if (null === $selectedGroup) {
throw new FireflyException('200032: Cannot find transaction. Possibly, a rule deleted this transaction after its creation.');
throw HttpException::fromStatusCode(410, '200032: Cannot find transaction. Possibly, a rule deleted this transaction after its creation.');
}
// enrich

View File

@@ -262,7 +262,7 @@ class ListController extends Controller
// filter selection
$collection = $unfiltered->filter(
static function (Recurrence $recurrence) use ($currency): ?Recurrence { // @phpstan-ignore-line
if (array_any($recurrence->recurrenceTransactions, fn ($transaction): bool => $transaction->transaction_currency_id === $currency->id || $transaction->foreign_currency_id === $currency->id)) {
if (array_any($recurrence->recurrenceTransactions, static fn ($transaction): bool => $transaction->transaction_currency_id === $currency->id || $transaction->foreign_currency_id === $currency->id)) {
return $recurrence;
}
@@ -311,7 +311,7 @@ class ListController extends Controller
$collection = $unfiltered->filter(
static function (Rule $rule) use ($currency): ?Rule { // @phpstan-ignore-line
if (array_any($rule->ruleTriggers, fn ($trigger): bool => 'currency_is' === $trigger->trigger_type && $currency->name === $trigger->trigger_value)) {
if (array_any($rule->ruleTriggers, static fn ($trigger): bool => 'currency_is' === $trigger->trigger_type && $currency->name === $trigger->trigger_value)) {
return $rule;
}

View File

@@ -91,7 +91,7 @@ class ConvertsDatesToUTC extends Command
}
$this->friendlyInfo(sprintf('Converting field "%s" of model "%s" to UTC.', $field, $shortModel));
$items->each(
function ($item) use ($field, $timezoneField): void {
static function ($item) use ($field, $timezoneField): void {
$date = Carbon::parse($item->{$field}, $item->{$timezoneField}); // @phpstan-ignore-line
$date->setTimezone('UTC');
$item->{$field} = $date->format('Y-m-d H:i:s'); // @phpstan-ignore-line

View File

@@ -103,7 +103,7 @@ class CorrectsPrimaryCurrencyAmounts extends Command
private function recalculateAccounts(UserGroup $userGroup): void
{
$set = $userGroup->accounts()->where(function (EloquentBuilder $q): void {
$set = $userGroup->accounts()->where(static function (EloquentBuilder $q): void {
$q->whereNotNull('virtual_balance');
// this needs a different piece of code for postgres.
@@ -226,10 +226,10 @@ class CorrectsPrimaryCurrencyAmounts extends Command
$set = DB::table('transactions')
->join('transaction_journals', 'transaction_journals.id', '=', 'transactions.transaction_journal_id')
->where('transaction_journals.user_group_id', $userGroup->id)
->where(function (DatabaseBuilder $q1) use ($currency): void {
$q1->where(function (DatabaseBuilder $q2) use ($currency): void {
->where(static function (DatabaseBuilder $q1) use ($currency): void {
$q1->where(static function (DatabaseBuilder $q2) use ($currency): void {
$q2->whereNot('transactions.transaction_currency_id', $currency->id)->whereNull('transactions.foreign_currency_id');
})->orWhere(function (DatabaseBuilder $q3) use ($currency): void {
})->orWhere(static function (DatabaseBuilder $q3) use ($currency): void {
$q3->whereNot('transactions.transaction_currency_id', $currency->id)->whereNot('transactions.foreign_currency_id', $currency->id);
});
})

View File

@@ -34,7 +34,7 @@ class RemovesBills extends Command
{
use ShowsFriendlyMessages;
protected $description = 'Remove bills from transactions that shouldn\'t have one.';
protected $description = 'Remove subscriptions from transactions that shouldn\'t have one.';
protected $signature = 'correction:bills';
/**

View File

@@ -198,15 +198,29 @@ class ApplyRules extends Command
$accountRepository = app(AccountRepositoryInterface::class);
$accountRepository->setUser($this->getUser());
foreach ($accountList as $accountId) {
$accountId = (int) $accountId;
$account = $accountRepository->find($accountId);
if (null !== $account && in_array($account->accountType->type, $this->acceptedAccounts, true)) {
$finalList->push($account);
$accountId = (int)$accountId;
if (0 === $accountId) {
$this->friendlyWarning('You provided an account with ID 0 (zero). It will be ignored.');
continue;
}
$account = $accountRepository->find($accountId);
if (null === $account) {
$this->friendlyWarning(sprintf('There is no account with ID #%d, it cannot be added.', $accountId));
continue;
}
$type = $account->accountType->type;
if (!in_array($account->accountType->type, $this->acceptedAccounts, true)) {
$this->friendlyWarning(sprintf('Account "%s" with ID #%d is of type "%s" and cannot be added.', $account->name, $accountId, $type));
continue;
}
$finalList->push($account);
}
if (0 === $finalList->count()) {
$this->friendlyError('Please make sure all accounts in --accounts are asset accounts or liabilities.');
$this->friendlyError('There are no accounts in the selection. Please make sure all accounts in --accounts are asset accounts or liabilities.');
return false;
}
@@ -225,13 +239,27 @@ class ApplyRules extends Command
$ruleGroupList = explode(',', $ruleGroupString);
foreach ($ruleGroupList as $ruleGroupId) {
$ruleGroup = $this->ruleGroupRepository->find((int) $ruleGroupId);
if (true === $ruleGroup->active) {
$this->ruleGroupSelection[] = $ruleGroup->id;
$ruleGroupId = (int)$ruleGroupId;
if (0 === $ruleGroupId) {
$this->friendlyWarning('You added a rule group with ID 0 (zero). It will be skipped.');
continue;
}
$ruleGroup = $this->ruleGroupRepository->find($ruleGroupId);
if (null === $ruleGroup) {
$this->friendlyWarning(sprintf('There is no rule group with ID #%d, this ID will be ignored.', $ruleGroupId));
continue;
}
if (false === $ruleGroup->active) {
$this->friendlyWarning(sprintf('Will ignore inactive rule group #%d ("%s")', $ruleGroup->id, $ruleGroup->title));
$this->friendlyWarning(sprintf('Rule group with ID #%d is not active, so this ID will be ignored.', $ruleGroupId));
continue;
}
$this->ruleGroupSelection[] = $ruleGroupId;
}
return true;
@@ -247,10 +275,24 @@ class ApplyRules extends Command
$ruleList = explode(',', $ruleString);
foreach ($ruleList as $ruleId) {
$rule = $this->ruleRepository->find((int) $ruleId);
if ($rule instanceof Rule && true === $rule->active) {
$this->ruleSelection[] = $rule->id;
$ruleId = (int)$ruleId;
if (0 === $ruleId) {
$this->friendlyWarning('You added a rule with ID 0 (zero). It will be skipped.');
continue;
}
$rule = $this->ruleRepository->find($ruleId);
if (null === $rule) {
$this->friendlyWarning(sprintf('There is no rule with ID #%d, this ID will be ignored.', $ruleId));
continue;
}
if (false === $rule->active) {
$this->friendlyWarning(sprintf('Rule with ID #%d is not active, so this ID will be ignored.', $ruleId));
continue;
}
$this->ruleSelection[] = $ruleId;
}
return true;
@@ -304,10 +346,12 @@ class ApplyRules extends Command
private function getRulesToApply(): Collection
{
Log::debug('getRulesToApply()');
$rulesToApply = new Collection();
/** @var RuleGroup $group */
foreach ($this->groups as $group) {
Log::debug(sprintf('Scanning rule group #%d', $group->id));
$rules = $this->ruleGroupRepository->getActiveStoreRules($group);
/** @var Rule $rule */
@@ -318,16 +362,20 @@ class ApplyRules extends Command
Log::debug(sprintf('Will include rule #%d "%s"', $rule->id, $rule->title));
$rulesToApply->push($rule);
}
if (!$test) {
Log::debug(sprintf('Will not include rule #%d', $rule->id));
}
}
}
Log::debug(sprintf('Found %d rules to apply.', $rulesToApply->count()));
return $rulesToApply;
}
private function includeRule(Rule $rule, RuleGroup $group): bool
{
return in_array($group->id, $this->ruleGroupSelection, true)
|| in_array($rule->id, $this->ruleSelection, true)
return in_array((int)$group->id, $this->ruleGroupSelection, true)
|| in_array((int)$rule->id, $this->ruleSelection, true)
|| $this->allRules;
}
}

View File

@@ -26,6 +26,7 @@ namespace FireflyIII\Events;
use FireflyIII\User;
use Illuminate\Queue\SerializesModels;
use SensitiveParameter;
/**
* Class RequestedNewPassword.
@@ -46,7 +47,7 @@ class RequestedNewPassword extends Event
/**
* Create a new event instance. This event is triggered when a users tries to reset his or her password.
*/
public function __construct(User $user, string $token, string $ipAddress)
public function __construct(User $user, #[SensitiveParameter] string $token, string $ipAddress)
{
$this->user = $user;
$this->token = $token;

View File

@@ -45,6 +45,7 @@ use Override;
use Symfony\Component\HttpFoundation\Exception\SuspiciousOperationException;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
use Symfony\Component\HttpKernel\Exception\GoneHttpException;
use Symfony\Component\HttpKernel\Exception\HttpException;
use Symfony\Component\HttpKernel\Exception\MethodNotAllowedHttpException;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
@@ -71,6 +72,7 @@ class Handler extends ExceptionHandler
AuthenticationException::class,
LaravelValidationException::class,
NotFoundHttpException::class,
GoneHttpException::class,
OAuthServerException::class,
LaravelOAuthException::class,
TokenMismatchException::class,

View File

@@ -70,8 +70,12 @@ class UpdatedGroupEventHandler
foreach ($event->transactionGroup->transactionJournals as $journal) {
$source = $journal->transactions()->where('amount', '<', '0')->first();
$dest = $journal->transactions()->where('amount', '>', '0')->first();
$repository->deleteStatisticsForModel($source->account, $journal->date);
$repository->deleteStatisticsForModel($dest->account, $journal->date);
if (null !== $source) {
$repository->deleteStatisticsForModel($source->account, $journal->date);
}
if (null !== $dest) {
$repository->deleteStatisticsForModel($dest->account, $journal->date);
}
$categories = $journal->categories;
$tags = $journal->tags;

View File

@@ -784,14 +784,23 @@ 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']);
$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']));
return true;
++$matched;
if (1 === count($list)) {
return true;
}
}
}
if (count($list) > 1 && $total === $matched && $matched === count($list)) {
Log::debug(sprintf('All %d searched tags are present.', $total));
return true;
}
}
Log::debug('Transaction has no tags from the list, so return false.');

View File

@@ -828,7 +828,7 @@ class GroupCollector implements GroupCollectorInterface
*/
foreach ($this->sorting as $field => $direction) {
$func = 'ASC' === $direction ? 'sortBy' : 'sortByDesc';
$collection = $collection->{$func}(function (array $product, int $key) use ($field) { // @phpstan-ignore-line
$collection = $collection->{$func}(static function (array $product, int $key) use ($field) { // @phpstan-ignore-line
// depends on $field:
if ('description' === $field) {
if (1 === count($product['transactions'])) {

View File

@@ -37,6 +37,7 @@ use Illuminate\View\View;
use Psr\Container\ContainerExceptionInterface;
use Psr\Container\NotFoundExceptionInterface;
use FireflyIII\Support\Facades\FireflyConfig;
use SensitiveParameter;
/**
* Class ResetPasswordController
@@ -97,7 +98,7 @@ class ResetPasswordController extends Controller
// database. Otherwise, we will parse the error and return the response.
$response = $this->broker()->reset(
$this->credentials($request),
function ($user, $password): void {
function ($user, #[SensitiveParameter] $password): void {
$this->resetPassword($user, $password);
}
);
@@ -123,7 +124,7 @@ class ResetPasswordController extends Controller
* @throws ContainerExceptionInterface
* @throws NotFoundExceptionInterface
*/
public function showResetForm(Request $request, $token = null)
public function showResetForm(Request $request, #[SensitiveParameter] $token = null)
{
if ('web' !== config('firefly.authentication_guard')) {
$message = sprintf('Cannot reset password when authenticating over "%s".', config('firefly.authentication_guard'));

View File

@@ -74,6 +74,7 @@ class IndexController extends Controller
{
$this->cleanupObjectGroups();
$this->repository->correctOrder();
$this->repository->correctTransfers();
$start = session('start');
$end = session('end');
$collection = $this->repository->getBills();

View File

@@ -29,7 +29,6 @@ use FireflyIII\Support\Facades\Navigation;
use Carbon\Carbon;
use FireflyIII\Helpers\Collector\GroupCollectorInterface;
use FireflyIII\Http\Controllers\Controller;
use FireflyIII\Models\Attachment;
use FireflyIII\Models\Bill;
use FireflyIII\Repositories\Bill\BillRepositoryInterface;
use FireflyIII\Support\JsonApi\Enrichments\SubscriptionEnrichment;
@@ -122,6 +121,7 @@ class ShowController extends Controller
*/
public function show(Request $request, Bill $bill): Factory|\Illuminate\Contracts\View\View
{
$this->repository->correctTransfers();
// add info about rules:
$rules = $this->repository->getRulesForBill($bill);
$subTitle = $bill->name;
@@ -184,7 +184,7 @@ class ShowController extends Controller
/** @var AttachmentTransformer $transformer */
$transformer = app(AttachmentTransformer::class);
$attachments = $collection->each(
static fn (Attachment $attachment) => $transformer->transform($attachment)
$transformer->transform(...)
);
}

View File

@@ -41,7 +41,7 @@ class IndexController extends Controller
// translations:
$this->middleware(
function ($request, $next) {
static function ($request, $next) {
app('view')->share('mainTitleIcon', 'fa-exchange');
app('view')->share('title', (string) trans('firefly.header_exchange_rates'));

View File

@@ -212,6 +212,13 @@ class ReconcileController extends Controller
$startBalance = Steam::accountsBalancesOptimized(new Collection()->push($account), $startDate)[$account->id];
$endBalance = Steam::accountsBalancesOptimized(new Collection()->push($account), $end)[$account->id];
// round balances.
foreach ($startBalance as $key => $value) {
$startBalance[$key] = Steam::bcround($value, $currency->decimal_places);
}
foreach ($endBalance as $key => $value) {
$endBalance[$key] = Steam::bcround($value, $currency->decimal_places);
}
// get the transactions

View File

@@ -198,12 +198,14 @@ class PreferencesController extends Controller
*/
public function postIndex(PreferencesRequest $request): Redirector|RedirectResponse
{
Log::debug('postIndex for preferences.');
// front page accounts
$frontpageAccounts = [];
if (is_array($request->get('frontpageAccounts')) && count($request->get('frontpageAccounts')) > 0) {
foreach ($request->get('frontpageAccounts') as $id) {
$frontpageAccounts[] = (int)$id;
}
Log::debug('Update frontpageAccounts', $frontpageAccounts);
Preferences::set('frontpageAccounts', $frontpageAccounts);
}
@@ -212,14 +214,17 @@ class PreferencesController extends Controller
foreach (config('notifications.notifications.user') as $key => $info) {
$key = sprintf('notification_%s', $key);
if (array_key_exists($key, $all)) {
Log::debug(sprintf('update notification to true: %s', $key));
Preferences::set($key, true);
}
if (!array_key_exists($key, $all)) {
Log::debug(sprintf('update notification to false: %s', $key));
Preferences::set($key, false);
}
}
// view range:
Log::debug(sprintf('Let viewRange to "%s"', $request->get('viewRange')));
Preferences::set('viewRange', $request->get('viewRange'));
// forget session values:
session()->forget('start');
@@ -319,6 +324,7 @@ class PreferencesController extends Controller
// save and continue
session()->flash('success', (string)trans('firefly.saved_preferences'));
Preferences::mark();
Log::debug('Done saving settings.');
return redirect(route('preferences.index'));
}

View File

@@ -52,6 +52,7 @@ use Illuminate\View\View;
use Laravel\Passport\ClientRepository;
use Psr\Container\ContainerExceptionInterface;
use Psr\Container\NotFoundExceptionInterface;
use SensitiveParameter;
/**
* Class ProfileController.
@@ -91,7 +92,7 @@ class ProfileController extends Controller
*
* @throws FireflyException
*/
public function confirmEmailChange(UserRepositoryInterface $repository, string $token): Redirector|RedirectResponse
public function confirmEmailChange(UserRepositoryInterface $repository, #[SensitiveParameter] string $token): Redirector|RedirectResponse
{
if (!$this->internalAuth) {
throw new FireflyException(trans('firefly.external_user_mgt_disabled'));
@@ -388,7 +389,7 @@ class ProfileController extends Controller
*
* @throws FireflyException
*/
public function undoEmailChange(UserRepositoryInterface $repository, string $token, string $hash): Redirector|RedirectResponse
public function undoEmailChange(UserRepositoryInterface $repository, #[SensitiveParameter] string $token, string $hash): Redirector|RedirectResponse
{
if (!$this->internalAuth) {
throw new FireflyException(trans('firefly.external_user_mgt_disabled'));

View File

@@ -58,6 +58,7 @@ class AcceptHeaders
// some routes are exempt from this.
$exempt = [
'api.v1.data.bulk.transactions',
'api.v1.attachments.upload',
];
if (('POST' === $method || 'PUT' === $method) && !$request->hasHeader('Content-Type') && !in_array($request->route()->getName(), $exempt, true)) {

View File

@@ -157,7 +157,7 @@ class CreateRecurringTransactions implements ShouldQueue
private function filterRecurrences(Collection $recurrences): Collection
{
return $recurrences->filter(
fn (Recurrence $recurrence): bool => $this->validRecurrence($recurrence)
$this->validRecurrence(...)
);
}

View File

@@ -53,6 +53,6 @@ class AccountMeta extends Model
protected function data(): Attribute
{
return Attribute::make(get: fn (mixed $value): string => (string)json_decode((string)$value, true), set: fn (mixed $value): array => ['data' => json_encode($value)]);
return Attribute::make(get: static fn (mixed $value): string => (string)json_decode((string)$value, true), set: static fn (mixed $value): array => ['data' => json_encode($value)]);
}
}

View File

@@ -103,16 +103,16 @@ class AvailableBudget extends Model
protected function endDate(): Attribute
{
return Attribute::make(
get: fn (string $value): Carbon => Carbon::parse($value),
set: fn (Carbon $value): string => $value->format('Y-m-d'),
get: Carbon::parse(...),
set: static fn (Carbon $value): string => $value->format('Y-m-d'),
);
}
protected function startDate(): Attribute
{
return Attribute::make(
get: fn (string $value): Carbon => Carbon::parse($value),
set: fn (Carbon $value): string => $value->format('Y-m-d'),
get: Carbon::parse(...),
set: static fn (Carbon $value): string => $value->format('Y-m-d'),
);
}

View File

@@ -52,6 +52,6 @@ class Configuration extends Model
*/
protected function data(): Attribute
{
return Attribute::make(get: fn ($value): mixed => json_decode((string)$value), set: fn ($value): array => ['data' => json_encode($value)]);
return Attribute::make(get: static fn ($value): mixed => json_decode((string)$value), set: static fn ($value): array => ['data' => json_encode($value)]);
}
}

View File

@@ -113,7 +113,7 @@ class Rule extends Model
protected function description(): Attribute
{
return Attribute::make(set: fn ($value): array => ['description' => e($value)]);
return Attribute::make(set: static fn ($value): array => ['description' => e($value)]);
}
protected function order(): Attribute

View File

@@ -57,7 +57,7 @@ class TransactionJournalMeta extends Model
protected function data(): Attribute
{
return Attribute::make(get: fn ($value): mixed => json_decode((string)$value, false), set: function ($value): array {
return Attribute::make(get: static fn ($value): mixed => json_decode((string)$value, false), set: static function ($value): array {
$data = json_encode($value);
return ['data' => $data, 'hash' => hash('sha256', $data)];

View File

@@ -45,7 +45,7 @@ class AppServiceProvider extends ServiceProvider
{
Schema::defaultStringLength(191);
// Passport::$clientUuids = false;
Response::macro('api', function (array $value) {
Response::macro('api', static function (array $value) {
$headers = [
'Cache-Control' => 'no-store',
];
@@ -61,7 +61,7 @@ class AppServiceProvider extends ServiceProvider
});
// blade extension
Blade::directive('activeXRoutePartial', function (string $route): string {
Blade::directive('activeXRoutePartial', static function (string $route): string {
$name = Route::getCurrentRoute()->getName() ?? '';
if (str_contains($name, $route)) {
return 'menu-open';
@@ -69,7 +69,7 @@ class AppServiceProvider extends ServiceProvider
return '';
});
Blade::if('partialroute', function (string $route, string $firstParam = ''): bool {
Blade::if('partialroute', static function (string $route, string $firstParam = ''): bool {
$name = Route::getCurrentRoute()->getName() ?? '';
if ('' === $firstParam && str_contains($name, $route)) {
return true;

View File

@@ -24,7 +24,6 @@ declare(strict_types=1);
namespace FireflyIII\Providers;
use FireflyIII\Support\Search\OperatorQuerySearch;
use FireflyIII\Support\Search\QueryParser\GdbotsQueryParser;
use FireflyIII\Support\Search\QueryParser\QueryParser;
use FireflyIII\Support\Search\QueryParser\QueryParserInterface;
use FireflyIII\Support\Search\SearchInterface;
@@ -49,16 +48,7 @@ class SearchServiceProvider extends ServiceProvider
public function register(): void
{
$this->app->bind(
static function (): QueryParserInterface {
return app(QueryParser::class);
// 2025-12-20 ignore this setting.
// $implementation = config('search.query_parser');
//
// return match ($implementation) {
// 'new' => app(QueryParser::class),
// default => app(GdbotsQueryParser::class),
// };
}
static fn (): QueryParserInterface => app(QueryParser::class)
);
$this->app->bind(

View File

@@ -23,8 +23,8 @@ declare(strict_types=1);
namespace FireflyIII\Repositories\Bill;
use FireflyIII\Support\Facades\Navigation;
use Carbon\Carbon;
use FireflyIII\Enums\TransactionTypeEnum;
use FireflyIII\Exceptions\FireflyException;
use FireflyIII\Factory\BillFactory;
use FireflyIII\Models\Attachment;
@@ -34,12 +34,14 @@ use FireflyIII\Models\ObjectGroup;
use FireflyIII\Models\Rule;
use FireflyIII\Models\Transaction;
use FireflyIII\Models\TransactionJournal;
use FireflyIII\Models\TransactionType;
use FireflyIII\Repositories\Journal\JournalRepositoryInterface;
use FireflyIII\Repositories\ObjectGroup\CreatesObjectGroups;
use FireflyIII\Services\Internal\Destroy\BillDestroyService;
use FireflyIII\Services\Internal\Update\BillUpdateService;
use FireflyIII\Support\CacheProperties;
use FireflyIII\Support\Facades\Amount;
use FireflyIII\Support\Facades\Navigation;
use FireflyIII\Support\Repositories\UserGroup\UserGroupInterface;
use FireflyIII\Support\Repositories\UserGroup\UserGroupTrait;
use Illuminate\Database\Query\JoinClause;
@@ -48,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.
@@ -244,7 +247,7 @@ class BillRepository implements BillRepositoryInterface, UserGroupInterface
/** @var null|Note $note */
$note = $bill->notes()->first();
return (string) $note?->text;
return (string)$note?->text;
}
public function getOverallAverage(Bill $bill): array
@@ -261,7 +264,7 @@ class BillRepository implements BillRepositoryInterface, UserGroupInterface
foreach ($journals as $journal) {
/** @var Transaction $transaction */
$transaction = $journal->transactions()->where('amount', '<', 0)->first();
$currencyId = (int) $journal->transaction_currency_id;
$currencyId = (int)$journal->transaction_currency_id;
$currency = $journal->transactionCurrency;
$result[$currencyId] ??= [
'sum' => '0',
@@ -274,10 +277,10 @@ class BillRepository implements BillRepositoryInterface, UserGroupInterface
'currency_symbol' => $currency->symbol,
'currency_decimal_places' => $currency->decimal_places,
];
$result[$currencyId]['sum'] = bcadd($result[$currencyId]['sum'], (string) $transaction->amount);
$result[$currencyId]['sum'] = bcadd($result[$currencyId]['sum'], (string)$transaction->amount);
$result[$currencyId]['pc_sum'] = bcadd($result[$currencyId]['pc_sum'], $transaction->native_amount ?? '0');
if ($journal->foreign_currency_id === Amount::getPrimaryCurrency()->id) {
$result[$currencyId]['pc_sum'] = bcadd($result[$currencyId]['pc_sum'], (string) $transaction->amount);
$result[$currencyId]['pc_sum'] = bcadd($result[$currencyId]['pc_sum'], (string)$transaction->amount);
}
++$result[$currencyId]['count'];
}
@@ -288,8 +291,8 @@ class BillRepository implements BillRepositoryInterface, UserGroupInterface
* @var array $arr
*/
foreach ($result as $currencyId => $arr) {
$result[$currencyId]['avg'] = bcdiv((string) $arr['sum'], (string) $arr['count']);
$result[$currencyId]['pc_avg'] = bcdiv((string) $arr['pc_sum'], (string) $arr['count']);
$result[$currencyId]['avg'] = bcdiv((string)$arr['sum'], (string)$arr['count']);
$result[$currencyId]['pc_avg'] = bcdiv((string)$arr['pc_sum'], (string)$arr['count']);
}
return $result;
@@ -398,7 +401,7 @@ class BillRepository implements BillRepositoryInterface, UserGroupInterface
if (null === $transaction) {
continue;
}
$currencyId = (int) $journal->transaction_currency_id;
$currencyId = (int)$journal->transaction_currency_id;
$currency = $journal->transactionCurrency;
$result[$currencyId] ??= [
'sum' => '0',
@@ -410,10 +413,10 @@ class BillRepository implements BillRepositoryInterface, UserGroupInterface
'currency_symbol' => $currency->symbol,
'currency_decimal_places' => $currency->decimal_places,
];
$result[$currencyId]['sum'] = bcadd($result[$currencyId]['sum'], (string) $transaction->amount);
$result[$currencyId]['sum'] = bcadd($result[$currencyId]['sum'], (string)$transaction->amount);
$result[$currencyId]['pc_sum'] = bcadd($result[$currencyId]['pc_sum'], $transaction->native_amount ?? '0');
if ($journal->foreign_currency_id === Amount::getPrimaryCurrency()->id) {
$result[$currencyId]['pc_sum'] = bcadd($result[$currencyId]['pc_sum'], (string) $transaction->amount);
$result[$currencyId]['pc_sum'] = bcadd($result[$currencyId]['pc_sum'], (string)$transaction->amount);
}
++$result[$currencyId]['count'];
}
@@ -424,8 +427,8 @@ class BillRepository implements BillRepositoryInterface, UserGroupInterface
* @var array $arr
*/
foreach ($result as $currencyId => $arr) {
$result[$currencyId]['avg'] = bcdiv((string) $arr['sum'], (string) $arr['count']);
$result[$currencyId]['pc_avg'] = bcdiv((string) $arr['pc_sum'], (string) $arr['count']);
$result[$currencyId]['avg'] = bcdiv((string)$arr['sum'], (string)$arr['count']);
$result[$currencyId]['pc_avg'] = bcdiv((string)$arr['pc_sum'], (string)$arr['count']);
}
return $result;
@@ -438,7 +441,7 @@ class BillRepository implements BillRepositoryInterface, UserGroupInterface
{
/** @var Transaction $transaction */
foreach ($transactions as $transaction) {
$journal = $bill->user->transactionJournals()->find((int) $transaction['transaction_journal_id']);
$journal = $bill->user->transactionJournals()->find((int)$transaction['transaction_journal_id']);
$journal->bill_id = $bill->id;
$journal->save();
Log::debug(sprintf('Linked journal #%d to bill #%d', $journal->id, $bill->id));
@@ -544,8 +547,8 @@ class BillRepository implements BillRepositoryInterface, UserGroupInterface
/** @var Collection $set */
$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,
$return[(int)$currency->id] ??= [
'id' => (string)$currency->id,
'name' => $currency->name,
'symbol' => $currency->symbol,
'code' => $currency->code,
@@ -557,9 +560,9 @@ class BillRepository implements BillRepositoryInterface, UserGroupInterface
/** @var TransactionJournal $transactionJournal */
foreach ($set as $transactionJournal) {
// grab currency from transaction.
$transactionCurrency = $transactionJournal->transactionCurrency;
$return[(int) $transactionCurrency->id] ??= [
'id' => (string) $transactionCurrency->id,
$transactionCurrency = $transactionJournal->transactionCurrency;
$return[(int)$transactionCurrency->id] ??= [
'id' => (string)$transactionCurrency->id,
'name' => $transactionCurrency->name,
'symbol' => $transactionCurrency->symbol,
'code' => $transactionCurrency->code,
@@ -568,7 +571,7 @@ class BillRepository implements BillRepositoryInterface, UserGroupInterface
];
// get currency from transaction as well.
$return[(int) $transactionCurrency->id]['sum'] = bcadd($return[(int) $transactionCurrency->id]['sum'], Amount::getAmountFromJournalObject($transactionJournal));
$return[(int)$transactionCurrency->id]['sum'] = bcadd($return[(int)$transactionCurrency->id]['sum'], Amount::getAmountFromJournalObject($transactionJournal));
// $setAmount = bcadd($setAmount, Amount::getAmountFromJournalObject($transactionJournal));
}
// Log::debug(sprintf('Bill #%d ("%s") with %d transaction(s) and sum %s %s', $bill->id, $bill->name, $set->count(), $currency->code, $setAmount));
@@ -622,14 +625,14 @@ class BillRepository implements BillRepositoryInterface, UserGroupInterface
$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] ??= [
'id' => (string) $currency->id,
'id' => (string)$currency->id,
'name' => $currency->name,
'symbol' => $currency->symbol,
'code' => $currency->code,
'decimal_places' => $currency->decimal_places,
'sum' => '0',
];
$return[$currency->id]['sum'] = bcadd($return[$currency->id]['sum'], bcmul($average, (string) $total));
$return[$currency->id]['sum'] = bcadd($return[$currency->id]['sum'], bcmul($average, (string)$total));
}
}
@@ -704,4 +707,20 @@ class BillRepository implements BillRepositoryInterface, UserGroupInterface
return $service->update($bill, $data);
}
#[Override]
public function correctTransfers(): void
{
/** @var null|TransactionType $withdrawal */
$withdrawal = TransactionType::where('type', TransactionTypeEnum::WITHDRAWAL->value)->first();
if (null === $withdrawal) {
return;
}
$this->user
->transactionJournals()
->whereNotNull('bill_id')
->where('transaction_type_id', '!=', $withdrawal->id)
->update(['bill_id' => null])
;
}
}

View File

@@ -54,6 +54,8 @@ interface BillRepositoryInterface
*/
public function correctOrder(): void;
public function correctTransfers(): void;
public function destroy(Bill $bill): bool;
public function destroyAll(): void;

View File

@@ -55,12 +55,12 @@ class ExchangeRateRepository implements ExchangeRateRepositoryInterface, UserGro
// orderBy('date', 'DESC')->toRawSql();
return
$this->userGroup->currencyExchangeRates()
->where(function (Builder $q1) use ($from, $to): void {
$q1->where(function (Builder $q) use ($from, $to): void {
->where(static function (Builder $q1) use ($from, $to): void {
$q1->where(static function (Builder $q) use ($from, $to): void {
$q->where('from_currency_id', $from->id)
->where('to_currency_id', $to->id)
;
})->orWhere(function (Builder $q) use ($from, $to): void {
})->orWhere(static function (Builder $q) use ($from, $to): void {
$q->where('from_currency_id', $to->id)
->where('to_currency_id', $from->id)
;

View File

@@ -39,6 +39,7 @@ use Illuminate\Database\QueryException;
use Illuminate\Support\Collection;
use Illuminate\Support\Str;
use Override;
use SensitiveParameter;
/**
* Class UserRepository.
@@ -74,7 +75,7 @@ class UserRepository implements UserRepositoryInterface
return true;
}
public function changePassword(User $user, string $password): bool
public function changePassword(User $user, #[SensitiveParameter] string $password): bool
{
$user->password = bcrypt($password);
$user->save();

View File

@@ -30,6 +30,7 @@ use FireflyIII\Models\UserGroup;
use FireflyIII\User;
use Illuminate\Contracts\Auth\Authenticatable;
use Illuminate\Support\Collection;
use SensitiveParameter;
/**
* Interface UserRepositoryInterface.
@@ -64,7 +65,7 @@ interface UserRepositoryInterface
/**
* @return mixed
*/
public function changePassword(User $user, string $password);
public function changePassword(User $user, #[SensitiveParameter] string $password);
public function changeStatus(User $user, bool $isBlocked, string $code): bool;

View File

@@ -27,6 +27,7 @@ use Illuminate\Support\Facades\Log;
use GuzzleHttp\Client;
use GuzzleHttp\Exception\GuzzleException;
use GuzzleHttp\Exception\RequestException;
use SensitiveParameter;
/**
* Class PwndVerifierV2.
@@ -36,7 +37,7 @@ class PwndVerifierV2 implements Verifier
/**
* Verify the given password against (some) service.
*/
public function validPassword(string $password): bool
public function validPassword(#[SensitiveParameter] string $password): bool
{
// Yes SHA1 is unsafe but in this context its fine.
$hash = sha1($password);

View File

@@ -23,6 +23,8 @@ declare(strict_types=1);
namespace FireflyIII\Services\Password;
use SensitiveParameter;
/**
* Interface Verifier.
*/
@@ -31,5 +33,5 @@ interface Verifier
/**
* Verify the given password against (some) service.
*/
public function validPassword(string $password): bool;
public function validPassword(#[SensitiveParameter] string $password): bool;
}

View File

@@ -33,6 +33,7 @@ use Illuminate\Contracts\Auth\Authenticatable;
use Illuminate\Contracts\Auth\UserProvider;
use Illuminate\Support\Str;
use Override;
use SensitiveParameter;
/**
* Class RemoteUserProvider
@@ -100,7 +101,7 @@ class RemoteUserProvider implements UserProvider
*
* @SuppressWarnings("PHPMD.UnusedFormalParameter")
*/
public function retrieveByToken($identifier, $token): ?Authenticatable
public function retrieveByToken($identifier, #[SensitiveParameter] $token): ?Authenticatable
{
Log::debug(sprintf('Now at %s', __METHOD__));
@@ -114,7 +115,7 @@ class RemoteUserProvider implements UserProvider
*
* @throws FireflyException
*/
public function updateRememberToken(Authenticatable $user, $token): void
public function updateRememberToken(Authenticatable $user, #[SensitiveParameter] $token): void
{
Log::debug(sprintf('Now at %s', __METHOD__));

View File

@@ -331,7 +331,7 @@ trait PeriodOverview
}
return $this->statistics->filter(
fn (PeriodStatistic $statistic): bool => $statistic->start->eq($start) && $statistic->end->eq($end) && $statistic->type === $type
static fn (PeriodStatistic $statistic): bool => $statistic->start->eq($start) && $statistic->end->eq($end) && $statistic->type === $type
);
}
@@ -344,7 +344,7 @@ trait PeriodOverview
}
return $this->statistics->filter(
fn (PeriodStatistic $statistic): bool => $statistic->start->eq($start) && $statistic->end->eq($end) && str_starts_with($statistic->type, $prefix)
static fn (PeriodStatistic $statistic): bool => $statistic->start->eq($start) && $statistic->end->eq($end) && str_starts_with($statistic->type, $prefix)
);
}

View File

@@ -24,6 +24,7 @@ declare(strict_types=1);
namespace FireflyIII\Support\Http\Controllers;
use FireflyIII\Models\Tag;
use Illuminate\Support\Facades\Log;
use FireflyIII\Enums\AccountTypeEnum;
use FireflyIII\Exceptions\FireflyException;
@@ -416,8 +417,21 @@ trait RenderPartialViews
$repository = app(TagRepositoryInterface::class);
$tags = $repository->get();
$grouped = [];
/** @var Tag $tag */
foreach ($tags as $tag) {
$year = (int) $tag->date?->year;
$grouped[$year] ??= [
'tags' => [],
'year' => 0 === $year ? trans('firefly.no_date') : $year,
];
$grouped[$year]['tags'][] = $tag;
}
ksort($grouped);
try {
$result = view('reports.options.tag', ['tags' => $tags])->render();
$result = view('reports.options.tag', ['tags' => $grouped])->render();
} catch (Throwable $e) {
Log::error(sprintf('Cannot render reports.options.tag: %s', $e->getMessage()));
$result = 'Could not render view.';

View File

@@ -179,7 +179,7 @@ class BudgetLimitEnrichment implements EnrichmentInterface
private function filterToBudget(array $expenses, int $budget): array
{
$result = array_filter($expenses, fn (array $item): bool => (int)$item['budget_id'] === $budget);
$result = array_filter($expenses, static fn (array $item): bool => (int)$item['budget_id'] === $budget);
Log::debug(sprintf('filterToBudget for budget #%d, from %d to %d items', $budget, count($expenses), count($result)));
return $result;
@@ -187,13 +187,13 @@ class BudgetLimitEnrichment implements EnrichmentInterface
private function stringifyIds(): void
{
$this->expenses = array_map(fn ($first): array => array_map(function (array $second): array {
$this->expenses = array_map(static fn ($first): array => array_map(static function (array $second): array {
$second['currency_id'] = (string)($second['currency_id'] ?? 0);
return $second;
}, $first), $this->expenses);
$this->pcExpenses = array_map(fn (array $first): array => array_map(function (array $second): array {
$this->pcExpenses = array_map(static fn (array $first): array => array_map(static function (array $second): array {
$second['currency_id'] ??= 0;
return $second;

View File

@@ -339,7 +339,7 @@ class RecurringEnrichment implements EnrichmentInterface
/** @var RecurrenceRepetition $repetition */
foreach ($set as $repetition) {
$recurrence = $this->collection->filter(fn (Recurrence $item): bool => (int)$item->id === (int)$repetition->recurrence_id)->first();
$recurrence = $this->collection->filter(static fn (Recurrence $item): bool => (int)$item->id === (int)$repetition->recurrence_id)->first();
$fromDate = clone ($recurrence->latest_date ?? $recurrence->first_date);
$recurrenceId = (int)$repetition->recurrence_id;
$repId = (int)$repetition->id;

View File

@@ -173,7 +173,7 @@ class SubscriptionEnrichment implements EnrichmentInterface
*/
protected function lastPaidDate(Bill $subscription, Collection $dates, Carbon $default): Carbon
{
$filtered = $dates->filter(fn (TransactionJournal $journal): bool => (int)$journal->bill_id === (int)$subscription->id);
$filtered = $dates->filter(static fn (TransactionJournal $journal): bool => (int)$journal->bill_id === (int)$subscription->id);
Log::debug(sprintf('Filtered down from %d to %d entries for bill #%d.', $dates->count(), $filtered->count(), $subscription->id));
if (0 === $filtered->count()) {
return $default;
@@ -294,7 +294,7 @@ class SubscriptionEnrichment implements EnrichmentInterface
// At this point the "next match" is exactly after the last time the bill was paid.
$result = [];
$filtered = $set->filter(fn (TransactionJournal $journal): bool => (int)$journal->bill_id === (int)$subscription->id);
$filtered = $set->filter(static fn (TransactionJournal $journal): bool => (int)$journal->bill_id === (int)$subscription->id);
foreach ($filtered as $entry) {
$array = [
'transaction_group_id' => (string)$entry->transaction_group_id,
@@ -385,7 +385,7 @@ class SubscriptionEnrichment implements EnrichmentInterface
private function filterPaidDates(array $entries): array
{
return array_map(function (array $entry): array {
return array_map(static function (array $entry): array {
unset($entry['date_object']);
return $entry;

View File

@@ -202,7 +202,6 @@ class Navigation
public function endOfPeriod(Carbon $end, string $repeatFreq): Carbon
{
$currentEnd = clone $end;
// Log::debug(sprintf('Now in endOfPeriod("%s", "%s").', $currentEnd->toIso8601String(), $repeatFreq));
if ('MTD' === $repeatFreq && $end->isFuture()) {
// fall back to a monthly schedule if the requested period is MTD.
@@ -325,6 +324,7 @@ class Navigation
}
unset($result);
if (!array_key_exists($repeatFreq, $functionMap)) {
Log::error(sprintf('Cannot do endOfPeriod for $repeat_freq "%s"', $repeatFreq));

View File

@@ -49,7 +49,7 @@ class Preferences
return Preference::where('user_id', $user->id)
->where('name', '!=', 'currencyPreference')
->where(function (Builder $q) use ($user): void {
->where(static function (Builder $q) use ($user): void {
$q->whereNull('user_group_id');
$q->orWhere('user_group_id', $user->user_group_id);
})
@@ -108,7 +108,7 @@ class Preferences
{
$result = [];
$preferences = Preference::where('user_id', $user->id)
->where(function (Builder $q) use ($user): void {
->where(static function (Builder $q) use ($user): void {
$q->whereNull('user_group_id');
$q->orWhere('user_group_id', $user->user_group_id);
})

View File

@@ -32,9 +32,9 @@ use Illuminate\Support\Facades\Log;
class TransactionSummarizer
{
private bool $convertToPrimary = false;
private bool $convertToPrimary = false;
private TransactionCurrency $default;
private User $user;
private User $user;
public function __construct(?User $user = null)
{
@@ -51,7 +51,7 @@ class TransactionSummarizer
$field = 'amount';
// grab default currency information.
$currencyId = (int)$journal['currency_id'];
$currencyId = (int) $journal['currency_id'];
$currencyName = $journal['currency_name'];
$currencySymbol = $journal['currency_symbol'];
$currencyCode = $journal['currency_code'];
@@ -67,8 +67,8 @@ class TransactionSummarizer
if ($this->convertToPrimary) {
// Log::debug('convertToPrimary is true.');
// if convert to primary currency, use the primary currency amount yes or no?
$usePrimary = $this->default->id !== (int)$journal['currency_id'];
$useForeign = $this->default->id === (int)$journal['foreign_currency_id'];
$usePrimary = $this->default->id !== (int) $journal['currency_id'];
$useForeign = $this->default->id === (int) $journal['foreign_currency_id'];
if ($usePrimary) {
// Log::debug(sprintf('Journal #%d switches to primary currency amount (original is %s)', $journal['transaction_journal_id'], $journal['currency_code']));
$field = 'pc_amount';
@@ -81,7 +81,7 @@ class TransactionSummarizer
if ($useForeign) {
// Log::debug(sprintf('Journal #%d switches to foreign amount (foreign is %s)', $journal['transaction_journal_id'], $journal['foreign_currency_code']));
$field = 'foreign_amount';
$currencyId = (int)$journal['foreign_currency_id'];
$currencyId = (int) $journal['foreign_currency_id'];
$currencyName = $journal['foreign_currency_name'];
$currencySymbol = $journal['foreign_currency_symbol'];
$currencyCode = $journal['foreign_currency_code'];
@@ -91,9 +91,13 @@ class TransactionSummarizer
if (!$this->convertToPrimary) {
// Log::debug('convertToPrimary is false.');
// use foreign amount?
$foreignCurrencyId = (int)$journal['foreign_currency_id'];
$foreignCurrencyId = (int) $journal['foreign_currency_id'];
if (0 !== $foreignCurrencyId) {
Log::debug(sprintf('Journal #%d also includes foreign amount (foreign is "%s")', $journal['transaction_journal_id'], $journal['foreign_currency_code']));
Log::debug(sprintf(
'Journal #%d also includes foreign amount (foreign is "%s")',
$journal['transaction_journal_id'],
$journal['foreign_currency_code']
));
$foreignCurrencyName = $journal['foreign_currency_name'];
$foreignCurrencySymbol = $journal['foreign_currency_symbol'];
$foreignCurrencyCode = $journal['foreign_currency_code'];
@@ -102,7 +106,7 @@ class TransactionSummarizer
}
// first process normal amount
$amount = (string)($journal[$field] ?? '0');
$amount = (string) ($journal[$field] ?? '0');
$array[$currencyId] ??= [
'sum' => '0',
'currency_id' => $currencyId,
@@ -121,7 +125,7 @@ class TransactionSummarizer
// then process foreign amount, if it exists.
if (0 !== $foreignCurrencyId && $includeForeign) {
$amount = (string)($journal['foreign_amount'] ?? '0');
$amount = (string) ($journal['foreign_amount'] ?? '0');
$array[$foreignCurrencyId] ??= [
'sum' => '0',
'currency_id' => $foreignCurrencyId,
@@ -149,14 +153,12 @@ class TransactionSummarizer
public function groupByDirection(array $journals, string $method, string $direction): array
{
$array = [];
$idKey = sprintf('%s_account_id', $direction);
$nameKey = sprintf('%s_account_name', $direction);
$convertToPrimary = Amount::convertToPrimary($this->user);
$primary = Amount::getPrimaryCurrencyByUserGroup($this->user->userGroup);
Log::debug(sprintf('groupByDirection(array, %s, %s).', $direction, $method));
foreach ($journals as $journal) {
// currency
@@ -193,13 +195,25 @@ class TransactionSummarizer
];
// add the data from the $field to the array.
$array[$key]['sum'] = bcadd($array[$key]['sum'], (string) Steam::{$method}((string)($journal[$field] ?? '0'))); // @phpstan-ignore-line
Log::debug(sprintf('Field for transaction #%d is "%s" (%s). Sum: %s', $journal['transaction_group_id'], $currencyCode, $field, $array[$key]['sum']));
$array[$key]['sum'] = bcadd($array[$key]['sum'], (string) Steam::{$method}((string) ($journal[$field] ?? '0'))); // @phpstan-ignore-line
Log::debug(sprintf(
'Field for transaction #%d is "%s" (%s). Sum: %s',
$journal['transaction_group_id'],
$currencyCode,
$field,
$array[$key]['sum']
));
// also do foreign amount, but only when convertToPrimary is false (otherwise we have it already)
// or when convertToPrimary is true and the foreign currency is ALSO not the default currency.
if ((!$convertToPrimary || $journal['foreign_currency_id'] !== $primary->id) && 0 !== (int)$journal['foreign_currency_id']) {
Log::debug(sprintf('Use foreign amount from transaction #%d: %s %s. Sum: %s', $journal['transaction_group_id'], $currencyCode, $journal['foreign_amount'], $array[$key]['sum']));
if ((!$convertToPrimary || $journal['foreign_currency_id'] !== $primary->id) && 0 !== (int) $journal['foreign_currency_id']) {
Log::debug(sprintf(
'Use foreign amount from transaction #%d: %s %s. Sum: %s',
$journal['transaction_group_id'],
$currencyCode,
$journal['foreign_amount'],
$array[$key]['sum']
));
$key = sprintf('%s-%s', $journal[$idKey], $journal['foreign_currency_id']);
$array[$key] ??= [
'id' => $journal[$idKey],
@@ -211,7 +225,7 @@ class TransactionSummarizer
'currency_code' => $journal['foreign_currency_code'],
'currency_decimal_places' => $journal['foreign_currency_decimal_places'],
];
$array[$key]['sum'] = bcadd($array[$key]['sum'], (string) Steam::{$method}((string)$journal['foreign_amount'])); // @phpstan-ignore-line
$array[$key]['sum'] = bcadd($array[$key]['sum'], (string) Steam::{$method}((string) $journal['foreign_amount'])); // @phpstan-ignore-line
}
}

View File

@@ -35,7 +35,7 @@ trait ValidatesWebhooks
public function withValidator(Validator $validator): void
{
$validator->after(
function (Validator $validator): void {
static function (Validator $validator): void {
Log::debug('Validating webhook');
if (count($validator->failed()) > 0) {
return;

View File

@@ -1245,9 +1245,9 @@ class OperatorQuerySearch implements SearchInterface
return false;
//
// all account related searches:
//
case 'account_is':
$this->searchAccount($value, SearchDirection::BOTH, StringPosition::IS);
@@ -1608,9 +1608,9 @@ class OperatorQuerySearch implements SearchInterface
break;
//
// cash account
//
case 'source_is_cash':
$account = $this->getCashAccount();
$this->collector->setSourceAccounts(new Collection()->push($account));
@@ -1647,9 +1647,9 @@ class OperatorQuerySearch implements SearchInterface
break;
//
// description
//
case 'description_starts':
$this->collector->descriptionStarts([$value]);
@@ -1690,9 +1690,9 @@ class OperatorQuerySearch implements SearchInterface
break;
//
// currency
//
case 'currency_is':
$currency = $this->findCurrency($value);
if ($currency instanceof TransactionCurrency) {
@@ -1741,9 +1741,9 @@ class OperatorQuerySearch implements SearchInterface
break;
//
// attachments
//
case 'has_attachments':
case '-has_no_attachments':
Log::debug('Set collector to filter on attachments.');
@@ -1758,7 +1758,7 @@ class OperatorQuerySearch implements SearchInterface
break;
//
// categories
case '-has_any_category':
case 'has_no_category':
@@ -1866,9 +1866,9 @@ class OperatorQuerySearch implements SearchInterface
break;
//
// budgets
//
case '-has_any_budget':
case 'has_no_budget':
$this->collector->withoutBudget();
@@ -1977,9 +1977,9 @@ class OperatorQuerySearch implements SearchInterface
break;
//
// bill
//
case '-has_any_bill':
case 'has_no_bill':
$this->collector->withoutBill();
@@ -2088,9 +2088,9 @@ class OperatorQuerySearch implements SearchInterface
break;
//
// tags
//
case '-has_any_tag':
case 'has_no_tag':
$this->collector->withoutTags();
@@ -2216,9 +2216,9 @@ class OperatorQuerySearch implements SearchInterface
break;
//
// notes
//
case 'notes_contains':
$this->collector->notesContain($value);
@@ -2281,9 +2281,9 @@ class OperatorQuerySearch implements SearchInterface
break;
//
// amount
//
case 'amount_is':
// strip comma's, make dots.
Log::debug(sprintf('Original value "%s"', $value));
@@ -2368,9 +2368,9 @@ class OperatorQuerySearch implements SearchInterface
break;
//
// transaction type
//
case 'transaction_type':
$this->collector->setTypes([ucfirst($value)]);
Log::debug(sprintf('Set "%s" using collector with value "%s"', $operator, $value));
@@ -2383,9 +2383,9 @@ class OperatorQuerySearch implements SearchInterface
break;
//
// dates
//
case '-date_on':
case 'date_on':
$range = $this->parseDateRange($operator, $value);
@@ -2581,9 +2581,9 @@ class OperatorQuerySearch implements SearchInterface
return false;
//
// external URL
//
case '-any_external_url':
case 'no_external_url':
$this->collector->withoutExternalUrl();
@@ -2648,9 +2648,9 @@ class OperatorQuerySearch implements SearchInterface
break;
//
// other fields
//
case 'external_id_is':
$this->collector->setExternalId($value);

View File

@@ -26,6 +26,7 @@ declare(strict_types=1);
namespace FireflyIII\Support\Search\QueryParser;
use Illuminate\Support\Facades\Log;
use SensitiveParameter;
/**
* Single-pass parser that processes query strings into structured nodes.
@@ -202,7 +203,7 @@ class QueryParser implements QueryParserInterface
return new NodeGroup($nodes, $prohibited);
}
private function createNode(string $token, string $fieldName, bool $prohibited): Node
private function createNode(#[SensitiveParameter] string $token, string $fieldName, bool $prohibited): Node
{
if ('' !== $fieldName) {
// OK dus hoe trim je \" correct?

View File

@@ -80,7 +80,7 @@ class Steam
$currency = $currencies[$account->id];
// second array
$accountSums = array_filter($arrayOfSums, fn (array $entry): bool => $entry['account_id'] === $account->id);
$accountSums = array_filter($arrayOfSums, static fn (array $entry): bool => $entry['account_id'] === $account->id);
if (0 === count($accountSums)) {
$result[$account->id] = $return;

View File

@@ -392,7 +392,7 @@ class General extends AbstractExtension
{
return new TwigFunction(
'phpdate',
static fn (string $str): string => date($str)
date(...)
);
}
@@ -400,9 +400,7 @@ class General extends AbstractExtension
{
return new TwigFunction(
'fireflyiiiconfig',
static function (string $string, mixed $default): mixed {
return FireflyConfig::get($string, $default)->data;
}
static fn (string $string, mixed $default): mixed => FireflyConfig::get($string, $default)->data
);
}
}

View File

@@ -54,7 +54,11 @@ class LinkToBill implements ActionInterface
$billName = $this->action->getValue($journal);
$bill = $repository->findByName($billName);
if (null !== $bill && TransactionTypeEnum::WITHDRAWAL->value === $journal['transaction_type_type']) {
/** @var TransactionJournal $object */
$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();
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));

View File

@@ -24,6 +24,8 @@ declare(strict_types=1);
namespace FireflyIII\TransactionRules\Expressions;
use FireflyIII\Exceptions\FireflyException;
use Illuminate\Support\Facades\Log;
use Symfony\Component\ExpressionLanguage\ExpressionLanguage;
use Symfony\Component\ExpressionLanguage\SyntaxError;
@@ -141,6 +143,11 @@ class ActionExpression
private function evaluateExpression(string $expr, array $journal): string
{
$result = $this->expressionLanguage->evaluate($expr, $journal);
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.');
}
return (string) $result;
}

View File

@@ -34,7 +34,7 @@ class ActionExpressionLanguageProvider implements ExpressionFunctionProviderInte
{
public function getFunctions(): array
{
$function = function ($arguments, $str): string {
$function = static function ($arguments, $str): string {
if (!is_string($str)) {
return (string) $str;
}

View File

@@ -425,9 +425,7 @@ class TransactionGroupTransformer extends AbstractTransformer
private function getSourceTransaction(TransactionJournal $journal): Transaction
{
$result = $journal->transactions->first(
static function (Transaction $transaction): bool {
return (float) $transaction->amount < 0; // lame but it works.
}
static fn (Transaction $transaction): bool => (float) $transaction->amount < 0
);
if (null === $result) {
throw new FireflyException(sprintf('Journal #%d unexpectedly has no source transaction.', $journal->id));
@@ -442,9 +440,7 @@ class TransactionGroupTransformer extends AbstractTransformer
private function getDestinationTransaction(TransactionJournal $journal): Transaction
{
$result = $journal->transactions->first(
static function (Transaction $transaction): bool {
return (float) $transaction->amount > 0; // lame but it works
}
static fn (Transaction $transaction): bool => (float) $transaction->amount > 0
);
if (null === $result) {
throw new FireflyException(sprintf('Journal #%d unexpectedly has no destination transaction.', $journal->id));

View File

@@ -24,9 +24,6 @@ declare(strict_types=1);
namespace FireflyIII;
use FireflyIII\Support\Facades\FireflyConfig;
use FireflyIII\Support\Facades\Preferences;
use Illuminate\Support\Facades\Log;
use Deprecated;
use Exception;
use FireflyIII\Enums\UserRoleEnum;
@@ -57,6 +54,8 @@ use FireflyIII\Models\UserRole;
use FireflyIII\Models\Webhook;
use FireflyIII\Notifications\Admin\UserRegistration;
use FireflyIII\Notifications\Admin\VersionCheckResult;
use FireflyIII\Support\Facades\FireflyConfig;
use FireflyIII\Support\Facades\Preferences;
use FireflyIII\Support\Models\ReturnsIntegerIdTrait;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
@@ -66,10 +65,12 @@ use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
use Illuminate\Notifications\Notification;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Request;
use Illuminate\Support\Str;
use Laravel\Passport\HasApiTokens;
use NotificationChannels\Pushover\PushoverReceiver;
use SensitiveParameter;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
class User extends Authenticatable
@@ -77,6 +78,7 @@ class User extends Authenticatable
use HasApiTokens;
use Notifiable;
use ReturnsIntegerIdTrait;
protected $fillable = ['email', 'password', 'blocked', 'blocked_code', 'user_group_id'];
protected $hidden = ['password', 'remember_token'];
protected $table = 'users';
@@ -258,7 +260,12 @@ class User extends Authenticatable
$dbRolesIds = $dbRoles->pluck('id')->toArray();
$dbRolesTitles = $dbRoles->pluck('title')->toArray();
$groupMemberships = $this->groupMemberships()->whereIn('user_role_id', $dbRolesIds)->where('user_group_id', $userGroup->id)->get();
$groupMemberships = $this
->groupMemberships()
->whereIn('user_role_id', $dbRolesIds)
->where('user_group_id', $userGroup->id)
->get()
;
if (0 === $groupMemberships->count()) {
Log::error(sprintf(
'User #%d "%s" does not have roles %s in user group #%d "%s"',
@@ -370,7 +377,7 @@ class User extends Authenticatable
return match ($driver) {
'mail' => $email,
default => null,
default => null
};
}
@@ -452,7 +459,7 @@ class User extends Authenticatable
*
* @param string $token
*/
public function sendPasswordResetNotification($token): void
public function sendPasswordResetNotification(#[SensitiveParameter] $token): void
{
$ipAddress = Request::ip();
@@ -528,10 +535,6 @@ class User extends Authenticatable
protected function casts(): array
{
return [
'created_at' => 'datetime',
'updated_at' => 'datetime',
'blocked' => 'boolean',
];
return ['created_at' => 'datetime', 'updated_at' => 'datetime', 'blocked' => 'boolean'];
}
}

View File

@@ -457,8 +457,9 @@ class FireflyValidator extends Validator
*
* @SuppressWarnings("PHPMD.UnusedFormalParameter")
*/
public function validateSecurePassword($attribute, string $value): bool
public function validateSecurePassword($attribute, ?string $value): bool
{
$value = (string)$value;
$verify = false;
if (array_key_exists('verify_password', $this->data)) {
$verify = 1 === (int) $this->data['verify_password'];

View File

@@ -3,6 +3,41 @@
All notable changes to this project will be documented in this file.
This project adheres to [Semantic Versioning](http://semver.org/).
## v6.4.15 - 2026-01-07
### Added
- [Issue 11264](https://github.com/firefly-iii/firefly-iii/issues/11264) (Add GUI for some settings, replacing environment variables) reported by @jacobburrell
- [Discussion 11433](https://github.com/orgs/firefly-iii/discussions/11433) (Updates to Date Range selection) started by @fett327
### Changed
- Moved some settings to your system settings
### Removed
- The following environment variables are removed and will no longer work. They are now in your settings.
- `ENABLE_EXTERNAL_MAP`
- `ENABLE_EXCHANGE_RATES`
- `ENABLE_EXTERNAL_RATES`
- `VALID_URL_PROTOCOLS`
- `ALLOW_WEBHOOKS`
- `USE_RUNNING_BALANCE`
- Removed sentry.io code
### Fixed
- [Issue 11378](https://github.com/firefly-iii/firefly-iii/issues/11378) (Wrong account balance with initial transfer from different currency) reported by @bozho
- [Issue 11383](https://github.com/firefly-iii/firefly-iii/issues/11383) (Login flow could redirect to javascript path) reported by @stefvonb
- [Issue 11388](https://github.com/firefly-iii/firefly-iii/issues/11388) (TypeError bugs during upgrade to 6.4.14 + account_balances corruption) reported by @jaconde2
- [Issue 11396](https://github.com/firefly-iii/firefly-iii/issues/11396) (Reconciliation adds extra digits) reported by @niklas2810
- [Issue 11399](https://github.com/firefly-iii/firefly-iii/issues/11399) (Unusual behavior in audit logs (multi-currency)) reported by @jgmm81
- [Issue 11403](https://github.com/firefly-iii/firefly-iii/issues/11403) (Error 404 when trying to view the details (Piggy banks section)) reported by @jgmm81
- [Issue 11410](https://github.com/firefly-iii/firefly-iii/issues/11410) (nitpick: Bulk edit tags should keep the option chosen instead of always changing back to "replace") reported by @jxtxzzw
- [Issue 11443](https://github.com/firefly-iii/firefly-iii/issues/11443) (Exception thrown, when subscription is in foreign currency) reported by @ajgon
- [Issue 11445](https://github.com/firefly-iii/firefly-iii/issues/11445) (“Reconcile” screen breaks when Preferences → Layout is set to “Year to date”) reported by @semonsir
- [Issue 11449](https://github.com/firefly-iii/firefly-iii/issues/11449) (Non-strict rules break with "Apply rule" and "Apply rule group") reported by @Bytenka
## v6.4.14 - 2025-12-17
### Fixed

62
composer.lock generated
View File

@@ -1878,16 +1878,16 @@
},
{
"name": "laravel/framework",
"version": "v12.44.0",
"version": "v12.47.0",
"source": {
"type": "git",
"url": "https://github.com/laravel/framework.git",
"reference": "592bbf1c036042958332eb98e3e8131b29102f33"
"reference": "ab8114c2e78f32e64eb238fc4b495bea3f8b80ec"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/laravel/framework/zipball/592bbf1c036042958332eb98e3e8131b29102f33",
"reference": "592bbf1c036042958332eb98e3e8131b29102f33",
"url": "https://api.github.com/repos/laravel/framework/zipball/ab8114c2e78f32e64eb238fc4b495bea3f8b80ec",
"reference": "ab8114c2e78f32e64eb238fc4b495bea3f8b80ec",
"shasum": ""
},
"require": {
@@ -2096,7 +2096,7 @@
"issues": "https://github.com/laravel/framework/issues",
"source": "https://github.com/laravel/framework"
},
"time": "2025-12-23T15:29:43+00:00"
"time": "2026-01-13T15:29:06+00:00"
},
{
"name": "laravel/passport",
@@ -2176,16 +2176,16 @@
},
{
"name": "laravel/prompts",
"version": "v0.3.8",
"version": "v0.3.9",
"source": {
"type": "git",
"url": "https://github.com/laravel/prompts.git",
"reference": "096748cdfb81988f60090bbb839ce3205ace0d35"
"reference": "5c41bf0555b7cfefaad4e66d3046675829581ac4"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/laravel/prompts/zipball/096748cdfb81988f60090bbb839ce3205ace0d35",
"reference": "096748cdfb81988f60090bbb839ce3205ace0d35",
"url": "https://api.github.com/repos/laravel/prompts/zipball/5c41bf0555b7cfefaad4e66d3046675829581ac4",
"reference": "5c41bf0555b7cfefaad4e66d3046675829581ac4",
"shasum": ""
},
"require": {
@@ -2229,22 +2229,22 @@
"description": "Add beautiful and user-friendly forms to your command-line applications.",
"support": {
"issues": "https://github.com/laravel/prompts/issues",
"source": "https://github.com/laravel/prompts/tree/v0.3.8"
"source": "https://github.com/laravel/prompts/tree/v0.3.9"
},
"time": "2025-11-21T20:52:52+00:00"
"time": "2026-01-07T21:00:29+00:00"
},
{
"name": "laravel/sanctum",
"version": "v4.2.1",
"version": "v4.2.3",
"source": {
"type": "git",
"url": "https://github.com/laravel/sanctum.git",
"reference": "f5fb373be39a246c74a060f2cf2ae2c2145b3664"
"reference": "47d26f1d310879ff757b971f5a6fc631d18663fd"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/laravel/sanctum/zipball/f5fb373be39a246c74a060f2cf2ae2c2145b3664",
"reference": "f5fb373be39a246c74a060f2cf2ae2c2145b3664",
"url": "https://api.github.com/repos/laravel/sanctum/zipball/47d26f1d310879ff757b971f5a6fc631d18663fd",
"reference": "47d26f1d310879ff757b971f5a6fc631d18663fd",
"shasum": ""
},
"require": {
@@ -2294,20 +2294,20 @@
"issues": "https://github.com/laravel/sanctum/issues",
"source": "https://github.com/laravel/sanctum"
},
"time": "2025-11-21T13:59:03+00:00"
"time": "2026-01-11T18:20:25+00:00"
},
{
"name": "laravel/serializable-closure",
"version": "v2.0.7",
"version": "v2.0.8",
"source": {
"type": "git",
"url": "https://github.com/laravel/serializable-closure.git",
"reference": "cb291e4c998ac50637c7eeb58189c14f5de5b9dd"
"reference": "7581a4407012f5f53365e11bafc520fd7f36bc9b"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/laravel/serializable-closure/zipball/cb291e4c998ac50637c7eeb58189c14f5de5b9dd",
"reference": "cb291e4c998ac50637c7eeb58189c14f5de5b9dd",
"url": "https://api.github.com/repos/laravel/serializable-closure/zipball/7581a4407012f5f53365e11bafc520fd7f36bc9b",
"reference": "7581a4407012f5f53365e11bafc520fd7f36bc9b",
"shasum": ""
},
"require": {
@@ -2355,7 +2355,7 @@
"issues": "https://github.com/laravel/serializable-closure/issues",
"source": "https://github.com/laravel/serializable-closure"
},
"time": "2025-11-21T20:52:36+00:00"
"time": "2026-01-08T16:22:46+00:00"
},
{
"name": "laravel/slack-notification-channel",
@@ -10081,12 +10081,12 @@
"version": "v3.16.3",
"source": {
"type": "git",
"url": "https://github.com/barryvdh/laravel-debugbar.git",
"url": "https://github.com/fruitcake/laravel-debugbar.git",
"reference": "c91e57ea113edd6526f5b8cd6b1c6ee02c67b28e"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/barryvdh/laravel-debugbar/zipball/c91e57ea113edd6526f5b8cd6b1c6ee02c67b28e",
"url": "https://api.github.com/repos/fruitcake/laravel-debugbar/zipball/c91e57ea113edd6526f5b8cd6b1c6ee02c67b28e",
"reference": "c91e57ea113edd6526f5b8cd6b1c6ee02c67b28e",
"shasum": ""
},
@@ -10146,8 +10146,8 @@
"webprofiler"
],
"support": {
"issues": "https://github.com/barryvdh/laravel-debugbar/issues",
"source": "https://github.com/barryvdh/laravel-debugbar/tree/v3.16.3"
"issues": "https://github.com/fruitcake/laravel-debugbar/issues",
"source": "https://github.com/fruitcake/laravel-debugbar/tree/v3.16.3"
},
"funding": [
{
@@ -11889,16 +11889,16 @@
},
{
"name": "rector/rector",
"version": "2.3.0",
"version": "2.3.1",
"source": {
"type": "git",
"url": "https://github.com/rectorphp/rector.git",
"reference": "f7166355dcf47482f27be59169b0825995f51c7d"
"reference": "9afc1bb43571b25629f353c61a9315b5ef31383a"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/rectorphp/rector/zipball/f7166355dcf47482f27be59169b0825995f51c7d",
"reference": "f7166355dcf47482f27be59169b0825995f51c7d",
"url": "https://api.github.com/repos/rectorphp/rector/zipball/9afc1bb43571b25629f353c61a9315b5ef31383a",
"reference": "9afc1bb43571b25629f353c61a9315b5ef31383a",
"shasum": ""
},
"require": {
@@ -11937,7 +11937,7 @@
],
"support": {
"issues": "https://github.com/rectorphp/rector/issues",
"source": "https://github.com/rectorphp/rector/tree/2.3.0"
"source": "https://github.com/rectorphp/rector/tree/2.3.1"
},
"funding": [
{
@@ -11945,7 +11945,7 @@
"type": "github"
}
],
"time": "2025-12-25T22:00:18+00:00"
"time": "2026-01-13T15:13:58+00:00"
},
{
"name": "sebastian/cli-parser",

View File

@@ -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' => 'develop/2026-01-05',
'build_time' => 1767598210,
'version' => 'develop/2026-01-13',
'build_time' => 1768333193,
'api_version' => '2.1.0', // field is no longer used.
'db_version' => 28, // field is no longer used.

View File

@@ -87,6 +87,7 @@ class TransactionCurrencySeeder extends Seeder
$currencies[] = ['code' => 'CZK', 'name' => 'Czech koruna', 'symbol' => 'Kč', 'decimal_places' => 2];
$currencies[] = ['code' => 'KZT', 'name' => 'Kazakhstani tenge', 'symbol' => '₸', 'decimal_places' => 2];
$currencies[] = ['code' => 'SAR', 'name' => 'Saudi Riyal', 'symbol' => 'SAR', 'decimal_places' => 2];
$currencies[] = ['code' => 'RSD', 'name' => 'Serbian Dinar', 'symbol' => 'RSD', 'decimal_places' => 2];
foreach ($currencies as $currency) {
if (null === TransactionCurrency::where('code', $currency['code'])->first()) {

View File

@@ -1,5 +1,8 @@
<?php
declare(strict_types=1);
/*
* WebhookDataSeeder.php

42
mago.toml Normal file
View File

@@ -0,0 +1,42 @@
# Welcome to Mago!
# For full documentation, see https://mago.carthage.software/tools/overview
php-version = "8.4.0"
[source]
workspace = "."
paths = ["app/", "database/factories/", "database/seeders/", "tests/"]
includes = ["vendor"]
excludes = []
[formatter]
print-width = 160
tab-width = 4
use-tabs = false
trailing-comma = false
method-chain-breaking-style = "same_line"
preserve-breaking-array-like = false
align-assignment-like = true
null-type-hint = "null_pipe"
[linter]
integrations = ["symfony", "laravel", "phpunit"]
[linter.rules]
ambiguous-function-call = { enabled = false }
literal-named-argument = { enabled = false }
halstead = { effort-threshold = 7000 }
prefer-early-continue = { enabled = false }
[analyzer]
find-unused-definitions = true
find-unused-expressions = true
analyze-dead-code = false
memoize-properties = true
allow-possibly-undefined-array-keys = true
check-throws = true
check-missing-override = false
find-unused-parameters = false
strict-list-index-checks = false
no-boolean-literal-comparison = false
check-missing-type-hints = false
register-super-globals = true

942
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -2,6 +2,8 @@
* Skin: Firefly III dark
* Built upon the code below with some extra things.
* https://raw.githubusercontent.com/anvyst/adminlte-skin-midnight/master/build/less/skins/skin-midnight.less
*
* To minify, just throw it in cyber chef.
* ------------
*/
.force-background-tags-input {
@@ -163,10 +165,17 @@
.skin-firefly-iii .ti-input {
border: 1px solid #353c42 !important;
}
.skin-firefly-iii code {
background-color: #343941;
color: #c9d1d9;
.skin-firefly-iii pre {
background-color: #1f2327;
border: 1px solid #3a4046;
}
.skin-firefly-iii code {
background-color:transparent;
color:#eee;
}
.skin-firefly-iii .modal-content {
position: relative;
background-color: #353c42;

File diff suppressed because one or more lines are too long

View File

@@ -1,6 +1,6 @@
/*
* Skin: Blue
* Slight changes for FF3
* Slight changes for FF3.
* ----------
*/
.skin-firefly-iii ::-moz-selection {

View File

@@ -68,8 +68,8 @@ export default {
props: ['source', 'destination', 'transactionType', 'value', 'error', 'no_currency', 'title',],
mounted() {
this.liability = false;
// console.log('I am mounted with a ' + this.transactionType + ' transaction type and currency id!');
// console.log(this.value);
console.log('ForeignAmountSelect is mounted with a ' + this.transactionType + ' transaction type and currency id!');
console.log(this.value);
this.loadCurrencies();
},
data() {
@@ -124,23 +124,25 @@ export default {
let sourceIsLiability = liabilities.indexOf(srcType) !== -1;
let destIsLiability = liabilities.indexOf(destType) !== -1;
// console.log(srcType + ' (source) is a liability: ' + sourceIsLiability);
// console.log(destType + ' (dest) is a liability: ' + destIsLiability);
// console.log('tType: ' + tType);
if (tType === 'transfer' || destIsLiability || sourceIsLiability) {
// console.log('Source is liability OR dest is liability, OR transfer. Lock list on currency of destination.');
console.log('Source or dest is a liability.')
console.log('Source is liability OR dest is liability, OR transfer. Lock list on currency of destination.');
// console.log('Length of currencies is ' + this.currencies.length);
// console.log(this.currencies);
this.liability = true;
// lock dropdown list on currencyID of destination.
// lock dropdown list on currencyID of destination UNLESS dest is not liab
for (const key in this.currencies) {
if (this.currencies.hasOwnProperty(key) && /^0$|^[1-9]\d*$/.test(key) && key <= 4294967294) {
if (
parseInt(this.currencies[key].id) === parseInt(this.destination.currency_id)
parseInt(this.currencies[key].id) === parseInt(this.destination.currency_id) || !destIsLiability
) {
// console.log('Enable currency!!');
console.log('Enable currency!!');
console.log(this.currencies[key]);
// console.log(this.destination);
// console.log(this.currencies[key]);
this.enabledCurrencies.push(this.currencies[key]);
}
}
@@ -151,6 +153,7 @@ export default {
// if type is withdrawal, list all but skip the source account ID.
if (tType === 'withdrawal' && this.source && false === sourceIsLiability) {
console.log('Type is withdrawal, there is a source, it is not a liability.')
for (const key in this.currencies) {
if (this.currencies.hasOwnProperty(key) && /^0$|^[1-9]\d*$/.test(key) && key <= 4294967294) {
if (this.source.currency_id !== this.currencies[key].id) {

View File

@@ -107,18 +107,18 @@
"multi_account_warning_withdrawal": "Pami\u0119taj, \u017ce konto \u017ar\u00f3d\u0142owe kolejnych podzia\u0142\u00f3w zostanie ustawione na konto zdefiniowane w pierwszym podziale wyp\u0142aty.",
"multi_account_warning_deposit": "Pami\u0119taj, \u017ce konto docelowe kolejnych podzia\u0142\u00f3w zostanie ustawione na konto zdefiniowane w pierwszym podziale wp\u0142aty.",
"multi_account_warning_transfer": "Pami\u0119taj, \u017ce konta \u017ar\u00f3d\u0142owe i docelowe kolejnych podzia\u0142\u00f3w zostan\u0105 ustawione na konto zdefiniowane w pierwszym podziale transferu.",
"webhook_trigger_ANY": "After any event",
"webhook_trigger_ANY": "Po ka\u017cdym wydarzeniu",
"webhook_trigger_STORE_TRANSACTION": "Po utworzeniu transakcji",
"webhook_trigger_UPDATE_TRANSACTION": "Po zmodyfikowaniu transakcji",
"webhook_trigger_DESTROY_TRANSACTION": "Po usuni\u0119ciu transakcji",
"webhook_trigger_STORE_BUDGET": "After budget creation",
"webhook_trigger_UPDATE_BUDGET": "After budget update",
"webhook_trigger_DESTROY_BUDGET": "After budget delete",
"webhook_trigger_STORE_UPDATE_BUDGET_LIMIT": "After budgeted amount change",
"webhook_trigger_STORE_BUDGET": "Po utworzeniu bud\u017cetu",
"webhook_trigger_UPDATE_BUDGET": "Po aktualizacji bud\u017cetu",
"webhook_trigger_DESTROY_BUDGET": "Po usuni\u0119ciu bud\u017cetu",
"webhook_trigger_STORE_UPDATE_BUDGET_LIMIT": "Po zmianie kwoty bud\u017cetu",
"webhook_response_TRANSACTIONS": "Szczeg\u00f3\u0142y transakcji",
"webhook_response_RELEVANT": "Relevant details",
"webhook_response_ACCOUNTS": "Szczeg\u00f3\u0142y konta",
"webhook_response_NONE": "No details",
"webhook_response_NONE": "Brak szczeg\u00f3\u0142\u00f3w",
"webhook_delivery_JSON": "JSON",
"actions": "Akcje",
"meta_data": "Metadane",

View File

@@ -3,8 +3,8 @@
"administrations_page_title": "Administra\u00e7\u00e3o financeira",
"administrations_index_menu": "Administra\u00e7\u00e3o financeira",
"expires_at": "Expira em",
"temp_administrations_introduction": "Firefly III will soon get the ability to manage multiple financial administrations. Right now, you only have the one. You can set the title of this administration and its primary currency. This replaces the previous setting where you would set your \"default currency\". This setting is now tied to the financial administration and can be different per administration.",
"administration_currency_form_help": "It may take a long time for the page to load if you change the primary currency because transaction may need to be converted to your (new) primary currency.",
"temp_administrations_introduction": "O Firefly III ter\u00e1 em breve a capacidade de gerir m\u00faltiplas administra\u00e7\u00f5es financeiras. Neste momento, voc\u00ea tem apenas um. Voc\u00ea pode definir o t\u00edtulo desta administra\u00e7\u00e3o e sua moeda prim\u00e1ria. Isso substitui a configura\u00e7\u00e3o anterior onde voc\u00ea iria definir sua \"moeda padr\u00e3o\". Esta defini\u00e7\u00e3o est\u00e1 agora ligada \u00e0 administra\u00e7\u00e3o financeira e pode ser diferente por administra\u00e7\u00e3o.",
"administration_currency_form_help": "Pode demorar muito tempo para a p\u00e1gina carregar, se voc\u00ea alterar a moeda principal, porque a transa\u00e7\u00e3o pode precisar ser convertida para a sua (nova) moeda principal.",
"administrations_page_edit_sub_title_js": "Editar administra\u00e7\u00e3o financeira \"{title}\"",
"table": "Tabela",
"welcome_back": "O que est\u00e1 acontecendo?",
@@ -102,23 +102,23 @@
"profile_oauth_client_secret_title": "Segredo do cliente",
"profile_oauth_client_secret_expl": "Aqui est\u00e1 o seu novo segredo de cliente. Esta \u00e9 a \u00fanica vez que ela ser\u00e1 mostrada, ent\u00e3o n\u00e3o o perca! Agora voc\u00ea pode usar este segredo para fazer requisi\u00e7\u00f5es de API.",
"profile_oauth_confidential": "Confidencial",
"profile_oauth_confidential_help": "Require the client to authenticate with a secret. Confidential clients can hold credentials in a secure way without exposing them to unauthorized parties. Public applications, such as native desktop or JavaScript SPA applications, are unable to hold secrets securely.",
"profile_oauth_confidential_help": "Exigir que o cliente se autentique com um segredo. Clientes confidenciais podem segurar credenciais de forma segura sem expor a partes n\u00e3o autorizadas. Aplica\u00e7\u00f5es p\u00fablicas, como aplica\u00e7\u00f5es de \u00e1rea de trabalho nativas ou JavaScript SPA, s\u00e3o incapazes de manter segredos com seguran\u00e7a.",
"multi_account_warning_unknown": "Dependendo do tipo de transa\u00e7\u00e3o que voc\u00ea criar, a conta de origem e\/ou de destino das divis\u00f5es subsequentes pode ser sobrescrita pelo que estiver definido na primeira divis\u00e3o da transa\u00e7\u00e3o.",
"multi_account_warning_withdrawal": "Tenha em mente que a conta de origem das divis\u00f5es subsequentes ser\u00e1 sobrescrita pelo que estiver definido na primeira divis\u00e3o da sa\u00edda.",
"multi_account_warning_deposit": "Tenha em mente que a conta de destino das divis\u00f5es subsequentes ser\u00e1 sobrescrita pelo que estiver definido na primeira divis\u00e3o da entrada.",
"multi_account_warning_transfer": "Tenha em mente que a conta de origem + de destino das divis\u00f5es subsequentes ser\u00e3o sobrescritas pelo que for definido na primeira divis\u00e3o da transfer\u00eancia.",
"webhook_trigger_ANY": "After any event",
"webhook_trigger_ANY": "Ap\u00f3s qualquer evento",
"webhook_trigger_STORE_TRANSACTION": "Ap\u00f3s cria\u00e7\u00e3o da transa\u00e7\u00e3o",
"webhook_trigger_UPDATE_TRANSACTION": "Ap\u00f3s atualiza\u00e7\u00e3o da transa\u00e7\u00e3o",
"webhook_trigger_DESTROY_TRANSACTION": "Ap\u00f3s exclus\u00e3o da transa\u00e7\u00e3o",
"webhook_trigger_STORE_BUDGET": "After budget creation",
"webhook_trigger_UPDATE_BUDGET": "After budget update",
"webhook_trigger_DESTROY_BUDGET": "After budget delete",
"webhook_trigger_STORE_UPDATE_BUDGET_LIMIT": "After budgeted amount change",
"webhook_trigger_STORE_BUDGET": "Ap\u00f3s cria\u00e7\u00e3o do or\u00e7amento",
"webhook_trigger_UPDATE_BUDGET": "Ap\u00f3s atualiza\u00e7\u00e3o do or\u00e7amento",
"webhook_trigger_DESTROY_BUDGET": "Ap\u00f3s exclus\u00e3o do or\u00e7amento",
"webhook_trigger_STORE_UPDATE_BUDGET_LIMIT": "Ap\u00f3s mudan\u00e7a de valor or\u00e7ado",
"webhook_response_TRANSACTIONS": "Detalhes da transa\u00e7\u00e3o",
"webhook_response_RELEVANT": "Relevant details",
"webhook_response_RELEVANT": "Detalhes relevantes",
"webhook_response_ACCOUNTS": "Detalhes da conta",
"webhook_response_NONE": "No details",
"webhook_response_NONE": "Sem detalhes",
"webhook_delivery_JSON": "JSON",
"actions": "A\u00e7\u00f5es",
"meta_data": "Meta dados",
@@ -160,7 +160,7 @@
"url": "URL",
"active": "Ativo",
"interest_date": "Data do juros",
"administration_currency": "Primary currency",
"administration_currency": "Moeda principal",
"title": "T\u00edtulo",
"date": "Data",
"book_date": "Data de lan\u00e7amento",
@@ -180,7 +180,7 @@
"list": {
"title": "T\u00edtulo",
"active": "Est\u00e1 ativo?",
"primary_currency": "Primary currency",
"primary_currency": "Moeda principal",
"trigger": "Gatilho",
"response": "Resposta",
"delivery": "Entrega",

View File

@@ -2480,6 +2480,7 @@ return [
'balanceFor' => 'Balance for :name',
'no_tags' => '(no tags)',
'nothing_found' => '(nothing found)',
'no_date' => '(no date)',
// page settings and wizard dialogs

View File

@@ -2,9 +2,13 @@
<label for="inputTags" class="col-sm-3 control-label">{{ 'select_tag'|_ }}</label>
<div class="col-sm-9">
<select id="inputTags" name="tag[]" multiple="multiple" class="form-control">
{% for tag in tags %}
<option value="{{ tag.id }}" label="{{ tag.tag|e('html') }}">{{ tag.tag|e('html') }}</option>
{% for year in tags %}
<optgroup label="{{ year.year }}">
{% for tag in year.tags %}
<option value="{{ tag.id }}" label="{{ tag.tag|e('html') }}">{{ tag.tag|e('html') }}</option>
{% endfor %}
{% endfor %}
</select>
</div>
</div>