Compare commits

...

61 Commits

Author SHA1 Message Date
github-actions[bot]
b6c2d23116 Merge pull request #10838 from firefly-iii/release-1756697534
🤖 Automatically merge the PR into the develop branch.
2025-09-01 05:32:22 +02:00
JC5
2a123354f9 🤖 Auto commit for release 'develop' on 2025-09-01 2025-09-01 05:32:14 +02:00
James Cole
1e7ea4b76c Improve account list and view. 2025-08-31 19:20:02 +02:00
James Cole
d959526eb3 Fix #10837 2025-08-31 19:07:45 +02:00
James Cole
8846ee9091 Fix #10827 2025-08-30 07:51:28 +02:00
github-actions[bot]
6eb8d0fc8c Merge pull request #10826 from firefly-iii/release-1756445011
🤖 Automatically merge the PR into the develop branch.
2025-08-29 07:23:42 +02:00
JC5
1b0e16b6a5 🤖 Auto commit for release 'develop' on 2025-08-29 2025-08-29 07:23:32 +02:00
James Cole
2e4df28288 Less logging. 2025-08-29 06:51:20 +02:00
James Cole
f3b7a3015d Fix #10824 2025-08-27 19:00:12 +02:00
James Cole
5de5e08b1d Fix #10820 2025-08-26 16:05:36 +02:00
James Cole
0a116cd04c Add API autocomplete tests. 2025-08-25 17:17:51 +02:00
James Cole
fd32a692c1 Add some API tests. 2025-08-25 15:31:51 +02:00
James Cole
1ac762aba8 Merge branch 'develop' of github.com:firefly-iii/firefly-iii into develop 2025-08-25 15:31:15 +02:00
James Cole
315dc532b6 Fix #10819 2025-08-25 15:31:07 +02:00
github-actions[bot]
e19ed1be15 Merge pull request #10817 from firefly-iii/release-1756092462
🤖 Automatically merge the PR into the develop branch.
2025-08-25 05:27:50 +02:00
JC5
cbb0621fd9 🤖 Auto commit for release 'develop' on 2025-08-25 2025-08-25 05:27:42 +02:00
James Cole
049cbab861 Remove superfluous logging. 2025-08-24 20:31:12 +02:00
James Cole
28b620fb5c Remove superfluous logging. 2025-08-24 20:30:40 +02:00
James Cole
c183f91ff6 Add issue to changelog 2025-08-24 20:29:39 +02:00
github-actions[bot]
172efae41c Merge pull request #10816 from firefly-iii/release-1756060004
🤖 Automatically merge the PR into the develop branch.
2025-08-24 20:26:54 +02:00
JC5
756e857ba0 🤖 Auto commit for release 'develop' on 2025-08-24 2025-08-24 20:26:44 +02:00
James Cole
1cde7aab0c Fix #10815 2025-08-24 20:14:02 +02:00
James Cole
2d67eece5d Various code cleanup. 2025-08-24 17:14:07 +02:00
James Cole
b1f79c4c0f Fix #10814 2025-08-24 13:31:00 +02:00
James Cole
33bd2ceae8 Fix amounts in transaction show. 2025-08-24 13:30:41 +02:00
James Cole
e68850f192 Fix changelog. 2025-08-24 06:51:48 +02:00
James Cole
450ac7e6ee Fix #10813 2025-08-24 06:51:28 +02:00
James Cole
91f52b5dbc Add copyright statements. 2025-08-23 11:26:04 +02:00
James Cole
eed2405d76 Update changelog before I forget about it. 2025-08-23 11:17:05 +02:00
github-actions[bot]
c956df7790 Merge pull request #10809 from firefly-iii/release-1755940492
🤖 Automatically merge the PR into the develop branch.
2025-08-23 11:15:02 +02:00
JC5
0fdccec6a8 🤖 Auto commit for release 'develop' on 2025-08-23 2025-08-23 11:14:52 +02:00
James Cole
8ded54d7a8 Fix #10808 2025-08-23 08:56:25 +02:00
James Cole
bb1b4ca5ca Fix #10807 2025-08-23 08:54:50 +02:00
James Cole
e90d60113b Extra validation and a new config variable for #10806 2025-08-23 08:47:34 +02:00
James Cole
d95dada0e0 Update changelog for #10804 2025-08-22 20:20:07 +02:00
James Cole
8722456595 Fix #10804 2025-08-22 20:19:52 +02:00
James Cole
b5ad226451 Remove unused headers. 2025-08-22 15:45:39 +02:00
James Cole
ddb0e66651 Update changelog. 2025-08-22 15:45:05 +02:00
James Cole
e802899608 Pre-filter expenses, fixes #10803 2025-08-22 11:53:08 +02:00
James Cole
0894d3bf42 Fix #10802 2025-08-22 11:09:17 +02:00
James Cole
80bcfd3bcd Small issues to fix #5532 2025-08-22 09:52:27 +02:00
James Cole
8c410f42bd Expand changelog. 2025-08-22 09:15:34 +02:00
James Cole
7a1021dffc Add spent + earned info to category chart. 2025-08-22 09:15:13 +02:00
github-actions[bot]
63883c9a84 Merge pull request #10801 from firefly-iii/release-1755840718
🤖 Automatically merge the PR into the develop branch.
2025-08-22 07:32:06 +02:00
JC5
50d7f9d1ec 🤖 Auto commit for release 'develop' on 2025-08-22 2025-08-22 07:31:58 +02:00
James Cole
ebc7ea0eb6 Fix amount display 2025-08-22 07:28:09 +02:00
github-actions[bot]
b905efd0aa Merge pull request #10800 from firefly-iii/release-1755839053
🤖 Automatically merge the PR into the develop branch.
2025-08-22 07:04:20 +02:00
JC5
93085599b7 🤖 Auto commit for release 'develop' on 2025-08-22 2025-08-22 07:04:13 +02:00
James Cole
8a8bbaf827 Remove test webhook, no longer necessary. 2025-08-22 06:57:25 +02:00
James Cole
96a66b894a Fix #10799 2025-08-22 06:49:16 +02:00
James Cole
1ba641c279 Prevent a loop. 2025-08-21 20:31:32 +02:00
James Cole
6c5ddfcb8a Add newsletter link. 2025-08-21 20:10:53 +02:00
James Cole
65ddc246dc Merge branch 'main' into develop 2025-08-21 20:07:32 +02:00
James Cole
e4aff5ff4c Update webhook code. 2025-08-21 20:07:12 +02:00
James Cole
6ddda13c3a Fix #10794 2025-08-20 10:13:23 +02:00
James Cole
46219c4678 Fix #10791 2025-08-20 09:09:38 +02:00
James Cole
a1595d0647 Fix #10790 2025-08-20 07:26:18 +02:00
James Cole
4ee9f9bb27 Fix collection and message generation. 2025-08-20 06:38:53 +02:00
James Cole
bcaa0bddea Update message generator. 2025-08-20 06:32:25 +02:00
James Cole
01cce49070 Expand webhook API, edit and create screen. 2025-08-20 06:22:55 +02:00
James Cole
293be04d40 Expand webhooks to support multiple delivery payloads, event triggers and responses. 2025-08-19 19:35:12 +02:00
169 changed files with 4363 additions and 1813 deletions

View File

