Compare commits

...

126 Commits

Author SHA1 Message Date
github-actions
9ecb414b02 Auto commit for release 'develop' on 2024-03-14 2024-03-14 01:29:53 +01:00
James Cole
ad4f908c24 CI will stop complaining about code base, bi-weekly release picks this up. 2024-03-13 06:52:16 +01:00
James Cole
025f739442 Reformat some code. 2024-03-13 06:51:31 +01:00
James Cole
6df7354c48 Rebuild frontend cause lazy. 2024-03-13 06:51:22 +01:00
James Cole
3f77c845ca Add last activity column 2024-03-13 06:50:08 +01:00
James Cole
d4771f7a5c Remove old configuration file. 2024-03-13 06:29:39 +01:00
James Cole
ec4e2bfa4f Fix https://github.com/firefly-iii/firefly-iii/issues/8663 2024-03-12 20:36:31 +01:00
github-actions
dfdbfae4b5 Auto commit for release 'develop' on 2024-03-11 2024-03-11 06:17:46 +01:00
James Cole
349d38b956 Merge branch 'main' into develop 2024-03-11 06:12:38 +01:00
James Cole
2267aa3ac4 Fix workflow 2024-03-11 06:12:29 +01:00
James Cole
2323aa454e Add git keep 2024-03-11 06:10:17 +01:00
James Cole
8b3317b665 Merge pull request #8658 from firefly-iii/dependabot/composer/develop/symfony/expression-language-7.0.3 2024-03-11 05:26:48 +01:00
James Cole
15f893c343 Merge pull request #8657 from firefly-iii/dependabot/npm_and_yarn/develop/alpinejs-3.13.7 2024-03-11 05:26:21 +01:00
James Cole
309b3e765e Merge pull request #8656 from firefly-iii/dependabot/npm_and_yarn/develop/i18next-23.10.1 2024-03-11 05:26:12 +01:00
dependabot[bot]
d3fad06e00 Bump symfony/expression-language from 6.4.3 to 7.0.3
Bumps [symfony/expression-language](https://github.com/symfony/expression-language) from 6.4.3 to 7.0.3.
- [Release notes](https://github.com/symfony/expression-language/releases)
- [Changelog](https://github.com/symfony/expression-language/blob/7.0/CHANGELOG.md)
- [Commits](https://github.com/symfony/expression-language/compare/v6.4.3...v7.0.3)

---
updated-dependencies:
- dependency-name: symfony/expression-language
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-03-11 03:55:59 +00:00
dependabot[bot]
834f24c99c Bump alpinejs from 3.13.6 to 3.13.7
Bumps [alpinejs](https://github.com/alpinejs/alpine/tree/HEAD/packages/alpinejs) from 3.13.6 to 3.13.7.
- [Release notes](https://github.com/alpinejs/alpine/releases)
- [Commits](https://github.com/alpinejs/alpine/commits/v3.13.7/packages/alpinejs)

---
updated-dependencies:
- dependency-name: alpinejs
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-03-11 03:39:56 +00:00
dependabot[bot]
35291e1298 Bump i18next from 23.10.0 to 23.10.1
Bumps [i18next](https://github.com/i18next/i18next) from 23.10.0 to 23.10.1.
- [Release notes](https://github.com/i18next/i18next/releases)
- [Changelog](https://github.com/i18next/i18next/blob/master/CHANGELOG.md)
- [Commits](https://github.com/i18next/i18next/compare/v23.10.0...v23.10.1)

---
updated-dependencies:
- dependency-name: i18next
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-03-11 03:39:44 +00:00
James Cole
ac4e9dcbc5 Code cleanup. 2024-03-10 17:15:38 +01:00
James Cole
d57806f2ba Drop hashes 2024-03-10 16:52:59 +01:00
James Cole
3b005c317d Remove 'strict-dynamic' 2024-03-10 16:49:16 +01:00
James Cole
e91903fed2 Different orderRemove self 2024-03-10 16:47:59 +01:00
James Cole
fee2002b0f Remove self 2024-03-10 16:47:36 +01:00
James Cole
f12e502eb8 Fix header 2024-03-10 16:46:33 +01:00
James Cole
24e62b1cee Fix header 2024-03-10 16:45:19 +01:00
James Cole
f559ec73e0 Add exception catch. 2024-03-10 16:44:41 +01:00
James Cole
530b501fcf Disable engine. [skip ci] 2024-03-10 11:57:54 +01:00
James Cole
d5ea78025e Fix a few small bugs and rearrange code. 2024-03-10 11:57:21 +01:00
James Cole
3413b9b5b5 Refresh notes in various actions. 2024-03-10 08:11:58 +01:00
James Cole
0b45c1aa76 Better validation, can now also use notes in expression. 2024-03-10 08:08:26 +01:00
James Cole
5718d1690a Add debug logging 2024-03-10 08:07:47 +01:00
James Cole
67b16cc070 Overrule "constant" and "enum" actions. 2024-03-10 06:46:38 +01:00
James Cole
5746ac3247 Add feature flag for expression engine and disable it by default. 2024-03-10 06:46:24 +01:00
James Cole
8a2c520b11 Update packages 2024-03-10 06:29:24 +01:00
James Cole
f46c14df8c Validation over GET, take precedence over other routes 2024-03-10 06:29:15 +01:00
James Cole
009fbba491 Drop "failedValidation" method because this is handled by the system already. 2024-03-10 06:28:58 +01:00
James Cole
53d84347c2 sprintf the rules 2024-03-10 06:24:32 +01:00
James Cole
1961487055 Reformat code. 2024-03-10 06:17:31 +01:00
James Cole
c9ce5df74b Merge pull request #8650 from michaelhthomas/feat/expression-engine
[feat] Rules Expression Engine
2024-03-10 06:04:06 +01:00
Michael Thomas
1371b6773e chore: ignore PHPMD unused parameter errors 2024-03-09 14:09:36 -05:00
James Cole
b9f1baf150 Update packages 2024-03-09 19:50:46 +01:00
James Cole
66b322e844 Fix methods and clean up code. 2024-03-09 19:46:16 +01:00
James Cole
487b65b669 Rebuild frontend. 2024-03-09 19:33:43 +01:00
James Cole
9078781d61 New endpoint, fixed logo, better account overview. 2024-03-09 19:31:27 +01:00
Michael Thomas
1ec830521a fix: resolve PHPstan errors 2024-03-09 13:02:04 -05:00
Michael Thomas
c4bf2aae7d fix: migrate action expression validation to separate rule class 2024-03-09 12:57:34 -05:00
Michael Thomas
69ca88d9f8 fix(api): use kebab case route for validate-expression endpoint 2024-03-09 12:07:20 -05:00
Michael Thomas
b38b7b2534 fix: drop unnecessary changes to composer.lock 2024-03-09 12:05:56 -05:00
Michael Thomas
f19bfc3b4b fix(ActionExpression): update list of valid variable names to reflect actual values 2024-03-09 12:03:46 -05:00
Michael Thomas
d22f9c09d7 fix(RuleAction): add return type to getValue 2024-03-09 12:02:47 -05:00
Michael Thomas
fc2da9eb42 fix(ExpressionController): remove unnecessary rule repository 2024-03-09 11:27:21 -05:00
James Cole
f2c9e20aef Fix other pages 2024-03-09 13:35:22 +01:00
James Cole
16b8ca2746 Add some spacing 2024-03-09 13:20:43 +01:00
James Cole
46ea074821 Merge branch 'main' into develop 2024-03-09 13:20:03 +01:00
James Cole
d2c89781e2 Rebuild frontend 2024-03-09 13:19:39 +01:00
James Cole
e54d711891 Improve colors. 2024-03-09 13:17:58 +01:00
James Cole
84d3ad4764 Remove unused local files. 2024-03-09 13:12:33 +01:00
James Cole
b908951a2d Refactor views 2024-03-09 13:08:23 +01:00
James Cole
8b87deea58 Refactor error pages 2024-03-09 13:03:02 +01:00
James Cole
0d7325b3dc Fix CSS and JS (on dashboard) 2024-03-09 12:21:45 +01:00
James Cole
a3fd99a498 Rename (unused) files. 2024-03-09 12:11:18 +01:00
James Cole
0ff405d1e0 Refactor views and CSS 2024-03-09 12:11:06 +01:00
James Cole
46a60af966 Clean up authentication views. 2024-03-09 08:13:53 +01:00
James Cole
591c9e3b39 Move old login screen. 2024-03-09 07:00:35 +01:00
James Cole
c30461b20b Update lock.yml
Signed-off-by: James Cole <james@firefly-iii.org>
2024-03-09 05:20:30 +01:00
James Cole
2c3f86d9bc Update lock.yml
Signed-off-by: James Cole <james@firefly-iii.org>
2024-03-09 05:19:17 +01:00
Michael Thomas
34349e4475 chore: fix typo 2024-03-07 21:37:24 -05:00
Michael Thomas
6acd5be5dc chore: remove accidental changes 2024-03-07 21:10:11 -05:00
Michael Thomas
55a2b4e789 feat: make all transaction journal variables globals
removes redundant reference to the `transaction` object by making all its properties global
2024-03-07 20:58:43 -05:00
Michael Thomas
f41397eb43 refactor: add method on RuleAction to compute action value 2024-03-07 19:02:40 -05:00
Michael Thomas
41fc1e8f82 Merge remote-tracking branch 'upstream/develop' into feat/expression-engine 2024-03-07 13:09:43 -05:00
Michael Thomas
bee219ebf7 refactor: inject ExpressionLanguage singleton using DI 2024-03-07 13:00:57 -05:00
Michael Thomas
438f602961 feat: surface expression validation errors when creating or updating rules 2024-03-07 12:23:32 -05:00
James Cole
429e72e681 Merge branch 'develop' of github.com:firefly-iii/firefly-iii into develop 2024-03-07 06:08:09 +01:00
James Cole
7a134781f2 Fix route name. 2024-03-07 06:01:39 +01:00
Michael Thomas
b572c1dcd3 Merge remote-tracking branch 'upstream/main' into feat/expression-engine 2024-03-06 21:38:40 -05:00
Michael Thomas
95593f847b feat: update all rules to support action value expressions 2024-03-06 20:54:50 -05:00
github-actions
b82fcbd97b Auto commit for release 'develop' on 2024-03-07 2024-03-07 01:29:08 +01:00
Michael Thomas
daddee7806 feat: support action expression parsing, validation, and evaluation 2024-03-06 17:50:16 -05:00
James Cole
930a08ec90 Better index for accounts. 2024-03-06 19:54:09 +01:00
James Cole
fd2edf3b23 Various code cleanup. 2024-03-06 07:16:01 +01:00
James Cole
0597255c08 Fix #8632 2024-03-06 07:01:21 +01:00
James Cole
955ab38a85 Merge pull request #8634 from WardenJakx/develop 2024-03-06 06:10:29 +01:00
WardenJakx
1311a0db8b fix broken link 2024-03-05 22:35:23 -05:00
James Cole
0ce9ee6a6c Ignore phpstan error [skip ci] 2024-03-05 19:39:20 +01:00
James Cole
3a339382d4 Add a button to go back to the v1 layout. 2024-03-05 19:38:45 +01:00
James Cole
a5b15bbc16 Rebuild frontend for basic account list. 2024-03-05 05:54:04 +01:00
James Cole
fbf89fd514 Merge branch 'develop' of github.com:firefly-iii/firefly-iii into develop 2024-03-04 20:42:05 +01:00
James Cole
b3223feba2 Add debug log 2024-03-04 20:41:59 +01:00
James Cole
88a9bc379e Expand account list 2024-03-04 20:41:34 +01:00
James Cole
b442b91b7c Merge pull request #8621 from firefly-iii/dependabot/composer/develop/barryvdh/laravel-ide-helper-3.0.0 2024-03-04 06:01:38 +01:00
dependabot[bot]
9fadbbe087 Bump barryvdh/laravel-ide-helper from 2.15.1 to 3.0.0
Bumps [barryvdh/laravel-ide-helper](https://github.com/barryvdh/laravel-ide-helper) from 2.15.1 to 3.0.0.
- [Release notes](https://github.com/barryvdh/laravel-ide-helper/releases)
- [Changelog](https://github.com/barryvdh/laravel-ide-helper/blob/master/CHANGELOG.md)
- [Commits](https://github.com/barryvdh/laravel-ide-helper/compare/v2.15.1...v3.0.0)

---
updated-dependencies:
- dependency-name: barryvdh/laravel-ide-helper
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-03-04 03:49:45 +00:00
github-actions
1ef7239276 Auto commit for release 'develop' on 2024-03-04 2024-03-04 01:30:26 +01:00
James Cole
ea573e9434 Remove some. 2024-03-03 20:07:47 +01:00
James Cole
34fa24e4a8 Remove some indices 2024-03-03 20:01:35 +01:00
James Cole
a1be4a4d8a Add indices 2024-03-03 19:58:51 +01:00
James Cole
b8e8af1e2a Update database version. 2024-03-03 17:58:13 +01:00
James Cole
c13a3fb30c Add missing indices. 2024-03-03 17:55:59 +01:00
James Cole
cb8fa4e1f4 Fix https://github.com/firefly-iii/firefly-iii/issues/8616 2024-03-03 13:45:05 +01:00
James Cole
bf7f4f9887 Fix https://github.com/firefly-iii/firefly-iii/issues/8597 2024-03-03 10:13:49 +01:00
James Cole
af48548e81 Fix https://github.com/firefly-iii/firefly-iii/issues/8608 2024-03-02 19:20:54 +01:00
github-actions
90d58ec8fa Auto commit for release 'v6.1.10' on 2024-03-02 2024-03-02 17:12:03 +01:00
github-actions
e92dd7f464 Merge branch 'develop' 2024-03-02 16:08:11 +00:00
James Cole
3bdf9eeed2 Jump to v34 2024-03-02 17:06:47 +01:00
James Cole
558ac7b0da Update changelog. 2024-03-02 16:53:19 +01:00
James Cole
9d0488ffbc fix phpstan issues. 2024-02-29 06:39:58 +01:00
James Cole
d7fa8b283e Merge branch 'develop' of github.com:firefly-iii/firefly-iii into develop 2024-02-29 06:27:24 +01:00
James Cole
a0097bd613 Fix issues with available budgets. 2024-02-29 06:26:23 +01:00
github-actions
ffc2156e5f Auto commit for release 'develop' on 2024-02-29 2024-02-29 01:28:57 +01:00
James Cole
e0a89bb5fe Rebuild JS 2024-02-26 20:08:14 +01:00
James Cole
647179cd3c Rebuild JS 2024-02-26 19:56:19 +01:00
James Cole
5106ccdbd7 Merge branch 'main' into develop 2024-02-26 19:55:14 +01:00
James Cole
7103098fe7 Better error handling 2024-02-26 19:54:45 +01:00
James Cole
f8072f0bfc Merge pull request #8588 from firefly-iii/dependabot/github_actions/JC5/firefly-iii-dev-33
Bump JC5/firefly-iii-dev from 32 to 33
2024-02-26 06:17:04 +01:00
dependabot[bot]
96ac3a95c8 Bump JC5/firefly-iii-dev from 32 to 33
Bumps [JC5/firefly-iii-dev](https://github.com/jc5/firefly-iii-dev) from 32 to 33.
- [Commits](https://github.com/jc5/firefly-iii-dev/compare/v32...v33)

---
updated-dependencies:
- dependency-name: JC5/firefly-iii-dev
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-02-26 03:23:03 +00:00
github-actions
cd713dc40f Auto commit for release 'develop' on 2024-02-26 2024-02-26 01:29:50 +01:00
James Cole
d9fba39d80 Fix detection of transaction type. 2024-02-25 18:24:10 +01:00
James Cole
2564470197 Update source account detection. 2024-02-25 18:14:30 +01:00
James Cole
9222c82af0 Expand frontend, first attempt at sorting. 2024-02-25 18:09:52 +01:00
James Cole
243f283bfd Fix tests 2024-02-24 08:36:57 +01:00
James Cole
5b60aaecc0 Update translations. 2024-02-24 08:19:23 +01:00
James Cole
20a4caec60 Add some missing translations 2024-02-24 06:03:34 +01:00
James Cole
99cc096b71 Fix https://github.com/firefly-iii/firefly-iii/issues/8578 2024-02-24 05:38:02 +01:00
James Cole
5626d1c56d Fix https://github.com/firefly-iii/firefly-iii/issues/8575 2024-02-23 17:06:25 +01:00
James Cole
68c9c4ec3c PHPStorm can order methods by alphabet, who knew. 2024-02-22 20:11:09 +01:00
James Cole
f9d4a43e05 This should fix the ci issues. 2024-02-22 06:57:57 +01:00
James Cole
92e7f344e0 Refresh update code. 2024-02-22 06:52:58 +01:00
1000 changed files with 112353 additions and 167013 deletions

View File

@@ -22,6 +22,9 @@
SCRIPT_DIR="$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )"
echo "Running PHP CS Fixer"
$SCRIPT_DIR/phpcs.sh
echo "Running PHPStan"
$SCRIPT_DIR/phpstan.sh
echo "Running PHPMD"
$SCRIPT_DIR/phpmd.sh

View File

@@ -8,16 +8,16 @@
"packages": [
{
"name": "composer/pcre",
"version": "3.1.1",
"version": "3.1.2",
"source": {
"type": "git",
"url": "https://github.com/composer/pcre.git",
"reference": "00104306927c7a0919b4ced2aaa6782c1e61a3c9"
"reference": "4775f35b2d70865807c89d32c8e7385b86eb0ace"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/composer/pcre/zipball/00104306927c7a0919b4ced2aaa6782c1e61a3c9",
"reference": "00104306927c7a0919b4ced2aaa6782c1e61a3c9",
"url": "https://api.github.com/repos/composer/pcre/zipball/4775f35b2d70865807c89d32c8e7385b86eb0ace",
"reference": "4775f35b2d70865807c89d32c8e7385b86eb0ace",
"shasum": ""
},
"require": {
@@ -59,7 +59,7 @@
],
"support": {
"issues": "https://github.com/composer/pcre/issues",
"source": "https://github.com/composer/pcre/tree/3.1.1"
"source": "https://github.com/composer/pcre/tree/3.1.2"
},
"funding": [
{
@@ -75,7 +75,7 @@
"type": "tidelift"
}
],
"time": "2023-10-11T07:11:09+00:00"
"time": "2024-03-07T15:38:35+00:00"
},
{
"name": "composer/semver",
@@ -226,16 +226,16 @@
},
{
"name": "friendsofphp/php-cs-fixer",
"version": "v3.49.0",
"version": "v3.51.0",
"source": {
"type": "git",
"url": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer.git",
"reference": "8742f7aa6f72a399688b65e4f58992c2d4681fc2"
"reference": "127fa74f010da99053e3f5b62672615b72dd6efd"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/PHP-CS-Fixer/PHP-CS-Fixer/zipball/8742f7aa6f72a399688b65e4f58992c2d4681fc2",
"reference": "8742f7aa6f72a399688b65e4f58992c2d4681fc2",
"url": "https://api.github.com/repos/PHP-CS-Fixer/PHP-CS-Fixer/zipball/127fa74f010da99053e3f5b62672615b72dd6efd",
"reference": "127fa74f010da99053e3f5b62672615b72dd6efd",
"shasum": ""
},
"require": {
@@ -245,7 +245,7 @@
"ext-json": "*",
"ext-tokenizer": "*",
"php": "^7.4 || ^8.0",
"sebastian/diff": "^4.0 || ^5.0",
"sebastian/diff": "^4.0 || ^5.0 || ^6.0",
"symfony/console": "^5.4 || ^6.0 || ^7.0",
"symfony/event-dispatcher": "^5.4 || ^6.0 || ^7.0",
"symfony/filesystem": "^5.4 || ^6.0 || ^7.0",
@@ -266,7 +266,8 @@
"php-cs-fixer/accessible-object": "^1.1",
"php-cs-fixer/phpunit-constraint-isidenticalstring": "^1.4",
"php-cs-fixer/phpunit-constraint-xmlmatchesxsd": "^1.4",
"phpunit/phpunit": "^9.6 || ^10.5.5",
"phpunit/phpunit": "^9.6 || ^10.5.5 || ^11.0.2",
"symfony/var-dumper": "^5.4 || ^6.0 || ^7.0",
"symfony/yaml": "^5.4 || ^6.0 || ^7.0"
},
"suggest": {
@@ -305,7 +306,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.49.0"
"source": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/tree/v3.51.0"
},
"funding": [
{
@@ -313,7 +314,7 @@
"type": "github"
}
],
"time": "2024-02-02T00:41:40+00:00"
"time": "2024-02-28T19:50:06+00:00"
},
{
"name": "psr/container",
@@ -470,29 +471,29 @@
},
{
"name": "sebastian/diff",
"version": "5.1.0",
"version": "6.0.1",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/diff.git",
"reference": "fbf413a49e54f6b9b17e12d900ac7f6101591b7f"
"reference": "ab83243ecc233de5655b76f577711de9f842e712"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/fbf413a49e54f6b9b17e12d900ac7f6101591b7f",
"reference": "fbf413a49e54f6b9b17e12d900ac7f6101591b7f",
"url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/ab83243ecc233de5655b76f577711de9f842e712",
"reference": "ab83243ecc233de5655b76f577711de9f842e712",
"shasum": ""
},
"require": {
"php": ">=8.1"
"php": ">=8.2"
},
"require-dev": {
"phpunit/phpunit": "^10.0",
"phpunit/phpunit": "^11.0",
"symfony/process": "^4.2 || ^5"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-main": "5.1-dev"
"dev-main": "6.0-dev"
}
},
"autoload": {
@@ -525,7 +526,7 @@
"support": {
"issues": "https://github.com/sebastianbergmann/diff/issues",
"security": "https://github.com/sebastianbergmann/diff/security/policy",
"source": "https://github.com/sebastianbergmann/diff/tree/5.1.0"
"source": "https://github.com/sebastianbergmann/diff/tree/6.0.1"
},
"funding": [
{
@@ -533,20 +534,20 @@
"type": "github"
}
],
"time": "2023-12-22T10:55:06+00:00"
"time": "2024-03-02T07:30:33+00:00"
},
{
"name": "symfony/console",
"version": "v7.0.3",
"version": "v7.0.4",
"source": {
"type": "git",
"url": "https://github.com/symfony/console.git",
"reference": "c5010d50f1ee4b25cfa0201d9915cf1b14071456"
"reference": "6b099f3306f7c9c2d2786ed736d0026b2903205f"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/console/zipball/c5010d50f1ee4b25cfa0201d9915cf1b14071456",
"reference": "c5010d50f1ee4b25cfa0201d9915cf1b14071456",
"url": "https://api.github.com/repos/symfony/console/zipball/6b099f3306f7c9c2d2786ed736d0026b2903205f",
"reference": "6b099f3306f7c9c2d2786ed736d0026b2903205f",
"shasum": ""
},
"require": {
@@ -610,7 +611,7 @@
"terminal"
],
"support": {
"source": "https://github.com/symfony/console/tree/v7.0.3"
"source": "https://github.com/symfony/console/tree/v7.0.4"
},
"funding": [
{
@@ -626,7 +627,7 @@
"type": "tidelift"
}
],
"time": "2024-01-23T15:02:46+00:00"
"time": "2024-02-22T20:27:20+00:00"
},
{
"name": "symfony/deprecation-contracts",
@@ -1521,16 +1522,16 @@
},
{
"name": "symfony/process",
"version": "v7.0.3",
"version": "v7.0.4",
"source": {
"type": "git",
"url": "https://github.com/symfony/process.git",
"reference": "937a195147e0c27b2759ade834169ed006d0bc74"
"reference": "0e7727191c3b71ebec6d529fa0e50a01ca5679e9"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/process/zipball/937a195147e0c27b2759ade834169ed006d0bc74",
"reference": "937a195147e0c27b2759ade834169ed006d0bc74",
"url": "https://api.github.com/repos/symfony/process/zipball/0e7727191c3b71ebec6d529fa0e50a01ca5679e9",
"reference": "0e7727191c3b71ebec6d529fa0e50a01ca5679e9",
"shasum": ""
},
"require": {
@@ -1562,7 +1563,7 @@
"description": "Executes commands in sub-processes",
"homepage": "https://symfony.com",
"support": {
"source": "https://github.com/symfony/process/tree/v7.0.3"
"source": "https://github.com/symfony/process/tree/v7.0.4"
},
"funding": [
{
@@ -1578,7 +1579,7 @@
"type": "tidelift"
}
],
"time": "2024-01-23T15:02:46+00:00"
"time": "2024-02-22T20:27:20+00:00"
},
{
"name": "symfony/service-contracts",
@@ -1726,16 +1727,16 @@
},
{
"name": "symfony/string",
"version": "v7.0.3",
"version": "v7.0.4",
"source": {
"type": "git",
"url": "https://github.com/symfony/string.git",
"reference": "524aac4a280b90a4420d8d6a040718d0586505ac"
"reference": "f5832521b998b0bec40bee688ad5de98d4cf111b"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/string/zipball/524aac4a280b90a4420d8d6a040718d0586505ac",
"reference": "524aac4a280b90a4420d8d6a040718d0586505ac",
"url": "https://api.github.com/repos/symfony/string/zipball/f5832521b998b0bec40bee688ad5de98d4cf111b",
"reference": "f5832521b998b0bec40bee688ad5de98d4cf111b",
"shasum": ""
},
"require": {
@@ -1792,7 +1793,7 @@
"utf8"
],
"support": {
"source": "https://github.com/symfony/string/tree/v7.0.3"
"source": "https://github.com/symfony/string/tree/v7.0.4"
},
"funding": [
{
@@ -1808,7 +1809,7 @@
"type": "tidelift"
}
],
"time": "2024-01-29T15:41:16+00:00"
"time": "2024-02-01T13:17:36+00:00"
}
],
"packages-dev": [],

View File

@@ -9,16 +9,16 @@
"packages-dev": [
{
"name": "composer/pcre",
"version": "3.1.1",
"version": "3.1.2",
"source": {
"type": "git",
"url": "https://github.com/composer/pcre.git",
"reference": "00104306927c7a0919b4ced2aaa6782c1e61a3c9"
"reference": "4775f35b2d70865807c89d32c8e7385b86eb0ace"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/composer/pcre/zipball/00104306927c7a0919b4ced2aaa6782c1e61a3c9",
"reference": "00104306927c7a0919b4ced2aaa6782c1e61a3c9",
"url": "https://api.github.com/repos/composer/pcre/zipball/4775f35b2d70865807c89d32c8e7385b86eb0ace",
"reference": "4775f35b2d70865807c89d32c8e7385b86eb0ace",
"shasum": ""
},
"require": {
@@ -60,7 +60,7 @@
],
"support": {
"issues": "https://github.com/composer/pcre/issues",
"source": "https://github.com/composer/pcre/tree/3.1.1"
"source": "https://github.com/composer/pcre/tree/3.1.2"
},
"funding": [
{
@@ -76,7 +76,7 @@
"type": "tidelift"
}
],
"time": "2023-10-11T07:11:09+00:00"
"time": "2024-03-07T15:38:35+00:00"
},
{
"name": "composer/xdebug-handler",
@@ -395,16 +395,16 @@
},
{
"name": "symfony/config",
"version": "v7.0.3",
"version": "v7.0.4",
"source": {
"type": "git",
"url": "https://github.com/symfony/config.git",
"reference": "86a5027869ca3d6bdecae6d5d6c2f77c8f2c1d16"
"reference": "44deeba7233f08f383185ffa37dace3b3bc87364"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/config/zipball/86a5027869ca3d6bdecae6d5d6c2f77c8f2c1d16",
"reference": "86a5027869ca3d6bdecae6d5d6c2f77c8f2c1d16",
"url": "https://api.github.com/repos/symfony/config/zipball/44deeba7233f08f383185ffa37dace3b3bc87364",
"reference": "44deeba7233f08f383185ffa37dace3b3bc87364",
"shasum": ""
},
"require": {
@@ -450,7 +450,7 @@
"description": "Helps you find, load, combine, autofill and validate configuration values of any kind",
"homepage": "https://symfony.com",
"support": {
"source": "https://github.com/symfony/config/tree/v7.0.3"
"source": "https://github.com/symfony/config/tree/v7.0.4"
},
"funding": [
{
@@ -466,20 +466,20 @@
"type": "tidelift"
}
],
"time": "2024-01-30T08:34:29+00:00"
"time": "2024-02-26T07:52:39+00:00"
},
{
"name": "symfony/dependency-injection",
"version": "v7.0.3",
"version": "v7.0.4",
"source": {
"type": "git",
"url": "https://github.com/symfony/dependency-injection.git",
"reference": "e915c6684b8e3ae90a4441f6823ebbb40edf0b92"
"reference": "47f37af245df8457ea63409fc242b3cc825ce5eb"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/dependency-injection/zipball/e915c6684b8e3ae90a4441f6823ebbb40edf0b92",
"reference": "e915c6684b8e3ae90a4441f6823ebbb40edf0b92",
"url": "https://api.github.com/repos/symfony/dependency-injection/zipball/47f37af245df8457ea63409fc242b3cc825ce5eb",
"reference": "47f37af245df8457ea63409fc242b3cc825ce5eb",
"shasum": ""
},
"require": {
@@ -530,7 +530,7 @@
"description": "Allows you to standardize and centralize the way objects are constructed in your application",
"homepage": "https://symfony.com",
"support": {
"source": "https://github.com/symfony/dependency-injection/tree/v7.0.3"
"source": "https://github.com/symfony/dependency-injection/tree/v7.0.4"
},
"funding": [
{
@@ -546,7 +546,7 @@
"type": "tidelift"
}
],
"time": "2024-01-30T08:34:29+00:00"
"time": "2024-02-22T20:27:20+00:00"
},
{
"name": "symfony/deprecation-contracts",
@@ -921,16 +921,16 @@
},
{
"name": "symfony/var-exporter",
"version": "v7.0.3",
"version": "v7.0.4",
"source": {
"type": "git",
"url": "https://github.com/symfony/var-exporter.git",
"reference": "1fb79308cb5fc2b44bff6e8af10a5af6812e05b8"
"reference": "dfb0acb6803eb714f05d97dd4c5abe6d5fa9fe41"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/var-exporter/zipball/1fb79308cb5fc2b44bff6e8af10a5af6812e05b8",
"reference": "1fb79308cb5fc2b44bff6e8af10a5af6812e05b8",
"url": "https://api.github.com/repos/symfony/var-exporter/zipball/dfb0acb6803eb714f05d97dd4c5abe6d5fa9fe41",
"reference": "dfb0acb6803eb714f05d97dd4c5abe6d5fa9fe41",
"shasum": ""
},
"require": {
@@ -975,7 +975,7 @@
"serialize"
],
"support": {
"source": "https://github.com/symfony/var-exporter/tree/v7.0.3"
"source": "https://github.com/symfony/var-exporter/tree/v7.0.4"
},
"funding": [
{
@@ -991,7 +991,7 @@
"type": "tidelift"
}
],
"time": "2024-01-23T15:02:46+00:00"
"time": "2024-02-26T10:35:24+00:00"
}
],
"aliases": [],

2
.github/CODEOWNERS vendored Normal file
View File

@@ -0,0 +1,2 @@
# code owners for this Firefly III related repository
* @JC5 @SDx3

View File

@@ -5,6 +5,14 @@ on:
schedule:
- cron: '0 0 * * *'
concurrency:
group: lock-threads
permissions:
issues: write
pull-requests: write
discussions: write
jobs:
lock:
permissions:
@@ -12,8 +20,9 @@ jobs:
pull-requests: write
runs-on: ubuntu-latest
steps:
- uses: JC5/lock-threads@main
- uses: dessant/lock-threads@v5
with:
github-token: ${{ github.token }}
issue-inactive-days: 7
pr-inactive-days: 7
log-output: true

View File

@@ -51,7 +51,7 @@ jobs:
CROWDIN_TOKEN: ${{ secrets.CROWDIN_TOKEN }}
- name: Cleanup translations
id: cleanup-transactions
uses: JC5/firefly-iii-dev@v32
uses: JC5/firefly-iii-dev@v34
with:
action: 'ff3:crowdin-warning'
output: ''
@@ -60,7 +60,7 @@ jobs:
GH_TOKEN: ''
- name: Cleanup changelog
id: cleanup-changelog
uses: JC5/firefly-iii-dev@v32
uses: JC5/firefly-iii-dev@v34
with:
action: 'ff3:changelog'
output: ''
@@ -69,7 +69,7 @@ jobs:
GH_TOKEN: ${{ secrets.CHANGELOG_TOKEN }}
- name: Extract changelog
id: extract-changelog
uses: JC5/firefly-iii-dev@v32
uses: JC5/firefly-iii-dev@v34
with:
action: 'ff3:extract-changelog'
output: 'output'
@@ -78,7 +78,7 @@ jobs:
GH_TOKEN: ""
- name: Replace version
id: replace-version
uses: JC5/firefly-iii-dev@v32
uses: JC5/firefly-iii-dev@v34
with:
action: 'ff3:version'
output: ''
@@ -88,7 +88,7 @@ jobs:
FF_III_VERSION: ${{ github.event_name == 'schedule' && 'develop' || github.event.inputs.version }}
- name: Generate JSON v1
id: json-v1
uses: JC5/firefly-iii-dev@v32
uses: JC5/firefly-iii-dev@v34
with:
action: 'ff3:json-translations v1'
output: ''
@@ -97,7 +97,7 @@ jobs:
GH_TOKEN: ''
- name: Generate JSON v2
id: json-v2
uses: JC5/firefly-iii-dev@v32
uses: JC5/firefly-iii-dev@v34
with:
action: 'ff3:json-translations v2'
output: ''
@@ -106,7 +106,7 @@ jobs:
GH_TOKEN: ''
- name: Code cleanup
id: code-cleanup
uses: JC5/firefly-iii-dev@v32
uses: JC5/firefly-iii-dev@v34
with:
action: 'ff3:code'
output: ''
@@ -115,11 +115,12 @@ jobs:
GH_TOKEN: ''
- name: Build new JS
run: |
npm upgrade
pwd
npm install
npm run build
- name: Build old JS
id: old-js
uses: JC5/firefly-iii-dev@v32
uses: JC5/firefly-iii-dev@v34
with:
action: 'ff3:old-js'
output: ''

View File

@@ -45,15 +45,6 @@ jobs:
- name: Install Composer dependencies
run: composer install --prefer-dist --no-interaction --no-progress --no-scripts
- name: PHPStan
run: .ci/phpstan.sh
- name: PHPMD
run: .ci/phpmd.sh
- name: PHP CS Fixer
run: .ci/phpcs.sh
- name: "Create database file"
run: touch storage/database/database.sqlite

View File

@@ -77,7 +77,7 @@ class AccountController extends Controller
$query = $data['query'];
$date = $data['date'] ?? today(config('app.timezone'));
$return = [];
$result = $this->repository->searchAccount((string) $query, $types, $this->parameters->get('limit'));
$result = $this->repository->searchAccount((string)$query, $types, $this->parameters->get('limit'));
// TODO this code is duplicated in the V2 Autocomplete controller, which means this code is due to be deprecated.
$defaultCurrency = app('amount')->getDefaultCurrency();
@@ -97,11 +97,11 @@ class AccountController extends Controller
}
$return[] = [
'id' => (string) $account->id,
'id' => (string)$account->id,
'name' => $account->name,
'name_with_balance' => $nameWithBalance,
'type' => $account->accountType->type,
'currency_id' => (string) $currency->id,
'currency_id' => (string)$currency->id,
'currency_name' => $currency->name,
'currency_code' => $currency->code,
'currency_symbol' => $currency->symbol,
@@ -114,8 +114,8 @@ class AccountController extends Controller
$return,
static function (array $left, array $right) {
$order = [AccountType::ASSET, AccountType::REVENUE, AccountType::EXPENSE];
$posA = (int) array_search($left['type'], $order, true);
$posB = (int) array_search($right['type'], $order, true);
$posA = (int)array_search($left['type'], $order, true);
$posB = (int)array_search($right['type'], $order, true);
return $posA - $posB;
}

View File

@@ -66,7 +66,7 @@ class BillController extends Controller
$filtered = $result->map(
static function (Bill $item) {
return [
'id' => (string) $item->id,
'id' => (string)$item->id,
'name' => $item->name,
'active' => $item->active,
];

View File

@@ -66,7 +66,7 @@ class BudgetController extends Controller
$filtered = $result->map(
static function (Budget $item) {
return [
'id' => (string) $item->id,
'id' => (string)$item->id,
'name' => $item->name,
];
}

View File

@@ -66,7 +66,7 @@ class CategoryController extends Controller
$filtered = $result->map(
static function (Category $item) {
return [
'id' => (string) $item->id,
'id' => (string)$item->id,
'name' => $item->name,
];
}

View File

@@ -68,7 +68,7 @@ class CurrencyController extends Controller
/** @var TransactionCurrency $currency */
foreach ($collection as $currency) {
$result[] = [
'id' => (string) $currency->id,
'id' => (string)$currency->id,
'name' => $currency->name,
'code' => $currency->code,
'symbol' => $currency->symbol,
@@ -94,7 +94,7 @@ class CurrencyController extends Controller
/** @var TransactionCurrency $currency */
foreach ($collection as $currency) {
$result[] = [
'id' => (string) $currency->id,
'id' => (string)$currency->id,
'name' => sprintf('%s (%s)', $currency->name, $currency->code),
'code' => $currency->code,
'symbol' => $currency->symbol,

View File

@@ -68,7 +68,7 @@ class ObjectGroupController extends Controller
/** @var ObjectGroup $objectGroup */
foreach ($result as $objectGroup) {
$return[] = [
'id' => (string) $objectGroup->id,
'id' => (string)$objectGroup->id,
'name' => $objectGroup->title,
'title' => $objectGroup->title,
];

View File

@@ -75,14 +75,14 @@ class PiggyBankController extends Controller
$currency = $this->accountRepository->getAccountCurrency($piggy->account) ?? $defaultCurrency;
$objectGroup = $piggy->objectGroups()->first();
$response[] = [
'id' => (string) $piggy->id,
'id' => (string)$piggy->id,
'name' => $piggy->name,
'currency_id' => (string) $currency->id,
'currency_id' => (string)$currency->id,
'currency_name' => $currency->name,
'currency_code' => $currency->code,
'currency_symbol' => $currency->symbol,
'currency_decimal_places' => $currency->decimal_places,
'object_group_id' => null === $objectGroup ? null : (string) $objectGroup->id,
'object_group_id' => null === $objectGroup ? null : (string)$objectGroup->id,
'object_group_title' => $objectGroup?->title,
];
}
@@ -107,7 +107,7 @@ class PiggyBankController extends Controller
$currentAmount = $this->piggyRepository->getRepetition($piggy)->currentamount ?? '0';
$objectGroup = $piggy->objectGroups()->first();
$response[] = [
'id' => (string) $piggy->id,
'id' => (string)$piggy->id,
'name' => $piggy->name,
'name_with_balance' => sprintf(
'%s (%s / %s)',
@@ -115,12 +115,12 @@ class PiggyBankController extends Controller
app('amount')->formatAnything($currency, $currentAmount, false),
app('amount')->formatAnything($currency, $piggy->targetamount, false),
),
'currency_id' => (string) $currency->id,
'currency_id' => (string)$currency->id,
'currency_name' => $currency->name,
'currency_code' => $currency->code,
'currency_symbol' => $currency->symbol,
'currency_decimal_places' => $currency->decimal_places,
'object_group_id' => null === $objectGroup ? null : (string) $objectGroup->id,
'object_group_id' => null === $objectGroup ? null : (string)$objectGroup->id,
'object_group_title' => $objectGroup?->title,
];
}

View File

@@ -66,7 +66,7 @@ class RecurrenceController extends Controller
/** @var Recurrence $recurrence */
foreach ($recurrences as $recurrence) {
$response[] = [
'id' => (string) $recurrence->id,
'id' => (string)$recurrence->id,
'name' => $recurrence->title,
'description' => $recurrence->description,
];

View File

@@ -65,7 +65,7 @@ class RuleController extends Controller
/** @var Rule $rule */
foreach ($rules as $rule) {
$response[] = [
'id' => (string) $rule->id,
'id' => (string)$rule->id,
'name' => $rule->title,
'description' => $rule->description,
];

View File

@@ -65,7 +65,7 @@ class RuleGroupController extends Controller
/** @var RuleGroup $group */
foreach ($groups as $group) {
$response[] = [
'id' => (string) $group->id,
'id' => (string)$group->id,
'name' => $group->title,
'description' => $group->description,
];

View File

@@ -68,7 +68,7 @@ class TagController extends Controller
/** @var Tag $tag */
foreach ($result as $tag) {
$array[] = [
'id' => (string) $tag->id,
'id' => (string)$tag->id,
'name' => $tag->tag,
'tag' => $tag->tag,
];

View File

@@ -76,8 +76,8 @@ class TransactionController extends Controller
/** @var TransactionJournal $journal */
foreach ($filtered as $journal) {
$array[] = [
'id' => (string) $journal->id,
'transaction_group_id' => (string) $journal->transaction_group_id,
'id' => (string)$journal->id,
'transaction_group_id' => (string)$journal->transaction_group_id,
'name' => $journal->description,
'description' => $journal->description,
];
@@ -96,7 +96,7 @@ class TransactionController extends Controller
$result = new Collection();
if (is_numeric($data['query'])) {
// search for group, not journal.
$firstResult = $this->groupRepository->find((int) $data['query']);
$firstResult = $this->groupRepository->find((int)$data['query']);
if (null !== $firstResult) {
// group may contain multiple journals, each a result:
foreach ($firstResult->transactionJournals as $journal) {
@@ -114,8 +114,8 @@ class TransactionController extends Controller
/** @var TransactionJournal $journal */
foreach ($result as $journal) {
$array[] = [
'id' => (string) $journal->id,
'transaction_group_id' => (string) $journal->transaction_group_id,
'id' => (string)$journal->id,
'transaction_group_id' => (string)$journal->transaction_group_id,
'name' => sprintf('#%d: %s', $journal->transaction_group_id, $journal->description),
'description' => sprintf('#%d: %s', $journal->transaction_group_id, $journal->description),
];

View File

@@ -65,7 +65,7 @@ class TransactionTypeController extends Controller
foreach ($types as $type) {
// different key for consistency.
$array[] = [
'id' => (string) $type->id,
'id' => (string)$type->id,
'name' => $type->type,
'type' => $type->type,
];

View File

@@ -104,7 +104,7 @@ class AccountController extends Controller
}
$currentSet = [
'label' => $account->name,
'currency_id' => (string) $currency->id,
'currency_id' => (string)$currency->id,
'currency_code' => $currency->code,
'currency_symbol' => $currency->symbol,
'currency_decimal_places' => $currency->decimal_places,

View File

@@ -76,38 +76,6 @@ 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;
}
/**
* Method to grab all parameters from the URL.
*/
@@ -216,4 +184,36 @@ abstract class Controller extends BaseController
return $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;
}
}

View File

@@ -70,8 +70,8 @@ class TransactionController extends Controller
// to respond to what is in the $query.
// this is OK because only one thing can be in the query at the moment.
if ($this->isUpdateTransactionAccount($params)) {
$original = $this->repository->find((int) $params['where']['account_id']);
$destination = $this->repository->find((int) $params['update']['account_id']);
$original = $this->repository->find((int)$params['where']['account_id']);
$destination = $this->repository->find((int)$params['update']['account_id']);
/** @var AccountDestroyService $service */
$service = app(AccountDestroyService::class);

View File

@@ -68,6 +68,32 @@ class ExportController extends Controller
return $this->returnExport('accounts');
}
/**
* @throws FireflyException
*/
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($data[$key]))
;
return $response;
}
/**
* This endpoint is documented at:
* https://api-docs.firefly-iii.org/?urls.primaryName=2.0.0%20(v1)#/data/exportBills
@@ -189,30 +215,4 @@ class ExportController extends Controller
return $this->returnExport('transactions');
}
/**
* @throws FireflyException
*/
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($data[$key]))
;
return $response;
}
}

View File

@@ -75,28 +75,28 @@ class TagController extends Controller
$genericSet = $collector->getExtractedJournals();
foreach ($genericSet as $journal) {
$currencyId = (int) $journal['currency_id'];
$foreignCurrencyId = (int) $journal['foreign_currency_id'];
$currencyId = (int)$journal['currency_id'];
$foreignCurrencyId = (int)$journal['foreign_currency_id'];
if (0 !== $currencyId) {
$response[$currencyId] ??= [
'difference' => '0',
'difference_float' => 0,
'currency_id' => (string) $currencyId,
'currency_id' => (string)$currencyId,
'currency_code' => $journal['currency_code'],
];
$response[$currencyId]['difference'] = bcadd($response[$currencyId]['difference'], $journal['amount']);
$response[$currencyId]['difference_float'] = (float) $response[$currencyId]['difference']; // float but on purpose.
$response[$currencyId]['difference_float'] = (float)$response[$currencyId]['difference']; // float but on purpose.
}
if (0 !== $foreignCurrencyId) {
$response[$foreignCurrencyId] ??= [
'difference' => '0',
'difference_float' => 0,
'currency_id' => (string) $foreignCurrencyId,
'currency_id' => (string)$foreignCurrencyId,
'currency_code' => $journal['foreign_currency_code'],
];
$response[$foreignCurrencyId]['difference'] = bcadd($response[$foreignCurrencyId]['difference'], $journal['foreign_amount']);
$response[$foreignCurrencyId]['difference_float'] = (float) $response[$foreignCurrencyId]['difference']; // float but on purpose.
$response[$foreignCurrencyId]['difference_float'] = (float)$response[$foreignCurrencyId]['difference']; // float but on purpose.
}
}
@@ -130,8 +130,8 @@ class TagController extends Controller
/** @var array $journal */
foreach ($genericSet as $journal) {
$currencyId = (int) $journal['currency_id'];
$foreignCurrencyId = (int) $journal['foreign_currency_id'];
$currencyId = (int)$journal['currency_id'];
$foreignCurrencyId = (int)$journal['foreign_currency_id'];
/** @var array $tag */
foreach ($journal['tags'] as $tag) {
@@ -142,15 +142,15 @@ class TagController extends Controller
// on currency ID
if (0 !== $currencyId) {
$response[$key] ??= [
'id' => (string) $tagId,
'id' => (string)$tagId,
'name' => $tag['name'],
'difference' => '0',
'difference_float' => 0,
'currency_id' => (string) $currencyId,
'currency_id' => (string)$currencyId,
'currency_code' => $journal['currency_code'],
];
$response[$key]['difference'] = bcadd($response[$key]['difference'], $journal['amount']);
$response[$key]['difference_float'] = (float) $response[$key]['difference']; // float but on purpose.
$response[$key]['difference_float'] = (float)$response[$key]['difference']; // float but on purpose.
}
// on foreign ID
@@ -158,11 +158,11 @@ class TagController extends Controller
$response[$foreignKey] = $journal[$foreignKey] ?? [
'difference' => '0',
'difference_float' => 0,
'currency_id' => (string) $foreignCurrencyId,
'currency_id' => (string)$foreignCurrencyId,
'currency_code' => $journal['foreign_currency_code'],
];
$response[$foreignKey]['difference'] = bcadd($response[$foreignKey]['difference'], $journal['foreign_amount']);
$response[$foreignKey]['difference_float'] = (float) $response[$foreignKey]['difference']; // float but on purpose.
$response[$foreignKey]['difference_float'] = (float)$response[$foreignKey]['difference']; // float but on purpose.
}
}
}

View File

@@ -0,0 +1,47 @@
<?php
/*
* ExpressionController.php
* Copyright (c) 2024 Michael Thomas
*
* 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\Api\V1\Controllers\Models\Rule;
use FireflyIII\Api\V1\Controllers\Controller;
use FireflyIII\Api\V1\Requests\Models\Rule\ValidateExpressionRequest;
use Illuminate\Http\JsonResponse;
/**
* Class ExpressionController
*/
class ExpressionController extends Controller
{
/**
* This endpoint is documented at:
* https://api-docs.firefly-iii.org/?urls.primaryName=2.0.0%20(v1)#/rules/validateExpression
*
* @SuppressWarnings(PHPMD.UnusedFormalParameter)
*/
public function validateExpression(ValidateExpressionRequest $request): JsonResponse
{
return response()->json([
'valid' => true,
]);
}
}

View File

@@ -118,23 +118,6 @@ 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
{
$result = false;
if ($start->greaterThanOrEqualTo($date) && $end->greaterThanOrEqualTo($date)) {
$result = true;
}
// start and end in the past? use $end
if ($start->lessThanOrEqualTo($date) && $end->lessThanOrEqualTo($date)) {
$result = true;
}
return $result;
}
private function getBalanceInformation(Carbon $start, Carbon $end): array
{
// prep some arrays:
@@ -152,7 +135,7 @@ class BasicController extends Controller
/** @var array $transactionJournal */
foreach ($set as $transactionJournal) {
$currencyId = (int) $transactionJournal['currency_id'];
$currencyId = (int)$transactionJournal['currency_id'];
$incomes[$currencyId] ??= '0';
$incomes[$currencyId] = bcadd(
$incomes[$currencyId],
@@ -170,7 +153,7 @@ class BasicController extends Controller
/** @var array $transactionJournal */
foreach ($set as $transactionJournal) {
$currencyId = (int) $transactionJournal['currency_id'];
$currencyId = (int)$transactionJournal['currency_id'];
$expenses[$currencyId] ??= '0';
$expenses[$currencyId] = bcadd($expenses[$currencyId], $transactionJournal['amount']);
$sums[$currencyId] ??= '0';
@@ -189,7 +172,7 @@ class BasicController extends Controller
'key' => sprintf('balance-in-%s', $currency->code),
'title' => trans('firefly.box_balance_in_currency', ['currency' => $currency->symbol]),
'monetary_value' => $sums[$currencyId] ?? '0',
'currency_id' => (string) $currency->id,
'currency_id' => (string)$currency->id,
'currency_code' => $currency->code,
'currency_symbol' => $currency->symbol,
'currency_decimal_places' => $currency->decimal_places,
@@ -202,7 +185,7 @@ class BasicController extends Controller
'key' => sprintf('spent-in-%s', $currency->code),
'title' => trans('firefly.box_spent_in_currency', ['currency' => $currency->symbol]),
'monetary_value' => $expenses[$currencyId] ?? '0',
'currency_id' => (string) $currency->id,
'currency_id' => (string)$currency->id,
'currency_code' => $currency->code,
'currency_symbol' => $currency->symbol,
'currency_decimal_places' => $currency->decimal_places,
@@ -214,7 +197,7 @@ class BasicController extends Controller
'key' => sprintf('earned-in-%s', $currency->code),
'title' => trans('firefly.box_earned_in_currency', ['currency' => $currency->symbol]),
'monetary_value' => $incomes[$currencyId] ?? '0',
'currency_id' => (string) $currency->id,
'currency_id' => (string)$currency->id,
'currency_code' => $currency->code,
'currency_symbol' => $currency->symbol,
'currency_decimal_places' => $currency->decimal_places,
@@ -248,7 +231,7 @@ class BasicController extends Controller
'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_id' => (string)$info['id'],
'currency_code' => $info['code'],
'currency_symbol' => $info['symbol'],
'currency_decimal_places' => $info['decimal_places'],
@@ -267,7 +250,7 @@ class BasicController extends Controller
'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_id' => (string)$info['id'],
'currency_code' => $info['code'],
'currency_symbol' => $info['symbol'],
'currency_decimal_places' => $info['decimal_places'],
@@ -294,21 +277,21 @@ class BasicController extends Controller
foreach ($spent as $row) {
// either an amount was budgeted or 0 is available.
$amount = (string) ($available[$row['currency_id']] ?? '0');
$amount = (string)($available[$row['currency_id']] ?? '0');
$spentInCurrency = $row['sum'];
$leftToSpend = bcadd($amount, $spentInCurrency);
$days = $today->diffInDays($end) + 1;
$perDay = '0';
if (0 !== $days && bccomp($leftToSpend, '0') > -1) {
$perDay = bcdiv($leftToSpend, (string) $days);
$perDay = bcdiv($leftToSpend, (string)$days);
}
$return[] = [
'key' => sprintf('left-to-spend-in-%s', $row['currency_code']),
'title' => trans('firefly.box_left_to_spend_in_currency', ['currency' => $row['currency_symbol']]),
'monetary_value' => $leftToSpend,
'currency_id' => (string) $row['currency_id'],
'currency_id' => (string)$row['currency_id'],
'currency_code' => $row['currency_code'],
'currency_symbol' => $row['currency_symbol'],
'currency_decimal_places' => $row['currency_decimal_places'],
@@ -368,7 +351,7 @@ class BasicController extends Controller
'key' => sprintf('net-worth-in-%s', $data['currency_code']),
'title' => trans('firefly.box_net_worth_in_currency', ['currency' => $data['currency_symbol']]),
'monetary_value' => $amount,
'currency_id' => (string) $data['currency_id'],
'currency_id' => (string)$data['currency_id'],
'currency_code' => $data['currency_code'],
'currency_symbol' => $data['currency_symbol'],
'currency_decimal_places' => $data['currency_decimal_places'],
@@ -380,4 +363,21 @@ 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
{
$result = false;
if ($start->greaterThanOrEqualTo($date) && $end->greaterThanOrEqualTo($date)) {
$result = true;
}
// start and end in the past? use $end
if ($start->lessThanOrEqualTo($date) && $end->lessThanOrEqualTo($date)) {
$result = true;
}
return $result;
}
}

View File

@@ -88,6 +88,35 @@ class ConfigurationController extends Controller
return response()->json($return);
}
/**
* Get all config values.
*/
private function getDynamicConfiguration(): array
{
$isDemoSite = app('fireflyconfig')->get('is_demo_site');
$updateCheck = app('fireflyconfig')->get('permission_update_check');
$lastCheck = app('fireflyconfig')->get('last_update_check');
$singleUser = app('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
@@ -145,33 +174,4 @@ class ConfigurationController extends Controller
return response()->json(['data' => $data])->header('Content-Type', self::CONTENT_TYPE);
}
/**
* Get all config values.
*/
private function getDynamicConfiguration(): array
{
$isDemoSite = app('fireflyconfig')->get('is_demo_site');
$updateCheck = app('fireflyconfig')->get('permission_update_check');
$lastCheck = app('fireflyconfig')->get('last_update_check');
$singleUser = app('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;
}
}

View File

@@ -83,12 +83,12 @@ class MoveTransactionsRequest extends FormRequest
$data = $validator->getData();
$repository = app(AccountRepositoryInterface::class);
$repository->setUser(auth()->user());
$original = $repository->find((int) $data['original_account']);
$destination = $repository->find((int) $data['destination_account']);
$original = $repository->find((int)$data['original_account']);
$destination = $repository->find((int)$data['destination_account']);
// not the same type:
if ($original->accountType->type !== $destination->accountType->type) {
$validator->errors()->add('title', (string) trans('validation.same_account_type'));
$validator->errors()->add('title', (string)trans('validation.same_account_type'));
return;
}
@@ -98,7 +98,7 @@ class MoveTransactionsRequest extends FormRequest
// check different scenario's.
if (null === $originalCurrency xor null === $destinationCurrency) {
$validator->errors()->add('title', (string) trans('validation.same_account_currency'));
$validator->errors()->add('title', (string)trans('validation.same_account_currency'));
return;
}
@@ -107,7 +107,7 @@ class MoveTransactionsRequest extends FormRequest
return;
}
if ($originalCurrency->code !== $destinationCurrency->code) {
$validator->errors()->add('title', (string) trans('validation.same_account_currency'));
$validator->errors()->add('title', (string)trans('validation.same_account_currency'));
}
}
}

View File

@@ -78,6 +78,25 @@ 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();
@@ -85,6 +104,25 @@ 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();
@@ -92,6 +130,25 @@ 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();
@@ -99,6 +156,25 @@ 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');
@@ -154,100 +230,6 @@ 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()) {
@@ -266,4 +248,22 @@ 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

@@ -83,7 +83,7 @@ class UpdateRequest extends FormRequest
$start = new Carbon($data['start']);
$end = new Carbon($data['end']);
if ($end->isBefore($start)) {
$validator->errors()->add('end', (string) trans('validation.date_after'));
$validator->errors()->add('end', (string)trans('validation.date_after'));
}
}
}

View File

@@ -73,6 +73,65 @@ class StoreRequest extends FormRequest
];
}
/**
* 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.
*/
@@ -136,63 +195,4 @@ class StoreRequest extends FormRequest
Log::channel('audit')->error(sprintf('Validation errors in %s', __CLASS__), $validator->errors()->toArray());
}
}
/**
* 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;
}
}

View File

@@ -78,6 +78,70 @@ 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.
*/
@@ -146,68 +210,4 @@ class UpdateRequest extends FormRequest
Log::channel('audit')->error(sprintf('Validation errors in %s', __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

@@ -24,6 +24,7 @@ declare(strict_types=1);
namespace FireflyIII\Api\V1\Requests\Models\Rule;
use FireflyIII\Rules\IsBoolean;
use FireflyIII\Rules\IsValidActionExpression;
use FireflyIII\Support\Request\ChecksLogin;
use FireflyIII\Support\Request\ConvertsDataTypes;
use FireflyIII\Support\Request\GetRuleConfiguration;
@@ -57,13 +58,48 @@ class StoreRequest extends FormRequest
'active' => ['active', 'boolean'],
];
$data = $this->getAllData($fields);
$data['triggers'] = $this->getRuleTriggers();
$data['actions'] = $this->getRuleActions();
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'],
'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.
*/
@@ -87,7 +123,7 @@ class StoreRequest extends FormRequest
'triggers.*.stop_processing' => [new IsBoolean()],
'triggers.*.active' => [new IsBoolean()],
'actions.*.type' => 'required|in:'.implode(',', $validActions),
'actions.*.value' => 'required_if:actions.*.type,'.$contextActions.'|ruleActionValue',
'actions.*.value' => [sprintf('required_if:actions.*.type,%s', $contextActions), new IsValidActionExpression(), 'ruleActionValue'],
'actions.*.stop_processing' => [new IsBoolean()],
'actions.*.active' => [new IsBoolean()],
'strict' => [new IsBoolean()],
@@ -197,40 +233,4 @@ class StoreRequest extends FormRequest
$validator->errors()->add(sprintf('actions.%d.active', $inactiveIndex), (string)trans('validation.at_least_one_active_action'));
}
}
private function getRuleTriggers(): array
{
$triggers = $this->get('triggers');
$return = [];
if (is_array($triggers)) {
foreach ($triggers as $trigger) {
$return[] = [
'type' => $trigger['type'],
'value' => $trigger['value'],
'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;
}
}

View File

@@ -47,16 +47,6 @@ class TestRequest extends FormRequest
];
}
public function rules(): array
{
return [
'start' => 'date',
'end' => 'date|after_or_equal:start',
'accounts' => '',
'accounts.*' => 'required|exists:accounts,id|belongsToUser:accounts',
];
}
private function getPage(): int
{
return 0 === (int)$this->query('page') ? 1 : (int)$this->query('page');
@@ -81,4 +71,14 @@ class TestRequest extends FormRequest
{
return $this->get('accounts');
}
public function rules(): array
{
return [
'start' => 'date',
'end' => 'date|after_or_equal:start',
'accounts' => '',
'accounts.*' => 'required|exists:accounts,id|belongsToUser:accounts',
];
}
}

View File

@@ -46,16 +46,6 @@ class TriggerRequest extends FormRequest
];
}
public function rules(): array
{
return [
'start' => 'date',
'end' => 'date|after_or_equal:start',
'accounts' => '',
'accounts.*' => 'exists:accounts,id|belongsToUser:accounts',
];
}
private function getDate(string $field): ?Carbon
{
$value = $this->query($field);
@@ -75,4 +65,14 @@ class TriggerRequest extends FormRequest
{
return $this->get('accounts') ?? [];
}
public function rules(): array
{
return [
'start' => 'date',
'end' => 'date|after_or_equal:start',
'accounts' => '',
'accounts.*' => 'exists:accounts,id|belongsToUser:accounts',
];
}
}

View File

@@ -25,6 +25,7 @@ namespace FireflyIII\Api\V1\Requests\Models\Rule;
use FireflyIII\Models\Rule;
use FireflyIII\Rules\IsBoolean;
use FireflyIII\Rules\IsValidActionExpression;
use FireflyIII\Support\Request\ChecksLogin;
use FireflyIII\Support\Request\ConvertsDataTypes;
use FireflyIII\Support\Request\GetRuleConfiguration;
@@ -70,6 +71,50 @@ 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;
$stopProcessing = array_key_exists('stop_processing', $trigger) ? $trigger['stop_processing'] : false;
$return[] = [
'type' => $trigger['type'],
'value' => $trigger['value'],
'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.
*/
@@ -96,7 +141,7 @@ class UpdateRequest extends FormRequest
'triggers.*.stop_processing' => [new IsBoolean()],
'triggers.*.active' => [new IsBoolean()],
'actions.*.type' => 'required|in:'.implode(',', $validActions),
'actions.*.value' => 'required_if:actions.*.type,'.$contextActions.'|ruleActionValue',
'actions.*.value' => [sprintf('required_if:actions.*.type,%s', $contextActions), new IsValidActionExpression(), 'ruleActionValue'],
'actions.*.stop_processing' => [new IsBoolean()],
'actions.*.active' => [new IsBoolean()],
'strict' => [new IsBoolean()],
@@ -204,48 +249,4 @@ class UpdateRequest extends FormRequest
$validator->errors()->add(sprintf('actions.%d.active', $inactiveIndex), (string)trans('validation.at_least_one_active_action'));
}
}
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;
$stopProcessing = array_key_exists('stop_processing', $trigger) ? $trigger['stop_processing'] : false;
$return[] = [
'type' => $trigger['type'],
'value' => $trigger['value'],
'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;
}
}

View File

@@ -1,6 +1,8 @@
/*!
* app.scss
* Copyright (c) 2019 james@firefly-iii.org
<?php
/**
* ValidateExpressionRequest.php
* Copyright (c) 2024 Michael Thomas
*
* This file is part of Firefly III (https://github.com/firefly-iii).
*
@@ -18,13 +20,23 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
/* TODO REMOVE ME */
// Variables
//@import "variables";
declare(strict_types=1);
// Bootstrap
//@import "~bootstrap-sass/assets/stylesheets/bootstrap";
namespace FireflyIII\Api\V1\Requests\Models\Rule;
// Font awesome
//@import "~font-awesome/css/font-awesome";
use FireflyIII\Rules\IsValidActionExpression;
use FireflyIII\Support\Request\ChecksLogin;
use Illuminate\Foundation\Http\FormRequest;
/**
* Class ValidateExpressionRequest
*/
class ValidateExpressionRequest extends FormRequest
{
use ChecksLogin;
public function rules(): array
{
return ['expression' => ['required', new IsValidActionExpression()]];
}
}

View File

@@ -46,16 +46,6 @@ class TestRequest extends FormRequest
];
}
public function rules(): array
{
return [
'start' => 'date',
'end' => 'date|after_or_equal:start',
'accounts' => '',
'accounts.*' => 'exists:accounts,id|belongsToUser:accounts',
];
}
private function getDate(string $field): ?Carbon
{
$value = $this->query($field);
@@ -75,4 +65,14 @@ class TestRequest extends FormRequest
{
return $this->get('accounts');
}
public function rules(): array
{
return [
'start' => 'date',
'end' => 'date|after_or_equal:start',
'accounts' => '',
'accounts.*' => 'exists:accounts,id|belongsToUser:accounts',
];
}
}

View File

@@ -46,14 +46,6 @@ class TriggerRequest extends FormRequest
];
}
public function rules(): array
{
return [
'start' => 'date',
'end' => 'date|after_or_equal:start',
];
}
private function getDate(string $field): ?Carbon
{
$value = $this->query($field);
@@ -77,4 +69,12 @@ class TriggerRequest extends FormRequest
return $this->get('accounts');
}
public function rules(): array
{
return [
'start' => 'date',
'end' => 'date|after_or_equal:start',
];
}
}

View File

@@ -69,134 +69,6 @@ class StoreRequest extends FormRequest
// TODO include location and ability to process it.
}
/**
* The rules that the incoming request must be matched against.
*/
public function rules(): array
{
app('log')->debug('Collect rules of TransactionStoreRequest');
$validProtocols = config('firefly.valid_url_protocols');
return [
// basic fields for group:
'group_title' => 'min:1|max:1000|nullable',
'error_if_duplicate_hash' => [new IsBoolean()],
'apply_rules' => [new IsBoolean()],
// transaction rules (in array for splits):
'transactions.*.type' => 'required|in:withdrawal,deposit,transfer,opening-balance,reconciliation',
'transactions.*.date' => ['required', new IsDateOrTime()],
'transactions.*.order' => 'numeric|min:0',
// currency info
'transactions.*.currency_id' => 'numeric|exists:transaction_currencies,id|nullable',
'transactions.*.currency_code' => 'min:3|max:51|exists:transaction_currencies,code|nullable',
'transactions.*.foreign_currency_id' => 'numeric|exists:transaction_currencies,id|nullable',
'transactions.*.foreign_currency_code' => 'min:3|max:51|exists:transaction_currencies,code|nullable',
// amount
'transactions.*.amount' => ['required', new IsValidPositiveAmount()],
'transactions.*.foreign_amount' => ['nullable', new IsValidZeroOrMoreAmount()],
// description
'transactions.*.description' => 'nullable|min:1|max:1000',
// source of transaction
'transactions.*.source_id' => ['numeric', 'nullable', new BelongsUser()],
'transactions.*.source_name' => 'min:1|max:255|nullable',
'transactions.*.source_iban' => 'min:1|max:255|nullable|iban',
'transactions.*.source_number' => 'min:1|max:255|nullable',
'transactions.*.source_bic' => 'min:1|max:255|nullable|bic',
// destination of transaction
'transactions.*.destination_id' => ['numeric', 'nullable', new BelongsUser()],
'transactions.*.destination_name' => 'min:1|max:255|nullable',
'transactions.*.destination_iban' => 'min:1|max:255|nullable|iban',
'transactions.*.destination_number' => 'min:1|max:255|nullable',
'transactions.*.destination_bic' => 'min:1|max:255|nullable|bic',
// budget, category, bill and piggy
'transactions.*.budget_id' => ['mustExist:budgets,id', new BelongsUser()],
'transactions.*.budget_name' => ['min:1', 'max:255', 'nullable', new BelongsUser()],
'transactions.*.category_id' => ['mustExist:categories,id', new BelongsUser(), 'nullable'],
'transactions.*.category_name' => 'min:1|max:255|nullable',
'transactions.*.bill_id' => ['numeric', 'nullable', 'mustExist:bills,id', new BelongsUser()],
'transactions.*.bill_name' => ['min:1', 'max:255', 'nullable', new BelongsUser()],
'transactions.*.piggy_bank_id' => ['numeric', 'nullable', 'mustExist:piggy_banks,id', new BelongsUser()],
'transactions.*.piggy_bank_name' => ['min:1', 'max:255', 'nullable', new BelongsUser()],
// other interesting fields
'transactions.*.reconciled' => [new IsBoolean()],
'transactions.*.notes' => 'min:1|max:32768|nullable',
'transactions.*.tags' => 'min:0|max:255',
'transactions.*.tags.*' => 'min:0|max:255',
// meta info fields
'transactions.*.internal_reference' => 'min:1|max:255|nullable',
'transactions.*.external_id' => 'min:1|max:255|nullable',
'transactions.*.recurrence_id' => 'min:1|max:255|nullable',
'transactions.*.bunq_payment_id' => 'min:1|max:255|nullable',
'transactions.*.external_url' => sprintf('min:1|max:255|nullable|url:%s', $validProtocols),
// SEPA fields:
'transactions.*.sepa_cc' => 'min:1|max:255|nullable',
'transactions.*.sepa_ct_op' => 'min:1|max:255|nullable',
'transactions.*.sepa_ct_id' => 'min:1|max:255|nullable',
'transactions.*.sepa_db' => 'min:1|max:255|nullable',
'transactions.*.sepa_country' => 'min:1|max:255|nullable',
'transactions.*.sepa_ep' => 'min:1|max:255|nullable',
'transactions.*.sepa_ci' => 'min:1|max:255|nullable',
'transactions.*.sepa_batch_id' => 'min:1|max:255|nullable',
// dates
'transactions.*.interest_date' => 'date|nullable',
'transactions.*.book_date' => 'date|nullable',
'transactions.*.process_date' => 'date|nullable',
'transactions.*.due_date' => 'date|nullable',
'transactions.*.payment_date' => 'date|nullable',
'transactions.*.invoice_date' => 'date|nullable',
];
}
/**
* Configure the validator instance.
*/
public function withValidator(Validator $validator): void
{
$validator->after(
function (Validator $validator): void {
// must be valid array.
$this->validateTransactionArray($validator);
// must submit at least one transaction.
app('log')->debug('Now going to validateOneTransaction');
$this->validateOneTransaction($validator);
app('log')->debug('Now done with validateOneTransaction');
// all journals must have a description
$this->validateDescriptions($validator);
// all transaction types must be equal:
$this->validateTransactionTypes($validator);
// validate foreign currency info
$this->validateForeignCurrencyInformation($validator);
// validate all account info
$this->validateAccountInformation($validator);
// validate source/destination is equal, depending on the transaction journal type.
$this->validateEqualAccounts($validator);
// the group must have a description if > 1 journal.
$this->validateGroupDescription($validator);
}
);
if ($validator->fails()) {
Log::channel('audit')->error(sprintf('Validation errors in %s', __CLASS__), $validator->errors()->toArray());
}
}
/**
* Get transaction data.
*/
@@ -291,4 +163,132 @@ class StoreRequest extends FormRequest
return $return;
}
/**
* The rules that the incoming request must be matched against.
*/
public function rules(): array
{
app('log')->debug('Collect rules of TransactionStoreRequest');
$validProtocols = config('firefly.valid_url_protocols');
return [
// basic fields for group:
'group_title' => 'min:1|max:1000|nullable',
'error_if_duplicate_hash' => [new IsBoolean()],
'apply_rules' => [new IsBoolean()],
// transaction rules (in array for splits):
'transactions.*.type' => 'required|in:withdrawal,deposit,transfer,opening-balance,reconciliation',
'transactions.*.date' => ['required', new IsDateOrTime()],
'transactions.*.order' => 'numeric|min:0',
// currency info
'transactions.*.currency_id' => 'numeric|exists:transaction_currencies,id|nullable',
'transactions.*.currency_code' => 'min:3|max:51|exists:transaction_currencies,code|nullable',
'transactions.*.foreign_currency_id' => 'numeric|exists:transaction_currencies,id|nullable',
'transactions.*.foreign_currency_code' => 'min:3|max:51|exists:transaction_currencies,code|nullable',
// amount
'transactions.*.amount' => ['required', new IsValidPositiveAmount()],
'transactions.*.foreign_amount' => ['nullable', new IsValidZeroOrMoreAmount()],
// description
'transactions.*.description' => 'nullable|min:1|max:1000',
// source of transaction
'transactions.*.source_id' => ['numeric', 'nullable', new BelongsUser()],
'transactions.*.source_name' => 'min:1|max:255|nullable',
'transactions.*.source_iban' => 'min:1|max:255|nullable|iban',
'transactions.*.source_number' => 'min:1|max:255|nullable',
'transactions.*.source_bic' => 'min:1|max:255|nullable|bic',
// destination of transaction
'transactions.*.destination_id' => ['numeric', 'nullable', new BelongsUser()],
'transactions.*.destination_name' => 'min:1|max:255|nullable',
'transactions.*.destination_iban' => 'min:1|max:255|nullable|iban',
'transactions.*.destination_number' => 'min:1|max:255|nullable',
'transactions.*.destination_bic' => 'min:1|max:255|nullable|bic',
// budget, category, bill and piggy
'transactions.*.budget_id' => ['mustExist:budgets,id', new BelongsUser()],
'transactions.*.budget_name' => ['min:1', 'max:255', 'nullable', new BelongsUser()],
'transactions.*.category_id' => ['mustExist:categories,id', new BelongsUser(), 'nullable'],
'transactions.*.category_name' => 'min:1|max:255|nullable',
'transactions.*.bill_id' => ['numeric', 'nullable', 'mustExist:bills,id', new BelongsUser()],
'transactions.*.bill_name' => ['min:1', 'max:255', 'nullable', new BelongsUser()],
'transactions.*.piggy_bank_id' => ['numeric', 'nullable', 'mustExist:piggy_banks,id', new BelongsUser()],
'transactions.*.piggy_bank_name' => ['min:1', 'max:255', 'nullable', new BelongsUser()],
// other interesting fields
'transactions.*.reconciled' => [new IsBoolean()],
'transactions.*.notes' => 'min:1|max:32768|nullable',
'transactions.*.tags' => 'min:0|max:255',
'transactions.*.tags.*' => 'min:0|max:255',
// meta info fields
'transactions.*.internal_reference' => 'min:1|max:255|nullable',
'transactions.*.external_id' => 'min:1|max:255|nullable',
'transactions.*.recurrence_id' => 'min:1|max:255|nullable',
'transactions.*.bunq_payment_id' => 'min:1|max:255|nullable',
'transactions.*.external_url' => sprintf('min:1|max:255|nullable|url:%s', $validProtocols),
// SEPA fields:
'transactions.*.sepa_cc' => 'min:1|max:255|nullable',
'transactions.*.sepa_ct_op' => 'min:1|max:255|nullable',
'transactions.*.sepa_ct_id' => 'min:1|max:255|nullable',
'transactions.*.sepa_db' => 'min:1|max:255|nullable',
'transactions.*.sepa_country' => 'min:1|max:255|nullable',
'transactions.*.sepa_ep' => 'min:1|max:255|nullable',
'transactions.*.sepa_ci' => 'min:1|max:255|nullable',
'transactions.*.sepa_batch_id' => 'min:1|max:255|nullable',
// dates
'transactions.*.interest_date' => 'date|nullable',
'transactions.*.book_date' => 'date|nullable',
'transactions.*.process_date' => 'date|nullable',
'transactions.*.due_date' => 'date|nullable',
'transactions.*.payment_date' => 'date|nullable',
'transactions.*.invoice_date' => 'date|nullable',
];
}
/**
* Configure the validator instance.
*/
public function withValidator(Validator $validator): void
{
$validator->after(
function (Validator $validator): void {
// must be valid array.
$this->validateTransactionArray($validator);
// must submit at least one transaction.
app('log')->debug('Now going to validateOneTransaction');
$this->validateOneTransaction($validator);
app('log')->debug('Now done with validateOneTransaction');
// all journals must have a description
$this->validateDescriptions($validator);
// all transaction types must be equal:
$this->validateTransactionTypes($validator);
// validate foreign currency info
$this->validateForeignCurrencyInformation($validator);
// validate all account info
$this->validateAccountInformation($validator);
// validate source/destination is equal, depending on the transaction journal type.
$this->validateEqualAccounts($validator);
// the group must have a description if > 1 journal.
$this->validateGroupDescription($validator);
}
);
if ($validator->fails()) {
Log::channel('audit')->error(sprintf('Validation errors in %s', __CLASS__), $validator->errors()->toArray());
}
}
}

View File

@@ -90,127 +90,6 @@ class UpdateRequest extends FormRequest
return $data;
}
/**
* The rules that the incoming request must be matched against.
*/
public function rules(): array
{
app('log')->debug(sprintf('Now in %s', __METHOD__));
$validProtocols = config('firefly.valid_url_protocols');
return [
// basic fields for group:
'group_title' => 'min:1|max:1000|nullable',
'apply_rules' => [new IsBoolean()],
// transaction rules (in array for splits):
'transactions.*.type' => 'in:withdrawal,deposit,transfer,opening-balance,reconciliation',
'transactions.*.date' => [new IsDateOrTime()],
'transactions.*.order' => 'numeric|min:0',
// group id:
'transactions.*.transaction_journal_id' => ['nullable', 'numeric', new BelongsUser()],
// currency info
'transactions.*.currency_id' => 'numeric|exists:transaction_currencies,id|nullable',
'transactions.*.currency_code' => 'min:3|max:51|exists:transaction_currencies,code|nullable',
'transactions.*.foreign_currency_id' => 'nullable|numeric|exists:transaction_currencies,id',
'transactions.*.foreign_currency_code' => 'nullable|min:3|max:51|exists:transaction_currencies,code',
// amount
'transactions.*.amount' => [new IsValidPositiveAmount()],
'transactions.*.foreign_amount' => ['nullable', new IsValidZeroOrMoreAmount()],
// description
'transactions.*.description' => 'nullable|min:1|max:1000',
// source of transaction
'transactions.*.source_id' => ['numeric', 'nullable', new BelongsUser()],
'transactions.*.source_name' => 'min:1|max:255|nullable',
// destination of transaction
'transactions.*.destination_id' => ['numeric', 'nullable', new BelongsUser()],
'transactions.*.destination_name' => 'min:1|max:255|nullable',
// budget, category, bill and piggy
'transactions.*.budget_id' => ['mustExist:budgets,id', new BelongsUser(), 'nullable'],
'transactions.*.budget_name' => ['min:1', 'max:255', 'nullable', new BelongsUser()],
'transactions.*.category_id' => ['mustExist:categories,id', new BelongsUser(), 'nullable'],
'transactions.*.category_name' => 'min:1|max:255|nullable',
'transactions.*.bill_id' => ['numeric', 'nullable', 'mustExist:bills,id', new BelongsUser()],
'transactions.*.bill_name' => ['min:1', 'max:255', 'nullable', new BelongsUser()],
// other interesting fields
'transactions.*.reconciled' => [new IsBoolean()],
'transactions.*.notes' => 'min:1|max:32768|nullable',
'transactions.*.tags' => 'min:0|max:255|nullable',
'transactions.*.tags.*' => 'min:0|max:255',
// meta info fields
'transactions.*.internal_reference' => 'min:1|max:255|nullable',
'transactions.*.external_id' => 'min:1|max:255|nullable',
'transactions.*.recurrence_id' => 'min:1|max:255|nullable',
'transactions.*.bunq_payment_id' => 'min:1|max:255|nullable',
'transactions.*.external_url' => sprintf('min:1|max:255|nullable|url:%s', $validProtocols),
// SEPA fields:
'transactions.*.sepa_cc' => 'min:1|max:255|nullable',
'transactions.*.sepa_ct_op' => 'min:1|max:255|nullable',
'transactions.*.sepa_ct_id' => 'min:1|max:255|nullable',
'transactions.*.sepa_db' => 'min:1|max:255|nullable',
'transactions.*.sepa_country' => 'min:1|max:255|nullable',
'transactions.*.sepa_ep' => 'min:1|max:255|nullable',
'transactions.*.sepa_ci' => 'min:1|max:255|nullable',
'transactions.*.sepa_batch_id' => 'min:1|max:255|nullable',
// dates
'transactions.*.interest_date' => 'date|nullable',
'transactions.*.book_date' => 'date|nullable',
'transactions.*.process_date' => 'date|nullable',
'transactions.*.due_date' => 'date|nullable',
'transactions.*.payment_date' => 'date|nullable',
'transactions.*.invoice_date' => 'date|nullable',
];
}
/**
* Configure the validator instance.
*/
public function withValidator(Validator $validator): void
{
app('log')->debug('Now in withValidator');
/** @var TransactionGroup $transactionGroup */
$transactionGroup = $this->route()->parameter('transactionGroup');
$validator->after(
function (Validator $validator) use ($transactionGroup): void {
// if more than one, verify that there are journal ID's present.
$this->validateJournalIds($validator, $transactionGroup);
// all transaction types must be equal:
$this->validateTransactionTypesForUpdate($validator);
// user wants to update a reconciled transaction.
// source, destination, amount + foreign_amount cannot be changed
// and must be omitted from the request.
$this->preventUpdateReconciled($validator, $transactionGroup);
// validate source/destination is equal, depending on the transaction journal type.
$this->validateEqualAccountsForUpdate($validator, $transactionGroup);
// see method:
// $this->preventNoAccountInfo($validator, );
// validate that the currency fits the source and/or destination account.
// validate all account info
$this->validateAccountInformationUpdate($validator, $transactionGroup);
}
);
if ($validator->fails()) {
Log::channel('audit')->error(sprintf('Validation errors in %s', __CLASS__), $validator->errors()->toArray());
}
}
/**
* Get transaction data.
*
@@ -258,7 +137,7 @@ class UpdateRequest extends FormRequest
{
foreach ($this->integerFields as $fieldName) {
if (array_key_exists($fieldName, $transaction)) {
$current[$fieldName] = $this->integerFromValue((string) $transaction[$fieldName]);
$current[$fieldName] = $this->integerFromValue((string)$transaction[$fieldName]);
}
}
@@ -273,7 +152,7 @@ class UpdateRequest extends FormRequest
{
foreach ($this->stringFields as $fieldName) {
if (array_key_exists($fieldName, $transaction)) {
$current[$fieldName] = $this->clearString((string) $transaction[$fieldName]);
$current[$fieldName] = $this->clearString((string)$transaction[$fieldName]);
}
}
@@ -288,7 +167,7 @@ class UpdateRequest extends FormRequest
{
foreach ($this->textareaFields as $fieldName) {
if (array_key_exists($fieldName, $transaction)) {
$current[$fieldName] = $this->clearStringKeepNewlines((string) $transaction[$fieldName]); // keep newlines
$current[$fieldName] = $this->clearStringKeepNewlines((string)$transaction[$fieldName]); // keep newlines
}
}
@@ -304,8 +183,8 @@ class UpdateRequest extends FormRequest
foreach ($this->dateFields as $fieldName) {
app('log')->debug(sprintf('Now at date field %s', $fieldName));
if (array_key_exists($fieldName, $transaction)) {
app('log')->debug(sprintf('New value: "%s"', (string) $transaction[$fieldName]));
$current[$fieldName] = $this->dateFromValue((string) $transaction[$fieldName]);
app('log')->debug(sprintf('New value: "%s"', (string)$transaction[$fieldName]));
$current[$fieldName] = $this->dateFromValue((string)$transaction[$fieldName]);
}
}
@@ -320,7 +199,7 @@ class UpdateRequest extends FormRequest
{
foreach ($this->booleanFields as $fieldName) {
if (array_key_exists($fieldName, $transaction)) {
$current[$fieldName] = $this->convertBoolean((string) $transaction[$fieldName]);
$current[$fieldName] = $this->convertBoolean((string)$transaction[$fieldName]);
}
}
@@ -355,11 +234,132 @@ class UpdateRequest extends FormRequest
$current[$fieldName] = sprintf('%.12f', $value);
}
if (!is_float($value)) {
$current[$fieldName] = (string) $value;
$current[$fieldName] = (string)$value;
}
}
}
return $current;
}
/**
* The rules that the incoming request must be matched against.
*/
public function rules(): array
{
app('log')->debug(sprintf('Now in %s', __METHOD__));
$validProtocols = config('firefly.valid_url_protocols');
return [
// basic fields for group:
'group_title' => 'min:1|max:1000|nullable',
'apply_rules' => [new IsBoolean()],
// transaction rules (in array for splits):
'transactions.*.type' => 'in:withdrawal,deposit,transfer,opening-balance,reconciliation',
'transactions.*.date' => [new IsDateOrTime()],
'transactions.*.order' => 'numeric|min:0',
// group id:
'transactions.*.transaction_journal_id' => ['nullable', 'numeric', new BelongsUser()],
// currency info
'transactions.*.currency_id' => 'numeric|exists:transaction_currencies,id|nullable',
'transactions.*.currency_code' => 'min:3|max:51|exists:transaction_currencies,code|nullable',
'transactions.*.foreign_currency_id' => 'nullable|numeric|exists:transaction_currencies,id',
'transactions.*.foreign_currency_code' => 'nullable|min:3|max:51|exists:transaction_currencies,code',
// amount
'transactions.*.amount' => [new IsValidPositiveAmount()],
'transactions.*.foreign_amount' => ['nullable', new IsValidZeroOrMoreAmount()],
// description
'transactions.*.description' => 'nullable|min:1|max:1000',
// source of transaction
'transactions.*.source_id' => ['numeric', 'nullable', new BelongsUser()],
'transactions.*.source_name' => 'min:1|max:255|nullable',
// destination of transaction
'transactions.*.destination_id' => ['numeric', 'nullable', new BelongsUser()],
'transactions.*.destination_name' => 'min:1|max:255|nullable',
// budget, category, bill and piggy
'transactions.*.budget_id' => ['mustExist:budgets,id', new BelongsUser(), 'nullable'],
'transactions.*.budget_name' => ['min:1', 'max:255', 'nullable', new BelongsUser()],
'transactions.*.category_id' => ['mustExist:categories,id', new BelongsUser(), 'nullable'],
'transactions.*.category_name' => 'min:1|max:255|nullable',
'transactions.*.bill_id' => ['numeric', 'nullable', 'mustExist:bills,id', new BelongsUser()],
'transactions.*.bill_name' => ['min:1', 'max:255', 'nullable', new BelongsUser()],
// other interesting fields
'transactions.*.reconciled' => [new IsBoolean()],
'transactions.*.notes' => 'min:1|max:32768|nullable',
'transactions.*.tags' => 'min:0|max:255|nullable',
'transactions.*.tags.*' => 'min:0|max:255',
// meta info fields
'transactions.*.internal_reference' => 'min:1|max:255|nullable',
'transactions.*.external_id' => 'min:1|max:255|nullable',
'transactions.*.recurrence_id' => 'min:1|max:255|nullable',
'transactions.*.bunq_payment_id' => 'min:1|max:255|nullable',
'transactions.*.external_url' => sprintf('min:1|max:255|nullable|url:%s', $validProtocols),
// SEPA fields:
'transactions.*.sepa_cc' => 'min:1|max:255|nullable',
'transactions.*.sepa_ct_op' => 'min:1|max:255|nullable',
'transactions.*.sepa_ct_id' => 'min:1|max:255|nullable',
'transactions.*.sepa_db' => 'min:1|max:255|nullable',
'transactions.*.sepa_country' => 'min:1|max:255|nullable',
'transactions.*.sepa_ep' => 'min:1|max:255|nullable',
'transactions.*.sepa_ci' => 'min:1|max:255|nullable',
'transactions.*.sepa_batch_id' => 'min:1|max:255|nullable',
// dates
'transactions.*.interest_date' => 'date|nullable',
'transactions.*.book_date' => 'date|nullable',
'transactions.*.process_date' => 'date|nullable',
'transactions.*.due_date' => 'date|nullable',
'transactions.*.payment_date' => 'date|nullable',
'transactions.*.invoice_date' => 'date|nullable',
];
}
/**
* Configure the validator instance.
*/
public function withValidator(Validator $validator): void
{
app('log')->debug('Now in withValidator');
/** @var TransactionGroup $transactionGroup */
$transactionGroup = $this->route()->parameter('transactionGroup');
$validator->after(
function (Validator $validator) use ($transactionGroup): void {
// if more than one, verify that there are journal ID's present.
$this->validateJournalIds($validator, $transactionGroup);
// all transaction types must be equal:
$this->validateTransactionTypesForUpdate($validator);
// user wants to update a reconciled transaction.
// source, destination, amount + foreign_amount cannot be changed
// and must be omitted from the request.
$this->preventUpdateReconciled($validator, $transactionGroup);
// validate source/destination is equal, depending on the transaction journal type.
$this->validateEqualAccountsForUpdate($validator, $transactionGroup);
// see method:
// $this->preventNoAccountInfo($validator, );
// validate that the currency fits the source and/or destination account.
// validate all account info
$this->validateAccountInformationUpdate($validator, $transactionGroup);
}
);
if ($validator->fails()) {
Log::channel('audit')->error(sprintf('Validation errors in %s', __CLASS__), $validator->errors()->toArray());
}
}
}

View File

@@ -72,7 +72,7 @@ class CategoryController extends Controller
$filtered = $result->map(
static function (Category $item) {
return [
'id' => (string) $item->id,
'id' => (string)$item->id,
'name' => $item->name,
];
}

View File

@@ -72,10 +72,10 @@ class TagController extends Controller
$filtered = $result->map(
static function (Tag $item) {
return [
'id' => (string) $item->id,
'name' => $item->tag,
'value' => (string) $item->id,
'label' => $item->tag,
'id' => (string)$item->id,
'name' => $item->tag,
'value' => (string)$item->id,
'label' => $item->tag,
];
}
);

View File

@@ -126,23 +126,23 @@ class AccountController extends Controller
$currency = $default;
}
$currentSet = [
'label' => $account->name,
'label' => $account->name,
// the currency that belongs to the account.
'currency_id' => (string)$currency->id,
'currency_code' => $currency->code,
'currency_symbol' => $currency->symbol,
'currency_decimal_places' => $currency->decimal_places,
'currency_id' => (string)$currency->id,
'currency_code' => $currency->code,
'currency_symbol' => $currency->symbol,
'currency_decimal_places' => $currency->decimal_places,
// the default currency of the user (could be the same!)
'native_currency_id' => (string)$default->id,
'native_currency_code' => $default->code,
'native_currency_symbol' => $default->symbol,
'native_currency_decimal_places' => $default->decimal_places,
'start' => $start->toAtomString(),
'end' => $end->toAtomString(),
'period' => '1D',
'entries' => [],
'native_entries' => [],
'native_currency_id' => (string)$default->id,
'native_currency_code' => $default->code,
'native_currency_symbol' => $default->symbol,
'native_currency_decimal_places' => $default->decimal_places,
'start' => $start->toAtomString(),
'end' => $end->toAtomString(),
'period' => '1D',
'entries' => [],
'native_entries' => [],
];
$currentStart = clone $start;
$range = app('steam')->balanceInRange($account, $start, clone $end, $currency);

View File

@@ -124,11 +124,11 @@ class BudgetController extends Controller
foreach ($rows as $row) {
$current = [
'label' => $budget->name,
'currency_id' => (string) $row['currency_id'],
'currency_id' => (string)$row['currency_id'],
'currency_code' => $row['currency_code'],
'currency_name' => $row['currency_name'],
'currency_decimal_places' => $row['currency_decimal_places'],
'native_currency_id' => (string) $row['native_currency_id'],
'native_currency_id' => (string)$row['native_currency_id'],
'native_currency_code' => $row['native_currency_code'],
'native_currency_name' => $row['native_currency_name'],
'native_currency_decimal_places' => $row['native_currency_decimal_places'],
@@ -189,12 +189,12 @@ class BudgetController extends Controller
foreach ($array as $currencyId => $block) {
$this->currencies[$currencyId] ??= TransactionCurrency::find($currencyId);
$return[$currencyId] ??= [
'currency_id' => (string) $currencyId,
'currency_id' => (string)$currencyId,
'currency_code' => $block['currency_code'],
'currency_name' => $block['currency_name'],
'currency_symbol' => $block['currency_symbol'],
'currency_decimal_places' => (int) $block['currency_decimal_places'],
'native_currency_id' => (string) $this->currency->id,
'currency_decimal_places' => (int)$block['currency_decimal_places'],
'native_currency_id' => (string)$this->currency->id,
'native_currency_code' => $this->currency->code,
'native_currency_name' => $this->currency->name,
'native_currency_symbol' => $this->currency->symbol,

View File

@@ -112,22 +112,22 @@ class CategoryController extends Controller
}
// create arrays
$return[$key] ??= [
'label' => $categoryName,
'currency_id' => (string)$currency->id,
'currency_code' => $currency->code,
'currency_name' => $currency->name,
'currency_symbol' => $currency->symbol,
'currency_decimal_places' => $currency->decimal_places,
'native_currency_id' => (string)$default->id,
'native_currency_code' => $default->code,
'native_currency_name' => $default->name,
'native_currency_symbol' => $default->symbol,
'native_currency_decimal_places' => $default->decimal_places,
'period' => null,
'start' => $start->toAtomString(),
'end' => $end->toAtomString(),
'amount' => '0',
'native_amount' => '0',
'label' => $categoryName,
'currency_id' => (string)$currency->id,
'currency_code' => $currency->code,
'currency_name' => $currency->name,
'currency_symbol' => $currency->symbol,
'currency_decimal_places' => $currency->decimal_places,
'native_currency_id' => (string)$default->id,
'native_currency_code' => $default->code,
'native_currency_name' => $default->name,
'native_currency_symbol' => $default->symbol,
'native_currency_decimal_places' => $default->decimal_places,
'period' => null,
'start' => $start->toAtomString(),
'end' => $end->toAtomString(),
'amount' => '0',
'native_amount' => '0',
];
// add monies

View File

@@ -67,43 +67,6 @@ class Controller extends BaseController
);
}
final protected function jsonApiList(string $key, LengthAwarePaginator $paginator, AbstractTransformer $transformer): array
{
$manager = new Manager();
$baseUrl = request()->getSchemeAndHttpHost().'/api/v2';
$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.
$transformer->collectMetaData($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 = request()->getSchemeAndHttpHost().'/api/v2';
$manager->setSerializer(new JsonApiSerializer($baseUrl));
$transformer->collectMetaData(new Collection([$object]));
$resource = new Item($object, $transformer, $key);
return $manager->createData($resource)->toArray();
}
/**
* TODO duplicate from V1 controller
* Method to grab all parameters from the URL.
@@ -184,4 +147,42 @@ class Controller extends BaseController
return $bag;
}
final protected function jsonApiList(string $key, LengthAwarePaginator $paginator, AbstractTransformer $transformer): array
{
$manager = new Manager();
$baseUrl = request()->getSchemeAndHttpHost().'/api/v2';
$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 = request()->getSchemeAndHttpHost().'/api/v2';
$manager->setSerializer(new JsonApiSerializer($baseUrl));
$transformer->collectMetaData(new Collection([$object]));
$resource = new Item($object, $transformer, $key);
return $manager->createData($resource)->toArray();
}
}

View File

@@ -0,0 +1,104 @@
<?php
/*
* IndexController.php
* Copyright (c) 2024 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\Api\V2\Controllers\Model\Account;
use FireflyIII\Api\V2\Controllers\Controller;
use FireflyIII\Api\V2\Request\Model\Account\IndexRequest;
use FireflyIII\Api\V2\Request\Model\Transaction\InfiniteListRequest;
use FireflyIII\Repositories\UserGroups\Account\AccountRepositoryInterface;
use FireflyIII\Transformers\V2\AccountTransformer;
use Illuminate\Http\JsonResponse;
use Illuminate\Pagination\LengthAwarePaginator;
class IndexController extends Controller
{
public const string RESOURCE_KEY = 'accounts';
private AccountRepositoryInterface $repository;
/**
* AccountController constructor.
*/
public function __construct()
{
parent::__construct();
$this->middleware(
function ($request, $next) {
$this->repository = app(AccountRepositoryInterface::class);
// new way of user group validation
$userGroup = $this->validateUserGroup($request);
if (null !== $userGroup) {
$this->repository->setUserGroup($userGroup);
}
return $next($request);
}
);
}
/**
* TODO see autocomplete/accountcontroller for list.
*/
public function index(IndexRequest $request): JsonResponse
{
$this->repository->resetAccountOrder();
$types = $request->getAccountTypes();
$instructions = $request->getSortInstructions('accounts');
$accounts = $this->repository->getAccountsByType($types, $instructions);
$pageSize = $this->parameters->get('limit');
$count = $accounts->count();
$accounts = $accounts->slice(($this->parameters->get('page') - 1) * $pageSize, $pageSize);
$paginator = new LengthAwarePaginator($accounts, $count, $pageSize, $this->parameters->get('page'));
$transformer = new AccountTransformer();
$this->parameters->set('sort', $instructions);
$transformer->setParameters($this->parameters); // give params to transformer
return response()
->json($this->jsonApiList('accounts', $paginator, $transformer))
->header('Content-Type', self::CONTENT_TYPE)
;
}
public function infiniteList(InfiniteListRequest $request): JsonResponse
{
$this->repository->resetAccountOrder();
// get accounts of the specified type, and return.
$types = $request->getAccountTypes();
// get from repository
$accounts = $this->repository->getAccountsInOrder($types, $request->getSortInstructions('accounts'), $request->getStartRow(), $request->getEndRow());
$total = $this->repository->countAccounts($types);
$count = $request->getEndRow() - $request->getStartRow();
$paginator = new LengthAwarePaginator($accounts, $total, $count, $this->parameters->get('page'));
$transformer = new AccountTransformer();
$transformer->setParameters($this->parameters); // give params to transformer
return response()
->json($this->jsonApiList(self::RESOURCE_KEY, $paginator, $transformer))
->header('Content-Type', self::CONTENT_TYPE)
;
}
}

View File

@@ -51,20 +51,6 @@ class ShowController extends Controller
);
}
/**
* Show a budget.
*/
public function show(Budget $budget): JsonResponse
{
$transformer = new BudgetTransformer();
$transformer->setParameters($this->parameters);
return response()
->api($this->jsonApiObject('budgets', $budget, $transformer))
->header('Content-Type', self::CONTENT_TYPE)
;
}
/**
* 2023-10-29 removed the cerSum reference, not sure where this is used atm
* so removed from api.php. Also applies to "spent" method.
@@ -80,6 +66,20 @@ class ShowController extends Controller
return response()->json($result);
}
/**
* Show a budget.
*/
public function show(Budget $budget): JsonResponse
{
$transformer = new BudgetTransformer();
$transformer->setParameters($this->parameters);
return response()
->api($this->jsonApiObject('budgets', $budget, $transformer))
->header('Content-Type', self::CONTENT_TYPE)
;
}
/**
* This endpoint is documented at:
* TODO add URL

View File

@@ -114,23 +114,6 @@ class BasicController extends Controller
return response()->json($total);
}
/**
* Check if date is outside session range.
*/
protected function notInDateRange(Carbon $date, Carbon $start, Carbon $end): bool // Validate a preference
{
$result = false;
if ($start->greaterThanOrEqualTo($date) && $end->greaterThanOrEqualTo($date)) {
$result = true;
}
// start and end in the past? use $end
if ($start->lessThanOrEqualTo($date) && $end->lessThanOrEqualTo($date)) {
$result = true;
}
return $result;
}
/**
* @throws FireflyException
*/
@@ -411,4 +394,21 @@ 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
{
$result = false;
if ($start->greaterThanOrEqualTo($date) && $end->greaterThanOrEqualTo($date)) {
$result = true;
}
// start and end in the past? use $end
if ($start->lessThanOrEqualTo($date) && $end->lessThanOrEqualTo($date)) {
$result = true;
}
return $result;
}
}

View File

@@ -24,7 +24,7 @@ declare(strict_types=1);
namespace FireflyIII\Api\V2\Controllers\Transaction\List;
use FireflyIII\Api\V2\Controllers\Controller;
use FireflyIII\Api\V2\Request\Model\Transaction\ListByCountRequest;
use FireflyIII\Api\V2\Request\Model\Transaction\InfiniteListRequest;
use FireflyIII\Api\V2\Request\Model\Transaction\ListRequest;
use FireflyIII\Helpers\Collector\GroupCollectorInterface;
use FireflyIII\Transformers\V2\TransactionGroupTransformer;
@@ -35,20 +35,24 @@ use Illuminate\Http\JsonResponse;
*/
class TransactionController extends Controller
{
public function listByCount(ListByCountRequest $request): JsonResponse
public function infiniteList(InfiniteListRequest $request): JsonResponse
{
// get sort instructions
$instructions = $request->getSortInstructions('transactions');
// collect transactions:
/** @var GroupCollectorInterface $collector */
$collector = app(GroupCollectorInterface::class);
$collector = app(GroupCollectorInterface::class);
$collector->setUserGroup(auth()->user()->userGroup)
->withAPIInformation()
->setStartRow($request->getStartRow())
->setEndRow($request->getEndRow())
->setTypes($request->getTransactionTypes())
->setSorting($instructions)
;
$start = $this->parameters->get('start');
$end = $this->parameters->get('end');
$start = $this->parameters->get('start');
$end = $this->parameters->get('end');
if (null !== $start) {
$collector->setStart($start);
}
@@ -56,12 +60,12 @@ class TransactionController extends Controller
$collector->setEnd($end);
}
$paginator = $collector->getPaginatedGroups();
$params = $request->buildParams();
$paginator = $collector->getPaginatedGroups();
$params = $request->buildParams();
$paginator->setPath(
sprintf(
'%s?%s',
route('api.v2.transactions.list'),
route('api.v2.infinite.transactions.list'),
$params
)
);
@@ -97,9 +101,6 @@ class TransactionController extends Controller
$collector->setEnd($end);
}
// $collector->dumpQuery();
// exit;
$paginator = $collector->getPaginatedGroups();
$params = $request->buildParams($pageSize);
$paginator->setPath(

View File

@@ -0,0 +1,69 @@
<?php
/*
* IndexRequest.php
* Copyright (c) 2024 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\Api\V2\Request\Model\Account;
use Carbon\Carbon;
use FireflyIII\Support\Http\Api\AccountFilter;
use FireflyIII\Support\Request\ChecksLogin;
use FireflyIII\Support\Request\ConvertsDataTypes;
use FireflyIII\Support\Request\GetSortInstructions;
use Illuminate\Foundation\Http\FormRequest;
/**
* Class IndexRequest
*
* Lots of code stolen from the SingleDateRequest.
*/
class IndexRequest extends FormRequest
{
use AccountFilter;
use ChecksLogin;
use ConvertsDataTypes;
use GetSortInstructions;
public function getAccountTypes(): array
{
$type = (string)$this->get('type', 'default');
return $this->mapAccountTypes($type);
}
/**
* Get all data from the request.
*/
public function getDate(): Carbon
{
return $this->getCarbonDate('date');
}
/**
* The rules that the incoming request must be matched against.
*/
public function rules(): array
{
return [
'date' => 'date|after:1900-01-01|before:2099-12-31',
];
}
}

View File

@@ -25,19 +25,23 @@ declare(strict_types=1);
namespace FireflyIII\Api\V2\Request\Model\Transaction;
use Carbon\Carbon;
use FireflyIII\Support\Http\Api\AccountFilter;
use FireflyIII\Support\Http\Api\TransactionFilter;
use FireflyIII\Support\Request\ChecksLogin;
use FireflyIII\Support\Request\ConvertsDataTypes;
use FireflyIII\Support\Request\GetSortInstructions;
use Illuminate\Foundation\Http\FormRequest;
/**
* Class ListRequest
* Class InfiniteListRequest
* Used specifically to list transactions.
*/
class ListByCountRequest extends FormRequest
class InfiniteListRequest extends FormRequest
{
use AccountFilter;
use ChecksLogin;
use ConvertsDataTypes;
use GetSortInstructions;
use TransactionFilter;
public function buildParams(): string
@@ -57,13 +61,6 @@ class ListByCountRequest extends FormRequest
return http_build_query($array);
}
public function getPage(): int
{
$page = $this->convertInteger('page');
return 0 === $page || $page > 65536 ? 1 : $page;
}
public function getStartRow(): int
{
$startRow = $this->convertInteger('start_row');
@@ -88,6 +85,20 @@ class ListByCountRequest extends FormRequest
return $this->getCarbonDate('end');
}
public function getAccountTypes(): array
{
$type = (string)$this->get('type', 'default');
return $this->mapAccountTypes($type);
}
public function getPage(): int
{
$page = $this->convertInteger('page');
return 0 === $page || $page > 65536 ? 1 : $page;
}
public function getTransactionTypes(): array
{
$type = (string)$this->get('type', 'default');

View File

@@ -78,145 +78,6 @@ class StoreRequest extends FormRequest
];
}
/**
* The rules that the incoming request must be matched against.
*/
public function rules(): array
{
app('log')->debug('V2: Collect rules of TransactionStoreRequest');
// at this point the userGroup can't be NULL because the
// authorize() method will complain. Loudly.
/** @var UserGroup $userGroup */
$userGroup = $this->getUserGroup();
return [
// basic fields for group:
'group_title' => 'min:1|max:1000|nullable',
'error_if_duplicate_hash' => [new IsBoolean()],
'apply_rules' => [new IsBoolean()],
// transaction rules (in array for splits):
'transactions.*.type' => 'required|in:withdrawal,deposit,transfer,opening-balance,reconciliation',
'transactions.*.date' => ['required', new IsDateOrTime()],
'transactions.*.order' => 'numeric|min:0',
// currency info
'transactions.*.currency_id' => 'numeric|exists:transaction_currencies,id|nullable',
'transactions.*.currency_code' => 'min:3|max:51|exists:transaction_currencies,code|nullable',
'transactions.*.foreign_currency_id' => 'numeric|exists:transaction_currencies,id|nullable',
'transactions.*.foreign_currency_code' => 'min:3|max:51|exists:transaction_currencies,code|nullable',
// amount
'transactions.*.amount' => ['required', new IsValidPositiveAmount()],
'transactions.*.foreign_amount' => ['nullable', new IsValidPositiveAmount()],
// description
'transactions.*.description' => 'nullable|min:1|max:1000',
// source of transaction
'transactions.*.source_id' => ['numeric', 'nullable', new BelongsUserGroup($userGroup)],
'transactions.*.source_name' => 'min:1|max:255|nullable',
'transactions.*.source_iban' => 'min:1|max:255|nullable|iban',
'transactions.*.source_number' => 'min:1|max:255|nullable',
'transactions.*.source_bic' => 'min:1|max:255|nullable|bic',
// destination of transaction
'transactions.*.destination_id' => ['numeric', 'nullable', new BelongsUserGroup($userGroup)],
'transactions.*.destination_name' => 'min:1|max:255|nullable',
'transactions.*.destination_iban' => 'min:1|max:255|nullable|iban',
'transactions.*.destination_number' => 'min:1|max:255|nullable',
'transactions.*.destination_bic' => 'min:1|max:255|nullable|bic',
// budget, category, bill and piggy
'transactions.*.budget_id' => ['mustExist:budgets,id', new BelongsUserGroup($userGroup)],
'transactions.*.budget_name' => ['min:1', 'max:255', 'nullable', new BelongsUserGroup($userGroup)],
'transactions.*.category_id' => ['mustExist:categories,id', new BelongsUserGroup($userGroup), 'nullable'],
'transactions.*.category_name' => 'min:1|max:255|nullable',
'transactions.*.bill_id' => ['numeric', 'nullable', 'mustExist:bills,id', new BelongsUserGroup($userGroup)],
'transactions.*.bill_name' => ['min:1', 'max:255', 'nullable', new BelongsUserGroup($userGroup)],
'transactions.*.piggy_bank_id' => ['numeric', 'nullable', 'mustExist:piggy_banks,id', new BelongsUserGroup($userGroup)],
'transactions.*.piggy_bank_name' => ['min:1', 'max:255', 'nullable', new BelongsUserGroup($userGroup)],
// other interesting fields
'transactions.*.reconciled' => [new IsBoolean()],
'transactions.*.notes' => 'min:1|max:32768|nullable',
'transactions.*.tags' => 'min:0|max:255',
'transactions.*.tags.*' => 'min:0|max:255',
// meta info fields
'transactions.*.internal_reference' => 'min:1|max:255|nullable',
'transactions.*.external_id' => 'min:1|max:255|nullable',
'transactions.*.recurrence_id' => 'min:1|max:255|nullable',
'transactions.*.bunq_payment_id' => 'min:1|max:255|nullable',
'transactions.*.external_url' => 'min:1|max:255|nullable|url',
// SEPA fields:
'transactions.*.sepa_cc' => 'min:1|max:255|nullable',
'transactions.*.sepa_ct_op' => 'min:1|max:255|nullable',
'transactions.*.sepa_ct_id' => 'min:1|max:255|nullable',
'transactions.*.sepa_db' => 'min:1|max:255|nullable',
'transactions.*.sepa_country' => 'min:1|max:255|nullable',
'transactions.*.sepa_ep' => 'min:1|max:255|nullable',
'transactions.*.sepa_ci' => 'min:1|max:255|nullable',
'transactions.*.sepa_batch_id' => 'min:1|max:255|nullable',
// dates
'transactions.*.interest_date' => 'date|nullable',
'transactions.*.book_date' => 'date|nullable',
'transactions.*.process_date' => 'date|nullable',
'transactions.*.due_date' => 'date|nullable',
'transactions.*.payment_date' => 'date|nullable',
'transactions.*.invoice_date' => 'date|nullable',
// TODO include location and ability to process it.
];
}
/**
* Configure the validator instance.
*/
public function withValidator(Validator $validator): void
{
/** @var User $user */
$user = auth()->user();
/** @var UserGroup $userGroup */
$userGroup = $this->getUserGroup();
$validator->after(
function (Validator $validator) use ($user, $userGroup): void {
// must be valid array.
$this->validateTransactionArray($validator); // does not need group validation.
// must submit at least one transaction.
app('log')->debug('Now going to validateOneTransaction');
$this->validateOneTransaction($validator); // does not need group validation.
app('log')->debug('Now done with validateOneTransaction');
// all journals must have a description
$this->validateDescriptions($validator); // does not need group validation.
// all transaction types must be equal:
$this->validateTransactionTypes($validator); // does not need group validation.
// validate foreign currency info
$this->validateForeignCurrencyInformation($validator); // does not need group validation.
// validate all account info
$this->validateAccountInformation($validator, $user, $userGroup);
// validate source/destination is equal, depending on the transaction journal type.
$this->validateEqualAccounts($validator);
// the group must have a description if > 1 journal.
$this->validateGroupDescription($validator);
}
);
if ($validator->fails()) {
Log::channel('audit')->error(sprintf('Validation errors in %s', __CLASS__), $validator->errors()->toArray());
}
}
/**
* Get transaction data.
*/
@@ -313,4 +174,143 @@ class StoreRequest extends FormRequest
return $return;
}
/**
* The rules that the incoming request must be matched against.
*/
public function rules(): array
{
app('log')->debug('V2: Collect rules of TransactionStoreRequest');
// at this point the userGroup can't be NULL because the
// authorize() method will complain. Loudly.
/** @var UserGroup $userGroup */
$userGroup = $this->getUserGroup();
return [
// basic fields for group:
'group_title' => 'min:1|max:1000|nullable',
'error_if_duplicate_hash' => [new IsBoolean()],
'apply_rules' => [new IsBoolean()],
// transaction rules (in array for splits):
'transactions.*.type' => 'required|in:withdrawal,deposit,transfer,opening-balance,reconciliation',
'transactions.*.date' => ['required', new IsDateOrTime()],
'transactions.*.order' => 'numeric|min:0',
// currency info
'transactions.*.currency_id' => 'numeric|exists:transaction_currencies,id|nullable',
'transactions.*.currency_code' => 'min:3|max:51|exists:transaction_currencies,code|nullable',
'transactions.*.foreign_currency_id' => 'numeric|exists:transaction_currencies,id|nullable',
'transactions.*.foreign_currency_code' => 'min:3|max:51|exists:transaction_currencies,code|nullable',
// amount
'transactions.*.amount' => ['required', new IsValidPositiveAmount()],
'transactions.*.foreign_amount' => ['nullable', new IsValidPositiveAmount()],
// description
'transactions.*.description' => 'nullable|min:1|max:1000',
// source of transaction
'transactions.*.source_id' => ['numeric', 'nullable', new BelongsUserGroup($userGroup)],
'transactions.*.source_name' => 'min:1|max:255|nullable',
'transactions.*.source_iban' => 'min:1|max:255|nullable|iban',
'transactions.*.source_number' => 'min:1|max:255|nullable',
'transactions.*.source_bic' => 'min:1|max:255|nullable|bic',
// destination of transaction
'transactions.*.destination_id' => ['numeric', 'nullable', new BelongsUserGroup($userGroup)],
'transactions.*.destination_name' => 'min:1|max:255|nullable',
'transactions.*.destination_iban' => 'min:1|max:255|nullable|iban',
'transactions.*.destination_number' => 'min:1|max:255|nullable',
'transactions.*.destination_bic' => 'min:1|max:255|nullable|bic',
// budget, category, bill and piggy
'transactions.*.budget_id' => ['mustExist:budgets,id', new BelongsUserGroup($userGroup)],
'transactions.*.budget_name' => ['min:1', 'max:255', 'nullable', new BelongsUserGroup($userGroup)],
'transactions.*.category_id' => ['mustExist:categories,id', new BelongsUserGroup($userGroup), 'nullable'],
'transactions.*.category_name' => 'min:1|max:255|nullable',
'transactions.*.bill_id' => ['numeric', 'nullable', 'mustExist:bills,id', new BelongsUserGroup($userGroup)],
'transactions.*.bill_name' => ['min:1', 'max:255', 'nullable', new BelongsUserGroup($userGroup)],
'transactions.*.piggy_bank_id' => ['numeric', 'nullable', 'mustExist:piggy_banks,id', new BelongsUserGroup($userGroup)],
'transactions.*.piggy_bank_name' => ['min:1', 'max:255', 'nullable', new BelongsUserGroup($userGroup)],
// other interesting fields
'transactions.*.reconciled' => [new IsBoolean()],
'transactions.*.notes' => 'min:1|max:32768|nullable',
'transactions.*.tags' => 'min:0|max:255',
'transactions.*.tags.*' => 'min:0|max:255',
// meta info fields
'transactions.*.internal_reference' => 'min:1|max:255|nullable',
'transactions.*.external_id' => 'min:1|max:255|nullable',
'transactions.*.recurrence_id' => 'min:1|max:255|nullable',
'transactions.*.bunq_payment_id' => 'min:1|max:255|nullable',
'transactions.*.external_url' => 'min:1|max:255|nullable|url',
// SEPA fields:
'transactions.*.sepa_cc' => 'min:1|max:255|nullable',
'transactions.*.sepa_ct_op' => 'min:1|max:255|nullable',
'transactions.*.sepa_ct_id' => 'min:1|max:255|nullable',
'transactions.*.sepa_db' => 'min:1|max:255|nullable',
'transactions.*.sepa_country' => 'min:1|max:255|nullable',
'transactions.*.sepa_ep' => 'min:1|max:255|nullable',
'transactions.*.sepa_ci' => 'min:1|max:255|nullable',
'transactions.*.sepa_batch_id' => 'min:1|max:255|nullable',
// dates
'transactions.*.interest_date' => 'date|nullable',
'transactions.*.book_date' => 'date|nullable',
'transactions.*.process_date' => 'date|nullable',
'transactions.*.due_date' => 'date|nullable',
'transactions.*.payment_date' => 'date|nullable',
'transactions.*.invoice_date' => 'date|nullable',
// TODO include location and ability to process it.
];
}
/**
* Configure the validator instance.
*/
public function withValidator(Validator $validator): void
{
/** @var User $user */
$user = auth()->user();
/** @var UserGroup $userGroup */
$userGroup = $this->getUserGroup();
$validator->after(
function (Validator $validator) use ($user, $userGroup): void {
// must be valid array.
$this->validateTransactionArray($validator); // does not need group validation.
// must submit at least one transaction.
app('log')->debug('Now going to validateOneTransaction');
$this->validateOneTransaction($validator); // does not need group validation.
app('log')->debug('Now done with validateOneTransaction');
// all journals must have a description
$this->validateDescriptions($validator); // does not need group validation.
// all transaction types must be equal:
$this->validateTransactionTypes($validator); // does not need group validation.
// validate foreign currency info
$this->validateForeignCurrencyInformation($validator); // does not need group validation.
// validate all account info
$this->validateAccountInformation($validator, $user, $userGroup);
// validate source/destination is equal, depending on the transaction journal type.
$this->validateEqualAccounts($validator);
// the group must have a description if > 1 journal.
$this->validateGroupDescription($validator);
}
);
if ($validator->fails()) {
Log::channel('audit')->error(sprintf('Validation errors in %s', __CLASS__), $validator->errors()->toArray());
}
}
}

View File

@@ -91,127 +91,6 @@ class UpdateRequest extends Request
return $data;
}
/**
* The rules that the incoming request must be matched against.
*/
public function rules(): array
{
app('log')->debug(sprintf('Now in %s', __METHOD__));
$validProtocols = config('firefly.valid_url_protocols');
return [
// basic fields for group:
'group_title' => 'min:1|max:1000|nullable',
'apply_rules' => [new IsBoolean()],
// transaction rules (in array for splits):
'transactions.*.type' => 'in:withdrawal,deposit,transfer,opening-balance,reconciliation',
'transactions.*.date' => [new IsDateOrTime()],
'transactions.*.order' => 'numeric|min:0',
// group id:
'transactions.*.transaction_journal_id' => ['nullable', 'numeric', new BelongsUser()],
// currency info
'transactions.*.currency_id' => 'numeric|exists:transaction_currencies,id|nullable',
'transactions.*.currency_code' => 'min:3|max:51|exists:transaction_currencies,code|nullable',
'transactions.*.foreign_currency_id' => 'nullable|numeric|exists:transaction_currencies,id',
'transactions.*.foreign_currency_code' => 'nullable|min:3|max:51|exists:transaction_currencies,code',
// amount
'transactions.*.amount' => ['nullable', new IsValidPositiveAmount()],
'transactions.*.foreign_amount' => ['nullable', new IsValidZeroOrMoreAmount()],
// description
'transactions.*.description' => 'nullable|min:1|max:1000',
// source of transaction
'transactions.*.source_id' => ['numeric', 'nullable', new BelongsUser()],
'transactions.*.source_name' => 'min:1|max:255|nullable',
// destination of transaction
'transactions.*.destination_id' => ['numeric', 'nullable', new BelongsUser()],
'transactions.*.destination_name' => 'min:1|max:255|nullable',
// budget, category, bill and piggy
'transactions.*.budget_id' => ['mustExist:budgets,id', new BelongsUser(), 'nullable'],
'transactions.*.budget_name' => ['min:1', 'max:255', 'nullable', new BelongsUser()],
'transactions.*.category_id' => ['mustExist:categories,id', new BelongsUser(), 'nullable'],
'transactions.*.category_name' => 'min:1|max:255|nullable',
'transactions.*.bill_id' => ['numeric', 'nullable', 'mustExist:bills,id', new BelongsUser()],
'transactions.*.bill_name' => ['min:1', 'max:255', 'nullable', new BelongsUser()],
// other interesting fields
'transactions.*.reconciled' => [new IsBoolean()],
'transactions.*.notes' => 'min:1|max:32768|nullable',
'transactions.*.tags' => 'min:0|max:255|nullable',
'transactions.*.tags.*' => 'min:0|max:255',
// meta info fields
'transactions.*.internal_reference' => 'min:1|max:255|nullable',
'transactions.*.external_id' => 'min:1|max:255|nullable',
'transactions.*.recurrence_id' => 'min:1|max:255|nullable',
'transactions.*.bunq_payment_id' => 'min:1|max:255|nullable',
'transactions.*.external_url' => sprintf('min:1|max:255|nullable|url:%s', $validProtocols),
// SEPA fields:
'transactions.*.sepa_cc' => 'min:1|max:255|nullable',
'transactions.*.sepa_ct_op' => 'min:1|max:255|nullable',
'transactions.*.sepa_ct_id' => 'min:1|max:255|nullable',
'transactions.*.sepa_db' => 'min:1|max:255|nullable',
'transactions.*.sepa_country' => 'min:1|max:255|nullable',
'transactions.*.sepa_ep' => 'min:1|max:255|nullable',
'transactions.*.sepa_ci' => 'min:1|max:255|nullable',
'transactions.*.sepa_batch_id' => 'min:1|max:255|nullable',
// dates
'transactions.*.interest_date' => 'date|nullable',
'transactions.*.book_date' => 'date|nullable',
'transactions.*.process_date' => 'date|nullable',
'transactions.*.due_date' => 'date|nullable',
'transactions.*.payment_date' => 'date|nullable',
'transactions.*.invoice_date' => 'date|nullable',
];
}
/**
* Configure the validator instance.
*/
public function withValidator(Validator $validator): void
{
app('log')->debug('Now in withValidator');
/** @var TransactionGroup $transactionGroup */
$transactionGroup = $this->route()->parameter('userGroupTransaction');
$validator->after(
function (Validator $validator) use ($transactionGroup): void {
// if more than one, verify that there are journal ID's present.
$this->validateJournalIds($validator, $transactionGroup);
// all transaction types must be equal:
$this->validateTransactionTypesForUpdate($validator);
// user wants to update a reconciled transaction.
// source, destination, amount + foreign_amount cannot be changed
// and must be omitted from the request.
$this->preventUpdateReconciled($validator, $transactionGroup);
// validate source/destination is equal, depending on the transaction journal type.
$this->validateEqualAccountsForUpdate($validator, $transactionGroup);
// see method:
// $this->preventNoAccountInfo($validator, );
// validate that the currency fits the source and/or destination account.
// validate all account info
$this->validateAccountInformationUpdate($validator, $transactionGroup);
}
);
if ($validator->fails()) {
Log::channel('audit')->error(sprintf('Validation errors in %s', __CLASS__), $validator->errors()->toArray());
}
}
/**
* Get transaction data.
*
@@ -259,7 +138,7 @@ class UpdateRequest extends Request
{
foreach ($this->integerFields as $fieldName) {
if (array_key_exists($fieldName, $transaction)) {
$current[$fieldName] = $this->integerFromValue((string) $transaction[$fieldName]);
$current[$fieldName] = $this->integerFromValue((string)$transaction[$fieldName]);
}
}
@@ -274,7 +153,7 @@ class UpdateRequest extends Request
{
foreach ($this->stringFields as $fieldName) {
if (array_key_exists($fieldName, $transaction)) {
$current[$fieldName] = $this->clearString((string) $transaction[$fieldName]);
$current[$fieldName] = $this->clearString((string)$transaction[$fieldName]);
}
}
@@ -289,7 +168,7 @@ class UpdateRequest extends Request
{
foreach ($this->textareaFields as $fieldName) {
if (array_key_exists($fieldName, $transaction)) {
$current[$fieldName] = $this->clearStringKeepNewlines((string) $transaction[$fieldName]); // keep newlines
$current[$fieldName] = $this->clearStringKeepNewlines((string)$transaction[$fieldName]); // keep newlines
}
}
@@ -305,8 +184,8 @@ class UpdateRequest extends Request
foreach ($this->dateFields as $fieldName) {
app('log')->debug(sprintf('Now at date field %s', $fieldName));
if (array_key_exists($fieldName, $transaction)) {
app('log')->debug(sprintf('New value: "%s"', (string) $transaction[$fieldName]));
$current[$fieldName] = $this->dateFromValue((string) $transaction[$fieldName]);
app('log')->debug(sprintf('New value: "%s"', (string)$transaction[$fieldName]));
$current[$fieldName] = $this->dateFromValue((string)$transaction[$fieldName]);
}
}
@@ -321,7 +200,7 @@ class UpdateRequest extends Request
{
foreach ($this->booleanFields as $fieldName) {
if (array_key_exists($fieldName, $transaction)) {
$current[$fieldName] = $this->convertBoolean((string) $transaction[$fieldName]);
$current[$fieldName] = $this->convertBoolean((string)$transaction[$fieldName]);
}
}
@@ -356,11 +235,132 @@ class UpdateRequest extends Request
$current[$fieldName] = sprintf('%.12f', $value);
}
if (!is_float($value)) {
$current[$fieldName] = (string) $value;
$current[$fieldName] = (string)$value;
}
}
}
return $current;
}
/**
* The rules that the incoming request must be matched against.
*/
public function rules(): array
{
app('log')->debug(sprintf('Now in %s', __METHOD__));
$validProtocols = config('firefly.valid_url_protocols');
return [
// basic fields for group:
'group_title' => 'min:1|max:1000|nullable',
'apply_rules' => [new IsBoolean()],
// transaction rules (in array for splits):
'transactions.*.type' => 'in:withdrawal,deposit,transfer,opening-balance,reconciliation',
'transactions.*.date' => [new IsDateOrTime()],
'transactions.*.order' => 'numeric|min:0',
// group id:
'transactions.*.transaction_journal_id' => ['nullable', 'numeric', new BelongsUser()],
// currency info
'transactions.*.currency_id' => 'numeric|exists:transaction_currencies,id|nullable',
'transactions.*.currency_code' => 'min:3|max:51|exists:transaction_currencies,code|nullable',
'transactions.*.foreign_currency_id' => 'nullable|numeric|exists:transaction_currencies,id',
'transactions.*.foreign_currency_code' => 'nullable|min:3|max:51|exists:transaction_currencies,code',
// amount
'transactions.*.amount' => ['nullable', new IsValidPositiveAmount()],
'transactions.*.foreign_amount' => ['nullable', new IsValidZeroOrMoreAmount()],
// description
'transactions.*.description' => 'nullable|min:1|max:1000',
// source of transaction
'transactions.*.source_id' => ['numeric', 'nullable', new BelongsUser()],
'transactions.*.source_name' => 'min:1|max:255|nullable',
// destination of transaction
'transactions.*.destination_id' => ['numeric', 'nullable', new BelongsUser()],
'transactions.*.destination_name' => 'min:1|max:255|nullable',
// budget, category, bill and piggy
'transactions.*.budget_id' => ['mustExist:budgets,id', new BelongsUser(), 'nullable'],
'transactions.*.budget_name' => ['min:1', 'max:255', 'nullable', new BelongsUser()],
'transactions.*.category_id' => ['mustExist:categories,id', new BelongsUser(), 'nullable'],
'transactions.*.category_name' => 'min:1|max:255|nullable',
'transactions.*.bill_id' => ['numeric', 'nullable', 'mustExist:bills,id', new BelongsUser()],
'transactions.*.bill_name' => ['min:1', 'max:255', 'nullable', new BelongsUser()],
// other interesting fields
'transactions.*.reconciled' => [new IsBoolean()],
'transactions.*.notes' => 'min:1|max:32768|nullable',
'transactions.*.tags' => 'min:0|max:255|nullable',
'transactions.*.tags.*' => 'min:0|max:255',
// meta info fields
'transactions.*.internal_reference' => 'min:1|max:255|nullable',
'transactions.*.external_id' => 'min:1|max:255|nullable',
'transactions.*.recurrence_id' => 'min:1|max:255|nullable',
'transactions.*.bunq_payment_id' => 'min:1|max:255|nullable',
'transactions.*.external_url' => sprintf('min:1|max:255|nullable|url:%s', $validProtocols),
// SEPA fields:
'transactions.*.sepa_cc' => 'min:1|max:255|nullable',
'transactions.*.sepa_ct_op' => 'min:1|max:255|nullable',
'transactions.*.sepa_ct_id' => 'min:1|max:255|nullable',
'transactions.*.sepa_db' => 'min:1|max:255|nullable',
'transactions.*.sepa_country' => 'min:1|max:255|nullable',
'transactions.*.sepa_ep' => 'min:1|max:255|nullable',
'transactions.*.sepa_ci' => 'min:1|max:255|nullable',
'transactions.*.sepa_batch_id' => 'min:1|max:255|nullable',
// dates
'transactions.*.interest_date' => 'date|nullable',
'transactions.*.book_date' => 'date|nullable',
'transactions.*.process_date' => 'date|nullable',
'transactions.*.due_date' => 'date|nullable',
'transactions.*.payment_date' => 'date|nullable',
'transactions.*.invoice_date' => 'date|nullable',
];
}
/**
* Configure the validator instance.
*/
public function withValidator(Validator $validator): void
{
app('log')->debug('Now in withValidator');
/** @var TransactionGroup $transactionGroup */
$transactionGroup = $this->route()->parameter('userGroupTransaction');
$validator->after(
function (Validator $validator) use ($transactionGroup): void {
// if more than one, verify that there are journal ID's present.
$this->validateJournalIds($validator, $transactionGroup);
// all transaction types must be equal:
$this->validateTransactionTypesForUpdate($validator);
// user wants to update a reconciled transaction.
// source, destination, amount + foreign_amount cannot be changed
// and must be omitted from the request.
$this->preventUpdateReconciled($validator, $transactionGroup);
// validate source/destination is equal, depending on the transaction journal type.
$this->validateEqualAccountsForUpdate($validator, $transactionGroup);
// see method:
// $this->preventNoAccountInfo($validator, );
// validate that the currency fits the source and/or destination account.
// validate all account info
$this->validateAccountInformationUpdate($validator, $transactionGroup);
}
);
if ($validator->fails()) {
Log::channel('audit')->error(sprintf('Validation errors in %s', __CLASS__), $validator->errors()->toArray());
}
}
}

View File

@@ -248,29 +248,14 @@ class FixAccountTypes extends Command
}
}
private function isLiability(string $destinationType): bool
{
return AccountType::LOAN === $destinationType || AccountType::DEBT === $destinationType || AccountType::MORTGAGE === $destinationType;
}
private function shouldBeTransfer(string $transactionType, string $sourceType, string $destinationType): bool
{
return TransactionType::TRANSFER === $transactionType && AccountType::ASSET === $sourceType && $this->isLiability($destinationType);
}
private function shouldBeDeposit(string $transactionType, string $sourceType, string $destinationType): bool
private function isLiability(string $destinationType): bool
{
return TransactionType::TRANSFER === $transactionType && $this->isLiability($sourceType) && AccountType::ASSET === $destinationType;
}
private function shouldGoToExpenseAccount(string $transactionType, string $sourceType, string $destinationType): bool
{
return TransactionType::WITHDRAWAL === $transactionType && AccountType::ASSET === $sourceType && AccountType::REVENUE === $destinationType;
}
private function shouldComeFromRevenueAccount(string $transactionType, string $sourceType, string $destinationType): bool
{
return TransactionType::DEPOSIT === $transactionType && AccountType::EXPENSE === $sourceType && AccountType::ASSET === $destinationType;
return AccountType::LOAN === $destinationType || AccountType::DEBT === $destinationType || AccountType::MORTGAGE === $destinationType;
}
private function makeTransfer(TransactionJournal $journal): void
@@ -286,6 +271,11 @@ class FixAccountTypes extends Command
$this->inspectJournal($journal);
}
private function shouldBeDeposit(string $transactionType, string $sourceType, string $destinationType): bool
{
return TransactionType::TRANSFER === $transactionType && $this->isLiability($sourceType) && AccountType::ASSET === $destinationType;
}
private function makeDeposit(TransactionJournal $journal): void
{
// from a liability to an asset should be a deposit.
@@ -299,6 +289,11 @@ class FixAccountTypes extends Command
$this->inspectJournal($journal);
}
private function shouldGoToExpenseAccount(string $transactionType, string $sourceType, string $destinationType): bool
{
return TransactionType::WITHDRAWAL === $transactionType && AccountType::ASSET === $sourceType && AccountType::REVENUE === $destinationType;
}
private function makeExpenseDestination(TransactionJournal $journal, Transaction $destination): void
{
// withdrawals with a revenue account as destination instead of an expense account.
@@ -320,6 +315,11 @@ class FixAccountTypes extends Command
$this->inspectJournal($journal);
}
private function shouldComeFromRevenueAccount(string $transactionType, string $sourceType, string $destinationType): bool
{
return TransactionType::DEPOSIT === $transactionType && AccountType::EXPENSE === $sourceType && AccountType::ASSET === $destinationType;
}
private function makeRevenueSource(TransactionJournal $journal, Transaction $source): void
{
// deposits with an expense account as source instead of a revenue account.
@@ -355,11 +355,6 @@ class FixAccountTypes extends Command
return in_array($accountType, $validTypes, true);
}
private function canCreateDestination(array $validDestinations): bool
{
return in_array(AccountTypeEnum::EXPENSE->value, $validDestinations, true);
}
private function giveNewRevenue(TransactionJournal $journal, Transaction $source): void
{
app('log')->debug(sprintf('An account of type "%s" could be a valid source.', AccountTypeEnum::REVENUE->value));
@@ -373,6 +368,11 @@ class FixAccountTypes extends Command
$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
{
app('log')->debug(sprintf('An account of type "%s" could be a valid destination.', AccountTypeEnum::EXPENSE->value));

View File

@@ -57,6 +57,19 @@ class CreateGroupMemberships extends Command
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.
*
@@ -93,17 +106,4 @@ class CreateGroupMemberships extends Command
$user->save();
}
}
/**
* @throws FireflyException
*/
private function createGroupMemberships(): void
{
$users = User::get();
/** @var User $user */
foreach ($users as $user) {
self::createGroupMembership($user);
}
}
}

