Compare commits

..

73 Commits

Author SHA1 Message Date
github-actions[bot]
7f827fb277 Merge pull request #10995 from firefly-iii/release-1759464256
🤖 Automatically merge the PR into the develop branch.
2025-10-03 06:04:22 +02:00
JC5
8e700944fd 🤖 Auto commit for release 'develop' on 2025-10-03 2025-10-03 06:04:16 +02:00
James Cole
037a128942 Add missing translation. 2025-10-03 05:57:21 +02:00
James Cole
ca364dc877 Remove stats from empty objects. 2025-10-03 05:53:50 +02:00
James Cole
e55af7186c Fix #10994 2025-10-03 05:24:24 +02:00
James Cole
354bbebbee Fix #10965 2025-10-02 19:52:51 +02:00
James Cole
a70fab1e87 Clean up piggy alerts. 2025-10-02 19:49:32 +02:00
James Cole
b9c93091cd Merge pull request #10993 from ctrl-f5/fix/report-category-trans-key
use correct translation key for category report income table
2025-10-02 10:41:56 +02:00
Nicky De Maeyer
4349f8f303 Merge branch 'develop' into fix/report-category-trans-key 2025-10-02 10:31:35 +02:00
Nicky De Maeyer
5088e20f25 use correct translation key for category report income table 2025-10-02 10:25:11 +02:00
Sander Dorigo
3f81aa7403 Fix #10988 2025-10-02 10:15:18 +02:00
github-actions[bot]
8885d1dbeb Merge pull request #10992 from firefly-iii/release-1759383569
🤖 Automatically merge the PR into the develop branch.
2025-10-02 07:39:36 +02:00
JC5
a6ba75d528 🤖 Auto commit for release 'develop' on 2025-10-02 2025-10-02 07:39:29 +02:00
github-actions[bot]
667cbb1332 Merge pull request #10991 from firefly-iii/release-1759381485
🤖 Automatically merge the PR into the develop branch.
2025-10-02 07:04:53 +02:00
JC5
d1bae875f7 🤖 Auto commit for release 'develop' on 2025-10-02 2025-10-02 07:04:45 +02:00
James Cole
c5cf529413 Update changelog. 2025-10-02 06:59:56 +02:00
James Cole
62b9f2785f Ignore db version in scripts. 2025-10-02 06:55:09 +02:00
James Cole
3b85f87502 Stop using "db_version", check versions instead. 2025-10-02 06:53:23 +02:00
James Cole
87d3d14504 Fix #10990 2025-10-01 20:28:24 +02:00
github-actions[bot]
5637573fd0 Merge pull request #10982 from firefly-iii/release-1759116145
🤖 Automatically merge the PR into the develop branch.
2025-09-29 05:22:35 +02:00
JC5
48255ae6ee 🤖 Auto commit for release 'develop' on 2025-09-29 2025-09-29 05:22:25 +02:00
James Cole
ea02986170 Merge pull request #10979 from maksimkurb/patch-1
Add Kazakhstani Tenge (KZT) currency
2025-09-28 15:28:15 +02:00
mergify[bot]
f4fbc15ac6 Merge branch 'develop' into patch-1 2025-09-28 07:17:01 +00:00
Maxim Kurbatov
a02c4b42a4 Add Kazakhstani Tenge (KZT) currency
Signed-off-by: Maxim Kurbatov <maxim@kurb.me>
2025-09-28 11:49:37 +05:00
James Cole
1ff47441ce Also add no budget and no category overview. 2025-09-27 16:07:03 +02:00
James Cole
2cc8568077 Clean up code. 2025-09-27 06:40:56 +02:00
github-actions[bot]
408687ec6b Merge pull request #10975 from firefly-iii/release-1758945886
🤖 Automatically merge the PR into the develop branch.
2025-09-27 06:04:55 +02:00
JC5
308abffb0b 🤖 Auto commit for release 'develop' on 2025-09-27 2025-09-27 06:04:46 +02:00
James Cole
6743b3fe83 Remove stats for other objects as well. 2025-09-27 06:01:14 +02:00
James Cole
ac0113e445 Fix some rector code. 2025-09-27 05:57:58 +02:00
James Cole
0f0a28c3d9 Add cached periods for tags. 2025-09-27 05:49:22 +02:00
James Cole
79f2d70211 Fix #10974 2025-09-27 05:35:20 +02:00
James Cole
8a06c0f7ec Merge branch 'develop' of github.com:firefly-iii/firefly-iii into develop
# Conflicts:
#	app/Support/Http/Controllers/PeriodOverview.php
2025-09-27 05:32:41 +02:00
James Cole
c54da62005 Enable period statistics for category. 2025-09-27 05:31:26 +02:00
github-actions[bot]
e5923202af Merge pull request #10973 from firefly-iii/release-1758914746
🤖 Automatically merge the PR into the develop branch.
2025-09-26 21:25:55 +02:00
JC5
eb6f78406e 🤖 Auto commit for release 'develop' on 2025-09-26 2025-09-26 21:25:46 +02:00
James Cole
d61f87f649 Fix URL 2025-09-26 20:47:44 +02:00
James Cole
33dcce7525 Optimize query for collecting transactions. 2025-09-26 20:46:23 +02:00
James Cole
b1d86c3a37 Merge branch 'develop' of github.com:firefly-iii/firefly-iii into develop
# Conflicts:
#	app/Models/PeriodStatistic.php
2025-09-26 19:48:46 +02:00
James Cole
822dee6e70 Allow statistics to be removed from /flush 2025-09-26 19:48:20 +02:00
github-actions[bot]
f6dac83777 Merge pull request #10972 from firefly-iii/release-1758908619
🤖 Automatically merge the PR into the develop branch.
2025-09-26 19:43:52 +02:00
JC5
d3c557ca22 🤖 Auto commit for release 'develop' on 2025-09-26 2025-09-26 19:43:39 +02:00
James Cole
853a99852e Also remove them when updating transactions. 2025-09-26 19:39:18 +02:00
James Cole
8f24ac4fcd Remove statistics when creating a new journal. 2025-09-26 19:38:26 +02:00
James Cole
8b09cfb8c9 Optimize query for period statistics. 2025-09-26 19:32:53 +02:00
James Cole
18ae950d2e Optimize queries. 2025-09-26 06:09:44 +02:00
James Cole
69dfbda847 Add empty statistic if necessary. 2025-09-26 06:06:43 +02:00
James Cole
4ec2fcdb8a Optimize queries for statistics. 2025-09-26 06:05:37 +02:00
James Cole
08879d31ba Rearrange some code. 2025-09-26 05:33:35 +02:00
James Cole
66d09450d3 Merge branch 'develop' of github.com:firefly-iii/firefly-iii into develop 2025-09-25 19:17:20 +02:00
James Cole
74f7c07a76 Add filter for transfer type. 2025-09-25 19:17:10 +02:00
github-actions[bot]
ae3d0a3e49 Merge pull request #10964 from firefly-iii/release-1758820271
🤖 Automatically merge the PR into the develop branch.
2025-09-25 19:11:21 +02:00
JC5
61e8d7d7a2 🤖 Auto commit for release 'develop' on 2025-09-25 2025-09-25 19:11:11 +02:00
James Cole
62c5440605 Add table for period statistics, and see what happens re: performance. 2025-09-25 19:07:02 +02:00
James Cole
0aa90b9453 Fix #10960 2025-09-24 19:51:39 +02:00
Sander Dorigo
855bc2f8e7 attempted fix for #10956 2025-09-24 14:39:54 +02:00
github-actions[bot]
d8f05492c3 Merge pull request #10955 from firefly-iii/release-1758652896
🤖 Automatically merge the PR into the develop branch.
2025-09-23 20:41:46 +02:00
JC5
4a264f34fa 🤖 Auto commit for release 'develop' on 2025-09-23 2025-09-23 20:41:36 +02:00
James Cole
5a1413e758 Fix #10954 2025-09-23 20:22:58 +02:00
github-actions[bot]
84dbeeb0ce Merge pull request #10946 from firefly-iii/release-1758511378
🤖 Automatically merge the PR into the develop branch.
2025-09-22 05:23:04 +02:00
JC5
d868dc0945 🤖 Auto commit for release 'develop' on 2025-09-22 2025-09-22 05:22:58 +02:00
James Cole
beecf9c229 Make sure demo user cannot send notifications. 2025-09-21 18:00:23 +02:00
github-actions[bot]
e39ba46398 Merge pull request #10943 from firefly-iii/release-1758470243
🤖 Automatically merge the PR into the develop branch.
2025-09-21 17:57:31 +02:00
JC5
e6b6a3cee5 🤖 Auto commit for release 'develop' on 2025-09-21 2025-09-21 17:57:23 +02:00
James Cole
b5483f6ad3 Merge branch 'develop' of github.com:firefly-iii/firefly-iii into develop 2025-09-21 17:52:16 +02:00
James Cole
a751218d53 Fix rule order for #10938 2025-09-21 17:52:07 +02:00
github-actions[bot]
2af5e6eeef Merge pull request #10942 from firefly-iii/release-1758460508
🤖 Automatically merge the PR into the develop branch.
2025-09-21 15:15:17 +02:00
JC5
013c43f9f2 🤖 Auto commit for release 'develop' on 2025-09-21 2025-09-21 15:15:08 +02:00
James Cole
7e08a1f33c Possible fix for #10940, not sure. 2025-09-21 15:11:16 +02:00
James Cole
e592b56d7a Merge branch 'develop' of github.com:firefly-iii/firefly-iii into develop 2025-09-21 15:06:32 +02:00
github-actions[bot]
a2479f71fe Merge pull request #10937 from firefly-iii/release-1758437913
🤖 Automatically merge the PR into the develop branch.
2025-09-21 08:58:42 +02:00
JC5
7d3b993b98 🤖 Auto commit for release 'develop' on 2025-09-21 2025-09-21 08:58:33 +02:00
James Cole
90623101a3 Add earned + spent, needs cleaning up still. 2025-09-21 08:54:26 +02:00
153 changed files with 6833 additions and 6249 deletions

View File

