Compare commits

...

54 Commits

Author SHA1 Message Date
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
github-actions
89ce2838d5 Auto commit for release 'develop' on 2024-02-22 2024-02-22 01:29:01 +01:00
James Cole
356b217692 Add missing array 2024-02-20 20:33:33 +01:00
James Cole
950e39b753 Clean up methods. 2024-02-20 20:05:43 +01:00
James Cole
8f14979717 Clean up rule set. 2024-02-20 19:59:55 +01:00
James Cole
aa2afd162e Fix https://github.com/orgs/firefly-iii/discussions/8557 2024-02-20 19:51:13 +01:00
James Cole
fe33352ec1 Fix translation for https://github.com/firefly-iii/firefly-iii/issues/8555 2024-02-19 05:57:06 +01:00
github-actions
65c5249815 Auto commit for release 'develop' on 2024-02-19 2024-02-19 01:30:23 +01:00
James Cole
b1afaea1aa Various updates. 2024-02-18 11:35:08 +01:00
James Cole
997dc3814b Add host header validation 2024-02-17 08:18:49 +01:00
James Cole
b37b5b86d4 Fix https://github.com/firefly-iii/firefly-iii/issues/8544 2024-02-16 20:42:04 +01:00
github-actions
b13a4e1016 Auto commit for release 'develop' on 2024-02-15 2024-02-15 01:29:20 +01:00
James Cole
7897ebc4d5 Update code 2024-02-14 19:47:35 +01:00
James Cole
ac17b82d85 Do not complain when the category is already set. 2024-02-14 19:34:36 +01:00
James Cole
1b1712d998 Make sure that errors in the incoming webhook url are caught. 2024-02-14 19:32:15 +01:00
James Cole
a2c0d9f7d0 The update checker can handle the development releases 2024-02-14 06:34:38 +01:00
James Cole
5b68b25c85 Merge pull request #8538 from firefly-iii/dependabot/composer/develop/phpunit/phpunit-11.0.3
Bump phpunit/phpunit from 10.5.10 to 11.0.3
2024-02-12 19:52:51 +01:00
dependabot[bot]
d3a215b575 Bump phpunit/phpunit from 10.5.10 to 11.0.3
Bumps [phpunit/phpunit](https://github.com/sebastianbergmann/phpunit) from 10.5.10 to 11.0.3.
- [Changelog](https://github.com/sebastianbergmann/phpunit/blob/11.0.3/ChangeLog-11.0.md)
- [Commits](https://github.com/sebastianbergmann/phpunit/compare/10.5.10...11.0.3)

---
updated-dependencies:
- dependency-name: phpunit/phpunit
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-02-12 03:35:09 +00:00
github-actions
5c352a0d3e Auto commit for release 'develop' on 2024-02-12 2024-02-12 01:29:15 +01:00
James Cole
fded058ea6 Rebuild frontend. 2024-02-11 19:16:27 +01:00
James Cole
99f041b114 Editable up until date. 2024-02-11 10:43:14 +01:00
James Cole
283b594995 Merge branch 'develop' of github.com:firefly-iii/firefly-iii into develop
# Conflicts:
#	app/Helpers/Collector/GroupCollector.php
2024-02-10 08:29:59 +01:00
James Cole
723aa65e7a Inline edit for v2 2024-02-10 08:28:59 +01:00
github-actions
64d315ad51 Auto commit for release 'develop' on 2024-02-08 2024-02-08 01:29:34 +01:00
James Cole
d0844356cb Fix https://github.com/firefly-iii/firefly-iii/issues/8521 2024-02-07 06:14:40 +01:00
James Cole
ba8d65835a Remove unused translation. 2024-02-07 06:02:43 +01:00
github-actions
fa3343f437 Auto commit for release 'v6.1.9' on 2024-02-05 2024-02-05 19:53:12 +01:00
James Cole
c5b8a951d2 Update changelog for new release. 2024-02-05 06:37:16 +01:00
James Cole
20b1fc05cb Don't put a "v" in front of the version when it's a develop version. 2024-02-05 06:29:19 +01:00
525 changed files with 93537 additions and 90440 deletions

View File

@@ -35,28 +35,32 @@ $finder = PhpCsFixer\Finder::create()
$config = new PhpCsFixer\Config();
return $config->setRules([
'no_unused_imports' => true,
'@PhpCsFixer' => true,
return $config->setRules(
[
// rule sets
'@PHP83Migration' => true,
'@PhpCsFixer' => true,
'@PhpCsFixer:risky' => true,
'@PSR12' => true,
'@PSR12:risky' => true,
'declare_strict_types' => true,
'strict_param' => true,
'comment_to_phpdoc' => false, // breaks phpstan lines in combination with PHPStorm.
'array_syntax' => ['syntax' => 'short'],
'no_unused_imports' => true,
'single_space_around_construct' => true,
'statement_indentation' => true,
'void_return' => true,
// disabled rules
'native_function_invocation' => false, // annoying
'php_unit_data_provider_name' => false, // bloody annoying long test names
'static_lambda' => false, // breaks the Response macro for API's.
'phpdoc_summary' => false, // annoying.
'single_space_around_construct' => [
'constructs_followed_by_a_single_space' => [
'protected',
],
],
'statement_indentation' => true,
'comment_to_phpdoc' => false, // breaks phpstan lines in combination with PHPStorm.
'type_declaration_spaces' => false,
'cast_spaces' => false,
// complex rules
'array_syntax' => ['syntax' => 'short'],
'binary_operator_spaces' => [
'default' => 'at_least_single_space',
'operators' => [
@@ -65,6 +69,5 @@ return $config->setRules([
'??=' => 'align_single_space_minimal_by_scope',
],
],
'void_return' => true,
])
->setFinder($finder);

View File

@@ -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",
@@ -1047,16 +1048,16 @@
},
{
"name": "symfony/polyfill-ctype",
"version": "v1.28.0",
"version": "v1.29.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-ctype.git",
"reference": "ea208ce43cbb04af6867b4fdddb1bdbf84cc28cb"
"reference": "ef4d7e442ca910c4764bce785146269b30cb5fc4"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/ea208ce43cbb04af6867b4fdddb1bdbf84cc28cb",
"reference": "ea208ce43cbb04af6867b4fdddb1bdbf84cc28cb",
"url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/ef4d7e442ca910c4764bce785146269b30cb5fc4",
"reference": "ef4d7e442ca910c4764bce785146269b30cb5fc4",
"shasum": ""
},
"require": {
@@ -1070,9 +1071,6 @@
},
"type": "library",
"extra": {
"branch-alias": {
"dev-main": "1.28-dev"
},
"thanks": {
"name": "symfony/polyfill",
"url": "https://github.com/symfony/polyfill"
@@ -1109,7 +1107,7 @@
"portable"
],
"support": {
"source": "https://github.com/symfony/polyfill-ctype/tree/v1.28.0"
"source": "https://github.com/symfony/polyfill-ctype/tree/v1.29.0"
},
"funding": [
{
@@ -1125,20 +1123,20 @@
"type": "tidelift"
}
],
"time": "2023-01-26T09:26:14+00:00"
"time": "2024-01-29T20:11:03+00:00"
},
{
"name": "symfony/polyfill-intl-grapheme",
"version": "v1.28.0",
"version": "v1.29.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-intl-grapheme.git",
"reference": "875e90aeea2777b6f135677f618529449334a612"
"reference": "32a9da87d7b3245e09ac426c83d334ae9f06f80f"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/875e90aeea2777b6f135677f618529449334a612",
"reference": "875e90aeea2777b6f135677f618529449334a612",
"url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/32a9da87d7b3245e09ac426c83d334ae9f06f80f",
"reference": "32a9da87d7b3245e09ac426c83d334ae9f06f80f",
"shasum": ""
},
"require": {
@@ -1149,9 +1147,6 @@
},
"type": "library",
"extra": {
"branch-alias": {
"dev-main": "1.28-dev"
},
"thanks": {
"name": "symfony/polyfill",
"url": "https://github.com/symfony/polyfill"
@@ -1190,7 +1185,7 @@
"shim"
],
"support": {
"source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.28.0"
"source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.29.0"
},
"funding": [
{
@@ -1206,20 +1201,20 @@
"type": "tidelift"
}
],
"time": "2023-01-26T09:26:14+00:00"
"time": "2024-01-29T20:11:03+00:00"
},
{
"name": "symfony/polyfill-intl-normalizer",
"version": "v1.28.0",
"version": "v1.29.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-intl-normalizer.git",
"reference": "8c4ad05dd0120b6a53c1ca374dca2ad0a1c4ed92"
"reference": "bc45c394692b948b4d383a08d7753968bed9a83d"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/8c4ad05dd0120b6a53c1ca374dca2ad0a1c4ed92",
"reference": "8c4ad05dd0120b6a53c1ca374dca2ad0a1c4ed92",
"url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/bc45c394692b948b4d383a08d7753968bed9a83d",
"reference": "bc45c394692b948b4d383a08d7753968bed9a83d",
"shasum": ""
},
"require": {
@@ -1230,9 +1225,6 @@
},
"type": "library",
"extra": {
"branch-alias": {
"dev-main": "1.28-dev"
},
"thanks": {
"name": "symfony/polyfill",
"url": "https://github.com/symfony/polyfill"
@@ -1274,7 +1266,7 @@
"shim"
],
"support": {
"source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.28.0"
"source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.29.0"
},
"funding": [
{
@@ -1290,20 +1282,20 @@
"type": "tidelift"
}
],
"time": "2023-01-26T09:26:14+00:00"
"time": "2024-01-29T20:11:03+00:00"
},
{
"name": "symfony/polyfill-mbstring",
"version": "v1.28.0",
"version": "v1.29.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-mbstring.git",
"reference": "42292d99c55abe617799667f454222c54c60e229"
"reference": "9773676c8a1bb1f8d4340a62efe641cf76eda7ec"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/42292d99c55abe617799667f454222c54c60e229",
"reference": "42292d99c55abe617799667f454222c54c60e229",
"url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/9773676c8a1bb1f8d4340a62efe641cf76eda7ec",
"reference": "9773676c8a1bb1f8d4340a62efe641cf76eda7ec",
"shasum": ""
},
"require": {
@@ -1317,9 +1309,6 @@
},
"type": "library",
"extra": {
"branch-alias": {
"dev-main": "1.28-dev"
},
"thanks": {
"name": "symfony/polyfill",
"url": "https://github.com/symfony/polyfill"
@@ -1357,7 +1346,7 @@
"shim"
],
"support": {
"source": "https://github.com/symfony/polyfill-mbstring/tree/v1.28.0"
"source": "https://github.com/symfony/polyfill-mbstring/tree/v1.29.0"
},
"funding": [
{
@@ -1373,20 +1362,20 @@
"type": "tidelift"
}
],
"time": "2023-07-28T09:04:16+00:00"
"time": "2024-01-29T20:11:03+00:00"
},
{
"name": "symfony/polyfill-php80",
"version": "v1.28.0",
"version": "v1.29.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-php80.git",
"reference": "6caa57379c4aec19c0a12a38b59b26487dcfe4b5"
"reference": "87b68208d5c1188808dd7839ee1e6c8ec3b02f1b"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/6caa57379c4aec19c0a12a38b59b26487dcfe4b5",
"reference": "6caa57379c4aec19c0a12a38b59b26487dcfe4b5",
"url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/87b68208d5c1188808dd7839ee1e6c8ec3b02f1b",
"reference": "87b68208d5c1188808dd7839ee1e6c8ec3b02f1b",
"shasum": ""
},
"require": {
@@ -1394,9 +1383,6 @@
},
"type": "library",
"extra": {
"branch-alias": {
"dev-main": "1.28-dev"
},
"thanks": {
"name": "symfony/polyfill",
"url": "https://github.com/symfony/polyfill"
@@ -1440,7 +1426,7 @@
"shim"
],
"support": {
"source": "https://github.com/symfony/polyfill-php80/tree/v1.28.0"
"source": "https://github.com/symfony/polyfill-php80/tree/v1.29.0"
},
"funding": [
{
@@ -1456,20 +1442,20 @@
"type": "tidelift"
}
],
"time": "2023-01-26T09:26:14+00:00"
"time": "2024-01-29T20:11:03+00:00"
},
{
"name": "symfony/polyfill-php81",
"version": "v1.28.0",
"version": "v1.29.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-php81.git",
"reference": "7581cd600fa9fd681b797d00b02f068e2f13263b"
"reference": "c565ad1e63f30e7477fc40738343c62b40bc672d"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-php81/zipball/7581cd600fa9fd681b797d00b02f068e2f13263b",
"reference": "7581cd600fa9fd681b797d00b02f068e2f13263b",
"url": "https://api.github.com/repos/symfony/polyfill-php81/zipball/c565ad1e63f30e7477fc40738343c62b40bc672d",
"reference": "c565ad1e63f30e7477fc40738343c62b40bc672d",
"shasum": ""
},
"require": {
@@ -1477,9 +1463,6 @@
},
"type": "library",
"extra": {
"branch-alias": {
"dev-main": "1.28-dev"
},
"thanks": {
"name": "symfony/polyfill",
"url": "https://github.com/symfony/polyfill"
@@ -1519,7 +1502,7 @@
"shim"
],
"support": {
"source": "https://github.com/symfony/polyfill-php81/tree/v1.28.0"
"source": "https://github.com/symfony/polyfill-php81/tree/v1.29.0"
},
"funding": [
{
@@ -1535,20 +1518,20 @@
"type": "tidelift"
}
],
"time": "2023-01-26T09:26:14+00:00"
"time": "2024-01-29T20:11:03+00:00"
},
{
"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": {
@@ -1580,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": [
{
@@ -1596,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",
@@ -1744,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": {
@@ -1810,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": [
{
@@ -1826,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

@@ -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",
@@ -680,16 +680,16 @@
},
{
"name": "symfony/polyfill-ctype",
"version": "v1.28.0",
"version": "v1.29.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-ctype.git",
"reference": "ea208ce43cbb04af6867b4fdddb1bdbf84cc28cb"
"reference": "ef4d7e442ca910c4764bce785146269b30cb5fc4"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/ea208ce43cbb04af6867b4fdddb1bdbf84cc28cb",
"reference": "ea208ce43cbb04af6867b4fdddb1bdbf84cc28cb",
"url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/ef4d7e442ca910c4764bce785146269b30cb5fc4",
"reference": "ef4d7e442ca910c4764bce785146269b30cb5fc4",
"shasum": ""
},
"require": {
@@ -703,9 +703,6 @@
},
"type": "library",
"extra": {
"branch-alias": {
"dev-main": "1.28-dev"
},
"thanks": {
"name": "symfony/polyfill",
"url": "https://github.com/symfony/polyfill"
@@ -742,7 +739,7 @@
"portable"
],
"support": {
"source": "https://github.com/symfony/polyfill-ctype/tree/v1.28.0"
"source": "https://github.com/symfony/polyfill-ctype/tree/v1.29.0"
},
"funding": [
{
@@ -758,20 +755,20 @@
"type": "tidelift"
}
],
"time": "2023-01-26T09:26:14+00:00"
"time": "2024-01-29T20:11:03+00:00"
},
{
"name": "symfony/polyfill-mbstring",
"version": "v1.28.0",
"version": "v1.29.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-mbstring.git",
"reference": "42292d99c55abe617799667f454222c54c60e229"
"reference": "9773676c8a1bb1f8d4340a62efe641cf76eda7ec"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/42292d99c55abe617799667f454222c54c60e229",
"reference": "42292d99c55abe617799667f454222c54c60e229",
"url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/9773676c8a1bb1f8d4340a62efe641cf76eda7ec",
"reference": "9773676c8a1bb1f8d4340a62efe641cf76eda7ec",
"shasum": ""
},
"require": {
@@ -785,9 +782,6 @@
},
"type": "library",
"extra": {
"branch-alias": {
"dev-main": "1.28-dev"
},
"thanks": {
"name": "symfony/polyfill",
"url": "https://github.com/symfony/polyfill"
@@ -825,7 +819,7 @@
"shim"
],
"support": {
"source": "https://github.com/symfony/polyfill-mbstring/tree/v1.28.0"
"source": "https://github.com/symfony/polyfill-mbstring/tree/v1.29.0"
},
"funding": [
{
@@ -841,7 +835,7 @@
"type": "tidelift"
}
],
"time": "2023-07-28T09:04:16+00:00"
"time": "2024-01-29T20:11:03+00:00"
},
{
"name": "symfony/service-contracts",
@@ -927,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": {
@@ -981,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": [
{
@@ -997,7 +991,7 @@
"type": "tidelift"
}
],
"time": "2024-01-23T15:02:46+00:00"
"time": "2024-02-26T10:35:24+00:00"
}
],
"aliases": [],

View File

@@ -1,6 +1,6 @@
# You can leave this on "local". If you change it to production most console commands will ask for extra confirmation.
# Never set it to "testing".
APP_ENV=local
APP_ENV=production
# Set to true if you want to see debug information in error screens.
APP_DEBUG=false
@@ -332,15 +332,7 @@ DEMO_PASSWORD=
FIREFLY_III_LAYOUT=v1
#
# If you have trouble configuring your Firefly III installation, DON'T BOTHER setting this variable.
# It won't work. It doesn't do ANYTHING. Don't believe the lies you read online. I'm not joking.
# This configuration value WILL NOT HELP.
#
# Notable exception to this rule is Synology, which, according to some users, will use APP_URL to rewrite stuff.
#
# This variable is ONLY used in some of the emails Firefly III sends around. Nowhere else.
# So when configuring anything WEB related this variable doesn't do anything. Nothing
#
# If you're stuck I understand you get desperate but look SOMEWHERE ELSE.
# Please make sure this URL matches the external URL of your Firefly III installation.
# It is used to validate specific requests and to generate URLs in emails.
#
APP_URL=http://localhost

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: ''
@@ -119,7 +119,7 @@ jobs:
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

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

@@ -68,7 +68,7 @@ class DestroyController extends Controller
$allExceptAssets = [AccountType::BENEFICIARY, AccountType::CASH, AccountType::CREDITCARD, AccountType::DEFAULT, AccountType::EXPENSE, AccountType::IMPORT, AccountType::INITIAL_BALANCE, AccountType::LIABILITY_CREDIT, AccountType::RECONCILIATION, AccountType::REVENUE];
$all = [AccountType::ASSET, AccountType::BENEFICIARY, AccountType::CASH, AccountType::CREDITCARD, AccountType::DEBT, AccountType::DEFAULT, AccountType::EXPENSE, AccountType::IMPORT, AccountType::INITIAL_BALANCE, AccountType::LIABILITY_CREDIT, AccountType::LOAN, AccountType::MORTGAGE, AccountType::RECONCILIATION];
$liabilities = [AccountType::DEBT, AccountType::LOAN, AccountType::MORTGAGE, AccountType::CREDITCARD];
$transactions = [TransactionType::WITHDRAWAL, TransactionType::DEPOSIT, TransactionType::TRANSFER, TransactionType::RECONCILIATION, TransactionType::OPENING_BALANCE];
$transactions = [TransactionType::WITHDRAWAL, TransactionType::DEPOSIT, TransactionType::TRANSFER, TransactionType::RECONCILIATION];
match ($objects) {
'budgets' => $this->destroyBudgets(),

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

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

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

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

@@ -64,6 +64,42 @@ class StoreRequest extends FormRequest
return $data;
}
private function getRuleTriggers(): array
{
$triggers = $this->get('triggers');
$return = [];
if (is_array($triggers)) {
foreach ($triggers as $trigger) {
$return[] = [
'type' => $trigger['type'],
'value' => $trigger['value'],
'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.
*/
@@ -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

@@ -70,6 +70,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.
*/
@@ -204,48 +248,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

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

View File

@@ -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.
*
@@ -362,4 +241,125 @@ class UpdateRequest extends FormRequest
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

@@ -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,41 @@ 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.
$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();
}
}

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,6 +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\InfiniteListRequest;
use FireflyIII\Api\V2\Request\Model\Transaction\ListRequest;
use FireflyIII\Helpers\Collector\GroupCollectorInterface;
use FireflyIII\Transformers\V2\TransactionGroupTransformer;
@@ -59,9 +60,6 @@ class TransactionController extends Controller
$collector->setEnd($end);
}
// $collector->dumpQuery();
// exit;
$paginator = $collector->getPaginatedGroups();
$params = $request->buildParams($pageSize);
$paginator->setPath(
@@ -77,4 +75,45 @@ class TransactionController extends Controller
->header('Content-Type', self::CONTENT_TYPE)
;
}
public function infiniteList(InfiniteListRequest $request): JsonResponse
{
// get sort instructions
$instructions = $request->getSortInstructions();
// collect transactions:
/** @var GroupCollectorInterface $collector */
$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');
if (null !== $start) {
$collector->setStart($start);
}
if (null !== $end) {
$collector->setEnd($end);
}
$paginator = $collector->getPaginatedGroups();
$params = $request->buildParams();
$paginator->setPath(
sprintf(
'%s?%s',
route('api.v2.infinite.transactions.list'),
$params
)
);
return response()
->json($this->jsonApiList('transactions', $paginator, new TransactionGroupTransformer()))
->header('Content-Type', self::CONTENT_TYPE)
;
}
}

View File

@@ -0,0 +1,132 @@
<?php
/*
* ListRequest.php
* Copyright (c) 2023 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\Transaction;
use Carbon\Carbon;
use FireflyIII\Support\Http\Api\TransactionFilter;
use FireflyIII\Support\Request\ChecksLogin;
use FireflyIII\Support\Request\ConvertsDataTypes;
use Illuminate\Foundation\Http\FormRequest;
/**
* Class InfiniteListRequest
* Used specifically to list transactions.
*/
class InfiniteListRequest extends FormRequest
{
use ChecksLogin;
use ConvertsDataTypes;
use TransactionFilter;
public function buildParams(): string
{
$array = [
'start_row' => $this->getStartRow(),
'end_row' => $this->getEndRow(),
];
$start = $this->getStartDate();
$end = $this->getEndDate();
if (null !== $start && null !== $end) {
$array['start'] = $start->format('Y-m-d');
$array['end'] = $end->format('Y-m-d');
}
return http_build_query($array);
}
public function getStartRow(): int
{
$startRow = $this->convertInteger('start_row');
return $startRow < 0 || $startRow > 4294967296 ? 0 : $startRow;
}
public function getEndRow(): int
{
$endRow = $this->convertInteger('end_row');
return $endRow <= 0 || $endRow > 4294967296 ? 100 : $endRow;
}
public function getStartDate(): ?Carbon
{
return $this->getCarbonDate('start');
}
public function getEndDate(): ?Carbon
{
return $this->getCarbonDate('end');
}
public function getPage(): int
{
$page = $this->convertInteger('page');
return 0 === $page || $page > 65536 ? 1 : $page;
}
public function getSortInstructions(): array
{
$allowed = config('firefly.sorting.allowed.transactions');
$set = $this->get('sorting', []);
$result = [];
if (0 === count($set)) {
return [];
}
foreach ($set as $info) {
$column = $info['column'] ?? 'NOPE';
$direction = $info['direction'] ?? 'NOPE';
if ('asc' !== $direction && 'desc' !== $direction) {
// skip invalid direction
continue;
}
if (false === in_array($column, $allowed, true)) {
// skip invalid column
continue;
}
$result[$column] = $direction;
}
return $result;
}
public function getTransactionTypes(): array
{
$type = (string)$this->get('type', 'default');
return $this->mapTransactionTypes($type);
}
public function rules(): array
{
return [
'start' => 'date',
'end' => 'date|after:start',
'start_row' => 'integer|min:0|max:4294967296',
'end_row' => 'integer|min:0|max:4294967296|gt:start_row',
];
}
}

View File

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

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.
*
@@ -363,4 +242,125 @@ class UpdateRequest extends Request
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

@@ -72,6 +72,16 @@ class UpgradeFireflyInstructions extends Command
}
}
// validate some settings.
if ('' === $text && 'local' === (string)config('app.env')) {
$text = 'Please set APP_ENV=production for a safer environment.';
}
$prefix = 'v';
if (str_starts_with($version, 'develop')) {
$prefix = '';
}
$this->newLine();
$this->showLogo();
$this->newLine();
@@ -79,7 +89,7 @@ class UpgradeFireflyInstructions extends Command
$this->boxed('');
if ('' === $text) {
$this->boxed(sprintf('Thank you for updating to Firefly III, v%s', $version));
$this->boxed(sprintf('Thank you for updating to Firefly III, %s%s', $prefix, $version));
$this->boxedInfo('There are no extra upgrade instructions.');
$this->boxed('Firefly III should be ready for use.');
$this->boxed('');
@@ -88,7 +98,7 @@ class UpgradeFireflyInstructions extends Command
return;
}
$this->boxed(sprintf('Thank you for updating to Firefly III, v%s!', $version));
$this->boxed(sprintf('Thank you for updating to Firefly III, %s%s!', $prefix, $version));
$this->boxedInfo($text);
$this->boxed('');
$this->showLine();
@@ -181,13 +191,24 @@ class UpgradeFireflyInstructions extends Command
$text = (string)$config[$compare];
}
}
// validate some settings.
if ('' === $text && 'local' === (string)config('app.env')) {
$text = 'Please set APP_ENV=production for a safer environment.';
}
$prefix = 'v';
if (str_starts_with($version, 'develop')) {
$prefix = '';
}
$this->newLine();
$this->showLogo();
$this->newLine();
$this->showLine();
$this->boxed('');
if ('' === $text) {
$this->boxed(sprintf('Thank you for installing Firefly III, v%s!', $version));
$this->boxed(sprintf('Thank you for installing Firefly III, %s%s!', $prefix, $version));
$this->boxedInfo('There are no extra installation instructions.');
$this->boxed('Firefly III should be ready for use.');
$this->boxed('');
@@ -196,7 +217,7 @@ class UpgradeFireflyInstructions extends Command
return;
}
$this->boxed(sprintf('Thank you for installing Firefly III, v%s!', $version));
$this->boxed(sprintf('Thank you for installing Firefly III, %s%s!', $prefix, $version));
$this->boxedInfo($text);
$this->boxed('');
$this->showLine();

View File

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

@@ -34,8 +34,7 @@ class RequestedVersionCheckStatus extends Event
{
use SerializesModels;
/** @var User The user */
public $user;
public User $user;
/**
* Create a new event instance. This event is triggered when Firefly III wants to know

View File

@@ -227,6 +227,16 @@ class Handler extends ExceptionHandler
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

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

@@ -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.
@@ -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();
@@ -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__));
@@ -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

@@ -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));
@@ -154,6 +140,7 @@ class BudgetLimitHandler
]
);
$availableBudget->save();
app('log')->debug(sprintf('ID of new AB is #%d', $availableBudget->id));
}
}
@@ -247,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

@@ -27,6 +27,8 @@ namespace FireflyIII\Handlers\Events\Model;
use FireflyIII\Events\Model\Rule\RuleActionFailedOnArray;
use FireflyIII\Events\Model\Rule\RuleActionFailedOnObject;
use FireflyIII\Notifications\User\RuleActionFailed;
use GuzzleHttp\Exception\ClientException;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Notification;
/**
@@ -56,7 +58,11 @@ class RuleHandler
$ruleLink = route('rules.edit', [$rule->id]);
$params = [$mainMessage, $groupTitle, $groupLink, $ruleTitle, $ruleLink];
try {
Notification::send($user, new RuleActionFailed($params));
} catch (ClientException $e) {
Log::error(sprintf('[a] Error sending notification that the rule action failed: %s', $e->getMessage()));
}
}
public function ruleActionFailedOnObject(RuleActionFailedOnObject $event): void
@@ -81,6 +87,10 @@ class RuleHandler
$ruleLink = route('rules.edit', [$rule->id]);
$params = [$mainMessage, $groupTitle, $groupLink, $ruleTitle, $ruleLink];
try {
Notification::send($user, new RuleActionFailed($params));
} catch (ClientException $e) {
Log::error(sprintf('[b] Error sending notification that the rule action failed: %s', $e->getMessage()));
}
}
}

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

@@ -33,7 +33,10 @@ use Illuminate\Database\Eloquent\Relations\HasMany;
*/
trait CollectorProperties
{
/** @var array<int, string> */
public array $sorting;
public const string TEST = 'Test';
private ?int $endRow;
private bool $expandGroupSearch;
private array $fields;
private bool $hasAccountInfo;
@@ -49,6 +52,7 @@ trait CollectorProperties
private ?int $page;
private array $postFilters;
private HasMany $query;
private ?int $startRow;
private array $stringFields;
/*
* This array is used to collect ALL tags the user may search for (using 'setTags').

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

@@ -61,12 +61,15 @@ class GroupCollector implements GroupCollectorInterface
*/
public function __construct()
{
$this->sorting = [];
$this->postFilters = [];
$this->tags = [];
$this->user = null;
$this->userGroup = null;
$this->limit = null;
$this->page = null;
$this->startRow = null;
$this->endRow = null;
$this->hasAccountInfo = false;
$this->hasCatInformation = false;
@@ -464,6 +467,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();
@@ -473,211 +479,14 @@ class GroupCollector implements GroupCollectorInterface
return $collection->slice($offset, $this->limit);
}
// OR filter the array according to the start and end row variable
if (null !== $this->startRow && null !== $this->endRow) {
return $collection->slice($this->startRow, $this->endRow);
}
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);
}
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;
}
private function getCollectedGroupIds(): array
{
return $this->query->get(['transaction_journals.transaction_group_id'])->pluck('transaction_group_id')->toArray();
@@ -931,6 +740,11 @@ class GroupCollector implements GroupCollectorInterface
private function postFilterCollection(Collection $collection): Collection
{
$currentCollection = $collection;
$countFilters = count($this->postFilters);
$countCollection = count($currentCollection);
if (0 === $countFilters && 0 === $countCollection) {
return $currentCollection;
}
app('log')->debug(sprintf('GroupCollector: postFilterCollection has %d filter(s) and %d transaction(s).', count($this->postFilters), count($currentCollection)));
/**
@@ -968,6 +782,195 @@ class GroupCollector implements GroupCollectorInterface
return $currentCollection;
}
/**
* 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 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;
}
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.
*/
@@ -1014,6 +1017,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.
*/
@@ -1057,4 +1073,59 @@ 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;
}
#[\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;
}
#[\Override]
public function setSorting(array $instructions): GroupCollectorInterface
{
$this->sorting = $instructions;
return $this;
}
}

View File

@@ -285,6 +285,13 @@ interface GroupCollectorInterface
*/
public function getPaginatedGroups(): LengthAwarePaginator;
public function setSorting(array $instructions): self;
/**
* Sort the collection on a column.
*/
public function sortCollection(Collection $collection): Collection;
public function hasAnyTag(): self;
/**
@@ -401,6 +408,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 +473,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;
/**
@@ -553,6 +570,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.
*/
@@ -563,11 +585,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.
*/

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
@@ -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) {
@@ -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(

View File

@@ -68,6 +68,9 @@ class ForgotPasswordController extends Controller
return view('error', compact('message'));
}
// validate host header.
$this->validateHost();
$this->validateEmail($request);
// verify if the user is not a demo user. If so, we give him back an error.
@@ -92,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.
*

View File

@@ -132,6 +132,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 +229,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

@@ -80,6 +80,7 @@ class ResetPasswordController extends Controller
return view('error', compact('message'));
}
$rules = [
'token' => 'required',
'email' => 'required|email',
@@ -90,7 +91,7 @@ class ResetPasswordController extends Controller
// Here we will attempt to reset the user's password. If it is successful we
// will update the password on an actual user model and persist it to the
// database. Otherwise we will parse the error and return the response.
// database. Otherwise, we will parse the error and return the response.
$response = $this->broker()->reset(
$this->credentials($request),
function ($user, $password): void {

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

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

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

@@ -24,7 +24,6 @@ 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;
@@ -141,21 +140,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:
@@ -178,7 +162,9 @@ class DebugController extends Controller
'db_version' => app('fireflyconfig')->get('db_version', 1)->data,
'php_version' => PHP_VERSION,
'php_os' => PHP_OS,
'uname' => php_uname('m'),
'interface' => \PHP_SAPI,
'bits' => \PHP_INT_SIZE * 8,
'bcscale' => bcscale(),
'display_errors' => ini_get('display_errors'),
'error_reporting' => $this->errorReporting((int)ini_get('error_reporting')),
@@ -201,6 +187,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.');
@@ -335,4 +322,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

@@ -78,7 +78,7 @@ class RecurrenceController extends Controller
$weekend = (int)$request->get('weekend');
$repetitionMoment = '';
$skip = (int)$request->get('skip');
$skip = $skip < 1 || $skip > 31 ? 1 : $skip;
$skip = $skip < 0 || $skip > 31 ? 0 : $skip;
$weekend = $weekend < 1 || $weekend > 4 ? 1 : $weekend;
if (false === $start || false === $end || false === $firstDate || false === $endDate) {

View File

@@ -141,24 +141,6 @@ class IndexController extends Controller
return view('piggy-banks.index', compact('piggyBanks', 'accounts'));
}
/**
* Set the order of a piggy bank.
*/
public function setOrder(Request $request, PiggyBank $piggyBank): JsonResponse
{
$objectGroupTitle = (string)$request->get('objectGroupTitle');
$newOrder = (int)$request->get('order');
$this->piggyRepos->setOrder($piggyBank, $newOrder);
if ('' !== $objectGroupTitle) {
$this->piggyRepos->setObjectGroup($piggyBank, $objectGroupTitle);
}
if ('' === $objectGroupTitle) {
$this->piggyRepos->removeObjectGroup($piggyBank);
}
return response()->json(['data' => 'OK']);
}
private function makeSums(array $piggyBanks): array
{
$sums = [];
@@ -193,4 +175,22 @@ class IndexController extends Controller
return $piggyBanks;
}
/**
* Set the order of a piggy bank.
*/
public function setOrder(Request $request, PiggyBank $piggyBank): JsonResponse
{
$objectGroupTitle = (string)$request->get('objectGroupTitle');
$newOrder = (int)$request->get('order');
$this->piggyRepos->setOrder($piggyBank, $newOrder);
if ('' !== $objectGroupTitle) {
$this->piggyRepos->setObjectGroup($piggyBank, $objectGroupTitle);
}
if ('' === $objectGroupTitle) {
$this->piggyRepos->removeObjectGroup($piggyBank);
}
return response()->json(['data' => 'OK']);
}
}

View File

@@ -497,6 +497,47 @@ class ProfileController extends Controller
return redirect(route('profile.index'));
}
/**
* TODO duplicate code.
*
* @throws FireflyException
*/
private function addToMFAHistory(string $mfaCode): void
{
/** @var array $mfaHistory */
$mfaHistory = app('preferences')->get('mfa_history', [])->data;
$entry = [
'time' => time(),
'code' => $mfaCode,
];
$mfaHistory[] = $entry;
app('preferences')->set('mfa_history', $mfaHistory);
$this->filterMFAHistory();
}
/**
* Remove old entries from the preferences array.
*/
private function filterMFAHistory(): void
{
/** @var array $mfaHistory */
$mfaHistory = app('preferences')->get('mfa_history', [])->data;
$newHistory = [];
$now = time();
foreach ($mfaHistory as $entry) {
$time = $entry['time'];
$code = $entry['code'];
if ($now - $time <= 300) {
$newHistory[] = [
'time' => $time,
'code' => $code,
];
}
}
app('preferences')->set('mfa_history', $newHistory);
}
/**
* Submit delete account.
*
@@ -631,45 +672,4 @@ class ProfileController extends Controller
return redirect(route('login'));
}
/**
* TODO duplicate code.
*
* @throws FireflyException
*/
private function addToMFAHistory(string $mfaCode): void
{
/** @var array $mfaHistory */
$mfaHistory = app('preferences')->get('mfa_history', [])->data;
$entry = [
'time' => time(),
'code' => $mfaCode,
];
$mfaHistory[] = $entry;
app('preferences')->set('mfa_history', $mfaHistory);
$this->filterMFAHistory();
}
/**
* Remove old entries from the preferences array.
*/
private function filterMFAHistory(): void
{
/** @var array $mfaHistory */
$mfaHistory = app('preferences')->get('mfa_history', [])->data;
$newHistory = [];
$now = time();
foreach ($mfaHistory as $entry) {
$time = $entry['time'];
$code = $entry['code'];
if ($now - $time <= 300) {
$newHistory[] = [
'time' => $time,
'code' => $code,
];
}
}
app('preferences')->set('mfa_history', $newHistory);
}
}