View File

@@ -71,7 +71,7 @@ class MigrateRecurrenceMeta extends Command
{
$configVar = app('fireflyconfig')->get(self::CONFIG_NAME, false);
if (null !== $configVar) {
return (bool) $configVar->data;
return (bool)$configVar->data;
}
return false;

View File

@@ -104,7 +104,7 @@ class MigrateToGroups extends Command
{
$configVar = app('fireflyconfig')->get(self::CONFIG_NAME, false);
if (null !== $configVar) {
return (bool) $configVar->data;
return (bool)$configVar->data;
}
return false;
@@ -193,22 +193,6 @@ class MigrateToGroups extends Command
);
}
private function findOpposingTransaction(TransactionJournal $journal, Transaction $transaction): ?Transaction
{
$set = $journal->transactions->filter(
static function (Transaction $subject) use ($transaction) {
$amount = (float) $transaction->amount * -1 === (float) $subject->amount; // intentional float
$identifier = $transaction->identifier === $subject->identifier;
app('log')->debug(sprintf('Amount the same? %s', var_export($amount, true)));
app('log')->debug(sprintf('ID the same? %s', var_export($identifier, true)));
return $amount && $identifier;
}
);
return $set->first();
}
/**
* @SuppressWarnings(PHPMD.ExcessiveMethodLength)
*/
@@ -299,6 +283,22 @@ class MigrateToGroups extends Command
];
}
private function findOpposingTransaction(TransactionJournal $journal, Transaction $transaction): ?Transaction
{
$set = $journal->transactions->filter(
static function (Transaction $subject) use ($transaction) {
$amount = (float)$transaction->amount * -1 === (float)$subject->amount; // intentional float
$identifier = $transaction->identifier === $subject->identifier;
app('log')->debug(sprintf('Amount the same? %s', var_export($amount, true)));
app('log')->debug(sprintf('ID the same? %s', var_export($identifier, true)));
return $amount && $identifier;
}
);
return $set->first();
}
private function getTransactionBudget(Transaction $left, Transaction $right): ?int
{
app('log')->debug('Now in getTransactionBudget()');

View File

@@ -117,6 +117,7 @@ class GracefulNotFoundHandler extends ExceptionHandler
return redirect(route('tags.index'));
case 'categories.show':
case 'categories.edit':
case 'categories.show.all':
$request->session()->reflash();

View File

@@ -128,7 +128,7 @@ class Handler extends ExceptionHandler
$errorCode = 500;
$errorCode = $e instanceof MethodNotAllowedHttpException ? 405 : $errorCode;
$isDebug = (bool) config('app.debug', false);
$isDebug = (bool)config('app.debug', false);
if ($isDebug) {
app('log')->debug(sprintf('Return JSON %s with debug.', get_class($e)));
@@ -185,7 +185,7 @@ class Handler extends ExceptionHandler
*/
public function report(\Throwable $e): void
{
$doMailError = (bool) config('firefly.send_error_message');
$doMailError = (bool)config('firefly.send_error_message');
if ($this->shouldntReportLocal($e) || !$doMailError) {
parent::report($e);
@@ -221,12 +221,22 @@ class Handler extends ExceptionHandler
// create job that will mail.
$ipAddress = request()->ip() ?? '0.0.0.0';
$job = new MailError($userData, (string) config('firefly.site_owner'), $ipAddress, $data);
$job = new MailError($userData, (string)config('firefly.site_owner'), $ipAddress, $data);
dispatch($job);
parent::report($e);
}
private function shouldntReportLocal(\Throwable $e): bool
{
return null !== Arr::first(
$this->dontReport,
static function ($type) use ($e) {
return $e instanceof $type;
}
);
}
/**
* Convert a validation exception into a response.
*
@@ -244,16 +254,6 @@ class Handler extends ExceptionHandler
;
}
private function shouldntReportLocal(\Throwable $e): bool
{
return null !== Arr::first(
$this->dontReport,
static function ($type) use ($e) {
return $e instanceof $type;
}
);
}
/**
* Only return the redirectTo property from the exception if it is a valid URL. Return NULL otherwise.
*/

View File

@@ -48,7 +48,7 @@ final class IntervalException extends \Exception
Periodicity $periodicity,
array $intervals,
int $code = 0,
?\Throwable $previous = null
?\Throwable $previous = null
): self {
$message = sprintf(
'The periodicity %s is unknown. Choose one of available periodicity: %s',

View File

@@ -121,21 +121,6 @@ class AccountFactory
return $return;
}
public function find(string $accountName, string $accountType): ?Account
{
app('log')->debug(sprintf('Now in AccountFactory::find("%s", "%s")', $accountName, $accountType));
$type = AccountType::whereType($accountType)->first();
// @var Account|null
return $this->user->accounts()->where('account_type_id', $type->id)->where('name', $accountName)->first();
}
public function setUser(User $user): void
{
$this->user = $user;
$this->accountRepository->setUser($user);
}
/**
* @throws FireflyException
*/
@@ -169,6 +154,15 @@ class AccountFactory
return $result;
}
public function find(string $accountName, string $accountType): ?Account
{
app('log')->debug(sprintf('Now in AccountFactory::find("%s", "%s")', $accountName, $accountType));
$type = AccountType::whereType($accountType)->first();
// @var Account|null
return $this->user->accounts()->where('account_type_id', $type->id)->where('name', $accountName)->first();
}
/**
* @throws FireflyException
*/
@@ -365,4 +359,10 @@ class AccountFactory
$updateService->setUser($account->user);
$updateService->update($account, ['order' => $order]);
}
public function setUser(User $user): void
{
$this->user = $user;
$this->accountRepository->setUser($user);
}
}

