Compare commits

..

45 Commits

Author SHA1 Message Date
github-actions[bot]
d3add7c92b Merge pull request #11647 from firefly-iii/release-1770268490
🤖 Automatically merge the PR into the develop branch.
2026-02-05 06:14:57 +01:00
JC5
a491e4921f 🤖 Auto commit for release 'develop' on 2026-02-05 2026-02-05 06:14:50 +01:00
James Cole
171bc03668 Fix running balance events. 2026-02-05 06:10:25 +01:00
James Cole
dd5476bfc7 Clean up events and filters. 2026-02-05 06:02:32 +01:00
James Cole
bc0769358d Clean up update handlers. 2026-02-05 05:51:44 +01:00
James Cole
ccf33f1db6 Also include delete event in new event triggers. 2026-02-05 05:47:37 +01:00
github-actions[bot]
35f611b3f2 Merge pull request #11645 from firefly-iii/release-1770234250
🤖 Automatically merge the PR into the develop branch.
2026-02-04 20:44:18 +01:00
JC5
e5d394533c 🤖 Auto commit for release 'develop' on 2026-02-04 2026-02-04 20:44:10 +01:00
James Cole
831d39a41e Catch missing nonce 2026-02-04 20:39:54 +01:00
James Cole
2920a9b9e3 Fix call. 2026-02-04 20:39:01 +01:00
James Cole
5c8204e963 Unify more event handlers. 2026-02-04 20:29:28 +01:00
James Cole
d25283f193 Clean up processing for group. 2026-02-04 20:17:47 +01:00
github-actions[bot]
20986e6426 Merge pull request #11644 from firefly-iii/release-1770218546
🤖 Automatically merge the PR into the develop branch.
2026-02-04 16:22:34 +01:00
JC5
9cd0ebe37e 🤖 Auto commit for release 'develop' on 2026-02-04 2026-02-04 16:22:26 +01:00
Sander Dorigo
9c2b83a971 Update event handlers 2026-02-04 16:16:27 +01:00
Sander Dorigo
e1d32da409 New event handler object 2026-02-04 08:29:09 +01:00
Sander Dorigo
c51df8cd83 Move events to service and repos 2026-02-04 08:18:35 +01:00
github-actions[bot]
9f016aed16 Merge pull request #11643 from firefly-iii/release-1770188788
🤖 Automatically merge the PR into the develop branch.
2026-02-04 08:06:37 +01:00
JC5
27df5ea800 🤖 Auto commit for release 'develop' on 2026-02-04 2026-02-04 08:06:28 +01:00
Sander Dorigo
2d7cdd36f0 Fix null pointer 2026-02-04 08:01:47 +01:00
github-actions[bot]
7888023c1a Merge pull request #11642 from firefly-iii/release-1770187820
🤖 Automatically merge the PR into the develop branch.
2026-02-04 07:50:28 +01:00
JC5
3032118788 🤖 Auto commit for release 'develop' on 2026-02-04 2026-02-04 07:50:20 +01:00
James Cole
89d96ddc17 Remove unused events. 2026-02-04 05:48:51 +01:00
James Cole
97c9937571 Clean up events for groups. 2026-02-04 05:46:19 +01:00
James Cole
c46aca0594 Clean up more events. 2026-02-03 21:04:07 +01:00
James Cole
0b33e1ff09 Refactor recalculation service. 2026-02-03 20:43:52 +01:00
James Cole
d267d2a0b0 Remove unused event handler. 2026-02-03 20:29:58 +01:00
James Cole
f2996dcebe Clean up event handlers and other code. 2026-02-03 20:24:16 +01:00
James Cole
ebb6a186cc Add some event handlers. 2026-02-03 18:58:24 +01:00
James Cole
304fae439a Expand listeners and observers. 2026-02-03 05:42:50 +01:00
James Cole
9ca81cf305 Clean up observer. 2026-02-02 20:15:06 +01:00
James Cole
cad5fb6d6b Clean up budget limit observer. 2026-02-02 20:13:00 +01:00
James Cole
53efafdbb2 Rename observers 2026-02-02 20:09:23 +01:00
James Cole
7922017288 Rename observers 2026-02-02 20:08:37 +01:00
James Cole
4e910a33dd Clean up events. 2026-02-02 20:02:45 +01:00
James Cole
bb031cdeb6 Clean up simple observers. 2026-02-02 19:59:05 +01:00
James Cole
60026bbcba Fix convert to primary amount. 2026-02-02 19:55:18 +01:00
James Cole
e96a1850da Merge branch 'develop' of github.com:firefly-iii/firefly-iii into develop 2026-02-02 19:38:00 +01:00
James Cole
bd2a746b8a Add new function. 2026-02-02 19:37:53 +01:00
github-actions[bot]
e4a3cbc9da Merge pull request #11640 from firefly-iii/release-1770044183
🤖 Automatically merge the PR into the develop branch.
2026-02-02 15:56:32 +01:00
JC5
18734b0edd 🤖 Auto commit for release 'develop' on 2026-02-02 2026-02-02 15:56:24 +01:00
Sander Dorigo
f52b3bf5f5 Merge branch 'develop' of https://github.com/firefly-iii/firefly-iii into develop 2026-02-02 15:39:06 +01:00
Sander Dorigo
3abba71f8d Fix null pointer 2026-02-02 15:39:03 +01:00
James Cole
6f558f424d Clean up notifications. 2026-01-31 11:51:34 +01:00
James Cole
4387876203 Replace lengthy notification calls, simplifies code. 2026-01-31 11:49:04 +01:00
115 changed files with 1711 additions and 2283 deletions

View File

@@ -31,7 +31,6 @@ use FireflyIII\Enums\UserRoleEnum;
use FireflyIII\Exceptions\FireflyException;
use FireflyIII\Models\Account;
use FireflyIII\Repositories\Account\AccountRepositoryInterface;
use FireflyIII\Support\Debug\Timer;
use FireflyIII\Support\Facades\Amount;
use FireflyIII\Support\Facades\Steam;
use FireflyIII\Support\Http\Api\AccountFilter;
@@ -80,7 +79,7 @@ class AccountController extends Controller
*/
public function accounts(AutocompleteApiRequest $request): JsonResponse
{
Log::debug('Before All.');
// Log::debug('Before All.');
['types' => $types, 'query' => $query, 'date' => $date, 'limit' => $limit] = $request->attributes->all();
$date ??= today(config('app.timezone'));
@@ -89,8 +88,6 @@ class AccountController extends Controller
$date->endOfDay();
$return = [];
$timer = Timer::getInstance();
$timer->start(sprintf('AC accounts "%s"', $query));
$result = $this->repository->searchAccount((string) $query, $types, $limit);
$allBalances = Steam::accountsBalancesOptimized($result, $date, $this->primaryCurrency, $this->convertToPrimary);
@@ -136,7 +133,6 @@ class AccountController extends Controller
return $posA - $posB;
});
$timer->stop(sprintf('AC accounts "%s"', $query));
return response()->api($return);
}

View File

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

View File

@@ -25,9 +25,6 @@ declare(strict_types=1);
namespace FireflyIII\Api\V1\Controllers\Models\Transaction;
use FireflyIII\Api\V1\Controllers\Controller;
use FireflyIII\Events\UpdatedAccount;
use FireflyIII\Models\Account;
use FireflyIII\Models\Transaction;
use FireflyIII\Models\TransactionGroup;
use FireflyIII\Models\TransactionJournal;
use FireflyIII\Repositories\Journal\JournalRepositoryInterface;
@@ -74,31 +71,9 @@ class DestroyController extends Controller
public function destroy(TransactionGroup $transactionGroup): JsonResponse
{
Log::debug(sprintf('Now in %s', __METHOD__));
// grab asset account(s) from group:
$accounts = [];
/** @var TransactionJournal $journal */
foreach ($transactionGroup->transactionJournals as $journal) {
/** @var Transaction $transaction */
foreach ($journal->transactions as $transaction) {
$type = $transaction->account->accountType->type;
// if is valid liability, trigger event!
if (in_array($type, config('firefly.valid_liabilities'), true)) {
$accounts[] = $transaction->account;
}
}
}
$this->groupRepository->destroy($transactionGroup);
Preferences::mark();
/** @var Account $account */
foreach ($accounts as $account) {
Log::debug(sprintf('Now going to trigger updated account event for account #%d', $account->id));
event(new UpdatedAccount($account));
}
return response()->json([], 204);
}