View File

@@ -278,6 +278,24 @@ class DoubleController extends Controller
return view('reports.double.partials.accounts', compact('sums', 'report'));
}
/**
* TODO this method is duplicated.
*/
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;
}
/**
* @return Factory|View
*/
@@ -468,22 +486,4 @@ class DoubleController extends Controller
return $result;
}
/**
* TODO this method is duplicated.
*/
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;
}
}

View File

@@ -159,7 +159,7 @@ class CreateController extends Controller
$oldActions = $this->getActionsForBill($bill);
// restore actions and triggers from old input:
if (null !== $request->old()) {
if (null !== $request->old() && is_array($request->old()) && count($request->old()) > 0) {
$oldTriggers = $this->getPreviousTriggers($request);
$oldActions = $this->getPreviousActions($request);
}

View File

@@ -132,29 +132,6 @@ class EditController extends Controller
return view('rules.rule.edit', compact('rule', 'subTitle', 'primaryTrigger', 'oldTriggers', 'oldActions', 'triggerCount', 'actionCount'));
}
/**
* Update the rule.
*
* @return Redirector|RedirectResponse
*/
public function update(RuleFormRequest $request, Rule $rule)
{
$data = $request->getRuleData();
$this->ruleRepos->update($rule, $data);
session()->flash('success', (string)trans('firefly.updated_rule', ['title' => $rule->title]));
app('preferences')->mark();
$redirect = redirect($this->getPreviousUrl('rules.edit.url'));
if (1 === (int)$request->get('return_to_edit')) {
session()->put('rules.edit.fromUpdate', true);
$redirect = redirect(route('rules.edit', [$rule->id]))->withInput(['return_to_edit' => 1]);
}
return $redirect;
}
/**
* @throws FireflyException
*/
@@ -196,4 +173,27 @@ class EditController extends Controller
return $renderedEntries;
}
/**
* Update the rule.
*
* @return Redirector|RedirectResponse
*/
public function update(RuleFormRequest $request, Rule $rule)
{
$data = $request->getRuleData();
$this->ruleRepos->update($rule, $data);
session()->flash('success', (string)trans('firefly.updated_rule', ['title' => $rule->title]));
app('preferences')->mark();
$redirect = redirect($this->getPreviousUrl('rules.edit.url'));
if (1 === (int)$request->get('return_to_edit')) {
session()->put('rules.edit.fromUpdate', true);
$redirect = redirect(route('rules.edit', [$rule->id]))->withInput(['return_to_edit' => 1]);
}
return $redirect;
}
}