View File

@@ -79,7 +79,7 @@ class RecurrenceFactory
$firstDate = $data['recurrence']['first_date'];
}
if (array_key_exists('nr_of_repetitions', $data['recurrence'])) {
$repetitions = (int) $data['recurrence']['nr_of_repetitions'];
$repetitions = (int)$data['recurrence']['nr_of_repetitions'];
}
if (array_key_exists('repeat_until', $data['recurrence'])) {
$repeatUntil = $data['recurrence']['repeat_until'];
@@ -116,7 +116,7 @@ class RecurrenceFactory
$recurrence->save();
if (array_key_exists('notes', $data['recurrence'])) {
$this->updateNote($recurrence, (string) $data['recurrence']['notes']);
$this->updateNote($recurrence, (string)$data['recurrence']['notes']);
}
$this->createRepetitions($recurrence, $data['repetitions'] ?? []);

View File

@@ -72,64 +72,6 @@ class TransactionFactory
return $this->create(app('steam')->negative($amount), $foreignAmount);
}
/**
* Create transaction with positive amount (for destination accounts).
*
* @throws FireflyException
*/
public function createPositive(string $amount, ?string $foreignAmount): Transaction
{
if ('' === $foreignAmount) {
$foreignAmount = null;
}
if (null !== $foreignAmount) {
$foreignAmount = app('steam')->positive($foreignAmount);
}
return $this->create(app('steam')->positive($amount), $foreignAmount);
}
public function setAccount(Account $account): void
{
$this->account = $account;
}
public function setAccountInformation(array $accountInformation): void
{
$this->accountInformation = $accountInformation;
}
public function setCurrency(TransactionCurrency $currency): void
{
$this->currency = $currency;
}
/**
* @param null|TransactionCurrency $foreignCurrency |null
*/
public function setForeignCurrency(?TransactionCurrency $foreignCurrency): void
{
$this->foreignCurrency = $foreignCurrency;
}
public function setJournal(TransactionJournal $journal): void
{
$this->journal = $journal;
}
public function setReconciled(bool $reconciled): void
{
$this->reconciled = $reconciled;
}
/**
* @SuppressWarnings(PHPMD.UnusedFormalParameter)
*/
public function setUser(User $user): void
{
// empty function.
}
/**
* @throws FireflyException
*/
@@ -224,4 +166,62 @@ class TransactionFactory
$service = app(AccountUpdateService::class);
$service->update($this->account, ['iban' => $this->accountInformation['iban']]);
}
/**
* Create transaction with positive amount (for destination accounts).
*
* @throws FireflyException
*/
public function createPositive(string $amount, ?string $foreignAmount): Transaction
{
if ('' === $foreignAmount) {
$foreignAmount = null;
}
if (null !== $foreignAmount) {
$foreignAmount = app('steam')->positive($foreignAmount);
}
return $this->create(app('steam')->positive($amount), $foreignAmount);
}
public function setAccount(Account $account): void
{
$this->account = $account;
}
public function setAccountInformation(array $accountInformation): void
{
$this->accountInformation = $accountInformation;
}
public function setCurrency(TransactionCurrency $currency): void
{
$this->currency = $currency;
}
/**
* @param null|TransactionCurrency $foreignCurrency |null
*/
public function setForeignCurrency(?TransactionCurrency $foreignCurrency): void
{
$this->foreignCurrency = $foreignCurrency;
}
public function setJournal(TransactionJournal $journal): void
{
$this->journal = $journal;
}
public function setReconciled(bool $reconciled): void
{
$this->reconciled = $reconciled;
}
/**
* @SuppressWarnings(PHPMD.UnusedFormalParameter)
*/
public function setUser(User $user): void
{
// empty function.
}
}

