Compare commits

..

125 Commits

Author SHA1 Message Date
github-actions[bot]
e74163a7ec Merge pull request #11663 from firefly-iii/release-1770445570
🤖 Automatically merge the PR into the develop branch.
2026-02-07 07:26:17 +01:00
JC5
c60094d231 🤖 Auto commit for release 'develop' on 2026-02-07 2026-02-07 07:26:10 +01:00
James Cole
39d46d469c Fix query parser logging. 2026-02-07 06:53:12 +01:00
github-actions[bot]
6caea5ffa3 Merge pull request #11662 from firefly-iii/release-1770442761
🤖 Automatically merge the PR into the develop branch.
2026-02-07 06:39:28 +01:00
JC5
4024f76a51 🤖 Auto commit for release 'develop' on 2026-02-07 2026-02-07 06:39:21 +01:00
James Cole
de84946371 Expand changelog. 2026-02-07 06:33:30 +01:00
James Cole
6d4aca54de Fix #11246 2026-02-07 06:32:11 +01:00
James Cole
256262b2ba Fix #11657 2026-02-07 06:16:23 +01:00
James Cole
fb035ba594 Fix #11660 2026-02-07 06:09:41 +01:00
James Cole
20776949a6 Clean up changelog. 2026-02-06 18:32:55 +01:00
github-actions[bot]
ad5a8a2934 Merge pull request #11656 from firefly-iii/release-1770398774
🤖 Automatically merge the PR into the develop branch.
2026-02-06 18:26:25 +01:00
JC5
e37ef69491 🤖 Auto commit for release 'develop' on 2026-02-06 2026-02-06 18:26:14 +01:00
James Cole
df8a406c58 Fix issue with email change. 2026-02-06 18:12:52 +01:00
James Cole
88d3e01065 Add events for opening balance. 2026-02-06 18:10:41 +01:00
James Cole
7a1c32f1aa Expand changelog. 2026-02-06 15:58:45 +01:00
James Cole
54df0d44f7 Clean up events 2026-02-06 15:47:34 +01:00
James Cole
1f7775032b Fix budgeted amounts. 2026-02-06 15:38:32 +01:00
github-actions[bot]
8cfe1e8047 Merge pull request #11655 from firefly-iii/release-1770383285
🤖 Automatically merge the PR into the develop branch.
2026-02-06 14:08:15 +01:00
JC5
f0be634829 🤖 Auto commit for release 'develop' on 2026-02-06 2026-02-06 14:08:05 +01:00
James Cole
485e1138f8 Merge branch 'develop' of github.com:firefly-iii/firefly-iii into develop 2026-02-06 14:03:26 +01:00
James Cole
9abf08b3be Fix view range. 2026-02-06 14:02:37 +01:00
github-actions[bot]
f08943b926 Merge pull request #11654 from firefly-iii/release-1770382517
🤖 Automatically merge the PR into the develop branch.
2026-02-06 13:55:25 +01:00
JC5
2de9926db8 🤖 Auto commit for release 'develop' on 2026-02-06 2026-02-06 13:55:17 +01:00
James Cole
b4d01d464d Clean up more events. 2026-02-06 13:49:50 +01:00
James Cole
229b45c7ad Clean up events for budgets. 2026-02-06 13:47:18 +01:00
James Cole
8e89c5af62 Clean up budget limit events. 2026-02-06 08:33:10 +01:00
James Cole
264fec7a6a Clean up budget limit events. 2026-02-06 08:20:04 +01:00
James Cole
d4b1d097fe Clean up events, although handler is still a little sloppy. 2026-02-06 07:23:31 +01:00
James Cole
bbd6acb824 Create events and respond to budget limit changes. 2026-02-06 06:24:57 +01:00
James Cole
4e7d12f06b Make sure webhook messages are sent. 2026-02-06 06:04:52 +01:00
James Cole
9811583379 Merge branch 'develop' of github.com:firefly-iii/firefly-iii into develop
# Conflicts:
#	app/Http/Controllers/Transaction/DeleteController.php
2026-02-06 05:59:34 +01:00
James Cole
0063cab690 Clean up config. 2026-02-06 05:59:03 +01:00
github-actions[bot]
d3add7c92b Merge pull request #11647 from firefly-iii/release-1770268490
🤖 Automatically merge the PR into the develop branch.
2026-02-05 06:14:57 +01:00
JC5
a491e4921f 🤖 Auto commit for release 'develop' on 2026-02-05 2026-02-05 06:14:50 +01:00
James Cole
171bc03668 Fix running balance events. 2026-02-05 06:10:25 +01:00
James Cole
dd5476bfc7 Clean up events and filters. 2026-02-05 06:02:32 +01:00
James Cole
bc0769358d Clean up update handlers. 2026-02-05 05:51:44 +01:00
James Cole
ccf33f1db6 Also include delete event in new event triggers. 2026-02-05 05:47:37 +01:00
github-actions[bot]
35f611b3f2 Merge pull request #11645 from firefly-iii/release-1770234250
🤖 Automatically merge the PR into the develop branch.
2026-02-04 20:44:18 +01:00
JC5
e5d394533c 🤖 Auto commit for release 'develop' on 2026-02-04 2026-02-04 20:44:10 +01:00
James Cole
831d39a41e Catch missing nonce 2026-02-04 20:39:54 +01:00
James Cole
2920a9b9e3 Fix call. 2026-02-04 20:39:01 +01:00
James Cole
5c8204e963 Unify more event handlers. 2026-02-04 20:29:28 +01:00
James Cole
d25283f193 Clean up processing for group. 2026-02-04 20:17:47 +01:00
github-actions[bot]
20986e6426 Merge pull request #11644 from firefly-iii/release-1770218546
🤖 Automatically merge the PR into the develop branch.
2026-02-04 16:22:34 +01:00
JC5
9cd0ebe37e 🤖 Auto commit for release 'develop' on 2026-02-04 2026-02-04 16:22:26 +01:00
Sander Dorigo
9c2b83a971 Update event handlers 2026-02-04 16:16:27 +01:00
Sander Dorigo
e1d32da409 New event handler object 2026-02-04 08:29:09 +01:00
Sander Dorigo
c51df8cd83 Move events to service and repos 2026-02-04 08:18:35 +01:00
github-actions[bot]
9f016aed16 Merge pull request #11643 from firefly-iii/release-1770188788
🤖 Automatically merge the PR into the develop branch.
2026-02-04 08:06:37 +01:00
JC5
27df5ea800 🤖 Auto commit for release 'develop' on 2026-02-04 2026-02-04 08:06:28 +01:00
Sander Dorigo
2d7cdd36f0 Fix null pointer 2026-02-04 08:01:47 +01:00
github-actions[bot]
7888023c1a Merge pull request #11642 from firefly-iii/release-1770187820
🤖 Automatically merge the PR into the develop branch.
2026-02-04 07:50:28 +01:00
JC5
3032118788 🤖 Auto commit for release 'develop' on 2026-02-04 2026-02-04 07:50:20 +01:00
James Cole
89d96ddc17 Remove unused events. 2026-02-04 05:48:51 +01:00
James Cole
97c9937571 Clean up events for groups. 2026-02-04 05:46:19 +01:00
James Cole
c46aca0594 Clean up more events. 2026-02-03 21:04:07 +01:00
James Cole
0b33e1ff09 Refactor recalculation service. 2026-02-03 20:43:52 +01:00
James Cole
d267d2a0b0 Remove unused event handler. 2026-02-03 20:29:58 +01:00
James Cole
f2996dcebe Clean up event handlers and other code. 2026-02-03 20:24:16 +01:00
James Cole
ebb6a186cc Add some event handlers. 2026-02-03 18:58:24 +01:00
James Cole
304fae439a Expand listeners and observers. 2026-02-03 05:42:50 +01:00
James Cole
9ca81cf305 Clean up observer. 2026-02-02 20:15:06 +01:00
James Cole
cad5fb6d6b Clean up budget limit observer. 2026-02-02 20:13:00 +01:00
James Cole
53efafdbb2 Rename observers 2026-02-02 20:09:23 +01:00
James Cole
7922017288 Rename observers 2026-02-02 20:08:37 +01:00
James Cole
4e910a33dd Clean up events. 2026-02-02 20:02:45 +01:00
James Cole
bb031cdeb6 Clean up simple observers. 2026-02-02 19:59:05 +01:00
James Cole
60026bbcba Fix convert to primary amount. 2026-02-02 19:55:18 +01:00
James Cole
e96a1850da Merge branch 'develop' of github.com:firefly-iii/firefly-iii into develop 2026-02-02 19:38:00 +01:00
James Cole
bd2a746b8a Add new function. 2026-02-02 19:37:53 +01:00
github-actions[bot]
e4a3cbc9da Merge pull request #11640 from firefly-iii/release-1770044183
🤖 Automatically merge the PR into the develop branch.
2026-02-02 15:56:32 +01:00
JC5
18734b0edd 🤖 Auto commit for release 'develop' on 2026-02-02 2026-02-02 15:56:24 +01:00
Sander Dorigo
f52b3bf5f5 Merge branch 'develop' of https://github.com/firefly-iii/firefly-iii into develop 2026-02-02 15:39:06 +01:00
Sander Dorigo
3abba71f8d Fix null pointer 2026-02-02 15:39:03 +01:00
github-actions[bot]
95bdc87ed7 Merge pull request #11639 from firefly-iii/release-1770040333
🤖 Automatically merge the PR into the develop branch.
2026-02-02 14:52:22 +01:00
JC5
9a66b4017b 🤖 Auto commit for release 'develop' on 2026-02-02 2026-02-02 14:52:13 +01:00
Sander Dorigo
610e3f3ae5 Try to fix null pointer 2026-02-02 14:39:03 +01:00
James Cole
a58e70c08b Merge pull request #11632 from mateuszkulapl/main
fix v2 layout dashboard transactions load
2026-02-02 08:42:54 +01:00
mergify[bot]
24c96d40c9 Merge branch 'develop' into main 2026-02-02 03:56:28 +00:00
github-actions[bot]
74ae59910f Merge pull request #11636 from firefly-iii/release-1770004540
🤖 Automatically merge the PR into the develop branch.
2026-02-02 04:55:48 +01:00
JC5
6d49815be9 🤖 Auto commit for release 'develop' on 2026-02-02 2026-02-02 04:55:40 +01:00
mergify[bot]
ae680cd41f Merge branch 'develop' into main 2026-02-01 12:11:25 +00:00
mateuszkulapl
5e9ea1ca10 fix v2 layout dashboard transactions load
update transactions function to accept multiple params (page, start, end)
to match usage in loadAccounts (resources/assets/v2/src/pages/dashboard/accounts.js)
2026-02-01 13:05:52 +01:00
James Cole
6f558f424d Clean up notifications. 2026-01-31 11:51:34 +01:00
James Cole
4387876203 Replace lengthy notification calls, simplifies code. 2026-01-31 11:49:04 +01:00
James Cole
1b1ce3e04e Fix more events #11544 2026-01-31 10:41:48 +01:00
James Cole
ff64675122 Fix stored account event. 2026-01-31 10:34:26 +01:00
James Cole
1c545b7a74 Merge branch 'main' into develop 2026-01-31 10:27:40 +01:00
James Cole
a433ddcd7e Clean up events. 2026-01-31 10:27:12 +01:00
James Cole
b0e1b6fe51 Merge pull request #11624 from firefly-iii/dependabot/composer/composer-2f4529dfab
Bump symfony/process from 7.4.3 to 7.4.5 in the composer group across 1 directory
2026-01-29 05:22:35 +01:00
dependabot[bot]
f5523e60b6 Bump symfony/process in the composer group across 1 directory
Bumps the composer group with 1 update in the / directory: [symfony/process](https://github.com/symfony/process).


Updates `symfony/process` from 7.4.3 to 7.4.5
- [Release notes](https://github.com/symfony/process/releases)
- [Changelog](https://github.com/symfony/process/blob/8.1/CHANGELOG.md)
- [Commits](https://github.com/symfony/process/compare/v7.4.3...v7.4.5)

---
updated-dependencies:
- dependency-name: symfony/process
  dependency-version: 7.4.5
  dependency-type: indirect
  dependency-group: composer
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-01-29 04:08:55 +00:00
James Cole
e300efe640 Merge pull request #11623 from firefly-iii/dependabot/composer/dot-ci/php-cs-fixer/composer-5ee8f56ee5
Bump symfony/process from 8.0.3 to 8.0.5 in /.ci/php-cs-fixer in the composer group across 1 directory
2026-01-29 05:07:50 +01:00
dependabot[bot]
a97f227ddb Bump symfony/process
Bumps the composer group with 1 update in the /.ci/php-cs-fixer directory: [symfony/process](https://github.com/symfony/process).


Updates `symfony/process` from 8.0.3 to 8.0.5
- [Release notes](https://github.com/symfony/process/releases)
- [Changelog](https://github.com/symfony/process/blob/8.1/CHANGELOG.md)
- [Commits](https://github.com/symfony/process/compare/v8.0.3...v8.0.5)

---
updated-dependencies:
- dependency-name: symfony/process
  dependency-version: 8.0.5
  dependency-type: indirect
  dependency-group: composer
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-01-28 21:29:36 +00:00
github-actions[bot]
5f8d7049b5 Merge pull request #11622 from firefly-iii/release-1769628396
🤖 Automatically merge the PR into the develop branch.
2026-01-28 20:26:45 +01:00
JC5
7e80f78f2e 🤖 Auto commit for release 'develop' on 2026-01-28 2026-01-28 20:26:36 +01:00
James Cole
ad922745c4 Fix #11620 2026-01-28 20:22:11 +01:00
github-actions[bot]
40abe74dc1 Merge pull request #11621 from firefly-iii/release-1769627627
🤖 Automatically merge the PR into the develop branch.
2026-01-28 20:13:55 +01:00
JC5
e5d2c4d163 🤖 Auto commit for release 'develop' on 2026-01-28 2026-01-28 20:13:48 +01:00
James Cole
2851053900 Fix nullpointer. 2026-01-28 20:08:32 +01:00
James Cole
b2f6ce1277 Merge pull request #11615 from nick322/feat/11614 2026-01-28 19:38:31 +01:00
Nick Huang
340b0661ba feat(#11614): Add New Taiwan Dollar to Currency Seeder 2026-01-28 15:57:52 +08:00
github-actions[bot]
be18f11f8c Merge pull request #11613 from firefly-iii/release-1769573331
🤖 Automatically merge the PR into the develop branch.
2026-01-28 05:08:59 +01:00
JC5
2f8ee67b31 🤖 Auto commit for release 'develop' on 2026-01-28 2026-01-28 05:08:52 +01:00
James Cole
1ecf55165e Merge branch 'main' into develop 2026-01-28 05:02:30 +01:00
James Cole
5aceccde4a Fix method call. 2026-01-28 05:02:14 +01:00
James Cole
abfaee5a55 Merge pull request #11612 from firefly-iii/dependabot/composer/composer-63bdf6e023 2026-01-28 04:17:00 +01:00
dependabot[bot]
fa65cc7ee2 Bump phpunit/phpunit in the composer group across 1 directory
Bumps the composer group with 1 update in the / directory: [phpunit/phpunit](https://github.com/sebastianbergmann/phpunit).


Updates `phpunit/phpunit` from 12.5.6 to 12.5.8
- [Release notes](https://github.com/sebastianbergmann/phpunit/releases)
- [Changelog](https://github.com/sebastianbergmann/phpunit/blob/12.5.8/ChangeLog-12.5.md)
- [Commits](https://github.com/sebastianbergmann/phpunit/compare/12.5.6...12.5.8)

---
updated-dependencies:
- dependency-name: phpunit/phpunit
  dependency-version: 12.5.8
  dependency-type: direct:development
  dependency-group: composer
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-01-27 22:40:18 +00:00
James Cole
d64f1d0c18 Do not report ModelNotFoundException 2026-01-27 20:18:04 +01:00
github-actions[bot]
31206ce56c Merge pull request #11608 from firefly-iii/release-1769541194
🤖 Automatically merge the PR into the develop branch.
2026-01-27 20:13:21 +01:00
JC5
e4e9a09522 🤖 Auto commit for release 'develop' on 2026-01-27 2026-01-27 20:13:14 +01:00
James Cole
11303dc6e2 Merge branch 'main' into develop 2026-01-27 20:08:54 +01:00
James Cole
993f5cd292 Add language files. 2026-01-27 20:08:43 +01:00
github-actions[bot]
cc0854c712 Merge pull request #11607 from firefly-iii/release-1769540197
🤖 Automatically merge the PR into the develop branch.
2026-01-27 19:56:46 +01:00
JC5
5c6aee0037 🤖 Auto commit for release 'develop' on 2026-01-27 2026-01-27 19:56:37 +01:00
James Cole
391f8c34cc Merge branch 'develop' of github.com:firefly-iii/firefly-iii into develop 2026-01-27 19:50:53 +01:00
James Cole
db6ed26d5a Fix bad pointer 2026-01-27 19:50:46 +01:00
github-actions[bot]
eece951036 Merge pull request #11606 from firefly-iii/release-1769539261
🤖 Automatically merge the PR into the develop branch.
2026-01-27 19:41:10 +01:00
JC5
3d7a62293b 🤖 Auto commit for release 'develop' on 2026-01-27 2026-01-27 19:41:01 +01:00
James Cole
2691dbe438 Add flag. 2026-01-27 19:36:35 +01:00
James Cole
fe971ec611 Add new setting. 2026-01-27 19:35:14 +01:00
github-actions[bot]
9e4c5435f0 Merge pull request #11605 from firefly-iii/release-1769538639
🤖 Automatically merge the PR into the develop branch.
2026-01-27 19:30:49 +01:00
JC5
cdb2b91813 🤖 Auto commit for release 'develop' on 2026-01-27 2026-01-27 19:30:39 +01:00
James Cole
f4cf158d21 Merge branch 'develop' of github.com:firefly-iii/firefly-iii into develop
# Conflicts:
#	app/Factory/TransactionJournalFactory.php
2026-01-27 19:25:56 +01:00
James Cole
b19f1d0353 Restore use of NullArrayObject 2026-01-27 19:25:23 +01:00
475 changed files with 20587 additions and 20879 deletions

View File

@@ -26,6 +26,7 @@ $paths = [
$current . '/../../config',
$current . '/../../routes',
$current . '/../../tests',
$current . '/../../resources/lang/en_US',
];
$finder = PhpCsFixer\Finder::create()

View File

@@ -402,16 +402,16 @@
},
{
"name": "friendsofphp/php-cs-fixer",
"version": "v3.93.0",
"version": "v3.93.1",
"source": {
"type": "git",
"url": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer.git",
"reference": "50895a07cface1385082e4caa6a6786c4e033468"
"reference": "b3546ab487c0762c39f308dc1ec0ea2c461fc21a"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/PHP-CS-Fixer/PHP-CS-Fixer/zipball/50895a07cface1385082e4caa6a6786c4e033468",
"reference": "50895a07cface1385082e4caa6a6786c4e033468",
"url": "https://api.github.com/repos/PHP-CS-Fixer/PHP-CS-Fixer/zipball/b3546ab487c0762c39f308dc1ec0ea2c461fc21a",
"reference": "b3546ab487c0762c39f308dc1ec0ea2c461fc21a",
"shasum": ""
},
"require": {
@@ -494,7 +494,7 @@
],
"support": {
"issues": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/issues",
"source": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/tree/v3.93.0"
"source": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/tree/v3.93.1"
},
"funding": [
{
@@ -502,7 +502,7 @@
"type": "github"
}
],
"time": "2026-01-23T17:33:21+00:00"
"time": "2026-01-28T23:50:50+00:00"
},
{
"name": "psr/container",
@@ -1640,16 +1640,16 @@
},
{
"name": "symfony/finder",
"version": "v8.0.4",
"version": "v8.0.5",
"source": {
"type": "git",
"url": "https://github.com/symfony/finder.git",
"reference": "42e48eb02e07d5f3771d194d67da117eb824c8c1"
"reference": "8bd576e97c67d45941365bf824e18dc8538e6eb0"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/finder/zipball/42e48eb02e07d5f3771d194d67da117eb824c8c1",
"reference": "42e48eb02e07d5f3771d194d67da117eb824c8c1",
"url": "https://api.github.com/repos/symfony/finder/zipball/8bd576e97c67d45941365bf824e18dc8538e6eb0",
"reference": "8bd576e97c67d45941365bf824e18dc8538e6eb0",
"shasum": ""
},
"require": {
@@ -1684,7 +1684,7 @@
"description": "Finds files and directories via an intuitive fluent interface",
"homepage": "https://symfony.com",
"support": {
"source": "https://github.com/symfony/finder/tree/v8.0.4"
"source": "https://github.com/symfony/finder/tree/v8.0.5"
},
"funding": [
{
@@ -1704,7 +1704,7 @@
"type": "tidelift"
}
],
"time": "2026-01-12T12:37:40+00:00"
"time": "2026-01-26T15:08:38+00:00"
},
{
"name": "symfony/options-resolver",
@@ -2358,16 +2358,16 @@
},
{
"name": "symfony/process",
"version": "v8.0.4",
"version": "v8.0.5",
"source": {
"type": "git",
"url": "https://github.com/symfony/process.git",
"reference": "10df72602d88c0a3fa685b822976a052611dd607"
"reference": "b5f3aa6762e33fd95efbaa2ec4f4bc9fdd16d674"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/process/zipball/10df72602d88c0a3fa685b822976a052611dd607",
"reference": "10df72602d88c0a3fa685b822976a052611dd607",
"url": "https://api.github.com/repos/symfony/process/zipball/b5f3aa6762e33fd95efbaa2ec4f4bc9fdd16d674",
"reference": "b5f3aa6762e33fd95efbaa2ec4f4bc9fdd16d674",
"shasum": ""
},
"require": {
@@ -2399,7 +2399,7 @@
"description": "Executes commands in sub-processes",
"homepage": "https://symfony.com",
"support": {
"source": "https://github.com/symfony/process/tree/v8.0.4"
"source": "https://github.com/symfony/process/tree/v8.0.5"
},
"funding": [
{
@@ -2419,7 +2419,7 @@
"type": "tidelift"
}
],
"time": "2026-01-23T11:07:10+00:00"
"time": "2026-01-26T15:08:38+00:00"
},
{
"name": "symfony/service-contracts",

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.
## 2026
- Nick Huang
- mateuszkulapl
- Gianluca Martino
- embedded

View File

@@ -31,7 +31,6 @@ use FireflyIII\Enums\UserRoleEnum;
use FireflyIII\Exceptions\FireflyException;
use FireflyIII\Models\Account;
use FireflyIII\Repositories\Account\AccountRepositoryInterface;
use FireflyIII\Support\Debug\Timer;
use FireflyIII\Support\Facades\Amount;
use FireflyIII\Support\Facades\Steam;
use FireflyIII\Support\Http\Api\AccountFilter;
@@ -80,7 +79,7 @@ class AccountController extends Controller
*/
public function accounts(AutocompleteApiRequest $request): JsonResponse
{
Log::debug('Before All.');
// Log::debug('Before All.');
['types' => $types, 'query' => $query, 'date' => $date, 'limit' => $limit] = $request->attributes->all();
$date ??= today(config('app.timezone'));
@@ -89,8 +88,6 @@ class AccountController extends Controller
$date->endOfDay();
$return = [];
$timer = Timer::getInstance();
$timer->start(sprintf('AC accounts "%s"', $query));
$result = $this->repository->searchAccount((string) $query, $types, $limit);
$allBalances = Steam::accountsBalancesOptimized($result, $date, $this->primaryCurrency, $this->convertToPrimary);
@@ -136,7 +133,6 @@ class AccountController extends Controller
return $posA - $posB;
});
$timer->stop(sprintf('AC accounts "%s"', $query));
return response()->api($return);
}

View File

@@ -104,6 +104,48 @@ class BudgetController extends Controller
return response()->json($this->clean($data));
}
private function filterLimit(int $currencyId, Collection $limits): ?BudgetLimit
{
$amount = '0';
$limit = null;
$converter = new ExchangeRateConverter();
/** @var BudgetLimit $current */
foreach ($limits as $current) {
if ($this->convertToPrimary) {
if ($current->transaction_currency_id === $this->primaryCurrency->id) {
// simply add it.
$amount = bcadd($amount, (string) $current->amount);
Log::debug(sprintf('Set amount in limit to %s', $amount));
}
if ($current->transaction_currency_id !== $this->primaryCurrency->id) {
// convert and then add it.
$converted = $converter->convert($current->transactionCurrency, $this->primaryCurrency, $current->start_date, $current->amount);
$amount = bcadd($amount, $converted);
Log::debug(sprintf(
'Budgeted in limit #%d: %s %s, converted to %s %s',
$current->id,
$current->transactionCurrency->code,
$current->amount,
$this->primaryCurrency->code,
$converted
));
Log::debug(sprintf('Set amount in limit to %s', $amount));
}
}
if ($current->transaction_currency_id === $currencyId) {
$limit = $current;
}
}
if (null !== $limit && $this->convertToPrimary) {
// convert and add all amounts.
$limit->amount = Steam::positive($amount);
Log::debug(sprintf('Final amount in limit with converted amount %s', $limit->amount));
}
return $limit;
}
/**
* @throws FireflyException
*/
@@ -250,46 +292,4 @@ class BudgetController extends Controller
return $return;
}
private function filterLimit(int $currencyId, Collection $limits): ?BudgetLimit
{
$amount = '0';
$limit = null;
$converter = new ExchangeRateConverter();
/** @var BudgetLimit $current */
foreach ($limits as $current) {
if ($this->convertToPrimary) {
if ($current->transaction_currency_id === $this->primaryCurrency->id) {
// simply add it.
$amount = bcadd($amount, (string) $current->amount);
Log::debug(sprintf('Set amount in limit to %s', $amount));
}
if ($current->transaction_currency_id !== $this->primaryCurrency->id) {
// convert and then add it.
$converted = $converter->convert($current->transactionCurrency, $this->primaryCurrency, $current->start_date, $current->amount);
$amount = bcadd($amount, $converted);
Log::debug(sprintf(
'Budgeted in limit #%d: %s %s, converted to %s %s',
$current->id,
$current->transactionCurrency->code,
$current->amount,
$this->primaryCurrency->code,
$converted
));
Log::debug(sprintf('Set amount in limit to %s', $amount));
}
}
if ($current->transaction_currency_id === $currencyId) {
$limit = $current;
}
}
if (null !== $limit && $this->convertToPrimary) {
// convert and add all amounts.
$limit->amount = Steam::positive($amount);
Log::debug(sprintf('Final amount in limit with converted amount %s', $limit->amount));
}
return $limit;
}
}

View File

@@ -98,6 +98,77 @@ abstract class Controller extends BaseController
});
}
/**
* Method to help build URL's.
*/
final protected function buildParams(): string
{
$return = '?';
$params = [];
foreach ($this->parameters as $key => $value) {
if ('page' === $key) {
continue;
}
if ($value instanceof Carbon) {
$params[$key] = $value->format('Y-m-d');
continue;
}
$params[$key] = $value;
}
return $return.http_build_query($params);
}
final protected function getManager(): Manager
{
// create some objects:
$manager = new Manager();
$baseUrl = request()->getSchemeAndHttpHost().'/api/v1';
$manager->setSerializer(new JsonApiSerializer($baseUrl));
return $manager;
}
final protected function jsonApiList(string $key, LengthAwarePaginator $paginator, AbstractTransformer $transformer): array
{
$manager = new Manager();
$baseUrl = sprintf('%s/api/v1/', request()->getSchemeAndHttpHost());
// TODO add stuff to path?
$manager->setSerializer(new JsonApiSerializer($baseUrl));
$objects = $paginator->getCollection();
// the transformer, at this point, needs to collect information that ALL items in the collection
// require, like meta-data and stuff like that, and save it for later.
// $objects = $transformer->collectMetaData($objects);
$paginator->setCollection($objects);
$resource = new FractalCollection($objects, $transformer, $key);
$resource->setPaginator(new IlluminatePaginatorAdapter($paginator));
return $manager->createData($resource)->toArray();
}
/**
* Returns a JSON API object and returns it.
*
* @param array<int, mixed>|Model $object
*/
final protected function jsonApiObject(string $key, array|Model $object, AbstractTransformer $transformer): array
{
// create some objects:
$manager = new Manager();
$baseUrl = sprintf('%s/api/v1', request()->getSchemeAndHttpHost());
$manager->setSerializer(new JsonApiSerializer($baseUrl));
$resource = new Item($object, $transformer, $key);
return $manager->createData($resource)->toArray();
}
#[Deprecated(message: <<<'TXT'
use Request classes
Method to grab all parameters from the URL
@@ -170,75 +241,4 @@ abstract class Controller extends BaseController
// return $this->getSortParameters($bag);
}
/**
* Method to help build URL's.
*/
final protected function buildParams(): string
{
$return = '?';
$params = [];
foreach ($this->parameters as $key => $value) {
if ('page' === $key) {
continue;
}
if ($value instanceof Carbon) {
$params[$key] = $value->format('Y-m-d');
continue;
}
$params[$key] = $value;
}
return $return.http_build_query($params);
}
final protected function getManager(): Manager
{
// create some objects:
$manager = new Manager();
$baseUrl = request()->getSchemeAndHttpHost().'/api/v1';
$manager->setSerializer(new JsonApiSerializer($baseUrl));
return $manager;
}
final protected function jsonApiList(string $key, LengthAwarePaginator $paginator, AbstractTransformer $transformer): array
{
$manager = new Manager();
$baseUrl = sprintf('%s/api/v1/', request()->getSchemeAndHttpHost());
// TODO add stuff to path?
$manager->setSerializer(new JsonApiSerializer($baseUrl));
$objects = $paginator->getCollection();
// the transformer, at this point, needs to collect information that ALL items in the collection
// require, like meta-data and stuff like that, and save it for later.
// $objects = $transformer->collectMetaData($objects);
$paginator->setCollection($objects);
$resource = new FractalCollection($objects, $transformer, $key);
$resource->setPaginator(new IlluminatePaginatorAdapter($paginator));
return $manager->createData($resource)->toArray();
}
/**
* Returns a JSON API object and returns it.
*
* @param array<int, mixed>|Model $object
*/
final protected function jsonApiObject(string $key, array|Model $object, AbstractTransformer $transformer): array
{
// create some objects:
$manager = new Manager();
$baseUrl = sprintf('%s/api/v1', request()->getSchemeAndHttpHost());
$manager->setSerializer(new JsonApiSerializer($baseUrl));
$resource = new Item($object, $transformer, $key);
return $manager->createData($resource)->toArray();
}
}

View File

@@ -137,70 +137,6 @@ class DestroyController extends Controller
return response()->json([], 204);
}
private function destroyBudgets(): void
{
/** @var AvailableBudgetRepositoryInterface $abRepository */
$abRepository = app(AvailableBudgetRepositoryInterface::class);
$abRepository->destroyAll();
/** @var BudgetLimitRepositoryInterface $blRepository */
$blRepository = app(BudgetLimitRepositoryInterface::class);
$blRepository->destroyAll();
/** @var BudgetRepositoryInterface $budgetRepository */
$budgetRepository = app(BudgetRepositoryInterface::class);
$budgetRepository->destroyAll();
}
private function destroyBills(): void
{
/** @var BillRepositoryInterface $repository */
$repository = app(BillRepositoryInterface::class);
$repository->destroyAll();
}
private function destroyPiggyBanks(): void
{
/** @var PiggyBankRepositoryInterface $repository */
$repository = app(PiggyBankRepositoryInterface::class);
$repository->destroyAll();
}
private function destroyRules(): void
{
/** @var RuleGroupRepositoryInterface $repository */
$repository = app(RuleGroupRepositoryInterface::class);
$repository->destroyAll();
}
private function destroyRecurringTransactions(): void
{
/** @var RecurringRepositoryInterface $repository */
$repository = app(RecurringRepositoryInterface::class);
$repository->destroyAll();
}
private function destroyCategories(): void
{
/** @var CategoryRepositoryInterface $categoryRepos */
$categoryRepos = app(CategoryRepositoryInterface::class);
$categoryRepos->destroyAll();
}
private function destroyTags(): void
{
/** @var TagRepositoryInterface $tagRepository */
$tagRepository = app(TagRepositoryInterface::class);
$tagRepository->destroyAll();
}
private function destroyObjectGroups(): void
{
/** @var ObjectGroupRepositoryInterface $repository */
$repository = app(ObjectGroupRepositoryInterface::class);
$repository->deleteAll();
}
/**
* @param array<int, string> $types
*/
@@ -229,6 +165,70 @@ class DestroyController extends Controller
}
}
private function destroyBills(): void
{
/** @var BillRepositoryInterface $repository */
$repository = app(BillRepositoryInterface::class);
$repository->destroyAll();
}
private function destroyBudgets(): void
{
/** @var AvailableBudgetRepositoryInterface $abRepository */
$abRepository = app(AvailableBudgetRepositoryInterface::class);
$abRepository->destroyAll();
/** @var BudgetLimitRepositoryInterface $blRepository */
$blRepository = app(BudgetLimitRepositoryInterface::class);
$blRepository->destroyAll();
/** @var BudgetRepositoryInterface $budgetRepository */
$budgetRepository = app(BudgetRepositoryInterface::class);
$budgetRepository->destroyAll();
}
private function destroyCategories(): void
{
/** @var CategoryRepositoryInterface $categoryRepos */
$categoryRepos = app(CategoryRepositoryInterface::class);
$categoryRepos->destroyAll();
}
private function destroyObjectGroups(): void
{
/** @var ObjectGroupRepositoryInterface $repository */
$repository = app(ObjectGroupRepositoryInterface::class);
$repository->deleteAll();
}
private function destroyPiggyBanks(): void
{
/** @var PiggyBankRepositoryInterface $repository */
$repository = app(PiggyBankRepositoryInterface::class);
$repository->destroyAll();
}
private function destroyRecurringTransactions(): void
{
/** @var RecurringRepositoryInterface $repository */
$repository = app(RecurringRepositoryInterface::class);
$repository->destroyAll();
}
private function destroyRules(): void
{
/** @var RuleGroupRepositoryInterface $repository */
$repository = app(RuleGroupRepositoryInterface::class);
$repository->destroyAll();
}
private function destroyTags(): void
{
/** @var TagRepositoryInterface $tagRepository */
$tagRepository = app(TagRepositoryInterface::class);
$tagRepository->destroyAll();
}
/**
* @param array<int, string> $types
*/

View File

@@ -72,33 +72,6 @@ class ExportController extends Controller
return $this->returnExport('accounts');
}
/**
* @throws FireflyException
* @throws DatetimeException
*/
private function returnExport(string $key): LaravelResponse
{
$date = date('Y-m-d-H-i-s');
$fileName = sprintf('%s-export-%s.csv', $date, $key);
$data = $this->exporter->export();
/** @var LaravelResponse $response */
$response = response($data[$key]);
$response
->header('Content-Description', 'File Transfer')
->header('Content-Type', 'application/octet-stream')
->header('Content-Disposition', 'attachment; filename='.$fileName)
->header('Content-Transfer-Encoding', 'binary')
->header('Connection', 'Keep-Alive')
->header('Expires', '0')
->header('Cache-Control', 'must-revalidate, post-check=0, pre-check=0')
->header('Pragma', 'public')
->header('Content-Length', (string) strlen((string) $data[$key]))
;
return $response;
}
/**
* @throws DatetimeException
* @throws FireflyException
@@ -204,4 +177,31 @@ class ExportController extends Controller
return $this->returnExport('transactions');
}
/**
* @throws FireflyException
* @throws DatetimeException
*/
private function returnExport(string $key): LaravelResponse
{
$date = date('Y-m-d-H-i-s');
$fileName = sprintf('%s-export-%s.csv', $date, $key);
$data = $this->exporter->export();
/** @var LaravelResponse $response */
$response = response($data[$key]);
$response
->header('Content-Description', 'File Transfer')
->header('Content-Type', 'application/octet-stream')
->header('Content-Disposition', 'attachment; filename='.$fileName)
->header('Content-Transfer-Encoding', 'binary')
->header('Connection', 'Keep-Alive')
->header('Expires', '0')
->header('Cache-Control', 'must-revalidate, post-check=0, pre-check=0')
->header('Pragma', 'public')
->header('Content-Length', (string) strlen((string) $data[$key]))
;
return $response;
}
}

View File

@@ -63,13 +63,6 @@ class DestroyController extends Controller
return response()->json([], 204);
}
public function destroySingleById(CurrencyExchangeRate $exchangeRate): JsonResponse
{
$this->repository->deleteRate($exchangeRate);
return response()->json([], 204);
}
public function destroySingleByDate(TransactionCurrency $from, TransactionCurrency $to, Carbon $date): JsonResponse
{
$exchangeRate = $this->repository->getSpecificRateOnDate($from, $to, $date);
@@ -82,4 +75,11 @@ class DestroyController extends Controller
return response()->json([], 204);
}
public function destroySingleById(CurrencyExchangeRate $exchangeRate): JsonResponse
{
$this->repository->deleteRate($exchangeRate);
return response()->json([], 204);
}
}

View File

@@ -75,14 +75,6 @@ class ShowController extends Controller
return response()->json($this->jsonApiList(self::RESOURCE_KEY, $paginator, $transformer))->header('Content-Type', self::CONTENT_TYPE);
}
public function showSingleById(CurrencyExchangeRate $exchangeRate): JsonResponse
{
$transformer = new ExchangeRateTransformer();
$transformer->setParameters($this->parameters);
return response()->api($this->jsonApiObject(self::RESOURCE_KEY, $exchangeRate, $transformer))->header('Content-Type', self::CONTENT_TYPE);
}
public function showSingleByDate(TransactionCurrency $from, TransactionCurrency $to, Carbon $date): JsonResponse
{
$transformer = new ExchangeRateTransformer();
@@ -95,4 +87,12 @@ class ShowController extends Controller
return response()->api($this->jsonApiObject(self::RESOURCE_KEY, $exchangeRate, $transformer))->header('Content-Type', self::CONTENT_TYPE);
}
public function showSingleById(CurrencyExchangeRate $exchangeRate): JsonResponse
{
$transformer = new ExchangeRateTransformer();
$transformer->setParameters($this->parameters);
return response()->api($this->jsonApiObject(self::RESOURCE_KEY, $exchangeRate, $transformer))->header('Content-Type', self::CONTENT_TYPE);
}
}

View File