@@ -151,16 +151,16 @@
},
{
"name": "composer/semver",
"version": "3.4.3",
"version": "3.4.4",
"source": {
"type": "git",
"url": "https://github.com/composer/semver.git",
"reference": "4313d26ada5e0c4edfbd1dc481a92ff7bff91f12"
"reference": "198166618906cb2de69b95d7d47e5fa8aa1b2b95"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/composer/semver/zipball/4313d26ada5e0c4edfbd1dc481a92ff7bff91f12",
"reference": "4313d26ada5e0c4edfbd1dc481a92ff7bff91f12",
"url": "https://api.github.com/repos/composer/semver/zipball/198166618906cb2de69b95d7d47e5fa8aa1b2b95",
"reference": "198166618906cb2de69b95d7d47e5fa8aa1b2b95",
"shasum": ""
},
"require": {
@@ -212,7 +212,7 @@
"support": {
"irc": "ircs://irc.libera.chat:6697/composer",
"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": [
{
@@ -222,13 +222,9 @@
{
"url": "https://github.com/composer",
"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",
@@ -959,23 +955,23 @@
},
{
"name": "react/promise",
"version": "v3.2.0",
"version": "v3.3.0",
"source": {
"type": "git",
"url": "https://github.com/reactphp/promise.git",
"reference": "8a164643313c71354582dc850b42b33fa12a4b63"
"reference": "23444f53a813a3296c1368bb104793ce8d88f04a"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/reactphp/promise/zipball/8a164643313c71354582dc850b42b33fa12a4b63",
"reference": "8a164643313c71354582dc850b42b33fa12a4b63",
"url": "https://api.github.com/repos/reactphp/promise/zipball/23444f53a813a3296c1368bb104793ce8d88f04a",
"reference": "23444f53a813a3296c1368bb104793ce8d88f04a",
"shasum": ""
},
"require": {
"php": ">=7.1.0"
},
"require-dev": {
"phpstan/phpstan": "1.10.39 || 1.4.10",
"phpstan/phpstan": "1.12.28 || 1.4.10",
"phpunit/phpunit": "^9.6 || ^7.5"
},
"type": "library",
@@ -1020,7 +1016,7 @@
],
"support": {
"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": [
{
@@ -1028,7 +1024,7 @@
"type": "open_collective"
}
],
"time": "2024-05-24T10:39:05+00:00"
"time": "2025-08-19T18:57:03+00:00"
},
{
"name": "react/socket",
@@ -1257,16 +1253,16 @@
},
{
"name": "symfony/console",
"version": "v7.3.2",
"version": "v7.3.3",
"source": {
"type": "git",
"url": "https://github.com/symfony/console.git",
"reference": "5f360ebc65c55265a74d23d7fe27f957870158a1"
"reference": "cb0102a1c5ac3807cf3fdf8bea96007df7fdbea7"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/console/zipball/5f360ebc65c55265a74d23d7fe27f957870158a1",
"reference": "5f360ebc65c55265a74d23d7fe27f957870158a1",
"url": "https://api.github.com/repos/symfony/console/zipball/cb0102a1c5ac3807cf3fdf8bea96007df7fdbea7",
"reference": "cb0102a1c5ac3807cf3fdf8bea96007df7fdbea7",
"shasum": ""
},
"require": {
@@ -1331,7 +1327,7 @@
"terminal"
],
"support": {
"source": "https://github.com/symfony/console/tree/v7.3.2"
"source": "https://github.com/symfony/console/tree/v7.3.3"
},
"funding": [
{
@@ -1351,7 +1347,7 @@
"type": "tidelift"
}
],
"time": "2025-07-30T17:13:41+00:00"
"time": "2025-08-25T06:35:40+00:00"
},
{
"name": "symfony/deprecation-contracts",
@@ -1422,16 +1418,16 @@
},
{
"name": "symfony/event-dispatcher",
"version": "v7.3.0",
"version": "v7.3.3",
"source": {
"type": "git",
"url": "https://github.com/symfony/event-dispatcher.git",
"reference": "497f73ac996a598c92409b44ac43b6690c4f666d"
"reference": "b7dc69e71de420ac04bc9ab830cf3ffebba48191"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/497f73ac996a598c92409b44ac43b6690c4f666d",
"reference": "497f73ac996a598c92409b44ac43b6690c4f666d",
"url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/b7dc69e71de420ac04bc9ab830cf3ffebba48191",
"reference": "b7dc69e71de420ac04bc9ab830cf3ffebba48191",
"shasum": ""
},
"require": {
@@ -1482,7 +1478,7 @@
"description": "Provides tools that allow your application components to communicate with each other by dispatching events and listening to them",
"homepage": "https://symfony.com",
"support": {
"source": "https://github.com/symfony/event-dispatcher/tree/v7.3.0"
"source": "https://github.com/symfony/event-dispatcher/tree/v7.3.3"
},
"funding": [
{
@@ -1493,12 +1489,16 @@
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://github.com/nicolas-grekas",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2025-04-22T09:11:45+00:00"
"time": "2025-08-13T11:49:31+00:00"
},
{
"name": "symfony/event-dispatcher-contracts",
@@ -1716,16 +1716,16 @@
},
{
"name": "symfony/options-resolver",
"version": "v7.3.2",
"version": "v7.3.3",
"source": {
"type": "git",
"url": "https://github.com/symfony/options-resolver.git",
"reference": "119bcf13e67dbd188e5dbc74228b1686f66acd37"
"reference": "0ff2f5c3df08a395232bbc3c2eb7e84912df911d"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/options-resolver/zipball/119bcf13e67dbd188e5dbc74228b1686f66acd37",
"reference": "119bcf13e67dbd188e5dbc74228b1686f66acd37",
"url": "https://api.github.com/repos/symfony/options-resolver/zipball/0ff2f5c3df08a395232bbc3c2eb7e84912df911d",
"reference": "0ff2f5c3df08a395232bbc3c2eb7e84912df911d",
"shasum": ""
},
"require": {
@@ -1763,7 +1763,7 @@
"options"
],
"support": {
"source": "https://github.com/symfony/options-resolver/tree/v7.3.2"
"source": "https://github.com/symfony/options-resolver/tree/v7.3.3"
},
"funding": [
{
@@ -1783,11 +1783,11 @@
"type": "tidelift"
}
],
"time": "2025-07-15T11:36:08+00:00"
"time": "2025-08-05T10:16:07+00:00"
},
{
"name": "symfony/polyfill-ctype",
"version": "v1.32.0",
"version": "v1.33.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-ctype.git",
@@ -1846,7 +1846,7 @@
"portable"
],
"support": {
"source": "https://github.com/symfony/polyfill-ctype/tree/v1.32.0"
"source": "https://github.com/symfony/polyfill-ctype/tree/v1.33.0"
},
"funding": [
{
@@ -1857,6 +1857,10 @@
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://github.com/nicolas-grekas",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
@@ -1866,16 +1870,16 @@
},
{
"name": "symfony/polyfill-intl-grapheme",
"version": "v1.32.0",
"version": "v1.33.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-intl-grapheme.git",
"reference": "b9123926e3b7bc2f98c02ad54f6a4b02b91a8abe"
"reference": "380872130d3a5dd3ace2f4010d95125fde5d5c70"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/b9123926e3b7bc2f98c02ad54f6a4b02b91a8abe",
"reference": "b9123926e3b7bc2f98c02ad54f6a4b02b91a8abe",
"url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/380872130d3a5dd3ace2f4010d95125fde5d5c70",
"reference": "380872130d3a5dd3ace2f4010d95125fde5d5c70",
"shasum": ""
},
"require": {
@@ -1924,7 +1928,7 @@
"shim"
],
"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": [
{
@@ -1935,16 +1939,20 @@
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://github.com/nicolas-grekas",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2024-09-09T11:45:10+00:00"
"time": "2025-06-27T09:58:17+00:00"
},
{
"name": "symfony/polyfill-intl-normalizer",
"version": "v1.32.0",
"version": "v1.33.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-intl-normalizer.git",
@@ -2005,7 +2013,7 @@
"shim"
],
"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": [
{
@@ -2016,6 +2024,10 @@
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://github.com/nicolas-grekas",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
@@ -2025,7 +2037,7 @@
},
{
"name": "symfony/polyfill-mbstring",
"version": "v1.32.0",
"version": "v1.33.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-mbstring.git",
@@ -2086,7 +2098,7 @@
"shim"
],
"support": {
"source": "https://github.com/symfony/polyfill-mbstring/tree/v1.32.0"
"source": "https://github.com/symfony/polyfill-mbstring/tree/v1.33.0"
},
"funding": [
{
@@ -2097,6 +2109,10 @@
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://github.com/nicolas-grekas",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
@@ -2106,7 +2122,7 @@
},
{
"name": "symfony/polyfill-php80",
"version": "v1.32.0",
"version": "v1.33.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-php80.git",
@@ -2166,7 +2182,7 @@
"shim"
],
"support": {
"source": "https://github.com/symfony/polyfill-php80/tree/v1.32.0"
"source": "https://github.com/symfony/polyfill-php80/tree/v1.33.0"
},
"funding": [
{
@@ -2177,6 +2193,10 @@
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://github.com/nicolas-grekas",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
@@ -2186,7 +2206,7 @@
},
{
"name": "symfony/polyfill-php81",
"version": "v1.32.0",
"version": "v1.33.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-php81.git",
@@ -2242,7 +2262,7 @@
"shim"
],
"support": {
"source": "https://github.com/symfony/polyfill-php81/tree/v1.32.0"
"source": "https://github.com/symfony/polyfill-php81/tree/v1.33.0"
},
"funding": [
{
@@ -2253,6 +2273,10 @@
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://github.com/nicolas-grekas",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
@@ -2262,16 +2286,16 @@
},
{
"name": "symfony/process",
"version": "v7.3.0",
"version": "v7.3.3",
"source": {
"type": "git",
"url": "https://github.com/symfony/process.git",
"reference": "40c295f2deb408d5e9d2d32b8ba1dd61e36f05af"
"reference": "32241012d521e2e8a9d713adb0812bb773b907f1"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/process/zipball/40c295f2deb408d5e9d2d32b8ba1dd61e36f05af",
"reference": "40c295f2deb408d5e9d2d32b8ba1dd61e36f05af",
"url": "https://api.github.com/repos/symfony/process/zipball/32241012d521e2e8a9d713adb0812bb773b907f1",
"reference": "32241012d521e2e8a9d713adb0812bb773b907f1",
"shasum": ""
},
"require": {
@@ -2303,7 +2327,7 @@
"description": "Executes commands in sub-processes",
"homepage": "https://symfony.com",
"support": {
"source": "https://github.com/symfony/process/tree/v7.3.0"
"source": "https://github.com/symfony/process/tree/v7.3.3"
},
"funding": [
{
@@ -2314,12 +2338,16 @@
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://github.com/nicolas-grekas",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2025-04-17T09:11:12+00:00"
"time": "2025-08-18T09:42:54+00:00"
},
{
"name": "symfony/service-contracts",
@@ -2468,16 +2496,16 @@
},
{
"name": "symfony/string",
"version": "v7.3.2",
"version": "v7.3.3",
"source": {
"type": "git",
"url": "https://github.com/symfony/string.git",
"reference": "42f505aff654e62ac7ac2ce21033818297ca89ca"
"reference": "17a426cce5fd1f0901fefa9b2a490d0038fd3c9c"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/string/zipball/42f505aff654e62ac7ac2ce21033818297ca89ca",
"reference": "42f505aff654e62ac7ac2ce21033818297ca89ca",
"url": "https://api.github.com/repos/symfony/string/zipball/17a426cce5fd1f0901fefa9b2a490d0038fd3c9c",
"reference": "17a426cce5fd1f0901fefa9b2a490d0038fd3c9c",
"shasum": ""
},
"require": {
@@ -2535,7 +2563,7 @@
"utf8"
],
"support": {
"source": "https://github.com/symfony/string/tree/v7.3.2"
"source": "https://github.com/symfony/string/tree/v7.3.3"
},
"funding": [
{
@@ -2555,7 +2583,7 @@
"type": "tidelift"
}
],
"time": "2025-07-10T08:47:49+00:00"
"time": "2025-08-25T06:35:40+00:00"
}
],
"packages-dev": [],

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/).
## 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
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/).
## 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
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/).
## 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
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/).
## 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
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/).
## 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
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,
'name' => $account->name,
'name_with_balance' => $nameWithBalance,
'active' => $account->active,
'type' => $account->accountType->type,
'currency_id' => (string) $useCurrency->id,
'currency_name' => $useCurrency->name,

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,5 +1,26 @@
<?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);
namespace FireflyIII\Api\V1\Controllers\Chart;

View File

@@ -97,13 +97,14 @@ class CategoryController extends Controller
$collector = app(GroupCollectorInterface::class);
$collector->setRange($start, $end)->withAccountInformation();
$collector->setXorAccounts($accounts)->withCategoryInformation();
$collector->setTypes([TransactionTypeEnum::WITHDRAWAL->value, TransactionTypeEnum::RECONCILIATION->value]);
$collector->setTypes([TransactionTypeEnum::WITHDRAWAL->value, TransactionTypeEnum::DEPOSIT->value]);
$journals = $collector->getExtractedJournals();
/** @var array $journal */
foreach ($journals as $journal) {
// find journal:
$journalCurrencyId = (int)$journal['currency_id'];
$type = $journal['transaction_type_type'];
$currency = $currencies[$journalCurrencyId] ?? $this->currencyRepos->find($journalCurrencyId);
$currencies[$journalCurrencyId] = $currency;
$currencyId = (int)$currency->id;
@@ -111,7 +112,7 @@ class CategoryController extends Controller
$currencyCode = (string)$currency->code;
$currencySymbol = (string)$currency->symbol;
$currencyDecimalPlaces = (int)$currency->decimal_places;
$amount = Steam::positive($journal['amount']);
$amount = Steam::positive((string)$journal['amount']);
$pcAmount = null;
// overrule if necessary:
@@ -151,22 +152,36 @@ class CategoryController extends Controller
'type' => 'bar',
'entries' => [
'spent' => '0',
'earned' => '0',
],
'pc_entries' => [
'spent' => '0',
'earned' => '0',
],
];
// 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) {
$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);
// 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));
}

View File

@@ -108,12 +108,7 @@ abstract class Controller extends BaseController
{
$bag = new ParameterBag();
$page = (int)request()->get('page');
if ($page < 1) {
$page = 1;
}
if ($page > 2 ** 16) {
$page = 2 ** 16;
}
$page = min(max(1, $page), 2 ** 16);
$bag->set('page', $page);
// some date fields:
@@ -131,20 +126,16 @@ abstract class Controller extends BaseController
$obj = null;
if (null !== $date) {
try {
$obj = Carbon::parse((string)$date);
$obj = Carbon::parse((string)$date, config('app.timezone'));
} catch (InvalidFormatException $e) {
// don't care
Log::warning(
sprintf(
'Ignored invalid date "%s" in API controller parameter check: %s',
substr((string)$date, 0, 20),
$e->getMessage()
)
);
Log::warning(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);
}
}
// integer fields:
$integers = ['limit'];

View File

@@ -25,6 +25,7 @@ declare(strict_types=1);
namespace FireflyIII\Api\V1\Controllers\Models\Account;
use FireflyIII\Api\V1\Controllers\Controller;
use FireflyIII\Api\V1\Requests\Models\Account\ShowRequest;
use FireflyIII\Exceptions\FireflyException;
use FireflyIII\Models\Account;
use FireflyIII\Repositories\Account\AccountRepositoryInterface;
@@ -33,7 +34,6 @@ use FireflyIII\Support\JsonApi\Enrichments\AccountEnrichment;
use FireflyIII\Transformers\AccountTransformer;
use FireflyIII\User;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Pagination\LengthAwarePaginator;
use League\Fractal\Pagination\IlluminatePaginatorAdapter;
use League\Fractal\Resource\Collection as FractalCollection;
@@ -71,23 +71,22 @@ class ShowController extends Controller
*
* @throws FireflyException
*/
public function index(Request $request): JsonResponse
public function index(ShowRequest $request): JsonResponse
{
$manager = $this->getManager();
$type = $request->get('type') ?? 'all';
$this->parameters->set('type', $type);
$params = $request->getParameters();
$this->parameters->set('type', $params['type']);
// types to get, page size:
$types = $this->mapAccountTypes($this->parameters->get('type'));
$pageSize = $this->parameters->get('limit');
$types = $this->mapAccountTypes($params['type']);
// get list of accounts. Count it and split it.
$this->repository->resetAccountOrder();
$collection = $this->repository->getAccountsByType($types, $this->parameters->get('sort') ?? []);
$collection = $this->repository->getAccountsByType($types, $params['sort']);
$count = $collection->count();
// continue sort:
$accounts = $collection->slice(($this->parameters->get('page') - 1) * $pageSize, $pageSize);
$accounts = $collection->slice(($this->parameters->get('page') - 1) * $params['limit'], $params['limit']);
// enrich
/** @var User $admin */
@@ -98,7 +97,7 @@ class ShowController extends Controller
$accounts = $enrichment->enrich($accounts);
// 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());
/** @var AccountTransformer $transformer */

View File

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

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

@@ -32,7 +32,9 @@ use FireflyIII\Generator\Webhook\MessageGeneratorInterface;
use FireflyIII\Models\TransactionGroup;
use FireflyIII\Models\Webhook;
use FireflyIII\Repositories\Webhook\WebhookRepositoryInterface;
use FireflyIII\Support\JsonApi\Enrichments\WebhookEnrichment;
use FireflyIII\Transformers\WebhookTransformer;
use FireflyIII\User;
use Illuminate\Http\JsonResponse;
use Illuminate\Pagination\LengthAwarePaginator;
use Illuminate\Support\Collection;
@@ -90,6 +92,13 @@ class ShowController extends Controller
$paginator = new LengthAwarePaginator($webhooks, $count, $pageSize, $this->parameters->get('page'));
$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 */
$transformer = app(WebhookTransformer::class);
$transformer->setParameters($this->parameters);
@@ -117,6 +126,13 @@ class ShowController extends Controller
Log::channel('audit')->info(sprintf('User views webhook #%d.', $webhook->id));
$manager = $this->getManager();
// enrich
/** @var User $admin */
$admin = auth()->user();
$enrichment = new WebhookEnrichment();
$enrichment->setUser($admin);
$webhook = $enrichment->enrichSingle($webhook);
/** @var WebhookTransformer $transformer */
$transformer = app(WebhookTransformer::class);
$transformer->setParameters($this->parameters);
@@ -156,7 +172,7 @@ class ShowController extends Controller
$engine->generateMessages();
// trigger event to send them:
Log::debug('send event RequestedSendWebhookMessages');
Log::debug('send event RequestedSendWebhookMessages from ShowController::triggerTransaction()');
event(new RequestedSendWebhookMessages());
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\Requests\Models\Webhook\CreateRequest;
use FireflyIII\Repositories\Webhook\WebhookRepositoryInterface;
use FireflyIII\Support\JsonApi\Enrichments\WebhookEnrichment;
use FireflyIII\Transformers\WebhookTransformer;
use FireflyIII\User;
use Illuminate\Http\JsonResponse;
use Illuminate\Support\Facades\Log;
use League\Fractal\Resource\Item;
@@ -68,6 +70,15 @@ class StoreController extends Controller
}
$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();
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\Models\Webhook;
use FireflyIII\Repositories\Webhook\WebhookRepositoryInterface;
use FireflyIII\Support\JsonApi\Enrichments\WebhookEnrichment;
use FireflyIII\Transformers\WebhookTransformer;
use FireflyIII\User;
use Illuminate\Http\JsonResponse;
use Illuminate\Support\Facades\Log;
use League\Fractal\Resource\Item;
@@ -70,6 +72,13 @@ class UpdateController extends Controller
$webhook = $this->repository->update($webhook, $data);
$manager = $this->getManager();
// enrich
/** @var User $admin */
$admin = auth()->user();
$enrichment = new WebhookEnrichment();
$enrichment->setUser($admin);
$webhook = $enrichment->enrichSingle($webhook);
Log::channel('audit')->info(sprintf('User updates webhook #%d', $webhook->id), $data);
/** @var WebhookTransformer $transformer */

View File

@@ -64,6 +64,7 @@ class ChartRequest extends FormRequest
'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'))),
'period' => sprintf('nullable|in:%s', implode(',', config('firefly.valid_view_ranges'))),
'accounts' => 'nullable|array',
'accounts.*' => 'exists:accounts,id',
];

View File

@@ -0,0 +1,98 @@
<?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 Carbon\Carbon;
use FireflyIII\Models\Preference;
use FireflyIII\Support\Facades\Preferences;
use FireflyIII\Support\Http\Api\AccountFilter;
use FireflyIII\Support\Request\ConvertsDataTypes;
use FireflyIII\User;
use Illuminate\Contracts\Validation\Validator;
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();
/** @var Preference $pageSize */
$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->convertString('sort', 'order'),
'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' => 'in:active,iban,name,order,-active,-iban,-name,-order', // TODO improve me.
'type' => sprintf('in:%s', $keys),
'limit' => 'number|min:1|max:131337',
'page' => 'number|min:1|max:131337',
];
}
public function withValidator(Validator $validator): void
{
$validator->after(
function (Validator $validator): void {
if ($validator->failed()) {
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

@@ -78,7 +78,7 @@ class StoreRequest extends FormRequest
'object_group_id' => 'numeric|belongsToUser:object_groups,id',
'object_group_title' => ['min:1', 'max:255'],
'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_code' => 'exists:transaction_currencies,code|required_without:transaction_currency_id',
'target_date' => 'date|nullable|after:start_date',

View File

@@ -24,15 +24,13 @@ declare(strict_types=1);
namespace FireflyIII\Api\V1\Requests\Models\Webhook;
use FireflyIII\Enums\WebhookResponse;
use FireflyIII\Enums\WebhookTrigger;
use FireflyIII\Exceptions\FireflyException;
use FireflyIII\Models\Webhook;
use FireflyIII\Rules\IsBoolean;
use FireflyIII\Support\Request\ChecksLogin;
use FireflyIII\Support\Request\ConvertsDataTypes;
use Illuminate\Contracts\Validation\Validator;
use FireflyIII\Support\Request\ValidatesWebhooks;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Support\Facades\Log;
/**
* Class CreateRequest
@@ -41,27 +39,28 @@ class CreateRequest extends FormRequest
{
use ChecksLogin;
use ConvertsDataTypes;
use ValidatesWebhooks;
public function getData(): array
{
$triggers = Webhook::getTriggersForValidation();
$responses = Webhook::getResponsesForValidation();
$deliveries = Webhook::getDeliveriesForValidation();
$fields = [
'title' => ['title', 'convertString'],
'active' => ['active', 'boolean'],
'trigger' => ['trigger', 'convertString'],
'response' => ['response', 'convertString'],
'delivery' => ['delivery', '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['trigger'] = $triggers[$return['trigger']] ?? (int)$return['trigger'];
$return['response'] = $responses[$return['response']] ?? (int)$return['response'];
$return['delivery'] = $deliveries[$return['delivery']] ?? (int)$return['delivery'];
$return['triggers'] = $triggers;
$return['responses'] = $responses;
$return['deliveries'] = $deliveries;
return $return;
}
@@ -71,59 +70,24 @@ class CreateRequest extends FormRequest
*/
public function rules(): array
{
$triggers = implode(',', array_keys(Webhook::getTriggersForValidation()));
$responses = implode(',', array_keys(Webhook::getResponsesForValidation()));
$deliveries = implode(',', array_keys(Webhook::getDeliveriesForValidation()));
$triggers = implode(',', array_values(Webhook::getTriggers()));
$responses = implode(',', array_values(Webhook::getResponses()));
$deliveries = implode(',', array_values(Webhook::getDeliveries()));
$validProtocols = config('firefly.valid_url_protocols');
return [
'title' => 'required|min:1|max:255|uniqueObjectForUser:webhooks,title',
'active' => [new IsBoolean()],
'trigger' => sprintf('required|in:%s', $triggers),
'response' => sprintf('required|in:%s', $responses),
'delivery' => sprintf('required|in:%s', $deliveries),
'url' => ['required', sprintf('url:%s', $validProtocols), 'uniqueWebhook'],
'trigger' => 'prohibited',
'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' => ['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;
use FireflyIII\Enums\WebhookResponse;
use FireflyIII\Enums\WebhookTrigger;
use FireflyIII\Exceptions\FireflyException;
use FireflyIII\Models\Webhook;
use FireflyIII\Rules\IsBoolean;
use FireflyIII\Support\Request\ChecksLogin;
use FireflyIII\Support\Request\ConvertsDataTypes;
use Illuminate\Contracts\Validation\Validator;
use FireflyIII\Support\Request\ValidatesWebhooks;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Support\Facades\Log;
/**
* Class UpdateRequest
@@ -41,37 +39,28 @@ class UpdateRequest extends FormRequest
{
use ChecksLogin;
use ConvertsDataTypes;
use ValidatesWebhooks;
public function getData(): array
{
$triggers = Webhook::getTriggersForValidation();
$responses = Webhook::getResponsesForValidation();
$deliveries = Webhook::getDeliveriesForValidation();
$fields = [
'title' => ['title', 'convertString'],
'active' => ['active', 'boolean'],
'trigger' => ['trigger', 'convertString'],
'response' => ['response', 'convertString'],
'delivery' => ['delivery', '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);
if (array_key_exists('trigger', $return)) {
$return['trigger'] = $triggers[$return['trigger']] ?? 0;
}
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['triggers'] = $triggers;
$return['responses'] = $responses;
$return['deliveries'] = $deliveries;
return $return;
}
@@ -81,9 +70,9 @@ class UpdateRequest extends FormRequest
*/
public function rules(): array
{
$triggers = implode(',', array_keys(Webhook::getTriggersForValidation()));
$responses = implode(',', array_keys(Webhook::getResponsesForValidation()));
$deliveries = implode(',', array_keys(Webhook::getDeliveriesForValidation()));
$triggers = implode(',', array_values(Webhook::getTriggers()));
$responses = implode(',', array_values(Webhook::getResponses()));
$deliveries = implode(',', array_values(Webhook::getDeliveries()));
$validProtocols = config('firefly.valid_url_protocols');
/** @var Webhook $webhook */
@@ -92,51 +81,18 @@ class UpdateRequest extends FormRequest
return [
'title' => sprintf('min:1|max:255|uniqueObjectForUser:webhooks,title,%d', $webhook->id),
'active' => [new IsBoolean()],
'trigger' => sprintf('in:%s', $triggers),
'response' => sprintf('in:%s', $responses),
'delivery' => sprintf('in:%s', $deliveries),
'trigger' => 'prohibited',
'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)],
];
}
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

@@ -28,6 +28,7 @@ namespace FireflyIII\Casts;
use Carbon\Carbon;
use Illuminate\Contracts\Database\Eloquent\CastsAttributes;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\Log;
/**
* Class SeparateTimezoneCaster
@@ -51,6 +52,7 @@ class SeparateTimezoneCaster implements CastsAttributes
$timeZone = $attributes[sprintf('%s_tz', $key)] ?? 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

@@ -1,9 +1,9 @@
<?php
declare(strict_types=1);
/*
* 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).
*
@@ -18,9 +18,11 @@ declare(strict_types=1);
* 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/.
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
declare(strict_types=1);
namespace FireflyIII\Console\Commands\Integrity;
use FireflyIII\Console\Commands\ShowsFriendlyMessages;

View File

@@ -133,6 +133,9 @@ class OutputsInstructions extends Command
if ('03-31' === $today) {
$colors = ['bright-blue', 'bright-red', 'white', 'white', 'bright-red', 'bright-blue', 'default', 'default'];
}
if ('ru_RU' === config('firefly.default_language')) {
$colors = ['blue', 'blue', 'blue', 'yellow', 'yellow', 'yellow', 'default', 'default'];
}
$this->line(sprintf('<fg=%s> ______ _ __ _ _____ _____ _____ </>', $colors[0]));
$this->line(sprintf('<fg=%s> | ____(_) / _| | |_ _|_ _|_ _| </>', $colors[1]));
@@ -245,7 +248,31 @@ class OutputsInstructions extends Command
'Be there or forever wonder.',
'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!',
];
}
$random = random_int(0, count($lines) - 1);
if ($addQuotes) {
$this->line(sprintf(' "%s"', $lines[$random]));
return;
}
$this->line(sprintf(' %s', $lines[$random]));
}
}

View File

@@ -1,9 +1,9 @@
<?php
declare(strict_types=1);
/*
* 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).
*
@@ -18,9 +18,11 @@ declare(strict_types=1);
* 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/.
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
declare(strict_types=1);
namespace FireflyIII\Console\Commands\System;
use FireflyIII\Console\Commands\ShowsFriendlyMessages;

View File

@@ -1,5 +1,26 @@
<?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);
namespace FireflyIII\Console\Commands\System;

View File

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

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 ACCOUNTS = 210;
case BUDGET = 230;
case RELEVANT = 240;
case NONE = 220;
}

View File

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

View File

@@ -1,6 +1,27 @@
<?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);
namespace FireflyIII\Events\Model\Bill;

View File

@@ -1,5 +1,26 @@
<?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);
namespace FireflyIII\Events\Model\Bill;

View File

@@ -1,5 +1,26 @@
<?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);
namespace FireflyIII\Events\Model\PiggyBank;

View File

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

View File

@@ -34,6 +34,8 @@ use FireflyIII\Models\TransactionGroup;
use FireflyIII\Models\TransactionJournal;
use FireflyIII\Models\Webhook;
use FireflyIII\Models\WebhookMessage;
use FireflyIII\Models\WebhookResponse as WebhookResponseModel;
use FireflyIII\Models\WebhookTrigger as WebhookTriggerModel;
use FireflyIII\Support\JsonApi\Enrichments\AccountEnrichment;
use FireflyIII\Support\JsonApi\Enrichments\BudgetEnrichment;
use FireflyIII\Support\JsonApi\Enrichments\BudgetLimitEnrichment;
@@ -80,7 +82,13 @@ class StandardMessageGenerator implements MessageGeneratorInterface
private function getWebhooks(): Collection
{
return $this->user->webhooks()->where('active', true)->where('trigger', $this->trigger)->get(['webhooks.*']);
return $this->user->webhooks()
->leftJoin('webhook_webhook_trigger', 'webhook_webhook_trigger.webhook_id', 'webhooks.id')
->leftJoin('webhook_triggers', 'webhook_webhook_trigger.webhook_trigger_id', 'webhook_triggers.id')
->where('active', true)
->whereIn('webhook_triggers.title', [$this->trigger->name, WebhookTrigger::ANY->name])
->get(['webhooks.*'])
;
}
/**
@@ -119,18 +127,21 @@ class StandardMessageGenerator implements MessageGeneratorInterface
// Line is ignored because all of Firefly III's Models have an id property.
Log::debug(sprintf('Now in generateMessage(#%d, %s#%d)', $webhook->id, $class, $model->id));
$uuid = Uuid::uuid4();
/** @var WebhookResponseModel $response */
$response = $webhook->webhookResponses()->first();
$triggers = $this->getTriggerTitles($webhook->webhookTriggers()->get());
$basicMessage = [
'uuid' => $uuid->toString(),
'user_id' => 0,
'user_group_id' => 0,
'trigger' => WebhookTrigger::from((int)$webhook->trigger)->name,
'response' => WebhookResponse::from((int)$webhook->response)->name,
'trigger' => $this->trigger->name,
'response' => $response->title, // guess that the database is correct.
'url' => $webhook->url,
'version' => sprintf('v%d', $this->getVersion()),
'content' => [],
];
// depends on the model how user_id is set:
switch ($class) {
default:
// Line is ignored because all of Firefly III's Models have an id property.
@@ -142,12 +153,14 @@ class StandardMessageGenerator implements MessageGeneratorInterface
/** @var Budget $model */
$basicMessage['user_id'] = $model->user_id;
$basicMessage['user_group_id'] = $model->user_group_id;
$relevantResponse = WebhookResponse::BUDGET->name;
break;
case BudgetLimit::class:
$basicMessage['user_id'] = $model->budget->user_id;
$basicMessage['user_group_id'] = $model->budget->user_group_id;
$relevantResponse = WebhookResponse::BUDGET->name;
break;
@@ -158,15 +171,15 @@ class StandardMessageGenerator implements MessageGeneratorInterface
break;
}
$responseTitle = $this->getRelevantResponse($triggers, $response, $class);
// then depends on the response what to put in the message:
switch ($webhook->response) {
switch ($responseTitle) {
default:
Log::error(sprintf('The response code for webhook #%d is "%d" and the message generator cant handle it. Soft fail.', $webhook->id, $webhook->response));
Log::error(sprintf('The response code for webhook #%d is "%s" and the message generator cant handle it. Soft fail.', $webhook->id, $webhook->response));
return;
case WebhookResponse::BUDGET->value:
case WebhookResponse::BUDGET->name:
$basicMessage['content'] = [];
if ($model instanceof Budget) {
$enrichment = new BudgetEnrichment();
@@ -177,12 +190,6 @@ class StandardMessageGenerator implements MessageGeneratorInterface
}
if ($model instanceof BudgetLimit) {
$user = $model->budget->user;
$enrichment = new BudgetEnrichment();
$enrichment->setUser($user);
$enrichment->setStart($model->start_date);
$enrichment->setEnd($model->end_date);
$budget = $enrichment->enrichSingle($model->budget);
$enrichment = new BudgetLimitEnrichment();
$enrichment->setUser($user);
@@ -198,12 +205,12 @@ class StandardMessageGenerator implements MessageGeneratorInterface
break;
case WebhookResponse::NONE->value:
case WebhookResponse::NONE->name:
$basicMessage['content'] = [];
break;
case WebhookResponse::TRANSACTIONS->value:
case WebhookResponse::TRANSACTIONS->name:
/** @var TransactionGroup $model */
$transformer = new TransactionGroupTransformer();
@@ -220,7 +227,7 @@ class StandardMessageGenerator implements MessageGeneratorInterface
break;
case WebhookResponse::ACCOUNTS->value:
case WebhookResponse::ACCOUNTS->name:
/** @var TransactionGroup $model */
$accounts = $this->collectAccounts($model);
$enrichment = new AccountEnrichment();
@@ -287,4 +294,50 @@ class StandardMessageGenerator implements MessageGeneratorInterface
{
$this->webhooks = $webhooks;
}
private function getRelevantResponse(array $triggers, WebhookResponseModel $response, $class): string
{
// return none if none.
if (WebhookResponse::NONE->name === $response->title) {
Log::debug(sprintf('Return "%s" because requested nothing.', WebhookResponse::NONE->name));
return WebhookResponse::NONE->name;
}
if (WebhookResponse::RELEVANT->name === $response->title) {
Log::debug('Expected response is any relevant data.');
// depends on the $class
switch ($class) {
case TransactionGroup::class:
Log::debug(sprintf('Return "%s" because class is %s', WebhookResponse::TRANSACTIONS->name, $class));
return WebhookResponse::TRANSACTIONS->name;
case Budget::class:
case BudgetLimit::class:
Log::debug(sprintf('Return "%s" because class is %s', WebhookResponse::BUDGET->name, $class));
return WebhookResponse::BUDGET->name;
default:
throw new FireflyException(sprintf('Cannot deal with "relevant" if the given object is a "%s"', $class));
}
}
Log::debug(sprintf('Return response again: %s', $response->title));
return $response->title;
}
private function getTriggerTitles(Collection $collection): array
{
$return = [];
/** @var WebhookTriggerModel $item */
foreach ($collection as $item) {
$return[] = $item->title;
}
return array_unique($return);
}
}

View File

@@ -111,7 +111,7 @@ class BillEventHandler
$bill = $event->bill;
/** @var bool $preference */
Preferences::getForUser($bill->user, 'notification_bill_reminder', true)->data;
$preference = Preferences::getForUser($bill->user, 'notification_bill_reminder', true)->data;
if (true === $preference) {
Log::debug('Bill reminder is true!');

View File

@@ -55,7 +55,7 @@ class DestroyedGroupEventHandler
$engine->setObjects(new Collection([$group]));
$engine->setTrigger(WebhookTrigger::DESTROY_TRANSACTION);
$engine->generateMessages();
Log::debug(sprintf('send event RequestedSendWebhookMessages from %s', __METHOD__));
event(new RequestedSendWebhookMessages());
}

View File

@@ -121,6 +121,7 @@ class StoredGroupEventHandler
$engine->generateMessages();
// trigger event to send them:
Log::debug(sprintf('send event RequestedSendWebhookMessages from %s', __METHOD__));
event(new RequestedSendWebhookMessages());
}
}

View File

@@ -167,6 +167,7 @@ class UpdatedGroupEventHandler
$engine->setTrigger(WebhookTrigger::UPDATE_TRANSACTION);
$engine->generateMessages();
Log::debug(sprintf('send event RequestedSendWebhookMessages from %s', __METHOD__));
event(new RequestedSendWebhookMessages());
}

View File

@@ -40,7 +40,7 @@ class WebhookEventHandler
{
Log::debug(sprintf('Now in %s', __METHOD__));
if (false === config('firefly.feature_flags.webhooks') || false === config('firefly.allow_webhooks')) {
Log::info('Webhook event handler is disabled, do not run sendWebhookMessages().');
Log::debug('Webhook event handler is disabled, do not run sendWebhookMessages().');
return;
}

View File

@@ -53,6 +53,7 @@ class BudgetLimitObserver
$engine->setTrigger(WebhookTrigger::STORE_UPDATE_BUDGET_LIMIT);
$engine->generateMessages();
Log::debug(sprintf('send event RequestedSendWebhookMessages from %s', __METHOD__));
event(new RequestedSendWebhookMessages());
}
@@ -90,6 +91,7 @@ class BudgetLimitObserver
$engine->setTrigger(WebhookTrigger::STORE_UPDATE_BUDGET_LIMIT);
$engine->generateMessages();
Log::debug(sprintf('send event RequestedSendWebhookMessages from %s', __METHOD__));
event(new RequestedSendWebhookMessages());
}
}

View File

@@ -54,7 +54,7 @@ class BudgetObserver
$engine->setObjects(new Collection()->push($budget));
$engine->setTrigger(WebhookTrigger::STORE_BUDGET);
$engine->generateMessages();
Log::debug(sprintf('send event RequestedSendWebhookMessages from %s', __METHOD__));
event(new RequestedSendWebhookMessages());
}
@@ -69,7 +69,7 @@ class BudgetObserver
$engine->setObjects(new Collection()->push($budget));
$engine->setTrigger(WebhookTrigger::UPDATE_BUDGET);
$engine->generateMessages();
Log::debug(sprintf('send event RequestedSendWebhookMessages from %s', __METHOD__));
event(new RequestedSendWebhookMessages());
}
@@ -85,7 +85,7 @@ class BudgetObserver
$engine->setObjects(new Collection()->push($budget));
$engine->setTrigger(WebhookTrigger::DESTROY_BUDGET);
$engine->generateMessages();
Log::debug(sprintf('send event RequestedSendWebhookMessages from %s', __METHOD__));
event(new RequestedSendWebhookMessages());
$repository = app(AttachmentRepositoryInterface::class);

View File

@@ -72,7 +72,7 @@ class TransactionObserver
}
$transaction->saveQuietly();
Log::debug('Transaction primary currency amounts are updated.');
Log::debug(sprintf('Transaction #%d primary currency amounts are updated.', $transaction->id));
}
public function deleting(?Transaction $transaction): void

View File

@@ -861,8 +861,9 @@ class GroupCollector implements GroupCollectorInterface
return new LengthAwarePaginator($set, $this->total, $total, 1);
}
$limit = $this->limit ?? 1;
return new LengthAwarePaginator($set, $this->total, $this->limit, $this->page);
return new LengthAwarePaginator($set, $this->total, $limit, $this->page);
}
/**

View File

@@ -158,18 +158,8 @@ class ShowController extends Controller
Log::debug('End collect transactions');
$timer->stop('collection');
// enrich data in arrays.
// enrich
// $enrichment = new TransactionGroupEnrichment();
// $enrichment->setUser(auth()->user());
// $groups->setCollection($enrichment->enrich($groups->getCollection()));
$groups->setPath(route('accounts.show', [$account->id, $start->format('Y-m-d'), $end->format('Y-m-d')]));
$showAll = false;
// correct
$now = today()->endOfDay();
if ($now->gt($end) || $now->lt($start)) {
$now = $end;

View File

@@ -129,7 +129,6 @@ class IndexController extends Controller
$spent = $spentArr[$this->primaryCurrency->id]['sum'] ?? '0';
unset($spentArr);
}
// number of days for consistent budgeting.
$activeDaysPassed = $this->activeDaysPassed($start, $end); // see method description.
$activeDaysLeft = $this->activeDaysLeft($start, $end); // see method description.

View File

@@ -37,6 +37,7 @@ use FireflyIII\Repositories\Currency\CurrencyRepositoryInterface;
use FireflyIII\Support\CacheProperties;
use FireflyIII\Support\Facades\Preferences;
use FireflyIII\Support\Facades\Steam;
use FireflyIII\Support\Http\Api\ExchangeRateConverter;
use FireflyIII\Support\Http\Controllers\AugumentData;
use FireflyIII\Support\Http\Controllers\ChartGeneration;
use FireflyIII\Support\Http\Controllers\DateCalculation;
@@ -504,6 +505,7 @@ class AccountController extends Controller
Log::debug(sprintf('Step is %s', $step));
$locale = Steam::getLocale();
$return = [];
$converter = new ExchangeRateConverter();
// fix for issue https://github.com/firefly-iii/firefly-iii/issues/8041
// have to make sure this chart is always based on the balance at the END of the period.
@@ -512,10 +514,10 @@ class AccountController extends Controller
$current = app('navigation')->endOfX($current, $step, null);
$format = (string)trans('config.month_and_day_js', [], $locale);
$accountCurrency = $this->accountRepository->getAccountCurrency($account);
Log::debug('Get and filter balance for entire range start');
$range = Steam::finalAccountBalanceInRange($account, $start, $end, $this->convertToPrimary);
$range = Steam::filterAccountBalances($range, $account, $this->convertToPrimary, $accountCurrency);
Log::debug('Get and filter balance for entire range end');
// temp, get end balance.
Log::debug(sprintf('period: Call finalAccountBalance with date/time "%s"', $end->toIso8601String()));
Steam::finalAccountBalance($account, $end);
@@ -552,7 +554,15 @@ class AccountController extends Controller
$carbon = Carbon::createFromFormat('Y-m-d', $newRange[$expectedIndex]['date'])->endOfDay();
}
}
Log::debug(sprintf('momentBalance is now %s', json_encode($momentBalance)));
Log::debug(sprintf('momentBalance[%s] is now %s', $current->format('Y-m-d H:i:s'), json_encode($momentBalance)));
// check, perhaps recalculate the amount in currency X if the
if ($accountCurrency->id !== $this->primaryCurrency->id && $this->convertToPrimary && array_key_exists($accountCurrency->code, $momentBalance)) {
$converted = $converter->convert($accountCurrency, $this->primaryCurrency, $current, $momentBalance[$accountCurrency->code]);
$momentBalance['pc_balance'] = $converted;
}
$return = $this->updateChartKeys($return, $momentBalance);
$previous = $momentBalance;

View File

@@ -105,7 +105,6 @@ class EditController extends Controller
/** @var RecurrenceTransformer $transformer */
$transformer = app(RecurrenceTransformer::class);
$transformer->setParameters(new ParameterBag());
$array = $transformer->transform($recurrence);
$budgets = ExpandedForm::makeSelectListWithEmpty($this->budgetRepos->getActiveBudgets());
$bills = ExpandedForm::makeSelectListWithEmpty($this->billRepository->getActiveBills());

View File

@@ -28,8 +28,7 @@ use FireflyIII\Http\Controllers\Controller;
use FireflyIII\Http\Requests\TriggerRecurrenceRequest;
use FireflyIII\Jobs\CreateRecurringTransactions;
use FireflyIII\Models\Recurrence;
use FireflyIII\Models\TransactionGroup;
use FireflyIII\Models\TransactionJournal;
use FireflyIII\Support\Facades\Preferences;
use Illuminate\Http\RedirectResponse;
use Illuminate\Support\Collection;
@@ -58,20 +57,11 @@ class TriggerController extends Controller
app('log')->debug('Done with recurrence.');
$groups = $job->getGroups();
/** @var TransactionGroup $group */
foreach ($groups as $group) {
/** @var TransactionJournal $journal */
foreach ($group->transactionJournals as $journal) {
app('log')->debug(sprintf('Set date of journal #%d to today!', $journal->id));
$journal->date = today(config('app.timezone'));
$journal->save();
}
}
$this->repository->markGroupsAsNow($groups);
$recurrence->latest_date = $backupDate;
$recurrence->latest_date_tz = $backupDate?->format('e');
$recurrence->save();
app('preferences')->mark();
Preferences::mark();
if (0 === $groups->count()) {
$request->session()->flash('info', (string) trans('firefly.no_new_transaction_in_recurrence'));

View File

@@ -25,7 +25,6 @@ declare(strict_types=1);
namespace FireflyIII\Http\Controllers\Rule;
use Throwable;
use Carbon\Carbon;
use FireflyIII\Exceptions\FireflyException;
use FireflyIII\Http\Controllers\Controller;
use FireflyIII\Http\Requests\SelectTransactionsRequest;
@@ -74,20 +73,16 @@ class SelectController extends Controller
/** @var User $user */
$user = auth()->user();
$accounts = implode(',', $request->get('accounts'));
$startDate = new Carbon($request->get('start'));
$endDate = new Carbon($request->get('end'));
// create new rule engine:
$newRuleEngine = app(RuleEngineInterface::class);
$newRuleEngine->setUser($user);
// add extra operators:
$newRuleEngine->addOperator(['type' => 'date_after', 'value' => $startDate->format('Y-m-d')]);
$newRuleEngine->addOperator(['type' => 'date_before', 'value' => $endDate->format('Y-m-d')]);
$newRuleEngine->addOperator(['type' => 'account_id', 'value' => $accounts]);
// set rules:
$newRuleEngine->setRules(new Collection([$rule]));
$newRuleEngine->setRules(new Collection()->push($rule));
$newRuleEngine->fire();
$resultCount = $newRuleEngine->getResults();
@@ -107,11 +102,9 @@ class SelectController extends Controller
return redirect(route('rules.index'));
}
// does the user have shared accounts?
$first = session('first', today(config('app.timezone'))->subYear())->format('Y-m-d');
$today = today(config('app.timezone'))->format('Y-m-d');
$subTitle = (string) trans('firefly.apply_rule_selection', ['title' => $rule->title]);
return view('rules.rule.select-transactions', compact('first', 'today', 'rule', 'subTitle'));
return view('rules.rule.select-transactions', compact('rule', 'subTitle'));
}
/**

View File

@@ -25,11 +25,9 @@ declare(strict_types=1);
namespace FireflyIII\Http\Controllers\RuleGroup;
use Exception;
use Carbon\Carbon;
use FireflyIII\Http\Controllers\Controller;
use FireflyIII\Http\Requests\SelectTransactionsRequest;
use FireflyIII\Models\RuleGroup;
use FireflyIII\Repositories\RuleGroup\RuleGroupRepositoryInterface;
use FireflyIII\TransactionRules\Engine\RuleEngineInterface;
use FireflyIII\User;
use Illuminate\Contracts\View\Factory;
@@ -42,8 +40,6 @@ use Illuminate\View\View;
*/
class ExecutionController extends Controller
{
private RuleGroupRepositoryInterface $ruleGroupRepository;
/**
* ExecutionController constructor.
*/
@@ -56,7 +52,6 @@ class ExecutionController extends Controller
app('view')->share('title', (string) trans('firefly.rules'));
app('view')->share('mainTitleIcon', 'fa-random');
$this->ruleGroupRepository = app(RuleGroupRepositoryInterface::class);
return $next($request);
}
@@ -74,15 +69,11 @@ class ExecutionController extends Controller
/** @var User $user */
$user = auth()->user();
$accounts = implode(',', $request->get('accounts'));
$startDate = new Carbon($request->get('start'));
$endDate = new Carbon($request->get('end'));
// create new rule engine:
$newRuleEngine = app(RuleEngineInterface::class);
$newRuleEngine->setUser($user);
// add extra operators:
$newRuleEngine->addOperator(['type' => 'date_after', 'value' => $startDate->format('Y-m-d')]);
$newRuleEngine->addOperator(['type' => 'date_before', 'value' => $endDate->format('Y-m-d')]);
$newRuleEngine->addOperator(['type' => 'account_id', 'value' => $accounts]);
// set rules:
@@ -104,10 +95,8 @@ class ExecutionController extends Controller
*/
public function selectTransactions(RuleGroup $ruleGroup)
{
$first = session('first')->format('Y-m-d');
$today = today(config('app.timezone'))->format('Y-m-d');
$subTitle = (string) trans('firefly.apply_rule_group_selection', ['title' => $ruleGroup->title]);
return view('rules.rule-group.select-transactions', compact('first', 'today', 'ruleGroup', 'subTitle'));
return view('rules.rule-group.select-transactions', compact('ruleGroup', 'subTitle'));
}
}