View File

@@ -129,26 +129,6 @@ class InstallController extends Controller
return response()->json($response);
}
/**
* Create specific RSA keys.
*/
public function keys(): void
{
$key = RSA::createKey(4096);
[$publicKey, $privateKey] = [
Passport::keyPath('oauth-public.key'),
Passport::keyPath('oauth-private.key'),
];
if (file_exists($publicKey) || file_exists($privateKey)) {
return;
}
file_put_contents($publicKey, (string)$key->getPublicKey());
file_put_contents($privateKey, $key->toString('PKCS1'));
}
/**
* @throws FireflyException
*/
@@ -173,4 +153,24 @@ class InstallController extends Controller
return true;
}
/**
* Create specific RSA keys.
*/
public function keys(): void
{
$key = RSA::createKey(4096);
[$publicKey, $privateKey] = [
Passport::keyPath('oauth-public.key'),
Passport::keyPath('oauth-private.key'),
];
if (file_exists($publicKey) || file_exists($privateKey)) {
return;
}
file_put_contents($publicKey, (string)$key->getPublicKey());
file_put_contents($privateKey, $key->toString('PKCS1'));
}
}

View File

@@ -135,38 +135,6 @@ class ConvertController extends Controller
);
}
/**
* Do the conversion.
*
* @return Redirector|RedirectResponse
*/
public function postIndex(Request $request, TransactionType $destinationType, TransactionGroup $group)
{
if (!$this->isEditableGroup($group)) {
return $this->redirectGroupToAccount($group);
}
/** @var TransactionJournal $journal */
foreach ($group->transactionJournals as $journal) {
// catch FF exception.
try {
$this->convertJournal($journal, $destinationType, $request->all());
} catch (FireflyException $e) {
session()->flash('error', $e->getMessage());
return redirect()->route('transactions.convert.index', [strtolower($destinationType->type), $group->id])->withInput();
}
}
// correct transfers:
$group->refresh();
session()->flash('success', (string)trans('firefly.converted_to_'.$destinationType->type));
event(new UpdatedTransactionGroup($group, true, true));
return redirect(route('transactions.show', [$group->id]));
}
private function getValidDepositSources(): array
{
// make repositories
@@ -291,6 +259,38 @@ class ConvertController extends Controller
return $grouped;
}
/**
* Do the conversion.
*
* @return Redirector|RedirectResponse
*/
public function postIndex(Request $request, TransactionType $destinationType, TransactionGroup $group)
{
if (!$this->isEditableGroup($group)) {
return $this->redirectGroupToAccount($group);
}
/** @var TransactionJournal $journal */
foreach ($group->transactionJournals as $journal) {
// catch FF exception.
try {
$this->convertJournal($journal, $destinationType, $request->all());
} catch (FireflyException $e) {
session()->flash('error', $e->getMessage());
return redirect()->route('transactions.convert.index', [strtolower($destinationType->type), $group->id])->withInput();
}
}
// correct transfers:
$group->refresh();
session()->flash('success', (string)trans('firefly.converted_to_'.$destinationType->type));
event(new UpdatedTransactionGroup($group, true, true));
return redirect(route('transactions.show', [$group->id]));
}
/**
* @throws FireflyException
*/