@@ -61,6 +61,30 @@ class StoreController extends Controller
});
}
public function store(StoreRequest $request): JsonResponse
{
$date = $request->getDate();
$rate = $request->getRate();
$from = $request->getFromCurrency();
$to = $request->getToCurrency();
// already has rate?
$object = $this->repository->getSpecificRateOnDate($from, $to, $date);
if ($object instanceof CurrencyExchangeRate) {
// just update it, no matter.
$rate = $this->repository->updateExchangeRate($object, $rate, $date);
}
if (!$object instanceof CurrencyExchangeRate) {
// store new
$rate = $this->repository->storeExchangeRate($from, $to, $rate, $date);
}
$transformer = new ExchangeRateTransformer();
$transformer->setParameters($this->parameters);
return response()->api($this->jsonApiObject(self::RESOURCE_KEY, $rate, $transformer))->header('Content-Type', self::CONTENT_TYPE);
}
public function storeByCurrencies(StoreByCurrenciesRequest $request, TransactionCurrency $from, TransactionCurrency $to): JsonResponse
{
$data = $request->getAll();
@@ -114,28 +138,4 @@ class StoreController extends Controller
return response()->json($this->jsonApiList(self::RESOURCE_KEY, $paginator, $transformer))->header('Content-Type', self::CONTENT_TYPE);
}
public function store(StoreRequest $request): JsonResponse
{
$date = $request->getDate();
$rate = $request->getRate();
$from = $request->getFromCurrency();
$to = $request->getToCurrency();
// already has rate?
$object = $this->repository->getSpecificRateOnDate($from, $to, $date);
if ($object instanceof CurrencyExchangeRate) {
// just update it, no matter.
$rate = $this->repository->updateExchangeRate($object, $rate, $date);
}
if (!$object instanceof CurrencyExchangeRate) {
// store new
$rate = $this->repository->storeExchangeRate($from, $to, $rate, $date);
}
$transformer = new ExchangeRateTransformer();
$transformer->setParameters($this->parameters);
return response()->api($this->jsonApiObject(self::RESOURCE_KEY, $rate, $transformer))->header('Content-Type', self::CONTENT_TYPE);
}
}

View File

@@ -57,17 +57,6 @@ class UpdateController extends Controller
});
}
public function updateById(UpdateRequest $request, CurrencyExchangeRate $exchangeRate): JsonResponse
{
$date = $request->getDate();
$rate = $request->getRate();
$exchangeRate = $this->repository->updateExchangeRate($exchangeRate, $rate, $date);
$transformer = new ExchangeRateTransformer();
$transformer->setParameters($this->parameters);
return response()->api($this->jsonApiObject(self::RESOURCE_KEY, $exchangeRate, $transformer))->header('Content-Type', self::CONTENT_TYPE);
}
public function updateByDate(UpdateRequest $request, TransactionCurrency $from, TransactionCurrency $to, Carbon $date): JsonResponse
{
$exchangeRate = $this->repository->getSpecificRateOnDate($from, $to, $date);
@@ -82,4 +71,15 @@ class UpdateController extends Controller
return response()->api($this->jsonApiObject(self::RESOURCE_KEY, $exchangeRate, $transformer))->header('Content-Type', self::CONTENT_TYPE);
}
public function updateById(UpdateRequest $request, CurrencyExchangeRate $exchangeRate): JsonResponse
{
$date = $request->getDate();
$rate = $request->getRate();
$exchangeRate = $this->repository->updateExchangeRate($exchangeRate, $rate, $date);
$transformer = new ExchangeRateTransformer();
$transformer->setParameters($this->parameters);
return response()->api($this->jsonApiObject(self::RESOURCE_KEY, $exchangeRate, $transformer))->header('Content-Type', self::CONTENT_TYPE);
}
}

View File

@@ -63,30 +63,28 @@ class TriggerController extends Controller
{
// find recurrence occurrence for this date and trigger it.
// grab the date from the last time the recurrence fired:
$backupDate = $recurrence->latest_date;
$date = $request->getDate();
$backupDate = $recurrence->latest_date;
$date = $request->getDate();
// fire the recurring cron job on the given date, then post-date the created transaction.
Log::info(sprintf('Trigger: will now fire recurring cron job task for date "%s".', $date->format('Y-m-d H:i:s')));
/** @var CreateRecurringTransactions $job */
$job = app(CreateRecurringTransactions::class);
$job = app(CreateRecurringTransactions::class);
$job->setRecurrences(new Collection()->push($recurrence));
$job->setDate($date);
$job->setForce(false);
$job->handle();
Log::debug('Done with recurrence.');
$groups = $job->getGroups();
$groups = $job->getGroups();
$this->repository->markGroupsAsNow($groups);
$recurrence->latest_date = $backupDate;
$recurrence->latest_date_tz = $backupDate?->format('e');
$recurrence->save();
$recurrence = $this->repository->setLatestDate($recurrence, $backupDate);
Preferences::mark();
// enrich groups and return them:
$paginator = new LengthAwarePaginator(new Collection(), 0, 1);
$paginator = new LengthAwarePaginator(new Collection(), 0, 1);
if ($groups->count() > 0) {
/** @var User $admin */
$admin = auth()->user();
@@ -98,20 +96,20 @@ class TriggerController extends Controller
$paginator = $collector->getPaginatedGroups();
}
$manager = $this->getManager();
$manager = $this->getManager();
$paginator->setPath(route('api.v1.recurrences.trigger', [$recurrence->id]).$this->buildParams());
// enrich
$admin = auth()->user();
$enrichment = new TransactionGroupEnrichment();
$admin = auth()->user();
$enrichment = new TransactionGroupEnrichment();
$enrichment->setUser($admin);
$transactions = $enrichment->enrich($paginator->getCollection());
$transactions = $enrichment->enrich($paginator->getCollection());
/** @var TransactionGroupTransformer $transformer */
$transformer = app(TransactionGroupTransformer::class);
$transformer = app(TransactionGroupTransformer::class);
$transformer->setParameters($this->parameters);
$resource = new FractalCollection($transactions, $transformer, 'transactions');
$resource = new FractalCollection($transactions, $transformer, 'transactions');
$resource->setPaginator(new IlluminatePaginatorAdapter($paginator));
return response()->json($manager->createData($resource)->toArray())->header('Content-Type', self::CONTENT_TYPE);

View File

@@ -25,9 +25,6 @@ declare(strict_types=1);
namespace FireflyIII\Api\V1\Controllers\Models\Transaction;
use FireflyIII\Api\V1\Controllers\Controller;
use FireflyIII\Events\UpdatedAccount;
use FireflyIII\Models\Account;
use FireflyIII\Models\Transaction;
use FireflyIII\Models\TransactionGroup;
use FireflyIII\Models\TransactionJournal;
use FireflyIII\Repositories\Journal\JournalRepositoryInterface;
@@ -74,31 +71,9 @@ class DestroyController extends Controller
public function destroy(TransactionGroup $transactionGroup): JsonResponse
{
Log::debug(sprintf('Now in %s', __METHOD__));
// grab asset account(s) from group:
$accounts = [];
/** @var TransactionJournal $journal */
foreach ($transactionGroup->transactionJournals as $journal) {
/** @var Transaction $transaction */
foreach ($journal->transactions as $transaction) {
$type = $transaction->account->accountType->type;
// if is valid liability, trigger event!
if (in_array($type, config('firefly.valid_liabilities'), true)) {
$accounts[] = $transaction->account;
}
}
}
$this->groupRepository->destroy($transactionGroup);
Preferences::mark();
/** @var Account $account */
foreach ($accounts as $account) {
Log::debug(sprintf('Now going to trigger updated account event for account #%d', $account->id));
event(new UpdatedAccount($account));
}
return response()->json([], 204);
}

View File

@@ -99,17 +99,6 @@ class ShowController extends Controller
return response()->json($manager->createData($resource)->toArray())->header('Content-Type', self::CONTENT_TYPE);
}
/**
* This endpoint is documented at:
* https://api-docs.firefly-iii.org/?urls.primaryName=2.0.0%20(v1)#/transactions/getTransactionByJournal
*
* Show a single transaction, by transaction journal.
*/
public function showJournal(TransactionJournal $transactionJournal): JsonResponse
{
return $this->show($transactionJournal->transactionGroup);
}
/**
* This endpoint is documented at:
* https://api-docs.firefly-iii.org/?urls.primaryName=2.0.0%20(v1)#/transactions/getTransaction
@@ -151,4 +140,15 @@ class ShowController extends Controller
return response()->json($manager->createData($resource)->toArray())->header('Content-Type', self::CONTENT_TYPE);
}
/**
* This endpoint is documented at:
* https://api-docs.firefly-iii.org/?urls.primaryName=2.0.0%20(v1)#/transactions/getTransactionByJournal
*
* Show a single transaction, by transaction journal.
*/
public function showJournal(TransactionJournal $transactionJournal): JsonResponse
{
return $this->show($transactionJournal->transactionGroup);
}
}

View File

@@ -27,14 +27,11 @@ namespace FireflyIII\Api\V1\Controllers\Models\Transaction;
use FireflyIII\Api\V1\Controllers\Controller;
use FireflyIII\Api\V1\Requests\Models\Transaction\StoreRequest;
use FireflyIII\Enums\UserRoleEnum;
use FireflyIII\Events\Model\TransactionGroup\CreatedSingleTransactionGroup;
use FireflyIII\Events\Model\TransactionGroup\TransactionGroupEventFlags;
use FireflyIII\Exceptions\DuplicateTransactionException;
use FireflyIII\Exceptions\FireflyException;
use FireflyIII\Helpers\Collector\GroupCollectorInterface;
use FireflyIII\Repositories\TransactionGroup\TransactionGroupRepositoryInterface;
use FireflyIII\Rules\IsDuplicateTransaction;
use FireflyIII\Support\Facades\Preferences;
use FireflyIII\Support\Http\Api\TransactionFilter;
use FireflyIII\Support\JsonApi\Enrichments\TransactionGroupEnrichment;
use FireflyIII\Transformers\TransactionGroupTransformer;
@@ -88,9 +85,9 @@ class StoreController extends Controller
public function store(StoreRequest $request): JsonResponse
{
Log::debug('Now in API StoreController::store()');
$data = $request->getAll();
$data['user'] = auth()->user();
$data['user_group'] = $this->userGroup;
$data = $request->getAll();
$data['user'] = auth()->user();
$data['user_group'] = $this->userGroup;
Log::channel('audit')->info('Store new transaction over API.', $data);
@@ -109,22 +106,15 @@ class StoreController extends Controller
throw new ValidationException($validator);
}
Preferences::mark();
$flags = new TransactionGroupEventFlags();
$flags->applyRules = $data['apply_rules'] ?? true;
$flags->fireWebhooks = $data['fire_webhooks'] ?? true;
$flags->batchSubmission = $data['batch_submission'] ?? false;
Log::debug('CreatedSingleTransactionGroup');
event(new CreatedSingleTransactionGroup($transactionGroup, $flags));
$manager = $this->getManager();
$manager = $this->getManager();
/** @var User $admin */
$admin = auth()->user();
$admin = auth()->user();
// use new group collector:
/** @var GroupCollectorInterface $collector */
$collector = app(GroupCollectorInterface::class);
$collector = app(GroupCollectorInterface::class);
$collector
->setUser($admin)
->setUserGroup($this->userGroup)
@@ -134,20 +124,20 @@ class StoreController extends Controller
->withAPIInformation()
;
$selectedGroup = $collector->getGroups()->first();
$selectedGroup = $collector->getGroups()->first();
if (null === $selectedGroup) {
throw HttpException::fromStatusCode(410, '200032: Cannot find transaction. Possibly, a rule deleted this transaction after its creation.');
}
// enrich
$enrichment = new TransactionGroupEnrichment();
$enrichment = new TransactionGroupEnrichment();
$enrichment->setUser($admin);
$selectedGroup = $enrichment->enrichSingle($selectedGroup);
$selectedGroup = $enrichment->enrichSingle($selectedGroup);
/** @var TransactionGroupTransformer $transformer */
$transformer = app(TransactionGroupTransformer::class);
$transformer = app(TransactionGroupTransformer::class);
$transformer->setParameters($this->parameters);
$resource = new Item($selectedGroup, $transformer, 'transactions');
$resource = new Item($selectedGroup, $transformer, 'transactions');
return response()->json($manager->createData($resource)->toArray())->header('Content-Type', self::CONTENT_TYPE);
}

View File

@@ -26,7 +26,10 @@ namespace FireflyIII\Api\V1\Controllers\Models\Transaction;
use FireflyIII\Api\V1\Controllers\Controller;
use FireflyIII\Api\V1\Requests\Models\Transaction\UpdateRequest;
use FireflyIII\Events\UpdatedTransactionGroup;
use FireflyIII\Events\Model\TransactionGroup\TransactionGroupEventFlags;
use FireflyIII\Events\Model\TransactionGroup\TransactionGroupEventObjects;
use FireflyIII\Events\Model\TransactionGroup\UpdatedSingleTransactionGroup;
use FireflyIII\Events\Model\Webhook\WebhookMessagesRequestSending;
use FireflyIII\Helpers\Collector\GroupCollectorInterface;
use FireflyIII\Models\TransactionGroup;
use FireflyIII\Repositories\TransactionGroup\TransactionGroupRepositoryInterface;
@@ -72,27 +75,32 @@ class UpdateController extends Controller
public function update(UpdateRequest $request, TransactionGroup $transactionGroup): JsonResponse
{
Log::debug('Now in update routine for transaction group');
$data = $request->getAll();
$oldHash = $this->groupRepository->getCompareHash($transactionGroup);
$transactionGroup = $this->groupRepository->update($transactionGroup, $data);
$newHash = $this->groupRepository->getCompareHash($transactionGroup);
$manager = $this->getManager();
$data = $request->getAll();
$oldHash = $this->groupRepository->getCompareHash($transactionGroup);
$objects = TransactionGroupEventObjects::collectFromTransactionGroup($transactionGroup);
$transactionGroup = $this->groupRepository->update($transactionGroup, $data);
$objects->appendFromTransactionGroup($transactionGroup);
$newHash = $this->groupRepository->getCompareHash($transactionGroup);
$manager = $this->getManager();
Preferences::mark();
$applyRules = $data['apply_rules'] ?? true;
$fireWebhooks = $data['fire_webhooks'] ?? true;
$runRecalculations = $oldHash !== $newHash;
$applyRules = $data['apply_rules'] ?? true;
$fireWebhooks = $data['fire_webhooks'] ?? true;
$runRecalculations = $oldHash !== $newHash;
// FIXME responds to a single event.
// flags in array?
event(new UpdatedTransactionGroup($transactionGroup, $applyRules, $fireWebhooks, $runRecalculations));
$flags = new TransactionGroupEventFlags();
$flags->applyRules = $applyRules;
$flags->fireWebhooks = $fireWebhooks;
$flags->recalculateCredit = $runRecalculations;
event(new UpdatedSingleTransactionGroup($flags, $objects));
event(new WebhookMessagesRequestSending());
/** @var User $admin */
$admin = auth()->user();
$admin = auth()->user();
// use new group collector:
/** @var GroupCollectorInterface $collector */
$collector = app(GroupCollectorInterface::class);
$collector = app(GroupCollectorInterface::class);
$collector
->setUser($admin)
// filter on transaction group.
@@ -101,20 +109,20 @@ class UpdateController extends Controller
->withAPIInformation()
;
$selectedGroup = $collector->getGroups()->first();
$selectedGroup = $collector->getGroups()->first();
if (null === $selectedGroup) {
throw new NotFoundHttpException();
}
// enrich
$enrichment = new TransactionGroupEnrichment();
$enrichment = new TransactionGroupEnrichment();
$enrichment->setUser($admin);
$selectedGroup = $enrichment->enrichSingle($selectedGroup);
$selectedGroup = $enrichment->enrichSingle($selectedGroup);
/** @var TransactionGroupTransformer $transformer */
$transformer = app(TransactionGroupTransformer::class);
$transformer = app(TransactionGroupTransformer::class);
$transformer->setParameters($this->parameters);
$resource = new Item($selectedGroup, $transformer, 'transactions');
$resource = new Item($selectedGroup, $transformer, 'transactions');
return response()->json($manager->createData($resource)->toArray())->header('Content-Type', self::CONTENT_TYPE);
}

View File

@@ -97,27 +97,6 @@ class UpdateController extends Controller
return response()->json($manager->createData($resource)->toArray())->header('Content-Type', self::CONTENT_TYPE);
}
public function makePrimary(TransactionCurrency $currency): JsonResponse
{
/** @var User $user */
$user = auth()->user();
$this->repository->enable($currency);
$this->repository->makePrimary($currency);
Preferences::mark();
$manager = $this->getManager();
$currency->refreshForUser($user);
/** @var CurrencyTransformer $transformer */
$transformer = app(CurrencyTransformer::class);
$transformer->setParameters($this->parameters);
$resource = new Item($currency, $transformer, 'currencies');
return response()->json($manager->createData($resource)->toArray())->header('Content-Type', self::CONTENT_TYPE);
}
/**
* This endpoint is documented at:
* https://api-docs.firefly-iii.org/?urls.primaryName=2.0.0%20(v1)#/currencies/enableCurrency
@@ -143,6 +122,27 @@ class UpdateController extends Controller
return response()->json($manager->createData($resource)->toArray())->header('Content-Type', self::CONTENT_TYPE);
}
public function makePrimary(TransactionCurrency $currency): JsonResponse
{
/** @var User $user */
$user = auth()->user();
$this->repository->enable($currency);
$this->repository->makePrimary($currency);
Preferences::mark();
$manager = $this->getManager();
$currency->refreshForUser($user);
/** @var CurrencyTransformer $transformer */
$transformer = app(CurrencyTransformer::class);
$transformer->setParameters($this->parameters);
$resource = new Item($currency, $transformer, 'currencies');
return response()->json($manager->createData($resource)->toArray())->header('Content-Type', self::CONTENT_TYPE);
}
/**
* This endpoint is documented at:
* https://api-docs.firefly-iii.org/?urls.primaryName=2.0.0%20(v1)#/currencies/updateCurrency

View File

@@ -112,6 +112,19 @@ class BasicController extends Controller
return response()->json($return);
}
/**
* Check if date is outside session range.
*/
protected function notInDateRange(Carbon $date, Carbon $start, Carbon $end): bool
{ // Validate a preference
if ($start->greaterThanOrEqualTo($date) && $end->greaterThanOrEqualTo($date)) {
return true;
}
// start and end in the past? use $end
return $start->lessThanOrEqualTo($date) && $end->lessThanOrEqualTo($date);
}
private function getBalanceInformation(Carbon $start, Carbon $end): array
{
Log::debug('getBalanceInformation');
@@ -312,145 +325,6 @@ class BasicController extends Controller
return $return;
}
private function getSubscriptionInformation(Carbon $start, Carbon $end): array
{
Log::debug(sprintf('Now in getBillInformation("%s", "%s")', $start->format('Y-m-d'), $end->format('Y-m-d-')));
/*
* Since both this method and the chart use the exact same data, we can suffice
* with calling the one method in the bill repository that will get this amount.
*/
$paidAmount = $this->billRepository->sumPaidInRange($start, $end);
$unpaidAmount = $this->billRepository->sumUnpaidInRange($start, $end);
$currencies = [$this->primaryCurrency->id => $this->primaryCurrency];
if ($this->convertToPrimary) {
$converter = new ExchangeRateConverter();
$newPaidAmount = [[
'id' => $this->primaryCurrency->id,
'name' => $this->primaryCurrency->name,
'symbol' => $this->primaryCurrency->symbol,
'code' => $this->primaryCurrency->code,
'decimal_places' => $this->primaryCurrency->decimal_places,
'sum' => '0',
]];
$newUnpaidAmount = [[
'id' => $this->primaryCurrency->id,
'name' => $this->primaryCurrency->name,
'symbol' => $this->primaryCurrency->symbol,
'code' => $this->primaryCurrency->code,
'decimal_places' => $this->primaryCurrency->decimal_places,
'sum' => '0',
]];
foreach ([$paidAmount, $unpaidAmount] as $index => $array) {
foreach ($array as $item) {
$currencyId = (int) $item['id'];
if (0 === $index) {
// paid amount
if ($currencyId === $this->primaryCurrency->id) {
$newPaidAmount[0]['sum'] = bcadd($newPaidAmount[0]['sum'], (string) $item['sum']);
continue;
}
$currencies[$currencyId] ??= $this->currencyRepos->find($currencyId);
$convertedAmount = $converter->convert($currencies[$currencyId], $this->primaryCurrency, $start, $item['sum']);
$newPaidAmount[0]['sum'] = bcadd($newPaidAmount[0]['sum'], $convertedAmount);
continue;
}
// unpaid amount
if ($currencyId === $this->primaryCurrency->id) {
$newUnpaidAmount[0]['sum'] = bcadd($newUnpaidAmount[0]['sum'], (string) $item['sum']);
continue;
}
$currencies[$currencyId] ??= $this->currencyRepos->find($currencyId);
$convertedAmount = $converter->convert($currencies[$currencyId], $this->primaryCurrency, $start, $item['sum']);
$newUnpaidAmount[0]['sum'] = bcadd($newUnpaidAmount[0]['sum'], $convertedAmount);
}
}
$paidAmount = $newPaidAmount;
$unpaidAmount = $newUnpaidAmount;
}
// var_dump($paidAmount);
// var_dump($unpaidAmount);
// exit;
$return = [];
/**
* @var array $info
*/
foreach ($paidAmount as $info) {
$amount = bcmul((string) $info['sum'], '-1');
$return[] = [
'key' => sprintf('bills-paid-in-%s', $info['code']),
'title' => trans('firefly.box_bill_paid_in_currency', ['currency' => $info['symbol']]),
'monetary_value' => $amount,
'currency_id' => (string) $info['id'],
'currency_code' => $info['code'],
'currency_symbol' => $info['symbol'],
'currency_decimal_places' => $info['decimal_places'],
'value_parsed' => Amount::formatFlat($info['symbol'], $info['decimal_places'], $amount, false),
'local_icon' => 'check',
'sub_title' => '',
];
}
/**
* @var array $info
*/
foreach ($unpaidAmount as $info) {
$amount = bcmul((string) $info['sum'], '-1');
$return[] = [
'key' => sprintf('bills-unpaid-in-%s', $info['code']),
'title' => trans('firefly.box_bill_unpaid_in_currency', ['currency' => $info['symbol']]),
'monetary_value' => $amount,
'currency_id' => (string) $info['id'],
'currency_code' => $info['code'],
'currency_symbol' => $info['symbol'],
'currency_decimal_places' => $info['decimal_places'],
'value_parsed' => Amount::formatFlat($info['symbol'], $info['decimal_places'], $amount, false),
'local_icon' => 'calendar-o',
'sub_title' => '',
];
}
Log::debug(sprintf('Done with getBillInformation("%s", "%s")', $start->format('Y-m-d'), $end->format('Y-m-d-')));
if (0 === count($return)) {
$currency = $this->primaryCurrency;
unset($info, $amount);
$return[] = [
'key' => sprintf('bills-paid-in-%s', $currency->code),
'title' => trans('firefly.box_bill_paid_in_currency', ['currency' => $currency->symbol]),
'monetary_value' => '0',
'currency_id' => (string) $currency->id,
'currency_code' => $currency->code,
'currency_symbol' => $currency->symbol,
'currency_decimal_places' => $currency->decimal_places,
'value_parsed' => Amount::formatFlat($currency->symbol, $currency->decimal_places, '0', false),
'local_icon' => 'check',
'sub_title' => '',
];
$return[] = [
'key' => sprintf('bills-unpaid-in-%s', $currency->code),
'title' => trans('firefly.box_bill_unpaid_in_currency', ['currency' => $currency->symbol]),
'monetary_value' => '0',
'currency_id' => (string) $currency->id,
'currency_code' => $currency->code,
'currency_symbol' => $currency->symbol,
'currency_decimal_places' => $currency->decimal_places,
'value_parsed' => Amount::formatFlat($currency->symbol, $currency->decimal_places, '0', false),
'local_icon' => 'calendar-o',
'sub_title' => '',
];
}
return $return;
}
/**
* @throws Exception
*/
@@ -650,16 +524,142 @@ class BasicController extends Controller
return $return;
}
/**
* Check if date is outside session range.
*/
protected function notInDateRange(Carbon $date, Carbon $start, Carbon $end): bool
{ // Validate a preference
if ($start->greaterThanOrEqualTo($date) && $end->greaterThanOrEqualTo($date)) {
return true;
private function getSubscriptionInformation(Carbon $start, Carbon $end): array
{
Log::debug(sprintf('Now in getBillInformation("%s", "%s")', $start->format('Y-m-d'), $end->format('Y-m-d-')));
/*
* Since both this method and the chart use the exact same data, we can suffice
* with calling the one method in the bill repository that will get this amount.
*/
$paidAmount = $this->billRepository->sumPaidInRange($start, $end);
$unpaidAmount = $this->billRepository->sumUnpaidInRange($start, $end);
$currencies = [$this->primaryCurrency->id => $this->primaryCurrency];
if ($this->convertToPrimary) {
$converter = new ExchangeRateConverter();
$newPaidAmount = [[
'id' => $this->primaryCurrency->id,
'name' => $this->primaryCurrency->name,
'symbol' => $this->primaryCurrency->symbol,
'code' => $this->primaryCurrency->code,
'decimal_places' => $this->primaryCurrency->decimal_places,
'sum' => '0',
]];
$newUnpaidAmount = [[
'id' => $this->primaryCurrency->id,
'name' => $this->primaryCurrency->name,
'symbol' => $this->primaryCurrency->symbol,
'code' => $this->primaryCurrency->code,
'decimal_places' => $this->primaryCurrency->decimal_places,
'sum' => '0',
]];
foreach ([$paidAmount, $unpaidAmount] as $index => $array) {
foreach ($array as $item) {
$currencyId = (int) $item['id'];
if (0 === $index) {
// paid amount
if ($currencyId === $this->primaryCurrency->id) {
$newPaidAmount[0]['sum'] = bcadd($newPaidAmount[0]['sum'], (string) $item['sum']);
continue;
}
$currencies[$currencyId] ??= $this->currencyRepos->find($currencyId);
$convertedAmount = $converter->convert($currencies[$currencyId], $this->primaryCurrency, $start, $item['sum']);
$newPaidAmount[0]['sum'] = bcadd($newPaidAmount[0]['sum'], $convertedAmount);
continue;
}
// unpaid amount
if ($currencyId === $this->primaryCurrency->id) {
$newUnpaidAmount[0]['sum'] = bcadd($newUnpaidAmount[0]['sum'], (string) $item['sum']);
continue;
}
$currencies[$currencyId] ??= $this->currencyRepos->find($currencyId);
$convertedAmount = $converter->convert($currencies[$currencyId], $this->primaryCurrency, $start, $item['sum']);
$newUnpaidAmount[0]['sum'] = bcadd($newUnpaidAmount[0]['sum'], $convertedAmount);
}
}
$paidAmount = $newPaidAmount;
$unpaidAmount = $newUnpaidAmount;
}
// start and end in the past? use $end
return $start->lessThanOrEqualTo($date) && $end->lessThanOrEqualTo($date);
// var_dump($paidAmount);
// var_dump($unpaidAmount);
// exit;
$return = [];
/**
* @var array $info
*/
foreach ($paidAmount as $info) {
$amount = bcmul((string) $info['sum'], '-1');
$return[] = [
'key' => sprintf('bills-paid-in-%s', $info['code']),
'title' => trans('firefly.box_bill_paid_in_currency', ['currency' => $info['symbol']]),
'monetary_value' => $amount,
'currency_id' => (string) $info['id'],
'currency_code' => $info['code'],
'currency_symbol' => $info['symbol'],
'currency_decimal_places' => $info['decimal_places'],
'value_parsed' => Amount::formatFlat($info['symbol'], $info['decimal_places'], $amount, false),
'local_icon' => 'check',
'sub_title' => '',
];
}
/**
* @var array $info
*/
foreach ($unpaidAmount as $info) {
$amount = bcmul((string) $info['sum'], '-1');
$return[] = [
'key' => sprintf('bills-unpaid-in-%s', $info['code']),
'title' => trans('firefly.box_bill_unpaid_in_currency', ['currency' => $info['symbol']]),
'monetary_value' => $amount,
'currency_id' => (string) $info['id'],
'currency_code' => $info['code'],
'currency_symbol' => $info['symbol'],
'currency_decimal_places' => $info['decimal_places'],
'value_parsed' => Amount::formatFlat($info['symbol'], $info['decimal_places'], $amount, false),
'local_icon' => 'calendar-o',
'sub_title' => '',
];
}
Log::debug(sprintf('Done with getBillInformation("%s", "%s")', $start->format('Y-m-d'), $end->format('Y-m-d-')));
if (0 === count($return)) {
$currency = $this->primaryCurrency;
unset($info, $amount);
$return[] = [
'key' => sprintf('bills-paid-in-%s', $currency->code),
'title' => trans('firefly.box_bill_paid_in_currency', ['currency' => $currency->symbol]),
'monetary_value' => '0',
'currency_id' => (string) $currency->id,
'currency_code' => $currency->code,
'currency_symbol' => $currency->symbol,
'currency_decimal_places' => $currency->decimal_places,
'value_parsed' => Amount::formatFlat($currency->symbol, $currency->decimal_places, '0', false),
'local_icon' => 'check',
'sub_title' => '',
];
$return[] = [
'key' => sprintf('bills-unpaid-in-%s', $currency->code),
'title' => trans('firefly.box_bill_unpaid_in_currency', ['currency' => $currency->symbol]),
'monetary_value' => '0',
'currency_id' => (string) $currency->id,
'currency_code' => $currency->code,
'currency_symbol' => $currency->symbol,
'currency_decimal_places' => $currency->decimal_places,
'value_parsed' => Amount::formatFlat($currency->symbol, $currency->decimal_places, '0', false),
'local_icon' => 'calendar-o',
'sub_title' => '',
];
}
return $return;
}
}

View File

@@ -25,8 +25,8 @@ declare(strict_types=1);
namespace FireflyIII\Api\V1\Controllers\System;
use FireflyIII\Api\V1\Controllers\Controller;
use FireflyIII\Events\Model\TransactionGroup\CreatedSingleTransactionGroup;
use FireflyIII\Events\Model\TransactionGroup\TransactionGroupEventFlags;
use FireflyIII\Events\Model\TransactionGroup\UserRequestedBatchProcessing;
use FireflyIII\Models\TransactionJournal;
use FireflyIII\Repositories\Journal\JournalRepositoryInterface;
use Illuminate\Http\JsonResponse;
@@ -44,6 +44,7 @@ class BatchController extends Controller
parent::__construct();
$this->middleware(function ($request, $next) {
$this->repository = app(JournalRepositoryInterface::class);
$this->repository->setUser(auth()->user()); // should not have to do this.
return $next($request);
});
@@ -64,7 +65,8 @@ class BatchController extends Controller
}
$flags = new TransactionGroupEventFlags();
$flags->applyRules = 'true' === $request->get('apply_rules');
event(new CreatedSingleTransactionGroup($group, $flags));
event(new UserRequestedBatchProcessing($flags));
// event(new CreatedSingleTransactionGroup($group, $flags));
return response()->json([], 204);
}

View File

@@ -86,37 +86,6 @@ class ConfigurationController extends Controller
return response()->api($return);
}
/**
* Get all config values.
*
* @throws FireflyException
*/
private function getDynamicConfiguration(): array
{
$isDemoSite = FireflyConfig::get('is_demo_site');
$updateCheck = FireflyConfig::get('permission_update_check');
$lastCheck = FireflyConfig::get('last_update_check');
$singleUser = FireflyConfig::get('single_user_mode');
return [
'is_demo_site' => $isDemoSite?->data,
'permission_update_check' => null === $updateCheck ? null : (int) $updateCheck->data,
'last_update_check' => null === $lastCheck ? null : (int) $lastCheck->data,
'single_user_mode' => $singleUser?->data,
];
}
private function getStaticConfiguration(): array
{
$list = EitherConfigKey::$static;
$return = [];
foreach ($list as $key) {
$return[$key] = config($key);
}
return $return;
}
/**
* This endpoint is documented at:
* https://api-docs.firefly-iii.org/?urls.primaryName=2.0.0%20(v1)#/configuration/getSingleConfiguration
@@ -170,6 +139,37 @@ class ConfigurationController extends Controller
return response()->api(['data' => $data])->header('Content-Type', self::CONTENT_TYPE);
}
/**
* Get all config values.
*
* @throws FireflyException
*/
private function getDynamicConfiguration(): array
{
$isDemoSite = FireflyConfig::get('is_demo_site');
$updateCheck = FireflyConfig::get('permission_update_check');
$lastCheck = FireflyConfig::get('last_update_check');
$singleUser = FireflyConfig::get('single_user_mode');
return [
'is_demo_site' => $isDemoSite?->data,
'permission_update_check' => null === $updateCheck ? null : (int) $updateCheck->data,
'last_update_check' => null === $lastCheck ? null : (int) $lastCheck->data,
'single_user_mode' => $singleUser?->data,
];
}
private function getStaticConfiguration(): array
{
$list = EitherConfigKey::$static;
$return = [];
foreach ($list as $key) {
$return[$key] = config($key);
}
return $return;
}
private function getWebhookConfiguration(string $configKey): array
{
switch ($configKey) {

View File

@@ -58,8 +58,6 @@ class UserController extends Controller
});
}
public function finishBatch(): JsonResponse {}
/**
* This endpoint is documented at:
* https://api-docs.firefly-iii.org/?urls.primaryName=2.0.0%20(v1)#/users/deleteUser
@@ -85,6 +83,8 @@ class UserController extends Controller
throw new FireflyException('200025: No access to function.');
}
public function finishBatch(): JsonResponse {}
/**
* This endpoint is documented at:
* https://api-docs.firefly-iii.org/?urls.primaryName=2.0.0%20(v1)#/users/listUser

View File

@@ -37,9 +37,6 @@ abstract class AggregateFormRequest extends ApiRequest
*/
protected array $requests = [];
/** @return array<array|string> */
abstract protected function getRequests(): array;
#[Override]
public function initialize(
array $query = [],
@@ -53,7 +50,7 @@ abstract class AggregateFormRequest extends ApiRequest
parent::initialize($query, $request, $attributes, $cookies, $files, $server, $content);
// instantiate all subrequests and share current requests' bags with them
Log::debug('Initializing AggregateFormRequest.');
// Log::debug('Initializing AggregateFormRequest.');
/** @var array|string $config */
foreach ($this->getRequests() as $config) {
@@ -62,7 +59,7 @@ abstract class AggregateFormRequest extends ApiRequest
if (!is_a($requestClass, Request::class, true)) {
throw new RuntimeException('getRequests() must return class-strings of subclasses of Request');
}
Log::debug(sprintf('Initializing subrequest %s', $requestClass));
// Log::debug(sprintf('Initializing subrequest %s', $requestClass));
$instance = $this->requests[] = new $requestClass();
$instance->request = $this->request;
@@ -77,7 +74,8 @@ abstract class AggregateFormRequest extends ApiRequest
$instance->handleConfig(is_array($config) ? $config : []);
}
}
Log::debug('Done initializing AggregateFormRequest.');
// Log::debug('Done initializing AggregateFormRequest.');
}
public function rules(): array
@@ -95,9 +93,12 @@ abstract class AggregateFormRequest extends ApiRequest
// register all subrequests' validators
foreach ($this->requests as $request) {
if (method_exists($request, 'withValidator')) {
Log::debug(sprintf('Process withValidator from class %s', $request::class));
// Log::debug(sprintf('Process withValidator from class %s', $request::class));
$request->withValidator($validator);
}
}
}
/** @return array<array|string> */
abstract protected function getRequests(): array;
}

View File

@@ -79,25 +79,6 @@ class GenericRequest extends FormRequest
return $return;
}
private function parseAccounts(): void
{
if (0 !== $this->accounts->count()) {
return;
}
$repository = app(AccountRepositoryInterface::class);
$repository->setUser(auth()->user());
$array = $this->get('accounts');
if (is_array($array)) {
foreach ($array as $accountId) {
$accountId = (int) $accountId;
$account = $repository->find($accountId);
if (null !== $account) {
$this->accounts->push($account);
}
}
}
}
public function getBills(): Collection
{
$this->parseBills();
@@ -105,25 +86,6 @@ class GenericRequest extends FormRequest
return $this->bills;
}
private function parseBills(): void
{
if (0 !== $this->bills->count()) {
return;
}
$repository = app(BillRepositoryInterface::class);
$repository->setUser(auth()->user());
$array = $this->get('bills');
if (is_array($array)) {
foreach ($array as $billId) {
$billId = (int) $billId;
$bill = $repository->find($billId);
if (null !== $bill) {
$this->bills->push($bill);
}
}
}
}
public function getBudgets(): Collection
{
$this->parseBudgets();
@@ -131,25 +93,6 @@ class GenericRequest extends FormRequest
return $this->budgets;
}
private function parseBudgets(): void
{
if (0 !== $this->budgets->count()) {
return;
}
$repository = app(BudgetRepositoryInterface::class);
$repository->setUser(auth()->user());
$array = $this->get('budgets');
if (is_array($array)) {
foreach ($array as $budgetId) {
$budgetId = (int) $budgetId;
$budget = $repository->find($budgetId);
if (null !== $budget) {
$this->budgets->push($budget);
}
}
}
}
public function getCategories(): Collection
{
$this->parseCategories();
@@ -157,25 +100,6 @@ class GenericRequest extends FormRequest
return $this->categories;
}
private function parseCategories(): void
{
if (0 !== $this->categories->count()) {
return;
}
$repository = app(CategoryRepositoryInterface::class);
$repository->setUser(auth()->user());
$array = $this->get('categories');
if (is_array($array)) {
foreach ($array as $categoryId) {
$categoryId = (int) $categoryId;
$category = $repository->find($categoryId);
if (null !== $category) {
$this->categories->push($category);
}
}
}
}
public function getEnd(): Carbon
{
$date = $this->getCarbonDate('end');
@@ -231,6 +155,97 @@ class GenericRequest extends FormRequest
return $this->tags;
}
/**
* The rules that the incoming request must be matched against.
*/
public function rules(): array
{
// this is cheating, but it works to initialize the collections.
$this->accounts = new Collection();
$this->budgets = new Collection();
$this->categories = new Collection();
$this->bills = new Collection();
$this->tags = new Collection();
return ['start' => 'required|date', 'end' => 'required|date|after_or_equal:start'];
}
private function parseAccounts(): void
{
if (0 !== $this->accounts->count()) {
return;
}
$repository = app(AccountRepositoryInterface::class);
$repository->setUser(auth()->user());
$array = $this->get('accounts');
if (is_array($array)) {
foreach ($array as $accountId) {
$accountId = (int) $accountId;
$account = $repository->find($accountId);
if (null !== $account) {
$this->accounts->push($account);
}
}
}
}
private function parseBills(): void
{
if (0 !== $this->bills->count()) {
return;
}
$repository = app(BillRepositoryInterface::class);
$repository->setUser(auth()->user());
$array = $this->get('bills');
if (is_array($array)) {
foreach ($array as $billId) {
$billId = (int) $billId;
$bill = $repository->find($billId);
if (null !== $bill) {
$this->bills->push($bill);
}
}
}
}
private function parseBudgets(): void
{
if (0 !== $this->budgets->count()) {
return;
}
$repository = app(BudgetRepositoryInterface::class);
$repository->setUser(auth()->user());
$array = $this->get('budgets');
if (is_array($array)) {
foreach ($array as $budgetId) {
$budgetId = (int) $budgetId;
$budget = $repository->find($budgetId);
if (null !== $budget) {
$this->budgets->push($budget);
}
}
}
}
private function parseCategories(): void
{
if (0 !== $this->categories->count()) {
return;
}
$repository = app(CategoryRepositoryInterface::class);
$repository->setUser(auth()->user());
$array = $this->get('categories');
if (is_array($array)) {
foreach ($array as $categoryId) {
$categoryId = (int) $categoryId;
$category = $repository->find($categoryId);
if (null !== $category) {
$this->categories->push($category);
}
}
}
}
private function parseTags(): void
{
if (0 !== $this->tags->count()) {
@@ -249,19 +264,4 @@ class GenericRequest extends FormRequest
}
}
}
/**
* The rules that the incoming request must be matched against.
*/
public function rules(): array
{
// this is cheating, but it works to initialize the collections.
$this->accounts = new Collection();
$this->budgets = new Collection();
$this->categories = new Collection();
$this->bills = new Collection();
$this->tags = new Collection();
return ['start' => 'required|date', 'end' => 'required|date|after_or_equal:start'];
}
}