View File

@@ -337,6 +337,9 @@ class ConvertController extends Controller
'type' => $transactionType->type,
];
$sourceTransaction = $journal->transactions()->where('amount', '<', 0)->first();
$amount = $sourceTransaction?->amount ?? '0';
// also set the currency to the currency of the source account, in case you're converting a deposit into a transfer.
if (TransactionTypeEnum::TRANSFER->value === $transactionType->type && TransactionTypeEnum::DEPOSIT->value === $journal->transactionType->type) {
$source = $this->accountRepository->find((int) $sourceId);
@@ -346,7 +349,20 @@ class ConvertController extends Controller
if ($sourceCurrency instanceof TransactionCurrency && $destCurrency instanceof TransactionCurrency && $sourceCurrency->code !== $destCurrency->code) {
$update['currency_id'] = $sourceCurrency->id;
$update['foreign_currency_id'] = $destCurrency->id;
$update['foreign_amount'] = '1'; // not the best solution but at this point the amount is hard to get.
$update['foreign_amount'] = Steam::positive($amount); // not the best solution but at this point the amount is hard to get.
}
}
// same thing for converting a withdrawal into a transfer, but with the currency of the destination account.
if (TransactionTypeEnum::TRANSFER->value === $transactionType->type && TransactionTypeEnum::WITHDRAWAL->value === $journal->transactionType->type) {
$source = $this->accountRepository->find((int) $sourceId);
$sourceCurrency = $this->accountRepository->getAccountCurrency($source);
$dest = $this->accountRepository->find((int) $destinationId);
$destCurrency = $this->accountRepository->getAccountCurrency($dest);
if ($sourceCurrency instanceof TransactionCurrency && $destCurrency instanceof TransactionCurrency && $sourceCurrency->code !== $destCurrency->code) {
$update['currency_id'] = $sourceCurrency->id;
$update['foreign_currency_id'] = $destCurrency->id;
$update['foreign_amount'] = Steam::positive($amount); // not the best solution but at this point the amount is hard to get.
}
}

