Compare commits

..

71 Commits

Author SHA1 Message Date
github-actions[bot]
40abe74dc1 Merge pull request #11621 from firefly-iii/release-1769627627
🤖 Automatically merge the PR into the develop branch.
2026-01-28 20:13:55 +01:00
JC5
e5d2c4d163 🤖 Auto commit for release 'develop' on 2026-01-28 2026-01-28 20:13:48 +01:00
James Cole
2851053900 Fix nullpointer. 2026-01-28 20:08:32 +01:00
James Cole
b2f6ce1277 Merge pull request #11615 from nick322/feat/11614 2026-01-28 19:38:31 +01:00
Nick Huang
340b0661ba feat(#11614): Add New Taiwan Dollar to Currency Seeder 2026-01-28 15:57:52 +08:00
github-actions[bot]
be18f11f8c Merge pull request #11613 from firefly-iii/release-1769573331
🤖 Automatically merge the PR into the develop branch.
2026-01-28 05:08:59 +01:00
JC5
2f8ee67b31 🤖 Auto commit for release 'develop' on 2026-01-28 2026-01-28 05:08:52 +01:00
James Cole
1ecf55165e Merge branch 'main' into develop 2026-01-28 05:02:30 +01:00
James Cole
5aceccde4a Fix method call. 2026-01-28 05:02:14 +01:00
James Cole
abfaee5a55 Merge pull request #11612 from firefly-iii/dependabot/composer/composer-63bdf6e023 2026-01-28 04:17:00 +01:00
dependabot[bot]
fa65cc7ee2 Bump phpunit/phpunit in the composer group across 1 directory
Bumps the composer group with 1 update in the / directory: [phpunit/phpunit](https://github.com/sebastianbergmann/phpunit).


Updates `phpunit/phpunit` from 12.5.6 to 12.5.8
- [Release notes](https://github.com/sebastianbergmann/phpunit/releases)
- [Changelog](https://github.com/sebastianbergmann/phpunit/blob/12.5.8/ChangeLog-12.5.md)
- [Commits](https://github.com/sebastianbergmann/phpunit/compare/12.5.6...12.5.8)

---
updated-dependencies:
- dependency-name: phpunit/phpunit
  dependency-version: 12.5.8
  dependency-type: direct:development
  dependency-group: composer
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-01-27 22:40:18 +00:00
James Cole
d64f1d0c18 Do not report ModelNotFoundException 2026-01-27 20:18:04 +01:00
github-actions[bot]
31206ce56c Merge pull request #11608 from firefly-iii/release-1769541194
🤖 Automatically merge the PR into the develop branch.
2026-01-27 20:13:21 +01:00
JC5
e4e9a09522 🤖 Auto commit for release 'develop' on 2026-01-27 2026-01-27 20:13:14 +01:00
James Cole
11303dc6e2 Merge branch 'main' into develop 2026-01-27 20:08:54 +01:00
James Cole
993f5cd292 Add language files. 2026-01-27 20:08:43 +01:00
github-actions[bot]
cc0854c712 Merge pull request #11607 from firefly-iii/release-1769540197
🤖 Automatically merge the PR into the develop branch.
2026-01-27 19:56:46 +01:00
JC5
5c6aee0037 🤖 Auto commit for release 'develop' on 2026-01-27 2026-01-27 19:56:37 +01:00
James Cole
391f8c34cc Merge branch 'develop' of github.com:firefly-iii/firefly-iii into develop 2026-01-27 19:50:53 +01:00
James Cole
db6ed26d5a Fix bad pointer 2026-01-27 19:50:46 +01:00
github-actions[bot]
eece951036 Merge pull request #11606 from firefly-iii/release-1769539261
🤖 Automatically merge the PR into the develop branch.
2026-01-27 19:41:10 +01:00
JC5
3d7a62293b 🤖 Auto commit for release 'develop' on 2026-01-27 2026-01-27 19:41:01 +01:00
James Cole
2691dbe438 Add flag. 2026-01-27 19:36:35 +01:00
James Cole
fe971ec611 Add new setting. 2026-01-27 19:35:14 +01:00
github-actions[bot]
9e4c5435f0 Merge pull request #11605 from firefly-iii/release-1769538639
🤖 Automatically merge the PR into the develop branch.
2026-01-27 19:30:49 +01:00
JC5
cdb2b91813 🤖 Auto commit for release 'develop' on 2026-01-27 2026-01-27 19:30:39 +01:00
James Cole
f4cf158d21 Merge branch 'develop' of github.com:firefly-iii/firefly-iii into develop
# Conflicts:
#	app/Factory/TransactionJournalFactory.php
2026-01-27 19:25:56 +01:00
James Cole
b19f1d0353 Restore use of NullArrayObject 2026-01-27 19:25:23 +01:00
github-actions[bot]
eb2c612476 Merge pull request #11604 from firefly-iii/release-1769534159
🤖 Automatically merge the PR into the develop branch.
2026-01-27 18:16:08 +01:00
JC5
e89eede8a0 🤖 Auto commit for release 'develop' on 2026-01-27 2026-01-27 18:15:59 +01:00
James Cole
00e09c4bd9 Fix #11601 2026-01-27 18:08:52 +01:00
James Cole
33e63434a3 Fix null pointers 2026-01-27 18:04:31 +01:00
James Cole
1b68e5374a Fix nulls 2026-01-27 18:04:16 +01:00
James Cole
f93e55f9b0 Fix null pointer. 2026-01-27 18:03:39 +01:00
James Cole
4a3f62df89 Fix call to old object. 2026-01-27 18:02:47 +01:00
James Cole
0c5ac39d5e Update composer packages 2026-01-27 18:00:17 +01:00
github-actions[bot]
ccb44d6fbd Merge pull request #11595 from firefly-iii/release-1769407906
🤖 Automatically merge the PR into the develop branch.
2026-01-26 07:11:54 +01:00
JC5
b9fe074080 🤖 Auto commit for release 'develop' on 2026-01-26 2026-01-26 07:11:46 +01:00
James Cole
033281ff51 Fix routes and add batch finish command. 2026-01-26 06:56:10 +01:00
github-actions[bot]
5e8d23ba91 Merge pull request #11592 from firefly-iii/release-1769398904
🤖 Automatically merge the PR into the develop branch.
2026-01-26 04:41:52 +01:00
JC5
35509f19ad 🤖 Auto commit for release 'develop' on 2026-01-26 2026-01-26 04:41:44 +01:00
github-actions[bot]
5e56eeb22e Merge pull request #11591 from firefly-iii/release-1769370043
🤖 Automatically merge the PR into the develop branch.
2026-01-25 20:40:53 +01:00
JC5
e921bb3ebe 🤖 Auto commit for release 'develop' on 2026-01-25 2026-01-25 20:40:43 +01:00
James Cole
353cd0f4f1 Try to fix call to trusted proxies 2026-01-25 20:36:25 +01:00
James Cole
1376ed16cf Rename file. 2026-01-25 20:32:10 +01:00
github-actions[bot]
36646b9c05 Merge pull request #11590 from firefly-iii/release-1769368675
🤖 Automatically merge the PR into the develop branch.
2026-01-25 20:18:02 +01:00
JC5
0b20c9d53b 🤖 Auto commit for release 'develop' on 2026-01-25 2026-01-25 20:17:55 +01:00
James Cole
b91d8661bc Clean up authentication. 2026-01-25 20:13:53 +01:00
James Cole
b684f3fc70 Merge pull request #11589 from mateuszkulapl/dark-mode-improvements
apply user-selected light/dark mode to form elements (checkboxes, date picker) #8613 #7620
2026-01-25 19:20:16 +01:00
mergify[bot]
c8a235b0b0 Merge branch 'develop' into dark-mode-improvements 2026-01-25 18:14:52 +00:00
mateuszkulapl
229db34d13 fix: apply user-selected light/dark mode to form elements (checkboxes, date picker) #8613 #7620 2026-01-25 18:45:19 +01:00
github-actions[bot]
744ad968e7 Merge pull request #11587 from firefly-iii/release-1769360073
🤖 Automatically merge the PR into the develop branch.
2026-01-25 17:54:41 +01:00
JC5
9b04d09dfc 🤖 Auto commit for release 'develop' on 2026-01-25 2026-01-25 17:54:33 +01:00
James Cole
66c899af62 Overrule middleware 2026-01-25 17:50:19 +01:00
github-actions[bot]
4779dd8d0d Merge pull request #11586 from firefly-iii/release-1769358688
🤖 Automatically merge the PR into the develop branch.
2026-01-25 17:31:34 +01:00
JC5
c47cc7ddc4 🤖 Auto commit for release 'develop' on 2026-01-25 2026-01-25 17:31:28 +01:00
James Cole
a97d46506b Config from the right place? 2026-01-25 17:27:27 +01:00
github-actions[bot]
2db4daa069 Merge pull request #11585 from firefly-iii/release-1769357347
🤖 Automatically merge the PR into the develop branch.
2026-01-25 17:09:14 +01:00
JC5
7f07d266f1 🤖 Auto commit for release 'develop' on 2026-01-25 2026-01-25 17:09:07 +01:00
github-actions[bot]
158081395b Merge pull request #11583 from firefly-iii/release-1769334926
🤖 Automatically merge the PR into the develop branch.
2026-01-25 10:55:33 +01:00
JC5
34a7fd3ef0 🤖 Auto commit for release 'develop' on 2026-01-25 2026-01-25 10:55:27 +01:00
James Cole
96aafacf43 Fix processing transactions in events. 2026-01-25 10:47:30 +01:00
James Cole
035d599910 Fix processing new transactions. 2026-01-25 10:22:02 +01:00
James Cole
f5a929d72e Clean up a bunch of code and improve transaction store events 2026-01-25 09:02:47 +01:00
James Cole
22b97ce8ef Clean up middleware and kernel files. 2026-01-24 20:36:20 +01:00
James Cole
832019792f Improve listeners 2026-01-24 19:02:18 +01:00
James Cole
76b8ff18b0 Transaction events for #11544 2026-01-24 18:52:07 +01:00
James Cole
8c0a82ac0a Add support for batch submission. 2026-01-24 18:34:49 +01:00
James Cole
7edc386cdd Fix strict types 2026-01-24 16:47:17 +01:00
James Cole
5437d07ec2 Merge branch 'develop' of github.com:firefly-iii/firefly-iii into develop
# Conflicts:
#	app/Providers/EventServiceProvider.php
2026-01-24 16:46:08 +01:00
James Cole
ef3a1401dd Remove more events for #11544 2026-01-24 16:45:28 +01:00
147 changed files with 4547 additions and 3666 deletions

View File

@@ -26,6 +26,7 @@ $paths = [
$current . '/../../config',
$current . '/../../routes',
$current . '/../../tests',
$current . '/../../resources/lang/en_US',
];
$finder = PhpCsFixer\Finder::create()

View File

@@ -1252,16 +1252,16 @@
},
{
"name": "symfony/console",
"version": "v8.0.3",
"version": "v8.0.4",
"source": {
"type": "git",
"url": "https://github.com/symfony/console.git",
"reference": "6145b304a5c1ea0bdbd0b04d297a5864f9a7d587"
"reference": "ace03c4cf9805080ff40cbeec69fca180c339a3b"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/console/zipball/6145b304a5c1ea0bdbd0b04d297a5864f9a7d587",
"reference": "6145b304a5c1ea0bdbd0b04d297a5864f9a7d587",
"url": "https://api.github.com/repos/symfony/console/zipball/ace03c4cf9805080ff40cbeec69fca180c339a3b",
"reference": "ace03c4cf9805080ff40cbeec69fca180c339a3b",
"shasum": ""
},
"require": {
@@ -1318,7 +1318,7 @@
"terminal"
],
"support": {
"source": "https://github.com/symfony/console/tree/v8.0.3"
"source": "https://github.com/symfony/console/tree/v8.0.4"
},
"funding": [
{
@@ -1338,7 +1338,7 @@
"type": "tidelift"
}
],
"time": "2025-12-23T14:52:06+00:00"
"time": "2026-01-13T13:06:50+00:00"
},
{
"name": "symfony/deprecation-contracts",
@@ -1409,16 +1409,16 @@
},
{
"name": "symfony/event-dispatcher",
"version": "v8.0.0",
"version": "v8.0.4",
"source": {
"type": "git",
"url": "https://github.com/symfony/event-dispatcher.git",
"reference": "573f95783a2ec6e38752979db139f09fec033f03"
"reference": "99301401da182b6cfaa4700dbe9987bb75474b47"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/573f95783a2ec6e38752979db139f09fec033f03",
"reference": "573f95783a2ec6e38752979db139f09fec033f03",
"url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/99301401da182b6cfaa4700dbe9987bb75474b47",
"reference": "99301401da182b6cfaa4700dbe9987bb75474b47",
"shasum": ""
},
"require": {
@@ -1470,7 +1470,7 @@
"description": "Provides tools that allow your application components to communicate with each other by dispatching events and listening to them",
"homepage": "https://symfony.com",
"support": {
"source": "https://github.com/symfony/event-dispatcher/tree/v8.0.0"
"source": "https://github.com/symfony/event-dispatcher/tree/v8.0.4"
},
"funding": [
{
@@ -1490,7 +1490,7 @@
"type": "tidelift"
}
],
"time": "2025-10-30T14:17:19+00:00"
"time": "2026-01-05T11:45:55+00:00"
},
{
"name": "symfony/event-dispatcher-contracts",
@@ -1640,16 +1640,16 @@
},
{
"name": "symfony/finder",
"version": "v8.0.3",
"version": "v8.0.5",
"source": {
"type": "git",
"url": "https://github.com/symfony/finder.git",
"reference": "dd3a2953570a283a2ba4e17063bb98c734cf5b12"
"reference": "8bd576e97c67d45941365bf824e18dc8538e6eb0"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/finder/zipball/dd3a2953570a283a2ba4e17063bb98c734cf5b12",
"reference": "dd3a2953570a283a2ba4e17063bb98c734cf5b12",
"url": "https://api.github.com/repos/symfony/finder/zipball/8bd576e97c67d45941365bf824e18dc8538e6eb0",
"reference": "8bd576e97c67d45941365bf824e18dc8538e6eb0",
"shasum": ""
},
"require": {
@@ -1684,7 +1684,7 @@
"description": "Finds files and directories via an intuitive fluent interface",
"homepage": "https://symfony.com",
"support": {
"source": "https://github.com/symfony/finder/tree/v8.0.3"
"source": "https://github.com/symfony/finder/tree/v8.0.5"
},
"funding": [
{
@@ -1704,7 +1704,7 @@
"type": "tidelift"
}
],
"time": "2025-12-23T14:52:06+00:00"
"time": "2026-01-26T15:08:38+00:00"
},
{
"name": "symfony/options-resolver",
@@ -2358,16 +2358,16 @@
},
{
"name": "symfony/process",
"version": "v8.0.3",
"version": "v8.0.5",
"source": {
"type": "git",
"url": "https://github.com/symfony/process.git",
"reference": "0cbbd88ec836f8757641c651bb995335846abb78"
"reference": "b5f3aa6762e33fd95efbaa2ec4f4bc9fdd16d674"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/process/zipball/0cbbd88ec836f8757641c651bb995335846abb78",
"reference": "0cbbd88ec836f8757641c651bb995335846abb78",
"url": "https://api.github.com/repos/symfony/process/zipball/b5f3aa6762e33fd95efbaa2ec4f4bc9fdd16d674",
"reference": "b5f3aa6762e33fd95efbaa2ec4f4bc9fdd16d674",
"shasum": ""
},
"require": {
@@ -2399,7 +2399,7 @@
"description": "Executes commands in sub-processes",
"homepage": "https://symfony.com",
"support": {
"source": "https://github.com/symfony/process/tree/v8.0.3"
"source": "https://github.com/symfony/process/tree/v8.0.5"
},
"funding": [
{
@@ -2419,7 +2419,7 @@
"type": "tidelift"
}
],
"time": "2025-12-19T10:01:18+00:00"
"time": "2026-01-26T15:08:38+00:00"
},
{
"name": "symfony/service-contracts",
@@ -2576,16 +2576,16 @@
},
{
"name": "symfony/string",
"version": "v8.0.1",
"version": "v8.0.4",
"source": {
"type": "git",
"url": "https://github.com/symfony/string.git",
"reference": "ba65a969ac918ce0cc3edfac6cdde847eba231dc"
"reference": "758b372d6882506821ed666032e43020c4f57194"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/string/zipball/ba65a969ac918ce0cc3edfac6cdde847eba231dc",
"reference": "ba65a969ac918ce0cc3edfac6cdde847eba231dc",
"url": "https://api.github.com/repos/symfony/string/zipball/758b372d6882506821ed666032e43020c4f57194",
"reference": "758b372d6882506821ed666032e43020c4f57194",
"shasum": ""
},
"require": {
@@ -2642,7 +2642,7 @@
"utf8"
],
"support": {
"source": "https://github.com/symfony/string/tree/v8.0.1"
"source": "https://github.com/symfony/string/tree/v8.0.4"
},
"funding": [
{
@@ -2662,7 +2662,7 @@
"type": "tidelift"
}
],
"time": "2025-12-01T09:13:36+00:00"
"time": "2026-01-12T12:37:40+00:00"
}
],
"packages-dev": [],

View File

@@ -4,6 +4,8 @@ Over time, many people have contributed to Firefly III. Their efforts are not al
Please find below all the people who contributed to the Firefly III code. Their names are mentioned in the year of their first contribution.
## 2026
- Nick Huang
- mateuszkulapl
- Gianluca Martino
- embedded

View File

@@ -158,7 +158,10 @@ class TagController extends Controller
'currency_id' => (string) $foreignCurrencyId,
'currency_code' => $journal['foreign_currency_code'],
];
$response[$foreignKey]['difference'] = bcadd((string) $response[$foreignKey]['difference'], Steam::positive($journal['foreign_amount']));
$response[$foreignKey]['difference'] = bcadd(
(string) $response[$foreignKey]['difference'],
Steam::positive($journal['foreign_amount'])
);
$response[$foreignKey]['difference_float'] = (float) $response[$foreignKey]['difference'];
}
}

View File

@@ -155,7 +155,10 @@ class TagController extends Controller
'currency_id' => (string) $foreignCurrencyId,
'currency_code' => $journal['foreign_currency_code'],
];
$response[$foreignKey]['difference'] = bcadd((string) $response[$foreignKey]['difference'], Steam::positive($journal['foreign_amount']));
$response[$foreignKey]['difference'] = bcadd(
(string) $response[$foreignKey]['difference'],
Steam::positive($journal['foreign_amount'])
);
$response[$foreignKey]['difference_float'] = (float) $response[$foreignKey]['difference']; // intentional float
}
}

