Compare commits

...

22 Commits

Author SHA1 Message Date
github-actions[bot]
158081395b Merge pull request #11583 from firefly-iii/release-1769334926
🤖 Automatically merge the PR into the develop branch.
2026-01-25 10:55:33 +01:00
JC5
34a7fd3ef0 🤖 Auto commit for release 'develop' on 2026-01-25 2026-01-25 10:55:27 +01:00
James Cole
96aafacf43 Fix processing transactions in events. 2026-01-25 10:47:30 +01:00
James Cole
035d599910 Fix processing new transactions. 2026-01-25 10:22:02 +01:00
James Cole
f5a929d72e Clean up a bunch of code and improve transaction store events 2026-01-25 09:02:47 +01:00
James Cole
22b97ce8ef Clean up middleware and kernel files. 2026-01-24 20:36:20 +01:00
James Cole
832019792f Improve listeners 2026-01-24 19:02:18 +01:00
James Cole
76b8ff18b0 Transaction events for #11544 2026-01-24 18:52:07 +01:00
James Cole
8c0a82ac0a Add support for batch submission. 2026-01-24 18:34:49 +01:00
James Cole
7edc386cdd Fix strict types 2026-01-24 16:47:17 +01:00
James Cole
5437d07ec2 Merge branch 'develop' of github.com:firefly-iii/firefly-iii into develop
# Conflicts:
#	app/Providers/EventServiceProvider.php
2026-01-24 16:46:08 +01:00
James Cole
ef3a1401dd Remove more events for #11544 2026-01-24 16:45:28 +01:00
github-actions[bot]
aa6169e314 Merge pull request #11582 from firefly-iii/release-1769259448
🤖 Automatically merge the PR into the develop branch.
2026-01-24 13:57:36 +01:00
JC5
4a4f1ff055 🤖 Auto commit for release 'develop' on 2026-01-24 2026-01-24 13:57:28 +01:00
James Cole
844470bf08 Move more events #11544 2026-01-24 13:52:35 +01:00
James Cole
92e985a9b8 Move more events #11544 2026-01-24 13:44:02 +01:00
James Cole
70e11098af Move more events #11544 2026-01-24 13:33:40 +01:00
James Cole
e76ab21091 Fix #11563 2026-01-24 07:17:19 +01:00
github-actions[bot]
87644923cf Merge pull request #11581 from firefly-iii/release-1769199015
🤖 Automatically merge the PR into the develop branch.
2026-01-23 21:10:26 +01:00
JC5
c46e9519c2 🤖 Auto commit for release 'develop' on 2026-01-23 2026-01-23 21:10:15 +01:00
github-actions[bot]
775933c3e8 Merge pull request #11580 from firefly-iii/release-1769183303
🤖 Automatically merge the PR into the develop branch.
2026-01-23 16:48:32 +01:00
JC5
5e316a1f05 🤖 Auto commit for release 'develop' on 2026-01-23 2026-01-23 16:48:23 +01:00
156 changed files with 1627 additions and 960 deletions

View File