View File

@@ -73,22 +73,6 @@ class Installer
return $next($request);
}
/**
* Is access denied error.
*/
protected function isAccessDenied(string $message): bool
{
return false !== stripos($message, 'Access denied');
}
/**
* Is no tables exist error.
*/
protected function noTablesExist(string $message): bool
{
return false !== stripos($message, 'Base table or view not found');
}
/**
* Check if the tables are created and accounted for.
*
@@ -125,6 +109,22 @@ class Installer
return false;
}
/**
* Is access denied error.
*/
protected function isAccessDenied(string $message): bool
{
return false !== stripos($message, 'Access denied');
}
/**
* Is no tables exist error.
*/
protected function noTablesExist(string $message): bool
{
return false !== stripos($message, 'Base table or view not found');
}
/**
* Check if the "db_version" variable is correct.
*/

View File

@@ -0,0 +1,41 @@
<?php
/*
* TrustHosts.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\Http\Middleware;
use Illuminate\Http\Middleware\TrustHosts as Middleware;
class TrustHosts extends Middleware
{
/**
* Get the host patterns that should be trusted.
*
* @return array<int, null|string>
*/
public function hosts(): array
{
return [
$this->allSubdomainsOfApplicationUrl(),
];
}
}

View File

@@ -150,6 +150,38 @@ class RecurrenceFormRequest extends FormRequest
return $return;
}
/**
* Parses repetition data.
*/
private function parseRepetitionData(): array
{
$value = $this->convertString('repetition_type');
$return = [
'type' => '',
'moment' => '',
];
if ('daily' === $value) {
$return['type'] = $value;
}
// monthly,17
// ndom,3,7
if (in_array(substr($value, 0, 6), ['yearly', 'weekly'], true)) {
$return['type'] = substr($value, 0, 6);
$return['moment'] = substr($value, 7);
}
if (str_starts_with($value, 'monthly')) {
$return['type'] = substr($value, 0, 7);
$return['moment'] = substr($value, 8);
}
if (str_starts_with($value, 'ndom')) {
$return['type'] = substr($value, 0, 4);
$return['moment'] = substr($value, 5);
}
return $return;
}
/**
* The rules for this request.
*/
@@ -316,36 +348,4 @@ class RecurrenceFormRequest extends FormRequest
$validator->errors()->add('withdrawal_destination_id', $message);
}
}
/**
* Parses repetition data.
*/
private function parseRepetitionData(): array
{
$value = $this->convertString('repetition_type');
$return = [
'type' => '',
'moment' => '',
];
if ('daily' === $value) {
$return['type'] = $value;
}
// monthly,17
// ndom,3,7
if (in_array(substr($value, 0, 6), ['yearly', 'weekly'], true)) {
$return['type'] = substr($value, 0, 6);
$return['moment'] = substr($value, 7);
}
if (str_starts_with($value, 'monthly')) {
$return['type'] = substr($value, 0, 7);
$return['moment'] = substr($value, 8);
}
if (str_starts_with($value, 'ndom')) {
$return['type'] = substr($value, 0, 4);
$return['moment'] = substr($value, 5);
}
return $return;
}
}

View File

@@ -58,6 +58,28 @@ class RuleFormRequest extends FormRequest
];
}
private function getRuleTriggerData(): array
{
$return = [];
$triggerData = $this->get('triggers');
if (is_array($triggerData)) {
foreach ($triggerData as $trigger) {
$stopProcessing = $trigger['stop_processing'] ?? '0';
$prohibited = $trigger['prohibited'] ?? '0';
$set = [
'type' => $trigger['type'] ?? 'invalid',
'value' => $trigger['value'] ?? '',
'stop_processing' => 1 === (int)$stopProcessing,
'prohibited' => 1 === (int)$prohibited,
];
$set = self::replaceAmountTrigger($set);
$return[] = $set;
}
}
return $return;
}
public static function replaceAmountTrigger(array $array): array
{
// do some sneaky search and replace.
@@ -83,6 +105,24 @@ class RuleFormRequest extends FormRequest
return $array;
}
private function getRuleActionData(): array
{
$return = [];
$actionData = $this->get('actions');
if (is_array($actionData)) {
foreach ($actionData as $action) {
$stopProcessing = $action['stop_processing'] ?? '0';
$return[] = [
'type' => $action['type'] ?? 'invalid',
'value' => $action['value'] ?? '',
'stop_processing' => 1 === (int)$stopProcessing,
];
}
}
return $return;
}
/**
* Rules for this request.
*/
@@ -127,44 +167,4 @@ class RuleFormRequest extends FormRequest
Log::channel('audit')->error(sprintf('Validation errors in %s', __CLASS__), $validator->errors()->toArray());
}
}
private function getRuleTriggerData(): array
{
$return = [];
$triggerData = $this->get('triggers');
if (is_array($triggerData)) {
foreach ($triggerData as $trigger) {
$stopProcessing = $trigger['stop_processing'] ?? '0';
$prohibited = $trigger['prohibited'] ?? '0';
$set = [
'type' => $trigger['type'] ?? 'invalid',
'value' => $trigger['value'] ?? '',
'stop_processing' => 1 === (int) $stopProcessing,
'prohibited' => 1 === (int) $prohibited,
];
$set = self::replaceAmountTrigger($set);
$return[] = $set;
}
}
return $return;
}
private function getRuleActionData(): array
{
$return = [];
$actionData = $this->get('actions');
if (is_array($actionData)) {
foreach ($actionData as $action) {
$stopProcessing = $action['stop_processing'] ?? '0';
$return[] = [
'type' => $action['type'] ?? 'invalid',
'value' => $action['value'] ?? '',
'stop_processing' => 1 === (int) $stopProcessing,
];
}
}
return $return;
}
}

View File

@@ -77,13 +77,6 @@ class CreateAutoBudgetLimits implements ShouldQueue
}
}
public function setDate(Carbon $date): void
{
$newDate = clone $date;
$newDate->startOfDay();
$this->date = $newDate;
}
/**
* @throws FireflyException
*/
@@ -362,4 +355,11 @@ class CreateAutoBudgetLimits implements ShouldQueue
}
app('log')->debug(sprintf('Done with auto budget #%d', $autoBudget->id));
}
public function setDate(Carbon $date): void
{
$newDate = clone $date;
$newDate->startOfDay();
$this->date = $newDate;
}
}

View File

@@ -152,23 +152,6 @@ class CreateRecurringTransactions implements ShouldQueue
app('preferences')->mark();
}
public function setDate(Carbon $date): void
{
$newDate = clone $date;
$newDate->startOfDay();
$this->date = $newDate;
}
public function setForce(bool $force): void
{
$this->force = $force;
}
public function setRecurrences(Collection $recurrences): void
{
$this->recurrences = $recurrences;
}
private function filterRecurrences(Collection $recurrences): Collection
{
return $recurrences->filter(
@@ -465,4 +448,21 @@ class CreateRecurringTransactions implements ShouldQueue
return $return;
}
public function setDate(Carbon $date): void
{
$newDate = clone $date;
$newDate->startOfDay();
$this->date = $newDate;
}
public function setForce(bool $force): void
{
$this->force = $force;
}
public function setRecurrences(Collection $recurrences): void
{
$this->recurrences = $recurrences;
}
}

View File

@@ -88,13 +88,6 @@ class DownloadExchangeRates implements ShouldQueue
}
}
public function setDate(Carbon $date): void
{
$newDate = clone $date;
$newDate->startOfDay();
$this->date = $newDate;
}
/**
* @throws GuzzleException
*/
@@ -185,4 +178,11 @@ class DownloadExchangeRates implements ShouldQueue
}
}
}
public function setDate(Carbon $date): void
{
$newDate = clone $date;
$newDate->startOfDay();
$this->date = $newDate;
}
}

View File

@@ -92,18 +92,6 @@ class WarnAboutBills implements ShouldQueue
app('preferences')->mark();
}
public function setDate(Carbon $date): void
{
$newDate = clone $date;
$newDate->startOfDay();
$this->date = $newDate;
}
public function setForce(bool $force): void
{
$this->force = $force;
}
private function hasDateFields(Bill $bill): bool
{
if (false === $bill->active) {
@@ -149,4 +137,16 @@ class WarnAboutBills implements ShouldQueue
app('log')->debug('Will now send warning!');
event(new WarnUserAboutBill($bill, $field, $diff));
}
public function setDate(Carbon $date): void
{
$newDate = clone $date;
$newDate->startOfDay();
$this->date = $newDate;
}
public function setForce(bool $force): void
{
$this->force = $force;
}
}

View File

@@ -63,7 +63,7 @@ use Illuminate\Database\Eloquent\Relations\MorphTo;
* @method static Builder|Location whereUpdatedAt($value)
* @method static Builder|Location whereZoomLevel($value)
*
* @property Collection<int, \FireflyIII\Models\TransactionJournal> $transactionJournals
* @property Collection<int, TransactionJournal> $transactionJournals
* @property null|int $transaction_journals_count
*
* @mixin Eloquent
@@ -101,11 +101,6 @@ class Location extends Model
return $this->morphMany(Account::class, 'locatable');
}
public function transactionJournals(): MorphMany
{
return $this->morphMany(TransactionJournal::class, 'locatable');
}
/**
* Get all the owning attachable models.
*/
@@ -114,6 +109,11 @@ class Location extends Model
return $this->morphTo();
}
public function transactionJournals(): MorphMany
{
return $this->morphMany(TransactionJournal::class, 'locatable');
}
protected function locatableId(): Attribute
{
return Attribute::make(

View File

@@ -112,11 +112,11 @@ class RuleActionFailed extends Notification
$slackUrl = '';
}
if (UrlValidator::isValidWebhookURL((string)$slackUrl)) {
app('log')->debug('Will send ruleActionFailed through Slack!');
app('log')->debug('Will send ruleActionFailed through Slack or Discord!');
return ['slack'];
}
app('log')->debug('Will NOT send ruleActionFailed through Slack');
app('log')->debug('Will NOT send ruleActionFailed through Slack or Discord');
return [];
}

View File

@@ -138,41 +138,6 @@ class AccountTasker implements AccountTaskerInterface
return $report;
}
/**
* @throws FireflyException
*/
public function getIncomeReport(Carbon $start, Carbon $end, Collection $accounts): array
{
// get all incomes for the given accounts in the given period!
// also transfers!
// get all transactions:
/** @var GroupCollectorInterface $collector */
$collector = app(GroupCollectorInterface::class);
$collector->setDestinationAccounts($accounts)->setRange($start, $end);
$collector->excludeSourceAccounts($accounts);
$collector->setTypes([TransactionType::DEPOSIT, TransactionType::TRANSFER])->withAccountInformation();
$report = $this->groupIncomeBySource($collector->getExtractedJournals());
// sort the result
// Obtain a list of columns
$sum = [];
foreach ($report['accounts'] as $accountId => $row) {
$sum[$accountId] = (float)$row['sum']; // intentional float
}
array_multisort($sum, SORT_DESC, $report['accounts']);
return $report;
}
public function setUser(null|Authenticatable|User $user): void
{
if ($user instanceof User) {
$this->user = $user;
}
}
/**
* @throws FireflyException
*/
@@ -233,6 +198,34 @@ class AccountTasker implements AccountTaskerInterface
return $report;
}
/**
* @throws FireflyException
*/
public function getIncomeReport(Carbon $start, Carbon $end, Collection $accounts): array
{
// get all incomes for the given accounts in the given period!
// also transfers!
// get all transactions:
/** @var GroupCollectorInterface $collector */
$collector = app(GroupCollectorInterface::class);
$collector->setDestinationAccounts($accounts)->setRange($start, $end);
$collector->excludeSourceAccounts($accounts);
$collector->setTypes([TransactionType::DEPOSIT, TransactionType::TRANSFER])->withAccountInformation();
$report = $this->groupIncomeBySource($collector->getExtractedJournals());
// sort the result
// Obtain a list of columns
$sum = [];
foreach ($report['accounts'] as $accountId => $row) {
$sum[$accountId] = (float)$row['sum']; // intentional float
}
array_multisort($sum, SORT_DESC, $report['accounts']);
return $report;
}
/**
* @throws FireflyException
*/
@@ -291,4 +284,11 @@ class AccountTasker implements AccountTaskerInterface
return $report;
}
public function setUser(null|Authenticatable|User $user): void
{
if ($user instanceof User) {
$this->user = $user;
}
}
}

View File

