Compare commits

...

39 Commits

Author SHA1 Message Date
github-actions[bot]
e5923202af Merge pull request #10973 from firefly-iii/release-1758914746
🤖 Automatically merge the PR into the develop branch.
2025-09-26 21:25:55 +02:00
JC5
eb6f78406e 🤖 Auto commit for release 'develop' on 2025-09-26 2025-09-26 21:25:46 +02:00
James Cole
d61f87f649 Fix URL 2025-09-26 20:47:44 +02:00
James Cole
33dcce7525 Optimize query for collecting transactions. 2025-09-26 20:46:23 +02:00
James Cole
b1d86c3a37 Merge branch 'develop' of github.com:firefly-iii/firefly-iii into develop
# Conflicts:
#	app/Models/PeriodStatistic.php
2025-09-26 19:48:46 +02:00
James Cole
822dee6e70 Allow statistics to be removed from /flush 2025-09-26 19:48:20 +02:00
github-actions[bot]
f6dac83777 Merge pull request #10972 from firefly-iii/release-1758908619
🤖 Automatically merge the PR into the develop branch.
2025-09-26 19:43:52 +02:00
JC5
d3c557ca22 🤖 Auto commit for release 'develop' on 2025-09-26 2025-09-26 19:43:39 +02:00
James Cole
853a99852e Also remove them when updating transactions. 2025-09-26 19:39:18 +02:00
James Cole
8f24ac4fcd Remove statistics when creating a new journal. 2025-09-26 19:38:26 +02:00
James Cole
8b09cfb8c9 Optimize query for period statistics. 2025-09-26 19:32:53 +02:00
James Cole
18ae950d2e Optimize queries. 2025-09-26 06:09:44 +02:00
James Cole
69dfbda847 Add empty statistic if necessary. 2025-09-26 06:06:43 +02:00
James Cole
4ec2fcdb8a Optimize queries for statistics. 2025-09-26 06:05:37 +02:00
James Cole
08879d31ba Rearrange some code. 2025-09-26 05:33:35 +02:00
James Cole
66d09450d3 Merge branch 'develop' of github.com:firefly-iii/firefly-iii into develop 2025-09-25 19:17:20 +02:00
James Cole
74f7c07a76 Add filter for transfer type. 2025-09-25 19:17:10 +02:00
github-actions[bot]
ae3d0a3e49 Merge pull request #10964 from firefly-iii/release-1758820271
🤖 Automatically merge the PR into the develop branch.
2025-09-25 19:11:21 +02:00
JC5
61e8d7d7a2 🤖 Auto commit for release 'develop' on 2025-09-25 2025-09-25 19:11:11 +02:00
James Cole
62c5440605 Add table for period statistics, and see what happens re: performance. 2025-09-25 19:07:02 +02:00
James Cole
0aa90b9453 Fix #10960 2025-09-24 19:51:39 +02:00
Sander Dorigo
855bc2f8e7 attempted fix for #10956 2025-09-24 14:39:54 +02:00
github-actions[bot]
d8f05492c3 Merge pull request #10955 from firefly-iii/release-1758652896
🤖 Automatically merge the PR into the develop branch.
2025-09-23 20:41:46 +02:00
JC5
4a264f34fa 🤖 Auto commit for release 'develop' on 2025-09-23 2025-09-23 20:41:36 +02:00
James Cole
5a1413e758 Fix #10954 2025-09-23 20:22:58 +02:00
github-actions[bot]
84dbeeb0ce Merge pull request #10946 from firefly-iii/release-1758511378
🤖 Automatically merge the PR into the develop branch.
2025-09-22 05:23:04 +02:00
JC5
d868dc0945 🤖 Auto commit for release 'develop' on 2025-09-22 2025-09-22 05:22:58 +02:00
James Cole
beecf9c229 Make sure demo user cannot send notifications. 2025-09-21 18:00:23 +02:00
github-actions[bot]
e39ba46398 Merge pull request #10943 from firefly-iii/release-1758470243
🤖 Automatically merge the PR into the develop branch.
2025-09-21 17:57:31 +02:00
JC5
e6b6a3cee5 🤖 Auto commit for release 'develop' on 2025-09-21 2025-09-21 17:57:23 +02:00
James Cole
b5483f6ad3 Merge branch 'develop' of github.com:firefly-iii/firefly-iii into develop 2025-09-21 17:52:16 +02:00
James Cole
a751218d53 Fix rule order for #10938 2025-09-21 17:52:07 +02:00
github-actions[bot]
2af5e6eeef Merge pull request #10942 from firefly-iii/release-1758460508
🤖 Automatically merge the PR into the develop branch.
2025-09-21 15:15:17 +02:00
JC5
013c43f9f2 🤖 Auto commit for release 'develop' on 2025-09-21 2025-09-21 15:15:08 +02:00
James Cole
7e08a1f33c Possible fix for #10940, not sure. 2025-09-21 15:11:16 +02:00
James Cole
e592b56d7a Merge branch 'develop' of github.com:firefly-iii/firefly-iii into develop 2025-09-21 15:06:32 +02:00
github-actions[bot]
a2479f71fe Merge pull request #10937 from firefly-iii/release-1758437913
🤖 Automatically merge the PR into the develop branch.
2025-09-21 08:58:42 +02:00
JC5
7d3b993b98 🤖 Auto commit for release 'develop' on 2025-09-21 2025-09-21 08:58:33 +02:00
James Cole
90623101a3 Add earned + spent, needs cleaning up still. 2025-09-21 08:54:26 +02:00
111 changed files with 6057 additions and 5553 deletions

View File

@@ -402,16 +402,16 @@
},
{
"name": "friendsofphp/php-cs-fixer",
"version": "v3.87.2",
"version": "v3.88.0",
"source": {
"type": "git",
"url": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer.git",
"reference": "da5f0a7858c79b56fc0b8c36d3efcfe5f37f0992"
"reference": "f23469674ae50d40e398bfff8018911a2a2b0dbe"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/PHP-CS-Fixer/PHP-CS-Fixer/zipball/da5f0a7858c79b56fc0b8c36d3efcfe5f37f0992",
"reference": "da5f0a7858c79b56fc0b8c36d3efcfe5f37f0992",
"url": "https://api.github.com/repos/PHP-CS-Fixer/PHP-CS-Fixer/zipball/f23469674ae50d40e398bfff8018911a2a2b0dbe",
"reference": "f23469674ae50d40e398bfff8018911a2a2b0dbe",
"shasum": ""
},
"require": {
@@ -438,12 +438,13 @@
"symfony/polyfill-mbstring": "^1.33",
"symfony/polyfill-php80": "^1.33",
"symfony/polyfill-php81": "^1.33",
"symfony/polyfill-php84": "^1.33",
"symfony/process": "^5.4.47 || ^6.4.24 || ^7.2",
"symfony/stopwatch": "^5.4.45 || ^6.4.24 || ^7.0"
},
"require-dev": {
"facile-it/paraunit": "^1.3.1 || ^2.7",
"infection/infection": "^0.29.14",
"infection/infection": "^0.31.0",
"justinrainbow/json-schema": "^6.5",
"keradus/cli-executor": "^2.2",
"mikey179/vfsstream": "^1.6.12",
@@ -451,7 +452,6 @@
"php-cs-fixer/phpunit-constraint-isidenticalstring": "^1.6",
"php-cs-fixer/phpunit-constraint-xmlmatchesxsd": "^1.6",
"phpunit/phpunit": "^9.6.25 || ^10.5.53 || ^11.5.34",
"symfony/polyfill-php84": "^1.33",
"symfony/var-dumper": "^5.4.48 || ^6.4.24 || ^7.3.2",
"symfony/yaml": "^5.4.45 || ^6.4.24 || ^7.3.2"
},
@@ -494,7 +494,7 @@
],
"support": {
"issues": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/issues",
"source": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/tree/v3.87.2"
"source": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/tree/v3.88.0"
},
"funding": [
{
@@ -502,7 +502,7 @@
"type": "github"
}
],
"time": "2025-09-10T09:51:40+00:00"
"time": "2025-09-24T21:31:42+00:00"
},
{
"name": "psr/container",
@@ -2283,6 +2283,86 @@
],
"time": "2024-09-09T11:45:10+00:00"
},
{
"name": "symfony/polyfill-php84",
"version": "v1.33.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-php84.git",
"reference": "d8ced4d875142b6a7426000426b8abc631d6b191"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-php84/zipball/d8ced4d875142b6a7426000426b8abc631d6b191",
"reference": "d8ced4d875142b6a7426000426b8abc631d6b191",
"shasum": ""
},
"require": {
"php": ">=7.2"
},
"type": "library",
"extra": {
"thanks": {
"url": "https://github.com/symfony/polyfill",
"name": "symfony/polyfill"
}
},
"autoload": {
"files": [
"bootstrap.php"
],
"psr-4": {
"Symfony\\Polyfill\\Php84\\": ""
},
"classmap": [
"Resources/stubs"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Nicolas Grekas",
"email": "p@tchwork.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Symfony polyfill backporting some PHP 8.4+ features to lower PHP versions",
"homepage": "https://symfony.com",
"keywords": [
"compatibility",
"polyfill",
"portable",
"shim"
],
"support": {
"source": "https://github.com/symfony/polyfill-php84/tree/v1.33.0"
},
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://github.com/nicolas-grekas",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2025-06-24T13:30:11+00:00"
},
{
"name": "symfony/process",
"version": "v7.3.3",

View File

@@ -158,18 +158,23 @@ class ShowController extends Controller
Log::debug(sprintf('Now in triggerTransaction(%d, %d)', $webhook->id, $group->id));
Log::channel('audit')->info(sprintf('User triggers webhook #%d on transaction group #%d.', $webhook->id, $group->id));
/** @var MessageGeneratorInterface $engine */
$engine = app(MessageGeneratorInterface::class);
$engine->setUser(auth()->user());
// tell the generator which trigger it should look for
$engine->setTrigger(WebhookTrigger::tryFrom($webhook->trigger));
// tell the generator which objects to process
$engine->setObjects(new Collection()->push($group));
// set the webhook to trigger
$engine->setWebhooks(new Collection()->push($webhook));
// tell the generator to generate the messages
$engine->generateMessages();
/** @var \FireflyIII\Models\WebhookTrigger $trigger */
foreach ($webhook->webhookTriggers as $trigger) {
/** @var MessageGeneratorInterface $engine */
$engine = app(MessageGeneratorInterface::class);
$engine->setUser(auth()->user());
// tell the generator which trigger it should look for
$engine->setTrigger(WebhookTrigger::tryFrom((int)$trigger->key));
// tell the generator which objects to process
$engine->setObjects(new Collection()->push($group));
// set the webhook to trigger
$engine->setWebhooks(new Collection()->push($webhook));
// tell the generator to generate the messages
$engine->generateMessages();
}
// trigger event to send them:
Log::debug('send event RequestedSendWebhookMessages from ShowController::triggerTransaction()');

View File

@@ -86,6 +86,7 @@ class GracefulNotFoundHandler extends ExceptionHandler
return $this->handleAttachment($request, $e);
case 'bills.show':
case 'subscriptions.show':
$request->session()->reflash();
return redirect(route('bills.index'));

View File

@@ -28,6 +28,7 @@ use FireflyIII\Events\RequestedSendWebhookMessages;
use FireflyIII\Events\StoredTransactionGroup;
use FireflyIII\Generator\Webhook\MessageGeneratorInterface;
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;
@@ -36,6 +37,8 @@ use Illuminate\Support\Facades\Log;
/**
* Class StoredGroupEventHandler
*
* TODO migrate to observer?
*/
class StoredGroupEventHandler
{
@@ -44,6 +47,7 @@ class StoredGroupEventHandler
$this->processRules($event);
$this->recalculateCredit($event);
$this->triggerWebhooks($event);
$this->removePeriodStatistics($event);
}
/**
@@ -94,6 +98,20 @@ class StoredGroupEventHandler
$object->recalculate();
}
private function removePeriodStatistics(StoredTransactionGroup $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();
$repository->deleteStatisticsForModel($source->account, $journal->date);
$repository->deleteStatisticsForModel($dest->account, $journal->date);
}
}
/**
* This method processes all webhooks that respond to the "stored transaction group" trigger (100)
*/

View File

@@ -31,6 +31,7 @@ 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\Models\AccountBalanceCalculator;
@@ -49,10 +50,26 @@ class UpdatedGroupEventHandler
$this->processRules($event);
$this->recalculateCredit($event);
$this->triggerWebhooks($event);
$this->removePeriodStatistics($event);
if ($event->runRecalculations) {
$this->updateRunningBalance($event);
}
}
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();
$repository->deleteStatisticsForModel($source->account, $journal->date);
$repository->deleteStatisticsForModel($dest->account, $journal->date);
}
}
/**

View File

@@ -102,7 +102,7 @@ class ShowController extends Controller
// make sure dates are end of day and start of day:
$start->startOfDay();
$end->endOfDay();
$end->endOfDay()->milli(0);
$location = $this->repository->getLocation($account);
$attachments = $this->repository->getAttachments($account);

View File

@@ -122,6 +122,11 @@ class NotificationController extends Controller
public function testNotification(Request $request): RedirectResponse
{
if (true === auth()->user()->hasRole('demo')) {
session()->flash('error', (string) trans('firefly.not_available_demo_user'));
return redirect(route('settings.notification.index'));
}
$all = $request->all();
$channel = $all['test_submit'] ?? '';

View File

@@ -27,6 +27,7 @@ use Carbon\Carbon;
use FireflyIII\Helpers\Update\UpdateTrait;
use FireflyIII\Http\Controllers\Controller;
use FireflyIII\Http\Middleware\IsDemoUser;
use FireflyIII\Support\Facades\FireflyConfig;
use Illuminate\Contracts\View\Factory;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
@@ -66,8 +67,8 @@ class UpdateController extends Controller
{
$subTitle = (string) trans('firefly.update_check_title');
$subTitleIcon = 'fa-star';
$permission = app('fireflyconfig')->get('permission_update_check', -1);
$channel = app('fireflyconfig')->get('update_channel', 'stable');
$permission = FireflyConfig::get('permission_update_check', -1);
$channel = FireflyConfig::get('update_channel', 'stable');
$selected = $permission->data;
$channelSelected = $channel->data;
$options = [
@@ -96,9 +97,9 @@ class UpdateController extends Controller
$channel = $request->get('update_channel');
$channel = in_array($channel, ['stable', 'beta', 'alpha'], true) ? $channel : 'stable';
app('fireflyconfig')->set('permission_update_check', $checkForUpdates);
app('fireflyconfig')->set('last_update_check', Carbon::now()->getTimestamp());
app('fireflyconfig')->set('update_channel', $channel);
FireflyConfig::set('permission_update_check', $checkForUpdates);
FireflyConfig::set('last_update_check', Carbon::now()->getTimestamp());
FireflyConfig::set('update_channel', $channel);
session()->flash('success', (string) trans('firefly.configuration_updated'));
return redirect(route('settings.update-check'));

View File

@@ -30,6 +30,7 @@ use FireflyIII\Enums\AccountTypeEnum;
use FireflyIII\Enums\TransactionTypeEnum;
use FireflyIII\Exceptions\FireflyException;
use FireflyIII\Http\Middleware\IsDemoUser;
use FireflyIII\Models\PeriodStatistic;
use FireflyIII\Models\TransactionType;
use FireflyIII\Repositories\PiggyBank\PiggyBankRepositoryInterface;
use FireflyIII\Support\Facades\Amount;
@@ -108,6 +109,8 @@ class DebugController extends Controller
Artisan::call('route:clear');
Artisan::call('view:clear');
PeriodStatistic::where('id', '>', 0)->delete();
// also do some recalculations.
Artisan::call('correction:recalculates-liabilities');
AccountBalanceCalculator::recalculateAll(false);

View File

@@ -241,4 +241,11 @@ class Account extends Model
get: static fn ($value) => (string)$value,
);
}
public function primaryPeriodStatistics(): MorphMany
{
return $this->morphMany(PeriodStatistic::class, 'primary_statable');
}
}

View File

@@ -0,0 +1,54 @@
<?php
declare(strict_types=1);
namespace FireflyIII\Models;
use FireflyIII\Casts\SeparateTimezoneCaster;
use FireflyIII\Support\Models\ReturnsIntegerUserIdTrait;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\MorphTo;
class PeriodStatistic extends Model
{
use ReturnsIntegerUserIdTrait;
protected function casts(): array
{
return [
'created_at' => 'datetime',
'updated_at' => 'datetime',
'start' => SeparateTimezoneCaster::class,
'end' => SeparateTimezoneCaster::class,
];
}
protected function count(): Attribute
{
return Attribute::make(
get: static fn ($value) => (int)$value,
);
}
public function primaryStatable(): MorphTo
{
return $this->morphTo();
}
public function secondaryStatable(): MorphTo
{
return $this->morphTo();
}
public function tertiaryStatable(): MorphTo
{
return $this->morphTo();
}
}

View File

@@ -43,6 +43,8 @@ use FireflyIII\Repositories\AuditLogEntry\ALERepository;
use FireflyIII\Repositories\AuditLogEntry\ALERepositoryInterface;
use FireflyIII\Repositories\ObjectGroup\ObjectGroupRepository;
use FireflyIII\Repositories\ObjectGroup\ObjectGroupRepositoryInterface;
use FireflyIII\Repositories\PeriodStatistic\PeriodStatisticRepository;
use FireflyIII\Repositories\PeriodStatistic\PeriodStatisticRepositoryInterface;
use FireflyIII\Repositories\TransactionType\TransactionTypeRepository;
use FireflyIII\Repositories\TransactionType\TransactionTypeRepositoryInterface;
use FireflyIII\Repositories\User\UserRepository;
@@ -161,6 +163,7 @@ class FireflyServiceProvider extends ServiceProvider
$this->app->bind(AttachmentHelperInterface::class, AttachmentHelper::class);
$this->app->bind(ALERepositoryInterface::class, ALERepository::class);
$this->app->bind(PeriodStatisticRepositoryInterface::class, PeriodStatisticRepository::class);
$this->app->bind(
static function (Application $app): ObjectGroupRepositoryInterface {

View File

@@ -546,6 +546,8 @@ class AccountRepository implements AccountRepositoryInterface, UserGroupInterfac
#[Override]
public function periodCollection(Account $account, Carbon $start, Carbon $end): array
{
Log::debug(sprintf('periodCollection(#%d, %s, %s)', $account->id, $start->format('Y-m-d'), $end->format('Y-m-d')));
return $account->transactions()
->leftJoin('transaction_journals', 'transaction_journals.id', '=', 'transactions.transaction_journal_id')
->leftJoin('transaction_types', 'transaction_types.id', '=', 'transaction_journals.transaction_type_id')

View File

@@ -0,0 +1,92 @@
<?php
declare(strict_types=1);
/*
* PeriodStatisticRepository.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/>.
*/
namespace FireflyIII\Repositories\PeriodStatistic;
use Carbon\Carbon;
use FireflyIII\Models\PeriodStatistic;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Log;
class PeriodStatisticRepository implements PeriodStatisticRepositoryInterface
{
public function findPeriodStatistics(Model $model, Carbon $start, Carbon $end, array $types): Collection
{
return $model->primaryPeriodStatistics()
->where('start', $start)
->where('end', $end)
->whereIn('type', $types)
->get()
;
}
public function findPeriodStatistic(Model $model, Carbon $start, Carbon $end, string $type): Collection
{
return $model->primaryPeriodStatistics()
->where('start', $start)
->where('end', $end)
->where('type', $type)
->get()
;
}
public function saveStatistic(Model $model, int $currencyId, Carbon $start, Carbon $end, string $type, int $count, string $amount): PeriodStatistic
{
$stat = new PeriodStatistic();
$stat->primaryStatable()->associate($model);
$stat->transaction_currency_id = $currencyId;
$stat->start = $start;
$stat->start_tz = $start->format('e');
$stat->end = $end;
$stat->end_tz = $end->format('e');
$stat->amount = $amount;
$stat->count = $count;
$stat->type = $type;
$stat->save();
Log::debug(sprintf(
'Saved #%d [currency #%d, Model %s #%d, %s to %s, %d, %s] as new statistic.',
$stat->id,
get_class($model),
$model->id,
$stat->transaction_currency_id,
$stat->start->toW3cString(),
$stat->end->toW3cString(),
$count,
$amount
));
return $stat;
}
public function allInRangeForModel(Model $model, Carbon $start, Carbon $end): Collection
{
return $model->primaryPeriodStatistics()->where('start', '>=', $start)->where('end', '<=', $end)->get();
}
public function deleteStatisticsForModel(Model $model, Carbon $date): void
{
$model->primaryPeriodStatistics()->where('start', '<=', $date)->where('end', '>=', $date)->delete();
}
}

View File

@@ -0,0 +1,42 @@
<?php
declare(strict_types=1);
/*
* PeriodStatisticRepositoryInterface.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/>.
*/
namespace FireflyIII\Repositories\PeriodStatistic;
use Carbon\Carbon;
use FireflyIII\Models\PeriodStatistic;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Collection;
interface PeriodStatisticRepositoryInterface
{
public function findPeriodStatistics(Model $model, Carbon $start, Carbon $end, array $types): Collection;
public function findPeriodStatistic(Model $model, Carbon $start, Carbon $end, string $type): Collection;
public function saveStatistic(Model $model, int $currencyId, Carbon $start, Carbon $end, string $type, int $count, string $amount): PeriodStatistic;
public function allInRangeForModel(Model $model, Carbon $start, Carbon $end): Collection;
public function deleteStatisticsForModel(Model $model, Carbon $date): void;
}

View File

@@ -41,280 +41,6 @@ use NumberFormatter;
*/
class Amount
{
/**
* This method will properly format the given number, in color or "black and white",
* as a currency, given two things: the currency required and the current locale.
*
* @throws FireflyException
*/
public function formatAnything(TransactionCurrency $format, string $amount, ?bool $coloured = null): string
{
return $this->formatFlat($format->symbol, $format->decimal_places, $amount, $coloured);
}
/**
* This method will properly format the given number, in color or "black and white",
* as a currency, given two things: the currency required and the current locale.
*
* @throws FireflyException
*/
public function formatFlat(string $symbol, int $decimalPlaces, string $amount, ?bool $coloured = null): string
{
$locale = Steam::getLocale();
$rounded = Steam::bcround($amount, $decimalPlaces);
$coloured ??= true;
$fmt = new NumberFormatter($locale, NumberFormatter::CURRENCY);
$fmt->setSymbol(NumberFormatter::CURRENCY_SYMBOL, $symbol);
$fmt->setAttribute(NumberFormatter::MIN_FRACTION_DIGITS, $decimalPlaces);
$fmt->setAttribute(NumberFormatter::MAX_FRACTION_DIGITS, $decimalPlaces);
$result = (string)$fmt->format((float)$rounded); // intentional float
if (true === $coloured) {
if (1 === bccomp($rounded, '0')) {
return sprintf('<span class="text-success money-positive">%s</span>', $result);
}
if (-1 === bccomp($rounded, '0')) {
return sprintf('<span class="text-danger money-negative">%s</span>', $result);
}
return sprintf('<span class="money-neutral">%s</span>', $result);
}
return $result;
}
public function formatByCurrencyId(int $currencyId, string $amount, ?bool $coloured = null): string
{
$format = $this->getTransactionCurrencyById($currencyId);
return $this->formatFlat($format->symbol, $format->decimal_places, $amount, $coloured);
}
public function getAllCurrencies(): Collection
{
return TransactionCurrency::orderBy('code', 'ASC')->get();
}
/**
* Experimental function to see if we can quickly and quietly get the amount from a journal.
* This depends on the user's default currency and the wish to have it converted.
*/
public function getAmountFromJournal(array $journal): string
{
$convertToPrimary = $this->convertToPrimary();
$currency = $this->getPrimaryCurrency();
$field = $convertToPrimary && $currency->id !== $journal['currency_id'] ? 'pc_amount' : 'amount';
$amount = $journal[$field] ?? '0';
// Log::debug(sprintf('Field is %s, amount is %s', $field, $amount));
// fallback, the transaction has a foreign amount in $currency.
if ($convertToPrimary && null !== $journal['foreign_amount'] && $currency->id === (int)$journal['foreign_currency_id']) {
$amount = $journal['foreign_amount'];
// Log::debug(sprintf('Overruled, amount is now %s', $amount));
}
return (string)$amount;
}
public function getTransactionCurrencyById(int $currencyId): TransactionCurrency
{
$instance = PreferencesSingleton::getInstance();
$key = sprintf('transaction_currency_%d', $currencyId);
/** @var null|TransactionCurrency $pref */
$pref = $instance->getPreference($key);
if (null !== $pref) {
return $pref;
}
$currency = TransactionCurrency::find($currencyId);
if (null === $currency) {
$message = sprintf('Could not find a transaction currency with ID #%d in %s', $currencyId, __METHOD__);
Log::error($message);
throw new FireflyException($message);
}
$instance->setPreference($key, $currency);
return $currency;
}
public function getTransactionCurrencyByCode(string $code): TransactionCurrency
{
$instance = PreferencesSingleton::getInstance();
$key = sprintf('transaction_currency_%s', $code);
/** @var null|TransactionCurrency $pref */
$pref = $instance->getPreference($key);
if (null !== $pref) {
return $pref;
}
$currency = TransactionCurrency::whereCode($code)->first();
if (null === $currency) {
$message = sprintf('Could not find a transaction currency with code "%s" in %s', $code, __METHOD__);
Log::error($message);
throw new FireflyException($message);
}
$instance->setPreference($key, $currency);
return $currency;
}
public function convertToPrimary(?User $user = null): bool
{
$instance = PreferencesSingleton::getInstance();
if (!$user instanceof User) {
$pref = $instance->getPreference('convert_to_primary_no_user');
if (null === $pref) {
$res = true === Preferences::get('convert_to_primary', false)->data && true === config('cer.enabled');
$instance->setPreference('convert_to_primary_no_user', $res);
return $res;
}
return $pref;
}
$key = sprintf('convert_to_primary_%d', $user->id);
$pref = $instance->getPreference($key);
if (null === $pref) {
$res = true === Preferences::getForUser($user, 'convert_to_primary', false)->data && true === config('cer.enabled');
$instance->setPreference($key, $res);
return $res;
}
return $pref;
}
public function getPrimaryCurrency(): TransactionCurrency
{
if (auth()->check()) {
/** @var User $user */
$user = auth()->user();
if (null !== $user->userGroup) {
return $this->getPrimaryCurrencyByUserGroup($user->userGroup);
}
}
return $this->getSystemCurrency();
}
public function getPrimaryCurrencyByUserGroup(UserGroup $userGroup): TransactionCurrency
{
$cache = new CacheProperties();
$cache->addProperty('getPrimaryCurrencyByGroup');
$cache->addProperty($userGroup->id);
if ($cache->has()) {
return $cache->get();
}
/** @var null|TransactionCurrency $primary */
$primary = $userGroup->currencies()->where('group_default', true)->first();
if (null === $primary) {
$primary = $this->getSystemCurrency();
// could be the user group has no default right now.
$userGroup->currencies()->sync([$primary->id => ['group_default' => true]]);
}
$cache->store($primary);
return $primary;
}
public function getSystemCurrency(): TransactionCurrency
{
return TransactionCurrency::whereNull('deleted_at')->where('code', 'EUR')->first();
}
/**
* Experimental function to see if we can quickly and quietly get the amount from a journal.
* This depends on the user's default currency and the wish to have it converted.
*/
public function getAmountFromJournalObject(TransactionJournal $journal): string
{
$convertToPrimary = $this->convertToPrimary();
$currency = $this->getPrimaryCurrency();
$field = $convertToPrimary && $currency->id !== $journal->transaction_currency_id ? 'pc_amount' : 'amount';
/** @var null|Transaction $sourceTransaction */
$sourceTransaction = $journal->transactions()->where('amount', '<', 0)->first();
if (null === $sourceTransaction) {
return '0';
}
$amount = $sourceTransaction->{$field} ?? '0';
if ((int)$sourceTransaction->foreign_currency_id === $currency->id) {
// use foreign amount instead!
$amount = (string)$sourceTransaction->foreign_amount; // hard coded to be foreign amount.
}
return $amount;
}
public function getCurrencies(): Collection
{
/** @var User $user */
$user = auth()->user();
return $user->currencies()->orderBy('code', 'ASC')->get();
}
/**
* This method returns the correct format rules required by accounting.js,
* the library used to format amounts in charts.
*
* Used only in one place.
*
* @throws FireflyException
*/
public function getJsConfig(): array
{
$config = $this->getLocaleInfo();
$negative = self::getAmountJsConfig($config['n_sep_by_space'], $config['n_sign_posn'], $config['negative_sign'], $config['n_cs_precedes']);
$positive = self::getAmountJsConfig($config['p_sep_by_space'], $config['p_sign_posn'], $config['positive_sign'], $config['p_cs_precedes']);
return [
'mon_decimal_point' => $config['mon_decimal_point'],
'mon_thousands_sep' => $config['mon_thousands_sep'],
'format' => [
'pos' => $positive,
'neg' => $negative,
'zero' => $positive,
],
];
}
/**
* @throws FireflyException
*/
private function getLocaleInfo(): array
{
// get config from preference, not from translation:
$locale = Steam::getLocale();
$array = Steam::getLocaleArray($locale);
setlocale(LC_MONETARY, $array);
$info = localeconv();
// correct variables
$info['n_cs_precedes'] = $this->getLocaleField($info, 'n_cs_precedes');
$info['p_cs_precedes'] = $this->getLocaleField($info, 'p_cs_precedes');
$info['n_sep_by_space'] = $this->getLocaleField($info, 'n_sep_by_space');
$info['p_sep_by_space'] = $this->getLocaleField($info, 'p_sep_by_space');
$fmt = new NumberFormatter($locale, NumberFormatter::CURRENCY);
$info['mon_decimal_point'] = $fmt->getSymbol(NumberFormatter::MONETARY_SEPARATOR_SYMBOL);
$info['mon_thousands_sep'] = $fmt->getSymbol(NumberFormatter::MONETARY_GROUPING_SEPARATOR_SYMBOL);
return $info;
}
private function getLocaleField(array $info, string $field): bool
{
return (is_bool($info[$field]) && true === $info[$field])
|| (is_int($info[$field]) && 1 === $info[$field]);
}
/**
* bool $sepBySpace is $localeconv['n_sep_by_space']
* int $signPosn = $localeconv['n_sign_posn']
@@ -384,4 +110,278 @@ class Amount
return $posA.$posD.'%v'.$space.$posB.'%s'.$posC.$posE;
}
public function convertToPrimary(?User $user = null): bool
{
$instance = PreferencesSingleton::getInstance();
if (!$user instanceof User) {
$pref = $instance->getPreference('convert_to_primary_no_user');
if (null === $pref) {
$res = true === Preferences::get('convert_to_primary', false)->data && true === config('cer.enabled');
$instance->setPreference('convert_to_primary_no_user', $res);
return $res;
}
return $pref;
}
$key = sprintf('convert_to_primary_%d', $user->id);
$pref = $instance->getPreference($key);
if (null === $pref) {
$res = true === Preferences::getForUser($user, 'convert_to_primary', false)->data && true === config('cer.enabled');
$instance->setPreference($key, $res);
return $res;
}
return $pref;
}
/**
* This method will properly format the given number, in color or "black and white",
* as a currency, given two things: the currency required and the current locale.
*
* @throws FireflyException
*/
public function formatAnything(TransactionCurrency $format, string $amount, ?bool $coloured = null): string
{
return $this->formatFlat($format->symbol, $format->decimal_places, $amount, $coloured);
}
public function formatByCurrencyId(int $currencyId, string $amount, ?bool $coloured = null): string
{
$format = $this->getTransactionCurrencyById($currencyId);
return $this->formatFlat($format->symbol, $format->decimal_places, $amount, $coloured);
}
/**
* This method will properly format the given number, in color or "black and white",
* as a currency, given two things: the currency required and the current locale.
*
* @throws FireflyException
*/
public function formatFlat(string $symbol, int $decimalPlaces, string $amount, ?bool $coloured = null): string
{
$locale = Steam::getLocale();
$rounded = Steam::bcround($amount, $decimalPlaces);
$coloured ??= true;
$fmt = new NumberFormatter($locale, NumberFormatter::CURRENCY);
$fmt->setSymbol(NumberFormatter::CURRENCY_SYMBOL, $symbol);
$fmt->setAttribute(NumberFormatter::MIN_FRACTION_DIGITS, $decimalPlaces);
$fmt->setAttribute(NumberFormatter::MAX_FRACTION_DIGITS, $decimalPlaces);
$result = (string)$fmt->format((float)$rounded); // intentional float
if (true === $coloured) {
if (1 === bccomp($rounded, '0')) {
return sprintf('<span class="text-success money-positive">%s</span>', $result);
}
if (-1 === bccomp($rounded, '0')) {
return sprintf('<span class="text-danger money-negative">%s</span>', $result);
}
return sprintf('<span class="money-neutral">%s</span>', $result);
}
return $result;
}
public function getAllCurrencies(): Collection
{
return TransactionCurrency::orderBy('code', 'ASC')->get();
}
/**
* Experimental function to see if we can quickly and quietly get the amount from a journal.
* This depends on the user's default currency and the wish to have it converted.
*/
public function getAmountFromJournal(array $journal): string
{
$convertToPrimary = $this->convertToPrimary();
$currency = $this->getPrimaryCurrency();
$field = $convertToPrimary && $currency->id !== $journal['currency_id'] ? 'pc_amount' : 'amount';
$amount = $journal[$field] ?? '0';
// Log::debug(sprintf('Field is %s, amount is %s', $field, $amount));
// fallback, the transaction has a foreign amount in $currency.
if ($convertToPrimary && null !== $journal['foreign_amount'] && $currency->id === (int)$journal['foreign_currency_id']) {
$amount = $journal['foreign_amount'];
// Log::debug(sprintf('Overruled, amount is now %s', $amount));
}
return (string)$amount;
}
/**
* Experimental function to see if we can quickly and quietly get the amount from a journal.
* This depends on the user's default currency and the wish to have it converted.
*/
public function getAmountFromJournalObject(TransactionJournal $journal): string
{
$convertToPrimary = $this->convertToPrimary();
$currency = $this->getPrimaryCurrency();
$field = $convertToPrimary && $currency->id !== $journal->transaction_currency_id ? 'pc_amount' : 'amount';
/** @var null|Transaction $sourceTransaction */
$sourceTransaction = $journal->transactions()->where('amount', '<', 0)->first();
if (null === $sourceTransaction) {
return '0';
}
$amount = $sourceTransaction->{$field} ?? '0';
if ((int)$sourceTransaction->foreign_currency_id === $currency->id) {
// use foreign amount instead!
$amount = (string)$sourceTransaction->foreign_amount; // hard coded to be foreign amount.
}
return $amount;
}
public function getCurrencies(): Collection
{
/** @var User $user */
$user = auth()->user();
return $user->currencies()->orderBy('code', 'ASC')->get();
}
/**
* This method returns the correct format rules required by accounting.js,
* the library used to format amounts in charts.
*
* Used only in one place.
*
* @throws FireflyException
*/
public function getJsConfig(): array
{
$config = $this->getLocaleInfo();
$negative = self::getAmountJsConfig($config['n_sep_by_space'], $config['n_sign_posn'], $config['negative_sign'], $config['n_cs_precedes']);
$positive = self::getAmountJsConfig($config['p_sep_by_space'], $config['p_sign_posn'], $config['positive_sign'], $config['p_cs_precedes']);
return [
'mon_decimal_point' => $config['mon_decimal_point'],
'mon_thousands_sep' => $config['mon_thousands_sep'],
'format' => [
'pos' => $positive,
'neg' => $negative,
'zero' => $positive,
],
];
}
public function getPrimaryCurrency(): TransactionCurrency
{
if (auth()->check()) {
/** @var User $user */
$user = auth()->user();
if (null !== $user->userGroup) {
return $this->getPrimaryCurrencyByUserGroup($user->userGroup);
}
}
return $this->getSystemCurrency();
}
public function getPrimaryCurrencyByUserGroup(UserGroup $userGroup): TransactionCurrency
{
$cache = new CacheProperties();
$cache->addProperty('getPrimaryCurrencyByGroup');
$cache->addProperty($userGroup->id);
if ($cache->has()) {
return $cache->get();
}
/** @var null|TransactionCurrency $primary */
$primary = $userGroup->currencies()->where('group_default', true)->first();
if (null === $primary) {
$primary = $this->getSystemCurrency();
// could be the user group has no default right now.
$userGroup->currencies()->sync([$primary->id => ['group_default' => true]]);
}
$cache->store($primary);
return $primary;
}
public function getSystemCurrency(): TransactionCurrency
{
return TransactionCurrency::whereNull('deleted_at')->where('code', 'EUR')->first();
}
public function getTransactionCurrencyByCode(string $code): TransactionCurrency
{
$instance = PreferencesSingleton::getInstance();
$key = sprintf('transaction_currency_%s', $code);
/** @var null|TransactionCurrency $pref */
$pref = $instance->getPreference($key);
if (null !== $pref) {
return $pref;
}
$currency = TransactionCurrency::whereCode($code)->first();
if (null === $currency) {
$message = sprintf('Could not find a transaction currency with code "%s" in %s', $code, __METHOD__);
Log::error($message);
throw new FireflyException($message);
}
$instance->setPreference($key, $currency);
return $currency;
}
public function getTransactionCurrencyById(int $currencyId): TransactionCurrency
{
$instance = PreferencesSingleton::getInstance();
$key = sprintf('transaction_currency_%d', $currencyId);
/** @var null|TransactionCurrency $pref */
$pref = $instance->getPreference($key);
if (null !== $pref) {
return $pref;
}
$currency = TransactionCurrency::find($currencyId);
if (null === $currency) {
$message = sprintf('Could not find a transaction currency with ID #%d in %s', $currencyId, __METHOD__);
Log::error($message);
throw new FireflyException($message);
}
$instance->setPreference($key, $currency);
return $currency;
}
private function getLocaleField(array $info, string $field): bool
{
return (is_bool($info[$field]) && true === $info[$field])
|| (is_int($info[$field]) && 1 === $info[$field]);
}
/**
* @throws FireflyException
*/
private function getLocaleInfo(): array
{
// get config from preference, not from translation:
$locale = Steam::getLocale();
$array = Steam::getLocaleArray($locale);
setlocale(LC_MONETARY, $array);
$info = localeconv();
// correct variables
$info['n_cs_precedes'] = $this->getLocaleField($info, 'n_cs_precedes');
$info['p_cs_precedes'] = $this->getLocaleField($info, 'p_cs_precedes');
$info['n_sep_by_space'] = $this->getLocaleField($info, 'n_sep_by_space');
$info['p_sep_by_space'] = $this->getLocaleField($info, 'p_sep_by_space');
$fmt = new NumberFormatter($locale, NumberFormatter::CURRENCY);
$info['mon_decimal_point'] = $fmt->getSymbol(NumberFormatter::MONETARY_SEPARATOR_SYMBOL);
$info['mon_thousands_sep'] = $fmt->getSymbol(NumberFormatter::MONETARY_GROUPING_SEPARATOR_SYMBOL);
return $info;
}
}

View File

@@ -86,7 +86,7 @@ class RemoteUserGuard implements Guard
$header = config('auth.guard_email');
if (null !== $header) {
$emailAddress = (string) (request()->server($header) ?? apache_request_headers()[$header] ?? null);
$emailAddress = (string)(request()->server($header) ?? apache_request_headers()[$header] ?? null);
$preference = Preferences::getForUser($retrievedUser, 'remote_guard_alt_email');
if ('' !== $emailAddress && null === $preference && $emailAddress !== $userID) {
@@ -102,13 +102,6 @@ class RemoteUserGuard implements Guard
$this->user = $retrievedUser;
}
public function guest(): bool
{
Log::debug(sprintf('Now at %s', __METHOD__));
return !$this->check();
}
public function check(): bool
{
Log::debug(sprintf('Now at %s', __METHOD__));
@@ -116,17 +109,11 @@ class RemoteUserGuard implements Guard
return $this->user() instanceof User;
}
public function user(): ?User
public function guest(): bool
{
Log::debug(sprintf('Now at %s', __METHOD__));
$user = $this->user;
if (!$user instanceof User) {
Log::debug('User is NULL');
return null;
}
return $user;
return !$this->check();
}
public function hasUser(): bool
@@ -157,6 +144,19 @@ class RemoteUserGuard implements Guard
Log::error(sprintf('Did not set user at %s', __METHOD__));
}
public function user(): ?User
{
Log::debug(sprintf('Now at %s', __METHOD__));
$user = $this->user;
if (!$user instanceof User) {
Log::debug('User is NULL');
return null;
}
return $user;
}
/**
* @throws FireflyException
*

View File

@@ -59,8 +59,8 @@ class Balance
$result = $query->get(['transactions.account_id', 'transactions.transaction_currency_id', 'transactions.balance_after']);
foreach ($result as $entry) {
$accountId = (int) $entry->account_id;
$currencyId = (int) $entry->transaction_currency_id;
$accountId = (int)$entry->account_id;
$currencyId = (int)$entry->transaction_currency_id;
$currencies[$currencyId] ??= Amount::getTransactionCurrencyById($currencyId);
$return[$accountId] ??= [];
if (array_key_exists($currencyId, $return[$accountId])) {

View File

@@ -68,7 +68,7 @@ class TagList implements BinderInterface
return true;
}
if (in_array((string) $tag->id, $list, true)) {
if (in_array((string)$tag->id, $list, true)) {
Log::debug(sprintf('TagList: (id) found tag #%d ("%s") in list.', $tag->id, $tag->tag));
return true;

View File

@@ -42,7 +42,7 @@ class TagOrId implements BinderInterface
$result = $repository->findByTag($value);
if (null === $result) {
$result = $repository->find((int) $value);
$result = $repository->find((int)$value);
}
if (null !== $result) {
return $result;

View File

@@ -41,7 +41,7 @@ class UserGroupAccount implements BinderInterface
if (auth()->check()) {
/** @var User $user */
$user = auth()->user();
$account = Account::where('id', (int) $value)
$account = Account::where('id', (int)$value)
->where('user_group_id', $user->user_group_id)
->first()
;

View File

@@ -41,7 +41,7 @@ class UserGroupBill implements BinderInterface
if (auth()->check()) {
/** @var User $user */
$user = auth()->user();
$currency = Bill::where('id', (int) $value)
$currency = Bill::where('id', (int)$value)
->where('user_group_id', $user->user_group_id)
->first()
;

View File

@@ -38,7 +38,7 @@ class UserGroupExchangeRate implements BinderInterface
if (auth()->check()) {
/** @var User $user */
$user = auth()->user();
$rate = CurrencyExchangeRate::where('id', (int) $value)
$rate = CurrencyExchangeRate::where('id', (int)$value)
->where('user_group_id', $user->user_group_id)
->first()
;

View File

@@ -38,7 +38,7 @@ class UserGroupTransaction implements BinderInterface
if (auth()->check()) {
/** @var User $user */
$user = auth()->user();
$group = TransactionGroup::where('id', (int) $value)
$group = TransactionGroup::where('id', (int)$value)
->where('user_group_id', $user->user_group_id)
->first()
;

View File

@@ -78,6 +78,14 @@ class CacheProperties
return Cache::has($this->hash);
}
/**
* @param mixed $data
*/
public function store($data): void
{
Cache::forever($this->hash, $data);
}
private function hash(): void
{
$content = '';
@@ -86,17 +94,9 @@ class CacheProperties
$content = sprintf('%s%s', $content, json_encode($property, JSON_THROW_ON_ERROR));
} catch (JsonException) {
// @ignoreException
$content = sprintf('%s%s', $content, hash('sha256', (string) Carbon::now()->getTimestamp()));
$content = sprintf('%s%s', $content, hash('sha256', (string)Carbon::now()->getTimestamp()));
}
}
$this->hash = substr(hash('sha256', $content), 0, 16);
}
/**
* @param mixed $data
*/
public function store($data): void
{
Cache::forever($this->hash, $data);
}
}

View File

@@ -37,27 +37,6 @@ class Calculator
private static ?SplObjectStorage $intervalMap = null; // @phpstan-ignore-line
private static array $intervals = [];
/**
* @throws IntervalException
*/
public function nextDateByInterval(Carbon $epoch, Periodicity $periodicity, int $skipInterval = 0): Carbon
{
if (!self::isAvailablePeriodicity($periodicity)) {
throw IntervalException::unavailable($periodicity, self::$intervals);
}
/** @var Periodicity\Interval $periodicity */
$periodicity = self::$intervalMap->offsetGet($periodicity);
$interval = $this->skipInterval($skipInterval);
return $periodicity->nextDate($epoch->clone(), $interval);
}
public function isAvailablePeriodicity(Periodicity $periodicity): bool
{
return self::containsInterval($periodicity);
}
private static function containsInterval(Periodicity $periodicity): bool
{
return self::loadIntervalMap()->contains($periodicity);
@@ -78,6 +57,27 @@ class Calculator
return self::$intervalMap;
}
public function isAvailablePeriodicity(Periodicity $periodicity): bool
{
return self::containsInterval($periodicity);
}
/**
* @throws IntervalException
*/
public function nextDateByInterval(Carbon $epoch, Periodicity $periodicity, int $skipInterval = 0): Carbon
{
if (!self::isAvailablePeriodicity($periodicity)) {
throw IntervalException::unavailable($periodicity, self::$intervals);
}
/** @var Periodicity\Interval $periodicity */
$periodicity = self::$intervalMap->offsetGet($periodicity);
$interval = $this->skipInterval($skipInterval);
return $periodicity->nextDate($epoch->clone(), $interval);
}
private function skipInterval(int $skip): int
{
return self::DEFAULT_INTERVAL + $skip;

View File

@@ -69,9 +69,9 @@ class FrontpageChartGenerator
Log::debug('Now in generate for budget chart.');
$budgets = $this->budgetRepository->getActiveBudgets();
$data = [
['label' => (string) trans('firefly.spent_in_budget'), 'entries' => [], 'type' => 'bar'],
['label' => (string) trans('firefly.left_to_spend'), 'entries' => [], 'type' => 'bar'],
['label' => (string) trans('firefly.overspent'), 'entries' => [], 'type' => 'bar'],
['label' => (string)trans('firefly.spent_in_budget'), 'entries' => [], 'type' => 'bar'],
['label' => (string)trans('firefly.left_to_spend'), 'entries' => [], 'type' => 'bar'],
['label' => (string)trans('firefly.overspent'), 'entries' => [], 'type' => 'bar'],
];
// loop al budgets:
@@ -84,6 +84,64 @@ class FrontpageChartGenerator
return $data;
}
public function setEnd(Carbon $end): void
{
$this->end = $end;
}
public function setStart(Carbon $start): void
{
$this->start = $start;
}
/**
* A basic setter for the user. Also updates the repositories with the right user.
*/
public function setUser(User $user): void
{
$this->budgetRepository->setUser($user);
$this->blRepository->setUser($user);
$this->opsRepository->setUser($user);
$locale = app('steam')->getLocale();
$this->monthAndDayFormat = (string)trans('config.month_and_day_js', [], $locale);
}
/**
* If a budget has budget limit, each limit is processed individually.
*/
private function budgetLimits(array $data, Budget $budget, Collection $limits): array
{
Log::debug('Start processing budget limits.');
/** @var BudgetLimit $limit */
foreach ($limits as $limit) {
$data = $this->processLimit($data, $budget, $limit);
}
Log::debug('Done processing budget limits.');
return $data;
}
/**
* When no limits are present, the expenses of the whole period are collected and grouped.
* This is grouped per currency. Because there is no limit set, "left to spend" and "overspent" are empty.
*/
private function noBudgetLimits(array $data, Budget $budget): array
{
$spent = $this->opsRepository->sumExpenses($this->start, $this->end, null, new Collection()->push($budget));
/** @var array $entry */
foreach ($spent as $entry) {
$title = sprintf('%s (%s)', $budget->name, $entry['currency_name']);
$data[0]['entries'][$title] = bcmul((string)$entry['sum'], '-1'); // spent
$data[1]['entries'][$title] = 0; // left to spend
$data[2]['entries'][$title] = 0; // overspent
}
return $data;
}
/**
* For each budget, gets all budget limits for the current time range.
* When no limits are present, the time range is used to collect information on money spent.
@@ -108,41 +166,6 @@ class FrontpageChartGenerator
return $result;
}
/**
* When no limits are present, the expenses of the whole period are collected and grouped.
* This is grouped per currency. Because there is no limit set, "left to spend" and "overspent" are empty.
*/
private function noBudgetLimits(array $data, Budget $budget): array
{
$spent = $this->opsRepository->sumExpenses($this->start, $this->end, null, new Collection()->push($budget));
/** @var array $entry */
foreach ($spent as $entry) {
$title = sprintf('%s (%s)', $budget->name, $entry['currency_name']);
$data[0]['entries'][$title] = bcmul((string) $entry['sum'], '-1'); // spent
$data[1]['entries'][$title] = 0; // left to spend
$data[2]['entries'][$title] = 0; // overspent
}
return $data;
}
/**
* If a budget has budget limit, each limit is processed individually.
*/
private function budgetLimits(array $data, Budget $budget, Collection $limits): array
{
Log::debug('Start processing budget limits.');
/** @var BudgetLimit $limit */
foreach ($limits as $limit) {
$data = $this->processLimit($data, $budget, $limit);
}
Log::debug('Done processing budget limits.');
return $data;
}
/**
* For each limit, the expenses from the time range of the limit are collected. Each row from the result is
* processed individually.
@@ -204,14 +227,14 @@ class FrontpageChartGenerator
Log::debug(sprintf('Amount is now "%s".', $amount));
}
$amount ??= '0';
$sumSpent = bcmul((string) $entry['sum'], '-1'); // spent
$sumSpent = bcmul((string)$entry['sum'], '-1'); // spent
$data[0]['entries'][$title] ??= '0';
$data[1]['entries'][$title] ??= '0';
$data[2]['entries'][$title] ??= '0';
$data[0]['entries'][$title] = bcadd((string) $data[0]['entries'][$title], 1 === bccomp($sumSpent, $amount) ? $amount : $sumSpent); // spent
$data[1]['entries'][$title] = bcadd((string) $data[1]['entries'][$title], 1 === bccomp($amount, $sumSpent) ? bcadd((string) $entry['sum'], $amount) : '0'); // left to spent
$data[2]['entries'][$title] = bcadd((string) $data[2]['entries'][$title], 1 === bccomp($amount, $sumSpent) ? '0' : bcmul(bcadd((string) $entry['sum'], $amount), '-1')); // overspent
$data[0]['entries'][$title] = bcadd((string)$data[0]['entries'][$title], 1 === bccomp($sumSpent, $amount) ? $amount : $sumSpent); // spent
$data[1]['entries'][$title] = bcadd((string)$data[1]['entries'][$title], 1 === bccomp($amount, $sumSpent) ? bcadd((string)$entry['sum'], $amount) : '0'); // left to spent
$data[2]['entries'][$title] = bcadd((string)$data[2]['entries'][$title], 1 === bccomp($amount, $sumSpent) ? '0' : bcmul(bcadd((string)$entry['sum'], $amount), '-1')); // overspent
Log::debug(sprintf('Amount [spent] is now %s.', $data[0]['entries'][$title]));
Log::debug(sprintf('Amount [left] is now %s.', $data[1]['entries'][$title]));
@@ -219,27 +242,4 @@ class FrontpageChartGenerator
return $data;
}
public function setEnd(Carbon $end): void
{
$this->end = $end;
}
public function setStart(Carbon $start): void
{
$this->start = $start;
}
/**
* A basic setter for the user. Also updates the repositories with the right user.
*/
public function setUser(User $user): void
{
$this->budgetRepository->setUser($user);
$this->blRepository->setUser($user);
$this->opsRepository->setUser($user);
$locale = app('steam')->getLocale();
$this->monthAndDayFormat = (string) trans('config.month_and_day_js', [], $locale);
}
}

View File

@@ -26,7 +26,6 @@ namespace FireflyIII\Support\Chart\Category;
use Carbon\Carbon;
use FireflyIII\Enums\AccountTypeEnum;
use FireflyIII\Models\Category;
use FireflyIII\Models\TransactionCurrency;
use FireflyIII\Repositories\Account\AccountRepositoryInterface;
use FireflyIII\Repositories\Category\CategoryRepositoryInterface;
@@ -96,6 +95,30 @@ class FrontpageChartGenerator
];
}
private function collectExpensesAll(Collection $categories, Collection $accounts): array
{
Log::debug(sprintf('Collect expenses for %d category(ies).', count($categories)));
$spent = $this->opsRepos->collectExpenses($this->start, $this->end, $accounts, $categories);
$tempData = [];
foreach ($categories as $category) {
$sums = $this->opsRepos->sumCollectedTransactionsByCategory($spent, $category, 'negative', $this->convertToPrimary);
if (0 === count($sums)) {
continue;
}
foreach ($sums as $currency) {
$this->addCurrency($currency);
$tempData[] = [
'name' => $category->name,
'sum' => $currency['sum'],
'sum_float' => round((float)$currency['sum'], $currency['currency_decimal_places']),
'currency_id' => (int)$currency['currency_id'],
];
}
}
return $tempData;
}
private function collectNoCatExpenses(Collection $accounts): array
{
$noCatExp = $this->noCatRepos->sumExpenses($this->start, $this->end, $accounts);
@@ -147,28 +170,4 @@ class FrontpageChartGenerator
return $currencyData;
}
private function collectExpensesAll(Collection $categories, Collection $accounts): array
{
Log::debug(sprintf('Collect expenses for %d category(ies).', count($categories)));
$spent = $this->opsRepos->collectExpenses($this->start, $this->end, $accounts, $categories);
$tempData = [];
foreach ($categories as $category) {
$sums = $this->opsRepos->sumCollectedTransactionsByCategory($spent, $category, 'negative', $this->convertToPrimary);
if (0 === count($sums)) {
continue;
}
foreach ($sums as $currency) {
$this->addCurrency($currency);
$tempData[] = [
'name' => $category->name,
'sum' => $currency['sum'],
'sum_float' => round((float)$currency['sum'], $currency['currency_decimal_places']),
'currency_id' => (int)$currency['currency_id'],
];
}
}
return $tempData;
}
}

View File

@@ -73,14 +73,14 @@ class WholePeriodChartGenerator
$code = $currency['currency_code'];
$name = $currency['currency_name'];
$chartData[sprintf('spent-in-%s', $code)] = [
'label' => (string) trans('firefly.box_spent_in_currency', ['currency' => $name]),
'label' => (string)trans('firefly.box_spent_in_currency', ['currency' => $name]),
'entries' => [],
'type' => 'bar',
'backgroundColor' => 'rgba(219, 68, 55, 0.5)', // red
];
$chartData[sprintf('earned-in-%s', $code)] = [
'label' => (string) trans('firefly.box_earned_in_currency', ['currency' => $name]),
'label' => (string)trans('firefly.box_earned_in_currency', ['currency' => $name]),
'entries' => [],
'type' => 'bar',
'backgroundColor' => 'rgba(0, 141, 76, 0.5)', // green

View File

@@ -44,10 +44,10 @@ class ChartData
public function add(array $data): void
{
if (array_key_exists('currency_id', $data)) {
$data['currency_id'] = (string) $data['currency_id'];
$data['currency_id'] = (string)$data['currency_id'];
}
if (array_key_exists('primary_currency_id', $data)) {
$data['primary_currency_id'] = (string) $data['primary_currency_id'];
$data['primary_currency_id'] = (string)$data['primary_currency_id'];
}
$required = ['start', 'date', 'end', 'entries'];
foreach ($required as $field) {

View File

@@ -39,7 +39,7 @@ class AutoBudgetCronjob extends AbstractCronjob
{
/** @var Configuration $config */
$config = FireflyConfig::get('last_ab_job', 0);
$lastTime = (int) $config->data;
$lastTime = (int)$config->data;
$diff = now(config('app.timezone'))->getTimestamp() - $lastTime;
$diffForHumans = now(config('app.timezone'))->diffForHumans(Carbon::createFromTimestamp($lastTime), null, true);
if (0 === $lastTime) {
@@ -80,7 +80,7 @@ class AutoBudgetCronjob extends AbstractCronjob
$this->jobSucceeded = true;
$this->message = 'Auto-budget cron job fired successfully.';
FireflyConfig::set('last_ab_job', (int) $this->date->format('U'));
FireflyConfig::set('last_ab_job', (int)$this->date->format('U'));
Log::info('Done with auto budget cron job task.');
}
}

View File

@@ -45,7 +45,7 @@ class BillWarningCronjob extends AbstractCronjob
/** @var Configuration $config */
$config = FireflyConfig::get('last_bw_job', 0);
$lastTime = (int) $config->data;
$lastTime = (int)$config->data;
$diff = now(config('app.timezone'))->getTimestamp() - $lastTime;
$diffForHumans = now(config('app.timezone'))->diffForHumans(Carbon::createFromTimestamp($lastTime), null, true);
@@ -93,8 +93,8 @@ class BillWarningCronjob extends AbstractCronjob
$this->jobSucceeded = true;
$this->message = 'Bill notification cron job fired successfully.';
FireflyConfig::set('last_bw_job', (int) $this->date->format('U'));
Log::info(sprintf('Marked the last time this job has run as "%s" (%d)', $this->date->format('Y-m-d H:i:s'), (int) $this->date->format('U')));
FireflyConfig::set('last_bw_job', (int)$this->date->format('U'));
Log::info(sprintf('Marked the last time this job has run as "%s" (%d)', $this->date->format('Y-m-d H:i:s'), (int)$this->date->format('U')));
Log::info('Done with bill notification cron job task.');
}
}

View File

@@ -39,7 +39,7 @@ class ExchangeRatesCronjob extends AbstractCronjob
{
/** @var Configuration $config */
$config = FireflyConfig::get('last_cer_job', 0);
$lastTime = (int) $config->data;
$lastTime = (int)$config->data;
$diff = now(config('app.timezone'))->getTimestamp() - $lastTime;
$diffForHumans = now(config('app.timezone'))->diffForHumans(Carbon::createFromTimestamp($lastTime), null, true);
if (0 === $lastTime) {
@@ -81,7 +81,7 @@ class ExchangeRatesCronjob extends AbstractCronjob
$this->jobSucceeded = true;
$this->message = 'Exchange rates cron job fired successfully.';
FireflyConfig::set('last_cer_job', (int) $this->date->format('U'));
FireflyConfig::set('last_cer_job', (int)$this->date->format('U'));
Log::info('Done with exchange rates job task.');
}
}

View File

@@ -45,7 +45,7 @@ class RecurringCronjob extends AbstractCronjob
/** @var Configuration $config */
$config = FireflyConfig::get('last_rt_job', 0);
$lastTime = (int) $config->data;
$lastTime = (int)$config->data;
$diff = now(config('app.timezone'))->getTimestamp() - $lastTime;
$diffForHumans = now(config('app.timezone'))->diffForHumans(Carbon::createFromTimestamp($lastTime), null, true);
@@ -90,8 +90,8 @@ class RecurringCronjob extends AbstractCronjob
$this->jobSucceeded = true;
$this->message = 'Recurring transactions cron job fired successfully.';
FireflyConfig::set('last_rt_job', (int) $this->date->format('U'));
Log::info(sprintf('Marked the last time this job has run as "%s" (%d)', $this->date->format('Y-m-d H:i:s'), (int) $this->date->format('U')));
FireflyConfig::set('last_rt_job', (int)$this->date->format('U'));
Log::info(sprintf('Marked the last time this job has run as "%s" (%d)', $this->date->format('Y-m-d H:i:s'), (int)$this->date->format('U')));
Log::info('Done with recurring cron job task.');
}
}

View File

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

View File

@@ -45,7 +45,7 @@ class WebhookCronjob extends AbstractCronjob
/** @var Configuration $config */
$config = FireflyConfig::get('last_webhook_job', 0);
$lastTime = (int) $config->data;
$lastTime = (int)$config->data;
$diff = now(config('app.timezone'))->getTimestamp() - $lastTime;
$diffForHumans = now(config('app.timezone'))->diffForHumans(Carbon::createFromTimestamp($lastTime), null, true);
@@ -90,8 +90,8 @@ class WebhookCronjob extends AbstractCronjob
$this->jobSucceeded = true;
$this->message = 'Send webhook messages cron job fired successfully.';
FireflyConfig::set('last_webhook_job', (int) $this->date->format('U'));
Log::info(sprintf('Marked the last time this job has run as "%s" (%d)', $this->date->format('Y-m-d H:i:s'), (int) $this->date->format('U')));
FireflyConfig::set('last_webhook_job', (int)$this->date->format('U'));
Log::info(sprintf('Marked the last time this job has run as "%s" (%d)', $this->date->format('Y-m-d H:i:s'), (int)$this->date->format('U')));
Log::info('Done with webhook cron job task.');
}
}

View File

@@ -28,8 +28,8 @@ use Illuminate\Support\Facades\Log;
class Timer
{
private array $times = [];
private static ?Timer $instance = null;
private array $times = [];
private function __construct()
{

View File

@@ -23,9 +23,9 @@ declare(strict_types=1);
namespace FireflyIII\Support;
use Illuminate\Database\Eloquent\Model;
use FireflyIII\Exceptions\FireflyException;
use FireflyIII\Support\Form\FormSupport;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Collection;
use Throwable;

View File

@@ -85,7 +85,7 @@ class ExportDataGenerator
private bool $exportTransactions;
private Carbon $start;
private User $user;
private UserGroup $userGroup; // @phpstan-ignore-line
private UserGroup $userGroup; // @phpstan-ignore-line
public function __construct()
{
@@ -141,6 +141,92 @@ class ExportDataGenerator
return $return;
}
/**
* @SuppressWarnings("PHPMD.UnusedFormalParameter")
*/
public function get(string $key, mixed $default = null): mixed
{
return null;
}
/**
* @SuppressWarnings("PHPMD.UnusedFormalParameter")
*/
public function has(mixed $key): mixed
{
return null;
}
public function setAccounts(Collection $accounts): void
{
$this->accounts = $accounts;
}
public function setEnd(Carbon $end): void
{
$this->end = $end;
}
public function setExportAccounts(bool $exportAccounts): void
{
$this->exportAccounts = $exportAccounts;
}
public function setExportBills(bool $exportBills): void
{
$this->exportBills = $exportBills;
}
public function setExportBudgets(bool $exportBudgets): void
{
$this->exportBudgets = $exportBudgets;
}
public function setExportCategories(bool $exportCategories): void
{
$this->exportCategories = $exportCategories;
}
public function setExportPiggies(bool $exportPiggies): void
{
$this->exportPiggies = $exportPiggies;
}
public function setExportRecurring(bool $exportRecurring): void
{
$this->exportRecurring = $exportRecurring;
}
public function setExportRules(bool $exportRules): void
{
$this->exportRules = $exportRules;
}
public function setExportTags(bool $exportTags): void
{
$this->exportTags = $exportTags;
}
public function setExportTransactions(bool $exportTransactions): void
{
$this->exportTransactions = $exportTransactions;
}
public function setStart(Carbon $start): void
{
$this->start = $start;
}
public function setUser(User $user): void
{
$this->user = $user;
}
public function setUserGroup(UserGroup $userGroup): void
{
$this->userGroup = $userGroup;
}
/**
* @throws CannotInsertRecord
* @throws Exception
@@ -222,11 +308,6 @@ class ExportDataGenerator
return $string;
}
public function setUser(User $user): void
{
$this->user = $user;
}
/**
* @throws CannotInsertRecord
* @throws Exception
@@ -588,14 +669,6 @@ class ExportDataGenerator
return $string;
}
/**
* @SuppressWarnings("PHPMD.UnusedFormalParameter")
*/
public function get(string $key, mixed $default = null): mixed
{
return null;
}
/**
* @throws CannotInsertRecord
* @throws Exception
@@ -828,11 +901,6 @@ class ExportDataGenerator
return $string;
}
public function setAccounts(Collection $accounts): void
{
$this->accounts = $accounts;
}
private function mergeTags(array $tags): string
{
if (0 === count($tags)) {
@@ -845,72 +913,4 @@ class ExportDataGenerator
return implode(',', $smol);
}
/**
* @SuppressWarnings("PHPMD.UnusedFormalParameter")
*/
public function has(mixed $key): mixed
{
return null;
}
public function setEnd(Carbon $end): void
{
$this->end = $end;
}
public function setExportAccounts(bool $exportAccounts): void
{
$this->exportAccounts = $exportAccounts;
}
public function setExportBills(bool $exportBills): void
{
$this->exportBills = $exportBills;
}
public function setExportBudgets(bool $exportBudgets): void
{
$this->exportBudgets = $exportBudgets;
}
public function setExportCategories(bool $exportCategories): void
{
$this->exportCategories = $exportCategories;
}
public function setExportPiggies(bool $exportPiggies): void
{
$this->exportPiggies = $exportPiggies;
}
public function setExportRecurring(bool $exportRecurring): void
{
$this->exportRecurring = $exportRecurring;
}
public function setExportRules(bool $exportRules): void
{
$this->exportRules = $exportRules;
}
public function setExportTags(bool $exportTags): void
{
$this->exportTags = $exportTags;
}
public function setExportTransactions(bool $exportTransactions): void
{
$this->exportTransactions = $exportTransactions;
}
public function setStart(Carbon $start): void
{
$this->start = $start;
}
public function setUserGroup(UserGroup $userGroup): void
{
$this->userGroup = $userGroup;
}
}

View File

@@ -23,6 +23,7 @@ declare(strict_types=1);
namespace FireflyIII\Support;
use Exception;
use FireflyIII\Exceptions\FireflyException;
use FireflyIII\Models\Configuration;
use Illuminate\Contracts\Encryption\DecryptException;
@@ -30,7 +31,6 @@ use Illuminate\Contracts\Encryption\EncryptException;
use Illuminate\Database\QueryException;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Log;
use Exception;
/**
* Class FireflyConfig.
@@ -46,34 +46,6 @@ class FireflyConfig
Configuration::where('name', $name)->forceDelete();
}
public function has(string $name): bool
{
return 1 === Configuration::where('name', $name)->count();
}
public function getEncrypted(string $name, mixed $default = null): ?Configuration
{
$result = $this->get($name, $default);
if (!$result instanceof Configuration) {
return null;
}
if ('' === $result->data) {
Log::warning(sprintf('Empty encrypted configuration value found: "%s"', $name));
return $result;
}
try {
$result->data = decrypt($result->data);
} catch (DecryptException $e) {
Log::error(sprintf('Could not decrypt configuration value "%s": %s', $name, $e->getMessage()));
return $result;
}
return $result;
}
/**
* @param null|bool|int|string $default
*
@@ -106,6 +78,56 @@ class FireflyConfig
return $this->set($name, $default);
}
public function getEncrypted(string $name, mixed $default = null): ?Configuration
{
$result = $this->get($name, $default);
if (!$result instanceof Configuration) {
return null;
}
if ('' === $result->data) {
Log::warning(sprintf('Empty encrypted configuration value found: "%s"', $name));
return $result;
}
try {
$result->data = decrypt($result->data);
} catch (DecryptException $e) {
Log::error(sprintf('Could not decrypt configuration value "%s": %s', $name, $e->getMessage()));
return $result;
}
return $result;
}
public function getFresh(string $name, mixed $default = null): ?Configuration
{
$config = Configuration::where('name', $name)->first(['id', 'name', 'data']);
if (null !== $config) {
return $config;
}
// no preference found and default is null:
if (null === $default) {
return null;
}
return $this->set($name, $default);
}
public function has(string $name): bool
{
return 1 === Configuration::where('name', $name)->count();
}
/**
* @param mixed $value
*/
public function put(string $name, $value): Configuration
{
return $this->set($name, $value);
}
public function set(string $name, mixed $value): Configuration
{
try {
@@ -135,28 +157,6 @@ class FireflyConfig
return $config;
}
public function getFresh(string $name, mixed $default = null): ?Configuration
{
$config = Configuration::where('name', $name)->first(['id', 'name', 'data']);
if (null !== $config) {
return $config;
}
// no preference found and default is null:
if (null === $default) {
return null;
}
return $this->set($name, $default);
}
/**
* @param mixed $value
*/
public function put(string $name, $value): Configuration
{
return $this->set($name, $value);
}
public function setEncrypted(string $name, mixed $value): Configuration
{
try {

View File

@@ -51,43 +51,12 @@ class AccountForm
$repository = $this->getAccountRepository();
$grouped = $this->getAccountsGrouped($types, $repository);
$cash = $repository->getCashAccount();
$key = (string) trans('firefly.cash_account_type');
$grouped[$key][$cash->id] = sprintf('(%s)', (string) trans('firefly.cash'));
$key = (string)trans('firefly.cash_account_type');
$grouped[$key][$cash->id] = sprintf('(%s)', (string)trans('firefly.cash'));
return $this->select($name, $grouped, $value, $options);
}
private function getAccountsGrouped(array $types, ?AccountRepositoryInterface $repository = null): array
{
if (!$repository instanceof AccountRepositoryInterface) {
$repository = $this->getAccountRepository();
}
$accountList = $repository->getActiveAccountsByType($types);
$liabilityTypes = [AccountTypeEnum::MORTGAGE->value, AccountTypeEnum::DEBT->value, AccountTypeEnum::CREDITCARD->value, AccountTypeEnum::LOAN->value];
$grouped = [];
/** @var Account $account */
foreach ($accountList as $account) {
$role = (string) $repository->getMetaValue($account, 'account_role');
if (in_array($account->accountType->type, $liabilityTypes, true)) {
$role = sprintf('l_%s', $account->accountType->type);
}
if ('' === $role) {
$role = 'no_account_type';
if (AccountTypeEnum::EXPENSE->value === $account->accountType->type) {
$role = 'expense_account';
}
if (AccountTypeEnum::REVENUE->value === $account->accountType->type) {
$role = 'revenue_account';
}
}
$key = (string) trans(sprintf('firefly.opt_group_%s', $role));
$grouped[$key][$account->id] = $account->name;
}
return $grouped;
}
/**
* Grouped dropdown list of all accounts that are valid as the destination of a withdrawal.
*/
@@ -98,8 +67,8 @@ class AccountForm
$grouped = $this->getAccountsGrouped($types, $repository);
$cash = $repository->getCashAccount();
$key = (string) trans('firefly.cash_account_type');
$grouped[$key][$cash->id] = sprintf('(%s)', (string) trans('firefly.cash'));
$key = (string)trans('firefly.cash_account_type');
$grouped[$key][$cash->id] = sprintf('(%s)', (string)trans('firefly.cash'));
return $this->select($name, $grouped, $value, $options);
}
@@ -173,4 +142,35 @@ class AccountForm
return $this->select($name, $grouped, $value, $options);
}
private function getAccountsGrouped(array $types, ?AccountRepositoryInterface $repository = null): array
{
if (!$repository instanceof AccountRepositoryInterface) {
$repository = $this->getAccountRepository();
}
$accountList = $repository->getActiveAccountsByType($types);
$liabilityTypes = [AccountTypeEnum::MORTGAGE->value, AccountTypeEnum::DEBT->value, AccountTypeEnum::CREDITCARD->value, AccountTypeEnum::LOAN->value];
$grouped = [];
/** @var Account $account */
foreach ($accountList as $account) {
$role = (string)$repository->getMetaValue($account, 'account_role');
if (in_array($account->accountType->type, $liabilityTypes, true)) {
$role = sprintf('l_%s', $account->accountType->type);
}
if ('' === $role) {
$role = 'no_account_type';
if (AccountTypeEnum::EXPENSE->value === $account->accountType->type) {
$role = 'expense_account';
}
if (AccountTypeEnum::REVENUE->value === $account->accountType->type) {
$role = 'revenue_account';
}
}
$key = (string)trans(sprintf('firefly.opt_group_%s', $role));
$grouped[$key][$account->id] = $account->name;
}
return $grouped;
}
}

View File

@@ -49,60 +49,6 @@ class CurrencyForm
return $this->currencyField($name, 'amount', $value, $options);
}
/**
* @phpstan-param view-string $view
*
* @throws FireflyException
*/
protected function currencyField(string $name, string $view, mixed $value = null, ?array $options = null): string
{
$label = $this->label($name, $options);
$options = $this->expandOptionArray($name, $label, $options);
$classes = $this->getHolderClasses($name);
$value = $this->fillFieldValue($name, $value);
$options['step'] = 'any';
$primaryCurrency = $options['currency'] ?? app('amount')->getPrimaryCurrency();
/** @var Collection $currencies */
$currencies = app('amount')->getCurrencies();
unset($options['currency'], $options['placeholder']);
// perhaps the currency has been sent to us in the field $amount_currency_id_$name (amount_currency_id_amount)
$preFilled = session('preFilled');
if (!is_array($preFilled)) {
$preFilled = [];
}
$key = 'amount_currency_id_'.$name;
$sentCurrencyId = array_key_exists($key, $preFilled) ? (int) $preFilled[$key] : $primaryCurrency->id;
app('log')->debug(sprintf('Sent currency ID is %d', $sentCurrencyId));
// find this currency in set of currencies:
foreach ($currencies as $currency) {
if ($currency->id === $sentCurrencyId) {
$primaryCurrency = $currency;
app('log')->debug(sprintf('default currency is now %s', $primaryCurrency->code));
break;
}
}
// make sure value is formatted nicely:
if (null !== $value && '' !== $value) {
$value = app('steam')->bcround($value, $primaryCurrency->decimal_places);
}
try {
$html = view('form.'.$view, compact('primaryCurrency', 'currencies', 'classes', 'name', 'label', 'value', 'options'))->render();
} catch (Throwable $e) {
app('log')->debug(sprintf('Could not render currencyField(): %s', $e->getMessage()));
$html = 'Could not render currencyField.';
throw new FireflyException($html, 0, $e);
}
return $html;
}
/**
* TODO describe and cleanup.
*
@@ -115,63 +61,6 @@ class CurrencyForm
return $this->allCurrencyField($name, 'balance', $value, $options);
}
/**
* TODO describe and cleanup
*
* @param mixed $value
*
* @throws FireflyException
*/
protected function allCurrencyField(string $name, string $view, $value = null, ?array $options = null): string
{
$label = $this->label($name, $options);
$options = $this->expandOptionArray($name, $label, $options);
$classes = $this->getHolderClasses($name);
$value = $this->fillFieldValue($name, $value);
$options['step'] = 'any';
$primaryCurrency = $options['currency'] ?? app('amount')->getPrimaryCurrency();
/** @var Collection $currencies */
$currencies = app('amount')->getAllCurrencies();
unset($options['currency'], $options['placeholder']);
// perhaps the currency has been sent to us in the field $amount_currency_id_$name (amount_currency_id_amount)
$preFilled = session('preFilled');
if (!is_array($preFilled)) {
$preFilled = [];
}
$key = 'amount_currency_id_'.$name;
$sentCurrencyId = array_key_exists($key, $preFilled) ? (int) $preFilled[$key] : $primaryCurrency->id;
app('log')->debug(sprintf('Sent currency ID is %d', $sentCurrencyId));
// find this currency in set of currencies:
foreach ($currencies as $currency) {
if ($currency->id === $sentCurrencyId) {
$primaryCurrency = $currency;
app('log')->debug(sprintf('default currency is now %s', $primaryCurrency->code));
break;
}
}
// make sure value is formatted nicely:
if (null !== $value && '' !== $value) {
$value = app('steam')->bcround($value, $primaryCurrency->decimal_places);
}
try {
$html = view('form.'.$view, compact('primaryCurrency', 'currencies', 'classes', 'name', 'label', 'value', 'options'))->render();
} catch (Throwable $e) {
app('log')->debug(sprintf('Could not render currencyField(): %s', $e->getMessage()));
$html = 'Could not render currencyField.';
throw new FireflyException($html, 0, $e);
}
return $html;
}
/**
* TODO cleanup and describe
*
@@ -207,7 +96,7 @@ class CurrencyForm
// get all currencies:
$list = $currencyRepos->get();
$array = [
0 => (string) trans('firefly.no_currency'),
0 => (string)trans('firefly.no_currency'),
];
/** @var TransactionCurrency $currency */
@@ -217,4 +106,115 @@ class CurrencyForm
return $this->select($name, $array, $value, $options);
}
/**
* TODO describe and cleanup
*
* @param mixed $value
*
* @throws FireflyException
*/
protected function allCurrencyField(string $name, string $view, $value = null, ?array $options = null): string
{
$label = $this->label($name, $options);
$options = $this->expandOptionArray($name, $label, $options);
$classes = $this->getHolderClasses($name);
$value = $this->fillFieldValue($name, $value);
$options['step'] = 'any';
$primaryCurrency = $options['currency'] ?? app('amount')->getPrimaryCurrency();
/** @var Collection $currencies */
$currencies = app('amount')->getAllCurrencies();
unset($options['currency'], $options['placeholder']);
// perhaps the currency has been sent to us in the field $amount_currency_id_$name (amount_currency_id_amount)
$preFilled = session('preFilled');
if (!is_array($preFilled)) {
$preFilled = [];
}
$key = 'amount_currency_id_'.$name;
$sentCurrencyId = array_key_exists($key, $preFilled) ? (int)$preFilled[$key] : $primaryCurrency->id;
app('log')->debug(sprintf('Sent currency ID is %d', $sentCurrencyId));
// find this currency in set of currencies:
foreach ($currencies as $currency) {
if ($currency->id === $sentCurrencyId) {
$primaryCurrency = $currency;
app('log')->debug(sprintf('default currency is now %s', $primaryCurrency->code));
break;
}
}
// make sure value is formatted nicely:
if (null !== $value && '' !== $value) {
$value = app('steam')->bcround($value, $primaryCurrency->decimal_places);
}
try {
$html = view('form.'.$view, compact('primaryCurrency', 'currencies', 'classes', 'name', 'label', 'value', 'options'))->render();
} catch (Throwable $e) {
app('log')->debug(sprintf('Could not render currencyField(): %s', $e->getMessage()));
$html = 'Could not render currencyField.';
throw new FireflyException($html, 0, $e);
}
return $html;
}
/**
* @phpstan-param view-string $view
*
* @throws FireflyException
*/
protected function currencyField(string $name, string $view, mixed $value = null, ?array $options = null): string
{
$label = $this->label($name, $options);
$options = $this->expandOptionArray($name, $label, $options);
$classes = $this->getHolderClasses($name);
$value = $this->fillFieldValue($name, $value);
$options['step'] = 'any';
$primaryCurrency = $options['currency'] ?? app('amount')->getPrimaryCurrency();
/** @var Collection $currencies */
$currencies = app('amount')->getCurrencies();
unset($options['currency'], $options['placeholder']);
// perhaps the currency has been sent to us in the field $amount_currency_id_$name (amount_currency_id_amount)
$preFilled = session('preFilled');
if (!is_array($preFilled)) {
$preFilled = [];
}
$key = 'amount_currency_id_'.$name;
$sentCurrencyId = array_key_exists($key, $preFilled) ? (int)$preFilled[$key] : $primaryCurrency->id;
app('log')->debug(sprintf('Sent currency ID is %d', $sentCurrencyId));
// find this currency in set of currencies:
foreach ($currencies as $currency) {
if ($currency->id === $sentCurrencyId) {
$primaryCurrency = $currency;
app('log')->debug(sprintf('default currency is now %s', $primaryCurrency->code));
break;
}
}
// make sure value is formatted nicely:
if (null !== $value && '' !== $value) {
$value = app('steam')->bcround($value, $primaryCurrency->decimal_places);
}
try {
$html = view('form.'.$view, compact('primaryCurrency', 'currencies', 'classes', 'name', 'label', 'value', 'options'))->render();
} catch (Throwable $e) {
app('log')->debug(sprintf('Could not render currencyField(): %s', $e->getMessage()));
$html = 'Could not render currencyField.';
throw new FireflyException($html, 0, $e);
}
return $html;
}
}

View File

@@ -54,15 +54,26 @@ trait FormSupport
return $html;
}
protected function label(string $name, ?array $options = null): string
/**
* @param mixed $selected
*/
public function select(string $name, ?array $list = null, $selected = null, ?array $options = null): string
{
$options ??= [];
if (array_key_exists('label', $options)) {
return $options['label'];
}
$name = str_replace('[]', '', $name);
$list ??= [];
$label = $this->label($name, $options);
$options = $this->expandOptionArray($name, $label, $options);
$classes = $this->getHolderClasses($name);
$selected = $this->fillFieldValue($name, $selected);
unset($options['autocomplete'], $options['placeholder']);
return (string)trans('form.'.$name);
try {
$html = view('form.select', compact('classes', 'name', 'label', 'selected', 'options', 'list'))->render();
} catch (Throwable $e) {
app('log')->debug(sprintf('Could not render select(): %s', $e->getMessage()));
$html = 'Could not render select.';
}
return $html;
}
/**
@@ -80,19 +91,6 @@ trait FormSupport
return $options;
}
protected function getHolderClasses(string $name): string
{
// Get errors from session:
/** @var null|MessageBag $errors */
$errors = session('errors');
if (null !== $errors && $errors->has($name)) {
return 'form-group has-error has-feedback';
}
return 'form-group';
}
/**
* @param null|mixed $value
*
@@ -116,28 +114,6 @@ trait FormSupport
return $value;
}
/**
* @param mixed $selected
*/
public function select(string $name, ?array $list = null, $selected = null, ?array $options = null): string
{
$list ??= [];
$label = $this->label($name, $options);
$options = $this->expandOptionArray($name, $label, $options);
$classes = $this->getHolderClasses($name);
$selected = $this->fillFieldValue($name, $selected);
unset($options['autocomplete'], $options['placeholder']);
try {
$html = view('form.select', compact('classes', 'name', 'label', 'selected', 'options', 'list'))->render();
} catch (Throwable $e) {
app('log')->debug(sprintf('Could not render select(): %s', $e->getMessage()));
$html = 'Could not render select.';
}
return $html;
}
protected function getAccountRepository(): AccountRepositoryInterface
{
return app(AccountRepositoryInterface::class);
@@ -147,4 +123,28 @@ trait FormSupport
{
return today(config('app.timezone'));
}
protected function getHolderClasses(string $name): string
{
// Get errors from session:
/** @var null|MessageBag $errors */
$errors = session('errors');
if (null !== $errors && $errors->has($name)) {
return 'form-group has-error has-feedback';
}
return 'form-group';
}
protected function label(string $name, ?array $options = null): string
{
$options ??= [];
if (array_key_exists('label', $options)) {
return $options['label'];
}
$name = str_replace('[]', '', $name);
return (string)trans('form.'.$name);
}
}

View File

@@ -47,7 +47,7 @@ class PiggyBankForm
/** @var PiggyBankRepositoryInterface $repository */
$repository = app(PiggyBankRepositoryInterface::class);
$piggyBanks = $repository->getPiggyBanksWithAmount();
$title = (string) trans('firefly.default_group_title_name');
$title = (string)trans('firefly.default_group_title_name');
$array = [];
$subList = [
0 => [
@@ -55,7 +55,7 @@ class PiggyBankForm
'title' => $title,
],
'piggies' => [
(string) trans('firefly.none_in_select_list'),
(string)trans('firefly.none_in_select_list'),
],
],
];

View File

@@ -66,12 +66,12 @@ class RuleForm
// get all currencies:
$list = $groupRepos->get();
$array = [
0 => (string) trans('firefly.none_in_select_list'),
0 => (string)trans('firefly.none_in_select_list'),
];
/** @var RuleGroup $group */
foreach ($list as $group) {
if (array_key_exists('hidden', $options) && (int) $options['hidden'] !== $group->id) {
if (array_key_exists('hidden', $options) && (int)$options['hidden'] !== $group->id) {
$array[$group->id] = $group->title;
}
}

View File

@@ -44,10 +44,10 @@ class AccountBalanceGrouped
private readonly ExchangeRateConverter $converter;
private array $currencies = [];
private array $data = [];
private TransactionCurrency $primary;
private Carbon $end;
private array $journals = [];
private string $preferredRange;
private TransactionCurrency $primary;
private Carbon $start;
public function __construct()
@@ -146,48 +146,49 @@ class AccountBalanceGrouped
$converter->summarize();
}
private function processJournal(array $journal): void
public function setAccounts(Collection $accounts): void
{
// format the date according to the period
$period = $journal['date']->format($this->carbonFormat);
$currencyId = (int)$journal['currency_id'];
$currency = $this->findCurrency($currencyId);
// set the array with monetary info, if it does not exist.
$this->createDefaultDataEntry($journal);
// set the array (in monetary info) with spent/earned in this $period, if it does not exist.
$this->createDefaultPeriodEntry($journal);
// is this journal's amount in- our outgoing?
$key = $this->getDataKey($journal);
$amount = 'spent' === $key ? Steam::negative($journal['amount']) : Steam::positive($journal['amount']);
// get conversion rate
$rate = $this->getRate($currency, $journal['date']);
$amountConverted = bcmul($amount, $rate);
// perhaps transaction already has the foreign amount in the primary currency.
if ((int)$journal['foreign_currency_id'] === $this->primary->id) {
$amountConverted = $journal['foreign_amount'] ?? '0';
$amountConverted = 'earned' === $key ? Steam::positive($amountConverted) : Steam::negative($amountConverted);
}
// add normal entry
$this->data[$currencyId][$period][$key] = bcadd((string)$this->data[$currencyId][$period][$key], $amount);
// add converted entry
$convertedKey = sprintf('pc_%s', $key);
$this->data[$currencyId][$period][$convertedKey] = bcadd((string)$this->data[$currencyId][$period][$convertedKey], $amountConverted);
$this->accountIds = $accounts->pluck('id')->toArray();
}
private function findCurrency(int $currencyId): TransactionCurrency
public function setEnd(Carbon $end): void
{
if (array_key_exists($currencyId, $this->currencies)) {
return $this->currencies[$currencyId];
}
$this->currencies[$currencyId] = Amount::getTransactionCurrencyById($currencyId);
$this->end = $end;
}
return $this->currencies[$currencyId];
public function setJournals(array $journals): void
{
$this->journals = $journals;
}
public function setPreferredRange(string $preferredRange): void
{
$this->preferredRange = $preferredRange;
$this->carbonFormat = Navigation::preferredCarbonFormatByPeriod($preferredRange);
}
public function setPrimary(TransactionCurrency $primary): void
{
$this->primary = $primary;
$primaryCurrencyId = $primary->id;
$this->currencies = [$primary->id => $primary]; // currency cache
$this->data[$primaryCurrencyId] = [
'currency_id' => (string)$primaryCurrencyId,
'currency_symbol' => $primary->symbol,
'currency_code' => $primary->code,
'currency_name' => $primary->name,
'currency_decimal_places' => $primary->decimal_places,
'primary_currency_id' => (string)$primaryCurrencyId,
'primary_currency_symbol' => $primary->symbol,
'primary_currency_code' => $primary->code,
'primary_currency_name' => $primary->name,
'primary_currency_decimal_places' => $primary->decimal_places,
];
}
public function setStart(Carbon $start): void
{
$this->start = $start;
}
private function createDefaultDataEntry(array $journal): void
@@ -220,6 +221,16 @@ class AccountBalanceGrouped
];
}
private function findCurrency(int $currencyId): TransactionCurrency
{
if (array_key_exists($currencyId, $this->currencies)) {
return $this->currencies[$currencyId];
}
$this->currencies[$currencyId] = Amount::getTransactionCurrencyById($currencyId);
return $this->currencies[$currencyId];
}
private function getDataKey(array $journal): string
{
// deposit = incoming
@@ -254,48 +265,37 @@ class AccountBalanceGrouped
return $rate;
}
public function setAccounts(Collection $accounts): void
private function processJournal(array $journal): void
{
$this->accountIds = $accounts->pluck('id')->toArray();
}
// format the date according to the period
$period = $journal['date']->format($this->carbonFormat);
$currencyId = (int)$journal['currency_id'];
$currency = $this->findCurrency($currencyId);
public function setPrimary(TransactionCurrency $primary): void
{
$this->primary = $primary;
$primaryCurrencyId = $primary->id;
$this->currencies = [$primary->id => $primary]; // currency cache
$this->data[$primaryCurrencyId] = [
'currency_id' => (string)$primaryCurrencyId,
'currency_symbol' => $primary->symbol,
'currency_code' => $primary->code,
'currency_name' => $primary->name,
'currency_decimal_places' => $primary->decimal_places,
'primary_currency_id' => (string)$primaryCurrencyId,
'primary_currency_symbol' => $primary->symbol,
'primary_currency_code' => $primary->code,
'primary_currency_name' => $primary->name,
'primary_currency_decimal_places' => $primary->decimal_places,
];
}
// set the array with monetary info, if it does not exist.
$this->createDefaultDataEntry($journal);
// set the array (in monetary info) with spent/earned in this $period, if it does not exist.
$this->createDefaultPeriodEntry($journal);
public function setEnd(Carbon $end): void
{
$this->end = $end;
}
// is this journal's amount in- our outgoing?
$key = $this->getDataKey($journal);
$amount = 'spent' === $key ? Steam::negative($journal['amount']) : Steam::positive($journal['amount']);
public function setJournals(array $journals): void
{
$this->journals = $journals;
}
// get conversion rate
$rate = $this->getRate($currency, $journal['date']);
$amountConverted = bcmul($amount, $rate);
public function setPreferredRange(string $preferredRange): void
{
$this->preferredRange = $preferredRange;
$this->carbonFormat = Navigation::preferredCarbonFormatByPeriod($preferredRange);
}
// perhaps transaction already has the foreign amount in the primary currency.
if ((int)$journal['foreign_currency_id'] === $this->primary->id) {
$amountConverted = $journal['foreign_amount'] ?? '0';
$amountConverted = 'earned' === $key ? Steam::positive($amountConverted) : Steam::negative($amountConverted);
}
public function setStart(Carbon $start): void
{
$this->start = $start;
// add normal entry
$this->data[$currencyId][$period][$key] = bcadd((string)$this->data[$currencyId][$period][$key], $amount);
// add converted entry
$convertedKey = sprintf('pc_%s', $key);
$this->data[$currencyId][$period][$convertedKey] = bcadd((string)$this->data[$currencyId][$period][$convertedKey], $amountConverted);
}
}

View File

@@ -39,7 +39,7 @@ trait CollectsAccountsFromFilter
// always collect from the query parameter, even when it's empty.
if (null !== $queryParameters['accounts']) {
foreach ($queryParameters['accounts'] as $accountId) {
$account = $this->repository->find((int) $accountId);
$account = $this->repository->find((int)$accountId);
if (null !== $account) {
$collection->push($account);
}

View File

@@ -94,6 +94,149 @@ class ExchangeRateConverter
return '0' === $rate ? '1' : $rate;
}
public function setIgnoreSettings(bool $ignoreSettings): void
{
$this->ignoreSettings = $ignoreSettings;
}
public function setUserGroup(UserGroup $userGroup): void
{
$this->userGroup = $userGroup;
}
public function summarize(): void
{
if (false === $this->enabled()) {
return;
}
Log::debug(sprintf('ExchangeRateConverter ran %d queries.', $this->queryCount));
}
private function getCacheKey(TransactionCurrency $from, TransactionCurrency $to, Carbon $date): string
{
return sprintf('cer-%d-%d-%s', $from->id, $to->id, $date->format('Y-m-d'));
}
/**
* @throws FireflyException
*/
private function getEuroId(): int
{
Log::debug('getEuroId()');
$cache = new CacheProperties();
$cache->addProperty('cer-euro-id');
if ($cache->has()) {
return (int)$cache->get();
}
$euro = Amount::getTransactionCurrencyByCode('EUR');
++$this->queryCount;
$cache->store($euro->id);
return $euro->id;
}
/**
* @throws FireflyException
*/
private function getEuroRate(TransactionCurrency $currency, Carbon $date): string
{
$euroId = $this->getEuroId();
if ($euroId === $currency->id) {
return '1';
}
$rate = $this->getFromDB($currency->id, $euroId, $date->format('Y-m-d'));
if (null !== $rate) {
// app('log')->debug(sprintf('Rate for %s to EUR is %s.', $currency->code, $rate));
return $rate;
}
$rate = $this->getFromDB($euroId, $currency->id, $date->format('Y-m-d'));
if (null !== $rate) {
return bcdiv('1', $rate);
// app('log')->debug(sprintf('Inverted rate for %s to EUR is %s.', $currency->code, $rate));
// return $rate;
}
// grab backup values from config file:
$backup = config(sprintf('cer.rates.%s', $currency->code));
if (null !== $backup) {
return bcdiv('1', (string)$backup);
// app('log')->debug(sprintf('Backup rate for %s to EUR is %s.', $currency->code, $backup));
// return $backup;
}
// app('log')->debug(sprintf('No rate for %s to EUR.', $currency->code));
return '0';
}
private function getFromDB(int $from, int $to, string $date): ?string
{
if ($from === $to) {
Log::debug('ExchangeRateConverter: From and to are the same, return "1".');
return '1';
}
$key = sprintf('cer-%d-%d-%s', $from, $to, $date);
// perhaps the rate has been cached during this particular run
$preparedRate = $this->prepared[$date][$from][$to] ?? null;
if (null !== $preparedRate && 0 !== bccomp('0', $preparedRate)) {
Log::debug(sprintf('ExchangeRateConverter: Found prepared rate from #%d to #%d on %s.', $from, $to, $date));
return $preparedRate;
}
$cache = new CacheProperties();
$cache->addProperty($key);
if ($cache->has()) {
$rate = $cache->get();
if ('' === $rate) {
return null;
}
Log::debug(sprintf('ExchangeRateConverter: Found cached rate from #%d to #%d on %s.', $from, $to, $date));
return $rate;
}
/** @var null|CurrencyExchangeRate $result */
$result = $this->userGroup->currencyExchangeRates()
->where('from_currency_id', $from)
->where('to_currency_id', $to)
->where('date', '<=', $date)
->orderBy('date', 'DESC')
->first()
;
++$this->queryCount;
$rate = (string)$result?->rate;
if ('' === $rate) {
app('log')->debug(sprintf('ExchangeRateConverter: Found no rate for #%d->#%d (%s) in the DB.', $from, $to, $date));
return null;
}
if (0 === bccomp('0', $rate)) {
app('log')->debug(sprintf('ExchangeRateConverter: Found rate for #%d->#%d (%s) in the DB, but it\'s zero.', $from, $to, $date));
return null;
}
app('log')->debug(sprintf('ExchangeRateConverter: Found rate for #%d->#%d (%s) in the DB: %s.', $from, $to, $date, $rate));
$cache->store($rate);
// if the rate has not been cached during this particular run, save it
$this->prepared[$date] ??= [
$from => [
$to => $rate,
],
];
// also save the exchange rate the other way around:
$this->prepared[$date] ??= [
$to => [
$from => bcdiv('1', $rate),
],
];
return $rate;
}
/**
* @throws FireflyException
*/
@@ -146,147 +289,4 @@ class ExchangeRateConverter
return $rate;
}
private function getCacheKey(TransactionCurrency $from, TransactionCurrency $to, Carbon $date): string
{
return sprintf('cer-%d-%d-%s', $from->id, $to->id, $date->format('Y-m-d'));
}
private function getFromDB(int $from, int $to, string $date): ?string
{
if ($from === $to) {
Log::debug('ExchangeRateConverter: From and to are the same, return "1".');
return '1';
}
$key = sprintf('cer-%d-%d-%s', $from, $to, $date);
// perhaps the rate has been cached during this particular run
$preparedRate = $this->prepared[$date][$from][$to] ?? null;
if (null !== $preparedRate && 0 !== bccomp('0', $preparedRate)) {
Log::debug(sprintf('ExchangeRateConverter: Found prepared rate from #%d to #%d on %s.', $from, $to, $date));
return $preparedRate;
}
$cache = new CacheProperties();
$cache->addProperty($key);
if ($cache->has()) {
$rate = $cache->get();
if ('' === $rate) {
return null;
}
Log::debug(sprintf('ExchangeRateConverter: Found cached rate from #%d to #%d on %s.', $from, $to, $date));
return $rate;
}
/** @var null|CurrencyExchangeRate $result */
$result = $this->userGroup->currencyExchangeRates()
->where('from_currency_id', $from)
->where('to_currency_id', $to)
->where('date', '<=', $date)
->orderBy('date', 'DESC')
->first()
;
++$this->queryCount;
$rate = (string) $result?->rate;
if ('' === $rate) {
app('log')->debug(sprintf('ExchangeRateConverter: Found no rate for #%d->#%d (%s) in the DB.', $from, $to, $date));
return null;
}
if (0 === bccomp('0', $rate)) {
app('log')->debug(sprintf('ExchangeRateConverter: Found rate for #%d->#%d (%s) in the DB, but it\'s zero.', $from, $to, $date));
return null;
}
app('log')->debug(sprintf('ExchangeRateConverter: Found rate for #%d->#%d (%s) in the DB: %s.', $from, $to, $date, $rate));
$cache->store($rate);
// if the rate has not been cached during this particular run, save it
$this->prepared[$date] ??= [
$from => [
$to => $rate,
],
];
// also save the exchange rate the other way around:
$this->prepared[$date] ??= [
$to => [
$from => bcdiv('1', $rate),
],
];
return $rate;
}
/**
* @throws FireflyException
*/
private function getEuroRate(TransactionCurrency $currency, Carbon $date): string
{
$euroId = $this->getEuroId();
if ($euroId === $currency->id) {
return '1';
}
$rate = $this->getFromDB($currency->id, $euroId, $date->format('Y-m-d'));
if (null !== $rate) {
// app('log')->debug(sprintf('Rate for %s to EUR is %s.', $currency->code, $rate));
return $rate;
}
$rate = $this->getFromDB($euroId, $currency->id, $date->format('Y-m-d'));
if (null !== $rate) {
return bcdiv('1', $rate);
// app('log')->debug(sprintf('Inverted rate for %s to EUR is %s.', $currency->code, $rate));
// return $rate;
}
// grab backup values from config file:
$backup = config(sprintf('cer.rates.%s', $currency->code));
if (null !== $backup) {
return bcdiv('1', (string) $backup);
// app('log')->debug(sprintf('Backup rate for %s to EUR is %s.', $currency->code, $backup));
// return $backup;
}
// app('log')->debug(sprintf('No rate for %s to EUR.', $currency->code));
return '0';
}
/**
* @throws FireflyException
*/
private function getEuroId(): int
{
Log::debug('getEuroId()');
$cache = new CacheProperties();
$cache->addProperty('cer-euro-id');
if ($cache->has()) {
return (int) $cache->get();
}
$euro = Amount::getTransactionCurrencyByCode('EUR');
++$this->queryCount;
$cache->store($euro->id);
return $euro->id;
}
public function setIgnoreSettings(bool $ignoreSettings): void
{
$this->ignoreSettings = $ignoreSettings;
}
public function setUserGroup(UserGroup $userGroup): void
{
$this->userGroup = $userGroup;
}
public function summarize(): void
{
if (false === $this->enabled()) {
return;
}
Log::debug(sprintf('ExchangeRateConverter ran %d queries.', $this->queryCount));
}
}

View File

@@ -60,7 +60,7 @@ class SummaryBalanceGrouped
$return[] = [
'key' => sprintf('%s-in-pc', $title),
'value' => $this->amounts[$key]['primary'] ?? '0',
'currency_id' => (string) $this->default->id,
'currency_id' => (string)$this->default->id,
'currency_code' => $this->default->code,
'currency_symbol' => $this->default->symbol,
'currency_decimal_places' => $this->default->decimal_places,
@@ -73,7 +73,7 @@ class SummaryBalanceGrouped
// skip primary entries.
continue;
}
$currencyId = (int) $currencyId;
$currencyId = (int)$currencyId;
$currency = $this->currencies[$currencyId] ?? $this->currencyRepository->find($currencyId);
$this->currencies[$currencyId] = $currency;
// create objects for big array.
@@ -87,7 +87,7 @@ class SummaryBalanceGrouped
$return[] = [
'key' => sprintf('%s-in-%s', $title, $currency->code),
'value' => $this->amounts[$key][$currencyId] ?? '0',
'currency_id' => (string) $currency->id,
'currency_id' => (string)$currency->id,
'currency_code' => $currency->code,
'currency_symbol' => $currency->symbol,
'currency_decimal_places' => $currency->decimal_places,
@@ -109,12 +109,12 @@ class SummaryBalanceGrouped
/** @var array $journal */
foreach ($journals as $journal) {
// transaction info:
$currencyId = (int) $journal['currency_id'];
$amount = bcmul((string) $journal['amount'], $multiplier);
$currencyId = (int)$journal['currency_id'];
$amount = bcmul((string)$journal['amount'], $multiplier);
$currency = $this->currencies[$currencyId] ?? Amount::getTransactionCurrencyById($currencyId);
$this->currencies[$currencyId] = $currency;
$pcAmount = $converter->convert($currency, $this->default, $journal['date'], $amount);
if ((int) $journal['foreign_currency_id'] === $this->default->id) {
if ((int)$journal['foreign_currency_id'] === $this->default->id) {
// use foreign amount instead
$pcAmount = $journal['foreign_amount'];
}
@@ -126,10 +126,10 @@ class SummaryBalanceGrouped
$this->amounts[self::SUM]['primary'] ??= '0';
// add values:
$this->amounts[$key][$currencyId] = bcadd((string) $this->amounts[$key][$currencyId], $amount);
$this->amounts[self::SUM][$currencyId] = bcadd((string) $this->amounts[self::SUM][$currencyId], $amount);
$this->amounts[$key]['primary'] = bcadd((string) $this->amounts[$key]['primary'], (string) $pcAmount);
$this->amounts[self::SUM]['primary'] = bcadd((string) $this->amounts[self::SUM]['primary'], (string) $pcAmount);
$this->amounts[$key][$currencyId] = bcadd((string)$this->amounts[$key][$currencyId], $amount);
$this->amounts[self::SUM][$currencyId] = bcadd((string)$this->amounts[self::SUM][$currencyId], $amount);
$this->amounts[$key]['primary'] = bcadd((string)$this->amounts[$key]['primary'], (string)$pcAmount);
$this->amounts[self::SUM]['primary'] = bcadd((string)$this->amounts[self::SUM]['primary'], (string)$pcAmount);
}
$converter->summarize();
}

View File

@@ -38,8 +38,8 @@ use Illuminate\Support\Facades\Log;
*/
trait ValidatesUserGroupTrait
{
protected User $user;
protected UserGroup $userGroup;
protected User $user;
/**
* An "undocumented" filter
@@ -62,11 +62,11 @@ trait ValidatesUserGroupTrait
$user = auth()->user();
$groupId = 0;
if (!$request->has('user_group_id')) {
$groupId = (int) $user->user_group_id;
$groupId = (int)$user->user_group_id;
Log::debug(sprintf('validateUserGroup: no user group submitted, use default group #%d.', $groupId));
}
if ($request->has('user_group_id')) {
$groupId = (int) $request->get('user_group_id');
$groupId = (int)$request->get('user_group_id');
Log::debug(sprintf('validateUserGroup: user group submitted, search for memberships in group #%d.', $groupId));
}
@@ -78,7 +78,7 @@ trait ValidatesUserGroupTrait
if (0 === $memberships->count()) {
Log::debug(sprintf('validateUserGroup: user has no access to group #%d.', $groupId));
throw new AuthorizationException((string) trans('validation.no_access_group'));
throw new AuthorizationException((string)trans('validation.no_access_group'));
}
// need to get the group from the membership:
@@ -86,14 +86,14 @@ trait ValidatesUserGroupTrait
if (null === $group) {
Log::debug(sprintf('validateUserGroup: group #%d does not exist.', $groupId));
throw new AuthorizationException((string) trans('validation.belongs_user_or_user_group'));
throw new AuthorizationException((string)trans('validation.belongs_user_or_user_group'));
}
Log::debug(sprintf('validateUserGroup: validate access of user to group #%d ("%s").', $groupId, $group->title));
$roles = property_exists($this, 'acceptedRoles') ? $this->acceptedRoles : []; // @phpstan-ignore-line
if (0 === count($roles)) {
Log::debug('validateUserGroup: no roles defined, so no access.');
throw new AuthorizationException((string) trans('validation.no_accepted_roles_defined'));
throw new AuthorizationException((string)trans('validation.no_accepted_roles_defined'));
}
Log::debug(sprintf('validateUserGroup: have %d roles to check.', count($roles)), $roles);
@@ -111,6 +111,6 @@ trait ValidatesUserGroupTrait
Log::debug('validateUserGroup: User does NOT have enough rights to access endpoint.');
throw new AuthorizationException((string) trans('validation.belongs_user_or_user_group'));
throw new AuthorizationException((string)trans('validation.belongs_user_or_user_group'));
}
}

View File

@@ -110,8 +110,8 @@ trait AugumentData
$grouped = $accounts->groupBy('id')->toArray();
$return = [];
foreach ($accountIds as $combinedId) {
$parts = explode('-', (string) $combinedId);
$accountId = (int) $parts[0];
$parts = explode('-', (string)$combinedId);
$accountId = (int)$parts[0];
if (array_key_exists($accountId, $grouped)) {
$return[$accountId] = $grouped[$accountId][0]['name'];
}
@@ -136,7 +136,7 @@ trait AugumentData
$return[$budgetId] = $grouped[$budgetId][0]['name'];
}
}
$return[0] = (string) trans('firefly.no_budget');
$return[0] = (string)trans('firefly.no_budget');
return $return;
}
@@ -152,13 +152,13 @@ trait AugumentData
$grouped = $categories->groupBy('id')->toArray();
$return = [];
foreach ($categoryIds as $combinedId) {
$parts = explode('-', (string) $combinedId);
$categoryId = (int) $parts[0];
$parts = explode('-', (string)$combinedId);
$categoryId = (int)$parts[0];
if (array_key_exists($categoryId, $grouped)) {
$return[$categoryId] = $grouped[$categoryId][0]['name'];
}
}
$return[0] = (string) trans('firefly.no_category');
$return[0] = (string)trans('firefly.no_category');
return $return;
}
@@ -249,7 +249,7 @@ trait AugumentData
}
$grouped[$name] ??= '0';
$grouped[$name] = bcadd((string) $journal['amount'], $grouped[$name]);
$grouped[$name] = bcadd((string)$journal['amount'], $grouped[$name]);
}
return $grouped;
@@ -272,7 +272,7 @@ trait AugumentData
];
// loop to support multi currency
foreach ($journals as $journal) {
$currencyId = (int) $journal['currency_id'];
$currencyId = (int)$journal['currency_id'];
// if not set, set to zero:
if (!array_key_exists($currencyId, $sum['per_currency'])) {
@@ -287,8 +287,8 @@ trait AugumentData
}
// add amount
$sum['per_currency'][$currencyId]['sum'] = bcadd($sum['per_currency'][$currencyId]['sum'], (string) $journal['amount']);
$sum['grand_sum'] = bcadd($sum['grand_sum'], (string) $journal['amount']);
$sum['per_currency'][$currencyId]['sum'] = bcadd($sum['per_currency'][$currencyId]['sum'], (string)$journal['amount']);
$sum['grand_sum'] = bcadd($sum['grand_sum'], (string)$journal['amount']);
}
return $sum;

View File

@@ -92,7 +92,7 @@ trait ChartGeneration
Log::debug(sprintf('Start balance for account #%d ("%s) is', $account->id, $account->name), $previous);
while ($currentStart <= $end) {
$format = $currentStart->format('Y-m-d');
$label = trim($currentStart->isoFormat((string) trans('config.month_and_day_js', [], $locale)));
$label = trim($currentStart->isoFormat((string)trans('config.month_and_day_js', [], $locale)));
$balance = $range[$format] ?? $previous;
$previous = $balance;
$currentStart->addDay();

View File

@@ -73,7 +73,7 @@ trait CreateStuff
/** @var AccountRepositoryInterface $repository */
$repository = app(AccountRepositoryInterface::class);
$assetAccount = [
'name' => (string) trans('firefly.cash_wallet', [], $language),
'name' => (string)trans('firefly.cash_wallet', [], $language),
'iban' => null,
'account_type_name' => 'asset',
'virtual_balance' => 0,
@@ -108,7 +108,7 @@ trait CreateStuff
Log::alert('NO OAuth keys were found. They have been created.');
file_put_contents($publicKey, (string) $key->getPublicKey());
file_put_contents($publicKey, (string)$key->getPublicKey());
file_put_contents($privateKey, $key->toString('PKCS1'));
}
@@ -120,7 +120,7 @@ trait CreateStuff
/** @var AccountRepositoryInterface $repository */
$repository = app(AccountRepositoryInterface::class);
$savingsAccount = [
'name' => (string) trans('firefly.new_savings_account', ['bank_name' => $request->get('bank_name')], $language),
'name' => (string)trans('firefly.new_savings_account', ['bank_name' => $request->get('bank_name')], $language),
'iban' => null,
'account_type_name' => 'asset',
'account_type_id' => null,

View File

@@ -63,32 +63,6 @@ trait CronRunner
];
}
protected function webhookCronJob(bool $force, Carbon $date): array
{
/** @var WebhookCronjob $webhook */
$webhook = app(WebhookCronjob::class);
$webhook->setForce($force);
$webhook->setDate($date);
try {
$webhook->fire();
} catch (FireflyException $e) {
return [
'job_fired' => false,
'job_succeeded' => false,
'job_errored' => true,
'message' => $e->getMessage(),
];
}
return [
'job_fired' => $webhook->jobFired,
'job_succeeded' => $webhook->jobSucceeded,
'job_errored' => $webhook->jobErrored,
'message' => $webhook->message,
];
}
protected function exchangeRatesCronJob(bool $force, Carbon $date): array
{
/** @var ExchangeRatesCronjob $exchangeRates */
@@ -166,4 +140,30 @@ trait CronRunner
'message' => $recurring->message,
];
}
protected function webhookCronJob(bool $force, Carbon $date): array
{
/** @var WebhookCronjob $webhook */
$webhook = app(WebhookCronjob::class);
$webhook->setForce($force);
$webhook->setDate($date);
try {
$webhook->fire();
} catch (FireflyException $e) {
return [
'job_fired' => false,
'job_succeeded' => false,
'job_errored' => true,
'message' => $e->getMessage(),
];
}
return [
'job_fired' => $webhook->jobFired,
'job_succeeded' => $webhook->jobSucceeded,
'job_errored' => $webhook->jobErrored,
'message' => $webhook->message,
];
}
}

View File

@@ -40,13 +40,13 @@ trait DateCalculation
*/
public function activeDaysLeft(Carbon $start, Carbon $end): int
{
$difference = (int) ($start->diffInDays($end, true) + 1);
$difference = (int)($start->diffInDays($end, true) + 1);
$today = today(config('app.timezone'))->startOfDay();
if ($start->lte($today) && $end->gte($today)) {
$difference = $today->diffInDays($end) + 1;
}
return (int) (0 === $difference ? 1 : $difference);
return (int)(0 === $difference ? 1 : $difference);
}
/**
@@ -63,7 +63,7 @@ trait DateCalculation
$difference = $start->diffInDays($today, true) + 1;
}
return (int) $difference;
return (int)$difference;
}
protected function calculateStep(Carbon $start, Carbon $end): string

View File

@@ -48,7 +48,7 @@ trait GetConfigurationData
E_COMPILE_ERROR | E_RECOVERABLE_ERROR | E_ERROR | E_CORE_ERROR => 'E_COMPILE_ERROR|E_RECOVERABLE_ERROR|E_ERROR|E_CORE_ERROR',
];
return $array[$value] ?? (string) $value;
return $array[$value] ?? (string)$value;
}
/**
@@ -64,7 +64,7 @@ trait GetConfigurationData
$currentStep = $options;
// get the text:
$currentStep['intro'] = (string) trans('intro.'.$route.'_'.$key);
$currentStep['intro'] = (string)trans('intro.'.$route.'_'.$key);
// save in array:
$steps[] = $currentStep;
@@ -133,41 +133,41 @@ trait GetConfigurationData
$todayEnd = app('navigation')->endOfPeriod($todayStart, $viewRange);
if ($todayStart->ne($start) || $todayEnd->ne($end)) {
$ranges[ucfirst((string) trans('firefly.today'))] = [$todayStart, $todayEnd];
$ranges[ucfirst((string)trans('firefly.today'))] = [$todayStart, $todayEnd];
}
// last seven days:
$seven = today(config('app.timezone'))->subDays(7);
$index = (string) trans('firefly.last_seven_days');
$index = (string)trans('firefly.last_seven_days');
$ranges[$index] = [$seven, new Carbon()];
// last 30 days:
$thirty = today(config('app.timezone'))->subDays(30);
$index = (string) trans('firefly.last_thirty_days');
$index = (string)trans('firefly.last_thirty_days');
$ranges[$index] = [$thirty, new Carbon()];
// month to date:
$monthBegin = today(config('app.timezone'))->startOfMonth();
$index = (string) trans('firefly.month_to_date');
$index = (string)trans('firefly.month_to_date');
$ranges[$index] = [$monthBegin, new Carbon()];
// year to date:
$yearBegin = today(config('app.timezone'))->startOfYear();
$index = (string) trans('firefly.year_to_date');
$index = (string)trans('firefly.year_to_date');
$ranges[$index] = [$yearBegin, new Carbon()];
// everything
$index = (string) trans('firefly.everything');
$index = (string)trans('firefly.everything');
$ranges[$index] = [$first, new Carbon()];
return [
'title' => $title,
'configuration' => [
'apply' => (string) trans('firefly.apply'),
'cancel' => (string) trans('firefly.cancel'),
'from' => (string) trans('firefly.from'),
'to' => (string) trans('firefly.to'),
'customRange' => (string) trans('firefly.customRange'),
'apply' => (string)trans('firefly.apply'),
'cancel' => (string)trans('firefly.cancel'),
'from' => (string)trans('firefly.from'),
'to' => (string)trans('firefly.to'),
'customRange' => (string)trans('firefly.customRange'),
'start' => $start->format('Y-m-d'),
'end' => $end->format('Y-m-d'),
'ranges' => $ranges,
@@ -192,7 +192,7 @@ trait GetConfigurationData
$currentStep = $options;
// get the text:
$currentStep['intro'] = (string) trans('intro.'.$route.'_'.$specificPage.'_'.$key);
$currentStep['intro'] = (string)trans('intro.'.$route.'_'.$specificPage.'_'.$key);
// save in array:
$steps[] = $currentStep;
@@ -207,7 +207,7 @@ trait GetConfigurationData
protected function verifyRecurringCronJob(): void
{
$config = FireflyConfig::get('last_rt_job', 0);
$lastTime = (int) $config?->data;
$lastTime = (int)$config?->data;
$now = Carbon::now()->getTimestamp();
app('log')->debug(sprintf('verifyRecurringCronJob: last time is %d ("%s"), now is %d', $lastTime, $config?->data, $now));
if (0 === $lastTime) {

View File

@@ -87,9 +87,9 @@ trait ModelInformation
/** @var AccountType $mortgage */
$mortgage = $repository->getAccountTypeByType(AccountTypeEnum::MORTGAGE->value);
$liabilityTypes = [
$debt->id => (string) trans(sprintf('firefly.account_type_%s', AccountTypeEnum::DEBT->value)),
$loan->id => (string) trans(sprintf('firefly.account_type_%s', AccountTypeEnum::LOAN->value)),
$mortgage->id => (string) trans(sprintf('firefly.account_type_%s', AccountTypeEnum::MORTGAGE->value)),
$debt->id => (string)trans(sprintf('firefly.account_type_%s', AccountTypeEnum::DEBT->value)),
$loan->id => (string)trans(sprintf('firefly.account_type_%s', AccountTypeEnum::LOAN->value)),
$mortgage->id => (string)trans(sprintf('firefly.account_type_%s', AccountTypeEnum::MORTGAGE->value)),
];
asort($liabilityTypes);
@@ -100,7 +100,7 @@ trait ModelInformation
{
$roles = [];
foreach (config('firefly.accountRoles') as $role) {
$roles[$role] = (string) trans(sprintf('firefly.account_role_%s', $role));
$roles[$role] = (string)trans(sprintf('firefly.account_role_%s', $role));
}
return $roles;
@@ -118,7 +118,7 @@ trait ModelInformation
$triggers = [];
foreach ($operators as $key => $operator) {
if ('user_action' !== $key && false === $operator['alias']) {
$triggers[$key] = (string) trans(sprintf('firefly.rule_trigger_%s_choice', $key));
$triggers[$key] = (string)trans(sprintf('firefly.rule_trigger_%s_choice', $key));
}
}
asort($triggers);
@@ -169,7 +169,7 @@ trait ModelInformation
$triggers = [];
foreach ($operators as $key => $operator) {
if ('user_action' !== $key && false === $operator['alias']) {
$triggers[$key] = (string) trans(sprintf('firefly.rule_trigger_%s_choice', $key));
$triggers[$key] = (string)trans(sprintf('firefly.rule_trigger_%s_choice', $key));
}
}
asort($triggers);

View File

@@ -30,12 +30,15 @@ use FireflyIII\Exceptions\FireflyException;
use FireflyIII\Helpers\Collector\GroupCollectorInterface;
use FireflyIII\Models\Account;
use FireflyIII\Models\Category;
use FireflyIII\Models\PeriodStatistic;
use FireflyIII\Models\Tag;
use FireflyIII\Repositories\Account\AccountRepositoryInterface;
use FireflyIII\Repositories\Journal\JournalRepositoryInterface;
use FireflyIII\Repositories\PeriodStatistic\PeriodStatisticRepositoryInterface;
use FireflyIII\Support\CacheProperties;
use FireflyIII\Support\Debug\Timer;
use FireflyIII\Support\Facades\Amount;
use FireflyIII\Support\Facades\Navigation;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Log;
/**
@@ -67,8 +70,11 @@ use Illuminate\Support\Facades\Log;
*/
trait PeriodOverview
{
protected AccountRepositoryInterface $accountRepository;
protected JournalRepositoryInterface $journalRepos;
protected AccountRepositoryInterface $accountRepository;
protected JournalRepositoryInterface $journalRepos;
protected PeriodStatisticRepositoryInterface $periodStatisticRepo;
private Collection $statistics; // temp data holder
private array $transactions; // temp data holder
/**
* This method returns "period entries", so nov-2015, dec-2015, etc. (this depends on the users session range)
@@ -79,163 +85,50 @@ trait PeriodOverview
*/
protected function getAccountPeriodOverview(Account $account, Carbon $start, Carbon $end): array
{
Log::debug('Now in getAccountPeriodOverview()');
$timer = Timer::getInstance();
$timer->start('account-period-total');
$this->accountRepository = app(AccountRepositoryInterface::class);
$range = Navigation::getViewRange(true);
[$start, $end] = $end < $start ? [$end, $start] : [$start, $end];
// properties for cache
$cache = new CacheProperties();
$cache->addProperty($start);
$cache->addProperty($end);
$cache->addProperty('account-show-period-entries');
$cache->addProperty($account->id);
if ($cache->has()) {
Log::debug('Return CACHED in getAccountPeriodOverview()');
return $cache->get();
}
Log::debug(sprintf('Now in getAccountPeriodOverview(#%d, %s %s)', $account->id, $start->format('Y-m-d H:i:s.u'), $end->format('Y-m-d H:i:s.u')));
$this->accountRepository = app(AccountRepositoryInterface::class);
$this->periodStatisticRepo = app(PeriodStatisticRepositoryInterface::class);
$range = Navigation::getViewRange(true);
[$start, $end] = $end < $start ? [$end, $start] : [$start, $end];
/** @var array $dates */
$dates = Navigation::blockPeriods($start, $end, $range);
$entries = [];
$dates = Navigation::blockPeriods($start, $end, $range);
[$start, $end] = $this->getPeriodFromBlocks($dates, $start, $end);
$this->statistics = $this->periodStatisticRepo->allInRangeForModel($account, $start, $end);
// run a custom query because doing this with the collector is MEGA slow.
$timer->start('account-period-collect');
$transactions = $this->accountRepository->periodCollection($account, $start, $end);
$timer->stop('account-period-collect');
// loop dates
// TODO needs to be re-arranged:
// get all period stats for entire range.
// loop blocks, an loop the types, and select the missing ones.
// create new ones, or use collected.
$entries = [];
Log::debug(sprintf('Count of loops: %d', count($dates)));
$loops = 0;
// stop after 10 loops for memory reasons.
$timer->start('account-period-loop');
foreach ($dates as $currentDate) {
$title = Navigation::periodShow($currentDate['start'], $currentDate['period']);
[$transactions, $spent] = $this->filterTransactionsByType(TransactionTypeEnum::WITHDRAWAL, $transactions, $currentDate['start'], $currentDate['end']);
[$transactions, $earned] = $this->filterTransactionsByType(TransactionTypeEnum::DEPOSIT, $transactions, $currentDate['start'], $currentDate['end']);
[$transactions, $transferredAway] = $this->filterTransfers('away', $transactions, $currentDate['start'], $currentDate['end']);
[$transactions, $transferredIn] = $this->filterTransfers('in', $transactions, $currentDate['start'], $currentDate['end']);
$entries[]
= [
'title' => $title,
'route' => route('accounts.show', [$account->id, $currentDate['start']->format('Y-m-d'), $currentDate['end']->format('Y-m-d')]),
'total_transactions' => count($spent) + count($earned) + count($transferredAway) + count($transferredIn),
'spent' => $this->groupByCurrency($spent),
'earned' => $this->groupByCurrency($earned),
'transferred_away' => $this->groupByCurrency($transferredAway),
'transferred_in' => $this->groupByCurrency($transferredIn),
];
++$loops;
$entries[] = $this->getSingleAccountPeriod($account, $currentDate['period'], $currentDate['start'], $currentDate['end']);
}
$timer->stop('account-period-loop');
$cache->store($entries);
$timer->stop('account-period-total');
Log::debug('End of getAccountPeriodOverview()');
return $entries;
}
private function filterTransactionsByType(TransactionTypeEnum $type, array $transactions, Carbon $start, Carbon $end): array
private function getPeriodFromBlocks(array $dates, Carbon $start, Carbon $end): array
{
$result = [];
$filtered = [];
/**
* @var int $index
* @var array $item
*/
foreach ($transactions as $index => $item) {
$date = Carbon::parse($item['date']);
$fits = $item['type'] === $type->value && $date >= $start && $date <= $end;
if ($fits) {
$result[] = $item;
unset($transactions[$index]);
Log::debug('Filter generated periods to select the oldest and newest date.');
foreach ($dates as $row) {
$currentStart = clone $row['start'];
$currentEnd = clone $row['end'];
if ($currentStart->lt($start)) {
Log::debug(sprintf('New start: was %s, now %s', $start->format('Y-m-d'), $currentStart->format('Y-m-d')));
$start = $currentStart;
}
if (!$fits) {
$filtered[] = $item;
if ($currentEnd->gt($end)) {
Log::debug(sprintf('New end: was %s, now %s', $end->format('Y-m-d'), $currentEnd->format('Y-m-d')));
$end = $currentEnd;
}
}
return [$filtered, $result];
}
private function filterTransfers(string $direction, array $transactions, Carbon $start, Carbon $end): array
{
$result = [];
$filtered = [];
/**
* @var int $index
* @var array $item
*/
foreach ($transactions as $index => $item) {
$date = Carbon::parse($item['date']);
if ($date >= $start && $date <= $end) {
if ('away' === $direction && -1 === bccomp((string)$item['amount'], '0')) {
$result[] = $item;
continue;
}
if ('in' === $direction && 1 === bccomp((string)$item['amount'], '0')) {
$result[] = $item;
continue;
}
}
$filtered[] = $item;
}
return [$filtered, $result];
}
private function groupByCurrency(array $journals): array
{
$return = [];
/** @var array $journal */
foreach ($journals as $journal) {
$currencyId = (int)$journal['currency_id'];
$currencyCode = $journal['currency_code'];
$currencyName = $journal['currency_name'];
$currencySymbol = $journal['currency_symbol'];
$currencyDecimalPlaces = $journal['currency_decimal_places'];
$foreignCurrencyId = $journal['foreign_currency_id'];
$amount = $journal['amount'] ?? '0';
if ($this->convertToPrimary && $currencyId !== $this->primaryCurrency->id && $foreignCurrencyId !== $this->primaryCurrency->id) {
$amount = $journal['pc_amount'] ?? '0';
$currencyId = $this->primaryCurrency->id;
$currencyCode = $this->primaryCurrency->code;
$currencyName = $this->primaryCurrency->name;
$currencySymbol = $this->primaryCurrency->symbol;
$currencyDecimalPlaces = $this->primaryCurrency->decimal_places;
}
if ($this->convertToPrimary && $currencyId !== $this->primaryCurrency->id && $foreignCurrencyId === $this->primaryCurrency->id) {
$currencyId = (int)$foreignCurrencyId;
$currencyCode = $journal['foreign_currency_code'];
$currencyName = $journal['foreign_currency_name'];
$currencySymbol = $journal['foreign_currency_symbol'];
$currencyDecimalPlaces = $journal['foreign_currency_decimal_places'];
$amount = $journal['foreign_amount'] ?? '0';
}
$return[$currencyId] ??= [
'amount' => '0',
'count' => 0,
'currency_id' => $currencyId,
'currency_name' => $currencyName,
'currency_code' => $currencyCode,
'currency_symbol' => $currencySymbol,
'currency_decimal_places' => $currencyDecimalPlaces,
];
$return[$currencyId]['amount'] = bcadd($return[$currencyId]['amount'], $amount);
++$return[$currencyId]['count'];
}
return $return;
return [$start, $end];
}
/**
@@ -311,23 +204,6 @@ trait PeriodOverview
return $entries;
}
/**
* Filter a list of journals by a set of dates, and then group them by currency.
*/
private function filterJournalsByDate(array $array, Carbon $start, Carbon $end): array
{
$result = [];
/** @var array $journal */
foreach ($array as $journal) {
if ($journal['date'] <= $end && $journal['date'] >= $start) {
$result[] = $journal;
}
}
return $result;
}
/**
* Same as above, but for lists that involve transactions without a budget.
*
@@ -389,15 +265,15 @@ trait PeriodOverview
*/
protected function getNoCategoryPeriodOverview(Carbon $theDate): array
{
app('log')->debug(sprintf('Now in getNoCategoryPeriodOverview(%s)', $theDate->format('Y-m-d')));
Log::debug(sprintf('Now in getNoCategoryPeriodOverview(%s)', $theDate->format('Y-m-d')));
$range = Navigation::getViewRange(true);
$first = $this->journalRepos->firstNull();
$start = null === $first ? new Carbon() : $first->date;
$end = clone $theDate;
$end = Navigation::endOfPeriod($end, $range);
app('log')->debug(sprintf('Start for getNoCategoryPeriodOverview() is %s', $start->format('Y-m-d')));
app('log')->debug(sprintf('End for getNoCategoryPeriodOverview() is %s', $end->format('Y-m-d')));
Log::debug(sprintf('Start for getNoCategoryPeriodOverview() is %s', $start->format('Y-m-d')));
Log::debug(sprintf('End for getNoCategoryPeriodOverview() is %s', $end->format('Y-m-d')));
// properties for cache
$dates = Navigation::blockPeriods($start, $end, $range);
@@ -443,11 +319,119 @@ trait PeriodOverview
'transferred' => $this->groupByCurrency($transferred),
];
}
app('log')->debug('End of loops');
Log::debug('End of loops');
return $entries;
}
protected function getSingleAccountPeriod(Account $account, string $period, Carbon $start, Carbon $end): array
{
Log::debug(sprintf('Now in getSingleAccountPeriod(#%d, %s %s)', $account->id, $start->format('Y-m-d'), $end->format('Y-m-d')));
$types = ['spent', 'earned', 'transferred_in', 'transferred_away'];
$return = [
'title' => Navigation::periodShow($start, $period),
'route' => route('accounts.show', [$account->id, $start->format('Y-m-d'), $end->format('Y-m-d')]),
'total_transactions' => 0,
];
$this->transactions = [];
foreach ($types as $type) {
$set = $this->getSingleAccountPeriodByType($account, $start, $end, $type);
$return['total_transactions'] += $set['count'];
unset($set['count']);
$return[$type] = $set;
}
return $return;
}
protected function filterStatistics(Carbon $start, Carbon $end, string $type): Collection
{
return $this->statistics->filter(
function (PeriodStatistic $statistic) use ($start, $end, $type) {
if (
!$statistic->end->equalTo($end)
&& $statistic->end->format('Y-m-d H:i:s') === $end->format('Y-m-d H:i:s')
) {
echo sprintf('End: "%s" vs "%s": %s', $statistic->end->toW3cString(), $end->toW3cString(), var_export($statistic->end->eq($end), true));
var_dump($statistic->end);
var_dump($end);
exit;
}
return $statistic->start->eq($start) && $statistic->end->eq($end) && $statistic->type === $type;
}
);
}
protected function getSingleAccountPeriodByType(Account $account, Carbon $start, Carbon $end, string $type): array
{
Log::debug(sprintf('Now in getSingleAccountPeriodByType(#%d, %s %s, %s)', $account->id, $start->format('Y-m-d'), $end->format('Y-m-d'), $type));
$statistics = $this->filterStatistics($start, $end, $type);
// nothing found, regenerate them.
if (0 === $statistics->count()) {
Log::debug(sprintf('Found nothing in this period for type "%s"', $type));
if (0 === count($this->transactions)) {
$this->transactions = $this->accountRepository->periodCollection($account, $start, $end);
}
switch ($type) {
default:
throw new FireflyException(sprintf('Cannot deal with account period type %s', $type));
case 'spent':
$result = $this->filterTransactionsByType(TransactionTypeEnum::WITHDRAWAL, $start, $end);
break;
case 'earned':
$result = $this->filterTransactionsByType(TransactionTypeEnum::DEPOSIT, $start, $end);
break;
case 'transferred_in':
$result = $this->filterTransfers('in', $start, $end);
break;
case 'transferred_away':
$result = $this->filterTransfers('away', $start, $end);
break;
}
// each result must be grouped by currency, then saved as period statistic.
Log::debug(sprintf('Going to group %d found journal(s)', count($result)));
$grouped = $this->groupByCurrency($result);
$this->saveGroupedAsStatistics($account, $start, $end, $type, $grouped);
return $grouped;
}
$grouped = [
'count' => 0,
];
/** @var PeriodStatistic $statistic */
foreach ($statistics as $statistic) {
$id = (int)$statistic->transaction_currency_id;
$currency = Amount::getTransactionCurrencyById($id);
$grouped[$id] = [
'amount' => (string)$statistic->amount,
'count' => (int)$statistic->count,
'currency_id' => $currency->id,
'currency_name' => $currency->name,
'currency_code' => $currency->code,
'currency_symbol' => $currency->symbol,
'currency_decimal_places' => $currency->decimal_places,
];
$grouped['count'] += (int)$statistic->count;
}
return $grouped;
}
/**
* This shows a period overview for a tag. It goes back in time and lists all relevant transactions and sums.
*
@@ -524,27 +508,6 @@ trait PeriodOverview
return $entries;
}
private function filterJournalsByTag(array $set, Tag $tag): array
{
$return = [];
foreach ($set as $entry) {
$found = false;
/** @var array $localTag */
foreach ($entry['tags'] as $localTag) {
if ($localTag['id'] === $tag->id) {
$found = true;
}
}
if (false === $found) {
continue;
}
$return[] = $entry;
}
return $return;
}
/**
* @throws FireflyException
*/
@@ -593,19 +556,89 @@ trait PeriodOverview
}
$entries[]
= [
'title' => $title,
'route' => route('transactions.index', [$transactionType, $currentDate['start']->format('Y-m-d'), $currentDate['end']->format('Y-m-d')]),
'total_transactions' => count($spent) + count($earned) + count($transferred),
'spent' => $this->groupByCurrency($spent),
'earned' => $this->groupByCurrency($earned),
'transferred' => $this->groupByCurrency($transferred),
];
'title' => $title,
'route' => route('transactions.index', [$transactionType, $currentDate['start']->format('Y-m-d'), $currentDate['end']->format('Y-m-d')]),
'total_transactions' => count($spent) + count($earned) + count($transferred),
'spent' => $this->groupByCurrency($spent),
'earned' => $this->groupByCurrency($earned),
'transferred' => $this->groupByCurrency($transferred),
];
++$loops;
}
return $entries;
}
protected function saveGroupedAsStatistics(Account $account, Carbon $start, Carbon $end, string $type, array $array): void
{
unset($array['count']);
Log::debug(sprintf('saveGroupedAsStatistics(#%d, %s, %s, "%s", array(%d))', $account->id, $start->format('Y-m-d'), $end->format('Y-m-d'), $type, count($array)));
foreach ($array as $entry) {
$this->periodStatisticRepo->saveStatistic($account, $entry['currency_id'], $start, $end, $type, $entry['count'], $entry['amount']);
}
if (0 === count($array)) {
Log::debug('Save empty statistic.');
$this->periodStatisticRepo->saveStatistic($account, $this->primaryCurrency->id, $start, $end, $type, 0, '0');
}
}
/**
* Filter a list of journals by a set of dates, and then group them by currency.
*/
private function filterJournalsByDate(array $array, Carbon $start, Carbon $end): array
{
$result = [];
/** @var array $journal */
foreach ($array as $journal) {
if ($journal['date'] <= $end && $journal['date'] >= $start) {
$result[] = $journal;
}
}
return $result;
}
private function filterJournalsByTag(array $set, Tag $tag): array
{
$return = [];
foreach ($set as $entry) {
$found = false;
/** @var array $localTag */
foreach ($entry['tags'] as $localTag) {
if ($localTag['id'] === $tag->id) {
$found = true;
}
}
if (false === $found) {
continue;
}
$return[] = $entry;
}
return $return;
}
private function filterTransactionsByType(TransactionTypeEnum $type, Carbon $start, Carbon $end): array
{
$result = [];
/**
* @var int $index
* @var array $item
*/
foreach ($this->transactions as $item) {
$date = Carbon::parse($item['date']);
$fits = $item['type'] === $type->value && $date >= $start && $date <= $end;
if ($fits) {
$result[] = $item;
}
}
return $result;
}
/**
* Return only transactions where $account is the source.
*/
@@ -639,4 +672,89 @@ trait PeriodOverview
return $return;
}
private function filterTransfers(string $direction, Carbon $start, Carbon $end): array
{
$result = [];
/**
* @var int $index
* @var array $item
*/
foreach ($this->transactions as $item) {
$date = Carbon::parse($item['date']);
if ($date >= $start && $date <= $end) {
if ('Transfer' === $item['type'] && 'away' === $direction && -1 === bccomp((string)$item['amount'], '0')) {
$result[] = $item;
continue;
}
if ('Transfer' === $item['type'] && 'in' === $direction && 1 === bccomp((string)$item['amount'], '0')) {
$result[] = $item;
}
}
}
return $result;
}
private function groupByCurrency(array $journals): array
{
Log::debug('groupByCurrency()');
$return = [
'count' => 0,
];
/** @var array $journal */
foreach ($journals as $journal) {
if (!array_key_exists('currency_id', $journal)) {
Log::debug('very strange!');
var_dump($journals);
exit;
}
$currencyId = (int)$journal['currency_id'];
$currencyCode = $journal['currency_code'];
$currencyName = $journal['currency_name'];
$currencySymbol = $journal['currency_symbol'];
$currencyDecimalPlaces = $journal['currency_decimal_places'];
$foreignCurrencyId = $journal['foreign_currency_id'];
$amount = $journal['amount'] ?? '0';
if ($this->convertToPrimary && $currencyId !== $this->primaryCurrency->id && $foreignCurrencyId !== $this->primaryCurrency->id) {
$amount = $journal['pc_amount'] ?? '0';
$currencyId = $this->primaryCurrency->id;
$currencyCode = $this->primaryCurrency->code;
$currencyName = $this->primaryCurrency->name;
$currencySymbol = $this->primaryCurrency->symbol;
$currencyDecimalPlaces = $this->primaryCurrency->decimal_places;
}
if ($this->convertToPrimary && $currencyId !== $this->primaryCurrency->id && $foreignCurrencyId === $this->primaryCurrency->id) {
$currencyId = (int)$foreignCurrencyId;
$currencyCode = $journal['foreign_currency_code'];
$currencyName = $journal['foreign_currency_name'];
$currencySymbol = $journal['foreign_currency_symbol'];
$currencyDecimalPlaces = $journal['foreign_currency_decimal_places'];
$amount = $journal['foreign_amount'] ?? '0';
}
$return[$currencyId] ??= [
'amount' => '0',
'count' => 0,
'currency_id' => $currencyId,
'currency_name' => $currencyName,
'currency_code' => $currencyCode,
'currency_symbol' => $currencySymbol,
'currency_decimal_places' => $currencyDecimalPlaces,
];
$return[$currencyId]['amount'] = bcadd($return[$currencyId]['amount'], $amount);
++$return[$currencyId]['count'];
++$return['count'];
}
return $return;
}
}

View File

@@ -56,10 +56,10 @@ trait RenderPartialViews
/** @var BudgetRepositoryInterface $budgetRepository */
$budgetRepository = app(BudgetRepositoryInterface::class);
$budget = $budgetRepository->find((int) $attributes['budgetId']);
$budget = $budgetRepository->find((int)$attributes['budgetId']);
$accountRepos = app(AccountRepositoryInterface::class);
$account = $accountRepos->find((int) $attributes['accountId']);
$account = $accountRepos->find((int)$attributes['accountId']);
if (null === $budget || null === $account) {
throw new FireflyException('Could not render popup.report.balance-amount because budget or account is null.');
@@ -115,7 +115,7 @@ trait RenderPartialViews
/** @var PopupReportInterface $popupHelper */
$popupHelper = app(PopupReportInterface::class);
$budget = $budgetRepository->find((int) $attributes['budgetId']);
$budget = $budgetRepository->find((int)$attributes['budgetId']);
if (null === $budget) {
// transactions without a budget.
$budget = new Budget();
@@ -146,7 +146,7 @@ trait RenderPartialViews
/** @var CategoryRepositoryInterface $categoryRepository */
$categoryRepository = app(CategoryRepositoryInterface::class);
$category = $categoryRepository->find((int) $attributes['categoryId']);
$category = $categoryRepository->find((int)$attributes['categoryId']);
$journals = $popupHelper->byCategory($category, $attributes);
try {
@@ -239,7 +239,7 @@ trait RenderPartialViews
/** @var PopupReportInterface $popupHelper */
$popupHelper = app(PopupReportInterface::class);
$account = $accountRepository->find((int) $attributes['accountId']);
$account = $accountRepository->find((int)$attributes['accountId']);
if (null === $account) {
return 'This is an unknown account. Apologies.';
@@ -310,7 +310,7 @@ trait RenderPartialViews
$triggers = [];
foreach ($operators as $key => $operator) {
if ('user_action' !== $key && false === $operator['alias']) {
$triggers[$key] = (string) trans(sprintf('firefly.rule_trigger_%s_choice', $key));
$triggers[$key] = (string)trans(sprintf('firefly.rule_trigger_%s_choice', $key));
}
}
asort($triggers);
@@ -325,7 +325,7 @@ trait RenderPartialViews
$count = ($index + 1);
try {
$rootOperator = OperatorQuerySearch::getRootOperator((string) $entry->trigger_type);
$rootOperator = OperatorQuerySearch::getRootOperator((string)$entry->trigger_type);
if (str_starts_with($rootOperator, '-')) {
$rootOperator = substr($rootOperator, 1);
}
@@ -335,7 +335,7 @@ trait RenderPartialViews
'oldTrigger' => $rootOperator,
'oldValue' => $entry->trigger_value,
'oldChecked' => $entry->stop_processing,
'oldProhibited' => str_starts_with((string) $entry->trigger_type, '-'),
'oldProhibited' => str_starts_with((string)$entry->trigger_type, '-'),
'count' => $count,
'triggers' => $triggers,
]
@@ -366,7 +366,7 @@ trait RenderPartialViews
/** @var PopupReportInterface $popupHelper */
$popupHelper = app(PopupReportInterface::class);
$account = $accountRepository->find((int) $attributes['accountId']);
$account = $accountRepository->find((int)$attributes['accountId']);
if (null === $account) {
return 'This is an unknown category. Apologies.';

View File

@@ -32,9 +32,9 @@ use FireflyIII\Support\Binder\AccountList;
use FireflyIII\User;
use Illuminate\Contracts\Validation\Validator as ValidatorContract;
use Illuminate\Routing\Route;
use Illuminate\Support\Facades\Validator;
use Illuminate\Support\Facades\Route as RouteFacade;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Route as RouteFacade;
use Illuminate\Support\Facades\Validator;
use function Safe\parse_url;
@@ -54,6 +54,22 @@ trait RequestInformation
return $parts['host'] ?? '';
}
final protected function getPageName(): string // get request info
{
return str_replace('.', '_', RouteFacade::currentRouteName());
}
/**
* Get the specific name of a page for intro.
*/
final protected function getSpecificPageName(): string // get request info
{
/** @var null|string $param */
$param = RouteFacade::current()->parameter('objectType');
return null === $param ? '' : sprintf('_%s', $param);
}
/**
* Get a list of triggers.
*/
@@ -67,7 +83,7 @@ trait RequestInformation
'type' => $triggerInfo['type'] ?? '',
'value' => $triggerInfo['value'] ?? '',
'prohibited' => $triggerInfo['prohibited'] ?? false,
'stop_processing' => 1 === (int) ($triggerInfo['stop_processing'] ?? '0'),
'stop_processing' => 1 === (int)($triggerInfo['stop_processing'] ?? '0'),
];
$current = RuleFormRequest::replaceAmountTrigger($current);
$triggers[] = $current;
@@ -103,22 +119,6 @@ trait RequestInformation
return $shownDemo;
}
final protected function getPageName(): string // get request info
{
return str_replace('.', '_', RouteFacade::currentRouteName());
}
/**
* Get the specific name of a page for intro.
*/
final protected function getSpecificPageName(): string // get request info
{
/** @var null|string $param */
$param = RouteFacade::current()->parameter('objectType');
return null === $param ? '' : sprintf('_%s', $param);
}
/**
* Check if date is outside session range.
*/
@@ -172,11 +172,11 @@ trait RequestInformation
final protected function validatePassword(User $user, string $current, string $new): bool // get request info
{
if (!Hash::check($current, $user->password)) {
throw new ValidationException((string) trans('firefly.invalid_current_password'));
throw new ValidationException((string)trans('firefly.invalid_current_password'));
}
if ($current === $new) {
throw new ValidationException((string) trans('firefly.should_change'));
throw new ValidationException((string)trans('firefly.should_change'));
}
return true;

View File

@@ -51,7 +51,7 @@ trait RuleManagement
[
'oldAction' => $oldAction['type'],
'oldValue' => $oldAction['value'] ?? '',
'oldChecked' => 1 === (int) ($oldAction['stop_processing'] ?? '0'),
'oldChecked' => 1 === (int)($oldAction['stop_processing'] ?? '0'),
'count' => $index + 1,
]
)->render();
@@ -78,7 +78,7 @@ trait RuleManagement
$triggers = [];
foreach ($operators as $key => $operator) {
if ('user_action' !== $key && false === $operator['alias']) {
$triggers[$key] = (string) trans(sprintf('firefly.rule_trigger_%s_choice', $key));
$triggers[$key] = (string)trans(sprintf('firefly.rule_trigger_%s_choice', $key));
}
}
asort($triggers);
@@ -94,8 +94,8 @@ trait RuleManagement
[
'oldTrigger' => OperatorQuerySearch::getRootOperator($oldTrigger['type']),
'oldValue' => $oldTrigger['value'] ?? '',
'oldChecked' => 1 === (int) ($oldTrigger['stop_processing'] ?? '0'),
'oldProhibited' => 1 === (int) ($oldTrigger['prohibited'] ?? '0'),
'oldChecked' => 1 === (int)($oldTrigger['stop_processing'] ?? '0'),
'oldProhibited' => 1 === (int)($oldTrigger['prohibited'] ?? '0'),
'count' => $index + 1,
'triggers' => $triggers,
]
@@ -124,7 +124,7 @@ trait RuleManagement
$triggers = [];
foreach ($operators as $key => $operator) {
if ('user_action' !== $key && false === $operator['alias']) {
$triggers[$key] = (string) trans(sprintf('firefly.rule_trigger_%s_choice', $key));
$triggers[$key] = (string)trans(sprintf('firefly.rule_trigger_%s_choice', $key));
}
}
asort($triggers);
@@ -132,7 +132,7 @@ trait RuleManagement
$index = 0;
foreach ($submittedOperators as $operator) {
$rootOperator = OperatorQuerySearch::getRootOperator($operator['type']);
$needsContext = (bool) config(sprintf('search.operators.%s.needs_context', $rootOperator));
$needsContext = (bool)config(sprintf('search.operators.%s.needs_context', $rootOperator));
try {
$renderedEntries[] = view(
@@ -164,8 +164,8 @@ trait RuleManagement
$repository = app(RuleGroupRepositoryInterface::class);
if (0 === $repository->count()) {
$data = [
'title' => (string) trans('firefly.default_rule_group_name'),
'description' => (string) trans('firefly.default_rule_group_description'),
'title' => (string)trans('firefly.default_rule_group_name'),
'description' => (string)trans('firefly.default_rule_group_description'),
'active' => true,
];

View File

@@ -49,7 +49,7 @@ trait UserNavigation
final protected function getPreviousUrl(string $identifier): string
{
app('log')->debug(sprintf('Trying to retrieve URL stored under "%s"', $identifier));
$url = (string) session($identifier);
$url = (string)session($identifier);
app('log')->debug(sprintf('The URL is %s', $url));
return app('steam')->getSafeUrl($url, route('index'));

View File

@@ -53,29 +53,29 @@ use Override;
*/
class AccountEnrichment implements EnrichmentInterface
{
private array $ids = [];
private array $accountTypeIds = [];
private array $accountTypes = [];
private Collection $collection;
private array $currencies = [];
private array $locations = [];
private array $meta = [];
private readonly TransactionCurrency $primaryCurrency;
private array $notes = [];
private array $openingBalances = [];
private User $user;
private UserGroup $userGroup;
private array $lastActivities = [];
private ?Carbon $date = null;
private ?Carbon $start = null;
private ?Carbon $end = null;
private array $accountTypeIds = [];
private array $accountTypes = [];
private array $balances = [];
private Collection $collection;
private readonly bool $convertToPrimary;
private array $balances = [];
private array $startBalances = [];
private array $endBalances = [];
private array $objectGroups = [];
private array $mappedObjects = [];
private array $sort = [];
private array $currencies = [];
private ?Carbon $date = null;
private ?Carbon $end = null;
private array $endBalances = [];
private array $ids = [];
private array $lastActivities = [];
private array $locations = [];
private array $mappedObjects = [];
private array $meta = [];
private array $notes = [];
private array $objectGroups = [];
private array $openingBalances = [];
private readonly TransactionCurrency $primaryCurrency;
private array $sort = [];
private ?Carbon $start = null;
private array $startBalances = [];
private User $user;
private UserGroup $userGroup;
/**
* TODO The account enricher must do conversion from and to the primary currency.
@@ -86,16 +86,6 @@ class AccountEnrichment implements EnrichmentInterface
$this->convertToPrimary = Amount::convertToPrimary();
}
#[Override]
public function enrichSingle(array|Model $model): Account|array
{
Log::debug(__METHOD__);
$collection = new Collection()->push($model);
$collection = $this->enrich($collection);
return $collection->first();
}
#[Override]
/**
* Do the actual enrichment.
@@ -121,114 +111,47 @@ class AccountEnrichment implements EnrichmentInterface
return $this->collection;
}
private function collectIds(): void
#[Override]
public function enrichSingle(array|Model $model): Account|array
{
/** @var Account $account */
foreach ($this->collection as $account) {
$this->ids[] = (int)$account->id;
$this->accountTypeIds[] = (int)$account->account_type_id;
}
$this->ids = array_unique($this->ids);
$this->accountTypeIds = array_unique($this->accountTypeIds);
Log::debug(__METHOD__);
$collection = new Collection()->push($model);
$collection = $this->enrich($collection);
return $collection->first();
}
private function getAccountTypes(): void
public function getDate(): Carbon
{
$types = AccountType::whereIn('id', $this->accountTypeIds)->get();
/** @var AccountType $type */
foreach ($types as $type) {
$this->accountTypes[(int)$type->id] = $type->type;
if (!$this->date instanceof Carbon) {
return now();
}
return $this->date;
}
private function collectMetaData(): void
public function setDate(?Carbon $date): void
{
$set = AccountMeta::whereIn('name', ['is_multi_currency', 'include_net_worth', 'currency_id', 'account_role', 'account_number', 'BIC', 'liability_direction', 'interest', 'interest_period', 'current_debt'])
->whereIn('account_id', $this->ids)
->get(['account_meta.id', 'account_meta.account_id', 'account_meta.name', 'account_meta.data'])->toArray()
;
/** @var array $entry */
foreach ($set as $entry) {
$this->meta[(int)$entry['account_id']][$entry['name']] = (string)$entry['data'];
if ('currency_id' === $entry['name']) {
$this->currencies[(int)$entry['data']] = true;
}
}
if (count($this->currencies) > 0) {
$currencies = TransactionCurrency::whereIn('id', array_keys($this->currencies))->get();
foreach ($currencies as $currency) {
$this->currencies[(int)$currency->id] = $currency;
}
}
$this->currencies[0] = $this->primaryCurrency;
foreach ($this->currencies as $id => $currency) {
if (true === $currency) {
throw new FireflyException(sprintf('Currency #%d not found.', $id));
}
if ($date instanceof Carbon) {
$date->endOfDay();
Log::debug(sprintf('Date is now %s', $date->toW3cString()));
}
$this->date = $date;
}
private function collectNotes(): void
public function setEnd(?Carbon $end): void
{
$notes = Note::query()->whereIn('noteable_id', $this->ids)
->whereNotNull('notes.text')
->where('notes.text', '!=', '')
->where('noteable_type', Account::class)->get(['notes.noteable_id', 'notes.text'])->toArray()
;
foreach ($notes as $note) {
$this->notes[(int)$note['noteable_id']] = (string)$note['text'];
}
Log::debug(sprintf('Enrich with %d note(s)', count($this->notes)));
$this->end = $end;
}
private function collectLocations(): void
public function setSort(array $sort): void
{
$locations = Location::query()->whereIn('locatable_id', $this->ids)
->where('locatable_type', Account::class)->get(['locations.locatable_id', 'locations.latitude', 'locations.longitude', 'locations.zoom_level'])->toArray()
;
foreach ($locations as $location) {
$this->locations[(int)$location['locatable_id']]
= [
'latitude' => (float)$location['latitude'],
'longitude' => (float)$location['longitude'],
'zoom_level' => (int)$location['zoom_level'],
];
}
Log::debug(sprintf('Enrich with %d locations(s)', count($this->locations)));
$this->sort = $sort;
}
private function collectOpeningBalances(): void
public function setStart(?Carbon $start): void
{
// use new group collector:
/** @var GroupCollectorInterface $collector */
$collector = app(GroupCollectorInterface::class);
$collector
->setUser($this->user)
->setUserGroup($this->userGroup)
->setAccounts($this->collection)
->withAccountInformation()
->setTypes([TransactionTypeEnum::OPENING_BALANCE->value])
;
$journals = $collector->getExtractedJournals();
foreach ($journals as $journal) {
$this->openingBalances[(int)$journal['source_account_id']]
= [
'amount' => Steam::negative($journal['amount']),
'date' => $journal['date'],
];
$this->openingBalances[(int)$journal['destination_account_id']]
= [
'amount' => Steam::positive($journal['amount']),
'date' => $journal['date'],
];
}
}
public function setUserGroup(UserGroup $userGroup): void
{
$this->userGroup = $userGroup;
$this->start = $start;
}
public function setUser(User $user): void
@@ -237,6 +160,11 @@ class AccountEnrichment implements EnrichmentInterface
$this->userGroup = $user->userGroup;
}
public function setUserGroup(UserGroup $userGroup): void
{
$this->userGroup = $userGroup;
}
private function appendCollectedData(): void
{
$this->collection = $this->collection->map(function (Account $item) {
@@ -357,11 +285,6 @@ class AccountEnrichment implements EnrichmentInterface
});
}
private function collectLastActivities(): void
{
$this->lastActivities = Steam::getLastActivities($this->ids);
}
private function collectBalances(): void
{
$this->balances = Steam::accountsBalancesOptimized($this->collection, $this->getDate(), $this->primaryCurrency, $this->convertToPrimary);
@@ -371,6 +294,79 @@ class AccountEnrichment implements EnrichmentInterface
}
}
private function collectIds(): void
{
/** @var Account $account */
foreach ($this->collection as $account) {
$this->ids[] = (int)$account->id;
$this->accountTypeIds[] = (int)$account->account_type_id;
}
$this->ids = array_unique($this->ids);
$this->accountTypeIds = array_unique($this->accountTypeIds);
}
private function collectLastActivities(): void
{
$this->lastActivities = Steam::getLastActivities($this->ids);
}
private function collectLocations(): void
{
$locations = Location::query()->whereIn('locatable_id', $this->ids)
->where('locatable_type', Account::class)->get(['locations.locatable_id', 'locations.latitude', 'locations.longitude', 'locations.zoom_level'])->toArray()
;
foreach ($locations as $location) {
$this->locations[(int)$location['locatable_id']]
= [
'latitude' => (float)$location['latitude'],
'longitude' => (float)$location['longitude'],
'zoom_level' => (int)$location['zoom_level'],
];
}
Log::debug(sprintf('Enrich with %d locations(s)', count($this->locations)));
}
private function collectMetaData(): void
{
$set = AccountMeta::whereIn('name', ['is_multi_currency', 'include_net_worth', 'currency_id', 'account_role', 'account_number', 'BIC', 'liability_direction', 'interest', 'interest_period', 'current_debt'])
->whereIn('account_id', $this->ids)
->get(['account_meta.id', 'account_meta.account_id', 'account_meta.name', 'account_meta.data'])->toArray()
;
/** @var array $entry */
foreach ($set as $entry) {
$this->meta[(int)$entry['account_id']][$entry['name']] = (string)$entry['data'];
if ('currency_id' === $entry['name']) {
$this->currencies[(int)$entry['data']] = true;
}
}
if (count($this->currencies) > 0) {
$currencies = TransactionCurrency::whereIn('id', array_keys($this->currencies))->get();
foreach ($currencies as $currency) {
$this->currencies[(int)$currency->id] = $currency;
}
}
$this->currencies[0] = $this->primaryCurrency;
foreach ($this->currencies as $id => $currency) {
if (true === $currency) {
throw new FireflyException(sprintf('Currency #%d not found.', $id));
}
}
}
private function collectNotes(): void
{
$notes = Note::query()->whereIn('noteable_id', $this->ids)
->whereNotNull('notes.text')
->where('notes.text', '!=', '')
->where('noteable_type', Account::class)->get(['notes.noteable_id', 'notes.text'])->toArray()
;
foreach ($notes as $note) {
$this->notes[(int)$note['noteable_id']] = (string)$note['text'];
}
Log::debug(sprintf('Enrich with %d note(s)', count($this->notes)));
}
private function collectObjectGroups(): void
{
$set = DB::table('object_groupables')
@@ -393,32 +389,41 @@ class AccountEnrichment implements EnrichmentInterface
}
}
public function setDate(?Carbon $date): void
private function collectOpeningBalances(): void
{
if ($date instanceof Carbon) {
$date->endOfDay();
Log::debug(sprintf('Date is now %s', $date->toW3cString()));
// use new group collector:
/** @var GroupCollectorInterface $collector */
$collector = app(GroupCollectorInterface::class);
$collector
->setUser($this->user)
->setUserGroup($this->userGroup)
->setAccounts($this->collection)
->withAccountInformation()
->setTypes([TransactionTypeEnum::OPENING_BALANCE->value])
;
$journals = $collector->getExtractedJournals();
foreach ($journals as $journal) {
$this->openingBalances[(int)$journal['source_account_id']]
= [
'amount' => Steam::negative($journal['amount']),
'date' => $journal['date'],
];
$this->openingBalances[(int)$journal['destination_account_id']]
= [
'amount' => Steam::positive($journal['amount']),
'date' => $journal['date'],
];
}
$this->date = $date;
}
public function getDate(): Carbon
private function getAccountTypes(): void
{
if (!$this->date instanceof Carbon) {
return now();
$types = AccountType::whereIn('id', $this->accountTypeIds)->get();
/** @var AccountType $type */
foreach ($types as $type) {
$this->accountTypes[(int)$type->id] = $type->type;
}
return $this->date;
}
public function setStart(?Carbon $start): void
{
$this->start = $start;
}
public function setEnd(?Carbon $end): void
{
$this->end = $end;
}
private function getBalanceDifference(int $id, TransactionCurrency $currency): ?string
@@ -437,11 +442,6 @@ class AccountEnrichment implements EnrichmentInterface
return bcsub($end, $start);
}
public function setSort(array $sort): void
{
$this->sort = $sort;
}
private function sortData(): void
{
$dbParams = config('firefly.allowed_db_sort_parameters.Account', []);

View File

@@ -40,20 +40,20 @@ use Override;
class AvailableBudgetEnrichment implements EnrichmentInterface
{
private User $user; // @phpstan-ignore-line
private UserGroup $userGroup; // @phpstan-ignore-line
private readonly bool $convertToPrimary;
private array $ids = [];
private array $currencyIds = [];
private Collection $collection; // @phpstan-ignore-line
private readonly bool $convertToPrimary; // @phpstan-ignore-line
private array $currencies = [];
private Collection $collection;
private array $spentInBudgets = [];
private array $spentOutsideBudgets = [];
private array $pcSpentInBudgets = [];
private array $pcSpentOutsideBudgets = [];
private array $currencyIds = [];
private array $ids = [];
private readonly NoBudgetRepositoryInterface $noBudgetRepository;
private readonly OperationsRepositoryInterface $opsRepository;
private array $pcSpentInBudgets = [];
private array $pcSpentOutsideBudgets = [];
private readonly BudgetRepositoryInterface $repository;
private array $spentInBudgets = [];
private array $spentOutsideBudgets = [];
private User $user;
private UserGroup $userGroup;
public function __construct()
{
@@ -104,6 +104,34 @@ class AvailableBudgetEnrichment implements EnrichmentInterface
$this->repository->setUserGroup($userGroup);
}
private function appendCollectedData(): void
{
$this->collection = $this->collection->map(function (AvailableBudget $item) {
$id = (int)$item->id;
$currencyId = $this->currencyIds[$id];
$currency = $this->currencies[$currencyId];
$meta = [
'currency' => $currency,
'spent_in_budgets' => $this->spentInBudgets[$id] ?? [],
'pc_spent_in_budgets' => $this->pcSpentInBudgets[$id] ?? [],
'spent_outside_budgets' => $this->spentOutsideBudgets[$id] ?? [],
'pc_spent_outside_budgets' => $this->pcSpentOutsideBudgets[$id] ?? [],
];
$item->meta = $meta;
return $item;
});
}
private function collectCurrencies(): void
{
$ids = array_unique(array_values($this->currencyIds));
$set = TransactionCurrency::whereIn('id', $ids)->get();
foreach ($set as $currency) {
$this->currencies[(int)$currency->id] = $currency;
}
}
private function collectIds(): void
{
/** @var AvailableBudget $availableBudget */
@@ -138,32 +166,4 @@ class AvailableBudgetEnrichment implements EnrichmentInterface
}
}
}
private function appendCollectedData(): void
{
$this->collection = $this->collection->map(function (AvailableBudget $item) {
$id = (int)$item->id;
$currencyId = $this->currencyIds[$id];
$currency = $this->currencies[$currencyId];
$meta = [
'currency' => $currency,
'spent_in_budgets' => $this->spentInBudgets[$id] ?? [],
'pc_spent_in_budgets' => $this->pcSpentInBudgets[$id] ?? [],
'spent_outside_budgets' => $this->spentOutsideBudgets[$id] ?? [],
'pc_spent_outside_budgets' => $this->pcSpentOutsideBudgets[$id] ?? [],
];
$item->meta = $meta;
return $item;
});
}
private function collectCurrencies(): void
{
$ids = array_unique(array_values($this->currencyIds));
$set = TransactionCurrency::whereIn('id', $ids)->get();
foreach ($set as $currency) {
$this->currencies[(int)$currency->id] = $currency;
}
}
}

View File

@@ -40,19 +40,19 @@ use Illuminate\Support\Facades\Log;
class BudgetEnrichment implements EnrichmentInterface
{
private Collection $collection;
private User $user;
private UserGroup $userGroup;
private array $ids = [];
private array $notes = [];
private array $autoBudgets = [];
private array $currencies = [];
private ?Carbon $start = null;
private ?Carbon $end = null;
private array $spent = [];
private array $pcSpent = [];
private array $objectGroups = [];
private array $mappedObjects = [];
private array $autoBudgets = [];
private Collection $collection;
private array $currencies = [];
private ?Carbon $end = null;
private array $ids = [];
private array $mappedObjects = [];
private array $notes = [];
private array $objectGroups = [];
private array $pcSpent = [];
private array $spent = [];
private ?Carbon $start = null;
private User $user;
private UserGroup $userGroup;
public function __construct() {}
@@ -79,6 +79,16 @@ class BudgetEnrichment implements EnrichmentInterface
return $collection->first();
}
public function setEnd(?Carbon $end): void
{
$this->end = $end;
}
public function setStart(?Carbon $start): void
{
$this->start = $start;
}
public function setUser(User $user): void
{
$this->user = $user;
@@ -90,28 +100,6 @@ class BudgetEnrichment implements EnrichmentInterface
$this->userGroup = $userGroup;
}
private function collectIds(): void
{
/** @var Budget $budget */
foreach ($this->collection as $budget) {
$this->ids[] = (int)$budget->id;
}
$this->ids = array_unique($this->ids);
}
private function collectNotes(): void
{
$notes = Note::query()->whereIn('noteable_id', $this->ids)
->whereNotNull('notes.text')
->where('notes.text', '!=', '')
->where('noteable_type', Budget::class)->get(['notes.noteable_id', 'notes.text'])->toArray()
;
foreach ($notes as $note) {
$this->notes[(int)$note['noteable_id']] = (string)$note['text'];
}
Log::debug(sprintf('Enrich with %d note(s)', count($this->notes)));
}
private function appendCollectedData(): void
{
$this->collection = $this->collection->map(function (Budget $item) {
@@ -130,7 +118,7 @@ class BudgetEnrichment implements EnrichmentInterface
// add object group if available
if (array_key_exists($id, $this->mappedObjects)) {
$key = $this->mappedObjects[$id];
$meta['object_group_id'] = (string) $this->objectGroups[$key]['id'];
$meta['object_group_id'] = (string)$this->objectGroups[$key]['id'];
$meta['object_group_title'] = $this->objectGroups[$key]['title'];
$meta['object_group_order'] = $this->objectGroups[$key]['order'];
}
@@ -177,14 +165,26 @@ class BudgetEnrichment implements EnrichmentInterface
}
}
public function setEnd(?Carbon $end): void
private function collectIds(): void
{
$this->end = $end;
/** @var Budget $budget */
foreach ($this->collection as $budget) {
$this->ids[] = (int)$budget->id;
}
$this->ids = array_unique($this->ids);
}
public function setStart(?Carbon $start): void
private function collectNotes(): void
{
$this->start = $start;
$notes = Note::query()->whereIn('noteable_id', $this->ids)
->whereNotNull('notes.text')
->where('notes.text', '!=', '')
->where('noteable_type', Budget::class)->get(['notes.noteable_id', 'notes.text'])->toArray()
;
foreach ($notes as $note) {
$this->notes[(int)$note['noteable_id']] = (string)$note['text'];
}
Log::debug(sprintf('Enrich with %d note(s)', count($this->notes)));
}
private function collectObjectGroups(): void

View File

@@ -40,19 +40,19 @@ use Illuminate\Support\Facades\Log;
class BudgetLimitEnrichment implements EnrichmentInterface
{
private User $user;
private UserGroup $userGroup; // @phpstan-ignore-line
private Collection $collection;
private array $ids = [];
private array $notes = [];
private Carbon $start;
private bool $convertToPrimary = true; // @phpstan-ignore-line
private array $currencies = [];
private array $currencyIds = [];
private Carbon $end;
private array $expenses = [];
private array $ids = [];
private array $notes = [];
private array $pcExpenses = [];
private array $currencyIds = [];
private array $currencies = [];
private bool $convertToPrimary = true;
private readonly TransactionCurrency $primaryCurrency;
private Carbon $start;
private User $user;
private UserGroup $userGroup;
public function __construct()
{
@@ -93,36 +93,6 @@ class BudgetLimitEnrichment implements EnrichmentInterface
$this->userGroup = $userGroup;
}
private function collectIds(): void
{
$this->start = $this->collection->min('start_date') ?? Carbon::now()->startOfMonth();
$this->end = $this->collection->max('end_date') ?? Carbon::now()->endOfMonth();
/** @var BudgetLimit $limit */
foreach ($this->collection as $limit) {
$id = (int)$limit->id;
$this->ids[] = $id;
if (0 !== (int)$limit->transaction_currency_id) {
$this->currencyIds[$id] = (int)$limit->transaction_currency_id;
}
}
$this->ids = array_unique($this->ids);
$this->currencyIds = array_unique($this->currencyIds);
}
private function collectNotes(): void
{
$notes = Note::query()->whereIn('noteable_id', $this->ids)
->whereNotNull('notes.text')
->where('notes.text', '!=', '')
->where('noteable_type', BudgetLimit::class)->get(['notes.noteable_id', 'notes.text'])->toArray()
;
foreach ($notes as $note) {
$this->notes[(int)$note['noteable_id']] = (string)$note['text'];
}
Log::debug(sprintf('Enrich with %d note(s)', count($this->notes)));
}
private function appendCollectedData(): void
{
$this->collection = $this->collection->map(function (BudgetLimit $item) {
@@ -179,6 +149,44 @@ class BudgetLimitEnrichment implements EnrichmentInterface
}
}
private function collectIds(): void
{
$this->start = $this->collection->min('start_date') ?? Carbon::now()->startOfMonth();
$this->end = $this->collection->max('end_date') ?? Carbon::now()->endOfMonth();
/** @var BudgetLimit $limit */
foreach ($this->collection as $limit) {
$id = (int)$limit->id;
$this->ids[] = $id;
if (0 !== (int)$limit->transaction_currency_id) {
$this->currencyIds[$id] = (int)$limit->transaction_currency_id;
}
}
$this->ids = array_unique($this->ids);
$this->currencyIds = array_unique($this->currencyIds);
}
private function collectNotes(): void
{
$notes = Note::query()->whereIn('noteable_id', $this->ids)
->whereNotNull('notes.text')
->where('notes.text', '!=', '')
->where('noteable_type', BudgetLimit::class)->get(['notes.noteable_id', 'notes.text'])->toArray()
;
foreach ($notes as $note) {
$this->notes[(int)$note['noteable_id']] = (string)$note['text'];
}
Log::debug(sprintf('Enrich with %d note(s)', count($this->notes)));
}
private function filterToBudget(array $expenses, int $budget): array
{
$result = array_filter($expenses, fn (array $item) => (int)$item['budget_id'] === $budget);
Log::debug(sprintf('filterToBudget for budget #%d, from %d to %d items', $budget, count($expenses), count($result)));
return $result;
}
private function stringifyIds(): void
{
$this->expenses = array_map(fn ($first) => array_map(function ($second) {
@@ -193,12 +201,4 @@ class BudgetLimitEnrichment implements EnrichmentInterface
return $second;
}, $first), $this->expenses);
}
private function filterToBudget(array $expenses, int $budget): array
{
$result = array_filter($expenses, fn (array $item) => (int)$item['budget_id'] === $budget);
Log::debug(sprintf('filterToBudget for budget #%d, from %d to %d items', $budget, count($expenses), count($result)));
return $result;
}
}

View File

@@ -38,18 +38,18 @@ use Illuminate\Support\Facades\Log;
class CategoryEnrichment implements EnrichmentInterface
{
private Collection $collection;
private User $user;
private UserGroup $userGroup;
private array $earned = [];
private ?Carbon $end = null;
private array $ids = [];
private array $notes = [];
private ?Carbon $start = null;
private ?Carbon $end = null;
private array $spent = [];
private array $pcSpent = [];
private array $earned = [];
private array $pcEarned = [];
private array $transfers = [];
private array $pcSpent = [];
private array $pcTransfers = [];
private array $spent = [];
private ?Carbon $start = null;
private array $transfers = [];
private User $user;
private UserGroup $userGroup;
public function enrich(Collection $collection): Collection
{
@@ -71,6 +71,16 @@ class CategoryEnrichment implements EnrichmentInterface
return $collection->first();
}
public function setEnd(?Carbon $end): void
{
$this->end = $end;
}
public function setStart(?Carbon $start): void
{
$this->start = $start;
}
public function setUser(User $user): void
{
$this->user = $user;
@@ -82,15 +92,6 @@ class CategoryEnrichment implements EnrichmentInterface
$this->userGroup = $userGroup;
}
private function collectIds(): void
{
/** @var Category $category */
foreach ($this->collection as $category) {
$this->ids[] = (int)$category->id;
}
$this->ids = array_unique($this->ids);
}
private function appendCollectedData(): void
{
$this->collection = $this->collection->map(function (Category $item) {
@@ -110,14 +111,13 @@ class CategoryEnrichment implements EnrichmentInterface
});
}
public function setEnd(?Carbon $end): void
private function collectIds(): void
{
$this->end = $end;
}
public function setStart(?Carbon $start): void
{
$this->start = $start;
/** @var Category $category */
foreach ($this->collection as $category) {
$this->ids[] = (int)$category->id;
}
$this->ids = array_unique($this->ids);
}
private function collectNotes(): void

View File

@@ -43,20 +43,20 @@ use Illuminate\Support\Facades\Log;
class PiggyBankEnrichment implements EnrichmentInterface
{
private User $user; // @phpstan-ignore-line
private UserGroup $userGroup; // @phpstan-ignore-line
private Collection $collection;
private array $ids = [];
private array $currencyIds = [];
private array $currencies = [];
private array $accountIds = [];
private array $accountIds = []; // @phpstan-ignore-line
private array $accounts = []; // @phpstan-ignore-line
private array $amounts = [];
private Collection $collection;
private array $currencies = [];
private array $currencyIds = [];
private array $ids = [];
// private array $accountCurrencies = [];
private array $notes = [];
private array $mappedObjects = [];
private array $mappedObjects = [];
private array $notes = [];
private array $objectGroups = [];
private readonly TransactionCurrency $primaryCurrency;
private array $amounts = [];
private array $accounts = [];
private array $objectGroups = [];
private User $user;
private UserGroup $userGroup;
public function __construct()
{
@@ -97,69 +97,6 @@ class PiggyBankEnrichment implements EnrichmentInterface
$this->userGroup = $userGroup;
}
private function collectIds(): void
{
/** @var PiggyBank $piggy */
foreach ($this->collection as $piggy) {
$id = (int)$piggy->id;
$this->ids[] = $id;
$this->currencyIds[$id] = (int)$piggy->transaction_currency_id;
}
$this->ids = array_unique($this->ids);
// collect currencies.
$currencies = TransactionCurrency::whereIn('id', $this->currencyIds)->get();
foreach ($currencies as $currency) {
$this->currencies[(int)$currency->id] = $currency;
}
// collect accounts
$set = DB::table('account_piggy_bank')->whereIn('piggy_bank_id', $this->ids)->get(['piggy_bank_id', 'account_id', 'current_amount', 'native_current_amount']);
foreach ($set as $item) {
$id = (int)$item->piggy_bank_id;
$accountId = (int)$item->account_id;
$this->amounts[$id] ??= [];
if (!array_key_exists($id, $this->accountIds)) {
$this->accountIds[$id] = (int)$item->account_id;
}
if (!array_key_exists($accountId, $this->amounts[$id])) {
$this->amounts[$id][$accountId] = [
'current_amount' => '0',
'pc_current_amount' => '0',
];
}
$this->amounts[$id][$accountId]['current_amount'] = bcadd($this->amounts[$id][$accountId]['current_amount'], (string) $item->current_amount);
if (null !== $this->amounts[$id][$accountId]['pc_current_amount'] && null !== $item->native_current_amount) {
$this->amounts[$id][$accountId]['pc_current_amount'] = bcadd($this->amounts[$id][$accountId]['pc_current_amount'], $item->native_current_amount);
}
}
// get account currency preference for ALL.
$set = AccountMeta::whereIn('account_id', array_values($this->accountIds))->where('name', 'currency_id')->get();
/** @var AccountMeta $item */
foreach ($set as $item) {
$accountId = (int)$item->account_id;
$currencyId = (int)$item->data;
if (!array_key_exists($currencyId, $this->currencies)) {
$this->currencies[$currencyId] = Amount::getTransactionCurrencyById($currencyId);
}
// $this->accountCurrencies[$accountId] = $this->currencies[$currencyId];
}
// get account info.
$set = Account::whereIn('id', array_values($this->accountIds))->get();
/** @var Account $item */
foreach ($set as $item) {
$id = (int)$item->id;
$this->accounts[$id] = [
'id' => $id,
'name' => $item->name,
];
}
}
private function appendCollectedData(): void
{
$this->collection = $this->collection->map(function (PiggyBank $item) {
@@ -193,7 +130,7 @@ class PiggyBankEnrichment implements EnrichmentInterface
// add object group if available
if (array_key_exists($id, $this->mappedObjects)) {
$key = $this->mappedObjects[$id];
$meta['object_group_id'] = (string) $this->objectGroups[$key]['id'];
$meta['object_group_id'] = (string)$this->objectGroups[$key]['id'];
$meta['object_group_title'] = $this->objectGroups[$key]['title'];
$meta['object_group_order'] = $this->objectGroups[$key]['order'];
}
@@ -229,6 +166,71 @@ class PiggyBankEnrichment implements EnrichmentInterface
});
}
private function collectCurrentAmounts(): void {}
private function collectIds(): void
{
/** @var PiggyBank $piggy */
foreach ($this->collection as $piggy) {
$id = (int)$piggy->id;
$this->ids[] = $id;
$this->currencyIds[$id] = (int)$piggy->transaction_currency_id;
}
$this->ids = array_unique($this->ids);
// collect currencies.
$currencies = TransactionCurrency::whereIn('id', $this->currencyIds)->get();
foreach ($currencies as $currency) {
$this->currencies[(int)$currency->id] = $currency;
}
// collect accounts
$set = DB::table('account_piggy_bank')->whereIn('piggy_bank_id', $this->ids)->get(['piggy_bank_id', 'account_id', 'current_amount', 'native_current_amount']);
foreach ($set as $item) {
$id = (int)$item->piggy_bank_id;
$accountId = (int)$item->account_id;
$this->amounts[$id] ??= [];
if (!array_key_exists($id, $this->accountIds)) {
$this->accountIds[$id] = (int)$item->account_id;
}
if (!array_key_exists($accountId, $this->amounts[$id])) {
$this->amounts[$id][$accountId] = [
'current_amount' => '0',
'pc_current_amount' => '0',
];
}
$this->amounts[$id][$accountId]['current_amount'] = bcadd($this->amounts[$id][$accountId]['current_amount'], (string)$item->current_amount);
if (null !== $this->amounts[$id][$accountId]['pc_current_amount'] && null !== $item->native_current_amount) {
$this->amounts[$id][$accountId]['pc_current_amount'] = bcadd($this->amounts[$id][$accountId]['pc_current_amount'], (string)$item->native_current_amount);
}
}
// get account currency preference for ALL.
$set = AccountMeta::whereIn('account_id', array_values($this->accountIds))->where('name', 'currency_id')->get();
/** @var AccountMeta $item */
foreach ($set as $item) {
$accountId = (int)$item->account_id;
$currencyId = (int)$item->data;
if (!array_key_exists($currencyId, $this->currencies)) {
$this->currencies[$currencyId] = Amount::getTransactionCurrencyById($currencyId);
}
// $this->accountCurrencies[$accountId] = $this->currencies[$currencyId];
}
// get account info.
$set = Account::whereIn('id', array_values($this->accountIds))->get();
/** @var Account $item */
foreach ($set as $item) {
$id = (int)$item->id;
$this->accounts[$id] = [
'id' => $id,
'name' => $item->name,
];
}
}
private function collectNotes(): void
{
$notes = Note::query()->whereIn('noteable_id', $this->ids)
@@ -264,8 +266,6 @@ class PiggyBankEnrichment implements EnrichmentInterface
}
}
private function collectCurrentAmounts(): void {}
/**
* Returns the suggested amount the user should save per month, or "".
*/

View File

@@ -38,16 +38,16 @@ use Illuminate\Support\Facades\Log;
class PiggyBankEventEnrichment implements EnrichmentInterface
{
private User $user; // @phpstan-ignore-line
private UserGroup $userGroup; // @phpstan-ignore-line
private array $accountCurrencies = []; // @phpstan-ignore-line
private array $accountIds = []; // @phpstan-ignore-line
private Collection $collection;
private array $currencies = [];
private array $groupIds = [];
private array $ids = [];
private array $journalIds = [];
private array $groupIds = [];
private array $accountIds = [];
private array $piggyBankIds = [];
private array $accountCurrencies = [];
private array $currencies = [];
private User $user;
private UserGroup $userGroup;
// private bool $convertToPrimary = false;
// private TransactionCurrency $primaryCurrency;
@@ -86,6 +86,30 @@ class PiggyBankEventEnrichment implements EnrichmentInterface
$this->userGroup = $userGroup;
}
private function appendCollectedData(): void
{
$this->collection = $this->collection->map(function (PiggyBankEvent $item) {
$id = (int)$item->id;
$piggyId = (int)$item->piggy_bank_id;
$journalId = (int)$item->transaction_journal_id;
$currency = null;
if (array_key_exists($piggyId, $this->accountIds)) {
$accountId = $this->accountIds[$piggyId];
if (array_key_exists($accountId, $this->accountCurrencies)) {
$currency = $this->accountCurrencies[$accountId];
}
}
$meta = [
'transaction_group_id' => array_key_exists($journalId, $this->groupIds) ? (string)$this->groupIds[$journalId] : null,
'currency' => $currency,
];
$item->meta = $meta;
return $item;
});
}
private function collectIds(): void
{
/** @var PiggyBankEvent $event */
@@ -125,28 +149,4 @@ class PiggyBankEventEnrichment implements EnrichmentInterface
$this->accountCurrencies[$accountId] = $this->currencies[$currencyId];
}
}
private function appendCollectedData(): void
{
$this->collection = $this->collection->map(function (PiggyBankEvent $item) {
$id = (int)$item->id;
$piggyId = (int)$item->piggy_bank_id;
$journalId = (int)$item->transaction_journal_id;
$currency = null;
if (array_key_exists($piggyId, $this->accountIds)) {
$accountId = $this->accountIds[$piggyId];
if (array_key_exists($accountId, $this->accountCurrencies)) {
$currency = $this->accountCurrencies[$accountId];
}
}
$meta = [
'transaction_group_id' => array_key_exists($journalId, $this->groupIds) ? (string)$this->groupIds[$journalId] : null,
'currency' => $currency,
];
$item->meta = $meta;
return $item;
});
}
}

View File

@@ -56,25 +56,25 @@ use function Safe\json_decode;
class RecurringEnrichment implements EnrichmentInterface
{
private Collection $collection;
private array $ids = [];
private array $accounts = [];
private Collection $collection;
// private array $transactionTypeIds = [];
// private array $transactionTypes = [];
private array $notes = [];
private array $repetitions = [];
private array $transactions = [];
private User $user;
private UserGroup $userGroup;
private string $language = 'en_US';
private array $currencyIds = [];
private array $foreignCurrencyIds = [];
private array $sourceAccountIds = [];
private array $destinationAccountIds = [];
private array $accounts = [];
private array $currencies = [];
private array $recurrenceIds = [];
private bool $convertToPrimary = false;
private array $currencies = [];
private array $currencyIds = [];
private array $destinationAccountIds = [];
private array $foreignCurrencyIds = [];
private array $ids = [];
private string $language = 'en_US';
private array $notes = [];
private readonly TransactionCurrency $primaryCurrency;
private bool $convertToPrimary = false;
private array $recurrenceIds = [];
private array $repetitions = [];
private array $sourceAccountIds = [];
private array $transactions = [];
private User $user;
private UserGroup $userGroup;
public function __construct()
{
@@ -107,139 +107,6 @@ class RecurringEnrichment implements EnrichmentInterface
return $collection->first();
}
public function setUser(User $user): void
{
$this->user = $user;
$this->setUserGroup($user->userGroup);
$this->getLanguage();
}
public function setUserGroup(UserGroup $userGroup): void
{
$this->userGroup = $userGroup;
}
private function collectIds(): void
{
/** @var Recurrence $recurrence */
foreach ($this->collection as $recurrence) {
$id = (int)$recurrence->id;
// $typeId = (int)$recurrence->transaction_type_id;
$this->ids[] = $id;
// $this->transactionTypeIds[$id] = $typeId;
}
$this->ids = array_unique($this->ids);
// collect transaction types.
// $transactionTypes = TransactionType::whereIn('id', array_unique($this->transactionTypeIds))->get();
// foreach ($transactionTypes as $transactionType) {
// $id = (int)$transactionType->id;
// $this->transactionTypes[$id] = TransactionTypeEnum::from($transactionType->type);
// }
}
private function collectRepetitions(): void
{
Log::debug('Start of enrichment: collectRepetitions()');
$repository = app(RecurringRepositoryInterface::class);
$repository->setUserGroup($this->userGroup);
$set = RecurrenceRepetition::whereIn('recurrence_id', $this->ids)->get();
/** @var RecurrenceRepetition $repetition */
foreach ($set as $repetition) {
$recurrence = $this->collection->filter(fn (Recurrence $item) => (int)$item->id === (int)$repetition->recurrence_id)->first();
$fromDate = clone ($recurrence->latest_date ?? $recurrence->first_date);
$id = (int)$repetition->recurrence_id;
$repId = (int)$repetition->id;
$this->repetitions[$id] ??= [];
// get the (future) occurrences for this specific type of repetition:
$amount = 'daily' === $repetition->repetition_type ? 9 : 5;
$set = $repository->getXOccurrencesSince($repetition, $fromDate, now(config('app.timezone')), $amount);
$occurrences = [];
/** @var Carbon $carbon */
foreach ($set as $carbon) {
$occurrences[] = $carbon->toAtomString();
}
$this->repetitions[$id][$repId] = [
'id' => (string)$repId,
'created_at' => $repetition->created_at->toAtomString(),
'updated_at' => $repetition->updated_at->toAtomString(),
'type' => $repetition->repetition_type,
'moment' => (string)$repetition->repetition_moment,
'skip' => (int)$repetition->repetition_skip,
'weekend' => RecurrenceRepetitionWeekend::from((int)$repetition->weekend)->value,
'description' => $this->getRepetitionDescription($repetition),
'occurrences' => $occurrences,
];
}
Log::debug('End of enrichment: collectRepetitions()');
}
private function collectTransactions(): void
{
$set = RecurrenceTransaction::whereIn('recurrence_id', $this->ids)->get();
/** @var RecurrenceTransaction $transaction */
foreach ($set as $transaction) {
$id = (int)$transaction->recurrence_id;
$transactionId = (int)$transaction->id;
$this->recurrenceIds[$transactionId] = $id;
$this->transactions[$id] ??= [];
$amount = $transaction->amount;
$foreignAmount = $transaction->foreign_amount;
$this->transactions[$id][$transactionId] = [
'id' => (string)$transactionId,
// 'recurrence_id' => $id,
'transaction_currency_id' => (int)$transaction->transaction_currency_id,
'foreign_currency_id' => null === $transaction->foreign_currency_id ? null : (int)$transaction->foreign_currency_id,
'source_id' => (int)$transaction->source_id,
'object_has_currency_setting' => true,
'destination_id' => (int)$transaction->destination_id,
'amount' => $amount,
'foreign_amount' => $foreignAmount,
'pc_amount' => null,
'pc_foreign_amount' => null,
'description' => $transaction->description,
'tags' => [],
'category_id' => null,
'category_name' => null,
'budget_id' => null,
'budget_name' => null,
'piggy_bank_id' => null,
'piggy_bank_name' => null,
'subscription_id' => null,
'subscription_name' => null,
];
// collect all kinds of meta data to be collected later.
$this->currencyIds[$transactionId] = (int)$transaction->transaction_currency_id;
$this->sourceAccountIds[$transactionId] = (int)$transaction->source_id;
$this->destinationAccountIds[$transactionId] = (int)$transaction->destination_id;
if (null !== $transaction->foreign_currency_id) {
$this->foreignCurrencyIds[$transactionId] = (int)$transaction->foreign_currency_id;
}
}
}
private function appendCollectedData(): void
{
$this->collection = $this->collection->map(function (Recurrence $item) {
$id = (int)$item->id;
$meta = [
'notes' => $this->notes[$id] ?? null,
'repetitions' => array_values($this->repetitions[$id] ?? []),
'transactions' => $this->processTransactions(array_values($this->transactions[$id] ?? [])),
];
$item->meta = $meta;
return $item;
});
}
/**
* Parse the repetition in a string that is user readable.
* TODO duplicate with repository.
@@ -290,96 +157,32 @@ class RecurringEnrichment implements EnrichmentInterface
return '';
}
private function getLanguage(): void
public function setUser(User $user): void
{
/** @var Preference $preference */
$preference = Preferences::getForUser($this->user, 'language', config('firefly.default_language', 'en_US'));
$language = $preference->data;
if (is_array($language)) {
$language = 'en_US';
}
$language = (string)$language;
$this->language = $language;
$this->user = $user;
$this->setUserGroup($user->userGroup);
$this->getLanguage();
}
private function collectCurrencies(): void
public function setUserGroup(UserGroup $userGroup): void
{
$all = array_merge(array_unique($this->currencyIds), array_unique($this->foreignCurrencyIds));
$currencies = TransactionCurrency::whereIn('id', array_unique($all))->get();
foreach ($currencies as $currency) {
$id = (int)$currency->id;
$this->currencies[$id] = $currency;
}
$this->userGroup = $userGroup;
}
private function processTransactions(array $transactions): array
private function appendCollectedData(): void
{
$return = [];
$converter = new ExchangeRateConverter();
foreach ($transactions as $transaction) {
$currencyId = $transaction['transaction_currency_id'];
$pcAmount = null;
$pcForeignAmount = null;
// set the same amount in the primary currency, if both are the same anyway.
if (true === $this->convertToPrimary && $currencyId === (int)$this->primaryCurrency->id) {
$pcAmount = $transaction['amount'];
}
// convert the amount to the primary currency, if it is not the same.
if (true === $this->convertToPrimary && $currencyId !== (int)$this->primaryCurrency->id) {
$pcAmount = $converter->convert($this->currencies[$currencyId], $this->primaryCurrency, today(), $transaction['amount']);
}
if (null !== $transaction['foreign_amount'] && null !== $transaction['foreign_currency_id']) {
$foreignCurrencyId = $transaction['foreign_currency_id'];
if ($foreignCurrencyId !== $this->primaryCurrency->id) {
$pcForeignAmount = $converter->convert($this->currencies[$foreignCurrencyId], $this->primaryCurrency, today(), $transaction['foreign_amount']);
}
}
$this->collection = $this->collection->map(function (Recurrence $item) {
$id = (int)$item->id;
$meta = [
'notes' => $this->notes[$id] ?? null,
'repetitions' => array_values($this->repetitions[$id] ?? []),
'transactions' => $this->processTransactions(array_values($this->transactions[$id] ?? [])),
];
$transaction['pc_amount'] = $pcAmount;
$transaction['pc_foreign_amount'] = $pcForeignAmount;
$item->meta = $meta;
$sourceId = $transaction['source_id'];
$transaction['source_name'] = $this->accounts[$sourceId]->name;
$transaction['source_iban'] = $this->accounts[$sourceId]->iban;
$transaction['source_type'] = $this->accounts[$sourceId]->accountType->type;
$transaction['source_id'] = (string)$transaction['source_id'];
$destId = $transaction['destination_id'];
$transaction['destination_name'] = $this->accounts[$destId]->name;
$transaction['destination_iban'] = $this->accounts[$destId]->iban;
$transaction['destination_type'] = $this->accounts[$destId]->accountType->type;
$transaction['destination_id'] = (string)$transaction['destination_id'];
$transaction['currency_id'] = (string)$currencyId;
$transaction['currency_name'] = $this->currencies[$currencyId]->name;
$transaction['currency_code'] = $this->currencies[$currencyId]->code;
$transaction['currency_symbol'] = $this->currencies[$currencyId]->symbol;
$transaction['currency_decimal_places'] = $this->currencies[$currencyId]->decimal_places;
$transaction['primary_currency_id'] = (string)$this->primaryCurrency->id;
$transaction['primary_currency_name'] = $this->primaryCurrency->name;
$transaction['primary_currency_code'] = $this->primaryCurrency->code;
$transaction['primary_currency_symbol'] = $this->primaryCurrency->symbol;
$transaction['primary_currency_decimal_places'] = $this->primaryCurrency->decimal_places;
// $transaction['foreign_currency_id'] = null;
$transaction['foreign_currency_name'] = null;
$transaction['foreign_currency_code'] = null;
$transaction['foreign_currency_symbol'] = null;
$transaction['foreign_currency_decimal_places'] = null;
if (null !== $transaction['foreign_currency_id']) {
$currencyId = $transaction['foreign_currency_id'];
$transaction['foreign_currency_id'] = (string)$currencyId;
$transaction['foreign_currency_name'] = $this->currencies[$currencyId]->name;
$transaction['foreign_currency_code'] = $this->currencies[$currencyId]->code;
$transaction['foreign_currency_symbol'] = $this->currencies[$currencyId]->symbol;
$transaction['foreign_currency_decimal_places'] = $this->currencies[$currencyId]->decimal_places;
}
unset($transaction['transaction_currency_id']);
$return[] = $transaction;
}
return $return;
return $item;
});
}
private function collectAccounts(): void
@@ -394,6 +197,180 @@ class RecurringEnrichment implements EnrichmentInterface
}
}
private function collectBillInfo(array $billIds): void
{
if (0 === count($billIds)) {
return;
}
$ids = Arr::pluck($billIds, 'bill_id');
$bills = Bill::whereIn('id', $ids)->get();
$mapped = [];
foreach ($bills as $bill) {
$mapped[(int)$bill->id] = $bill;
}
foreach ($billIds as $info) {
$recurrenceId = $info['recurrence_id'];
$transactionId = $info['transaction_id'];
$this->transactions[$recurrenceId][$transactionId]['subscription_name'] = $mapped[$info['bill_id']]->name ?? '';
}
}
private function collectBudgetInfo(array $budgetIds): void
{
if (0 === count($budgetIds)) {
return;
}
$ids = Arr::pluck($budgetIds, 'budget_id');
$categories = Budget::whereIn('id', $ids)->get();
$mapped = [];
foreach ($categories as $category) {
$mapped[(int)$category->id] = $category;
}
foreach ($budgetIds as $info) {
$recurrenceId = $info['recurrence_id'];
$transactionId = $info['transaction_id'];
$this->transactions[$recurrenceId][$transactionId]['budget_name'] = $mapped[$info['budget_id']]->name ?? '';
}
}
private function collectCategoryIdInfo(array $categoryIds): void
{
if (0 === count($categoryIds)) {
return;
}
$ids = Arr::pluck($categoryIds, 'category_id');
$categories = Category::whereIn('id', $ids)->get();
$mapped = [];
foreach ($categories as $category) {
$mapped[(int)$category->id] = $category;
}
foreach ($categoryIds as $info) {
$recurrenceId = $info['recurrence_id'];
$transactionId = $info['transaction_id'];
$this->transactions[$recurrenceId][$transactionId]['category_name'] = $mapped[$info['category_id']]->name ?? '';
}
}
/**
* TODO This method does look-up in a loop.
*/
private function collectCategoryNameInfo(array $categoryNames): void
{
if (0 === count($categoryNames)) {
return;
}
$factory = app(CategoryFactory::class);
$factory->setUser($this->user);
foreach ($categoryNames as $info) {
$recurrenceId = $info['recurrence_id'];
$transactionId = $info['transaction_id'];
$category = $factory->findOrCreate(null, $info['category_name']);
if (null !== $category) {
$this->transactions[$recurrenceId][$transactionId]['category_id'] = (string)$category->id;
$this->transactions[$recurrenceId][$transactionId]['category_name'] = $category->name;
}
}
}
private function collectCurrencies(): void
{
$all = array_merge(array_unique($this->currencyIds), array_unique($this->foreignCurrencyIds));
$currencies = TransactionCurrency::whereIn('id', array_unique($all))->get();
foreach ($currencies as $currency) {
$id = (int)$currency->id;
$this->currencies[$id] = $currency;
}
}
private function collectIds(): void
{
/** @var Recurrence $recurrence */
foreach ($this->collection as $recurrence) {
$id = (int)$recurrence->id;
// $typeId = (int)$recurrence->transaction_type_id;
$this->ids[] = $id;
// $this->transactionTypeIds[$id] = $typeId;
}
$this->ids = array_unique($this->ids);
// collect transaction types.
// $transactionTypes = TransactionType::whereIn('id', array_unique($this->transactionTypeIds))->get();
// foreach ($transactionTypes as $transactionType) {
// $id = (int)$transactionType->id;
// $this->transactionTypes[$id] = TransactionTypeEnum::from($transactionType->type);
// }
}
private function collectNotes(): void
{
$notes = Note::query()->whereIn('noteable_id', $this->ids)
->whereNotNull('notes.text')
->where('notes.text', '!=', '')
->where('noteable_type', Recurrence::class)->get(['notes.noteable_id', 'notes.text'])->toArray()
;
foreach ($notes as $note) {
$this->notes[(int)$note['noteable_id']] = (string)$note['text'];
}
Log::debug(sprintf('Enrich with %d note(s)', count($this->notes)));
}
private function collectPiggyBankInfo(array $piggyBankIds): void
{
if (0 === count($piggyBankIds)) {
return;
}
$ids = Arr::pluck($piggyBankIds, 'piggy_bank_id');
$piggyBanks = PiggyBank::whereIn('id', $ids)->get();
$mapped = [];
foreach ($piggyBanks as $piggyBank) {
$mapped[(int)$piggyBank->id] = $piggyBank;
}
foreach ($piggyBankIds as $info) {
$recurrenceId = $info['recurrence_id'];
$transactionId = $info['transaction_id'];
$this->transactions[$recurrenceId][$transactionId]['piggy_bank_name'] = $mapped[$info['piggy_bank_id']]->name ?? '';
}
}
private function collectRepetitions(): void
{
Log::debug('Start of enrichment: collectRepetitions()');
$repository = app(RecurringRepositoryInterface::class);
$repository->setUserGroup($this->userGroup);
$set = RecurrenceRepetition::whereIn('recurrence_id', $this->ids)->get();
/** @var RecurrenceRepetition $repetition */
foreach ($set as $repetition) {
$recurrence = $this->collection->filter(fn (Recurrence $item) => (int)$item->id === (int)$repetition->recurrence_id)->first();
$fromDate = clone ($recurrence->latest_date ?? $recurrence->first_date);
$id = (int)$repetition->recurrence_id;
$repId = (int)$repetition->id;
$this->repetitions[$id] ??= [];
// get the (future) occurrences for this specific type of repetition:
$amount = 'daily' === $repetition->repetition_type ? 9 : 5;
$set = $repository->getXOccurrencesSince($repetition, $fromDate, now(config('app.timezone')), $amount);
$occurrences = [];
/** @var Carbon $carbon */
foreach ($set as $carbon) {
$occurrences[] = $carbon->toAtomString();
}
$this->repetitions[$id][$repId] = [
'id' => (string)$repId,
'created_at' => $repetition->created_at->toAtomString(),
'updated_at' => $repetition->updated_at->toAtomString(),
'type' => $repetition->repetition_type,
'moment' => (string)$repetition->repetition_moment,
'skip' => (int)$repetition->repetition_skip,
'weekend' => RecurrenceRepetitionWeekend::from((int)$repetition->weekend)->value,
'description' => $this->getRepetitionDescription($repetition),
'occurrences' => $occurrences,
];
}
Log::debug('End of enrichment: collectRepetitions()');
}
private function collectTransactionMetaData(): void
{
$ids = array_keys($this->transactions);
@@ -504,109 +481,132 @@ class RecurringEnrichment implements EnrichmentInterface
$this->collectBudgetInfo($budgetIds);
}
private function collectBillInfo(array $billIds): void
private function collectTransactions(): void
{
if (0 === count($billIds)) {
return;
}
$ids = Arr::pluck($billIds, 'bill_id');
$bills = Bill::whereIn('id', $ids)->get();
$mapped = [];
foreach ($bills as $bill) {
$mapped[(int)$bill->id] = $bill;
}
foreach ($billIds as $info) {
$recurrenceId = $info['recurrence_id'];
$transactionId = $info['transaction_id'];
$this->transactions[$recurrenceId][$transactionId]['subscription_name'] = $mapped[$info['bill_id']]->name ?? '';
}
}
$set = RecurrenceTransaction::whereIn('recurrence_id', $this->ids)->get();
private function collectPiggyBankInfo(array $piggyBankIds): void
{
if (0 === count($piggyBankIds)) {
return;
}
$ids = Arr::pluck($piggyBankIds, 'piggy_bank_id');
$piggyBanks = PiggyBank::whereIn('id', $ids)->get();
$mapped = [];
foreach ($piggyBanks as $piggyBank) {
$mapped[(int)$piggyBank->id] = $piggyBank;
}
foreach ($piggyBankIds as $info) {
$recurrenceId = $info['recurrence_id'];
$transactionId = $info['transaction_id'];
$this->transactions[$recurrenceId][$transactionId]['piggy_bank_name'] = $mapped[$info['piggy_bank_id']]->name ?? '';
}
}
/** @var RecurrenceTransaction $transaction */
foreach ($set as $transaction) {
$id = (int)$transaction->recurrence_id;
$transactionId = (int)$transaction->id;
$this->recurrenceIds[$transactionId] = $id;
$this->transactions[$id] ??= [];
$amount = $transaction->amount;
$foreignAmount = $transaction->foreign_amount;
private function collectCategoryIdInfo(array $categoryIds): void
{
if (0 === count($categoryIds)) {
return;
}
$ids = Arr::pluck($categoryIds, 'category_id');
$categories = Category::whereIn('id', $ids)->get();
$mapped = [];
foreach ($categories as $category) {
$mapped[(int)$category->id] = $category;
}
foreach ($categoryIds as $info) {
$recurrenceId = $info['recurrence_id'];
$transactionId = $info['transaction_id'];
$this->transactions[$recurrenceId][$transactionId]['category_name'] = $mapped[$info['category_id']]->name ?? '';
}
}
$this->transactions[$id][$transactionId] = [
'id' => (string)$transactionId,
// 'recurrence_id' => $id,
'transaction_currency_id' => (int)$transaction->transaction_currency_id,
'foreign_currency_id' => null === $transaction->foreign_currency_id ? null : (int)$transaction->foreign_currency_id,
'source_id' => (int)$transaction->source_id,
'object_has_currency_setting' => true,
'destination_id' => (int)$transaction->destination_id,
'amount' => $amount,
'foreign_amount' => $foreignAmount,
'pc_amount' => null,
'pc_foreign_amount' => null,
'description' => $transaction->description,
'tags' => [],
'category_id' => null,
'category_name' => null,
'budget_id' => null,
'budget_name' => null,
'piggy_bank_id' => null,
'piggy_bank_name' => null,
'subscription_id' => null,
'subscription_name' => null,
/**
* TODO This method does look-up in a loop.
*/
private function collectCategoryNameInfo(array $categoryNames): void
{
if (0 === count($categoryNames)) {
return;
}
$factory = app(CategoryFactory::class);
$factory->setUser($this->user);
foreach ($categoryNames as $info) {
$recurrenceId = $info['recurrence_id'];
$transactionId = $info['transaction_id'];
$category = $factory->findOrCreate(null, $info['category_name']);
if (null !== $category) {
$this->transactions[$recurrenceId][$transactionId]['category_id'] = (string)$category->id;
$this->transactions[$recurrenceId][$transactionId]['category_name'] = $category->name;
];
// collect all kinds of meta data to be collected later.
$this->currencyIds[$transactionId] = (int)$transaction->transaction_currency_id;
$this->sourceAccountIds[$transactionId] = (int)$transaction->source_id;
$this->destinationAccountIds[$transactionId] = (int)$transaction->destination_id;
if (null !== $transaction->foreign_currency_id) {
$this->foreignCurrencyIds[$transactionId] = (int)$transaction->foreign_currency_id;
}
}
}
private function collectBudgetInfo(array $budgetIds): void
private function getLanguage(): void
{
if (0 === count($budgetIds)) {
return;
}
$ids = Arr::pluck($budgetIds, 'budget_id');
$categories = Budget::whereIn('id', $ids)->get();
$mapped = [];
foreach ($categories as $category) {
$mapped[(int)$category->id] = $category;
}
foreach ($budgetIds as $info) {
$recurrenceId = $info['recurrence_id'];
$transactionId = $info['transaction_id'];
$this->transactions[$recurrenceId][$transactionId]['budget_name'] = $mapped[$info['budget_id']]->name ?? '';
/** @var Preference $preference */
$preference = Preferences::getForUser($this->user, 'language', config('firefly.default_language', 'en_US'));
$language = $preference->data;
if (is_array($language)) {
$language = 'en_US';
}
$language = (string)$language;
$this->language = $language;
}
private function collectNotes(): void
private function processTransactions(array $transactions): array
{
$notes = Note::query()->whereIn('noteable_id', $this->ids)
->whereNotNull('notes.text')
->where('notes.text', '!=', '')
->where('noteable_type', Recurrence::class)->get(['notes.noteable_id', 'notes.text'])->toArray()
;
foreach ($notes as $note) {
$this->notes[(int)$note['noteable_id']] = (string)$note['text'];
$return = [];
$converter = new ExchangeRateConverter();
foreach ($transactions as $transaction) {
$currencyId = $transaction['transaction_currency_id'];
$pcAmount = null;
$pcForeignAmount = null;
// set the same amount in the primary currency, if both are the same anyway.
if (true === $this->convertToPrimary && $currencyId === (int)$this->primaryCurrency->id) {
$pcAmount = $transaction['amount'];
}
// convert the amount to the primary currency, if it is not the same.
if (true === $this->convertToPrimary && $currencyId !== (int)$this->primaryCurrency->id) {
$pcAmount = $converter->convert($this->currencies[$currencyId], $this->primaryCurrency, today(), $transaction['amount']);
}
if (null !== $transaction['foreign_amount'] && null !== $transaction['foreign_currency_id']) {
$foreignCurrencyId = $transaction['foreign_currency_id'];
if ($foreignCurrencyId !== $this->primaryCurrency->id) {
$pcForeignAmount = $converter->convert($this->currencies[$foreignCurrencyId], $this->primaryCurrency, today(), $transaction['foreign_amount']);
}
}
$transaction['pc_amount'] = $pcAmount;
$transaction['pc_foreign_amount'] = $pcForeignAmount;
$sourceId = $transaction['source_id'];
$transaction['source_name'] = $this->accounts[$sourceId]->name;
$transaction['source_iban'] = $this->accounts[$sourceId]->iban;
$transaction['source_type'] = $this->accounts[$sourceId]->accountType->type;
$transaction['source_id'] = (string)$transaction['source_id'];
$destId = $transaction['destination_id'];
$transaction['destination_name'] = $this->accounts[$destId]->name;
$transaction['destination_iban'] = $this->accounts[$destId]->iban;
$transaction['destination_type'] = $this->accounts[$destId]->accountType->type;
$transaction['destination_id'] = (string)$transaction['destination_id'];
$transaction['currency_id'] = (string)$currencyId;
$transaction['currency_name'] = $this->currencies[$currencyId]->name;
$transaction['currency_code'] = $this->currencies[$currencyId]->code;
$transaction['currency_symbol'] = $this->currencies[$currencyId]->symbol;
$transaction['currency_decimal_places'] = $this->currencies[$currencyId]->decimal_places;
$transaction['primary_currency_id'] = (string)$this->primaryCurrency->id;
$transaction['primary_currency_name'] = $this->primaryCurrency->name;
$transaction['primary_currency_code'] = $this->primaryCurrency->code;
$transaction['primary_currency_symbol'] = $this->primaryCurrency->symbol;
$transaction['primary_currency_decimal_places'] = $this->primaryCurrency->decimal_places;
// $transaction['foreign_currency_id'] = null;
$transaction['foreign_currency_name'] = null;
$transaction['foreign_currency_code'] = null;
$transaction['foreign_currency_symbol'] = null;
$transaction['foreign_currency_decimal_places'] = null;
if (null !== $transaction['foreign_currency_id']) {
$currencyId = $transaction['foreign_currency_id'];
$transaction['foreign_currency_id'] = (string)$currencyId;
$transaction['foreign_currency_name'] = $this->currencies[$currencyId]->name;
$transaction['foreign_currency_code'] = $this->currencies[$currencyId]->code;
$transaction['foreign_currency_symbol'] = $this->currencies[$currencyId]->symbol;
$transaction['foreign_currency_decimal_places'] = $this->currencies[$currencyId]->decimal_places;
}
unset($transaction['transaction_currency_id']);
$return[] = $transaction;
}
Log::debug(sprintf('Enrich with %d note(s)', count($this->notes)));
return $return;
}
}

View File

@@ -46,20 +46,20 @@ use Illuminate\Support\Facades\Log;
class SubscriptionEnrichment implements EnrichmentInterface
{
private User $user;
private UserGroup $userGroup; // @phpstan-ignore-line
private Collection $collection;
private BillDateCalculator $calculator;
private Collection $collection; // @phpstan-ignore-line
private readonly bool $convertToPrimary;
private ?Carbon $start = null;
private ?Carbon $end = null;
private array $subscriptionIds = [];
private array $objectGroups = [];
private array $mappedObjects = [];
private array $paidDates = [];
private array $notes = [];
private array $payDates = [];
private ?Carbon $end = null;
private array $mappedObjects = [];
private array $notes = [];
private array $objectGroups = [];
private array $paidDates = [];
private array $payDates = [];
private readonly TransactionCurrency $primaryCurrency;
private BillDateCalculator $calculator;
private ?Carbon $start = null;
private array $subscriptionIds = [];
private User $user;
private UserGroup $userGroup;
public function __construct()
{
@@ -151,17 +151,14 @@ class SubscriptionEnrichment implements EnrichmentInterface
return $collection->first();
}
private function collectNotes(): void
public function setEnd(?Carbon $end): void
{
$notes = Note::query()->whereIn('noteable_id', $this->subscriptionIds)
->whereNotNull('notes.text')
->where('notes.text', '!=', '')
->where('noteable_type', Bill::class)->get(['notes.noteable_id', 'notes.text'])->toArray()
;
foreach ($notes as $note) {
$this->notes[(int)$note['noteable_id']] = (string)$note['text'];
}
Log::debug(sprintf('Enrich with %d note(s)', count($this->notes)));
$this->end = $end;
}
public function setStart(?Carbon $start): void
{
$this->start = $start;
}
public function setUser(User $user): void
@@ -175,13 +172,40 @@ class SubscriptionEnrichment implements EnrichmentInterface
$this->userGroup = $userGroup;
}
private function collectSubscriptionIds(): void
/**
* Returns the latest date in the set, or start when set is empty.
*/
protected function lastPaidDate(Bill $subscription, Collection $dates, Carbon $default): Carbon
{
/** @var Bill $bill */
foreach ($this->collection as $bill) {
$this->subscriptionIds[] = (int)$bill->id;
$filtered = $dates->filter(fn (TransactionJournal $journal) => (int)$journal->bill_id === (int)$subscription->id);
Log::debug(sprintf('Filtered down from %d to %d entries for bill #%d.', $dates->count(), $filtered->count(), $subscription->id));
if (0 === $filtered->count()) {
return $default;
}
$this->subscriptionIds = array_unique($this->subscriptionIds);
$latest = $filtered->first()->date;
/** @var TransactionJournal $journal */
foreach ($filtered as $journal) {
if ($journal->date->gte($latest)) {
$latest = $journal->date;
}
}
return $latest;
}
private function collectNotes(): void
{
$notes = Note::query()->whereIn('noteable_id', $this->subscriptionIds)
->whereNotNull('notes.text')
->where('notes.text', '!=', '')
->where('noteable_type', Bill::class)->get(['notes.noteable_id', 'notes.text'])->toArray()
;
foreach ($notes as $note) {
$this->notes[(int)$note['noteable_id']] = (string)$note['text'];
}
Log::debug(sprintf('Enrich with %d note(s)', count($this->notes)));
}
private function collectObjectGroups(): void
@@ -329,63 +353,6 @@ class SubscriptionEnrichment implements EnrichmentInterface
}
public function setStart(?Carbon $start): void
{
$this->start = $start;
}
public function setEnd(?Carbon $end): void
{
$this->end = $end;
}
/**
* Returns the latest date in the set, or start when set is empty.
*/
protected function lastPaidDate(Bill $subscription, Collection $dates, Carbon $default): Carbon
{
$filtered = $dates->filter(fn (TransactionJournal $journal) => (int)$journal->bill_id === (int)$subscription->id);
Log::debug(sprintf('Filtered down from %d to %d entries for bill #%d.', $dates->count(), $filtered->count(), $subscription->id));
if (0 === $filtered->count()) {
return $default;
}
$latest = $filtered->first()->date;
/** @var TransactionJournal $journal */
foreach ($filtered as $journal) {
if ($journal->date->gte($latest)) {
$latest = $journal->date;
}
}
return $latest;
}
private function getLastPaidDate(array $paidData): ?Carbon
{
// Log::debug('getLastPaidDate()');
$return = null;
foreach ($paidData as $entry) {
if (null !== $return) {
/** @var Carbon $current */
$current = $entry['date_object'];
if ($current->gt($return)) {
$return = clone $current;
}
Log::debug(sprintf('[a] Last paid date is: %s', $return->format('Y-m-d')));
}
if (null === $return) {
/** @var Carbon $return */
$return = $entry['date_object'];
Log::debug(sprintf('[b] Last paid date is: %s', $return->format('Y-m-d')));
}
}
// Log::debug(sprintf('[c] Last paid date is: "%s"', $return?->format('Y-m-d')));
return $return;
}
private function collectPayDates(): void
{
if (!$this->start instanceof Carbon || !$this->end instanceof Carbon) {
@@ -411,6 +378,15 @@ class SubscriptionEnrichment implements EnrichmentInterface
}
}
private function collectSubscriptionIds(): void
{
/** @var Bill $bill */
foreach ($this->collection as $bill) {
$this->subscriptionIds[] = (int)$bill->id;
}
$this->subscriptionIds = array_unique($this->subscriptionIds);
}
private function filterPaidDates(array $entries): array
{
return array_map(function (array $entry) {
@@ -420,6 +396,30 @@ class SubscriptionEnrichment implements EnrichmentInterface
}, $entries);
}
private function getLastPaidDate(array $paidData): ?Carbon
{
// Log::debug('getLastPaidDate()');
$return = null;
foreach ($paidData as $entry) {
if (null !== $return) {
/** @var Carbon $current */
$current = $entry['date_object'];
if ($current->gt($return)) {
$return = clone $current;
}
Log::debug(sprintf('[a] Last paid date is: %s', $return->format('Y-m-d')));
}
if (null === $return) {
/** @var Carbon $return */
$return = $entry['date_object'];
Log::debug(sprintf('[b] Last paid date is: %s', $return->format('Y-m-d')));
}
}
// Log::debug(sprintf('[c] Last paid date is: "%s"', $return?->format('Y-m-d')));
return $return;
}
private function getNextExpectedMatch(array $payDates): ?Carbon
{
// next expected match

View File

@@ -45,17 +45,17 @@ use Override;
class TransactionGroupEnrichment implements EnrichmentInterface
{
private array $attachmentCount = [];
private Collection $collection;
private readonly array $dateFields;
private array $journalIds = [];
private array $locations = [];
private array $metaData = [];
private array $notes = [];
private array $tags = [];
private User $user; // @phpstan-ignore-line
private array $attachmentCount = [];
private Collection $collection;
private readonly array $dateFields;
private array $journalIds = [];
private array $locations = [];
private array $metaData = [];
private array $notes = [];
private readonly TransactionCurrency $primaryCurrency;
private UserGroup $userGroup; // @phpstan-ignore-line
private array $tags = []; // @phpstan-ignore-line
private User $user;
private UserGroup $userGroup; // @phpstan-ignore-line
public function __construct()
{
@@ -63,20 +63,6 @@ class TransactionGroupEnrichment implements EnrichmentInterface
$this->primaryCurrency = Amount::getPrimaryCurrency();
}
#[Override]
public function enrichSingle(array|Model $model): array|TransactionGroup
{
Log::debug(__METHOD__);
if (is_array($model)) {
$collection = new Collection()->push($model);
$collection = $this->enrich($collection);
return $collection->first();
}
throw new FireflyException('Cannot enrich single model.');
}
#[Override]
public function enrich(Collection $collection): Collection
{
@@ -96,93 +82,29 @@ class TransactionGroupEnrichment implements EnrichmentInterface
return $this->collection;
}
private function collectJournalIds(): void
#[Override]
public function enrichSingle(array|Model $model): array|TransactionGroup
{
/** @var array $group */
foreach ($this->collection as $group) {
foreach ($group['transactions'] as $journal) {
$this->journalIds[] = $journal['transaction_journal_id'];
}
Log::debug(__METHOD__);
if (is_array($model)) {
$collection = new Collection()->push($model);
$collection = $this->enrich($collection);
return $collection->first();
}
$this->journalIds = array_unique($this->journalIds);
throw new FireflyException('Cannot enrich single model.');
}
private function collectNotes(): void
public function setUser(User $user): void
{
$notes = Note::query()->whereIn('noteable_id', $this->journalIds)
->whereNotNull('notes.text')
->where('notes.text', '!=', '')
->where('noteable_type', TransactionJournal::class)->get(['notes.noteable_id', 'notes.text'])->toArray()
;
foreach ($notes as $note) {
$this->notes[(int) $note['noteable_id']] = (string) $note['text'];
}
Log::debug(sprintf('Enrich with %d note(s)', count($this->notes)));
$this->user = $user;
$this->userGroup = $user->userGroup;
}
private function collectTags(): void
public function setUserGroup(UserGroup $userGroup): void
{
$set = Tag::leftJoin('tag_transaction_journal', 'tags.id', '=', 'tag_transaction_journal.tag_id')
->whereIn('tag_transaction_journal.transaction_journal_id', $this->journalIds)
->get(['tag_transaction_journal.transaction_journal_id', 'tags.tag'])->toArray()
;
foreach ($set as $item) {
$journalId = $item['transaction_journal_id'];
$this->tags[$journalId] ??= [];
$this->tags[$journalId][] = $item['tag'];
}
}
private function collectMetaData(): void
{
$set = TransactionJournalMeta::whereIn('transaction_journal_id', $this->journalIds)->get(['transaction_journal_id', 'name', 'data'])->toArray();
foreach ($set as $entry) {
$name = $entry['name'];
$data = (string) $entry['data'];
if ('' === $data) {
continue;
}
if (in_array($name, $this->dateFields, true)) {
// Log::debug(sprintf('Meta data for "%s" is a date : "%s"', $name, $data));
$this->metaData[$entry['transaction_journal_id']][$name] = Carbon::parse($data, config('app.timezone'));
// Log::debug(sprintf('Meta data for "%s" converts to: "%s"', $name, $this->metaData[$entry['transaction_journal_id']][$name]->toW3CString()));
continue;
}
$this->metaData[(int) $entry['transaction_journal_id']][$name] = $data;
}
}
private function collectLocations(): void
{
$locations = Location::query()->whereIn('locatable_id', $this->journalIds)
->where('locatable_type', TransactionJournal::class)->get(['locations.locatable_id', 'locations.latitude', 'locations.longitude', 'locations.zoom_level'])->toArray()
;
foreach ($locations as $location) {
$this->locations[(int) $location['locatable_id']]
= [
'latitude' => (float) $location['latitude'],
'longitude' => (float) $location['longitude'],
'zoom_level' => (int) $location['zoom_level'],
];
}
Log::debug(sprintf('Enrich with %d locations(s)', count($this->locations)));
}
private function collectAttachmentCount(): void
{
// select count(id) as nr_of_attachments, attachable_id from attachments
// group by attachable_id
$attachments = Attachment::query()
->whereIn('attachable_id', $this->journalIds)
->where('attachable_type', TransactionJournal::class)
->groupBy('attachable_id')
->get(['attachable_id', DB::raw('COUNT(id) as nr_of_attachments')])
->toArray()
;
foreach ($attachments as $row) {
$this->attachmentCount[(int) $row['attachable_id']] = (int) $row['nr_of_attachments'];
}
$this->userGroup = $userGroup;
}
private function appendCollectedData(): void
@@ -196,7 +118,7 @@ class TransactionGroupEnrichment implements EnrichmentInterface
$this->collection = $this->collection->map(function (array $item) use ($primaryCurrency, $notes, $tags, $metaData, $locations, $attachmentCount) {
foreach ($item['transactions'] as $index => $transaction) {
$journalId = (int) $transaction['transaction_journal_id'];
$journalId = (int)$transaction['transaction_journal_id'];
// attach notes if they exist:
$item['transactions'][$index]['notes'] = array_key_exists($journalId, $notes) ? $notes[$journalId] : null;
@@ -216,11 +138,11 @@ class TransactionGroupEnrichment implements EnrichmentInterface
// primary currency
$item['transactions'][$index]['primary_currency'] = [
'id' => (string) $primaryCurrency->id,
'code' => $primaryCurrency->code,
'name' => $primaryCurrency->name,
'symbol' => $primaryCurrency->symbol,
'decimal_places' => $primaryCurrency->decimal_places,
'id' => (string)$primaryCurrency->id,
'code' => $primaryCurrency->code,
'name' => $primaryCurrency->name,
'symbol' => $primaryCurrency->symbol,
'decimal_places' => $primaryCurrency->decimal_places,
];
// append meta data
@@ -248,14 +170,92 @@ class TransactionGroupEnrichment implements EnrichmentInterface
});
}
public function setUser(User $user): void
private function collectAttachmentCount(): void
{
$this->user = $user;
$this->userGroup = $user->userGroup;
// select count(id) as nr_of_attachments, attachable_id from attachments
// group by attachable_id
$attachments = Attachment::query()
->whereIn('attachable_id', $this->journalIds)
->where('attachable_type', TransactionJournal::class)
->groupBy('attachable_id')
->get(['attachable_id', DB::raw('COUNT(id) as nr_of_attachments')])
->toArray()
;
foreach ($attachments as $row) {
$this->attachmentCount[(int)$row['attachable_id']] = (int)$row['nr_of_attachments'];
}
}
public function setUserGroup(UserGroup $userGroup): void
private function collectJournalIds(): void
{
$this->userGroup = $userGroup;
/** @var array $group */
foreach ($this->collection as $group) {
foreach ($group['transactions'] as $journal) {
$this->journalIds[] = $journal['transaction_journal_id'];
}
}
$this->journalIds = array_unique($this->journalIds);
}
private function collectLocations(): void
{
$locations = Location::query()->whereIn('locatable_id', $this->journalIds)
->where('locatable_type', TransactionJournal::class)->get(['locations.locatable_id', 'locations.latitude', 'locations.longitude', 'locations.zoom_level'])->toArray()
;
foreach ($locations as $location) {
$this->locations[(int)$location['locatable_id']]
= [
'latitude' => (float)$location['latitude'],
'longitude' => (float)$location['longitude'],
'zoom_level' => (int)$location['zoom_level'],
];
}
Log::debug(sprintf('Enrich with %d locations(s)', count($this->locations)));
}
private function collectMetaData(): void
{
$set = TransactionJournalMeta::whereIn('transaction_journal_id', $this->journalIds)->get(['transaction_journal_id', 'name', 'data'])->toArray();
foreach ($set as $entry) {
$name = $entry['name'];
$data = (string)$entry['data'];
if ('' === $data) {
continue;
}
if (in_array($name, $this->dateFields, true)) {
// Log::debug(sprintf('Meta data for "%s" is a date : "%s"', $name, $data));
$this->metaData[$entry['transaction_journal_id']][$name] = Carbon::parse($data, config('app.timezone'));
// Log::debug(sprintf('Meta data for "%s" converts to: "%s"', $name, $this->metaData[$entry['transaction_journal_id']][$name]->toW3CString()));
continue;
}
$this->metaData[(int)$entry['transaction_journal_id']][$name] = $data;
}
}
private function collectNotes(): void
{
$notes = Note::query()->whereIn('noteable_id', $this->journalIds)
->whereNotNull('notes.text')
->where('notes.text', '!=', '')
->where('noteable_type', TransactionJournal::class)->get(['notes.noteable_id', 'notes.text'])->toArray()
;
foreach ($notes as $note) {
$this->notes[(int)$note['noteable_id']] = (string)$note['text'];
}
Log::debug(sprintf('Enrich with %d note(s)', count($this->notes)));
}
private function collectTags(): void
{
$set = Tag::leftJoin('tag_transaction_journal', 'tags.id', '=', 'tag_transaction_journal.tag_id')
->whereIn('tag_transaction_journal.transaction_journal_id', $this->journalIds)
->get(['tag_transaction_journal.transaction_journal_id', 'tags.tag'])->toArray()
;
foreach ($set as $item) {
$journalId = $item['transaction_journal_id'];
$this->tags[$journalId] ??= [];
$this->tags[$journalId][] = $item['tag'];
}
}
}

View File

@@ -43,16 +43,15 @@ use stdClass;
class WebhookEnrichment implements EnrichmentInterface
{
private Collection $collection;
private User $user; // @phpstan-ignore-line
private UserGroup $userGroup; // @phpstan-ignore-line
private array $ids = [];
private array $deliveries = [];
private array $responses = [];
private array $triggers = [];
private array $webhookDeliveries = [];
private array $webhookResponses = [];
private array $webhookTriggers = [];
private array $deliveries = []; // @phpstan-ignore-line
private array $ids = []; // @phpstan-ignore-line
private array $responses = [];
private array $triggers = [];
private User $user;
private UserGroup $userGroup;
private array $webhookDeliveries = [];
private array $webhookResponses = [];
private array $webhookTriggers = [];
public function enrich(Collection $collection): Collection
{
@@ -86,6 +85,20 @@ class WebhookEnrichment implements EnrichmentInterface
$this->userGroup = $userGroup;
}
private function appendCollectedInfo(): void
{
$this->collection = $this->collection->map(function (Webhook $item) {
$meta = [
'deliveries' => $this->webhookDeliveries[$item->id] ?? [],
'responses' => $this->webhookResponses[$item->id] ?? [],
'triggers' => $this->webhookTriggers[$item->id] ?? [],
];
$item->meta = $meta;
return $item;
});
}
private function collectIds(): void
{
/** @var Webhook $webhook */
@@ -147,18 +160,4 @@ class WebhookEnrichment implements EnrichmentInterface
$this->webhookTriggers[$id][] = WebhookTriggerEnum::from($this->triggers[$triggerId])->name;
}
}
private function appendCollectedInfo(): void
{
$this->collection = $this->collection->map(function (Webhook $item) {
$meta = [
'deliveries' => $this->webhookDeliveries[$item->id] ?? [],
'responses' => $this->webhookResponses[$item->id] ?? [],
'triggers' => $this->webhookTriggers[$item->id] ?? [],
];
$item->meta = $meta;
return $item;
});
}
}

View File

@@ -62,6 +62,47 @@ class AccountBalanceCalculator
$object->optimizedCalculation(new Collection());
}
public static function recalculateForJournal(TransactionJournal $transactionJournal): void
{
Log::debug(__METHOD__);
$object = new self();
$set = [];
foreach ($transactionJournal->transactions as $transaction) {
$set[$transaction->account_id] = $transaction->account;
}
$accounts = new Collection()->push(...$set);
$object->optimizedCalculation($accounts, $transactionJournal->date);
}
private function getLatestBalance(int $accountId, int $currencyId, ?Carbon $notBefore): string
{
if (!$notBefore instanceof Carbon) {
return '0';
}
Log::debug(sprintf('getLatestBalance: notBefore date is "%s", calculating', $notBefore->format('Y-m-d')));
$query = Transaction::leftJoin('transaction_journals', 'transaction_journals.id', '=', 'transactions.transaction_journal_id')
->whereNull('transactions.deleted_at')
->where('transaction_journals.transaction_currency_id', $currencyId)
->whereNull('transaction_journals.deleted_at')
// this order is the same as GroupCollector
->orderBy('transaction_journals.date', 'DESC')
->orderBy('transaction_journals.order', 'ASC')
->orderBy('transaction_journals.id', 'DESC')
->orderBy('transaction_journals.description', 'DESC')
->orderBy('transactions.amount', 'DESC')
->where('transactions.account_id', $accountId)
;
$notBefore->startOfDay();
$query->where('transaction_journals.date', '<', $notBefore);
$first = $query->first(['transactions.id', 'transactions.balance_dirty', 'transactions.transaction_currency_id', 'transaction_journals.date', 'transactions.account_id', 'transactions.amount', 'transactions.balance_after']);
$balance = (string)($first->balance_after ?? '0');
Log::debug(sprintf('getLatestBalance: found balance: %s in transaction #%d', $balance, $first->id ?? 0));
return $balance;
}
private function optimizedCalculation(Collection $accounts, ?Carbon $notBefore = null): void
{
Log::debug('start of optimizedCalculation');
@@ -123,34 +164,6 @@ class AccountBalanceCalculator
$this->storeAccountBalances($balances);
}
private function getLatestBalance(int $accountId, int $currencyId, ?Carbon $notBefore): string
{
if (!$notBefore instanceof Carbon) {
return '0';
}
Log::debug(sprintf('getLatestBalance: notBefore date is "%s", calculating', $notBefore->format('Y-m-d')));
$query = Transaction::leftJoin('transaction_journals', 'transaction_journals.id', '=', 'transactions.transaction_journal_id')
->whereNull('transactions.deleted_at')
->where('transaction_journals.transaction_currency_id', $currencyId)
->whereNull('transaction_journals.deleted_at')
// this order is the same as GroupCollector
->orderBy('transaction_journals.date', 'DESC')
->orderBy('transaction_journals.order', 'ASC')
->orderBy('transaction_journals.id', 'DESC')
->orderBy('transaction_journals.description', 'DESC')
->orderBy('transactions.amount', 'DESC')
->where('transactions.account_id', $accountId)
;
$notBefore->startOfDay();
$query->where('transaction_journals.date', '<', $notBefore);
$first = $query->first(['transactions.id', 'transactions.balance_dirty', 'transactions.transaction_currency_id', 'transaction_journals.date', 'transactions.account_id', 'transactions.amount', 'transactions.balance_after']);
$balance = (string)($first->balance_after ?? '0');
Log::debug(sprintf('getLatestBalance: found balance: %s in transaction #%d', $balance, $first->id ?? 0));
return $balance;
}
private function storeAccountBalances(array $balances): void
{
/**
@@ -196,17 +209,4 @@ class AccountBalanceCalculator
}
}
}
public static function recalculateForJournal(TransactionJournal $transactionJournal): void
{
Log::debug(__METHOD__);
$object = new self();
$set = [];
foreach ($transactionJournal->transactions as $transaction) {
$set[$transaction->account_id] = $transaction->account;
}
$accounts = new Collection()->push(...$set);
$object->optimizedCalculation($accounts, $transactionJournal->date);
}
}

View File

@@ -39,7 +39,7 @@ trait ReturnsIntegerIdTrait
protected function id(): Attribute
{
return Attribute::make(
get: static fn ($value) => (int) $value,
get: static fn ($value) => (int)$value,
);
}
}

View File

@@ -37,14 +37,14 @@ trait ReturnsIntegerUserIdTrait
protected function userGroupId(): Attribute
{
return Attribute::make(
get: static fn ($value) => (int) $value,
get: static fn ($value) => (int)$value,
);
}
protected function userId(): Attribute
{
return Attribute::make(
get: static fn ($value) => (int) $value,
get: static fn ($value) => (int)$value,
);
}
}

View File

@@ -88,24 +88,6 @@ class Navigation
return $this->nextDateByInterval($date, $functionMap[$repeatFreq], $skip);
}
public function nextDateByInterval(Carbon $epoch, Periodicity $periodicity, int $skipInterval = 0): Carbon
{
try {
return $this->calculator->nextDateByInterval($epoch, $periodicity, $skipInterval);
} catch (IntervalException $exception) {
Log::warning($exception->getMessage(), ['exception' => $exception]);
} catch (Throwable $exception) {
Log::error($exception->getMessage(), ['exception' => $exception]);
}
Log::debug(
'Any error occurred to calculate the next date.',
['date' => $epoch, 'periodicity' => $periodicity->name, 'skipInterval' => $skipInterval]
);
return $epoch;
}
public function blockPeriods(Carbon $start, Carbon $end, string $range): array
{
if ($end < $start) {
@@ -159,6 +141,421 @@ class Navigation
return $periods;
}
public function daysUntilEndOfMonth(Carbon $date): int
{
$endOfMonth = $date->copy()->endOfMonth();
return (int)$date->diffInDays($endOfMonth, true);
}
public function diffInPeriods(string $period, int $skip, Carbon $beginning, Carbon $end): int
{
Log::debug(sprintf(
'diffInPeriods: %s (skip: %d), between %s and %s.',
$period,
$skip,
$beginning->format('Y-m-d'),
$end->format('Y-m-d')
));
$map = [
'daily' => 'diffInDays',
'weekly' => 'diffInWeeks',
'monthly' => 'diffInMonths',
'quarterly' => 'diffInMonths',
'half-year' => 'diffInMonths',
'yearly' => 'diffInYears',
];
if (!array_key_exists($period, $map)) {
Log::warning(sprintf('No diffInPeriods for period "%s"', $period));
return 1;
}
$func = $map[$period];
// first do the diff
$floatDiff = $beginning->{$func}($end, true); // @phpstan-ignore-line
// then correct for quarterly or half-year
if ('quarterly' === $period) {
Log::debug(sprintf('Q: Corrected %f to %f', $floatDiff, $floatDiff / 3));
$floatDiff /= 3;
}
if ('half-year' === $period) {
Log::debug(sprintf('H: Corrected %f to %f', $floatDiff, $floatDiff / 6));
$floatDiff /= 6;
}
// then do ceil()
$diff = ceil($floatDiff);
Log::debug(sprintf('Diff is %f periods (%d rounded up)', $floatDiff, $diff));
if ($skip > 0) {
$parameter = $skip + 1;
$diff = ceil($diff / $parameter) * $parameter;
Log::debug(sprintf(
'diffInPeriods: skip is %d, so param is %d, and diff becomes %d',
$skip,
$parameter,
$diff
));
}
return (int)$diff;
}
public function endOfPeriod(Carbon $end, string $repeatFreq): Carbon
{
$currentEnd = clone $end;
// Log::debug(sprintf('Now in endOfPeriod("%s", "%s").', $currentEnd->toIso8601String(), $repeatFreq));
$functionMap = [
'1D' => 'endOfDay',
'daily' => 'endOfDay',
'1W' => 'addWeek',
'week' => 'addWeek',
'weekly' => 'addWeek',
'1M' => 'addMonth',
'month' => 'addMonth',
'monthly' => 'addMonth',
'3M' => 'addQuarter',
'quarter' => 'addQuarter',
'quarterly' => 'addQuarter',
'6M' => 'addMonths',
'half-year' => 'addMonths',
'half_year' => 'addMonths',
'year' => 'addYear',
'yearly' => 'addYear',
'1Y' => 'addYear',
];
$modifierMap = ['half-year' => 6, 'half_year' => 6, '6M' => 6];
$subDay = ['week', 'weekly', '1W', 'month', 'monthly', '1M', '3M', 'quarter', 'quarterly', '6M', 'half-year', 'half_year', '1Y', 'year', 'yearly'];
if ('custom' === $repeatFreq) {
// if the repeat frequency is "custom", use the current session start/end to see how large the range is,
// and use that to "add" another period.
// if there is no session data available use "30 days" as a default.
$diffInDays = 30;
if (null !== session('start') && null !== session('end')) {
Log::debug('Session data available.');
/** @var Carbon $tStart */
$tStart = session('start', today(config('app.timezone'))->startOfMonth());
/** @var Carbon $tEnd */
$tEnd = session('end', today(config('app.timezone'))->endOfMonth());
$diffInDays = (int)$tStart->diffInDays($tEnd, true);
}
Log::debug(sprintf('Diff in days is %d', $diffInDays));
$currentEnd->addDays($diffInDays);
return $currentEnd;
}
if ('MTD' === $repeatFreq) {
$today = today();
if ($today->isSameMonth($end)) {
return $today->endOfDay()->milli(0);
}
return $end->endOfMonth();
}
$result = match ($repeatFreq) {
'last7' => $currentEnd->addDays(7)->startOfDay(),
'last30' => $currentEnd->addDays(30)->startOfDay(),
'last90' => $currentEnd->addDays(90)->startOfDay(),
'last365' => $currentEnd->addDays(365)->startOfDay(),
'MTD' => $currentEnd->startOfMonth()->startOfDay(),
'QTD' => $currentEnd->firstOfQuarter()->startOfDay(),
'YTD' => $currentEnd->startOfYear()->startOfDay(),
default => null,
};
if (null !== $result) {
return $result;
}
unset($result);
if (!array_key_exists($repeatFreq, $functionMap)) {
Log::error(sprintf('Cannot do endOfPeriod for $repeat_freq "%s"', $repeatFreq));
return $end;
}
$function = $functionMap[$repeatFreq];
if (array_key_exists($repeatFreq, $modifierMap)) {
$currentEnd->{$function}($modifierMap[$repeatFreq])->milli(0); // @phpstan-ignore-line
if (in_array($repeatFreq, $subDay, true)) {
$currentEnd->subDay();
}
$currentEnd->endOfDay()->milli(0);
return $currentEnd;
}
$currentEnd->{$function}(); // @phpstan-ignore-line
$currentEnd->endOfDay()->milli(0);
if (in_array($repeatFreq, $subDay, true)) {
$currentEnd->subDay();
}
// Log::debug(sprintf('Final result: %s', $currentEnd->toIso8601String()));
return $currentEnd;
}
public function endOfX(Carbon $theCurrentEnd, string $repeatFreq, ?Carbon $maxDate): Carbon
{
$functionMap = [
'1D' => 'endOfDay',
'daily' => 'endOfDay',
'1W' => 'endOfWeek',
'week' => 'endOfWeek',
'weekly' => 'endOfWeek',
'month' => 'endOfMonth',
'1M' => 'endOfMonth',
'monthly' => 'endOfMonth',
'3M' => 'lastOfQuarter',
'quarter' => 'lastOfQuarter',
'quarterly' => 'lastOfQuarter',
'1Y' => 'endOfYear',
'year' => 'endOfYear',
'yearly' => 'endOfYear',
];
$currentEnd = clone $theCurrentEnd;
if (array_key_exists($repeatFreq, $functionMap)) {
$function = $functionMap[$repeatFreq];
$currentEnd->{$function}(); // @phpstan-ignore-line
}
if ($maxDate instanceof Carbon && $currentEnd > $maxDate) {
return clone $maxDate;
}
return $currentEnd;
}
/**
* Returns the user's view range and if necessary, corrects the dynamic view
* range to a normal range.
*/
public function getViewRange(bool $correct): string
{
$range = app('preferences')->get('viewRange', '1M')->data ?? '1M';
if (is_array($range)) {
$range = '1M';
}
$range = (string)$range;
if (!$correct) {
return $range;
}
return match ($range) {
'last7' => '1W',
'last30', 'MTD' => '1M',
'last90', 'QTD' => '3M',
'last365', 'YTD' => '1Y',
default => $range,
};
}
/**
* @throws FireflyException
*/
public function listOfPeriods(Carbon $start, Carbon $end): array
{
$locale = app('steam')->getLocale();
// define period to increment
$increment = 'addDay';
$format = $this->preferredCarbonFormat($start, $end);
$displayFormat = (string)trans('config.month_and_day_js', [], $locale);
$diff = $start->diffInMonths($end, true);
// increment by month (for year)
if ($diff >= 1.0001 && $diff < 12.001) {
$increment = 'addMonth';
$displayFormat = (string)trans('config.month_js');
}
// increment by year (for multi-year)
if ($diff >= 12.0001) {
$increment = 'addYear';
$displayFormat = (string)trans('config.year_js');
}
$begin = clone $start;
$entries = [];
while ($begin < $end) {
$formatted = $begin->format($format);
$displayed = $begin->isoFormat($displayFormat);
$entries[$formatted] = $displayed;
$begin->{$increment}(); // @phpstan-ignore-line
}
return $entries;
}
public function nextDateByInterval(Carbon $epoch, Periodicity $periodicity, int $skipInterval = 0): Carbon
{
try {
return $this->calculator->nextDateByInterval($epoch, $periodicity, $skipInterval);
} catch (IntervalException $exception) {
Log::warning($exception->getMessage(), ['exception' => $exception]);
} catch (Throwable $exception) {
Log::error($exception->getMessage(), ['exception' => $exception]);
}
Log::debug(
'Any error occurred to calculate the next date.',
['date' => $epoch, 'periodicity' => $periodicity->name, 'skipInterval' => $skipInterval]
);
return $epoch;
}
public function periodShow(Carbon $theDate, string $repeatFrequency): string
{
$date = clone $theDate;
$formatMap = [
'1D' => (string)trans('config.specific_day_js'),
'daily' => (string)trans('config.specific_day_js'),
'custom' => (string)trans('config.specific_day_js'),
'1W' => (string)trans('config.week_in_year_js'),
'week' => (string)trans('config.week_in_year_js'),
'weekly' => (string)trans('config.week_in_year_js'),
'1M' => (string)trans('config.month_js'),
'month' => (string)trans('config.month_js'),
'monthly' => (string)trans('config.month_js'),
'1Y' => (string)trans('config.year_js'),
'year' => (string)trans('config.year_js'),
'yearly' => (string)trans('config.year_js'),
'6M' => (string)trans('config.half_year_js'),
];
if (array_key_exists($repeatFrequency, $formatMap)) {
return $date->isoFormat($formatMap[$repeatFrequency]);
}
if ('3M' === $repeatFrequency || 'quarter' === $repeatFrequency) {
$quarter = ceil($theDate->month / 3);
return sprintf('Q%d %d', $quarter, $theDate->year);
}
// special formatter for quarter of year
Log::error(sprintf('No date formats for frequency "%s"!', $repeatFrequency));
throw new FireflyException(sprintf('No date formats for frequency "%s"!', $repeatFrequency));
return $date->format('Y-m-d');
}
/**
* If the date difference between start and end is less than a month, method returns "Y-m-d". If the difference is
* less than a year, method returns "Y-m". If the date difference is larger, method returns "Y".
*/
public function preferredCarbonFormat(Carbon $start, Carbon $end): string
{
$format = 'Y-m-d';
$diff = $start->diffInMonths($end, true);
// Log::debug(sprintf('preferredCarbonFormat(%s, %s) = %f', $start->format('Y-m-d'), $end->format('Y-m-d'), $diff));
if ($diff >= 1.001 && $diff < 12.001) {
// Log::debug(sprintf('Return Y-m because %s', $diff));
$format = 'Y-m';
}
if ($diff >= 12.001) {
// Log::debug(sprintf('Return Y because %s', $diff));
return 'Y';
}
return $format;
}
/**
* Same as preferredCarbonFormat but by string
*/
public function preferredCarbonFormatByPeriod(string $period): string
{
return match ($period) {
default => 'Y-m-d',
// '1D' => 'Y-m-d',
'1W' => '\WW,Y',
'1M' => 'Y-m',
'3M', '6M' => '\QQ,Y',
'1Y' => 'Y',
};
}
/**
* If the date difference between start and end is less than a month, method returns trans(config.month_and_day).
* If the difference is less than a year, method returns "config.month". If the date difference is larger, method
* returns "config.year".
*/
public function preferredCarbonLocalizedFormat(Carbon $start, Carbon $end): string
{
$locale = app('steam')->getLocale();
$diff = $start->diffInMonths($end, true);
if ($diff >= 1.001 && $diff < 12.001) {
return (string)trans('config.month_js', [], $locale);
}
if ($diff >= 12.001) {
return (string)trans('config.year_js', [], $locale);
}
return (string)trans('config.month_and_day_js', [], $locale);
}
/**
* If the date difference between start and end is less than a month, method returns "endOfDay". If the difference
* is less than a year, method returns "endOfMonth". If the date difference is larger, method returns "endOfYear".
*/
public function preferredEndOfPeriod(Carbon $start, Carbon $end): string
{
$diff = $start->diffInMonths($end, true);
if ($diff >= 1.001 && $diff < 12.001) {
return 'endOfMonth';
}
if ($diff >= 12.001) {
return 'endOfYear';
}
return 'endOfDay';
}
/**
* If the date difference between start and end is less than a month, method returns "1D". If the difference is
* less than a year, method returns "1M". If the date difference is larger, method returns "1Y".
*/
public function preferredRangeFormat(Carbon $start, Carbon $end): string
{
$diff = $start->diffInMonths($end, true);
if ($diff >= 1.001 && $diff < 12.001) {
return '1M';
}
if ($diff >= 12.001) {
return '1Y';
}
return '1D';
}
/**
* If the date difference between start and end is less than a month, method returns "%Y-%m-%d". If the difference
* is less than a year, method returns "%Y-%m". If the date difference is larger, method returns "%Y".
*/
public function preferredSqlFormat(Carbon $start, Carbon $end): string
{
$diff = $start->diffInMonths($end, true);
if ($diff >= 1.001 && $diff < 12.001) {
return '%Y-%m';
}
if ($diff >= 12.001) {
return '%Y';
}
return '%Y-%m-%d';
}
public function startOfPeriod(Carbon $theDate, string $repeatFreq): Carbon
{
$date = clone $theDate;
@@ -236,401 +633,6 @@ class Navigation
return $theDate;
}
public function endOfPeriod(Carbon $end, string $repeatFreq): Carbon
{
$currentEnd = clone $end;
// Log::debug(sprintf('Now in endOfPeriod("%s", "%s").', $currentEnd->toIso8601String(), $repeatFreq));
$functionMap = [
'1D' => 'endOfDay',
'daily' => 'endOfDay',
'1W' => 'addWeek',
'week' => 'addWeek',
'weekly' => 'addWeek',
'1M' => 'addMonth',
'month' => 'addMonth',
'monthly' => 'addMonth',
'3M' => 'addQuarter',
'quarter' => 'addQuarter',
'quarterly' => 'addQuarter',
'6M' => 'addMonths',
'half-year' => 'addMonths',
'half_year' => 'addMonths',
'year' => 'addYear',
'yearly' => 'addYear',
'1Y' => 'addYear',
];
$modifierMap = ['half-year' => 6, 'half_year' => 6, '6M' => 6];
$subDay = ['week', 'weekly', '1W', 'month', 'monthly', '1M', '3M', 'quarter', 'quarterly', '6M', 'half-year', 'half_year', '1Y', 'year', 'yearly'];
if ('custom' === $repeatFreq) {
// if the repeat frequency is "custom", use the current session start/end to see how large the range is,
// and use that to "add" another period.
// if there is no session data available use "30 days" as a default.
$diffInDays = 30;
if (null !== session('start') && null !== session('end')) {
Log::debug('Session data available.');
/** @var Carbon $tStart */
$tStart = session('start', today(config('app.timezone'))->startOfMonth());
/** @var Carbon $tEnd */
$tEnd = session('end', today(config('app.timezone'))->endOfMonth());
$diffInDays = (int) $tStart->diffInDays($tEnd, true);
}
Log::debug(sprintf('Diff in days is %d', $diffInDays));
$currentEnd->addDays($diffInDays);
return $currentEnd;
}
if ('MTD' === $repeatFreq) {
$today = today();
if ($today->isSameMonth($end)) {
return $today->endOfDay();
}
return $end->endOfMonth();
}
$result = match ($repeatFreq) {
'last7' => $currentEnd->addDays(7)->startOfDay(),
'last30' => $currentEnd->addDays(30)->startOfDay(),
'last90' => $currentEnd->addDays(90)->startOfDay(),
'last365' => $currentEnd->addDays(365)->startOfDay(),
'MTD' => $currentEnd->startOfMonth()->startOfDay(),
'QTD' => $currentEnd->firstOfQuarter()->startOfDay(),
'YTD' => $currentEnd->startOfYear()->startOfDay(),
default => null,
};
if (null !== $result) {
return $result;
}
unset($result);
if (!array_key_exists($repeatFreq, $functionMap)) {
Log::error(sprintf('Cannot do endOfPeriod for $repeat_freq "%s"', $repeatFreq));
return $end;
}
$function = $functionMap[$repeatFreq];
if (array_key_exists($repeatFreq, $modifierMap)) {
$currentEnd->{$function}($modifierMap[$repeatFreq]); // @phpstan-ignore-line
if (in_array($repeatFreq, $subDay, true)) {
$currentEnd->subDay();
}
$currentEnd->endOfDay();
return $currentEnd;
}
$currentEnd->{$function}(); // @phpstan-ignore-line
$currentEnd->endOfDay();
if (in_array($repeatFreq, $subDay, true)) {
$currentEnd->subDay();
}
// Log::debug(sprintf('Final result: %s', $currentEnd->toIso8601String()));
return $currentEnd;
}
public function daysUntilEndOfMonth(Carbon $date): int
{
$endOfMonth = $date->copy()->endOfMonth();
return (int) $date->diffInDays($endOfMonth, true);
}
public function diffInPeriods(string $period, int $skip, Carbon $beginning, Carbon $end): int
{
Log::debug(sprintf(
'diffInPeriods: %s (skip: %d), between %s and %s.',
$period,
$skip,
$beginning->format('Y-m-d'),
$end->format('Y-m-d')
));
$map = [
'daily' => 'diffInDays',
'weekly' => 'diffInWeeks',
'monthly' => 'diffInMonths',
'quarterly' => 'diffInMonths',
'half-year' => 'diffInMonths',
'yearly' => 'diffInYears',
];
if (!array_key_exists($period, $map)) {
Log::warning(sprintf('No diffInPeriods for period "%s"', $period));
return 1;
}
$func = $map[$period];
// first do the diff
$floatDiff = $beginning->{$func}($end, true); // @phpstan-ignore-line
// then correct for quarterly or half-year
if ('quarterly' === $period) {
Log::debug(sprintf('Q: Corrected %f to %f', $floatDiff, $floatDiff / 3));
$floatDiff /= 3;
}
if ('half-year' === $period) {
Log::debug(sprintf('H: Corrected %f to %f', $floatDiff, $floatDiff / 6));
$floatDiff /= 6;
}
// then do ceil()
$diff = ceil($floatDiff);
Log::debug(sprintf('Diff is %f periods (%d rounded up)', $floatDiff, $diff));
if ($skip > 0) {
$parameter = $skip + 1;
$diff = ceil($diff / $parameter) * $parameter;
Log::debug(sprintf(
'diffInPeriods: skip is %d, so param is %d, and diff becomes %d',
$skip,
$parameter,
$diff
));
}
return (int) $diff;
}
public function endOfX(Carbon $theCurrentEnd, string $repeatFreq, ?Carbon $maxDate): Carbon
{
$functionMap = [
'1D' => 'endOfDay',
'daily' => 'endOfDay',
'1W' => 'endOfWeek',
'week' => 'endOfWeek',
'weekly' => 'endOfWeek',
'month' => 'endOfMonth',
'1M' => 'endOfMonth',
'monthly' => 'endOfMonth',
'3M' => 'lastOfQuarter',
'quarter' => 'lastOfQuarter',
'quarterly' => 'lastOfQuarter',
'1Y' => 'endOfYear',
'year' => 'endOfYear',
'yearly' => 'endOfYear',
];
$currentEnd = clone $theCurrentEnd;
if (array_key_exists($repeatFreq, $functionMap)) {
$function = $functionMap[$repeatFreq];
$currentEnd->{$function}(); // @phpstan-ignore-line
}
if ($maxDate instanceof Carbon && $currentEnd > $maxDate) {
return clone $maxDate;
}
return $currentEnd;
}
/**
* Returns the user's view range and if necessary, corrects the dynamic view
* range to a normal range.
*/
public function getViewRange(bool $correct): string
{
$range = app('preferences')->get('viewRange', '1M')->data ?? '1M';
if (is_array($range)) {
$range = '1M';
}
$range = (string) $range;
if (!$correct) {
return $range;
}
return match ($range) {
'last7' => '1W',
'last30', 'MTD' => '1M',
'last90', 'QTD' => '3M',
'last365', 'YTD' => '1Y',
default => $range,
};
}
/**
* @throws FireflyException
*/
public function listOfPeriods(Carbon $start, Carbon $end): array
{
$locale = app('steam')->getLocale();
// define period to increment
$increment = 'addDay';
$format = $this->preferredCarbonFormat($start, $end);
$displayFormat = (string) trans('config.month_and_day_js', [], $locale);
$diff = $start->diffInMonths($end, true);
// increment by month (for year)
if ($diff >= 1.0001 && $diff < 12.001) {
$increment = 'addMonth';
$displayFormat = (string) trans('config.month_js');
}
// increment by year (for multi-year)
if ($diff >= 12.0001) {
$increment = 'addYear';
$displayFormat = (string) trans('config.year_js');
}
$begin = clone $start;
$entries = [];
while ($begin < $end) {
$formatted = $begin->format($format);
$displayed = $begin->isoFormat($displayFormat);
$entries[$formatted] = $displayed;
$begin->{$increment}(); // @phpstan-ignore-line
}
return $entries;
}
/**
* If the date difference between start and end is less than a month, method returns "Y-m-d". If the difference is
* less than a year, method returns "Y-m". If the date difference is larger, method returns "Y".
*/
public function preferredCarbonFormat(Carbon $start, Carbon $end): string
{
$format = 'Y-m-d';
$diff = $start->diffInMonths($end, true);
// Log::debug(sprintf('preferredCarbonFormat(%s, %s) = %f', $start->format('Y-m-d'), $end->format('Y-m-d'), $diff));
if ($diff >= 1.001 && $diff < 12.001) {
// Log::debug(sprintf('Return Y-m because %s', $diff));
$format = 'Y-m';
}
if ($diff >= 12.001) {
// Log::debug(sprintf('Return Y because %s', $diff));
return 'Y';
}
return $format;
}
public function periodShow(Carbon $theDate, string $repeatFrequency): string
{
$date = clone $theDate;
$formatMap = [
'1D' => (string) trans('config.specific_day_js'),
'daily' => (string) trans('config.specific_day_js'),
'custom' => (string) trans('config.specific_day_js'),
'1W' => (string) trans('config.week_in_year_js'),
'week' => (string) trans('config.week_in_year_js'),
'weekly' => (string) trans('config.week_in_year_js'),
'1M' => (string) trans('config.month_js'),
'month' => (string) trans('config.month_js'),
'monthly' => (string) trans('config.month_js'),
'1Y' => (string) trans('config.year_js'),
'year' => (string) trans('config.year_js'),
'yearly' => (string) trans('config.year_js'),
'6M' => (string) trans('config.half_year_js'),
];
if (array_key_exists($repeatFrequency, $formatMap)) {
return $date->isoFormat($formatMap[$repeatFrequency]);
}
if ('3M' === $repeatFrequency || 'quarter' === $repeatFrequency) {
$quarter = ceil($theDate->month / 3);
return sprintf('Q%d %d', $quarter, $theDate->year);
}
// special formatter for quarter of year
Log::error(sprintf('No date formats for frequency "%s"!', $repeatFrequency));
return $date->format('Y-m-d');
}
/**
* Same as preferredCarbonFormat but by string
*/
public function preferredCarbonFormatByPeriod(string $period): string
{
return match ($period) {
default => 'Y-m-d',
// '1D' => 'Y-m-d',
'1W' => '\WW,Y',
'1M' => 'Y-m',
'3M', '6M' => '\QQ,Y',
'1Y' => 'Y',
};
}
/**
* If the date difference between start and end is less than a month, method returns trans(config.month_and_day).
* If the difference is less than a year, method returns "config.month". If the date difference is larger, method
* returns "config.year".
*/
public function preferredCarbonLocalizedFormat(Carbon $start, Carbon $end): string
{
$locale = app('steam')->getLocale();
$diff = $start->diffInMonths($end, true);
if ($diff >= 1.001 && $diff < 12.001) {
return (string) trans('config.month_js', [], $locale);
}
if ($diff >= 12.001) {
return (string) trans('config.year_js', [], $locale);
}
return (string) trans('config.month_and_day_js', [], $locale);
}
/**
* If the date difference between start and end is less than a month, method returns "endOfDay". If the difference
* is less than a year, method returns "endOfMonth". If the date difference is larger, method returns "endOfYear".
*/
public function preferredEndOfPeriod(Carbon $start, Carbon $end): string
{
$diff = $start->diffInMonths($end, true);
if ($diff >= 1.001 && $diff < 12.001) {
return 'endOfMonth';
}
if ($diff >= 12.001) {
return 'endOfYear';
}
return 'endOfDay';
}
/**
* If the date difference between start and end is less than a month, method returns "1D". If the difference is
* less than a year, method returns "1M". If the date difference is larger, method returns "1Y".
*/
public function preferredRangeFormat(Carbon $start, Carbon $end): string
{
$diff = $start->diffInMonths($end, true);
if ($diff >= 1.001 && $diff < 12.001) {
return '1M';
}
if ($diff >= 12.001) {
return '1Y';
}
return '1D';
}
/**
* If the date difference between start and end is less than a month, method returns "%Y-%m-%d". If the difference
* is less than a year, method returns "%Y-%m". If the date difference is larger, method returns "%Y".
*/
public function preferredSqlFormat(Carbon $start, Carbon $end): string
{
$diff = $start->diffInMonths($end, true);
if ($diff >= 1.001 && $diff < 12.001) {
return '%Y-%m';
}
if ($diff >= 12.001) {
return '%Y';
}
return '%Y-%m-%d';
}
/**
* @throws FireflyException
*/
@@ -680,7 +682,7 @@ class Navigation
/** @var Carbon $tEnd */
$tEnd = session('end', today(config('app.timezone'))->endOfMonth());
$diffInDays = (int) $tStart->diffInDays($tEnd, true);
$diffInDays = (int)$tStart->diffInDays($tEnd, true);
$date->subDays($diffInDays * $subtract);
return $date;

View File

@@ -39,6 +39,92 @@ use Spatie\Period\Precision;
trait RecalculatesAvailableBudgetsTrait
{
private function calculateAmount(AvailableBudget $availableBudget): void
{
$repository = app(BudgetLimitRepositoryInterface::class);
$repository->setUser($availableBudget->user);
$newAmount = '0';
$abPeriod = Period::make($availableBudget->start_date, $availableBudget->end_date, Precision::DAY());
Log::debug(
sprintf(
'Now at AB #%d, ("%s" to "%s")',
$availableBudget->id,
$availableBudget->start_date->format('Y-m-d'),
$availableBudget->end_date->format('Y-m-d')
)
);
// have to recalculate everything just in case.
$set = $repository->getAllBudgetLimitsByCurrency($availableBudget->transactionCurrency, $availableBudget->start_date, $availableBudget->end_date);
Log::debug(sprintf('Found %d interesting budget limit(s).', $set->count()));
/** @var BudgetLimit $budgetLimit */
foreach ($set as $budgetLimit) {
Log::debug(
sprintf(
'Found interesting budget limit #%d ("%s" to "%s")',
$budgetLimit->id,
$budgetLimit->start_date->format('Y-m-d'),
$budgetLimit->end_date->format('Y-m-d')
)
);
// overlap in days:
$limitPeriod = Period::make(
$budgetLimit->start_date,
$budgetLimit->end_date,
precision : Precision::DAY(),
boundaries: Boundaries::EXCLUDE_NONE()
);
// if both equal each other, amount from this BL must be added to the AB
if ($limitPeriod->equals($abPeriod)) {
Log::debug('This budget limit is equal to the available budget period.');
$newAmount = bcadd($newAmount, (string)$budgetLimit->amount);
}
// if budget limit period is inside AB period, it can be added in full.
if (!$limitPeriod->equals($abPeriod) && $abPeriod->contains($limitPeriod)) {
Log::debug('This budget limit is smaller than the available budget period.');
$newAmount = bcadd($newAmount, (string)$budgetLimit->amount);
}
if (!$limitPeriod->equals($abPeriod) && !$abPeriod->contains($limitPeriod) && $abPeriod->overlapsWith($limitPeriod)) {
Log::debug('This budget limit is something else entirely!');
$overlap = $abPeriod->overlap($limitPeriod);
if ($overlap instanceof Period) {
$length = $overlap->length();
$daily = bcmul($this->getDailyAmount($budgetLimit), (string)$length);
$newAmount = bcadd($newAmount, $daily);
}
}
}
if (0 === bccomp('0', $newAmount)) {
Log::debug('New amount is zero, deleting AB.');
$availableBudget->delete();
return;
}
Log::debug(sprintf('Concluded new amount for this AB must be %s', $newAmount));
$availableBudget->amount = app('steam')->bcround($newAmount, $availableBudget->transactionCurrency->decimal_places);
$availableBudget->save();
}
private function getDailyAmount(BudgetLimit $budgetLimit): string
{
if (0 === $budgetLimit->id) {
return '0';
}
$limitPeriod = Period::make(
$budgetLimit->start_date,
$budgetLimit->end_date,
precision : Precision::DAY(),
boundaries: Boundaries::EXCLUDE_NONE()
);
$days = $limitPeriod->length();
$amount = bcdiv($budgetLimit->amount, (string)$days, 12);
Log::debug(
sprintf('Total amount for budget limit #%d is %s. Nr. of days is %d. Amount per day is %s', $budgetLimit->id, $budgetLimit->amount, $days, $amount)
);
return $amount;
}
private function updateAvailableBudget(BudgetLimit $budgetLimit): void
{
Log::debug(sprintf('Now in updateAvailableBudget(limit #%d)', $budgetLimit->id));
@@ -83,7 +169,7 @@ trait RecalculatesAvailableBudgetsTrait
if (null === $viewRange || is_array($viewRange)) {
$viewRange = '1M';
}
$viewRange = (string) $viewRange;
$viewRange = (string)$viewRange;
$start = app('navigation')->startOfPeriod($budgetLimit->start_date, $viewRange);
$end = app('navigation')->startOfPeriod($budgetLimit->end_date, $viewRange);
@@ -111,7 +197,7 @@ trait RecalculatesAvailableBudgetsTrait
// if not exists:
$currentPeriod = Period::make($current, $currentEnd, precision: Precision::DAY(), boundaries: Boundaries::EXCLUDE_NONE());
$daily = $this->getDailyAmount($budgetLimit);
$amount = bcmul($daily, (string) $currentPeriod->length(), 12);
$amount = bcmul($daily, (string)$currentPeriod->length(), 12);
// no need to calculate if period is equal.
if ($currentPeriod->equals($limitPeriod)) {
@@ -144,90 +230,4 @@ trait RecalculatesAvailableBudgetsTrait
$current = app('navigation')->addPeriod($current, $viewRange, 0);
}
}
private function calculateAmount(AvailableBudget $availableBudget): void
{
$repository = app(BudgetLimitRepositoryInterface::class);
$repository->setUser($availableBudget->user);
$newAmount = '0';
$abPeriod = Period::make($availableBudget->start_date, $availableBudget->end_date, Precision::DAY());
Log::debug(
sprintf(
'Now at AB #%d, ("%s" to "%s")',
$availableBudget->id,
$availableBudget->start_date->format('Y-m-d'),
$availableBudget->end_date->format('Y-m-d')
)
);
// have to recalculate everything just in case.
$set = $repository->getAllBudgetLimitsByCurrency($availableBudget->transactionCurrency, $availableBudget->start_date, $availableBudget->end_date);
Log::debug(sprintf('Found %d interesting budget limit(s).', $set->count()));
/** @var BudgetLimit $budgetLimit */
foreach ($set as $budgetLimit) {
Log::debug(
sprintf(
'Found interesting budget limit #%d ("%s" to "%s")',
$budgetLimit->id,
$budgetLimit->start_date->format('Y-m-d'),
$budgetLimit->end_date->format('Y-m-d')
)
);
// overlap in days:
$limitPeriod = Period::make(
$budgetLimit->start_date,
$budgetLimit->end_date,
precision : Precision::DAY(),
boundaries: Boundaries::EXCLUDE_NONE()
);
// if both equal each other, amount from this BL must be added to the AB
if ($limitPeriod->equals($abPeriod)) {
Log::debug('This budget limit is equal to the available budget period.');
$newAmount = bcadd($newAmount, (string) $budgetLimit->amount);
}
// if budget limit period is inside AB period, it can be added in full.
if (!$limitPeriod->equals($abPeriod) && $abPeriod->contains($limitPeriod)) {
Log::debug('This budget limit is smaller than the available budget period.');
$newAmount = bcadd($newAmount, (string) $budgetLimit->amount);
}
if (!$limitPeriod->equals($abPeriod) && !$abPeriod->contains($limitPeriod) && $abPeriod->overlapsWith($limitPeriod)) {
Log::debug('This budget limit is something else entirely!');
$overlap = $abPeriod->overlap($limitPeriod);
if ($overlap instanceof Period) {
$length = $overlap->length();
$daily = bcmul($this->getDailyAmount($budgetLimit), (string) $length);
$newAmount = bcadd($newAmount, $daily);
}
}
}
if (0 === bccomp('0', $newAmount)) {
Log::debug('New amount is zero, deleting AB.');
$availableBudget->delete();
return;
}
Log::debug(sprintf('Concluded new amount for this AB must be %s', $newAmount));
$availableBudget->amount = app('steam')->bcround($newAmount, $availableBudget->transactionCurrency->decimal_places);
$availableBudget->save();
}
private function getDailyAmount(BudgetLimit $budgetLimit): string
{
if (0 === $budgetLimit->id) {
return '0';
}
$limitPeriod = Period::make(
$budgetLimit->start_date,
$budgetLimit->end_date,
precision : Precision::DAY(),
boundaries: Boundaries::EXCLUDE_NONE()
);
$days = $limitPeriod->length();
$amount = bcdiv($budgetLimit->amount, (string) $days, 12);
Log::debug(
sprintf('Total amount for budget limit #%d is %s. Nr. of days is %d. Amount per day is %s', $budgetLimit->id, $budgetLimit->amount, $days, $amount)
);
return $amount;
}
}

View File

@@ -114,99 +114,13 @@ class ParseDateString
return new Carbon('1984-09-17');
}
// maybe a year, nothing else?
if (4 === strlen($date) && is_numeric($date) && (int) $date > 1000 && (int) $date <= 3000) {
if (4 === strlen($date) && is_numeric($date) && (int)$date > 1000 && (int)$date <= 3000) {
return new Carbon(sprintf('%d-01-01', $date));
}
throw new FireflyException(sprintf('[d] Not a recognised date format: "%s"', $date));
}
protected function parseKeyword(string $keyword): Carbon
{
$today = today(config('app.timezone'))->startOfDay();
return match ($keyword) {
default => $today,
'yesterday' => $today->subDay(),
'tomorrow' => $today->addDay(),
'start of this week' => $today->startOfWeek(CarbonInterface::MONDAY),
'end of this week' => $today->endOfWeek(CarbonInterface::SUNDAY),
'start of this month' => $today->startOfMonth(),
'end of this month' => $today->endOfMonth(),
'start of this quarter' => $today->startOfQuarter(),
'end of this quarter' => $today->endOfQuarter(),
'start of this year' => $today->startOfYear(),
'end of this year' => $today->endOfYear(),
};
}
protected function parseDefaultDate(string $date): Carbon
{
$result = false;
try {
$result = Carbon::createFromFormat('Y-m-d', $date);
} catch (InvalidFormatException $e) {
Log::error(sprintf('parseDefaultDate("%s") ran into an error, but dont mind: %s', $date, $e->getMessage()));
}
if (false === $result) {
return today(config('app.timezone'))->startOfDay();
}
return $result;
}
protected function parseRelativeDate(string $date): Carbon
{
Log::debug(sprintf('Now in parseRelativeDate("%s")', $date));
$parts = explode(' ', $date);
$today = today(config('app.timezone'))->startOfDay();
$functions = [
[
'd' => 'subDays',
'w' => 'subWeeks',
'm' => 'subMonths',
'q' => 'subQuarters',
'y' => 'subYears',
],
[
'd' => 'addDays',
'w' => 'addWeeks',
'm' => 'addMonths',
'q' => 'addQuarters',
'y' => 'addYears',
],
];
foreach ($parts as $part) {
Log::debug(sprintf('Now parsing part "%s"', $part));
$part = trim($part);
// verify if correct
$pattern = '/[+-]\d+[wqmdy]/';
$result = preg_match($pattern, $part);
if (0 === $result) {
Log::error(sprintf('Part "%s" does not match regular expression. Will be skipped.', $part));
continue;
}
$direction = str_starts_with($part, '+') ? 1 : 0;
$period = $part[strlen($part) - 1];
$number = (int) substr($part, 1, -1);
if (!array_key_exists($period, $functions[$direction])) {
Log::error(sprintf('No method for direction %d and period "%s".', $direction, $period));
continue;
}
$func = $functions[$direction][$period];
Log::debug(sprintf('Will now do %s(%d) on %s', $func, $number, $today->format('Y-m-d')));
$today->{$func}($number); // @phpstan-ignore-line
Log::debug(sprintf('Resulting date is %s', $today->format('Y-m-d')));
}
return $today;
}
public function parseRange(string $date): array
{
// several types of range can be submitted
@@ -269,16 +183,34 @@ class ParseDateString
return false;
}
/**
* format of string is xxxx-xx-DD
*/
protected function parseDayRange(string $date): array
protected function isDayYearRange(string $date): bool
{
$parts = explode('-', $date);
// if regex for YYYY-xx-DD:
$pattern = '/^(19|20)\d\d-xx-(0[1-9]|[12]\d|3[01])$/';
$result = preg_match($pattern, $date);
if (0 !== $result) {
Log::debug(sprintf('"%s" is a day/year range.', $date));
return [
'day' => $parts[2],
];
return true;
}
Log::debug(sprintf('"%s" is not a day/year range.', $date));
return false;
}
protected function isMonthDayRange(string $date): bool
{
// if regex for xxxx-MM-DD:
$pattern = '/^xxxx-(0[1-9]|1[012])-(0[1-9]|[12]\d|3[01])$/';
$result = preg_match($pattern, $date);
if (0 !== $result) {
Log::debug(sprintf('"%s" is a month/day range.', $date));
return true;
}
Log::debug(sprintf('"%s" is not a month/day range.', $date));
return false;
}
protected function isMonthRange(string $date): bool
@@ -296,17 +228,19 @@ class ParseDateString
return false;
}
/**
* format of string is xxxx-MM-xx
*/
protected function parseMonthRange(string $date): array
protected function isMonthYearRange(string $date): bool
{
Log::debug(sprintf('parseMonthRange: Parsed "%s".', $date));
$parts = explode('-', $date);
// if regex for YYYY-MM-xx:
$pattern = '/^(19|20)\d\d-(0[1-9]|1[012])-xx$/';
$result = preg_match($pattern, $date);
if (0 !== $result) {
Log::debug(sprintf('"%s" is a month/year range.', $date));
return [
'month' => $parts[1],
];
return true;
}
Log::debug(sprintf('"%s" is not a month/year range.', $date));
return false;
}
protected function isYearRange(string $date): bool
@@ -324,6 +258,131 @@ class ParseDateString
return false;
}
/**
* format of string is xxxx-xx-DD
*/
protected function parseDayRange(string $date): array
{
$parts = explode('-', $date);
return [
'day' => $parts[2],
];
}
protected function parseDefaultDate(string $date): Carbon
{
$result = false;
try {
$result = Carbon::createFromFormat('Y-m-d', $date);
} catch (InvalidFormatException $e) {
Log::error(sprintf('parseDefaultDate("%s") ran into an error, but dont mind: %s', $date, $e->getMessage()));
}
if (false === $result) {
return today(config('app.timezone'))->startOfDay();
}
return $result;
}
protected function parseKeyword(string $keyword): Carbon
{
$today = today(config('app.timezone'))->startOfDay();
return match ($keyword) {
default => $today,
'yesterday' => $today->subDay(),
'tomorrow' => $today->addDay(),
'start of this week' => $today->startOfWeek(CarbonInterface::MONDAY),
'end of this week' => $today->endOfWeek(CarbonInterface::SUNDAY),
'start of this month' => $today->startOfMonth(),
'end of this month' => $today->endOfMonth(),
'start of this quarter' => $today->startOfQuarter(),
'end of this quarter' => $today->endOfQuarter(),
'start of this year' => $today->startOfYear(),
'end of this year' => $today->endOfYear(),
};
}
/**
* format of string is xxxx-MM-xx
*/
protected function parseMonthRange(string $date): array
{
Log::debug(sprintf('parseMonthRange: Parsed "%s".', $date));
$parts = explode('-', $date);
return [
'month' => $parts[1],
];
}
/**
* format of string is YYYY-MM-xx
*/
protected function parseMonthYearRange(string $date): array
{
Log::debug(sprintf('parseMonthYearRange: Parsed "%s".', $date));
$parts = explode('-', $date);
return [
'year' => $parts[0],
'month' => $parts[1],
];
}
protected function parseRelativeDate(string $date): Carbon
{
Log::debug(sprintf('Now in parseRelativeDate("%s")', $date));
$parts = explode(' ', $date);
$today = today(config('app.timezone'))->startOfDay();
$functions = [
[
'd' => 'subDays',
'w' => 'subWeeks',
'm' => 'subMonths',
'q' => 'subQuarters',
'y' => 'subYears',
],
[
'd' => 'addDays',
'w' => 'addWeeks',
'm' => 'addMonths',
'q' => 'addQuarters',
'y' => 'addYears',
],
];
foreach ($parts as $part) {
Log::debug(sprintf('Now parsing part "%s"', $part));
$part = trim($part);
// verify if correct
$pattern = '/[+-]\d+[wqmdy]/';
$result = preg_match($pattern, $part);
if (0 === $result) {
Log::error(sprintf('Part "%s" does not match regular expression. Will be skipped.', $part));
continue;
}
$direction = str_starts_with($part, '+') ? 1 : 0;
$period = $part[strlen($part) - 1];
$number = (int)substr($part, 1, -1);
if (!array_key_exists($period, $functions[$direction])) {
Log::error(sprintf('No method for direction %d and period "%s".', $direction, $period));
continue;
}
$func = $functions[$direction][$period];
Log::debug(sprintf('Will now do %s(%d) on %s', $func, $number, $today->format('Y-m-d')));
$today->{$func}($number); // @phpstan-ignore-line
Log::debug(sprintf('Resulting date is %s', $today->format('Y-m-d')));
}
return $today;
}
/**
* format of string is YYYY-xx-xx
*/
@@ -337,50 +396,6 @@ class ParseDateString
];
}
protected function isMonthDayRange(string $date): bool
{
// if regex for xxxx-MM-DD:
$pattern = '/^xxxx-(0[1-9]|1[012])-(0[1-9]|[12]\d|3[01])$/';
$result = preg_match($pattern, $date);
if (0 !== $result) {
Log::debug(sprintf('"%s" is a month/day range.', $date));
return true;
}
Log::debug(sprintf('"%s" is not a month/day range.', $date));
return false;
}
/**
* format of string is xxxx-MM-DD
*/
private function parseMonthDayRange(string $date): array
{
Log::debug(sprintf('parseMonthDayRange: Parsed "%s".', $date));
$parts = explode('-', $date);
return [
'month' => $parts[1],
'day' => $parts[2],
];
}
protected function isDayYearRange(string $date): bool
{
// if regex for YYYY-xx-DD:
$pattern = '/^(19|20)\d\d-xx-(0[1-9]|[12]\d|3[01])$/';
$result = preg_match($pattern, $date);
if (0 !== $result) {
Log::debug(sprintf('"%s" is a day/year range.', $date));
return true;
}
Log::debug(sprintf('"%s" is not a day/year range.', $date));
return false;
}
/**
* format of string is YYYY-xx-DD
*/
@@ -395,32 +410,17 @@ class ParseDateString
];
}
protected function isMonthYearRange(string $date): bool
{
// if regex for YYYY-MM-xx:
$pattern = '/^(19|20)\d\d-(0[1-9]|1[012])-xx$/';
$result = preg_match($pattern, $date);
if (0 !== $result) {
Log::debug(sprintf('"%s" is a month/year range.', $date));
return true;
}
Log::debug(sprintf('"%s" is not a month/year range.', $date));
return false;
}
/**
* format of string is YYYY-MM-xx
* format of string is xxxx-MM-DD
*/
protected function parseMonthYearRange(string $date): array
private function parseMonthDayRange(string $date): array
{
Log::debug(sprintf('parseMonthYearRange: Parsed "%s".', $date));
Log::debug(sprintf('parseMonthDayRange: Parsed "%s".', $date));
$parts = explode('-', $date);
return [
'year' => $parts[0],
'month' => $parts[1],
'day' => $parts[2],
];
}
}

View File

@@ -57,64 +57,11 @@ class Preferences
;
}
public function get(string $name, array|bool|int|string|null $default = null): ?Preference
public function beginsWith(User $user, string $search): Collection
{
/** @var null|User $user */
$user = auth()->user();
if (null === $user) {
$preference = new Preference();
$preference->data = $default;
$value = sprintf('%s%%', $search);
return $preference;
}
return $this->getForUser($user, $name, $default);
}
public function getForUser(User $user, string $name, array|bool|int|string|null $default = null): ?Preference
{
// Log::debug(sprintf('getForUser(#%d, "%s")', $user->id, $name));
// don't care about user group ID, except for some specific preferences.
$userGroupId = $this->getUserGroupId($user, $name);
$query = Preference::where('user_id', $user->id)->where('name', $name);
if (null !== $userGroupId) {
Log::debug('Include user group ID in query');
$query->where('user_group_id', $userGroupId);
}
$preference = $query->first(['id', 'user_id', 'user_group_id', 'name', 'data', 'updated_at', 'created_at']);
if (null !== $preference && null === $preference->data) {
$preference->delete();
$preference = null;
Log::debug('Removed empty preference.');
}
if (null !== $preference) {
// Log::debug(sprintf('Found preference #%d for user #%d: %s', $preference->id, $user->id, $name));
return $preference;
}
// no preference found and default is null:
if (null === $default) {
Log::debug('Return NULL, create no preference.');
// return NULL
return null;
}
return $this->setForUser($user, $name, $default);
}
private function getUserGroupId(User $user, string $preferenceName): ?int
{
$groupId = null;
$items = config('firefly.admin_specific_prefs') ?? [];
if (in_array($preferenceName, $items, true)) {
return (int) $user->user_group_id;
}
return $groupId;
return Preference::where('user_id', $user->id)->whereLike('name', $value)->get();
}
public function delete(string $name): bool
@@ -128,6 +75,14 @@ class Preferences
return true;
}
/**
* Find by name, has no user ID in it, because the method is called from an unauthenticated route any way.
*/
public function findByName(string $name): Collection
{
return Preference::where('name', $name)->get();
}
public function forget(User $user, string $name): void
{
$key = sprintf('preference%s%s', $user->id, $name);
@@ -135,57 +90,18 @@ class Preferences
Cache::put($key, '', 5);
}
public function setForUser(User $user, string $name, array|bool|int|string|null $value): Preference
public function get(string $name, array|bool|int|string|null $default = null): ?Preference
{
$fullName = sprintf('preference%s%s', $user->id, $name);
$userGroupId = $this->getUserGroupId($user, $name);
$userGroupId = 0 === (int) $userGroupId ? null : (int) $userGroupId;
/** @var null|User $user */
$user = auth()->user();
if (null === $user) {
$preference = new Preference();
$preference->data = $default;
Cache::forget($fullName);
$query = Preference::where('user_id', $user->id)->where('name', $name);
if (null !== $userGroupId) {
Log::debug('Include user group ID in query');
$query->where('user_group_id', $userGroupId);
return $preference;
}
$preference = $query->first(['id', 'user_id', 'user_group_id', 'name', 'data', 'updated_at', 'created_at']);
if (null !== $preference && null === $value) {
$preference->delete();
return new Preference();
}
if (null === $value) {
return new Preference();
}
if (null === $preference) {
$preference = new Preference();
$preference->user_id = (int) $user->id;
$preference->user_group_id = $userGroupId;
$preference->name = $name;
}
$preference->data = $value;
$preference->save();
Cache::forever($fullName, $preference);
return $preference;
}
public function beginsWith(User $user, string $search): Collection
{
$value = sprintf('%s%%', $search);
return Preference::where('user_id', $user->id)->whereLike('name', $value)->get();
}
/**
* Find by name, has no user ID in it, because the method is called from an unauthenticated route any way.
*/
public function findByName(string $name): Collection
{
return Preference::where('name', $name)->get();
return $this->getForUser($user, $name, $default);
}
public function getArrayForUser(User $user, array $list): array
@@ -265,6 +181,41 @@ class Preferences
return $result;
}
public function getForUser(User $user, string $name, array|bool|int|string|null $default = null): ?Preference
{
// Log::debug(sprintf('getForUser(#%d, "%s")', $user->id, $name));
// don't care about user group ID, except for some specific preferences.
$userGroupId = $this->getUserGroupId($user, $name);
$query = Preference::where('user_id', $user->id)->where('name', $name);
if (null !== $userGroupId) {
Log::debug('Include user group ID in query');
$query->where('user_group_id', $userGroupId);
}
$preference = $query->first(['id', 'user_id', 'user_group_id', 'name', 'data', 'updated_at', 'created_at']);
if (null !== $preference && null === $preference->data) {
$preference->delete();
$preference = null;
Log::debug('Removed empty preference.');
}
if (null !== $preference) {
// Log::debug(sprintf('Found preference #%d for user #%d: %s', $preference->id, $user->id, $name));
return $preference;
}
// no preference found and default is null:
if (null === $default) {
Log::debug('Return NULL, create no preference.');
// return NULL
return null;
}
return $this->setForUser($user, $name, $default);
}
public function getFresh(string $name, array|bool|int|string|null $default = null): ?Preference
{
/** @var null|User $user */
@@ -299,7 +250,7 @@ class Preferences
if (is_array($lastActivity)) {
$lastActivity = implode(',', $lastActivity);
}
$setting = hash('sha256', (string) $lastActivity);
$setting = hash('sha256', (string)$lastActivity);
$instance->setPreference('last_activity', $setting);
return $setting;
@@ -341,4 +292,53 @@ class Preferences
return $this->set($name, $encrypted);
}
public function setForUser(User $user, string $name, array|bool|int|string|null $value): Preference
{
$fullName = sprintf('preference%s%s', $user->id, $name);
$userGroupId = $this->getUserGroupId($user, $name);
$userGroupId = 0 === (int)$userGroupId ? null : (int)$userGroupId;
Cache::forget($fullName);
$query = Preference::where('user_id', $user->id)->where('name', $name);
if (null !== $userGroupId) {
Log::debug('Include user group ID in query');
$query->where('user_group_id', $userGroupId);
}
$preference = $query->first(['id', 'user_id', 'user_group_id', 'name', 'data', 'updated_at', 'created_at']);
if (null !== $preference && null === $value) {
$preference->delete();
return new Preference();
}
if (null === $value) {
return new Preference();
}
if (null === $preference) {
$preference = new Preference();
$preference->user_id = (int)$user->id;
$preference->user_group_id = $userGroupId;
$preference->name = $name;
}
$preference->data = $value;
$preference->save();
Cache::forever($fullName, $preference);
return $preference;
}
private function getUserGroupId(User $user, string $preferenceName): ?int
{
$groupId = null;
$items = config('firefly.admin_specific_prefs') ?? [];
if (in_array($preferenceName, $items, true)) {
return (int)$user->user_group_id;
}
return $groupId;
}
}

View File

@@ -91,43 +91,6 @@ class BudgetReportGenerator
}
}
/**
* Process each row of expenses collected for the "Account per budget" partial
*/
private function processExpenses(array $expenses): void
{
foreach ($expenses['budgets'] as $budget) {
$this->processBudgetExpenses($expenses, $budget);
}
}
/**
* Process each set of transactions for each row of expenses.
*/
private function processBudgetExpenses(array $expenses, array $budget): void
{
$budgetId = (int) $budget['id'];
$currencyId = (int) $expenses['currency_id'];
foreach ($budget['transaction_journals'] as $journal) {
$sourceAccountId = $journal['source_account_id'];
$this->report[$sourceAccountId]['currencies'][$currencyId]
??= [
'currency_id' => $expenses['currency_id'],
'currency_symbol' => $expenses['currency_symbol'],
'currency_name' => $expenses['currency_name'],
'currency_decimal_places' => $expenses['currency_decimal_places'],
'budgets' => [],
];
$this->report[$sourceAccountId]['currencies'][$currencyId]['budgets'][$budgetId]
??= '0';
$this->report[$sourceAccountId]['currencies'][$currencyId]['budgets'][$budgetId]
= bcadd($this->report[$sourceAccountId]['currencies'][$currencyId]['budgets'][$budgetId], (string) $journal['amount']);
}
}
/**
* Generates the data necessary to create the card that displays
* the budget overview in the general report.
@@ -144,6 +107,43 @@ class BudgetReportGenerator
$this->percentageReport();
}
public function getReport(): array
{
return $this->report;
}
public function setAccounts(Collection $accounts): void
{
$this->accounts = $accounts;
}
public function setBudgets(Collection $budgets): void
{
$this->budgets = $budgets;
}
public function setEnd(Carbon $end): void
{
$this->end = $end;
}
public function setStart(Carbon $start): void
{
$this->start = $start;
}
/**
* @throws FireflyException
*/
public function setUser(User $user): void
{
$this->repository->setUser($user);
$this->blRepository->setUser($user);
$this->opsRepository->setUser($user);
$this->nbRepository->setUser($user);
$this->currency = app('amount')->getPrimaryCurrencyByUserGroup($user->userGroup);
}
/**
* Start the budgets block on the default report by processing every budget.
*/
@@ -157,6 +157,90 @@ class BudgetReportGenerator
}
}
/**
* Calculate the expenses for transactions without a budget. Part of the "budgets" block of the default report.
*/
private function noBudgetReport(): void
{
// add no budget info.
$this->report['budgets'][0] = [
'budget_id' => null,
'budget_name' => null,
'no_budget' => true,
'budget_limits' => [],
];
$noBudget = $this->nbRepository->sumExpenses($this->start, $this->end, $this->accounts);
foreach ($noBudget as $noBudgetEntry) {
// currency information:
$nbCurrencyId = (int)($noBudgetEntry['currency_id'] ?? $this->currency->id);
$nbCurrencyCode = $noBudgetEntry['currency_code'] ?? $this->currency->code;
$nbCurrencyName = $noBudgetEntry['currency_name'] ?? $this->currency->name;
$nbCurrencySymbol = $noBudgetEntry['currency_symbol'] ?? $this->currency->symbol;
$nbCurrencyDp = $noBudgetEntry['currency_decimal_places'] ?? $this->currency->decimal_places;
$this->report['budgets'][0]['budget_limits'][] = [
'budget_limit_id' => null,
'start_date' => $this->start,
'end_date' => $this->end,
'budgeted' => '0',
'budgeted_pct' => '0',
'spent' => $noBudgetEntry['sum'],
'spent_pct' => '0',
'left' => '0',
'overspent' => '0',
'currency_id' => $nbCurrencyId,
'currency_code' => $nbCurrencyCode,
'currency_name' => $nbCurrencyName,
'currency_symbol' => $nbCurrencySymbol,
'currency_decimal_places' => $nbCurrencyDp,
];
$this->report['sums'][$nbCurrencyId]['spent'] = bcadd($this->report['sums'][$nbCurrencyId]['spent'] ?? '0', (string)$noBudgetEntry['sum']);
// append currency info because it may be missing:
$this->report['sums'][$nbCurrencyId]['currency_id'] = $nbCurrencyId;
$this->report['sums'][$nbCurrencyId]['currency_code'] = $nbCurrencyCode;
$this->report['sums'][$nbCurrencyId]['currency_name'] = $nbCurrencyName;
$this->report['sums'][$nbCurrencyId]['currency_symbol'] = $nbCurrencySymbol;
$this->report['sums'][$nbCurrencyId]['currency_decimal_places'] = $nbCurrencyDp;
// append other sums because they might be missing:
$this->report['sums'][$nbCurrencyId]['overspent'] ??= '0';
$this->report['sums'][$nbCurrencyId]['left'] ??= '0';
$this->report['sums'][$nbCurrencyId]['budgeted'] ??= '0';
}
}
/**
* Calculate the percentages for each budget. Part of the "budgets" block on the default report.
*/
private function percentageReport(): void
{
// make percentages based on total amount.
foreach ($this->report['budgets'] as $budgetId => $data) {
foreach ($data['budget_limits'] as $limitId => $entry) {
$budgetId = (int)$budgetId;
$limitId = (int)$limitId;
$currencyId = (int)$entry['currency_id'];
$spent = $entry['spent'];
$totalSpent = $this->report['sums'][$currencyId]['spent'] ?? '0';
$spentPct = '0';
$budgeted = $entry['budgeted'];
$totalBudgeted = $this->report['sums'][$currencyId]['budgeted'] ?? '0';
$budgetedPct = '0';
if (0 !== bccomp((string)$spent, '0') && 0 !== bccomp($totalSpent, '0')) {
$spentPct = round((float)bcmul(bcdiv((string)$spent, $totalSpent), '100'));
}
if (0 !== bccomp((string)$budgeted, '0') && 0 !== bccomp($totalBudgeted, '0')) {
$budgetedPct = round((float)bcmul(bcdiv((string)$budgeted, $totalBudgeted), '100'));
}
$this->report['sums'][$currencyId]['budgeted'] ??= '0';
$this->report['budgets'][$budgetId]['budget_limits'][$limitId]['spent_pct'] = $spentPct;
$this->report['budgets'][$budgetId]['budget_limits'][$limitId]['budgeted_pct'] = $budgetedPct;
}
}
}
/**
* Process expenses etc. for a single budget for the budgets block on the default report.
*/
@@ -179,6 +263,43 @@ class BudgetReportGenerator
}
}
/**
* Process each set of transactions for each row of expenses.
*/
private function processBudgetExpenses(array $expenses, array $budget): void
{
$budgetId = (int)$budget['id'];
$currencyId = (int)$expenses['currency_id'];
foreach ($budget['transaction_journals'] as $journal) {
$sourceAccountId = $journal['source_account_id'];
$this->report[$sourceAccountId]['currencies'][$currencyId]
??= [
'currency_id' => $expenses['currency_id'],
'currency_symbol' => $expenses['currency_symbol'],
'currency_name' => $expenses['currency_name'],
'currency_decimal_places' => $expenses['currency_decimal_places'],
'budgets' => [],
];
$this->report[$sourceAccountId]['currencies'][$currencyId]['budgets'][$budgetId]
??= '0';
$this->report[$sourceAccountId]['currencies'][$currencyId]['budgets'][$budgetId]
= bcadd($this->report[$sourceAccountId]['currencies'][$currencyId]['budgets'][$budgetId], (string)$journal['amount']);
}
}
/**
* Process each row of expenses collected for the "Account per budget" partial
*/
private function processExpenses(array $expenses): void
{
foreach ($expenses['budgets'] as $budget) {
$this->processBudgetExpenses($expenses, $budget);
}
}
/**
* Process a single budget limit for the budgets block on the default report.
*/
@@ -223,130 +344,9 @@ class BudgetReportGenerator
'currency_symbol' => $limitCurrency->symbol,
'currency_decimal_places' => $limitCurrency->decimal_places,
];
$this->report['sums'][$currencyId]['budgeted'] = bcadd((string) $this->report['sums'][$currencyId]['budgeted'], $limit->amount);
$this->report['sums'][$currencyId]['spent'] = bcadd((string) $this->report['sums'][$currencyId]['spent'], $spent);
$this->report['sums'][$currencyId]['left'] = bcadd((string) $this->report['sums'][$currencyId]['left'], bcadd($limit->amount, $spent));
$this->report['sums'][$currencyId]['overspent'] = bcadd((string) $this->report['sums'][$currencyId]['overspent'], $overspent);
}
/**
* Calculate the expenses for transactions without a budget. Part of the "budgets" block of the default report.
*/
private function noBudgetReport(): void
{
// add no budget info.
$this->report['budgets'][0] = [
'budget_id' => null,
'budget_name' => null,
'no_budget' => true,
'budget_limits' => [],
];
$noBudget = $this->nbRepository->sumExpenses($this->start, $this->end, $this->accounts);
foreach ($noBudget as $noBudgetEntry) {
// currency information:
$nbCurrencyId = (int) ($noBudgetEntry['currency_id'] ?? $this->currency->id);
$nbCurrencyCode = $noBudgetEntry['currency_code'] ?? $this->currency->code;
$nbCurrencyName = $noBudgetEntry['currency_name'] ?? $this->currency->name;
$nbCurrencySymbol = $noBudgetEntry['currency_symbol'] ?? $this->currency->symbol;
$nbCurrencyDp = $noBudgetEntry['currency_decimal_places'] ?? $this->currency->decimal_places;
$this->report['budgets'][0]['budget_limits'][] = [
'budget_limit_id' => null,
'start_date' => $this->start,
'end_date' => $this->end,
'budgeted' => '0',
'budgeted_pct' => '0',
'spent' => $noBudgetEntry['sum'],
'spent_pct' => '0',
'left' => '0',
'overspent' => '0',
'currency_id' => $nbCurrencyId,
'currency_code' => $nbCurrencyCode,
'currency_name' => $nbCurrencyName,
'currency_symbol' => $nbCurrencySymbol,
'currency_decimal_places' => $nbCurrencyDp,
];
$this->report['sums'][$nbCurrencyId]['spent'] = bcadd($this->report['sums'][$nbCurrencyId]['spent'] ?? '0', (string) $noBudgetEntry['sum']);
// append currency info because it may be missing:
$this->report['sums'][$nbCurrencyId]['currency_id'] = $nbCurrencyId;
$this->report['sums'][$nbCurrencyId]['currency_code'] = $nbCurrencyCode;
$this->report['sums'][$nbCurrencyId]['currency_name'] = $nbCurrencyName;
$this->report['sums'][$nbCurrencyId]['currency_symbol'] = $nbCurrencySymbol;
$this->report['sums'][$nbCurrencyId]['currency_decimal_places'] = $nbCurrencyDp;
// append other sums because they might be missing:
$this->report['sums'][$nbCurrencyId]['overspent'] ??= '0';
$this->report['sums'][$nbCurrencyId]['left'] ??= '0';
$this->report['sums'][$nbCurrencyId]['budgeted'] ??= '0';
}
}
/**
* Calculate the percentages for each budget. Part of the "budgets" block on the default report.
*/
private function percentageReport(): void
{
// make percentages based on total amount.
foreach ($this->report['budgets'] as $budgetId => $data) {
foreach ($data['budget_limits'] as $limitId => $entry) {
$budgetId = (int) $budgetId;
$limitId = (int) $limitId;
$currencyId = (int) $entry['currency_id'];
$spent = $entry['spent'];
$totalSpent = $this->report['sums'][$currencyId]['spent'] ?? '0';
$spentPct = '0';
$budgeted = $entry['budgeted'];
$totalBudgeted = $this->report['sums'][$currencyId]['budgeted'] ?? '0';
$budgetedPct = '0';
if (0 !== bccomp((string) $spent, '0') && 0 !== bccomp($totalSpent, '0')) {
$spentPct = round((float) bcmul(bcdiv((string) $spent, $totalSpent), '100'));
}
if (0 !== bccomp((string) $budgeted, '0') && 0 !== bccomp($totalBudgeted, '0')) {
$budgetedPct = round((float) bcmul(bcdiv((string) $budgeted, $totalBudgeted), '100'));
}
$this->report['sums'][$currencyId]['budgeted'] ??= '0';
$this->report['budgets'][$budgetId]['budget_limits'][$limitId]['spent_pct'] = $spentPct;
$this->report['budgets'][$budgetId]['budget_limits'][$limitId]['budgeted_pct'] = $budgetedPct;
}
}
}
public function getReport(): array
{
return $this->report;
}
public function setAccounts(Collection $accounts): void
{
$this->accounts = $accounts;
}
public function setBudgets(Collection $budgets): void
{
$this->budgets = $budgets;
}
public function setEnd(Carbon $end): void
{
$this->end = $end;
}
public function setStart(Carbon $start): void
{
$this->start = $start;
}
/**
* @throws FireflyException
*/
public function setUser(User $user): void
{
$this->repository->setUser($user);
$this->blRepository->setUser($user);
$this->opsRepository->setUser($user);
$this->nbRepository->setUser($user);
$this->currency = app('amount')->getPrimaryCurrencyByUserGroup($user->userGroup);
$this->report['sums'][$currencyId]['budgeted'] = bcadd((string)$this->report['sums'][$currencyId]['budgeted'], $limit->amount);
$this->report['sums'][$currencyId]['spent'] = bcadd((string)$this->report['sums'][$currencyId]['spent'], $spent);
$this->report['sums'][$currencyId]['left'] = bcadd((string)$this->report['sums'][$currencyId]['left'], bcadd($limit->amount, $spent));
$this->report['sums'][$currencyId]['overspent'] = bcadd((string)$this->report['sums'][$currencyId]['overspent'], $overspent);
}
}

View File

@@ -83,17 +83,69 @@ class CategoryReportGenerator
}
}
/**
* Process one of the spent arrays from the operations method.
*/
private function processOpsArray(array $data): void
public function setAccounts(Collection $accounts): void
{
/**
* @var int $currencyId
* @var array $currencyRow
*/
foreach ($data as $currencyId => $currencyRow) {
$this->processCurrencyArray($currencyId, $currencyRow);
$this->accounts = $accounts;
}
public function setEnd(Carbon $end): void
{
$this->end = $end;
}
public function setStart(Carbon $start): void
{
$this->start = $start;
}
public function setUser(User $user): void
{
$this->noCatRepository->setUser($user);
$this->opsRepository->setUser($user);
}
private function processCategoryRow(int $currencyId, array $currencyRow, int $categoryId, array $categoryRow): void
{
$key = sprintf('%s-%s', $currencyId, $categoryId);
$this->report['categories'][$key] ??= [
'id' => $categoryId,
'title' => $categoryRow['name'],
'currency_id' => $currencyRow['currency_id'],
'currency_symbol' => $currencyRow['currency_symbol'],
'currency_name' => $currencyRow['currency_name'],
'currency_code' => $currencyRow['currency_code'],
'currency_decimal_places' => $currencyRow['currency_decimal_places'],
'spent' => '0',
'earned' => '0',
'sum' => '0',
];
// loop journals:
foreach ($categoryRow['transaction_journals'] as $journal) {
// sum of sums
$this->report['sums'][$currencyId]['sum'] = bcadd((string)$this->report['sums'][$currencyId]['sum'], (string)$journal['amount']);
// sum of spent:
$this->report['sums'][$currencyId]['spent'] = -1 === bccomp((string)$journal['amount'], '0') ? bcadd(
(string)$this->report['sums'][$currencyId]['spent'],
(string)$journal['amount']
) : $this->report['sums'][$currencyId]['spent'];
// sum of earned
$this->report['sums'][$currencyId]['earned'] = 1 === bccomp((string)$journal['amount'], '0') ? bcadd(
(string)$this->report['sums'][$currencyId]['earned'],
(string)$journal['amount']
) : $this->report['sums'][$currencyId]['earned'];
// sum of category
$this->report['categories'][$key]['sum'] = bcadd((string)$this->report['categories'][$key]['sum'], (string)$journal['amount']);
// total spent in category
$this->report['categories'][$key]['spent'] = -1 === bccomp((string)$journal['amount'], '0') ? bcadd(
(string)$this->report['categories'][$key]['spent'],
(string)$journal['amount']
) : $this->report['categories'][$key]['spent'];
// total earned in category
$this->report['categories'][$key]['earned'] = 1 === bccomp((string)$journal['amount'], '0') ? bcadd(
(string)$this->report['categories'][$key]['earned'],
(string)$journal['amount']
) : $this->report['categories'][$key]['earned'];
}
}
@@ -119,69 +171,17 @@ class CategoryReportGenerator
}
}
private function processCategoryRow(int $currencyId, array $currencyRow, int $categoryId, array $categoryRow): void
/**
* Process one of the spent arrays from the operations method.
*/
private function processOpsArray(array $data): void
{
$key = sprintf('%s-%s', $currencyId, $categoryId);
$this->report['categories'][$key] ??= [
'id' => $categoryId,
'title' => $categoryRow['name'],
'currency_id' => $currencyRow['currency_id'],
'currency_symbol' => $currencyRow['currency_symbol'],
'currency_name' => $currencyRow['currency_name'],
'currency_code' => $currencyRow['currency_code'],
'currency_decimal_places' => $currencyRow['currency_decimal_places'],
'spent' => '0',
'earned' => '0',
'sum' => '0',
];
// loop journals:
foreach ($categoryRow['transaction_journals'] as $journal) {
// sum of sums
$this->report['sums'][$currencyId]['sum'] = bcadd((string) $this->report['sums'][$currencyId]['sum'], (string) $journal['amount']);
// sum of spent:
$this->report['sums'][$currencyId]['spent'] = -1 === bccomp((string) $journal['amount'], '0') ? bcadd(
(string) $this->report['sums'][$currencyId]['spent'],
(string) $journal['amount']
) : $this->report['sums'][$currencyId]['spent'];
// sum of earned
$this->report['sums'][$currencyId]['earned'] = 1 === bccomp((string) $journal['amount'], '0') ? bcadd(
(string) $this->report['sums'][$currencyId]['earned'],
(string) $journal['amount']
) : $this->report['sums'][$currencyId]['earned'];
// sum of category
$this->report['categories'][$key]['sum'] = bcadd((string) $this->report['categories'][$key]['sum'], (string) $journal['amount']);
// total spent in category
$this->report['categories'][$key]['spent'] = -1 === bccomp((string) $journal['amount'], '0') ? bcadd(
(string) $this->report['categories'][$key]['spent'],
(string) $journal['amount']
) : $this->report['categories'][$key]['spent'];
// total earned in category
$this->report['categories'][$key]['earned'] = 1 === bccomp((string) $journal['amount'], '0') ? bcadd(
(string) $this->report['categories'][$key]['earned'],
(string) $journal['amount']
) : $this->report['categories'][$key]['earned'];
/**
* @var int $currencyId
* @var array $currencyRow
*/
foreach ($data as $currencyId => $currencyRow) {
$this->processCurrencyArray($currencyId, $currencyRow);
}
}
public function setAccounts(Collection $accounts): void
{
$this->accounts = $accounts;
}
public function setEnd(Carbon $end): void
{
$this->end = $end;
}
public function setStart(Carbon $start): void
{
$this->start = $start;
}
public function setUser(User $user): void
{
$this->noCatRepository->setUser($user);
$this->opsRepository->setUser($user);
}
}

View File

@@ -43,13 +43,6 @@ class TransactionSummarizer
}
}
public function setUser(User $user): void
{
$this->user = $user;
$this->default = Amount::getPrimaryCurrencyByUserGroup($user->userGroup);
$this->convertToPrimary = Amount::convertToPrimary($user);
}
public function groupByCurrencyId(array $journals, string $method = 'negative', bool $includeForeign = true): array
{
Log::debug(sprintf('Now in groupByCurrencyId([%d journals], "%s", %s)', count($journals), $method, var_export($includeForeign, true)));
@@ -58,7 +51,7 @@ class TransactionSummarizer
$field = 'amount';
// grab default currency information.
$currencyId = (int) $journal['currency_id'];
$currencyId = (int)$journal['currency_id'];
$currencyName = $journal['currency_name'];
$currencySymbol = $journal['currency_symbol'];
$currencyCode = $journal['currency_code'];
@@ -74,8 +67,8 @@ class TransactionSummarizer
if ($this->convertToPrimary) {
// Log::debug('convertToPrimary is true.');
// if convert to primary currency, use the primary currency amount yes or no?
$usePrimary = $this->default->id !== (int) $journal['currency_id'];
$useForeign = $this->default->id === (int) $journal['foreign_currency_id'];
$usePrimary = $this->default->id !== (int)$journal['currency_id'];
$useForeign = $this->default->id === (int)$journal['foreign_currency_id'];
if ($usePrimary) {
// Log::debug(sprintf('Journal #%d switches to primary currency amount (original is %s)', $journal['transaction_journal_id'], $journal['currency_code']));
$field = 'pc_amount';
@@ -88,7 +81,7 @@ class TransactionSummarizer
if ($useForeign) {
// Log::debug(sprintf('Journal #%d switches to foreign amount (foreign is %s)', $journal['transaction_journal_id'], $journal['foreign_currency_code']));
$field = 'foreign_amount';
$currencyId = (int) $journal['foreign_currency_id'];
$currencyId = (int)$journal['foreign_currency_id'];
$currencyName = $journal['foreign_currency_name'];
$currencySymbol = $journal['foreign_currency_symbol'];
$currencyCode = $journal['foreign_currency_code'];
@@ -98,7 +91,7 @@ class TransactionSummarizer
if (!$this->convertToPrimary) {
// Log::debug('convertToPrimary is false.');
// use foreign amount?
$foreignCurrencyId = (int) $journal['foreign_currency_id'];
$foreignCurrencyId = (int)$journal['foreign_currency_id'];
if (0 !== $foreignCurrencyId) {
Log::debug(sprintf('Journal #%d also includes foreign amount (foreign is "%s")', $journal['transaction_journal_id'], $journal['foreign_currency_code']));
$foreignCurrencyName = $journal['foreign_currency_name'];
@@ -109,7 +102,7 @@ class TransactionSummarizer
}
// first process normal amount
$amount = (string) ($journal[$field] ?? '0');
$amount = (string)($journal[$field] ?? '0');
$array[$currencyId] ??= [
'sum' => '0',
'currency_id' => $currencyId,
@@ -128,7 +121,7 @@ class TransactionSummarizer
// then process foreign amount, if it exists.
if (0 !== $foreignCurrencyId && true === $includeForeign) {
$amount = (string) ($journal['foreign_amount'] ?? '0');
$amount = (string)($journal['foreign_amount'] ?? '0');
$array[$foreignCurrencyId] ??= [
'sum' => '0',
'currency_id' => $foreignCurrencyId,
@@ -200,12 +193,12 @@ class TransactionSummarizer
];
// add the data from the $field to the array.
$array[$key]['sum'] = bcadd($array[$key]['sum'], Steam::{$method}((string) ($journal[$field] ?? '0'))); // @phpstan-ignore-line
$array[$key]['sum'] = bcadd($array[$key]['sum'], Steam::{$method}((string)($journal[$field] ?? '0'))); // @phpstan-ignore-line
Log::debug(sprintf('Field for transaction #%d is "%s" (%s). Sum: %s', $journal['transaction_group_id'], $currencyCode, $field, $array[$key]['sum']));
// also do foreign amount, but only when convertToPrimary is false (otherwise we have it already)
// or when convertToPrimary is true and the foreign currency is ALSO not the default currency.
if ((!$convertToPrimary || $journal['foreign_currency_id'] !== $primary->id) && 0 !== (int) $journal['foreign_currency_id']) {
if ((!$convertToPrimary || $journal['foreign_currency_id'] !== $primary->id) && 0 !== (int)$journal['foreign_currency_id']) {
Log::debug(sprintf('Use foreign amount from transaction #%d: %s %s. Sum: %s', $journal['transaction_group_id'], $currencyCode, $journal['foreign_amount'], $array[$key]['sum']));
$key = sprintf('%s-%s', $journal[$idKey], $journal['foreign_currency_id']);
$array[$key] ??= [
@@ -218,7 +211,7 @@ class TransactionSummarizer
'currency_code' => $journal['foreign_currency_code'],
'currency_decimal_places' => $journal['foreign_currency_decimal_places'],
];
$array[$key]['sum'] = bcadd($array[$key]['sum'], Steam::{$method}((string) $journal['foreign_amount'])); // @phpstan-ignore-line
$array[$key]['sum'] = bcadd($array[$key]['sum'], Steam::{$method}((string)$journal['foreign_amount'])); // @phpstan-ignore-line
}
}
@@ -230,4 +223,11 @@ class TransactionSummarizer
Log::debug(sprintf('Overrule convertToPrimary to become %s', var_export($convertToPrimary, true)));
$this->convertToPrimary = $convertToPrimary;
}
public function setUser(User $user): void
{
$this->user = $user;
$this->default = Amount::getPrimaryCurrencyByUserGroup($user->userGroup);
$this->convertToPrimary = Amount::convertToPrimary($user);
}
}

View File

@@ -58,7 +58,7 @@ trait CalculateRangeOccurrences
{
$return = [];
$attempts = 0;
$dayOfMonth = (int) $moment;
$dayOfMonth = (int)$moment;
if ($start->day > $dayOfMonth) {
// day has passed already, add a month.
$start->addMonth();
@@ -113,7 +113,7 @@ trait CalculateRangeOccurrences
app('log')->debug('Rep is weekly.');
// monday = 1
// sunday = 7
$dayOfWeek = (int) $moment;
$dayOfWeek = (int)$moment;
app('log')->debug(sprintf('DoW in repetition is %d, in mutator is %d', $dayOfWeek, $start->dayOfWeekIso));
if ($start->dayOfWeekIso > $dayOfWeek) {
// day has already passed this week, add one week:

View File

@@ -63,7 +63,7 @@ trait CalculateXOccurrences
$mutator = clone $date;
$total = 0;
$attempts = 0;
$dayOfMonth = (int) $moment;
$dayOfMonth = (int)$moment;
if ($mutator->day > $dayOfMonth) {
// day has passed already, add a month.
$mutator->addMonth();
@@ -127,7 +127,7 @@ trait CalculateXOccurrences
// monday = 1
// sunday = 7
$mutator->addDay(); // always assume today has passed.
$dayOfWeek = (int) $moment;
$dayOfWeek = (int)$moment;
if ($mutator->dayOfWeekIso > $dayOfWeek) {
// day has already passed this week, add one week:
$mutator->addWeek();

View File

@@ -68,7 +68,7 @@ trait CalculateXOccurrencesSince
$mutator = clone $date;
$total = 0;
$attempts = 0;
$dayOfMonth = (int) $moment;
$dayOfMonth = (int)$moment;
$dayOfMonth = 0 === $dayOfMonth ? 1 : $dayOfMonth;
if ($mutator->day > $dayOfMonth) {
Log::debug(sprintf('%d is after %d, add a month. Mutator is now...', $mutator->day, $dayOfMonth));
@@ -145,7 +145,7 @@ trait CalculateXOccurrencesSince
// sunday = 7
// Removed assumption today has passed, see issue https://github.com/firefly-iii/firefly-iii/issues/4798
// $mutator->addDay(); // always assume today has passed.
$dayOfWeek = (int) $moment;
$dayOfWeek = (int)$moment;
if ($mutator->dayOfWeekIso > $dayOfWeek) {
// day has already passed this week, add one week:
$mutator->addWeek();

View File

@@ -46,19 +46,17 @@ trait AppendsLocationData
return $return;
}
private function validLongitude(string $longitude): bool
{
$number = (float) $longitude;
return $number >= -180 && $number <= 180;
}
private function validLatitude(string $latitude): bool
{
$number = (float) $latitude;
return $number >= -90 && $number <= 90;
}
/**
* Abstract method stolen from "InteractsWithInput".
*
* @param null $key
* @param bool $default
*
* @return mixed
*
* @SuppressWarnings("PHPMD.BooleanArgumentFlag")
*/
abstract public function boolean($key = null, $default = false);
/**
* Abstract method.
@@ -69,6 +67,22 @@ trait AppendsLocationData
*/
abstract public function has($key);
/**
* Abstract method.
*
* @return string
*/
abstract public function method();
/**
* Abstract method.
*
* @param mixed ...$patterns
*
* @return mixed
*/
abstract public function routeIs(...$patterns);
/**
* Read the submitted Request data and add new or updated Location data to the array.
*/
@@ -132,72 +146,22 @@ trait AppendsLocationData
return sprintf('%s_%s', $prefix, $key);
}
private function isValidPost(?string $prefix): bool
private function isValidEmptyPUT(?string $prefix): bool
{
app('log')->debug('Now in isValidPost()');
$longitudeKey = $this->getLocationKey($prefix, 'longitude');
$latitudeKey = $this->getLocationKey($prefix, 'latitude');
$zoomLevelKey = $this->getLocationKey($prefix, 'zoom_level');
$hasLocationKey = $this->getLocationKey($prefix, 'has_location');
// fields must not be null:
if (null !== $this->get($longitudeKey) && null !== $this->get($latitudeKey) && null !== $this->get($zoomLevelKey)) {
app('log')->debug('All fields present');
// if is POST and route contains API, this is enough:
if ('POST' === $this->method() && $this->routeIs('api.v1.*')) {
app('log')->debug('Is API location');
$longitudeKey = $this->getLocationKey($prefix, 'longitude');
$latitudeKey = $this->getLocationKey($prefix, 'latitude');
$zoomLevelKey = $this->getLocationKey($prefix, 'zoom_level');
return true;
}
// if is POST and route does not contain API, must also have "has_location" = true
if ('POST' === $this->method() && $this->routeIs('*.store') && !$this->routeIs('api.v1.*') && '' !== $hasLocationKey) {
app('log')->debug('Is POST + store route.');
$hasLocation = $this->boolean($hasLocationKey);
if (true === $hasLocation) {
app('log')->debug('Has form form location');
return true;
}
app('log')->debug('Does not have form location');
return false;
}
app('log')->debug('Is not POST API or POST form');
return false;
}
app('log')->debug('Fields not present');
return false;
return (
null === $this->get($longitudeKey)
&& null === $this->get($latitudeKey)
&& null === $this->get($zoomLevelKey))
&& (
'PUT' === $this->method()
|| ('POST' === $this->method() && $this->routeIs('*.update'))
);
}
/**
* Abstract method.
*
* @return string
*/
abstract public function method();
/**
* Abstract method.
*
* @param mixed ...$patterns
*
* @return mixed
*/
abstract public function routeIs(...$patterns);
/**
* Abstract method stolen from "InteractsWithInput".
*
* @param null $key
* @param bool $default
*
* @return mixed
*
* @SuppressWarnings("PHPMD.BooleanArgumentFlag")
*/
abstract public function boolean($key = null, $default = false);
private function isValidPUT(?string $prefix): bool
{
$longitudeKey = $this->getLocationKey($prefix, 'longitude');
@@ -238,19 +202,55 @@ trait AppendsLocationData
return false;
}
private function isValidEmptyPUT(?string $prefix): bool
private function isValidPost(?string $prefix): bool
{
$longitudeKey = $this->getLocationKey($prefix, 'longitude');
$latitudeKey = $this->getLocationKey($prefix, 'latitude');
$zoomLevelKey = $this->getLocationKey($prefix, 'zoom_level');
app('log')->debug('Now in isValidPost()');
$longitudeKey = $this->getLocationKey($prefix, 'longitude');
$latitudeKey = $this->getLocationKey($prefix, 'latitude');
$zoomLevelKey = $this->getLocationKey($prefix, 'zoom_level');
$hasLocationKey = $this->getLocationKey($prefix, 'has_location');
// fields must not be null:
if (null !== $this->get($longitudeKey) && null !== $this->get($latitudeKey) && null !== $this->get($zoomLevelKey)) {
app('log')->debug('All fields present');
// if is POST and route contains API, this is enough:
if ('POST' === $this->method() && $this->routeIs('api.v1.*')) {
app('log')->debug('Is API location');
return (
null === $this->get($longitudeKey)
&& null === $this->get($latitudeKey)
&& null === $this->get($zoomLevelKey))
&& (
'PUT' === $this->method()
|| ('POST' === $this->method() && $this->routeIs('*.update'))
);
return true;
}
// if is POST and route does not contain API, must also have "has_location" = true
if ('POST' === $this->method() && $this->routeIs('*.store') && !$this->routeIs('api.v1.*') && '' !== $hasLocationKey) {
app('log')->debug('Is POST + store route.');
$hasLocation = $this->boolean($hasLocationKey);
if (true === $hasLocation) {
app('log')->debug('Has form form location');
return true;
}
app('log')->debug('Does not have form location');
return false;
}
app('log')->debug('Is not POST API or POST form');
return false;
}
app('log')->debug('Fields not present');
return false;
}
private function validLatitude(string $latitude): bool
{
$number = (float)$latitude;
return $number >= -90 && $number <= 90;
}
private function validLongitude(string $longitude): bool
{
$number = (float)$longitude;
return $number >= -180 && $number <= 180;
}
}

View File

@@ -86,10 +86,10 @@ trait ChecksLogin
$userGroup = $this->route()?->parameter('userGroup');
if (null === $userGroup) {
app('log')->debug('Request class has no userGroup parameter, but perhaps there is a parameter.');
$userGroupId = (int) $this->get('user_group_id');
$userGroupId = (int)$this->get('user_group_id');
if (0 === $userGroupId) {
app('log')->debug(sprintf('Request class has no user_group_id parameter, grab default from user (group #%d).', $user->user_group_id));
$userGroupId = (int) $user->user_group_id;
$userGroupId = (int)$user->user_group_id;
}
$userGroup = UserGroup::find($userGroupId);
if (null === $userGroup) {

View File

@@ -99,28 +99,6 @@ trait ConvertsDataTypes
return Steam::filterSpaces($string);
}
public function convertSortParameters(string $field, string $class): array
{
// assume this all works, because the validator would have caught any errors.
$parameter = (string)request()->query->get($field);
if ('' === $parameter) {
return [];
}
$parts = explode(',', $parameter);
$sortParameters = [];
foreach ($parts as $part) {
$part = trim($part);
$direction = 'asc';
if ('-' === $part[0]) {
$part = substr($part, 1);
$direction = 'desc';
}
$sortParameters[] = [$part, $direction];
}
return $sortParameters;
}
public function clearString(?string $string): ?string
{
$string = $this->clearStringKeepNewlines($string);
@@ -159,6 +137,36 @@ trait ConvertsDataTypes
return Steam::filterSpaces($this->convertString($field));
}
/**
* Return integer value.
*/
public function convertInteger(string $field): int
{
return (int)$this->get($field);
}
public function convertSortParameters(string $field, string $class): array
{
// assume this all works, because the validator would have caught any errors.
$parameter = (string)request()->query->get($field);
if ('' === $parameter) {
return [];
}
$parts = explode(',', $parameter);
$sortParameters = [];
foreach ($parts as $part) {
$part = trim($part);
$direction = 'asc';
if ('-' === $part[0]) {
$part = substr($part, 1);
$direction = 'desc';
}
$sortParameters[] = [$part, $direction];
}
return $sortParameters;
}
/**
* Return string value.
*/
@@ -178,14 +186,6 @@ trait ConvertsDataTypes
*/
abstract public function get(string $key, mixed $default = null): mixed;
/**
* Return integer value.
*/
public function convertInteger(string $field): int
{
return (int)$this->get($field);
}
/**
* TODO duplicate, see SelectTransactionsRequest
*
@@ -218,6 +218,16 @@ trait ConvertsDataTypes
return $collection;
}
/**
* Abstract method that always exists in the Request classes that use this
* trait, OR a stub needs to be added by any other class that uses this train.
*
* @param mixed $key
*
* @return mixed
*/
abstract public function has($key);
/**
* Return string value with newlines.
*/
@@ -386,16 +396,6 @@ trait ConvertsDataTypes
return $return;
}
/**
* Abstract method that always exists in the Request classes that use this
* trait, OR a stub needs to be added by any other class that uses this train.
*
* @param mixed $key
*
* @return mixed
*/
abstract public function has($key);
/**
* Return date or NULL.
*/
@@ -418,6 +418,21 @@ trait ConvertsDataTypes
return $result;
}
/**
* Parse to integer
*/
protected function integerFromValue(?string $string): ?int
{
if (null === $string) {
return null;
}
if ('' === $string) {
return null;
}
return (int)$string;
}
/**
* Return integer value, or NULL when it's not set.
*/
@@ -463,19 +478,4 @@ trait ConvertsDataTypes
return $return;
}
/**
* Parse to integer
*/
protected function integerFromValue(?string $string): ?int
{
if (null === $string) {
return null;
}
if ('' === $string) {
return null;
}
return (int)$string;
}
}

View File

@@ -38,12 +38,12 @@ trait GetRecurrenceData
foreach ($stringKeys as $key) {
if (array_key_exists($key, $transaction)) {
$return[$key] = (string) $transaction[$key];
$return[$key] = (string)$transaction[$key];
}
}
foreach ($intKeys as $key) {
if (array_key_exists($key, $transaction)) {
$return[$key] = (int) $transaction[$key];
$return[$key] = (int)$transaction[$key];
}
}
foreach ($keys as $key) {

View File

@@ -25,10 +25,10 @@ declare(strict_types=1);
namespace FireflyIII\Support\Request;
use Illuminate\Validation\Validator;
use FireflyIII\Enums\WebhookTrigger;
use FireflyIII\Models\Webhook;
use Illuminate\Support\Facades\Log;
use Illuminate\Validation\Validator;
trait ValidatesWebhooks
{

View File

@@ -92,7 +92,7 @@ class AccountSearch implements GenericSearchInterface
break;
case self::SEARCH_ID:
$searchQuery->where('accounts.id', '=', (int) $originalQuery);
$searchQuery->where('accounts.id', '=', (int)$originalQuery);
break;

File diff suppressed because it is too large Load Diff

View File

@@ -76,7 +76,7 @@ class GdbotsQueryParser implements QueryParserInterface
case $node instanceof GdbotsNode\Field:
return new FieldNode(
$node->getValue(),
(string) $node->getNode()->getValue(),
(string)$node->getNode()->getValue(),
BoolOperator::PROHIBITED === $node->getBoolOperator()
);
@@ -98,7 +98,7 @@ class GdbotsQueryParser implements QueryParserInterface
case $node instanceof GdbotsNode\Mention:
case $node instanceof GdbotsNode\Emoticon:
case $node instanceof GdbotsNode\Emoji:
return new StringNode((string) $node->getValue(), BoolOperator::PROHIBITED === $node->getBoolOperator());
return new StringNode((string)$node->getValue(), BoolOperator::PROHIBITED === $node->getBoolOperator());
default:
throw new FireflyException(

View File

@@ -46,22 +46,6 @@ class QueryParser implements QueryParserInterface
return $this->buildNodeGroup(false);
}
private function buildNodeGroup(bool $isSubquery, bool $prohibited = false): NodeGroup
{
$nodes = [];
$nodeResult = $this->buildNextNode($isSubquery);
while ($nodeResult->node instanceof Node) {
$nodes[] = $nodeResult->node;
if ($nodeResult->isSubqueryEnd) {
break;
}
$nodeResult = $this->buildNextNode($isSubquery);
}
return new NodeGroup($nodes, $prohibited);
}
private function buildNextNode(bool $isSubquery): NodeResult
{
$tokenUnderConstruction = '';
@@ -194,6 +178,22 @@ class QueryParser implements QueryParserInterface
return new NodeResult($finalNode, true);
}
private function buildNodeGroup(bool $isSubquery, bool $prohibited = false): NodeGroup
{
$nodes = [];
$nodeResult = $this->buildNextNode($isSubquery);
while ($nodeResult->node instanceof Node) {
$nodes[] = $nodeResult->node;
if ($nodeResult->isSubqueryEnd) {
break;
}
$nodeResult = $this->buildNextNode($isSubquery);
}
return new NodeGroup($nodes, $prohibited);
}
private function createNode(string $token, string $fieldName, bool $prohibited): Node
{
if ('' !== $fieldName) {

View File

@@ -45,6 +45,11 @@ class PreferencesSingleton
return self::$instance;
}
public function getPreference(string $key): mixed
{
return $this->preferences[$key] ?? null;
}
public function resetPreferences(): void
{
$this->preferences = [];
@@ -54,9 +59,4 @@ class PreferencesSingleton
{
$this->preferences[$key] = $value;
}
public function getPreference(string $key): mixed
{
return $this->preferences[$key] ?? null;
}
}

View File

@@ -47,6 +47,82 @@ use function Safe\preg_replace;
*/
class Steam
{
public function accountsBalancesOptimized(Collection $accounts, Carbon $date, ?TransactionCurrency $primary = null, ?bool $convertToPrimary = null): array
{
Log::debug(sprintf('accountsBalancesOptimized: Called for %d account(s) with date/time "%s"', $accounts->count(), $date->toIso8601String()));
$result = [];
$convertToPrimary ??= Amount::convertToPrimary();
$primary ??= Amount::getPrimaryCurrency();
$currencies = $this->getCurrencies($accounts);
// balance(s) in all currencies for ALL accounts.
$arrayOfSums = Transaction::whereIn('account_id', $accounts->pluck('id')->toArray())
->leftJoin('transaction_journals', 'transaction_journals.id', '=', 'transactions.transaction_journal_id')
->leftJoin('transaction_currencies', 'transaction_currencies.id', '=', 'transactions.transaction_currency_id')
->where('transaction_journals.date', '<=', $date->format('Y-m-d H:i:s'))
->groupBy(['transactions.account_id', 'transaction_currencies.code'])
->get(['transactions.account_id', 'transaction_currencies.code', DB::raw('SUM(transactions.amount) as sum_of_amount')])->toArray()
;
/** @var Account $account */
foreach ($accounts as $account) {
// this array is PER account, so we wait a bit before we change code here.
$return = [
'pc_balance' => '0',
'balance' => '0', // this key is overwritten right away, but I must remember it is always created.
];
$currency = $currencies[$account->id];
// second array
$accountSum = array_filter($arrayOfSums, fn ($entry) => $entry['account_id'] === $account->id);
if (0 === count($accountSum)) {
$result[$account->id] = $return;
continue;
}
$accountSum = array_values($accountSum)[0];
$sumOfAmount = (string)$accountSum['sum_of_amount'];
$sumOfAmount = $this->floatalize('' === $sumOfAmount ? '0' : $sumOfAmount);
$sumsByCode = [
$accountSum['code'] => $sumOfAmount,
];
// Log::debug('All balances are (joined)', $others);
// if there is no request to convert, take this as "balance" and "pc_balance".
$return['balance'] = $sumsByCode[$currency->code] ?? '0';
if (!$convertToPrimary) {
unset($return['pc_balance']);
// Log::debug(sprintf('Set balance to %s, unset pc_balance', $return['balance']));
}
// if there is a request to convert, convert to "pc_balance" and use "balance" for whichever amount is in the primary currency.
if ($convertToPrimary) {
$return['pc_balance'] = $this->convertAllBalances($sumsByCode, $primary, $date);
// Log::debug(sprintf('Set pc_balance to %s', $return['pc_balance']));
}
// either way, the balance is always combined with the virtual balance:
$virtualBalance = (string)('' === (string)$account->virtual_balance ? '0' : $account->virtual_balance);
if ($convertToPrimary) {
// the primary currency balance is combined with a converted virtual_balance:
$converter = new ExchangeRateConverter();
$pcVirtualBalance = $converter->convert($currency, $primary, $date, $virtualBalance);
$return['pc_balance'] = bcadd($pcVirtualBalance, $return['pc_balance']);
// Log::debug(sprintf('Primary virtual balance makes the primary total %s', $return['pc_balance']));
}
if (!$convertToPrimary) {
// if not, also increase the balance + primary balance for consistency.
$return['balance'] = bcadd($return['balance'], $virtualBalance);
// Log::debug(sprintf('Virtual balance makes the (primary currency) total %s', $return['balance']));
}
$final = array_merge($return, $sumsByCode);
$result[$account->id] = $final;
// Log::debug('Final balance is', $final);
}
return $result;
}
/**
* https://stackoverflow.com/questions/1642614/how-to-ceil-floor-and-round-bcmath-numbers
*/
@@ -75,18 +151,6 @@ class Steam
return $number;
}
public function filterAccountBalances(array $total, Account $account, bool $convertToPrimary, ?TransactionCurrency $currency = null): array
{
Log::debug(sprintf('filterAccountBalances(#%d)', $account->id));
$return = [];
foreach ($total as $key => $value) {
$return[$key] = $this->filterAccountBalance($value, $account, $convertToPrimary, $currency);
}
Log::debug(sprintf('end of filterAccountBalances(#%d)', $account->id));
return $return;
}
public function filterAccountBalance(array $set, Account $account, bool $convertToPrimary, ?TransactionCurrency $currency = null): array
{
Log::debug(sprintf('filterAccountBalance(#%d)', $account->id), $set);
@@ -138,6 +202,18 @@ class Steam
return $set;
}
public function filterAccountBalances(array $total, Account $account, bool $convertToPrimary, ?TransactionCurrency $currency = null): array
{
Log::debug(sprintf('filterAccountBalances(#%d)', $account->id));
$return = [];
foreach ($total as $key => $value) {
$return[$key] = $this->filterAccountBalance($value, $account, $convertToPrimary, $currency);
}
Log::debug(sprintf('end of filterAccountBalances(#%d)', $account->id));
return $return;
}
public function filterSpaces(string $string): string
{
$search = [
@@ -197,6 +273,95 @@ class Steam
return str_replace($search, '', $string);
}
/**
* Returns smaller than or equal to, so be careful with END OF DAY.
*
* Returns the balance of an account at exact moment given. Array with at least one value.
* Always returns:
* "balance": balance in the account's currency OR user's primary currency if the account has no currency
* "EUR": balance in EUR (or whatever currencies the account has balance in)
*
* If the user has $convertToPrimary:
* "balance": balance in the account's currency OR user's primary currency if the account has no currency
* --> "pc_balance": balance in the user's primary currency, with all amounts converted to the primary currency.
* "EUR": balance in EUR (or whatever currencies the account has balance in)
*/
public function finalAccountBalance(Account $account, Carbon $date, ?TransactionCurrency $primary = null, ?bool $convertToPrimary = null): array
{
$cache = new CacheProperties();
$cache->addProperty($account->id);
$cache->addProperty($date);
if ($cache->has()) {
Log::debug(sprintf('CACHED finalAccountBalance(#%d, %s)', $account->id, $date->format('Y-m-d H:i:s')));
// return $cache->get();
}
// Log::debug(sprintf('finalAccountBalance(#%d, %s)', $account->id, $date->format('Y-m-d H:i:s')));
if (null === $convertToPrimary) {
$convertToPrimary = Amount::convertToPrimary($account->user);
}
if (!$primary instanceof TransactionCurrency) {
$primary = Amount::getPrimaryCurrencyByUserGroup($account->user->userGroup);
}
// account balance thing.
$currencyPresent = isset($account->meta) && array_key_exists('currency', $account->meta) && null !== $account->meta['currency'];
if ($currencyPresent) {
$accountCurrency = $account->meta['currency'];
}
if (!$currencyPresent) {
$accountCurrency = $this->getAccountCurrency($account);
}
$hasCurrency = null !== $accountCurrency;
$currency = $hasCurrency ? $accountCurrency : $primary;
$return = [
'pc_balance' => '0',
'balance' => '0', // this key is overwritten right away, but I must remember it is always created.
];
// balance(s) in all currencies.
$array = $account->transactions()
->leftJoin('transaction_journals', 'transaction_journals.id', '=', 'transactions.transaction_journal_id')
->leftJoin('transaction_currencies', 'transaction_currencies.id', '=', 'transactions.transaction_currency_id')
->where('transaction_journals.date', '<=', $date->format('Y-m-d H:i:s'))
->get(['transaction_currencies.code', 'transactions.amount'])->toArray()
;
$others = $this->groupAndSumTransactions($array, 'code', 'amount');
// Log::debug('All balances are (joined)', $others);
// if there is no request to convert, take this as "balance" and "pc_balance".
$return['balance'] = $others[$currency->code] ?? '0';
if (!$convertToPrimary) {
unset($return['pc_balance']);
// Log::debug(sprintf('Set balance to %s, unset pc_balance', $return['balance']));
}
// if there is a request to convert, convert to "pc_balance" and use "balance" for whichever amount is in the primary currency.
if ($convertToPrimary) {
$return['pc_balance'] = $this->convertAllBalances($others, $primary, $date); // todo sum all and convert.
// Log::debug(sprintf('Set pc_balance to %s', $return['pc_balance']));
}
// either way, the balance is always combined with the virtual balance:
$virtualBalance = (string)('' === (string)$account->virtual_balance ? '0' : $account->virtual_balance);
if ($convertToPrimary) {
// the primary currency balance is combined with a converted virtual_balance:
$converter = new ExchangeRateConverter();
$pcVirtualBalance = $converter->convert($currency, $primary, $date, $virtualBalance);
$return['pc_balance'] = bcadd($pcVirtualBalance, $return['pc_balance']);
// Log::debug(sprintf('Primary virtual balance makes the primary total %s', $return['pc_balance']));
}
if (!$convertToPrimary) {
// if not, also increase the balance + primary balance for consistency.
$return['balance'] = bcadd($return['balance'], $virtualBalance);
// Log::debug(sprintf('Virtual balance makes the (primary currency) total %s', $return['balance']));
}
$final = array_merge($return, $others);
// Log::debug('Final balance is', $final);
$cache->store($final);
return $final;
}
public function finalAccountBalanceInRange(Account $account, Carbon $start, Carbon $end, bool $convertToPrimary): array
{
// expand period.
@@ -321,169 +486,34 @@ class Steam
return $balances;
}
public function accountsBalancesOptimized(Collection $accounts, Carbon $date, ?TransactionCurrency $primary = null, ?bool $convertToPrimary = null): array
{
Log::debug(sprintf('accountsBalancesOptimized: Called for %d account(s) with date/time "%s"', $accounts->count(), $date->toIso8601String()));
$result = [];
$convertToPrimary ??= Amount::convertToPrimary();
$primary ??= Amount::getPrimaryCurrency();
$currencies = $this->getCurrencies($accounts);
// balance(s) in all currencies for ALL accounts.
$arrayOfSums = Transaction::whereIn('account_id', $accounts->pluck('id')->toArray())
->leftJoin('transaction_journals', 'transaction_journals.id', '=', 'transactions.transaction_journal_id')
->leftJoin('transaction_currencies', 'transaction_currencies.id', '=', 'transactions.transaction_currency_id')
->where('transaction_journals.date', '<=', $date->format('Y-m-d H:i:s'))
->groupBy(['transactions.account_id', 'transaction_currencies.code'])
->get(['transactions.account_id', 'transaction_currencies.code', DB::raw('SUM(transactions.amount) as sum_of_amount')])->toArray()
;
/** @var Account $account */
foreach ($accounts as $account) {
// this array is PER account, so we wait a bit before we change code here.
$return = [
'pc_balance' => '0',
'balance' => '0', // this key is overwritten right away, but I must remember it is always created.
];
$currency = $currencies[$account->id];
// second array
$accountSum = array_filter($arrayOfSums, fn ($entry) => $entry['account_id'] === $account->id);
if (0 === count($accountSum)) {
$result[$account->id] = $return;
continue;
}
$accountSum = array_values($accountSum)[0];
$sumOfAmount = (string)$accountSum['sum_of_amount'];
$sumOfAmount = $this->floatalize('' === $sumOfAmount ? '0' : $sumOfAmount);
$sumsByCode = [
$accountSum['code'] => $sumOfAmount,
];
// Log::debug('All balances are (joined)', $others);
// if there is no request to convert, take this as "balance" and "pc_balance".
$return['balance'] = $sumsByCode[$currency->code] ?? '0';
if (!$convertToPrimary) {
unset($return['pc_balance']);
// Log::debug(sprintf('Set balance to %s, unset pc_balance', $return['balance']));
}
// if there is a request to convert, convert to "pc_balance" and use "balance" for whichever amount is in the primary currency.
if ($convertToPrimary) {
$return['pc_balance'] = $this->convertAllBalances($sumsByCode, $primary, $date);
// Log::debug(sprintf('Set pc_balance to %s', $return['pc_balance']));
}
// either way, the balance is always combined with the virtual balance:
$virtualBalance = (string)('' === (string)$account->virtual_balance ? '0' : $account->virtual_balance);
if ($convertToPrimary) {
// the primary currency balance is combined with a converted virtual_balance:
$converter = new ExchangeRateConverter();
$pcVirtualBalance = $converter->convert($currency, $primary, $date, $virtualBalance);
$return['pc_balance'] = bcadd($pcVirtualBalance, $return['pc_balance']);
// Log::debug(sprintf('Primary virtual balance makes the primary total %s', $return['pc_balance']));
}
if (!$convertToPrimary) {
// if not, also increase the balance + primary balance for consistency.
$return['balance'] = bcadd($return['balance'], $virtualBalance);
// Log::debug(sprintf('Virtual balance makes the (primary currency) total %s', $return['balance']));
}
$final = array_merge($return, $sumsByCode);
$result[$account->id] = $final;
// Log::debug('Final balance is', $final);
}
return $result;
}
/**
* Returns smaller than or equal to, so be careful with END OF DAY.
* https://framework.zend.com/downloads/archives
*
* Returns the balance of an account at exact moment given. Array with at least one value.
* Always returns:
* "balance": balance in the account's currency OR user's primary currency if the account has no currency
* "EUR": balance in EUR (or whatever currencies the account has balance in)
*
* If the user has $convertToPrimary:
* "balance": balance in the account's currency OR user's primary currency if the account has no currency
* --> "pc_balance": balance in the user's primary currency, with all amounts converted to the primary currency.
* "EUR": balance in EUR (or whatever currencies the account has balance in)
* Convert a scientific notation to float
* Additionally fixed a problem with PHP <= 5.2.x with big integers
*/
public function finalAccountBalance(Account $account, Carbon $date, ?TransactionCurrency $primary = null, ?bool $convertToPrimary = null): array
public function floatalize(string $value): string
{
$value = strtoupper($value);
if (!str_contains($value, 'E')) {
return $value;
}
Log::debug(sprintf('Floatalizing %s', $value));
$cache = new CacheProperties();
$cache->addProperty($account->id);
$cache->addProperty($date);
if ($cache->has()) {
Log::debug(sprintf('CACHED finalAccountBalance(#%d, %s)', $account->id, $date->format('Y-m-d H:i:s')));
$number = substr($value, 0, (int)strpos($value, 'E'));
if (str_contains($number, '.')) {
$post = strlen(substr($number, (int)strpos($number, '.') + 1));
$mantis = substr($value, (int)strpos($value, 'E') + 1);
if ($mantis < 0) {
$post += abs((int)$mantis);
}
// return $cache->get();
}
// Log::debug(sprintf('finalAccountBalance(#%d, %s)', $account->id, $date->format('Y-m-d H:i:s')));
if (null === $convertToPrimary) {
$convertToPrimary = Amount::convertToPrimary($account->user);
}
if (!$primary instanceof TransactionCurrency) {
$primary = Amount::getPrimaryCurrencyByUserGroup($account->user->userGroup);
}
// account balance thing.
$currencyPresent = isset($account->meta) && array_key_exists('currency', $account->meta) && null !== $account->meta['currency'];
if ($currencyPresent) {
$accountCurrency = $account->meta['currency'];
}
if (!$currencyPresent) {
$accountCurrency = $this->getAccountCurrency($account);
}
$hasCurrency = null !== $accountCurrency;
$currency = $hasCurrency ? $accountCurrency : $primary;
$return = [
'pc_balance' => '0',
'balance' => '0', // this key is overwritten right away, but I must remember it is always created.
];
// balance(s) in all currencies.
$array = $account->transactions()
->leftJoin('transaction_journals', 'transaction_journals.id', '=', 'transactions.transaction_journal_id')
->leftJoin('transaction_currencies', 'transaction_currencies.id', '=', 'transactions.transaction_currency_id')
->where('transaction_journals.date', '<=', $date->format('Y-m-d H:i:s'))
->get(['transaction_currencies.code', 'transactions.amount'])->toArray()
;
$others = $this->groupAndSumTransactions($array, 'code', 'amount');
// Log::debug('All balances are (joined)', $others);
// if there is no request to convert, take this as "balance" and "pc_balance".
$return['balance'] = $others[$currency->code] ?? '0';
if (!$convertToPrimary) {
unset($return['pc_balance']);
// Log::debug(sprintf('Set balance to %s, unset pc_balance', $return['balance']));
}
// if there is a request to convert, convert to "pc_balance" and use "balance" for whichever amount is in the primary currency.
if ($convertToPrimary) {
$return['pc_balance'] = $this->convertAllBalances($others, $primary, $date); // todo sum all and convert.
// Log::debug(sprintf('Set pc_balance to %s', $return['pc_balance']));
// TODO careless float could break financial math.
return number_format((float)$value, $post, '.', '');
}
// either way, the balance is always combined with the virtual balance:
$virtualBalance = (string)('' === (string)$account->virtual_balance ? '0' : $account->virtual_balance);
if ($convertToPrimary) {
// the primary currency balance is combined with a converted virtual_balance:
$converter = new ExchangeRateConverter();
$pcVirtualBalance = $converter->convert($currency, $primary, $date, $virtualBalance);
$return['pc_balance'] = bcadd($pcVirtualBalance, $return['pc_balance']);
// Log::debug(sprintf('Primary virtual balance makes the primary total %s', $return['pc_balance']));
}
if (!$convertToPrimary) {
// if not, also increase the balance + primary balance for consistency.
$return['balance'] = bcadd($return['balance'], $virtualBalance);
// Log::debug(sprintf('Virtual balance makes the (primary currency) total %s', $return['balance']));
}
$final = array_merge($return, $others);
// Log::debug('Final balance is', $final);
$cache->store($final);
return $final;
// TODO careless float could break financial math.
return number_format((float)$value, 0, '.', '');
}
public function getAccountCurrency(Account $account): ?TransactionCurrency
@@ -503,45 +533,6 @@ class Steam
return Amount::getTransactionCurrencyById((int)$result->data);
}
private function groupAndSumTransactions(array $array, string $group, string $field): array
{
$return = [];
foreach ($array as $item) {
$groupKey = $item[$group] ?? 'unknown';
$return[$groupKey] = bcadd($return[$groupKey] ?? '0', (string)$item[$field]);
}
return $return;
}
private function convertAllBalances(array $others, TransactionCurrency $primary, Carbon $date): string
{
$total = '0';
$converter = new ExchangeRateConverter();
$singleton = PreferencesSingleton::getInstance();
foreach ($others as $key => $amount) {
$preference = $singleton->getPreference($key);
try {
$currency = $preference ?? Amount::getTransactionCurrencyByCode($key);
} catch (FireflyException) {
continue;
}
if (null === $preference) {
$singleton->setPreference($key, $currency);
}
$current = $amount;
if ($currency->id !== $primary->id) {
$current = $converter->convert($currency, $primary, $date, $amount);
Log::debug(sprintf('Convert %s %s to %s %s', $currency->code, $amount, $primary->code, $current));
}
$total = bcadd($current, $total);
}
return $total;
}
/**
* @throws FireflyException
*/
@@ -563,6 +554,21 @@ class Steam
return (string)$host;
}
/**
* Get user's language.
*
* @throws FireflyException
*/
public function getLanguage(): string // get preference
{
$preference = app('preferences')->get('language', config('firefly.default_language', 'en_US'))->data;
if (!is_string($preference)) {
throw new FireflyException(sprintf('Preference "language" must be a string, but is unexpectedly a "%s".', gettype($preference)));
}
return str_replace('-', '_', $preference);
}
public function getLastActivities(array $accounts): array
{
$list = [];
@@ -588,38 +594,29 @@ class Steam
*/
public function getLocale(): string // get preference
{
$locale = app('preferences')->get('locale', config('firefly.default_locale', 'equal'))->data;
$singleton = PreferencesSingleton::getInstance();
$cached = $singleton->getPreference('locale');
if (null !== $cached) {
return $cached;
}
$locale = app('preferences')->get('locale', config('firefly.default_locale', 'equal'))->data;
if (is_array($locale)) {
$locale = 'equal';
}
if ('equal' === $locale) {
$locale = $this->getLanguage();
}
$locale = (string)$locale;
$locale = (string)$locale;
// Check for Windows to replace the locale correctly.
if ('WIN' === strtoupper(substr(PHP_OS, 0, 3))) {
return str_replace('_', '-', $locale);
$locale = str_replace('_', '-', $locale);
}
$singleton->setPreference('locale', $locale);
return $locale;
}
/**
* Get user's language.
*
* @throws FireflyException
*/
public function getLanguage(): string // get preference
{
$preference = app('preferences')->get('language', config('firefly.default_language', 'en_US'))->data;
if (!is_string($preference)) {
throw new FireflyException(sprintf('Preference "language" must be a string, but is unexpectedly a "%s".', gettype($preference)));
}
return str_replace('-', '_', $preference);
}
public function getLocaleArray(string $locale): array
{
return [
@@ -681,36 +678,6 @@ class Steam
return $amount;
}
/**
* https://framework.zend.com/downloads/archives
*
* Convert a scientific notation to float
* Additionally fixed a problem with PHP <= 5.2.x with big integers
*/
public function floatalize(string $value): string
{
$value = strtoupper($value);
if (!str_contains($value, 'E')) {
return $value;
}
Log::debug(sprintf('Floatalizing %s', $value));
$number = substr($value, 0, (int)strpos($value, 'E'));
if (str_contains($number, '.')) {
$post = strlen(substr($number, (int)strpos($number, '.') + 1));
$mantis = substr($value, (int)strpos($value, 'E') + 1);
if ($mantis < 0) {
$post += abs((int)$mantis);
}
// TODO careless float could break financial math.
return number_format((float)$value, $post, '.', '');
}
// TODO careless float could break financial math.
return number_format((float)$value, 0, '.', '');
}
public function opposite(?string $amount = null): ?string
{
if (null === $amount) {
@@ -768,6 +735,33 @@ class Steam
return $amount;
}
private function convertAllBalances(array $others, TransactionCurrency $primary, Carbon $date): string
{
$total = '0';
$converter = new ExchangeRateConverter();
$singleton = PreferencesSingleton::getInstance();
foreach ($others as $key => $amount) {
$preference = $singleton->getPreference($key);
try {
$currency = $preference ?? Amount::getTransactionCurrencyByCode($key);
} catch (FireflyException) {
continue;
}
if (null === $preference) {
$singleton->setPreference($key, $currency);
}
$current = $amount;
if ($currency->id !== $primary->id) {
$current = $converter->convert($currency, $primary, $date, $amount);
Log::debug(sprintf('Convert %s %s to %s %s', $currency->code, $amount, $primary->code, $current));
}
$total = bcadd($current, $total);
}
return $total;
}
private function getCurrencies(Collection $accounts): array
{
$currencies = [];
@@ -811,4 +805,16 @@ class Steam
return $accountCurrencies;
}
private function groupAndSumTransactions(array $array, string $group, string $field): array
{
$return = [];
foreach ($array as $item) {
$groupKey = $item[$group] ?? 'unknown';
$return[$groupKey] = bcadd($return[$groupKey] ?? '0', (string)$item[$field]);
}
return $return;
}
}

View File

@@ -49,7 +49,7 @@ trait GeneratesInstallationId
if (null === $config) {
$uuid4 = Uuid::uuid4();
$uniqueId = (string) $uuid4;
$uniqueId = (string)$uuid4;
app('log')->info(sprintf('Created Firefly III installation ID %s', $uniqueId));
app('fireflyconfig')->set('installation_id', $uniqueId);
}

View File

@@ -43,22 +43,18 @@ class OAuthKeys
private const string PRIVATE_KEY = 'oauth_private_key';
private const string PUBLIC_KEY = 'oauth_public_key';
public static function verifyKeysRoutine(): void
public static function generateKeys(): void
{
if (!self::keysInDatabase() && !self::hasKeyFiles()) {
self::generateKeys();
self::storeKeysInDB();
Artisan::registerCommand(new KeysCommand());
Artisan::call('firefly-iii:laravel-passport-keys');
}
return;
}
if (self::keysInDatabase() && !self::hasKeyFiles()) {
self::restoreKeysFromDB();
public static function hasKeyFiles(): bool
{
$private = storage_path('oauth-private.key');
$public = storage_path('oauth-public.key');
return;
}
if (!self::keysInDatabase() && self::hasKeyFiles()) {
self::storeKeysInDB();
}
return file_exists($private) && file_exists($public);
}
public static function keysInDatabase(): bool
@@ -68,8 +64,8 @@ class OAuthKeys
// better check if keys are in the database:
if (app('fireflyconfig')->has(self::PRIVATE_KEY) && app('fireflyconfig')->has(self::PUBLIC_KEY)) {
try {
$privateKey = (string) app('fireflyconfig')->get(self::PRIVATE_KEY)?->data;
$publicKey = (string) app('fireflyconfig')->get(self::PUBLIC_KEY)?->data;
$privateKey = (string)app('fireflyconfig')->get(self::PRIVATE_KEY)?->data;
$publicKey = (string)app('fireflyconfig')->get(self::PUBLIC_KEY)?->data;
} catch (ContainerExceptionInterface|FireflyException|NotFoundExceptionInterface $e) {
app('log')->error(sprintf('Could not validate keysInDatabase(): %s', $e->getMessage()));
app('log')->error($e->getTraceAsString());
@@ -82,35 +78,13 @@ class OAuthKeys
return false;
}
public static function hasKeyFiles(): bool
{
$private = storage_path('oauth-private.key');
$public = storage_path('oauth-public.key');
return file_exists($private) && file_exists($public);
}
public static function generateKeys(): void
{
Artisan::registerCommand(new KeysCommand());
Artisan::call('firefly-iii:laravel-passport-keys');
}
public static function storeKeysInDB(): void
{
$private = storage_path('oauth-private.key');
$public = storage_path('oauth-public.key');
app('fireflyconfig')->set(self::PRIVATE_KEY, Crypt::encrypt(file_get_contents($private)));
app('fireflyconfig')->set(self::PUBLIC_KEY, Crypt::encrypt(file_get_contents($public)));
}
/**
* @throws FireflyException
*/
public static function restoreKeysFromDB(): bool
{
$privateKey = (string) app('fireflyconfig')->get(self::PRIVATE_KEY)?->data;
$publicKey = (string) app('fireflyconfig')->get(self::PUBLIC_KEY)?->data;
$privateKey = (string)app('fireflyconfig')->get(self::PRIVATE_KEY)?->data;
$publicKey = (string)app('fireflyconfig')->get(self::PUBLIC_KEY)?->data;
try {
$privateContent = Crypt::decrypt($privateKey);
@@ -132,4 +106,30 @@ class OAuthKeys
return true;
}
public static function storeKeysInDB(): void
{
$private = storage_path('oauth-private.key');
$public = storage_path('oauth-public.key');
app('fireflyconfig')->set(self::PRIVATE_KEY, Crypt::encrypt(file_get_contents($private)));
app('fireflyconfig')->set(self::PUBLIC_KEY, Crypt::encrypt(file_get_contents($public)));
}
public static function verifyKeysRoutine(): void
{
if (!self::keysInDatabase() && !self::hasKeyFiles()) {
self::generateKeys();
self::storeKeysInDB();
return;
}
if (self::keysInDatabase() && !self::hasKeyFiles()) {
self::restoreKeysFromDB();
return;
}
if (!self::keysInDatabase() && self::hasKeyFiles()) {
self::storeKeysInDB();
}
}
}

View File

@@ -29,10 +29,10 @@ use FireflyIII\Models\TransactionCurrency;
use FireflyIII\Repositories\Account\AccountRepositoryInterface;
use FireflyIII\Support\Facades\Amount;
use Illuminate\Support\Facades\Log;
use Override;
use Twig\Extension\AbstractExtension;
use Twig\TwigFilter;
use Twig\TwigFunction;
use Override;
/**
* Contains all amount formatting routines.
@@ -48,6 +48,17 @@ class AmountFormat extends AbstractExtension
];
}
#[Override]
public function getFunctions(): array
{
return [
$this->formatAmountByAccount(),
$this->formatAmountBySymbol(),
$this->formatAmountByCurrency(),
$this->formatAmountByCode(),
];
}
protected function formatAmount(): TwigFilter
{
return new TwigFilter(
@@ -61,30 +72,6 @@ class AmountFormat extends AbstractExtension
);
}
protected function formatAmountPlain(): TwigFilter
{
return new TwigFilter(
'formatAmountPlain',
static function (string $string): string {
$currency = Amount::getPrimaryCurrency();
return Amount::formatAnything($currency, $string, false);
},
['is_safe' => ['html']]
);
}
#[Override]
public function getFunctions(): array
{
return [
$this->formatAmountByAccount(),
$this->formatAmountBySymbol(),
$this->formatAmountByCurrency(),
$this->formatAmountByCode(),
];
}
/**
* Will format the amount by the currency related to the given account.
*
@@ -107,6 +94,47 @@ class AmountFormat extends AbstractExtension
);
}
/**
* Use the code to format a currency.
*/
protected function formatAmountByCode(): TwigFunction
{
// formatAmountByCode
return new TwigFunction(
'formatAmountByCode',
static function (string $amount, string $code, ?bool $coloured = null): string {
$coloured ??= true;
try {
$currency = Amount::getTransactionCurrencyByCode($code);
} catch (FireflyException) {
Log::error(sprintf('Could not find currency with code "%s". Fallback to primary currency.', $code));
$currency = Amount::getPrimaryCurrency();
Log::error(sprintf('Fallback currency is "%s".', $currency->code));
}
return Amount::formatAnything($currency, $amount, $coloured);
},
['is_safe' => ['html']]
);
}
/**
* Will format the amount by the currency related to the given account.
*/
protected function formatAmountByCurrency(): TwigFunction
{
return new TwigFunction(
'formatAmountByCurrency',
static function (TransactionCurrency $currency, string $amount, ?bool $coloured = null): string {
$coloured ??= true;
return Amount::formatAnything($currency, $amount, $coloured);
},
['is_safe' => ['html']]
);
}
/**
* Will format the amount by the currency related to the given account.
*/
@@ -135,42 +163,14 @@ class AmountFormat extends AbstractExtension
);
}
/**
* Will format the amount by the currency related to the given account.
*/
protected function formatAmountByCurrency(): TwigFunction
protected function formatAmountPlain(): TwigFilter
{
return new TwigFunction(
'formatAmountByCurrency',
static function (TransactionCurrency $currency, string $amount, ?bool $coloured = null): string {
$coloured ??= true;
return new TwigFilter(
'formatAmountPlain',
static function (string $string): string {
$currency = Amount::getPrimaryCurrency();
return Amount::formatAnything($currency, $amount, $coloured);
},
['is_safe' => ['html']]
);
}
/**
* Use the code to format a currency.
*/
protected function formatAmountByCode(): TwigFunction
{
// formatAmountByCode
return new TwigFunction(
'formatAmountByCode',
static function (string $amount, string $code, ?bool $coloured = null): string {
$coloured ??= true;
try {
$currency = Amount::getTransactionCurrencyByCode($code);
} catch (FireflyException) {
Log::error(sprintf('Could not find currency with code "%s". Fallback to primary currency.', $code));
$currency = Amount::getPrimaryCurrency();
Log::error(sprintf('Fallback currency is "%s".', $currency->code));
}
return Amount::formatAnything($currency, $amount, $coloured);
return Amount::formatAnything($currency, $string, false);
},
['is_safe' => ['html']]
);

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