View File

@@ -41,8 +41,6 @@ class SelectTransactionsRequest extends FormRequest
public function rules(): array
{
return [
'start' => 'required|date|after:1970-01-02|before:2038-01-17|before:end|required_with:end',
'end' => 'required|date|after:1970-01-02|before:2038-01-17|after:start|required_with:start',
'accounts' => 'required',
'accounts.*' => 'required|exists:accounts,id|belongsToUser:accounts',
];

View File

@@ -37,6 +37,7 @@ use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Log;
/**
* Class CreateAutoBudgetLimits
@@ -59,7 +60,7 @@ class CreateAutoBudgetLimits implements ShouldQueue
$newDate = clone $date;
$newDate->startOfDay();
$this->date = $newDate;
app('log')->debug(sprintf('Created new CreateAutoBudgetLimits("%s")', $this->date->format('Y-m-d')));
Log::debug(sprintf('Created new CreateAutoBudgetLimits("%s")', $this->date->format('Y-m-d')));
}
}
@@ -70,9 +71,9 @@ class CreateAutoBudgetLimits implements ShouldQueue
*/
public function handle(): void
{
app('log')->debug(sprintf('Now at start of CreateAutoBudgetLimits() job for %s.', $this->date->format('D d M Y')));
Log::debug(sprintf('Now at start of CreateAutoBudgetLimits() job for %s.', $this->date->format('D d M Y')));
$autoBudgets = AutoBudget::get();
app('log')->debug(sprintf('Found %d auto budgets.', $autoBudgets->count()));
Log::debug(sprintf('Found %d auto budgets.', $autoBudgets->count()));
foreach ($autoBudgets as $autoBudget) {
$this->handleAutoBudget($autoBudget);
}
@@ -84,18 +85,18 @@ class CreateAutoBudgetLimits implements ShouldQueue
private function handleAutoBudget(AutoBudget $autoBudget): void
{
if (null === $autoBudget->budget) {
app('log')->info(sprintf('Auto budget #%d is associated with a deleted budget.', $autoBudget->id));
Log::info(sprintf('Auto budget #%d is associated with a deleted budget.', $autoBudget->id));
$autoBudget->delete();
return;
}
if (false === $autoBudget->budget->active) {
app('log')->info(sprintf('Auto budget #%d is associated with an inactive budget.', $autoBudget->id));
Log::info(sprintf('Auto budget #%d is associated with an inactive budget.', $autoBudget->id));
return;
}
if (!$this->isMagicDay($autoBudget)) {
app('log')->info(
Log::info(
sprintf(
'Today (%s) is not a magic day for %s auto-budget #%d (part of budget #%d "%s")',
$this->date->format('Y-m-d'),
@@ -105,11 +106,11 @@ class CreateAutoBudgetLimits implements ShouldQueue
$autoBudget->budget->name
)
);
app('log')->debug(sprintf('Done with auto budget #%d', $autoBudget->id));
Log::debug(sprintf('Done with auto budget #%d', $autoBudget->id));
return;
}
app('log')->info(
Log::info(
sprintf(
'Today (%s) is a magic day for %s auto-budget #%d (part of budget #%d "%s")',
$this->date->format('Y-m-d'),
@@ -131,7 +132,7 @@ class CreateAutoBudgetLimits implements ShouldQueue
// that's easy: create one.
// do nothing else.
$this->createBudgetLimit($autoBudget, $start, $end);
app('log')->debug(sprintf('Done with auto budget #%d', $autoBudget->id));
Log::debug(sprintf('Done with auto budget #%d', $autoBudget->id));
return;
}
@@ -139,18 +140,18 @@ class CreateAutoBudgetLimits implements ShouldQueue
if (!$budgetLimit instanceof BudgetLimit && AutoBudgetType::AUTO_BUDGET_ROLLOVER->value === (int) $autoBudget->auto_budget_type) {
// budget limit exists already,
$this->createRollover($autoBudget);
app('log')->debug(sprintf('Done with auto budget #%d', $autoBudget->id));
Log::debug(sprintf('Done with auto budget #%d', $autoBudget->id));
return;
}
if (!$budgetLimit instanceof BudgetLimit && AutoBudgetType::AUTO_BUDGET_ADJUSTED->value === (int) $autoBudget->auto_budget_type) {
// budget limit exists already,
$this->createAdjustedLimit($autoBudget);
app('log')->debug(sprintf('Done with auto budget #%d', $autoBudget->id));
Log::debug(sprintf('Done with auto budget #%d', $autoBudget->id));
return;
}
app('log')->debug(sprintf('Done with auto budget #%d', $autoBudget->id));
Log::debug(sprintf('Done with auto budget #%d', $autoBudget->id));
}
/**
@@ -193,7 +194,7 @@ class CreateAutoBudgetLimits implements ShouldQueue
private function findBudgetLimit(Budget $budget, Carbon $start, Carbon $end): ?BudgetLimit
{
app('log')->debug(
Log::debug(
sprintf(
'Going to find a budget limit for budget #%d ("%s") between %s and %s',
$budget->id,
@@ -212,21 +213,21 @@ class CreateAutoBudgetLimits implements ShouldQueue
private function createBudgetLimit(AutoBudget $autoBudget, Carbon $start, Carbon $end, ?string $amount = null): void
{
app('log')->debug(sprintf('No budget limit exist. Must create one for auto-budget #%d', $autoBudget->id));
Log::debug(sprintf('No budget limit exist. Must create one for auto-budget #%d', $autoBudget->id));
if (null !== $amount) {
app('log')->debug(sprintf('Amount is overruled and will be set to %s', $amount));
Log::debug(sprintf('Amount is overruled and will be set to %s', $amount));
}
$budgetLimit = new BudgetLimit();
$budgetLimit->budget()->associate($autoBudget->budget);
$budgetLimit->transactionCurrency()->associate($autoBudget->transactionCurrency);
$budgetLimit->start_date = $start;
$budgetLimit->end_date = $end;
$budgetLimit->start_date = clone $start;
$budgetLimit->end_date = clone $end;
$budgetLimit->amount = $amount ?? $autoBudget->amount;
$budgetLimit->period = $autoBudget->period;
$budgetLimit->generated = 1;
$budgetLimit->save();
app('log')->debug(sprintf('Created budget limit #%d.', $budgetLimit->id));
Log::debug(sprintf('Created budget limit #%d.', $budgetLimit->id));
}
/**
@@ -234,7 +235,7 @@ class CreateAutoBudgetLimits implements ShouldQueue
*/
private function createRollover(AutoBudget $autoBudget): void
{
app('log')->debug(sprintf('Will now manage rollover for auto budget #%d', $autoBudget->id));
Log::debug(sprintf('Will now manage rollover for auto budget #%d', $autoBudget->id));
// current period:
$start = app('navigation')->startOfPeriod($this->date, $autoBudget->period);
$end = app('navigation')->endOfPeriod($start, $autoBudget->period);
@@ -243,7 +244,7 @@ class CreateAutoBudgetLimits implements ShouldQueue
$previousStart = app('navigation')->subtractPeriod($start, $autoBudget->period);
$previousEnd = app('navigation')->endOfPeriod($previousStart, $autoBudget->period);
app('log')->debug(
Log::debug(
sprintf(
'Current period is %s-%s, so previous period is %s-%s',
$start->format('Y-m-d'),
@@ -257,44 +258,44 @@ class CreateAutoBudgetLimits implements ShouldQueue
$budgetLimit = $this->findBudgetLimit($autoBudget->budget, $previousStart, $previousEnd);
if (!$budgetLimit instanceof BudgetLimit) {
app('log')->debug('No budget limit exists in previous period, so create one.');
Log::debug('No budget limit exists in previous period, so create one.');
// if not, create it and we're done.
$this->createBudgetLimit($autoBudget, $start, $end);
app('log')->debug(sprintf('Done with auto budget #%d', $autoBudget->id));
Log::debug(sprintf('Done with auto budget #%d', $autoBudget->id));
return;
}
app('log')->debug('Budget limit exists for previous period.');
Log::debug('Budget limit exists for previous period.');
// if has one, calculate expenses and use that as a base.
$repository = app(OperationsRepositoryInterface::class);
$repository->setUser($autoBudget->budget->user);
$spent = $repository->sumExpenses($previousStart, $previousEnd, null, new Collection([$autoBudget->budget]), $autoBudget->transactionCurrency);
$currencyId = $autoBudget->transaction_currency_id;
$spentAmount = $spent[$currencyId]['sum'] ?? '0';
app('log')->debug(sprintf('Spent in previous budget period (%s-%s) is %s', $previousStart->format('Y-m-d'), $previousEnd->format('Y-m-d'), $spentAmount));
Log::debug(sprintf('Spent in previous budget period (%s-%s) is %s', $previousStart->format('Y-m-d'), $previousEnd->format('Y-m-d'), $spentAmount));
// if you spent more in previous budget period, than whatever you had previous budget period, the amount resets
// previous budget limit + spent
$budgetLeft = bcadd($budgetLimit->amount, $spentAmount);
$totalAmount = $autoBudget->amount;
app('log')->debug(sprintf('Total amount left for previous budget period is %s', $budgetLeft));
Log::debug(sprintf('Total amount left for previous budget period is %s', $budgetLeft));
if (-1 !== bccomp('0', $budgetLeft)) {
app('log')->info(sprintf('The amount left is negative, so it will be reset to %s.', $totalAmount));
Log::info(sprintf('The amount left is negative, so it will be reset to %s.', $totalAmount));
}
if (1 !== bccomp('0', $budgetLeft)) {
$totalAmount = bcadd($budgetLeft, $totalAmount);
app('log')->info(sprintf('The amount left is positive, so the new amount will be %s.', $totalAmount));
Log::info(sprintf('The amount left is positive, so the new amount will be %s.', $totalAmount));
}
// create budget limit:
$this->createBudgetLimit($autoBudget, $start, $end, $totalAmount);
app('log')->debug(sprintf('Done with auto budget #%d', $autoBudget->id));
Log::debug(sprintf('Done with auto budget #%d', $autoBudget->id));
}
private function createAdjustedLimit(AutoBudget $autoBudget): void
{
app('log')->debug(sprintf('Will now manage rollover for auto budget #%d', $autoBudget->id));
Log::debug(sprintf('Will now manage rollover for auto budget #%d', $autoBudget->id));
// current period:
$start = app('navigation')->startOfPeriod($this->date, $autoBudget->period);
$end = app('navigation')->endOfPeriod($start, $autoBudget->period);
@@ -303,7 +304,7 @@ class CreateAutoBudgetLimits implements ShouldQueue
$previousStart = app('navigation')->subtractPeriod($start, $autoBudget->period);
$previousEnd = app('navigation')->endOfPeriod($previousStart, $autoBudget->period);
app('log')->debug(
Log::debug(
sprintf(
'Current period is %s-%s, so previous period is %s-%s',
$start->format('Y-m-d'),
@@ -317,13 +318,13 @@ class CreateAutoBudgetLimits implements ShouldQueue
$budgetLimit = $this->findBudgetLimit($autoBudget->budget, $previousStart, $previousEnd);
if (!$budgetLimit instanceof BudgetLimit) {
app('log')->debug('No budget limit exists in previous period, so create one.');
Log::debug('No budget limit exists in previous period, so create one.');
// if not, create standard amount, and we're done.
$this->createBudgetLimit($autoBudget, $start, $end);
return;
}
app('log')->debug('Budget limit exists for previous period.');
Log::debug('Budget limit exists for previous period.');
// if has one, calculate expenses and use that as a base.
$repository = app(OperationsRepositoryInterface::class);
@@ -331,31 +332,31 @@ class CreateAutoBudgetLimits implements ShouldQueue
$spent = $repository->sumExpenses($previousStart, $previousEnd, null, new Collection([$autoBudget->budget]), $autoBudget->transactionCurrency);
$currencyId = $autoBudget->transaction_currency_id;
$spentAmount = $spent[$currencyId]['sum'] ?? '0';
app('log')->debug(sprintf('Spent in previous budget period (%s-%s) is %s', $previousStart->format('Y-m-d'), $previousEnd->format('Y-m-d'), $spentAmount));
Log::debug(sprintf('Spent in previous budget period (%s-%s) is %s', $previousStart->format('Y-m-d'), $previousEnd->format('Y-m-d'), $spentAmount));
// what you spent in previous period PLUS the amount for the current period,
// if that is more than zero, that's the amount that will be set.
$budgetAvailable = bcadd(bcadd($budgetLimit->amount, $autoBudget->amount), $spentAmount);
$totalAmount = $autoBudget->amount;
app('log')->debug(sprintf('Total amount available for current budget period is %s', $budgetAvailable));
Log::debug(sprintf('Total amount available for current budget period is %s', $budgetAvailable));
if (-1 !== bccomp($budgetAvailable, $totalAmount)) {
app('log')->info(sprintf('There is no overspending, no need to adjust. Budget limit amount will be %s.', $budgetAvailable));
Log::info(sprintf('There is no overspending, no need to adjust. Budget limit amount will be %s.', $budgetAvailable));
// create budget limit:
$this->createBudgetLimit($autoBudget, $start, $end, $budgetAvailable);
}
if (1 !== bccomp($budgetAvailable, $totalAmount) && 1 === bccomp($budgetAvailable, '0')) {
app('log')->info(sprintf('There was overspending, so the new amount will be %s.', $budgetAvailable));
Log::info(sprintf('There was overspending, so the new amount will be %s.', $budgetAvailable));
// create budget limit:
$this->createBudgetLimit($autoBudget, $start, $end, $budgetAvailable);
}
if (1 !== bccomp($budgetAvailable, $totalAmount) && -1 === bccomp($budgetAvailable, '0')) {
app('log')->info('There was overspending, but so much even this period cant fix that. Reset it to 1.');
Log::info('There was overspending, but so much even this period cant fix that. Reset it to 1.');
// create budget limit:
$this->createBudgetLimit($autoBudget, $start, $end, '1');
}
app('log')->debug(sprintf('Done with auto budget #%d', $autoBudget->id));
Log::debug(sprintf('Done with auto budget #%d', $autoBudget->id));
}
public function setDate(Carbon $date): void

View File

@@ -24,14 +24,15 @@ declare(strict_types=1);
namespace FireflyIII\Models;
use FireflyIII\Enums\WebhookDelivery;
use FireflyIII\Enums\WebhookResponse;
use FireflyIII\Enums\WebhookTrigger;
use FireflyIII\Enums\WebhookDelivery as WebhookDeliveryEnum;
use FireflyIII\Enums\WebhookResponse as WebhookResponseEnum;
use FireflyIII\Enums\WebhookTrigger as WebhookTriggerEnum;
use FireflyIII\Support\Models\ReturnsIntegerIdTrait;
use FireflyIII\Support\Models\ReturnsIntegerUserIdTrait;
use FireflyIII\User;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\SoftDeletes;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
@@ -56,7 +57,7 @@ class Webhook extends Model
public static function getDeliveries(): array
{
$array = [];
$set = WebhookDelivery::cases();
$set = WebhookDeliveryEnum::cases();
foreach ($set as $item) {
$array[$item->value] = $item->name;
}
@@ -67,7 +68,7 @@ class Webhook extends Model
public static function getDeliveriesForValidation(): array
{
$array = [];
$set = WebhookDelivery::cases();
$set = WebhookDeliveryEnum::cases();
foreach ($set as $item) {
$array[$item->name] = $item->value;
$array[$item->value] = $item->value;
@@ -79,7 +80,7 @@ class Webhook extends Model
public static function getResponses(): array
{
$array = [];
$set = WebhookResponse::cases();
$set = WebhookResponseEnum::cases();
foreach ($set as $item) {
$array[$item->value] = $item->name;
}
@@ -90,7 +91,7 @@ class Webhook extends Model
public static function getResponsesForValidation(): array
{
$array = [];
$set = WebhookResponse::cases();
$set = WebhookResponseEnum::cases();
foreach ($set as $item) {
$array[$item->name] = $item->value;
$array[$item->value] = $item->value;
@@ -102,7 +103,7 @@ class Webhook extends Model
public static function getTriggers(): array
{
$array = [];
$set = WebhookTrigger::cases();
$set = WebhookTriggerEnum::cases();
foreach ($set as $item) {
$array[$item->value] = $item->name;
}
@@ -113,7 +114,7 @@ class Webhook extends Model
public static function getTriggersForValidation(): array
{
$array = [];
$set = WebhookTrigger::cases();
$set = WebhookTriggerEnum::cases();
foreach ($set as $item) {
$array[$item->name] = $item->value;
$array[$item->value] = $item->value;
@@ -155,6 +156,21 @@ class Webhook extends Model
return $this->hasMany(WebhookMessage::class);
}
public function webhookDeliveries(): BelongsToMany
{
return $this->belongsToMany(WebhookDelivery::class);
}
public function webhookResponses(): BelongsToMany
{
return $this->belongsToMany(WebhookResponse::class);
}
public function webhookTriggers(): BelongsToMany
{
return $this->belongsToMany(WebhookTrigger::class);
}
protected function casts(): array
{
return [

View File

@@ -0,0 +1,47 @@
<?php
/*
* WebhookDelivery.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\Models;
use FireflyIII\Support\Models\ReturnsIntegerIdTrait;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Model;
class WebhookDelivery extends Model
{
use ReturnsIntegerIdTrait;
/**
* Get the ID
*
* @SuppressWarnings("PHPMD.ShortMethodName")
*/
protected function key(): Attribute
{
return Attribute::make(
get: static fn ($value) => (int) $value,
);
}
}

View File

@@ -0,0 +1,47 @@
<?php
/*
* WebhookResponse.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\Models;
use FireflyIII\Support\Models\ReturnsIntegerIdTrait;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Model;
class WebhookResponse extends Model
{
use ReturnsIntegerIdTrait;
/**
* Get the ID
*
* @SuppressWarnings("PHPMD.ShortMethodName")
*/
protected function key(): Attribute
{
return Attribute::make(
get: static fn ($value) => (int) $value,
);
}
}

View File

@@ -0,0 +1,47 @@
<?php
/*
* WebhookTrigger.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\Models;
use FireflyIII\Support\Models\ReturnsIntegerIdTrait;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Model;
class WebhookTrigger extends Model
{
use ReturnsIntegerIdTrait;
/**
* Get the ID
*
* @SuppressWarnings("PHPMD.ShortMethodName")
*/
protected function key(): Attribute
{
return Attribute::make(
get: static fn ($value) => (int) $value,
);
}
}

View File

@@ -1,5 +1,26 @@
<?php
/*
* SubscriptionsOverdueReminder.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\Notifications\User;

View File

@@ -268,7 +268,7 @@ class BudgetRepository implements BudgetRepositoryInterface, UserGroupInterface
*/
foreach ($budgets as $index => $budget) {
$budget->order = $index + 1;
$budget->save();
$budget->saveQuietly();
}
// other budgets, set to 0.
$this->user->budgets()->where('active', 0)->update(['order' => 0]);

View File

@@ -35,6 +35,7 @@ use FireflyIII\Models\RecurrenceMeta;
use FireflyIII\Models\RecurrenceRepetition;
use FireflyIII\Models\RecurrenceTransaction;
use FireflyIII\Models\RecurrenceTransactionMeta;
use FireflyIII\Models\TransactionGroup;
use FireflyIII\Models\TransactionJournal;
use FireflyIII\Models\TransactionJournalMeta;
use FireflyIII\Services\Internal\Destroy\RecurrenceDestroyService;
@@ -582,4 +583,17 @@ class RecurringRepository implements RecurringRepositoryInterface, UserGroupInte
return $service->update($recurrence, $data);
}
public function markGroupsAsNow(Collection $groups): void
{
/** @var TransactionGroup $group */
foreach ($groups as $group) {
/** @var TransactionJournal $journal */
foreach ($group->transactionJournals as $journal) {
Log::debug(sprintf('Set date of journal #%d to today!', $journal->id));
$journal->date = now(config('app.timezone'));
$journal->save();
}
}
}
}

View File

@@ -139,6 +139,8 @@ interface RecurringRepositoryInterface
*/
public function getXOccurrencesSince(RecurrenceRepetition $repetition, Carbon $date, Carbon $afterDate, int $count): array;
public function markGroupsAsNow(Collection $groups): void;
/**
* Parse the repetition in a string that is user readable.
*/

View File

@@ -24,9 +24,13 @@ declare(strict_types=1);
namespace FireflyIII\Repositories\Webhook;
use FireflyIII\Exceptions\FireflyException;
use FireflyIII\Models\Webhook;
use FireflyIII\Models\WebhookAttempt;
use FireflyIII\Models\WebhookDelivery;
use FireflyIII\Models\WebhookMessage;
use FireflyIII\Models\WebhookResponse;
use FireflyIII\Models\WebhookTrigger;
use FireflyIII\Support\Repositories\UserGroup\UserGroupInterface;
use FireflyIII\Support\Repositories\UserGroup\UserGroupTrait;
use Illuminate\Support\Collection;
@@ -41,11 +45,20 @@ class WebhookRepository implements WebhookRepositoryInterface, UserGroupInterfac
public function all(): Collection
{
return $this->user->webhooks()->get();
return $this->user->webhooks()
// only get upgraded webhooks
->where('delivery', 1)
->where('response', 1)
->where('trigger', 1)
->get()
;
}
public function destroy(Webhook $webhook): void
{
// force delete all messages and attempts:
$webhook->webhookMessages()->delete();
$webhook->delete();
}
@@ -93,32 +106,102 @@ class WebhookRepository implements WebhookRepositoryInterface, UserGroupInterfac
'user_group_id' => $this->user->user_group_id,
'active' => $data['active'] ?? false,
'title' => $data['title'] ?? null,
'trigger' => $data['trigger'],
'response' => $data['response'],
'delivery' => $data['delivery'],
// 'trigger' => $data['trigger'],
// 'response' => $data['response'],
// 'delivery' => $data['delivery'],
'trigger' => 1,
'response' => 1,
'delivery' => 1,
'secret' => $secret,
'url' => $data['url'],
];
return Webhook::create($fullData);
/** @var Webhook $webhook */
$webhook = Webhook::create($fullData);
$triggers = new Collection();
$responses = new Collection();
$deliveries = new Collection();
foreach ($data['triggers'] as $trigger) {
// get the relevant ID:
$object = WebhookTrigger::where('title', $trigger)->first();
if (null === $object) {
throw new FireflyException(sprintf('Could not find webhook trigger with title "%s".', $trigger));
}
$triggers->push($object);
}
$webhook->webhookTriggers()->saveMany($triggers);
foreach ($data['responses'] as $response) {
// get the relevant ID:
$object = WebhookResponse::where('title', $response)->first();
if (null === $object) {
throw new FireflyException(sprintf('Could not find webhook response with title "%s".', $response));
}
$responses->push($object);
}
$webhook->webhookResponses()->saveMany($responses);
foreach ($data['deliveries'] as $delivery) {
// get the relevant ID:
$object = WebhookDelivery::where('title', $delivery)->first();
if (null === $object) {
throw new FireflyException(sprintf('Could not find webhook delivery with title "%s".', $delivery));
}
$deliveries->push($object);
}
$webhook->webhookDeliveries()->saveMany($deliveries);
return $webhook;
}
public function update(Webhook $webhook, array $data): Webhook
{
$webhook->active = $data['active'] ?? $webhook->active;
$webhook->trigger = $data['trigger'] ?? $webhook->trigger;
$webhook->response = $data['response'] ?? $webhook->response;
$webhook->delivery = $data['delivery'] ?? $webhook->delivery;
$webhook->title = $data['title'] ?? $webhook->title;
$webhook->url = $data['url'] ?? $webhook->url;
if (true === $data['secret']) {
if (array_key_exists('secret', $data) && true === $data['secret']) {
$secret = Str::random(24);
$webhook->secret = $secret;
}
$webhook->save();
$triggers = new Collection();
$responses = new Collection();
$deliveries = new Collection();
foreach ($data['triggers'] as $trigger) {
// get the relevant ID:
$object = WebhookTrigger::where('title', $trigger)->first();
if (null === $object) {
throw new FireflyException(sprintf('Could not find webhook trigger with title "%s".', $trigger));
}
$triggers->push($object);
}
$webhook->webhookTriggers()->sync($triggers);
foreach ($data['responses'] as $response) {
// get the relevant ID:
$object = WebhookResponse::where('title', $response)->first();
if (null === $object) {
throw new FireflyException(sprintf('Could not find webhook response with title "%s".', $response));
}
$responses->push($object);
}
$webhook->webhookResponses()->sync($responses);
foreach ($data['deliveries'] as $delivery) {
// get the relevant ID:
$object = WebhookDelivery::where('title', $delivery)->first();
if (null === $object) {
throw new FireflyException(sprintf('Could not find webhook delivery with title "%s".', $delivery));
}
$deliveries->push($object);
}
$webhook->webhookDeliveries()->sync($deliveries);
return $webhook;
}
}

View File

@@ -52,6 +52,7 @@ class EitherConfigKey
'firefly.languages',
'app.timezone',
'firefly.valid_view_ranges',
'firefly.preselected_accounts',
// triggers and actions:
'firefly.rule-actions',

View File

@@ -103,7 +103,7 @@ class ExchangeRateConverter
// find in cache
if (null !== $res) {
Log::debug(sprintf('ExchangeRateConverter: Return cached rate from %s to %s on %s.', $from->code, $to->code, $date->format('Y-m-d')));
Log::debug(sprintf('ExchangeRateConverter: Return cached rate (%s) from %s to %s on %s.', $res, $from->code, $to->code, $date->format('Y-m-d')));
return $res;
}

View File

@@ -249,14 +249,14 @@ class AccountEnrichment implements EnrichmentInterface
'opening_balance_date' => null,
'opening_balance_amount' => null,
'account_number' => null,
'notes' => $notes[$id] ?? null,
'notes' => $this->notes[$id] ?? null,
'last_activity' => $this->lastActivities[$id] ?? null,
];
// add object group if available
if (array_key_exists($id, $this->mappedObjects)) {
$key = $this->mappedObjects[$id];
$meta['object_group_id'] = $this->objectGroups[$key]['id'];
$meta['object_group_id'] = (string) $this->objectGroups[$key]['id'];
$meta['object_group_title'] = $this->objectGroups[$key]['title'];
$meta['object_group_order'] = $this->objectGroups[$key]['order'];
}
@@ -327,6 +327,7 @@ class AccountEnrichment implements EnrichmentInterface
$openingBalance = null;
$pcOpeningBalance = null;
}
$meta['current_balance_date'] = $this->getDate();
$meta['balances'] = [
'current_balance' => $currentBalance,
'pc_current_balance' => $pcCurrentBalance,
@@ -378,13 +379,17 @@ class AccountEnrichment implements EnrichmentInterface
public function setDate(?Carbon $date): void
{
if (null !== $date) {
$date->endOfDay();
Log::debug(sprintf('Date is now %s', $date->toW3cString()));
}
$this->date = $date;
}
public function getDate(): Carbon
{
if (null === $this->date) {
return today();
return now();
}
return $this->date;

View File

@@ -1,5 +1,26 @@
<?php
/*
* BudgetEnrichment.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\Support\JsonApi\Enrichments;
@@ -117,7 +138,7 @@ class BudgetEnrichment implements EnrichmentInterface
// add object group if available
if (array_key_exists($id, $this->mappedObjects)) {
$key = $this->mappedObjects[$id];
$meta['object_group_id'] = $this->objectGroups[$key]['id'];
$meta['object_group_id'] = (string) $this->objectGroups[$key]['id'];
$meta['object_group_title'] = $this->objectGroups[$key]['title'];
$meta['object_group_order'] = $this->objectGroups[$key]['order'];
}

View File

@@ -1,5 +1,26 @@
<?php
/*
* BudgetLimitEnrichment.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\Support\JsonApi\Enrichments;
@@ -135,6 +156,7 @@ class BudgetLimitEnrichment implements EnrichmentInterface
/** @var BudgetLimit $budgetLimit */
foreach ($this->collection as $budgetLimit) {
$id = (int)$budgetLimit->id;
$filteredExpenses = $this->filterToBudget($expenses, $budgetLimit->budget_id);
$filteredExpenses = $repository->sumCollectedExpenses($expenses, $budgetLimit->start_date, $budgetLimit->end_date, $budgetLimit->transactionCurrency, false);
$this->expenses[$id] = array_values($filteredExpenses);
@@ -175,4 +197,11 @@ class BudgetLimitEnrichment implements EnrichmentInterface
}, $first);
}, $this->expenses);
}
private function filterToBudget(array $expenses, int $budget): array
{
return array_filter($expenses, function (array $item) use ($budget) {
return (int)$item['budget_id'] === $budget;
});
}
}

View File

@@ -1,5 +1,26 @@
<?php
/*
* CategoryEnrichment.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\Support\JsonApi\Enrichments;

View File

@@ -1,5 +1,26 @@
<?php
/*
* PiggyBankEnrichment.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\Support\JsonApi\Enrichments;
@@ -105,7 +126,7 @@ class PiggyBankEnrichment implements EnrichmentInterface
'pc_current_amount' => '0',
];
}
$this->amounts[$id][$accountId]['current_amount'] = bcadd($this->amounts[$id][$accountId]['current_amount'], $item->current_amount);
$this->amounts[$id][$accountId]['current_amount'] = bcadd($this->amounts[$id][$accountId]['current_amount'], (string) $item->current_amount);
if (null !== $this->amounts[$id][$accountId]['pc_current_amount'] && null !== $item->native_current_amount) {
$this->amounts[$id][$accountId]['pc_current_amount'] = bcadd($this->amounts[$id][$accountId]['pc_current_amount'], $item->native_current_amount);
}

View File

@@ -1,5 +1,26 @@
<?php
/*
* PiggyBankEventEnrichment.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\Support\JsonApi\Enrichments;

View File

@@ -1,5 +1,26 @@
<?php
/*
* RecurringEnrichment.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\Support\JsonApi\Enrichments;
@@ -38,7 +59,7 @@ class RecurringEnrichment implements EnrichmentInterface
private Collection $collection;
private array $ids = [];
private array $transactionTypeIds = [];
private array $transactionTypes = [];
// private array $transactionTypes = [];
private array $notes = [];
private array $repetitions = [];
private array $transactions = [];
@@ -110,11 +131,11 @@ class RecurringEnrichment implements EnrichmentInterface
$this->ids = array_unique($this->ids);
// collect transaction types.
$transactionTypes = TransactionType::whereIn('id', array_unique($this->transactionTypeIds))->get();
foreach ($transactionTypes as $transactionType) {
$id = (int)$transactionType->id;
$this->transactionTypes[$id] = TransactionTypeEnum::from($transactionType->type);
}
// $transactionTypes = TransactionType::whereIn('id', array_unique($this->transactionTypeIds))->get();
// foreach ($transactionTypes as $transactionType) {
// $id = (int)$transactionType->id;
// $this->transactionTypes[$id] = TransactionTypeEnum::from($transactionType->type);
// }
}
private function collectRepetitions(): void
@@ -129,7 +150,7 @@ class RecurringEnrichment implements EnrichmentInterface
$recurrence = $this->collection->filter(function (Recurrence $item) use ($repetition) {
return (int)$item->id === (int)$repetition->recurrence_id;
})->first();
$fromDate = $recurrence->latest_date ?? $recurrence->first_date;
$fromDate = clone ($recurrence->latest_date ?? $recurrence->first_date);
$id = (int)$repetition->recurrence_id;
$repId = (int)$repetition->id;
$this->repetitions[$id] ??= [];
@@ -137,6 +158,7 @@ class RecurringEnrichment implements EnrichmentInterface
// get the (future) occurrences for this specific type of repetition:
$amount = 'daily' === $repetition->repetition_type ? 9 : 5;
$set = $repository->getXOccurrencesSince($repetition, $fromDate, now(config('app.timezone')), $amount);
$occurrences = [];
/** @var Carbon $carbon */
foreach ($set as $carbon) {
@@ -378,7 +400,7 @@ class RecurringEnrichment implements EnrichmentInterface
private function collectTransactionMetaData(): void
{
$ids = array_keys($this->transactions);
$meta = RecurrenceTransactionMeta::whereIn('rt_id', $ids)->get();
$meta = RecurrenceTransactionMeta::whereNull('deleted_at')->whereIn('rt_id', $ids)->get();
// other meta-data to be collected:
$billIds = [];
$piggyBankIds = [];
@@ -388,8 +410,15 @@ class RecurringEnrichment implements EnrichmentInterface
foreach ($meta as $entry) {
$id = (int)$entry->id;
$transactionId = (int)$entry->rt_id;
$recurrenceId = $this->recurrenceIds[$transactionId];
$name = (string)$entry->name;
// this should refer to another array, were rtIds can be used to find the recurrence.
$recurrenceId = $this->recurrenceIds[$transactionId] ?? 0;
$name = (string)$entry->name ?? '';
if (0 === $recurrenceId) {
Log::error(sprintf('Could not find recurrence ID for recurrence transaction ID %d', $transactionId));
continue;
}
switch ($name) {
default:

View File

@@ -1,5 +1,26 @@
<?php
/*
* SubscriptionEnrichment.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\Support\JsonApi\Enrichments;
@@ -101,7 +122,7 @@ class SubscriptionEnrichment implements EnrichmentInterface
// add object group if available
if (array_key_exists($id, $this->mappedObjects)) {
$key = $this->mappedObjects[$id];
$meta['object_group_id'] = $objectGroups[$key]['id'];
$meta['object_group_id'] = (string) $objectGroups[$key]['id'];
$meta['object_group_title'] = $objectGroups[$key]['title'];
$meta['object_group_order'] = $objectGroups[$key]['order'];
}
@@ -362,7 +383,7 @@ class SubscriptionEnrichment implements EnrichmentInterface
Log::debug(sprintf('[b] Last paid date is: %s', $return->format('Y-m-d')));
}
}
Log::debug(sprintf('[c] Last paid date is: "%s"', $return?->format('Y-m-d')));
// Log::debug(sprintf('[c] Last paid date is: "%s"', $return?->format('Y-m-d')));
return $return;
}

View File

@@ -143,9 +143,9 @@ class TransactionGroupEnrichment implements EnrichmentInterface
continue;
}
if (in_array($name, $this->dateFields, true)) {
Log::debug(sprintf('Meta data for "%s" is a date : "%s"', $name, $data));
// Log::debug(sprintf('Meta data for "%s" is a date : "%s"', $name, $data));
$this->metaData[$entry['transaction_journal_id']][$name] = Carbon::parse($data, config('app.timezone'));
Log::debug(sprintf('Meta data for "%s" converts to: "%s"', $name, $this->metaData[$entry['transaction_journal_id']][$name]->toW3CString()));
// Log::debug(sprintf('Meta data for "%s" converts to: "%s"', $name, $this->metaData[$entry['transaction_journal_id']][$name]->toW3CString()));
continue;
}

View File

@@ -0,0 +1,164 @@
<?php
/*
* WebhookEnrichment.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\Support\JsonApi\Enrichments;
use FireflyIII\Enums\WebhookDelivery as WebhookDeliveryEnum;
use FireflyIII\Enums\WebhookResponse as WebhookResponseEnum;
use FireflyIII\Enums\WebhookTrigger as WebhookTriggerEnum;
use FireflyIII\Models\UserGroup;
use FireflyIII\Models\Webhook;
use FireflyIII\Models\WebhookDelivery;
use FireflyIII\Models\WebhookResponse;
use FireflyIII\Models\WebhookTrigger;
use FireflyIII\User;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
use stdClass;
class WebhookEnrichment implements EnrichmentInterface
{
private Collection $collection;
private User $user;
private UserGroup $userGroup;
private array $ids = [];
private array $deliveries = [];
private array $responses = [];
private array $triggers = [];
private array $webhookDeliveries = [];
private array $webhookResponses = [];
private array $webhookTriggers = [];
public function enrich(Collection $collection): Collection
{
$this->collection = $collection;
if ($this->collection->count() > 0) {
$this->collectIds();
$this->collectInfo();
$this->collectWebhookInfo();
$this->appendCollectedInfo();
}
return $this->collection;
}
public function enrichSingle(array|Model $model): array|Model
{
Log::debug(__METHOD__);
$collection = new Collection([$model]);
$collection = $this->enrich($collection);
return $collection->first();
}
public function setUser(User $user): void
{
$this->user = $user;
}
public function setUserGroup(UserGroup $userGroup): void
{
$this->userGroup = $userGroup;
}
private function collectIds(): void
{
/** @var Webhook $webhook */
foreach ($this->collection as $webhook) {
$this->ids[] = $webhook->id;
}
$this->ids = array_unique($this->ids);
}
private function collectInfo(): void
{
$all = WebhookDelivery::get();
/** @var WebhookDelivery $item */
foreach ($all as $item) {
$this->deliveries[$item->id] = $item->key;
}
$all = WebhookResponse::get();
/** @var WebhookResponse $item */
foreach ($all as $item) {
$this->responses[$item->id] = $item->key;
}
$all = WebhookTrigger::get();
/** @var WebhookTrigger $item */
foreach ($all as $item) {
$this->triggers[$item->id] = $item->key;
}
}
private function collectWebhookInfo(): void
{
$set = DB::table('webhook_webhook_delivery')->whereIn('webhook_id', $this->ids)->get(['webhook_id', 'webhook_delivery_id']);
/** @var stdClass $item */
foreach ($set as $item) {
$id = $item->webhook_id;
$deliveryId = $item->webhook_delivery_id;
$this->webhookDeliveries[$id][] = WebhookDeliveryEnum::from($this->deliveries[$deliveryId])->name;
}
$set = DB::table('webhook_webhook_response')->whereIn('webhook_id', $this->ids)->get(['webhook_id', 'webhook_response_id']);
/** @var stdClass $item */
foreach ($set as $item) {
$id = $item->webhook_id;
$responseId = $item->webhook_response_id;
$this->webhookResponses[$id][] = WebhookResponseEnum::from($this->responses[$responseId])->name;
}
$set = DB::table('webhook_webhook_trigger')->whereIn('webhook_id', $this->ids)->get(['webhook_id', 'webhook_trigger_id']);
/** @var stdClass $item */
foreach ($set as $item) {
$id = $item->webhook_id;
$triggerId = $item->webhook_trigger_id;
$this->webhookTriggers[$id][] = WebhookTriggerEnum::from($this->triggers[$triggerId])->name;
}
}
private function appendCollectedInfo(): void
{
$this->collection = $this->collection->map(function (Webhook $item) {
$meta = [
'deliveries' => $this->webhookDeliveries[$item->id] ?? [],
'responses' => $this->webhookResponses[$item->id] ?? [],
'triggers' => $this->webhookTriggers[$item->id] ?? [],
];
$item->meta = $meta;
return $item;
});
}
}

View File

@@ -1,5 +1,26 @@
<?php
/*
* RecalculatesAvailableBudgetsTrait.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\Support\Observers;

View File

@@ -255,7 +255,7 @@ trait ConvertsDataTypes
if (10 === strlen((string) $value)) {
// probably a date format.
try {
$carbon = Carbon::createFromFormat('Y-m-d', $value);
$carbon = Carbon::createFromFormat('Y-m-d', $value, config('app.timezone'));
} catch (InvalidDateException $e) { // @phpstan-ignore-line
Log::error(sprintf('[1] "%s" is not a valid date: %s', $value, $e->getMessage()));
@@ -276,7 +276,7 @@ trait ConvertsDataTypes
// is an atom string, I hope?
try {
$carbon = Carbon::parse($value);
$carbon = Carbon::parse($value, $value, config('app.timezone'));
} catch (InvalidDateException $e) { // @phpstan-ignore-line
Log::error(sprintf('[3] "%s" is not a valid date or time: %s', $value, $e->getMessage()));

View File

@@ -0,0 +1,95 @@
<?php
/*
* ValidatesWebhooks.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\Support\Request;
use FireflyIII\Enums\WebhookTrigger;
use FireflyIII\Models\Webhook;
use Illuminate\Contracts\Validation\Validator;
use Illuminate\Support\Facades\Log;
trait ValidatesWebhooks
{
public function withValidator(Validator $validator): void
{
$validator->after(
function (Validator $validator): void {
Log::debug('Validating webhook');
if ($validator->failed()) {
return;
}
$data = $validator->getData();
$triggers = $data['triggers'] ?? [];
$responses = $data['responses'] ?? [];
if (0 === count($triggers) || 0 === count($responses)) {
Log::debug('No trigger or response, return.');
return;
}
$validTriggers = array_values(Webhook::getTriggers());
$validResponses = array_values(Webhook::getResponses());
$containsAny = false;
$count = 0;
foreach ($triggers as $trigger) {
if (!in_array($trigger, $validTriggers, true)) {
return;
}
++$count;
if ($trigger === WebhookTrigger::ANY->name) {
$containsAny = true;
}
}
if ($containsAny && $count > 1) {
$validator->errors()->add('triggers.0', trans('validation.only_any_trigger'));
return;
}
foreach ($responses as $response) {
if (!in_array($response, $validResponses, true)) {
return;
}
}
// some combinations are illegal.
foreach ($triggers as $i => $trigger) {
$forbidden = config(sprintf('webhooks.forbidden_responses.%s', $trigger));
if (null === $forbidden) {
$validator->errors()->add(sprintf('triggers.%d', $i), trans('validation.unknown_webhook_trigger', ['trigger' => $trigger]));
continue;
}
foreach ($responses as $ii => $response) {
if (in_array($response, $forbidden, true)) {
Log::debug(sprintf('Trigger %s and response %s are forbidden.', $trigger, $response));
$validator->errors()->add(sprintf('responses.%d', $ii), trans('validation.bad_webhook_combination', ['trigger' => $trigger, 'response' => $response]));
return;
}
}
}
}
);
}
}

View File

@@ -1,5 +1,26 @@
<?php
/*
* PreferencesSingleton.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\Support\Singleton;

View File

@@ -224,6 +224,7 @@ class Steam
*/
$request = clone $start;
$request->subDay()->endOfDay();
Log::debug('Get first balance to start.');
Log::debug(sprintf('finalAccountBalanceInRange: Call finalAccountBalance with date/time "%s"', $request->toIso8601String()));
$startBalance = $this->finalAccountBalance($account, $request);
$primaryCurrency = Amount::getPrimaryCurrencyByUserGroup($account->user->userGroup);
@@ -315,7 +316,7 @@ class Steam
Log::debug(sprintf('Updated entry [%s]', $carbonKey), $currentBalance);
}
$cache->store($balances);
Log::debug('End of method');
Log::debug('End of method finalAccountBalanceInRange');
return $balances;
}

View File

@@ -78,10 +78,6 @@ class AccountTransformer extends AbstractTransformer
$zoomLevel = $account->meta['location']['zoom_level'] ?? null;
$order = $account->order;
// date (for balance etc.)
$date = $this->getDate();
$date->endOfDay();
// get primary currency as fallback:
$currency = $this->primary; // assume primary currency
if ($hasCurrencySettings) {
@@ -141,7 +137,7 @@ class AccountTransformer extends AbstractTransformer
'debt_amount' => $account->meta['balances']['debt_amount'],
'pc_debt_amount' => $account->meta['balances']['pc_debt_amount'],
'current_balance_date' => $date->toAtomString(),
'current_balance_date' => $account->meta['current_balance_date']->toAtomString(),
'notes' => $account->meta['notes'] ?? null,
'monthly_payment_date' => $monthlyPaymentDate,
'credit_card_type' => $creditCardType,

View File

@@ -51,9 +51,12 @@ class WebhookTransformer extends AbstractTransformer
'active' => $webhook->active,
'title' => $webhook->title,
'secret' => $webhook->secret,
'trigger' => $this->getEnum('trigger', $webhook->trigger),
'response' => $this->getEnum('response', $webhook->response),
'delivery' => $this->getEnum('delivery', $webhook->delivery),
'triggers' => $webhook->meta['triggers'],
'deliveries' => $webhook->meta['deliveries'],
'responses' => $webhook->meta['responses'],
// 'trigger' => $this->getEnum('trigger', $webhook->trigger),
// 'response' => $this->getEnum('response', $webhook->response),
// 'delivery' => $this->getEnum('delivery', $webhook->delivery),
'url' => $webhook->url,
'links' => [
[

View File

@@ -3,6 +3,41 @@
All notable changes to this project will be documented in this file.
This project adheres to [Semantic Versioning](http://semver.org/).
## 6.4.0 - 2025-09-01
### Added
- [Issue 5532](https://github.com/firefly-iii/firefly-iii/issues/5532) (Asset prices and exchange rates) reported by @svozniuk
- [Discussion 10725](https://github.com/orgs/firefly-iii/discussions/10725) (New webhook triggers) started by @Billos. See the [documentation](https://docs.firefly-iii.org/how-to/firefly-iii/features/webhooks/).
### Changed
- Initial release.
### Fixed
- [Issue 10790](https://github.com/firefly-iii/firefly-iii/issues/10790) (Undefined variable $occurrences) reported by @senna1992
- [Issue 10791](https://github.com/firefly-iii/firefly-iii/issues/10791) (Clone and edit a transaction with a different currency doesn't clear the foreign transaction amount) reported by @jxtxzzw
- [Issue 10794](https://github.com/firefly-iii/firefly-iii/issues/10794) (Error with recurring transaction) reported by @MaximSN
- [Issue 10799](https://github.com/firefly-iii/firefly-iii/issues/10799) (Budget - "Left (per day)" not showing the correct value) reported by @GensHaze
- [Issue 10802](https://github.com/firefly-iii/firefly-iii/issues/10802) (Crash when trying to update a budget limit) reported by @Billos
- [Issue 10803](https://github.com/firefly-iii/firefly-iii/issues/10803) (Issue in /v1/budget-limits spent attribute) reported by @Billos
- [Issue 10804](https://github.com/firefly-iii/firefly-iii/issues/10804) (No notes information included in the "List all accounts" API call) reported by @gpampuro
- [Issue 10808](https://github.com/firefly-iii/firefly-iii/issues/10808) (cron job Error: Undefined variable $preference) reported by @MexerSam
- [Issue 10813](https://github.com/firefly-iii/firefly-iii/issues/10813) (Error "Argument #2 ($symbol) must be of type string" while try open subscriptions section) reported by @mrResident
- [Issue 10819](https://github.com/firefly-iii/firefly-iii/issues/10819) (Internal Server Error when trying to open piggy banks) reported by @noantiq
- [Issue 10820](https://github.com/firefly-iii/firefly-iii/issues/10820) (Unable to search date 1970-01-01 to apply rule.) reported by @Kage1
- [Issue 10824](https://github.com/firefly-iii/firefly-iii/issues/10824) (Converting withdrawal to transfer to account in different currency doesn't allow setting correct currencies) reported by @avee87
### API
- [Issue 8345](https://github.com/firefly-iii/firefly-iii/issues/8345) (API: Distinguish spent & earned at `/v2/chart/category/dashboard` (or future `v2/categories`)) reported by @dreautall
- [Issue 10804](https://github.com/firefly-iii/firefly-iii/issues/10804) (No notes information included in the "List all accounts" API call) reported by @gpampuro
- [Issue 10806](https://github.com/firefly-iii/firefly-iii/issues/10806) (API: `/v1/chart/balance/balance` has undocumented `period` parameter) reported by @dreautall
- [Issue 10807](https://github.com/firefly-iii/firefly-iii/issues/10807) (API: `/v1/bills` field `object_group_id` returns int, should be string) reported by @dreautall
- [Issue 10815](https://github.com/firefly-iii/firefly-iii/issues/10815) (API: `/v1/accounts` balance is off by a day) reported by @dreautall
- [Issue 10827](https://github.com/firefly-iii/firefly-iii/issues/10827) (Trigger Recurrence by API) reported by @MexerSam
## 6.3.2 - 2025-08-20
### Fixed

486
composer.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -78,8 +78,8 @@ return [
'running_balance_column' => env('USE_RUNNING_BALANCE', false),
// see cer.php for exchange rates feature flag.
],
'version' => '6.3.2',
'build_time' => 1755576388,
'version' => 'develop/2025-09-01',
'build_time' => 1756697429,
'api_version' => '2.1.0', // field is no longer used.
'db_version' => 26,

View File

@@ -248,6 +248,7 @@ return [
'multi_account_warning_deposit',
'multi_account_warning_transfer',
'webhook_trigger_ANY',
'webhook_trigger_STORE_TRANSACTION',
'webhook_trigger_UPDATE_TRANSACTION',
'webhook_trigger_DESTROY_TRANSACTION',
@@ -258,6 +259,7 @@ return [
'webhook_trigger_STORE_UPDATE_BUDGET_LIMIT',
'webhook_response_TRANSACTIONS',
'webhook_response_RELEVANT',
'webhook_response_ACCOUNTS',
'webhook_response_NONE',
'webhook_delivery_JSON',

107
config/webhooks.php Normal file
View File

@@ -0,0 +1,107 @@
<?php
/*
* webhooks.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);
// this is hard coded, which is unfortunate.
use FireflyIII\Enums\WebhookResponse;
use FireflyIII\Enums\WebhookTrigger;
return [
'force_relevant_response' => [
WebhookTrigger::STORE_TRANSACTION->name => [
WebhookTrigger::STORE_BUDGET->name,
WebhookTrigger::UPDATE_BUDGET->name,
WebhookTrigger::DESTROY_BUDGET->name,
WebhookTrigger::STORE_UPDATE_BUDGET_LIMIT->name,
],
WebhookTrigger::UPDATE_TRANSACTION->name => [
WebhookTrigger::STORE_BUDGET->name,
WebhookTrigger::UPDATE_BUDGET->name,
WebhookTrigger::DESTROY_BUDGET->name,
WebhookTrigger::STORE_UPDATE_BUDGET_LIMIT->name,
],
WebhookTrigger::DESTROY_TRANSACTION->name => [
WebhookTrigger::STORE_BUDGET->name,
WebhookTrigger::UPDATE_BUDGET->name,
WebhookTrigger::DESTROY_BUDGET->name,
WebhookTrigger::STORE_UPDATE_BUDGET_LIMIT->name,
],
WebhookTrigger::STORE_BUDGET->name => [
WebhookTrigger::STORE_TRANSACTION->name,
WebhookTrigger::UPDATE_TRANSACTION->name,
WebhookTrigger::DESTROY_TRANSACTION->name,
],
WebhookTrigger::UPDATE_BUDGET->name => [
WebhookTrigger::STORE_TRANSACTION->name,
WebhookTrigger::UPDATE_TRANSACTION->name,
WebhookTrigger::DESTROY_TRANSACTION->name,
],
WebhookTrigger::DESTROY_BUDGET->name => [
WebhookTrigger::STORE_TRANSACTION->name,
WebhookTrigger::UPDATE_TRANSACTION->name,
WebhookTrigger::DESTROY_TRANSACTION->name,
],
WebhookTrigger::STORE_UPDATE_BUDGET_LIMIT->name => [
WebhookTrigger::STORE_TRANSACTION->name,
WebhookTrigger::UPDATE_TRANSACTION->name,
WebhookTrigger::DESTROY_TRANSACTION->name,
],
],
'forbidden_responses' => [
WebhookTrigger::ANY->name => [
WebhookResponse::BUDGET->name,
WebhookResponse::TRANSACTIONS->name,
WebhookResponse::ACCOUNTS->name,
],
WebhookTrigger::STORE_TRANSACTION->name => [
WebhookResponse::BUDGET->name,
],
WebhookTrigger::UPDATE_TRANSACTION->name => [
WebhookResponse::BUDGET->name,
],
WebhookTrigger::DESTROY_TRANSACTION->name => [
WebhookResponse::BUDGET->name,
],
WebhookTrigger::STORE_BUDGET->name => [
WebhookResponse::TRANSACTIONS->name,
WebhookResponse::ACCOUNTS->name,
],
WebhookTrigger::UPDATE_BUDGET->name => [
WebhookResponse::TRANSACTIONS->name,
WebhookResponse::ACCOUNTS->name,
],
WebhookTrigger::DESTROY_BUDGET->name => [
WebhookResponse::TRANSACTIONS->name,
WebhookResponse::ACCOUNTS->name,
],
WebhookTrigger::STORE_UPDATE_BUDGET_LIMIT->name => [
WebhookResponse::TRANSACTIONS->name,
WebhookResponse::ACCOUNTS->name,
],
],
];

View File

@@ -1,5 +1,26 @@
<?php
/*
* 2025_07_10_065736_rename_tag_mode.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/>.
*/
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Log;

View File

@@ -0,0 +1,148 @@
<?php
/*
* 2025_08_19_180459_create_webhook_details_tables.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/>.
*/
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\QueryException;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration {
private const TABLE_ALREADY_EXISTS = 'If this table exists already (see the error message), this is not a problem. Other errors? Please open a discussion on GitHub.';
private const TABLE_ERROR = 'Could not create table "%s": %s';
/**
* Run the migrations.
*/
public function up(): void
{
if (!Schema::hasTable('webhook_triggers')) {
Schema::create('webhook_triggers', function (Blueprint $table) {
$table->id();
$table->timestamps();
$table->smallInteger('key')->unsigned();
$table->string('title', 100);
$table->unique(['key', 'title']);
});
}
if (!Schema::hasTable('webhook_responses')) {
Schema::create('webhook_responses', function (Blueprint $table) {
$table->id();
$table->timestamps();
$table->smallInteger('key')->unsigned();
$table->string('title', 100);
$table->unique(['key', 'title']);
});
}
if (!Schema::hasTable('webhook_deliveries')) {
Schema::create('webhook_deliveries', function (Blueprint $table) {
$table->id();
$table->timestamps();
$table->smallInteger('key')->unsigned();
$table->string('title', 100);
$table->unique(['key', 'title']);
});
}
// webhook_webhook_trigger
if (!Schema::hasTable('webhook_webhook_trigger')) {
try {
Schema::create(
'webhook_webhook_trigger',
static function (Blueprint $table): void {
$table->increments('id');
$table->integer('webhook_id', false, true);
$table->bigInteger('webhook_trigger_id', false, true);
$table->foreign('webhook_id')->references('id')->on('webhooks')->onDelete('cascade');
$table->foreign('webhook_trigger_id','link_to_trigger')->references('id')->on('webhook_triggers')->onDelete('cascade');
// unique combi:
$table->unique(['webhook_id', 'webhook_trigger_id']);
}
);
} catch (QueryException $e) {
app('log')->error(sprintf(self::TABLE_ERROR, 'webhook_webhook_trigger', $e->getMessage()));
app('log')->error(self::TABLE_ALREADY_EXISTS);
}
}
// webhook_webhook_response
if (!Schema::hasTable('webhook_webhook_response')) {
try {
Schema::create(
'webhook_webhook_response',
static function (Blueprint $table): void {
$table->increments('id');
$table->integer('webhook_id', false, true);
$table->bigInteger('webhook_response_id', false, true);
$table->foreign('webhook_id')->references('id')->on('webhooks')->onDelete('cascade');
$table->foreign('webhook_response_id','link_to_response')->references('id')->on('webhook_responses')->onDelete('cascade');
// unique combi:
$table->unique(['webhook_id', 'webhook_response_id']);
}
);
} catch (QueryException $e) {
app('log')->error(sprintf(self::TABLE_ERROR, 'webhook_webhook_response', $e->getMessage()));
app('log')->error(self::TABLE_ALREADY_EXISTS);
}
}
// webhook_webhook_delivery
if (!Schema::hasTable('webhook_webhook_delivery')) {
try {
Schema::create(
'webhook_webhook_delivery',
static function (Blueprint $table): void {
$table->increments('id');
$table->integer('webhook_id', false, true);
$table->bigInteger('webhook_delivery_id', false, true);
$table->foreign('webhook_id')->references('id')->on('webhooks')->onDelete('cascade');
$table->foreign('webhook_delivery_id','link_to_delivery')->references('id')->on('webhook_deliveries')->onDelete('cascade');
// unique combi:
$table->unique(['webhook_id', 'webhook_delivery_id']);
}
);
} catch (QueryException $e) {
app('log')->error(sprintf(self::TABLE_ERROR, 'webhook_webhook_delivery', $e->getMessage()));
app('log')->error(self::TABLE_ALREADY_EXISTS);
}
}
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('webhook_webhook_delivery');
Schema::dropIfExists('webhook_webhook_trigger');
Schema::dropIfExists('webhook_webhook_response');
Schema::dropIfExists('webhook_triggers');
Schema::dropIfExists('webhook_responses');
Schema::dropIfExists('webhook_deliveries');
}
};

View File

@@ -35,25 +35,10 @@ class AccountTypeSeeder extends Seeder
{
public function run(): void
{
$types = [
AccountTypeEnum::DEFAULT->value,
AccountTypeEnum::CASH->value,
AccountTypeEnum::ASSET->value,
AccountTypeEnum::EXPENSE->value,
AccountTypeEnum::REVENUE->value,
AccountTypeEnum::INITIAL_BALANCE->value,
AccountTypeEnum::BENEFICIARY->value,
AccountTypeEnum::IMPORT->value,
AccountTypeEnum::LOAN->value,
AccountTypeEnum::RECONCILIATION->value,
AccountTypeEnum::DEBT->value,
AccountTypeEnum::MORTGAGE->value,
AccountTypeEnum::LIABILITY_CREDIT->value,
];
foreach ($types as $type) {
if (null === AccountType::where('type', $type)->first()) {
foreach(AccountTypeEnum::cases() as $type) {
if (null === AccountType::where('type', $type->value)->first()) {
try {
AccountType::create(['type' => $type]);
AccountType::create(['type' => $type->value]);
} catch (PDOException $e) {
// @ignoreException
}

View File

@@ -43,5 +43,6 @@ class DatabaseSeeder extends Seeder
$this->call(ConfigSeeder::class);
$this->call(UserRoleSeeder::class);
$this->call(ExchangeRateSeeder::class);
$this->call(WebhookDataSeeder::class);
}
}

View File

@@ -35,24 +35,17 @@ class TransactionTypeSeeder extends Seeder
{
public function run(): void
{
$types = [
TransactionTypeEnum::WITHDRAWAL->value,
TransactionTypeEnum::DEPOSIT->value,
TransactionTypeEnum::TRANSFER->value,
TransactionTypeEnum::OPENING_BALANCE->value,
TransactionTypeEnum::RECONCILIATION->value,
TransactionTypeEnum::INVALID->value,
TransactionTypeEnum::LIABILITY_CREDIT->value,
];
foreach ($types as $type) {
if (null === TransactionType::where('type', $type)->first()) {
/** @var TransactionTypeEnum $type */
foreach (TransactionTypeEnum::cases() as $type) {
if (null === TransactionType::where('type', $type->value)->first()) {
try {
TransactionType::create(['type' => $type]);
TransactionType::create(['type' => $type->value]);
} catch (PDOException $e) {
// @ignoreException
}
}
}
}
}

View File

@@ -39,16 +39,11 @@ class UserRoleSeeder extends Seeder
*/
public function run(): void
{
$roles = [];
/** @var UserRoleEnum $role */
foreach (UserRoleEnum::cases() as $role) {
$roles[] = $role->value;
}
/** @var string $role */
foreach ($roles as $role) {
if (null === UserRole::where('title', $role)->first()) {
if (null === UserRole::where('title', $role->value)->first()) {
try {
UserRole::create(['title' => $role]);
UserRole::create(['title' => $role->value]);
} catch (PDOException $e) {
// @ignoreException
}

View File

@@ -0,0 +1,69 @@
<?php
/*
* WebhookDataSeeder.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 Database\Seeders;
use FireflyIII\Enums\WebhookTrigger;
use FireflyIII\Enums\WebhookResponse;
use FireflyIII\Enums\WebhookDelivery;
use FireflyIII\Models\WebhookTrigger as WebhookTriggerModel;
use FireflyIII\Models\WebhookResponse as WebhookResponseModel;
use FireflyIII\Models\WebhookDelivery as WebhookDeliveryModel;
use Illuminate\Database\Seeder;
class WebhookDataSeeder extends Seeder
{
/**
* Run the database seeds.
*/
public function run(): void
{
foreach (WebhookTrigger::cases() as $trigger) {
if (null === WebhookTriggerModel::where('key', $trigger->value)->where('title', $trigger->name)->first()) {
try {
WebhookTriggerModel::create(['key' => $trigger->value, 'title' => $trigger->name]);
} catch (\PDOException $e) {
// @ignoreException
}
}
}
foreach (WebhookResponse::cases() as $response) {
if (null === WebhookResponseModel::where('key', $response->value)->where('title', $response->name)->first()) {
try {
WebhookResponseModel::create(['key' => $response->value, 'title' => $response->name]);
} catch (\PDOException $e) {
// @ignoreException
}
}
}
foreach (WebhookDelivery::cases() as $delivery) {
if (null === WebhookDeliveryModel::where('key', $delivery->value)->where('title', $delivery->name)->first()) {
try {
WebhookDeliveryModel::create(['key' => $delivery->value, 'title' => $delivery->name]);
} catch (\PDOException $e) {
// @ignoreException
}
}
}
}
}

325
package-lock.json generated
View File

@@ -2592,9 +2592,9 @@
}
},
"node_modules/@rollup/rollup-android-arm-eabi": {
"version": "4.46.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.46.3.tgz",
"integrity": "sha512-UmTdvXnLlqQNOCJnyksjPs1G4GqXNGW1LrzCe8+8QoaLhhDeTXYBgJ3k6x61WIhlHX2U+VzEJ55TtIjR/HTySA==",
"version": "4.50.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.50.0.tgz",
"integrity": "sha512-lVgpeQyy4fWN5QYebtW4buT/4kn4p4IJ+kDNB4uYNT5b8c8DLJDg6titg20NIg7E8RWwdWZORW6vUFfrLyG3KQ==",
"cpu": [
"arm"
],
@@ -2606,9 +2606,9 @@
]
},
"node_modules/@rollup/rollup-android-arm64": {
"version": "4.46.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.46.3.tgz",
"integrity": "sha512-8NoxqLpXm7VyeI0ocidh335D6OKT0UJ6fHdnIxf3+6oOerZZc+O7r+UhvROji6OspyPm+rrIdb1gTXtVIqn+Sg==",
"version": "4.50.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.50.0.tgz",
"integrity": "sha512-2O73dR4Dc9bp+wSYhviP6sDziurB5/HCym7xILKifWdE9UsOe2FtNcM+I4xZjKrfLJnq5UR8k9riB87gauiQtw==",
"cpu": [
"arm64"
],
@@ -2620,9 +2620,9 @@
]
},
"node_modules/@rollup/rollup-darwin-arm64": {
"version": "4.46.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.46.3.tgz",
"integrity": "sha512-csnNavqZVs1+7/hUKtgjMECsNG2cdB8F7XBHP6FfQjqhjF8rzMzb3SLyy/1BG7YSfQ+bG75Ph7DyedbUqwq1rA==",
"version": "4.50.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.50.0.tgz",
"integrity": "sha512-vwSXQN8T4sKf1RHr1F0s98Pf8UPz7pS6P3LG9NSmuw0TVh7EmaE+5Ny7hJOZ0M2yuTctEsHHRTMi2wuHkdS6Hg==",
"cpu": [
"arm64"
],
@@ -2634,9 +2634,9 @@
]
},
"node_modules/@rollup/rollup-darwin-x64": {
"version": "4.46.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.46.3.tgz",
"integrity": "sha512-r2MXNjbuYabSIX5yQqnT8SGSQ26XQc8fmp6UhlYJd95PZJkQD1u82fWP7HqvGUf33IsOC6qsiV+vcuD4SDP6iw==",
"version": "4.50.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.50.0.tgz",
"integrity": "sha512-cQp/WG8HE7BCGyFVuzUg0FNmupxC+EPZEwWu2FCGGw5WDT1o2/YlENbm5e9SMvfDFR6FRhVCBePLqj0o8MN7Vw==",
"cpu": [
"x64"
],
@@ -2648,9 +2648,9 @@
]
},
"node_modules/@rollup/rollup-freebsd-arm64": {
"version": "4.46.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.46.3.tgz",
"integrity": "sha512-uluObTmgPJDuJh9xqxyr7MV61Imq+0IvVsAlWyvxAaBSNzCcmZlhfYcRhCdMaCsy46ccZa7vtDDripgs9Jkqsw==",
"version": "4.50.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.50.0.tgz",
"integrity": "sha512-UR1uTJFU/p801DvvBbtDD7z9mQL8J80xB0bR7DqW7UGQHRm/OaKzp4is7sQSdbt2pjjSS72eAtRh43hNduTnnQ==",
"cpu": [
"arm64"
],
@@ -2662,9 +2662,9 @@
]
},
"node_modules/@rollup/rollup-freebsd-x64": {
"version": "4.46.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.46.3.tgz",
"integrity": "sha512-AVJXEq9RVHQnejdbFvh1eWEoobohUYN3nqJIPI4mNTMpsyYN01VvcAClxflyk2HIxvLpRcRggpX1m9hkXkpC/A==",
"version": "4.50.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.50.0.tgz",
"integrity": "sha512-G/DKyS6PK0dD0+VEzH/6n/hWDNPDZSMBmqsElWnCRGrYOb2jC0VSupp7UAHHQ4+QILwkxSMaYIbQ72dktp8pKA==",
"cpu": [
"x64"
],
@@ -2676,9 +2676,9 @@
]
},
"node_modules/@rollup/rollup-linux-arm-gnueabihf": {
"version": "4.46.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.46.3.tgz",
"integrity": "sha512-byyflM+huiwHlKi7VHLAYTKr67X199+V+mt1iRgJenAI594vcmGGddWlu6eHujmcdl6TqSNnvqaXJqZdnEWRGA==",
"version": "4.50.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.50.0.tgz",
"integrity": "sha512-u72Mzc6jyJwKjJbZZcIYmd9bumJu7KNmHYdue43vT1rXPm2rITwmPWF0mmPzLm9/vJWxIRbao/jrQmxTO0Sm9w==",
"cpu": [
"arm"
],
@@ -2690,9 +2690,9 @@
]
},
"node_modules/@rollup/rollup-linux-arm-musleabihf": {
"version": "4.46.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.46.3.tgz",
"integrity": "sha512-aLm3NMIjr4Y9LklrH5cu7yybBqoVCdr4Nvnm8WB7PKCn34fMCGypVNpGK0JQWdPAzR/FnoEoFtlRqZbBBLhVoQ==",
"version": "4.50.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.50.0.tgz",
"integrity": "sha512-S4UefYdV0tnynDJV1mdkNawp0E5Qm2MtSs330IyHgaccOFrwqsvgigUD29uT+B/70PDY1eQ3t40+xf6wIvXJyg==",
"cpu": [
"arm"
],
@@ -2704,9 +2704,9 @@
]
},
"node_modules/@rollup/rollup-linux-arm64-gnu": {
"version": "4.46.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.46.3.tgz",
"integrity": "sha512-VtilE6eznJRDIoFOzaagQodUksTEfLIsvXymS+UdJiSXrPW7Ai+WG4uapAc3F7Hgs791TwdGh4xyOzbuzIZrnw==",
"version": "4.50.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.50.0.tgz",
"integrity": "sha512-1EhkSvUQXJsIhk4msxP5nNAUWoB4MFDHhtc4gAYvnqoHlaL9V3F37pNHabndawsfy/Tp7BPiy/aSa6XBYbaD1g==",
"cpu": [
"arm64"
],
@@ -2718,9 +2718,9 @@
]
},
"node_modules/@rollup/rollup-linux-arm64-musl": {
"version": "4.46.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.46.3.tgz",
"integrity": "sha512-dG3JuS6+cRAL0GQ925Vppafi0qwZnkHdPeuZIxIPXqkCLP02l7ka+OCyBoDEv8S+nKHxfjvjW4OZ7hTdHkx8/w==",
"version": "4.50.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.50.0.tgz",
"integrity": "sha512-EtBDIZuDtVg75xIPIK1l5vCXNNCIRM0OBPUG+tbApDuJAy9mKago6QxX+tfMzbCI6tXEhMuZuN1+CU8iDW+0UQ==",
"cpu": [
"arm64"
],
@@ -2732,9 +2732,9 @@
]
},
"node_modules/@rollup/rollup-linux-loongarch64-gnu": {
"version": "4.46.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.46.3.tgz",
"integrity": "sha512-iU8DxnxEKJptf8Vcx4XvAUdpkZfaz0KWfRrnIRrOndL0SvzEte+MTM7nDH4A2Now4FvTZ01yFAgj6TX/mZl8hQ==",
"version": "4.50.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.50.0.tgz",
"integrity": "sha512-BGYSwJdMP0hT5CCmljuSNx7+k+0upweM2M4YGfFBjnFSZMHOLYR0gEEj/dxyYJ6Zc6AiSeaBY8dWOa11GF/ppQ==",
"cpu": [
"loong64"
],
@@ -2746,9 +2746,9 @@
]
},
"node_modules/@rollup/rollup-linux-ppc64-gnu": {
"version": "4.46.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.46.3.tgz",
"integrity": "sha512-VrQZp9tkk0yozJoQvQcqlWiqaPnLM6uY1qPYXvukKePb0fqaiQtOdMJSxNFUZFsGw5oA5vvVokjHrx8a9Qsz2A==",
"version": "4.50.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.50.0.tgz",
"integrity": "sha512-I1gSMzkVe1KzAxKAroCJL30hA4DqSi+wGc5gviD0y3IL/VkvcnAqwBf4RHXHyvH66YVHxpKO8ojrgc4SrWAnLg==",
"cpu": [
"ppc64"
],
@@ -2760,9 +2760,9 @@
]
},
"node_modules/@rollup/rollup-linux-riscv64-gnu": {
"version": "4.46.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.46.3.tgz",
"integrity": "sha512-uf2eucWSUb+M7b0poZ/08LsbcRgaDYL8NCGjUeFMwCWFwOuFcZ8D9ayPl25P3pl+D2FH45EbHdfyUesQ2Lt9wA==",
"version": "4.50.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.50.0.tgz",
"integrity": "sha512-bSbWlY3jZo7molh4tc5dKfeSxkqnf48UsLqYbUhnkdnfgZjgufLS/NTA8PcP/dnvct5CCdNkABJ56CbclMRYCA==",
"cpu": [
"riscv64"
],
@@ -2774,9 +2774,9 @@
]
},
"node_modules/@rollup/rollup-linux-riscv64-musl": {
"version": "4.46.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.46.3.tgz",
"integrity": "sha512-7tnUcDvN8DHm/9ra+/nF7lLzYHDeODKKKrh6JmZejbh1FnCNZS8zMkZY5J4sEipy2OW1d1Ncc4gNHUd0DLqkSg==",
"version": "4.50.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.50.0.tgz",
"integrity": "sha512-LSXSGumSURzEQLT2e4sFqFOv3LWZsEF8FK7AAv9zHZNDdMnUPYH3t8ZlaeYYZyTXnsob3htwTKeWtBIkPV27iQ==",
"cpu": [
"riscv64"
],
@@ -2788,9 +2788,9 @@
]
},
"node_modules/@rollup/rollup-linux-s390x-gnu": {
"version": "4.46.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.46.3.tgz",
"integrity": "sha512-MUpAOallJim8CsJK+4Lc9tQzlfPbHxWDrGXZm2z6biaadNpvh3a5ewcdat478W+tXDoUiHwErX/dOql7ETcLqg==",
"version": "4.50.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.50.0.tgz",
"integrity": "sha512-CxRKyakfDrsLXiCyucVfVWVoaPA4oFSpPpDwlMcDFQvrv3XY6KEzMtMZrA+e/goC8xxp2WSOxHQubP8fPmmjOQ==",
"cpu": [
"s390x"
],
@@ -2802,9 +2802,9 @@
]
},
"node_modules/@rollup/rollup-linux-x64-gnu": {
"version": "4.46.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.46.3.tgz",
"integrity": "sha512-F42IgZI4JicE2vM2PWCe0N5mR5vR0gIdORPqhGQ32/u1S1v3kLtbZ0C/mi9FFk7C5T0PgdeyWEPajPjaUpyoKg==",
"version": "4.50.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.50.0.tgz",
"integrity": "sha512-8PrJJA7/VU8ToHVEPu14FzuSAqVKyo5gg/J8xUerMbyNkWkO9j2ExBho/68RnJsMGNJq4zH114iAttgm7BZVkA==",
"cpu": [
"x64"
],
@@ -2816,9 +2816,9 @@
]
},
"node_modules/@rollup/rollup-linux-x64-musl": {
"version": "4.46.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.46.3.tgz",
"integrity": "sha512-oLc+JrwwvbimJUInzx56Q3ujL3Kkhxehg7O1gWAYzm8hImCd5ld1F2Gry5YDjR21MNb5WCKhC9hXgU7rRlyegQ==",
"version": "4.50.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.50.0.tgz",
"integrity": "sha512-SkE6YQp+CzpyOrbw7Oc4MgXFvTw2UIBElvAvLCo230pyxOLmYwRPwZ/L5lBe/VW/qT1ZgND9wJfOsdy0XptRvw==",
"cpu": [
"x64"
],
@@ -2829,10 +2829,24 @@
"linux"
]
},
"node_modules/@rollup/rollup-openharmony-arm64": {
"version": "4.50.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.50.0.tgz",
"integrity": "sha512-PZkNLPfvXeIOgJWA804zjSFH7fARBBCpCXxgkGDRjjAhRLOR8o0IGS01ykh5GYfod4c2yiiREuDM8iZ+pVsT+Q==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"openharmony"
]
},
"node_modules/@rollup/rollup-win32-arm64-msvc": {
"version": "4.46.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.46.3.tgz",
"integrity": "sha512-lOrQ+BVRstruD1fkWg9yjmumhowR0oLAAzavB7yFSaGltY8klttmZtCLvOXCmGE9mLIn8IBV/IFrQOWz5xbFPg==",
"version": "4.50.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.50.0.tgz",
"integrity": "sha512-q7cIIdFvWQoaCbLDUyUc8YfR3Jh2xx3unO8Dn6/TTogKjfwrax9SyfmGGK6cQhKtjePI7jRfd7iRYcxYs93esg==",
"cpu": [
"arm64"
],
@@ -2844,9 +2858,9 @@
]
},
"node_modules/@rollup/rollup-win32-ia32-msvc": {
"version": "4.46.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.46.3.tgz",
"integrity": "sha512-vvrVKPRS4GduGR7VMH8EylCBqsDcw6U+/0nPDuIjXQRbHJc6xOBj+frx8ksfZAh6+Fptw5wHrN7etlMmQnPQVg==",
"version": "4.50.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.50.0.tgz",
"integrity": "sha512-XzNOVg/YnDOmFdDKcxxK410PrcbcqZkBmz+0FicpW5jtjKQxcW1BZJEQOF0NJa6JO7CZhett8GEtRN/wYLYJuw==",
"cpu": [
"ia32"
],
@@ -2858,9 +2872,9 @@
]
},
"node_modules/@rollup/rollup-win32-x64-msvc": {
"version": "4.46.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.46.3.tgz",
"integrity": "sha512-fi3cPxCnu3ZeM3EwKZPgXbWoGzm2XHgB/WShKI81uj8wG0+laobmqy5wbgEwzstlbLu4MyO8C19FyhhWseYKNQ==",
"version": "4.50.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.50.0.tgz",
"integrity": "sha512-xMmiWRR8sp72Zqwjgtf3QbZfF1wdh8X2ABu3EaozvZcyHJeU0r+XAnXdKgs4cCAp6ORoYoCygipYP1mjmbjrsg==",
"cpu": [
"x64"
],
@@ -3158,9 +3172,9 @@
}
},
"node_modules/@types/node-forge": {
"version": "1.3.13",
"resolved": "https://registry.npmjs.org/@types/node-forge/-/node-forge-1.3.13.tgz",
"integrity": "sha512-zePQJSW5QkwSHKRApqWCVKeKoSOt4xvEnLENZPjyvm9Ezdf/EyDeJM7jqLzOwjVICQQzvLZ63T55MKdJB5H6ww==",
"version": "1.3.14",
"resolved": "https://registry.npmjs.org/@types/node-forge/-/node-forge-1.3.14.tgz",
"integrity": "sha512-mhVF2BnD4BO+jtOp7z1CdzaK4mbuK0LLQYAvdOLqHTavxFNq4zA1EmYkpnFjP8HOUzedfQkRnp0E2ulSAYSzAw==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -3256,42 +3270,42 @@
}
},
"node_modules/@vue/compiler-core": {
"version": "3.5.18",
"resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.18.tgz",
"integrity": "sha512-3slwjQrrV1TO8MoXgy3aynDQ7lslj5UqDxuHnrzHtpON5CBinhWjJETciPngpin/T3OuW3tXUf86tEurusnztw==",
"version": "3.5.20",
"resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.20.tgz",
"integrity": "sha512-8TWXUyiqFd3GmP4JTX9hbiTFRwYHgVL/vr3cqhr4YQ258+9FADwvj7golk2sWNGHR67QgmCZ8gz80nQcMokhwg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/parser": "^7.28.0",
"@vue/shared": "3.5.18",
"@babel/parser": "^7.28.3",
"@vue/shared": "3.5.20",
"entities": "^4.5.0",
"estree-walker": "^2.0.2",
"source-map-js": "^1.2.1"
}
},
"node_modules/@vue/compiler-dom": {
"version": "3.5.18",
"resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.18.tgz",
"integrity": "sha512-RMbU6NTU70++B1JyVJbNbeFkK+A+Q7y9XKE2EM4NLGm2WFR8x9MbAtWxPPLdm0wUkuZv9trpwfSlL6tjdIa1+A==",
"version": "3.5.20",
"resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.20.tgz",
"integrity": "sha512-whB44M59XKjqUEYOMPYU0ijUV0G+4fdrHVKDe32abNdX/kJe1NUEMqsi4cwzXa9kyM9w5S8WqFsrfo1ogtBZGQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@vue/compiler-core": "3.5.18",
"@vue/shared": "3.5.18"
"@vue/compiler-core": "3.5.20",
"@vue/shared": "3.5.20"
}
},
"node_modules/@vue/compiler-sfc": {
"version": "3.5.18",
"resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.18.tgz",
"integrity": "sha512-5aBjvGqsWs+MoxswZPoTB9nSDb3dhd1x30xrrltKujlCxo48j8HGDNj3QPhF4VIS0VQDUrA1xUfp2hEa+FNyXA==",
"version": "3.5.20",
"resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.20.tgz",
"integrity": "sha512-SFcxapQc0/feWiSBfkGsa1v4DOrnMAQSYuvDMpEaxbpH5dKbnEM5KobSNSgU+1MbHCl+9ftm7oQWxvwDB6iBfw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/parser": "^7.28.0",
"@vue/compiler-core": "3.5.18",
"@vue/compiler-dom": "3.5.18",
"@vue/compiler-ssr": "3.5.18",
"@vue/shared": "3.5.18",
"@babel/parser": "^7.28.3",
"@vue/compiler-core": "3.5.20",
"@vue/compiler-dom": "3.5.20",
"@vue/compiler-ssr": "3.5.20",
"@vue/shared": "3.5.20",
"estree-walker": "^2.0.2",
"magic-string": "^0.30.17",
"postcss": "^8.5.6",
@@ -3299,14 +3313,14 @@
}
},
"node_modules/@vue/compiler-ssr": {
"version": "3.5.18",
"resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.18.tgz",
"integrity": "sha512-xM16Ak7rSWHkM3m22NlmcdIM+K4BMyFARAfV9hYFl+SFuRzrZ3uGMNW05kA5pmeMa0X9X963Kgou7ufdbpOP9g==",
"version": "3.5.20",
"resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.20.tgz",
"integrity": "sha512-RSl5XAMc5YFUXpDQi+UQDdVjH9FnEpLDHIALg5J0ITHxkEzJ8uQLlo7CIbjPYqmZtt6w0TsIPbo1izYXwDG7JA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@vue/compiler-dom": "3.5.18",
"@vue/shared": "3.5.18"
"@vue/compiler-dom": "3.5.20",
"@vue/shared": "3.5.20"
}
},
"node_modules/@vue/component-compiler-utils": {
@@ -3388,9 +3402,9 @@
"license": "MIT"
},
"node_modules/@vue/shared": {
"version": "3.5.18",
"resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.18.tgz",
"integrity": "sha512-cZy8Dq+uuIXbxCZpuLd2GJdeSO/lIzIspC2WtkqIpje5QyFbvLaI5wZtdUjLHjGZrlVX6GilejatWwVYYRc8tA==",
"version": "3.5.20",
"resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.20.tgz",
"integrity": "sha512-SoRGP596KU/ig6TfgkCMbXkr4YJ91n/QSdMuqeP5r3hVIYA3CPHUBCc7Skak0EAKV+5lL4KyIh61VA/pK1CIAA==",
"dev": true,
"license": "MIT"
},
@@ -4170,9 +4184,9 @@
"license": "ISC"
},
"node_modules/bootstrap": {
"version": "5.3.7",
"resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-5.3.7.tgz",
"integrity": "sha512-7KgiD8UHjfcPBHEpDNg+zGz8L3LqR3GVwqZiBRFX04a1BCArZOz1r2kjly2HQ0WokqTO0v1nF+QAt8dsW4lKlw==",
"version": "5.3.8",
"resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-5.3.8.tgz",
"integrity": "sha512-HP1SZDqaLDPwsNiqRqi5NcP0SSXciX2s9E+RyqJIIqGo+vJeN5AJVM98CXmW/Wux0nQ5L7jeWUdplCEf0Ee+tg==",
"funding": [
{
"type": "github",
@@ -4326,9 +4340,9 @@
}
},
"node_modules/browserslist": {
"version": "4.25.3",
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.25.3.tgz",
"integrity": "sha512-cDGv1kkDI4/0e5yON9yM5G/0A5u8sf5TnmdX5C9qHzI9PPu++sQ9zjm1k9NiOrf3riY4OkK0zSGqfvJyJsgCBQ==",
"version": "4.25.4",
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.25.4.tgz",
"integrity": "sha512-4jYpcjabC606xJ3kw2QwGEZKX0Aw7sgQdZCvIK9dhVSPh76BKo+C+btT1RRofH7B+8iNpEbgGNVWiLki5q93yg==",
"dev": true,
"funding": [
{
@@ -4346,8 +4360,8 @@
],
"license": "MIT",
"dependencies": {
"caniuse-lite": "^1.0.30001735",
"electron-to-chromium": "^1.5.204",
"caniuse-lite": "^1.0.30001737",
"electron-to-chromium": "^1.5.211",
"node-releases": "^2.0.19",
"update-browserslist-db": "^1.1.3"
},
@@ -4486,9 +4500,9 @@
}
},
"node_modules/caniuse-lite": {
"version": "1.0.30001735",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001735.tgz",
"integrity": "sha512-EV/laoX7Wq2J9TQlyIXRxTJqIw4sxfXS4OYgudGxBYRuTv0q7AM6yMEpU/Vo1I94thg9U6EZ2NfZx9GJq83u7w==",
"version": "1.0.30001739",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001739.tgz",
"integrity": "sha512-y+j60d6ulelrNSwpPyrHdl+9mJnQzHBr08xm48Qno0nSk4h3Qojh+ziv2qE6rXf4k3tadF4o1J/1tAbVm1NtnA==",
"dev": true,
"funding": [
{
@@ -4930,13 +4944,13 @@
"license": "MIT"
},
"node_modules/core-js-compat": {
"version": "3.45.0",
"resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.45.0.tgz",
"integrity": "sha512-gRoVMBawZg0OnxaVv3zpqLLxaHmsubEGyTnqdpI/CEBvX4JadI1dMSHxagThprYRtSVbuQxvi6iUatdPxohHpA==",
"version": "3.45.1",
"resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.45.1.tgz",
"integrity": "sha512-tqTt5T4PzsMIZ430XGviK4vzYSoeNJ6CXODi6c/voxOT6IZqBht5/EKaSNnYiEjjRYxjVz7DQIsOsY0XNi8PIA==",
"dev": true,
"license": "MIT",
"dependencies": {
"browserslist": "^4.25.1"
"browserslist": "^4.25.3"
},
"funding": {
"type": "opencollective",
@@ -5700,9 +5714,9 @@
"license": "MIT"
},
"node_modules/electron-to-chromium": {
"version": "1.5.205",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.205.tgz",
"integrity": "sha512-gBtbT7IgOHu7CwdtIiXwbNRD1l6oG6GAyanmwMCLVqaoGy92Jfe1dSHLiSj8xUEZNxOTIVlXuaAalMMD+S4y0w==",
"version": "1.5.211",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.211.tgz",
"integrity": "sha512-IGBvimJkotaLzFnwIVgW9/UD/AOJ2tByUmeOrtqBfACSbAw5b1G0XpvdaieKyc7ULmbwXVx+4e4Be8pOPBrYkw==",
"dev": true,
"license": "ISC"
},
@@ -6159,9 +6173,9 @@
"license": "MIT"
},
"node_modules/fast-uri": {
"version": "3.0.6",
"resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.0.6.tgz",
"integrity": "sha512-Atfo14OibSv5wAp4VWNsFYE1AchQRTv9cBGWET4pZWHzYshFSS9NQI6I57rdKn9croWVMbYFbLhJ+yJvmZIIHw==",
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz",
"integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==",
"dev": true,
"funding": [
{
@@ -7052,9 +7066,9 @@
}
},
"node_modules/i18next": {
"version": "25.3.6",
"resolved": "https://registry.npmjs.org/i18next/-/i18next-25.3.6.tgz",
"integrity": "sha512-dThZ0CTCM3sUG/qS0ZtQYZQcUI6DtBN8yBHK+SKEqihPcEYmjVWh/YJ4luic73Iq6Uxhp6q7LJJntRK5+1t7jQ==",
"version": "25.4.2",
"resolved": "https://registry.npmjs.org/i18next/-/i18next-25.4.2.tgz",
"integrity": "sha512-gD4T25a6ovNXsfXY1TwHXXXLnD/K2t99jyYMCSimSCBnBRJVQr5j+VAaU83RJCPzrTGhVQ6dqIga66xO2rtd5g==",
"funding": [
{
"type": "individual",
@@ -7818,9 +7832,9 @@
}
},
"node_modules/laravel-vite-plugin": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/laravel-vite-plugin/-/laravel-vite-plugin-2.0.0.tgz",
"integrity": "sha512-pnaKHInJgiWpG/g+LmaISHl7D/1s5wnOXnrGiBdt4NOs+tYZRw0v/ZANELGX2/dGgHyEzO+iZ6x4idpoK04z/Q==",
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/laravel-vite-plugin/-/laravel-vite-plugin-2.0.1.tgz",
"integrity": "sha512-zQuvzWfUKQu9oNVi1o0RZAJCwhGsdhx4NEOyrVQwJHaWDseGP9tl7XUPLY2T8Cj6+IrZ6lmyxlR1KC8unf3RLA==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -7958,13 +7972,13 @@
}
},
"node_modules/magic-string": {
"version": "0.30.17",
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz",
"integrity": "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==",
"version": "0.30.18",
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.18.tgz",
"integrity": "sha512-yi8swmWbO17qHhwIBNeeZxTceJMeBvWJaId6dyvTSOwTipqeHhMhOrz6513r1sOKnpvQ7zkhlG8tPrpilwTxHQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@jridgewell/sourcemap-codec": "^1.5.0"
"@jridgewell/sourcemap-codec": "^1.5.5"
}
},
"node_modules/make-dir": {
@@ -10123,9 +10137,9 @@
}
},
"node_modules/rollup": {
"version": "4.46.3",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.46.3.tgz",
"integrity": "sha512-RZn2XTjXb8t5g13f5YclGoilU/kwT696DIkY3sywjdZidNSi3+vseaQov7D7BZXVJCPv3pDWUN69C78GGbXsKw==",
"version": "4.50.0",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.50.0.tgz",
"integrity": "sha512-/Zl4D8zPifNmyGzJS+3kVoyXeDeT/GrsJM94sACNg9RtUE0hrHa1bNPtRSrfHTMH5HjRzce6K7rlTh3Khiw+pw==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -10139,26 +10153,27 @@
"npm": ">=8.0.0"
},
"optionalDependencies": {
"@rollup/rollup-android-arm-eabi": "4.46.3",
"@rollup/rollup-android-arm64": "4.46.3",
"@rollup/rollup-darwin-arm64": "4.46.3",
"@rollup/rollup-darwin-x64": "4.46.3",
"@rollup/rollup-freebsd-arm64": "4.46.3",
"@rollup/rollup-freebsd-x64": "4.46.3",
"@rollup/rollup-linux-arm-gnueabihf": "4.46.3",
"@rollup/rollup-linux-arm-musleabihf": "4.46.3",
"@rollup/rollup-linux-arm64-gnu": "4.46.3",
"@rollup/rollup-linux-arm64-musl": "4.46.3",
"@rollup/rollup-linux-loongarch64-gnu": "4.46.3",
"@rollup/rollup-linux-ppc64-gnu": "4.46.3",
"@rollup/rollup-linux-riscv64-gnu": "4.46.3",
"@rollup/rollup-linux-riscv64-musl": "4.46.3",
"@rollup/rollup-linux-s390x-gnu": "4.46.3",
"@rollup/rollup-linux-x64-gnu": "4.46.3",
"@rollup/rollup-linux-x64-musl": "4.46.3",
"@rollup/rollup-win32-arm64-msvc": "4.46.3",
"@rollup/rollup-win32-ia32-msvc": "4.46.3",
"@rollup/rollup-win32-x64-msvc": "4.46.3",
"@rollup/rollup-android-arm-eabi": "4.50.0",
"@rollup/rollup-android-arm64": "4.50.0",
"@rollup/rollup-darwin-arm64": "4.50.0",
"@rollup/rollup-darwin-x64": "4.50.0",
"@rollup/rollup-freebsd-arm64": "4.50.0",
"@rollup/rollup-freebsd-x64": "4.50.0",
"@rollup/rollup-linux-arm-gnueabihf": "4.50.0",
"@rollup/rollup-linux-arm-musleabihf": "4.50.0",
"@rollup/rollup-linux-arm64-gnu": "4.50.0",
"@rollup/rollup-linux-arm64-musl": "4.50.0",
"@rollup/rollup-linux-loongarch64-gnu": "4.50.0",
"@rollup/rollup-linux-ppc64-gnu": "4.50.0",
"@rollup/rollup-linux-riscv64-gnu": "4.50.0",
"@rollup/rollup-linux-riscv64-musl": "4.50.0",
"@rollup/rollup-linux-s390x-gnu": "4.50.0",
"@rollup/rollup-linux-x64-gnu": "4.50.0",
"@rollup/rollup-linux-x64-musl": "4.50.0",
"@rollup/rollup-openharmony-arm64": "4.50.0",
"@rollup/rollup-win32-arm64-msvc": "4.50.0",
"@rollup/rollup-win32-ia32-msvc": "4.50.0",
"@rollup/rollup-win32-x64-msvc": "4.50.0",
"fsevents": "~2.3.2"
}
},
@@ -10214,9 +10229,9 @@
"license": "MIT"
},
"node_modules/sass": {
"version": "1.90.0",
"resolved": "https://registry.npmjs.org/sass/-/sass-1.90.0.tgz",
"integrity": "sha512-9GUyuksjw70uNpb1MTYWsH9MQHOHY6kwfnkafC24+7aOMZn9+rVMBxRbLvw756mrBFbIsFg6Xw9IkR2Fnn3k+Q==",
"version": "1.91.0",
"resolved": "https://registry.npmjs.org/sass/-/sass-1.91.0.tgz",
"integrity": "sha512-aFOZHGf+ur+bp1bCHZ+u8otKGh77ZtmFyXDo4tlYvT7PWql41Kwd8wdkPqhhT+h2879IVblcHFglIMofsFd1EA==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -11005,13 +11020,17 @@
}
},
"node_modules/tapable": {
"version": "2.2.2",
"resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.2.tgz",
"integrity": "sha512-Re10+NauLTMCudc7T5WLFLAwDhQ0JWdrMK+9B2M8zR5hRExKmsRDCBA7/aV/pNJFltmBFO5BAMlQFi/vq3nKOg==",
"version": "2.2.3",
"resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.3.tgz",
"integrity": "sha512-ZL6DDuAlRlLGghwcfmSn9sK3Hr6ArtyudlSAiCqQ6IfE+b+HHbydbYDIG15IfS5do+7XQQBdBiubF/cV2dnDzg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=6"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/webpack"
}
},
"node_modules/terser": {
@@ -11527,14 +11546,14 @@
}
},
"node_modules/vite": {
"version": "7.1.2",
"resolved": "https://registry.npmjs.org/vite/-/vite-7.1.2.tgz",
"integrity": "sha512-J0SQBPlQiEXAF7tajiH+rUooJPo0l8KQgyg4/aMunNtrOa7bwuZJsJbDWzeljqQpgftxuq5yNJxQ91O9ts29UQ==",
"version": "7.1.3",
"resolved": "https://registry.npmjs.org/vite/-/vite-7.1.3.tgz",
"integrity": "sha512-OOUi5zjkDxYrKhTV3V7iKsoS37VUM7v40+HuwEmcrsf11Cdx9y3DIr2Px6liIcZFwt3XSRpQvFpL3WVy7ApkGw==",
"dev": true,
"license": "MIT",
"dependencies": {
"esbuild": "^0.25.0",
"fdir": "^6.4.6",
"fdir": "^6.5.0",
"picomatch": "^4.0.3",
"postcss": "^8.5.6",
"rollup": "^4.43.0",

View File

@@ -1,4 +1,25 @@
<?php
/*
* index.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);
use Illuminate\Contracts\Http\Kernel;

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