@@ -50,122 +50,6 @@ class OperationsRepository implements OperationsRepositoryInterface
return $this->sortByCurrency($journals, 'negative');
}
public function setUser(null|Authenticatable|User $user): void
{
if ($user instanceof User) {
$this->user = $user;
}
}
/**
* This method returns a list of all the deposit transaction journals (as arrays) set in that period
* which have the specified accounts. It's grouped per currency, with as few details in the array
* as possible. Amounts are always positive.
*/
public function listIncome(Carbon $start, Carbon $end, ?Collection $accounts = null): array
{
$journals = $this->getTransactions($start, $end, $accounts, TransactionType::DEPOSIT);
return $this->sortByCurrency($journals, 'positive');
}
/**
* @SuppressWarnings(PHPMD.ExcessiveParameterList)
*/
public function sumExpenses(
Carbon $start,
Carbon $end,
?Collection $accounts = null,
?Collection $expense = null,
?TransactionCurrency $currency = null
): array {
$journals = $this->getTransactionsForSum(TransactionType::WITHDRAWAL, $start, $end, $accounts, $expense, $currency);
return $this->groupByCurrency($journals, 'negative');
}
/**
* @SuppressWarnings(PHPMD.ExcessiveParameterList)
*/
public function sumExpensesByDestination(
Carbon $start,
Carbon $end,
?Collection $accounts = null,
?Collection $expense = null,
?TransactionCurrency $currency = null
): array {
$journals = $this->getTransactionsForSum(TransactionType::WITHDRAWAL, $start, $end, $accounts, $expense, $currency);
return $this->groupByDirection($journals, 'destination', 'negative');
}
/**
* @SuppressWarnings(PHPMD.ExcessiveParameterList)
*/
public function sumExpensesBySource(
Carbon $start,
Carbon $end,
?Collection $accounts = null,
?Collection $expense = null,
?TransactionCurrency $currency = null
): array {
$journals = $this->getTransactionsForSum(TransactionType::WITHDRAWAL, $start, $end, $accounts, $expense, $currency);
return $this->groupByDirection($journals, 'source', 'negative');
}
/**
* @SuppressWarnings(PHPMD.ExcessiveParameterList)
*/
public function sumIncome(
Carbon $start,
Carbon $end,
?Collection $accounts = null,
?Collection $revenue = null,
?TransactionCurrency $currency = null
): array {
$journals = $this->getTransactionsForSum(TransactionType::DEPOSIT, $start, $end, $accounts, $revenue, $currency);
return $this->groupByCurrency($journals, 'positive');
}
/**
* @SuppressWarnings(PHPMD.ExcessiveParameterList)
*/
public function sumIncomeByDestination(
Carbon $start,
Carbon $end,
?Collection $accounts = null,
?Collection $revenue = null,
?TransactionCurrency $currency = null
): array {
$journals = $this->getTransactionsForSum(TransactionType::DEPOSIT, $start, $end, $accounts, $revenue, $currency);
return $this->groupByDirection($journals, 'destination', 'positive');
}
/**
* @SuppressWarnings(PHPMD.ExcessiveParameterList)
*/
public function sumIncomeBySource(
Carbon $start,
Carbon $end,
?Collection $accounts = null,
?Collection $revenue = null,
?TransactionCurrency $currency = null
): array {
$journals = $this->getTransactionsForSum(TransactionType::DEPOSIT, $start, $end, $accounts, $revenue, $currency);
return $this->groupByDirection($journals, 'source', 'positive');
}
public function sumTransfers(Carbon $start, Carbon $end, ?Collection $accounts = null, ?TransactionCurrency $currency = null): array
{
$journals = $this->getTransactionsForSum(TransactionType::TRANSFER, $start, $end, $accounts, null, $currency);
return $this->groupByEither($journals);
}
/**
* Collect transactions with some parameters
*/
@@ -180,6 +64,13 @@ class OperationsRepository implements OperationsRepositoryInterface
return $collector->getExtractedJournals();
}
public function setUser(null|Authenticatable|User $user): void
{
if ($user instanceof User) {
$this->user = $user;
}
}
private function sortByCurrency(array $journals, string $direction): array
{
$array = [];
@@ -216,6 +107,33 @@ class OperationsRepository implements OperationsRepositoryInterface
return $array;
}
/**
* This method returns a list of all the deposit transaction journals (as arrays) set in that period
* which have the specified accounts. It's grouped per currency, with as few details in the array
* as possible. Amounts are always positive.
*/
public function listIncome(Carbon $start, Carbon $end, ?Collection $accounts = null): array
{
$journals = $this->getTransactions($start, $end, $accounts, TransactionType::DEPOSIT);
return $this->sortByCurrency($journals, 'positive');
}
/**
* @SuppressWarnings(PHPMD.ExcessiveParameterList)
*/
public function sumExpenses(
Carbon $start,
Carbon $end,
?Collection $accounts = null,
?Collection $expense = null,
?TransactionCurrency $currency = null
): array {
$journals = $this->getTransactionsForSum(TransactionType::WITHDRAWAL, $start, $end, $accounts, $expense, $currency);
return $this->groupByCurrency($journals, 'negative');
}
/**
* @SuppressWarnings(PHPMD.ExcessiveParameterList)
* @SuppressWarnings(PHPMD.NPathComplexity)
@@ -329,6 +247,21 @@ class OperationsRepository implements OperationsRepositoryInterface
return $array;
}
/**
* @SuppressWarnings(PHPMD.ExcessiveParameterList)
*/
public function sumExpensesByDestination(
Carbon $start,
Carbon $end,
?Collection $accounts = null,
?Collection $expense = null,
?TransactionCurrency $currency = null
): array {
$journals = $this->getTransactionsForSum(TransactionType::WITHDRAWAL, $start, $end, $accounts, $expense, $currency);
return $this->groupByDirection($journals, 'destination', 'negative');
}
private function groupByDirection(array $journals, string $direction, string $method): array
{
$array = [];
@@ -369,6 +302,73 @@ class OperationsRepository implements OperationsRepositoryInterface
return $array;
}
/**
* @SuppressWarnings(PHPMD.ExcessiveParameterList)
*/
public function sumExpensesBySource(
Carbon $start,
Carbon $end,
?Collection $accounts = null,
?Collection $expense = null,
?TransactionCurrency $currency = null
): array {
$journals = $this->getTransactionsForSum(TransactionType::WITHDRAWAL, $start, $end, $accounts, $expense, $currency);
return $this->groupByDirection($journals, 'source', 'negative');
}
/**
* @SuppressWarnings(PHPMD.ExcessiveParameterList)
*/
public function sumIncome(
Carbon $start,
Carbon $end,
?Collection $accounts = null,
?Collection $revenue = null,
?TransactionCurrency $currency = null
): array {
$journals = $this->getTransactionsForSum(TransactionType::DEPOSIT, $start, $end, $accounts, $revenue, $currency);
return $this->groupByCurrency($journals, 'positive');
}
/**
* @SuppressWarnings(PHPMD.ExcessiveParameterList)
*/
public function sumIncomeByDestination(
Carbon $start,
Carbon $end,
?Collection $accounts = null,
?Collection $revenue = null,
?TransactionCurrency $currency = null
): array {
$journals = $this->getTransactionsForSum(TransactionType::DEPOSIT, $start, $end, $accounts, $revenue, $currency);
return $this->groupByDirection($journals, 'destination', 'positive');
}
/**
* @SuppressWarnings(PHPMD.ExcessiveParameterList)
*/
public function sumIncomeBySource(
Carbon $start,
Carbon $end,
?Collection $accounts = null,
?Collection $revenue = null,
?TransactionCurrency $currency = null
): array {
$journals = $this->getTransactionsForSum(TransactionType::DEPOSIT, $start, $end, $accounts, $revenue, $currency);
return $this->groupByDirection($journals, 'source', 'positive');
}
public function sumTransfers(Carbon $start, Carbon $end, ?Collection $accounts = null, ?TransactionCurrency $currency = null): array
{
$journals = $this->getTransactionsForSum(TransactionType::TRANSFER, $start, $end, $accounts, null, $currency);
return $this->groupByEither($journals);
}
private function groupByEither(array $journals): array
{
$return = [];

View File

@@ -201,8 +201,8 @@ class AvailableBudgetRepository implements AvailableBudgetRepositoryInterface
$availableBudget = new AvailableBudget();
$availableBudget->user()->associate($this->user);
$availableBudget->transactionCurrency()->associate($currency);
$availableBudget->start_date = $start->startOfDay();
$availableBudget->end_date = $end->endOfDay();
$availableBudget->start_date = $start->startOfDay()->format('Y-m-d'); // @phpstan-ignore-line
$availableBudget->end_date = $end->endOfDay()->format('Y-m-d'); // @phpstan-ignore-line
}
$availableBudget->amount = $amount;
$availableBudget->save();
@@ -234,8 +234,8 @@ class AvailableBudgetRepository implements AvailableBudgetRepositoryInterface
'user_group_id' => $this->user->user_group_id,
'transaction_currency_id' => $data['currency_id'],
'amount' => $data['amount'],
'start_date' => $start,
'end_date' => $end,
'start_date' => $start->format('Y-m-d'),
'end_date' => $end->format('Y-m-d'),
]
);
}
@@ -256,7 +256,7 @@ class AvailableBudgetRepository implements AvailableBudgetRepositoryInterface
$start = $data['start'];
if ($start instanceof Carbon) {
$start = $data['start']->startOfDay();
$availableBudget->start_date = $start;
$availableBudget->start_date = $start->format('Y-m-d');
$availableBudget->save();
}
}
@@ -265,7 +265,7 @@ class AvailableBudgetRepository implements AvailableBudgetRepositoryInterface
$end = $data['end'];
if ($end instanceof Carbon) {
$end = $data['end']->endOfDay();
$availableBudget->end_date = $end;
$availableBudget->end_date = $end->format('Y-m-d');
$availableBudget->save();
}
}

View File