View File

@@ -26,6 +26,7 @@ namespace FireflyIII\Api\V1\Requests\Models\BudgetLimit;
use Carbon\Carbon;
use FireflyIII\Factory\TransactionCurrencyFactory;
use FireflyIII\Repositories\Currency\CurrencyRepositoryInterface;
use FireflyIII\Rules\IsBoolean;
use FireflyIII\Rules\IsValidPositiveAmount;
use FireflyIII\Support\Facades\Amount;
@@ -89,24 +90,24 @@ class StoreRequest extends FormRequest
if (0 !== count($validator->failed())) {
return;
}
$data = $validator->getData();
$data = $validator->getData();
// if no currency has been provided, use the user's default currency:
/** @var TransactionCurrencyFactory $factory */
$factory = app(TransactionCurrencyFactory::class);
$currency = $factory->find($data['currency_id'] ?? null, $data['currency_code'] ?? null);
$factory = app(TransactionCurrencyFactory::class);
$currency = $factory->find($data['currency_id'] ?? null, $data['currency_code'] ?? null);
if (null === $currency) {
$currency = Amount::getPrimaryCurrency();
}
$currency->enabled = true;
$currency->save();
$repository = app(CurrencyRepositoryInterface::class);
$repository->enable($currency);
// validator already concluded start and end are valid dates:
$start = Carbon::parse($data['start'], config('app.timezone'));
$end = Carbon::parse($data['end'], config('app.timezone'));
$start = Carbon::parse($data['start'], config('app.timezone'));
$end = Carbon::parse($data['end'], config('app.timezone'));
// find limit with same date range and currency.
$limit = $budget
$limit = $budget
->budgetlimits()
->where('budget_limits.start_date', $start->format('Y-m-d'))
->where('budget_limits.end_date', $end->format('Y-m-d'))

View File

@@ -70,65 +70,6 @@ class StoreRequest extends FormRequest
return ['recurrence' => $recurrence, 'transactions' => $this->getTransactionData(), 'repetitions' => $this->getRepetitionData()];
}
/**
* Returns the transaction data as it is found in the submitted data. It's a complex method according to code
* standards, but it just has a lot of ??-statements because of the fields that may or may not exist.
*/
private function getTransactionData(): array
{
$return = [];
// transaction data:
/** @var null|array $transactions */
$transactions = $this->get('transactions');
if (null === $transactions) {
return [];
}
/** @var array $transaction */
foreach ($transactions as $transaction) {
$return[] = $this->getSingleTransactionData($transaction);
}
return $return;
}
/**
* Returns the repetition data as it is found in the submitted data.
*/
private function getRepetitionData(): array
{
$return = [];
// repetition data:
/** @var null|array $repetitions */
$repetitions = $this->get('repetitions');
if (null === $repetitions) {
return [];
}
/** @var array $repetition */
foreach ($repetitions as $repetition) {
$current = [];
if (array_key_exists('type', $repetition)) {
$current['type'] = $repetition['type'];
}
if (array_key_exists('moment', $repetition)) {
$current['moment'] = $repetition['moment'];
}
if (array_key_exists('skip', $repetition)) {
$current['skip'] = (int) $repetition['skip'];
}
if (array_key_exists('weekend', $repetition)) {
$current['weekend'] = (int) $repetition['weekend'];
}
$return[] = $current;
}
return $return;
}
/**
* The rules that the incoming request must be matched against.
*/
@@ -190,4 +131,63 @@ class StoreRequest extends FormRequest
Log::channel('audit')->error(sprintf('Validation errors in %s', self::class), $validator->errors()->toArray());
}
}
/**
* Returns the repetition data as it is found in the submitted data.
*/
private function getRepetitionData(): array
{
$return = [];
// repetition data:
/** @var null|array $repetitions */
$repetitions = $this->get('repetitions');
if (null === $repetitions) {
return [];
}
/** @var array $repetition */
foreach ($repetitions as $repetition) {
$current = [];
if (array_key_exists('type', $repetition)) {
$current['type'] = $repetition['type'];
}
if (array_key_exists('moment', $repetition)) {
$current['moment'] = $repetition['moment'];
}
if (array_key_exists('skip', $repetition)) {
$current['skip'] = (int) $repetition['skip'];
}
if (array_key_exists('weekend', $repetition)) {
$current['weekend'] = (int) $repetition['weekend'];
}
$return[] = $current;
}
return $return;
}
/**
* Returns the transaction data as it is found in the submitted data. It's a complex method according to code
* standards, but it just has a lot of ??-statements because of the fields that may or may not exist.
*/
private function getTransactionData(): array
{
$return = [];
// transaction data:
/** @var null|array $transactions */
$transactions = $this->get('transactions');
if (null === $transactions) {
return [];
}
/** @var array $transaction */
foreach ($transactions as $transaction) {
$return[] = $this->getSingleTransactionData($transaction);
}
return $return;
}
}

View File

@@ -77,70 +77,6 @@ class UpdateRequest extends FormRequest
return $return;
}
/**
* Returns the repetition data as it is found in the submitted data.
*/
private function getRepetitionData(): ?array
{
$return = [];
// repetition data:
/** @var null|array $repetitions */
$repetitions = $this->get('repetitions');
if (null === $repetitions) {
return null;
}
/** @var array $repetition */
foreach ($repetitions as $repetition) {
$current = [];
if (array_key_exists('type', $repetition)) {
$current['type'] = $repetition['type'];
}
if (array_key_exists('moment', $repetition)) {
$current['moment'] = (string) $repetition['moment'];
}
if (array_key_exists('skip', $repetition)) {
$current['skip'] = (int) $repetition['skip'];
}
if (array_key_exists('weekend', $repetition)) {
$current['weekend'] = (int) $repetition['weekend'];
}
$return[] = $current;
}
if (0 === count($return)) {
return null;
}
return $return;
}
/**
* Returns the transaction data as it is found in the submitted data. It's a complex method according to code
* standards, but it just has a lot of ??-statements because of the fields that may or may not exist.
*/
private function getTransactionData(): array
{
$return = [];
// transaction data:
/** @var null|array $transactions */
$transactions = $this->get('transactions');
if (null === $transactions) {
return [];
}
/** @var array $transaction */
foreach ($transactions as $transaction) {
$return[] = $this->getSingleTransactionData($transaction);
}
return $return;
}
/**
* The rules that the incoming request must be matched against.
*/
@@ -207,4 +143,68 @@ class UpdateRequest extends FormRequest
Log::channel('audit')->error(sprintf('Validation errors in %s', self::class), $validator->errors()->toArray());
}
}
/**
* Returns the repetition data as it is found in the submitted data.
*/
private function getRepetitionData(): ?array
{
$return = [];
// repetition data:
/** @var null|array $repetitions */
$repetitions = $this->get('repetitions');
if (null === $repetitions) {
return null;
}
/** @var array $repetition */
foreach ($repetitions as $repetition) {
$current = [];
if (array_key_exists('type', $repetition)) {
$current['type'] = $repetition['type'];
}
if (array_key_exists('moment', $repetition)) {
$current['moment'] = (string) $repetition['moment'];
}
if (array_key_exists('skip', $repetition)) {
$current['skip'] = (int) $repetition['skip'];
}
if (array_key_exists('weekend', $repetition)) {
$current['weekend'] = (int) $repetition['weekend'];
}
$return[] = $current;
}
if (0 === count($return)) {
return null;
}
return $return;
}
/**
* Returns the transaction data as it is found in the submitted data. It's a complex method according to code
* standards, but it just has a lot of ??-statements because of the fields that may or may not exist.
*/
private function getTransactionData(): array
{
$return = [];
// transaction data:
/** @var null|array $transactions */
$transactions = $this->get('transactions');
if (null === $transactions) {
return [];
}
/** @var array $transaction */
foreach ($transactions as $transaction) {
$return[] = $this->getSingleTransactionData($transaction);
}
return $return;
}
}

View File

@@ -65,43 +65,6 @@ class StoreRequest extends FormRequest
return $data;
}
private function getRuleTriggers(): array
{
$triggers = $this->get('triggers');
$return = [];
if (is_array($triggers)) {
foreach ($triggers as $trigger) {
$return[] = [
'type' => $trigger['type'] ?? '',
'value' => $trigger['value'] ?? null,
'prohibited' => $this->convertBoolean((string) ($trigger['prohibited'] ?? 'false')),
'active' => $this->convertBoolean((string) ($trigger['active'] ?? 'true')),
'stop_processing' => $this->convertBoolean((string) ($trigger['stop_processing'] ?? 'false')),
];
}
}
return $return;
}
private function getRuleActions(): array
{
$actions = $this->get('actions');
$return = [];
if (is_array($actions)) {
foreach ($actions as $action) {
$return[] = [
'type' => $action['type'],
'value' => $action['value'],
'active' => $this->convertBoolean((string) ($action['active'] ?? 'true')),
'stop_processing' => $this->convertBoolean((string) ($action['stop_processing'] ?? 'false')),
];
}
}
return $return;
}
/**
* The rules that the incoming request must be matched against.
*/
@@ -150,19 +113,6 @@ class StoreRequest extends FormRequest
}
}
/**
* Adds an error to the validator when there are no triggers in the array of data.
*/
protected function atLeastOneTrigger(Validator $validator): void
{
$data = $validator->getData();
$triggers = $data['triggers'] ?? [];
// need at least one trigger
if (!is_countable($triggers) || 0 === count($triggers)) {
$validator->errors()->add('title', (string) trans('validation.at_least_one_trigger'));
}
}
/**
* Adds an error to the validator when there are no repetitions in the array of data.
*/
@@ -176,6 +126,35 @@ class StoreRequest extends FormRequest
}
}
/**
* Adds an error to the validator when there are no ACTIVE actions in the array of data.
*/
protected function atLeastOneActiveAction(Validator $validator): void
{
$data = $validator->getData();
/** @var null|array|int|string $actions */
$actions = $data['actions'] ?? [];
// need at least one trigger
if (!is_countable($actions) || 0 === count($actions)) {
return;
}
$allInactive = true;
$inactiveIndex = 0;
foreach ($actions as $index => $action) {
$active = array_key_exists('active', $action) ? $action['active'] : true; // assume true
if (true === $active) {
$allInactive = false;
}
if (false === $active) {
$inactiveIndex = $index;
}
}
if ($allInactive) {
$validator->errors()->add(sprintf('actions.%d.active', $inactiveIndex), (string) trans('validation.at_least_one_active_action'));
}
}
/**
* Adds an error to the validator when there are no ACTIVE triggers in the array of data.
*/
@@ -206,31 +185,52 @@ class StoreRequest extends FormRequest
}
/**
* Adds an error to the validator when there are no ACTIVE actions in the array of data.
* Adds an error to the validator when there are no triggers in the array of data.
*/
protected function atLeastOneActiveAction(Validator $validator): void
protected function atLeastOneTrigger(Validator $validator): void
{
$data = $validator->getData();
/** @var null|array|int|string $actions */
$actions = $data['actions'] ?? [];
$data = $validator->getData();
$triggers = $data['triggers'] ?? [];
// need at least one trigger
if (!is_countable($actions) || 0 === count($actions)) {
return;
}
$allInactive = true;
$inactiveIndex = 0;
foreach ($actions as $index => $action) {
$active = array_key_exists('active', $action) ? $action['active'] : true; // assume true
if (true === $active) {
$allInactive = false;
}
if (false === $active) {
$inactiveIndex = $index;
}
}
if ($allInactive) {
$validator->errors()->add(sprintf('actions.%d.active', $inactiveIndex), (string) trans('validation.at_least_one_active_action'));
if (!is_countable($triggers) || 0 === count($triggers)) {
$validator->errors()->add('title', (string) trans('validation.at_least_one_trigger'));
}
}
private function getRuleActions(): array
{
$actions = $this->get('actions');
$return = [];
if (is_array($actions)) {
foreach ($actions as $action) {
$return[] = [
'type' => $action['type'],
'value' => $action['value'],
'active' => $this->convertBoolean((string) ($action['active'] ?? 'true')),
'stop_processing' => $this->convertBoolean((string) ($action['stop_processing'] ?? 'false')),
];
}
}
return $return;
}
private function getRuleTriggers(): array
{
$triggers = $this->get('triggers');
$return = [];
if (is_array($triggers)) {
foreach ($triggers as $trigger) {
$return[] = [
'type' => $trigger['type'] ?? '',
'value' => $trigger['value'] ?? null,
'prohibited' => $this->convertBoolean((string) ($trigger['prohibited'] ?? 'false')),
'active' => $this->convertBoolean((string) ($trigger['active'] ?? 'true')),
'stop_processing' => $this->convertBoolean((string) ($trigger['stop_processing'] ?? 'false')),
];
}
}
return $return;
}
}

View File

@@ -42,9 +42,19 @@ class TestRequest extends FormRequest
return ['page' => $this->getPage(), 'start' => $this->getDate('start'), 'end' => $this->getDate('end'), 'accounts' => $this->getAccounts()];
}
private function getPage(): int
public function rules(): array
{
return 0 === (int) $this->query('page') ? 1 : (int) $this->query('page');
return [
'start' => 'date|after:1970-01-02|before:2038-01-17',
'end' => 'date|after_or_equal:start|after:1970-01-02|before:2038-01-17',
'accounts' => '',
'accounts.*' => 'required|exists:accounts,id|belongsToUser:accounts',
];
}
private function getAccounts(): array
{
return $this->get('accounts') ?? [];
}
private function getDate(string $field): ?Carbon
@@ -58,18 +68,8 @@ class TestRequest extends FormRequest
return null === $this->query($field) ? null : Carbon::createFromFormat('Y-m-d', substr($value, 0, 10));
}
private function getAccounts(): array
private function getPage(): int
{
return $this->get('accounts') ?? [];
}
public function rules(): array
{
return [
'start' => 'date|after:1970-01-02|before:2038-01-17',
'end' => 'date|after_or_equal:start|after:1970-01-02|before:2038-01-17',
'accounts' => '',
'accounts.*' => 'required|exists:accounts,id|belongsToUser:accounts',
];
return 0 === (int) $this->query('page') ? 1 : (int) $this->query('page');
}
}

View File

@@ -42,6 +42,21 @@ class TriggerRequest extends FormRequest
return ['start' => $this->getDate('start'), 'end' => $this->getDate('end'), 'accounts' => $this->getAccounts()];
}
public function rules(): array
{
return [
'start' => 'date|after:1970-01-02|before:2038-01-17',
'end' => 'date|after_or_equal:start|after:1970-01-02|before:2038-01-17',
'accounts' => '',
'accounts.*' => 'exists:accounts,id|belongsToUser:accounts',
];
}
private function getAccounts(): array
{
return $this->get('accounts') ?? [];
}
private function getDate(string $field): ?Carbon
{
$value = $this->query($field);
@@ -52,19 +67,4 @@ class TriggerRequest extends FormRequest
return null === $this->query($field) ? null : Carbon::createFromFormat('Y-m-d', substr($value, 0, 10));
}
private function getAccounts(): array
{
return $this->get('accounts') ?? [];
}
public function rules(): array
{
return [
'start' => 'date|after:1970-01-02|before:2038-01-17',
'end' => 'date|after_or_equal:start|after:1970-01-02|before:2038-01-17',
'accounts' => '',
'accounts.*' => 'exists:accounts,id|belongsToUser:accounts',
];
}
}

View File

@@ -72,52 +72,6 @@ class UpdateRequest extends FormRequest
return $return;
}
private function getRuleTriggers(): ?array
{
if (!$this->has('triggers')) {
return null;
}
$triggers = $this->get('triggers');
$return = [];
if (is_array($triggers)) {
foreach ($triggers as $trigger) {
$active = array_key_exists('active', $trigger) ? $trigger['active'] : true;
$prohibited = array_key_exists('prohibited', $trigger) ? $trigger['prohibited'] : false;
$stopProcessing = array_key_exists('stop_processing', $trigger) ? $trigger['stop_processing'] : false;
$return[] = [
'type' => $trigger['type'],
'value' => $trigger['value'],
'prohibited' => $prohibited,
'active' => $active,
'stop_processing' => $stopProcessing,
];
}
}
return $return;
}
private function getRuleActions(): ?array
{
if (!$this->has('actions')) {
return null;
}
$actions = $this->get('actions');
$return = [];
if (is_array($actions)) {
foreach ($actions as $action) {
$return[] = [
'type' => $action['type'],
'value' => $action['value'],
'active' => $this->convertBoolean((string) ($action['active'] ?? 'false')),
'stop_processing' => $this->convertBoolean((string) ($action['stop_processing'] ?? 'false')),
];
}
}
return $return;
}
/**
* The rules that the incoming request must be matched against.
*/
@@ -170,46 +124,6 @@ class UpdateRequest extends FormRequest
}
}
/**
* Adds an error to the validator when there are no repetitions in the array of data.
*/
protected function atLeastOneTrigger(Validator $validator): void
{
$data = $validator->getData();
$triggers = $data['triggers'] ?? null;
// need at least one trigger
if (is_array($triggers) && 0 === count($triggers)) {
$validator->errors()->add('title', (string) trans('validation.at_least_one_trigger'));
}
}
/**
* Adds an error to the validator when there are no repetitions in the array of data.
*/
protected function atLeastOneValidTrigger(Validator $validator): void
{
$data = $validator->getData();
$triggers = $data['triggers'] ?? [];
$allInactive = true;
$inactiveIndex = 0;
// need at least one trigger
if (is_array($triggers) && 0 === count($triggers)) {
return;
}
foreach ($triggers as $index => $trigger) {
$active = array_key_exists('active', $trigger) ? $trigger['active'] : true; // assume true
if (true === $active) {
$allInactive = false;
}
if (false === $active) {
$inactiveIndex = $index;
}
}
if ($allInactive) {
$validator->errors()->add(sprintf('triggers.%d.active', $inactiveIndex), (string) trans('validation.at_least_one_active_trigger'));
}
}
/**
* Adds an error to the validator when there are no repetitions in the array of data.
*/
@@ -223,6 +137,19 @@ class UpdateRequest extends FormRequest
}
}
/**
* Adds an error to the validator when there are no repetitions in the array of data.
*/
protected function atLeastOneTrigger(Validator $validator): void
{
$data = $validator->getData();
$triggers = $data['triggers'] ?? null;
// need at least one trigger
if (is_array($triggers) && 0 === count($triggers)) {
$validator->errors()->add('title', (string) trans('validation.at_least_one_trigger'));
}
}
/**
* Adds an error to the validator when there are no repetitions in the array of data.
*/
@@ -250,4 +177,77 @@ class UpdateRequest extends FormRequest
$validator->errors()->add(sprintf('actions.%d.active', $inactiveIndex), (string) trans('validation.at_least_one_active_action'));
}
}
/**
* Adds an error to the validator when there are no repetitions in the array of data.
*/
protected function atLeastOneValidTrigger(Validator $validator): void
{
$data = $validator->getData();
$triggers = $data['triggers'] ?? [];
$allInactive = true;
$inactiveIndex = 0;
// need at least one trigger
if (is_array($triggers) && 0 === count($triggers)) {
return;
}
foreach ($triggers as $index => $trigger) {
$active = array_key_exists('active', $trigger) ? $trigger['active'] : true; // assume true
if (true === $active) {
$allInactive = false;
}
if (false === $active) {
$inactiveIndex = $index;
}
}
if ($allInactive) {
$validator->errors()->add(sprintf('triggers.%d.active', $inactiveIndex), (string) trans('validation.at_least_one_active_trigger'));
}
}
private function getRuleActions(): ?array
{
if (!$this->has('actions')) {
return null;
}
$actions = $this->get('actions');
$return = [];
if (is_array($actions)) {
foreach ($actions as $action) {
$return[] = [
'type' => $action['type'],
'value' => $action['value'],
'active' => $this->convertBoolean((string) ($action['active'] ?? 'false')),
'stop_processing' => $this->convertBoolean((string) ($action['stop_processing'] ?? 'false')),
];
}
}
return $return;
}
private function getRuleTriggers(): ?array
{
if (!$this->has('triggers')) {
return null;
}
$triggers = $this->get('triggers');
$return = [];
if (is_array($triggers)) {
foreach ($triggers as $trigger) {
$active = array_key_exists('active', $trigger) ? $trigger['active'] : true;
$prohibited = array_key_exists('prohibited', $trigger) ? $trigger['prohibited'] : false;
$stopProcessing = array_key_exists('stop_processing', $trigger) ? $trigger['stop_processing'] : false;
$return[] = [
'type' => $trigger['type'],
'value' => $trigger['value'],
'prohibited' => $prohibited,
'active' => $active,
'stop_processing' => $stopProcessing,
];
}
}
return $return;
}
}

View File

@@ -42,6 +42,21 @@ class TestRequest extends FormRequest
return ['start' => $this->getDate('start'), 'end' => $this->getDate('end'), 'accounts' => $this->getAccounts()];
}
public function rules(): array
{
return [
'start' => 'date|after:1970-01-02|before:2038-01-17',
'end' => 'date|after_or_equal:start|after:1970-01-02|before:2038-01-17',
'accounts' => '',
'accounts.*' => 'exists:accounts,id|belongsToUser:accounts',
];
}
private function getAccounts(): array
{
return $this->get('accounts') ?? [];
}
private function getDate(string $field): ?Carbon
{
$value = $this->query($field);
@@ -52,19 +67,4 @@ class TestRequest extends FormRequest
return null === $this->query($field) ? null : Carbon::createFromFormat('Y-m-d', substr($value, 0, 10));
}
private function getAccounts(): array
{
return $this->get('accounts') ?? [];
}
public function rules(): array
{
return [
'start' => 'date|after:1970-01-02|before:2038-01-17',
'end' => 'date|after_or_equal:start|after:1970-01-02|before:2038-01-17',
'accounts' => '',
'accounts.*' => 'exists:accounts,id|belongsToUser:accounts',
];
}
}

View File

@@ -42,15 +42,9 @@ class TriggerRequest extends FormRequest
return ['start' => $this->getDate('start'), 'end' => $this->getDate('end'), 'accounts' => $this->getAccounts()];
}
private function getDate(string $field): ?Carbon
public function rules(): array
{
$value = $this->query($field);
if (is_array($value)) {
return null;
}
$value = (string) $value;
return null === $this->query($field) ? null : Carbon::createFromFormat('Y-m-d', substr($value, 0, 10));
return ['start' => 'date|after:1970-01-02|before:2038-01-17', 'end' => 'date|after_or_equal:start|after:1970-01-02|before:2038-01-17'];
}
private function getAccounts(): array
@@ -62,8 +56,14 @@ class TriggerRequest extends FormRequest
return $this->get('accounts');
}
public function rules(): array
private function getDate(string $field): ?Carbon
{
return ['start' => 'date|after:1970-01-02|before:2038-01-17', 'end' => 'date|after_or_equal:start|after:1970-01-02|before:2038-01-17'];
$value = $this->query($field);
if (is_array($value)) {
return null;
}
$value = (string) $value;
return null === $this->query($field) ? null : Carbon::createFromFormat('Y-m-d', substr($value, 0, 10));
}
}

View File

@@ -73,106 +73,6 @@ class StoreRequest extends FormRequest
// TODO include location and ability to process it.
}
/**
* Get transaction data.
*/
private function getTransactionData(): array
{
$return = [];
/**
* @var array $transaction
*/
foreach ($this->get('transactions') as $transaction) {
$object = new NullArrayObject($transaction);
$return[] = [
'type' => $this->clearString($object['type']),
'date' => $this->dateFromValue($object['date']),
'order' => $this->integerFromValue((string) $object['order']),
'currency_id' => $this->integerFromValue((string) $object['currency_id']),
'currency_code' => $this->clearString((string) $object['currency_code']),
// location
'latitude' => $this->floatFromValue((string) $object['latitude']),
'longitude' => $this->floatFromValue((string) $object['longitude']),
'zoom_level' => $this->integerFromValue((string) $object['zoom_level']),
// foreign currency info:
'foreign_currency_id' => $this->integerFromValue((string) $object['foreign_currency_id']),
'foreign_currency_code' => $this->clearString((string) $object['foreign_currency_code']),
// amount and foreign amount. Cannot be 0.
'amount' => $this->clearString((string) $object['amount']),
'foreign_amount' => $this->clearString((string) $object['foreign_amount']),
// description.
'description' => $this->clearString($object['description']),
// source of transaction. If everything is null, assume cash account.
'source_id' => $this->integerFromValue((string) $object['source_id']),
'source_name' => $this->clearString((string) $object['source_name']),
'source_iban' => $this->clearIban((string) $object['source_iban']),
'source_number' => $this->clearString((string) $object['source_number']),
'source_bic' => $this->clearString((string) $object['source_bic']),
// destination of transaction. If everything is null, assume cash account.
'destination_id' => $this->integerFromValue((string) $object['destination_id']),
'destination_name' => $this->clearString((string) $object['destination_name']),
'destination_iban' => $this->clearIban((string) $object['destination_iban']),
'destination_number' => $this->clearString((string) $object['destination_number']),
'destination_bic' => $this->clearString((string) $object['destination_bic']),
// budget info
'budget_id' => $this->integerFromValue((string) $object['budget_id']),
'budget_name' => $this->clearString((string) $object['budget_name']),
// category info
'category_id' => $this->integerFromValue((string) $object['category_id']),
'category_name' => $this->clearString((string) $object['category_name']),
// journal bill reference. Optional. Will only work for withdrawals
'bill_id' => $this->integerFromValue((string) $object['bill_id']),
'bill_name' => $this->clearString((string) $object['bill_name']),
// piggy bank reference. Optional. Will only work for transfers
'piggy_bank_id' => $this->integerFromValue((string) $object['piggy_bank_id']),
'piggy_bank_name' => $this->clearString((string) $object['piggy_bank_name']),
// some other interesting properties
'reconciled' => $this->convertBoolean((string) $object['reconciled']),
'notes' => $this->clearStringKeepNewlines((string) $object['notes']),
'tags' => $this->arrayFromValue($object['tags']),
// all custom fields:
'internal_reference' => $this->clearString((string) $object['internal_reference']),
'external_id' => $this->clearString((string) $object['external_id']),
'original_source' => sprintf('ff3-v%s', config('firefly.version')),
'recurrence_id' => $this->integerFromValue($object['recurrence_id']),
'bunq_payment_id' => $this->clearString((string) $object['bunq_payment_id']),
'external_url' => $this->clearString((string) $object['external_url']),
'sepa_cc' => $this->clearString((string) $object['sepa_cc']),
'sepa_ct_op' => $this->clearString((string) $object['sepa_ct_op']),
'sepa_ct_id' => $this->clearString((string) $object['sepa_ct_id']),
'sepa_db' => $this->clearString((string) $object['sepa_db']),
'sepa_country' => $this->clearString((string) $object['sepa_country']),
'sepa_ep' => $this->clearString((string) $object['sepa_ep']),
'sepa_ci' => $this->clearString((string) $object['sepa_ci']),
'sepa_batch_id' => $this->clearString((string) $object['sepa_batch_id']),
// custom date fields. Must be Carbon objects. Presence is optional.
'interest_date' => $this->dateFromValue($object['interest_date']),
'book_date' => $this->dateFromValue($object['book_date']),
'process_date' => $this->dateFromValue($object['process_date']),
'due_date' => $this->dateFromValue($object['due_date']),
'payment_date' => $this->dateFromValue($object['payment_date']),
'invoice_date' => $this->dateFromValue($object['invoice_date']),
];
}
return $return;
}
/**
* The rules that the incoming request must be matched against.
*/
@@ -305,4 +205,104 @@ class StoreRequest extends FormRequest
Log::channel('audit')->error(sprintf('Validation errors in %s', self::class), $validator->errors()->toArray());
}
}
/**
* Get transaction data.
*/
private function getTransactionData(): array
{
$return = [];
/**
* @var array $transaction
*/
foreach ($this->get('transactions') as $transaction) {
$object = new NullArrayObject($transaction);
$return[] = [
'type' => $this->clearString($object['type']),
'date' => $this->dateFromValue($object['date']),
'order' => $this->integerFromValue((string) $object['order']),
'currency_id' => $this->integerFromValue((string) $object['currency_id']),
'currency_code' => $this->clearString((string) $object['currency_code']),
// location
'latitude' => $this->floatFromValue((string) $object['latitude']),
'longitude' => $this->floatFromValue((string) $object['longitude']),
'zoom_level' => $this->integerFromValue((string) $object['zoom_level']),
// foreign currency info:
'foreign_currency_id' => $this->integerFromValue((string) $object['foreign_currency_id']),
'foreign_currency_code' => $this->clearString((string) $object['foreign_currency_code']),
// amount and foreign amount. Cannot be 0.
'amount' => $this->clearString((string) $object['amount']),
'foreign_amount' => $this->clearString((string) $object['foreign_amount']),
// description.
'description' => $this->clearString($object['description']),
// source of transaction. If everything is null, assume cash account.
'source_id' => $this->integerFromValue((string) $object['source_id']),
'source_name' => $this->clearString((string) $object['source_name']),
'source_iban' => $this->clearIban((string) $object['source_iban']),
'source_number' => $this->clearString((string) $object['source_number']),
'source_bic' => $this->clearString((string) $object['source_bic']),
// destination of transaction. If everything is null, assume cash account.
'destination_id' => $this->integerFromValue((string) $object['destination_id']),
'destination_name' => $this->clearString((string) $object['destination_name']),
'destination_iban' => $this->clearIban((string) $object['destination_iban']),
'destination_number' => $this->clearString((string) $object['destination_number']),
'destination_bic' => $this->clearString((string) $object['destination_bic']),
// budget info
'budget_id' => $this->integerFromValue((string) $object['budget_id']),
'budget_name' => $this->clearString((string) $object['budget_name']),
// category info
'category_id' => $this->integerFromValue((string) $object['category_id']),
'category_name' => $this->clearString((string) $object['category_name']),
// journal bill reference. Optional. Will only work for withdrawals
'bill_id' => $this->integerFromValue((string) $object['bill_id']),
'bill_name' => $this->clearString((string) $object['bill_name']),
// piggy bank reference. Optional. Will only work for transfers
'piggy_bank_id' => $this->integerFromValue((string) $object['piggy_bank_id']),
'piggy_bank_name' => $this->clearString((string) $object['piggy_bank_name']),
// some other interesting properties
'reconciled' => $this->convertBoolean((string) $object['reconciled']),
'notes' => $this->clearStringKeepNewlines((string) $object['notes']),
'tags' => $this->arrayFromValue($object['tags']),
// all custom fields:
'internal_reference' => $this->clearString((string) $object['internal_reference']),
'external_id' => $this->clearString((string) $object['external_id']),
'original_source' => sprintf('ff3-v%s', config('firefly.version')),
'recurrence_id' => $this->integerFromValue($object['recurrence_id']),
'bunq_payment_id' => $this->clearString((string) $object['bunq_payment_id']),
'external_url' => $this->clearString((string) $object['external_url']),
'sepa_cc' => $this->clearString((string) $object['sepa_cc']),
'sepa_ct_op' => $this->clearString((string) $object['sepa_ct_op']),
'sepa_ct_id' => $this->clearString((string) $object['sepa_ct_id']),
'sepa_db' => $this->clearString((string) $object['sepa_db']),
'sepa_country' => $this->clearString((string) $object['sepa_country']),
'sepa_ep' => $this->clearString((string) $object['sepa_ep']),
'sepa_ci' => $this->clearString((string) $object['sepa_ci']),
'sepa_batch_id' => $this->clearString((string) $object['sepa_batch_id']),
// custom date fields. Must be Carbon objects. Presence is optional.
'interest_date' => $this->dateFromValue($object['interest_date']),
'book_date' => $this->dateFromValue($object['book_date']),
'process_date' => $this->dateFromValue($object['process_date']),
'due_date' => $this->dateFromValue($object['due_date']),
'payment_date' => $this->dateFromValue($object['payment_date']),
'invoice_date' => $this->dateFromValue($object['invoice_date']),
];
}
return $return;
}
}

View File

@@ -133,158 +133,6 @@ class UpdateRequest extends FormRequest
return $data;
}
/**
* Get transaction data.
*
* @throws FireflyException
*/
private function getTransactionData(): array
{
Log::debug(sprintf('Now in %s', __METHOD__));
$return = [];
/** @var null|array $transactions */
$transactions = $this->get('transactions');
if (!is_countable($transactions)) {
return $return;
}
/** @var null|array $transaction */
foreach ($transactions as $transaction) {
if (!is_array($transaction)) {
throw new FireflyException('Invalid data submitted: transaction is not array.');
}
// default response is to update nothing in the transaction:
$current = [];
$current = $this->getIntegerData($current, $transaction);
$current = $this->getStringData($current, $transaction);
$current = $this->getNlStringData($current, $transaction);
$current = $this->getDateData($current, $transaction);
$current = $this->getBooleanData($current, $transaction);
$current = $this->getArrayData($current, $transaction);
$current = $this->getFloatData($current, $transaction);
$return[] = $current;
}
return $return;
}
/**
* For each field, add it to the array if a reference is present in the request:
*
* @param array<string, string> $current
* @param array<string, mixed> $transaction
*/
private function getIntegerData(array $current, array $transaction): array
{
foreach ($this->integerFields as $fieldName) {
if (array_key_exists($fieldName, $transaction)) {
$current[$fieldName] = $this->integerFromValue((string) $transaction[$fieldName]);
}
}
return $current;
}
/**
* @param array<string, string> $current
* @param array<string, mixed> $transaction
*/
private function getStringData(array $current, array $transaction): array
{
foreach ($this->stringFields as $fieldName) {
if (array_key_exists($fieldName, $transaction)) {
$current[$fieldName] = $this->clearString((string) $transaction[$fieldName]);
}
}
return $current;
}
/**
* @param array<string, string> $current
* @param array<string, mixed> $transaction
*/
private function getNlStringData(array $current, array $transaction): array
{
foreach ($this->textareaFields as $fieldName) {
if (array_key_exists($fieldName, $transaction)) {
$current[$fieldName] = $this->clearStringKeepNewlines((string) $transaction[$fieldName]); // keep newlines
}
}
return $current;
}
/**
* @param array<string, string> $current
* @param array<string, mixed> $transaction
*/
private function getDateData(array $current, array $transaction): array
{
foreach ($this->dateFields as $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]);
}
}
return $current;
}
/**
* @param array<string, string> $current
* @param array<string, mixed> $transaction
*/
private function getBooleanData(array $current, array $transaction): array
{
foreach ($this->booleanFields as $fieldName) {
if (array_key_exists($fieldName, $transaction)) {
$current[$fieldName] = $this->convertBoolean((string) $transaction[$fieldName]);
}
}
return $current;
}
/**
* @param array<string, string> $current
* @param array<string, mixed> $transaction
*/
private function getArrayData(array $current, array $transaction): array
{
foreach ($this->arrayFields as $fieldName) {
if (array_key_exists($fieldName, $transaction)) {
$current[$fieldName] = $this->arrayFromValue($transaction[$fieldName]);
}
}
return $current;
}
/**
* @param array<string, string> $current
* @param array<string, mixed> $transaction
*/
private function getFloatData(array $current, array $transaction): array
{
foreach ($this->floatFields as $fieldName) {
if (array_key_exists($fieldName, $transaction)) {
$value = $transaction[$fieldName];
if (is_float($value)) {
$current[$fieldName] = sprintf('%.12f', $value);
}
if (!is_float($value)) {
$current[$fieldName] = (string) $value;
}
}
}
return $current;
}
/**
* The rules that the incoming request must be matched against.
*/
@@ -406,4 +254,156 @@ class UpdateRequest extends FormRequest
Log::channel('audit')->error(sprintf('Validation errors in %s', self::class), $validator->errors()->toArray());
}
}
/**
* @param array<string, string> $current
* @param array<string, mixed> $transaction
*/
private function getArrayData(array $current, array $transaction): array
{
foreach ($this->arrayFields as $fieldName) {
if (array_key_exists($fieldName, $transaction)) {
$current[$fieldName] = $this->arrayFromValue($transaction[$fieldName]);
}
}
return $current;
}
/**
* @param array<string, string> $current
* @param array<string, mixed> $transaction
*/
private function getBooleanData(array $current, array $transaction): array
{
foreach ($this->booleanFields as $fieldName) {
if (array_key_exists($fieldName, $transaction)) {
$current[$fieldName] = $this->convertBoolean((string) $transaction[$fieldName]);
}
}
return $current;
}
/**
* @param array<string, string> $current
* @param array<string, mixed> $transaction
*/
private function getDateData(array $current, array $transaction): array
{
foreach ($this->dateFields as $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]);
}
}
return $current;
}
/**
* @param array<string, string> $current
* @param array<string, mixed> $transaction
*/
private function getFloatData(array $current, array $transaction): array
{
foreach ($this->floatFields as $fieldName) {
if (array_key_exists($fieldName, $transaction)) {
$value = $transaction[$fieldName];
if (is_float($value)) {
$current[$fieldName] = sprintf('%.12f', $value);
}
if (!is_float($value)) {
$current[$fieldName] = (string) $value;
}
}
}
return $current;
}
/**
* For each field, add it to the array if a reference is present in the request:
*
* @param array<string, string> $current
* @param array<string, mixed> $transaction
*/
private function getIntegerData(array $current, array $transaction): array
{
foreach ($this->integerFields as $fieldName) {
if (array_key_exists($fieldName, $transaction)) {
$current[$fieldName] = $this->integerFromValue((string) $transaction[$fieldName]);
}
}
return $current;
}
/**
* @param array<string, string> $current
* @param array<string, mixed> $transaction
*/
private function getNlStringData(array $current, array $transaction): array
{
foreach ($this->textareaFields as $fieldName) {
if (array_key_exists($fieldName, $transaction)) {
$current[$fieldName] = $this->clearStringKeepNewlines((string) $transaction[$fieldName]); // keep newlines
}
}
return $current;
}
/**
* @param array<string, string> $current
* @param array<string, mixed> $transaction
*/
private function getStringData(array $current, array $transaction): array
{
foreach ($this->stringFields as $fieldName) {
if (array_key_exists($fieldName, $transaction)) {
$current[$fieldName] = $this->clearString((string) $transaction[$fieldName]);
}
}
return $current;
}
/**
* Get transaction data.
*
* @throws FireflyException
*/
private function getTransactionData(): array
{
Log::debug(sprintf('Now in %s', __METHOD__));
$return = [];
/** @var null|array $transactions */
$transactions = $this->get('transactions');
if (!is_countable($transactions)) {
return $return;
}
/** @var null|array $transaction */
foreach ($transactions as $transaction) {
if (!is_array($transaction)) {
throw new FireflyException('Invalid data submitted: transaction is not array.');
}
// default response is to update nothing in the transaction:
$current = [];
$current = $this->getIntegerData($current, $transaction);
$current = $this->getStringData($current, $transaction);
$current = $this->getNlStringData($current, $transaction);
$current = $this->getDateData($current, $transaction);
$current = $this->getBooleanData($current, $transaction);
$current = $this->getArrayData($current, $transaction);
$current = $this->getFloatData($current, $transaction);
$return[] = $current;
}
return $return;
}
}