View File

@@ -141,49 +141,6 @@ class TransactionJournalFactory
return $collection;
}
/**
* Set the user.
*/
public function setUser(User $user): void
{
$this->user = $user;
$this->currencyRepository->setUser($this->user);
$this->tagFactory->setUser($user);
$this->billRepository->setUser($this->user);
$this->budgetRepository->setUser($this->user);
$this->categoryRepository->setUser($this->user);
$this->piggyRepository->setUser($this->user);
$this->accountRepository->setUser($this->user);
}
public function setErrorOnHash(bool $errorOnHash): void
{
$this->errorOnHash = $errorOnHash;
if (true === $errorOnHash) {
app('log')->info('Will trigger duplication alert for this journal.');
}
}
protected function storeMeta(TransactionJournal $journal, NullArrayObject $data, string $field): void
{
$set = [
'journal' => $journal,
'name' => $field,
'data' => (string) ($data[$field] ?? ''),
];
if ($data[$field] instanceof Carbon) {
$data[$field]->setTimezone(config('app.timezone'));
app('log')->debug(sprintf('%s Date: %s (%s)', $field, $data[$field], $data[$field]->timezone->getName()));
$set['data'] = $data[$field]->format('Y-m-d H:i:s');
}
app('log')->debug(sprintf('Going to store meta-field "%s", with value "%s".', $set['name'], $set['data']));
/** @var TransactionJournalMetaFactory $factory */
$factory = app(TransactionJournalMetaFactory::class);
$factory->updateOrCreate($set);
}
/**
* TODO typeOverrule: the account validator may have another opinion on the transaction type. not sure what to do
* with this.
@@ -203,11 +160,11 @@ class TransactionJournalFactory
$type = $this->typeRepository->findTransactionType(null, $row['type']);
$carbon = $row['date'] ?? today(config('app.timezone'));
$order = $row['order'] ?? 0;
$currency = $this->currencyRepository->findCurrency((int) $row['currency_id'], $row['currency_code']);
$currency = $this->currencyRepository->findCurrency((int)$row['currency_id'], $row['currency_code']);
$foreignCurrency = $this->currencyRepository->findCurrencyNull($row['foreign_currency_id'], $row['foreign_currency_code']);
$bill = $this->billRepository->findBill((int) $row['bill_id'], $row['bill_name']);
$bill = $this->billRepository->findBill((int)$row['bill_id'], $row['bill_name']);
$billId = TransactionType::WITHDRAWAL === $type->type && null !== $bill ? $bill->id : null;
$description = (string) $row['description'];
$description = (string)$row['description'];
// Manipulate basic fields
$carbon->setTimezone(config('app.timezone'));
@@ -286,7 +243,7 @@ class TransactionJournalFactory
$transactionFactory->setReconciled($row['reconciled'] ?? false);
try {
$negative = $transactionFactory->createNegative((string) $row['amount'], (string) $row['foreign_amount']);
$negative = $transactionFactory->createNegative((string)$row['amount'], (string)$row['foreign_amount']);
} catch (FireflyException $e) {
app('log')->error(sprintf('Exception creating negative transaction: %s', $e->getMessage()));
$this->forceDeleteOnError(new Collection([$journal]));
@@ -305,7 +262,7 @@ class TransactionJournalFactory
$transactionFactory->setReconciled($row['reconciled'] ?? false);
try {
$transactionFactory->createPositive((string) $row['amount'], (string) $row['foreign_amount']);
$transactionFactory->createPositive((string)$row['amount'], (string)$row['foreign_amount']);
} catch (FireflyException $e) {
app('log')->error(sprintf('Exception creating positive transaction: %s', $e->getMessage()));
$this->forceTrDelete($negative);
@@ -326,18 +283,6 @@ class TransactionJournalFactory
return $journal;
}
private function storeLocation(TransactionJournal $journal, NullArrayObject $data): void
{
if (true === $data['store_location']) {
$location = new Location();
$location->longitude = $data['longitude'];
$location->latitude = $data['latitude'];
$location->zoom_level = $data['zoom_level'];
$location->locatable()->associate($journal);
$location->save();
}
}
private function hashArray(NullArrayObject $row): string
{
$dataRow = $row->getArrayCopy();
@@ -382,7 +327,7 @@ class TransactionJournalFactory
app('log')->warning(sprintf('Found a duplicate in errorIfDuplicate because hash %s is not unique!', $hash));
$journal = $result->transactionJournal()->withTrashed()->first();
$group = $journal?->transactionGroup()->withTrashed()->first();
$groupId = (int) $group?->id;
$groupId = (int)$group?->id;
throw new DuplicateTransactionException(sprintf('Duplicate of transaction #%d.', $groupId));
}
@@ -400,10 +345,10 @@ class TransactionJournalFactory
// validate source account.
$array = [
'id' => null !== $data['source_id'] ? (int) $data['source_id'] : null,
'name' => null !== $data['source_name'] ? (string) $data['source_name'] : null,
'iban' => null !== $data['source_iban'] ? (string) $data['source_iban'] : null,
'number' => null !== $data['source_number'] ? (string) $data['source_number'] : null,
'id' => null !== $data['source_id'] ? (int)$data['source_id'] : null,
'name' => null !== $data['source_name'] ? (string)$data['source_name'] : null,
'iban' => null !== $data['source_iban'] ? (string)$data['source_iban'] : null,
'number' => null !== $data['source_number'] ? (string)$data['source_number'] : null,
];
$validSource = $this->accountValidator->validateSource($array);
@@ -415,10 +360,10 @@ class TransactionJournalFactory
// validate destination account
$array = [
'id' => null !== $data['destination_id'] ? (int) $data['destination_id'] : null,
'name' => null !== $data['destination_name'] ? (string) $data['destination_name'] : null,
'iban' => null !== $data['destination_iban'] ? (string) $data['destination_iban'] : null,
'number' => null !== $data['destination_number'] ? (string) $data['destination_number'] : null,
'id' => null !== $data['destination_id'] ? (int)$data['destination_id'] : null,
'name' => null !== $data['destination_name'] ? (string)$data['destination_name'] : null,
'iban' => null !== $data['destination_iban'] ? (string)$data['destination_iban'] : null,
'number' => null !== $data['destination_number'] ? (string)$data['destination_number'] : null,
];
$validDestination = $this->accountValidator->validateDestination($array);
@@ -428,6 +373,21 @@ class TransactionJournalFactory
}
}
/**
* Set the user.
*/
public function setUser(User $user): void
{
$this->user = $user;
$this->currencyRepository->setUser($this->user);
$this->tagFactory->setUser($user);
$this->billRepository->setUser($this->user);
$this->budgetRepository->setUser($this->user);
$this->categoryRepository->setUser($this->user);
$this->piggyRepository->setUser($this->user);
$this->accountRepository->setUser($this->user);
}
private function reconciliationSanityCheck(?Account $sourceAccount, ?Account $destinationAccount): array
{
app('log')->debug(sprintf('Now in %s', __METHOD__));
@@ -550,7 +510,7 @@ class TransactionJournalFactory
{
app('log')->debug('Will now store piggy event.');
$piggyBank = $this->piggyRepository->findPiggyBank((int) $data['piggy_bank_id'], $data['piggy_bank_name']);
$piggyBank = $this->piggyRepository->findPiggyBank((int)$data['piggy_bank_id'], $data['piggy_bank_name']);
if (null !== $piggyBank) {
$this->piggyEventFactory->create($journal, $piggyBank);
@@ -567,4 +527,44 @@ class TransactionJournalFactory
$this->storeMeta($journal, $transaction, $field);
}
}
protected function storeMeta(TransactionJournal $journal, NullArrayObject $data, string $field): void
{
$set = [
'journal' => $journal,
'name' => $field,
'data' => (string)($data[$field] ?? ''),
];
if ($data[$field] instanceof Carbon) {
$data[$field]->setTimezone(config('app.timezone'));
app('log')->debug(sprintf('%s Date: %s (%s)', $field, $data[$field], $data[$field]->timezone->getName()));
$set['data'] = $data[$field]->format('Y-m-d H:i:s');
}
app('log')->debug(sprintf('Going to store meta-field "%s", with value "%s".', $set['name'], $set['data']));
/** @var TransactionJournalMetaFactory $factory */
$factory = app(TransactionJournalMetaFactory::class);
$factory->updateOrCreate($set);
}
private function storeLocation(TransactionJournal $journal, NullArrayObject $data): void
{
if (true === $data['store_location']) {
$location = new Location();
$location->longitude = $data['longitude'];
$location->latitude = $data['latitude'];
$location->zoom_level = $data['zoom_level'];
$location->locatable()->associate($journal);
$location->save();
}
}
public function setErrorOnHash(bool $errorOnHash): void
{
$this->errorOnHash = $errorOnHash;
if (true === $errorOnHash) {
app('log')->info('Will trigger duplication alert for this journal.');
}
}
}

