From c2ffedb5068ac9690011b5211e0372e727fb709c Mon Sep 17 00:00:00 2001 From: James Cole Date: Tue, 12 Aug 2025 20:44:58 +0200 Subject: [PATCH 01/17] Remove link. --- readme.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/readme.md b/readme.md index 5075f4400a..25a7d39e24 100644 --- a/readme.md +++ b/readme.md @@ -173,8 +173,6 @@ Over time, [many people have contributed to Firefly III](https://github.com/fire The Firefly III logo is made by the excellent Cherie Woo. -Featured|HelloGitHub - [packagist-shield]: https://img.shields.io/packagist/v/grumpydictator/firefly-iii.svg?style=flat-square [packagist-url]: https://packagist.org/packages/grumpydictator/firefly-iii [license-shield]: https://img.shields.io/github/license/firefly-iii/firefly-iii.svg?style=flat-square From 5ef0889f50d83d2c5df6c75ebf3c053e34a9d48b Mon Sep 17 00:00:00 2001 From: James Cole Date: Tue, 12 Aug 2025 20:45:42 +0200 Subject: [PATCH 02/17] Lol lets remove this file. --- temp-file.txt | 1 - 1 file changed, 1 deletion(-) delete mode 100644 temp-file.txt diff --git a/temp-file.txt b/temp-file.txt deleted file mode 100644 index edd8488031..0000000000 --- a/temp-file.txt +++ /dev/null @@ -1 +0,0 @@ -Sun Mar 16 16:48:09 UTC 2025 From 536cd78e292862f6e0359f3c63d2f528cebcbb8b Mon Sep 17 00:00:00 2001 From: James Cole Date: Wed, 13 Aug 2025 07:43:11 +0200 Subject: [PATCH 03/17] Templates for new releases. --- .github/release-notes/alpha.md | 20 ++++++++++++++++++++ .github/release-notes/beta.md | 20 ++++++++++++++++++++ .github/release-notes/branch.md | 1 + .github/release-notes/develop.md | 16 ++++++++++++++++ .github/release-notes/release.md | 16 ++++++++++++++++ 5 files changed, 73 insertions(+) create mode 100644 .github/release-notes/alpha.md create mode 100644 .github/release-notes/beta.md create mode 100644 .github/release-notes/branch.md create mode 100644 .github/release-notes/develop.md create mode 100644 .github/release-notes/release.md diff --git a/.github/release-notes/alpha.md b/.github/release-notes/alpha.md new file mode 100644 index 0000000000..7b775f0893 --- /dev/null +++ b/.github/release-notes/alpha.md @@ -0,0 +1,20 @@ +Welcome to this ALPHA release of Firefly III. This alpha release contains the latest fixes, translations and features. It is probably buggy and may not work as expected. You can download the release below, and adventurous Docker users can find this release under the `alpha` tag. + +:warning: Please be careful with this alpha release, as it may not work as expected. + +Alpha releases are created to test new features and fixes before they are included in a stable release. They are not recommended for production use. This release was created on %date and may contain unexpected bugs. Data loss is rare but is entirely possible. + +## Changelog (not final) + +%changelog + +## Installation and upgrade instructions + +* Please read the installation instructions for [Docker](https://docs.firefly-iii.org/how-to/firefly-iii/installation/docker/), [Portainer](https://docs.firefly-iii.org/how-to/firefly-iii/installation/portainer/), [Kubernetes](https://docs.firefly-iii.org/how-to/firefly-iii/installation/kubernetes/) or [self-managed servers](https://docs.firefly-iii.org/how-to/firefly-iii/installation/self-managed/). +* Alternatively, read the upgrade instructions for [Docker](https://docs.firefly-iii.org/how-to/firefly-iii/upgrade/docker/), [Kubernetes](https://docs.firefly-iii.org/how-to/firefly-iii/upgrade/kubernetes/) or [self-managed servers](https://docs.firefly-iii.org/how-to/firefly-iii/upgrade/self-managed/) + +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/). + +## 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. diff --git a/.github/release-notes/beta.md b/.github/release-notes/beta.md new file mode 100644 index 0000000000..0a8f9a9d36 --- /dev/null +++ b/.github/release-notes/beta.md @@ -0,0 +1,20 @@ +Welcome to this BETA release of Firefly III. This beta release contains the latest fixes, translations and features. It should not be buggy, but may not not work as expected. You can download the release below, and adventurous Docker users can find this release under the `beta` tag. + +:warning: Please be careful with this release, as it may not work as expected. + +Beta releases are created to test new features and fixes before they are included in a stable release. They are not recommended for production use. This release was created on %date and may contain unexpected bugs. Data loss is rare but is entirely possible. + +## Changelog (not final) + +%changelog + +## Installation and upgrade instructions + +* Please read the installation instructions for [Docker](https://docs.firefly-iii.org/how-to/firefly-iii/installation/docker/), [Portainer](https://docs.firefly-iii.org/how-to/firefly-iii/installation/portainer/), [Kubernetes](https://docs.firefly-iii.org/how-to/firefly-iii/installation/kubernetes/) or [self-managed servers](https://docs.firefly-iii.org/how-to/firefly-iii/installation/self-managed/). +* Alternatively, read the upgrade instructions for [Docker](https://docs.firefly-iii.org/how-to/firefly-iii/upgrade/docker/), [Kubernetes](https://docs.firefly-iii.org/how-to/firefly-iii/upgrade/kubernetes/) or [self-managed servers](https://docs.firefly-iii.org/how-to/firefly-iii/upgrade/self-managed/) + +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/). + +## 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. diff --git a/.github/release-notes/branch.md b/.github/release-notes/branch.md new file mode 100644 index 0000000000..874c6610ad --- /dev/null +++ b/.github/release-notes/branch.md @@ -0,0 +1 @@ +(not yet in use) diff --git a/.github/release-notes/develop.md b/.github/release-notes/develop.md new file mode 100644 index 0000000000..6b38234b65 --- /dev/null +++ b/.github/release-notes/develop.md @@ -0,0 +1,16 @@ +Welcome to the latest development release of Firefly III. This test release contains the absolute latest fixes, translations and features. It is probably buggy and may not work as expected. You can download the release below, and adventurous Docker users can find this release under the `develop` tag. + +:warning: Please be careful with this pre-release, as it may not work as expected. + +This release was created on %date and may contain unexpected bugs. Data loss is rare but is entirely possible. A changelog is not included with this development release. + +## Installation and upgrade instructions + +* Please read the installation instructions for [Docker](https://docs.firefly-iii.org/how-to/firefly-iii/installation/docker/), [Portainer](https://docs.firefly-iii.org/how-to/firefly-iii/installation/portainer/), [Kubernetes](https://docs.firefly-iii.org/how-to/firefly-iii/installation/kubernetes/) or [self-managed servers](https://docs.firefly-iii.org/how-to/firefly-iii/installation/self-managed/). +* Alternatively, read the upgrade instructions for [Docker](https://docs.firefly-iii.org/how-to/firefly-iii/upgrade/docker/), [Kubernetes](https://docs.firefly-iii.org/how-to/firefly-iii/upgrade/kubernetes/) or [self-managed servers](https://docs.firefly-iii.org/how-to/firefly-iii/upgrade/self-managed/) + +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/). + +## 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. diff --git a/.github/release-notes/release.md b/.github/release-notes/release.md new file mode 100644 index 0000000000..950b519e01 --- /dev/null +++ b/.github/release-notes/release.md @@ -0,0 +1,16 @@ +Welcome to release %version of Firefly III. It contains the latest fixes, translations and features. Docker users can find this release under the `latest` tag. + +## Changelog + +%changelog + +## Installation and upgrade instructions + +* Please read the installation instructions for [Docker](https://docs.firefly-iii.org/how-to/firefly-iii/installation/docker/), [Portainer](https://docs.firefly-iii.org/how-to/firefly-iii/installation/portainer/), [Kubernetes](https://docs.firefly-iii.org/how-to/firefly-iii/installation/kubernetes/) or [self-managed servers](https://docs.firefly-iii.org/how-to/firefly-iii/installation/self-managed/). +* Alternatively, read the upgrade instructions for [Docker](https://docs.firefly-iii.org/how-to/firefly-iii/upgrade/docker/), [Kubernetes](https://docs.firefly-iii.org/how-to/firefly-iii/upgrade/kubernetes/) or [self-managed servers](https://docs.firefly-iii.org/how-to/firefly-iii/upgrade/self-managed/) + +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/). + +## 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. From d762136e0b31f9cfe40bc2327330bc3f30be8055 Mon Sep 17 00:00:00 2001 From: James Cole Date: Wed, 13 Aug 2025 07:46:02 +0200 Subject: [PATCH 04/17] Clean up action. --- .github/workflows/release.yml | 113 +++------------------------------- 1 file changed, 10 insertions(+), 103 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 494617402d..ec6d04d575 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -242,111 +242,18 @@ jobs: env: FIREFLY_III_ROOT: /github/workspace GH_TOKEN: "" + - name: Generate release description + id: release-description + uses: JC5/firefly-iii-dev@main + with: + action: "ff3:generate-release-notes firefly-iii ${{ github.event.inputs.version }}" + output: 'output' + env: + FIREFLY_III_ROOT: /github/workspace + GH_TOKEN: "" - name: Describe new release run: | - - # describe the development release. - if [[ "develop" == "$version" ]]; then - echo 'Describe the latest develop release' - rm -f output.txt - touch output.txt - sudo chown -R runner:docker output.txt - echo "Weekly development release of Firefly III with the latest fixes, translations and features. Docker users can find this release under the \`develop\` tag." >> output.txt - echo "" >> output.txt - echo "This release was created on **$(date +'%Y-%m-%d %H:%M')** and may contain unexpected bugs. Data loss is rare but is not impossible. 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/)." >> output.txt - echo "" >> output.txt - echo "* Please read the installation instructions for [Docker](https://docs.firefly-iii.org/how-to/firefly-iii/installation/docker/), [Portainer](https://docs.firefly-iii.org/how-to/firefly-iii/installation/portainer/), [Kubernetes](https://docs.firefly-iii.org/how-to/firefly-iii/installation/kubernetes/) or [self-managed servers](https://docs.firefly-iii.org/how-to/firefly-iii/installation/self-managed/)" >> output.txt - echo "* Or read the upgrade instructions for [Docker](https://docs.firefly-iii.org/how-to/firefly-iii/upgrade/docker/), [Kubernetes](https://docs.firefly-iii.org/how-to/firefly-iii/upgrade/kubernetes/) or [self-managed servers](https://docs.firefly-iii.org/how-to/firefly-iii/upgrade/self-managed/)" >> output.txt - echo "" >> output.txt - echo ":warning: Please be careful with this pre-release, as it may not work as expected." >> output.txt - - # donations! - echo '' >> output.txt - echo '### Support Firefly III' >> output.txt - echo 'Did you know you can support the development of Firefly III? You can donate in many ways, like GitHub Sponsors or Patreon. For more information, please [follow this link](https://bit.ly/donate-to-Firefly-III) for more information.' >> output.txt - echo '' >> output.txt - fi - # describe a branch release - if [[ "$version" == branch* ]]; then - echo 'Describe a branch release' - rm -f output.txt - touch output.txt - sudo chown -R runner:docker output.txt - echo "Irregular BRANCH release of Firefly III. This release contains specific features or changes. Docker users can find this release under the \`$version\` tag." >> output.txt - echo "" >> output.txt - echo "This release was created on **$(date +'%Y-%m-%d %H:%M')** and may contain unexpected bugs. Data loss is rare but is not impossible. 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/)." >> output.txt - echo "" >> output.txt - echo "* Please read the installation instructions for [Docker](https://docs.firefly-iii.org/how-to/firefly-iii/installation/docker/), [Portainer](https://docs.firefly-iii.org/how-to/firefly-iii/installation/portainer/), [Kubernetes](https://docs.firefly-iii.org/how-to/firefly-iii/installation/kubernetes/) or [self-managed servers](https://docs.firefly-iii.org/how-to/firefly-iii/installation/self-managed/)" >> output.txt - echo "* Or read the upgrade instructions for [Docker](https://docs.firefly-iii.org/how-to/firefly-iii/upgrade/docker/), [Kubernetes](https://docs.firefly-iii.org/how-to/firefly-iii/upgrade/kubernetes/) or [self-managed servers](https://docs.firefly-iii.org/how-to/firefly-iii/upgrade/self-managed/)" >> output.txt - echo "" >> output.txt - echo ":warning: Please be careful with this branch pre-release, as it may not work as expected." >> output.txt - fi - # describe the main release - if [[ "develop" != "$version" ]] && [[ "$version" != branch* ]] && [[ "$version" != *alpha* ]] && [[ "$version" != *beta* ]]; then - echo 'Describe the latest release' - sudo chown -R runner:docker output.txt - - # the changelog is in output.txt - mv output.txt output2.txt - - touch output.txt - echo '' >> output.txt - echo "Welcome to release $version of Firefly III. It contains the the latest fixes, translations and features. Docker users can find this release under the \`latest\` tag." >> output.txt - echo '' >> output.txt - - # add changelog to file. - cat output2.txt >> output.txt - echo '' >> output.txt - rm -f output2.txt - - echo '### Instructions' >> output.txt - echo '' >> output.txt - echo "* Installation instructions for [Docker](https://docs.firefly-iii.org/how-to/firefly-iii/installation/docker/), [Portainer](https://docs.firefly-iii.org/how-to/firefly-iii/installation/portainer/), [Kubernetes](https://docs.firefly-iii.org/how-to/firefly-iii/installation/kubernetes/) or [self-managed servers](https://docs.firefly-iii.org/how-to/firefly-iii/installation/self-managed/)" >> output.txt - echo "* Or read the upgrade instructions for [Docker](https://docs.firefly-iii.org/how-to/firefly-iii/upgrade/docker/), [Kubernetes](https://docs.firefly-iii.org/how-to/firefly-iii/upgrade/kubernetes/) or [self-managed servers](https://docs.firefly-iii.org/how-to/firefly-iii/upgrade/self-managed/)" >> output.txt - echo "* 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/)." >> output.txt - - # donations! - echo '' >> output.txt - echo '### Support Firefly III' >> output.txt - echo 'Did you know you can support the development of Firefly III? You can donate in many ways, like GitHub Sponsors or Patreon. For more information, please [follow this link](https://bit.ly/donate-to-Firefly-III) for more information.' >> output.txt - echo '' >> output.txt - fi - - # describe alpha release - if [[ "$version" == *alpha* ]]; then - echo 'Describe an ALPHA release' - rm -f output.txt - touch output.txt - sudo chown -R runner:docker output.txt - echo "Very early ALPHA release of Firefly III. This release contains specific features or changes. Docker users can find this release under the \`$version\` tag." >> output.txt - echo '' >> output.txt - echo "This release was created on **$(date +'%Y-%m-%d %H:%M')** and may contain unexpected bugs. Data loss is rare but is not impossible. 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/)." >> output.txt - echo '' >> output.txt - echo '### Instructions' >> output.txt - echo '' >> output.txt - echo "* Installation instructions for [Docker](https://docs.firefly-iii.org/how-to/firefly-iii/installation/docker/), [Portainer](https://docs.firefly-iii.org/how-to/firefly-iii/installation/portainer/), [Kubernetes](https://docs.firefly-iii.org/how-to/firefly-iii/installation/kubernetes/) or [self-managed servers](https://docs.firefly-iii.org/how-to/firefly-iii/installation/self-managed/)" >> output.txt - echo "* Or read the upgrade instructions for [Docker](https://docs.firefly-iii.org/how-to/firefly-iii/upgrade/docker/), [Kubernetes](https://docs.firefly-iii.org/how-to/firefly-iii/upgrade/kubernetes/) or [self-managed servers](https://docs.firefly-iii.org/how-to/firefly-iii/upgrade/self-managed/)" >> output.txt - echo "* 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/)." >> output.txt - - fi - - # describe beta release - if [[ "$version" == *beta* ]]; then - echo 'Describe a BETA release' - rm -f output.txt - touch output.txt - sudo chown -R runner:docker output.txt - echo "Very early BETA release of Firefly III. This release contains specific features or changes. Docker users can find this release under the \`$version\` tag." >> output.txt - echo '' >> output.txt - echo "This release was created on **$(date +'%Y-%m-%d %H:%M')** and may contain unexpected bugs. Data loss is rare but is not impossible. 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/)." >> output.txt - echo '' >> output.txt - echo '### Instructions' >> output.txt - echo '' >> output.txt - echo "* Installation instructions for [Docker](https://docs.firefly-iii.org/how-to/firefly-iii/installation/docker/), [Portainer](https://docs.firefly-iii.org/how-to/firefly-iii/installation/portainer/), [Kubernetes](https://docs.firefly-iii.org/how-to/firefly-iii/installation/kubernetes/) or [self-managed servers](https://docs.firefly-iii.org/how-to/firefly-iii/installation/self-managed/)" >> output.txt - echo "* Or read the upgrade instructions for [Docker](https://docs.firefly-iii.org/how-to/firefly-iii/upgrade/docker/), [Kubernetes](https://docs.firefly-iii.org/how-to/firefly-iii/upgrade/kubernetes/) or [self-managed servers](https://docs.firefly-iii.org/how-to/firefly-iii/upgrade/self-managed/)" >> output.txt - echo "* 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/)." >> output.txt - - fi + echo "Should have nothing to do here, but just in case." env: version: ${{ github.event_name == 'schedule' && 'develop' || github.event.inputs.version }} - name: Merge all into working branch From d56088d08637f8001bc7d54c290ec92d564311d2 Mon Sep 17 00:00:00 2001 From: JC5 Date: Wed, 13 Aug 2025 07:52:52 +0200 Subject: [PATCH 05/17] =?UTF-8?q?=F0=9F=A4=96=20Auto=20commit=20for=20rele?= =?UTF-8?q?ase=20'develop'=20on=202025-08-13?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- composer.lock | 24 ++- config/firefly.php | 4 +- package-lock.json | 214 ++++++++++++------------ resources/assets/v1/src/locales/de.json | 2 +- 4 files changed, 128 insertions(+), 116 deletions(-) diff --git a/composer.lock b/composer.lock index d601034330..b502f9f472 100644 --- a/composer.lock +++ b/composer.lock @@ -12598,16 +12598,16 @@ }, { "name": "sebastian/recursion-context", - "version": "7.0.0", + "version": "7.0.1", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/recursion-context.git", - "reference": "c405ae3a63e01b32eb71577f8ec1604e39858a7c" + "reference": "0b01998a7d5b1f122911a66bebcb8d46f0c82d8c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/c405ae3a63e01b32eb71577f8ec1604e39858a7c", - "reference": "c405ae3a63e01b32eb71577f8ec1604e39858a7c", + "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/0b01998a7d5b1f122911a66bebcb8d46f0c82d8c", + "reference": "0b01998a7d5b1f122911a66bebcb8d46f0c82d8c", "shasum": "" }, "require": { @@ -12650,15 +12650,27 @@ "support": { "issues": "https://github.com/sebastianbergmann/recursion-context/issues", "security": "https://github.com/sebastianbergmann/recursion-context/security/policy", - "source": "https://github.com/sebastianbergmann/recursion-context/tree/7.0.0" + "source": "https://github.com/sebastianbergmann/recursion-context/tree/7.0.1" }, "funding": [ { "url": "https://github.com/sebastianbergmann", "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/recursion-context", + "type": "tidelift" } ], - "time": "2025-02-07T05:00:01+00:00" + "time": "2025-08-13T04:44:59+00:00" }, { "name": "sebastian/type", diff --git a/config/firefly.php b/config/firefly.php index a4af3951a7..8e7e49fabd 100644 --- a/config/firefly.php +++ b/config/firefly.php @@ -78,8 +78,8 @@ return [ 'running_balance_column' => env('USE_RUNNING_BALANCE', false), // see cer.php for exchange rates feature flag. ], - 'version' => '6.3.0-beta.1', - 'build_time' => 1755023090, + 'version' => 'develop/2025-08-13', + 'build_time' => 1755064254, 'api_version' => '2.1.0', // field is no longer used. 'db_version' => 26, diff --git a/package-lock.json b/package-lock.json index 3d52360e60..563fe2e3a4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1707,9 +1707,9 @@ "license": "MIT" }, "node_modules/@esbuild/aix-ppc64": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.8.tgz", - "integrity": "sha512-urAvrUedIqEiFR3FYSLTWQgLu5tb+m0qZw0NBEasUeo6wuqatkMDaRT+1uABiGXEu5vqgPd7FGE1BhsAIy9QVA==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.9.tgz", + "integrity": "sha512-OaGtL73Jck6pBKjNIe24BnFE6agGl+6KxDtTfHhy1HmhthfKouEcOhqpSL64K4/0WCtbKFLOdzD/44cJ4k9opA==", "cpu": [ "ppc64" ], @@ -1724,9 +1724,9 @@ } }, "node_modules/@esbuild/android-arm": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.8.tgz", - "integrity": "sha512-RONsAvGCz5oWyePVnLdZY/HHwA++nxYWIX1atInlaW6SEkwq6XkP3+cb825EUcRs5Vss/lGh/2YxAb5xqc07Uw==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.9.tgz", + "integrity": "sha512-5WNI1DaMtxQ7t7B6xa572XMXpHAaI/9Hnhk8lcxF4zVN4xstUgTlvuGDorBguKEnZO70qwEcLpfifMLoxiPqHQ==", "cpu": [ "arm" ], @@ -1741,9 +1741,9 @@ } }, "node_modules/@esbuild/android-arm64": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.8.tgz", - "integrity": "sha512-OD3p7LYzWpLhZEyATcTSJ67qB5D+20vbtr6vHlHWSQYhKtzUYrETuWThmzFpZtFsBIxRvhO07+UgVA9m0i/O1w==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.9.tgz", + "integrity": "sha512-IDrddSmpSv51ftWslJMvl3Q2ZT98fUSL2/rlUXuVqRXHCs5EUF1/f+jbjF5+NG9UffUDMCiTyh8iec7u8RlTLg==", "cpu": [ "arm64" ], @@ -1758,9 +1758,9 @@ } }, "node_modules/@esbuild/android-x64": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.8.tgz", - "integrity": "sha512-yJAVPklM5+4+9dTeKwHOaA+LQkmrKFX96BM0A/2zQrbS6ENCmxc4OVoBs5dPkCCak2roAD+jKCdnmOqKszPkjA==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.9.tgz", + "integrity": "sha512-I853iMZ1hWZdNllhVZKm34f4wErd4lMyeV7BLzEExGEIZYsOzqDWDf+y082izYUE8gtJnYHdeDpN/6tUdwvfiw==", "cpu": [ "x64" ], @@ -1775,9 +1775,9 @@ } }, "node_modules/@esbuild/darwin-arm64": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.8.tgz", - "integrity": "sha512-Jw0mxgIaYX6R8ODrdkLLPwBqHTtYHJSmzzd+QeytSugzQ0Vg4c5rDky5VgkoowbZQahCbsv1rT1KW72MPIkevw==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.9.tgz", + "integrity": "sha512-XIpIDMAjOELi/9PB30vEbVMs3GV1v2zkkPnuyRRURbhqjyzIINwj+nbQATh4H9GxUgH1kFsEyQMxwiLFKUS6Rg==", "cpu": [ "arm64" ], @@ -1792,9 +1792,9 @@ } }, "node_modules/@esbuild/darwin-x64": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.8.tgz", - "integrity": "sha512-Vh2gLxxHnuoQ+GjPNvDSDRpoBCUzY4Pu0kBqMBDlK4fuWbKgGtmDIeEC081xi26PPjn+1tct+Bh8FjyLlw1Zlg==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.9.tgz", + "integrity": "sha512-jhHfBzjYTA1IQu8VyrjCX4ApJDnH+ez+IYVEoJHeqJm9VhG9Dh2BYaJritkYK3vMaXrf7Ogr/0MQ8/MeIefsPQ==", "cpu": [ "x64" ], @@ -1809,9 +1809,9 @@ } }, "node_modules/@esbuild/freebsd-arm64": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.8.tgz", - "integrity": "sha512-YPJ7hDQ9DnNe5vxOm6jaie9QsTwcKedPvizTVlqWG9GBSq+BuyWEDazlGaDTC5NGU4QJd666V0yqCBL2oWKPfA==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.9.tgz", + "integrity": "sha512-z93DmbnY6fX9+KdD4Ue/H6sYs+bhFQJNCPZsi4XWJoYblUqT06MQUdBCpcSfuiN72AbqeBFu5LVQTjfXDE2A6Q==", "cpu": [ "arm64" ], @@ -1826,9 +1826,9 @@ } }, "node_modules/@esbuild/freebsd-x64": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.8.tgz", - "integrity": "sha512-MmaEXxQRdXNFsRN/KcIimLnSJrk2r5H8v+WVafRWz5xdSVmWLoITZQXcgehI2ZE6gioE6HirAEToM/RvFBeuhw==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.9.tgz", + "integrity": "sha512-mrKX6H/vOyo5v71YfXWJxLVxgy1kyt1MQaD8wZJgJfG4gq4DpQGpgTB74e5yBeQdyMTbgxp0YtNj7NuHN0PoZg==", "cpu": [ "x64" ], @@ -1843,9 +1843,9 @@ } }, "node_modules/@esbuild/linux-arm": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.8.tgz", - "integrity": "sha512-FuzEP9BixzZohl1kLf76KEVOsxtIBFwCaLupVuk4eFVnOZfU+Wsn+x5Ryam7nILV2pkq2TqQM9EZPsOBuMC+kg==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.9.tgz", + "integrity": "sha512-HBU2Xv78SMgaydBmdor38lg8YDnFKSARg1Q6AT0/y2ezUAKiZvc211RDFHlEZRFNRVhcMamiToo7bDx3VEOYQw==", "cpu": [ "arm" ], @@ -1860,9 +1860,9 @@ } }, "node_modules/@esbuild/linux-arm64": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.8.tgz", - "integrity": "sha512-WIgg00ARWv/uYLU7lsuDK00d/hHSfES5BzdWAdAig1ioV5kaFNrtK8EqGcUBJhYqotlUByUKz5Qo6u8tt7iD/w==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.9.tgz", + "integrity": "sha512-BlB7bIcLT3G26urh5Dmse7fiLmLXnRlopw4s8DalgZ8ef79Jj4aUcYbk90g8iCa2467HX8SAIidbL7gsqXHdRw==", "cpu": [ "arm64" ], @@ -1877,9 +1877,9 @@ } }, "node_modules/@esbuild/linux-ia32": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.8.tgz", - "integrity": "sha512-A1D9YzRX1i+1AJZuFFUMP1E9fMaYY+GnSQil9Tlw05utlE86EKTUA7RjwHDkEitmLYiFsRd9HwKBPEftNdBfjg==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.9.tgz", + "integrity": "sha512-e7S3MOJPZGp2QW6AK6+Ly81rC7oOSerQ+P8L0ta4FhVi+/j/v2yZzx5CqqDaWjtPFfYz21Vi1S0auHrap3Ma3A==", "cpu": [ "ia32" ], @@ -1894,9 +1894,9 @@ } }, "node_modules/@esbuild/linux-loong64": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.8.tgz", - "integrity": "sha512-O7k1J/dwHkY1RMVvglFHl1HzutGEFFZ3kNiDMSOyUrB7WcoHGf96Sh+64nTRT26l3GMbCW01Ekh/ThKM5iI7hQ==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.9.tgz", + "integrity": "sha512-Sbe10Bnn0oUAB2AalYztvGcK+o6YFFA/9829PhOCUS9vkJElXGdphz0A3DbMdP8gmKkqPmPcMJmJOrI3VYB1JQ==", "cpu": [ "loong64" ], @@ -1911,9 +1911,9 @@ } }, "node_modules/@esbuild/linux-mips64el": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.8.tgz", - "integrity": "sha512-uv+dqfRazte3BzfMp8PAQXmdGHQt2oC/y2ovwpTteqrMx2lwaksiFZ/bdkXJC19ttTvNXBuWH53zy/aTj1FgGw==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.9.tgz", + "integrity": "sha512-YcM5br0mVyZw2jcQeLIkhWtKPeVfAerES5PvOzaDxVtIyZ2NUBZKNLjC5z3/fUlDgT6w89VsxP2qzNipOaaDyA==", "cpu": [ "mips64el" ], @@ -1928,9 +1928,9 @@ } }, "node_modules/@esbuild/linux-ppc64": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.8.tgz", - "integrity": "sha512-GyG0KcMi1GBavP5JgAkkstMGyMholMDybAf8wF5A70CALlDM2p/f7YFE7H92eDeH/VBtFJA5MT4nRPDGg4JuzQ==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.9.tgz", + "integrity": "sha512-++0HQvasdo20JytyDpFvQtNrEsAgNG2CY1CLMwGXfFTKGBGQT3bOeLSYE2l1fYdvML5KUuwn9Z8L1EWe2tzs1w==", "cpu": [ "ppc64" ], @@ -1945,9 +1945,9 @@ } }, "node_modules/@esbuild/linux-riscv64": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.8.tgz", - "integrity": "sha512-rAqDYFv3yzMrq7GIcen3XP7TUEG/4LK86LUPMIz6RT8A6pRIDn0sDcvjudVZBiiTcZCY9y2SgYX2lgK3AF+1eg==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.9.tgz", + "integrity": "sha512-uNIBa279Y3fkjV+2cUjx36xkx7eSjb8IvnL01eXUKXez/CBHNRw5ekCGMPM0BcmqBxBcdgUWuUXmVWwm4CH9kg==", "cpu": [ "riscv64" ], @@ -1962,9 +1962,9 @@ } }, "node_modules/@esbuild/linux-s390x": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.8.tgz", - "integrity": "sha512-Xutvh6VjlbcHpsIIbwY8GVRbwoviWT19tFhgdA7DlenLGC/mbc3lBoVb7jxj9Z+eyGqvcnSyIltYUrkKzWqSvg==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.9.tgz", + "integrity": "sha512-Mfiphvp3MjC/lctb+7D287Xw1DGzqJPb/J2aHHcHxflUo+8tmN/6d4k6I2yFR7BVo5/g7x2Monq4+Yew0EHRIA==", "cpu": [ "s390x" ], @@ -1979,9 +1979,9 @@ } }, "node_modules/@esbuild/linux-x64": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.8.tgz", - "integrity": "sha512-ASFQhgY4ElXh3nDcOMTkQero4b1lgubskNlhIfJrsH5OKZXDpUAKBlNS0Kx81jwOBp+HCeZqmoJuihTv57/jvQ==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.9.tgz", + "integrity": "sha512-iSwByxzRe48YVkmpbgoxVzn76BXjlYFXC7NvLYq+b+kDjyyk30J0JY47DIn8z1MO3K0oSl9fZoRmZPQI4Hklzg==", "cpu": [ "x64" ], @@ -1996,9 +1996,9 @@ } }, "node_modules/@esbuild/netbsd-arm64": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.8.tgz", - "integrity": "sha512-d1KfruIeohqAi6SA+gENMuObDbEjn22olAR7egqnkCD9DGBG0wsEARotkLgXDu6c4ncgWTZJtN5vcgxzWRMzcw==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.9.tgz", + "integrity": "sha512-9jNJl6FqaUG+COdQMjSCGW4QiMHH88xWbvZ+kRVblZsWrkXlABuGdFJ1E9L7HK+T0Yqd4akKNa/lO0+jDxQD4Q==", "cpu": [ "arm64" ], @@ -2013,9 +2013,9 @@ } }, "node_modules/@esbuild/netbsd-x64": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.8.tgz", - "integrity": "sha512-nVDCkrvx2ua+XQNyfrujIG38+YGyuy2Ru9kKVNyh5jAys6n+l44tTtToqHjino2My8VAY6Lw9H7RI73XFi66Cg==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.9.tgz", + "integrity": "sha512-RLLdkflmqRG8KanPGOU7Rpg829ZHu8nFy5Pqdi9U01VYtG9Y0zOG6Vr2z4/S+/3zIyOxiK6cCeYNWOFR9QP87g==", "cpu": [ "x64" ], @@ -2030,9 +2030,9 @@ } }, "node_modules/@esbuild/openbsd-arm64": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.8.tgz", - "integrity": "sha512-j8HgrDuSJFAujkivSMSfPQSAa5Fxbvk4rgNAS5i3K+r8s1X0p1uOO2Hl2xNsGFppOeHOLAVgYwDVlmxhq5h+SQ==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.9.tgz", + "integrity": "sha512-YaFBlPGeDasft5IIM+CQAhJAqS3St3nJzDEgsgFixcfZeyGPCd6eJBWzke5piZuZ7CtL656eOSYKk4Ls2C0FRQ==", "cpu": [ "arm64" ], @@ -2047,9 +2047,9 @@ } }, "node_modules/@esbuild/openbsd-x64": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.8.tgz", - "integrity": "sha512-1h8MUAwa0VhNCDp6Af0HToI2TJFAn1uqT9Al6DJVzdIBAd21m/G0Yfc77KDM3uF3T/YaOgQq3qTJHPbTOInaIQ==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.9.tgz", + "integrity": "sha512-1MkgTCuvMGWuqVtAvkpkXFmtL8XhWy+j4jaSO2wxfJtilVCi0ZE37b8uOdMItIHz4I6z1bWWtEX4CJwcKYLcuA==", "cpu": [ "x64" ], @@ -2064,9 +2064,9 @@ } }, "node_modules/@esbuild/openharmony-arm64": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.8.tgz", - "integrity": "sha512-r2nVa5SIK9tSWd0kJd9HCffnDHKchTGikb//9c7HX+r+wHYCpQrSgxhlY6KWV1nFo1l4KFbsMlHk+L6fekLsUg==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.9.tgz", + "integrity": "sha512-4Xd0xNiMVXKh6Fa7HEJQbrpP3m3DDn43jKxMjxLLRjWnRsfxjORYJlXPO4JNcXtOyfajXorRKY9NkOpTHptErg==", "cpu": [ "arm64" ], @@ -2081,9 +2081,9 @@ } }, "node_modules/@esbuild/sunos-x64": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.8.tgz", - "integrity": "sha512-zUlaP2S12YhQ2UzUfcCuMDHQFJyKABkAjvO5YSndMiIkMimPmxA+BYSBikWgsRpvyxuRnow4nS5NPnf9fpv41w==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.9.tgz", + "integrity": "sha512-WjH4s6hzo00nNezhp3wFIAfmGZ8U7KtrJNlFMRKxiI9mxEK1scOMAaa9i4crUtu+tBr+0IN6JCuAcSBJZfnphw==", "cpu": [ "x64" ], @@ -2098,9 +2098,9 @@ } }, "node_modules/@esbuild/win32-arm64": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.8.tgz", - "integrity": "sha512-YEGFFWESlPva8hGL+zvj2z/SaK+pH0SwOM0Nc/d+rVnW7GSTFlLBGzZkuSU9kFIGIo8q9X3ucpZhu8PDN5A2sQ==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.9.tgz", + "integrity": "sha512-mGFrVJHmZiRqmP8xFOc6b84/7xa5y5YvR1x8djzXpJBSv/UsNK6aqec+6JDjConTgvvQefdGhFDAs2DLAds6gQ==", "cpu": [ "arm64" ], @@ -2115,9 +2115,9 @@ } }, "node_modules/@esbuild/win32-ia32": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.8.tgz", - "integrity": "sha512-hiGgGC6KZ5LZz58OL/+qVVoZiuZlUYlYHNAmczOm7bs2oE1XriPFi5ZHHrS8ACpV5EjySrnoCKmcbQMN+ojnHg==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.9.tgz", + "integrity": "sha512-b33gLVU2k11nVx1OhX3C8QQP6UHQK4ZtN56oFWvVXvz2VkDoe6fbG8TOgHFxEvqeqohmRnIHe5A1+HADk4OQww==", "cpu": [ "ia32" ], @@ -2132,9 +2132,9 @@ } }, "node_modules/@esbuild/win32-x64": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.8.tgz", - "integrity": "sha512-cn3Yr7+OaaZq1c+2pe+8yxC8E144SReCQjN6/2ynubzYjvyqZjTXfQJpAcQpsdJq3My7XADANiYGHoFC69pLQw==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.9.tgz", + "integrity": "sha512-PPOl1mi6lpLNQxnGoyAfschAodRFYXJ+9fs6WHXz7CSWKbOqiMZsubC+BQsVKuul+3vKLuwTHsS2c2y9EoKwxQ==", "cpu": [ "x64" ], @@ -5863,9 +5863,9 @@ } }, "node_modules/esbuild": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.8.tgz", - "integrity": "sha512-vVC0USHGtMi8+R4Kz8rt6JhEWLxsv9Rnu/lGYbPR8u47B+DCBksq9JarW0zOO7bs37hyOK1l2/oqtbciutL5+Q==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.9.tgz", + "integrity": "sha512-CRbODhYyQx3qp7ZEwzxOk4JBqmD/seJrzPa/cGjY1VtIn5E09Oi9/dB4JwctnfZ8Q8iT7rioVv5k/FNT/uf54g==", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -5876,32 +5876,32 @@ "node": ">=18" }, "optionalDependencies": { - "@esbuild/aix-ppc64": "0.25.8", - "@esbuild/android-arm": "0.25.8", - "@esbuild/android-arm64": "0.25.8", - "@esbuild/android-x64": "0.25.8", - "@esbuild/darwin-arm64": "0.25.8", - "@esbuild/darwin-x64": "0.25.8", - "@esbuild/freebsd-arm64": "0.25.8", - "@esbuild/freebsd-x64": "0.25.8", - "@esbuild/linux-arm": "0.25.8", - "@esbuild/linux-arm64": "0.25.8", - "@esbuild/linux-ia32": "0.25.8", - "@esbuild/linux-loong64": "0.25.8", - "@esbuild/linux-mips64el": "0.25.8", - "@esbuild/linux-ppc64": "0.25.8", - "@esbuild/linux-riscv64": "0.25.8", - "@esbuild/linux-s390x": "0.25.8", - "@esbuild/linux-x64": "0.25.8", - "@esbuild/netbsd-arm64": "0.25.8", - "@esbuild/netbsd-x64": "0.25.8", - "@esbuild/openbsd-arm64": "0.25.8", - "@esbuild/openbsd-x64": "0.25.8", - "@esbuild/openharmony-arm64": "0.25.8", - "@esbuild/sunos-x64": "0.25.8", - "@esbuild/win32-arm64": "0.25.8", - "@esbuild/win32-ia32": "0.25.8", - "@esbuild/win32-x64": "0.25.8" + "@esbuild/aix-ppc64": "0.25.9", + "@esbuild/android-arm": "0.25.9", + "@esbuild/android-arm64": "0.25.9", + "@esbuild/android-x64": "0.25.9", + "@esbuild/darwin-arm64": "0.25.9", + "@esbuild/darwin-x64": "0.25.9", + "@esbuild/freebsd-arm64": "0.25.9", + "@esbuild/freebsd-x64": "0.25.9", + "@esbuild/linux-arm": "0.25.9", + "@esbuild/linux-arm64": "0.25.9", + "@esbuild/linux-ia32": "0.25.9", + "@esbuild/linux-loong64": "0.25.9", + "@esbuild/linux-mips64el": "0.25.9", + "@esbuild/linux-ppc64": "0.25.9", + "@esbuild/linux-riscv64": "0.25.9", + "@esbuild/linux-s390x": "0.25.9", + "@esbuild/linux-x64": "0.25.9", + "@esbuild/netbsd-arm64": "0.25.9", + "@esbuild/netbsd-x64": "0.25.9", + "@esbuild/openbsd-arm64": "0.25.9", + "@esbuild/openbsd-x64": "0.25.9", + "@esbuild/openharmony-arm64": "0.25.9", + "@esbuild/sunos-x64": "0.25.9", + "@esbuild/win32-arm64": "0.25.9", + "@esbuild/win32-ia32": "0.25.9", + "@esbuild/win32-x64": "0.25.9" } }, "node_modules/escalade": { diff --git a/resources/assets/v1/src/locales/de.json b/resources/assets/v1/src/locales/de.json index b15143e386..5b0607c38e 100644 --- a/resources/assets/v1/src/locales/de.json +++ b/resources/assets/v1/src/locales/de.json @@ -102,7 +102,7 @@ "profile_oauth_client_secret_title": "Client Secret", "profile_oauth_client_secret_expl": "Hier ist Ihr neuer pers\u00f6nlicher Zugangsschl\u00fcssel. Dies ist das einzige Mal, dass er angezeigt wird, also verlieren Sie ihn nicht! Sie k\u00f6nnen diesen Token jetzt verwenden, um API-Anfragen zu stellen.", "profile_oauth_confidential": "Vertraulich", - "profile_oauth_confidential_help": "Require the client to authenticate with a secret. Confidential clients can hold credentials in a secure way without exposing them to unauthorized parties. Public applications, such as native desktop or JavaScript SPA applications, are unable to hold secrets securely.", + "profile_oauth_confidential_help": "Verlangen Sie vom Client, sich mit einem Geheimnis zu authentifizieren. Vertrauliche Clients k\u00f6nnen Anmeldedaten auf sichere Weise speichern, ohne sie Unbefugten zug\u00e4nglich zu machen. \u00d6ffentliche Anwendungen, wie native Desktop- oder JavaScript-SPA-Anwendungen, sind nicht f\u00e4hig, Geheimnisse sicher zu speichern.", "multi_account_warning_unknown": "Abh\u00e4ngig von der Art der Buchung, die Sie anlegen, kann das Quell- und\/oder Zielkonto nachfolgender Aufteilungen durch das \u00fcberschrieben werden, was in der ersten Aufteilung der Buchung definiert wurde.", "multi_account_warning_withdrawal": "Bedenken Sie, dass das Quellkonto nachfolgender Aufteilungen von dem, was in der ersten Aufteilung der Abhebung definiert ist, au\u00dfer Kraft gesetzt wird.", "multi_account_warning_deposit": "Bedenken Sie, dass das Zielkonto nachfolgender Aufteilungen von dem, was in der ersten Aufteilung der Einnahmen definiert ist, au\u00dfer Kraft gesetzt wird.", From b39e3e5965974652448a2fe9fa6665f7340f560c Mon Sep 17 00:00:00 2001 From: James Cole Date: Wed, 13 Aug 2025 07:56:31 +0200 Subject: [PATCH 06/17] Expand template. --- .github/release-notes/develop.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.github/release-notes/develop.md b/.github/release-notes/develop.md index 6b38234b65..236b2dfde8 100644 --- a/.github/release-notes/develop.md +++ b/.github/release-notes/develop.md @@ -2,7 +2,11 @@ Welcome to the latest development release of Firefly III. This test release cont :warning: Please be careful with this pre-release, as it may not work as expected. -This release was created on %date and may contain unexpected bugs. Data loss is rare but is entirely possible. A changelog is not included with this development release. +This release was created on %date and may contain unexpected bugs. Data loss is rare but is entirely possible. A changelog is not included with this development release. + +## Changelog + +The changelog for this release may not be up-to-date, so it is not included. However, [changelog.md](https://github.com/firefly-iii/firefly-iii/blob/develop/changelog.md) may already contain entries for the future release. ## Installation and upgrade instructions From 33ea27b411fb48d7ce081e673217bfe6c9b7f7cf Mon Sep 17 00:00:00 2001 From: James Cole Date: Wed, 13 Aug 2025 08:03:59 +0200 Subject: [PATCH 07/17] Fix template --- .github/release-notes/develop.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/release-notes/develop.md b/.github/release-notes/develop.md index 236b2dfde8..69a0670cba 100644 --- a/.github/release-notes/develop.md +++ b/.github/release-notes/develop.md @@ -2,7 +2,7 @@ Welcome to the latest development release of Firefly III. This test release cont :warning: Please be careful with this pre-release, as it may not work as expected. -This release was created on %date and may contain unexpected bugs. Data loss is rare but is entirely possible. A changelog is not included with this development release. +This release was created on %date and may contain unexpected bugs. Data loss is rare but is entirely possible. ## Changelog From 020c8ad933b3fcd0812b48a77170ab25ffbce1ce Mon Sep 17 00:00:00 2001 From: James Cole Date: Wed, 13 Aug 2025 10:15:36 +0200 Subject: [PATCH 08/17] Clean up and expand templates. --- .github/release-notes/alpha.md | 8 ++++---- .github/release-notes/beta.md | 10 +++++----- .github/release-notes/branch.md | 21 ++++++++++++++++++++- .github/release-notes/develop.md | 4 ++-- .github/release-notes/release.md | 2 +- 5 files changed, 32 insertions(+), 13 deletions(-) diff --git a/.github/release-notes/alpha.md b/.github/release-notes/alpha.md index 7b775f0893..a9dca0594a 100644 --- a/.github/release-notes/alpha.md +++ b/.github/release-notes/alpha.md @@ -1,8 +1,8 @@ -Welcome to this ALPHA release of Firefly III. This alpha release contains the latest fixes, translations and features. It is probably buggy and may not work as expected. You can download the release below, and adventurous Docker users can find this release under the `alpha` tag. +Welcome to release %version of Firefly III. This **alpha** release contains the latest fixes, translations and features. It is probably buggy and may not work as expected. You can download the release below, and adventurous Docker users can find this release under the `alpha` tag. :warning: Please be careful with this alpha release, as it may not work as expected. -Alpha releases are created to test new features and fixes before they are included in a stable release. They are not recommended for production use. This release was created on %date and may contain unexpected bugs. Data loss is rare but is entirely possible. +Alpha releases are created to test new features and fixes before they are included in a stable release. They are not recommended for production use. This release was created on %date and may contain unexpected bugs. Data loss is rare but possible. ## Changelog (not final) @@ -13,8 +13,8 @@ Alpha releases are created to test new features and fixes before they are includ * Please read the installation instructions for [Docker](https://docs.firefly-iii.org/how-to/firefly-iii/installation/docker/), [Portainer](https://docs.firefly-iii.org/how-to/firefly-iii/installation/portainer/), [Kubernetes](https://docs.firefly-iii.org/how-to/firefly-iii/installation/kubernetes/) or [self-managed servers](https://docs.firefly-iii.org/how-to/firefly-iii/installation/self-managed/). * Alternatively, read the upgrade instructions for [Docker](https://docs.firefly-iii.org/how-to/firefly-iii/upgrade/docker/), [Kubernetes](https://docs.firefly-iii.org/how-to/firefly-iii/upgrade/kubernetes/) or [self-managed servers](https://docs.firefly-iii.org/how-to/firefly-iii/upgrade/self-managed/) -The releases are signed, and you can verify them using the [Firefly III releases PGP key](https://docs.firefly-iii.org/explanation/more-information/signatures/). +The 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/). ## 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. +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. diff --git a/.github/release-notes/beta.md b/.github/release-notes/beta.md index 0a8f9a9d36..300aceee44 100644 --- a/.github/release-notes/beta.md +++ b/.github/release-notes/beta.md @@ -1,8 +1,8 @@ -Welcome to this BETA release of Firefly III. This beta release contains the latest fixes, translations and features. It should not be buggy, but may not not work as expected. You can download the release below, and adventurous Docker users can find this release under the `beta` tag. +Welcome to release %version of Firefly III. This **beta** release contains the latest fixes, translations and features. It may be buggy, nor work as expected. You can download the release below, and adventurous Docker users can find this release under the `beta` tag. -:warning: Please be careful with this release, as it may not work as expected. +:warning: Please be careful with this beta release, as it may not work as expected. -Beta releases are created to test new features and fixes before they are included in a stable release. They are not recommended for production use. This release was created on %date and may contain unexpected bugs. Data loss is rare but is entirely possible. +Alpha releases are created to test new features and fixes before they are included in a stable release. They are not recommended for production use. This release was created on %date and may contain unexpected bugs. Data loss is rare but possible. ## Changelog (not final) @@ -13,8 +13,8 @@ Beta releases are created to test new features and fixes before they are include * Please read the installation instructions for [Docker](https://docs.firefly-iii.org/how-to/firefly-iii/installation/docker/), [Portainer](https://docs.firefly-iii.org/how-to/firefly-iii/installation/portainer/), [Kubernetes](https://docs.firefly-iii.org/how-to/firefly-iii/installation/kubernetes/) or [self-managed servers](https://docs.firefly-iii.org/how-to/firefly-iii/installation/self-managed/). * Alternatively, read the upgrade instructions for [Docker](https://docs.firefly-iii.org/how-to/firefly-iii/upgrade/docker/), [Kubernetes](https://docs.firefly-iii.org/how-to/firefly-iii/upgrade/kubernetes/) or [self-managed servers](https://docs.firefly-iii.org/how-to/firefly-iii/upgrade/self-managed/) -The releases are signed, and you can verify them using the [Firefly III releases PGP key](https://docs.firefly-iii.org/explanation/more-information/signatures/). +The 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/). ## 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. +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. diff --git a/.github/release-notes/branch.md b/.github/release-notes/branch.md index 874c6610ad..f5fe6ac5a3 100644 --- a/.github/release-notes/branch.md +++ b/.github/release-notes/branch.md @@ -1 +1,20 @@ -(not yet in use) +Welcome to release %version of Firefly III. This branch-related release contains the latest fixes, translations and features. It is probably buggy and may not work as expected. You can download the release below, and adventurous Docker users can find this release under the `branch-*` tag. + +:warning: Please be careful with this branch release, as it may not work as expected. + +Branch releases are created to test large new features that are developed alongside the normal release flow. They are not recommended for production use. This release was created on %date and may contain unexpected bugs. Data loss is rare but possible. + +## Changelog + +There is no changelog for this release, as it is not final. However, [changelog.md](https://github.com/firefly-iii/firefly-iii/blob/develop/changelog.md) may already contain entries for the future release that this branch will be a part of. + +## Installation and upgrade instructions + +* Please read the installation instructions for [Docker](https://docs.firefly-iii.org/how-to/firefly-iii/installation/docker/), [Portainer](https://docs.firefly-iii.org/how-to/firefly-iii/installation/portainer/), [Kubernetes](https://docs.firefly-iii.org/how-to/firefly-iii/installation/kubernetes/) or [self-managed servers](https://docs.firefly-iii.org/how-to/firefly-iii/installation/self-managed/). +* Alternatively, read the upgrade instructions for [Docker](https://docs.firefly-iii.org/how-to/firefly-iii/upgrade/docker/), [Kubernetes](https://docs.firefly-iii.org/how-to/firefly-iii/upgrade/kubernetes/) or [self-managed servers](https://docs.firefly-iii.org/how-to/firefly-iii/upgrade/self-managed/) + +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/). + +## 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. diff --git a/.github/release-notes/develop.md b/.github/release-notes/develop.md index 69a0670cba..8b5271af05 100644 --- a/.github/release-notes/develop.md +++ b/.github/release-notes/develop.md @@ -2,7 +2,7 @@ Welcome to the latest development release of Firefly III. This test release cont :warning: Please be careful with this pre-release, as it may not work as expected. -This release was created on %date and may contain unexpected bugs. Data loss is rare but is entirely possible. +This release was created on %date and may contain unexpected bugs. Data loss is rare but possible. ## Changelog @@ -17,4 +17,4 @@ The releases are signed, and you can verify them using the [Firefly III releases ## 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. +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. diff --git a/.github/release-notes/release.md b/.github/release-notes/release.md index 950b519e01..c1442d5630 100644 --- a/.github/release-notes/release.md +++ b/.github/release-notes/release.md @@ -13,4 +13,4 @@ The releases are signed, and you can verify them using the [Firefly III releases ## 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. +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. From fc9ef290f1aff58f8ac93ec0a666f075690f42be Mon Sep 17 00:00:00 2001 From: James Cole Date: Fri, 15 Aug 2025 07:11:34 +0200 Subject: [PATCH 09/17] Clean up API endpoints. --- .../Controllers/Chart/AccountController.php | 122 +++--------------- .../Controllers/Chart/BalanceController.php | 17 ++- app/Support/Chart/ChartData.php | 3 + .../Http/Api/AccountBalanceGrouped.php | 92 ++++++------- app/Support/Http/Api/CleansChartData.php | 32 ++--- .../Http/Api/CollectsAccountsFromFilter.php | 2 +- routes/api.php | 31 +++-- 7 files changed, 119 insertions(+), 180 deletions(-) diff --git a/app/Api/V1/Controllers/Chart/AccountController.php b/app/Api/V1/Controllers/Chart/AccountController.php index becc661d3e..62212f71e2 100644 --- a/app/Api/V1/Controllers/Chart/AccountController.php +++ b/app/Api/V1/Controllers/Chart/AccountController.php @@ -24,13 +24,11 @@ declare(strict_types=1); namespace FireflyIII\Api\V1\Controllers\Chart; -use Carbon\Carbon; use FireflyIII\Api\V1\Controllers\Controller; use FireflyIII\Api\V1\Requests\Chart\ChartRequest; -use FireflyIII\Api\V1\Requests\Data\DateRequest; use FireflyIII\Enums\AccountTypeEnum; +use FireflyIII\Enums\UserRoleEnum; use FireflyIII\Exceptions\FireflyException; -use FireflyIII\Exceptions\ValidationException; use FireflyIII\Models\Account; use FireflyIII\Models\Preference; use FireflyIII\Models\TransactionCurrency; @@ -40,7 +38,6 @@ use FireflyIII\Support\Facades\Preferences; use FireflyIII\Support\Facades\Steam; use FireflyIII\Support\Http\Api\ApiSupport; use FireflyIII\Support\Http\Api\CollectsAccountsFromFilter; -use FireflyIII\User; use Illuminate\Http\JsonResponse; use Illuminate\Support\Facades\Log; @@ -52,6 +49,8 @@ class AccountController extends Controller use ApiSupport; use CollectsAccountsFromFilter; + protected array $acceptedRoles = [UserRoleEnum::READ_ONLY]; + private ChartData $chartData; private AccountRepositoryInterface $repository; @@ -63,11 +62,11 @@ class AccountController extends Controller parent::__construct(); $this->middleware( function ($request, $next) { - /** @var User $user */ - $user = auth()->user(); $this->chartData = new ChartData(); $this->repository = app(AccountRepositoryInterface::class); - $this->repository->setUser($user); + + $userGroup = $this->validateUserGroup($request); + $this->repository->setUserGroup($userGroup); return $next($request); } @@ -75,11 +74,9 @@ class AccountController extends Controller } /** - * TODO fix documentation - * * @throws FireflyException */ - public function dashboard(ChartRequest $request): JsonResponse + public function overview(ChartRequest $request): JsonResponse { $queryParameters = $request->getParameters(); $accounts = $this->getAccountList($queryParameters); @@ -110,24 +107,30 @@ class AccountController extends Controller $range = Steam::finalAccountBalanceInRange($account, $params['start'], clone $params['end'], $this->convertToPrimary); - $previous = array_values($range)[0]['balance']; - $pcPrevious = null; + $previous = array_values($range)[0]['balance']; + $pcPrevious = null; if (!$currency instanceof TransactionCurrency) { $currency = $this->default; } - $currentSet = [ + $currentSet = [ 'label' => $account->name, // the currency that belongs to the account. 'currency_id' => (string)$currency->id, + 'currency_name' => $currency->name, 'currency_code' => $currency->code, 'currency_symbol' => $currency->symbol, 'currency_decimal_places' => $currency->decimal_places, + // the primary currency + 'primary_currency_id' => (string)$this->primaryCurrency->id, + // the default currency of the user (could be the same!) 'date' => $params['start']->toAtomString(), - 'start' => $params['start']->toAtomString(), - 'end' => $params['end']->toAtomString(), + 'start_date' => $params['start']->toAtomString(), + 'end_date' => $params['end']->toAtomString(), + 'type' => 'line', + 'yAxisID' => 0, 'period' => '1D', 'entries' => [], ]; @@ -150,7 +153,7 @@ class AccountController extends Controller // do the same for the primary currency balance, if relevant: - $pcBalance = null; + $pcBalance = null; if ($this->convertToPrimary) { $pcBalance = array_key_exists($format, $range) ? $range[$format]['pc_balance'] : $pcPrevious; $pcPrevious = $pcBalance; @@ -162,97 +165,12 @@ class AccountController extends Controller $this->chartData->add($currentSet); } - /** - * This endpoint is documented at: - * https://api-docs.firefly-iii.org/?urls.primaryName=2.0.0%20(v1)#/charts/getChartAccountOverview - * - * @throws ValidationException - */ - public function overview(DateRequest $request): JsonResponse - { - // parameters for chart: - $dates = $request->getAll(); - - - /** @var Carbon $start */ - $start = $dates['start']; - - /** @var Carbon $end */ - $end = $dates['end']; - - // set dates to end of day + start of day: - $start->startOfDay(); - $end->endOfDay(); - - $frontPageIds = $this->getFrontPageAccountIds(); - $accounts = $this->repository->getAccountsById($frontPageIds); - $chartData = []; - - /** @var Account $account */ - foreach ($accounts as $account) { - Log::debug(sprintf('Rendering chart data for account %s (%d)', $account->name, $account->id)); - $currency = $this->repository->getAccountCurrency($account) ?? $this->primaryCurrency; - $currentStart = clone $start; - $range = Steam::finalAccountBalanceInRange($account, $start, clone $end, $this->convertToPrimary); - $previous = array_values($range)[0]['balance']; - $pcPrevious = null; - $currentSet = [ - 'label' => $account->name, - 'currency_id' => (string)$currency->id, - 'currency_code' => $currency->code, - 'currency_symbol' => $currency->symbol, - 'currency_decimal_places' => $currency->decimal_places, - 'start_date' => $start->toAtomString(), - 'end_date' => $end->toAtomString(), - 'type' => 'line', // line, area or bar - 'yAxisID' => 0, // 0, 1, 2 - 'entries' => [], - ]; - - // add "pc_entries" if convertToPrimary is true: - if ($this->convertToPrimary) { - $currentSet['pc_entries'] = []; - $currentSet['primary_currency_id'] = (string)$this->primaryCurrency->id; - $currentSet['primary_currency_code'] = $this->primaryCurrency->code; - $currentSet['primary_currency_symbol'] = $this->primaryCurrency->symbol; - $currentSet['primary_currency_decimal_places'] = $this->primaryCurrency->decimal_places; - $pcPrevious = array_values($range)[0]['pc_balance']; - - } - - // also get the primary balance if convertToPrimary is true: - while ($currentStart <= $end) { - $format = $currentStart->format('Y-m-d'); - $label = $currentStart->toAtomString(); - - // balance is based on "balance" from the $range variable. - $balance = array_key_exists($format, $range) ? $range[$format]['balance'] : $previous; - $previous = $balance; - $currentSet['entries'][$label] = $balance; - - // do the same for the primary balance, if relevant: - $pcBalance = null; - if ($this->convertToPrimary) { - $pcBalance = array_key_exists($format, $range) ? $range[$format]['pc_balance'] : $pcPrevious; - $pcPrevious = $pcBalance; - $currentSet['pc_entries'][$label] = $pcBalance; - } - - $currentStart->addDay(); - - } - $chartData[] = $currentSet; - } - - return response()->json($chartData); - } - private function getFrontPageAccountIds(): array { $defaultSet = $this->repository->getAccountsByType([AccountTypeEnum::ASSET->value])->pluck('id')->toArray(); /** @var Preference $frontpage */ - $frontpage = Preferences::get('frontpageAccounts', $defaultSet); + $frontpage = Preferences::get('frontpageAccounts', $defaultSet); if (!(is_array($frontpage->data) && count($frontpage->data) > 0)) { $frontpage->data = $defaultSet; diff --git a/app/Api/V1/Controllers/Chart/BalanceController.php b/app/Api/V1/Controllers/Chart/BalanceController.php index 9be3c42830..083eb0bf33 100644 --- a/app/Api/V1/Controllers/Chart/BalanceController.php +++ b/app/Api/V1/Controllers/Chart/BalanceController.php @@ -7,6 +7,7 @@ namespace FireflyIII\Api\V1\Controllers\Chart; use FireflyIII\Api\V1\Controllers\Controller; use FireflyIII\Api\V1\Requests\Chart\ChartRequest; use FireflyIII\Enums\TransactionTypeEnum; +use FireflyIII\Enums\UserRoleEnum; use FireflyIII\Exceptions\FireflyException; use FireflyIII\Helpers\Collector\GroupCollectorInterface; use FireflyIII\Models\TransactionCurrency; @@ -25,8 +26,9 @@ class BalanceController extends Controller { use CleansChartData; use CollectsAccountsFromFilter; + protected array $acceptedRoles = [UserRoleEnum::READ_ONLY]; - private ChartData $chartData; + private array $chartData; private GroupCollectorInterface $collector; private AccountRepositoryInterface $repository; @@ -42,7 +44,7 @@ class BalanceController extends Controller $userGroup = $this->validateUserGroup($request); $this->repository->setUserGroup($userGroup); $this->collector->setUserGroup($userGroup); - $this->chartData = new ChartData(); + $this->chartData = []; // $this->default = app('amount')->getPrimaryCurrency(); return $next($request); @@ -66,10 +68,6 @@ class BalanceController extends Controller $queryParameters = $request->getParameters(); $accounts = $this->getAccountList($queryParameters); - // prepare for currency conversion and data collection: - /** @var TransactionCurrency $primary */ - $primary = Amount::getPrimaryCurrency(); - // get journals for entire period: $this->collector->setRange($queryParameters['start'], $queryParameters['end']) @@ -81,7 +79,7 @@ class BalanceController extends Controller $object = new AccountBalanceGrouped(); $object->setPreferredRange($queryParameters['period']); - $object->setPrimary($primary); + $object->setPrimary($this->primaryCurrency); $object->setAccounts($accounts); $object->setJournals($journals); $object->setStart($queryParameters['start']); @@ -89,9 +87,10 @@ class BalanceController extends Controller $object->groupByCurrencyAndPeriod(); $data = $object->convertToChartData(); foreach ($data as $entry) { - $this->chartData->add($entry); + $this->chartData[] = $entry; } + $this->chartData= $this->clean($this->chartData); - return response()->json($this->chartData->render()); + return response()->json($this->chartData); } } diff --git a/app/Support/Chart/ChartData.php b/app/Support/Chart/ChartData.php index 1e04e0a654..ab03e3dd31 100644 --- a/app/Support/Chart/ChartData.php +++ b/app/Support/Chart/ChartData.php @@ -26,6 +26,9 @@ namespace FireflyIII\Support\Chart; use FireflyIII\Exceptions\FireflyException; +/** + * @deprecated + */ class ChartData { private array $series; diff --git a/app/Support/Http/Api/AccountBalanceGrouped.php b/app/Support/Http/Api/AccountBalanceGrouped.php index e3d84f3896..4363e05db8 100644 --- a/app/Support/Http/Api/AccountBalanceGrouped.php +++ b/app/Support/Http/Api/AccountBalanceGrouped.php @@ -28,6 +28,8 @@ use Carbon\Carbon; use FireflyIII\Enums\TransactionTypeEnum; use FireflyIII\Exceptions\FireflyException; use FireflyIII\Models\TransactionCurrency; +use FireflyIII\Support\Facades\Navigation; +use FireflyIII\Support\Facades\Steam; use Illuminate\Support\Collection; use Illuminate\Support\Facades\Log; @@ -64,36 +66,38 @@ class AccountBalanceGrouped /** @var array $currency */ foreach ($this->data as $currency) { // income and expense array prepped: - $income = [ + $income = [ 'label' => 'earned', - 'currency_id' => (string) $currency['currency_id'], + 'currency_id' => (string)$currency['currency_id'], 'currency_symbol' => $currency['currency_symbol'], 'currency_code' => $currency['currency_code'], 'currency_decimal_places' => $currency['currency_decimal_places'], - 'primary_currency_id' => (string) $currency['primary_currency_id'], + 'primary_currency_id' => (string)$currency['primary_currency_id'], 'primary_currency_symbol' => $currency['primary_currency_symbol'], 'primary_currency_code' => $currency['primary_currency_code'], 'primary_currency_decimal_places' => $currency['primary_currency_decimal_places'], 'date' => $this->start->toAtomString(), - 'start' => $this->start->toAtomString(), - 'end' => $this->end->toAtomString(), + 'start_date' => $this->start->toAtomString(), + 'end_date' => $this->end->toAtomString(), + 'yAxisID' => 0, 'period' => $this->preferredRange, 'entries' => [], - 'primary_entries' => [], + 'pc_entries' => [], ]; - $expense = [ + $expense = [ 'label' => 'spent', - 'currency_id' => (string) $currency['currency_id'], + 'currency_id' => (string)$currency['currency_id'], 'currency_symbol' => $currency['currency_symbol'], 'currency_code' => $currency['currency_code'], 'currency_decimal_places' => $currency['currency_decimal_places'], - 'primary_currency_id' => (string) $currency['primary_currency_id'], + 'primary_currency_id' => (string)$currency['primary_currency_id'], 'primary_currency_symbol' => $currency['primary_currency_symbol'], 'primary_currency_code' => $currency['primary_currency_code'], 'primary_currency_decimal_places' => $currency['primary_currency_decimal_places'], 'date' => $this->start->toAtomString(), - 'start' => $this->start->toAtomString(), - 'end' => $this->end->toAtomString(), + 'start_date' => $this->start->toAtomString(), + 'end_date' => $this->end->toAtomString(), + 'yAxisID' => 0, 'period' => $this->preferredRange, 'entries' => [], 'pc_entries' => [], @@ -101,22 +105,22 @@ class AccountBalanceGrouped // loop all possible periods between $start and $end, and add them to the correct dataset. $currentStart = clone $this->start; while ($currentStart <= $this->end) { - $key = $currentStart->format($this->carbonFormat); - $label = $currentStart->toAtomString(); + $key = $currentStart->format($this->carbonFormat); + $label = $currentStart->toAtomString(); // normal entries - $income['entries'][$label] = app('steam')->bcround($currency[$key]['earned'] ?? '0', $currency['currency_decimal_places']); - $expense['entries'][$label] = app('steam')->bcround($currency[$key]['spent'] ?? '0', $currency['currency_decimal_places']); + $income['entries'][$label] = Steam::bcround($currency[$key]['earned'] ?? '0', $currency['currency_decimal_places']); + $expense['entries'][$label] = Steam::bcround($currency[$key]['spent'] ?? '0', $currency['currency_decimal_places']); // converted entries - $income['pc_entries'][$label] = app('steam')->bcround($currency[$key]['pc_earned'] ?? '0', $currency['primary_currency_decimal_places']); - $expense['pc_entries'][$label] = app('steam')->bcround($currency[$key]['pc_spent'] ?? '0', $currency['primary_currency_decimal_places']); + $income['pc_entries'][$label] = Steam::bcround($currency[$key]['pc_earned'] ?? '0', $currency['primary_currency_decimal_places']); + $expense['pc_entries'][$label] = Steam::bcround($currency[$key]['pc_spent'] ?? '0', $currency['primary_currency_decimal_places']); // next loop - $currentStart = app('navigation')->addPeriod($currentStart, $this->preferredRange, 0); + $currentStart = Navigation::addPeriod($currentStart, $this->preferredRange, 0); } - $chartData[] = $income; - $chartData[] = $expense; + $chartData[] = $income; + $chartData[] = $expense; } return $chartData; @@ -142,9 +146,9 @@ class AccountBalanceGrouped private function processJournal(array $journal): void { // format the date according to the period - $period = $journal['date']->format($this->carbonFormat); - $currencyId = (int) $journal['currency_id']; - $currency = $this->findCurrency($currencyId); + $period = $journal['date']->format($this->carbonFormat); + $currencyId = (int)$journal['currency_id']; + $currency = $this->findCurrency($currencyId); // set the array with monetary info, if it does not exist. $this->createDefaultDataEntry($journal); @@ -152,25 +156,25 @@ class AccountBalanceGrouped $this->createDefaultPeriodEntry($journal); // is this journal's amount in- our outgoing? - $key = $this->getDataKey($journal); - $amount = 'spent' === $key ? app('steam')->negative($journal['amount']) : app('steam')->positive($journal['amount']); + $key = $this->getDataKey($journal); + $amount = 'spent' === $key ? Steam::negative($journal['amount']) : Steam::positive($journal['amount']); // get conversion rate - $rate = $this->getRate($currency, $journal['date']); - $amountConverted = bcmul((string) $amount, $rate); + $rate = $this->getRate($currency, $journal['date']); + $amountConverted = bcmul((string)$amount, $rate); // perhaps transaction already has the foreign amount in the primary currency. - if ((int) $journal['foreign_currency_id'] === $this->primary->id) { + if ((int)$journal['foreign_currency_id'] === $this->primary->id) { $amountConverted = $journal['foreign_amount'] ?? '0'; - $amountConverted = 'earned' === $key ? app('steam')->positive($amountConverted) : app('steam')->negative($amountConverted); + $amountConverted = 'earned' === $key ? Steam::positive($amountConverted) : Steam::negative($amountConverted); } // add normal entry - $this->data[$currencyId][$period][$key] = bcadd((string) $this->data[$currencyId][$period][$key], (string) $amount); + $this->data[$currencyId][$period][$key] = bcadd((string)$this->data[$currencyId][$period][$key], (string)$amount); // add converted entry $convertedKey = sprintf('pc_%s', $key); - $this->data[$currencyId][$period][$convertedKey] = bcadd((string) $this->data[$currencyId][$period][$convertedKey], (string) $amountConverted); + $this->data[$currencyId][$period][$convertedKey] = bcadd((string)$this->data[$currencyId][$period][$convertedKey], (string)$amountConverted); } private function findCurrency(int $currencyId): TransactionCurrency @@ -185,15 +189,15 @@ class AccountBalanceGrouped private function createDefaultDataEntry(array $journal): void { - $currencyId = (int) $journal['currency_id']; + $currencyId = (int)$journal['currency_id']; $this->data[$currencyId] ??= [ - 'currency_id' => (string) $currencyId, + 'currency_id' => (string)$currencyId, 'currency_symbol' => $journal['currency_symbol'], 'currency_code' => $journal['currency_code'], 'currency_name' => $journal['currency_name'], 'currency_decimal_places' => $journal['currency_decimal_places'], // primary currency info (could be the same) - 'primary_currency_id' => (string) $this->primary->id, + 'primary_currency_id' => (string)$this->primary->id, 'primary_currency_code' => $this->primary->code, 'primary_currency_symbol' => $this->primary->symbol, 'primary_currency_decimal_places' => $this->primary->decimal_places, @@ -202,14 +206,14 @@ class AccountBalanceGrouped private function createDefaultPeriodEntry(array $journal): void { - $currencyId = (int) $journal['currency_id']; - $period = $journal['date']->format($this->carbonFormat); + $currencyId = (int)$journal['currency_id']; + $period = $journal['date']->format($this->carbonFormat); $this->data[$currencyId][$period] ??= [ - 'period' => $period, - 'spent' => '0', - 'earned' => '0', - 'pc_spent' => '0', - 'pc_earned' => '0', + 'period' => $period, + 'spent' => '0', + 'earned' => '0', + 'pc_spent' => '0', + 'pc_earned' => '0', ]; } @@ -258,12 +262,12 @@ class AccountBalanceGrouped $primaryCurrencyId = $primary->id; $this->currencies = [$primary->id => $primary]; // currency cache $this->data[$primaryCurrencyId] = [ - 'currency_id' => (string) $primaryCurrencyId, + 'currency_id' => (string)$primaryCurrencyId, 'currency_symbol' => $primary->symbol, 'currency_code' => $primary->code, 'currency_name' => $primary->name, 'currency_decimal_places' => $primary->decimal_places, - 'primary_currency_id' => (string) $primaryCurrencyId, + 'primary_currency_id' => (string)$primaryCurrencyId, 'primary_currency_symbol' => $primary->symbol, 'primary_currency_code' => $primary->code, 'primary_currency_name' => $primary->name, @@ -284,7 +288,7 @@ class AccountBalanceGrouped public function setPreferredRange(string $preferredRange): void { $this->preferredRange = $preferredRange; - $this->carbonFormat = app('navigation')->preferredCarbonFormatByPeriod($preferredRange); + $this->carbonFormat = Navigation::preferredCarbonFormatByPeriod($preferredRange); } public function setStart(Carbon $start): void diff --git a/app/Support/Http/Api/CleansChartData.php b/app/Support/Http/Api/CleansChartData.php index 46a6a2e4ab..747edc5103 100644 --- a/app/Support/Http/Api/CleansChartData.php +++ b/app/Support/Http/Api/CleansChartData.php @@ -47,24 +47,26 @@ trait CleansChartData * @var array $array */ foreach ($data as $index => $array) { - if (array_key_exists('currency_id', $array)) { - $array['currency_id'] = (string) $array['currency_id']; - } - if (array_key_exists('primary_currency_id', $array)) { - $array['primary_currency_id'] = (string) $array['primary_currency_id']; - } - if (!array_key_exists('start', $array)) { - throw new FireflyException(sprintf('Data-set "%s" is missing the "start"-variable.', $index)); - } - if (!array_key_exists('end', $array)) { - throw new FireflyException(sprintf('Data-set "%s" is missing the "end"-variable.', $index)); - } - if (!array_key_exists('period', $array)) { - throw new FireflyException(sprintf('Data-set "%s" is missing the "period"-variable.', $index)); - } + $array = $this->cleanSingleArray($index, $array); $return[] = $array; } return $return; } + + private function cleanSingleArray(mixed $index, array $array): array { + if (array_key_exists('currency_id', $array)) { + $array['currency_id'] = (string)$array['currency_id']; + } + if (array_key_exists('primary_currency_id', $array)) { + $array['primary_currency_id'] = (string)$array['primary_currency_id']; + } + $required = ['start_date', 'end_date', 'period', 'yAxisID']; + foreach ($required as $field) { + if (!array_key_exists($field, $array)) { + throw new FireflyException(sprintf('Data-set "%s" is missing the "%s"-variable.', $index, $field)); + } + } + return $array; + } } diff --git a/app/Support/Http/Api/CollectsAccountsFromFilter.php b/app/Support/Http/Api/CollectsAccountsFromFilter.php index db304c5e7e..7c10a6c1e1 100644 --- a/app/Support/Http/Api/CollectsAccountsFromFilter.php +++ b/app/Support/Http/Api/CollectsAccountsFromFilter.php @@ -67,7 +67,7 @@ trait CollectsAccountsFromFilter if ('all' === $queryParameters['preselected']) { return $this->repository->getAccountsByType([AccountTypeEnum::ASSET->value, AccountTypeEnum::DEFAULT->value, AccountTypeEnum::LOAN->value, AccountTypeEnum::DEBT->value, AccountTypeEnum::MORTGAGE->value]); } - if ('assets' === $queryParameters['preselected']) { + if ('assets' === $queryParameters['preselected'] || 'Asset account' === $queryParameters['preselected']) { return $this->repository->getAccountsByType([AccountTypeEnum::ASSET->value, AccountTypeEnum::DEFAULT->value]); } if ('liabilities' === $queryParameters['preselected']) { diff --git a/routes/api.php b/routes/api.php index fc8ed57ab2..6d39318506 100644 --- a/routes/api.php +++ b/routes/api.php @@ -33,6 +33,10 @@ use Illuminate\Support\Facades\Route; * \__/ |_| | _| `._____| \______/ \______/ |__| |_______|_______/ */ +if (!defined('DATEFORMAT')) { + define('DATEFORMAT', '(19|20)[0-9]{2}-?[0-9]{2}-?[0-9]{2}'); +} + // Autocomplete controllers Route::group( [ @@ -69,24 +73,34 @@ Route::group( 'as' => 'api.v1.exchange-rates.', ], static function (): void { + // get all Route::get('', ['uses' => 'IndexController@index', 'as' => 'index']); + // get list of rates Route::get('rates/{fromCurrencyCode}/{toCurrencyCode}', ['uses' => 'ShowController@show', 'as' => 'show']); - Route::get('{userGroupExchangeRate}', ['uses' => 'ShowController@showSingle', 'as' => 'show.single']); + // get single rate + Route::get('{userGroupExchangeRate}', ['uses' => 'ShowController@showSingleById', 'as' => 'show.single']); + Route::get('rates/{fromCurrencyCode}/{toCurrencyCode}/{date}', ['uses' => 'ShowController@showSingleByDate', 'as' => 'show.by-date'])->where(['start_date' => DATEFORMAT]); + + // delete all rates Route::delete('rates/{fromCurrencyCode}/{toCurrencyCode}', ['uses' => 'DestroyController@destroy', 'as' => 'destroy']); - Route::delete('{userGroupExchangeRate}', ['uses' => 'DestroyController@destroySingle', 'as' => 'destroy.single']); - Route::put('{userGroupExchangeRate}', ['uses' => 'UpdateController@update', 'as' => 'update']); + // delete single rate + Route::delete('{userGroupExchangeRate}', ['uses' => 'DestroyController@destroySingleById', 'as' => 'destroy.single']); + Route::delete('rates/{fromCurrencyCode}/{toCurrencyCode}/{date}', ['uses' => 'DestroyController@destroySingleByDate', 'as' => 'destroy.by-date'])->where(['start_date' => DATEFORMAT]); + + // update single + Route::put('{userGroupExchangeRate}', ['uses' => 'UpdateController@updateById', 'as' => 'update']); + Route::put('rates/{fromCurrencyCode}/{toCurrencyCode}/{date}', ['uses' => 'UpdateController@updateByDate', 'as' => 'update.by-date'])->where(['start_date' => DATEFORMAT]); + // post new rate Route::post('', ['uses' => 'StoreController@store', 'as' => 'store']); + Route::post('by-date/{date}', ['uses' => 'StoreController@storeByDate', 'as' => 'store.by-date'])->where(['start_date' => DATEFORMAT]); + Route::post('by-currencies/{fromCurrencyCode}/{toCurrencyCode}', ['uses' => 'StoreController@storeByCurrencies', 'as' => 'store.by-currencies']); } ); -// CHART ROUTES. - -// chart balance - // CHART ROUTES Route::group( [ - 'namespace' => 'FireflyIII\Api\V2\Controllers\Chart', + 'namespace' => 'FireflyIII\Api\V1\Controllers\Chart', 'prefix' => 'v1/chart/balance', 'as' => 'api.v1.chart.balance', ], @@ -104,7 +118,6 @@ Route::group( ], static function (): void { Route::get('overview', ['uses' => 'AccountController@overview', 'as' => 'overview']); - Route::get('dashboard', ['uses' => 'AccountController@dashboard', 'as' => 'dashboard']); } ); From 844b8d48c4b33d456ded93b6b01bb78c17ce9eb9 Mon Sep 17 00:00:00 2001 From: James Cole Date: Fri, 15 Aug 2025 07:44:14 +0200 Subject: [PATCH 10/17] Update category overview to be multi-currency aware. --- .../V1/Controllers/Chart/BudgetController.php | 36 +++++----- .../Controllers/Chart/CategoryController.php | 65 ++++++++++++------- .../Http/Api/AccountBalanceGrouped.php | 2 + app/Support/Http/Api/CleansChartData.php | 5 +- routes/api.php | 4 +- 5 files changed, 71 insertions(+), 41 deletions(-) diff --git a/app/Api/V1/Controllers/Chart/BudgetController.php b/app/Api/V1/Controllers/Chart/BudgetController.php index e58d657d5b..59add4b2af 100644 --- a/app/Api/V1/Controllers/Chart/BudgetController.php +++ b/app/Api/V1/Controllers/Chart/BudgetController.php @@ -50,7 +50,7 @@ class BudgetController extends Controller use CleansChartData; use ValidatesUserGroupTrait; - protected array $acceptedRoles = [UserRoleEnum::READ_ONLY]; + protected array $acceptedRoles = [UserRoleEnum::READ_ONLY]; protected OperationsRepositoryInterface $opsRepository; private BudgetLimitRepositoryInterface $blRepository; @@ -81,15 +81,15 @@ class BudgetController extends Controller * * @throws FireflyException */ - public function dashboard(DateRequest $request): JsonResponse + public function overview(DateRequest $request): JsonResponse { - $params = $request->getAll(); + $params = $request->getAll(); /** @var Carbon $start */ - $start = $params['start']; + $start = $params['start']; /** @var Carbon $end */ - $end = $params['end']; + $end = $params['end']; // code from FrontpageChartGenerator, but not in separate class $budgets = $this->repository->getActiveBudgets(); @@ -116,12 +116,12 @@ class BudgetController extends Controller $expenses = $this->processExpenses($budget->id, $spent, $start, $end); /** - * @var int $currencyId + * @var int $currencyId * @var array $row */ foreach ($expenses as $currencyId => $row) { // budgeted, left and overspent are now 0. - $limit = $this->filterLimit($currencyId, $limits); + $limit = $this->filterLimit($currencyId, $limits); if (null !== $limit) { $row['budgeted'] = $limit->amount; $row['left'] = bcsub($row['budgeted'], bcmul($row['spent'], '-1')); @@ -140,7 +140,7 @@ class BudgetController extends Controller // } // is always an array - $return = []; + $return = []; foreach ($rows as $row) { $current = [ 'label' => $budget->name, @@ -149,14 +149,20 @@ class BudgetController extends Controller 'currency_name' => $row['currency_name'], 'currency_decimal_places' => $row['currency_decimal_places'], 'period' => null, - 'start' => $row['start'], - 'end' => $row['end'], + 'date' => $row['start'], + 'start_date' => $row['start'], + 'end_date' => $row['end'], + 'yAxisID' => 0, + 'type' => 'bar', 'entries' => [ 'budgeted' => $row['budgeted'], 'spent' => $row['spent'], 'left' => $row['left'], 'overspent' => $row['overspent'], ], + 'pc_entries' => [ + + ], ]; $return[] = $current; } @@ -191,7 +197,7 @@ class BudgetController extends Controller * This array contains the expenses in this budget. Grouped per currency. * The grouping is on the main currency only. * - * @var int $currencyId + * @var int $currencyId * @var array $block */ foreach ($spent as $currencyId => $block) { @@ -209,7 +215,7 @@ class BudgetController extends Controller 'left' => '0', 'overspent' => '0', ]; - $currentBudgetArray = $block['budgets'][$budgetId]; + $currentBudgetArray = $block['budgets'][$budgetId]; // var_dump($return); /** @var array $journal */ @@ -250,7 +256,7 @@ class BudgetController extends Controller private function processLimit(Budget $budget, BudgetLimit $limit): array { Log::debug(sprintf('Created new ExchangeRateConverter in %s', __METHOD__)); - $end = clone $limit->end_date; + $end = clone $limit->end_date; $end->endOfDay(); $spent = $this->opsRepository->listExpenses($limit->start_date, $end, null, new Collection([$budget])); $limitCurrencyId = $limit->transaction_currency_id; @@ -258,8 +264,8 @@ class BudgetController extends Controller /** @var array $entry */ // only spent the entry where the entry's currency matches the budget limit's currency // so $filtered will only have 1 or 0 entries - $filtered = array_filter($spent, fn ($entry) => $entry['currency_id'] === $limitCurrencyId); - $result = $this->processExpenses($budget->id, $filtered, $limit->start_date, $end); + $filtered = array_filter($spent, fn($entry) => $entry['currency_id'] === $limitCurrencyId); + $result = $this->processExpenses($budget->id, $filtered, $limit->start_date, $end); if (1 === count($result)) { $compare = bccomp($limit->amount, (string)app('steam')->positive($result[$limitCurrencyId]['spent'])); $result[$limitCurrencyId]['budgeted'] = $limit->amount; diff --git a/app/Api/V1/Controllers/Chart/CategoryController.php b/app/Api/V1/Controllers/Chart/CategoryController.php index 94968d09cc..986f2f4042 100644 --- a/app/Api/V1/Controllers/Chart/CategoryController.php +++ b/app/Api/V1/Controllers/Chart/CategoryController.php @@ -34,6 +34,7 @@ use FireflyIII\Exceptions\FireflyException; use FireflyIII\Helpers\Collector\GroupCollectorInterface; use FireflyIII\Repositories\Account\AccountRepositoryInterface; use FireflyIII\Repositories\Currency\CurrencyRepositoryInterface; +use FireflyIII\Support\Facades\Steam; use FireflyIII\Support\Http\Api\CleansChartData; use FireflyIII\Support\Http\Api\ExchangeRateConverter; use FireflyIII\Support\Http\Api\ValidatesUserGroupTrait; @@ -77,10 +78,10 @@ class CategoryController extends Controller * * @SuppressWarnings("PHPMD.UnusedFormalParameter") */ - public function dashboard(DateRequest $request): JsonResponse + public function overview(DateRequest $request): JsonResponse { /** @var Carbon $start */ - $start = $this->parameters->get('start'); + $start = $this->parameters->get('start'); /** @var Carbon $end */ $end = $this->parameters->get('end'); @@ -91,11 +92,11 @@ class CategoryController extends Controller // get journals for entire period: /** @var GroupCollectorInterface $collector */ - $collector = app(GroupCollectorInterface::class); + $collector = app(GroupCollectorInterface::class); $collector->setRange($start, $end)->withAccountInformation(); $collector->setXorAccounts($accounts)->withCategoryInformation(); $collector->setTypes([TransactionTypeEnum::WITHDRAWAL->value, TransactionTypeEnum::RECONCILIATION->value]); - $journals = $collector->getExtractedJournals(); + $journals = $collector->getExtractedJournals(); /** @var array $journal */ foreach ($journals as $journal) { @@ -108,44 +109,62 @@ class CategoryController extends Controller $currencyCode = (string)$currency->code; $currencySymbol = (string)$currency->symbol; $currencyDecimalPlaces = (int)$currency->decimal_places; - $amount = app('steam')->positive($journal['amount']); + $amount = Steam::positive($journal['amount']); + $pcAmount = null; // overrule if necessary: + if ($this->convertToPrimary && $journalCurrencyId === $this->primaryCurrency->id) { + $pcAmount = $amount; + } if ($this->convertToPrimary && $journalCurrencyId !== $this->primaryCurrency->id) { $currencyId = (int)$this->primaryCurrency->id; $currencyName = (string)$this->primaryCurrency->name; $currencyCode = (string)$this->primaryCurrency->code; $currencySymbol = (string)$this->primaryCurrency->symbol; $currencyDecimalPlaces = (int)$this->primaryCurrency->decimal_places; - $convertedAmount = $converter->convert($currency, $this->primaryCurrency, $journal['date'], $amount); - Log::debug(sprintf('Converted %s %s to %s %s', $journal['currency_code'], $amount, $this->primaryCurrency->code, $convertedAmount)); - $amount = $convertedAmount; + $pcAmount = $converter->convert($currency, $this->primaryCurrency, $journal['date'], $amount); + Log::debug(sprintf('Converted %s %s to %s %s', $journal['currency_code'], $amount, $this->primaryCurrency->code, $pcAmount)); } - $categoryName = $journal['category_name'] ?? (string)trans('firefly.no_category'); - $key = sprintf('%s-%s', $categoryName, $currencyCode); + $categoryName = $journal['category_name'] ?? (string)trans('firefly.no_category'); + $key = sprintf('%s-%s', $categoryName, $currencyCode); // create arrays $return[$key] ??= [ - 'label' => $categoryName, - 'currency_id' => (string)$currencyId, - 'currency_code' => $currencyCode, - 'currency_name' => $currencyName, - 'currency_symbol' => $currencySymbol, - 'currency_decimal_places' => $currencyDecimalPlaces, - 'period' => null, - 'start' => $start->toAtomString(), - 'end' => $end->toAtomString(), - 'amount' => '0', + 'label' => $categoryName, + 'currency_id' => (string)$currencyId, + 'currency_name' => $currencyName, + 'currency_code' => $currencyCode, + 'currency_symbol' => $currencySymbol, + 'currency_decimal_places' => $currencyDecimalPlaces, + 'primary_currency_id' => (string)$this->primaryCurrency->id, + 'primary_currency_name' => (string)$this->primaryCurrency->name, + 'primary_currency_code' => (string)$this->primaryCurrency->code, + 'primary_currency_symbol' => (string)$this->primaryCurrency->symbol, + 'primary_currency_decimal_places' => (int)$this->primaryCurrency->decimal_places, + 'period' => null, + 'start_date' => $start->toAtomString(), + 'end_date' => $end->toAtomString(), + 'yAxisID' => 0, + 'type' => 'bar', + 'entries' => [ + 'spent' => '0' + ], + 'pc_entries' => [ + 'spent' => '0' + ], ]; // add monies - $return[$key]['amount'] = bcadd($return[$key]['amount'], (string)$amount); + $return[$key]['entries']['spent'] = bcadd($return[$key]['entries']['spent'], (string)$amount); + if (null !== $pcAmount) { + $return[$key]['pc_entries']['spent'] = bcadd($return[$key]['pc_entries']['spent'], (string)$pcAmount); + } } - $return = array_values($return); + $return = array_values($return); // order by amount - usort($return, static fn (array $a, array $b) => (float)$a['amount'] < (float)$b['amount'] ? 1 : -1); + usort($return, static fn(array $a, array $b) => (float)$a['entries']['spent'] < (float)$b['entries']['spent'] ? 1 : -1); return response()->json($this->clean($return)); } diff --git a/app/Support/Http/Api/AccountBalanceGrouped.php b/app/Support/Http/Api/AccountBalanceGrouped.php index 4363e05db8..6d258df38c 100644 --- a/app/Support/Http/Api/AccountBalanceGrouped.php +++ b/app/Support/Http/Api/AccountBalanceGrouped.php @@ -80,6 +80,7 @@ class AccountBalanceGrouped 'start_date' => $this->start->toAtomString(), 'end_date' => $this->end->toAtomString(), 'yAxisID' => 0, + 'type' => 'line', 'period' => $this->preferredRange, 'entries' => [], 'pc_entries' => [], @@ -97,6 +98,7 @@ class AccountBalanceGrouped 'date' => $this->start->toAtomString(), 'start_date' => $this->start->toAtomString(), 'end_date' => $this->end->toAtomString(), + 'type' => 'line', 'yAxisID' => 0, 'period' => $this->preferredRange, 'entries' => [], diff --git a/app/Support/Http/Api/CleansChartData.php b/app/Support/Http/Api/CleansChartData.php index 747edc5103..702926d35c 100644 --- a/app/Support/Http/Api/CleansChartData.php +++ b/app/Support/Http/Api/CleansChartData.php @@ -61,7 +61,10 @@ trait CleansChartData if (array_key_exists('primary_currency_id', $array)) { $array['primary_currency_id'] = (string)$array['primary_currency_id']; } - $required = ['start_date', 'end_date', 'period', 'yAxisID']; + $required = [ + 'start_date', 'end_date', 'period', 'yAxisID','type','entries','pc_entries', + 'currency_id', 'primary_currency_id' + ]; foreach ($required as $field) { if (!array_key_exists($field, $array)) { throw new FireflyException(sprintf('Data-set "%s" is missing the "%s"-variable.', $index, $field)); diff --git a/routes/api.php b/routes/api.php index 6d39318506..2d346dce5b 100644 --- a/routes/api.php +++ b/routes/api.php @@ -128,7 +128,7 @@ Route::group( 'as' => 'api.v1.chart.budget.', ], static function (): void { - Route::get('dashboard', ['uses' => 'BudgetController@dashboard', 'as' => 'dashboard']); + Route::get('overview', ['uses' => 'BudgetController@overview', 'as' => 'overview']); } ); @@ -139,7 +139,7 @@ Route::group( 'as' => 'api.v1.chart.category.', ], static function (): void { - Route::get('dashboard', ['uses' => 'CategoryController@dashboard', 'as' => 'dashboard']); + Route::get('overview', ['uses' => 'CategoryController@overview', 'as' => 'overview']); } ); From 1305fafd380a746c65b61d539a8de5d39eaad0c3 Mon Sep 17 00:00:00 2001 From: James Cole Date: Fri, 15 Aug 2025 07:50:13 +0200 Subject: [PATCH 11/17] Add multi-currency for budget chart --- .../V1/Controllers/Chart/BudgetController.php | 60 +++++++++++++++---- 1 file changed, 47 insertions(+), 13 deletions(-) diff --git a/app/Api/V1/Controllers/Chart/BudgetController.php b/app/Api/V1/Controllers/Chart/BudgetController.php index 59add4b2af..ae0fca6dab 100644 --- a/app/Api/V1/Controllers/Chart/BudgetController.php +++ b/app/Api/V1/Controllers/Chart/BudgetController.php @@ -110,10 +110,12 @@ class BudgetController extends Controller private function processBudget(Budget $budget, Carbon $start, Carbon $end): array { // get all limits: - $limits = $this->blRepository->getBudgetLimits($budget, $start, $end); - $rows = []; - $spent = $this->opsRepository->listExpenses($start, $end, null, new Collection([$budget])); - $expenses = $this->processExpenses($budget->id, $spent, $start, $end); + $limits = $this->blRepository->getBudgetLimits($budget, $start, $end); + $rows = []; + $spent = $this->opsRepository->listExpenses($start, $end, null, new Collection([$budget])); + $expenses = $this->processExpenses($budget->id, $spent, $start, $end); + $converter = new ExchangeRateConverter(); + $currencies = [$this->primaryCurrency->id => $this->primaryCurrency,]; /** * @var int $currencyId @@ -122,6 +124,13 @@ class BudgetController extends Controller foreach ($expenses as $currencyId => $row) { // budgeted, left and overspent are now 0. $limit = $this->filterLimit($currencyId, $limits); + + // primary currency entries + $row['pc_budgeted'] = '0'; + $row['pc_spent'] = '0'; + $row['pc_left'] = '0'; + $row['pc_overspent'] = '0'; + if (null !== $limit) { $row['budgeted'] = $limit->amount; $row['left'] = bcsub($row['budgeted'], bcmul($row['spent'], '-1')); @@ -129,6 +138,21 @@ class BudgetController extends Controller $row['left'] = 1 === bccomp($row['left'], '0') ? $row['left'] : '0'; $row['overspent'] = 1 === bccomp($row['overspent'], '0') ? $row['overspent'] : '0'; } + + // convert data if necessary. + if (true === $this->convertToPrimary && $currencyId !== $this->primaryCurrency->id) { + $currencies[$currencyId] ??= TransactionCurrency::find($currencyId); + $row['pc_budgeted'] = $converter->convert($currencies[$currencyId], $this->primaryCurrency, $start, $row['budgeted']); + $row['pc_spent'] = $converter->convert($currencies[$currencyId], $this->primaryCurrency, $start, $row['spent']); + $row['pc_left'] = $converter->convert($currencies[$currencyId], $this->primaryCurrency, $start, $row['left']); + $row['pc_overspent'] = $converter->convert($currencies[$currencyId], $this->primaryCurrency, $start, $row['overspent']); + } + if (true === $this->convertToPrimary && $currencyId === $this->primaryCurrency->id) { + $row['pc_budgeted'] = $row['budgeted']; + $row['pc_spent'] = $row['spent']; + $row['pc_left'] = $row['left']; + $row['pc_overspent'] = $row['overspent']; + } $rows[] = $row; } @@ -145,23 +169,33 @@ class BudgetController extends Controller $current = [ 'label' => $budget->name, 'currency_id' => (string)$row['currency_id'], - 'currency_code' => $row['currency_code'], 'currency_name' => $row['currency_name'], + 'currency_code' => $row['currency_code'], 'currency_decimal_places' => $row['currency_decimal_places'], - 'period' => null, - 'date' => $row['start'], - 'start_date' => $row['start'], - 'end_date' => $row['end'], - 'yAxisID' => 0, - 'type' => 'bar', - 'entries' => [ + + 'primary_currency_id' => (string)$this->primaryCurrency->id, + 'primary_currency_name' => $this->primaryCurrency->name, + 'primary_currency_code' => $this->primaryCurrency->code, + 'primary_currency_symbol' => $this->primaryCurrency->symbol, + 'primary_currency_decimal_places' => $this->primaryCurrency->decimal_places, + + 'period' => null, + 'date' => $row['start'], + 'start_date' => $row['start'], + 'end_date' => $row['end'], + 'yAxisID' => 0, + 'type' => 'bar', + 'entries' => [ 'budgeted' => $row['budgeted'], 'spent' => $row['spent'], 'left' => $row['left'], 'overspent' => $row['overspent'], ], 'pc_entries' => [ - + 'budgeted' => $row['pc_budgeted'], + 'spent' => '0', + 'left' => '0', + 'overspent' => '0', ], ]; $return[] = $current; From 9b2263c7bb5555190ef743d5a3599669658aaa11 Mon Sep 17 00:00:00 2001 From: James Cole Date: Fri, 15 Aug 2025 11:28:23 +0200 Subject: [PATCH 12/17] Match exchange rate API with API docs. --- .../DestroyController.php | 21 ++--- .../CurrencyExchangeRate/ShowController.php | 20 ++++- .../CurrencyExchangeRate/StoreController.php | 39 +++++++++ .../CurrencyExchangeRate/UpdateController.php | 30 +++++-- .../CurrencyExchangeRate/DestroyRequest.php | 2 +- .../StoreByDateRequest.php | 87 +++++++++++++++++++ .../CurrencyExchangeRate/UpdateRequest.php | 2 + .../ExchangeRate/ExchangeRateRepository.php | 42 +++++---- .../ExchangeRateRepositoryInterface.php | 1 + app/Transformers/ExchangeRateTransformer.php | 2 + routes/api.php | 12 +-- 11 files changed, 216 insertions(+), 42 deletions(-) create mode 100644 app/Api/V1/Requests/Models/CurrencyExchangeRate/StoreByDateRequest.php diff --git a/app/Api/V1/Controllers/Models/CurrencyExchangeRate/DestroyController.php b/app/Api/V1/Controllers/Models/CurrencyExchangeRate/DestroyController.php index 30a3b79677..9ba01de025 100644 --- a/app/Api/V1/Controllers/Models/CurrencyExchangeRate/DestroyController.php +++ b/app/Api/V1/Controllers/Models/CurrencyExchangeRate/DestroyController.php @@ -59,23 +59,24 @@ class DestroyController extends Controller public function destroy(DestroyRequest $request, TransactionCurrency $from, TransactionCurrency $to): JsonResponse { - $date = $request->getDate(); - if (!$date instanceof Carbon) { - throw new ValidationException('Date is required'); - } - $rate = $this->repository->getSpecificRateOnDate($from, $to, $date); - if (!$rate instanceof CurrencyExchangeRate) { - throw new NotFoundHttpException(); - } - $this->repository->deleteRate($rate); + $this->repository->deleteRates($from, $to); return response()->json([], 204); } - public function destroySingle(CurrencyExchangeRate $exchangeRate): JsonResponse + public function destroySingleById(CurrencyExchangeRate $exchangeRate): JsonResponse { $this->repository->deleteRate($exchangeRate); + return response()->json([], 204); + } + public function destroySingleByDate(TransactionCurrency $from, TransactionCurrency $to, Carbon $date): JsonResponse + { + $exchangeRate = $this->repository->getSpecificRateOnDate($from, $to, $date); + if(null !== $exchangeRate) { + $this->repository->deleteRate($exchangeRate); + } + return response()->json([], 204); } } diff --git a/app/Api/V1/Controllers/Models/CurrencyExchangeRate/ShowController.php b/app/Api/V1/Controllers/Models/CurrencyExchangeRate/ShowController.php index bea0bb1078..abda7e7761 100644 --- a/app/Api/V1/Controllers/Models/CurrencyExchangeRate/ShowController.php +++ b/app/Api/V1/Controllers/Models/CurrencyExchangeRate/ShowController.php @@ -24,6 +24,7 @@ declare(strict_types=1); namespace FireflyIII\Api\V1\Controllers\Models\CurrencyExchangeRate; +use Carbon\Carbon; use FireflyIII\Api\V1\Controllers\Controller; use FireflyIII\Enums\UserRoleEnum; use FireflyIII\Models\CurrencyExchangeRate; @@ -33,6 +34,7 @@ use FireflyIII\Support\Http\Api\ValidatesUserGroupTrait; use FireflyIII\Transformers\ExchangeRateTransformer; use Illuminate\Http\JsonResponse; use Illuminate\Pagination\LengthAwarePaginator; +use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; /** * Class ShowController @@ -76,7 +78,7 @@ class ShowController extends Controller ; } - public function showSingle(CurrencyExchangeRate $exchangeRate): JsonResponse + public function showSingleById(CurrencyExchangeRate $exchangeRate): JsonResponse { $transformer = new ExchangeRateTransformer(); $transformer->setParameters($this->parameters); @@ -86,4 +88,20 @@ class ShowController extends Controller ->header('Content-Type', self::CONTENT_TYPE) ; } + + public function showSingleByDate(TransactionCurrency $from, TransactionCurrency $to, Carbon $date): JsonResponse + { + $transformer = new ExchangeRateTransformer(); + $transformer->setParameters($this->parameters); + + $exchangeRate = $this->repository->getSpecificRateOnDate($from, $to, $date); + if(null === $exchangeRate) { + throw new NotFoundHttpException(); + } + + return response() + ->api($this->jsonApiObject(self::RESOURCE_KEY, $exchangeRate, $transformer)) + ->header('Content-Type', self::CONTENT_TYPE) + ; + } } diff --git a/app/Api/V1/Controllers/Models/CurrencyExchangeRate/StoreController.php b/app/Api/V1/Controllers/Models/CurrencyExchangeRate/StoreController.php index 8353d637a2..3e2d21dae2 100644 --- a/app/Api/V1/Controllers/Models/CurrencyExchangeRate/StoreController.php +++ b/app/Api/V1/Controllers/Models/CurrencyExchangeRate/StoreController.php @@ -24,14 +24,19 @@ declare(strict_types=1); namespace FireflyIII\Api\V1\Controllers\Models\CurrencyExchangeRate; +use Carbon\Carbon; +use FireflyIII\Api\V1\Requests\Models\CurrencyExchangeRate\StoreByDateRequest; use FireflyIII\Enums\UserRoleEnum; use FireflyIII\Models\CurrencyExchangeRate; use FireflyIII\Api\V1\Requests\Models\CurrencyExchangeRate\StoreRequest; use FireflyIII\Api\V1\Controllers\Controller; +use FireflyIII\Models\TransactionCurrency; use FireflyIII\Repositories\ExchangeRate\ExchangeRateRepositoryInterface; use FireflyIII\Support\Http\Api\ValidatesUserGroupTrait; use FireflyIII\Transformers\ExchangeRateTransformer; use Illuminate\Http\JsonResponse; +use Illuminate\Pagination\LengthAwarePaginator; +use Illuminate\Support\Collection; class StoreController extends Controller { @@ -54,6 +59,40 @@ class StoreController extends Controller ); } + public function storeByDate(StoreByDateRequest $request, Carbon $date): JsonResponse { + + $data = $request->getAll(); + $from = $request->getFromCurrency(); + $collection = new Collection(); + foreach($data['rates'] as $key => $rate) { + $to = TransactionCurrency::where('code', $key)->first(); + if(null === $to) { + continue; // should not happen. + } + $existing = $this->repository->getSpecificRateOnDate($from, $to, $date); + if(null !== $existing) { + // update existing rate. + $existing = $this->repository->updateExchangeRate($existing, $rate); + $collection->push($existing); + continue; + } + if(null === $existing) { + $new = $this->repository->storeExchangeRate($from, $to, $rate, $date); + $collection->push($new); + } + } + + $count = $collection->count(); + $paginator = new LengthAwarePaginator($collection, $count, $count, 1); + $transformer = new ExchangeRateTransformer(); + $transformer->setParameters($this->parameters); // give params to transformer + + return response() + ->json($this->jsonApiList(self::RESOURCE_KEY, $paginator, $transformer)) + ->header('Content-Type', self::CONTENT_TYPE) + ; + } + public function store(StoreRequest $request): JsonResponse { $date = $request->getDate(); diff --git a/app/Api/V1/Controllers/Models/CurrencyExchangeRate/UpdateController.php b/app/Api/V1/Controllers/Models/CurrencyExchangeRate/UpdateController.php index 788faf874d..99b4d04bbc 100644 --- a/app/Api/V1/Controllers/Models/CurrencyExchangeRate/UpdateController.php +++ b/app/Api/V1/Controllers/Models/CurrencyExchangeRate/UpdateController.php @@ -24,21 +24,24 @@ declare(strict_types=1); namespace FireflyIII\Api\V1\Controllers\Models\CurrencyExchangeRate; -use FireflyIII\Api\V1\Requests\Models\CurrencyExchangeRate\UpdateRequest; +use Carbon\Carbon; use FireflyIII\Api\V1\Controllers\Controller; +use FireflyIII\Api\V1\Requests\Models\CurrencyExchangeRate\UpdateRequest; use FireflyIII\Enums\UserRoleEnum; use FireflyIII\Models\CurrencyExchangeRate; +use FireflyIII\Models\TransactionCurrency; use FireflyIII\Repositories\ExchangeRate\ExchangeRateRepositoryInterface; use FireflyIII\Support\Http\Api\ValidatesUserGroupTrait; use FireflyIII\Transformers\ExchangeRateTransformer; use Illuminate\Http\JsonResponse; +use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; class UpdateController extends Controller { use ValidatesUserGroupTrait; public const string RESOURCE_KEY = 'exchange-rates'; - protected array $acceptedRoles = [UserRoleEnum::OWNER]; + protected array $acceptedRoles = [UserRoleEnum::OWNER]; private ExchangeRateRepositoryInterface $repository; public function __construct() @@ -54,7 +57,7 @@ class UpdateController extends Controller ); } - public function update(UpdateRequest $request, CurrencyExchangeRate $exchangeRate): JsonResponse + public function updateById(UpdateRequest $request, CurrencyExchangeRate $exchangeRate): JsonResponse { $date = $request->getDate(); $rate = $request->getRate(); @@ -64,7 +67,24 @@ class UpdateController extends Controller return response() ->api($this->jsonApiObject(self::RESOURCE_KEY, $exchangeRate, $transformer)) - ->header('Content-Type', self::CONTENT_TYPE) - ; + ->header('Content-Type', self::CONTENT_TYPE); + } + + public function updateByDate(UpdateRequest $request, TransactionCurrency $from, TransactionCurrency $to, Carbon $date): JsonResponse + { + $exchangeRate = $this->repository->getSpecificRateOnDate($from, $to, $date); + if (null === $exchangeRate) { + throw new NotFoundHttpException(); + } + $date = $request->getDate(); + $rate = $request->getRate(); + $exchangeRate = $this->repository->updateExchangeRate($exchangeRate, $rate, $date); + + $transformer = new ExchangeRateTransformer(); + $transformer->setParameters($this->parameters); + + return response() + ->api($this->jsonApiObject(self::RESOURCE_KEY, $exchangeRate, $transformer)) + ->header('Content-Type', self::CONTENT_TYPE); } } diff --git a/app/Api/V1/Requests/Models/CurrencyExchangeRate/DestroyRequest.php b/app/Api/V1/Requests/Models/CurrencyExchangeRate/DestroyRequest.php index 1762d7348c..82ff8ca3de 100644 --- a/app/Api/V1/Requests/Models/CurrencyExchangeRate/DestroyRequest.php +++ b/app/Api/V1/Requests/Models/CurrencyExchangeRate/DestroyRequest.php @@ -45,7 +45,7 @@ class DestroyRequest extends FormRequest public function rules(): array { return [ - 'date' => 'required|date|after:1970-01-02|before:2038-01-17', + // 'date' => 'required|date|after:1970-01-02|before:2038-01-17', ]; } } diff --git a/app/Api/V1/Requests/Models/CurrencyExchangeRate/StoreByDateRequest.php b/app/Api/V1/Requests/Models/CurrencyExchangeRate/StoreByDateRequest.php new file mode 100644 index 0000000000..17c68b1183 --- /dev/null +++ b/app/Api/V1/Requests/Models/CurrencyExchangeRate/StoreByDateRequest.php @@ -0,0 +1,87 @@ + $this->get('from'), + 'rates' => $this->get('rates', []), + ]; + } + + public function getFromCurrency(): TransactionCurrency + { + return TransactionCurrency::where('code', $this->get('from'))->first(); + } + + /** + * The rules that the incoming request must be matched against. + */ + public function rules(): array + { + return [ + 'from' => 'required|exists:transaction_currencies,code', + 'rates' => 'required|array', + 'rates.*' => 'required|numeric|min:0.0000000001', + ]; + } + + public function withValidator(Validator $validator): void + { + $from = $this->getFromCurrency(); + + $validator->after( + static function (Validator $validator) use ($from): void { + $data = $validator->getData(); + $rates = $data['rates'] ?? []; + if (0 === count($rates)) { + $validator->errors()->add('rates', 'No rates given.'); + return; + } + foreach ($rates as $key => $entry) { + if ($key === $from->code) { + $validator->errors()->add(sprintf('rates.%s', $key), sprintf('Cannot convert from "%s" to itself.', $key)); + continue; + } + $to = TransactionCurrency::where('code', $key)->first(); + if (null === $to) { + $validator->errors()->add(sprintf('rates.%s', $key), sprintf('Invalid currency code "%s".', $key)); + } + } + }); + } +} diff --git a/app/Api/V1/Requests/Models/CurrencyExchangeRate/UpdateRequest.php b/app/Api/V1/Requests/Models/CurrencyExchangeRate/UpdateRequest.php index ab37c34114..8114b7abed 100644 --- a/app/Api/V1/Requests/Models/CurrencyExchangeRate/UpdateRequest.php +++ b/app/Api/V1/Requests/Models/CurrencyExchangeRate/UpdateRequest.php @@ -52,6 +52,8 @@ class UpdateRequest extends FormRequest return [ 'date' => 'date|after:1970-01-02|before:2038-01-17', 'rate' => 'required|numeric|gt:0', + 'from' => 'nullable|exists:transaction_currencies,code', + 'to' => 'nullable|exists:transaction_currencies,code', ]; } } diff --git a/app/Repositories/ExchangeRate/ExchangeRateRepository.php b/app/Repositories/ExchangeRate/ExchangeRateRepository.php index f7936e0979..bcaf547cf8 100644 --- a/app/Repositories/ExchangeRate/ExchangeRateRepository.php +++ b/app/Repositories/ExchangeRate/ExchangeRateRepository.php @@ -55,20 +55,17 @@ class ExchangeRateRepository implements ExchangeRateRepositoryInterface, UserGro // orderBy('date', 'DESC')->toRawSql(); return $this->userGroup->currencyExchangeRates() - ->where(function (Builder $q1) use ($from, $to): void { - $q1->where(function (Builder $q) use ($from, $to): void { - $q->where('from_currency_id', $from->id) - ->where('to_currency_id', $to->id) - ; - })->orWhere(function (Builder $q) use ($from, $to): void { - $q->where('from_currency_id', $to->id) - ->where('to_currency_id', $from->id) - ; - }); - }) - ->orderBy('date', 'DESC') - ->get(['currency_exchange_rates.*']) - ; + ->where(function (Builder $q1) use ($from, $to): void { + $q1->where(function (Builder $q) use ($from, $to): void { + $q->where('from_currency_id', $from->id) + ->where('to_currency_id', $to->id); + })->orWhere(function (Builder $q) use ($from, $to): void { + $q->where('from_currency_id', $to->id) + ->where('to_currency_id', $from->id); + }); + }) + ->orderBy('date', 'DESC') + ->get(['currency_exchange_rates.*']); } @@ -78,11 +75,10 @@ class ExchangeRateRepository implements ExchangeRateRepositoryInterface, UserGro /** @var null|CurrencyExchangeRate */ return $this->userGroup->currencyExchangeRates() - ->where('from_currency_id', $from->id) - ->where('to_currency_id', $to->id) - ->where('date', $date->format('Y-m-d')) - ->first() - ; + ->where('from_currency_id', $from->id) + ->where('to_currency_id', $to->id) + ->where('date', $date->format('Y-m-d')) + ->first(); } #[Override] @@ -112,4 +108,12 @@ class ExchangeRateRepository implements ExchangeRateRepositoryInterface, UserGro return $object; } + + public function deleteRates(TransactionCurrency $from, TransactionCurrency $to): void + { + $this->userGroup->currencyExchangeRates() + ->where('from_currency_id', $from->id) + ->where('to_currency_id', $to->id) + ->delete(); + } } diff --git a/app/Repositories/ExchangeRate/ExchangeRateRepositoryInterface.php b/app/Repositories/ExchangeRate/ExchangeRateRepositoryInterface.php index 4d2d024e01..956c6a0787 100644 --- a/app/Repositories/ExchangeRate/ExchangeRateRepositoryInterface.php +++ b/app/Repositories/ExchangeRate/ExchangeRateRepositoryInterface.php @@ -46,6 +46,7 @@ use Illuminate\Support\Collection; interface ExchangeRateRepositoryInterface { public function deleteRate(CurrencyExchangeRate $rate): void; + public function deleteRates(TransactionCurrency $from, TransactionCurrency $to): void; public function getAll(): Collection; diff --git a/app/Transformers/ExchangeRateTransformer.php b/app/Transformers/ExchangeRateTransformer.php index 6303fb0184..18566cc69a 100644 --- a/app/Transformers/ExchangeRateTransformer.php +++ b/app/Transformers/ExchangeRateTransformer.php @@ -48,11 +48,13 @@ class ExchangeRateTransformer extends AbstractTransformer 'updated_at' => $rate->updated_at->toAtomString(), 'from_currency_id' => (string) $rate->fromCurrency->id, + 'from_currency_name' => $rate->fromCurrency->name, 'from_currency_code' => $rate->fromCurrency->code, 'from_currency_symbol' => $rate->fromCurrency->symbol, 'from_currency_decimal_places' => $rate->fromCurrency->decimal_places, 'to_currency_id' => (string) $rate->toCurrency->id, + 'to_currency_name' => $rate->toCurrency->name, 'to_currency_code' => $rate->toCurrency->code, 'to_currency_symbol' => $rate->toCurrency->symbol, 'to_currency_decimal_places' => $rate->toCurrency->decimal_places, diff --git a/routes/api.php b/routes/api.php index 2d346dce5b..09a72d07f4 100644 --- a/routes/api.php +++ b/routes/api.php @@ -76,20 +76,20 @@ Route::group( // get all Route::get('', ['uses' => 'IndexController@index', 'as' => 'index']); // get list of rates - Route::get('rates/{fromCurrencyCode}/{toCurrencyCode}', ['uses' => 'ShowController@show', 'as' => 'show']); - // get single rate Route::get('{userGroupExchangeRate}', ['uses' => 'ShowController@showSingleById', 'as' => 'show.single']); - Route::get('rates/{fromCurrencyCode}/{toCurrencyCode}/{date}', ['uses' => 'ShowController@showSingleByDate', 'as' => 'show.by-date'])->where(['start_date' => DATEFORMAT]); + Route::get('{fromCurrencyCode}/{toCurrencyCode}', ['uses' => 'ShowController@show', 'as' => 'show']); + Route::get('{fromCurrencyCode}/{toCurrencyCode}/{date}', ['uses' => 'ShowController@showSingleByDate', 'as' => 'show.by-date'])->where(['start_date' => DATEFORMAT]); // delete all rates - Route::delete('rates/{fromCurrencyCode}/{toCurrencyCode}', ['uses' => 'DestroyController@destroy', 'as' => 'destroy']); + Route::delete('{fromCurrencyCode}/{toCurrencyCode}', ['uses' => 'DestroyController@destroy', 'as' => 'destroy']); // delete single rate Route::delete('{userGroupExchangeRate}', ['uses' => 'DestroyController@destroySingleById', 'as' => 'destroy.single']); - Route::delete('rates/{fromCurrencyCode}/{toCurrencyCode}/{date}', ['uses' => 'DestroyController@destroySingleByDate', 'as' => 'destroy.by-date'])->where(['start_date' => DATEFORMAT]); + Route::delete('{fromCurrencyCode}/{toCurrencyCode}/{date}', ['uses' => 'DestroyController@destroySingleByDate', 'as' => 'destroy.by-date'])->where(['start_date' => DATEFORMAT]); // update single Route::put('{userGroupExchangeRate}', ['uses' => 'UpdateController@updateById', 'as' => 'update']); - Route::put('rates/{fromCurrencyCode}/{toCurrencyCode}/{date}', ['uses' => 'UpdateController@updateByDate', 'as' => 'update.by-date'])->where(['start_date' => DATEFORMAT]); + Route::put('{fromCurrencyCode}/{toCurrencyCode}/{date}', ['uses' => 'UpdateController@updateByDate', 'as' => 'update.by-date'])->where(['start_date' => DATEFORMAT]); + // post new rate Route::post('', ['uses' => 'StoreController@store', 'as' => 'store']); Route::post('by-date/{date}', ['uses' => 'StoreController@storeByDate', 'as' => 'store.by-date'])->where(['start_date' => DATEFORMAT]); From 0b3fd335ad3f6b0952f5b769a55d215aa90f854c Mon Sep 17 00:00:00 2001 From: James Cole Date: Fri, 15 Aug 2025 11:35:16 +0200 Subject: [PATCH 13/17] Fix other endpoint. --- .../CurrencyExchangeRate/StoreController.php | 79 +++++++++++++------ .../StoreByCurrenciesRequest.php | 73 +++++++++++++++++ 2 files changed, 126 insertions(+), 26 deletions(-) create mode 100644 app/Api/V1/Requests/Models/CurrencyExchangeRate/StoreByCurrenciesRequest.php diff --git a/app/Api/V1/Controllers/Models/CurrencyExchangeRate/StoreController.php b/app/Api/V1/Controllers/Models/CurrencyExchangeRate/StoreController.php index 3e2d21dae2..676b621485 100644 --- a/app/Api/V1/Controllers/Models/CurrencyExchangeRate/StoreController.php +++ b/app/Api/V1/Controllers/Models/CurrencyExchangeRate/StoreController.php @@ -25,11 +25,12 @@ declare(strict_types=1); namespace FireflyIII\Api\V1\Controllers\Models\CurrencyExchangeRate; use Carbon\Carbon; +use FireflyIII\Api\V1\Controllers\Controller; +use FireflyIII\Api\V1\Requests\Models\CurrencyExchangeRate\StoreByCurrenciesRequest; use FireflyIII\Api\V1\Requests\Models\CurrencyExchangeRate\StoreByDateRequest; +use FireflyIII\Api\V1\Requests\Models\CurrencyExchangeRate\StoreRequest; use FireflyIII\Enums\UserRoleEnum; use FireflyIII\Models\CurrencyExchangeRate; -use FireflyIII\Api\V1\Requests\Models\CurrencyExchangeRate\StoreRequest; -use FireflyIII\Api\V1\Controllers\Controller; use FireflyIII\Models\TransactionCurrency; use FireflyIII\Repositories\ExchangeRate\ExchangeRateRepositoryInterface; use FireflyIII\Support\Http\Api\ValidatesUserGroupTrait; @@ -43,7 +44,7 @@ class StoreController extends Controller use ValidatesUserGroupTrait; public const string RESOURCE_KEY = 'exchange-rates'; - protected array $acceptedRoles = [UserRoleEnum::OWNER]; + protected array $acceptedRoles = [UserRoleEnum::OWNER]; private ExchangeRateRepositoryInterface $repository; public function __construct() @@ -59,27 +60,23 @@ class StoreController extends Controller ); } - public function storeByDate(StoreByDateRequest $request, Carbon $date): JsonResponse { + public function storeByCurrencies(StoreByCurrenciesRequest $request, TransactionCurrency $from, TransactionCurrency $to): JsonResponse + { - $data = $request->getAll(); - $from = $request->getFromCurrency(); + $data = $request->getAll(); $collection = new Collection(); - foreach($data['rates'] as $key => $rate) { - $to = TransactionCurrency::where('code', $key)->first(); - if(null === $to) { - continue; // should not happen. - } + + foreach ($data as $date => $rate) { + $date = Carbon::createFromFormat('Y-m-d', $date); $existing = $this->repository->getSpecificRateOnDate($from, $to, $date); - if(null !== $existing) { + if (null !== $existing) { // update existing rate. - $existing = $this->repository->updateExchangeRate($existing, $rate); + $existing = $this->repository->updateExchangeRate($existing, $rate); $collection->push($existing); continue; } - if(null === $existing) { - $new = $this->repository->storeExchangeRate($from, $to, $rate, $date); - $collection->push($new); - } + $new = $this->repository->storeExchangeRate($from, $to, $rate, $date); + $collection->push($new); } $count = $collection->count(); @@ -89,19 +86,50 @@ class StoreController extends Controller return response() ->json($this->jsonApiList(self::RESOURCE_KEY, $paginator, $transformer)) - ->header('Content-Type', self::CONTENT_TYPE) - ; + ->header('Content-Type', self::CONTENT_TYPE); + } + + public function storeByDate(StoreByDateRequest $request, Carbon $date): JsonResponse + { + + $data = $request->getAll(); + $from = $request->getFromCurrency(); + $collection = new Collection(); + foreach ($data['rates'] as $key => $rate) { + $to = TransactionCurrency::where('code', $key)->first(); + if (null === $to) { + continue; // should not happen. + } + $existing = $this->repository->getSpecificRateOnDate($from, $to, $date); + if (null !== $existing) { + // update existing rate. + $existing = $this->repository->updateExchangeRate($existing, $rate); + $collection->push($existing); + continue; + } + $new = $this->repository->storeExchangeRate($from, $to, $rate, $date); + $collection->push($new); + } + + $count = $collection->count(); + $paginator = new LengthAwarePaginator($collection, $count, $count, 1); + $transformer = new ExchangeRateTransformer(); + $transformer->setParameters($this->parameters); // give params to transformer + + return response() + ->json($this->jsonApiList(self::RESOURCE_KEY, $paginator, $transformer)) + ->header('Content-Type', self::CONTENT_TYPE); } public function store(StoreRequest $request): JsonResponse { - $date = $request->getDate(); - $rate = $request->getRate(); - $from = $request->getFromCurrency(); - $to = $request->getToCurrency(); + $date = $request->getDate(); + $rate = $request->getRate(); + $from = $request->getFromCurrency(); + $to = $request->getToCurrency(); // already has rate? - $object = $this->repository->getSpecificRateOnDate($from, $to, $date); + $object = $this->repository->getSpecificRateOnDate($from, $to, $date); if ($object instanceof CurrencyExchangeRate) { // just update it, no matter. $rate = $this->repository->updateExchangeRate($object, $rate, $date); @@ -116,7 +144,6 @@ class StoreController extends Controller return response() ->api($this->jsonApiObject(self::RESOURCE_KEY, $rate, $transformer)) - ->header('Content-Type', self::CONTENT_TYPE) - ; + ->header('Content-Type', self::CONTENT_TYPE); } } diff --git a/app/Api/V1/Requests/Models/CurrencyExchangeRate/StoreByCurrenciesRequest.php b/app/Api/V1/Requests/Models/CurrencyExchangeRate/StoreByCurrenciesRequest.php new file mode 100644 index 0000000000..6ab4d88faf --- /dev/null +++ b/app/Api/V1/Requests/Models/CurrencyExchangeRate/StoreByCurrenciesRequest.php @@ -0,0 +1,73 @@ +all(); + } + + /** + * The rules that the incoming request must be matched against. + */ + public function rules(): array + { + return [ + '*' => 'required|numeric|min:0.0000000001', + ]; + } + + public function withValidator(Validator $validator): void + { + $validator->after( + static function (Validator $validator): void { + $data = $validator->getData() ?? []; + foreach ($data as $date => $rate) { + try { + $date = Carbon::createFromFormat('Y-m-d', $date); + } catch (InvalidFormatException $e) { + $validator->errors()->add('date', 'Invalid date info'); + return; + } + if (!is_numeric($rate)) { + $validator->errors()->add('rate', 'Rate must be a number.'); + return; + } + } + }); + } +} From 0e29e282df3431c505af3bb6249db6b78764e1c5 Mon Sep 17 00:00:00 2001 From: James Cole Date: Fri, 15 Aug 2025 11:38:25 +0200 Subject: [PATCH 14/17] Fix translations. --- .../StoreByCurrenciesRequest.php | 4 +- .../StoreByDateRequest.php | 4 +- resources/lang/en_US/validation.php | 402 +++++++++--------- 3 files changed, 204 insertions(+), 206 deletions(-) diff --git a/app/Api/V1/Requests/Models/CurrencyExchangeRate/StoreByCurrenciesRequest.php b/app/Api/V1/Requests/Models/CurrencyExchangeRate/StoreByCurrenciesRequest.php index 6ab4d88faf..58e2adbfb3 100644 --- a/app/Api/V1/Requests/Models/CurrencyExchangeRate/StoreByCurrenciesRequest.php +++ b/app/Api/V1/Requests/Models/CurrencyExchangeRate/StoreByCurrenciesRequest.php @@ -60,11 +60,11 @@ class StoreByCurrenciesRequest extends FormRequest try { $date = Carbon::createFromFormat('Y-m-d', $date); } catch (InvalidFormatException $e) { - $validator->errors()->add('date', 'Invalid date info'); + $validator->errors()->add('date', trans('validation.date',['attribute' => 'date'])); return; } if (!is_numeric($rate)) { - $validator->errors()->add('rate', 'Rate must be a number.'); + $validator->errors()->add('rate', trans('validation.number',['attribute' => 'rate'])); return; } } diff --git a/app/Api/V1/Requests/Models/CurrencyExchangeRate/StoreByDateRequest.php b/app/Api/V1/Requests/Models/CurrencyExchangeRate/StoreByDateRequest.php index 17c68b1183..19dab5dbdd 100644 --- a/app/Api/V1/Requests/Models/CurrencyExchangeRate/StoreByDateRequest.php +++ b/app/Api/V1/Requests/Models/CurrencyExchangeRate/StoreByDateRequest.php @@ -74,12 +74,12 @@ class StoreByDateRequest extends FormRequest } foreach ($rates as $key => $entry) { if ($key === $from->code) { - $validator->errors()->add(sprintf('rates.%s', $key), sprintf('Cannot convert from "%s" to itself.', $key)); + $validator->errors()->add(sprintf('rates.%s', $key), trans('validation.convert_to_itself', ['code' => $key])); continue; } $to = TransactionCurrency::where('code', $key)->first(); if (null === $to) { - $validator->errors()->add(sprintf('rates.%s', $key), sprintf('Invalid currency code "%s".', $key)); + $validator->errors()->add(sprintf('rates.%s', $key), trans('validation.invalid_currency_code', ['code' => $key])); } } }); diff --git a/resources/lang/en_US/validation.php b/resources/lang/en_US/validation.php index 6794b8b8f7..3dd5cdd9b7 100644 --- a/resources/lang/en_US/validation.php +++ b/resources/lang/en_US/validation.php @@ -21,174 +21,173 @@ */ - declare(strict_types=1); return [ - 'invalid_account_type' => 'A piggy bank can only be linked to asset accounts and liabilities', - 'invalid_account_currency' => 'This account does not use the currency you have selected', - 'current_amount_too_much' => 'The combined amount in "current_amount" cannot exceed the "target_amount".', - 'filter_must_be_in' => 'Filter ":filter" must be one of: :values', - 'filter_not_string' => 'Filter ":filter" is expected to be a string of text', - 'bad_api_filter' => 'This API endpoint does not support ":filter" as a filter.', - 'nog_logged_in' => 'You are not logged in.', - 'bad_type_source' => 'Firefly III can\'t determine the transaction type based on this source account.', - 'bad_type_destination' => 'Firefly III can\'t determine the transaction type based on this destination account.', - 'missing_where' => 'Array is missing "where"-clause', - 'missing_update' => 'Array is missing "update"-clause', - 'invalid_where_key' => 'JSON contains an invalid key for the "where"-clause', - 'invalid_update_key' => 'JSON contains an invalid key for the "update"-clause', - 'invalid_query_data' => 'There is invalid data in the %s:%s field of your query.', - 'invalid_query_account_type' => 'Your query contains accounts of different types, which is not allowed.', - 'invalid_query_currency' => 'Your query contains accounts that have different currency settings, which is not allowed.', - 'iban' => 'This is not a valid IBAN.', - 'zero_or_more' => 'The value cannot be negative.', - 'more_than_zero' => 'The value must be more than zero.', - 'more_than_zero_correct' => 'The value must be zero or more.', - 'no_asset_account' => 'This is not an asset account.', - 'date_or_time' => 'The value must be a valid date or time value (ISO 8601).', - 'source_equals_destination' => 'The source account equals the destination account.', - 'unique_account_number_for_user' => 'It looks like this account number is already in use.', - 'unique_user_group_for_user' => 'It looks like this administration title is already in use.', - 'unique_iban_for_user' => 'It looks like this IBAN is already in use.', - 'reconciled_forbidden_field' => 'This transaction is already reconciled, you cannot change the ":field"', - 'deleted_user' => 'Due to security constraints, you cannot register using this email address.', - 'rule_trigger_value' => 'This value is invalid for the selected trigger.', - 'rule_action_expression' => 'Invalid expression. :error', - 'rule_action_value' => 'This value is invalid for the selected action.', - 'file_already_attached' => 'Uploaded file ":name" is already attached to this object.', - 'file_attached' => 'Successfully uploaded file ":name".', - 'file_zero' => 'The file is zero bytes in size.', - 'must_exist' => 'The ID in field :attribute does not exist in the database.', - 'all_accounts_equal' => 'All accounts in this field must be equal.', - 'group_title_mandatory' => 'A group title is mandatory when there is more than one transaction.', - 'transaction_types_equal' => 'All splits must be of the same type.', - 'invalid_transaction_type' => 'Invalid transaction type.', - 'invalid_selection' => 'Your selection is invalid.', - 'belongs_user' => 'This value is linked to an object that does not seem to exist.', - 'belongs_user_or_user_group' => 'This value is linked to an object that does not seem to exist in your current financial administration.', - 'no_access_group' => 'The user has no access to this administration.', - 'no_accepted_roles_defined' => 'No access roles have been defined for this endpoint, access denied.', - 'at_least_one_transaction' => 'Need at least one transaction.', - 'recurring_transaction_id' => 'Need at least one transaction.', - 'need_id_to_match' => 'You need to submit this entry with an ID for the API to be able to match it.', - 'too_many_unmatched' => 'Too many submitted transactions cannot be matched to their respective database entries. Make sure existing entries have a valid ID.', - 'id_does_not_match' => 'Submitted ID #:id does not match expected ID. Make sure it matches or omit the field.', - 'at_least_one_repetition' => 'Need at least one repetition.', - 'require_repeat_until' => 'Require either a number of repetitions, or an end date (repeat_until). Not both.', - 'require_currency_info' => 'The content of this field is invalid without currency information.', - 'require_currency_id_code' => 'Please set either "transaction_currency_id" or "transaction_currency_code".', - 'not_transfer_account' => 'This account is not an account that can be used for transfers.', - 'require_currency_amount' => 'The content of this field is invalid without foreign amount information.', - 'require_foreign_currency' => 'This field requires a number', - 'require_foreign_dest' => 'This field value must match the currency of the destination account.', - 'require_foreign_src' => 'This field value must match the currency of the source account.', - 'equal_description' => 'Transaction description should not equal global description.', - 'file_invalid_mime' => 'File ":name" is of type ":mime" which is not accepted as a new upload.', - 'file_too_large' => 'File ":name" is too large.', - 'belongs_to_user' => 'The value of :attribute is unknown.', - 'accepted' => 'The :attribute must be accepted.', - 'bic' => 'This is not a valid BIC.', - 'at_least_one_trigger' => 'Rule must have at least one trigger.', - 'at_least_one_active_trigger' => 'Rule must have at least one active trigger.', - 'at_least_one_action' => 'Rule must have at least one action.', - 'at_least_one_active_action' => 'Rule must have at least one active action.', - 'base64' => 'This is not valid base64 encoded data.', - 'model_id_invalid' => 'The given ID seems invalid for this model.', - 'less' => ':attribute must be less than 10,000,000', - 'active_url' => 'The :attribute is not a valid URL.', - 'after' => 'The :attribute must be a date after :date.', - 'date_after' => 'The start date must be before the end date.', - 'alpha' => 'The :attribute may only contain letters.', - 'alpha_dash' => 'The :attribute may only contain letters, numbers, and dashes.', - 'alpha_num' => 'The :attribute may only contain letters and numbers.', - 'array' => 'The :attribute must be an array.', - 'unique_for_user' => 'There already is an entry with this :attribute.', - 'before' => 'The :attribute must be a date before :date.', - 'unique_object_for_user' => 'This name is already in use.', - 'unique_account_for_user' => 'This account name is already in use.', + 'invalid_account_type' => 'A piggy bank can only be linked to asset accounts and liabilities', + 'invalid_account_currency' => 'This account does not use the currency you have selected', + 'current_amount_too_much' => 'The combined amount in "current_amount" cannot exceed the "target_amount".', + 'filter_must_be_in' => 'Filter ":filter" must be one of: :values', + 'filter_not_string' => 'Filter ":filter" is expected to be a string of text', + 'bad_api_filter' => 'This API endpoint does not support ":filter" as a filter.', + 'nog_logged_in' => 'You are not logged in.', + 'bad_type_source' => 'Firefly III can\'t determine the transaction type based on this source account.', + 'bad_type_destination' => 'Firefly III can\'t determine the transaction type based on this destination account.', + 'missing_where' => 'Array is missing "where"-clause', + 'missing_update' => 'Array is missing "update"-clause', + 'invalid_where_key' => 'JSON contains an invalid key for the "where"-clause', + 'invalid_update_key' => 'JSON contains an invalid key for the "update"-clause', + 'invalid_query_data' => 'There is invalid data in the %s:%s field of your query.', + 'invalid_query_account_type' => 'Your query contains accounts of different types, which is not allowed.', + 'invalid_query_currency' => 'Your query contains accounts that have different currency settings, which is not allowed.', + 'iban' => 'This is not a valid IBAN.', + 'zero_or_more' => 'The value cannot be negative.', + 'more_than_zero' => 'The value must be more than zero.', + 'more_than_zero_correct' => 'The value must be zero or more.', + 'no_asset_account' => 'This is not an asset account.', + 'date_or_time' => 'The value must be a valid date or time value (ISO 8601).', + 'source_equals_destination' => 'The source account equals the destination account.', + 'unique_account_number_for_user' => 'It looks like this account number is already in use.', + 'unique_user_group_for_user' => 'It looks like this administration title is already in use.', + 'unique_iban_for_user' => 'It looks like this IBAN is already in use.', + 'reconciled_forbidden_field' => 'This transaction is already reconciled, you cannot change the ":field"', + 'deleted_user' => 'Due to security constraints, you cannot register using this email address.', + 'rule_trigger_value' => 'This value is invalid for the selected trigger.', + 'rule_action_expression' => 'Invalid expression. :error', + 'rule_action_value' => 'This value is invalid for the selected action.', + 'file_already_attached' => 'Uploaded file ":name" is already attached to this object.', + 'file_attached' => 'Successfully uploaded file ":name".', + 'file_zero' => 'The file is zero bytes in size.', + 'must_exist' => 'The ID in field :attribute does not exist in the database.', + 'all_accounts_equal' => 'All accounts in this field must be equal.', + 'group_title_mandatory' => 'A group title is mandatory when there is more than one transaction.', + 'transaction_types_equal' => 'All splits must be of the same type.', + 'invalid_transaction_type' => 'Invalid transaction type.', + 'invalid_selection' => 'Your selection is invalid.', + 'belongs_user' => 'This value is linked to an object that does not seem to exist.', + 'belongs_user_or_user_group' => 'This value is linked to an object that does not seem to exist in your current financial administration.', + 'no_access_group' => 'The user has no access to this administration.', + 'no_accepted_roles_defined' => 'No access roles have been defined for this endpoint, access denied.', + 'at_least_one_transaction' => 'Need at least one transaction.', + 'recurring_transaction_id' => 'Need at least one transaction.', + 'need_id_to_match' => 'You need to submit this entry with an ID for the API to be able to match it.', + 'too_many_unmatched' => 'Too many submitted transactions cannot be matched to their respective database entries. Make sure existing entries have a valid ID.', + 'id_does_not_match' => 'Submitted ID #:id does not match expected ID. Make sure it matches or omit the field.', + 'at_least_one_repetition' => 'Need at least one repetition.', + 'require_repeat_until' => 'Require either a number of repetitions, or an end date (repeat_until). Not both.', + 'require_currency_info' => 'The content of this field is invalid without currency information.', + 'require_currency_id_code' => 'Please set either "transaction_currency_id" or "transaction_currency_code".', + 'not_transfer_account' => 'This account is not an account that can be used for transfers.', + 'require_currency_amount' => 'The content of this field is invalid without foreign amount information.', + 'require_foreign_currency' => 'This field requires a number', + 'require_foreign_dest' => 'This field value must match the currency of the destination account.', + 'require_foreign_src' => 'This field value must match the currency of the source account.', + 'equal_description' => 'Transaction description should not equal global description.', + 'file_invalid_mime' => 'File ":name" is of type ":mime" which is not accepted as a new upload.', + 'file_too_large' => 'File ":name" is too large.', + 'belongs_to_user' => 'The value of :attribute is unknown.', + 'accepted' => 'The :attribute must be accepted.', + 'bic' => 'This is not a valid BIC.', + 'at_least_one_trigger' => 'Rule must have at least one trigger.', + 'at_least_one_active_trigger' => 'Rule must have at least one active trigger.', + 'at_least_one_action' => 'Rule must have at least one action.', + 'at_least_one_active_action' => 'Rule must have at least one active action.', + 'base64' => 'This is not valid base64 encoded data.', + 'model_id_invalid' => 'The given ID seems invalid for this model.', + 'less' => ':attribute must be less than 10,000,000', + 'active_url' => 'The :attribute is not a valid URL.', + 'after' => 'The :attribute must be a date after :date.', + 'date_after' => 'The start date must be before the end date.', + 'alpha' => 'The :attribute may only contain letters.', + 'alpha_dash' => 'The :attribute may only contain letters, numbers, and dashes.', + 'alpha_num' => 'The :attribute may only contain letters and numbers.', + 'array' => 'The :attribute must be an array.', + 'unique_for_user' => 'There already is an entry with this :attribute.', + 'before' => 'The :attribute must be a date before :date.', + 'unique_object_for_user' => 'This name is already in use.', + 'unique_account_for_user' => 'This account name is already in use.', - - 'between.numeric' => 'The :attribute must be between :min and :max.', - 'between.file' => 'The :attribute must be between :min and :max kilobytes.', - 'between.string' => 'The :attribute must be between :min and :max characters.', - 'between.array' => 'The :attribute must have between :min and :max items.', - 'boolean' => 'The :attribute field must be true or false.', - 'confirmed' => 'The :attribute confirmation does not match.', - 'date' => 'The :attribute is not a valid date.', - 'date_format' => 'The :attribute does not match the format :format.', - 'different' => 'The :attribute and :other must be different.', - 'digits' => 'The :attribute must be :digits digits.', - 'digits_between' => 'The :attribute must be between :min and :max digits.', - 'email' => 'The :attribute must be a valid email address.', - 'filled' => 'The :attribute field is required.', - 'exists' => 'The selected :attribute is invalid.', - 'image' => 'The :attribute must be an image.', - 'in' => 'The selected :attribute is invalid.', - 'integer' => 'The :attribute must be an integer.', - 'ip' => 'The :attribute must be a valid IP address.', - 'json' => 'The :attribute must be a valid JSON string.', - 'max.numeric' => 'The :attribute may not be greater than :max.', - 'max.file' => 'The :attribute may not be greater than :max kilobytes.', - 'max.string' => 'The :attribute may not be greater than :max characters.', - 'max.array' => 'The :attribute may not have more than :max items.', - 'mimes' => 'The :attribute must be a file of type: :values.', - 'min.numeric' => 'The :attribute must be at least :min.', - 'lte.numeric' => 'The :attribute must be less than or equal :value.', - 'min.file' => 'The :attribute must be at least :min kilobytes.', - 'min.string' => 'The :attribute must be at least :min characters.', - 'min.array' => 'The :attribute must have at least :min items.', - 'not_in' => 'The selected :attribute is invalid.', - 'numeric' => 'The :attribute must be a number.', - 'scientific_notation' => 'The :attribute cannot use the scientific notation.', - 'numeric_primary' => 'The primary currency amount must be a number.', - 'numeric_destination' => 'The destination amount must be a number.', - 'numeric_source' => 'The source amount must be a number.', - 'generic_invalid' => 'This value is invalid.', - 'transaction_type_changed' => 'If you change the type of the transaction, make sure the correct source/destination accounts are set.', - 'regex' => 'The :attribute format is invalid.', - 'required' => 'The :attribute field is required.', - 'required_if' => 'The :attribute field is required when :other is :value.', - 'required_unless' => 'The :attribute field is required unless :other is in :values.', - 'required_with' => 'The :attribute field is required when :values is present.', - 'required_with_all' => 'The :attribute field is required when :values is present.', - 'required_without' => 'The :attribute field is required when :values is not present.', - 'required_without_all' => 'The :attribute field is required when none of :values are present.', - 'same' => 'The :attribute and :other must match.', - 'size.numeric' => 'The :attribute must be :size.', - 'amount_min_over_max' => 'The minimum amount cannot be larger than the maximum amount.', - 'size.file' => 'The :attribute must be :size kilobytes.', - 'size.string' => 'The :attribute must be :size characters.', - 'size.array' => 'The :attribute must contain :size items.', - 'unique' => 'The :attribute has already been taken.', - 'string' => 'The :attribute must be a string.', - 'url' => 'The :attribute format is invalid.', - 'timezone' => 'The :attribute must be a valid zone.', - '2fa_code' => 'The :attribute field is invalid.', - 'dimensions' => 'The :attribute has invalid image dimensions.', - 'distinct' => 'The :attribute field has a duplicate value.', - 'file' => 'The :attribute must be a file.', - 'in_array' => 'The :attribute field does not exist in :other.', - 'present' => 'The :attribute field must be present.', - 'amount_zero' => 'The total amount cannot be zero.', - 'current_target_amount' => 'The current amount must be less than the target amount.', - 'unique_piggy_bank_for_user' => 'The name of the piggy bank must be unique.', - 'unique_object_group' => 'The group name must be unique', - 'starts_with' => 'The value must start with :values.', - 'unique_webhook' => 'You already have a webhook with this combination of URL, trigger, response and delivery.', - 'unique_existing_webhook' => 'You already have another webhook with this combination of URL, trigger, response and delivery.', - 'same_account_type' => 'Both accounts must be of the same account type', - 'same_account_currency' => 'Both accounts must have the same currency setting', - 'piggy_no_change_currency' => 'Because there are piggy banks linked to this account, you cannot change the currency of the account.', + 'between.numeric' => 'The :attribute must be between :min and :max.', + 'between.file' => 'The :attribute must be between :min and :max kilobytes.', + 'between.string' => 'The :attribute must be between :min and :max characters.', + 'between.array' => 'The :attribute must have between :min and :max items.', + 'boolean' => 'The :attribute field must be true or false.', + 'confirmed' => 'The :attribute confirmation does not match.', + 'date' => 'The :attribute is not a valid date.', + 'date_format' => 'The :attribute does not match the format :format.', + 'different' => 'The :attribute and :other must be different.', + 'digits' => 'The :attribute must be :digits digits.', + 'digits_between' => 'The :attribute must be between :min and :max digits.', + 'email' => 'The :attribute must be a valid email address.', + 'filled' => 'The :attribute field is required.', + 'exists' => 'The selected :attribute is invalid.', + 'image' => 'The :attribute must be an image.', + 'in' => 'The selected :attribute is invalid.', + 'integer' => 'The :attribute must be an integer.', + 'ip' => 'The :attribute must be a valid IP address.', + 'json' => 'The :attribute must be a valid JSON string.', + 'max.numeric' => 'The :attribute may not be greater than :max.', + 'max.file' => 'The :attribute may not be greater than :max kilobytes.', + 'max.string' => 'The :attribute may not be greater than :max characters.', + 'max.array' => 'The :attribute may not have more than :max items.', + 'mimes' => 'The :attribute must be a file of type: :values.', + 'min.numeric' => 'The :attribute must be at least :min.', + 'lte.numeric' => 'The :attribute must be less than or equal :value.', + 'min.file' => 'The :attribute must be at least :min kilobytes.', + 'min.string' => 'The :attribute must be at least :min characters.', + 'min.array' => 'The :attribute must have at least :min items.', + 'not_in' => 'The selected :attribute is invalid.', + 'numeric' => 'The :attribute must be a number.', + 'convert_to_itself' => 'Cannot store currency exchange rate for ":code", because from and to currency are the same.', + 'invalid_currency_code' => 'Currency code ":code" is invalid', + 'scientific_notation' => 'The :attribute cannot use the scientific notation.', + 'numeric_primary' => 'The primary currency amount must be a number.', + 'numeric_destination' => 'The destination amount must be a number.', + 'numeric_source' => 'The source amount must be a number.', + 'generic_invalid' => 'This value is invalid.', + 'transaction_type_changed' => 'If you change the type of the transaction, make sure the correct source/destination accounts are set.', + 'regex' => 'The :attribute format is invalid.', + 'required' => 'The :attribute field is required.', + 'required_if' => 'The :attribute field is required when :other is :value.', + 'required_unless' => 'The :attribute field is required unless :other is in :values.', + 'required_with' => 'The :attribute field is required when :values is present.', + 'required_with_all' => 'The :attribute field is required when :values is present.', + 'required_without' => 'The :attribute field is required when :values is not present.', + 'required_without_all' => 'The :attribute field is required when none of :values are present.', + 'same' => 'The :attribute and :other must match.', + 'size.numeric' => 'The :attribute must be :size.', + 'amount_min_over_max' => 'The minimum amount cannot be larger than the maximum amount.', + 'size.file' => 'The :attribute must be :size kilobytes.', + 'size.string' => 'The :attribute must be :size characters.', + 'size.array' => 'The :attribute must contain :size items.', + 'unique' => 'The :attribute has already been taken.', + 'string' => 'The :attribute must be a string.', + 'url' => 'The :attribute format is invalid.', + 'timezone' => 'The :attribute must be a valid zone.', + '2fa_code' => 'The :attribute field is invalid.', + 'dimensions' => 'The :attribute has invalid image dimensions.', + 'distinct' => 'The :attribute field has a duplicate value.', + 'file' => 'The :attribute must be a file.', + 'in_array' => 'The :attribute field does not exist in :other.', + 'present' => 'The :attribute field must be present.', + 'amount_zero' => 'The total amount cannot be zero.', + 'current_target_amount' => 'The current amount must be less than the target amount.', + 'unique_piggy_bank_for_user' => 'The name of the piggy bank must be unique.', + 'unique_object_group' => 'The group name must be unique', + 'starts_with' => 'The value must start with :values.', + 'unique_webhook' => 'You already have a webhook with this combination of URL, trigger, response and delivery.', + 'unique_existing_webhook' => 'You already have another webhook with this combination of URL, trigger, response and delivery.', + 'same_account_type' => 'Both accounts must be of the same account type', + 'same_account_currency' => 'Both accounts must have the same currency setting', + 'piggy_no_change_currency' => 'Because there are piggy banks linked to this account, you cannot change the currency of the account.', - - 'secure_password' => 'This is not a secure password. Please try again. For more information, visit https://bit.ly/FF3-password', - 'valid_recurrence_rep_type' => 'Invalid repetition type for recurring transactions.', - 'valid_recurrence_rep_moment' => 'Invalid repetition moment for this type of repetition.', - 'invalid_account_info' => 'Invalid account information.', - 'attributes' => [ + 'secure_password' => 'This is not a secure password. Please try again. For more information, visit https://bit.ly/FF3-password', + 'valid_recurrence_rep_type' => 'Invalid repetition type for recurring transactions.', + 'valid_recurrence_rep_moment' => 'Invalid repetition moment for this type of repetition.', + 'invalid_account_info' => 'Invalid account information.', + 'attributes' => [ 'email' => 'email address', 'description' => 'description', 'amount' => 'amount', @@ -227,59 +226,58 @@ return [ ], // validation of accounts: - 'withdrawal_source_need_data' => 'Need to get a valid source account ID and/or valid source account name to continue.', - 'withdrawal_source_bad_data' => '[a] Could not find a valid source account when searching for ID ":id" or name ":name".', - 'withdrawal_dest_need_data' => '[a] Need to get a valid destination account ID and/or valid destination account name to continue.', - 'withdrawal_dest_bad_data' => 'Could not find a valid destination account when searching for ID ":id" or name ":name".', + 'withdrawal_source_need_data' => 'Need to get a valid source account ID and/or valid source account name to continue.', + 'withdrawal_source_bad_data' => '[a] Could not find a valid source account when searching for ID ":id" or name ":name".', + 'withdrawal_dest_need_data' => '[a] Need to get a valid destination account ID and/or valid destination account name to continue.', + 'withdrawal_dest_bad_data' => 'Could not find a valid destination account when searching for ID ":id" or name ":name".', - 'withdrawal_dest_iban_exists' => 'This destination account IBAN is already in use by an asset account or a liability and cannot be used as a withdrawal destination.', - 'deposit_src_iban_exists' => 'This source account IBAN is already in use by an asset account or a liability and cannot be used as a deposit source.', + 'withdrawal_dest_iban_exists' => 'This destination account IBAN is already in use by an asset account or a liability and cannot be used as a withdrawal destination.', + 'deposit_src_iban_exists' => 'This source account IBAN is already in use by an asset account or a liability and cannot be used as a deposit source.', - 'reconciliation_source_bad_data' => 'Could not find a valid reconciliation account when searching for ID ":id" or name ":name".', + 'reconciliation_source_bad_data' => 'Could not find a valid reconciliation account when searching for ID ":id" or name ":name".', - 'generic_source_bad_data' => '[e] Could not find a valid source account when searching for ID ":id" or name ":name".', + 'generic_source_bad_data' => '[e] Could not find a valid source account when searching for ID ":id" or name ":name".', - 'deposit_source_need_data' => 'Need to get a valid source account ID and/or valid source account name to continue.', - 'deposit_source_bad_data' => '[b] Could not find a valid source account when searching for ID ":id" or name ":name".', - 'deposit_dest_need_data' => '[b] Need to get a valid destination account ID and/or valid destination account name to continue.', - 'deposit_dest_bad_data' => 'Could not find a valid destination account when searching for ID ":id" or name ":name".', - 'deposit_dest_wrong_type' => 'The submitted destination account is not of the right type.', + 'deposit_source_need_data' => 'Need to get a valid source account ID and/or valid source account name to continue.', + 'deposit_source_bad_data' => '[b] Could not find a valid source account when searching for ID ":id" or name ":name".', + 'deposit_dest_need_data' => '[b] Need to get a valid destination account ID and/or valid destination account name to continue.', + 'deposit_dest_bad_data' => 'Could not find a valid destination account when searching for ID ":id" or name ":name".', + 'deposit_dest_wrong_type' => 'The submitted destination account is not of the right type.', + 'transfer_source_need_data' => 'Need to get a valid source account ID and/or valid source account name to continue.', + 'transfer_source_bad_data' => '[c] Could not find a valid source account when searching for ID ":id" or name ":name".', + 'transfer_dest_need_data' => '[c] Need to get a valid destination account ID and/or valid destination account name to continue.', + 'transfer_dest_bad_data' => 'Could not find a valid destination account when searching for ID ":id" or name ":name".', + 'need_id_in_edit' => 'Each split must have transaction_journal_id (either valid ID or 0).', - 'transfer_source_need_data' => 'Need to get a valid source account ID and/or valid source account name to continue.', - 'transfer_source_bad_data' => '[c] Could not find a valid source account when searching for ID ":id" or name ":name".', - 'transfer_dest_need_data' => '[c] Need to get a valid destination account ID and/or valid destination account name to continue.', - 'transfer_dest_bad_data' => 'Could not find a valid destination account when searching for ID ":id" or name ":name".', - 'need_id_in_edit' => 'Each split must have transaction_journal_id (either valid ID or 0).', + 'ob_source_need_data' => 'Need to get a valid source account ID and/or valid source account name to continue.', + 'lc_source_need_data' => 'Need to get a valid source account ID to continue.', + 'ob_dest_need_data' => '[d] Need to get a valid destination account ID and/or valid destination account name to continue.', + 'ob_dest_bad_data' => 'Could not find a valid destination account when searching for ID ":id" or name ":name".', + 'reconciliation_either_account' => 'To submit a reconciliation, you must submit either a source or a destination account. Not both, not neither.', - 'ob_source_need_data' => 'Need to get a valid source account ID and/or valid source account name to continue.', - 'lc_source_need_data' => 'Need to get a valid source account ID to continue.', - 'ob_dest_need_data' => '[d] Need to get a valid destination account ID and/or valid destination account name to continue.', - 'ob_dest_bad_data' => 'Could not find a valid destination account when searching for ID ":id" or name ":name".', - 'reconciliation_either_account' => 'To submit a reconciliation, you must submit either a source or a destination account. Not both, not neither.', + 'generic_invalid_source' => 'You can\'t use this account as the source account.', + 'generic_invalid_destination' => 'You can\'t use this account as the destination account.', - 'generic_invalid_source' => 'You can\'t use this account as the source account.', - 'generic_invalid_destination' => 'You can\'t use this account as the destination account.', + 'generic_no_source' => 'You must submit source account information or submit a transaction journal ID.', + 'generic_no_destination' => 'You must submit destination account information or submit a transaction journal ID.', - 'generic_no_source' => 'You must submit source account information or submit a transaction journal ID.', - 'generic_no_destination' => 'You must submit destination account information or submit a transaction journal ID.', + 'gte.numeric' => 'The :attribute must be greater than or equal to :value.', + 'gt.numeric' => 'The :attribute must be greater than :value.', + 'gte.file' => 'The :attribute must be greater than or equal to :value kilobytes.', + 'gte.string' => 'The :attribute must be greater than or equal to :value characters.', + 'gte.array' => 'The :attribute must have :value items or more.', + 'missing_with' => 'The :attribute cannot be combined with another field.', - 'gte.numeric' => 'The :attribute must be greater than or equal to :value.', - 'gt.numeric' => 'The :attribute must be greater than :value.', - 'gte.file' => 'The :attribute must be greater than or equal to :value kilobytes.', - 'gte.string' => 'The :attribute must be greater than or equal to :value characters.', - 'gte.array' => 'The :attribute must have :value items or more.', - 'missing_with' => 'The :attribute cannot be combined with another field.', + 'amount_required_for_auto_budget' => 'The amount is required.', + 'auto_budget_amount_positive' => 'The amount must be more than zero.', - 'amount_required_for_auto_budget' => 'The amount is required.', - 'auto_budget_amount_positive' => 'The amount must be more than zero.', - - 'auto_budget_period_mandatory' => 'The auto budget period is a mandatory field.', + 'auto_budget_period_mandatory' => 'The auto budget period is a mandatory field.', // no access to administration: - 'no_auth_user_group' => 'You have to be logged in to access this administration.', - 'no_access_user_group' => 'You do not have the correct access rights for this administration.', - 'administration_owner_rename' => 'You can\'t rename your standard administration.', - 'existing_mfa_code' => 'Please enter a valid code', + 'no_auth_user_group' => 'You have to be logged in to access this administration.', + 'no_access_user_group' => 'You do not have the correct access rights for this administration.', + 'administration_owner_rename' => 'You can\'t rename your standard administration.', + 'existing_mfa_code' => 'Please enter a valid code', ]; From 4885dbc78e21f8e32a9047259acaa4afb3acd72f Mon Sep 17 00:00:00 2001 From: James Cole Date: Fri, 15 Aug 2025 13:33:22 +0200 Subject: [PATCH 15/17] Fix currency exchange rate UI. --- public/v1/js/.gitkeep | 0 resources/assets/v1/src/components/exchange-rates/Rates.vue | 6 +++--- 2 files changed, 3 insertions(+), 3 deletions(-) mode change 100644 => 100755 public/v1/js/.gitkeep diff --git a/public/v1/js/.gitkeep b/public/v1/js/.gitkeep old mode 100644 new mode 100755 diff --git a/resources/assets/v1/src/components/exchange-rates/Rates.vue b/resources/assets/v1/src/components/exchange-rates/Rates.vue index 2ac2a6ef25..0ac13a94ad 100644 --- a/resources/assets/v1/src/components/exchange-rates/Rates.vue +++ b/resources/assets/v1/src/components/exchange-rates/Rates.vue @@ -284,9 +284,9 @@ export default { // console.log(parts); // delete A to B - axios.delete("./api/v1/exchange-rates/rates/" + parts.from + '/' + parts.to + '?date=' + format(parts.date, 'yyyy-MM-dd')); + axios.delete("./api/v1/exchange-rates/" + parts.from + '/' + parts.to + '/' + format(parts.date, 'yyyy-MM-dd')); // delete B to A. - axios.delete("./api/v1/exchange-rates/rates/" + parts.to + '/' + parts.from + '?date=' + format(parts.date, 'yyyy-MM-dd')); + axios.delete("./api/v1/exchange-rates/" + parts.to + '/' + parts.from + '/' + format(parts.date, 'yyyy-MM-dd')); this.rates.splice(index, 1); }, @@ -327,7 +327,7 @@ export default { downloadRates: function (page) { this.tempRates = {}; this.loading = true; - axios.get("./api/v1/exchange-rates/rates/" + this.from_code + '/' + this.to_code + '?page=' + page).then((response) => { + axios.get("./api/v1/exchange-rates/" + this.from_code + '/' + this.to_code + '?page=' + page).then((response) => { for (let i in response.data.data) { if (response.data.data.hasOwnProperty(i)) { let current = response.data.data[i]; From f2dc0d234b0940e502f6ae6b7d0cb0df78261905 Mon Sep 17 00:00:00 2001 From: JC5 Date: Fri, 15 Aug 2025 13:37:27 +0200 Subject: [PATCH 16/17] =?UTF-8?q?=F0=9F=A4=96=20Auto=20commit=20for=20rele?= =?UTF-8?q?ase=20'develop'=20on=202025-08-15?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .ci/php-cs-fixer/composer.lock | 32 +-- .../Controllers/Chart/AccountController.php | 12 +- .../Controllers/Chart/BalanceController.php | 5 +- .../V1/Controllers/Chart/BudgetController.php | 72 +++---- .../Controllers/Chart/CategoryController.php | 38 ++-- .../DestroyController.php | 5 +- .../CurrencyExchangeRate/ShowController.php | 8 +- .../CurrencyExchangeRate/StoreController.php | 39 ++-- .../CurrencyExchangeRate/UpdateController.php | 10 +- .../StoreByCurrenciesRequest.php | 9 +- .../StoreByDateRequest.php | 9 +- .../ExchangeRate/ExchangeRateRepository.php | 40 ++-- .../ExchangeRateRepositoryInterface.php | 1 + .../Http/Api/AccountBalanceGrouped.php | 40 ++-- app/Support/Http/Api/CleansChartData.php | 10 +- composer.lock | 38 ++-- config/firefly.php | 4 +- package-lock.json | 188 +++++++++--------- public/v1/js/.gitkeep | 0 resources/assets/v1/src/locales/fr.json | 2 +- 20 files changed, 293 insertions(+), 269 deletions(-) mode change 100755 => 100644 public/v1/js/.gitkeep diff --git a/.ci/php-cs-fixer/composer.lock b/.ci/php-cs-fixer/composer.lock index 04ea32ba02..40ac982c67 100644 --- a/.ci/php-cs-fixer/composer.lock +++ b/.ci/php-cs-fixer/composer.lock @@ -345,16 +345,16 @@ }, { "name": "fidry/cpu-core-counter", - "version": "1.2.0", + "version": "1.3.0", "source": { "type": "git", "url": "https://github.com/theofidry/cpu-core-counter.git", - "reference": "8520451a140d3f46ac33042715115e290cf5785f" + "reference": "db9508f7b1474469d9d3c53b86f817e344732678" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/theofidry/cpu-core-counter/zipball/8520451a140d3f46ac33042715115e290cf5785f", - "reference": "8520451a140d3f46ac33042715115e290cf5785f", + "url": "https://api.github.com/repos/theofidry/cpu-core-counter/zipball/db9508f7b1474469d9d3c53b86f817e344732678", + "reference": "db9508f7b1474469d9d3c53b86f817e344732678", "shasum": "" }, "require": { @@ -364,10 +364,10 @@ "fidry/makefile": "^0.2.0", "fidry/php-cs-fixer-config": "^1.1.2", "phpstan/extension-installer": "^1.2.0", - "phpstan/phpstan": "^1.9.2", - "phpstan/phpstan-deprecation-rules": "^1.0.0", - "phpstan/phpstan-phpunit": "^1.2.2", - "phpstan/phpstan-strict-rules": "^1.4.4", + "phpstan/phpstan": "^2.0", + "phpstan/phpstan-deprecation-rules": "^2.0.0", + "phpstan/phpstan-phpunit": "^2.0", + "phpstan/phpstan-strict-rules": "^2.0", "phpunit/phpunit": "^8.5.31 || ^9.5.26", "webmozarts/strict-phpunit": "^7.5" }, @@ -394,7 +394,7 @@ ], "support": { "issues": "https://github.com/theofidry/cpu-core-counter/issues", - "source": "https://github.com/theofidry/cpu-core-counter/tree/1.2.0" + "source": "https://github.com/theofidry/cpu-core-counter/tree/1.3.0" }, "funding": [ { @@ -402,20 +402,20 @@ "type": "github" } ], - "time": "2024-08-06T10:04:20+00:00" + "time": "2025-08-14T07:29:31+00:00" }, { "name": "friendsofphp/php-cs-fixer", - "version": "v3.85.1", + "version": "v3.86.0", "source": { "type": "git", "url": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer.git", - "reference": "2fb6d7f6c3398dca5786a1635b27405d73a417ba" + "reference": "4a952bd19dc97879b0620f495552ef09b55f7d36" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/PHP-CS-Fixer/PHP-CS-Fixer/zipball/2fb6d7f6c3398dca5786a1635b27405d73a417ba", - "reference": "2fb6d7f6c3398dca5786a1635b27405d73a417ba", + "url": "https://api.github.com/repos/PHP-CS-Fixer/PHP-CS-Fixer/zipball/4a952bd19dc97879b0620f495552ef09b55f7d36", + "reference": "4a952bd19dc97879b0620f495552ef09b55f7d36", "shasum": "" }, "require": { @@ -499,7 +499,7 @@ ], "support": { "issues": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/issues", - "source": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/tree/v3.85.1" + "source": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/tree/v3.86.0" }, "funding": [ { @@ -507,7 +507,7 @@ "type": "github" } ], - "time": "2025-07-29T22:22:50+00:00" + "time": "2025-08-13T22:36:21+00:00" }, { "name": "psr/container", diff --git a/app/Api/V1/Controllers/Chart/AccountController.php b/app/Api/V1/Controllers/Chart/AccountController.php index 62212f71e2..7c762aece1 100644 --- a/app/Api/V1/Controllers/Chart/AccountController.php +++ b/app/Api/V1/Controllers/Chart/AccountController.php @@ -65,7 +65,7 @@ class AccountController extends Controller $this->chartData = new ChartData(); $this->repository = app(AccountRepositoryInterface::class); - $userGroup = $this->validateUserGroup($request); + $userGroup = $this->validateUserGroup($request); $this->repository->setUserGroup($userGroup); return $next($request); @@ -107,12 +107,12 @@ class AccountController extends Controller $range = Steam::finalAccountBalanceInRange($account, $params['start'], clone $params['end'], $this->convertToPrimary); - $previous = array_values($range)[0]['balance']; - $pcPrevious = null; + $previous = array_values($range)[0]['balance']; + $pcPrevious = null; if (!$currency instanceof TransactionCurrency) { $currency = $this->default; } - $currentSet = [ + $currentSet = [ 'label' => $account->name, // the currency that belongs to the account. @@ -153,7 +153,7 @@ class AccountController extends Controller // do the same for the primary currency balance, if relevant: - $pcBalance = null; + $pcBalance = null; if ($this->convertToPrimary) { $pcBalance = array_key_exists($format, $range) ? $range[$format]['pc_balance'] : $pcPrevious; $pcPrevious = $pcBalance; @@ -170,7 +170,7 @@ class AccountController extends Controller $defaultSet = $this->repository->getAccountsByType([AccountTypeEnum::ASSET->value])->pluck('id')->toArray(); /** @var Preference $frontpage */ - $frontpage = Preferences::get('frontpageAccounts', $defaultSet); + $frontpage = Preferences::get('frontpageAccounts', $defaultSet); if (!(is_array($frontpage->data) && count($frontpage->data) > 0)) { $frontpage->data = $defaultSet; diff --git a/app/Api/V1/Controllers/Chart/BalanceController.php b/app/Api/V1/Controllers/Chart/BalanceController.php index 083eb0bf33..52354aa7a3 100644 --- a/app/Api/V1/Controllers/Chart/BalanceController.php +++ b/app/Api/V1/Controllers/Chart/BalanceController.php @@ -12,7 +12,6 @@ use FireflyIII\Exceptions\FireflyException; use FireflyIII\Helpers\Collector\GroupCollectorInterface; use FireflyIII\Models\TransactionCurrency; use FireflyIII\Repositories\Account\AccountRepositoryInterface; -use FireflyIII\Support\Chart\ChartData; use FireflyIII\Support\Facades\Amount; use FireflyIII\Support\Http\Api\AccountBalanceGrouped; use FireflyIII\Support\Http\Api\CleansChartData; @@ -26,7 +25,7 @@ class BalanceController extends Controller { use CleansChartData; use CollectsAccountsFromFilter; - protected array $acceptedRoles = [UserRoleEnum::READ_ONLY]; + protected array $acceptedRoles = [UserRoleEnum::READ_ONLY]; private array $chartData; private GroupCollectorInterface $collector; @@ -89,7 +88,7 @@ class BalanceController extends Controller foreach ($data as $entry) { $this->chartData[] = $entry; } - $this->chartData= $this->clean($this->chartData); + $this->chartData = $this->clean($this->chartData); return response()->json($this->chartData); } diff --git a/app/Api/V1/Controllers/Chart/BudgetController.php b/app/Api/V1/Controllers/Chart/BudgetController.php index ae0fca6dab..a053ba00b7 100644 --- a/app/Api/V1/Controllers/Chart/BudgetController.php +++ b/app/Api/V1/Controllers/Chart/BudgetController.php @@ -50,7 +50,7 @@ class BudgetController extends Controller use CleansChartData; use ValidatesUserGroupTrait; - protected array $acceptedRoles = [UserRoleEnum::READ_ONLY]; + protected array $acceptedRoles = [UserRoleEnum::READ_ONLY]; protected OperationsRepositoryInterface $opsRepository; private BudgetLimitRepositoryInterface $blRepository; @@ -83,13 +83,13 @@ class BudgetController extends Controller */ public function overview(DateRequest $request): JsonResponse { - $params = $request->getAll(); + $params = $request->getAll(); /** @var Carbon $start */ - $start = $params['start']; + $start = $params['start']; /** @var Carbon $end */ - $end = $params['end']; + $end = $params['end']; // code from FrontpageChartGenerator, but not in separate class $budgets = $this->repository->getActiveBudgets(); @@ -115,19 +115,19 @@ class BudgetController extends Controller $spent = $this->opsRepository->listExpenses($start, $end, null, new Collection([$budget])); $expenses = $this->processExpenses($budget->id, $spent, $start, $end); $converter = new ExchangeRateConverter(); - $currencies = [$this->primaryCurrency->id => $this->primaryCurrency,]; + $currencies = [$this->primaryCurrency->id => $this->primaryCurrency]; /** - * @var int $currencyId + * @var int $currencyId * @var array $row */ foreach ($expenses as $currencyId => $row) { // budgeted, left and overspent are now 0. - $limit = $this->filterLimit($currencyId, $limits); + $limit = $this->filterLimit($currencyId, $limits); // primary currency entries $row['pc_budgeted'] = '0'; - $row['pc_spent'] = '0'; + $row['pc_spent'] = '0'; $row['pc_left'] = '0'; $row['pc_overspent'] = '0'; @@ -142,18 +142,18 @@ class BudgetController extends Controller // convert data if necessary. if (true === $this->convertToPrimary && $currencyId !== $this->primaryCurrency->id) { $currencies[$currencyId] ??= TransactionCurrency::find($currencyId); - $row['pc_budgeted'] = $converter->convert($currencies[$currencyId], $this->primaryCurrency, $start, $row['budgeted']); - $row['pc_spent'] = $converter->convert($currencies[$currencyId], $this->primaryCurrency, $start, $row['spent']); - $row['pc_left'] = $converter->convert($currencies[$currencyId], $this->primaryCurrency, $start, $row['left']); - $row['pc_overspent'] = $converter->convert($currencies[$currencyId], $this->primaryCurrency, $start, $row['overspent']); + $row['pc_budgeted'] = $converter->convert($currencies[$currencyId], $this->primaryCurrency, $start, $row['budgeted']); + $row['pc_spent'] = $converter->convert($currencies[$currencyId], $this->primaryCurrency, $start, $row['spent']); + $row['pc_left'] = $converter->convert($currencies[$currencyId], $this->primaryCurrency, $start, $row['left']); + $row['pc_overspent'] = $converter->convert($currencies[$currencyId], $this->primaryCurrency, $start, $row['overspent']); } if (true === $this->convertToPrimary && $currencyId === $this->primaryCurrency->id) { - $row['pc_budgeted'] = $row['budgeted']; - $row['pc_spent'] = $row['spent']; - $row['pc_left'] = $row['left']; - $row['pc_overspent'] = $row['overspent']; + $row['pc_budgeted'] = $row['budgeted']; + $row['pc_spent'] = $row['spent']; + $row['pc_left'] = $row['left']; + $row['pc_overspent'] = $row['overspent']; } - $rows[] = $row; + $rows[] = $row; } @@ -164,14 +164,14 @@ class BudgetController extends Controller // } // is always an array - $return = []; + $return = []; foreach ($rows as $row) { $current = [ - 'label' => $budget->name, - 'currency_id' => (string)$row['currency_id'], - 'currency_name' => $row['currency_name'], - 'currency_code' => $row['currency_code'], - 'currency_decimal_places' => $row['currency_decimal_places'], + 'label' => $budget->name, + 'currency_id' => (string)$row['currency_id'], + 'currency_name' => $row['currency_name'], + 'currency_code' => $row['currency_code'], + 'currency_decimal_places' => $row['currency_decimal_places'], 'primary_currency_id' => (string)$this->primaryCurrency->id, 'primary_currency_name' => $this->primaryCurrency->name, @@ -179,19 +179,19 @@ class BudgetController extends Controller 'primary_currency_symbol' => $this->primaryCurrency->symbol, 'primary_currency_decimal_places' => $this->primaryCurrency->decimal_places, - 'period' => null, - 'date' => $row['start'], - 'start_date' => $row['start'], - 'end_date' => $row['end'], - 'yAxisID' => 0, - 'type' => 'bar', - 'entries' => [ + 'period' => null, + 'date' => $row['start'], + 'start_date' => $row['start'], + 'end_date' => $row['end'], + 'yAxisID' => 0, + 'type' => 'bar', + 'entries' => [ 'budgeted' => $row['budgeted'], 'spent' => $row['spent'], 'left' => $row['left'], 'overspent' => $row['overspent'], ], - 'pc_entries' => [ + 'pc_entries' => [ 'budgeted' => $row['pc_budgeted'], 'spent' => '0', 'left' => '0', @@ -231,7 +231,7 @@ class BudgetController extends Controller * This array contains the expenses in this budget. Grouped per currency. * The grouping is on the main currency only. * - * @var int $currencyId + * @var int $currencyId * @var array $block */ foreach ($spent as $currencyId => $block) { @@ -249,7 +249,7 @@ class BudgetController extends Controller 'left' => '0', 'overspent' => '0', ]; - $currentBudgetArray = $block['budgets'][$budgetId]; + $currentBudgetArray = $block['budgets'][$budgetId]; // var_dump($return); /** @var array $journal */ @@ -290,7 +290,7 @@ class BudgetController extends Controller private function processLimit(Budget $budget, BudgetLimit $limit): array { Log::debug(sprintf('Created new ExchangeRateConverter in %s', __METHOD__)); - $end = clone $limit->end_date; + $end = clone $limit->end_date; $end->endOfDay(); $spent = $this->opsRepository->listExpenses($limit->start_date, $end, null, new Collection([$budget])); $limitCurrencyId = $limit->transaction_currency_id; @@ -298,8 +298,8 @@ class BudgetController extends Controller /** @var array $entry */ // only spent the entry where the entry's currency matches the budget limit's currency // so $filtered will only have 1 or 0 entries - $filtered = array_filter($spent, fn($entry) => $entry['currency_id'] === $limitCurrencyId); - $result = $this->processExpenses($budget->id, $filtered, $limit->start_date, $end); + $filtered = array_filter($spent, fn ($entry) => $entry['currency_id'] === $limitCurrencyId); + $result = $this->processExpenses($budget->id, $filtered, $limit->start_date, $end); if (1 === count($result)) { $compare = bccomp($limit->amount, (string)app('steam')->positive($result[$limitCurrencyId]['spent'])); $result[$limitCurrencyId]['budgeted'] = $limit->amount; diff --git a/app/Api/V1/Controllers/Chart/CategoryController.php b/app/Api/V1/Controllers/Chart/CategoryController.php index 986f2f4042..9073627218 100644 --- a/app/Api/V1/Controllers/Chart/CategoryController.php +++ b/app/Api/V1/Controllers/Chart/CategoryController.php @@ -81,7 +81,7 @@ class CategoryController extends Controller public function overview(DateRequest $request): JsonResponse { /** @var Carbon $start */ - $start = $this->parameters->get('start'); + $start = $this->parameters->get('start'); /** @var Carbon $end */ $end = $this->parameters->get('end'); @@ -92,25 +92,25 @@ class CategoryController extends Controller // get journals for entire period: /** @var GroupCollectorInterface $collector */ - $collector = app(GroupCollectorInterface::class); + $collector = app(GroupCollectorInterface::class); $collector->setRange($start, $end)->withAccountInformation(); $collector->setXorAccounts($accounts)->withCategoryInformation(); $collector->setTypes([TransactionTypeEnum::WITHDRAWAL->value, TransactionTypeEnum::RECONCILIATION->value]); - $journals = $collector->getExtractedJournals(); + $journals = $collector->getExtractedJournals(); /** @var array $journal */ foreach ($journals as $journal) { // find journal: - $journalCurrencyId = (int)$journal['currency_id']; - $currency = $currencies[$journalCurrencyId] ?? $this->currencyRepos->find($journalCurrencyId); - $currencies[$journalCurrencyId] = $currency; - $currencyId = (int)$currency->id; - $currencyName = (string)$currency->name; - $currencyCode = (string)$currency->code; - $currencySymbol = (string)$currency->symbol; - $currencyDecimalPlaces = (int)$currency->decimal_places; - $amount = Steam::positive($journal['amount']); - $pcAmount = null; + $journalCurrencyId = (int)$journal['currency_id']; + $currency = $currencies[$journalCurrencyId] ?? $this->currencyRepos->find($journalCurrencyId); + $currencies[$journalCurrencyId] = $currency; + $currencyId = (int)$currency->id; + $currencyName = (string)$currency->name; + $currencyCode = (string)$currency->code; + $currencySymbol = (string)$currency->symbol; + $currencyDecimalPlaces = (int)$currency->decimal_places; + $amount = Steam::positive($journal['amount']); + $pcAmount = null; // overrule if necessary: if ($this->convertToPrimary && $journalCurrencyId === $this->primaryCurrency->id) { @@ -127,8 +127,8 @@ class CategoryController extends Controller } - $categoryName = $journal['category_name'] ?? (string)trans('firefly.no_category'); - $key = sprintf('%s-%s', $categoryName, $currencyCode); + $categoryName = $journal['category_name'] ?? (string)trans('firefly.no_category'); + $key = sprintf('%s-%s', $categoryName, $currencyCode); // create arrays $return[$key] ??= [ 'label' => $categoryName, @@ -148,10 +148,10 @@ class CategoryController extends Controller 'yAxisID' => 0, 'type' => 'bar', 'entries' => [ - 'spent' => '0' + 'spent' => '0', ], 'pc_entries' => [ - 'spent' => '0' + 'spent' => '0', ], ]; @@ -161,10 +161,10 @@ class CategoryController extends Controller $return[$key]['pc_entries']['spent'] = bcadd($return[$key]['pc_entries']['spent'], (string)$pcAmount); } } - $return = array_values($return); + $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)$b['entries']['spent'] ? 1 : -1); return response()->json($this->clean($return)); } diff --git a/app/Api/V1/Controllers/Models/CurrencyExchangeRate/DestroyController.php b/app/Api/V1/Controllers/Models/CurrencyExchangeRate/DestroyController.php index 9ba01de025..265a72b616 100644 --- a/app/Api/V1/Controllers/Models/CurrencyExchangeRate/DestroyController.php +++ b/app/Api/V1/Controllers/Models/CurrencyExchangeRate/DestroyController.php @@ -28,13 +28,11 @@ use Carbon\Carbon; use FireflyIII\Api\V1\Controllers\Controller; use FireflyIII\Api\V1\Requests\Models\CurrencyExchangeRate\DestroyRequest; use FireflyIII\Enums\UserRoleEnum; -use FireflyIII\Exceptions\ValidationException; use FireflyIII\Models\CurrencyExchangeRate; use FireflyIII\Models\TransactionCurrency; use FireflyIII\Repositories\ExchangeRate\ExchangeRateRepositoryInterface; use FireflyIII\Support\Http\Api\ValidatesUserGroupTrait; use Illuminate\Http\JsonResponse; -use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; class DestroyController extends Controller { @@ -70,10 +68,11 @@ class DestroyController extends Controller return response()->json([], 204); } + public function destroySingleByDate(TransactionCurrency $from, TransactionCurrency $to, Carbon $date): JsonResponse { $exchangeRate = $this->repository->getSpecificRateOnDate($from, $to, $date); - if(null !== $exchangeRate) { + if (null !== $exchangeRate) { $this->repository->deleteRate($exchangeRate); } diff --git a/app/Api/V1/Controllers/Models/CurrencyExchangeRate/ShowController.php b/app/Api/V1/Controllers/Models/CurrencyExchangeRate/ShowController.php index abda7e7761..3b2f8c6e4d 100644 --- a/app/Api/V1/Controllers/Models/CurrencyExchangeRate/ShowController.php +++ b/app/Api/V1/Controllers/Models/CurrencyExchangeRate/ShowController.php @@ -91,17 +91,17 @@ class ShowController extends Controller public function showSingleByDate(TransactionCurrency $from, TransactionCurrency $to, Carbon $date): JsonResponse { - $transformer = new ExchangeRateTransformer(); + $transformer = new ExchangeRateTransformer(); $transformer->setParameters($this->parameters); - $exchangeRate = $this->repository->getSpecificRateOnDate($from, $to, $date); - if(null === $exchangeRate) { + $exchangeRate = $this->repository->getSpecificRateOnDate($from, $to, $date); + if (null === $exchangeRate) { throw new NotFoundHttpException(); } return response() ->api($this->jsonApiObject(self::RESOURCE_KEY, $exchangeRate, $transformer)) ->header('Content-Type', self::CONTENT_TYPE) - ; + ; } } diff --git a/app/Api/V1/Controllers/Models/CurrencyExchangeRate/StoreController.php b/app/Api/V1/Controllers/Models/CurrencyExchangeRate/StoreController.php index 676b621485..0e0f0fa8a1 100644 --- a/app/Api/V1/Controllers/Models/CurrencyExchangeRate/StoreController.php +++ b/app/Api/V1/Controllers/Models/CurrencyExchangeRate/StoreController.php @@ -43,7 +43,7 @@ class StoreController extends Controller { use ValidatesUserGroupTrait; - public const string RESOURCE_KEY = 'exchange-rates'; + public const string RESOURCE_KEY = 'exchange-rates'; protected array $acceptedRoles = [UserRoleEnum::OWNER]; private ExchangeRateRepositoryInterface $repository; @@ -63,8 +63,8 @@ class StoreController extends Controller public function storeByCurrencies(StoreByCurrenciesRequest $request, TransactionCurrency $from, TransactionCurrency $to): JsonResponse { - $data = $request->getAll(); - $collection = new Collection(); + $data = $request->getAll(); + $collection = new Collection(); foreach ($data as $date => $rate) { $date = Carbon::createFromFormat('Y-m-d', $date); @@ -73,9 +73,10 @@ class StoreController extends Controller // update existing rate. $existing = $this->repository->updateExchangeRate($existing, $rate); $collection->push($existing); + continue; } - $new = $this->repository->storeExchangeRate($from, $to, $rate, $date); + $new = $this->repository->storeExchangeRate($from, $to, $rate, $date); $collection->push($new); } @@ -86,17 +87,18 @@ class StoreController extends Controller return response() ->json($this->jsonApiList(self::RESOURCE_KEY, $paginator, $transformer)) - ->header('Content-Type', self::CONTENT_TYPE); + ->header('Content-Type', self::CONTENT_TYPE) + ; } public function storeByDate(StoreByDateRequest $request, Carbon $date): JsonResponse { - $data = $request->getAll(); - $from = $request->getFromCurrency(); - $collection = new Collection(); + $data = $request->getAll(); + $from = $request->getFromCurrency(); + $collection = new Collection(); foreach ($data['rates'] as $key => $rate) { - $to = TransactionCurrency::where('code', $key)->first(); + $to = TransactionCurrency::where('code', $key)->first(); if (null === $to) { continue; // should not happen. } @@ -105,9 +107,10 @@ class StoreController extends Controller // update existing rate. $existing = $this->repository->updateExchangeRate($existing, $rate); $collection->push($existing); + continue; } - $new = $this->repository->storeExchangeRate($from, $to, $rate, $date); + $new = $this->repository->storeExchangeRate($from, $to, $rate, $date); $collection->push($new); } @@ -118,18 +121,19 @@ class StoreController extends Controller return response() ->json($this->jsonApiList(self::RESOURCE_KEY, $paginator, $transformer)) - ->header('Content-Type', self::CONTENT_TYPE); + ->header('Content-Type', self::CONTENT_TYPE) + ; } public function store(StoreRequest $request): JsonResponse { - $date = $request->getDate(); - $rate = $request->getRate(); - $from = $request->getFromCurrency(); - $to = $request->getToCurrency(); + $date = $request->getDate(); + $rate = $request->getRate(); + $from = $request->getFromCurrency(); + $to = $request->getToCurrency(); // already has rate? - $object = $this->repository->getSpecificRateOnDate($from, $to, $date); + $object = $this->repository->getSpecificRateOnDate($from, $to, $date); if ($object instanceof CurrencyExchangeRate) { // just update it, no matter. $rate = $this->repository->updateExchangeRate($object, $rate, $date); @@ -144,6 +148,7 @@ class StoreController extends Controller return response() ->api($this->jsonApiObject(self::RESOURCE_KEY, $rate, $transformer)) - ->header('Content-Type', self::CONTENT_TYPE); + ->header('Content-Type', self::CONTENT_TYPE) + ; } } diff --git a/app/Api/V1/Controllers/Models/CurrencyExchangeRate/UpdateController.php b/app/Api/V1/Controllers/Models/CurrencyExchangeRate/UpdateController.php index 99b4d04bbc..8844407f7d 100644 --- a/app/Api/V1/Controllers/Models/CurrencyExchangeRate/UpdateController.php +++ b/app/Api/V1/Controllers/Models/CurrencyExchangeRate/UpdateController.php @@ -40,7 +40,7 @@ class UpdateController extends Controller { use ValidatesUserGroupTrait; - public const string RESOURCE_KEY = 'exchange-rates'; + public const string RESOURCE_KEY = 'exchange-rates'; protected array $acceptedRoles = [UserRoleEnum::OWNER]; private ExchangeRateRepositoryInterface $repository; @@ -67,7 +67,8 @@ class UpdateController extends Controller return response() ->api($this->jsonApiObject(self::RESOURCE_KEY, $exchangeRate, $transformer)) - ->header('Content-Type', self::CONTENT_TYPE); + ->header('Content-Type', self::CONTENT_TYPE) + ; } public function updateByDate(UpdateRequest $request, TransactionCurrency $from, TransactionCurrency $to, Carbon $date): JsonResponse @@ -80,11 +81,12 @@ class UpdateController extends Controller $rate = $request->getRate(); $exchangeRate = $this->repository->updateExchangeRate($exchangeRate, $rate, $date); - $transformer = new ExchangeRateTransformer(); + $transformer = new ExchangeRateTransformer(); $transformer->setParameters($this->parameters); return response() ->api($this->jsonApiObject(self::RESOURCE_KEY, $exchangeRate, $transformer)) - ->header('Content-Type', self::CONTENT_TYPE); + ->header('Content-Type', self::CONTENT_TYPE) + ; } } diff --git a/app/Api/V1/Requests/Models/CurrencyExchangeRate/StoreByCurrenciesRequest.php b/app/Api/V1/Requests/Models/CurrencyExchangeRate/StoreByCurrenciesRequest.php index 58e2adbfb3..67706bdc69 100644 --- a/app/Api/V1/Requests/Models/CurrencyExchangeRate/StoreByCurrenciesRequest.php +++ b/app/Api/V1/Requests/Models/CurrencyExchangeRate/StoreByCurrenciesRequest.php @@ -60,14 +60,17 @@ class StoreByCurrenciesRequest extends FormRequest try { $date = Carbon::createFromFormat('Y-m-d', $date); } catch (InvalidFormatException $e) { - $validator->errors()->add('date', trans('validation.date',['attribute' => 'date'])); + $validator->errors()->add('date', trans('validation.date', ['attribute' => 'date'])); + return; } if (!is_numeric($rate)) { - $validator->errors()->add('rate', trans('validation.number',['attribute' => 'rate'])); + $validator->errors()->add('rate', trans('validation.number', ['attribute' => 'rate'])); + return; } } - }); + } + ); } } diff --git a/app/Api/V1/Requests/Models/CurrencyExchangeRate/StoreByDateRequest.php b/app/Api/V1/Requests/Models/CurrencyExchangeRate/StoreByDateRequest.php index 19dab5dbdd..433e6ac90b 100644 --- a/app/Api/V1/Requests/Models/CurrencyExchangeRate/StoreByDateRequest.php +++ b/app/Api/V1/Requests/Models/CurrencyExchangeRate/StoreByDateRequest.php @@ -38,7 +38,7 @@ class StoreByDateRequest extends FormRequest public function getAll(): array { return [ - 'from' => $this->get('from'), + 'from' => $this->get('from'), 'rates' => $this->get('rates', []), ]; } @@ -62,7 +62,7 @@ class StoreByDateRequest extends FormRequest public function withValidator(Validator $validator): void { - $from = $this->getFromCurrency(); + $from = $this->getFromCurrency(); $validator->after( static function (Validator $validator) use ($from): void { @@ -70,11 +70,13 @@ class StoreByDateRequest extends FormRequest $rates = $data['rates'] ?? []; if (0 === count($rates)) { $validator->errors()->add('rates', 'No rates given.'); + return; } foreach ($rates as $key => $entry) { if ($key === $from->code) { $validator->errors()->add(sprintf('rates.%s', $key), trans('validation.convert_to_itself', ['code' => $key])); + continue; } $to = TransactionCurrency::where('code', $key)->first(); @@ -82,6 +84,7 @@ class StoreByDateRequest extends FormRequest $validator->errors()->add(sprintf('rates.%s', $key), trans('validation.invalid_currency_code', ['code' => $key])); } } - }); + } + ); } } diff --git a/app/Repositories/ExchangeRate/ExchangeRateRepository.php b/app/Repositories/ExchangeRate/ExchangeRateRepository.php index bcaf547cf8..d2f8544a4a 100644 --- a/app/Repositories/ExchangeRate/ExchangeRateRepository.php +++ b/app/Repositories/ExchangeRate/ExchangeRateRepository.php @@ -55,17 +55,20 @@ class ExchangeRateRepository implements ExchangeRateRepositoryInterface, UserGro // orderBy('date', 'DESC')->toRawSql(); return $this->userGroup->currencyExchangeRates() - ->where(function (Builder $q1) use ($from, $to): void { - $q1->where(function (Builder $q) use ($from, $to): void { - $q->where('from_currency_id', $from->id) - ->where('to_currency_id', $to->id); - })->orWhere(function (Builder $q) use ($from, $to): void { - $q->where('from_currency_id', $to->id) - ->where('to_currency_id', $from->id); - }); - }) - ->orderBy('date', 'DESC') - ->get(['currency_exchange_rates.*']); + ->where(function (Builder $q1) use ($from, $to): void { + $q1->where(function (Builder $q) use ($from, $to): void { + $q->where('from_currency_id', $from->id) + ->where('to_currency_id', $to->id) + ; + })->orWhere(function (Builder $q) use ($from, $to): void { + $q->where('from_currency_id', $to->id) + ->where('to_currency_id', $from->id) + ; + }); + }) + ->orderBy('date', 'DESC') + ->get(['currency_exchange_rates.*']) + ; } @@ -75,10 +78,11 @@ class ExchangeRateRepository implements ExchangeRateRepositoryInterface, UserGro /** @var null|CurrencyExchangeRate */ return $this->userGroup->currencyExchangeRates() - ->where('from_currency_id', $from->id) - ->where('to_currency_id', $to->id) - ->where('date', $date->format('Y-m-d')) - ->first(); + ->where('from_currency_id', $from->id) + ->where('to_currency_id', $to->id) + ->where('date', $date->format('Y-m-d')) + ->first() + ; } #[Override] @@ -112,8 +116,8 @@ class ExchangeRateRepository implements ExchangeRateRepositoryInterface, UserGro public function deleteRates(TransactionCurrency $from, TransactionCurrency $to): void { $this->userGroup->currencyExchangeRates() - ->where('from_currency_id', $from->id) - ->where('to_currency_id', $to->id) - ->delete(); + ->where('from_currency_id', $from->id) + ->where('to_currency_id', $to->id) + ->delete(); } } diff --git a/app/Repositories/ExchangeRate/ExchangeRateRepositoryInterface.php b/app/Repositories/ExchangeRate/ExchangeRateRepositoryInterface.php index 956c6a0787..04f8ac1072 100644 --- a/app/Repositories/ExchangeRate/ExchangeRateRepositoryInterface.php +++ b/app/Repositories/ExchangeRate/ExchangeRateRepositoryInterface.php @@ -46,6 +46,7 @@ use Illuminate\Support\Collection; interface ExchangeRateRepositoryInterface { public function deleteRate(CurrencyExchangeRate $rate): void; + public function deleteRates(TransactionCurrency $from, TransactionCurrency $to): void; public function getAll(): Collection; diff --git a/app/Support/Http/Api/AccountBalanceGrouped.php b/app/Support/Http/Api/AccountBalanceGrouped.php index 6d258df38c..397146be61 100644 --- a/app/Support/Http/Api/AccountBalanceGrouped.php +++ b/app/Support/Http/Api/AccountBalanceGrouped.php @@ -66,7 +66,7 @@ class AccountBalanceGrouped /** @var array $currency */ foreach ($this->data as $currency) { // income and expense array prepped: - $income = [ + $income = [ 'label' => 'earned', 'currency_id' => (string)$currency['currency_id'], 'currency_symbol' => $currency['currency_symbol'], @@ -85,7 +85,7 @@ class AccountBalanceGrouped 'entries' => [], 'pc_entries' => [], ]; - $expense = [ + $expense = [ 'label' => 'spent', 'currency_id' => (string)$currency['currency_id'], 'currency_symbol' => $currency['currency_symbol'], @@ -107,22 +107,22 @@ class AccountBalanceGrouped // loop all possible periods between $start and $end, and add them to the correct dataset. $currentStart = clone $this->start; while ($currentStart <= $this->end) { - $key = $currentStart->format($this->carbonFormat); - $label = $currentStart->toAtomString(); + $key = $currentStart->format($this->carbonFormat); + $label = $currentStart->toAtomString(); // normal entries - $income['entries'][$label] = Steam::bcround($currency[$key]['earned'] ?? '0', $currency['currency_decimal_places']); - $expense['entries'][$label] = Steam::bcround($currency[$key]['spent'] ?? '0', $currency['currency_decimal_places']); + $income['entries'][$label] = Steam::bcround($currency[$key]['earned'] ?? '0', $currency['currency_decimal_places']); + $expense['entries'][$label] = Steam::bcround($currency[$key]['spent'] ?? '0', $currency['currency_decimal_places']); // converted entries $income['pc_entries'][$label] = Steam::bcround($currency[$key]['pc_earned'] ?? '0', $currency['primary_currency_decimal_places']); $expense['pc_entries'][$label] = Steam::bcround($currency[$key]['pc_spent'] ?? '0', $currency['primary_currency_decimal_places']); // next loop - $currentStart = Navigation::addPeriod($currentStart, $this->preferredRange, 0); + $currentStart = Navigation::addPeriod($currentStart, $this->preferredRange, 0); } - $chartData[] = $income; - $chartData[] = $expense; + $chartData[] = $income; + $chartData[] = $expense; } return $chartData; @@ -148,9 +148,9 @@ class AccountBalanceGrouped private function processJournal(array $journal): void { // format the date according to the period - $period = $journal['date']->format($this->carbonFormat); - $currencyId = (int)$journal['currency_id']; - $currency = $this->findCurrency($currencyId); + $period = $journal['date']->format($this->carbonFormat); + $currencyId = (int)$journal['currency_id']; + $currency = $this->findCurrency($currencyId); // set the array with monetary info, if it does not exist. $this->createDefaultDataEntry($journal); @@ -158,12 +158,12 @@ class AccountBalanceGrouped $this->createDefaultPeriodEntry($journal); // is this journal's amount in- our outgoing? - $key = $this->getDataKey($journal); - $amount = 'spent' === $key ? Steam::negative($journal['amount']) : Steam::positive($journal['amount']); + $key = $this->getDataKey($journal); + $amount = 'spent' === $key ? Steam::negative($journal['amount']) : Steam::positive($journal['amount']); // get conversion rate - $rate = $this->getRate($currency, $journal['date']); - $amountConverted = bcmul((string)$amount, $rate); + $rate = $this->getRate($currency, $journal['date']); + $amountConverted = bcmul((string)$amount, $rate); // perhaps transaction already has the foreign amount in the primary currency. if ((int)$journal['foreign_currency_id'] === $this->primary->id) { @@ -172,7 +172,7 @@ class AccountBalanceGrouped } // add normal entry - $this->data[$currencyId][$period][$key] = bcadd((string)$this->data[$currencyId][$period][$key], (string)$amount); + $this->data[$currencyId][$period][$key] = bcadd((string)$this->data[$currencyId][$period][$key], (string)$amount); // add converted entry $convertedKey = sprintf('pc_%s', $key); @@ -191,7 +191,7 @@ class AccountBalanceGrouped private function createDefaultDataEntry(array $journal): void { - $currencyId = (int)$journal['currency_id']; + $currencyId = (int)$journal['currency_id']; $this->data[$currencyId] ??= [ 'currency_id' => (string)$currencyId, 'currency_symbol' => $journal['currency_symbol'], @@ -208,8 +208,8 @@ class AccountBalanceGrouped private function createDefaultPeriodEntry(array $journal): void { - $currencyId = (int)$journal['currency_id']; - $period = $journal['date']->format($this->carbonFormat); + $currencyId = (int)$journal['currency_id']; + $period = $journal['date']->format($this->carbonFormat); $this->data[$currencyId][$period] ??= [ 'period' => $period, 'spent' => '0', diff --git a/app/Support/Http/Api/CleansChartData.php b/app/Support/Http/Api/CleansChartData.php index 702926d35c..4b02726c1b 100644 --- a/app/Support/Http/Api/CleansChartData.php +++ b/app/Support/Http/Api/CleansChartData.php @@ -47,14 +47,15 @@ trait CleansChartData * @var array $array */ foreach ($data as $index => $array) { - $array = $this->cleanSingleArray($index, $array); + $array = $this->cleanSingleArray($index, $array); $return[] = $array; } return $return; } - private function cleanSingleArray(mixed $index, array $array): array { + private function cleanSingleArray(mixed $index, array $array): array + { if (array_key_exists('currency_id', $array)) { $array['currency_id'] = (string)$array['currency_id']; } @@ -62,14 +63,15 @@ trait CleansChartData $array['primary_currency_id'] = (string)$array['primary_currency_id']; } $required = [ - 'start_date', 'end_date', 'period', 'yAxisID','type','entries','pc_entries', - 'currency_id', 'primary_currency_id' + 'start_date', 'end_date', 'period', 'yAxisID', 'type', 'entries', 'pc_entries', + 'currency_id', 'primary_currency_id', ]; foreach ($required as $field) { if (!array_key_exists($field, $array)) { throw new FireflyException(sprintf('Data-set "%s" is missing the "%s"-variable.', $index, $field)); } } + return $array; } } diff --git a/composer.lock b/composer.lock index b502f9f472..a6297fc8ce 100644 --- a/composer.lock +++ b/composer.lock @@ -1878,16 +1878,16 @@ }, { "name": "laravel/framework", - "version": "v12.23.1", + "version": "v12.24.0", "source": { "type": "git", "url": "https://github.com/laravel/framework.git", - "reference": "2a0e9331a0db904236143fe915c281ff4be274a3" + "reference": "6dcf2c46da23d159f35d6246234953a74b740d83" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/framework/zipball/2a0e9331a0db904236143fe915c281ff4be274a3", - "reference": "2a0e9331a0db904236143fe915c281ff4be274a3", + "url": "https://api.github.com/repos/laravel/framework/zipball/6dcf2c46da23d159f35d6246234953a74b740d83", + "reference": "6dcf2c46da23d159f35d6246234953a74b740d83", "shasum": "" }, "require": { @@ -1997,7 +1997,7 @@ "league/flysystem-read-only": "^3.25.1", "league/flysystem-sftp-v3": "^3.25.1", "mockery/mockery": "^1.6.10", - "orchestra/testbench-core": "^10.0.0", + "orchestra/testbench-core": "^10.6.0", "pda/pheanstalk": "^5.0.6|^7.0.0", "php-http/discovery": "^1.15", "phpstan/phpstan": "^2.0", @@ -2091,7 +2091,7 @@ "issues": "https://github.com/laravel/framework/issues", "source": "https://github.com/laravel/framework" }, - "time": "2025-08-12T17:35:05+00:00" + "time": "2025-08-13T20:30:36+00:00" }, { "name": "laravel/passport", @@ -10987,16 +10987,16 @@ }, { "name": "nikic/php-parser", - "version": "v5.6.0", + "version": "v5.6.1", "source": { "type": "git", "url": "https://github.com/nikic/PHP-Parser.git", - "reference": "221b0d0fdf1369c71047ad1d18bb5880017bbc56" + "reference": "f103601b29efebd7ff4a1ca7b3eeea9e3336a2a2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/221b0d0fdf1369c71047ad1d18bb5880017bbc56", - "reference": "221b0d0fdf1369c71047ad1d18bb5880017bbc56", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/f103601b29efebd7ff4a1ca7b3eeea9e3336a2a2", + "reference": "f103601b29efebd7ff4a1ca7b3eeea9e3336a2a2", "shasum": "" }, "require": { @@ -11015,7 +11015,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "5.0-dev" + "dev-master": "5.x-dev" } }, "autoload": { @@ -11039,9 +11039,9 @@ ], "support": { "issues": "https://github.com/nikic/PHP-Parser/issues", - "source": "https://github.com/nikic/PHP-Parser/tree/v5.6.0" + "source": "https://github.com/nikic/PHP-Parser/tree/v5.6.1" }, - "time": "2025-07-27T20:03:57+00:00" + "time": "2025-08-13T20:13:15+00:00" }, { "name": "phar-io/manifest", @@ -11876,16 +11876,16 @@ }, { "name": "rector/rector", - "version": "2.1.2", + "version": "2.1.3", "source": { "type": "git", "url": "https://github.com/rectorphp/rector.git", - "reference": "40a71441dd73fa150a66102f5ca1364c44fc8fff" + "reference": "dd430c869fddf4965049c8fd6f5ee49f155cfddf" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/rectorphp/rector/zipball/40a71441dd73fa150a66102f5ca1364c44fc8fff", - "reference": "40a71441dd73fa150a66102f5ca1364c44fc8fff", + "url": "https://api.github.com/repos/rectorphp/rector/zipball/dd430c869fddf4965049c8fd6f5ee49f155cfddf", + "reference": "dd430c869fddf4965049c8fd6f5ee49f155cfddf", "shasum": "" }, "require": { @@ -11924,7 +11924,7 @@ ], "support": { "issues": "https://github.com/rectorphp/rector/issues", - "source": "https://github.com/rectorphp/rector/tree/2.1.2" + "source": "https://github.com/rectorphp/rector/tree/2.1.3" }, "funding": [ { @@ -11932,7 +11932,7 @@ "type": "github" } ], - "time": "2025-07-17T19:30:06+00:00" + "time": "2025-08-13T11:43:04+00:00" }, { "name": "sebastian/cli-parser", diff --git a/config/firefly.php b/config/firefly.php index 8e7e49fabd..c6f9d33788 100644 --- a/config/firefly.php +++ b/config/firefly.php @@ -78,8 +78,8 @@ return [ 'running_balance_column' => env('USE_RUNNING_BALANCE', false), // see cer.php for exchange rates feature flag. ], - 'version' => 'develop/2025-08-13', - 'build_time' => 1755064254, + 'version' => 'develop/2025-08-15', + 'build_time' => 1755257739, 'api_version' => '2.1.0', // field is no longer used. 'db_version' => 26, diff --git a/package-lock.json b/package-lock.json index 563fe2e3a4..336c6d0d6d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -53,22 +53,22 @@ } }, "node_modules/@babel/core": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.0.tgz", - "integrity": "sha512-UlLAnTPrFdNGoFtbSXwcGFQBtQZJCNjaN6hQNP3UPvuNXT1i82N26KL3dZeIpNalWywr9IuQuncaAfUaS1g6sQ==", + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.3.tgz", + "integrity": "sha512-yDBHV9kQNcr2/sUr9jghVyz9C3Y5G2zUM2H2lo+9mKv4sFgbA8s8Z9t8D1jiTkGoO/NoIfKMyKWr4s6CN23ZwQ==", "dev": true, "license": "MIT", "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.27.1", - "@babel/generator": "^7.28.0", + "@babel/generator": "^7.28.3", "@babel/helper-compilation-targets": "^7.27.2", - "@babel/helper-module-transforms": "^7.27.3", - "@babel/helpers": "^7.27.6", - "@babel/parser": "^7.28.0", + "@babel/helper-module-transforms": "^7.28.3", + "@babel/helpers": "^7.28.3", + "@babel/parser": "^7.28.3", "@babel/template": "^7.27.2", - "@babel/traverse": "^7.28.0", - "@babel/types": "^7.28.0", + "@babel/traverse": "^7.28.3", + "@babel/types": "^7.28.2", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", @@ -94,14 +94,14 @@ } }, "node_modules/@babel/generator": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.0.tgz", - "integrity": "sha512-lJjzvrbEeWrhB4P3QBsH7tey117PjLZnDbLiQEKjQ/fNJTjuq4HSqgFA+UNSwZT8D7dxxbnuSBMsa1lrWzKlQg==", + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.3.tgz", + "integrity": "sha512-3lSpxGgvnmZznmBkCRnVREPUFJv2wrv9iAoFDvADJc0ypmdOxdUtcLeBgBJ6zE0PMeTKnxeQzyk0xTBq4Ep7zw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/parser": "^7.28.0", - "@babel/types": "^7.28.0", + "@babel/parser": "^7.28.3", + "@babel/types": "^7.28.2", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" @@ -151,18 +151,18 @@ } }, "node_modules/@babel/helper-create-class-features-plugin": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.27.1.tgz", - "integrity": "sha512-QwGAmuvM17btKU5VqXfb+Giw4JcN0hjuufz3DYnpeVDvZLAObloM77bhMXiqry3Iio+Ai4phVRDwl6WU10+r5A==", + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.28.3.tgz", + "integrity": "sha512-V9f6ZFIYSLNEbuGA/92uOvYsGCJNsuA8ESZ4ldc09bWk/j8H8TKiPw8Mk1eG6olpnO0ALHJmYfZvF4MEE4gajg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-annotate-as-pure": "^7.27.1", + "@babel/helper-annotate-as-pure": "^7.27.3", "@babel/helper-member-expression-to-functions": "^7.27.1", "@babel/helper-optimise-call-expression": "^7.27.1", "@babel/helper-replace-supers": "^7.27.1", "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", - "@babel/traverse": "^7.27.1", + "@babel/traverse": "^7.28.3", "semver": "^6.3.1" }, "engines": { @@ -266,15 +266,15 @@ } }, "node_modules/@babel/helper-module-transforms": { - "version": "7.27.3", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.27.3.tgz", - "integrity": "sha512-dSOvYwvyLsWBeIRyOeHXp5vPj5l1I011r52FM1+r1jCERv+aFXYk4whgQccYEGYxK2H3ZAIA8nuPkQ0HaUo3qg==", + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.3.tgz", + "integrity": "sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw==", "dev": true, "license": "MIT", "dependencies": { "@babel/helper-module-imports": "^7.27.1", "@babel/helper-validator-identifier": "^7.27.1", - "@babel/traverse": "^7.27.3" + "@babel/traverse": "^7.28.3" }, "engines": { "node": ">=6.9.0" @@ -387,24 +387,24 @@ } }, "node_modules/@babel/helper-wrap-function": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.27.1.tgz", - "integrity": "sha512-NFJK2sHUvrjo8wAU/nQTWU890/zB2jj0qBcCbZbbf+005cAsv6tMjXz31fBign6M5ov1o0Bllu+9nbqkfsjjJQ==", + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.28.3.tgz", + "integrity": "sha512-zdf983tNfLZFletc0RRXYrHrucBEg95NIFMkn6K9dbeMYnsgHaSBGcQqdsCSStG2PYwRre0Qc2NNSCXbG+xc6g==", "dev": true, "license": "MIT", "dependencies": { - "@babel/template": "^7.27.1", - "@babel/traverse": "^7.27.1", - "@babel/types": "^7.27.1" + "@babel/template": "^7.27.2", + "@babel/traverse": "^7.28.3", + "@babel/types": "^7.28.2" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helpers": { - "version": "7.28.2", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.2.tgz", - "integrity": "sha512-/V9771t+EgXz62aCcyofnQhGM8DQACbRhvzKFsXKC9QM+5MadF8ZmIm0crDMaz3+o0h0zXfJnd4EhbYbxsrcFw==", + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.3.tgz", + "integrity": "sha512-PTNtvUQihsAsDHMOP5pfobP8C6CM4JWXmP8DrEIt46c3r2bf87Ua1zoqevsMo9g+tWDwgWrFP5EIxuBx5RudAw==", "dev": true, "license": "MIT", "dependencies": { @@ -416,13 +416,13 @@ } }, "node_modules/@babel/parser": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.0.tgz", - "integrity": "sha512-jVZGvOxOuNSsuQuLRTh13nU0AogFlw32w/MT+LV6D3sP5WdbW61E77RnkbaO2dUvmPAYrBDJXGn5gGS6tH4j8g==", + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.3.tgz", + "integrity": "sha512-7+Ey1mAgYqFAx2h0RuoxcQT5+MlG3GTV0TQrgr7/ZliKsm/MNDxVVutlWaziMq7wJNAz8MTqz55XLpWvva6StA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/types": "^7.28.0" + "@babel/types": "^7.28.2" }, "bin": { "parser": "bin/babel-parser.js" @@ -499,14 +499,14 @@ } }, "node_modules/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly/-/plugin-bugfix-v8-static-class-fields-redefine-readonly-7.27.1.tgz", - "integrity": "sha512-6BpaYGDavZqkI6yT+KSPdpZFfpnd68UKXbcjI9pJ13pvHhPrCKWOOLp+ysvMeA+DxnhuPpgIaRpxRxo5A9t5jw==", + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly/-/plugin-bugfix-v8-static-class-fields-redefine-readonly-7.28.3.tgz", + "integrity": "sha512-b6YTX108evsvE4YgWyQ921ZAFFQm3Bn+CA3+ZXlNVnPhx+UfsVURoPjfGAPCjBgrqo30yX/C2nZGX96DxvR9Iw==", "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.27.1", - "@babel/traverse": "^7.27.1" + "@babel/traverse": "^7.28.3" }, "engines": { "node": ">=6.9.0" @@ -726,13 +726,13 @@ } }, "node_modules/@babel/plugin-transform-class-static-block": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.27.1.tgz", - "integrity": "sha512-s734HmYU78MVzZ++joYM+NkJusItbdRcbm+AGRgJCt3iA+yux0QpD9cBVdz3tKyrjVYWRl7j0mHSmv4lhV0aoA==", + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.28.3.tgz", + "integrity": "sha512-LtPXlBbRoc4Njl/oh1CeD/3jC+atytbnf/UqLoqTDcEYGUPj022+rvfkbDYieUrSj3CaV4yHDByPE+T2HwfsJg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-create-class-features-plugin": "^7.27.1", + "@babel/helper-create-class-features-plugin": "^7.28.3", "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { @@ -743,9 +743,9 @@ } }, "node_modules/@babel/plugin-transform-classes": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.28.0.tgz", - "integrity": "sha512-IjM1IoJNw72AZFlj33Cu8X0q2XK/6AaVC3jQu+cgQ5lThWD5ajnuUAml80dqRmOhmPkTH8uAwnpMu9Rvj0LTRA==", + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.28.3.tgz", + "integrity": "sha512-DoEWC5SuxuARF2KdKmGUq3ghfPMO6ZzR12Dnp5gubwbeWJo4dbNWXJPVlwvh4Zlq6Z7YVvL8VFxeSOJgjsx4Sg==", "dev": true, "license": "MIT", "dependencies": { @@ -754,7 +754,7 @@ "@babel/helper-globals": "^7.28.0", "@babel/helper-plugin-utils": "^7.27.1", "@babel/helper-replace-supers": "^7.27.1", - "@babel/traverse": "^7.28.0" + "@babel/traverse": "^7.28.3" }, "engines": { "node": ">=6.9.0" @@ -1284,9 +1284,9 @@ } }, "node_modules/@babel/plugin-transform-regenerator": { - "version": "7.28.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.28.1.tgz", - "integrity": "sha512-P0QiV/taaa3kXpLY+sXla5zec4E+4t4Aqc9ggHlfZ7a2cp8/x/Gv08jfwEtn9gnnYIMvHx6aoOZ8XJL8eU71Dg==", + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.28.3.tgz", + "integrity": "sha512-K3/M/a4+ESb5LEldjQb+XSrpY0nF+ZBFlTCbSnKaYAMfD8v33O6PMs4uYnOk19HlcsI8WMu3McdFPTiQHF/1/A==", "dev": true, "license": "MIT", "dependencies": { @@ -1333,9 +1333,9 @@ } }, "node_modules/@babel/plugin-transform-runtime": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.28.0.tgz", - "integrity": "sha512-dGopk9nZrtCs2+nfIem25UuHyt5moSJamArzIoh9/vezUQPmYDOzjaHDCkAzuGJibCIkPup8rMT2+wYB6S73cA==", + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.28.3.tgz", + "integrity": "sha512-Y6ab1kGqZ0u42Zv/4a7l0l72n9DKP/MKoKWaUSBylrhNZO2prYuqFOLbn5aW5SIFXwSH93yfjbgllL8lxuGKLg==", "dev": true, "license": "MIT", "dependencies": { @@ -1512,9 +1512,9 @@ } }, "node_modules/@babel/preset-env": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.28.0.tgz", - "integrity": "sha512-VmaxeGOwuDqzLl5JUkIRM1X2Qu2uKGxHEQWh+cvvbl7JuJRgKGJSfsEF/bUaxFhJl/XAyxBe7q7qSuTbKFuCyg==", + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.28.3.tgz", + "integrity": "sha512-ROiDcM+GbYVPYBOeCR6uBXKkQpBExLl8k9HO1ygXEyds39j+vCCsjmj7S8GOniZQlEs81QlkdJZe76IpLSiqpg==", "dev": true, "license": "MIT", "dependencies": { @@ -1526,7 +1526,7 @@ "@babel/plugin-bugfix-safari-class-field-initializer-scope": "^7.27.1", "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.27.1", "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.27.1", - "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": "^7.27.1", + "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": "^7.28.3", "@babel/plugin-proposal-private-property-in-object": "7.21.0-placeholder-for-preset-env.2", "@babel/plugin-syntax-import-assertions": "^7.27.1", "@babel/plugin-syntax-import-attributes": "^7.27.1", @@ -1537,8 +1537,8 @@ "@babel/plugin-transform-block-scoped-functions": "^7.27.1", "@babel/plugin-transform-block-scoping": "^7.28.0", "@babel/plugin-transform-class-properties": "^7.27.1", - "@babel/plugin-transform-class-static-block": "^7.27.1", - "@babel/plugin-transform-classes": "^7.28.0", + "@babel/plugin-transform-class-static-block": "^7.28.3", + "@babel/plugin-transform-classes": "^7.28.3", "@babel/plugin-transform-computed-properties": "^7.27.1", "@babel/plugin-transform-destructuring": "^7.28.0", "@babel/plugin-transform-dotall-regex": "^7.27.1", @@ -1570,7 +1570,7 @@ "@babel/plugin-transform-private-methods": "^7.27.1", "@babel/plugin-transform-private-property-in-object": "^7.27.1", "@babel/plugin-transform-property-literals": "^7.27.1", - "@babel/plugin-transform-regenerator": "^7.28.0", + "@babel/plugin-transform-regenerator": "^7.28.3", "@babel/plugin-transform-regexp-modifiers": "^7.27.1", "@babel/plugin-transform-reserved-words": "^7.27.1", "@babel/plugin-transform-shorthand-properties": "^7.27.1", @@ -1622,9 +1622,9 @@ } }, "node_modules/@babel/runtime": { - "version": "7.28.2", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.2.tgz", - "integrity": "sha512-KHp2IflsnGywDjBWDkR9iEqiWSpc8GIi0lgTT3mOElT0PP1tG26P4tmFI2YvAdzgq9RGyoHZQEIEdZy6Ec5xCA==", + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.3.tgz", + "integrity": "sha512-9uIQ10o0WGdpP6GDhXcdOJPJuDgFtIDtN/9+ArJQ2NAfAmiuhTQdzkaTGR33v43GYS2UrSA0eX2pPPHoFVvpxA==", "license": "MIT", "engines": { "node": ">=6.9.0" @@ -1646,18 +1646,18 @@ } }, "node_modules/@babel/traverse": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.0.tgz", - "integrity": "sha512-mGe7UK5wWyh0bKRfupsUchrQGqvDbZDbKJw+kcRGSmdHVYrv+ltd0pnpDTVpiTqnaBru9iEvA8pz8W46v0Amwg==", + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.3.tgz", + "integrity": "sha512-7w4kZYHneL3A6NP2nxzHvT3HCZ7puDZZjFMqDpBPECub79sTtSO5CGXDkKrTQq8ksAwfD/XI2MRFX23njdDaIQ==", "dev": true, "license": "MIT", "dependencies": { "@babel/code-frame": "^7.27.1", - "@babel/generator": "^7.28.0", + "@babel/generator": "^7.28.3", "@babel/helper-globals": "^7.28.0", - "@babel/parser": "^7.28.0", + "@babel/parser": "^7.28.3", "@babel/template": "^7.27.2", - "@babel/types": "^7.28.0", + "@babel/types": "^7.28.2", "debug": "^4.3.1" }, "engines": { @@ -3148,9 +3148,9 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "24.2.1", - "resolved": "https://registry.npmjs.org/@types/node/-/node-24.2.1.tgz", - "integrity": "sha512-DRh5K+ka5eJic8CjH7td8QpYEV6Zo10gfRkjHCO3weqZHWDtAaSTFtl4+VMqOJ4N5jcuhZ9/l+yy8rVgw7BQeQ==", + "version": "24.3.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.3.0.tgz", + "integrity": "sha512-aPTXCrfwnDLj4VvXrm+UUCQjNEvJgNA8s5F1cvwQU+3KNltTOkBm1j30uNLyqqPNe7gE3KFzImYoZEfLhp4Yow==", "dev": true, "license": "MIT", "dependencies": { @@ -4486,9 +4486,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001734", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001734.tgz", - "integrity": "sha512-uhE1Ye5vgqju6OI71HTQqcBCZrvHugk0MjLak7Q+HfoBgoq5Bi+5YnwjP4fjDgrtYr/l8MVRBvzz9dPD4KyK0A==", + "version": "1.0.30001735", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001735.tgz", + "integrity": "sha512-EV/laoX7Wq2J9TQlyIXRxTJqIw4sxfXS4OYgudGxBYRuTv0q7AM6yMEpU/Vo1I94thg9U6EZ2NfZx9GJq83u7w==", "dev": true, "funding": [ { @@ -5700,9 +5700,9 @@ "license": "MIT" }, "node_modules/electron-to-chromium": { - "version": "1.5.200", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.200.tgz", - "integrity": "sha512-rFCxROw7aOe4uPTfIAx+rXv9cEcGx+buAF4npnhtTqCJk5KDFRnh3+KYj7rdVh6lsFt5/aPs+Irj9rZ33WMA7w==", + "version": "1.5.202", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.202.tgz", + "integrity": "sha512-NxbYjRmiHcHXV1Ws3fWUW+SLb62isauajk45LUJ/HgIOkUA7jLZu/X2Iif+X9FBNK8QkF9Zb4Q2mcwXCcY30mg==", "dev": true, "license": "ISC" }, @@ -7052,9 +7052,9 @@ } }, "node_modules/i18next": { - "version": "25.3.4", - "resolved": "https://registry.npmjs.org/i18next/-/i18next-25.3.4.tgz", - "integrity": "sha512-AHklEYFLiRRxW1Cb6zE9lfnEtYvsydRC8nRS3RSKGX3zCqZ8nLZwMaUsrb80YuccPNv2RNokDL8LkTNnp+6mDw==", + "version": "25.3.6", + "resolved": "https://registry.npmjs.org/i18next/-/i18next-25.3.6.tgz", + "integrity": "sha512-dThZ0CTCM3sUG/qS0ZtQYZQcUI6DtBN8yBHK+SKEqihPcEYmjVWh/YJ4luic73Iq6Uxhp6q7LJJntRK5+1t7jQ==", "funding": [ { "type": "individual", @@ -11170,11 +11170,14 @@ } }, "node_modules/tinyglobby/node_modules/fdir": { - "version": "6.4.6", - "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.6.tgz", - "integrity": "sha512-hiFoqpyZcfNm1yc4u8oWCf9A2c4D3QjCrks3zmoVKVxpQRzmPNar1hUJcBG2RQHvEVGDN+Jm81ZheVLAQMK6+w==", + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", "dev": true, "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, "peerDependencies": { "picomatch": "^3 || ^4" }, @@ -11617,11 +11620,14 @@ "license": "MIT" }, "node_modules/vite/node_modules/fdir": { - "version": "6.4.6", - "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.6.tgz", - "integrity": "sha512-hiFoqpyZcfNm1yc4u8oWCf9A2c4D3QjCrks3zmoVKVxpQRzmPNar1hUJcBG2RQHvEVGDN+Jm81ZheVLAQMK6+w==", + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", "dev": true, "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, "peerDependencies": { "picomatch": "^3 || ^4" }, @@ -11844,9 +11850,9 @@ "license": "BSD-2-Clause" }, "node_modules/webpack": { - "version": "5.101.1", - "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.101.1.tgz", - "integrity": "sha512-rHY3vHXRbkSfhG6fH8zYQdth/BtDgXXuR2pHF++1f/EBkI8zkgM5XWfsC3BvOoW9pr1CvZ1qQCxhCEsbNgT50g==", + "version": "5.101.2", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.101.2.tgz", + "integrity": "sha512-4JLXU0tD6OZNVqlwzm3HGEhAHufSiyv+skb7q0d2367VDMzrU1Q/ZeepvkcHH0rZie6uqEtTQQe0OEOOluH3Mg==", "dev": true, "license": "MIT", "dependencies": { diff --git a/public/v1/js/.gitkeep b/public/v1/js/.gitkeep old mode 100755 new mode 100644 diff --git a/resources/assets/v1/src/locales/fr.json b/resources/assets/v1/src/locales/fr.json index 462e02a54e..630079dacb 100644 --- a/resources/assets/v1/src/locales/fr.json +++ b/resources/assets/v1/src/locales/fr.json @@ -102,7 +102,7 @@ "profile_oauth_client_secret_title": "Secret du client", "profile_oauth_client_secret_expl": "Voici votre nouveau secret de client. C'est la seule fois qu'il sera affich\u00e9, donc ne le perdez pas ! Vous pouvez maintenant utiliser ce secret pour faire des requ\u00eates d'API.", "profile_oauth_confidential": "Confidentiel", - "profile_oauth_confidential_help": "Require the client to authenticate with a secret. Confidential clients can hold credentials in a secure way without exposing them to unauthorized parties. Public applications, such as native desktop or JavaScript SPA applications, are unable to hold secrets securely.", + "profile_oauth_confidential_help": "Exiger que le client s'authentifie avec un secret. Les clients confidentiels peuvent d\u00e9tenir des informations d'identification de mani\u00e8re s\u00e9curis\u00e9e sans les exposer \u00e0 des tiers non autoris\u00e9s. Les applications publiques, telles que les applications de bureau natif ou les SPA JavaScript, ne peuvent pas tenir des secrets en toute s\u00e9curit\u00e9.", "multi_account_warning_unknown": "Selon le type d'op\u00e9ration que vous cr\u00e9ez, le(s) compte(s) source et\/ou de destination des s\u00e9parations suivantes peuvent \u00eatre remplac\u00e9s par celui de la premi\u00e8re s\u00e9paration de l'op\u00e9ration.", "multi_account_warning_withdrawal": "Gardez en t\u00eate que le compte source des s\u00e9parations suivantes peut \u00eatre remplac\u00e9 par celui de la premi\u00e8re s\u00e9paration de la d\u00e9pense.", "multi_account_warning_deposit": "Gardez en t\u00eate que le compte de destination des s\u00e9parations suivantes peut \u00eatre remplac\u00e9 par celui de la premi\u00e8re s\u00e9paration du d\u00e9p\u00f4t.", From 71807b67fcc5748b32a5dee06c008e49391ce2da Mon Sep 17 00:00:00 2001 From: JC5 Date: Fri, 15 Aug 2025 13:43:29 +0200 Subject: [PATCH 17/17] =?UTF-8?q?=F0=9F=A4=96=20Auto=20commit=20for=20rele?= =?UTF-8?q?ase=20'v6.3.0-beta.2'=20on=202025-08-15?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/Repositories/ExchangeRate/ExchangeRateRepository.php | 3 ++- config/firefly.php | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/app/Repositories/ExchangeRate/ExchangeRateRepository.php b/app/Repositories/ExchangeRate/ExchangeRateRepository.php index d2f8544a4a..1fecb036fd 100644 --- a/app/Repositories/ExchangeRate/ExchangeRateRepository.php +++ b/app/Repositories/ExchangeRate/ExchangeRateRepository.php @@ -118,6 +118,7 @@ class ExchangeRateRepository implements ExchangeRateRepositoryInterface, UserGro $this->userGroup->currencyExchangeRates() ->where('from_currency_id', $from->id) ->where('to_currency_id', $to->id) - ->delete(); + ->delete() + ; } } diff --git a/config/firefly.php b/config/firefly.php index c6f9d33788..998250e370 100644 --- a/config/firefly.php +++ b/config/firefly.php @@ -78,8 +78,8 @@ return [ 'running_balance_column' => env('USE_RUNNING_BALANCE', false), // see cer.php for exchange rates feature flag. ], - 'version' => 'develop/2025-08-15', - 'build_time' => 1755257739, + 'version' => '6.3.0-beta.2', + 'build_time' => 1755258109, 'api_version' => '2.1.0', // field is no longer used. 'db_version' => 26,