View File

@@ -62,14 +62,6 @@ class ConvertsDatesToUTC extends Command
return Command::SUCCESS;
}
private function ConvertModeltoUTC(string $model, array $fields): void
{
/** @var string $field */
foreach ($fields as $field) {
$this->convertFieldtoUTC($model, $field);
}
}
private function convertFieldtoUTC(string $model, string $field): void
{
$this->info(sprintf('Converting %s.%s to UTC', $model, $field));
@@ -98,4 +90,12 @@ class ConvertsDatesToUTC extends Command
$item->save();
});
}
private function ConvertModeltoUTC(string $model, array $fields): void
{
/** @var string $field */
foreach ($fields as $field) {
$this->convertFieldtoUTC($model, $field);
}
}
}

View File

@@ -123,56 +123,17 @@ class CorrectsAccountTypes extends Command
return 0;
}
private function stupidLaravel(): void
private function canCreateDestination(array $validDestinations): bool
{
$this->count = 0;
return in_array(AccountTypeEnum::EXPENSE->value, $validDestinations, true);
}
private function inspectJournal(TransactionJournal $journal): void
/**
* Can only create revenue accounts out of the blue.
*/
private function canCreateSource(array $validSources): bool
{
Log::debug(sprintf('Now inspecting journal #%d', $journal->id));
$transactions = $journal->transactions()->count();
if (2 !== $transactions) {
Log::debug(sprintf('Journal has %d transactions, so can\'t fix.', $transactions));
$this->friendlyError(sprintf('Cannot inspect transaction journal #%d because it has %d transaction(s) instead of 2.', $journal->id, $transactions));
return;
}
$type = $journal->transactionType->type;
$sourceTransaction = $this->getSourceTransaction($journal);
$destTransaction = $this->getDestinationTransaction($journal);
$sourceAccount = $sourceTransaction->account;
$sourceAccountType = $sourceAccount->accountType->type;
$destAccount = $destTransaction->account;
$destAccountType = $destAccount->accountType->type;
if (!array_key_exists($type, $this->expected)) {
Log::info(sprintf('No source/destination info for transaction type %s.', $type));
$this->friendlyError(sprintf('No source/destination info for transaction type %s.', $type));
return;
}
if (!array_key_exists($sourceAccountType, $this->expected[$type])) {
Log::debug(sprintf('[a] Going to fix journal #%d', $journal->id));
$this->fixJournal($journal, $type, $sourceTransaction, $destTransaction);
return;
}
$expectedTypes = $this->expected[$type][$sourceAccountType];
if (!in_array($destAccountType, $expectedTypes, true)) {
Log::debug(sprintf('[b] Going to fix journal #%d', $journal->id));
$this->fixJournal($journal, $type, $sourceTransaction, $destTransaction);
}
}
private function getSourceTransaction(TransactionJournal $journal): Transaction
{
return $journal->transactions->firstWhere('amount', '<', 0);
}
private function getDestinationTransaction(TransactionJournal $journal): Transaction
{
return $journal->transactions->firstWhere('amount', '>', 0);
return in_array(AccountTypeEnum::REVENUE->value, $validSources, true);
}
private function fixJournal(TransactionJournal $journal, string $transactionType, Transaction $source, Transaction $dest): void
@@ -266,12 +227,113 @@ class CorrectsAccountTypes extends Command
}
}
private function shouldBeTransfer(string $transactionType, string $sourceType, string $destinationType): bool
private function getDestinationTransaction(TransactionJournal $journal): Transaction
{
return
TransactionTypeEnum::TRANSFER->value === $transactionType
&& AccountTypeEnum::ASSET->value === $sourceType
&& $this->isLiability($destinationType);
return $journal->transactions->firstWhere('amount', '>', 0);
}
private function getSourceTransaction(TransactionJournal $journal): Transaction
{
return $journal->transactions->firstWhere('amount', '<', 0);
}
private function giveNewDestinationAccount(TransactionJournal $journal, Account $newDestination): void
{
$destTransaction = $this->getDestinationTransaction($journal);
$oldDest = $destTransaction->account;
$destTransaction->account_id = $newDestination->id;
$destTransaction->save();
$message = sprintf(
'Transaction journal #%d, destination account changed from #%d ("%s") to #%d ("%s").',
$journal->id,
$oldDest->id,
$oldDest->name,
$newDestination->id,
$newDestination->name
);
$this->friendlyInfo($message);
$journal->refresh();
Log::debug($message);
}
private function giveNewExpense(TransactionJournal $journal, Transaction $destination): void
{
Log::debug(sprintf('An account of type "%s" could be a valid destination.', AccountTypeEnum::EXPENSE->value));
$this->factory->setUser($journal->user);
$name = $destination->account->name;
$newDestination = $this->factory->findOrCreate($name, AccountTypeEnum::EXPENSE->value);
$destination->account()->associate($newDestination);
$destination->save();
$this->friendlyPositive(sprintf(
'Firefly III gave transaction #%d a new destination %s: #%d ("%s").',
$journal->transaction_group_id,
AccountTypeEnum::EXPENSE->value,
$newDestination->id,
$newDestination->name
));
Log::debug(sprintf('Associated account #%d with transaction #%d', $newDestination->id, $destination->id));
$this->inspectJournal($journal);
}
private function giveNewRevenue(TransactionJournal $journal, Transaction $source): void
{
Log::debug(sprintf('An account of type "%s" could be a valid source.', AccountTypeEnum::REVENUE->value));
$this->factory->setUser($journal->user);
$name = $source->account->name;
$newSource = $this->factory->findOrCreate($name, AccountTypeEnum::REVENUE->value);
$source->account()->associate($newSource);
$source->save();
$this->friendlyPositive(sprintf(
'Firefly III gave transaction #%d a new source %s: #%d ("%s").',
$journal->transaction_group_id,
AccountTypeEnum::REVENUE->value,
$newSource->id,
$newSource->name
));
Log::debug(sprintf('Associated account #%d with transaction #%d', $newSource->id, $source->id));
$this->inspectJournal($journal);
}
private function hasValidAccountType(array $validTypes, string $accountType): bool
{
return in_array($accountType, $validTypes, true);
}
private function inspectJournal(TransactionJournal $journal): void
{
Log::debug(sprintf('Now inspecting journal #%d', $journal->id));
$transactions = $journal->transactions()->count();
if (2 !== $transactions) {
Log::debug(sprintf('Journal has %d transactions, so can\'t fix.', $transactions));
$this->friendlyError(sprintf('Cannot inspect transaction journal #%d because it has %d transaction(s) instead of 2.', $journal->id, $transactions));
return;
}
$type = $journal->transactionType->type;
$sourceTransaction = $this->getSourceTransaction($journal);
$destTransaction = $this->getDestinationTransaction($journal);
$sourceAccount = $sourceTransaction->account;
$sourceAccountType = $sourceAccount->accountType->type;
$destAccount = $destTransaction->account;
$destAccountType = $destAccount->accountType->type;
if (!array_key_exists($type, $this->expected)) {
Log::info(sprintf('No source/destination info for transaction type %s.', $type));
$this->friendlyError(sprintf('No source/destination info for transaction type %s.', $type));
return;
}
if (!array_key_exists($sourceAccountType, $this->expected[$type])) {
Log::debug(sprintf('[a] Going to fix journal #%d', $journal->id));
$this->fixJournal($journal, $type, $sourceTransaction, $destTransaction);
return;
}
$expectedTypes = $this->expected[$type][$sourceAccountType];
if (!in_array($destAccountType, $expectedTypes, true)) {
Log::debug(sprintf('[b] Going to fix journal #%d', $journal->id));
$this->fixJournal($journal, $type, $sourceTransaction, $destTransaction);
}
}
private function isLiability(string $destinationType): bool
@@ -282,27 +344,6 @@ class CorrectsAccountTypes extends Command
|| AccountTypeEnum::MORTGAGE->value === $destinationType;
}
private function makeTransfer(TransactionJournal $journal): void
{
// from an asset to a liability should be a withdrawal:
$withdrawal = TransactionType::whereType(TransactionTypeEnum::WITHDRAWAL->value)->first();
$journal->transactionType()->associate($withdrawal);
$journal->save();
$message = sprintf('Converted transaction #%d from a transfer to a withdrawal.', $journal->id);
$this->friendlyInfo($message);
Log::debug($message);
// check it again:
$this->inspectJournal($journal);
}
private function shouldBeDeposit(string $transactionType, string $sourceType, string $destinationType): bool
{
return
TransactionTypeEnum::TRANSFER->value === $transactionType
&& $this->isLiability($sourceType)
&& AccountTypeEnum::ASSET->value === $destinationType;
}
private function makeDeposit(TransactionJournal $journal): void
{
// from a liability to an asset should be a deposit.
@@ -316,14 +357,6 @@ class CorrectsAccountTypes extends Command
$this->inspectJournal($journal);
}
private function shouldGoToExpenseAccount(string $transactionType, string $sourceType, string $destinationType): bool
{
return
TransactionTypeEnum::WITHDRAWAL->value === $transactionType
&& AccountTypeEnum::ASSET->value === $sourceType
&& AccountTypeEnum::REVENUE->value === $destinationType;
}
private function makeExpenseDestination(TransactionJournal $journal, Transaction $destination): void
{
// withdrawals with a revenue account as destination instead of an expense account.
@@ -345,14 +378,6 @@ class CorrectsAccountTypes extends Command
$this->inspectJournal($journal);
}
private function shouldComeFromRevenueAccount(string $transactionType, string $sourceType, string $destinationType): bool
{
return
TransactionTypeEnum::DEPOSIT->value === $transactionType
&& AccountTypeEnum::EXPENSE->value === $sourceType
&& AccountTypeEnum::ASSET->value === $destinationType;
}
private function makeRevenueSource(TransactionJournal $journal, Transaction $source): void
{
// deposits with an expense account as source instead of a revenue account.
@@ -375,78 +400,53 @@ class CorrectsAccountTypes extends Command
$this->inspectJournal($journal);
}
/**
* Can only create revenue accounts out of the blue.
*/
private function canCreateSource(array $validSources): bool
private function makeTransfer(TransactionJournal $journal): void
{
return in_array(AccountTypeEnum::REVENUE->value, $validSources, true);
}
private function hasValidAccountType(array $validTypes, string $accountType): bool
{
return in_array($accountType, $validTypes, true);
}
private function giveNewRevenue(TransactionJournal $journal, Transaction $source): void
{
Log::debug(sprintf('An account of type "%s" could be a valid source.', AccountTypeEnum::REVENUE->value));
$this->factory->setUser($journal->user);
$name = $source->account->name;
$newSource = $this->factory->findOrCreate($name, AccountTypeEnum::REVENUE->value);
$source->account()->associate($newSource);
$source->save();
$this->friendlyPositive(sprintf(
'Firefly III gave transaction #%d a new source %s: #%d ("%s").',
$journal->transaction_group_id,
AccountTypeEnum::REVENUE->value,
$newSource->id,
$newSource->name
));
Log::debug(sprintf('Associated account #%d with transaction #%d', $newSource->id, $source->id));
$this->inspectJournal($journal);
}
private function canCreateDestination(array $validDestinations): bool
{
return in_array(AccountTypeEnum::EXPENSE->value, $validDestinations, true);
}
private function giveNewExpense(TransactionJournal $journal, Transaction $destination): void
{
Log::debug(sprintf('An account of type "%s" could be a valid destination.', AccountTypeEnum::EXPENSE->value));
$this->factory->setUser($journal->user);
$name = $destination->account->name;
$newDestination = $this->factory->findOrCreate($name, AccountTypeEnum::EXPENSE->value);
$destination->account()->associate($newDestination);
$destination->save();
$this->friendlyPositive(sprintf(
'Firefly III gave transaction #%d a new destination %s: #%d ("%s").',
$journal->transaction_group_id,
AccountTypeEnum::EXPENSE->value,
$newDestination->id,
$newDestination->name
));
Log::debug(sprintf('Associated account #%d with transaction #%d', $newDestination->id, $destination->id));
$this->inspectJournal($journal);
}
private function giveNewDestinationAccount(TransactionJournal $journal, Account $newDestination): void
{
$destTransaction = $this->getDestinationTransaction($journal);
$oldDest = $destTransaction->account;
$destTransaction->account_id = $newDestination->id;
$destTransaction->save();
$message = sprintf(
'Transaction journal #%d, destination account changed from #%d ("%s") to #%d ("%s").',
$journal->id,
$oldDest->id,
$oldDest->name,
$newDestination->id,
$newDestination->name
);
// from an asset to a liability should be a withdrawal:
$withdrawal = TransactionType::whereType(TransactionTypeEnum::WITHDRAWAL->value)->first();
$journal->transactionType()->associate($withdrawal);
$journal->save();
$message = sprintf('Converted transaction #%d from a transfer to a withdrawal.', $journal->id);
$this->friendlyInfo($message);
$journal->refresh();
Log::debug($message);
// check it again:
$this->inspectJournal($journal);
}
private function shouldBeDeposit(string $transactionType, string $sourceType, string $destinationType): bool
{
return
TransactionTypeEnum::TRANSFER->value === $transactionType
&& $this->isLiability($sourceType)
&& AccountTypeEnum::ASSET->value === $destinationType;
}
private function shouldBeTransfer(string $transactionType, string $sourceType, string $destinationType): bool
{
return
TransactionTypeEnum::TRANSFER->value === $transactionType
&& AccountTypeEnum::ASSET->value === $sourceType
&& $this->isLiability($destinationType);
}
private function shouldComeFromRevenueAccount(string $transactionType, string $sourceType, string $destinationType): bool
{
return
TransactionTypeEnum::DEPOSIT->value === $transactionType
&& AccountTypeEnum::EXPENSE->value === $sourceType
&& AccountTypeEnum::ASSET->value === $destinationType;
}
private function shouldGoToExpenseAccount(string $transactionType, string $sourceType, string $destinationType): bool
{
return
TransactionTypeEnum::WITHDRAWAL->value === $transactionType
&& AccountTypeEnum::ASSET->value === $sourceType
&& AccountTypeEnum::REVENUE->value === $destinationType;
}
private function stupidLaravel(): void
{
$this->count = 0;
}
}

View File

@@ -38,6 +38,8 @@ use FireflyIII\Models\Transaction;
use FireflyIII\Models\TransactionJournal;
use FireflyIII\Models\TransactionType;
use FireflyIII\Repositories\Account\AccountRepositoryInterface;
use FireflyIII\Services\Internal\Destroy\GenericDestroyService;
use FireflyIII\Services\Internal\Destroy\JournalDestroyService;
use FireflyIII\Support\Facades\Amount;
use FireflyIII\Support\Facades\Steam;
use Illuminate\Console\Command;
@@ -51,9 +53,13 @@ class CorrectsAmounts extends Command
protected $description = 'This command makes sure positive and negative amounts are recorded correctly.';
protected $signature = 'correction:amounts';
private JournalDestroyService $service;
private GenericDestroyService $genericService;
public function handle(): int
{
$this->service = new JournalDestroyService();
$this->genericService = new GenericDestroyService();
// transfers must not have foreign currency info if both accounts have the same currency.
$this->correctTransfers();
// auto budgets must be positive
@@ -177,8 +183,7 @@ class CorrectsAmounts extends Command
private function deleteJournal(TransactionJournal $journal): void
{
$journal->transactionGroup?->delete();
$journal->delete();
$this->service->destroy($journal);
}
private function fixAutoBudgets(): void
@@ -248,6 +253,33 @@ class CorrectsAmounts extends Command
$this->friendlyInfo(sprintf('Corrected %d recurring transaction amount(s).', $count));
}
private function fixRuleTrigger(RuleTrigger $item): bool
{
try {
$check = bccomp((string) $item->trigger_value, '0');
} catch (ValueError) {
$this->friendlyError(sprintf(
'Rule #%d contained invalid %s-trigger "%s". The trigger has been removed, and the rule is disabled.',
$item->rule_id,
$item->trigger_type,
$item->trigger_value
));
$item->rule->active = false;
$item->rule->save();
$this->genericService->deleteRuleTrigger($item);
return false;
}
if (-1 === $check) {
$item->trigger_value = Steam::positive($item->trigger_value);
$item->save();
return true;
}
return false;
}
/**
* Foreach loop is unavoidable here.
*/
@@ -269,33 +301,6 @@ class CorrectsAmounts extends Command
$this->friendlyInfo(sprintf('Corrected %d rule trigger amount(s).', $fixed));
}
private function fixRuleTrigger(RuleTrigger $item): bool
{
try {
$check = bccomp((string) $item->trigger_value, '0');
} catch (ValueError) {
$this->friendlyError(sprintf(
'Rule #%d contained invalid %s-trigger "%s". The trigger has been removed, and the rule is disabled.',
$item->rule_id,
$item->trigger_type,
$item->trigger_value
));
$item->rule->active = false;
$item->rule->save();
$item->forceDelete();
return false;
}
if (-1 === $check) {
$item->trigger_value = Steam::positive($item->trigger_value);
$item->save();
return true;
}
return false;
}
private function validateJournal(TransactionJournal $journal): bool
{
$countSource = $journal->transactions()->where('amount', '<', 0)->count();

View File

@@ -25,8 +25,10 @@ declare(strict_types=1);
namespace FireflyIII\Console\Commands\Correction;
use FireflyIII\Console\Commands\ShowsFriendlyMessages;
use FireflyIII\Events\UpdatedTransactionGroup;
use FireflyIII\Handlers\Events\UpdatedGroupEventHandler;
use FireflyIII\Events\Model\TransactionGroup\TransactionGroupEventFlags;
use FireflyIII\Events\Model\TransactionGroup\TransactionGroupEventObjects;
use FireflyIII\Events\Model\TransactionGroup\UpdatedSingleTransactionGroup;
use FireflyIII\Events\Model\Webhook\WebhookMessagesRequestSending;
use FireflyIII\Models\TransactionGroup;
use FireflyIII\Models\TransactionJournal;
use Illuminate\Console\Command;
@@ -44,8 +46,8 @@ class CorrectsGroupAccounts extends Command
*/
public function handle(): int
{
$groups = [];
$res = TransactionJournal::groupBy('transaction_group_id')->get(['transaction_group_id', DB::raw('COUNT(transaction_group_id) as the_count')]);
$groups = [];
$res = TransactionJournal::groupBy('transaction_group_id')->get(['transaction_group_id', DB::raw('COUNT(transaction_group_id) as the_count')]);
/** @var TransactionJournal $journal */
foreach ($res as $journal) {
@@ -53,14 +55,17 @@ class CorrectsGroupAccounts extends Command
$groups[] = (int) $journal->transaction_group_id;
}
}
$handler = new UpdatedGroupEventHandler();
$flags = new TransactionGroupEventFlags();
$flags->applyRules = true;
$flags->fireWebhooks = true;
$flags->recalculateCredit = true;
$objects = new TransactionGroupEventObjects();
foreach ($groups as $groupId) {
$group = TransactionGroup::find($groupId);
// TODO in theory the "unifyAccounts" method could lead to the need for run recalculations.
// FIXME needs to be a collection.
$event = new UpdatedTransactionGroup($group, true, true, false);
$handler->unifyAccounts($event);
$objects->appendFromTransactionGroup($group);
}
event(new UpdatedSingleTransactionGroup($flags, $objects));
event(new WebhookMessagesRequestSending());
return 0;
}

View File

@@ -51,33 +51,6 @@ class CorrectsIbans extends Command
return 0;
}
private function filterIbans(Collection $accounts): void
{
/** @var Account $account */
foreach ($accounts as $account) {
$iban = (string) $account->iban;
$newIban = Steam::filterSpaces($iban);
if ('' !== $iban && $iban !== $newIban) {
$account->iban = $newIban;
$account->save();
$this->friendlyInfo(sprintf('Removed spaces from IBAN of account #%d', $account->id));
++$this->count;
}
// same for account number:
$accountNumber = $account->accountMeta->where('name', 'account_number')->first();
if (null !== $accountNumber) {
$number = (string) $accountNumber->value;
$newNumber = Steam::filterSpaces($number);
if ('' !== $number && $number !== $newNumber) {
$accountNumber->value = $newNumber;
$accountNumber->save();
$this->friendlyInfo(sprintf('Removed spaces from account number of account #%d', $account->id));
++$this->count;
}
}
}
}
private function countAndCorrectIbans(Collection $accounts): void
{
$set = [];
@@ -119,4 +92,31 @@ class CorrectsIbans extends Command
}
}
}
private function filterIbans(Collection $accounts): void
{
/** @var Account $account */
foreach ($accounts as $account) {
$iban = (string) $account->iban;
$newIban = Steam::filterSpaces($iban);
if ('' !== $iban && $iban !== $newIban) {
$account->iban = $newIban;
$account->save();
$this->friendlyInfo(sprintf('Removed spaces from IBAN of account #%d', $account->id));
++$this->count;
}
// same for account number:
$accountNumber = $account->accountMeta->where('name', 'account_number')->first();
if (null !== $accountNumber) {
$number = (string) $accountNumber->value;
$newNumber = Steam::filterSpaces($number);
if ('' !== $number && $number !== $newNumber) {
$accountNumber->value = $newNumber;
$accountNumber->save();
$this->friendlyInfo(sprintf('Removed spaces from account number of account #%d', $account->id));
++$this->count;
}
}
}
}
}

View File

@@ -68,6 +68,11 @@ class CorrectsInvertedBudgetLimits extends Command
$budgetLimit->end_date = $start;
$budgetLimit->saveQuietly();
}
if ($set->count() > 0) {
// FIXME here be a available budget event.
}
if (1 === $set->count()) {
$this->friendlyInfo('Corrected one budget limit to have the right start/end dates.');

View File

@@ -65,16 +65,6 @@ class CorrectsOpeningBalanceCurrencies extends Command
return 0;
}
private function getJournals(): Collection
{
/** @var Collection */
return TransactionJournal::leftJoin('transaction_types', 'transaction_types.id', '=', 'transaction_journals.transaction_type_id')
->whereNull('transaction_journals.deleted_at')
->where('transaction_types.type', TransactionTypeEnum::OPENING_BALANCE->value)
->get(['transaction_journals.*'])
;
}
private function correctJournal(TransactionJournal $journal): int
{
// get the asset account for this opening balance:
@@ -107,6 +97,25 @@ class CorrectsOpeningBalanceCurrencies extends Command
return null;
}
private function getCurrency(Account $account): TransactionCurrency
{
/** @var AccountRepositoryInterface $repos */
$repos = app(AccountRepositoryInterface::class);
$repos->setUser($account->user);
return $repos->getAccountCurrency($account) ?? Amount::getPrimaryCurrencyByUserGroup($account->userGroup);
}
private function getJournals(): Collection
{
/** @var Collection */
return TransactionJournal::leftJoin('transaction_types', 'transaction_types.id', '=', 'transaction_journals.transaction_type_id')
->whereNull('transaction_journals.deleted_at')
->where('transaction_types.type', TransactionTypeEnum::OPENING_BALANCE->value)
->get(['transaction_journals.*'])
;
}
private function setCorrectCurrency(Account $account, TransactionJournal $journal): int
{
$currency = $this->getCurrency($account);
@@ -128,13 +137,4 @@ class CorrectsOpeningBalanceCurrencies extends Command
return $count;
}
private function getCurrency(Account $account): TransactionCurrency
{
/** @var AccountRepositoryInterface $repos */
$repos = app(AccountRepositoryInterface::class);
$repos->setUser($account->user);
return $repos->getAccountCurrency($account) ?? Amount::getPrimaryCurrencyByUserGroup($account->userGroup);
}
}

View File

@@ -25,28 +25,9 @@ declare(strict_types=1);
namespace FireflyIII\Console\Commands\Correction;
use FireflyIII\Console\Commands\ShowsFriendlyMessages;
use FireflyIII\Handlers\Observer\TransactionObserver;
use FireflyIII\Models\Account;
use FireflyIII\Models\AutoBudget;
use FireflyIII\Models\AvailableBudget;
use FireflyIII\Models\Bill;
use FireflyIII\Models\Budget;
use FireflyIII\Models\BudgetLimit;
use FireflyIII\Models\PiggyBank;
use FireflyIII\Models\PiggyBankEvent;
use FireflyIII\Models\Transaction;
use FireflyIII\Models\TransactionCurrency;
use FireflyIII\Models\UserGroup;
use FireflyIII\Repositories\PiggyBank\PiggyBankRepositoryInterface;
use FireflyIII\Repositories\UserGroup\UserGroupRepositoryInterface;
use FireflyIII\Support\Facades\Amount;
use FireflyIII\Services\Internal\Recalculate\PrimaryAmountRecalculationService;
use FireflyIII\Support\Facades\FireflyConfig;
use FireflyIII\Support\Facades\Preferences;
use FireflyIII\Support\Http\Api\ExchangeRateConverter;
use Illuminate\Console\Command;
use Illuminate\Database\Eloquent\Builder as EloquentBuilder;
use Illuminate\Database\Query\Builder as DatabaseBuilder;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
class CorrectsPrimaryCurrencyAmounts extends Command
@@ -70,186 +51,11 @@ class CorrectsPrimaryCurrencyAmounts extends Command
Log::debug('Will update all primary currency amounts. This may take some time.');
$this->friendlyWarning('Recalculating primary currency amounts for all objects. This may take some time!');
/** @var UserGroupRepositoryInterface $repository */
$repository = app(UserGroupRepositoryInterface::class);
$calculator = new PrimaryAmountRecalculationService();
$calculator->recalculate();
Preferences::mark();
/** @var UserGroup $userGroup */
foreach ($repository->getAll() as $userGroup) {
$this->recalculateForGroup($userGroup);
}
$this->friendlyInfo('Recalculated all primary currency amounts.');
return 0;
}
private function recalculateForGroup(UserGroup $userGroup): void
{
Log::debug(sprintf('Now recalculating for user group #%d', $userGroup->id));
$this->recalculateAccounts($userGroup);
// do a check with the group's currency so we can skip some stuff.
$currency = Amount::getPrimaryCurrencyByUserGroup($userGroup);
$this->recalculatePiggyBanks($userGroup, $currency);
$this->recalculateBudgets($userGroup, $currency);
$this->recalculateAvailableBudgets($userGroup, $currency);
$this->recalculateBills($userGroup, $currency);
$this->calculateTransactions($userGroup, $currency);
}
private function recalculateAccounts(UserGroup $userGroup): void
{
$set = $userGroup
->accounts()
->where(static function (EloquentBuilder $q): void {
$q->whereNotNull('virtual_balance');
// this needs a different piece of code for postgres.
if ('pgsql' === config('database.default')) {
$q->orWhere(DB::raw('CAST(virtual_balance AS TEXT)'), '!=', '');
}
if ('pgsql' !== config('database.default')) {
$q->orWhere('virtual_balance', '!=', '');
}
})
->get()
;
/** @var Account $account */
foreach ($set as $account) {
$account->touch();
}
Log::debug(sprintf('Recalculated %d accounts for user group #%d.', $set->count(), $userGroup->id));
}
private function recalculatePiggyBanks(UserGroup $userGroup, TransactionCurrency $currency): void
{
$converter = new ExchangeRateConverter();
$converter->setUserGroup($userGroup);
$converter->setIgnoreSettings(true);
$repository = app(PiggyBankRepositoryInterface::class);
$repository->setUserGroup($userGroup);
$set = $repository->getPiggyBanks();
$set = $set->filter(static fn (PiggyBank $piggyBank): bool => $currency->id !== $piggyBank->transaction_currency_id);
foreach ($set as $piggyBank) {
$piggyBank->encrypted = false;
$piggyBank->save();
foreach ($piggyBank->accounts as $account) {
$account->pivot->native_current_amount = null;
if (0 !== bccomp((string) $account->pivot->current_amount, '0')) {
$account->pivot->native_current_amount = $converter->convert(
$piggyBank->transactionCurrency,
$currency,
today(),
(string) $account->pivot->current_amount
);
}
$account->pivot->save();
}
$this->recalculatePiggyBankEvents($piggyBank);
}
Log::debug(sprintf('Recalculated %d piggy banks for user group #%d.', $set->count(), $userGroup->id));
}
private function recalculatePiggyBankEvents(PiggyBank $piggyBank): void
{
$set = $piggyBank->piggyBankEvents()->get();
$set->each(static function (PiggyBankEvent $event): void { // @phpstan-ignore-line
$event->touch();
});
Log::debug(sprintf('Recalculated %d piggy bank events.', $set->count()));
}
private function recalculateBudgets(UserGroup $userGroup, TransactionCurrency $currency): void
{
$set = $userGroup->budgets()->get();
/** @var Budget $budget */
foreach ($set as $budget) {
$this->recalculateBudgetLimits($budget, $currency);
$this->recalculateAutoBudgets($budget, $currency);
}
Log::debug(sprintf('Recalculated %d budgets.', $set->count()));
}
private function recalculateBudgetLimits(Budget $budget, TransactionCurrency $currency): void
{
$set = $budget->budgetlimits()->where('transaction_currency_id', '!=', $currency->id)->get();
/** @var BudgetLimit $limit */
foreach ($set as $limit) {
Log::debug(sprintf('Will now touch BL #%d', $limit->id));
$limit->touch();
Log::debug(sprintf('Done with touch BL #%d', $limit->id));
}
Log::debug(sprintf('Recalculated %d budget limits for budget #%d.', $set->count(), $budget->id));
}
private function recalculateAutoBudgets(Budget $budget, TransactionCurrency $currency): void
{
$set = $budget->autoBudgets()->where('transaction_currency_id', '!=', $currency->id)->get();
/** @var AutoBudget $autoBudget */
foreach ($set as $autoBudget) {
$autoBudget->touch();
}
Log::debug(sprintf('Recalculated %d auto budgets for budget #%d.', $set->count(), $budget->id));
}
private function recalculateAvailableBudgets(UserGroup $userGroup, TransactionCurrency $currency): void
{
Log::debug('Start with available budgets.');
$set = $userGroup->availableBudgets()->where('transaction_currency_id', '!=', $currency->id)->get();
/** @var AvailableBudget $budget */
foreach ($set as $budget) {
$budget->touch();
}
Log::debug(sprintf('Recalculated %d available budgets.', $set->count()));
}
private function recalculateBills(UserGroup $userGroup, TransactionCurrency $currency): void
{
$set = $userGroup->bills()->where('transaction_currency_id', '!=', $currency->id)->get();
/** @var Bill $bill */
foreach ($set as $bill) {
$bill->touch();
}
Log::debug(sprintf('Recalculated %d bills.', $set->count()));
}
private function calculateTransactions(UserGroup $userGroup, TransactionCurrency $currency): void
{
// custom query because of the potential size of this update.
$set = DB::table('transactions')
->join('transaction_journals', 'transaction_journals.id', '=', 'transactions.transaction_journal_id')
->where('transaction_journals.user_group_id', $userGroup->id)
->where(static function (DatabaseBuilder $q1) use ($currency): void {
$q1->where(static function (DatabaseBuilder $q2) use ($currency): void {
$q2->whereNot('transactions.transaction_currency_id', $currency->id)->whereNull('transactions.foreign_currency_id');
})->orWhere(static function (DatabaseBuilder $q3) use ($currency): void {
$q3->whereNot('transactions.transaction_currency_id', $currency->id)->whereNot('transactions.foreign_currency_id', $currency->id);
});
})
// ->where(static function (DatabaseBuilder $q) use ($currency): void {
// $q->whereNot('transactions.transaction_currency_id', $currency->id)
// ->whereNot('transactions.foreign_currency_id', $currency->id)
// ;
// })
->get(['transactions.id'])
;
TransactionObserver::$recalculate = false;
foreach ($set as $item) {
// here we are.
/** @var null|Transaction $transaction */
$transaction = Transaction::find($item->id);
$transaction?->touch();
}
TransactionObserver::$recalculate = true;
Log::debug(sprintf('Recalculated %d transactions.', $set->count()));
}
}

View File

@@ -54,17 +54,6 @@ class CorrectsRecurringTransactions extends Command
return 0;
}
/**
* Laravel will execute ALL __construct() methods for ALL commands whenever a SINGLE command is
* executed. This leads to noticeable slow-downs and class calls. To prevent this, this method should
* be called from the handle method instead of using the constructor to initialize the command.
*/
private function stupidLaravel(): void
{
$this->recurringRepos = app(RecurringRepositoryInterface::class);
$this->userRepos = app(UserRepositoryInterface::class);
}
private function correctTransactions(): void
{
$users = $this->userRepos->all();
@@ -75,17 +64,6 @@ class CorrectsRecurringTransactions extends Command
}
}
private function processUser(User $user): void
{
$this->recurringRepos->setUser($user);
$recurrences = $this->recurringRepos->get();
/** @var Recurrence $recurrence */
foreach ($recurrences as $recurrence) {
$this->processRecurrence($recurrence);
}
}
private function processRecurrence(Recurrence $recurrence): void
{
/** @var RecurrenceTransaction $transaction */
@@ -115,4 +93,26 @@ class CorrectsRecurringTransactions extends Command
}
}
}
private function processUser(User $user): void
{
$this->recurringRepos->setUser($user);
$recurrences = $this->recurringRepos->get();
/** @var Recurrence $recurrence */
foreach ($recurrences as $recurrence) {
$this->processRecurrence($recurrence);
}
}
/**
* Laravel will execute ALL __construct() methods for ALL commands whenever a SINGLE command is
* executed. This leads to noticeable slow-downs and class calls. To prevent this, this method should
* be called from the handle method instead of using the constructor to initialize the command.
*/
private function stupidLaravel(): void
{
$this->recurringRepos = app(RecurringRepositoryInterface::class);
$this->userRepos = app(UserRepositoryInterface::class);
}
}

View File

@@ -25,7 +25,6 @@ declare(strict_types=1);
namespace FireflyIII\Console\Commands\Correction;
use FireflyIII\Console\Commands\ShowsFriendlyMessages;
use FireflyIII\Models\AccountBalance;
use FireflyIII\Models\AvailableBudget;
use FireflyIII\Models\Bill;
use FireflyIII\Models\BudgetLimit;
@@ -47,7 +46,6 @@ class CorrectsTimezoneInformation extends Command
use ShowsFriendlyMessages;
public static array $models = [
AccountBalance::class => ['date'], // done
AvailableBudget::class => ['start_date', 'end_date'], // done
Bill::class => ['date', 'end_date', 'extension_date'], // done
BudgetLimit::class => ['start_date', 'end_date'], // done

View File

@@ -67,6 +67,15 @@ class CorrectsTransactionTypes extends Command
return 0;
}
private function changeJournal(TransactionJournal $journal, string $expectedType): void
{
$type = TransactionType::whereType($expectedType)->first();
if (null !== $type) {
$journal->transaction_type_id = $type->id;
$journal->save();
}
}
/**
* Collect all transaction journals.
*/
@@ -105,31 +114,6 @@ class CorrectsTransactionTypes extends Command
return false;
}
/**
* @throws FireflyException
*/
private function getSourceAccount(TransactionJournal $journal): Account
{
$collection = $journal->transactions->filter(static fn (Transaction $transaction): bool => $transaction->amount < 0);
if (0 === $collection->count()) {
throw new FireflyException(sprintf('300001: Journal #%d has no source transaction.', $journal->id));
}
if (1 !== $collection->count()) {
throw new FireflyException(sprintf('300002: Journal #%d has multiple source transactions.', $journal->id));
}
/** @var Transaction $transaction */
$transaction = $collection->first();
/** @var null|Account $account */
$account = $transaction->account;
if (null === $account) {
throw new FireflyException(sprintf('300003: Journal #%d, transaction #%d has no source account.', $journal->id, $transaction->id));
}
return $account;
}
/**
* @throws FireflyException
*/
@@ -155,12 +139,28 @@ class CorrectsTransactionTypes extends Command
return $account;
}
private function changeJournal(TransactionJournal $journal, string $expectedType): void
/**
* @throws FireflyException
*/
private function getSourceAccount(TransactionJournal $journal): Account
{
$type = TransactionType::whereType($expectedType)->first();
if (null !== $type) {
$journal->transaction_type_id = $type->id;
$journal->save();
$collection = $journal->transactions->filter(static fn (Transaction $transaction): bool => $transaction->amount < 0);
if (0 === $collection->count()) {
throw new FireflyException(sprintf('300001: Journal #%d has no source transaction.', $journal->id));
}
if (1 !== $collection->count()) {
throw new FireflyException(sprintf('300002: Journal #%d has multiple source transactions.', $journal->id));
}
/** @var Transaction $transaction */
$transaction = $collection->first();
/** @var null|Account $account */
$account = $transaction->account;
if (null === $account) {
throw new FireflyException(sprintf('300003: Journal #%d, transaction #%d has no source account.', $journal->id, $transaction->id));
}
return $account;
}
}

View File