View File

@@ -67,6 +67,14 @@ class MonthReportGenerator implements ReportGeneratorInterface
return $result;
}
/**
* Return the preferred period.
*/
protected function preferredPeriod(): string
{
return 'day';
}
/**
* Set accounts.
*/
@@ -130,12 +138,4 @@ class MonthReportGenerator implements ReportGeneratorInterface
{
return $this;
}
/**
* Return the preferred period.
*/
protected function preferredPeriod(): string
{
return 'day';
}
}

View File

@@ -125,26 +125,6 @@ class MonthReportGenerator implements ReportGeneratorInterface
return $this;
}
/**
* Set the involved budgets.
*/
public function setBudgets(Collection $budgets): ReportGeneratorInterface
{
$this->budgets = $budgets;
return $this;
}
/**
* Set the involved accounts.
*/
public function setAccounts(Collection $accounts): ReportGeneratorInterface
{
$this->accounts = $accounts;
return $this;
}
/**
* Get the expenses.
*/
@@ -170,4 +150,24 @@ class MonthReportGenerator implements ReportGeneratorInterface
return $journals;
}
/**
* Set the involved budgets.
*/
public function setBudgets(Collection $budgets): ReportGeneratorInterface
{
$this->budgets = $budgets;
return $this;
}
/**
* Set the involved accounts.
*/
public function setAccounts(Collection $accounts): ReportGeneratorInterface
{
$this->accounts = $accounts;
return $this;
}
}