@@ -402,16 +402,16 @@
},
{
"name": "friendsofphp/php-cs-fixer",
"version": "v3.87.2",
"version": "v3.88.2",
"source": {
"type": "git",
"url": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer.git",
"reference": "da5f0a7858c79b56fc0b8c36d3efcfe5f37f0992"
"reference": "a8d15584bafb0f0d9d938827840060fd4a3ebc99"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/PHP-CS-Fixer/PHP-CS-Fixer/zipball/da5f0a7858c79b56fc0b8c36d3efcfe5f37f0992",
"reference": "da5f0a7858c79b56fc0b8c36d3efcfe5f37f0992",
"url": "https://api.github.com/repos/PHP-CS-Fixer/PHP-CS-Fixer/zipball/a8d15584bafb0f0d9d938827840060fd4a3ebc99",
"reference": "a8d15584bafb0f0d9d938827840060fd4a3ebc99",
"shasum": ""
},
"require": {
@@ -438,12 +438,13 @@
"symfony/polyfill-mbstring": "^1.33",
"symfony/polyfill-php80": "^1.33",
"symfony/polyfill-php81": "^1.33",
"symfony/polyfill-php84": "^1.33",
"symfony/process": "^5.4.47 || ^6.4.24 || ^7.2",
"symfony/stopwatch": "^5.4.45 || ^6.4.24 || ^7.0"
},
"require-dev": {
"facile-it/paraunit": "^1.3.1 || ^2.7",
"infection/infection": "^0.29.14",
"infection/infection": "^0.31.0",
"justinrainbow/json-schema": "^6.5",
"keradus/cli-executor": "^2.2",
"mikey179/vfsstream": "^1.6.12",
@@ -451,7 +452,6 @@
"php-cs-fixer/phpunit-constraint-isidenticalstring": "^1.6",
"php-cs-fixer/phpunit-constraint-xmlmatchesxsd": "^1.6",
"phpunit/phpunit": "^9.6.25 || ^10.5.53 || ^11.5.34",
"symfony/polyfill-php84": "^1.33",
"symfony/var-dumper": "^5.4.48 || ^6.4.24 || ^7.3.2",
"symfony/yaml": "^5.4.45 || ^6.4.24 || ^7.3.2"
},
@@ -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.87.2"
"source": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/tree/v3.88.2"
},
"funding": [
{
@@ -502,7 +502,7 @@
"type": "github"
}
],
"time": "2025-09-10T09:51:40+00:00"
"time": "2025-09-27T00:24:15+00:00"
},
{
"name": "psr/container",
@@ -1252,16 +1252,16 @@
},
{
"name": "symfony/console",
"version": "v7.3.3",
"version": "v7.3.4",
"source": {
"type": "git",
"url": "https://github.com/symfony/console.git",
"reference": "cb0102a1c5ac3807cf3fdf8bea96007df7fdbea7"
"reference": "2b9c5fafbac0399a20a2e82429e2bd735dcfb7db"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/console/zipball/cb0102a1c5ac3807cf3fdf8bea96007df7fdbea7",
"reference": "cb0102a1c5ac3807cf3fdf8bea96007df7fdbea7",
"url": "https://api.github.com/repos/symfony/console/zipball/2b9c5fafbac0399a20a2e82429e2bd735dcfb7db",
"reference": "2b9c5fafbac0399a20a2e82429e2bd735dcfb7db",
"shasum": ""
},
"require": {
@@ -1326,7 +1326,7 @@
"terminal"
],
"support": {
"source": "https://github.com/symfony/console/tree/v7.3.3"
"source": "https://github.com/symfony/console/tree/v7.3.4"
},
"funding": [
{
@@ -1346,7 +1346,7 @@
"type": "tidelift"
}
],
"time": "2025-08-25T06:35:40+00:00"
"time": "2025-09-22T15:31:00+00:00"
},
{
"name": "symfony/deprecation-contracts",
@@ -2284,17 +2284,97 @@
"time": "2024-09-09T11:45:10+00:00"
},
{
"name": "symfony/process",
"version": "v7.3.3",
"name": "symfony/polyfill-php84",
"version": "v1.33.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/process.git",
"reference": "32241012d521e2e8a9d713adb0812bb773b907f1"
"url": "https://github.com/symfony/polyfill-php84.git",
"reference": "d8ced4d875142b6a7426000426b8abc631d6b191"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/process/zipball/32241012d521e2e8a9d713adb0812bb773b907f1",
"reference": "32241012d521e2e8a9d713adb0812bb773b907f1",
"url": "https://api.github.com/repos/symfony/polyfill-php84/zipball/d8ced4d875142b6a7426000426b8abc631d6b191",
"reference": "d8ced4d875142b6a7426000426b8abc631d6b191",
"shasum": ""
},
"require": {
"php": ">=7.2"
},
"type": "library",
"extra": {
"thanks": {
"url": "https://github.com/symfony/polyfill",
"name": "symfony/polyfill"
}
},
"autoload": {
"files": [
"bootstrap.php"
],
"psr-4": {
"Symfony\\Polyfill\\Php84\\": ""
},
"classmap": [
"Resources/stubs"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Nicolas Grekas",
"email": "p@tchwork.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Symfony polyfill backporting some PHP 8.4+ features to lower PHP versions",
"homepage": "https://symfony.com",
"keywords": [
"compatibility",
"polyfill",
"portable",
"shim"
],
"support": {
"source": "https://github.com/symfony/polyfill-php84/tree/v1.33.0"
},
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://github.com/nicolas-grekas",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2025-06-24T13:30:11+00:00"
},
{
"name": "symfony/process",
"version": "v7.3.4",
"source": {
"type": "git",
"url": "https://github.com/symfony/process.git",
"reference": "f24f8f316367b30810810d4eb30c543d7003ff3b"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/process/zipball/f24f8f316367b30810810d4eb30c543d7003ff3b",
"reference": "f24f8f316367b30810810d4eb30c543d7003ff3b",
"shasum": ""
},
"require": {
@@ -2326,7 +2406,7 @@
"description": "Executes commands in sub-processes",
"homepage": "https://symfony.com",
"support": {
"source": "https://github.com/symfony/process/tree/v7.3.3"
"source": "https://github.com/symfony/process/tree/v7.3.4"
},
"funding": [
{
@@ -2346,7 +2426,7 @@
"type": "tidelift"
}
],
"time": "2025-08-18T09:42:54+00:00"
"time": "2025-09-11T10:12:26+00:00"
},
{
"name": "symfony/service-contracts",
@@ -2495,16 +2575,16 @@
},
{
"name": "symfony/string",
"version": "v7.3.3",
"version": "v7.3.4",
"source": {
"type": "git",
"url": "https://github.com/symfony/string.git",
"reference": "17a426cce5fd1f0901fefa9b2a490d0038fd3c9c"
"reference": "f96476035142921000338bad71e5247fbc138872"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/string/zipball/17a426cce5fd1f0901fefa9b2a490d0038fd3c9c",
"reference": "17a426cce5fd1f0901fefa9b2a490d0038fd3c9c",
"url": "https://api.github.com/repos/symfony/string/zipball/f96476035142921000338bad71e5247fbc138872",
"reference": "f96476035142921000338bad71e5247fbc138872",
"shasum": ""
},
"require": {
@@ -2519,7 +2599,6 @@
},
"require-dev": {
"symfony/emoji": "^7.1",
"symfony/error-handler": "^6.4|^7.0",
"symfony/http-client": "^6.4|^7.0",
"symfony/intl": "^6.4|^7.0",
"symfony/translation-contracts": "^2.5|^3.0",
@@ -2562,7 +2641,7 @@
"utf8"
],
"support": {
"source": "https://github.com/symfony/string/tree/v7.3.3"
"source": "https://github.com/symfony/string/tree/v7.3.4"
},
"funding": [
{
@@ -2582,7 +2661,7 @@
"type": "tidelift"
}
],
"time": "2025-08-25T06:35:40+00:00"
"time": "2025-09-11T14:36:48+00:00"
}
],
"packages-dev": [],

View File

@@ -4,6 +4,7 @@ Over time, many people have contributed to Firefly III. Their efforts are not al
Please find below all the people who contributed to the Firefly III code. Their names are mentioned in the year of their first contribution.
## 2025
- Nicky De Maeyer
- Denis Iskandarov
- =
- Lompi

View File

@@ -28,8 +28,10 @@ use FireflyIII\Api\V1\Controllers\Controller;
use FireflyIII\Api\V1\Requests\Autocomplete\AutocompleteRequest;
use FireflyIII\Enums\UserRoleEnum;
use FireflyIII\Models\PiggyBank;
use FireflyIII\Models\TransactionCurrency;
use FireflyIII\Repositories\Account\AccountRepositoryInterface;
use FireflyIII\Repositories\PiggyBank\PiggyBankRepositoryInterface;
use FireflyIII\Support\Facades\Amount;
use Illuminate\Http\JsonResponse;
/**
@@ -96,6 +98,7 @@ class PiggyBankController extends Controller
/** @var PiggyBank $piggy */
foreach ($piggies as $piggy) {
/** @var TransactionCurrency $currency */
$currency = $piggy->transactionCurrency;
$currentAmount = $this->piggyRepository->getCurrentAmount($piggy);
$objectGroup = $piggy->objectGroups()->first();
@@ -105,8 +108,8 @@ class PiggyBankController extends Controller
'name_with_balance' => sprintf(
'%s (%s / %s)',
$piggy->name,
app('amount')->formatAnything($currency, $currentAmount, false),
app('amount')->formatAnything($currency, $piggy->target_amount, false),
Amount::formatAnything($currency, $currentAmount, false),
Amount::formatAnything($currency, $piggy->target_amount, false),
),
'currency_id' => (string) $currency->id,
'currency_name' => $currency->name,

View File

@@ -72,7 +72,7 @@ class DestroyController extends Controller
public function destroySingleByDate(TransactionCurrency $from, TransactionCurrency $to, Carbon $date): JsonResponse
{
$exchangeRate = $this->repository->getSpecificRateOnDate($from, $to, $date);
if (null !== $exchangeRate) {
if ($exchangeRate instanceof CurrencyExchangeRate) {
$this->repository->deleteRate($exchangeRate);
}

View File

@@ -95,7 +95,7 @@ class ShowController extends Controller
$transformer->setParameters($this->parameters);
$exchangeRate = $this->repository->getSpecificRateOnDate($from, $to, $date);
if (null === $exchangeRate) {
if (!$exchangeRate instanceof CurrencyExchangeRate) {
throw new NotFoundHttpException();
}

View File

@@ -74,7 +74,7 @@ class UpdateController extends Controller
public function updateByDate(UpdateRequest $request, TransactionCurrency $from, TransactionCurrency $to, Carbon $date): JsonResponse
{
$exchangeRate = $this->repository->getSpecificRateOnDate($from, $to, $date);
if (null === $exchangeRate) {
if (!$exchangeRate instanceof CurrencyExchangeRate) {
throw new NotFoundHttpException();
}
$date = $request->getDate();

View File

@@ -68,10 +68,6 @@ class UpdateController extends Controller
$data = $request->getAll();
$piggyBank = $this->repository->update($piggyBank, $data);
if (array_key_exists('current_amount', $data) && '' !== $data['current_amount']) {
$this->repository->setCurrentAmount($piggyBank, $data['current_amount']);
}
// enrich
/** @var User $admin */
$admin = auth()->user();

View File

@@ -158,18 +158,23 @@ class ShowController extends Controller
Log::debug(sprintf('Now in triggerTransaction(%d, %d)', $webhook->id, $group->id));
Log::channel('audit')->info(sprintf('User triggers webhook #%d on transaction group #%d.', $webhook->id, $group->id));
/** @var MessageGeneratorInterface $engine */
$engine = app(MessageGeneratorInterface::class);
$engine->setUser(auth()->user());
// tell the generator which trigger it should look for
$engine->setTrigger(WebhookTrigger::tryFrom($webhook->trigger));
// tell the generator which objects to process
$engine->setObjects(new Collection()->push($group));
// set the webhook to trigger
$engine->setWebhooks(new Collection()->push($webhook));
// tell the generator to generate the messages
$engine->generateMessages();
/** @var \FireflyIII\Models\WebhookTrigger $trigger */
foreach ($webhook->webhookTriggers as $trigger) {
/** @var MessageGeneratorInterface $engine */
$engine = app(MessageGeneratorInterface::class);
$engine->setUser(auth()->user());
// tell the generator which trigger it should look for
$engine->setTrigger(WebhookTrigger::tryFrom((int)$trigger->key));
// tell the generator which objects to process
$engine->setObjects(new Collection()->push($group));
// set the webhook to trigger
$engine->setWebhooks(new Collection()->push($webhook));
// tell the generator to generate the messages
$engine->generateMessages();
}
// trigger event to send them:
Log::debug('send event RequestedSendWebhookMessages from ShowController::triggerTransaction()');

View File

@@ -44,8 +44,18 @@ class DateRequest extends FormRequest
*/
public function getAll(): array
{
$start = $this->getCarbonDate('start');
$end = $this->getCarbonDate('end');
$start = $this->getCarbonDate('start');
$end = $this->getCarbonDate('end');
if (null === $start) {
$start = now()->startOfMonth();
}
if (null === $end) {
$end = now()->endOfMonth();
}
// sanity check on dates:
[$start, $end] = $end < $start ? [$end, $start] : [$start, $end];
$start->startOfDay();
$end->endOfDay();
if ($start->diffInYears($end, true) > 5) {

View File

@@ -64,7 +64,7 @@ class UpdateRequest extends FormRequest
*/
public function getAll(): array
{
app('log')->debug(sprintf('Now in %s', __METHOD__));
Log::debug(sprintf('Now in %s', __METHOD__));
$this->integerFields = ['order', 'currency_id', 'foreign_currency_id', 'transaction_journal_id', 'source_id', 'destination_id', 'budget_id', 'category_id', 'bill_id', 'recurrence_id'];
$this->dateFields = ['date', 'interest_date', 'book_date', 'process_date', 'due_date', 'payment_date', 'invoice_date'];
$this->textareaFields = ['notes'];
@@ -97,7 +97,7 @@ class UpdateRequest extends FormRequest
*/
private function getTransactionData(): array
{
app('log')->debug(sprintf('Now in %s', __METHOD__));
Log::debug(sprintf('Now in %s', __METHOD__));
$return = [];
/** @var null|array $transactions */
@@ -181,7 +181,7 @@ class UpdateRequest extends FormRequest
private function getDateData(array $current, array $transaction): array
{
foreach ($this->dateFields as $fieldName) {
app('log')->debug(sprintf('Now at date field %s', $fieldName));
Log::debug(sprintf('Now at date field %s', $fieldName));
if (array_key_exists($fieldName, $transaction)) {
Log::debug(sprintf('New value: "%s"', $transaction[$fieldName]));
$current[$fieldName] = $this->dateFromValue((string) $transaction[$fieldName]);
@@ -247,7 +247,7 @@ class UpdateRequest extends FormRequest
*/
public function rules(): array
{
app('log')->debug(sprintf('Now in %s', __METHOD__));
Log::debug(sprintf('Now in %s', __METHOD__));
$validProtocols = config('firefly.valid_url_protocols');
return [
@@ -330,7 +330,7 @@ class UpdateRequest extends FormRequest
*/
public function withValidator(Validator $validator): void
{
app('log')->debug('Now in withValidator');
Log::debug('Now in withValidator');
/** @var TransactionGroup $transactionGroup */
$transactionGroup = $this->route()->parameter('transactionGroup');

View File

@@ -28,7 +28,6 @@ namespace FireflyIII\Casts;
use Carbon\Carbon;
use Illuminate\Contracts\Database\Eloquent\CastsAttributes;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\Log;
/**
* Class SeparateTimezoneCaster

View File

@@ -26,12 +26,14 @@ namespace FireflyIII\Console\Commands\System;
use Carbon\Carbon;
use FireflyIII\Support\System\GeneratesInstallationId;
use FireflyIII\Support\System\IsOldVersion;
use Illuminate\Console\Command;
use Random\RandomException;
class OutputsInstructions extends Command
{
use GeneratesInstallationId;
use IsOldVersion;
protected $description = 'Instructions in case of upgrade trouble.';
@@ -58,7 +60,7 @@ class OutputsInstructions extends Command
*/
private function updateInstructions(): void
{
$version = (string) config('firefly.version');
$version = (string)config('firefly.version');
/** @var array $config */
$config = config('upgrade.text.upgrade');
@@ -68,12 +70,12 @@ class OutputsInstructions extends Command
foreach (array_keys($config) as $compare) {
// if string starts with:
if (str_starts_with($version, $compare)) {
$text = (string) $config[$compare];
$text = (string)$config[$compare];
}
}
// validate some settings.
if ('' === $text && 'local' === (string) config('app.env')) {
if ('' === $text && 'local' === (string)config('app.env')) {
$text = 'Please set APP_ENV=production for a safer environment.';
}
@@ -191,7 +193,7 @@ class OutputsInstructions extends Command
*/
private function installInstructions(): void
{
$version = (string) config('firefly.version');
$version = (string)config('firefly.version');
/** @var array $config */
$config = config('upgrade.text.install');
@@ -201,12 +203,12 @@ class OutputsInstructions extends Command
foreach (array_keys($config) as $compare) {
// if string starts with:
if (str_starts_with($version, $compare)) {
$text = (string) $config[$compare];
$text = (string)$config[$compare];
}
}
// validate some settings.
if ('' === $text && 'local' === (string) config('app.env')) {
if ('' === $text && 'local' === (string)config('app.env')) {
$text = 'Please set APP_ENV=production for a safer environment.';
}
@@ -242,14 +244,15 @@ class OutputsInstructions extends Command
private function someQuote(): void
{
$lines = [
'Forgive yourself for not being at peace.',
'Doesn\'t look like anything to me.',
'Be proud of what you make.',
'Be there or forever wonder.',
'A year from now you will wish you had started today.',
$lines = [
'"Forgive yourself for not being at peace."',
'"Doesn\'t look like anything to me."',
'"Be proud of what you make."',
'"Be there or forever wonder."',
'"A year from now you will wish you had started today."',
'🇺🇦 Слава Україні!',
'🇺🇦 Slava Ukraini!',
];
$addQuotes = true;
// fuck the Russian aggression in Ukraine.
@@ -260,8 +263,7 @@ class OutputsInstructions extends Command
// going on, to allow that to happen.
if ('ru_RU' === config('firefly.default_language')) {
$addQuotes = false;
$lines = [
$lines = [
'🇺🇦 Слава Україні!',
'🇺🇦 Slava Ukraini!',
];
@@ -272,11 +274,6 @@ class OutputsInstructions extends Command
} catch (RandomException) {
$random = 0;
}
if ($addQuotes) {
$this->line(sprintf(' "%s"', $lines[$random]));
return;
}
$this->line(sprintf(' %s', $lines[$random]));
}

View File

@@ -25,6 +25,7 @@ declare(strict_types=1);
namespace FireflyIII\Console\Commands\System;
use FireflyIII\Console\Commands\ShowsFriendlyMessages;
use FireflyIII\Support\Facades\FireflyConfig;
use Illuminate\Console\Command;
class SetsLatestVersion extends Command
@@ -45,8 +46,7 @@ class SetsLatestVersion extends Command
return 0;
}
app('fireflyconfig')->set('db_version', config('firefly.db_version'));
app('fireflyconfig')->set('ff3_version', config('firefly.version'));
FireflyConfig::set('ff3_version', config('firefly.version'));
$this->friendlyInfo('Updated version.');
return 0;

View File

@@ -283,7 +283,7 @@ class ApplyRules extends Command
if (null !== $endString && '' !== $endString) {
$inputEnd = Carbon::createFromFormat('Y-m-d', $endString);
}
if (null === $inputEnd || null === $inputStart) {
if (!$inputEnd instanceof Carbon || null === $inputStart) {
Log::error('Could not parse start or end date in verifyInputDate().');
return;

View File

@@ -24,6 +24,7 @@ declare(strict_types=1);
namespace FireflyIII\Console\Commands\Upgrade;
use FireflyIII\Support\Facades\FireflyConfig;
use Illuminate\Support\Facades\Log;
use Safe\Exceptions\InfoException;
@@ -86,10 +87,8 @@ class UpgradesDatabase extends Command
$this->friendlyLine(sprintf('Now executing %s', $command));
$this->call($command, $args);
}
// set new DB version.
app('fireflyconfig')->set('db_version', (int) config('firefly.db_version'));
// index will set FF3 version.
app('fireflyconfig')->set('ff3_version', (string) config('firefly.version'));
FireflyConfig::set('ff3_version', (string) config('firefly.version'));
return 0;
}

View File

@@ -86,6 +86,7 @@ class GracefulNotFoundHandler extends ExceptionHandler
return $this->handleAttachment($request, $e);
case 'bills.show':
case 'subscriptions.show':
$request->session()->reflash();
return redirect(route('bills.index'));

View File

@@ -242,6 +242,7 @@ class PiggyBankFactory
}
}
}
Log::debug('Looping all accounts.');
/** @var array $info */
foreach ($accounts as $info) {
@@ -251,6 +252,7 @@ class PiggyBankFactory
continue;
}
Log::debug(sprintf('Working on account #%d', $account->id));
if (array_key_exists('current_amount', $info) && null !== $info['current_amount']) {
// an amount is set, first check out if there is a difference with the previous amount.
$previous = $toBeLinked[$account->id]['current_amount'] ?? '0';
@@ -258,22 +260,24 @@ class PiggyBankFactory
// create event for difference.
if (0 !== bccomp($diff, '0')) {
// 2025-10-01 for issue #10990 disable this event.
Log::debug(sprintf('[a] Will save event for difference %s (previous value was %s)', $diff, $previous));
event(new ChangedAmount($piggyBank, $diff, null, null));
// event(new ChangedAmount($piggyBank, $diff, null, null));
}
$toBeLinked[$account->id] = ['current_amount' => $info['current_amount']];
Log::debug(sprintf('[a] Will link account #%d with amount %s', $account->id, $info['current_amount']));
}
if (array_key_exists('current_amount', $info) && null === $info['current_amount']) {
// an amount is set, first check out if there is a difference with the previous amount.
// no amount is set, first check out if there is a difference with the previous amount.
$previous = $toBeLinked[$account->id]['current_amount'] ?? '0';
$diff = bcsub('0', $previous);
// create event for difference.
if (0 !== bccomp($diff, '0')) {
// 2025-10-01 for issue #10990 disable this event.
Log::debug(sprintf('[b] Will save event for difference %s (previous value was %s)', $diff, $previous));
event(new ChangedAmount($piggyBank, $diff, null, null));
// event(new ChangedAmount($piggyBank, $diff, null, null));
}
// no amount set, use previous amount or go to ZERO.
@@ -282,7 +286,8 @@ class PiggyBankFactory
// create event:
Log::debug('linkToAccountIds: Trigger change for positive amount [b].');
event(new ChangedAmount($piggyBank, $toBeLinked[$account->id]['current_amount'] ?? '0', null, null));
// 2025-10-01 for issue #10990 disable this event.
// event(new ChangedAmount($piggyBank, $toBeLinked[$account->id]['current_amount'] ?? '0', null, null));
}
if (!array_key_exists('current_amount', $info)) {
$toBeLinked[$account->id] ??= [];

View File

@@ -28,6 +28,7 @@ use FireflyIII\Events\RequestedSendWebhookMessages;
use FireflyIII\Events\StoredTransactionGroup;
use FireflyIII\Generator\Webhook\MessageGeneratorInterface;
use FireflyIII\Models\TransactionJournal;
use FireflyIII\Repositories\PeriodStatistic\PeriodStatisticRepositoryInterface;
use FireflyIII\Repositories\RuleGroup\RuleGroupRepositoryInterface;
use FireflyIII\Services\Internal\Support\CreditRecalculateService;
use FireflyIII\TransactionRules\Engine\RuleEngineInterface;
@@ -36,6 +37,8 @@ use Illuminate\Support\Facades\Log;
/**
* Class StoredGroupEventHandler
*
* TODO migrate to observer?
*/
class StoredGroupEventHandler
{
@@ -44,6 +47,7 @@ class StoredGroupEventHandler
$this->processRules($event);
$this->recalculateCredit($event);
$this->triggerWebhooks($event);
$this->removePeriodStatistics($event);
}
/**
@@ -94,6 +98,38 @@ class StoredGroupEventHandler
$object->recalculate();
}
private function removePeriodStatistics(StoredTransactionGroup $event): void
{
/** @var PeriodStatisticRepositoryInterface $repository */
$repository = app(PeriodStatisticRepositoryInterface::class);
/** @var TransactionJournal $journal */
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);
$categories = $journal->categories;
$tags = $journal->tags;
$budgets = $journal->budgets;
foreach ($categories as $category) {
$repository->deleteStatisticsForModel($category, $journal->date);
}
foreach ($tags as $tag) {
$repository->deleteStatisticsForModel($tag, $journal->date);
}
foreach ($budgets as $budget) {
$repository->deleteStatisticsForModel($budget, $journal->date);
}
if (0 === $categories->count()) {
$repository->deleteStatisticsForPrefix($journal->userGroup, 'no_category', $journal->date);
}
if (0 === $budgets->count()) {
$repository->deleteStatisticsForPrefix($journal->userGroup, 'no_budget', $journal->date);
}
}
}
/**
* This method processes all webhooks that respond to the "stored transaction group" trigger (100)
*/

View File

@@ -31,6 +31,7 @@ use FireflyIII\Generator\Webhook\MessageGeneratorInterface;
use FireflyIII\Models\Account;
use FireflyIII\Models\Transaction;
use FireflyIII\Models\TransactionJournal;
use FireflyIII\Repositories\PeriodStatistic\PeriodStatisticRepositoryInterface;
use FireflyIII\Repositories\RuleGroup\RuleGroupRepositoryInterface;
use FireflyIII\Services\Internal\Support\CreditRecalculateService;
use FireflyIII\Support\Models\AccountBalanceCalculator;
@@ -49,10 +50,35 @@ class UpdatedGroupEventHandler
$this->processRules($event);
$this->recalculateCredit($event);
$this->triggerWebhooks($event);
$this->removePeriodStatistics($event);
if ($event->runRecalculations) {
$this->updateRunningBalance($event);
}
}
/**
* TODO duplicate
*/
private function removePeriodStatistics(UpdatedTransactionGroup $event): void
{
/** @var PeriodStatisticRepositoryInterface $repository */
$repository = app(PeriodStatisticRepositoryInterface::class);
/** @var TransactionJournal $journal */
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);
foreach ($journal->categories as $category) {
$repository->deleteStatisticsForModel($category, $journal->date);
}
foreach ($journal->tags as $tag) {
$repository->deleteStatisticsForModel($tag, $journal->date);
}
}
}
/**

View File

@@ -62,5 +62,8 @@ class WebhookEventHandler
Log::debug(sprintf('Skip message #%d', $message->id));
}
}
// clean up sent messages table:
WebhookMessage::where('webhook_messages.sent', true)->where('webhook_messages.created_at', '<', now()->subDays(30))->delete();
}
}

View File

@@ -102,7 +102,7 @@ class ShowController extends Controller
// make sure dates are end of day and start of day:
$start->startOfDay();
$end->endOfDay();
$end->endOfDay()->milli(0);
$location = $this->repository->getLocation($account);
$attachments = $this->repository->getAttachments($account);

View File

@@ -122,6 +122,11 @@ class NotificationController extends Controller
public function testNotification(Request $request): RedirectResponse
{
if (true === auth()->user()->hasRole('demo')) {
session()->flash('error', (string) trans('firefly.not_available_demo_user'));
return redirect(route('settings.notification.index'));
}
$all = $request->all();
$channel = $all['test_submit'] ?? '';

View File

@@ -27,6 +27,7 @@ use Carbon\Carbon;
use FireflyIII\Helpers\Update\UpdateTrait;
use FireflyIII\Http\Controllers\Controller;
use FireflyIII\Http\Middleware\IsDemoUser;
use FireflyIII\Support\Facades\FireflyConfig;
use Illuminate\Contracts\View\Factory;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
@@ -66,8 +67,8 @@ class UpdateController extends Controller
{
$subTitle = (string) trans('firefly.update_check_title');
$subTitleIcon = 'fa-star';
$permission = app('fireflyconfig')->get('permission_update_check', -1);
$channel = app('fireflyconfig')->get('update_channel', 'stable');
$permission = FireflyConfig::get('permission_update_check', -1);
$channel = FireflyConfig::get('update_channel', 'stable');
$selected = $permission->data;
$channelSelected = $channel->data;
$options = [
@@ -96,9 +97,9 @@ class UpdateController extends Controller
$channel = $request->get('update_channel');
$channel = in_array($channel, ['stable', 'beta', 'alpha'], true) ? $channel : 'stable';
app('fireflyconfig')->set('permission_update_check', $checkForUpdates);
app('fireflyconfig')->set('last_update_check', Carbon::now()->getTimestamp());
app('fireflyconfig')->set('update_channel', $channel);
FireflyConfig::set('permission_update_check', $checkForUpdates);
FireflyConfig::set('last_update_check', Carbon::now()->getTimestamp());
FireflyConfig::set('update_channel', $channel);
session()->flash('success', (string) trans('firefly.configuration_updated'));
return redirect(route('settings.update-check'));

View File

@@ -92,7 +92,7 @@ class ShowController extends Controller
// get first journal ever to set off the budget period overview.
$first = $this->journalRepos->firstNull();
$firstDate = $first instanceof TransactionJournal ? $first->date : $start;
$periods = $this->getNoBudgetPeriodOverview($firstDate, $end);
$periods = $this->getNoModelPeriodOverview('budget', $firstDate, $end);
$page = (int) $request->get('page');
$pageSize = (int) app('preferences')->get('listPageSize', 50)->data;

View File

@@ -35,6 +35,7 @@ use FireflyIII\Support\Http\Controllers\PeriodOverview;
use Illuminate\Contracts\View\Factory;
use Illuminate\Http\Request;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Log;
use Illuminate\View\View;
/**
@@ -74,7 +75,7 @@ class NoCategoryController extends Controller
*/
public function show(Request $request, ?Carbon $start = null, ?Carbon $end = null)
{
app('log')->debug('Start of noCategory()');
Log::debug('Start of noCategory()');
$start ??= session('start');
$end ??= session('end');
@@ -82,14 +83,12 @@ class NoCategoryController extends Controller
/** @var Carbon $end */
$page = (int) $request->get('page');
$pageSize = (int) app('preferences')->get('listPageSize', 50)->data;
$subTitle = trans(
'firefly.without_category_between',
['start' => $start->isoFormat($this->monthAndDayFormat), 'end' => $end->isoFormat($this->monthAndDayFormat)]
);
$periods = $this->getNoCategoryPeriodOverview($start);
$subTitle = trans('firefly.without_category_between', ['start' => $start->isoFormat($this->monthAndDayFormat), 'end' => $end->isoFormat($this->monthAndDayFormat)]);
$first = $this->journalRepos->firstNull()->date ?? clone $start;
$periods = $this->getNoModelPeriodOverview('category', $first, $end);
app('log')->debug(sprintf('Start for noCategory() is %s', $start->format('Y-m-d')));
app('log')->debug(sprintf('End for noCategory() is %s', $end->format('Y-m-d')));
Log::debug(sprintf('Start for noCategory() is %s', $start->format('Y-m-d')));
Log::debug(sprintf('End for noCategory() is %s', $end->format('Y-m-d')));
/** @var GroupCollectorInterface $collector */
$collector = app(GroupCollectorInterface::class);
@@ -117,13 +116,13 @@ class NoCategoryController extends Controller
$periods = new Collection();
$page = (int) $request->get('page');
$pageSize = (int) app('preferences')->get('listPageSize', 50)->data;
app('log')->debug('Start of noCategory()');
Log::debug('Start of noCategory()');
$subTitle = (string) trans('firefly.all_journals_without_category');
$first = $this->journalRepos->firstNull();
$start = $first instanceof TransactionJournal ? $first->date : new Carbon();
$end = today(config('app.timezone'));
app('log')->debug(sprintf('Start for noCategory() is %s', $start->format('Y-m-d')));
app('log')->debug(sprintf('End for noCategory() is %s', $end->format('Y-m-d')));
Log::debug(sprintf('Start for noCategory() is %s', $start->format('Y-m-d')));
Log::debug(sprintf('End for noCategory() is %s', $end->format('Y-m-d')));
/** @var GroupCollectorInterface $collector */
$collector = app(GroupCollectorInterface::class);

View File

@@ -30,6 +30,7 @@ use FireflyIII\Enums\AccountTypeEnum;
use FireflyIII\Enums\TransactionTypeEnum;
use FireflyIII\Exceptions\FireflyException;
use FireflyIII\Http\Middleware\IsDemoUser;
use FireflyIII\Models\PeriodStatistic;
use FireflyIII\Models\TransactionType;
use FireflyIII\Repositories\PiggyBank\PiggyBankRepositoryInterface;
use FireflyIII\Support\Facades\Amount;
@@ -108,6 +109,8 @@ class DebugController extends Controller
Artisan::call('route:clear');
Artisan::call('view:clear');
PeriodStatistic::where('id', '>', 0)->delete();
// also do some recalculations.
Artisan::call('correction:recalculates-liabilities');
AccountBalanceCalculator::recalculateAll(false);
@@ -181,7 +184,6 @@ class DebugController extends Controller
$currentDriver = DB::getDriverName();
return [
'db_version' => app('fireflyconfig')->get('db_version', 1)->data,
'php_version' => PHP_VERSION,
'php_os' => PHP_OS,
'uname' => php_uname('m'),

View File

@@ -24,6 +24,7 @@ declare(strict_types=1);
namespace FireflyIII\Http\Controllers\System;
use FireflyIII\Support\Facades\FireflyConfig;
use Illuminate\Support\Facades\Artisan;
use Illuminate\Support\Facades\Cache;
use Exception;
@@ -81,10 +82,7 @@ class InstallController extends Controller
{
app('view')->share('FF_VERSION', config('firefly.version'));
// index will set FF3 version.
app('fireflyconfig')->set('ff3_version', (string) config('firefly.version'));
// set new DB version.
app('fireflyconfig')->set('db_version', (int) config('firefly.db_version'));
FireflyConfig::set('ff3_version', (string) config('firefly.version'));
return view('install.index');
}

View File

@@ -31,6 +31,7 @@ use FireflyIII\Http\Controllers\Controller;
use FireflyIII\Repositories\Account\AccountRepositoryInterface;
use FireflyIII\Repositories\TransactionGroup\TransactionGroupRepositoryInterface;
use FireflyIII\Services\Internal\Update\GroupCloneService;
use FireflyIII\Support\Facades\Preferences;
use Illuminate\Contracts\View\Factory;
use Illuminate\Contracts\View\View;
use Illuminate\Http\JsonResponse;
@@ -76,7 +77,7 @@ class CreateController extends Controller
// event!
event(new StoredTransactionGroup($newGroup, true, true));
app('preferences')->mark();
Preferences::mark();
$title = $newGroup->title ?? $newGroup->transactionJournals->first()->description;
$link = route('transactions.show', [$newGroup->id]);
@@ -103,7 +104,7 @@ class CreateController extends Controller
* */
public function create(?string $objectType)
{
app('preferences')->mark();
Preferences::mark();
$sourceId = (int) request()->get('source');
$destinationId = (int) request()->get('destination');
@@ -114,7 +115,9 @@ class CreateController extends Controller
$preFilled = session()->has('preFilled') ? session('preFilled') : [];
$subTitle = (string) trans(sprintf('breadcrumbs.create_%s', strtolower((string) $objectType)));
$subTitleIcon = 'fa-plus';
$optionalFields = app('preferences')->get('transaction_journal_optional_fields', [])->data;
/** @var null|array $optionalFields */
$optionalFields = Preferences::get('transaction_journal_optional_fields', [])->data;
$allowedOpposingTypes = config('firefly.allowed_opposing_types');
$accountToTypes = config('firefly.account_to_transaction');
$previousUrl = $this->rememberPreviousUrl('transactions.create.url');

View File

@@ -26,6 +26,7 @@ namespace FireflyIII\Http\Middleware;
use Closure;
use FireflyIII\Exceptions\FireflyException;
use FireflyIII\Support\System\IsOldVersion;
use FireflyIII\Support\System\OAuthKeys;
use Illuminate\Database\QueryException;
use Illuminate\Http\Request;
@@ -37,6 +38,8 @@ use Illuminate\Support\Facades\Log;
*/
class Installer
{
use IsOldVersion;
/**
* Handle an incoming request.
*
@@ -65,7 +68,7 @@ class Installer
// run installer when no tables are present,
// or when old scheme version
// or when old firefly version
if ($this->hasNoTables() || $this->oldDBVersion() || $this->oldVersion()) {
if ($this->hasNoTables() || $this->isOldVersionInstalled()) {
return response()->redirectTo(route('installer.index'));
}
OAuthKeys::verifyKeysRoutine();
@@ -126,59 +129,4 @@ class Installer
{
return false !== stripos($message, 'Base table or view not found');
}
/**
* Check if the "db_version" variable is correct.
*/
private function oldDBVersion(): bool
{
// older version in config than database?
$configVersion = (int) config('firefly.db_version');
$dbVersion = (int) app('fireflyconfig')->getFresh('db_version', 1)->data;
if ($configVersion > $dbVersion) {
Log::warning(
sprintf(
'The current configured version (%d) is older than the required version (%d). Redirect to migrate routine.',
$dbVersion,
$configVersion
)
);
return true;
}
// Log::info(sprintf('Configured DB version (%d) equals expected DB version (%d)', $dbVersion, $configVersion));
return false;
}
/**
* Check if the "firefly_version" variable is correct.
*/
private function oldVersion(): bool
{
// version compare thing.
$configVersion = (string) config('firefly.version');
$dbVersion = (string) app('fireflyconfig')->getFresh('ff3_version', '1.0')->data;
if (str_starts_with($configVersion, 'develop')) {
Log::debug('Skipping version check for develop version.');
return false;
}
if (1 === version_compare($configVersion, $dbVersion)) {
Log::warning(
sprintf(
'The current configured Firefly III version (%s) is older than the required version (%s). Redirect to migrate routine.',
$dbVersion,
$configVersion
)
);
return true;
}
// Log::info(sprintf('Installed Firefly III version (%s) equals expected Firefly III version (%s)', $dbVersion, $configVersion));
return false;
}
}

View File

@@ -241,4 +241,11 @@ class Account extends Model
get: static fn ($value) => (string)$value,
);
}
public function primaryPeriodStatistics(): MorphMany
{
return $this->morphMany(PeriodStatistic::class, 'primary_statable');
}
}

View File

@@ -109,4 +109,9 @@ class Category extends Model
'user_group_id' => 'integer',
];
}
public function primaryPeriodStatistics(): MorphMany
{
return $this->morphMany(PeriodStatistic::class, 'primary_statable');
}
}

View File

@@ -0,0 +1,60 @@
<?php
declare(strict_types=1);
namespace FireflyIII\Models;
use FireflyIII\Casts\SeparateTimezoneCaster;
use FireflyIII\Support\Models\ReturnsIntegerUserIdTrait;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\MorphTo;
class PeriodStatistic extends Model
{
use ReturnsIntegerUserIdTrait;
protected function casts(): array
{
return [
'created_at' => 'datetime',
'updated_at' => 'datetime',
'start' => SeparateTimezoneCaster::class,
'end' => SeparateTimezoneCaster::class,
];
}
public function userGroup(): BelongsTo
{
return $this->belongsTo(UserGroup::class);
}
protected function count(): Attribute
{
return Attribute::make(
get: static fn ($value) => (int)$value,
);
}
public function primaryStatable(): MorphTo
{
return $this->morphTo();
}
public function secondaryStatable(): MorphTo
{
return $this->morphTo();
}
public function tertiaryStatable(): MorphTo
{
return $this->morphTo();
}
}

View File

@@ -104,4 +104,9 @@ class Tag extends Model
'user_group_id' => 'integer',
];
}
public function primaryPeriodStatistics(): MorphMany
{
return $this->morphMany(PeriodStatistic::class, 'primary_statable');
}
}

View File

@@ -76,6 +76,14 @@ class UserGroup extends Model
return $this->hasMany(Account::class);
}
/**
* Link to accounts.
*/
public function periodStatistics(): HasMany
{
return $this->hasMany(PeriodStatistic::class);
}
/**
* Link to attachments.
*/

View File

@@ -43,6 +43,8 @@ use FireflyIII\Repositories\AuditLogEntry\ALERepository;
use FireflyIII\Repositories\AuditLogEntry\ALERepositoryInterface;
use FireflyIII\Repositories\ObjectGroup\ObjectGroupRepository;
use FireflyIII\Repositories\ObjectGroup\ObjectGroupRepositoryInterface;
use FireflyIII\Repositories\PeriodStatistic\PeriodStatisticRepository;
use FireflyIII\Repositories\PeriodStatistic\PeriodStatisticRepositoryInterface;
use FireflyIII\Repositories\TransactionType\TransactionTypeRepository;
use FireflyIII\Repositories\TransactionType\TransactionTypeRepositoryInterface;
use FireflyIII\Repositories\User\UserRepository;
@@ -174,6 +176,18 @@ class FireflyServiceProvider extends ServiceProvider
}
);
$this->app->bind(
static function (Application $app): PeriodStatisticRepositoryInterface {
/** @var PeriodStatisticRepository $repository */
$repository = app(PeriodStatisticRepository::class);
if ($app->auth->check()) { // @phpstan-ignore-line (phpstan does not understand the reference to auth)
$repository->setUser(auth()->user());
}
return $repository;
}
);
$this->app->bind(
static function (Application $app): WebhookRepositoryInterface {
/** @var WebhookRepository $repository */

View File

@@ -546,6 +546,8 @@ class AccountRepository implements AccountRepositoryInterface, UserGroupInterfac
#[Override]
public function periodCollection(Account $account, Carbon $start, Carbon $end): array
{
Log::debug(sprintf('periodCollection(#%d, %s, %s)', $account->id, $start->format('Y-m-d'), $end->format('Y-m-d')));
return $account->transactions()
->leftJoin('transaction_journals', 'transaction_journals.id', '=', 'transactions.transaction_journal_id')
->leftJoin('transaction_types', 'transaction_types.id', '=', 'transaction_journals.transaction_type_id')

View File

@@ -358,4 +358,43 @@ class CategoryRepository implements CategoryRepositoryInterface, UserGroupInterf
return $service->update($category, $data);
}
public function periodCollection(Category $category, Carbon $start, Carbon $end): array
{
Log::debug(sprintf('periodCollection(#%d, %s, %s)', $category->id, $start->format('Y-m-d'), $end->format('Y-m-d')));
return $category->transactionJournals()
->leftJoin('transactions', 'transactions.transaction_journal_id', '=', 'transaction_journals.id')
->leftJoin('transaction_types', 'transaction_types.id', '=', 'transaction_journals.transaction_type_id')
->leftJoin('transaction_currencies', 'transaction_currencies.id', '=', 'transactions.transaction_currency_id')
->leftJoin('transaction_currencies as foreign_currencies', 'foreign_currencies.id', '=', 'transactions.foreign_currency_id')
->where('transaction_journals.date', '>=', $start)
->where('transaction_journals.date', '<=', $end)
->where('transactions.amount', '>', 0)
->get([
// currencies
'transaction_currencies.id as currency_id',
'transaction_currencies.code as currency_code',
'transaction_currencies.name as currency_name',
'transaction_currencies.symbol as currency_symbol',
'transaction_currencies.decimal_places as currency_decimal_places',
// foreign
'foreign_currencies.id as foreign_currency_id',
'foreign_currencies.code as foreign_currency_code',
'foreign_currencies.name as foreign_currency_name',
'foreign_currencies.symbol as foreign_currency_symbol',
'foreign_currencies.decimal_places as foreign_currency_decimal_places',
// fields
'transaction_journals.date',
'transaction_types.type',
'transaction_journals.transaction_currency_id',
'transactions.amount',
'transactions.native_amount as pc_amount',
'transactions.foreign_amount',
])
->toArray()
;
}
}

View File

@@ -48,6 +48,8 @@ interface CategoryRepositoryInterface
public function categoryStartsWith(string $query, int $limit): Collection;
public function periodCollection(Category $category, Carbon $start, Carbon $end): array;
public function destroy(Category $category): bool;
/**

View File

@@ -510,9 +510,7 @@ class OperationsRepository implements OperationsRepositoryInterface, UserGroupIn
$summarizer->setConvertToPrimary($convertToPrimary);
// filter $journals by range AND currency if it is present.
$expenses = array_filter($expenses, static function (array $expense) use ($category): bool {
return $expense['category_id'] === $category->id;
});
$expenses = array_filter($expenses, static fn (array $expense): bool => $expense['category_id'] === $category->id);
return $summarizer->groupByCurrencyId($expenses, $method, false);
}

View File

@@ -0,0 +1,143 @@
<?php
declare(strict_types=1);
/*
* PeriodStatisticRepository.php
* Copyright (c) 2025 james@firefly-iii.org
*
* This file is part of Firefly III (https://github.com/firefly-iii).
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
namespace FireflyIII\Repositories\PeriodStatistic;
use Carbon\Carbon;
use FireflyIII\Models\PeriodStatistic;
use FireflyIII\Models\UserGroup;
use FireflyIII\Support\Repositories\UserGroup\UserGroupInterface;
use FireflyIII\Support\Repositories\UserGroup\UserGroupTrait;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Log;
use Override;
class PeriodStatisticRepository implements PeriodStatisticRepositoryInterface, UserGroupInterface
{
use UserGroupTrait;
public function findPeriodStatistics(Model $model, Carbon $start, Carbon $end, array $types): Collection
{
return $model->primaryPeriodStatistics()
->where('start', $start)
->where('end', $end)
->whereIn('type', $types)
->get()
;
}
public function findPeriodStatistic(Model $model, Carbon $start, Carbon $end, string $type): Collection
{
return $model->primaryPeriodStatistics()
->where('start', $start)
->where('end', $end)
->where('type', $type)
->get()
;
}
public function saveStatistic(Model $model, int $currencyId, Carbon $start, Carbon $end, string $type, int $count, string $amount): PeriodStatistic
{
$stat = new PeriodStatistic();
$stat->primaryStatable()->associate($model);
$stat->transaction_currency_id = $currencyId;
$stat->user_group_id = $this->getUserGroup()->id;
$stat->start = $start;
$stat->start_tz = $start->format('e');
$stat->end = $end;
$stat->end_tz = $end->format('e');
$stat->amount = $amount;
$stat->count = $count;
$stat->type = $type;
$stat->save();
Log::debug(sprintf(
'Saved #%d [currency #%d, Model %s #%d, %s to %s, %d, %s] as new statistic.',
$stat->id,
$model::class,
$model->id,
$stat->transaction_currency_id,
$stat->start->toW3cString(),
$stat->end->toW3cString(),
$count,
$amount
));
return $stat;
}
public function allInRangeForModel(Model $model, Carbon $start, Carbon $end): Collection
{
return $model->primaryPeriodStatistics()->where('start', '>=', $start)->where('end', '<=', $end)->get();
}
public function deleteStatisticsForModel(Model $model, Carbon $date): void
{
$model->primaryPeriodStatistics()->where('start', '<=', $date)->where('end', '>=', $date)->delete();
}
#[Override]
public function allInRangeForPrefix(string $prefix, Carbon $start, Carbon $end): Collection
{
return $this->userGroup->periodStatistics()
->where('type', 'LIKE', sprintf('%s%%', $prefix))
->where('start', '>=', $start)->where('end', '<=', $end)->get()
;
}
#[Override]
public function savePrefixedStatistic(string $prefix, int $currencyId, Carbon $start, Carbon $end, string $type, int $count, string $amount): PeriodStatistic
{
$stat = new PeriodStatistic();
$stat->transaction_currency_id = $currencyId;
$stat->user_group_id = $this->getUserGroup()->id;
$stat->start = $start;
$stat->start_tz = $start->format('e');
$stat->end = $end;
$stat->end_tz = $end->format('e');
$stat->amount = $amount;
$stat->count = $count;
$stat->type = sprintf('%s_%s', $prefix, $type);
$stat->save();
Log::debug(sprintf(
'Saved #%d [currency #%d, type "%s", %s to %s, %d, %s] as new statistic.',
$stat->id,
$stat->transaction_currency_id,
$stat->type,
$stat->start->toW3cString(),
$stat->end->toW3cString(),
$count,
$amount
));
return $stat;
}
#[Override]
public function deleteStatisticsForPrefix(UserGroup $userGroup, string $prefix, Carbon $date): void
{
$userGroup->periodStatistics()->where('start', '<=', $date)->where('end', '>=', $date)->where('type', 'LIKE', sprintf('%s%%', $prefix))->delete();
}
}

View File

@@ -0,0 +1,49 @@
<?php
declare(strict_types=1);
/*
* PeriodStatisticRepositoryInterface.php
* Copyright (c) 2025 james@firefly-iii.org
*
* This file is part of Firefly III (https://github.com/firefly-iii).
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
namespace FireflyIII\Repositories\PeriodStatistic;
use Carbon\Carbon;
use FireflyIII\Models\PeriodStatistic;
use FireflyIII\Models\UserGroup;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Collection;
interface PeriodStatisticRepositoryInterface
{
public function findPeriodStatistics(Model $model, Carbon $start, Carbon $end, array $types): Collection;
public function findPeriodStatistic(Model $model, Carbon $start, Carbon $end, string $type): Collection;
public function saveStatistic(Model $model, int $currencyId, Carbon $start, Carbon $end, string $type, int $count, string $amount): PeriodStatistic;
public function savePrefixedStatistic(string $prefix, int $currencyId, Carbon $start, Carbon $end, string $type, int $count, string $amount): PeriodStatistic;
public function allInRangeForModel(Model $model, Carbon $start, Carbon $end): Collection;
public function allInRangeForPrefix(string $prefix, Carbon $start, Carbon $end): Collection;
public function deleteStatisticsForModel(Model $model, Carbon $date): void;
public function deleteStatisticsForPrefix(UserGroup $userGroup, string $prefix, Carbon $date): void;
}

View File

@@ -36,6 +36,7 @@ use FireflyIII\Models\Transaction;
use FireflyIII\Models\TransactionJournal;
use FireflyIII\Repositories\ObjectGroup\CreatesObjectGroups;
use FireflyIII\Support\Facades\Amount;
use FireflyIII\Support\Facades\Steam;
use FireflyIII\Support\Http\Api\ExchangeRateConverter;
use Illuminate\Support\Facades\Log;
@@ -67,7 +68,7 @@ trait ModifiesPiggyBanks
{
$currentAmount = $this->getCurrentAmount($piggyBank, $account);
$pivot = $piggyBank->accounts()->where('accounts.id', $account->id)->first()->pivot;
$pivot->current_amount = bcsub($currentAmount, $amount);
$pivot->current_amount = bcsub((string) $currentAmount, $amount);
$pivot->native_current_amount = null;
// also update native_current_amount.
@@ -90,7 +91,7 @@ trait ModifiesPiggyBanks
{
$currentAmount = $this->getCurrentAmount($piggyBank, $account);
$pivot = $piggyBank->accounts()->where('accounts.id', $account->id)->first()->pivot;
$pivot->current_amount = bcadd($currentAmount, $amount);
$pivot->current_amount = bcadd((string) $currentAmount, $amount);
$pivot->native_current_amount = null;
// also update native_current_amount.
@@ -122,13 +123,13 @@ trait ModifiesPiggyBanks
if (0 !== bccomp($piggyBank->target_amount, '0')) {
$leftToSave = bcsub($piggyBank->target_amount, $savedSoFar);
$maxAmount = 1 === bccomp($leftOnAccount, $leftToSave) ? $leftToSave : $leftOnAccount;
$leftToSave = bcsub($piggyBank->target_amount, (string) $savedSoFar);
$maxAmount = 1 === bccomp((string) $leftOnAccount, $leftToSave) ? $leftToSave : $leftOnAccount;
Log::debug(sprintf('Left to save: %s', $leftToSave));
Log::debug(sprintf('Maximum amount: %s', $maxAmount));
}
$compare = bccomp($amount, $maxAmount);
$compare = bccomp($amount, (string) $maxAmount);
$result = $compare <= 0;
Log::debug(sprintf('Compare <= 0? %d, so canAddAmount is %s', $compare, var_export($result, true)));
@@ -140,7 +141,7 @@ trait ModifiesPiggyBanks
{
$savedSoFar = $this->getCurrentAmount($piggyBank, $account);
return bccomp($amount, $savedSoFar) <= 0;
return bccomp($amount, (string) $savedSoFar) <= 0;
}
/**
@@ -227,16 +228,15 @@ trait ModifiesPiggyBanks
$factory->user = $this->user;
// the piggy bank currency is set or updated FIRST, if it exists.
$factory->linkToAccountIds($piggyBank, $data['accounts'] ?? []);
// if the piggy bank is now smaller than the sum of the money saved,
// remove money from all accounts until the piggy bank is the right amount.
$currentAmount = $this->getCurrentAmount($piggyBank);
if (1 === bccomp($currentAmount, (string)$piggyBank->target_amount) && 0 !== bccomp((string)$piggyBank->target_amount, '0')) {
if (1 === bccomp((string) $currentAmount, (string)$piggyBank->target_amount) && 0 !== bccomp((string)$piggyBank->target_amount, '0')) {
Log::debug(sprintf('Current amount is %s, target amount is %s', $currentAmount, $piggyBank->target_amount));
$difference = bcsub((string)$piggyBank->target_amount, $currentAmount);
$difference = bcsub((string)$piggyBank->target_amount, (string) $currentAmount);
// an amount will be removed, create "negative" event:
// Log::debug(sprintf('ChangedAmount: is triggered with difference "%s"', $difference));
@@ -244,7 +244,7 @@ trait ModifiesPiggyBanks
// question is, from which account(s) to remove the difference?
// solution: just start from the top until there is no more money left to remove.
$this->removeAmountFromAll($piggyBank, app('steam')->positive($difference));
$this->removeAmountFromAll($piggyBank, Steam::positive($difference));
}
// update using name:

View File

@@ -23,6 +23,7 @@ declare(strict_types=1);
namespace FireflyIII\Repositories\Tag;
use Override;
use Carbon\Carbon;
use FireflyIII\Enums\TransactionTypeEnum;
use FireflyIII\Factory\TagFactory;
@@ -379,4 +380,44 @@ class TagRepository implements TagRepositoryInterface, UserGroupInterface
/** @var null|Location */
return $tag->locations()->first();
}
#[Override]
public function periodCollection(Tag $tag, Carbon $start, Carbon $end): array
{
Log::debug(sprintf('periodCollection(#%d, %s, %s)', $tag->id, $start->format('Y-m-d'), $end->format('Y-m-d')));
return $tag->transactionJournals()
->leftJoin('transactions', 'transactions.transaction_journal_id', '=', 'transaction_journals.id')
->leftJoin('transaction_types', 'transaction_types.id', '=', 'transaction_journals.transaction_type_id')
->leftJoin('transaction_currencies', 'transaction_currencies.id', '=', 'transactions.transaction_currency_id')
->leftJoin('transaction_currencies as foreign_currencies', 'foreign_currencies.id', '=', 'transactions.foreign_currency_id')
->where('transaction_journals.date', '>=', $start)
->where('transaction_journals.date', '<=', $end)
->where('transactions.amount', '>', 0)
->get([
// currencies
'transaction_currencies.id as currency_id',
'transaction_currencies.code as currency_code',
'transaction_currencies.name as currency_name',
'transaction_currencies.symbol as currency_symbol',
'transaction_currencies.decimal_places as currency_decimal_places',
// foreign
'foreign_currencies.id as foreign_currency_id',
'foreign_currencies.code as foreign_currency_code',
'foreign_currencies.name as foreign_currency_name',
'foreign_currencies.symbol as foreign_currency_symbol',
'foreign_currencies.decimal_places as foreign_currency_decimal_places',
// fields
'transaction_journals.date',
'transaction_types.type',
'transaction_journals.transaction_currency_id',
'transactions.amount',
'transactions.native_amount as pc_amount',
'transactions.foreign_amount',
])
->toArray()
;
}
}

View File

@@ -51,6 +51,8 @@ interface TagRepositoryInterface
*/
public function destroy(Tag $tag): bool;
public function periodCollection(Tag $tag, Carbon $start, Carbon $end): array;
/**
* Destroy all tags.
*/

View File

@@ -26,6 +26,7 @@ namespace FireflyIII\Services\FireflyIIIOrg\Update;
use Carbon\Carbon;
use FireflyIII\Events\NewVersionAvailable;
use FireflyIII\Support\System\IsOldVersion;
use GuzzleHttp\Client;
use GuzzleHttp\Exception\GuzzleException;
use Illuminate\Support\Facades\Log;
@@ -38,6 +39,8 @@ use function Safe\json_decode;
*/
class UpdateRequest implements UpdateRequestInterface
{
use IsOldVersion;
public function getUpdateInformation(string $channel): array
{
Log::debug(sprintf('Now in getUpdateInformation(%s)', $channel));
@@ -183,20 +186,15 @@ class UpdateRequest implements UpdateRequestInterface
private function parseResultDevelop(string $current, string $latest, array $information): array
{
Log::debug(sprintf('User is running develop version "%s"', $current));
$parts = explode('/', $current);
$compare = $this->compareDevelopVersions($current, $latest);
$return = [];
/** @var Carbon $devDate */
$devDate = Carbon::createFromFormat('Y-m-d', $parts[1]);
if ($devDate->lte($information['date'])) {
Log::debug(sprintf('This development release is older, release = %s, latest version %s = %s', $devDate->format('Y-m-d'), $latest, $information['date']->format('Y-m-d')));
if (-1 === $compare) {
$return['level'] = 'info';
$return['message'] = (string) trans('firefly.update_current_dev_older', ['version' => $current, 'new_version' => $latest]);
return $return;
}
Log::debug(sprintf('This development release is newer, release = %s, latest version %s = %s', $devDate->format('Y-m-d'), $latest, $information['date']->format('Y-m-d')));
$return['level'] = 'info';
$return['message'] = (string) trans('firefly.update_current_dev_newer', ['version' => $current, 'new_version' => $latest]);

View File

@@ -276,66 +276,66 @@ class CreditRecalculateService
if ($isSameAccount && $isCredit && $this->isWithdrawalIn($usedAmount, $type)) { // case 1
$usedAmount = app('steam')->positive($usedAmount);
return bcadd($leftOfDebt, $usedAmount);
return bcadd($leftOfDebt, (string) $usedAmount);
// Log::debug(sprintf('Case 1 (withdrawal into credit liability): %s + %s = %s', app('steam')->bcround($leftOfDebt, 2), app('steam')->bcround($usedAmount, 2), app('steam')->bcround($result, 2)));
}
if ($isSameAccount && $isCredit && $this->isWithdrawalOut($usedAmount, $type)) { // case 2
$usedAmount = app('steam')->positive($usedAmount);
return bcsub($leftOfDebt, $usedAmount);
return bcsub($leftOfDebt, (string) $usedAmount);
// Log::debug(sprintf('Case 2 (withdrawal away from liability): %s - %s = %s', app('steam')->bcround($leftOfDebt, 2), app('steam')->bcround($usedAmount, 2), app('steam')->bcround($result, 2)));
}
if ($isSameAccount && $isCredit && $this->isDepositOut($usedAmount, $type)) { // case 3
$usedAmount = app('steam')->positive($usedAmount);
return bcsub($leftOfDebt, $usedAmount);
return bcsub($leftOfDebt, (string) $usedAmount);
// Log::debug(sprintf('Case 3 (deposit away from liability): %s - %s = %s', app('steam')->bcround($leftOfDebt, 2), app('steam')->bcround($usedAmount, 2), app('steam')->bcround($result, 2)));
}
if ($isSameAccount && $isCredit && $this->isDepositIn($usedAmount, $type)) { // case 4
$usedAmount = app('steam')->positive($usedAmount);
return bcadd($leftOfDebt, $usedAmount);
return bcadd($leftOfDebt, (string) $usedAmount);
// Log::debug(sprintf('Case 4 (deposit into credit liability): %s + %s = %s', app('steam')->bcround($leftOfDebt, 2), app('steam')->bcround($usedAmount, 2), app('steam')->bcround($result, 2)));
}
if ($isSameAccount && $isCredit && $this->isTransferIn($usedAmount, $type)) { // case 5
$usedAmount = app('steam')->positive($usedAmount);
return bcadd($leftOfDebt, $usedAmount);
return bcadd($leftOfDebt, (string) $usedAmount);
// Log::debug(sprintf('Case 5 (transfer into credit liability): %s + %s = %s', app('steam')->bcround($leftOfDebt, 2), app('steam')->bcround($usedAmount, 2), app('steam')->bcround($result, 2)));
}
if ($isSameAccount && $isDebit && $this->isWithdrawalIn($usedAmount, $type)) { // case 6
$usedAmount = app('steam')->positive($usedAmount);
return bcsub($leftOfDebt, $usedAmount);
return bcsub($leftOfDebt, (string) $usedAmount);
// Log::debug(sprintf('Case 6 (withdrawal into debit liability): %s - %s = %s', app('steam')->bcround($leftOfDebt, 2), app('steam')->bcround($usedAmount, 2), app('steam')->bcround($result, 2)));
}
if ($isSameAccount && $isDebit && $this->isDepositOut($usedAmount, $type)) { // case 7
$usedAmount = app('steam')->positive($usedAmount);
return bcadd($leftOfDebt, $usedAmount);
return bcadd($leftOfDebt, (string) $usedAmount);
// Log::debug(sprintf('Case 7 (deposit away from liability): %s + %s = %s', app('steam')->bcround($leftOfDebt, 2), app('steam')->bcround($usedAmount, 2), app('steam')->bcround($result, 2)));
}
if ($isSameAccount && $isDebit && $this->isWithdrawalOut($usedAmount, $type)) { // case 8
$usedAmount = app('steam')->positive($usedAmount);
return bcadd($leftOfDebt, $usedAmount);
return bcadd($leftOfDebt, (string) $usedAmount);
// Log::debug(sprintf('Case 8 (withdrawal away from liability): %s + %s = %s', app('steam')->bcround($leftOfDebt, 2), app('steam')->bcround($usedAmount, 2), app('steam')->bcround($result, 2)));
}
if ($isSameAccount && $isDebit && $this->isTransferIn($usedAmount, $type)) { // case 9
$usedAmount = app('steam')->positive($usedAmount);
return bcsub($leftOfDebt, $usedAmount);
return bcsub($leftOfDebt, (string) $usedAmount);
// 2024-10-05, #9225 this used to say you would owe more, but a transfer INTO a debit from wherever means you owe LESS.
// Log::debug(sprintf('Case 9 (transfer into debit liability, means you owe LESS): %s - %s = %s', app('steam')->bcround($leftOfDebt, 2), app('steam')->bcround($usedAmount, 2), app('steam')->bcround($result, 2)));
}
if ($isSameAccount && $isDebit && $this->isTransferOut($usedAmount, $type)) { // case 10
$usedAmount = app('steam')->positive($usedAmount);
return bcadd($leftOfDebt, $usedAmount);
return bcadd($leftOfDebt, (string) $usedAmount);
// 2024-10-05, #9225 this used to say you would owe less, but a transfer OUT OF a debit from wherever means you owe MORE.
// Log::debug(sprintf('Case 10 (transfer out of debit liability, means you owe MORE): %s + %s = %s', app('steam')->bcround($leftOfDebt, 2), app('steam')->bcround($usedAmount, 2), app('steam')->bcround($result, 2)));
}
@@ -344,7 +344,7 @@ class CreditRecalculateService
if (in_array($type, [TransactionTypeEnum::WITHDRAWAL->value, TransactionTypeEnum::DEPOSIT->value, TransactionTypeEnum::TRANSFER->value], true)) {
$usedAmount = app('steam')->negative($usedAmount);
return bcadd($leftOfDebt, $usedAmount);
return bcadd($leftOfDebt, (string) $usedAmount);
// Log::debug(sprintf('Case X (all other cases): %s + %s = %s', app('steam')->bcround($leftOfDebt, 2), app('steam')->bcround($usedAmount, 2), app('steam')->bcround($result, 2)));
}

View File

@@ -41,280 +41,6 @@ use NumberFormatter;
*/
class Amount
{
/**
* This method will properly format the given number, in color or "black and white",
* as a currency, given two things: the currency required and the current locale.
*
* @throws FireflyException
*/
public function formatAnything(TransactionCurrency $format, string $amount, ?bool $coloured = null): string
{
return $this->formatFlat($format->symbol, $format->decimal_places, $amount, $coloured);
}
/**
* This method will properly format the given number, in color or "black and white",
* as a currency, given two things: the currency required and the current locale.
*
* @throws FireflyException
*/
public function formatFlat(string $symbol, int $decimalPlaces, string $amount, ?bool $coloured = null): string
{
$locale = Steam::getLocale();
$rounded = Steam::bcround($amount, $decimalPlaces);
$coloured ??= true;
$fmt = new NumberFormatter($locale, NumberFormatter::CURRENCY);
$fmt->setSymbol(NumberFormatter::CURRENCY_SYMBOL, $symbol);
$fmt->setAttribute(NumberFormatter::MIN_FRACTION_DIGITS, $decimalPlaces);
$fmt->setAttribute(NumberFormatter::MAX_FRACTION_DIGITS, $decimalPlaces);
$result = (string)$fmt->format((float)$rounded); // intentional float
if (true === $coloured) {
if (1 === bccomp($rounded, '0')) {
return sprintf('<span class="text-success money-positive">%s</span>', $result);
}
if (-1 === bccomp($rounded, '0')) {
return sprintf('<span class="text-danger money-negative">%s</span>', $result);
}
return sprintf('<span class="money-neutral">%s</span>', $result);
}
return $result;
}
public function formatByCurrencyId(int $currencyId, string $amount, ?bool $coloured = null): string
{
$format = $this->getTransactionCurrencyById($currencyId);
return $this->formatFlat($format->symbol, $format->decimal_places, $amount, $coloured);
}
public function getAllCurrencies(): Collection
{
return TransactionCurrency::orderBy('code', 'ASC')->get();
}
/**
* Experimental function to see if we can quickly and quietly get the amount from a journal.
* This depends on the user's default currency and the wish to have it converted.
*/
public function getAmountFromJournal(array $journal): string
{
$convertToPrimary = $this->convertToPrimary();
$currency = $this->getPrimaryCurrency();
$field = $convertToPrimary && $currency->id !== $journal['currency_id'] ? 'pc_amount' : 'amount';
$amount = $journal[$field] ?? '0';
// Log::debug(sprintf('Field is %s, amount is %s', $field, $amount));
// fallback, the transaction has a foreign amount in $currency.
if ($convertToPrimary && null !== $journal['foreign_amount'] && $currency->id === (int)$journal['foreign_currency_id']) {
$amount = $journal['foreign_amount'];
// Log::debug(sprintf('Overruled, amount is now %s', $amount));
}
return (string)$amount;
}
public function getTransactionCurrencyById(int $currencyId): TransactionCurrency
{
$instance = PreferencesSingleton::getInstance();
$key = sprintf('transaction_currency_%d', $currencyId);
/** @var null|TransactionCurrency $pref */
$pref = $instance->getPreference($key);
if (null !== $pref) {
return $pref;
}
$currency = TransactionCurrency::find($currencyId);
if (null === $currency) {
$message = sprintf('Could not find a transaction currency with ID #%d in %s', $currencyId, __METHOD__);
Log::error($message);
throw new FireflyException($message);
}
$instance->setPreference($key, $currency);
return $currency;
}
public function getTransactionCurrencyByCode(string $code): TransactionCurrency
{
$instance = PreferencesSingleton::getInstance();
$key = sprintf('transaction_currency_%s', $code);
/** @var null|TransactionCurrency $pref */
$pref = $instance->getPreference($key);
if (null !== $pref) {
return $pref;
}
$currency = TransactionCurrency::whereCode($code)->first();
if (null === $currency) {
$message = sprintf('Could not find a transaction currency with code "%s" in %s', $code, __METHOD__);
Log::error($message);
throw new FireflyException($message);
}
$instance->setPreference($key, $currency);
return $currency;
}
public function convertToPrimary(?User $user = null): bool
{
$instance = PreferencesSingleton::getInstance();
if (!$user instanceof User) {
$pref = $instance->getPreference('convert_to_primary_no_user');
if (null === $pref) {
$res = true === Preferences::get('convert_to_primary', false)->data && true === config('cer.enabled');
$instance->setPreference('convert_to_primary_no_user', $res);
return $res;
}
return $pref;
}
$key = sprintf('convert_to_primary_%d', $user->id);
$pref = $instance->getPreference($key);
if (null === $pref) {
$res = true === Preferences::getForUser($user, 'convert_to_primary', false)->data && true === config('cer.enabled');
$instance->setPreference($key, $res);
return $res;
}
return $pref;
}
public function getPrimaryCurrency(): TransactionCurrency
{
if (auth()->check()) {
/** @var User $user */
$user = auth()->user();
if (null !== $user->userGroup) {
return $this->getPrimaryCurrencyByUserGroup($user->userGroup);
}
}
return $this->getSystemCurrency();
}
public function getPrimaryCurrencyByUserGroup(UserGroup $userGroup): TransactionCurrency
{
$cache = new CacheProperties();
$cache->addProperty('getPrimaryCurrencyByGroup');
$cache->addProperty($userGroup->id);
if ($cache->has()) {
return $cache->get();
}
/** @var null|TransactionCurrency $primary */
$primary = $userGroup->currencies()->where('group_default', true)->first();
if (null === $primary) {
$primary = $this->getSystemCurrency();
// could be the user group has no default right now.
$userGroup->currencies()->sync([$primary->id => ['group_default' => true]]);
}
$cache->store($primary);
return $primary;
}
public function getSystemCurrency(): TransactionCurrency
{
return TransactionCurrency::whereNull('deleted_at')->where('code', 'EUR')->first();
}
/**
* Experimental function to see if we can quickly and quietly get the amount from a journal.
* This depends on the user's default currency and the wish to have it converted.
*/
public function getAmountFromJournalObject(TransactionJournal $journal): string
{
$convertToPrimary = $this->convertToPrimary();
$currency = $this->getPrimaryCurrency();
$field = $convertToPrimary && $currency->id !== $journal->transaction_currency_id ? 'pc_amount' : 'amount';
/** @var null|Transaction $sourceTransaction */
$sourceTransaction = $journal->transactions()->where('amount', '<', 0)->first();
if (null === $sourceTransaction) {
return '0';
}
$amount = $sourceTransaction->{$field} ?? '0';
if ((int)$sourceTransaction->foreign_currency_id === $currency->id) {
// use foreign amount instead!
$amount = (string)$sourceTransaction->foreign_amount; // hard coded to be foreign amount.
}
return $amount;
}
public function getCurrencies(): Collection
{
/** @var User $user */
$user = auth()->user();
return $user->currencies()->orderBy('code', 'ASC')->get();
}
/**
* This method returns the correct format rules required by accounting.js,
* the library used to format amounts in charts.
*
* Used only in one place.
*
* @throws FireflyException
*/
public function getJsConfig(): array
{
$config = $this->getLocaleInfo();
$negative = self::getAmountJsConfig($config['n_sep_by_space'], $config['n_sign_posn'], $config['negative_sign'], $config['n_cs_precedes']);
$positive = self::getAmountJsConfig($config['p_sep_by_space'], $config['p_sign_posn'], $config['positive_sign'], $config['p_cs_precedes']);
return [
'mon_decimal_point' => $config['mon_decimal_point'],
'mon_thousands_sep' => $config['mon_thousands_sep'],
'format' => [
'pos' => $positive,
'neg' => $negative,
'zero' => $positive,
],
];
}
/**
* @throws FireflyException
*/
private function getLocaleInfo(): array
{
// get config from preference, not from translation:
$locale = Steam::getLocale();
$array = Steam::getLocaleArray($locale);
setlocale(LC_MONETARY, $array);
$info = localeconv();
// correct variables
$info['n_cs_precedes'] = $this->getLocaleField($info, 'n_cs_precedes');
$info['p_cs_precedes'] = $this->getLocaleField($info, 'p_cs_precedes');
$info['n_sep_by_space'] = $this->getLocaleField($info, 'n_sep_by_space');
$info['p_sep_by_space'] = $this->getLocaleField($info, 'p_sep_by_space');
$fmt = new NumberFormatter($locale, NumberFormatter::CURRENCY);
$info['mon_decimal_point'] = $fmt->getSymbol(NumberFormatter::MONETARY_SEPARATOR_SYMBOL);
$info['mon_thousands_sep'] = $fmt->getSymbol(NumberFormatter::MONETARY_GROUPING_SEPARATOR_SYMBOL);
return $info;
}
private function getLocaleField(array $info, string $field): bool
{
return (is_bool($info[$field]) && true === $info[$field])
|| (is_int($info[$field]) && 1 === $info[$field]);
}
/**
* bool $sepBySpace is $localeconv['n_sep_by_space']
* int $signPosn = $localeconv['n_sign_posn']
@@ -384,4 +110,278 @@ class Amount
return $posA.$posD.'%v'.$space.$posB.'%s'.$posC.$posE;
}
public function convertToPrimary(?User $user = null): bool
{
$instance = PreferencesSingleton::getInstance();
if (!$user instanceof User) {
$pref = $instance->getPreference('convert_to_primary_no_user');
if (null === $pref) {
$res = true === Preferences::get('convert_to_primary', false)->data && true === config('cer.enabled');
$instance->setPreference('convert_to_primary_no_user', $res);
return $res;
}
return $pref;
}
$key = sprintf('convert_to_primary_%d', $user->id);
$pref = $instance->getPreference($key);
if (null === $pref) {
$res = true === Preferences::getForUser($user, 'convert_to_primary', false)->data && true === config('cer.enabled');
$instance->setPreference($key, $res);
return $res;
}
return $pref;
}
/**
* This method will properly format the given number, in color or "black and white",
* as a currency, given two things: the currency required and the current locale.
*
* @throws FireflyException
*/
public function formatAnything(TransactionCurrency $format, string $amount, ?bool $coloured = null): string
{
return $this->formatFlat($format->symbol, $format->decimal_places, $amount, $coloured);
}
public function formatByCurrencyId(int $currencyId, string $amount, ?bool $coloured = null): string
{
$format = $this->getTransactionCurrencyById($currencyId);
return $this->formatFlat($format->symbol, $format->decimal_places, $amount, $coloured);
}
/**
* This method will properly format the given number, in color or "black and white",
* as a currency, given two things: the currency required and the current locale.
*
* @throws FireflyException
*/
public function formatFlat(string $symbol, int $decimalPlaces, string $amount, ?bool $coloured = null): string
{
$locale = Steam::getLocale();
$rounded = Steam::bcround($amount, $decimalPlaces);
$coloured ??= true;
$fmt = new NumberFormatter($locale, NumberFormatter::CURRENCY);
$fmt->setSymbol(NumberFormatter::CURRENCY_SYMBOL, $symbol);
$fmt->setAttribute(NumberFormatter::MIN_FRACTION_DIGITS, $decimalPlaces);
$fmt->setAttribute(NumberFormatter::MAX_FRACTION_DIGITS, $decimalPlaces);
$result = (string)$fmt->format((float)$rounded); // intentional float
if (true === $coloured) {
if (1 === bccomp($rounded, '0')) {
return sprintf('<span class="text-success money-positive">%s</span>', $result);
}
if (-1 === bccomp($rounded, '0')) {
return sprintf('<span class="text-danger money-negative">%s</span>', $result);
}
return sprintf('<span class="money-neutral">%s</span>', $result);
}
return $result;
}
public function getAllCurrencies(): Collection
{
return TransactionCurrency::orderBy('code', 'ASC')->get();
}
/**
* Experimental function to see if we can quickly and quietly get the amount from a journal.
* This depends on the user's default currency and the wish to have it converted.
*/
public function getAmountFromJournal(array $journal): string
{
$convertToPrimary = $this->convertToPrimary();
$currency = $this->getPrimaryCurrency();
$field = $convertToPrimary && $currency->id !== $journal['currency_id'] ? 'pc_amount' : 'amount';
$amount = $journal[$field] ?? '0';
// Log::debug(sprintf('Field is %s, amount is %s', $field, $amount));
// fallback, the transaction has a foreign amount in $currency.
if ($convertToPrimary && null !== $journal['foreign_amount'] && $currency->id === (int)$journal['foreign_currency_id']) {
$amount = $journal['foreign_amount'];
// Log::debug(sprintf('Overruled, amount is now %s', $amount));
}
return (string)$amount;
}
/**
* Experimental function to see if we can quickly and quietly get the amount from a journal.
* This depends on the user's default currency and the wish to have it converted.
*/
public function getAmountFromJournalObject(TransactionJournal $journal): string
{
$convertToPrimary = $this->convertToPrimary();
$currency = $this->getPrimaryCurrency();
$field = $convertToPrimary && $currency->id !== $journal->transaction_currency_id ? 'pc_amount' : 'amount';
/** @var null|Transaction $sourceTransaction */
$sourceTransaction = $journal->transactions()->where('amount', '<', 0)->first();
if (null === $sourceTransaction) {
return '0';
}
$amount = $sourceTransaction->{$field} ?? '0';
if ((int)$sourceTransaction->foreign_currency_id === $currency->id) {
// use foreign amount instead!
$amount = (string)$sourceTransaction->foreign_amount; // hard coded to be foreign amount.
}
return $amount;
}
public function getCurrencies(): Collection
{
/** @var User $user */
$user = auth()->user();
return $user->currencies()->orderBy('code', 'ASC')->get();
}
/**
* This method returns the correct format rules required by accounting.js,
* the library used to format amounts in charts.
*
* Used only in one place.
*
* @throws FireflyException
*/
public function getJsConfig(): array
{
$config = $this->getLocaleInfo();
$negative = self::getAmountJsConfig($config['n_sep_by_space'], $config['n_sign_posn'], $config['negative_sign'], $config['n_cs_precedes']);
$positive = self::getAmountJsConfig($config['p_sep_by_space'], $config['p_sign_posn'], $config['positive_sign'], $config['p_cs_precedes']);
return [
'mon_decimal_point' => $config['mon_decimal_point'],
'mon_thousands_sep' => $config['mon_thousands_sep'],
'format' => [
'pos' => $positive,
'neg' => $negative,
'zero' => $positive,
],
];
}
public function getPrimaryCurrency(): TransactionCurrency
{
if (auth()->check()) {
/** @var User $user */
$user = auth()->user();
if (null !== $user->userGroup) {
return $this->getPrimaryCurrencyByUserGroup($user->userGroup);
}
}
return $this->getSystemCurrency();
}
public function getPrimaryCurrencyByUserGroup(UserGroup $userGroup): TransactionCurrency
{
$cache = new CacheProperties();
$cache->addProperty('getPrimaryCurrencyByGroup');
$cache->addProperty($userGroup->id);
if ($cache->has()) {
return $cache->get();
}
/** @var null|TransactionCurrency $primary */
$primary = $userGroup->currencies()->where('group_default', true)->first();
if (null === $primary) {
$primary = $this->getSystemCurrency();
// could be the user group has no default right now.
$userGroup->currencies()->sync([$primary->id => ['group_default' => true]]);
}
$cache->store($primary);
return $primary;
}
public function getSystemCurrency(): TransactionCurrency
{
return TransactionCurrency::whereNull('deleted_at')->where('code', 'EUR')->first();
}
public function getTransactionCurrencyByCode(string $code): TransactionCurrency
{
$instance = PreferencesSingleton::getInstance();
$key = sprintf('transaction_currency_%s', $code);
/** @var null|TransactionCurrency $pref */
$pref = $instance->getPreference($key);
if (null !== $pref) {
return $pref;
}
$currency = TransactionCurrency::whereCode($code)->first();
if (null === $currency) {
$message = sprintf('Could not find a transaction currency with code "%s" in %s', $code, __METHOD__);
Log::error($message);
throw new FireflyException($message);
}
$instance->setPreference($key, $currency);
return $currency;
}
public function getTransactionCurrencyById(int $currencyId): TransactionCurrency
{
$instance = PreferencesSingleton::getInstance();
$key = sprintf('transaction_currency_%d', $currencyId);
/** @var null|TransactionCurrency $pref */
$pref = $instance->getPreference($key);
if (null !== $pref) {
return $pref;
}
$currency = TransactionCurrency::find($currencyId);
if (null === $currency) {
$message = sprintf('Could not find a transaction currency with ID #%d in %s', $currencyId, __METHOD__);
Log::error($message);
throw new FireflyException($message);
}
$instance->setPreference($key, $currency);
return $currency;
}
private function getLocaleField(array $info, string $field): bool
{
return (is_bool($info[$field]) && true === $info[$field])
|| (is_int($info[$field]) && 1 === $info[$field]);
}
/**
* @throws FireflyException
*/
private function getLocaleInfo(): array
{
// get config from preference, not from translation:
$locale = Steam::getLocale();
$array = Steam::getLocaleArray($locale);
setlocale(LC_MONETARY, $array);
$info = localeconv();
// correct variables
$info['n_cs_precedes'] = $this->getLocaleField($info, 'n_cs_precedes');
$info['p_cs_precedes'] = $this->getLocaleField($info, 'p_cs_precedes');
$info['n_sep_by_space'] = $this->getLocaleField($info, 'n_sep_by_space');
$info['p_sep_by_space'] = $this->getLocaleField($info, 'p_sep_by_space');
$fmt = new NumberFormatter($locale, NumberFormatter::CURRENCY);
$info['mon_decimal_point'] = $fmt->getSymbol(NumberFormatter::MONETARY_SEPARATOR_SYMBOL);
$info['mon_thousands_sep'] = $fmt->getSymbol(NumberFormatter::MONETARY_GROUPING_SEPARATOR_SYMBOL);
return $info;
}
}

View File

@@ -86,7 +86,7 @@ class RemoteUserGuard implements Guard
$header = config('auth.guard_email');
if (null !== $header) {
$emailAddress = (string) (request()->server($header) ?? apache_request_headers()[$header] ?? null);
$emailAddress = (string)(request()->server($header) ?? apache_request_headers()[$header] ?? null);
$preference = Preferences::getForUser($retrievedUser, 'remote_guard_alt_email');
if ('' !== $emailAddress && null === $preference && $emailAddress !== $userID) {
@@ -102,13 +102,6 @@ class RemoteUserGuard implements Guard
$this->user = $retrievedUser;
}
public function guest(): bool
{
Log::debug(sprintf('Now at %s', __METHOD__));
return !$this->check();
}
public function check(): bool
{
Log::debug(sprintf('Now at %s', __METHOD__));
@@ -116,17 +109,11 @@ class RemoteUserGuard implements Guard
return $this->user() instanceof User;
}
public function user(): ?User
public function guest(): bool
{
Log::debug(sprintf('Now at %s', __METHOD__));
$user = $this->user;
if (!$user instanceof User) {
Log::debug('User is NULL');
return null;
}
return $user;
return !$this->check();
}
public function hasUser(): bool
@@ -157,6 +144,19 @@ class RemoteUserGuard implements Guard
Log::error(sprintf('Did not set user at %s', __METHOD__));
}
public function user(): ?User
{
Log::debug(sprintf('Now at %s', __METHOD__));
$user = $this->user;
if (!$user instanceof User) {
Log::debug('User is NULL');
return null;
}
return $user;
}
/**
* @throws FireflyException
*

View File

@@ -59,8 +59,8 @@ class Balance
$result = $query->get(['transactions.account_id', 'transactions.transaction_currency_id', 'transactions.balance_after']);
foreach ($result as $entry) {
$accountId = (int) $entry->account_id;
$currencyId = (int) $entry->transaction_currency_id;
$accountId = (int)$entry->account_id;
$currencyId = (int)$entry->transaction_currency_id;
$currencies[$currencyId] ??= Amount::getTransactionCurrencyById($currencyId);
$return[$accountId] ??= [];
if (array_key_exists($currencyId, $return[$accountId])) {

View File

@@ -68,7 +68,7 @@ class TagList implements BinderInterface
return true;
}
if (in_array((string) $tag->id, $list, true)) {
if (in_array((string)$tag->id, $list, true)) {
Log::debug(sprintf('TagList: (id) found tag #%d ("%s") in list.', $tag->id, $tag->tag));
return true;

View File

@@ -42,7 +42,7 @@ class TagOrId implements BinderInterface
$result = $repository->findByTag($value);
if (null === $result) {
$result = $repository->find((int) $value);
$result = $repository->find((int)$value);
}
if (null !== $result) {
return $result;

View File

@@ -41,7 +41,7 @@ class UserGroupAccount implements BinderInterface
if (auth()->check()) {
/** @var User $user */
$user = auth()->user();
$account = Account::where('id', (int) $value)
$account = Account::where('id', (int)$value)
->where('user_group_id', $user->user_group_id)
->first()
;

View File

@@ -41,7 +41,7 @@ class UserGroupBill implements BinderInterface
if (auth()->check()) {
/** @var User $user */
$user = auth()->user();
$currency = Bill::where('id', (int) $value)
$currency = Bill::where('id', (int)$value)
->where('user_group_id', $user->user_group_id)
->first()
;

View File

@@ -38,7 +38,7 @@ class UserGroupExchangeRate implements BinderInterface
if (auth()->check()) {
/** @var User $user */
$user = auth()->user();
$rate = CurrencyExchangeRate::where('id', (int) $value)
$rate = CurrencyExchangeRate::where('id', (int)$value)
->where('user_group_id', $user->user_group_id)
->first()
;

View File

@@ -38,7 +38,7 @@ class UserGroupTransaction implements BinderInterface
if (auth()->check()) {
/** @var User $user */
$user = auth()->user();
$group = TransactionGroup::where('id', (int) $value)
$group = TransactionGroup::where('id', (int)$value)
->where('user_group_id', $user->user_group_id)
->first()
;

View File

@@ -78,6 +78,14 @@ class CacheProperties
return Cache::has($this->hash);
}
/**
* @param mixed $data
*/
public function store($data): void
{
Cache::forever($this->hash, $data);
}
private function hash(): void
{
$content = '';
@@ -86,17 +94,9 @@ class CacheProperties
$content = sprintf('%s%s', $content, json_encode($property, JSON_THROW_ON_ERROR));
} catch (JsonException) {
// @ignoreException
$content = sprintf('%s%s', $content, hash('sha256', (string) Carbon::now()->getTimestamp()));
$content = sprintf('%s%s', $content, hash('sha256', (string)Carbon::now()->getTimestamp()));
}
}
$this->hash = substr(hash('sha256', $content), 0, 16);
}
/**
* @param mixed $data
*/
public function store($data): void
{
Cache::forever($this->hash, $data);
}
}

View File

@@ -37,27 +37,6 @@ class Calculator
private static ?SplObjectStorage $intervalMap = null; // @phpstan-ignore-line
private static array $intervals = [];
/**
* @throws IntervalException
*/
public function nextDateByInterval(Carbon $epoch, Periodicity $periodicity, int $skipInterval = 0): Carbon
{
if (!self::isAvailablePeriodicity($periodicity)) {
throw IntervalException::unavailable($periodicity, self::$intervals);
}
/** @var Periodicity\Interval $periodicity */
$periodicity = self::$intervalMap->offsetGet($periodicity);
$interval = $this->skipInterval($skipInterval);
return $periodicity->nextDate($epoch->clone(), $interval);
}
public function isAvailablePeriodicity(Periodicity $periodicity): bool
{
return self::containsInterval($periodicity);
}
private static function containsInterval(Periodicity $periodicity): bool
{
return self::loadIntervalMap()->contains($periodicity);
@@ -78,6 +57,27 @@ class Calculator
return self::$intervalMap;
}
public function isAvailablePeriodicity(Periodicity $periodicity): bool
{
return self::containsInterval($periodicity);
}
/**
* @throws IntervalException
*/
public function nextDateByInterval(Carbon $epoch, Periodicity $periodicity, int $skipInterval = 0): Carbon
{
if (!self::isAvailablePeriodicity($periodicity)) {
throw IntervalException::unavailable($periodicity, self::$intervals);
}
/** @var Periodicity\Interval $periodicity */
$periodicity = self::$intervalMap->offsetGet($periodicity);
$interval = $this->skipInterval($skipInterval);
return $periodicity->nextDate($epoch->clone(), $interval);
}
private function skipInterval(int $skip): int
{
return self::DEFAULT_INTERVAL + $skip;

View File

@@ -69,9 +69,9 @@ class FrontpageChartGenerator
Log::debug('Now in generate for budget chart.');
$budgets = $this->budgetRepository->getActiveBudgets();
$data = [
['label' => (string) trans('firefly.spent_in_budget'), 'entries' => [], 'type' => 'bar'],
['label' => (string) trans('firefly.left_to_spend'), 'entries' => [], 'type' => 'bar'],
['label' => (string) trans('firefly.overspent'), 'entries' => [], 'type' => 'bar'],
['label' => (string)trans('firefly.spent_in_budget'), 'entries' => [], 'type' => 'bar'],
['label' => (string)trans('firefly.left_to_spend'), 'entries' => [], 'type' => 'bar'],
['label' => (string)trans('firefly.overspent'), 'entries' => [], 'type' => 'bar'],
];
// loop al budgets:
@@ -84,6 +84,64 @@ class FrontpageChartGenerator
return $data;
}
public function setEnd(Carbon $end): void
{
$this->end = $end;
}
public function setStart(Carbon $start): void
{
$this->start = $start;
}
/**
* A basic setter for the user. Also updates the repositories with the right user.
*/
public function setUser(User $user): void
{
$this->budgetRepository->setUser($user);
$this->blRepository->setUser($user);
$this->opsRepository->setUser($user);
$locale = app('steam')->getLocale();
$this->monthAndDayFormat = (string)trans('config.month_and_day_js', [], $locale);
}
/**
* If a budget has budget limit, each limit is processed individually.
*/
private function budgetLimits(array $data, Budget $budget, Collection $limits): array
{
Log::debug('Start processing budget limits.');
/** @var BudgetLimit $limit */
foreach ($limits as $limit) {
$data = $this->processLimit($data, $budget, $limit);
}
Log::debug('Done processing budget limits.');
return $data;
}
/**
* When no limits are present, the expenses of the whole period are collected and grouped.
* This is grouped per currency. Because there is no limit set, "left to spend" and "overspent" are empty.
*/
private function noBudgetLimits(array $data, Budget $budget): array
{
$spent = $this->opsRepository->sumExpenses($this->start, $this->end, null, new Collection()->push($budget));
/** @var array $entry */
foreach ($spent as $entry) {
$title = sprintf('%s (%s)', $budget->name, $entry['currency_name']);
$data[0]['entries'][$title] = bcmul((string)$entry['sum'], '-1'); // spent
$data[1]['entries'][$title] = 0; // left to spend
$data[2]['entries'][$title] = 0; // overspent
}
return $data;
}
/**
* For each budget, gets all budget limits for the current time range.
* When no limits are present, the time range is used to collect information on money spent.
@@ -108,41 +166,6 @@ class FrontpageChartGenerator
return $result;
}
/**
* When no limits are present, the expenses of the whole period are collected and grouped.
* This is grouped per currency. Because there is no limit set, "left to spend" and "overspent" are empty.
*/
private function noBudgetLimits(array $data, Budget $budget): array
{
$spent = $this->opsRepository->sumExpenses($this->start, $this->end, null, new Collection()->push($budget));
/** @var array $entry */
foreach ($spent as $entry) {
$title = sprintf('%s (%s)', $budget->name, $entry['currency_name']);
$data[0]['entries'][$title] = bcmul((string) $entry['sum'], '-1'); // spent
$data[1]['entries'][$title] = 0; // left to spend
$data[2]['entries'][$title] = 0; // overspent
}
return $data;
}
/**
* If a budget has budget limit, each limit is processed individually.
*/
private function budgetLimits(array $data, Budget $budget, Collection $limits): array
{
Log::debug('Start processing budget limits.');
/** @var BudgetLimit $limit */
foreach ($limits as $limit) {
$data = $this->processLimit($data, $budget, $limit);
}
Log::debug('Done processing budget limits.');
return $data;
}
/**
* For each limit, the expenses from the time range of the limit are collected. Each row from the result is
* processed individually.
@@ -204,14 +227,14 @@ class FrontpageChartGenerator
Log::debug(sprintf('Amount is now "%s".', $amount));
}
$amount ??= '0';
$sumSpent = bcmul((string) $entry['sum'], '-1'); // spent
$sumSpent = bcmul((string)$entry['sum'], '-1'); // spent
$data[0]['entries'][$title] ??= '0';
$data[1]['entries'][$title] ??= '0';
$data[2]['entries'][$title] ??= '0';
$data[0]['entries'][$title] = bcadd((string) $data[0]['entries'][$title], 1 === bccomp($sumSpent, $amount) ? $amount : $sumSpent); // spent
$data[1]['entries'][$title] = bcadd((string) $data[1]['entries'][$title], 1 === bccomp($amount, $sumSpent) ? bcadd((string) $entry['sum'], $amount) : '0'); // left to spent
$data[2]['entries'][$title] = bcadd((string) $data[2]['entries'][$title], 1 === bccomp($amount, $sumSpent) ? '0' : bcmul(bcadd((string) $entry['sum'], $amount), '-1')); // overspent
$data[0]['entries'][$title] = bcadd((string)$data[0]['entries'][$title], 1 === bccomp($sumSpent, $amount) ? $amount : $sumSpent); // spent
$data[1]['entries'][$title] = bcadd((string)$data[1]['entries'][$title], 1 === bccomp($amount, $sumSpent) ? bcadd((string)$entry['sum'], $amount) : '0'); // left to spent
$data[2]['entries'][$title] = bcadd((string)$data[2]['entries'][$title], 1 === bccomp($amount, $sumSpent) ? '0' : bcmul(bcadd((string)$entry['sum'], $amount), '-1')); // overspent
Log::debug(sprintf('Amount [spent] is now %s.', $data[0]['entries'][$title]));
Log::debug(sprintf('Amount [left] is now %s.', $data[1]['entries'][$title]));
@@ -219,27 +242,4 @@ class FrontpageChartGenerator
return $data;
}
public function setEnd(Carbon $end): void
{
$this->end = $end;
}
public function setStart(Carbon $start): void
{
$this->start = $start;
}
/**
* A basic setter for the user. Also updates the repositories with the right user.
*/
public function setUser(User $user): void
{
$this->budgetRepository->setUser($user);
$this->blRepository->setUser($user);
$this->opsRepository->setUser($user);
$locale = app('steam')->getLocale();
$this->monthAndDayFormat = (string) trans('config.month_and_day_js', [], $locale);
}
}

View File

@@ -26,7 +26,6 @@ namespace FireflyIII\Support\Chart\Category;
use Carbon\Carbon;
use FireflyIII\Enums\AccountTypeEnum;
use FireflyIII\Models\Category;
use FireflyIII\Models\TransactionCurrency;
use FireflyIII\Repositories\Account\AccountRepositoryInterface;
use FireflyIII\Repositories\Category\CategoryRepositoryInterface;
@@ -96,6 +95,30 @@ class FrontpageChartGenerator
];
}
private function collectExpensesAll(Collection $categories, Collection $accounts): array
{
Log::debug(sprintf('Collect expenses for %d category(ies).', count($categories)));
$spent = $this->opsRepos->collectExpenses($this->start, $this->end, $accounts, $categories);
$tempData = [];
foreach ($categories as $category) {
$sums = $this->opsRepos->sumCollectedTransactionsByCategory($spent, $category, 'negative', $this->convertToPrimary);
if (0 === count($sums)) {
continue;
}
foreach ($sums as $currency) {
$this->addCurrency($currency);
$tempData[] = [
'name' => $category->name,
'sum' => $currency['sum'],
'sum_float' => round((float)$currency['sum'], $currency['currency_decimal_places']),
'currency_id' => (int)$currency['currency_id'],
];
}
}
return $tempData;
}
private function collectNoCatExpenses(Collection $accounts): array
{
$noCatExp = $this->noCatRepos->sumExpenses($this->start, $this->end, $accounts);
@@ -147,28 +170,4 @@ class FrontpageChartGenerator
return $currencyData;
}
private function collectExpensesAll(Collection $categories, Collection $accounts): array
{
Log::debug(sprintf('Collect expenses for %d category(ies).', count($categories)));
$spent = $this->opsRepos->collectExpenses($this->start, $this->end, $accounts, $categories);
$tempData = [];
foreach ($categories as $category) {
$sums = $this->opsRepos->sumCollectedTransactionsByCategory($spent, $category, 'negative', $this->convertToPrimary);
if (0 === count($sums)) {
continue;
}
foreach ($sums as $currency) {
$this->addCurrency($currency);
$tempData[] = [
'name' => $category->name,
'sum' => $currency['sum'],
'sum_float' => round((float)$currency['sum'], $currency['currency_decimal_places']),
'currency_id' => (int)$currency['currency_id'],
];
}
}
return $tempData;
}
}

View File

@@ -73,14 +73,14 @@ class WholePeriodChartGenerator
$code = $currency['currency_code'];
$name = $currency['currency_name'];
$chartData[sprintf('spent-in-%s', $code)] = [
'label' => (string) trans('firefly.box_spent_in_currency', ['currency' => $name]),
'label' => (string)trans('firefly.box_spent_in_currency', ['currency' => $name]),
'entries' => [],
'type' => 'bar',
'backgroundColor' => 'rgba(219, 68, 55, 0.5)', // red
];
$chartData[sprintf('earned-in-%s', $code)] = [
'label' => (string) trans('firefly.box_earned_in_currency', ['currency' => $name]),
'label' => (string)trans('firefly.box_earned_in_currency', ['currency' => $name]),
'entries' => [],
'type' => 'bar',
'backgroundColor' => 'rgba(0, 141, 76, 0.5)', // green

View File

@@ -44,10 +44,10 @@ class ChartData
public function add(array $data): void
{
if (array_key_exists('currency_id', $data)) {
$data['currency_id'] = (string) $data['currency_id'];
$data['currency_id'] = (string)$data['currency_id'];
}
if (array_key_exists('primary_currency_id', $data)) {
$data['primary_currency_id'] = (string) $data['primary_currency_id'];
$data['primary_currency_id'] = (string)$data['primary_currency_id'];
}
$required = ['start', 'date', 'end', 'entries'];
foreach ($required as $field) {

View File

@@ -39,7 +39,7 @@ class AutoBudgetCronjob extends AbstractCronjob
{
/** @var Configuration $config */
$config = FireflyConfig::get('last_ab_job', 0);
$lastTime = (int) $config->data;
$lastTime = (int)$config->data;
$diff = now(config('app.timezone'))->getTimestamp() - $lastTime;
$diffForHumans = now(config('app.timezone'))->diffForHumans(Carbon::createFromTimestamp($lastTime), null, true);
if (0 === $lastTime) {
@@ -80,7 +80,7 @@ class AutoBudgetCronjob extends AbstractCronjob
$this->jobSucceeded = true;
$this->message = 'Auto-budget cron job fired successfully.';
FireflyConfig::set('last_ab_job', (int) $this->date->format('U'));
FireflyConfig::set('last_ab_job', (int)$this->date->format('U'));
Log::info('Done with auto budget cron job task.');
}
}

View File

@@ -45,7 +45,7 @@ class BillWarningCronjob extends AbstractCronjob
/** @var Configuration $config */
$config = FireflyConfig::get('last_bw_job', 0);
$lastTime = (int) $config->data;
$lastTime = (int)$config->data;
$diff = now(config('app.timezone'))->getTimestamp() - $lastTime;
$diffForHumans = now(config('app.timezone'))->diffForHumans(Carbon::createFromTimestamp($lastTime), null, true);
@@ -93,8 +93,8 @@ class BillWarningCronjob extends AbstractCronjob
$this->jobSucceeded = true;
$this->message = 'Bill notification cron job fired successfully.';
FireflyConfig::set('last_bw_job', (int) $this->date->format('U'));
Log::info(sprintf('Marked the last time this job has run as "%s" (%d)', $this->date->format('Y-m-d H:i:s'), (int) $this->date->format('U')));
FireflyConfig::set('last_bw_job', (int)$this->date->format('U'));
Log::info(sprintf('Marked the last time this job has run as "%s" (%d)', $this->date->format('Y-m-d H:i:s'), (int)$this->date->format('U')));
Log::info('Done with bill notification cron job task.');
}
}

View File

@@ -39,7 +39,7 @@ class ExchangeRatesCronjob extends AbstractCronjob
{
/** @var Configuration $config */
$config = FireflyConfig::get('last_cer_job', 0);
$lastTime = (int) $config->data;
$lastTime = (int)$config->data;
$diff = now(config('app.timezone'))->getTimestamp() - $lastTime;
$diffForHumans = now(config('app.timezone'))->diffForHumans(Carbon::createFromTimestamp($lastTime), null, true);
if (0 === $lastTime) {
@@ -81,7 +81,7 @@ class ExchangeRatesCronjob extends AbstractCronjob
$this->jobSucceeded = true;
$this->message = 'Exchange rates cron job fired successfully.';
FireflyConfig::set('last_cer_job', (int) $this->date->format('U'));
FireflyConfig::set('last_cer_job', (int)$this->date->format('U'));
Log::info('Done with exchange rates job task.');
}
}

View File

@@ -45,7 +45,7 @@ class RecurringCronjob extends AbstractCronjob
/** @var Configuration $config */
$config = FireflyConfig::get('last_rt_job', 0);
$lastTime = (int) $config->data;
$lastTime = (int)$config->data;
$diff = now(config('app.timezone'))->getTimestamp() - $lastTime;
$diffForHumans = now(config('app.timezone'))->diffForHumans(Carbon::createFromTimestamp($lastTime), null, true);
@@ -90,8 +90,8 @@ class RecurringCronjob extends AbstractCronjob
$this->jobSucceeded = true;
$this->message = 'Recurring transactions cron job fired successfully.';
FireflyConfig::set('last_rt_job', (int) $this->date->format('U'));
Log::info(sprintf('Marked the last time this job has run as "%s" (%d)', $this->date->format('Y-m-d H:i:s'), (int) $this->date->format('U')));
FireflyConfig::set('last_rt_job', (int)$this->date->format('U'));
Log::info(sprintf('Marked the last time this job has run as "%s" (%d)', $this->date->format('Y-m-d H:i:s'), (int)$this->date->format('U')));
Log::info('Done with recurring cron job task.');
}
}

View File

@@ -42,7 +42,7 @@ class UpdateCheckCronjob extends AbstractCronjob
// should not check for updates:
$permission = FireflyConfig::get('permission_update_check', -1);
$value = (int) $permission->data;
$value = (int)$permission->data;
if (1 !== $value) {
Log::debug('Update check is not enabled.');
// get stuff from job:

View File

@@ -45,7 +45,7 @@ class WebhookCronjob extends AbstractCronjob
/** @var Configuration $config */
$config = FireflyConfig::get('last_webhook_job', 0);
$lastTime = (int) $config->data;
$lastTime = (int)$config->data;
$diff = now(config('app.timezone'))->getTimestamp() - $lastTime;
$diffForHumans = now(config('app.timezone'))->diffForHumans(Carbon::createFromTimestamp($lastTime), null, true);
@@ -90,8 +90,8 @@ class WebhookCronjob extends AbstractCronjob
$this->jobSucceeded = true;
$this->message = 'Send webhook messages cron job fired successfully.';
FireflyConfig::set('last_webhook_job', (int) $this->date->format('U'));
Log::info(sprintf('Marked the last time this job has run as "%s" (%d)', $this->date->format('Y-m-d H:i:s'), (int) $this->date->format('U')));
FireflyConfig::set('last_webhook_job', (int)$this->date->format('U'));
Log::info(sprintf('Marked the last time this job has run as "%s" (%d)', $this->date->format('Y-m-d H:i:s'), (int)$this->date->format('U')));
Log::info('Done with webhook cron job task.');
}
}

View File

@@ -28,8 +28,8 @@ use Illuminate\Support\Facades\Log;
class Timer
{
private array $times = [];
private static ?Timer $instance = null;
private array $times = [];
private function __construct()
{
@@ -38,7 +38,7 @@ class Timer
public static function getInstance(): self
{
if (null === self::$instance) {
if (!self::$instance instanceof self) {
self::$instance = new self();
}

View File

@@ -23,9 +23,9 @@ declare(strict_types=1);
namespace FireflyIII\Support;
use Illuminate\Database\Eloquent\Model;
use FireflyIII\Exceptions\FireflyException;
use FireflyIII\Support\Form\FormSupport;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Collection;
use Throwable;

View File

@@ -85,7 +85,7 @@ class ExportDataGenerator
private bool $exportTransactions;
private Carbon $start;
private User $user;
private UserGroup $userGroup; // @phpstan-ignore-line
private UserGroup $userGroup; // @phpstan-ignore-line
public function __construct()
{
@@ -141,6 +141,92 @@ class ExportDataGenerator
return $return;
}
/**
* @SuppressWarnings("PHPMD.UnusedFormalParameter")
*/
public function get(string $key, mixed $default = null): mixed
{
return null;
}
/**
* @SuppressWarnings("PHPMD.UnusedFormalParameter")
*/
public function has(mixed $key): mixed
{
return null;
}
public function setAccounts(Collection $accounts): void
{
$this->accounts = $accounts;
}
public function setEnd(Carbon $end): void
{
$this->end = $end;
}
public function setExportAccounts(bool $exportAccounts): void
{
$this->exportAccounts = $exportAccounts;
}
public function setExportBills(bool $exportBills): void
{
$this->exportBills = $exportBills;
}
public function setExportBudgets(bool $exportBudgets): void
{
$this->exportBudgets = $exportBudgets;
}
public function setExportCategories(bool $exportCategories): void
{
$this->exportCategories = $exportCategories;
}
public function setExportPiggies(bool $exportPiggies): void
{
$this->exportPiggies = $exportPiggies;
}
public function setExportRecurring(bool $exportRecurring): void
{
$this->exportRecurring = $exportRecurring;
}
public function setExportRules(bool $exportRules): void
{
$this->exportRules = $exportRules;
}
public function setExportTags(bool $exportTags): void
{
$this->exportTags = $exportTags;
}
public function setExportTransactions(bool $exportTransactions): void
{
$this->exportTransactions = $exportTransactions;
}
public function setStart(Carbon $start): void
{
$this->start = $start;
}
public function setUser(User $user): void
{
$this->user = $user;
}
public function setUserGroup(UserGroup $userGroup): void
{
$this->userGroup = $userGroup;
}
/**
* @throws CannotInsertRecord
* @throws Exception
@@ -222,11 +308,6 @@ class ExportDataGenerator
return $string;
}
public function setUser(User $user): void
{
$this->user = $user;
}
/**
* @throws CannotInsertRecord
* @throws Exception
@@ -588,14 +669,6 @@ class ExportDataGenerator
return $string;
}
/**
* @SuppressWarnings("PHPMD.UnusedFormalParameter")
*/
public function get(string $key, mixed $default = null): mixed
{
return null;
}
/**
* @throws CannotInsertRecord
* @throws Exception
@@ -828,11 +901,6 @@ class ExportDataGenerator
return $string;
}
public function setAccounts(Collection $accounts): void
{
$this->accounts = $accounts;
}
private function mergeTags(array $tags): string
{
if (0 === count($tags)) {
@@ -845,72 +913,4 @@ class ExportDataGenerator
return implode(',', $smol);
}
/**
* @SuppressWarnings("PHPMD.UnusedFormalParameter")
*/
public function has(mixed $key): mixed
{
return null;
}
public function setEnd(Carbon $end): void
{
$this->end = $end;
}
public function setExportAccounts(bool $exportAccounts): void
{
$this->exportAccounts = $exportAccounts;
}
public function setExportBills(bool $exportBills): void
{
$this->exportBills = $exportBills;
}
public function setExportBudgets(bool $exportBudgets): void
{
$this->exportBudgets = $exportBudgets;
}
public function setExportCategories(bool $exportCategories): void
{
$this->exportCategories = $exportCategories;
}
public function setExportPiggies(bool $exportPiggies): void
{
$this->exportPiggies = $exportPiggies;
}
public function setExportRecurring(bool $exportRecurring): void
{
$this->exportRecurring = $exportRecurring;
}
public function setExportRules(bool $exportRules): void
{
$this->exportRules = $exportRules;
}
public function setExportTags(bool $exportTags): void
{
$this->exportTags = $exportTags;
}
public function setExportTransactions(bool $exportTransactions): void
{
$this->exportTransactions = $exportTransactions;
}
public function setStart(Carbon $start): void
{
$this->start = $start;
}
public function setUserGroup(UserGroup $userGroup): void
{
$this->userGroup = $userGroup;
}
}

View File

@@ -23,6 +23,7 @@ declare(strict_types=1);
namespace FireflyIII\Support;
use Exception;
use FireflyIII\Exceptions\FireflyException;
use FireflyIII\Models\Configuration;
use Illuminate\Contracts\Encryption\DecryptException;
@@ -30,7 +31,6 @@ use Illuminate\Contracts\Encryption\EncryptException;
use Illuminate\Database\QueryException;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Log;
use Exception;
/**
* Class FireflyConfig.
@@ -46,34 +46,6 @@ class FireflyConfig
Configuration::where('name', $name)->forceDelete();
}
public function has(string $name): bool
{
return 1 === Configuration::where('name', $name)->count();
}
public function getEncrypted(string $name, mixed $default = null): ?Configuration
{
$result = $this->get($name, $default);
if (!$result instanceof Configuration) {
return null;
}
if ('' === $result->data) {
Log::warning(sprintf('Empty encrypted configuration value found: "%s"', $name));
return $result;
}
try {
$result->data = decrypt($result->data);
} catch (DecryptException $e) {
Log::error(sprintf('Could not decrypt configuration value "%s": %s', $name, $e->getMessage()));
return $result;
}
return $result;
}
/**
* @param null|bool|int|string $default
*
@@ -106,6 +78,56 @@ class FireflyConfig
return $this->set($name, $default);
}
public function getEncrypted(string $name, mixed $default = null): ?Configuration
{
$result = $this->get($name, $default);
if (!$result instanceof Configuration) {
return null;
}
if ('' === $result->data) {
Log::warning(sprintf('Empty encrypted configuration value found: "%s"', $name));
return $result;
}
try {
$result->data = decrypt($result->data);
} catch (DecryptException $e) {
Log::error(sprintf('Could not decrypt configuration value "%s": %s', $name, $e->getMessage()));
return $result;
}
return $result;
}
public function getFresh(string $name, mixed $default = null): ?Configuration
{
$config = Configuration::where('name', $name)->first(['id', 'name', 'data']);
if (null !== $config) {
return $config;
}
// no preference found and default is null:
if (null === $default) {
return null;
}
return $this->set($name, $default);
}
public function has(string $name): bool
{
return 1 === Configuration::where('name', $name)->count();
}
/**
* @param mixed $value
*/
public function put(string $name, $value): Configuration
{
return $this->set($name, $value);
}
public function set(string $name, mixed $value): Configuration
{
try {
@@ -135,28 +157,6 @@ class FireflyConfig
return $config;
}
public function getFresh(string $name, mixed $default = null): ?Configuration
{
$config = Configuration::where('name', $name)->first(['id', 'name', 'data']);
if (null !== $config) {
return $config;
}
// no preference found and default is null:
if (null === $default) {
return null;
}
return $this->set($name, $default);
}
/**
* @param mixed $value
*/
public function put(string $name, $value): Configuration
{
return $this->set($name, $value);
}
public function setEncrypted(string $name, mixed $value): Configuration
{
try {

View File

@@ -51,43 +51,12 @@ class AccountForm
$repository = $this->getAccountRepository();
$grouped = $this->getAccountsGrouped($types, $repository);
$cash = $repository->getCashAccount();
$key = (string) trans('firefly.cash_account_type');
$grouped[$key][$cash->id] = sprintf('(%s)', (string) trans('firefly.cash'));
$key = (string)trans('firefly.cash_account_type');
$grouped[$key][$cash->id] = sprintf('(%s)', (string)trans('firefly.cash'));
return $this->select($name, $grouped, $value, $options);
}
private function getAccountsGrouped(array $types, ?AccountRepositoryInterface $repository = null): array
{
if (!$repository instanceof AccountRepositoryInterface) {
$repository = $this->getAccountRepository();
}
$accountList = $repository->getActiveAccountsByType($types);
$liabilityTypes = [AccountTypeEnum::MORTGAGE->value, AccountTypeEnum::DEBT->value, AccountTypeEnum::CREDITCARD->value, AccountTypeEnum::LOAN->value];
$grouped = [];
/** @var Account $account */
foreach ($accountList as $account) {
$role = (string) $repository->getMetaValue($account, 'account_role');
if (in_array($account->accountType->type, $liabilityTypes, true)) {
$role = sprintf('l_%s', $account->accountType->type);
}
if ('' === $role) {
$role = 'no_account_type';
if (AccountTypeEnum::EXPENSE->value === $account->accountType->type) {
$role = 'expense_account';
}
if (AccountTypeEnum::REVENUE->value === $account->accountType->type) {
$role = 'revenue_account';
}
}
$key = (string) trans(sprintf('firefly.opt_group_%s', $role));
$grouped[$key][$account->id] = $account->name;
}
return $grouped;
}
/**
* Grouped dropdown list of all accounts that are valid as the destination of a withdrawal.
*/
@@ -98,8 +67,8 @@ class AccountForm
$grouped = $this->getAccountsGrouped($types, $repository);
$cash = $repository->getCashAccount();
$key = (string) trans('firefly.cash_account_type');
$grouped[$key][$cash->id] = sprintf('(%s)', (string) trans('firefly.cash'));
$key = (string)trans('firefly.cash_account_type');
$grouped[$key][$cash->id] = sprintf('(%s)', (string)trans('firefly.cash'));
return $this->select($name, $grouped, $value, $options);
}
@@ -173,4 +142,35 @@ class AccountForm
return $this->select($name, $grouped, $value, $options);
}
private function getAccountsGrouped(array $types, ?AccountRepositoryInterface $repository = null): array
{
if (!$repository instanceof AccountRepositoryInterface) {
$repository = $this->getAccountRepository();
}
$accountList = $repository->getActiveAccountsByType($types);
$liabilityTypes = [AccountTypeEnum::MORTGAGE->value, AccountTypeEnum::DEBT->value, AccountTypeEnum::CREDITCARD->value, AccountTypeEnum::LOAN->value];
$grouped = [];
/** @var Account $account */
foreach ($accountList as $account) {
$role = (string)$repository->getMetaValue($account, 'account_role');
if (in_array($account->accountType->type, $liabilityTypes, true)) {
$role = sprintf('l_%s', $account->accountType->type);
}
if ('' === $role) {
$role = 'no_account_type';
if (AccountTypeEnum::EXPENSE->value === $account->accountType->type) {
$role = 'expense_account';
}
if (AccountTypeEnum::REVENUE->value === $account->accountType->type) {
$role = 'revenue_account';
}
}
$key = (string)trans(sprintf('firefly.opt_group_%s', $role));
$grouped[$key][$account->id] = $account->name;
}
return $grouped;
}
}

View File

@@ -49,60 +49,6 @@ class CurrencyForm
return $this->currencyField($name, 'amount', $value, $options);
}
/**
* @phpstan-param view-string $view
*
* @throws FireflyException
*/
protected function currencyField(string $name, string $view, mixed $value = null, ?array $options = null): string
{
$label = $this->label($name, $options);
$options = $this->expandOptionArray($name, $label, $options);
$classes = $this->getHolderClasses($name);
$value = $this->fillFieldValue($name, $value);
$options['step'] = 'any';
$primaryCurrency = $options['currency'] ?? app('amount')->getPrimaryCurrency();
/** @var Collection $currencies */
$currencies = app('amount')->getCurrencies();
unset($options['currency'], $options['placeholder']);
// perhaps the currency has been sent to us in the field $amount_currency_id_$name (amount_currency_id_amount)
$preFilled = session('preFilled');
if (!is_array($preFilled)) {
$preFilled = [];
}
$key = 'amount_currency_id_'.$name;
$sentCurrencyId = array_key_exists($key, $preFilled) ? (int) $preFilled[$key] : $primaryCurrency->id;
app('log')->debug(sprintf('Sent currency ID is %d', $sentCurrencyId));
// find this currency in set of currencies:
foreach ($currencies as $currency) {
if ($currency->id === $sentCurrencyId) {
$primaryCurrency = $currency;
app('log')->debug(sprintf('default currency is now %s', $primaryCurrency->code));
break;
}
}
// make sure value is formatted nicely:
if (null !== $value && '' !== $value) {
$value = app('steam')->bcround($value, $primaryCurrency->decimal_places);
}
try {
$html = view('form.'.$view, compact('primaryCurrency', 'currencies', 'classes', 'name', 'label', 'value', 'options'))->render();
} catch (Throwable $e) {
app('log')->debug(sprintf('Could not render currencyField(): %s', $e->getMessage()));
$html = 'Could not render currencyField.';
throw new FireflyException($html, 0, $e);
}
return $html;
}
/**
* TODO describe and cleanup.
*
@@ -115,63 +61,6 @@ class CurrencyForm
return $this->allCurrencyField($name, 'balance', $value, $options);
}
/**
* TODO describe and cleanup
*
* @param mixed $value
*
* @throws FireflyException
*/
protected function allCurrencyField(string $name, string $view, $value = null, ?array $options = null): string
{
$label = $this->label($name, $options);
$options = $this->expandOptionArray($name, $label, $options);
$classes = $this->getHolderClasses($name);
$value = $this->fillFieldValue($name, $value);
$options['step'] = 'any';
$primaryCurrency = $options['currency'] ?? app('amount')->getPrimaryCurrency();
/** @var Collection $currencies */
$currencies = app('amount')->getAllCurrencies();
unset($options['currency'], $options['placeholder']);
// perhaps the currency has been sent to us in the field $amount_currency_id_$name (amount_currency_id_amount)
$preFilled = session('preFilled');
if (!is_array($preFilled)) {
$preFilled = [];
}
$key = 'amount_currency_id_'.$name;
$sentCurrencyId = array_key_exists($key, $preFilled) ? (int) $preFilled[$key] : $primaryCurrency->id;
app('log')->debug(sprintf('Sent currency ID is %d', $sentCurrencyId));
// find this currency in set of currencies:
foreach ($currencies as $currency) {
if ($currency->id === $sentCurrencyId) {
$primaryCurrency = $currency;
app('log')->debug(sprintf('default currency is now %s', $primaryCurrency->code));
break;
}
}
// make sure value is formatted nicely:
if (null !== $value && '' !== $value) {
$value = app('steam')->bcround($value, $primaryCurrency->decimal_places);
}
try {
$html = view('form.'.$view, compact('primaryCurrency', 'currencies', 'classes', 'name', 'label', 'value', 'options'))->render();
} catch (Throwable $e) {
app('log')->debug(sprintf('Could not render currencyField(): %s', $e->getMessage()));
$html = 'Could not render currencyField.';
throw new FireflyException($html, 0, $e);
}
return $html;
}
/**
* TODO cleanup and describe
*
@@ -207,7 +96,7 @@ class CurrencyForm
// get all currencies:
$list = $currencyRepos->get();
$array = [
0 => (string) trans('firefly.no_currency'),
0 => (string)trans('firefly.no_currency'),
];
/** @var TransactionCurrency $currency */
@@ -217,4 +106,115 @@ class CurrencyForm
return $this->select($name, $array, $value, $options);
}
/**
* TODO describe and cleanup
*
* @param mixed $value
*
* @throws FireflyException
*/
protected function allCurrencyField(string $name, string $view, $value = null, ?array $options = null): string
{
$label = $this->label($name, $options);
$options = $this->expandOptionArray($name, $label, $options);
$classes = $this->getHolderClasses($name);
$value = $this->fillFieldValue($name, $value);
$options['step'] = 'any';
$primaryCurrency = $options['currency'] ?? app('amount')->getPrimaryCurrency();
/** @var Collection $currencies */
$currencies = app('amount')->getAllCurrencies();
unset($options['currency'], $options['placeholder']);
// perhaps the currency has been sent to us in the field $amount_currency_id_$name (amount_currency_id_amount)
$preFilled = session('preFilled');
if (!is_array($preFilled)) {
$preFilled = [];
}
$key = 'amount_currency_id_'.$name;
$sentCurrencyId = array_key_exists($key, $preFilled) ? (int)$preFilled[$key] : $primaryCurrency->id;
app('log')->debug(sprintf('Sent currency ID is %d', $sentCurrencyId));
// find this currency in set of currencies:
foreach ($currencies as $currency) {
if ($currency->id === $sentCurrencyId) {
$primaryCurrency = $currency;
app('log')->debug(sprintf('default currency is now %s', $primaryCurrency->code));
break;
}
}
// make sure value is formatted nicely:
if (null !== $value && '' !== $value) {
$value = app('steam')->bcround($value, $primaryCurrency->decimal_places);
}
try {
$html = view('form.'.$view, compact('primaryCurrency', 'currencies', 'classes', 'name', 'label', 'value', 'options'))->render();
} catch (Throwable $e) {
app('log')->debug(sprintf('Could not render currencyField(): %s', $e->getMessage()));
$html = 'Could not render currencyField.';
throw new FireflyException($html, 0, $e);
}
return $html;
}
/**
* @phpstan-param view-string $view
*
* @throws FireflyException
*/
protected function currencyField(string $name, string $view, mixed $value = null, ?array $options = null): string
{
$label = $this->label($name, $options);
$options = $this->expandOptionArray($name, $label, $options);
$classes = $this->getHolderClasses($name);
$value = $this->fillFieldValue($name, $value);
$options['step'] = 'any';
$primaryCurrency = $options['currency'] ?? app('amount')->getPrimaryCurrency();
/** @var Collection $currencies */
$currencies = app('amount')->getCurrencies();
unset($options['currency'], $options['placeholder']);
// perhaps the currency has been sent to us in the field $amount_currency_id_$name (amount_currency_id_amount)
$preFilled = session('preFilled');
if (!is_array($preFilled)) {
$preFilled = [];
}
$key = 'amount_currency_id_'.$name;
$sentCurrencyId = array_key_exists($key, $preFilled) ? (int)$preFilled[$key] : $primaryCurrency->id;
app('log')->debug(sprintf('Sent currency ID is %d', $sentCurrencyId));
// find this currency in set of currencies:
foreach ($currencies as $currency) {
if ($currency->id === $sentCurrencyId) {
$primaryCurrency = $currency;
app('log')->debug(sprintf('default currency is now %s', $primaryCurrency->code));
break;
}
}
// make sure value is formatted nicely:
if (null !== $value && '' !== $value) {
$value = app('steam')->bcround($value, $primaryCurrency->decimal_places);
}
try {
$html = view('form.'.$view, compact('primaryCurrency', 'currencies', 'classes', 'name', 'label', 'value', 'options'))->render();
} catch (Throwable $e) {
app('log')->debug(sprintf('Could not render currencyField(): %s', $e->getMessage()));
$html = 'Could not render currencyField.';
throw new FireflyException($html, 0, $e);
}
return $html;
}
}