@@ -70,271 +70,6 @@ class CorrectsUnevenAmount extends Command
return 0;
}
private function convertOldStyleTransfers(): void
{
Log::debug('convertOldStyleTransfers()');
// select transactions with a foreign amount and a foreign currency. and it's a transfer. and they are different.
$transactions = Transaction::distinct()
->leftJoin('transaction_journals', 'transaction_journals.id', 'transactions.transaction_journal_id')
->leftJoin('transaction_types', 'transaction_types.id', 'transaction_journals.transaction_type_id')
->where('transaction_types.type', TransactionTypeEnum::TRANSFER->value)
->whereNotNull('foreign_currency_id')
->whereNotNull('foreign_amount')
->get(['transactions.transaction_journal_id'])
;
$count = 0;
/** @var Transaction $transaction */
foreach ($transactions as $transaction) {
/** @var null|TransactionJournal $journal */
$journal = TransactionJournal::find($transaction->transaction_journal_id);
if (null === $journal) {
Log::debug('Found no journal, continue.');
continue;
}
// needs to be a transfer.
if (TransactionTypeEnum::TRANSFER->value !== $journal->transactionType->type) {
Log::debug('Must be a transfer, continue.');
continue;
}
/** @var null|Transaction $destination */
$destination = $journal->transactions()->where('amount', '>', 0)->first();
/** @var null|Transaction $source */
$source = $journal->transactions()->where('amount', '<', 0)->first();
if (null === $destination || null === $source) {
Log::debug('Source or destination transaction is NULL, continue.');
// will be picked up later.
continue;
}
if ($source->transaction_currency_id === $destination->transaction_currency_id) {
Log::debug('Ready to swap data between transactions.');
$destination->foreign_currency_id = $source->transaction_currency_id;
$destination->foreign_amount = Steam::positive($source->amount);
$destination->transaction_currency_id = $source->foreign_currency_id;
$destination->amount = Steam::positive($source->foreign_amount);
$destination->balance_dirty = true;
$source->balance_dirty = true;
$destination->save();
$source->save();
$this->friendlyWarning(sprintf('Corrected foreign amounts of transfer #%d.', $journal->id));
++$count;
}
}
if (0 === $count) {
return;
}
$this->friendlyPositive(sprintf('Fixed %d transfer(s) with unbalanced amounts.', $count));
}
private function fixUnevenAmounts(): void
{
Log::debug('fixUnevenAmounts()');
$journals = DB::table('transactions')
->groupBy('transaction_journal_id')
->whereNull('deleted_at')
->get(['transaction_journal_id', DB::raw('SUM(amount) AS the_sum')])
;
/** @var stdClass $entry */
foreach ($journals as $entry) {
$sum = (string) $entry->the_sum;
$sum = Steam::floatalize($sum);
if (
!is_numeric($sum)
|| '' === $sum // @phpstan-ignore-line
|| str_contains($sum, 'e')
|| str_contains($sum, ',')
) {
$message = sprintf('Journal #%d has an invalid sum ("%s"). No sure what to do.', $entry->transaction_journal_id, $entry->the_sum);
$this->friendlyWarning($message);
Log::warning($message);
++$this->count;
continue;
}
$res = -1;
try {
$res = bccomp($sum, '0');
} catch (ValueError $e) {
$this->friendlyError(sprintf('Could not bccomp("%s", "0").', $sum));
Log::error($e->getMessage());
Log::error($e->getTraceAsString());
}
if (0 !== $res) {
$this->fixJournal((int) $entry->transaction_journal_id);
}
}
}
private function fixJournal(int $param): void
{
// one of the transactions is bad.
$journal = TransactionJournal::find($param);
if (null === $journal) {
return;
}
/** @var null|Transaction $source */
$source = $journal->transactions()->where('amount', '<', 0)->first();
if (null === $source) {
$this->friendlyError(sprintf(
'Journal #%d ("%s") has no source transaction. It will be deleted to maintain database consistency.',
$journal->id ?? 0,
$journal->description ?? ''
));
Transaction::where('transaction_journal_id', $journal->id ?? 0)->forceDelete();
TransactionJournal::where('id', $journal->id ?? 0)->forceDelete();
++$this->count;
return;
}
$amount = bcmul('-1', (string) $source->amount);
// fix amount of destination:
/** @var null|Transaction $destination */
$destination = $journal->transactions()->where('amount', '>', 0)->first();
if (null === $destination) {
$this->friendlyError(sprintf(
'Journal #%d ("%s") has no destination transaction. It will be deleted to maintain database consistency.',
$journal->id ?? 0,
$journal->description ?? ''
));
Transaction::where('transaction_journal_id', $journal->id ?? 0)->forceDelete();
TransactionJournal::where('id', $journal->id ?? 0)->forceDelete();
++$this->count;
return;
}
// may still be able to salvage this journal if it is a transfer with foreign currency info
if ($this->isForeignCurrencyTransfer($journal) || $this->isBetweenAssetAndLiability($journal)) {
Log::debug(sprintf('Can skip foreign currency transfer / asset+liability transaction #%d.', $journal->id));
return;
}
$message = sprintf('Sum of journal #%d is not zero, journal is broken and now fixed.', $journal->id);
$this->friendlyWarning($message);
Log::warning($message);
$destination->amount = $amount;
$destination->save();
$message = sprintf('Corrected amount in transaction journal #%d', $param);
$this->friendlyInfo($message);
++$this->count;
}
private function isForeignCurrencyTransfer(TransactionJournal $journal): bool
{
if (TransactionTypeEnum::TRANSFER->value !== $journal->transactionType->type) {
return false;
}
/** @var Transaction $destination */
$destination = $journal->transactions()->where('amount', '>', 0)->first();
/** @var Transaction $source */
$source = $journal->transactions()->where('amount', '<', 0)->first();
// safety catch on NULL should not be necessary, we just had that catch.
// source amount = dest foreign amount
// source currency = dest foreign currency
// dest amount = source foreign currency
// dest currency = source foreign currency
// Log::debug(sprintf('[a] %s', bccomp(\FireflyIII\Support\Facades\Steam::positive($source->amount), \FireflyIII\Support\Facades\Steam::positive($destination->foreign_amount))));
// Log::debug(sprintf('[b] %s', bccomp(\FireflyIII\Support\Facades\Steam::positive($destination->amount), \FireflyIII\Support\Facades\Steam::positive($source->foreign_amount))));
// Log::debug(sprintf('[c] %s', var_export($source->transaction_currency_id === $destination->foreign_currency_id,true)));
// Log::debug(sprintf('[d] %s', var_export((int) $destination->transaction_currency_id ===(int) $source->foreign_currency_id, true)));
return
0 === bccomp(Steam::positive($source->amount), Steam::positive($destination->foreign_amount))
&& $source->transaction_currency_id === $destination->foreign_currency_id
&& 0 === bccomp(Steam::positive($destination->amount), Steam::positive($source->foreign_amount))
&& (int) $destination->transaction_currency_id === (int) $source->foreign_currency_id;
}
private function matchCurrencies(): void
{
$journals = TransactionJournal::leftJoin('transactions', 'transaction_journals.id', 'transactions.transaction_journal_id')->where(
'transactions.transaction_currency_id',
'!=',
DB::raw('transaction_journals.transaction_currency_id')
)->get(['transaction_journals.*']);
$count = 0;
/** @var TransactionJournal $journal */
foreach ($journals as $journal) {
if (!$this->isForeignCurrencyTransfer($journal) && !$this->isBetweenAssetAndLiability($journal)) {
Transaction::where('transaction_journal_id', $journal->id)->update(['transaction_currency_id' => $journal->transaction_currency_id]);
++$count;
continue;
}
Log::debug(sprintf('Can skip foreign currency transfer or transaction between asset and liability #%d.', $journal->id));
}
if (0 === $count) {
return;
}
$this->friendlyPositive(sprintf('Fixed %d journal(s) with mismatched currencies.', $journals->count()));
}
private function isBetweenAssetAndLiability(TransactionJournal $journal): bool
{
/** @var null|Transaction $sourceTransaction */
$sourceTransaction = $journal->transactions()->where('amount', '<', 0)->first();
/** @var null|Transaction $destinationTransaction */
$destinationTransaction = $journal->transactions()->where('amount', '>', 0)->first();
if (null === $sourceTransaction || null === $destinationTransaction) {
Log::warning('Either transaction is false, stop.');
return false;
}
if (null === $sourceTransaction->foreign_amount || null === $destinationTransaction->foreign_amount) {
Log::warning('Either foreign amount is false, stop.');
return false;
}
$source = $sourceTransaction->account;
$destination = $destinationTransaction->account;
if (null === $source || null === $destination) {
Log::warning('Either is false, stop.');
return false;
}
$sourceTypes = [AccountTypeEnum::LOAN->value, AccountTypeEnum::DEBT->value, AccountTypeEnum::MORTGAGE->value];
// source is liability, destination is asset
if (in_array($source->accountType->type, $sourceTypes, true) && AccountTypeEnum::ASSET->value === $destination->accountType->type) {
Log::debug('Source is a liability account, destination is an asset account, return TRUE.');
return true;
}
// source is asset, destination is liability
if (in_array($destination->accountType->type, $sourceTypes, true) && AccountTypeEnum::ASSET->value === $source->accountType->type) {
Log::debug('Destination is a liability account, source is an asset account, return TRUE.');
return true;
}
return false;
}
private function convertOldStyleTransactions(): void
{
/** @var AccountRepositoryInterface $repository */
@@ -448,4 +183,269 @@ class CorrectsUnevenAmount extends Command
$this->friendlyPositive(sprintf('Fixed %d journal(s) with unbalanced amounts.', $count));
}
private function convertOldStyleTransfers(): void
{
Log::debug('convertOldStyleTransfers()');
// select transactions with a foreign amount and a foreign currency. and it's a transfer. and they are different.
$transactions = Transaction::distinct()
->leftJoin('transaction_journals', 'transaction_journals.id', 'transactions.transaction_journal_id')
->leftJoin('transaction_types', 'transaction_types.id', 'transaction_journals.transaction_type_id')
->where('transaction_types.type', TransactionTypeEnum::TRANSFER->value)
->whereNotNull('foreign_currency_id')
->whereNotNull('foreign_amount')
->get(['transactions.transaction_journal_id'])
;
$count = 0;
/** @var Transaction $transaction */
foreach ($transactions as $transaction) {
/** @var null|TransactionJournal $journal */
$journal = TransactionJournal::find($transaction->transaction_journal_id);
if (null === $journal) {
Log::debug('Found no journal, continue.');
continue;
}
// needs to be a transfer.
if (TransactionTypeEnum::TRANSFER->value !== $journal->transactionType->type) {
Log::debug('Must be a transfer, continue.');
continue;
}
/** @var null|Transaction $destination */
$destination = $journal->transactions()->where('amount', '>', 0)->first();
/** @var null|Transaction $source */
$source = $journal->transactions()->where('amount', '<', 0)->first();
if (null === $destination || null === $source) {
Log::debug('Source or destination transaction is NULL, continue.');
// will be picked up later.
continue;
}
if ($source->transaction_currency_id === $destination->transaction_currency_id) {
Log::debug('Ready to swap data between transactions.');
$destination->foreign_currency_id = $source->transaction_currency_id;
$destination->foreign_amount = Steam::positive($source->amount);
$destination->transaction_currency_id = $source->foreign_currency_id;
$destination->amount = Steam::positive($source->foreign_amount);
$destination->balance_dirty = true;
$source->balance_dirty = true;
$destination->save();
$source->save();
$this->friendlyWarning(sprintf('Corrected foreign amounts of transfer #%d.', $journal->id));
++$count;
}
}
if (0 === $count) {
return;
}
$this->friendlyPositive(sprintf('Fixed %d transfer(s) with unbalanced amounts.', $count));
}
private function fixJournal(int $param): void
{
// one of the transactions is bad.
$journal = TransactionJournal::find($param);
if (null === $journal) {
return;
}
/** @var null|Transaction $source */
$source = $journal->transactions()->where('amount', '<', 0)->first();
if (null === $source) {
$this->friendlyError(sprintf(
'Journal #%d ("%s") has no source transaction. It will be deleted to maintain database consistency.',
$journal->id ?? 0,
$journal->description ?? ''
));
Transaction::where('transaction_journal_id', $journal->id ?? 0)->forceDelete();
TransactionJournal::where('id', $journal->id ?? 0)->forceDelete();
++$this->count;
return;
}
$amount = bcmul('-1', (string) $source->amount);
// fix amount of destination:
/** @var null|Transaction $destination */
$destination = $journal->transactions()->where('amount', '>', 0)->first();
if (null === $destination) {
$this->friendlyError(sprintf(
'Journal #%d ("%s") has no destination transaction. It will be deleted to maintain database consistency.',
$journal->id ?? 0,
$journal->description ?? ''
));
Transaction::where('transaction_journal_id', $journal->id ?? 0)->forceDelete();
TransactionJournal::where('id', $journal->id ?? 0)->forceDelete();
++$this->count;
return;
}
// may still be able to salvage this journal if it is a transfer with foreign currency info
if ($this->isForeignCurrencyTransfer($journal) || $this->isBetweenAssetAndLiability($journal)) {
Log::debug(sprintf('Can skip foreign currency transfer / asset+liability transaction #%d.', $journal->id));
return;
}
$message = sprintf('Sum of journal #%d is not zero, journal is broken and now fixed.', $journal->id);
$this->friendlyWarning($message);
Log::warning($message);
$destination->amount = $amount;
$destination->save();
$message = sprintf('Corrected amount in transaction journal #%d', $param);
$this->friendlyInfo($message);
++$this->count;
}
private function fixUnevenAmounts(): void
{
Log::debug('fixUnevenAmounts()');
$journals = DB::table('transactions')
->groupBy('transaction_journal_id')
->whereNull('deleted_at')
->get(['transaction_journal_id', DB::raw('SUM(amount) AS the_sum')])
;
/** @var stdClass $entry */
foreach ($journals as $entry) {
$sum = (string) $entry->the_sum;
$sum = Steam::floatalize($sum);
if (
!is_numeric($sum)
|| '' === $sum // @phpstan-ignore-line
|| str_contains($sum, 'e')
|| str_contains($sum, ',')
) {
$message = sprintf('Journal #%d has an invalid sum ("%s"). No sure what to do.', $entry->transaction_journal_id, $entry->the_sum);
$this->friendlyWarning($message);
Log::warning($message);
++$this->count;
continue;
}
$res = -1;
try {
$res = bccomp($sum, '0');
} catch (ValueError $e) {
$this->friendlyError(sprintf('Could not bccomp("%s", "0").', $sum));
Log::error($e->getMessage());
Log::error($e->getTraceAsString());
}
if (0 !== $res) {
$this->fixJournal((int) $entry->transaction_journal_id);
}
}
}
private function isBetweenAssetAndLiability(TransactionJournal $journal): bool
{
/** @var null|Transaction $sourceTransaction */
$sourceTransaction = $journal->transactions()->where('amount', '<', 0)->first();
/** @var null|Transaction $destinationTransaction */
$destinationTransaction = $journal->transactions()->where('amount', '>', 0)->first();
if (null === $sourceTransaction || null === $destinationTransaction) {
Log::warning('Either transaction is false, stop.');
return false;
}
if (null === $sourceTransaction->foreign_amount || null === $destinationTransaction->foreign_amount) {
Log::warning('Either foreign amount is false, stop.');
return false;
}
$source = $sourceTransaction->account;
$destination = $destinationTransaction->account;
if (null === $source || null === $destination) {
Log::warning('Either is false, stop.');
return false;
}
$sourceTypes = [AccountTypeEnum::LOAN->value, AccountTypeEnum::DEBT->value, AccountTypeEnum::MORTGAGE->value];
// source is liability, destination is asset
if (in_array($source->accountType->type, $sourceTypes, true) && AccountTypeEnum::ASSET->value === $destination->accountType->type) {
Log::debug('Source is a liability account, destination is an asset account, return TRUE.');
return true;
}
// source is asset, destination is liability
if (in_array($destination->accountType->type, $sourceTypes, true) && AccountTypeEnum::ASSET->value === $source->accountType->type) {
Log::debug('Destination is a liability account, source is an asset account, return TRUE.');
return true;
}
return false;
}
private function isForeignCurrencyTransfer(TransactionJournal $journal): bool
{
if (TransactionTypeEnum::TRANSFER->value !== $journal->transactionType->type) {
return false;
}
/** @var Transaction $destination */
$destination = $journal->transactions()->where('amount', '>', 0)->first();
/** @var Transaction $source */
$source = $journal->transactions()->where('amount', '<', 0)->first();
// safety catch on NULL should not be necessary, we just had that catch.
// source amount = dest foreign amount
// source currency = dest foreign currency
// dest amount = source foreign currency
// dest currency = source foreign currency
// Log::debug(sprintf('[a] %s', bccomp(\FireflyIII\Support\Facades\Steam::positive($source->amount), \FireflyIII\Support\Facades\Steam::positive($destination->foreign_amount))));
// Log::debug(sprintf('[b] %s', bccomp(\FireflyIII\Support\Facades\Steam::positive($destination->amount), \FireflyIII\Support\Facades\Steam::positive($source->foreign_amount))));
// Log::debug(sprintf('[c] %s', var_export($source->transaction_currency_id === $destination->foreign_currency_id,true)));
// Log::debug(sprintf('[d] %s', var_export((int) $destination->transaction_currency_id ===(int) $source->foreign_currency_id, true)));
return
0 === bccomp(Steam::positive($source->amount), Steam::positive($destination->foreign_amount))
&& $source->transaction_currency_id === $destination->foreign_currency_id
&& 0 === bccomp(Steam::positive($destination->amount), Steam::positive($source->foreign_amount))
&& (int) $destination->transaction_currency_id === (int) $source->foreign_currency_id;
}
private function matchCurrencies(): void
{
$journals = TransactionJournal::leftJoin('transactions', 'transaction_journals.id', 'transactions.transaction_journal_id')->where(
'transactions.transaction_currency_id',
'!=',
DB::raw('transaction_journals.transaction_currency_id')
)->get(['transaction_journals.*']);
$count = 0;
/** @var TransactionJournal $journal */
foreach ($journals as $journal) {
if (!$this->isForeignCurrencyTransfer($journal) && !$this->isBetweenAssetAndLiability($journal)) {
Transaction::where('transaction_journal_id', $journal->id)->update(['transaction_currency_id' => $journal->transaction_currency_id]);
++$count;
continue;
}
Log::debug(sprintf('Can skip foreign currency transfer or transaction between asset and liability #%d.', $journal->id));
}
if (0 === $count) {
return;
}
$this->friendlyPositive(sprintf('Fixed %d journal(s) with mismatched currencies.', $journals->count()));
}
}

View File

@@ -42,31 +42,6 @@ class CreatesGroupMemberships extends Command
protected $description = 'Update group memberships';
protected $signature = 'correction:create-group-memberships';
/**
* Execute the console command.
*
* @throws FireflyException
*/
public function handle(): int
{
$this->createGroupMemberships();
return 0;
}
/**
* @throws FireflyException
*/
private function createGroupMemberships(): void
{
$users = User::get();
/** @var User $user */
foreach ($users as $user) {
self::createGroupMembership($user);
}
}
/**
* TODO move to helper.
*
@@ -98,4 +73,29 @@ class CreatesGroupMemberships extends Command
$user->save();
}
}
/**
* Execute the console command.
*
* @throws FireflyException
*/
public function handle(): int
{
$this->createGroupMemberships();
return 0;
}
/**
* @throws FireflyException
*/
private function createGroupMemberships(): void
{
$users = User::get();
/** @var User $user */
foreach ($users as $user) {
self::createGroupMembership($user);
}
}
}

View File

@@ -51,6 +51,30 @@ class RemovesEmptyJournals extends Command
return 0;
}
private function deleteEmptyJournals(): void
{
$count = 0;
$set = TransactionJournal::leftJoin('transactions', 'transactions.transaction_journal_id', '=', 'transaction_journals.id')
->groupBy('transaction_journals.id')
->whereNull('transactions.transaction_journal_id')
->get(['transaction_journals.id'])
;
foreach ($set as $entry) {
try {
/** @var null|TransactionJournal $journal */
$journal = TransactionJournal::find($entry->id);
$journal?->delete();
} catch (QueryException $e) {
Log::info(sprintf('Could not delete entry: %s', $e->getMessage()));
Log::error($e->getTraceAsString());
}
$this->friendlyInfo(sprintf('Deleted empty transaction journal #%d', $entry->id));
++$count;
}
}
/**
* Delete transactions and their journals if they have an uneven number of transactions.
*/
@@ -85,28 +109,4 @@ class RemovesEmptyJournals extends Command
}
}
}
private function deleteEmptyJournals(): void
{
$count = 0;
$set = TransactionJournal::leftJoin('transactions', 'transactions.transaction_journal_id', '=', 'transaction_journals.id')
->groupBy('transaction_journals.id')
->whereNull('transactions.transaction_journal_id')
->get(['transaction_journals.id'])
;
foreach ($set as $entry) {
try {
/** @var null|TransactionJournal $journal */
$journal = TransactionJournal::find($entry->id);
$journal?->delete();
} catch (QueryException $e) {
Log::info(sprintf('Could not delete entry: %s', $e->getMessage()));
Log::error($e->getTraceAsString());
}
$this->friendlyInfo(sprintf('Deleted empty transaction journal #%d', $entry->id));
++$count;
}
}
}

View File

@@ -95,11 +95,19 @@ class RemovesLinksToDeletedObjects extends Command
$this->friendlyNeutral('Validated links to deleted objects.');
}
private function cleanupTags(array $tags): void
private function cleanupBudgets(array $budgets): void
{
$count = DB::table('tag_transaction_journal')->whereIn('tag_id', $tags)->delete();
$count = DB::table('budget_transaction_journal')->whereIn('budget_id', $budgets)->delete();
if ($count > 0) {
$this->friendlyInfo(sprintf('Removed %d old relationship(s) categories transactions and tags.', $count));
$this->friendlyInfo(sprintf('Removed %d old relationship(s) between budgets and transactions.', $count));
}
}
private function cleanupCategories(array $categories): void
{
$count = DB::table('category_transaction_journal')->whereIn('category_id', $categories)->delete();
if ($count > 0) {
$this->friendlyInfo(sprintf('Removed %d old relationship(s) categories categories and transactions.', $count));
}
}
@@ -127,19 +135,11 @@ class RemovesLinksToDeletedObjects extends Command
}
}
private function cleanupBudgets(array $budgets): void
private function cleanupTags(array $tags): void
{
$count = DB::table('budget_transaction_journal')->whereIn('budget_id', $budgets)->delete();
$count = DB::table('tag_transaction_journal')->whereIn('tag_id', $tags)->delete();
if ($count > 0) {
$this->friendlyInfo(sprintf('Removed %d old relationship(s) between budgets and transactions.', $count));
}
}
private function cleanupCategories(array $categories): void
{
$count = DB::table('category_transaction_journal')->whereIn('category_id', $categories)->delete();
if ($count > 0) {
$this->friendlyInfo(sprintf('Removed %d old relationship(s) categories categories and transactions.', $count));
$this->friendlyInfo(sprintf('Removed %d old relationship(s) categories transactions and tags.', $count));
}
}
}

View File

@@ -56,6 +56,27 @@ class RemovesOrphanedTransactions extends Command
return 0;
}
private function deleteFromOrphanedAccounts(): void
{
$set = Transaction::leftJoin('accounts', 'transactions.account_id', '=', 'accounts.id')->whereNotNull('accounts.deleted_at')->get(['transactions.*']);
$count = 0;
/** @var Transaction $transaction */
foreach ($set as $transaction) {
// delete journals
/** @var null|TransactionJournal $journal */
$journal = TransactionJournal::find($transaction->transaction_journal_id);
$journal?->delete();
Transaction::where('transaction_journal_id', $transaction->transaction_journal_id)->delete();
$this->friendlyWarning(sprintf(
'Deleted transaction journal #%d because account #%d was already deleted.',
$transaction->transaction_journal_id,
$transaction->account_id
));
++$count;
}
}
private function deleteOrphanedJournals(): void
{
$set = TransactionJournal::leftJoin('transaction_groups', 'transaction_journals.transaction_group_id', 'transaction_groups.id')
@@ -111,25 +132,4 @@ class RemovesOrphanedTransactions extends Command
}
}
}
private function deleteFromOrphanedAccounts(): void
{
$set = Transaction::leftJoin('accounts', 'transactions.account_id', '=', 'accounts.id')->whereNotNull('accounts.deleted_at')->get(['transactions.*']);
$count = 0;
/** @var Transaction $transaction */
foreach ($set as $transaction) {
// delete journals
/** @var null|TransactionJournal $journal */
$journal = TransactionJournal::find($transaction->transaction_journal_id);
$journal?->delete();
Transaction::where('transaction_journal_id', $transaction->transaction_journal_id)->delete();
$this->friendlyWarning(sprintf(
'Deleted transaction journal #%d because account #%d was already deleted.',
$transaction->transaction_journal_id,
$transaction->account_id
));
++$count;
}
}
}

View File

@@ -48,6 +48,26 @@ class RestoresOAuthKeys extends Command
return 0;
}
private function generateKeys(): void
{
OAuthKeys::generateKeys();
}
private function keysInDatabase(): bool
{
return OAuthKeys::keysInDatabase();
}
private function keysOnDrive(): bool
{
return OAuthKeys::hasKeyFiles();
}
private function restoreKeysFromDB(): bool
{
return OAuthKeys::restoreKeysFromDB();
}
private function restoreOAuthKeys(): void
{
if (!$this->keysInDatabase() && !$this->keysOnDrive()) {
@@ -76,28 +96,8 @@ class RestoresOAuthKeys extends Command
}
}
private function keysInDatabase(): bool
{
return OAuthKeys::keysInDatabase();
}
private function keysOnDrive(): bool
{
return OAuthKeys::hasKeyFiles();
}
private function generateKeys(): void
{
OAuthKeys::generateKeys();
}
private function storeKeysInDB(): void
{
OAuthKeys::storeKeysInDB();
}
private function restoreKeysFromDB(): bool
{
return OAuthKeys::restoreKeysFromDB();
}
}

View File

@@ -43,6 +43,14 @@ class TriggersCreditCalculation extends Command
return 0;
}
private function processAccount(Account $account): void
{
/** @var CreditRecalculateService $object */
$object = app(CreditRecalculateService::class);
$object->setAccount($account);
$object->recalculate();
}
private function processAccounts(): void
{
$accounts = Account::leftJoin('account_types', 'accounts.account_type_id', 'account_types.id')->whereIn(
@@ -53,12 +61,4 @@ class TriggersCreditCalculation extends Command
$this->processAccount($account);
}
}
private function processAccount(Account $account): void
{
/** @var CreditRecalculateService $object */
$object = app(CreditRecalculateService::class);
$object->setAccount($account);
$object->recalculate();
}
}

View File

@@ -134,45 +134,55 @@ class ExportsData extends Command
}
/**
* Laravel will execute ALL __construct() methods for ALL commands whenever a SINGLE command is
* executed. This leads to noticeable slow-downs and class calls. To prevent this, this method should
* be called from the handle method instead of using the constructor to initialize the command.
* @throws FireflyException
* @throws FilesystemException
*/
private function stupidLaravel(): void
private function exportData(array $options, array $data): void
{
$this->journalRepository = app(JournalRepositoryInterface::class);
$this->accountRepository = app(AccountRepositoryInterface::class);
$date = Carbon::now()->format('Y_m_d');
foreach ($data as $key => $content) {
$file = sprintf('%s%s_%s.csv', $options['directory'], $date, $key);
if (false === $options['force'] && file_exists($file)) {
throw new FireflyException(sprintf('File "%s" exists already. Use --force to overwrite.', $file));
}
if (true === $options['force'] && file_exists($file)) {
$this->friendlyWarning(sprintf('File "%s" exists already but will be replaced.', $file));
}
// continue to write to file.
file_put_contents($file, $content);
$this->friendlyPositive(sprintf('Wrote %s-export to file "%s".', $key, $file));
}
}
/**
* @throws FireflyException
* @throws Exception
*/
private function parseOptions(): array
private function getAccountsParameter(): Collection
{
$start = $this->getDateParameter('start');
$end = $this->getDateParameter('end');
$accounts = $this->getAccountsParameter();
$export = $this->getExportDirectory();
$final = new Collection();
$accounts = new Collection();
$accountList = (string) $this->option('accounts');
$types = [AccountTypeEnum::ASSET->value, AccountTypeEnum::LOAN->value, AccountTypeEnum::DEBT->value, AccountTypeEnum::MORTGAGE->value];
if ('' !== $accountList) {
$accountIds = explode(',', $accountList);
$accounts = $this->accountRepository->getAccountsById($accountIds);
}
if ('' === $accountList) {
$accounts = $this->accountRepository->getAccountsByType($types);
}
return [
'export' => [
'transactions' => $this->option('export-transactions'),
'accounts' => $this->option('export-accounts'),
'budgets' => $this->option('export-budgets'),
'categories' => $this->option('export-categories'),
'tags' => $this->option('export-tags'),
'recurring' => $this->option('export-recurring'),
'rules' => $this->option('export-rules'),
'bills' => $this->option('export-subscriptions'),
'piggies' => $this->option('export-piggies'),
],
'start' => $start,
'end' => $end,
'accounts' => $accounts,
'directory' => $export,
'force' => $this->option('force'),
];
// filter accounts,
/** @var Account $account */
foreach ($accounts as $account) {
if (in_array($account->accountType->type, $types, true)) {
$final->push($account);
}
}
if (0 === $final->count()) {
throw new FireflyException('300007: Ended up with zero valid accounts to export from.');
}
return $final;
}
/**
@@ -228,37 +238,6 @@ class ExportsData extends Command
return $date;
}
/**
* @throws FireflyException
*/
private function getAccountsParameter(): Collection
{
$final = new Collection();
$accounts = new Collection();
$accountList = (string) $this->option('accounts');
$types = [AccountTypeEnum::ASSET->value, AccountTypeEnum::LOAN->value, AccountTypeEnum::DEBT->value, AccountTypeEnum::MORTGAGE->value];
if ('' !== $accountList) {
$accountIds = explode(',', $accountList);
$accounts = $this->accountRepository->getAccountsById($accountIds);
}
if ('' === $accountList) {
$accounts = $this->accountRepository->getAccountsByType($types);
}
// filter accounts,
/** @var Account $account */
foreach ($accounts as $account) {
if (in_array($account->accountType->type, $types, true)) {
$final->push($account);
}
}
if (0 === $final->count()) {
throw new FireflyException('300007: Ended up with zero valid accounts to export from.');
}
return $final;
}
/**
* @throws FireflyException
*/
@@ -277,22 +256,43 @@ class ExportsData extends Command
/**
* @throws FireflyException
* @throws FilesystemException
* @throws Exception
*/
private function exportData(array $options, array $data): void
private function parseOptions(): array
{
$date = Carbon::now()->format('Y_m_d');
foreach ($data as $key => $content) {
$file = sprintf('%s%s_%s.csv', $options['directory'], $date, $key);
if (false === $options['force'] && file_exists($file)) {
throw new FireflyException(sprintf('File "%s" exists already. Use --force to overwrite.', $file));
}
if (true === $options['force'] && file_exists($file)) {
$this->friendlyWarning(sprintf('File "%s" exists already but will be replaced.', $file));
}
// continue to write to file.
file_put_contents($file, $content);
$this->friendlyPositive(sprintf('Wrote %s-export to file "%s".', $key, $file));
}
$start = $this->getDateParameter('start');
$end = $this->getDateParameter('end');
$accounts = $this->getAccountsParameter();
$export = $this->getExportDirectory();
return [
'export' => [
'transactions' => $this->option('export-transactions'),
'accounts' => $this->option('export-accounts'),
'budgets' => $this->option('export-budgets'),
'categories' => $this->option('export-categories'),
'tags' => $this->option('export-tags'),
'recurring' => $this->option('export-recurring'),
'rules' => $this->option('export-rules'),
'bills' => $this->option('export-subscriptions'),
'piggies' => $this->option('export-piggies'),
],
'start' => $start,
'end' => $end,
'accounts' => $accounts,
'directory' => $export,
'force' => $this->option('force'),
];
}
/**
* Laravel will execute ALL __construct() methods for ALL commands whenever a SINGLE command is
* executed. This leads to noticeable slow-downs and class calls. To prevent this, this method should
* be called from the handle method instead of using the constructor to initialize the command.
*/
private function stupidLaravel(): void
{
$this->journalRepository = app(JournalRepositoryInterface::class);
$this->accountRepository = app(AccountRepositoryInterface::class);
}
}

View File

@@ -54,6 +54,45 @@ class ReportsEmptyObjects extends Command
return 0;
}
/**
* Reports on accounts with no transactions.
*/
private function reportAccounts(): void
{
$set = Account::leftJoin('transactions', 'transactions.account_id', '=', 'accounts.id')
->leftJoin('users', 'accounts.user_id', '=', 'users.id')
->groupBy(['accounts.id', 'accounts.encrypted', 'accounts.name', 'accounts.user_id', 'users.email'])
->whereNull('transactions.account_id')
->get(['accounts.id', 'accounts.encrypted', 'accounts.name', 'accounts.user_id', 'users.email'])
;
/** @var stdClass $entry */
foreach ($set as $entry) {
$line = 'User #%d (%s) has account #%d ("%s") which has no transactions.';
$line = sprintf($line, $entry->user_id, $entry->email, $entry->id, $entry->name);
$this->friendlyWarning($line);
}
}
/**
* Reports on budgets with no budget limits (which makes them pointless).
*/
private function reportBudgetLimits(): void
{
$set = Budget::leftJoin('budget_limits', 'budget_limits.budget_id', '=', 'budgets.id')
->leftJoin('users', 'budgets.user_id', '=', 'users.id')
->groupBy(['budgets.id', 'budgets.name', 'budgets.encrypted', 'budgets.user_id', 'users.email'])
->whereNull('budget_limits.id')
->get(['budgets.id', 'budgets.name', 'budgets.user_id', 'budgets.encrypted', 'users.email'])
;
/** @var Budget $entry */
foreach ($set as $entry) {
$line = sprintf('User #%d (%s) has budget #%d ("%s") which has no budget limits.', $entry->user_id, $entry->email, $entry->id, $entry->name);
$this->friendlyWarning($line);
}
}
/**
* Report on budgets with no transactions or journals.
*/
@@ -110,43 +149,4 @@ class ReportsEmptyObjects extends Command
$this->friendlyWarning($line);
}
}
/**
* Reports on accounts with no transactions.
*/
private function reportAccounts(): void
{
$set = Account::leftJoin('transactions', 'transactions.account_id', '=', 'accounts.id')
->leftJoin('users', 'accounts.user_id', '=', 'users.id')
->groupBy(['accounts.id', 'accounts.encrypted', 'accounts.name', 'accounts.user_id', 'users.email'])
->whereNull('transactions.account_id')
->get(['accounts.id', 'accounts.encrypted', 'accounts.name', 'accounts.user_id', 'users.email'])
;
/** @var stdClass $entry */
foreach ($set as $entry) {
$line = 'User #%d (%s) has account #%d ("%s") which has no transactions.';
$line = sprintf($line, $entry->user_id, $entry->email, $entry->id, $entry->name);
$this->friendlyWarning($line);
}
}
/**
* Reports on budgets with no budget limits (which makes them pointless).
*/
private function reportBudgetLimits(): void
{
$set = Budget::leftJoin('budget_limits', 'budget_limits.budget_id', '=', 'budgets.id')
->leftJoin('users', 'budgets.user_id', '=', 'users.id')
->groupBy(['budgets.id', 'budgets.name', 'budgets.encrypted', 'budgets.user_id', 'users.email'])
->whereNull('budget_limits.id')
->get(['budgets.id', 'budgets.name', 'budgets.user_id', 'budgets.encrypted', 'users.email'])
;
/** @var Budget $entry */
foreach ($set as $entry) {
$line = sprintf('User #%d (%s) has budget #%d ("%s") which has no budget limits.', $entry->user_id, $entry->email, $entry->id, $entry->name);
$this->friendlyWarning($line);
}
}
}

View File