View File

@@ -124,26 +124,6 @@ class MonthReportGenerator implements ReportGeneratorInterface
return $this;
}
/**
* Set the categories involved in this report.
*/
public function setCategories(Collection $categories): ReportGeneratorInterface
{
$this->categories = $categories;
return $this;
}
/**
* Set the involved accounts.
*/
public function setAccounts(Collection $accounts): ReportGeneratorInterface
{
$this->accounts = $accounts;
return $this;
}
/**
* Get the expenses for this report.
*/
@@ -168,6 +148,26 @@ class MonthReportGenerator implements ReportGeneratorInterface
return $transactions;
}
/**
* Set the categories involved in this report.
*/
public function setCategories(Collection $categories): ReportGeneratorInterface
{
$this->categories = $categories;
return $this;
}
/**
* Set the involved accounts.
*/
public function setAccounts(Collection $accounts): ReportGeneratorInterface
{
$this->accounts = $accounts;
return $this;
}
/**
* Get the income for this report.
*/

View File

@@ -72,31 +72,6 @@ class StandardMessageGenerator implements MessageGeneratorInterface
$this->run();
}
public function getVersion(): int
{
return $this->version;
}
public function setObjects(Collection $objects): void
{
$this->objects = $objects;
}
public function setTrigger(int $trigger): void
{
$this->trigger = $trigger;
}
public function setUser(User $user): void
{
$this->user = $user;
}
public function setWebhooks(Collection $webhooks): void
{
$this->webhooks = $webhooks;
}
private function getWebhooks(): Collection
{
return $this->user->webhooks()->where('active', true)->where('trigger', $this->trigger)->get(['webhooks.*']);
@@ -206,6 +181,11 @@ class StandardMessageGenerator implements MessageGeneratorInterface
$this->storeMessage($webhook, $basicMessage);
}
public function getVersion(): int
{
return $this->version;
}
private function collectAccounts(TransactionGroup $transactionGroup): Collection
{
$accounts = new Collection();
@@ -232,4 +212,24 @@ class StandardMessageGenerator implements MessageGeneratorInterface
$webhookMessage->save();
app('log')->debug(sprintf('Stored new webhook message #%d', $webhookMessage->id));
}
public function setObjects(Collection $objects): void
{
$this->objects = $objects;
}
public function setTrigger(int $trigger): void
{
$this->trigger = $trigger;
}
public function setUser(User $user): void
{
$this->user = $user;
}
public function setWebhooks(Collection $webhooks): void
{
$this->webhooks = $webhooks;
}
}

View File

@@ -49,20 +49,6 @@ class BudgetLimitHandler
$this->updateAvailableBudget($event->budgetLimit);
}
public function deleted(Deleted $event): void
{
app('log')->debug(sprintf('BudgetLimitHandler::deleted(#%s)', $event->budgetLimit->id));
$budgetLimit = $event->budgetLimit;
$budgetLimit->id = 0;
$this->updateAvailableBudget($event->budgetLimit);
}
public function updated(Updated $event): void
{
app('log')->debug(sprintf('BudgetLimitHandler::updated(#%s)', $event->budgetLimit->id));
$this->updateAvailableBudget($event->budgetLimit);
}
private function updateAvailableBudget(BudgetLimit $budgetLimit): void
{
app('log')->debug(sprintf('Now in updateAvailableBudget(#%d)', $budgetLimit->id));
@@ -248,4 +234,18 @@ class BudgetLimitHandler
return $amount;
}
public function deleted(Deleted $event): void
{
app('log')->debug(sprintf('BudgetLimitHandler::deleted(#%s)', $event->budgetLimit->id));
$budgetLimit = $event->budgetLimit;
$budgetLimit->id = 0;
$this->updateAvailableBudget($event->budgetLimit);
}
public function updated(Updated $event): void
{
app('log')->debug(sprintf('BudgetLimitHandler::updated(#%s)', $event->budgetLimit->id));
$this->updateAvailableBudget($event->budgetLimit);
}
}

View File