View File

@@ -54,15 +54,26 @@ trait FormSupport
return $html;
}
protected function label(string $name, ?array $options = null): string
/**
* @param mixed $selected
*/
public function select(string $name, ?array $list = null, $selected = null, ?array $options = null): string
{
$options ??= [];
if (array_key_exists('label', $options)) {
return $options['label'];
}
$name = str_replace('[]', '', $name);
$list ??= [];
$label = $this->label($name, $options);
$options = $this->expandOptionArray($name, $label, $options);
$classes = $this->getHolderClasses($name);
$selected = $this->fillFieldValue($name, $selected);
unset($options['autocomplete'], $options['placeholder']);
return (string)trans('form.'.$name);
try {
$html = view('form.select', compact('classes', 'name', 'label', 'selected', 'options', 'list'))->render();
} catch (Throwable $e) {
app('log')->debug(sprintf('Could not render select(): %s', $e->getMessage()));
$html = 'Could not render select.';
}
return $html;
}
/**
@@ -80,19 +91,6 @@ trait FormSupport
return $options;
}
protected function getHolderClasses(string $name): string
{
// Get errors from session:
/** @var null|MessageBag $errors */
$errors = session('errors');
if (null !== $errors && $errors->has($name)) {
return 'form-group has-error has-feedback';
}
return 'form-group';
}
/**
* @param null|mixed $value
*
@@ -116,28 +114,6 @@ trait FormSupport
return $value;
}
/**
* @param mixed $selected
*/
public function select(string $name, ?array $list = null, $selected = null, ?array $options = null): string
{
$list ??= [];
$label = $this->label($name, $options);
$options = $this->expandOptionArray($name, $label, $options);
$classes = $this->getHolderClasses($name);
$selected = $this->fillFieldValue($name, $selected);
unset($options['autocomplete'], $options['placeholder']);
try {
$html = view('form.select', compact('classes', 'name', 'label', 'selected', 'options', 'list'))->render();
} catch (Throwable $e) {
app('log')->debug(sprintf('Could not render select(): %s', $e->getMessage()));
$html = 'Could not render select.';
}
return $html;
}
protected function getAccountRepository(): AccountRepositoryInterface
{
return app(AccountRepositoryInterface::class);
@@ -147,4 +123,28 @@ trait FormSupport
{
return today(config('app.timezone'));
}
protected function getHolderClasses(string $name): string
{
// Get errors from session:
/** @var null|MessageBag $errors */
$errors = session('errors');
if (null !== $errors && $errors->has($name)) {
return 'form-group has-error has-feedback';
}
return 'form-group';
}
protected function label(string $name, ?array $options = null): string
{
$options ??= [];
if (array_key_exists('label', $options)) {
return $options['label'];
}
$name = str_replace('[]', '', $name);
return (string)trans('form.'.$name);
}
}