@@ -39,7 +39,6 @@ use FireflyIII\Models\TransactionType;
use FireflyIII\Repositories\Account\AccountRepositoryInterface;
use FireflyIII\Repositories\UserGroups\Currency\CurrencyRepositoryInterface;
use FireflyIII\Services\Internal\Destroy\BudgetDestroyService;
use FireflyIII\Support\Facades\Amount;
use FireflyIII\Support\Http\Api\ExchangeRateConverter;
use FireflyIII\User;
use Illuminate\Contracts\Auth\Authenticatable;
@@ -170,6 +169,40 @@ class BudgetRepository implements BudgetRepositoryInterface
;
}
/**
* How many days of this budget limit are between start and end?
*/
private function daysInOverlap(BudgetLimit $limit, Carbon $start, Carbon $end): int
{
// start1 = $start
// start2 = $limit->start_date
// start1 = $end
// start2 = $limit->end_date
// limit is larger than start and end (inclusive)
// |-----------|
// |----------------|
if ($start->gte($limit->start_date) && $end->lte($limit->end_date)) {
return $start->diffInDays($end) + 1; // add one day
}
// limit starts earlier and limit ends first:
// |-----------|
// |-------|
if ($limit->start_date->lte($start) && $limit->end_date->lte($end)) {
// return days in the range $start-$limit_end
return $start->diffInDays($limit->end_date) + 1; // add one day, the day itself
}
// limit starts later and limit ends earlier
// |-----------|
// |-------|
if ($limit->start_date->gte($start) && $limit->end_date->gte($end)) {
// return days in the range $limit_start - $end
return $limit->start_date->diffInDays($end) + 1; // add one day, the day itself
}
return 0;
}
public function budgetedInPeriodForBudget(Budget $budget, Carbon $start, Carbon $end): array
{
app('log')->debug(sprintf('Now in budgetedInPeriod(#%d, "%s", "%s")', $budget->id, $start->format('Y-m-d'), $end->format('Y-m-d')));
@@ -291,11 +324,115 @@ class BudgetRepository implements BudgetRepositoryInterface
return $budget;
}
private function updateRuleActions(string $oldName, string $newName): void
{
$types = ['set_budget'];
$actions = RuleAction::leftJoin('rules', 'rules.id', '=', 'rule_actions.rule_id')
->where('rules.user_id', $this->user->id)
->whereIn('rule_actions.action_type', $types)
->where('rule_actions.action_value', $oldName)
->get(['rule_actions.*'])
;
app('log')->debug(sprintf('Found %d actions to update.', $actions->count()));
/** @var RuleAction $action */
foreach ($actions as $action) {
$action->action_value = $newName;
$action->save();
app('log')->debug(sprintf('Updated action %d: %s', $action->id, $action->action_value));
}
}
private function updateRuleTriggers(string $oldName, string $newName): void
{
$types = ['budget_is'];
$triggers = RuleTrigger::leftJoin('rules', 'rules.id', '=', 'rule_triggers.rule_id')
->where('rules.user_id', $this->user->id)
->whereIn('rule_triggers.trigger_type', $types)
->where('rule_triggers.trigger_value', $oldName)
->get(['rule_triggers.*'])
;
app('log')->debug(sprintf('Found %d triggers to update.', $triggers->count()));
/** @var RuleTrigger $trigger */
foreach ($triggers as $trigger) {
$trigger->trigger_value = $newName;
$trigger->save();
app('log')->debug(sprintf('Updated trigger %d: %s', $trigger->id, $trigger->trigger_value));
}
}
private function setNoteText(Budget $budget, string $text): void
{
$dbNote = $budget->notes()->first();
if ('' !== $text) {
if (null === $dbNote) {
$dbNote = new Note();
$dbNote->noteable()->associate($budget);
}
$dbNote->text = trim($text);
$dbNote->save();
return;
}
if (null !== $dbNote) {
$dbNote->delete();
}
}
public function getAutoBudget(Budget $budget): ?AutoBudget
{
return $budget->autoBudgets()->first();
}
/**
* @throws FireflyException
*/
private function updateAutoBudget(Budget $budget, array $data): void
{
// update or create auto-budget:
$autoBudget = $this->getAutoBudget($budget);
// grab default currency:
$currency = app('amount')->getDefaultCurrencyByUserGroup($this->user->userGroup);
if (null === $autoBudget) {
// at this point it's a blind assumption auto_budget_type is 1 or 2.
$autoBudget = new AutoBudget();
$autoBudget->auto_budget_type = $data['auto_budget_type'];
$autoBudget->budget_id = $budget->id;
$autoBudget->transaction_currency_id = $currency->id;
}
// set or update the currency.
if (array_key_exists('currency_id', $data) || array_key_exists('currency_code', $data)) {
/** @var CurrencyRepositoryInterface $repos */
$repos = app(CurrencyRepositoryInterface::class);
$currencyId = (int)($data['currency_id'] ?? 0);
$currencyCode = (string)($data['currency_code'] ?? '');
$currency = $repos->find($currencyId);
if (null === $currency) {
$currency = $repos->findByCode($currencyCode);
}
if (null !== $currency) {
$autoBudget->transaction_currency_id = $currency->id;
}
}
// change values if submitted or presented:
if (array_key_exists('auto_budget_type', $data)) {
$autoBudget->auto_budget_type = $data['auto_budget_type'];
}
if (array_key_exists('auto_budget_amount', $data)) {
$autoBudget->amount = $data['auto_budget_amount'];
}
if (array_key_exists('auto_budget_period', $data)) {
$autoBudget->period = $data['auto_budget_period'];
}
$autoBudget->save();
}
/**
* Find a budget or return NULL
*
@@ -676,142 +813,4 @@ class BudgetRepository implements BudgetRepositoryInterface
{
return (int)$this->user->budgets()->max('order');
}
/**
* How many days of this budget limit are between start and end?
*/
private function daysInOverlap(BudgetLimit $limit, Carbon $start, Carbon $end): int
{
// start1 = $start
// start2 = $limit->start_date
// start1 = $end
// start2 = $limit->end_date
// limit is larger than start and end (inclusive)
// |-----------|
// |----------------|
if ($start->gte($limit->start_date) && $end->lte($limit->end_date)) {
return $start->diffInDays($end) + 1; // add one day
}
// limit starts earlier and limit ends first:
// |-----------|
// |-------|
if ($limit->start_date->lte($start) && $limit->end_date->lte($end)) {
// return days in the range $start-$limit_end
return $start->diffInDays($limit->end_date) + 1; // add one day, the day itself
}
// limit starts later and limit ends earlier
// |-----------|
// |-------|
if ($limit->start_date->gte($start) && $limit->end_date->gte($end)) {
// return days in the range $limit_start - $end
return $limit->start_date->diffInDays($end) + 1; // add one day, the day itself
}
return 0;
}
private function updateRuleActions(string $oldName, string $newName): void
{
$types = ['set_budget'];
$actions = RuleAction::leftJoin('rules', 'rules.id', '=', 'rule_actions.rule_id')
->where('rules.user_id', $this->user->id)
->whereIn('rule_actions.action_type', $types)
->where('rule_actions.action_value', $oldName)
->get(['rule_actions.*'])
;
app('log')->debug(sprintf('Found %d actions to update.', $actions->count()));
/** @var RuleAction $action */
foreach ($actions as $action) {
$action->action_value = $newName;
$action->save();
app('log')->debug(sprintf('Updated action %d: %s', $action->id, $action->action_value));
}
}
private function updateRuleTriggers(string $oldName, string $newName): void
{
$types = ['budget_is'];
$triggers = RuleTrigger::leftJoin('rules', 'rules.id', '=', 'rule_triggers.rule_id')
->where('rules.user_id', $this->user->id)
->whereIn('rule_triggers.trigger_type', $types)
->where('rule_triggers.trigger_value', $oldName)
->get(['rule_triggers.*'])
;
app('log')->debug(sprintf('Found %d triggers to update.', $triggers->count()));
/** @var RuleTrigger $trigger */
foreach ($triggers as $trigger) {
$trigger->trigger_value = $newName;
$trigger->save();
app('log')->debug(sprintf('Updated trigger %d: %s', $trigger->id, $trigger->trigger_value));
}
}
private function setNoteText(Budget $budget, string $text): void
{
$dbNote = $budget->notes()->first();
if ('' !== $text) {
if (null === $dbNote) {
$dbNote = new Note();
$dbNote->noteable()->associate($budget);
}
$dbNote->text = trim($text);
$dbNote->save();
return;
}
if (null !== $dbNote) {
$dbNote->delete();
}
}
/**
* @throws FireflyException
*/
private function updateAutoBudget(Budget $budget, array $data): void
{
// update or create auto-budget:
$autoBudget = $this->getAutoBudget($budget);
// grab default currency:
$currency = app('amount')->getDefaultCurrencyByUserGroup($this->user->userGroup);
if (null === $autoBudget) {
// at this point it's a blind assumption auto_budget_type is 1 or 2.
$autoBudget = new AutoBudget();
$autoBudget->auto_budget_type = $data['auto_budget_type'];
$autoBudget->budget_id = $budget->id;
$autoBudget->transaction_currency_id = $currency->id;
}
// set or update the currency.
if (array_key_exists('currency_id', $data) || array_key_exists('currency_code', $data)) {
/** @var CurrencyRepositoryInterface $repos */
$repos = app(CurrencyRepositoryInterface::class);
$currencyId = (int) ($data['currency_id'] ?? 0);
$currencyCode = (string) ($data['currency_code'] ?? '');
$currency = $repos->find($currencyId);
if (null === $currency) {
$currency = $repos->findByCode($currencyCode);
}
if (null !== $currency) {
$autoBudget->transaction_currency_id = $currency->id;
}
}
// change values if submitted or presented:
if (array_key_exists('auto_budget_type', $data)) {
$autoBudget->auto_budget_type = $data['auto_budget_type'];
}
if (array_key_exists('auto_budget_amount', $data)) {
$autoBudget->amount = $data['auto_budget_amount'];
}
if (array_key_exists('auto_budget_period', $data)) {
$autoBudget->period = $data['auto_budget_period'];
}
$autoBudget->save();
}
}

View File

@@ -189,6 +189,14 @@ class OperationsRepository implements OperationsRepositoryInterface
}
}
private function getBudgets(): Collection
{
/** @var BudgetRepositoryInterface $repos */
$repos = app(BudgetRepositoryInterface::class);
return $repos->getActiveBudgets();
}
/**
* @SuppressWarnings(PHPMD.ExcessiveParameterList)
*/
@@ -288,12 +296,4 @@ class OperationsRepository implements OperationsRepositoryInterface
return $array;
}
private function getBudgets(): Collection
{
/** @var BudgetRepositoryInterface $repos */
$repos = app(BudgetRepositoryInterface::class);
return $repos->getActiveBudgets();
}
}

View File

@@ -210,6 +210,34 @@ class CategoryRepository implements CategoryRepositoryInterface
return $firstJournalDate;
}
private function getFirstJournalDate(Category $category): ?Carbon
{
$query = $category->transactionJournals()->orderBy('date', 'ASC');
$result = $query->first(['transaction_journals.*']);
if (null !== $result) {
return $result->date;
}
return null;
}
private function getFirstTransactionDate(Category $category): ?Carbon
{
// check transactions:
$query = $category->transactions()
->leftJoin('transaction_journals', 'transaction_journals.id', '=', 'transactions.transaction_journal_id')
->orderBy('transaction_journals.date', 'ASC')
;
$lastTransaction = $query->first(['transaction_journals.*']);
if (null !== $lastTransaction) {
return new Carbon($lastTransaction->date);
}
return null;
}
public function getAttachments(Category $category): Collection
{
$set = $category->attachments()->get();
@@ -271,56 +299,6 @@ class CategoryRepository implements CategoryRepositoryInterface
return $lastJournalDate;
}
public function searchCategory(string $query, int $limit): Collection
{
$search = $this->user->categories();
if ('' !== $query) {
$search->where('name', 'LIKE', sprintf('%%%s%%', $query));
}
return $search->take($limit)->get();
}
/**
* @throws \Exception
*/
public function update(Category $category, array $data): Category
{
/** @var CategoryUpdateService $service */
$service = app(CategoryUpdateService::class);
$service->setUser($this->user);
return $service->update($category, $data);
}
private function getFirstJournalDate(Category $category): ?Carbon
{
$query = $category->transactionJournals()->orderBy('date', 'ASC');
$result = $query->first(['transaction_journals.*']);
if (null !== $result) {
return $result->date;
}
return null;
}
private function getFirstTransactionDate(Category $category): ?Carbon
{
// check transactions:
$query = $category->transactions()
->leftJoin('transaction_journals', 'transaction_journals.id', '=', 'transactions.transaction_journal_id')
->orderBy('transaction_journals.date', 'ASC')
;
$lastTransaction = $query->first(['transaction_journals.*']);
if (null !== $lastTransaction) {
return new Carbon($lastTransaction->date);
}
return null;
}
private function getLastJournalDate(Category $category, Collection $accounts): ?Carbon
{
$query = $category->transactionJournals()->orderBy('date', 'DESC');
@@ -361,4 +339,26 @@ class CategoryRepository implements CategoryRepositoryInterface
return null;
}
public function searchCategory(string $query, int $limit): Collection
{
$search = $this->user->categories();
if ('' !== $query) {
$search->where('name', 'LIKE', sprintf('%%%s%%', $query));
}
return $search->take($limit)->get();
}
/**
* @throws \Exception
*/
public function update(Category $category, array $data): Category
{
/** @var CategoryUpdateService $service */
$service = app(CategoryUpdateService::class);
$service->setUser($this->user);
return $service->update($category, $data);
}
}

View File

@@ -115,6 +115,14 @@ class OperationsRepository implements OperationsRepositoryInterface
}
}
/**
* Returns a list of all the categories belonging to a user.
*/
private function getCategories(): Collection
{
return $this->user->categories()->get();
}
/**
* This method returns a list of all the deposit transaction journals (as arrays) set in that period
* which have the specified category set to them. It's grouped per currency, with as few details in the array
@@ -420,12 +428,4 @@ class OperationsRepository implements OperationsRepositoryInterface
return $array;
}
/**
* Returns a list of all the categories belonging to a user.
*/
private function getCategories(): Collection
{
return $this->user->categories()->get();
}
}

View File

@@ -245,6 +245,25 @@ class LinkTypeRepository implements LinkTypeRepositoryInterface
;
}
/**
* @throws \Exception
*/
private function setNoteText(TransactionJournalLink $link, string $text): void
{
$dbNote = $link->notes()->first();
if ('' !== $text) {
if (null === $dbNote) {
$dbNote = new Note();
$dbNote->noteable()->associate($link);
}
$dbNote->text = trim($text);
$dbNote->save();
return;
}
$dbNote?->delete();
}
public function switchLinkById(int $linkId): bool
{
/** @var null|TransactionJournalLink $link */
@@ -293,23 +312,4 @@ class LinkTypeRepository implements LinkTypeRepositoryInterface
return $journalLink;
}
/**
* @throws \Exception
*/
private function setNoteText(TransactionJournalLink $link, string $text): void
{
$dbNote = $link->notes()->first();
if ('' !== $text) {
if (null === $dbNote) {
$dbNote = new Note();
$dbNote->noteable()->associate($link);
}
$dbNote->text = trim($text);
$dbNote->save();
return;
}
$dbNote?->delete();
}
}

View File