@@ -220,7 +220,7 @@ class UserEventHandler
public function sendAdminRegistrationNotification(RegisteredUser $event): void
{
$sendMail = (bool) app('fireflyconfig')->get('notification_admin_new_reg', true)->data;
$sendMail = (bool)app('fireflyconfig')->get('notification_admin_new_reg', true)->data;
if ($sendMail) {
/** @var UserRepositoryInterface $repository */
$repository = app(UserRepositoryInterface::class);
@@ -285,7 +285,7 @@ class UserEventHandler
$oldEmail = $event->oldEmail;
$user = $event->user;
$token = app('preferences')->getForUser($user, 'email_change_undo_token', 'invalid');
$hashed = hash('sha256', sprintf('%s%s', (string) config('app.key'), $oldEmail));
$hashed = hash('sha256', sprintf('%s%s', (string)config('app.key'), $oldEmail));
$url = route('profile.undo-email-change', [$token->data, $hashed]);
try {
@@ -347,7 +347,7 @@ class UserEventHandler
*/
public function sendRegistrationMail(RegisteredUser $event): void
{
$sendMail = (bool) app('fireflyconfig')->get('notification_user_new_reg', true)->data;
$sendMail = (bool)app('fireflyconfig')->get('notification_user_new_reg', true)->data;
if ($sendMail) {
try {
Notification::send($event->user, new UserRegistrationNotification());

View File

@@ -81,6 +81,27 @@ trait AttachmentCollection
return $this;
}
/**
* Join table to get attachment information.
*/
private function joinAttachmentTables(): void
{
if (false === $this->hasJoinedAttTables) {
// join some extra tables:
$this->hasJoinedAttTables = true;
$this->query->leftJoin('attachments', 'attachments.attachable_id', '=', 'transaction_journals.id')
->where(
static function (EloquentBuilder $q1): void { // @phpstan-ignore-line
$q1->where('attachments.attachable_type', TransactionJournal::class);
$q1->where('attachments.uploaded', true);
$q1->whereNull('attachments.deleted_at');
$q1->orWhereNull('attachments.attachable_type');
}
)
;
}
}
public function withAttachmentInformation(): GroupCollectorInterface
{
$this->fields[] = 'attachments.id as attachment_id';
@@ -511,25 +532,4 @@ trait AttachmentCollection
return $this;
}
/**
* Join table to get attachment information.
*/
private function joinAttachmentTables(): void
{
if (false === $this->hasJoinedAttTables) {
// join some extra tables:
$this->hasJoinedAttTables = true;
$this->query->leftJoin('attachments', 'attachments.attachable_id', '=', 'transaction_journals.id')
->where(
static function (EloquentBuilder $q1): void { // @phpstan-ignore-line
$q1->where('attachments.attachable_type', TransactionJournal::class);
$q1->where('attachments.uploaded', true);
$q1->whereNull('attachments.deleted_at');
$q1->orWhereNull('attachments.attachable_type');
}
)
;
}
}
}

View File

@@ -34,6 +34,10 @@ use Illuminate\Database\Eloquent\Relations\HasMany;
trait CollectorProperties
{
public const string TEST = 'Test';
/** @var array<int, string> */
public array $sorting;
private ?int $endRow;
private bool $expandGroupSearch;
private array $fields;
private bool $hasAccountInfo;
@@ -49,10 +53,8 @@ trait CollectorProperties
private ?int $page;
private array $postFilters;
private HasMany $query;
private ?int $startRow;
private array $stringFields;
private ?int $startRow;
private ?int $endRow;
/*
* This array is used to collect ALL tags the user may search for (using 'setTags').
* This way the user can call 'setTags' multiple times and get a joined result.

View File

@@ -171,6 +171,19 @@ trait MetaCollection
return $this;
}
/**
* Join table to get tag information.
*/
protected function joinMetaDataTables(): void
{
if (false === $this->hasJoinedMetaTables) {
$this->hasJoinedMetaTables = true;
$this->query->leftJoin('journal_meta', 'transaction_journals.id', '=', 'journal_meta.transaction_journal_id');
$this->fields[] = 'journal_meta.name as meta_name';
$this->fields[] = 'journal_meta.data as meta_data';
}
}
public function excludeExternalUrl(string $url): GroupCollectorInterface
{
$this->joinMetaDataTables();
@@ -369,6 +382,19 @@ trait MetaCollection
return $this;
}
/**
* Join table to get tag information.
*/
protected function joinTagTables(): void
{
if (false === $this->hasJoinedTagTables) {
// join some extra tables:
$this->hasJoinedTagTables = true;
$this->query->leftJoin('tag_transaction_journal', 'tag_transaction_journal.transaction_journal_id', '=', 'transaction_journals.id');
$this->query->leftJoin('tags', 'tag_transaction_journal.tag_id', '=', 'tags.id');
}
}
public function internalReferenceContains(string $internalReference): GroupCollectorInterface
{
$internalReference = (string)json_encode($internalReference);
@@ -539,6 +565,63 @@ trait MetaCollection
return $this;
}
/**
* Limit results to a SPECIFIC set of tags.
*/
public function setAllTags(Collection $tags): GroupCollectorInterface
{
Log::debug(sprintf('Now in setAllTags(%d tag(s))', $tags->count()));
$this->withTagInformation();
$this->query->whereNotNull('tag_transaction_journal.tag_id');
// this method adds a "postFilter" to the collector.
$list = $tags->pluck('tag')->toArray();
$list = array_map('strtolower', $list);
$filter = static function (array $object) use ($list): bool|array {
$includedJournals = [];
$return = $object;
unset($return['transactions']);
$return['transactions'] = [];
Log::debug(sprintf('Now in setAllTags(%s) filter', implode(', ', $list)));
$expectedTagCount = count($list);
$foundTagCount = 0;
foreach ($object['transactions'] as $transaction) {
$transactionTagCount = count($transaction['tags']);
app('log')->debug(sprintf('Transaction #%d has %d tag(s)', $transaction['transaction_journal_id'], $transactionTagCount));
if ($transactionTagCount < $expectedTagCount) {
app('log')->debug(sprintf('Transaction has %d tag(s), we expect %d tag(s), return false.', $transactionTagCount, $expectedTagCount));
return false;
}
foreach ($transaction['tags'] as $tag) {
Log::debug(sprintf('"%s" versus', strtolower($tag['name'])), $list);
if (in_array(strtolower($tag['name']), $list, true)) {
app('log')->debug(sprintf('Transaction has tag "%s" so count++.', $tag['name']));
++$foundTagCount;
$journalId = $transaction['transaction_journal_id'];
// #8377 prevent adding a transaction twice when multiple tag searches find this transaction
if (!in_array($journalId, $includedJournals, true)) {
$includedJournals[] = $journalId;
$return['transactions'][] = $transaction;
}
}
}
}
Log::debug(sprintf('Found %d tags, need at least %d.', $foundTagCount, $expectedTagCount));
// found at least the expected tags.
$result = $foundTagCount >= $expectedTagCount;
if (true === $result) {
return $return;
}
return false;
};
$this->postFilters[] = $filter;
return $this;
}
/**
* Limit the search to a specific bill.
*/
@@ -669,63 +752,6 @@ trait MetaCollection
return $this;
}
/**
* Limit results to a SPECIFIC set of tags.
*/
public function setAllTags(Collection $tags): GroupCollectorInterface
{
Log::debug(sprintf('Now in setAllTags(%d tag(s))', $tags->count()));
$this->withTagInformation();
$this->query->whereNotNull('tag_transaction_journal.tag_id');
// this method adds a "postFilter" to the collector.
$list = $tags->pluck('tag')->toArray();
$list = array_map('strtolower', $list);
$filter = static function (array $object) use ($list): bool|array {
$includedJournals = [];
$return = $object;
unset($return['transactions']);
$return['transactions'] = [];
Log::debug(sprintf('Now in setAllTags(%s) filter', implode(', ', $list)));
$expectedTagCount = count($list);
$foundTagCount = 0;
foreach ($object['transactions'] as $transaction) {
$transactionTagCount = count($transaction['tags']);
app('log')->debug(sprintf('Transaction #%d has %d tag(s)', $transaction['transaction_journal_id'], $transactionTagCount));
if ($transactionTagCount < $expectedTagCount) {
app('log')->debug(sprintf('Transaction has %d tag(s), we expect %d tag(s), return false.', $transactionTagCount, $expectedTagCount));
return false;
}
foreach ($transaction['tags'] as $tag) {
Log::debug(sprintf('"%s" versus', strtolower($tag['name'])), $list);
if (in_array(strtolower($tag['name']), $list, true)) {
app('log')->debug(sprintf('Transaction has tag "%s" so count++.', $tag['name']));
++$foundTagCount;
$journalId = $transaction['transaction_journal_id'];
// #8377 prevent adding a transaction twice when multiple tag searches find this transaction
if (!in_array($journalId, $includedJournals, true)) {
$includedJournals[] = $journalId;
$return['transactions'][] = $transaction;
}
}
}
}
Log::debug(sprintf('Found %d tags, need at least %d.', $foundTagCount, $expectedTagCount));
// found at least the expected tags.
$result = $foundTagCount >= $expectedTagCount;
if (true === $result) {
return $return;
}
return false;
};
$this->postFilters[] = $filter;
return $this;
}
/**
* Limit results to any of the tags in the list.
*/
@@ -938,30 +964,4 @@ trait MetaCollection
return $this;
}
/**
* Join table to get tag information.
*/
protected function joinMetaDataTables(): void
{
if (false === $this->hasJoinedMetaTables) {
$this->hasJoinedMetaTables = true;
$this->query->leftJoin('journal_meta', 'transaction_journals.id', '=', 'journal_meta.transaction_journal_id');
$this->fields[] = 'journal_meta.name as meta_name';
$this->fields[] = 'journal_meta.data as meta_data';
}
}
/**
* Join table to get tag information.
*/
protected function joinTagTables(): void
{
if (false === $this->hasJoinedTagTables) {
// join some extra tables:
$this->hasJoinedTagTables = true;
$this->query->leftJoin('tag_transaction_journal', 'tag_transaction_journal.transaction_journal_id', '=', 'transaction_journals.id');
$this->query->leftJoin('tags', 'tag_transaction_journal.tag_id', '=', 'tags.id');
}
}
}

View File

@@ -25,6 +25,7 @@ namespace FireflyIII\Helpers\Collector;
use Carbon\Carbon;
use Carbon\Exceptions\InvalidFormatException;
use Exception;
use FireflyIII\Exceptions\FireflyException;
use FireflyIII\Helpers\Collector\Extensions\AccountCollection;
use FireflyIII\Helpers\Collector\Extensions\AmountCollection;
@@ -61,6 +62,7 @@ class GroupCollector implements GroupCollectorInterface
*/
public function __construct()
{
$this->sorting = [];
$this->postFilters = [];
$this->tags = [];
$this->user = null;
@@ -466,6 +468,9 @@ class GroupCollector implements GroupCollectorInterface
// filter the array using all available post filters:
$collection = $this->postFilterCollection($collection);
// sort the collection, if sort instructions are present.
$collection = $this->sortCollection($collection);
// count it and continue:
$this->total = $collection->count();
@@ -483,226 +488,6 @@ class GroupCollector implements GroupCollectorInterface
return $collection;
}
/**
* Same as getGroups but everything is in a paginator.
*/
public function getPaginatedGroups(): LengthAwarePaginator
{
$set = $this->getGroups();
if (0 === $this->limit) {
$this->setLimit(50);
}
if (null !== $this->startRow && null !== $this->endRow) {
$total = $this->endRow - $this->startRow;
return new LengthAwarePaginator($set, $this->total, $total, 1);
}
return new LengthAwarePaginator($set, $this->total, $this->limit, $this->page);
}
/**
* Limit the number of returned entries.
*/
public function setLimit(int $limit): GroupCollectorInterface
{
$this->limit = $limit;
// app('log')->debug(sprintf('GroupCollector: The limit is now %d', $limit));
return $this;
}
public function isNotReconciled(): GroupCollectorInterface
{
$this->query->where('source.reconciled', 0)->where('destination.reconciled', 0);
return $this;
}
public function isReconciled(): GroupCollectorInterface
{
$this->query->where('source.reconciled', 1)->where('destination.reconciled', 1);
return $this;
}
/**
* Limit results to a specific currency, either foreign or normal one.
*/
public function setCurrency(TransactionCurrency $currency): GroupCollectorInterface
{
$this->query->where(
static function (EloquentBuilder $q) use ($currency): void { // @phpstan-ignore-line
$q->where('source.transaction_currency_id', $currency->id);
$q->orWhere('source.foreign_currency_id', $currency->id);
}
);
return $this;
}
public function setExpandGroupSearch(bool $expandGroupSearch): GroupCollectorInterface
{
$this->expandGroupSearch = $expandGroupSearch;
return $this;
}
public function setForeignCurrency(TransactionCurrency $currency): GroupCollectorInterface
{
$this->query->where('source.foreign_currency_id', $currency->id);
return $this;
}
/**
* Limit the result to a set of specific transaction groups.
*/
public function setIds(array $groupIds): GroupCollectorInterface
{
$this->query->whereIn('transaction_groups.id', $groupIds);
return $this;
}
/**
* Limit the result to a set of specific journals.
*/
public function setJournalIds(array $journalIds): GroupCollectorInterface
{
if (0 !== count($journalIds)) {
// make all integers.
$integerIDs = array_map('intval', $journalIds);
Log::debug(sprintf('GroupCollector: setJournalIds: %s', implode(', ', $integerIDs)));
$this->query->whereIn('transaction_journals.id', $integerIDs);
}
return $this;
}
/**
* Set the page to get.
*/
public function setPage(int $page): GroupCollectorInterface
{
$page = 0 === $page ? 1 : $page;
$this->page = $page;
// app('log')->debug(sprintf('GroupCollector: page is now %d', $page));
return $this;
}
/**
* Search for words in descriptions.
*/
public function setSearchWords(array $array): GroupCollectorInterface
{
if (0 === count($array)) {
return $this;
}
$this->query->where(
static function (EloquentBuilder $q) use ($array): void { // @phpstan-ignore-line
$q->where(
static function (EloquentBuilder $q1) use ($array): void {
foreach ($array as $word) {
$keyword = sprintf('%%%s%%', $word);
$q1->where('transaction_journals.description', 'LIKE', $keyword);
}
}
);
$q->orWhere(
static function (EloquentBuilder $q2) use ($array): void {
foreach ($array as $word) {
$keyword = sprintf('%%%s%%', $word);
$q2->where('transaction_groups.title', 'LIKE', $keyword);
}
}
);
}
);
return $this;
}
/**
* Limit the search to one specific transaction group.
*/
public function setTransactionGroup(TransactionGroup $transactionGroup): GroupCollectorInterface
{
$this->query->where('transaction_groups.id', $transactionGroup->id);
return $this;
}
/**
* Limit the included transaction types.
*/
public function setTypes(array $types): GroupCollectorInterface
{
$this->query->whereIn('transaction_types.type', $types);
return $this;
}
/**
* Set the user object and start the query.
*/
public function setUser(User $user): GroupCollectorInterface
{
if (null === $this->user) {
$this->user = $user;
$this->startQuery();
}
return $this;
}
/**
* Set the user object and start the query.
*/
public function setUserGroup(UserGroup $userGroup): GroupCollectorInterface
{
if (null === $this->userGroup) {
$this->userGroup = $userGroup;
$this->startQueryForGroup();
}
return $this;
}
/**
* Automatically include all stuff required to make API calls work.
*/
public function withAPIInformation(): GroupCollectorInterface
{
// include source + destination account name and type.
$this->withAccountInformation()
// include category ID + name (if any)
->withCategoryInformation()
// include budget ID + name (if any)
->withBudgetInformation()
// include bill ID + name (if any)
->withBillInformation()
;
return $this;
}
public function setEndRow(int $endRow): self
{
$this->endRow = $endRow;
return $this;
}
public function setStartRow(int $startRow): self
{
$this->startRow = $startRow;
return $this;
}
private function getCollectedGroupIds(): array
{
return $this->query->get(['transaction_journals.transaction_group_id'])->pluck('transaction_group_id')->toArray();
@@ -998,6 +783,233 @@ class GroupCollector implements GroupCollectorInterface
return $currentCollection;
}
#[\Override]
public function sortCollection(Collection $collection): Collection
{
/**
* @var string $field
* @var string $direction
*/
foreach ($this->sorting as $field => $direction) {
$func = 'ASC' === $direction ? 'sortBy' : 'sortByDesc';
$collection = $collection->{$func}(function (array $product, int $key) use ($field) { // @phpstan-ignore-line
// depends on $field:
if ('description' === $field) {
if (1 === count($product['transactions'])) {
return array_values($product['transactions'])[0][$field];
}
if (count($product['transactions']) > 1) {
return $product['title'];
}
return 'zzz';
}
exit('here we are');
});
}
return $collection;
}
/**
* Same as getGroups but everything is in a paginator.
*/
public function getPaginatedGroups(): LengthAwarePaginator
{
$set = $this->getGroups();
if (0 === $this->limit) {
$this->setLimit(50);
}
if (null !== $this->startRow && null !== $this->endRow) {
/** @var int $total */
$total = $this->endRow - $this->startRow;
return new LengthAwarePaginator($set, $this->total, $total, 1);
}
return new LengthAwarePaginator($set, $this->total, $this->limit, $this->page);
}
/**
* Limit the number of returned entries.
*/
public function setLimit(int $limit): GroupCollectorInterface
{
$this->limit = $limit;
// app('log')->debug(sprintf('GroupCollector: The limit is now %d', $limit));
return $this;
}
public function isNotReconciled(): GroupCollectorInterface
{
$this->query->where('source.reconciled', 0)->where('destination.reconciled', 0);
return $this;
}
public function isReconciled(): GroupCollectorInterface
{
$this->query->where('source.reconciled', 1)->where('destination.reconciled', 1);
return $this;
}
/**
* Limit results to a specific currency, either foreign or normal one.
*/
public function setCurrency(TransactionCurrency $currency): GroupCollectorInterface
{
$this->query->where(
static function (EloquentBuilder $q) use ($currency): void { // @phpstan-ignore-line
$q->where('source.transaction_currency_id', $currency->id);
$q->orWhere('source.foreign_currency_id', $currency->id);
}
);
return $this;
}
public function setEndRow(int $endRow): self
{
$this->endRow = $endRow;
return $this;
}
public function setExpandGroupSearch(bool $expandGroupSearch): GroupCollectorInterface
{
$this->expandGroupSearch = $expandGroupSearch;
return $this;
}
public function setForeignCurrency(TransactionCurrency $currency): GroupCollectorInterface
{
$this->query->where('source.foreign_currency_id', $currency->id);
return $this;
}
/**
* Limit the result to a set of specific transaction groups.
*/
public function setIds(array $groupIds): GroupCollectorInterface
{
$this->query->whereIn('transaction_groups.id', $groupIds);
return $this;
}
/**
* Limit the result to a set of specific journals.
*/
public function setJournalIds(array $journalIds): GroupCollectorInterface
{
if (0 !== count($journalIds)) {
// make all integers.
$integerIDs = array_map('intval', $journalIds);
Log::debug(sprintf('GroupCollector: setJournalIds: %s', implode(', ', $integerIDs)));
$this->query->whereIn('transaction_journals.id', $integerIDs);
}
return $this;
}
/**
* Set the page to get.
*/
public function setPage(int $page): GroupCollectorInterface
{
$page = 0 === $page ? 1 : $page;
$this->page = $page;
// app('log')->debug(sprintf('GroupCollector: page is now %d', $page));
return $this;
}
/**
* Search for words in descriptions.
*/
public function setSearchWords(array $array): GroupCollectorInterface
{
if (0 === count($array)) {
return $this;
}
$this->query->where(
static function (EloquentBuilder $q) use ($array): void { // @phpstan-ignore-line
$q->where(
static function (EloquentBuilder $q1) use ($array): void {
foreach ($array as $word) {
$keyword = sprintf('%%%s%%', $word);
$q1->where('transaction_journals.description', 'LIKE', $keyword);
}
}
);
$q->orWhere(
static function (EloquentBuilder $q2) use ($array): void {
foreach ($array as $word) {
$keyword = sprintf('%%%s%%', $word);
$q2->where('transaction_groups.title', 'LIKE', $keyword);
}
}
);
}
);
return $this;
}
#[\Override]
public function setSorting(array $instructions): GroupCollectorInterface
{
$this->sorting = $instructions;
return $this;
}
public function setStartRow(int $startRow): self
{
$this->startRow = $startRow;
return $this;
}
/**
* Limit the search to one specific transaction group.
*/
public function setTransactionGroup(TransactionGroup $transactionGroup): GroupCollectorInterface
{
$this->query->where('transaction_groups.id', $transactionGroup->id);
return $this;
}
/**
* Limit the included transaction types.
*/
public function setTypes(array $types): GroupCollectorInterface
{
$this->query->whereIn('transaction_types.type', $types);
return $this;
}
/**
* Set the user object and start the query.
*/
public function setUser(User $user): GroupCollectorInterface
{
if (null === $this->user) {
$this->user = $user;
$this->startQuery();
}
return $this;
}
/**
* Build the query.
*/
@@ -1044,6 +1056,19 @@ class GroupCollector implements GroupCollectorInterface
;
}
/**
* Set the user object and start the query.
*/
public function setUserGroup(UserGroup $userGroup): GroupCollectorInterface
{
if (null === $this->userGroup) {
$this->userGroup = $userGroup;
$this->startQueryForGroup();
}
return $this;
}
/**
* Build the query.
*/
@@ -1087,4 +1112,22 @@ class GroupCollector implements GroupCollectorInterface
->orderBy('source.amount', 'DESC')
;
}
/**
* Automatically include all stuff required to make API calls work.
*/
public function withAPIInformation(): GroupCollectorInterface
{
// include source + destination account name and type.
$this->withAccountInformation()
// include category ID + name (if any)
->withCategoryInformation()
// include budget ID + name (if any)
->withBudgetInformation()
// include bill ID + name (if any)
->withBillInformation()
;
return $this;
}
}

View File

@@ -401,6 +401,11 @@ interface GroupCollectorInterface
*/
public function setAfter(Carbon $date): self;
/**
* Limit results to a SPECIFIC set of tags.
*/
public function setAllTags(Collection $tags): self;
/**
* Collect transactions before a specific date.
*/
@@ -461,6 +466,11 @@ interface GroupCollectorInterface
*/
public function setEnd(Carbon $end): self;
/**
* Set the page to get.
*/
public function setEndRow(int $endRow): self;
public function setExpandGroupSearch(bool $expandGroupSearch): self;
/**
@@ -526,16 +536,6 @@ interface GroupCollectorInterface
*/
public function setPage(int $page): self;
/**
* Set the page to get.
*/
public function setStartRow(int $startRow): self;
/**
* Set the page to get.
*/
public function setEndRow(int $endRow): self;
/**
* Set the start and end time of the results to return.
*/
@@ -553,6 +553,8 @@ interface GroupCollectorInterface
public function setSepaCT(string $sepaCT): self;
public function setSorting(array $instructions): self;
/**
* Set source accounts.
*/
@@ -563,6 +565,11 @@ interface GroupCollectorInterface
*/
public function setStart(Carbon $start): self;
/**
* Set the page to get.
*/
public function setStartRow(int $startRow): self;
/**
* Limit results to a specific tag.
*/
@@ -573,11 +580,6 @@ interface GroupCollectorInterface
*/
public function setTags(Collection $tags): self;
/**
* Limit results to a SPECIFIC set of tags.
*/
public function setAllTags(Collection $tags): self;
/**
* Limit the search to one specific transaction group.
*/
@@ -613,6 +615,11 @@ interface GroupCollectorInterface
*/
public function setXorAccounts(Collection $accounts): self;
/**
* Sort the collection on a column.
*/
public function sortCollection(Collection $collection): Collection;
/**
* Automatically include all stuff required to make API calls work.
*/

View File

@@ -51,7 +51,7 @@ class NetWorth implements NetWorthInterface
private CurrencyRepositoryInterface $currencyRepos;
private User $user;
private null|UserGroup $userGroup;
private ?UserGroup $userGroup;
/**
* This method collects the user's net worth in ALL the user's currencies
@@ -121,12 +121,12 @@ class NetWorth implements NetWorthInterface
$netWorth[$currencyCode] ??= [
'balance' => '0',
'native_balance' => '0',
'currency_id' => (string) $currency->id,
'currency_id' => (string)$currency->id,
'currency_code' => $currency->code,
'currency_name' => $currency->name,
'currency_symbol' => $currency->symbol,
'currency_decimal_places' => $currency->decimal_places,
'native_currency_id' => (string) $default->id,
'native_currency_id' => (string)$default->id,
'native_currency_code' => $default->code,
'native_currency_name' => $default->name,
'native_currency_symbol' => $default->symbol,
@@ -144,6 +144,15 @@ class NetWorth implements NetWorthInterface
return $netWorth;
}
private function getRepository(): AccountRepositoryInterface|AdminAccountRepositoryInterface
{
if (null === $this->userGroup) {
return $this->accountRepository;
}
return $this->adminAccountRepository;
}
public function setUser(null|Authenticatable|User $user): void
{
if (!$user instanceof User) {
@@ -189,7 +198,7 @@ class NetWorth implements NetWorthInterface
}
$return[$currency->id] ??= [
'id' => (string) $currency->id,
'id' => (string)$currency->id,
'name' => $currency->name,
'symbol' => $currency->symbol,
'code' => $currency->code,
@@ -202,15 +211,6 @@ class NetWorth implements NetWorthInterface
return $return;
}
private function getRepository(): AccountRepositoryInterface|AdminAccountRepositoryInterface
{
if (null === $this->userGroup) {
return $this->accountRepository;
}
return $this->adminAccountRepository;
}
private function getAccounts(): Collection
{
$accounts = $this->getRepository()->getAccountsByType(
@@ -220,7 +220,7 @@ class NetWorth implements NetWorthInterface
/** @var Account $account */
foreach ($accounts as $account) {
if (1 === (int) $this->getRepository()->getMetaValue($account, 'include_net_worth')) {
if (1 === (int)$this->getRepository()->getMetaValue($account, 'include_net_worth')) {
$filtered->push($account);
}
}

View File

@@ -58,7 +58,7 @@ class CreateController extends Controller
$this->middleware(
function ($request, $next) {
app('view')->share('mainTitleIcon', 'fa-credit-card');
app('view')->share('title', (string) trans('firefly.accounts'));
app('view')->share('title', (string)trans('firefly.accounts'));
$this->repository = app(AccountRepositoryInterface::class);
$this->attachments = app(AttachmentHelperInterface::class);
@@ -77,7 +77,7 @@ class CreateController extends Controller
{
$defaultCurrency = app('amount')->getDefaultCurrency();
$subTitleIcon = config(sprintf('firefly.subIconsByIdentifier.%s', $objectType));
$subTitle = (string) trans(sprintf('firefly.make_new_%s_account', $objectType));
$subTitle = (string)trans(sprintf('firefly.make_new_%s_account', $objectType));
$roles = $this->getRoles();
$liabilityTypes = $this->getLiabilityTypes();
$hasOldInput = null !== $request->old('_token');
@@ -96,9 +96,9 @@ class CreateController extends Controller
// interest calculation periods:
$interestPeriods = [
'daily' => (string) trans('firefly.interest_calc_daily'),
'monthly' => (string) trans('firefly.interest_calc_monthly'),
'yearly' => (string) trans('firefly.interest_calc_yearly'),
'daily' => (string)trans('firefly.interest_calc_daily'),
'monthly' => (string)trans('firefly.interest_calc_monthly'),
'yearly' => (string)trans('firefly.interest_calc_yearly'),
];
// pre fill some data
@@ -106,7 +106,7 @@ class CreateController extends Controller
'preFilled',
[
'currency_id' => $defaultCurrency->id,
'include_net_worth' => $hasOldInput ? (bool) $request->old('include_net_worth') : true,
'include_net_worth' => $hasOldInput ? (bool)$request->old('include_net_worth') : true,
]
);
// issue #8321
@@ -139,7 +139,7 @@ class CreateController extends Controller
{
$data = $request->getAccountData();
$account = $this->repository->store($data);
$request->session()->flash('success', (string) trans('firefly.stored_new_account', ['name' => $account->name]));
$request->session()->flash('success', (string)trans('firefly.stored_new_account', ['name' => $account->name]));
app('preferences')->mark();
Log::channel('audit')->info('Stored new account.', $data);
@@ -162,7 +162,7 @@ class CreateController extends Controller
}
if (null !== $files && auth()->user()->hasRole('demo')) {
Log::channel('audit')->warning(sprintf('The demo user is trying to upload attachments in %s.', __METHOD__));
session()->flash('info', (string) trans('firefly.no_att_demo_user'));
session()->flash('info', (string)trans('firefly.no_att_demo_user'));
}
if (count($this->attachments->getMessages()->get('attachments')) > 0) {
@@ -171,7 +171,7 @@ class CreateController extends Controller
// redirect to previous URL.
$redirect = redirect($this->getPreviousUrl('accounts.create.url'));
if (1 === (int) $request->get('create_another')) {
if (1 === (int)$request->get('create_another')) {
// set value so create routine will not overwrite URL:
$request->session()->put('accounts.create.fromStore', true);

View File

@@ -57,7 +57,7 @@ class EditController extends Controller
$this->middleware(
function ($request, $next) {
app('view')->share('mainTitleIcon', 'fa-credit-card');
app('view')->share('title', (string) trans('firefly.accounts'));
app('view')->share('title', (string)trans('firefly.accounts'));
$this->repository = app(AccountRepositoryInterface::class);
$this->attachments = app(AttachmentHelperInterface::class);
@@ -81,7 +81,7 @@ class EditController extends Controller
}
$objectType = config('firefly.shortNamesByFullName')[$account->accountType->type];
$subTitle = (string) trans(sprintf('firefly.edit_%s_account', $objectType), ['name' => $account->name]);
$subTitle = (string)trans(sprintf('firefly.edit_%s_account', $objectType), ['name' => $account->name]);
$subTitleIcon = config(sprintf('firefly.subIconsByIdentifier.%s', $objectType));
$roles = $this->getRoles();
$liabilityTypes = $this->getLiabilityTypes();
@@ -106,9 +106,9 @@ class EditController extends Controller
// interest calculation periods:
$interestPeriods = [
'daily' => (string) trans('firefly.interest_calc_daily'),
'monthly' => (string) trans('firefly.interest_calc_monthly'),
'yearly' => (string) trans('firefly.interest_calc_yearly'),
'daily' => (string)trans('firefly.interest_calc_daily'),
'monthly' => (string)trans('firefly.interest_calc_monthly'),
'yearly' => (string)trans('firefly.interest_calc_yearly'),
];
// put previous url in session if not redirect from store (not "return_to_edit").
@@ -117,7 +117,7 @@ class EditController extends Controller
}
$request->session()->forget('accounts.edit.fromUpdate');
$openingBalanceAmount = (string) $repository->getOpeningBalanceAmount($account);
$openingBalanceAmount = (string)$repository->getOpeningBalanceAmount($account);
if ('0' === $openingBalanceAmount) {
$openingBalanceAmount = '';
}
@@ -143,17 +143,17 @@ class EditController extends Controller
'cc_type' => $repository->getMetaValue($account, 'cc_type'),
'cc_monthly_payment_date' => $repository->getMetaValue($account, 'cc_monthly_payment_date'),
'BIC' => $repository->getMetaValue($account, 'BIC'),
'opening_balance_date' => substr((string) $openingBalanceDate, 0, 10),
'opening_balance_date' => substr((string)$openingBalanceDate, 0, 10),
'liability_type_id' => $account->account_type_id,
'opening_balance' => app('steam')->bcround($openingBalanceAmount, $currency->decimal_places),
'liability_direction' => $this->repository->getMetaValue($account, 'liability_direction'),
'virtual_balance' => app('steam')->bcround($virtualBalance, $currency->decimal_places),
'currency_id' => $currency->id,
'include_net_worth' => $hasOldInput ? (bool) $request->old('include_net_worth') : $includeNetWorth,
'include_net_worth' => $hasOldInput ? (bool)$request->old('include_net_worth') : $includeNetWorth,
'interest' => $repository->getMetaValue($account, 'interest'),
'interest_period' => $repository->getMetaValue($account, 'interest_period'),
'notes' => $this->repository->getNoteText($account),
'active' => $hasOldInput ? (bool) $request->old('active') : $account->active,
'active' => $hasOldInput ? (bool)$request->old('active') : $account->active,
];
if ('' === $openingBalanceAmount) {
$preFilled['opening_balance'] = '';
@@ -178,7 +178,7 @@ class EditController extends Controller
$data = $request->getAccountData();
$this->repository->update($account, $data);
Log::channel('audit')->info(sprintf('Updated account #%d.', $account->id), $data);
$request->session()->flash('success', (string) trans('firefly.updated_account', ['name' => $account->name]));
$request->session()->flash('success', (string)trans('firefly.updated_account', ['name' => $account->name]));
// store new attachment(s):
/** @var null|array $files */
@@ -188,7 +188,7 @@ class EditController extends Controller
}
if (null !== $files && auth()->user()->hasRole('demo')) {
Log::channel('audit')->warning(sprintf('The demo user is trying to upload attachments in %s.', __METHOD__));
session()->flash('info', (string) trans('firefly.no_att_demo_user'));
session()->flash('info', (string)trans('firefly.no_att_demo_user'));
}
if (count($this->attachments->getMessages()->get('attachments')) > 0) {
@@ -197,7 +197,7 @@ class EditController extends Controller
// redirect
$redirect = redirect($this->getPreviousUrl('accounts.edit.url'));
if (1 === (int) $request->get('return_to_edit')) {
if (1 === (int)$request->get('return_to_edit')) {
// set value so edit routine will not overwrite URL:
$request->session()->put('accounts.edit.fromUpdate', true);

View File

@@ -95,6 +95,21 @@ class ForgotPasswordController extends Controller
return back()->with('status', trans($response));
}
/**
* @throws FireflyException
*/
private function validateHost(): void
{
$configuredHost = parse_url((string)config('app.url'), PHP_URL_HOST);
if (false === $configuredHost || null === $configuredHost) {
throw new FireflyException('Please set a valid and correct Firefly III URL in the APP_URL environment variable.');
}
$host = request()->host();
if ($configuredHost !== $host) {
throw new FireflyException('The Host-header does not match the host in the APP_URL environment variable. Please make sure these match. See also: https://bit.ly/FF3-host-header');
}
}
/**
* Show form for email recovery.
*
@@ -121,19 +136,4 @@ class ForgotPasswordController extends Controller
return view('auth.passwords.email')->with(compact('allowRegistration', 'pageTitle'));
}
/**
* @throws FireflyException
*/
private function validateHost(): void
{
$configuredHost = parse_url((string)config('app.url'), PHP_URL_HOST);
if (false === $configuredHost || null === $configuredHost) {
throw new FireflyException('Please set a valid and correct Firefly III URL in the APP_URL environment variable.');
}
$host = request()->host();
if ($configuredHost !== $host) {
throw new FireflyException('The Host-header does not match the host in the APP_URL environment variable. Please make sure these match. See also: https://bit.ly/FF3-host-header');
}
}
}

View File

@@ -80,7 +80,18 @@ class LoginController extends Controller
Log::channel('audit')->info(sprintf('User is trying to login using "%s"', $request->get($this->username())));
app('log')->debug('User is trying to login.');
$this->validateLogin($request);
try {
$this->validateLogin($request);
} catch (ValidationException $e) {
return redirect(route('login'))
->withErrors(
[
$this->username => trans('auth.failed'),
]
)
->onlyInput($this->username)
;
}
app('log')->debug('Login data is present.');
// Copied directly from AuthenticatesUsers, but with logging added:
@@ -91,7 +102,6 @@ class LoginController extends Controller
Log::channel('audit')->warning(sprintf('Login for user "%s" was locked out.', $request->get($this->username())));
app('log')->error(sprintf('Login for user "%s" was locked out.', $request->get($this->username())));
$this->fireLockoutEvent($request);
$this->sendLockoutResponse($request);
}
// Copied directly from AuthenticatesUsers, but with logging added:
@@ -132,6 +142,25 @@ class LoginController extends Controller
return $this->username;
}
/**
* Get the failed login response instance.
*
* @SuppressWarnings(PHPMD.UnusedFormalParameter)
*
* @throws ValidationException
*/
protected function sendFailedLoginResponse(Request $request): void
{
$exception = ValidationException::withMessages(
[
$this->username() => [trans('auth.failed')],
]
);
$exception->redirectTo = route('login');
throw $exception;
}
/**
* Log the user out of the application.
*
@@ -210,23 +239,4 @@ class LoginController extends Controller
return view('auth.login', compact('allowRegistration', 'email', 'remember', 'allowReset', 'title', 'usernameField'));
}
/**
* Get the failed login response instance.
*
* @SuppressWarnings(PHPMD.UnusedFormalParameter)
*
* @throws ValidationException
*/
protected function sendFailedLoginResponse(Request $request): void
{
$exception = ValidationException::withMessages(
[
$this->username() => [trans('auth.failed')],
]
);
$exception->redirectTo = route('login');
throw $exception;
}
}

View File

@@ -109,6 +109,31 @@ class RegisterController extends Controller
return redirect($this->redirectPath());
}
/**
* @throws FireflyException
*/
protected function allowedToRegister(): bool
{
// is allowed to register?
$allowRegistration = true;
try {
$singleUserMode = app('fireflyconfig')->get('single_user_mode', config('firefly.configuration.single_user_mode'))->data;
} catch (ContainerExceptionInterface|NotFoundExceptionInterface $e) {
$singleUserMode = true;
}
$userCount = User::count();
$guard = config('auth.defaults.guard');
if (true === $singleUserMode && $userCount > 0 && 'web' === $guard) {
$allowRegistration = false;
}
if ('web' !== $guard) {
$allowRegistration = false;
}
return $allowRegistration;
}
/**
* Show the application registration form if the invitation code is valid.
*
@@ -164,29 +189,4 @@ class RegisterController extends Controller
return view('auth.register', compact('isDemoSite', 'email', 'pageTitle'));
}
/**
* @throws FireflyException
*/
protected function allowedToRegister(): bool
{
// is allowed to register?
$allowRegistration = true;
try {
$singleUserMode = app('fireflyconfig')->get('single_user_mode', config('firefly.configuration.single_user_mode'))->data;
} catch (ContainerExceptionInterface|NotFoundExceptionInterface $e) {
$singleUserMode = true;
}
$userCount = User::count();
$guard = config('auth.defaults.guard');
if (true === $singleUserMode && $userCount > 0 && 'web' === $guard) {
$allowRegistration = false;
}
if ('web' !== $guard) {
$allowRegistration = false;
}
return $allowRegistration;
}
}

View File

@@ -31,7 +31,6 @@ use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Routing\Redirector;
use PragmaRX\Google2FALaravel\Support\Authenticator;
use Preferences;
/**
* Class TwoFactorController.

View File

@@ -52,7 +52,7 @@ class EditController extends Controller
$this->middleware(
function ($request, $next) {
app('view')->share('title', (string) trans('firefly.bills'));
app('view')->share('title', (string)trans('firefly.bills'));
app('view')->share('mainTitleIcon', 'fa-calendar-o');
$this->attachments = app(AttachmentHelperInterface::class);
$this->repository = app(BillRepositoryInterface::class);
@@ -75,10 +75,10 @@ class EditController extends Controller
$billPeriods = config('firefly.bill_periods');
foreach ($billPeriods as $current) {
$periods[$current] = (string) trans('firefly.'.$current);
$periods[$current] = (string)trans('firefly.'.$current);
}
$subTitle = (string) trans('firefly.edit_bill', ['name' => $bill->name]);
$subTitle = (string)trans('firefly.edit_bill', ['name' => $bill->name]);
// put previous url in session if not redirect from store (not "return_to_edit").
if (true !== session('bills.edit.fromUpdate')) {
@@ -99,7 +99,7 @@ class EditController extends Controller
'extension_date' => $bill->extension_date,
'notes' => $this->repository->getNoteText($bill),
'transaction_currency_id' => $bill->transaction_currency_id,
'active' => $hasOldInput ? (bool) $request->old('active') : $bill->active,
'active' => $hasOldInput ? (bool)$request->old('active') : $bill->active,
'object_group' => null !== $bill->objectGroups->first() ? $bill->objectGroups->first()->title : '',
];
@@ -119,7 +119,7 @@ class EditController extends Controller
Log::channel('audit')->info(sprintf('Updated bill #%d.', $bill->id), $billData);
$request->session()->flash('success', (string) trans('firefly.updated_bill', ['name' => $bill->name]));
$request->session()->flash('success', (string)trans('firefly.updated_bill', ['name' => $bill->name]));
app('preferences')->mark();
/** @var null|array $files */
@@ -129,7 +129,7 @@ class EditController extends Controller
}
if (null !== $files && auth()->user()->hasRole('demo')) {
Log::channel('audit')->warning(sprintf('The demo user is trying to upload attachments in %s.', __METHOD__));
session()->flash('info', (string) trans('firefly.no_att_demo_user'));
session()->flash('info', (string)trans('firefly.no_att_demo_user'));
}
// flash messages
@@ -138,7 +138,7 @@ class EditController extends Controller
}
$redirect = redirect($this->getPreviousUrl('bills.edit.url'));
if (1 === (int) $request->get('return_to_edit')) {
if (1 === (int)$request->get('return_to_edit')) {
$request->session()->put('bills.edit.fromUpdate', true);
$redirect = redirect(route('bills.edit', [$bill->id]))->withInput(['return_to_edit' => 1]);

View File

@@ -134,24 +134,6 @@ class IndexController extends Controller
return view('bills.index', compact('bills', 'sums', 'total', 'totals', 'today'));
}
/**
* Set the order of a bill.
*/
public function setOrder(Request $request, Bill $bill): JsonResponse
{
$objectGroupTitle = (string)$request->get('objectGroupTitle');
$newOrder = (int)$request->get('order');
$this->repository->setOrder($bill, $newOrder);
if ('' !== $objectGroupTitle) {
$this->repository->setObjectGroup($bill, $objectGroupTitle);
}
if ('' === $objectGroupTitle) {
$this->repository->removeObjectGroup($bill);
}
return response()->json(['data' => 'OK']);
}
/**
* @throws FireflyException
*/
@@ -268,4 +250,22 @@ class IndexController extends Controller
return $totals;
}
/**
* Set the order of a bill.
*/
public function setOrder(Request $request, Bill $bill): JsonResponse
{
$objectGroupTitle = (string)$request->get('objectGroupTitle');
$newOrder = (int)$request->get('order');
$this->repository->setOrder($bill, $newOrder);
if ('' !== $objectGroupTitle) {
$this->repository->setObjectGroup($bill, $objectGroupTitle);
}
if ('' === $objectGroupTitle) {
$this->repository->removeObjectGroup($bill);
}
return response()->json(['data' => 'OK']);
}
}