View File

@@ -47,7 +47,7 @@ class PiggyBankForm
/** @var PiggyBankRepositoryInterface $repository */
$repository = app(PiggyBankRepositoryInterface::class);
$piggyBanks = $repository->getPiggyBanksWithAmount();
$title = (string) trans('firefly.default_group_title_name');
$title = (string)trans('firefly.default_group_title_name');
$array = [];
$subList = [
0 => [
@@ -55,7 +55,7 @@ class PiggyBankForm
'title' => $title,
],
'piggies' => [
(string) trans('firefly.none_in_select_list'),
(string)trans('firefly.none_in_select_list'),
],
],
];

View File

@@ -66,12 +66,12 @@ class RuleForm
// get all currencies:
$list = $groupRepos->get();
$array = [
0 => (string) trans('firefly.none_in_select_list'),
0 => (string)trans('firefly.none_in_select_list'),
];
/** @var RuleGroup $group */
foreach ($list as $group) {
if (array_key_exists('hidden', $options) && (int) $options['hidden'] !== $group->id) {
if (array_key_exists('hidden', $options) && (int)$options['hidden'] !== $group->id) {
$array[$group->id] = $group->title;
}
}

View File

@@ -44,10 +44,10 @@ class AccountBalanceGrouped
private readonly ExchangeRateConverter $converter;
private array $currencies = [];
private array $data = [];
private TransactionCurrency $primary;
private Carbon $end;
private array $journals = [];
private string $preferredRange;
private TransactionCurrency $primary;
private Carbon $start;
public function __construct()
@@ -146,48 +146,49 @@ class AccountBalanceGrouped
$converter->summarize();
}
private function processJournal(array $journal): void
public function setAccounts(Collection $accounts): void
{
// format the date according to the period
$period = $journal['date']->format($this->carbonFormat);
$currencyId = (int)$journal['currency_id'];
$currency = $this->findCurrency($currencyId);
// set the array with monetary info, if it does not exist.
$this->createDefaultDataEntry($journal);
// set the array (in monetary info) with spent/earned in this $period, if it does not exist.
$this->createDefaultPeriodEntry($journal);
// is this journal's amount in- our outgoing?
$key = $this->getDataKey($journal);
$amount = 'spent' === $key ? Steam::negative($journal['amount']) : Steam::positive($journal['amount']);
// get conversion rate
$rate = $this->getRate($currency, $journal['date']);
$amountConverted = bcmul($amount, $rate);
// perhaps transaction already has the foreign amount in the primary currency.
if ((int)$journal['foreign_currency_id'] === $this->primary->id) {
$amountConverted = $journal['foreign_amount'] ?? '0';
$amountConverted = 'earned' === $key ? Steam::positive($amountConverted) : Steam::negative($amountConverted);
}
// add normal entry
$this->data[$currencyId][$period][$key] = bcadd((string)$this->data[$currencyId][$period][$key], $amount);
// add converted entry
$convertedKey = sprintf('pc_%s', $key);
$this->data[$currencyId][$period][$convertedKey] = bcadd((string)$this->data[$currencyId][$period][$convertedKey], $amountConverted);
$this->accountIds = $accounts->pluck('id')->toArray();
}
private function findCurrency(int $currencyId): TransactionCurrency
public function setEnd(Carbon $end): void
{
if (array_key_exists($currencyId, $this->currencies)) {
return $this->currencies[$currencyId];
}
$this->currencies[$currencyId] = Amount::getTransactionCurrencyById($currencyId);
$this->end = $end;
}
return $this->currencies[$currencyId];
public function setJournals(array $journals): void
{
$this->journals = $journals;
}
public function setPreferredRange(string $preferredRange): void
{
$this->preferredRange = $preferredRange;
$this->carbonFormat = Navigation::preferredCarbonFormatByPeriod($preferredRange);
}
public function setPrimary(TransactionCurrency $primary): void
{
$this->primary = $primary;
$primaryCurrencyId = $primary->id;
$this->currencies = [$primary->id => $primary]; // currency cache
$this->data[$primaryCurrencyId] = [
'currency_id' => (string)$primaryCurrencyId,
'currency_symbol' => $primary->symbol,
'currency_code' => $primary->code,
'currency_name' => $primary->name,
'currency_decimal_places' => $primary->decimal_places,
'primary_currency_id' => (string)$primaryCurrencyId,
'primary_currency_symbol' => $primary->symbol,
'primary_currency_code' => $primary->code,
'primary_currency_name' => $primary->name,
'primary_currency_decimal_places' => $primary->decimal_places,
];
}
public function setStart(Carbon $start): void
{
$this->start = $start;
}
private function createDefaultDataEntry(array $journal): void
@@ -220,6 +221,16 @@ class AccountBalanceGrouped
];
}
private function findCurrency(int $currencyId): TransactionCurrency
{
if (array_key_exists($currencyId, $this->currencies)) {
return $this->currencies[$currencyId];
}
$this->currencies[$currencyId] = Amount::getTransactionCurrencyById($currencyId);
return $this->currencies[$currencyId];
}
private function getDataKey(array $journal): string
{
// deposit = incoming
@@ -254,48 +265,37 @@ class AccountBalanceGrouped
return $rate;
}
public function setAccounts(Collection $accounts): void
private function processJournal(array $journal): void
{
$this->accountIds = $accounts->pluck('id')->toArray();
}
// format the date according to the period
$period = $journal['date']->format($this->carbonFormat);
$currencyId = (int)$journal['currency_id'];
$currency = $this->findCurrency($currencyId);
public function setPrimary(TransactionCurrency $primary): void
{
$this->primary = $primary;
$primaryCurrencyId = $primary->id;
$this->currencies = [$primary->id => $primary]; // currency cache
$this->data[$primaryCurrencyId] = [
'currency_id' => (string)$primaryCurrencyId,
'currency_symbol' => $primary->symbol,
'currency_code' => $primary->code,
'currency_name' => $primary->name,
'currency_decimal_places' => $primary->decimal_places,
'primary_currency_id' => (string)$primaryCurrencyId,
'primary_currency_symbol' => $primary->symbol,
'primary_currency_code' => $primary->code,
'primary_currency_name' => $primary->name,
'primary_currency_decimal_places' => $primary->decimal_places,
];
}
// set the array with monetary info, if it does not exist.
$this->createDefaultDataEntry($journal);
// set the array (in monetary info) with spent/earned in this $period, if it does not exist.
$this->createDefaultPeriodEntry($journal);
public function setEnd(Carbon $end): void
{
$this->end = $end;
}
// is this journal's amount in- our outgoing?
$key = $this->getDataKey($journal);
$amount = 'spent' === $key ? Steam::negative($journal['amount']) : Steam::positive($journal['amount']);
public function setJournals(array $journals): void
{
$this->journals = $journals;
}
// get conversion rate
$rate = $this->getRate($currency, $journal['date']);
$amountConverted = bcmul($amount, $rate);
public function setPreferredRange(string $preferredRange): void
{
$this->preferredRange = $preferredRange;
$this->carbonFormat = Navigation::preferredCarbonFormatByPeriod($preferredRange);
}
// perhaps transaction already has the foreign amount in the primary currency.
if ((int)$journal['foreign_currency_id'] === $this->primary->id) {
$amountConverted = $journal['foreign_amount'] ?? '0';
$amountConverted = 'earned' === $key ? Steam::positive($amountConverted) : Steam::negative($amountConverted);
}
public function setStart(Carbon $start): void
{
$this->start = $start;
// add normal entry
$this->data[$currencyId][$period][$key] = bcadd((string)$this->data[$currencyId][$period][$key], $amount);
// add converted entry
$convertedKey = sprintf('pc_%s', $key);
$this->data[$currencyId][$period][$convertedKey] = bcadd((string)$this->data[$currencyId][$period][$convertedKey], $amountConverted);
}
}

