Merge pull request #10906 from firefly-iii/develop

🤖 Automatically merge the PR into the main branch.
This commit is contained in:
github-actions[bot]
2025-09-13 18:38:12 +02:00
committed by GitHub
426 changed files with 6275 additions and 5982 deletions

View File

@@ -151,16 +151,16 @@
}, },
{ {
"name": "composer/semver", "name": "composer/semver",
"version": "3.4.3", "version": "3.4.4",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/composer/semver.git", "url": "https://github.com/composer/semver.git",
"reference": "4313d26ada5e0c4edfbd1dc481a92ff7bff91f12" "reference": "198166618906cb2de69b95d7d47e5fa8aa1b2b95"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/composer/semver/zipball/4313d26ada5e0c4edfbd1dc481a92ff7bff91f12", "url": "https://api.github.com/repos/composer/semver/zipball/198166618906cb2de69b95d7d47e5fa8aa1b2b95",
"reference": "4313d26ada5e0c4edfbd1dc481a92ff7bff91f12", "reference": "198166618906cb2de69b95d7d47e5fa8aa1b2b95",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@@ -212,7 +212,7 @@
"support": { "support": {
"irc": "ircs://irc.libera.chat:6697/composer", "irc": "ircs://irc.libera.chat:6697/composer",
"issues": "https://github.com/composer/semver/issues", "issues": "https://github.com/composer/semver/issues",
"source": "https://github.com/composer/semver/tree/3.4.3" "source": "https://github.com/composer/semver/tree/3.4.4"
}, },
"funding": [ "funding": [
{ {
@@ -222,13 +222,9 @@
{ {
"url": "https://github.com/composer", "url": "https://github.com/composer",
"type": "github" "type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/composer/composer",
"type": "tidelift"
} }
], ],
"time": "2024-09-19T14:15:21+00:00" "time": "2025-08-20T19:15:30+00:00"
}, },
{ {
"name": "composer/xdebug-handler", "name": "composer/xdebug-handler",
@@ -406,16 +402,16 @@
}, },
{ {
"name": "friendsofphp/php-cs-fixer", "name": "friendsofphp/php-cs-fixer",
"version": "v3.86.0", "version": "v3.87.2",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer.git", "url": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer.git",
"reference": "4a952bd19dc97879b0620f495552ef09b55f7d36" "reference": "da5f0a7858c79b56fc0b8c36d3efcfe5f37f0992"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/PHP-CS-Fixer/PHP-CS-Fixer/zipball/4a952bd19dc97879b0620f495552ef09b55f7d36", "url": "https://api.github.com/repos/PHP-CS-Fixer/PHP-CS-Fixer/zipball/da5f0a7858c79b56fc0b8c36d3efcfe5f37f0992",
"reference": "4a952bd19dc97879b0620f495552ef09b55f7d36", "reference": "da5f0a7858c79b56fc0b8c36d3efcfe5f37f0992",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@@ -426,39 +422,38 @@
"ext-hash": "*", "ext-hash": "*",
"ext-json": "*", "ext-json": "*",
"ext-tokenizer": "*", "ext-tokenizer": "*",
"fidry/cpu-core-counter": "^1.2", "fidry/cpu-core-counter": "^1.3",
"php": "^7.4 || ^8.0", "php": "^7.4 || ^8.0",
"react/child-process": "^0.6.6", "react/child-process": "^0.6.6",
"react/event-loop": "^1.5", "react/event-loop": "^1.5",
"react/promise": "^3.2", "react/promise": "^3.3",
"react/socket": "^1.16", "react/socket": "^1.16",
"react/stream": "^1.4", "react/stream": "^1.4",
"sebastian/diff": "^4.0.6 || ^5.1.1 || ^6.0.2 || ^7.0", "sebastian/diff": "^4.0.6 || ^5.1.1 || ^6.0.2 || ^7.0",
"symfony/console": "^5.4.47 || ^6.4.13 || ^7.0", "symfony/console": "^5.4.47 || ^6.4.24 || ^7.0",
"symfony/event-dispatcher": "^5.4.45 || ^6.4.13 || ^7.0", "symfony/event-dispatcher": "^5.4.45 || ^6.4.24 || ^7.0",
"symfony/filesystem": "^5.4.45 || ^6.4.13 || ^7.0", "symfony/filesystem": "^5.4.45 || ^6.4.24 || ^7.0",
"symfony/finder": "^5.4.45 || ^6.4.17 || ^7.0", "symfony/finder": "^5.4.45 || ^6.4.24 || ^7.0",
"symfony/options-resolver": "^5.4.45 || ^6.4.16 || ^7.0", "symfony/options-resolver": "^5.4.45 || ^6.4.24 || ^7.0",
"symfony/polyfill-mbstring": "^1.32", "symfony/polyfill-mbstring": "^1.33",
"symfony/polyfill-php80": "^1.32", "symfony/polyfill-php80": "^1.33",
"symfony/polyfill-php81": "^1.32", "symfony/polyfill-php81": "^1.33",
"symfony/process": "^5.4.47 || ^6.4.20 || ^7.2", "symfony/process": "^5.4.47 || ^6.4.24 || ^7.2",
"symfony/stopwatch": "^5.4.45 || ^6.4.19 || ^7.0" "symfony/stopwatch": "^5.4.45 || ^6.4.24 || ^7.0"
}, },
"require-dev": { "require-dev": {
"facile-it/paraunit": "^1.3.1 || ^2.6", "facile-it/paraunit": "^1.3.1 || ^2.7",
"infection/infection": "^0.29.14", "infection/infection": "^0.29.14",
"justinrainbow/json-schema": "^5.3 || ^6.4", "justinrainbow/json-schema": "^6.5",
"keradus/cli-executor": "^2.2", "keradus/cli-executor": "^2.2",
"mikey179/vfsstream": "^1.6.12", "mikey179/vfsstream": "^1.6.12",
"php-coveralls/php-coveralls": "^2.8", "php-coveralls/php-coveralls": "^2.8",
"php-cs-fixer/accessible-object": "^1.1",
"php-cs-fixer/phpunit-constraint-isidenticalstring": "^1.6", "php-cs-fixer/phpunit-constraint-isidenticalstring": "^1.6",
"php-cs-fixer/phpunit-constraint-xmlmatchesxsd": "^1.6", "php-cs-fixer/phpunit-constraint-xmlmatchesxsd": "^1.6",
"phpunit/phpunit": "^9.6.23 || ^10.5.47 || ^11.5.25", "phpunit/phpunit": "^9.6.25 || ^10.5.53 || ^11.5.34",
"symfony/polyfill-php84": "^1.32", "symfony/polyfill-php84": "^1.33",
"symfony/var-dumper": "^5.4.48 || ^6.4.23 || ^7.3.1", "symfony/var-dumper": "^5.4.48 || ^6.4.24 || ^7.3.2",
"symfony/yaml": "^5.4.45 || ^6.4.23 || ^7.3.1" "symfony/yaml": "^5.4.45 || ^6.4.24 || ^7.3.2"
}, },
"suggest": { "suggest": {
"ext-dom": "For handling output formats in XML", "ext-dom": "For handling output formats in XML",
@@ -499,7 +494,7 @@
], ],
"support": { "support": {
"issues": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/issues", "issues": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/issues",
"source": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/tree/v3.86.0" "source": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/tree/v3.87.2"
}, },
"funding": [ "funding": [
{ {
@@ -507,7 +502,7 @@
"type": "github" "type": "github"
} }
], ],
"time": "2025-08-13T22:36:21+00:00" "time": "2025-09-10T09:51:40+00:00"
}, },
{ {
"name": "psr/container", "name": "psr/container",
@@ -959,23 +954,23 @@
}, },
{ {
"name": "react/promise", "name": "react/promise",
"version": "v3.2.0", "version": "v3.3.0",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/reactphp/promise.git", "url": "https://github.com/reactphp/promise.git",
"reference": "8a164643313c71354582dc850b42b33fa12a4b63" "reference": "23444f53a813a3296c1368bb104793ce8d88f04a"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/reactphp/promise/zipball/8a164643313c71354582dc850b42b33fa12a4b63", "url": "https://api.github.com/repos/reactphp/promise/zipball/23444f53a813a3296c1368bb104793ce8d88f04a",
"reference": "8a164643313c71354582dc850b42b33fa12a4b63", "reference": "23444f53a813a3296c1368bb104793ce8d88f04a",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
"php": ">=7.1.0" "php": ">=7.1.0"
}, },
"require-dev": { "require-dev": {
"phpstan/phpstan": "1.10.39 || 1.4.10", "phpstan/phpstan": "1.12.28 || 1.4.10",
"phpunit/phpunit": "^9.6 || ^7.5" "phpunit/phpunit": "^9.6 || ^7.5"
}, },
"type": "library", "type": "library",
@@ -1020,7 +1015,7 @@
], ],
"support": { "support": {
"issues": "https://github.com/reactphp/promise/issues", "issues": "https://github.com/reactphp/promise/issues",
"source": "https://github.com/reactphp/promise/tree/v3.2.0" "source": "https://github.com/reactphp/promise/tree/v3.3.0"
}, },
"funding": [ "funding": [
{ {
@@ -1028,7 +1023,7 @@
"type": "open_collective" "type": "open_collective"
} }
], ],
"time": "2024-05-24T10:39:05+00:00" "time": "2025-08-19T18:57:03+00:00"
}, },
{ {
"name": "react/socket", "name": "react/socket",
@@ -1257,16 +1252,16 @@
}, },
{ {
"name": "symfony/console", "name": "symfony/console",
"version": "v7.3.2", "version": "v7.3.3",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/symfony/console.git", "url": "https://github.com/symfony/console.git",
"reference": "5f360ebc65c55265a74d23d7fe27f957870158a1" "reference": "cb0102a1c5ac3807cf3fdf8bea96007df7fdbea7"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/symfony/console/zipball/5f360ebc65c55265a74d23d7fe27f957870158a1", "url": "https://api.github.com/repos/symfony/console/zipball/cb0102a1c5ac3807cf3fdf8bea96007df7fdbea7",
"reference": "5f360ebc65c55265a74d23d7fe27f957870158a1", "reference": "cb0102a1c5ac3807cf3fdf8bea96007df7fdbea7",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@@ -1331,7 +1326,7 @@
"terminal" "terminal"
], ],
"support": { "support": {
"source": "https://github.com/symfony/console/tree/v7.3.2" "source": "https://github.com/symfony/console/tree/v7.3.3"
}, },
"funding": [ "funding": [
{ {
@@ -1351,7 +1346,7 @@
"type": "tidelift" "type": "tidelift"
} }
], ],
"time": "2025-07-30T17:13:41+00:00" "time": "2025-08-25T06:35:40+00:00"
}, },
{ {
"name": "symfony/deprecation-contracts", "name": "symfony/deprecation-contracts",
@@ -1422,16 +1417,16 @@
}, },
{ {
"name": "symfony/event-dispatcher", "name": "symfony/event-dispatcher",
"version": "v7.3.0", "version": "v7.3.3",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/symfony/event-dispatcher.git", "url": "https://github.com/symfony/event-dispatcher.git",
"reference": "497f73ac996a598c92409b44ac43b6690c4f666d" "reference": "b7dc69e71de420ac04bc9ab830cf3ffebba48191"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/497f73ac996a598c92409b44ac43b6690c4f666d", "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/b7dc69e71de420ac04bc9ab830cf3ffebba48191",
"reference": "497f73ac996a598c92409b44ac43b6690c4f666d", "reference": "b7dc69e71de420ac04bc9ab830cf3ffebba48191",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@@ -1482,7 +1477,7 @@
"description": "Provides tools that allow your application components to communicate with each other by dispatching events and listening to them", "description": "Provides tools that allow your application components to communicate with each other by dispatching events and listening to them",
"homepage": "https://symfony.com", "homepage": "https://symfony.com",
"support": { "support": {
"source": "https://github.com/symfony/event-dispatcher/tree/v7.3.0" "source": "https://github.com/symfony/event-dispatcher/tree/v7.3.3"
}, },
"funding": [ "funding": [
{ {
@@ -1493,12 +1488,16 @@
"url": "https://github.com/fabpot", "url": "https://github.com/fabpot",
"type": "github" "type": "github"
}, },
{
"url": "https://github.com/nicolas-grekas",
"type": "github"
},
{ {
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift" "type": "tidelift"
} }
], ],
"time": "2025-04-22T09:11:45+00:00" "time": "2025-08-13T11:49:31+00:00"
}, },
{ {
"name": "symfony/event-dispatcher-contracts", "name": "symfony/event-dispatcher-contracts",
@@ -1716,16 +1715,16 @@
}, },
{ {
"name": "symfony/options-resolver", "name": "symfony/options-resolver",
"version": "v7.3.2", "version": "v7.3.3",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/symfony/options-resolver.git", "url": "https://github.com/symfony/options-resolver.git",
"reference": "119bcf13e67dbd188e5dbc74228b1686f66acd37" "reference": "0ff2f5c3df08a395232bbc3c2eb7e84912df911d"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/symfony/options-resolver/zipball/119bcf13e67dbd188e5dbc74228b1686f66acd37", "url": "https://api.github.com/repos/symfony/options-resolver/zipball/0ff2f5c3df08a395232bbc3c2eb7e84912df911d",
"reference": "119bcf13e67dbd188e5dbc74228b1686f66acd37", "reference": "0ff2f5c3df08a395232bbc3c2eb7e84912df911d",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@@ -1763,7 +1762,7 @@
"options" "options"
], ],
"support": { "support": {
"source": "https://github.com/symfony/options-resolver/tree/v7.3.2" "source": "https://github.com/symfony/options-resolver/tree/v7.3.3"
}, },
"funding": [ "funding": [
{ {
@@ -1783,11 +1782,11 @@
"type": "tidelift" "type": "tidelift"
} }
], ],
"time": "2025-07-15T11:36:08+00:00" "time": "2025-08-05T10:16:07+00:00"
}, },
{ {
"name": "symfony/polyfill-ctype", "name": "symfony/polyfill-ctype",
"version": "v1.32.0", "version": "v1.33.0",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/symfony/polyfill-ctype.git", "url": "https://github.com/symfony/polyfill-ctype.git",
@@ -1846,7 +1845,7 @@
"portable" "portable"
], ],
"support": { "support": {
"source": "https://github.com/symfony/polyfill-ctype/tree/v1.32.0" "source": "https://github.com/symfony/polyfill-ctype/tree/v1.33.0"
}, },
"funding": [ "funding": [
{ {
@@ -1857,6 +1856,10 @@
"url": "https://github.com/fabpot", "url": "https://github.com/fabpot",
"type": "github" "type": "github"
}, },
{
"url": "https://github.com/nicolas-grekas",
"type": "github"
},
{ {
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift" "type": "tidelift"
@@ -1866,16 +1869,16 @@
}, },
{ {
"name": "symfony/polyfill-intl-grapheme", "name": "symfony/polyfill-intl-grapheme",
"version": "v1.32.0", "version": "v1.33.0",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/symfony/polyfill-intl-grapheme.git", "url": "https://github.com/symfony/polyfill-intl-grapheme.git",
"reference": "b9123926e3b7bc2f98c02ad54f6a4b02b91a8abe" "reference": "380872130d3a5dd3ace2f4010d95125fde5d5c70"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/b9123926e3b7bc2f98c02ad54f6a4b02b91a8abe", "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/380872130d3a5dd3ace2f4010d95125fde5d5c70",
"reference": "b9123926e3b7bc2f98c02ad54f6a4b02b91a8abe", "reference": "380872130d3a5dd3ace2f4010d95125fde5d5c70",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@@ -1924,7 +1927,7 @@
"shim" "shim"
], ],
"support": { "support": {
"source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.32.0" "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.33.0"
}, },
"funding": [ "funding": [
{ {
@@ -1935,16 +1938,20 @@
"url": "https://github.com/fabpot", "url": "https://github.com/fabpot",
"type": "github" "type": "github"
}, },
{
"url": "https://github.com/nicolas-grekas",
"type": "github"
},
{ {
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift" "type": "tidelift"
} }
], ],
"time": "2024-09-09T11:45:10+00:00" "time": "2025-06-27T09:58:17+00:00"
}, },
{ {
"name": "symfony/polyfill-intl-normalizer", "name": "symfony/polyfill-intl-normalizer",
"version": "v1.32.0", "version": "v1.33.0",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/symfony/polyfill-intl-normalizer.git", "url": "https://github.com/symfony/polyfill-intl-normalizer.git",
@@ -2005,7 +2012,7 @@
"shim" "shim"
], ],
"support": { "support": {
"source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.32.0" "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.33.0"
}, },
"funding": [ "funding": [
{ {
@@ -2016,6 +2023,10 @@
"url": "https://github.com/fabpot", "url": "https://github.com/fabpot",
"type": "github" "type": "github"
}, },
{
"url": "https://github.com/nicolas-grekas",
"type": "github"
},
{ {
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift" "type": "tidelift"
@@ -2025,7 +2036,7 @@
}, },
{ {
"name": "symfony/polyfill-mbstring", "name": "symfony/polyfill-mbstring",
"version": "v1.32.0", "version": "v1.33.0",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/symfony/polyfill-mbstring.git", "url": "https://github.com/symfony/polyfill-mbstring.git",
@@ -2086,7 +2097,7 @@
"shim" "shim"
], ],
"support": { "support": {
"source": "https://github.com/symfony/polyfill-mbstring/tree/v1.32.0" "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.33.0"
}, },
"funding": [ "funding": [
{ {
@@ -2097,6 +2108,10 @@
"url": "https://github.com/fabpot", "url": "https://github.com/fabpot",
"type": "github" "type": "github"
}, },
{
"url": "https://github.com/nicolas-grekas",
"type": "github"
},
{ {
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift" "type": "tidelift"
@@ -2106,7 +2121,7 @@
}, },
{ {
"name": "symfony/polyfill-php80", "name": "symfony/polyfill-php80",
"version": "v1.32.0", "version": "v1.33.0",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/symfony/polyfill-php80.git", "url": "https://github.com/symfony/polyfill-php80.git",
@@ -2166,7 +2181,7 @@
"shim" "shim"
], ],
"support": { "support": {
"source": "https://github.com/symfony/polyfill-php80/tree/v1.32.0" "source": "https://github.com/symfony/polyfill-php80/tree/v1.33.0"
}, },
"funding": [ "funding": [
{ {
@@ -2177,6 +2192,10 @@
"url": "https://github.com/fabpot", "url": "https://github.com/fabpot",
"type": "github" "type": "github"
}, },
{
"url": "https://github.com/nicolas-grekas",
"type": "github"
},
{ {
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift" "type": "tidelift"
@@ -2186,7 +2205,7 @@
}, },
{ {
"name": "symfony/polyfill-php81", "name": "symfony/polyfill-php81",
"version": "v1.32.0", "version": "v1.33.0",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/symfony/polyfill-php81.git", "url": "https://github.com/symfony/polyfill-php81.git",
@@ -2242,7 +2261,7 @@
"shim" "shim"
], ],
"support": { "support": {
"source": "https://github.com/symfony/polyfill-php81/tree/v1.32.0" "source": "https://github.com/symfony/polyfill-php81/tree/v1.33.0"
}, },
"funding": [ "funding": [
{ {
@@ -2253,6 +2272,10 @@
"url": "https://github.com/fabpot", "url": "https://github.com/fabpot",
"type": "github" "type": "github"
}, },
{
"url": "https://github.com/nicolas-grekas",
"type": "github"
},
{ {
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift" "type": "tidelift"
@@ -2262,16 +2285,16 @@
}, },
{ {
"name": "symfony/process", "name": "symfony/process",
"version": "v7.3.0", "version": "v7.3.3",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/symfony/process.git", "url": "https://github.com/symfony/process.git",
"reference": "40c295f2deb408d5e9d2d32b8ba1dd61e36f05af" "reference": "32241012d521e2e8a9d713adb0812bb773b907f1"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/symfony/process/zipball/40c295f2deb408d5e9d2d32b8ba1dd61e36f05af", "url": "https://api.github.com/repos/symfony/process/zipball/32241012d521e2e8a9d713adb0812bb773b907f1",
"reference": "40c295f2deb408d5e9d2d32b8ba1dd61e36f05af", "reference": "32241012d521e2e8a9d713adb0812bb773b907f1",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@@ -2303,7 +2326,7 @@
"description": "Executes commands in sub-processes", "description": "Executes commands in sub-processes",
"homepage": "https://symfony.com", "homepage": "https://symfony.com",
"support": { "support": {
"source": "https://github.com/symfony/process/tree/v7.3.0" "source": "https://github.com/symfony/process/tree/v7.3.3"
}, },
"funding": [ "funding": [
{ {
@@ -2314,12 +2337,16 @@
"url": "https://github.com/fabpot", "url": "https://github.com/fabpot",
"type": "github" "type": "github"
}, },
{
"url": "https://github.com/nicolas-grekas",
"type": "github"
},
{ {
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift" "type": "tidelift"
} }
], ],
"time": "2025-04-17T09:11:12+00:00" "time": "2025-08-18T09:42:54+00:00"
}, },
{ {
"name": "symfony/service-contracts", "name": "symfony/service-contracts",
@@ -2468,16 +2495,16 @@
}, },
{ {
"name": "symfony/string", "name": "symfony/string",
"version": "v7.3.2", "version": "v7.3.3",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/symfony/string.git", "url": "https://github.com/symfony/string.git",
"reference": "42f505aff654e62ac7ac2ce21033818297ca89ca" "reference": "17a426cce5fd1f0901fefa9b2a490d0038fd3c9c"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/symfony/string/zipball/42f505aff654e62ac7ac2ce21033818297ca89ca", "url": "https://api.github.com/repos/symfony/string/zipball/17a426cce5fd1f0901fefa9b2a490d0038fd3c9c",
"reference": "42f505aff654e62ac7ac2ce21033818297ca89ca", "reference": "17a426cce5fd1f0901fefa9b2a490d0038fd3c9c",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@@ -2535,7 +2562,7 @@
"utf8" "utf8"
], ],
"support": { "support": {
"source": "https://github.com/symfony/string/tree/v7.3.2" "source": "https://github.com/symfony/string/tree/v7.3.3"
}, },
"funding": [ "funding": [
{ {
@@ -2555,7 +2582,7 @@
"type": "tidelift" "type": "tidelift"
} }
], ],
"time": "2025-07-10T08:47:49+00:00" "time": "2025-08-25T06:35:40+00:00"
} }
], ],
"packages-dev": [], "packages-dev": [],

View File

@@ -1,6 +1,4 @@
parameters: parameters:
scanFiles:
- ../_ide_helper.php
paths: paths:
- ../app - ../app
- ../database - ../database
@@ -9,28 +7,24 @@ parameters:
- ../bootstrap/app.php - ../bootstrap/app.php
universalObjectCratesClasses: universalObjectCratesClasses:
- Illuminate\Database\Eloquent\Model - Illuminate\Database\Eloquent\Model
# TODO: slowly remove these parameters and fix the issues found.
reportUnmatchedIgnoredErrors: true reportUnmatchedIgnoredErrors: true
ignoreErrors: ignoreErrors:
# TODO: slowly remove these exceptions and fix the issues found. # all errors below I will never fix.
- '#Dynamic call to static method#' # all the Laravel ORM things depend on this. - '#expects view-string\|null, string given#'
- identifier: varTag.nativeType - '#expects view-string, string given#'
- identifier: varTag.type - "#Parameter \\#[1-2] \\$num[1-2] of function bc[a-z]+ expects numeric-string, [a-z\\-|&]+ given#"
- identifier: missingType.generics # not interesting enough to fix.
- -
identifier: larastan.noEnvCallsOutsideOfConfig identifier: larastan.noEnvCallsOutsideOfConfig
path: ../app/Console/Commands/System/CreatesDatabase.php path: ../app/Console/Commands/System/CreatesDatabase.php
- identifier: missingType.iterableValue # not interesting enough to fix. - identifier: missingType.iterableValue # not interesting enough to fix.
- identifier: missingType.generics # not interesting enough to fix. - identifier: varTag.type # needs a custom extension for every repository, not gonna happen.
- "#Parameter \\#[1-2] \\$num[1-2] of function bc[a-z]+ expects numeric-string, [a-z\\-|&]+ given#" - '#Dynamic call to static method Illuminate#'
- '#expects view-string, string given#' - '#Call to an undefined method Illuminate\\Database\\Eloquent\\Relations\\HasMany::before#' # is custom scope
- '#expects view-string\|null, string given#' - '#Call to an undefined method Illuminate\\Database\\Eloquent\\Relations\\HasMany::after#' # is custom scope
- '#Call to an undefined method Illuminate\\Database\\Eloquent\\Relations\\HasMany::withTrashed#' # is to allow soft delete
# phpstan can't handle this so we ignore them. - '#Call to an undefined method Illuminate\\Database\\Eloquent\\Relations\\HasMany::accountTypeIn#' # is a custom scope
- '#Call to an undefined method Illuminate\\Database\\Eloquent\\Relations\\HasMany::before#' - '#Call to an undefined method Illuminate\\Database\\Eloquent\\Relations\\BelongsTo::withTrashed#' # is to allow soft delete
- '#Call to an undefined method Illuminate\\Database\\Eloquent\\Relations\\HasMany::after#'
- '#Call to an undefined method Illuminate\\Database\\Eloquent\\Relations\\HasMany::withTrashed#'
- '#Call to an undefined method Illuminate\\Database\\Eloquent\\Relations\\HasMany::accountTypeIn#'
- '#Call to an undefined method Illuminate\\Database\\Eloquent\\Relations\\BelongsTo::withTrashed#'
# The level 8 is the highest level. original was 5 # The level 8 is the highest level. original was 5
# 7 is more than enough, higher just leaves NULL things. # 7 is more than enough, higher just leaves NULL things.

View File

@@ -314,8 +314,9 @@ DEMO_USERNAME=
DEMO_PASSWORD= DEMO_PASSWORD=
# #
# Disable or enable the running balance column data # Disable or enable the running balance column data.
# Please disable this. It's a very experimental feature. # If you enable this, please also run "php artisan firefly-iii:correct-database"
# This will take some time the first run.
# #
USE_RUNNING_BALANCE=false USE_RUNNING_BALANCE=false

View File

@@ -16,6 +16,10 @@ Alpha releases are created to test new features and fixes before they are includ
The release files are signed, and you can verify them using the [Firefly III releases PGP key](https://docs.firefly-iii.org/explanation/more-information/signatures/). The release files are signed, and you can verify them using the [Firefly III releases PGP key](https://docs.firefly-iii.org/explanation/more-information/signatures/).
## Develop with Firefly III
Are you interested in (future) API changes to Firefly III, or other interesting dev-related updates? Sign up to the [Firefly III developer newsletter](https://firefly-iii.kit.com/dev) to receive low-frequency updates about the development of Firefly III.
## Support Firefly III ## Support Firefly III
Did you know you can support the development of Firefly III? You can donate in many ways, like GitHub Sponsors or Patreon. Please [follow this link](https://bit.ly/donate-to-Firefly-III) for more information. Thank you for your consideration. Did you know you can support the development of Firefly III? You can donate in many ways, like GitHub Sponsors or Patreon. Please [follow this link](https://bit.ly/donate-to-Firefly-III) for more information. Thank you for your consideration.

View File

@@ -16,6 +16,10 @@ Alpha releases are created to test new features and fixes before they are includ
The release files are signed, and you can verify them using the [Firefly III releases PGP key](https://docs.firefly-iii.org/explanation/more-information/signatures/). The release files are signed, and you can verify them using the [Firefly III releases PGP key](https://docs.firefly-iii.org/explanation/more-information/signatures/).
## Develop with Firefly III
Are you interested in (future) API changes to Firefly III, or other interesting dev-related updates? Sign up to the [Firefly III developer newsletter](https://firefly-iii.kit.com/dev) to receive low-frequency updates about the development of Firefly III.
## Support Firefly III ## Support Firefly III
Did you know you can support the development of Firefly III? You can donate in many ways, like GitHub Sponsors or Patreon. Please [follow this link](https://bit.ly/donate-to-Firefly-III) for more information. Thank you for your consideration. Did you know you can support the development of Firefly III? You can donate in many ways, like GitHub Sponsors or Patreon. Please [follow this link](https://bit.ly/donate-to-Firefly-III) for more information. Thank you for your consideration.

View File

@@ -16,6 +16,10 @@ There is no changelog for this release, as it is not final. However, [changelog.
The release files are signed, and you can verify them using the [Firefly III releases PGP key](https://docs.firefly-iii.org/explanation/more-information/signatures/). The release files are signed, and you can verify them using the [Firefly III releases PGP key](https://docs.firefly-iii.org/explanation/more-information/signatures/).
## Develop with Firefly III
Are you interested in (future) API changes to Firefly III, or other interesting dev-related updates? Sign up to the [Firefly III developer newsletter](https://firefly-iii.kit.com/dev) to receive low-frequency updates about the development of Firefly III.
## Support Firefly III ## Support Firefly III
Did you know you can support the development of Firefly III? You can donate in many ways, like GitHub Sponsors or Patreon. Please [follow this link](https://bit.ly/donate-to-Firefly-III) for more information. Thank you for your consideration. Did you know you can support the development of Firefly III? You can donate in many ways, like GitHub Sponsors or Patreon. Please [follow this link](https://bit.ly/donate-to-Firefly-III) for more information. Thank you for your consideration.

View File

@@ -16,6 +16,10 @@ The changelog for this release may not be up-to-date, so it is not included. How
The releases are signed, and you can verify them using the [Firefly III releases PGP key](https://docs.firefly-iii.org/explanation/more-information/signatures/). The releases are signed, and you can verify them using the [Firefly III releases PGP key](https://docs.firefly-iii.org/explanation/more-information/signatures/).
## Develop with Firefly III
Are you interested in (future) API changes to Firefly III, or other interesting dev-related updates? Sign up to the [Firefly III developer newsletter](https://firefly-iii.kit.com/dev) to receive low-frequency updates about the development of Firefly III.
## Support Firefly III ## Support Firefly III
Did you know you can support the development of Firefly III? You can donate in many ways, like GitHub Sponsors or Patreon. Please [follow this link](https://bit.ly/donate-to-Firefly-III) for more information. Thank you for your consideration. Did you know you can support the development of Firefly III? You can donate in many ways, like GitHub Sponsors or Patreon. Please [follow this link](https://bit.ly/donate-to-Firefly-III) for more information. Thank you for your consideration.

View File

@@ -11,6 +11,10 @@ Welcome to release %version of Firefly III. It contains the latest fixes, transl
The releases are signed, and you can verify them using the [Firefly III releases PGP key](https://docs.firefly-iii.org/explanation/more-information/signatures/). The releases are signed, and you can verify them using the [Firefly III releases PGP key](https://docs.firefly-iii.org/explanation/more-information/signatures/).
## Develop with Firefly III
Are you interested in (future) API changes to Firefly III, or other interesting dev-related updates? Sign up to the [Firefly III developer newsletter](https://firefly-iii.kit.com/dev) to receive low-frequency updates about the development of Firefly III.
## Support Firefly III ## Support Firefly III
Did you know you can support the development of Firefly III? You can donate in many ways, like GitHub Sponsors or Patreon. Please [follow this link](https://bit.ly/donate-to-Firefly-III) for more information. Thank you for your consideration. Did you know you can support the development of Firefly III? You can donate in many ways, like GitHub Sponsors or Patreon. Please [follow this link](https://bit.ly/donate-to-Firefly-III) for more information. Thank you for your consideration.

View File

@@ -114,6 +114,7 @@ class AccountController extends Controller
'id' => (string) $account->id, 'id' => (string) $account->id,
'name' => $account->name, 'name' => $account->name,
'name_with_balance' => $nameWithBalance, 'name_with_balance' => $nameWithBalance,
'active' => $account->active,
'type' => $account->accountType->type, 'type' => $account->accountType->type,
'currency_id' => (string) $useCurrency->id, 'currency_id' => (string) $useCurrency->id,
'currency_name' => $useCurrency->name, 'currency_name' => $useCurrency->name,

View File

@@ -69,6 +69,7 @@ class BudgetController extends Controller
static fn (Budget $item) => [ static fn (Budget $item) => [
'id' => (string) $item->id, 'id' => (string) $item->id,
'name' => $item->name, 'name' => $item->name,
'active' => $item->active,
] ]
); );

View File

@@ -69,6 +69,7 @@ class RecurrenceController extends Controller
'id' => (string) $recurrence->id, 'id' => (string) $recurrence->id,
'name' => $recurrence->title, 'name' => $recurrence->title,
'description' => $recurrence->description, 'description' => $recurrence->description,
'active' => $recurrence->active,
]; ];
} }

View File

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

View File

@@ -69,6 +69,7 @@ class RuleGroupController extends Controller
'id' => (string) $group->id, 'id' => (string) $group->id,
'name' => $group->title, 'name' => $group->title,
'description' => $group->description, 'description' => $group->description,
'active' => $group->active,
]; ];
} }

View File

@@ -1,5 +1,26 @@
<?php <?php
/*
* BalanceController.php
* Copyright (c) 2025 james@firefly-iii.org
*
* This file is part of Firefly III (https://github.com/firefly-iii).
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
declare(strict_types=1); declare(strict_types=1);
namespace FireflyIII\Api\V1\Controllers\Chart; namespace FireflyIII\Api\V1\Controllers\Chart;
@@ -10,9 +31,7 @@ use FireflyIII\Enums\TransactionTypeEnum;
use FireflyIII\Enums\UserRoleEnum; use FireflyIII\Enums\UserRoleEnum;
use FireflyIII\Exceptions\FireflyException; use FireflyIII\Exceptions\FireflyException;
use FireflyIII\Helpers\Collector\GroupCollectorInterface; use FireflyIII\Helpers\Collector\GroupCollectorInterface;
use FireflyIII\Models\TransactionCurrency;
use FireflyIII\Repositories\Account\AccountRepositoryInterface; use FireflyIII\Repositories\Account\AccountRepositoryInterface;
use FireflyIII\Support\Facades\Amount;
use FireflyIII\Support\Http\Api\AccountBalanceGrouped; use FireflyIII\Support\Http\Api\AccountBalanceGrouped;
use FireflyIII\Support\Http\Api\CleansChartData; use FireflyIII\Support\Http\Api\CleansChartData;
use FireflyIII\Support\Http\Api\CollectsAccountsFromFilter; use FireflyIII\Support\Http\Api\CollectsAccountsFromFilter;

View File

@@ -31,10 +31,10 @@ use FireflyIII\Enums\UserRoleEnum;
use FireflyIII\Exceptions\FireflyException; use FireflyIII\Exceptions\FireflyException;
use FireflyIII\Models\Budget; use FireflyIII\Models\Budget;
use FireflyIII\Models\BudgetLimit; use FireflyIII\Models\BudgetLimit;
use FireflyIII\Models\TransactionCurrency;
use FireflyIII\Repositories\Budget\BudgetLimitRepositoryInterface; use FireflyIII\Repositories\Budget\BudgetLimitRepositoryInterface;
use FireflyIII\Repositories\Budget\BudgetRepositoryInterface; use FireflyIII\Repositories\Budget\BudgetRepositoryInterface;
use FireflyIII\Repositories\Budget\OperationsRepositoryInterface; use FireflyIII\Repositories\Budget\OperationsRepositoryInterface;
use FireflyIII\Support\Facades\Amount;
use FireflyIII\Support\Http\Api\CleansChartData; use FireflyIII\Support\Http\Api\CleansChartData;
use FireflyIII\Support\Http\Api\ExchangeRateConverter; use FireflyIII\Support\Http\Api\ExchangeRateConverter;
use FireflyIII\Support\Http\Api\ValidatesUserGroupTrait; use FireflyIII\Support\Http\Api\ValidatesUserGroupTrait;
@@ -55,7 +55,6 @@ class BudgetController extends Controller
protected OperationsRepositoryInterface $opsRepository; protected OperationsRepositoryInterface $opsRepository;
private BudgetLimitRepositoryInterface $blRepository; private BudgetLimitRepositoryInterface $blRepository;
private array $currencies = []; private array $currencies = [];
private TransactionCurrency $currency;
private BudgetRepositoryInterface $repository; private BudgetRepositoryInterface $repository;
public function __construct() public function __construct()
@@ -115,7 +114,7 @@ class BudgetController extends Controller
// get all limits: // get all limits:
$limits = $this->blRepository->getBudgetLimits($budget, $start, $end); $limits = $this->blRepository->getBudgetLimits($budget, $start, $end);
$rows = []; $rows = [];
$spent = $this->opsRepository->listExpenses($start, $end, null, new Collection([$budget])); $spent = $this->opsRepository->listExpenses($start, $end, null, new Collection()->push($budget));
$expenses = $this->processExpenses($budget->id, $spent, $start, $end); $expenses = $this->processExpenses($budget->id, $spent, $start, $end);
$converter = new ExchangeRateConverter(); $converter = new ExchangeRateConverter();
$currencies = [$this->primaryCurrency->id => $this->primaryCurrency]; $currencies = [$this->primaryCurrency->id => $this->primaryCurrency];
@@ -134,9 +133,9 @@ class BudgetController extends Controller
$row['pc_left'] = '0'; $row['pc_left'] = '0';
$row['pc_overspent'] = '0'; $row['pc_overspent'] = '0';
if (null !== $limit) { if ($limit instanceof BudgetLimit) {
$row['budgeted'] = $limit->amount; $row['budgeted'] = $limit->amount;
$row['left'] = bcsub($row['budgeted'], bcmul($row['spent'], '-1')); $row['left'] = bcsub((string) $row['budgeted'], bcmul((string) $row['spent'], '-1'));
$row['overspent'] = bcmul($row['left'], '-1'); $row['overspent'] = bcmul($row['left'], '-1');
$row['left'] = 1 === bccomp($row['left'], '0') ? $row['left'] : '0'; $row['left'] = 1 === bccomp($row['left'], '0') ? $row['left'] : '0';
$row['overspent'] = 1 === bccomp($row['overspent'], '0') ? $row['overspent'] : '0'; $row['overspent'] = 1 === bccomp($row['overspent'], '0') ? $row['overspent'] : '0';
@@ -144,7 +143,7 @@ class BudgetController extends Controller
// convert data if necessary. // convert data if necessary.
if (true === $this->convertToPrimary && $currencyId !== $this->primaryCurrency->id) { if (true === $this->convertToPrimary && $currencyId !== $this->primaryCurrency->id) {
$currencies[$currencyId] ??= TransactionCurrency::find($currencyId); $currencies[$currencyId] ??= Amount::getTransactionCurrencyById($currencyId);
$row['pc_budgeted'] = $converter->convert($currencies[$currencyId], $this->primaryCurrency, $start, $row['budgeted']); $row['pc_budgeted'] = $converter->convert($currencies[$currencyId], $this->primaryCurrency, $start, $row['budgeted']);
$row['pc_spent'] = $converter->convert($currencies[$currencyId], $this->primaryCurrency, $start, $row['spent']); $row['pc_spent'] = $converter->convert($currencies[$currencyId], $this->primaryCurrency, $start, $row['spent']);
$row['pc_left'] = $converter->convert($currencies[$currencyId], $this->primaryCurrency, $start, $row['left']); $row['pc_left'] = $converter->convert($currencies[$currencyId], $this->primaryCurrency, $start, $row['left']);
@@ -201,18 +200,18 @@ class BudgetController extends Controller
return $return; return $return;
} }
/** // /**
* When no budget limits are present, the expenses of the whole period are collected and grouped. // * When no budget limits are present, the expenses of the whole period are collected and grouped.
* This is grouped per currency. Because there is no limit set, "left to spend" and "overspent" are empty. // * This is grouped per currency. Because there is no limit set, "left to spend" and "overspent" are empty.
* // *
* @throws FireflyException // * @throws FireflyException
*/ // */
private function noBudgetLimits(Budget $budget, Carbon $start, Carbon $end): array // private function noBudgetLimits(Budget $budget, Carbon $start, Carbon $end): array
{ // {
$spent = $this->opsRepository->listExpenses($start, $end, null, new Collection([$budget])); // $spent = $this->opsRepository->listExpenses($start, $end, null, new Collection()->push($budget));
//
return $this->processExpenses($budget->id, $spent, $start, $end); // return $this->processExpenses($budget->id, $spent, $start, $end);
} // }
/** /**
* Shared between the "noBudgetLimits" function and "processLimit". Will take a single set of expenses and return * Shared between the "noBudgetLimits" function and "processLimit". Will take a single set of expenses and return
@@ -232,7 +231,7 @@ class BudgetController extends Controller
* @var array $block * @var array $block
*/ */
foreach ($spent as $currencyId => $block) { foreach ($spent as $currencyId => $block) {
$this->currencies[$currencyId] ??= TransactionCurrency::find($currencyId); $this->currencies[$currencyId] ??= Amount::getTransactionCurrencyById($currencyId);
$return[$currencyId] ??= [ $return[$currencyId] ??= [
'currency_id' => (string)$currencyId, 'currency_id' => (string)$currencyId,
'currency_code' => $block['currency_code'], 'currency_code' => $block['currency_code'],
@@ -251,66 +250,68 @@ class BudgetController extends Controller
// var_dump($return); // var_dump($return);
/** @var array $journal */ /** @var array $journal */
foreach ($currentBudgetArray['transaction_journals'] as $journal) { foreach ($currentBudgetArray['transaction_journals'] as $journal) {
$return[$currencyId]['spent'] = bcadd($return[$currencyId]['spent'], (string)$journal['amount']); /** @var numeric-string $amount */
$amount = (string)$journal['amount'];
$return[$currencyId]['spent'] = bcadd($return[$currencyId]['spent'], $amount);
} }
} }
return $return; return $return;
} }
/** // /**
* Function that processes each budget limit (per budget). // * Function that processes each budget limit (per budget).
* // *
* If you have a budget limit in EUR, only transactions in EUR will be considered. // * If you have a budget limit in EUR, only transactions in EUR will be considered.
* If you have a budget limit in GBP, only transactions in GBP will be considered. // * If you have a budget limit in GBP, only transactions in GBP will be considered.
* // *
* If you have a budget limit in EUR, and a transaction in GBP, it will not be considered for the EUR budget limit. // * If you have a budget limit in EUR, and a transaction in GBP, it will not be considered for the EUR budget limit.
* // *
* @throws FireflyException // * @throws FireflyException
*/ // */
private function budgetLimits(Budget $budget, Collection $limits): array // private function budgetLimits(Budget $budget, Collection $limits): array
{ // {
Log::debug(sprintf('Now in budgetLimits(#%d)', $budget->id)); // Log::debug(sprintf('Now in budgetLimits(#%d)', $budget->id));
$data = []; // $data = [];
//
// /** @var BudgetLimit $limit */
// foreach ($limits as $limit) {
// $data = array_merge($data, $this->processLimit($budget, $limit));
// }
//
// return $data;
// }
/** @var BudgetLimit $limit */ // /**
foreach ($limits as $limit) { // * @throws FireflyException
$data = array_merge($data, $this->processLimit($budget, $limit)); // */
} // private function processLimit(Budget $budget, BudgetLimit $limit): array
// {
return $data; // Log::debug(sprintf('Created new ExchangeRateConverter in %s', __METHOD__));
} // $end = clone $limit->end_date;
// $end->endOfDay();
/** // $spent = $this->opsRepository->listExpenses($limit->start_date, $end, null, new Collection()->push($budget));
* @throws FireflyException // $limitCurrencyId = $limit->transaction_currency_id;
*/ //
private function processLimit(Budget $budget, BudgetLimit $limit): array // /** @var array $entry */
{ // // only spent the entry where the entry's currency matches the budget limit's currency
Log::debug(sprintf('Created new ExchangeRateConverter in %s', __METHOD__)); // // so $filtered will only have 1 or 0 entries
$end = clone $limit->end_date; // $filtered = array_filter($spent, fn ($entry) => $entry['currency_id'] === $limitCurrencyId);
$end->endOfDay(); // $result = $this->processExpenses($budget->id, $filtered, $limit->start_date, $end);
$spent = $this->opsRepository->listExpenses($limit->start_date, $end, null, new Collection([$budget])); // if (1 === count($result)) {
$limitCurrencyId = $limit->transaction_currency_id; // $compare = bccomp($limit->amount, (string)app('steam')->positive($result[$limitCurrencyId]['spent']));
// $result[$limitCurrencyId]['budgeted'] = $limit->amount;
/** @var array $entry */ // if (1 === $compare) {
// only spent the entry where the entry's currency matches the budget limit's currency // // convert this amount into the primary currency:
// so $filtered will only have 1 or 0 entries // $result[$limitCurrencyId]['left'] = bcadd($limit->amount, (string)$result[$limitCurrencyId]['spent']);
$filtered = array_filter($spent, fn ($entry) => $entry['currency_id'] === $limitCurrencyId); // }
$result = $this->processExpenses($budget->id, $filtered, $limit->start_date, $end); // if ($compare <= 0) {
if (1 === count($result)) { // $result[$limitCurrencyId]['overspent'] = app('steam')->positive(bcadd($limit->amount, (string)$result[$limitCurrencyId]['spent']));
$compare = bccomp($limit->amount, (string)app('steam')->positive($result[$limitCurrencyId]['spent'])); // }
$result[$limitCurrencyId]['budgeted'] = $limit->amount; // }
if (1 === $compare) { //
// convert this amount into the primary currency: // return $result;
$result[$limitCurrencyId]['left'] = bcadd($limit->amount, (string)$result[$limitCurrencyId]['spent']); // }
}
if ($compare <= 0) {
$result[$limitCurrencyId]['overspent'] = app('steam')->positive(bcadd($limit->amount, (string)$result[$limitCurrencyId]['spent']));
}
}
return $result;
}
private function filterLimit(int $currencyId, Collection $limits): ?BudgetLimit private function filterLimit(int $currencyId, Collection $limits): ?BudgetLimit
{ {

View File

@@ -97,13 +97,14 @@ class CategoryController extends Controller
$collector = app(GroupCollectorInterface::class); $collector = app(GroupCollectorInterface::class);
$collector->setRange($start, $end)->withAccountInformation(); $collector->setRange($start, $end)->withAccountInformation();
$collector->setXorAccounts($accounts)->withCategoryInformation(); $collector->setXorAccounts($accounts)->withCategoryInformation();
$collector->setTypes([TransactionTypeEnum::WITHDRAWAL->value, TransactionTypeEnum::RECONCILIATION->value]); $collector->setTypes([TransactionTypeEnum::WITHDRAWAL->value, TransactionTypeEnum::DEPOSIT->value]);
$journals = $collector->getExtractedJournals(); $journals = $collector->getExtractedJournals();
/** @var array $journal */ /** @var array $journal */
foreach ($journals as $journal) { foreach ($journals as $journal) {
// find journal: // find journal:
$journalCurrencyId = (int)$journal['currency_id']; $journalCurrencyId = (int)$journal['currency_id'];
$type = $journal['transaction_type_type'];
$currency = $currencies[$journalCurrencyId] ?? $this->currencyRepos->find($journalCurrencyId); $currency = $currencies[$journalCurrencyId] ?? $this->currencyRepos->find($journalCurrencyId);
$currencies[$journalCurrencyId] = $currency; $currencies[$journalCurrencyId] = $currency;
$currencyId = (int)$currency->id; $currencyId = (int)$currency->id;
@@ -111,7 +112,7 @@ class CategoryController extends Controller
$currencyCode = (string)$currency->code; $currencyCode = (string)$currency->code;
$currencySymbol = (string)$currency->symbol; $currencySymbol = (string)$currency->symbol;
$currencyDecimalPlaces = (int)$currency->decimal_places; $currencyDecimalPlaces = (int)$currency->decimal_places;
$amount = Steam::positive($journal['amount']); $amount = Steam::positive((string)$journal['amount']);
$pcAmount = null; $pcAmount = null;
// overrule if necessary: // overrule if necessary:
@@ -151,22 +152,36 @@ class CategoryController extends Controller
'type' => 'bar', 'type' => 'bar',
'entries' => [ 'entries' => [
'spent' => '0', 'spent' => '0',
'earned' => '0',
], ],
'pc_entries' => [ 'pc_entries' => [
'spent' => '0', 'spent' => '0',
'earned' => '0',
], ],
]; ];
// add monies // add monies
$return[$key]['entries']['spent'] = bcadd($return[$key]['entries']['spent'], (string)$amount); // expenses to spent
if (TransactionTypeEnum::WITHDRAWAL->value === $type) {
$return[$key]['entries']['spent'] = bcadd($return[$key]['entries']['spent'], $amount);
if (null !== $pcAmount) { if (null !== $pcAmount) {
$return[$key]['pc_entries']['spent'] = bcadd($return[$key]['pc_entries']['spent'], (string)$pcAmount); $return[$key]['pc_entries']['spent'] = bcadd($return[$key]['pc_entries']['spent'], $pcAmount);
}
continue;
}
// positive amount = earned
if (TransactionTypeEnum::DEPOSIT->value === $type) {
$return[$key]['entries']['earned'] = bcadd($return[$key]['entries']['earned'], $amount);
if (null !== $pcAmount) {
$return[$key]['pc_entries']['earned'] = bcadd($return[$key]['pc_entries']['earned'], $pcAmount);
}
} }
} }
$return = array_values($return); $return = array_values($return);
// order by amount // order by amount
usort($return, static fn (array $a, array $b) => (float)$a['entries']['spent'] < (float)$b['entries']['spent'] ? 1 : -1); usort($return, static fn (array $a, array $b) => ((float)$a['entries']['spent'] + (float)$a['entries']['earned']) < ((float)$b['entries']['spent'] + (float)$b['entries']['earned']) ? 1 : -1);
return response()->json($this->clean($return)); return response()->json($this->clean($return));
} }

View File

@@ -27,7 +27,6 @@ namespace FireflyIII\Api\V1\Controllers;
use Carbon\Carbon; use Carbon\Carbon;
use Carbon\Exceptions\InvalidFormatException; use Carbon\Exceptions\InvalidFormatException;
use FireflyIII\Exceptions\BadHttpHeaderException; use FireflyIII\Exceptions\BadHttpHeaderException;
use FireflyIII\Models\Preference;
use FireflyIII\Models\TransactionCurrency; use FireflyIII\Models\TransactionCurrency;
use FireflyIII\Support\Facades\Amount; use FireflyIII\Support\Facades\Amount;
use FireflyIII\Support\Facades\Steam; use FireflyIII\Support\Facades\Steam;
@@ -66,8 +65,6 @@ abstract class Controller extends BaseController
protected const string JSON_CONTENT_TYPE = 'application/json'; protected const string JSON_CONTENT_TYPE = 'application/json';
protected array $accepts = ['application/json', 'application/vnd.api+json']; protected array $accepts = ['application/json', 'application/vnd.api+json'];
/** @var array<int, string> */
protected array $allowedSort;
protected bool $convertToPrimary = false; protected bool $convertToPrimary = false;
protected TransactionCurrency $primaryCurrency; protected TransactionCurrency $primaryCurrency;
protected ParameterBag $parameters; protected ParameterBag $parameters;
@@ -78,7 +75,6 @@ abstract class Controller extends BaseController
public function __construct() public function __construct()
{ {
// get global parameters // get global parameters
$this->allowedSort = config('firefly.allowed_sort_parameters');
$this->middleware( $this->middleware(
function ($request, $next) { function ($request, $next) {
$this->parameters = $this->getParameters(); $this->parameters = $this->getParameters();
@@ -108,12 +104,7 @@ abstract class Controller extends BaseController
{ {
$bag = new ParameterBag(); $bag = new ParameterBag();
$page = (int)request()->get('page'); $page = (int)request()->get('page');
if ($page < 1) { $page = min(max(1, $page), 2 ** 16);
$page = 1;
}
if ($page > 2 ** 16) {
$page = 2 ** 16;
}
$bag->set('page', $page); $bag->set('page', $page);
// some date fields: // some date fields:
@@ -131,20 +122,16 @@ abstract class Controller extends BaseController
$obj = null; $obj = null;
if (null !== $date) { if (null !== $date) {
try { try {
$obj = Carbon::parse((string)$date); $obj = Carbon::parse((string)$date, config('app.timezone'));
} catch (InvalidFormatException $e) { } catch (InvalidFormatException $e) {
// don't care // don't care
Log::warning( Log::warning(sprintf('Ignored invalid date "%s" in API controller parameter check: %s', substr((string)$date, 0, 20), $e->getMessage()));
sprintf(
'Ignored invalid date "%s" in API controller parameter check: %s',
substr((string)$date, 0, 20),
$e->getMessage()
)
);
} }
} }
if ($obj instanceof Carbon) {
$bag->set($field, $obj); $bag->set($field, $obj);
} }
}
// integer fields: // integer fields:
$integers = ['limit']; $integers = ['limit'];
@@ -159,13 +146,7 @@ abstract class Controller extends BaseController
} }
if (null !== $value) { if (null !== $value) {
$value = (int)$value; $value = (int)$value;
if ($value < 1) { $value = min(max(1, $value), 2 ** 16);
$value = 1;
}
if ($value > 2 ** 16) {
$value = 2 ** 16;
}
$bag->set($integer, $value); $bag->set($integer, $value);
} }
if (null === $value if (null === $value
@@ -175,46 +156,14 @@ abstract class Controller extends BaseController
/** @var User $user */ /** @var User $user */
$user = auth()->user(); $user = auth()->user();
/** @var Preference $pageSize */
$pageSize = (int)app('preferences')->getForUser($user, 'listPageSize', 50)->data; $pageSize = (int)app('preferences')->getForUser($user, 'listPageSize', 50)->data;
$bag->set($integer, $pageSize); $bag->set($integer, $pageSize);
} }
} }
// sort fields: // sort fields:
return $this->getSortParameters($bag);
}
private function getSortParameters(ParameterBag $bag): ParameterBag
{
$sortParameters = [];
try {
$param = (string)request()->query->get('sort');
} catch (BadRequestException $e) {
Log::error('Request field "sort" contains a non-scalar value. Value set to NULL.');
Log::error($e->getMessage());
Log::error($e->getTraceAsString());
$param = '';
}
if ('' === $param) {
return $bag;
}
$parts = explode(',', $param);
foreach ($parts as $part) {
$part = trim($part);
$direction = 'asc';
if ('-' === $part[0]) {
$part = substr($part, 1);
$direction = 'desc';
}
if (in_array($part, $this->allowedSort, true)) {
$sortParameters[] = [$part, $direction];
}
}
$bag->set('sort', $sortParameters);
return $bag; return $bag;
// return $this->getSortParameters($bag);
} }
/** /**
@@ -283,8 +232,6 @@ abstract class Controller extends BaseController
$baseUrl = sprintf('%s/api/v1', request()->getSchemeAndHttpHost()); $baseUrl = sprintf('%s/api/v1', request()->getSchemeAndHttpHost());
$manager->setSerializer(new JsonApiSerializer($baseUrl)); $manager->setSerializer(new JsonApiSerializer($baseUrl));
// $transformer->collectMetaData(new Collection([$object]));
$resource = new Item($object, $transformer, $key); $resource = new Item($object, $transformer, $key);
return $manager->createData($resource)->toArray(); return $manager->createData($resource)->toArray();

View File

@@ -76,7 +76,7 @@ class BudgetController extends Controller
/** @var Budget $budget */ /** @var Budget $budget */
foreach ($budgets as $budget) { foreach ($budgets as $budget) {
$expenses = $this->opsRepository->sumExpenses($start, $end, $assetAccounts, new Collection([$budget])); $expenses = $this->opsRepository->sumExpenses($start, $end, $assetAccounts, new Collection()->push($budget));
/** @var array $expense */ /** @var array $expense */
foreach ($expenses as $expense) { foreach ($expenses as $expense) {

View File

@@ -76,7 +76,7 @@ class CategoryController extends Controller
/** @var Category $category */ /** @var Category $category */
foreach ($categories as $category) { foreach ($categories as $category) {
$expenses = $this->opsRepository->sumExpenses($start, $end, $assetAccounts, new Collection([$category])); $expenses = $this->opsRepository->sumExpenses($start, $end, $assetAccounts, new Collection()->push($category));
/** @var array $expense */ /** @var array $expense */
foreach ($expenses as $expense) { foreach ($expenses as $expense) {

View File

@@ -76,7 +76,7 @@ class CategoryController extends Controller
/** @var Category $category */ /** @var Category $category */
foreach ($categories as $category) { foreach ($categories as $category) {
$expenses = $this->opsRepository->sumIncome($start, $end, $assetAccounts, new Collection([$category])); $expenses = $this->opsRepository->sumIncome($start, $end, $assetAccounts, new Collection()->push($category));
/** @var array $expense */ /** @var array $expense */
foreach ($expenses as $expense) { foreach ($expenses as $expense) {

View File

@@ -29,6 +29,7 @@ use FireflyIII\Api\V1\Requests\Insight\GenericRequest;
use FireflyIII\Enums\TransactionTypeEnum; use FireflyIII\Enums\TransactionTypeEnum;
use FireflyIII\Helpers\Collector\GroupCollectorInterface; use FireflyIII\Helpers\Collector\GroupCollectorInterface;
use FireflyIII\Support\Facades\Amount; use FireflyIII\Support\Facades\Amount;
use FireflyIII\Support\Facades\Steam;
use Illuminate\Http\JsonResponse; use Illuminate\Http\JsonResponse;
/** /**
@@ -71,7 +72,7 @@ class PeriodController extends Controller
'currency_id' => (string) $currencyId, 'currency_id' => (string) $currencyId,
'currency_code' => $currencyCode, 'currency_code' => $currencyCode,
]; ];
$response[$currencyId]['difference'] = bcadd($response[$currencyId]['difference'], (string) app('steam')->positive($journal[$field])); $response[$currencyId]['difference'] = bcadd($response[$currencyId]['difference'], Steam::positive($journal[$field]));
$response[$currencyId]['difference_float'] = (float) $response[$currencyId]['difference']; // float but on purpose. $response[$currencyId]['difference_float'] = (float) $response[$currencyId]['difference']; // float but on purpose.
} }

View File

@@ -30,6 +30,7 @@ use FireflyIII\Enums\TransactionTypeEnum;
use FireflyIII\Helpers\Collector\GroupCollectorInterface; use FireflyIII\Helpers\Collector\GroupCollectorInterface;
use FireflyIII\Repositories\Tag\TagRepositoryInterface; use FireflyIII\Repositories\Tag\TagRepositoryInterface;
use FireflyIII\Support\Facades\Amount; use FireflyIII\Support\Facades\Amount;
use FireflyIII\Support\Facades\Steam;
use Illuminate\Http\JsonResponse; use Illuminate\Http\JsonResponse;
/** /**
@@ -97,7 +98,7 @@ class TagController extends Controller
'currency_id' => (string) $currencyId, 'currency_id' => (string) $currencyId,
'currency_code' => $currencyCode, 'currency_code' => $currencyCode,
]; ];
$response[$currencyId]['difference'] = bcadd($response[$currencyId]['difference'], (string) app('steam')->positive($journal[$field])); $response[$currencyId]['difference'] = bcadd($response[$currencyId]['difference'], Steam::positive($journal[$field]));
$response[$currencyId]['difference_float'] = (float) $response[$currencyId]['difference']; $response[$currencyId]['difference_float'] = (float) $response[$currencyId]['difference'];
} }
@@ -148,7 +149,7 @@ class TagController extends Controller
'currency_id' => (string) $currencyId, 'currency_id' => (string) $currencyId,
'currency_code' => $journal['currency_code'], 'currency_code' => $journal['currency_code'],
]; ];
$response[$key]['difference'] = bcadd((string) $response[$key]['difference'], (string) app('steam')->positive($journal['amount'])); $response[$key]['difference'] = bcadd((string) $response[$key]['difference'], Steam::positive($journal['amount']));
$response[$key]['difference_float'] = (float) $response[$key]['difference']; $response[$key]['difference_float'] = (float) $response[$key]['difference'];
} }
@@ -160,10 +161,7 @@ class TagController extends Controller
'currency_id' => (string) $foreignCurrencyId, 'currency_id' => (string) $foreignCurrencyId,
'currency_code' => $journal['foreign_currency_code'], 'currency_code' => $journal['foreign_currency_code'],
]; ];
$response[$foreignKey]['difference'] = bcadd( $response[$foreignKey]['difference'] = bcadd((string) $response[$foreignKey]['difference'], Steam::positive($journal['foreign_amount']));
(string) $response[$foreignKey]['difference'],
(string) app('steam')->positive($journal['foreign_amount'])
);
$response[$foreignKey]['difference_float'] = (float) $response[$foreignKey]['difference']; $response[$foreignKey]['difference_float'] = (float) $response[$foreignKey]['difference'];
} }
} }

View File

@@ -76,7 +76,7 @@ class CategoryController extends Controller
/** @var Category $category */ /** @var Category $category */
foreach ($categories as $category) { foreach ($categories as $category) {
$expenses = $this->opsRepository->sumTransfers($start, $end, $assetAccounts, new Collection([$category])); $expenses = $this->opsRepository->sumTransfers($start, $end, $assetAccounts, new Collection()->push($category));
/** @var array $expense */ /** @var array $expense */
foreach ($expenses as $expense) { foreach ($expenses as $expense) {

View File

@@ -29,6 +29,7 @@ use FireflyIII\Api\V1\Requests\Insight\GenericRequest;
use FireflyIII\Enums\TransactionTypeEnum; use FireflyIII\Enums\TransactionTypeEnum;
use FireflyIII\Helpers\Collector\GroupCollectorInterface; use FireflyIII\Helpers\Collector\GroupCollectorInterface;
use FireflyIII\Support\Facades\Amount; use FireflyIII\Support\Facades\Amount;
use FireflyIII\Support\Facades\Steam;
use Illuminate\Http\JsonResponse; use Illuminate\Http\JsonResponse;
/** /**
@@ -71,7 +72,7 @@ class PeriodController extends Controller
'currency_id' => (string) $currencyId, 'currency_id' => (string) $currencyId,
'currency_code' => $currencyCode, 'currency_code' => $currencyCode,
]; ];
$response[$currencyId]['difference'] = bcadd($response[$currencyId]['difference'], (string) app('steam')->positive($journal[$field])); $response[$currencyId]['difference'] = bcadd($response[$currencyId]['difference'], Steam::positive($journal[$field]));
$response[$currencyId]['difference_float'] = (float) $response[$currencyId]['difference']; $response[$currencyId]['difference_float'] = (float) $response[$currencyId]['difference'];
} }

View File

@@ -30,6 +30,7 @@ use FireflyIII\Enums\TransactionTypeEnum;
use FireflyIII\Helpers\Collector\GroupCollectorInterface; use FireflyIII\Helpers\Collector\GroupCollectorInterface;
use FireflyIII\Repositories\Tag\TagRepositoryInterface; use FireflyIII\Repositories\Tag\TagRepositoryInterface;
use FireflyIII\Support\Facades\Amount; use FireflyIII\Support\Facades\Amount;
use FireflyIII\Support\Facades\Steam;
use Illuminate\Http\JsonResponse; use Illuminate\Http\JsonResponse;
/** /**
@@ -95,7 +96,7 @@ class TagController extends Controller
'currency_id' => (string) $currencyId, 'currency_id' => (string) $currencyId,
'currency_code' => $currencyCode, 'currency_code' => $currencyCode,
]; ];
$response[$currencyId]['difference'] = bcadd($response[$currencyId]['difference'], (string) app('steam')->positive($journal[$field])); $response[$currencyId]['difference'] = bcadd($response[$currencyId]['difference'], Steam::positive($journal[$field]));
$response[$currencyId]['difference_float'] = (float) $response[$currencyId]['difference']; $response[$currencyId]['difference_float'] = (float) $response[$currencyId]['difference'];
} }
@@ -146,7 +147,7 @@ class TagController extends Controller
'currency_id' => (string) $currencyId, 'currency_id' => (string) $currencyId,
'currency_code' => $journal['currency_code'], 'currency_code' => $journal['currency_code'],
]; ];
$response[$key]['difference'] = bcadd((string) $response[$key]['difference'], (string) app('steam')->positive($journal['amount'])); $response[$key]['difference'] = bcadd((string) $response[$key]['difference'], Steam::positive($journal['amount']));
$response[$key]['difference_float'] = (float) $response[$key]['difference']; $response[$key]['difference_float'] = (float) $response[$key]['difference'];
} }
@@ -160,7 +161,7 @@ class TagController extends Controller
]; ];
$response[$foreignKey]['difference'] = bcadd( $response[$foreignKey]['difference'] = bcadd(
(string) $response[$foreignKey]['difference'], (string) $response[$foreignKey]['difference'],
(string) app('steam')->positive($journal['foreign_amount']) Steam::positive($journal['foreign_amount'])
); );
$response[$foreignKey]['difference_float'] = (float) $response[$foreignKey]['difference']; // intentional float $response[$foreignKey]['difference_float'] = (float) $response[$foreignKey]['difference']; // intentional float
} }

View File

@@ -152,7 +152,7 @@ class ListController extends Controller
// use new group collector: // use new group collector:
/** @var GroupCollectorInterface $collector */ /** @var GroupCollectorInterface $collector */
$collector = app(GroupCollectorInterface::class); $collector = app(GroupCollectorInterface::class);
$collector->setUser($admin)->setAccounts(new Collection([$account])) $collector->setUser($admin)->setAccounts(new Collection()->push($account))
->withAPIInformation()->setLimit($pageSize)->setPage($this->parameters->get('page'))->setTypes($types) ->withAPIInformation()->setLimit($pageSize)->setPage($this->parameters->get('page'))->setTypes($types)
; ;

View File

@@ -25,6 +25,7 @@ declare(strict_types=1);
namespace FireflyIII\Api\V1\Controllers\Models\Account; namespace FireflyIII\Api\V1\Controllers\Models\Account;
use FireflyIII\Api\V1\Controllers\Controller; use FireflyIII\Api\V1\Controllers\Controller;
use FireflyIII\Api\V1\Requests\Models\Account\ShowRequest;
use FireflyIII\Exceptions\FireflyException; use FireflyIII\Exceptions\FireflyException;
use FireflyIII\Models\Account; use FireflyIII\Models\Account;
use FireflyIII\Repositories\Account\AccountRepositoryInterface; use FireflyIII\Repositories\Account\AccountRepositoryInterface;
@@ -33,7 +34,6 @@ use FireflyIII\Support\JsonApi\Enrichments\AccountEnrichment;
use FireflyIII\Transformers\AccountTransformer; use FireflyIII\Transformers\AccountTransformer;
use FireflyIII\User; use FireflyIII\User;
use Illuminate\Http\JsonResponse; use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Pagination\LengthAwarePaginator; use Illuminate\Pagination\LengthAwarePaginator;
use League\Fractal\Pagination\IlluminatePaginatorAdapter; use League\Fractal\Pagination\IlluminatePaginatorAdapter;
use League\Fractal\Resource\Collection as FractalCollection; use League\Fractal\Resource\Collection as FractalCollection;
@@ -71,34 +71,38 @@ class ShowController extends Controller
* *
* @throws FireflyException * @throws FireflyException
*/ */
public function index(Request $request): JsonResponse public function index(ShowRequest $request): JsonResponse
{ {
$manager = $this->getManager(); $manager = $this->getManager();
$type = $request->get('type') ?? 'all'; $params = $request->getParameters();
$this->parameters->set('type', $type); $this->parameters->set('type', $params['type']);
// types to get, page size: // types to get, page size:
$types = $this->mapAccountTypes($this->parameters->get('type')); $types = $this->mapAccountTypes($params['type']);
$pageSize = $this->parameters->get('limit');
// get list of accounts. Count it and split it. // get list of accounts. Count it and split it.
$this->repository->resetAccountOrder(); $this->repository->resetAccountOrder();
$collection = $this->repository->getAccountsByType($types, $this->parameters->get('sort') ?? []); $collection = $this->repository->getAccountsByType($types, $params['sort']);
$count = $collection->count(); $count = $collection->count();
// continue sort: // continue sort:
$accounts = $collection->slice(($this->parameters->get('page') - 1) * $pageSize, $pageSize); // TODO if the user sorts on DB dependent field there must be no slice before enrichment, only after.
// TODO still need to figure out how to do this easily.
$accounts = $collection->slice(($this->parameters->get('page') - 1) * $params['limit'], $params['limit']);
// enrich // enrich
/** @var User $admin */ /** @var User $admin */
$admin = auth()->user(); $admin = auth()->user();
$enrichment = new AccountEnrichment(); $enrichment = new AccountEnrichment();
$enrichment->setSort($params['sort']);
$enrichment->setDate($this->parameters->get('date')); $enrichment->setDate($this->parameters->get('date'));
$enrichment->setStart($this->parameters->get('start'));
$enrichment->setEnd($this->parameters->get('end'));
$enrichment->setUser($admin); $enrichment->setUser($admin);
$accounts = $enrichment->enrich($accounts); $accounts = $enrichment->enrich($accounts);
// make paginator: // make paginator:
$paginator = new LengthAwarePaginator($accounts, $count, $pageSize, $this->parameters->get('page')); $paginator = new LengthAwarePaginator($accounts, $count, $params['limit'], $this->parameters->get('page'));
$paginator->setPath(route('api.v1.accounts.index').$this->buildParams()); $paginator->setPath(route('api.v1.accounts.index').$this->buildParams());
/** @var AccountTransformer $transformer */ /** @var AccountTransformer $transformer */
@@ -117,7 +121,7 @@ class ShowController extends Controller
* *
* Show single instance. * Show single instance.
*/ */
public function show(Account $account): JsonResponse public function show(ShowRequest $request, Account $account): JsonResponse
{ {
// get list of accounts. Count it and split it. // get list of accounts. Count it and split it.
$this->repository->resetAccountOrder(); $this->repository->resetAccountOrder();
@@ -129,6 +133,8 @@ class ShowController extends Controller
$admin = auth()->user(); $admin = auth()->user();
$enrichment = new AccountEnrichment(); $enrichment = new AccountEnrichment();
$enrichment->setDate($this->parameters->get('date')); $enrichment->setDate($this->parameters->get('date'));
$enrichment->setStart($this->parameters->get('start'));
$enrichment->setEnd($this->parameters->get('end'));
$enrichment->setUser($admin); $enrichment->setUser($admin);
$account = $enrichment->enrichSingle($account); $account = $enrichment->enrichSingle($account);

View File

@@ -114,8 +114,8 @@ class ShowController extends Controller
public function show(AvailableBudget $availableBudget): JsonResponse public function show(AvailableBudget $availableBudget): JsonResponse
{ {
$manager = $this->getManager(); $manager = $this->getManager();
$start = $this->parameters->get('start'); // $start = $this->parameters->get('start');
$end = $this->parameters->get('end'); // $end = $this->parameters->get('end');
/** @var AvailableBudgetTransformer $transformer */ /** @var AvailableBudgetTransformer $transformer */
$transformer = app(AvailableBudgetTransformer::class); $transformer = app(AvailableBudgetTransformer::class);
@@ -126,8 +126,8 @@ class ShowController extends Controller
$admin = auth()->user(); $admin = auth()->user();
$enrichment = new AvailableBudgetEnrichment(); $enrichment = new AvailableBudgetEnrichment();
$enrichment->setUser($admin); $enrichment->setUser($admin);
$enrichment->setStart($start); // $enrichment->setStart($start);
$enrichment->setEnd($end); // $enrichment->setEnd($end);
$availableBudget = $enrichment->enrichSingle($availableBudget); $availableBudget = $enrichment->enrichSingle($availableBudget);

View File

@@ -84,6 +84,8 @@ class ShowController extends Controller
$enrichment->setUser($admin); $enrichment->setUser($admin);
$enrichment->setStart($this->parameters->get('start')); $enrichment->setStart($this->parameters->get('start'));
$enrichment->setEnd($this->parameters->get('end')); $enrichment->setEnd($this->parameters->get('end'));
/** @var Budget $budget */
$budget = $enrichment->enrichSingle($budget); $budget = $enrichment->enrichSingle($budget);

View File

@@ -86,7 +86,7 @@ class UpdateController extends Controller
$admin = auth()->user(); $admin = auth()->user();
$enrichment = new BudgetLimitEnrichment(); $enrichment = new BudgetLimitEnrichment();
$enrichment->setUser($admin); $enrichment->setUser($admin);
$budgetLimit = $enrichment->enrich($budgetLimit); $budgetLimit = $enrichment->enrichSingle($budgetLimit);
/** @var BudgetLimitTransformer $transformer */ /** @var BudgetLimitTransformer $transformer */
$transformer = app(BudgetLimitTransformer::class); $transformer = app(BudgetLimitTransformer::class);

View File

@@ -33,6 +33,7 @@ use FireflyIII\Enums\UserRoleEnum;
use FireflyIII\Models\CurrencyExchangeRate; use FireflyIII\Models\CurrencyExchangeRate;
use FireflyIII\Models\TransactionCurrency; use FireflyIII\Models\TransactionCurrency;
use FireflyIII\Repositories\ExchangeRate\ExchangeRateRepositoryInterface; use FireflyIII\Repositories\ExchangeRate\ExchangeRateRepositoryInterface;
use FireflyIII\Support\Facades\Amount;
use FireflyIII\Support\Http\Api\ValidatesUserGroupTrait; use FireflyIII\Support\Http\Api\ValidatesUserGroupTrait;
use FireflyIII\Transformers\ExchangeRateTransformer; use FireflyIII\Transformers\ExchangeRateTransformer;
use Illuminate\Http\JsonResponse; use Illuminate\Http\JsonResponse;
@@ -69,7 +70,7 @@ class StoreController extends Controller
foreach ($data as $date => $rate) { foreach ($data as $date => $rate) {
$date = Carbon::createFromFormat('Y-m-d', $date); $date = Carbon::createFromFormat('Y-m-d', $date);
$existing = $this->repository->getSpecificRateOnDate($from, $to, $date); $existing = $this->repository->getSpecificRateOnDate($from, $to, $date);
if (null !== $existing) { if ($existing instanceof CurrencyExchangeRate) {
// update existing rate. // update existing rate.
$existing = $this->repository->updateExchangeRate($existing, $rate); $existing = $this->repository->updateExchangeRate($existing, $rate);
$collection->push($existing); $collection->push($existing);
@@ -98,12 +99,9 @@ class StoreController extends Controller
$from = $request->getFromCurrency(); $from = $request->getFromCurrency();
$collection = new Collection(); $collection = new Collection();
foreach ($data['rates'] as $key => $rate) { foreach ($data['rates'] as $key => $rate) {
$to = TransactionCurrency::where('code', $key)->first(); $to = Amount::getTransactionCurrencyByCode($key);
if (null === $to) {
continue; // should not happen.
}
$existing = $this->repository->getSpecificRateOnDate($from, $to, $date); $existing = $this->repository->getSpecificRateOnDate($from, $to, $date);
if (null !== $existing) { if ($existing instanceof CurrencyExchangeRate) {
// update existing rate. // update existing rate.
$existing = $this->repository->updateExchangeRate($existing, $rate); $existing = $this->repository->updateExchangeRate($existing, $rate);
$collection->push($existing); $collection->push($existing);

View File

@@ -0,0 +1,126 @@
<?php
declare(strict_types=1);
/*
* TriggerController.php
* Copyright (c) 2025 james@firefly-iii.org
*
* This file is part of Firefly III (https://github.com/firefly-iii).
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
namespace FireflyIII\Api\V1\Controllers\Models\Recurrence;
use FireflyIII\Api\V1\Controllers\Controller;
use FireflyIII\Api\V1\Requests\Generic\SingleDateRequest;
use FireflyIII\Helpers\Collector\GroupCollectorInterface;
use FireflyIII\Jobs\CreateRecurringTransactions;
use FireflyIII\Models\Recurrence;
use FireflyIII\Repositories\Recurring\RecurringRepositoryInterface;
use FireflyIII\Support\Facades\Preferences;
use FireflyIII\Support\JsonApi\Enrichments\TransactionGroupEnrichment;
use FireflyIII\Transformers\TransactionGroupTransformer;
use FireflyIII\User;
use Illuminate\Http\JsonResponse;
use Illuminate\Pagination\LengthAwarePaginator;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Log;
use League\Fractal\Pagination\IlluminatePaginatorAdapter;
use League\Fractal\Resource\Collection as FractalCollection;
class TriggerController extends Controller
{
private RecurringRepositoryInterface $repository;
/**
* RecurrenceController constructor.
*/
public function __construct()
{
parent::__construct();
$this->middleware(
function ($request, $next) {
$this->repository = app(RecurringRepositoryInterface::class);
$this->repository->setUser(auth()->user());
return $next($request);
}
);
}
public function trigger(SingleDateRequest $request, Recurrence $recurrence): JsonResponse
{
// find recurrence occurrence for this date and trigger it.
// grab the date from the last time the recurrence fired:
$backupDate = $recurrence->latest_date;
$date = $request->getDate();
// fire the recurring cron job on the given date, then post-date the created transaction.
Log::info(sprintf('Trigger: will now fire recurring cron job task for date "%s".', $date->format('Y-m-d H:i:s')));
/** @var CreateRecurringTransactions $job */
$job = app(CreateRecurringTransactions::class);
$job->setRecurrences(new Collection()->push($recurrence));
$job->setDate($date);
$job->setForce(false);
$job->handle();
Log::debug('Done with recurrence.');
$groups = $job->getGroups();
$this->repository->markGroupsAsNow($groups);
$recurrence->latest_date = $backupDate;
$recurrence->latest_date_tz = $backupDate?->format('e');
$recurrence->save();
Preferences::mark();
// enrich groups and return them:
if (0 === $groups->count()) {
$paginator = new LengthAwarePaginator(new Collection(), 0, 1);
}
if ($groups->count() > 0) {
/** @var User $admin */
$admin = auth()->user();
// use new group collector:
/** @var GroupCollectorInterface $collector */
$collector = app(GroupCollectorInterface::class);
$collector
->setUser($admin)
->setIds($groups->pluck('id')->toArray())
->withAPIInformation()
;
$paginator = $collector->getPaginatedGroups();
}
$manager = $this->getManager();
$paginator->setPath(route('api.v1.recurrences.trigger', [$recurrence->id]).$this->buildParams());
// enrich
$admin = auth()->user();
$enrichment = new TransactionGroupEnrichment();
$enrichment->setUser($admin);
$transactions = $enrichment->enrich($paginator->getCollection());
/** @var TransactionGroupTransformer $transformer */
$transformer = app(TransactionGroupTransformer::class);
$transformer->setParameters($this->parameters);
$resource = new FractalCollection($transactions, $transformer, 'transactions');
$resource->setPaginator(new IlluminatePaginatorAdapter($paginator));
return response()->json($manager->createData($resource)->toArray())->header('Content-Type', self::CONTENT_TYPE);
}
}

View File

@@ -75,7 +75,7 @@ class TriggerController extends Controller
/** @var RuleEngineInterface $ruleEngine */ /** @var RuleEngineInterface $ruleEngine */
$ruleEngine = app(RuleEngineInterface::class); $ruleEngine = app(RuleEngineInterface::class);
$ruleEngine->setRules(new Collection([$rule])); $ruleEngine->setRules(new Collection()->push($rule));
// overrule the rule(s) if necessary. // overrule the rule(s) if necessary.
if (array_key_exists('start', $parameters) && null !== $parameters['start']) { if (array_key_exists('start', $parameters) && null !== $parameters['start']) {
@@ -129,7 +129,7 @@ class TriggerController extends Controller
/** @var RuleEngineInterface $ruleEngine */ /** @var RuleEngineInterface $ruleEngine */
$ruleEngine = app(RuleEngineInterface::class); $ruleEngine = app(RuleEngineInterface::class);
$ruleEngine->setRules(new Collection([$rule])); $ruleEngine->setRules(new Collection()->push($rule));
// overrule the rule(s) if necessary. // overrule the rule(s) if necessary.
if (array_key_exists('start', $parameters) && null !== $parameters['start']) { if (array_key_exists('start', $parameters) && null !== $parameters['start']) {

View File

@@ -30,9 +30,7 @@ use FireflyIII\Helpers\Collector\GroupCollectorInterface;
use FireflyIII\Models\Account; use FireflyIII\Models\Account;
use FireflyIII\Models\Bill; use FireflyIII\Models\Bill;
use FireflyIII\Models\Recurrence; use FireflyIII\Models\Recurrence;
use FireflyIII\Models\RecurrenceTransaction;
use FireflyIII\Models\Rule; use FireflyIII\Models\Rule;
use FireflyIII\Models\RuleTrigger;
use FireflyIII\Models\TransactionCurrency; use FireflyIII\Models\TransactionCurrency;
use FireflyIII\Repositories\Account\AccountRepositoryInterface; use FireflyIII\Repositories\Account\AccountRepositoryInterface;
use FireflyIII\Repositories\Bill\BillRepositoryInterface; use FireflyIII\Repositories\Bill\BillRepositoryInterface;
@@ -192,7 +190,7 @@ class ListController extends Controller
$enrichment->setUser($admin); $enrichment->setUser($admin);
$enrichment->setStart($this->parameters->get('start')); $enrichment->setStart($this->parameters->get('start'));
$enrichment->setEnd($this->parameters->get('end')); $enrichment->setEnd($this->parameters->get('end'));
$bills = $enrichment->enrichSingle($bills); $bills = $enrichment->enrich($bills);
// make paginator: // make paginator:
$paginator = new LengthAwarePaginator($bills, $count, $pageSize, $this->parameters->get('page')); $paginator = new LengthAwarePaginator($bills, $count, $pageSize, $this->parameters->get('page'));
@@ -268,7 +266,6 @@ class ListController extends Controller
// filter selection // filter selection
$collection = $unfiltered->filter( $collection = $unfiltered->filter(
static function (Recurrence $recurrence) use ($currency) { // @phpstan-ignore-line static function (Recurrence $recurrence) use ($currency) { // @phpstan-ignore-line
/** @var RecurrenceTransaction $transaction */
if (array_any($recurrence->recurrenceTransactions, fn ($transaction) => $transaction->transaction_currency_id === $currency->id || $transaction->foreign_currency_id === $currency->id)) { if (array_any($recurrence->recurrenceTransactions, fn ($transaction) => $transaction->transaction_currency_id === $currency->id || $transaction->foreign_currency_id === $currency->id)) {
return $recurrence; return $recurrence;
} }
@@ -320,7 +317,6 @@ class ListController extends Controller
$collection = $unfiltered->filter( $collection = $unfiltered->filter(
static function (Rule $rule) use ($currency) { // @phpstan-ignore-line static function (Rule $rule) use ($currency) { // @phpstan-ignore-line
/** @var RuleTrigger $trigger */
if (array_any($rule->ruleTriggers, fn ($trigger) => 'currency_is' === $trigger->trigger_type && $currency->name === $trigger->trigger_value)) { if (array_any($rule->ruleTriggers, fn ($trigger) => 'currency_is' === $trigger->trigger_type && $currency->name === $trigger->trigger_value)) {
return $rule; return $rule;
} }

View File

@@ -481,7 +481,7 @@ class BasicController extends Controller
$currencies = []; $currencies = [];
// first, create an entry for each entry in the "available" array. // first, create an entry for each entry in the "available" array.
/** @var array $availableBudget */ /** @var string $availableBudget */
foreach ($available as $currencyId => $availableBudget) { foreach ($available as $currencyId => $availableBudget) {
$currencies[$currencyId] ??= $this->currencyRepos->find($currencyId); $currencies[$currencyId] ??= $this->currencyRepos->find($currencyId);
$return[$currencyId] = [ $return[$currencyId] = [

View File

@@ -133,7 +133,6 @@ class ConfigurationController extends Controller
*/ */
public function show(string $configKey): JsonResponse public function show(string $configKey): JsonResponse
{ {
$data = [];
$dynamic = $this->getDynamicConfiguration(); $dynamic = $this->getDynamicConfiguration();
$shortKey = str_replace('configuration.', '', $configKey); $shortKey = str_replace('configuration.', '', $configKey);
if (str_starts_with($configKey, 'configuration.')) { if (str_starts_with($configKey, 'configuration.')) {
@@ -156,13 +155,11 @@ class ConfigurationController extends Controller
} }
// fallback // fallback
if (!str_starts_with($configKey, 'configuration.')) {
$data = [ $data = [
'title' => $configKey, 'title' => $configKey,
'value' => config($configKey), 'value' => config($shortKey),
'editable' => false, 'editable' => false,
]; ];
}
return response()->api(['data' => $data])->header('Content-Type', self::JSON_CONTENT_TYPE); return response()->api(['data' => $data])->header('Content-Type', self::JSON_CONTENT_TYPE);
} }

View File

@@ -32,7 +32,9 @@ use FireflyIII\Generator\Webhook\MessageGeneratorInterface;
use FireflyIII\Models\TransactionGroup; use FireflyIII\Models\TransactionGroup;
use FireflyIII\Models\Webhook; use FireflyIII\Models\Webhook;
use FireflyIII\Repositories\Webhook\WebhookRepositoryInterface; use FireflyIII\Repositories\Webhook\WebhookRepositoryInterface;
use FireflyIII\Support\JsonApi\Enrichments\WebhookEnrichment;
use FireflyIII\Transformers\WebhookTransformer; use FireflyIII\Transformers\WebhookTransformer;
use FireflyIII\User;
use Illuminate\Http\JsonResponse; use Illuminate\Http\JsonResponse;
use Illuminate\Pagination\LengthAwarePaginator; use Illuminate\Pagination\LengthAwarePaginator;
use Illuminate\Support\Collection; use Illuminate\Support\Collection;
@@ -90,6 +92,13 @@ class ShowController extends Controller
$paginator = new LengthAwarePaginator($webhooks, $count, $pageSize, $this->parameters->get('page')); $paginator = new LengthAwarePaginator($webhooks, $count, $pageSize, $this->parameters->get('page'));
$paginator->setPath(route('api.v1.webhooks.index').$this->buildParams()); $paginator->setPath(route('api.v1.webhooks.index').$this->buildParams());
// enrich
/** @var User $admin */
$admin = auth()->user();
$enrichment = new WebhookEnrichment();
$enrichment->setUser($admin);
$webhooks = $enrichment->enrich($webhooks);
/** @var WebhookTransformer $transformer */ /** @var WebhookTransformer $transformer */
$transformer = app(WebhookTransformer::class); $transformer = app(WebhookTransformer::class);
$transformer->setParameters($this->parameters); $transformer->setParameters($this->parameters);
@@ -117,6 +126,13 @@ class ShowController extends Controller
Log::channel('audit')->info(sprintf('User views webhook #%d.', $webhook->id)); Log::channel('audit')->info(sprintf('User views webhook #%d.', $webhook->id));
$manager = $this->getManager(); $manager = $this->getManager();
// enrich
/** @var User $admin */
$admin = auth()->user();
$enrichment = new WebhookEnrichment();
$enrichment->setUser($admin);
$webhook = $enrichment->enrichSingle($webhook);
/** @var WebhookTransformer $transformer */ /** @var WebhookTransformer $transformer */
$transformer = app(WebhookTransformer::class); $transformer = app(WebhookTransformer::class);
$transformer->setParameters($this->parameters); $transformer->setParameters($this->parameters);
@@ -149,14 +165,14 @@ class ShowController extends Controller
// tell the generator which trigger it should look for // tell the generator which trigger it should look for
$engine->setTrigger(WebhookTrigger::tryFrom($webhook->trigger)); $engine->setTrigger(WebhookTrigger::tryFrom($webhook->trigger));
// tell the generator which objects to process // tell the generator which objects to process
$engine->setObjects(new Collection([$group])); $engine->setObjects(new Collection()->push($group));
// set the webhook to trigger // set the webhook to trigger
$engine->setWebhooks(new Collection([$webhook])); $engine->setWebhooks(new Collection()->push($webhook));
// tell the generator to generate the messages // tell the generator to generate the messages
$engine->generateMessages(); $engine->generateMessages();
// trigger event to send them: // trigger event to send them:
Log::debug('send event RequestedSendWebhookMessages'); Log::debug('send event RequestedSendWebhookMessages from ShowController::triggerTransaction()');
event(new RequestedSendWebhookMessages()); event(new RequestedSendWebhookMessages());
return response()->json([], 204); return response()->json([], 204);

View File

@@ -27,7 +27,9 @@ namespace FireflyIII\Api\V1\Controllers\Webhook;
use FireflyIII\Api\V1\Controllers\Controller; use FireflyIII\Api\V1\Controllers\Controller;
use FireflyIII\Api\V1\Requests\Models\Webhook\CreateRequest; use FireflyIII\Api\V1\Requests\Models\Webhook\CreateRequest;
use FireflyIII\Repositories\Webhook\WebhookRepositoryInterface; use FireflyIII\Repositories\Webhook\WebhookRepositoryInterface;
use FireflyIII\Support\JsonApi\Enrichments\WebhookEnrichment;
use FireflyIII\Transformers\WebhookTransformer; use FireflyIII\Transformers\WebhookTransformer;
use FireflyIII\User;
use Illuminate\Http\JsonResponse; use Illuminate\Http\JsonResponse;
use Illuminate\Support\Facades\Log; use Illuminate\Support\Facades\Log;
use League\Fractal\Resource\Item; use League\Fractal\Resource\Item;
@@ -68,6 +70,15 @@ class StoreController extends Controller
} }
$webhook = $this->repository->store($data); $webhook = $this->repository->store($data);
// enrich
/** @var User $admin */
$admin = auth()->user();
$enrichment = new WebhookEnrichment();
$enrichment->setUser($admin);
$webhook = $enrichment->enrichSingle($webhook);
$manager = $this->getManager(); $manager = $this->getManager();
Log::channel('audit')->info('User stores new webhook', $data); Log::channel('audit')->info('User stores new webhook', $data);

View File

@@ -28,7 +28,9 @@ use FireflyIII\Api\V1\Controllers\Controller;
use FireflyIII\Api\V1\Requests\Models\Webhook\UpdateRequest; use FireflyIII\Api\V1\Requests\Models\Webhook\UpdateRequest;
use FireflyIII\Models\Webhook; use FireflyIII\Models\Webhook;
use FireflyIII\Repositories\Webhook\WebhookRepositoryInterface; use FireflyIII\Repositories\Webhook\WebhookRepositoryInterface;
use FireflyIII\Support\JsonApi\Enrichments\WebhookEnrichment;
use FireflyIII\Transformers\WebhookTransformer; use FireflyIII\Transformers\WebhookTransformer;
use FireflyIII\User;
use Illuminate\Http\JsonResponse; use Illuminate\Http\JsonResponse;
use Illuminate\Support\Facades\Log; use Illuminate\Support\Facades\Log;
use League\Fractal\Resource\Item; use League\Fractal\Resource\Item;
@@ -70,6 +72,15 @@ class UpdateController extends Controller
$webhook = $this->repository->update($webhook, $data); $webhook = $this->repository->update($webhook, $data);
$manager = $this->getManager(); $manager = $this->getManager();
// enrich
/** @var User $admin */
$admin = auth()->user();
$enrichment = new WebhookEnrichment();
$enrichment->setUser($admin);
/** @var Webhook $webhook */
$webhook = $enrichment->enrichSingle($webhook);
Log::channel('audit')->info(sprintf('User updates webhook #%d', $webhook->id), $data); Log::channel('audit')->info(sprintf('User updates webhook #%d', $webhook->id), $data);
/** @var WebhookTransformer $transformer */ /** @var WebhookTransformer $transformer */

View File

@@ -24,7 +24,7 @@ declare(strict_types=1);
namespace FireflyIII\Api\V1\Requests\Chart; namespace FireflyIII\Api\V1\Requests\Chart;
use Illuminate\Contracts\Validation\Validator; use Illuminate\Validation\Validator;
use FireflyIII\Enums\UserRoleEnum; use FireflyIII\Enums\UserRoleEnum;
use FireflyIII\Support\Http\Api\ValidatesUserGroupTrait; use FireflyIII\Support\Http\Api\ValidatesUserGroupTrait;
use FireflyIII\Support\Request\ChecksLogin; use FireflyIII\Support\Request\ChecksLogin;
@@ -64,6 +64,7 @@ class ChartRequest extends FormRequest
'end' => 'required|date|after:1970-01-02|before:2038-01-17|after_or_equal:start', 'end' => 'required|date|after:1970-01-02|before:2038-01-17|after_or_equal:start',
'preselected' => sprintf('nullable|in:%s', implode(',', config('firefly.preselected_accounts'))), 'preselected' => sprintf('nullable|in:%s', implode(',', config('firefly.preselected_accounts'))),
'period' => sprintf('nullable|in:%s', implode(',', config('firefly.valid_view_ranges'))), 'period' => sprintf('nullable|in:%s', implode(',', config('firefly.valid_view_ranges'))),
'accounts' => 'nullable|array',
'accounts.*' => 'exists:accounts,id', 'accounts.*' => 'exists:accounts,id',
]; ];

View File

@@ -24,7 +24,7 @@ declare(strict_types=1);
namespace FireflyIII\Api\V1\Requests\Data\Bulk; namespace FireflyIII\Api\V1\Requests\Data\Bulk;
use Illuminate\Contracts\Validation\Validator; use Illuminate\Validation\Validator;
use FireflyIII\Repositories\Account\AccountRepositoryInterface; use FireflyIII\Repositories\Account\AccountRepositoryInterface;
use FireflyIII\Support\Request\ChecksLogin; use FireflyIII\Support\Request\ChecksLogin;
use FireflyIII\Support\Request\ConvertsDataTypes; use FireflyIII\Support\Request\ConvertsDataTypes;

View File

@@ -24,7 +24,7 @@ declare(strict_types=1);
namespace FireflyIII\Api\V1\Requests\Data\Bulk; namespace FireflyIII\Api\V1\Requests\Data\Bulk;
use Illuminate\Contracts\Validation\Validator; use Illuminate\Validation\Validator;
use JsonException; use JsonException;
use FireflyIII\Enums\ClauseType; use FireflyIII\Enums\ClauseType;
use FireflyIII\Rules\IsValidBulkClause; use FireflyIII\Rules\IsValidBulkClause;

View File

@@ -0,0 +1,99 @@
<?php
declare(strict_types=1);
/*
* ShowRequest.php
* Copyright (c) 2025 james@firefly-iii.org
*
* This file is part of Firefly III (https://github.com/firefly-iii).
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
namespace FireflyIII\Api\V1\Requests\Models\Account;
use Illuminate\Validation\Validator;
use Carbon\Carbon;
use FireflyIII\Models\Account;
use FireflyIII\Rules\IsValidSortInstruction;
use FireflyIII\Support\Facades\Preferences;
use FireflyIII\Support\Http\Api\AccountFilter;
use FireflyIII\Support\Request\ConvertsDataTypes;
use FireflyIII\User;
use Illuminate\Foundation\Http\FormRequest;
class ShowRequest extends FormRequest
{
use AccountFilter;
use ConvertsDataTypes;
public function getParameters(): array
{
$limit = $this->convertInteger('limit');
if (0 === $limit) {
// get default for user:
/** @var User $user */
$user = auth()->user();
$limit = (int)Preferences::getForUser($user, 'listPageSize', 50)->data;
}
$page = $this->convertInteger('page');
$page = min(max(1, $page), 2 ** 16);
return [
'type' => $this->convertString('type', 'all'),
'limit' => $limit,
'sort' => $this->convertSortParameters('sort', Account::class),
'page' => $page,
];
}
public function rules(): array
{
$keys = implode(',', array_keys($this->types));
return [
'date' => 'date',
'start' => 'date|present_with:end|before_or_equal:end|before:2038-01-17|after:1970-01-02',
'end' => 'date|present_with:start|after_or_equal:start|before:2038-01-17|after:1970-01-02',
'sort' => ['nullable', new IsValidSortInstruction(Account::class)],
'type' => sprintf('in:%s', $keys),
'limit' => 'numeric|min:1|max:131337',
'page' => 'numeric|min:1|max:131337',
];
}
public function withValidator(Validator $validator): void
{
$validator->after(
function (Validator $validator): void {
if (count($validator->failed()) > 0) {
return;
}
$data = $validator->getData();
if (array_key_exists('date', $data) && array_key_exists('start', $data) && array_key_exists('end', $data)) {
// assume valid dates, before we got here.
$start = Carbon::parse($data['start'], config('app.timezone'))->startOfDay();
$end = Carbon::parse($data['end'], config('app.timezone'))->endOfDay();
$date = Carbon::parse($data['date'], config('app.timezone'));
if (!$date->between($start, $end)) {
$validator->errors()->add('date', (string)trans('validation.between_date'));
}
}
}
);
}
}

View File

@@ -24,7 +24,7 @@ declare(strict_types=1);
namespace FireflyIII\Api\V1\Requests\Models\Account; namespace FireflyIII\Api\V1\Requests\Models\Account;
use Illuminate\Contracts\Validation\Validator; use Illuminate\Validation\Validator;
use FireflyIII\Models\Account; use FireflyIII\Models\Account;
use FireflyIII\Models\Location; use FireflyIII\Models\Location;
use FireflyIII\Repositories\Account\AccountRepositoryInterface; use FireflyIII\Repositories\Account\AccountRepositoryInterface;

View File

@@ -24,7 +24,7 @@ declare(strict_types=1);
namespace FireflyIII\Api\V1\Requests\Models\AvailableBudget; namespace FireflyIII\Api\V1\Requests\Models\AvailableBudget;
use Illuminate\Contracts\Validation\Validator; use Illuminate\Validation\Validator;
use Carbon\Carbon; use Carbon\Carbon;
use FireflyIII\Rules\IsValidPositiveAmount; use FireflyIII\Rules\IsValidPositiveAmount;
use FireflyIII\Support\Request\ChecksLogin; use FireflyIII\Support\Request\ChecksLogin;

View File

@@ -24,7 +24,7 @@ declare(strict_types=1);
namespace FireflyIII\Api\V1\Requests\Models\Bill; namespace FireflyIII\Api\V1\Requests\Models\Bill;
use Illuminate\Contracts\Validation\Validator; use Illuminate\Validation\Validator;
use ValueError; use ValueError;
use TypeError; use TypeError;
use FireflyIII\Rules\IsBoolean; use FireflyIII\Rules\IsBoolean;

View File

@@ -24,7 +24,7 @@ declare(strict_types=1);
namespace FireflyIII\Api\V1\Requests\Models\Bill; namespace FireflyIII\Api\V1\Requests\Models\Bill;
use Illuminate\Contracts\Validation\Validator; use Illuminate\Validation\Validator;
use FireflyIII\Models\Bill; use FireflyIII\Models\Bill;
use FireflyIII\Rules\IsBoolean; use FireflyIII\Rules\IsBoolean;
use FireflyIII\Rules\IsValidPositiveAmount; use FireflyIII\Rules\IsValidPositiveAmount;

View File

@@ -24,7 +24,7 @@ declare(strict_types=1);
namespace FireflyIII\Api\V1\Requests\Models\Budget; namespace FireflyIII\Api\V1\Requests\Models\Budget;
use Illuminate\Contracts\Validation\Validator; use Illuminate\Validation\Validator;
use FireflyIII\Rules\IsBoolean; use FireflyIII\Rules\IsBoolean;
use FireflyIII\Rules\IsValidPositiveAmount; use FireflyIII\Rules\IsValidPositiveAmount;
use FireflyIII\Support\Request\ChecksLogin; use FireflyIII\Support\Request\ChecksLogin;

View File

@@ -24,7 +24,7 @@ declare(strict_types=1);
namespace FireflyIII\Api\V1\Requests\Models\Budget; namespace FireflyIII\Api\V1\Requests\Models\Budget;
use Illuminate\Contracts\Validation\Validator; use Illuminate\Validation\Validator;
use FireflyIII\Models\Budget; use FireflyIII\Models\Budget;
use FireflyIII\Rules\IsBoolean; use FireflyIII\Rules\IsBoolean;
use FireflyIII\Rules\IsValidPositiveAmount; use FireflyIII\Rules\IsValidPositiveAmount;

View File

@@ -24,7 +24,7 @@ declare(strict_types=1);
namespace FireflyIII\Api\V1\Requests\Models\BudgetLimit; namespace FireflyIII\Api\V1\Requests\Models\BudgetLimit;
use Illuminate\Contracts\Validation\Validator; use Illuminate\Validation\Validator;
use Carbon\Carbon; use Carbon\Carbon;
use FireflyIII\Rules\IsValidPositiveAmount; use FireflyIII\Rules\IsValidPositiveAmount;
use FireflyIII\Support\Request\ChecksLogin; use FireflyIII\Support\Request\ChecksLogin;

View File

@@ -28,7 +28,7 @@ use Carbon\Carbon;
use Carbon\Exceptions\InvalidFormatException; use Carbon\Exceptions\InvalidFormatException;
use FireflyIII\Support\Request\ChecksLogin; use FireflyIII\Support\Request\ChecksLogin;
use FireflyIII\Support\Request\ConvertsDataTypes; use FireflyIII\Support\Request\ConvertsDataTypes;
use Illuminate\Contracts\Validation\Validator; use Illuminate\Validation\Validator;
use Illuminate\Foundation\Http\FormRequest; use Illuminate\Foundation\Http\FormRequest;
class StoreByCurrenciesRequest extends FormRequest class StoreByCurrenciesRequest extends FormRequest
@@ -55,11 +55,11 @@ class StoreByCurrenciesRequest extends FormRequest
{ {
$validator->after( $validator->after(
static function (Validator $validator): void { static function (Validator $validator): void {
$data = $validator->getData() ?? []; $data = $validator->getData();
foreach ($data as $date => $rate) { foreach ($data as $date => $rate) {
try { try {
$date = Carbon::createFromFormat('Y-m-d', $date); Carbon::createFromFormat('Y-m-d', $date);
} catch (InvalidFormatException $e) { } catch (InvalidFormatException) {
$validator->errors()->add('date', trans('validation.date', ['attribute' => 'date'])); $validator->errors()->add('date', trans('validation.date', ['attribute' => 'date']));
return; return;

View File

@@ -24,10 +24,12 @@ declare(strict_types=1);
namespace FireflyIII\Api\V1\Requests\Models\CurrencyExchangeRate; namespace FireflyIII\Api\V1\Requests\Models\CurrencyExchangeRate;
use Illuminate\Validation\Validator;
use FireflyIII\Exceptions\FireflyException;
use FireflyIII\Models\TransactionCurrency; use FireflyIII\Models\TransactionCurrency;
use FireflyIII\Support\Facades\Amount;
use FireflyIII\Support\Request\ChecksLogin; use FireflyIII\Support\Request\ChecksLogin;
use FireflyIII\Support\Request\ConvertsDataTypes; use FireflyIII\Support\Request\ConvertsDataTypes;
use Illuminate\Contracts\Validation\Validator;
use Illuminate\Foundation\Http\FormRequest; use Illuminate\Foundation\Http\FormRequest;
class StoreByDateRequest extends FormRequest class StoreByDateRequest extends FormRequest
@@ -35,6 +37,9 @@ class StoreByDateRequest extends FormRequest
use ChecksLogin; use ChecksLogin;
use ConvertsDataTypes; use ConvertsDataTypes;
/**
* @return array<string, mixed>
*/
public function getAll(): array public function getAll(): array
{ {
return [ return [
@@ -45,11 +50,13 @@ class StoreByDateRequest extends FormRequest
public function getFromCurrency(): TransactionCurrency public function getFromCurrency(): TransactionCurrency
{ {
return TransactionCurrency::where('code', $this->get('from'))->first(); return Amount::getTransactionCurrencyByCode((string)$this->get('from'));
} }
/** /**
* The rules that the incoming request must be matched against. * The rules that the incoming request must be matched against.
*
* @return array<string, string>
*/ */
public function rules(): array public function rules(): array
{ {
@@ -79,8 +86,10 @@ class StoreByDateRequest extends FormRequest
continue; continue;
} }
$to = TransactionCurrency::where('code', $key)->first();
if (null === $to) { try {
Amount::getTransactionCurrencyByCode((string)$key);
} catch (FireflyException) {
$validator->errors()->add(sprintf('rates.%s', $key), trans('validation.invalid_currency_code', ['code' => $key])); $validator->errors()->add(sprintf('rates.%s', $key), trans('validation.invalid_currency_code', ['code' => $key]));
} }
} }

View File

@@ -26,6 +26,7 @@ namespace FireflyIII\Api\V1\Requests\Models\CurrencyExchangeRate;
use Carbon\Carbon; use Carbon\Carbon;
use FireflyIII\Models\TransactionCurrency; use FireflyIII\Models\TransactionCurrency;
use FireflyIII\Support\Facades\Amount;
use FireflyIII\Support\Request\ChecksLogin; use FireflyIII\Support\Request\ChecksLogin;
use FireflyIII\Support\Request\ConvertsDataTypes; use FireflyIII\Support\Request\ConvertsDataTypes;
use Illuminate\Foundation\Http\FormRequest; use Illuminate\Foundation\Http\FormRequest;
@@ -42,7 +43,7 @@ class StoreRequest extends FormRequest
public function getFromCurrency(): TransactionCurrency public function getFromCurrency(): TransactionCurrency
{ {
return TransactionCurrency::where('code', $this->get('from'))->first(); return Amount::getTransactionCurrencyByCode((string) $this->get('from'));
} }
public function getRate(): string public function getRate(): string
@@ -52,7 +53,7 @@ class StoreRequest extends FormRequest
public function getToCurrency(): TransactionCurrency public function getToCurrency(): TransactionCurrency
{ {
return TransactionCurrency::where('code', $this->get('to'))->first(); return Amount::getTransactionCurrencyByCode((string) $this->get('to'));
} }
/** /**

View File

@@ -24,7 +24,8 @@ declare(strict_types=1);
namespace FireflyIII\Api\V1\Requests\Models\PiggyBank; namespace FireflyIII\Api\V1\Requests\Models\PiggyBank;
use Illuminate\Contracts\Validation\Validator; use Illuminate\Validation\Validator;
use FireflyIII\Support\Facades\Amount;
use FireflyIII\Models\TransactionCurrency; use FireflyIII\Models\TransactionCurrency;
use FireflyIII\Repositories\Account\AccountRepositoryInterface; use FireflyIII\Repositories\Account\AccountRepositoryInterface;
use FireflyIII\Rules\IsValidZeroOrMoreAmount; use FireflyIII\Rules\IsValidZeroOrMoreAmount;
@@ -78,7 +79,7 @@ class StoreRequest extends FormRequest
'object_group_id' => 'numeric|belongsToUser:object_groups,id', 'object_group_id' => 'numeric|belongsToUser:object_groups,id',
'object_group_title' => ['min:1', 'max:255'], 'object_group_title' => ['min:1', 'max:255'],
'target_amount' => ['required', new IsValidZeroOrMoreAmount()], 'target_amount' => ['required', new IsValidZeroOrMoreAmount()],
'start_date' => 'date|nullable', 'start_date' => 'required|date|after:1970-01-01|before:2038-01-17',
'transaction_currency_id' => 'exists:transaction_currencies,id|required_without:transaction_currency_code', 'transaction_currency_id' => 'exists:transaction_currencies,id|required_without:transaction_currency_code',
'transaction_currency_code' => 'exists:transaction_currencies,code|required_without:transaction_currency_id', 'transaction_currency_code' => 'exists:transaction_currencies,code|required_without:transaction_currency_id',
'target_date' => 'date|nullable|after:start_date', 'target_date' => 'date|nullable|after:start_date',
@@ -96,7 +97,7 @@ class StoreRequest extends FormRequest
// validate start before end only if both are there. // validate start before end only if both are there.
$data = $validator->getData(); $data = $validator->getData();
$currency = $this->getCurrencyFromData($validator, $data); $currency = $this->getCurrencyFromData($validator, $data);
if (null === $currency) { if (!$currency instanceof TransactionCurrency) {
return; return;
} }
$targetAmount = (string) ($data['target_amount'] ?? '0'); $targetAmount = (string) ($data['target_amount'] ?? '0');
@@ -135,16 +136,10 @@ class StoreRequest extends FormRequest
private function getCurrencyFromData(Validator $validator, array $data): ?TransactionCurrency private function getCurrencyFromData(Validator $validator, array $data): ?TransactionCurrency
{ {
if (array_key_exists('transaction_currency_code', $data) && '' !== (string) $data['transaction_currency_code']) { if (array_key_exists('transaction_currency_code', $data) && '' !== (string) $data['transaction_currency_code']) {
$currency = TransactionCurrency::whereCode($data['transaction_currency_code'])->first(); return Amount::getTransactionCurrencyByCode((string) $data['transaction_currency_code']);
if (null !== $currency) {
return $currency;
}
} }
if (array_key_exists('transaction_currency_id', $data) && '' !== (string) $data['transaction_currency_id']) { if (array_key_exists('transaction_currency_id', $data) && '' !== (string) $data['transaction_currency_id']) {
$currency = TransactionCurrency::find((int) $data['transaction_currency_id']); return Amount::getTransactionCurrencyById((int) $data['transaction_currency_id']);
if (null !== $currency) {
return $currency;
}
} }
$validator->errors()->add('transaction_currency_id', trans('validation.require_currency_id_code')); $validator->errors()->add('transaction_currency_id', trans('validation.require_currency_id_code'));

View File

@@ -24,7 +24,7 @@ declare(strict_types=1);
namespace FireflyIII\Api\V1\Requests\Models\Recurrence; namespace FireflyIII\Api\V1\Requests\Models\Recurrence;
use Illuminate\Contracts\Validation\Validator; use Illuminate\Validation\Validator;
use FireflyIII\Rules\BelongsUser; use FireflyIII\Rules\BelongsUser;
use FireflyIII\Rules\IsBoolean; use FireflyIII\Rules\IsBoolean;
use FireflyIII\Rules\IsValidPositiveAmount; use FireflyIII\Rules\IsValidPositiveAmount;

View File

@@ -24,7 +24,7 @@ declare(strict_types=1);
namespace FireflyIII\Api\V1\Requests\Models\Recurrence; namespace FireflyIII\Api\V1\Requests\Models\Recurrence;
use Illuminate\Contracts\Validation\Validator; use Illuminate\Validation\Validator;
use FireflyIII\Models\Recurrence; use FireflyIII\Models\Recurrence;
use FireflyIII\Rules\BelongsUser; use FireflyIII\Rules\BelongsUser;
use FireflyIII\Rules\IsBoolean; use FireflyIII\Rules\IsBoolean;

View File

@@ -24,7 +24,7 @@ declare(strict_types=1);
namespace FireflyIII\Api\V1\Requests\Models\Rule; namespace FireflyIII\Api\V1\Requests\Models\Rule;
use Illuminate\Contracts\Validation\Validator; use Illuminate\Validation\Validator;
use FireflyIII\Rules\IsBoolean; use FireflyIII\Rules\IsBoolean;
use FireflyIII\Rules\IsValidActionExpression; use FireflyIII\Rules\IsValidActionExpression;
use FireflyIII\Support\Request\ChecksLogin; use FireflyIII\Support\Request\ChecksLogin;

View File

@@ -24,7 +24,7 @@ declare(strict_types=1);
namespace FireflyIII\Api\V1\Requests\Models\Rule; namespace FireflyIII\Api\V1\Requests\Models\Rule;
use Illuminate\Contracts\Validation\Validator; use Illuminate\Validation\Validator;
use FireflyIII\Models\Rule; use FireflyIII\Models\Rule;
use FireflyIII\Rules\IsBoolean; use FireflyIII\Rules\IsBoolean;
use FireflyIII\Rules\IsValidActionExpression; use FireflyIII\Rules\IsValidActionExpression;

View File

@@ -24,7 +24,7 @@ declare(strict_types=1);
namespace FireflyIII\Api\V1\Requests\Models\Transaction; namespace FireflyIII\Api\V1\Requests\Models\Transaction;
use Illuminate\Contracts\Validation\Validator; use Illuminate\Validation\Validator;
use FireflyIII\Models\Location; use FireflyIII\Models\Location;
use FireflyIII\Rules\BelongsUser; use FireflyIII\Rules\BelongsUser;
use FireflyIII\Rules\IsBoolean; use FireflyIII\Rules\IsBoolean;

View File

@@ -24,7 +24,7 @@ declare(strict_types=1);
namespace FireflyIII\Api\V1\Requests\Models\Transaction; namespace FireflyIII\Api\V1\Requests\Models\Transaction;
use Illuminate\Contracts\Validation\Validator; use Illuminate\Validation\Validator;
use FireflyIII\Exceptions\FireflyException; use FireflyIII\Exceptions\FireflyException;
use FireflyIII\Models\TransactionGroup; use FireflyIII\Models\TransactionGroup;
use FireflyIII\Rules\BelongsUser; use FireflyIII\Rules\BelongsUser;

View File

@@ -24,7 +24,7 @@ declare(strict_types=1);
namespace FireflyIII\Api\V1\Requests\Models\TransactionLink; namespace FireflyIII\Api\V1\Requests\Models\TransactionLink;
use Illuminate\Contracts\Validation\Validator; use Illuminate\Validation\Validator;
use FireflyIII\Repositories\Journal\JournalRepositoryInterface; use FireflyIII\Repositories\Journal\JournalRepositoryInterface;
use FireflyIII\Repositories\LinkType\LinkTypeRepositoryInterface; use FireflyIII\Repositories\LinkType\LinkTypeRepositoryInterface;
use FireflyIII\Support\Request\ChecksLogin; use FireflyIII\Support\Request\ChecksLogin;

View File

@@ -24,7 +24,7 @@ declare(strict_types=1);
namespace FireflyIII\Api\V1\Requests\Models\TransactionLink; namespace FireflyIII\Api\V1\Requests\Models\TransactionLink;
use Illuminate\Contracts\Validation\Validator; use Illuminate\Validation\Validator;
use FireflyIII\Models\TransactionJournalLink; use FireflyIII\Models\TransactionJournalLink;
use FireflyIII\Repositories\Journal\JournalRepositoryInterface; use FireflyIII\Repositories\Journal\JournalRepositoryInterface;
use FireflyIII\Repositories\LinkType\LinkTypeRepositoryInterface; use FireflyIII\Repositories\LinkType\LinkTypeRepositoryInterface;

View File

@@ -24,15 +24,13 @@ declare(strict_types=1);
namespace FireflyIII\Api\V1\Requests\Models\Webhook; namespace FireflyIII\Api\V1\Requests\Models\Webhook;
use FireflyIII\Enums\WebhookResponse; use FireflyIII\Exceptions\FireflyException;
use FireflyIII\Enums\WebhookTrigger;
use FireflyIII\Models\Webhook; use FireflyIII\Models\Webhook;
use FireflyIII\Rules\IsBoolean; use FireflyIII\Rules\IsBoolean;
use FireflyIII\Support\Request\ChecksLogin; use FireflyIII\Support\Request\ChecksLogin;
use FireflyIII\Support\Request\ConvertsDataTypes; use FireflyIII\Support\Request\ConvertsDataTypes;
use Illuminate\Contracts\Validation\Validator; use FireflyIII\Support\Request\ValidatesWebhooks;
use Illuminate\Foundation\Http\FormRequest; use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Support\Facades\Log;
/** /**
* Class CreateRequest * Class CreateRequest
@@ -41,27 +39,28 @@ class CreateRequest extends FormRequest
{ {
use ChecksLogin; use ChecksLogin;
use ConvertsDataTypes; use ConvertsDataTypes;
use ValidatesWebhooks;
public function getData(): array public function getData(): array
{ {
$triggers = Webhook::getTriggersForValidation();
$responses = Webhook::getResponsesForValidation();
$deliveries = Webhook::getDeliveriesForValidation();
$fields = [ $fields = [
'title' => ['title', 'convertString'], 'title' => ['title', 'convertString'],
'active' => ['active', 'boolean'], 'active' => ['active', 'boolean'],
'trigger' => ['trigger', 'convertString'],
'response' => ['response', 'convertString'],
'delivery' => ['delivery', 'convertString'],
'url' => ['url', 'convertString'], 'url' => ['url', 'convertString'],
]; ];
$triggers = $this->get('triggers', []);
$responses = $this->get('responses', []);
$deliveries = $this->get('deliveries', []);
if (0 === count($triggers) || 0 === count($responses) || 0 === count($deliveries)) {
throw new FireflyException('Unexpectedly got no responses, triggers or deliveries.');
}
// this is the way.
$return = $this->getAllData($fields); $return = $this->getAllData($fields);
$return['trigger'] = $triggers[$return['trigger']] ?? (int)$return['trigger']; $return['triggers'] = $triggers;
$return['response'] = $responses[$return['response']] ?? (int)$return['response']; $return['responses'] = $responses;
$return['delivery'] = $deliveries[$return['delivery']] ?? (int)$return['delivery']; $return['deliveries'] = $deliveries;
return $return; return $return;
} }
@@ -71,59 +70,24 @@ class CreateRequest extends FormRequest
*/ */
public function rules(): array public function rules(): array
{ {
$triggers = implode(',', array_keys(Webhook::getTriggersForValidation())); $triggers = implode(',', array_values(Webhook::getTriggers()));
$responses = implode(',', array_keys(Webhook::getResponsesForValidation())); $responses = implode(',', array_values(Webhook::getResponses()));
$deliveries = implode(',', array_keys(Webhook::getDeliveriesForValidation())); $deliveries = implode(',', array_values(Webhook::getDeliveries()));
$validProtocols = config('firefly.valid_url_protocols'); $validProtocols = config('firefly.valid_url_protocols');
return [ return [
'title' => 'required|min:1|max:255|uniqueObjectForUser:webhooks,title', 'title' => 'required|min:1|max:255|uniqueObjectForUser:webhooks,title',
'active' => [new IsBoolean()], 'active' => [new IsBoolean()],
'trigger' => sprintf('required|in:%s', $triggers), 'trigger' => 'prohibited',
'response' => sprintf('required|in:%s', $responses), 'triggers' => 'required|array|min:1|max:10',
'delivery' => sprintf('required|in:%s', $deliveries), 'triggers.*' => sprintf('required|in:%s', $triggers),
'url' => ['required', sprintf('url:%s', $validProtocols), 'uniqueWebhook'], 'response' => 'prohibited',
'responses' => 'required|array|min:1|max:1',
'responses.*' => sprintf('required|in:%s', $responses),
'delivery' => 'prohibited',
'deliveries' => 'required|array|min:1|max:1',
'deliveries.*' => sprintf('required|in:%s', $deliveries),
'url' => ['required', sprintf('url:%s', $validProtocols)],
]; ];
} }
public function withValidator(Validator $validator): void
{
$validator->after(
function (Validator $validator): void {
Log::debug('Validating webhook');
$data = $validator->getData();
$trigger = $data['trigger'] ?? null;
$response = $data['response'] ?? null;
if (null === $trigger || null === $response) {
Log::debug('No trigger or response, return.');
return;
}
$triggers = array_keys(Webhook::getTriggersForValidation());
$responses = array_keys(Webhook::getResponsesForValidation());
if (!in_array($trigger, $triggers, true) || !in_array($response, $responses, true)) {
return;
}
// cannot deliver budget info.
if (is_int($trigger)) {
Log::debug(sprintf('Trigger was integer (%d).', $trigger));
$trigger = WebhookTrigger::from($trigger)->name;
}
if (is_int($response)) {
Log::debug(sprintf('Response was integer (%d).', $response));
$response = WebhookResponse::from($response)->name;
}
Log::debug(sprintf('Trigger is %s, response is %s', $trigger, $response));
if (str_contains($trigger, 'TRANSACTION') && str_contains($response, 'BUDGET')) {
$validator->errors()->add('response', trans('validation.webhook_budget_info'));
}
if (str_contains($trigger, 'BUDGET') && str_contains($response, 'ACCOUNT')) {
$validator->errors()->add('response', trans('validation.webhook_account_info'));
}
if (str_contains($trigger, 'BUDGET') && str_contains($response, 'TRANSACTION')) {
$validator->errors()->add('response', trans('validation.webhook_transaction_info'));
}
}
);
}
} }

View File

@@ -24,15 +24,13 @@ declare(strict_types=1);
namespace FireflyIII\Api\V1\Requests\Models\Webhook; namespace FireflyIII\Api\V1\Requests\Models\Webhook;
use FireflyIII\Enums\WebhookResponse; use FireflyIII\Exceptions\FireflyException;
use FireflyIII\Enums\WebhookTrigger;
use FireflyIII\Models\Webhook; use FireflyIII\Models\Webhook;
use FireflyIII\Rules\IsBoolean; use FireflyIII\Rules\IsBoolean;
use FireflyIII\Support\Request\ChecksLogin; use FireflyIII\Support\Request\ChecksLogin;
use FireflyIII\Support\Request\ConvertsDataTypes; use FireflyIII\Support\Request\ConvertsDataTypes;
use Illuminate\Contracts\Validation\Validator; use FireflyIII\Support\Request\ValidatesWebhooks;
use Illuminate\Foundation\Http\FormRequest; use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Support\Facades\Log;
/** /**
* Class UpdateRequest * Class UpdateRequest
@@ -41,37 +39,28 @@ class UpdateRequest extends FormRequest
{ {
use ChecksLogin; use ChecksLogin;
use ConvertsDataTypes; use ConvertsDataTypes;
use ValidatesWebhooks;
public function getData(): array public function getData(): array
{ {
$triggers = Webhook::getTriggersForValidation();
$responses = Webhook::getResponsesForValidation();
$deliveries = Webhook::getDeliveriesForValidation();
$fields = [ $fields = [
'title' => ['title', 'convertString'], 'title' => ['title', 'convertString'],
'active' => ['active', 'boolean'], 'active' => ['active', 'boolean'],
'trigger' => ['trigger', 'convertString'],
'response' => ['response', 'convertString'],
'delivery' => ['delivery', 'convertString'],
'url' => ['url', 'convertString'], 'url' => ['url', 'convertString'],
]; ];
// this is the way. $triggers = $this->get('triggers', []);
$responses = $this->get('responses', []);
$deliveries = $this->get('deliveries', []);
if (0 === count($triggers) || 0 === count($responses) || 0 === count($deliveries)) {
throw new FireflyException('Unexpectedly got no responses, triggers or deliveries.');
}
$return = $this->getAllData($fields); $return = $this->getAllData($fields);
if (array_key_exists('trigger', $return)) { $return['triggers'] = $triggers;
$return['trigger'] = $triggers[$return['trigger']] ?? 0; $return['responses'] = $responses;
} $return['deliveries'] = $deliveries;
if (array_key_exists('response', $return)) {
$return['response'] = $responses[$return['response']] ?? 0;
}
if (array_key_exists('delivery', $return)) {
$return['delivery'] = $deliveries[$return['delivery']] ?? 0;
}
$return['secret'] = null !== $this->get('secret');
if (null !== $this->get('title')) {
$return['title'] = $this->convertString('title');
}
return $return; return $return;
} }
@@ -81,9 +70,9 @@ class UpdateRequest extends FormRequest
*/ */
public function rules(): array public function rules(): array
{ {
$triggers = implode(',', array_keys(Webhook::getTriggersForValidation())); $triggers = implode(',', array_values(Webhook::getTriggers()));
$responses = implode(',', array_keys(Webhook::getResponsesForValidation())); $responses = implode(',', array_values(Webhook::getResponses()));
$deliveries = implode(',', array_keys(Webhook::getDeliveriesForValidation())); $deliveries = implode(',', array_values(Webhook::getDeliveries()));
$validProtocols = config('firefly.valid_url_protocols'); $validProtocols = config('firefly.valid_url_protocols');
/** @var Webhook $webhook */ /** @var Webhook $webhook */
@@ -92,51 +81,18 @@ class UpdateRequest extends FormRequest
return [ return [
'title' => sprintf('min:1|max:255|uniqueObjectForUser:webhooks,title,%d', $webhook->id), 'title' => sprintf('min:1|max:255|uniqueObjectForUser:webhooks,title,%d', $webhook->id),
'active' => [new IsBoolean()], 'active' => [new IsBoolean()],
'trigger' => sprintf('in:%s', $triggers),
'response' => sprintf('in:%s', $responses), 'trigger' => 'prohibited',
'delivery' => sprintf('in:%s', $deliveries), 'triggers' => 'required|array|min:1|max:10',
'triggers.*' => sprintf('required|in:%s', $triggers),
'response' => 'prohibited',
'responses' => 'required|array|min:1|max:1',
'responses.*' => sprintf('required|in:%s', $responses),
'delivery' => 'prohibited',
'deliveries' => 'required|array|min:1|max:1',
'deliveries.*' => sprintf('required|in:%s', $deliveries),
'url' => [sprintf('url:%s', $validProtocols), sprintf('uniqueExistingWebhook:%d', $webhook->id)], 'url' => [sprintf('url:%s', $validProtocols), sprintf('uniqueExistingWebhook:%d', $webhook->id)],
]; ];
} }
public function withValidator(Validator $validator): void
{
$validator->after(
function (Validator $validator): void {
Log::debug('Validating webhook');
$data = $validator->getData();
$trigger = $data['trigger'] ?? null;
$response = $data['response'] ?? null;
if (null === $trigger || null === $response) {
Log::debug('No trigger or response, return.');
return;
}
$triggers = array_keys(Webhook::getTriggersForValidation());
$responses = array_keys(Webhook::getResponsesForValidation());
if (!in_array($trigger, $triggers, true) || !in_array($response, $responses, true)) {
return;
}
// cannot deliver budget info.
if (is_int($trigger)) {
Log::debug(sprintf('Trigger was integer (%d).', $trigger));
$trigger = WebhookTrigger::from($trigger)->name;
}
if (is_int($response)) {
Log::debug(sprintf('Response was integer (%d).', $response));
$response = WebhookResponse::from($response)->name;
}
Log::debug(sprintf('Trigger is %s, response is %s', $trigger, $response));
if (str_contains($trigger, 'TRANSACTION') && str_contains($response, 'BUDGET')) {
$validator->errors()->add('response', trans('validation.webhook_budget_info'));
}
if (str_contains($trigger, 'BUDGET') && str_contains($response, 'ACCOUNT')) {
$validator->errors()->add('response', trans('validation.webhook_account_info'));
}
if (str_contains($trigger, 'BUDGET') && str_contains($response, 'TRANSACTION')) {
$validator->errors()->add('response', trans('validation.webhook_transaction_info'));
}
}
);
}
} }

View File

@@ -24,7 +24,7 @@ declare(strict_types=1);
namespace FireflyIII\Api\V1\Requests\System; namespace FireflyIII\Api\V1\Requests\System;
use Illuminate\Contracts\Validation\Validator; use Illuminate\Validation\Validator;
use FireflyIII\Rules\IsBoolean; use FireflyIII\Rules\IsBoolean;
use FireflyIII\Support\Request\ChecksLogin; use FireflyIII\Support\Request\ChecksLogin;
use FireflyIII\Support\Request\ConvertsDataTypes; use FireflyIII\Support\Request\ConvertsDataTypes;

View File

@@ -28,6 +28,7 @@ namespace FireflyIII\Casts;
use Carbon\Carbon; use Carbon\Carbon;
use Illuminate\Contracts\Database\Eloquent\CastsAttributes; use Illuminate\Contracts\Database\Eloquent\CastsAttributes;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\Log;
/** /**
* Class SeparateTimezoneCaster * Class SeparateTimezoneCaster
@@ -51,6 +52,7 @@ class SeparateTimezoneCaster implements CastsAttributes
$timeZone = $attributes[sprintf('%s_tz', $key)] ?? config('app.timezone'); $timeZone = $attributes[sprintf('%s_tz', $key)] ?? config('app.timezone');
return Carbon::parse($value, $timeZone)->setTimezone(config('app.timezone')); return Carbon::parse($value, $timeZone)->setTimezone(config('app.timezone'));
// Log::debug(sprintf('SeparateTimezoneCaster: %s.%s = %s', str_replace('FireflyIII\\Models\\','',get_class($model)), $key, $result->toAtomString()));
} }
/** /**

View File

@@ -45,9 +45,7 @@ class CorrectsGroupAccounts extends Command
public function handle(): int public function handle(): int
{ {
$groups = []; $groups = [];
$res = TransactionJournal::groupBy('transaction_group_id') $res = TransactionJournal::groupBy('transaction_group_id')->get(['transaction_group_id', DB::raw('COUNT(transaction_group_id) as the_count')]);
->get(['transaction_group_id', DB::raw('COUNT(transaction_group_id) as the_count')])// @phpstan-ignore-line
;
/** @var TransactionJournal $journal */ /** @var TransactionJournal $journal */
foreach ($res as $journal) { foreach ($res as $journal) {

View File

@@ -57,8 +57,6 @@ class CorrectsPiggyBanks extends Command
$event->transaction_journal_id = null; $event->transaction_journal_id = null;
$event->save(); $event->save();
++$count; ++$count;
continue;
} }
} }
if (0 !== $count) { if (0 !== $count) {

View File

@@ -263,9 +263,9 @@ class CorrectsUnevenAmount extends Command
// Log::debug(sprintf('[c] %s', var_export($source->transaction_currency_id === $destination->foreign_currency_id,true))); // Log::debug(sprintf('[c] %s', var_export($source->transaction_currency_id === $destination->foreign_currency_id,true)));
// Log::debug(sprintf('[d] %s', var_export((int) $destination->transaction_currency_id ===(int) $source->foreign_currency_id, true))); // Log::debug(sprintf('[d] %s', var_export((int) $destination->transaction_currency_id ===(int) $source->foreign_currency_id, true)));
if (0 === bccomp((string) app('steam')->positive($source->amount), (string) app('steam')->positive($destination->foreign_amount)) if (0 === bccomp(Steam::positive($source->amount), Steam::positive($destination->foreign_amount))
&& $source->transaction_currency_id === $destination->foreign_currency_id && $source->transaction_currency_id === $destination->foreign_currency_id
&& 0 === bccomp((string) app('steam')->positive($destination->amount), (string) app('steam')->positive($source->foreign_amount)) && 0 === bccomp(Steam::positive($destination->amount), Steam::positive($source->foreign_amount))
&& (int) $destination->transaction_currency_id === (int) $source->foreign_currency_id && (int) $destination->transaction_currency_id === (int) $source->foreign_currency_id
) { ) {
return true; return true;
@@ -302,10 +302,10 @@ class CorrectsUnevenAmount extends Command
private function isBetweenAssetAndLiability(TransactionJournal $journal): bool private function isBetweenAssetAndLiability(TransactionJournal $journal): bool
{ {
/** @var Transaction $sourceTransaction */ /** @var null|Transaction $sourceTransaction */
$sourceTransaction = $journal->transactions()->where('amount', '<', 0)->first(); $sourceTransaction = $journal->transactions()->where('amount', '<', 0)->first();
/** @var Transaction $destinationTransaction */ /** @var null|Transaction $destinationTransaction */
$destinationTransaction = $journal->transactions()->where('amount', '>', 0)->first(); $destinationTransaction = $journal->transactions()->where('amount', '>', 0)->first();
if (null === $sourceTransaction || null === $destinationTransaction) { if (null === $sourceTransaction || null === $destinationTransaction) {
Log::warning('Either transaction is false, stop.'); Log::warning('Either transaction is false, stop.');

View File

@@ -55,10 +55,7 @@ class RemovesEmptyJournals extends Command
*/ */
private function deleteUnevenJournals(): void private function deleteUnevenJournals(): void
{ {
$set = Transaction::whereNull('deleted_at') $set = Transaction::whereNull('deleted_at')->groupBy('transactions.transaction_journal_id')->get([DB::raw('COUNT(transactions.transaction_journal_id) as the_count'), 'transaction_journal_id']);
->groupBy('transactions.transaction_journal_id')
->get([DB::raw('COUNT(transactions.transaction_journal_id) as the_count'), 'transaction_journal_id']) // @phpstan-ignore-line
;
$total = 0; $total = 0;
/** @var Transaction $row */ /** @var Transaction $row */

View File

@@ -71,7 +71,6 @@ class RestoresOAuthKeys extends Command
$this->storeKeysInDB(); $this->storeKeysInDB();
$this->friendlyInfo('Stored OAuth keys in database.'); $this->friendlyInfo('Stored OAuth keys in database.');
return;
} }
} }

View File

@@ -1,9 +1,9 @@
<?php <?php
declare(strict_types=1);
/* /*
* ValidatesEnvironmentVariables.php * ValidatesEnvironmentVariables.php
* Copyright (c) 2025 james@firefly-iii.org. * Copyright (c) 2025 james@firefly-iii.org
* *
* This file is part of Firefly III (https://github.com/firefly-iii). * This file is part of Firefly III (https://github.com/firefly-iii).
* *
@@ -18,9 +18,11 @@ declare(strict_types=1);
* GNU Affero General Public License for more details. * GNU Affero General Public License for more details.
* *
* You should have received a copy of the GNU Affero General Public License * 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/. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
declare(strict_types=1);
namespace FireflyIII\Console\Commands\Integrity; namespace FireflyIII\Console\Commands\Integrity;
use FireflyIII\Console\Commands\ShowsFriendlyMessages; use FireflyIII\Console\Commands\ShowsFriendlyMessages;
@@ -30,18 +32,7 @@ class ValidatesEnvironmentVariables extends Command
{ {
use ShowsFriendlyMessages; use ShowsFriendlyMessages;
/**
* The console command description.
*
* @var null|string
*/
protected $description = 'Makes sure you use the correct variables.'; protected $description = 'Makes sure you use the correct variables.';
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'integrity:validates-environment-variables'; protected $signature = 'integrity:validates-environment-variables';
/** /**

View File

@@ -515,7 +515,7 @@ class ForcesDecimalSize extends Command
continue; continue;
} }
// fix $field by rounding it down correctly. // fix $field by rounding it down correctly.
$pow = (float) 10 ** $currency->decimal_places; $pow = 10.0 ** $currency->decimal_places;
$correct = bcdiv((string) round((float) $value * $pow), (string) $pow, 12); $correct = bcdiv((string) round((float) $value * $pow), (string) $pow, 12);
$this->friendlyWarning(sprintf('Transaction #%d has amount with value "%s", this has been corrected to "%s".', $item->id, $value, $correct)); $this->friendlyWarning(sprintf('Transaction #%d has amount with value "%s", this has been corrected to "%s".', $item->id, $value, $correct));
@@ -546,7 +546,7 @@ class ForcesDecimalSize extends Command
continue; continue;
} }
// fix $field by rounding it down correctly. // fix $field by rounding it down correctly.
$pow = (float) 10 ** $currency->decimal_places; $pow = 10.0 ** $currency->decimal_places;
$correct = bcdiv((string) round((float) $value * $pow), (string) $pow, 12); $correct = bcdiv((string) round((float) $value * $pow), (string) $pow, 12);
$this->friendlyWarning( $this->friendlyWarning(
sprintf('Transaction #%d has foreign amount with value "%s", this has been corrected to "%s".', $item->id, $value, $correct) sprintf('Transaction #%d has foreign amount with value "%s", this has been corrected to "%s".', $item->id, $value, $correct)

View File

@@ -27,6 +27,7 @@ namespace FireflyIII\Console\Commands\System;
use Carbon\Carbon; use Carbon\Carbon;
use FireflyIII\Support\System\GeneratesInstallationId; use FireflyIII\Support\System\GeneratesInstallationId;
use Illuminate\Console\Command; use Illuminate\Console\Command;
use Random\RandomException;
class OutputsInstructions extends Command class OutputsInstructions extends Command
{ {
@@ -133,6 +134,9 @@ class OutputsInstructions extends Command
if ('03-31' === $today) { if ('03-31' === $today) {
$colors = ['bright-blue', 'bright-red', 'white', 'white', 'bright-red', 'bright-blue', 'default', 'default']; $colors = ['bright-blue', 'bright-red', 'white', 'white', 'bright-red', 'bright-blue', 'default', 'default'];
} }
if ('ru_RU' === config('firefly.default_language')) {
$colors = ['blue', 'blue', 'blue', 'yellow', 'yellow', 'yellow', 'default', 'default'];
}
$this->line(sprintf('<fg=%s> ______ _ __ _ _____ _____ _____ </>', $colors[0])); $this->line(sprintf('<fg=%s> ______ _ __ _ _____ _____ _____ </>', $colors[0]));
$this->line(sprintf('<fg=%s> | ____(_) / _| | |_ _|_ _|_ _| </>', $colors[1])); $this->line(sprintf('<fg=%s> | ____(_) / _| | |_ _|_ _|_ _| </>', $colors[1]));
@@ -245,7 +249,35 @@ class OutputsInstructions extends Command
'Be there or forever wonder.', 'Be there or forever wonder.',
'A year from now you will wish you had started today.', 'A year from now you will wish you had started today.',
]; ];
$addQuotes = true;
// fuck the Russian aggression in Ukraine.
// There is no point even trying to be neutral, because you cant. When I say you cant be neutral on
// a moving train, it means the world is already moving in certain directions. Children are going
// hungry, wars are taking place. In a situation like that, to be neutral or to try to be neutral,
// to stand aside, not to take a stand, not to participate, is to collaborate with whatever is
// going on, to allow that to happen.
if ('ru_RU' === config('firefly.default_language')) {
$addQuotes = false;
$lines = [
'🇺🇦 Слава Україні!',
'🇺🇦 Slava Ukraini!',
];
}
try {
$random = random_int(0, count($lines) - 1); $random = random_int(0, count($lines) - 1);
} catch (RandomException) {
$random = 0;
}
if ($addQuotes) {
$this->line(sprintf(' "%s"', $lines[$random])); $this->line(sprintf(' "%s"', $lines[$random]));
return;
}
$this->line(sprintf(' %s', $lines[$random]));
} }
} }

View File

@@ -1,9 +1,9 @@
<?php <?php
declare(strict_types=1);
/* /*
* RecalculatesRunningBalance.php * RecalculatesRunningBalance.php
* Copyright (c) 2025 james@firefly-iii.org. * Copyright (c) 2025 james@firefly-iii.org
* *
* This file is part of Firefly III (https://github.com/firefly-iii). * This file is part of Firefly III (https://github.com/firefly-iii).
* *
@@ -18,9 +18,11 @@ declare(strict_types=1);
* GNU Affero General Public License for more details. * GNU Affero General Public License for more details.
* *
* You should have received a copy of the GNU Affero General Public License * 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/. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
declare(strict_types=1);
namespace FireflyIII\Console\Commands\System; namespace FireflyIII\Console\Commands\System;
use FireflyIII\Console\Commands\ShowsFriendlyMessages; use FireflyIII\Console\Commands\ShowsFriendlyMessages;
@@ -48,7 +50,7 @@ class RecalculatesRunningBalance extends Command
/** /**
* Execute the console command. * Execute the console command.
*/ */
public function handle() public function handle(): int
{ {
if (true === config('firefly.feature_flags.running_balance_column')) { if (true === config('firefly.feature_flags.running_balance_column')) {
$this->friendlyInfo('Will recalculate account balances. This may take a LONG time. Please be patient.'); $this->friendlyInfo('Will recalculate account balances. This may take a LONG time. Please be patient.');
@@ -58,6 +60,8 @@ class RecalculatesRunningBalance extends Command
return 0; return 0;
} }
$this->friendlyWarning('This command has been disabled.'); $this->friendlyWarning('This command has been disabled.');
return 0;
} }
private function correctBalanceAmounts(bool $forced): void private function correctBalanceAmounts(bool $forced): void

View File

@@ -1,5 +1,26 @@
<?php <?php
/*
* ResetsErrorMailLimit.php
* Copyright (c) 2025 james@firefly-iii.org
*
* This file is part of Firefly III (https://github.com/firefly-iii).
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
declare(strict_types=1); declare(strict_types=1);
namespace FireflyIII\Console\Commands\System; namespace FireflyIII\Console\Commands\System;
@@ -8,6 +29,9 @@ use FireflyIII\Console\Commands\ShowsFriendlyMessages;
use Illuminate\Console\Command; use Illuminate\Console\Command;
use Symfony\Component\Console\Command\Command as CommandAlias; use Symfony\Component\Console\Command\Command as CommandAlias;
use function Safe\file_put_contents;
use function Safe\json_encode;
class ResetsErrorMailLimit extends Command class ResetsErrorMailLimit extends Command
{ {
use ShowsFriendlyMessages; use ShowsFriendlyMessages;

View File

@@ -78,8 +78,8 @@ class ScansAttachments extends Command
} }
$tempFileName = tempnam(sys_get_temp_dir(), 'FireflyIII'); $tempFileName = tempnam(sys_get_temp_dir(), 'FireflyIII');
file_put_contents($tempFileName, $decryptedContent); file_put_contents($tempFileName, $decryptedContent);
$attachment->md5 = (string)md5_file($tempFileName); $attachment->md5 = md5_file($tempFileName);
$attachment->mime = (string)mime_content_type($tempFileName); $attachment->mime = mime_content_type($tempFileName);
$attachment->save(); $attachment->save();
$this->friendlyInfo(sprintf('Fixed attachment #%d', $attachment->id)); $this->friendlyInfo(sprintf('Fixed attachment #%d', $attachment->id));
} }

View File

@@ -66,7 +66,7 @@ class RemovesDatabaseDecryption extends Command
* @var string $table * @var string $table
* @var array $fields * @var array $fields
*/ */
foreach ($tables as $table => $fields) { foreach ($tables as $table => $fields) { // @phpstan-ignore-line
$this->decryptTable($table, $fields); $this->decryptTable($table, $fields);
} }

View File

@@ -25,9 +25,11 @@ declare(strict_types=1);
namespace FireflyIII\Console\Commands\Upgrade; namespace FireflyIII\Console\Commands\Upgrade;
use FireflyIII\Console\Commands\ShowsFriendlyMessages; use FireflyIII\Console\Commands\ShowsFriendlyMessages;
use FireflyIII\Exceptions\FireflyException;
use FireflyIII\Models\Preference; use FireflyIII\Models\Preference;
use FireflyIII\Models\TransactionCurrency; use FireflyIII\Models\TransactionCurrency;
use FireflyIII\Models\UserGroup; use FireflyIII\Models\UserGroup;
use FireflyIII\Support\Facades\Amount;
use FireflyIII\User; use FireflyIII\User;
use Illuminate\Console\Command; use Illuminate\Console\Command;
use Illuminate\Support\Collection; use Illuminate\Support\Collection;
@@ -65,7 +67,7 @@ class UpgradesCurrencyPreferences extends Command
{ {
$configVar = app('fireflyconfig')->get(self::CONFIG_NAME, false); $configVar = app('fireflyconfig')->get(self::CONFIG_NAME, false);
if (null !== $configVar) { if (null !== $configVar) {
return (bool) $configVar->data; return (bool)$configVar->data;
} }
return false; return false;
@@ -117,9 +119,10 @@ class UpgradesCurrencyPreferences extends Command
// set the default currency for the user and for the group: // set the default currency for the user and for the group:
$preference = $this->getPreference($user); $preference = $this->getPreference($user);
$primaryCurrency = TransactionCurrency::where('code', $preference)->first();
if (null === $primaryCurrency) { try {
// get EUR $primaryCurrency = Amount::getTransactionCurrencyByCode($preference);
} catch (FireflyException) {
$primaryCurrency = TransactionCurrency::where('code', 'EUR')->first(); $primaryCurrency = TransactionCurrency::where('code', 'EUR')->first();
} }
$user->currencies()->updateExistingPivot($primaryCurrency->id, ['user_default' => true]); $user->currencies()->updateExistingPivot($primaryCurrency->id, ['user_default' => true]);
@@ -135,7 +138,7 @@ class UpgradesCurrencyPreferences extends Command
} }
if (null !== $preference->data && !is_array($preference->data)) { if (null !== $preference->data && !is_array($preference->data)) {
return (string) $preference->data; return (string)$preference->data;
} }
return 'EUR'; return 'EUR';

View File

@@ -75,6 +75,7 @@ class UpgradesDatabase extends Command
'upgrade:610-currency-preferences', 'upgrade:610-currency-preferences',
'upgrade:620-piggy-banks', 'upgrade:620-piggy-banks',
'upgrade:620-pc-amounts', 'upgrade:620-pc-amounts',
'upgrade:640-upgrade-webhooks',
'firefly-iii:correct-database', 'firefly-iii:correct-database',
]; ];
$args = []; $args = [];

View File

@@ -157,7 +157,6 @@ class UpgradesLiabilitiesEight extends Command
$service = new TransactionGroupDestroyService(); $service = new TransactionGroupDestroyService();
$service->destroy($group); $service->destroy($group);
return;
} }
} }

View File

@@ -0,0 +1,116 @@
<?php
/*
* UpgradesWebhooks.php
* Copyright (c) 2025 james@firefly-iii.org
*
* This file is part of Firefly III (https://github.com/firefly-iii).
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
declare(strict_types=1);
namespace FireflyIII\Console\Commands\Upgrade;
use FireflyIII\Console\Commands\ShowsFriendlyMessages;
use FireflyIII\Enums\WebhookDelivery;
use FireflyIII\Enums\WebhookResponse;
use FireflyIII\Enums\WebhookTrigger;
use FireflyIII\Models\Webhook;
use FireflyIII\Models\WebhookDelivery as WebhookDeliveryModel;
use FireflyIII\Models\WebhookResponse as WebhookResponseModel;
use FireflyIII\Models\WebhookTrigger as WebhookTriggerModel;
use Illuminate\Console\Command;
class UpgradesWebhooks extends Command
{
use ShowsFriendlyMessages;
public const string CONFIG_NAME = '640_upgrade_webhooks';
protected $description = 'Upgrade webhooks so they can handle multiple triggers.';
protected $signature = 'upgrade:640-upgrade-webhooks {--F|force : Force the execution of this command.}';
/**
* Execute the console command.
*/
public function handle(): int
{
if ($this->isExecuted() && true !== $this->option('force')) {
$this->friendlyInfo('This command has already been executed.');
return 0;
}
$this->upgradeWebhooks();
$this->markAsExecuted();
$this->friendlyPositive('Upgraded webhooks.');
return 0;
}
private function isExecuted(): bool
{
$configVar = app('fireflyconfig')->get(self::CONFIG_NAME, false);
if (null !== $configVar) {
return (bool)$configVar->data;
}
return false;
}
private function upgradeWebhooks(): void
{
$set = Webhook::where('delivery', '>', 1)->orWhere('trigger', '>', 1)->orWhere('response', '>', 1)->get();
/** @var Webhook $webhook */
foreach ($set as $webhook) {
$this->upgradeWebhook($webhook);
}
}
private function upgradeWebhook(Webhook $webhook): void
{
$delivery = WebhookDelivery::tryFrom((int)$webhook->delivery);
$response = WebhookResponse::tryFrom((int)$webhook->response);
$trigger = WebhookTrigger::tryFrom((int)$webhook->trigger);
if (null === $delivery || null === $response || null === $trigger) {
$this->friendlyError(sprintf('[a] Webhook #%d has an invalid delivery, response or trigger value. Will not upgrade.', $webhook->id));
return;
}
$deliveryModel = WebhookDeliveryModel::where('key', $delivery->value)->first();
$responseModel = WebhookResponseModel::where('key', $response->value)->first();
$triggerModel = WebhookTriggerModel::where('key', $trigger->value)->first();
if (null === $deliveryModel || null === $responseModel || null === $triggerModel) {
$this->friendlyError(sprintf('[b] Webhook #%d has an invalid delivery, response or trigger model. Will not upgrade.', $webhook->id));
return;
}
$webhook->webhookDeliveries()->attach([$deliveryModel->id]);
$webhook->webhookResponses()->attach([$responseModel->id]);
$webhook->webhookTriggers()->attach([$triggerModel->id]);
$webhook->delivery = 1;
$webhook->response = 1;
$webhook->trigger = 1;
$webhook->save();
$this->friendlyPositive(sprintf('Webhook #%d upgraded.', $webhook->id));
}
private function markAsExecuted(): void
{
app('fireflyconfig')->set(self::CONFIG_NAME, true);
}
}

View File

@@ -32,5 +32,6 @@ enum WebhookResponse: int
case TRANSACTIONS = 200; case TRANSACTIONS = 200;
case ACCOUNTS = 210; case ACCOUNTS = 210;
case BUDGET = 230; case BUDGET = 230;
case RELEVANT = 240;
case NONE = 220; case NONE = 220;
} }

View File

@@ -29,6 +29,7 @@ namespace FireflyIII\Enums;
*/ */
enum WebhookTrigger: int enum WebhookTrigger: int
{ {
case ANY = 50;
case STORE_TRANSACTION = 100; case STORE_TRANSACTION = 100;
case UPDATE_TRANSACTION = 110; case UPDATE_TRANSACTION = 110;
case DESTROY_TRANSACTION = 120; case DESTROY_TRANSACTION = 120;

View File

@@ -37,7 +37,7 @@ class ActuallyLoggedIn extends Event
public User $user; public User $user;
public function __construct(null|Authenticatable|User $user) public function __construct(Authenticatable|User|null $user)
{ {
if ($user instanceof User) { if ($user instanceof User) {
$this->user = $user; $this->user = $user;

View File

@@ -1,6 +1,27 @@
<?php <?php
/*
* WarnUserAboutBill.php
* Copyright (c) 2025 james@firefly-iii.org
*
* This file is part of Firefly III (https://github.com/firefly-iii).
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
declare(strict_types=1); declare(strict_types=1);
namespace FireflyIII\Events\Model\Bill; namespace FireflyIII\Events\Model\Bill;

View File

@@ -1,5 +1,26 @@
<?php <?php
/*
* WarnUserAboutOverdueSubscriptions.php
* Copyright (c) 2025 james@firefly-iii.org
*
* This file is part of Firefly III (https://github.com/firefly-iii).
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
declare(strict_types=1); declare(strict_types=1);
namespace FireflyIII\Events\Model\Bill; namespace FireflyIII\Events\Model\Bill;

View File

@@ -1,5 +1,26 @@
<?php <?php
/*
* ChangedName.php
* Copyright (c) 2025 james@firefly-iii.org
*
* This file is part of Firefly III (https://github.com/firefly-iii).
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
declare(strict_types=1); declare(strict_types=1);
namespace FireflyIII\Events\Model\PiggyBank; namespace FireflyIII\Events\Model\PiggyBank;

View File

@@ -35,7 +35,7 @@ class DisabledMFA extends Event
public User $user; public User $user;
public function __construct(null|Authenticatable|User $user) public function __construct(Authenticatable|User|null $user)
{ {
if ($user instanceof User) { if ($user instanceof User) {
$this->user = $user; $this->user = $user;

View File

@@ -35,7 +35,7 @@ class EnabledMFA extends Event
public User $user; public User $user;
public function __construct(null|Authenticatable|User $user) public function __construct(Authenticatable|User|null $user)
{ {
if ($user instanceof User) { if ($user instanceof User) {
$this->user = $user; $this->user = $user;

View File

@@ -35,7 +35,7 @@ class MFABackupFewLeft extends Event
public User $user; public User $user;
public function __construct(null|Authenticatable|User $user, public int $count) public function __construct(Authenticatable|User|null $user, public int $count)
{ {
if ($user instanceof User) { if ($user instanceof User) {
$this->user = $user; $this->user = $user;

View File

@@ -35,7 +35,7 @@ class MFABackupNoLeft extends Event
public User $user; public User $user;
public function __construct(null|Authenticatable|User $user) public function __construct(Authenticatable|User|null $user)
{ {
if ($user instanceof User) { if ($user instanceof User) {
$this->user = $user; $this->user = $user;

View File

@@ -35,7 +35,7 @@ class MFAManyFailedAttempts extends Event
public User $user; public User $user;
public function __construct(null|Authenticatable|User $user, public int $count) public function __construct(Authenticatable|User|null $user, public int $count)
{ {
if ($user instanceof User) { if ($user instanceof User) {
$this->user = $user; $this->user = $user;

View File

@@ -35,7 +35,7 @@ class MFANewBackupCodes extends Event
public User $user; public User $user;
public function __construct(null|Authenticatable|User $user) public function __construct(Authenticatable|User|null $user)
{ {
if ($user instanceof User) { if ($user instanceof User) {
$this->user = $user; $this->user = $user;

View File

@@ -35,7 +35,7 @@ class MFAUsedBackupCode extends Event
public User $user; public User $user;
public function __construct(null|Authenticatable|User $user) public function __construct(Authenticatable|User|null $user)
{ {
if ($user instanceof User) { if ($user instanceof User) {
$this->user = $user; $this->user = $user;

View File

@@ -35,7 +35,7 @@ class UserAttemptedLogin extends Event
public User $user; public User $user;
public function __construct(null|Authenticatable|User $user) public function __construct(Authenticatable|User|null $user)
{ {
if ($user instanceof User) { if ($user instanceof User) {
$this->user = $user; $this->user = $user;

View File

@@ -128,10 +128,6 @@ class GracefulNotFoundHandler extends ExceptionHandler
return redirect(route('categories.index')); return redirect(route('categories.index'));
case 'rules.edit': case 'rules.edit':
$request->session()->reflash();
return redirect(route('rules.index'));
case 'rule-groups.edit': case 'rule-groups.edit':
$request->session()->reflash(); $request->session()->reflash();
@@ -170,7 +166,7 @@ class GracefulNotFoundHandler extends ExceptionHandler
} }
/** @var null|Account $account */ /** @var null|Account $account */
$account = $user->accounts()->with(['accountType'])->withTrashed()->find($accountId); $account = $user->accounts()->withTrashed()->with(['accountType'])->find($accountId);
if (null === $account) { if (null === $account) {
app('log')->error(sprintf('Could not find account %d, so give big fat error.', $accountId)); app('log')->error(sprintf('Could not find account %d, so give big fat error.', $accountId));

View File

@@ -282,7 +282,7 @@ class PiggyBankFactory
// create event: // create event:
Log::debug('linkToAccountIds: Trigger change for positive amount [b].'); Log::debug('linkToAccountIds: Trigger change for positive amount [b].');
event(new ChangedAmount($piggyBank, $toBeLinked[$account->id]['current_amount'], null, null)); event(new ChangedAmount($piggyBank, $toBeLinked[$account->id]['current_amount'] ?? '0', null, null));
} }
if (!array_key_exists('current_amount', $info)) { if (!array_key_exists('current_amount', $info)) {
$toBeLinked[$account->id] ??= []; $toBeLinked[$account->id] ??= [];

View File

@@ -30,6 +30,7 @@ use FireflyIII\Models\Recurrence;
use FireflyIII\Services\Internal\Support\RecurringTransactionTrait; use FireflyIII\Services\Internal\Support\RecurringTransactionTrait;
use FireflyIII\Services\Internal\Support\TransactionTypeTrait; use FireflyIII\Services\Internal\Support\TransactionTypeTrait;
use FireflyIII\User; use FireflyIII\User;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\MessageBag; use Illuminate\Support\MessageBag;
/** /**
@@ -62,8 +63,8 @@ class RecurrenceFactory
$type = $this->findTransactionType(ucfirst((string) $data['recurrence']['type'])); $type = $this->findTransactionType(ucfirst((string) $data['recurrence']['type']));
} catch (FireflyException $e) { } catch (FireflyException $e) {
$message = sprintf('Cannot make a recurring transaction of type "%s"', $data['recurrence']['type']); $message = sprintf('Cannot make a recurring transaction of type "%s"', $data['recurrence']['type']);
app('log')->error($message); Log::error($message);
app('log')->error($e->getTraceAsString()); Log::error($e->getTraceAsString());
throw new FireflyException($message, 0, $e); throw new FireflyException($message, 0, $e);
} }
@@ -107,6 +108,7 @@ class RecurrenceFactory
'title' => $title, 'title' => $title,
'description' => $description, 'description' => $description,
'first_date' => $firstDate?->format('Y-m-d'), 'first_date' => $firstDate?->format('Y-m-d'),
'first_date_tz' => $firstDate?->format('e'),
'repeat_until' => $repetitions > 0 ? null : $repeatUntilString, 'repeat_until' => $repetitions > 0 ? null : $repeatUntilString,
'latest_date' => null, 'latest_date' => null,
'repetitions' => $repetitions, 'repetitions' => $repetitions,
@@ -125,8 +127,8 @@ class RecurrenceFactory
try { try {
$this->createTransactions($recurrence, $data['transactions'] ?? []); $this->createTransactions($recurrence, $data['transactions'] ?? []);
} catch (FireflyException $e) { } catch (FireflyException $e) {
app('log')->error($e->getMessage()); Log::error($e->getMessage());
app('log')->error($e->getTraceAsString()); Log::error($e->getTraceAsString());
$recurrence->forceDelete(); $recurrence->forceDelete();
$message = sprintf('Could not create recurring transaction: %s', $e->getMessage()); $message = sprintf('Could not create recurring transaction: %s', $e->getMessage());
$this->errors->add('store', $message); $this->errors->add('store', $message);

View File

@@ -26,7 +26,9 @@ namespace FireflyIII\Factory;
use FireflyIII\Exceptions\FireflyException; use FireflyIII\Exceptions\FireflyException;
use FireflyIII\Models\TransactionCurrency; use FireflyIII\Models\TransactionCurrency;
use FireflyIII\Support\Facades\Amount;
use Illuminate\Database\QueryException; use Illuminate\Database\QueryException;
use Illuminate\Support\Facades\Log;
/** /**
* Class TransactionCurrencyFactory * Class TransactionCurrencyFactory
@@ -41,14 +43,14 @@ class TransactionCurrencyFactory
$data['code'] = e($data['code']); $data['code'] = e($data['code']);
$data['symbol'] = e($data['symbol']); $data['symbol'] = e($data['symbol']);
$data['name'] = e($data['name']); $data['name'] = e($data['name']);
$data['decimal_places'] = (int) $data['decimal_places']; $data['decimal_places'] = (int)$data['decimal_places'];
// if the code already exists (deleted) // if the code already exists (deleted)
// force delete it and then create the transaction: // force delete it and then create the transaction:
$count = TransactionCurrency::withTrashed()->whereCode($data['code'])->count(); $count = TransactionCurrency::withTrashed()->whereCode($data['code'])->count();
if (1 === $count) { if (1 === $count) {
$old = TransactionCurrency::withTrashed()->whereCode($data['code'])->first(); $old = TransactionCurrency::withTrashed()->whereCode($data['code'])->first();
$old->forceDelete(); $old->forceDelete();
app('log')->warning(sprintf('Force deleted old currency with ID #%d and code "%s".', $old->id, $data['code'])); Log::warning(sprintf('Force deleted old currency with ID #%d and code "%s".', $old->id, $data['code']));
} }
try { try {
@@ -64,8 +66,8 @@ class TransactionCurrencyFactory
); );
} catch (QueryException $e) { } catch (QueryException $e) {
$result = null; $result = null;
app('log')->error(sprintf('Could not create new currency: %s', $e->getMessage())); Log::error(sprintf('Could not create new currency: %s', $e->getMessage()));
app('log')->error($e->getTraceAsString()); Log::error($e->getTraceAsString());
throw new FireflyException('400004: Could not store new currency.', 0, $e); throw new FireflyException('400004: Could not store new currency.', 0, $e);
} }
@@ -76,32 +78,33 @@ class TransactionCurrencyFactory
public function find(?int $currencyId, ?string $currencyCode): ?TransactionCurrency public function find(?int $currencyId, ?string $currencyCode): ?TransactionCurrency
{ {
$currencyCode = e($currencyCode); $currencyCode = e($currencyCode);
$currencyId = (int) $currencyId; $currencyId = (int)$currencyId;
$currency = null;
if ('' === $currencyCode && 0 === $currencyId) { if ('' === $currencyCode && 0 === $currencyId) {
app('log')->debug('Cannot find anything on empty currency code and empty currency ID!'); Log::debug('Cannot find anything on empty currency code and empty currency ID!');
return null; return null;
} }
// first by ID: // first by ID:
if ($currencyId > 0) { if ($currencyId > 0) {
$currency = TransactionCurrency::find($currencyId); try {
if (null !== $currency) { $currency = Amount::getTransactionCurrencyById($currencyId);
return $currency; } catch (FireflyException) {
Log::warning(sprintf('Currency ID is #%d but found nothing!', $currencyId));
} }
app('log')->warning(sprintf('Currency ID is %d but found nothing!', $currencyId));
} }
// then by code: // then by code:
if ('' !== $currencyCode) { if ('' !== $currencyCode && null === $currency) {
$currency = TransactionCurrency::whereCode($currencyCode)->first(); try {
if (null !== $currency) { $currency = Amount::getTransactionCurrencyByCode($currencyCode);
} catch (FireflyException) {
Log::warning(sprintf('Currency code is "%s" but found nothing!', $currencyCode));
}
}
Log::info(sprintf('Found currency #%d based on ID %d and code "%s".', $currency->id, $currencyId, $currencyCode));
return $currency; return $currency;
} }
app('log')->warning(sprintf('Currency code is %d but found nothing!', $currencyCode));
}
app('log')->warning('Found nothing for currency.');
return null;
}
} }

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