View File

@@ -27,7 +27,8 @@ namespace FireflyIII\Api\V1\Controllers\Models\Transaction;
use FireflyIII\Api\V1\Controllers\Controller;
use FireflyIII\Api\V1\Requests\Models\Transaction\StoreRequest;
use FireflyIII\Enums\UserRoleEnum;
use FireflyIII\Events\StoredTransactionGroup;
use FireflyIII\Events\Model\TransactionGroup\CreatedSingleTransactionGroup;
use FireflyIII\Events\Model\TransactionGroup\TransactionGroupEventFlags;
use FireflyIII\Exceptions\DuplicateTransactionException;
use FireflyIII\Exceptions\FireflyException;
use FireflyIII\Helpers\Collector\GroupCollectorInterface;
@@ -87,9 +88,9 @@ class StoreController extends Controller
public function store(StoreRequest $request): JsonResponse
{
Log::debug('Now in API StoreController::store()');
$data = $request->getAll();
$data['user'] = auth()->user();
$data['user_group'] = $this->userGroup;
$data = $request->getAll();
$data['user'] = auth()->user();
$data['user_group'] = $this->userGroup;
Log::channel('audit')->info('Store new transaction over API.', $data);
@@ -109,18 +110,21 @@ class StoreController extends Controller
throw new ValidationException($validator);
}
Preferences::mark();
$applyRules = $data['apply_rules'] ?? true;
$fireWebhooks = $data['fire_webhooks'] ?? true;
event(new StoredTransactionGroup($transactionGroup, $applyRules, $fireWebhooks));
$flags = new TransactionGroupEventFlags();
$flags->applyRules = $data['apply_rules'] ?? true;
$flags->fireWebhooks = $data['fire_webhooks'] ?? true;
$flags->batchSubmission = $data['batch_submission'] ?? false;
Log::debug('CreatedSingleTransactionGroup');
event(new CreatedSingleTransactionGroup($transactionGroup, $flags));
$manager = $this->getManager();
$manager = $this->getManager();
/** @var User $admin */
$admin = auth()->user();
$admin = auth()->user();
// use new group collector:
/** @var GroupCollectorInterface $collector */
$collector = app(GroupCollectorInterface::class);
$collector = app(GroupCollectorInterface::class);
$collector
->setUser($admin)
->setUserGroup($this->userGroup)
@@ -130,20 +134,20 @@ class StoreController extends Controller
->withAPIInformation()
;
$selectedGroup = $collector->getGroups()->first();
$selectedGroup = $collector->getGroups()->first();
if (null === $selectedGroup) {
throw HttpException::fromStatusCode(410, '200032: Cannot find transaction. Possibly, a rule deleted this transaction after its creation.');
}
// enrich
$enrichment = new TransactionGroupEnrichment();
$enrichment = new TransactionGroupEnrichment();
$enrichment->setUser($admin);
$selectedGroup = $enrichment->enrichSingle($selectedGroup);
$selectedGroup = $enrichment->enrichSingle($selectedGroup);
/** @var TransactionGroupTransformer $transformer */
$transformer = app(TransactionGroupTransformer::class);
$transformer = app(TransactionGroupTransformer::class);
$transformer->setParameters($this->parameters);
$resource = new Item($selectedGroup, $transformer, 'transactions');
$resource = new Item($selectedGroup, $transformer, 'transactions');
return response()->json($manager->createData($resource)->toArray())->header('Content-Type', self::CONTENT_TYPE);
}

View File

@@ -82,6 +82,9 @@ class UpdateController extends Controller
$applyRules = $data['apply_rules'] ?? true;
$fireWebhooks = $data['fire_webhooks'] ?? true;
$runRecalculations = $oldHash !== $newHash;
// FIXME responds to a single event.
// flags in array?
event(new UpdatedTransactionGroup($transactionGroup, $applyRules, $fireWebhooks, $runRecalculations));
/** @var User $admin */

View File

@@ -0,0 +1,71 @@
<?php
declare(strict_types=1);
/*
* BatchController.php
* Copyright (c) 2026 james@firefly-iii.org
*
* This file is part of Firefly III (https://github.com/firefly-iii).
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
namespace FireflyIII\Api\V1\Controllers\System;
use FireflyIII\Api\V1\Controllers\Controller;
use FireflyIII\Events\Model\TransactionGroup\CreatedSingleTransactionGroup;
use FireflyIII\Events\Model\TransactionGroup\TransactionGroupEventFlags;
use FireflyIII\Models\TransactionJournal;
use FireflyIII\Repositories\Journal\JournalRepositoryInterface;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
class BatchController extends Controller
{
private JournalRepositoryInterface $repository;
/**
* UserController constructor.
*/
public function __construct()
{
parent::__construct();
$this->middleware(function ($request, $next) {
$this->repository = app(JournalRepositoryInterface::class);
return $next($request);
});
}
public function finishBatch(Request $request): JsonResponse
{
$journals = $this->repository->getUncompletedJournals();
if (0 === count($journals)) {
return response()->json([], 204);
}
/** @var TransactionJournal $first */
$first = $journals->first();
$group = $first?->transactionGroup;
if (null === $group) {
return response()->json([], 204);
}
$flags = new TransactionGroupEventFlags();
$flags->applyRules = 'true' === $request->get('apply_rules');
event(new CreatedSingleTransactionGroup($group, $flags));
return response()->json([], 204);
}
}

View File

@@ -58,6 +58,8 @@ class UserController extends Controller
});
}
public function finishBatch(): JsonResponse {}
/**
* This endpoint is documented at:
* https://api-docs.firefly-iii.org/?urls.primaryName=2.0.0%20(v1)#/users/deleteUser

View File

@@ -47,7 +47,7 @@ class StoreRequest extends FormRequest
*/
public function getAll(): array
{
$fields = ['order' => ['order', 'convertInteger']];
$fields = ['order' => ['order', 'convertInteger']];
$data = $this->getAllData($fields);
$data['name'] = $this->convertString('name');
$data['accounts'] = $this->parseAccounts($this->get('accounts'));

View File

@@ -64,6 +64,7 @@ class StoreRequest extends FormRequest
return [
'group_title' => $this->convertString('group_title'),
'error_if_duplicate_hash' => $this->boolean('error_if_duplicate_hash'),
'batch_submission' => $this->boolean('batch_submission'),
'apply_rules' => $this->boolean('apply_rules', true),
'fire_webhooks' => $this->boolean('fire_webhooks', true),
'transactions' => $this->getTransactionData(),

View File

@@ -113,7 +113,7 @@ class UpdateRequest extends FormRequest
];
$this->booleanFields = ['reconciled'];
$this->arrayFields = ['tags'];
$data = [];
$data = ['batch_submission' => false];
if ($this->has('transactions')) {
$data['transactions'] = $this->getTransactionData();
}
@@ -123,6 +123,9 @@ class UpdateRequest extends FormRequest
if ($this->has('fire_webhooks')) {
$data['fire_webhooks'] = $this->boolean('fire_webhooks', true);
}
if ($this->has('batch_submission')) {
$data['batch_submission'] = $this->boolean('batch_submission');
}
if ($this->has('group_title')) {
$data['group_title'] = $this->convertString('group_title');
}

View File

@@ -57,6 +57,7 @@ class CorrectsGroupAccounts extends Command
foreach ($groups as $groupId) {
$group = TransactionGroup::find($groupId);
// TODO in theory the "unifyAccounts" method could lead to the need for run recalculations.
// FIXME needs to be a collection.
$event = new UpdatedTransactionGroup($group, true, true, false);
$handler->unifyAccounts($event);
}

View File

@@ -68,7 +68,7 @@ class CorrectsMetaDataFields extends Command
private function rename(string $original, string $update): void
{
$total = DB::table('journal_meta')->where('name', '=', $original)->update(['name' => $update]);
$total = DB::table('journal_meta')->where('name', '=', $original)->update(['name' => $update]);
$this->count += $total;
}
}

View File

@@ -369,7 +369,7 @@ class CorrectsUnevenAmount extends Command
continue;
}
if (!$this->isBetweenAssetAndLiability($journal)) {
Log::debug('Not between asset and liability, continue.');
// Log::debug('Not between asset and liability, continue.');
continue;
}

View File