View File

@@ -27,14 +27,11 @@ 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\Model\TransactionGroup\CreatedSingleTransactionGroup;
use FireflyIII\Events\Model\TransactionGroup\TransactionGroupEventFlags;
use FireflyIII\Exceptions\DuplicateTransactionException;
use FireflyIII\Exceptions\FireflyException;
use FireflyIII\Helpers\Collector\GroupCollectorInterface;
use FireflyIII\Repositories\TransactionGroup\TransactionGroupRepositoryInterface;
use FireflyIII\Rules\IsDuplicateTransaction;
use FireflyIII\Support\Facades\Preferences;
use FireflyIII\Support\Http\Api\TransactionFilter;
use FireflyIII\Support\JsonApi\Enrichments\TransactionGroupEnrichment;
use FireflyIII\Transformers\TransactionGroupTransformer;
@@ -88,9 +85,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,22 +106,15 @@ class StoreController extends Controller
throw new ValidationException($validator);
}
Preferences::mark();
$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)
@@ -134,20 +124,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

@@ -26,7 +26,9 @@ namespace FireflyIII\Api\V1\Controllers\Models\Transaction;
use FireflyIII\Api\V1\Controllers\Controller;
use FireflyIII\Api\V1\Requests\Models\Transaction\UpdateRequest;
use FireflyIII\Events\UpdatedTransactionGroup;
use FireflyIII\Events\Model\TransactionGroup\TransactionGroupEventFlags;
use FireflyIII\Events\Model\TransactionGroup\TransactionGroupEventObjects;
use FireflyIII\Events\Model\TransactionGroup\UpdatedSingleTransactionGroup;
use FireflyIII\Helpers\Collector\GroupCollectorInterface;
use FireflyIII\Models\TransactionGroup;
use FireflyIII\Repositories\TransactionGroup\TransactionGroupRepositoryInterface;
@@ -72,27 +74,31 @@ class UpdateController extends Controller
public function update(UpdateRequest $request, TransactionGroup $transactionGroup): JsonResponse
{
Log::debug('Now in update routine for transaction group');
$data = $request->getAll();
$oldHash = $this->groupRepository->getCompareHash($transactionGroup);
$transactionGroup = $this->groupRepository->update($transactionGroup, $data);
$newHash = $this->groupRepository->getCompareHash($transactionGroup);
$manager = $this->getManager();
$data = $request->getAll();
$oldHash = $this->groupRepository->getCompareHash($transactionGroup);
$objects = TransactionGroupEventObjects::collectFromTransactionGroup($transactionGroup);
$transactionGroup = $this->groupRepository->update($transactionGroup, $data);
$objects->appendFromTransactionGroup($transactionGroup);
$newHash = $this->groupRepository->getCompareHash($transactionGroup);
$manager = $this->getManager();
Preferences::mark();
$applyRules = $data['apply_rules'] ?? true;
$fireWebhooks = $data['fire_webhooks'] ?? true;
$runRecalculations = $oldHash !== $newHash;
$applyRules = $data['apply_rules'] ?? true;
$fireWebhooks = $data['fire_webhooks'] ?? true;
$runRecalculations = $oldHash !== $newHash;
// FIXME responds to a single event.
// flags in array?
event(new UpdatedTransactionGroup($transactionGroup, $applyRules, $fireWebhooks, $runRecalculations));
$flags = new TransactionGroupEventFlags();
$flags->applyRules = $applyRules;
$flags->fireWebhooks = $fireWebhooks;
$flags->recalculateCredit = $runRecalculations;
event(new UpdatedSingleTransactionGroup($flags, $objects));
/** @var User $admin */
$admin = auth()->user();
$admin = auth()->user();
// use new group collector:
/** @var GroupCollectorInterface $collector */
$collector = app(GroupCollectorInterface::class);
$collector = app(GroupCollectorInterface::class);
$collector
->setUser($admin)
// filter on transaction group.
@@ -101,20 +107,20 @@ class UpdateController extends Controller
->withAPIInformation()
;
$selectedGroup = $collector->getGroups()->first();
$selectedGroup = $collector->getGroups()->first();
if (null === $selectedGroup) {
throw new NotFoundHttpException();
}
// enrich
$enrichment = new TransactionGroupEnrichment();
$enrichment = new TransactionGroupEnrichment();
$enrichment->setUser($admin);
$selectedGroup = $enrichment->enrichSingle($selectedGroup);
$selectedGroup = $enrichment->enrichSingle($selectedGroup);
/** @var TransactionGroupTransformer $transformer */
$transformer = app(TransactionGroupTransformer::class);
$transformer = app(TransactionGroupTransformer::class);
$transformer->setParameters($this->parameters);
$resource = new Item($selectedGroup, $transformer, 'transactions');
$resource = new Item($selectedGroup, $transformer, 'transactions');
return response()->json($manager->createData($resource)->toArray())->header('Content-Type', self::CONTENT_TYPE);
}

View File

@@ -25,7 +25,6 @@ declare(strict_types=1);
namespace FireflyIII\Api\V1\Controllers\System;
use FireflyIII\Api\V1\Controllers\Controller;
use FireflyIII\Events\Model\TransactionGroup\CreatedSingleTransactionGroup;
use FireflyIII\Events\Model\TransactionGroup\TransactionGroupEventFlags;
use FireflyIII\Events\Model\TransactionGroup\UserRequestedBatchProcessing;
use FireflyIII\Models\TransactionJournal;

View File