@@ -279,6 +279,25 @@ trait ModifiesPiggyBanks
return true;
}
private function updateNote(PiggyBank $piggyBank, string $note): bool
{
if ('' === $note) {
$dbNote = $piggyBank->notes()->first();
$dbNote?->delete();
return true;
}
$dbNote = $piggyBank->notes()->first();
if (null === $dbNote) {
$dbNote = new Note();
$dbNote->noteable()->associate($piggyBank);
}
$dbNote->text = trim($note);
$dbNote->save();
return true;
}
public function update(PiggyBank $piggyBank, array $data): PiggyBank
{
$piggyBank = $this->updateProperties($piggyBank, $data);
@@ -339,25 +358,6 @@ trait ModifiesPiggyBanks
return $piggyBank;
}
private function updateNote(PiggyBank $piggyBank, string $note): bool
{
if ('' === $note) {
$dbNote = $piggyBank->notes()->first();
$dbNote?->delete();
return true;
}
$dbNote = $piggyBank->notes()->first();
if (null === $dbNote) {
$dbNote = new Note();
$dbNote->noteable()->associate($piggyBank);
}
$dbNote->text = trim($note);
$dbNote->save();
return true;
}
private function updateProperties(PiggyBank $piggyBank, array $data): PiggyBank
{
if (array_key_exists('name', $data) && '' !== $data['name']) {

View File

@@ -400,6 +400,21 @@ class RecurringRepository implements RecurringRepositoryInterface
return $this->filterMaxDate($repeatUntil, $occurrences);
}
private function filterMaxDate(?Carbon $max, array $occurrences): array
{
if (null === $max) {
return $occurrences;
}
$filtered = [];
foreach ($occurrences as $date) {
if ($date->lte($max)) {
$filtered[] = $date;
}
}
return $filtered;
}
/**
* Parse the repetition in a string that is user readable.
*
@@ -555,19 +570,4 @@ class RecurringRepository implements RecurringRepositoryInterface
return $service->update($recurrence, $data);
}
private function filterMaxDate(?Carbon $max, array $occurrences): array
{
if (null === $max) {
return $occurrences;
}
$filtered = [];
foreach ($occurrences as $date) {
if ($date->lte($max)) {
$filtered[] = $date;
}
}
return $filtered;
}
}

View File

@@ -280,6 +280,26 @@ class RuleRepository implements RuleRepositoryInterface
return $this->user->rules()->find($ruleId);
}
private function setRuleTrigger(string $moment, Rule $rule): void
{
/** @var null|RuleTrigger $trigger */
$trigger = $rule->ruleTriggers()->where('trigger_type', 'user_action')->first();
if (null !== $trigger) {
$trigger->trigger_value = $moment;
$trigger->save();
return;
}
$trigger = new RuleTrigger();
$trigger->order = 0;
$trigger->trigger_type = 'user_action';
$trigger->trigger_value = $moment;
$trigger->rule_id = $rule->id;
$trigger->active = true;
$trigger->stop_processing = false;
$trigger->save();
}
public function resetRuleOrder(RuleGroup $ruleGroup): bool
{
$groupRepository = app(RuleGroupRepositoryInterface::class);
@@ -336,6 +356,59 @@ class RuleRepository implements RuleRepositoryInterface
return (int)$ruleGroup->rules()->max('order');
}
private function storeTriggers(Rule $rule, array $data): void
{
$order = 1;
foreach ($data['triggers'] as $trigger) {
$value = $trigger['value'] ?? '';
$stopProcessing = $trigger['stop_processing'] ?? false;
$active = $trigger['active'] ?? true;
$type = $trigger['type'];
if (true === ($trigger['prohibited'] ?? false) && !str_starts_with($type, '-')) {
$type = sprintf('-%s', $type);
}
// empty the value in case the rule needs no context
// TODO create a helper to automatically return these.
$needTrue = [
'reconciled',
'has_attachments',
'has_any_category',
'has_any_budget',
'has_any_bill',
'has_any_tag',
'any_notes',
'any_external_url',
'has_no_attachments',
'has_no_category',
'has_no_budget',
'has_no_bill',
'has_no_tag',
'no_notes',
'no_external_url',
'source_is_cash',
'destination_is_cash',
'account_is_cash',
'exists',
'no_external_id',
'any_external_id',
];
if (in_array($type, $needTrue, true)) {
$value = '';
}
$triggerValues = [
'action' => $type,
'value' => $value,
'stop_processing' => $stopProcessing,
'order' => $order,
'active' => $active,
];
$this->storeTrigger($rule, $triggerValues);
++$order;
}
}
public function storeTrigger(Rule $rule, array $values): RuleTrigger
{
$ruleTrigger = new RuleTrigger();
@@ -350,6 +423,25 @@ class RuleRepository implements RuleRepositoryInterface
return $ruleTrigger;
}
private function storeActions(Rule $rule, array $data): void
{
$order = 1;
foreach ($data['actions'] as $action) {
$value = $action['value'] ?? '';
$stopProcessing = $action['stop_processing'] ?? false;
$active = $action['active'] ?? true;
$actionValues = [
'action' => $action['type'],
'value' => $value,
'stop_processing' => $stopProcessing,
'order' => $order,
'active' => $active,
];
$this->storeAction($rule, $actionValues);
++$order;
}
}
public function storeAction(Rule $rule, array $values): RuleAction
{
$ruleAction = new RuleAction();
@@ -426,96 +518,4 @@ class RuleRepository implements RuleRepositoryInterface
return $rule;
}
private function setRuleTrigger(string $moment, Rule $rule): void
{
/** @var null|RuleTrigger $trigger */
$trigger = $rule->ruleTriggers()->where('trigger_type', 'user_action')->first();
if (null !== $trigger) {
$trigger->trigger_value = $moment;
$trigger->save();
return;
}
$trigger = new RuleTrigger();
$trigger->order = 0;
$trigger->trigger_type = 'user_action';
$trigger->trigger_value = $moment;
$trigger->rule_id = $rule->id;
$trigger->active = true;
$trigger->stop_processing = false;
$trigger->save();
}
private function storeTriggers(Rule $rule, array $data): void
{
$order = 1;
foreach ($data['triggers'] as $trigger) {
$value = $trigger['value'] ?? '';
$stopProcessing = $trigger['stop_processing'] ?? false;
$active = $trigger['active'] ?? true;
$type = $trigger['type'];
if (true === ($trigger['prohibited'] ?? false) && !str_starts_with($type, '-')) {
$type = sprintf('-%s', $type);
}
// empty the value in case the rule needs no context
// TODO create a helper to automatically return these.
$needTrue = [
'reconciled',
'has_attachments',
'has_any_category',
'has_any_budget',
'has_any_bill',
'has_any_tag',
'any_notes',
'any_external_url',
'has_no_attachments',
'has_no_category',
'has_no_budget',
'has_no_bill',
'has_no_tag',
'no_notes',
'no_external_url',
'source_is_cash',
'destination_is_cash',
'account_is_cash',
'exists',
'no_external_id',
'any_external_id',
];
if(in_array($type, $needTrue, true)) {
$value = '';
}
$triggerValues = [
'action' => $type,
'value' => $value,
'stop_processing' => $stopProcessing,
'order' => $order,
'active' => $active,
];
$this->storeTrigger($rule, $triggerValues);
++$order;
}
}
private function storeActions(Rule $rule, array $data): void
{
$order = 1;
foreach ($data['actions'] as $action) {
$value = $action['value'] ?? '';
$stopProcessing = $action['stop_processing'] ?? false;
$active = $action['active'] ?? true;
$actionValues = [
'action' => $action['type'],
'value' => $value,
'stop_processing' => $stopProcessing,
'order' => $order,
'active' => $active,
];
$this->storeAction($rule, $actionValues);
++$order;
}
}
}

View File

@@ -151,6 +151,49 @@ class RuleGroupRepository implements RuleGroupRepositoryInterface
return true;
}
private function resetRuleActionOrder(Rule $rule): void
{
$actions = $rule->ruleActions()
->orderBy('order', 'ASC')
->orderBy('active', 'DESC')
->orderBy('action_type', 'ASC')
->get()
;
$index = 1;
/** @var RuleAction $action */
foreach ($actions as $action) {
if ($action->order !== $index) {
$action->order = $index;
$action->save();
app('log')->debug(sprintf('Rule action #%d was on spot %d but must be on spot %d', $action->id, $action->order, $index));
}
++$index;
}
}
private function resetRuleTriggerOrder(Rule $rule): void
{
$triggers = $rule->ruleTriggers()
->orderBy('order', 'ASC')
->orderBy('active', 'DESC')
->orderBy('trigger_type', 'ASC')
->get()
;
$index = 1;
/** @var RuleTrigger $trigger */
foreach ($triggers as $trigger) {
$order = $trigger->order;
if ($order !== $index) {
$trigger->order = $index;
$trigger->save();
app('log')->debug(sprintf('Rule trigger #%d was on spot %d but must be on spot %d', $trigger->id, $order, $index));
}
++$index;
}
}
public function destroyAll(): void
{
Log::channel('audit')->info('Delete all rule groups through destroyAll');
@@ -412,47 +455,4 @@ class RuleGroupRepository implements RuleGroupRepositoryInterface
return $ruleGroup;
}
private function resetRuleActionOrder(Rule $rule): void
{
$actions = $rule->ruleActions()
->orderBy('order', 'ASC')
->orderBy('active', 'DESC')
->orderBy('action_type', 'ASC')
->get()
;
$index = 1;
/** @var RuleAction $action */
foreach ($actions as $action) {
if ($action->order !== $index) {
$action->order = $index;
$action->save();
app('log')->debug(sprintf('Rule action #%d was on spot %d but must be on spot %d', $action->id, $action->order, $index));
}
++$index;
}
}
private function resetRuleTriggerOrder(Rule $rule): void
{
$triggers = $rule->ruleTriggers()
->orderBy('order', 'ASC')
->orderBy('active', 'DESC')
->orderBy('trigger_type', 'ASC')
->get()
;
$index = 1;
/** @var RuleTrigger $trigger */
foreach ($triggers as $trigger) {
$order = $trigger->order;
if ($order !== $index) {
$trigger->order = $index;
$trigger->save();
app('log')->debug(sprintf('Rule trigger #%d was on spot %d but must be on spot %d', $trigger->id, $order, $index));
}
++$index;
}
}
}

View File

@@ -120,6 +120,14 @@ class OperationsRepository implements OperationsRepositoryInterface
}
}
private function getTags(): Collection
{
/** @var TagRepositoryInterface $repository */
$repository = app(TagRepositoryInterface::class);
return $repository->get();
}
/**
* This method returns a list of all the deposit transaction journals (as arrays) set in that period
* which have the specified tag(s) set to them. It's grouped per currency, with as few details in the array
@@ -215,12 +223,4 @@ class OperationsRepository implements OperationsRepositoryInterface
{
throw new FireflyException(sprintf('%s is not yet implemented.', __METHOD__));
}
private function getTags(): Collection
{
/** @var TagRepositoryInterface $repository */
$repository = app(TagRepositoryInterface::class);
return $repository->get();
}
}

View File

@@ -90,6 +90,46 @@ class TransactionGroupRepository implements TransactionGroupRepositoryInterface
return $result;
}
private function expandJournal(TransactionJournal $journal): array
{
$array = $journal->toArray();
$array['transactions'] = [];
$array['meta'] = $journal->transactionJournalMeta->toArray();
$array['tags'] = $journal->tags->toArray();
$array['categories'] = $journal->categories->toArray();
$array['budgets'] = $journal->budgets->toArray();
$array['notes'] = $journal->notes->toArray();
$array['locations'] = [];
$array['attachments'] = $journal->attachments->toArray();
$array['links'] = [];
$array['piggy_bank_events'] = $journal->piggyBankEvents->toArray();
/** @var Transaction $transaction */
foreach ($journal->transactions as $transaction) {
$array['transactions'][] = $this->expandTransaction($transaction);
}
return $array;
}
private function expandTransaction(Transaction $transaction): array
{
$array = $transaction->toArray();
$array['account'] = $transaction->account->toArray();
$array['budgets'] = [];
$array['categories'] = [];
foreach ($transaction->categories as $category) {
$array['categories'][] = $category->toArray();
}
foreach ($transaction->budgets as $budget) {
$array['budgets'][] = $budget->toArray();
}
return $array;
}
/**
* Return all attachments for all journals in the group.
*/
@@ -201,6 +241,48 @@ class TransactionGroupRepository implements TransactionGroupRepositoryInterface
return $return;
}
private function getFormattedAmount(TransactionJournal $journal): string
{
/** @var Transaction $transaction */
$transaction = $journal->transactions->first();
$currency = $transaction->transactionCurrency;
$type = $journal->transactionType->type;
$amount = app('steam')->positive($transaction->amount);
$return = '';
if (TransactionType::WITHDRAWAL === $type) {
$return = app('amount')->formatAnything($currency, app('steam')->negative($amount));
}
if (TransactionType::WITHDRAWAL !== $type) {
$return = app('amount')->formatAnything($currency, $amount);
}
return $return;
}
private function getFormattedForeignAmount(TransactionJournal $journal): string
{
/** @var Transaction $transaction */
$transaction = $journal->transactions->first();
if (null === $transaction->foreign_amount || '' === $transaction->foreign_amount) {
return '';
}
if (0 === bccomp('0', $transaction->foreign_amount)) {
return '';
}
$currency = $transaction->foreignCurrency;
$type = $journal->transactionType->type;
$amount = app('steam')->positive($transaction->foreign_amount);
$return = '';
if (TransactionType::WITHDRAWAL === $type) {
$return = app('amount')->formatAnything($currency, app('steam')->negative($amount));
}
if (TransactionType::WITHDRAWAL !== $type) {
$return = app('amount')->formatAnything($currency, $amount);
}
return $return;
}
public function getLocation(int $journalId): ?Location
{
/** @var TransactionJournal $journal */
@@ -351,86 +433,4 @@ class TransactionGroupRepository implements TransactionGroupRepositoryInterface
return $service->update($transactionGroup, $data);
}
private function expandJournal(TransactionJournal $journal): array
{
$array = $journal->toArray();
$array['transactions'] = [];
$array['meta'] = $journal->transactionJournalMeta->toArray();
$array['tags'] = $journal->tags->toArray();
$array['categories'] = $journal->categories->toArray();
$array['budgets'] = $journal->budgets->toArray();
$array['notes'] = $journal->notes->toArray();
$array['locations'] = [];
$array['attachments'] = $journal->attachments->toArray();
$array['links'] = [];
$array['piggy_bank_events'] = $journal->piggyBankEvents->toArray();
/** @var Transaction $transaction */
foreach ($journal->transactions as $transaction) {
$array['transactions'][] = $this->expandTransaction($transaction);
}
return $array;
}
private function expandTransaction(Transaction $transaction): array
{
$array = $transaction->toArray();
$array['account'] = $transaction->account->toArray();
$array['budgets'] = [];
$array['categories'] = [];
foreach ($transaction->categories as $category) {
$array['categories'][] = $category->toArray();
}
foreach ($transaction->budgets as $budget) {
$array['budgets'][] = $budget->toArray();
}
return $array;
}
private function getFormattedAmount(TransactionJournal $journal): string
{
/** @var Transaction $transaction */
$transaction = $journal->transactions->first();
$currency = $transaction->transactionCurrency;
$type = $journal->transactionType->type;
$amount = app('steam')->positive($transaction->amount);
$return = '';
if (TransactionType::WITHDRAWAL === $type) {
$return = app('amount')->formatAnything($currency, app('steam')->negative($amount));
}
if (TransactionType::WITHDRAWAL !== $type) {
$return = app('amount')->formatAnything($currency, $amount);
}
return $return;
}
private function getFormattedForeignAmount(TransactionJournal $journal): string
{
/** @var Transaction $transaction */
$transaction = $journal->transactions->first();
if (null === $transaction->foreign_amount || '' === $transaction->foreign_amount) {
return '';
}
if (0 === bccomp('0', $transaction->foreign_amount)) {
return '';
}
$currency = $transaction->foreignCurrency;
$type = $journal->transactionType->type;
$amount = app('steam')->positive($transaction->foreign_amount);
$return = '';
if (TransactionType::WITHDRAWAL === $type) {
$return = app('amount')->formatAnything($currency, app('steam')->negative($amount));
}
if (TransactionType::WITHDRAWAL !== $type) {
$return = app('amount')->formatAnything($currency, $amount);
}
return $return;
}
}

View File