View File

@@ -116,7 +116,6 @@ class IndexController extends Controller
// get all available budgets:
$availableBudgets = $this->getAllAvailableBudgets($start, $end);
// get all active budgets:
$budgets = $this->getAllBudgets($start, $end, $currencies, $defaultCurrency);
$sums = $this->getSums($budgets);
@@ -159,24 +158,6 @@ class IndexController extends Controller
);
}
public function reorder(Request $request, BudgetRepositoryInterface $repository): JsonResponse
{
$this->abRepository->cleanup();
$budgetIds = $request->get('budgetIds');
foreach ($budgetIds as $index => $budgetId) {
$budgetId = (int)$budgetId;
$budget = $repository->find($budgetId);
if (null !== $budget) {
app('log')->debug(sprintf('Set budget #%d ("%s") to position %d', $budget->id, $budget->name, $index + 1));
$repository->setBudgetOrder($budget, $index + 1);
}
}
app('preferences')->mark();
return response()->json(['OK']);
}
private function getAllAvailableBudgets(Carbon $start, Carbon $end): array
{
// get all available budgets.
@@ -316,4 +297,22 @@ class IndexController extends Controller
return $sums;
}
public function reorder(Request $request, BudgetRepositoryInterface $repository): JsonResponse
{
$this->abRepository->cleanup();
$budgetIds = $request->get('budgetIds');
foreach ($budgetIds as $index => $budgetId) {
$budgetId = (int)$budgetId;
$budget = $repository->find($budgetId);
if (null !== $budget) {
app('log')->debug(sprintf('Set budget #%d ("%s") to position %d', $budget->id, $budget->name, $index + 1));
$repository->setBudgetOrder($budget, $index + 1);
}
}
app('preferences')->mark();
return response()->json(['OK']);
}
}

View File

@@ -409,6 +409,58 @@ class AccountController extends Controller
return response()->json($data);
}
/**
* @throws FireflyException
*/
private function periodByCurrency(Carbon $start, Carbon $end, Account $account, TransactionCurrency $currency): array
{
app('log')->debug(sprintf('Now in periodByCurrency("%s", "%s", %s, "%s")', $start->format('Y-m-d'), $end->format('Y-m-d'), $account->id, $currency->code));
$locale = app('steam')->getLocale();
$step = $this->calculateStep($start, $end);
$result = [
'label' => sprintf('%s (%s)', $account->name, $currency->symbol),
'currency_symbol' => $currency->symbol,
'currency_code' => $currency->code,
];
$entries = [];
$current = clone $start;
app('log')->debug(sprintf('Step is %s', $step));
// fix for issue https://github.com/firefly-iii/firefly-iii/issues/8041
// have to make sure this chart is always based on the balance at the END of the period.
// This period depends on the size of the chart
$current = app('navigation')->endOfX($current, $step, null);
app('log')->debug(sprintf('$current date is %s', $current->format('Y-m-d')));
if ('1D' === $step) {
// per day the entire period, balance for every day.
$format = (string)trans('config.month_and_day_js', [], $locale);
$range = app('steam')->balanceInRange($account, $start, $end, $currency);
$previous = array_values($range)[0];
while ($end >= $current) {
$theDate = $current->format('Y-m-d');
$balance = $range[$theDate] ?? $previous;
$label = $current->isoFormat($format);
$entries[$label] = (float)$balance;
$previous = $balance;
$current->addDay();
}
}
if ('1W' === $step || '1M' === $step || '1Y' === $step) {
while ($end >= $current) {
app('log')->debug(sprintf('Current is: %s', $current->format('Y-m-d')));
$balance = (float)app('steam')->balance($account, $current, $currency);
$label = app('navigation')->periodShow($current, $step);
$entries[$label] = $balance;
$current = app('navigation')->addPeriod($current, $step, 0);
// here too, to fix #8041, the data is corrected to the end of the period.
$current = app('navigation')->endOfX($current, $step, null);
}
}
$result['entries'] = $entries;
return $result;
}
/**
* Shows the balances for a given set of dates and accounts.
*
@@ -512,56 +564,4 @@ class AccountController extends Controller
return response()->json($data);
}
/**
* @throws FireflyException
*/
private function periodByCurrency(Carbon $start, Carbon $end, Account $account, TransactionCurrency $currency): array
{
app('log')->debug(sprintf('Now in periodByCurrency("%s", "%s", %s, "%s")', $start->format('Y-m-d'), $end->format('Y-m-d'), $account->id, $currency->code));
$locale = app('steam')->getLocale();
$step = $this->calculateStep($start, $end);
$result = [
'label' => sprintf('%s (%s)', $account->name, $currency->symbol),
'currency_symbol' => $currency->symbol,
'currency_code' => $currency->code,
];
$entries = [];
$current = clone $start;
app('log')->debug(sprintf('Step is %s', $step));
// fix for issue https://github.com/firefly-iii/firefly-iii/issues/8041
// have to make sure this chart is always based on the balance at the END of the period.
// This period depends on the size of the chart
$current = app('navigation')->endOfX($current, $step, null);
app('log')->debug(sprintf('$current date is %s', $current->format('Y-m-d')));
if ('1D' === $step) {
// per day the entire period, balance for every day.
$format = (string)trans('config.month_and_day_js', [], $locale);
$range = app('steam')->balanceInRange($account, $start, $end, $currency);
$previous = array_values($range)[0];
while ($end >= $current) {
$theDate = $current->format('Y-m-d');
$balance = $range[$theDate] ?? $previous;
$label = $current->isoFormat($format);
$entries[$label] = (float)$balance;
$previous = $balance;
$current->addDay();
}
}
if ('1W' === $step || '1M' === $step || '1Y' === $step) {
while ($end >= $current) {
app('log')->debug(sprintf('Current is: %s', $current->format('Y-m-d')));
$balance = (float)app('steam')->balance($account, $current, $currency);
$label = app('navigation')->periodShow($current, $step);
$entries[$label] = $balance;
$current = app('navigation')->addPeriod($current, $step, 0);
// here too, to fix #8041, the data is corrected to the end of the period.
$current = app('navigation')->endOfX($current, $step, null);
}
}
$result['entries'] = $entries;
return $result;
}
}

View File

@@ -195,6 +195,23 @@ class BudgetReportController extends Controller
return response()->json($data);
}
private function makeEntries(Carbon $start, Carbon $end): array
{
$return = [];
$format = app('navigation')->preferredCarbonLocalizedFormat($start, $end);
$preferredRange = app('navigation')->preferredRangeFormat($start, $end);
$currentStart = clone $start;
while ($currentStart <= $end) {
$currentEnd = app('navigation')->endOfPeriod($currentStart, $preferredRange);
$key = $currentStart->isoFormat($format);
$return[$key] = '0';
$currentStart = clone $currentEnd;
$currentStart->addDay()->startOfDay();
}
return $return;
}
/**
* Chart that groups expenses by the account.
*/
@@ -224,21 +241,4 @@ class BudgetReportController extends Controller
return response()->json($data);
}
private function makeEntries(Carbon $start, Carbon $end): array
{
$return = [];
$format = app('navigation')->preferredCarbonLocalizedFormat($start, $end);
$preferredRange = app('navigation')->preferredRangeFormat($start, $end);
$currentStart = clone $start;
while ($currentStart <= $end) {
$currentEnd = app('navigation')->endOfPeriod($currentStart, $preferredRange);
$key = $currentStart->isoFormat($format);
$return[$key] = '0';
$currentStart = clone $currentEnd;
$currentStart->addDay()->startOfDay();
}
return $return;
}
}

View File

@@ -94,6 +94,11 @@ class CategoryController extends Controller
return response()->json($data);
}
private function getDate(): Carbon
{
return today(config('app.timezone'));
}
/**
* Shows the category chart on the front page.
* TODO test method for category refactor.
@@ -141,66 +146,6 @@ class CategoryController extends Controller
return response()->json($data);
}
/**
* Chart for period for transactions without a category.
* TODO test me.
*/
public function reportPeriodNoCategory(Collection $accounts, Carbon $start, Carbon $end): JsonResponse
{
$cache = new CacheProperties();
$cache->addProperty($start);
$cache->addProperty($end);
$cache->addProperty('chart.category.period.no-cat');
$cache->addProperty($accounts->pluck('id')->toArray());
if ($cache->has()) {
return response()->json($cache->get());
}
$data = $this->reportPeriodChart($accounts, $start, $end, null);
$cache->store($data);
return response()->json($data);
}
/**
* Chart for a specific period.
* TODO test me, for category refactor.
*
* @throws FireflyException
*/
public function specificPeriod(Category $category, Carbon $date): JsonResponse
{
$range = app('navigation')->getViewRange(false);
$start = app('navigation')->startOfPeriod($date, $range);
$end = session()->get('end');
if ($end < $start) {
[$end, $start] = [$start, $end];
}
$cache = new CacheProperties();
$cache->addProperty($start);
$cache->addProperty($end);
$cache->addProperty($category->id);
$cache->addProperty('chart.category.period-chart');
if ($cache->has()) {
return response()->json($cache->get());
}
/** @var WholePeriodChartGenerator $chartGenerator */
$chartGenerator = app(WholePeriodChartGenerator::class);
$chartData = $chartGenerator->generate($category, $start, $end);
$data = $this->generator->multiSet($chartData);
$cache->store($data);
return response()->json($data);
}
private function getDate(): Carbon
{
return today(config('app.timezone'));
}
/**
* Generate report chart for either with or without category.
*/
@@ -279,4 +224,59 @@ class CategoryController extends Controller
return $this->generator->multiSet($chartData);
}
/**
* Chart for period for transactions without a category.
* TODO test me.
*/
public function reportPeriodNoCategory(Collection $accounts, Carbon $start, Carbon $end): JsonResponse
{
$cache = new CacheProperties();
$cache->addProperty($start);
$cache->addProperty($end);
$cache->addProperty('chart.category.period.no-cat');
$cache->addProperty($accounts->pluck('id')->toArray());
if ($cache->has()) {
return response()->json($cache->get());
}
$data = $this->reportPeriodChart($accounts, $start, $end, null);
$cache->store($data);
return response()->json($data);
}
/**
* Chart for a specific period.
* TODO test me, for category refactor.
*
* @throws FireflyException
*/
public function specificPeriod(Category $category, Carbon $date): JsonResponse
{
$range = app('navigation')->getViewRange(false);
$start = app('navigation')->startOfPeriod($date, $range);
$end = session()->get('end');
if ($end < $start) {
[$end, $start] = [$start, $end];
}
$cache = new CacheProperties();
$cache->addProperty($start);
$cache->addProperty($end);
$cache->addProperty($category->id);
$cache->addProperty('chart.category.period-chart');
if ($cache->has()) {
return response()->json($cache->get());
}
/** @var WholePeriodChartGenerator $chartGenerator */
$chartGenerator = app(WholePeriodChartGenerator::class);
$chartData = $chartGenerator->generate($category, $start, $end);
$data = $this->generator->multiSet($chartData);
$cache->store($data);
return response()->json($data);
}
}

View File

@@ -269,6 +269,26 @@ class CategoryReportController extends Controller
return response()->json($data);
}
/**
* TODO duplicate function
*/
private function makeEntries(Carbon $start, Carbon $end): array
{
$return = [];
$format = app('navigation')->preferredCarbonLocalizedFormat($start, $end);
$preferredRange = app('navigation')->preferredRangeFormat($start, $end);
$currentStart = clone $start;
while ($currentStart <= $end) {
$currentEnd = app('navigation')->endOfPeriod($currentStart, $preferredRange);
$key = $currentStart->isoFormat($format);
$return[$key] = '0';
$currentStart = clone $currentEnd;
$currentStart->addDay()->startOfDay();
}
return $return;
}
public function sourceExpense(Collection $accounts, Collection $categories, Carbon $start, Carbon $end): JsonResponse
{
$result = [];
@@ -324,24 +344,4 @@ class CategoryReportController extends Controller
return response()->json($data);
}
/**
* TODO duplicate function
*/
private function makeEntries(Carbon $start, Carbon $end): array
{
$return = [];
$format = app('navigation')->preferredCarbonLocalizedFormat($start, $end);
$preferredRange = app('navigation')->preferredRangeFormat($start, $end);
$currentStart = clone $start;
while ($currentStart <= $end) {
$currentEnd = app('navigation')->endOfPeriod($currentStart, $preferredRange);
$key = $currentStart->isoFormat($format);
$return[$key] = '0';
$currentStart = clone $currentEnd;
$currentStart->addDay()->startOfDay();
}
return $return;
}
}

View File

@@ -212,6 +212,44 @@ class DoubleReportController extends Controller
return response()->json($data);
}
/**
* TODO duplicate function
*/
private function getCounterpartName(Collection $accounts, int $id, string $name, ?string $iban): string
{
/** @var Account $account */
foreach ($accounts as $account) {
if ($account->name === $name && $account->id !== $id) {
return $account->name;
}
if (null !== $account->iban && $account->iban === $iban && $account->id !== $id) {
return $account->iban;
}
}
return $name;
}
/**
* TODO duplicate function
*/
private function makeEntries(Carbon $start, Carbon $end): array
{
$return = [];
$format = app('navigation')->preferredCarbonLocalizedFormat($start, $end);
$preferredRange = app('navigation')->preferredRangeFormat($start, $end);
$currentStart = clone $start;
while ($currentStart <= $end) {
$currentEnd = app('navigation')->endOfPeriod($currentStart, $preferredRange);
$key = $currentStart->isoFormat($format);
$return[$key] = '0';
$currentStart = clone $currentEnd;
$currentStart->addDay()->startOfDay();
}
return $return;
}
public function tagExpense(Collection $accounts, Collection $others, Carbon $start, Carbon $end): JsonResponse
{
$result = [];
@@ -317,42 +355,4 @@ class DoubleReportController extends Controller
return response()->json($data);
}
/**
* TODO duplicate function
*/
private function getCounterpartName(Collection $accounts, int $id, string $name, ?string $iban): string
{
/** @var Account $account */
foreach ($accounts as $account) {
if ($account->name === $name && $account->id !== $id) {
return $account->name;
}
if (null !== $account->iban && $account->iban === $iban && $account->id !== $id) {
return $account->iban;
}
}
return $name;
}
/**
* TODO duplicate function
*/
private function makeEntries(Carbon $start, Carbon $end): array
{
$return = [];
$format = app('navigation')->preferredCarbonLocalizedFormat($start, $end);
$preferredRange = app('navigation')->preferredRangeFormat($start, $end);
$currentStart = clone $start;
while ($currentStart <= $end) {
$currentEnd = app('navigation')->endOfPeriod($currentStart, $preferredRange);
$key = $currentStart->isoFormat($format);
$return[$key] = '0';
$currentStart = clone $currentEnd;
$currentStart->addDay()->startOfDay();
}
return $return;
}
}

View File

@@ -102,27 +102,27 @@ class ExpenseReportController extends Controller
/** @var Account $exp */
$exp = $combination->first();
$chartData[$exp->id.'-in'] = [
'label' => sprintf('%s (%s)', $name, (string) trans('firefly.income')),
'label' => sprintf('%s (%s)', $name, (string)trans('firefly.income')),
'type' => 'bar',
'yAxisID' => 'y-axis-0',
'entries' => [],
];
$chartData[$exp->id.'-out'] = [
'label' => sprintf('%s (%s)', $name, (string) trans('firefly.expenses')),
'label' => sprintf('%s (%s)', $name, (string)trans('firefly.expenses')),
'type' => 'bar',
'yAxisID' => 'y-axis-0',
'entries' => [],
];
// total in, total out:
$chartData[$exp->id.'-total-in'] = [
'label' => sprintf('%s (%s)', $name, (string) trans('firefly.sum_of_income')),
'label' => sprintf('%s (%s)', $name, (string)trans('firefly.sum_of_income')),
'type' => 'line',
'fill' => false,
'yAxisID' => 'y-axis-1',
'entries' => [],
];
$chartData[$exp->id.'-total-out'] = [
'label' => sprintf('%s (%s)', $name, (string) trans('firefly.sum_of_expenses')),
'label' => sprintf('%s (%s)', $name, (string)trans('firefly.sum_of_expenses')),
'type' => 'line',
'fill' => false,
'yAxisID' => 'y-axis-1',

View File

@@ -143,7 +143,7 @@ class ReportController extends Controller
$cache->addProperty($accounts);
$cache->addProperty($end);
if ($cache->has()) {
return response()->json($cache->get());
// return response()->json($cache->get());
}
app('log')->debug('Going to do operations for accounts ', $accounts->pluck('id')->toArray());
@@ -220,11 +220,14 @@ class ReportController extends Controller
$currentEnd = app('navigation')->endOfPeriod($currentEnd, $preferredRange);
}
while ($currentStart <= $currentEnd) {
$key = $currentStart->format($format);
$title = $currentStart->isoFormat($titleFormat);
$income['entries'][$title] = app('steam')->bcround($currency[$key]['earned'] ?? '0', $currency['currency_decimal_places']);
$expense['entries'][$title] = app('steam')->bcround($currency[$key]['spent'] ?? '0', $currency['currency_decimal_places']);
$currentStart = app('navigation')->addPeriod($currentStart, $preferredRange, 0);
$key = $currentStart->format($format);
$title = $currentStart->isoFormat($titleFormat);
// #8663 make sure the period exists in the data previously collected.
if (array_key_exists($key, $currency)) {
$income['entries'][$title] = app('steam')->bcround($currency[$key]['earned'] ?? '0', $currency['currency_decimal_places']);
$expense['entries'][$title] = app('steam')->bcround($currency[$key]['spent'] ?? '0', $currency['currency_decimal_places']);
}
$currentStart = app('navigation')->addPeriod($currentStart, $preferredRange, 0);
}
$chartData[] = $income;

View File

@@ -274,6 +274,26 @@ class TagReportController extends Controller
return response()->json($data);
}
/**
* TODO duplicate function
*/
private function makeEntries(Carbon $start, Carbon $end): array
{
$return = [];
$format = app('navigation')->preferredCarbonLocalizedFormat($start, $end);
$preferredRange = app('navigation')->preferredRangeFormat($start, $end);
$currentStart = clone $start;
while ($currentStart <= $end) {
$currentEnd = app('navigation')->endOfPeriod($currentStart, $preferredRange);
$key = $currentStart->isoFormat($format);
$return[$key] = '0';
$currentStart = clone $currentEnd;
$currentStart->addDay()->startOfDay();
}
return $return;
}
public function sourceExpense(Collection $accounts, Collection $tags, Carbon $start, Carbon $end): JsonResponse
{
$result = [];
@@ -381,24 +401,4 @@ class TagReportController extends Controller
return response()->json($data);
}
/**
* TODO duplicate function
*/
private function makeEntries(Carbon $start, Carbon $end): array
{
$return = [];
$format = app('navigation')->preferredCarbonLocalizedFormat($start, $end);
$preferredRange = app('navigation')->preferredRangeFormat($start, $end);
$currentStart = clone $start;
while ($currentStart <= $end) {
$currentEnd = app('navigation')->endOfPeriod($currentStart, $preferredRange);
$key = $currentStart->isoFormat($format);
$return[$key] = '0';
$currentStart = clone $currentEnd;
$currentStart->addDay()->startOfDay();
}
return $return;
}
}

View File

@@ -69,6 +69,11 @@ abstract class Controller extends BaseController
$authGuard = config('firefly.authentication_guard');
$logoutUrl = config('firefly.custom_logout_url');
// overrule v2 layout back to v1.
if ('true' === request()->get('force_default_layout') && 'v2' === config('firefly.layout')) {
app('view')->getFinder()->setPaths([realpath(base_path('resources/views'))]); // @phpstan-ignore-line
}
app('view')->share('authGuard', $authGuard);
app('view')->share('logoutUrl', $logoutUrl);

View File

@@ -24,6 +24,7 @@ declare(strict_types=1);
namespace FireflyIII\Http\Controllers;
use Carbon\Carbon;
use Exception;
use FireflyIII\Exceptions\FireflyException;
use FireflyIII\Http\Middleware\IsDemoUser;
use FireflyIII\Models\AccountType;
@@ -140,21 +141,6 @@ class DebugController extends Controller
return view('debug', compact('table', 'now', 'logContent'));
}
/**
* Flash all types of messages.
*
* @return Redirector|RedirectResponse
*/
public function testFlash(Request $request)
{
$request->session()->flash('success', 'This is a success message.');
$request->session()->flash('info', 'This is an info message.');
$request->session()->flash('warning', 'This is a warning.');
$request->session()->flash('error', 'This is an error!');
return redirect(route('home'));
}
private function generateTable(): string
{
// system information:
@@ -202,6 +188,7 @@ class DebugController extends Controller
try {
if (file_exists('/var/www/counter-main.txt')) {
$return['build'] = trim((string)file_get_contents('/var/www/counter-main.txt'));
app('log')->debug(sprintf('build is now "%s"', $return['build']));
}
} catch (\Exception $e) { // @phpstan-ignore-line
app('log')->debug('Could not check build counter, but thats ok.');
@@ -336,4 +323,19 @@ class DebugController extends Controller
return implode(' ', $flags);
}
/**
* Flash all types of messages.
*
* @return Redirector|RedirectResponse
*/
public function testFlash(Request $request)
{
$request->session()->flash('success', 'This is a success message.');
$request->session()->flash('info', 'This is an info message.');
$request->session()->flash('warning', 'This is a warning.');
$request->session()->flash('error', 'This is an error!');
return redirect(route('home'));
}
}

View File

@@ -67,7 +67,7 @@ class IndexController extends Controller
public function export(): LaravelResponse|RedirectResponse
{
if (auth()->user()->hasRole('demo')) {
session()->flash('info', (string) trans('firefly.demo_user_export'));
session()->flash('info', (string)trans('firefly.demo_user_export'));
return redirect(route('export.index'));
}

View File

@@ -129,7 +129,7 @@ class BoxController extends Controller
app('log')->debug('Left to spend is positive or zero!');
$boxTitle = (string)trans('firefly.left_to_spend');
$activeDaysLeft = $this->activeDaysLeft($start, $end); // see method description.
$display = 1; // not overspent
$display = 1; // not overspent
$leftPerDayAmount = bcdiv($leftToSpendAmount, (string)$activeDaysLeft);
app('log')->debug(sprintf('Left to spend per day is %s', $leftPerDayAmount));
}

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