Compare commits

...

129 Commits

Author SHA1 Message Date
James Cole
020c8ad933 Clean up and expand templates. 2025-08-13 10:15:36 +02:00
James Cole
33ea27b411 Fix template 2025-08-13 08:03:59 +02:00
James Cole
b39e3e5965 Expand template. 2025-08-13 07:56:31 +02:00
James Cole
ba7e504f12 Merge branch 'develop' of github.com:firefly-iii/firefly-iii into develop 2025-08-13 07:54:53 +02:00
James Cole
a732ce2a20 Merge branch 'main' into develop
# Conflicts:
#	.github/workflows/release.yml
2025-08-13 07:54:44 +02:00
James Cole
2b76c4801f Remove steps no longer necessary. 2025-08-13 07:54:15 +02:00
github-actions[bot]
dd09dd59e9 Merge pull request #10752 from firefly-iii/release-1755064372
🤖 Automatically merge the PR into the develop branch.
2025-08-13 07:53:05 +02:00
JC5
d56088d086 🤖 Auto commit for release 'develop' on 2025-08-13 2025-08-13 07:52:52 +02:00
James Cole
f27f00cfa0 Change command. 2025-08-13 07:48:29 +02:00
James Cole
d762136e0b Clean up action. 2025-08-13 07:46:02 +02:00
James Cole
536cd78e29 Templates for new releases. 2025-08-13 07:43:11 +02:00
James Cole
5ef0889f50 Lol lets remove this file. 2025-08-12 20:45:42 +02:00
James Cole
c2ffedb506 Remove link. 2025-08-12 20:44:58 +02:00
github-actions[bot]
fe702a3030 Merge pull request #10751 from firefly-iii/develop
🤖 Automatically merge the PR into the main branch.
2025-08-12 20:27:03 +02:00
github-actions[bot]
2244d366bd Merge pull request #10750 from firefly-iii/release-1755023209
🤖 Automatically merge the PR into the develop branch.
2025-08-12 20:26:57 +02:00
JC5
ac16e0294e 🤖 Auto commit for release 'v6.3.0-beta.1' on 2025-08-12 2025-08-12 20:26:49 +02:00
github-actions[bot]
bf1b0ee3c2 Merge pull request #10749 from firefly-iii/release-1755022724
🤖 Automatically merge the PR into the develop branch.
2025-08-12 20:18:52 +02:00
JC5
ffec4bbfba 🤖 Auto commit for release 'develop' on 2025-08-12 2025-08-12 20:18:44 +02:00
James Cole
ae6e103c09 Fix translation. 2025-08-12 20:13:48 +02:00
James Cole
99d4471f75 Merge branch 'main' into develop 2025-08-11 19:19:22 +02:00
James Cole
7f1fb72298 Merge pull request #10744 from firefly-iii/dependabot/github_actions/github/command-2.0.2
Bump github/command from 2.0.1 to 2.0.2
2025-08-11 09:43:28 +02:00
dependabot[bot]
009c2ed5d9 Bump github/command from 2.0.1 to 2.0.2
Bumps [github/command](https://github.com/github/command) from 2.0.1 to 2.0.2.
- [Release notes](https://github.com/github/command/releases)
- [Commits](https://github.com/github/command/compare/v2.0.1...v2.0.2)

---
updated-dependencies:
- dependency-name: github/command
  dependency-version: 2.0.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-08-11 05:23:27 +00:00
github-actions[bot]
44fc00299c Merge pull request #10742 from firefly-iii/release-1754883400
🤖 Automatically merge the PR into the develop branch.
2025-08-11 05:36:47 +02:00
JC5
774fc4281c 🤖 Auto commit for release 'develop' on 2025-08-11 2025-08-11 05:36:40 +02:00
github-actions[bot]
c47955c069 Merge pull request #10741 from firefly-iii/release-1754821000
🤖 Automatically merge the PR into the develop branch.
2025-08-10 12:16:47 +02:00
JC5
e7569644f7 🤖 Auto commit for release 'develop' on 2025-08-10 2025-08-10 12:16:41 +02:00
James Cole
c567474043 Optimize balance collection. 2025-08-10 12:12:30 +02:00
github-actions[bot]
9f394e92fe Merge pull request #10740 from firefly-iii/release-1754804137
🤖 Automatically merge the PR into the develop branch.
2025-08-10 07:35:46 +02:00
JC5
66befc7e44 🤖 Auto commit for release 'develop' on 2025-08-10 2025-08-10 07:35:37 +02:00
James Cole
c3a28fc698 Fix three years ago. 2025-08-10 07:31:10 +02:00
github-actions[bot]
ef317d5b3c Merge pull request #10739 from firefly-iii/release-1754802161
🤖 Automatically merge the PR into the develop branch.
2025-08-10 07:02:58 +02:00
JC5
152301f9ee 🤖 Auto commit for release 'develop' on 2025-08-10 2025-08-10 07:02:41 +02:00
James Cole
645e9ba1f7 Go back 3 years max. 2025-08-10 06:58:06 +02:00
James Cole
56487c3a33 Fix equation. 2025-08-10 06:54:04 +02:00
James Cole
b8062a915c Fix equation. 2025-08-10 06:53:21 +02:00
James Cole
5780c9512a Optimize convert all balances. 2025-08-10 06:53:08 +02:00
github-actions[bot]
71d39707d9 Merge pull request #10738 from firefly-iii/release-1754800979
🤖 Automatically merge the PR into the develop branch.
2025-08-10 06:43:10 +02:00
JC5
9ccb8ae692 🤖 Auto commit for release 'develop' on 2025-08-10 2025-08-10 06:42:59 +02:00
James Cole
8cd50bb5bd Merge branch 'develop' of github.com:firefly-iii/firefly-iii into develop 2025-08-10 06:38:34 +02:00
James Cole
ae9e1278e5 Expand some timers, fix reports. 2025-08-10 06:38:23 +02:00
github-actions[bot]
58c03797b2 Merge pull request #10737 from firefly-iii/release-1754766186
🤖 Automatically merge the PR into the develop branch.
2025-08-09 21:03:19 +02:00
JC5
7db38b4c6c 🤖 Auto commit for release 'develop' on 2025-08-09 2025-08-09 21:03:06 +02:00
James Cole
da6b447e64 Add some extra debug info. 2025-08-09 20:42:49 +02:00
github-actions[bot]
c19ac2b0f3 Merge pull request #10736 from firefly-iii/release-1754764141
🤖 Automatically merge the PR into the develop branch.
2025-08-09 20:29:09 +02:00
JC5
d5ca2171b3 🤖 Auto commit for release 'develop' on 2025-08-09 2025-08-09 20:29:01 +02:00
James Cole
20972cb29f Merge errors. 2025-08-09 20:24:45 +02:00
James Cole
7b714d0866 Add some time logs. 2025-08-09 20:24:15 +02:00
github-actions[bot]
240ae8fa57 Merge pull request #10735 from firefly-iii/release-1754763336
🤖 Automatically merge the PR into the develop branch.
2025-08-09 20:16:11 +02:00
JC5
5a2f6b2652 🤖 Auto commit for release 'develop' on 2025-08-09 2025-08-09 20:15:36 +02:00
James Cole
4196ce31f0 Fix account thing overview. 2025-08-09 20:10:40 +02:00
James Cole
be8ca5db50 Update changelog. 2025-08-09 19:58:59 +02:00
github-actions[bot]
30a417ea3c Merge pull request #10734 from firefly-iii/release-1754750646
🤖 Automatically merge the PR into the develop branch.
2025-08-09 16:44:16 +02:00
JC5
695ed940e0 🤖 Auto commit for release 'develop' on 2025-08-09 2025-08-09 16:44:06 +02:00
James Cole
1353554cf8 Remove expensive call from loop. 2025-08-09 16:38:32 +02:00
James Cole
e1ba2732af Remove non-optimized method. 2025-08-09 16:34:25 +02:00
James Cole
42b57c0e0e Migrate to optimized method. 2025-08-09 16:31:11 +02:00
James Cole
a6072753b2 Remove PR 2025-08-09 16:01:44 +02:00
github-actions[bot]
e92c224c39 Merge pull request #10733 from firefly-iii/release-1754748022
🤖 Automatically merge the PR into the develop branch.
2025-08-09 16:00:30 +02:00
JC5
a3ed7ec8f6 🤖 Auto commit for release 'develop' on 2025-08-09 2025-08-09 16:00:22 +02:00
James Cole
17a2f99dff Order and palce in changelog. 2025-08-09 15:55:29 +02:00
James Cole
c14971543c Update changelog. 2025-08-09 15:43:36 +02:00
James Cole
55f899608d Add multi currency to piggy overview. 2025-08-09 15:33:03 +02:00
github-actions[bot]
83be63f27e Merge pull request #10731 from firefly-iii/release-1754731244
🤖 Automatically merge the PR into the develop branch.
2025-08-09 11:20:53 +02:00
JC5
ed48d190e5 🤖 Auto commit for release 'develop' on 2025-08-09 2025-08-09 11:20:44 +02:00
James Cole
3c3b6615e6 Make message singular / plural 2025-08-09 11:16:46 +02:00
James Cole
e71e5a877b Fix count for overdue bills. 2025-08-09 11:13:03 +02:00
James Cole
b2a65dc660 Small change in text. 2025-08-09 08:48:25 +02:00
James Cole
d66dccd076 Fix bad slack URL 2025-08-09 08:47:22 +02:00
github-actions[bot]
c1128b28f2 Merge pull request #10730 from firefly-iii/release-1754721510
🤖 Automatically merge the PR into the develop branch.
2025-08-09 08:38:39 +02:00
JC5
da8e78c28d 🤖 Auto commit for release 'develop' on 2025-08-09 2025-08-09 08:38:30 +02:00
James Cole
f50aa6b0ce Fix spam whoopsie. 2025-08-09 08:34:18 +02:00
github-actions[bot]
661e4e53e6 Merge pull request #10729 from firefly-iii/release-1754719442
🤖 Automatically merge the PR into the develop branch.
2025-08-09 08:04:11 +02:00
JC5
3eeda4a6aa 🤖 Auto commit for release 'develop' on 2025-08-09 2025-08-09 08:04:02 +02:00
James Cole
4dba9cea21 Fire warnings for bills and expand webhook message cron job (#10696 and #10703 and #6836) 2025-08-09 07:59:38 +02:00
James Cole
6aab5fab05 Fix #9650 2025-08-09 06:59:55 +02:00
github-actions[bot]
4b0597d19a Merge pull request #10728 from firefly-iii/release-1754679825
🤖 Automatically merge the PR into the develop branch.
2025-08-08 21:03:55 +02:00
JC5
92f534bcb3 🤖 Auto commit for release 'develop' on 2025-08-08 2025-08-08 21:03:45 +02:00
James Cole
76e91be4dc Optimize array. 2025-08-08 20:59:24 +02:00
James Cole
deca4fed56 Clean up API and display of transactions. 2025-08-08 20:18:04 +02:00
James Cole
73512b0365 Add a basic singleton to save on queries. 2025-08-08 15:44:15 +02:00
github-actions[bot]
aaffc125e7 Merge pull request #10724 from firefly-iii/release-1754630264
🤖 Automatically merge the PR into the develop branch.
2025-08-08 07:17:53 +02:00
JC5
41a48c39a0 🤖 Auto commit for release 'develop' on 2025-08-08 2025-08-08 07:17:44 +02:00
James Cole
2d96bd84b5 Fix #9640 2025-08-08 06:43:05 +02:00
James Cole
ad1c1d2254 Fix #10071 2025-08-08 06:42:54 +02:00
github-actions[bot]
813206766d Merge pull request #10723 from firefly-iii/release-1754591858
🤖 Automatically merge the PR into the develop branch.
2025-08-07 20:37:46 +02:00
JC5
bb25d4a82a 🤖 Auto commit for release 'develop' on 2025-08-07 2025-08-07 20:37:38 +02:00
James Cole
f3b78beecc Fix another null 2025-08-07 20:33:24 +02:00
github-actions[bot]
64073768fe Merge pull request #10722 from firefly-iii/release-1754590638
🤖 Automatically merge the PR into the develop branch.
2025-08-07 20:17:25 +02:00
JC5
fe6dd0f901 🤖 Auto commit for release 'develop' on 2025-08-07 2025-08-07 20:17:19 +02:00
James Cole
aac8d11ff6 Merge branch 'develop' of github.com:firefly-iii/firefly-iii into develop 2025-08-07 20:13:35 +02:00
James Cole
afa99a35b5 Fix NULLs 2025-08-07 20:13:29 +02:00
James Cole
e9cb0a51d7 Catch NULL. 2025-08-07 20:11:46 +02:00
github-actions[bot]
9fbcccfd02 Merge pull request #10721 from firefly-iii/release-1754589876
🤖 Automatically merge the PR into the develop branch.
2025-08-07 20:04:43 +02:00
JC5
468c9c9d56 🤖 Auto commit for release 'develop' on 2025-08-07 2025-08-07 20:04:36 +02:00
James Cole
f76b27a73d Fix #10565 and fix #10600 2025-08-07 19:59:02 +02:00
James Cole
579fe81616 Fix #10656 2025-08-07 19:52:12 +02:00
James Cole
ec9ba53690 Fix #10678 2025-08-07 19:48:00 +02:00
James Cole
85337c53d4 Fix currency collection. 2025-08-07 19:37:36 +02:00
James Cole
eb6d585bb2 Fix a variety of code. 2025-08-07 19:09:25 +02:00
James Cole
378ffbc609 Fix #10720 2025-08-07 17:55:25 +02:00
James Cole
3b3c8e5bcd Add object group to various items. 2025-08-07 07:46:49 +02:00
github-actions[bot]
75cbdb6a57 Merge pull request #10719 from firefly-iii/release-1754540665
🤖 Automatically merge the PR into the develop branch.
2025-08-07 06:24:32 +02:00
JC5
cdaff0d983 🤖 Auto commit for release 'develop' on 2025-08-07 2025-08-07 06:24:25 +02:00
James Cole
dda3863889 Fix account_id key. 2025-08-07 06:20:36 +02:00
github-actions[bot]
57dc423b3f Merge pull request #10718 from firefly-iii/release-1754540258
🤖 Automatically merge the PR into the develop branch.
2025-08-07 06:17:51 +02:00
JC5
c0570bc3b2 🤖 Auto commit for release 'develop' on 2025-08-07 2025-08-07 06:17:38 +02:00
James Cole
65110d1666 Merge branch 'develop' of github.com:firefly-iii/firefly-iii into develop 2025-08-07 06:12:02 +02:00
James Cole
5a10b29402 Set to zero instead of null. 2025-08-07 06:11:56 +02:00
github-actions[bot]
028544ca2e Merge pull request #10717 from firefly-iii/release-1754539541
🤖 Automatically merge the PR into the develop branch.
2025-08-07 06:05:53 +02:00
JC5
af46729372 🤖 Auto commit for release 'develop' on 2025-08-07 2025-08-07 06:05:41 +02:00
James Cole
4c7789a668 Add missing access rights. 2025-08-07 06:01:14 +02:00
James Cole
292908048c Add enrichment to recurring 2025-08-07 05:59:26 +02:00
James Cole
e300314e05 Catch NULL. 2025-08-07 05:56:31 +02:00
James Cole
4f1f360346 Add primary to array. 2025-08-07 05:55:15 +02:00
James Cole
bff856aeff Rename method, remove 0. 2025-08-07 05:52:56 +02:00
James Cole
7f5a1bda8d Catch null 2025-08-06 20:53:30 +02:00
github-actions[bot]
b506281bd6 Merge pull request #10716 from firefly-iii/release-1754505595
🤖 Automatically merge the PR into the develop branch.
2025-08-06 20:40:02 +02:00
JC5
dfe9b3e787 🤖 Auto commit for release 'develop' on 2025-08-06 2025-08-06 20:39:55 +02:00
James Cole
2428a2a7c5 Expand API test code. 2025-08-06 20:27:59 +02:00
James Cole
0e8f608e00 Optimize available budgets. 2025-08-06 20:23:58 +02:00
James Cole
70071767ab Collect account balances, optimized. 2025-08-06 20:15:02 +02:00
James Cole
0ad6beb66c Optimize recurrence enrichment. 2025-08-06 15:35:29 +02:00
James Cole
1197f65589 Start work on recurring transaction enrichment. 2025-08-06 10:46:23 +02:00
James Cole
7bbf2dcc6f Add enrichment. 2025-08-06 09:18:29 +02:00
James Cole
d4ab69ebe6 Expand piggy bank enrichment. 2025-08-06 09:15:54 +02:00
James Cole
c8c552602e Expand piggy bank events. 2025-08-05 19:49:13 +02:00
James Cole
1921a8050b Fix #10709 2025-08-05 18:56:31 +02:00
James Cole
f488feda93 Merge branch 'develop' of github.com:firefly-iii/firefly-iii into develop 2025-08-05 17:57:57 +02:00
James Cole
d90c033b83 Fix #10708 2025-08-05 17:57:52 +02:00
156 changed files with 4150 additions and 2094 deletions

20
.github/release-notes/alpha.md vendored Normal file
View File

@@ -0,0 +1,20 @@
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 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 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.

20
.github/release-notes/beta.md vendored Normal file
View File

@@ -0,0 +1,20 @@
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 beta 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 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 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.

20
.github/release-notes/branch.md vendored Normal file
View File

@@ -0,0 +1,20 @@
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.

20
.github/release-notes/develop.md vendored Normal file
View File

@@ -0,0 +1,20 @@
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 possible.
## 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
* 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. Thank you for your consideration.

16
.github/release-notes/release.md vendored Normal file
View File

@@ -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. Thank you for your consideration.

View File

@@ -13,7 +13,7 @@ jobs:
close_duplicates:
runs-on: ubuntu-latest
steps:
- uses: github/command@v2.0.1
- uses: github/command@v2.0.2
id: command
with:
allowed_contexts: "issue"

View File

@@ -19,7 +19,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v5
with:
fetch-depth: 0
- name: Import GPG key
@@ -233,122 +233,15 @@ jobs:
git push
env:
version: ${{ github.event_name == 'schedule' && 'develop' || github.event.inputs.version }}
- name: Extract changelog
id: extract-changelog
- name: Generate release description
id: release-description
uses: JC5/firefly-iii-dev@main
with:
action: 'ff3:extract-changelog'
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
env:
version: ${{ github.event_name == 'schedule' && 'develop' || github.event.inputs.version }}
- name: Merge all into working branch
run: |
MERGE_INTO=develop

View File

@@ -31,6 +31,7 @@ use FireflyIII\Exceptions\FireflyException;
use FireflyIII\Models\Account;
use FireflyIII\Repositories\Account\AccountRepositoryInterface;
use FireflyIII\Support\Debug\Timer;
use FireflyIII\Support\Facades\Amount;
use FireflyIII\Support\Facades\Steam;
use FireflyIII\Support\Http\Api\AccountFilter;
use FireflyIII\User;
@@ -79,17 +80,20 @@ class AccountController extends Controller
*/
public function accounts(AutocompleteRequest $request): JsonResponse
{
$data = $request->getData();
$types = $data['types'];
$query = $data['query'];
$date = $data['date'] ?? today(config('app.timezone'));
$return = [];
Timer::start(sprintf('AC accounts "%s"', $query));
$result = $this->repository->searchAccount((string) $query, $types, $this->parameters->get('limit'));
$data = $request->getData();
$types = $data['types'];
$query = $data['query'];
$date = $data['date'] ?? today(config('app.timezone'));
$return = [];
$timer = Timer::getInstance();
$timer->start(sprintf('AC accounts "%s"', $query));
$result = $this->repository->searchAccount((string) $query, $types, $this->parameters->get('limit'));
// set date to subday + end-of-day for account balance. so it is at $date 23:59:59
$date->endOfDay();
$allBalances = Steam::accountsBalancesOptimized($result, $date, $this->primaryCurrency, $this->convertToPrimary);
/** @var Account $account */
foreach ($result as $account) {
$nameWithBalance = $account->name;
@@ -98,15 +102,11 @@ class AccountController extends Controller
if (in_array($account->accountType->type, $this->balanceTypes, true)) {
// this one is correct.
Log::debug(sprintf('accounts: Call finalAccountBalance with date/time "%s"', $date->toIso8601String()));
$balance = Steam::finalAccountBalance($account, $date);
$balance = $allBalances[$account->id] ?? [];
$key = $this->convertToPrimary && $currency->id !== $this->primaryCurrency->id ? 'pc_balance' : 'balance';
$useCurrency = $this->convertToPrimary && $currency->id !== $this->primaryCurrency->id ? $this->primaryCurrency : $currency;
$amount = $balance[$key] ?? '0';
$nameWithBalance = sprintf(
'%s (%s)',
$account->name,
app('amount')->formatAnything($useCurrency, $amount, false)
);
$nameWithBalance = sprintf('%s (%s)', $account->name, Amount::formatAnything($useCurrency, $amount, false));
}
$return[] = [
@@ -138,7 +138,7 @@ class AccountController extends Controller
return $posA - $posB;
}
);
Timer::stop(sprintf('AC accounts "%s"', $query));
$timer->stop(sprintf('AC accounts "%s"', $query));
return response()->api($return);
}

View File

@@ -0,0 +1,97 @@
<?php
declare(strict_types=1);
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\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;
use FireflyIII\Support\Http\Api\CollectsAccountsFromFilter;
use Illuminate\Http\JsonResponse;
/**
* Class BalanceController
*/
class BalanceController extends Controller
{
use CleansChartData;
use CollectsAccountsFromFilter;
private ChartData $chartData;
private GroupCollectorInterface $collector;
private AccountRepositoryInterface $repository;
// private TransactionCurrency $default;
public function __construct()
{
parent::__construct();
$this->middleware(
function ($request, $next) {
$this->repository = app(AccountRepositoryInterface::class);
$this->collector = app(GroupCollectorInterface::class);
$userGroup = $this->validateUserGroup($request);
$this->repository->setUserGroup($userGroup);
$this->collector->setUserGroup($userGroup);
$this->chartData = new ChartData();
// $this->default = app('amount')->getPrimaryCurrency();
return $next($request);
}
);
}
/**
* The code is practically a duplicate of ReportController::operations.
*
* Currency is up to the account/transactions in question, but conversion to the default
* currency is possible.
*
* If the transaction being processed is already in native currency OR if the
* foreign amount is in the native currency, the amount will not be converted.
*
* @throws FireflyException
*/
public function balance(ChartRequest $request): JsonResponse
{
$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'])
->withAccountInformation()
->setXorAccounts($accounts)
->setTypes([TransactionTypeEnum::WITHDRAWAL->value, TransactionTypeEnum::DEPOSIT->value, TransactionTypeEnum::RECONCILIATION->value, TransactionTypeEnum::TRANSFER->value])
;
$journals = $this->collector->getExtractedJournals();
$object = new AccountBalanceGrouped();
$object->setPreferredRange($queryParameters['period']);
$object->setPrimary($primary);
$object->setAccounts($accounts);
$object->setJournals($journals);
$object->setStart($queryParameters['start']);
$object->setEnd($queryParameters['end']);
$object->groupByCurrencyAndPeriod();
$data = $object->convertToChartData();
foreach ($data as $entry) {
$this->chartData->add($entry);
}
return response()->json($this->chartData->render());
}
}

View File

@@ -30,6 +30,7 @@ use FireflyIII\Helpers\Collector\GroupCollectorInterface;
use FireflyIII\Models\Account;
use FireflyIII\Repositories\Account\AccountRepositoryInterface;
use FireflyIII\Support\Http\Api\TransactionFilter;
use FireflyIII\Support\JsonApi\Enrichments\PiggyBankEnrichment;
use FireflyIII\Support\JsonApi\Enrichments\TransactionGroupEnrichment;
use FireflyIII\Transformers\AttachmentTransformer;
use FireflyIII\Transformers\PiggyBankTransformer;
@@ -117,6 +118,13 @@ class ListController extends Controller
$count = $collection->count();
$piggyBanks = $collection->slice(($this->parameters->get('page') - 1) * $pageSize, $pageSize);
// enrich
/** @var User $admin */
$admin = auth()->user();
$enrichment = new PiggyBankEnrichment();
$enrichment->setUser($admin);
$piggyBanks = $enrichment->enrich($piggyBanks);
// make paginator:
$paginator = new LengthAwarePaginator($piggyBanks, $count, $pageSize, $this->parameters->get('page'));
$paginator->setPath(route('api.v1.accounts.piggy-banks', [$account->id]).$this->buildParams());
@@ -125,7 +133,7 @@ class ListController extends Controller
$transformer = app(PiggyBankTransformer::class);
$transformer->setParameters($this->parameters);
$resource = new FractalCollection($piggyBanks, $transformer, 'piggy_banks');
$resource = new FractalCollection($piggyBanks, $transformer, 'piggy-banks');
$resource->setPaginator(new IlluminatePaginatorAdapter($paginator));
return response()->json($manager->createData($resource)->toArray())->header('Content-Type', self::CONTENT_TYPE);

View File

@@ -28,7 +28,9 @@ use FireflyIII\Api\V1\Controllers\Controller;
use FireflyIII\Api\V1\Requests\Models\Category\StoreRequest;
use FireflyIII\Exceptions\FireflyException;
use FireflyIII\Repositories\Category\CategoryRepositoryInterface;
use FireflyIII\Support\JsonApi\Enrichments\CategoryEnrichment;
use FireflyIII\Transformers\CategoryTransformer;
use FireflyIII\User;
use Illuminate\Http\JsonResponse;
use League\Fractal\Resource\Item;
@@ -72,6 +74,15 @@ class StoreController extends Controller
$transformer = app(CategoryTransformer::class);
$transformer->setParameters($this->parameters);
// enrich
/** @var User $admin */
$admin = auth()->user();
$enrichment = new CategoryEnrichment();
$enrichment->setUser($admin);
$enrichment->setStart($this->parameters->get('start'));
$enrichment->setEnd($this->parameters->get('end'));
$category = $enrichment->enrichSingle($category);
$resource = new Item($category, $transformer, 'categories');
return response()->json($manager->createData($resource)->toArray())->header('Content-Type', self::CONTENT_TYPE);

View File

@@ -28,7 +28,9 @@ use FireflyIII\Api\V1\Controllers\Controller;
use FireflyIII\Api\V1\Requests\Models\Category\UpdateRequest;
use FireflyIII\Models\Category;
use FireflyIII\Repositories\Category\CategoryRepositoryInterface;
use FireflyIII\Support\JsonApi\Enrichments\CategoryEnrichment;
use FireflyIII\Transformers\CategoryTransformer;
use FireflyIII\User;
use Illuminate\Http\JsonResponse;
use League\Fractal\Resource\Item;
@@ -71,6 +73,15 @@ class UpdateController extends Controller
$transformer = app(CategoryTransformer::class);
$transformer->setParameters($this->parameters);
// enrich
/** @var User $admin */
$admin = auth()->user();
$enrichment = new CategoryEnrichment();
$enrichment->setUser($admin);
$enrichment->setStart($this->parameters->get('start'));
$enrichment->setEnd($this->parameters->get('end'));
$category = $enrichment->enrichSingle($category);
$resource = new Item($category, $transformer, 'categories');
return response()->json($manager->createData($resource)->toArray())->header('Content-Type', self::CONTENT_TYPE);

View File

@@ -25,6 +25,7 @@ declare(strict_types=1);
namespace FireflyIII\Api\V1\Controllers\Models\CurrencyExchangeRate;
use FireflyIII\Api\V1\Controllers\Controller;
use FireflyIII\Enums\UserRoleEnum;
use FireflyIII\Repositories\ExchangeRate\ExchangeRateRepositoryInterface;
use FireflyIII\Support\Http\Api\ValidatesUserGroupTrait;
use FireflyIII\Transformers\ExchangeRateTransformer;
@@ -39,7 +40,7 @@ class IndexController extends Controller
use ValidatesUserGroupTrait;
public const string RESOURCE_KEY = 'currency_exchange_rates';
protected array $acceptedRoles = [UserRoleEnum::OWNER];
private ExchangeRateRepositoryInterface $repository;
public function __construct()

View File

@@ -25,6 +25,7 @@ declare(strict_types=1);
namespace FireflyIII\Api\V1\Controllers\Models\CurrencyExchangeRate;
use FireflyIII\Api\V1\Controllers\Controller;
use FireflyIII\Enums\UserRoleEnum;
use FireflyIII\Models\CurrencyExchangeRate;
use FireflyIII\Models\TransactionCurrency;
use FireflyIII\Repositories\ExchangeRate\ExchangeRateRepositoryInterface;
@@ -41,7 +42,7 @@ class ShowController extends Controller
use ValidatesUserGroupTrait;
public const string RESOURCE_KEY = 'exchange-rates';
protected array $acceptedRoles = [UserRoleEnum::OWNER];
private ExchangeRateRepositoryInterface $repository;
public function __construct()

View File

@@ -24,6 +24,7 @@ declare(strict_types=1);
namespace FireflyIII\Api\V1\Controllers\Models\CurrencyExchangeRate;
use FireflyIII\Enums\UserRoleEnum;
use FireflyIII\Models\CurrencyExchangeRate;
use FireflyIII\Api\V1\Requests\Models\CurrencyExchangeRate\StoreRequest;
use FireflyIII\Api\V1\Controllers\Controller;
@@ -37,7 +38,7 @@ class StoreController extends Controller
use ValidatesUserGroupTrait;
public const string RESOURCE_KEY = 'exchange-rates';
protected array $acceptedRoles = [UserRoleEnum::OWNER];
private ExchangeRateRepositoryInterface $repository;
public function __construct()

View File

@@ -26,6 +26,7 @@ namespace FireflyIII\Api\V1\Controllers\Models\CurrencyExchangeRate;
use FireflyIII\Api\V1\Requests\Models\CurrencyExchangeRate\UpdateRequest;
use FireflyIII\Api\V1\Controllers\Controller;
use FireflyIII\Enums\UserRoleEnum;
use FireflyIII\Models\CurrencyExchangeRate;
use FireflyIII\Repositories\ExchangeRate\ExchangeRateRepositoryInterface;
use FireflyIII\Support\Http\Api\ValidatesUserGroupTrait;
@@ -37,7 +38,7 @@ class UpdateController extends Controller
use ValidatesUserGroupTrait;
public const string RESOURCE_KEY = 'exchange-rates';
protected array $acceptedRoles = [UserRoleEnum::OWNER];
private ExchangeRateRepositoryInterface $repository;
public function __construct()

View File

@@ -28,6 +28,7 @@ use FireflyIII\Api\V1\Controllers\Controller;
use FireflyIII\Exceptions\FireflyException;
use FireflyIII\Models\ObjectGroup;
use FireflyIII\Repositories\ObjectGroup\ObjectGroupRepositoryInterface;
use FireflyIII\Support\JsonApi\Enrichments\PiggyBankEnrichment;
use FireflyIII\Support\JsonApi\Enrichments\SubscriptionEnrichment;
use FireflyIII\Transformers\BillTransformer;
use FireflyIII\Transformers\PiggyBankTransformer;
@@ -124,6 +125,13 @@ class ListController extends Controller
$count = $collection->count();
$piggyBanks = $collection->slice(($this->parameters->get('page') - 1) * $pageSize, $pageSize);
// enrich
/** @var User $admin */
$admin = auth()->user();
$enrichment = new PiggyBankEnrichment();
$enrichment->setUser($admin);
$piggyBanks = $enrichment->enrich($piggyBanks);
// make paginator:
$paginator = new LengthAwarePaginator($piggyBanks, $count, $pageSize, $this->parameters->get('page'));
$paginator->setPath(route('api.v1.object-groups.piggy-banks', [$objectGroup->id]).$this->buildParams());
@@ -132,7 +140,7 @@ class ListController extends Controller
$transformer = app(PiggyBankTransformer::class);
$transformer->setParameters($this->parameters);
$resource = new FractalCollection($piggyBanks, $transformer, 'piggy_banks');
$resource = new FractalCollection($piggyBanks, $transformer, 'piggy-banks');
$resource->setPaginator(new IlluminatePaginatorAdapter($paginator));
return response()->json($manager->createData($resource)->toArray())->header('Content-Type', self::CONTENT_TYPE);

View File

@@ -29,6 +29,7 @@ use FireflyIII\Exceptions\FireflyException;
use FireflyIII\Models\PiggyBank;
use FireflyIII\Repositories\PiggyBank\PiggyBankRepositoryInterface;
use FireflyIII\Support\JsonApi\Enrichments\AccountEnrichment;
use FireflyIII\Support\JsonApi\Enrichments\PiggyBankEventEnrichment;
use FireflyIII\Transformers\AccountTransformer;
use FireflyIII\Transformers\AttachmentTransformer;
use FireflyIII\Transformers\PiggyBankEventTransformer;
@@ -148,6 +149,13 @@ class ListController extends Controller
$count = $collection->count();
$events = $collection->slice(($this->parameters->get('page') - 1) * $pageSize, $pageSize);
// enrich
/** @var User $admin */
$admin = auth()->user();
$enrichment = new PiggyBankEventEnrichment();
$enrichment->setUser($admin);
$events = $enrichment->enrich($events);
// make paginator:
$paginator = new LengthAwarePaginator($events, $count, $pageSize, $this->parameters->get('page'));
$paginator->setPath(route('api.v1.piggy-banks.events', [$piggyBank->id]).$this->buildParams());
@@ -156,7 +164,7 @@ class ListController extends Controller
$transformer = app(PiggyBankEventTransformer::class);
$transformer->setParameters($this->parameters);
$resource = new FractalCollection($events, $transformer, 'piggy_bank_events');
$resource = new FractalCollection($events, $transformer, sprintf('piggy-banks/%d/events', $piggyBank->id));
$resource->setPaginator(new IlluminatePaginatorAdapter($paginator));
return response()->json($manager->createData($resource)->toArray())->header('Content-Type', self::CONTENT_TYPE);

View File

@@ -28,7 +28,9 @@ use FireflyIII\Api\V1\Controllers\Controller;
use FireflyIII\Exceptions\FireflyException;
use FireflyIII\Models\PiggyBank;
use FireflyIII\Repositories\PiggyBank\PiggyBankRepositoryInterface;
use FireflyIII\Support\JsonApi\Enrichments\PiggyBankEnrichment;
use FireflyIII\Transformers\PiggyBankTransformer;
use FireflyIII\User;
use Illuminate\Http\JsonResponse;
use Illuminate\Pagination\LengthAwarePaginator;
use League\Fractal\Pagination\IlluminatePaginatorAdapter;
@@ -77,6 +79,13 @@ class ShowController extends Controller
$count = $collection->count();
$piggyBanks = $collection->slice(($this->parameters->get('page') - 1) * $pageSize, $pageSize);
// enrich
/** @var User $admin */
$admin = auth()->user();
$enrichment = new PiggyBankEnrichment();
$enrichment->setUser($admin);
$piggyBanks = $enrichment->enrich($piggyBanks);
// make paginator:
$paginator = new LengthAwarePaginator($piggyBanks, $count, $pageSize, $this->parameters->get('page'));
$paginator->setPath(route('api.v1.piggy-banks.index').$this->buildParams());
@@ -85,7 +94,7 @@ class ShowController extends Controller
$transformer = app(PiggyBankTransformer::class);
$transformer->setParameters($this->parameters);
$resource = new FractalCollection($piggyBanks, $transformer, 'piggy_banks');
$resource = new FractalCollection($piggyBanks, $transformer, 'piggy-banks');
$resource->setPaginator(new IlluminatePaginatorAdapter($paginator));
return response()->json($manager->createData($resource)->toArray())->header('Content-Type', self::CONTENT_TYPE);
@@ -101,11 +110,19 @@ class ShowController extends Controller
{
$manager = $this->getManager();
// enrich
/** @var User $admin */
$admin = auth()->user();
$enrichment = new PiggyBankEnrichment();
$enrichment->setUser($admin);
$piggyBank = $enrichment->enrichSingle($piggyBank);
/** @var PiggyBankTransformer $transformer */
$transformer = app(PiggyBankTransformer::class);
$transformer->setParameters($this->parameters);
$resource = new Item($piggyBank, $transformer, 'piggy_banks');
$resource = new Item($piggyBank, $transformer, 'piggy-banks');
return response()->json($manager->createData($resource)->toArray())->header('Content-Type', self::CONTENT_TYPE);
}

View File

@@ -28,7 +28,9 @@ use FireflyIII\Api\V1\Controllers\Controller;
use FireflyIII\Api\V1\Requests\Models\PiggyBank\StoreRequest;
use FireflyIII\Exceptions\FireflyException;
use FireflyIII\Repositories\PiggyBank\PiggyBankRepositoryInterface;
use FireflyIII\Support\JsonApi\Enrichments\PiggyBankEnrichment;
use FireflyIII\Transformers\PiggyBankTransformer;
use FireflyIII\User;
use Illuminate\Http\JsonResponse;
use League\Fractal\Resource\Item;
@@ -68,6 +70,13 @@ class StoreController extends Controller
$piggyBank = $this->repository->store($request->getAll());
$manager = $this->getManager();
// enrich
/** @var User $admin */
$admin = auth()->user();
$enrichment = new PiggyBankEnrichment();
$enrichment->setUser($admin);
$piggyBank = $enrichment->enrichSingle($piggyBank);
/** @var PiggyBankTransformer $transformer */
$transformer = app(PiggyBankTransformer::class);
$transformer->setParameters($this->parameters);

View File

@@ -28,7 +28,9 @@ use FireflyIII\Api\V1\Controllers\Controller;
use FireflyIII\Api\V1\Requests\Models\PiggyBank\UpdateRequest;
use FireflyIII\Models\PiggyBank;
use FireflyIII\Repositories\PiggyBank\PiggyBankRepositoryInterface;
use FireflyIII\Support\JsonApi\Enrichments\PiggyBankEnrichment;
use FireflyIII\Transformers\PiggyBankTransformer;
use FireflyIII\User;
use Illuminate\Http\JsonResponse;
use League\Fractal\Resource\Item;
@@ -70,13 +72,20 @@ class UpdateController extends Controller
$this->repository->setCurrentAmount($piggyBank, $data['current_amount']);
}
// enrich
/** @var User $admin */
$admin = auth()->user();
$enrichment = new PiggyBankEnrichment();
$enrichment->setUser($admin);
$piggyBank = $enrichment->enrichSingle($piggyBank);
$manager = $this->getManager();
/** @var PiggyBankTransformer $transformer */
$transformer = app(PiggyBankTransformer::class);
$transformer->setParameters($this->parameters);
$resource = new Item($piggyBank, $transformer, 'piggy_banks');
$resource = new Item($piggyBank, $transformer, 'piggy-banks');
return response()->json($manager->createData($resource)->toArray())->header('Content-Type', self::CONTENT_TYPE);
}

View File

@@ -28,7 +28,9 @@ use FireflyIII\Api\V1\Controllers\Controller;
use FireflyIII\Exceptions\FireflyException;
use FireflyIII\Models\Recurrence;
use FireflyIII\Repositories\Recurring\RecurringRepositoryInterface;
use FireflyIII\Support\JsonApi\Enrichments\RecurringEnrichment;
use FireflyIII\Transformers\RecurrenceTransformer;
use FireflyIII\User;
use Illuminate\Http\JsonResponse;
use Illuminate\Pagination\LengthAwarePaginator;
use League\Fractal\Pagination\IlluminatePaginatorAdapter;
@@ -76,17 +78,24 @@ class ShowController extends Controller
// get list of budgets. Count it and split it.
$collection = $this->repository->get();
$count = $collection->count();
$piggyBanks = $collection->slice(($this->parameters->get('page') - 1) * $pageSize, $pageSize);
$recurrences = $collection->slice(($this->parameters->get('page') - 1) * $pageSize, $pageSize);
// enrich
/** @var User $admin */
$admin = auth()->user();
$enrichment = new RecurringEnrichment();
$enrichment->setUser($admin);
$recurrences = $enrichment->enrich($recurrences);
// make paginator:
$paginator = new LengthAwarePaginator($piggyBanks, $count, $pageSize, $this->parameters->get('page'));
$paginator = new LengthAwarePaginator($recurrences, $count, $pageSize, $this->parameters->get('page'));
$paginator->setPath(route('api.v1.recurrences.index').$this->buildParams());
/** @var RecurrenceTransformer $transformer */
$transformer = app(RecurrenceTransformer::class);
$transformer->setParameters($this->parameters);
$resource = new FractalCollection($piggyBanks, $transformer, 'recurrences');
$resource = new FractalCollection($recurrences, $transformer, 'recurrences');
$resource->setPaginator(new IlluminatePaginatorAdapter($paginator));
return response()->json($manager->createData($resource)->toArray())->header('Content-Type', self::CONTENT_TYPE);
@@ -102,6 +111,13 @@ class ShowController extends Controller
{
$manager = $this->getManager();
// enrich
/** @var User $admin */
$admin = auth()->user();
$enrichment = new RecurringEnrichment();
$enrichment->setUser($admin);
$recurrence = $enrichment->enrichSingle($recurrence);
/** @var RecurrenceTransformer $transformer */
$transformer = app(RecurrenceTransformer::class);
$transformer->setParameters($this->parameters);

View File

@@ -28,7 +28,9 @@ use FireflyIII\Api\V1\Controllers\Controller;
use FireflyIII\Api\V1\Requests\Models\Recurrence\StoreRequest;
use FireflyIII\Exceptions\FireflyException;
use FireflyIII\Repositories\Recurring\RecurringRepositoryInterface;
use FireflyIII\Support\JsonApi\Enrichments\RecurringEnrichment;
use FireflyIII\Transformers\RecurrenceTransformer;
use FireflyIII\User;
use Illuminate\Http\JsonResponse;
use League\Fractal\Resource\Item;
@@ -69,6 +71,13 @@ class StoreController extends Controller
$recurrence = $this->repository->store($data);
$manager = $this->getManager();
// enrich
/** @var User $admin */
$admin = auth()->user();
$enrichment = new RecurringEnrichment();
$enrichment->setUser($admin);
$recurrence = $enrichment->enrichSingle($recurrence);
/** @var RecurrenceTransformer $transformer */
$transformer = app(RecurrenceTransformer::class);
$transformer->setParameters($this->parameters);

View File

@@ -28,7 +28,9 @@ use FireflyIII\Api\V1\Controllers\Controller;
use FireflyIII\Api\V1\Requests\Models\Recurrence\UpdateRequest;
use FireflyIII\Models\Recurrence;
use FireflyIII\Repositories\Recurring\RecurringRepositoryInterface;
use FireflyIII\Support\JsonApi\Enrichments\RecurringEnrichment;
use FireflyIII\Transformers\RecurrenceTransformer;
use FireflyIII\User;
use Illuminate\Http\JsonResponse;
use League\Fractal\Resource\Item;
@@ -67,6 +69,13 @@ class UpdateController extends Controller
$recurrence = $this->repository->update($recurrence, $data);
$manager = $this->getManager();
// enrich
/** @var User $admin */
$admin = auth()->user();
$enrichment = new RecurringEnrichment();
$enrichment->setUser($admin);
$recurrence = $enrichment->enrichSingle($recurrence);
/** @var RecurrenceTransformer $transformer */
$transformer = app(RecurrenceTransformer::class);
$transformer->setParameters($this->parameters);

View File

@@ -29,6 +29,7 @@ use FireflyIII\Exceptions\FireflyException;
use FireflyIII\Models\TransactionGroup;
use FireflyIII\Models\TransactionJournal;
use FireflyIII\Repositories\Journal\JournalAPIRepositoryInterface;
use FireflyIII\Support\JsonApi\Enrichments\PiggyBankEventEnrichment;
use FireflyIII\Transformers\AttachmentTransformer;
use FireflyIII\Transformers\PiggyBankEventTransformer;
use FireflyIII\Transformers\TransactionLinkTransformer;
@@ -113,6 +114,14 @@ class ListController extends Controller
}
$count = $collection->count();
$events = $collection->slice(($this->parameters->get('page') - 1) * $pageSize, $pageSize);
// enrich
/** @var User $admin */
$admin = auth()->user();
$enrichment = new PiggyBankEventEnrichment();
$enrichment->setUser($admin);
$events = $enrichment->enrich($events);
// make paginator:
$paginator = new LengthAwarePaginator($events, $count, $pageSize, $this->parameters->get('page'));
$paginator->setPath(route('api.v1.transactions.piggy-bank-events', [$transactionGroup->id]).$this->buildParams());

View File

@@ -44,6 +44,7 @@ use FireflyIII\Support\Http\Api\AccountFilter;
use FireflyIII\Support\Http\Api\TransactionFilter;
use FireflyIII\Support\JsonApi\Enrichments\AccountEnrichment;
use FireflyIII\Support\JsonApi\Enrichments\BudgetLimitEnrichment;
use FireflyIII\Support\JsonApi\Enrichments\RecurringEnrichment;
use FireflyIII\Support\JsonApi\Enrichments\SubscriptionEnrichment;
use FireflyIII\Support\JsonApi\Enrichments\TransactionGroupEnrichment;
use FireflyIII\Transformers\AccountTransformer;
@@ -276,17 +277,24 @@ class ListController extends Controller
}
);
$count = $collection->count();
$piggyBanks = $collection->slice(($this->parameters->get('page') - 1) * $pageSize, $pageSize);
$recurrences = $collection->slice(($this->parameters->get('page') - 1) * $pageSize, $pageSize);
// enrich
/** @var User $admin */
$admin = auth()->user();
$enrichment = new RecurringEnrichment();
$enrichment->setUser($admin);
$recurrences = $enrichment->enrich($recurrences);
// make paginator:
$paginator = new LengthAwarePaginator($piggyBanks, $count, $pageSize, $this->parameters->get('page'));
$paginator = new LengthAwarePaginator($recurrences, $count, $pageSize, $this->parameters->get('page'));
$paginator->setPath(route('api.v1.currencies.recurrences', [$currency->code]).$this->buildParams());
/** @var RecurrenceTransformer $transformer */
$transformer = app(RecurrenceTransformer::class);
$transformer->setParameters($this->parameters);
$resource = new FractalCollection($piggyBanks, $transformer, 'recurrences');
$resource = new FractalCollection($recurrences, $transformer, 'recurrences');
$resource->setPaginator(new IlluminatePaginatorAdapter($paginator));
return response()->json($manager->createData($resource)->toArray())->header('Content-Type', self::CONTENT_TYPE);

