Compare commits

...

76 Commits

Author SHA1 Message Date
github-actions[bot]
9f016aed16 Merge pull request #11643 from firefly-iii/release-1770188788
🤖 Automatically merge the PR into the develop branch.
2026-02-04 08:06:37 +01:00
JC5
27df5ea800 🤖 Auto commit for release 'develop' on 2026-02-04 2026-02-04 08:06:28 +01:00
Sander Dorigo
2d7cdd36f0 Fix null pointer 2026-02-04 08:01:47 +01:00
github-actions[bot]
7888023c1a Merge pull request #11642 from firefly-iii/release-1770187820
🤖 Automatically merge the PR into the develop branch.
2026-02-04 07:50:28 +01:00
JC5
3032118788 🤖 Auto commit for release 'develop' on 2026-02-04 2026-02-04 07:50:20 +01:00
James Cole
89d96ddc17 Remove unused events. 2026-02-04 05:48:51 +01:00
James Cole
97c9937571 Clean up events for groups. 2026-02-04 05:46:19 +01:00
James Cole
c46aca0594 Clean up more events. 2026-02-03 21:04:07 +01:00
James Cole
0b33e1ff09 Refactor recalculation service. 2026-02-03 20:43:52 +01:00
James Cole
d267d2a0b0 Remove unused event handler. 2026-02-03 20:29:58 +01:00
James Cole
f2996dcebe Clean up event handlers and other code. 2026-02-03 20:24:16 +01:00
James Cole
ebb6a186cc Add some event handlers. 2026-02-03 18:58:24 +01:00
James Cole
304fae439a Expand listeners and observers. 2026-02-03 05:42:50 +01:00
James Cole
9ca81cf305 Clean up observer. 2026-02-02 20:15:06 +01:00
James Cole
cad5fb6d6b Clean up budget limit observer. 2026-02-02 20:13:00 +01:00
James Cole
53efafdbb2 Rename observers 2026-02-02 20:09:23 +01:00
James Cole
7922017288 Rename observers 2026-02-02 20:08:37 +01:00
James Cole
4e910a33dd Clean up events. 2026-02-02 20:02:45 +01:00
James Cole
bb031cdeb6 Clean up simple observers. 2026-02-02 19:59:05 +01:00
James Cole
60026bbcba Fix convert to primary amount. 2026-02-02 19:55:18 +01:00
James Cole
e96a1850da Merge branch 'develop' of github.com:firefly-iii/firefly-iii into develop 2026-02-02 19:38:00 +01:00
James Cole
bd2a746b8a Add new function. 2026-02-02 19:37:53 +01:00
github-actions[bot]
e4a3cbc9da Merge pull request #11640 from firefly-iii/release-1770044183
🤖 Automatically merge the PR into the develop branch.
2026-02-02 15:56:32 +01:00
JC5
18734b0edd 🤖 Auto commit for release 'develop' on 2026-02-02 2026-02-02 15:56:24 +01:00
Sander Dorigo
f52b3bf5f5 Merge branch 'develop' of https://github.com/firefly-iii/firefly-iii into develop 2026-02-02 15:39:06 +01:00
Sander Dorigo
3abba71f8d Fix null pointer 2026-02-02 15:39:03 +01:00
github-actions[bot]
95bdc87ed7 Merge pull request #11639 from firefly-iii/release-1770040333
🤖 Automatically merge the PR into the develop branch.
2026-02-02 14:52:22 +01:00
JC5
9a66b4017b 🤖 Auto commit for release 'develop' on 2026-02-02 2026-02-02 14:52:13 +01:00
Sander Dorigo
610e3f3ae5 Try to fix null pointer 2026-02-02 14:39:03 +01:00
James Cole
a58e70c08b Merge pull request #11632 from mateuszkulapl/main
fix v2 layout dashboard transactions load
2026-02-02 08:42:54 +01:00
mergify[bot]
24c96d40c9 Merge branch 'develop' into main 2026-02-02 03:56:28 +00:00
github-actions[bot]
74ae59910f Merge pull request #11636 from firefly-iii/release-1770004540
🤖 Automatically merge the PR into the develop branch.
2026-02-02 04:55:48 +01:00
JC5
6d49815be9 🤖 Auto commit for release 'develop' on 2026-02-02 2026-02-02 04:55:40 +01:00
mergify[bot]
ae680cd41f Merge branch 'develop' into main 2026-02-01 12:11:25 +00:00
mateuszkulapl
5e9ea1ca10 fix v2 layout dashboard transactions load
update transactions function to accept multiple params (page, start, end)
to match usage in loadAccounts (resources/assets/v2/src/pages/dashboard/accounts.js)
2026-02-01 13:05:52 +01:00
James Cole
6f558f424d Clean up notifications. 2026-01-31 11:51:34 +01:00
James Cole
4387876203 Replace lengthy notification calls, simplifies code. 2026-01-31 11:49:04 +01:00
James Cole
1b1ce3e04e Fix more events #11544 2026-01-31 10:41:48 +01:00
James Cole
ff64675122 Fix stored account event. 2026-01-31 10:34:26 +01:00
James Cole
1c545b7a74 Merge branch 'main' into develop 2026-01-31 10:27:40 +01:00
James Cole
a433ddcd7e Clean up events. 2026-01-31 10:27:12 +01:00
James Cole
b0e1b6fe51 Merge pull request #11624 from firefly-iii/dependabot/composer/composer-2f4529dfab
Bump symfony/process from 7.4.3 to 7.4.5 in the composer group across 1 directory
2026-01-29 05:22:35 +01:00
dependabot[bot]
f5523e60b6 Bump symfony/process in the composer group across 1 directory
Bumps the composer group with 1 update in the / directory: [symfony/process](https://github.com/symfony/process).


Updates `symfony/process` from 7.4.3 to 7.4.5
- [Release notes](https://github.com/symfony/process/releases)
- [Changelog](https://github.com/symfony/process/blob/8.1/CHANGELOG.md)
- [Commits](https://github.com/symfony/process/compare/v7.4.3...v7.4.5)

---
updated-dependencies:
- dependency-name: symfony/process
  dependency-version: 7.4.5
  dependency-type: indirect
  dependency-group: composer
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-01-29 04:08:55 +00:00
James Cole
e300efe640 Merge pull request #11623 from firefly-iii/dependabot/composer/dot-ci/php-cs-fixer/composer-5ee8f56ee5
Bump symfony/process from 8.0.3 to 8.0.5 in /.ci/php-cs-fixer in the composer group across 1 directory
2026-01-29 05:07:50 +01:00
dependabot[bot]
a97f227ddb Bump symfony/process
Bumps the composer group with 1 update in the /.ci/php-cs-fixer directory: [symfony/process](https://github.com/symfony/process).


Updates `symfony/process` from 8.0.3 to 8.0.5
- [Release notes](https://github.com/symfony/process/releases)
- [Changelog](https://github.com/symfony/process/blob/8.1/CHANGELOG.md)
- [Commits](https://github.com/symfony/process/compare/v8.0.3...v8.0.5)

---
updated-dependencies:
- dependency-name: symfony/process
  dependency-version: 8.0.5
  dependency-type: indirect
  dependency-group: composer
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-01-28 21:29:36 +00:00
github-actions[bot]
5f8d7049b5 Merge pull request #11622 from firefly-iii/release-1769628396
🤖 Automatically merge the PR into the develop branch.
2026-01-28 20:26:45 +01:00
JC5
7e80f78f2e 🤖 Auto commit for release 'develop' on 2026-01-28 2026-01-28 20:26:36 +01:00
James Cole
ad922745c4 Fix #11620 2026-01-28 20:22:11 +01:00
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
193 changed files with 5713 additions and 6562 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

@@ -402,16 +402,16 @@
},
{
"name": "friendsofphp/php-cs-fixer",
"version": "v3.93.0",
"version": "v3.93.1",
"source": {
"type": "git",
"url": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer.git",
"reference": "50895a07cface1385082e4caa6a6786c4e033468"
"reference": "b3546ab487c0762c39f308dc1ec0ea2c461fc21a"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/PHP-CS-Fixer/PHP-CS-Fixer/zipball/50895a07cface1385082e4caa6a6786c4e033468",
"reference": "50895a07cface1385082e4caa6a6786c4e033468",
"url": "https://api.github.com/repos/PHP-CS-Fixer/PHP-CS-Fixer/zipball/b3546ab487c0762c39f308dc1ec0ea2c461fc21a",
"reference": "b3546ab487c0762c39f308dc1ec0ea2c461fc21a",
"shasum": ""
},
"require": {
@@ -494,7 +494,7 @@
],
"support": {
"issues": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/issues",
"source": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/tree/v3.93.0"
"source": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/tree/v3.93.1"
},
"funding": [
{
@@ -502,7 +502,7 @@
"type": "github"
}
],
"time": "2026-01-23T17:33:21+00:00"
"time": "2026-01-28T23:50:50+00:00"
},
{
"name": "psr/container",
@@ -1640,16 +1640,16 @@
},
{
"name": "symfony/finder",
"version": "v8.0.4",
"version": "v8.0.5",
"source": {
"type": "git",
"url": "https://github.com/symfony/finder.git",
"reference": "42e48eb02e07d5f3771d194d67da117eb824c8c1"
"reference": "8bd576e97c67d45941365bf824e18dc8538e6eb0"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/finder/zipball/42e48eb02e07d5f3771d194d67da117eb824c8c1",
"reference": "42e48eb02e07d5f3771d194d67da117eb824c8c1",
"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.4"
"source": "https://github.com/symfony/finder/tree/v8.0.5"
},
"funding": [
{
@@ -1704,7 +1704,7 @@
"type": "tidelift"
}
],
"time": "2026-01-12T12:37:40+00:00"
"time": "2026-01-26T15:08:38+00:00"
},
{
"name": "symfony/options-resolver",
@@ -2358,16 +2358,16 @@
},
{
"name": "symfony/process",
"version": "v8.0.4",
"version": "v8.0.5",
"source": {
"type": "git",
"url": "https://github.com/symfony/process.git",
"reference": "10df72602d88c0a3fa685b822976a052611dd607"
"reference": "b5f3aa6762e33fd95efbaa2ec4f4bc9fdd16d674"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/process/zipball/10df72602d88c0a3fa685b822976a052611dd607",
"reference": "10df72602d88c0a3fa685b822976a052611dd607",
"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.4"
"source": "https://github.com/symfony/process/tree/v8.0.5"
},
"funding": [
{
@@ -2419,7 +2419,7 @@
"type": "tidelift"
}
],
"time": "2026-01-23T11:07:10+00:00"
"time": "2026-01-26T15:08:38+00:00"
},
{
"name": "symfony/service-contracts",

View File

@@ -4,6 +4,7 @@ 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

@@ -63,30 +63,28 @@ class TriggerController extends Controller
{
// find recurrence occurrence for this date and trigger it.
// grab the date from the last time the recurrence fired:
$backupDate = $recurrence->latest_date;
$date = $request->getDate();
$backupDate = $recurrence->latest_date;
$date = $request->getDate();
// fire the recurring cron job on the given date, then post-date the created transaction.
Log::info(sprintf('Trigger: will now fire recurring cron job task for date "%s".', $date->format('Y-m-d H:i:s')));
/** @var CreateRecurringTransactions $job */
$job = app(CreateRecurringTransactions::class);
$job = app(CreateRecurringTransactions::class);
$job->setRecurrences(new Collection()->push($recurrence));
$job->setDate($date);
$job->setForce(false);
$job->handle();
Log::debug('Done with recurrence.');
$groups = $job->getGroups();
$groups = $job->getGroups();
$this->repository->markGroupsAsNow($groups);
$recurrence->latest_date = $backupDate;
$recurrence->latest_date_tz = $backupDate?->format('e');
$recurrence->save();
$recurrence = $this->repository->setLatestDate($recurrence, $backupDate);
Preferences::mark();
// enrich groups and return them:
$paginator = new LengthAwarePaginator(new Collection(), 0, 1);
$paginator = new LengthAwarePaginator(new Collection(), 0, 1);
if ($groups->count() > 0) {
/** @var User $admin */
$admin = auth()->user();
@@ -98,20 +96,20 @@ class TriggerController extends Controller
$paginator = $collector->getPaginatedGroups();
}
$manager = $this->getManager();
$manager = $this->getManager();
$paginator->setPath(route('api.v1.recurrences.trigger', [$recurrence->id]).$this->buildParams());
// enrich
$admin = auth()->user();
$enrichment = new TransactionGroupEnrichment();
$admin = auth()->user();
$enrichment = new TransactionGroupEnrichment();
$enrichment->setUser($admin);
$transactions = $enrichment->enrich($paginator->getCollection());
$transactions = $enrichment->enrich($paginator->getCollection());
/** @var TransactionGroupTransformer $transformer */
$transformer = app(TransactionGroupTransformer::class);
$transformer = app(TransactionGroupTransformer::class);
$transformer->setParameters($this->parameters);
$resource = new FractalCollection($transactions, $transformer, 'transactions');
$resource = new FractalCollection($transactions, $transformer, 'transactions');
$resource->setPaginator(new IlluminatePaginatorAdapter($paginator));
return response()->json($manager->createData($resource)->toArray())->header('Content-Type', self::CONTENT_TYPE);

View File

@@ -26,7 +26,8 @@ namespace FireflyIII\Api\V1\Controllers\Models\Transaction;
use FireflyIII\Api\V1\Controllers\Controller;
use FireflyIII\Api\V1\Requests\Models\Transaction\UpdateRequest;
use FireflyIII\Events\UpdatedTransactionGroup;
use FireflyIII\Events\Model\TransactionGroup\TransactionGroupEventFlags;
use FireflyIII\Events\Model\TransactionGroup\UpdatedSingleTransactionGroup;
use FireflyIII\Helpers\Collector\GroupCollectorInterface;
use FireflyIII\Models\TransactionGroup;
use FireflyIII\Repositories\TransactionGroup\TransactionGroupRepositoryInterface;
@@ -72,27 +73,29 @@ class UpdateController extends Controller
public function update(UpdateRequest $request, TransactionGroup $transactionGroup): JsonResponse
{
Log::debug('Now in update routine for transaction group');
$data = $request->getAll();
$oldHash = $this->groupRepository->getCompareHash($transactionGroup);
$transactionGroup = $this->groupRepository->update($transactionGroup, $data);
$newHash = $this->groupRepository->getCompareHash($transactionGroup);
$manager = $this->getManager();
$data = $request->getAll();
$oldHash = $this->groupRepository->getCompareHash($transactionGroup);
$transactionGroup = $this->groupRepository->update($transactionGroup, $data);
$newHash = $this->groupRepository->getCompareHash($transactionGroup);
$manager = $this->getManager();
Preferences::mark();
$applyRules = $data['apply_rules'] ?? true;
$fireWebhooks = $data['fire_webhooks'] ?? true;
$runRecalculations = $oldHash !== $newHash;
$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));
$flags = new TransactionGroupEventFlags();
$flags->applyRules = $applyRules;
$flags->fireWebhooks = $fireWebhooks;
$flags->recalculateCredit = $runRecalculations;
event(new UpdatedSingleTransactionGroup($transactionGroup, $flags));
/** @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)
// filter on transaction group.
@@ -101,20 +104,20 @@ class UpdateController extends Controller
->withAPIInformation()
;
$selectedGroup = $collector->getGroups()->first();
$selectedGroup = $collector->getGroups()->first();
if (null === $selectedGroup) {
throw new NotFoundHttpException();
}
// 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

@@ -25,8 +25,8 @@ declare(strict_types=1);
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\Events\Model\TransactionGroup\UserRequestedBatchProcessing;
use FireflyIII\Models\TransactionJournal;
use FireflyIII\Repositories\Journal\JournalRepositoryInterface;
use Illuminate\Http\JsonResponse;
@@ -44,6 +44,7 @@ class BatchController extends Controller
parent::__construct();
$this->middleware(function ($request, $next) {
$this->repository = app(JournalRepositoryInterface::class);
$this->repository->setUser(auth()->user()); // should not have to do this.
return $next($request);
});
@@ -64,7 +65,8 @@ class BatchController extends Controller
}
$flags = new TransactionGroupEventFlags();
$flags->applyRules = 'true' === $request->get('apply_rules');
event(new CreatedSingleTransactionGroup($group, $flags));
event(new UserRequestedBatchProcessing($flags));
// event(new CreatedSingleTransactionGroup($group, $flags));
return response()->json([], 204);
}

View File

@@ -26,6 +26,7 @@ namespace FireflyIII\Api\V1\Requests\Models\BudgetLimit;
use Carbon\Carbon;
use FireflyIII\Factory\TransactionCurrencyFactory;
use FireflyIII\Repositories\Currency\CurrencyRepositoryInterface;
use FireflyIII\Rules\IsBoolean;
use FireflyIII\Rules\IsValidPositiveAmount;
use FireflyIII\Support\Facades\Amount;
@@ -89,24 +90,24 @@ class StoreRequest extends FormRequest
if (0 !== count($validator->failed())) {
return;
}
$data = $validator->getData();
$data = $validator->getData();
// if no currency has been provided, use the user's default currency:
/** @var TransactionCurrencyFactory $factory */
$factory = app(TransactionCurrencyFactory::class);
$currency = $factory->find($data['currency_id'] ?? null, $data['currency_code'] ?? null);
$factory = app(TransactionCurrencyFactory::class);
$currency = $factory->find($data['currency_id'] ?? null, $data['currency_code'] ?? null);
if (null === $currency) {
$currency = Amount::getPrimaryCurrency();
}
$currency->enabled = true;
$currency->save();
$repository = app(CurrencyRepositoryInterface::class);
$repository->enable($currency);
// validator already concluded start and end are valid dates:
$start = Carbon::parse($data['start'], config('app.timezone'));
$end = Carbon::parse($data['end'], config('app.timezone'));
$start = Carbon::parse($data['start'], config('app.timezone'));
$end = Carbon::parse($data['end'], config('app.timezone'));
// find limit with same date range and currency.
$limit = $budget
$limit = $budget
->budgetlimits()
->where('budget_limits.start_date', $start->format('Y-m-d'))
->where('budget_limits.end_date', $end->format('Y-m-d'))

View File

@@ -38,6 +38,8 @@ use FireflyIII\Models\Transaction;
use FireflyIII\Models\TransactionJournal;
use FireflyIII\Models\TransactionType;
use FireflyIII\Repositories\Account\AccountRepositoryInterface;
use FireflyIII\Services\Internal\Destroy\GenericDestroyService;
use FireflyIII\Services\Internal\Destroy\JournalDestroyService;
use FireflyIII\Support\Facades\Amount;
use FireflyIII\Support\Facades\Steam;
use Illuminate\Console\Command;
@@ -51,9 +53,13 @@ class CorrectsAmounts extends Command
protected $description = 'This command makes sure positive and negative amounts are recorded correctly.';
protected $signature = 'correction:amounts';
private JournalDestroyService $service;
private GenericDestroyService $genericService;
public function handle(): int
{
$this->service = new JournalDestroyService();
$this->genericService = new GenericDestroyService();
// transfers must not have foreign currency info if both accounts have the same currency.
$this->correctTransfers();
// auto budgets must be positive
@@ -177,8 +183,7 @@ class CorrectsAmounts extends Command
private function deleteJournal(TransactionJournal $journal): void
{
$journal->transactionGroup?->delete();
$journal->delete();
$this->service->destroy($journal);
}
private function fixAutoBudgets(): void
@@ -282,7 +287,7 @@ class CorrectsAmounts extends Command
));
$item->rule->active = false;
$item->rule->save();
$item->forceDelete();
$this->genericService->deleteRuleTrigger($item);
return false;
}

View File

@@ -25,8 +25,8 @@ declare(strict_types=1);
namespace FireflyIII\Console\Commands\Correction;
use FireflyIII\Console\Commands\ShowsFriendlyMessages;
use FireflyIII\Events\UpdatedTransactionGroup;
use FireflyIII\Handlers\Events\UpdatedGroupEventHandler;
use FireflyIII\Events\Model\TransactionGroup\TransactionGroupEventFlags;
use FireflyIII\Events\Model\TransactionGroup\UpdatedSingleTransactionGroup;
use FireflyIII\Models\TransactionGroup;
use FireflyIII\Models\TransactionJournal;
use Illuminate\Console\Command;
@@ -44,8 +44,8 @@ class CorrectsGroupAccounts extends Command
*/
public function handle(): int
{
$groups = [];
$res = TransactionJournal::groupBy('transaction_group_id')->get(['transaction_group_id', DB::raw('COUNT(transaction_group_id) as the_count')]);
$groups = [];
$res = TransactionJournal::groupBy('transaction_group_id')->get(['transaction_group_id', DB::raw('COUNT(transaction_group_id) as the_count')]);
/** @var TransactionJournal $journal */
foreach ($res as $journal) {
@@ -53,13 +53,13 @@ class CorrectsGroupAccounts extends Command
$groups[] = (int) $journal->transaction_group_id;
}
}
$handler = new UpdatedGroupEventHandler();
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);
$group = TransactionGroup::find($groupId);
$flags = new TransactionGroupEventFlags();
$flags->applyRules = true;
$flags->fireWebhooks = true;
$flags->recalculateCredit = true;
event(new UpdatedSingleTransactionGroup($group, $flags));
}
return 0;

View File

@@ -25,28 +25,9 @@ declare(strict_types=1);
namespace FireflyIII\Console\Commands\Correction;
use FireflyIII\Console\Commands\ShowsFriendlyMessages;
use FireflyIII\Handlers\Observer\TransactionObserver;
use FireflyIII\Models\Account;
use FireflyIII\Models\AutoBudget;
use FireflyIII\Models\AvailableBudget;
use FireflyIII\Models\Bill;
use FireflyIII\Models\Budget;
use FireflyIII\Models\BudgetLimit;
use FireflyIII\Models\PiggyBank;
use FireflyIII\Models\PiggyBankEvent;
use FireflyIII\Models\Transaction;
use FireflyIII\Models\TransactionCurrency;
use FireflyIII\Models\UserGroup;
use FireflyIII\Repositories\PiggyBank\PiggyBankRepositoryInterface;
use FireflyIII\Repositories\UserGroup\UserGroupRepositoryInterface;
use FireflyIII\Support\Facades\Amount;
use FireflyIII\Services\Internal\Recalculate\PrimaryAmountRecalculationService;
use FireflyIII\Support\Facades\FireflyConfig;
use FireflyIII\Support\Facades\Preferences;
use FireflyIII\Support\Http\Api\ExchangeRateConverter;
use Illuminate\Console\Command;
use Illuminate\Database\Eloquent\Builder as EloquentBuilder;
use Illuminate\Database\Query\Builder as DatabaseBuilder;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
class CorrectsPrimaryCurrencyAmounts extends Command
@@ -70,186 +51,11 @@ class CorrectsPrimaryCurrencyAmounts extends Command
Log::debug('Will update all primary currency amounts. This may take some time.');
$this->friendlyWarning('Recalculating primary currency amounts for all objects. This may take some time!');
/** @var UserGroupRepositoryInterface $repository */
$repository = app(UserGroupRepositoryInterface::class);
$calculator = new PrimaryAmountRecalculationService();
$calculator->recalculate();
Preferences::mark();
/** @var UserGroup $userGroup */
foreach ($repository->getAll() as $userGroup) {
$this->recalculateForGroup($userGroup);
}
$this->friendlyInfo('Recalculated all primary currency amounts.');
return 0;
}
private function recalculateForGroup(UserGroup $userGroup): void
{
Log::debug(sprintf('Now recalculating for user group #%d', $userGroup->id));
$this->recalculateAccounts($userGroup);
// do a check with the group's currency so we can skip some stuff.
$currency = Amount::getPrimaryCurrencyByUserGroup($userGroup);
$this->recalculatePiggyBanks($userGroup, $currency);
$this->recalculateBudgets($userGroup, $currency);
$this->recalculateAvailableBudgets($userGroup, $currency);
$this->recalculateBills($userGroup, $currency);
$this->calculateTransactions($userGroup, $currency);
}
private function recalculateAccounts(UserGroup $userGroup): void
{
$set = $userGroup
->accounts()
->where(static function (EloquentBuilder $q): void {
$q->whereNotNull('virtual_balance');
// this needs a different piece of code for postgres.
if ('pgsql' === config('database.default')) {
$q->orWhere(DB::raw('CAST(virtual_balance AS TEXT)'), '!=', '');
}
if ('pgsql' !== config('database.default')) {
$q->orWhere('virtual_balance', '!=', '');
}
})
->get()
;
/** @var Account $account */
foreach ($set as $account) {
$account->touch();
}
Log::debug(sprintf('Recalculated %d accounts for user group #%d.', $set->count(), $userGroup->id));
}
private function recalculatePiggyBanks(UserGroup $userGroup, TransactionCurrency $currency): void
{
$converter = new ExchangeRateConverter();
$converter->setUserGroup($userGroup);
$converter->setIgnoreSettings(true);
$repository = app(PiggyBankRepositoryInterface::class);
$repository->setUserGroup($userGroup);
$set = $repository->getPiggyBanks();
$set = $set->filter(static fn (PiggyBank $piggyBank): bool => $currency->id !== $piggyBank->transaction_currency_id);
foreach ($set as $piggyBank) {
$piggyBank->encrypted = false;
$piggyBank->save();
foreach ($piggyBank->accounts as $account) {
$account->pivot->native_current_amount = null;
if (0 !== bccomp((string) $account->pivot->current_amount, '0')) {
$account->pivot->native_current_amount = $converter->convert(
$piggyBank->transactionCurrency,
$currency,
today(),
(string) $account->pivot->current_amount
);
}
$account->pivot->save();
}
$this->recalculatePiggyBankEvents($piggyBank);
}
Log::debug(sprintf('Recalculated %d piggy banks for user group #%d.', $set->count(), $userGroup->id));
}
private function recalculatePiggyBankEvents(PiggyBank $piggyBank): void
{
$set = $piggyBank->piggyBankEvents()->get();
$set->each(static function (PiggyBankEvent $event): void { // @phpstan-ignore-line
$event->touch();
});
Log::debug(sprintf('Recalculated %d piggy bank events.', $set->count()));
}
private function recalculateBudgets(UserGroup $userGroup, TransactionCurrency $currency): void
{
$set = $userGroup->budgets()->get();
/** @var Budget $budget */
foreach ($set as $budget) {
$this->recalculateBudgetLimits($budget, $currency);
$this->recalculateAutoBudgets($budget, $currency);
}
Log::debug(sprintf('Recalculated %d budgets.', $set->count()));
}
private function recalculateBudgetLimits(Budget $budget, TransactionCurrency $currency): void
{
$set = $budget->budgetlimits()->where('transaction_currency_id', '!=', $currency->id)->get();
/** @var BudgetLimit $limit */
foreach ($set as $limit) {
Log::debug(sprintf('Will now touch BL #%d', $limit->id));
$limit->touch();
Log::debug(sprintf('Done with touch BL #%d', $limit->id));
}
Log::debug(sprintf('Recalculated %d budget limits for budget #%d.', $set->count(), $budget->id));
}
private function recalculateAutoBudgets(Budget $budget, TransactionCurrency $currency): void
{
$set = $budget->autoBudgets()->where('transaction_currency_id', '!=', $currency->id)->get();
/** @var AutoBudget $autoBudget */
foreach ($set as $autoBudget) {
$autoBudget->touch();
}
Log::debug(sprintf('Recalculated %d auto budgets for budget #%d.', $set->count(), $budget->id));
}
private function recalculateAvailableBudgets(UserGroup $userGroup, TransactionCurrency $currency): void
{
Log::debug('Start with available budgets.');
$set = $userGroup->availableBudgets()->where('transaction_currency_id', '!=', $currency->id)->get();
/** @var AvailableBudget $budget */
foreach ($set as $budget) {
$budget->touch();
}
Log::debug(sprintf('Recalculated %d available budgets.', $set->count()));
}
private function recalculateBills(UserGroup $userGroup, TransactionCurrency $currency): void
{
$set = $userGroup->bills()->where('transaction_currency_id', '!=', $currency->id)->get();
/** @var Bill $bill */
foreach ($set as $bill) {
$bill->touch();
}
Log::debug(sprintf('Recalculated %d bills.', $set->count()));
}
private function calculateTransactions(UserGroup $userGroup, TransactionCurrency $currency): void
{
// custom query because of the potential size of this update.
$set = DB::table('transactions')
->join('transaction_journals', 'transaction_journals.id', '=', 'transactions.transaction_journal_id')
->where('transaction_journals.user_group_id', $userGroup->id)
->where(static function (DatabaseBuilder $q1) use ($currency): void {
$q1->where(static function (DatabaseBuilder $q2) use ($currency): void {
$q2->whereNot('transactions.transaction_currency_id', $currency->id)->whereNull('transactions.foreign_currency_id');
})->orWhere(static function (DatabaseBuilder $q3) use ($currency): void {
$q3->whereNot('transactions.transaction_currency_id', $currency->id)->whereNot('transactions.foreign_currency_id', $currency->id);
});
})
// ->where(static function (DatabaseBuilder $q) use ($currency): void {
// $q->whereNot('transactions.transaction_currency_id', $currency->id)
// ->whereNot('transactions.foreign_currency_id', $currency->id)
// ;
// })
->get(['transactions.id'])
;
TransactionObserver::$recalculate = false;
foreach ($set as $item) {
// here we are.
/** @var null|Transaction $transaction */
$transaction = Transaction::find($item->id);
$transaction?->touch();
}
TransactionObserver::$recalculate = true;
Log::debug(sprintf('Recalculated %d transactions.', $set->count()));
}
}

View File

@@ -25,7 +25,6 @@ declare(strict_types=1);
namespace FireflyIII\Console\Commands\Correction;
use FireflyIII\Console\Commands\ShowsFriendlyMessages;
use FireflyIII\Models\AccountBalance;
use FireflyIII\Models\AvailableBudget;
use FireflyIII\Models\Bill;
use FireflyIII\Models\BudgetLimit;
@@ -47,7 +46,6 @@ class CorrectsTimezoneInformation extends Command
use ShowsFriendlyMessages;
public static array $models = [
AccountBalance::class => ['date'], // done
AvailableBudget::class => ['start_date', 'end_date'], // done
Bill::class => ['date', 'end_date', 'extension_date'], // done
BudgetLimit::class => ['start_date', 'end_date'], // done

View File

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

View File

@@ -1,8 +1,10 @@
<?php
declare(strict_types=1);
/*
* StoredAccount.php
* Copyright (c) 2021 james@firefly-iii.org
* CreatedNewAccount.php
* Copyright (c) 2026 james@firefly-iii.org
*
* This file is part of Firefly III (https://github.com/firefly-iii).
*
@@ -20,17 +22,13 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
declare(strict_types=1);
namespace FireflyIII\Events;
namespace FireflyIII\Events\Model\Account;
use FireflyIII\Events\Event;
use FireflyIII\Models\Account;
use Illuminate\Queue\SerializesModels;
/**
* Class StoredAccount
*/
class StoredAccount extends Event
class CreatedNewAccount extends Event
{
use SerializesModels;

View File

@@ -1,8 +1,10 @@
<?php
/**
* UpdatedTransactionGroup.php
* Copyright (c) 2019 james@firefly-iii.org
declare(strict_types=1);
/*
* UpdatedExistingAccount.php
* Copyright (c) 2026 james@firefly-iii.org
*
* This file is part of Firefly III (https://github.com/firefly-iii).
*
@@ -20,17 +22,13 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
declare(strict_types=1);
namespace FireflyIII\Events\Model\Account;
namespace FireflyIII\Events;
use FireflyIII\Models\TransactionGroup;
use FireflyIII\Events\Event;
use FireflyIII\Models\Account;
use Illuminate\Queue\SerializesModels;
/**
* Class UpdatedTransactionGroup.
*/
class UpdatedTransactionGroup extends Event
class UpdatedExistingAccount extends Event
{
use SerializesModels;
@@ -38,9 +36,6 @@ class UpdatedTransactionGroup extends Event
* Create a new event instance.
*/
public function __construct(
public TransactionGroup $transactionGroup,
public bool $applyRules,
public bool $fireWebhooks,
public bool $runRecalculations
public Account $account
) {}
}

View File

@@ -3,7 +3,7 @@
declare(strict_types=1);
/*
* CreatedTransactionGroupBatch.php
* DestroyedSingleTransactionGroup.php
* Copyright (c) 2026 james@firefly-iii.org
*
* This file is part of Firefly III (https://github.com/firefly-iii).
@@ -25,10 +25,10 @@ declare(strict_types=1);
namespace FireflyIII\Events\Model\TransactionGroup;
use FireflyIII\Events\Event;
use FireflyIII\Models\TransactionGroup;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Collection;
class CreatedTransactionGroupInBatch extends Event
class DestroyedSingleTransactionGroup extends Event
{
use SerializesModels;
@@ -36,7 +36,6 @@ class CreatedTransactionGroupInBatch extends Event
* Create a new event instance.
*/
public function __construct(
public Collection $collection,
public array $flags
public TransactionGroup $transactionGroup
) {}
}

View File

@@ -29,6 +29,9 @@ use FireflyIII\Models\RuleGroup;
use FireflyIII\Models\TransactionGroup;
use Illuminate\Queue\SerializesModels;
/**
* @deprecated
*/
class TriggeredStoredTransactionGroup extends Event
{
use SerializesModels;

View File

@@ -1,8 +1,10 @@
<?php
/**
* StoredTransactionGroup.php
* Copyright (c) 2019 james@firefly-iii.org
declare(strict_types=1);
/*
* UpdatedSingleTransactionGroup.php
* Copyright (c) 2026 james@firefly-iii.org
*
* This file is part of Firefly III (https://github.com/firefly-iii).
*
@@ -20,17 +22,13 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
declare(strict_types=1);
namespace FireflyIII\Events;
namespace FireflyIII\Events\Model\TransactionGroup;
use FireflyIII\Events\Event;
use FireflyIII\Models\TransactionGroup;
use Illuminate\Queue\SerializesModels;
/**
* Class StoredTransactionGroup.
*/
class StoredTransactionGroup extends Event
class UpdatedSingleTransactionGroup extends Event
{
use SerializesModels;
@@ -39,7 +37,6 @@ class StoredTransactionGroup extends Event
*/
public function __construct(
public TransactionGroup $transactionGroup,
public bool $applyRules,
public bool $fireWebhooks
public TransactionGroupEventFlags $flags
) {}
}

View File

@@ -0,0 +1,37 @@
<?php
declare(strict_types=1);
/*
* UserRequestedBatchProcessing.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\Support\Facades\Log;
class UserRequestedBatchProcessing extends Event
{
public function __construct(
public TransactionGroupEventFlags $flags
) {
Log::debug(__METHOD__);
}
}

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

@@ -25,7 +25,7 @@ declare(strict_types=1);
namespace FireflyIII\Factory;
use FireflyIII\Enums\AccountTypeEnum;
use FireflyIII\Events\StoredAccount;
use FireflyIII\Events\Model\Account\CreatedNewAccount;
use FireflyIII\Exceptions\FireflyException;
use FireflyIII\Models\Account;
use FireflyIII\Models\AccountType;
@@ -122,7 +122,7 @@ class AccountFactory
$return = $this->createAccount($type, $data);
event(new StoredAccount($return));
event(new CreatedNewAccount($return));
return $return;
}

View File

@@ -50,6 +50,7 @@ use FireflyIII\Services\Internal\Destroy\JournalDestroyService;
use FireflyIII\Services\Internal\Support\JournalServiceTrait;
use FireflyIII\Support\Facades\Amount;
use FireflyIII\Support\Facades\FireflyConfig;
use FireflyIII\Support\NullArrayObject;
use FireflyIII\User;
use FireflyIII\Validation\AccountValidator;
use Illuminate\Support\Collection;
@@ -108,10 +109,6 @@ class TransactionJournalFactory
public function create(array $data): Collection
{
Log::debug('Now in TransactionJournalFactory::create()');
// convert to special object.
// $dataObject = new NullArrayObject($data);
Log::debug('Start of TransactionJournalFactory::create()');
$collection = new Collection();
$transactions = $data['transactions'] ?? [];
if (0 === count($transactions)) {
@@ -126,7 +123,7 @@ class TransactionJournalFactory
foreach ($transactions as $index => $row) {
$row['batch_submission'] = $batchSubmission;
Log::debug(sprintf('Now creating journal %d/%d', $index + 1, count($transactions)));
$journal = $this->createJournal($row);
$journal = $this->createJournal(new NullArrayObject($row));
if ($journal instanceof TransactionJournal) {
$collection->push($journal);
}
@@ -162,7 +159,7 @@ class TransactionJournalFactory
*
* @SuppressWarnings("PHPMD.ExcessiveMethodLength")
*/
private function createJournal(array $row): ?TransactionJournal
private function createJournal(NullArrayObject $row): ?TransactionJournal
{
Log::debug('Now in TransactionJournalFactory::createJournal()');
$row['import_hash_v2'] = $this->hashArray($row);
@@ -175,11 +172,11 @@ class TransactionJournalFactory
$order = $row['order'] ?? 0;
Log::debug('Find currency or return default.');
$currency = $this->currencyRepository->findCurrency((int) $row['currency_id'] ?? 0, (string) ($row['currency_code'] ?? ''));
$currency = $this->currencyRepository->findCurrency((int) $row['currency_id'], $row['currency_code']);
Log::debug('Find foreign currency or return NULL.');
$foreignCurrency = $this->currencyRepository->findCurrencyNull($row['foreign_currency_id'] ?? 0, $row['foreign_currency_code'] ?? '');
$bill = $this->billRepository->findBill((int) $row['bill_id'] ?? 0, $row['bill_name'] ?? '');
$foreignCurrency = $this->currencyRepository->findCurrencyNull($row['foreign_currency_id'], $row['foreign_currency_code']);
$bill = $this->billRepository->findBill((int) $row['bill_id'], $row['bill_name']);
$billId = TransactionTypeEnum::WITHDRAWAL->value === $type->type && $bill instanceof Bill ? $bill->id : null;
$description = (string) $row['description'];
@@ -187,7 +184,6 @@ class TransactionJournalFactory
$carbon->setTimezone(config('app.timezone'));
// 2024-11-19, overrule timezone with UTC and store it as UTC.
if (true === FireflyConfig::get('utc', false)->data) {
$carbon->setTimezone('UTC');
}
@@ -345,7 +341,7 @@ class TransactionJournalFactory
return $journal;
}
private function hashArray(array $row): string
private function hashArray(NullArrayObject $row): string
{
unset($row['import_hash_v2'], $row['original_source']);
@@ -356,7 +352,7 @@ class TransactionJournalFactory
$json = microtime();
}
$hash = hash('sha256', $json);
Log::debug(sprintf('The hash is: %s', $hash), $row);
Log::debug(sprintf('The hash is: %s', $hash), $row->getArrayCopy());
return $hash;
}
@@ -398,7 +394,7 @@ class TransactionJournalFactory
/**
* @throws FireflyException
*/
private function validateAccounts(array $data): void
private function validateAccounts(NullArrayObject $data): void
{
Log::debug(sprintf('Now in %s', __METHOD__));
$transactionType = $data['type'] ?? 'invalid';
@@ -576,7 +572,7 @@ class TransactionJournalFactory
/**
* Link a piggy bank to this journal.
*/
private function storePiggyEvent(TransactionJournal $journal, array $data): void
private function storePiggyEvent(TransactionJournal $journal, NullArrayObject $data): void
{
Log::debug('Will now store piggy event.');
@@ -591,10 +587,10 @@ class TransactionJournalFactory
Log::debug('Create no piggy event');
}
private function storeMetaFields(TransactionJournal $journal, array $transaction): void
private function storeMetaFields(TransactionJournal $journal, NullArrayObject $transaction): void
{
foreach ($this->fields as $field) {
$this->storeMeta($journal, $transaction, $field);
$this->storeMeta($journal, $transaction->getArrayCopy(), $field);
}
}
@@ -614,7 +610,7 @@ class TransactionJournalFactory
$factory->updateOrCreate($set);
}
private function storeLocation(TransactionJournal $journal, array $data): void
private function storeLocation(TransactionJournal $journal, NullArrayObject $data): void
{
if (!in_array(null, [$data['longitude'], $data['latitude'], $data['zoom_level']], true)) {
$location = new Location();

View File

@@ -0,0 +1,46 @@
<?php
declare(strict_types=1);
/*
* WebhookMessageFactory.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\Factory;
use FireflyIII\Models\Webhook;
use FireflyIII\Models\WebhookMessage;
use Illuminate\Support\Facades\Log;
class WebhookMessageFactory
{
public function create(Webhook $webhook, array $data): WebhookMessage
{
$webhookMessage = new WebhookMessage();
$webhookMessage->webhook()->associate($webhook);
$webhookMessage->sent = false;
$webhookMessage->errored = false;
$webhookMessage->uuid = $data['uuid'];
$webhookMessage->message = $data;
$webhookMessage->save();
Log::debug(sprintf('Stored new webhook message #%d', $webhookMessage->id));
return $webhookMessage;
}
}

View File

@@ -27,13 +27,13 @@ namespace FireflyIII\Generator\Webhook;
use FireflyIII\Enums\WebhookResponse;
use FireflyIII\Enums\WebhookTrigger;
use FireflyIII\Exceptions\FireflyException;
use FireflyIII\Factory\WebhookMessageFactory;
use FireflyIII\Models\Budget;
use FireflyIII\Models\BudgetLimit;
use FireflyIII\Models\Transaction;
use FireflyIII\Models\TransactionGroup;
use FireflyIII\Models\TransactionJournal;
use FireflyIII\Models\Webhook;
use FireflyIII\Models\WebhookMessage;
use FireflyIII\Models\WebhookResponse as WebhookResponseModel;
use FireflyIII\Models\WebhookTrigger as WebhookTriggerModel;
use FireflyIII\Support\JsonApi\Enrichments\AccountEnrichment;
@@ -254,7 +254,8 @@ class StandardMessageGenerator implements MessageGeneratorInterface
$basicMessage['content'][] = $transformer->transform($account);
}
}
$this->storeMessage($webhook, $basicMessage);
$factory = new WebhookMessageFactory();
$factory->create($webhook, $basicMessage);
}
public function getVersion(): int
@@ -277,18 +278,6 @@ class StandardMessageGenerator implements MessageGeneratorInterface
return $accounts->unique();
}
private function storeMessage(Webhook $webhook, array $message): void
{
$webhookMessage = new WebhookMessage();
$webhookMessage->webhook()->associate($webhook);
$webhookMessage->sent = false;
$webhookMessage->errored = false;
$webhookMessage->uuid = $message['uuid'];
$webhookMessage->message = $message;
$webhookMessage->save();
Log::debug(sprintf('Stored new webhook message #%d', $webhookMessage->id));
}
public function setObjects(Collection $objects): void
{
$this->objects = $objects;

View File

@@ -1,70 +0,0 @@
<?php
/**
* APIEventHandler.php
* Copyright (c) 2019 james@firefly-iii.org
*
* This file is part of Firefly III (https://github.com/firefly-iii).
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
declare(strict_types=1);
namespace FireflyIII\Handlers\Events;
use Exception;
use FireflyIII\Notifications\User\NewAccessToken;
use FireflyIII\Repositories\User\UserRepositoryInterface;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Notification;
use Laravel\Passport\Events\AccessTokenCreated;
/**
* Class APIEventHandler
*/
class APIEventHandler
{
/**
* Respond to the creation of an access token.
*/
public function accessTokenCreated(AccessTokenCreated $event): void
{
Log::debug(__METHOD__);
/** @var UserRepositoryInterface $repository */
$repository = app(UserRepositoryInterface::class);
$user = $repository->find((int) $event->userId);
if (null !== $user) {
try {
Notification::send($user, new NewAccessToken());
} catch (Exception $e) {
$message = $e->getMessage();
if (str_contains($message, 'Bcc')) {
Log::warning('[Bcc] Could not send notification. Please validate your email settings, use the .env.example file as a guide.');
return;
}
if (str_contains($message, 'RFC 2822')) {
Log::warning('[RFC] Could not send notification. Please validate your email settings, use the .env.example file as a guide.');
return;
}
Log::error($e->getMessage());
Log::error($e->getTraceAsString());
}
}
}
}

View File

@@ -1,44 +0,0 @@
<?php
/*
* StoredAccountEventHandler.php
* Copyright (c) 2021 james@firefly-iii.org
*
* This file is part of Firefly III (https://github.com/firefly-iii).
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
declare(strict_types=1);
namespace FireflyIII\Handlers\Events;
use FireflyIII\Events\StoredAccount;
use FireflyIII\Services\Internal\Support\CreditRecalculateService;
/**
* Class StoredAccountEventHandler
*/
class StoredAccountEventHandler
{
public function recalculateCredit(StoredAccount $event): void
{
$account = $event->account;
/** @var CreditRecalculateService $object */
$object = app(CreditRecalculateService::class);
$object->setAccount($account);
$object->recalculate();
}
}

View File

@@ -1,187 +0,0 @@
<?php
/**
* StoredGroupEventHandler.php
* Copyright (c) 2019 james@firefly-iii.org
*
* This file is part of Firefly III (https://github.com/firefly-iii).
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
declare(strict_types=1);
namespace FireflyIII\Handlers\Events;
use FireflyIII\Enums\WebhookTrigger;
use FireflyIII\Events\Model\TransactionGroup\TriggeredStoredTransactionGroup;
use FireflyIII\Events\Model\Webhook\WebhookMessagesRequestSending;
use FireflyIII\Events\StoredTransactionGroup;
use FireflyIII\Generator\Webhook\MessageGeneratorInterface;
use FireflyIII\Models\RuleGroup;
use FireflyIII\Models\Transaction;
use FireflyIII\Models\TransactionJournal;
use FireflyIII\Repositories\PeriodStatistic\PeriodStatisticRepositoryInterface;
use FireflyIII\Repositories\RuleGroup\RuleGroupRepositoryInterface;
use FireflyIII\Services\Internal\Support\CreditRecalculateService;
use FireflyIII\TransactionRules\Engine\RuleEngineInterface;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Log;
/**
* Class StoredGroupEventHandler
*
* TODO migrate to observer?
*/
class StoredGroupEventHandler
{
public function runAllHandlers(StoredTransactionGroup $event): void
{
// $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);
}
/**
* This method grabs all the users rules and processes them.
*/
private function processRules(StoredTransactionGroup $storedGroupEvent, ?RuleGroup $ruleGroup): void
{
if (false === $storedGroupEvent->applyRules) {
Log::info(sprintf('Will not run rules on group #%d', $storedGroupEvent->transactionGroup->id));
return;
}
Log::debug('Now in StoredGroupEventHandler::processRules()');
$journals = $storedGroupEvent->transactionGroup->transactionJournals;
$array = [];
/** @var TransactionJournal $journal */
foreach ($journals as $journal) {
$array[] = $journal->id;
}
$journalIds = implode(',', $array);
Log::debug(sprintf('Add local operator for journal(s): %s', $journalIds));
// collect rules:
$ruleGroupRepository = app(RuleGroupRepositoryInterface::class);
$ruleGroupRepository->setUser($storedGroupEvent->transactionGroup->user);
// add the groups to the rule engine.
// it should run the rules in the group and cancel the group if necessary.
if (null === $ruleGroup) {
Log::debug('Fire processRules with ALL store-journal rule groups.');
$groups = $ruleGroupRepository->getRuleGroupsWithRules('store-journal');
}
if (null !== $ruleGroup) {
Log::debug(sprintf('Fire processRules with rule group #%d.', $ruleGroup->id));
$groups = new Collection([$ruleGroup]);
}
// create and fire rule engine.
$newRuleEngine = app(RuleEngineInterface::class);
$newRuleEngine->setUser($storedGroupEvent->transactionGroup->user);
$newRuleEngine->addOperator(['type' => 'journal_id', 'value' => $journalIds]);
$newRuleEngine->setRuleGroups($groups);
$newRuleEngine->fire();
}
private function recalculateCredit(StoredTransactionGroup $event): void
{
$group = $event->transactionGroup;
/** @var CreditRecalculateService $object */
$object = app(CreditRecalculateService::class);
$object->setGroup($group);
$object->recalculate();
}
private function removePeriodStatistics(StoredTransactionGroup $event): void
{
/** @var PeriodStatisticRepositoryInterface $repository */
$repository = app(PeriodStatisticRepositoryInterface::class);
/** @var TransactionJournal $journal */
foreach ($event->transactionGroup->transactionJournals as $journal) {
/** @var null|Transaction $source */
$source = $journal->transactions()->where('amount', '<', '0')->first();
/** @var null|Transaction $dest */
$dest = $journal->transactions()->where('amount', '>', '0')->first();
if (null !== $source) {
$repository->deleteStatisticsForModel($source->account, $journal->date);
}
if (null !== $dest) {
$repository->deleteStatisticsForModel($dest->account, $journal->date);
}
$categories = $journal->categories;
$tags = $journal->tags;
$budgets = $journal->budgets;
foreach ($categories as $category) {
$repository->deleteStatisticsForModel($category, $journal->date);
}
foreach ($tags as $tag) {
$repository->deleteStatisticsForModel($tag, $journal->date);
}
foreach ($budgets as $budget) {
$repository->deleteStatisticsForModel($budget, $journal->date);
}
if (0 === $categories->count()) {
$repository->deleteStatisticsForPrefix($journal->userGroup, 'no_category', $journal->date);
}
if (0 === $budgets->count()) {
$repository->deleteStatisticsForPrefix($journal->userGroup, 'no_budget', $journal->date);
}
}
}
/**
* This method processes all webhooks that respond to the "stored transaction group" trigger (100)
*/
private function triggerWebhooks(StoredTransactionGroup $storedGroupEvent): void
{
Log::debug(__METHOD__);
$group = $storedGroupEvent->transactionGroup;
if (false === $storedGroupEvent->fireWebhooks) {
Log::info(sprintf('Will not fire webhooks for transaction group #%d', $group->id));
return;
}
$user = $group->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(new Collection()->push($group));
// 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());
}
}

View File

@@ -1,44 +0,0 @@
<?php
/*
* UpdatedAccountEventHandler.php
* Copyright (c) 2021 james@firefly-iii.org
*
* This file is part of Firefly III (https://github.com/firefly-iii).
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
declare(strict_types=1);
namespace FireflyIII\Handlers\Events;
use FireflyIII\Events\UpdatedAccount;
use FireflyIII\Services\Internal\Support\CreditRecalculateService;
/**
* Class UpdatedAccountEventHandler
*/
class UpdatedAccountEventHandler
{
public function recalculateCredit(UpdatedAccount $event): void
{
$account = $event->account;
/** @var CreditRecalculateService $object */
$object = app(CreditRecalculateService::class);
$object->setAccount($account);
$object->recalculate();
}
}

View File

@@ -0,0 +1,45 @@
<?php
declare(strict_types=1);
/*
* ConversionParameters.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\Handlers\ExchangeRate;
use Carbon\Carbon;
use FireflyIII\Models\TransactionCurrency;
use FireflyIII\User;
use Illuminate\Database\Eloquent\Model;
class ConversionParameters
{
public User $user;
public Model $model;
public ?TransactionCurrency $originalCurrency = null;
public string $amountField;
public string $primaryAmountField;
public Carbon $date;
public function __construct()
{
$this->date = now();
}
}

View File

@@ -0,0 +1,104 @@
<?php
declare(strict_types=1);
/*
* ConvertsAmountToPrimaryAmount.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\Handlers\ExchangeRate;
use FireflyIII\Support\Facades\Amount;
use FireflyIII\Support\Http\Api\ExchangeRateConverter;
use Illuminate\Support\Facades\Log;
class ConvertsAmountToPrimaryAmount
{
public static function convert(ConversionParameters $params): void
{
$amountField = $params->amountField;
$primaryAmountField = $params->primaryAmountField;
if (!Amount::convertToPrimary($params->user)) {
Log::debug(sprintf(
'User does not want to do conversion, no need to convert %s and store it in field %s for %s #%d.',
$params->amountField,
$params->primaryAmountField,
get_class($params->model),
$params->model->id
));
$params->model->{$primaryAmountField} = null;
$params->model->saveQuietly();
return;
}
if (null === $params->originalCurrency) {
Log::debug(sprintf(
'Original currency field is empty, no need to convert %s and store it in field %s for %s #%d.',
$params->amountField,
$params->primaryAmountField,
get_class($params->model),
$params->model->id
));
return;
}
$primaryCurrency = Amount::getPrimaryCurrencyByUserGroup($params->user->userGroup);
Log::debug(sprintf(
'Will convert amount in field %s from %s to %s and store it in %s',
$params->originalCurrency->code,
$primaryCurrency->code,
$params->amountField,
$params->primaryAmountField
));
if ($params->originalCurrency->id === $primaryCurrency->id) {
Log::debug('Both currencies are the same, do nothing.');
return;
}
// field is empty or zero, do nothing.
$amount = (string) $params->model->{$amountField};
if ('' === $amount || 0 === bccomp($amount, '0')) {
Log::debug(sprintf('Amount "%s" in field "%s" cannot be used, do nothing.', $amount, $amountField));
$params->model->{$amountField} = null;
$params->model->{$primaryAmountField} = null;
$params->model->saveQuietly();
return;
}
$converter = new ExchangeRateConverter();
$newAmount = $converter->convert($params->originalCurrency, $primaryCurrency, now(), $amount);
$converter->setUserGroup($params->user->userGroup);
$converter->setIgnoreSettings(true);
$params->model->{$primaryAmountField} = $newAmount;
$params->model->saveQuietly();
Log::debug(sprintf(
'Converted field "%s" of %s #%d from %s %s to %s %s (in field "%s")',
$amountField,
get_class($params->model),
$params->model->id,
$params->originalCurrency->code,
$amount,
$primaryCurrency->code,
$newAmount,
$primaryAmountField
));
}
}

View File

@@ -24,9 +24,9 @@ declare(strict_types=1);
namespace FireflyIII\Handlers\Observer;
use FireflyIII\Handlers\ExchangeRate\ConversionParameters;
use FireflyIII\Handlers\ExchangeRate\ConvertsAmountToPrimaryAmount;
use FireflyIII\Models\AutoBudget;
use FireflyIII\Support\Facades\Amount;
use FireflyIII\Support\Http\Api\ExchangeRateConverter;
use Illuminate\Support\Facades\Log;
class AutoBudgetObserver
@@ -37,26 +37,20 @@ class AutoBudgetObserver
$this->updatePrimaryCurrencyAmount($autoBudget);
}
private function updatePrimaryCurrencyAmount(AutoBudget $autoBudget): void
{
if (!Amount::convertToPrimary($autoBudget->budget->user)) {
return;
}
$userCurrency = Amount::getPrimaryCurrencyByUserGroup($autoBudget->budget->user->userGroup);
$autoBudget->native_amount = null;
if ($autoBudget->transactionCurrency->id !== $userCurrency->id) {
$converter = new ExchangeRateConverter();
$converter->setUserGroup($autoBudget->budget->user->userGroup);
$converter->setIgnoreSettings(true);
$autoBudget->native_amount = $converter->convert($autoBudget->transactionCurrency, $userCurrency, today(), $autoBudget->amount);
}
$autoBudget->saveQuietly();
Log::debug('Auto budget primary currency amount is updated.');
}
public function updated(AutoBudget $autoBudget): void
{
Log::debug('Observe "updated" of an auto budget.');
$this->updatePrimaryCurrencyAmount($autoBudget);
}
private function updatePrimaryCurrencyAmount(AutoBudget $autoBudget): void
{
$params = new ConversionParameters();
$params->user = $autoBudget->budget->user;
$params->model = $autoBudget;
$params->originalCurrency = $autoBudget->transactionCurrency;
$params->amountField = 'amount';
$params->primaryAmountField = 'native_amount';
ConvertsAmountToPrimaryAmount::convert($params);
}
}

View File

@@ -24,41 +24,30 @@ declare(strict_types=1);
namespace FireflyIII\Handlers\Observer;
use FireflyIII\Handlers\ExchangeRate\ConversionParameters;
use FireflyIII\Handlers\ExchangeRate\ConvertsAmountToPrimaryAmount;
use FireflyIII\Models\AvailableBudget;
use FireflyIII\Support\Facades\Amount;
use FireflyIII\Support\Http\Api\ExchangeRateConverter;
use Illuminate\Support\Facades\Log;
class AvailableBudgetObserver
{
public function created(AvailableBudget $availableBudget): void
{
// Log::debug('Observe "created" of an available budget.');
$this->updatePrimaryCurrencyAmount($availableBudget);
}
public function updated(AvailableBudget $availableBudget): void
{
$this->updatePrimaryCurrencyAmount($availableBudget);
}
private function updatePrimaryCurrencyAmount(AvailableBudget $availableBudget): void
{
if (!Amount::convertToPrimary($availableBudget->user)) {
// Log::debug('Do not update primary currency available amount of the available budget.');
return;
}
$userCurrency = Amount::getPrimaryCurrencyByUserGroup($availableBudget->user->userGroup);
$availableBudget->native_amount = null;
if ($availableBudget->transactionCurrency->id !== $userCurrency->id) {
$converter = new ExchangeRateConverter();
$converter->setUserGroup($availableBudget->user->userGroup);
$converter->setIgnoreSettings(true);
$availableBudget->native_amount = $converter->convert($availableBudget->transactionCurrency, $userCurrency, today(), $availableBudget->amount);
}
$availableBudget->saveQuietly();
Log::debug('Available budget primary currency amount is updated.');
}
public function updated(AvailableBudget $availableBudget): void
{
// Log::debug('Observe "updated" of an available budget.');
$this->updatePrimaryCurrencyAmount($availableBudget);
$params = new ConversionParameters();
$params->user = $availableBudget->user;
$params->model = $availableBudget;
$params->originalCurrency = $availableBudget->transactionCurrency;
$params->amountField = 'amount';
$params->primaryAmountField = 'native_amount';
ConvertsAmountToPrimaryAmount::convert($params);
}
}

View File

@@ -23,12 +23,11 @@ declare(strict_types=1);
namespace FireflyIII\Handlers\Observer;
use FireflyIII\Handlers\ExchangeRate\ConversionParameters;
use FireflyIII\Handlers\ExchangeRate\ConvertsAmountToPrimaryAmount;
use FireflyIII\Models\Attachment;
use FireflyIII\Models\Bill;
use FireflyIII\Repositories\Attachment\AttachmentRepositoryInterface;
use FireflyIII\Support\Facades\Amount;
use FireflyIII\Support\Http\Api\ExchangeRateConverter;
use Illuminate\Support\Facades\Log;
/**
* Class BillObserver
@@ -37,35 +36,14 @@ class BillObserver
{
public function created(Bill $bill): void
{
// Log::debug('Observe "created" of a bill.');
$this->updatePrimaryCurrencyAmount($bill);
}
private function updatePrimaryCurrencyAmount(Bill $bill): void
{
if (!Amount::convertToPrimary($bill->user)) {
return;
}
$userCurrency = Amount::getPrimaryCurrencyByUserGroup($bill->user->userGroup);
$bill->native_amount_min = null;
$bill->native_amount_max = null;
if ($bill->transactionCurrency->id !== $userCurrency->id) {
$converter = new ExchangeRateConverter();
$converter->setUserGroup($bill->user->userGroup);
$converter->setIgnoreSettings(true);
$bill->native_amount_min = $converter->convert($bill->transactionCurrency, $userCurrency, today(), $bill->amount_min);
$bill->native_amount_max = $converter->convert($bill->transactionCurrency, $userCurrency, today(), $bill->amount_max);
}
$bill->saveQuietly();
Log::debug('Bill primary currency amounts are updated.');
}
public function deleting(Bill $bill): void
{
$repository = app(AttachmentRepositoryInterface::class);
$repository->setUser($bill->user);
// Log::debug('Observe "deleting" of a bill.');
/** @var Attachment $attachment */
foreach ($bill->attachments()->get() as $attachment) {
$repository->destroy($attachment);
@@ -78,4 +56,20 @@ class BillObserver
// Log::debug('Observe "updated" of a bill.');
$this->updatePrimaryCurrencyAmount($bill);
}
private function updatePrimaryCurrencyAmount(Bill $bill): void
{
$params = new ConversionParameters();
$params->user = $bill->user;
$params->model = $bill;
$params->originalCurrency = $bill->transactionCurrency;
$params->amountField = 'amount_min';
$params->primaryAmountField = 'native_amount_min';
ConvertsAmountToPrimaryAmount::convert($params);
// and again!
$params->amountField = 'amount_max';
$params->primaryAmountField = 'native_amount_max';
ConvertsAmountToPrimaryAmount::convert($params);
}
}

View File

@@ -27,9 +27,9 @@ namespace FireflyIII\Handlers\Observer;
use FireflyIII\Enums\WebhookTrigger;
use FireflyIII\Events\Model\Webhook\WebhookMessagesRequestSending;
use FireflyIII\Generator\Webhook\MessageGeneratorInterface;
use FireflyIII\Handlers\ExchangeRate\ConversionParameters;
use FireflyIII\Handlers\ExchangeRate\ConvertsAmountToPrimaryAmount;
use FireflyIII\Models\BudgetLimit;
use FireflyIII\Support\Facades\Amount;
use FireflyIII\Support\Http\Api\ExchangeRateConverter;
use FireflyIII\Support\Observers\RecalculatesAvailableBudgetsTrait;
use FireflyIII\Support\Singleton\PreferencesSingleton;
use Illuminate\Support\Collection;
@@ -44,18 +44,30 @@ class BudgetLimitObserver
Log::debug('Observe "created" of a budget limit.');
$this->updatePrimaryCurrencyAmount($budgetLimit);
$this->updateAvailableBudget($budgetLimit);
$this->sendWebhookMessages('fire_webhooks_bl_store', WebhookTrigger::STORE_UPDATE_BUDGET_LIMIT, $budgetLimit);
}
public function updated(BudgetLimit $budgetLimit): void
{
Log::debug('Observe "updated" of a budget limit.');
$this->updatePrimaryCurrencyAmount($budgetLimit);
$this->updateAvailableBudget($budgetLimit);
$this->sendWebhookMessages('fire_webhooks_bl_update', WebhookTrigger::STORE_UPDATE_BUDGET_LIMIT, $budgetLimit);
}
private function sendWebhookMessages(string $key, WebhookTrigger $trigger, BudgetLimit $budgetLimit): void
{
// this is a lame trick to communicate with the observer.
$singleton = PreferencesSingleton::getInstance();
if (true === $singleton->getPreference('fire_webhooks_bl_store')) {
if (true === $singleton->getPreference($key)) {
$user = $budgetLimit->budget->user;
/** @var MessageGeneratorInterface $engine */
$engine = app(MessageGeneratorInterface::class);
$engine->setUser($user);
$engine->setObjects(new Collection()->push($budgetLimit));
$engine->setTrigger(WebhookTrigger::STORE_UPDATE_BUDGET_LIMIT);
$engine->setTrigger($trigger);
$engine->generateMessages();
Log::debug(sprintf('send event WebhookMessagesRequestSending from %s', __METHOD__));
@@ -65,44 +77,12 @@ class BudgetLimitObserver
private function updatePrimaryCurrencyAmount(BudgetLimit $budgetLimit): void
{
if (!Amount::convertToPrimary($budgetLimit->budget->user)) {
// Log::debug('Do not update primary currency amount of the budget limit.');
return;
}
$userCurrency = Amount::getPrimaryCurrencyByUserGroup($budgetLimit->budget->user->userGroup);
$budgetLimit->native_amount = null;
if ($budgetLimit->transactionCurrency->id !== $userCurrency->id) {
$converter = new ExchangeRateConverter();
$converter->setUserGroup($budgetLimit->budget->user->userGroup);
$converter->setIgnoreSettings(true);
$budgetLimit->native_amount = $converter->convert($budgetLimit->transactionCurrency, $userCurrency, today(), $budgetLimit->amount);
}
$budgetLimit->saveQuietly();
Log::debug('Budget limit primary currency amounts are updated.');
}
public function updated(BudgetLimit $budgetLimit): void
{
Log::debug('Observe "updated" of a budget limit.');
$this->updatePrimaryCurrencyAmount($budgetLimit);
$this->updateAvailableBudget($budgetLimit);
// this is a lame trick to communicate with the observer.
$singleton = PreferencesSingleton::getInstance();
if (true === $singleton->getPreference('fire_webhooks_bl_update')) {
$user = $budgetLimit->budget->user;
/** @var MessageGeneratorInterface $engine */
$engine = app(MessageGeneratorInterface::class);
$engine->setUser($user);
$engine->setObjects(new Collection()->push($budgetLimit));
$engine->setTrigger(WebhookTrigger::STORE_UPDATE_BUDGET_LIMIT);
$engine->generateMessages();
Log::debug(sprintf('send event WebhookMessagesRequestSending from %s', __METHOD__));
event(new WebhookMessagesRequestSending());
}
$params = new ConversionParameters();
$params->user = $budgetLimit->budget->user;
$params->model = $budgetLimit;
$params->originalCurrency = $budgetLimit->transactionCurrency;
$params->amountField = 'amount';
$params->primaryAmountField = 'native_amount';
ConvertsAmountToPrimaryAmount::convert($params);
}
}

View File

@@ -64,27 +64,6 @@ class BudgetObserver
}
}
public function updated(Budget $budget): void
{
Log::debug(sprintf('Observe "updated" of budget #%d ("%s").', $budget->id, $budget->name));
// this is a lame trick to communicate with the observer.
$singleton = PreferencesSingleton::getInstance();
if (true === $singleton->getPreference('fire_webhooks_budget_update')) {
$user = $budget->user;
/** @var MessageGeneratorInterface $engine */
$engine = app(MessageGeneratorInterface::class);
$engine->setUser($user);
$engine->setObjects(new Collection()->push($budget));
$engine->setTrigger(WebhookTrigger::UPDATE_BUDGET);
$engine->generateMessages();
Log::debug(sprintf('send event WebhookMessagesRequestSending from %s', __METHOD__));
event(new WebhookMessagesRequestSending());
}
}
public function deleting(Budget $budget): void
{
Log::debug('Observe "deleting" of a budget.');
@@ -123,4 +102,25 @@ class BudgetObserver
// recalculate available budgets.
}
public function updated(Budget $budget): void
{
Log::debug(sprintf('Observe "updated" of budget #%d ("%s").', $budget->id, $budget->name));
// this is a lame trick to communicate with the observer.
$singleton = PreferencesSingleton::getInstance();
if (true === $singleton->getPreference('fire_webhooks_budget_update')) {
$user = $budget->user;
/** @var MessageGeneratorInterface $engine */
$engine = app(MessageGeneratorInterface::class);
$engine->setUser($user);
$engine->setObjects(new Collection()->push($budget));
$engine->setTrigger(WebhookTrigger::UPDATE_BUDGET);
$engine->generateMessages();
Log::debug(sprintf('send event WebhookMessagesRequestSending from %s', __METHOD__));
event(new WebhookMessagesRequestSending());
}
}
}

View File

@@ -29,55 +29,12 @@ use FireflyIII\Models\Attachment;
use FireflyIII\Models\Transaction;
use FireflyIII\Models\TransactionGroup;
use FireflyIII\Models\TransactionJournal;
use FireflyIII\Repositories\Account\AccountRepositoryInterface;
use FireflyIII\Repositories\Attachment\AttachmentRepositoryInterface;
use FireflyIII\Support\Facades\Amount;
use FireflyIII\Support\Http\Api\ExchangeRateConverter;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
/**
* Class AccountObserver
*/
class AccountObserver
class DeletedAccountObserver
{
public function created(Account $account): void
{
// Log::debug('Observe "created" of an account.');
$this->updatePrimaryCurrencyAmount($account);
}
private function updatePrimaryCurrencyAmount(Account $account): void
{
if (!Amount::convertToPrimary($account->user)) {
return;
}
$userCurrency = Amount::getPrimaryCurrencyByUserGroup($account->user->userGroup);
$repository = app(AccountRepositoryInterface::class);
$currency = $repository->getAccountCurrency($account);
if (
null !== $currency
&& $currency->id !== $userCurrency->id
&& '' !== (string) $account->virtual_balance
&& 0 !== bccomp($account->virtual_balance, '0')
) {
$converter = new ExchangeRateConverter();
$converter->setUserGroup($account->user->userGroup);
$converter->setIgnoreSettings(true);
$account->native_virtual_balance = $converter->convert($currency, $userCurrency, today(), $account->virtual_balance);
}
if ('' === (string) $account->virtual_balance || 0 === bccomp($account->virtual_balance, '0')) {
$account->virtual_balance = null;
$account->native_virtual_balance = null;
}
$account->saveQuietly();
// Log::debug('Account primary currency virtual balance is updated.');
}
/**
* Also delete related objects.
*/
public function deleting(Account $account): void
{
Log::debug('Observe "deleting" of an account.');
@@ -117,10 +74,4 @@ class AccountObserver
$account->notes()->delete();
$account->locations()->delete();
}
public function updated(Account $account): void
{
// Log::debug('Observe "updated" of an account.');
$this->updatePrimaryCurrencyAmount($account);
}
}

View File

@@ -29,7 +29,7 @@ use Illuminate\Support\Facades\Log;
/**
* Class AttachmentObserver
*/
class AttachmentObserver
class DeletedAttachmentObserver
{
public function deleting(Attachment $attachment): void
{

View File

@@ -31,7 +31,7 @@ use Illuminate\Support\Facades\Log;
/**
* Class CategoryObserver
*/
class CategoryObserver
class DeletedCategoryObserver
{
public function deleting(Category $category): void
{

View File

@@ -31,7 +31,7 @@ use Illuminate\Support\Facades\Log;
/**
* Class RecurrenceObserver
*/
class RecurrenceObserver
class DeletedRecurrenceObserver
{
public function deleting(Recurrence $recurrence): void
{

View File

@@ -29,7 +29,7 @@ use Illuminate\Support\Facades\Log;
/**
* Class RecurrenceTransactionObserver
*/
class RecurrenceTransactionObserver
class DeletedRecurrenceTransactionObserver
{
public function deleting(RecurrenceTransaction $transaction): void
{

View File

@@ -29,7 +29,7 @@ use Illuminate\Support\Facades\Log;
/**
* Class RuleGroupObserver
*/
class RuleGroupObserver
class DeletedRuleGroupObserver
{
public function deleting(RuleGroup $ruleGroup): void
{

View File

@@ -29,7 +29,7 @@ use Illuminate\Support\Facades\Log;
/**
* Class RuleObserver
*/
class RuleObserver
class DeletedRuleObserver
{
public function deleting(Rule $rule): void
{

View File

@@ -31,7 +31,7 @@ use Illuminate\Support\Facades\Log;
/**
* Class TagObserver
*/
class TagObserver
class DeletedTagObserver
{
public function deleting(Tag $tag): void
{

View File

@@ -29,7 +29,7 @@ use Illuminate\Support\Facades\Log;
/**
* Class TransactionGroup
*/
class TransactionGroupObserver
class DeletedTransactionGroupObserver
{
public function deleting(TransactionGroup $transactionGroup): void
{

View File

@@ -33,7 +33,7 @@ use Illuminate\Support\Facades\Log;
/**
* Class TransactionJournalObserver
*/
class TransactionJournalObserver
class DeletedTransactionJournalObserver
{
public function deleting(TransactionJournal $transactionJournal): void
{

View File

@@ -29,7 +29,7 @@ use Illuminate\Support\Facades\Log;
/**
* Class WebhookMessageObserver
*/
class WebhookMessageObserver
class DeletedWebhookMessageObserver
{
public function deleting(WebhookMessage $webhookMessage): void
{

View File

@@ -29,7 +29,7 @@ use Illuminate\Support\Facades\Log;
/**
* Class WebhookObserver
*/
class WebhookObserver
class DeletedWebhookObserver
{
public function deleting(Webhook $webhook): void
{

View File

@@ -24,9 +24,9 @@ declare(strict_types=1);
namespace FireflyIII\Handlers\Observer;
use FireflyIII\Handlers\ExchangeRate\ConversionParameters;
use FireflyIII\Handlers\ExchangeRate\ConvertsAmountToPrimaryAmount;
use FireflyIII\Models\PiggyBankEvent;
use FireflyIII\Support\Facades\Amount;
use FireflyIII\Support\Http\Api\ExchangeRateConverter;
use Illuminate\Support\Facades\Log;
class PiggyBankEventObserver
@@ -37,32 +37,27 @@ class PiggyBankEventObserver
$this->updatePrimaryCurrencyAmount($event);
}
private function updatePrimaryCurrencyAmount(PiggyBankEvent $event): void
{
$user = $event->piggyBank->accounts()->first()?->user;
if (null === $user) {
Log::warning('Piggy bank seems to have no accounts. Break.');
return;
}
if (!Amount::convertToPrimary($user)) {
return;
}
$userCurrency = Amount::getPrimaryCurrencyByUserGroup($event->piggyBank->accounts()->first()->user->userGroup);
$event->native_amount = null;
if ($event->piggyBank->transactionCurrency->id !== $userCurrency->id) {
$converter = new ExchangeRateConverter();
$converter->setUserGroup($event->piggyBank->accounts()->first()->user->userGroup);
$converter->setIgnoreSettings(true);
$event->native_amount = $converter->convert($event->piggyBank->transactionCurrency, $userCurrency, today(), $event->amount);
}
$event->saveQuietly();
Log::debug('Piggy bank event primary currency amount is updated.');
}
public function updated(PiggyBankEvent $event): void
{
Log::debug('Observe "updated" of a piggy bank event.');
$this->updatePrimaryCurrencyAmount($event);
}
private function updatePrimaryCurrencyAmount(PiggyBankEvent $event): void
{
$user = $event->piggyBank->accounts()->first()?->user;
if (null === $user) {
Log::warning('Piggy bank seems to have no accounts. Break.');
return;
}
$params = new ConversionParameters();
$params->user = $user;
$params->model = $event;
$params->originalCurrency = $event->piggyBank->transactionCurrency;
$params->amountField = 'amount';
$params->primaryAmountField = 'native_amount';
ConvertsAmountToPrimaryAmount::convert($params);
}
}

View File

@@ -24,11 +24,11 @@ declare(strict_types=1);
namespace FireflyIII\Handlers\Observer;
use FireflyIII\Handlers\ExchangeRate\ConversionParameters;
use FireflyIII\Handlers\ExchangeRate\ConvertsAmountToPrimaryAmount;
use FireflyIII\Models\Attachment;
use FireflyIII\Models\PiggyBank;
use FireflyIII\Repositories\Attachment\AttachmentRepositoryInterface;
use FireflyIII\Support\Facades\Amount;
use FireflyIII\Support\Http\Api\ExchangeRateConverter;
use Illuminate\Support\Facades\Log;
/**
@@ -42,26 +42,6 @@ class PiggyBankObserver
$this->updatePrimaryCurrencyAmount($piggyBank);
}
private function updatePrimaryCurrencyAmount(PiggyBank $piggyBank): void
{
$group = $piggyBank->accounts()->first()?->user->userGroup;
if (null === $group) {
Log::debug(sprintf('No account(s) yet for piggy bank #%d.', $piggyBank->id));
return;
}
$userCurrency = Amount::getPrimaryCurrencyByUserGroup($group);
$piggyBank->native_target_amount = null;
if ($piggyBank->transactionCurrency->id !== $userCurrency->id) {
$converter = new ExchangeRateConverter();
$converter->setIgnoreSettings(true);
$converter->setUserGroup($group);
$piggyBank->native_target_amount = $converter->convert($piggyBank->transactionCurrency, $userCurrency, today(), $piggyBank->target_amount);
}
$piggyBank->saveQuietly();
Log::debug('Piggy bank primary currency target amount is updated.');
}
/**
* Also delete related objects.
*/
@@ -88,4 +68,22 @@ class PiggyBankObserver
Log::debug('Observe "updated" of a piggy bank.');
$this->updatePrimaryCurrencyAmount($piggyBank);
}
private function updatePrimaryCurrencyAmount(PiggyBank $piggyBank): void
{
$group = $piggyBank->accounts()->first()?->user->userGroup;
if (null === $group) {
Log::debug(sprintf('No account(s) yet for piggy bank #%d.', $piggyBank->id));
return;
}
$params = new ConversionParameters();
$params->user = $piggyBank->accounts()->first()?->user;
$params->model = $piggyBank;
$params->originalCurrency = $piggyBank->transactionCurrency;
$params->amountField = 'target_amount';
$params->primaryAmountField = 'native_target_amount';
ConvertsAmountToPrimaryAmount::convert($params);
}
}

View File

@@ -23,11 +23,9 @@ declare(strict_types=1);
namespace FireflyIII\Handlers\Observer;
use FireflyIII\Handlers\ExchangeRate\ConversionParameters;
use FireflyIII\Handlers\ExchangeRate\ConvertsAmountToPrimaryAmount;
use FireflyIII\Models\Transaction;
use FireflyIII\Support\Facades\Amount;
use FireflyIII\Support\Facades\FireflyConfig;
use FireflyIII\Support\Http\Api\ExchangeRateConverter;
use FireflyIII\Support\Models\AccountBalanceCalculator;
use Illuminate\Support\Facades\Log;
/**
@@ -39,55 +37,10 @@ class TransactionObserver
public function created(Transaction $transaction): void
{
return;
Log::debug(sprintf('Observed creation of Transaction #%d.', $transaction->id));
$this->updatePrimaryCurrencyAmount($transaction);
}
private function updatePrimaryCurrencyAmount(Transaction $transaction): void
{
if (!Amount::convertToPrimary($transaction->transactionJournal->user)) {
return;
}
$userCurrency = Amount::getPrimaryCurrencyByUserGroup($transaction->transactionJournal->user->userGroup);
$transaction->native_amount = null;
$transaction->native_foreign_amount = null;
// first normal amount
if (
$transaction->transactionCurrency->id !== $userCurrency->id
&& (
null === $transaction->foreign_currency_id
|| null !== $transaction->foreign_currency_id
&& $transaction->foreign_currency_id !== $userCurrency->id
)
) {
$converter = new ExchangeRateConverter();
$converter->setUserGroup($transaction->transactionJournal->user->userGroup);
$converter->setIgnoreSettings(true);
$transaction->native_amount = $converter->convert(
$transaction->transactionCurrency,
$userCurrency,
$transaction->transactionJournal->date,
$transaction->amount
);
}
// then foreign amount
if ($transaction->foreignCurrency?->id !== $userCurrency->id && null !== $transaction->foreign_amount && null !== $transaction->foreignCurrency) {
$converter = new ExchangeRateConverter();
$converter->setUserGroup($transaction->transactionJournal->user->userGroup);
$converter->setIgnoreSettings(true);
$transaction->native_foreign_amount = $converter->convert(
$transaction->foreignCurrency,
$userCurrency,
$transaction->transactionJournal->date,
$transaction->foreign_amount
);
}
$transaction->saveQuietly();
Log::debug(sprintf('Transaction #%d primary currency amounts are updated.', $transaction->id));
}
public function deleting(?Transaction $transaction): void
{
Log::debug('Observe "deleting" of a transaction.');
@@ -96,15 +49,29 @@ class TransactionObserver
public function updated(Transaction $transaction): void
{
// Log::debug('Observe "updated" of a transaction.');
if (
true === FireflyConfig::get('use_running_balance', config('firefly.feature_flags.running_balance_column'))->data
&& self::$recalculate
&& 1 === bccomp($transaction->amount, '0')
) {
Log::debug('Trigger recalculateForJournal');
AccountBalanceCalculator::recalculateForJournal($transaction->transactionJournal);
}
$this->updatePrimaryCurrencyAmount($transaction);
}
private function updatePrimaryCurrencyAmount(Transaction $transaction): void
{
// convert "amount" to "native_amount"
$params = new ConversionParameters();
$params->user = $transaction->transactionJournal->user;
$params->model = $transaction;
$params->originalCurrency = $transaction->transactionCurrency;
$params->amountField = 'amount';
$params->date = $transaction->transactionJournal->date;
$params->primaryAmountField = 'native_amount';
ConvertsAmountToPrimaryAmount::convert($params);
// convert "foreign_amount" to "native_foreign_amount"
$params = new ConversionParameters();
$params->user = $transaction->transactionJournal->user;
$params->model = $transaction;
$params->originalCurrency = $transaction->foreignCurrency;
$params->date = $transaction->transactionJournal->date;
$params->amountField = 'foreign_amount';
$params->primaryAmountField = 'native_foreign_amount';
ConvertsAmountToPrimaryAmount::convert($params);
}
}

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

@@ -24,7 +24,8 @@ declare(strict_types=1);
namespace FireflyIII\Http\Controllers\Transaction;
use FireflyIII\Events\UpdatedTransactionGroup;
use FireflyIII\Events\Model\TransactionGroup\TransactionGroupEventFlags;
use FireflyIII\Events\Model\TransactionGroup\UpdatedSingleTransactionGroup;
use FireflyIII\Http\Controllers\Controller;
use FireflyIII\Http\Requests\BulkEditJournalRequest;
use FireflyIII\Models\TransactionJournal;
@@ -115,10 +116,12 @@ class BulkController extends Controller
}
}
$flags = new TransactionGroupEventFlags();
// run rules on changed journals:
/** @var TransactionJournal $journal */
foreach ($collection as $journal) { // @phpstan-ignore-line
event(new UpdatedTransactionGroup($journal->transactionGroup, true, true, false));
foreach ($collection as $journal) {
event(new UpdatedSingleTransactionGroup($journal->transactionGroup, $flags));
}
Preferences::mark();

View File

@@ -26,7 +26,8 @@ namespace FireflyIII\Http\Controllers\Transaction;
use Exception;
use FireflyIII\Enums\AccountTypeEnum;
use FireflyIII\Enums\TransactionTypeEnum;
use FireflyIII\Events\UpdatedTransactionGroup;
use FireflyIII\Events\Model\TransactionGroup\TransactionGroupEventFlags;
use FireflyIII\Events\Model\TransactionGroup\UpdatedSingleTransactionGroup;
use FireflyIII\Exceptions\FireflyException;
use FireflyIII\Http\Controllers\Controller;
use FireflyIII\Models\Account;
@@ -305,7 +306,8 @@ class ConvertController extends Controller
$group->refresh();
session()->flash('success', (string) trans('firefly.converted_to_'.$destinationType->type));
event(new UpdatedTransactionGroup($group, true, true, true));
$flags = new TransactionGroupEventFlags();
event(new UpdatedSingleTransactionGroup($group, $flags));
return redirect(route('transactions.show', [$group->id]));
}

View File

@@ -79,7 +79,6 @@ class CreateController extends Controller
// event!
$flags = new TransactionGroupEventFlags();
event(new CreatedSingleTransactionGroup($group, $flags));
// event(new StoredTransactionGroup($newGroup, true, true));
Preferences::mark();

View File

@@ -26,7 +26,8 @@ namespace FireflyIII\Http\Controllers\Transaction;
use Carbon\Carbon;
use FireflyIII\Enums\AccountTypeEnum;
use FireflyIII\Enums\TransactionTypeEnum;
use FireflyIII\Events\UpdatedTransactionGroup;
use FireflyIII\Events\Model\TransactionGroup\TransactionGroupEventFlags;
use FireflyIII\Events\Model\TransactionGroup\UpdatedSingleTransactionGroup;
use FireflyIII\Exceptions\FireflyException;
use FireflyIII\Http\Controllers\Controller;
use FireflyIII\Http\Requests\MassDeleteJournalRequest;
@@ -194,15 +195,15 @@ class MassController extends Controller
*/
private function updateJournal(int $journalId, MassEditJournalRequest $request): void
{
$journal = $this->repository->find($journalId);
$journal = $this->repository->find($journalId);
if (!$journal instanceof TransactionJournal) {
throw new FireflyException(sprintf('Trying to edit non-existent or deleted journal #%d', $journalId));
}
$service = app(JournalUpdateService::class);
$service = app(JournalUpdateService::class);
// for each field, call the update service.
$service->setTransactionJournal($journal);
$data = [
$data = [
'date' => $this->getDateFromRequest($request, $journal->id, 'date'),
'description' => $this->getStringFromRequest($request, $journal->id, 'description'),
'source_id' => $this->getIntFromRequest($request, $journal->id, 'source_id'),
@@ -220,8 +221,10 @@ class MassController extends Controller
$service->setData($data);
$service->update();
// trigger rules
$runRecalculations = $service->isCompareHashChanged();
event(new UpdatedTransactionGroup($journal->transactionGroup, true, true, $runRecalculations));
$runRecalculations = $service->isCompareHashChanged();
$flags = new TransactionGroupEventFlags();
$flags->recalculateCredit = $runRecalculations;
event(new UpdatedSingleTransactionGroup($journal->transactionGroup, $flags));
}
private function getDateFromRequest(MassEditJournalRequest $request, int $journalId, string $key): ?Carbon

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

@@ -0,0 +1,74 @@
<?php
declare(strict_types=1);
/*
* TriggersCreditRecalculation.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\Account;
use FireflyIII\Events\Model\Account\CreatedNewAccount;
use FireflyIII\Events\Model\Account\UpdatedExistingAccount;
use FireflyIII\Handlers\ExchangeRate\ConversionParameters;
use FireflyIII\Handlers\ExchangeRate\ConvertsAmountToPrimaryAmount;
use FireflyIII\Models\Account;
use FireflyIII\Repositories\Account\AccountRepositoryInterface;
use FireflyIII\Services\Internal\Support\CreditRecalculateService;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Support\Facades\Log;
class UpdatesAccountInformation implements ShouldQueue
{
public function handle(CreatedNewAccount|UpdatedExistingAccount $event): void
{
$this->recalculateCredit($event->account);
$this->updateVirtualBalance($event->account);
}
private function recalculateCredit(Account $account): void
{
Log::debug('Will call CreditRecalculateService because a new account was created or updated.');
/** @var CreditRecalculateService $object */
$object = app(CreditRecalculateService::class);
$object->setAccount($account);
$object->recalculate();
}
private function updateVirtualBalance(Account $account): void
{
Log::debug('Will updateVirtualBalance');
$repository = app(AccountRepositoryInterface::class);
$currency = $repository->getAccountCurrency($account);
if (null !== $currency) {
// only when the account has a currency, because that is the only way for the
// account to have a virtual balance.
$params = new ConversionParameters();
$params->user = $account->user;
$params->model = $account;
$params->originalCurrency = $currency;
$params->amountField = 'virtual_balance';
$params->primaryAmountField = 'native_virtual_balance';
ConvertsAmountToPrimaryAmount::convert($params);
Log::debug('Account primary currency virtual balance is updated.');
}
}
}

View File

@@ -26,12 +26,11 @@ namespace FireflyIII\Listeners\Model\Rule;
use FireflyIII\Events\Model\Rule\RuleActionFailedOnArray;
use FireflyIII\Events\Model\Rule\RuleActionFailedOnObject;
use FireflyIII\Notifications\NotificationSender;
use FireflyIII\Notifications\User\RuleActionFailed;
use FireflyIII\Support\Facades\Preferences;
use GuzzleHttp\Exception\ClientException;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Notification;
class NotifiesUserAboutFailedRuleAction implements ShouldQueue
{
@@ -58,11 +57,6 @@ class NotifiesUserAboutFailedRuleAction implements ShouldQueue
$ruleTitle = $rule->title;
$ruleLink = route('rules.edit', [$rule->id]);
$params = [$mainMessage, $groupTitle, $groupLink, $ruleTitle, $ruleLink];
try {
Notification::send($user, new RuleActionFailed($params));
} catch (ClientException $e) {
Log::error(sprintf('[a] Error sending notification that the rule action failed: %s', $e->getMessage()));
}
NotificationSender::send($user, new RuleActionFailed($params));
}
}

View File

@@ -24,13 +24,12 @@ declare(strict_types=1);
namespace FireflyIII\Listeners\Model\Subscription;
use Exception;
use FireflyIII\Events\Model\Subscription\SubscriptionNeedsExtensionOrRenewal;
use FireflyIII\Notifications\NotificationSender;
use FireflyIII\Notifications\User\BillReminder;
use FireflyIII\Support\Facades\Preferences;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Notification;
class NotifiesAboutExtensionOrRenewal implements ShouldQueue
{
@@ -44,24 +43,7 @@ class NotifiesAboutExtensionOrRenewal implements ShouldQueue
if (true === $preference) {
Log::debug('Subscription reminder is true!');
try {
Notification::send($subscription->user, new BillReminder($subscription, $event->field, $event->diff));
} catch (Exception $e) {
$message = $e->getMessage();
if (str_contains($message, 'Bcc')) {
Log::warning('[Bcc] Could not send notification. Please validate your email settings, use the .env.example file as a guide.');
return;
}
if (str_contains($message, 'RFC 2822')) {
Log::warning('[RFC] Could not send notification. Please validate your email settings, use the .env.example file as a guide.');
return;
}
Log::error($e->getMessage());
Log::error($e->getTraceAsString());
}
NotificationSender::send($subscription->user, new BillReminder($subscription, $event->field, $event->diff));
return;
}

View File

@@ -24,14 +24,13 @@ declare(strict_types=1);
namespace FireflyIII\Listeners\Model\Subscription;
use Exception;
use FireflyIII\Events\Model\Subscription\SubscriptionsAreOverdueForPayment;
use FireflyIII\Models\Bill;
use FireflyIII\Notifications\NotificationSender;
use FireflyIII\Notifications\User\SubscriptionsOverdueReminder;
use FireflyIII\Support\Facades\Preferences;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Notification;
class NotifiesAboutOverdueSubscriptions implements ShouldQueue
{
@@ -78,23 +77,6 @@ class NotifiesAboutOverdueSubscriptions implements ShouldQueue
Preferences::setForUser($bill->user, $key, true);
}
Log::warning('should hit this ONCE');
try {
Notification::send($user, new SubscriptionsOverdueReminder($toBeWarned));
} catch (Exception $e) {
$message = $e->getMessage();
if (str_contains($message, 'Bcc')) {
Log::warning('[Bcc] Could not send notification. Please validate your email settings, use the .env.example file as a guide.');
return;
}
if (str_contains($message, 'RFC 2822')) {
Log::warning('[RFC] Could not send notification. Please validate your email settings, use the .env.example file as a guide.');
return;
}
Log::error($e->getMessage());
Log::error($e->getTraceAsString());
}
NotificationSender::send($user, new SubscriptionsOverdueReminder($toBeWarned));
}
}

View File

@@ -24,16 +24,15 @@ declare(strict_types=1);
namespace FireflyIII\Listeners\Model\TransactionGroup;
use Exception;
use FireflyIII\Events\Model\TransactionGroup\TransactionGroupsRequestedReporting;
use FireflyIII\Models\TransactionGroup;
use FireflyIII\Notifications\NotificationSender;
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 implements ShouldQueue
{
@@ -71,23 +70,7 @@ class MailsNewTransactionsReport implements ShouldQueue
$groups[] = $transformer->transformObject($group);
}
try {
Notification::send($user, new TransactionCreation($groups));
} catch (Exception $e) {
$message = $e->getMessage();
if (str_contains($message, 'Bcc')) {
Log::warning('[Bcc] Could not send notification. Please validate your email settings, use the .env.example file as a guide.');
return;
}
if (str_contains($message, 'RFC 2822')) {
Log::warning('[RFC] Could not send notification. Please validate your email settings, use the .env.example file as a guide.');
return;
}
Log::error($e->getMessage());
Log::error($e->getTraceAsString());
}
NotificationSender::send($user, new TransactionCreation($groups));
Log::debug('If there is no error above this line, message was sent.');
}
}

View File

@@ -1,8 +1,10 @@
<?php
declare(strict_types=1);
/*
* DestroyedGroupEventHandler.php
* Copyright (c) 2021 james@firefly-iii.org
* ProcessesDestroyedTransactionGroup.php
* Copyright (c) 2026 james@firefly-iii.org
*
* This file is part of Firefly III (https://github.com/firefly-iii).
*
@@ -20,31 +22,27 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
declare(strict_types=1);
namespace FireflyIII\Handlers\Events;
namespace FireflyIII\Listeners\Model\TransactionGroup;
use FireflyIII\Enums\WebhookTrigger;
use FireflyIII\Events\DestroyedTransactionGroup;
use FireflyIII\Events\Model\TransactionGroup\DestroyedSingleTransactionGroup;
use FireflyIII\Events\Model\Webhook\WebhookMessagesRequestSending;
use FireflyIII\Generator\Webhook\MessageGeneratorInterface;
use FireflyIII\Support\Facades\FireflyConfig;
use FireflyIII\Support\Models\AccountBalanceCalculator;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Log;
/**
* Class DestroyedGroupEventHandler
*/
class DestroyedGroupEventHandler
class ProcessesDestroyedTransactionGroup implements ShouldQueue
{
public function runAllHandlers(DestroyedTransactionGroup $event): void
public function handle(DestroyedSingleTransactionGroup $event): void
{
$this->triggerWebhooks($event);
$this->updateRunningBalance($event);
}
private function triggerWebhooks(DestroyedTransactionGroup $destroyedGroupEvent): void
private function triggerWebhooks(DestroyedSingleTransactionGroup $destroyedGroupEvent): void
{
Log::debug('DestroyedTransactionGroup:triggerWebhooks');
$group = $destroyedGroupEvent->transactionGroup;
@@ -60,7 +58,7 @@ class DestroyedGroupEventHandler
event(new WebhookMessagesRequestSending());
}
private function updateRunningBalance(DestroyedTransactionGroup $event): void
private function updateRunningBalance(DestroyedSingleTransactionGroup $event): void
{
if (false === FireflyConfig::get('use_running_balance', config('firefly.feature_flags.running_balance_column'))->data) {
return;

View File

@@ -27,10 +27,12 @@ namespace FireflyIII\Listeners\Model\TransactionGroup;
use Carbon\Carbon;
use FireflyIII\Enums\WebhookTrigger;
use FireflyIII\Events\Model\TransactionGroup\CreatedSingleTransactionGroup;
use FireflyIII\Events\Model\TransactionGroup\UserRequestedBatchProcessing;
use FireflyIII\Events\Model\Webhook\WebhookMessagesRequestSending;
use FireflyIII\Generator\Webhook\MessageGeneratorInterface;
use FireflyIII\Models\Account;
use FireflyIII\Models\TransactionGroup;
use FireflyIII\Models\TransactionJournal;
use FireflyIII\Models\TransactionJournalMeta;
use FireflyIII\Repositories\Journal\JournalRepositoryInterface;
use FireflyIII\Repositories\PeriodStatistic\PeriodStatisticRepositoryInterface;
@@ -45,23 +47,34 @@ use Illuminate\Support\Facades\Log;
class ProcessesNewTransactionGroup implements ShouldQueue
{
public function handle(CreatedSingleTransactionGroup $event): void
public function handle(CreatedSingleTransactionGroup|UserRequestedBatchProcessing $event): void
{
Log::debug(sprintf('In ProcessesNewTransactionGroup::handle(#%d)', $event->transactionGroup->id));
if (true === $event->flags->batchSubmission) {
Log::debug(sprintf('Will do nothing for group #%d because it is part of a batch.', $event->transactionGroup->id));
$groupId = 0;
$collection = new Collection();
if ($event instanceof CreatedSingleTransactionGroup) {
Log::debug(sprintf('In ProcessesNewTransactionGroup::handle(#%d)', $event->transactionGroup->id));
$groupId = $event->transactionGroup->id;
$collection = $event->transactionGroup->transactionJournals;
}
if ($event instanceof UserRequestedBatchProcessing) {
Log::debug('User called UserRequestedBatchProcessing');
}
$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.', $groupId));
return;
}
Log::debug(sprintf('Will join group #%d with all other open transaction groups and process them.', $event->transactionGroup->id));
$collection = $event->transactionGroup->transactionJournals;
Log::debug(sprintf('Will (joined with group #%d) collect all open transaction groups and process them.', $groupId));
$repository = app(JournalRepositoryInterface::class);
$set = $collection->merge($repository->getUncompletedJournals());
$set = $collection->merge($repository->getAllUncompletedJournals());
if (0 === $set->count()) {
Log::debug('Set is empty, never mind.');
return;
}
Log::debug(sprintf('Set count is %d', $set->count()));
if (!$event->flags->applyRules) {
Log::debug(sprintf('Will NOT process rules for %d journal(s)', $set->count()));
}
@@ -80,12 +93,10 @@ class ProcessesNewTransactionGroup implements ShouldQueue
if ($event->flags->fireWebhooks) {
$this->fireWebhooks($set);
}
// always remove old statistics.
$this->removePeriodStatistics($set);
// always remove old relevant statistics.
self::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);
}
@@ -93,43 +104,54 @@ class ProcessesNewTransactionGroup implements ShouldQueue
$repository->markAsCompleted($set);
}
private function recalculateRunningBalance(Collection $set): void
private function getFromInternalDate(array $ids): Carbon
{
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();
$entries = TransactionJournalMeta::whereIn('transaction_journal_id', $ids)->where('name', '_internal_previous_date')->get(['journal_meta.*']);
$array = $entries->toArray();
$return = today()->subDay();
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;
$date = Carbon::parse($array[0]['data']);
$return = $date->lt($return) ? $date : $return;
}
return $return;
}
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();
$fromInternalDate = $this->getFromInternalDate($set->pluck('id')->toArray());
$earliest = $fromInternalDate->lt($earliest) ? $fromInternalDate : $earliest;
Log::debug(sprintf('Found earliest date: %s', $earliest->toW3cString()));
// get accounts
$accounts = Account::leftJoin('transactions', 'transactions.account_id', 'accounts.id')
$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.*'])
;
Log::debug('Found accounts to process', $accounts->pluck('id')->toArray());
AccountBalanceCalculator::optimizedCalculation($accounts, $earliest);
}
private function removePeriodStatistics(Collection $set): void
public static function removePeriodStatistics(Collection $set): void
{
Log::debug('Always remove period statistics');
if (auth()->check()) {
Log::debug('Always remove period statistics');
/** @var PeriodStatisticRepositoryInterface $repository */
$repository = app(PeriodStatisticRepositoryInterface::class);
$repository->deleteStatisticsForCollection($set);
/** @var PeriodStatisticRepositoryInterface $repository */
$repository = app(PeriodStatisticRepositoryInterface::class);
$repository->deleteStatisticsForCollection($set);
}
}
private function fireWebhooks(Collection $set): void
@@ -138,7 +160,10 @@ class ProcessesNewTransactionGroup implements ShouldQueue
$groups = TransactionGroup::whereIn('id', array_unique($set->pluck('transaction_group_id')->toArray()))->get();
Log::debug(__METHOD__);
$user = $set->first()->user;
/** @var TransactionJournal $first */
$first = $set->first();
$user = $first->user;
/** @var MessageGeneratorInterface $engine */
$engine = app(MessageGeneratorInterface::class);
@@ -170,8 +195,11 @@ class ProcessesNewTransactionGroup implements ShouldQueue
{
Log::debug(sprintf('Will now processRules for %d journal(s)', $set->count()));
$array = $set->pluck('id')->toArray();
/** @var TransactionJournal $first */
$first = $set->first();
$journalIds = implode(',', $array);
$user = $set->first()->user;
$user = $first->user;
Log::debug(sprintf('Add local operator for journal(s): %s', $journalIds));
// collect rules:

View File

@@ -1,8 +1,10 @@
<?php
/**
* UpdatedGroupEventHandler.php
* Copyright (c) 2019 james@firefly-iii.org
declare(strict_types=1);
/*
* ProcessesUpdatedTransactionGroup.php
* Copyright (c) 2026 james@firefly-iii.org
*
* This file is part of Firefly III (https://github.com/firefly-iii).
*
@@ -19,19 +21,17 @@
* 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\Model\TransactionGroup;
use FireflyIII\Enums\TransactionTypeEnum;
use FireflyIII\Enums\WebhookTrigger;
use FireflyIII\Events\Model\TransactionGroup\UpdatedSingleTransactionGroup;
use FireflyIII\Events\Model\Webhook\WebhookMessagesRequestSending;
use FireflyIII\Events\UpdatedTransactionGroup;
use FireflyIII\Generator\Webhook\MessageGeneratorInterface;
use FireflyIII\Models\Account;
use FireflyIII\Models\Transaction;
use FireflyIII\Models\TransactionJournal;
use FireflyIII\Repositories\PeriodStatistic\PeriodStatisticRepositoryInterface;
use FireflyIII\Repositories\RuleGroup\RuleGroupRepositoryInterface;
use FireflyIII\Services\Internal\Support\CreditRecalculateService;
use FireflyIII\Support\Facades\FireflyConfig;
@@ -40,71 +40,31 @@ use FireflyIII\TransactionRules\Engine\RuleEngineInterface;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Log;
/**
* Class UpdatedGroupEventHandler
*/
class UpdatedGroupEventHandler
class ProcessesUpdatedTransactionGroup
{
public function runAllHandlers(UpdatedTransactionGroup $event): void
public function handle(UpdatedSingleTransactionGroup $event): void
{
Log::debug('Now in handle() for UpdatedSingleTransactionGroup');
$this->unifyAccounts($event);
$this->processRules($event);
$this->recalculateCredit($event);
$this->triggerWebhooks($event);
$this->removePeriodStatistics($event);
if ($event->runRecalculations) {
$this->updateRunningBalance($event);
}
}
ProcessesNewTransactionGroup::removePeriodStatistics($event->transactionGroup->transactionJournals);
$this->updateRunningBalance($event);
/**
* TODO duplicate
*/
private function removePeriodStatistics(UpdatedTransactionGroup $event): void
{
/** @var PeriodStatisticRepositoryInterface $repository */
$repository = app(PeriodStatisticRepositoryInterface::class);
/** @var TransactionJournal $journal */
foreach ($event->transactionGroup->transactionJournals as $journal) {
$source = $journal->transactions()->where('amount', '<', '0')->first();
$dest = $journal->transactions()->where('amount', '>', '0')->first();
if (null !== $source) {
$repository->deleteStatisticsForModel($source->account, $journal->date);
}
if (null !== $dest) {
$repository->deleteStatisticsForModel($dest->account, $journal->date);
}
$categories = $journal->categories;
$tags = $journal->tags;
$budgets = $journal->budgets;
foreach ($categories as $category) {
$repository->deleteStatisticsForModel($category, $journal->date);
}
foreach ($tags as $tag) {
$repository->deleteStatisticsForModel($tag, $journal->date);
}
foreach ($budgets as $budget) {
$repository->deleteStatisticsForModel($budget, $journal->date);
}
if (0 === $categories->count()) {
$repository->deleteStatisticsForPrefix($journal->userGroup, 'no_category', $journal->date);
}
if (0 === $budgets->count()) {
$repository->deleteStatisticsForPrefix($journal->userGroup, 'no_budget', $journal->date);
}
}
Log::debug('Done with handle() for UpdatedSingleTransactionGroup');
}
/**
* This method will make sure all source / destination accounts are the same.
*/
public function unifyAccounts(UpdatedTransactionGroup $updatedGroupEvent): void
public function unifyAccounts(UpdatedSingleTransactionGroup $updatedGroupEvent): void
{
Log::debug('Now in unifyAccounts()');
$group = $updatedGroupEvent->transactionGroup;
if (1 === $group->transactionJournals->count()) {
Log::debug('Nothing to do in unifyAccounts()');
return;
}
@@ -142,14 +102,16 @@ class UpdatedGroupEventHandler
// set all destination transactions to destination account:
Transaction::whereIn('transaction_journal_id', $all)->where('amount', '>', 0)->update(['account_id' => $destAccount->id]);
}
Log::debug('Done with unifyAccounts()');
}
/**
* This method will check all the rules when a journal is updated.
*/
private function processRules(UpdatedTransactionGroup $updatedGroupEvent): void
private function processRules(UpdatedSingleTransactionGroup $updatedGroupEvent): void
{
if (false === $updatedGroupEvent->applyRules) {
Log::debug('Now in processRules()');
if (false === $updatedGroupEvent->flags->applyRules) {
Log::info(sprintf('Will not run rules on group #%d', $updatedGroupEvent->transactionGroup->id));
return;
@@ -177,23 +139,26 @@ class UpdatedGroupEventHandler
$newRuleEngine->addOperator(['type' => 'journal_id', 'value' => $journalIds]);
$newRuleEngine->setRuleGroups($groups);
$newRuleEngine->fire();
Log::debug('Done with processRules()');
}
private function recalculateCredit(UpdatedTransactionGroup $event): void
private function recalculateCredit(UpdatedSingleTransactionGroup $event): void
{
Log::debug('Now in recalculateCredit()');
$group = $event->transactionGroup;
/** @var CreditRecalculateService $object */
$object = app(CreditRecalculateService::class);
$object->setGroup($group);
$object->recalculate();
Log::debug('Done with recalculateCredit()');
}
private function triggerWebhooks(UpdatedTransactionGroup $updatedGroupEvent): void
private function triggerWebhooks(UpdatedSingleTransactionGroup $updatedGroupEvent): void
{
Log::debug(__METHOD__);
Log::debug('Now in triggerWebhooks()');
$group = $updatedGroupEvent->transactionGroup;
if (false === $updatedGroupEvent->fireWebhooks) {
if (false === $updatedGroupEvent->flags->fireWebhooks) {
Log::info(sprintf('Will not fire webhooks for transaction group #%d', $group->id));
return;
@@ -209,10 +174,12 @@ class UpdatedGroupEventHandler
Log::debug(sprintf('send event WebhookMessagesRequestSending from %s', __METHOD__));
event(new WebhookMessagesRequestSending());
Log::debug('End of triggerWebhooks()');
}
private function updateRunningBalance(UpdatedTransactionGroup $event): void
private function updateRunningBalance(UpdatedSingleTransactionGroup $event): void
{
Log::debug('Now in updateRunningBalance()');
if (false === FireflyConfig::get('use_running_balance', config('firefly.feature_flags.running_balance_column'))->data) {
return;
}
@@ -221,5 +188,6 @@ class UpdatedGroupEventHandler
foreach ($group->transactionJournals as $journal) {
AccountBalanceCalculator::recalculateForJournal($journal);
}
Log::debug('Done with updateRunningBalance()');
}
}

View File

@@ -25,7 +25,6 @@ declare(strict_types=1);
namespace FireflyIII\Listeners\Security\System;
use Database\Seeders\ExchangeRateSeeder;
use Exception;
use FireflyIII\Enums\UserRoleEnum;
use FireflyIII\Events\Security\System\NewUserRegistered;
use FireflyIII\Exceptions\FireflyException;
@@ -34,13 +33,13 @@ use FireflyIII\Models\UserGroup;
use FireflyIII\Models\UserRole;
use FireflyIII\Notifications\Admin\UserRegistration as AdminRegistrationNotification;
use FireflyIII\Notifications\Notifiables\OwnerNotifiable;
use FireflyIII\Notifications\NotificationSender;
use FireflyIII\Notifications\User\UserRegistration as UserRegistrationNotification;
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 implements ShouldQueue
{
@@ -117,24 +116,7 @@ class HandlesNewUserRegistration implements ShouldQueue
if (!$sendMail) {
return;
}
try {
Notification::send($owner, new AdminRegistrationNotification($user));
} catch (Exception $e) {
$message = $e->getMessage();
if (str_contains($message, 'Bcc')) {
Log::warning('[Bcc] Could not send notification. Please validate your email settings, use the .env.example file as a guide.');
return;
}
if (str_contains($message, 'RFC 2822')) {
Log::warning('[RFC] Could not send notification. Please validate your email settings, use the .env.example file as a guide.');
return;
}
Log::error($e->getMessage());
Log::error($e->getTraceAsString());
}
NotificationSender::send($owner, new AdminRegistrationNotification($user));
}
private function sendRegistrationMail(User $user): void
@@ -144,22 +126,6 @@ class HandlesNewUserRegistration implements ShouldQueue
return;
}
try {
Notification::send($user, new UserRegistrationNotification());
} catch (Exception $e) {
$message = $e->getMessage();
if (str_contains($message, 'Bcc')) {
Log::warning('[Bcc] Could not send notification. Please validate your email settings, use the .env.example file as a guide.');
return;
}
if (str_contains($message, 'RFC 2822')) {
Log::warning('[RFC] Could not send notification. Please validate your email settings, use the .env.example file as a guide.');
return;
}
Log::error($e->getMessage());
Log::error($e->getTraceAsString());
}
NotificationSender::send($user, new UserRegistrationNotification());
}
}

View File

@@ -31,11 +31,11 @@ use FireflyIII\Mail\InvitationMail;
use FireflyIII\Models\InvitedUser;
use FireflyIII\Notifications\Admin\UserInvitation;
use FireflyIII\Notifications\Notifiables\OwnerNotifiable;
use FireflyIII\Notifications\NotificationSender;
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 NotifiesAboutNewInvitation implements ShouldQueue
{
@@ -68,22 +68,6 @@ class NotifiesAboutNewInvitation implements ShouldQueue
return;
}
try {
Notification::send(new OwnerNotifiable(), new UserInvitation($invitee));
} catch (Exception $e) {
$message = $e->getMessage();
if (str_contains($message, 'Bcc')) {
Log::warning('[Bcc] Could not send notification. Please validate your email settings, use the .env.example file as a guide.');
return;
}
if (str_contains($message, 'RFC 2822')) {
Log::warning('[RFC] Could not send notification. Please validate your email settings, use the .env.example file as a guide.');
return;
}
Log::error($e->getMessage());
Log::error($e->getTraceAsString());
}
NotificationSender::send(new OwnerNotifiable(), new UserInvitation($invitee));
}
}

View File

@@ -24,14 +24,12 @@ declare(strict_types=1);
namespace FireflyIII\Listeners\Security\System;
use Exception;
use FireflyIII\Events\Security\System\SystemFoundNewVersionOnline;
use FireflyIII\Notifications\Admin\VersionCheckResult;
use FireflyIII\Notifications\Notifiables\OwnerNotifiable;
use FireflyIII\Notifications\NotificationSender;
use FireflyIII\Support\Facades\FireflyConfig;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Notification;
class NotifiesOwnerAboutNewVersion implements ShouldQueue
{
@@ -42,23 +40,7 @@ class NotifiesOwnerAboutNewVersion implements ShouldQueue
return;
}
try {
$owner = new OwnerNotifiable();
Notification::send($owner, new VersionCheckResult($event->message));
} catch (Exception $e) {
$message = $e->getMessage();
if (str_contains($message, 'Bcc')) {
Log::warning('[Bcc] Could not send notification. Please validate your email settings, use the .env.example file as a guide.');
return;
}
if (str_contains($message, 'RFC 2822')) {
Log::warning('[RFC] Could not send notification. Please validate your email settings, use the .env.example file as a guide.');
return;
}
Log::error($e->getMessage());
Log::error($e->getTraceAsString());
}
$owner = new OwnerNotifiable();
NotificationSender::send($owner, new VersionCheckResult($event->message));
}
}

View File

@@ -24,35 +24,17 @@ declare(strict_types=1);
namespace FireflyIII\Listeners\Security\System;
use Exception;
use FireflyIII\Events\Security\System\UnknownUserTriedLogin;
use FireflyIII\Notifications\Admin\UnknownUserLoginAttempt;
use FireflyIII\Notifications\Notifiables\OwnerNotifiable;
use FireflyIII\Notifications\NotificationSender;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Notification;
class NotifiesOwnerAboutUnknownUser implements ShouldQueue
{
public function handle(UnknownUserTriedLogin $event): void
{
try {
$owner = new OwnerNotifiable();
Notification::send($owner, new UnknownUserLoginAttempt($event->address));
} catch (Exception $e) {
$message = $e->getMessage();
if (str_contains($message, 'Bcc')) {
Log::warning('[Bcc] Could not send notification. Please validate your email settings, use the .env.example file as a guide.');
return;
}
if (str_contains($message, 'RFC 2822')) {
Log::warning('[RFC] Could not send notification. Please validate your email settings, use the .env.example file as a guide.');
return;
}
Log::error($e->getMessage());
Log::error($e->getTraceAsString());
}
$owner = new OwnerNotifiable();
NotificationSender::send($owner, new UnknownUserLoginAttempt($event->address));
}
}

View File

@@ -24,37 +24,17 @@ declare(strict_types=1);
namespace FireflyIII\Listeners\Security\User;
use Exception;
use FireflyIII\Events\Security\User\UserHasDisabledMFA;
use FireflyIII\Notifications\NotificationSender;
use FireflyIII\Notifications\Security\DisabledMFANotification;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Notification;
class NotifiesUserAboutDisabledMFA implements ShouldQueue
{
public function handle(UserHasDisabledMFA $event): void
{
Log::debug(sprintf('Now in %s', __METHOD__));
$user = $event->user;
try {
Notification::send($user, new DisabledMFANotification($user));
} catch (Exception $e) {
$message = $e->getMessage();
if (str_contains($message, 'Bcc')) {
Log::warning('[Bcc] Could not send notification. Please validate your email settings, use the .env.example file as a guide.');
return;
}
if (str_contains($message, 'RFC 2822')) {
Log::warning('[RFC] Could not send notification. Please validate your email settings, use the .env.example file as a guide.');
return;
}
Log::error($e->getMessage());
Log::error($e->getTraceAsString());
}
NotificationSender::send($event->user, new DisabledMFANotification($event->user));
}
}

View File

@@ -24,12 +24,11 @@ declare(strict_types=1);
namespace FireflyIII\Listeners\Security\User;
use Exception;
use FireflyIII\Events\Security\User\UserHasEnabledMFA;
use FireflyIII\Notifications\NotificationSender;
use FireflyIII\Notifications\Security\EnabledMFANotification;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Notification;
class NotifiesUserAboutEnabledMFA implements ShouldQueue
{
@@ -38,23 +37,6 @@ class NotifiesUserAboutEnabledMFA implements ShouldQueue
Log::debug(sprintf('Now in %s', __METHOD__));
$user = $event->user;
try {
Notification::send($user, new EnabledMFANotification($user));
} catch (Exception $e) {
$message = $e->getMessage();
if (str_contains($message, 'Bcc')) {
Log::warning('[Bcc] Could not send notification. Please validate your email settings, use the .env.example file as a guide.');
return;
}
if (str_contains($message, 'RFC 2822')) {
Log::warning('[RFC] Could not send notification. Please validate your email settings, use the .env.example file as a guide.');
return;
}
Log::error($e->getMessage());
Log::error($e->getTraceAsString());
}
NotificationSender::send($user, new EnabledMFANotification($user));
}
}

View File

@@ -24,32 +24,15 @@ declare(strict_types=1);
namespace FireflyIII\Listeners\Security\User;
use Exception;
use FireflyIII\Events\Security\User\UserFailedLoginAttempt;
use FireflyIII\Notifications\NotificationSender;
use FireflyIII\Notifications\Security\UserFailedLoginAttempt as NotificationFailedLoginAttempt;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Notification;
class NotifiesUserAboutFailedLogin implements ShouldQueue
{
public function handle(UserFailedLoginAttempt $event): void
{
try {
Notification::send($event->user, new \FireflyIII\Notifications\Security\UserFailedLoginAttempt($event->user));
} catch (Exception $e) {
$message = $e->getMessage();
if (str_contains($message, 'Bcc')) {
Log::warning('[Bcc] Could not send notification. Please validate your email settings, use the .env.example file as a guide.');
return;
}
if (str_contains($message, 'RFC 2822')) {
Log::warning('[RFC] Could not send notification. Please validate your email settings, use the .env.example file as a guide.');
return;
}
Log::error($e->getMessage());
Log::error($e->getTraceAsString());
}
NotificationSender::send($event->user, new NotificationFailedLoginAttempt($event->user));
}
}

View File

@@ -24,12 +24,11 @@ declare(strict_types=1);
namespace FireflyIII\Listeners\Security\User;
use Exception;
use FireflyIII\Events\Security\User\UserHasFewMFABackupCodesLeft;
use FireflyIII\Notifications\NotificationSender;
use FireflyIII\Notifications\Security\MFABackupFewLeftNotification;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Notification;
class NotifiesUserAboutFewCodesLeft implements ShouldQueue
{
@@ -39,23 +38,6 @@ class NotifiesUserAboutFewCodesLeft implements ShouldQueue
$user = $event->user;
$count = $event->count;
try {
Notification::send($user, new MFABackupFewLeftNotification($user, $count));
} catch (Exception $e) {
$message = $e->getMessage();
if (str_contains($message, 'Bcc')) {
Log::warning('[Bcc] Could not send notification. Please validate your email settings, use the .env.example file as a guide.');
return;
}
if (str_contains($message, 'RFC 2822')) {
Log::warning('[RFC] Could not send notification. Please validate your email settings, use the .env.example file as a guide.');
return;
}
Log::error($e->getMessage());
Log::error($e->getTraceAsString());
}
NotificationSender::send($user, new MFABackupFewLeftNotification($user, $count));
}
}

View File

@@ -0,0 +1,44 @@
<?php
declare(strict_types=1);
/*
* NotifiesUserAboutNewAccessToken.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\Security\User;
use FireflyIII\Notifications\NotificationSender;
use FireflyIII\Notifications\User\NewAccessToken;
use FireflyIII\Repositories\User\UserRepositoryInterface;
use Laravel\Passport\Events\AccessTokenCreated;
class NotifiesUserAboutNewAccessToken
{
public function handle(AccessTokenCreated $event): void
{
/** @var UserRepositoryInterface $repository */
$repository = app(UserRepositoryInterface::class);
$user = $repository->find((int) $event->userId);
if (null !== $user) {
NotificationSender::send($user, new NewAccessToken());
}
}
}

View File

@@ -24,37 +24,18 @@ declare(strict_types=1);
namespace FireflyIII\Listeners\Security\User;
use Exception;
use FireflyIII\Events\Security\User\UserHasGeneratedNewBackupCodes;
use FireflyIII\Notifications\NotificationSender;
use FireflyIII\Notifications\Security\NewBackupCodesNotification;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Notification;
class NotifiesUserAboutNewBackupCodes implements ShouldQueue
{
public function handle(UserHasGeneratedNewBackupCodes $event): void
{
Log::debug(sprintf('Now in %s', __METHOD__));
$user = $event->user;
try {
Notification::send($user, new NewBackupCodesNotification($user));
} catch (Exception $e) {
$message = $e->getMessage();
if (str_contains($message, 'Bcc')) {
Log::warning('[Bcc] Could not send notification. Please validate your email settings, use the .env.example file as a guide.');
return;
}
if (str_contains($message, 'RFC 2822')) {
Log::warning('[RFC] Could not send notification. Please validate your email settings, use the .env.example file as a guide.');
return;
}
Log::error($e->getMessage());
Log::error($e->getTraceAsString());
}
NotificationSender::send($user, new NewBackupCodesNotification($user));
}
}

View File

@@ -24,13 +24,11 @@ declare(strict_types=1);
namespace FireflyIII\Listeners\Security\User;
use Exception;
use FireflyIII\Events\Security\User\UserLoggedInFromNewIpAddress;
use FireflyIII\Notifications\NotificationSender;
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 implements ShouldQueue
{
@@ -50,23 +48,7 @@ class NotifiesUserAboutNewIpAddress implements ShouldQueue
/** @var array $entry */
foreach ($list as $index => $entry) {
if (false === $entry['notified']) {
try {
Notification::send($user, new UserLogin());
} catch (Exception $e) {
$message = $e->getMessage();
if (str_contains($message, 'Bcc')) {
Log::warning('[Bcc] Could not send notification. Please validate your email settings, use the .env.example file as a guide.');
return;
}
if (str_contains($message, 'RFC 2822')) {
Log::warning('[RFC] Could not send notification. Please validate your email settings, use the .env.example file as a guide.');
return;
}
Log::error($e->getMessage());
Log::error($e->getTraceAsString());
}
NotificationSender::send($user, new UserLogin());
}
$list[$index]['notified'] = true;
}

View File

@@ -24,12 +24,11 @@ declare(strict_types=1);
namespace FireflyIII\Listeners\Security\User;
use Exception;
use FireflyIII\Events\Security\User\UserHasNoMFABackupCodesLeft;
use FireflyIII\Notifications\NotificationSender;
use FireflyIII\Notifications\Security\MFABackupNoLeftNotification;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Notification;
class NotifiesUserAboutNoCodesLeft implements ShouldQueue
{
@@ -39,22 +38,6 @@ class NotifiesUserAboutNoCodesLeft implements ShouldQueue
$user = $event->user;
try {
Notification::send($user, new MFABackupNoLeftNotification($user));
} catch (Exception $e) {
$message = $e->getMessage();
if (str_contains($message, 'Bcc')) {
Log::warning('[Bcc] Could not send notification. Please validate your email settings, use the .env.example file as a guide.');
return;
}
if (str_contains($message, 'RFC 2822')) {
Log::warning('[RFC] Could not send notification. Please validate your email settings, use the .env.example file as a guide.');
return;
}
Log::error($e->getMessage());
Log::error($e->getTraceAsString());
}
NotificationSender::send($user, new MFABackupNoLeftNotification($user));
}
}

View File

@@ -24,12 +24,11 @@ declare(strict_types=1);
namespace FireflyIII\Listeners\Security\User;
use Exception;
use FireflyIII\Events\Security\User\UserKeepsFailingMFA;
use FireflyIII\Notifications\NotificationSender;
use FireflyIII\Notifications\Security\MFAManyFailedAttemptsNotification;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Notification;
class NotifiesUserAboutRepeatedMFAFailures implements ShouldQueue
{
@@ -39,23 +38,6 @@ class NotifiesUserAboutRepeatedMFAFailures implements ShouldQueue
$user = $event->user;
$count = $event->count;
try {
Notification::send($user, new MFAManyFailedAttemptsNotification($user, $count));
} catch (Exception $e) {
$message = $e->getMessage();
if (str_contains($message, 'Bcc')) {
Log::warning('[Bcc] Could not send notification. Please validate your email settings, use the .env.example file as a guide.');
return;
}
if (str_contains($message, 'RFC 2822')) {
Log::warning('[RFC] Could not send notification. Please validate your email settings, use the .env.example file as a guide.');
return;
}
Log::error($e->getMessage());
Log::error($e->getTraceAsString());
}
NotificationSender::send($user, new MFAManyFailedAttemptsNotification($user, $count));
}
}

View File

@@ -24,12 +24,11 @@ declare(strict_types=1);
namespace FireflyIII\Listeners\Security\User;
use Exception;
use FireflyIII\Events\Security\User\UserHasUsedBackupCode;
use FireflyIII\Notifications\NotificationSender;
use FireflyIII\Notifications\Security\MFAUsedBackupCodeNotification;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Notification;
class NotifiesUserAboutUsedBackupCode implements ShouldQueue
{
@@ -38,23 +37,6 @@ class NotifiesUserAboutUsedBackupCode implements ShouldQueue
Log::debug(sprintf('Now in %s', __METHOD__));
$user = $event->user;
try {
Notification::send($user, new MFAUsedBackupCodeNotification($user));
} catch (Exception $e) {
$message = $e->getMessage();
if (str_contains($message, 'Bcc')) {
Log::warning('[Bcc] Could not send notification. Please validate your email settings, use the .env.example file as a guide.');
return;
}
if (str_contains($message, 'RFC 2822')) {
Log::warning('[RFC] Could not send notification. Please validate your email settings, use the .env.example file as a guide.');
return;
}
Log::error($e->getMessage());
Log::error($e->getTraceAsString());
}
NotificationSender::send($user, new MFAUsedBackupCodeNotification($user));
}
}

View File

@@ -24,33 +24,15 @@ declare(strict_types=1);
namespace FireflyIII\Listeners\Security\User;
use Exception;
use FireflyIII\Events\Security\User\UserRequestedNewPassword;
use FireflyIII\Notifications\NotificationSender;
use FireflyIII\Notifications\User\UserNewPassword;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Notification;
class SendsUserNewPassword implements ShouldQueue
{
public function handle(UserRequestedNewPassword $event): void
{
try {
Notification::send($event->user, new UserNewPassword(route('password.reset', [$event->token])));
} catch (Exception $e) {
$message = $e->getMessage();
if (str_contains($message, 'Bcc')) {
Log::warning('[Bcc] Could not send notification. Please validate your email settings, use the .env.example file as a guide.');
return;
}
if (str_contains($message, 'RFC 2822')) {
Log::warning('[RFC] Could not send notification. Please validate your email settings, use the .env.example file as a guide.');
return;
}
Log::error($e->getMessage());
Log::error($e->getTraceAsString());
}
NotificationSender::send($event->user, new UserNewPassword(route('password.reset', [$event->token])));
}
}

View File

@@ -1,8 +1,10 @@
<?php
declare(strict_types=1);
/*
* PreferencesEventHandler.php
* Copyright (c) 2024 james@firefly-iii.org.
* RecalculatesPrimaryCurrencyAmounts.php
* Copyright (c) 2026 james@firefly-iii.org
*
* This file is part of Firefly III (https://github.com/firefly-iii).
*
@@ -17,12 +19,10 @@
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see https://www.gnu.org/licenses/.
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
declare(strict_types=1);
namespace FireflyIII\Handlers\Events;
namespace FireflyIII\Listeners\System;
use FireflyIII\Events\Preferences\UserGroupChangedPrimaryCurrency;
use FireflyIII\Models\Budget;
@@ -30,15 +30,15 @@ use FireflyIII\Models\PiggyBank;
use FireflyIII\Models\UserGroup;
use FireflyIII\Repositories\Budget\BudgetRepositoryInterface;
use FireflyIII\Repositories\PiggyBank\PiggyBankRepositoryInterface;
use FireflyIII\Services\Internal\Recalculate\PrimaryAmountRecalculationService;
use FireflyIII\Support\Facades\Amount;
use Illuminate\Database\Query\Builder;
use Illuminate\Support\Facades\Artisan;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
class PreferencesEventHandler
class RecalculatesPrimaryCurrencyAmounts
{
public function resetPrimaryCurrencyAmounts(UserGroupChangedPrimaryCurrency $event): void
public function handle(UserGroupChangedPrimaryCurrency $event): void
{
// Reset the primary currency amounts for all objects that have it.
Log::debug('Resetting primary currency amounts for all objects.');
@@ -50,79 +50,67 @@ class PreferencesEventHandler
'bills' => ['native_amount_min', 'native_amount_max'],
];
foreach ($tables as $table => $columns) {
Log::debug(sprintf('Now processing table "%s"', $table));
foreach ($columns as $column) {
Log::debug(sprintf('Resetting column %s in table %s.', $column, $table));
Log::debug(sprintf('Resetting column "%s" in table "%s".', $column, $table));
DB::table($table)->where('user_group_id', $event->userGroup->id)->update([$column => null]);
}
}
$this->resetPiggyBanks($event->userGroup);
$this->resetBudgets($event->userGroup);
$this->resetTransactions($event->userGroup);
Log::debug('Have now reset all primary amounts to NULL.');
// fire laravel command to recalculate them all.
if (Amount::convertToPrimary()) {
Log::debug('Will now convert to primary currency.');
Artisan::call('correction:recalculate-pc-amounts');
Log::debug('Will now convert amounts to primary currency.');
$calculator = new PrimaryAmountRecalculationService();
$calculator->recalculate();
return;
}
Log::debug('Will NOT convert to primary currency.');
}
private function resetPiggyBanks(UserGroup $userGroup): void
private function resetBudget(Budget $budget): void
{
$repository = app(PiggyBankRepositoryInterface::class);
$repository->setUserGroup($userGroup);
$piggyBanks = $repository->getPiggyBanks();
Log::debug(sprintf('Resetting %d piggy bank(s).', $piggyBanks->count()));
/** @var PiggyBank $piggyBank */
foreach ($piggyBanks as $piggyBank) {
if (null !== $piggyBank->native_target_amount) {
Log::debug(sprintf('Resetting native_target_amount for piggy bank #%d.', $piggyBank->id));
$piggyBank->native_target_amount = null;
$piggyBank->saveQuietly();
foreach ($budget->autoBudgets as $autoBudget) {
if ('' === (string) $autoBudget->native_amount) {
continue;
}
foreach ($piggyBank->accounts as $account) {
if (null !== $account->pivot->native_current_amount) {
Log::debug(sprintf('Resetting native_current_amount for piggy bank #%d and account #%d.', $piggyBank->id, $account->id));
$account->pivot->native_current_amount = null;
$account->pivot->save();
}
}
foreach ($piggyBank->piggyBankEvents as $event) {
if (null !== $event->native_amount) {
Log::debug(sprintf('Resetting native_amount for piggy bank #%d and event #%d.', $piggyBank->id, $event->id));
$event->native_amount = null;
$event->saveQuietly();
}
Log::debug(sprintf('Resetting native_amount for budget #%d and auto budget #%d.', $budget->id, $autoBudget->id));
$autoBudget->native_amount = null;
$autoBudget->saveQuietly();
}
foreach ($budget->budgetlimits as $limit) {
if ('' !== (string) $limit->native_amount) {
Log::debug(sprintf('Resetting native_amount for budget #%d and budget limit #%d.', $budget->id, $limit->id));
$limit->native_amount = null;
$limit->saveQuietly();
}
}
}
private function resetBudgets(UserGroup $userGroup): void
private function resetPiggyBank(PiggyBank $piggyBank): void
{
$repository = app(BudgetRepositoryInterface::class);
$repository->setUserGroup($userGroup);
$set = $repository->getBudgets();
Log::debug(sprintf('Resetting %d budget(s).', $set->count()));
/** @var Budget $budget */
foreach ($set as $budget) {
foreach ($budget->autoBudgets as $autoBudget) {
if (null === $autoBudget->native_amount) {
continue;
}
Log::debug(sprintf('Resetting native_amount for budget #%d and auto budget #%d.', $budget->id, $autoBudget->id));
$autoBudget->native_amount = null;
$autoBudget->saveQuietly();
if ('' !== (string) $piggyBank->native_target_amount) {
Log::debug(sprintf('Resetting native_target_amount for piggy bank #%d.', $piggyBank->id));
$piggyBank->native_target_amount = null;
$piggyBank->saveQuietly();
}
foreach ($piggyBank->accounts as $account) {
if ('' !== (string) $account->pivot->native_current_amount) {
Log::debug(sprintf('Resetting native_current_amount for piggy bank #%d and account #%d.', $piggyBank->id, $account->id));
$account->pivot->native_current_amount = null;
$account->pivot->save();
}
foreach ($budget->budgetlimits as $limit) {
if (null !== $limit->native_amount) {
Log::debug(sprintf('Resetting native_amount for budget #%d and budget limit #%d.', $budget->id, $limit->id));
$limit->native_amount = null;
$limit->saveQuietly();
}
}
foreach ($piggyBank->piggyBankEvents as $event) {
if ('' !== (string) $event->native_amount) {
Log::debug(sprintf('Resetting native_amount for piggy bank #%d and event #%d.', $piggyBank->id, $event->id));
$event->native_amount = null;
$event->saveQuietly();
}
}
}
@@ -134,10 +122,42 @@ class PreferencesEventHandler
->join('transaction_journals', 'transaction_journals.id', '=', 'transactions.transaction_journal_id')
->where('transaction_journals.user_group_id', $userGroup->id)
->where(static function (Builder $q): void {
$q->whereNotNull('native_amount')->orWhereNotNull('native_foreign_amount');
$q
->whereNotNull('native_amount')
->orWhereNotNull('native_foreign_amount')
->orWhere('native_amount', '!=', '')
->orWhere('native_foreign_amount', '!=', '')
;
})
->update(['native_amount' => null, 'native_foreign_amount' => null])
;
Log::debug(sprintf('Reset %d transactions.', $success));
}
private function resetPiggyBanks(UserGroup $userGroup): void
{
$repository = app(PiggyBankRepositoryInterface::class);
$repository->setUserGroup($userGroup);
$piggyBanks = $repository->getPiggyBanks();
Log::debug(sprintf('Reset primary currency of %d piggy bank(s).', $piggyBanks->count()));
/** @var PiggyBank $piggyBank */
foreach ($piggyBanks as $piggyBank) {
$this->resetPiggyBank($piggyBank);
}
}
private function resetBudgets(UserGroup $userGroup): void
{
$repository = app(BudgetRepositoryInterface::class);
$repository->setUserGroup($userGroup);
$set = $repository->getBudgets();
Log::debug(sprintf('Reset primary currency of %d budget(s).', $set->count()));
/** @var Budget $budget */
foreach ($set as $budget) {
$this->resetBudget($budget);
}
}
}

View File

@@ -24,9 +24,9 @@ declare(strict_types=1);
namespace FireflyIII\Listeners\Test;
use Exception;
use FireflyIII\Events\Test\OwnerTestsNotificationChannel;
use FireflyIII\Events\Test\UserTestsNotificationChannel;
use FireflyIII\Notifications\NotificationSender;
use FireflyIII\Notifications\Test\OwnerTestNotificationEmail;
use FireflyIII\Notifications\Test\OwnerTestNotificationPushover;
use FireflyIII\Notifications\Test\OwnerTestNotificationSlack;
@@ -34,7 +34,6 @@ use FireflyIII\Notifications\Test\UserTestNotificationEmail;
use FireflyIII\Notifications\Test\UserTestNotificationPushover;
use FireflyIII\Notifications\Test\UserTestNotificationSlack;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Notification;
class SendsTestNotification
{
@@ -82,24 +81,7 @@ class SendsTestNotification
return;
}
Log::debug(sprintf('Will send %s as a notification.', $class));
try {
Notification::send($event->user, new $class());
} catch (Exception $e) {
$message = $e->getMessage();
if (str_contains($message, 'Bcc')) {
Log::warning('[Bcc] Could not send notification. Please validate your email settings, use the .env.example file as a guide.');
return;
}
if (str_contains($message, 'RFC 2822')) {
Log::warning('[RFC] Could not send notification. Please validate your email settings, use the .env.example file as a guide.');
return;
}
Log::error($e->getMessage());
Log::error($e->getTraceAsString());
}
NotificationSender::send($event->user, new $class());
Log::debug(sprintf('If you see no errors above this line, test notification was sent over channel "%s"', $event->channel));
}
}

View File

@@ -24,7 +24,7 @@ declare(strict_types=1);
namespace FireflyIII\Models;
use FireflyIII\Enums\AccountTypeEnum;
use FireflyIII\Handlers\Observer\AccountObserver;
use FireflyIII\Handlers\Observer\DeletedAccountObserver;
use FireflyIII\Support\Models\ReturnsIntegerIdTrait;
use FireflyIII\Support\Models\ReturnsIntegerUserIdTrait;
use FireflyIII\User;
@@ -42,7 +42,7 @@ use Illuminate\Database\Eloquent\Relations\MorphToMany;
use Illuminate\Database\Eloquent\SoftDeletes;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
#[ObservedBy([AccountObserver::class])]
#[ObservedBy([DeletedAccountObserver::class])]
class Account extends Model
{
use HasFactory;

View File

@@ -1,52 +0,0 @@
<?php
/*
* AccountBalance.php
* Copyright (c) 2025 james@firefly-iii.org.
*
* This file is part of Firefly III (https://github.com/firefly-iii).
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see https://www.gnu.org/licenses/.
*/
declare(strict_types=1);
namespace FireflyIII\Models;
use FireflyIII\Casts\SeparateTimezoneCaster;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class AccountBalance extends Model
{
use HasFactory;
protected $fillable = ['account_id', 'title', 'transaction_currency_id', 'balance', 'date', 'date_tz'];
public function account(): BelongsTo
{
return $this->belongsTo(Account::class);
}
public function transactionCurrency(): BelongsTo
{
return $this->belongsTo(TransactionCurrency::class);
}
protected function casts(): array
{
return ['date' => SeparateTimezoneCaster::class, 'balance' => 'string'];
}
}

View File

@@ -23,7 +23,7 @@ declare(strict_types=1);
namespace FireflyIII\Models;
use FireflyIII\Handlers\Observer\AttachmentObserver;
use FireflyIII\Handlers\Observer\DeletedAttachmentObserver;
use FireflyIII\Support\Models\ReturnsIntegerIdTrait;
use FireflyIII\Support\Models\ReturnsIntegerUserIdTrait;
use FireflyIII\User;
@@ -36,7 +36,7 @@ use Illuminate\Database\Eloquent\Relations\MorphTo;
use Illuminate\Database\Eloquent\SoftDeletes;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
#[ObservedBy([AttachmentObserver::class])]
#[ObservedBy([DeletedAttachmentObserver::class])]
class Attachment extends Model
{
use ReturnsIntegerIdTrait;

View File

@@ -24,7 +24,7 @@ declare(strict_types=1);
namespace FireflyIII\Models;
use FireflyIII\Handlers\Observer\CategoryObserver;
use FireflyIII\Handlers\Observer\DeletedCategoryObserver;
use FireflyIII\Support\Models\ReturnsIntegerIdTrait;
use FireflyIII\Support\Models\ReturnsIntegerUserIdTrait;
use FireflyIII\User;
@@ -36,7 +36,7 @@ use Illuminate\Database\Eloquent\Relations\MorphMany;
use Illuminate\Database\Eloquent\SoftDeletes;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
#[ObservedBy([CategoryObserver::class])]
#[ObservedBy([DeletedCategoryObserver::class])]
class Category extends Model
{
use ReturnsIntegerIdTrait;

View File

@@ -26,7 +26,7 @@ namespace FireflyIII\Models;
use Carbon\Carbon;
use FireflyIII\Casts\SeparateTimezoneCaster;
use FireflyIII\Handlers\Observer\RecurrenceObserver;
use FireflyIII\Handlers\Observer\DeletedRecurrenceObserver;
use FireflyIII\Support\Models\ReturnsIntegerIdTrait;
use FireflyIII\Support\Models\ReturnsIntegerUserIdTrait;
use FireflyIII\User;
@@ -43,7 +43,7 @@ use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
* @property Carbon $first_date
* @property null|Carbon $latest_date
*/
#[ObservedBy([RecurrenceObserver::class])]
#[ObservedBy([DeletedRecurrenceObserver::class])]
class Recurrence extends Model
{
use ReturnsIntegerIdTrait;

View File

@@ -24,7 +24,7 @@ declare(strict_types=1);
namespace FireflyIII\Models;
use FireflyIII\Handlers\Observer\RecurrenceTransactionObserver;
use FireflyIII\Handlers\Observer\DeletedRecurrenceTransactionObserver;
use FireflyIII\Support\Models\ReturnsIntegerIdTrait;
use Illuminate\Database\Eloquent\Attributes\ObservedBy;
use Illuminate\Database\Eloquent\Casts\Attribute;
@@ -33,7 +33,7 @@ use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\SoftDeletes;
#[ObservedBy([RecurrenceTransactionObserver::class])]
#[ObservedBy([DeletedRecurrenceTransactionObserver::class])]
class RecurrenceTransaction extends Model
{
use ReturnsIntegerIdTrait;

View File

@@ -23,7 +23,7 @@ declare(strict_types=1);
namespace FireflyIII\Models;
use FireflyIII\Handlers\Observer\RuleObserver;
use FireflyIII\Handlers\Observer\DeletedRuleObserver;
use FireflyIII\Support\Models\ReturnsIntegerIdTrait;
use FireflyIII\Support\Models\ReturnsIntegerUserIdTrait;
use FireflyIII\User;
@@ -38,7 +38,7 @@ use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
/**
* @property User $user
*/
#[ObservedBy([RuleObserver::class])]
#[ObservedBy([DeletedRuleObserver::class])]
class Rule extends Model
{
use ReturnsIntegerIdTrait;

View File

@@ -23,7 +23,7 @@ declare(strict_types=1);
namespace FireflyIII\Models;
use FireflyIII\Handlers\Observer\RuleGroupObserver;
use FireflyIII\Handlers\Observer\DeletedRuleGroupObserver;
use FireflyIII\Support\Models\ReturnsIntegerIdTrait;
use FireflyIII\Support\Models\ReturnsIntegerUserIdTrait;
use FireflyIII\User;
@@ -40,7 +40,7 @@ use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
* @property User $user
* @property Collection $rules
*/
#[ObservedBy([RuleGroupObserver::class])]
#[ObservedBy([DeletedRuleGroupObserver::class])]
class RuleGroup extends Model
{
use ReturnsIntegerIdTrait;

View File

@@ -24,7 +24,7 @@ declare(strict_types=1);
namespace FireflyIII\Models;
use FireflyIII\Casts\SeparateTimezoneCaster;
use FireflyIII\Handlers\Observer\TagObserver;
use FireflyIII\Handlers\Observer\DeletedTagObserver;
use FireflyIII\Support\Models\ReturnsIntegerIdTrait;
use FireflyIII\Support\Models\ReturnsIntegerUserIdTrait;
use FireflyIII\User;
@@ -36,7 +36,7 @@ use Illuminate\Database\Eloquent\Relations\MorphMany;
use Illuminate\Database\Eloquent\SoftDeletes;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
#[ObservedBy([TagObserver::class])]
#[ObservedBy([DeletedTagObserver::class])]
class Tag extends Model
{
use ReturnsIntegerIdTrait;

View File

@@ -23,7 +23,7 @@ declare(strict_types=1);
namespace FireflyIII\Models;
use FireflyIII\Handlers\Observer\TransactionGroupObserver;
use FireflyIII\Handlers\Observer\DeletedTransactionGroupObserver;
use FireflyIII\Support\Models\ReturnsIntegerIdTrait;
use FireflyIII\Support\Models\ReturnsIntegerUserIdTrait;
use FireflyIII\User;
@@ -41,7 +41,7 @@ use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
* @property UserGroup $userGroup
* @property Collection<TransactionJournal> $transactionJournals
*/
#[ObservedBy([TransactionGroupObserver::class])]
#[ObservedBy([DeletedTransactionGroupObserver::class])]
class TransactionGroup extends Model
{
use ReturnsIntegerIdTrait;

View File

@@ -26,7 +26,7 @@ namespace FireflyIII\Models;
use Carbon\Carbon;
use FireflyIII\Casts\SeparateTimezoneCaster;
use FireflyIII\Enums\TransactionTypeEnum;
use FireflyIII\Handlers\Observer\TransactionJournalObserver;
use FireflyIII\Handlers\Observer\DeletedTransactionJournalObserver;
use FireflyIII\Support\Models\ReturnsIntegerIdTrait;
use FireflyIII\Support\Models\ReturnsIntegerUserIdTrait;
use FireflyIII\User;
@@ -50,7 +50,7 @@ use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
*
* @property TransactionGroup $transactionGroup
*/
#[ObservedBy([TransactionJournalObserver::class])]
#[ObservedBy([DeletedTransactionJournalObserver::class])]
class TransactionJournal extends Model
{
use HasFactory;

View File

@@ -27,7 +27,7 @@ namespace FireflyIII\Models;
use FireflyIII\Enums\WebhookDelivery as WebhookDeliveryEnum;
use FireflyIII\Enums\WebhookResponse as WebhookResponseEnum;
use FireflyIII\Enums\WebhookTrigger as WebhookTriggerEnum;
use FireflyIII\Handlers\Observer\WebhookObserver;
use FireflyIII\Handlers\Observer\DeletedWebhookObserver;
use FireflyIII\Support\Models\ReturnsIntegerIdTrait;
use FireflyIII\Support\Models\ReturnsIntegerUserIdTrait;
use FireflyIII\User;
@@ -39,7 +39,7 @@ use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\SoftDeletes;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
#[ObservedBy([WebhookObserver::class])]
#[ObservedBy([DeletedWebhookObserver::class])]
class Webhook extends Model
{
use ReturnsIntegerIdTrait;

View File

@@ -24,7 +24,7 @@ declare(strict_types=1);
namespace FireflyIII\Models;
use FireflyIII\Handlers\Observer\WebhookMessageObserver;
use FireflyIII\Handlers\Observer\DeletedWebhookMessageObserver;
use FireflyIII\Support\Models\ReturnsIntegerIdTrait;
use FireflyIII\User;
use Illuminate\Database\Eloquent\Attributes\ObservedBy;
@@ -34,7 +34,7 @@ use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
#[ObservedBy([WebhookMessageObserver::class])]
#[ObservedBy([DeletedWebhookMessageObserver::class])]
class WebhookMessage extends Model
{
use ReturnsIntegerIdTrait;

View File

@@ -0,0 +1,59 @@
<?php
declare(strict_types=1);
/*
* NotificationSender.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\Notifications;
use Exception;
use FireflyIII\Notifications\Notifiables\OwnerNotifiable;
use FireflyIII\User;
use GuzzleHttp\Exception\ClientException;
use Illuminate\Notifications\Notification;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Notification as NotificationFacade;
class NotificationSender
{
public static function send(OwnerNotifiable|User $user, Notification $notification): void
{
try {
NotificationFacade::send($user, $notification);
} catch (ClientException $e) {
Log::error(sprintf('[a] Error sending notification: %s', $e->getMessage()));
} catch (Exception $e) {
$message = $e->getMessage();
if (str_contains($message, 'Bcc')) {
Log::warning('[Bcc] Could not send notification. Please validate your email settings, use the .env.example file as a guide.');
return;
}
if (str_contains($message, 'RFC 2822')) {
Log::warning('[RFC] Could not send notification. Please validate your email settings, use the .env.example file as a guide.');
return;
}
Log::error($e->getMessage());
Log::error($e->getTraceAsString());
}
}
}

View File

@@ -1,49 +0,0 @@
<?php
/*
* AccountPolicy.php
* Copyright (c) 2024 james@firefly-iii.org.
*
* This file is part of Firefly III (https://github.com/firefly-iii).
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see https://www.gnu.org/licenses/.
*/
declare(strict_types=1);
namespace FireflyIII\Policies;
use FireflyIII\Entities\AccountBalance;
use FireflyIII\User;
class AccountBalancePolicy
{
/**
* TODO needs better authentication.
*/
public function view(User $user, AccountBalance $accountBalance): bool
{
return true;
}
/**
* Everybody can do this, but selection should limit to user.
*
* @return true
*/
public function viewAny(): bool
{
return true;
}
}

View File

@@ -23,14 +23,7 @@ declare(strict_types=1);
namespace FireflyIII\Providers;
use FireflyIII\Events\DestroyedTransactionGroup;
use FireflyIII\Events\Model\TransactionGroup\TriggeredStoredTransactionGroup;
use FireflyIII\Events\Preferences\UserGroupChangedPrimaryCurrency;
use FireflyIII\Events\StoredAccount;
use FireflyIII\Events\UpdatedAccount;
use FireflyIII\Events\UpdatedTransactionGroup;
use Illuminate\Foundation\Support\Providers\EventServiceProvider as ServiceProvider;
use Laravel\Passport\Events\AccessTokenCreated;
use Override;
/**

View File

@@ -262,6 +262,12 @@ class JournalRepository implements JournalRepositoryInterface, UserGroupInterfac
;
}
#[Override]
public function getAllUncompletedJournals(): Collection
{
return TransactionJournal::where('completed', false)->get(['transaction_journals.*']);
}
#[Override]
public function markAsCompleted(Collection $set): void
{

View File

@@ -54,6 +54,8 @@ interface JournalRepositoryInterface
public function getUncompletedJournals(): Collection;
public function getAllUncompletedJournals(): Collection;
public function markAsCompleted(Collection $set): void;
/**

View File

@@ -187,9 +187,11 @@ class PeriodStatisticRepository implements PeriodStatisticRepositoryInterface, U
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.*']);
Log::debug('Collected transaction IDs', $transactions->pluck('id')->toArray());
// collect all accounts and delete stats:
$accounts = Account::whereIn('id', $transactions->pluck('account_id')->toArray())->get(['accounts.*']);
Log::debug('Collected account IDs', $accounts->pluck('id')->toArray());
$dates = $set->pluck('date');
$this->deleteStatisticsForType(Account::class, $accounts, $dates);
@@ -202,6 +204,7 @@ class PeriodStatisticRepository implements PeriodStatisticRepositoryInterface, U
->pluck('category_id')
->toArray()
)->get(['categories.*']);
Log::debug('Collected category IDs', $categories->pluck('id')->toArray());
$this->deleteStatisticsForType(Category::class, $categories, $dates);
// budgets, same thing
@@ -213,6 +216,7 @@ class PeriodStatisticRepository implements PeriodStatisticRepositoryInterface, U
->pluck('budget_id')
->toArray()
)->get(['budgets.*']);
Log::debug('Collected budget IDs', $categories->pluck('id')->toArray());
$this->deleteStatisticsForType(Budget::class, $budgets, $dates);
// tags
@@ -224,16 +228,20 @@ class PeriodStatisticRepository implements PeriodStatisticRepositoryInterface, U
->pluck('tag_id')
->toArray()
)->get(['tags.*']);
Log::debug('Collected tag IDs', $categories->pluck('id')->toArray());
$this->deleteStatisticsForType(Tag::class, $tags, $dates);
// remove for no tag, no cat, etc.
if (0 === $categories->count()) {
Log::debug('No categories, delete "no_category" stats.');
$this->deleteStatisticsForPrefix('no_category', $dates);
}
if (0 === $budgets->count()) {
Log::debug('No budgets, delete "no_category" stats.');
$this->deleteStatisticsForPrefix('no_budget', $dates);
}
if (0 === $tags->count()) {
Log::debug('No tags, delete "no_category" stats.');
$this->deleteStatisticsForPrefix('no_tag', $dates);
}
}

View File

@@ -51,6 +51,7 @@ use Illuminate\Database\Eloquent\Builder;
use Illuminate\Pagination\LengthAwarePaginator;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Log;
use Override;
use function Safe\json_decode;
use function Safe\json_encode;
@@ -594,4 +595,14 @@ class RecurringRepository implements RecurringRepositoryInterface, UserGroupInte
}
}
}
#[Override]
public function setLatestDate(Recurrence $recurrence, ?Carbon $date): Recurrence
{
$recurrence->latest_date = $date;
$recurrence->latest_date_tz = $date?->format('e');
$recurrence->save();
return $recurrence;
}
}

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