@@ -53,7 +53,7 @@ abstract class AggregateFormRequest extends ApiRequest
parent::initialize($query, $request, $attributes, $cookies, $files, $server, $content);
// instantiate all subrequests and share current requests' bags with them
Log::debug('Initializing AggregateFormRequest.');
// Log::debug('Initializing AggregateFormRequest.');
/** @var array|string $config */
foreach ($this->getRequests() as $config) {
@@ -62,7 +62,7 @@ abstract class AggregateFormRequest extends ApiRequest
if (!is_a($requestClass, Request::class, true)) {
throw new RuntimeException('getRequests() must return class-strings of subclasses of Request');
}
Log::debug(sprintf('Initializing subrequest %s', $requestClass));
// Log::debug(sprintf('Initializing subrequest %s', $requestClass));
$instance = $this->requests[] = new $requestClass();
$instance->request = $this->request;
@@ -77,7 +77,8 @@ abstract class AggregateFormRequest extends ApiRequest
$instance->handleConfig(is_array($config) ? $config : []);
}
}
Log::debug('Done initializing AggregateFormRequest.');
// Log::debug('Done initializing AggregateFormRequest.');
}
public function rules(): array
@@ -95,7 +96,7 @@ abstract class AggregateFormRequest extends ApiRequest
// register all subrequests' validators
foreach ($this->requests as $request) {
if (method_exists($request, 'withValidator')) {
Log::debug(sprintf('Process withValidator from class %s', $request::class));
// Log::debug(sprintf('Process withValidator from class %s', $request::class));
$request->withValidator($validator);
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -25,9 +25,7 @@ declare(strict_types=1);
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
{
@@ -37,9 +35,7 @@ class CreatedSingleTransactionGroup extends Event
* Create a new event instance.
*/
public function __construct(
public TransactionGroup $transactionGroup,
public TransactionGroupEventFlags $flags
) {
Log::debug(__METHOD__);
}
public TransactionGroupEventFlags $flags,
public TransactionGroupEventObjects $objects
) {}
}

View File

@@ -1,8 +1,10 @@
<?php
/**
* StoredTransactionGroup.php
* Copyright (c) 2019 james@firefly-iii.org
declare(strict_types=1);
/*
* DestroyedSingleTransactionGroup.php
* Copyright (c) 2026 james@firefly-iii.org
*
* This file is part of Firefly III (https://github.com/firefly-iii).
*
@@ -20,17 +22,12 @@
* 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 FireflyIII\Models\TransactionGroup;
use FireflyIII\Events\Event;
use Illuminate\Queue\SerializesModels;
/**
* Class StoredTransactionGroup.
*/
class StoredTransactionGroup extends Event
class DestroyedSingleTransactionGroup extends Event
{
use SerializesModels;
@@ -38,8 +35,7 @@ class StoredTransactionGroup extends Event
* Create a new event instance.
*/
public function __construct(
public TransactionGroup $transactionGroup,
public bool $applyRules,
public bool $fireWebhooks
public TransactionGroupEventFlags $flags,
public TransactionGroupEventObjects $objects
) {}
}

View File

@@ -0,0 +1,72 @@
<?php
declare(strict_types=1);
namespace FireflyIII\Events\Model\TransactionGroup;
use FireflyIII\Models\Transaction;
use FireflyIII\Models\TransactionGroup;
use FireflyIII\Models\TransactionJournal;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Log;
/**
* This class collects all objects before and after the creation, removal or updating
* of a transaction group. The idea is that this class contains all relevant objects.
* Right now, that means journals, tags, accounts, budgets and categories.
*
* By collecting these objects (in case of an update: before AND after update) there
* is a unified set of objects to manage: update balances, recalculate credits, etc.
*/
class TransactionGroupEventObjects
{
public Collection $accounts;
public Collection $budgets;
public Collection $categories;
public Collection $tags;
public Collection $transactionGroups;
public Collection $transactionJournals;
public function __construct()
{
$this->accounts = new Collection();
$this->budgets = new Collection();
$this->categories = new Collection();
$this->tags = new Collection();
$this->transactionGroups = new Collection();
$this->transactionJournals = new Collection();
}
public static function collectFromTransactionGroup(TransactionGroup $transactionGroup): self
{
Log::debug(sprintf('collectFromTransactionGroup(#%d)', $transactionGroup->id));
$object = new self();
$object->appendFromTransactionGroup($transactionGroup);
return $object;
}
public function appendFromTransactionGroup(TransactionGroup $transactionGroup): void
{
$this->transactionGroups->push($transactionGroup);
/** @var TransactionJournal $journal */
foreach ($transactionGroup->transactionJournals as $journal) {
$this->transactionJournals->push($journal);
$this->budgets = $this->budgets->merge($journal->budgets);
$this->categories = $this->categories->merge($journal->categories);
$this->tags = $this->tags->merge($journal->tags);
/** @var Transaction $transaction */
foreach ($journal->transactions as $transaction) {
$this->accounts->push($transaction->account);
}
}
$this->transactionGroups = $this->transactionGroups->unique('id');
$this->transactionJournals = $this->transactionJournals->unique('id');
$this->budgets = $this->budgets->unique('id');
$this->categories = $this->categories->unique('id');
$this->tags = $this->tags->unique('id');
$this->accounts = $this->accounts->unique('id');
}
}

View File

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

View File

@@ -1,8 +1,10 @@
<?php
/**
* UpdatedTransactionGroup.php
* Copyright (c) 2019 james@firefly-iii.org
declare(strict_types=1);
/*
* UpdatedSingleTransactionGroup.php
* Copyright (c) 2026 james@firefly-iii.org
*
* This file is part of Firefly III (https://github.com/firefly-iii).
*
@@ -20,17 +22,12 @@
* 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 FireflyIII\Models\TransactionGroup;
use FireflyIII\Events\Event;
use Illuminate\Queue\SerializesModels;
/**
* Class UpdatedTransactionGroup.
*/
class UpdatedTransactionGroup extends Event
class UpdatedSingleTransactionGroup extends Event
{
use SerializesModels;
@@ -38,9 +35,7 @@ class UpdatedTransactionGroup extends Event
* Create a new event instance.
*/
public function __construct(
public TransactionGroup $transactionGroup,
public bool $applyRules,
public bool $fireWebhooks,
public bool $runRecalculations
public TransactionGroupEventFlags $flags,
public TransactionGroupEventObjects $objects
) {}
}

View File

@@ -109,10 +109,6 @@ class TransactionJournalFactory
public function create(array $data): Collection
{
Log::debug('Now in TransactionJournalFactory::create()');
// convert to special object.
// $dataObject = new NullArrayObject($data);
Log::debug('Start of TransactionJournalFactory::create()');
$collection = new Collection();
$transactions = $data['transactions'] ?? [];
if (0 === count($transactions)) {
@@ -188,7 +184,6 @@ class TransactionJournalFactory
$carbon->setTimezone(config('app.timezone'));
// 2024-11-19, overrule timezone with UTC and store it as UTC.
if (true === FireflyConfig::get('utc', false)->data) {
$carbon->setTimezone('UTC');
}
@@ -333,7 +328,7 @@ class TransactionJournalFactory
throw new FireflyException($e->getMessage(), 0, $e);
}
Log::debug(sprintf('Is part of a batch submission? %s', var_export($row['batch_submission'], 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);

View File

@@ -0,0 +1,46 @@
<?php
declare(strict_types=1);
/*
* WebhookMessageFactory.php
* Copyright (c) 2026 james@firefly-iii.org
*
* This file is part of Firefly III (https://github.com/firefly-iii).
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
namespace FireflyIII\Factory;
use FireflyIII\Models\Webhook;
use FireflyIII\Models\WebhookMessage;
use Illuminate\Support\Facades\Log;
class WebhookMessageFactory
{
public function create(Webhook $webhook, array $data): WebhookMessage
{
$webhookMessage = new WebhookMessage();
$webhookMessage->webhook()->associate($webhook);
$webhookMessage->sent = false;
$webhookMessage->errored = false;
$webhookMessage->uuid = $data['uuid'];
$webhookMessage->message = $data;
$webhookMessage->save();
Log::debug(sprintf('Stored new webhook message #%d', $webhookMessage->id));
return $webhookMessage;
}
}

View File

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

View File

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

View File

@@ -1,74 +0,0 @@
<?php
/*
* DestroyedGroupEventHandler.php
* Copyright (c) 2021 james@firefly-iii.org
*
* This file is part of Firefly III (https://github.com/firefly-iii).
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
declare(strict_types=1);
namespace FireflyIII\Handlers\Events;
use FireflyIII\Enums\WebhookTrigger;
use FireflyIII\Events\DestroyedTransactionGroup;
use FireflyIII\Events\Model\Webhook\WebhookMessagesRequestSending;
use FireflyIII\Generator\Webhook\MessageGeneratorInterface;
use FireflyIII\Support\Facades\FireflyConfig;
use FireflyIII\Support\Models\AccountBalanceCalculator;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Log;
/**
* Class DestroyedGroupEventHandler
*/
class DestroyedGroupEventHandler
{
public function runAllHandlers(DestroyedTransactionGroup $event): void
{
$this->triggerWebhooks($event);
$this->updateRunningBalance($event);
}
private function triggerWebhooks(DestroyedTransactionGroup $destroyedGroupEvent): void
{
Log::debug('DestroyedTransactionGroup:triggerWebhooks');
$group = $destroyedGroupEvent->transactionGroup;
$user = $group->user;
/** @var MessageGeneratorInterface $engine */
$engine = app(MessageGeneratorInterface::class);
$engine->setUser($user);
$engine->setObjects(new Collection()->push($group));
$engine->setTrigger(WebhookTrigger::DESTROY_TRANSACTION);
$engine->generateMessages();
Log::debug(sprintf('send event WebhookMessagesRequestSending from %s', __METHOD__));
event(new WebhookMessagesRequestSending());
}
private function updateRunningBalance(DestroyedTransactionGroup $event): void
{
if (false === FireflyConfig::get('use_running_balance', config('firefly.feature_flags.running_balance_column'))->data) {
return;
}
Log::debug(__METHOD__);
$group = $event->transactionGroup;
foreach ($group->transactionJournals as $journal) {
AccountBalanceCalculator::recalculateForJournal($journal);
}
}
}

View File

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

View File

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

View File

@@ -1,225 +0,0 @@
<?php
/**
* UpdatedGroupEventHandler.php
* Copyright (c) 2019 james@firefly-iii.org
*
* This file is part of Firefly III (https://github.com/firefly-iii).
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
declare(strict_types=1);
namespace FireflyIII\Handlers\Events;
use FireflyIII\Enums\TransactionTypeEnum;
use FireflyIII\Enums\WebhookTrigger;
use FireflyIII\Events\Model\Webhook\WebhookMessagesRequestSending;
use FireflyIII\Events\UpdatedTransactionGroup;
use FireflyIII\Generator\Webhook\MessageGeneratorInterface;
use FireflyIII\Models\Account;
use FireflyIII\Models\Transaction;
use FireflyIII\Models\TransactionJournal;
use FireflyIII\Repositories\PeriodStatistic\PeriodStatisticRepositoryInterface;
use FireflyIII\Repositories\RuleGroup\RuleGroupRepositoryInterface;
use FireflyIII\Services\Internal\Support\CreditRecalculateService;
use FireflyIII\Support\Facades\FireflyConfig;
use FireflyIII\Support\Models\AccountBalanceCalculator;
use FireflyIII\TransactionRules\Engine\RuleEngineInterface;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Log;
/**
* Class UpdatedGroupEventHandler
*/
class UpdatedGroupEventHandler
{
public function runAllHandlers(UpdatedTransactionGroup $event): void
{
$this->unifyAccounts($event);
$this->processRules($event);
$this->recalculateCredit($event);
$this->triggerWebhooks($event);
$this->removePeriodStatistics($event);
if ($event->runRecalculations) {
$this->updateRunningBalance($event);
}
}
/**
* TODO duplicate
*/
private function removePeriodStatistics(UpdatedTransactionGroup $event): void
{
/** @var PeriodStatisticRepositoryInterface $repository */
$repository = app(PeriodStatisticRepositoryInterface::class);
/** @var TransactionJournal $journal */
foreach ($event->transactionGroup->transactionJournals as $journal) {
$source = $journal->transactions()->where('amount', '<', '0')->first();
$dest = $journal->transactions()->where('amount', '>', '0')->first();
if (null !== $source) {
$repository->deleteStatisticsForModel($source->account, $journal->date);
}
if (null !== $dest) {
$repository->deleteStatisticsForModel($dest->account, $journal->date);
}
$categories = $journal->categories;
$tags = $journal->tags;
$budgets = $journal->budgets;
foreach ($categories as $category) {
$repository->deleteStatisticsForModel($category, $journal->date);
}
foreach ($tags as $tag) {
$repository->deleteStatisticsForModel($tag, $journal->date);
}
foreach ($budgets as $budget) {
$repository->deleteStatisticsForModel($budget, $journal->date);
}
if (0 === $categories->count()) {
$repository->deleteStatisticsForPrefix($journal->userGroup, 'no_category', $journal->date);
}
if (0 === $budgets->count()) {
$repository->deleteStatisticsForPrefix($journal->userGroup, 'no_budget', $journal->date);
}
}
}
/**
* This method will make sure all source / destination accounts are the same.
*/
public function unifyAccounts(UpdatedTransactionGroup $updatedGroupEvent): void
{
$group = $updatedGroupEvent->transactionGroup;
if (1 === $group->transactionJournals->count()) {
return;
}
// first journal:
/** @var null|TransactionJournal $first */
$first = $group
->transactionJournals()
->orderBy('transaction_journals.date', 'DESC')
->orderBy('transaction_journals.order', 'ASC')
->orderBy('transaction_journals.id', 'DESC')
->orderBy('transaction_journals.description', 'DESC')
->first()
;
if (null === $first) {
Log::warning(sprintf('Group #%d has no transaction journals.', $group->id));
return;
}
$all = $group->transactionJournals()->get()->pluck('id')->toArray();
/** @var Account $sourceAccount */
$sourceAccount = $first->transactions()->where('amount', '<', '0')->first()->account;
/** @var Account $destAccount */
$destAccount = $first->transactions()->where('amount', '>', '0')->first()->account;
$type = $first->transactionType->type;
if (TransactionTypeEnum::TRANSFER->value === $type || TransactionTypeEnum::WITHDRAWAL->value === $type) {
// set all source transactions to source account:
Transaction::whereIn('transaction_journal_id', $all)->where('amount', '<', 0)->update(['account_id' => $sourceAccount->id]);
}
if (TransactionTypeEnum::TRANSFER->value === $type || TransactionTypeEnum::DEPOSIT->value === $type) {
// set all destination transactions to destination account:
Transaction::whereIn('transaction_journal_id', $all)->where('amount', '>', 0)->update(['account_id' => $destAccount->id]);
}
}
/**
* This method will check all the rules when a journal is updated.
*/
private function processRules(UpdatedTransactionGroup $updatedGroupEvent): void
{
if (false === $updatedGroupEvent->applyRules) {
Log::info(sprintf('Will not run rules on group #%d', $updatedGroupEvent->transactionGroup->id));
return;
}
$journals = $updatedGroupEvent->transactionGroup->transactionJournals;
$array = [];
/** @var TransactionJournal $journal */
foreach ($journals as $journal) {
$array[] = $journal->id;
}
$journalIds = implode(',', $array);
Log::debug(sprintf('Add local operator for journal(s): %s', $journalIds));
// collect rules:
$ruleGroupRepository = app(RuleGroupRepositoryInterface::class);
$ruleGroupRepository->setUser($updatedGroupEvent->transactionGroup->user);
$groups = $ruleGroupRepository->getRuleGroupsWithRules('update-journal');
// file rule engine.
$newRuleEngine = app(RuleEngineInterface::class);
$newRuleEngine->setUser($updatedGroupEvent->transactionGroup->user);
$newRuleEngine->addOperator(['type' => 'journal_id', 'value' => $journalIds]);
$newRuleEngine->setRuleGroups($groups);
$newRuleEngine->fire();
}
private function recalculateCredit(UpdatedTransactionGroup $event): void
{
$group = $event->transactionGroup;
/** @var CreditRecalculateService $object */
$object = app(CreditRecalculateService::class);
$object->setGroup($group);
$object->recalculate();
}
private function triggerWebhooks(UpdatedTransactionGroup $updatedGroupEvent): void
{
Log::debug(__METHOD__);
$group = $updatedGroupEvent->transactionGroup;
if (false === $updatedGroupEvent->fireWebhooks) {
Log::info(sprintf('Will not fire webhooks for transaction group #%d', $group->id));
return;
}
$user = $group->user;
/** @var MessageGeneratorInterface $engine */
$engine = app(MessageGeneratorInterface::class);
$engine->setUser($user);
$engine->setObjects(new Collection()->push($group));
$engine->setTrigger(WebhookTrigger::UPDATE_TRANSACTION);
$engine->generateMessages();
Log::debug(sprintf('send event WebhookMessagesRequestSending from %s', __METHOD__));
event(new WebhookMessagesRequestSending());
}
private function updateRunningBalance(UpdatedTransactionGroup $event): void
{
if (false === FireflyConfig::get('use_running_balance', config('firefly.feature_flags.running_balance_column'))->data) {
return;
}
Log::debug(__METHOD__);
$group = $event->transactionGroup;
foreach ($group->transactionJournals as $journal) {
AccountBalanceCalculator::recalculateForJournal($journal);
}
}
}

View File

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

View File

@@ -0,0 +1,104 @@
<?php
declare(strict_types=1);
/*
* ConvertsAmountToPrimaryAmount.php
* Copyright (c) 2026 james@firefly-iii.org
*
* This file is part of Firefly III (https://github.com/firefly-iii).
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
namespace FireflyIII\Handlers\ExchangeRate;
use FireflyIII\Support\Facades\Amount;
use FireflyIII\Support\Http\Api\ExchangeRateConverter;
use Illuminate\Support\Facades\Log;
class ConvertsAmountToPrimaryAmount
{
public static function convert(ConversionParameters $params): void
{
$amountField = $params->amountField;
$primaryAmountField = $params->primaryAmountField;
if (!Amount::convertToPrimary($params->user)) {
Log::debug(sprintf(
'User does not want to do conversion, no need to convert %s and store it in field %s for %s #%d.',
$params->amountField,
$params->primaryAmountField,
get_class($params->model),
$params->model->id
));
$params->model->{$primaryAmountField} = null;
$params->model->saveQuietly();
return;
}
if (null === $params->originalCurrency) {
Log::debug(sprintf(
'Original currency field is empty, no need to convert %s and store it in field %s for %s #%d.',
$params->amountField,
$params->primaryAmountField,
get_class($params->model),
$params->model->id
));
return;
}
$primaryCurrency = Amount::getPrimaryCurrencyByUserGroup($params->user->userGroup);
Log::debug(sprintf(
'Will convert amount in field %s from %s to %s and store it in %s',
$params->originalCurrency->code,
$primaryCurrency->code,
$params->amountField,
$params->primaryAmountField
));
if ($params->originalCurrency->id === $primaryCurrency->id) {
Log::debug('Both currencies are the same, do nothing.');
return;
}
// field is empty or zero, do nothing.
$amount = (string) $params->model->{$amountField};
if ('' === $amount || 0 === bccomp($amount, '0')) {
Log::debug(sprintf('Amount "%s" in field "%s" cannot be used, do nothing.', $amount, $amountField));
$params->model->{$amountField} = null;
$params->model->{$primaryAmountField} = null;
$params->model->saveQuietly();
return;
}
$converter = new ExchangeRateConverter();
$converter->setUserGroup($params->user->userGroup);
$converter->setIgnoreSettings(true);
$newAmount = $converter->convert($params->originalCurrency, $primaryCurrency, now(), $amount);
$params->model->{$primaryAmountField} = $newAmount;
$params->model->saveQuietly();
Log::debug(sprintf(
'Converted field "%s" of %s #%d from %s %s to %s %s (in field "%s")',
$amountField,
get_class($params->model),
$params->model->id,
$params->originalCurrency->code,
$amount,
$primaryCurrency->code,
$newAmount,
$primaryAmountField
));
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -33,14 +33,8 @@ use FireflyIII\Repositories\Attachment\AttachmentRepositoryInterface;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
/**
* Class AccountObserver
*/
class AccountObserver
class DeletedAccountObserver
{
/**
* Also delete related objects.
*/
public function deleting(Account $account): void
{
Log::debug('Observe "deleting" of an account.');

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -24,7 +24,9 @@ declare(strict_types=1);
namespace FireflyIII\Http\Controllers\Transaction;
use FireflyIII\Events\UpdatedTransactionGroup;
use FireflyIII\Events\Model\TransactionGroup\TransactionGroupEventFlags;
use FireflyIII\Events\Model\TransactionGroup\TransactionGroupEventObjects;
use FireflyIII\Events\Model\TransactionGroup\UpdatedSingleTransactionGroup;
use FireflyIII\Http\Controllers\Controller;
use FireflyIII\Http\Requests\BulkEditJournalRequest;
use FireflyIII\Models\TransactionJournal;
@@ -115,11 +117,15 @@ class BulkController extends Controller
}
}
$flags = new TransactionGroupEventFlags();
$objects = new TransactionGroupEventObjects();
// run rules on changed journals:
/** @var TransactionJournal $journal */
foreach ($collection as $journal) { // @phpstan-ignore-line
event(new UpdatedTransactionGroup($journal->transactionGroup, true, true, false));
foreach ($collection as $journal) {
$objects->appendFromTransactionGroup($journal->transactionGroup);
}
event(new UpdatedSingleTransactionGroup($flags, $objects));
Preferences::mark();
$request->session()->flash('success', trans_choice('firefly.mass_edited_transactions_success', $count));

View File

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

View File

@@ -24,8 +24,6 @@ declare(strict_types=1);
namespace FireflyIII\Http\Controllers\Transaction;
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,11 +74,6 @@ class CreateController extends Controller
$service = app(GroupCloneService::class);
$newGroup = $service->cloneGroup($group);
// event!
$flags = new TransactionGroupEventFlags();
event(new CreatedSingleTransactionGroup($group, $flags));
// event(new StoredTransactionGroup($newGroup, true, true));
Preferences::mark();
$title = $newGroup->title ?? $newGroup->transactionJournals->first()->description;

View File

@@ -24,12 +24,9 @@ declare(strict_types=1);
namespace FireflyIII\Http\Controllers\Transaction;
use FireflyIII\Events\UpdatedAccount;
use FireflyIII\Http\Controllers\Controller;
use FireflyIII\Models\Account;
use FireflyIII\Models\Transaction;
use FireflyIII\Models\TransactionGroup;
use FireflyIII\Models\TransactionJournal;
use FireflyIII\Repositories\TransactionGroup\TransactionGroupRepositoryInterface;
use FireflyIII\Support\Facades\Preferences;
use FireflyIII\Support\Facades\Steam;
@@ -112,29 +109,7 @@ class DeleteController extends Controller
}
$objectType = strtolower($journal->transaction_type_type ?? $journal->transactionType->type);
session()->flash('success', (string) trans('firefly.deleted_'.strtolower($objectType), ['description' => $group->title ?? $journal->description]));
// grab asset account(s) from group:
$accounts = [];
/** @var TransactionJournal $currentJournal */
foreach ($group->transactionJournals as $currentJournal) {
/** @var Transaction $transaction */
foreach ($currentJournal->transactions as $transaction) {
$type = $transaction->account->accountType->type;
// if is valid liability, trigger event!
if (in_array($type, config('firefly.valid_liabilities'), true)) {
$accounts[] = $transaction->account;
}
}
}
$this->repository->destroy($group);
/** @var Account $account */
foreach ($accounts as $account) {
Log::debug(sprintf('Now going to trigger updated account event for account #%d', $account->id));
event(new UpdatedAccount($account));
}
Preferences::mark();
return redirect($this->getPreviousUrl('transactions.delete.url'));

View File

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

View File

@@ -25,8 +25,6 @@ declare(strict_types=1);
namespace FireflyIII\Jobs;
use Carbon\Carbon;
use FireflyIII\Events\Model\TransactionGroup\CreatedSingleTransactionGroup;
use FireflyIII\Events\Model\TransactionGroup\TransactionGroupEventFlags;
use FireflyIII\Events\Model\TransactionGroup\TransactionGroupsRequestedReporting;
use FireflyIII\Exceptions\DuplicateTransactionException;
use FireflyIII\Exceptions\FireflyException;
@@ -336,7 +334,7 @@ class CreateRecurringTransactions implements ShouldQueue
Log::debug(sprintf('%s IS today (%s)', $date->format('Y-m-d'), $this->date->format('Y-m-d')));
// count created journals on THIS day.
$journalCount = $this->repository->getJournalCount($recurrence, $date, $date);
$journalCount = $this->repository->getJournalCount($recurrence, $date, $date);
if ($journalCount > 0 && false === $this->force) {
Log::info(sprintf('Already created %d journal(s) for date %s', $journalCount, $date->format('Y-m-d')));
@@ -354,11 +352,11 @@ class CreateRecurringTransactions implements ShouldQueue
}
// create transaction array and send to factory.
$groupTitle = null;
$count = $recurrence->recurrenceTransactions->count();
$groupTitle = null;
$count = $recurrence->recurrenceTransactions->count();
// #8844, if there is one recurrence transaction, use the first title as the title.
// #9305, if there is one recurrence transaction, group title must be NULL.
$groupTitle = null;
$groupTitle = null;
// #8844, if there are more, use the recurrence transaction itself.
if ($count > 1) {
@@ -371,7 +369,7 @@ class CreateRecurringTransactions implements ShouldQueue
return null;
}
$array = [
$array = [
'user' => $recurrence->user,
'user_group' => $recurrence->user->userGroup,
'group_title' => $groupTitle,
@@ -379,21 +377,13 @@ class CreateRecurringTransactions implements ShouldQueue
];
/** @var TransactionGroup $group */
$group = $this->groupRepository->store($array);
$group = $this->groupRepository->store($array);
++$this->created;
Log::info(sprintf('Created new transaction group #%d', $group->id));
// trigger event:
$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:
$recurrence->latest_date = $date;
$recurrence->latest_date_tz = $date->format('e');
$recurrence->save();
$this->repository->setLatestDate($recurrence, $date);
return $group;
}

View File

@@ -26,14 +26,15 @@ namespace FireflyIII\Listeners\Model\Account;
use FireflyIII\Events\Model\Account\CreatedNewAccount;
use FireflyIII\Events\Model\Account\UpdatedExistingAccount;
use FireflyIII\Handlers\ExchangeRate\ConversionParameters;
use FireflyIII\Handlers\ExchangeRate\ConvertsAmountToPrimaryAmount;
use FireflyIII\Models\Account;
use FireflyIII\Repositories\Account\AccountRepositoryInterface;
use FireflyIII\Services\Internal\Support\CreditRecalculateService;
use FireflyIII\Support\Facades\Amount;
use FireflyIII\Support\Http\Api\ExchangeRateConverter;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Support\Facades\Log;
class UpdatesAccountInformation
class UpdatesAccountInformation implements ShouldQueue
{
public function handle(CreatedNewAccount|UpdatedExistingAccount $event): void
{
@@ -43,7 +44,7 @@ class UpdatesAccountInformation
private function recalculateCredit(Account $account): void
{
Log::debug('Will call CreditRecalculateService because a new account was created.');
Log::debug('Will call CreditRecalculateService because a new account was created or updated.');
/** @var CreditRecalculateService $object */
$object = app(CreditRecalculateService::class);
@@ -53,32 +54,21 @@ class UpdatesAccountInformation
private function updateVirtualBalance(Account $account): void
{
if (!Amount::convertToPrimary($account->user)) {
Log::debug('After account creation, no need to convert virtual balance.');
Log::debug('Will updateVirtualBalance');
$repository = app(AccountRepositoryInterface::class);
$currency = $repository->getAccountCurrency($account);
return;
if (null !== $currency) {
// only when the account has a currency, because that is the only way for the
// account to have a virtual balance.
$params = new ConversionParameters();
$params->user = $account->user;
$params->model = $account;
$params->originalCurrency = $currency;
$params->amountField = 'virtual_balance';
$params->primaryAmountField = 'native_virtual_balance';
ConvertsAmountToPrimaryAmount::convert($params);
Log::debug('Account primary currency virtual balance is updated.');
}
Log::debug('After account creation, convert virtual balance.');
$userCurrency = Amount::getPrimaryCurrencyByUserGroup($account->user->userGroup);
$repository = app(AccountRepositoryInterface::class);
$currency = $repository->getAccountCurrency($account);
if (
null !== $currency
&& $currency->id !== $userCurrency->id
&& '' !== (string) $account->virtual_balance
&& 0 !== bccomp($account->virtual_balance, '0')
) {
$converter = new ExchangeRateConverter();
$converter->setUserGroup($account->user->userGroup);
$converter->setIgnoreSettings(true);
$account->native_virtual_balance = $converter->convert($currency, $userCurrency, today(), $account->virtual_balance);
}
if ('' === (string) $account->virtual_balance || 0 === bccomp($account->virtual_balance, '0')) {
$account->virtual_balance = null;
$account->native_virtual_balance = null;
}
$account->saveQuietly();
// Log::debug('Account primary currency virtual balance is updated.');
}
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,56 @@
<?php
declare(strict_types=1);
/*
* ProcessesDestroyedTransactionGroup.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 FireflyIII\Enums\WebhookTrigger;
use FireflyIII\Events\Model\TransactionGroup\DestroyedSingleTransactionGroup;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Support\Facades\Log;
class ProcessesDestroyedTransactionGroup implements ShouldQueue
{
use SupportsGroupProcessingTrait;
public function handle(DestroyedSingleTransactionGroup $event): void
{
Log::debug(sprintf('User called %s', get_class($event)));
if (!$event->flags->recalculateCredit) {
Log::debug(sprintf('Will NOT recalculate credit for %d journal(s)', $event->objects->transactionJournals->count()));
}
if (!$event->flags->fireWebhooks) {
Log::debug(sprintf('Will NOT fire webhooks for %d journal(s)', $event->objects->transactionJournals->count()));
}
if ($event->flags->recalculateCredit) {
$this->recalculateCredit($event->objects->accounts);
}
if ($event->flags->fireWebhooks) {
$this->createWebhookMessages($event->objects->transactionGroups, WebhookTrigger::DESTROY_TRANSACTION);
}
$this->removePeriodStatistics($event->objects);
$this->recalculateRunningBalance($event->objects);
}
}

View File

@@ -24,192 +24,53 @@ declare(strict_types=1);
namespace FireflyIII\Listeners\Model\TransactionGroup;
use Carbon\Carbon;
use FireflyIII\Enums\WebhookTrigger;
use FireflyIII\Events\Model\TransactionGroup\CreatedSingleTransactionGroup;
use FireflyIII\Events\Model\TransactionGroup\UserRequestedBatchProcessing;
use FireflyIII\Events\Model\Webhook\WebhookMessagesRequestSending;
use FireflyIII\Generator\Webhook\MessageGeneratorInterface;
use FireflyIII\Models\Account;
use FireflyIII\Models\TransactionGroup;
use FireflyIII\Models\TransactionJournal;
use FireflyIII\Models\TransactionJournalMeta;
use FireflyIII\Repositories\Journal\JournalRepositoryInterface;
use FireflyIII\Repositories\PeriodStatistic\PeriodStatisticRepositoryInterface;
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
{
use SupportsGroupProcessingTrait;
public function handle(CreatedSingleTransactionGroup|UserRequestedBatchProcessing $event): void
{
$groupId = 0;
$collection = new Collection();
if ($event instanceof CreatedSingleTransactionGroup) {
Log::debug(sprintf('In ProcessesNewTransactionGroup::handle(#%d)', $event->transactionGroup->id));
$groupId = $event->transactionGroup->id;
$collection = $event->transactionGroup->transactionJournals;
}
if ($event instanceof UserRequestedBatchProcessing) {
Log::debug('User called UserRequestedBatchProcessing');
}
Log::debug(sprintf('Running event handler for %s', get_class($event)));
$setting = FireflyConfig::get('enable_batch_processing', false)->data;
if (true === $event->flags->batchSubmission && true === $setting) {
Log::debug(sprintf('Will do nothing for group #%d because it is part of a batch.', $groupId));
Log::debug('Will do nothing for event because it is part of a batch.');
return;
}
Log::debug(sprintf('Will (joined with group #%d) collect all open transaction groups and process them.', $groupId));
$repository = app(JournalRepositoryInterface::class);
$set = $collection->merge($repository->getUncompletedJournals());
if (0 === $set->count()) {
Log::debug('Set is empty, never mind.');
$journals = $event->objects->transactionJournals->merge($repository->getAllUncompletedJournals());
return;
}
Log::debug(sprintf('Set count is %d', $set->count()));
Log::debug(sprintf('Transaction journal count is %d', $journals->count()));
if (!$event->flags->applyRules) {
Log::debug(sprintf('Will NOT process rules for %d journal(s)', $set->count()));
Log::debug(sprintf('Will NOT process rules for %d journal(s)', $journals->count()));
}
if (!$event->flags->recalculateCredit) {
Log::debug(sprintf('Will NOT recalculate credit for %d journal(s)', $set->count()));
Log::debug(sprintf('Will NOT recalculate credit for %d journal(s)', $journals->count()));
}
if (!$event->flags->fireWebhooks) {
Log::debug(sprintf('Will NOT fire webhooks for %d journal(s)', $set->count()));
Log::debug(sprintf('Will NOT fire webhooks for %d journal(s)', $journals->count()));
}
if ($event->flags->applyRules) {
$this->processRules($set);
$this->processRules($journals, 'store-journal');
}
if ($event->flags->recalculateCredit) {
$this->recalculateCredit($set);
$this->recalculateCredit($event->objects->accounts);
}
if ($event->flags->fireWebhooks) {
$this->fireWebhooks($set);
$this->createWebhookMessages($event->objects->transactionGroups, WebhookTrigger::STORE_TRANSACTION);
}
// always remove old relevant statistics.
$this->removePeriodStatistics($set);
// recalculate running balance if necessary.
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']);
/** @var Carbon $earliest */
$earliest = $date->lt($earliest) ? $date : $earliest;
}
Log::debug(sprintf('Found earliest date: %s', $earliest->toW3cString()));
// 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.*'])
;
Log::debug('Found accounts to process', $accounts->pluck('id')->toArray());
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__);
/** @var TransactionJournal $first */
$first = $set->first();
$user = $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();
/** @var TransactionJournal $first */
$first = $set->first();
$journalIds = implode(',', $array);
$user = $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();
$this->removePeriodStatistics($event->objects);
$this->recalculateRunningBalance($event->objects);
$repository->markAsCompleted($journals);
}
}

View File

@@ -0,0 +1,128 @@
<?php
declare(strict_types=1);
/*
* ProcessesUpdatedTransactionGroup.php
* Copyright (c) 2026 james@firefly-iii.org
*
* This file is part of Firefly III (https://github.com/firefly-iii).
*
* 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 FireflyIII\Enums\TransactionTypeEnum;
use FireflyIII\Enums\WebhookTrigger;
use FireflyIII\Events\Model\TransactionGroup\UpdatedSingleTransactionGroup;
use FireflyIII\Models\Account;
use FireflyIII\Models\Transaction;
use FireflyIII\Models\TransactionGroup;
use FireflyIII\Models\TransactionJournal;
use Illuminate\Support\Facades\Log;
class ProcessesUpdatedTransactionGroup
{
use SupportsGroupProcessingTrait;
public function handle(UpdatedSingleTransactionGroup $event): void
{
Log::debug(sprintf('Now handling event %s', get_class($event)));
$this->unifyAccounts($event);
Log::debug(sprintf('Transaction journal count is %d', $event->objects->transactionJournals->count()));
if (!$event->flags->applyRules) {
Log::debug(sprintf('Will NOT process rules for %d journal(s)', $event->objects->transactionJournals->count()));
}
if (!$event->flags->recalculateCredit) {
Log::debug(sprintf('Will NOT recalculate credit for %d journal(s)', $event->objects->transactionJournals->count()));
}
if (!$event->flags->fireWebhooks) {
Log::debug(sprintf('Will NOT fire webhooks for %d journal(s)', $event->objects->transactionJournals->count()));
}
if ($event->flags->applyRules) {
$this->processRules($event->objects->transactionJournals, 'update-journal');
}
if ($event->flags->recalculateCredit) {
$this->recalculateCredit($event->objects->accounts);
}
if ($event->flags->fireWebhooks) {
$this->createWebhookMessages($event->objects->transactionGroups, WebhookTrigger::UPDATE_TRANSACTION);
}
$this->removePeriodStatistics($event->objects);
$this->recalculateRunningBalance($event->objects);
Log::debug('Done with handle() for UpdatedSingleTransactionGroup');
}
/**
* This method will make sure all source / destination accounts are the same.
*/
protected function unifyAccounts(UpdatedSingleTransactionGroup $updatedGroupEvent): void
{
Log::debug('Now in unifyAccounts()');
/** @var TransactionGroup $group */
foreach ($updatedGroupEvent->objects->transactionGroups as $group) {
$this->unifyAccountsForGroup($group);
}
Log::debug('Done with unifyAccounts()');
}
private function unifyAccountsForGroup(TransactionGroup $group): void
{
if (1 === $group->transactionJournals->count()) {
Log::debug('Nothing to do in unifyAccounts()');
return;
}
// first journal:
/** @var null|TransactionJournal $first */
$first = $group
->transactionJournals()
->orderBy('transaction_journals.date', 'DESC')
->orderBy('transaction_journals.order', 'ASC')
->orderBy('transaction_journals.id', 'DESC')
->orderBy('transaction_journals.description', 'DESC')
->first()
;
if (null === $first) {
Log::warning(sprintf('Group #%d has no transaction journals.', $group->id));
return;
}
$all = $group->transactionJournals()->get()->pluck('id')->toArray();
/** @var Account $sourceAccount */
$sourceAccount = $first->transactions()->where('amount', '<', '0')->first()->account;
/** @var Account $destAccount */
$destAccount = $first->transactions()->where('amount', '>', '0')->first()->account;
$type = $first->transactionType->type;
if (TransactionTypeEnum::TRANSFER->value === $type || TransactionTypeEnum::WITHDRAWAL->value === $type) {
// set all source transactions to source account:
Transaction::whereIn('transaction_journal_id', $all)->where('amount', '<', 0)->update(['account_id' => $sourceAccount->id]);
}
if (TransactionTypeEnum::TRANSFER->value === $type || TransactionTypeEnum::DEPOSIT->value === $type) {
// set all destination transactions to destination account:
Transaction::whereIn('transaction_journal_id', $all)->where('amount', '>', 0)->update(['account_id' => $destAccount->id]);
}
}
}

View File

@@ -0,0 +1,170 @@
<?php
declare(strict_types=1);
namespace FireflyIII\Listeners\Model\TransactionGroup;
use Carbon\Carbon;
use FireflyIII\Enums\WebhookTrigger;
use FireflyIII\Events\Model\TransactionGroup\TransactionGroupEventObjects;
use FireflyIII\Generator\Webhook\MessageGeneratorInterface;
use FireflyIII\Models\Account;
use FireflyIII\Models\Budget;
use FireflyIII\Models\Category;
use FireflyIII\Models\Tag;
use FireflyIII\Models\TransactionGroup;
use FireflyIII\Models\TransactionJournal;
use FireflyIII\Models\TransactionJournalMeta;
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\Support\Collection;
use Illuminate\Support\Facades\Log;
trait SupportsGroupProcessingTrait
{
private function recalculateCredit(Collection $accounts): void
{
Log::debug(sprintf('Will now recalculateCredit for %d account(s)', $accounts->count()));
/** @var CreditRecalculateService $object */
$object = app(CreditRecalculateService::class);
$object->setAccounts($accounts);
$object->recalculate();
Log::debug(sprintf('Done with recalculateCredit for %d account(s)', $accounts->count()));
}
private function createWebhookMessages(Collection $groups, WebhookTrigger $trigger): void
{
Log::debug(sprintf('Will now create webhook messages for %d group(s)', $groups->count()));
/** @var TransactionGroup $first */
$first = $groups->first();
$user = $first->user;
/** @var MessageGeneratorInterface $engine */
$engine = app(MessageGeneratorInterface::class);
$engine->setUser($user);
// tell the generator which trigger it should look for
$engine->setTrigger($trigger);
// tell the generator which objects to process
$engine->setObjects($groups);
// tell the generator to generate the messages
$engine->generateMessages();
Log::debug(sprintf('Done with create webhook messages for %d group(s)', $groups->count()));
}
protected function removePeriodStatistics(TransactionGroupEventObjects $objects): void
{
if (!auth()->check()) {
Log::debug('Will NOT remove period statistics for all objects, because no user detected.');
}
Log::debug('Will now remove period statistics for all objects.');
// since you get a bunch of journals AND a bunch of
// objects, this needs to be a collection
/** @var PeriodStatisticRepositoryInterface $repository */
$repository = app(PeriodStatisticRepositoryInterface::class);
$dates = $this->collectDatesFromJournals($objects->transactionJournals);
$repository->deleteStatisticsForType(Account::class, $objects->accounts, $dates);
$repository->deleteStatisticsForType(Budget::class, $objects->budgets, $dates);
$repository->deleteStatisticsForType(Category::class, $objects->categories, $dates);
$repository->deleteStatisticsForType(Tag::class, $objects->tags, $dates);
// remove if no stuff present:
// remove for no tag, no cat, etc.
if (0 === $objects->budgets->count()) {
Log::debug('No budgets, delete "no_category" stats.');
$repository->deleteStatisticsForPrefix('no_budget', $dates);
}
if (0 === $objects->categories->count()) {
Log::debug('No categories, delete "no_category" stats.');
$repository->deleteStatisticsForPrefix('no_category', $dates);
}
if (0 === $objects->tags->count()) {
Log::debug('No tags, delete "no_category" stats.');
$repository->deleteStatisticsForPrefix('no_tag', $dates);
}
Log::debug('Done with remove period statistics for all objects.');
}
private function collectDatesFromJournals(Collection $journals): Collection
{
$collection = $journals->pluck('date');
if (0 === count($collection)) {
$collection->push(now(config('app.timezone')));
}
return $collection;
}
protected function processRules(Collection $set, string $type): void
{
Log::debug(sprintf('Will now processRules("%s") for %d journal(s)', $type, $set->count()));
$array = $set->pluck('id')->toArray();
/** @var TransactionJournal $first */
$first = $set->first();
$journalIds = implode(',', $array);
$user = $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(sprintf('Fire processRules with ALL %s rule groups.', $type));
$groups = $ruleGroupRepository->getRuleGroupsWithRules($type);
// create and fire rule engine.
$newRuleEngine = app(RuleEngineInterface::class);
$newRuleEngine->setUser($user);
$newRuleEngine->addOperator(['type' => 'journal_id', 'value' => $journalIds]);
$newRuleEngine->setRuleGroups($groups);
$newRuleEngine->fire();
Log::debug(sprintf('Done with processRules("%s") for %d journal(s)', $type, $set->count()));
}
protected function recalculateRunningBalance(TransactionGroupEventObjects $objects): void
{
Log::debug('Now in recalculateRunningBalance');
if (false === FireflyConfig::get('use_running_balance', config('firefly.feature_flags.running_balance_column'))->data) {
Log::debug('Running balance is disabled.');
return;
}
// find the earliest date in the set, based on date and _internal_previous_date
$earliest = $objects->transactionJournals->pluck('date')->sort()->first();
$fromInternalDate = $this->getFromInternalDate($objects->transactionJournals->pluck('id')->toArray());
$earliest = $fromInternalDate->lt($earliest) ? $fromInternalDate : $earliest;
Log::debug(sprintf('Found earliest date: %s', $earliest->toW3cString()));
Log::debug('Found accounts to process', $objects->accounts->pluck('id')->toArray());
AccountBalanceCalculator::optimizedCalculation($objects->accounts, $earliest);
}
private function getFromInternalDate(array $ids): Carbon
{
$entries = TransactionJournalMeta::whereIn('transaction_journal_id', $ids)->where('name', '_internal_previous_date')->get(['journal_meta.*']);
$array = $entries->toArray();
$return = today()->subDay();
if (count($array) > 0) {
usort($array, function (array $a, array $b) {
return Carbon::parse($a['data'])->gt(Carbon::parse($b['data']));
});
$date = Carbon::parse($array[0]['data']);
$return = $date->lt($return) ? $date : $return;
}
return $return;
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,44 @@
<?php
declare(strict_types=1);
/*
* NotifiesUserAboutNewAccessToken.php
* Copyright (c) 2026 james@firefly-iii.org
*
* This file is part of Firefly III (https://github.com/firefly-iii).
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
namespace FireflyIII\Listeners\Security\User;
use FireflyIII\Notifications\NotificationSender;
use FireflyIII\Notifications\User\NewAccessToken;
use FireflyIII\Repositories\User\UserRepositoryInterface;
use Laravel\Passport\Events\AccessTokenCreated;
class NotifiesUserAboutNewAccessToken
{
public function handle(AccessTokenCreated $event): void
{
/** @var UserRepositoryInterface $repository */
$repository = app(UserRepositoryInterface::class);
$user = $repository->find((int) $event->userId);
if (null !== $user) {
NotificationSender::send($user, new NewAccessToken());
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,59 @@
<?php
declare(strict_types=1);
/*
* NotificationSender.php
* Copyright (c) 2026 james@firefly-iii.org
*
* This file is part of Firefly III (https://github.com/firefly-iii).
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
namespace FireflyIII\Notifications;
use Exception;
use FireflyIII\Notifications\Notifiables\OwnerNotifiable;
use FireflyIII\User;
use GuzzleHttp\Exception\ClientException;
use Illuminate\Notifications\Notification;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Notification as NotificationFacade;
class NotificationSender
{
public static function send(OwnerNotifiable|User $user, Notification $notification): void
{
try {
NotificationFacade::send($user, $notification);
} catch (ClientException $e) {
Log::error(sprintf('[a] Error sending notification: %s', $e->getMessage()));
} catch (Exception $e) {
$message = $e->getMessage();
if (str_contains($message, 'Bcc')) {
Log::warning('[Bcc] Could not send notification. Please validate your email settings, use the .env.example file as a guide.');
return;
}
if (str_contains($message, 'RFC 2822')) {
Log::warning('[RFC] Could not send notification. Please validate your email settings, use the .env.example file as a guide.');
return;
}
Log::error($e->getMessage());
Log::error($e->getTraceAsString());
}
}
}

View File

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

View File

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

View File

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

View File

@@ -26,8 +26,6 @@ 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\Tag;
use FireflyIII\Models\Transaction;
@@ -36,7 +34,6 @@ 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;
@@ -184,65 +181,29 @@ class PeriodStatisticRepository implements PeriodStatisticRepositoryInterface, U
#[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.*']);
Log::debug('Collected transaction IDs', $transactions->pluck('id')->toArray());
// collect all accounts and delete stats:
$accounts = Account::whereIn('id', $transactions->pluck('account_id')->toArray())->get(['accounts.*']);
Log::debug('Collected account IDs', $accounts->pluck('id')->toArray());
$dates = $set->pluck('date');
$this->deleteStatisticsForType(Account::class, $accounts, $dates);
// 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.*']);
Log::debug('Collected category IDs', $categories->pluck('id')->toArray());
$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.*']);
Log::debug('Collected budget IDs', $categories->pluck('id')->toArray());
$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.*']);
Log::debug('Collected tag IDs', $categories->pluck('id')->toArray());
$this->deleteStatisticsForType(Tag::class, $tags, $dates);
// remove for no tag, no cat, etc.
if (0 === $categories->count()) {
Log::debug('No categories, delete "no_category" stats.');
$this->deleteStatisticsForPrefix('no_category', $dates);
}
if (0 === $budgets->count()) {
Log::debug('No budgets, delete "no_category" stats.');
$this->deleteStatisticsForPrefix('no_budget', $dates);
}
if (0 === $tags->count()) {
Log::debug('No tags, delete "no_category" stats.');
$this->deleteStatisticsForPrefix('no_tag', $dates);
}
// Log::debug(sprintf('Delete statistics for %d transaction journals.', count($set)));
// // collect all transactions:
// $transactions = Transaction::whereIn('transaction_journal_id', $set->pluck('id')->toArray())->get(['transactions.*']);
// Log::debug('Collected transaction IDs', $transactions->pluck('id')->toArray());
//
// // collect all accounts and delete stats:
// $accounts = Account::whereIn('id', $transactions->pluck('account_id')->toArray())->get(['accounts.*']);
// Log::debug('Collected account IDs', $accounts->pluck('id')->toArray());
// $dates = $set->pluck('date');
// $this->deleteStatisticsForType(Account::class, $accounts, $dates);
//
// // remove for no tag, no cat, etc.
// if (0 === $categories->count()) {
// Log::debug('No categories, delete "no_category" stats.');
// $this->deleteStatisticsForPrefix('no_category', $dates);
// }
// if (0 === $budgets->count()) {
// Log::debug('No budgets, delete "no_category" stats.');
// $this->deleteStatisticsForPrefix('no_budget', $dates);
// }
// if (0 === $tags->count()) {
// Log::debug('No tags, delete "no_category" stats.');
// $this->deleteStatisticsForPrefix('no_tag', $dates);
// }
}
}

View File

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

View File

@@ -48,6 +48,8 @@ use Illuminate\Support\Collection;
*/
interface RecurringRepositoryInterface
{
public function setLatestDate(Recurrence $recurrence, Carbon $date): Recurrence;
public function createdPreviously(Recurrence $recurrence, Carbon $date): bool;
/**

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