View File

@@ -39,7 +39,7 @@ trait CollectsAccountsFromFilter
// always collect from the query parameter, even when it's empty.
if (null !== $queryParameters['accounts']) {
foreach ($queryParameters['accounts'] as $accountId) {
$account = $this->repository->find((int) $accountId);
$account = $this->repository->find((int)$accountId);
if (null !== $account) {
$collection->push($account);
}

View File

@@ -94,6 +94,149 @@ class ExchangeRateConverter
return '0' === $rate ? '1' : $rate;
}
public function setIgnoreSettings(bool $ignoreSettings): void
{
$this->ignoreSettings = $ignoreSettings;
}
public function setUserGroup(UserGroup $userGroup): void
{
$this->userGroup = $userGroup;
}
public function summarize(): void
{
if (false === $this->enabled()) {
return;
}
Log::debug(sprintf('ExchangeRateConverter ran %d queries.', $this->queryCount));
}
private function getCacheKey(TransactionCurrency $from, TransactionCurrency $to, Carbon $date): string
{
return sprintf('cer-%d-%d-%s', $from->id, $to->id, $date->format('Y-m-d'));
}
/**
* @throws FireflyException
*/
private function getEuroId(): int
{
Log::debug('getEuroId()');
$cache = new CacheProperties();
$cache->addProperty('cer-euro-id');
if ($cache->has()) {
return (int)$cache->get();
}
$euro = Amount::getTransactionCurrencyByCode('EUR');
++$this->queryCount;
$cache->store($euro->id);
return $euro->id;
}
/**
* @throws FireflyException
*/
private function getEuroRate(TransactionCurrency $currency, Carbon $date): string
{
$euroId = $this->getEuroId();
if ($euroId === $currency->id) {
return '1';
}
$rate = $this->getFromDB($currency->id, $euroId, $date->format('Y-m-d'));
if (null !== $rate) {
// app('log')->debug(sprintf('Rate for %s to EUR is %s.', $currency->code, $rate));
return $rate;
}
$rate = $this->getFromDB($euroId, $currency->id, $date->format('Y-m-d'));
if (null !== $rate) {
return bcdiv('1', $rate);
// app('log')->debug(sprintf('Inverted rate for %s to EUR is %s.', $currency->code, $rate));
// return $rate;
}
// grab backup values from config file:
$backup = config(sprintf('cer.rates.%s', $currency->code));
if (null !== $backup) {
return bcdiv('1', (string)$backup);
// app('log')->debug(sprintf('Backup rate for %s to EUR is %s.', $currency->code, $backup));
// return $backup;
}
// app('log')->debug(sprintf('No rate for %s to EUR.', $currency->code));
return '0';
}
private function getFromDB(int $from, int $to, string $date): ?string
{
if ($from === $to) {
Log::debug('ExchangeRateConverter: From and to are the same, return "1".');
return '1';
}
$key = sprintf('cer-%d-%d-%s', $from, $to, $date);
// perhaps the rate has been cached during this particular run
$preparedRate = $this->prepared[$date][$from][$to] ?? null;
if (null !== $preparedRate && 0 !== bccomp('0', $preparedRate)) {
Log::debug(sprintf('ExchangeRateConverter: Found prepared rate from #%d to #%d on %s.', $from, $to, $date));
return $preparedRate;
}
$cache = new CacheProperties();
$cache->addProperty($key);
if ($cache->has()) {
$rate = $cache->get();
if ('' === $rate) {
return null;
}
Log::debug(sprintf('ExchangeRateConverter: Found cached rate from #%d to #%d on %s.', $from, $to, $date));
return $rate;
}
/** @var null|CurrencyExchangeRate $result */
$result = $this->userGroup->currencyExchangeRates()
->where('from_currency_id', $from)
->where('to_currency_id', $to)
->where('date', '<=', $date)
->orderBy('date', 'DESC')
->first()
;
++$this->queryCount;
$rate = (string)$result?->rate;
if ('' === $rate) {
app('log')->debug(sprintf('ExchangeRateConverter: Found no rate for #%d->#%d (%s) in the DB.', $from, $to, $date));
return null;
}
if (0 === bccomp('0', $rate)) {
app('log')->debug(sprintf('ExchangeRateConverter: Found rate for #%d->#%d (%s) in the DB, but it\'s zero.', $from, $to, $date));
return null;
}
app('log')->debug(sprintf('ExchangeRateConverter: Found rate for #%d->#%d (%s) in the DB: %s.', $from, $to, $date, $rate));
$cache->store($rate);
// if the rate has not been cached during this particular run, save it
$this->prepared[$date] ??= [
$from => [
$to => $rate,
],
];
// also save the exchange rate the other way around:
$this->prepared[$date] ??= [
$to => [
$from => bcdiv('1', $rate),
],
];
return $rate;
}
/**
* @throws FireflyException
*/
@@ -146,147 +289,4 @@ class ExchangeRateConverter
return $rate;
}
private function getCacheKey(TransactionCurrency $from, TransactionCurrency $to, Carbon $date): string
{
return sprintf('cer-%d-%d-%s', $from->id, $to->id, $date->format('Y-m-d'));
}
private function getFromDB(int $from, int $to, string $date): ?string
{
if ($from === $to) {
Log::debug('ExchangeRateConverter: From and to are the same, return "1".');
return '1';
}
$key = sprintf('cer-%d-%d-%s', $from, $to, $date);
// perhaps the rate has been cached during this particular run
$preparedRate = $this->prepared[$date][$from][$to] ?? null;
if (null !== $preparedRate && 0 !== bccomp('0', $preparedRate)) {
Log::debug(sprintf('ExchangeRateConverter: Found prepared rate from #%d to #%d on %s.', $from, $to, $date));
return $preparedRate;
}
$cache = new CacheProperties();
$cache->addProperty($key);
if ($cache->has()) {
$rate = $cache->get();
if ('' === $rate) {
return null;
}
Log::debug(sprintf('ExchangeRateConverter: Found cached rate from #%d to #%d on %s.', $from, $to, $date));
return $rate;
}
/** @var null|CurrencyExchangeRate $result */
$result = $this->userGroup->currencyExchangeRates()
->where('from_currency_id', $from)
->where('to_currency_id', $to)
->where('date', '<=', $date)
->orderBy('date', 'DESC')
->first()
;
++$this->queryCount;
$rate = (string) $result?->rate;
if ('' === $rate) {
app('log')->debug(sprintf('ExchangeRateConverter: Found no rate for #%d->#%d (%s) in the DB.', $from, $to, $date));
return null;
}
if (0 === bccomp('0', $rate)) {
app('log')->debug(sprintf('ExchangeRateConverter: Found rate for #%d->#%d (%s) in the DB, but it\'s zero.', $from, $to, $date));
return null;
}
app('log')->debug(sprintf('ExchangeRateConverter: Found rate for #%d->#%d (%s) in the DB: %s.', $from, $to, $date, $rate));
$cache->store($rate);
// if the rate has not been cached during this particular run, save it
$this->prepared[$date] ??= [
$from => [
$to => $rate,
],
];
// also save the exchange rate the other way around:
$this->prepared[$date] ??= [
$to => [
$from => bcdiv('1', $rate),
],
];
return $rate;
}
/**
* @throws FireflyException
*/
private function getEuroRate(TransactionCurrency $currency, Carbon $date): string
{
$euroId = $this->getEuroId();
if ($euroId === $currency->id) {
return '1';
}
$rate = $this->getFromDB($currency->id, $euroId, $date->format('Y-m-d'));
if (null !== $rate) {
// app('log')->debug(sprintf('Rate for %s to EUR is %s.', $currency->code, $rate));
return $rate;
}
$rate = $this->getFromDB($euroId, $currency->id, $date->format('Y-m-d'));
if (null !== $rate) {
return bcdiv('1', $rate);
// app('log')->debug(sprintf('Inverted rate for %s to EUR is %s.', $currency->code, $rate));
// return $rate;
}
// grab backup values from config file:
$backup = config(sprintf('cer.rates.%s', $currency->code));
if (null !== $backup) {
return bcdiv('1', (string) $backup);
// app('log')->debug(sprintf('Backup rate for %s to EUR is %s.', $currency->code, $backup));
// return $backup;
}
// app('log')->debug(sprintf('No rate for %s to EUR.', $currency->code));
return '0';
}
/**
* @throws FireflyException
*/
private function getEuroId(): int
{
Log::debug('getEuroId()');
$cache = new CacheProperties();
$cache->addProperty('cer-euro-id');
if ($cache->has()) {
return (int) $cache->get();
}
$euro = Amount::getTransactionCurrencyByCode('EUR');
++$this->queryCount;
$cache->store($euro->id);
return $euro->id;
}
public function setIgnoreSettings(bool $ignoreSettings): void
{
$this->ignoreSettings = $ignoreSettings;
}
public function setUserGroup(UserGroup $userGroup): void
{
$this->userGroup = $userGroup;
}
public function summarize(): void
{
if (false === $this->enabled()) {
return;
}
Log::debug(sprintf('ExchangeRateConverter ran %d queries.', $this->queryCount));
}
}

View File