@@ -402,16 +402,16 @@
},
{
"name": "friendsofphp/php-cs-fixer",
"version": "v3.92.5",
"version": "v3.93.0",
"source": {
"type": "git",
"url": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer.git",
"reference": "260cc8c4a1d2f6d2f22cd4f9c70aa72e55ebac58"
"reference": "50895a07cface1385082e4caa6a6786c4e033468"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/PHP-CS-Fixer/PHP-CS-Fixer/zipball/260cc8c4a1d2f6d2f22cd4f9c70aa72e55ebac58",
"reference": "260cc8c4a1d2f6d2f22cd4f9c70aa72e55ebac58",
"url": "https://api.github.com/repos/PHP-CS-Fixer/PHP-CS-Fixer/zipball/50895a07cface1385082e4caa6a6786c4e033468",
"reference": "50895a07cface1385082e4caa6a6786c4e033468",
"shasum": ""
},
"require": {
@@ -443,14 +443,14 @@
},
"require-dev": {
"facile-it/paraunit": "^1.3.1 || ^2.7",
"infection/infection": "^0.31",
"infection/infection": "^0.32",
"justinrainbow/json-schema": "^6.6",
"keradus/cli-executor": "^2.3",
"mikey179/vfsstream": "^1.6.12",
"php-coveralls/php-coveralls": "^2.9",
"php-cs-fixer/phpunit-constraint-isidenticalstring": "^1.6",
"php-cs-fixer/phpunit-constraint-xmlmatchesxsd": "^1.6",
"phpunit/phpunit": "^9.6.31 || ^10.5.60 || ^11.5.46",
"phpunit/phpunit": "^9.6.31 || ^10.5.60 || ^11.5.48",
"symfony/polyfill-php85": "^1.33",
"symfony/var-dumper": "^5.4.48 || ^6.4.26 || ^7.4.0 || ^8.0",
"symfony/yaml": "^5.4.45 || ^6.4.30 || ^7.4.1 || ^8.0"
@@ -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.92.5"
"source": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/tree/v3.93.0"
},
"funding": [
{
@@ -502,7 +502,7 @@
"type": "github"
}
],
"time": "2026-01-08T21:57:37+00:00"
"time": "2026-01-23T17:33:21+00:00"
},
{
"name": "psr/container",
@@ -1252,16 +1252,16 @@
},
{
"name": "symfony/console",
"version": "v8.0.3",
"version": "v8.0.4",
"source": {
"type": "git",
"url": "https://github.com/symfony/console.git",
"reference": "6145b304a5c1ea0bdbd0b04d297a5864f9a7d587"
"reference": "ace03c4cf9805080ff40cbeec69fca180c339a3b"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/console/zipball/6145b304a5c1ea0bdbd0b04d297a5864f9a7d587",
"reference": "6145b304a5c1ea0bdbd0b04d297a5864f9a7d587",
"url": "https://api.github.com/repos/symfony/console/zipball/ace03c4cf9805080ff40cbeec69fca180c339a3b",
"reference": "ace03c4cf9805080ff40cbeec69fca180c339a3b",
"shasum": ""
},
"require": {
@@ -1318,7 +1318,7 @@
"terminal"
],
"support": {
"source": "https://github.com/symfony/console/tree/v8.0.3"
"source": "https://github.com/symfony/console/tree/v8.0.4"
},
"funding": [
{
@@ -1338,7 +1338,7 @@
"type": "tidelift"
}
],
"time": "2025-12-23T14:52:06+00:00"
"time": "2026-01-13T13:06:50+00:00"
},
{
"name": "symfony/deprecation-contracts",
@@ -1409,16 +1409,16 @@
},
{
"name": "symfony/event-dispatcher",
"version": "v8.0.0",
"version": "v8.0.4",
"source": {
"type": "git",
"url": "https://github.com/symfony/event-dispatcher.git",
"reference": "573f95783a2ec6e38752979db139f09fec033f03"
"reference": "99301401da182b6cfaa4700dbe9987bb75474b47"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/573f95783a2ec6e38752979db139f09fec033f03",
"reference": "573f95783a2ec6e38752979db139f09fec033f03",
"url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/99301401da182b6cfaa4700dbe9987bb75474b47",
"reference": "99301401da182b6cfaa4700dbe9987bb75474b47",
"shasum": ""
},
"require": {
@@ -1470,7 +1470,7 @@
"description": "Provides tools that allow your application components to communicate with each other by dispatching events and listening to them",
"homepage": "https://symfony.com",
"support": {
"source": "https://github.com/symfony/event-dispatcher/tree/v8.0.0"
"source": "https://github.com/symfony/event-dispatcher/tree/v8.0.4"
},
"funding": [
{
@@ -1490,7 +1490,7 @@
"type": "tidelift"
}
],
"time": "2025-10-30T14:17:19+00:00"
"time": "2026-01-05T11:45:55+00:00"
},
{
"name": "symfony/event-dispatcher-contracts",
@@ -1640,16 +1640,16 @@
},
{
"name": "symfony/finder",
"version": "v8.0.3",
"version": "v8.0.4",
"source": {
"type": "git",
"url": "https://github.com/symfony/finder.git",
"reference": "dd3a2953570a283a2ba4e17063bb98c734cf5b12"
"reference": "42e48eb02e07d5f3771d194d67da117eb824c8c1"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/finder/zipball/dd3a2953570a283a2ba4e17063bb98c734cf5b12",
"reference": "dd3a2953570a283a2ba4e17063bb98c734cf5b12",
"url": "https://api.github.com/repos/symfony/finder/zipball/42e48eb02e07d5f3771d194d67da117eb824c8c1",
"reference": "42e48eb02e07d5f3771d194d67da117eb824c8c1",
"shasum": ""
},
"require": {
@@ -1684,7 +1684,7 @@
"description": "Finds files and directories via an intuitive fluent interface",
"homepage": "https://symfony.com",
"support": {
"source": "https://github.com/symfony/finder/tree/v8.0.3"
"source": "https://github.com/symfony/finder/tree/v8.0.4"
},
"funding": [
{
@@ -1704,7 +1704,7 @@
"type": "tidelift"
}
],
"time": "2025-12-23T14:52:06+00:00"
"time": "2026-01-12T12:37:40+00:00"
},
{
"name": "symfony/options-resolver",
@@ -2358,16 +2358,16 @@
},
{
"name": "symfony/process",
"version": "v8.0.3",
"version": "v8.0.4",
"source": {
"type": "git",
"url": "https://github.com/symfony/process.git",
"reference": "0cbbd88ec836f8757641c651bb995335846abb78"
"reference": "10df72602d88c0a3fa685b822976a052611dd607"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/process/zipball/0cbbd88ec836f8757641c651bb995335846abb78",
"reference": "0cbbd88ec836f8757641c651bb995335846abb78",
"url": "https://api.github.com/repos/symfony/process/zipball/10df72602d88c0a3fa685b822976a052611dd607",
"reference": "10df72602d88c0a3fa685b822976a052611dd607",
"shasum": ""
},
"require": {
@@ -2399,7 +2399,7 @@
"description": "Executes commands in sub-processes",
"homepage": "https://symfony.com",
"support": {
"source": "https://github.com/symfony/process/tree/v8.0.3"
"source": "https://github.com/symfony/process/tree/v8.0.4"
},
"funding": [
{
@@ -2419,7 +2419,7 @@
"type": "tidelift"
}
],
"time": "2025-12-19T10:01:18+00:00"
"time": "2026-01-23T11:07:10+00:00"
},
{
"name": "symfony/service-contracts",
@@ -2576,16 +2576,16 @@
},
{
"name": "symfony/string",
"version": "v8.0.1",
"version": "v8.0.4",
"source": {
"type": "git",
"url": "https://github.com/symfony/string.git",
"reference": "ba65a969ac918ce0cc3edfac6cdde847eba231dc"
"reference": "758b372d6882506821ed666032e43020c4f57194"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/string/zipball/ba65a969ac918ce0cc3edfac6cdde847eba231dc",
"reference": "ba65a969ac918ce0cc3edfac6cdde847eba231dc",
"url": "https://api.github.com/repos/symfony/string/zipball/758b372d6882506821ed666032e43020c4f57194",
"reference": "758b372d6882506821ed666032e43020c4f57194",
"shasum": ""
},
"require": {
@@ -2642,7 +2642,7 @@
"utf8"
],
"support": {
"source": "https://github.com/symfony/string/tree/v8.0.1"
"source": "https://github.com/symfony/string/tree/v8.0.4"
},
"funding": [
{
@@ -2662,7 +2662,7 @@
"type": "tidelift"
}
],
"time": "2025-12-01T09:13:36+00:00"
"time": "2026-01-12T12:37:40+00:00"
}
],
"packages-dev": [],

View File

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

View File

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

View File

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

View File

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

View File

@@ -26,7 +26,7 @@ namespace FireflyIII\Api\V1\Controllers\Webhook;
use FireflyIII\Api\V1\Controllers\Controller;
use FireflyIII\Enums\WebhookTrigger;
use FireflyIII\Events\RequestedSendWebhookMessages;
use FireflyIII\Events\Model\Webhook\WebhookMessagesRequestSending;
use FireflyIII\Generator\Webhook\MessageGeneratorInterface;
use FireflyIII\Models\TransactionGroup;
use FireflyIII\Models\Webhook;
@@ -176,8 +176,8 @@ class ShowController extends Controller
}
// trigger event to send them:
Log::debug('send event RequestedSendWebhookMessages from ShowController::triggerTransaction()');
event(new RequestedSendWebhookMessages());
Log::debug(sprintf('send event WebhookMessagesRequestSending from %s', __METHOD__));
event(new WebhookMessagesRequestSending());
return response()->json([], 204);
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,42 @@
<?php
declare(strict_types=1);
/*
* TransactionGroupRequestsAuditLogEntry.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\Database\Eloquent\Model;
use Illuminate\Queue\SerializesModels;
class TransactionGroupRequestsAuditLogEntry extends Event
{
use SerializesModels;
public function __construct(
public Model $changer,
public Model $auditable,
public string $field,
public mixed $before,
public mixed $after
) {}
}

View File

@@ -1,8 +1,10 @@
<?php
/**
* RequestedReportOnJournals.php
* Copyright (c) 2019 james@firefly-iii.org
declare(strict_types=1);
/*
* TransactionGroupsRequestedReporting.php
* Copyright (c) 2026 james@firefly-iii.org
*
* This file is part of Firefly III (https://github.com/firefly-iii).
*
@@ -20,24 +22,14 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
declare(strict_types=1);
namespace FireflyIII\Events\Model\TransactionGroup;
namespace FireflyIII\Events;
use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Broadcasting\PrivateChannel;
use Illuminate\Foundation\Events\Dispatchable;
use FireflyIII\Events\Event;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Log;
/**
* Class RequestedReportOnJournals
*/
class RequestedReportOnJournals
class TransactionGroupsRequestedReporting extends Event
{
use Dispatchable;
use InteractsWithSockets;
use SerializesModels;
/**
@@ -46,15 +38,5 @@ class RequestedReportOnJournals
public function __construct(
public int $userId,
public Collection $groups
) {
Log::debug('In event RequestedReportOnJournals.');
}
/**
* Get the channels the event should broadcast on.
*/
public function broadcastOn(): PrivateChannel
{
return new PrivateChannel('channel-name');
}
) {}
}