View File

@@ -248,10 +248,10 @@ class BasicController extends Controller
'currency_code' => $currency->code,
'currency_symbol' => $currency->symbol,
'currency_decimal_places' => $currency->decimal_places,
'value_parsed' => app('amount')->formatAnything($currency, $sums[$currencyId]['sum'] ?? '0', false),
'value_parsed' => Amount::formatAnything($currency, $sums[$currencyId]['sum'] ?? '0', false),
'local_icon' => 'balance-scale',
'sub_title' => app('amount')->formatAnything($currency, $expenses[$currencyId]['sum'] ?? '0', false)
.' + '.app('amount')->formatAnything($currency, $incomes[$currencyId]['sum'] ?? '0', false),
'sub_title' => Amount::formatAnything($currency, $expenses[$currencyId]['sum'] ?? '0', false)
.' + '.Amount::formatAnything($currency, $incomes[$currencyId]['sum'] ?? '0', false),
];
$return[] = [
'key' => sprintf('spent-in-%s', $currency->code),
@@ -261,7 +261,7 @@ class BasicController extends Controller
'currency_code' => $currency->code,
'currency_symbol' => $currency->symbol,
'currency_decimal_places' => $currency->decimal_places,
'value_parsed' => app('amount')->formatAnything($currency, $expenses[$currencyId]['sum'] ?? '0', false),
'value_parsed' => Amount::formatAnything($currency, $expenses[$currencyId]['sum'] ?? '0', false),
'local_icon' => 'balance-scale',
'sub_title' => '',
];
@@ -273,7 +273,7 @@ class BasicController extends Controller
'currency_code' => $currency->code,
'currency_symbol' => $currency->symbol,
'currency_decimal_places' => $currency->decimal_places,
'value_parsed' => app('amount')->formatAnything($currency, $incomes[$currencyId]['sum'] ?? '0', false),
'value_parsed' => Amount::formatAnything($currency, $incomes[$currencyId]['sum'] ?? '0', false),
'local_icon' => 'balance-scale',
'sub_title' => '',
];
@@ -289,10 +289,10 @@ class BasicController extends Controller
'currency_code' => $currency->code,
'currency_symbol' => $currency->symbol,
'currency_decimal_places' => $currency->decimal_places,
'value_parsed' => app('amount')->formatAnything($currency, '0', false),
'value_parsed' => Amount::formatAnything($currency, '0', false),
'local_icon' => 'balance-scale',
'sub_title' => app('amount')->formatAnything($currency, '0', false)
.' + '.app('amount')->formatAnything($currency, '0', false),
'sub_title' => Amount::formatAnything($currency, '0', false)
.' + '.Amount::formatAnything($currency, '0', false),
];
$return[] = [
'key' => sprintf('spent-in-%s', $currency->code),
@@ -302,7 +302,7 @@ class BasicController extends Controller
'currency_code' => $currency->code,
'currency_symbol' => $currency->symbol,
'currency_decimal_places' => $currency->decimal_places,
'value_parsed' => app('amount')->formatAnything($currency, '0', false),
'value_parsed' => Amount::formatAnything($currency, '0', false),
'local_icon' => 'balance-scale',
'sub_title' => '',
];
@@ -314,7 +314,7 @@ class BasicController extends Controller
'currency_code' => $currency->code,
'currency_symbol' => $currency->symbol,
'currency_decimal_places' => $currency->decimal_places,
'value_parsed' => app('amount')->formatAnything($currency, '0', false),
'value_parsed' => Amount::formatAnything($currency, '0', false),
'local_icon' => 'balance-scale',
'sub_title' => '',
];
@@ -405,7 +405,7 @@ class BasicController extends Controller
'currency_code' => $info['code'],
'currency_symbol' => $info['symbol'],
'currency_decimal_places' => $info['decimal_places'],
'value_parsed' => app('amount')->formatFlat($info['symbol'], $info['decimal_places'], $amount, false),
'value_parsed' => Amount::formatFlat($info['symbol'], $info['decimal_places'], $amount, false),
'local_icon' => 'check',
'sub_title' => '',
];
@@ -424,7 +424,7 @@ class BasicController extends Controller
'currency_code' => $info['code'],
'currency_symbol' => $info['symbol'],
'currency_decimal_places' => $info['decimal_places'],
'value_parsed' => app('amount')->formatFlat($info['symbol'], $info['decimal_places'], $amount, false),
'value_parsed' => Amount::formatFlat($info['symbol'], $info['decimal_places'], $amount, false),
'local_icon' => 'calendar-o',
'sub_title' => '',
];
@@ -443,7 +443,7 @@ class BasicController extends Controller
'currency_code' => $currency->code,
'currency_symbol' => $currency->symbol,
'currency_decimal_places' => $currency->decimal_places,
'value_parsed' => app('amount')->formatFlat($currency->symbol, $currency->decimal_places, '0', false),
'value_parsed' => Amount::formatFlat($currency->symbol, $currency->decimal_places, '0', false),
'local_icon' => 'check',
'sub_title' => '',
];
@@ -455,7 +455,7 @@ class BasicController extends Controller
'currency_code' => $currency->code,
'currency_symbol' => $currency->symbol,
'currency_decimal_places' => $currency->decimal_places,
'value_parsed' => app('amount')->formatFlat($currency->symbol, $currency->decimal_places, '0', false),
'value_parsed' => Amount::formatFlat($currency->symbol, $currency->decimal_places, '0', false),
'local_icon' => 'calendar-o',
'sub_title' => '',
];
@@ -493,14 +493,9 @@ class BasicController extends Controller
'currency_code' => $currencies[$currencyId]->code,
'currency_symbol' => $currencies[$currencyId]->symbol,
'currency_decimal_places' => $currencies[$currencyId]->decimal_places,
'value_parsed' => app('amount')->formatFlat($currencies[$currencyId]->symbol, $currencies[$currencyId]->decimal_places, $availableBudget, false),
'value_parsed' => Amount::formatFlat($currencies[$currencyId]->symbol, $currencies[$currencyId]->decimal_places, $availableBudget, false),
'local_icon' => 'money',
'sub_title' => app('amount')->formatFlat(
$currencies[$currencyId]->symbol,
$currencies[$currencyId]->decimal_places,
$availableBudget,
false
),
'sub_title' => Amount::formatFlat($currencies[$currencyId]->symbol, $currencies[$currencyId]->decimal_places, $availableBudget, false),
];
}
foreach ($spent as $row) {
@@ -529,18 +524,14 @@ class BasicController extends Controller
'currency_code' => $row['currency_code'],
'currency_symbol' => $row['currency_symbol'],
'currency_decimal_places' => $row['currency_decimal_places'],
'value_parsed' => app('amount')->formatFlat($row['currency_symbol'], $row['currency_decimal_places'], $leftToSpend, false),
'value_parsed' => Amount::formatFlat($row['currency_symbol'], $row['currency_decimal_places'], $leftToSpend, false),
'local_icon' => 'money',
'sub_title' => app('amount')->formatFlat(
$row['currency_symbol'],
$row['currency_decimal_places'],
$perDay,
false
),
'sub_title' => Amount::formatFlat($row['currency_symbol'], $row['currency_decimal_places'], $perDay, false),
];
}
unset($leftToSpend);
if (0 === count($return)) {
$days = (int) $start->diffInDays($end, true) + 1;
// a small trick to get every expense in this period, regardless of budget.
$spent = $this->opsRepository->sumExpenses($start, $end, null, new Collection());
foreach ($spent as $row) {
@@ -563,14 +554,9 @@ class BasicController extends Controller
'currency_code' => $row['currency_code'],
'currency_symbol' => $row['currency_symbol'],
'currency_decimal_places' => $row['currency_decimal_places'],
'value_parsed' => app('amount')->formatFlat($row['currency_symbol'], $row['currency_decimal_places'], $spentInCurrency, false),
'value_parsed' => Amount::formatFlat($row['currency_symbol'], $row['currency_decimal_places'], $spentInCurrency, false),
'local_icon' => 'money',
'sub_title' => app('amount')->formatFlat(
$row['currency_symbol'],
$row['currency_decimal_places'],
$perDay,
false
),
'sub_title' => Amount::formatFlat($row['currency_symbol'], $row['currency_decimal_places'], $perDay, false),
];
}
@@ -587,9 +573,9 @@ class BasicController extends Controller
// 'currency_code' => $currency->code,
// 'currency_symbol' => $currency->symbol,
// 'currency_decimal_places' => $currency->decimal_places,
// 'value_parsed' => app('amount')->formatFlat($currency->symbol, $currency->decimal_places, '0', false),
// 'value_parsed' => Amount::formatFlat($currency->symbol, $currency->decimal_places, '0', false),
// 'local_icon' => 'money',
// 'sub_title' => app('amount')->formatFlat(
// 'sub_title' => Amount::formatFlat(
// $currency->symbol,
// $currency->decimal_places,
// '0',
@@ -642,7 +628,7 @@ class BasicController extends Controller
'currency_code' => $data['currency_code'],
'currency_symbol' => $data['currency_symbol'],
'currency_decimal_places' => $data['currency_decimal_places'],
'value_parsed' => app('amount')->formatFlat($data['currency_symbol'], $data['currency_decimal_places'], $data['balance'], false),
'value_parsed' => Amount::formatFlat($data['currency_symbol'], $data['currency_decimal_places'], $data['balance'], false),
'local_icon' => 'line-chart',
'sub_title' => '',
];
@@ -656,7 +642,7 @@ class BasicController extends Controller
'currency_code' => $this->primaryCurrency->code,
'currency_symbol' => $this->primaryCurrency->symbol,
'currency_decimal_places' => $this->primaryCurrency->decimal_places,
'value_parsed' => app('amount')->formatFlat($this->primaryCurrency->symbol, $this->primaryCurrency->decimal_places, '0', false),
'value_parsed' => Amount::formatFlat($this->primaryCurrency->symbol, $this->primaryCurrency->decimal_places, '0', false),
'local_icon' => 'line-chart',
'sub_title' => '',
];

View File

@@ -54,6 +54,7 @@ class CronController extends Controller
$return['exchange_rates'] = $this->exchangeRatesCronJob($config['force'], $config['date']);
}
$return['bill_notifications'] = $this->billWarningCronJob($config['force'], $config['date']);
$return['webhooks'] = $this->webhookCronJob($config['force'], $config['date']);
return response()->api($return);
}

View File

@@ -40,6 +40,7 @@ use FireflyIII\Models\TransactionCurrency;
use FireflyIII\Models\UserGroup;
use FireflyIII\Repositories\PiggyBank\PiggyBankRepositoryInterface;
use FireflyIII\Repositories\UserGroup\UserGroupRepositoryInterface;
use FireflyIII\Support\Facades\Amount;
use FireflyIII\Support\Facades\Preferences;
use FireflyIII\Support\Http\Api\ExchangeRateConverter;
use Illuminate\Console\Command;
@@ -72,6 +73,8 @@ class CorrectsPrimaryCurrencyAmounts extends Command
/** @var UserGroupRepositoryInterface $repository */
$repository = app(UserGroupRepositoryInterface::class);
Preferences::mark();
/** @var UserGroup $userGroup */
foreach ($repository->getAll() as $userGroup) {
$this->recalculateForGroup($userGroup);
@@ -87,8 +90,7 @@ class CorrectsPrimaryCurrencyAmounts extends Command
$this->recalculateAccounts($userGroup);
// do a check with the group's currency so we can skip some stuff.
Preferences::mark();
$currency = app('amount')->getPrimaryCurrencyByUserGroup($userGroup);
$currency = Amount::getPrimaryCurrencyByUserGroup($userGroup);
$this->recalculatePiggyBanks($userGroup, $currency);
$this->recalculateBudgets($userGroup, $currency);

View File

@@ -32,6 +32,7 @@ use FireflyIII\Support\Cronjobs\BillWarningCronjob;
use FireflyIII\Support\Cronjobs\ExchangeRatesCronjob;
use FireflyIII\Support\Cronjobs\RecurringCronjob;
use FireflyIII\Support\Cronjobs\UpdateCheckCronjob;
use FireflyIII\Support\Cronjobs\WebhookCronjob;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Log;
use InvalidArgumentException;
@@ -50,6 +51,7 @@ class Cron extends Command
{--create-recurring : Create recurring transactions. Other tasks will be skipped unless also requested.}
{--create-auto-budgets : Create auto budgets. Other tasks will be skipped unless also requested.}
{--send-bill-warnings : Send bill warnings. Other tasks will be skipped unless also requested.}
{--send-webhook-messages : Sends any stray webhook messages (with a maximum of 5).}
';
public function handle(): int
@@ -58,7 +60,8 @@ class Cron extends Command
&& !$this->option('create-recurring')
&& !$this->option('create-auto-budgets')
&& !$this->option('send-bill-warnings')
&& !$this->option('check-version');
&& !$this->option('check-version')
&& !$this->option('send-webhook-messages');
$date = null;
try {
@@ -122,6 +125,16 @@ class Cron extends Command
$this->friendlyError($e->getMessage());
}
}
// Fire webhook messages cron job.
if ($doAll || $this->option('send-webhook-messages')) {
try {
$this->webhookCronJob($force, $date);
} catch (FireflyException $e) {
app('log')->error($e->getMessage());
app('log')->error($e->getTraceAsString());
$this->friendlyError($e->getMessage());
}
}
$this->friendlyInfo('More feedback on the cron jobs can be found in the log files.');
@@ -239,4 +252,26 @@ class Cron extends Command
$this->friendlyPositive(sprintf('"Send bill warnings" cron ran with success: %s', $autoBudget->message));
}
}
private function webhookCronJob(bool $force, ?Carbon $date): void
{
$webhook = new WebhookCronjob();
$webhook->setForce($force);
// set date in cron job:
if ($date instanceof Carbon) {
$webhook->setDate($date);
}
$webhook->fire();
if ($webhook->jobErrored) {
$this->friendlyError(sprintf('Error in "webhook" cron: %s', $webhook->message));
}
if ($webhook->jobFired) {
$this->friendlyInfo(sprintf('"Webhook" cron fired: %s', $webhook->message));
}
if ($webhook->jobSucceeded) {
$this->friendlyPositive(sprintf('"Webhook" cron ran with success: %s', $webhook->message));
}
}
}

View File

@@ -0,0 +1,20 @@
<?php
declare(strict_types=1);
namespace FireflyIII\Events\Model\Bill;
use FireflyIII\Events\Event;
use FireflyIII\Models\Bill;
use Illuminate\Queue\SerializesModels;
/**
* Class WarnUserAboutBill.
*/
class WarnUserAboutBill extends Event
{
use SerializesModels;
public function __construct(public Bill $bill, public string $field, public int $diff) {}
}

View File

@@ -0,0 +1,17 @@
<?php
declare(strict_types=1);
namespace FireflyIII\Events\Model\Bill;
use FireflyIII\Events\Event;
use FireflyIII\User;
use Illuminate\Queue\SerializesModels;
class WarnUserAboutOverdueSubscriptions extends Event
{
use SerializesModels;
public function __construct(public User $user, public array $overdue) {}
}

View File

@@ -35,6 +35,6 @@ class UserGroupChangedPrimaryCurrency extends Event
public function __construct(public UserGroup $userGroup)
{
Log::debug('User group changed default currency.');
Log::debug('User group changed primary currency.');
}
}

View File

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

View File

@@ -24,48 +24,118 @@ declare(strict_types=1);
namespace FireflyIII\Handlers\Events;
use FireflyIII\Events\WarnUserAboutBill;
use FireflyIII\Notifications\User\BillReminder;
use Illuminate\Support\Facades\Notification;
use Exception;
use FireflyIII\Events\Model\Bill\WarnUserAboutBill;
use FireflyIII\Events\Model\Bill\WarnUserAboutOverdueSubscriptions;
use FireflyIII\Models\Bill;
use FireflyIII\Notifications\User\BillReminder;
use FireflyIII\Notifications\User\SubscriptionsOverdueReminder;
use FireflyIII\Support\Facades\Preferences;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Notification;
/**
* Class BillEventHandler
*/
class BillEventHandler
{
public function warnAboutOverdueSubscriptions(WarnUserAboutOverdueSubscriptions $event): void
{
Log::debug(sprintf('Now in %s', __METHOD__));
// make sure user does not get the warning twice.
$overdue = $event->overdue;
$user = $event->user;
$toBeWarned = [];
Log::debug(sprintf('%d bills to warn about.', count($overdue)));
foreach ($overdue as $item) {
/** @var Bill $bill */
$bill = $item['bill'];
$key = sprintf('bill_overdue_%s_%s', $bill->id, substr(hash('sha256', json_encode($item['dates']['pay_dates'], JSON_THROW_ON_ERROR)), 0, 10));
$pref = Preferences::getForUser($bill->user, $key, false);
if (true === $pref->data) {
Log::debug(sprintf('User #%d has already been warned about overdue subscription #%d.', $bill->user->id, $bill->id));
continue;
}
$toBeWarned[] = $item;
}
unset($bill);
Log::debug(sprintf('Now %d bills to warn about.', count($toBeWarned)));
/** @var bool $sendNotification */
$sendNotification = Preferences::getForUser($user, 'notification_bill_reminder', true)->data;
if (false === $sendNotification) {
Log::debug('User has disabled bill reminders.');
return;
}
Log::debug(sprintf('Will warn about %d overdue subscription(s).', count($toBeWarned)));
if (0 === count($toBeWarned)) {
Log::debug('No overdue subscriptions to warn about.');
return;
}
foreach ($toBeWarned as $item) {
/** @var Bill $bill */
$bill = $item['bill'];
$key = sprintf('bill_overdue_%s_%s', $bill->id, substr(hash('sha256', json_encode($item['dates']['pay_dates'], JSON_THROW_ON_ERROR)), 0, 10));
Preferences::setForUser($bill->user, $key, true);
}
Log::warning('should hit this ONCE');
try {
Notification::send($user, new SubscriptionsOverdueReminder($toBeWarned));
} catch (Exception $e) {
$message = $e->getMessage();
if (str_contains($message, 'Bcc')) {
Log::warning('[Bcc] Could not send notification. Please validate your email settings, use the .env.example file as a guide.');
return;
}
if (str_contains($message, 'RFC 2822')) {
Log::warning('[RFC] Could not send notification. Please validate your email settings, use the .env.example file as a guide.');
return;
}
Log::error($e->getMessage());
Log::error($e->getTraceAsString());
}
}
public function warnAboutBill(WarnUserAboutBill $event): void
{
app('log')->debug(sprintf('Now in %s', __METHOD__));
Log::debug(sprintf('Now in %s', __METHOD__));
$bill = $event->bill;
$bill = $event->bill;
/** @var bool $preference */
$preference = app('preferences')->getForUser($bill->user, 'notification_bill_reminder', true)->data;
Preferences::getForUser($bill->user, 'notification_bill_reminder', true)->data;
if (true === $preference) {
app('log')->debug('Bill reminder is true!');
Log::debug('Bill reminder is true!');
try {
Notification::send($bill->user, new BillReminder($bill, $event->field, $event->diff));
} catch (Exception $e) {
$message = $e->getMessage();
if (str_contains($message, 'Bcc')) {
app('log')->warning('[Bcc] Could not send notification. Please validate your email settings, use the .env.example file as a guide.');
Log::warning('[Bcc] Could not send notification. Please validate your email settings, use the .env.example file as a guide.');
return;
}
if (str_contains($message, 'RFC 2822')) {
app('log')->warning('[RFC] Could not send notification. Please validate your email settings, use the .env.example file as a guide.');
Log::warning('[RFC] Could not send notification. Please validate your email settings, use the .env.example file as a guide.');
return;
}
app('log')->error($e->getMessage());
app('log')->error($e->getTraceAsString());
Log::error($e->getMessage());
Log::error($e->getTraceAsString());
}
return;
}
if (false === $preference) {
app('log')->debug('User has disabled bill reminders.');
}
Log::debug('User has disabled bill reminders.');
}
}

View File