@@ -112,6 +112,34 @@ class UserGroupRepository implements UserGroupRepositoryInterface
return $collection;
}
/**
* Because there is the chance that a group with this name already exists,
* Firefly III runs a little loop of combinations to make sure the group name is unique.
*/
private function createNewUserGroup(User $user): UserGroup
{
$loop = 0;
$groupName = $user->email;
$exists = true;
$existingGroup = null;
while ($exists && $loop < 10) {
$existingGroup = $this->findByName($groupName);
if (null === $existingGroup) {
$exists = false;
/** @var null|UserGroup $existingGroup */
$existingGroup = $this->store(['user' => $user, 'title' => $groupName]);
}
if (null !== $existingGroup) {
// group already exists
$groupName = sprintf('%s-%s', $user->email, substr(sha1(rand(1000, 9999).microtime()), 0, 4));
}
++$loop;
}
return $existingGroup;
}
public function findByName(string $title): ?UserGroup
{
return UserGroup::whereTitle($title)->first();
@@ -239,34 +267,6 @@ class UserGroupRepository implements UserGroupRepositoryInterface
return $userGroup;
}
/**
* Because there is the chance that a group with this name already exists,
* Firefly III runs a little loop of combinations to make sure the group name is unique.
*/
private function createNewUserGroup(User $user): UserGroup
{
$loop = 0;
$groupName = $user->email;
$exists = true;
$existingGroup = null;
while ($exists && $loop < 10) {
$existingGroup = $this->findByName($groupName);
if (null === $existingGroup) {
$exists = false;
/** @var null|UserGroup $existingGroup */
$existingGroup = $this->store(['user' => $user, 'title' => $groupName]);
}
if (null !== $existingGroup) {
// group already exists
$groupName = sprintf('%s-%s', $user->email, substr(sha1(rand(1000, 9999).microtime()), 0, 4));
}
++$loop;
}
return $existingGroup;
}
private function simplifyListByName(array $roles): array
{
if (in_array(UserRoleEnum::OWNER->value, $roles, true)) {

View File

@@ -150,6 +150,14 @@ class CurrencyRepository implements CurrencyRepositoryInterface
return null;
}
private function countJournals(TransactionCurrency $currency): int
{
$count = $currency->transactions()->whereNull('deleted_at')->count() + $currency->transactionJournals()->whereNull('deleted_at')->count();
// also count foreign:
return $count + Transaction::where('foreign_currency_id', $currency->id)->count();
}
/**
* Returns ALL currencies, regardless of whether they are enabled or not.
*/
@@ -372,12 +380,4 @@ class CurrencyRepository implements CurrencyRepositoryInterface
return $service->update($currency, $data);
}
private function countJournals(TransactionCurrency $currency): int
{
$count = $currency->transactions()->whereNull('deleted_at')->count() + $currency->transactionJournals()->whereNull('deleted_at')->count();
// also count foreign:
return $count + Transaction::where('foreign_currency_id', $currency->id)->count();
}
}

View File

@@ -65,32 +65,6 @@ class BelongsUser implements ValidationRule
}
}
protected function countField(string $class, string $field, string $value): int
{
$value = trim($value);
$objects = [];
// get all objects belonging to user:
if (PiggyBank::class === $class) {
$objects = PiggyBank::leftJoin('accounts', 'accounts.id', '=', 'piggy_banks.account_id')
->where('accounts.user_id', '=', auth()->user()->id)->get(['piggy_banks.*'])
;
}
if (PiggyBank::class !== $class) {
$objects = $class::where('user_id', '=', auth()->user()->id)->get();
}
$count = 0;
foreach ($objects as $object) {
$objectValue = trim((string)$object->{$field}); // @phpstan-ignore-line
app('log')->debug(sprintf('Comparing object "%s" with value "%s"', $objectValue, $value));
if ($objectValue === $value) {
++$count;
app('log')->debug(sprintf('Hit! Count is now %d', $count));
}
}
return $count;
}
private function parseAttribute(string $attribute): string
{
$parts = explode('.', $attribute);
@@ -121,6 +95,32 @@ class BelongsUser implements ValidationRule
return 1 === $count;
}
protected function countField(string $class, string $field, string $value): int
{
$value = trim($value);
$objects = [];
// get all objects belonging to user:
if (PiggyBank::class === $class) {
$objects = PiggyBank::leftJoin('accounts', 'accounts.id', '=', 'piggy_banks.account_id')
->where('accounts.user_id', '=', auth()->user()->id)->get(['piggy_banks.*'])
;
}
if (PiggyBank::class !== $class) {
$objects = $class::where('user_id', '=', auth()->user()->id)->get();
}
$count = 0;
foreach ($objects as $object) {
$objectValue = trim((string)$object->{$field}); // @phpstan-ignore-line
app('log')->debug(sprintf('Comparing object "%s" with value "%s"', $objectValue, $value));
if ($objectValue === $value) {
++$count;
app('log')->debug(sprintf('Hit! Count is now %d', $count));
}
}
return $count;
}
private function validateBillId(int $value): bool
{
if (0 === $value) {

View File

@@ -78,32 +78,6 @@ class BelongsUserGroup implements ValidationRule
}
}
protected function countField(string $class, string $field, string $value): int
{
$value = trim($value);
$objects = [];
// get all objects belonging to user:
if (PiggyBank::class === $class) {
$objects = PiggyBank::leftJoin('accounts', 'accounts.id', '=', 'piggy_banks.account_id')
->where('accounts.user_group_id', '=', $this->userGroup->id)->get(['piggy_banks.*'])
;
}
if (PiggyBank::class !== $class) {
$objects = $class::where('user_group_id', '=', $this->userGroup->id)->get();
}
$count = 0;
foreach ($objects as $object) {
$objectValue = trim((string)$object->{$field}); // @phpstan-ignore-line
app('log')->debug(sprintf('Comparing object "%s" with value "%s"', $objectValue, $value));
if ($objectValue === $value) {
++$count;
app('log')->debug(sprintf('Hit! Count is now %d', $count));
}
}
return $count;
}
private function parseAttribute(string $attribute): string
{
$parts = explode('.', $attribute);
@@ -134,6 +108,32 @@ class BelongsUserGroup implements ValidationRule
return 1 === $count;
}
protected function countField(string $class, string $field, string $value): int
{
$value = trim($value);
$objects = [];
// get all objects belonging to user:
if (PiggyBank::class === $class) {
$objects = PiggyBank::leftJoin('accounts', 'accounts.id', '=', 'piggy_banks.account_id')
->where('accounts.user_group_id', '=', $this->userGroup->id)->get(['piggy_banks.*'])
;
}
if (PiggyBank::class !== $class) {
$objects = $class::where('user_group_id', '=', $this->userGroup->id)->get();
}
$count = 0;
foreach ($objects as $object) {
$objectValue = trim((string)$object->{$field}); // @phpstan-ignore-line
app('log')->debug(sprintf('Comparing object "%s" with value "%s"', $objectValue, $value));
if ($objectValue === $value) {
++$count;
app('log')->debug(sprintf('Hit! Count is now %d', $count));
}
}
return $count;
}
private function validateBillId(int $value): bool
{
if (0 === $value) {

View File

@@ -57,6 +57,15 @@ class IsValidAttachmentModel implements ValidationRule
$this->model = $model;
}
private function normalizeModel(string $model): string
{
$search = ['FireflyIII\Models\\'];
$replace = '';
$model = str_replace($search, $replace, $model);
return sprintf('FireflyIII\Models\%s', $model);
}
/**
* @SuppressWarnings(PHPMD.UnusedFormalParameter)
*/
@@ -84,15 +93,6 @@ class IsValidAttachmentModel implements ValidationRule
}
}
private function normalizeModel(string $model): string
{
$search = ['FireflyIII\Models\\'];
$replace = '';
$model = str_replace($search, $replace, $model);
return sprintf('FireflyIII\Models\%s', $model);
}
private function validateAccount(int $value): bool
{
/** @var AccountRepositoryInterface $repository */

View File

@@ -28,6 +28,7 @@ use Carbon\Carbon;
use FireflyIII\Events\NewVersionAvailable;
use GuzzleHttp\Client;
use GuzzleHttp\Exception\GuzzleException;
use Illuminate\Support\Facades\Log;
/**
* Class UpdateRequest
@@ -42,7 +43,7 @@ class UpdateRequest implements UpdateRequestInterface
'message' => (string)trans('firefly.unknown_error'),
];
// try get array from update server:
// try to get array from update server:
$updateInfo = $this->contactServer($channel);
if ('error' === $updateInfo['level']) {
app('log')->error('Update information contains an error.');
@@ -128,20 +129,22 @@ class UpdateRequest implements UpdateRequestInterface
return $return;
}
/**
* TODO make shorter
*/
private function parseResult(array $information): array
{
app('log')->debug('Now in parseResult()', $information);
$return = [
'level' => 'error',
'message' => (string)trans('firefly.unknown_error'),
];
$current = config('firefly.version');
$latest = $information['version'];
$current = (string)config('firefly.version');
$latest = (string)$information['version'];
// strip the 'v' from the version if it's there.
if (str_starts_with($latest, 'v')) {
$latest = substr($latest, 1);
}
if (str_starts_with($current, 'develop')) {
return $this->parseResultDevelop($current, $latest, $information);
}
$compare = version_compare($latest, $current);
@@ -149,73 +152,129 @@ class UpdateRequest implements UpdateRequestInterface
// -1: you're running a newer version:
if (-1 === $compare) {
$return['level'] = 'info';
$return['message'] = (string)trans('firefly.update_newer_version_alert', ['your_version' => $current, 'new_version' => $latest]);
app('log')->debug('User is running a newer version', $return);
return $return;
return $this->runsNewerVersion($current, $latest);
}
// running the current version:
if (0 === $compare) {
$return['level'] = 'info';
$return['message'] = (string)trans('firefly.update_current_version_alert', ['version' => $current]);
app('log')->debug('User is the current version.', $return);
return $return;
return $this->runsSameVersion($current);
}
// a newer version is available!
/** @var Carbon $released */
$released = $information['date'];
$today = today(config('app.timezone'))->startOfDay();
$diff = $today->diffInDays($released);
$expectedDiff = config('firefly.update_minimum_age') ?? 6;
// it's still very fresh, and user wants a stable release:
if ($diff <= $expectedDiff) {
$isBeta = $information['is_beta'] ?? false;
$isAlpha = $information['is_alpha'] ?? false;
// it's new but alpha:
if (true === $isAlpha) {
return $this->releasedNewAlpha($current, $latest, $released);
}
if (true === $isBeta) {
return $this->releasedNewBeta($current, $latest, $released);
}
return $this->releasedNewVersion($current, $latest, $released);
}
private function parseResultDevelop(string $current, string $latest, array $information): array
{
Log::debug(sprintf('User is running develop version "%s"', $current));
$parts = explode('/', $current);
$return = [];
/** @var Carbon $devDate */
$devDate = Carbon::createFromFormat('Y-m-d', $parts[1]);
if ($devDate->lte($information['date'])) {
Log::debug(sprintf('This development release is older, release = %s, latest version %s = %s', $devDate->format('Y-m-d'), $latest, $information['date']->format('Y-m-d')));
$return['level'] = 'info';
$return['message'] = (string)trans(
'firefly.just_new_release',
[
'version' => $latest,
'date' => $released->isoFormat((string)trans('config.month_and_day_js')),
'days' => $expectedDiff,
]
);
app('log')->debug('Release is very fresh.', $return);
$return['message'] = (string)trans('firefly.update_current_dev_older', ['version' => $current, 'new_version' => $latest]);
return $return;
}
Log::debug(sprintf('This development release is newer, release = %s, latest version %s = %s', $devDate->format('Y-m-d'), $latest, $information['date']->format('Y-m-d')));
$return['level'] = 'info';
$return['message'] = (string)trans('firefly.update_current_dev_newer', ['version' => $current, 'new_version' => $latest]);
return $return;
}
// it's been around for a while:
$return['level'] = 'success';
$return['message'] = (string)trans(
private function runsNewerVersion(string $current, string $latest): array
{
$return = [
'level' => 'info',
'message' => (string)trans('firefly.update_newer_version_alert', ['your_version' => $current, 'new_version' => $latest]),
];
app('log')->debug('User is running a newer version', $return);
return $return;
}
private function runsSameVersion(string $current): array
{
$return = [
'level' => 'info',
'message' => (string)trans('firefly.update_current_version_alert', ['version' => $current]),
];
app('log')->debug('User is the current version.', $return);
return $return;
}
private function releasedNewAlpha(string $current, string $latest, Carbon $date): array
{
app('log')->debug('New release is also a alpha!');
$message = (string)trans(
'firefly.update_new_version_alert',
[
'your_version' => $current,
'new_version' => $latest,
'date' => $released->isoFormat((string)trans('config.month_and_day_js')),
'date' => $date->isoFormat((string)trans('config.month_and_day_js')),
]
);
app('log')->debug('New release is old enough.');
// add warning in case of alpha or beta:
// append warning if beta or alpha.
$isBeta = $information['is_beta'] ?? false;
if (true === $isBeta) {
$return['message'] = sprintf('%s %s', $return['message'], trans('firefly.update_version_beta'));
return [
'level' => 'success',
'message' => sprintf('%s %s', $message, trans('firefly.update_version_alpha')),
];
}
private function releasedNewBeta(string $current, string $latest, Carbon $date): array
{
app('log')->debug('New release is also a beta!');
$message = (string)trans(
'firefly.update_new_version_alert',
[
'your_version' => $current,
'new_version' => $latest,
'date' => $date->isoFormat((string)trans('config.month_and_day_js')),
]
);
return [
'level' => 'success',
'message' => sprintf('%s %s', $message, trans('firefly.update_version_beta')),
];
}
$isAlpha = $information['is_alpha'] ?? false;
if (true === $isAlpha) {
$return['message'] = sprintf('%s %s', $return['message'], trans('firefly.update_version_alpha'));
app('log')->debug('New release is also a alpha!');
}
app('log')->debug('New release is here!', $return);
private function releasedNewVersion(string $current, string $latest, Carbon $date): array
{
app('log')->debug('New release is old enough.');
$message = (string)trans(
'firefly.update_new_version_alert',
[
'your_version' => $current,
'new_version' => $latest,
'date' => $date->isoFormat((string)trans('config.month_and_day_js')),
]
);
app('log')->debug('New release is here!', [$message]);
event(new NewVersionAvailable($message));
// send event, this may result in a notification.
event(new NewVersionAvailable($return['message']));
return $return;
return [
'level' => 'success',
'message' => $message,
];
}
}

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