View File

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

View File

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

View File

@@ -1,8 +1,10 @@
<?php
/**
* RequestedVersionCheckStatus.php
* Copyright (c) 2019 james@firefly-iii.org
declare(strict_types=1);
/*
* SystemRequestedVersionCheck.php
* Copyright (c) 2026 james@firefly-iii.org
*
* This file is part of Firefly III (https://github.com/firefly-iii).
*
@@ -20,24 +22,16 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
declare(strict_types=1);
namespace FireflyIII\Events;
namespace FireflyIII\Events\Security\System;
use FireflyIII\Events\Event;
use FireflyIII\User;
use Illuminate\Queue\SerializesModels;
/**
* Class RequestedVersionCheckStatus
*/
class RequestedVersionCheckStatus extends Event
class SystemRequestedVersionCheck extends Event
{
use SerializesModels;
/**
* Create a new event instance. This event is triggered when Firefly III wants to know
* what the deal is with the version checker.
*/
public function __construct(
public User $user
) {}

View File

@@ -50,7 +50,6 @@ 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;
@@ -110,22 +109,24 @@ class TransactionJournalFactory
{
Log::debug('Now in TransactionJournalFactory::create()');
// convert to special object.
$dataObject = new NullArrayObject($data);
// $dataObject = new NullArrayObject($data);
Log::debug('Start of TransactionJournalFactory::create()');
$collection = new Collection();
$transactions = $dataObject['transactions'] ?? [];
$collection = new Collection();
$transactions = $data['transactions'] ?? [];
if (0 === count($transactions)) {
Log::error('There are no transactions in the array, the TransactionJournalFactory cannot continue.');
return new Collection();
}
$batchSubmission = $data['batch_submission'] ?? false;
try {
/** @var array $row */
foreach ($transactions as $index => $row) {
$row['batch_submission'] = $batchSubmission;
Log::debug(sprintf('Now creating journal %d/%d', $index + 1, count($transactions)));
$journal = $this->createJournal(new NullArrayObject($row));
$journal = $this->createJournal($row);
if ($journal instanceof TransactionJournal) {
$collection->push($journal);
}
@@ -161,7 +162,7 @@ class TransactionJournalFactory
*
* @SuppressWarnings("PHPMD.ExcessiveMethodLength")
*/
private function createJournal(NullArrayObject $row): ?TransactionJournal
private function createJournal(array $row): ?TransactionJournal
{
Log::debug('Now in TransactionJournalFactory::createJournal()');
$row['import_hash_v2'] = $this->hashArray($row);
@@ -273,7 +274,7 @@ class TransactionJournalFactory
'date_tz' => $carbon->format('e'),
'order' => $order,
'tag_count' => 0,
'completed' => 0,
'completed' => !$row['batch_submission'],
]);
Log::debug(sprintf('Created new journal #%d: "%s"', $journal->id, $journal->description));
@@ -331,7 +332,7 @@ class TransactionJournalFactory
throw new FireflyException($e->getMessage(), 0, $e);
}
$journal->completed = true;
Log::debug(sprintf('Is part of a batch submission? %s', var_export($row['batch_submission'], true)));
$journal->save();
$this->storeBudget($journal, $row);
$this->storeCategory($journal, $row);
@@ -344,20 +345,18 @@ class TransactionJournalFactory
return $journal;
}
private function hashArray(NullArrayObject $row): string
private function hashArray(array $row): string
{
$dataRow = $row->getArrayCopy();
unset($dataRow['import_hash_v2'], $dataRow['original_source']);
unset($row['import_hash_v2'], $row['original_source']);
try {
$json = json_encode($dataRow, JSON_THROW_ON_ERROR);
$json = json_encode($row, JSON_THROW_ON_ERROR);
} catch (JsonException $e) {
Log::error(sprintf('Could not encode dataRow: %s', $e->getMessage()));
$json = microtime();
}
$hash = hash('sha256', $json);
Log::debug(sprintf('The hash is: %s', $hash), $dataRow);
$hash = hash('sha256', $json);
Log::debug(sprintf('The hash is: %s', $hash), $row);
return $hash;
}
@@ -399,7 +398,7 @@ class TransactionJournalFactory
/**
* @throws FireflyException
*/
private function validateAccounts(NullArrayObject $data): void
private function validateAccounts(array $data): void
{
Log::debug(sprintf('Now in %s', __METHOD__));
$transactionType = $data['type'] ?? 'invalid';
@@ -577,7 +576,7 @@ class TransactionJournalFactory
/**
* Link a piggy bank to this journal.
*/
private function storePiggyEvent(TransactionJournal $journal, NullArrayObject $data): void
private function storePiggyEvent(TransactionJournal $journal, array $data): void
{
Log::debug('Will now store piggy event.');
@@ -592,17 +591,17 @@ class TransactionJournalFactory
Log::debug('Create no piggy event');
}
private function storeMetaFields(TransactionJournal $journal, NullArrayObject $transaction): void
private function storeMetaFields(TransactionJournal $journal, array $transaction): void
{
foreach ($this->fields as $field) {
$this->storeMeta($journal, $transaction, $field);
}
}
protected function storeMeta(TransactionJournal $journal, NullArrayObject $data, string $field): void
protected function storeMeta(TransactionJournal $journal, array $data, string $field): void
{
$set = ['journal' => $journal, 'name' => $field, 'data' => (string) ($data[$field] ?? '')];
if ($data[$field] instanceof Carbon) {
if (array_key_exists($field, $data) && $data[$field] instanceof Carbon) {
$data[$field]->setTimezone(config('app.timezone'));
Log::debug(sprintf('%s Date: %s (%s)', $field, $data[$field], $data[$field]->timezone->getName()));
$set['data'] = $data[$field]->format('Y-m-d H:i:s');
@@ -615,7 +614,7 @@ class TransactionJournalFactory
$factory->updateOrCreate($set);
}
private function storeLocation(TransactionJournal $journal, NullArrayObject $data): void
private function storeLocation(TransactionJournal $journal, array $data): void
{
if (!in_array(null, [$data['longitude'], $data['latitude'], $data['zoom_level']], true)) {
$location = new Location();

View File

@@ -26,7 +26,7 @@ namespace FireflyIII\Handlers\Events;
use FireflyIII\Enums\WebhookTrigger;
use FireflyIII\Events\DestroyedTransactionGroup;
use FireflyIII\Events\RequestedSendWebhookMessages;
use FireflyIII\Events\Model\Webhook\WebhookMessagesRequestSending;
use FireflyIII\Generator\Webhook\MessageGeneratorInterface;
use FireflyIII\Support\Facades\FireflyConfig;
use FireflyIII\Support\Models\AccountBalanceCalculator;
@@ -56,8 +56,8 @@ class DestroyedGroupEventHandler
$engine->setObjects(new Collection()->push($group));
$engine->setTrigger(WebhookTrigger::DESTROY_TRANSACTION);
$engine->generateMessages();
Log::debug(sprintf('send event RequestedSendWebhookMessages from %s', __METHOD__));
event(new RequestedSendWebhookMessages());
Log::debug(sprintf('send event WebhookMessagesRequestSending from %s', __METHOD__));
event(new WebhookMessagesRequestSending());
}
private function updateRunningBalance(DestroyedTransactionGroup $event): void

View File

@@ -25,7 +25,7 @@ namespace FireflyIII\Handlers\Events;
use FireflyIII\Enums\WebhookTrigger;
use FireflyIII\Events\Model\TransactionGroup\TriggeredStoredTransactionGroup;
use FireflyIII\Events\RequestedSendWebhookMessages;
use FireflyIII\Events\Model\Webhook\WebhookMessagesRequestSending;
use FireflyIII\Events\StoredTransactionGroup;
use FireflyIII\Generator\Webhook\MessageGeneratorInterface;
use FireflyIII\Models\RuleGroup;
@@ -47,16 +47,16 @@ class StoredGroupEventHandler
{
public function runAllHandlers(StoredTransactionGroup $event): void
{
$this->processRules($event, null);
$this->recalculateCredit($event);
$this->triggerWebhooks($event);
// $this->processRules($event, null);
// $this->recalculateCredit($event);
// $this->triggerWebhooks($event);
$this->removePeriodStatistics($event);
}
public function triggerRulesManually(TriggeredStoredTransactionGroup $event): void
{
$newEvent = new StoredTransactionGroup($event->transactionGroup, true, false);
$this->processRules($newEvent, $event->ruleGroup);
// $newEvent = new StoredTransactionGroup($event->transactionGroup, true, false);
// $this->processRules($newEvent, $event->ruleGroup);
}
/**
@@ -181,7 +181,7 @@ class StoredGroupEventHandler
$engine->generateMessages();
// trigger event to send them:
Log::debug(sprintf('send event RequestedSendWebhookMessages from %s', __METHOD__));
event(new RequestedSendWebhookMessages());
Log::debug(sprintf('send event WebhookMessagesRequestSending from %s', __METHOD__));
event(new WebhookMessagesRequestSending());
}
}

View File

@@ -25,7 +25,7 @@ namespace FireflyIII\Handlers\Events;
use FireflyIII\Enums\TransactionTypeEnum;
use FireflyIII\Enums\WebhookTrigger;
use FireflyIII\Events\RequestedSendWebhookMessages;
use FireflyIII\Events\Model\Webhook\WebhookMessagesRequestSending;
use FireflyIII\Events\UpdatedTransactionGroup;
use FireflyIII\Generator\Webhook\MessageGeneratorInterface;
use FireflyIII\Models\Account;
@@ -207,8 +207,8 @@ class UpdatedGroupEventHandler
$engine->setTrigger(WebhookTrigger::UPDATE_TRANSACTION);
$engine->generateMessages();
Log::debug(sprintf('send event RequestedSendWebhookMessages from %s', __METHOD__));
event(new RequestedSendWebhookMessages());
Log::debug(sprintf('send event WebhookMessagesRequestSending from %s', __METHOD__));
event(new WebhookMessagesRequestSending());
}
private function updateRunningBalance(UpdatedTransactionGroup $event): void

View File

@@ -25,7 +25,7 @@ declare(strict_types=1);
namespace FireflyIII\Handlers\Observer;
use FireflyIII\Enums\WebhookTrigger;
use FireflyIII\Events\RequestedSendWebhookMessages;
use FireflyIII\Events\Model\Webhook\WebhookMessagesRequestSending;
use FireflyIII\Generator\Webhook\MessageGeneratorInterface;
use FireflyIII\Models\BudgetLimit;
use FireflyIII\Support\Facades\Amount;
@@ -58,8 +58,8 @@ class BudgetLimitObserver
$engine->setTrigger(WebhookTrigger::STORE_UPDATE_BUDGET_LIMIT);
$engine->generateMessages();
Log::debug(sprintf('send event RequestedSendWebhookMessages from %s', __METHOD__));
event(new RequestedSendWebhookMessages());
Log::debug(sprintf('send event WebhookMessagesRequestSending from %s', __METHOD__));
event(new WebhookMessagesRequestSending());
}
}
@@ -101,8 +101,8 @@ class BudgetLimitObserver
$engine->setTrigger(WebhookTrigger::STORE_UPDATE_BUDGET_LIMIT);
$engine->generateMessages();
Log::debug(sprintf('send event RequestedSendWebhookMessages from %s', __METHOD__));
event(new RequestedSendWebhookMessages());
Log::debug(sprintf('send event WebhookMessagesRequestSending from %s', __METHOD__));
event(new WebhookMessagesRequestSending());
}
}
}

View File

@@ -24,7 +24,7 @@ declare(strict_types=1);
namespace FireflyIII\Handlers\Observer;
use FireflyIII\Enums\WebhookTrigger;
use FireflyIII\Events\RequestedSendWebhookMessages;
use FireflyIII\Events\Model\Webhook\WebhookMessagesRequestSending;
use FireflyIII\Generator\Webhook\MessageGeneratorInterface;
use FireflyIII\Models\Attachment;
use FireflyIII\Models\Budget;
@@ -59,8 +59,8 @@ class BudgetObserver
$engine->setObjects(new Collection()->push($budget));
$engine->setTrigger(WebhookTrigger::STORE_BUDGET);
$engine->generateMessages();
Log::debug(sprintf('send event RequestedSendWebhookMessages from %s', __METHOD__));
event(new RequestedSendWebhookMessages());
Log::debug(sprintf('send event WebhookMessagesRequestSending from %s', __METHOD__));
event(new WebhookMessagesRequestSending());
}
}
@@ -80,8 +80,8 @@ class BudgetObserver
$engine->setObjects(new Collection()->push($budget));
$engine->setTrigger(WebhookTrigger::UPDATE_BUDGET);
$engine->generateMessages();
Log::debug(sprintf('send event RequestedSendWebhookMessages from %s', __METHOD__));
event(new RequestedSendWebhookMessages());
Log::debug(sprintf('send event WebhookMessagesRequestSending from %s', __METHOD__));
event(new WebhookMessagesRequestSending());
}
}
@@ -97,8 +97,8 @@ class BudgetObserver
$engine->setObjects(new Collection()->push($budget));
$engine->setTrigger(WebhookTrigger::DESTROY_BUDGET);
$engine->generateMessages();
Log::debug(sprintf('send event RequestedSendWebhookMessages from %s', __METHOD__));
event(new RequestedSendWebhookMessages());
Log::debug(sprintf('send event WebhookMessagesRequestSending from %s', __METHOD__));
event(new WebhookMessagesRequestSending());
$repository = app(AttachmentRepositoryInterface::class);
$repository->setUser($budget->user);

View File

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

View File

@@ -773,30 +773,22 @@ trait MetaCollection
$this->query->whereNotNull('tag_transaction_journal.tag_id');
// Added this while fixing #10898, not sure why a post filter was ever necessary.
$this->query->whereIn('tag_transaction_journal.tag_id', $tags->pluck('id')->toArray());
// Removed again for #11473 because it breaks multiple tag filters.
// $this->query->whereIn('tag_transaction_journal.tag_id', $tags->pluck('id')->toArray());
// this method adds a "postFilter" to the collector.
$list = $tags->pluck('tag')->toArray();
$list = array_map(strtolower(...), $list);
$filter = static function (array $object) use ($list): bool {
Log::debug(sprintf('Now in setTags(%s) filter', implode(', ', $list)));
Log::debug(sprintf('Now in setTags (any) filter: %s', implode(', ', $list)));
foreach ($object['transactions'] as $transaction) {
$total = count($transaction['tags']);
$matched = 0;
foreach ($transaction['tags'] as $tag) {
Log::debug(sprintf('"%s" versus', strtolower((string) $tag['name'])), $list);
if (in_array(strtolower((string) $tag['name']), $list, true)) {
Log::debug(sprintf('Transaction has tag "%s" so return true.', $tag['name']));
++$matched;
if (1 === count($list)) {
return true;
}
}
}
if (count($list) > 1 && $total === $matched && $matched === count($list)) {
Log::debug(sprintf('All %d searched tags are present.', $total));
return true;
return true;
}
}
}
Log::debug('Transaction has no tags from the list, so return false.');

View File

@@ -433,6 +433,7 @@ class GroupCollector implements GroupCollectorInterface
*/
public function getGroups(): Collection
{
Log::debug('Now in getGroups()');
if ($this->expandGroupSearch) {
// get group ID's for the query:
$groupIds = $this->getCollectedGroupIds();
@@ -440,6 +441,8 @@ class GroupCollector implements GroupCollectorInterface
$this->query->orWhereIn('transaction_journals.transaction_group_id', $groupIds);
}
$result = $this->query->get($this->fields);
// $this->dumpQueryInLogs();
// Log::debug(sprintf('Count of result is %d', $result->count()));
// now to parse this into an array.
$collection = $this->parseArray($result);
@@ -758,6 +761,12 @@ class GroupCollector implements GroupCollectorInterface
count($currentCollection)
));
if (0 === $currentCollection->count()) {
Log::debug('Found nothing anyway, return empty collection.');
return $currentCollection;
}
/**
* @var Closure $function
*/
@@ -775,16 +784,25 @@ class GroupCollector implements GroupCollectorInterface
$result = $function($item);
if (false === $result) {
// skip other filters, continue to next item.
Log::debug('Result is false');
continue;
}
// if the result is a bool, use the unedited results.
if (true === $result) {
Log::debug('Result is true');
$nextCollection->push($item);
continue;
}
// if the result is an array, the filter has changed what's being returned.
if (is_array($result)) {
Log::debug('Result is array');
$nextCollection->push($result);
continue;
}
Log::debug('Result is something else!');
}
$currentCollection = $nextCollection;
Log::debug(sprintf('GroupCollector: postFilterCollection has %d transaction(s) left.', count($currentCollection)));
@@ -827,6 +845,7 @@ class GroupCollector implements GroupCollectorInterface
*/
public function getPaginatedGroups(): LengthAwarePaginator
{
Log::debug('Now in getPaginatedGroups()');
$set = $this->getGroups();
if (0 === $this->limit) {
$this->setLimit(50);

View File

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

View File

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

View File

@@ -192,7 +192,10 @@ class IndexController extends Controller
if (count($bill['paid_dates']) < count($bill['pay_dates'])) {
$count = count($bill['pay_dates']) - count($bill['paid_dates']);
if ($count > 0) {
$avg = bcdiv(bcadd((string) $bill['amount_min'], (string) $bill['amount_max']), '2');
$avg = bcdiv(
bcadd((string) $bill['amount_min'], (string) $bill['amount_max']),
'2'
);
$avg = bcmul($avg, (string) $count);
$sums[$groupOrder][$currencyId]['total_left_to_pay'] = bcadd($sums[$groupOrder][$currencyId]['total_left_to_pay'], $avg);
Log::debug(

View File

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

View File

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

View File

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

View File

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

View File

@@ -23,7 +23,7 @@ declare(strict_types=1);
namespace FireflyIII\Http\Controllers;
use FireflyIII\Events\RequestedSendWebhookMessages;
use FireflyIII\Events\Model\Webhook\WebhookMessagesRequestSending;
use FireflyIII\Models\TransactionCurrency;
use FireflyIII\Support\Facades\Amount;
use FireflyIII\Support\Facades\FireflyConfig;
@@ -152,10 +152,10 @@ abstract class Controller extends BaseController
View::share('original_route_name', Route::currentRouteName());
// lottery to send any remaining webhooks:
if (7 === random_int(1, 10)) {
if (7 === random_int(1, 30)) {
// trigger event to send them:
Log::debug('send event RequestedSendWebhookMessages through lottery');
event(new RequestedSendWebhookMessages());
Log::debug(sprintf('send event WebhookMessagesRequestSending from %s', __METHOD__));
event(new WebhookMessagesRequestSending());
}
}
View::share('darkMode', $darkMode);

View File

@@ -27,7 +27,7 @@ use Carbon\Carbon;
use Carbon\Exceptions\InvalidFormatException;
use Exception;
use FireflyIII\Enums\AccountTypeEnum;
use FireflyIII\Events\RequestedVersionCheckStatus;
use FireflyIII\Events\Security\System\SystemRequestedVersionCheck;
use FireflyIII\Exceptions\FireflyException;
use FireflyIII\Helpers\Collector\GroupCollectorInterface;
use FireflyIII\Http\Middleware\Installer;
@@ -178,7 +178,7 @@ class HomeController extends Controller
/** @var User $user */
$user = auth()->user();
event(new RequestedVersionCheckStatus($user));
event(new SystemRequestedVersionCheck($user));
return view('index', [
'count' => $count,
@@ -202,7 +202,7 @@ class HomeController extends Controller
/** @var User $user */
$user = auth()->user();
event(new RequestedVersionCheckStatus($user));
event(new SystemRequestedVersionCheck($user));
return view('index', ['subTitle' => $subTitle, 'start' => $start, 'end' => $end, 'pageTitle' => $pageTitle]);
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -25,8 +25,9 @@ declare(strict_types=1);
namespace FireflyIII\Jobs;
use Carbon\Carbon;
use FireflyIII\Events\RequestedReportOnJournals;
use FireflyIII\Events\StoredTransactionGroup;
use FireflyIII\Events\Model\TransactionGroup\CreatedSingleTransactionGroup;
use FireflyIII\Events\Model\TransactionGroup\TransactionGroupEventFlags;
use FireflyIII\Events\Model\TransactionGroup\TransactionGroupsRequestedReporting;
use FireflyIII\Exceptions\DuplicateTransactionException;
use FireflyIII\Exceptions\FireflyException;
use FireflyIII\Models\Recurrence;
@@ -145,7 +146,7 @@ class CreateRecurringTransactions implements ShouldQueue
Log::debug('Now running report thing.');
// will now send email to users.
foreach ($result as $userId => $journals) {
event(new RequestedReportOnJournals($userId, $journals));
event(new TransactionGroupsRequestedReporting($userId, $journals));
}
Log::debug('Done with handle()');
@@ -383,7 +384,10 @@ class CreateRecurringTransactions implements ShouldQueue
Log::info(sprintf('Created new transaction group #%d', $group->id));
// trigger event:
event(new StoredTransactionGroup($group, $recurrence->apply_rules, true));
$flags = new TransactionGroupEventFlags();
$flags->applyRules = $recurrence->apply_rules;
event(new CreatedSingleTransactionGroup($group, $flags));
// event(new StoredTransactionGroup($group, $recurrence->apply_rules, true));
$this->groups->push($group);
// update recurring thing:

View File

@@ -1,8 +1,10 @@
<?php
/**
* AutomationHandler.php
* Copyright (c) 2019 james@firefly-iii.org
declare(strict_types=1);
/*
* MailsNewTransactionsReport.php
* Copyright (c) 2026 james@firefly-iii.org
*
* This file is part of Firefly III (https://github.com/firefly-iii).
*
@@ -20,34 +22,24 @@
* 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 Exception;
use FireflyIII\Events\RequestedReportOnJournals;
use FireflyIII\Exceptions\FireflyException;
use FireflyIII\Events\Model\TransactionGroup\TransactionGroupsRequestedReporting;
use FireflyIII\Models\TransactionGroup;
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 AutomationHandler
*/
class AutomationHandler
class MailsNewTransactionsReport implements ShouldQueue
{
/**
* Respond to the creation of X journals.
*
* @throws FireflyException
*/
public function reportJournals(RequestedReportOnJournals $event): void
public function handle(TransactionGroupsRequestedReporting $event): void
{
Log::debug('In reportJournals.');
Log::debug('In MailsNewTransactionsReport.');
/** @var UserRepositoryInterface $repository */
$repository = app(UserRepositoryInterface::class);

View File

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

View File

@@ -1,8 +1,10 @@
<?php
declare(strict_types=1);
/*
* AuditEventHandler.php
* Copyright (c) 2022 james@firefly-iii.org
* StoresAuditLogEntry.php
* Copyright (c) 2026 james@firefly-iii.org
*
* This file is part of Firefly III (https://github.com/firefly-iii).
*
@@ -20,21 +22,17 @@
* 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 Carbon\Carbon;
use FireflyIII\Events\TriggeredAuditLog;
use FireflyIII\Events\Model\TransactionGroup\TransactionGroupRequestsAuditLogEntry;
use FireflyIII\Repositories\AuditLogEntry\ALERepositoryInterface;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Support\Facades\Log;
/**
* Class AuditEventHandler
*/
class AuditEventHandler
class StoresAuditLogEntry implements ShouldQueue
{
public function storeAuditEvent(TriggeredAuditLog $event): void
public function handle(TransactionGroupRequestsAuditLogEntry $event): void
{
$array = [
'auditable' => $event->auditable,

View File

@@ -1,8 +1,10 @@
<?php
declare(strict_types=1);
/*
* WebhookEventHandler.php
* Copyright (c) 2021 james@firefly-iii.org
* SendsWebhookMessages.php
* Copyright (c) 2026 james@firefly-iii.org
*
* This file is part of Firefly III (https://github.com/firefly-iii).
*
@@ -20,26 +22,20 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
declare(strict_types=1);
namespace FireflyIII\Handlers\Events;
namespace FireflyIII\Listeners\Model\Webhook;
use FireflyIII\Events\Model\Webhook\WebhookMessagesRequestSending;
use FireflyIII\Jobs\SendWebhookMessage;
use FireflyIII\Models\WebhookMessage;
use FireflyIII\Support\Facades\FireflyConfig;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Support\Facades\Log;
/**
* Class WebhookEventHandler
*/
class WebhookEventHandler
class SendsWebhookMessages implements ShouldQueue
{
/**
* Will try to send at most 3 messages so the flow doesn't get broken too much.
*/
public function sendWebhookMessages(): void
public function handle(WebhookMessagesRequestSending $event): void
{
Log::debug(sprintf('Now in %s', __METHOD__));
Log::debug(sprintf('Now in %s for %s', __METHOD__, get_class($event)));
if (false === config('firefly.feature_flags.webhooks') || false === FireflyConfig::get('allow_webhooks', config('firefly.allow_webhooks'))->data) {
Log::debug('Webhook event handler is disabled, do not run sendWebhookMessages().');
@@ -69,6 +65,6 @@ class WebhookEventHandler
}
// clean up sent messages table:
WebhookMessage::where('webhook_messages.sent', true)->where('webhook_messages.created_at', '<', now()->subDays(30))->delete();
WebhookMessage::where('webhook_messages.sent', true)->where('webhook_messages.created_at', '<', now()->subDays(14))->delete();
}
}

View File

@@ -1,8 +1,10 @@
<?php
/**
* VersionCheckEventHandler.php
* Copyright (c) 2019 james@firefly-iii.org
declare(strict_types=1);
/*
* ChecksForNewVersion.php
* Copyright (c) 2026 james@firefly-iii.org
*
* This file is part of Firefly III (https://github.com/firefly-iii).
*
@@ -19,40 +21,28 @@
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
declare(strict_types=1);
namespace FireflyIII\Handlers\Events;
namespace FireflyIII\Listeners\Security\System;
use Carbon\Carbon;
use Deprecated;
use FireflyIII\Events\RequestedVersionCheckStatus;
use FireflyIII\Events\Security\System\SystemRequestedVersionCheck;
use FireflyIII\Exceptions\FireflyException;
use FireflyIII\Helpers\Update\UpdateTrait;
use FireflyIII\Models\Configuration;
use FireflyIII\Repositories\User\UserRepositoryInterface;
use FireflyIII\Support\Facades\FireflyConfig;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Support\Facades\Log;
use Psr\Container\ContainerExceptionInterface;
use Psr\Container\NotFoundExceptionInterface;
/**
* Class VersionCheckEventHandler
*/
class VersionCheckEventHandler
class ChecksForNewVersion implements ShouldQueue
{
use UpdateTrait;
/**
* Checks with GitHub to see if there is a new version.
*
* @throws ContainerExceptionInterface
* @throws FireflyException
* @throws NotFoundExceptionInterface
*/
#[Deprecated(message: '?')]
public function checkForUpdates(RequestedVersionCheckStatus $event): void
public function handle(SystemRequestedVersionCheck $event): void
{
Log::debug('Now in checkForUpdates()');
Log::debug(sprintf('Now in %s', __METHOD__));
// should not check for updates:
$permission = FireflyConfig::get('permission_update_check', -1);
@@ -96,7 +86,7 @@ class VersionCheckEventHandler
* @throws ContainerExceptionInterface
* @throws NotFoundExceptionInterface
*/
protected function warnToCheckForUpdates(RequestedVersionCheckStatus $event): void
private function warnToCheckForUpdates(SystemRequestedVersionCheck $event): void
{
/** @var UserRepositoryInterface $repository */
$repository = app(UserRepositoryInterface::class);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -23,19 +23,12 @@ declare(strict_types=1);
namespace FireflyIII\Providers;
use FireflyIII\Events\Admin\InvitationCreated;
use FireflyIII\Events\DestroyedTransactionGroup;
use FireflyIII\Events\Model\TransactionGroup\TriggeredStoredTransactionGroup;
use FireflyIII\Events\Preferences\UserGroupChangedPrimaryCurrency;
use FireflyIII\Events\RequestedReportOnJournals;
use FireflyIII\Events\RequestedSendWebhookMessages;
use FireflyIII\Events\RequestedVersionCheckStatus;
use FireflyIII\Events\StoredAccount;
use FireflyIII\Events\StoredTransactionGroup;
use FireflyIII\Events\TriggeredAuditLog;
use FireflyIII\Events\UpdatedAccount;
use FireflyIII\Events\UpdatedTransactionGroup;
use Illuminate\Auth\Events\Login;
use Illuminate\Foundation\Support\Providers\EventServiceProvider as ServiceProvider;
use Laravel\Passport\Events\AccessTokenCreated;
use Override;
@@ -48,96 +41,19 @@ use Override;
class EventServiceProvider extends ServiceProvider
{
protected $listen = [
// is a User related event.
// RegisteredUser::class => [
// 'FireflyIII\Handlers\Events\UserEventHandler@createExchangeRates',
// ],
// UserAttemptedLogin::class => [
// 'FireflyIII\Handlers\Events\UserEventHandler@sendLoginAttemptNotification',
// ],
// is a User related event.
Login::class => [
'FireflyIII\Handlers\Events\UserEventHandler@checkSingleUserIsAdmin',
'FireflyIII\Handlers\Events\UserEventHandler@demoUserBackToEnglish',
],
// DetectedNewIPAddress::class => [
// 'FireflyIII\Handlers\Events\UserEventHandler@notifyNewIPAddress',
// ],
RequestedVersionCheckStatus::class => ['FireflyIII\Handlers\Events\VersionCheckEventHandler@checkForUpdates'],
RequestedReportOnJournals::class => ['FireflyIII\Handlers\Events\AutomationHandler@reportJournals'],
// is a User related event.
InvitationCreated::class => [
'FireflyIII\Handlers\Events\AdminEventHandler@sendInvitationNotification',
'FireflyIII\Handlers\Events\UserEventHandler@sendRegistrationInvite',
],
// is a Transaction Journal related event.
StoredTransactionGroup::class => ['FireflyIII\Handlers\Events\StoredGroupEventHandler@runAllHandlers'],
TriggeredStoredTransactionGroup::class => ['FireflyIII\Handlers\Events\StoredGroupEventHandler@triggerRulesManually'],
// StoredTransactionGroup::class => ['FireflyIII\Handlers\Events\StoredGroupEventHandler@runAllHandlers'],
// TriggeredStoredTransactionGroup::class => ['FireflyIII\Handlers\Events\StoredGroupEventHandler@triggerRulesManually'],
// is a Transaction Journal related event.
UpdatedTransactionGroup::class => ['FireflyIII\Handlers\Events\UpdatedGroupEventHandler@runAllHandlers'],
DestroyedTransactionGroup::class => ['FireflyIII\Handlers\Events\DestroyedGroupEventHandler@runAllHandlers'],
// UpdatedTransactionGroup::class => ['FireflyIII\Handlers\Events\UpdatedGroupEventHandler@runAllHandlers'],
// DestroyedTransactionGroup::class => ['FireflyIII\Handlers\Events\DestroyedGroupEventHandler@runAllHandlers'],
// API related events:
AccessTokenCreated::class => ['FireflyIII\Handlers\Events\APIEventHandler@accessTokenCreated'],
// Webhook related event:
RequestedSendWebhookMessages::class => ['FireflyIII\Handlers\Events\WebhookEventHandler@sendWebhookMessages'],
// AccessTokenCreated::class => ['FireflyIII\Handlers\Events\APIEventHandler@accessTokenCreated'],
// account related events:
StoredAccount::class => ['FireflyIII\Handlers\Events\StoredAccountEventHandler@recalculateCredit'],
UpdatedAccount::class => ['FireflyIII\Handlers\Events\UpdatedAccountEventHandler@recalculateCredit'],
// subscription related events:
// SubscriptionNeedsExtensionOrRenewal::class => [
// 'FireflyIII\Handlers\Events\BillEventHandler@warnAboutBill',
// ],
// WarnUserAboutOverdueSubscriptions::class => [
// 'FireflyIII\Handlers\Events\BillEventHandler@warnAboutOverdueSubscriptions',
// ],
// audit log events:
TriggeredAuditLog::class => ['FireflyIII\Handlers\Events\AuditEventHandler@storeAuditEvent'],
// piggy bank related events:
// PiggyBankAmountIsChanged::class => [
// 'FireflyIII\Handlers\Events\Model\PiggyBankEventHandler@changePiggyAmount',
// ],
// ChangedName::class => [
// 'FireflyIII\Handlers\Events\Model\PiggyBankEventHandler@changedPiggyBankName',
// ],
// rule actions
// RuleActionFailedOnArray::class => [
// 'FireflyIII\Handlers\Events\Model\RuleHandler@ruleActionFailedOnArray',
// ],
// RuleActionFailedOnObject::class => [
// 'FireflyIII\Handlers\Events\Model\RuleHandler@ruleActionFailedOnObject',
// ],
// security related
// EnabledMFA::class => [
// 'FireflyIII\Handlers\Events\Security\MFAHandler@sendMFAEnabledMail',
// ],
// DisabledMFA::class => [
// 'FireflyIII\Handlers\Events\Security\MFAHandler@sendMFADisabledMail',
// ],
// MFANewBackupCodes::class => [
// 'FireflyIII\Handlers\Events\Security\MFAHandler@sendNewMFABackupCodesMail',
// ],
// MFAUsedBackupCode::class => [
// 'FireflyIII\Handlers\Events\Security\MFAHandler@sendUsedBackupCodeMail',
// ],
// MFABackupFewLeft::class => [
// 'FireflyIII\Handlers\Events\Security\MFAHandler@sendBackupFewLeftMail',
// ],
// MFABackupNoLeft::class => [
// 'FireflyIII\Handlers\Events\Security\MFAHandler@sendBackupNoLeftMail',
// ],
// MFAManyFailedAttempts::class => [
// 'FireflyIII\Handlers\Events\Security\MFAHandler@sendMFAFailedAttemptsMail',
// ],
// StoredAccount::class => ['FireflyIII\Handlers\Events\StoredAccountEventHandler@recalculateCredit'],
// UpdatedAccount::class => ['FireflyIII\Handlers\Events\UpdatedAccountEventHandler@recalculateCredit'],
// preferences
UserGroupChangedPrimaryCurrency::class => ['FireflyIII\Handlers\Events\PreferencesEventHandler@resetPrimaryCurrencyAmounts'],
// UserGroupChangedPrimaryCurrency::class => ['FireflyIII\Handlers\Events\PreferencesEventHandler@resetPrimaryCurrencyAmounts'],
];
/**

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -36,7 +36,6 @@ use FireflyIII\Repositories\Account\AccountRepositoryInterface;
use FireflyIII\Repositories\Budget\BudgetRepositoryInterface;
use FireflyIII\Repositories\Category\CategoryRepositoryInterface;
use FireflyIII\Rules\UniqueIban;
use FireflyIII\Support\NullArrayObject;
use Illuminate\Support\Facades\Log;
use Safe\Exceptions\JsonException;
@@ -378,7 +377,7 @@ trait JournalServiceTrait
return $amount;
}
protected function storeBudget(TransactionJournal $journal, NullArrayObject $data): void
protected function storeBudget(TransactionJournal $journal, array $data): void
{
if (TransactionTypeEnum::WITHDRAWAL->value !== $journal->transactionType->type) {
$journal->budgets()->sync([]);
@@ -396,7 +395,7 @@ trait JournalServiceTrait
$journal->budgets()->sync([]);
}
protected function storeCategory(TransactionJournal $journal, NullArrayObject $data): void
protected function storeCategory(TransactionJournal $journal, array $data): void
{
$category = $this->categoryRepository->findCategory($data['category_id'], $data['category_name']);
if (null !== $category) {

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