@@ -55,6 +55,20 @@ class ValidatesEnvironmentVariables extends Command
return Command::SUCCESS;
}
private function validateGuard(): bool
{
$guard = config('auth.defaults.guard');
if ('web' !== $guard && 'remote_user_guard' !== $guard) {
$this->friendlyError(sprintf('AUTHENTICATION_GUARD "%s" is not a valid guard for Firefly III.', $guard));
$this->friendlyError('Please check your .env file and make sure you use a valid setting.');
$this->friendlyError('Valid guards are: web, remote_user_guard');
return false;
}
return true;
}
private function validateLanguage(): bool
{
$language = config('firefly.default_language');
@@ -80,20 +94,6 @@ class ValidatesEnvironmentVariables extends Command
return true;
}
private function validateGuard(): bool
{
$guard = config('auth.defaults.guard');
if ('web' !== $guard && 'remote_user_guard' !== $guard) {
$this->friendlyError(sprintf('AUTHENTICATION_GUARD "%s" is not a valid guard for Firefly III.', $guard));
$this->friendlyError('Please check your .env file and make sure you use a valid setting.');
$this->friendlyError('Valid guards are: web, remote_user_guard');
return false;
}
return true;
}
private function validateStaticToken(): bool
{
$token = (string) config('firefly.static_cron_token');

View File

@@ -39,16 +39,16 @@ trait ShowsFriendlyMessages
$this->friendlyNeutral($message);
}
public function friendlyNeutral(string $message): void
{
$this->line(sprintf(' [i] %s', trim($message)));
}
public function friendlyLine(string $message): void
{
$this->line(sprintf(' %s', trim($message)));
}
public function friendlyNeutral(string $message): void
{
$this->line(sprintf(' [i] %s', trim($message)));
}
public function friendlyPositive(string $message): void
{
$this->info(sprintf(' [✓] %s', trim($message)));

View File

@@ -112,19 +112,53 @@ class ForcesDecimalSize extends Command
return 0;
}
private function determineDatabaseType(): void
/**
* This method loops over all accounts and validates the amounts.
*/
private function correctAccountAmounts(TransactionCurrency $currency, array $fields): void
{
// switch stuff based on database connection:
$this->operator = 'REGEXP';
$this->regularExpression = '\'\\\.[\\\d]{%d}[1-9]+\'';
$this->cast = 'CHAR';
if ('pgsql' === config('database.default')) {
$this->operator = 'SIMILAR TO';
$this->regularExpression = '\'%%\.[\d]{%d}[1-9]+%%\'';
$this->cast = 'TEXT';
$operator = $this->operator;
$cast = $this->cast;
$regularExpression = $this->regularExpression;
/** @var Builder $query */
$query = Account::leftJoin('account_meta', 'accounts.id', '=', 'account_meta.account_id')
->where('account_meta.name', 'currency_id')
->where('account_meta.data', json_encode((string) $currency->id))
;
$query->where(static function (Builder $q) use ($fields, $currency, $operator, $cast, $regularExpression): void {
foreach ($fields as $field) {
$q->orWhere(
DB::raw(sprintf('CAST(accounts.%s AS %s)', $field, $cast)),
$operator,
DB::raw(sprintf($regularExpression, $currency->decimal_places))
);
}
});
$result = $query->get(['accounts.*']);
if (0 === $result->count()) {
$this->friendlyPositive(sprintf('All accounts in %s are OK', $currency->code));
return;
}
if ('sqlite' === config('database.default')) {
$this->regularExpression = '"\.[\d]{%d}[1-9]+"';
/** @var Account $account */
foreach ($result as $account) {
/** @var string $field */
foreach ($fields as $field) {
$value = $account->{$field};
if (null === $value) {
continue;
}
// fix $field by rounding it down correctly.
$pow = 10 ** $currency->decimal_places;
$correct = bcdiv((string) round($value * $pow), (string) $pow, 12);
$this->friendlyInfo(sprintf('Account #%d has %s with value "%s", this has been corrected to "%s".', $account->id, $field, $value, $correct));
/** @var null|Account $updateAccount */
$updateAccount = Account::find($account->id);
$updateAccount?->update([$field => $correct]);
}
}
}
@@ -230,56 +264,6 @@ class ForcesDecimalSize extends Command
}
}
/**
* This method loops over all accounts and validates the amounts.
*/
private function correctAccountAmounts(TransactionCurrency $currency, array $fields): void
{
$operator = $this->operator;
$cast = $this->cast;
$regularExpression = $this->regularExpression;
/** @var Builder $query */
$query = Account::leftJoin('account_meta', 'accounts.id', '=', 'account_meta.account_id')
->where('account_meta.name', 'currency_id')
->where('account_meta.data', json_encode((string) $currency->id))
;
$query->where(static function (Builder $q) use ($fields, $currency, $operator, $cast, $regularExpression): void {
foreach ($fields as $field) {
$q->orWhere(
DB::raw(sprintf('CAST(accounts.%s AS %s)', $field, $cast)),
$operator,
DB::raw(sprintf($regularExpression, $currency->decimal_places))
);
}
});
$result = $query->get(['accounts.*']);
if (0 === $result->count()) {
$this->friendlyPositive(sprintf('All accounts in %s are OK', $currency->code));
return;
}
/** @var Account $account */
foreach ($result as $account) {
/** @var string $field */
foreach ($fields as $field) {
$value = $account->{$field};
if (null === $value) {
continue;
}
// fix $field by rounding it down correctly.
$pow = 10 ** $currency->decimal_places;
$correct = bcdiv((string) round($value * $pow), (string) $pow, 12);
$this->friendlyInfo(sprintf('Account #%d has %s with value "%s", this has been corrected to "%s".', $account->id, $field, $value, $correct));
/** @var null|Account $updateAccount */
$updateAccount = Account::find($account->id);
$updateAccount?->update([$field => $correct]);
}
}
}
/**
* This method fixes all auto budgets in currency $currency.
*/
@@ -328,6 +312,58 @@ class ForcesDecimalSize extends Command
}
}
/**
* This method fixes all piggy banks in currency $currency.
*/
private function correctPiggyAmounts(TransactionCurrency $currency, array $fields): void
{
$operator = $this->operator;
$cast = $this->cast;
$regularExpression = $this->regularExpression;
/** @var Builder $query */
$query = PiggyBank::leftJoin('accounts', 'piggy_banks.account_id', '=', 'accounts.id')
->leftJoin('account_meta', 'accounts.id', '=', 'account_meta.account_id')
->where('account_meta.name', 'currency_id')
->where('account_meta.data', json_encode((string) $currency->id))
->where(static function (Builder $q) use ($fields, $currency, $operator, $cast, $regularExpression): void {
foreach ($fields as $field) {
$q->orWhere(
DB::raw(sprintf('CAST(piggy_banks.%s AS %s)', $field, $cast)),
$operator,
DB::raw(sprintf($regularExpression, $currency->decimal_places))
);
}
})
;
$result = $query->get(['piggy_banks.*']);
if (0 === $result->count()) {
$this->friendlyPositive(sprintf('All piggy banks in %s are OK', $currency->code));
return;
}
/** @var PiggyBank $item */
foreach ($result as $item) {
/** @var string $field */
foreach ($fields as $field) {
$value = $item->{$field};
if (null === $value) {
continue;
}
// fix $field by rounding it down correctly.
$pow = 10 ** $currency->decimal_places;
$correct = bcdiv((string) round($value * $pow), (string) $pow, 12);
$this->friendlyWarning(sprintf('Piggy bank #%d has %s with value "%s", this has been corrected to "%s".', $item->id, $field, $value, $correct));
/** @var null|PiggyBank $piggyBank */
$piggyBank = PiggyBank::find($item->id);
$piggyBank?->update([$field => $correct]);
}
}
}
/**
* This method fixes all piggy bank events in currency $currency.
*/
@@ -447,58 +483,6 @@ class ForcesDecimalSize extends Command
}
}
/**
* This method fixes all piggy banks in currency $currency.
*/
private function correctPiggyAmounts(TransactionCurrency $currency, array $fields): void
{
$operator = $this->operator;
$cast = $this->cast;
$regularExpression = $this->regularExpression;
/** @var Builder $query */
$query = PiggyBank::leftJoin('accounts', 'piggy_banks.account_id', '=', 'accounts.id')
->leftJoin('account_meta', 'accounts.id', '=', 'account_meta.account_id')
->where('account_meta.name', 'currency_id')
->where('account_meta.data', json_encode((string) $currency->id))
->where(static function (Builder $q) use ($fields, $currency, $operator, $cast, $regularExpression): void {
foreach ($fields as $field) {
$q->orWhere(
DB::raw(sprintf('CAST(piggy_banks.%s AS %s)', $field, $cast)),
$operator,
DB::raw(sprintf($regularExpression, $currency->decimal_places))
);
}
})
;
$result = $query->get(['piggy_banks.*']);
if (0 === $result->count()) {
$this->friendlyPositive(sprintf('All piggy banks in %s are OK', $currency->code));
return;
}
/** @var PiggyBank $item */
foreach ($result as $item) {
/** @var string $field */
foreach ($fields as $field) {
$value = $item->{$field};
if (null === $value) {
continue;
}
// fix $field by rounding it down correctly.
$pow = 10 ** $currency->decimal_places;
$correct = bcdiv((string) round($value * $pow), (string) $pow, 12);
$this->friendlyWarning(sprintf('Piggy bank #%d has %s with value "%s", this has been corrected to "%s".', $item->id, $field, $value, $correct));
/** @var null|PiggyBank $piggyBank */
$piggyBank = PiggyBank::find($item->id);
$piggyBank?->update([$field => $correct]);
}
}
}
/**
* This method fixes all transactions in currency $currency.
*/
@@ -570,6 +554,22 @@ class ForcesDecimalSize extends Command
}
}
private function determineDatabaseType(): void
{
// switch stuff based on database connection:
$this->operator = 'REGEXP';
$this->regularExpression = '\'\\\.[\\\d]{%d}[1-9]+\'';
$this->cast = 'CHAR';
if ('pgsql' === config('database.default')) {
$this->operator = 'SIMILAR TO';
$this->regularExpression = '\'%%\.[\d]{%d}[1-9]+%%\'';
$this->cast = 'TEXT';
}
if ('sqlite' === config('database.default')) {
$this->regularExpression = '"\.[\d]{%d}[1-9]+"';
}
}
private function updateDecimals(): void
{
$this->friendlyInfo('Going to force the size of DECIMAL columns. Please hold.');

View File

@@ -55,110 +55,6 @@ class OutputsInstructions extends Command
return 0;
}
/**
* Render upgrade instructions.
*/
private function updateInstructions(): void
{
$version = (string) config('firefly.version');
/** @var array $config */
$config = config('upgrade.text.upgrade');
$text = '';
/** @var string $compare */
foreach (array_keys($config) as $compare) {
// if string starts with:
if (str_starts_with($version, $compare)) {
$text = (string) $config[$compare];
}
}
// validate some settings.
if ('' === $text && 'local' === (string) config('app.env')) {
$text = 'Please set APP_ENV=production for a safer environment.';
}
$prefix = 'v';
if (str_starts_with($version, 'develop') || str_starts_with($version, 'branch')) {
$prefix = '';
}
$this->newLine();
$this->showLogo();
$this->newLine();
$this->newLine();
$this->showLine();
$this->boxed('');
if ('' === $text) {
$this->boxed(sprintf('Thank you for updating to Firefly III, %s%s', $prefix, $version));
$this->boxedInfo('There are no extra upgrade instructions.');
$this->boxed('Firefly III should be ready for use.');
$this->boxed('');
$this->donationText();
$this->boxed('');
$this->showLine();
return;
}
$this->boxed(sprintf('Thank you for updating to Firefly III, %s%s!', $prefix, $version));
$this->boxedInfo($text);
$this->boxed('');
$this->donationText();
$this->boxed('');
$this->showLine();
}
/**
* The logo takes up 8 lines of code. So 8 colors can be used.
*/
private function showLogo(): void
{
$today = Carbon::now()->format('m-d');
$month = Carbon::now()->format('m');
// variation in colors and effects just because I can!
// default is Ukraine flag:
$colors = ['blue', 'blue', 'blue', 'yellow', 'yellow', 'yellow', 'default', 'default'];
// 5th of May is Dutch liberation day and 29th of April is Dutch King's Day and September 17 is my birthday.
if ('05-01' === $today || '04-29' === $today || '09-17' === $today) {
$colors = ['red', 'red', 'red', 'white', 'white', 'blue', 'blue', 'blue'];
}
// National Coming Out Day, International Day Against Homophobia, Biphobia and Transphobia and Pride Month
if ('10-11' === $today || '05-17' === $today || '06' === $month) {
$colors = ['red', 'bright-red', 'yellow', 'green', 'blue', 'magenta', 'default', 'default'];
}
// International Transgender Day of Visibility
if ('03-31' === $today) {
$colors = ['bright-blue', 'bright-red', 'white', 'white', 'bright-red', 'bright-blue', 'default', 'default'];
}
if ('ru_RU' === config('firefly.default_language')) {
$colors = ['blue', 'blue', 'blue', 'yellow', 'yellow', 'yellow', 'default', 'default'];
}
$this->line(sprintf('<fg=%s> ______ _ __ _ _____ _____ _____ </>', $colors[0]));
$this->line(sprintf('<fg=%s> | ____(_) / _| | |_ _|_ _|_ _| </>', $colors[1]));
$this->line(sprintf('<fg=%s> | |__ _ _ __ ___| |_| |_ _ | | | | | | </>', $colors[2]));
$this->line(sprintf('<fg=%s> | __| | | \'__/ _ \ _| | | | | | | | | | | </>', $colors[3]));
$this->line(sprintf('<fg=%s> | | | | | | __/ | | | |_| | _| |_ _| |_ _| |_ </>', $colors[4]));
$this->line(sprintf('<fg=%s> |_| |_|_| \___|_| |_|\__, | |_____|_____|_____| </>', $colors[5]));
$this->line(sprintf('<fg=%s> __/ | </>', $colors[6]));
$this->line(sprintf('<fg=%s> |___/ </>', $colors[7]));
$this->someQuote();
}
/**
* Show a line.
*/
private function showLine(): void
{
$this->line(sprintf('+%s+', str_repeat('-', 78)));
}
/**
* Show a nice box.
*/
@@ -242,6 +138,54 @@ class OutputsInstructions extends Command
$this->showLine();
}
/**
* Show a line.
*/
private function showLine(): void
{
$this->line(sprintf('+%s+', str_repeat('-', 78)));
}
/**
* The logo takes up 8 lines of code. So 8 colors can be used.
*/
private function showLogo(): void
{
$today = Carbon::now()->format('m-d');
$month = Carbon::now()->format('m');
// variation in colors and effects just because I can!
// default is Ukraine flag:
$colors = ['blue', 'blue', 'blue', 'yellow', 'yellow', 'yellow', 'default', 'default'];
// 5th of May is Dutch liberation day and 29th of April is Dutch King's Day and September 17 is my birthday.
if ('05-01' === $today || '04-29' === $today || '09-17' === $today) {
$colors = ['red', 'red', 'red', 'white', 'white', 'blue', 'blue', 'blue'];
}
// National Coming Out Day, International Day Against Homophobia, Biphobia and Transphobia and Pride Month
if ('10-11' === $today || '05-17' === $today || '06' === $month) {
$colors = ['red', 'bright-red', 'yellow', 'green', 'blue', 'magenta', 'default', 'default'];
}
// International Transgender Day of Visibility
if ('03-31' === $today) {
$colors = ['bright-blue', 'bright-red', 'white', 'white', 'bright-red', 'bright-blue', 'default', 'default'];
}
if ('ru_RU' === config('firefly.default_language')) {
$colors = ['blue', 'blue', 'blue', 'yellow', 'yellow', 'yellow', 'default', 'default'];
}
$this->line(sprintf('<fg=%s> ______ _ __ _ _____ _____ _____ </>', $colors[0]));
$this->line(sprintf('<fg=%s> | ____(_) / _| | |_ _|_ _|_ _| </>', $colors[1]));
$this->line(sprintf('<fg=%s> | |__ _ _ __ ___| |_| |_ _ | | | | | | </>', $colors[2]));
$this->line(sprintf('<fg=%s> | __| | | \'__/ _ \ _| | | | | | | | | | | </>', $colors[3]));
$this->line(sprintf('<fg=%s> | | | | | | __/ | | | |_| | _| |_ _| |_ _| |_ </>', $colors[4]));
$this->line(sprintf('<fg=%s> |_| |_|_| \___|_| |_|\__, | |_____|_____|_____| </>', $colors[5]));
$this->line(sprintf('<fg=%s> __/ | </>', $colors[6]));
$this->line(sprintf('<fg=%s> |___/ </>', $colors[7]));
$this->someQuote();
}
private function someQuote(): void
{
$lines = [
@@ -273,4 +217,60 @@ class OutputsInstructions extends Command
}
$this->line(sprintf(' %s', $lines[$random]));
}
/**
* Render upgrade instructions.
*/
private function updateInstructions(): void
{
$version = (string) config('firefly.version');
/** @var array $config */
$config = config('upgrade.text.upgrade');
$text = '';
/** @var string $compare */
foreach (array_keys($config) as $compare) {
// if string starts with:
if (str_starts_with($version, $compare)) {
$text = (string) $config[$compare];
}
}
// validate some settings.
if ('' === $text && 'local' === (string) config('app.env')) {
$text = 'Please set APP_ENV=production for a safer environment.';
}
$prefix = 'v';
if (str_starts_with($version, 'develop') || str_starts_with($version, 'branch')) {
$prefix = '';
}
$this->newLine();
$this->showLogo();
$this->newLine();
$this->newLine();
$this->showLine();
$this->boxed('');
if ('' === $text) {
$this->boxed(sprintf('Thank you for updating to Firefly III, %s%s', $prefix, $version));
$this->boxedInfo('There are no extra upgrade instructions.');
$this->boxed('Firefly III should be ready for use.');
$this->boxed('');
$this->donationText();
$this->boxed('');
$this->showLine();
return;
}
$this->boxed(sprintf('Thank you for updating to Firefly III, %s%s!', $prefix, $version));
$this->boxedInfo($text);
$this->boxed('');
$this->donationText();
$this->boxed('');
$this->showLine();
}
}

View File

@@ -140,6 +140,44 @@ class ApplyRules extends Command
return 0;
}
private function getRulesToApply(): Collection
{
Log::debug('getRulesToApply()');
$rulesToApply = new Collection();
/** @var RuleGroup $group */
foreach ($this->groups as $group) {
Log::debug(sprintf('Scanning rule group #%d', $group->id));
$rules = $this->ruleGroupRepository->getActiveStoreRules($group);
/** @var Rule $rule */
foreach ($rules as $rule) {
// if in rule selection, or group in selection or all rules, it's included.
$test = $this->includeRule($rule, $group);
if ($test) {
Log::debug(sprintf('Will include rule #%d "%s"', $rule->id, $rule->title));
$rulesToApply->push($rule);
}
if (!$test) {
Log::debug(sprintf('Will not include rule #%d', $rule->id));
}
}
}
Log::debug(sprintf('Found %d rules to apply.', $rulesToApply->count()));
return $rulesToApply;
}
private function grabAllRules(): void
{
$this->groups = $this->ruleGroupRepository->getActiveGroups();
}
private function includeRule(Rule $rule, RuleGroup $group): bool
{
return in_array((int) $group->id, $this->ruleGroupSelection, true) || in_array((int) $rule->id, $this->ruleSelection, true) || $this->allRules;
}
/**
* Laravel will execute ALL __construct() methods for ALL commands whenever a SINGLE command is
* executed. This leads to noticeable slow-downs and class calls. To prevent this, this method should
@@ -234,6 +272,47 @@ class ApplyRules extends Command
return true;
}
/**
* @throws FireflyException
*/
private function verifyInputDates(): void
{
// parse start date.
$inputStart = today(config('app.timezone'))->startOfMonth();
$startString = $this->option('start_date');
if (null === $startString) {
/** @var JournalRepositoryInterface $repository */
$repository = app(JournalRepositoryInterface::class);
$repository->setUser($this->getUser());
$first = $repository->firstNull();
if (null !== $first) {
$inputStart = $first->date;
}
}
if (null !== $startString && '' !== $startString) {
$inputStart = Carbon::createFromFormat('Y-m-d', $startString);
}
// parse end date
$inputEnd = today(config('app.timezone'));
$endString = $this->option('end_date');
if (null !== $endString && '' !== $endString) {
$inputEnd = Carbon::createFromFormat('Y-m-d', $endString);
}
if (!$inputEnd instanceof Carbon || null === $inputStart) {
Log::error('Could not parse start or end date in verifyInputDate().');
return;
}
if ($inputStart > $inputEnd) {
[$inputEnd, $inputStart] = [$inputStart, $inputEnd];
}
$this->startDate = $inputStart;
$this->endDate = $inputEnd;
}
private function verifyInputRuleGroups(): bool
{
$ruleGroupString = $this->option('rule_groups');
@@ -302,83 +381,4 @@ class ApplyRules extends Command
return true;
}
/**
* @throws FireflyException
*/
private function verifyInputDates(): void
{
// parse start date.
$inputStart = today(config('app.timezone'))->startOfMonth();
$startString = $this->option('start_date');
if (null === $startString) {
/** @var JournalRepositoryInterface $repository */
$repository = app(JournalRepositoryInterface::class);
$repository->setUser($this->getUser());
$first = $repository->firstNull();
if (null !== $first) {
$inputStart = $first->date;
}
}
if (null !== $startString && '' !== $startString) {
$inputStart = Carbon::createFromFormat('Y-m-d', $startString);
}
// parse end date
$inputEnd = today(config('app.timezone'));
$endString = $this->option('end_date');
if (null !== $endString && '' !== $endString) {
$inputEnd = Carbon::createFromFormat('Y-m-d', $endString);
}
if (!$inputEnd instanceof Carbon || null === $inputStart) {
Log::error('Could not parse start or end date in verifyInputDate().');
return;
}
if ($inputStart > $inputEnd) {
[$inputEnd, $inputStart] = [$inputStart, $inputEnd];
}
$this->startDate = $inputStart;
$this->endDate = $inputEnd;
}
private function grabAllRules(): void
{
$this->groups = $this->ruleGroupRepository->getActiveGroups();
}
private function getRulesToApply(): Collection
{
Log::debug('getRulesToApply()');
$rulesToApply = new Collection();
/** @var RuleGroup $group */
foreach ($this->groups as $group) {
Log::debug(sprintf('Scanning rule group #%d', $group->id));
$rules = $this->ruleGroupRepository->getActiveStoreRules($group);
/** @var Rule $rule */
foreach ($rules as $rule) {
// if in rule selection, or group in selection or all rules, it's included.
$test = $this->includeRule($rule, $group);
if ($test) {
Log::debug(sprintf('Will include rule #%d "%s"', $rule->id, $rule->title));
$rulesToApply->push($rule);
}
if (!$test) {
Log::debug(sprintf('Will not include rule #%d', $rule->id));
}
}
}
Log::debug(sprintf('Found %d rules to apply.', $rulesToApply->count()));
return $rulesToApply;
}
private function includeRule(Rule $rule, RuleGroup $group): bool
{
return in_array((int) $group->id, $this->ruleGroupSelection, true) || in_array((int) $rule->id, $this->ruleSelection, true) || $this->allRules;
}
}

View File

@@ -142,6 +142,45 @@ class Cron extends Command
return 0;
}
private function autoBudgetCronJob(bool $force, ?Carbon $date): void
{
$autoBudget = new AutoBudgetCronjob();
$autoBudget->setForce($force);
// set date in cron job:
if ($date instanceof Carbon) {
$autoBudget->setDate($date);
}
$autoBudget->fire();
if ($autoBudget->jobErrored) {
$this->friendlyError(sprintf('Error in "create auto budgets" cron: %s', $autoBudget->message));
}
if ($autoBudget->jobFired) {
$this->friendlyInfo(sprintf('"Create auto budgets" cron fired: %s', $autoBudget->message));
}
if ($autoBudget->jobSucceeded) {
$this->friendlyPositive(sprintf('"Create auto budgets" cron ran with success: %s', $autoBudget->message));
}
}
private function checkForUpdates(bool $force): void
{
$updateCheck = new UpdateCheckCronjob();
$updateCheck->setForce($force);
$updateCheck->fire();
if ($updateCheck->jobErrored) {
$this->friendlyError(sprintf('Error in "update check" cron: %s', $updateCheck->message));
}
if ($updateCheck->jobFired) {
$this->friendlyInfo(sprintf('"Update check" cron fired: %s', $updateCheck->message));
}
if ($updateCheck->jobSucceeded) {
$this->friendlyPositive(sprintf('"Update check" cron ran with success: %s', $updateCheck->message));
}
}
private function exchangeRatesCronJob(bool $force, ?Carbon $date): void
{
Log::debug(sprintf('Created new ExchangeRateConverter in %s', __METHOD__));
@@ -165,23 +204,6 @@ class Cron extends Command
}
}
private function checkForUpdates(bool $force): void
{
$updateCheck = new UpdateCheckCronjob();
$updateCheck->setForce($force);
$updateCheck->fire();
if ($updateCheck->jobErrored) {
$this->friendlyError(sprintf('Error in "update check" cron: %s', $updateCheck->message));
}
if ($updateCheck->jobFired) {
$this->friendlyInfo(sprintf('"Update check" cron fired: %s', $updateCheck->message));
}
if ($updateCheck->jobSucceeded) {
$this->friendlyPositive(sprintf('"Update check" cron ran with success: %s', $updateCheck->message));
}
}
/**
* @throws FireflyException
*/
@@ -207,28 +229,6 @@ class Cron extends Command
}
}
private function autoBudgetCronJob(bool $force, ?Carbon $date): void
{
$autoBudget = new AutoBudgetCronjob();
$autoBudget->setForce($force);
// set date in cron job:
if ($date instanceof Carbon) {
$autoBudget->setDate($date);
}
$autoBudget->fire();
if ($autoBudget->jobErrored) {
$this->friendlyError(sprintf('Error in "create auto budgets" cron: %s', $autoBudget->message));
}
if ($autoBudget->jobFired) {
$this->friendlyInfo(sprintf('"Create auto budgets" cron fired: %s', $autoBudget->message));
}
if ($autoBudget->jobSucceeded) {
$this->friendlyPositive(sprintf('"Create auto budgets" cron ran with success: %s', $autoBudget->message));
}
}
/**
* @throws FireflyException
*/

View File

@@ -86,6 +86,44 @@ class AddsTransactionIdentifiers extends Command
return 0;
}
private function findOpposing(Transaction $transaction, array $exclude): ?Transaction
{
// find opposing:
$amount = bcmul($transaction->amount, '-1');
try {
/** @var Transaction $opposing */
$opposing = Transaction::where('transaction_journal_id', $transaction->transaction_journal_id)
->where('amount', $amount)
->where('identifier', '=', 0)
->whereNotIn('id', $exclude)
->first()
;
} catch (QueryException $e) {
Log::error($e->getMessage());
$this->friendlyError('Firefly III could not find the "identifier" field in the "transactions" table.');
$this->friendlyError(sprintf('This field is required for Firefly III version %s to run.', config('firefly.version')));
$this->friendlyError('Please run "php artisan migrate --force" to add this field to the table.');
$this->friendlyError('Then, run "php artisan firefly:upgrade-database" to try again.');
return null;
}
return $opposing;
}
private function isExecuted(): bool
{
$configVar = FireflyConfig::get(self::CONFIG_NAME, false);
return (bool) $configVar?->data;
}
private function markAsExecuted(): void
{
FireflyConfig::set(self::CONFIG_NAME, true);
}
/**
* Laravel will execute ALL __construct() methods for ALL commands whenever a SINGLE command is
* executed. This leads to noticeable slow-downs and class calls. To prevent this, this method should
@@ -97,13 +135,6 @@ class AddsTransactionIdentifiers extends Command
$this->count = 0;
}
private function isExecuted(): bool
{
$configVar = FireflyConfig::get(self::CONFIG_NAME, false);
return (bool) $configVar?->data;
}
/**
* Grab all positive transactions from this journal that are not deleted. for each one, grab the negative opposing
* one which has 0 as an identifier and give it the same identifier.
@@ -130,35 +161,4 @@ class AddsTransactionIdentifiers extends Command
++$identifier;
}
}
private function findOpposing(Transaction $transaction, array $exclude): ?Transaction
{
// find opposing:
$amount = bcmul($transaction->amount, '-1');
try {
/** @var Transaction $opposing */
$opposing = Transaction::where('transaction_journal_id', $transaction->transaction_journal_id)
->where('amount', $amount)
->where('identifier', '=', 0)
->whereNotIn('id', $exclude)
->first()
;
} catch (QueryException $e) {
Log::error($e->getMessage());
$this->friendlyError('Firefly III could not find the "identifier" field in the "transactions" table.');
$this->friendlyError(sprintf('This field is required for Firefly III version %s to run.', config('firefly.version')));
$this->friendlyError('Please run "php artisan migrate --force" to add this field to the table.');
$this->friendlyError('Then, run "php artisan firefly:upgrade-database" to try again.');
return null;
}
return $opposing;
}
private function markAsExecuted(): void
{
FireflyConfig::set(self::CONFIG_NAME, true);
}
}

View File

@@ -75,34 +75,6 @@ class RemovesDatabaseDecryption extends Command
return 0;
}
private function decryptTable(string $table, array $fields): void
{
if ($this->isDecrypted($table)) {
return;
}
foreach ($fields as $field) {
$this->decryptField($table, $field);
}
$this->friendlyPositive(sprintf('Decrypted the data in table "%s".', $table));
// mark as decrypted:
$configName = sprintf('is_decrypted_%s', $table);
FireflyConfig::set($configName, true);
}
private function isDecrypted(string $table): bool
{
$configName = sprintf('is_decrypted_%s', $table);
$configVar = null;
try {
$configVar = FireflyConfig::get($configName, false);
} catch (FireflyException $e) {
Log::error($e->getMessage());
}
return (bool) $configVar?->data;
}
private function decryptField(string $table, string $field): void
{
$rows = DB::table($table)->get(['id', $field]);
@@ -113,6 +85,29 @@ class RemovesDatabaseDecryption extends Command
}
}
private function decryptPreferencesRow(int $id, string $value): void
{
// try to json_decrypt the value.
try {
$newValue = json_decode($value, true, 512, JSON_THROW_ON_ERROR) ?? $value;
} catch (JsonException $e) {
$message = sprintf('Could not JSON decode preference row #%d: %s. This does not have to be a problem.', $id, $e->getMessage());
$this->friendlyError($message);
Log::warning($message);
Log::warning($value);
Log::warning($e->getTraceAsString());
return;
}
/** @var null|Preference $object */
$object = Preference::find($id);
if (null !== $object) {
$object->data = $newValue;
$object->save();
}
}
private function decryptRow(string $table, string $field, stdClass $row): void
{
$original = $row->{$field};
@@ -143,6 +138,34 @@ class RemovesDatabaseDecryption extends Command
}
}
private function decryptTable(string $table, array $fields): void
{
if ($this->isDecrypted($table)) {
return;
}
foreach ($fields as $field) {
$this->decryptField($table, $field);
}
$this->friendlyPositive(sprintf('Decrypted the data in table "%s".', $table));
// mark as decrypted:
$configName = sprintf('is_decrypted_%s', $table);
FireflyConfig::set($configName, true);
}
private function isDecrypted(string $table): bool
{
$configName = sprintf('is_decrypted_%s', $table);
$configVar = null;
try {
$configVar = FireflyConfig::get($configName, false);
} catch (FireflyException $e) {
Log::error($e->getMessage());
}
return (bool) $configVar?->data;
}
/**
* Tries to decrypt data. Will only throw an exception when the MAC is invalid.
*
@@ -164,27 +187,4 @@ class RemovesDatabaseDecryption extends Command
return $value;
}
private function decryptPreferencesRow(int $id, string $value): void
{
// try to json_decrypt the value.
try {
$newValue = json_decode($value, true, 512, JSON_THROW_ON_ERROR) ?? $value;
} catch (JsonException $e) {
$message = sprintf('Could not JSON decode preference row #%d: %s. This does not have to be a problem.', $id, $e->getMessage());
$this->friendlyError($message);
Log::warning($message);
Log::warning($value);
Log::warning($e->getTraceAsString());
return;
}
/** @var null|Preference $object */
$object = Preference::find($id);
if (null !== $object) {
$object->data = $newValue;
$object->save();
}
}
}

View File

@@ -58,6 +58,11 @@ class RepairsAccountBalances extends Command
return 0;
}
private function correctBalanceAmounts(): void
{
AccountBalanceCalculator::recalculateAll(false);
}
private function isExecuted(): bool
{
$configVar = FireflyConfig::get(self::CONFIG_NAME, false);
@@ -69,9 +74,4 @@ class RepairsAccountBalances extends Command
{
FireflyConfig::set(self::CONFIG_NAME, true);
}
private function correctBalanceAmounts(): void
{
AccountBalanceCalculator::recalculateAll(false);
}
}

View File

@@ -73,6 +73,18 @@ class UpgradesAccountCurrencies extends Command
return 0;
}
private function isExecuted(): bool
{
$configVar = FireflyConfig::get(self::CONFIG_NAME, false);
return (bool) $configVar?->data;
}
private function markAsExecuted(): void
{
FireflyConfig::set(self::CONFIG_NAME, true);
}
/**
* Laravel will execute ALL __construct() methods for ALL commands whenever a SINGLE command is
* executed. This leads to noticeable slow-downs and class calls. To prevent this, this method should
@@ -85,35 +97,6 @@ class UpgradesAccountCurrencies extends Command
$this->count = 0;
}
private function isExecuted(): bool
{
$configVar = FireflyConfig::get(self::CONFIG_NAME, false);
return (bool) $configVar?->data;
}
private function updateAccountCurrencies(): void
{
$users = $this->userRepos->all();
foreach ($users as $user) {
$this->updateCurrenciesForUser($user);
}
}
private function updateCurrenciesForUser(User $user): void
{
$this->accountRepos->setUser($user);
$accounts = $this->accountRepos->getAccountsByType([AccountTypeEnum::DEFAULT->value, AccountTypeEnum::ASSET->value]);
// get user's currency preference:
$primaryCurrency = Amount::getPrimaryCurrencyByUserGroup($user->userGroup);
/** @var Account $account */
foreach ($accounts as $account) {
$this->updateAccount($account, $primaryCurrency);
}
}
private function updateAccount(Account $account, TransactionCurrency $currency): void
{
$this->accountRepos->setUser($account->user);
@@ -153,8 +136,25 @@ class UpgradesAccountCurrencies extends Command
}
}
private function markAsExecuted(): void
private function updateAccountCurrencies(): void
{
FireflyConfig::set(self::CONFIG_NAME, true);
$users = $this->userRepos->all();
foreach ($users as $user) {
$this->updateCurrenciesForUser($user);
}
}
private function updateCurrenciesForUser(User $user): void
{
$this->accountRepos->setUser($user);
$accounts = $this->accountRepos->getAccountsByType([AccountTypeEnum::DEFAULT->value, AccountTypeEnum::ASSET->value]);
// get user's currency preference:
$primaryCurrency = Amount::getPrimaryCurrencyByUserGroup($user->userGroup);
/** @var Account $account */
foreach ($accounts as $account) {
$this->updateAccount($account, $primaryCurrency);
}
}
}

View File

@@ -84,20 +84,6 @@ class UpgradesBillsToRules extends Command
return 0;
}
/**
* Laravel will execute ALL __construct() methods for ALL commands whenever a SINGLE command is
* executed. This leads to noticeable slow-downs and class calls. To prevent this, this method should
* be called from the handle method instead of using the constructor to initialize the command.
*/
private function stupidLaravel(): void
{
$this->count = 0;
$this->userRepository = app(UserRepositoryInterface::class);
$this->ruleGroupRepository = app(RuleGroupRepositoryInterface::class);
$this->billRepository = app(BillRepositoryInterface::class);
$this->ruleRepository = app(RuleRepositoryInterface::class);
}
private function isExecuted(): bool
{
$configVar = FireflyConfig::get(self::CONFIG_NAME, false);
@@ -105,34 +91,9 @@ class UpgradesBillsToRules extends Command
return (bool) $configVar?->data;
}
/**
* Migrate bills to new rule structure for a specific user.
*/
private function migrateUser(User $user): void
private function markAsExecuted(): void
{
$this->ruleGroupRepository->setUser($user);
$this->billRepository->setUser($user);
$this->ruleRepository->setUser($user);
/** @var Preference $lang */
$lang = Preferences::getForUser($user, 'language', 'en_US');
$language = null !== $lang->data && !is_array($lang->data) ? (string) $lang->data : 'en_US';
$groupTitle = (string) trans('firefly.rulegroup_for_bills_title', [], $language);
$ruleGroup = $this->ruleGroupRepository->findByTitle($groupTitle);
if (!$ruleGroup instanceof RuleGroup) {
$ruleGroup = $this->ruleGroupRepository->store([
'title' => (string) trans('firefly.rulegroup_for_bills_title', [], $language),
'description' => (string) trans('firefly.rulegroup_for_bills_description', [], $language),
'active' => true,
]);
}
$bills = $this->billRepository->getBills();
/** @var Bill $bill */
foreach ($bills as $bill) {
$this->migrateBill($ruleGroup, $bill, $lang);
}
FireflyConfig::set(self::CONFIG_NAME, true);
}
private function migrateBill(RuleGroup $ruleGroup, Bill $bill, Preference $language): void
@@ -183,8 +144,47 @@ class UpgradesBillsToRules extends Command
++$this->count;
}
private function markAsExecuted(): void
/**
* Migrate bills to new rule structure for a specific user.
*/
private function migrateUser(User $user): void
{
FireflyConfig::set(self::CONFIG_NAME, true);
$this->ruleGroupRepository->setUser($user);
$this->billRepository->setUser($user);
$this->ruleRepository->setUser($user);
/** @var Preference $lang */
$lang = Preferences::getForUser($user, 'language', 'en_US');
$language = null !== $lang->data && !is_array($lang->data) ? (string) $lang->data : 'en_US';
$groupTitle = (string) trans('firefly.rulegroup_for_bills_title', [], $language);
$ruleGroup = $this->ruleGroupRepository->findByTitle($groupTitle);
if (!$ruleGroup instanceof RuleGroup) {
$ruleGroup = $this->ruleGroupRepository->store([
'title' => (string) trans('firefly.rulegroup_for_bills_title', [], $language),
'description' => (string) trans('firefly.rulegroup_for_bills_description', [], $language),
'active' => true,
]);
}
$bills = $this->billRepository->getBills();
/** @var Bill $bill */
foreach ($bills as $bill) {
$this->migrateBill($ruleGroup, $bill, $lang);
}
}
/**
* Laravel will execute ALL __construct() methods for ALL commands whenever a SINGLE command is
* executed. This leads to noticeable slow-downs and class calls. To prevent this, this method should
* be called from the handle method instead of using the constructor to initialize the command.
*/
private function stupidLaravel(): void
{
$this->count = 0;
$this->userRepository = app(UserRepositoryInterface::class);
$this->ruleGroupRepository = app(RuleGroupRepositoryInterface::class);
$this->billRepository = app(BillRepositoryInterface::class);
$this->ruleRepository = app(RuleRepositoryInterface::class);
}
}

View File

@@ -57,23 +57,6 @@ class UpgradesBudgetLimitPeriods extends Command
return 0;
}
private function isExecuted(): bool
{
$configVar = FireflyConfig::get(self::CONFIG_NAME, false);
return (bool) $configVar->data;
}
private function theresNoLimit(): void
{
$limits = BudgetLimit::whereNull('period')->get();
/** @var BudgetLimit $limit */
foreach ($limits as $limit) {
$this->fixLimit($limit);
}
}
private function fixLimit(BudgetLimit $limit): void
{
$period = $this->getLimitPeriod($limit);
@@ -91,7 +74,7 @@ class UpgradesBudgetLimitPeriods extends Command
return;
}
$limit->period = $period;
$limit->save();
$limit->saveQuietly();
$msg = sprintf(
'Budget limit #%d (%s - %s) period is "%s".',
@@ -155,8 +138,25 @@ class UpgradesBudgetLimitPeriods extends Command
return null;
}
private function isExecuted(): bool
{
$configVar = FireflyConfig::get(self::CONFIG_NAME, false);
return (bool) $configVar->data;
}
private function markAsExecuted(): void
{
FireflyConfig::set(self::CONFIG_NAME, true);
}
private function theresNoLimit(): void
{
$limits = BudgetLimit::whereNull('period')->get();
/** @var BudgetLimit $limit */
foreach ($limits as $limit) {
$this->fixLimit($limit);
}
}
}

View File