@@ -295,7 +295,11 @@ class UpgradesTransferCurrencies extends Command
{
if (null === $this->sourceTransaction->transaction_currency_id && $this->sourceCurrency instanceof TransactionCurrency) {
$this->sourceTransaction->transaction_currency_id = $this->sourceCurrency->id;
$message = sprintf('Transaction #%d has no currency setting, now set to %s.', $this->sourceTransaction->id, $this->sourceCurrency->code);
$message = sprintf(
'Transaction #%d has no currency setting, now set to %s.',
$this->sourceTransaction->id,
$this->sourceCurrency->code
);
$this->friendlyInfo($message);
++$this->count;
$this->sourceTransaction->save();
@@ -335,7 +339,11 @@ class UpgradesTransferCurrencies extends Command
{
if (null === $this->destinationTransaction->transaction_currency_id && $this->destinationCurrency instanceof TransactionCurrency) {
$this->destinationTransaction->transaction_currency_id = $this->destinationCurrency->id;
$message = sprintf('Transaction #%d has no currency setting, now set to %s.', $this->destinationTransaction->id, $this->destinationCurrency->code);
$message = sprintf(
'Transaction #%d has no currency setting, now set to %s.',
$this->destinationTransaction->id,
$this->destinationCurrency->code
);
$this->friendlyInfo($message);
++$this->count;
$this->destinationTransaction->save();

View File

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

View File

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

View File

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

View File

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

View File

@@ -31,6 +31,7 @@ use FireflyIII\Jobs\MailError;
use FireflyIII\Support\Facades\Steam;
use Illuminate\Auth\Access\AuthorizationException;
use Illuminate\Auth\AuthenticationException;
use Illuminate\Database\Eloquent\ModelNotFoundException;
use Illuminate\Database\QueryException;
use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler;
use Illuminate\Http\JsonResponse;
@@ -71,6 +72,7 @@ class Handler extends ExceptionHandler
AuthenticationException::class,
LaravelValidationException::class,
NotFoundHttpException::class,
ModelNotFoundException::class,
GoneHttpException::class,
OAuthServerException::class,
LaravelOAuthException::class,

View File

@@ -110,22 +110,24 @@ class TransactionJournalFactory
{
Log::debug('Now in TransactionJournalFactory::create()');
// convert to special object.
$dataObject = new NullArrayObject($data);
// $dataObject = new NullArrayObject($data);
Log::debug('Start of TransactionJournalFactory::create()');
$collection = new Collection();
$transactions = $dataObject['transactions'] ?? [];
$collection = new Collection();
$transactions = $data['transactions'] ?? [];
if (0 === count($transactions)) {
Log::error('There are no transactions in the array, the TransactionJournalFactory cannot continue.');
return new Collection();
}
$batchSubmission = $data['batch_submission'] ?? false;
try {
/** @var array $row */
foreach ($transactions as $index => $row) {
$row['batch_submission'] = $batchSubmission;
Log::debug(sprintf('Now creating journal %d/%d', $index + 1, count($transactions)));
$journal = $this->createJournal(new NullArrayObject($row));
$journal = $this->createJournal(new NullArrayObject($row));
if ($journal instanceof TransactionJournal) {
$collection->push($journal);
}
@@ -273,7 +275,7 @@ class TransactionJournalFactory
'date_tz' => $carbon->format('e'),
'order' => $order,
'tag_count' => 0,
'completed' => 0,
'completed' => !$row['batch_submission'],
]);
Log::debug(sprintf('Created new journal #%d: "%s"', $journal->id, $journal->description));
@@ -331,7 +333,7 @@ class TransactionJournalFactory
throw new FireflyException($e->getMessage(), 0, $e);
}
$journal->completed = true;
Log::debug(sprintf('Is part of a batch submission? %s', var_export($row['batch_submission'], true)));
$journal->save();
$this->storeBudget($journal, $row);
$this->storeCategory($journal, $row);
@@ -346,18 +348,16 @@ class TransactionJournalFactory
private function hashArray(NullArrayObject $row): string
{
$dataRow = $row->getArrayCopy();
unset($dataRow['import_hash_v2'], $dataRow['original_source']);
unset($row['import_hash_v2'], $row['original_source']);
try {
$json = json_encode($dataRow, JSON_THROW_ON_ERROR);
$json = json_encode($row, JSON_THROW_ON_ERROR);
} catch (JsonException $e) {
Log::error(sprintf('Could not encode dataRow: %s', $e->getMessage()));
$json = microtime();
}
$hash = hash('sha256', $json);
Log::debug(sprintf('The hash is: %s', $hash), $dataRow);
$hash = hash('sha256', $json);
Log::debug(sprintf('The hash is: %s', $hash), $row->getArrayCopy());
return $hash;
}
@@ -595,14 +595,14 @@ class TransactionJournalFactory
private function storeMetaFields(TransactionJournal $journal, NullArrayObject $transaction): void
{
foreach ($this->fields as $field) {
$this->storeMeta($journal, $transaction, $field);
$this->storeMeta($journal, $transaction->getArrayCopy(), $field);
}
}
protected function storeMeta(TransactionJournal $journal, NullArrayObject $data, string $field): void
protected function storeMeta(TransactionJournal $journal, array $data, string $field): void
{
$set = ['journal' => $journal, 'name' => $field, 'data' => (string) ($data[$field] ?? '')];
if ($data[$field] instanceof Carbon) {
if (array_key_exists($field, $data) && $data[$field] instanceof Carbon) {
$data[$field]->setTimezone(config('app.timezone'));
Log::debug(sprintf('%s Date: %s (%s)', $field, $data[$field], $data[$field]->timezone->getName()));
$set['data'] = $data[$field]->format('Y-m-d H:i:s');

View File

@@ -47,16 +47,16 @@ class StoredGroupEventHandler
{
public function runAllHandlers(StoredTransactionGroup $event): void
{
$this->processRules($event, null);
$this->recalculateCredit($event);
$this->triggerWebhooks($event);
// $this->processRules($event, null);
// $this->recalculateCredit($event);
// $this->triggerWebhooks($event);
$this->removePeriodStatistics($event);
}
public function triggerRulesManually(TriggeredStoredTransactionGroup $event): void
{
$newEvent = new StoredTransactionGroup($event->transactionGroup, true, false);
$this->processRules($newEvent, $event->ruleGroup);
// $newEvent = new StoredTransactionGroup($event->transactionGroup, true, false);
// $this->processRules($newEvent, $event->ruleGroup);
}
/**

View File

@@ -24,7 +24,6 @@ declare(strict_types=1);
namespace FireflyIII\Handlers\Observer;
use FireflyIII\Models\Transaction;
use FireflyIII\Models\TransactionJournal;
use FireflyIII\Support\Facades\Amount;
use FireflyIII\Support\Facades\FireflyConfig;
use FireflyIII\Support\Http\Api\ExchangeRateConverter;
@@ -40,19 +39,8 @@ class TransactionObserver
public function created(Transaction $transaction): void
{
Log::debug('Observe "created" of a transaction.');
if (
true === FireflyConfig::get('use_running_balance', config('firefly.feature_flags.running_balance_column'))->data && (
1 === bccomp($transaction->amount, '0')
&& self::$recalculate
)
) {
Log::debug('Trigger recalculateForJournal');
$journal = $transaction->transactionJournal;
if ($journal instanceof TransactionJournal) {
AccountBalanceCalculator::recalculateForJournal($journal);
}
}
return;
$this->updatePrimaryCurrencyAmount($transaction);
}

View File

@@ -441,8 +441,8 @@ class GroupCollector implements GroupCollectorInterface
$this->query->orWhereIn('transaction_journals.transaction_group_id', $groupIds);
}
$result = $this->query->get($this->fields);
$this->dumpQueryInLogs();
Log::debug(sprintf('Count of result is %d', $result->count()));
// $this->dumpQueryInLogs();
// Log::debug(sprintf('Count of result is %d', $result->count()));
// now to parse this into an array.
$collection = $this->parseArray($result);

View File

@@ -61,36 +61,38 @@ class ConfigurationController extends Controller
*/
public function index(): Factory|\Illuminate\Contracts\View\View
{
$subTitle = (string) trans('firefly.instance_configuration');
$subTitleIcon = 'fa-wrench';
$subTitle = (string) trans('firefly.instance_configuration');
$subTitleIcon = 'fa-wrench';
Log::channel('audit')->info('User visits admin config index.');
// all available configuration and their default value in case
// they don't exist yet.
$singleUserMode = FireflyConfig::get('single_user_mode', config('firefly.configuration.single_user_mode'))->data;
$isDemoSite = FireflyConfig::get('is_demo_site', config('firefly.configuration.is_demo_site'))->data;
$siteOwner = config('firefly.site_owner');
$singleUserMode = FireflyConfig::get('single_user_mode', config('firefly.configuration.single_user_mode'))->data;
$isDemoSite = FireflyConfig::get('is_demo_site', config('firefly.configuration.is_demo_site'))->data;
$siteOwner = config('firefly.site_owner');
$enableExchangeRates = FireflyConfig::get('enable_exchange_rates', config('cer.enabled'))->data;
$useRunningBalance = FireflyConfig::get('use_running_balance', config('firefly.feature_flags.running_balance_column'))->data;
$enableExternalMap = FireflyConfig::get('enable_external_map', config('firefly.enable_external_map'))->data;
$enableExternalRates = FireflyConfig::get('enable_external_rates', config('cer.download_enabled'))->data;
$allowWebhooks = FireflyConfig::get('allow_webhooks', config('firefly.allow_webhooks'))->data;
$validUrlProtocols = FireflyConfig::get('valid_url_protocols', config('firefly.valid_url_protocols'))->data;
$enableExchangeRates = FireflyConfig::get('enable_exchange_rates', config('cer.enabled'))->data;
$useRunningBalance = FireflyConfig::get('use_running_balance', config('firefly.feature_flags.running_balance_column'))->data;
$enableExternalMap = FireflyConfig::get('enable_external_map', config('firefly.enable_external_map'))->data;
$enableExternalRates = FireflyConfig::get('enable_external_rates', config('cer.download_enabled'))->data;
$allowWebhooks = FireflyConfig::get('allow_webhooks', config('firefly.allow_webhooks'))->data;
$enableBatchProcessing = FireflyConfig::get('enable_batch_processing', false)->data;
$validUrlProtocols = FireflyConfig::get('valid_url_protocols', config('firefly.valid_url_protocols'))->data;
return view('settings.configuration.index', [
'subTitle' => $subTitle,
'subTitleIcon' => $subTitleIcon,
'singleUserMode' => $singleUserMode,
'isDemoSite' => $isDemoSite,
'siteOwner' => $siteOwner,
'enableExchangeRates' => $enableExchangeRates,
'useRunningBalance' => $useRunningBalance,
'enableExternalMap' => $enableExternalMap,
'enableExternalRates' => $enableExternalRates,
'allowWebhooks' => $allowWebhooks,
'validUrlProtocols' => $validUrlProtocols,
'subTitle' => $subTitle,
'subTitleIcon' => $subTitleIcon,
'singleUserMode' => $singleUserMode,
'isDemoSite' => $isDemoSite,
'siteOwner' => $siteOwner,
'enableExchangeRates' => $enableExchangeRates,
'useRunningBalance' => $useRunningBalance,
'enableExternalMap' => $enableExternalMap,
'enableExternalRates' => $enableExternalRates,
'allowWebhooks' => $allowWebhooks,
'enableBatchProcessing' => $enableBatchProcessing,
'validUrlProtocols' => $validUrlProtocols,
]);
}

View File

@@ -23,7 +23,7 @@ declare(strict_types=1);
namespace FireflyIII\Http\Controllers\Admin;
use FireflyIII\Events\Admin\InvitationCreated;
use FireflyIII\Events\Security\System\NewInvitationCreated;
use FireflyIII\Exceptions\FireflyException;
use FireflyIII\Http\Controllers\Controller;
use FireflyIII\Http\Middleware\IsDemoUser;
@@ -202,7 +202,7 @@ class UserController extends Controller
session()->flash('info', trans('firefly.user_is_invited', ['address' => $address]));
// event!
event(new InvitationCreated($invitee));
event(new NewInvitationCreated($invitee));
return redirect(route('settings.users'));
}

View File

@@ -167,7 +167,7 @@ class LoginController extends Controller
*/
protected function sendFailedLoginResponse(Request $request): void
{
$exception = ValidationException::withMessages([$this->username() => [trans('auth.failed')]]);
$exception = ValidationException::withMessages([$this->username() => [trans('auth.failed')]]);
$exception->redirectTo = route('login');
throw $exception;

View File

@@ -192,7 +192,10 @@ class IndexController extends Controller
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 = 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);
Log::debug(

View File

@@ -197,7 +197,13 @@ class BudgetLimitController extends Controller
if ($request->expectsJson()) {
$array = $limit->toArray();
// add some extra metadata:
$spentArr = $this->opsRepository->sumExpenses($limit->start_date, $limit->end_date, null, new Collection()->push($budget), $currency);
$spentArr = $this->opsRepository->sumExpenses(
$limit->start_date,
$limit->end_date,
null,
new Collection()->push($budget),
$currency
);
$array['spent'] = $spentArr[$currency->id]['sum'] ?? '0';
$array['left_formatted'] = Amount::formatAnything($limit->transactionCurrency, bcadd($array['spent'], (string) $array['amount']));
$array['amount_formatted'] = Amount::formatAnything($limit->transactionCurrency, $limit['amount']);

View File

@@ -73,7 +73,7 @@ class BillController extends Controller
*/
foreach ($paid as $info) {
$amount = $info['sum'];
$label = (string) trans('firefly.paid_in_currency', ['currency' => $info['name']]);
$label = (string) trans('firefly.paid_in_currency', ['currency' => $info['name']]);
$chartData[$label] = ['amount' => $amount, 'currency_symbol' => $info['symbol'], 'currency_code' => $info['code']];
}
@@ -82,7 +82,7 @@ class BillController extends Controller
*/
foreach ($unpaid as $info) {
$amount = $info['sum'];
$label = (string) trans('firefly.unpaid_in_currency', ['currency' => $info['name']]);
$label = (string) trans('firefly.unpaid_in_currency', ['currency' => $info['name']]);
$chartData[$label] = ['amount' => $amount, 'currency_symbol' => $info['symbol'], 'currency_code' => $info['code']];
}

View File

@@ -539,7 +539,13 @@ class BudgetController extends Controller
}
// get spent amount in this period for this currency.
$sum = $this->opsRepository->sumExpenses($currentStart, $currentEnd, $accounts, new Collection()->push($budget), $currency);
$sum = $this->opsRepository->sumExpenses(
$currentStart,
$currentEnd,
$accounts,
new Collection()->push($budget),
$currency
);
$amount = Steam::positive($sum[$currency->id]['sum'] ?? '0');
$chartData[0]['entries'][$title] = Steam::bcround($amount, $currency->decimal_places);

View File

@@ -76,7 +76,11 @@ class TransactionController extends Controller
foreach ($result as $journal) {
$budget = $journal['budget_name'] ?? (string) trans('firefly.no_budget');
$title = sprintf('%s (%s)', $budget, $journal['currency_symbol']);
$data[$title] ??= ['amount' => '0', 'currency_symbol' => $journal['currency_symbol'], 'currency_code' => $journal['currency_code']];
$data[$title] ??= [
'amount' => '0',
'currency_symbol' => $journal['currency_symbol'],
'currency_code' => $journal['currency_code'],
];
$data[$title]['amount'] = bcadd($data[$title]['amount'], (string) $journal['amount']);
}
$chart = $this->generator->multiCurrencyPieChart($data);
@@ -122,7 +126,11 @@ class TransactionController extends Controller
foreach ($result as $journal) {
$category = $journal['category_name'] ?? (string) trans('firefly.no_category');
$title = sprintf('%s (%s)', $category, $journal['currency_symbol']);
$data[$title] ??= ['amount' => '0', 'currency_symbol' => $journal['currency_symbol'], 'currency_code' => $journal['currency_code']];
$data[$title] ??= [
'amount' => '0',
'currency_symbol' => $journal['currency_symbol'],
'currency_code' => $journal['currency_code'],
];
$data[$title]['amount'] = bcadd($data[$title]['amount'], (string) $journal['amount']);
}
$chart = $this->generator->multiCurrencyPieChart($data);
@@ -168,7 +176,11 @@ class TransactionController extends Controller
foreach ($result as $journal) {
$name = $journal['destination_account_name'];
$title = sprintf('%s (%s)', $name, $journal['currency_symbol']);
$data[$title] ??= ['amount' => '0', 'currency_symbol' => $journal['currency_symbol'], 'currency_code' => $journal['currency_code']];
$data[$title] ??= [
'amount' => '0',
'currency_symbol' => $journal['currency_symbol'],
'currency_code' => $journal['currency_code'],
];
$data[$title]['amount'] = bcadd($data[$title]['amount'], (string) $journal['amount']);
}
$chart = $this->generator->multiCurrencyPieChart($data);
@@ -214,7 +226,11 @@ class TransactionController extends Controller
foreach ($result as $journal) {
$name = $journal['source_account_name'];
$title = sprintf('%s (%s)', $name, $journal['currency_symbol']);
$data[$title] ??= ['amount' => '0', 'currency_symbol' => $journal['currency_symbol'], 'currency_code' => $journal['currency_code']];
$data[$title] ??= [
'amount' => '0',
'currency_symbol' => $journal['currency_symbol'],
'currency_code' => $journal['currency_code'],
];
$data[$title]['amount'] = bcadd($data[$title]['amount'], (string) $journal['amount']);
}
$chart = $this->generator->multiCurrencyPieChart($data);

View File

@@ -95,7 +95,11 @@ class CategoryController extends Controller
'categories' => [],
];
$report[$sourceAccountId]['currencies'][$currencyId]['categories'][$category['id']] ??= ['spent' => '0', 'earned' => '0', 'sum' => '0'];
$report[$sourceAccountId]['currencies'][$currencyId]['categories'][$category['id']] ??= [
'spent' => '0',
'earned' => '0',
'sum' => '0',
];
$report[$sourceAccountId]['currencies'][$currencyId]['categories'][$category['id']]['spent'] = bcadd(
$report[$sourceAccountId]['currencies'][$currencyId]['categories'][$category['id']]['spent'],
(string) $journal['amount']
@@ -122,7 +126,11 @@ class CategoryController extends Controller
'currency_decimal_places' => $currency['currency_decimal_places'],
'categories' => [],
];
$report[$destinationId]['currencies'][$currencyId]['categories'][$category['id']] ??= ['spent' => '0', 'earned' => '0', 'sum' => '0'];
$report[$destinationId]['currencies'][$currencyId]['categories'][$category['id']] ??= [
'spent' => '0',
'earned' => '0',
'sum' => '0',
];
$report[$destinationId]['currencies'][$currencyId]['categories'][$category['id']]['earned'] = bcadd(
$report[$destinationId]['currencies'][$currencyId]['categories'][$category['id']]['earned'],
(string) $journal['amount']

View File

@@ -24,7 +24,8 @@ declare(strict_types=1);
namespace FireflyIII\Http\Controllers\Transaction;
use FireflyIII\Events\StoredTransactionGroup;
use FireflyIII\Events\Model\TransactionGroup\CreatedSingleTransactionGroup;
use FireflyIII\Events\Model\TransactionGroup\TransactionGroupEventFlags;
use FireflyIII\Http\Controllers\Controller;
use FireflyIII\Models\TransactionGroup;
use FireflyIII\Repositories\Account\AccountRepositoryInterface;
@@ -76,7 +77,9 @@ class CreateController extends Controller
$newGroup = $service->cloneGroup($group);
// event!
event(new StoredTransactionGroup($newGroup, true, true));
$flags = new TransactionGroupEventFlags();
event(new CreatedSingleTransactionGroup($group, $flags));
// event(new StoredTransactionGroup($newGroup, true, true));
Preferences::mark();
@@ -107,29 +110,29 @@ class CreateController extends Controller
{
Preferences::mark();
$sourceId = (int) request()->get('source');
$destinationId = (int) request()->get('destination');
$sourceId = (int) request()->get('source');
$destinationId = (int) request()->get('destination');
/** @var AccountRepositoryInterface $accountRepository */
$accountRepository = app(AccountRepositoryInterface::class);
$cash = $accountRepository->getCashAccount();
$preFilled = session()->has('preFilled') ? session('preFilled') : [];
$subTitle = (string) trans(sprintf('breadcrumbs.create_%s', strtolower((string) $objectType)));
$subTitleIcon = 'fa-plus';
$accountRepository = app(AccountRepositoryInterface::class);
$cash = $accountRepository->getCashAccount();
$preFilled = session()->has('preFilled') ? session('preFilled') : [];
$subTitle = (string) trans(sprintf('breadcrumbs.create_%s', strtolower((string) $objectType)));
$subTitleIcon = 'fa-plus';
/** @var null|array $optionalFields */
$optionalFields = Preferences::get('transaction_journal_optional_fields', [])->data;
$allowedOpposingTypes = config('firefly.allowed_opposing_types');
$accountToTypes = config('firefly.account_to_transaction');
$previousUrl = $this->rememberPreviousUrl('transactions.create.url');
$parts = parse_url((string) $previousUrl);
$search = sprintf('?%s', $parts['query'] ?? '');
$previousUrl = str_replace($search, '', $previousUrl);
$optionalFields = Preferences::get('transaction_journal_optional_fields', [])->data;
$allowedOpposingTypes = config('firefly.allowed_opposing_types');
$accountToTypes = config('firefly.account_to_transaction');
$previousUrl = $this->rememberPreviousUrl('transactions.create.url');
$parts = parse_url((string) $previousUrl);
$search = sprintf('?%s', $parts['query'] ?? '');
$previousUrl = str_replace($search, '', $previousUrl);
if (!is_array($optionalFields)) {
$optionalFields = [];
}
// not really a fan of this, but meh.
$optionalDateFields = [
$optionalDateFields = [
'interest_date' => $optionalFields['interest_date'] ?? false,
'book_date' => $optionalFields['book_date'] ?? false,
'process_date' => $optionalFields['process_date'] ?? false,
@@ -139,13 +142,13 @@ class CreateController extends Controller
];
$optionalFields['external_url'] ??= false;
$optionalFields['location'] ??= false;
$optionalFields['location']
= $optionalFields['location'] && true === FireflyConfig::get('enable_external_map', config('firefly.enable_external_map'))->data;
$optionalFields['location'] = $optionalFields['location']
&& true === FireflyConfig::get('enable_external_map', config('firefly.enable_external_map'))->data;
// map info:
$longitude = config('firefly.default_location.longitude');
$latitude = config('firefly.default_location.latitude');
$zoomLevel = config('firefly.default_location.zoom_level');
$longitude = config('firefly.default_location.longitude');
$latitude = config('firefly.default_location.latitude');
$zoomLevel = config('firefly.default_location.zoom_level');
session()->put('preFilled', $preFilled);

View File

@@ -83,29 +83,29 @@ class EditController extends Controller
}
/** @var AccountRepositoryInterface $repository */
$repository = app(AccountRepositoryInterface::class);
$allowedOpposingTypes = config('firefly.allowed_opposing_types');
$accountToTypes = config('firefly.account_to_transaction');
$expectedSourceTypes = config('firefly.expected_source_types');
$allowedSourceDests = config('firefly.source_dests');
$title = $transactionGroup->transactionJournals()->count() > 1
$repository = app(AccountRepositoryInterface::class);
$allowedOpposingTypes = config('firefly.allowed_opposing_types');
$accountToTypes = config('firefly.account_to_transaction');
$expectedSourceTypes = config('firefly.expected_source_types');
$allowedSourceDests = config('firefly.source_dests');
$title = $transactionGroup->transactionJournals()->count() > 1
? $transactionGroup->title
: $transactionGroup->transactionJournals()->first()->description;
$subTitle = (string) trans('firefly.edit_transaction_title', ['description' => $title]);
$subTitleIcon = 'fa-plus';
$cash = $repository->getCashAccount();
$previousUrl = $this->rememberPreviousUrl('transactions.edit.url');
$parts = parse_url((string) $previousUrl);
$search = sprintf('?%s', $parts['query'] ?? '');
$previousUrl = str_replace($search, '', $previousUrl);
$subTitle = (string) trans('firefly.edit_transaction_title', ['description' => $title]);
$subTitleIcon = 'fa-plus';
$cash = $repository->getCashAccount();
$previousUrl = $this->rememberPreviousUrl('transactions.edit.url');
$parts = parse_url((string) $previousUrl);
$search = sprintf('?%s', $parts['query'] ?? '');
$previousUrl = str_replace($search, '', $previousUrl);
// settings necessary for v2
$optionalFields = Preferences::get('transaction_journal_optional_fields', [])->data;
$optionalFields = Preferences::get('transaction_journal_optional_fields', [])->data;
if (!is_array($optionalFields)) {
$optionalFields = [];
}
// not really a fan of this, but meh.
$optionalDateFields = [
$optionalDateFields = [
'interest_date' => $optionalFields['interest_date'] ?? false,
'book_date' => $optionalFields['book_date'] ?? false,
'process_date' => $optionalFields['process_date'] ?? false,
@@ -115,13 +115,13 @@ class EditController extends Controller
];
$optionalFields['external_url'] ??= false;
$optionalFields['location'] ??= false;
$optionalFields['location']
= $optionalFields['location'] && true === FireflyConfig::get('enable_external_map', config('firefly.enable_external_map'))->data;
$optionalFields['location'] = $optionalFields['location']
&& true === FireflyConfig::get('enable_external_map', config('firefly.enable_external_map'))->data;
// map info voor v2:
$longitude = config('firefly.default_location.longitude');
$latitude = config('firefly.default_location.latitude');
$zoomLevel = config('firefly.default_location.zoom_level');
$longitude = config('firefly.default_location.longitude');
$latitude = config('firefly.default_location.latitude');
$zoomLevel = config('firefly.default_location.zoom_level');
return view('transactions.edit', [
'cash' => $cash,

View File

@@ -78,7 +78,7 @@ class EditController extends Controller
}
$subTitleIcon = 'fa-pencil';
$subTitle = (string) trans('breadcrumbs.edit_currency', ['name' => $currency->name]);
$subTitle = (string) trans('breadcrumbs.edit_currency', ['name' => $currency->name]);
$currency->symbol = htmlentities($currency->symbol);
// is currently enabled (for this user?)

View File

@@ -23,34 +23,21 @@ declare(strict_types=1);
namespace FireflyIII\Http;
use FireflyIII\Http\Middleware\AcceptHeaders;
use FireflyIII\Http\Middleware\Authenticate;
use FireflyIII\Http\Middleware\Binder;
use FireflyIII\Http\Middleware\EncryptCookies;
use FireflyIII\Http\Middleware\InstallationId;
use FireflyIII\Http\Middleware\Installer;
use FireflyIII\Http\Middleware\InterestingMessage;
use FireflyIII\Http\Middleware\IsAdmin;
use FireflyIII\Http\Middleware\Range;
use FireflyIII\Http\Middleware\RedirectIfAuthenticated;
use FireflyIII\Http\Middleware\SecureHeaders;
use FireflyIII\Http\Middleware\StartFireflySession;
use FireflyIII\Http\Middleware\TrimStrings;
use FireflyIII\Http\Middleware\TrustProxies;
use FireflyIII\Http\Middleware\VerifyCsrfToken;
use Illuminate\Auth\Middleware\AuthenticateWithBasicAuth;
use Illuminate\Auth\Middleware\Authorize;
use Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse;
use Illuminate\Foundation\Http\Kernel as HttpKernel;
use Illuminate\Foundation\Http\Middleware\CheckForMaintenanceMode;
use Illuminate\Foundation\Http\Middleware\ConvertEmptyStringsToNull;
use Illuminate\Foundation\Http\Middleware\ValidatePostSize;
use Illuminate\Routing\Middleware\ThrottleRequests;
use Illuminate\Session\Middleware\AuthenticateSession;
use Illuminate\View\Middleware\ShareErrorsFromSession;
use Laravel\Passport\Http\Middleware\CreateFreshApiToken;
use Laravel\Sanctum\Http\Middleware\EnsureFrontendRequestsAreStateful;
use PragmaRX\Google2FALaravel\Middleware as MFAMiddleware;
/**
* Class Kernel
@@ -58,7 +45,7 @@ use PragmaRX\Google2FALaravel\Middleware as MFAMiddleware;
class Kernel extends HttpKernel
{
protected $middleware = [
SecureHeaders::class,
// SecureHeaders::class,
CheckForMaintenanceMode::class,
ValidatePostSize::class,
TrimStrings::class,
@@ -74,102 +61,5 @@ class Kernel extends HttpKernel
'guest' => RedirectIfAuthenticated::class,
'throttle' => ThrottleRequests::class,
];
protected $middlewareGroups = [
// does not check login
// does not check 2fa
// does not check activation
'web' => [
EncryptCookies::class,
AddQueuedCookiesToResponse::class,
StartFireflySession::class,
ShareErrorsFromSession::class,
VerifyCsrfToken::class,
AuthenticateSession::class,
CreateFreshApiToken::class,
],
// only the basic variable binders.
'binders-only' => [Installer::class, EncryptCookies::class, AddQueuedCookiesToResponse::class, Binder::class],
// MUST NOT be logged in. Does not care about 2FA or confirmation.
'user-not-logged-in' => [
Installer::class,
EncryptCookies::class,
AddQueuedCookiesToResponse::class,
StartFireflySession::class,
ShareErrorsFromSession::class,
VerifyCsrfToken::class,
Binder::class,
RedirectIfAuthenticated::class,
],
// MUST be logged in.
// MUST NOT have 2FA
// don't care about confirmation:
'user-logged-in-no-2fa' => [
Installer::class,
EncryptCookies::class,
AddQueuedCookiesToResponse::class,
StartFireflySession::class,
ShareErrorsFromSession::class,
VerifyCsrfToken::class,
Binder::class,
Authenticate::class,
// RedirectIfTwoFactorAuthenticated::class,
],
// MUST be logged in
// don't care about 2fa
// don't care about confirmation.
'user-simple-auth' => [
EncryptCookies::class,
AddQueuedCookiesToResponse::class,
StartFireflySession::class,
ShareErrorsFromSession::class,
VerifyCsrfToken::class,
Binder::class,
Authenticate::class,
],
// MUST be logged in
// MUST have 2fa
// MUST be confirmed.
// (this group includes the other Firefly III middleware)
'user-full-auth' => [
EncryptCookies::class,
AddQueuedCookiesToResponse::class,
StartFireflySession::class,
ShareErrorsFromSession::class,
VerifyCsrfToken::class,
Authenticate::class,
MFAMiddleware::class,
Range::class,
Binder::class,
InterestingMessage::class,
CreateFreshApiToken::class,
],
// MUST be logged in
// MUST have 2fa
// MUST be confirmed.
// MUST have owner role
// (this group includes the other Firefly III middleware)
'admin' => [
EncryptCookies::class,
AddQueuedCookiesToResponse::class,
StartFireflySession::class,
ShareErrorsFromSession::class,
VerifyCsrfToken::class,
Authenticate::class,
// AuthenticateTwoFactor::class,
IsAdmin::class,
Range::class,
Binder::class,
CreateFreshApiToken::class,
],
// full API authentication
'api' => [AcceptHeaders::class, EnsureFrontendRequestsAreStateful::class, 'auth:api,sanctum', 'bindings'],
// do only bindings, no auth
'api_basic' => [AcceptHeaders::class, 'bindings'],
];
protected $middlewarePriority = [StartFireflySession::class, ShareErrorsFromSession::class, Authenticate::class, Binder::class, Authorize::class];
}

View File

@@ -75,7 +75,7 @@ class Binder
*
* @return mixed
*/
private function performBinding(string $key, string $value, Route $route)
private function performBinding(string $key, object|string $value, Route $route)
{
$class = $this->binders[$key];

View File

@@ -45,6 +45,6 @@ class TrustProxies extends Middleware
*/
public function __construct()
{
$this->proxies = (string) config('firefly.trusted_proxies');
$this->proxies = (string) config('trustedproxy.proxies');
}
}

View File

@@ -41,14 +41,15 @@ class ConfigurationRequest extends FormRequest
public function getConfigurationData(): array
{
return [
'single_user_mode' => $this->boolean('single_user_mode'),
'enable_exchange_rates' => $this->boolean('enable_exchange_rates'),
'use_running_balance' => $this->boolean('use_running_balance'),
'enable_external_map' => $this->boolean('enable_external_map'),
'enable_external_rates' => $this->boolean('enable_external_rates'),
'allow_webhooks' => $this->boolean('allow_webhooks'),
'valid_url_protocols' => $this->string('valid_url_protocols'),
'is_demo_site' => $this->boolean('is_demo_site'),
'single_user_mode' => $this->boolean('single_user_mode'),
'enable_exchange_rates' => $this->boolean('enable_exchange_rates'),
'use_running_balance' => $this->boolean('use_running_balance'),
'enable_external_map' => $this->boolean('enable_external_map'),
'enable_external_rates' => $this->boolean('enable_external_rates'),
'allow_webhooks' => $this->boolean('allow_webhooks'),
'valid_url_protocols' => $this->string('valid_url_protocols'),
'is_demo_site' => $this->boolean('is_demo_site'),
'enable_batch_processing' => $this->boolean('enable_batch_processing'),
];
}
@@ -59,14 +60,15 @@ class ConfigurationRequest extends FormRequest
{
// fixed
return [
'single_user_mode' => 'min:0|max:1|numeric',
'enable_exchange_rates' => 'min:0|max:1|numeric',
'use_running_balance' => 'min:0|max:1|numeric',
'enable_external_map' => 'min:0|max:1|numeric',
'enable_external_rates' => 'min:0|max:1|numeric',
'allow_webhooks' => 'min:0|max:1|numeric',
'valid_url_protocols' => 'min:0|max:255',
'is_demo_site' => 'min:0|max:1|numeric',
'single_user_mode' => 'min:0|max:1|numeric',
'enable_exchange_rates' => 'min:0|max:1|numeric',
'use_running_balance' => 'min:0|max:1|numeric',
'enable_external_map' => 'min:0|max:1|numeric',
'enable_external_rates' => 'min:0|max:1|numeric',
'allow_webhooks' => 'min:0|max:1|numeric',
'enable_batch_processing' => 'min:0|max:1|numeric',
'valid_url_protocols' => 'min:0|max:255',
'is_demo_site' => 'min:0|max:1|numeric',
];
}

View File

@@ -25,8 +25,9 @@ declare(strict_types=1);
namespace FireflyIII\Jobs;
use Carbon\Carbon;
use FireflyIII\Events\Model\TransactionGroup\CreatedSingleTransactionGroup;
use FireflyIII\Events\Model\TransactionGroup\TransactionGroupEventFlags;
use FireflyIII\Events\Model\TransactionGroup\TransactionGroupsRequestedReporting;
use FireflyIII\Events\StoredTransactionGroup;
use FireflyIII\Exceptions\DuplicateTransactionException;
use FireflyIII\Exceptions\FireflyException;
use FireflyIII\Models\Recurrence;
@@ -383,7 +384,10 @@ class CreateRecurringTransactions implements ShouldQueue
Log::info(sprintf('Created new transaction group #%d', $group->id));
// trigger event:
event(new StoredTransactionGroup($group, $recurrence->apply_rules, true));
$flags = new TransactionGroupEventFlags();
$flags->applyRules = $recurrence->apply_rules;
event(new CreatedSingleTransactionGroup($group, $flags));
// event(new StoredTransactionGroup($group, $recurrence->apply_rules, true));
$this->groups->push($group);
// update recurring thing:

View File

@@ -31,10 +31,11 @@ use FireflyIII\Notifications\User\TransactionCreation;
use FireflyIII\Repositories\User\UserRepositoryInterface;
use FireflyIII\Support\Facades\Preferences;
use FireflyIII\Transformers\TransactionGroupTransformer;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Notification;
class MailsNewTransactionsReport
class MailsNewTransactionsReport implements ShouldQueue
{
public function handle(TransactionGroupsRequestedReporting $event): void
{

View File

@@ -0,0 +1,198 @@
<?php
declare(strict_types=1);
/*
* ProcessesNewTransactionGroup.php
* Copyright (c) 2026 james@firefly-iii.org
*
* This file is part of Firefly III (https://github.com/firefly-iii).
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
namespace FireflyIII\Listeners\Model\TransactionGroup;
use Carbon\Carbon;
use FireflyIII\Enums\WebhookTrigger;
use FireflyIII\Events\Model\TransactionGroup\CreatedSingleTransactionGroup;
use FireflyIII\Events\Model\Webhook\WebhookMessagesRequestSending;
use FireflyIII\Generator\Webhook\MessageGeneratorInterface;
use FireflyIII\Models\Account;
use FireflyIII\Models\TransactionGroup;
use FireflyIII\Models\TransactionJournalMeta;
use FireflyIII\Repositories\Journal\JournalRepositoryInterface;
use FireflyIII\Repositories\PeriodStatistic\PeriodStatisticRepositoryInterface;
use FireflyIII\Repositories\RuleGroup\RuleGroupRepositoryInterface;
use FireflyIII\Services\Internal\Support\CreditRecalculateService;
use FireflyIII\Support\Facades\FireflyConfig;
use FireflyIII\Support\Models\AccountBalanceCalculator;
use FireflyIII\TransactionRules\Engine\RuleEngineInterface;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Log;
class ProcessesNewTransactionGroup implements ShouldQueue
{
public function handle(CreatedSingleTransactionGroup $event): void
{
Log::debug(sprintf('In ProcessesNewTransactionGroup::handle(#%d)', $event->transactionGroup->id));
$setting = FireflyConfig::get('enable_batch_processing', false)->data;
if (true === $event->flags->batchSubmission && true === $setting) {
Log::debug(sprintf(
'Will do nothing for group #%d because it is part of a batch (setting:%s).',
$event->transactionGroup->id,
var_export($setting, true)
));
return;
}
Log::debug(sprintf('Will join group #%d with all other open transaction groups and process them.', $event->transactionGroup->id));
$collection = $event->transactionGroup->transactionJournals;
$repository = app(JournalRepositoryInterface::class);
$set = $collection->merge($repository->getUncompletedJournals());
if (0 === $set->count()) {
Log::debug('Set is empty, never mind.');
return;
}
if (!$event->flags->applyRules) {
Log::debug(sprintf('Will NOT process rules for %d journal(s)', $set->count()));
}
if (!$event->flags->recalculateCredit) {
Log::debug(sprintf('Will NOT recalculate credit for %d journal(s)', $set->count()));
}
if (!$event->flags->fireWebhooks) {
Log::debug(sprintf('Will NOT fire webhooks for %d journal(s)', $set->count()));
}
if ($event->flags->applyRules) {
$this->processRules($set);
}
if ($event->flags->recalculateCredit) {
$this->recalculateCredit($set);
}
if ($event->flags->fireWebhooks) {
$this->fireWebhooks($set);
}
// always remove old statistics.
$this->removePeriodStatistics($set);
// recalculate running balance if necessary.
Log::debug('Observe "created" of a transaction.');
if (true === FireflyConfig::get('use_running_balance', config('firefly.feature_flags.running_balance_column'))->data) {
$this->recalculateRunningBalance($set);
}
$repository->markAsCompleted($set);
}
private function recalculateRunningBalance(Collection $set): void
{
Log::debug('Now in recalculateRunningBalance');
// find the earliest date in the set, based on date and _internal_previous_date
$earliest = $set->pluck('date')->sort()->first();
$entries = TransactionJournalMeta::whereIn('transaction_journal_id', $set->pluck('id')->toArray())->where('name', '_internal_previous_date')->get([
'journal_meta.*',
]);
$array = $entries->toArray();
if (count($array) > 0) {
usort($array, function (array $a, array $b) {
return Carbon::parse($a['data'])->gt(Carbon::parse($b['data']));
});
/** @var Carbon $date */
$date = Carbon::parse($array[0]['data']);
$earliest = $date->lt($earliest) ? $date : $earliest;
}
// get accounts
$accounts = Account::leftJoin('transactions', 'transactions.account_id', 'accounts.id')
->leftJoin('transaction_journals', 'transaction_journals.id', 'transactions.transaction_journal_id')
->leftJoin('account_types', 'account_types.id', 'accounts.account_type_id')
->whereIn('transaction_journals.id', $set->pluck('id')->toArray())
->get(['accounts.*'])
;
AccountBalanceCalculator::optimizedCalculation($accounts, $earliest);
}
private function removePeriodStatistics(Collection $set): void
{
Log::debug('Always remove period statistics');
/** @var PeriodStatisticRepositoryInterface $repository */
$repository = app(PeriodStatisticRepositoryInterface::class);
$repository->deleteStatisticsForCollection($set);
}
private function fireWebhooks(Collection $set): void
{
// collect transaction groups by set ids.
$groups = TransactionGroup::whereIn('id', array_unique($set->pluck('transaction_group_id')->toArray()))->get();
Log::debug(__METHOD__);
$user = $set->first()->user;
/** @var MessageGeneratorInterface $engine */
$engine = app(MessageGeneratorInterface::class);
$engine->setUser($user);
// tell the generator which trigger it should look for
$engine->setTrigger(WebhookTrigger::STORE_TRANSACTION);
// tell the generator which objects to process
$engine->setObjects($groups);
// tell the generator to generate the messages
$engine->generateMessages();
// trigger event to send them:
Log::debug(sprintf('send event WebhookMessagesRequestSending from %s', __METHOD__));
event(new WebhookMessagesRequestSending());
}
private function recalculateCredit(Collection $set): void
{
Log::debug(sprintf('Will now recalculateCredit for %d journal(s)', $set->count()));
/** @var CreditRecalculateService $object */
$object = app(CreditRecalculateService::class);
$object->setJournals($set);
$object->recalculate();
}
private function processRules(Collection $set): void
{
Log::debug(sprintf('Will now processRules for %d journal(s)', $set->count()));
$array = $set->pluck('id')->toArray();
$journalIds = implode(',', $array);
$user = $set->first()->user;
Log::debug(sprintf('Add local operator for journal(s): %s', $journalIds));
// collect rules:
$ruleGroupRepository = app(RuleGroupRepositoryInterface::class);
$ruleGroupRepository->setUser($user);
// add the groups to the rule engine.
// it should run the rules in the group and cancel the group if necessary.
Log::debug('Fire processRules with ALL store-journal rule groups.');
$groups = $ruleGroupRepository->getRuleGroupsWithRules('store-journal');
// create and fire rule engine.
$newRuleEngine = app(RuleEngineInterface::class);
$newRuleEngine->setUser($user);
$newRuleEngine->addOperator(['type' => 'journal_id', 'value' => $journalIds]);
$newRuleEngine->setRuleGroups($groups);
$newRuleEngine->fire();
}
}

View File

@@ -27,9 +27,10 @@ namespace FireflyIII\Listeners\Model\TransactionGroup;
use Carbon\Carbon;
use FireflyIII\Events\Model\TransactionGroup\TransactionGroupRequestsAuditLogEntry;
use FireflyIII\Repositories\AuditLogEntry\ALERepositoryInterface;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Support\Facades\Log;
class StoresAuditLogEntry
class StoresAuditLogEntry implements ShouldQueue
{
public function handle(TransactionGroupRequestsAuditLogEntry $event): void
{

View File

@@ -28,9 +28,10 @@ use FireflyIII\Events\Model\Webhook\WebhookMessagesRequestSending;
use FireflyIII\Jobs\SendWebhookMessage;
use FireflyIII\Models\WebhookMessage;
use FireflyIII\Support\Facades\FireflyConfig;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Support\Facades\Log;
class SendsWebhookMessages
class SendsWebhookMessages implements ShouldQueue
{
public function handle(WebhookMessagesRequestSending $event): void
{

View File

@@ -31,11 +31,12 @@ use FireflyIII\Helpers\Update\UpdateTrait;
use FireflyIII\Models\Configuration;
use FireflyIII\Repositories\User\UserRepositoryInterface;
use FireflyIII\Support\Facades\FireflyConfig;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Support\Facades\Log;
use Psr\Container\ContainerExceptionInterface;
use Psr\Container\NotFoundExceptionInterface;
class ChecksForNewVersion
class ChecksForNewVersion implements ShouldQueue
{
use UpdateTrait;

View File

@@ -38,10 +38,11 @@ use FireflyIII\Notifications\User\UserRegistration as UserRegistrationNotificati
use FireflyIII\Repositories\User\UserRepositoryInterface;
use FireflyIII\Support\Facades\FireflyConfig;
use FireflyIII\User;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Notification;
class HandlesNewUserRegistration
class HandlesNewUserRegistration implements ShouldQueue
{
public function handle(NewUserRegistered $event): void
{

View File

@@ -1,8 +1,10 @@
<?php
/**
* AdminEventHandler.php
* Copyright (c) 2019 james@firefly-iii.org
declare(strict_types=1);
/*
* NotifiesAboutNewInvitation.php
* Copyright (c) 2026 james@firefly-iii.org
*
* This file is part of Firefly III (https://github.com/firefly-iii).
*
@@ -19,24 +21,47 @@
* 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\Handlers\Events;
namespace FireflyIII\Listeners\Security\System;
use Exception;
use FireflyIII\Events\Admin\InvitationCreated;
use FireflyIII\Events\Security\System\NewInvitationCreated;
use FireflyIII\Exceptions\FireflyException;
use FireflyIII\Mail\InvitationMail;
use FireflyIII\Models\InvitedUser;
use FireflyIII\Notifications\Admin\UserInvitation;
use FireflyIII\Notifications\Notifiables\OwnerNotifiable;
use FireflyIII\Support\Facades\FireflyConfig;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Mail;
use Illuminate\Support\Facades\Notification;
/**
* Class AdminEventHandler.
*/
class AdminEventHandler
class NotifiesAboutNewInvitation implements ShouldQueue
{
public function sendInvitationNotification(InvitationCreated $event): void
public function handle(NewInvitationCreated $event): void
{
$this->sendInvitationNotification($event->invitee);
$this->sendRegistrationInvite($event->invitee);
}
private function sendRegistrationInvite(InvitedUser $invitee): void
{
$email = $invitee->email;
$admin = $invitee->user->email;
$url = route('invite', [$invitee->invite_code]);
try {
Mail::to($email)->send(new InvitationMail($invitee, $admin, $url));
} catch (Exception $e) {
Log::error($e->getMessage());
Log::error($e->getTraceAsString());
throw new FireflyException($e->getMessage(), 0, $e);
}
}
private function sendInvitationNotification(InvitedUser $invitee): void
{
$sendMail = FireflyConfig::get('notification_invite_created', true)->data;
if (false === $sendMail) {
@@ -44,7 +69,7 @@ class AdminEventHandler
}
try {
Notification::send(new OwnerNotifiable(), new UserInvitation($event->invitee));
Notification::send(new OwnerNotifiable(), new UserInvitation($invitee));
} catch (Exception $e) {
$message = $e->getMessage();
if (str_contains($message, 'Bcc')) {
@@ -61,6 +86,4 @@ class AdminEventHandler
Log::error($e->getTraceAsString());
}
}
// Send new version message to admin.
}

View File

@@ -29,10 +29,11 @@ use FireflyIII\Events\Security\System\SystemFoundNewVersionOnline;
use FireflyIII\Notifications\Admin\VersionCheckResult;
use FireflyIII\Notifications\Notifiables\OwnerNotifiable;
use FireflyIII\Support\Facades\FireflyConfig;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Notification;
class NotifiesOwnerAboutNewVersion
class NotifiesOwnerAboutNewVersion implements ShouldQueue
{
public function handle(SystemFoundNewVersionOnline $event): void
{

View File

@@ -28,10 +28,11 @@ use Exception;
use FireflyIII\Events\Security\System\UnknownUserTriedLogin;
use FireflyIII\Notifications\Admin\UnknownUserLoginAttempt;
use FireflyIII\Notifications\Notifiables\OwnerNotifiable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Notification;
class NotifiesOwnerAboutUnknownUser
class NotifiesOwnerAboutUnknownUser implements ShouldQueue
{
public function handle(UnknownUserTriedLogin $event): void
{

View File

@@ -30,10 +30,11 @@ use FireflyIII\Exceptions\FireflyException;
use FireflyIII\Mail\ConfirmEmailChangeMail;
use FireflyIII\Mail\UndoEmailChangeMail;
use FireflyIII\Support\Facades\Preferences;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Mail;
class HandlesChangeOfUserEmailAddress
class HandlesChangeOfUserEmailAddress implements ShouldQueue
{
public function handle(UserChangedEmailAddress $event): void
{

View File

@@ -27,10 +27,11 @@ namespace FireflyIII\Listeners\Security\User;
use Exception;
use FireflyIII\Events\Security\User\UserHasEnabledMFA;
use FireflyIII\Notifications\Security\EnabledMFANotification;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Notification;
class NotifiesUserAboutEnabledMFA
class NotifiesUserAboutEnabledMFA implements ShouldQueue
{
public function handle(UserHasEnabledMFA $event): void
{

View File

@@ -26,10 +26,11 @@ namespace FireflyIII\Listeners\Security\User;
use Exception;
use FireflyIII\Events\Security\User\UserFailedLoginAttempt;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Notification;
class NotifiesUserAboutFailedLogin
class NotifiesUserAboutFailedLogin implements ShouldQueue
{
public function handle(UserFailedLoginAttempt $event): void
{

View File

@@ -27,10 +27,11 @@ namespace FireflyIII\Listeners\Security\User;
use Exception;
use FireflyIII\Events\Security\User\UserHasFewMFABackupCodesLeft;
use FireflyIII\Notifications\Security\MFABackupFewLeftNotification;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Notification;
class NotifiesUserAboutFewCodesLeft
class NotifiesUserAboutFewCodesLeft implements ShouldQueue
{
public function handle(UserHasFewMFABackupCodesLeft $event): void
{

View File

@@ -27,10 +27,11 @@ namespace FireflyIII\Listeners\Security\User;
use Exception;
use FireflyIII\Events\Security\User\UserHasGeneratedNewBackupCodes;
use FireflyIII\Notifications\Security\NewBackupCodesNotification;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Notification;
class NotifiesUserAboutNewBackupCodes
class NotifiesUserAboutNewBackupCodes implements ShouldQueue
{
public function handle(UserHasGeneratedNewBackupCodes $event): void
{

View File

@@ -28,10 +28,11 @@ use Exception;
use FireflyIII\Events\Security\User\UserLoggedInFromNewIpAddress;
use FireflyIII\Notifications\User\UserLogin;
use FireflyIII\Support\Facades\Preferences;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Notification;
class NotifiesUserAboutNewIpAddress
class NotifiesUserAboutNewIpAddress implements ShouldQueue
{
public function handle(UserLoggedInFromNewIpAddress $event): void
{

View File

@@ -27,10 +27,11 @@ namespace FireflyIII\Listeners\Security\User;
use Exception;
use FireflyIII\Events\Security\User\UserHasNoMFABackupCodesLeft;
use FireflyIII\Notifications\Security\MFABackupNoLeftNotification;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Notification;
class NotifiesUserAboutNoCodesLeft
class NotifiesUserAboutNoCodesLeft implements ShouldQueue
{
public function handle(UserHasNoMFABackupCodesLeft $event): void
{

View File

@@ -27,10 +27,11 @@ namespace FireflyIII\Listeners\Security\User;
use Exception;
use FireflyIII\Events\Security\User\UserKeepsFailingMFA;
use FireflyIII\Notifications\Security\MFAManyFailedAttemptsNotification;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Notification;
class NotifiesUserAboutRepeatedMFAFailures
class NotifiesUserAboutRepeatedMFAFailures implements ShouldQueue
{
public function handle(UserKeepsFailingMFA $event): void
{

View File

@@ -27,10 +27,11 @@ namespace FireflyIII\Listeners\Security\User;
use Exception;
use FireflyIII\Events\Security\User\UserHasUsedBackupCode;
use FireflyIII\Notifications\Security\MFAUsedBackupCodeNotification;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Notification;
class NotifiesUserAboutUsedBackupCode
class NotifiesUserAboutUsedBackupCode implements ShouldQueue
{
public function handle(UserHasUsedBackupCode $event): void
{

View File

@@ -1,8 +1,10 @@
<?php
/**
* UserEventHandler.php
* Copyright (c) 2019 james@firefly-iii.org
declare(strict_types=1);
/*
* RespondsToNewLogin.php
* Copyright (c) 2026 james@firefly-iii.org
*
* This file is part of Firefly III (https://github.com/firefly-iii).
*
@@ -19,43 +21,40 @@
* 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\Handlers\Events;
namespace FireflyIII\Listeners\Security\User;
use Exception;
use FireflyIII\Events\Admin\InvitationCreated;
use FireflyIII\Exceptions\FireflyException;
use FireflyIII\Mail\InvitationMail;
use FireflyIII\Repositories\User\UserRepositoryInterface;
use FireflyIII\Support\Facades\Preferences;
use FireflyIII\User;
use Illuminate\Auth\Events\Login;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Mail;
use InvalidArgumentException;
/**
* Class UserEventHandler.
*
* This class responds to any events that have anything to do with the User object.
*
* The method name reflects what is being done. This is in the present tense.
*/
class UserEventHandler
class RespondsToNewLogin implements ShouldQueue
{
public function handle(Login $event): void
{
$user = $event->user;
if (!$user instanceof User) {
throw new InvalidArgumentException(sprintf('User cannot be an instance of %s.', get_class($user)));
}
$this->checkSingleUserIsAdmin($user);
$this->demoUserBackToEnglish($user);
}
/**
* Fires to see if a user is admin.
*/
public function checkSingleUserIsAdmin(Login $event): void
private function checkSingleUserIsAdmin(User $user): void
{
/** @var UserRepositoryInterface $repository */
$repository = app(UserRepositoryInterface::class);
/** @var User $user */
$user = $event->user;
$count = $repository->count();
// only act when there is 1 user in the system and he has no admin rights.
// only act when there is 1 user in the system, and he has no admin rights.
if (1 === $count && !$repository->hasRole($user, 'owner')) {
// user is the only user but does not have role "owner".
$role = $repository->getRole('owner');
@@ -74,13 +73,10 @@ class UserEventHandler
/**
* Set the demo user back to English.
*/
public function demoUserBackToEnglish(Login $event): void
private function demoUserBackToEnglish(User $user): void
{
/** @var UserRepositoryInterface $repository */
$repository = app(UserRepositoryInterface::class);
/** @var User $user */
$user = $event->user;
if ($repository->hasRole($user, 'demo')) {
// set user back to English.
Preferences::setForUser($user, 'language', 'en_US');
@@ -89,23 +85,4 @@ class UserEventHandler
Preferences::mark();
}
}
/**
* @throws FireflyException
*/
public function sendRegistrationInvite(InvitationCreated $event): void
{
$invitee = $event->invitee->email;
$admin = $event->invitee->user->email;
$url = route('invite', [$event->invitee->invite_code]);
try {
Mail::to($invitee)->send(new InvitationMail($invitee, $admin, $url));
} catch (Exception $e) {
Log::error($e->getMessage());
Log::error($e->getTraceAsString());
throw new FireflyException($e->getMessage(), 0, $e);
}
}
}

View File

@@ -27,10 +27,11 @@ namespace FireflyIII\Listeners\Security\User;
use Exception;
use FireflyIII\Events\Security\User\UserRequestedNewPassword;
use FireflyIII\Notifications\User\UserNewPassword;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Notification;
class SendsUserNewPassword
class SendsUserNewPassword implements ShouldQueue
{
public function handle(UserRequestedNewPassword $event): void
{

View File

@@ -28,9 +28,10 @@ use Carbon\Carbon;
use FireflyIII\Events\Security\User\UserLoggedInFromNewIpAddress;
use FireflyIII\Events\Security\User\UserSuccessfullyLoggedIn;
use FireflyIII\Support\Facades\Preferences;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Support\Facades\Log;
class StoresNewIpAddress
class StoresNewIpAddress implements ShouldQueue
{
public function handle(UserSuccessfullyLoggedIn $event): void
{

View File

@@ -60,8 +60,11 @@ class Account extends Model
*
* @throws NotFoundHttpException
*/
public static function routeBinder(string $value): self
public static function routeBinder(self|string $value): self
{
if ($value instanceof self) {
$value = (int) $value->id;
}
if (auth()->check()) {
$accountId = (int) $value;

View File

@@ -62,8 +62,11 @@ class Attachment extends Model
*
* @throws NotFoundHttpException
*/
public static function routeBinder(string $value): self
public static function routeBinder(self|string $value): self
{
if ($value instanceof self) {
$value = (int) $value->id;
}
if (auth()->check()) {
$attachmentId = (int) $value;

View File

@@ -59,8 +59,11 @@ class AvailableBudget extends Model
*
* @throws NotFoundHttpException
*/
public static function routeBinder(string $value): self
public static function routeBinder(self|string $value): self
{
if ($value instanceof self) {
$value = (int) $value->id;
}
if (auth()->check()) {
$availableBudgetId = (int) $value;

View File

@@ -74,8 +74,11 @@ class Bill extends Model
*
* @throws NotFoundHttpException
*/
public static function routeBinder(string $value): self
public static function routeBinder(self|string $value): self
{
if ($value instanceof self) {
$value = (int) $value->id;
}
if (auth()->check()) {
$billId = (int) $value;

View File

@@ -53,8 +53,11 @@ class Budget extends Model
*
* @throws NotFoundHttpException
*/
public static function routeBinder(string $value): self
public static function routeBinder(self|string $value): self
{
if ($value instanceof self) {
$value = (int) $value->id;
}
if (auth()->check()) {
$budgetId = (int) $value;

View File

@@ -53,8 +53,11 @@ class BudgetLimit extends Model
*
* @throws NotFoundHttpException
*/
public static function routeBinder(string $value): self
public static function routeBinder(self|string $value): self
{
if ($value instanceof self) {
$value = (int) $value->id;
}
if (auth()->check()) {
$budgetLimitId = (int) $value;
$budgetLimit = self::where('budget_limits.id', $budgetLimitId)

View File

@@ -52,8 +52,11 @@ class Category extends Model
*
* @throws NotFoundHttpException
*/
public static function routeBinder(string $value): self
public static function routeBinder(self|string $value): self
{
if ($value instanceof self) {
$value = (int) $value->id;
}
if (auth()->check()) {
$categoryId = (int) $value;

View File

@@ -42,8 +42,11 @@ class InvitedUser extends Model
/**
* Route binder. Converts the key in the URL to the specified object (or throw 404).
*/
public static function routeBinder(string $value): self
public static function routeBinder(self|string $value): self
{
if ($value instanceof self) {
$value = (int) $value->id;
}
if (auth()->check()) {
$attemptId = (int) $value;

View File

@@ -41,8 +41,11 @@ class LinkType extends Model
*
* @throws NotFoundHttpException
*/
public static function routeBinder(string $value): self
public static function routeBinder(self|string $value): self
{
if ($value instanceof self) {
$value = (int) $value->id;
}
if (auth()->check()) {
$linkTypeId = (int) $value;
$linkType = self::find($linkTypeId);

View File

@@ -45,8 +45,11 @@ class ObjectGroup extends Model
*
* @throws NotFoundHttpException
*/
public static function routeBinder(string $value): self
public static function routeBinder(self|string $value): self
{
if ($value instanceof self) {
$value = (int) $value->id;
}
if (auth()->check()) {
$objectGroupId = (int) $value;

View File

@@ -60,8 +60,11 @@ class PiggyBank extends Model
*
* @throws NotFoundHttpException
*/
public static function routeBinder(string $value): self
public static function routeBinder(self|string $value): self
{
if ($value instanceof self) {
$value = (int) $value->id;
}
if (auth()->check()) {
$piggyBankId = (int) $value;
$piggyBank = self::where('piggy_banks.id', $piggyBankId)

View File

@@ -42,8 +42,11 @@ class Preference extends Model
*
* @throws NotFoundHttpException
*/
public static function routeBinder(string $value): self
public static function routeBinder(self|string $value): self
{
if ($value instanceof self) {
$value = (int) $value->id;
}
if (auth()->check()) {
/** @var User $user */
$user = auth()->user();

View File

@@ -74,8 +74,11 @@ class Recurrence extends Model
*
* @throws NotFoundHttpException
*/
public static function routeBinder(string $value): self
public static function routeBinder(self|string $value): self
{
if ($value instanceof self) {
$value = (int) $value->id;
}
if (auth()->check()) {
$recurrenceId = (int) $value;

View File

@@ -52,8 +52,11 @@ class Rule extends Model
*
* @throws NotFoundHttpException
*/
public static function routeBinder(string $value): self
public static function routeBinder(self|string $value): self
{
if ($value instanceof self) {
$value = (int) $value->id;
}
if (auth()->check()) {
$ruleId = (int) $value;

View File

@@ -54,8 +54,11 @@ class RuleGroup extends Model
*
* @throws NotFoundHttpException
*/
public static function routeBinder(string $value): self
public static function routeBinder(self|string $value): self
{
if ($value instanceof self) {
$value = (int) $value->id;
}
if (auth()->check()) {
$ruleGroupId = (int) $value;

View File

@@ -52,8 +52,11 @@ class Tag extends Model
*
* @throws NotFoundHttpException
*/
public static function routeBinder(string $value): self
public static function routeBinder(self|string $value): self
{
if ($value instanceof self) {
$value = (int) $value->id;
}
if (auth()->check()) {
$tagId = (int) $value;

View File

@@ -48,8 +48,11 @@ class TransactionCurrency extends Model
*
* @throws NotFoundHttpException
*/
public static function routeBinder(string $value): self
public static function routeBinder(self|string $value): self
{
if ($value instanceof self) {
$value = (int) $value->id;
}
if (auth()->check()) {
$currencyId = (int) $value;
$currency = self::find($currencyId);

View File

@@ -55,8 +55,11 @@ class TransactionGroup extends Model
*
* @throws NotFoundHttpException
*/
public static function routeBinder(string $value): self
public static function routeBinder(self|string $value): self
{
if ($value instanceof self) {
$value = (int) $value->id;
}
Log::debug(sprintf('Now in %s("%s")', __METHOD__, $value));
if (auth()->check()) {
$groupId = (int) $value;

View File

@@ -79,8 +79,11 @@ class TransactionJournal extends Model
*
* @throws NotFoundHttpException
*/
public static function routeBinder(string $value): self
public static function routeBinder(self|string $value): self
{
if ($value instanceof self) {
$value = (int) $value->id;
}
if (auth()->check()) {
$journalId = (int) $value;

View File

@@ -41,8 +41,12 @@ class TransactionJournalLink extends Model
*
* @throws NotFoundHttpException
*/
public static function routeBinder(string $value): self
public static function routeBinder(self|string $value): self
{
if ($value instanceof self) {
$value = (int) $value->id;
}
if (auth()->check()) {
$linkId = (int) $value;
$link = self::where('journal_links.id', $linkId)

View File

@@ -72,12 +72,15 @@ class TransactionType extends Model
*
* @throws NotFoundHttpException
*/
public static function routeBinder(string $type): self
public static function routeBinder(self|string $value): self
{
if (!auth()->check()) {
throw new NotFoundHttpException();
}
$transactionType = self::where('type', ucfirst($type))->first();
if ($value instanceof self) {
$value = (string) $value->type;
}
$transactionType = self::where('type', ucfirst($value))->first();
if (null !== $transactionType) {
return $transactionType;
}

View File

@@ -44,9 +44,13 @@ class UserGroup extends Model
*
* @throws NotFoundHttpException
*/
public static function routeBinder(string $value): self
public static function routeBinder(self|string $value): self
{
if (auth()->check()) {
if ($value instanceof self) {
$value = (int) $value->id;
}
$userGroupId = (int) $value;
/** @var User $user */

View File

@@ -130,9 +130,12 @@ class Webhook extends Model
*
* @throws NotFoundHttpException
*/
public static function routeBinder(string $value): self
public static function routeBinder(self|string $value): self
{
if (auth()->check()) {
if ($value instanceof self) {
$value = (int) $value->id;
}
$webhookId = (int) $value;
/** @var User $user */

View File

@@ -42,9 +42,12 @@ class WebhookAttempt extends Model
*
* @throws NotFoundHttpException
*/
public static function routeBinder(string $value): self
public static function routeBinder(self|string $value): self
{
if (auth()->check()) {
if ($value instanceof self) {
$value = (int) $value->id;
}
$attemptId = (int) $value;
/** @var User $user */

View File

@@ -44,9 +44,12 @@ class WebhookMessage extends Model
*
* @throws NotFoundHttpException
*/
public static function routeBinder(string $value): self
public static function routeBinder(self|string $value): self
{
if (auth()->check()) {
if ($value instanceof self) {
$value = (int) $value->id;
}
$messageId = (int) $value;
/** @var User $user */

View File

@@ -58,7 +58,7 @@ class SubscriptionsOverdueReminder extends Notification
$info = [];
$count = 0;
foreach ($this->overdue as $item) {
$current = ['bill' => $item['bill']];
$current = ['bill' => $item['bill']];
$current['pay_dates'] = array_map(static fn (string $date): string => new Carbon($date)->isoFormat((string) trans(
'config.month_and_day_moment_js'
)), $item['dates']['pay_dates']);

View File

@@ -23,15 +23,12 @@ declare(strict_types=1);
namespace FireflyIII\Providers;
use FireflyIII\Events\Admin\InvitationCreated;
use FireflyIII\Events\DestroyedTransactionGroup;
use FireflyIII\Events\Model\TransactionGroup\TriggeredStoredTransactionGroup;
use FireflyIII\Events\Preferences\UserGroupChangedPrimaryCurrency;
use FireflyIII\Events\StoredAccount;
use FireflyIII\Events\StoredTransactionGroup;
use FireflyIII\Events\UpdatedAccount;
use FireflyIII\Events\UpdatedTransactionGroup;
use Illuminate\Auth\Events\Login;
use Illuminate\Foundation\Support\Providers\EventServiceProvider as ServiceProvider;
use Laravel\Passport\Events\AccessTokenCreated;
use Override;
@@ -44,31 +41,19 @@ use Override;
class EventServiceProvider extends ServiceProvider
{
protected $listen = [
Login::class => [
'FireflyIII\Handlers\Events\UserEventHandler@checkSingleUserIsAdmin',
'FireflyIII\Handlers\Events\UserEventHandler@demoUserBackToEnglish',
],
// is a User related event.
InvitationCreated::class => [
'FireflyIII\Handlers\Events\AdminEventHandler@sendInvitationNotification',
'FireflyIII\Handlers\Events\UserEventHandler@sendRegistrationInvite',
],
// is a Transaction Journal related event.
StoredTransactionGroup::class => ['FireflyIII\Handlers\Events\StoredGroupEventHandler@runAllHandlers'],
TriggeredStoredTransactionGroup::class => ['FireflyIII\Handlers\Events\StoredGroupEventHandler@triggerRulesManually'],
// StoredTransactionGroup::class => ['FireflyIII\Handlers\Events\StoredGroupEventHandler@runAllHandlers'],
// TriggeredStoredTransactionGroup::class => ['FireflyIII\Handlers\Events\StoredGroupEventHandler@triggerRulesManually'],
// is a Transaction Journal related event.
UpdatedTransactionGroup::class => ['FireflyIII\Handlers\Events\UpdatedGroupEventHandler@runAllHandlers'],
DestroyedTransactionGroup::class => ['FireflyIII\Handlers\Events\DestroyedGroupEventHandler@runAllHandlers'],
// UpdatedTransactionGroup::class => ['FireflyIII\Handlers\Events\UpdatedGroupEventHandler@runAllHandlers'],
// DestroyedTransactionGroup::class => ['FireflyIII\Handlers\Events\DestroyedGroupEventHandler@runAllHandlers'],
// API related events:
AccessTokenCreated::class => ['FireflyIII\Handlers\Events\APIEventHandler@accessTokenCreated'],
// AccessTokenCreated::class => ['FireflyIII\Handlers\Events\APIEventHandler@accessTokenCreated'],
// account related events:
StoredAccount::class => ['FireflyIII\Handlers\Events\StoredAccountEventHandler@recalculateCredit'],
UpdatedAccount::class => ['FireflyIII\Handlers\Events\UpdatedAccountEventHandler@recalculateCredit'],
// StoredAccount::class => ['FireflyIII\Handlers\Events\StoredAccountEventHandler@recalculateCredit'],
// UpdatedAccount::class => ['FireflyIII\Handlers\Events\UpdatedAccountEventHandler@recalculateCredit'],
// preferences
UserGroupChangedPrimaryCurrency::class => ['FireflyIII\Handlers\Events\PreferencesEventHandler@resetPrimaryCurrencyAmounts'],
// UserGroupChangedPrimaryCurrency::class => ['FireflyIII\Handlers\Events\PreferencesEventHandler@resetPrimaryCurrencyAmounts'],
];
/**

View File

@@ -39,6 +39,7 @@ use FireflyIII\Support\CacheProperties;
use FireflyIII\Support\Repositories\UserGroup\UserGroupInterface;
use FireflyIII\Support\Repositories\UserGroup\UserGroupTrait;
use Illuminate\Support\Collection;
use Override;
/**
* Class JournalRepository.
@@ -250,4 +251,20 @@ class JournalRepository implements JournalRepositoryInterface, UserGroupInterfac
return $journal;
}
#[Override]
public function getUncompletedJournals(): Collection
{
return $this->userGroup
->transactionJournals()
->where('completed', false)
->get(['transaction_journals.*'])
;
}
#[Override]
public function markAsCompleted(Collection $set): void
{
TransactionJournal::whereIn('id', $set->pluck('id')->toArray())->update(['completed' => true]);
}
}

View File

@@ -52,6 +52,10 @@ interface JournalRepositoryInterface
*/
public function destroyGroup(TransactionGroup $transactionGroup): void;
public function getUncompletedJournals(): Collection;
public function markAsCompleted(Collection $set): void;
/**
* Deletes a journal.
*/

View File

@@ -25,12 +25,18 @@ declare(strict_types=1);
namespace FireflyIII\Repositories\PeriodStatistic;
use Carbon\Carbon;
use FireflyIII\Models\Account;
use FireflyIII\Models\Budget;
use FireflyIII\Models\Category;
use FireflyIII\Models\PeriodStatistic;
use FireflyIII\Models\UserGroup;
use FireflyIII\Models\Tag;
use FireflyIII\Models\Transaction;
use FireflyIII\Support\Repositories\UserGroup\UserGroupInterface;
use FireflyIII\Support\Repositories\UserGroup\UserGroupTrait;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
use Override;
@@ -137,8 +143,98 @@ class PeriodStatisticRepository implements PeriodStatisticRepositoryInterface, U
}
#[Override]
public function deleteStatisticsForPrefix(UserGroup $userGroup, string $prefix, Carbon $date): void
public function deleteStatisticsForPrefix(string $prefix, Collection $dates): void
{
$userGroup->periodStatistics()->where('start', '<=', $date)->where('end', '>=', $date)->where('type', 'LIKE', sprintf('%s%%', $prefix))->delete();
$count = $this->userGroup
->periodStatistics()
->where(function (Builder $q) use ($dates): void {
foreach ($dates as $date) {
$q->where(function (Builder $q1) use ($date): void {
$q1->where('start', '<=', $date)->where('end', '>=', $date);
});
}
})
->where('type', 'LIKE', sprintf('%s%%', $prefix))
->delete()
;
Log::debug(sprintf('Deleted %d entries for prefix "%s"', $count, $prefix));
}
public function deleteStatisticsForType(string $class, Collection $objects, Collection $dates): void
{
if (0 === count($objects)) {
Log::debug(sprintf('Nothing to delete in deleteStatisticsForType("%s")', $class));
return;
}
$count = PeriodStatistic::where('primary_statable_type', $class)
->whereIn('primary_statable_id', $objects->pluck('id')->toArray())
->where(function (Builder $q) use ($dates): void {
foreach ($dates as $date) {
$q->where(function (Builder $q1) use ($date): void {
$q1->where('start', '<=', $date)->where('end', '>=', $date);
});
}
})
->delete()
;
Log::debug(sprintf('Delete %d statistics for %dx %s', $count, $objects->count(), $class));
}
#[Override]
public function deleteStatisticsForCollection(Collection $set): void
{
Log::debug(sprintf('Delete statistics for %d transaction journals.', count($set)));
// collect all transactions:
$transactions = Transaction::whereIn('transaction_journal_id', $set->pluck('id')->toArray())->get(['transactions.*']);
// collect all accounts and delete stats:
$accounts = Account::whereIn('id', $transactions->pluck('account_id')->toArray())->get(['accounts.*']);
$dates = $set->pluck('date');
$this->deleteStatisticsForType(Account::class, $accounts, $dates);
// collect all categories, and remove stats.
$categories = Category::whereIn(
'id',
DB::table('category_transaction_journal')
->whereIn('transaction_journal_id', $set->pluck('id')->toArray())
->get(['category_transaction_journal.category_id'])
->pluck('category_id')
->toArray()
)->get(['categories.*']);
$this->deleteStatisticsForType(Category::class, $categories, $dates);
// budgets, same thing
$budgets = Budget::whereIn(
'id',
DB::table('budget_transaction_journal')
->whereIn('transaction_journal_id', $set->pluck('id')->toArray())
->get(['budget_transaction_journal.budget_id'])
->pluck('budget_id')
->toArray()
)->get(['budgets.*']);
$this->deleteStatisticsForType(Budget::class, $budgets, $dates);
// tags
$tags = Tag::whereIn(
'id',
DB::table('tag_transaction_journal')
->whereIn('transaction_journal_id', $set->pluck('id')->toArray())
->get(['tag_transaction_journal.tag_id'])
->pluck('tag_id')
->toArray()
)->get(['tags.*']);
$this->deleteStatisticsForType(Tag::class, $tags, $dates);
// remove for no tag, no cat, etc.
if (0 === $categories->count()) {
$this->deleteStatisticsForPrefix('no_category', $dates);
}
if (0 === $budgets->count()) {
$this->deleteStatisticsForPrefix('no_budget', $dates);
}
if (0 === $tags->count()) {
$this->deleteStatisticsForPrefix('no_tag', $dates);
}
}
}

View File

@@ -26,12 +26,13 @@ namespace FireflyIII\Repositories\PeriodStatistic;
use Carbon\Carbon;
use FireflyIII\Models\PeriodStatistic;
use FireflyIII\Models\UserGroup;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Collection;
interface PeriodStatisticRepositoryInterface
{
public function deleteStatisticsForCollection(Collection $set);
public function findPeriodStatistics(Model $model, Carbon $start, Carbon $end, array $types): Collection;
public function findPeriodStatistic(Model $model, Carbon $start, Carbon $end, string $type): Collection;
@@ -54,5 +55,5 @@ interface PeriodStatisticRepositoryInterface
public function deleteStatisticsForModel(Model $model, Carbon $date): void;
public function deleteStatisticsForPrefix(UserGroup $userGroup, string $prefix, Carbon $date): void;
public function deleteStatisticsForPrefix(string $prefix, Collection $dates): void;
}

View File

@@ -172,7 +172,11 @@ class OperationsRepository implements OperationsRepositoryInterface, UserGroupIn
}
$listedJournals[] = $journalId;
$array[$currencyId]['tags'][$tagId] ??= ['id' => $tagId, 'name' => $tagName, 'transaction_journals' => []];
$array[$currencyId]['tags'][$tagId] ??= [
'id' => $tagId,
'name' => $tagName,
'transaction_journals' => [],
];
$journalId = (int) $journal['transaction_journal_id'];
$array[$currencyId]['tags'][$tagId]['transaction_journals'][$journalId] = [
'amount' => Steam::positive($journal['amount']),

View File

@@ -34,6 +34,7 @@ use FireflyIII\Models\TransactionGroup;
use FireflyIII\Models\TransactionJournal;
use FireflyIII\Repositories\Account\AccountRepositoryInterface;
use FireflyIII\Support\Facades\Steam;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Log;
/**
@@ -43,9 +44,15 @@ class CreditRecalculateService
{
private ?Account $account = null;
private ?TransactionGroup $group = null;
private Collection $journals;
private AccountRepositoryInterface $repository;
private array $work = [];
public function __construct()
{
$this->journals = new Collection();
}
public function recalculate(): void
{
if (true !== config('firefly.feature_flags.handle_debts')) {
@@ -58,25 +65,46 @@ class CreditRecalculateService
// work based on account.
$this->processAccount();
}
if ($this->journals->count() > 0) {
$this->processJournals();
}
if (0 === count($this->work)) {
Log::debug('No work found for recalculate() to do.');
return;
}
$this->processWork();
}
private function processGroup(): void
private function collectFromJournals(Collection $transactionJournals): void
{
/** @var TransactionJournal $journal */
foreach ($this->group->transactionJournals as $journal) {
try {
$this->findByJournal($journal);
} catch (FireflyException $e) {
Log::error($e->getTraceAsString());
Log::error(sprintf('Could not find work account for transaction group #%d.', $this->group->id));
Log::debug('Now in collectFromJournals()');
$valid = config('firefly.valid_liabilities');
$accounts = Account::leftJoin('transactions', 'transactions.account_id', 'accounts.id')
->leftJoin('transaction_journals', 'transaction_journals.id', 'transactions.transaction_journal_id')
->leftJoin('account_types', 'account_types.id', 'accounts.account_type_id')
->whereIn('transaction_journals.id', $transactionJournals->pluck('id')->toArray())
->whereIn('account_types.type', $valid)
->get(['accounts.*'])
;
if ($accounts->count() > 0) {
Log::debug(sprintf('Found %d account(s) to process.', $accounts->count()));
foreach ($accounts as $account) {
$this->work[] = $account;
}
}
}
private function processGroup(): void
{
$this->collectFromJournals($this->group->transactionJournals);
}
private function processJournals(): void
{
$this->collectFromJournals($this->journals);
}
/**
* @throws FireflyException
*/
@@ -460,4 +488,9 @@ class CreditRecalculateService
{
$this->group = $group;
}
public function setJournals(Collection $journals): void
{
$this->journals = $journals;
}
}

View File

@@ -42,6 +42,7 @@ class DynamicConfigKey
'configuration.enable_external_map', // boolean
'configuration.enable_external_rates', // boolean
'configuration.allow_webhooks', // boolean
'configuration.enable_batch_processing', // boolean
'configuration.valid_url_protocols', // string ("http,https")
];

View File

@@ -170,7 +170,7 @@ class AccountBalanceGrouped
{
$this->primary = $primary;
$primaryCurrencyId = $primary->id;
$this->currencies = [$primary->id => $primary]; // currency cache
$this->currencies = [$primary->id => $primary]; // currency cache
$this->data[$primaryCurrencyId] = [
'currency_id' => (string) $primaryCurrencyId,
'currency_symbol' => $primary->symbol,

View File

@@ -222,7 +222,14 @@ trait AugumentData
$currentEnd->addMonth();
}
// primary currency amount.
$expenses = $opsRepository->sumExpenses($currentStart, $currentEnd, null, $budgetCollection, $entry->transactionCurrency, $this->convertToPrimary);
$expenses = $opsRepository->sumExpenses(
$currentStart,
$currentEnd,
null,
$budgetCollection,
$entry->transactionCurrency,
$this->convertToPrimary
);
$spent = $expenses[$currency->id]['sum'] ?? '0';
$entry->pc_spent = $spent;

View File

@@ -157,7 +157,7 @@ trait GetConfigurationData
// previous year:
$yearBegin = today(config('app.timezone'))->subYear()->startOfYear();
$index = (string) trans('firefly.previous_year', ['year' => $yearBegin->year]);
$index = (string) trans('firefly.previous_year', ['year' => $yearBegin->year]);
$ranges[$index] = [$yearBegin, $yearBegin->clone()->endOfYear()];
// everything

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