@@ -73,6 +73,7 @@ class PreferencesEventHandler
$repository = app(PiggyBankRepositoryInterface::class);
$repository->setUserGroup($userGroup);
$piggyBanks = $repository->getPiggyBanks();
Log::debug(sprintf('Resetting %d piggy bank(s).', $piggyBanks->count()));
/** @var PiggyBank $piggyBank */
foreach ($piggyBanks as $piggyBank) {
@@ -104,6 +105,8 @@ class PreferencesEventHandler
$repository->setUserGroup($userGroup);
$set = $repository->getBudgets();
Log::debug(sprintf('Resetting %d budget(s).', $set->count()));
/** @var Budget $budget */
foreach ($set as $budget) {
foreach ($budget->autoBudgets as $autoBudget) {

View File

@@ -77,8 +77,8 @@ class NetWorth implements NetWorthInterface
Log::debug(sprintf('Now in byAccounts("%s", "%s")', $ids, $date->format('Y-m-d H:i:s')));
$primary = Amount::getPrimaryCurrency();
$netWorth = [];
Log::debug(sprintf('NetWorth: finalAccountsBalance("%s")', $date->format('Y-m-d H:i:s')));
$balances = Steam::finalAccountsBalance($accounts, $date);
Log::debug(sprintf('NetWorth: accountsBalancesOptimized("%s")', $date->format('Y-m-d H:i:s')));
$balances = Steam::accountsBalancesOptimized($accounts, $date, null, $convertToPrimary);
/** @var Account $account */
foreach ($accounts as $account) {
@@ -143,8 +143,8 @@ class NetWorth implements NetWorthInterface
*/
$accounts = $this->getAccounts();
$return = [];
Log::debug(sprintf('SumNetWorth: finalAccountsBalance("%s")', $date->format('Y-m-d H:i:s')));
$balances = Steam::finalAccountsBalance($accounts, $date);
Log::debug(sprintf('SumNetWorth: accountsBalancesOptimized("%s")', $date->format('Y-m-d H:i:s')));
$balances = Steam::accountsBalancesOptimized($accounts, $date);
foreach ($accounts as $account) {
$currency = $this->accountRepository->getAccountCurrency($account);
$balance = $balances[$account->id]['balance'] ?? '0';

View File

@@ -93,10 +93,10 @@ class IndexController extends Controller
$start->subSecond();
$ids = $accounts->pluck('id')->toArray();
Log::debug(sprintf('inactive start: finalAccountsBalance("%s")', $start->format('Y-m-d H:i:s')));
Log::debug(sprintf('inactive end: finalAccountsBalance("%s")', $end->format('Y-m-d H:i:s')));
$startBalances = Steam::finalAccountsBalance($accounts, $start);
$endBalances = Steam::finalAccountsBalance($accounts, $end);
Log::debug(sprintf('inactive start: accountsBalancesOptimized("%s")', $start->format('Y-m-d H:i:s')));
Log::debug(sprintf('inactive end: accountsBalancesOptimized("%s")', $end->format('Y-m-d H:i:s')));
$startBalances = Steam::accountsBalancesOptimized($accounts, $start, $this->primaryCurrency, $this->convertToPrimary);
$endBalances = Steam::accountsBalancesOptimized($accounts, $end, $this->primaryCurrency, $this->convertToPrimary);
$activities = Steam::getLastActivities($ids);
@@ -170,10 +170,10 @@ class IndexController extends Controller
$start->subSecond();
$ids = $accounts->pluck('id')->toArray();
Log::debug(sprintf('index start: finalAccountsBalance("%s")', $start->format('Y-m-d H:i:s')));
Log::debug(sprintf('index end: finalAccountsBalance("%s")', $end->format('Y-m-d H:i:s')));
$startBalances = Steam::finalAccountsBalance($accounts, $start);
$endBalances = Steam::finalAccountsBalance($accounts, $end);
Log::debug(sprintf('index start: accountsBalancesOptimized("%s")', $start->format('Y-m-d H:i:s')));
Log::debug(sprintf('index end: accountsBalancesOptimized("%s")', $end->format('Y-m-d H:i:s')));
$startBalances = Steam::accountsBalancesOptimized($accounts, $start, $this->primaryCurrency, $this->convertToPrimary);
$endBalances = Steam::accountsBalancesOptimized($accounts, $end, $this->primaryCurrency, $this->convertToPrimary);
$activities = Steam::getLastActivities($ids);

View File

@@ -40,6 +40,7 @@ use Illuminate\Routing\Redirector;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Log;
use Illuminate\View\View;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
/**
* Class ShowController
@@ -81,7 +82,9 @@ class ShowController extends Controller
* */
public function show(Request $request, Account $account, ?Carbon $start = null, ?Carbon $end = null)
{
if (0 === $account->id) {
throw new NotFoundHttpException();
}
$objectType = config(sprintf('firefly.shortNamesByFullName.%s', $account->accountType->type));
if (!$this->isEditableAccount($account)) {
@@ -115,20 +118,27 @@ class ShowController extends Controller
$chartUrl = route('chart.account.period', [$account->id, $start->format('Y-m-d'), $end->format('Y-m-d')]);
$firstTransaction = $this->repository->oldestJournalDate($account) ?? $start;
Log::debug('Start period overview');
Timer::start('period-overview');
// go back max 3 years.
$threeYearsAgo = clone $start;
$threeYearsAgo->startOfYear()->subYears(3);
if ($firstTransaction->lt($threeYearsAgo)) {
$firstTransaction = clone $threeYearsAgo;
}
Log::debug('Start period overview');
$timer = Timer::getInstance();
$timer->start('period-overview');
$periods = $this->getAccountPeriodOverview($account, $firstTransaction, $end);
Log::debug('End period overview');
Timer::stop('period-overview');
$timer->stop('period-overview');
// if layout = v2, overrule the page title.
if ('v1' !== config('view.layout')) {
$subTitle = (string) trans('firefly.all_journals_for_account', ['name' => $account->name]);
}
Log::debug('Collect transactions');
Timer::start('collection');
$timer->start('collection');
/** @var GroupCollectorInterface $collector */
$collector = app(GroupCollectorInterface::class);
@@ -136,6 +146,7 @@ class ShowController extends Controller
->setAccounts(new Collection([$account]))
->setLimit($pageSize)
->setPage($page)
->withAttachmentInformation()
->withAPIInformation()
->setRange($start, $end)
;
@@ -146,7 +157,7 @@ class ShowController extends Controller
Log::debug('End collect transactions');
Timer::stop('collection');
$timer->stop('collection');
// enrich data in arrays.

View File

@@ -57,7 +57,7 @@ class IndexController extends Controller
$this->middleware(
function ($request, $next) {
app('view')->share('title', (string) trans('firefly.bills'));
app('view')->share('title', (string)trans('firefly.bills'));
app('view')->share('mainTitleIcon', 'fa-calendar-o');
$this->repository = app(BillRepositoryInterface::class);
@@ -79,7 +79,6 @@ class IndexController extends Controller
$total = $collection->count();
$parameters = new ParameterBag();
// sub one day from temp start so the last paid date is one day before it should be.
$tempStart = clone $start;
@@ -112,7 +111,7 @@ class IndexController extends Controller
$bills = [
0 => [ // the index is the order, not the ID.
'object_group_id' => 0,
'object_group_title' => (string) trans('firefly.default_group_title_name'),
'object_group_title' => (string)trans('firefly.default_group_title_name'),
'bills' => [],
],
];
@@ -120,7 +119,7 @@ class IndexController extends Controller
/** @var Bill $bill */
foreach ($collection as $bill) {
$array = $transformer->transform($bill);
$groupOrder = (int) $array['object_group_order'];
$groupOrder = (int)$array['object_group_order'];
// make group array if necessary:
$bills[$groupOrder] ??= [
'object_group_id' => $array['object_group_id'],
@@ -173,16 +172,28 @@ class IndexController extends Controller
'currency_symbol' => $bill['currency_symbol'],
'currency_decimal_places' => $bill['currency_decimal_places'],
'avg' => '0',
'total_left_to_pay' => '0',
'period' => $range,
'per_period' => '0',
];
// only fill in avg when bill is active.
if (null !== $bill['next_expected_match']) {
$avg = bcdiv(bcadd((string) $bill['amount_min'], (string) $bill['amount_max']), '2');
$avg = bcmul($avg, (string) count($bill['pay_dates']));
$avg = bcdiv(bcadd((string)$bill['amount_min'], (string)$bill['amount_max']), '2');
$avg = bcmul($avg, (string)count($bill['pay_dates']));
$sums[$groupOrder][$currencyId]['avg'] = bcadd($sums[$groupOrder][$currencyId]['avg'], $avg);
}
// only fill in total_left_to_pay when bill is not yet paid.
if (count($bill['paid_dates']) < count($bill['pay_dates'])) {
$count = count($bill['pay_dates']) - count($bill['paid_dates']);
if ($count > 0) {
$avg = bcdiv(bcadd((string)$bill['amount_min'], (string)$bill['amount_max']), '2');
$avg = bcmul($avg, (string)$count);
$sums[$groupOrder][$currencyId]['total_left_to_pay'] = bcadd($sums[$groupOrder][$currencyId]['total_left_to_pay'], $avg);
}
}
// fill in per period regardless:
$sums[$groupOrder][$currencyId]['per_period'] = bcadd($sums[$groupOrder][$currencyId]['per_period'], $this->amountPerPeriod($bill, $range));
}
@@ -193,7 +204,7 @@ class IndexController extends Controller
private function amountPerPeriod(array $bill, string $range): string
{
$avg = bcdiv(bcadd((string) $bill['amount_min'], (string) $bill['amount_max']), '2');
$avg = bcdiv(bcadd((string)$bill['amount_min'], (string)$bill['amount_max']), '2');
app('log')->debug(sprintf('Amount per period for bill #%d "%s"', $bill['id'], $bill['name']));
app('log')->debug(sprintf('Average is %s', $avg));
@@ -206,8 +217,8 @@ class IndexController extends Controller
'weekly' => '52.17',
'daily' => '365.24',
];
$yearAmount = bcmul($avg, bcdiv($multiplies[$bill['repeat_freq']], (string) ($bill['skip'] + 1)));
app('log')->debug(sprintf('Amount per year is %s (%s * %s / %s)', $yearAmount, $avg, $multiplies[$bill['repeat_freq']], (string) ($bill['skip'] + 1)));
$yearAmount = bcmul($avg, bcdiv($multiplies[$bill['repeat_freq']], (string)($bill['skip'] + 1)));
app('log')->debug(sprintf('Amount per year is %s (%s * %s / %s)', $yearAmount, $avg, $multiplies[$bill['repeat_freq']], (string)($bill['skip'] + 1)));
// per period:
$division = [
@@ -258,8 +269,8 @@ class IndexController extends Controller
'period' => $entry['period'],
'per_period' => '0',
];
$totals[$currencyId]['avg'] = bcadd($totals[$currencyId]['avg'], (string) $entry['avg']);
$totals[$currencyId]['per_period'] = bcadd($totals[$currencyId]['per_period'], (string) $entry['per_period']);
$totals[$currencyId]['avg'] = bcadd($totals[$currencyId]['avg'], (string)$entry['avg']);
$totals[$currencyId]['per_period'] = bcadd($totals[$currencyId]['per_period'], (string)$entry['per_period']);
}
}
@@ -271,8 +282,8 @@ class IndexController extends Controller
*/
public function setOrder(Request $request, Bill $bill): JsonResponse
{
$objectGroupTitle = (string) $request->get('objectGroupTitle');
$newOrder = (int) $request->get('order');
$objectGroupTitle = (string)$request->get('objectGroupTitle');
$newOrder = (int)$request->get('order');
$this->repository->setOrder($bill, $newOrder);
if ('' !== $objectGroupTitle) {
$this->repository->setObjectGroup($bill, $objectGroupTitle);

View File

@@ -35,6 +35,7 @@ use FireflyIII\Models\TransactionCurrency;
use FireflyIII\Repositories\Account\AccountRepositoryInterface;
use FireflyIII\Repositories\Currency\CurrencyRepositoryInterface;
use FireflyIII\Support\CacheProperties;
use FireflyIII\Support\Facades\Preferences;
use FireflyIII\Support\Facades\Steam;
use FireflyIII\Support\Http\Controllers\AugumentData;
use FireflyIII\Support\Http\Controllers\ChartGeneration;
@@ -78,6 +79,7 @@ class AccountController extends Controller
/**
* Shows the balances for all the user's expense accounts (on the front page).
* 2025-08-06 validated for multi (primary) currency
*
* This chart is (multi) currency aware.
*/
@@ -112,11 +114,11 @@ class AccountController extends Controller
$accountNames = $this->extractNames($accounts);
// grab all balances
Log::debug(sprintf('expenseAccounts: finalAccountsBalance("%s")', $start->format('Y-m-d H:i:s')));
Log::debug(sprintf('expenseAccounts: finalAccountsBalance("%s")', $end->format('Y-m-d H:i:s')));
$startBalances = Steam::finalAccountsBalance($accounts, $start);
$endBalances = Steam::finalAccountsBalance($accounts, $end);
Log::debug(sprintf('expenseAccounts: accountsBalancesOptimized("%s")', $start->format('Y-m-d H:i:s')));
Log::debug(sprintf('expenseAccounts: accountsBalancesOptimized("%s")', $end->format('Y-m-d H:i:s')));
$startBalances = Steam::accountsBalancesOptimized($accounts, $start, $this->primaryCurrency, $this->convertToPrimary);
$endBalances = Steam::accountsBalancesOptimized($accounts, $end, $this->primaryCurrency, $this->convertToPrimary);
Log::debug('Done collecting balances');
// loop the accounts, then check for balance and currency info.
foreach ($accounts as $account) {
// Log::debug(sprintf('[a] Now in account #%d ("%s")', $account->id, $account->name));
@@ -157,7 +159,7 @@ class AccountController extends Controller
$tempData[] = [
'name' => $accountNames[$account->id],
'difference' => $diff,
'diff_float' => (float) $diff, // intentional float
'diff_float' => (float)$diff, // intentional float
'currency_id' => $currencies[$searchCode]->id,
];
}
@@ -182,7 +184,7 @@ class AccountController extends Controller
foreach ($currencies as $currencyId => $currency) {
$dataSet
= [
'label' => (string) trans('firefly.spent'),
'label' => (string)trans('firefly.spent'),
'type' => 'bar',
'currency_symbol' => $currency->symbol,
'currency_code' => $currency->code,
@@ -195,7 +197,7 @@ class AccountController extends Controller
foreach ($tempData as $entry) {
$currencyId = $entry['currency_id'];
$name = $entry['name'];
$chartData[$currencyId]['entries'][$name] = (float) $entry['difference'];
$chartData[$currencyId]['entries'][$name] = (float)$entry['difference'];
}
$data = $this->generator->multiSet($chartData);
@@ -223,6 +225,7 @@ class AccountController extends Controller
$cache = new CacheProperties();
$cache->addProperty($account->id);
$cache->addProperty($start);
$cache->addProperty($this->convertToPrimary);
$cache->addProperty($end);
$cache->addProperty('chart.account.expense-budget');
if ($cache->has()) {
@@ -231,7 +234,10 @@ class AccountController extends Controller
/** @var GroupCollectorInterface $collector */
$collector = app(GroupCollectorInterface::class);
$collector->setAccounts(new Collection([$account]))->setRange($start, $end)->withBudgetInformation()->setTypes([TransactionTypeEnum::WITHDRAWAL->value]);
$collector->setAccounts(new Collection([$account]))
->setRange($start, $end)
->withBudgetInformation()->setTypes([TransactionTypeEnum::WITHDRAWAL->value])
;
$journals = $collector->getExtractedJournals();
$chartData = [];
$result = [];
@@ -239,19 +245,37 @@ class AccountController extends Controller
/** @var array $journal */
foreach ($journals as $journal) {
$budgetId = (int) $journal['budget_id'];
$budgetId = (int)$journal['budget_id'];
$key = sprintf('%d-%d', $budgetId, $journal['currency_id']);
$budgetIds[] = $budgetId;
// currency info:
$currencyId = (int)$journal['currency_id'];
$currencyName = $journal['currency_name'];
$currencySymbol = $journal['currency_symbol'];
$currencyCode = $journal['currency_code'];
$currencyDecimalPlaces = $journal['currency_decimal_places'];
$field = 'amount';
if ($this->convertToPrimary && $this->primaryCurrency->id !== $currencyId) {
$field = 'pc_amount';
$currencyName = $this->primaryCurrency->name;
$currencySymbol = $this->primaryCurrency->symbol;
$currencyCode = $this->primaryCurrency->code;
$currencyDecimalPlaces = $this->primaryCurrency->decimal_places;
}
if (!array_key_exists($key, $result)) {
$result[$key] = [
'total' => '0',
'budget_id' => $budgetId,
'currency_name' => $journal['currency_name'],
'currency_symbol' => $journal['currency_symbol'],
'currency_code' => $journal['currency_code'],
'total' => '0',
'budget_id' => $budgetId,
'currency_name' => $currencyName,
'currency_symbol' => $currencySymbol,
'currency_code' => $currencyCode,
'currency_decimal_places' => $currencyDecimalPlaces,
];
}
$result[$key]['total'] = bcadd((string) $journal['amount'], $result[$key]['total']);
$result[$key]['total'] = bcadd((string)$journal[$field], $result[$key]['total']);
}
$names = $this->getBudgetNames($budgetIds);
@@ -259,7 +283,7 @@ class AccountController extends Controller
foreach ($result as $row) {
$budgetId = $row['budget_id'];
$name = $names[$budgetId];
$label = (string) trans('firefly.name_in_currency', ['name' => $name, 'currency' => $row['currency_name']]);
$label = (string)trans('firefly.name_in_currency', ['name' => $name, 'currency' => $row['currency_name']]);
$chartData[$label] = ['amount' => $row['total'], 'currency_symbol' => $row['currency_symbol'], 'currency_code' => $row['currency_code']];
}
@@ -289,6 +313,7 @@ class AccountController extends Controller
$cache->addProperty($account->id);
$cache->addProperty($start);
$cache->addProperty($end);
$cache->addProperty($this->convertToPrimary);
$cache->addProperty('chart.account.expense-category');
if ($cache->has()) {
return response()->json($cache->get());
@@ -305,22 +330,39 @@ class AccountController extends Controller
foreach ($journals as $journal) {
$key = sprintf('%d-%d', $journal['category_id'], $journal['currency_id']);
if (!array_key_exists($key, $result)) {
$result[$key] = [
'total' => '0',
'category_id' => (int) $journal['category_id'],
'currency_name' => $journal['currency_name'],
'currency_symbol' => $journal['currency_symbol'],
'currency_code' => $journal['currency_code'],
// currency info:
$currencyId = (int)$journal['currency_id'];
$currencyName = $journal['currency_name'];
$currencySymbol = $journal['currency_symbol'];
$currencyCode = $journal['currency_code'];
$currencyDecimalPlaces = $journal['currency_decimal_places'];
$field = 'amount';
if ($this->convertToPrimary && $this->primaryCurrency->id !== $currencyId) {
$field = 'pc_amount';
$currencyName = $this->primaryCurrency->name;
$currencySymbol = $this->primaryCurrency->symbol;
$currencyCode = $this->primaryCurrency->code;
$currencyDecimalPlaces = $this->primaryCurrency->decimal_places;
}
$result[$key] = [
'total' => '0',
'category_id' => (int)$journal['category_id'],
'currency_name' => $currencyName,
'currency_code' => $currencyCode,
'currency_symbol' => $currencySymbol,
'currency_decimal_places' => $currencyDecimalPlaces,
];
}
$result[$key]['total'] = bcadd((string) $journal['amount'], $result[$key]['total']);
$result[$key]['total'] = bcadd((string)$journal[$field], $result[$key]['total']);
}
$names = $this->getCategoryNames(array_keys($result));
foreach ($result as $row) {
$categoryId = $row['category_id'];
$name = $names[$categoryId] ?? '(unknown)';
$label = (string) trans('firefly.name_in_currency', ['name' => $name, 'currency' => $row['currency_name']]);
$label = (string)trans('firefly.name_in_currency', ['name' => $name, 'currency' => $row['currency_name']]);
$chartData[$label] = ['amount' => $row['total'], 'currency_symbol' => $row['currency_symbol'], 'currency_code' => $row['currency_code']];
}
@@ -341,11 +383,11 @@ class AccountController extends Controller
$end = clone session('end', today(config('app.timezone'))->endOfMonth());
$defaultSet = $repository->getAccountsByType([AccountTypeEnum::DEFAULT->value, AccountTypeEnum::ASSET->value])->pluck('id')->toArray();
// Log::debug('Default set is ', $defaultSet);
$frontpage = app('preferences')->get('frontpageAccounts', $defaultSet);
$frontpage = Preferences::get('frontpageAccounts', $defaultSet);
$frontpageArray = !is_array($frontpage->data) ? [] : $frontpage->data;
Log::debug('Frontpage preference set is ', $frontpageArray);
if (0 === count($frontpageArray)) {
app('preferences')->set('frontpageAccounts', $defaultSet);
Preferences::set('frontpageAccounts', $defaultSet);
Log::debug('frontpage set is empty!');
}
$accounts = $repository->getAccountsById($frontpageArray);
@@ -375,6 +417,7 @@ class AccountController extends Controller
$cache = new CacheProperties();
$cache->addProperty($account->id);
$cache->addProperty($start);
$cache->addProperty($this->convertToPrimary);
$cache->addProperty($end);
$cache->addProperty('chart.account.income-category');
if ($cache->has()) {
@@ -394,22 +437,39 @@ class AccountController extends Controller
foreach ($journals as $journal) {
$key = sprintf('%d-%d', $journal['category_id'], $journal['currency_id']);
if (!array_key_exists($key, $result)) {
$result[$key] = [
'total' => '0',
'category_id' => $journal['category_id'],
'currency_name' => $journal['currency_name'],
'currency_symbol' => $journal['currency_symbol'],
'currency_code' => $journal['currency_code'],
// currency info:
$currencyId = (int)$journal['currency_id'];
$currencyName = $journal['currency_name'];
$currencySymbol = $journal['currency_symbol'];
$currencyCode = $journal['currency_code'];
$currencyDecimalPlaces = $journal['currency_decimal_places'];
$field = 'amount';
if ($this->convertToPrimary && $this->primaryCurrency->id !== $currencyId) {
$field = 'pc_amount';
$currencyName = $this->primaryCurrency->name;
$currencySymbol = $this->primaryCurrency->symbol;
$currencyCode = $this->primaryCurrency->code;
$currencyDecimalPlaces = $this->primaryCurrency->decimal_places;
}
$result[$key] = [
'total' => '0',
'category_id' => $journal['category_id'],
'currency_name' => $currencyName,
'currency_code' => $currencyCode,
'currency_symbol' => $currencySymbol,
'currency_decimal_places' => $currencyDecimalPlaces,
];
}
$result[$key]['total'] = bcadd((string) $journal['amount'], $result[$key]['total']);
$result[$key]['total'] = bcadd((string)$journal[$field], $result[$key]['total']);
}
$names = $this->getCategoryNames(array_keys($result));
foreach ($result as $row) {
$categoryId = $row['category_id'];
$name = $names[$categoryId] ?? '(unknown)';
$label = (string) trans('firefly.name_in_currency', ['name' => $name, 'currency' => $row['currency_name']]);
$label = (string)trans('firefly.name_in_currency', ['name' => $name, 'currency' => $row['currency_name']]);
$chartData[$label] = ['amount' => $row['total'], 'currency_symbol' => $row['currency_symbol'], 'currency_code' => $row['currency_code']];
}
$data = $this->generator->multiCurrencyPieChart($chartData);
@@ -450,7 +510,7 @@ class AccountController extends Controller
// This period depends on the size of the chart
$current = clone $start;
$current = app('navigation')->endOfX($current, $step, null);
$format = (string) trans('config.month_and_day_js', [], $locale);
$format = (string)trans('config.month_and_day_js', [], $locale);
$accountCurrency = $this->accountRepository->getAccountCurrency($account);
$range = Steam::finalAccountBalanceInRange($account, $start, $end, $this->convertToPrimary);
@@ -512,7 +572,7 @@ class AccountController extends Controller
foreach ($return as $key => $info) {
if ('balance' !== $key && 'pc_balance' !== $key) {
// assume it's a currency:
$setCurrency = $this->currencyRepository->findByCode((string) $key);
$setCurrency = $this->currencyRepository->findByCode((string)$key);
$info['currency_symbol'] = $setCurrency->symbol;
$info['currency_code'] = $setCurrency->code;
$info['label'] = sprintf('%s (%s)', $account->name, $setCurrency->symbol);
@@ -525,7 +585,7 @@ class AccountController extends Controller
if ('pc_balance' === $key) {
$info['currency_symbol'] = $this->primaryCurrency->symbol;
$info['currency_code'] = $this->primaryCurrency->code;
$info['label'] = sprintf('%s (%s) (%s)', $account->name, (string) trans('firefly.sum'), $this->primaryCurrency->symbol);
$info['label'] = sprintf('%s (%s) (%s)', $account->name, (string)trans('firefly.sum'), $this->primaryCurrency->symbol);
}
$chartData[] = $info;
}
@@ -594,10 +654,10 @@ class AccountController extends Controller
$accountNames = $this->extractNames($accounts);
// grab all balances
Log::debug(sprintf('revAccounts: finalAccountsBalance("%s")', $start->format('Y-m-d H:i:s')));
Log::debug(sprintf('revAccounts: finalAccountsBalance("%s")', $end->format('Y-m-d H:i:s')));
$startBalances = Steam::finalAccountsBalance($accounts, $start);
$endBalances = Steam::finalAccountsBalance($accounts, $end);
Log::debug(sprintf('revAccounts: accountsBalancesOptimized("%s")', $start->format('Y-m-d H:i:s')));
Log::debug(sprintf('revAccounts: accountsBalancesOptimized("%s")', $end->format('Y-m-d H:i:s')));
$startBalances = Steam::accountsBalancesOptimized($accounts, $start, $this->primaryCurrency, $this->convertToPrimary);
$endBalances = Steam::accountsBalancesOptimized($accounts, $end, $this->primaryCurrency, $this->convertToPrimary);
// loop the accounts, then check for balance and currency info.
@@ -640,7 +700,7 @@ class AccountController extends Controller
$tempData[] = [
'name' => $accountNames[$account->id],
'difference' => $diff,
'diff_float' => (float) $diff, // intentional float
'diff_float' => (float)$diff, // intentional float
'currency_id' => $currencies[$searchCode]->id,
];
}
@@ -667,7 +727,7 @@ class AccountController extends Controller
foreach ($currencies as $currencyId => $currency) {
$dataSet
= [
'label' => (string) trans('firefly.earned'),
'label' => (string)trans('firefly.earned'),
'type' => 'bar',
'currency_symbol' => $currency->symbol,
'currency_code' => $currency->code,

View File

@@ -23,6 +23,7 @@ declare(strict_types=1);
namespace FireflyIII\Http\Controllers;
use FireflyIII\Events\RequestedSendWebhookMessages;
use FireflyIII\Models\TransactionCurrency;
use FireflyIII\Support\Facades\Amount;
use FireflyIII\Support\Facades\Steam;
@@ -33,6 +34,7 @@ use Illuminate\Foundation\Bus\DispatchesJobs;
use Illuminate\Foundation\Validation\ValidatesRequests;
use Illuminate\Routing\Controller as BaseController;
use Illuminate\Support\Facades\Config;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\View;
use Illuminate\Support\Facades\Route;
@@ -141,6 +143,14 @@ abstract class Controller extends BaseController
View::share('shownDemo', $shownDemo);
View::share('current_route_name', $page);
View::share('original_route_name', Route::currentRouteName());
// lottery to send any remaining webhooks:
if (7 === random_int(1, 10)) {
// trigger event to send them:
Log::debug('send event RequestedSendWebhookMessages through lottery');
event(new RequestedSendWebhookMessages());
}
}
View::share('darkMode', $darkMode);

View File

@@ -157,6 +157,11 @@ class DebugController extends Controller
return view('debug', compact('table', 'now', 'logContent'));
}
public function apiTest()
{
return view('test.api-test');
}
private function generateTable(): string
{
// system information:

View File

@@ -32,8 +32,10 @@ use FireflyIII\Models\PiggyBank;
use FireflyIII\Repositories\ObjectGroup\OrganisesObjectGroups;
use FireflyIII\Repositories\PiggyBank\PiggyBankRepositoryInterface;
use FireflyIII\Support\JsonApi\Enrichments\AccountEnrichment;
use FireflyIII\Support\JsonApi\Enrichments\PiggyBankEnrichment;
use FireflyIII\Transformers\AccountTransformer;
use FireflyIII\Transformers\PiggyBankTransformer;
use FireflyIII\User;
use Illuminate\Contracts\View\Factory;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
@@ -114,6 +116,13 @@ class IndexController extends Controller
$transformer->setParameters(new ParameterBag());
$piggyBanks = [];
// enrich
/** @var User $admin */
$admin = auth()->user();
$enrichment = new PiggyBankEnrichment();
$enrichment->setUser($admin);
$collection = $enrichment->enrich($collection);
/** @var PiggyBank $piggy */
foreach ($collection as $piggy) {
$array = $transformer->transform($piggy);
@@ -170,14 +179,6 @@ class IndexController extends Controller
$return[$accountId]['target'] = '0';
$return[$accountId]['to_save'] = '0';
}
// calculate new interesting fields:
// $return[$accountId]['left'] -= $array['current_amount'];
// $return[$accountId]['saved'] += $array['current_amount'];
// $return[$accountId]['target'] += $array['target_amount'];
// $return[$accountId]['to_save'] += ($array['target_amount'] - $array['current_amount']);
// $return['account_name'] = $account['name'];
}
}
@@ -193,7 +194,7 @@ class IndexController extends Controller
// loop all accounts in this piggy bank subtract the current amount from "left to save" in the $accounts array.
/** @var array $piggyAccount */
foreach ($piggyBank['accounts'] as $piggyAccount) {
$accountId = $piggyAccount['id'];
$accountId = $piggyAccount['account_id'];
if (array_key_exists($accountId, $accounts)) {
$accounts[$accountId]['left'] = bcsub((string) $accounts[$accountId]['left'], (string) $piggyAccount['current_amount']);
$accounts[$accountId]['saved'] = bcadd((string) $accounts[$accountId]['saved'], (string) $piggyAccount['current_amount']);

View File

@@ -29,7 +29,9 @@ use FireflyIII\Exceptions\FireflyException;
use FireflyIII\Http\Controllers\Controller;
use FireflyIII\Models\PiggyBank;
use FireflyIII\Repositories\PiggyBank\PiggyBankRepositoryInterface;
use FireflyIII\Support\JsonApi\Enrichments\PiggyBankEnrichment;
use FireflyIII\Transformers\PiggyBankTransformer;
use FireflyIII\User;
use Illuminate\Contracts\View\Factory;
use Illuminate\View\View;
use Symfony\Component\HttpFoundation\ParameterBag;
@@ -75,6 +77,13 @@ class ShowController extends Controller
$parameters = new ParameterBag();
$parameters->set('end', $end);
// enrich
/** @var User $admin */
$admin = auth()->user();
$enrichment = new PiggyBankEnrichment();
$enrichment->setUser($admin);
$piggyBank = $enrichment->enrichSingle($piggyBank);
/** @var PiggyBankTransformer $transformer */
$transformer = app(PiggyBankTransformer::class);
$transformer->setParameters($parameters);

View File

@@ -23,6 +23,7 @@ declare(strict_types=1);
namespace FireflyIII\Http\Controllers;
use FireflyIII\Support\Singleton\PreferencesSingleton;
use JsonException;
use Carbon\Carbon;
use FireflyIII\Enums\AccountTypeEnum;
@@ -269,7 +270,9 @@ class PreferencesController extends Controller
if ($convertToPrimary && !$this->convertToPrimary) {
// set to true!
Log::debug('User sets convertToPrimary to true.');
Preferences::set('convert_to_primary', $convertToPrimary);
Preferences::set('convert_to_primary', true);
$singleton = PreferencesSingleton::getInstance();
$singleton->resetPreferences();
event(new UserGroupChangedPrimaryCurrency(auth()->user()->userGroup));
}
Preferences::set('convert_to_primary', $convertToPrimary);

View File

@@ -34,7 +34,10 @@ use FireflyIII\Models\RecurrenceRepetition;
use FireflyIII\Repositories\Bill\BillRepositoryInterface;
use FireflyIII\Repositories\Budget\BudgetRepositoryInterface;
use FireflyIII\Repositories\Recurring\RecurringRepositoryInterface;
use FireflyIII\Support\Facades\ExpandedForm;
use FireflyIII\Support\JsonApi\Enrichments\RecurringEnrichment;
use FireflyIII\Transformers\RecurrenceTransformer;
use FireflyIII\User;
use Illuminate\Contracts\View\Factory;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
@@ -92,13 +95,20 @@ class EditController extends Controller
throw new FireflyException('This recurring transaction has no meta-data. You will have to delete it and recreate it. Sorry!');
}
// enrich
/** @var User $admin */
$admin = auth()->user();
$enrichment = new RecurringEnrichment();
$enrichment->setUser($admin);
$recurrence = $enrichment->enrichSingle($recurrence);
/** @var RecurrenceTransformer $transformer */
$transformer = app(RecurrenceTransformer::class);
$transformer->setParameters(new ParameterBag());
$array = $transformer->transform($recurrence);
$budgets = app('expandedform')->makeSelectListWithEmpty($this->budgetRepos->getActiveBudgets());
$bills = app('expandedform')->makeSelectListWithEmpty($this->billRepository->getActiveBills());
$budgets = ExpandedForm::makeSelectListWithEmpty($this->budgetRepos->getActiveBudgets());
$bills = ExpandedForm::makeSelectListWithEmpty($this->billRepository->getActiveBills());
/** @var RecurrenceRepetition $repetition */
$repetition = $recurrence->recurrenceRepetitions()->first();

View File

@@ -29,7 +29,9 @@ use FireflyIII\Http\Controllers\Controller;
use FireflyIII\Models\Recurrence;
use FireflyIII\Repositories\Recurring\RecurringRepositoryInterface;
use FireflyIII\Support\Http\Controllers\GetConfigurationData;
use FireflyIII\Support\JsonApi\Enrichments\RecurringEnrichment;
use FireflyIII\Transformers\RecurrenceTransformer;
use FireflyIII\User;
use Illuminate\Contracts\View\Factory;
use Illuminate\Http\Request;
use Illuminate\Pagination\LengthAwarePaginator;
@@ -85,6 +87,13 @@ class IndexController extends Controller
$total = $collection->count();
$recurrences = $collection->slice(($page - 1) * $pageSize, $pageSize);
// enrich
/** @var User $admin */
$admin = auth()->user();
$enrichment = new RecurringEnrichment();
$enrichment->setUser($admin);
$recurrences = $enrichment->enrich($recurrences);
/** @var RecurrenceTransformer $transformer */
$transformer = app(RecurrenceTransformer::class);
$transformer->setParameters(new ParameterBag());

View File

@@ -32,8 +32,10 @@ use FireflyIII\Models\Recurrence;
use FireflyIII\Repositories\Attachment\AttachmentRepositoryInterface;
use FireflyIII\Repositories\Recurring\RecurringRepositoryInterface;
use FireflyIII\Support\Http\Controllers\GetConfigurationData;
use FireflyIII\Support\JsonApi\Enrichments\RecurringEnrichment;
use FireflyIII\Transformers\AttachmentTransformer;
use FireflyIII\Transformers\RecurrenceTransformer;
use FireflyIII\User;
use Illuminate\Contracts\View\Factory;
use Illuminate\View\View;
use Symfony\Component\HttpFoundation\ParameterBag;
@@ -80,6 +82,13 @@ class ShowController extends Controller
{
$repos = app(AttachmentRepositoryInterface::class);
// enrich
/** @var User $admin */
$admin = auth()->user();
$enrichment = new RecurringEnrichment();
$enrichment->setUser($admin);
$recurrence = $enrichment->enrichSingle($recurrence);
/** @var RecurrenceTransformer $transformer */
$transformer = app(RecurrenceTransformer::class);
$transformer->setParameters(new ParameterBag());

View File

@@ -23,6 +23,7 @@ declare(strict_types=1);
namespace FireflyIII\Http\Controllers\Report;
use FireflyIII\Support\Facades\Navigation;
use Throwable;
use Carbon\Carbon;
use FireflyIII\Exceptions\FireflyException;
@@ -290,11 +291,12 @@ class BudgetController extends Controller
$cache->addProperty('budget-period-report');
$cache->addProperty($accounts->pluck('id')->toArray());
if ($cache->has()) {
return $cache->get();
// return $cache->get();
}
$periods = app('navigation')->listOfPeriods($start, $end);
$keyFormat = app('navigation')->preferredCarbonFormat($start, $end);
$periods = Navigation::listOfPeriods($start, $end);
$keyFormat = Navigation::preferredCarbonFormat($start, $end);
// list expenses for budgets in account(s)
$expenses = $this->opsRepository->listExpenses($start, $end, $accounts);
@@ -303,6 +305,17 @@ class BudgetController extends Controller
foreach ($currency['budgets'] as $budget) {
$count = 0;
foreach ($budget['transaction_journals'] as $journal) {
// #10678
// skip transactions between two asset / liability accounts.
if (
in_array($journal['source_account_type'], config('firefly.valid_currency_account_types'), true)
&& in_array($journal['destination_account_type'], config('firefly.valid_currency_account_types'), true)
) {
continue;
}
++$count;
$key = sprintf('%d-%d', $budget['id'], $currency['currency_id']);
$dateKey = $journal['date']->format($keyFormat);

View File

@@ -188,16 +188,7 @@ class ReportController extends Controller
$start->endOfDay(); // end of day so the final balance is at the end of that day.
$end->endOfDay();
app('view')->share(
'subTitle',
trans(
'firefly.report_default',
[
'start' => $start->isoFormat($this->monthAndDayFormat),
'end' => $end->isoFormat($this->monthAndDayFormat),
]
)
);
app('view')->share('subTitle', trans('firefly.report_default', ['start' => $start->isoFormat($this->monthAndDayFormat), 'end' => $end->isoFormat($this->monthAndDayFormat)]));
$generator = ReportGeneratorFactory::reportGenerator('Standard', $start, $end);
$generator->setAccounts($accounts);
@@ -222,16 +213,7 @@ class ReportController extends Controller
$start->endOfDay(); // end of day so the final balance is at the end of that day.
$end->endOfDay();
app('view')->share(
'subTitle',
trans(
'firefly.report_double',
[
'start' => $start->isoFormat($this->monthAndDayFormat),
'end' => $end->isoFormat($this->monthAndDayFormat),
]
)
);
app('view')->share('subTitle', trans('firefly.report_double', ['start' => $start->isoFormat($this->monthAndDayFormat), 'end' => $end->isoFormat($this->monthAndDayFormat)]));
$generator = ReportGeneratorFactory::reportGenerator('Account', $start, $end);
$generator->setAccounts($accounts);

View File

@@ -270,6 +270,7 @@ class CreateController extends Controller
$data = $request->getRuleData();
$rule = $this->ruleRepos->store($data);
session()->flash('success_url', route('rules.select-transactions', [$rule->id]));
session()->flash('success', (string) trans('firefly.stored_new_rule', ['title' => $rule->title]));
app('preferences')->mark();

View File

@@ -252,6 +252,7 @@ class TagController extends Controller
$collector->setRange($start, $end)->setLimit($pageSize)->setPage($page)->withAccountInformation()
->setTag($tag)->withBudgetInformation()->withCategoryInformation()
->withAttachmentInformation()
;
$groups = $collector->getPaginatedGroups();
$groups->setPath($path);
@@ -283,6 +284,7 @@ class TagController extends Controller
$collector = app(GroupCollectorInterface::class);
$collector->setRange($start, $end)->setLimit($pageSize)->setPage($page)->withAccountInformation()
->setTag($tag)->withBudgetInformation()->withCategoryInformation()
->withAttachmentInformation()
;
$groups = $collector->getPaginatedGroups();
$groups->setPath($path);

View File

@@ -25,16 +25,20 @@ declare(strict_types=1);
namespace FireflyIII\Http\Controllers\Transaction;
use FireflyIII\Exceptions\FireflyException;
use FireflyIII\Helpers\Collector\GroupCollectorInterface;
use FireflyIII\Http\Controllers\Controller;
use FireflyIII\Models\TransactionGroup;
use FireflyIII\Models\TransactionJournal;
use FireflyIII\Repositories\AuditLogEntry\ALERepositoryInterface;
use FireflyIII\Repositories\TransactionGroup\TransactionGroupRepositoryInterface;
use FireflyIII\Support\JsonApi\Enrichments\TransactionGroupEnrichment;
use FireflyIII\Transformers\TransactionGroupTransformer;
use FireflyIII\User;
use Illuminate\Contracts\View\Factory;
use Illuminate\Http\JsonResponse;
use Illuminate\View\View;
use Symfony\Component\HttpFoundation\ParameterBag;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
/**
* Class ShowController
@@ -57,7 +61,7 @@ class ShowController extends Controller
$this->repository = app(TransactionGroupRepositoryInterface::class);
$this->aleRepository = app(ALERepositoryInterface::class);
app('view')->share('title', (string) trans('firefly.transactions'));
app('view')->share('title', (string)trans('firefly.transactions'));
app('view')->share('mainTitleIcon', 'fa-exchange');
return $next($request);
@@ -80,38 +84,62 @@ class ShowController extends Controller
*/
public function show(TransactionGroup $transactionGroup)
{
/** @var User $admin */
$admin = auth()->user();
// use new group collector:
/** @var GroupCollectorInterface $collector */
$collector = app(GroupCollectorInterface::class);
$collector->setUser($admin)->setTransactionGroup($transactionGroup)->withAPIInformation();
$selectedGroup = $collector->getGroups()->first();
if (null === $selectedGroup) {
throw new NotFoundHttpException();
}
// enrich
$enrichment = new TransactionGroupEnrichment();
$enrichment->setUser($admin);
$selectedGroup = $enrichment->enrichSingle($selectedGroup);
/** @var null|TransactionJournal $first */
$first = $transactionGroup->transactionJournals()->first(['transaction_journals.*']);
$splits = $transactionGroup->transactionJournals()->count();
$splits = count($selectedGroup['transactions']);
$keys = array_keys($selectedGroup['transactions']);
$first = $selectedGroup['transactions'][array_shift($keys)];
unset($keys);
if (null === $first) {
throw new FireflyException('This transaction is broken :(.');
}
$type = (string) trans(sprintf('firefly.%s', $first->transactionType->type));
$title = 1 === $splits ? $first->description : $transactionGroup->title;
$type = (string)trans(sprintf('firefly.%s', $first['transaction_type_type']));
$title = 1 === $splits ? $first['description'] : $selectedGroup['title'];
$subTitle = sprintf('%s: "%s"', $type, $title);
// enrich
$enrichment = new TransactionGroupEnrichment();
$enrichment->setUser($admin);
$selectedGroup = $enrichment->enrichSingle($selectedGroup);
/** @var TransactionGroupTransformer $transformer */
$transformer = app(TransactionGroupTransformer::class);
$transformer->setParameters(new ParameterBag());
$groupArray = $transformer->transformObject($transactionGroup);
// do some calculations:
$amounts = $this->getAmounts($groupArray);
$accounts = $this->getAccounts($groupArray);
$amounts = $this->getAmounts($selectedGroup);
$accounts = $this->getAccounts($selectedGroup);
foreach (array_keys($groupArray['transactions']) as $index) {
$groupArray['transactions'][$index]['tags'] = $this->repository->getTagObjects(
(int) $groupArray['transactions'][$index]['transaction_journal_id']
);
foreach (array_keys($selectedGroup['transactions']) as $index) {
$selectedGroup['transactions'][$index]['tags'] = $this->repository->getTagObjects((int)$selectedGroup['transactions'][$index]['transaction_journal_id']);
}
// get audit log entries:
$groupLogEntries = $this->aleRepository->getForObject($transactionGroup);
$logEntries = [];
foreach ($transactionGroup->transactionJournals as $journal) {
$logEntries[$journal->id] = $this->aleRepository->getForObject($journal);
foreach ($selectedGroup['transactions'] as $journal) {
$logEntries[$journal['transaction_journal_id']] = $this->aleRepository->getForId(TransactionJournal::class, $journal['transaction_journal_id']);
}
$events = $this->repository->getPiggyEvents($transactionGroup);
@@ -129,6 +157,7 @@ class ShowController extends Controller
'groupLogEntries',
'subTitle',
'splits',
'selectedGroup',
'groupArray',
'events',
'attachments',
@@ -142,34 +171,38 @@ class ShowController extends Controller
{
$amounts = [];
foreach ($group['transactions'] as $transaction) {
// add normal amount:
$symbol = $transaction['currency_symbol'];
if (!array_key_exists($symbol, $amounts)) {
$amounts[$symbol] = [
'amount' => '0',
'symbol' => $symbol,
'decimal_places' => $transaction['currency_decimal_places'],
];
}
$amounts[$symbol]['amount'] = bcadd($amounts[$symbol]['amount'], (string) $transaction['amount']);
if (null !== $transaction['foreign_amount'] && '' !== $transaction['foreign_amount']
&& 0 !== bccomp(
'0',
(string) $transaction['foreign_amount']
)) {
$amounts[$symbol] ??= [
'amount' => '0',
'symbol' => $symbol,
'decimal_places' => $transaction['currency_decimal_places'],
];
$amounts[$symbol]['amount'] = bcadd($amounts[$symbol]['amount'], (string)$transaction['amount']);
// add foreign amount:
if (null !== $transaction['foreign_amount'] && '' !== $transaction['foreign_amount'] && 0 !== bccomp('0', (string)$transaction['foreign_amount'])) {
// same for foreign currency:
$foreignSymbol = $transaction['foreign_currency_symbol'];
if (!array_key_exists($foreignSymbol, $amounts)) {
$amounts[$foreignSymbol] = [
'amount' => '0',
'symbol' => $foreignSymbol,
'decimal_places' => $transaction['foreign_currency_decimal_places'],
];
}
$amounts[$foreignSymbol]['amount'] = bcadd(
$amounts[$foreignSymbol]['amount'],
(string) $transaction['foreign_amount']
);
$amounts[$foreignSymbol] ??= [
'amount' => '0',
'symbol' => $foreignSymbol,
'decimal_places' => $transaction['foreign_currency_decimal_places'],
];
$amounts[$foreignSymbol]['amount'] = bcadd($amounts[$foreignSymbol]['amount'], (string)$transaction['foreign_amount']);
}
// add primary currency amount
if (null !== $transaction['pc_amount'] && $transaction['currency_id'] !== $this->primaryCurrency->id) {
// same for foreign currency:
$primarySymbol = $this->primaryCurrency->symbol;
$amounts[$primarySymbol] ??= [
'amount' => '0',
'symbol' => $this->primaryCurrency->symbol,
'decimal_places' => $this->primaryCurrency->decimal_places,
];
$amounts[$primarySymbol]['amount'] = bcadd($amounts[$primarySymbol]['amount'], (string)$transaction['pc_amount']);
}
}
return $amounts;
@@ -184,16 +217,16 @@ class ShowController extends Controller
foreach ($group['transactions'] as $transaction) {
$accounts['source'][] = [
'type' => $transaction['source_type'],
'id' => $transaction['source_id'],
'name' => $transaction['source_name'],
'iban' => $transaction['source_iban'],
'type' => $transaction['source_account_type'],
'id' => $transaction['source_account_id'],
'name' => $transaction['source_account_name'],
'iban' => $transaction['source_account_iban'],
];
$accounts['destination'][] = [
'type' => $transaction['destination_type'],
'id' => $transaction['destination_id'],
'name' => $transaction['destination_name'],
'iban' => $transaction['destination_iban'],
'type' => $transaction['destination_account_type'],
'id' => $transaction['destination_account_id'],
'name' => $transaction['destination_account_name'],
'iban' => $transaction['destination_account_iban'],
];
}

View File

@@ -25,13 +25,18 @@ declare(strict_types=1);
namespace FireflyIII\Jobs;
use Carbon\Carbon;
use FireflyIII\Events\WarnUserAboutBill;
use FireflyIII\Events\Model\Bill\WarnUserAboutBill;
use FireflyIII\Events\Model\Bill\WarnUserAboutOverdueSubscriptions;
use FireflyIII\Models\Bill;
use FireflyIII\Support\Facades\Navigation;
use FireflyIII\Support\JsonApi\Enrichments\SubscriptionEnrichment;
use FireflyIII\User;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Log;
/**
* Class WarnAboutBills
@@ -63,7 +68,7 @@ class WarnAboutBills implements ShouldQueue
$this->force = false;
app('log')->debug(sprintf('Created new WarnAboutBills("%s")', $this->date->format('Y-m-d')));
Log::debug(sprintf('Created new WarnAboutBills("%s")', $this->date->format('Y-m-d')));
}
/**
@@ -71,36 +76,42 @@ class WarnAboutBills implements ShouldQueue
*/
public function handle(): void
{
app('log')->debug(sprintf('Now at start of WarnAboutBills() job for %s.', $this->date->format('D d M Y')));
$bills = Bill::all();
Log::debug(sprintf('Now at start of WarnAboutBills() job for %s.', $this->date->format('D d M Y')));
foreach (User::all() as $user) {
$bills = $user->bills()->where('active', true)->get();
$overdue = [];
/** @var Bill $bill */
foreach ($bills as $bill) {
app('log')->debug(sprintf('Now checking bill #%d ("%s")', $bill->id, $bill->name));
if ($this->hasDateFields($bill)) {
if ($this->needsWarning($bill, 'end_date')) {
$this->sendWarning($bill, 'end_date');
/** @var Bill $bill */
foreach ($bills as $bill) {
Log::debug(sprintf('Now checking bill #%d ("%s")', $bill->id, $bill->name));
$dates = $this->getDates($bill);
if ($this->needsOverdueAlert($dates)) {
$overdue[] = ['bill' => $bill, 'dates' => $dates];
}
if ($this->needsWarning($bill, 'extension_date')) {
$this->sendWarning($bill, 'extension_date');
if ($this->hasDateFields($bill)) {
if ($this->needsWarning($bill, 'end_date')) {
$this->sendWarning($bill, 'end_date');
}
if ($this->needsWarning($bill, 'extension_date')) {
$this->sendWarning($bill, 'extension_date');
}
}
}
$this->sendOverdueAlerts($user, $overdue);
}
app('log')->debug('Done with handle()');
Log::debug('Done with handle()');
// clear cache:
app('preferences')->mark();
}
private function hasDateFields(Bill $bill): bool
{
if (false === $bill->active) {
app('log')->debug('Bill is not active.');
Log::debug('Bill is not active.');
return false;
}
if (null === $bill->end_date && null === $bill->extension_date) {
app('log')->debug('Bill has no date fields.');
Log::debug('Bill has no date fields.');
return false;
}
@@ -115,7 +126,7 @@ class WarnAboutBills implements ShouldQueue
}
$diff = $this->getDiff($bill, $field);
$list = config('firefly.bill_reminder_periods');
app('log')->debug(sprintf('Difference in days for field "%s" ("%s") is %d day(s)', $field, $bill->{$field}->format('Y-m-d'), $diff));
Log::debug(sprintf('Difference in days for field "%s" ("%s") is %d day(s)', $field, $bill->{$field}->format('Y-m-d'), $diff));
if (in_array($diff, $list, true)) {
return true;
}
@@ -128,13 +139,13 @@ class WarnAboutBills implements ShouldQueue
$today = clone $this->date;
$carbon = clone $bill->{$field};
return (int) $today->diffInDays($carbon);
return (int)$today->diffInDays($carbon);
}
private function sendWarning(Bill $bill, string $field): void
{
$diff = $this->getDiff($bill, $field);
app('log')->debug('Will now send warning!');
Log::debug('Will now send warning!');
event(new WarnUserAboutBill($bill, $field, $diff));
}
@@ -149,4 +160,49 @@ class WarnAboutBills implements ShouldQueue
{
$this->force = $force;
}
private function getDates(Bill $bill): array
{
$start = clone $this->date;
$start = Navigation::startOfPeriod($start, $bill->repeat_freq);
$end = clone $start;
$end = Navigation::endOfPeriod($end, $bill->repeat_freq);
$enrichment = new SubscriptionEnrichment();
$enrichment->setUser($bill->user);
$enrichment->setStart($start);
$enrichment->setEnd($end);
$single = $enrichment->enrichSingle($bill);
return [
'pay_dates' => $single->meta['pay_dates'] ?? [],
'paid_dates' => $single->meta['paid_dates'] ?? [],
];
}
private function needsOverdueAlert(array $dates): bool
{
$count = count($dates['pay_dates']) - count($dates['paid_dates']);
if (0 === $count || 0 === count($dates['pay_dates'])) {
return false;
}
// the earliest date in the list of pay dates must be 48hrs or more ago.
$earliest = new Carbon($dates['pay_dates'][0]);
$earliest->startOfDay();
Log::debug(sprintf('Earliest expected pay date is %s', $earliest->toAtomString()));
$diff = $earliest->diffInDays($this->date);
Log::debug(sprintf('Difference in days is %s', $diff));
if ($diff < 2) {
return false;
}
return true;
}
private function sendOverdueAlerts(User $user, array $overdue): void
{
if (count($overdue) > 0) {
Log::debug(sprintf('Will now send warning about overdue bill for user #%d.', $user->id));
event(new WarnUserAboutOverdueSubscriptions($user, $overdue));
}
}
}

View File

@@ -0,0 +1,113 @@
<?php
declare(strict_types=1);
namespace FireflyIII\Notifications\User;
use Carbon\Carbon;
use FireflyIII\Notifications\ReturnsAvailableChannels;
use FireflyIII\Notifications\ReturnsSettings;
use FireflyIII\User;
use Illuminate\Bus\Queueable;
use Illuminate\Notifications\Messages\MailMessage;
use Illuminate\Notifications\Messages\SlackMessage;
use Illuminate\Notifications\Notification;
use NotificationChannels\Pushover\PushoverMessage;
class SubscriptionsOverdueReminder extends Notification
{
use Queueable;
public function __construct(private array $overdue) {}
/**
* @SuppressWarnings("PHPMD.UnusedFormalParameter")
*/
public function toArray(User $notifiable): array
{
return [
];
}
/**
* @SuppressWarnings("PHPMD.UnusedFormalParameter")
*/
public function toMail(User $notifiable): MailMessage
{
// format the data
$info = [];
$count = 0;
foreach ($this->overdue as $item) {
$current = [
'bill' => $item['bill'],
];
$current['pay_dates'] = array_map(
static function (string $date): string {
return new Carbon($date)->isoFormat((string)trans('config.month_and_day_moment_js'));
},
$item['dates']['pay_dates']
);
$info[] = $current;
++$count;
}
return new MailMessage()
->markdown('emails.subscriptions-overdue-warning', ['info' => $info, 'count' => $count])
->subject($this->getSubject())
;
}
private function getSubject(): string
{
if (count($this->overdue) > 1) {
return (string)trans('email.subscriptions_overdue_subject_multi', ['count' => count($this->overdue)]);
}
return (string)trans('email.subscriptions_overdue_subject_single');
}
public function toNtfy(User $notifiable): Message
{
$settings = ReturnsSettings::getSettings('ntfy', 'user', $notifiable);
$message = new Message();
$message->topic($settings['ntfy_topic']);
$message->title($this->getSubject());
$message->body((string)trans('email.bill_warning_please_action'));
return $message;
}
/**
* @SuppressWarnings("PHPMD.UnusedFormalParameter")
*/
public function toPushover(User $notifiable): PushoverMessage
{
return PushoverMessage::create((string)trans('email.bill_warning_please_action'))
->title($this->getSubject())
;
}
/**
* @SuppressWarnings("PHPMD.UnusedFormalParameter")
*/
public function toSlack(User $notifiable): SlackMessage
{
$url = route('bills.index');
return new SlackMessage()
->warning()
->attachment(static function ($attachment) use ($url): void {
$attachment->title((string)trans('firefly.visit_bills'), $url);
})
->content($this->getSubject())
;
}
/**
* @SuppressWarnings("PHPMD.UnusedFormalParameter")
*/
public function via(User $notifiable): array
{
return ReturnsAvailableChannels::returnChannels('user', $notifiable);
}
}

View File

@@ -27,6 +27,8 @@ use FireflyIII\Events\ActuallyLoggedIn;
use FireflyIII\Events\Admin\InvitationCreated;
use FireflyIII\Events\DestroyedTransactionGroup;
use FireflyIII\Events\DetectedNewIPAddress;
use FireflyIII\Events\Model\Bill\WarnUserAboutBill;
use FireflyIII\Events\Model\Bill\WarnUserAboutOverdueSubscriptions;
use FireflyIII\Events\Model\BudgetLimit\Created;
use FireflyIII\Events\Model\BudgetLimit\Deleted;
use FireflyIII\Events\Model\BudgetLimit\Updated;
@@ -58,7 +60,6 @@ use FireflyIII\Events\TriggeredAuditLog;
use FireflyIII\Events\UpdatedAccount;
use FireflyIII\Events\UpdatedTransactionGroup;
use FireflyIII\Events\UserChangedEmail;
use FireflyIII\Events\WarnUserAboutBill;
use FireflyIII\Handlers\Observer\AccountObserver;
use FireflyIII\Handlers\Observer\AttachmentObserver;
use FireflyIII\Handlers\Observer\AutoBudgetObserver;
@@ -114,150 +115,153 @@ class EventServiceProvider extends ServiceProvider
protected $listen
= [
// is a User related event.
RegisteredUser::class => [
RegisteredUser::class => [
'FireflyIII\Handlers\Events\UserEventHandler@sendRegistrationMail',
'FireflyIII\Handlers\Events\UserEventHandler@sendAdminRegistrationNotification',
'FireflyIII\Handlers\Events\UserEventHandler@attachUserRole',
'FireflyIII\Handlers\Events\UserEventHandler@createGroupMembership',
'FireflyIII\Handlers\Events\UserEventHandler@createExchangeRates',
],
UserAttemptedLogin::class => [
UserAttemptedLogin::class => [
'FireflyIII\Handlers\Events\UserEventHandler@sendLoginAttemptNotification',
],
// is a User related event.
Login::class => [
Login::class => [
'FireflyIII\Handlers\Events\UserEventHandler@checkSingleUserIsAdmin',
'FireflyIII\Handlers\Events\UserEventHandler@demoUserBackToEnglish',
],
ActuallyLoggedIn::class => [
ActuallyLoggedIn::class => [
'FireflyIII\Handlers\Events\UserEventHandler@storeUserIPAddress',
],
DetectedNewIPAddress::class => [
DetectedNewIPAddress::class => [
'FireflyIII\Handlers\Events\UserEventHandler@notifyNewIPAddress',
],
RequestedVersionCheckStatus::class => [
RequestedVersionCheckStatus::class => [
'FireflyIII\Handlers\Events\VersionCheckEventHandler@checkForUpdates',
],
RequestedReportOnJournals::class => [
RequestedReportOnJournals::class => [
'FireflyIII\Handlers\Events\AutomationHandler@reportJournals',
],
// is a User related event.
RequestedNewPassword::class => [
RequestedNewPassword::class => [
'FireflyIII\Handlers\Events\UserEventHandler@sendNewPassword',
],
UserTestNotificationChannel::class => [
UserTestNotificationChannel::class => [
'FireflyIII\Handlers\Events\UserEventHandler@sendTestNotification',
],
// is a User related event.
UserChangedEmail::class => [
UserChangedEmail::class => [
'FireflyIII\Handlers\Events\UserEventHandler@sendEmailChangeConfirmMail',
'FireflyIII\Handlers\Events\UserEventHandler@sendEmailChangeUndoMail',
],
// admin related
OwnerTestNotificationChannel::class => [
OwnerTestNotificationChannel::class => [
'FireflyIII\Handlers\Events\AdminEventHandler@sendTestNotification',
],
NewVersionAvailable::class => [
NewVersionAvailable::class => [
'FireflyIII\Handlers\Events\AdminEventHandler@sendNewVersion',
],
InvitationCreated::class => [
InvitationCreated::class => [
'FireflyIII\Handlers\Events\AdminEventHandler@sendInvitationNotification',
'FireflyIII\Handlers\Events\UserEventHandler@sendRegistrationInvite',
],
UnknownUserAttemptedLogin::class => [
UnknownUserAttemptedLogin::class => [
'FireflyIII\Handlers\Events\AdminEventHandler@sendLoginAttemptNotification',
],
// is a Transaction Journal related event.
StoredTransactionGroup::class => [
StoredTransactionGroup::class => [
'FireflyIII\Handlers\Events\StoredGroupEventHandler@runAllHandlers',
],
// is a Transaction Journal related event.
UpdatedTransactionGroup::class => [
UpdatedTransactionGroup::class => [
'FireflyIII\Handlers\Events\UpdatedGroupEventHandler@runAllHandlers',
],
DestroyedTransactionGroup::class => [
DestroyedTransactionGroup::class => [
'FireflyIII\Handlers\Events\DestroyedGroupEventHandler@runAllHandlers',
],
// API related events:
AccessTokenCreated::class => [
AccessTokenCreated::class => [
'FireflyIII\Handlers\Events\APIEventHandler@accessTokenCreated',
],
// Webhook related event:
RequestedSendWebhookMessages::class => [
RequestedSendWebhookMessages::class => [
'FireflyIII\Handlers\Events\WebhookEventHandler@sendWebhookMessages',
],
// account related events:
StoredAccount::class => [
StoredAccount::class => [
'FireflyIII\Handlers\Events\StoredAccountEventHandler@recalculateCredit',
],
UpdatedAccount::class => [
UpdatedAccount::class => [
'FireflyIII\Handlers\Events\UpdatedAccountEventHandler@recalculateCredit',
],
// bill related events:
WarnUserAboutBill::class => [
WarnUserAboutBill::class => [
'FireflyIII\Handlers\Events\BillEventHandler@warnAboutBill',
],
WarnUserAboutOverdueSubscriptions::class => [
'FireflyIII\Handlers\Events\BillEventHandler@warnAboutOverdueSubscriptions',
],
// audit log events:
TriggeredAuditLog::class => [
TriggeredAuditLog::class => [
'FireflyIII\Handlers\Events\AuditEventHandler@storeAuditEvent',
],
// piggy bank related events:
ChangedAmount::class => [
ChangedAmount::class => [
'FireflyIII\Handlers\Events\Model\PiggyBankEventHandler@changePiggyAmount',
],
ChangedName::class => [
ChangedName::class => [
'FireflyIII\Handlers\Events\Model\PiggyBankEventHandler@changedPiggyBankName',
],
// budget related events: CRUD budget limit
Created::class => [
Created::class => [
'FireflyIII\Handlers\Events\Model\BudgetLimitHandler@created',
],
Updated::class => [
Updated::class => [
'FireflyIII\Handlers\Events\Model\BudgetLimitHandler@updated',
],
Deleted::class => [
Deleted::class => [
'FireflyIII\Handlers\Events\Model\BudgetLimitHandler@deleted',
],
// rule actions
RuleActionFailedOnArray::class => [
RuleActionFailedOnArray::class => [
'FireflyIII\Handlers\Events\Model\RuleHandler@ruleActionFailedOnArray',
],
RuleActionFailedOnObject::class => [
RuleActionFailedOnObject::class => [
'FireflyIII\Handlers\Events\Model\RuleHandler@ruleActionFailedOnObject',
],
// security related
EnabledMFA::class => [
EnabledMFA::class => [
'FireflyIII\Handlers\Events\Security\MFAHandler@sendMFAEnabledMail',
],
DisabledMFA::class => [
DisabledMFA::class => [
'FireflyIII\Handlers\Events\Security\MFAHandler@sendMFADisabledMail',
],
MFANewBackupCodes::class => [
MFANewBackupCodes::class => [
'FireflyIII\Handlers\Events\Security\MFAHandler@sendNewMFABackupCodesMail',
],
MFAUsedBackupCode::class => [
MFAUsedBackupCode::class => [
'FireflyIII\Handlers\Events\Security\MFAHandler@sendUsedBackupCodeMail',
],
MFABackupFewLeft::class => [
MFABackupFewLeft::class => [
'FireflyIII\Handlers\Events\Security\MFAHandler@sendBackupFewLeftMail',
],
MFABackupNoLeft::class => [
MFABackupNoLeft::class => [
'FireflyIII\Handlers\Events\Security\MFAHandler@sendBackupNoLeftMail',
],
MFAManyFailedAttempts::class => [
MFAManyFailedAttempts::class => [
'FireflyIII\Handlers\Events\Security\MFAHandler@sendMFAFailedAttemptsMail',
],
// preferences
UserGroupChangedPrimaryCurrency::class => [
UserGroupChangedPrimaryCurrency::class => [
'FireflyIII\Handlers\Events\PreferencesEventHandler@resetPrimaryCurrencyAmounts',
],
];

View File

@@ -562,7 +562,13 @@ class AccountRepository implements AccountRepositoryInterface, UserGroupInterfac
'foreign_currencies.decimal_places as foreign_currency_decimal_places',
// fields
'transaction_journals.date', 'transaction_types.type', 'transaction_journals.transaction_currency_id', 'transactions.amount'])
'transaction_journals.date',
'transaction_types.type',
'transaction_journals.transaction_currency_id',
'transactions.amount',
'transactions.native_amount as pc_amount',
'transactions.foreign_amount',
])
->toArray()
;

View File

@@ -50,10 +50,10 @@ class AccountTasker implements AccountTaskerInterface, UserGroupInterface
$yesterday = clone $start;
$yesterday->subDay()->endOfDay(); // exactly up until $start but NOT including.
$end->endOfDay(); // needs to be end of day to be correct.
Log::debug(sprintf('getAccountReport: finalAccountsBalance("%s")', $yesterday->format('Y-m-d H:i:s')));
Log::debug(sprintf('getAccountReport: finalAccountsBalance("%s")', $end->format('Y-m-d H:i:s')));
$startSet = Steam::finalAccountsBalance($accounts, $yesterday);
$endSet = Steam::finalAccountsBalance($accounts, $end);
Log::debug(sprintf('getAccountReport: accountsBalancesOptimized("%s")', $yesterday->format('Y-m-d H:i:s')));
Log::debug(sprintf('getAccountReport: accountsBalancesOptimized("%s")', $end->format('Y-m-d H:i:s')));
$startSet = Steam::accountsBalancesOptimized($accounts, $yesterday);
$endSet = Steam::accountsBalancesOptimized($accounts, $end);
Log::debug('Start of accountreport');
/** @var AccountRepositoryInterface $repository */

View File

@@ -52,4 +52,10 @@ class ALERepository implements ALERepositoryInterface
return $auditLogEntry;
}
public function getForId(string $model, int $modelId): Collection
{
// all Models have an ID.
return AuditLogEntry::where('auditable_id', $modelId)->where('auditable_type', $model)->get();
}
}

View File

@@ -46,5 +46,7 @@ interface ALERepositoryInterface
{
public function getForObject(Model $model): Collection;
public function getForId(string $model, int $modelId): Collection;
public function store(array $data): AuditLogEntry;
}

View File

@@ -202,8 +202,11 @@ class OperationsRepository implements OperationsRepositoryInterface, UserGroupIn
'amount' => $amount,
'destination_account_id' => $journal['destination_account_id'],
'destination_account_name' => $journal['destination_account_name'],
'destination_account_type' => $journal['destination_account_type'],
'currency_id' => $journalCurrencyId,
'source_account_id' => $journal['source_account_id'],
'source_account_name' => $journal['source_account_name'],
'source_account_type' => $journal['source_account_type'],
'category_name' => $journal['category_name'],
'description' => $journal['description'],
'transaction_group_id' => $journal['transaction_group_id'],

View File

@@ -32,6 +32,7 @@ use GuzzleHttp\Client;
use GuzzleHttp\Exception\ConnectException;
use GuzzleHttp\Exception\GuzzleException;
use GuzzleHttp\Exception\RequestException;
use Illuminate\Support\Facades\Log;
use JsonException;
use function Safe\json_encode;
@@ -65,9 +66,9 @@ class StandardWebhookSender implements WebhookSenderInterface
try {
$signature = $signatureGenerator->generate($this->message);
} catch (FireflyException $e) {
app('log')->error('Did not send message because of a Firefly III Exception.');
app('log')->error($e->getMessage());
app('log')->error($e->getTraceAsString());
Log::error('Did not send message because of a Firefly III Exception.');
Log::error($e->getMessage());
Log::error($e->getTraceAsString());
$attempt = new WebhookAttempt();
$attempt->webhookMessage()->associate($this->message);
$attempt->status_code = 0;
@@ -80,14 +81,14 @@ class StandardWebhookSender implements WebhookSenderInterface
return;
}
app('log')->debug(sprintf('Trying to send webhook message #%d', $this->message->id));
Log::debug(sprintf('Trying to send webhook message #%d', $this->message->id));
try {
$json = json_encode($this->message->message, JSON_THROW_ON_ERROR);
} catch (JsonException $e) {
app('log')->error('Did not send message because of a JSON error.');
app('log')->error($e->getMessage());
app('log')->error($e->getTraceAsString());
Log::error('Did not send message because of a JSON error.');
Log::error($e->getMessage());
Log::error($e->getTraceAsString());
$attempt = new WebhookAttempt();
$attempt->webhookMessage()->associate($this->message);
$attempt->status_code = 0;
@@ -115,9 +116,9 @@ class StandardWebhookSender implements WebhookSenderInterface
try {
$res = $client->request('POST', $this->message->webhook->url, $options);
} catch (ConnectException|RequestException $e) {
app('log')->error('The webhook could NOT be submitted but Firefly III caught the error below.');
app('log')->error($e->getMessage());
app('log')->error($e->getTraceAsString());
Log::error('The webhook could NOT be submitted but Firefly III caught the error below.');
Log::error($e->getMessage());
Log::error($e->getTraceAsString());
$logs = sprintf("%s\n%s", $e->getMessage(), $e->getTraceAsString());
@@ -130,9 +131,9 @@ class StandardWebhookSender implements WebhookSenderInterface
$attempt->status_code = 0;
if (method_exists($e, 'hasResponse') && method_exists($e, 'getResponse')) {
$attempt->status_code = $e->hasResponse() ? $e->getResponse()->getStatusCode() : 0;
app('log')->error(sprintf('The status code of the error response is: %d', $attempt->status_code));
Log::error(sprintf('The status code of the error response is: %d', $attempt->status_code));
$body = (string) ($e->hasResponse() ? $e->getResponse()->getBody() : '');
app('log')->error(sprintf('The body of the error response is: %s', $body));
Log::error(sprintf('The body of the error response is: %s', $body));
}
$attempt->logs = $logs;
$attempt->save();
@@ -142,9 +143,9 @@ class StandardWebhookSender implements WebhookSenderInterface
$this->message->sent = true;
$this->message->save();
app('log')->debug(sprintf('Webhook message #%d was sent. Status code %d', $this->message->id, $res->getStatusCode()));
app('log')->debug(sprintf('Webhook request body size: %d bytes', strlen($json)));
app('log')->debug(sprintf('Response body: %s', $res->getBody()));
Log::debug(sprintf('Webhook message #%d was sent. Status code %d', $this->message->id, $res->getStatusCode()));
Log::debug(sprintf('Webhook request body size: %d bytes', strlen($json)));
Log::debug(sprintf('Response body: %s', $res->getBody()));
}
public function setMessage(WebhookMessage $message): void

View File

@@ -29,9 +29,9 @@ use FireflyIII\Models\TransactionCurrency;
use FireflyIII\Models\TransactionJournal;
use FireflyIII\Models\UserGroup;
use FireflyIII\Support\Facades\Preferences;
use FireflyIII\Support\Singleton\PreferencesSingleton;
use FireflyIII\User;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Log;
use NumberFormatter;
/**
@@ -116,11 +116,28 @@ class Amount
public function convertToPrimary(?User $user = null): bool
{
$instance = PreferencesSingleton::getInstance();
if (!$user instanceof User) {
return true === Preferences::get('convert_to_primary', false)->data && true === config('cer.enabled');
$pref = $instance->getPreference('convert_to_primary_no_user');
if (null === $pref) {
$res = true === Preferences::get('convert_to_primary', false)->data && true === config('cer.enabled');
$instance->setPreference('convert_to_primary_no_user', $res);
return $res;
}
return $pref;
}
$key = sprintf('convert_to_primary_%d', $user->id);
$pref = $instance->getPreference($key);
if (null === $pref) {
$res = true === Preferences::getForUser($user, 'convert_to_primary', false)->data && true === config('cer.enabled');
$instance->setPreference($key, $res);
return $res;
}
return true === Preferences::getForUser($user, 'convert_to_primary', false)->data && true === config('cer.enabled');
return $pref;
}
public function getPrimaryCurrency(): TransactionCurrency

View File

@@ -28,6 +28,7 @@ use Carbon\Carbon;
use FireflyIII\Jobs\CreateAutoBudgetLimits;
use FireflyIII\Models\Configuration;
use FireflyIII\Support\Facades\FireflyConfig;
use Illuminate\Support\Facades\Log;
/**
* Class AutoBudgetCronjob
@@ -42,22 +43,22 @@ class AutoBudgetCronjob extends AbstractCronjob
$diff = Carbon::now()->getTimestamp() - $lastTime;
$diffForHumans = today(config('app.timezone'))->diffForHumans(Carbon::createFromTimestamp($lastTime), null, true);
if (0 === $lastTime) {
app('log')->info('Auto budget cron-job has never fired before.');
Log::info('Auto budget cron-job has never fired before.');
}
// less than half a day ago:
if ($lastTime > 0 && $diff <= 43200) {
app('log')->info(sprintf('It has been %s since the auto budget cron-job has fired.', $diffForHumans));
Log::info(sprintf('It has been %s since the auto budget cron-job has fired.', $diffForHumans));
if (false === $this->force) {
app('log')->info('The auto budget cron-job will not fire now.');
Log::info('The auto budget cron-job will not fire now.');
$this->message = sprintf('It has been %s since the auto budget cron-job has fired. It will not fire now.', $diffForHumans);
return;
}
app('log')->info('Execution of the auto budget cron-job has been FORCED.');
Log::info('Execution of the auto budget cron-job has been FORCED.');
}
if ($lastTime > 0 && $diff > 43200) {
app('log')->info(sprintf('It has been %s since the auto budget cron-job has fired. It will fire now!', $diffForHumans));
Log::info(sprintf('It has been %s since the auto budget cron-job has fired. It will fire now!', $diffForHumans));
}
$this->fireAutoBudget();
@@ -66,7 +67,7 @@ class AutoBudgetCronjob extends AbstractCronjob
private function fireAutoBudget(): void
{
app('log')->info(sprintf('Will now fire auto budget cron job task for date "%s".', $this->date->format('Y-m-d')));
Log::info(sprintf('Will now fire auto budget cron job task for date "%s".', $this->date->format('Y-m-d')));
/** @var CreateAutoBudgetLimits $job */
$job = app(CreateAutoBudgetLimits::class, [$this->date]);
@@ -80,6 +81,6 @@ class AutoBudgetCronjob extends AbstractCronjob
$this->message = 'Auto-budget cron job fired successfully.';
FireflyConfig::set('last_ab_job', (int) $this->date->format('U'));
app('log')->info('Done with auto budget cron job task.');
Log::info('Done with auto budget cron job task.');
}
}

View File

@@ -28,6 +28,8 @@ use Carbon\Carbon;
use FireflyIII\Exceptions\FireflyException;
use FireflyIII\Jobs\WarnAboutBills;
use FireflyIII\Models\Configuration;
use FireflyIII\Support\Facades\FireflyConfig;
use Illuminate\Support\Facades\Log;
/**
* Class BillWarningCronjob
@@ -39,22 +41,22 @@ class BillWarningCronjob extends AbstractCronjob
*/
public function fire(): void
{
app('log')->debug(sprintf('Now in %s', __METHOD__));
Log::debug(sprintf('Now in %s', __METHOD__));
/** @var Configuration $config */
$config = app('fireflyconfig')->get('last_bw_job', 0);
$config = FireflyConfig::get('last_bw_job', 0);
$lastTime = (int) $config->data;
$diff = Carbon::now()->getTimestamp() - $lastTime;
$diffForHumans = today(config('app.timezone'))->diffForHumans(Carbon::createFromTimestamp($lastTime), null, true);
if (0 === $lastTime) {
app('log')->info('The bill notification cron-job has never fired before.');
Log::info('The bill notification cron-job has never fired before.');
}
// less than half a day ago:
if ($lastTime > 0 && $diff <= 43200) {
app('log')->info(sprintf('It has been %s since the bill notification cron-job has fired.', $diffForHumans));
Log::info(sprintf('It has been %s since the bill notification cron-job has fired.', $diffForHumans));
if (false === $this->force) {
app('log')->info('The cron-job will not fire now.');
Log::info('The cron-job will not fire now.');
$this->message = sprintf('It has been %s since the bill notification cron-job has fired. It will not fire now.', $diffForHumans);
$this->jobFired = false;
$this->jobErrored = false;
@@ -63,11 +65,11 @@ class BillWarningCronjob extends AbstractCronjob
return;
}
app('log')->info('Execution of the bill notification cron-job has been FORCED.');
Log::info('Execution of the bill notification cron-job has been FORCED.');
}
if ($lastTime > 0 && $diff > 43200) {
app('log')->info(sprintf('It has been %s since the bill notification cron-job has fired. It will fire now!', $diffForHumans));
Log::info(sprintf('It has been %s since the bill notification cron-job has fired. It will fire now!', $diffForHumans));
}
$this->fireWarnings();
@@ -77,7 +79,7 @@ class BillWarningCronjob extends AbstractCronjob
private function fireWarnings(): void
{
app('log')->info(sprintf('Will now fire bill notification job task for date "%s".', $this->date->format('Y-m-d H:i:s')));
Log::info(sprintf('Will now fire bill notification job task for date "%s".', $this->date->format('Y-m-d H:i:s')));
/** @var WarnAboutBills $job */
$job = app(WarnAboutBills::class);
@@ -91,8 +93,8 @@ class BillWarningCronjob extends AbstractCronjob
$this->jobSucceeded = true;
$this->message = 'Bill notification cron job fired successfully.';
app('fireflyconfig')->set('last_bw_job', (int) $this->date->format('U'));
app('log')->info(sprintf('Marked the last time this job has run as "%s" (%d)', $this->date->format('Y-m-d H:i:s'), (int) $this->date->format('U')));
app('log')->info('Done with bill notification cron job task.');
FireflyConfig::set('last_bw_job', (int) $this->date->format('U'));
Log::info(sprintf('Marked the last time this job has run as "%s" (%d)', $this->date->format('Y-m-d H:i:s'), (int) $this->date->format('U')));
Log::info('Done with bill notification cron job task.');
}
}

View File

@@ -27,6 +27,7 @@ namespace FireflyIII\Support\Cronjobs;
use Carbon\Carbon;
use FireflyIII\Jobs\DownloadExchangeRates;
use FireflyIII\Models\Configuration;
use FireflyIII\Support\Facades\FireflyConfig;
use Illuminate\Support\Facades\Log;
/**
@@ -37,7 +38,7 @@ class ExchangeRatesCronjob extends AbstractCronjob
public function fire(): void
{
/** @var Configuration $config */
$config = app('fireflyconfig')->get('last_cer_job', 0);
$config = FireflyConfig::get('last_cer_job', 0);
$lastTime = (int) $config->data;
$diff = Carbon::now()->getTimestamp() - $lastTime;
$diffForHumans = today(config('app.timezone'))->diffForHumans(Carbon::createFromTimestamp($lastTime), null, true);
@@ -80,7 +81,7 @@ class ExchangeRatesCronjob extends AbstractCronjob
$this->jobSucceeded = true;
$this->message = 'Exchange rates cron job fired successfully.';
app('fireflyconfig')->set('last_cer_job', (int) $this->date->format('U'));
FireflyConfig::set('last_cer_job', (int) $this->date->format('U'));
Log::info('Done with exchange rates job task.');
}
}

View File

@@ -41,7 +41,7 @@ class UpdateCheckCronjob extends AbstractCronjob
Log::debug('Now in checkForUpdates()');
// should not check for updates:
$permission = app('fireflyconfig')->get('permission_update_check', -1);
$permission = FireflyConfig::get('permission_update_check', -1);
$value = (int) $permission->data;
if (1 !== $value) {
Log::debug('Update check is not enabled.');

View File

@@ -0,0 +1,97 @@
<?php
/**
* RecurringCronjob.php
* Copyright (c) 2019 james@firefly-iii.org
*
* This file is part of Firefly III (https://github.com/firefly-iii).
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
declare(strict_types=1);
namespace FireflyIII\Support\Cronjobs;
use Carbon\Carbon;
use FireflyIII\Events\RequestedSendWebhookMessages;
use FireflyIII\Exceptions\FireflyException;
use FireflyIII\Models\Configuration;
use FireflyIII\Support\Facades\FireflyConfig;
use Illuminate\Support\Facades\Log;
/**
* Class WebhookCronjob
*/
class WebhookCronjob extends AbstractCronjob
{
/**
* @throws FireflyException
*/
public function fire(): void
{
Log::debug(sprintf('Now in %s', __METHOD__));
/** @var Configuration $config */
$config = FireflyConfig::get('last_webhook_job', 0);
$lastTime = (int) $config->data;
$diff = Carbon::now()->getTimestamp() - $lastTime;
$diffForHumans = today(config('app.timezone'))->diffForHumans(Carbon::createFromTimestamp($lastTime), null, true);
if (0 === $lastTime) {
Log::info('The webhook cron-job has never fired before.');
}
// less than ten minutes ago.
if ($lastTime > 0 && $diff <= 600) {
Log::info(sprintf('It has been %s since the webhook cron-job has fired.', $diffForHumans));
if (false === $this->force) {
Log::info('The cron-job will not fire now.');
$this->message = sprintf('It has been %s since the webhook cron-job has fired. It will not fire now.', $diffForHumans);
$this->jobFired = false;
$this->jobErrored = false;
$this->jobSucceeded = false;
return;
}
Log::info('Execution of the webhook cron-job has been FORCED.');
}
if ($lastTime > 0 && $diff > 600) {
Log::info(sprintf('It has been %s since the webhook cron-job has fired. It will fire now!', $diffForHumans));
}
$this->fireWebhookmessages();
app('preferences')->mark();
}
private function fireWebhookmessages(): void
{
Log::info(sprintf('Will now send webhook messages for date "%s".', $this->date->format('Y-m-d H:i:s')));
Log::debug('send event RequestedSendWebhookMessages through cron job.');
event(new RequestedSendWebhookMessages());
// get stuff from job:
$this->jobFired = true;
$this->jobErrored = false;
$this->jobSucceeded = true;
$this->message = 'Send webhook messages cron job fired successfully.';
FireflyConfig::set('last_webhook_job', (int) $this->date->format('U'));
Log::info(sprintf('Marked the last time this job has run as "%s" (%d)', $this->date->format('Y-m-d H:i:s'), (int) $this->date->format('U')));
Log::info('Done with webhook cron job task.');
}
}

View File

@@ -28,19 +28,34 @@ use Illuminate\Support\Facades\Log;
class Timer
{
private static array $times = [];
private array $times = [];
private static ?Timer $instance = null;
public static function start(string $title): void
private function __construct()
{
self::$times[$title] = microtime(true);
// Private constructor to prevent direct instantiation.
}
public static function stop(string $title): void
public static function getInstance(): self
{
$start = self::$times[$title] ?? 0;
if (null === self::$instance) {
self::$instance = new self();
}
return self::$instance;
}
public function start(string $title): void
{
$this->times[$title] = microtime(true);
}
public function stop(string $title): void
{
$start = $this->times[$title] ?? 0;
$end = microtime(true);
$diff = $end - $start;
unset(self::$times[$title]);
unset($this->times[$title]);
Log::debug(sprintf('Timer "%s" took %f seconds', $title, $diff));
}
}

View File

@@ -67,7 +67,7 @@ trait ChartGeneration
/** @var AccountRepositoryInterface $accountRepos */
$accountRepos = app(AccountRepositoryInterface::class);
$default = app('amount')->getPrimaryCurrency();
$primary = app('amount')->getPrimaryCurrency();
$chartData = [];
Log::debug(sprintf('Start of accountBalanceChart(list, %s, %s)', $start->format('Y-m-d H:i:s'), $end->format('Y-m-d H:i:s')));
@@ -75,10 +75,10 @@ trait ChartGeneration
/** @var Account $account */
foreach ($accounts as $account) {
Log::debug(sprintf('Now at account #%d ("%s)', $account->id, $account->name));
$currency = $accountRepos->getAccountCurrency($account) ?? $default;
$usePrimary = $convertToPrimary && $default->id !== $currency->id;
$currency = $accountRepos->getAccountCurrency($account) ?? $primary;
$usePrimary = $convertToPrimary && $primary->id !== $currency->id;
$field = $convertToPrimary ? 'pc_balance' : 'balance';
$currency = $usePrimary ? $default : $currency;
$currency = $usePrimary ? $primary : $currency;
Log::debug(sprintf('Will use field %s', $field));
$currentSet = [
'label' => $account->name,

View File

@@ -30,6 +30,7 @@ use FireflyIII\Support\Cronjobs\AutoBudgetCronjob;
use FireflyIII\Support\Cronjobs\BillWarningCronjob;
use FireflyIII\Support\Cronjobs\ExchangeRatesCronjob;
use FireflyIII\Support\Cronjobs\RecurringCronjob;
use FireflyIII\Support\Cronjobs\WebhookCronjob;
/**
* Trait CronRunner
@@ -62,6 +63,32 @@ trait CronRunner
];
}
protected function webhookCronJob(bool $force, Carbon $date): array
{
/** @var WebhookCronjob $webhook */
$webhook = app(WebhookCronjob::class);
$webhook->setForce($force);
$webhook->setDate($date);
try {
$webhook->fire();
} catch (FireflyException $e) {
return [
'job_fired' => false,
'job_succeeded' => false,
'job_errored' => true,
'message' => $e->getMessage(),
];
}
return [
'job_fired' => $webhook->jobFired,
'job_succeeded' => $webhook->jobSucceeded,
'job_errored' => $webhook->jobErrored,
'message' => $webhook->message,
];
}
protected function exchangeRatesCronJob(bool $force, Carbon $date): array
{
/** @var ExchangeRatesCronjob $exchangeRates */

View File

@@ -35,6 +35,7 @@ use FireflyIII\Repositories\Account\AccountRepositoryInterface;
use FireflyIII\Repositories\Journal\JournalRepositoryInterface;
use FireflyIII\Support\CacheProperties;
use FireflyIII\Support\Debug\Timer;
use FireflyIII\Support\Facades\Navigation;
use Illuminate\Support\Facades\Log;
/**
@@ -79,9 +80,10 @@ trait PeriodOverview
protected function getAccountPeriodOverview(Account $account, Carbon $start, Carbon $end): array
{
Log::debug('Now in getAccountPeriodOverview()');
Timer::start('account-period-total');
$timer = Timer::getInstance();
$timer->start('account-period-total');
$this->accountRepository = app(AccountRepositoryInterface::class);
$range = app('navigation')->getViewRange(true);
$range = Navigation::getViewRange(true);
[$start, $end] = $end < $start ? [$end, $start] : [$start, $end];
// properties for cache
@@ -91,32 +93,30 @@ trait PeriodOverview
$cache->addProperty('account-show-period-entries');
$cache->addProperty($account->id);
if ($cache->has()) {
Log::debug('Return CACHED in getAccountPeriodOverview()');
return $cache->get();
}
/** @var array $dates */
$dates = app('navigation')->blockPeriods($start, $end, $range);
$dates = Navigation::blockPeriods($start, $end, $range);
$entries = [];
$spent = [];
$earned = [];
$transferredAway = [];
$transferredIn = [];
// run a custom query because doing this with the collector is MEGA slow.
$timer->start('account-period-collect');
$transactions = $this->accountRepository->periodCollection($account, $start, $end);
$timer->stop('account-period-collect');
// loop dates
Log::debug(sprintf('Count of loops: %d', count($dates)));
$loops = 0;
// stop after 10 loops for memory reasons.
$timer->start('account-period-loop');
foreach ($dates as $currentDate) {
$title = app('navigation')->periodShow($currentDate['start'], $currentDate['period']);
if ($loops < 10) {
[$transactions, $spent] = $this->filterTransactionsByType(TransactionTypeEnum::WITHDRAWAL, $transactions, $currentDate['start'], $currentDate['end']);
[$transactions, $earned] = $this->filterTransactionsByType(TransactionTypeEnum::DEPOSIT, $transactions, $currentDate['start'], $currentDate['end']);
[$transactions, $transferredAway] = $this->filterTransfers('away', $transactions, $currentDate['start'], $currentDate['end']);
[$transactions, $transferredIn] = $this->filterTransfers('in', $transactions, $currentDate['start'], $currentDate['end']);
}
$title = Navigation::periodShow($currentDate['start'], $currentDate['period']);
[$transactions, $spent] = $this->filterTransactionsByType(TransactionTypeEnum::WITHDRAWAL, $transactions, $currentDate['start'], $currentDate['end']);
[$transactions, $earned] = $this->filterTransactionsByType(TransactionTypeEnum::DEPOSIT, $transactions, $currentDate['start'], $currentDate['end']);
[$transactions, $transferredAway] = $this->filterTransfers('away', $transactions, $currentDate['start'], $currentDate['end']);
[$transactions, $transferredIn] = $this->filterTransfers('in', $transactions, $currentDate['start'], $currentDate['end']);
$entries[]
= [
'title' => $title,
@@ -129,8 +129,9 @@ trait PeriodOverview
];
++$loops;
}
$timer->stop('account-period-loop');
$cache->store($entries);
Timer::stop('account-period-total');
$timer->stop('account-period-total');
Log::debug('End of getAccountPeriodOverview()');
return $entries;
@@ -138,7 +139,8 @@ trait PeriodOverview
private function filterTransactionsByType(TransactionTypeEnum $type, array $transactions, Carbon $start, Carbon $end): array
{
$result = [];
$result = [];
$filtered = [];
/**
* @var int $index
@@ -146,38 +148,46 @@ trait PeriodOverview
*/
foreach ($transactions as $index => $item) {
$date = Carbon::parse($item['date']);
if ($item['type'] === $type->value && $date >= $start && $date <= $end) {
$fits = $item['type'] === $type->value && $date >= $start && $date <= $end;
if ($fits) {
$result[] = $item;
unset($transactions[$index]);
}
if (!$fits) {
$filtered[] = $item;
}
}
return [$transactions, $result];
return [$filtered, $result];
}
private function filterTransfers(string $direction, array $transactions, Carbon $start, Carbon $end): array
{
$result = [];
$result = [];
$filtered = [];
/**
* @var int $index
* @var array $item
*/
foreach ($transactions as $index => $item) {
$date = Carbon::parse($item['date']);
$date = Carbon::parse($item['date']);
if ($date >= $start && $date <= $end) {
if ('away' === $direction && -1 === bccomp((string) $item['amount'], '0')) {
if ('away' === $direction && -1 === bccomp((string)$item['amount'], '0')) {
$result[] = $item;
unset($transactions[$index]);
continue;
}
if ('in' === $direction && 1 === bccomp((string) $item['amount'], '0')) {
if ('in' === $direction && 1 === bccomp((string)$item['amount'], '0')) {
$result[] = $item;
unset($transactions[$index]);
continue;
}
}
$filtered[] = $item;
}
return [$transactions, $result];
return [$filtered, $result];
}
private function groupByCurrency(array $journals): array
@@ -186,7 +196,7 @@ trait PeriodOverview
/** @var array $journal */
foreach ($journals as $journal) {
$currencyId = (int) $journal['currency_id'];
$currencyId = (int)$journal['currency_id'];
$currencyCode = $journal['currency_code'];
$currencyName = $journal['currency_name'];
$currencySymbol = $journal['currency_symbol'];
@@ -203,7 +213,7 @@ trait PeriodOverview
$currencyDecimalPlaces = $this->primaryCurrency->decimal_places;
}
if ($this->convertToPrimary && $currencyId !== $this->primaryCurrency->id && $foreignCurrencyId === $this->primaryCurrency->id) {
$currencyId = (int) $foreignCurrencyId;
$currencyId = (int)$foreignCurrencyId;
$currencyCode = $journal['foreign_currency_code'];
$currencyName = $journal['foreign_currency_name'];
$currencySymbol = $journal['foreign_currency_symbol'];
@@ -235,7 +245,7 @@ trait PeriodOverview
*/
protected function getCategoryPeriodOverview(Category $category, Carbon $start, Carbon $end): array
{
$range = app('navigation')->getViewRange(true);
$range = Navigation::getViewRange(true);
[$start, $end] = $end < $start ? [$end, $start] : [$start, $end];
// properties for entries with their amounts.
@@ -251,7 +261,7 @@ trait PeriodOverview
}
/** @var array $dates */
$dates = app('navigation')->blockPeriods($start, $end, $range);
$dates = Navigation::blockPeriods($start, $end, $range);
$entries = [];
// collect all expenses in this period:
@@ -281,7 +291,7 @@ trait PeriodOverview
$spent = $this->filterJournalsByDate($spentSet, $currentDate['start'], $currentDate['end']);
$earned = $this->filterJournalsByDate($earnedSet, $currentDate['start'], $currentDate['end']);
$transferred = $this->filterJournalsByDate($transferSet, $currentDate['start'], $currentDate['end']);
$title = app('navigation')->periodShow($currentDate['end'], $currentDate['period']);
$title = Navigation::periodShow($currentDate['end'], $currentDate['period']);
$entries[]
= [
'transactions' => 0,
@@ -327,7 +337,7 @@ trait PeriodOverview
*/
protected function getNoBudgetPeriodOverview(Carbon $start, Carbon $end): array
{
$range = app('navigation')->getViewRange(true);
$range = Navigation::getViewRange(true);
[$start, $end] = $end < $start ? [$end, $start] : [$start, $end];
@@ -342,7 +352,7 @@ trait PeriodOverview
}
/** @var array $dates */
$dates = app('navigation')->blockPeriods($start, $end, $range);
$dates = Navigation::blockPeriods($start, $end, $range);
$entries = [];
// get all expenses without a budget.
@@ -353,7 +363,7 @@ trait PeriodOverview
foreach ($dates as $currentDate) {
$set = $this->filterJournalsByDate($journals, $currentDate['start'], $currentDate['end']);
$title = app('navigation')->periodShow($currentDate['end'], $currentDate['period']);
$title = Navigation::periodShow($currentDate['end'], $currentDate['period']);
$entries[]
= [
'title' => $title,
@@ -380,17 +390,17 @@ trait PeriodOverview
protected function getNoCategoryPeriodOverview(Carbon $theDate): array
{
app('log')->debug(sprintf('Now in getNoCategoryPeriodOverview(%s)', $theDate->format('Y-m-d')));
$range = app('navigation')->getViewRange(true);
$range = Navigation::getViewRange(true);
$first = $this->journalRepos->firstNull();
$start = null === $first ? new Carbon() : $first->date;
$end = clone $theDate;
$end = app('navigation')->endOfPeriod($end, $range);
$end = Navigation::endOfPeriod($end, $range);
app('log')->debug(sprintf('Start for getNoCategoryPeriodOverview() is %s', $start->format('Y-m-d')));
app('log')->debug(sprintf('End for getNoCategoryPeriodOverview() is %s', $end->format('Y-m-d')));
// properties for cache
$dates = app('navigation')->blockPeriods($start, $end, $range);
$dates = Navigation::blockPeriods($start, $end, $range);
$entries = [];
// collect all expenses in this period:
@@ -422,7 +432,7 @@ trait PeriodOverview
$spent = $this->filterJournalsByDate($spentSet, $currentDate['start'], $currentDate['end']);
$earned = $this->filterJournalsByDate($earnedSet, $currentDate['start'], $currentDate['end']);
$transferred = $this->filterJournalsByDate($transferSet, $currentDate['start'], $currentDate['end']);
$title = app('navigation')->periodShow($currentDate['end'], $currentDate['period']);
$title = Navigation::periodShow($currentDate['end'], $currentDate['period']);
$entries[]
= [
'title' => $title,
@@ -445,7 +455,7 @@ trait PeriodOverview
*/
protected function getTagPeriodOverview(Tag $tag, Carbon $start, Carbon $end): array // period overview for tags.
{
$range = app('navigation')->getViewRange(true);
$range = Navigation::getViewRange(true);
[$start, $end] = $end < $start ? [$end, $start] : [$start, $end];
// properties for cache
@@ -459,7 +469,7 @@ trait PeriodOverview
}
/** @var array $dates */
$dates = app('navigation')->blockPeriods($start, $end, $range);
$dates = Navigation::blockPeriods($start, $end, $range);
$entries = [];
// collect all expenses in this period:
@@ -495,7 +505,7 @@ trait PeriodOverview
$spent = $this->filterJournalsByDate($spentSet, $currentDate['start'], $currentDate['end']);
$earned = $this->filterJournalsByDate($earnedSet, $currentDate['start'], $currentDate['end']);
$transferred = $this->filterJournalsByDate($transferSet, $currentDate['start'], $currentDate['end']);
$title = app('navigation')->periodShow($currentDate['end'], $currentDate['period']);
$title = Navigation::periodShow($currentDate['end'], $currentDate['period']);
$entries[]
= [
'transactions' => 0,
@@ -540,7 +550,7 @@ trait PeriodOverview
*/
protected function getTransactionPeriodOverview(string $transactionType, Carbon $start, Carbon $end): array
{
$range = app('navigation')->getViewRange(true);
$range = Navigation::getViewRange(true);
$types = config(sprintf('firefly.transactionTypesByType.%s', $transactionType));
[$start, $end] = $end < $start ? [$end, $start] : [$start, $end];
@@ -555,7 +565,7 @@ trait PeriodOverview
}
/** @var array $dates */
$dates = app('navigation')->blockPeriods($start, $end, $range);
$dates = Navigation::blockPeriods($start, $end, $range);
$entries = [];
$spent = [];
$earned = [];
@@ -567,7 +577,7 @@ trait PeriodOverview
$loops = 0;
foreach ($dates as $currentDate) {
$title = app('navigation')->periodShow($currentDate['end'], $currentDate['period']);
$title = Navigation::periodShow($currentDate['end'], $currentDate['period']);
if ($loops < 10) {
// set to correct array
@@ -582,14 +592,14 @@ trait PeriodOverview
}
}
$entries[]
= [
'title' => $title,
'route' => route('transactions.index', [$transactionType, $currentDate['start']->format('Y-m-d'), $currentDate['end']->format('Y-m-d')]),
'total_transactions' => count($spent) + count($earned) + count($transferred),
'spent' => $this->groupByCurrency($spent),
'earned' => $this->groupByCurrency($earned),
'transferred' => $this->groupByCurrency($transferred),
];
= [
'title' => $title,
'route' => route('transactions.index', [$transactionType, $currentDate['start']->format('Y-m-d'), $currentDate['end']->format('Y-m-d')]),
'total_transactions' => count($spent) + count($earned) + count($transferred),
'spent' => $this->groupByCurrency($spent),
'earned' => $this->groupByCurrency($earned),
'transferred' => $this->groupByCurrency($transferred),
];
++$loops;
}
@@ -605,7 +615,7 @@ trait PeriodOverview
/** @var array $journal */
foreach ($journals as $journal) {
if ($account->id === (int) $journal['source_account_id']) {
if ($account->id === (int)$journal['source_account_id']) {
$return[] = $journal;
}
}
@@ -622,7 +632,7 @@ trait PeriodOverview
/** @var array $journal */
foreach ($journals as $journal) {
if ($account->id === (int) $journal['destination_account_id']) {
if ($account->id === (int)$journal['destination_account_id']) {
$return[] = $journal;
}
}

View File

@@ -33,6 +33,7 @@ use FireflyIII\Models\AccountMeta;
use FireflyIII\Models\AccountType;
use FireflyIII\Models\Location;
use FireflyIII\Models\Note;
use FireflyIII\Models\ObjectGroup;
use FireflyIII\Models\TransactionCurrency;
use FireflyIII\Models\UserGroup;
use FireflyIII\Support\Facades\Amount;
@@ -41,6 +42,7 @@ use FireflyIII\Support\Http\Api\ExchangeRateConverter;
use FireflyIII\User;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
use Override;
@@ -51,36 +53,30 @@ use Override;
*/
class AccountEnrichment implements EnrichmentInterface
{
private array $accountIds;
private array $accountTypeIds;
private array $accountTypes;
private array $ids = [];
private array $accountTypeIds = [];
private array $accountTypes = [];
private Collection $collection;
private array $currencies;
private array $locations;
private array $meta;
private array $currencies = [];
private array $locations = [];
private array $meta = [];
private TransactionCurrency $primaryCurrency;
private array $notes;
private array $openingBalances;
private array $notes = [];
private array $openingBalances = [];
private User $user;
private UserGroup $userGroup;
private array $lastActivities;
private ?Carbon $date = null;
private bool $convertToPrimary = false;
private array $lastActivities = [];
private ?Carbon $date = null;
private bool $convertToPrimary;
private array $balances = [];
private array $objectGroups = [];
private array $mappedObjects = [];
/**
* TODO The account enricher must do conversion from and to the primary currency.
*/
public function __construct()
{
$this->accountIds = [];
$this->openingBalances = [];
$this->currencies = [];
$this->accountTypeIds = [];
$this->accountTypes = [];
$this->meta = [];
$this->notes = [];
$this->lastActivities = [];
$this->locations = [];
$this->primaryCurrency = Amount::getPrimaryCurrency();
$this->convertToPrimary = Amount::convertToPrimary();
}
@@ -105,27 +101,28 @@ class AccountEnrichment implements EnrichmentInterface
// prep local fields
$this->collection = $collection;
$this->collectAccountIds();
$this->collectIds();
$this->getAccountTypes();
$this->collectMetaData();
$this->collectNotes();
$this->collectLastActivities();
$this->collectLocations();
$this->collectOpeningBalances();
$this->collectObjectGroups();
$this->collectBalances();
$this->appendCollectedData();
return $this->collection;
}
private function collectAccountIds(): void
private function collectIds(): void
{
/** @var Account $account */
foreach ($this->collection as $account) {
$this->accountIds[] = (int) $account->id;
$this->accountTypeIds[] = (int) $account->account_type_id;
$this->ids[] = (int)$account->id;
$this->accountTypeIds[] = (int)$account->account_type_id;
}
$this->accountIds = array_unique($this->accountIds);
$this->ids = array_unique($this->ids);
$this->accountTypeIds = array_unique($this->accountTypeIds);
}
@@ -135,27 +132,29 @@ class AccountEnrichment implements EnrichmentInterface
/** @var AccountType $type */
foreach ($types as $type) {
$this->accountTypes[(int) $type->id] = $type->type;
$this->accountTypes[(int)$type->id] = $type->type;
}
}
private function collectMetaData(): void
{
$set = AccountMeta::whereIn('name', ['is_multi_currency', 'include_net_worth', 'currency_id', 'account_role', 'account_number', 'BIC', 'liability_direction', 'interest', 'interest_period', 'current_debt'])
->whereIn('account_id', $this->accountIds)
->whereIn('account_id', $this->ids)
->get(['account_meta.id', 'account_meta.account_id', 'account_meta.name', 'account_meta.data'])->toArray()
;
/** @var array $entry */
foreach ($set as $entry) {
$this->meta[(int) $entry['account_id']][$entry['name']] = (string) $entry['data'];
$this->meta[(int)$entry['account_id']][$entry['name']] = (string)$entry['data'];
if ('currency_id' === $entry['name']) {
$this->currencies[(int) $entry['data']] = true;
$this->currencies[(int)$entry['data']] = true;
}
}
$currencies = TransactionCurrency::whereIn('id', array_keys($this->currencies))->get();
foreach ($currencies as $currency) {
$this->currencies[(int) $currency->id] = $currency;
if (count($this->currencies) > 0) {
$currencies = TransactionCurrency::whereIn('id', array_keys($this->currencies))->get();
foreach ($currencies as $currency) {
$this->currencies[(int)$currency->id] = $currency;
}
}
$this->currencies[0] = $this->primaryCurrency;
foreach ($this->currencies as $id => $currency) {
@@ -167,28 +166,28 @@ class AccountEnrichment implements EnrichmentInterface
private function collectNotes(): void
{
$notes = Note::query()->whereIn('noteable_id', $this->accountIds)
$notes = Note::query()->whereIn('noteable_id', $this->ids)
->whereNotNull('notes.text')
->where('notes.text', '!=', '')
->where('noteable_type', Account::class)->get(['notes.noteable_id', 'notes.text'])->toArray()
;
foreach ($notes as $note) {
$this->notes[(int) $note['noteable_id']] = (string) $note['text'];
$this->notes[(int)$note['noteable_id']] = (string)$note['text'];
}
Log::debug(sprintf('Enrich with %d note(s)', count($this->notes)));
}
private function collectLocations(): void
{
$locations = Location::query()->whereIn('locatable_id', $this->accountIds)
$locations = Location::query()->whereIn('locatable_id', $this->ids)
->where('locatable_type', Account::class)->get(['locations.locatable_id', 'locations.latitude', 'locations.longitude', 'locations.zoom_level'])->toArray()
;
foreach ($locations as $location) {
$this->locations[(int) $location['locatable_id']]
$this->locations[(int)$location['locatable_id']]
= [
'latitude' => (float) $location['latitude'],
'longitude' => (float) $location['longitude'],
'zoom_level' => (int) $location['zoom_level'],
'latitude' => (float)$location['latitude'],
'longitude' => (float)$location['longitude'],
'zoom_level' => (int)$location['zoom_level'],
];
}
Log::debug(sprintf('Enrich with %d locations(s)', count($this->locations)));
@@ -208,12 +207,12 @@ class AccountEnrichment implements EnrichmentInterface
;
$journals = $collector->getExtractedJournals();
foreach ($journals as $journal) {
$this->openingBalances[(int) $journal['source_account_id']]
$this->openingBalances[(int)$journal['source_account_id']]
= [
'amount' => Steam::negative($journal['amount']),
'date' => $journal['date'],
];
$this->openingBalances[(int) $journal['destination_account_id']]
$this->openingBalances[(int)$journal['destination_account_id']]
= [
'amount' => Steam::positive($journal['amount']),
'date' => $journal['date'],
@@ -234,59 +233,72 @@ class AccountEnrichment implements EnrichmentInterface
private function appendCollectedData(): void
{
$notes = $this->notes;
$openingBalances = $this->openingBalances;
$locations = $this->locations;
$lastActivities = $this->lastActivities;
$this->collection = $this->collection->map(function (Account $item) use ($notes, $openingBalances, $locations, $lastActivities) {
$item->full_account_type = $this->accountTypes[(int) $item->account_type_id] ?? null;
$accountMeta = [
'currency' => null,
'location' => [
$this->collection = $this->collection->map(function (Account $item) {
$id = (int)$item->id;
$item->full_account_type = $this->accountTypes[(int)$item->account_type_id] ?? null;
$meta = [
'currency' => null,
'location' => [
'latitude' => null,
'longitude' => null,
'zoom_level' => null,
],
'opening_balance_date' => null,
'object_group_id' => null,
'object_group_order' => null,
'object_group_title' => null,
'opening_balance_date' => null,
'opening_balance_amount' => null,
'account_number' => null,
'notes' => $notes[$id] ?? null,
'last_activity' => $this->lastActivities[$id] ?? null,
];
if (array_key_exists((int) $item->id, $this->meta)) {
foreach ($this->meta[(int) $item->id] as $name => $value) {
$accountMeta[$name] = $value;
// add object group if available
if (array_key_exists($id, $this->mappedObjects)) {
$key = $this->mappedObjects[$id];
$meta['object_group_id'] = $this->objectGroups[$key]['id'];
$meta['object_group_title'] = $this->objectGroups[$key]['title'];
$meta['object_group_order'] = $this->objectGroups[$key]['order'];
}
// if location, add location:
if (array_key_exists($id, $this->locations)) {
$meta['location'] = $this->locations[$id];
}
if (array_key_exists($id, $this->meta)) {
foreach ($this->meta[$id] as $name => $value) {
$meta[$name] = $value;
}
}
// also add currency, if present.
if (array_key_exists('currency_id', $accountMeta)) {
$currencyId = (int) $accountMeta['currency_id'];
$accountMeta['currency'] = $this->currencies[$currencyId];
if (array_key_exists('currency_id', $meta)) {
$currencyId = (int)$meta['currency_id'];
$meta['currency'] = $this->currencies[$currencyId];
}
// if notes, add notes.
if (array_key_exists($item->id, $notes)) {
$accountMeta['notes'] = $notes[$item->id];
}
// if opening balance, add opening balance
if (array_key_exists($item->id, $openingBalances)) {
$accountMeta['opening_balance_date'] = $openingBalances[$item->id]['date'];
$accountMeta['opening_balance_amount'] = $openingBalances[$item->id]['amount'];
if (array_key_exists($id, $this->openingBalances)) {
$meta['opening_balance_date'] = $this->openingBalances[$id]['date'];
$meta['opening_balance_amount'] = $this->openingBalances[$id]['amount'];
}
// add balances
// get currencies:
$currency = $this->primaryCurrency; // assume primary currency
if (null !== $accountMeta['currency']) {
$currency = $accountMeta['currency'];
if (null !== $meta['currency']) {
$currency = $meta['currency'];
}
// get the current balance:
$date = $this->getDate();
$finalBalance = Steam::finalAccountBalance($item, $date, $this->primaryCurrency, $this->convertToPrimary);
// $finalBalance = Steam::finalAccountBalance($item, $date, $this->primaryCurrency, $this->convertToPrimary);
$finalBalance = $this->balances[$id];
Log::debug(sprintf('Call finalAccountBalance(%s) with date/time "%s"', var_export($this->convertToPrimary, true), $date->toIso8601String()), $finalBalance);
// collect current balances:
$currentBalance = Steam::bcround($finalBalance[$currency->code] ?? '0', $currency->decimal_places);
$openingBalance = Steam::bcround($accountMeta['opening_balance_amount'] ?? '0', $currency->decimal_places);
$openingBalance = Steam::bcround($meta['opening_balance_amount'] ?? '0', $currency->decimal_places);
$virtualBalance = Steam::bcround($account->virtual_balance ?? '0', $currency->decimal_places);
$debtAmount = $accountMeta['current_debt'] ?? null;
$debtAmount = $meta['current_debt'] ?? null;
// set some pc_ default values to NULL:
$pcCurrentBalance = null;
@@ -311,11 +323,11 @@ class AccountEnrichment implements EnrichmentInterface
}
// set opening balance(s) to NULL if the date is null
if (null === $accountMeta['opening_balance_date']) {
if (null === $meta['opening_balance_date']) {
$openingBalance = null;
$pcOpeningBalance = null;
}
$accountMeta['balances'] = [
$meta['balances'] = [
'current_balance' => $currentBalance,
'pc_current_balance' => $pcCurrentBalance,
'opening_balance' => $openingBalance,
@@ -326,16 +338,7 @@ class AccountEnrichment implements EnrichmentInterface
'pc_debt_amount' => $pcDebtAmount,
];
// end add balances
// if location, add location:
if (array_key_exists($item->id, $locations)) {
$accountMeta['location'] = $locations[$item->id];
}
if (array_key_exists($item->id, $lastActivities)) {
$accountMeta['last_activity'] = $lastActivities[$item->id];
}
$item->meta = $accountMeta;
$item->meta = $meta;
return $item;
});
@@ -343,10 +346,35 @@ class AccountEnrichment implements EnrichmentInterface
private function collectLastActivities(): void
{
$this->lastActivities = Steam::getLastActivities($this->accountIds);
$this->lastActivities = Steam::getLastActivities($this->ids);
}
private function collectBalances(): void {}
private function collectBalances(): void
{
$this->balances = Steam::accountsBalancesOptimized($this->collection, $this->getDate(), $this->primaryCurrency, $this->convertToPrimary);
}
private function collectObjectGroups(): void
{
$set = DB::table('object_groupables')
->whereIn('object_groupable_id', $this->ids)
->where('object_groupable_type', Account::class)
->get(['object_groupable_id', 'object_group_id'])
;
$ids = array_unique($set->pluck('object_group_id')->toArray());
foreach ($set as $entry) {
$this->mappedObjects[(int)$entry->object_groupable_id] = (int)$entry->object_group_id;
}
$groups = ObjectGroup::whereIn('id', $ids)->get(['id', 'title', 'order'])->toArray();
foreach ($groups as $group) {
$group['id'] = (int)$group['id'];
$group['order'] = (int)$group['order'];
$this->objectGroups[(int)$group['id']] = $group;
}
}
public function setDate(?Carbon $date): void
{

View File

@@ -43,8 +43,10 @@ class AvailableBudgetEnrichment implements EnrichmentInterface
private User $user;
private UserGroup $userGroup;
private TransactionCurrency $primaryCurrency;
private bool $convertToPrimary = false;
private bool $convertToPrimary;
private array $ids = [];
private array $currencyIds = [];
private array $currencies = [];
private Collection $collection;
private array $spentInBudgets = [];
private array $spentOutsideBudgets = [];
@@ -72,6 +74,7 @@ class AvailableBudgetEnrichment implements EnrichmentInterface
{
$this->collection = $collection;
$this->collectIds();
$this->collectCurrencies();
$this->collectSpentInfo();
$this->appendCollectedData();
@@ -108,7 +111,8 @@ class AvailableBudgetEnrichment implements EnrichmentInterface
{
/** @var AvailableBudget $availableBudget */
foreach ($this->collection as $availableBudget) {
$this->ids[] = (int) $availableBudget->id;
$this->ids[] = (int)$availableBudget->id;
$this->currencyIds[(int)$availableBudget->id] = (int)$availableBudget->transaction_currency_id;
}
$this->ids = array_unique($this->ids);
}
@@ -121,15 +125,17 @@ class AvailableBudgetEnrichment implements EnrichmentInterface
$spentInBudgets = $this->opsRepository->collectExpenses($start, $end, null, $allActive, null);
$spentOutsideBudgets = $this->noBudgetRepository->collectExpenses($start, $end, null, null, null);
foreach ($this->collection as $availableBudget) {
$id = (int) $availableBudget->id;
$filteredSpentInBudgets = $this->opsRepository->sumCollectedExpenses($spentInBudgets, $availableBudget->start_date, $availableBudget->end_date, $availableBudget->transactionCurrency, false);
$filteredSpentOutsideBudgets = $this->opsRepository->sumCollectedExpenses($spentOutsideBudgets, $availableBudget->start_date, $availableBudget->end_date, $availableBudget->transactionCurrency, false);
$id = (int)$availableBudget->id;
$currencyId = $this->currencyIds[$id];
$currency = $this->currencies[$currencyId];
$filteredSpentInBudgets = $this->opsRepository->sumCollectedExpenses($spentInBudgets, $availableBudget->start_date, $availableBudget->end_date, $currency, false);
$filteredSpentOutsideBudgets = $this->opsRepository->sumCollectedExpenses($spentOutsideBudgets, $availableBudget->start_date, $availableBudget->end_date, $currency, false);
$this->spentInBudgets[$id] = array_values($filteredSpentInBudgets);
$this->spentOutsideBudgets[$id] = array_values($filteredSpentOutsideBudgets);
if (true === $this->convertToPrimary) {
$pcFilteredSpentInBudgets = $this->opsRepository->sumCollectedExpenses($spentInBudgets, $availableBudget->start_date, $availableBudget->end_date, $availableBudget->transactionCurrency, true);
$pcFilteredSpentOutsideBudgets = $this->opsRepository->sumCollectedExpenses($spentOutsideBudgets, $availableBudget->start_date, $availableBudget->end_date, $availableBudget->transactionCurrency, true);
$pcFilteredSpentInBudgets = $this->opsRepository->sumCollectedExpenses($spentInBudgets, $availableBudget->start_date, $availableBudget->end_date, $currency, true);
$pcFilteredSpentOutsideBudgets = $this->opsRepository->sumCollectedExpenses($spentOutsideBudgets, $availableBudget->start_date, $availableBudget->end_date, $currency, true);
$this->pcSpentInBudgets[$id] = array_values($pcFilteredSpentInBudgets);
$this->pcSpentOutsideBudgets[$id] = array_values($pcFilteredSpentOutsideBudgets);
}
@@ -145,22 +151,29 @@ class AvailableBudgetEnrichment implements EnrichmentInterface
private function appendCollectedData(): void
{
$spentInsideBudgets = $this->spentInBudgets;
$spentOutsideBudgets = $this->spentOutsideBudgets;
$pcSpentInBudgets = $this->pcSpentInBudgets;
$pcSpentOutsideBudgets = $this->pcSpentOutsideBudgets;
$this->collection = $this->collection->map(function (AvailableBudget $item) use ($spentInsideBudgets, $spentOutsideBudgets, $pcSpentInBudgets, $pcSpentOutsideBudgets) {
$id = (int) $item->id;
$this->collection = $this->collection->map(function (AvailableBudget $item) {
$id = (int)$item->id;
$currencyId = $this->currencyIds[$id];
$currency = $this->currencies[$currencyId];
$meta = [
'spent_in_budgets' => $spentInsideBudgets[$id] ?? [],
'pc_spent_in_budgets' => $pcSpentInBudgets[$id] ?? [],
'spent_outside_budgets' => $spentOutsideBudgets[$id] ?? [],
'pc_spent_outside_budgets' => $pcSpentOutsideBudgets[$id] ?? [],
'currency' => $currency,
'spent_in_budgets' => $this->spentInBudgets[$id] ?? [],
'pc_spent_in_budgets' => $this->pcSpentInBudgets[$id] ?? [],
'spent_outside_budgets' => $this->spentOutsideBudgets[$id] ?? [],
'pc_spent_outside_budgets' => $this->pcSpentOutsideBudgets[$id] ?? [],
];
$item->meta = $meta;
return $item;
});
}
private function collectCurrencies(): void
{
$ids = array_unique(array_values($this->currencyIds));
$set = TransactionCurrency::whereIn('id', $ids)->get();
foreach ($set as $currency) {
$this->currencies[(int)$currency->id] = $currency;
}
}
}

View File

@@ -8,6 +8,7 @@ use Carbon\Carbon;
use FireflyIII\Models\AutoBudget;
use FireflyIII\Models\Budget;
use FireflyIII\Models\Note;
use FireflyIII\Models\ObjectGroup;
use FireflyIII\Models\TransactionCurrency;
use FireflyIII\Models\UserGroup;
use FireflyIII\Repositories\Budget\OperationsRepositoryInterface;
@@ -15,23 +16,26 @@ use FireflyIII\Support\Facades\Amount;
use FireflyIII\User;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
class BudgetEnrichment implements EnrichmentInterface
{
private Collection $collection;
private bool $convertToPrimary = true;
private bool $convertToPrimary;
private TransactionCurrency $primaryCurrency;
private User $user;
private UserGroup $userGroup;
private array $ids = [];
private array $notes = [];
private array $autoBudgets = [];
private array $currencies = [];
private ?Carbon $start = null;
private ?Carbon $end = null;
private array $spent = [];
private array $pcSpent = [];
private array $ids = [];
private array $notes = [];
private array $autoBudgets = [];
private array $currencies = [];
private ?Carbon $start = null;
private ?Carbon $end = null;
private array $spent = [];
private array $pcSpent = [];
private array $objectGroups = [];
private array $mappedObjects = [];
public function __construct()
{
@@ -46,6 +50,8 @@ class BudgetEnrichment implements EnrichmentInterface
$this->collectNotes();
$this->collectAutoBudgets();
$this->collectExpenses();
$this->collectObjectGroups();
$this->appendCollectedData();
return $this->collection;
@@ -98,12 +104,25 @@ class BudgetEnrichment implements EnrichmentInterface
$this->collection = $this->collection->map(function (Budget $item) {
$id = (int)$item->id;
$meta = [
'notes' => $this->notes[$id] ?? null,
'currency' => $this->currencies[$id] ?? null,
'auto_budget' => $this->autoBudgets[$id] ?? null,
'spent' => $this->spent[$id] ?? null,
'pc_spent' => $this->pcSpent[$id] ?? null,
'object_group_id' => null,
'object_group_order' => null,
'object_group_title' => null,
'notes' => $this->notes[$id] ?? null,
'currency' => $this->currencies[$id] ?? null,
'auto_budget' => $this->autoBudgets[$id] ?? null,
'spent' => $this->spent[$id] ?? null,
'pc_spent' => $this->pcSpent[$id] ?? null,
];
// add object group if available
if (array_key_exists($id, $this->mappedObjects)) {
$key = $this->mappedObjects[$id];
$meta['object_group_id'] = $this->objectGroups[$key]['id'];
$meta['object_group_title'] = $this->objectGroups[$key]['title'];
$meta['object_group_order'] = $this->objectGroups[$key]['order'];
}
$item->meta = $meta;
return $item;
@@ -154,4 +173,26 @@ class BudgetEnrichment implements EnrichmentInterface
{
$this->start = $start;
}
private function collectObjectGroups(): void
{
$set = DB::table('object_groupables')
->whereIn('object_groupable_id', $this->ids)
->where('object_groupable_type', Budget::class)
->get(['object_groupable_id', 'object_group_id'])
;
$ids = array_unique($set->pluck('object_group_id')->toArray());
foreach ($set as $entry) {
$this->mappedObjects[(int)$entry->object_groupable_id] = (int)$entry->object_group_id;
}
$groups = ObjectGroup::whereIn('id', $ids)->get(['id', 'title', 'order'])->toArray();
foreach ($groups as $group) {
$group['id'] = (int)$group['id'];
$group['order'] = (int)$group['order'];
$this->objectGroups[(int)$group['id']] = $group;
}
}
}

View File

@@ -29,6 +29,8 @@ class BudgetLimitEnrichment implements EnrichmentInterface
private Collection $budgets;
private array $expenses = [];
private array $pcExpenses = [];
private array $currencyIds = [];
private array $currencies = [];
private bool $convertToPrimary = true;
private TransactionCurrency $primaryCurrency;
@@ -42,6 +44,7 @@ class BudgetLimitEnrichment implements EnrichmentInterface
{
$this->collection = $collection;
$this->collectIds();
$this->collectCurrencies();
$this->collectNotes();
$this->collectBudgets();
$this->appendCollectedData();
@@ -71,14 +74,19 @@ class BudgetLimitEnrichment implements EnrichmentInterface
private function collectIds(): void
{
$this->start = $this->collection->min('start_date');
$this->end = $this->collection->max('end_date');
$this->start = $this->collection->min('start_date');
$this->end = $this->collection->max('end_date');
/** @var BudgetLimit $limit */
foreach ($this->collection as $limit) {
$this->ids[] = (int)$limit->id;
$id = (int)$limit->id;
$this->ids[] = $id;
if (0 !== (int)$limit->transaction_currency_id) {
$this->currencyIds[$id] = (int)$limit->transaction_currency_id;
}
}
$this->ids = array_unique($this->ids);
$this->ids = array_unique($this->ids);
$this->currencyIds = array_unique($this->currencyIds);
}
private function collectNotes(): void
@@ -98,10 +106,15 @@ class BudgetLimitEnrichment implements EnrichmentInterface
{
$this->collection = $this->collection->map(function (BudgetLimit $item) {
$id = (int)$item->id;
$currencyId = (int)$item->transaction_currency_id;
if (0 === $currencyId) {
$currencyId = $this->primaryCurrency->id;
}
$meta = [
'notes' => $this->notes[$id] ?? null,
'spent' => $this->expenses[$id] ?? [],
'pc_spent' => $this->pcExpenses[$id] ?? [],
'currency' => $this->currencies[$currencyId],
];
$item->meta = $meta;
@@ -133,4 +146,13 @@ class BudgetLimitEnrichment implements EnrichmentInterface
}
}
}
private function collectCurrencies(): void
{
$this->currencies[$this->primaryCurrency->id] = $this->primaryCurrency;
$currencies = TransactionCurrency::whereIn('id', $this->currencyIds)->whereNot('id', $this->primaryCurrency->id)->get();
foreach ($currencies as $currency) {
$this->currencies[(int)$currency->id] = $currency;
}
}
}

View File

@@ -0,0 +1,273 @@
<?php
declare(strict_types=1);
namespace FireflyIII\Support\JsonApi\Enrichments;
use Carbon\Carbon;
use FireflyIII\Models\Account;
use FireflyIII\Models\AccountMeta;
use FireflyIII\Models\Note;
use FireflyIII\Models\ObjectGroup;
use FireflyIII\Models\PiggyBank;
use FireflyIII\Models\TransactionCurrency;
use FireflyIII\Models\UserGroup;
use FireflyIII\Support\Facades\Amount;
use FireflyIII\Support\Facades\Steam;
use FireflyIII\User;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
class PiggyBankEnrichment implements EnrichmentInterface
{
private User $user;
private UserGroup $userGroup;
private Collection $collection;
private array $ids = [];
private array $currencyIds = [];
private array $currencies = [];
private array $accountIds = [];
// private array $accountCurrencies = [];
private array $notes = [];
private array $mappedObjects = [];
private TransactionCurrency $primaryCurrency;
private array $amounts = [];
public function __construct()
{
$this->primaryCurrency = Amount::getPrimaryCurrency();
}
public function enrich(Collection $collection): Collection
{
$this->collection = $collection;
$this->collectIds();
$this->collectObjectGroups();
$this->collectNotes();
$this->collectCurrentAmounts();
$this->appendCollectedData();
return $this->collection;
}
public function enrichSingle(array|Model $model): array|Model
{
Log::debug(__METHOD__);
$collection = new Collection([$model]);
$collection = $this->enrich($collection);
return $collection->first();
}
public function setUser(User $user): void
{
$this->user = $user;
$this->setUserGroup($user->userGroup);
}
public function setUserGroup(UserGroup $userGroup): void
{
$this->userGroup = $userGroup;
}
private function collectIds(): void
{
/** @var PiggyBank $piggy */
foreach ($this->collection as $piggy) {
$id = (int)$piggy->id;
$this->ids[] = $id;
$this->currencyIds[$id] = (int)$piggy->transaction_currency_id;
}
$this->ids = array_unique($this->ids);
// collect currencies.
$currencies = TransactionCurrency::whereIn('id', $this->currencyIds)->get();
foreach ($currencies as $currency) {
$this->currencies[(int)$currency->id] = $currency;
}
// collect accounts
$set = DB::table('account_piggy_bank')->whereIn('piggy_bank_id', $this->ids)->get(['piggy_bank_id', 'account_id', 'current_amount', 'native_current_amount']);
foreach ($set as $item) {
$id = (int)$item->piggy_bank_id;
$accountId = (int)$item->account_id;
$this->amounts[$id] ??= [];
if (!array_key_exists($id, $this->accountIds)) {
$this->accountIds[$id] = (int)$item->account_id;
}
if (!array_key_exists($accountId, $this->amounts[$id])) {
$this->amounts[$id][$accountId] = [
'current_amount' => '0',
'pc_current_amount' => '0',
];
}
$this->amounts[$id][$accountId]['current_amount'] = bcadd($this->amounts[$id][$accountId]['current_amount'], $item->current_amount);
if (null !== $this->amounts[$id][$accountId]['pc_current_amount'] && null !== $item->native_current_amount) {
$this->amounts[$id][$accountId]['pc_current_amount'] = bcadd($this->amounts[$id][$accountId]['pc_current_amount'], $item->native_current_amount);
}
}
// get account currency preference for ALL.
$set = AccountMeta::whereIn('account_id', array_values($this->accountIds))->where('name', 'currency_id')->get();
/** @var AccountMeta $item */
foreach ($set as $item) {
$accountId = (int)$item->account_id;
$currencyId = (int)$item->data;
if (!array_key_exists($currencyId, $this->currencies)) {
$this->currencies[$currencyId] = TransactionCurrency::find($currencyId);
}
// $this->accountCurrencies[$accountId] = $this->currencies[$currencyId];
}
// get account info.
$set = Account::whereIn('id', array_values($this->accountIds))->get();
/** @var Account $item */
foreach ($set as $item) {
$id = (int)$item->id;
$this->accounts[$id] = [
'id' => $id,
'name' => $item->name,
];
}
}
private function appendCollectedData(): void
{
$this->collection = $this->collection->map(function (PiggyBank $item) {
$id = (int)$item->id;
$currencyId = (int)$item->transaction_currency_id;
$currency = $this->currencies[$currencyId] ?? $this->primaryCurrency;
$targetAmount = null;
if (0 !== bccomp($item->target_amount, '0')) {
$targetAmount = $item->target_amount;
}
$meta = [
'notes' => $this->notes[$id] ?? null,
'currency' => $this->currencies[$currencyId] ?? null,
// 'auto_budget' => $this->autoBudgets[$id] ?? null,
// 'spent' => $this->spent[$id] ?? null,
// 'pc_spent' => $this->pcSpent[$id] ?? null,
'object_group_id' => null,
'object_group_order' => null,
'object_group_title' => null,
'current_amount' => '0',
'pc_current_amount' => '0',
'target_amount' => null === $targetAmount ? null : Steam::bcround($targetAmount, $currency->decimal_places),
'pc_target_amount' => null === $item->native_target_amount ? null : Steam::bcround($item->native_target_amount, $this->primaryCurrency->decimal_places),
'left_to_save' => null,
'pc_left_to_save' => null,
'save_per_month' => null,
'pc_save_per_month' => null,
'accounts' => [],
];
// add object group if available
if (array_key_exists($id, $this->mappedObjects)) {
$key = $this->mappedObjects[$id];
$meta['object_group_id'] = $this->objectGroups[$key]['id'];
$meta['object_group_title'] = $this->objectGroups[$key]['title'];
$meta['object_group_order'] = $this->objectGroups[$key]['order'];
}
// add current amount(s).
foreach ($this->amounts[$id] as $accountId => $row) {
$meta['accounts'][] = [
'account_id' => (string)$accountId,
'name' => $this->accounts[$accountId]['name'] ?? '',
'current_amount' => Steam::bcround($row['current_amount'], $currency->decimal_places),
'pc_current_amount' => Steam::bcround($row['pc_current_amount'], $this->primaryCurrency->decimal_places),
];
$meta['current_amount'] = bcadd($meta['current_amount'], $row['current_amount']);
// only add pc_current_amount when the pc_current_amount is set
$meta['pc_current_amount'] = null === $row['pc_current_amount'] ? null : bcadd($meta['pc_current_amount'], $row['pc_current_amount']);
}
$meta['current_amount'] = Steam::bcround($meta['current_amount'], $currency->decimal_places);
// only round this number when pc_current_amount is set.
$meta['pc_current_amount'] = null === $meta['pc_current_amount'] ? null : Steam::bcround($meta['pc_current_amount'], $this->primaryCurrency->decimal_places);
// calculate left to save, only when there is a target amount.
if (null !== $targetAmount) {
$meta['left_to_save'] = bcsub($meta['target_amount'], $meta['current_amount']);
$meta['pc_left_to_save'] = null === $meta['pc_target_amount'] ? null : bcsub($meta['pc_target_amount'], $meta['pc_current_amount']);
}
// get suggested per month.
$meta['save_per_month'] = Steam::bcround($this->getSuggestedMonthlyAmount($item->start_date, $item->target_date, $meta['target_amount'], $meta['current_amount']), $currency->decimal_places);
$meta['pc_save_per_month'] = Steam::bcround($this->getSuggestedMonthlyAmount($item->start_date, $item->target_date, $meta['pc_target_amount'], $meta['pc_current_amount']), $currency->decimal_places);
$item->meta = $meta;
return $item;
});
}
private function collectNotes(): void
{
$notes = Note::query()->whereIn('noteable_id', $this->ids)
->whereNotNull('notes.text')
->where('notes.text', '!=', '')
->where('noteable_type', PiggyBank::class)->get(['notes.noteable_id', 'notes.text'])->toArray()
;
foreach ($notes as $note) {
$this->notes[(int)$note['noteable_id']] = (string)$note['text'];
}
Log::debug(sprintf('Enrich with %d note(s)', count($this->notes)));
}
private function collectObjectGroups(): void
{
$set = DB::table('object_groupables')
->whereIn('object_groupable_id', $this->ids)
->where('object_groupable_type', PiggyBank::class)
->get(['object_groupable_id', 'object_group_id'])
;
$ids = array_unique($set->pluck('object_group_id')->toArray());
foreach ($set as $entry) {
$this->mappedObjects[(int)$entry->object_groupable_id] = (int)$entry->object_group_id;
}
$groups = ObjectGroup::whereIn('id', $ids)->get(['id', 'title', 'order'])->toArray();
foreach ($groups as $group) {
$group['id'] = (int)$group['id'];
$group['order'] = (int)$group['order'];
$this->objectGroups[(int)$group['id']] = $group;
}
}
private function collectCurrentAmounts(): void {}
/**
* Returns the suggested amount the user should save per month, or "".
*/
private function getSuggestedMonthlyAmount(?Carbon $startDate, ?Carbon $targetDate, ?string $targetAmount, string $currentAmount): string
{
if (null === $targetAmount || null === $targetDate || null === $startDate) {
return '0';
}
$savePerMonth = '0';
if (1 === bccomp($targetAmount, $currentAmount)) {
$now = today(config('app.timezone'));
$diffInMonths = (int)$startDate->diffInMonths($targetDate);
$remainingAmount = bcsub($targetAmount, $currentAmount);
// more than 1 month to go and still need money to save:
if ($diffInMonths > 0 && 1 === bccomp($remainingAmount, '0')) {
$savePerMonth = bcdiv($remainingAmount, (string)$diffInMonths);
}
// less than 1 month to go but still need money to save:
if (0 === $diffInMonths && 1 === bccomp($remainingAmount, '0')) {
$savePerMonth = $remainingAmount;
}
}
return $savePerMonth;
}
}

View File

@@ -0,0 +1,132 @@
<?php
declare(strict_types=1);
namespace FireflyIII\Support\JsonApi\Enrichments;
use FireflyIII\Models\AccountMeta;
use FireflyIII\Models\PiggyBankEvent;
use FireflyIII\Models\TransactionCurrency;
use FireflyIII\Models\TransactionJournal;
use FireflyIII\Models\UserGroup;
use FireflyIII\Support\Facades\Amount;
use FireflyIII\User;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
class PiggyBankEventEnrichment implements EnrichmentInterface
{
private User $user;
private UserGroup $userGroup;
private Collection $collection;
private array $ids = [];
private array $journalIds = [];
private array $groupIds = [];
private array $accountIds = [];
private array $piggyBankIds = [];
private array $accountCurrencies = [];
private array $currencies = [];
// private bool $convertToPrimary = false;
// private TransactionCurrency $primaryCurrency;
public function __construct()
{
// $this->convertToPrimary = Amount::convertToPrimary();
// $this->primaryCurrency = Amount::getPrimaryCurrency();
}
public function enrich(Collection $collection): Collection
{
$this->collection = $collection;
$this->collectIds();
$this->appendCollectedData();
return $this->collection;
}
public function enrichSingle(array|Model $model): array|Model
{
Log::debug(__METHOD__);
$collection = new Collection([$model]);
$collection = $this->enrich($collection);
return $collection->first();
}
public function setUser(User $user): void
{
$this->user = $user;
$this->setUserGroup($user->userGroup);
}
public function setUserGroup(UserGroup $userGroup): void
{
$this->userGroup = $userGroup;
}
private function collectIds(): void
{
/** @var PiggyBankEvent $event */
foreach ($this->collection as $event) {
$this->ids[] = (int)$event->id;
$this->journalIds[(int)$event->id] = (int)$event->transaction_journal_id;
$this->piggyBankIds[(int)$event->id] = (int)$event->piggy_bank_id;
}
$this->ids = array_unique($this->ids);
// collect groups with journal info.
$set = TransactionJournal::whereIn('id', $this->journalIds)->get(['id', 'transaction_group_id']);
/** @var TransactionJournal $item */
foreach ($set as $item) {
$this->groupIds[(int)$item->id] = (int)$item->transaction_group_id;
}
// collect account info.
$set = DB::table('account_piggy_bank')->whereIn('piggy_bank_id', $this->piggyBankIds)->get(['piggy_bank_id', 'account_id']);
foreach ($set as $item) {
$id = (int)$item->piggy_bank_id;
if (!array_key_exists($id, $this->accountIds)) {
$this->accountIds[$id] = (int)$item->account_id;
}
}
// get account currency preference for ALL.
$set = AccountMeta::whereIn('account_id', array_values($this->accountIds))->where('name', 'currency_id')->get();
/** @var AccountMeta $item */
foreach ($set as $item) {
$accountId = (int)$item->account_id;
$currencyId = (int)$item->data;
if (!array_key_exists($currencyId, $this->currencies)) {
$this->currencies[$currencyId] = TransactionCurrency::find($currencyId);
}
$this->accountCurrencies[$accountId] = $this->currencies[$currencyId];
}
}
private function appendCollectedData(): void
{
$this->collection = $this->collection->map(function (PiggyBankEvent $item) {
$id = (int)$item->id;
$piggyId = (int)$item->piggy_bank_id;
$journalId = (int)$item->transaction_journal_id;
$currency = null;
if (array_key_exists($piggyId, $this->accountIds)) {
$accountId = $this->accountIds[$piggyId];
if (array_key_exists($accountId, $this->accountCurrencies)) {
$currency = $this->accountCurrencies[$accountId];
}
}
$meta = [
'transaction_group_id' => array_key_exists($journalId, $this->groupIds) ? (string)$this->groupIds[$journalId] : null,
'currency' => $currency,
];
$item->meta = $meta;
return $item;
});
}
}

View File

@@ -0,0 +1,586 @@
<?php
declare(strict_types=1);
namespace FireflyIII\Support\JsonApi\Enrichments;
use Carbon\Carbon;
use FireflyIII\Enums\RecurrenceRepetitionWeekend;
use FireflyIII\Enums\TransactionTypeEnum;
use FireflyIII\Exceptions\FireflyException;
use FireflyIII\Factory\CategoryFactory;
use FireflyIII\Models\Account;
use FireflyIII\Models\Bill;
use FireflyIII\Models\Budget;
use FireflyIII\Models\Category;
use FireflyIII\Models\Note;
use FireflyIII\Models\PiggyBank;
use FireflyIII\Models\Preference;
use FireflyIII\Models\Recurrence;
use FireflyIII\Models\RecurrenceRepetition;
use FireflyIII\Models\RecurrenceTransaction;
use FireflyIII\Models\RecurrenceTransactionMeta;
use FireflyIII\Models\TransactionCurrency;
use FireflyIII\Models\TransactionType;
use FireflyIII\Models\UserGroup;
use FireflyIII\Repositories\Recurring\RecurringRepositoryInterface;
use FireflyIII\Support\Facades\Amount;
use FireflyIII\Support\Facades\Preferences;
use FireflyIII\Support\Http\Api\ExchangeRateConverter;
use FireflyIII\User;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Arr;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Log;
class RecurringEnrichment implements EnrichmentInterface
{
private Collection $collection;
private array $ids = [];
private array $transactionTypeIds = [];
private array $transactionTypes = [];
private array $notes = [];
private array $repetitions = [];
private array $transactions = [];
private User $user;
private UserGroup $userGroup;
private string $language = 'en_US';
private array $currencyIds = [];
private array $foreignCurrencyIds = [];
private array $sourceAccountIds = [];
private array $destinationAccountIds = [];
private array $accounts = [];
private array $currencies = [];
private array $recurrenceIds = [];
private TransactionCurrency $primaryCurrency;
private bool $convertToPrimary = false;
public function __construct()
{
$this->primaryCurrency = Amount::getPrimaryCurrency();
$this->convertToPrimary = Amount::convertToPrimary();
}
public function enrich(Collection $collection): Collection
{
$this->collection = $collection;
$this->collectIds();
$this->collectRepetitions();
$this->collectTransactions();
$this->collectCurrencies();
$this->collectNotes();
$this->collectAccounts();
$this->collectTransactionMetaData();
$this->appendCollectedData();
return $this->collection;
}
public function enrichSingle(array|Model $model): array|Model
{
Log::debug(__METHOD__);
$collection = new Collection([$model]);
$collection = $this->enrich($collection);
return $collection->first();
}
public function setUser(User $user): void
{
$this->user = $user;
$this->setUserGroup($user->userGroup);
$this->getLanguage();
}
public function setUserGroup(UserGroup $userGroup): void
{
$this->userGroup = $userGroup;
}
private function collectIds(): void
{
/** @var Recurrence $recurrence */
foreach ($this->collection as $recurrence) {
$id = (int)$recurrence->id;
$typeId = (int)$recurrence->transaction_type_id;
$this->ids[] = $id;
$this->transactionTypeIds[$id] = $typeId;
}
$this->ids = array_unique($this->ids);
// collect transaction types.
$transactionTypes = TransactionType::whereIn('id', array_unique($this->transactionTypeIds))->get();
foreach ($transactionTypes as $transactionType) {
$id = (int)$transactionType->id;
$this->transactionTypes[$id] = TransactionTypeEnum::from($transactionType->type);
}
}
private function collectRepetitions(): void
{
Log::debug('Start of enrichment: collectRepetitions()');
$repository = app(RecurringRepositoryInterface::class);
$repository->setUserGroup($this->userGroup);
$set = RecurrenceRepetition::whereIn('recurrence_id', $this->ids)->get();
/** @var RecurrenceRepetition $repetition */
foreach ($set as $repetition) {
$recurrence = $this->collection->filter(function (Recurrence $item) use ($repetition) {
return (int)$item->id === (int)$repetition->recurrence_id;
})->first();
$fromDate = $recurrence->latest_date ?? $recurrence->first_date;
$id = (int)$repetition->recurrence_id;
$repId = (int)$repetition->id;
$this->repetitions[$id] ??= [];
// get the (future) occurrences for this specific type of repetition:
$amount = 'daily' === $repetition->repetition_type ? 9 : 5;
$set = $repository->getXOccurrencesSince($repetition, $fromDate, now(config('app.timezone')), $amount);
/** @var Carbon $carbon */
foreach ($set as $carbon) {
$occurrences[] = $carbon->toAtomString();
}
$this->repetitions[$id][$repId] = [
'id' => (string)$repId,
'created_at' => $repetition->created_at->toAtomString(),
'updated_at' => $repetition->updated_at->toAtomString(),
'type' => $repetition->repetition_type,
'moment' => (string)$repetition->moment,
'skip' => (int)$repetition->skip,
'weekend' => RecurrenceRepetitionWeekend::from((int)$repetition->weekend),
'description' => $this->getRepetitionDescription($repetition),
'occurrences' => $occurrences,
];
}
Log::debug('End of enrichment: collectRepetitions()');
}
private function collectTransactions(): void
{
$set = RecurrenceTransaction::whereIn('recurrence_id', $this->ids)->get();
/** @var RecurrenceTransaction $transaction */
foreach ($set as $transaction) {
$id = (int)$transaction->recurrence_id;
$transactionId = (int)$transaction->id;
$this->recurrenceIds[$transactionId] = $id;
$this->transactions[$id] ??= [];
$amount = $transaction->amount;
$foreignAmount = $transaction->foreign_amount;
$this->transactions[$id][$transactionId] = [
'id' => (string)$transactionId,
// 'recurrence_id' => $id,
'transaction_currency_id' => (int)$transaction->transaction_currency_id,
'foreign_currency_id' => null === $transaction->foreign_currency_id ? null : (int)$transaction->foreign_currency_id,
'source_id' => (int)$transaction->source_id,
'object_has_currency_setting' => true,
'destination_id' => (int)$transaction->destination_id,
'amount' => $amount,
'foreign_amount' => $foreignAmount,
'pc_amount' => null,
'pc_foreign_amount' => null,
'description' => $transaction->description,
'tags' => [],
'category_id' => null,
'category_name' => null,
'budget_id' => null,
'budget_name' => null,
'piggy_bank_id' => null,
'piggy_bank_name' => null,
'subscription_id' => null,
'subscription_name' => null,
];
// collect all kinds of meta data to be collected later.
$this->currencyIds[$transactionId] = (int)$transaction->transaction_currency_id;
$this->sourceAccountIds[$transactionId] = (int)$transaction->source_id;
$this->destinationAccountIds[$transactionId] = (int)$transaction->destination_id;
if (null !== $transaction->foreign_currency_id) {
$this->foreignCurrencyIds[$transactionId] = (int)$transaction->foreign_currency_id;
}
}
}
private function appendCollectedData(): void
{
$this->collection = $this->collection->map(function (Recurrence $item) {
$id = (int)$item->id;
$meta = [
'notes' => $this->notes[$id] ?? null,
'repetitions' => array_values($this->repetitions[$id] ?? []),
'transactions' => $this->processTransactions(array_values($this->transactions[$id] ?? [])),
];
$item->meta = $meta;
return $item;
});
}
/**
* Parse the repetition in a string that is user readable.
* TODO duplicate with repository.
*/
public function getRepetitionDescription(RecurrenceRepetition $repetition): string
{
if ('daily' === $repetition->repetition_type) {
return (string)trans('firefly.recurring_daily', [], $this->language);
}
if ('weekly' === $repetition->repetition_type) {
$dayOfWeek = trans(sprintf('config.dow_%s', $repetition->repetition_moment), [], $this->language);
if ($repetition->repetition_skip > 0) {
return (string)trans('firefly.recurring_weekly_skip', ['weekday' => $dayOfWeek, 'skip' => $repetition->repetition_skip + 1], $this->language);
}
return (string)trans('firefly.recurring_weekly', ['weekday' => $dayOfWeek], $this->language);
}
if ('monthly' === $repetition->repetition_type) {
if ($repetition->repetition_skip > 0) {
return (string)trans('firefly.recurring_monthly_skip', ['dayOfMonth' => $repetition->repetition_moment, 'skip' => $repetition->repetition_skip + 1], $this->language);
}
return (string)trans('firefly.recurring_monthly', ['dayOfMonth' => $repetition->repetition_moment, 'skip' => $repetition->repetition_skip - 1], $this->language);
}
if ('ndom' === $repetition->repetition_type) {
$parts = explode(',', $repetition->repetition_moment);
// first part is number of week, second is weekday.
$dayOfWeek = trans(sprintf('config.dow_%s', $parts[1]), [], $this->language);
if ($repetition->repetition_skip > 0) {
return (string)trans('firefly.recurring_ndom_skip', ['skip' => $repetition->repetition_skip, 'weekday' => $dayOfWeek, 'dayOfMonth' => $parts[0]], $this->language);
}
return (string)trans('firefly.recurring_ndom', ['weekday' => $dayOfWeek, 'dayOfMonth' => $parts[0]], $this->language);
}
if ('yearly' === $repetition->repetition_type) {
$today = today(config('app.timezone'))->endOfYear();
$repDate = Carbon::createFromFormat('Y-m-d', $repetition->repetition_moment);
if (!$repDate instanceof Carbon) {
$repDate = clone $today;
}
// $diffInYears = (int)$today->diffInYears($repDate, true);
// $repDate->addYears($diffInYears); // technically not necessary.
$string = $repDate->isoFormat((string)trans('config.month_and_day_no_year_js'));
return (string)trans('firefly.recurring_yearly', ['date' => $string], $this->language);
}
return '';
}
private function getLanguage(): void
{
/** @var Preference $preference */
$preference = Preferences::getForUser($this->user, 'language', config('firefly.default_language', 'en_US'));
$language = $preference->data;
if (is_array($language)) {
$language = 'en_US';
}
$language = (string)$language;
$this->language = $language;
}
private function collectCurrencies(): void
{
$all = array_merge(array_unique($this->currencyIds), array_unique($this->foreignCurrencyIds));
$currencies = TransactionCurrency::whereIn('id', array_unique($all))->get();
foreach ($currencies as $currency) {
$id = (int)$currency->id;
$this->currencies[$id] = $currency;
}
}
private function processTransactions(array $transactions): array
{
$return = [];
$converter = new ExchangeRateConverter();
foreach ($transactions as $transaction) {
$currencyId = $transaction['transaction_currency_id'];
$pcAmount = null;
$pcForeignAmount = null;
// set the same amount in the primary currency, if both are the same anyway.
if (true === $this->convertToPrimary && $currencyId === (int)$this->primaryCurrency->id) {
$pcAmount = $transaction['amount'];
}
// convert the amount to the primary currency, if it is not the same.
if (true === $this->convertToPrimary && $currencyId !== (int)$this->primaryCurrency->id) {
$pcAmount = $converter->convert($this->currencies[$currencyId], $this->primaryCurrency, today(), $transaction['amount']);
}
if (null !== $transaction['foreign_amount'] && null !== $transaction['foreign_currency_id']) {
$foreignCurrencyId = $transaction['foreign_currency_id'];
if ($foreignCurrencyId !== $this->primaryCurrency->id) {
$pcForeignAmount = $converter->convert($this->currencies[$foreignCurrencyId], $this->primaryCurrency, today(), $transaction['foreign_amount']);
}
}
$transaction['pc_amount'] = $pcAmount;
$transaction['pc_foreign_amount'] = $pcForeignAmount;
$sourceId = $transaction['source_id'];
$transaction['source_name'] = $this->accounts[$sourceId]->name;
$transaction['source_iban'] = $this->accounts[$sourceId]->iban;
$transaction['source_type'] = $this->accounts[$sourceId]->accountType->type;
$transaction['source_id'] = (string)$transaction['source_id'];
$destId = $transaction['destination_id'];
$transaction['destination_name'] = $this->accounts[$destId]->name;
$transaction['destination_iban'] = $this->accounts[$destId]->iban;
$transaction['destination_type'] = $this->accounts[$destId]->accountType->type;
$transaction['destination_id'] = (string)$transaction['destination_id'];
$transaction['currency_id'] = (string)$currencyId;
$transaction['currency_name'] = $this->currencies[$currencyId]->name;
$transaction['currency_code'] = $this->currencies[$currencyId]->code;
$transaction['currency_symbol'] = $this->currencies[$currencyId]->symbol;
$transaction['currency_decimal_places'] = $this->currencies[$currencyId]->decimal_places;
$transaction['primary_currency_id'] = (string)$this->primaryCurrency->id;
$transaction['primary_currency_name'] = $this->primaryCurrency->name;
$transaction['primary_currency_code'] = $this->primaryCurrency->code;
$transaction['primary_currency_symbol'] = $this->primaryCurrency->symbol;
$transaction['primary_currency_decimal_places'] = $this->primaryCurrency->decimal_places;
// $transaction['foreign_currency_id'] = null;
$transaction['foreign_currency_name'] = null;
$transaction['foreign_currency_code'] = null;
$transaction['foreign_currency_symbol'] = null;
$transaction['foreign_currency_decimal_places'] = null;
if (null !== $transaction['foreign_currency_id']) {
$currencyId = $transaction['foreign_currency_id'];
$transaction['foreign_currency_id'] = (string)$currencyId;
$transaction['foreign_currency_name'] = $this->currencies[$currencyId]->name;
$transaction['foreign_currency_code'] = $this->currencies[$currencyId]->code;
$transaction['foreign_currency_symbol'] = $this->currencies[$currencyId]->symbol;
$transaction['foreign_currency_decimal_places'] = $this->currencies[$currencyId]->decimal_places;
}
unset($transaction['transaction_currency_id']);
$return[] = $transaction;
}
return $return;
}
private function collectAccounts(): void
{
$all = array_merge(array_unique($this->sourceAccountIds), array_unique($this->destinationAccountIds));
$accounts = Account::with(['accountType'])->whereIn('id', array_unique($all))->get();
/** @var Account $account */
foreach ($accounts as $account) {
$id = (int)$account->id;
$this->accounts[$id] = $account;
}
}
private function collectTransactionMetaData(): void
{
$ids = array_keys($this->transactions);
$meta = RecurrenceTransactionMeta::whereIn('rt_id', $ids)->get();
// other meta-data to be collected:
$billIds = [];
$piggyBankIds = [];
$categoryIds = [];
$categoryNames = [];
$budgetIds = [];
foreach ($meta as $entry) {
$id = (int)$entry->id;
$transactionId = (int)$entry->rt_id;
$recurrenceId = $this->recurrenceIds[$transactionId];
$name = (string)$entry->name;
switch ($name) {
default:
throw new FireflyException(sprintf('Recurrence transformer cant handle field "%s"', $name));
case 'bill_id':
if ((int)$entry->value > 0) {
$this->transactions[$recurrenceId][$transactionId]['subscription_id'] = $entry->value;
if (!array_key_exists($id, $billIds)) {
$billIds[$id] = [
'recurrence_id' => $recurrenceId,
'transaction_id' => $transactionId,
'bill_id' => (int)$entry->value,
];
}
}
break;
case 'tags':
$this->transactions[$recurrenceId][$transactionId]['tags'] = json_decode((string)$entry->value);
break;
case 'piggy_bank_id':
if ((int)$entry->value > 0) {
$this->transactions[$recurrenceId][$transactionId]['piggy_bank_id'] = (string)$entry->value;
if (!array_key_exists($id, $piggyBankIds)) {
$piggyBankIds[$id] = [
'recurrence_id' => $recurrenceId,
'transaction_id' => $transactionId,
'piggy_bank_id' => (int)$entry->value,
];
}
}
break;
case 'category_id':
if ((int)$entry->value > 0) {
$this->transactions[$recurrenceId][$transactionId]['category_id'] = (string)$entry->value;
if (!array_key_exists($id, $categoryIds)) {
$categoryIds[$id] = [
'recurrence_id' => $recurrenceId,
'transaction_id' => $transactionId,
'category_id' => (int)$entry->value,
];
}
}
break;
case 'category_name':
if ('' !== (string)$entry->value) {
$this->transactions[$recurrenceId][$transactionId]['category_name'] = (string)$entry->value;
if (!array_key_exists($id, $categoryIds)) {
$categoryNames[$id] = [
'recurrence_id' => $recurrenceId,
'transaction_id' => $transactionId,
'category_name' => $entry->value,
];
}
}
break;
case 'budget_id':
if ((int)$entry->value > 0) {
$this->transactions[$recurrenceId][$transactionId]['budget_id'] = (string)$entry->value;
if (!array_key_exists($id, $budgetIds)) {
$budgetIds[$id] = [
'recurrence_id' => $recurrenceId,
'transaction_id' => $transactionId,
'budget_id' => (int)$entry->value,
];
}
}
break;
}
}
$this->collectBillInfo($billIds);
$this->collectPiggyBankInfo($piggyBankIds);
$this->collectCategoryIdInfo($categoryIds);
$this->collectCategoryNameInfo($categoryNames);
$this->collectBudgetInfo($budgetIds);
}
private function collectBillInfo(array $billIds): void
{
if (0 === count($billIds)) {
return;
}
$ids = Arr::pluck($billIds, 'bill_id');
$bills = Bill::whereIn('id', $ids)->get();
$mapped = [];
foreach ($bills as $bill) {
$mapped[(int)$bill->id] = $bill;
}
foreach ($billIds as $info) {
$recurrenceId = $info['recurrence_id'];
$transactionId = $info['transaction_id'];
$this->transactions[$recurrenceId][$transactionId]['subscription_name'] = $mapped[$info['bill_id']]->name ?? '';
}
}
private function collectPiggyBankInfo(array $piggyBankIds): void
{
if (0 === count($piggyBankIds)) {
return;
}
$ids = Arr::pluck($piggyBankIds, 'piggy_bank_id');
$piggyBanks = PiggyBank::whereIn('id', $ids)->get();
$mapped = [];
foreach ($piggyBanks as $piggyBank) {
$mapped[(int)$piggyBank->id] = $piggyBank;
}
foreach ($piggyBankIds as $info) {
$recurrenceId = $info['recurrence_id'];
$transactionId = $info['transaction_id'];
$this->transactions[$recurrenceId][$transactionId]['piggy_bank_name'] = $mapped[$info['piggy_bank_id']]->name ?? '';
}
}
private function collectCategoryIdInfo(array $categoryIds): void
{
if (0 === count($categoryIds)) {
return;
}
$ids = Arr::pluck($categoryIds, 'category_id');
$categories = Category::whereIn('id', $ids)->get();
$mapped = [];
foreach ($categories as $category) {
$mapped[(int)$category->id] = $category;
}
foreach ($categoryIds as $info) {
$recurrenceId = $info['recurrence_id'];
$transactionId = $info['transaction_id'];
$this->transactions[$recurrenceId][$transactionId]['category_name'] = $mapped[$info['category_id']]->name ?? '';
}
}
/**
* TODO This method does look-up in a loop.
*/
private function collectCategoryNameInfo(array $categoryNames): void
{
if (0 === count($categoryNames)) {
return;
}
$factory = app(CategoryFactory::class);
$factory->setUser($this->user);
foreach ($categoryNames as $info) {
$recurrenceId = $info['recurrence_id'];
$transactionId = $info['transaction_id'];
$category = $factory->findOrCreate(null, $info['category_name']);
if (null !== $category) {
$this->transactions[$recurrenceId][$transactionId]['category_id'] = (string)$category->id;
$this->transactions[$recurrenceId][$transactionId]['category_name'] = $category->name;
}
}
}
private function collectBudgetInfo(array $budgetIds): void
{
if (0 === count($budgetIds)) {
return;
}
$ids = Arr::pluck($budgetIds, 'budget_id');
$categories = Budget::whereIn('id', $ids)->get();
$mapped = [];
foreach ($categories as $category) {
$mapped[(int)$category->id] = $category;
}
foreach ($budgetIds as $info) {
$recurrenceId = $info['recurrence_id'];
$transactionId = $info['transaction_id'];
$this->transactions[$recurrenceId][$transactionId]['budget_name'] = $mapped[$info['budget_id']]->name ?? '';
}
}
private function collectNotes(): void
{
$notes = Note::query()->whereIn('noteable_id', $this->ids)
->whereNotNull('notes.text')
->where('notes.text', '!=', '')
->where('noteable_type', Recurrence::class)->get(['notes.noteable_id', 'notes.text'])->toArray()
;
foreach ($notes as $note) {
$this->notes[(int)$note['noteable_id']] = (string)$note['text'];
}
Log::debug(sprintf('Enrich with %d note(s)', count($this->notes)));
}
}

View File

@@ -27,15 +27,15 @@ class SubscriptionEnrichment implements EnrichmentInterface
private User $user;
private UserGroup $userGroup;
private Collection $collection;
private bool $convertToPrimary = false;
private ?Carbon $start = null;
private ?Carbon $end = null;
private array $subscriptionIds = [];
private array $objectGroups = [];
private array $mappedObjects = [];
private array $paidDates = [];
private array $notes = [];
private array $payDates = [];
private bool $convertToPrimary;
private ?Carbon $start = null;
private ?Carbon $end = null;
private array $subscriptionIds = [];
private array $objectGroups = [];
private array $mappedObjects = [];
private array $paidDates = [];
private array $notes = [];
private array $payDates = [];
private TransactionCurrency $primaryCurrency;
private BillDateCalculator $calculator;
@@ -56,6 +56,9 @@ class SubscriptionEnrichment implements EnrichmentInterface
$this->collectPaidDates();
$this->collectPayDates();
// TODO clean me up.
$notes = $this->notes;
$objectGroups = $this->objectGroups;
$paidDates = $this->paidDates;
@@ -342,7 +345,7 @@ class SubscriptionEnrichment implements EnrichmentInterface
private function getLastPaidDate(array $paidData): ?Carbon
{
Log::debug('getLastPaidDate()');
// Log::debug('getLastPaidDate()');
$return = null;
foreach ($paidData as $entry) {
if (null !== $return) {
@@ -351,15 +354,15 @@ class SubscriptionEnrichment implements EnrichmentInterface
if ($current->gt($return)) {
$return = clone $current;
}
Log::debug(sprintf('Last paid date is: %s', $return->format('Y-m-d')));
Log::debug(sprintf('[a] Last paid date is: %s', $return->format('Y-m-d')));
}
if (null === $return) {
/** @var Carbon $return */
$return = $entry['date_object'];
Log::debug(sprintf('Last paid date is: %s', $return->format('Y-m-d')));
Log::debug(sprintf('[b] Last paid date is: %s', $return->format('Y-m-d')));
}
}
Log::debug(sprintf('Last paid date is: "%s"', $return?->format('Y-m-d')));
Log::debug(sprintf('[c] Last paid date is: "%s"', $return?->format('Y-m-d')));
return $return;
}

View File

@@ -45,26 +45,20 @@ use Override;
class TransactionGroupEnrichment implements EnrichmentInterface
{
private array $attachmentCount;
private array $attachmentCount = [];
private Collection $collection;
private readonly array $dateFields;
private array $journalIds;
private array $locations;
private array $metaData; // @phpstan-ignore-line
private array $notes; // @phpstan-ignore-line
private array $tags;
private array $journalIds = [];
private array $locations = [];
private array $metaData = [];
private array $notes = [];
private array $tags = [];
private User $user;
private readonly TransactionCurrency $primaryCurrency;
private UserGroup $userGroup;
public function __construct()
{
$this->notes = [];
$this->journalIds = [];
$this->tags = [];
$this->metaData = [];
$this->locations = [];
$this->attachmentCount = [];
$this->dateFields = ['interest_date', 'book_date', 'process_date', 'due_date', 'payment_date', 'invoice_date'];
$this->primaryCurrency = Amount::getPrimaryCurrency();
}

View File

@@ -110,8 +110,8 @@ class BillDateCalculator
$currentStart = clone $nextExpectedMatch;
++$loop;
if ($loop > 12) {
Log::debug('Loop is more than 12, so we break.');
if ($loop > 31) {
Log::debug('Loop is more than 31, so we break.');
break;
}

View File

@@ -25,6 +25,7 @@ namespace FireflyIII\Support;
use FireflyIII\Exceptions\FireflyException;
use FireflyIII\Models\Preference;
use FireflyIII\Support\Singleton\PreferencesSingleton;
use FireflyIII\User;
use Illuminate\Contracts\Encryption\DecryptException;
use Illuminate\Contracts\Encryption\EncryptException;
@@ -283,6 +284,12 @@ class Preferences
*/
public function lastActivity(): string
{
$instance = PreferencesSingleton::getInstance();
$pref = $instance->getPreference('last_activity');
if (null !== $pref) {
// Log::debug(sprintf('Found last activity in singleton: %s', $pref));
return $pref;
}
$lastActivity = microtime();
$preference = $this->get('lastActivity', microtime());
@@ -292,13 +299,17 @@ class Preferences
if (is_array($lastActivity)) {
$lastActivity = implode(',', $lastActivity);
}
$setting = hash('sha256', (string) $lastActivity);
$instance->setPreference('last_activity', $setting);
return hash('sha256', (string) $lastActivity);
return $setting;
}
public function mark(): void
{
$this->set('lastActivity', microtime());
$instance = PreferencesSingleton::getInstance();
$instance->setPreference('last_activity', microtime());
Session::forget('first');
}

View File

@@ -25,6 +25,7 @@ declare(strict_types=1);
namespace FireflyIII\Support\Repositories\Recurring;
use Carbon\Carbon;
use Illuminate\Support\Facades\Log;
/**
* Class CalculateXOccurrencesSince
@@ -37,7 +38,7 @@ trait CalculateXOccurrencesSince
*/
protected function getXDailyOccurrencesSince(Carbon $date, Carbon $afterDate, int $count, int $skipMod): array
{
app('log')->debug(sprintf('Now in %s', __METHOD__));
Log::debug(sprintf('Now in %s', __METHOD__));
$return = [];
$mutator = clone $date;
$total = 0;
@@ -62,7 +63,7 @@ trait CalculateXOccurrencesSince
*/
protected function getXMonthlyOccurrencesSince(Carbon $date, Carbon $afterDate, int $count, int $skipMod, string $moment): array
{
app('log')->debug(sprintf('Now in %s(%s, %s, %d)', __METHOD__, $date->format('Y-m-d'), $afterDate->format('Y-m-d'), $count));
Log::debug(sprintf('Now in %s(%s, %s, %d)', __METHOD__, $date->format('Y-m-d'), $afterDate->format('Y-m-d'), $count));
$return = [];
$mutator = clone $date;
$total = 0;
@@ -70,24 +71,25 @@ trait CalculateXOccurrencesSince
$dayOfMonth = (int) $moment;
$dayOfMonth = 0 === $dayOfMonth ? 1 : $dayOfMonth;
if ($mutator->day > $dayOfMonth) {
app('log')->debug(sprintf('%d is after %d, add a month. Mutator is now', $mutator->day, $dayOfMonth));
Log::debug(sprintf('%d is after %d, add a month. Mutator is now...', $mutator->day, $dayOfMonth));
// day has passed already, add a month.
$mutator->addMonth();
app('log')->debug(sprintf('%s', $mutator->format('Y-m-d')));
Log::debug(sprintf('%s', $mutator->toAtomString()));
}
while ($total < $count) {
$domCorrected = min($dayOfMonth, $mutator->daysInMonth);
$mutator->day = $domCorrected;
app('log')->debug(sprintf('Mutator is now %s', $mutator->format('Y-m-d')));
$mutator->setTime(0, 0, 0);
if (0 === $attempts % $skipMod && $mutator->gte($afterDate)) {
app('log')->debug('Is added to the list.');
Log::debug(sprintf('Mutator is now %s and is added to the list.', $mutator->toAtomString()));
$return[] = clone $mutator;
++$total;
}
++$attempts;
$mutator = $mutator->endOfMonth()->addDay();
}
Log::debug('Collected enough occurrences.');
return $return;
}
@@ -100,7 +102,7 @@ trait CalculateXOccurrencesSince
*/
protected function getXNDomOccurrencesSince(Carbon $date, Carbon $afterDate, int $count, int $skipMod, string $moment): array
{
app('log')->debug(sprintf('Now in %s', __METHOD__));
Log::debug(sprintf('Now in %s', __METHOD__));
$return = [];
$total = 0;
$attempts = 0;
@@ -134,7 +136,7 @@ trait CalculateXOccurrencesSince
*/
protected function getXWeeklyOccurrencesSince(Carbon $date, Carbon $afterDate, int $count, int $skipMod, string $moment): array
{
app('log')->debug(sprintf('Now in %s', __METHOD__));
Log::debug(sprintf('Now in %s', __METHOD__));
$return = [];
$total = 0;
$attempts = 0;
@@ -173,7 +175,7 @@ trait CalculateXOccurrencesSince
*/
protected function getXYearlyOccurrencesSince(Carbon $date, Carbon $afterDate, int $count, int $skipMod, string $moment): array
{
app('log')->debug(sprintf('Now in %s(%s, %d, %d, %s)', __METHOD__, $date->format('Y-m-d'), $date->format('Y-m-d'), $count, $skipMod));
Log::debug(sprintf('Now in %s(%s, %d, %d, %s)', __METHOD__, $date->format('Y-m-d'), $date->format('Y-m-d'), $count, $skipMod));
$return = [];
$mutator = clone $date;
$total = 0;
@@ -181,19 +183,19 @@ trait CalculateXOccurrencesSince
$date = new Carbon($moment);
$date->year = $mutator->year;
if ($mutator > $date) {
app('log')->debug(
Log::debug(
sprintf('mutator (%s) > date (%s), so add a year to date (%s)', $mutator->format('Y-m-d'), $date->format('Y-m-d'), $date->format('Y-m-d'))
);
$date->addYear();
app('log')->debug(sprintf('Date is now %s', $date->format('Y-m-d')));
Log::debug(sprintf('Date is now %s', $date->format('Y-m-d')));
}
$obj = clone $date;
while ($total < $count) {
app('log')->debug(sprintf('total (%d) < count (%d) so go.', $total, $count));
app('log')->debug(sprintf('attempts (%d) %% skipmod (%d) === %d', $attempts, $skipMod, $attempts % $skipMod));
app('log')->debug(sprintf('Obj (%s) gte afterdate (%s)? %s', $obj->format('Y-m-d'), $afterDate->format('Y-m-d'), var_export($obj->gte($afterDate), true)));
Log::debug(sprintf('total (%d) < count (%d) so go.', $total, $count));
Log::debug(sprintf('attempts (%d) %% skipmod (%d) === %d', $attempts, $skipMod, $attempts % $skipMod));
Log::debug(sprintf('Obj (%s) gte afterdate (%s)? %s', $obj->format('Y-m-d'), $afterDate->format('Y-m-d'), var_export($obj->gte($afterDate), true)));
if (0 === $attempts % $skipMod && $obj->gte($afterDate)) {
app('log')->debug('All conditions true, add obj.');
Log::debug('All conditions true, add obj.');
$return[] = clone $obj;
++$total;
}

View File

@@ -0,0 +1,41 @@
<?php
declare(strict_types=1);
namespace FireflyIII\Support\Singleton;
class PreferencesSingleton
{
private static ?PreferencesSingleton $instance = null;
private array $preferences = [];
private function __construct()
{
// Private constructor to prevent direct instantiation.
}
public static function getInstance(): self
{
if (null === self::$instance) {
self::$instance = new self();
}
return self::$instance;
}
public function resetPreferences(): void
{
$this->preferences = [];
}
public function setPreference(string $key, mixed $value): void
{
$this->preferences[$key] = $value;
}
public function getPreference(string $key): mixed
{
return $this->preferences[$key] ?? null;
}
}

View File

@@ -24,21 +24,23 @@ declare(strict_types=1);
namespace FireflyIII\Support;
use Carbon\Carbon;
use Exception;
use FireflyIII\Exceptions\FireflyException;
use FireflyIII\Models\Account;
use FireflyIII\Models\AccountMeta;
use FireflyIII\Models\Transaction;
use FireflyIII\Models\TransactionCurrency;
use FireflyIII\Support\Facades\Amount;
use FireflyIII\Support\Http\Api\ExchangeRateConverter;
use FireflyIII\Support\Singleton\PreferencesSingleton;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Str;
use Exception;
use ValueError;
use function Safe\preg_replace;
use function Safe\parse_url;
use function Safe\preg_replace;
/**
* Class Steam.
@@ -277,7 +279,7 @@ class Steam
$carbon = new Carbon($entry->date, $entry->date_tz);
$carbonKey = $carbon->format('Y-m-d');
// make sure sum is a string:
$sumOfDay = (string) ($entry->sum_of_day ?? '0');
$sumOfDay = (string)($entry->sum_of_day ?? '0');
// #10426 make sure sum is not in scientific notation.
$sumOfDay = $this->floatalize($sumOfDay);
@@ -292,20 +294,20 @@ class Steam
// add amount to current balance in currency code.
$currentBalance[$entryCurrency->code] ??= '0';
$currentBalance[$entryCurrency->code] = bcadd($sumOfDay, (string) $currentBalance[$entryCurrency->code]);
$currentBalance[$entryCurrency->code] = bcadd($sumOfDay, (string)$currentBalance[$entryCurrency->code]);
// if not requested to convert to primary currency, add the amount to "balance", do nothing else.
if (!$convertToPrimary) {
$currentBalance['balance'] = bcadd((string) $currentBalance['balance'], $sumOfDay);
$currentBalance['balance'] = bcadd((string)$currentBalance['balance'], $sumOfDay);
}
// if convert to primary currency add the converted amount to "pc_balance".
// if there is a request to convert, convert to "pc_balance" and use "balance" for whichever amount is in the primary currency.
if ($convertToPrimary) {
$pcSumOfDay = $converter->convert($entryCurrency, $primaryCurrency, $carbon, $sumOfDay);
$currentBalance['pc_balance'] = bcadd((string) ($currentBalance['pc_balance'] ?? '0'), $pcSumOfDay);
$currentBalance['pc_balance'] = bcadd((string)($currentBalance['pc_balance'] ?? '0'), $pcSumOfDay);
// if it's the same currency as the entry, also add to balance (see other code).
if ($currency->id === $entryCurrency->id) {
$currentBalance['balance'] = bcadd((string) $currentBalance['balance'], $sumOfDay);
$currentBalance['balance'] = bcadd((string)$currentBalance['balance'], $sumOfDay);
}
}
// add to final array.
@@ -318,6 +320,82 @@ class Steam
return $balances;
}
public function accountsBalancesOptimized(Collection $accounts, Carbon $date, ?TransactionCurrency $primary = null, ?bool $convertToPrimary = null): array
{
Log::debug(sprintf('accountsBalancesOptimized: Called for %d account(s) with date/time "%s"', $accounts->count(), $date->toIso8601String()));
$result = [];
$convertToPrimary ??= Amount::convertToPrimary();
$primary ??= Amount::getPrimaryCurrency();
$currencies = $this->getCurrencies($accounts);
// balance(s) in all currencies for ALL accounts.
$arrayOfSums = Transaction::whereIn('account_id', $accounts->pluck('id')->toArray())
->leftJoin('transaction_journals', 'transaction_journals.id', '=', 'transactions.transaction_journal_id')
->leftJoin('transaction_currencies', 'transaction_currencies.id', '=', 'transactions.transaction_currency_id')
->where('transaction_journals.date', '<=', $date->format('Y-m-d H:i:s'))
->groupBy(['transactions.account_id', 'transaction_currencies.code'])
->get(['transactions.account_id', 'transaction_currencies.code', DB::raw('SUM(transactions.amount) as sum_of_amount')])->toArray()
;
/** @var Account $account */
foreach ($accounts as $account) {
// this array is PER account, so we wait a bit before we change code here.
$return = [
'pc_balance' => '0',
'balance' => '0', // this key is overwritten right away, but I must remember it is always created.
];
$currency = $currencies[$account->id];
// second array
$accountSum = array_filter($arrayOfSums, function ($entry) use ($account) {
return $entry['account_id'] === $account->id;
});
if (0 === count($accountSum)) {
$result[$account->id] = $return;
continue;
}
$accountSum = array_values($accountSum)[0];
$sumsByCode = [
$accountSum['code'] => $accountSum['sum_of_amount'],
];
// Log::debug('All balances are (joined)', $others);
// if there is no request to convert, take this as "balance" and "pc_balance".
$return['balance'] = $sumsByCode[$currency->code] ?? '0';
if (!$convertToPrimary) {
unset($return['pc_balance']);
// Log::debug(sprintf('Set balance to %s, unset pc_balance', $return['balance']));
}
// if there is a request to convert, convert to "pc_balance" and use "balance" for whichever amount is in the primary currency.
if ($convertToPrimary) {
$return['pc_balance'] = $this->convertAllBalances($sumsByCode, $primary, $date);
// Log::debug(sprintf('Set pc_balance to %s', $return['pc_balance']));
}
// either way, the balance is always combined with the virtual balance:
$virtualBalance = (string)('' === (string)$account->virtual_balance ? '0' : $account->virtual_balance);
if ($convertToPrimary) {
// the primary currency balance is combined with a converted virtual_balance:
$converter = new ExchangeRateConverter();
$pcVirtualBalance = $converter->convert($currency, $primary, $date, $virtualBalance);
$return['pc_balance'] = bcadd($pcVirtualBalance, $return['pc_balance']);
// Log::debug(sprintf('Primary virtual balance makes the primary total %s', $return['pc_balance']));
}
if (!$convertToPrimary) {
// if not, also increase the balance + primary balance for consistency.
$return['balance'] = bcadd($return['balance'], $virtualBalance);
// Log::debug(sprintf('Virtual balance makes the (primary currency) total %s', $return['balance']));
}
$final = array_merge($return, $sumsByCode);
$result[$account->id] = $final;
// Log::debug('Final balance is', $final);
}
return $result;
}
/**
* Returns smaller than or equal to, so be careful with END OF DAY.
*
@@ -340,7 +418,7 @@ class Steam
if ($cache->has()) {
Log::debug(sprintf('CACHED finalAccountBalance(#%d, %s)', $account->id, $date->format('Y-m-d H:i:s')));
return $cache->get();
// return $cache->get();
}
// Log::debug(sprintf('finalAccountBalance(#%d, %s)', $account->id, $date->format('Y-m-d H:i:s')));
if (null === $convertToPrimary) {
@@ -361,8 +439,8 @@ class Steam
$hasCurrency = null !== $accountCurrency;
$currency = $hasCurrency ? $accountCurrency : $primary;
$return = [
'pc_balance' => '0',
'balance' => '0', // this key is overwritten right away, but I must remember it is always created.
'pc_balance' => '0',
'balance' => '0', // this key is overwritten right away, but I must remember it is always created.
];
// balance(s) in all currencies.
$array = $account->transactions()
@@ -386,7 +464,7 @@ class Steam
}
// either way, the balance is always combined with the virtual balance:
$virtualBalance = (string) ('' === (string) $account->virtual_balance ? '0' : $account->virtual_balance);
$virtualBalance = (string)('' === (string)$account->virtual_balance ? '0' : $account->virtual_balance);
if ($convertToPrimary) {
// the primary currency balance is combined with a converted virtual_balance:
@@ -421,7 +499,7 @@ class Steam
return null;
}
return TransactionCurrency::find((int) $result->data);
return TransactionCurrency::find((int)$result->data);
}
private function groupAndSumTransactions(array $array, string $group, string $field): array
@@ -430,7 +508,7 @@ class Steam
foreach ($array as $item) {
$groupKey = $item[$group] ?? 'unknown';
$return[$groupKey] = bcadd($return[$groupKey] ?? '0', (string) $item[$field]);
$return[$groupKey] = bcadd($return[$groupKey] ?? '0', (string)$item[$field]);
}
return $return;
@@ -440,30 +518,27 @@ class Steam
{
$total = '0';
$converter = new ExchangeRateConverter();
$singleton = PreferencesSingleton::getInstance();
foreach ($others as $key => $amount) {
$currency = TransactionCurrency::where('code', $key)->first();
$preference = $singleton->getPreference($key);
$currency = $preference ?? TransactionCurrency::where('code', $key)->first();
if (null === $currency) {
continue;
}
$current = $converter->convert($currency, $primary, $date, $amount);
Log::debug(sprintf('Convert %s %s to %s %s', $currency->code, $amount, $primary->code, $current));
$total = bcadd($current, $total);
if (null === $preference) {
$singleton->setPreference($key, $currency);
}
$current = $amount;
if ($currency->id !== $primary->id) {
$current = $converter->convert($currency, $primary, $date, $amount);
Log::debug(sprintf('Convert %s %s to %s %s', $currency->code, $amount, $primary->code, $current));
}
$total = bcadd($current, $total);
}
return $total;
}
public function finalAccountsBalance(Collection $accounts, Carbon $date): array
{
Log::debug(sprintf('finalAccountsBalance: Call finalAccountBalance with date/time "%s"', $date->toIso8601String()));
$balances = [];
foreach ($accounts as $account) {
$balances[$account->id] = $this->finalAccountBalance($account, $date);
}
return $balances;
}
/**
* @throws FireflyException
*/
@@ -478,11 +553,11 @@ class Steam
$hostName = $ipAddress;
}
if ('' !== (string) $hostName && $hostName !== $ipAddress) {
if ('' !== (string)$hostName && $hostName !== $ipAddress) {
$host = $hostName;
}
return (string) $host;
return (string)$host;
}
public function getLastActivities(array $accounts): array
@@ -497,9 +572,9 @@ class Steam
/** @var Transaction $entry */
foreach ($set as $entry) {
$date = new Carbon($entry->max_date, config('app.timezone'));
$date = new Carbon($entry->max_date, config('app.timezone'));
$date->setTimezone(config('app.timezone'));
$list[(int) $entry->account_id] = $date;
$list[(int)$entry->account_id] = $date;
}
return $list;
@@ -517,7 +592,7 @@ class Steam
if ('equal' === $locale) {
$locale = $this->getLanguage();
}
$locale = (string) $locale;
$locale = (string)$locale;
// Check for Windows to replace the locale correctly.
if ('WIN' === strtoupper(substr(PHP_OS, 0, 3))) {
@@ -617,20 +692,20 @@ class Steam
}
Log::debug(sprintf('Floatalizing %s', $value));
$number = substr($value, 0, (int) strpos($value, 'E'));
$number = substr($value, 0, (int)strpos($value, 'E'));
if (str_contains($number, '.')) {
$post = strlen(substr($number, (int) strpos($number, '.') + 1));
$mantis = substr($value, (int) strpos($value, 'E') + 1);
$post = strlen(substr($number, (int)strpos($number, '.') + 1));
$mantis = substr($value, (int)strpos($value, 'E') + 1);
if ($mantis < 0) {
$post += abs((int) $mantis);
$post += abs((int)$mantis);
}
// TODO careless float could break financial math.
return number_format((float) $value, $post, '.', '');
return number_format((float)$value, $post, '.', '');
}
// TODO careless float could break financial math.
return number_format((float) $value, 0, '.', '');
return number_format((float)$value, 0, '.', '');
}
public function opposite(?string $amount = null): ?string
@@ -650,24 +725,24 @@ class Steam
// has a K in it, remove the K and multiply by 1024.
$bytes = bcmul(rtrim($string, 'k'), '1024');
return (int) $bytes;
return (int)$bytes;
}
if (false !== stripos($string, 'm')) {
// has a M in it, remove the M and multiply by 1048576.
$bytes = bcmul(rtrim($string, 'm'), '1048576');
return (int) $bytes;
return (int)$bytes;
}
if (false !== stripos($string, 'g')) {
// has a G in it, remove the G and multiply by (1024)^3.
$bytes = bcmul(rtrim($string, 'g'), '1073741824');
return (int) $bytes;
return (int)$bytes;
}
return (int) $string;
return (int)$string;
}
public function positive(string $amount): string
@@ -689,4 +764,48 @@ class Steam
return $amount;
}
private function getCurrencies(Collection $accounts): array
{
$currencies = [];
$accountCurrencies = [];
$accountPreferences = [];
$primary = Amount::getPrimaryCurrency();
$currencies[$primary->id] = $primary;
$ids = $accounts->pluck('id')->toArray();
$result = AccountMeta::whereIn('account_id', $ids)->where('name', 'currency_id')->get();
/** @var AccountMeta $item */
foreach ($result as $item) {
$integer = (int)$item->data;
if (0 !== $integer) {
$accountPreferences[(int)$item->account_id] = $integer;
}
}
// collect those currencies, skip primary because we already have it.
$set = TransactionCurrency::whereIn('id', $accountPreferences)->where('id', '!=', $primary->id)->get();
foreach ($set as $item) {
$currencies[$item->id] = $item;
}
/** @var Account $account */
foreach ($accounts as $account) {
$accountId = $account->id;
$currencyPresent = isset($account->meta) && array_key_exists('currency', $account->meta) && null !== $account->meta['currency'];
if ($currencyPresent) {
$currencyId = $account->meta['currency']->id;
$currencies[$currencyId] ??= $account->meta['currency'];
$accountCurrencies[$accountId] = $account->meta['currency'];
}
if (!$currencyPresent && !array_key_exists($accountId, $accountPreferences)) {
$accountCurrencies[$accountId] = $primary;
}
if (!$currencyPresent && array_key_exists($accountId, $accountPreferences)) {
$accountCurrencies[$accountId] = $currencies[$accountPreferences[$account->id]];
}
}
return $accountCurrencies;
}
}

View File

@@ -108,6 +108,10 @@ class AccountTransformer extends AbstractTransformer
'type' => strtolower($accountType),
'account_role' => $accountRole,
'object_group_id' => $account->meta['object_group_id'],
'object_group_order' => $account->meta['object_group_order'],
'object_group_title' => $account->meta['object_group_title'],
// currency information, structured for 6.3.0.
'object_has_currency_setting' => $hasCurrencySettings,
@@ -141,7 +145,7 @@ class AccountTransformer extends AbstractTransformer
'notes' => $account->meta['notes'] ?? null,
'monthly_payment_date' => $monthlyPaymentDate,
'credit_card_type' => $creditCardType,
'account_number' => $account->meta['account_number'] ?? null,
'account_number' => $account->meta['account_number'],
'iban' => '' === $account->iban ? null : $account->iban,
'bic' => $account->meta['BIC'] ?? null,
'opening_balance_date' => $openingBalanceDate,
@@ -153,7 +157,7 @@ class AccountTransformer extends AbstractTransformer
'longitude' => $longitude,
'latitude' => $latitude,
'zoom_level' => $zoomLevel,
'last_activity' => array_key_exists('last_activity', $account->meta) ? $account->meta['last_activity']->toAtomString() : null,
'last_activity' => $account->meta['last_activity']?->toAtomString(),
'links' => [
[
'rel' => 'self',

View File

@@ -55,7 +55,6 @@ class AttachmentTransformer extends AbstractTransformer
'updated_at' => $attachment->updated_at->toAtomString(),
'attachable_id' => (string) $attachment->attachable_id,
'attachable_type' => str_replace('FireflyIII\Models\\', '', $attachment->attachable_type),
'md5' => $attachment->md5,
'hash' => $attachment->md5,
'filename' => $attachment->filename,
'download_url' => route('api.v1.attachments.download', [$attachment->id]),

View File

@@ -51,7 +51,7 @@ class AvailableBudgetTransformer extends AbstractTransformer
*/
public function transform(AvailableBudget $availableBudget): array
{
$currency = $availableBudget->transactionCurrency;
$currency = $availableBudget->meta['currency'];
$amount = Steam::bcround($availableBudget->amount, $currency->decimal_places);
$pcAmount = null;

View File

@@ -93,7 +93,6 @@ class BillTransformer extends AbstractTransformer
'object_group_order' => $bill->meta['object_group_order'],
'object_group_title' => $bill->meta['object_group_title'],
'paid_dates' => $bill->meta['paid_dates'],
'pay_dates' => $bill->meta['pay_dates'],
'next_expected_match' => $bill->meta['nem']?->toAtomString(),

View File

@@ -64,10 +64,7 @@ class BudgetLimitTransformer extends AbstractTransformer
public function transform(BudgetLimit $budgetLimit): array
{
$currency = $budgetLimit->transactionCurrency;
if (null === $currency) {
$currency = $this->primaryCurrency;
}
$currency = $budgetLimit->meta['currency'];
$amount = Steam::bcround($budgetLimit->amount, $currency->decimal_places);
$pcAmount = null;
if ($this->convertToPrimary && $currency->id === $this->primaryCurrency->id) {

View File

@@ -86,6 +86,9 @@ class BudgetTransformer extends AbstractTransformer
'notes' => $budget->meta['notes'],
'auto_budget_type' => $abType,
'auto_budget_period' => $abPeriod,
'object_group_id' => $budget->meta['object_group_id'],
'object_group_order' => $budget->meta['object_group_order'],
'object_group_title' => $budget->meta['object_group_title'],
// new currency settings.
'object_has_currency_setting' => null !== $budget->meta['currency'],
@@ -103,8 +106,8 @@ class BudgetTransformer extends AbstractTransformer
'auto_budget_amount' => $abAmount,
'pc_auto_budget_amount' => $abPrimary,
'spent' => $this->beautify($budget->meta['spent']),
'pc_spent' => $this->beautify($budget->meta['pc_spent']),
'spent' => null === $budget->meta['spent'] ? null : $this->beautify($budget->meta['spent']),
'pc_spent' => null === $budget->meta['pc_spent'] ? null : $this->beautify($budget->meta['pc_spent']),
'links' => [
[
'rel' => 'self',

View File

@@ -59,9 +59,8 @@ class CategoryTransformer extends AbstractTransformer
// category never has currency settings.
'object_has_currency_setting' => false,
'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' => (int)$this->primaryCurrency->decimal_places,

View File

@@ -26,8 +26,7 @@ namespace FireflyIII\Transformers;
use FireflyIII\Exceptions\FireflyException;
use FireflyIII\Models\PiggyBankEvent;
use FireflyIII\Repositories\Account\AccountRepositoryInterface;
use FireflyIII\Repositories\PiggyBank\PiggyBankRepositoryInterface;
use FireflyIII\Models\TransactionCurrency;
use FireflyIII\Support\Facades\Amount;
use FireflyIII\Support\Facades\Steam;
@@ -36,16 +35,16 @@ use FireflyIII\Support\Facades\Steam;
*/
class PiggyBankEventTransformer extends AbstractTransformer
{
private readonly PiggyBankRepositoryInterface $piggyRepos;
private readonly AccountRepositoryInterface $repository;
private TransactionCurrency $primaryCurrency;
private bool $convertToPrimary = false;
/**
* PiggyBankEventTransformer constructor.
*/
public function __construct()
{
$this->repository = app(AccountRepositoryInterface::class);
$this->piggyRepos = app(PiggyBankRepositoryInterface::class);
$this->primaryCurrency = Amount::getPrimaryCurrency();
$this->convertToPrimary = Amount::convertToPrimary();
}
/**
@@ -55,39 +54,43 @@ class PiggyBankEventTransformer extends AbstractTransformer
*/
public function transform(PiggyBankEvent $event): array
{
// get account linked to piggy bank
$account = $event->piggyBank->accounts()->first();
// set up repositories.
$this->repository->setUser($account->user);
$this->piggyRepos->setUser($account->user);
// get associated currency or fall back to the default:
$currency = $this->repository->getAccountCurrency($account) ?? Amount::getPrimaryCurrencyByUserGroup($account->user->userGroup);
// get associated journal and transaction, if any:
$journalId = $event->transaction_journal_id;
$groupId = null;
if (0 !== (int) $journalId) {
$groupId = (int) $event->transactionJournal->transaction_group_id;
$journalId = (int) $journalId;
$currency = $event->meta['currency'] ?? $this->primaryCurrency;
$amount = Steam::bcround($event->amount, $currency->decimal_places);
$primaryAmount = null;
if ($this->convertToPrimary && $currency->id === $this->primaryCurrency->id) {
$primaryAmount = $amount;
}
if ($this->convertToPrimary && $currency->id !== $this->primaryCurrency->id) {
$primaryAmount = Steam::bcround($event->native_amount, $this->primaryCurrency->decimal_places);
}
return [
'id' => (string) $event->id,
'created_at' => $event->created_at?->toAtomString(),
'updated_at' => $event->updated_at?->toAtomString(),
'amount' => Steam::bcround($event->amount, $currency->decimal_places),
'currency_id' => (string) $currency->id,
'currency_code' => $currency->code,
'currency_symbol' => $currency->symbol,
'currency_decimal_places' => $currency->decimal_places,
'transaction_journal_id' => null !== $journalId ? (string) $journalId : null,
'transaction_group_id' => null !== $groupId ? (string) $groupId : null,
'links' => [
'id' => (string)$event->id,
'created_at' => $event->created_at?->toAtomString(),
'updated_at' => $event->updated_at?->toAtomString(),
'amount' => $amount,
'pc_amount' => $primaryAmount,
// currencies according to 6.3.0
'has_currency_setting' => true,
'currency_id' => (string)$currency->id,
'currency_name' => $currency->name,
'currency_code' => $currency->code,
'currency_symbol' => $currency->symbol,
'currency_decimal_places' => $currency->decimal_places,
'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,
'transaction_journal_id' => null !== $event->transaction_journal_id ? (string)$event->transaction_journal_id : null,
'transaction_group_id' => $event->meta['transaction_group_id'],
'links' => [
[
'rel' => 'self',
'uri' => '/piggy_bank_events/'.$event->id,
'uri' => sprintf('/piggy-banks/%d/events/%s', $event->piggy_bank_id, $event->id),
],
],
];

View File

@@ -25,27 +25,23 @@ declare(strict_types=1);
namespace FireflyIII\Transformers;
use FireflyIII\Exceptions\FireflyException;
use FireflyIII\Models\ObjectGroup;
use FireflyIII\Models\PiggyBank;
use FireflyIII\Repositories\Account\AccountRepositoryInterface;
use FireflyIII\Repositories\PiggyBank\PiggyBankRepositoryInterface;
use FireflyIII\Support\Facades\Steam;
use FireflyIII\Models\TransactionCurrency;
use FireflyIII\Support\Facades\Amount;
/**
* Class PiggyBankTransformer
*/
class PiggyBankTransformer extends AbstractTransformer
{
private readonly AccountRepositoryInterface $accountRepos;
private readonly PiggyBankRepositoryInterface $piggyRepos;
private TransactionCurrency $primaryCurrency;
/**
* PiggyBankTransformer constructor.
*/
public function __construct()
{
$this->accountRepos = app(AccountRepositoryInterface::class);
$this->piggyRepos = app(PiggyBankRepositoryInterface::class);
$this->primaryCurrency = Amount::getPrimaryCurrency();
}
/**
@@ -55,74 +51,58 @@ class PiggyBankTransformer extends AbstractTransformer
*/
public function transform(PiggyBank $piggyBank): array
{
$user = $piggyBank->accounts()->first()->user;
// set up repositories
$this->accountRepos->setUser($user);
$this->piggyRepos->setUser($user);
// note
$notes = $this->piggyRepos->getNoteText($piggyBank);
$notes = '' === $notes ? null : $notes;
$objectGroupId = null;
$objectGroupOrder = null;
$objectGroupTitle = null;
/** @var null|ObjectGroup $objectGroup */
$objectGroup = $piggyBank->objectGroups->first();
if (null !== $objectGroup) {
$objectGroupId = $objectGroup->id;
$objectGroupOrder = $objectGroup->order;
$objectGroupTitle = $objectGroup->title;
}
// get currently saved amount:
$currency = $piggyBank->transactionCurrency;
$currentAmount = $this->piggyRepos->getCurrentAmount($piggyBank);
// Amounts, depending on 0.0 state of target amount
$percentage = null;
$targetAmount = $piggyBank->target_amount;
$leftToSave = null;
$savePerMonth = null;
if (0 !== bccomp($targetAmount, '0')) { // target amount is not 0.00
$leftToSave = bcsub($piggyBank->target_amount, $currentAmount);
$percentage = (int) bcmul(bcdiv($currentAmount, $targetAmount), '100');
$targetAmount = Steam::bcround($targetAmount, $currency->decimal_places);
$leftToSave = Steam::bcround($leftToSave, $currency->decimal_places);
$savePerMonth = Steam::bcround($this->piggyRepos->getSuggestedMonthlyAmount($piggyBank), $currency->decimal_places);
$percentage = null;
if (null !== $piggyBank->meta['target_amount'] && 0 !== bccomp($piggyBank->meta['current_amount'], '0')) { // target amount is not 0.00
$percentage = (int)bcmul(bcdiv($piggyBank->meta['current_amount'], $piggyBank->meta['target_amount']), '100');
}
$startDate = $piggyBank->start_date?->format('Y-m-d');
$targetDate = $piggyBank->target_date?->format('Y-m-d');
$startDate = $piggyBank->start_date?->toAtomString();
$targetDate = $piggyBank->target_date?->toAtomString();
return [
'id' => (string) $piggyBank->id,
'created_at' => $piggyBank->created_at->toAtomString(),
'updated_at' => $piggyBank->updated_at->toAtomString(),
'name' => $piggyBank->name,
'accounts' => $this->renderAccounts($piggyBank),
'currency_id' => (string) $currency->id,
'currency_code' => $currency->code,
'currency_symbol' => $currency->symbol,
'currency_decimal_places' => $currency->decimal_places,
'target_amount' => $targetAmount,
'percentage' => $percentage,
'current_amount' => $currentAmount,
'left_to_save' => $leftToSave,
'save_per_month' => $savePerMonth,
'start_date' => $startDate,
'target_date' => $targetDate,
'order' => $piggyBank->order,
'active' => true,
'notes' => $notes,
'object_group_id' => null !== $objectGroupId ? (string) $objectGroupId : null,
'object_group_order' => $objectGroupOrder,
'object_group_title' => $objectGroupTitle,
'links' => [
'id' => (string)$piggyBank->id,
'created_at' => $piggyBank->created_at->toAtomString(),
'updated_at' => $piggyBank->updated_at->toAtomString(),
'name' => $piggyBank->name,
'percentage' => $percentage,
'start_date' => $startDate,
'target_date' => $targetDate,
'order' => $piggyBank->order,
'active' => true,
'notes' => $piggyBank->meta['notes'],
'object_group_id' => $piggyBank->meta['object_group_id'],
'object_group_order' => $piggyBank->meta['object_group_order'],
'object_group_title' => $piggyBank->meta['object_group_title'],
'accounts' => $piggyBank->meta['accounts'],
// currency settings, 6.3.0.
'object_has_currency_setting' => true,
'currency_id' => (string)$piggyBank->meta['currency']->id,
'currency_name' => $piggyBank->meta['currency']->name,
'currency_code' => $piggyBank->meta['currency']->code,
'currency_symbol' => $piggyBank->meta['currency']->symbol,
'currency_decimal_places' => $piggyBank->meta['currency']->decimal_places,
'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' => (int)$this->primaryCurrency->decimal_places,
'target_amount' => $piggyBank->meta['target_amount'],
'pc_target_amount' => $piggyBank->meta['pc_target_amount'],
'current_amount' => $piggyBank->meta['current_amount'],
'pc_current_amount' => $piggyBank->meta['pc_current_amount'],
'left_to_save' => $piggyBank->meta['left_to_save'],
'pc_left_to_save' => $piggyBank->meta['pc_left_to_save'],
'save_per_month' => $piggyBank->meta['save_per_month'],
'pc_save_per_month' => $piggyBank->meta['pc_save_per_month'],
'links' => [
[
'rel' => 'self',
'uri' => '/piggy_banks/'.$piggyBank->id,
'uri' => sprintf('/piggy-banks/%d', $piggyBank->id),
],
],
];
@@ -133,10 +113,10 @@ class PiggyBankTransformer extends AbstractTransformer
$return = [];
foreach ($piggyBank->accounts()->get() as $account) {
$return[] = [
'id' => (string) $account->id,
'name' => $account->name,
'current_amount' => (string) $account->pivot->current_amount,
'pc_current_amount' => (string) $account->pivot->native_current_amount,
'id' => (string)$account->id,
'name' => $account->name,
'current_amount' => (string)$account->pivot->current_amount,
'pc_current_amount' => (string)$account->pivot->native_current_amount,
// TODO add balance, add left to save.
];
}

View File

@@ -24,45 +24,19 @@ declare(strict_types=1);
namespace FireflyIII\Transformers;
use Carbon\Carbon;
use FireflyIII\Exceptions\FireflyException;
use FireflyIII\Factory\CategoryFactory;
use FireflyIII\Models\Account;
use FireflyIII\Models\Recurrence;
use FireflyIII\Models\RecurrenceRepetition;
use FireflyIII\Models\RecurrenceTransaction;
use FireflyIII\Models\RecurrenceTransactionMeta;
use FireflyIII\Repositories\Bill\BillRepositoryInterface;
use FireflyIII\Repositories\Budget\BudgetRepositoryInterface;
use FireflyIII\Repositories\PiggyBank\PiggyBankRepositoryInterface;
use FireflyIII\Repositories\Recurring\RecurringRepositoryInterface;
use FireflyIII\Support\Facades\Steam;
use Illuminate\Support\Facades\Log;
use function Safe\json_decode;
/**
* Class RecurringTransactionTransformer
*/
class RecurrenceTransformer extends AbstractTransformer
{
private readonly BillRepositoryInterface $billRepos;
private readonly BudgetRepositoryInterface $budgetRepos;
private readonly CategoryFactory $factory;
private readonly PiggyBankRepositoryInterface $piggyRepos;
private readonly RecurringRepositoryInterface $repository;
/**
* RecurrenceTransformer constructor.
*/
public function __construct()
{
$this->repository = app(RecurringRepositoryInterface::class);
$this->piggyRepos = app(PiggyBankRepositoryInterface::class);
$this->factory = app(CategoryFactory::class);
$this->budgetRepos = app(BudgetRepositoryInterface::class);
$this->billRepos = app(BillRepositoryInterface::class);
}
public function __construct() {}
/**
* Transform the recurring transaction.
@@ -72,34 +46,28 @@ class RecurrenceTransformer extends AbstractTransformer
public function transform(Recurrence $recurrence): array
{
Log::debug('Now in Recurrence::transform()');
$this->repository->setUser($recurrence->user);
$this->piggyRepos->setUser($recurrence->user);
$this->factory->setUser($recurrence->user);
$this->budgetRepos->setUser($recurrence->user);
Log::debug('Set user.');
$shortType = (string) config(sprintf('firefly.transactionTypesToShort.%s', $recurrence->transactionType->type));
$notes = $this->repository->getNoteText($recurrence);
$reps = 0 === (int) $recurrence->repetitions ? null : (int) $recurrence->repetitions;
$shortType = (string)config(sprintf('firefly.transactionTypesToShort.%s', $recurrence->transactionType->type));
$reps = 0 === (int)$recurrence->repetitions ? null : (int)$recurrence->repetitions;
Log::debug('Get basic data.');
// basic data.
return [
'id' => (string) $recurrence->id,
'id' => (string)$recurrence->id,
'created_at' => $recurrence->created_at->toAtomString(),
'updated_at' => $recurrence->updated_at->toAtomString(),
'type' => $shortType,
'title' => $recurrence->title,
'description' => $recurrence->description,
'first_date' => $recurrence->first_date->format('Y-m-d'),
'latest_date' => $recurrence->latest_date?->format('Y-m-d'),
'repeat_until' => $recurrence->repeat_until?->format('Y-m-d'),
'first_date' => $recurrence->first_date->toAtomString(),
'latest_date' => $recurrence->latest_date?->toAtomString(),
'repeat_until' => $recurrence->repeat_until?->toAtomString(),
'apply_rules' => $recurrence->apply_rules,
'active' => $recurrence->active,
'nr_of_repetitions' => $reps,
'notes' => '' === $notes ? null : $notes,
'repetitions' => $this->getRepetitions($recurrence),
'transactions' => $this->getTransactions($recurrence),
'notes' => $recurrence->meta['notes'],
'repetitions' => $recurrence->meta['repetitions'],
'transactions' => $recurrence->meta['transactions'],
'links' => [
[
'rel' => 'self',
@@ -108,208 +76,4 @@ class RecurrenceTransformer extends AbstractTransformer
],
];
}
/**
* @throws FireflyException
*/
private function getRepetitions(Recurrence $recurrence): array
{
Log::debug('Now in getRepetitions().');
$fromDate = $recurrence->latest_date ?? $recurrence->first_date;
$return = [];
/** @var RecurrenceRepetition $repetition */
foreach ($recurrence->recurrenceRepetitions as $repetition) {
$repetitionArray = [
'id' => (string) $repetition->id,
'created_at' => $repetition->created_at->toAtomString(),
'updated_at' => $repetition->updated_at->toAtomString(),
'type' => $repetition->repetition_type,
'moment' => $repetition->repetition_moment,
'skip' => $repetition->repetition_skip,
'weekend' => $repetition->weekend,
'description' => $this->repository->repetitionDescription($repetition),
'occurrences' => [],
];
// get the (future) occurrences for this specific type of repetition:
$amount = 'daily' === $repetition->repetition_type ? 9 : 5;
$occurrences = $this->repository->getXOccurrencesSince($repetition, $fromDate, now(), $amount);
/** @var Carbon $carbon */
foreach ($occurrences as $carbon) {
$repetitionArray['occurrences'][] = $carbon->toAtomString();
}
$return[] = $repetitionArray;
}
return $return;
}
/**
* @throws FireflyException
*/
private function getTransactions(Recurrence $recurrence): array
{
Log::debug(sprintf('Now in %s', __METHOD__));
$return = [];
// get all transactions:
/** @var RecurrenceTransaction $transaction */
foreach ($recurrence->recurrenceTransactions()->get() as $transaction) {
/** @var null|Account $sourceAccount */
$sourceAccount = $transaction->sourceAccount;
/** @var null|Account $destinationAccount */
$destinationAccount = $transaction->destinationAccount;
$foreignCurrencyCode = null;
$foreignCurrencySymbol = null;
$foreignCurrencyDp = null;
$foreignCurrencyId = null;
if (null !== $transaction->foreign_currency_id) {
$foreignCurrencyId = (int) $transaction->foreign_currency_id;
$foreignCurrencyCode = $transaction->foreignCurrency->code;
$foreignCurrencySymbol = $transaction->foreignCurrency->symbol;
$foreignCurrencyDp = $transaction->foreignCurrency->decimal_places;
}
// source info:
$sourceName = '';
$sourceId = null;
$sourceType = null;
$sourceIban = null;
if (null !== $sourceAccount) {
$sourceName = $sourceAccount->name;
$sourceId = $sourceAccount->id;
$sourceType = $sourceAccount->accountType->type;
$sourceIban = $sourceAccount->iban;
}
$destinationName = '';
$destinationId = null;
$destinationType = null;
$destinationIban = null;
if (null !== $destinationAccount) {
$destinationName = $destinationAccount->name;
$destinationId = $destinationAccount->id;
$destinationType = $destinationAccount->accountType->type;
$destinationIban = $destinationAccount->iban;
}
$amount = Steam::bcround($transaction->amount, $transaction->transactionCurrency->decimal_places);
$foreignAmount = null;
if (null !== $transaction->foreign_currency_id && null !== $transaction->foreign_amount) {
$foreignAmount = Steam::bcround($transaction->foreign_amount, $foreignCurrencyDp);
}
$transactionArray = [
'id' => (string) $transaction->id,
'currency_id' => (string) $transaction->transaction_currency_id,
'currency_code' => $transaction->transactionCurrency->code,
'currency_symbol' => $transaction->transactionCurrency->symbol,
'currency_decimal_places' => $transaction->transactionCurrency->decimal_places,
'foreign_currency_id' => null === $foreignCurrencyId ? null : (string) $foreignCurrencyId,
'foreign_currency_code' => $foreignCurrencyCode,
'foreign_currency_symbol' => $foreignCurrencySymbol,
'foreign_currency_decimal_places' => $foreignCurrencyDp,
'source_id' => (string) $sourceId,
'source_name' => $sourceName,
'source_iban' => $sourceIban,
'source_type' => $sourceType,
'destination_id' => (string) $destinationId,
'destination_name' => $destinationName,
'destination_iban' => $destinationIban,
'destination_type' => $destinationType,
'amount' => $amount,
'foreign_amount' => $foreignAmount,
'description' => $transaction->description,
];
$transactionArray = $this->getTransactionMeta($transaction, $transactionArray);
if (null !== $transaction->foreign_currency_id) {
$transactionArray['foreign_currency_code'] = $transaction->foreignCurrency->code;
$transactionArray['foreign_currency_symbol'] = $transaction->foreignCurrency->symbol;
$transactionArray['foreign_currency_decimal_places'] = $transaction->foreignCurrency->decimal_places;
}
// store transaction in recurrence array.
$return[] = $transactionArray;
}
return $return;
}
/**
* @throws FireflyException
*/
private function getTransactionMeta(RecurrenceTransaction $transaction, array $array): array
{
Log::debug(sprintf('Now in %s', __METHOD__));
$array['tags'] = [];
$array['category_id'] = null;
$array['category_name'] = null;
$array['budget_id'] = null;
$array['budget_name'] = null;
$array['piggy_bank_id'] = null;
$array['piggy_bank_name'] = null;
$array['bill_id'] = null;
$array['bill_name'] = null;
/** @var RecurrenceTransactionMeta $transactionMeta */
foreach ($transaction->recurrenceTransactionMeta as $transactionMeta) {
switch ($transactionMeta->name) {
default:
throw new FireflyException(sprintf('Recurrence transformer cant handle field "%s"', $transactionMeta->name));
case 'bill_id':
$bill = $this->billRepos->find((int) $transactionMeta->value);
if (null !== $bill) {
$array['bill_id'] = (string) $bill->id;
$array['bill_name'] = $bill->name;
}
break;
case 'tags':
$array['tags'] = json_decode((string) $transactionMeta->value);
break;
case 'piggy_bank_id':
$piggy = $this->piggyRepos->find((int) $transactionMeta->value);
if (null !== $piggy) {
$array['piggy_bank_id'] = (string) $piggy->id;
$array['piggy_bank_name'] = $piggy->name;
}
break;
case 'category_id':
$category = $this->factory->findOrCreate((int) $transactionMeta->value, null);
if (null !== $category) {
$array['category_id'] = (string) $category->id;
$array['category_name'] = $category->name;
}
break;
case 'category_name':
$category = $this->factory->findOrCreate(null, $transactionMeta->value);
if (null !== $category) {
$array['category_id'] = (string) $category->id;
$array['category_name'] = $category->name;
}
break;
case 'budget_id':
$budget = $this->budgetRepos->find((int) $transactionMeta->value);
if (null !== $budget) {
$array['budget_id'] = (string) $budget->id;
$array['budget_name'] = $budget->name;
}
break;
}
}
return $array;
}
}

View File

@@ -3,6 +3,53 @@
All notable changes to this project will be documented in this file.
This project adheres to [Semantic Versioning](http://semver.org/).
## 6.3.0 - 2025-08-xx
> ⚠️ Firefly III v6.3.0 introduces a lot of API changes that deal with multi-currency support. Make sure your beloved apps are updated to support this.
### Added
- [Issue 6836](https://github.com/firefly-iii/firefly-iii/issues/6836) (Send email about coming/past-due bills) reported by @elgatho
- [Issue 9640](https://github.com/firefly-iii/firefly-iii/issues/9640) (UI Improvements for Rules) reported by @siriuspal
- [Issue 9650](https://github.com/firefly-iii/firefly-iii/issues/9650) (Extra line in bills overview) reported by @poudenes
- Add Arabic as language, translations follow.
### Changed
- [Issue 10071](https://github.com/firefly-iii/firefly-iii/issues/10071) (Allow toggling password field to text) reported by @ragul-engg
- Renamed all instances of "default" and "native" currency to "primary" currency. This influences translations and API endpoints. The database is not changed because that's difficult to do reliably.
### Removed
- Any API-field called `default_*` or `native_*`. Use `primary_*` instead.
- All v2 endpoints.
### Fixed
- [Issue 9849](https://github.com/firefly-iii/firefly-iii/issues/9849) ("Display native amounts" not taken into account in report's pie charts) reported by @polter-rnd
- [Issue 10565](https://github.com/firefly-iii/firefly-iii/issues/10565) (Unable to delete reconciliation transaction) reported by @berta24
- [Issue 10600](https://github.com/firefly-iii/firefly-iii/issues/10600) (Show attachmen iccon when listing tranactions) reported by @JcMinarro
- [Discussion 10618](https://github.com/orgs/firefly-iii/discussions/10618) (Starting balance includes transactions that occur at 00:00 on the 1st of month) started by @jteez
- [Issue 10646](https://github.com/firefly-iii/firefly-iii/issues/10646) (Webhooks fire even if disabled) reported by @lvu
- [Issue 10656](https://github.com/firefly-iii/firefly-iii/issues/10656) (spent info "per day" shows the period total) reported by @frank-bg
- [Issue 10678](https://github.com/firefly-iii/firefly-iii/issues/10678) (Transactions from asset to liability account do not appear on category reports.) reported by @slackspace-io
- [Issue 10687](https://github.com/firefly-iii/firefly-iii/issues/10687) (Creating new Piggy Bank via API fails (Unexpected empty currency)) reported by @Madnex
- [Issue 10700](https://github.com/firefly-iii/firefly-iii/issues/10700) (Setting financial year date is inconsistent due to timezone calculations) reported by @AgeManning
- [Issue 10702](https://github.com/firefly-iii/firefly-iii/issues/10702) (Wrong order of months in category report) reported by @kapuett
- [Issue 10703](https://github.com/firefly-iii/firefly-iii/issues/10703) (Fire more than 5 webhooks in batch or through the cron job, and document it.) reported by @JC5
- [Issue 10704](https://github.com/firefly-iii/firefly-iii/issues/10704) (Some triggers with rule automation seems to have an issue) reported by @Alienlog
- [Issue 10706](https://github.com/firefly-iii/firefly-iii/issues/10706) (Add KRW in Default Currency List) reported by @readingsnail
- [Issue 10708](https://github.com/firefly-iii/firefly-iii/issues/10708) (Incomplete display of a rule when a trigger negates "description caontains") reported by @dethegeek
- [Issue 10709](https://github.com/firefly-iii/firefly-iii/issues/10709) (has_any_external_id search parameter invalid) reported by @Alienlog
- Tag overview will no longer search for tags dated < 1970.
### API
- All remaining API v2 endpoints are deprecated and removed in favour of the API v1 endpoints.
- All API read endpoints now support multi-currency. Fields such as the balance and amount fields will also be available as `pc_*`-fields. Objects with currency information also come with new `primary_currency_*` fields.
- All API read endpoints are DB optimized and should be faster.
- All documentation should be in sync again.
- [More info in the docs](https://docs.firefly-iii.org/references/firefly-iii/api/).
## 6.2.21 - 2025-07-18
### Added

336
composer.lock generated
View File

@@ -586,33 +586,32 @@
},
{
"name": "doctrine/inflector",
"version": "2.0.10",
"version": "2.1.0",
"source": {
"type": "git",
"url": "https://github.com/doctrine/inflector.git",
"reference": "5817d0659c5b50c9b950feb9af7b9668e2c436bc"
"reference": "6d6c96277ea252fc1304627204c3d5e6e15faa3b"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/doctrine/inflector/zipball/5817d0659c5b50c9b950feb9af7b9668e2c436bc",
"reference": "5817d0659c5b50c9b950feb9af7b9668e2c436bc",
"url": "https://api.github.com/repos/doctrine/inflector/zipball/6d6c96277ea252fc1304627204c3d5e6e15faa3b",
"reference": "6d6c96277ea252fc1304627204c3d5e6e15faa3b",
"shasum": ""
},
"require": {
"php": "^7.2 || ^8.0"
},
"require-dev": {
"doctrine/coding-standard": "^11.0",
"phpstan/phpstan": "^1.8",
"phpstan/phpstan-phpunit": "^1.1",
"phpstan/phpstan-strict-rules": "^1.3",
"phpunit/phpunit": "^8.5 || ^9.5",
"vimeo/psalm": "^4.25 || ^5.4"
"doctrine/coding-standard": "^12.0 || ^13.0",
"phpstan/phpstan": "^1.12 || ^2.0",
"phpstan/phpstan-phpunit": "^1.4 || ^2.0",
"phpstan/phpstan-strict-rules": "^1.6 || ^2.0",
"phpunit/phpunit": "^8.5 || ^12.2"
},
"type": "library",
"autoload": {
"psr-4": {
"Doctrine\\Inflector\\": "lib/Doctrine/Inflector"
"Doctrine\\Inflector\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
@@ -657,7 +656,7 @@
],
"support": {
"issues": "https://github.com/doctrine/inflector/issues",
"source": "https://github.com/doctrine/inflector/tree/2.0.10"
"source": "https://github.com/doctrine/inflector/tree/2.1.0"
},
"funding": [
{
@@ -673,7 +672,7 @@
"type": "tidelift"
}
],
"time": "2024-02-18T20:23:39+00:00"
"time": "2025-08-10T19:31:58+00:00"
},
{
"name": "doctrine/lexer",
@@ -939,16 +938,16 @@
},
{
"name": "filp/whoops",
"version": "2.18.3",
"version": "2.18.4",
"source": {
"type": "git",
"url": "https://github.com/filp/whoops.git",
"reference": "59a123a3d459c5a23055802237cb317f609867e5"
"reference": "d2102955e48b9fd9ab24280a7ad12ed552752c4d"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/filp/whoops/zipball/59a123a3d459c5a23055802237cb317f609867e5",
"reference": "59a123a3d459c5a23055802237cb317f609867e5",
"url": "https://api.github.com/repos/filp/whoops/zipball/d2102955e48b9fd9ab24280a7ad12ed552752c4d",
"reference": "d2102955e48b9fd9ab24280a7ad12ed552752c4d",
"shasum": ""
},
"require": {
@@ -998,7 +997,7 @@
],
"support": {
"issues": "https://github.com/filp/whoops/issues",
"source": "https://github.com/filp/whoops/tree/2.18.3"
"source": "https://github.com/filp/whoops/tree/2.18.4"
},
"funding": [
{
@@ -1006,7 +1005,7 @@
"type": "github"
}
],
"time": "2025-06-16T00:02:10+00:00"
"time": "2025-08-08T12:00:00+00:00"
},
{
"name": "firebase/php-jwt",
@@ -1879,16 +1878,16 @@
},
{
"name": "laravel/framework",
"version": "v12.21.0",
"version": "v12.23.1",
"source": {
"type": "git",
"url": "https://github.com/laravel/framework.git",
"reference": "ac8c4e73bf1b5387b709f7736d41427e6af1c93b"
"reference": "2a0e9331a0db904236143fe915c281ff4be274a3"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/laravel/framework/zipball/ac8c4e73bf1b5387b709f7736d41427e6af1c93b",
"reference": "ac8c4e73bf1b5387b709f7736d41427e6af1c93b",
"url": "https://api.github.com/repos/laravel/framework/zipball/2a0e9331a0db904236143fe915c281ff4be274a3",
"reference": "2a0e9331a0db904236143fe915c281ff4be274a3",
"shasum": ""
},
"require": {
@@ -1929,6 +1928,8 @@
"symfony/mailer": "^7.2.0",
"symfony/mime": "^7.2.0",
"symfony/polyfill-php83": "^1.31",
"symfony/polyfill-php84": "^1.31",
"symfony/polyfill-php85": "^1.31",
"symfony/process": "^7.2.0",
"symfony/routing": "^7.2.0",
"symfony/uid": "^7.2.0",
@@ -2090,7 +2091,7 @@
"issues": "https://github.com/laravel/framework/issues",
"source": "https://github.com/laravel/framework"
},
"time": "2025-07-22T15:41:55+00:00"
"time": "2025-08-12T17:35:05+00:00"
},
{
"name": "laravel/passport",
@@ -3878,29 +3879,29 @@
},
{
"name": "nette/utils",
"version": "v4.0.7",
"version": "v4.0.8",
"source": {
"type": "git",
"url": "https://github.com/nette/utils.git",
"reference": "e67c4061eb40b9c113b218214e42cb5a0dda28f2"
"reference": "c930ca4e3cf4f17dcfb03037703679d2396d2ede"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/nette/utils/zipball/e67c4061eb40b9c113b218214e42cb5a0dda28f2",
"reference": "e67c4061eb40b9c113b218214e42cb5a0dda28f2",
"url": "https://api.github.com/repos/nette/utils/zipball/c930ca4e3cf4f17dcfb03037703679d2396d2ede",
"reference": "c930ca4e3cf4f17dcfb03037703679d2396d2ede",
"shasum": ""
},
"require": {
"php": "8.0 - 8.4"
"php": "8.0 - 8.5"
},
"conflict": {
"nette/finder": "<3",
"nette/schema": "<1.2.2"
},
"require-dev": {
"jetbrains/phpstorm-attributes": "dev-master",
"jetbrains/phpstorm-attributes": "^1.2",
"nette/tester": "^2.5",
"phpstan/phpstan": "^1.0",
"phpstan/phpstan-nette": "^2.0@stable",
"tracy/tracy": "^2.9"
},
"suggest": {
@@ -3918,6 +3919,9 @@
}
},
"autoload": {
"psr-4": {
"Nette\\": "src"
},
"classmap": [
"src/"
]
@@ -3958,9 +3962,9 @@
],
"support": {
"issues": "https://github.com/nette/utils/issues",
"source": "https://github.com/nette/utils/tree/v4.0.7"
"source": "https://github.com/nette/utils/tree/v4.0.8"
},
"time": "2025-06-03T04:55:08+00:00"
"time": "2025-08-06T21:43:34+00:00"
},
{
"name": "nunomaduro/collision",
@@ -5101,16 +5105,16 @@
},
{
"name": "predis/predis",
"version": "v3.1.0",
"version": "v3.2.0",
"source": {
"type": "git",
"url": "https://github.com/predis/predis.git",
"reference": "202e0c5322b906ec4c761c0cefebad6d0959a699"
"reference": "9e9deec4dfd3ebf65d32eb368f498c646ba2ecd8"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/predis/predis/zipball/202e0c5322b906ec4c761c0cefebad6d0959a699",
"reference": "202e0c5322b906ec4c761c0cefebad6d0959a699",
"url": "https://api.github.com/repos/predis/predis/zipball/9e9deec4dfd3ebf65d32eb368f498c646ba2ecd8",
"reference": "9e9deec4dfd3ebf65d32eb368f498c646ba2ecd8",
"shasum": ""
},
"require": {
@@ -5152,7 +5156,7 @@
],
"support": {
"issues": "https://github.com/predis/predis/issues",
"source": "https://github.com/predis/predis/tree/v3.1.0"
"source": "https://github.com/predis/predis/tree/v3.2.0"
},
"funding": [
{
@@ -5160,7 +5164,7 @@
"type": "github"
}
],
"time": "2025-07-22T15:37:44+00:00"
"time": "2025-08-06T06:41:24+00:00"
},
{
"name": "psr/cache",
@@ -8508,6 +8512,158 @@
],
"time": "2024-09-09T11:45:10+00:00"
},
{
"name": "symfony/polyfill-php84",
"version": "v1.32.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-php84.git",
"reference": "000df7860439609837bbe28670b0be15783b7fbf"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-php84/zipball/000df7860439609837bbe28670b0be15783b7fbf",
"reference": "000df7860439609837bbe28670b0be15783b7fbf",
"shasum": ""
},
"require": {
"php": ">=7.2"
},
"type": "library",
"extra": {
"thanks": {
"url": "https://github.com/symfony/polyfill",
"name": "symfony/polyfill"
}
},
"autoload": {
"files": [
"bootstrap.php"
],
"psr-4": {
"Symfony\\Polyfill\\Php84\\": ""
},
"classmap": [
"Resources/stubs"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Nicolas Grekas",
"email": "p@tchwork.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Symfony polyfill backporting some PHP 8.4+ features to lower PHP versions",
"homepage": "https://symfony.com",
"keywords": [
"compatibility",
"polyfill",
"portable",
"shim"
],
"support": {
"source": "https://github.com/symfony/polyfill-php84/tree/v1.32.0"
},
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2025-02-20T12:04:08+00:00"
},
{
"name": "symfony/polyfill-php85",
"version": "v1.32.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-php85.git",
"reference": "6fedf31ce4e3648f4ff5ca58bfd53127d38f05fd"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-php85/zipball/6fedf31ce4e3648f4ff5ca58bfd53127d38f05fd",
"reference": "6fedf31ce4e3648f4ff5ca58bfd53127d38f05fd",
"shasum": ""
},
"require": {
"php": ">=7.2"
},
"type": "library",
"extra": {
"thanks": {
"url": "https://github.com/symfony/polyfill",
"name": "symfony/polyfill"
}
},
"autoload": {
"files": [
"bootstrap.php"
],
"psr-4": {
"Symfony\\Polyfill\\Php85\\": ""
},
"classmap": [
"Resources/stubs"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Nicolas Grekas",
"email": "p@tchwork.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Symfony polyfill backporting some PHP 8.5+ features to lower PHP versions",
"homepage": "https://symfony.com",
"keywords": [
"compatibility",
"polyfill",
"portable",
"shim"
],
"support": {
"source": "https://github.com/symfony/polyfill-php85/tree/v1.32.0"
},
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2025-05-02T08:40:52+00:00"
},
{
"name": "symfony/polyfill-uuid",
"version": "v1.32.0",
@@ -10344,16 +10500,16 @@
},
{
"name": "driftingly/rector-laravel",
"version": "2.0.5",
"version": "2.0.6",
"source": {
"type": "git",
"url": "https://github.com/driftingly/rector-laravel.git",
"reference": "ac61de4f267c23249d175d7fc9149fd01528567d"
"reference": "5be95811801fc06126dd844beaeb6a41721ba3d3"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/driftingly/rector-laravel/zipball/ac61de4f267c23249d175d7fc9149fd01528567d",
"reference": "ac61de4f267c23249d175d7fc9149fd01528567d",
"url": "https://api.github.com/repos/driftingly/rector-laravel/zipball/5be95811801fc06126dd844beaeb6a41721ba3d3",
"reference": "5be95811801fc06126dd844beaeb6a41721ba3d3",
"shasum": ""
},
"require": {
@@ -10373,9 +10529,9 @@
"description": "Rector upgrades rules for Laravel Framework",
"support": {
"issues": "https://github.com/driftingly/rector-laravel/issues",
"source": "https://github.com/driftingly/rector-laravel/tree/2.0.5"
"source": "https://github.com/driftingly/rector-laravel/tree/2.0.6"
},
"time": "2025-05-14T17:30:41+00:00"
"time": "2025-08-08T22:10:01+00:00"
},
{
"name": "fakerphp/faker",
@@ -11615,16 +11771,16 @@
},
{
"name": "phpunit/phpunit",
"version": "12.3.0",
"version": "12.3.4",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/phpunit.git",
"reference": "264da860d6fe0d00582355a6ecbbf7ae57b44895"
"reference": "429095031bd38cb5070ca44166bd9dd5a9245dd6"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/264da860d6fe0d00582355a6ecbbf7ae57b44895",
"reference": "264da860d6fe0d00582355a6ecbbf7ae57b44895",
"url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/429095031bd38cb5070ca44166bd9dd5a9245dd6",
"reference": "429095031bd38cb5070ca44166bd9dd5a9245dd6",
"shasum": ""
},
"require": {
@@ -11634,7 +11790,7 @@
"ext-mbstring": "*",
"ext-xml": "*",
"ext-xmlwriter": "*",
"myclabs/deep-copy": "^1.13.3",
"myclabs/deep-copy": "^1.13.4",
"phar-io/manifest": "^2.0.4",
"phar-io/version": "^3.2.1",
"php": ">=8.3",
@@ -11644,13 +11800,13 @@
"phpunit/php-text-template": "^5.0.0",
"phpunit/php-timer": "^8.0.0",
"sebastian/cli-parser": "^4.0.0",
"sebastian/comparator": "^7.1.0",
"sebastian/comparator": "^7.1.2",
"sebastian/diff": "^7.0.0",
"sebastian/environment": "^8.0.2",
"sebastian/exporter": "^7.0.0",
"sebastian/global-state": "^8.0.0",
"sebastian/object-enumerator": "^7.0.0",
"sebastian/type": "^6.0.2",
"sebastian/type": "^6.0.3",
"sebastian/version": "^6.0.0",
"staabm/side-effects-detector": "^1.0.5"
},
@@ -11692,7 +11848,7 @@
"support": {
"issues": "https://github.com/sebastianbergmann/phpunit/issues",
"security": "https://github.com/sebastianbergmann/phpunit/security/policy",
"source": "https://github.com/sebastianbergmann/phpunit/tree/12.3.0"
"source": "https://github.com/sebastianbergmann/phpunit/tree/12.3.4"
},
"funding": [
{
@@ -11716,7 +11872,7 @@
"type": "tidelift"
}
],
"time": "2025-08-01T05:14:47+00:00"
"time": "2025-08-12T07:35:30+00:00"
},
{
"name": "rector/rector",
@@ -11837,16 +11993,16 @@
},
{
"name": "sebastian/comparator",
"version": "7.1.0",
"version": "7.1.2",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/comparator.git",
"reference": "03d905327dccc0851c9a08d6a979dfc683826b6f"
"reference": "1a7c2bce03a13a457ed3c975dfd331b3b4b133aa"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/03d905327dccc0851c9a08d6a979dfc683826b6f",
"reference": "03d905327dccc0851c9a08d6a979dfc683826b6f",
"url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/1a7c2bce03a13a457ed3c975dfd331b3b4b133aa",
"reference": "1a7c2bce03a13a457ed3c975dfd331b3b4b133aa",
"shasum": ""
},
"require": {
@@ -11905,7 +12061,7 @@
"support": {
"issues": "https://github.com/sebastianbergmann/comparator/issues",
"security": "https://github.com/sebastianbergmann/comparator/security/policy",
"source": "https://github.com/sebastianbergmann/comparator/tree/7.1.0"
"source": "https://github.com/sebastianbergmann/comparator/tree/7.1.2"
},
"funding": [
{
@@ -11925,7 +12081,7 @@
"type": "tidelift"
}
],
"time": "2025-06-17T07:41:58+00:00"
"time": "2025-08-10T08:50:08+00:00"
},
{
"name": "sebastian/complexity",
@@ -12054,16 +12210,16 @@
},
{
"name": "sebastian/environment",
"version": "8.0.2",
"version": "8.0.3",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/environment.git",
"reference": "d364b9e5d0d3b18a2573351a1786fbf96b7e0792"
"reference": "24a711b5c916efc6d6e62aa65aa2ec98fef77f68"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/d364b9e5d0d3b18a2573351a1786fbf96b7e0792",
"reference": "d364b9e5d0d3b18a2573351a1786fbf96b7e0792",
"url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/24a711b5c916efc6d6e62aa65aa2ec98fef77f68",
"reference": "24a711b5c916efc6d6e62aa65aa2ec98fef77f68",
"shasum": ""
},
"require": {
@@ -12106,7 +12262,7 @@
"support": {
"issues": "https://github.com/sebastianbergmann/environment/issues",
"security": "https://github.com/sebastianbergmann/environment/security/policy",
"source": "https://github.com/sebastianbergmann/environment/tree/8.0.2"
"source": "https://github.com/sebastianbergmann/environment/tree/8.0.3"
},
"funding": [
{
@@ -12126,7 +12282,7 @@
"type": "tidelift"
}
],
"time": "2025-05-21T15:05:44+00:00"
"time": "2025-08-12T14:11:56+00:00"
},
{
"name": "sebastian/exporter",
@@ -12442,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": {
@@ -12494,28 +12650,40 @@
"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",
"version": "6.0.2",
"version": "6.0.3",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/type.git",
"reference": "1d7cd6e514384c36d7a390347f57c385d4be6069"
"reference": "e549163b9760b8f71f191651d22acf32d56d6d4d"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/sebastianbergmann/type/zipball/1d7cd6e514384c36d7a390347f57c385d4be6069",
"reference": "1d7cd6e514384c36d7a390347f57c385d4be6069",
"url": "https://api.github.com/repos/sebastianbergmann/type/zipball/e549163b9760b8f71f191651d22acf32d56d6d4d",
"reference": "e549163b9760b8f71f191651d22acf32d56d6d4d",
"shasum": ""
},
"require": {
@@ -12551,15 +12719,27 @@
"support": {
"issues": "https://github.com/sebastianbergmann/type/issues",
"security": "https://github.com/sebastianbergmann/type/security/policy",
"source": "https://github.com/sebastianbergmann/type/tree/6.0.2"
"source": "https://github.com/sebastianbergmann/type/tree/6.0.3"
},
"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/type",
"type": "tidelift"
}
],
"time": "2025-03-18T13:37:31+00:00"
"time": "2025-08-09T06:57:12+00:00"
},
{
"name": "sebastian/version",

View File

@@ -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-05',
'build_time' => 1754394714,
'version' => 'develop/2025-08-13',
'build_time' => 1755064254,
'api_version' => '2.1.0', // field is no longer used.
'db_version' => 26,

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