@@ -70,7 +70,7 @@ class UpgradesBudgetLimits extends Command
if (null !== $user) {
$currency = Amount::getPrimaryCurrencyByUserGroup($user->userGroup);
$budgetLimit->transaction_currency_id = $currency->id;
$budgetLimit->save();
$budgetLimit->saveQuietly();
$this->friendlyInfo(sprintf(
'Budget limit #%d (part of budget "%s") now has a currency setting (%s).',
$budgetLimit->id,

View File

@@ -64,6 +64,24 @@ class UpgradesCurrencyPreferences extends Command
return 0;
}
private function getPreference(User $user): string
{
$preference = Preference::where('user_id', $user->id)
->where('name', 'currencyPreference')
->first(['id', 'user_id', 'name', 'data', 'updated_at', 'created_at'])
;
if (null === $preference) {
return 'EUR';
}
if (null !== $preference->data && !is_array($preference->data)) {
return (string) $preference->data;
}
return 'EUR';
}
private function isExecuted(): bool
{
$configVar = FireflyConfig::get(self::CONFIG_NAME, false);
@@ -71,6 +89,11 @@ class UpgradesCurrencyPreferences extends Command
return (bool) $configVar?->data;
}
private function markAsExecuted(): void
{
FireflyConfig::set(self::CONFIG_NAME, true);
}
private function runUpgrade(): void
{
$groups = UserGroup::get();
@@ -126,27 +149,4 @@ class UpgradesCurrencyPreferences extends Command
$user->currencies()->updateExistingPivot($primaryCurrency->id, ['user_default' => true]);
$user->userGroup->currencies()->updateExistingPivot($primaryCurrency->id, ['group_default' => true]);
}
private function getPreference(User $user): string
{
$preference = Preference::where('user_id', $user->id)
->where('name', 'currencyPreference')
->first(['id', 'user_id', 'name', 'data', 'updated_at', 'created_at'])
;
if (null === $preference) {
return 'EUR';
}
if (null !== $preference->data && !is_array($preference->data)) {
return (string) $preference->data;
}
return 'EUR';
}
private function markAsExecuted(): void
{
FireflyConfig::set(self::CONFIG_NAME, true);
}
}

View File

@@ -68,11 +68,48 @@ class UpgradesJournalMetaData extends Command
return 0;
}
private function isMigrated(): bool
private function getIdsForBudgets(): array
{
$configVar = FireflyConfig::get(UpgradesToGroups::CONFIG_NAME, false);
$transactions = DB::table('budget_transaction')
->distinct()
->pluck('transaction_id')
->toArray()
;
$array = [];
$chunks = array_chunk($transactions, 500);
return (bool) $configVar->data;
foreach ($chunks as $chunk) {
$set = DB::table('transactions')
->whereIn('transactions.id', $chunk)
->pluck('transaction_journal_id')
->toArray()
;
$array = array_merge($array, $set);
}
return $array;
}
private function getIdsForCategories(): array
{
$transactions = DB::table('category_transaction')
->distinct()
->pluck('transaction_id')
->toArray()
;
$array = [];
$chunks = array_chunk($transactions, 500);
foreach ($chunks as $chunk) {
$set = DB::table('transactions')
->whereIn('transactions.id', $chunk)
->pluck('transaction_journal_id')
->toArray()
;
$array = array_merge($array, $set);
}
return $array;
}
private function isExecuted(): bool
@@ -82,6 +119,18 @@ class UpgradesJournalMetaData extends Command
return (bool) $configVar->data;
}
private function isMigrated(): bool
{
$configVar = FireflyConfig::get(UpgradesToGroups::CONFIG_NAME, false);
return (bool) $configVar->data;
}
private function markAsExecuted(): void
{
FireflyConfig::set(self::CONFIG_NAME, true);
}
private function migrateAll(): void
{
$this->migrateBudgets();
@@ -108,28 +157,6 @@ class UpgradesJournalMetaData extends Command
}
}
private function getIdsForBudgets(): array
{
$transactions = DB::table('budget_transaction')
->distinct()
->pluck('transaction_id')
->toArray()
;
$array = [];
$chunks = array_chunk($transactions, 500);
foreach ($chunks as $chunk) {
$set = DB::table('transactions')
->whereIn('transactions.id', $chunk)
->pluck('transaction_journal_id')
->toArray()
;
$array = array_merge($array, $set);
}
return $array;
}
private function migrateBudgetsForJournal(TransactionJournal $journal): void
{
// grab category from first transaction
@@ -179,28 +206,6 @@ class UpgradesJournalMetaData extends Command
}
}
private function getIdsForCategories(): array
{
$transactions = DB::table('category_transaction')
->distinct()
->pluck('transaction_id')
->toArray()
;
$array = [];
$chunks = array_chunk($transactions, 500);
foreach ($chunks as $chunk) {
$set = DB::table('transactions')
->whereIn('transactions.id', $chunk)
->pluck('transaction_journal_id')
->toArray()
;
$array = array_merge($array, $set);
}
return $array;
}
private function migrateCategoriesForJournal(TransactionJournal $journal): void
{
// grab category from first transaction
@@ -229,9 +234,4 @@ class UpgradesJournalMetaData extends Command
$journal->categories()->sync([$category->id]);
}
}
private function markAsExecuted(): void
{
FireflyConfig::set(self::CONFIG_NAME, true);
}
}

View File

@@ -61,6 +61,36 @@ class UpgradesLiabilities extends Command
return 0;
}
private function correctOpeningBalance(Account $account, TransactionJournal $openingBalance): void
{
$source = $this->getSourceTransaction($openingBalance);
$destination = $this->getDestinationTransaction($openingBalance);
if (!$source instanceof Transaction || !$destination instanceof Transaction) {
return;
}
// source MUST be the liability.
if ($destination->account_id === $account->id) {
// so if not, switch things around:
$sourceAccountId = $source->account_id;
$source->account_id = $destination->account_id;
$destination->account_id = $sourceAccountId;
$source->save();
$destination->save();
}
}
private function getDestinationTransaction(TransactionJournal $journal): ?Transaction
{
/** @var null|Transaction */
return $journal->transactions()->where('amount', '>', 0)->first();
}
private function getSourceTransaction(TransactionJournal $journal): ?Transaction
{
/** @var null|Transaction */
return $journal->transactions()->where('amount', '<', 0)->first();
}
private function isExecuted(): bool
{
$configVar = FireflyConfig::get(self::CONFIG_NAME, false);
@@ -68,14 +98,9 @@ class UpgradesLiabilities extends Command
return (bool) $configVar?->data;
}
private function upgradeLiabilities(): void
private function markAsExecuted(): void
{
$users = User::get();
/** @var User $user */
foreach ($users as $user) {
$this->upgradeForUser($user);
}
FireflyConfig::set(self::CONFIG_NAME, true);
}
private function upgradeForUser(User $user): void
@@ -96,6 +121,16 @@ class UpgradesLiabilities extends Command
}
}
private function upgradeLiabilities(): void
{
$users = User::get();
/** @var User $user */
foreach ($users as $user) {
$this->upgradeForUser($user);
}
}
private function upgradeLiability(Account $account): void
{
/** @var AccountRepositoryInterface $repository */
@@ -117,39 +152,4 @@ class UpgradesLiabilities extends Command
$factory->crud($account, 'liability_direction', 'debit');
}
}
private function correctOpeningBalance(Account $account, TransactionJournal $openingBalance): void
{
$source = $this->getSourceTransaction($openingBalance);
$destination = $this->getDestinationTransaction($openingBalance);
if (!$source instanceof Transaction || !$destination instanceof Transaction) {
return;
}
// source MUST be the liability.
if ($destination->account_id === $account->id) {
// so if not, switch things around:
$sourceAccountId = $source->account_id;
$source->account_id = $destination->account_id;
$destination->account_id = $sourceAccountId;
$source->save();
$destination->save();
}
}
private function getSourceTransaction(TransactionJournal $journal): ?Transaction
{
/** @var null|Transaction */
return $journal->transactions()->where('amount', '<', 0)->first();
}
private function getDestinationTransaction(TransactionJournal $journal): ?Transaction
{
/** @var null|Transaction */
return $journal->transactions()->where('amount', '>', 0)->first();
}
private function markAsExecuted(): void
{
FireflyConfig::set(self::CONFIG_NAME, true);
}
}

View File

@@ -63,59 +63,40 @@ class UpgradesLiabilitiesEight extends Command
return 0;
}
private function isExecuted(): bool
private function deleteCreditTransaction(Account $account): void
{
$configVar = FireflyConfig::get(self::CONFIG_NAME, false);
return (bool) $configVar?->data;
}
private function upgradeLiabilities(): void
{
$users = User::get();
/** @var User $user */
foreach ($users as $user) {
$this->upgradeForUser($user);
}
}
private function upgradeForUser(User $user): void
{
$accounts = $user
->accounts()
->leftJoin('account_types', 'account_types.id', '=', 'accounts.account_type_id')
->whereIn('account_types.type', config('firefly.valid_liabilities'))
->get(['accounts.*'])
$liabilityType = TransactionType::whereType(TransactionTypeEnum::LIABILITY_CREDIT->value)->first();
$liabilityJournal = TransactionJournal::leftJoin('transactions', 'transactions.transaction_journal_id', '=', 'transaction_journals.id')
->where('transactions.account_id', $account->id)
->where('transaction_journals.transaction_type_id', $liabilityType->id)
->first(['transaction_journals.*'])
;
/** @var Account $account */
foreach ($accounts as $account) {
$this->upgradeLiability($account);
$service = app(CreditRecalculateService::class);
$service->setAccount($account);
$service->recalculate();
if (null !== $liabilityJournal && null !== $liabilityJournal->transactionGroup) {
$group = $liabilityJournal->transactionGroup;
$service = new TransactionGroupDestroyService();
$service->destroy($group);
}
}
private function upgradeLiability(Account $account): void
private function deleteTransactions(Account $account): int
{
/** @var AccountRepositoryInterface $repository */
$repository = app(AccountRepositoryInterface::class);
$repository->setUser($account->user);
$count = 0;
$journals = TransactionJournal::leftJoin('transactions', 'transaction_journals.id', '=', 'transactions.transaction_journal_id')->where(
'transactions.account_id',
$account->id
)->get(['transaction_journals.*']);
$direction = $repository->getMetaValue($account, 'liability_direction');
if ('credit' === $direction && $this->hasBadOpening($account)) {
$this->deleteCreditTransaction($account);
$this->reverseOpeningBalance($account);
$this->friendlyInfo(sprintf('Corrected opening balance for liability #%d ("%s")', $account->id, $account->name));
}
if ('credit' === $direction) {
$count = $this->deleteTransactions($account);
if ($count > 0) {
$this->friendlyInfo(sprintf('Removed %d old format transaction(s) for liability #%d ("%s")', $count, $account->id, $account->name));
$service = app(TransactionGroupDestroyService::class);
/** @var TransactionJournal $journal */
foreach ($journals as $journal) {
if (null !== $journal->transactionGroup) {
$service->destroy($journal->transactionGroup);
++$count;
}
}
return $count;
}
private function hasBadOpening(Account $account): bool
@@ -142,19 +123,16 @@ class UpgradesLiabilitiesEight extends Command
return (bool) $openingJournal->date->isSameDay($liabilityJournal->date);
}
private function deleteCreditTransaction(Account $account): void
private function isExecuted(): bool
{
$liabilityType = TransactionType::whereType(TransactionTypeEnum::LIABILITY_CREDIT->value)->first();
$liabilityJournal = TransactionJournal::leftJoin('transactions', 'transactions.transaction_journal_id', '=', 'transaction_journals.id')
->where('transactions.account_id', $account->id)
->where('transaction_journals.transaction_type_id', $liabilityType->id)
->first(['transaction_journals.*'])
;
if (null !== $liabilityJournal && null !== $liabilityJournal->transactionGroup) {
$group = $liabilityJournal->transactionGroup;
$service = new TransactionGroupDestroyService();
$service->destroy($group);
}
$configVar = FireflyConfig::get(self::CONFIG_NAME, false);
return (bool) $configVar?->data;
}
private function markAsExecuted(): void
{
FireflyConfig::set(self::CONFIG_NAME, true);
}
private function reverseOpeningBalance(Account $account): void
@@ -186,29 +164,51 @@ class UpgradesLiabilitiesEight extends Command
Log::warning('Did not find opening balance.');
}
private function deleteTransactions(Account $account): int
private function upgradeForUser(User $user): void
{
$count = 0;
$journals = TransactionJournal::leftJoin('transactions', 'transaction_journals.id', '=', 'transactions.transaction_journal_id')->where(
'transactions.account_id',
$account->id
)->get(['transaction_journals.*']);
$accounts = $user
->accounts()
->leftJoin('account_types', 'account_types.id', '=', 'accounts.account_type_id')
->whereIn('account_types.type', config('firefly.valid_liabilities'))
->get(['accounts.*'])
;
$service = app(TransactionGroupDestroyService::class);
/** @var Account $account */
foreach ($accounts as $account) {
$this->upgradeLiability($account);
$service = app(CreditRecalculateService::class);
$service->setAccount($account);
$service->recalculate();
}
}
/** @var TransactionJournal $journal */
foreach ($journals as $journal) {
if (null !== $journal->transactionGroup) {
$service->destroy($journal->transactionGroup);
++$count;
private function upgradeLiabilities(): void
{
$users = User::get();
/** @var User $user */
foreach ($users as $user) {
$this->upgradeForUser($user);
}
}
private function upgradeLiability(Account $account): void
{
/** @var AccountRepositoryInterface $repository */
$repository = app(AccountRepositoryInterface::class);
$repository->setUser($account->user);
$direction = $repository->getMetaValue($account, 'liability_direction');
if ('credit' === $direction && $this->hasBadOpening($account)) {
$this->deleteCreditTransaction($account);
$this->reverseOpeningBalance($account);
$this->friendlyInfo(sprintf('Corrected opening balance for liability #%d ("%s")', $account->id, $account->name));
}
if ('credit' === $direction) {
$count = $this->deleteTransactions($account);
if ($count > 0) {
$this->friendlyInfo(sprintf('Removed %d old format transaction(s) for liability #%d ("%s")', $count, $account->id, $account->name));
}
}
return $count;
}
private function markAsExecuted(): void
{
FireflyConfig::set(self::CONFIG_NAME, true);
}
}

View File

@@ -70,17 +70,9 @@ class UpgradesMultiPiggyBanks extends Command
return (bool) $configVar?->data;
}
private function upgradePiggyBanks(): void
private function markAsExecuted(): void
{
$this->repository = app(PiggyBankRepositoryInterface::class);
$this->accountRepository = app(AccountRepositoryInterface::class);
$set = PiggyBank::whereNotNull('account_id')->get();
Log::debug(sprintf('Will update %d piggy banks(s).', $set->count()));
/** @var PiggyBank $piggyBank */
foreach ($set as $piggyBank) {
$this->upgradePiggyBank($piggyBank);
}
FireflyConfig::set(self::CONFIG_NAME, true);
}
private function upgradePiggyBank(PiggyBank $piggyBank): void
@@ -109,8 +101,16 @@ class UpgradesMultiPiggyBanks extends Command
$piggyBank->piggyBankRepetitions()->delete();
}
private function markAsExecuted(): void
private function upgradePiggyBanks(): void
{
FireflyConfig::set(self::CONFIG_NAME, true);
$this->repository = app(PiggyBankRepositoryInterface::class);
$this->accountRepository = app(AccountRepositoryInterface::class);
$set = PiggyBank::whereNotNull('account_id')->get();
Log::debug(sprintf('Will update %d piggy banks(s).', $set->count()));
/** @var PiggyBank $piggyBank */
foreach ($set as $piggyBank) {
$this->upgradePiggyBank($piggyBank);
}
}
}

View File

@@ -71,18 +71,9 @@ class UpgradesRecurrenceMetaData extends Command
return (bool) $configVar?->data;
}
private function migrateMetaData(): int
private function markAsExecuted(): void
{
$count = 0;
// get all recurrence meta data:
$collection = RecurrenceMeta::with('recurrence')->get();
/** @var RecurrenceMeta $meta */
foreach ($collection as $meta) {
$count += $this->migrateEntry($meta);
}
return $count;
FireflyConfig::set(self::CONFIG_NAME, true);
}
private function migrateEntry(RecurrenceMeta $meta): int
@@ -109,8 +100,17 @@ class UpgradesRecurrenceMetaData extends Command
return 1;
}
private function markAsExecuted(): void
private function migrateMetaData(): int
{
FireflyConfig::set(self::CONFIG_NAME, true);
$count = 0;
// get all recurrence meta data:
$collection = RecurrenceMeta::with('recurrence')->get();
/** @var RecurrenceMeta $meta */
foreach ($collection as $meta) {
$count += $this->migrateEntry($meta);
}
return $count;
}
}

View File

@@ -68,6 +68,11 @@ class UpgradesRuleActions extends Command
return (bool) $configVar?->data;
}
private function markAsExecuted(): void
{
FireflyConfig::set(self::CONFIG_NAME, true);
}
private function replaceEqualSign(): void
{
$count = 0;
@@ -179,9 +184,4 @@ class UpgradesRuleActions extends Command
}
}
}
private function markAsExecuted(): void
{
FireflyConfig::set(self::CONFIG_NAME, true);
}
}

View File

@@ -56,6 +56,11 @@ class UpgradesTagLocations extends Command
return 0;
}
private function hasLocationDetails(Tag $tag): bool
{
return !in_array(null, [$tag->latitude, $tag->longitude, $tag->zoomLevel], true);
}
private function isExecuted(): bool
{
$configVar = FireflyConfig::get(self::CONFIG_NAME, false);
@@ -63,21 +68,9 @@ class UpgradesTagLocations extends Command
return (bool) $configVar?->data;
}
private function migrateTagLocations(): void
private function markAsExecuted(): void
{
$tags = Tag::get();
/** @var Tag $tag */
foreach ($tags as $tag) {
if ($this->hasLocationDetails($tag)) {
$this->migrateLocationDetails($tag);
}
}
}
private function hasLocationDetails(Tag $tag): bool
{
return !in_array(null, [$tag->latitude, $tag->longitude, $tag->zoomLevel], true);
FireflyConfig::set(self::CONFIG_NAME, true);
}
private function migrateLocationDetails(Tag $tag): void
@@ -95,8 +88,15 @@ class UpgradesTagLocations extends Command
$tag->save();
}
private function markAsExecuted(): void
private function migrateTagLocations(): void
{
FireflyConfig::set(self::CONFIG_NAME, true);
$tags = Tag::get();
/** @var Tag $tag */
foreach ($tags as $tag) {
if ($this->hasLocationDetails($tag)) {
$this->migrateLocationDetails($tag);
}
}
}
}

View File

@@ -83,100 +83,18 @@ class UpgradesToGroups extends Command
return 0;
}
/**
* Laravel will execute ALL __construct() methods for ALL commands whenever a SINGLE command is
* executed. This leads to noticeable slow-downs and class calls. To prevent this, this method should
* be called from the handle method instead of using the constructor to initialize the command.
*/
private function stupidLaravel(): void
private function findOpposingTransaction(TransactionJournal $journal, Transaction $transaction): ?Transaction
{
$this->count = 0;
$this->journalRepository = app(JournalRepositoryInterface::class);
$this->service = app(JournalDestroyService::class);
$this->groupFactory = app(TransactionGroupFactory::class);
$this->cliRepository = app(JournalCLIRepositoryInterface::class);
}
$set = $journal->transactions->filter(static function (Transaction $subject) use ($transaction): bool {
$amount = ((float) $transaction->amount * -1) === (float) $subject->amount; // intentional float
$identifier = $transaction->identifier === $subject->identifier;
Log::debug(sprintf('Amount the same? %s', var_export($amount, true)));
Log::debug(sprintf('ID the same? %s', var_export($identifier, true)));
private function isMigrated(): bool
{
$configVar = FireflyConfig::get(self::CONFIG_NAME, false);
return $amount && $identifier;
});
return (bool) $configVar?->data;
}
/**
* @throws Exception
*/
private function makeGroupsFromSplitJournals(): void
{
$splitJournals = $this->cliRepository->getSplitJournals();
if ($splitJournals->count() > 0) {
$this->friendlyLine(sprintf('Going to convert %d split transaction(s). Please hold..', $splitJournals->count()));
/** @var TransactionJournal $journal */
foreach ($splitJournals as $journal) {
$this->makeMultiGroup($journal);
}
}
}
/**
* @throws Exception
*/
private function makeMultiGroup(TransactionJournal $journal): void
{
// double check transaction count.
if ($journal->transactions->count() <= 2) {
Log::debug(sprintf('Will not try to convert journal #%d because it has 2 or fewer transactions.', $journal->id));
return;
}
Log::debug(sprintf('Will now try to convert journal #%d', $journal->id));
$this->journalRepository->setUser($journal->user);
$this->groupFactory->setUser($journal->user);
$this->cliRepository->setUser($journal->user);
$data = [
// mandatory fields.
'group_title' => $journal->description,
'transactions' => [],
];
$destTransactions = $this->getDestinationTransactions($journal);
Log::debug(sprintf('Will use %d positive transactions to create a new group.', $destTransactions->count()));
/** @var Transaction $transaction */
foreach ($destTransactions as $transaction) {
$data['transactions'][] = $this->generateTransaction($journal, $transaction);
}
Log::debug(sprintf('Now calling transaction journal factory (%d transactions in array)', count($data['transactions'])));
$group = $this->groupFactory->create($data);
Log::debug('Done calling transaction journal factory');
// delete the old transaction journal.
$this->service->destroy($journal);
++$this->count;
// report on result:
Log::debug(sprintf(
'Migrated journal #%d into group #%d with these journals: #%s',
$journal->id,
$group->id,
implode(', #', $group->transactionJournals->pluck('id')->toArray())
));
$this->friendlyInfo(sprintf(
'Migrated journal #%d into group #%d with these journals: #%s',
$journal->id,
$group->id,
implode(', #', $group->transactionJournals->pluck('id')->toArray())
));
}
private function getDestinationTransactions(TransactionJournal $journal): Collection
{
return $journal->transactions->filter(static fn (Transaction $transaction): bool => $transaction->amount > 0);
return $set->first();
}
/**
@@ -268,18 +186,9 @@ class UpgradesToGroups extends Command
];
}
private function findOpposingTransaction(TransactionJournal $journal, Transaction $transaction): ?Transaction
private function getDestinationTransactions(TransactionJournal $journal): Collection
{
$set = $journal->transactions->filter(static function (Transaction $subject) use ($transaction): bool {
$amount = ((float) $transaction->amount * -1) === (float) $subject->amount; // intentional float
$identifier = $transaction->identifier === $subject->identifier;
Log::debug(sprintf('Amount the same? %s', var_export($amount, true)));
Log::debug(sprintf('ID the same? %s', var_export($identifier, true)));
return $amount && $identifier;
});
return $set->first();
return $journal->transactions->filter(static fn (Transaction $transaction): bool => $transaction->amount > 0);
}
private function getTransactionBudget(Transaction $left, Transaction $right): ?int
@@ -336,6 +245,25 @@ class UpgradesToGroups extends Command
return null;
}
private function giveGroup(array $array): void
{
$groupId = DB::table('transaction_groups')->insertGetId([
'created_at' => Carbon::now()->format('Y-m-d H:i:s'),
'updated_at' => Carbon::now()->format('Y-m-d H:i:s'),
'title' => null,
'user_id' => $array['user_id'],
]);
DB::table('transaction_journals')->where('id', $array['id'])->update(['transaction_group_id' => $groupId]);
++$this->count;
}
private function isMigrated(): bool
{
$configVar = FireflyConfig::get(self::CONFIG_NAME, false);
return (bool) $configVar?->data;
}
/**
* Gives all journals without a group a group.
*/
@@ -354,20 +282,92 @@ class UpgradesToGroups extends Command
}
}
private function giveGroup(array $array): void
/**
* @throws Exception
*/
private function makeGroupsFromSplitJournals(): void
{
$groupId = DB::table('transaction_groups')->insertGetId([
'created_at' => Carbon::now()->format('Y-m-d H:i:s'),
'updated_at' => Carbon::now()->format('Y-m-d H:i:s'),
'title' => null,
'user_id' => $array['user_id'],
]);
DB::table('transaction_journals')->where('id', $array['id'])->update(['transaction_group_id' => $groupId]);
$splitJournals = $this->cliRepository->getSplitJournals();
if ($splitJournals->count() > 0) {
$this->friendlyLine(sprintf('Going to convert %d split transaction(s). Please hold..', $splitJournals->count()));
/** @var TransactionJournal $journal */
foreach ($splitJournals as $journal) {
$this->makeMultiGroup($journal);
}
}
}
/**
* @throws Exception
*/
private function makeMultiGroup(TransactionJournal $journal): void
{
// double check transaction count.
if ($journal->transactions->count() <= 2) {
Log::debug(sprintf('Will not try to convert journal #%d because it has 2 or fewer transactions.', $journal->id));
return;
}
Log::debug(sprintf('Will now try to convert journal #%d', $journal->id));
$this->journalRepository->setUser($journal->user);
$this->groupFactory->setUser($journal->user);
$this->cliRepository->setUser($journal->user);
$data = [
// mandatory fields.
'group_title' => $journal->description,
'transactions' => [],
];
$destTransactions = $this->getDestinationTransactions($journal);
Log::debug(sprintf('Will use %d positive transactions to create a new group.', $destTransactions->count()));
/** @var Transaction $transaction */
foreach ($destTransactions as $transaction) {
$data['transactions'][] = $this->generateTransaction($journal, $transaction);
}
Log::debug(sprintf('Now calling transaction journal factory (%d transactions in array)', count($data['transactions'])));
$group = $this->groupFactory->create($data);
Log::debug('Done calling transaction journal factory');
// delete the old transaction journal.
$this->service->destroy($journal);
++$this->count;
// report on result:
Log::debug(sprintf(
'Migrated journal #%d into group #%d with these journals: #%s',
$journal->id,
$group->id,
implode(', #', $group->transactionJournals->pluck('id')->toArray())
));
$this->friendlyInfo(sprintf(
'Migrated journal #%d into group #%d with these journals: #%s',
$journal->id,
$group->id,
implode(', #', $group->transactionJournals->pluck('id')->toArray())
));
}
private function markAsMigrated(): void
{
FireflyConfig::set(self::CONFIG_NAME, true);
}
/**
* Laravel will execute ALL __construct() methods for ALL commands whenever a SINGLE command is
* executed. This leads to noticeable slow-downs and class calls. To prevent this, this method should
* be called from the handle method instead of using the constructor to initialize the command.
*/
private function stupidLaravel(): void
{
$this->count = 0;
$this->journalRepository = app(JournalRepositoryInterface::class);
$this->service = app(JournalDestroyService::class);
$this->groupFactory = app(TransactionGroupFactory::class);
$this->cliRepository = app(JournalCLIRepositoryInterface::class);
}
}

View File

@@ -79,17 +79,303 @@ class UpgradesTransferCurrencies extends Command
}
/**
* Laravel will execute ALL __construct() methods for ALL commands whenever a SINGLE command is
* executed. This leads to noticeable slow-downs and class calls. To prevent this, this method should
* be called from the handle method instead of using the constructor to initialize the command.
* The destination transaction must have the correct currency. If not, it will be set by
* taking it from the destination account's preference.
*/
private function stupidLaravel(): void
private function fixDestinationUnmatchedCurrency(): void
{
$this->count = 0;
$this->accountRepos = app(AccountRepositoryInterface::class);
$this->cliRepos = app(JournalCLIRepositoryInterface::class);
$this->accountCurrencies = [];
$this->resetInformation();
if (
$this->destinationCurrency instanceof TransactionCurrency
&& null === $this->destinationTransaction->foreign_amount
&& (int) $this->destinationTransaction->transaction_currency_id !== $this->destinationCurrency->id
) {
$message = sprintf(
'Transaction #%d has a currency setting #%d that should be #%d. Amount remains %s, currency is changed.',
$this->destinationTransaction->id,
$this->destinationTransaction->transaction_currency_id,
$this->destinationAccount->id,
$this->destinationTransaction->amount
);
$this->friendlyWarning($message);
++$this->count;
$this->destinationTransaction->transaction_currency_id = $this->destinationCurrency->id;
$this->destinationTransaction->save();
}
}
/**
* The destination transaction must have a currency. If not, it will be added by
* taking it from the destination account's preference.
*/
private function fixDestNoCurrency(): void
{
if (null === $this->destinationTransaction->transaction_currency_id && $this->destinationCurrency instanceof TransactionCurrency) {
$this->destinationTransaction->transaction_currency_id = $this->destinationCurrency->id;
$message = sprintf(
'Transaction #%d has no currency setting, now set to %s.',
$this->destinationTransaction->id,
$this->destinationCurrency->code
);
$this->friendlyInfo($message);
++$this->count;
$this->destinationTransaction->save();
}
}
/**
* If the foreign amount of the destination transaction is null, but that of the other isn't, use this piece of code
* to restore it.
*/
private function fixDestNullForeignAmount(): void
{
if (null === $this->destinationTransaction->foreign_amount && null !== $this->sourceTransaction->foreign_amount) {
$this->destinationTransaction->foreign_amount = bcmul($this->sourceTransaction->foreign_amount, '-1');
$this->destinationTransaction->save();
++$this->count;
$this->friendlyInfo(sprintf(
'Restored foreign amount of destination transaction #%d to %s',
$this->destinationTransaction->id,
$this->destinationTransaction->foreign_amount
));
}
}
/**
* If the destination account currency is the same as the source currency,
* both foreign_amount and foreign_currency_id fields must be NULL
* for both transactions (because foreign currency info would not make sense)
*/
private function fixInvalidForeignCurrency(): void
{
if ($this->destinationCurrency->id === $this->sourceCurrency->id) {
// update both transactions to match:
$this->sourceTransaction->foreign_amount = null;
$this->sourceTransaction->foreign_currency_id = null;
$this->destinationTransaction->foreign_amount = null;
$this->destinationTransaction->foreign_currency_id = null;
$this->sourceTransaction->save();
$this->destinationTransaction->save();
}
}
/**
* If destination account currency is different from source account currency,
* then both transactions must get the source account's currency as normal currency
* and the opposing account's currency as foreign currency.
*/
private function fixMismatchedForeignCurrency(): void
{
if ($this->sourceCurrency->id !== $this->destinationCurrency->id) {
$this->sourceTransaction->transaction_currency_id = $this->sourceCurrency->id;
$this->sourceTransaction->foreign_currency_id = $this->destinationCurrency->id;
$this->destinationTransaction->transaction_currency_id = $this->sourceCurrency->id;
$this->destinationTransaction->foreign_currency_id = $this->destinationCurrency->id;
$this->sourceTransaction->save();
$this->destinationTransaction->save();
++$this->count;
$this->friendlyInfo(sprintf(
'Verified foreign currency ID of transaction #%d and #%d',
$this->sourceTransaction->id,
$this->destinationTransaction->id
));
}
}
/**
* The source transaction must have a currency. If not, it will be added by
* taking it from the source account's preference.
*/
private function fixSourceNoCurrency(): void
{
if (null === $this->sourceTransaction->transaction_currency_id && $this->sourceCurrency instanceof TransactionCurrency) {
$this->sourceTransaction->transaction_currency_id = $this->sourceCurrency->id;
$message = sprintf(
'Transaction #%d has no currency setting, now set to %s.',
$this->sourceTransaction->id,
$this->sourceCurrency->code
);
$this->friendlyInfo($message);
++$this->count;
$this->sourceTransaction->save();
}
}
/**
* If the foreign amount of the source transaction is null, but that of the other isn't, use this piece of code
* to restore it.
*/
private function fixSourceNullForeignAmount(): void
{
if (null === $this->sourceTransaction->foreign_amount && null !== $this->destinationTransaction->foreign_amount) {
$this->sourceTransaction->foreign_amount = bcmul($this->destinationTransaction->foreign_amount, '-1');
$this->sourceTransaction->save();
++$this->count;
$this->friendlyInfo(sprintf(
'Restored foreign amount of source transaction #%d to %s',
$this->sourceTransaction->id,
$this->sourceTransaction->foreign_amount
));
}
}
/**
* The source transaction must have the correct currency. If not, it will be set by
* taking it from the source account's preference.
*/
private function fixSourceUnmatchedCurrency(): void
{
if (
$this->sourceCurrency instanceof TransactionCurrency
&& null === $this->sourceTransaction->foreign_amount
&& (int) $this->sourceTransaction->transaction_currency_id !== $this->sourceCurrency->id
) {
$message = sprintf(
'Transaction #%d has a currency setting #%d that should be #%d. Amount remains %s, currency is changed.',
$this->sourceTransaction->id,
$this->sourceTransaction->transaction_currency_id,
$this->sourceAccount->id,
$this->sourceTransaction->amount
);
$this->friendlyWarning($message);
++$this->count;
$this->sourceTransaction->transaction_currency_id = $this->sourceCurrency->id;
$this->sourceTransaction->save();
}
}
/**
* This method makes sure that the transaction journal uses the currency given in the source transaction.
*/
private function fixTransactionJournalCurrency(TransactionJournal $journal): void
{
if ((int) $journal->transaction_currency_id !== $this->sourceCurrency->id) {
$oldCurrencyCode = $journal->transactionCurrency->code ?? '(nothing)';
$journal->transaction_currency_id = $this->sourceCurrency->id;
$message = sprintf(
'Transfer #%d ("%s") has been updated to use %s instead of %s.',
$journal->id,
$journal->description,
$this->sourceCurrency->code,
$oldCurrencyCode
);
++$this->count;
$this->friendlyInfo($message);
$journal->save();
}
}
private function getCurrency(Account $account): ?TransactionCurrency
{
$accountId = $account->id;
if (array_key_exists($accountId, $this->accountCurrencies) && 0 === $this->accountCurrencies[$accountId]) {
return null;
}
if (array_key_exists($accountId, $this->accountCurrencies) && $this->accountCurrencies[$accountId] instanceof TransactionCurrency) {
return $this->accountCurrencies[$accountId];
}
$currency = $this->accountRepos->getAccountCurrency($account);
if (!$currency instanceof TransactionCurrency) {
$this->accountCurrencies[$accountId] = 0;
return null;
}
$this->accountCurrencies[$accountId] = $currency;
return $currency;
}
/**
* Extract destination transaction, destination account + destination account currency from the journal.
*/
private function getDestinationInformation(TransactionJournal $journal): void
{
$this->destinationTransaction = $this->getDestinationTransaction($journal);
$this->destinationAccount = $this->destinationTransaction?->account;
$this->destinationCurrency = $this->destinationAccount instanceof Account ? $this->getCurrency($this->destinationAccount) : null;
}
private function getDestinationTransaction(TransactionJournal $transfer): ?Transaction
{
/** @var null|Transaction */
return $transfer->transactions()->where('amount', '>', 0)->first();
}
/**
* Extract source transaction, source account + source account currency from the journal.
*/
private function getSourceInformation(TransactionJournal $journal): void
{
$this->sourceTransaction = $this->getSourceTransaction($journal);
$this->sourceAccount = $this->sourceTransaction?->account;
$this->sourceCurrency = $this->sourceAccount instanceof Account ? $this->getCurrency($this->sourceAccount) : null;
}
private function getSourceTransaction(TransactionJournal $transfer): ?Transaction
{
/** @var null|Transaction */
return $transfer->transactions()->where('amount', '<', 0)->first();
}
/**
* Is either the source or destination transaction NULL?
*/
private function isEmptyTransactions(): bool
{
return
!$this->sourceTransaction instanceof Transaction
|| !$this->destinationTransaction instanceof Transaction
|| !$this->sourceAccount instanceof Account
|| !$this->destinationAccount instanceof Account;
}
private function isExecuted(): bool
{
$configVar = FireflyConfig::get(self::CONFIG_NAME, false);
return (bool) $configVar?->data;
}
private function isNoCurrencyPresent(): bool
{
// source account must have a currency preference.
if (!$this->sourceCurrency instanceof TransactionCurrency) {
$message = sprintf('Account #%d ("%s") must have currency preference but has none.', $this->sourceAccount->id, $this->sourceAccount->name);
Log::error($message);
$this->friendlyError($message);
return true;
}
// destination account must have a currency preference.
if (!$this->destinationCurrency instanceof TransactionCurrency) {
$message = sprintf(
'Account #%d ("%s") must have currency preference but has none.',
$this->destinationAccount->id,
$this->destinationAccount->name
);
Log::error($message);
$this->friendlyError($message);
return true;
}
return false;
}
/**
* Is this a split transaction journal?
*/
private function isSplitJournal(TransactionJournal $transfer): bool
{
return $transfer->transactions->count() > 2;
}
private function markAsExecuted(): void
{
FireflyConfig::set(self::CONFIG_NAME, true);
}
/**
@@ -105,13 +391,6 @@ class UpgradesTransferCurrencies extends Command
$this->destinationCurrency = null;
}
private function isExecuted(): bool
{
$configVar = FireflyConfig::get(self::CONFIG_NAME, false);
return (bool) $configVar?->data;
}
/**
* This routine verifies that transfers have the correct currency settings for the accounts they are linked to.
* For transfers, this is can be a destructive routine since we FORCE them into a currency setting whether they
@@ -130,6 +409,20 @@ class UpgradesTransferCurrencies extends Command
}
}
/**
* Laravel will execute ALL __construct() methods for ALL commands whenever a SINGLE command is
* executed. This leads to noticeable slow-downs and class calls. To prevent this, this method should
* be called from the handle method instead of using the constructor to initialize the command.
*/
private function stupidLaravel(): void
{
$this->count = 0;
$this->accountRepos = app(AccountRepositoryInterface::class);
$this->cliRepos = app(JournalCLIRepositoryInterface::class);
$this->accountCurrencies = [];
$this->resetInformation();
}
private function updateTransferCurrency(TransactionJournal $transfer): void
{
$this->resetInformation();
@@ -187,297 +480,4 @@ class UpgradesTransferCurrencies extends Command
// fix journal itself:
$this->fixTransactionJournalCurrency($transfer);
}
/**
* Is this a split transaction journal?
*/
private function isSplitJournal(TransactionJournal $transfer): bool
{
return $transfer->transactions->count() > 2;
}
/**
* Extract source transaction, source account + source account currency from the journal.
*/
private function getSourceInformation(TransactionJournal $journal): void
{
$this->sourceTransaction = $this->getSourceTransaction($journal);
$this->sourceAccount = $this->sourceTransaction?->account;
$this->sourceCurrency = $this->sourceAccount instanceof Account ? $this->getCurrency($this->sourceAccount) : null;
}
private function getSourceTransaction(TransactionJournal $transfer): ?Transaction
{
/** @var null|Transaction */
return $transfer->transactions()->where('amount', '<', 0)->first();
}
private function getCurrency(Account $account): ?TransactionCurrency
{
$accountId = $account->id;
if (array_key_exists($accountId, $this->accountCurrencies) && 0 === $this->accountCurrencies[$accountId]) {
return null;
}
if (array_key_exists($accountId, $this->accountCurrencies) && $this->accountCurrencies[$accountId] instanceof TransactionCurrency) {
return $this->accountCurrencies[$accountId];
}
$currency = $this->accountRepos->getAccountCurrency($account);
if (!$currency instanceof TransactionCurrency) {
$this->accountCurrencies[$accountId] = 0;
return null;
}
$this->accountCurrencies[$accountId] = $currency;
return $currency;
}
/**
* Extract destination transaction, destination account + destination account currency from the journal.
*/
private function getDestinationInformation(TransactionJournal $journal): void
{
$this->destinationTransaction = $this->getDestinationTransaction($journal);
$this->destinationAccount = $this->destinationTransaction?->account;
$this->destinationCurrency = $this->destinationAccount instanceof Account ? $this->getCurrency($this->destinationAccount) : null;
}
private function getDestinationTransaction(TransactionJournal $transfer): ?Transaction
{
/** @var null|Transaction */
return $transfer->transactions()->where('amount', '>', 0)->first();
}
/**
* Is either the source or destination transaction NULL?
*/
private function isEmptyTransactions(): bool
{
return
!$this->sourceTransaction instanceof Transaction
|| !$this->destinationTransaction instanceof Transaction
|| !$this->sourceAccount instanceof Account
|| !$this->destinationAccount instanceof Account;
}
private function isNoCurrencyPresent(): bool
{
// source account must have a currency preference.
if (!$this->sourceCurrency instanceof TransactionCurrency) {
$message = sprintf('Account #%d ("%s") must have currency preference but has none.', $this->sourceAccount->id, $this->sourceAccount->name);
Log::error($message);
$this->friendlyError($message);
return true;
}
// destination account must have a currency preference.
if (!$this->destinationCurrency instanceof TransactionCurrency) {
$message = sprintf(
'Account #%d ("%s") must have currency preference but has none.',
$this->destinationAccount->id,
$this->destinationAccount->name
);
Log::error($message);
$this->friendlyError($message);
return true;
}
return false;
}
/**
* The source transaction must have a currency. If not, it will be added by
* taking it from the source account's preference.
*/
private function fixSourceNoCurrency(): void
{
if (null === $this->sourceTransaction->transaction_currency_id && $this->sourceCurrency instanceof TransactionCurrency) {
$this->sourceTransaction->transaction_currency_id = $this->sourceCurrency->id;
$message = sprintf(
'Transaction #%d has no currency setting, now set to %s.',
$this->sourceTransaction->id,
$this->sourceCurrency->code
);
$this->friendlyInfo($message);
++$this->count;
$this->sourceTransaction->save();
}
}
/**
* The source transaction must have the correct currency. If not, it will be set by
* taking it from the source account's preference.
*/
private function fixSourceUnmatchedCurrency(): void
{
if (
$this->sourceCurrency instanceof TransactionCurrency
&& null === $this->sourceTransaction->foreign_amount
&& (int) $this->sourceTransaction->transaction_currency_id !== $this->sourceCurrency->id
) {
$message = sprintf(
'Transaction #%d has a currency setting #%d that should be #%d. Amount remains %s, currency is changed.',
$this->sourceTransaction->id,
$this->sourceTransaction->transaction_currency_id,
$this->sourceAccount->id,
$this->sourceTransaction->amount
);
$this->friendlyWarning($message);
++$this->count;
$this->sourceTransaction->transaction_currency_id = $this->sourceCurrency->id;
$this->sourceTransaction->save();
}
}
/**
* The destination transaction must have a currency. If not, it will be added by
* taking it from the destination account's preference.
*/
private function fixDestNoCurrency(): void
{
if (null === $this->destinationTransaction->transaction_currency_id && $this->destinationCurrency instanceof TransactionCurrency) {
$this->destinationTransaction->transaction_currency_id = $this->destinationCurrency->id;
$message = sprintf(
'Transaction #%d has no currency setting, now set to %s.',
$this->destinationTransaction->id,
$this->destinationCurrency->code
);
$this->friendlyInfo($message);
++$this->count;
$this->destinationTransaction->save();
}
}
/**
* The destination transaction must have the correct currency. If not, it will be set by
* taking it from the destination account's preference.
*/
private function fixDestinationUnmatchedCurrency(): void
{
if (
$this->destinationCurrency instanceof TransactionCurrency
&& null === $this->destinationTransaction->foreign_amount
&& (int) $this->destinationTransaction->transaction_currency_id !== $this->destinationCurrency->id
) {
$message = sprintf(
'Transaction #%d has a currency setting #%d that should be #%d. Amount remains %s, currency is changed.',
$this->destinationTransaction->id,
$this->destinationTransaction->transaction_currency_id,
$this->destinationAccount->id,
$this->destinationTransaction->amount
);
$this->friendlyWarning($message);
++$this->count;
$this->destinationTransaction->transaction_currency_id = $this->destinationCurrency->id;
$this->destinationTransaction->save();
}
}
/**
* If the destination account currency is the same as the source currency,
* both foreign_amount and foreign_currency_id fields must be NULL
* for both transactions (because foreign currency info would not make sense)
*/
private function fixInvalidForeignCurrency(): void
{
if ($this->destinationCurrency->id === $this->sourceCurrency->id) {
// update both transactions to match:
$this->sourceTransaction->foreign_amount = null;
$this->sourceTransaction->foreign_currency_id = null;
$this->destinationTransaction->foreign_amount = null;
$this->destinationTransaction->foreign_currency_id = null;
$this->sourceTransaction->save();
$this->destinationTransaction->save();
}
}
/**
* If destination account currency is different from source account currency,
* then both transactions must get the source account's currency as normal currency
* and the opposing account's currency as foreign currency.
*/
private function fixMismatchedForeignCurrency(): void
{
if ($this->sourceCurrency->id !== $this->destinationCurrency->id) {
$this->sourceTransaction->transaction_currency_id = $this->sourceCurrency->id;
$this->sourceTransaction->foreign_currency_id = $this->destinationCurrency->id;
$this->destinationTransaction->transaction_currency_id = $this->sourceCurrency->id;
$this->destinationTransaction->foreign_currency_id = $this->destinationCurrency->id;
$this->sourceTransaction->save();
$this->destinationTransaction->save();
++$this->count;
$this->friendlyInfo(sprintf(
'Verified foreign currency ID of transaction #%d and #%d',
$this->sourceTransaction->id,
$this->destinationTransaction->id
));
}
}
/**
* If the foreign amount of the source transaction is null, but that of the other isn't, use this piece of code
* to restore it.
*/
private function fixSourceNullForeignAmount(): void
{
if (null === $this->sourceTransaction->foreign_amount && null !== $this->destinationTransaction->foreign_amount) {
$this->sourceTransaction->foreign_amount = bcmul($this->destinationTransaction->foreign_amount, '-1');
$this->sourceTransaction->save();
++$this->count;
$this->friendlyInfo(sprintf(
'Restored foreign amount of source transaction #%d to %s',
$this->sourceTransaction->id,
$this->sourceTransaction->foreign_amount
));
}
}
/**
* If the foreign amount of the destination transaction is null, but that of the other isn't, use this piece of code
* to restore it.
*/
private function fixDestNullForeignAmount(): void
{
if (null === $this->destinationTransaction->foreign_amount && null !== $this->sourceTransaction->foreign_amount) {
$this->destinationTransaction->foreign_amount = bcmul($this->sourceTransaction->foreign_amount, '-1');
$this->destinationTransaction->save();
++$this->count;
$this->friendlyInfo(sprintf(
'Restored foreign amount of destination transaction #%d to %s',
$this->destinationTransaction->id,
$this->destinationTransaction->foreign_amount
));
}
}
/**
* This method makes sure that the transaction journal uses the currency given in the source transaction.
*/
private function fixTransactionJournalCurrency(TransactionJournal $journal): void
{
if ((int) $journal->transaction_currency_id !== $this->sourceCurrency->id) {
$oldCurrencyCode = $journal->transactionCurrency->code ?? '(nothing)';
$journal->transaction_currency_id = $this->sourceCurrency->id;
$message = sprintf(
'Transfer #%d ("%s") has been updated to use %s instead of %s.',
$journal->id,
$journal->description,
$this->sourceCurrency->code,
$oldCurrencyCode
);
++$this->count;
$this->friendlyInfo($message);
$journal->save();
}
}
private function markAsExecuted(): void
{
FireflyConfig::set(self::CONFIG_NAME, true);
}
}