@@ -60,7 +60,7 @@ class SummaryBalanceGrouped
$return[] = [
'key' => sprintf('%s-in-pc', $title),
'value' => $this->amounts[$key]['primary'] ?? '0',
'currency_id' => (string) $this->default->id,
'currency_id' => (string)$this->default->id,
'currency_code' => $this->default->code,
'currency_symbol' => $this->default->symbol,
'currency_decimal_places' => $this->default->decimal_places,
@@ -73,7 +73,7 @@ class SummaryBalanceGrouped
// skip primary entries.
continue;
}
$currencyId = (int) $currencyId;
$currencyId = (int)$currencyId;
$currency = $this->currencies[$currencyId] ?? $this->currencyRepository->find($currencyId);
$this->currencies[$currencyId] = $currency;
// create objects for big array.
@@ -87,7 +87,7 @@ class SummaryBalanceGrouped
$return[] = [
'key' => sprintf('%s-in-%s', $title, $currency->code),
'value' => $this->amounts[$key][$currencyId] ?? '0',
'currency_id' => (string) $currency->id,
'currency_id' => (string)$currency->id,
'currency_code' => $currency->code,
'currency_symbol' => $currency->symbol,
'currency_decimal_places' => $currency->decimal_places,
@@ -109,12 +109,12 @@ class SummaryBalanceGrouped
/** @var array $journal */
foreach ($journals as $journal) {
// transaction info:
$currencyId = (int) $journal['currency_id'];
$amount = bcmul((string) $journal['amount'], $multiplier);
$currencyId = (int)$journal['currency_id'];
$amount = bcmul((string)$journal['amount'], $multiplier);
$currency = $this->currencies[$currencyId] ?? Amount::getTransactionCurrencyById($currencyId);
$this->currencies[$currencyId] = $currency;
$pcAmount = $converter->convert($currency, $this->default, $journal['date'], $amount);
if ((int) $journal['foreign_currency_id'] === $this->default->id) {
if ((int)$journal['foreign_currency_id'] === $this->default->id) {
// use foreign amount instead
$pcAmount = $journal['foreign_amount'];
}
@@ -126,10 +126,10 @@ class SummaryBalanceGrouped
$this->amounts[self::SUM]['primary'] ??= '0';
// add values:
$this->amounts[$key][$currencyId] = bcadd((string) $this->amounts[$key][$currencyId], $amount);
$this->amounts[self::SUM][$currencyId] = bcadd((string) $this->amounts[self::SUM][$currencyId], $amount);
$this->amounts[$key]['primary'] = bcadd((string) $this->amounts[$key]['primary'], (string) $pcAmount);
$this->amounts[self::SUM]['primary'] = bcadd((string) $this->amounts[self::SUM]['primary'], (string) $pcAmount);
$this->amounts[$key][$currencyId] = bcadd((string)$this->amounts[$key][$currencyId], $amount);
$this->amounts[self::SUM][$currencyId] = bcadd((string)$this->amounts[self::SUM][$currencyId], $amount);
$this->amounts[$key]['primary'] = bcadd((string)$this->amounts[$key]['primary'], (string)$pcAmount);
$this->amounts[self::SUM]['primary'] = bcadd((string)$this->amounts[self::SUM]['primary'], (string)$pcAmount);
}
$converter->summarize();
}

View File

@@ -38,8 +38,8 @@ use Illuminate\Support\Facades\Log;
*/
trait ValidatesUserGroupTrait
{
protected User $user;
protected UserGroup $userGroup;
protected User $user;
/**
* An "undocumented" filter
@@ -62,11 +62,11 @@ trait ValidatesUserGroupTrait
$user = auth()->user();
$groupId = 0;
if (!$request->has('user_group_id')) {
$groupId = (int) $user->user_group_id;
$groupId = (int)$user->user_group_id;
Log::debug(sprintf('validateUserGroup: no user group submitted, use default group #%d.', $groupId));
}
if ($request->has('user_group_id')) {
$groupId = (int) $request->get('user_group_id');
$groupId = (int)$request->get('user_group_id');
Log::debug(sprintf('validateUserGroup: user group submitted, search for memberships in group #%d.', $groupId));
}
@@ -78,7 +78,7 @@ trait ValidatesUserGroupTrait
if (0 === $memberships->count()) {
Log::debug(sprintf('validateUserGroup: user has no access to group #%d.', $groupId));
throw new AuthorizationException((string) trans('validation.no_access_group'));
throw new AuthorizationException((string)trans('validation.no_access_group'));
}
// need to get the group from the membership:
@@ -86,14 +86,14 @@ trait ValidatesUserGroupTrait
if (null === $group) {
Log::debug(sprintf('validateUserGroup: group #%d does not exist.', $groupId));
throw new AuthorizationException((string) trans('validation.belongs_user_or_user_group'));
throw new AuthorizationException((string)trans('validation.belongs_user_or_user_group'));
}
Log::debug(sprintf('validateUserGroup: validate access of user to group #%d ("%s").', $groupId, $group->title));
$roles = property_exists($this, 'acceptedRoles') ? $this->acceptedRoles : []; // @phpstan-ignore-line
if (0 === count($roles)) {
Log::debug('validateUserGroup: no roles defined, so no access.');
throw new AuthorizationException((string) trans('validation.no_accepted_roles_defined'));
throw new AuthorizationException((string)trans('validation.no_accepted_roles_defined'));
}
Log::debug(sprintf('validateUserGroup: have %d roles to check.', count($roles)), $roles);
@@ -111,6 +111,6 @@ trait ValidatesUserGroupTrait
Log::debug('validateUserGroup: User does NOT have enough rights to access endpoint.');
throw new AuthorizationException((string) trans('validation.belongs_user_or_user_group'));
throw new AuthorizationException((string)trans('validation.belongs_user_or_user_group'));
}
}

View File

@@ -110,8 +110,8 @@ trait AugumentData
$grouped = $accounts->groupBy('id')->toArray();
$return = [];
foreach ($accountIds as $combinedId) {
$parts = explode('-', (string) $combinedId);
$accountId = (int) $parts[0];
$parts = explode('-', (string)$combinedId);
$accountId = (int)$parts[0];
if (array_key_exists($accountId, $grouped)) {
$return[$accountId] = $grouped[$accountId][0]['name'];
}
@@ -136,7 +136,7 @@ trait AugumentData
$return[$budgetId] = $grouped[$budgetId][0]['name'];
}
}
$return[0] = (string) trans('firefly.no_budget');
$return[0] = (string)trans('firefly.no_budget');
return $return;
}
@@ -152,13 +152,13 @@ trait AugumentData
$grouped = $categories->groupBy('id')->toArray();
$return = [];
foreach ($categoryIds as $combinedId) {
$parts = explode('-', (string) $combinedId);
$categoryId = (int) $parts[0];
$parts = explode('-', (string)$combinedId);
$categoryId = (int)$parts[0];
if (array_key_exists($categoryId, $grouped)) {
$return[$categoryId] = $grouped[$categoryId][0]['name'];
}
}
$return[0] = (string) trans('firefly.no_category');
$return[0] = (string)trans('firefly.no_category');
return $return;
}
@@ -249,7 +249,7 @@ trait AugumentData
}
$grouped[$name] ??= '0';
$grouped[$name] = bcadd((string) $journal['amount'], $grouped[$name]);
$grouped[$name] = bcadd((string)$journal['amount'], $grouped[$name]);
}
return $grouped;
@@ -272,7 +272,7 @@ trait AugumentData
];
// loop to support multi currency
foreach ($journals as $journal) {
$currencyId = (int) $journal['currency_id'];
$currencyId = (int)$journal['currency_id'];
// if not set, set to zero:
if (!array_key_exists($currencyId, $sum['per_currency'])) {
@@ -287,8 +287,8 @@ trait AugumentData
}
// add amount
$sum['per_currency'][$currencyId]['sum'] = bcadd($sum['per_currency'][$currencyId]['sum'], (string) $journal['amount']);
$sum['grand_sum'] = bcadd($sum['grand_sum'], (string) $journal['amount']);
$sum['per_currency'][$currencyId]['sum'] = bcadd($sum['per_currency'][$currencyId]['sum'], (string)$journal['amount']);
$sum['grand_sum'] = bcadd($sum['grand_sum'], (string)$journal['amount']);
}
return $sum;

View File

@@ -92,7 +92,7 @@ trait ChartGeneration
Log::debug(sprintf('Start balance for account #%d ("%s) is', $account->id, $account->name), $previous);
while ($currentStart <= $end) {
$format = $currentStart->format('Y-m-d');
$label = trim($currentStart->isoFormat((string) trans('config.month_and_day_js', [], $locale)));
$label = trim($currentStart->isoFormat((string)trans('config.month_and_day_js', [], $locale)));
$balance = $range[$format] ?? $previous;
$previous = $balance;
$currentStart->addDay();

View File

@@ -73,7 +73,7 @@ trait CreateStuff
/** @var AccountRepositoryInterface $repository */
$repository = app(AccountRepositoryInterface::class);
$assetAccount = [
'name' => (string) trans('firefly.cash_wallet', [], $language),
'name' => (string)trans('firefly.cash_wallet', [], $language),
'iban' => null,
'account_type_name' => 'asset',
'virtual_balance' => 0,
@@ -108,7 +108,7 @@ trait CreateStuff
Log::alert('NO OAuth keys were found. They have been created.');
file_put_contents($publicKey, (string) $key->getPublicKey());
file_put_contents($publicKey, (string)$key->getPublicKey());
file_put_contents($privateKey, $key->toString('PKCS1'));
}
@@ -120,7 +120,7 @@ trait CreateStuff
/** @var AccountRepositoryInterface $repository */
$repository = app(AccountRepositoryInterface::class);
$savingsAccount = [
'name' => (string) trans('firefly.new_savings_account', ['bank_name' => $request->get('bank_name')], $language),
'name' => (string)trans('firefly.new_savings_account', ['bank_name' => $request->get('bank_name')], $language),
'iban' => null,
'account_type_name' => 'asset',
'account_type_id' => null,

View File

@@ -63,32 +63,6 @@ trait CronRunner
];
}
protected function webhookCronJob(bool $force, Carbon $date): array
{
/** @var WebhookCronjob $webhook */
$webhook = app(WebhookCronjob::class);
$webhook->setForce($force);
$webhook->setDate($date);
try {
$webhook->fire();
} catch (FireflyException $e) {
return [
'job_fired' => false,
'job_succeeded' => false,
'job_errored' => true,
'message' => $e->getMessage(),
];
}
return [
'job_fired' => $webhook->jobFired,
'job_succeeded' => $webhook->jobSucceeded,
'job_errored' => $webhook->jobErrored,
'message' => $webhook->message,
];
}
protected function exchangeRatesCronJob(bool $force, Carbon $date): array
{
/** @var ExchangeRatesCronjob $exchangeRates */
@@ -166,4 +140,30 @@ trait CronRunner
'message' => $recurring->message,
];
}
protected function webhookCronJob(bool $force, Carbon $date): array
{
/** @var WebhookCronjob $webhook */
$webhook = app(WebhookCronjob::class);
$webhook->setForce($force);
$webhook->setDate($date);
try {
$webhook->fire();
} catch (FireflyException $e) {
return [
'job_fired' => false,
'job_succeeded' => false,
'job_errored' => true,
'message' => $e->getMessage(),
];
}
return [
'job_fired' => $webhook->jobFired,
'job_succeeded' => $webhook->jobSucceeded,
'job_errored' => $webhook->jobErrored,
'message' => $webhook->message,
];
}
}

View File