View File

@@ -72,6 +72,99 @@ class UpgradesVariousCurrencyInformation extends Command
return 0;
}
private function getCurrency(Account $account): ?TransactionCurrency
{
$accountId = $account->id;
if (array_key_exists($accountId, $this->accountCurrencies) && 0 === $this->accountCurrencies[$accountId]) {
return null;
}
if (array_key_exists($accountId, $this->accountCurrencies) && $this->accountCurrencies[$accountId] instanceof TransactionCurrency) {
return $this->accountCurrencies[$accountId];
}
$currency = $this->accountRepos->getAccountCurrency($account);
if (!$currency instanceof TransactionCurrency) {
$this->accountCurrencies[$accountId] = 0;
return null;
}
$this->accountCurrencies[$accountId] = $currency;
return $currency;
}
/**
* Gets the transaction that determines the transaction that "leads" and will determine
* the currency to be used by all transactions, and the journal itself.
*/
private function getLeadTransaction(TransactionJournal $journal): ?Transaction
{
/** @var null|Transaction $lead */
$lead = null;
switch ($journal->transactionType->type) {
default:
break;
case TransactionTypeEnum::WITHDRAWAL->value:
$lead = $journal->transactions()->where('amount', '<', 0)->first();
break;
case TransactionTypeEnum::DEPOSIT->value:
$lead = $journal->transactions()->where('amount', '>', 0)->first();
break;
case TransactionTypeEnum::OPENING_BALANCE->value:
// whichever isn't an initial balance account:
$lead = $journal
->transactions()
->leftJoin('accounts', 'transactions.account_id', '=', 'accounts.id')
->leftJoin('account_types', 'accounts.account_type_id', '=', 'account_types.id')
->where('account_types.type', '!=', AccountTypeEnum::INITIAL_BALANCE->value)
->first(['transactions.*'])
;
break;
case TransactionTypeEnum::RECONCILIATION->value:
// whichever isn't the reconciliation account:
$lead = $journal
->transactions()
->leftJoin('accounts', 'transactions.account_id', '=', 'accounts.id')
->leftJoin('account_types', 'accounts.account_type_id', '=', 'account_types.id')
->where('account_types.type', '!=', AccountTypeEnum::RECONCILIATION->value)
->first(['transactions.*'])
;
break;
}
return $lead;
}
private function isExecuted(): bool
{
$configVar = FireflyConfig::get(self::CONFIG_NAME, false);
return (bool) $configVar?->data;
}
private function isMultiCurrency(Account $account): bool
{
$value = $this->accountRepos->getMetaValue($account, 'is_multi_currency');
if (null === $value) {
return false;
}
return '1' === $value;
}
private function markAsExecuted(): void
{
FireflyConfig::set(self::CONFIG_NAME, true);
}
/**
* Laravel will execute ALL __construct() methods for ALL commands whenever a SINGLE command is
* executed. This leads to noticeable slow-downs and class calls. To prevent this, this method should
@@ -86,34 +179,6 @@ class UpgradesVariousCurrencyInformation extends Command
$this->cliRepos = app(JournalCLIRepositoryInterface::class);
}
private function isExecuted(): bool
{
$configVar = FireflyConfig::get(self::CONFIG_NAME, false);
return (bool) $configVar?->data;
}
/**
* This routine verifies that withdrawals, deposits and opening balances have the correct currency settings for
* the accounts they are linked to.
* Both source and destination must match the respective currency preference of the related asset account.
* So FF3 must verify all transactions.
*/
private function updateOtherJournalsCurrencies(): void
{
$set = $this->cliRepos->getAllJournals([
TransactionTypeEnum::WITHDRAWAL->value,
TransactionTypeEnum::DEPOSIT->value,
TransactionTypeEnum::OPENING_BALANCE->value,
TransactionTypeEnum::RECONCILIATION->value,
]);
/** @var TransactionJournal $journal */
foreach ($set as $journal) {
$this->updateJournalCurrency($journal);
}
}
private function updateJournalCurrency(TransactionJournal $journal): void
{
$this->accountRepos->setUser($journal->user);
@@ -166,88 +231,23 @@ class UpgradesVariousCurrencyInformation extends Command
}
/**
* Gets the transaction that determines the transaction that "leads" and will determine
* the currency to be used by all transactions, and the journal itself.
* This routine verifies that withdrawals, deposits and opening balances have the correct currency settings for
* the accounts they are linked to.
* Both source and destination must match the respective currency preference of the related asset account.
* So FF3 must verify all transactions.
*/
private function getLeadTransaction(TransactionJournal $journal): ?Transaction
private function updateOtherJournalsCurrencies(): void
{
/** @var null|Transaction $lead */
$lead = null;
$set = $this->cliRepos->getAllJournals([
TransactionTypeEnum::WITHDRAWAL->value,
TransactionTypeEnum::DEPOSIT->value,
TransactionTypeEnum::OPENING_BALANCE->value,
TransactionTypeEnum::RECONCILIATION->value,
]);
switch ($journal->transactionType->type) {
default:
break;
case TransactionTypeEnum::WITHDRAWAL->value:
$lead = $journal->transactions()->where('amount', '<', 0)->first();
break;
case TransactionTypeEnum::DEPOSIT->value:
$lead = $journal->transactions()->where('amount', '>', 0)->first();
break;
case TransactionTypeEnum::OPENING_BALANCE->value:
// whichever isn't an initial balance account:
$lead = $journal
->transactions()
->leftJoin('accounts', 'transactions.account_id', '=', 'accounts.id')
->leftJoin('account_types', 'accounts.account_type_id', '=', 'account_types.id')
->where('account_types.type', '!=', AccountTypeEnum::INITIAL_BALANCE->value)
->first(['transactions.*'])
;
break;
case TransactionTypeEnum::RECONCILIATION->value:
// whichever isn't the reconciliation account:
$lead = $journal
->transactions()
->leftJoin('accounts', 'transactions.account_id', '=', 'accounts.id')
->leftJoin('account_types', 'accounts.account_type_id', '=', 'account_types.id')
->where('account_types.type', '!=', AccountTypeEnum::RECONCILIATION->value)
->first(['transactions.*'])
;
break;
/** @var TransactionJournal $journal */
foreach ($set as $journal) {
$this->updateJournalCurrency($journal);
}
return $lead;
}
private function getCurrency(Account $account): ?TransactionCurrency
{
$accountId = $account->id;
if (array_key_exists($accountId, $this->accountCurrencies) && 0 === $this->accountCurrencies[$accountId]) {
return null;
}
if (array_key_exists($accountId, $this->accountCurrencies) && $this->accountCurrencies[$accountId] instanceof TransactionCurrency) {
return $this->accountCurrencies[$accountId];
}
$currency = $this->accountRepos->getAccountCurrency($account);
if (!$currency instanceof TransactionCurrency) {
$this->accountCurrencies[$accountId] = 0;
return null;
}
$this->accountCurrencies[$accountId] = $currency;
return $currency;
}
private function isMultiCurrency(Account $account): bool
{
$value = $this->accountRepos->getMetaValue($account, 'is_multi_currency');
if (null === $value) {
return false;
}
return '1' === $value;
}
private function markAsExecuted(): void
{
FireflyConfig::set(self::CONFIG_NAME, true);
}
}

View File

@@ -69,18 +69,9 @@ class UpgradesWebhooks extends Command
return (bool) $configVar?->data;
}
private function upgradeWebhooks(): void
private function markAsExecuted(): void
{
$set = Webhook::where('delivery', '>', 1)
->orWhere('trigger', '>', 1)
->orWhere('response', '>', 1)
->get()
;
/** @var Webhook $webhook */
foreach ($set as $webhook) {
$this->upgradeWebhook($webhook);
}
FireflyConfig::set(self::CONFIG_NAME, true);
}
private function upgradeWebhook(Webhook $webhook): void
@@ -111,8 +102,17 @@ class UpgradesWebhooks extends Command
$this->friendlyPositive(sprintf('Webhook #%d upgraded.', $webhook->id));
}
private function markAsExecuted(): void
private function upgradeWebhooks(): void
{
FireflyConfig::set(self::CONFIG_NAME, true);
$set = Webhook::where('delivery', '>', 1)
->orWhere('trigger', '>', 1)
->orWhere('response', '>', 1)
->get()
;
/** @var Webhook $webhook */
foreach ($set as $webhook) {
$this->upgradeWebhook($webhook);
}
}
}

View File

@@ -37,6 +37,15 @@ use Illuminate\Support\Facades\Log;
*/
trait VerifiesAccessToken
{
/**
* Abstract method to make sure trait knows about method "option".
*
* @param null|string $key
*
* @return mixed
*/
abstract public function option($key = null);
/**
* @throws FireflyException
*/
@@ -54,15 +63,6 @@ trait VerifiesAccessToken
return $user;
}
/**
* Abstract method to make sure trait knows about method "option".
*
* @param null|string $key
*
* @return mixed
*/
abstract public function option($key = null);
/**
* Returns false when given token does not match given user token.
*/

View File

@@ -1,8 +1,10 @@
<?php
declare(strict_types=1);
/*
* UpdatedAccount.php
* Copyright (c) 2021 james@firefly-iii.org
* CreatedNewAccount.php
* Copyright (c) 2026 james@firefly-iii.org
*
* This file is part of Firefly III (https://github.com/firefly-iii).
*
@@ -20,17 +22,13 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
declare(strict_types=1);
namespace FireflyIII\Events;
namespace FireflyIII\Events\Model\Account;
use FireflyIII\Events\Event;
use FireflyIII\Models\Account;
use Illuminate\Queue\SerializesModels;
/**
* Class UpdatedAccount
*/
class UpdatedAccount extends Event
class CreatedNewAccount extends Event
{
use SerializesModels;

View File

@@ -1,8 +1,10 @@
<?php
declare(strict_types=1);
/*
* StoredAccount.php
* Copyright (c) 2021 james@firefly-iii.org
* UpdatedExistingAccount.php
* Copyright (c) 2026 james@firefly-iii.org
*
* This file is part of Firefly III (https://github.com/firefly-iii).
*
@@ -20,17 +22,13 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
declare(strict_types=1);
namespace FireflyIII\Events;
namespace FireflyIII\Events\Model\Account;
use FireflyIII\Events\Event;
use FireflyIII\Models\Account;
use Illuminate\Queue\SerializesModels;
/**
* Class StoredAccount
*/
class StoredAccount extends Event
class UpdatedExistingAccount extends Event
{
use SerializesModels;

View File

@@ -0,0 +1,39 @@
<?php
declare(strict_types=1);
/*
* CreatedBudget.php
* Copyright (c) 2026 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\Events\Model\Budget;
use FireflyIII\Events\Event;
use FireflyIII\Models\Budget;
use Illuminate\Queue\SerializesModels;
class CreatedBudget extends Event
{
use SerializesModels;
public function __construct(
public Budget $budget,
public bool $createWebhookMessages
) {}
}

View File

@@ -0,0 +1,39 @@
<?php
declare(strict_types=1);
/*
* DestroyedBudget.php
* Copyright (c) 2026 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\Events\Model\Budget;
use FireflyIII\Events\Event;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Log;
class DestroyedBudget extends Event
{
use SerializesModels;
public function __construct()
{
Log::debug('Created event DestroyedBudget');
}
}

View File

@@ -1,8 +1,10 @@
<?php
/**
* DestroyedTransactionGroup.php
* Copyright (c) 2019 james@firefly-iii.org
declare(strict_types=1);
/*
* DestroyingBudget.php
* Copyright (c) 2026 james@firefly-iii.org
*
* This file is part of Firefly III (https://github.com/firefly-iii).
*
@@ -20,27 +22,20 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
declare(strict_types=1);
namespace FireflyIII\Events\Model\Budget;
namespace FireflyIII\Events;
use FireflyIII\Models\TransactionGroup;
use FireflyIII\Events\Event;
use FireflyIII\Models\Budget;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Log;
/**
* Class DestroyedTransactionGroup.
*/
class DestroyedTransactionGroup extends Event
class DestroyingBudget extends Event
{
use SerializesModels;
/**
* Create a new event instance.
*/
public function __construct(
public TransactionGroup $transactionGroup
public Budget $budget
) {
Log::debug(sprintf('Now in %s', __METHOD__));
Log::debug(sprintf('Created event DestroyingBudget(#%d)', $budget->id));
}
}

View File

@@ -0,0 +1,39 @@
<?php
declare(strict_types=1);
/*
* UpdatedBudget.php
* Copyright (c) 2026 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\Events\Model\Budget;
use FireflyIII\Events\Event;
use FireflyIII\Models\Budget;
use Illuminate\Queue\SerializesModels;
class UpdatedBudget extends Event
{
use SerializesModels;
public function __construct(
public Budget $budget,
public bool $createWebhookMessages
) {}
}

View File

@@ -0,0 +1,39 @@
<?php
declare(strict_types=1);
/*
* CreatedBudgetLimit.php
* Copyright (c) 2026 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\Events\Model\BudgetLimit;
use FireflyIII\Events\Event;
use FireflyIII\Models\BudgetLimit;
use Illuminate\Queue\SerializesModels;
class CreatedBudgetLimit extends Event
{
use SerializesModels;
public function __construct(
public BudgetLimit $budgetLimit,
public bool $createWebhookMessages
) {}
}

View File

@@ -0,0 +1,44 @@
<?php
declare(strict_types=1);
/*
* DestroyedBudgetLimit.php
* Copyright (c) 2026 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\Events\Model\BudgetLimit;
use Carbon\Carbon;
use FireflyIII\Events\Event;
use FireflyIII\Models\Budget;
use FireflyIII\User;
use Illuminate\Queue\SerializesModels;
class DestroyedBudgetLimit extends Event
{
use SerializesModels;
public function __construct(
public User $user,
public Budget $budget,
public Carbon $start,
public Carbon $end,
public bool $createWebhookMessages
) {}
}

View File

@@ -0,0 +1,39 @@
<?php
declare(strict_types=1);
/*
* UpdatedBudgetLimit.php
* Copyright (c) 2026 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\Events\Model\BudgetLimit;
use FireflyIII\Events\Event;
use FireflyIII\Models\BudgetLimit;
use Illuminate\Queue\SerializesModels;
class UpdatedBudgetLimit extends Event
{
use SerializesModels;
public function __construct(
public BudgetLimit $budgetLimit,
public bool $createWebhookMessages
) {}
}

View File

@@ -25,9 +25,7 @@ declare(strict_types=1);
namespace FireflyIII\Events\Model\TransactionGroup;
use FireflyIII\Events\Event;
use FireflyIII\Models\TransactionGroup;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Log;
class CreatedSingleTransactionGroup extends Event
{
@@ -37,9 +35,7 @@ class CreatedSingleTransactionGroup extends Event
* Create a new event instance.
*/
public function __construct(
public TransactionGroup $transactionGroup,
public TransactionGroupEventFlags $flags
) {
Log::debug(__METHOD__);
}
public TransactionGroupEventFlags $flags,
public TransactionGroupEventObjects $objects
) {}
}

View File

@@ -1,8 +1,10 @@
<?php
/**
* StoredTransactionGroup.php
* Copyright (c) 2019 james@firefly-iii.org
declare(strict_types=1);
/*
* DestroyedSingleTransactionGroup.php
* Copyright (c) 2026 james@firefly-iii.org
*
* This file is part of Firefly III (https://github.com/firefly-iii).
*
@@ -20,17 +22,12 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
declare(strict_types=1);
namespace FireflyIII\Events\Model\TransactionGroup;
namespace FireflyIII\Events;
use FireflyIII\Models\TransactionGroup;
use FireflyIII\Events\Event;
use Illuminate\Queue\SerializesModels;
/**
* Class StoredTransactionGroup.
*/
class StoredTransactionGroup extends Event
class DestroyedSingleTransactionGroup extends Event
{
use SerializesModels;
@@ -38,8 +35,7 @@ class StoredTransactionGroup extends Event
* Create a new event instance.
*/
public function __construct(
public TransactionGroup $transactionGroup,
public bool $applyRules,
public bool $fireWebhooks
public TransactionGroupEventFlags $flags,
public TransactionGroupEventObjects $objects
) {}
}

View File

@@ -0,0 +1,72 @@
<?php
declare(strict_types=1);
namespace FireflyIII\Events\Model\TransactionGroup;
use FireflyIII\Models\Transaction;
use FireflyIII\Models\TransactionGroup;
use FireflyIII\Models\TransactionJournal;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Log;
/**
* This class collects all objects before and after the creation, removal or updating
* of a transaction group. The idea is that this class contains all relevant objects.
* Right now, that means journals, tags, accounts, budgets and categories.
*
* By collecting these objects (in case of an update: before AND after update) there
* is a unified set of objects to manage: update balances, recalculate credits, etc.
*/
class TransactionGroupEventObjects
{
public Collection $accounts;
public Collection $budgets;
public Collection $categories;
public Collection $tags;
public Collection $transactionGroups;
public Collection $transactionJournals;
public function __construct()
{
$this->accounts = new Collection();
$this->budgets = new Collection();
$this->categories = new Collection();
$this->tags = new Collection();
$this->transactionGroups = new Collection();
$this->transactionJournals = new Collection();
}
public static function collectFromTransactionGroup(TransactionGroup $transactionGroup): self
{
Log::debug(sprintf('collectFromTransactionGroup(#%d)', $transactionGroup->id));
$object = new self();
$object->appendFromTransactionGroup($transactionGroup);
return $object;
}
public function appendFromTransactionGroup(TransactionGroup $transactionGroup): void
{
$this->transactionGroups->push($transactionGroup);
/** @var TransactionJournal $journal */
foreach ($transactionGroup->transactionJournals as $journal) {
$this->transactionJournals->push($journal);
$this->budgets = $this->budgets->merge($journal->budgets);
$this->categories = $this->categories->merge($journal->categories);
$this->tags = $this->tags->merge($journal->tags);
/** @var Transaction $transaction */
foreach ($journal->transactions as $transaction) {
$this->accounts->push($transaction->account);
}
}
$this->transactionGroups = $this->transactionGroups->unique('id');
$this->transactionJournals = $this->transactionJournals->unique('id');
$this->budgets = $this->budgets->unique('id');
$this->categories = $this->categories->unique('id');
$this->tags = $this->tags->unique('id');
$this->accounts = $this->accounts->unique('id');
}
}

View File

@@ -1,47 +0,0 @@
<?php
declare(strict_types=1);
/*
* TriggeredStoredTransactionGroup.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\Events\Model\TransactionGroup;
use FireflyIII\Events\Event;
use FireflyIII\Models\RuleGroup;
use FireflyIII\Models\TransactionGroup;
use Illuminate\Queue\SerializesModels;
class TriggeredStoredTransactionGroup extends Event
{
use SerializesModels;
public ?RuleGroup $ruleGroup = null;
/**
* Create a new event instance.
*/
public function __construct(
public TransactionGroup $transactionGroup,
?RuleGroup $ruleGroup = null
) {
$this->ruleGroup = $ruleGroup;
}
}

View File

@@ -3,7 +3,7 @@
declare(strict_types=1);
/*
* CreatedTransactionGroupBatch.php
* UpdatedSingleTransactionGroup.php
* Copyright (c) 2026 james@firefly-iii.org
*
* This file is part of Firefly III (https://github.com/firefly-iii).
@@ -26,9 +26,8 @@ namespace FireflyIII\Events\Model\TransactionGroup;
use FireflyIII\Events\Event;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Collection;
class CreatedTransactionGroupInBatch extends Event
class UpdatedSingleTransactionGroup extends Event
{
use SerializesModels;
@@ -36,7 +35,7 @@ class CreatedTransactionGroupInBatch extends Event
* Create a new event instance.
*/
public function __construct(
public Collection $collection,
public array $flags
public TransactionGroupEventFlags $flags,
public TransactionGroupEventObjects $objects
) {}
}

View File

@@ -0,0 +1,37 @@
<?php
declare(strict_types=1);
/*
* UserRequestedBatchProcessing.php
* Copyright (c) 2026 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\Events\Model\TransactionGroup;
use FireflyIII\Events\Event;
use Illuminate\Support\Facades\Log;
class UserRequestedBatchProcessing extends Event
{
public function __construct(
public TransactionGroupEventFlags $flags
) {
Log::debug(__METHOD__);
}
}

View File

@@ -1,46 +0,0 @@
<?php
/**
* UpdatedTransactionGroup.php
* Copyright (c) 2019 james@firefly-iii.org
*
* This file is part of Firefly III (https://github.com/firefly-iii).
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
declare(strict_types=1);
namespace FireflyIII\Events;
use FireflyIII\Models\TransactionGroup;
use Illuminate\Queue\SerializesModels;
/**
* Class UpdatedTransactionGroup.
*/
class UpdatedTransactionGroup extends Event
{
use SerializesModels;
/**
* Create a new event instance.
*/
public function __construct(
public TransactionGroup $transactionGroup,
public bool $applyRules,
public bool $fireWebhooks,
public bool $runRecalculations
) {}
}

View File

@@ -181,46 +181,6 @@ class GracefulNotFoundHandler extends ExceptionHandler
return redirect(route('accounts.index', [$shortType]));
}
/**
* @return Response
*
* @throws Throwable
*/
private function handleGroup(Request $request, Throwable $exception)
{
Log::debug('404 page is probably a deleted group. Redirect to overview of group types.');
/** @var User $user */
$user = auth()->user();
$route = $request->route();
$param = $route->parameter('transactionGroup');
$groupId = is_object($param) ? 0 : (int) $param;
/** @var null|TransactionGroup $group */
$group = $user->transactionGroups()->withTrashed()->find($groupId);
if (null === $group) {
Log::error(sprintf('Could not find group %d, so give big fat error.', $groupId));
return parent::render($request, $exception);
}
/** @var null|TransactionJournal $journal */
$journal = $group->transactionJournals()->withTrashed()->first();
if (null === $journal) {
Log::error(sprintf('Could not find journal for group %d, so give big fat error.', $groupId));
return parent::render($request, $exception);
}
$type = $journal->transactionType->type;
$request->session()->reflash();
if (TransactionTypeEnum::RECONCILIATION->value === $type) {
return redirect(route('accounts.index', ['asset']));
}
return redirect(route('transactions.index', [strtolower((string) $type)]));
}
/**
* @return Response
*
@@ -265,4 +225,44 @@ class GracefulNotFoundHandler extends ExceptionHandler
return parent::render($request, $exception);
}
/**
* @return Response
*
* @throws Throwable
*/
private function handleGroup(Request $request, Throwable $exception)
{
Log::debug('404 page is probably a deleted group. Redirect to overview of group types.');
/** @var User $user */
$user = auth()->user();
$route = $request->route();
$param = $route->parameter('transactionGroup');
$groupId = is_object($param) ? 0 : (int) $param;
/** @var null|TransactionGroup $group */
$group = $user->transactionGroups()->withTrashed()->find($groupId);
if (null === $group) {
Log::error(sprintf('Could not find group %d, so give big fat error.', $groupId));
return parent::render($request, $exception);
}
/** @var null|TransactionJournal $journal */
$journal = $group->transactionJournals()->withTrashed()->first();
if (null === $journal) {
Log::error(sprintf('Could not find journal for group %d, so give big fat error.', $groupId));
return parent::render($request, $exception);
}
$type = $journal->transactionType->type;
$request->session()->reflash();
if (TransactionTypeEnum::RECONCILIATION->value === $type) {
return redirect(route('accounts.index', ['asset']));
}
return redirect(route('transactions.index', [strtolower((string) $type)]));
}
}

View File

@@ -31,6 +31,7 @@ use FireflyIII\Jobs\MailError;
use FireflyIII\Support\Facades\Steam;
use Illuminate\Auth\Access\AuthorizationException;
use Illuminate\Auth\AuthenticationException;
use Illuminate\Database\Eloquent\ModelNotFoundException;
use Illuminate\Database\QueryException;
use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler;
use Illuminate\Http\JsonResponse;
@@ -71,6 +72,7 @@ class Handler extends ExceptionHandler
AuthenticationException::class,
LaravelValidationException::class,
NotFoundHttpException::class,
ModelNotFoundException::class,
GoneHttpException::class,
OAuthServerException::class,
LaravelOAuthException::class,
@@ -258,11 +260,6 @@ class Handler extends ExceptionHandler
parent::report($e);
}
private function shouldntReportLocal(Throwable $e): bool
{
return null !== Arr::first($this->dontReport, static fn ($type): bool => $e instanceof $type);
}
/**
* Convert a validation exception into a response.
*
@@ -296,4 +293,9 @@ class Handler extends ExceptionHandler
return null !== $previousHost && $previousHost === $safeHost ? $previous : $safe;
}
private function shouldntReportLocal(Throwable $e): bool
{
return null !== Arr::first($this->dontReport, static fn ($type): bool => $e instanceof $type);
}
}

View File

@@ -25,7 +25,7 @@ declare(strict_types=1);
namespace FireflyIII\Factory;
use FireflyIII\Enums\AccountTypeEnum;
use FireflyIII\Events\StoredAccount;
use FireflyIII\Events\Model\Account\CreatedNewAccount;
use FireflyIII\Exceptions\FireflyException;
use FireflyIII\Models\Account;
use FireflyIII\Models\AccountType;
@@ -67,6 +67,43 @@ class AccountFactory
$this->validFields = config('firefly.valid_account_fields');
}
/**
* @throws FireflyException
*/
public function create(array $data): Account
{
Log::debug('Now in AccountFactory::create()');
$type = $this->getAccountType($data);
$data['iban'] = $this->filterIban($data['iban'] ?? null);
// account may exist already:
$return = $this->find($data['name'], $type->type);
if ($return instanceof Account) {
return $return;
}
$return = $this->createAccount($type, $data);
event(new CreatedNewAccount($return));
return $return;
}
public function find(string $accountName, string $accountType): ?Account
{
Log::debug(sprintf('Now in AccountFactory::find("%s", "%s")', $accountName, $accountType));
$type = AccountType::whereType($accountType)->first();
/** @var null|Account */
return $this->user
->accounts()
->where('account_type_id', $type->id)
->where('name', $accountName)
->first()
;
}
/**
* @throws FireflyException
*/
@@ -104,27 +141,10 @@ class AccountFactory
return $return;
}
/**
* @throws FireflyException
*/
public function create(array $data): Account
public function setUser(User $user): void
{
Log::debug('Now in AccountFactory::create()');
$type = $this->getAccountType($data);
$data['iban'] = $this->filterIban($data['iban'] ?? null);
// account may exist already:
$return = $this->find($data['name'], $type->type);
if ($return instanceof Account) {
return $return;
}
$return = $this->createAccount($type, $data);
event(new StoredAccount($return));
return $return;
$this->user = $user;
$this->accountRepository->setUser($user);
}
/**
@@ -160,18 +180,28 @@ class AccountFactory
return $result;
}
public function find(string $accountName, string $accountType): ?Account
/**
* @throws FireflyException
*/
private function cleanMetaDataArray(Account $account, array $data): array
{
Log::debug(sprintf('Now in AccountFactory::find("%s", "%s")', $accountName, $accountType));
$type = AccountType::whereType($accountType)->first();
$currencyId = array_key_exists('currency_id', $data) ? (int) $data['currency_id'] : 0;
$currencyCode = array_key_exists('currency_code', $data) ? (string) $data['currency_code'] : '';
$accountRole = array_key_exists('account_role', $data) ? (string) $data['account_role'] : null;
$currency = $this->getCurrency($currencyId, $currencyCode);
/** @var null|Account */
return $this->user
->accounts()
->where('account_type_id', $type->id)
->where('name', $accountName)
->first()
;
// only asset account may have a role:
if (AccountTypeEnum::ASSET->value !== $account->accountType->type) {
$accountRole = '';
}
// only liability may have direction:
if (array_key_exists('liability_direction', $data) && !in_array($account->accountType->type, config('firefly.valid_liabilities'), true)) {
$data['liability_direction'] = null;
}
$data['account_role'] = $accountRole;
$data['currency_id'] = $currency->id;
return $data;
}
/**
@@ -245,25 +275,27 @@ class AccountFactory
/**
* @throws FireflyException
*/
private function cleanMetaDataArray(Account $account, array $data): array
private function storeCreditLiability(Account $account, array $data): void
{
$currencyId = array_key_exists('currency_id', $data) ? (int) $data['currency_id'] : 0;
$currencyCode = array_key_exists('currency_code', $data) ? (string) $data['currency_code'] : '';
$accountRole = array_key_exists('account_role', $data) ? (string) $data['account_role'] : null;
$currency = $this->getCurrency($currencyId, $currencyCode);
// only asset account may have a role:
if (AccountTypeEnum::ASSET->value !== $account->accountType->type) {
$accountRole = '';
Log::debug('storeCreditLiability');
$account->refresh();
$accountType = $account->accountType->type;
$direction = $this->accountRepository->getMetaValue($account, 'liability_direction');
$valid = config('firefly.valid_liabilities');
if (in_array($accountType, $valid, true)) {
Log::debug('Is a liability with credit ("i am owed") direction.');
if ($this->validOBData($data)) {
Log::debug('Has valid CL data.');
$openingBalance = $data['opening_balance'];
$openingBalanceDate = $data['opening_balance_date'];
// store credit transaction.
$this->updateCreditTransaction($account, $direction, $openingBalance, $openingBalanceDate);
}
if (!$this->validOBData($data)) {
Log::debug('Does NOT have valid CL data, deletr any CL transaction.');
$this->deleteCreditTransaction($account);
}
}
// only liability may have direction:
if (array_key_exists('liability_direction', $data) && !in_array($account->accountType->type, config('firefly.valid_liabilities'), true)) {
$data['liability_direction'] = null;
}
$data['account_role'] = $accountRole;
$data['currency_id'] = $currency->id;
return $data;
}
private function storeMetaData(Account $account, array $data): void
@@ -324,32 +356,6 @@ class AccountFactory
}
}
/**
* @throws FireflyException
*/
private function storeCreditLiability(Account $account, array $data): void
{
Log::debug('storeCreditLiability');
$account->refresh();
$accountType = $account->accountType->type;
$direction = $this->accountRepository->getMetaValue($account, 'liability_direction');
$valid = config('firefly.valid_liabilities');
if (in_array($accountType, $valid, true)) {
Log::debug('Is a liability with credit ("i am owed") direction.');
if ($this->validOBData($data)) {
Log::debug('Has valid CL data.');
$openingBalance = $data['opening_balance'];
$openingBalanceDate = $data['opening_balance_date'];
// store credit transaction.
$this->updateCreditTransaction($account, $direction, $openingBalance, $openingBalanceDate);
}
if (!$this->validOBData($data)) {
Log::debug('Does NOT have valid CL data, deletr any CL transaction.');
$this->deleteCreditTransaction($account);
}
}
}
/**
* @throws FireflyException
*/
@@ -370,10 +376,4 @@ class AccountFactory
$updateService->setUser($account->user);
$updateService->update($account, ['order' => $order]);
}
public function setUser(User $user): void
{
$this->user = $user;
$this->accountRepository->setUser($user);
}
}

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