@@ -40,13 +40,13 @@ trait DateCalculation
*/
public function activeDaysLeft(Carbon $start, Carbon $end): int
{
$difference = (int) ($start->diffInDays($end, true) + 1);
$difference = (int)($start->diffInDays($end, true) + 1);
$today = today(config('app.timezone'))->startOfDay();
if ($start->lte($today) && $end->gte($today)) {
$difference = $today->diffInDays($end) + 1;
}
return (int) (0 === $difference ? 1 : $difference);
return (int)(0 === $difference ? 1 : $difference);
}
/**
@@ -63,7 +63,7 @@ trait DateCalculation
$difference = $start->diffInDays($today, true) + 1;
}
return (int) $difference;
return (int)$difference;
}
protected function calculateStep(Carbon $start, Carbon $end): string

View File

@@ -48,7 +48,7 @@ trait GetConfigurationData
E_COMPILE_ERROR | E_RECOVERABLE_ERROR | E_ERROR | E_CORE_ERROR => 'E_COMPILE_ERROR|E_RECOVERABLE_ERROR|E_ERROR|E_CORE_ERROR',
];
return $array[$value] ?? (string) $value;
return $array[$value] ?? (string)$value;
}
/**
@@ -64,7 +64,7 @@ trait GetConfigurationData
$currentStep = $options;
// get the text:
$currentStep['intro'] = (string) trans('intro.'.$route.'_'.$key);
$currentStep['intro'] = (string)trans('intro.'.$route.'_'.$key);
// save in array:
$steps[] = $currentStep;
@@ -133,41 +133,41 @@ trait GetConfigurationData
$todayEnd = app('navigation')->endOfPeriod($todayStart, $viewRange);
if ($todayStart->ne($start) || $todayEnd->ne($end)) {
$ranges[ucfirst((string) trans('firefly.today'))] = [$todayStart, $todayEnd];
$ranges[ucfirst((string)trans('firefly.today'))] = [$todayStart, $todayEnd];
}
// last seven days:
$seven = today(config('app.timezone'))->subDays(7);
$index = (string) trans('firefly.last_seven_days');
$index = (string)trans('firefly.last_seven_days');
$ranges[$index] = [$seven, new Carbon()];
// last 30 days:
$thirty = today(config('app.timezone'))->subDays(30);
$index = (string) trans('firefly.last_thirty_days');
$index = (string)trans('firefly.last_thirty_days');
$ranges[$index] = [$thirty, new Carbon()];
// month to date:
$monthBegin = today(config('app.timezone'))->startOfMonth();
$index = (string) trans('firefly.month_to_date');
$index = (string)trans('firefly.month_to_date');
$ranges[$index] = [$monthBegin, new Carbon()];
// year to date:
$yearBegin = today(config('app.timezone'))->startOfYear();
$index = (string) trans('firefly.year_to_date');
$index = (string)trans('firefly.year_to_date');
$ranges[$index] = [$yearBegin, new Carbon()];
// everything
$index = (string) trans('firefly.everything');
$index = (string)trans('firefly.everything');
$ranges[$index] = [$first, new Carbon()];
return [
'title' => $title,
'configuration' => [
'apply' => (string) trans('firefly.apply'),
'cancel' => (string) trans('firefly.cancel'),
'from' => (string) trans('firefly.from'),
'to' => (string) trans('firefly.to'),
'customRange' => (string) trans('firefly.customRange'),
'apply' => (string)trans('firefly.apply'),
'cancel' => (string)trans('firefly.cancel'),
'from' => (string)trans('firefly.from'),
'to' => (string)trans('firefly.to'),
'customRange' => (string)trans('firefly.customRange'),
'start' => $start->format('Y-m-d'),
'end' => $end->format('Y-m-d'),
'ranges' => $ranges,
@@ -192,7 +192,7 @@ trait GetConfigurationData
$currentStep = $options;
// get the text:
$currentStep['intro'] = (string) trans('intro.'.$route.'_'.$specificPage.'_'.$key);
$currentStep['intro'] = (string)trans('intro.'.$route.'_'.$specificPage.'_'.$key);
// save in array:
$steps[] = $currentStep;
@@ -207,7 +207,7 @@ trait GetConfigurationData
protected function verifyRecurringCronJob(): void
{
$config = FireflyConfig::get('last_rt_job', 0);
$lastTime = (int) $config?->data;
$lastTime = (int)$config?->data;
$now = Carbon::now()->getTimestamp();
app('log')->debug(sprintf('verifyRecurringCronJob: last time is %d ("%s"), now is %d', $lastTime, $config?->data, $now));
if (0 === $lastTime) {

View File

@@ -87,9 +87,9 @@ trait ModelInformation
/** @var AccountType $mortgage */
$mortgage = $repository->getAccountTypeByType(AccountTypeEnum::MORTGAGE->value);
$liabilityTypes = [
$debt->id => (string) trans(sprintf('firefly.account_type_%s', AccountTypeEnum::DEBT->value)),
$loan->id => (string) trans(sprintf('firefly.account_type_%s', AccountTypeEnum::LOAN->value)),
$mortgage->id => (string) trans(sprintf('firefly.account_type_%s', AccountTypeEnum::MORTGAGE->value)),
$debt->id => (string)trans(sprintf('firefly.account_type_%s', AccountTypeEnum::DEBT->value)),
$loan->id => (string)trans(sprintf('firefly.account_type_%s', AccountTypeEnum::LOAN->value)),
$mortgage->id => (string)trans(sprintf('firefly.account_type_%s', AccountTypeEnum::MORTGAGE->value)),
];
asort($liabilityTypes);
@@ -100,7 +100,7 @@ trait ModelInformation
{
$roles = [];
foreach (config('firefly.accountRoles') as $role) {
$roles[$role] = (string) trans(sprintf('firefly.account_role_%s', $role));
$roles[$role] = (string)trans(sprintf('firefly.account_role_%s', $role));
}
return $roles;
@@ -118,7 +118,7 @@ trait ModelInformation
$triggers = [];
foreach ($operators as $key => $operator) {
if ('user_action' !== $key && false === $operator['alias']) {
$triggers[$key] = (string) trans(sprintf('firefly.rule_trigger_%s_choice', $key));
$triggers[$key] = (string)trans(sprintf('firefly.rule_trigger_%s_choice', $key));
}
}
asort($triggers);
@@ -169,7 +169,7 @@ trait ModelInformation
$triggers = [];
foreach ($operators as $key => $operator) {
if ('user_action' !== $key && false === $operator['alias']) {
$triggers[$key] = (string) trans(sprintf('firefly.rule_trigger_%s_choice', $key));
$triggers[$key] = (string)trans(sprintf('firefly.rule_trigger_%s_choice', $key));
}
}
asort($triggers);

View File

@@ -30,13 +30,21 @@ use FireflyIII\Exceptions\FireflyException;
use FireflyIII\Helpers\Collector\GroupCollectorInterface;
use FireflyIII\Models\Account;
use FireflyIII\Models\Category;
use FireflyIII\Models\PeriodStatistic;
use FireflyIII\Models\Tag;
use FireflyIII\Repositories\Account\AccountRepositoryInterface;
use FireflyIII\Repositories\Category\CategoryRepositoryInterface;
use FireflyIII\Repositories\Journal\JournalRepositoryInterface;
use FireflyIII\Repositories\PeriodStatistic\PeriodStatisticRepositoryInterface;
use FireflyIII\Repositories\Tag\TagRepositoryInterface;
use FireflyIII\Support\CacheProperties;
use FireflyIII\Support\Debug\Timer;
use FireflyIII\Support\Facades\Amount;
use FireflyIII\Support\Facades\Navigation;
use FireflyIII\Support\Facades\Steam;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Str;
/**
* Trait PeriodOverview.
@@ -67,8 +75,13 @@ use Illuminate\Support\Facades\Log;
*/
trait PeriodOverview
{
protected AccountRepositoryInterface $accountRepository;
protected JournalRepositoryInterface $journalRepos;
protected AccountRepositoryInterface $accountRepository;
protected CategoryRepositoryInterface $categoryRepository;
protected TagRepositoryInterface $tagRepository;
protected JournalRepositoryInterface $journalRepos;
protected PeriodStatisticRepositoryInterface $periodStatisticRepo;
private Collection $statistics; // temp data holder
private array $transactions; // temp data holder
/**
* This method returns "period entries", so nov-2015, dec-2015, etc. (this depends on the users session range)
@@ -79,163 +92,45 @@ trait PeriodOverview
*/
protected function getAccountPeriodOverview(Account $account, Carbon $start, Carbon $end): array
{
Log::debug('Now in getAccountPeriodOverview()');
$timer = Timer::getInstance();
$timer->start('account-period-total');
$this->accountRepository = app(AccountRepositoryInterface::class);
$range = Navigation::getViewRange(true);
[$start, $end] = $end < $start ? [$end, $start] : [$start, $end];
// properties for cache
$cache = new CacheProperties();
$cache->addProperty($start);
$cache->addProperty($end);
$cache->addProperty('account-show-period-entries');
$cache->addProperty($account->id);
if ($cache->has()) {
Log::debug('Return CACHED in getAccountPeriodOverview()');
return $cache->get();
}
Log::debug(sprintf('Now in getAccountPeriodOverview(#%d, %s %s)', $account->id, $start->format('Y-m-d H:i:s.u'), $end->format('Y-m-d H:i:s.u')));
$this->accountRepository = app(AccountRepositoryInterface::class);
$this->accountRepository->setUser($account->user);
$this->periodStatisticRepo = app(PeriodStatisticRepositoryInterface::class);
$range = Navigation::getViewRange(true);
[$start, $end] = $end < $start ? [$end, $start] : [$start, $end];
/** @var array $dates */
$dates = Navigation::blockPeriods($start, $end, $range);
$entries = [];
$dates = Navigation::blockPeriods($start, $end, $range);
[$start, $end] = $this->getPeriodFromBlocks($dates, $start, $end);
$this->statistics = $this->periodStatisticRepo->allInRangeForModel($account, $start, $end);
// run a custom query because doing this with the collector is MEGA slow.
$timer->start('account-period-collect');
$transactions = $this->accountRepository->periodCollection($account, $start, $end);
$timer->stop('account-period-collect');
// loop dates
$entries = [];
Log::debug(sprintf('Count of loops: %d', count($dates)));
$loops = 0;
// stop after 10 loops for memory reasons.
$timer->start('account-period-loop');
foreach ($dates as $currentDate) {
$title = Navigation::periodShow($currentDate['start'], $currentDate['period']);
[$transactions, $spent] = $this->filterTransactionsByType(TransactionTypeEnum::WITHDRAWAL, $transactions, $currentDate['start'], $currentDate['end']);
[$transactions, $earned] = $this->filterTransactionsByType(TransactionTypeEnum::DEPOSIT, $transactions, $currentDate['start'], $currentDate['end']);
[$transactions, $transferredAway] = $this->filterTransfers('away', $transactions, $currentDate['start'], $currentDate['end']);
[$transactions, $transferredIn] = $this->filterTransfers('in', $transactions, $currentDate['start'], $currentDate['end']);
$entries[]
= [
'title' => $title,
'route' => route('accounts.show', [$account->id, $currentDate['start']->format('Y-m-d'), $currentDate['end']->format('Y-m-d')]),
'total_transactions' => count($spent) + count($earned) + count($transferredAway) + count($transferredIn),
'spent' => $this->groupByCurrency($spent),
'earned' => $this->groupByCurrency($earned),
'transferred_away' => $this->groupByCurrency($transferredAway),
'transferred_in' => $this->groupByCurrency($transferredIn),
];
++$loops;
$entries[] = $this->getSingleModelPeriod($account, $currentDate['period'], $currentDate['start'], $currentDate['end']);
}
$timer->stop('account-period-loop');
$cache->store($entries);
$timer->stop('account-period-total');
Log::debug('End of getAccountPeriodOverview()');
return $entries;
}
private function filterTransactionsByType(TransactionTypeEnum $type, array $transactions, Carbon $start, Carbon $end): array
private function getPeriodFromBlocks(array $dates, Carbon $start, Carbon $end): array
{
$result = [];
$filtered = [];
/**
* @var int $index
* @var array $item
*/
foreach ($transactions as $index => $item) {
$date = Carbon::parse($item['date']);
$fits = $item['type'] === $type->value && $date >= $start && $date <= $end;
if ($fits) {
$result[] = $item;
unset($transactions[$index]);
Log::debug('Filter generated periods to select the oldest and newest date.');
foreach ($dates as $row) {
$currentStart = clone $row['start'];
$currentEnd = clone $row['end'];
if ($currentStart->lt($start)) {
Log::debug(sprintf('New start: was %s, now %s', $start->format('Y-m-d'), $currentStart->format('Y-m-d')));
$start = $currentStart;
}
if (!$fits) {
$filtered[] = $item;
if ($currentEnd->gt($end)) {
Log::debug(sprintf('New end: was %s, now %s', $end->format('Y-m-d'), $currentEnd->format('Y-m-d')));
$end = $currentEnd;
}
}
return [$filtered, $result];
}
private function filterTransfers(string $direction, array $transactions, Carbon $start, Carbon $end): array
{
$result = [];
$filtered = [];
/**
* @var int $index
* @var array $item
*/
foreach ($transactions as $index => $item) {
$date = Carbon::parse($item['date']);
if ($date >= $start && $date <= $end) {
if ('away' === $direction && -1 === bccomp((string)$item['amount'], '0')) {
$result[] = $item;
continue;
}
if ('in' === $direction && 1 === bccomp((string)$item['amount'], '0')) {
$result[] = $item;
continue;
}
}
$filtered[] = $item;
}
return [$filtered, $result];
}
private function groupByCurrency(array $journals): array
{
$return = [];
/** @var array $journal */
foreach ($journals as $journal) {
$currencyId = (int)$journal['currency_id'];
$currencyCode = $journal['currency_code'];
$currencyName = $journal['currency_name'];
$currencySymbol = $journal['currency_symbol'];
$currencyDecimalPlaces = $journal['currency_decimal_places'];
$foreignCurrencyId = $journal['foreign_currency_id'];
$amount = $journal['amount'] ?? '0';
if ($this->convertToPrimary && $currencyId !== $this->primaryCurrency->id && $foreignCurrencyId !== $this->primaryCurrency->id) {
$amount = $journal['pc_amount'] ?? '0';
$currencyId = $this->primaryCurrency->id;
$currencyCode = $this->primaryCurrency->code;
$currencyName = $this->primaryCurrency->name;
$currencySymbol = $this->primaryCurrency->symbol;
$currencyDecimalPlaces = $this->primaryCurrency->decimal_places;
}
if ($this->convertToPrimary && $currencyId !== $this->primaryCurrency->id && $foreignCurrencyId === $this->primaryCurrency->id) {
$currencyId = (int)$foreignCurrencyId;
$currencyCode = $journal['foreign_currency_code'];
$currencyName = $journal['foreign_currency_name'];
$currencySymbol = $journal['foreign_currency_symbol'];
$currencyDecimalPlaces = $journal['foreign_currency_decimal_places'];
$amount = $journal['foreign_amount'] ?? '0';
}
$return[$currencyId] ??= [
'amount' => '0',
'count' => 0,
'currency_id' => $currencyId,
'currency_name' => $currencyName,
'currency_code' => $currencyCode,
'currency_symbol' => $currencySymbol,
'currency_decimal_places' => $currencyDecimalPlaces,
];
$return[$currencyId]['amount'] = bcadd($return[$currencyId]['amount'], $amount);
++$return[$currencyId]['count'];
}
return $return;
return [$start, $end];
}
/**
@@ -245,89 +140,28 @@ trait PeriodOverview
*/
protected function getCategoryPeriodOverview(Category $category, Carbon $start, Carbon $end): array
{
$range = Navigation::getViewRange(true);
[$start, $end] = $end < $start ? [$end, $start] : [$start, $end];
$this->categoryRepository = app(CategoryRepositoryInterface::class);
$this->categoryRepository->setUser($category->user);
$this->periodStatisticRepo = app(PeriodStatisticRepositoryInterface::class);
// properties for entries with their amounts.
$cache = new CacheProperties();
$cache->addProperty($start);
$cache->addProperty($end);
$cache->addProperty($range);
$cache->addProperty('category-show-period-entries');
$cache->addProperty($category->id);
if ($cache->has()) {
return $cache->get();
}
$range = Navigation::getViewRange(true);
[$start, $end] = $end < $start ? [$end, $start] : [$start, $end];
/** @var array $dates */
$dates = Navigation::blockPeriods($start, $end, $range);
$entries = [];
$dates = Navigation::blockPeriods($start, $end, $range);
$entries = [];
[$start, $end] = $this->getPeriodFromBlocks($dates, $start, $end);
$this->statistics = $this->periodStatisticRepo->allInRangeForModel($category, $start, $end);
// collect all expenses in this period:
/** @var GroupCollectorInterface $collector */
$collector = app(GroupCollectorInterface::class);
$collector->setCategory($category);
$collector->setRange($start, $end);
$collector->setTypes([TransactionTypeEnum::DEPOSIT->value]);
$earnedSet = $collector->getExtractedJournals();
// collect all income in this period:
/** @var GroupCollectorInterface $collector */
$collector = app(GroupCollectorInterface::class);
$collector->setCategory($category);
$collector->setRange($start, $end);
$collector->setTypes([TransactionTypeEnum::WITHDRAWAL->value]);
$spentSet = $collector->getExtractedJournals();
// collect all transfers in this period:
/** @var GroupCollectorInterface $collector */
$collector = app(GroupCollectorInterface::class);
$collector->setCategory($category);
$collector->setRange($start, $end);
$collector->setTypes([TransactionTypeEnum::TRANSFER->value]);
$transferSet = $collector->getExtractedJournals();
Log::debug(sprintf('Count of loops: %d', count($dates)));
foreach ($dates as $currentDate) {
$spent = $this->filterJournalsByDate($spentSet, $currentDate['start'], $currentDate['end']);
$earned = $this->filterJournalsByDate($earnedSet, $currentDate['start'], $currentDate['end']);
$transferred = $this->filterJournalsByDate($transferSet, $currentDate['start'], $currentDate['end']);
$title = Navigation::periodShow($currentDate['end'], $currentDate['period']);
$entries[]
= [
'transactions' => 0,
'title' => $title,
'route' => route(
'categories.show',
[$category->id, $currentDate['start']->format('Y-m-d'), $currentDate['end']->format('Y-m-d')]
),
'total_transactions' => count($spent) + count($earned) + count($transferred),
'spent' => $this->groupByCurrency($spent),
'earned' => $this->groupByCurrency($earned),
'transferred' => $this->groupByCurrency($transferred),
];
$entries[] = $this->getSingleModelPeriod($category, $currentDate['period'], $currentDate['start'], $currentDate['end']);
}
$cache->store($entries);
return $entries;
}
/**
* Filter a list of journals by a set of dates, and then group them by currency.
*/
private function filterJournalsByDate(array $array, Carbon $start, Carbon $end): array
{
$result = [];
/** @var array $journal */
foreach ($array as $journal) {
if ($journal['date'] <= $end && $journal['date'] >= $start) {
$result[] = $journal;
}
}
return $result;
}
/**
* Same as above, but for lists that involve transactions without a budget.
*
@@ -335,117 +169,269 @@ trait PeriodOverview
*
* @throws FireflyException
*/
protected function getNoBudgetPeriodOverview(Carbon $start, Carbon $end): array
protected function getNoModelPeriodOverview(string $model, Carbon $start, Carbon $end): array
{
$range = Navigation::getViewRange(true);
[$start, $end] = $end < $start ? [$end, $start] : [$start, $end];
$cache = new CacheProperties();
$cache->addProperty($start);
$cache->addProperty($end);
$cache->addProperty($this->convertToPrimary);
$cache->addProperty('no-budget-period-entries');
if ($cache->has()) {
return $cache->get();
}
Log::debug(sprintf('Now in getNoModelPeriodOverview(%s, %s %s)', $model, $start->format('Y-m-d'), $end->format('Y-m-d')));
$this->periodStatisticRepo = app(PeriodStatisticRepositoryInterface::class);
$range = Navigation::getViewRange(true);
[$start, $end] = $end < $start ? [$end, $start] : [$start, $end];
/** @var array $dates */
$dates = Navigation::blockPeriods($start, $end, $range);
$entries = [];
// get all expenses without a budget.
/** @var GroupCollectorInterface $collector */
$collector = app(GroupCollectorInterface::class);
$collector->setRange($start, $end)->withoutBudget()->withAccountInformation()->setTypes([TransactionTypeEnum::WITHDRAWAL->value]);
$journals = $collector->getExtractedJournals();
$dates = Navigation::blockPeriods($start, $end, $range);
[$start, $end] = $this->getPeriodFromBlocks($dates, $start, $end);
$entries = [];
$this->statistics = $this->periodStatisticRepo->allInRangeForPrefix(sprintf('no_%s', $model), $start, $end);
Log::debug(sprintf('Collected %d stats', $this->statistics->count()));
foreach ($dates as $currentDate) {
$set = $this->filterJournalsByDate($journals, $currentDate['start'], $currentDate['end']);
$title = Navigation::periodShow($currentDate['end'], $currentDate['period']);
$entries[]
= [
'title' => $title,
'route' => route('budgets.no-budget', [$currentDate['start']->format('Y-m-d'), $currentDate['end']->format('Y-m-d')]),
'total_transactions' => count($set),
'spent' => $this->groupByCurrency($set),
'earned' => [],
'transferred_away' => [],
'transferred_in' => [],
];
$entries[] = $this->getSingleNoModelPeriodOverview($model, $currentDate['start'], $currentDate['end'], $currentDate['period']);
}
$cache->store($entries);
return $entries;
}
/**
* TODO fix the date.
*
* Show period overview for no category view.
*
* @throws FireflyException
*/
protected function getNoCategoryPeriodOverview(Carbon $theDate): array
private function getSingleNoModelPeriodOverview(string $model, Carbon $start, Carbon $end, string $period): array
{
app('log')->debug(sprintf('Now in getNoCategoryPeriodOverview(%s)', $theDate->format('Y-m-d')));
$range = Navigation::getViewRange(true);
$first = $this->journalRepos->firstNull();
$start = null === $first ? new Carbon() : $first->date;
$end = clone $theDate;
$end = Navigation::endOfPeriod($end, $range);
Log::debug(sprintf('getSingleNoModelPeriodOverview(%s, %s, %s, %s)', $model, $start->format('Y-m-d'), $end->format('Y-m-d'), $period));
$statistics = $this->filterPrefixedStatistics($start, $end, sprintf('no_%s', $model));
$title = Navigation::periodShow($end, $period);
app('log')->debug(sprintf('Start for getNoCategoryPeriodOverview() is %s', $start->format('Y-m-d')));
app('log')->debug(sprintf('End for getNoCategoryPeriodOverview() is %s', $end->format('Y-m-d')));
if (0 === $statistics->count()) {
Log::debug(sprintf('Found no statistics in period %s - %s, regenerating them.', $start->format('Y-m-d'), $end->format('Y-m-d')));
// properties for cache
$dates = Navigation::blockPeriods($start, $end, $range);
$entries = [];
switch ($model) {
default:
throw new FireflyException(sprintf('Cannot deal with model of type "%s"', $model));
// collect all expenses in this period:
/** @var GroupCollectorInterface $collector */
$collector = app(GroupCollectorInterface::class);
$collector->withoutCategory();
$collector->setRange($start, $end);
$collector->setTypes([TransactionTypeEnum::DEPOSIT->value]);
$earnedSet = $collector->getExtractedJournals();
case 'budget':
// get all expenses without a budget.
/** @var GroupCollectorInterface $collector */
$collector = app(GroupCollectorInterface::class);
$collector->setRange($start, $end)->withoutBudget()->withAccountInformation()->setTypes([TransactionTypeEnum::WITHDRAWAL->value]);
$spent = $collector->getExtractedJournals();
$earned = [];
$transferred = [];
// collect all income in this period:
/** @var GroupCollectorInterface $collector */
$collector = app(GroupCollectorInterface::class);
$collector->withoutCategory();
$collector->setRange($start, $end);
$collector->setTypes([TransactionTypeEnum::WITHDRAWAL->value]);
$spentSet = $collector->getExtractedJournals();
break;
// collect all transfers in this period:
/** @var GroupCollectorInterface $collector */
$collector = app(GroupCollectorInterface::class);
$collector->withoutCategory();
$collector->setRange($start, $end);
$collector->setTypes([TransactionTypeEnum::TRANSFER->value]);
$transferSet = $collector->getExtractedJournals();
case 'category':
// collect all expenses in this period:
/** @var GroupCollectorInterface $collector */
$collector = app(GroupCollectorInterface::class);
$collector->withoutCategory();
$collector->setRange($start, $end);
$collector->setTypes([TransactionTypeEnum::DEPOSIT->value]);
$earned = $collector->getExtractedJournals();
/** @var array $currentDate */
foreach ($dates as $currentDate) {
$spent = $this->filterJournalsByDate($spentSet, $currentDate['start'], $currentDate['end']);
$earned = $this->filterJournalsByDate($earnedSet, $currentDate['start'], $currentDate['end']);
$transferred = $this->filterJournalsByDate($transferSet, $currentDate['start'], $currentDate['end']);
$title = Navigation::periodShow($currentDate['end'], $currentDate['period']);
$entries[]
= [
'title' => $title,
'route' => route('categories.no-category', [$currentDate['start']->format('Y-m-d'), $currentDate['end']->format('Y-m-d')]),
'total_transactions' => count($spent) + count($earned) + count($transferred),
'spent' => $this->groupByCurrency($spent),
'earned' => $this->groupByCurrency($earned),
'transferred' => $this->groupByCurrency($transferred),
];
// collect all income in this period:
/** @var GroupCollectorInterface $collector */
$collector = app(GroupCollectorInterface::class);
$collector->withoutCategory();
$collector->setRange($start, $end);
$collector->setTypes([TransactionTypeEnum::WITHDRAWAL->value]);
$spent = $collector->getExtractedJournals();
// collect all transfers in this period:
/** @var GroupCollectorInterface $collector */
$collector = app(GroupCollectorInterface::class);
$collector->withoutCategory();
$collector->setRange($start, $end);
$collector->setTypes([TransactionTypeEnum::TRANSFER->value]);
$transferred = $collector->getExtractedJournals();
break;
}
$groupedSpent = $this->groupByCurrency($spent);
$groupedEarned = $this->groupByCurrency($earned);
$groupedTransferred = $this->groupByCurrency($transferred);
$entry
= [
'title' => $title,
'route' => route(sprintf('%s.no-%s', Str::plural($model), $model), [$start->format('Y-m-d'), $end->format('Y-m-d')]),
'total_transactions' => count($spent),
'spent' => $groupedSpent,
'earned' => $groupedEarned,
'transferred' => $groupedTransferred,
];
$this->saveGroupedForPrefix(sprintf('no_%s', $model), $start, $end, 'spent', $groupedSpent);
$this->saveGroupedForPrefix(sprintf('no_%s', $model), $start, $end, 'earned', $groupedEarned);
$this->saveGroupedForPrefix(sprintf('no_%s', $model), $start, $end, 'transferred', $groupedTransferred);
return $entry;
}
app('log')->debug('End of loops');
Log::debug(sprintf('Found %d statistics in period %s - %s.', count($statistics), $start->format('Y-m-d'), $end->format('Y-m-d')));
return $entries;
$entry
= [
'title' => $title,
'route' => route(sprintf('%s.no-%s', Str::plural($model), $model), [$start->format('Y-m-d'), $end->format('Y-m-d')]),
'total_transactions' => 0,
'spent' => [],
'earned' => [],
'transferred' => [],
];
$grouped = [];
/** @var PeriodStatistic $statistic */
foreach ($statistics as $statistic) {
$type = str_replace(sprintf('no_%s_', $model), '', $statistic->type);
$id = (int)$statistic->transaction_currency_id;
$currency = Amount::getTransactionCurrencyById($id);
$grouped[$type]['count'] ??= 0;
$grouped[$type][$id] = [
'amount' => (string)$statistic->amount,
'count' => (int)$statistic->count,
'currency_id' => $currency->id,
'currency_name' => $currency->name,
'currency_code' => $currency->code,
'currency_symbol' => $currency->symbol,
'currency_decimal_places' => $currency->decimal_places,
];
$grouped[$type]['count'] += (int)$statistic->count;
}
$types = ['spent', 'earned', 'transferred'];
foreach ($types as $type) {
if (array_key_exists($type, $grouped)) {
$entry['total_transactions'] += $grouped[$type]['count'];
unset($grouped[$type]['count']);
$entry[$type] = $grouped[$type];
}
}
return $entry;
}
protected function getSingleModelPeriod(Model $model, string $period, Carbon $start, Carbon $end): array
{
Log::debug(sprintf('Now in getSingleModelPeriod(%s #%d, %s %s)', $model::class, $model->id, $start->format('Y-m-d'), $end->format('Y-m-d')));
$types = ['spent', 'earned', 'transferred_in', 'transferred_away'];
$return = [
'title' => Navigation::periodShow($start, $period),
'route' => route(sprintf('%s.show', strtolower(Str::plural(class_basename($model)))), [$model->id, $start->format('Y-m-d'), $end->format('Y-m-d')]),
'total_transactions' => 0,
];
$this->transactions = [];
foreach ($types as $type) {
$set = $this->getSingleModelPeriodByType($model, $start, $end, $type);
$return['total_transactions'] += $set['count'];
unset($set['count']);
$return[$type] = $set;
}
return $return;
}
private function filterStatistics(Carbon $start, Carbon $end, string $type): Collection
{
if (0 === $this->statistics->count()) {
Log::debug('Have no statistic to filter!');
return new Collection();
}
return $this->statistics->filter(
fn (PeriodStatistic $statistic) => $statistic->start->eq($start) && $statistic->end->eq($end) && $statistic->type === $type
);
}
private function filterPrefixedStatistics(Carbon $start, Carbon $end, string $prefix): Collection
{
if (0 === $this->statistics->count()) {
Log::debug('Have no statistic to filter!');
return new Collection();
}
return $this->statistics->filter(
fn (PeriodStatistic $statistic) => $statistic->start->eq($start) && $statistic->end->eq($end) && str_starts_with($statistic->type, $prefix)
);
}
private function getSingleModelPeriodByType(Model $model, Carbon $start, Carbon $end, string $type): array
{
Log::debug(sprintf('Now in getSingleModelPeriodByType(%s #%d, %s %s, %s)', $model::class, $model->id, $start->format('Y-m-d'), $end->format('Y-m-d'), $type));
$statistics = $this->filterStatistics($start, $end, $type);
// nothing found, regenerate them.
if (0 === $statistics->count()) {
Log::debug(sprintf('Found nothing in this period for type "%s"', $type));
if (0 === count($this->transactions)) {
switch ($model::class) {
default:
throw new FireflyException(sprintf('Cannot deal with model of type "%s"', $model::class));
case Category::class:
$this->transactions = $this->categoryRepository->periodCollection($model, $start, $end);
break;
case Account::class:
$this->transactions = $this->accountRepository->periodCollection($model, $start, $end);
break;
case Tag::class:
$this->transactions = $this->tagRepository->periodCollection($model, $start, $end);
break;
}
}
switch ($type) {
default:
throw new FireflyException(sprintf('Cannot deal with category period type %s', $type));
case 'spent':
$result = $this->filterTransactionsByType(TransactionTypeEnum::WITHDRAWAL, $start, $end);
break;
case 'earned':
$result = $this->filterTransactionsByType(TransactionTypeEnum::DEPOSIT, $start, $end);
break;
case 'transferred_in':
$result = $this->filterTransfers('in', $start, $end);
break;
case 'transferred_away':
$result = $this->filterTransfers('away', $start, $end);
break;
}
// each result must be grouped by currency, then saved as period statistic.
Log::debug(sprintf('Going to group %d found journal(s)', count($result)));
$grouped = $this->groupByCurrency($result);
$this->saveGroupedAsStatistics($model, $start, $end, $type, $grouped);
return $grouped;
}
$grouped = [
'count' => 0,
];
/** @var PeriodStatistic $statistic */
foreach ($statistics as $statistic) {
$id = (int)$statistic->transaction_currency_id;
$currency = Amount::getTransactionCurrencyById($id);
$grouped[$id] = [
'amount' => (string)$statistic->amount,
'count' => (int)$statistic->count,
'currency_id' => $currency->id,
'currency_name' => $currency->name,
'currency_code' => $currency->code,
'currency_symbol' => $currency->symbol,
'currency_decimal_places' => $currency->decimal_places,
];
$grouped['count'] += (int)$statistic->count;
}
return $grouped;
}
/**
@@ -455,96 +441,28 @@ trait PeriodOverview
*/
protected function getTagPeriodOverview(Tag $tag, Carbon $start, Carbon $end): array // period overview for tags.
{
$range = Navigation::getViewRange(true);
[$start, $end] = $end < $start ? [$end, $start] : [$start, $end];
$this->tagRepository = app(TagRepositoryInterface::class);
$this->tagRepository->setUser($tag->user);
$this->periodStatisticRepo = app(PeriodStatisticRepositoryInterface::class);
// properties for cache
$cache = new CacheProperties();
$cache->addProperty($start);
$cache->addProperty($end);
$cache->addProperty('tag-period-entries');
$cache->addProperty($tag->id);
if ($cache->has()) {
return $cache->get();
}
$range = Navigation::getViewRange(true);
[$start, $end] = $end < $start ? [$end, $start] : [$start, $end];
/** @var array $dates */
$dates = Navigation::blockPeriods($start, $end, $range);
$entries = [];
$dates = Navigation::blockPeriods($start, $end, $range);
$entries = [];
[$start, $end] = $this->getPeriodFromBlocks($dates, $start, $end);
$this->statistics = $this->periodStatisticRepo->allInRangeForModel($tag, $start, $end);
// collect all expenses in this period:
/** @var GroupCollectorInterface $collector */
$collector = app(GroupCollectorInterface::class);
$collector->setTag($tag);
$collector->setRange($start, $end);
$collector->setTypes([TransactionTypeEnum::DEPOSIT->value]);
$earnedSet = $collector->getExtractedJournals();
// collect all income in this period:
/** @var GroupCollectorInterface $collector */
$collector = app(GroupCollectorInterface::class);
$collector->setTag($tag);
$collector->setRange($start, $end);
$collector->setTypes([TransactionTypeEnum::WITHDRAWAL->value]);
$spentSet = $collector->getExtractedJournals();
// collect all transfers in this period:
/** @var GroupCollectorInterface $collector */
$collector = app(GroupCollectorInterface::class);
$collector->setTag($tag);
$collector->setRange($start, $end);
$collector->setTypes([TransactionTypeEnum::TRANSFER->value]);
$transferSet = $collector->getExtractedJournals();
// filer all of them:
$earnedSet = $this->filterJournalsByTag($earnedSet, $tag);
$spentSet = $this->filterJournalsByTag($spentSet, $tag);
$transferSet = $this->filterJournalsByTag($transferSet, $tag);
Log::debug(sprintf('Count of loops: %d', count($dates)));
foreach ($dates as $currentDate) {
$spent = $this->filterJournalsByDate($spentSet, $currentDate['start'], $currentDate['end']);
$earned = $this->filterJournalsByDate($earnedSet, $currentDate['start'], $currentDate['end']);
$transferred = $this->filterJournalsByDate($transferSet, $currentDate['start'], $currentDate['end']);
$title = Navigation::periodShow($currentDate['end'], $currentDate['period']);
$entries[]
= [
'transactions' => 0,
'title' => $title,
'route' => route(
'tags.show',
[$tag->id, $currentDate['start']->format('Y-m-d'), $currentDate['end']->format('Y-m-d')]
),
'total_transactions' => count($spent) + count($earned) + count($transferred),
'spent' => $this->groupByCurrency($spent),
'earned' => $this->groupByCurrency($earned),
'transferred' => $this->groupByCurrency($transferred),
];
$entries[] = $this->getSingleModelPeriod($tag, $currentDate['period'], $currentDate['start'], $currentDate['end']);
}
return $entries;
}
private function filterJournalsByTag(array $set, Tag $tag): array
{
$return = [];
foreach ($set as $entry) {
$found = false;
/** @var array $localTag */
foreach ($entry['tags'] as $localTag) {
if ($localTag['id'] === $tag->id) {
$found = true;
}
}
if (false === $found) {
continue;
}
$return[] = $entry;
}
return $return;
}
/**
* @throws FireflyException
*/
@@ -606,35 +524,148 @@ trait PeriodOverview
return $entries;
}
/**
* Return only transactions where $account is the source.
*/
private function filterTransferredAway(Account $account, array $journals): array
private function saveGroupedAsStatistics(Model $model, Carbon $start, Carbon $end, string $type, array $array): void
{
$return = [];
/** @var array $journal */
foreach ($journals as $journal) {
if ($account->id === (int)$journal['source_account_id']) {
$return[] = $journal;
}
unset($array['count']);
Log::debug(sprintf('saveGroupedAsStatistics(%s #%d, %s, %s, "%s", array(%d))', $model::class, $model->id, $start->format('Y-m-d'), $end->format('Y-m-d'), $type, count($array)));
foreach ($array as $entry) {
$this->periodStatisticRepo->saveStatistic($model, $entry['currency_id'], $start, $end, $type, $entry['count'], $entry['amount']);
}
if (0 === count($array)) {
Log::debug('Save empty statistic.');
$this->periodStatisticRepo->saveStatistic($model, $this->primaryCurrency->id, $start, $end, $type, 0, '0');
}
}
return $return;
private function saveGroupedForPrefix(string $prefix, Carbon $start, Carbon $end, string $type, array $array): void
{
unset($array['count']);
Log::debug(sprintf('saveGroupedForPrefix("%s", %s, %s, "%s", array(%d))', $prefix, $start->format('Y-m-d'), $end->format('Y-m-d'), $type, count($array)));
foreach ($array as $entry) {
$this->periodStatisticRepo->savePrefixedStatistic($prefix, $entry['currency_id'], $start, $end, $type, $entry['count'], $entry['amount']);
}
if (0 === count($array)) {
Log::debug('Save empty statistic.');
$this->periodStatisticRepo->savePrefixedStatistic($prefix, $this->primaryCurrency->id, $start, $end, $type, 0, '0');
}
}
/**
* Return only transactions where $account is the source.
* Filter a list of journals by a set of dates, and then group them by currency.
*/
private function filterTransferredIn(Account $account, array $journals): array
private function filterJournalsByDate(array $array, Carbon $start, Carbon $end): array
{
$return = [];
$result = [];
/** @var array $journal */
foreach ($array as $journal) {
if ($journal['date'] <= $end && $journal['date'] >= $start) {
$result[] = $journal;
}
}
return $result;
}
private function filterTransactionsByType(TransactionTypeEnum $type, Carbon $start, Carbon $end): array
{
$result = [];
/**
* @var int $index
* @var array $item
*/
foreach ($this->transactions as $item) {
$date = Carbon::parse($item['date']);
$fits = $item['type'] === $type->value && $date >= $start && $date <= $end;
if ($fits) {
// if type is withdrawal, negative amount:
if (TransactionTypeEnum::WITHDRAWAL === $type && 1 === bccomp((string)$item['amount'], '0')) {
$item['amount'] = Steam::negative($item['amount']);
}
$result[] = $item;
}
}
return $result;
}
private function filterTransfers(string $direction, Carbon $start, Carbon $end): array
{
$result = [];
/**
* @var int $index
* @var array $item
*/
foreach ($this->transactions as $item) {
$date = Carbon::parse($item['date']);
if ($date >= $start && $date <= $end) {
if ('Transfer' === $item['type'] && 'away' === $direction && -1 === bccomp((string)$item['amount'], '0')) {
$result[] = $item;
continue;
}
if ('Transfer' === $item['type'] && 'in' === $direction && 1 === bccomp((string)$item['amount'], '0')) {
$result[] = $item;
}
}
}
return $result;
}
private function groupByCurrency(array $journals): array
{
Log::debug('groupByCurrency()');
$return = [
'count' => 0,
];
if (0 === count($journals)) {
return $return;
}
/** @var array $journal */
foreach ($journals as $journal) {
if ($account->id === (int)$journal['destination_account_id']) {
$return[] = $journal;
$currencyId = (int)$journal['currency_id'];
$currencyCode = $journal['currency_code'];
$currencyName = $journal['currency_name'];
$currencySymbol = $journal['currency_symbol'];
$currencyDecimalPlaces = $journal['currency_decimal_places'];
$foreignCurrencyId = $journal['foreign_currency_id'];
$amount = $journal['amount'] ?? '0';
if ($this->convertToPrimary && $currencyId !== $this->primaryCurrency->id && $foreignCurrencyId !== $this->primaryCurrency->id) {
$amount = $journal['pc_amount'] ?? '0';
$currencyId = $this->primaryCurrency->id;
$currencyCode = $this->primaryCurrency->code;
$currencyName = $this->primaryCurrency->name;
$currencySymbol = $this->primaryCurrency->symbol;
$currencyDecimalPlaces = $this->primaryCurrency->decimal_places;
}
if ($this->convertToPrimary && $currencyId !== $this->primaryCurrency->id && $foreignCurrencyId === $this->primaryCurrency->id) {
$currencyId = (int)$foreignCurrencyId;
$currencyCode = $journal['foreign_currency_code'];
$currencyName = $journal['foreign_currency_name'];
$currencySymbol = $journal['foreign_currency_symbol'];
$currencyDecimalPlaces = $journal['foreign_currency_decimal_places'];
$amount = $journal['foreign_amount'] ?? '0';
}
$return[$currencyId] ??= [
'amount' => '0',
'count' => 0,
'currency_id' => $currencyId,
'currency_name' => $currencyName,
'currency_code' => $currencyCode,
'currency_symbol' => $currencySymbol,
'currency_decimal_places' => $currencyDecimalPlaces,
];
$return[$currencyId]['amount'] = bcadd((string)$return[$currencyId]['amount'], $amount);
++$return[$currencyId]['count'];
++$return['count'];
}
return $return;

View File

@@ -56,10 +56,10 @@ trait RenderPartialViews
/** @var BudgetRepositoryInterface $budgetRepository */
$budgetRepository = app(BudgetRepositoryInterface::class);
$budget = $budgetRepository->find((int) $attributes['budgetId']);
$budget = $budgetRepository->find((int)$attributes['budgetId']);
$accountRepos = app(AccountRepositoryInterface::class);
$account = $accountRepos->find((int) $attributes['accountId']);
$account = $accountRepos->find((int)$attributes['accountId']);
if (null === $budget || null === $account) {
throw new FireflyException('Could not render popup.report.balance-amount because budget or account is null.');
@@ -115,7 +115,7 @@ trait RenderPartialViews
/** @var PopupReportInterface $popupHelper */
$popupHelper = app(PopupReportInterface::class);
$budget = $budgetRepository->find((int) $attributes['budgetId']);
$budget = $budgetRepository->find((int)$attributes['budgetId']);
if (null === $budget) {
// transactions without a budget.
$budget = new Budget();
@@ -146,7 +146,7 @@ trait RenderPartialViews
/** @var CategoryRepositoryInterface $categoryRepository */
$categoryRepository = app(CategoryRepositoryInterface::class);
$category = $categoryRepository->find((int) $attributes['categoryId']);
$category = $categoryRepository->find((int)$attributes['categoryId']);
$journals = $popupHelper->byCategory($category, $attributes);
try {
@@ -239,7 +239,7 @@ trait RenderPartialViews
/** @var PopupReportInterface $popupHelper */
$popupHelper = app(PopupReportInterface::class);
$account = $accountRepository->find((int) $attributes['accountId']);
$account = $accountRepository->find((int)$attributes['accountId']);
if (null === $account) {
return 'This is an unknown account. Apologies.';
@@ -310,7 +310,7 @@ trait RenderPartialViews
$triggers = [];
foreach ($operators as $key => $operator) {
if ('user_action' !== $key && false === $operator['alias']) {
$triggers[$key] = (string) trans(sprintf('firefly.rule_trigger_%s_choice', $key));
$triggers[$key] = (string)trans(sprintf('firefly.rule_trigger_%s_choice', $key));
}
}
asort($triggers);
@@ -325,7 +325,7 @@ trait RenderPartialViews
$count = ($index + 1);
try {
$rootOperator = OperatorQuerySearch::getRootOperator((string) $entry->trigger_type);
$rootOperator = OperatorQuerySearch::getRootOperator((string)$entry->trigger_type);
if (str_starts_with($rootOperator, '-')) {
$rootOperator = substr($rootOperator, 1);
}
@@ -335,7 +335,7 @@ trait RenderPartialViews
'oldTrigger' => $rootOperator,
'oldValue' => $entry->trigger_value,
'oldChecked' => $entry->stop_processing,
'oldProhibited' => str_starts_with((string) $entry->trigger_type, '-'),
'oldProhibited' => str_starts_with((string)$entry->trigger_type, '-'),
'count' => $count,
'triggers' => $triggers,
]
@@ -366,7 +366,7 @@ trait RenderPartialViews
/** @var PopupReportInterface $popupHelper */
$popupHelper = app(PopupReportInterface::class);
$account = $accountRepository->find((int) $attributes['accountId']);
$account = $accountRepository->find((int)$attributes['accountId']);
if (null === $account) {
return 'This is an unknown category. Apologies.';

View File

@@ -32,9 +32,9 @@ use FireflyIII\Support\Binder\AccountList;
use FireflyIII\User;
use Illuminate\Contracts\Validation\Validator as ValidatorContract;
use Illuminate\Routing\Route;
use Illuminate\Support\Facades\Validator;
use Illuminate\Support\Facades\Route as RouteFacade;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Route as RouteFacade;
use Illuminate\Support\Facades\Validator;
use function Safe\parse_url;
@@ -54,6 +54,22 @@ trait RequestInformation
return $parts['host'] ?? '';
}
final protected function getPageName(): string // get request info
{
return str_replace('.', '_', RouteFacade::currentRouteName());
}
/**
* Get the specific name of a page for intro.
*/
final protected function getSpecificPageName(): string // get request info
{
/** @var null|string $param */
$param = RouteFacade::current()->parameter('objectType');
return null === $param ? '' : sprintf('_%s', $param);
}
/**
* Get a list of triggers.
*/
@@ -67,7 +83,7 @@ trait RequestInformation
'type' => $triggerInfo['type'] ?? '',
'value' => $triggerInfo['value'] ?? '',
'prohibited' => $triggerInfo['prohibited'] ?? false,
'stop_processing' => 1 === (int) ($triggerInfo['stop_processing'] ?? '0'),
'stop_processing' => 1 === (int)($triggerInfo['stop_processing'] ?? '0'),
];
$current = RuleFormRequest::replaceAmountTrigger($current);
$triggers[] = $current;
@@ -103,22 +119,6 @@ trait RequestInformation
return $shownDemo;
}
final protected function getPageName(): string // get request info
{
return str_replace('.', '_', RouteFacade::currentRouteName());
}
/**
* Get the specific name of a page for intro.
*/
final protected function getSpecificPageName(): string // get request info
{
/** @var null|string $param */
$param = RouteFacade::current()->parameter('objectType');
return null === $param ? '' : sprintf('_%s', $param);
}
/**
* Check if date is outside session range.
*/
@@ -172,11 +172,11 @@ trait RequestInformation
final protected function validatePassword(User $user, string $current, string $new): bool // get request info
{
if (!Hash::check($current, $user->password)) {
throw new ValidationException((string) trans('firefly.invalid_current_password'));
throw new ValidationException((string)trans('firefly.invalid_current_password'));
}
if ($current === $new) {
throw new ValidationException((string) trans('firefly.should_change'));
throw new ValidationException((string)trans('firefly.should_change'));
}
return true;

View File

@@ -51,7 +51,7 @@ trait RuleManagement
[
'oldAction' => $oldAction['type'],
'oldValue' => $oldAction['value'] ?? '',
'oldChecked' => 1 === (int) ($oldAction['stop_processing'] ?? '0'),
'oldChecked' => 1 === (int)($oldAction['stop_processing'] ?? '0'),
'count' => $index + 1,
]
)->render();
@@ -78,7 +78,7 @@ trait RuleManagement
$triggers = [];
foreach ($operators as $key => $operator) {
if ('user_action' !== $key && false === $operator['alias']) {
$triggers[$key] = (string) trans(sprintf('firefly.rule_trigger_%s_choice', $key));
$triggers[$key] = (string)trans(sprintf('firefly.rule_trigger_%s_choice', $key));
}
}
asort($triggers);
@@ -94,8 +94,8 @@ trait RuleManagement
[
'oldTrigger' => OperatorQuerySearch::getRootOperator($oldTrigger['type']),
'oldValue' => $oldTrigger['value'] ?? '',
'oldChecked' => 1 === (int) ($oldTrigger['stop_processing'] ?? '0'),
'oldProhibited' => 1 === (int) ($oldTrigger['prohibited'] ?? '0'),
'oldChecked' => 1 === (int)($oldTrigger['stop_processing'] ?? '0'),
'oldProhibited' => 1 === (int)($oldTrigger['prohibited'] ?? '0'),
'count' => $index + 1,
'triggers' => $triggers,
]
@@ -124,7 +124,7 @@ trait RuleManagement
$triggers = [];
foreach ($operators as $key => $operator) {
if ('user_action' !== $key && false === $operator['alias']) {
$triggers[$key] = (string) trans(sprintf('firefly.rule_trigger_%s_choice', $key));
$triggers[$key] = (string)trans(sprintf('firefly.rule_trigger_%s_choice', $key));
}
}
asort($triggers);
@@ -132,7 +132,7 @@ trait RuleManagement
$index = 0;
foreach ($submittedOperators as $operator) {
$rootOperator = OperatorQuerySearch::getRootOperator($operator['type']);
$needsContext = (bool) config(sprintf('search.operators.%s.needs_context', $rootOperator));
$needsContext = (bool)config(sprintf('search.operators.%s.needs_context', $rootOperator));
try {
$renderedEntries[] = view(
@@ -164,8 +164,8 @@ trait RuleManagement
$repository = app(RuleGroupRepositoryInterface::class);
if (0 === $repository->count()) {
$data = [
'title' => (string) trans('firefly.default_rule_group_name'),
'description' => (string) trans('firefly.default_rule_group_description'),
'title' => (string)trans('firefly.default_rule_group_name'),
'description' => (string)trans('firefly.default_rule_group_description'),
'active' => true,
];

View File

@@ -49,7 +49,7 @@ trait UserNavigation
final protected function getPreviousUrl(string $identifier): string
{
app('log')->debug(sprintf('Trying to retrieve URL stored under "%s"', $identifier));
$url = (string) session($identifier);
$url = (string)session($identifier);
app('log')->debug(sprintf('The URL is %s', $url));
return app('steam')->getSafeUrl($url, route('index'));

View File

@@ -53,29 +53,29 @@ use Override;
*/
class AccountEnrichment implements EnrichmentInterface
{
private array $ids = [];
private array $accountTypeIds = [];
private array $accountTypes = [];
private Collection $collection;
private array $currencies = [];
private array $locations = [];
private array $meta = [];
private readonly TransactionCurrency $primaryCurrency;
private array $notes = [];
private array $openingBalances = [];
private User $user;
private UserGroup $userGroup;
private array $lastActivities = [];
private ?Carbon $date = null;
private ?Carbon $start = null;
private ?Carbon $end = null;
private array $accountTypeIds = [];
private array $accountTypes = [];
private array $balances = [];
private Collection $collection;
private readonly bool $convertToPrimary;
private array $balances = [];
private array $startBalances = [];
private array $endBalances = [];
private array $objectGroups = [];
private array $mappedObjects = [];
private array $sort = [];
private array $currencies = [];
private ?Carbon $date = null;
private ?Carbon $end = null;
private array $endBalances = [];
private array $ids = [];
private array $lastActivities = [];
private array $locations = [];
private array $mappedObjects = [];
private array $meta = [];
private array $notes = [];
private array $objectGroups = [];
private array $openingBalances = [];
private readonly TransactionCurrency $primaryCurrency;
private array $sort = [];
private ?Carbon $start = null;
private array $startBalances = [];
private User $user;
private UserGroup $userGroup;
/**
* TODO The account enricher must do conversion from and to the primary currency.
@@ -86,16 +86,6 @@ class AccountEnrichment implements EnrichmentInterface
$this->convertToPrimary = Amount::convertToPrimary();
}
#[Override]
public function enrichSingle(array|Model $model): Account|array
{
Log::debug(__METHOD__);
$collection = new Collection()->push($model);
$collection = $this->enrich($collection);
return $collection->first();
}
#[Override]
/**
* Do the actual enrichment.
@@ -121,114 +111,47 @@ class AccountEnrichment implements EnrichmentInterface
return $this->collection;
}
private function collectIds(): void
#[Override]
public function enrichSingle(array|Model $model): Account|array
{
/** @var Account $account */
foreach ($this->collection as $account) {
$this->ids[] = (int)$account->id;
$this->accountTypeIds[] = (int)$account->account_type_id;
}
$this->ids = array_unique($this->ids);
$this->accountTypeIds = array_unique($this->accountTypeIds);
Log::debug(__METHOD__);
$collection = new Collection()->push($model);
$collection = $this->enrich($collection);
return $collection->first();
}
private function getAccountTypes(): void
public function getDate(): Carbon
{
$types = AccountType::whereIn('id', $this->accountTypeIds)->get();
/** @var AccountType $type */
foreach ($types as $type) {
$this->accountTypes[(int)$type->id] = $type->type;
if (!$this->date instanceof Carbon) {
return now();
}
return $this->date;
}
private function collectMetaData(): void
public function setDate(?Carbon $date): void
{
$set = AccountMeta::whereIn('name', ['is_multi_currency', 'include_net_worth', 'currency_id', 'account_role', 'account_number', 'BIC', 'liability_direction', 'interest', 'interest_period', 'current_debt'])
->whereIn('account_id', $this->ids)
->get(['account_meta.id', 'account_meta.account_id', 'account_meta.name', 'account_meta.data'])->toArray()
;
/** @var array $entry */
foreach ($set as $entry) {
$this->meta[(int)$entry['account_id']][$entry['name']] = (string)$entry['data'];
if ('currency_id' === $entry['name']) {
$this->currencies[(int)$entry['data']] = true;
}
}
if (count($this->currencies) > 0) {
$currencies = TransactionCurrency::whereIn('id', array_keys($this->currencies))->get();
foreach ($currencies as $currency) {
$this->currencies[(int)$currency->id] = $currency;
}
}
$this->currencies[0] = $this->primaryCurrency;
foreach ($this->currencies as $id => $currency) {
if (true === $currency) {
throw new FireflyException(sprintf('Currency #%d not found.', $id));
}
if ($date instanceof Carbon) {
$date->endOfDay();
Log::debug(sprintf('Date is now %s', $date->toW3cString()));
}
$this->date = $date;
}
private function collectNotes(): void
public function setEnd(?Carbon $end): void
{
$notes = Note::query()->whereIn('noteable_id', $this->ids)
->whereNotNull('notes.text')
->where('notes.text', '!=', '')
->where('noteable_type', Account::class)->get(['notes.noteable_id', 'notes.text'])->toArray()
;
foreach ($notes as $note) {
$this->notes[(int)$note['noteable_id']] = (string)$note['text'];
}
Log::debug(sprintf('Enrich with %d note(s)', count($this->notes)));
$this->end = $end;
}
private function collectLocations(): void
public function setSort(array $sort): void
{
$locations = Location::query()->whereIn('locatable_id', $this->ids)
->where('locatable_type', Account::class)->get(['locations.locatable_id', 'locations.latitude', 'locations.longitude', 'locations.zoom_level'])->toArray()
;
foreach ($locations as $location) {
$this->locations[(int)$location['locatable_id']]
= [
'latitude' => (float)$location['latitude'],
'longitude' => (float)$location['longitude'],
'zoom_level' => (int)$location['zoom_level'],
];
}
Log::debug(sprintf('Enrich with %d locations(s)', count($this->locations)));
$this->sort = $sort;
}
private function collectOpeningBalances(): void
public function setStart(?Carbon $start): void
{
// use new group collector:
/** @var GroupCollectorInterface $collector */
$collector = app(GroupCollectorInterface::class);
$collector
->setUser($this->user)
->setUserGroup($this->userGroup)
->setAccounts($this->collection)
->withAccountInformation()
->setTypes([TransactionTypeEnum::OPENING_BALANCE->value])
;
$journals = $collector->getExtractedJournals();
foreach ($journals as $journal) {
$this->openingBalances[(int)$journal['source_account_id']]
= [
'amount' => Steam::negative($journal['amount']),
'date' => $journal['date'],
];
$this->openingBalances[(int)$journal['destination_account_id']]
= [
'amount' => Steam::positive($journal['amount']),
'date' => $journal['date'],
];
}
}
public function setUserGroup(UserGroup $userGroup): void
{
$this->userGroup = $userGroup;
$this->start = $start;
}
public function setUser(User $user): void
@@ -237,6 +160,11 @@ class AccountEnrichment implements EnrichmentInterface
$this->userGroup = $user->userGroup;
}
public function setUserGroup(UserGroup $userGroup): void
{
$this->userGroup = $userGroup;
}
private function appendCollectedData(): void
{
$this->collection = $this->collection->map(function (Account $item) {
@@ -357,11 +285,6 @@ class AccountEnrichment implements EnrichmentInterface
});
}
private function collectLastActivities(): void
{
$this->lastActivities = Steam::getLastActivities($this->ids);
}
private function collectBalances(): void
{
$this->balances = Steam::accountsBalancesOptimized($this->collection, $this->getDate(), $this->primaryCurrency, $this->convertToPrimary);
@@ -371,6 +294,79 @@ class AccountEnrichment implements EnrichmentInterface
}
}
private function collectIds(): void
{
/** @var Account $account */
foreach ($this->collection as $account) {
$this->ids[] = (int)$account->id;
$this->accountTypeIds[] = (int)$account->account_type_id;
}
$this->ids = array_unique($this->ids);
$this->accountTypeIds = array_unique($this->accountTypeIds);
}
private function collectLastActivities(): void
{
$this->lastActivities = Steam::getLastActivities($this->ids);
}
private function collectLocations(): void
{
$locations = Location::query()->whereIn('locatable_id', $this->ids)
->where('locatable_type', Account::class)->get(['locations.locatable_id', 'locations.latitude', 'locations.longitude', 'locations.zoom_level'])->toArray()
;
foreach ($locations as $location) {
$this->locations[(int)$location['locatable_id']]
= [
'latitude' => (float)$location['latitude'],
'longitude' => (float)$location['longitude'],
'zoom_level' => (int)$location['zoom_level'],
];
}
Log::debug(sprintf('Enrich with %d locations(s)', count($this->locations)));
}
private function collectMetaData(): void
{
$set = AccountMeta::whereIn('name', ['is_multi_currency', 'include_net_worth', 'currency_id', 'account_role', 'account_number', 'BIC', 'liability_direction', 'interest', 'interest_period', 'current_debt'])
->whereIn('account_id', $this->ids)
->get(['account_meta.id', 'account_meta.account_id', 'account_meta.name', 'account_meta.data'])->toArray()
;
/** @var array $entry */
foreach ($set as $entry) {
$this->meta[(int)$entry['account_id']][$entry['name']] = (string)$entry['data'];
if ('currency_id' === $entry['name']) {
$this->currencies[(int)$entry['data']] = true;
}
}
if (count($this->currencies) > 0) {
$currencies = TransactionCurrency::whereIn('id', array_keys($this->currencies))->get();
foreach ($currencies as $currency) {
$this->currencies[(int)$currency->id] = $currency;
}
}
$this->currencies[0] = $this->primaryCurrency;
foreach ($this->currencies as $id => $currency) {
if (true === $currency) {
throw new FireflyException(sprintf('Currency #%d not found.', $id));
}
}
}
private function collectNotes(): void
{
$notes = Note::query()->whereIn('noteable_id', $this->ids)
->whereNotNull('notes.text')
->where('notes.text', '!=', '')
->where('noteable_type', Account::class)->get(['notes.noteable_id', 'notes.text'])->toArray()
;
foreach ($notes as $note) {
$this->notes[(int)$note['noteable_id']] = (string)$note['text'];
}
Log::debug(sprintf('Enrich with %d note(s)', count($this->notes)));
}
private function collectObjectGroups(): void
{
$set = DB::table('object_groupables')
@@ -393,32 +389,41 @@ class AccountEnrichment implements EnrichmentInterface
}
}
public function setDate(?Carbon $date): void
private function collectOpeningBalances(): void
{
if ($date instanceof Carbon) {
$date->endOfDay();
Log::debug(sprintf('Date is now %s', $date->toW3cString()));
// use new group collector:
/** @var GroupCollectorInterface $collector */
$collector = app(GroupCollectorInterface::class);
$collector
->setUser($this->user)
->setUserGroup($this->userGroup)
->setAccounts($this->collection)
->withAccountInformation()
->setTypes([TransactionTypeEnum::OPENING_BALANCE->value])
;
$journals = $collector->getExtractedJournals();
foreach ($journals as $journal) {
$this->openingBalances[(int)$journal['source_account_id']]
= [
'amount' => Steam::negative($journal['amount']),
'date' => $journal['date'],
];
$this->openingBalances[(int)$journal['destination_account_id']]
= [
'amount' => Steam::positive($journal['amount']),
'date' => $journal['date'],
];
}
$this->date = $date;
}
public function getDate(): Carbon
private function getAccountTypes(): void
{
if (!$this->date instanceof Carbon) {
return now();
$types = AccountType::whereIn('id', $this->accountTypeIds)->get();
/** @var AccountType $type */
foreach ($types as $type) {
$this->accountTypes[(int)$type->id] = $type->type;
}
return $this->date;
}
public function setStart(?Carbon $start): void
{
$this->start = $start;
}
public function setEnd(?Carbon $end): void
{
$this->end = $end;
}
private function getBalanceDifference(int $id, TransactionCurrency $currency): ?string
@@ -437,11 +442,6 @@ class AccountEnrichment implements EnrichmentInterface
return bcsub($end, $start);
}
public function setSort(array $sort): void
{
$this->sort = $sort;
}
private function sortData(): void
{
$dbParams = config('firefly.allowed_db_sort_parameters.Account', []);

View File

@@ -40,20 +40,20 @@ use Override;
class AvailableBudgetEnrichment implements EnrichmentInterface
{
private User $user; // @phpstan-ignore-line
private UserGroup $userGroup; // @phpstan-ignore-line
private readonly bool $convertToPrimary;
private array $ids = [];
private array $currencyIds = [];
private Collection $collection; // @phpstan-ignore-line
private readonly bool $convertToPrimary; // @phpstan-ignore-line
private array $currencies = [];
private Collection $collection;
private array $spentInBudgets = [];
private array $spentOutsideBudgets = [];
private array $pcSpentInBudgets = [];
private array $pcSpentOutsideBudgets = [];
private array $currencyIds = [];
private array $ids = [];
private readonly NoBudgetRepositoryInterface $noBudgetRepository;
private readonly OperationsRepositoryInterface $opsRepository;
private array $pcSpentInBudgets = [];
private array $pcSpentOutsideBudgets = [];
private readonly BudgetRepositoryInterface $repository;
private array $spentInBudgets = [];
private array $spentOutsideBudgets = [];
private User $user;
private UserGroup $userGroup;
public function __construct()
{
@@ -104,6 +104,34 @@ class AvailableBudgetEnrichment implements EnrichmentInterface
$this->repository->setUserGroup($userGroup);
}
private function appendCollectedData(): void
{
$this->collection = $this->collection->map(function (AvailableBudget $item) {
$id = (int)$item->id;
$currencyId = $this->currencyIds[$id];
$currency = $this->currencies[$currencyId];
$meta = [
'currency' => $currency,
'spent_in_budgets' => $this->spentInBudgets[$id] ?? [],
'pc_spent_in_budgets' => $this->pcSpentInBudgets[$id] ?? [],
'spent_outside_budgets' => $this->spentOutsideBudgets[$id] ?? [],
'pc_spent_outside_budgets' => $this->pcSpentOutsideBudgets[$id] ?? [],
];
$item->meta = $meta;
return $item;
});
}
private function collectCurrencies(): void
{
$ids = array_unique(array_values($this->currencyIds));
$set = TransactionCurrency::whereIn('id', $ids)->get();
foreach ($set as $currency) {
$this->currencies[(int)$currency->id] = $currency;
}
}
private function collectIds(): void
{
/** @var AvailableBudget $availableBudget */
@@ -138,32 +166,4 @@ class AvailableBudgetEnrichment implements EnrichmentInterface
}
}
}
private function appendCollectedData(): void
{
$this->collection = $this->collection->map(function (AvailableBudget $item) {
$id = (int)$item->id;
$currencyId = $this->currencyIds[$id];
$currency = $this->currencies[$currencyId];
$meta = [
'currency' => $currency,
'spent_in_budgets' => $this->spentInBudgets[$id] ?? [],
'pc_spent_in_budgets' => $this->pcSpentInBudgets[$id] ?? [],
'spent_outside_budgets' => $this->spentOutsideBudgets[$id] ?? [],
'pc_spent_outside_budgets' => $this->pcSpentOutsideBudgets[$id] ?? [],
];
$item->meta = $meta;
return $item;
});
}
private function collectCurrencies(): void
{
$ids = array_unique(array_values($this->currencyIds));
$set = TransactionCurrency::whereIn('id', $ids)->get();
foreach ($set as $currency) {
$this->currencies[(int)$currency->id] = $currency;
}
}
}

View File

@@ -40,19 +40,19 @@ use Illuminate\Support\Facades\Log;
class BudgetEnrichment implements EnrichmentInterface
{
private Collection $collection;
private User $user;
private UserGroup $userGroup;
private array $ids = [];
private array $notes = [];
private array $autoBudgets = [];
private array $currencies = [];
private ?Carbon $start = null;
private ?Carbon $end = null;
private array $spent = [];
private array $pcSpent = [];
private array $objectGroups = [];
private array $mappedObjects = [];
private array $autoBudgets = [];
private Collection $collection;
private array $currencies = [];
private ?Carbon $end = null;
private array $ids = [];
private array $mappedObjects = [];
private array $notes = [];
private array $objectGroups = [];
private array $pcSpent = [];
private array $spent = [];
private ?Carbon $start = null;
private User $user;
private UserGroup $userGroup;
public function __construct() {}
@@ -79,6 +79,16 @@ class BudgetEnrichment implements EnrichmentInterface
return $collection->first();
}
public function setEnd(?Carbon $end): void
{
$this->end = $end;
}
public function setStart(?Carbon $start): void
{
$this->start = $start;
}
public function setUser(User $user): void
{
$this->user = $user;
@@ -90,28 +100,6 @@ class BudgetEnrichment implements EnrichmentInterface
$this->userGroup = $userGroup;
}
private function collectIds(): void
{
/** @var Budget $budget */
foreach ($this->collection as $budget) {
$this->ids[] = (int)$budget->id;
}
$this->ids = array_unique($this->ids);
}
private function collectNotes(): void
{
$notes = Note::query()->whereIn('noteable_id', $this->ids)
->whereNotNull('notes.text')
->where('notes.text', '!=', '')
->where('noteable_type', Budget::class)->get(['notes.noteable_id', 'notes.text'])->toArray()
;
foreach ($notes as $note) {
$this->notes[(int)$note['noteable_id']] = (string)$note['text'];
}
Log::debug(sprintf('Enrich with %d note(s)', count($this->notes)));
}
private function appendCollectedData(): void
{
$this->collection = $this->collection->map(function (Budget $item) {
@@ -130,7 +118,7 @@ class BudgetEnrichment implements EnrichmentInterface
// add object group if available
if (array_key_exists($id, $this->mappedObjects)) {
$key = $this->mappedObjects[$id];
$meta['object_group_id'] = (string) $this->objectGroups[$key]['id'];
$meta['object_group_id'] = (string)$this->objectGroups[$key]['id'];
$meta['object_group_title'] = $this->objectGroups[$key]['title'];
$meta['object_group_order'] = $this->objectGroups[$key]['order'];
}
@@ -177,14 +165,26 @@ class BudgetEnrichment implements EnrichmentInterface
}
}
public function setEnd(?Carbon $end): void
private function collectIds(): void
{
$this->end = $end;
/** @var Budget $budget */
foreach ($this->collection as $budget) {
$this->ids[] = (int)$budget->id;
}
$this->ids = array_unique($this->ids);
}
public function setStart(?Carbon $start): void
private function collectNotes(): void
{
$this->start = $start;
$notes = Note::query()->whereIn('noteable_id', $this->ids)
->whereNotNull('notes.text')
->where('notes.text', '!=', '')
->where('noteable_type', Budget::class)->get(['notes.noteable_id', 'notes.text'])->toArray()
;
foreach ($notes as $note) {
$this->notes[(int)$note['noteable_id']] = (string)$note['text'];
}
Log::debug(sprintf('Enrich with %d note(s)', count($this->notes)));
}
private function collectObjectGroups(): void

View File

@@ -40,19 +40,19 @@ use Illuminate\Support\Facades\Log;
class BudgetLimitEnrichment implements EnrichmentInterface
{
private User $user;
private UserGroup $userGroup; // @phpstan-ignore-line
private Collection $collection;
private array $ids = [];
private array $notes = [];
private Carbon $start;
private bool $convertToPrimary = true; // @phpstan-ignore-line
private array $currencies = [];
private array $currencyIds = [];
private Carbon $end;
private array $expenses = [];
private array $ids = [];
private array $notes = [];
private array $pcExpenses = [];
private array $currencyIds = [];
private array $currencies = [];
private bool $convertToPrimary = true;
private readonly TransactionCurrency $primaryCurrency;
private Carbon $start;
private User $user;
private UserGroup $userGroup;
public function __construct()
{
@@ -93,36 +93,6 @@ class BudgetLimitEnrichment implements EnrichmentInterface
$this->userGroup = $userGroup;
}
private function collectIds(): void
{
$this->start = $this->collection->min('start_date') ?? Carbon::now()->startOfMonth();
$this->end = $this->collection->max('end_date') ?? Carbon::now()->endOfMonth();
/** @var BudgetLimit $limit */
foreach ($this->collection as $limit) {
$id = (int)$limit->id;
$this->ids[] = $id;
if (0 !== (int)$limit->transaction_currency_id) {
$this->currencyIds[$id] = (int)$limit->transaction_currency_id;
}
}
$this->ids = array_unique($this->ids);
$this->currencyIds = array_unique($this->currencyIds);
}
private function collectNotes(): void
{
$notes = Note::query()->whereIn('noteable_id', $this->ids)
->whereNotNull('notes.text')
->where('notes.text', '!=', '')
->where('noteable_type', BudgetLimit::class)->get(['notes.noteable_id', 'notes.text'])->toArray()
;
foreach ($notes as $note) {
$this->notes[(int)$note['noteable_id']] = (string)$note['text'];
}
Log::debug(sprintf('Enrich with %d note(s)', count($this->notes)));
}
private function appendCollectedData(): void
{
$this->collection = $this->collection->map(function (BudgetLimit $item) {
@@ -179,6 +149,44 @@ class BudgetLimitEnrichment implements EnrichmentInterface
}
}
private function collectIds(): void
{
$this->start = $this->collection->min('start_date') ?? Carbon::now()->startOfMonth();
$this->end = $this->collection->max('end_date') ?? Carbon::now()->endOfMonth();
/** @var BudgetLimit $limit */
foreach ($this->collection as $limit) {
$id = (int)$limit->id;
$this->ids[] = $id;
if (0 !== (int)$limit->transaction_currency_id) {
$this->currencyIds[$id] = (int)$limit->transaction_currency_id;
}
}
$this->ids = array_unique($this->ids);
$this->currencyIds = array_unique($this->currencyIds);
}
private function collectNotes(): void
{
$notes = Note::query()->whereIn('noteable_id', $this->ids)
->whereNotNull('notes.text')
->where('notes.text', '!=', '')
->where('noteable_type', BudgetLimit::class)->get(['notes.noteable_id', 'notes.text'])->toArray()
;
foreach ($notes as $note) {
$this->notes[(int)$note['noteable_id']] = (string)$note['text'];
}
Log::debug(sprintf('Enrich with %d note(s)', count($this->notes)));
}
private function filterToBudget(array $expenses, int $budget): array
{
$result = array_filter($expenses, fn (array $item) => (int)$item['budget_id'] === $budget);
Log::debug(sprintf('filterToBudget for budget #%d, from %d to %d items', $budget, count($expenses), count($result)));
return $result;
}
private function stringifyIds(): void
{
$this->expenses = array_map(fn ($first) => array_map(function ($second) {
@@ -193,12 +201,4 @@ class BudgetLimitEnrichment implements EnrichmentInterface
return $second;
}, $first), $this->expenses);
}
private function filterToBudget(array $expenses, int $budget): array
{
$result = array_filter($expenses, fn (array $item) => (int)$item['budget_id'] === $budget);
Log::debug(sprintf('filterToBudget for budget #%d, from %d to %d items', $budget, count($expenses), count($result)));
return $result;
}
}

View File

@@ -38,18 +38,18 @@ use Illuminate\Support\Facades\Log;
class CategoryEnrichment implements EnrichmentInterface
{
private Collection $collection;
private User $user;
private UserGroup $userGroup;
private array $earned = [];
private ?Carbon $end = null;
private array $ids = [];
private array $notes = [];
private ?Carbon $start = null;
private ?Carbon $end = null;
private array $spent = [];
private array $pcSpent = [];
private array $earned = [];
private array $pcEarned = [];
private array $transfers = [];
private array $pcSpent = [];
private array $pcTransfers = [];
private array $spent = [];
private ?Carbon $start = null;
private array $transfers = [];
private User $user;
private UserGroup $userGroup;
public function enrich(Collection $collection): Collection
{
@@ -71,6 +71,16 @@ class CategoryEnrichment implements EnrichmentInterface
return $collection->first();
}
public function setEnd(?Carbon $end): void
{
$this->end = $end;
}
public function setStart(?Carbon $start): void
{
$this->start = $start;
}
public function setUser(User $user): void
{
$this->user = $user;
@@ -82,15 +92,6 @@ class CategoryEnrichment implements EnrichmentInterface
$this->userGroup = $userGroup;
}
private function collectIds(): void
{
/** @var Category $category */
foreach ($this->collection as $category) {
$this->ids[] = (int)$category->id;
}
$this->ids = array_unique($this->ids);
}
private function appendCollectedData(): void
{
$this->collection = $this->collection->map(function (Category $item) {
@@ -110,14 +111,13 @@ class CategoryEnrichment implements EnrichmentInterface
});
}
public function setEnd(?Carbon $end): void
private function collectIds(): void
{
$this->end = $end;
}
public function setStart(?Carbon $start): void
{
$this->start = $start;
/** @var Category $category */
foreach ($this->collection as $category) {
$this->ids[] = (int)$category->id;
}
$this->ids = array_unique($this->ids);
}
private function collectNotes(): void
@@ -135,7 +135,7 @@ class CategoryEnrichment implements EnrichmentInterface
private function collectTransactions(): void
{
if (null !== $this->start && null !== $this->end) {
if ($this->start instanceof Carbon && $this->end instanceof Carbon) {
/** @var OperationsRepositoryInterface $opsRepository */
$opsRepository = app(OperationsRepositoryInterface::class);
$opsRepository->setUser($this->user);

View File

@@ -43,24 +43,26 @@ use Illuminate\Support\Facades\Log;
class PiggyBankEnrichment implements EnrichmentInterface
{
private User $user; // @phpstan-ignore-line
private UserGroup $userGroup; // @phpstan-ignore-line
private Collection $collection;
private array $ids = [];
private array $currencyIds = [];
private array $currencies = [];
private array $accountIds = [];
private array $accountIds = []; // @phpstan-ignore-line
private array $accounts = []; // @phpstan-ignore-line
private array $amounts = [];
private Collection $collection;
private array $currencies = [];
private array $currencyIds = [];
private array $ids = [];
// private array $accountCurrencies = [];
private array $notes = [];
private array $mappedObjects = [];
private array $mappedObjects = [];
private array $notes = [];
private array $objectGroups = [];
private readonly TransactionCurrency $primaryCurrency;
private array $amounts = [];
private array $accounts = [];
private array $objectGroups = [];
private User $user;
private UserGroup $userGroup;
private ?Carbon $date;
public function __construct()
{
$this->primaryCurrency = Amount::getPrimaryCurrency();
$this->date = now(config('app.timezone'));
}
public function enrich(Collection $collection): Collection
@@ -69,7 +71,6 @@ class PiggyBankEnrichment implements EnrichmentInterface
$this->collectIds();
$this->collectObjectGroups();
$this->collectNotes();
$this->collectCurrentAmounts();
$this->appendCollectedData();
@@ -97,69 +98,6 @@ class PiggyBankEnrichment implements EnrichmentInterface
$this->userGroup = $userGroup;
}
private function collectIds(): void
{
/** @var PiggyBank $piggy */
foreach ($this->collection as $piggy) {
$id = (int)$piggy->id;
$this->ids[] = $id;
$this->currencyIds[$id] = (int)$piggy->transaction_currency_id;
}
$this->ids = array_unique($this->ids);
// collect currencies.
$currencies = TransactionCurrency::whereIn('id', $this->currencyIds)->get();
foreach ($currencies as $currency) {
$this->currencies[(int)$currency->id] = $currency;
}
// collect accounts
$set = DB::table('account_piggy_bank')->whereIn('piggy_bank_id', $this->ids)->get(['piggy_bank_id', 'account_id', 'current_amount', 'native_current_amount']);
foreach ($set as $item) {
$id = (int)$item->piggy_bank_id;
$accountId = (int)$item->account_id;
$this->amounts[$id] ??= [];
if (!array_key_exists($id, $this->accountIds)) {
$this->accountIds[$id] = (int)$item->account_id;
}
if (!array_key_exists($accountId, $this->amounts[$id])) {
$this->amounts[$id][$accountId] = [
'current_amount' => '0',
'pc_current_amount' => '0',
];
}
$this->amounts[$id][$accountId]['current_amount'] = bcadd($this->amounts[$id][$accountId]['current_amount'], (string) $item->current_amount);
if (null !== $this->amounts[$id][$accountId]['pc_current_amount'] && null !== $item->native_current_amount) {
$this->amounts[$id][$accountId]['pc_current_amount'] = bcadd($this->amounts[$id][$accountId]['pc_current_amount'], $item->native_current_amount);
}
}
// get account currency preference for ALL.
$set = AccountMeta::whereIn('account_id', array_values($this->accountIds))->where('name', 'currency_id')->get();
/** @var AccountMeta $item */
foreach ($set as $item) {
$accountId = (int)$item->account_id;
$currencyId = (int)$item->data;
if (!array_key_exists($currencyId, $this->currencies)) {
$this->currencies[$currencyId] = Amount::getTransactionCurrencyById($currencyId);
}
// $this->accountCurrencies[$accountId] = $this->currencies[$currencyId];
}
// get account info.
$set = Account::whereIn('id', array_values($this->accountIds))->get();
/** @var Account $item */
foreach ($set as $item) {
$id = (int)$item->id;
$this->accounts[$id] = [
'id' => $id,
'name' => $item->name,
];
}
}
private function appendCollectedData(): void
{
$this->collection = $this->collection->map(function (PiggyBank $item) {
@@ -193,7 +131,7 @@ class PiggyBankEnrichment implements EnrichmentInterface
// add object group if available
if (array_key_exists($id, $this->mappedObjects)) {
$key = $this->mappedObjects[$id];
$meta['object_group_id'] = (string) $this->objectGroups[$key]['id'];
$meta['object_group_id'] = (string)$this->objectGroups[$key]['id'];
$meta['object_group_title'] = $this->objectGroups[$key]['title'];
$meta['object_group_order'] = $this->objectGroups[$key]['order'];
}
@@ -205,9 +143,9 @@ class PiggyBankEnrichment implements EnrichmentInterface
'current_amount' => Steam::bcround($row['current_amount'], $currency->decimal_places),
'pc_current_amount' => Steam::bcround($row['pc_current_amount'], $this->primaryCurrency->decimal_places),
];
$meta['current_amount'] = bcadd($meta['current_amount'], $row['current_amount']);
$meta['current_amount'] = bcadd($meta['current_amount'], (string) $row['current_amount']);
// only add pc_current_amount when the pc_current_amount is set
$meta['pc_current_amount'] = null === $row['pc_current_amount'] ? null : bcadd($meta['pc_current_amount'], $row['pc_current_amount']);
$meta['pc_current_amount'] = null === $row['pc_current_amount'] ? null : bcadd((string) $meta['pc_current_amount'], (string) $row['pc_current_amount']);
}
$meta['current_amount'] = Steam::bcround($meta['current_amount'], $currency->decimal_places);
// only round this number when pc_current_amount is set.
@@ -215,13 +153,13 @@ class PiggyBankEnrichment implements EnrichmentInterface
// calculate left to save, only when there is a target amount.
if (null !== $targetAmount) {
$meta['left_to_save'] = bcsub($meta['target_amount'], $meta['current_amount']);
$meta['pc_left_to_save'] = null === $meta['pc_target_amount'] ? null : bcsub($meta['pc_target_amount'], $meta['pc_current_amount']);
$meta['left_to_save'] = bcsub((string) $meta['target_amount'], (string) $meta['current_amount']);
$meta['pc_left_to_save'] = null === $meta['pc_target_amount'] ? null : bcsub((string) $meta['pc_target_amount'], (string) $meta['pc_current_amount']);
}
// get suggested per month.
$meta['save_per_month'] = Steam::bcround($this->getSuggestedMonthlyAmount($item->start_date, $item->target_date, $meta['target_amount'], $meta['current_amount']), $currency->decimal_places);
$meta['pc_save_per_month'] = Steam::bcround($this->getSuggestedMonthlyAmount($item->start_date, $item->target_date, $meta['pc_target_amount'], $meta['pc_current_amount']), $currency->decimal_places);
$meta['save_per_month'] = Steam::bcround($this->getSuggestedMonthlyAmount($this->date, $item->target_date, $meta['target_amount'], $meta['current_amount']), $currency->decimal_places);
$meta['pc_save_per_month'] = Steam::bcround($this->getSuggestedMonthlyAmount($this->date, $item->target_date, $meta['pc_target_amount'], $meta['pc_current_amount']), $currency->decimal_places);
$item->meta = $meta;
@@ -229,6 +167,74 @@ class PiggyBankEnrichment implements EnrichmentInterface
});
}
public function setDate(?Carbon $date): void
{
$this->date = $date;
}
private function collectIds(): void
{
/** @var PiggyBank $piggy */
foreach ($this->collection as $piggy) {
$id = (int)$piggy->id;
$this->ids[] = $id;
$this->currencyIds[$id] = (int)$piggy->transaction_currency_id;
}
$this->ids = array_unique($this->ids);
// collect currencies.
$currencies = TransactionCurrency::whereIn('id', $this->currencyIds)->get();
foreach ($currencies as $currency) {
$this->currencies[(int)$currency->id] = $currency;
}
// collect accounts
$set = DB::table('account_piggy_bank')->whereIn('piggy_bank_id', $this->ids)->get(['piggy_bank_id', 'account_id', 'current_amount', 'native_current_amount']);
foreach ($set as $item) {
$id = (int)$item->piggy_bank_id;
$accountId = (int)$item->account_id;
$this->amounts[$id] ??= [];
if (!array_key_exists($id, $this->accountIds)) {
$this->accountIds[$id] = (int)$item->account_id;
}
if (!array_key_exists($accountId, $this->amounts[$id])) {
$this->amounts[$id][$accountId] = [
'current_amount' => '0',
'pc_current_amount' => '0',
];
}
$this->amounts[$id][$accountId]['current_amount'] = bcadd((string) $this->amounts[$id][$accountId]['current_amount'], (string)$item->current_amount);
if (null !== $this->amounts[$id][$accountId]['pc_current_amount'] && null !== $item->native_current_amount) {
$this->amounts[$id][$accountId]['pc_current_amount'] = bcadd($this->amounts[$id][$accountId]['pc_current_amount'], (string)$item->native_current_amount);
}
}
// get account currency preference for ALL.
$set = AccountMeta::whereIn('account_id', array_values($this->accountIds))->where('name', 'currency_id')->get();
/** @var AccountMeta $item */
foreach ($set as $item) {
$accountId = (int)$item->account_id;
$currencyId = (int)$item->data;
if (!array_key_exists($currencyId, $this->currencies)) {
$this->currencies[$currencyId] = Amount::getTransactionCurrencyById($currencyId);
}
// $this->accountCurrencies[$accountId] = $this->currencies[$currencyId];
}
// get account info.
$set = Account::whereIn('id', array_values($this->accountIds))->get();
/** @var Account $item */
foreach ($set as $item) {
$id = (int)$item->id;
$this->accounts[$id] = [
'id' => $id,
'name' => $item->name,
];
}
}
private function collectNotes(): void
{
$notes = Note::query()->whereIn('noteable_id', $this->ids)
@@ -264,8 +270,6 @@ class PiggyBankEnrichment implements EnrichmentInterface
}
}
private function collectCurrentAmounts(): void {}
/**
* Returns the suggested amount the user should save per month, or "".
*/

View File

@@ -38,16 +38,16 @@ use Illuminate\Support\Facades\Log;
class PiggyBankEventEnrichment implements EnrichmentInterface
{
private User $user; // @phpstan-ignore-line
private UserGroup $userGroup; // @phpstan-ignore-line
private array $accountCurrencies = []; // @phpstan-ignore-line
private array $accountIds = []; // @phpstan-ignore-line
private Collection $collection;
private array $currencies = [];
private array $groupIds = [];
private array $ids = [];
private array $journalIds = [];
private array $groupIds = [];
private array $accountIds = [];
private array $piggyBankIds = [];
private array $accountCurrencies = [];
private array $currencies = [];
private User $user;
private UserGroup $userGroup;
// private bool $convertToPrimary = false;
// private TransactionCurrency $primaryCurrency;
@@ -86,6 +86,30 @@ class PiggyBankEventEnrichment implements EnrichmentInterface
$this->userGroup = $userGroup;
}
private function appendCollectedData(): void
{
$this->collection = $this->collection->map(function (PiggyBankEvent $item) {
$id = (int)$item->id;
$piggyId = (int)$item->piggy_bank_id;
$journalId = (int)$item->transaction_journal_id;
$currency = null;
if (array_key_exists($piggyId, $this->accountIds)) {
$accountId = $this->accountIds[$piggyId];
if (array_key_exists($accountId, $this->accountCurrencies)) {
$currency = $this->accountCurrencies[$accountId];
}
}
$meta = [
'transaction_group_id' => array_key_exists($journalId, $this->groupIds) ? (string)$this->groupIds[$journalId] : null,
'currency' => $currency,
];
$item->meta = $meta;
return $item;
});
}
private function collectIds(): void
{
/** @var PiggyBankEvent $event */
@@ -125,28 +149,4 @@ class PiggyBankEventEnrichment implements EnrichmentInterface
$this->accountCurrencies[$accountId] = $this->currencies[$currencyId];
}
}
private function appendCollectedData(): void
{
$this->collection = $this->collection->map(function (PiggyBankEvent $item) {
$id = (int)$item->id;
$piggyId = (int)$item->piggy_bank_id;
$journalId = (int)$item->transaction_journal_id;
$currency = null;
if (array_key_exists($piggyId, $this->accountIds)) {
$accountId = $this->accountIds[$piggyId];
if (array_key_exists($accountId, $this->accountCurrencies)) {
$currency = $this->accountCurrencies[$accountId];
}
}
$meta = [
'transaction_group_id' => array_key_exists($journalId, $this->groupIds) ? (string)$this->groupIds[$journalId] : null,
'currency' => $currency,
];
$item->meta = $meta;
return $item;
});
}
}

Some files were not shown because too many files have changed in this diff Show More