From e5c409a8fc73e36bf79069c66928c4564d552de8 Mon Sep 17 00:00:00 2001 From: JC5 Date: Sat, 13 Sep 2025 18:52:04 +0200 Subject: [PATCH 01/58] =?UTF-8?q?=F0=9F=A4=96=20Auto=20commit=20for=20rele?= =?UTF-8?q?ase=20'develop'=20on=202025-09-13?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- config/firefly.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/config/firefly.php b/config/firefly.php index c2af8616ae..eecaf07c61 100644 --- a/config/firefly.php +++ b/config/firefly.php @@ -78,8 +78,8 @@ return [ 'running_balance_column' => env('USE_RUNNING_BALANCE', false), // see cer.php for exchange rates feature flag. ], - 'version' => '6.4.0', - 'build_time' => 1757781366, + 'version' => 'develop/2025-09-13', + 'build_time' => 1757782204, 'api_version' => '2.1.0', // field is no longer used. 'db_version' => 26, From 30df6684cb2921a1435f06262a1de7ce66c0d23d Mon Sep 17 00:00:00 2001 From: James Cole Date: Sun, 14 Sep 2025 07:45:54 +0200 Subject: [PATCH 02/58] Fix another missing filter for #10803 --- .../Budget/OperationsRepository.php | 2 +- .../Enrichments/BudgetLimitEnrichment.php | 52 ++++++++++--------- 2 files changed, 28 insertions(+), 26 deletions(-) diff --git a/app/Repositories/Budget/OperationsRepository.php b/app/Repositories/Budget/OperationsRepository.php index 211c4fa1f5..badde3a9fb 100644 --- a/app/Repositories/Budget/OperationsRepository.php +++ b/app/Repositories/Budget/OperationsRepository.php @@ -314,7 +314,7 @@ class OperationsRepository implements OperationsRepositoryInterface, UserGroupIn #[Override] public function collectExpenses(Carbon $start, Carbon $end, ?Collection $accounts = null, ?Collection $budgets = null, ?TransactionCurrency $currency = null): array { - Log::debug(sprintf('Start of %s(date, date, array, array, "%s").', __METHOD__, $currency?->code)); + Log::debug(sprintf('Start of %s(%s, %s, array, array, "%s").', __METHOD__, $start->toW3cString(), $end->toW3cString(), $currency?->code)); // this collector excludes all transfers TO liabilities (which are also withdrawals) // because those expenses only become expenses once they move from the liability to the friend. // 2024-12-24 disable the exclusion for now. diff --git a/app/Support/JsonApi/Enrichments/BudgetLimitEnrichment.php b/app/Support/JsonApi/Enrichments/BudgetLimitEnrichment.php index 87e8b7a0a8..c0af1e7735 100644 --- a/app/Support/JsonApi/Enrichments/BudgetLimitEnrichment.php +++ b/app/Support/JsonApi/Enrichments/BudgetLimitEnrichment.php @@ -40,18 +40,18 @@ 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 Carbon $end; - private array $expenses = []; - private array $pcExpenses = []; - private array $currencyIds = []; - private array $currencies = []; - private bool $convertToPrimary = true; + private User $user; + private UserGroup $userGroup; // @phpstan-ignore-line + private Collection $collection; + private array $ids = []; + private array $notes = []; + private Carbon $start; + private Carbon $end; + private array $expenses = []; + private array $pcExpenses = []; + private array $currencyIds = []; + private array $currencies = []; + private bool $convertToPrimary = true; private readonly TransactionCurrency $primaryCurrency; public function __construct() @@ -95,8 +95,8 @@ 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(); + $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) { @@ -113,10 +113,9 @@ class BudgetLimitEnrichment implements EnrichmentInterface 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() - ; + ->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']; } @@ -145,18 +144,19 @@ class BudgetLimitEnrichment implements EnrichmentInterface private function collectBudgets(): void { - $budgetIds = $this->collection->pluck('budget_id')->unique()->toArray(); - $budgets = Budget::whereIn('id', $budgetIds)->get(); + $budgetIds = $this->collection->pluck('budget_id')->unique()->toArray(); + $budgets = Budget::whereIn('id', $budgetIds)->get(); $repository = app(OperationsRepository::class); $repository->setUser($this->user); - $expenses = $repository->collectExpenses($this->start, $this->end, null, $budgets, null); + $expenses = $repository->collectExpenses($this->start, $this->end, null, $budgets, null); /** @var BudgetLimit $budgetLimit */ foreach ($this->collection as $budgetLimit) { + Log::debug(sprintf('Filtering expenses for budget limit #%d (budget #%d)', $budgetLimit->id, $budgetLimit->budget_id)); $id = (int)$budgetLimit->id; $filteredExpenses = $this->filterToBudget($expenses, $budgetLimit->budget_id); - $filteredExpenses = $repository->sumCollectedExpenses($expenses, $budgetLimit->start_date, $budgetLimit->end_date, $budgetLimit->transactionCurrency, false); + $filteredExpenses = $repository->sumCollectedExpenses($filteredExpenses, $budgetLimit->start_date, $budgetLimit->end_date, $budgetLimit->transactionCurrency, false); $this->expenses[$id] = array_values($filteredExpenses); if (true === $this->convertToPrimary && $budgetLimit->transactionCurrency->id !== $this->primaryCurrency->id) { @@ -180,13 +180,13 @@ class BudgetLimitEnrichment implements EnrichmentInterface private function stringifyIds(): void { - $this->expenses = array_map(fn ($first) => array_map(function ($second) { + $this->expenses = array_map(fn($first) => array_map(function ($second) { $second['currency_id'] = (string)($second['currency_id'] ?? 0); return $second; }, $first), $this->expenses); - $this->pcExpenses = array_map(fn ($first) => array_map(function ($second) { + $this->pcExpenses = array_map(fn($first) => array_map(function ($second) { $second['currency_id'] = (string)($second['currency_id'] ?? 0); return $second; @@ -195,6 +195,8 @@ class BudgetLimitEnrichment implements EnrichmentInterface private function filterToBudget(array $expenses, int $budget): array { - return array_filter($expenses, fn (array $item) => (int)$item['budget_id'] === $budget); + $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; } } From fad016f92f62a28d21119cc496dd13ccfbeeb156 Mon Sep 17 00:00:00 2001 From: James Cole Date: Sun, 14 Sep 2025 07:46:46 +0200 Subject: [PATCH 03/58] Update changelog. --- changelog.md | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/changelog.md b/changelog.md index b37af58197..93c2a96ae5 100644 --- a/changelog.md +++ b/changelog.md @@ -3,7 +3,13 @@ All notable changes to this project will be documented in this file. This project adheres to [Semantic Versioning](http://semver.org/). -## 6.4.0 - 2025-09-19 +## 6.4.1 - 2025-09-15 + +### Fixed + +- Fixed a missing filter from [issue 10803](https://github.com/firefly-iii/firefly-iii/issues/10803). + +## 6.4.0 - 2025-09-14 ### Added From 9e6f9d16e4936d6f24eda419af2beb3df6a116a7 Mon Sep 17 00:00:00 2001 From: James Cole Date: Sun, 14 Sep 2025 08:55:08 +0200 Subject: [PATCH 04/58] Move observers to attributes. --- app/Models/Account.php | 3 ++ app/Models/Attachment.php | 3 ++ app/Models/AutoBudget.php | 24 ++++++---- app/Models/AvailableBudget.php | 21 +++++---- app/Models/Bill.php | 25 +++++----- app/Models/Budget.php | 3 ++ app/Models/BudgetLimit.php | 18 +++---- app/Models/Category.php | 4 ++ app/Models/PiggyBank.php | 20 ++++---- app/Models/PiggyBankEvent.php | 11 +++-- app/Models/Recurrence.php | 13 ++++-- app/Models/RecurrenceTransaction.php | 19 ++++---- app/Models/Rule.php | 15 +++--- app/Models/RuleGroup.php | 4 ++ app/Models/Tag.php | 6 ++- app/Models/Transaction.php | 4 ++ app/Models/TransactionGroup.php | 4 ++ app/Models/TransactionJournal.php | 5 ++ app/Models/Webhook.php | 4 ++ app/Models/WebhookMessage.php | 13 ++++-- app/Providers/EventServiceProvider.php | 65 -------------------------- 21 files changed, 144 insertions(+), 140 deletions(-) diff --git a/app/Models/Account.php b/app/Models/Account.php index 683b4a2973..eef5d6fcd5 100644 --- a/app/Models/Account.php +++ b/app/Models/Account.php @@ -23,6 +23,8 @@ declare(strict_types=1); namespace FireflyIII\Models; +use FireflyIII\Handlers\Observer\AccountObserver; +use Illuminate\Database\Eloquent\Attributes\ObservedBy; use Illuminate\Database\Eloquent\Attributes\Scope; use FireflyIII\Enums\AccountTypeEnum; use FireflyIII\Support\Models\ReturnsIntegerIdTrait; @@ -40,6 +42,7 @@ use Illuminate\Database\Eloquent\Relations\MorphToMany; use Illuminate\Database\Eloquent\SoftDeletes; use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; +#[ObservedBy([AccountObserver::class])] class Account extends Model { use HasFactory; diff --git a/app/Models/Attachment.php b/app/Models/Attachment.php index 2287666db1..ed5a20bab3 100644 --- a/app/Models/Attachment.php +++ b/app/Models/Attachment.php @@ -23,9 +23,11 @@ declare(strict_types=1); namespace FireflyIII\Models; +use FireflyIII\Handlers\Observer\AttachmentObserver; use FireflyIII\Support\Models\ReturnsIntegerIdTrait; use FireflyIII\Support\Models\ReturnsIntegerUserIdTrait; use FireflyIII\User; +use Illuminate\Database\Eloquent\Attributes\ObservedBy; use Illuminate\Database\Eloquent\Casts\Attribute; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo; @@ -34,6 +36,7 @@ use Illuminate\Database\Eloquent\Relations\MorphTo; use Illuminate\Database\Eloquent\SoftDeletes; use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; +#[ObservedBy([AttachmentObserver::class])] class Attachment extends Model { use ReturnsIntegerIdTrait; diff --git a/app/Models/AutoBudget.php b/app/Models/AutoBudget.php index 7f53584616..d1aa6475c9 100644 --- a/app/Models/AutoBudget.php +++ b/app/Models/AutoBudget.php @@ -25,31 +25,37 @@ declare(strict_types=1); namespace FireflyIII\Models; use Deprecated; +use FireflyIII\Handlers\Observer\AutoBudgetObserver; use FireflyIII\Support\Models\ReturnsIntegerIdTrait; +use Illuminate\Database\Eloquent\Attributes\ObservedBy; use Illuminate\Database\Eloquent\Casts\Attribute; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\SoftDeletes; +#[ObservedBy([AutoBudgetObserver::class])] class AutoBudget extends Model { use ReturnsIntegerIdTrait; use SoftDeletes; - #[Deprecated] /** @deprecated */ + #[Deprecated] + /** @deprecated */ public const int AUTO_BUDGET_ADJUSTED = 3; - #[Deprecated] /** @deprecated */ - public const int AUTO_BUDGET_RESET = 1; + #[Deprecated] + /** @deprecated */ + public const int AUTO_BUDGET_RESET = 1; - #[Deprecated] /** @deprecated */ + #[Deprecated] + /** @deprecated */ public const int AUTO_BUDGET_ROLLOVER = 2; protected $casts - = [ + = [ 'amount' => 'string', 'native_amount' => 'string', ]; - protected $fillable = ['budget_id', 'amount', 'period', 'native_amount']; + protected $fillable = ['budget_id', 'amount', 'period', 'native_amount']; public function budget(): BelongsTo { @@ -64,14 +70,14 @@ class AutoBudget extends Model protected function amount(): Attribute { return Attribute::make( - get: static fn ($value) => (string) $value, + get: static fn($value) => (string)$value, ); } protected function budgetId(): Attribute { return Attribute::make( - get: static fn ($value) => (int) $value, + get: static fn($value) => (int)$value, ); } @@ -85,7 +91,7 @@ class AutoBudget extends Model protected function transactionCurrencyId(): Attribute { return Attribute::make( - get: static fn ($value) => (int) $value, + get: static fn($value) => (int)$value, ); } } diff --git a/app/Models/AvailableBudget.php b/app/Models/AvailableBudget.php index 576864a2ea..f26f7f33d7 100644 --- a/app/Models/AvailableBudget.php +++ b/app/Models/AvailableBudget.php @@ -24,15 +24,18 @@ declare(strict_types=1); namespace FireflyIII\Models; use Carbon\Carbon; +use FireflyIII\Handlers\Observer\AvailableBudgetObserver; use FireflyIII\Support\Models\ReturnsIntegerIdTrait; use FireflyIII\Support\Models\ReturnsIntegerUserIdTrait; use FireflyIII\User; +use Illuminate\Database\Eloquent\Attributes\ObservedBy; use Illuminate\Database\Eloquent\Casts\Attribute; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\SoftDeletes; use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; +#[ObservedBy([AvailableBudgetObserver::class])] class AvailableBudget extends Model { use ReturnsIntegerIdTrait; @@ -49,13 +52,13 @@ class AvailableBudget extends Model public static function routeBinder(string $value): self { if (auth()->check()) { - $availableBudgetId = (int) $value; + $availableBudgetId = (int)$value; /** @var User $user */ - $user = auth()->user(); + $user = auth()->user(); /** @var null|AvailableBudget $availableBudget */ - $availableBudget = $user->availableBudgets()->find($availableBudgetId); + $availableBudget = $user->availableBudgets()->find($availableBudgetId); if (null !== $availableBudget) { return $availableBudget; } @@ -77,30 +80,30 @@ class AvailableBudget extends Model protected function amount(): Attribute { return Attribute::make( - get: static fn ($value) => (string) $value, + get: static fn($value) => (string)$value, ); } protected function endDate(): Attribute { return Attribute::make( - get: fn (string $value) => Carbon::parse($value), - set: fn (Carbon $value) => $value->format('Y-m-d'), + get: fn(string $value) => Carbon::parse($value), + set: fn(Carbon $value) => $value->format('Y-m-d'), ); } protected function startDate(): Attribute { return Attribute::make( - get: fn (string $value) => Carbon::parse($value), - set: fn (Carbon $value) => $value->format('Y-m-d'), + get: fn(string $value) => Carbon::parse($value), + set: fn(Carbon $value) => $value->format('Y-m-d'), ); } protected function transactionCurrencyId(): Attribute { return Attribute::make( - get: static fn ($value) => (int) $value, + get: static fn($value) => (int)$value, ); } diff --git a/app/Models/Bill.php b/app/Models/Bill.php index a0f59bc9d4..e44d0f3098 100644 --- a/app/Models/Bill.php +++ b/app/Models/Bill.php @@ -24,9 +24,11 @@ declare(strict_types=1); namespace FireflyIII\Models; use FireflyIII\Casts\SeparateTimezoneCaster; +use FireflyIII\Handlers\Observer\BillObserver; use FireflyIII\Support\Models\ReturnsIntegerIdTrait; use FireflyIII\Support\Models\ReturnsIntegerUserIdTrait; use FireflyIII\User; +use Illuminate\Database\Eloquent\Attributes\ObservedBy; use Illuminate\Database\Eloquent\Casts\Attribute; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo; @@ -36,6 +38,7 @@ use Illuminate\Database\Eloquent\Relations\MorphToMany; use Illuminate\Database\Eloquent\SoftDeletes; use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; +#[ObservedBy([BillObserver::class])] class Bill extends Model { use ReturnsIntegerIdTrait; @@ -43,7 +46,7 @@ class Bill extends Model use SoftDeletes; protected $fillable - = [ + = [ 'name', 'match', 'amount_min', @@ -75,13 +78,13 @@ class Bill extends Model public static function routeBinder(string $value): self { if (auth()->check()) { - $billId = (int) $value; + $billId = (int)$value; /** @var User $user */ - $user = auth()->user(); + $user = auth()->user(); /** @var null|Bill $bill */ - $bill = $user->bills()->find($billId); + $bill = $user->bills()->find($billId); if (null !== $bill) { return $bill; } @@ -121,7 +124,7 @@ class Bill extends Model */ public function setAmountMaxAttribute($value): void { - $this->attributes['amount_max'] = (string) $value; + $this->attributes['amount_max'] = (string)$value; } /** @@ -129,7 +132,7 @@ class Bill extends Model */ public function setAmountMinAttribute($value): void { - $this->attributes['amount_min'] = (string) $value; + $this->attributes['amount_min'] = (string)$value; } public function transactionCurrency(): BelongsTo @@ -148,7 +151,7 @@ class Bill extends Model protected function amountMax(): Attribute { return Attribute::make( - get: static fn ($value) => (string) $value, + get: static fn($value) => (string)$value, ); } @@ -158,14 +161,14 @@ class Bill extends Model protected function amountMin(): Attribute { return Attribute::make( - get: static fn ($value) => (string) $value, + get: static fn($value) => (string)$value, ); } protected function order(): Attribute { return Attribute::make( - get: static fn ($value) => (int) $value, + get: static fn($value) => (int)$value, ); } @@ -175,14 +178,14 @@ class Bill extends Model protected function skip(): Attribute { return Attribute::make( - get: static fn ($value) => (int) $value, + get: static fn($value) => (int)$value, ); } protected function transactionCurrencyId(): Attribute { return Attribute::make( - get: static fn ($value) => (int) $value, + get: static fn($value) => (int)$value, ); } diff --git a/app/Models/Budget.php b/app/Models/Budget.php index c5c1c29d46..086764d66d 100644 --- a/app/Models/Budget.php +++ b/app/Models/Budget.php @@ -23,9 +23,11 @@ declare(strict_types=1); namespace FireflyIII\Models; +use FireflyIII\Handlers\Observer\BudgetObserver; use FireflyIII\Support\Models\ReturnsIntegerIdTrait; use FireflyIII\Support\Models\ReturnsIntegerUserIdTrait; use FireflyIII\User; +use Illuminate\Database\Eloquent\Attributes\ObservedBy; use Illuminate\Database\Eloquent\Casts\Attribute; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo; @@ -35,6 +37,7 @@ use Illuminate\Database\Eloquent\Relations\MorphMany; use Illuminate\Database\Eloquent\SoftDeletes; use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; +#[ObservedBy([BudgetObserver::class])] class Budget extends Model { use ReturnsIntegerIdTrait; diff --git a/app/Models/BudgetLimit.php b/app/Models/BudgetLimit.php index e7270fc7b0..247cf24ad4 100644 --- a/app/Models/BudgetLimit.php +++ b/app/Models/BudgetLimit.php @@ -24,13 +24,16 @@ declare(strict_types=1); namespace FireflyIII\Models; use FireflyIII\Casts\SeparateTimezoneCaster; +use FireflyIII\Handlers\Observer\BudgetLimitObserver; use FireflyIII\Support\Models\ReturnsIntegerIdTrait; +use Illuminate\Database\Eloquent\Attributes\ObservedBy; use Illuminate\Database\Eloquent\Casts\Attribute; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\MorphMany; use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; +#[ObservedBy([BudgetLimitObserver::class])] class BudgetLimit extends Model { use ReturnsIntegerIdTrait; @@ -45,12 +48,11 @@ class BudgetLimit extends Model public static function routeBinder(string $value): self { if (auth()->check()) { - $budgetLimitId = (int) $value; + $budgetLimitId = (int)$value; $budgetLimit = self::where('budget_limits.id', $budgetLimitId) - ->leftJoin('budgets', 'budgets.id', '=', 'budget_limits.budget_id') - ->where('budgets.user_id', auth()->user()->id) - ->first(['budget_limits.*']) - ; + ->leftJoin('budgets', 'budgets.id', '=', 'budget_limits.budget_id') + ->where('budgets.user_id', auth()->user()->id) + ->first(['budget_limits.*']); if (null !== $budgetLimit) { return $budgetLimit; } @@ -83,21 +85,21 @@ class BudgetLimit extends Model protected function amount(): Attribute { return Attribute::make( - get: static fn ($value) => (string) $value, + get: static fn($value) => (string)$value, ); } protected function budgetId(): Attribute { return Attribute::make( - get: static fn ($value) => (int) $value, + get: static fn($value) => (int)$value, ); } protected function transactionCurrencyId(): Attribute { return Attribute::make( - get: static fn ($value) => (int) $value, + get: static fn($value) => (int)$value, ); } diff --git a/app/Models/Category.php b/app/Models/Category.php index 3e50f448c6..1f961f561d 100644 --- a/app/Models/Category.php +++ b/app/Models/Category.php @@ -24,9 +24,12 @@ declare(strict_types=1); namespace FireflyIII\Models; +use FireflyIII\Handlers\Observer\AccountObserver; +use FireflyIII\Handlers\Observer\CategoryObserver; use FireflyIII\Support\Models\ReturnsIntegerIdTrait; use FireflyIII\Support\Models\ReturnsIntegerUserIdTrait; use FireflyIII\User; +use Illuminate\Database\Eloquent\Attributes\ObservedBy; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\BelongsToMany; @@ -34,6 +37,7 @@ use Illuminate\Database\Eloquent\Relations\MorphMany; use Illuminate\Database\Eloquent\SoftDeletes; use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; +#[ObservedBy([CategoryObserver::class])] class Category extends Model { use ReturnsIntegerIdTrait; diff --git a/app/Models/PiggyBank.php b/app/Models/PiggyBank.php index 08d5a2563d..ae161b8c93 100644 --- a/app/Models/PiggyBank.php +++ b/app/Models/PiggyBank.php @@ -23,7 +23,9 @@ declare(strict_types=1); namespace FireflyIII\Models; +use FireflyIII\Handlers\Observer\PiggyBankObserver; use FireflyIII\Support\Models\ReturnsIntegerIdTrait; +use Illuminate\Database\Eloquent\Attributes\ObservedBy; use Illuminate\Database\Eloquent\Casts\Attribute; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo; @@ -34,6 +36,7 @@ use Illuminate\Database\Eloquent\Relations\MorphToMany; use Illuminate\Database\Eloquent\SoftDeletes; use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; +#[ObservedBy([PiggyBankObserver::class])] class PiggyBank extends Model { use ReturnsIntegerIdTrait; @@ -49,12 +52,11 @@ class PiggyBank extends Model public static function routeBinder(string $value): self { if (auth()->check()) { - $piggyBankId = (int) $value; + $piggyBankId = (int)$value; $piggyBank = self::where('piggy_banks.id', $piggyBankId) - ->leftJoin('account_piggy_bank', 'account_piggy_bank.piggy_bank_id', '=', 'piggy_banks.id') - ->leftJoin('accounts', 'accounts.id', '=', 'account_piggy_bank.account_id') - ->where('accounts.user_id', auth()->user()->id)->first(['piggy_banks.*']) - ; + ->leftJoin('account_piggy_bank', 'account_piggy_bank.piggy_bank_id', '=', 'piggy_banks.id') + ->leftJoin('accounts', 'accounts.id', '=', 'account_piggy_bank.account_id') + ->where('accounts.user_id', auth()->user()->id)->first(['piggy_banks.*']); if (null !== $piggyBank) { return $piggyBank; } @@ -109,7 +111,7 @@ class PiggyBank extends Model */ public function setTargetAmountAttribute($value): void { - $this->attributes['target_amount'] = (string) $value; + $this->attributes['target_amount'] = (string)$value; } public function transactionCurrency(): BelongsTo @@ -120,14 +122,14 @@ class PiggyBank extends Model protected function accountId(): Attribute { return Attribute::make( - get: static fn ($value) => (int) $value, + get: static fn($value) => (int)$value, ); } protected function order(): Attribute { return Attribute::make( - get: static fn ($value) => (int) $value, + get: static fn($value) => (int)$value, ); } @@ -137,7 +139,7 @@ class PiggyBank extends Model protected function targetAmount(): Attribute { return Attribute::make( - get: static fn ($value) => (string) $value, + get: static fn($value) => (string)$value, ); } diff --git a/app/Models/PiggyBankEvent.php b/app/Models/PiggyBankEvent.php index 733792ffeb..9599c2f027 100644 --- a/app/Models/PiggyBankEvent.php +++ b/app/Models/PiggyBankEvent.php @@ -24,18 +24,21 @@ declare(strict_types=1); namespace FireflyIII\Models; use FireflyIII\Casts\SeparateTimezoneCaster; +use FireflyIII\Handlers\Observer\PiggyBankEventObserver; use FireflyIII\Support\Models\ReturnsIntegerIdTrait; +use Illuminate\Database\Eloquent\Attributes\ObservedBy; use Illuminate\Database\Eloquent\Casts\Attribute; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo; +#[ObservedBy([PiggyBankEventObserver::class])] class PiggyBankEvent extends Model { use ReturnsIntegerIdTrait; protected $fillable = ['piggy_bank_id', 'transaction_journal_id', 'date', 'date_tz', 'amount', 'native_amount']; - protected $hidden = ['amount_encrypted']; + protected $hidden = ['amount_encrypted']; public function piggyBank(): BelongsTo { @@ -47,7 +50,7 @@ class PiggyBankEvent extends Model */ public function setAmountAttribute($value): void { - $this->attributes['amount'] = (string) $value; + $this->attributes['amount'] = (string)$value; } public function transactionJournal(): BelongsTo @@ -61,14 +64,14 @@ class PiggyBankEvent extends Model protected function amount(): Attribute { return Attribute::make( - get: static fn ($value) => (string) $value, + get: static fn($value) => (string)$value, ); } protected function piggyBankId(): Attribute { return Attribute::make( - get: static fn ($value) => (int) $value, + get: static fn($value) => (int)$value, ); } diff --git a/app/Models/Recurrence.php b/app/Models/Recurrence.php index 4e369fca27..78051e6bc6 100644 --- a/app/Models/Recurrence.php +++ b/app/Models/Recurrence.php @@ -25,9 +25,11 @@ declare(strict_types=1); namespace FireflyIII\Models; use FireflyIII\Casts\SeparateTimezoneCaster; +use FireflyIII\Handlers\Observer\RecurrenceObserver; use FireflyIII\Support\Models\ReturnsIntegerIdTrait; use FireflyIII\Support\Models\ReturnsIntegerUserIdTrait; use FireflyIII\User; +use Illuminate\Database\Eloquent\Attributes\ObservedBy; use Illuminate\Database\Eloquent\Casts\Attribute; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo; @@ -36,6 +38,7 @@ use Illuminate\Database\Eloquent\Relations\MorphMany; use Illuminate\Database\Eloquent\SoftDeletes; use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; +#[ObservedBy([RecurrenceObserver::class])] class Recurrence extends Model { use ReturnsIntegerIdTrait; @@ -43,7 +46,7 @@ class Recurrence extends Model use SoftDeletes; protected $fillable - = ['user_id', 'user_group_id', 'transaction_type_id', 'title', 'description', 'first_date', 'first_date_tz', 'repeat_until', 'repeat_until_tz', 'latest_date', 'latest_date_tz', 'repetitions', 'apply_rules', 'active']; + = ['user_id', 'user_group_id', 'transaction_type_id', 'title', 'description', 'first_date', 'first_date_tz', 'repeat_until', 'repeat_until_tz', 'latest_date', 'latest_date_tz', 'repetitions', 'apply_rules', 'active']; protected $table = 'recurrences'; @@ -55,13 +58,13 @@ class Recurrence extends Model public static function routeBinder(string $value): self { if (auth()->check()) { - $recurrenceId = (int) $value; + $recurrenceId = (int)$value; /** @var User $user */ - $user = auth()->user(); + $user = auth()->user(); /** @var null|Recurrence $recurrence */ - $recurrence = $user->recurrences()->find($recurrenceId); + $recurrence = $user->recurrences()->find($recurrenceId); if (null !== $recurrence) { return $recurrence; } @@ -116,7 +119,7 @@ class Recurrence extends Model protected function transactionTypeId(): Attribute { return Attribute::make( - get: static fn ($value) => (int) $value, + get: static fn($value) => (int)$value, ); } diff --git a/app/Models/RecurrenceTransaction.php b/app/Models/RecurrenceTransaction.php index 2c17d52ebb..245e19865f 100644 --- a/app/Models/RecurrenceTransaction.php +++ b/app/Models/RecurrenceTransaction.php @@ -24,20 +24,23 @@ declare(strict_types=1); namespace FireflyIII\Models; +use FireflyIII\Handlers\Observer\RecurrenceTransactionObserver; use FireflyIII\Support\Models\ReturnsIntegerIdTrait; +use Illuminate\Database\Eloquent\Attributes\ObservedBy; use Illuminate\Database\Eloquent\Casts\Attribute; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\HasMany; use Illuminate\Database\Eloquent\SoftDeletes; +#[ObservedBy([RecurrenceTransactionObserver::class])] class RecurrenceTransaction extends Model { use ReturnsIntegerIdTrait; use SoftDeletes; protected $fillable - = [ + = [ 'recurrence_id', 'transaction_currency_id', 'foreign_currency_id', @@ -88,49 +91,49 @@ class RecurrenceTransaction extends Model protected function amount(): Attribute { return Attribute::make( - get: static fn ($value) => (string) $value, + get: static fn($value) => (string)$value, ); } protected function destinationId(): Attribute { return Attribute::make( - get: static fn ($value) => (int) $value, + get: static fn($value) => (int)$value, ); } protected function foreignAmount(): Attribute { return Attribute::make( - get: static fn ($value) => (string) $value, + get: static fn($value) => (string)$value, ); } protected function recurrenceId(): Attribute { return Attribute::make( - get: static fn ($value) => (int) $value, + get: static fn($value) => (int)$value, ); } protected function sourceId(): Attribute { return Attribute::make( - get: static fn ($value) => (int) $value, + get: static fn($value) => (int)$value, ); } protected function transactionCurrencyId(): 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, ); } diff --git a/app/Models/Rule.php b/app/Models/Rule.php index 5308eae4d5..78f2906ccb 100644 --- a/app/Models/Rule.php +++ b/app/Models/Rule.php @@ -23,9 +23,11 @@ declare(strict_types=1); namespace FireflyIII\Models; +use FireflyIII\Handlers\Observer\RuleObserver; use FireflyIII\Support\Models\ReturnsIntegerIdTrait; use FireflyIII\Support\Models\ReturnsIntegerUserIdTrait; use FireflyIII\User; +use Illuminate\Database\Eloquent\Attributes\ObservedBy; use Illuminate\Database\Eloquent\Casts\Attribute; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo; @@ -33,6 +35,7 @@ use Illuminate\Database\Eloquent\Relations\HasMany; use Illuminate\Database\Eloquent\SoftDeletes; use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; +#[ObservedBy([RuleObserver::class])] class Rule extends Model { use ReturnsIntegerIdTrait; @@ -49,13 +52,13 @@ class Rule extends Model public static function routeBinder(string $value): self { if (auth()->check()) { - $ruleId = (int) $value; + $ruleId = (int)$value; /** @var User $user */ - $user = auth()->user(); + $user = auth()->user(); /** @var null|Rule $rule */ - $rule = $user->rules()->find($ruleId); + $rule = $user->rules()->find($ruleId); if (null !== $rule) { return $rule; } @@ -86,7 +89,7 @@ class Rule extends Model protected function description(): Attribute { - return Attribute::make(set: fn ($value) => ['description' => e($value)]); + return Attribute::make(set: fn($value) => ['description' => e($value)]); } public function userGroup(): BelongsTo @@ -97,14 +100,14 @@ class Rule extends Model protected function order(): Attribute { return Attribute::make( - get: static fn ($value) => (int) $value, + get: static fn($value) => (int)$value, ); } protected function ruleGroupId(): Attribute { return Attribute::make( - get: static fn ($value) => (int) $value, + get: static fn($value) => (int)$value, ); } diff --git a/app/Models/RuleGroup.php b/app/Models/RuleGroup.php index 1a25031a7e..48b96e1a67 100644 --- a/app/Models/RuleGroup.php +++ b/app/Models/RuleGroup.php @@ -23,9 +23,12 @@ declare(strict_types=1); namespace FireflyIII\Models; +use FireflyIII\Handlers\Observer\AccountObserver; +use FireflyIII\Handlers\Observer\RuleGroupObserver; use FireflyIII\Support\Models\ReturnsIntegerIdTrait; use FireflyIII\Support\Models\ReturnsIntegerUserIdTrait; use FireflyIII\User; +use Illuminate\Database\Eloquent\Attributes\ObservedBy; use Illuminate\Database\Eloquent\Casts\Attribute; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo; @@ -33,6 +36,7 @@ use Illuminate\Database\Eloquent\Relations\HasMany; use Illuminate\Database\Eloquent\SoftDeletes; use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; +#[ObservedBy([RuleGroupObserver::class])] class RuleGroup extends Model { use ReturnsIntegerIdTrait; diff --git a/app/Models/Tag.php b/app/Models/Tag.php index 3af4799730..df1082bc09 100644 --- a/app/Models/Tag.php +++ b/app/Models/Tag.php @@ -24,9 +24,12 @@ declare(strict_types=1); namespace FireflyIII\Models; use FireflyIII\Casts\SeparateTimezoneCaster; +use FireflyIII\Handlers\Observer\AccountObserver; +use FireflyIII\Handlers\Observer\TagObserver; use FireflyIII\Support\Models\ReturnsIntegerIdTrait; use FireflyIII\Support\Models\ReturnsIntegerUserIdTrait; use FireflyIII\User; +use Illuminate\Database\Eloquent\Attributes\ObservedBy; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\BelongsToMany; @@ -34,6 +37,7 @@ use Illuminate\Database\Eloquent\Relations\MorphMany; use Illuminate\Database\Eloquent\SoftDeletes; use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; +#[ObservedBy([TagObserver::class])] class Tag extends Model { use ReturnsIntegerIdTrait; @@ -42,7 +46,7 @@ class Tag extends Model protected $fillable = ['user_id', 'user_group_id', 'tag', 'date', 'date_tz', 'description', 'tag_mode']; - protected $hidden = ['zoomLevel', 'latitude', 'longitude']; + protected $hidden = ['zoomLevel', 'zoom_level', 'latitude', 'longitude']; /** * Route binder. Converts the key in the URL to the specified object (or throw 404). diff --git a/app/Models/Transaction.php b/app/Models/Transaction.php index a563e3656f..342a2f6b0d 100644 --- a/app/Models/Transaction.php +++ b/app/Models/Transaction.php @@ -23,6 +23,9 @@ declare(strict_types=1); namespace FireflyIII\Models; +use FireflyIII\Handlers\Observer\AccountObserver; +use FireflyIII\Handlers\Observer\TransactionObserver; +use Illuminate\Database\Eloquent\Attributes\ObservedBy; use Illuminate\Database\Eloquent\Attributes\Scope; use Carbon\Carbon; use FireflyIII\Support\Models\ReturnsIntegerIdTrait; @@ -34,6 +37,7 @@ use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\BelongsToMany; use Illuminate\Database\Eloquent\SoftDeletes; +#[ObservedBy([TransactionObserver::class])] class Transaction extends Model { use HasFactory; diff --git a/app/Models/TransactionGroup.php b/app/Models/TransactionGroup.php index a09af2b231..a13c0008ad 100644 --- a/app/Models/TransactionGroup.php +++ b/app/Models/TransactionGroup.php @@ -23,15 +23,19 @@ declare(strict_types=1); namespace FireflyIII\Models; +use FireflyIII\Handlers\Observer\AccountObserver; +use FireflyIII\Handlers\Observer\TransactionGroupObserver; use FireflyIII\Support\Models\ReturnsIntegerIdTrait; use FireflyIII\Support\Models\ReturnsIntegerUserIdTrait; use FireflyIII\User; +use Illuminate\Database\Eloquent\Attributes\ObservedBy; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\HasMany; use Illuminate\Database\Eloquent\SoftDeletes; use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; +#[ObservedBy([TransactionGroupObserver::class])] class TransactionGroup extends Model { use ReturnsIntegerIdTrait; diff --git a/app/Models/TransactionJournal.php b/app/Models/TransactionJournal.php index 609d7dc353..72f2ba70f2 100644 --- a/app/Models/TransactionJournal.php +++ b/app/Models/TransactionJournal.php @@ -23,6 +23,9 @@ declare(strict_types=1); namespace FireflyIII\Models; +use FireflyIII\Handlers\Observer\AccountObserver; +use FireflyIII\Handlers\Observer\TransactionJournalObserver; +use Illuminate\Database\Eloquent\Attributes\ObservedBy; use Illuminate\Database\Eloquent\Attributes\Scope; use Carbon\Carbon; use FireflyIII\Casts\SeparateTimezoneCaster; @@ -46,6 +49,8 @@ use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; * @method EloquentBuilder|static after() * @method static EloquentBuilder|static query() */ + +#[ObservedBy([TransactionJournalObserver::class])] class TransactionJournal extends Model { use HasFactory; diff --git a/app/Models/Webhook.php b/app/Models/Webhook.php index a836b7fad6..797ca82876 100644 --- a/app/Models/Webhook.php +++ b/app/Models/Webhook.php @@ -27,9 +27,12 @@ namespace FireflyIII\Models; use FireflyIII\Enums\WebhookDelivery as WebhookDeliveryEnum; use FireflyIII\Enums\WebhookResponse as WebhookResponseEnum; use FireflyIII\Enums\WebhookTrigger as WebhookTriggerEnum; +use FireflyIII\Handlers\Observer\AccountObserver; +use FireflyIII\Handlers\Observer\WebhookObserver; use FireflyIII\Support\Models\ReturnsIntegerIdTrait; use FireflyIII\Support\Models\ReturnsIntegerUserIdTrait; use FireflyIII\User; +use Illuminate\Database\Eloquent\Attributes\ObservedBy; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\BelongsToMany; @@ -37,6 +40,7 @@ use Illuminate\Database\Eloquent\Relations\HasMany; use Illuminate\Database\Eloquent\SoftDeletes; use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; +#[ObservedBy([WebhookObserver::class])] class Webhook extends Model { use ReturnsIntegerIdTrait; diff --git a/app/Models/WebhookMessage.php b/app/Models/WebhookMessage.php index 77d6a0642c..c1014d60aa 100644 --- a/app/Models/WebhookMessage.php +++ b/app/Models/WebhookMessage.php @@ -24,14 +24,17 @@ declare(strict_types=1); namespace FireflyIII\Models; +use FireflyIII\Handlers\Observer\WebhookMessageObserver; use FireflyIII\Support\Models\ReturnsIntegerIdTrait; use FireflyIII\User; +use Illuminate\Database\Eloquent\Attributes\ObservedBy; use Illuminate\Database\Eloquent\Casts\Attribute; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\HasMany; use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; +#[ObservedBy([WebhookMessageObserver::class])] class WebhookMessage extends Model { use ReturnsIntegerIdTrait; @@ -44,13 +47,13 @@ class WebhookMessage extends Model public static function routeBinder(string $value): self { if (auth()->check()) { - $messageId = (int) $value; + $messageId = (int)$value; /** @var User $user */ - $user = auth()->user(); + $user = auth()->user(); /** @var null|WebhookMessage $message */ - $message = self::find($messageId); + $message = self::find($messageId); if (null !== $message && $message->webhook->user_id === $user->id) { return $message; } @@ -75,14 +78,14 @@ class WebhookMessage extends Model protected function sent(): Attribute { return Attribute::make( - get: static fn ($value) => (bool) $value, + get: static fn($value) => (bool)$value, ); } protected function webhookId(): Attribute { return Attribute::make( - get: static fn ($value) => (int) $value, + get: static fn($value) => (int)$value, ); } diff --git a/app/Providers/EventServiceProvider.php b/app/Providers/EventServiceProvider.php index 5cff75ae42..a63903a280 100644 --- a/app/Providers/EventServiceProvider.php +++ b/app/Providers/EventServiceProvider.php @@ -57,46 +57,6 @@ use FireflyIII\Events\TriggeredAuditLog; use FireflyIII\Events\UpdatedAccount; use FireflyIII\Events\UpdatedTransactionGroup; use FireflyIII\Events\UserChangedEmail; -use FireflyIII\Handlers\Observer\AccountObserver; -use FireflyIII\Handlers\Observer\AttachmentObserver; -use FireflyIII\Handlers\Observer\AutoBudgetObserver; -use FireflyIII\Handlers\Observer\AvailableBudgetObserver; -use FireflyIII\Handlers\Observer\BillObserver; -use FireflyIII\Handlers\Observer\BudgetLimitObserver; -use FireflyIII\Handlers\Observer\BudgetObserver; -use FireflyIII\Handlers\Observer\CategoryObserver; -use FireflyIII\Handlers\Observer\PiggyBankEventObserver; -use FireflyIII\Handlers\Observer\PiggyBankObserver; -use FireflyIII\Handlers\Observer\RecurrenceObserver; -use FireflyIII\Handlers\Observer\RecurrenceTransactionObserver; -use FireflyIII\Handlers\Observer\RuleGroupObserver; -use FireflyIII\Handlers\Observer\RuleObserver; -use FireflyIII\Handlers\Observer\TagObserver; -use FireflyIII\Handlers\Observer\TransactionGroupObserver; -use FireflyIII\Handlers\Observer\TransactionJournalObserver; -use FireflyIII\Handlers\Observer\TransactionObserver; -use FireflyIII\Handlers\Observer\WebhookMessageObserver; -use FireflyIII\Handlers\Observer\WebhookObserver; -use FireflyIII\Models\Account; -use FireflyIII\Models\Attachment; -use FireflyIII\Models\AutoBudget; -use FireflyIII\Models\AvailableBudget; -use FireflyIII\Models\Bill; -use FireflyIII\Models\Budget; -use FireflyIII\Models\BudgetLimit; -use FireflyIII\Models\Category; -use FireflyIII\Models\PiggyBank; -use FireflyIII\Models\PiggyBankEvent; -use FireflyIII\Models\Recurrence; -use FireflyIII\Models\RecurrenceTransaction; -use FireflyIII\Models\Rule; -use FireflyIII\Models\RuleGroup; -use FireflyIII\Models\Tag; -use FireflyIII\Models\Transaction; -use FireflyIII\Models\TransactionGroup; -use FireflyIII\Models\TransactionJournal; -use FireflyIII\Models\Webhook; -use FireflyIII\Models\WebhookMessage; use Illuminate\Auth\Events\Login; use Illuminate\Foundation\Support\Providers\EventServiceProvider as ServiceProvider; use Laravel\Passport\Events\AccessTokenCreated; @@ -258,30 +218,5 @@ class EventServiceProvider extends ServiceProvider #[Override] public function boot(): void { - $this->registerObservers(); - } - - private function registerObservers(): void - { - Attachment::observe(new AttachmentObserver()); - Account::observe(new AccountObserver()); - AutoBudget::observe(new AutoBudgetObserver()); - AvailableBudget::observe(new AvailableBudgetObserver()); - Bill::observe(new BillObserver()); - Budget::observe(new BudgetObserver()); - BudgetLimit::observe(new BudgetLimitObserver()); - Category::observe(new CategoryObserver()); - PiggyBank::observe(new PiggyBankObserver()); - PiggyBankEvent::observe(new PiggyBankEventObserver()); - Recurrence::observe(new RecurrenceObserver()); - RecurrenceTransaction::observe(new RecurrenceTransactionObserver()); - Rule::observe(new RuleObserver()); - RuleGroup::observe(new RuleGroupObserver()); - Tag::observe(new TagObserver()); - Transaction::observe(new TransactionObserver()); - TransactionJournal::observe(new TransactionJournalObserver()); - TransactionGroup::observe(new TransactionGroupObserver()); - Webhook::observe(new WebhookObserver()); - WebhookMessage::observe(new WebhookMessageObserver()); } } From c2d3f5da16c56763b06a755191080dfa859f3394 Mon Sep 17 00:00:00 2001 From: James Cole Date: Sun, 14 Sep 2025 08:55:29 +0200 Subject: [PATCH 05/58] Allow budget store to have optional webhook using "fire_webhooks". --- .../Models/Budget/StoreController.php | 14 ++++---- .../Requests/Models/Budget/StoreRequest.php | 6 ++++ .../Models/Transaction/StoreRequest.php | 1 + app/Handlers/Observer/BudgetObserver.php | 34 +++++++++++-------- app/Repositories/Budget/BudgetRepository.php | 5 +++ app/Support/Request/ConvertsDataTypes.php | 6 ++++ 6 files changed, 46 insertions(+), 20 deletions(-) diff --git a/app/Api/V1/Controllers/Models/Budget/StoreController.php b/app/Api/V1/Controllers/Models/Budget/StoreController.php index b6d9e85e6c..3e4954f474 100644 --- a/app/Api/V1/Controllers/Models/Budget/StoreController.php +++ b/app/Api/V1/Controllers/Models/Budget/StoreController.php @@ -67,22 +67,24 @@ class StoreController extends Controller */ public function store(StoreRequest $request): JsonResponse { - $budget = $this->repository->store($request->getAll()); + $data = $request->getAll(); + $data['fire_webhooks'] = $data['fire_webhooks'] ?? true; + $budget = $this->repository->store($data); $budget->refresh(); - $manager = $this->getManager(); + $manager = $this->getManager(); // enrich /** @var User $admin */ - $admin = auth()->user(); - $enrichment = new BudgetEnrichment(); + $admin = auth()->user(); + $enrichment = new BudgetEnrichment(); $enrichment->setUser($admin); - $budget = $enrichment->enrichSingle($budget); + $budget = $enrichment->enrichSingle($budget); /** @var BudgetTransformer $transformer */ $transformer = app(BudgetTransformer::class); $transformer->setParameters($this->parameters); - $resource = new Item($budget, $transformer, 'budgets'); + $resource = new Item($budget, $transformer, 'budgets'); return response()->json($manager->createData($resource)->toArray())->header('Content-Type', self::CONTENT_TYPE); } diff --git a/app/Api/V1/Requests/Models/Budget/StoreRequest.php b/app/Api/V1/Requests/Models/Budget/StoreRequest.php index 9a0118406d..fc17d164dd 100644 --- a/app/Api/V1/Requests/Models/Budget/StoreRequest.php +++ b/app/Api/V1/Requests/Models/Budget/StoreRequest.php @@ -59,6 +59,9 @@ class StoreRequest extends FormRequest 'auto_budget_type' => ['auto_budget_type', 'convertString'], 'auto_budget_amount' => ['auto_budget_amount', 'convertString'], 'auto_budget_period' => ['auto_budget_period', 'convertString'], + + // webhooks + 'fire_webhooks' => ['fire_webhooks','boolean'] ]; return $this->getAllData($fields); @@ -79,6 +82,9 @@ class StoreRequest extends FormRequest 'auto_budget_type' => 'in:reset,rollover,adjusted,none', 'auto_budget_amount' => ['required_if:auto_budget_type,reset', 'required_if:auto_budget_type,rollover', 'required_if:auto_budget_type,adjusted', new IsValidPositiveAmount()], 'auto_budget_period' => 'in:daily,weekly,monthly,quarterly,half_year,yearly|required_if:auto_budget_type,reset|required_if:auto_budget_type,rollover|required_if:auto_budget_type,adjusted', + + // webhooks + 'fire_webhooks' => [new IsBoolean()], ]; } diff --git a/app/Api/V1/Requests/Models/Transaction/StoreRequest.php b/app/Api/V1/Requests/Models/Transaction/StoreRequest.php index a1a20fe4d1..6e85264031 100644 --- a/app/Api/V1/Requests/Models/Transaction/StoreRequest.php +++ b/app/Api/V1/Requests/Models/Transaction/StoreRequest.php @@ -183,6 +183,7 @@ class StoreRequest extends FormRequest // basic fields for group: 'group_title' => 'min:1|max:1000|nullable', 'error_if_duplicate_hash' => [new IsBoolean()], + 'fire_webhooks' => [new IsBoolean()], 'apply_rules' => [new IsBoolean()], // location rules diff --git a/app/Handlers/Observer/BudgetObserver.php b/app/Handlers/Observer/BudgetObserver.php index d7366d3a4b..156946e917 100644 --- a/app/Handlers/Observer/BudgetObserver.php +++ b/app/Handlers/Observer/BudgetObserver.php @@ -31,6 +31,7 @@ use FireflyIII\Models\Budget; use FireflyIII\Models\BudgetLimit; use FireflyIII\Repositories\Attachment\AttachmentRepositoryInterface; use FireflyIII\Support\Observers\RecalculatesAvailableBudgetsTrait; +use FireflyIII\Support\Singleton\PreferencesSingleton; use Illuminate\Support\Collection; use Illuminate\Support\Facades\Log; @@ -45,23 +46,28 @@ class BudgetObserver { Log::debug(sprintf('Observe "created" of budget #%d ("%s").', $budget->id, $budget->name)); - // fire event. - $user = $budget->user; + // this is a lame trick to communicate with the observer. + $singleton = PreferencesSingleton::getInstance(); - /** @var MessageGeneratorInterface $engine */ - $engine = app(MessageGeneratorInterface::class); - $engine->setUser($user); - $engine->setObjects(new Collection()->push($budget)); - $engine->setTrigger(WebhookTrigger::STORE_BUDGET); - $engine->generateMessages(); - Log::debug(sprintf('send event RequestedSendWebhookMessages from %s', __METHOD__)); - event(new RequestedSendWebhookMessages()); + if (true === $singleton->getPreference('fire_webhooks_budget_create')) { + // fire event. + $user = $budget->user; + + /** @var MessageGeneratorInterface $engine */ + $engine = app(MessageGeneratorInterface::class); + $engine->setUser($user); + $engine->setObjects(new Collection()->push($budget)); + $engine->setTrigger(WebhookTrigger::STORE_BUDGET); + $engine->generateMessages(); + Log::debug(sprintf('send event RequestedSendWebhookMessages from %s', __METHOD__)); + event(new RequestedSendWebhookMessages()); + } } public function updated(Budget $budget): void { Log::debug(sprintf('Observe "updated" of budget #%d ("%s").', $budget->id, $budget->name)); - $user = $budget->user; + $user = $budget->user; /** @var MessageGeneratorInterface $engine */ $engine = app(MessageGeneratorInterface::class); @@ -77,10 +83,10 @@ class BudgetObserver { Log::debug('Observe "deleting" of a budget.'); - $user = $budget->user; + $user = $budget->user; /** @var MessageGeneratorInterface $engine */ - $engine = app(MessageGeneratorInterface::class); + $engine = app(MessageGeneratorInterface::class); $engine->setUser($user); $engine->setObjects(new Collection()->push($budget)); $engine->setTrigger(WebhookTrigger::DESTROY_BUDGET); @@ -88,7 +94,7 @@ class BudgetObserver Log::debug(sprintf('send event RequestedSendWebhookMessages from %s', __METHOD__)); event(new RequestedSendWebhookMessages()); - $repository = app(AttachmentRepositoryInterface::class); + $repository = app(AttachmentRepositoryInterface::class); $repository->setUser($budget->user); /** @var Attachment $attachment */ diff --git a/app/Repositories/Budget/BudgetRepository.php b/app/Repositories/Budget/BudgetRepository.php index 3aa57328de..8c39398b69 100644 --- a/app/Repositories/Budget/BudgetRepository.php +++ b/app/Repositories/Budget/BudgetRepository.php @@ -44,6 +44,7 @@ use FireflyIII\Support\Facades\Steam; use FireflyIII\Support\Http\Api\ExchangeRateConverter; use FireflyIII\Support\Repositories\UserGroup\UserGroupInterface; use FireflyIII\Support\Repositories\UserGroup\UserGroupTrait; +use FireflyIII\Support\Singleton\PreferencesSingleton; use Illuminate\Database\QueryException; use Illuminate\Support\Collection; use Illuminate\Support\Facades\DB; @@ -724,6 +725,10 @@ class BudgetRepository implements BudgetRepositoryInterface, UserGroupInterface { $order = $this->getMaxOrder(); + // this is a lame trick to communicate with the observer. + $singleton = PreferencesSingleton::getInstance(); + $singleton->setPreference('fire_webhooks_budget_create', $data['fire_webhooks'] ?? true); + try { $newBudget = Budget::create( [ diff --git a/app/Support/Request/ConvertsDataTypes.php b/app/Support/Request/ConvertsDataTypes.php index c90541b081..bc8da96efb 100644 --- a/app/Support/Request/ConvertsDataTypes.php +++ b/app/Support/Request/ConvertsDataTypes.php @@ -258,6 +258,12 @@ trait ConvertsDataTypes if ('yes' === $value) { return true; } + if ('on' === $value) { + return true; + } + if ('y' === $value) { + return true; + } if ('1' === $value) { return true; } From 935453796e3284ad1163ca056bf2ca01762dcdd2 Mon Sep 17 00:00:00 2001 From: James Cole Date: Sun, 14 Sep 2025 08:59:00 +0200 Subject: [PATCH 06/58] Allow budget update to have webhooks controlled with "fire_webhooks" --- .../Models/Budget/UpdateController.php | 21 +++---- .../Requests/Models/Budget/UpdateRequest.php | 6 ++ app/Handlers/Observer/BudgetObserver.php | 24 +++++--- app/Repositories/Budget/BudgetRepository.php | 56 ++++++++++--------- 4 files changed, 59 insertions(+), 48 deletions(-) diff --git a/app/Api/V1/Controllers/Models/Budget/UpdateController.php b/app/Api/V1/Controllers/Models/Budget/UpdateController.php index b6524ba738..8ec009b7e1 100644 --- a/app/Api/V1/Controllers/Models/Budget/UpdateController.php +++ b/app/Api/V1/Controllers/Models/Budget/UpdateController.php @@ -57,30 +57,25 @@ class UpdateController extends Controller ); } - /** - * This endpoint is documented at: - * https://api-docs.firefly-iii.org/?urls.primaryName=2.0.0%20(v1)#/budgets/updateBudget - * - * Update a budget. - */ public function update(UpdateRequest $request, Budget $budget): JsonResponse { - $data = $request->getAll(); - $budget = $this->repository->update($budget, $data); - $manager = $this->getManager(); + $data = $request->getAll(); + $data['fire_webhooks'] = $data['fire_webhooks'] ?? true; + $budget = $this->repository->update($budget, $data); + $manager = $this->getManager(); // enrich /** @var User $admin */ - $admin = auth()->user(); - $enrichment = new BudgetEnrichment(); + $admin = auth()->user(); + $enrichment = new BudgetEnrichment(); $enrichment->setUser($admin); - $budget = $enrichment->enrichSingle($budget); + $budget = $enrichment->enrichSingle($budget); /** @var BudgetTransformer $transformer */ $transformer = app(BudgetTransformer::class); $transformer->setParameters($this->parameters); - $resource = new Item($budget, $transformer, 'budgets'); + $resource = new Item($budget, $transformer, 'budgets'); return response()->json($manager->createData($resource)->toArray())->header('Content-Type', self::CONTENT_TYPE); } diff --git a/app/Api/V1/Requests/Models/Budget/UpdateRequest.php b/app/Api/V1/Requests/Models/Budget/UpdateRequest.php index 6eb0dc7acc..3a89dbaa15 100644 --- a/app/Api/V1/Requests/Models/Budget/UpdateRequest.php +++ b/app/Api/V1/Requests/Models/Budget/UpdateRequest.php @@ -59,6 +59,9 @@ class UpdateRequest extends FormRequest 'auto_budget_type' => ['auto_budget_type', 'convertString'], 'auto_budget_amount' => ['auto_budget_amount', 'convertString'], 'auto_budget_period' => ['auto_budget_period', 'convertString'], + + // webhooks + 'fire_webhooks' => ['fire_webhooks','boolean'] ]; $allData = $this->getAllData($fields); if (array_key_exists('auto_budget_type', $allData)) { @@ -91,6 +94,9 @@ class UpdateRequest extends FormRequest 'auto_budget_currency_code' => 'exists:transaction_currencies,code', 'auto_budget_amount' => ['nullable', new IsValidPositiveAmount()], 'auto_budget_period' => 'in:daily,weekly,monthly,quarterly,half_year,yearly', + + // webhooks + 'fire_webhooks' => [new IsBoolean()], ]; } diff --git a/app/Handlers/Observer/BudgetObserver.php b/app/Handlers/Observer/BudgetObserver.php index 156946e917..9030c4f955 100644 --- a/app/Handlers/Observer/BudgetObserver.php +++ b/app/Handlers/Observer/BudgetObserver.php @@ -67,16 +67,22 @@ class BudgetObserver public function updated(Budget $budget): void { Log::debug(sprintf('Observe "updated" of budget #%d ("%s").', $budget->id, $budget->name)); - $user = $budget->user; - /** @var MessageGeneratorInterface $engine */ - $engine = app(MessageGeneratorInterface::class); - $engine->setUser($user); - $engine->setObjects(new Collection()->push($budget)); - $engine->setTrigger(WebhookTrigger::UPDATE_BUDGET); - $engine->generateMessages(); - Log::debug(sprintf('send event RequestedSendWebhookMessages from %s', __METHOD__)); - event(new RequestedSendWebhookMessages()); + // this is a lame trick to communicate with the observer. + $singleton = PreferencesSingleton::getInstance(); + + if (true === $singleton->getPreference('fire_webhooks_budget_update')) { + $user = $budget->user; + + /** @var MessageGeneratorInterface $engine */ + $engine = app(MessageGeneratorInterface::class); + $engine->setUser($user); + $engine->setObjects(new Collection()->push($budget)); + $engine->setTrigger(WebhookTrigger::UPDATE_BUDGET); + $engine->generateMessages(); + Log::debug(sprintf('send event RequestedSendWebhookMessages from %s', __METHOD__)); + event(new RequestedSendWebhookMessages()); + } } public function deleting(Budget $budget): void diff --git a/app/Repositories/Budget/BudgetRepository.php b/app/Repositories/Budget/BudgetRepository.php index 8c39398b69..b893334320 100644 --- a/app/Repositories/Budget/BudgetRepository.php +++ b/app/Repositories/Budget/BudgetRepository.php @@ -86,7 +86,7 @@ class BudgetRepository implements BudgetRepositoryInterface, UserGroupInterface public function budgetedInPeriod(Carbon $start, Carbon $end): array { - app('log')->debug(sprintf('Now in budgetedInPeriod("%s", "%s")', $start->format('Y-m-d'), $end->format('Y-m-d'))); + Log::debug(sprintf('Now in budgetedInPeriod("%s", "%s")', $start->format('Y-m-d'), $end->format('Y-m-d'))); $return = []; /** @var BudgetLimitRepository $limitRepository */ @@ -98,12 +98,12 @@ class BudgetRepository implements BudgetRepositoryInterface, UserGroupInterface /** @var Budget $budget */ foreach ($budgets as $budget) { - app('log')->debug(sprintf('Budget #%d: "%s"', $budget->id, $budget->name)); + Log::debug(sprintf('Budget #%d: "%s"', $budget->id, $budget->name)); $limits = $limitRepository->getBudgetLimits($budget, $start, $end); /** @var BudgetLimit $limit */ foreach ($limits as $limit) { - app('log')->debug(sprintf('Budget limit #%d', $limit->id)); + Log::debug(sprintf('Budget limit #%d', $limit->id)); $currency = $limit->transactionCurrency; $rate = $converter->getCurrencyRate($currency, $primaryCurrency, $end); $currencyCode = $currency->code; @@ -125,7 +125,7 @@ class BudgetRepository implements BudgetRepositoryInterface, UserGroupInterface if ($limit->start_date->isSameDay($start) && $limit->end_date->isSameDay($end)) { $return[$currencyCode]['sum'] = bcadd($return[$currencyCode]['sum'], (string) $limit->amount); $return[$currencyCode]['pc_sum'] = bcmul($rate, $return[$currencyCode]['sum']); - app('log')->debug(sprintf('Add full amount [1]: %s', $limit->amount)); + Log::debug(sprintf('Add full amount [1]: %s', $limit->amount)); continue; } @@ -133,7 +133,7 @@ class BudgetRepository implements BudgetRepositoryInterface, UserGroupInterface if ($start->lte($limit->start_date) && $end->gte($limit->end_date)) { $return[$currencyCode]['sum'] = bcadd($return[$currencyCode]['sum'], (string) $limit->amount); $return[$currencyCode]['pc_sum'] = bcmul($rate, $return[$currencyCode]['sum']); - app('log')->debug(sprintf('Add full amount [2]: %s', $limit->amount)); + Log::debug(sprintf('Add full amount [2]: %s', $limit->amount)); continue; } @@ -142,7 +142,7 @@ class BudgetRepository implements BudgetRepositoryInterface, UserGroupInterface $amount = bcmul(bcdiv((string) $limit->amount, (string) $total), (string) $days); $return[$currencyCode]['sum'] = bcadd($return[$currencyCode]['sum'], $amount); $return[$currencyCode]['pc_sum'] = bcmul($rate, $return[$currencyCode]['sum']); - app('log')->debug( + Log::debug( sprintf( 'Amount per day: %s (%s over %d days). Total amount for %d days: %s', bcdiv((string) $limit->amount, (string) $total), @@ -203,19 +203,19 @@ class BudgetRepository implements BudgetRepositoryInterface, UserGroupInterface public function budgetedInPeriodForBudget(Budget $budget, Carbon $start, Carbon $end): array { - app('log')->debug(sprintf('Now in budgetedInPeriod(#%d, "%s", "%s")', $budget->id, $start->format('Y-m-d'), $end->format('Y-m-d'))); + Log::debug(sprintf('Now in budgetedInPeriod(#%d, "%s", "%s")', $budget->id, $start->format('Y-m-d'), $end->format('Y-m-d'))); $return = []; /** @var BudgetLimitRepository $limitRepository */ $limitRepository = app(BudgetLimitRepository::class); $limitRepository->setUser($this->user); - app('log')->debug(sprintf('Budget #%d: "%s"', $budget->id, $budget->name)); + Log::debug(sprintf('Budget #%d: "%s"', $budget->id, $budget->name)); $limits = $limitRepository->getBudgetLimits($budget, $start, $end); /** @var BudgetLimit $limit */ foreach ($limits as $limit) { - app('log')->debug(sprintf('Budget limit #%d', $limit->id)); + Log::debug(sprintf('Budget limit #%d', $limit->id)); $currency = $limit->transactionCurrency; $return[$currency->id] ??= [ 'id' => (string) $currency->id, @@ -228,14 +228,14 @@ class BudgetRepository implements BudgetRepositoryInterface, UserGroupInterface // same period if ($limit->start_date->isSameDay($start) && $limit->end_date->isSameDay($end)) { $return[$currency->id]['sum'] = bcadd($return[$currency->id]['sum'], (string) $limit->amount); - app('log')->debug(sprintf('Add full amount [1]: %s', $limit->amount)); + Log::debug(sprintf('Add full amount [1]: %s', $limit->amount)); continue; } // limit is inside of date range if ($start->lte($limit->start_date) && $end->gte($limit->end_date)) { $return[$currency->id]['sum'] = bcadd($return[$currency->id]['sum'], (string) $limit->amount); - app('log')->debug(sprintf('Add full amount [2]: %s', $limit->amount)); + Log::debug(sprintf('Add full amount [2]: %s', $limit->amount)); continue; } @@ -243,7 +243,7 @@ class BudgetRepository implements BudgetRepositoryInterface, UserGroupInterface $days = $this->daysInOverlap($limit, $start, $end); $amount = bcmul(bcdiv((string) $limit->amount, (string) $total), (string) $days); $return[$currency->id]['sum'] = bcadd($return[$currency->id]['sum'], $amount); - app('log')->debug( + Log::debug( sprintf( 'Amount per day: %s (%s over %d days). Total amount for %d days: %s', bcdiv((string) $limit->amount, (string) $total), @@ -283,7 +283,11 @@ class BudgetRepository implements BudgetRepositoryInterface, UserGroupInterface */ public function update(Budget $budget, array $data): Budget { - app('log')->debug('Now in update()'); + Log::debug('Now in update()'); + + // this is a lame trick to communicate with the observer. + $singleton = PreferencesSingleton::getInstance(); + $singleton->setPreference('fire_webhooks_budget_update', $data['fire_webhooks'] ?? true); $oldName = $budget->name; if (array_key_exists('name', $data)) { @@ -331,13 +335,13 @@ class BudgetRepository implements BudgetRepositoryInterface, UserGroupInterface ->where('rule_actions.action_value', $oldName) ->get(['rule_actions.*']) ; - app('log')->debug(sprintf('Found %d actions to update.', $actions->count())); + Log::debug(sprintf('Found %d actions to update.', $actions->count())); /** @var RuleAction $action */ foreach ($actions as $action) { $action->action_value = $newName; $action->save(); - app('log')->debug(sprintf('Updated action %d: %s', $action->id, $action->action_value)); + Log::debug(sprintf('Updated action %d: %s', $action->id, $action->action_value)); } } @@ -350,13 +354,13 @@ class BudgetRepository implements BudgetRepositoryInterface, UserGroupInterface ->where('rule_triggers.trigger_value', $oldName) ->get(['rule_triggers.*']) ; - app('log')->debug(sprintf('Found %d triggers to update.', $triggers->count())); + Log::debug(sprintf('Found %d triggers to update.', $triggers->count())); /** @var RuleTrigger $trigger */ foreach ($triggers as $trigger) { $trigger->trigger_value = $newName; $trigger->save(); - app('log')->debug(sprintf('Updated trigger %d: %s', $trigger->id, $trigger->trigger_value)); + Log::debug(sprintf('Updated trigger %d: %s', $trigger->id, $trigger->trigger_value)); } } @@ -487,17 +491,17 @@ class BudgetRepository implements BudgetRepositoryInterface, UserGroupInterface public function findBudget(?int $budgetId, ?string $budgetName): ?Budget { - app('log')->debug('Now in findBudget()'); - app('log')->debug(sprintf('Searching for budget with ID #%d...', $budgetId)); + Log::debug('Now in findBudget()'); + Log::debug(sprintf('Searching for budget with ID #%d...', $budgetId)); $result = $this->find((int) $budgetId); if (!$result instanceof Budget && null !== $budgetName && '' !== $budgetName) { - app('log')->debug(sprintf('Searching for budget with name %s...', $budgetName)); + Log::debug(sprintf('Searching for budget with name %s...', $budgetName)); $result = $this->findByName($budgetName); } if ($result instanceof Budget) { - app('log')->debug(sprintf('Found budget #%d: %s', $result->id, $result->name)); + Log::debug(sprintf('Found budget #%d: %s', $result->id, $result->name)); } - app('log')->debug(sprintf('Found result is null? %s', var_export(!$result instanceof Budget, true))); + Log::debug(sprintf('Found result is null? %s', var_export(!$result instanceof Budget, true))); return $result; } @@ -594,7 +598,7 @@ class BudgetRepository implements BudgetRepositoryInterface, UserGroupInterface public function spentInPeriod(Carbon $start, Carbon $end): array { - app('log')->debug(sprintf('Now in %s', __METHOD__)); + Log::debug(sprintf('Now in %s', __METHOD__)); $start->startOfDay(); $end->endOfDay(); @@ -656,7 +660,7 @@ class BudgetRepository implements BudgetRepositoryInterface, UserGroupInterface public function spentInPeriodForBudget(Budget $budget, Carbon $start, Carbon $end): array { - app('log')->debug(sprintf('Now in %s', __METHOD__)); + Log::debug(sprintf('Now in %s', __METHOD__)); $start->startOfDay(); $end->endOfDay(); @@ -740,8 +744,8 @@ class BudgetRepository implements BudgetRepositoryInterface, UserGroupInterface ] ); } catch (QueryException $e) { - app('log')->error($e->getMessage()); - app('log')->error($e->getTraceAsString()); + Log::error($e->getMessage()); + Log::error($e->getTraceAsString()); throw new FireflyException('400002: Could not store budget.', 0, $e); } From 9d9483e20f9563631ab38eba1f57a8b1feef2ad0 Mon Sep 17 00:00:00 2001 From: James Cole Date: Sun, 14 Sep 2025 09:00:01 +0200 Subject: [PATCH 07/58] Refactor models. --- app/Models/Account.php | 149 +++++++++++----------- app/Models/AccountMeta.php | 13 +- app/Models/AccountType.php | 72 ++++++----- app/Models/Attachment.php | 10 +- app/Models/AuditLogEntry.php | 16 +-- app/Models/AvailableBudget.php | 32 ++--- app/Models/Bill.php | 42 +++---- app/Models/Budget.php | 22 ++-- app/Models/BudgetLimit.php | 14 +-- app/Models/Category.php | 9 +- app/Models/Configuration.php | 19 ++- app/Models/CurrencyExchangeRate.php | 57 ++++----- app/Models/GroupMembership.php | 14 +-- app/Models/InvitedUser.php | 5 +- app/Models/LinkType.php | 2 +- app/Models/Location.php | 14 +-- app/Models/Note.php | 14 +-- app/Models/ObjectGroup.php | 22 ++-- app/Models/PiggyBank.php | 32 ++--- app/Models/PiggyBankEvent.php | 14 +-- app/Models/PiggyBankRepetition.php | 87 +++++++------ app/Models/Preference.php | 14 +-- app/Models/Recurrence.php | 14 +-- app/Models/RecurrenceMeta.php | 16 +-- app/Models/RecurrenceRepetition.php | 30 +++-- app/Models/RecurrenceTransaction.php | 24 ++-- app/Models/RecurrenceTransactionMeta.php | 16 +-- app/Models/Rule.php | 38 +++--- app/Models/RuleAction.php | 32 ++--- app/Models/RuleGroup.php | 21 ++-- app/Models/RuleTrigger.php | 28 ++--- app/Models/Tag.php | 9 +- app/Models/Transaction.php | 151 +++++++++++------------ app/Models/TransactionCurrency.php | 18 +-- app/Models/TransactionGroup.php | 12 +- app/Models/TransactionJournal.php | 96 +++++++------- app/Models/TransactionJournalLink.php | 55 ++++----- app/Models/TransactionJournalMeta.php | 35 +++--- app/Models/TransactionType.php | 37 +++--- app/Models/UserGroup.php | 8 +- app/Models/Webhook.php | 15 ++- app/Models/WebhookAttempt.php | 8 +- app/Models/WebhookDelivery.php | 2 +- app/Models/WebhookMessage.php | 22 ++-- app/Models/WebhookResponse.php | 2 +- app/Models/WebhookTrigger.php | 2 +- 46 files changed, 688 insertions(+), 676 deletions(-) diff --git a/app/Models/Account.php b/app/Models/Account.php index eef5d6fcd5..aedc24f00b 100644 --- a/app/Models/Account.php +++ b/app/Models/Account.php @@ -23,13 +23,13 @@ declare(strict_types=1); namespace FireflyIII\Models; -use FireflyIII\Handlers\Observer\AccountObserver; -use Illuminate\Database\Eloquent\Attributes\ObservedBy; -use Illuminate\Database\Eloquent\Attributes\Scope; use FireflyIII\Enums\AccountTypeEnum; +use FireflyIII\Handlers\Observer\AccountObserver; use FireflyIII\Support\Models\ReturnsIntegerIdTrait; use FireflyIII\Support\Models\ReturnsIntegerUserIdTrait; use FireflyIII\User; +use Illuminate\Database\Eloquent\Attributes\ObservedBy; +use Illuminate\Database\Eloquent\Attributes\Scope; use Illuminate\Database\Eloquent\Builder as EloquentBuilder; use Illuminate\Database\Eloquent\Casts\Attribute; use Illuminate\Database\Eloquent\Factories\HasFactory; @@ -50,9 +50,9 @@ class Account extends Model use ReturnsIntegerUserIdTrait; use SoftDeletes; - protected $fillable = ['user_id', 'user_group_id', 'account_type_id', 'name', 'active', 'virtual_balance', 'iban', 'native_virtual_balance']; + protected $fillable = ['user_id', 'user_group_id', 'account_type_id', 'name', 'active', 'virtual_balance', 'iban', 'native_virtual_balance']; - protected $hidden = ['encrypted']; + protected $hidden = ['encrypted']; private bool $joinedAccountTypes = false; /** @@ -63,13 +63,13 @@ class Account extends Model public static function routeBinder(string $value): self { if (auth()->check()) { - $accountId = (int) $value; + $accountId = (int)$value; /** @var User $user */ - $user = auth()->user(); + $user = auth()->user(); /** @var null|Account $account */ - $account = $user->accounts()->with(['accountType'])->find($accountId); + $account = $user->accounts()->with(['accountType'])->find($accountId); if (null !== $account) { return $account; } @@ -98,39 +98,6 @@ class Account extends Model return $this->morphMany(Attachment::class, 'attachable'); } - /** - * Get the account number. - */ - protected function accountNumber(): Attribute - { - return Attribute::make(get: function () { - /** @var null|AccountMeta $metaValue */ - $metaValue = $this->accountMeta() - ->where('name', 'account_number') - ->first() - ; - - return null !== $metaValue ? $metaValue->data : ''; - }); - } - - public function accountMeta(): HasMany - { - return $this->hasMany(AccountMeta::class); - } - - protected function editName(): Attribute - { - return Attribute::make(get: function () { - $name = $this->name; - if (AccountTypeEnum::CASH->value === $this->accountType->type) { - return ''; - } - - return $name; - }); - } - public function locations(): MorphMany { return $this->morphMany(Location::class, 'locatable'); @@ -157,19 +124,9 @@ class Account extends Model return $this->belongsToMany(PiggyBank::class); } - #[Scope] - protected function accountTypeIn(EloquentBuilder $query, array $types): void - { - if (false === $this->joinedAccountTypes) { - $query->leftJoin('account_types', 'account_types.id', '=', 'accounts.account_type_id'); - $this->joinedAccountTypes = true; - } - $query->whereIn('account_types.type', $types); - } - public function setVirtualBalanceAttribute(mixed $value): void { - $value = (string) $value; + $value = (string)$value; if ('' === $value) { $value = null; } @@ -189,42 +146,48 @@ class Account extends Model protected function accountId(): Attribute { return Attribute::make( - get: static fn ($value) => (int) $value, + get: static fn($value) => (int)$value, ); } + /** + * Get the account number. + */ + protected function accountNumber(): Attribute + { + return Attribute::make(get: function () { + /** @var null|AccountMeta $metaValue */ + $metaValue = $this->accountMeta() + ->where('name', 'account_number') + ->first(); + + return null !== $metaValue ? $metaValue->data : ''; + }); + } + + public function accountMeta(): HasMany + { + return $this->hasMany(AccountMeta::class); + } + /** * Get the user ID */ protected function accountTypeId(): Attribute { return Attribute::make( - get: static fn ($value) => (int) $value, + get: static fn($value) => (int)$value, ); } - protected function iban(): Attribute + #[Scope] + protected function accountTypeIn(EloquentBuilder $query, array $types): void { - return Attribute::make( - get: static fn ($value) => null === $value ? null : trim(str_replace(' ', '', (string) $value)), - ); - } - - protected function order(): Attribute - { - return Attribute::make( - get: static fn ($value) => (int) $value, - ); - } - - /** - * Get the virtual balance - */ - protected function virtualBalance(): Attribute - { - return Attribute::make( - get: static fn ($value) => (string) $value, - ); + if (false === $this->joinedAccountTypes) { + $query->leftJoin('account_types', 'account_types.id', '=', 'accounts.account_type_id'); + $this->joinedAccountTypes = true; + } + $query->whereIn('account_types.type', $types); } protected function casts(): array @@ -241,4 +204,40 @@ class Account extends Model 'native_virtual_balance' => 'string', ]; } + + protected function editName(): Attribute + { + return Attribute::make(get: function () { + $name = $this->name; + if (AccountTypeEnum::CASH->value === $this->accountType->type) { + return ''; + } + + return $name; + }); + } + + protected function iban(): Attribute + { + return Attribute::make( + get: static fn($value) => null === $value ? null : trim(str_replace(' ', '', (string)$value)), + ); + } + + protected function order(): Attribute + { + return Attribute::make( + get: static fn($value) => (int)$value, + ); + } + + /** + * Get the virtual balance + */ + protected function virtualBalance(): Attribute + { + return Attribute::make( + get: static fn($value) => (string)$value, + ); + } } diff --git a/app/Models/AccountMeta.php b/app/Models/AccountMeta.php index 61d8644ff3..ef87a0a508 100644 --- a/app/Models/AccountMeta.php +++ b/app/Models/AccountMeta.php @@ -23,11 +23,10 @@ declare(strict_types=1); namespace FireflyIII\Models; -use Illuminate\Database\Eloquent\Casts\Attribute; use FireflyIII\Support\Models\ReturnsIntegerIdTrait; +use Illuminate\Database\Eloquent\Casts\Attribute; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo; - use function Safe\json_decode; use function Safe\json_encode; @@ -43,11 +42,6 @@ class AccountMeta extends Model return $this->belongsTo(Account::class); } - protected function data(): Attribute - { - return Attribute::make(get: fn (mixed $value) => (string) json_decode((string) $value, true), set: fn (mixed $value) => ['data' => json_encode($value)]); - } - protected function casts(): array { return [ @@ -55,4 +49,9 @@ class AccountMeta extends Model 'updated_at' => 'datetime', ]; } + + protected function data(): Attribute + { + return Attribute::make(get: fn(mixed $value) => (string)json_decode((string)$value, true), set: fn(mixed $value) => ['data' => json_encode($value)]); + } } diff --git a/app/Models/AccountType.php b/app/Models/AccountType.php index d4571fde77..b147593f38 100644 --- a/app/Models/AccountType.php +++ b/app/Models/AccountType.php @@ -32,55 +32,69 @@ class AccountType extends Model { use ReturnsIntegerIdTrait; - #[Deprecated] /** @deprecated */ - public const string ASSET = 'Asset account'; + #[Deprecated] + /** @deprecated */ + public const string ASSET = 'Asset account'; - #[Deprecated] /** @deprecated */ - public const string BENEFICIARY = 'Beneficiary account'; + #[Deprecated] + /** @deprecated */ + public const string BENEFICIARY = 'Beneficiary account'; - #[Deprecated] /** @deprecated */ - public const string CASH = 'Cash account'; + #[Deprecated] + /** @deprecated */ + public const string CASH = 'Cash account'; - #[Deprecated] /** @deprecated */ - public const string CREDITCARD = 'Credit card'; + #[Deprecated] + /** @deprecated */ + public const string CREDITCARD = 'Credit card'; - #[Deprecated] /** @deprecated */ - public const string DEBT = 'Debt'; + #[Deprecated] + /** @deprecated */ + public const string DEBT = 'Debt'; - #[Deprecated] /** @deprecated */ - public const string DEFAULT = 'Default account'; + #[Deprecated] + /** @deprecated */ + public const string DEFAULT = 'Default account'; - #[Deprecated] /** @deprecated */ - public const string EXPENSE = 'Expense account'; + #[Deprecated] + /** @deprecated */ + public const string EXPENSE = 'Expense account'; - #[Deprecated] /** @deprecated */ - public const string IMPORT = 'Import account'; + #[Deprecated] + /** @deprecated */ + public const string IMPORT = 'Import account'; - #[Deprecated] /** @deprecated */ - public const string INITIAL_BALANCE = 'Initial balance account'; + #[Deprecated] + /** @deprecated */ + public const string INITIAL_BALANCE = 'Initial balance account'; - #[Deprecated] /** @deprecated */ + #[Deprecated] + /** @deprecated */ public const string LIABILITY_CREDIT = 'Liability credit account'; - #[Deprecated] /** @deprecated */ - public const string LOAN = 'Loan'; + #[Deprecated] + /** @deprecated */ + public const string LOAN = 'Loan'; - #[Deprecated] /** @deprecated */ - public const string MORTGAGE = 'Mortgage'; + #[Deprecated] + /** @deprecated */ + public const string MORTGAGE = 'Mortgage'; - #[Deprecated] /** @deprecated */ - public const string RECONCILIATION = 'Reconciliation account'; + #[Deprecated] + /** @deprecated */ + public const string RECONCILIATION = 'Reconciliation account'; - #[Deprecated] /** @deprecated */ - public const string REVENUE = 'Revenue account'; + #[Deprecated] + /** @deprecated */ + public const string REVENUE = 'Revenue account'; protected $casts - = [ + = [ 'created_at' => 'datetime', 'updated_at' => 'datetime', ]; - protected $fillable = ['type']; + protected $fillable = ['type']; public function accounts(): HasMany { diff --git a/app/Models/Attachment.php b/app/Models/Attachment.php index ed5a20bab3..dd468558ab 100644 --- a/app/Models/Attachment.php +++ b/app/Models/Attachment.php @@ -53,13 +53,13 @@ class Attachment extends Model public static function routeBinder(string $value): self { if (auth()->check()) { - $attachmentId = (int) $value; + $attachmentId = (int)$value; /** @var User $user */ - $user = auth()->user(); + $user = auth()->user(); /** @var null|Attachment $attachment */ - $attachment = $user->attachments()->find($attachmentId); + $attachment = $user->attachments()->find($attachmentId); if (null !== $attachment) { return $attachment; } @@ -86,7 +86,7 @@ class Attachment extends Model */ public function fileName(): string { - return sprintf('at-%s.data', (string) $this->id); + return sprintf('at-%s.data', (string)$this->id); } /** @@ -100,7 +100,7 @@ class Attachment extends Model protected function attachableId(): Attribute { return Attribute::make( - get: static fn ($value) => (int) $value, + get: static fn($value) => (int)$value, ); } diff --git a/app/Models/AuditLogEntry.php b/app/Models/AuditLogEntry.php index 53773692a1..baf532bfb0 100644 --- a/app/Models/AuditLogEntry.php +++ b/app/Models/AuditLogEntry.php @@ -48,14 +48,7 @@ class AuditLogEntry extends Model protected function auditableId(): Attribute { return Attribute::make( - get: static fn ($value) => (int) $value, - ); - } - - protected function changerId(): Attribute - { - return Attribute::make( - get: static fn ($value) => (int) $value, + get: static fn($value) => (int)$value, ); } @@ -69,4 +62,11 @@ class AuditLogEntry extends Model 'deleted_at' => 'datetime', ]; } + + protected function changerId(): Attribute + { + return Attribute::make( + get: static fn($value) => (int)$value, + ); + } } diff --git a/app/Models/AvailableBudget.php b/app/Models/AvailableBudget.php index f26f7f33d7..f3f63b4e06 100644 --- a/app/Models/AvailableBudget.php +++ b/app/Models/AvailableBudget.php @@ -84,6 +84,22 @@ class AvailableBudget extends Model ); } + protected function casts(): array + { + return [ + 'created_at' => 'datetime', + 'updated_at' => 'datetime', + 'deleted_at' => 'datetime', + 'start_date' => 'date', + 'end_date' => 'date', + 'transaction_currency_id' => 'int', + 'amount' => 'string', + 'native_amount' => 'string', + 'user_id' => 'integer', + 'user_group_id' => 'integer', + ]; + } + protected function endDate(): Attribute { return Attribute::make( @@ -106,20 +122,4 @@ class AvailableBudget extends Model get: static fn($value) => (int)$value, ); } - - protected function casts(): array - { - return [ - 'created_at' => 'datetime', - 'updated_at' => 'datetime', - 'deleted_at' => 'datetime', - 'start_date' => 'date', - 'end_date' => 'date', - 'transaction_currency_id' => 'int', - 'amount' => 'string', - 'native_amount' => 'string', - 'user_id' => 'integer', - 'user_group_id' => 'integer', - ]; - } } diff --git a/app/Models/Bill.php b/app/Models/Bill.php index e44d0f3098..a84552ba82 100644 --- a/app/Models/Bill.php +++ b/app/Models/Bill.php @@ -165,6 +165,27 @@ class Bill extends Model ); } + protected function casts(): array + { + return [ + 'created_at' => 'datetime', + 'updated_at' => 'datetime', + 'deleted_at' => 'datetime', + 'date' => SeparateTimezoneCaster::class, + 'end_date' => SeparateTimezoneCaster::class, + 'extension_date' => SeparateTimezoneCaster::class, + 'skip' => 'int', + 'automatch' => 'boolean', + 'active' => 'boolean', + 'name_encrypted' => 'boolean', + 'match_encrypted' => 'boolean', + 'amount_min' => 'string', + 'amount_max' => 'string', + 'native_amount_min' => 'string', + 'native_amount_max' => 'string', + ]; + } + protected function order(): Attribute { return Attribute::make( @@ -188,25 +209,4 @@ class Bill extends Model get: static fn($value) => (int)$value, ); } - - protected function casts(): array - { - return [ - 'created_at' => 'datetime', - 'updated_at' => 'datetime', - 'deleted_at' => 'datetime', - 'date' => SeparateTimezoneCaster::class, - 'end_date' => SeparateTimezoneCaster::class, - 'extension_date' => SeparateTimezoneCaster::class, - 'skip' => 'int', - 'automatch' => 'boolean', - 'active' => 'boolean', - 'name_encrypted' => 'boolean', - 'match_encrypted' => 'boolean', - 'amount_min' => 'string', - 'amount_max' => 'string', - 'native_amount_min' => 'string', - 'native_amount_max' => 'string', - ]; - } } diff --git a/app/Models/Budget.php b/app/Models/Budget.php index 086764d66d..1a1c2bb9dc 100644 --- a/app/Models/Budget.php +++ b/app/Models/Budget.php @@ -46,7 +46,7 @@ class Budget extends Model protected $fillable = ['user_id', 'user_group_id', 'name', 'active', 'order', 'user_group_id']; - protected $hidden = ['encrypted']; + protected $hidden = ['encrypted']; /** * Route binder. Converts the key in the URL to the specified object (or throw 404). @@ -56,13 +56,13 @@ class Budget extends Model public static function routeBinder(string $value): self { if (auth()->check()) { - $budgetId = (int) $value; + $budgetId = (int)$value; /** @var User $user */ - $user = auth()->user(); + $user = auth()->user(); /** @var null|Budget $budget */ - $budget = $user->budgets()->find($budgetId); + $budget = $user->budgets()->find($budgetId); if (null !== $budget) { return $budget; } @@ -109,13 +109,6 @@ class Budget extends Model return $this->belongsToMany(Transaction::class, 'budget_transaction', 'budget_id'); } - protected function order(): Attribute - { - return Attribute::make( - get: static fn ($value) => (int) $value, - ); - } - protected function casts(): array { return [ @@ -128,4 +121,11 @@ class Budget extends Model 'user_group_id' => 'integer', ]; } + + protected function order(): Attribute + { + return Attribute::make( + get: static fn($value) => (int)$value, + ); + } } diff --git a/app/Models/BudgetLimit.php b/app/Models/BudgetLimit.php index 247cf24ad4..a646d4d1e2 100644 --- a/app/Models/BudgetLimit.php +++ b/app/Models/BudgetLimit.php @@ -96,13 +96,6 @@ class BudgetLimit extends Model ); } - protected function transactionCurrencyId(): Attribute - { - return Attribute::make( - get: static fn($value) => (int)$value, - ); - } - protected function casts(): array { return [ @@ -115,4 +108,11 @@ class BudgetLimit extends Model 'native_amount' => 'string', ]; } + + protected function transactionCurrencyId(): Attribute + { + return Attribute::make( + get: static fn($value) => (int)$value, + ); + } } diff --git a/app/Models/Category.php b/app/Models/Category.php index 1f961f561d..1726c1110c 100644 --- a/app/Models/Category.php +++ b/app/Models/Category.php @@ -24,7 +24,6 @@ declare(strict_types=1); namespace FireflyIII\Models; -use FireflyIII\Handlers\Observer\AccountObserver; use FireflyIII\Handlers\Observer\CategoryObserver; use FireflyIII\Support\Models\ReturnsIntegerIdTrait; use FireflyIII\Support\Models\ReturnsIntegerUserIdTrait; @@ -46,7 +45,7 @@ class Category extends Model protected $fillable = ['user_id', 'user_group_id', 'name']; - protected $hidden = ['encrypted']; + protected $hidden = ['encrypted']; /** * Route binder. Converts the key in the URL to the specified object (or throw 404). @@ -56,13 +55,13 @@ class Category extends Model public static function routeBinder(string $value): self { if (auth()->check()) { - $categoryId = (int) $value; + $categoryId = (int)$value; /** @var User $user */ - $user = auth()->user(); + $user = auth()->user(); /** @var null|Category $category */ - $category = $user->categories()->find($categoryId); + $category = $user->categories()->find($categoryId); if (null !== $category) { return $category; } diff --git a/app/Models/Configuration.php b/app/Models/Configuration.php index 7ed2b13f2e..49e7559236 100644 --- a/app/Models/Configuration.php +++ b/app/Models/Configuration.php @@ -23,11 +23,10 @@ declare(strict_types=1); namespace FireflyIII\Models; -use Illuminate\Database\Eloquent\Casts\Attribute; use FireflyIII\Support\Models\ReturnsIntegerIdTrait; +use Illuminate\Database\Eloquent\Casts\Attribute; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\SoftDeletes; - use function Safe\json_decode; use function Safe\json_encode; @@ -38,14 +37,6 @@ class Configuration extends Model protected $table = 'configuration'; - /** - * TODO can be replaced with native laravel code. - */ - protected function data(): Attribute - { - return Attribute::make(get: fn ($value) => json_decode((string) $value), set: fn ($value) => ['data' => json_encode($value)]); - } - protected function casts(): array { return [ @@ -54,4 +45,12 @@ class Configuration extends Model 'deleted_at' => 'datetime', ]; } + + /** + * TODO can be replaced with native laravel code. + */ + protected function data(): Attribute + { + return Attribute::make(get: fn($value) => json_decode((string)$value), set: fn($value) => ['data' => json_encode($value)]); + } } diff --git a/app/Models/CurrencyExchangeRate.php b/app/Models/CurrencyExchangeRate.php index f5117715fe..fc919dbf80 100644 --- a/app/Models/CurrencyExchangeRate.php +++ b/app/Models/CurrencyExchangeRate.php @@ -37,6 +37,7 @@ class CurrencyExchangeRate extends Model use ReturnsIntegerIdTrait; use ReturnsIntegerUserIdTrait; use SoftDeletes; + protected $fillable = ['user_id', 'from_currency_id', 'to_currency_id', 'date', 'date_tz', 'rate']; public function fromCurrency(): BelongsTo @@ -54,34 +55,6 @@ class CurrencyExchangeRate extends Model return $this->belongsTo(User::class); } - protected function fromCurrencyId(): Attribute - { - return Attribute::make( - get: static fn ($value) => (int) $value, - ); - } - - protected function rate(): Attribute - { - return Attribute::make( - get: static fn ($value) => (string) $value, - ); - } - - protected function toCurrencyId(): Attribute - { - return Attribute::make( - get: static fn ($value) => (int) $value, - ); - } - - protected function userRate(): Attribute - { - return Attribute::make( - get: static fn ($value) => (string) $value, - ); - } - protected function casts(): array { return [ @@ -96,4 +69,32 @@ class CurrencyExchangeRate extends Model 'user_rate' => 'string', ]; } + + protected function fromCurrencyId(): Attribute + { + return Attribute::make( + get: static fn($value) => (int)$value, + ); + } + + protected function rate(): Attribute + { + return Attribute::make( + get: static fn($value) => (string)$value, + ); + } + + protected function toCurrencyId(): Attribute + { + return Attribute::make( + get: static fn($value) => (int)$value, + ); + } + + protected function userRate(): Attribute + { + return Attribute::make( + get: static fn($value) => (string)$value, + ); + } } diff --git a/app/Models/GroupMembership.php b/app/Models/GroupMembership.php index 830826967e..e16178fa9b 100644 --- a/app/Models/GroupMembership.php +++ b/app/Models/GroupMembership.php @@ -53,13 +53,6 @@ class GroupMembership extends Model return $this->belongsTo(UserRole::class); } - protected function userRoleId(): Attribute - { - return Attribute::make( - get: static fn ($value) => (int) $value, - ); - } - protected function casts(): array { return [ @@ -69,4 +62,11 @@ class GroupMembership extends Model 'user_group_id' => 'integer', ]; } + + protected function userRoleId(): Attribute + { + return Attribute::make( + get: static fn($value) => (int)$value, + ); + } } diff --git a/app/Models/InvitedUser.php b/app/Models/InvitedUser.php index d543d82c47..272096faa8 100644 --- a/app/Models/InvitedUser.php +++ b/app/Models/InvitedUser.php @@ -36,6 +36,7 @@ class InvitedUser extends Model { use ReturnsIntegerIdTrait; use ReturnsIntegerUserIdTrait; + protected $fillable = ['user_group_id', 'user_id', 'email', 'invite_code', 'expires', 'expires_tz', 'redeemed']; /** @@ -44,10 +45,10 @@ class InvitedUser extends Model public static function routeBinder(string $value): self { if (auth()->check()) { - $attemptId = (int) $value; + $attemptId = (int)$value; /** @var null|InvitedUser $attempt */ - $attempt = self::find($attemptId); + $attempt = self::find($attemptId); if (null !== $attempt) { return $attempt; } diff --git a/app/Models/LinkType.php b/app/Models/LinkType.php index f7b662961c..e399ad2e9a 100644 --- a/app/Models/LinkType.php +++ b/app/Models/LinkType.php @@ -44,7 +44,7 @@ class LinkType extends Model public static function routeBinder(string $value): self { if (auth()->check()) { - $linkTypeId = (int) $value; + $linkTypeId = (int)$value; $linkType = self::find($linkTypeId); if (null !== $linkType) { return $linkType; diff --git a/app/Models/Location.php b/app/Models/Location.php index ce5744dba9..2e971b6f67 100644 --- a/app/Models/Location.php +++ b/app/Models/Location.php @@ -66,13 +66,6 @@ class Location extends Model return $this->morphMany(TransactionJournal::class, 'locatable'); } - protected function locatableId(): Attribute - { - return Attribute::make( - get: static fn ($value) => (int) $value, - ); - } - protected function casts(): array { return [ @@ -84,4 +77,11 @@ class Location extends Model 'longitude' => 'float', ]; } + + protected function locatableId(): Attribute + { + return Attribute::make( + get: static fn($value) => (int)$value, + ); + } } diff --git a/app/Models/Note.php b/app/Models/Note.php index 6c8c56653e..71d0531976 100644 --- a/app/Models/Note.php +++ b/app/Models/Note.php @@ -44,13 +44,6 @@ class Note extends Model return $this->morphTo(); } - protected function noteableId(): Attribute - { - return Attribute::make( - get: static fn ($value) => (int) $value, - ); - } - protected function casts(): array { return [ @@ -59,4 +52,11 @@ class Note extends Model 'deleted_at' => 'datetime', ]; } + + protected function noteableId(): Attribute + { + return Attribute::make( + get: static fn($value) => (int)$value, + ); + } } diff --git a/app/Models/ObjectGroup.php b/app/Models/ObjectGroup.php index d7833412fb..71a4d9b9ba 100644 --- a/app/Models/ObjectGroup.php +++ b/app/Models/ObjectGroup.php @@ -37,6 +37,7 @@ class ObjectGroup extends Model { use ReturnsIntegerIdTrait; use ReturnsIntegerUserIdTrait; + protected $fillable = ['title', 'order', 'user_id', 'user_group_id']; /** @@ -47,12 +48,11 @@ class ObjectGroup extends Model public static function routeBinder(string $value): self { if (auth()->check()) { - $objectGroupId = (int) $value; + $objectGroupId = (int)$value; /** @var null|ObjectGroup $objectGroup */ - $objectGroup = self::where('object_groups.id', $objectGroupId) - ->where('object_groups.user_id', auth()->user()->id)->first() - ; + $objectGroup = self::where('object_groups.id', $objectGroupId) + ->where('object_groups.user_id', auth()->user()->id)->first(); if (null !== $objectGroup) { return $objectGroup; } @@ -90,13 +90,6 @@ class ObjectGroup extends Model return $this->morphedByMany(PiggyBank::class, 'object_groupable'); } - protected function order(): Attribute - { - return Attribute::make( - get: static fn ($value) => (int) $value, - ); - } - protected function casts(): array { return [ @@ -107,4 +100,11 @@ class ObjectGroup extends Model 'deleted_at' => 'datetime', ]; } + + protected function order(): Attribute + { + return Attribute::make( + get: static fn($value) => (int)$value, + ); + } } diff --git a/app/Models/PiggyBank.php b/app/Models/PiggyBank.php index ae161b8c93..c493198b09 100644 --- a/app/Models/PiggyBank.php +++ b/app/Models/PiggyBank.php @@ -126,6 +126,22 @@ class PiggyBank extends Model ); } + protected function casts(): array + { + return [ + 'created_at' => 'datetime', + 'updated_at' => 'datetime', + 'deleted_at' => 'datetime', + 'start_date' => 'date', + 'target_date' => 'date', + 'order' => 'int', + 'active' => 'boolean', + 'encrypted' => 'boolean', + 'target_amount' => 'string', + 'native_target_amount' => 'string', + ]; + } + protected function order(): Attribute { return Attribute::make( @@ -142,20 +158,4 @@ class PiggyBank extends Model get: static fn($value) => (string)$value, ); } - - protected function casts(): array - { - return [ - 'created_at' => 'datetime', - 'updated_at' => 'datetime', - 'deleted_at' => 'datetime', - 'start_date' => 'date', - 'target_date' => 'date', - 'order' => 'int', - 'active' => 'boolean', - 'encrypted' => 'boolean', - 'target_amount' => 'string', - 'native_target_amount' => 'string', - ]; - } } diff --git a/app/Models/PiggyBankEvent.php b/app/Models/PiggyBankEvent.php index 9599c2f027..81578a9544 100644 --- a/app/Models/PiggyBankEvent.php +++ b/app/Models/PiggyBankEvent.php @@ -68,13 +68,6 @@ class PiggyBankEvent extends Model ); } - protected function piggyBankId(): Attribute - { - return Attribute::make( - get: static fn($value) => (int)$value, - ); - } - protected function casts(): array { return [ @@ -85,4 +78,11 @@ class PiggyBankEvent extends Model 'native_amount' => 'string', ]; } + + protected function piggyBankId(): Attribute + { + return Attribute::make( + get: static fn($value) => (int)$value, + ); + } } diff --git a/app/Models/PiggyBankRepetition.php b/app/Models/PiggyBankRepetition.php index f0f6125b53..fa6d9823ed 100644 --- a/app/Models/PiggyBankRepetition.php +++ b/app/Models/PiggyBankRepetition.php @@ -23,10 +23,10 @@ declare(strict_types=1); namespace FireflyIII\Models; -use Illuminate\Database\Eloquent\Attributes\Scope; use Carbon\Carbon; use FireflyIII\Casts\SeparateTimezoneCaster; use FireflyIII\Support\Models\ReturnsIntegerIdTrait; +use Illuminate\Database\Eloquent\Attributes\Scope; use Illuminate\Database\Eloquent\Builder as EloquentBuilder; use Illuminate\Database\Eloquent\Casts\Attribute; use Illuminate\Database\Eloquent\Model; @@ -43,12 +43,48 @@ class PiggyBankRepetition extends Model return $this->belongsTo(PiggyBank::class); } + /** + * @param mixed $value + */ + public function setCurrentAmountAttribute($value): void + { + $this->attributes['current_amount'] = (string)$value; + } + + protected function casts(): array + { + return [ + 'created_at' => 'datetime', + 'updated_at' => 'datetime', + 'start_date' => SeparateTimezoneCaster::class, + 'target_date' => SeparateTimezoneCaster::class, + 'virtual_balance' => 'string', + ]; + } + + /** + * Get the amount + */ + protected function currentAmount(): Attribute + { + return Attribute::make( + get: static fn($value) => (string)$value, + ); + } + #[Scope] protected function onDates(EloquentBuilder $query, Carbon $start, Carbon $target): EloquentBuilder { return $query->where('start_date', $start->format('Y-m-d'))->where('target_date', $target->format('Y-m-d')); } + protected function piggyBankId(): Attribute + { + return Attribute::make( + get: static fn($value) => (int)$value, + ); + } + /** * @return EloquentBuilder */ @@ -61,48 +97,11 @@ class PiggyBankRepetition extends Model $q->orWhereNull('start_date'); } ) - ->where( - static function (EloquentBuilder $q) use ($date): void { - $q->where('target_date', '>=', $date->format('Y-m-d 00:00:00')); - $q->orWhereNull('target_date'); - } - ) - ; - } - - /** - * @param mixed $value - */ - public function setCurrentAmountAttribute($value): void - { - $this->attributes['current_amount'] = (string) $value; - } - - /** - * Get the amount - */ - protected function currentAmount(): Attribute - { - return Attribute::make( - get: static fn ($value) => (string) $value, - ); - } - - protected function piggyBankId(): Attribute - { - return Attribute::make( - get: static fn ($value) => (int) $value, - ); - } - - protected function casts(): array - { - return [ - 'created_at' => 'datetime', - 'updated_at' => 'datetime', - 'start_date' => SeparateTimezoneCaster::class, - 'target_date' => SeparateTimezoneCaster::class, - 'virtual_balance' => 'string', - ]; + ->where( + static function (EloquentBuilder $q) use ($date): void { + $q->where('target_date', '>=', $date->format('Y-m-d 00:00:00')); + $q->orWhereNull('target_date'); + } + ); } } diff --git a/app/Models/Preference.php b/app/Models/Preference.php index 0e282cf1a5..f357f17cc7 100644 --- a/app/Models/Preference.php +++ b/app/Models/Preference.php @@ -46,16 +46,16 @@ class Preference extends Model { if (auth()->check()) { /** @var User $user */ - $user = auth()->user(); + $user = auth()->user(); // some preferences do not have an administration ID. // some need it, to make sure the correct one is selected. - $userGroupId = (int) $user->user_group_id; + $userGroupId = (int)$user->user_group_id; $userGroupId = 0 === $userGroupId ? null : $userGroupId; /** @var null|Preference $preference */ - $preference = null; - $items = config('firefly.admin_specific_prefs'); + $preference = null; + $items = config('firefly.admin_specific_prefs'); if (null !== $userGroupId && in_array($value, $items, true)) { // find a preference with a specific user_group_id $preference = $user->preferences()->where('user_group_id', $userGroupId)->where('name', $value)->first(); @@ -67,18 +67,18 @@ class Preference extends Model // try again with ID, but this time don't care about the preferred user_group_id if (null === $preference) { - $preference = $user->preferences()->where('id', (int) $value)->first(); + $preference = $user->preferences()->where('id', (int)$value)->first(); } if (null !== $preference) { /** @var Preference $preference */ return $preference; } - $default = config('firefly.default_preferences'); + $default = config('firefly.default_preferences'); if (array_key_exists($value, $default)) { $preference = new self(); $preference->name = $value; $preference->data = $default[$value]; - $preference->user_id = (int) $user->id; + $preference->user_id = (int)$user->id; $preference->user_group_id = in_array($value, $items, true) ? $userGroupId : null; $preference->save(); diff --git a/app/Models/Recurrence.php b/app/Models/Recurrence.php index 78051e6bc6..724ccdb06f 100644 --- a/app/Models/Recurrence.php +++ b/app/Models/Recurrence.php @@ -116,13 +116,6 @@ class Recurrence extends Model return $this->belongsTo(TransactionType::class); } - protected function transactionTypeId(): Attribute - { - return Attribute::make( - get: static fn($value) => (int)$value, - ); - } - protected function casts(): array { return [ @@ -142,4 +135,11 @@ class Recurrence extends Model 'user_group_id' => 'integer', ]; } + + protected function transactionTypeId(): Attribute + { + return Attribute::make( + get: static fn($value) => (int)$value, + ); + } } diff --git a/app/Models/RecurrenceMeta.php b/app/Models/RecurrenceMeta.php index 7430c942f6..fddd2e8292 100644 --- a/app/Models/RecurrenceMeta.php +++ b/app/Models/RecurrenceMeta.php @@ -37,20 +37,13 @@ class RecurrenceMeta extends Model protected $fillable = ['recurrence_id', 'name', 'value']; - protected $table = 'recurrences_meta'; + protected $table = 'recurrences_meta'; public function recurrence(): BelongsTo { return $this->belongsTo(Recurrence::class); } - protected function recurrenceId(): Attribute - { - return Attribute::make( - get: static fn ($value) => (int) $value, - ); - } - protected function casts(): array { return [ @@ -61,4 +54,11 @@ class RecurrenceMeta extends Model 'value' => 'string', ]; } + + protected function recurrenceId(): Attribute + { + return Attribute::make( + get: static fn($value) => (int)$value, + ); + } } diff --git a/app/Models/RecurrenceRepetition.php b/app/Models/RecurrenceRepetition.php index 93ad02a196..920f8d93a1 100644 --- a/app/Models/RecurrenceRepetition.php +++ b/app/Models/RecurrenceRepetition.php @@ -36,20 +36,24 @@ class RecurrenceRepetition extends Model use ReturnsIntegerIdTrait; use SoftDeletes; - #[Deprecated] /** @deprecated */ - public const int WEEKEND_DO_NOTHING = 1; + #[Deprecated] + /** @deprecated */ + public const int WEEKEND_DO_NOTHING = 1; - #[Deprecated] /** @deprecated */ + #[Deprecated] + /** @deprecated */ public const int WEEKEND_SKIP_CREATION = 2; - #[Deprecated] /** @deprecated */ - public const int WEEKEND_TO_FRIDAY = 3; + #[Deprecated] + /** @deprecated */ + public const int WEEKEND_TO_FRIDAY = 3; - #[Deprecated] /** @deprecated */ - public const int WEEKEND_TO_MONDAY = 4; + #[Deprecated] + /** @deprecated */ + public const int WEEKEND_TO_MONDAY = 4; protected $casts - = [ + = [ 'created_at' => 'datetime', 'updated_at' => 'datetime', 'deleted_at' => 'datetime', @@ -59,9 +63,9 @@ class RecurrenceRepetition extends Model 'weekend' => 'int', ]; - protected $fillable = ['recurrence_id', 'weekend', 'repetition_type', 'repetition_moment', 'repetition_skip']; + protected $fillable = ['recurrence_id', 'weekend', 'repetition_type', 'repetition_moment', 'repetition_skip']; - protected $table = 'recurrences_repetitions'; + protected $table = 'recurrences_repetitions'; public function recurrence(): BelongsTo { @@ -78,21 +82,21 @@ class RecurrenceRepetition extends Model protected function recurrenceId(): Attribute { return Attribute::make( - get: static fn ($value) => (int) $value, + get: static fn($value) => (int)$value, ); } protected function repetitionSkip(): Attribute { return Attribute::make( - get: static fn ($value) => (int) $value, + get: static fn($value) => (int)$value, ); } protected function weekend(): Attribute { return Attribute::make( - get: static fn ($value) => (int) $value, + get: static fn($value) => (int)$value, ); } } diff --git a/app/Models/RecurrenceTransaction.php b/app/Models/RecurrenceTransaction.php index 245e19865f..4c1a5f6f2c 100644 --- a/app/Models/RecurrenceTransaction.php +++ b/app/Models/RecurrenceTransaction.php @@ -95,6 +95,18 @@ class RecurrenceTransaction extends Model ); } + protected function casts(): array + { + return [ + 'created_at' => 'datetime', + 'updated_at' => 'datetime', + 'deleted_at' => 'datetime', + 'amount' => 'string', + 'foreign_amount' => 'string', + 'description' => 'string', + ]; + } + protected function destinationId(): Attribute { return Attribute::make( @@ -136,16 +148,4 @@ class RecurrenceTransaction extends Model get: static fn($value) => (int)$value, ); } - - protected function casts(): array - { - return [ - 'created_at' => 'datetime', - 'updated_at' => 'datetime', - 'deleted_at' => 'datetime', - 'amount' => 'string', - 'foreign_amount' => 'string', - 'description' => 'string', - ]; - } } diff --git a/app/Models/RecurrenceTransactionMeta.php b/app/Models/RecurrenceTransactionMeta.php index a334b9c433..5f2644b0f6 100644 --- a/app/Models/RecurrenceTransactionMeta.php +++ b/app/Models/RecurrenceTransactionMeta.php @@ -37,20 +37,13 @@ class RecurrenceTransactionMeta extends Model protected $fillable = ['rt_id', 'name', 'value']; - protected $table = 'rt_meta'; + protected $table = 'rt_meta'; public function recurrenceTransaction(): BelongsTo { return $this->belongsTo(RecurrenceTransaction::class, 'rt_id'); } - protected function rtId(): Attribute - { - return Attribute::make( - get: static fn ($value) => (int) $value, - ); - } - protected function casts(): array { return [ @@ -61,4 +54,11 @@ class RecurrenceTransactionMeta extends Model 'value' => 'string', ]; } + + protected function rtId(): Attribute + { + return Attribute::make( + get: static fn($value) => (int)$value, + ); + } } diff --git a/app/Models/Rule.php b/app/Models/Rule.php index 78f2906ccb..a8de1ea45b 100644 --- a/app/Models/Rule.php +++ b/app/Models/Rule.php @@ -87,30 +87,11 @@ class Rule extends Model return $this->hasMany(RuleTrigger::class); } - protected function description(): Attribute - { - return Attribute::make(set: fn($value) => ['description' => e($value)]); - } - public function userGroup(): BelongsTo { return $this->belongsTo(UserGroup::class); } - protected function order(): Attribute - { - return Attribute::make( - get: static fn($value) => (int)$value, - ); - } - - protected function ruleGroupId(): Attribute - { - return Attribute::make( - get: static fn($value) => (int)$value, - ); - } - protected function casts(): array { return [ @@ -126,4 +107,23 @@ class Rule extends Model 'user_group_id' => 'integer', ]; } + + protected function description(): Attribute + { + return Attribute::make(set: fn($value) => ['description' => e($value)]); + } + + protected function order(): Attribute + { + return Attribute::make( + get: static fn($value) => (int)$value, + ); + } + + protected function ruleGroupId(): Attribute + { + return Attribute::make( + get: static fn($value) => (int)$value, + ); + } } diff --git a/app/Models/RuleAction.php b/app/Models/RuleAction.php index 60ebc7254f..3603c6cc65 100644 --- a/app/Models/RuleAction.php +++ b/app/Models/RuleAction.php @@ -42,7 +42,7 @@ class RuleAction extends Model if (false === config('firefly.feature_flags.expression_engine')) { Log::debug('Expression engine is disabled, returning action value as string.'); - return (string) $this->action_value; + return (string)$this->action_value; } if (true === config('firefly.feature_flags.expression_engine') && str_starts_with($this->action_value, '\=')) { // return literal string. @@ -54,7 +54,7 @@ class RuleAction extends Model $result = $expr->evaluate($journal); } catch (SyntaxError $e) { Log::error(sprintf('Expression engine failed to evaluate expression "%s" with error "%s".', $this->action_value, $e->getMessage())); - $result = (string) $this->action_value; + $result = (string)$this->action_value; } Log::debug(sprintf('Expression engine is enabled, result of expression "%s" is "%s".', $this->action_value, $result)); @@ -66,20 +66,6 @@ class RuleAction extends Model return $this->belongsTo(Rule::class); } - protected function order(): Attribute - { - return Attribute::make( - get: static fn ($value) => (int) $value, - ); - } - - protected function ruleId(): Attribute - { - return Attribute::make( - get: static fn ($value) => (int) $value, - ); - } - protected function casts(): array { return [ @@ -90,4 +76,18 @@ class RuleAction extends Model 'stop_processing' => 'boolean', ]; } + + protected function order(): Attribute + { + return Attribute::make( + get: static fn($value) => (int)$value, + ); + } + + protected function ruleId(): Attribute + { + return Attribute::make( + get: static fn($value) => (int)$value, + ); + } } diff --git a/app/Models/RuleGroup.php b/app/Models/RuleGroup.php index 48b96e1a67..d53155d888 100644 --- a/app/Models/RuleGroup.php +++ b/app/Models/RuleGroup.php @@ -23,7 +23,6 @@ declare(strict_types=1); namespace FireflyIII\Models; -use FireflyIII\Handlers\Observer\AccountObserver; use FireflyIII\Handlers\Observer\RuleGroupObserver; use FireflyIII\Support\Models\ReturnsIntegerIdTrait; use FireflyIII\Support\Models\ReturnsIntegerUserIdTrait; @@ -53,13 +52,13 @@ class RuleGroup extends Model public static function routeBinder(string $value): self { if (auth()->check()) { - $ruleGroupId = (int) $value; + $ruleGroupId = (int)$value; /** @var User $user */ - $user = auth()->user(); + $user = auth()->user(); /** @var null|RuleGroup $ruleGroup */ - $ruleGroup = $user->ruleGroups()->find($ruleGroupId); + $ruleGroup = $user->ruleGroups()->find($ruleGroupId); if (null !== $ruleGroup) { return $ruleGroup; } @@ -78,13 +77,6 @@ class RuleGroup extends Model return $this->hasMany(Rule::class); } - protected function order(): Attribute - { - return Attribute::make( - get: static fn ($value) => (int) $value, - ); - } - protected function casts(): array { return [ @@ -98,4 +90,11 @@ class RuleGroup extends Model 'user_group_id' => 'integer', ]; } + + protected function order(): Attribute + { + return Attribute::make( + get: static fn($value) => (int)$value, + ); + } } diff --git a/app/Models/RuleTrigger.php b/app/Models/RuleTrigger.php index 5c9f4fca2e..9fedd62399 100644 --- a/app/Models/RuleTrigger.php +++ b/app/Models/RuleTrigger.php @@ -39,20 +39,6 @@ class RuleTrigger extends Model return $this->belongsTo(Rule::class); } - protected function order(): Attribute - { - return Attribute::make( - get: static fn ($value) => (int) $value, - ); - } - - protected function ruleId(): Attribute - { - return Attribute::make( - get: static fn ($value) => (int) $value, - ); - } - protected function casts(): array { return [ @@ -63,4 +49,18 @@ class RuleTrigger extends Model 'stop_processing' => 'boolean', ]; } + + protected function order(): Attribute + { + return Attribute::make( + get: static fn($value) => (int)$value, + ); + } + + protected function ruleId(): Attribute + { + return Attribute::make( + get: static fn($value) => (int)$value, + ); + } } diff --git a/app/Models/Tag.php b/app/Models/Tag.php index df1082bc09..22572b8a06 100644 --- a/app/Models/Tag.php +++ b/app/Models/Tag.php @@ -24,7 +24,6 @@ declare(strict_types=1); namespace FireflyIII\Models; use FireflyIII\Casts\SeparateTimezoneCaster; -use FireflyIII\Handlers\Observer\AccountObserver; use FireflyIII\Handlers\Observer\TagObserver; use FireflyIII\Support\Models\ReturnsIntegerIdTrait; use FireflyIII\Support\Models\ReturnsIntegerUserIdTrait; @@ -46,7 +45,7 @@ class Tag extends Model protected $fillable = ['user_id', 'user_group_id', 'tag', 'date', 'date_tz', 'description', 'tag_mode']; - protected $hidden = ['zoomLevel', 'zoom_level', 'latitude', 'longitude']; + protected $hidden = ['zoomLevel', 'zoom_level', 'latitude', 'longitude']; /** * Route binder. Converts the key in the URL to the specified object (or throw 404). @@ -56,13 +55,13 @@ class Tag extends Model public static function routeBinder(string $value): self { if (auth()->check()) { - $tagId = (int) $value; + $tagId = (int)$value; /** @var User $user */ - $user = auth()->user(); + $user = auth()->user(); /** @var null|Tag $tag */ - $tag = $user->tags()->find($tagId); + $tag = $user->tags()->find($tagId); if (null !== $tag) { return $tag; } diff --git a/app/Models/Transaction.php b/app/Models/Transaction.php index 342a2f6b0d..e0b521d30f 100644 --- a/app/Models/Transaction.php +++ b/app/Models/Transaction.php @@ -23,12 +23,11 @@ declare(strict_types=1); namespace FireflyIII\Models; -use FireflyIII\Handlers\Observer\AccountObserver; +use Carbon\Carbon; use FireflyIII\Handlers\Observer\TransactionObserver; +use FireflyIII\Support\Models\ReturnsIntegerIdTrait; use Illuminate\Database\Eloquent\Attributes\ObservedBy; use Illuminate\Database\Eloquent\Attributes\Scope; -use Carbon\Carbon; -use FireflyIII\Support\Models\ReturnsIntegerIdTrait; use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Casts\Attribute; use Illuminate\Database\Eloquent\Factories\HasFactory; @@ -45,7 +44,7 @@ class Transaction extends Model use SoftDeletes; protected $fillable - = [ + = [ 'account_id', 'transaction_journal_id', 'description', @@ -93,6 +92,31 @@ class Transaction extends Model return $this->belongsTo(TransactionCurrency::class, 'foreign_currency_id'); } + /** + * @param mixed $value + */ + public function setAmountAttribute($value): void + { + $this->attributes['amount'] = (string)$value; + } + + public function transactionCurrency(): BelongsTo + { + return $this->belongsTo(TransactionCurrency::class); + } + + public function transactionJournal(): BelongsTo + { + return $this->belongsTo(TransactionJournal::class); + } + + protected function accountId(): Attribute + { + return Attribute::make( + get: static fn($value) => (int)$value, + ); + } + /** * Check for transactions AFTER a specified date. */ @@ -121,6 +145,23 @@ class Transaction extends Model return false; } + /** + * Get the amount + */ + protected function amount(): Attribute + { + return Attribute::make( + get: static fn($value) => (string)$value, + ); + } + + protected function balanceDirty(): Attribute + { + return Attribute::make( + get: static fn($value) => 1 === (int)$value, + ); + } + /** * Check for transactions BEFORE the specified date. */ @@ -133,78 +174,6 @@ class Transaction extends Model $query->where('transaction_journals.date', '<=', $date->format('Y-m-d 23:59:59')); } - #[Scope] - protected function transactionTypes(Builder $query, array $types): void - { - if (!self::isJoined($query, 'transaction_journals')) { - $query->leftJoin('transaction_journals', 'transaction_journals.id', '=', 'transactions.transaction_journal_id'); - } - - if (!self::isJoined($query, 'transaction_types')) { - $query->leftJoin('transaction_types', 'transaction_types.id', '=', 'transaction_journals.transaction_type_id'); - } - $query->whereIn('transaction_types.type', $types); - } - - /** - * @param mixed $value - */ - public function setAmountAttribute($value): void - { - $this->attributes['amount'] = (string) $value; - } - - public function transactionCurrency(): BelongsTo - { - return $this->belongsTo(TransactionCurrency::class); - } - - public function transactionJournal(): BelongsTo - { - return $this->belongsTo(TransactionJournal::class); - } - - protected function accountId(): Attribute - { - return Attribute::make( - get: static fn ($value) => (int) $value, - ); - } - - /** - * Get the amount - */ - protected function amount(): Attribute - { - return Attribute::make( - get: static fn ($value) => (string) $value, - ); - } - - protected function balanceDirty(): Attribute - { - return Attribute::make( - get: static fn ($value) => 1 === (int) $value, - ); - } - - /** - * Get the foreign amount - */ - protected function foreignAmount(): Attribute - { - return Attribute::make( - get: static fn ($value) => (string) $value, - ); - } - - protected function transactionJournalId(): Attribute - { - return Attribute::make( - get: static fn ($value) => (int) $value, - ); - } - protected function casts(): array { return [ @@ -225,4 +194,34 @@ class Transaction extends Model 'native_foreign_amount' => 'string', ]; } + + /** + * Get the foreign amount + */ + protected function foreignAmount(): Attribute + { + return Attribute::make( + get: static fn($value) => (string)$value, + ); + } + + protected function transactionJournalId(): Attribute + { + return Attribute::make( + get: static fn($value) => (int)$value, + ); + } + + #[Scope] + protected function transactionTypes(Builder $query, array $types): void + { + if (!self::isJoined($query, 'transaction_journals')) { + $query->leftJoin('transaction_journals', 'transaction_journals.id', '=', 'transactions.transaction_journal_id'); + } + + if (!self::isJoined($query, 'transaction_types')) { + $query->leftJoin('transaction_types', 'transaction_types.id', '=', 'transaction_journals.transaction_type_id'); + } + $query->whereIn('transaction_types.type', $types); + } } diff --git a/app/Models/TransactionCurrency.php b/app/Models/TransactionCurrency.php index 83dfd6bfe0..7ecfbd18db 100644 --- a/app/Models/TransactionCurrency.php +++ b/app/Models/TransactionCurrency.php @@ -40,7 +40,7 @@ class TransactionCurrency extends Model public ?bool $userGroupEnabled = null; public ?bool $userGroupNative = null; - protected $fillable = ['name', 'code', 'symbol', 'decimal_places', 'enabled']; + protected $fillable = ['name', 'code', 'symbol', 'decimal_places', 'enabled']; /** * Route binder. Converts the key in the URL to the specified object (or throw 404). @@ -50,7 +50,7 @@ class TransactionCurrency extends Model public static function routeBinder(string $value): self { if (auth()->check()) { - $currencyId = (int) $value; + $currencyId = (int)$value; $currency = self::find($currencyId); if (null !== $currency) { $currency->refreshForUser(auth()->user()); @@ -101,13 +101,6 @@ class TransactionCurrency extends Model return $this->belongsToMany(User::class)->withTimestamps()->withPivot('user_default'); } - protected function decimalPlaces(): Attribute - { - return Attribute::make( - get: static fn ($value) => (int) $value, - ); - } - protected function casts(): array { return [ @@ -118,4 +111,11 @@ class TransactionCurrency extends Model 'enabled' => 'bool', ]; } + + protected function decimalPlaces(): Attribute + { + return Attribute::make( + get: static fn($value) => (int)$value, + ); + } } diff --git a/app/Models/TransactionGroup.php b/app/Models/TransactionGroup.php index a13c0008ad..ec8bf0faae 100644 --- a/app/Models/TransactionGroup.php +++ b/app/Models/TransactionGroup.php @@ -23,7 +23,6 @@ declare(strict_types=1); namespace FireflyIII\Models; -use FireflyIII\Handlers\Observer\AccountObserver; use FireflyIII\Handlers\Observer\TransactionGroupObserver; use FireflyIII\Support\Models\ReturnsIntegerIdTrait; use FireflyIII\Support\Models\ReturnsIntegerUserIdTrait; @@ -53,17 +52,16 @@ class TransactionGroup extends Model { app('log')->debug(sprintf('Now in %s("%s")', __METHOD__, $value)); if (auth()->check()) { - $groupId = (int) $value; + $groupId = (int)$value; /** @var User $user */ - $user = auth()->user(); + $user = auth()->user(); app('log')->debug(sprintf('User authenticated as %s', $user->email)); /** @var null|TransactionGroup $group */ - $group = $user->transactionGroups() - ->with(['transactionJournals', 'transactionJournals.transactions']) - ->where('transaction_groups.id', $groupId)->first(['transaction_groups.*']) - ; + $group = $user->transactionGroups() + ->with(['transactionJournals', 'transactionJournals.transactions']) + ->where('transaction_groups.id', $groupId)->first(['transaction_groups.*']); if (null !== $group) { app('log')->debug(sprintf('Found group #%d.', $group->id)); diff --git a/app/Models/TransactionJournal.php b/app/Models/TransactionJournal.php index 72f2ba70f2..dde97c2fce 100644 --- a/app/Models/TransactionJournal.php +++ b/app/Models/TransactionJournal.php @@ -23,16 +23,15 @@ declare(strict_types=1); namespace FireflyIII\Models; -use FireflyIII\Handlers\Observer\AccountObserver; -use FireflyIII\Handlers\Observer\TransactionJournalObserver; -use Illuminate\Database\Eloquent\Attributes\ObservedBy; -use Illuminate\Database\Eloquent\Attributes\Scope; use Carbon\Carbon; use FireflyIII\Casts\SeparateTimezoneCaster; use FireflyIII\Enums\TransactionTypeEnum; +use FireflyIII\Handlers\Observer\TransactionJournalObserver; use FireflyIII\Support\Models\ReturnsIntegerIdTrait; use FireflyIII\Support\Models\ReturnsIntegerUserIdTrait; use FireflyIII\User; +use Illuminate\Database\Eloquent\Attributes\ObservedBy; +use Illuminate\Database\Eloquent\Attributes\Scope; use Illuminate\Database\Eloquent\Builder as EloquentBuilder; use Illuminate\Database\Eloquent\Casts\Attribute; use Illuminate\Database\Eloquent\Factories\HasFactory; @@ -49,7 +48,6 @@ use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; * @method EloquentBuilder|static after() * @method static EloquentBuilder|static query() */ - #[ObservedBy([TransactionJournalObserver::class])] class TransactionJournal extends Model { @@ -59,7 +57,7 @@ class TransactionJournal extends Model use SoftDeletes; protected $fillable - = [ + = [ 'user_id', 'user_group_id', 'transaction_type_id', @@ -83,13 +81,13 @@ class TransactionJournal extends Model public static function routeBinder(string $value): self { if (auth()->check()) { - $journalId = (int) $value; + $journalId = (int)$value; /** @var User $user */ - $user = auth()->user(); + $user = auth()->user(); /** @var null|TransactionJournal $journal */ - $journal = $user->transactionJournals()->where('transaction_journals.id', $journalId)->first(['transaction_journals.*']); + $journal = $user->transactionJournals()->where('transaction_journals.id', $journalId)->first(['transaction_journals.*']); if (null !== $journal) { return $journal; } @@ -170,32 +168,6 @@ class TransactionJournal extends Model return $query->where('transaction_journals.date', '<=', $date->format('Y-m-d H:i:s')); } - #[Scope] - protected function transactionTypes(EloquentBuilder $query, array $types): void - { - if (!self::isJoined($query, 'transaction_types')) { - $query->leftJoin('transaction_types', 'transaction_types.id', '=', 'transaction_journals.transaction_type_id'); - } - if (0 !== count($types)) { - $query->whereIn('transaction_types.type', $types); - } - } - - /** - * Checks if tables are joined. - */ - public static function isJoined(EloquentBuilder $query, string $table): bool - { - $joins = $query->getQuery()->joins; - foreach ($joins as $join) { - if ($join->table === $table) { - return true; - } - } - - return false; - } - public function sourceJournalLinks(): HasMany { return $this->hasMany(TransactionJournalLink::class, 'source_id'); @@ -236,20 +208,6 @@ class TransactionJournal extends Model return $this->belongsTo(UserGroup::class); } - protected function order(): Attribute - { - return Attribute::make( - get: static fn ($value) => (int) $value, - ); - } - - protected function transactionTypeId(): Attribute - { - return Attribute::make( - get: static fn ($value) => (int) $value, - ); - } - protected function casts(): array { return [ @@ -268,4 +226,44 @@ class TransactionJournal extends Model 'user_group_id' => 'integer', ]; } + + protected function order(): Attribute + { + return Attribute::make( + get: static fn($value) => (int)$value, + ); + } + + protected function transactionTypeId(): Attribute + { + return Attribute::make( + get: static fn($value) => (int)$value, + ); + } + + #[Scope] + protected function transactionTypes(EloquentBuilder $query, array $types): void + { + if (!self::isJoined($query, 'transaction_types')) { + $query->leftJoin('transaction_types', 'transaction_types.id', '=', 'transaction_journals.transaction_type_id'); + } + if (0 !== count($types)) { + $query->whereIn('transaction_types.type', $types); + } + } + + /** + * Checks if tables are joined. + */ + public static function isJoined(EloquentBuilder $query, string $table): bool + { + $joins = $query->getQuery()->joins; + foreach ($joins as $join) { + if ($join->table === $table) { + return true; + } + } + + return false; + } } diff --git a/app/Models/TransactionJournalLink.php b/app/Models/TransactionJournalLink.php index 92adb6a7e2..4880d15c39 100644 --- a/app/Models/TransactionJournalLink.php +++ b/app/Models/TransactionJournalLink.php @@ -44,14 +44,13 @@ class TransactionJournalLink extends Model public static function routeBinder(string $value): self { if (auth()->check()) { - $linkId = (int) $value; + $linkId = (int)$value; $link = self::where('journal_links.id', $linkId) - ->leftJoin('transaction_journals as t_a', 't_a.id', '=', 'source_id') - ->leftJoin('transaction_journals as t_b', 't_b.id', '=', 'destination_id') - ->where('t_a.user_id', auth()->user()->id) - ->where('t_b.user_id', auth()->user()->id) - ->first(['journal_links.*']) - ; + ->leftJoin('transaction_journals as t_a', 't_a.id', '=', 'source_id') + ->leftJoin('transaction_journals as t_b', 't_b.id', '=', 'destination_id') + ->where('t_a.user_id', auth()->user()->id) + ->where('t_b.user_id', auth()->user()->id) + ->first(['journal_links.*']); if (null !== $link) { return $link; } @@ -83,27 +82,6 @@ class TransactionJournalLink extends Model return $this->belongsTo(TransactionJournal::class, 'source_id'); } - protected function destinationId(): Attribute - { - return Attribute::make( - get: static fn ($value) => (int) $value, - ); - } - - protected function linkTypeId(): Attribute - { - return Attribute::make( - get: static fn ($value) => (int) $value, - ); - } - - protected function sourceId(): Attribute - { - return Attribute::make( - get: static fn ($value) => (int) $value, - ); - } - protected function casts(): array { return [ @@ -111,4 +89,25 @@ class TransactionJournalLink extends Model 'updated_at' => 'datetime', ]; } + + protected function destinationId(): Attribute + { + return Attribute::make( + get: static fn($value) => (int)$value, + ); + } + + protected function linkTypeId(): Attribute + { + return Attribute::make( + get: static fn($value) => (int)$value, + ); + } + + protected function sourceId(): Attribute + { + return Attribute::make( + get: static fn($value) => (int)$value, + ); + } } diff --git a/app/Models/TransactionJournalMeta.php b/app/Models/TransactionJournalMeta.php index 80b7e42ca0..7dd67b6f86 100644 --- a/app/Models/TransactionJournalMeta.php +++ b/app/Models/TransactionJournalMeta.php @@ -28,7 +28,6 @@ use Illuminate\Database\Eloquent\Casts\Attribute; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\SoftDeletes; - use function Safe\json_decode; use function Safe\json_encode; @@ -39,29 +38,13 @@ class TransactionJournalMeta extends Model protected $fillable = ['transaction_journal_id', 'name', 'data', 'hash']; - protected $table = 'journal_meta'; - - protected function data(): Attribute - { - return Attribute::make(get: fn ($value) => json_decode((string) $value, false), set: function ($value) { - $data = json_encode($value); - - return ['data' => $data, 'hash' => hash('sha256', $data)]; - }); - } + protected $table = 'journal_meta'; public function transactionJournal(): BelongsTo { return $this->belongsTo(TransactionJournal::class); } - protected function transactionJournalId(): Attribute - { - return Attribute::make( - get: static fn ($value) => (int) $value, - ); - } - protected function casts(): array { return [ @@ -70,4 +53,20 @@ class TransactionJournalMeta extends Model 'deleted_at' => 'datetime', ]; } + + protected function data(): Attribute + { + return Attribute::make(get: fn($value) => json_decode((string)$value, false), set: function ($value) { + $data = json_encode($value); + + return ['data' => $data, 'hash' => hash('sha256', $data)]; + }); + } + + protected function transactionJournalId(): Attribute + { + return Attribute::make( + get: static fn($value) => (int)$value, + ); + } } diff --git a/app/Models/TransactionType.php b/app/Models/TransactionType.php index 0b04ead8b5..4062911fae 100644 --- a/app/Models/TransactionType.php +++ b/app/Models/TransactionType.php @@ -36,34 +36,41 @@ class TransactionType extends Model use ReturnsIntegerIdTrait; use SoftDeletes; - #[Deprecated] /** @deprecated */ - public const string DEPOSIT = 'Deposit'; + #[Deprecated] + /** @deprecated */ + public const string DEPOSIT = 'Deposit'; - #[Deprecated] /** @deprecated */ - public const string INVALID = 'Invalid'; + #[Deprecated] + /** @deprecated */ + public const string INVALID = 'Invalid'; - #[Deprecated] /** @deprecated */ + #[Deprecated] + /** @deprecated */ public const string LIABILITY_CREDIT = 'Liability credit'; - #[Deprecated] /** @deprecated */ - public const string OPENING_BALANCE = 'Opening balance'; + #[Deprecated] + /** @deprecated */ + public const string OPENING_BALANCE = 'Opening balance'; - #[Deprecated] /** @deprecated */ - public const string RECONCILIATION = 'Reconciliation'; + #[Deprecated] + /** @deprecated */ + public const string RECONCILIATION = 'Reconciliation'; - #[Deprecated] /** @deprecated */ - public const string TRANSFER = 'Transfer'; + #[Deprecated] + /** @deprecated */ + public const string TRANSFER = 'Transfer'; - #[Deprecated] /** @deprecated */ - public const string WITHDRAWAL = 'Withdrawal'; + #[Deprecated] + /** @deprecated */ + public const string WITHDRAWAL = 'Withdrawal'; protected $casts - = [ + = [ 'created_at' => 'datetime', 'updated_at' => 'datetime', 'deleted_at' => 'datetime', ]; - protected $fillable = ['type']; + protected $fillable = ['type']; /** * Route binder. Converts the key in the URL to the specified object (or throw 404). diff --git a/app/Models/UserGroup.php b/app/Models/UserGroup.php index fbae2d70f6..7b20a2980e 100644 --- a/app/Models/UserGroup.php +++ b/app/Models/UserGroup.php @@ -47,19 +47,19 @@ class UserGroup extends Model public static function routeBinder(string $value): self { if (auth()->check()) { - $userGroupId = (int) $value; + $userGroupId = (int)$value; /** @var User $user */ - $user = auth()->user(); + $user = auth()->user(); /** @var null|UserGroup $userGroup */ - $userGroup = self::find($userGroupId); + $userGroup = self::find($userGroupId); if (null === $userGroup) { throw new NotFoundHttpException(); } // need at least ready only to be aware of the user group's existence, // but owner/full role (in the group) or global owner role may overrule this. - $access = $user->hasRoleInGroupOrOwner($userGroup, UserRoleEnum::READ_ONLY) || $user->hasRole('owner'); + $access = $user->hasRoleInGroupOrOwner($userGroup, UserRoleEnum::READ_ONLY) || $user->hasRole('owner'); if ($access) { return $userGroup; } diff --git a/app/Models/Webhook.php b/app/Models/Webhook.php index 797ca82876..5b19fe269c 100644 --- a/app/Models/Webhook.php +++ b/app/Models/Webhook.php @@ -27,7 +27,6 @@ namespace FireflyIII\Models; use FireflyIII\Enums\WebhookDelivery as WebhookDeliveryEnum; use FireflyIII\Enums\WebhookResponse as WebhookResponseEnum; use FireflyIII\Enums\WebhookTrigger as WebhookTriggerEnum; -use FireflyIII\Handlers\Observer\AccountObserver; use FireflyIII\Handlers\Observer\WebhookObserver; use FireflyIII\Support\Models\ReturnsIntegerIdTrait; use FireflyIII\Support\Models\ReturnsIntegerUserIdTrait; @@ -138,10 +137,10 @@ class Webhook extends Model $webhookId = (int)$value; /** @var User $user */ - $user = auth()->user(); + $user = auth()->user(); /** @var null|Webhook $webhook */ - $webhook = $user->webhooks()->find($webhookId); + $webhook = $user->webhooks()->find($webhookId); if (null !== $webhook) { return $webhook; } @@ -155,16 +154,16 @@ class Webhook extends Model return $this->belongsTo(User::class); } - public function webhookMessages(): HasMany - { - return $this->hasMany(WebhookMessage::class); - } - public function webhookDeliveries(): BelongsToMany { return $this->belongsToMany(WebhookDelivery::class); } + public function webhookMessages(): HasMany + { + return $this->hasMany(WebhookMessage::class); + } + public function webhookResponses(): BelongsToMany { return $this->belongsToMany(WebhookResponse::class); diff --git a/app/Models/WebhookAttempt.php b/app/Models/WebhookAttempt.php index fa283f9ca2..c38fd15fe1 100644 --- a/app/Models/WebhookAttempt.php +++ b/app/Models/WebhookAttempt.php @@ -45,13 +45,13 @@ class WebhookAttempt extends Model public static function routeBinder(string $value): self { if (auth()->check()) { - $attemptId = (int) $value; + $attemptId = (int)$value; /** @var User $user */ - $user = auth()->user(); + $user = auth()->user(); /** @var null|WebhookAttempt $attempt */ - $attempt = self::find($attemptId); + $attempt = self::find($attemptId); if (null !== $attempt && $attempt->webhookMessage->webhook->user_id === $user->id) { return $attempt; } @@ -68,7 +68,7 @@ class WebhookAttempt extends Model protected function webhookMessageId(): Attribute { return Attribute::make( - get: static fn ($value) => (int) $value, + get: static fn($value) => (int)$value, ); } } diff --git a/app/Models/WebhookDelivery.php b/app/Models/WebhookDelivery.php index a43a47d417..614a5cb3c2 100644 --- a/app/Models/WebhookDelivery.php +++ b/app/Models/WebhookDelivery.php @@ -41,7 +41,7 @@ class WebhookDelivery extends Model protected function key(): Attribute { return Attribute::make( - get: static fn ($value) => (int) $value, + get: static fn($value) => (int)$value, ); } } diff --git a/app/Models/WebhookMessage.php b/app/Models/WebhookMessage.php index c1014d60aa..cae9a4a73f 100644 --- a/app/Models/WebhookMessage.php +++ b/app/Models/WebhookMessage.php @@ -72,6 +72,17 @@ class WebhookMessage extends Model return $this->hasMany(WebhookAttempt::class); } + protected function casts(): array + { + return [ + 'sent' => 'boolean', + 'errored' => 'boolean', + 'uuid' => 'string', + 'message' => 'json', + 'logs' => 'json', + ]; + } + /** * Get the amount */ @@ -88,15 +99,4 @@ class WebhookMessage extends Model get: static fn($value) => (int)$value, ); } - - protected function casts(): array - { - return [ - 'sent' => 'boolean', - 'errored' => 'boolean', - 'uuid' => 'string', - 'message' => 'json', - 'logs' => 'json', - ]; - } } diff --git a/app/Models/WebhookResponse.php b/app/Models/WebhookResponse.php index c970e8b70e..5c8cb45311 100644 --- a/app/Models/WebhookResponse.php +++ b/app/Models/WebhookResponse.php @@ -41,7 +41,7 @@ class WebhookResponse extends Model protected function key(): Attribute { return Attribute::make( - get: static fn ($value) => (int) $value, + get: static fn($value) => (int)$value, ); } } diff --git a/app/Models/WebhookTrigger.php b/app/Models/WebhookTrigger.php index 4bd8cf444d..7d9a6ea1fa 100644 --- a/app/Models/WebhookTrigger.php +++ b/app/Models/WebhookTrigger.php @@ -41,7 +41,7 @@ class WebhookTrigger extends Model protected function key(): Attribute { return Attribute::make( - get: static fn ($value) => (int) $value, + get: static fn($value) => (int)$value, ); } } From 768bd892c8a9a533bcb0327da77ada62fb1423fa Mon Sep 17 00:00:00 2001 From: James Cole Date: Sun, 14 Sep 2025 09:14:41 +0200 Subject: [PATCH 08/58] Allow sending of webhooks from budget limit store. --- .../Models/BudgetLimit/StoreController.php | 23 ++++---- .../Models/BudgetLimit/StoreRequest.php | 58 +++++++++++++++++++ app/Handlers/Observer/BudgetLimitObserver.php | 26 ++++++--- .../Budget/BudgetLimitRepository.php | 24 +++++--- resources/lang/en_US/validation.php | 1 + 5 files changed, 103 insertions(+), 29 deletions(-) diff --git a/app/Api/V1/Controllers/Models/BudgetLimit/StoreController.php b/app/Api/V1/Controllers/Models/BudgetLimit/StoreController.php index c3b88d69f2..910502662c 100644 --- a/app/Api/V1/Controllers/Models/BudgetLimit/StoreController.php +++ b/app/Api/V1/Controllers/Models/BudgetLimit/StoreController.php @@ -67,26 +67,27 @@ class StoreController extends Controller */ public function store(StoreRequest $request, Budget $budget): JsonResponse { - $data = $request->getAll(); - $data['start_date'] = $data['start']; - $data['end_date'] = $data['end']; - $data['budget_id'] = $budget->id; + $data = $request->getAll(); + $data['start_date'] = $data['start']; + $data['end_date'] = $data['end']; + $data['fire_webhooks'] = $data['fire_webhooks'] ?? true; + $data['budget_id'] = $budget->id; - $budgetLimit = $this->blRepository->store($data); - $manager = $this->getManager(); + $budgetLimit = $this->blRepository->store($data); + $manager = $this->getManager(); // enrich /** @var User $admin */ - $admin = auth()->user(); - $enrichment = new BudgetLimitEnrichment(); + $admin = auth()->user(); + $enrichment = new BudgetLimitEnrichment(); $enrichment->setUser($admin); - $budgetLimit = $enrichment->enrichSingle($budgetLimit); + $budgetLimit = $enrichment->enrichSingle($budgetLimit); /** @var BudgetLimitTransformer $transformer */ - $transformer = app(BudgetLimitTransformer::class); + $transformer = app(BudgetLimitTransformer::class); $transformer->setParameters($this->parameters); - $resource = new Item($budgetLimit, $transformer, 'budget_limits'); + $resource = new Item($budgetLimit, $transformer, 'budget_limits'); return response()->json($manager->createData($resource)->toArray())->header('Content-Type', self::CONTENT_TYPE); } diff --git a/app/Api/V1/Requests/Models/BudgetLimit/StoreRequest.php b/app/Api/V1/Requests/Models/BudgetLimit/StoreRequest.php index 48c77cf2a2..2afabc38f3 100644 --- a/app/Api/V1/Requests/Models/BudgetLimit/StoreRequest.php +++ b/app/Api/V1/Requests/Models/BudgetLimit/StoreRequest.php @@ -24,10 +24,18 @@ declare(strict_types=1); namespace FireflyIII\Api\V1\Requests\Models\BudgetLimit; +use Carbon\Carbon; +use FireflyIII\Exceptions\FireflyException; +use FireflyIII\Factory\TransactionCurrencyFactory; +use FireflyIII\Models\Budget; +use FireflyIII\Rules\IsBoolean; use FireflyIII\Rules\IsValidPositiveAmount; +use FireflyIII\Support\Facades\Amount; use FireflyIII\Support\Request\ChecksLogin; use FireflyIII\Support\Request\ConvertsDataTypes; use Illuminate\Foundation\Http\FormRequest; +use Illuminate\Support\Facades\Log; +use Illuminate\Validation\Validator; /** * Class StoreRequest @@ -49,6 +57,9 @@ class StoreRequest extends FormRequest 'currency_id' => $this->convertInteger('currency_id'), 'currency_code' => $this->convertString('currency_code'), 'notes' => $this->stringWithNewlines('notes'), + + // for webhooks: + 'fire_webhooks' => $this->boolean('fire_webhooks', true), ]; } @@ -64,6 +75,53 @@ class StoreRequest extends FormRequest 'currency_id' => 'numeric|exists:transaction_currencies,id', 'currency_code' => 'min:3|max:51|exists:transaction_currencies,code', 'notes' => 'nullable|min:0|max:32768', + + // webhooks + 'fire_webhooks' => [new IsBoolean()], ]; } + + /** + * Configure the validator instance. + */ + public function withValidator(Validator $validator): void + { + $budget = $this->route()->parameter('budget'); + $validator->after( + static function (Validator $validator) use ($budget): void { + if(0 !== count($validator->failed())) { + return; + } + $data = $validator->getData(); + + // if no currency has been provided, use the user's default currency: + /** @var TransactionCurrencyFactory $factory */ + $factory = app(TransactionCurrencyFactory::class); + $currency = $factory->find($data['currency_id'] ?? null, $data['currency_code'] ?? null); + if (null === $currency) { + $currency = Amount::getPrimaryCurrency(); + } + $currency->enabled = true; + $currency->save(); + + // validator already concluded start and end are valid dates: + $start = Carbon::parse($data['start'], config('app.timezone')); + $end = Carbon::parse($data['end'], config('app.timezone')); + + // find limit with same date range and currency. + $limit = $budget->budgetlimits() + ->where('budget_limits.start_date', $start->format('Y-m-d')) + ->where('budget_limits.end_date', $end->format('Y-m-d')) + ->where('budget_limits.transaction_currency_id', $currency->id) + ->first(['budget_limits.*']) + ; + if(null !== $limit) { + $validator->errors()->add('start', trans('validation.limit_exists')); + } + } + ); + if ($validator->fails()) { + Log::channel('audit')->error(sprintf('Validation errors in %s', self::class), $validator->errors()->toArray()); + } + } } diff --git a/app/Handlers/Observer/BudgetLimitObserver.php b/app/Handlers/Observer/BudgetLimitObserver.php index ae6b1aac6a..c29b30dbde 100644 --- a/app/Handlers/Observer/BudgetLimitObserver.php +++ b/app/Handlers/Observer/BudgetLimitObserver.php @@ -31,6 +31,7 @@ use FireflyIII\Models\BudgetLimit; use FireflyIII\Support\Facades\Amount; use FireflyIII\Support\Http\Api\ExchangeRateConverter; use FireflyIII\Support\Observers\RecalculatesAvailableBudgetsTrait; +use FireflyIII\Support\Singleton\PreferencesSingleton; use Illuminate\Support\Collection; use Illuminate\Support\Facades\Log; @@ -44,17 +45,24 @@ class BudgetLimitObserver $this->updatePrimaryCurrencyAmount($budgetLimit); $this->updateAvailableBudget($budgetLimit); - $user = $budgetLimit->budget->user; - /** @var MessageGeneratorInterface $engine */ - $engine = app(MessageGeneratorInterface::class); - $engine->setUser($user); - $engine->setObjects(new Collection()->push($budgetLimit)); - $engine->setTrigger(WebhookTrigger::STORE_UPDATE_BUDGET_LIMIT); - $engine->generateMessages(); + // this is a lame trick to communicate with the observer. + $singleton = PreferencesSingleton::getInstance(); - Log::debug(sprintf('send event RequestedSendWebhookMessages from %s', __METHOD__)); - event(new RequestedSendWebhookMessages()); + if (true === $singleton->getPreference('fire_webhooks_bl_store')) { + + $user = $budgetLimit->budget->user; + + /** @var MessageGeneratorInterface $engine */ + $engine = app(MessageGeneratorInterface::class); + $engine->setUser($user); + $engine->setObjects(new Collection()->push($budgetLimit)); + $engine->setTrigger(WebhookTrigger::STORE_UPDATE_BUDGET_LIMIT); + $engine->generateMessages(); + + Log::debug(sprintf('send event RequestedSendWebhookMessages from %s', __METHOD__)); + event(new RequestedSendWebhookMessages()); + } } private function updatePrimaryCurrencyAmount(BudgetLimit $budgetLimit): void diff --git a/app/Repositories/Budget/BudgetLimitRepository.php b/app/Repositories/Budget/BudgetLimitRepository.php index 8974613364..67378a92d4 100644 --- a/app/Repositories/Budget/BudgetLimitRepository.php +++ b/app/Repositories/Budget/BudgetLimitRepository.php @@ -31,8 +31,10 @@ use FireflyIII\Models\Budget; use FireflyIII\Models\BudgetLimit; use FireflyIII\Models\Note; use FireflyIII\Models\TransactionCurrency; +use FireflyIII\Support\Facades\Amount; use FireflyIII\Support\Repositories\UserGroup\UserGroupInterface; use FireflyIII\Support\Repositories\UserGroup\UserGroupTrait; +use FireflyIII\Support\Singleton\PreferencesSingleton; use Illuminate\Database\Eloquent\Builder; use Illuminate\Support\Collection; use Illuminate\Support\Facades\Log; @@ -271,7 +273,7 @@ class BudgetLimitRepository implements BudgetLimitRepositoryInterface, UserGroup $factory = app(TransactionCurrencyFactory::class); $currency = $factory->find($data['currency_id'] ?? null, $data['currency_code'] ?? null); if (null === $currency) { - $currency = app('amount')->getPrimaryCurrencyByUserGroup($this->user->userGroup); + $currency = Amount::getPrimaryCurrencyByUserGroup($this->user->userGroup); } $currency->enabled = true; $currency->save(); @@ -293,7 +295,11 @@ class BudgetLimitRepository implements BudgetLimitRepositoryInterface, UserGroup if (null !== $limit) { throw new FireflyException('200027: Budget limit already exists.'); } - app('log')->debug('No existing budget limit, create a new one'); + Log::debug('No existing budget limit, create a new one'); + + // this is a lame trick to communicate with the observer. + $singleton = PreferencesSingleton::getInstance(); + $singleton->setPreference('fire_webhooks_bl_store', $data['fire_webhooks'] ?? true); // or create one and return it. $limit = new BudgetLimit(); @@ -309,7 +315,7 @@ class BudgetLimitRepository implements BudgetLimitRepositoryInterface, UserGroup $this->setNoteText($limit, $noteText); } - app('log')->debug(sprintf('Created new budget limit with ID #%d and amount %s', $limit->id, $data['amount'])); + Log::debug(sprintf('Created new budget limit with ID #%d and amount %s', $limit->id, $data['amount'])); return $limit; } @@ -393,7 +399,7 @@ class BudgetLimitRepository implements BudgetLimitRepositoryInterface, UserGroup ->where('budget_limits.end_date', $end->format('Y-m-d 00:00:00')) ->count('budget_limits.*') ; - app('log')->debug(sprintf('Found %d budget limits.', $limits)); + Log::debug(sprintf('Found %d budget limits.', $limits)); // there might be a budget limit for these dates: /** @var null|BudgetLimit $limit */ @@ -405,7 +411,7 @@ class BudgetLimitRepository implements BudgetLimitRepositoryInterface, UserGroup // if more than 1 limit found, delete the others: if ($limits > 1 && null !== $limit) { - app('log')->debug(sprintf('Found more than 1, delete all except #%d', $limit->id)); + Log::debug(sprintf('Found more than 1, delete all except #%d', $limit->id)); $budget->budgetlimits() ->where('budget_limits.start_date', $start->format('Y-m-d 00:00:00')) ->where('budget_limits.end_date', $end->format('Y-m-d 00:00:00')) @@ -417,20 +423,20 @@ class BudgetLimitRepository implements BudgetLimitRepositoryInterface, UserGroup // Returns 0 if the two operands are equal, // 1 if the left_operand is larger than the right_operand, -1 otherwise. if (null !== $limit && bccomp($amount, '0') <= 0) { - app('log')->debug(sprintf('%s is zero, delete budget limit #%d', $amount, $limit->id)); + Log::debug(sprintf('%s is zero, delete budget limit #%d', $amount, $limit->id)); $limit->delete(); return null; } // update if exists: if (null !== $limit) { - app('log')->debug(sprintf('Existing budget limit is #%d, update this to amount %s', $limit->id, $amount)); + Log::debug(sprintf('Existing budget limit is #%d, update this to amount %s', $limit->id, $amount)); $limit->amount = $amount; $limit->save(); return $limit; } - app('log')->debug('No existing budget limit, create a new one'); + Log::debug('No existing budget limit, create a new one'); // or create one and return it. $limit = new BudgetLimit(); $limit->budget()->associate($budget); @@ -440,7 +446,7 @@ class BudgetLimitRepository implements BudgetLimitRepositoryInterface, UserGroup $limit->end_date_tz = $end->format('e'); $limit->amount = $amount; $limit->save(); - app('log')->debug(sprintf('Created new budget limit with ID #%d and amount %s', $limit->id, $amount)); + Log::debug(sprintf('Created new budget limit with ID #%d and amount %s', $limit->id, $amount)); return $limit; } diff --git a/resources/lang/en_US/validation.php b/resources/lang/en_US/validation.php index 9c9052b050..7c2134f5a2 100644 --- a/resources/lang/en_US/validation.php +++ b/resources/lang/en_US/validation.php @@ -24,6 +24,7 @@ declare(strict_types=1); return [ + 'limit_exists' => 'There is already a budget limit (amount) for this budget and currency in the given period.', 'invalid_sort_instruction' => 'The sort instruction is invalid for an object of type ":object".', 'invalid_sort_instruction_index' => 'The sort instruction at index #:index is invalid for an object of type ":object".', 'no_sort_instructions' => 'There are no sort instructions defined for an object of type ":object".', From 9075fa8ac8690763ce5fabce362567986c47d235 Mon Sep 17 00:00:00 2001 From: James Cole Date: Sun, 14 Sep 2025 09:21:32 +0200 Subject: [PATCH 09/58] Allow webhooks to be send for budget limit update. --- .../Models/BudgetLimit/UpdateController.php | 1 + .../Models/BudgetLimit/UpdateRequest.php | 7 + app/Handlers/Observer/BudgetLimitObserver.php | 25 ++-- .../Budget/BudgetLimitRepository.php | 124 +++++++++--------- .../Budget/BudgetLimitRepositoryInterface.php | 2 +- 5 files changed, 88 insertions(+), 71 deletions(-) diff --git a/app/Api/V1/Controllers/Models/BudgetLimit/UpdateController.php b/app/Api/V1/Controllers/Models/BudgetLimit/UpdateController.php index cd17067b5c..cca3953140 100644 --- a/app/Api/V1/Controllers/Models/BudgetLimit/UpdateController.php +++ b/app/Api/V1/Controllers/Models/BudgetLimit/UpdateController.php @@ -77,6 +77,7 @@ class UpdateController extends Controller throw new FireflyException('20028: The budget limit does not belong to the budget.'); } $data = $request->getAll(); + $data['fire_webhooks'] = $data['fire_webhooks'] ?? true; $data['budget_id'] = $budget->id; $budgetLimit = $this->blRepository->update($budgetLimit, $data); $manager = $this->getManager(); diff --git a/app/Api/V1/Requests/Models/BudgetLimit/UpdateRequest.php b/app/Api/V1/Requests/Models/BudgetLimit/UpdateRequest.php index 42f7849292..4f877b4865 100644 --- a/app/Api/V1/Requests/Models/BudgetLimit/UpdateRequest.php +++ b/app/Api/V1/Requests/Models/BudgetLimit/UpdateRequest.php @@ -24,6 +24,7 @@ declare(strict_types=1); namespace FireflyIII\Api\V1\Requests\Models\BudgetLimit; +use FireflyIII\Rules\IsBoolean; use Illuminate\Validation\Validator; use Carbon\Carbon; use FireflyIII\Rules\IsValidPositiveAmount; @@ -52,6 +53,9 @@ class UpdateRequest extends FormRequest 'currency_id' => ['currency_id', 'convertInteger'], 'currency_code' => ['currency_code', 'convertString'], 'notes' => ['notes', 'stringWithNewlines'], + + // webhooks + 'fire_webhooks' => ['fire_webhooks','boolean'] ]; if (false === $this->has('notes')) { // ignore notes, not submitted. @@ -73,6 +77,9 @@ class UpdateRequest extends FormRequest 'currency_id' => 'numeric|exists:transaction_currencies,id', 'currency_code' => 'min:3|max:51|exists:transaction_currencies,code', 'notes' => 'nullable|min:0|max:32768', + + // webhooks + 'fire_webhooks' => [new IsBoolean()], ]; } diff --git a/app/Handlers/Observer/BudgetLimitObserver.php b/app/Handlers/Observer/BudgetLimitObserver.php index c29b30dbde..4fb16a06fa 100644 --- a/app/Handlers/Observer/BudgetLimitObserver.php +++ b/app/Handlers/Observer/BudgetLimitObserver.php @@ -75,7 +75,7 @@ class BudgetLimitObserver $userCurrency = app('amount')->getPrimaryCurrencyByUserGroup($budgetLimit->budget->user->userGroup); $budgetLimit->native_amount = null; if ($budgetLimit->transactionCurrency->id !== $userCurrency->id) { - $converter = new ExchangeRateConverter(); + $converter = new ExchangeRateConverter(); $converter->setUserGroup($budgetLimit->budget->user->userGroup); $converter->setIgnoreSettings(true); $budgetLimit->native_amount = $converter->convert($budgetLimit->transactionCurrency, $userCurrency, today(), $budgetLimit->amount); @@ -90,16 +90,21 @@ class BudgetLimitObserver $this->updatePrimaryCurrencyAmount($budgetLimit); $this->updateAvailableBudget($budgetLimit); - $user = $budgetLimit->budget->user; + // this is a lame trick to communicate with the observer. + $singleton = PreferencesSingleton::getInstance(); - /** @var MessageGeneratorInterface $engine */ - $engine = app(MessageGeneratorInterface::class); - $engine->setUser($user); - $engine->setObjects(new Collection()->push($budgetLimit)); - $engine->setTrigger(WebhookTrigger::STORE_UPDATE_BUDGET_LIMIT); - $engine->generateMessages(); + if (true === $singleton->getPreference('fire_webhooks_bl_update')) { + $user = $budgetLimit->budget->user; - Log::debug(sprintf('send event RequestedSendWebhookMessages from %s', __METHOD__)); - event(new RequestedSendWebhookMessages()); + /** @var MessageGeneratorInterface $engine */ + $engine = app(MessageGeneratorInterface::class); + $engine->setUser($user); + $engine->setObjects(new Collection()->push($budgetLimit)); + $engine->setTrigger(WebhookTrigger::STORE_UPDATE_BUDGET_LIMIT); + $engine->generateMessages(); + + Log::debug(sprintf('send event RequestedSendWebhookMessages from %s', __METHOD__)); + event(new RequestedSendWebhookMessages()); + } } } diff --git a/app/Repositories/Budget/BudgetLimitRepository.php b/app/Repositories/Budget/BudgetLimitRepository.php index 67378a92d4..857052b081 100644 --- a/app/Repositories/Budget/BudgetLimitRepository.php +++ b/app/Repositories/Budget/BudgetLimitRepository.php @@ -375,11 +375,15 @@ class BudgetLimitRepository implements BudgetLimitRepositoryInterface, UserGroup } // catch unexpected null: if (null === $currency) { - $currency = $budgetLimit->transactionCurrency ?? app('amount')->getPrimaryCurrencyByUserGroup($this->user->userGroup); + $currency = $budgetLimit->transactionCurrency ?? Amount::getPrimaryCurrencyByUserGroup($this->user->userGroup); } $currency->enabled = true; $currency->save(); + // this is a lame trick to communicate with the observer. + $singleton = PreferencesSingleton::getInstance(); + $singleton->setPreference('fire_webhooks_bl_update', $data['fire_webhooks'] ?? true); + $budgetLimit->transaction_currency_id = $currency->id; $budgetLimit->save(); @@ -391,63 +395,63 @@ class BudgetLimitRepository implements BudgetLimitRepositoryInterface, UserGroup return $budgetLimit; } - public function updateLimitAmount(Budget $budget, Carbon $start, Carbon $end, string $amount): ?BudgetLimit - { - // count the limits: - $limits = $budget->budgetlimits() - ->where('budget_limits.start_date', $start->format('Y-m-d 00:00:00')) - ->where('budget_limits.end_date', $end->format('Y-m-d 00:00:00')) - ->count('budget_limits.*') - ; - Log::debug(sprintf('Found %d budget limits.', $limits)); - - // there might be a budget limit for these dates: - /** @var null|BudgetLimit $limit */ - $limit = $budget->budgetlimits() - ->where('budget_limits.start_date', $start->format('Y-m-d 00:00:00')) - ->where('budget_limits.end_date', $end->format('Y-m-d 00:00:00')) - ->first(['budget_limits.*']) - ; - - // if more than 1 limit found, delete the others: - if ($limits > 1 && null !== $limit) { - Log::debug(sprintf('Found more than 1, delete all except #%d', $limit->id)); - $budget->budgetlimits() - ->where('budget_limits.start_date', $start->format('Y-m-d 00:00:00')) - ->where('budget_limits.end_date', $end->format('Y-m-d 00:00:00')) - ->where('budget_limits.id', '!=', $limit->id)->delete() - ; - } - - // delete if amount is zero. - // Returns 0 if the two operands are equal, - // 1 if the left_operand is larger than the right_operand, -1 otherwise. - if (null !== $limit && bccomp($amount, '0') <= 0) { - Log::debug(sprintf('%s is zero, delete budget limit #%d', $amount, $limit->id)); - $limit->delete(); - - return null; - } - // update if exists: - if (null !== $limit) { - Log::debug(sprintf('Existing budget limit is #%d, update this to amount %s', $limit->id, $amount)); - $limit->amount = $amount; - $limit->save(); - - return $limit; - } - Log::debug('No existing budget limit, create a new one'); - // or create one and return it. - $limit = new BudgetLimit(); - $limit->budget()->associate($budget); - $limit->start_date = $start->startOfDay(); - $limit->start_date_tz = $start->format('e'); - $limit->end_date = $end->startOfDay(); - $limit->end_date_tz = $end->format('e'); - $limit->amount = $amount; - $limit->save(); - Log::debug(sprintf('Created new budget limit with ID #%d and amount %s', $limit->id, $amount)); - - return $limit; - } +// public function updateLimitAmount(Budget $budget, Carbon $start, Carbon $end, string $amount): ?BudgetLimit +// { +// // count the limits: +// $limits = $budget->budgetlimits() +// ->where('budget_limits.start_date', $start->format('Y-m-d 00:00:00')) +// ->where('budget_limits.end_date', $end->format('Y-m-d 00:00:00')) +// ->count('budget_limits.*') +// ; +// Log::debug(sprintf('Found %d budget limits.', $limits)); +// +// // there might be a budget limit for these dates: +// /** @var null|BudgetLimit $limit */ +// $limit = $budget->budgetlimits() +// ->where('budget_limits.start_date', $start->format('Y-m-d 00:00:00')) +// ->where('budget_limits.end_date', $end->format('Y-m-d 00:00:00')) +// ->first(['budget_limits.*']) +// ; +// +// // if more than 1 limit found, delete the others: +// if ($limits > 1 && null !== $limit) { +// Log::debug(sprintf('Found more than 1, delete all except #%d', $limit->id)); +// $budget->budgetlimits() +// ->where('budget_limits.start_date', $start->format('Y-m-d 00:00:00')) +// ->where('budget_limits.end_date', $end->format('Y-m-d 00:00:00')) +// ->where('budget_limits.id', '!=', $limit->id)->delete() +// ; +// } +// +// // delete if amount is zero. +// // Returns 0 if the two operands are equal, +// // 1 if the left_operand is larger than the right_operand, -1 otherwise. +// if (null !== $limit && bccomp($amount, '0') <= 0) { +// Log::debug(sprintf('%s is zero, delete budget limit #%d', $amount, $limit->id)); +// $limit->delete(); +// +// return null; +// } +// // update if exists: +// if (null !== $limit) { +// Log::debug(sprintf('Existing budget limit is #%d, update this to amount %s', $limit->id, $amount)); +// $limit->amount = $amount; +// $limit->save(); +// +// return $limit; +// } +// Log::debug('No existing budget limit, create a new one'); +// // or create one and return it. +// $limit = new BudgetLimit(); +// $limit->budget()->associate($budget); +// $limit->start_date = $start->startOfDay(); +// $limit->start_date_tz = $start->format('e'); +// $limit->end_date = $end->startOfDay(); +// $limit->end_date_tz = $end->format('e'); +// $limit->amount = $amount; +// $limit->save(); +// Log::debug(sprintf('Created new budget limit with ID #%d and amount %s', $limit->id, $amount)); +// +// return $limit; +// } } diff --git a/app/Repositories/Budget/BudgetLimitRepositoryInterface.php b/app/Repositories/Budget/BudgetLimitRepositoryInterface.php index c56093ef61..7d8ccde5bb 100644 --- a/app/Repositories/Budget/BudgetLimitRepositoryInterface.php +++ b/app/Repositories/Budget/BudgetLimitRepositoryInterface.php @@ -81,5 +81,5 @@ interface BudgetLimitRepositoryInterface public function update(BudgetLimit $budgetLimit, array $data): BudgetLimit; - public function updateLimitAmount(Budget $budget, Carbon $start, Carbon $end, string $amount): ?BudgetLimit; + //public function updateLimitAmount(Budget $budget, Carbon $start, Carbon $end, string $amount): ?BudgetLimit; } From de9efb0727f394bb26f29ae9efae003c63042882 Mon Sep 17 00:00:00 2001 From: JC5 Date: Mon, 15 Sep 2025 05:23:47 +0200 Subject: [PATCH 10/58] =?UTF-8?q?=F0=9F=A4=96=20Auto=20commit=20for=20rele?= =?UTF-8?q?ase=20'develop'=20on=202025-09-15?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Models/Budget/StoreController.php | 16 +-- .../Models/Budget/UpdateController.php | 16 +-- .../Models/BudgetLimit/StoreController.php | 24 ++-- .../Models/BudgetLimit/UpdateController.php | 2 +- .../Requests/Models/Budget/StoreRequest.php | 36 +++--- .../Requests/Models/Budget/UpdateRequest.php | 36 +++--- .../Models/BudgetLimit/StoreRequest.php | 42 +++--- .../Models/BudgetLimit/UpdateRequest.php | 26 ++-- .../Models/Transaction/StoreRequest.php | 2 +- app/Handlers/Observer/BudgetLimitObserver.php | 6 +- app/Handlers/Observer/BudgetObserver.php | 10 +- app/Models/Account.php | 25 ++-- app/Models/AccountMeta.php | 3 +- app/Models/AccountType.php | 30 ++--- app/Models/Attachment.php | 6 +- app/Models/AuditLogEntry.php | 4 +- app/Models/AutoBudget.php | 12 +- app/Models/AvailableBudget.php | 16 +-- app/Models/Bill.php | 16 +-- app/Models/Budget.php | 8 +- app/Models/BudgetLimit.php | 13 +- app/Models/Category.php | 6 +- app/Models/Configuration.php | 3 +- app/Models/CurrencyExchangeRate.php | 8 +- app/Models/GroupMembership.php | 2 +- app/Models/InvitedUser.php | 2 +- app/Models/Location.php | 2 +- app/Models/Note.php | 2 +- app/Models/ObjectGroup.php | 7 +- app/Models/PiggyBank.php | 13 +- app/Models/PiggyBankEvent.php | 6 +- app/Models/PiggyBankRepetition.php | 17 +-- app/Models/Preference.php | 8 +- app/Models/Recurrence.php | 8 +- app/Models/RecurrenceMeta.php | 4 +- app/Models/RecurrenceRepetition.php | 18 +-- app/Models/RecurrenceTransaction.php | 16 +-- app/Models/RecurrenceTransactionMeta.php | 4 +- app/Models/Rule.php | 10 +- app/Models/RuleAction.php | 4 +- app/Models/RuleGroup.php | 6 +- app/Models/RuleTrigger.php | 4 +- app/Models/Tag.php | 6 +- app/Models/Transaction.php | 12 +- app/Models/TransactionCurrency.php | 4 +- app/Models/TransactionGroup.php | 9 +- app/Models/TransactionJournal.php | 10 +- app/Models/TransactionJournalLink.php | 17 +-- app/Models/TransactionJournalMeta.php | 7 +- app/Models/TransactionType.php | 16 +-- app/Models/UserGroup.php | 6 +- app/Models/Webhook.php | 4 +- app/Models/WebhookAttempt.php | 6 +- app/Models/WebhookDelivery.php | 2 +- app/Models/WebhookMessage.php | 8 +- app/Models/WebhookResponse.php | 2 +- app/Models/WebhookTrigger.php | 2 +- app/Providers/EventServiceProvider.php | 4 +- .../Budget/BudgetLimitRepository.php | 122 +++++++++--------- .../Budget/BudgetLimitRepositoryInterface.php | 2 +- app/Repositories/Budget/BudgetRepository.php | 4 +- .../Enrichments/BudgetLimitEnrichment.php | 24 ++-- composer.lock | 28 ++-- config/firefly.php | 4 +- package-lock.json | 26 ++-- 65 files changed, 416 insertions(+), 408 deletions(-) diff --git a/app/Api/V1/Controllers/Models/Budget/StoreController.php b/app/Api/V1/Controllers/Models/Budget/StoreController.php index 3e4954f474..66a8f36153 100644 --- a/app/Api/V1/Controllers/Models/Budget/StoreController.php +++ b/app/Api/V1/Controllers/Models/Budget/StoreController.php @@ -67,24 +67,24 @@ class StoreController extends Controller */ public function store(StoreRequest $request): JsonResponse { - $data = $request->getAll(); - $data['fire_webhooks'] = $data['fire_webhooks'] ?? true; - $budget = $this->repository->store($data); + $data = $request->getAll(); + $data['fire_webhooks'] ??= true; + $budget = $this->repository->store($data); $budget->refresh(); - $manager = $this->getManager(); + $manager = $this->getManager(); // enrich /** @var User $admin */ - $admin = auth()->user(); - $enrichment = new BudgetEnrichment(); + $admin = auth()->user(); + $enrichment = new BudgetEnrichment(); $enrichment->setUser($admin); - $budget = $enrichment->enrichSingle($budget); + $budget = $enrichment->enrichSingle($budget); /** @var BudgetTransformer $transformer */ $transformer = app(BudgetTransformer::class); $transformer->setParameters($this->parameters); - $resource = new Item($budget, $transformer, 'budgets'); + $resource = new Item($budget, $transformer, 'budgets'); return response()->json($manager->createData($resource)->toArray())->header('Content-Type', self::CONTENT_TYPE); } diff --git a/app/Api/V1/Controllers/Models/Budget/UpdateController.php b/app/Api/V1/Controllers/Models/Budget/UpdateController.php index 8ec009b7e1..54bddb72a6 100644 --- a/app/Api/V1/Controllers/Models/Budget/UpdateController.php +++ b/app/Api/V1/Controllers/Models/Budget/UpdateController.php @@ -59,23 +59,23 @@ class UpdateController extends Controller public function update(UpdateRequest $request, Budget $budget): JsonResponse { - $data = $request->getAll(); - $data['fire_webhooks'] = $data['fire_webhooks'] ?? true; - $budget = $this->repository->update($budget, $data); - $manager = $this->getManager(); + $data = $request->getAll(); + $data['fire_webhooks'] ??= true; + $budget = $this->repository->update($budget, $data); + $manager = $this->getManager(); // enrich /** @var User $admin */ - $admin = auth()->user(); - $enrichment = new BudgetEnrichment(); + $admin = auth()->user(); + $enrichment = new BudgetEnrichment(); $enrichment->setUser($admin); - $budget = $enrichment->enrichSingle($budget); + $budget = $enrichment->enrichSingle($budget); /** @var BudgetTransformer $transformer */ $transformer = app(BudgetTransformer::class); $transformer->setParameters($this->parameters); - $resource = new Item($budget, $transformer, 'budgets'); + $resource = new Item($budget, $transformer, 'budgets'); return response()->json($manager->createData($resource)->toArray())->header('Content-Type', self::CONTENT_TYPE); } diff --git a/app/Api/V1/Controllers/Models/BudgetLimit/StoreController.php b/app/Api/V1/Controllers/Models/BudgetLimit/StoreController.php index 910502662c..3b16016efa 100644 --- a/app/Api/V1/Controllers/Models/BudgetLimit/StoreController.php +++ b/app/Api/V1/Controllers/Models/BudgetLimit/StoreController.php @@ -67,27 +67,27 @@ class StoreController extends Controller */ public function store(StoreRequest $request, Budget $budget): JsonResponse { - $data = $request->getAll(); - $data['start_date'] = $data['start']; - $data['end_date'] = $data['end']; - $data['fire_webhooks'] = $data['fire_webhooks'] ?? true; - $data['budget_id'] = $budget->id; + $data = $request->getAll(); + $data['start_date'] = $data['start']; + $data['end_date'] = $data['end']; + $data['fire_webhooks'] ??= true; + $data['budget_id'] = $budget->id; - $budgetLimit = $this->blRepository->store($data); - $manager = $this->getManager(); + $budgetLimit = $this->blRepository->store($data); + $manager = $this->getManager(); // enrich /** @var User $admin */ - $admin = auth()->user(); - $enrichment = new BudgetLimitEnrichment(); + $admin = auth()->user(); + $enrichment = new BudgetLimitEnrichment(); $enrichment->setUser($admin); - $budgetLimit = $enrichment->enrichSingle($budgetLimit); + $budgetLimit = $enrichment->enrichSingle($budgetLimit); /** @var BudgetLimitTransformer $transformer */ - $transformer = app(BudgetLimitTransformer::class); + $transformer = app(BudgetLimitTransformer::class); $transformer->setParameters($this->parameters); - $resource = new Item($budgetLimit, $transformer, 'budget_limits'); + $resource = new Item($budgetLimit, $transformer, 'budget_limits'); return response()->json($manager->createData($resource)->toArray())->header('Content-Type', self::CONTENT_TYPE); } diff --git a/app/Api/V1/Controllers/Models/BudgetLimit/UpdateController.php b/app/Api/V1/Controllers/Models/BudgetLimit/UpdateController.php index cca3953140..3517062e2b 100644 --- a/app/Api/V1/Controllers/Models/BudgetLimit/UpdateController.php +++ b/app/Api/V1/Controllers/Models/BudgetLimit/UpdateController.php @@ -77,7 +77,7 @@ class UpdateController extends Controller throw new FireflyException('20028: The budget limit does not belong to the budget.'); } $data = $request->getAll(); - $data['fire_webhooks'] = $data['fire_webhooks'] ?? true; + $data['fire_webhooks'] ??= true; $data['budget_id'] = $budget->id; $budgetLimit = $this->blRepository->update($budgetLimit, $data); $manager = $this->getManager(); diff --git a/app/Api/V1/Requests/Models/Budget/StoreRequest.php b/app/Api/V1/Requests/Models/Budget/StoreRequest.php index fc17d164dd..fd36873cbb 100644 --- a/app/Api/V1/Requests/Models/Budget/StoreRequest.php +++ b/app/Api/V1/Requests/Models/Budget/StoreRequest.php @@ -48,20 +48,20 @@ class StoreRequest extends FormRequest public function getAll(): array { $fields = [ - 'name' => ['name', 'convertString'], - 'active' => ['active', 'boolean'], - 'order' => ['active', 'convertInteger'], - 'notes' => ['notes', 'convertString'], + 'name' => ['name', 'convertString'], + 'active' => ['active', 'boolean'], + 'order' => ['active', 'convertInteger'], + 'notes' => ['notes', 'convertString'], // auto budget currency: - 'currency_id' => ['auto_budget_currency_id', 'convertInteger'], - 'currency_code' => ['auto_budget_currency_code', 'convertString'], - 'auto_budget_type' => ['auto_budget_type', 'convertString'], - 'auto_budget_amount' => ['auto_budget_amount', 'convertString'], - 'auto_budget_period' => ['auto_budget_period', 'convertString'], + 'currency_id' => ['auto_budget_currency_id', 'convertInteger'], + 'currency_code' => ['auto_budget_currency_code', 'convertString'], + 'auto_budget_type' => ['auto_budget_type', 'convertString'], + 'auto_budget_amount' => ['auto_budget_amount', 'convertString'], + 'auto_budget_period' => ['auto_budget_period', 'convertString'], // webhooks - 'fire_webhooks' => ['fire_webhooks','boolean'] + 'fire_webhooks' => ['fire_webhooks', 'boolean'], ]; return $this->getAllData($fields); @@ -73,15 +73,15 @@ class StoreRequest extends FormRequest public function rules(): array { return [ - 'name' => 'required|min:1|max:255|uniqueObjectForUser:budgets,name', - 'active' => [new IsBoolean()], - 'currency_id' => 'exists:transaction_currencies,id', - 'currency_code' => 'exists:transaction_currencies,code', - 'notes' => 'nullable|min:1|max:32768', + 'name' => 'required|min:1|max:255|uniqueObjectForUser:budgets,name', + 'active' => [new IsBoolean()], + 'currency_id' => 'exists:transaction_currencies,id', + 'currency_code' => 'exists:transaction_currencies,code', + 'notes' => 'nullable|min:1|max:32768', // auto budget info - 'auto_budget_type' => 'in:reset,rollover,adjusted,none', - 'auto_budget_amount' => ['required_if:auto_budget_type,reset', 'required_if:auto_budget_type,rollover', 'required_if:auto_budget_type,adjusted', new IsValidPositiveAmount()], - 'auto_budget_period' => 'in:daily,weekly,monthly,quarterly,half_year,yearly|required_if:auto_budget_type,reset|required_if:auto_budget_type,rollover|required_if:auto_budget_type,adjusted', + 'auto_budget_type' => 'in:reset,rollover,adjusted,none', + 'auto_budget_amount' => ['required_if:auto_budget_type,reset', 'required_if:auto_budget_type,rollover', 'required_if:auto_budget_type,adjusted', new IsValidPositiveAmount()], + 'auto_budget_period' => 'in:daily,weekly,monthly,quarterly,half_year,yearly|required_if:auto_budget_type,reset|required_if:auto_budget_type,rollover|required_if:auto_budget_type,adjusted', // webhooks 'fire_webhooks' => [new IsBoolean()], diff --git a/app/Api/V1/Requests/Models/Budget/UpdateRequest.php b/app/Api/V1/Requests/Models/Budget/UpdateRequest.php index 3a89dbaa15..870cc3294f 100644 --- a/app/Api/V1/Requests/Models/Budget/UpdateRequest.php +++ b/app/Api/V1/Requests/Models/Budget/UpdateRequest.php @@ -50,18 +50,18 @@ class UpdateRequest extends FormRequest { // this is the way: $fields = [ - 'name' => ['name', 'convertString'], - 'active' => ['active', 'boolean'], - 'order' => ['order', 'convertInteger'], - 'notes' => ['notes', 'convertString'], - 'currency_id' => ['auto_budget_currency_id', 'convertInteger'], - 'currency_code' => ['auto_budget_currency_code', 'convertString'], - 'auto_budget_type' => ['auto_budget_type', 'convertString'], - 'auto_budget_amount' => ['auto_budget_amount', 'convertString'], - 'auto_budget_period' => ['auto_budget_period', 'convertString'], + 'name' => ['name', 'convertString'], + 'active' => ['active', 'boolean'], + 'order' => ['order', 'convertInteger'], + 'notes' => ['notes', 'convertString'], + 'currency_id' => ['auto_budget_currency_id', 'convertInteger'], + 'currency_code' => ['auto_budget_currency_code', 'convertString'], + 'auto_budget_type' => ['auto_budget_type', 'convertString'], + 'auto_budget_amount' => ['auto_budget_amount', 'convertString'], + 'auto_budget_period' => ['auto_budget_period', 'convertString'], // webhooks - 'fire_webhooks' => ['fire_webhooks','boolean'] + 'fire_webhooks' => ['fire_webhooks', 'boolean'], ]; $allData = $this->getAllData($fields); if (array_key_exists('auto_budget_type', $allData)) { @@ -86,14 +86,14 @@ class UpdateRequest extends FormRequest $budget = $this->route()->parameter('budget'); return [ - 'name' => sprintf('min:1|max:100|uniqueObjectForUser:budgets,name,%d', $budget->id), - 'active' => [new IsBoolean()], - 'notes' => 'nullable|min:1|max:32768', - 'auto_budget_type' => 'in:reset,rollover,adjusted,none', - 'auto_budget_currency_id' => 'exists:transaction_currencies,id', - 'auto_budget_currency_code' => 'exists:transaction_currencies,code', - 'auto_budget_amount' => ['nullable', new IsValidPositiveAmount()], - 'auto_budget_period' => 'in:daily,weekly,monthly,quarterly,half_year,yearly', + 'name' => sprintf('min:1|max:100|uniqueObjectForUser:budgets,name,%d', $budget->id), + 'active' => [new IsBoolean()], + 'notes' => 'nullable|min:1|max:32768', + 'auto_budget_type' => 'in:reset,rollover,adjusted,none', + 'auto_budget_currency_id' => 'exists:transaction_currencies,id', + 'auto_budget_currency_code' => 'exists:transaction_currencies,code', + 'auto_budget_amount' => ['nullable', new IsValidPositiveAmount()], + 'auto_budget_period' => 'in:daily,weekly,monthly,quarterly,half_year,yearly', // webhooks 'fire_webhooks' => [new IsBoolean()], diff --git a/app/Api/V1/Requests/Models/BudgetLimit/StoreRequest.php b/app/Api/V1/Requests/Models/BudgetLimit/StoreRequest.php index 2afabc38f3..1a705223de 100644 --- a/app/Api/V1/Requests/Models/BudgetLimit/StoreRequest.php +++ b/app/Api/V1/Requests/Models/BudgetLimit/StoreRequest.php @@ -25,9 +25,7 @@ declare(strict_types=1); namespace FireflyIII\Api\V1\Requests\Models\BudgetLimit; use Carbon\Carbon; -use FireflyIII\Exceptions\FireflyException; use FireflyIII\Factory\TransactionCurrencyFactory; -use FireflyIII\Models\Budget; use FireflyIII\Rules\IsBoolean; use FireflyIII\Rules\IsValidPositiveAmount; use FireflyIII\Support\Facades\Amount; @@ -69,12 +67,12 @@ class StoreRequest extends FormRequest public function rules(): array { return [ - 'start' => 'required|before:end|date', - 'end' => 'required|after:start|date', - 'amount' => ['required', new IsValidPositiveAmount()], - 'currency_id' => 'numeric|exists:transaction_currencies,id', - 'currency_code' => 'min:3|max:51|exists:transaction_currencies,code', - 'notes' => 'nullable|min:0|max:32768', + 'start' => 'required|before:end|date', + 'end' => 'required|after:start|date', + 'amount' => ['required', new IsValidPositiveAmount()], + 'currency_id' => 'numeric|exists:transaction_currencies,id', + 'currency_code' => 'min:3|max:51|exists:transaction_currencies,code', + 'notes' => 'nullable|min:0|max:32768', // webhooks 'fire_webhooks' => [new IsBoolean()], @@ -86,36 +84,36 @@ class StoreRequest extends FormRequest */ public function withValidator(Validator $validator): void { - $budget = $this->route()->parameter('budget'); + $budget = $this->route()->parameter('budget'); $validator->after( static function (Validator $validator) use ($budget): void { - if(0 !== count($validator->failed())) { + if (0 !== count($validator->failed())) { return; } - $data = $validator->getData(); + $data = $validator->getData(); // if no currency has been provided, use the user's default currency: /** @var TransactionCurrencyFactory $factory */ - $factory = app(TransactionCurrencyFactory::class); - $currency = $factory->find($data['currency_id'] ?? null, $data['currency_code'] ?? null); + $factory = app(TransactionCurrencyFactory::class); + $currency = $factory->find($data['currency_id'] ?? null, $data['currency_code'] ?? null); if (null === $currency) { $currency = Amount::getPrimaryCurrency(); } - $currency->enabled = true; + $currency->enabled = true; $currency->save(); // validator already concluded start and end are valid dates: - $start = Carbon::parse($data['start'], config('app.timezone')); - $end = Carbon::parse($data['end'], config('app.timezone')); + $start = Carbon::parse($data['start'], config('app.timezone')); + $end = Carbon::parse($data['end'], config('app.timezone')); // find limit with same date range and currency. - $limit = $budget->budgetlimits() - ->where('budget_limits.start_date', $start->format('Y-m-d')) - ->where('budget_limits.end_date', $end->format('Y-m-d')) - ->where('budget_limits.transaction_currency_id', $currency->id) - ->first(['budget_limits.*']) + $limit = $budget->budgetlimits() + ->where('budget_limits.start_date', $start->format('Y-m-d')) + ->where('budget_limits.end_date', $end->format('Y-m-d')) + ->where('budget_limits.transaction_currency_id', $currency->id) + ->first(['budget_limits.*']) ; - if(null !== $limit) { + if (null !== $limit) { $validator->errors()->add('start', trans('validation.limit_exists')); } } diff --git a/app/Api/V1/Requests/Models/BudgetLimit/UpdateRequest.php b/app/Api/V1/Requests/Models/BudgetLimit/UpdateRequest.php index 4f877b4865..5262ab4427 100644 --- a/app/Api/V1/Requests/Models/BudgetLimit/UpdateRequest.php +++ b/app/Api/V1/Requests/Models/BudgetLimit/UpdateRequest.php @@ -47,15 +47,15 @@ class UpdateRequest extends FormRequest public function getAll(): array { $fields = [ - 'start' => ['start', 'date'], - 'end' => ['end', 'date'], - 'amount' => ['amount', 'convertString'], - 'currency_id' => ['currency_id', 'convertInteger'], - 'currency_code' => ['currency_code', 'convertString'], - 'notes' => ['notes', 'stringWithNewlines'], + 'start' => ['start', 'date'], + 'end' => ['end', 'date'], + 'amount' => ['amount', 'convertString'], + 'currency_id' => ['currency_id', 'convertInteger'], + 'currency_code' => ['currency_code', 'convertString'], + 'notes' => ['notes', 'stringWithNewlines'], // webhooks - 'fire_webhooks' => ['fire_webhooks','boolean'] + 'fire_webhooks' => ['fire_webhooks', 'boolean'], ]; if (false === $this->has('notes')) { // ignore notes, not submitted. @@ -71,12 +71,12 @@ class UpdateRequest extends FormRequest public function rules(): array { return [ - 'start' => 'date|after:1970-01-02|before:2038-01-17', - 'end' => 'date|after:1970-01-02|before:2038-01-17', - 'amount' => ['nullable', new IsValidPositiveAmount()], - 'currency_id' => 'numeric|exists:transaction_currencies,id', - 'currency_code' => 'min:3|max:51|exists:transaction_currencies,code', - 'notes' => 'nullable|min:0|max:32768', + 'start' => 'date|after:1970-01-02|before:2038-01-17', + 'end' => 'date|after:1970-01-02|before:2038-01-17', + 'amount' => ['nullable', new IsValidPositiveAmount()], + 'currency_id' => 'numeric|exists:transaction_currencies,id', + 'currency_code' => 'min:3|max:51|exists:transaction_currencies,code', + 'notes' => 'nullable|min:0|max:32768', // webhooks 'fire_webhooks' => [new IsBoolean()], diff --git a/app/Api/V1/Requests/Models/Transaction/StoreRequest.php b/app/Api/V1/Requests/Models/Transaction/StoreRequest.php index 6e85264031..4ad8a851cb 100644 --- a/app/Api/V1/Requests/Models/Transaction/StoreRequest.php +++ b/app/Api/V1/Requests/Models/Transaction/StoreRequest.php @@ -183,7 +183,7 @@ class StoreRequest extends FormRequest // basic fields for group: 'group_title' => 'min:1|max:1000|nullable', 'error_if_duplicate_hash' => [new IsBoolean()], - 'fire_webhooks' => [new IsBoolean()], + 'fire_webhooks' => [new IsBoolean()], 'apply_rules' => [new IsBoolean()], // location rules diff --git a/app/Handlers/Observer/BudgetLimitObserver.php b/app/Handlers/Observer/BudgetLimitObserver.php index 4fb16a06fa..3c6558f0e9 100644 --- a/app/Handlers/Observer/BudgetLimitObserver.php +++ b/app/Handlers/Observer/BudgetLimitObserver.php @@ -51,7 +51,7 @@ class BudgetLimitObserver if (true === $singleton->getPreference('fire_webhooks_bl_store')) { - $user = $budgetLimit->budget->user; + $user = $budgetLimit->budget->user; /** @var MessageGeneratorInterface $engine */ $engine = app(MessageGeneratorInterface::class); @@ -75,7 +75,7 @@ class BudgetLimitObserver $userCurrency = app('amount')->getPrimaryCurrencyByUserGroup($budgetLimit->budget->user->userGroup); $budgetLimit->native_amount = null; if ($budgetLimit->transactionCurrency->id !== $userCurrency->id) { - $converter = new ExchangeRateConverter(); + $converter = new ExchangeRateConverter(); $converter->setUserGroup($budgetLimit->budget->user->userGroup); $converter->setIgnoreSettings(true); $budgetLimit->native_amount = $converter->convert($budgetLimit->transactionCurrency, $userCurrency, today(), $budgetLimit->amount); @@ -94,7 +94,7 @@ class BudgetLimitObserver $singleton = PreferencesSingleton::getInstance(); if (true === $singleton->getPreference('fire_webhooks_bl_update')) { - $user = $budgetLimit->budget->user; + $user = $budgetLimit->budget->user; /** @var MessageGeneratorInterface $engine */ $engine = app(MessageGeneratorInterface::class); diff --git a/app/Handlers/Observer/BudgetObserver.php b/app/Handlers/Observer/BudgetObserver.php index 9030c4f955..2a78e1f4be 100644 --- a/app/Handlers/Observer/BudgetObserver.php +++ b/app/Handlers/Observer/BudgetObserver.php @@ -51,7 +51,7 @@ class BudgetObserver if (true === $singleton->getPreference('fire_webhooks_budget_create')) { // fire event. - $user = $budget->user; + $user = $budget->user; /** @var MessageGeneratorInterface $engine */ $engine = app(MessageGeneratorInterface::class); @@ -72,7 +72,7 @@ class BudgetObserver $singleton = PreferencesSingleton::getInstance(); if (true === $singleton->getPreference('fire_webhooks_budget_update')) { - $user = $budget->user; + $user = $budget->user; /** @var MessageGeneratorInterface $engine */ $engine = app(MessageGeneratorInterface::class); @@ -89,10 +89,10 @@ class BudgetObserver { Log::debug('Observe "deleting" of a budget.'); - $user = $budget->user; + $user = $budget->user; /** @var MessageGeneratorInterface $engine */ - $engine = app(MessageGeneratorInterface::class); + $engine = app(MessageGeneratorInterface::class); $engine->setUser($user); $engine->setObjects(new Collection()->push($budget)); $engine->setTrigger(WebhookTrigger::DESTROY_BUDGET); @@ -100,7 +100,7 @@ class BudgetObserver Log::debug(sprintf('send event RequestedSendWebhookMessages from %s', __METHOD__)); event(new RequestedSendWebhookMessages()); - $repository = app(AttachmentRepositoryInterface::class); + $repository = app(AttachmentRepositoryInterface::class); $repository->setUser($budget->user); /** @var Attachment $attachment */ diff --git a/app/Models/Account.php b/app/Models/Account.php index aedc24f00b..e76ba418ee 100644 --- a/app/Models/Account.php +++ b/app/Models/Account.php @@ -50,9 +50,9 @@ class Account extends Model use ReturnsIntegerUserIdTrait; use SoftDeletes; - protected $fillable = ['user_id', 'user_group_id', 'account_type_id', 'name', 'active', 'virtual_balance', 'iban', 'native_virtual_balance']; + protected $fillable = ['user_id', 'user_group_id', 'account_type_id', 'name', 'active', 'virtual_balance', 'iban', 'native_virtual_balance']; - protected $hidden = ['encrypted']; + protected $hidden = ['encrypted']; private bool $joinedAccountTypes = false; /** @@ -66,10 +66,10 @@ class Account extends Model $accountId = (int)$value; /** @var User $user */ - $user = auth()->user(); + $user = auth()->user(); /** @var null|Account $account */ - $account = $user->accounts()->with(['accountType'])->find($accountId); + $account = $user->accounts()->with(['accountType'])->find($accountId); if (null !== $account) { return $account; } @@ -126,7 +126,7 @@ class Account extends Model public function setVirtualBalanceAttribute(mixed $value): void { - $value = (string)$value; + $value = (string)$value; if ('' === $value) { $value = null; } @@ -146,7 +146,7 @@ class Account extends Model protected function accountId(): Attribute { return Attribute::make( - get: static fn($value) => (int)$value, + get: static fn ($value) => (int)$value, ); } @@ -158,8 +158,9 @@ class Account extends Model return Attribute::make(get: function () { /** @var null|AccountMeta $metaValue */ $metaValue = $this->accountMeta() - ->where('name', 'account_number') - ->first(); + ->where('name', 'account_number') + ->first() + ; return null !== $metaValue ? $metaValue->data : ''; }); @@ -176,7 +177,7 @@ class Account extends Model protected function accountTypeId(): Attribute { return Attribute::make( - get: static fn($value) => (int)$value, + get: static fn ($value) => (int)$value, ); } @@ -220,14 +221,14 @@ class Account extends Model protected function iban(): Attribute { return Attribute::make( - get: static fn($value) => null === $value ? null : trim(str_replace(' ', '', (string)$value)), + get: static fn ($value) => null === $value ? null : trim(str_replace(' ', '', (string)$value)), ); } protected function order(): Attribute { return Attribute::make( - get: static fn($value) => (int)$value, + get: static fn ($value) => (int)$value, ); } @@ -237,7 +238,7 @@ class Account extends Model protected function virtualBalance(): Attribute { return Attribute::make( - get: static fn($value) => (string)$value, + get: static fn ($value) => (string)$value, ); } } diff --git a/app/Models/AccountMeta.php b/app/Models/AccountMeta.php index ef87a0a508..a91e5eb778 100644 --- a/app/Models/AccountMeta.php +++ b/app/Models/AccountMeta.php @@ -27,6 +27,7 @@ use FireflyIII\Support\Models\ReturnsIntegerIdTrait; use Illuminate\Database\Eloquent\Casts\Attribute; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo; + use function Safe\json_decode; use function Safe\json_encode; @@ -52,6 +53,6 @@ class AccountMeta extends Model protected function data(): Attribute { - return Attribute::make(get: fn(mixed $value) => (string)json_decode((string)$value, true), set: fn(mixed $value) => ['data' => json_encode($value)]); + return Attribute::make(get: fn (mixed $value) => (string)json_decode((string)$value, true), set: fn (mixed $value) => ['data' => json_encode($value)]); } } diff --git a/app/Models/AccountType.php b/app/Models/AccountType.php index b147593f38..d4b82b45fd 100644 --- a/app/Models/AccountType.php +++ b/app/Models/AccountType.php @@ -34,39 +34,39 @@ class AccountType extends Model #[Deprecated] /** @deprecated */ - public const string ASSET = 'Asset account'; + public const string ASSET = 'Asset account'; #[Deprecated] /** @deprecated */ - public const string BENEFICIARY = 'Beneficiary account'; + public const string BENEFICIARY = 'Beneficiary account'; #[Deprecated] /** @deprecated */ - public const string CASH = 'Cash account'; + public const string CASH = 'Cash account'; #[Deprecated] /** @deprecated */ - public const string CREDITCARD = 'Credit card'; + public const string CREDITCARD = 'Credit card'; #[Deprecated] /** @deprecated */ - public const string DEBT = 'Debt'; + public const string DEBT = 'Debt'; #[Deprecated] /** @deprecated */ - public const string DEFAULT = 'Default account'; + public const string DEFAULT = 'Default account'; #[Deprecated] /** @deprecated */ - public const string EXPENSE = 'Expense account'; + public const string EXPENSE = 'Expense account'; #[Deprecated] /** @deprecated */ - public const string IMPORT = 'Import account'; + public const string IMPORT = 'Import account'; #[Deprecated] /** @deprecated */ - public const string INITIAL_BALANCE = 'Initial balance account'; + public const string INITIAL_BALANCE = 'Initial balance account'; #[Deprecated] /** @deprecated */ @@ -74,27 +74,27 @@ class AccountType extends Model #[Deprecated] /** @deprecated */ - public const string LOAN = 'Loan'; + public const string LOAN = 'Loan'; #[Deprecated] /** @deprecated */ - public const string MORTGAGE = 'Mortgage'; + public const string MORTGAGE = 'Mortgage'; #[Deprecated] /** @deprecated */ - public const string RECONCILIATION = 'Reconciliation account'; + public const string RECONCILIATION = 'Reconciliation account'; #[Deprecated] /** @deprecated */ - public const string REVENUE = 'Revenue account'; + public const string REVENUE = 'Revenue account'; protected $casts - = [ + = [ 'created_at' => 'datetime', 'updated_at' => 'datetime', ]; - protected $fillable = ['type']; + protected $fillable = ['type']; public function accounts(): HasMany { diff --git a/app/Models/Attachment.php b/app/Models/Attachment.php index dd468558ab..0323697cb8 100644 --- a/app/Models/Attachment.php +++ b/app/Models/Attachment.php @@ -56,10 +56,10 @@ class Attachment extends Model $attachmentId = (int)$value; /** @var User $user */ - $user = auth()->user(); + $user = auth()->user(); /** @var null|Attachment $attachment */ - $attachment = $user->attachments()->find($attachmentId); + $attachment = $user->attachments()->find($attachmentId); if (null !== $attachment) { return $attachment; } @@ -100,7 +100,7 @@ class Attachment extends Model protected function attachableId(): Attribute { return Attribute::make( - get: static fn($value) => (int)$value, + get: static fn ($value) => (int)$value, ); } diff --git a/app/Models/AuditLogEntry.php b/app/Models/AuditLogEntry.php index baf532bfb0..fded5ca815 100644 --- a/app/Models/AuditLogEntry.php +++ b/app/Models/AuditLogEntry.php @@ -48,7 +48,7 @@ class AuditLogEntry extends Model protected function auditableId(): Attribute { return Attribute::make( - get: static fn($value) => (int)$value, + get: static fn ($value) => (int)$value, ); } @@ -66,7 +66,7 @@ class AuditLogEntry extends Model protected function changerId(): Attribute { return Attribute::make( - get: static fn($value) => (int)$value, + get: static fn ($value) => (int)$value, ); } } diff --git a/app/Models/AutoBudget.php b/app/Models/AutoBudget.php index d1aa6475c9..73dad3c6aa 100644 --- a/app/Models/AutoBudget.php +++ b/app/Models/AutoBudget.php @@ -45,17 +45,17 @@ class AutoBudget extends Model #[Deprecated] /** @deprecated */ - public const int AUTO_BUDGET_RESET = 1; + public const int AUTO_BUDGET_RESET = 1; #[Deprecated] /** @deprecated */ public const int AUTO_BUDGET_ROLLOVER = 2; protected $casts - = [ + = [ 'amount' => 'string', 'native_amount' => 'string', ]; - protected $fillable = ['budget_id', 'amount', 'period', 'native_amount']; + protected $fillable = ['budget_id', 'amount', 'period', 'native_amount']; public function budget(): BelongsTo { @@ -70,14 +70,14 @@ class AutoBudget extends Model protected function amount(): Attribute { return Attribute::make( - get: static fn($value) => (string)$value, + get: static fn ($value) => (string)$value, ); } protected function budgetId(): Attribute { return Attribute::make( - get: static fn($value) => (int)$value, + get: static fn ($value) => (int)$value, ); } @@ -91,7 +91,7 @@ class AutoBudget extends Model protected function transactionCurrencyId(): Attribute { return Attribute::make( - get: static fn($value) => (int)$value, + get: static fn ($value) => (int)$value, ); } } diff --git a/app/Models/AvailableBudget.php b/app/Models/AvailableBudget.php index f3f63b4e06..109abd0307 100644 --- a/app/Models/AvailableBudget.php +++ b/app/Models/AvailableBudget.php @@ -55,10 +55,10 @@ class AvailableBudget extends Model $availableBudgetId = (int)$value; /** @var User $user */ - $user = auth()->user(); + $user = auth()->user(); /** @var null|AvailableBudget $availableBudget */ - $availableBudget = $user->availableBudgets()->find($availableBudgetId); + $availableBudget = $user->availableBudgets()->find($availableBudgetId); if (null !== $availableBudget) { return $availableBudget; } @@ -80,7 +80,7 @@ class AvailableBudget extends Model protected function amount(): Attribute { return Attribute::make( - get: static fn($value) => (string)$value, + get: static fn ($value) => (string)$value, ); } @@ -103,23 +103,23 @@ class AvailableBudget extends Model protected function endDate(): Attribute { return Attribute::make( - get: fn(string $value) => Carbon::parse($value), - set: fn(Carbon $value) => $value->format('Y-m-d'), + get: fn (string $value) => Carbon::parse($value), + set: fn (Carbon $value) => $value->format('Y-m-d'), ); } protected function startDate(): Attribute { return Attribute::make( - get: fn(string $value) => Carbon::parse($value), - set: fn(Carbon $value) => $value->format('Y-m-d'), + get: fn (string $value) => Carbon::parse($value), + set: fn (Carbon $value) => $value->format('Y-m-d'), ); } protected function transactionCurrencyId(): Attribute { return Attribute::make( - get: static fn($value) => (int)$value, + get: static fn ($value) => (int)$value, ); } } diff --git a/app/Models/Bill.php b/app/Models/Bill.php index a84552ba82..b57d98df1d 100644 --- a/app/Models/Bill.php +++ b/app/Models/Bill.php @@ -46,7 +46,7 @@ class Bill extends Model use SoftDeletes; protected $fillable - = [ + = [ 'name', 'match', 'amount_min', @@ -81,10 +81,10 @@ class Bill extends Model $billId = (int)$value; /** @var User $user */ - $user = auth()->user(); + $user = auth()->user(); /** @var null|Bill $bill */ - $bill = $user->bills()->find($billId); + $bill = $user->bills()->find($billId); if (null !== $bill) { return $bill; } @@ -151,7 +151,7 @@ class Bill extends Model protected function amountMax(): Attribute { return Attribute::make( - get: static fn($value) => (string)$value, + get: static fn ($value) => (string)$value, ); } @@ -161,7 +161,7 @@ class Bill extends Model protected function amountMin(): Attribute { return Attribute::make( - get: static fn($value) => (string)$value, + get: static fn ($value) => (string)$value, ); } @@ -189,7 +189,7 @@ class Bill extends Model protected function order(): Attribute { return Attribute::make( - get: static fn($value) => (int)$value, + get: static fn ($value) => (int)$value, ); } @@ -199,14 +199,14 @@ class Bill extends Model protected function skip(): Attribute { return Attribute::make( - get: static fn($value) => (int)$value, + get: static fn ($value) => (int)$value, ); } protected function transactionCurrencyId(): Attribute { return Attribute::make( - get: static fn($value) => (int)$value, + get: static fn ($value) => (int)$value, ); } } diff --git a/app/Models/Budget.php b/app/Models/Budget.php index 1a1c2bb9dc..4375c6c2a7 100644 --- a/app/Models/Budget.php +++ b/app/Models/Budget.php @@ -46,7 +46,7 @@ class Budget extends Model protected $fillable = ['user_id', 'user_group_id', 'name', 'active', 'order', 'user_group_id']; - protected $hidden = ['encrypted']; + protected $hidden = ['encrypted']; /** * Route binder. Converts the key in the URL to the specified object (or throw 404). @@ -59,10 +59,10 @@ class Budget extends Model $budgetId = (int)$value; /** @var User $user */ - $user = auth()->user(); + $user = auth()->user(); /** @var null|Budget $budget */ - $budget = $user->budgets()->find($budgetId); + $budget = $user->budgets()->find($budgetId); if (null !== $budget) { return $budget; } @@ -125,7 +125,7 @@ class Budget extends Model protected function order(): Attribute { return Attribute::make( - get: static fn($value) => (int)$value, + get: static fn ($value) => (int)$value, ); } } diff --git a/app/Models/BudgetLimit.php b/app/Models/BudgetLimit.php index a646d4d1e2..66f12753c6 100644 --- a/app/Models/BudgetLimit.php +++ b/app/Models/BudgetLimit.php @@ -50,9 +50,10 @@ class BudgetLimit extends Model if (auth()->check()) { $budgetLimitId = (int)$value; $budgetLimit = self::where('budget_limits.id', $budgetLimitId) - ->leftJoin('budgets', 'budgets.id', '=', 'budget_limits.budget_id') - ->where('budgets.user_id', auth()->user()->id) - ->first(['budget_limits.*']); + ->leftJoin('budgets', 'budgets.id', '=', 'budget_limits.budget_id') + ->where('budgets.user_id', auth()->user()->id) + ->first(['budget_limits.*']) + ; if (null !== $budgetLimit) { return $budgetLimit; } @@ -85,14 +86,14 @@ class BudgetLimit extends Model protected function amount(): Attribute { return Attribute::make( - get: static fn($value) => (string)$value, + get: static fn ($value) => (string)$value, ); } protected function budgetId(): Attribute { return Attribute::make( - get: static fn($value) => (int)$value, + get: static fn ($value) => (int)$value, ); } @@ -112,7 +113,7 @@ class BudgetLimit extends Model protected function transactionCurrencyId(): Attribute { return Attribute::make( - get: static fn($value) => (int)$value, + get: static fn ($value) => (int)$value, ); } } diff --git a/app/Models/Category.php b/app/Models/Category.php index 1726c1110c..018fbdbef4 100644 --- a/app/Models/Category.php +++ b/app/Models/Category.php @@ -45,7 +45,7 @@ class Category extends Model protected $fillable = ['user_id', 'user_group_id', 'name']; - protected $hidden = ['encrypted']; + protected $hidden = ['encrypted']; /** * Route binder. Converts the key in the URL to the specified object (or throw 404). @@ -58,10 +58,10 @@ class Category extends Model $categoryId = (int)$value; /** @var User $user */ - $user = auth()->user(); + $user = auth()->user(); /** @var null|Category $category */ - $category = $user->categories()->find($categoryId); + $category = $user->categories()->find($categoryId); if (null !== $category) { return $category; } diff --git a/app/Models/Configuration.php b/app/Models/Configuration.php index 49e7559236..3fd2d82a19 100644 --- a/app/Models/Configuration.php +++ b/app/Models/Configuration.php @@ -27,6 +27,7 @@ use FireflyIII\Support\Models\ReturnsIntegerIdTrait; use Illuminate\Database\Eloquent\Casts\Attribute; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\SoftDeletes; + use function Safe\json_decode; use function Safe\json_encode; @@ -51,6 +52,6 @@ class Configuration extends Model */ protected function data(): Attribute { - return Attribute::make(get: fn($value) => json_decode((string)$value), set: fn($value) => ['data' => json_encode($value)]); + return Attribute::make(get: fn ($value) => json_decode((string)$value), set: fn ($value) => ['data' => json_encode($value)]); } } diff --git a/app/Models/CurrencyExchangeRate.php b/app/Models/CurrencyExchangeRate.php index fc919dbf80..c484ae2cc5 100644 --- a/app/Models/CurrencyExchangeRate.php +++ b/app/Models/CurrencyExchangeRate.php @@ -73,28 +73,28 @@ class CurrencyExchangeRate extends Model protected function fromCurrencyId(): Attribute { return Attribute::make( - get: static fn($value) => (int)$value, + get: static fn ($value) => (int)$value, ); } protected function rate(): Attribute { return Attribute::make( - get: static fn($value) => (string)$value, + get: static fn ($value) => (string)$value, ); } protected function toCurrencyId(): Attribute { return Attribute::make( - get: static fn($value) => (int)$value, + get: static fn ($value) => (int)$value, ); } protected function userRate(): Attribute { return Attribute::make( - get: static fn($value) => (string)$value, + get: static fn ($value) => (string)$value, ); } } diff --git a/app/Models/GroupMembership.php b/app/Models/GroupMembership.php index e16178fa9b..52f6cf9b24 100644 --- a/app/Models/GroupMembership.php +++ b/app/Models/GroupMembership.php @@ -66,7 +66,7 @@ class GroupMembership extends Model protected function userRoleId(): Attribute { return Attribute::make( - get: static fn($value) => (int)$value, + get: static fn ($value) => (int)$value, ); } } diff --git a/app/Models/InvitedUser.php b/app/Models/InvitedUser.php index 272096faa8..81e945b5e9 100644 --- a/app/Models/InvitedUser.php +++ b/app/Models/InvitedUser.php @@ -48,7 +48,7 @@ class InvitedUser extends Model $attemptId = (int)$value; /** @var null|InvitedUser $attempt */ - $attempt = self::find($attemptId); + $attempt = self::find($attemptId); if (null !== $attempt) { return $attempt; } diff --git a/app/Models/Location.php b/app/Models/Location.php index 2e971b6f67..b8f2b31151 100644 --- a/app/Models/Location.php +++ b/app/Models/Location.php @@ -81,7 +81,7 @@ class Location extends Model protected function locatableId(): Attribute { return Attribute::make( - get: static fn($value) => (int)$value, + get: static fn ($value) => (int)$value, ); } } diff --git a/app/Models/Note.php b/app/Models/Note.php index 71d0531976..2adeacc457 100644 --- a/app/Models/Note.php +++ b/app/Models/Note.php @@ -56,7 +56,7 @@ class Note extends Model protected function noteableId(): Attribute { return Attribute::make( - get: static fn($value) => (int)$value, + get: static fn ($value) => (int)$value, ); } } diff --git a/app/Models/ObjectGroup.php b/app/Models/ObjectGroup.php index 71a4d9b9ba..189e543a41 100644 --- a/app/Models/ObjectGroup.php +++ b/app/Models/ObjectGroup.php @@ -51,8 +51,9 @@ class ObjectGroup extends Model $objectGroupId = (int)$value; /** @var null|ObjectGroup $objectGroup */ - $objectGroup = self::where('object_groups.id', $objectGroupId) - ->where('object_groups.user_id', auth()->user()->id)->first(); + $objectGroup = self::where('object_groups.id', $objectGroupId) + ->where('object_groups.user_id', auth()->user()->id)->first() + ; if (null !== $objectGroup) { return $objectGroup; } @@ -104,7 +105,7 @@ class ObjectGroup extends Model protected function order(): Attribute { return Attribute::make( - get: static fn($value) => (int)$value, + get: static fn ($value) => (int)$value, ); } } diff --git a/app/Models/PiggyBank.php b/app/Models/PiggyBank.php index c493198b09..d4eb25a787 100644 --- a/app/Models/PiggyBank.php +++ b/app/Models/PiggyBank.php @@ -54,9 +54,10 @@ class PiggyBank extends Model if (auth()->check()) { $piggyBankId = (int)$value; $piggyBank = self::where('piggy_banks.id', $piggyBankId) - ->leftJoin('account_piggy_bank', 'account_piggy_bank.piggy_bank_id', '=', 'piggy_banks.id') - ->leftJoin('accounts', 'accounts.id', '=', 'account_piggy_bank.account_id') - ->where('accounts.user_id', auth()->user()->id)->first(['piggy_banks.*']); + ->leftJoin('account_piggy_bank', 'account_piggy_bank.piggy_bank_id', '=', 'piggy_banks.id') + ->leftJoin('accounts', 'accounts.id', '=', 'account_piggy_bank.account_id') + ->where('accounts.user_id', auth()->user()->id)->first(['piggy_banks.*']) + ; if (null !== $piggyBank) { return $piggyBank; } @@ -122,7 +123,7 @@ class PiggyBank extends Model protected function accountId(): Attribute { return Attribute::make( - get: static fn($value) => (int)$value, + get: static fn ($value) => (int)$value, ); } @@ -145,7 +146,7 @@ class PiggyBank extends Model protected function order(): Attribute { return Attribute::make( - get: static fn($value) => (int)$value, + get: static fn ($value) => (int)$value, ); } @@ -155,7 +156,7 @@ class PiggyBank extends Model protected function targetAmount(): Attribute { return Attribute::make( - get: static fn($value) => (string)$value, + get: static fn ($value) => (string)$value, ); } } diff --git a/app/Models/PiggyBankEvent.php b/app/Models/PiggyBankEvent.php index 81578a9544..3395e3df53 100644 --- a/app/Models/PiggyBankEvent.php +++ b/app/Models/PiggyBankEvent.php @@ -38,7 +38,7 @@ class PiggyBankEvent extends Model protected $fillable = ['piggy_bank_id', 'transaction_journal_id', 'date', 'date_tz', 'amount', 'native_amount']; - protected $hidden = ['amount_encrypted']; + protected $hidden = ['amount_encrypted']; public function piggyBank(): BelongsTo { @@ -64,7 +64,7 @@ class PiggyBankEvent extends Model protected function amount(): Attribute { return Attribute::make( - get: static fn($value) => (string)$value, + get: static fn ($value) => (string)$value, ); } @@ -82,7 +82,7 @@ class PiggyBankEvent extends Model protected function piggyBankId(): Attribute { return Attribute::make( - get: static fn($value) => (int)$value, + get: static fn ($value) => (int)$value, ); } } diff --git a/app/Models/PiggyBankRepetition.php b/app/Models/PiggyBankRepetition.php index fa6d9823ed..41af799266 100644 --- a/app/Models/PiggyBankRepetition.php +++ b/app/Models/PiggyBankRepetition.php @@ -68,7 +68,7 @@ class PiggyBankRepetition extends Model protected function currentAmount(): Attribute { return Attribute::make( - get: static fn($value) => (string)$value, + get: static fn ($value) => (string)$value, ); } @@ -81,7 +81,7 @@ class PiggyBankRepetition extends Model protected function piggyBankId(): Attribute { return Attribute::make( - get: static fn($value) => (int)$value, + get: static fn ($value) => (int)$value, ); } @@ -97,11 +97,12 @@ class PiggyBankRepetition extends Model $q->orWhereNull('start_date'); } ) - ->where( - static function (EloquentBuilder $q) use ($date): void { - $q->where('target_date', '>=', $date->format('Y-m-d 00:00:00')); - $q->orWhereNull('target_date'); - } - ); + ->where( + static function (EloquentBuilder $q) use ($date): void { + $q->where('target_date', '>=', $date->format('Y-m-d 00:00:00')); + $q->orWhereNull('target_date'); + } + ) + ; } } diff --git a/app/Models/Preference.php b/app/Models/Preference.php index f357f17cc7..151e5bbd35 100644 --- a/app/Models/Preference.php +++ b/app/Models/Preference.php @@ -46,7 +46,7 @@ class Preference extends Model { if (auth()->check()) { /** @var User $user */ - $user = auth()->user(); + $user = auth()->user(); // some preferences do not have an administration ID. // some need it, to make sure the correct one is selected. @@ -54,8 +54,8 @@ class Preference extends Model $userGroupId = 0 === $userGroupId ? null : $userGroupId; /** @var null|Preference $preference */ - $preference = null; - $items = config('firefly.admin_specific_prefs'); + $preference = null; + $items = config('firefly.admin_specific_prefs'); if (null !== $userGroupId && in_array($value, $items, true)) { // find a preference with a specific user_group_id $preference = $user->preferences()->where('user_group_id', $userGroupId)->where('name', $value)->first(); @@ -73,7 +73,7 @@ class Preference extends Model /** @var Preference $preference */ return $preference; } - $default = config('firefly.default_preferences'); + $default = config('firefly.default_preferences'); if (array_key_exists($value, $default)) { $preference = new self(); $preference->name = $value; diff --git a/app/Models/Recurrence.php b/app/Models/Recurrence.php index 724ccdb06f..b3fa7a3a8c 100644 --- a/app/Models/Recurrence.php +++ b/app/Models/Recurrence.php @@ -46,7 +46,7 @@ class Recurrence extends Model use SoftDeletes; protected $fillable - = ['user_id', 'user_group_id', 'transaction_type_id', 'title', 'description', 'first_date', 'first_date_tz', 'repeat_until', 'repeat_until_tz', 'latest_date', 'latest_date_tz', 'repetitions', 'apply_rules', 'active']; + = ['user_id', 'user_group_id', 'transaction_type_id', 'title', 'description', 'first_date', 'first_date_tz', 'repeat_until', 'repeat_until_tz', 'latest_date', 'latest_date_tz', 'repetitions', 'apply_rules', 'active']; protected $table = 'recurrences'; @@ -61,10 +61,10 @@ class Recurrence extends Model $recurrenceId = (int)$value; /** @var User $user */ - $user = auth()->user(); + $user = auth()->user(); /** @var null|Recurrence $recurrence */ - $recurrence = $user->recurrences()->find($recurrenceId); + $recurrence = $user->recurrences()->find($recurrenceId); if (null !== $recurrence) { return $recurrence; } @@ -139,7 +139,7 @@ class Recurrence extends Model protected function transactionTypeId(): Attribute { return Attribute::make( - get: static fn($value) => (int)$value, + get: static fn ($value) => (int)$value, ); } } diff --git a/app/Models/RecurrenceMeta.php b/app/Models/RecurrenceMeta.php index fddd2e8292..d031e0b883 100644 --- a/app/Models/RecurrenceMeta.php +++ b/app/Models/RecurrenceMeta.php @@ -37,7 +37,7 @@ class RecurrenceMeta extends Model protected $fillable = ['recurrence_id', 'name', 'value']; - protected $table = 'recurrences_meta'; + protected $table = 'recurrences_meta'; public function recurrence(): BelongsTo { @@ -58,7 +58,7 @@ class RecurrenceMeta extends Model protected function recurrenceId(): Attribute { return Attribute::make( - get: static fn($value) => (int)$value, + get: static fn ($value) => (int)$value, ); } } diff --git a/app/Models/RecurrenceRepetition.php b/app/Models/RecurrenceRepetition.php index 920f8d93a1..52d8259877 100644 --- a/app/Models/RecurrenceRepetition.php +++ b/app/Models/RecurrenceRepetition.php @@ -38,7 +38,7 @@ class RecurrenceRepetition extends Model #[Deprecated] /** @deprecated */ - public const int WEEKEND_DO_NOTHING = 1; + public const int WEEKEND_DO_NOTHING = 1; #[Deprecated] /** @deprecated */ @@ -46,14 +46,14 @@ class RecurrenceRepetition extends Model #[Deprecated] /** @deprecated */ - public const int WEEKEND_TO_FRIDAY = 3; + public const int WEEKEND_TO_FRIDAY = 3; #[Deprecated] /** @deprecated */ - public const int WEEKEND_TO_MONDAY = 4; + public const int WEEKEND_TO_MONDAY = 4; protected $casts - = [ + = [ 'created_at' => 'datetime', 'updated_at' => 'datetime', 'deleted_at' => 'datetime', @@ -63,9 +63,9 @@ class RecurrenceRepetition extends Model 'weekend' => 'int', ]; - protected $fillable = ['recurrence_id', 'weekend', 'repetition_type', 'repetition_moment', 'repetition_skip']; + protected $fillable = ['recurrence_id', 'weekend', 'repetition_type', 'repetition_moment', 'repetition_skip']; - protected $table = 'recurrences_repetitions'; + protected $table = 'recurrences_repetitions'; public function recurrence(): BelongsTo { @@ -82,21 +82,21 @@ class RecurrenceRepetition extends Model protected function recurrenceId(): Attribute { return Attribute::make( - get: static fn($value) => (int)$value, + get: static fn ($value) => (int)$value, ); } protected function repetitionSkip(): Attribute { return Attribute::make( - get: static fn($value) => (int)$value, + get: static fn ($value) => (int)$value, ); } protected function weekend(): Attribute { return Attribute::make( - get: static fn($value) => (int)$value, + get: static fn ($value) => (int)$value, ); } } diff --git a/app/Models/RecurrenceTransaction.php b/app/Models/RecurrenceTransaction.php index 4c1a5f6f2c..c2d91a54ad 100644 --- a/app/Models/RecurrenceTransaction.php +++ b/app/Models/RecurrenceTransaction.php @@ -40,7 +40,7 @@ class RecurrenceTransaction extends Model use SoftDeletes; protected $fillable - = [ + = [ 'recurrence_id', 'transaction_currency_id', 'foreign_currency_id', @@ -91,7 +91,7 @@ class RecurrenceTransaction extends Model protected function amount(): Attribute { return Attribute::make( - get: static fn($value) => (string)$value, + get: static fn ($value) => (string)$value, ); } @@ -110,42 +110,42 @@ class RecurrenceTransaction extends Model protected function destinationId(): Attribute { return Attribute::make( - get: static fn($value) => (int)$value, + get: static fn ($value) => (int)$value, ); } protected function foreignAmount(): Attribute { return Attribute::make( - get: static fn($value) => (string)$value, + get: static fn ($value) => (string)$value, ); } protected function recurrenceId(): Attribute { return Attribute::make( - get: static fn($value) => (int)$value, + get: static fn ($value) => (int)$value, ); } protected function sourceId(): Attribute { return Attribute::make( - get: static fn($value) => (int)$value, + get: static fn ($value) => (int)$value, ); } protected function transactionCurrencyId(): 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, ); } } diff --git a/app/Models/RecurrenceTransactionMeta.php b/app/Models/RecurrenceTransactionMeta.php index 5f2644b0f6..442da02559 100644 --- a/app/Models/RecurrenceTransactionMeta.php +++ b/app/Models/RecurrenceTransactionMeta.php @@ -37,7 +37,7 @@ class RecurrenceTransactionMeta extends Model protected $fillable = ['rt_id', 'name', 'value']; - protected $table = 'rt_meta'; + protected $table = 'rt_meta'; public function recurrenceTransaction(): BelongsTo { @@ -58,7 +58,7 @@ class RecurrenceTransactionMeta extends Model protected function rtId(): Attribute { return Attribute::make( - get: static fn($value) => (int)$value, + get: static fn ($value) => (int)$value, ); } } diff --git a/app/Models/Rule.php b/app/Models/Rule.php index a8de1ea45b..7755e00984 100644 --- a/app/Models/Rule.php +++ b/app/Models/Rule.php @@ -55,10 +55,10 @@ class Rule extends Model $ruleId = (int)$value; /** @var User $user */ - $user = auth()->user(); + $user = auth()->user(); /** @var null|Rule $rule */ - $rule = $user->rules()->find($ruleId); + $rule = $user->rules()->find($ruleId); if (null !== $rule) { return $rule; } @@ -110,20 +110,20 @@ class Rule extends Model protected function description(): Attribute { - return Attribute::make(set: fn($value) => ['description' => e($value)]); + return Attribute::make(set: fn ($value) => ['description' => e($value)]); } protected function order(): Attribute { return Attribute::make( - get: static fn($value) => (int)$value, + get: static fn ($value) => (int)$value, ); } protected function ruleGroupId(): Attribute { return Attribute::make( - get: static fn($value) => (int)$value, + get: static fn ($value) => (int)$value, ); } } diff --git a/app/Models/RuleAction.php b/app/Models/RuleAction.php index 3603c6cc65..0bb29a5236 100644 --- a/app/Models/RuleAction.php +++ b/app/Models/RuleAction.php @@ -80,14 +80,14 @@ class RuleAction extends Model protected function order(): Attribute { return Attribute::make( - get: static fn($value) => (int)$value, + get: static fn ($value) => (int)$value, ); } protected function ruleId(): Attribute { return Attribute::make( - get: static fn($value) => (int)$value, + get: static fn ($value) => (int)$value, ); } } diff --git a/app/Models/RuleGroup.php b/app/Models/RuleGroup.php index d53155d888..99f6655f63 100644 --- a/app/Models/RuleGroup.php +++ b/app/Models/RuleGroup.php @@ -55,10 +55,10 @@ class RuleGroup extends Model $ruleGroupId = (int)$value; /** @var User $user */ - $user = auth()->user(); + $user = auth()->user(); /** @var null|RuleGroup $ruleGroup */ - $ruleGroup = $user->ruleGroups()->find($ruleGroupId); + $ruleGroup = $user->ruleGroups()->find($ruleGroupId); if (null !== $ruleGroup) { return $ruleGroup; } @@ -94,7 +94,7 @@ class RuleGroup extends Model protected function order(): Attribute { return Attribute::make( - get: static fn($value) => (int)$value, + get: static fn ($value) => (int)$value, ); } } diff --git a/app/Models/RuleTrigger.php b/app/Models/RuleTrigger.php index 9fedd62399..c3de8048ba 100644 --- a/app/Models/RuleTrigger.php +++ b/app/Models/RuleTrigger.php @@ -53,14 +53,14 @@ class RuleTrigger extends Model protected function order(): Attribute { return Attribute::make( - get: static fn($value) => (int)$value, + get: static fn ($value) => (int)$value, ); } protected function ruleId(): Attribute { return Attribute::make( - get: static fn($value) => (int)$value, + get: static fn ($value) => (int)$value, ); } } diff --git a/app/Models/Tag.php b/app/Models/Tag.php index 22572b8a06..c9fc2f134b 100644 --- a/app/Models/Tag.php +++ b/app/Models/Tag.php @@ -45,7 +45,7 @@ class Tag extends Model protected $fillable = ['user_id', 'user_group_id', 'tag', 'date', 'date_tz', 'description', 'tag_mode']; - protected $hidden = ['zoomLevel', 'zoom_level', 'latitude', 'longitude']; + protected $hidden = ['zoomLevel', 'zoom_level', 'latitude', 'longitude']; /** * Route binder. Converts the key in the URL to the specified object (or throw 404). @@ -58,10 +58,10 @@ class Tag extends Model $tagId = (int)$value; /** @var User $user */ - $user = auth()->user(); + $user = auth()->user(); /** @var null|Tag $tag */ - $tag = $user->tags()->find($tagId); + $tag = $user->tags()->find($tagId); if (null !== $tag) { return $tag; } diff --git a/app/Models/Transaction.php b/app/Models/Transaction.php index e0b521d30f..e33cb0a1ac 100644 --- a/app/Models/Transaction.php +++ b/app/Models/Transaction.php @@ -44,7 +44,7 @@ class Transaction extends Model use SoftDeletes; protected $fillable - = [ + = [ 'account_id', 'transaction_journal_id', 'description', @@ -113,7 +113,7 @@ class Transaction extends Model protected function accountId(): Attribute { return Attribute::make( - get: static fn($value) => (int)$value, + get: static fn ($value) => (int)$value, ); } @@ -151,14 +151,14 @@ class Transaction extends Model protected function amount(): Attribute { return Attribute::make( - get: static fn($value) => (string)$value, + get: static fn ($value) => (string)$value, ); } protected function balanceDirty(): Attribute { return Attribute::make( - get: static fn($value) => 1 === (int)$value, + get: static fn ($value) => 1 === (int)$value, ); } @@ -201,14 +201,14 @@ class Transaction extends Model protected function foreignAmount(): Attribute { return Attribute::make( - get: static fn($value) => (string)$value, + get: static fn ($value) => (string)$value, ); } protected function transactionJournalId(): Attribute { return Attribute::make( - get: static fn($value) => (int)$value, + get: static fn ($value) => (int)$value, ); } diff --git a/app/Models/TransactionCurrency.php b/app/Models/TransactionCurrency.php index 7ecfbd18db..6f02375355 100644 --- a/app/Models/TransactionCurrency.php +++ b/app/Models/TransactionCurrency.php @@ -40,7 +40,7 @@ class TransactionCurrency extends Model public ?bool $userGroupEnabled = null; public ?bool $userGroupNative = null; - protected $fillable = ['name', 'code', 'symbol', 'decimal_places', 'enabled']; + protected $fillable = ['name', 'code', 'symbol', 'decimal_places', 'enabled']; /** * Route binder. Converts the key in the URL to the specified object (or throw 404). @@ -115,7 +115,7 @@ class TransactionCurrency extends Model protected function decimalPlaces(): Attribute { return Attribute::make( - get: static fn($value) => (int)$value, + get: static fn ($value) => (int)$value, ); } } diff --git a/app/Models/TransactionGroup.php b/app/Models/TransactionGroup.php index ec8bf0faae..81e7aac2e6 100644 --- a/app/Models/TransactionGroup.php +++ b/app/Models/TransactionGroup.php @@ -55,13 +55,14 @@ class TransactionGroup extends Model $groupId = (int)$value; /** @var User $user */ - $user = auth()->user(); + $user = auth()->user(); app('log')->debug(sprintf('User authenticated as %s', $user->email)); /** @var null|TransactionGroup $group */ - $group = $user->transactionGroups() - ->with(['transactionJournals', 'transactionJournals.transactions']) - ->where('transaction_groups.id', $groupId)->first(['transaction_groups.*']); + $group = $user->transactionGroups() + ->with(['transactionJournals', 'transactionJournals.transactions']) + ->where('transaction_groups.id', $groupId)->first(['transaction_groups.*']) + ; if (null !== $group) { app('log')->debug(sprintf('Found group #%d.', $group->id)); diff --git a/app/Models/TransactionJournal.php b/app/Models/TransactionJournal.php index dde97c2fce..3aa0099446 100644 --- a/app/Models/TransactionJournal.php +++ b/app/Models/TransactionJournal.php @@ -57,7 +57,7 @@ class TransactionJournal extends Model use SoftDeletes; protected $fillable - = [ + = [ 'user_id', 'user_group_id', 'transaction_type_id', @@ -84,10 +84,10 @@ class TransactionJournal extends Model $journalId = (int)$value; /** @var User $user */ - $user = auth()->user(); + $user = auth()->user(); /** @var null|TransactionJournal $journal */ - $journal = $user->transactionJournals()->where('transaction_journals.id', $journalId)->first(['transaction_journals.*']); + $journal = $user->transactionJournals()->where('transaction_journals.id', $journalId)->first(['transaction_journals.*']); if (null !== $journal) { return $journal; } @@ -230,14 +230,14 @@ class TransactionJournal extends Model protected function order(): Attribute { return Attribute::make( - get: static fn($value) => (int)$value, + get: static fn ($value) => (int)$value, ); } protected function transactionTypeId(): Attribute { return Attribute::make( - get: static fn($value) => (int)$value, + get: static fn ($value) => (int)$value, ); } diff --git a/app/Models/TransactionJournalLink.php b/app/Models/TransactionJournalLink.php index 4880d15c39..ab1a40c260 100644 --- a/app/Models/TransactionJournalLink.php +++ b/app/Models/TransactionJournalLink.php @@ -46,11 +46,12 @@ class TransactionJournalLink extends Model if (auth()->check()) { $linkId = (int)$value; $link = self::where('journal_links.id', $linkId) - ->leftJoin('transaction_journals as t_a', 't_a.id', '=', 'source_id') - ->leftJoin('transaction_journals as t_b', 't_b.id', '=', 'destination_id') - ->where('t_a.user_id', auth()->user()->id) - ->where('t_b.user_id', auth()->user()->id) - ->first(['journal_links.*']); + ->leftJoin('transaction_journals as t_a', 't_a.id', '=', 'source_id') + ->leftJoin('transaction_journals as t_b', 't_b.id', '=', 'destination_id') + ->where('t_a.user_id', auth()->user()->id) + ->where('t_b.user_id', auth()->user()->id) + ->first(['journal_links.*']) + ; if (null !== $link) { return $link; } @@ -93,21 +94,21 @@ class TransactionJournalLink extends Model protected function destinationId(): Attribute { return Attribute::make( - get: static fn($value) => (int)$value, + get: static fn ($value) => (int)$value, ); } protected function linkTypeId(): Attribute { return Attribute::make( - get: static fn($value) => (int)$value, + get: static fn ($value) => (int)$value, ); } protected function sourceId(): Attribute { return Attribute::make( - get: static fn($value) => (int)$value, + get: static fn ($value) => (int)$value, ); } } diff --git a/app/Models/TransactionJournalMeta.php b/app/Models/TransactionJournalMeta.php index 7dd67b6f86..4a748bfb8b 100644 --- a/app/Models/TransactionJournalMeta.php +++ b/app/Models/TransactionJournalMeta.php @@ -28,6 +28,7 @@ use Illuminate\Database\Eloquent\Casts\Attribute; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\SoftDeletes; + use function Safe\json_decode; use function Safe\json_encode; @@ -38,7 +39,7 @@ class TransactionJournalMeta extends Model protected $fillable = ['transaction_journal_id', 'name', 'data', 'hash']; - protected $table = 'journal_meta'; + protected $table = 'journal_meta'; public function transactionJournal(): BelongsTo { @@ -56,7 +57,7 @@ class TransactionJournalMeta extends Model protected function data(): Attribute { - return Attribute::make(get: fn($value) => json_decode((string)$value, false), set: function ($value) { + return Attribute::make(get: fn ($value) => json_decode((string)$value, false), set: function ($value) { $data = json_encode($value); return ['data' => $data, 'hash' => hash('sha256', $data)]; @@ -66,7 +67,7 @@ class TransactionJournalMeta extends Model protected function transactionJournalId(): Attribute { return Attribute::make( - get: static fn($value) => (int)$value, + get: static fn ($value) => (int)$value, ); } } diff --git a/app/Models/TransactionType.php b/app/Models/TransactionType.php index 4062911fae..cac754f110 100644 --- a/app/Models/TransactionType.php +++ b/app/Models/TransactionType.php @@ -38,11 +38,11 @@ class TransactionType extends Model #[Deprecated] /** @deprecated */ - public const string DEPOSIT = 'Deposit'; + public const string DEPOSIT = 'Deposit'; #[Deprecated] /** @deprecated */ - public const string INVALID = 'Invalid'; + public const string INVALID = 'Invalid'; #[Deprecated] /** @deprecated */ @@ -50,27 +50,27 @@ class TransactionType extends Model #[Deprecated] /** @deprecated */ - public const string OPENING_BALANCE = 'Opening balance'; + public const string OPENING_BALANCE = 'Opening balance'; #[Deprecated] /** @deprecated */ - public const string RECONCILIATION = 'Reconciliation'; + public const string RECONCILIATION = 'Reconciliation'; #[Deprecated] /** @deprecated */ - public const string TRANSFER = 'Transfer'; + public const string TRANSFER = 'Transfer'; #[Deprecated] /** @deprecated */ - public const string WITHDRAWAL = 'Withdrawal'; + public const string WITHDRAWAL = 'Withdrawal'; protected $casts - = [ + = [ 'created_at' => 'datetime', 'updated_at' => 'datetime', 'deleted_at' => 'datetime', ]; - protected $fillable = ['type']; + protected $fillable = ['type']; /** * Route binder. Converts the key in the URL to the specified object (or throw 404). diff --git a/app/Models/UserGroup.php b/app/Models/UserGroup.php index 7b20a2980e..0c142b69cb 100644 --- a/app/Models/UserGroup.php +++ b/app/Models/UserGroup.php @@ -50,16 +50,16 @@ class UserGroup extends Model $userGroupId = (int)$value; /** @var User $user */ - $user = auth()->user(); + $user = auth()->user(); /** @var null|UserGroup $userGroup */ - $userGroup = self::find($userGroupId); + $userGroup = self::find($userGroupId); if (null === $userGroup) { throw new NotFoundHttpException(); } // need at least ready only to be aware of the user group's existence, // but owner/full role (in the group) or global owner role may overrule this. - $access = $user->hasRoleInGroupOrOwner($userGroup, UserRoleEnum::READ_ONLY) || $user->hasRole('owner'); + $access = $user->hasRoleInGroupOrOwner($userGroup, UserRoleEnum::READ_ONLY) || $user->hasRole('owner'); if ($access) { return $userGroup; } diff --git a/app/Models/Webhook.php b/app/Models/Webhook.php index 5b19fe269c..ab7e477e37 100644 --- a/app/Models/Webhook.php +++ b/app/Models/Webhook.php @@ -137,10 +137,10 @@ class Webhook extends Model $webhookId = (int)$value; /** @var User $user */ - $user = auth()->user(); + $user = auth()->user(); /** @var null|Webhook $webhook */ - $webhook = $user->webhooks()->find($webhookId); + $webhook = $user->webhooks()->find($webhookId); if (null !== $webhook) { return $webhook; } diff --git a/app/Models/WebhookAttempt.php b/app/Models/WebhookAttempt.php index c38fd15fe1..a2de5dac3b 100644 --- a/app/Models/WebhookAttempt.php +++ b/app/Models/WebhookAttempt.php @@ -48,10 +48,10 @@ class WebhookAttempt extends Model $attemptId = (int)$value; /** @var User $user */ - $user = auth()->user(); + $user = auth()->user(); /** @var null|WebhookAttempt $attempt */ - $attempt = self::find($attemptId); + $attempt = self::find($attemptId); if (null !== $attempt && $attempt->webhookMessage->webhook->user_id === $user->id) { return $attempt; } @@ -68,7 +68,7 @@ class WebhookAttempt extends Model protected function webhookMessageId(): Attribute { return Attribute::make( - get: static fn($value) => (int)$value, + get: static fn ($value) => (int)$value, ); } } diff --git a/app/Models/WebhookDelivery.php b/app/Models/WebhookDelivery.php index 614a5cb3c2..33acf3b9b7 100644 --- a/app/Models/WebhookDelivery.php +++ b/app/Models/WebhookDelivery.php @@ -41,7 +41,7 @@ class WebhookDelivery extends Model protected function key(): Attribute { return Attribute::make( - get: static fn($value) => (int)$value, + get: static fn ($value) => (int)$value, ); } } diff --git a/app/Models/WebhookMessage.php b/app/Models/WebhookMessage.php index cae9a4a73f..dbccb97cbd 100644 --- a/app/Models/WebhookMessage.php +++ b/app/Models/WebhookMessage.php @@ -50,10 +50,10 @@ class WebhookMessage extends Model $messageId = (int)$value; /** @var User $user */ - $user = auth()->user(); + $user = auth()->user(); /** @var null|WebhookMessage $message */ - $message = self::find($messageId); + $message = self::find($messageId); if (null !== $message && $message->webhook->user_id === $user->id) { return $message; } @@ -89,14 +89,14 @@ class WebhookMessage extends Model protected function sent(): Attribute { return Attribute::make( - get: static fn($value) => (bool)$value, + get: static fn ($value) => (bool)$value, ); } protected function webhookId(): Attribute { return Attribute::make( - get: static fn($value) => (int)$value, + get: static fn ($value) => (int)$value, ); } } diff --git a/app/Models/WebhookResponse.php b/app/Models/WebhookResponse.php index 5c8cb45311..7b3e785a73 100644 --- a/app/Models/WebhookResponse.php +++ b/app/Models/WebhookResponse.php @@ -41,7 +41,7 @@ class WebhookResponse extends Model protected function key(): Attribute { return Attribute::make( - get: static fn($value) => (int)$value, + get: static fn ($value) => (int)$value, ); } } diff --git a/app/Models/WebhookTrigger.php b/app/Models/WebhookTrigger.php index 7d9a6ea1fa..b7ccd7cfc5 100644 --- a/app/Models/WebhookTrigger.php +++ b/app/Models/WebhookTrigger.php @@ -41,7 +41,7 @@ class WebhookTrigger extends Model protected function key(): Attribute { return Attribute::make( - get: static fn($value) => (int)$value, + get: static fn ($value) => (int)$value, ); } } diff --git a/app/Providers/EventServiceProvider.php b/app/Providers/EventServiceProvider.php index a63903a280..1b221013f5 100644 --- a/app/Providers/EventServiceProvider.php +++ b/app/Providers/EventServiceProvider.php @@ -216,7 +216,5 @@ class EventServiceProvider extends ServiceProvider * Register any events for your application. */ #[Override] - public function boot(): void - { - } + public function boot(): void {} } diff --git a/app/Repositories/Budget/BudgetLimitRepository.php b/app/Repositories/Budget/BudgetLimitRepository.php index 857052b081..ab80405609 100644 --- a/app/Repositories/Budget/BudgetLimitRepository.php +++ b/app/Repositories/Budget/BudgetLimitRepository.php @@ -298,7 +298,7 @@ class BudgetLimitRepository implements BudgetLimitRepositoryInterface, UserGroup Log::debug('No existing budget limit, create a new one'); // this is a lame trick to communicate with the observer. - $singleton = PreferencesSingleton::getInstance(); + $singleton = PreferencesSingleton::getInstance(); $singleton->setPreference('fire_webhooks_bl_store', $data['fire_webhooks'] ?? true); // or create one and return it. @@ -381,7 +381,7 @@ class BudgetLimitRepository implements BudgetLimitRepositoryInterface, UserGroup $currency->save(); // this is a lame trick to communicate with the observer. - $singleton = PreferencesSingleton::getInstance(); + $singleton = PreferencesSingleton::getInstance(); $singleton->setPreference('fire_webhooks_bl_update', $data['fire_webhooks'] ?? true); $budgetLimit->transaction_currency_id = $currency->id; @@ -395,63 +395,63 @@ class BudgetLimitRepository implements BudgetLimitRepositoryInterface, UserGroup return $budgetLimit; } -// public function updateLimitAmount(Budget $budget, Carbon $start, Carbon $end, string $amount): ?BudgetLimit -// { -// // count the limits: -// $limits = $budget->budgetlimits() -// ->where('budget_limits.start_date', $start->format('Y-m-d 00:00:00')) -// ->where('budget_limits.end_date', $end->format('Y-m-d 00:00:00')) -// ->count('budget_limits.*') -// ; -// Log::debug(sprintf('Found %d budget limits.', $limits)); -// -// // there might be a budget limit for these dates: -// /** @var null|BudgetLimit $limit */ -// $limit = $budget->budgetlimits() -// ->where('budget_limits.start_date', $start->format('Y-m-d 00:00:00')) -// ->where('budget_limits.end_date', $end->format('Y-m-d 00:00:00')) -// ->first(['budget_limits.*']) -// ; -// -// // if more than 1 limit found, delete the others: -// if ($limits > 1 && null !== $limit) { -// Log::debug(sprintf('Found more than 1, delete all except #%d', $limit->id)); -// $budget->budgetlimits() -// ->where('budget_limits.start_date', $start->format('Y-m-d 00:00:00')) -// ->where('budget_limits.end_date', $end->format('Y-m-d 00:00:00')) -// ->where('budget_limits.id', '!=', $limit->id)->delete() -// ; -// } -// -// // delete if amount is zero. -// // Returns 0 if the two operands are equal, -// // 1 if the left_operand is larger than the right_operand, -1 otherwise. -// if (null !== $limit && bccomp($amount, '0') <= 0) { -// Log::debug(sprintf('%s is zero, delete budget limit #%d', $amount, $limit->id)); -// $limit->delete(); -// -// return null; -// } -// // update if exists: -// if (null !== $limit) { -// Log::debug(sprintf('Existing budget limit is #%d, update this to amount %s', $limit->id, $amount)); -// $limit->amount = $amount; -// $limit->save(); -// -// return $limit; -// } -// Log::debug('No existing budget limit, create a new one'); -// // or create one and return it. -// $limit = new BudgetLimit(); -// $limit->budget()->associate($budget); -// $limit->start_date = $start->startOfDay(); -// $limit->start_date_tz = $start->format('e'); -// $limit->end_date = $end->startOfDay(); -// $limit->end_date_tz = $end->format('e'); -// $limit->amount = $amount; -// $limit->save(); -// Log::debug(sprintf('Created new budget limit with ID #%d and amount %s', $limit->id, $amount)); -// -// return $limit; -// } + // public function updateLimitAmount(Budget $budget, Carbon $start, Carbon $end, string $amount): ?BudgetLimit + // { + // // count the limits: + // $limits = $budget->budgetlimits() + // ->where('budget_limits.start_date', $start->format('Y-m-d 00:00:00')) + // ->where('budget_limits.end_date', $end->format('Y-m-d 00:00:00')) + // ->count('budget_limits.*') + // ; + // Log::debug(sprintf('Found %d budget limits.', $limits)); + // + // // there might be a budget limit for these dates: + // /** @var null|BudgetLimit $limit */ + // $limit = $budget->budgetlimits() + // ->where('budget_limits.start_date', $start->format('Y-m-d 00:00:00')) + // ->where('budget_limits.end_date', $end->format('Y-m-d 00:00:00')) + // ->first(['budget_limits.*']) + // ; + // + // // if more than 1 limit found, delete the others: + // if ($limits > 1 && null !== $limit) { + // Log::debug(sprintf('Found more than 1, delete all except #%d', $limit->id)); + // $budget->budgetlimits() + // ->where('budget_limits.start_date', $start->format('Y-m-d 00:00:00')) + // ->where('budget_limits.end_date', $end->format('Y-m-d 00:00:00')) + // ->where('budget_limits.id', '!=', $limit->id)->delete() + // ; + // } + // + // // delete if amount is zero. + // // Returns 0 if the two operands are equal, + // // 1 if the left_operand is larger than the right_operand, -1 otherwise. + // if (null !== $limit && bccomp($amount, '0') <= 0) { + // Log::debug(sprintf('%s is zero, delete budget limit #%d', $amount, $limit->id)); + // $limit->delete(); + // + // return null; + // } + // // update if exists: + // if (null !== $limit) { + // Log::debug(sprintf('Existing budget limit is #%d, update this to amount %s', $limit->id, $amount)); + // $limit->amount = $amount; + // $limit->save(); + // + // return $limit; + // } + // Log::debug('No existing budget limit, create a new one'); + // // or create one and return it. + // $limit = new BudgetLimit(); + // $limit->budget()->associate($budget); + // $limit->start_date = $start->startOfDay(); + // $limit->start_date_tz = $start->format('e'); + // $limit->end_date = $end->startOfDay(); + // $limit->end_date_tz = $end->format('e'); + // $limit->amount = $amount; + // $limit->save(); + // Log::debug(sprintf('Created new budget limit with ID #%d and amount %s', $limit->id, $amount)); + // + // return $limit; + // } } diff --git a/app/Repositories/Budget/BudgetLimitRepositoryInterface.php b/app/Repositories/Budget/BudgetLimitRepositoryInterface.php index 7d8ccde5bb..defb1c7d49 100644 --- a/app/Repositories/Budget/BudgetLimitRepositoryInterface.php +++ b/app/Repositories/Budget/BudgetLimitRepositoryInterface.php @@ -81,5 +81,5 @@ interface BudgetLimitRepositoryInterface public function update(BudgetLimit $budgetLimit, array $data): BudgetLimit; - //public function updateLimitAmount(Budget $budget, Carbon $start, Carbon $end, string $amount): ?BudgetLimit; + // public function updateLimitAmount(Budget $budget, Carbon $start, Carbon $end, string $amount): ?BudgetLimit; } diff --git a/app/Repositories/Budget/BudgetRepository.php b/app/Repositories/Budget/BudgetRepository.php index b893334320..8aaaa40b8a 100644 --- a/app/Repositories/Budget/BudgetRepository.php +++ b/app/Repositories/Budget/BudgetRepository.php @@ -286,7 +286,7 @@ class BudgetRepository implements BudgetRepositoryInterface, UserGroupInterface Log::debug('Now in update()'); // this is a lame trick to communicate with the observer. - $singleton = PreferencesSingleton::getInstance(); + $singleton = PreferencesSingleton::getInstance(); $singleton->setPreference('fire_webhooks_budget_update', $data['fire_webhooks'] ?? true); $oldName = $budget->name; @@ -730,7 +730,7 @@ class BudgetRepository implements BudgetRepositoryInterface, UserGroupInterface $order = $this->getMaxOrder(); // this is a lame trick to communicate with the observer. - $singleton = PreferencesSingleton::getInstance(); + $singleton = PreferencesSingleton::getInstance(); $singleton->setPreference('fire_webhooks_budget_create', $data['fire_webhooks'] ?? true); try { diff --git a/app/Support/JsonApi/Enrichments/BudgetLimitEnrichment.php b/app/Support/JsonApi/Enrichments/BudgetLimitEnrichment.php index c0af1e7735..0ba9e1f1a3 100644 --- a/app/Support/JsonApi/Enrichments/BudgetLimitEnrichment.php +++ b/app/Support/JsonApi/Enrichments/BudgetLimitEnrichment.php @@ -95,8 +95,8 @@ 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(); + $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) { @@ -113,9 +113,10 @@ class BudgetLimitEnrichment implements EnrichmentInterface 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(); + ->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']; } @@ -144,12 +145,12 @@ class BudgetLimitEnrichment implements EnrichmentInterface private function collectBudgets(): void { - $budgetIds = $this->collection->pluck('budget_id')->unique()->toArray(); - $budgets = Budget::whereIn('id', $budgetIds)->get(); + $budgetIds = $this->collection->pluck('budget_id')->unique()->toArray(); + $budgets = Budget::whereIn('id', $budgetIds)->get(); $repository = app(OperationsRepository::class); $repository->setUser($this->user); - $expenses = $repository->collectExpenses($this->start, $this->end, null, $budgets, null); + $expenses = $repository->collectExpenses($this->start, $this->end, null, $budgets, null); /** @var BudgetLimit $budgetLimit */ foreach ($this->collection as $budgetLimit) { @@ -180,13 +181,13 @@ class BudgetLimitEnrichment implements EnrichmentInterface private function stringifyIds(): void { - $this->expenses = array_map(fn($first) => array_map(function ($second) { + $this->expenses = array_map(fn ($first) => array_map(function ($second) { $second['currency_id'] = (string)($second['currency_id'] ?? 0); return $second; }, $first), $this->expenses); - $this->pcExpenses = array_map(fn($first) => array_map(function ($second) { + $this->pcExpenses = array_map(fn ($first) => array_map(function ($second) { $second['currency_id'] = (string)($second['currency_id'] ?? 0); return $second; @@ -195,8 +196,9 @@ class BudgetLimitEnrichment implements EnrichmentInterface private function filterToBudget(array $expenses, int $budget): array { - $result = array_filter($expenses, fn(array $item) => (int)$item['budget_id'] === $budget); + $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; } } diff --git a/composer.lock b/composer.lock index 50beadca73..3bc0182e4b 100644 --- a/composer.lock +++ b/composer.lock @@ -11819,16 +11819,16 @@ }, { "name": "phpunit/phpunit", - "version": "12.3.10", + "version": "12.3.11", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "0d401d0df2e3c1703be425ecdc2d04f5c095938d" + "reference": "6a62f2b394e042884e4997ddc8b8db1ce56a0009" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/0d401d0df2e3c1703be425ecdc2d04f5c095938d", - "reference": "0d401d0df2e3c1703be425ecdc2d04f5c095938d", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/6a62f2b394e042884e4997ddc8b8db1ce56a0009", + "reference": "6a62f2b394e042884e4997ddc8b8db1ce56a0009", "shasum": "" }, "require": { @@ -11847,7 +11847,7 @@ "phpunit/php-invoker": "^6.0.0", "phpunit/php-text-template": "^5.0.0", "phpunit/php-timer": "^8.0.0", - "sebastian/cli-parser": "^4.0.0", + "sebastian/cli-parser": "^4.1.0", "sebastian/comparator": "^7.1.3", "sebastian/diff": "^7.0.0", "sebastian/environment": "^8.0.3", @@ -11896,7 +11896,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/phpunit/issues", "security": "https://github.com/sebastianbergmann/phpunit/security/policy", - "source": "https://github.com/sebastianbergmann/phpunit/tree/12.3.10" + "source": "https://github.com/sebastianbergmann/phpunit/tree/12.3.11" }, "funding": [ { @@ -11920,7 +11920,7 @@ "type": "tidelift" } ], - "time": "2025-09-11T10:35:19+00:00" + "time": "2025-09-14T06:21:44+00:00" }, { "name": "rector/rector", @@ -11984,16 +11984,16 @@ }, { "name": "sebastian/cli-parser", - "version": "4.1.0", + "version": "4.2.0", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/cli-parser.git", - "reference": "8fd93be538992d556aaa45c74570129448a42084" + "reference": "90f41072d220e5c40df6e8635f5dafba2d9d4d04" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/cli-parser/zipball/8fd93be538992d556aaa45c74570129448a42084", - "reference": "8fd93be538992d556aaa45c74570129448a42084", + "url": "https://api.github.com/repos/sebastianbergmann/cli-parser/zipball/90f41072d220e5c40df6e8635f5dafba2d9d4d04", + "reference": "90f41072d220e5c40df6e8635f5dafba2d9d4d04", "shasum": "" }, "require": { @@ -12005,7 +12005,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "4.1-dev" + "dev-main": "4.2-dev" } }, "autoload": { @@ -12029,7 +12029,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/cli-parser/issues", "security": "https://github.com/sebastianbergmann/cli-parser/security/policy", - "source": "https://github.com/sebastianbergmann/cli-parser/tree/4.1.0" + "source": "https://github.com/sebastianbergmann/cli-parser/tree/4.2.0" }, "funding": [ { @@ -12049,7 +12049,7 @@ "type": "tidelift" } ], - "time": "2025-09-13T14:16:18+00:00" + "time": "2025-09-14T09:36:45+00:00" }, { "name": "sebastian/comparator", diff --git a/config/firefly.php b/config/firefly.php index eecaf07c61..9ebbd995ed 100644 --- a/config/firefly.php +++ b/config/firefly.php @@ -78,8 +78,8 @@ return [ 'running_balance_column' => env('USE_RUNNING_BALANCE', false), // see cer.php for exchange rates feature flag. ], - 'version' => 'develop/2025-09-13', - 'build_time' => 1757782204, + 'version' => 'develop/2025-09-15', + 'build_time' => 1757906521, 'api_version' => '2.1.0', // field is no longer used. 'db_version' => 26, diff --git a/package-lock.json b/package-lock.json index 211a5d750c..46e4e426d6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3159,13 +3159,13 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "24.3.3", - "resolved": "https://registry.npmjs.org/@types/node/-/node-24.3.3.tgz", - "integrity": "sha512-GKBNHjoNw3Kra1Qg5UXttsY5kiWMEfoHq2TmXb+b1rcm6N7B3wTrFYIf/oSZ1xNQ+hVVijgLkiDZh7jRRsh+Gw==", + "version": "24.4.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.4.0.tgz", + "integrity": "sha512-gUuVEAK4/u6F9wRLznPUU4WGUacSEBDPoC2TrBkw3GAnOLHBL45QdfHOXp1kJ4ypBGLxTOB+t7NJLpKoC3gznQ==", "dev": true, "license": "MIT", "dependencies": { - "undici-types": "~7.10.0" + "undici-types": "~7.11.0" } }, "node_modules/@types/node-forge": { @@ -3949,9 +3949,9 @@ } }, "node_modules/axios": { - "version": "1.12.1", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.12.1.tgz", - "integrity": "sha512-Kn4kbSXpkFHCGE6rBFNwIv0GQs4AvDT80jlveJDKFxjbTYMUeB4QtsdPCv6H8Cm19Je7IU6VFtRl2zWZI0rudQ==", + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.12.2.tgz", + "integrity": "sha512-vMJzPewAlRyOgxV2dU0Cuz2O8zzzx9VYtbJOaBgXFeLc4IV/Eg50n4LowmehOOR61S8ZMpc2K5Sa7g6A4jfkUw==", "dev": true, "license": "MIT", "dependencies": { @@ -5375,9 +5375,9 @@ "license": "MIT" }, "node_modules/debug": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", - "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", "dev": true, "license": "MIT", "dependencies": { @@ -11357,9 +11357,9 @@ } }, "node_modules/undici-types": { - "version": "7.10.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.10.0.tgz", - "integrity": "sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag==", + "version": "7.11.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.11.0.tgz", + "integrity": "sha512-kt1ZriHTi7MU+Z/r9DOdAI3ONdaR3M3csEaRc6ewa4f4dTvX4cQCbJ4NkEn0ohE4hHtq85+PhPSTY+pO/1PwgA==", "dev": true, "license": "MIT" }, From ecfb3e2f95048b1cbf8524dd90f9ab779760cf2e Mon Sep 17 00:00:00 2001 From: James Cole Date: Mon, 15 Sep 2025 19:20:51 +0200 Subject: [PATCH 11/58] Fix #10854 and another issue (again). --- app/Http/Controllers/Controller.php | 3 +- .../Transaction/ShowController.php | 68 +++++++------------ .../TransactionGroupRepository.php | 5 +- app/Support/Amount.php | 60 ++++++++-------- app/Support/Twig/AmountFormat.php | 18 ++--- resources/views/transactions/show.twig | 4 +- 6 files changed, 72 insertions(+), 86 deletions(-) diff --git a/app/Http/Controllers/Controller.php b/app/Http/Controllers/Controller.php index 7a4c542730..0919f8c199 100644 --- a/app/Http/Controllers/Controller.php +++ b/app/Http/Controllers/Controller.php @@ -26,6 +26,7 @@ namespace FireflyIII\Http\Controllers; use FireflyIII\Events\RequestedSendWebhookMessages; use FireflyIII\Models\TransactionCurrency; use FireflyIII\Support\Facades\Amount; +use FireflyIII\Support\Facades\Preferences; use FireflyIII\Support\Facades\Steam; use FireflyIII\Support\Http\Controllers\RequestInformation; use FireflyIII\Support\Http\Controllers\UserNavigation; @@ -133,7 +134,7 @@ abstract class Controller extends BaseController $this->primaryCurrency = Amount::getPrimaryCurrency(); $language = Steam::getLanguage(); $locale = Steam::getLocale(); - $darkMode = app('preferences')->get('darkMode', 'browser')->data; + $darkMode = Preferences::get('darkMode', 'browser')->data; $this->convertToPrimary = Amount::convertToPrimary(); $page = $this->getPageName(); $shownDemo = $this->hasSeenDemo(); diff --git a/app/Http/Controllers/Transaction/ShowController.php b/app/Http/Controllers/Transaction/ShowController.php index 2d9e81f511..12627a55fe 100644 --- a/app/Http/Controllers/Transaction/ShowController.php +++ b/app/Http/Controllers/Transaction/ShowController.php @@ -85,52 +85,52 @@ class ShowController extends Controller public function show(TransactionGroup $transactionGroup) { /** @var User $admin */ - $admin = auth()->user(); + $admin = auth()->user(); // use new group collector: /** @var GroupCollectorInterface $collector */ - $collector = app(GroupCollectorInterface::class); + $collector = app(GroupCollectorInterface::class); $collector->setUser($admin)->setTransactionGroup($transactionGroup)->withAPIInformation(); /** @var null|TransactionGroup $selectedGroup */ - $selectedGroup = $collector->getGroups()->first(); + $selectedGroup = $collector->getGroups()->first(); if (null === $selectedGroup) { throw new NotFoundHttpException(); } // enrich - $enrichment = new TransactionGroupEnrichment(); + $enrichment = new TransactionGroupEnrichment(); $enrichment->setUser($admin); - $selectedGroup = $enrichment->enrichSingle($selectedGroup); + $selectedGroup = $enrichment->enrichSingle($selectedGroup); - $splits = count($selectedGroup['transactions']); - $keys = array_keys($selectedGroup['transactions']); - $first = $selectedGroup['transactions'][array_shift($keys)]; + $splits = count($selectedGroup['transactions']); + $keys = array_keys($selectedGroup['transactions']); + $first = $selectedGroup['transactions'][array_shift($keys)]; unset($keys); if (null === $first) { throw new FireflyException('This transaction is broken :(.'); } - $type = (string)trans(sprintf('firefly.%s', $first['transaction_type_type'])); - $title = 1 === $splits ? $first['description'] : $selectedGroup['title']; - $subTitle = sprintf('%s: "%s"', $type, $title); + $type = (string)trans(sprintf('firefly.%s', $first['transaction_type_type'])); + $title = 1 === $splits ? $first['description'] : $selectedGroup['title']; + $subTitle = sprintf('%s: "%s"', $type, $title); // enrich - $enrichment = new TransactionGroupEnrichment(); + $enrichment = new TransactionGroupEnrichment(); $enrichment->setUser($admin); /** @var array $selectedGroup */ - $selectedGroup = $enrichment->enrichSingle($selectedGroup); + $selectedGroup = $enrichment->enrichSingle($selectedGroup); /** @var TransactionGroupTransformer $transformer */ - $transformer = app(TransactionGroupTransformer::class); + $transformer = app(TransactionGroupTransformer::class); $transformer->setParameters(new ParameterBag()); - $groupArray = $transformer->transformObject($transactionGroup); + $groupArray = $transformer->transformObject($transactionGroup); // do some calculations: - $amounts = $this->getAmounts($selectedGroup); - $accounts = $this->getAccounts($selectedGroup); + $amounts = $this->getAmounts($selectedGroup); + $accounts = $this->getAccounts($selectedGroup); foreach (array_keys($selectedGroup['transactions']) as $index) { $selectedGroup['transactions'][$index]['tags'] = $this->repository->getTagObjects((int)$selectedGroup['transactions'][$index]['transaction_journal_id']); @@ -142,29 +142,11 @@ class ShowController extends Controller $logEntries[$journal['transaction_journal_id']] = $this->aleRepository->getForId(TransactionJournal::class, $journal['transaction_journal_id']); } - $events = $this->repository->getPiggyEvents($transactionGroup); - $attachments = $this->repository->getAttachments($transactionGroup); - $links = $this->repository->getLinks($transactionGroup); + $events = $this->repository->getPiggyEvents($transactionGroup); + $attachments = $this->repository->getAttachments($transactionGroup); + $links = $this->repository->getLinks($transactionGroup); - return view( - 'transactions.show', - compact( - 'transactionGroup', - 'amounts', - 'first', - 'type', - 'logEntries', - 'groupLogEntries', - 'subTitle', - 'splits', - 'selectedGroup', - 'groupArray', - 'events', - 'attachments', - 'links', - 'accounts', - ) - ); + return view('transactions.show', compact('transactionGroup', 'amounts', 'first', 'type', 'logEntries', 'groupLogEntries', 'subTitle', 'splits', 'selectedGroup', 'groupArray', 'events', 'attachments', 'links', 'accounts')); } private function getAmounts(array $group): array @@ -173,7 +155,7 @@ class ShowController extends Controller foreach ($group['transactions'] as $transaction) { // add normal amount: $symbol = $transaction['currency_symbol']; - $amounts[$symbol] ??= [ + $amounts[$symbol] ??= [ 'amount' => '0', 'symbol' => $symbol, 'decimal_places' => $transaction['currency_decimal_places'], @@ -184,7 +166,7 @@ class ShowController extends Controller if (null !== $transaction['foreign_amount'] && '' !== $transaction['foreign_amount'] && 0 !== bccomp('0', (string)$transaction['foreign_amount'])) { // same for foreign currency: $foreignSymbol = $transaction['foreign_currency_symbol']; - $amounts[$foreignSymbol] ??= [ + $amounts[$foreignSymbol] ??= [ 'amount' => '0', 'symbol' => $foreignSymbol, 'decimal_places' => $transaction['foreign_currency_decimal_places'], @@ -195,7 +177,7 @@ class ShowController extends Controller if (null !== $transaction['pc_amount'] && $transaction['currency_id'] !== $this->primaryCurrency->id) { // same for foreign currency: $primarySymbol = $this->primaryCurrency->symbol; - $amounts[$primarySymbol] ??= [ + $amounts[$primarySymbol] ??= [ 'amount' => '0', 'symbol' => $this->primaryCurrency->symbol, 'decimal_places' => $this->primaryCurrency->decimal_places, @@ -210,7 +192,7 @@ class ShowController extends Controller private function getAccounts(array $group): array { - $accounts = [ + $accounts = [ 'source' => [], 'destination' => [], ]; diff --git a/app/Repositories/TransactionGroup/TransactionGroupRepository.php b/app/Repositories/TransactionGroup/TransactionGroupRepository.php index fec5d3f42f..e658b8756d 100644 --- a/app/Repositories/TransactionGroup/TransactionGroupRepository.php +++ b/app/Repositories/TransactionGroup/TransactionGroupRepository.php @@ -370,8 +370,11 @@ class TransactionGroupRepository implements TransactionGroupRepositoryInterface, public function getTagObjects(int $journalId): Collection { - /** @var TransactionJournal $journal */ + /** @var TransactionJournal|null $journal */ $journal = $this->user->transactionJournals()->find($journalId); + if(null ===$journal) { + return new Collection(); + } return $journal->tags()->whereNull('deleted_at')->get(); } diff --git a/app/Support/Amount.php b/app/Support/Amount.php index 547cf8dd79..53c067e015 100644 --- a/app/Support/Amount.php +++ b/app/Support/Amount.php @@ -60,15 +60,15 @@ class Amount */ public function formatFlat(string $symbol, int $decimalPlaces, string $amount, ?bool $coloured = null): string { - $locale = Steam::getLocale(); - $rounded = Steam::bcround($amount, $decimalPlaces); + $locale = Steam::getLocale(); + $rounded = Steam::bcround($amount, $decimalPlaces); $coloured ??= true; - $fmt = new NumberFormatter($locale, NumberFormatter::CURRENCY); + $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 + $result = (string)$fmt->format((float)$rounded); // intentional float if (true === $coloured) { if (1 === bccomp($rounded, '0')) { @@ -122,13 +122,13 @@ class Amount $key = sprintf('transaction_currency_%d', $currencyId); /** @var null|TransactionCurrency $pref */ - $pref = $instance->getPreference($key); + $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', $currencyId); + $message = sprintf('Could not find a transaction currency with ID #%d in %s', $currencyId, __METHOD__); Log::error($message); throw new FireflyException($message); @@ -144,13 +144,13 @@ class Amount $key = sprintf('transaction_currency_%s', $code); /** @var null|TransactionCurrency $pref */ - $pref = $instance->getPreference($key); + $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"', $code); + $message = sprintf('Could not find a transaction currency with code "%s" in %s', $code, __METHOD__); Log::error($message); throw new FireflyException($message); @@ -174,8 +174,8 @@ class Amount return $pref; } - $key = sprintf('convert_to_primary_%d', $user->id); - $pref = $instance->getPreference($key); + $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); @@ -201,7 +201,7 @@ class Amount public function getPrimaryCurrencyByUserGroup(UserGroup $userGroup): TransactionCurrency { - $cache = new CacheProperties(); + $cache = new CacheProperties(); $cache->addProperty('getPrimaryCurrencyByGroup'); $cache->addProperty($userGroup->id); if ($cache->has()) { @@ -231,16 +231,16 @@ class Amount */ public function getAmountFromJournalObject(TransactionJournal $journal): string { - $convertToPrimary = $this->convertToPrimary(); - $currency = $this->getPrimaryCurrency(); - $field = $convertToPrimary && $currency->id !== $journal->transaction_currency_id ? 'pc_amount' : 'amount'; + $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'; + $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. @@ -288,20 +288,20 @@ class Amount private function getLocaleInfo(): array { // get config from preference, not from translation: - $locale = Steam::getLocale(); - $array = Steam::getLocaleArray($locale); + $locale = Steam::getLocale(); + $array = Steam::getLocaleArray($locale); setlocale(LC_MONETARY, $array); - $info = localeconv(); + $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_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'); + $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); + $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); @@ -333,11 +333,11 @@ class Amount // there are five possible positions for the "+" or "-" sign (if it is even used) // pos_a and pos_e could be the ( and ) symbol. - $posA = ''; // before everything - $posB = ''; // before currency symbol - $posC = ''; // after currency symbol - $posD = ''; // before amount - $posE = ''; // after everything + $posA = ''; // before everything + $posB = ''; // before currency symbol + $posC = ''; // after currency symbol + $posD = ''; // before amount + $posE = ''; // after everything // format would be (currency before amount) // AB%sC_D%vE @@ -379,9 +379,9 @@ class Amount } if ($csPrecedes) { - return $posA.$posB.'%s'.$posC.$space.$posD.'%v'.$posE; + return $posA . $posB . '%s' . $posC . $space . $posD . '%v' . $posE; } - return $posA.$posD.'%v'.$space.$posB.'%s'.$posC.$posE; + return $posA . $posD . '%v' . $space . $posB . '%s' . $posC . $posE; } } diff --git a/app/Support/Twig/AmountFormat.php b/app/Support/Twig/AmountFormat.php index ac6efaa8b7..20402e0217 100644 --- a/app/Support/Twig/AmountFormat.php +++ b/app/Support/Twig/AmountFormat.php @@ -114,20 +114,20 @@ class AmountFormat extends AbstractExtension { return new TwigFunction( 'formatAmountBySymbol', - static function (string $amount, ?string $symbol, ?int $decimalPlaces = null, ?bool $coloured = null): string { + static function (string $amount, ?string $symbol = null, ?int $decimalPlaces = null, ?bool $coloured = null): string { if (null === $symbol) { $message = sprintf('formatAmountBySymbol("%s", %s, %d, %s) was called without a symbol. Please browse to /flush to clear your cache.', $amount, var_export($symbol, true), $decimalPlaces, var_export($coloured, true)); Log::error($message); - - throw new FireflyException($message); + $currency = Amount::getPrimaryCurrency(); + } + if (null !== $symbol) { + $decimalPlaces ??= 2; + $coloured ??= true; + $currency = new TransactionCurrency(); + $currency->symbol = $symbol; + $currency->decimal_places = $decimalPlaces; } - - $decimalPlaces ??= 2; - $coloured ??= true; - $currency = new TransactionCurrency(); - $currency->symbol = $symbol; - $currency->decimal_places = $decimalPlaces; return Amount::formatAnything($currency, $amount, $coloured); }, diff --git a/resources/views/transactions/show.twig b/resources/views/transactions/show.twig index 9202da88dd..1b9463fc83 100644 --- a/resources/views/transactions/show.twig +++ b/resources/views/transactions/show.twig @@ -203,7 +203,7 @@ {% set boxSize = 4 %} {% endif %}
- {% for index,journal in selectedGroup.transactions %} + {% for index, journal in selectedGroup.transactions %}
@@ -440,7 +440,7 @@ {{ 'tags'|_ }} {% for tag in journal.tags %} - {% if null != tag.id %} + {% if null != tag.id and '' != tag.id %}

{{ tag.tag }}

{% endif %} {% endfor %} From 53335077ff619d65cececfffcaf40f11353e34ae Mon Sep 17 00:00:00 2001 From: JC5 Date: Mon, 15 Sep 2025 19:25:59 +0200 Subject: [PATCH 12/58] =?UTF-8?q?=F0=9F=A4=96=20Auto=20commit=20for=20rele?= =?UTF-8?q?ase=20'develop'=20on=202025-09-15?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Transaction/ShowController.php | 48 ++--- .../TransactionGroupRepository.php | 4 +- app/Support/Amount.php | 56 ++--- app/Support/Twig/AmountFormat.php | 6 +- config/firefly.php | 2 +- package-lock.json | 194 +++++++++--------- 6 files changed, 155 insertions(+), 155 deletions(-) diff --git a/app/Http/Controllers/Transaction/ShowController.php b/app/Http/Controllers/Transaction/ShowController.php index 12627a55fe..7c2fff6105 100644 --- a/app/Http/Controllers/Transaction/ShowController.php +++ b/app/Http/Controllers/Transaction/ShowController.php @@ -85,52 +85,52 @@ class ShowController extends Controller public function show(TransactionGroup $transactionGroup) { /** @var User $admin */ - $admin = auth()->user(); + $admin = auth()->user(); // use new group collector: /** @var GroupCollectorInterface $collector */ - $collector = app(GroupCollectorInterface::class); + $collector = app(GroupCollectorInterface::class); $collector->setUser($admin)->setTransactionGroup($transactionGroup)->withAPIInformation(); /** @var null|TransactionGroup $selectedGroup */ - $selectedGroup = $collector->getGroups()->first(); + $selectedGroup = $collector->getGroups()->first(); if (null === $selectedGroup) { throw new NotFoundHttpException(); } // enrich - $enrichment = new TransactionGroupEnrichment(); + $enrichment = new TransactionGroupEnrichment(); $enrichment->setUser($admin); - $selectedGroup = $enrichment->enrichSingle($selectedGroup); + $selectedGroup = $enrichment->enrichSingle($selectedGroup); - $splits = count($selectedGroup['transactions']); - $keys = array_keys($selectedGroup['transactions']); - $first = $selectedGroup['transactions'][array_shift($keys)]; + $splits = count($selectedGroup['transactions']); + $keys = array_keys($selectedGroup['transactions']); + $first = $selectedGroup['transactions'][array_shift($keys)]; unset($keys); if (null === $first) { throw new FireflyException('This transaction is broken :(.'); } - $type = (string)trans(sprintf('firefly.%s', $first['transaction_type_type'])); - $title = 1 === $splits ? $first['description'] : $selectedGroup['title']; - $subTitle = sprintf('%s: "%s"', $type, $title); + $type = (string)trans(sprintf('firefly.%s', $first['transaction_type_type'])); + $title = 1 === $splits ? $first['description'] : $selectedGroup['title']; + $subTitle = sprintf('%s: "%s"', $type, $title); // enrich - $enrichment = new TransactionGroupEnrichment(); + $enrichment = new TransactionGroupEnrichment(); $enrichment->setUser($admin); /** @var array $selectedGroup */ - $selectedGroup = $enrichment->enrichSingle($selectedGroup); + $selectedGroup = $enrichment->enrichSingle($selectedGroup); /** @var TransactionGroupTransformer $transformer */ - $transformer = app(TransactionGroupTransformer::class); + $transformer = app(TransactionGroupTransformer::class); $transformer->setParameters(new ParameterBag()); - $groupArray = $transformer->transformObject($transactionGroup); + $groupArray = $transformer->transformObject($transactionGroup); // do some calculations: - $amounts = $this->getAmounts($selectedGroup); - $accounts = $this->getAccounts($selectedGroup); + $amounts = $this->getAmounts($selectedGroup); + $accounts = $this->getAccounts($selectedGroup); foreach (array_keys($selectedGroup['transactions']) as $index) { $selectedGroup['transactions'][$index]['tags'] = $this->repository->getTagObjects((int)$selectedGroup['transactions'][$index]['transaction_journal_id']); @@ -142,9 +142,9 @@ class ShowController extends Controller $logEntries[$journal['transaction_journal_id']] = $this->aleRepository->getForId(TransactionJournal::class, $journal['transaction_journal_id']); } - $events = $this->repository->getPiggyEvents($transactionGroup); - $attachments = $this->repository->getAttachments($transactionGroup); - $links = $this->repository->getLinks($transactionGroup); + $events = $this->repository->getPiggyEvents($transactionGroup); + $attachments = $this->repository->getAttachments($transactionGroup); + $links = $this->repository->getLinks($transactionGroup); return view('transactions.show', compact('transactionGroup', 'amounts', 'first', 'type', 'logEntries', 'groupLogEntries', 'subTitle', 'splits', 'selectedGroup', 'groupArray', 'events', 'attachments', 'links', 'accounts')); } @@ -155,7 +155,7 @@ class ShowController extends Controller foreach ($group['transactions'] as $transaction) { // add normal amount: $symbol = $transaction['currency_symbol']; - $amounts[$symbol] ??= [ + $amounts[$symbol] ??= [ 'amount' => '0', 'symbol' => $symbol, 'decimal_places' => $transaction['currency_decimal_places'], @@ -166,7 +166,7 @@ class ShowController extends Controller if (null !== $transaction['foreign_amount'] && '' !== $transaction['foreign_amount'] && 0 !== bccomp('0', (string)$transaction['foreign_amount'])) { // same for foreign currency: $foreignSymbol = $transaction['foreign_currency_symbol']; - $amounts[$foreignSymbol] ??= [ + $amounts[$foreignSymbol] ??= [ 'amount' => '0', 'symbol' => $foreignSymbol, 'decimal_places' => $transaction['foreign_currency_decimal_places'], @@ -177,7 +177,7 @@ class ShowController extends Controller if (null !== $transaction['pc_amount'] && $transaction['currency_id'] !== $this->primaryCurrency->id) { // same for foreign currency: $primarySymbol = $this->primaryCurrency->symbol; - $amounts[$primarySymbol] ??= [ + $amounts[$primarySymbol] ??= [ 'amount' => '0', 'symbol' => $this->primaryCurrency->symbol, 'decimal_places' => $this->primaryCurrency->decimal_places, @@ -192,7 +192,7 @@ class ShowController extends Controller private function getAccounts(array $group): array { - $accounts = [ + $accounts = [ 'source' => [], 'destination' => [], ]; diff --git a/app/Repositories/TransactionGroup/TransactionGroupRepository.php b/app/Repositories/TransactionGroup/TransactionGroupRepository.php index e658b8756d..c8c03b7a38 100644 --- a/app/Repositories/TransactionGroup/TransactionGroupRepository.php +++ b/app/Repositories/TransactionGroup/TransactionGroupRepository.php @@ -370,9 +370,9 @@ class TransactionGroupRepository implements TransactionGroupRepositoryInterface, public function getTagObjects(int $journalId): Collection { - /** @var TransactionJournal|null $journal */ + /** @var null|TransactionJournal $journal */ $journal = $this->user->transactionJournals()->find($journalId); - if(null ===$journal) { + if (null === $journal) { return new Collection(); } diff --git a/app/Support/Amount.php b/app/Support/Amount.php index 53c067e015..c3d82f0caf 100644 --- a/app/Support/Amount.php +++ b/app/Support/Amount.php @@ -60,15 +60,15 @@ class Amount */ public function formatFlat(string $symbol, int $decimalPlaces, string $amount, ?bool $coloured = null): string { - $locale = Steam::getLocale(); - $rounded = Steam::bcround($amount, $decimalPlaces); + $locale = Steam::getLocale(); + $rounded = Steam::bcround($amount, $decimalPlaces); $coloured ??= true; - $fmt = new NumberFormatter($locale, NumberFormatter::CURRENCY); + $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 + $result = (string)$fmt->format((float)$rounded); // intentional float if (true === $coloured) { if (1 === bccomp($rounded, '0')) { @@ -122,7 +122,7 @@ class Amount $key = sprintf('transaction_currency_%d', $currencyId); /** @var null|TransactionCurrency $pref */ - $pref = $instance->getPreference($key); + $pref = $instance->getPreference($key); if (null !== $pref) { return $pref; } @@ -144,7 +144,7 @@ class Amount $key = sprintf('transaction_currency_%s', $code); /** @var null|TransactionCurrency $pref */ - $pref = $instance->getPreference($key); + $pref = $instance->getPreference($key); if (null !== $pref) { return $pref; } @@ -174,8 +174,8 @@ class Amount return $pref; } - $key = sprintf('convert_to_primary_%d', $user->id); - $pref = $instance->getPreference($key); + $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); @@ -201,7 +201,7 @@ class Amount public function getPrimaryCurrencyByUserGroup(UserGroup $userGroup): TransactionCurrency { - $cache = new CacheProperties(); + $cache = new CacheProperties(); $cache->addProperty('getPrimaryCurrencyByGroup'); $cache->addProperty($userGroup->id); if ($cache->has()) { @@ -231,16 +231,16 @@ class Amount */ public function getAmountFromJournalObject(TransactionJournal $journal): string { - $convertToPrimary = $this->convertToPrimary(); - $currency = $this->getPrimaryCurrency(); - $field = $convertToPrimary && $currency->id !== $journal->transaction_currency_id ? 'pc_amount' : 'amount'; + $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'; + $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. @@ -288,20 +288,20 @@ class Amount private function getLocaleInfo(): array { // get config from preference, not from translation: - $locale = Steam::getLocale(); - $array = Steam::getLocaleArray($locale); + $locale = Steam::getLocale(); + $array = Steam::getLocaleArray($locale); setlocale(LC_MONETARY, $array); - $info = localeconv(); + $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_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'); + $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); + $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); @@ -333,11 +333,11 @@ class Amount // there are five possible positions for the "+" or "-" sign (if it is even used) // pos_a and pos_e could be the ( and ) symbol. - $posA = ''; // before everything - $posB = ''; // before currency symbol - $posC = ''; // after currency symbol - $posD = ''; // before amount - $posE = ''; // after everything + $posA = ''; // before everything + $posB = ''; // before currency symbol + $posC = ''; // after currency symbol + $posD = ''; // before amount + $posE = ''; // after everything // format would be (currency before amount) // AB%sC_D%vE @@ -379,9 +379,9 @@ class Amount } if ($csPrecedes) { - return $posA . $posB . '%s' . $posC . $space . $posD . '%v' . $posE; + return $posA.$posB.'%s'.$posC.$space.$posD.'%v'.$posE; } - return $posA . $posD . '%v' . $space . $posB . '%s' . $posC . $posE; + return $posA.$posD.'%v'.$space.$posB.'%s'.$posC.$posE; } } diff --git a/app/Support/Twig/AmountFormat.php b/app/Support/Twig/AmountFormat.php index 20402e0217..39cc1594db 100644 --- a/app/Support/Twig/AmountFormat.php +++ b/app/Support/Twig/AmountFormat.php @@ -117,13 +117,13 @@ class AmountFormat extends AbstractExtension static function (string $amount, ?string $symbol = null, ?int $decimalPlaces = null, ?bool $coloured = null): string { if (null === $symbol) { - $message = sprintf('formatAmountBySymbol("%s", %s, %d, %s) was called without a symbol. Please browse to /flush to clear your cache.', $amount, var_export($symbol, true), $decimalPlaces, var_export($coloured, true)); + $message = sprintf('formatAmountBySymbol("%s", %s, %d, %s) was called without a symbol. Please browse to /flush to clear your cache.', $amount, var_export($symbol, true), $decimalPlaces, var_export($coloured, true)); Log::error($message); $currency = Amount::getPrimaryCurrency(); } if (null !== $symbol) { - $decimalPlaces ??= 2; - $coloured ??= true; + $decimalPlaces ??= 2; + $coloured ??= true; $currency = new TransactionCurrency(); $currency->symbol = $symbol; $currency->decimal_places = $decimalPlaces; diff --git a/config/firefly.php b/config/firefly.php index 9ebbd995ed..7fa9eff12a 100644 --- a/config/firefly.php +++ b/config/firefly.php @@ -79,7 +79,7 @@ return [ // see cer.php for exchange rates feature flag. ], 'version' => 'develop/2025-09-15', - 'build_time' => 1757906521, + 'build_time' => 1757957039, 'api_version' => '2.1.0', // field is no longer used. 'db_version' => 26, diff --git a/package-lock.json b/package-lock.json index 46e4e426d6..b8d603f07b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2589,9 +2589,9 @@ } }, "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.50.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.50.1.tgz", - "integrity": "sha512-HJXwzoZN4eYTdD8bVV22DN8gsPCAj3V20NHKOs8ezfXanGpmVPR7kalUHd+Y31IJp9stdB87VKPFbsGY3H/2ag==", + "version": "4.50.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.50.2.tgz", + "integrity": "sha512-uLN8NAiFVIRKX9ZQha8wy6UUs06UNSZ32xj6giK/rmMXAgKahwExvK6SsmgU5/brh4w/nSgj8e0k3c1HBQpa0A==", "cpu": [ "arm" ], @@ -2603,9 +2603,9 @@ ] }, "node_modules/@rollup/rollup-android-arm64": { - "version": "4.50.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.50.1.tgz", - "integrity": "sha512-PZlsJVcjHfcH53mOImyt3bc97Ep3FJDXRpk9sMdGX0qgLmY0EIWxCag6EigerGhLVuL8lDVYNnSo8qnTElO4xw==", + "version": "4.50.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.50.2.tgz", + "integrity": "sha512-oEouqQk2/zxxj22PNcGSskya+3kV0ZKH+nQxuCCOGJ4oTXBdNTbv+f/E3c74cNLeMO1S5wVWacSws10TTSB77g==", "cpu": [ "arm64" ], @@ -2617,9 +2617,9 @@ ] }, "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.50.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.50.1.tgz", - "integrity": "sha512-xc6i2AuWh++oGi4ylOFPmzJOEeAa2lJeGUGb4MudOtgfyyjr4UPNK+eEWTPLvmPJIY/pgw6ssFIox23SyrkkJw==", + "version": "4.50.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.50.2.tgz", + "integrity": "sha512-OZuTVTpj3CDSIxmPgGH8en/XtirV5nfljHZ3wrNwvgkT5DQLhIKAeuFSiwtbMto6oVexV0k1F1zqURPKf5rI1Q==", "cpu": [ "arm64" ], @@ -2631,9 +2631,9 @@ ] }, "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.50.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.50.1.tgz", - "integrity": "sha512-2ofU89lEpDYhdLAbRdeyz/kX3Y2lpYc6ShRnDjY35bZhd2ipuDMDi6ZTQ9NIag94K28nFMofdnKeHR7BT0CATw==", + "version": "4.50.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.50.2.tgz", + "integrity": "sha512-Wa/Wn8RFkIkr1vy1k1PB//VYhLnlnn5eaJkfTQKivirOvzu5uVd2It01ukeQstMursuz7S1bU+8WW+1UPXpa8A==", "cpu": [ "x64" ], @@ -2645,9 +2645,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.50.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.50.1.tgz", - "integrity": "sha512-wOsE6H2u6PxsHY/BeFHA4VGQN3KUJFZp7QJBmDYI983fgxq5Th8FDkVuERb2l9vDMs1D5XhOrhBrnqcEY6l8ZA==", + "version": "4.50.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.50.2.tgz", + "integrity": "sha512-QkzxvH3kYN9J1w7D1A+yIMdI1pPekD+pWx7G5rXgnIlQ1TVYVC6hLl7SOV9pi5q9uIDF9AuIGkuzcbF7+fAhow==", "cpu": [ "arm64" ], @@ -2659,9 +2659,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.50.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.50.1.tgz", - "integrity": "sha512-A/xeqaHTlKbQggxCqispFAcNjycpUEHP52mwMQZUNqDUJFFYtPHCXS1VAG29uMlDzIVr+i00tSFWFLivMcoIBQ==", + "version": "4.50.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.50.2.tgz", + "integrity": "sha512-dkYXB0c2XAS3a3jmyDkX4Jk0m7gWLFzq1C3qUnJJ38AyxIF5G/dyS4N9B30nvFseCfgtCEdbYFhk0ChoCGxPog==", "cpu": [ "x64" ], @@ -2673,9 +2673,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.50.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.50.1.tgz", - "integrity": "sha512-54v4okehwl5TaSIkpp97rAHGp7t3ghinRd/vyC1iXqXMfjYUTm7TfYmCzXDoHUPTTf36L8pr0E7YsD3CfB3ZDg==", + "version": "4.50.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.50.2.tgz", + "integrity": "sha512-9VlPY/BN3AgbukfVHAB8zNFWB/lKEuvzRo1NKev0Po8sYFKx0i+AQlCYftgEjcL43F2h9Ui1ZSdVBc4En/sP2w==", "cpu": [ "arm" ], @@ -2687,9 +2687,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.50.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.50.1.tgz", - "integrity": "sha512-p/LaFyajPN/0PUHjv8TNyxLiA7RwmDoVY3flXHPSzqrGcIp/c2FjwPPP5++u87DGHtw+5kSH5bCJz0mvXngYxw==", + "version": "4.50.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.50.2.tgz", + "integrity": "sha512-+GdKWOvsifaYNlIVf07QYan1J5F141+vGm5/Y8b9uCZnG/nxoGqgCmR24mv0koIWWuqvFYnbURRqw1lv7IBINw==", "cpu": [ "arm" ], @@ -2701,9 +2701,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.50.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.50.1.tgz", - "integrity": "sha512-2AbMhFFkTo6Ptna1zO7kAXXDLi7H9fGTbVaIq2AAYO7yzcAsuTNWPHhb2aTA6GPiP+JXh85Y8CiS54iZoj4opw==", + "version": "4.50.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.50.2.tgz", + "integrity": "sha512-df0Eou14ojtUdLQdPFnymEQteENwSJAdLf5KCDrmZNsy1c3YaCNaJvYsEUHnrg+/DLBH612/R0xd3dD03uz2dg==", "cpu": [ "arm64" ], @@ -2715,9 +2715,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.50.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.50.1.tgz", - "integrity": "sha512-Cgef+5aZwuvesQNw9eX7g19FfKX5/pQRIyhoXLCiBOrWopjo7ycfB292TX9MDcDijiuIJlx1IzJz3IoCPfqs9w==", + "version": "4.50.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.50.2.tgz", + "integrity": "sha512-iPeouV0UIDtz8j1YFR4OJ/zf7evjauqv7jQ/EFs0ClIyL+by++hiaDAfFipjOgyz6y6xbDvJuiU4HwpVMpRFDQ==", "cpu": [ "arm64" ], @@ -2728,10 +2728,10 @@ "linux" ] }, - "node_modules/@rollup/rollup-linux-loongarch64-gnu": { - "version": "4.50.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.50.1.tgz", - "integrity": "sha512-RPhTwWMzpYYrHrJAS7CmpdtHNKtt2Ueo+BlLBjfZEhYBhK00OsEqM08/7f+eohiF6poe0YRDDd8nAvwtE/Y62Q==", + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.50.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.50.2.tgz", + "integrity": "sha512-OL6KaNvBopLlj5fTa5D5bau4W82f+1TyTZRr2BdnfsrnQnmdxh4okMxR2DcDkJuh4KeoQZVuvHvzuD/lyLn2Kw==", "cpu": [ "loong64" ], @@ -2743,9 +2743,9 @@ ] }, "node_modules/@rollup/rollup-linux-ppc64-gnu": { - "version": "4.50.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.50.1.tgz", - "integrity": "sha512-eSGMVQw9iekut62O7eBdbiccRguuDgiPMsw++BVUg+1K7WjZXHOg/YOT9SWMzPZA+w98G+Fa1VqJgHZOHHnY0Q==", + "version": "4.50.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.50.2.tgz", + "integrity": "sha512-I21VJl1w6z/K5OTRl6aS9DDsqezEZ/yKpbqlvfHbW0CEF5IL8ATBMuUx6/mp683rKTK8thjs/0BaNrZLXetLag==", "cpu": [ "ppc64" ], @@ -2757,9 +2757,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.50.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.50.1.tgz", - "integrity": "sha512-S208ojx8a4ciIPrLgazF6AgdcNJzQE4+S9rsmOmDJkusvctii+ZvEuIC4v/xFqzbuP8yDjn73oBlNDgF6YGSXQ==", + "version": "4.50.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.50.2.tgz", + "integrity": "sha512-Hq6aQJT/qFFHrYMjS20nV+9SKrXL2lvFBENZoKfoTH2kKDOJqff5OSJr4x72ZaG/uUn+XmBnGhfr4lwMRrmqCQ==", "cpu": [ "riscv64" ], @@ -2771,9 +2771,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-musl": { - "version": "4.50.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.50.1.tgz", - "integrity": "sha512-3Ag8Ls1ggqkGUvSZWYcdgFwriy2lWo+0QlYgEFra/5JGtAd6C5Hw59oojx1DeqcA2Wds2ayRgvJ4qxVTzCHgzg==", + "version": "4.50.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.50.2.tgz", + "integrity": "sha512-82rBSEXRv5qtKyr0xZ/YMF531oj2AIpLZkeNYxmKNN6I2sVE9PGegN99tYDLK2fYHJITL1P2Lgb4ZXnv0PjQvw==", "cpu": [ "riscv64" ], @@ -2785,9 +2785,9 @@ ] }, "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.50.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.50.1.tgz", - "integrity": "sha512-t9YrKfaxCYe7l7ldFERE1BRg/4TATxIg+YieHQ966jwvo7ddHJxPj9cNFWLAzhkVsbBvNA4qTbPVNsZKBO4NSg==", + "version": "4.50.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.50.2.tgz", + "integrity": "sha512-4Q3S3Hy7pC6uaRo9gtXUTJ+EKo9AKs3BXKc2jYypEcMQ49gDPFU2P1ariX9SEtBzE5egIX6fSUmbmGazwBVF9w==", "cpu": [ "s390x" ], @@ -2799,9 +2799,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.50.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.50.1.tgz", - "integrity": "sha512-MCgtFB2+SVNuQmmjHf+wfI4CMxy3Tk8XjA5Z//A0AKD7QXUYFMQcns91K6dEHBvZPCnhJSyDWLApk40Iq/H3tA==", + "version": "4.50.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.50.2.tgz", + "integrity": "sha512-9Jie/At6qk70dNIcopcL4p+1UirusEtznpNtcq/u/C5cC4HBX7qSGsYIcG6bdxj15EYWhHiu02YvmdPzylIZlA==", "cpu": [ "x64" ], @@ -2813,9 +2813,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.50.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.50.1.tgz", - "integrity": "sha512-nEvqG+0jeRmqaUMuwzlfMKwcIVffy/9KGbAGyoa26iu6eSngAYQ512bMXuqqPrlTyfqdlB9FVINs93j534UJrg==", + "version": "4.50.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.50.2.tgz", + "integrity": "sha512-HPNJwxPL3EmhzeAnsWQCM3DcoqOz3/IC6de9rWfGR8ZCuEHETi9km66bH/wG3YH0V3nyzyFEGUZeL5PKyy4xvw==", "cpu": [ "x64" ], @@ -2827,9 +2827,9 @@ ] }, "node_modules/@rollup/rollup-openharmony-arm64": { - "version": "4.50.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.50.1.tgz", - "integrity": "sha512-RDsLm+phmT3MJd9SNxA9MNuEAO/J2fhW8GXk62G/B4G7sLVumNFbRwDL6v5NrESb48k+QMqdGbHgEtfU0LCpbA==", + "version": "4.50.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.50.2.tgz", + "integrity": "sha512-nMKvq6FRHSzYfKLHZ+cChowlEkR2lj/V0jYj9JnGUVPL2/mIeFGmVM2mLaFeNa5Jev7W7TovXqXIG2d39y1KYA==", "cpu": [ "arm64" ], @@ -2841,9 +2841,9 @@ ] }, "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.50.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.50.1.tgz", - "integrity": "sha512-hpZB/TImk2FlAFAIsoElM3tLzq57uxnGYwplg6WDyAxbYczSi8O2eQ+H2Lx74504rwKtZ3N2g4bCUkiamzS6TQ==", + "version": "4.50.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.50.2.tgz", + "integrity": "sha512-eFUvvnTYEKeTyHEijQKz81bLrUQOXKZqECeiWH6tb8eXXbZk+CXSG2aFrig2BQ/pjiVRj36zysjgILkqarS2YA==", "cpu": [ "arm64" ], @@ -2855,9 +2855,9 @@ ] }, "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.50.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.50.1.tgz", - "integrity": "sha512-SXjv8JlbzKM0fTJidX4eVsH+Wmnp0/WcD8gJxIZyR6Gay5Qcsmdbi9zVtnbkGPG8v2vMR1AD06lGWy5FLMcG7A==", + "version": "4.50.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.50.2.tgz", + "integrity": "sha512-cBaWmXqyfRhH8zmUxK3d3sAhEWLrtMjWBRwdMMHJIXSjvjLKvv49adxiEz+FJ8AP90apSDDBx2Tyd/WylV6ikA==", "cpu": [ "ia32" ], @@ -2869,9 +2869,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.50.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.50.1.tgz", - "integrity": "sha512-StxAO/8ts62KZVRAm4JZYq9+NqNsV7RvimNK+YM7ry//zebEH6meuugqW/P5OFUCjyQgui+9fUxT6d5NShvMvA==", + "version": "4.50.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.50.2.tgz", + "integrity": "sha512-APwKy6YUhvZaEoHyM+9xqmTpviEI+9eL7LoCH+aLcvWYHJ663qG5zx7WzWZY+a9qkg5JtzcMyJ9z0WtQBMDmgA==", "cpu": [ "x64" ], @@ -4061,9 +4061,9 @@ "license": "MIT" }, "node_modules/baseline-browser-mapping": { - "version": "2.8.3", - "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.3.tgz", - "integrity": "sha512-mcE+Wr2CAhHNWxXN/DdTI+n4gsPc5QpXpWnyCQWiQYIYZX+ZMJ8juXZgjRa/0/YPJo/NSsgW15/YgmI4nbysYw==", + "version": "2.8.4", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.4.tgz", + "integrity": "sha512-L+YvJwGAgwJBV1p6ffpSTa2KRc69EeeYGYjRVWKs0GKrK+LON0GC0gV+rKSNtALEDvMDqkvCFq9r1r94/Gjwxw==", "dev": true, "license": "Apache-2.0", "bin": { @@ -5819,9 +5819,9 @@ } }, "node_modules/error-ex": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", - "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz", + "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==", "dev": true, "license": "MIT", "dependencies": { @@ -10145,9 +10145,9 @@ } }, "node_modules/rollup": { - "version": "4.50.1", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.50.1.tgz", - "integrity": "sha512-78E9voJHwnXQMiQdiqswVLZwJIzdBKJ1GdI5Zx6XwoFKUIk09/sSrr+05QFzvYb8q6Y9pPV45zzDuYa3907TZA==", + "version": "4.50.2", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.50.2.tgz", + "integrity": "sha512-BgLRGy7tNS9H66aIMASq1qSYbAAJV6Z6WR4QYTvj5FgF15rZ/ympT1uixHXwzbZUBDbkvqUI1KR0fH1FhMaQ9w==", "dev": true, "license": "MIT", "dependencies": { @@ -10161,27 +10161,27 @@ "npm": ">=8.0.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.50.1", - "@rollup/rollup-android-arm64": "4.50.1", - "@rollup/rollup-darwin-arm64": "4.50.1", - "@rollup/rollup-darwin-x64": "4.50.1", - "@rollup/rollup-freebsd-arm64": "4.50.1", - "@rollup/rollup-freebsd-x64": "4.50.1", - "@rollup/rollup-linux-arm-gnueabihf": "4.50.1", - "@rollup/rollup-linux-arm-musleabihf": "4.50.1", - "@rollup/rollup-linux-arm64-gnu": "4.50.1", - "@rollup/rollup-linux-arm64-musl": "4.50.1", - "@rollup/rollup-linux-loongarch64-gnu": "4.50.1", - "@rollup/rollup-linux-ppc64-gnu": "4.50.1", - "@rollup/rollup-linux-riscv64-gnu": "4.50.1", - "@rollup/rollup-linux-riscv64-musl": "4.50.1", - "@rollup/rollup-linux-s390x-gnu": "4.50.1", - "@rollup/rollup-linux-x64-gnu": "4.50.1", - "@rollup/rollup-linux-x64-musl": "4.50.1", - "@rollup/rollup-openharmony-arm64": "4.50.1", - "@rollup/rollup-win32-arm64-msvc": "4.50.1", - "@rollup/rollup-win32-ia32-msvc": "4.50.1", - "@rollup/rollup-win32-x64-msvc": "4.50.1", + "@rollup/rollup-android-arm-eabi": "4.50.2", + "@rollup/rollup-android-arm64": "4.50.2", + "@rollup/rollup-darwin-arm64": "4.50.2", + "@rollup/rollup-darwin-x64": "4.50.2", + "@rollup/rollup-freebsd-arm64": "4.50.2", + "@rollup/rollup-freebsd-x64": "4.50.2", + "@rollup/rollup-linux-arm-gnueabihf": "4.50.2", + "@rollup/rollup-linux-arm-musleabihf": "4.50.2", + "@rollup/rollup-linux-arm64-gnu": "4.50.2", + "@rollup/rollup-linux-arm64-musl": "4.50.2", + "@rollup/rollup-linux-loong64-gnu": "4.50.2", + "@rollup/rollup-linux-ppc64-gnu": "4.50.2", + "@rollup/rollup-linux-riscv64-gnu": "4.50.2", + "@rollup/rollup-linux-riscv64-musl": "4.50.2", + "@rollup/rollup-linux-s390x-gnu": "4.50.2", + "@rollup/rollup-linux-x64-gnu": "4.50.2", + "@rollup/rollup-linux-x64-musl": "4.50.2", + "@rollup/rollup-openharmony-arm64": "4.50.2", + "@rollup/rollup-win32-arm64-msvc": "4.50.2", + "@rollup/rollup-win32-ia32-msvc": "4.50.2", + "@rollup/rollup-win32-x64-msvc": "4.50.2", "fsevents": "~2.3.2" } }, @@ -11398,9 +11398,9 @@ } }, "node_modules/unicode-property-aliases-ecmascript": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.1.0.tgz", - "integrity": "sha512-6t3foTQI9qne+OZoVQB/8x8rk2k1eVy1gRXhV3oFQ5T6R1dqQ1xtin3XqSlx3+ATBkliTaR/hHyJBm+LVPNM8w==", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.2.0.tgz", + "integrity": "sha512-hpbDzxUY9BFwX+UeBnxv3Sh1q7HFxj48DTmXchNgRa46lO8uj3/1iEn3MiNUYTg1g9ctIqXCCERn8gYZhHC5lQ==", "dev": true, "license": "MIT", "engines": { From b653d63d3d445276f0f48bceeec273fd63e148f5 Mon Sep 17 00:00:00 2001 From: James Cole Date: Tue, 16 Sep 2025 20:44:54 +0200 Subject: [PATCH 13/58] Add new correction command, will probably fix #10833 --- .../Commands/Correction/CorrectsDatabase.php | 3 +- .../RemovesLinksToDeletedObjects.php | 97 +++++++++++++++++++ .../2024_11_30_075826_multi_piggy.php | 2 +- 3 files changed, 100 insertions(+), 2 deletions(-) create mode 100644 app/Console/Commands/Correction/RemovesLinksToDeletedObjects.php diff --git a/app/Console/Commands/Correction/CorrectsDatabase.php b/app/Console/Commands/Correction/CorrectsDatabase.php index 5850619f61..4288a4af40 100644 --- a/app/Console/Commands/Correction/CorrectsDatabase.php +++ b/app/Console/Commands/Correction/CorrectsDatabase.php @@ -75,7 +75,8 @@ class CorrectsDatabase extends Command 'correction:recalculates-liabilities', 'correction:preferences', // 'correction:transaction-types', // resource heavy, disabled. - 'correction:recalculate-pc-amounts', // not necessary, disabled. + 'correction:recalculate-pc-amounts', + 'correction:remove-links-to-deleted-objects', 'firefly-iii:report-integrity', ]; foreach ($commands as $command) { diff --git a/app/Console/Commands/Correction/RemovesLinksToDeletedObjects.php b/app/Console/Commands/Correction/RemovesLinksToDeletedObjects.php new file mode 100644 index 0000000000..56f0c737c9 --- /dev/null +++ b/app/Console/Commands/Correction/RemovesLinksToDeletedObjects.php @@ -0,0 +1,97 @@ +. + */ + +namespace FireflyIII\Console\Commands\Correction; + +use FireflyIII\Console\Commands\ShowsFriendlyMessages; +use FireflyIII\Models\Budget; +use FireflyIII\Models\Category; +use FireflyIII\Models\TransactionJournal; +use Illuminate\Console\Command; +use Illuminate\Support\Facades\DB; + +class RemovesLinksToDeletedObjects extends Command +{ + use ShowsFriendlyMessages; + + /** + * The name and signature of the console command. + * + * @var string + */ + protected $signature = 'correction:remove-links-to-deleted-objects'; + + /** + * The console command description. + * + * @var string + */ + protected $description = 'Removes deleted entries from intermediate tables.'; + + /** + * Execute the console command. + */ + public function handle() + { + // accounts + // remove soft-deleted accounts from account_balances + // remove soft-deleted accounts from account_meta + // remove soft-deleted accounts from account_piggy_bank + // remove soft-deleted accounts from attachments. + + // journals + // remove soft-deleted journals from attachments + // audit_log_entries + $deleted = TransactionJournal::withTrashed()->whereNotNull('deleted_at')->get('transaction_journals.id')->pluck('id')->toArray(); + $count = DB::table('tag_transaction_journal')->whereIn('transaction_journal_id', $deleted)->delete(); + if ($count > 0) { + $this->friendlyInfo(sprintf('Removed %d old relationship(s) between tags and transactions.', $count)); + } + unset($deleted); + // budgets + // from auto_budgets + // from budget_limits + $deleted = Budget::withTrashed()->whereNotNull('deleted_at')->get('budgets.id')->pluck('id')->toArray(); + $count = DB::table('budget_transaction')->whereIn('budget_id', $deleted)->delete(); + if ($count > 0) { + $this->friendlyInfo(sprintf('Removed %d old relationship(s) between budgets and transactions.', $count)); + } + $count = DB::table('budget_transaction_journal')->whereIn('budget_id', $deleted)->delete(); + if ($count > 0) { + $this->friendlyInfo(sprintf('Removed %d old relationship(s) between budgets and transactions.', $count)); + } + unset($deleted); + // -> category_transaction + // -> category_transaction_journal + $deleted = Category::withTrashed()->whereNotNull('deleted_at')->get('categories.id')->pluck('id')->toArray(); + $count = DB::table('category_transaction')->whereIn('category_id', $deleted)->delete(); + if ($count > 0) { + $this->friendlyInfo(sprintf('Removed %d old relationship(s) between categories and transactions.', $count)); + } + $count = DB::table('category_transaction_journal')->whereIn('category_id', $deleted)->delete(); + if ($count > 0) { + $this->friendlyInfo(sprintf('Removed %d old relationship(s) categories budgets and transactions.', $count)); + } + $this->friendlyNeutral('Validated links to deleted objects.'); + + + } +} diff --git a/database/migrations/2024_11_30_075826_multi_piggy.php b/database/migrations/2024_11_30_075826_multi_piggy.php index 19442cd051..a89f29440d 100644 --- a/database/migrations/2024_11_30_075826_multi_piggy.php +++ b/database/migrations/2024_11_30_075826_multi_piggy.php @@ -140,7 +140,7 @@ return new class () extends Migration { $table->dropColumn('transaction_currency_id'); // 2. make column non-nullable. - $table->unsignedInteger('account_id')->change(); + $table->unsignedInteger('account_id')->nullable()->change(); // 5. add new index $table->foreign('account_id')->references('id')->on('accounts')->onDelete('cascade'); From 090aecb5f55646cbc9b154fdfccda548cb98e618 Mon Sep 17 00:00:00 2001 From: James Cole Date: Tue, 16 Sep 2025 20:50:25 +0200 Subject: [PATCH 14/58] Clean up command. --- .../RemovesLinksToDeletedObjects.php | 87 +++++++++++-------- 1 file changed, 53 insertions(+), 34 deletions(-) diff --git a/app/Console/Commands/Correction/RemovesLinksToDeletedObjects.php b/app/Console/Commands/Correction/RemovesLinksToDeletedObjects.php index 56f0c737c9..ea79dfa5e1 100644 --- a/app/Console/Commands/Correction/RemovesLinksToDeletedObjects.php +++ b/app/Console/Commands/Correction/RemovesLinksToDeletedObjects.php @@ -24,6 +24,7 @@ namespace FireflyIII\Console\Commands\Correction; use FireflyIII\Console\Commands\ShowsFriendlyMessages; use FireflyIII\Models\Budget; use FireflyIII\Models\Category; +use FireflyIII\Models\Tag; use FireflyIII\Models\TransactionJournal; use Illuminate\Console\Command; use Illuminate\Support\Facades\DB; @@ -51,47 +52,65 @@ class RemovesLinksToDeletedObjects extends Command */ public function handle() { - // accounts - // remove soft-deleted accounts from account_balances - // remove soft-deleted accounts from account_meta - // remove soft-deleted accounts from account_piggy_bank - // remove soft-deleted accounts from attachments. + $deletedTags = Tag::withTrashed()->whereNotNull('deleted_at')->get('tags.id')->pluck('id')->toArray(); + $deletedJournals = TransactionJournal::withTrashed()->whereNotNull('deleted_at')->get('transaction_journals.id')->pluck('id')->toArray(); + $deletedBudgets = Budget::withTrashed()->whereNotNull('deleted_at')->get('budgets.id')->pluck('id')->toArray(); + $deletedCategories = Category::withTrashed()->whereNotNull('deleted_at')->get('categories.id')->pluck('id')->toArray(); - // journals - // remove soft-deleted journals from attachments - // audit_log_entries - $deleted = TransactionJournal::withTrashed()->whereNotNull('deleted_at')->get('transaction_journals.id')->pluck('id')->toArray(); - $count = DB::table('tag_transaction_journal')->whereIn('transaction_journal_id', $deleted)->delete(); - if ($count > 0) { - $this->friendlyInfo(sprintf('Removed %d old relationship(s) between tags and transactions.', $count)); + if (count($deletedTags) > 0) { + $this->cleanupTags($deletedTags); } - unset($deleted); - // budgets - // from auto_budgets - // from budget_limits - $deleted = Budget::withTrashed()->whereNotNull('deleted_at')->get('budgets.id')->pluck('id')->toArray(); - $count = DB::table('budget_transaction')->whereIn('budget_id', $deleted)->delete(); - if ($count > 0) { - $this->friendlyInfo(sprintf('Removed %d old relationship(s) between budgets and transactions.', $count)); + if (count($deletedJournals) > 0) { + $this->cleanupJournals($deletedJournals); } - $count = DB::table('budget_transaction_journal')->whereIn('budget_id', $deleted)->delete(); - if ($count > 0) { - $this->friendlyInfo(sprintf('Removed %d old relationship(s) between budgets and transactions.', $count)); + if (count($deletedBudgets) > 0) { + $this->cleanupBudgets($deletedBudgets); } - unset($deleted); - // -> category_transaction - // -> category_transaction_journal - $deleted = Category::withTrashed()->whereNotNull('deleted_at')->get('categories.id')->pluck('id')->toArray(); - $count = DB::table('category_transaction')->whereIn('category_id', $deleted)->delete(); - if ($count > 0) { - $this->friendlyInfo(sprintf('Removed %d old relationship(s) between categories and transactions.', $count)); - } - $count = DB::table('category_transaction_journal')->whereIn('category_id', $deleted)->delete(); - if ($count > 0) { - $this->friendlyInfo(sprintf('Removed %d old relationship(s) categories budgets and transactions.', $count)); + if (count($deletedCategories) > 0) { + $this->cleanupCategories($deletedCategories); } $this->friendlyNeutral('Validated links to deleted objects.'); } + + private function cleanupTags(array $tags): void + { + $count = DB::table('tag_transaction_journal')->whereIn('tag_id', $tags)->delete(); + if ($count > 0) { + $this->friendlyInfo(sprintf('Removed %d old relationship(s) categories transactions and tags.', $count)); + } + } + + private function cleanupJournals(array $journals): void + { + $count = DB::table('tag_transaction_journal')->whereIn('transaction_journal_id', $journals)->delete(); + if ($count > 0) { + $this->friendlyInfo(sprintf('Removed %d old relationship(s) between tags and transactions.', $count)); + } + $count = DB::table('budget_transaction_journal')->whereIn('transaction_journal_id', $journals)->delete(); + if ($count > 0) { + $this->friendlyInfo(sprintf('Removed %d old relationship(s) between budgets and transactions.', $count)); + } + $count = DB::table('category_transaction_journal')->whereIn('transaction_journal_id', $journals)->delete(); + if ($count > 0) { + $this->friendlyInfo(sprintf('Removed %d old relationship(s) categories and transactions.', $count)); + } + } + + private function cleanupBudgets(array $budgets): void + { + $count = DB::table('budget_transaction_journal')->whereIn('budget_id', $budgets)->delete(); + if ($count > 0) { + $this->friendlyInfo(sprintf('Removed %d old relationship(s) between budgets and transactions.', $count)); + } + } + + private function cleanupCategories(array $categories): void + { + $count = DB::table('category_transaction_journal')->whereIn('category_id', $categories)->delete(); + if ($count > 0) { + $this->friendlyInfo(sprintf('Removed %d old relationship(s) categories categories and transactions.', $count)); + } + } } From 6a7c35e7bcf40963dcc93d769f0128386a4f145b Mon Sep 17 00:00:00 2001 From: JC5 Date: Tue, 16 Sep 2025 20:55:27 +0200 Subject: [PATCH 15/58] =?UTF-8?q?=F0=9F=A4=96=20Auto=20commit=20for=20rele?= =?UTF-8?q?ase=20'develop'=20on=202025-09-16?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../RemovesLinksToDeletedObjects.php | 6 +- composer.lock | 106 +++++++++++++++--- config/firefly.php | 4 +- package-lock.json | 28 ++--- resources/assets/v1/src/locales/it.json | 4 +- 5 files changed, 111 insertions(+), 37 deletions(-) diff --git a/app/Console/Commands/Correction/RemovesLinksToDeletedObjects.php b/app/Console/Commands/Correction/RemovesLinksToDeletedObjects.php index ea79dfa5e1..7b174418b0 100644 --- a/app/Console/Commands/Correction/RemovesLinksToDeletedObjects.php +++ b/app/Console/Commands/Correction/RemovesLinksToDeletedObjects.php @@ -1,4 +1,6 @@ whereNotNull('deleted_at')->get('tags.id')->pluck('id')->toArray(); $deletedJournals = TransactionJournal::withTrashed()->whereNotNull('deleted_at')->get('transaction_journals.id')->pluck('id')->toArray(); diff --git a/composer.lock b/composer.lock index 3bc0182e4b..ac2158c967 100644 --- a/composer.lock +++ b/composer.lock @@ -324,16 +324,16 @@ }, { "name": "dasprid/enum", - "version": "1.0.6", + "version": "1.0.7", "source": { "type": "git", "url": "https://github.com/DASPRiD/Enum.git", - "reference": "8dfd07c6d2cf31c8da90c53b83c026c7696dda90" + "reference": "b5874fa9ed0043116c72162ec7f4fb50e02e7cce" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/DASPRiD/Enum/zipball/8dfd07c6d2cf31c8da90c53b83c026c7696dda90", - "reference": "8dfd07c6d2cf31c8da90c53b83c026c7696dda90", + "url": "https://api.github.com/repos/DASPRiD/Enum/zipball/b5874fa9ed0043116c72162ec7f4fb50e02e7cce", + "reference": "b5874fa9ed0043116c72162ec7f4fb50e02e7cce", "shasum": "" }, "require": { @@ -368,9 +368,9 @@ ], "support": { "issues": "https://github.com/DASPRiD/Enum/issues", - "source": "https://github.com/DASPRiD/Enum/tree/1.0.6" + "source": "https://github.com/DASPRiD/Enum/tree/1.0.7" }, - "time": "2024-08-09T14:30:48+00:00" + "time": "2025-09-16T12:23:56+00:00" }, { "name": "defuse/php-encryption", @@ -1878,16 +1878,16 @@ }, { "name": "laravel/framework", - "version": "v12.28.1", + "version": "v12.29.0", "source": { "type": "git", "url": "https://github.com/laravel/framework.git", - "reference": "868c1f2d3dba4df6d21e3a8d818479f094cfd942" + "reference": "a9e4c73086f5ba38383e9c1d74b84fe46aac730b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/framework/zipball/868c1f2d3dba4df6d21e3a8d818479f094cfd942", - "reference": "868c1f2d3dba4df6d21e3a8d818479f094cfd942", + "url": "https://api.github.com/repos/laravel/framework/zipball/a9e4c73086f5ba38383e9c1d74b84fe46aac730b", + "reference": "a9e4c73086f5ba38383e9c1d74b84fe46aac730b", "shasum": "" }, "require": { @@ -1915,6 +1915,7 @@ "monolog/monolog": "^3.0", "nesbot/carbon": "^3.8.4", "nunomaduro/termwind": "^2.0", + "phiki/phiki": "v2.0.0", "php": "^8.2", "psr/container": "^1.1.1|^2.0.1", "psr/log": "^1.0|^2.0|^3.0", @@ -2024,7 +2025,7 @@ "ext-pdo": "Required to use all database features.", "ext-posix": "Required to use all features of the queue worker.", "ext-redis": "Required to use the Redis cache and queue drivers (^4.0|^5.0|^6.0).", - "fakerphp/faker": "Required to use the eloquent factory builder (^1.9.1).", + "fakerphp/faker": "Required to generate fake data using the fake() helper (^1.23).", "filp/whoops": "Required for friendly error pages in development (^2.14.3).", "laravel/tinker": "Required to use the tinker console command (^2.0).", "league/flysystem-aws-s3-v3": "Required to use the Flysystem S3 driver (^3.25.1).", @@ -2093,7 +2094,7 @@ "issues": "https://github.com/laravel/framework/issues", "source": "https://github.com/laravel/framework" }, - "time": "2025-09-04T14:58:12+00:00" + "time": "2025-09-16T14:15:03+00:00" }, { "name": "laravel/passport", @@ -4349,6 +4350,77 @@ }, "time": "2020-10-15T08:29:30+00:00" }, + { + "name": "phiki/phiki", + "version": "v2.0.0", + "source": { + "type": "git", + "url": "https://github.com/phikiphp/phiki.git", + "reference": "461f6dd7e91dc3a95463b42f549ac7d0aab4702f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phikiphp/phiki/zipball/461f6dd7e91dc3a95463b42f549ac7d0aab4702f", + "reference": "461f6dd7e91dc3a95463b42f549ac7d0aab4702f", + "shasum": "" + }, + "require": { + "ext-mbstring": "*", + "league/commonmark": "^2.5.3", + "php": "^8.2", + "psr/simple-cache": "^3.0" + }, + "require-dev": { + "illuminate/support": "^11.45", + "laravel/pint": "^1.18.1", + "orchestra/testbench": "^9.15", + "pestphp/pest": "^3.5.1", + "phpstan/extension-installer": "^1.4.3", + "phpstan/phpstan": "^2.0", + "symfony/var-dumper": "^7.1.6" + }, + "type": "library", + "extra": { + "laravel": { + "providers": [ + "Phiki\\Adapters\\Laravel\\PhikiServiceProvider" + ] + } + }, + "autoload": { + "psr-4": { + "Phiki\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ryan Chandler", + "email": "support@ryangjchandler.co.uk", + "homepage": "https://ryangjchandler.co.uk", + "role": "Developer" + } + ], + "description": "Syntax highlighting using TextMate grammars in PHP.", + "support": { + "issues": "https://github.com/phikiphp/phiki/issues", + "source": "https://github.com/phikiphp/phiki/tree/v2.0.0" + }, + "funding": [ + { + "url": "https://github.com/sponsors/ryangjchandler", + "type": "github" + }, + { + "url": "https://buymeacoffee.com/ryangjchandler", + "type": "other" + } + ], + "time": "2025-08-28T18:20:27+00:00" + }, { "name": "php-http/client-common", "version": "2.7.2", @@ -11332,16 +11404,16 @@ }, { "name": "phpstan/phpstan", - "version": "2.1.25", + "version": "2.1.26", "source": { "type": "git", "url": "https://github.com/phpstan/phpstan.git", - "reference": "4087d28bd252895874e174d65e26b2c202ed893a" + "reference": "b13345001a8553ec405b7741be0c6b8d7f8b5bf5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan/zipball/4087d28bd252895874e174d65e26b2c202ed893a", - "reference": "4087d28bd252895874e174d65e26b2c202ed893a", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/b13345001a8553ec405b7741be0c6b8d7f8b5bf5", + "reference": "b13345001a8553ec405b7741be0c6b8d7f8b5bf5", "shasum": "" }, "require": { @@ -11386,7 +11458,7 @@ "type": "github" } ], - "time": "2025-09-12T14:26:42+00:00" + "time": "2025-09-16T11:33:46+00:00" }, { "name": "phpstan/phpstan-deprecation-rules", diff --git a/config/firefly.php b/config/firefly.php index 7fa9eff12a..d5c69546c3 100644 --- a/config/firefly.php +++ b/config/firefly.php @@ -78,8 +78,8 @@ return [ 'running_balance_column' => env('USE_RUNNING_BALANCE', false), // see cer.php for exchange rates feature flag. ], - 'version' => 'develop/2025-09-15', - 'build_time' => 1757957039, + 'version' => 'develop/2025-09-16', + 'build_time' => 1758048808, 'api_version' => '2.1.0', // field is no longer used. 'db_version' => 26, diff --git a/package-lock.json b/package-lock.json index b8d603f07b..671dd29016 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3159,13 +3159,13 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "24.4.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-24.4.0.tgz", - "integrity": "sha512-gUuVEAK4/u6F9wRLznPUU4WGUacSEBDPoC2TrBkw3GAnOLHBL45QdfHOXp1kJ4ypBGLxTOB+t7NJLpKoC3gznQ==", + "version": "24.5.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.5.0.tgz", + "integrity": "sha512-y1dMvuvJspJiPSDZUQ+WMBvF7dpnEqN4x9DDC9ie5Fs/HUZJA3wFp7EhHoVaKX/iI0cRoECV8X2jL8zi0xrHCg==", "dev": true, "license": "MIT", "dependencies": { - "undici-types": "~7.11.0" + "undici-types": "~7.12.0" } }, "node_modules/@types/node-forge": { @@ -4347,9 +4347,9 @@ } }, "node_modules/browserslist": { - "version": "4.26.0", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.26.0.tgz", - "integrity": "sha512-P9go2WrP9FiPwLv3zqRD/Uoxo0RSHjzFCiQz7d4vbmwNqQFo9T9WCeP/Qn5EbcKQY6DBbkxEXNcpJOmncNrb7A==", + "version": "4.26.2", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.26.2.tgz", + "integrity": "sha512-ECFzp6uFOSB+dcZ5BK/IBaGWssbSYBHvuMeMt3MMFyhI0Z8SqGgEkBLARgpRH3hutIgPVsALcMwbDrJqPxQ65A==", "dev": true, "funding": [ { @@ -4367,7 +4367,7 @@ ], "license": "MIT", "dependencies": { - "baseline-browser-mapping": "^2.8.2", + "baseline-browser-mapping": "^2.8.3", "caniuse-lite": "^1.0.30001741", "electron-to-chromium": "^1.5.218", "node-releases": "^2.0.21", @@ -4508,9 +4508,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001741", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001741.tgz", - "integrity": "sha512-QGUGitqsc8ARjLdgAfxETDhRbJ0REsP6O3I96TAth/mVjh2cYzN2u+3AzPP3aVSm2FehEItaJw1xd+IGBXWeSw==", + "version": "1.0.30001743", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001743.tgz", + "integrity": "sha512-e6Ojr7RV14Un7dz6ASD0aZDmQPT/A+eZU+nuTNfjqmRrmkmQlnTNWH0SKmqagx9PeW87UVqapSurtAXifmtdmw==", "dev": true, "funding": [ { @@ -11357,9 +11357,9 @@ } }, "node_modules/undici-types": { - "version": "7.11.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.11.0.tgz", - "integrity": "sha512-kt1ZriHTi7MU+Z/r9DOdAI3ONdaR3M3csEaRc6ewa4f4dTvX4cQCbJ4NkEn0ohE4hHtq85+PhPSTY+pO/1PwgA==", + "version": "7.12.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.12.0.tgz", + "integrity": "sha512-goOacqME2GYyOZZfb5Lgtu+1IDmAlAEu5xnD3+xTzS10hT0vzpf0SPjkXwAw9Jm+4n/mQGDP3LO8CPbYROeBfQ==", "dev": true, "license": "MIT" }, diff --git a/resources/assets/v1/src/locales/it.json b/resources/assets/v1/src/locales/it.json index dc449681e6..08b853b545 100644 --- a/resources/assets/v1/src/locales/it.json +++ b/resources/assets/v1/src/locales/it.json @@ -160,7 +160,7 @@ "url": "URL", "active": "Attivo", "interest_date": "Data di valuta", - "administration_currency": "Primary currency", + "administration_currency": "Valuta primaria", "title": "Titolo", "date": "Data", "book_date": "Data contabile", @@ -180,7 +180,7 @@ "list": { "title": "Titolo", "active": "\u00c8 attivo?", - "primary_currency": "Primary currency", + "primary_currency": "Valuta primaria", "trigger": "Trigger", "response": "Risposta", "delivery": "Consegna", From cb6b3d5f856e7785b66ae5043f95eca712c56d9f Mon Sep 17 00:00:00 2001 From: James Cole Date: Tue, 16 Sep 2025 21:09:29 +0200 Subject: [PATCH 16/58] Fix #10891 --- app/Models/CurrencyExchangeRate.php | 2 +- app/Support/Cronjobs/AutoBudgetCronjob.php | 4 ++-- app/Support/Cronjobs/BillWarningCronjob.php | 4 ++-- app/Support/Cronjobs/ExchangeRatesCronjob.php | 4 ++-- app/Support/Cronjobs/RecurringCronjob.php | 4 ++-- app/Support/Cronjobs/WebhookCronjob.php | 4 ++-- 6 files changed, 11 insertions(+), 11 deletions(-) diff --git a/app/Models/CurrencyExchangeRate.php b/app/Models/CurrencyExchangeRate.php index c484ae2cc5..7edec3b797 100644 --- a/app/Models/CurrencyExchangeRate.php +++ b/app/Models/CurrencyExchangeRate.php @@ -38,7 +38,7 @@ class CurrencyExchangeRate extends Model use ReturnsIntegerUserIdTrait; use SoftDeletes; - protected $fillable = ['user_id', 'from_currency_id', 'to_currency_id', 'date', 'date_tz', 'rate']; + protected $fillable = ['user_id','user_group_id', 'from_currency_id', 'to_currency_id', 'date', 'date_tz', 'rate']; public function fromCurrency(): BelongsTo { diff --git a/app/Support/Cronjobs/AutoBudgetCronjob.php b/app/Support/Cronjobs/AutoBudgetCronjob.php index f71a4a4da1..9a2a5a0d66 100644 --- a/app/Support/Cronjobs/AutoBudgetCronjob.php +++ b/app/Support/Cronjobs/AutoBudgetCronjob.php @@ -40,8 +40,8 @@ class AutoBudgetCronjob extends AbstractCronjob /** @var Configuration $config */ $config = FireflyConfig::get('last_ab_job', 0); $lastTime = (int) $config->data; - $diff = Carbon::now()->getTimestamp() - $lastTime; - $diffForHumans = today(config('app.timezone'))->diffForHumans(Carbon::createFromTimestamp($lastTime), null, true); + $diff = now(config('app.timezone'))->getTimestamp() - $lastTime; + $diffForHumans = now(config('app.timezone'))->diffForHumans(Carbon::createFromTimestamp($lastTime), null, true); if (0 === $lastTime) { Log::info('Auto budget cron-job has never fired before.'); } diff --git a/app/Support/Cronjobs/BillWarningCronjob.php b/app/Support/Cronjobs/BillWarningCronjob.php index aece4d403f..8a30bb9c0a 100644 --- a/app/Support/Cronjobs/BillWarningCronjob.php +++ b/app/Support/Cronjobs/BillWarningCronjob.php @@ -46,8 +46,8 @@ class BillWarningCronjob extends AbstractCronjob /** @var Configuration $config */ $config = FireflyConfig::get('last_bw_job', 0); $lastTime = (int) $config->data; - $diff = Carbon::now()->getTimestamp() - $lastTime; - $diffForHumans = today(config('app.timezone'))->diffForHumans(Carbon::createFromTimestamp($lastTime), null, true); + $diff = now(config('app.timezone'))->getTimestamp() - $lastTime; + $diffForHumans = now(config('app.timezone'))->diffForHumans(Carbon::createFromTimestamp($lastTime), null, true); if (0 === $lastTime) { Log::info('The bill notification cron-job has never fired before.'); diff --git a/app/Support/Cronjobs/ExchangeRatesCronjob.php b/app/Support/Cronjobs/ExchangeRatesCronjob.php index f7d80b8033..71c9a8e587 100644 --- a/app/Support/Cronjobs/ExchangeRatesCronjob.php +++ b/app/Support/Cronjobs/ExchangeRatesCronjob.php @@ -40,8 +40,8 @@ class ExchangeRatesCronjob extends AbstractCronjob /** @var Configuration $config */ $config = FireflyConfig::get('last_cer_job', 0); $lastTime = (int) $config->data; - $diff = Carbon::now()->getTimestamp() - $lastTime; - $diffForHumans = today(config('app.timezone'))->diffForHumans(Carbon::createFromTimestamp($lastTime), null, true); + $diff = now(config('app.timezone'))->getTimestamp() - $lastTime; + $diffForHumans = now(config('app.timezone'))->diffForHumans(Carbon::createFromTimestamp($lastTime), null, true); if (0 === $lastTime) { Log::info('Exchange rates cron-job has never fired before.'); } diff --git a/app/Support/Cronjobs/RecurringCronjob.php b/app/Support/Cronjobs/RecurringCronjob.php index cc6bb7f901..c722733253 100644 --- a/app/Support/Cronjobs/RecurringCronjob.php +++ b/app/Support/Cronjobs/RecurringCronjob.php @@ -46,8 +46,8 @@ class RecurringCronjob extends AbstractCronjob /** @var Configuration $config */ $config = FireflyConfig::get('last_rt_job', 0); $lastTime = (int) $config->data; - $diff = Carbon::now()->getTimestamp() - $lastTime; - $diffForHumans = today(config('app.timezone'))->diffForHumans(Carbon::createFromTimestamp($lastTime), null, true); + $diff = now(config('app.timezone'))->getTimestamp() - $lastTime; + $diffForHumans = now(config('app.timezone'))->diffForHumans(Carbon::createFromTimestamp($lastTime), null, true); if (0 === $lastTime) { Log::info('Recurring transactions cron-job has never fired before.'); diff --git a/app/Support/Cronjobs/WebhookCronjob.php b/app/Support/Cronjobs/WebhookCronjob.php index dcac057140..0cd1899380 100644 --- a/app/Support/Cronjobs/WebhookCronjob.php +++ b/app/Support/Cronjobs/WebhookCronjob.php @@ -46,8 +46,8 @@ class WebhookCronjob extends AbstractCronjob /** @var Configuration $config */ $config = FireflyConfig::get('last_webhook_job', 0); $lastTime = (int) $config->data; - $diff = Carbon::now()->getTimestamp() - $lastTime; - $diffForHumans = today(config('app.timezone'))->diffForHumans(Carbon::createFromTimestamp($lastTime), null, true); + $diff = now(config('app.timezone'))->getTimestamp() - $lastTime; + $diffForHumans = now(config('app.timezone'))->diffForHumans(Carbon::createFromTimestamp($lastTime), null, true); if (0 === $lastTime) { Log::info('The webhook cron-job has never fired before.'); From 3491fbb99dc131736c92a0a3d7376049224261ff Mon Sep 17 00:00:00 2001 From: James Cole Date: Wed, 17 Sep 2025 07:09:40 +0200 Subject: [PATCH 17/58] Force account search to validate it did not just find the source account. #10920 --- app/Factory/TransactionJournalFactory.php | 2 +- .../Account/AccountRepository.php | 13 ++-- .../Internal/Support/JournalServiceTrait.php | 74 ++++++++++++------- app/Validation/Account/DepositValidation.php | 34 +++++---- app/Validation/Account/OBValidation.php | 23 +++--- .../Account/WithdrawalValidation.php | 25 ++++--- app/Validation/AccountValidator.php | 43 +++++------ resources/lang/en_US/validation.php | 12 +-- 8 files changed, 126 insertions(+), 100 deletions(-) diff --git a/app/Factory/TransactionJournalFactory.php b/app/Factory/TransactionJournalFactory.php index 7206947be2..dd2d9e6830 100644 --- a/app/Factory/TransactionJournalFactory.php +++ b/app/Factory/TransactionJournalFactory.php @@ -222,7 +222,7 @@ class TransactionJournalFactory Log::debug('Source info:', $sourceInfo); Log::debug('Destination info:', $destInfo); $sourceAccount = $this->getAccount($type->type, 'source', $sourceInfo); - $destinationAccount = $this->getAccount($type->type, 'destination', $destInfo); + $destinationAccount = $this->getAccount($type->type, 'destination', $destInfo, $sourceAccount); Log::debug('Done with getAccount(2x)'); diff --git a/app/Repositories/Account/AccountRepository.php b/app/Repositories/Account/AccountRepository.php index c9039b7c0f..c2a14d8d81 100644 --- a/app/Repositories/Account/AccountRepository.php +++ b/app/Repositories/Account/AccountRepository.php @@ -45,6 +45,7 @@ use FireflyIII\Support\Repositories\UserGroup\UserGroupTrait; use Illuminate\Database\Eloquent\Builder as EloquentBuilder; use Illuminate\Database\Eloquent\Relations\HasMany; use Illuminate\Support\Collection; +use Illuminate\Support\Facades\Log; use Illuminate\Support\Facades\Storage; use Override; @@ -150,18 +151,18 @@ class AccountRepository implements AccountRepositoryInterface, UserGroupInterfac $query->leftJoin('account_types', 'accounts.account_type_id', '=', 'account_types.id'); $query->whereIn('account_types.type', $types); } - app('log')->debug(sprintf('Searching for account named "%s" (of user #%d) of the following type(s)', $name, $this->user->id), ['types' => $types]); + Log::debug(sprintf('Searching for account named "%s" (of user #%d) of the following type(s)', $name, $this->user->id), ['types' => $types]); $query->where('accounts.name', $name); /** @var null|Account $account */ $account = $query->first(['accounts.*']); if (null === $account) { - app('log')->debug(sprintf('There is no account with name "%s" of types', $name), $types); + Log::debug(sprintf('There is no account with name "%s" of types', $name), $types); return null; } - app('log')->debug(sprintf('Found #%d (%s) with type id %d', $account->id, $account->name, $account->account_type_id)); + Log::debug(sprintf('Found #%d (%s) with type id %d', $account->id, $account->name, $account->account_type_id)); return $account; } @@ -465,14 +466,14 @@ class AccountRepository implements AccountRepositoryInterface, UserGroupInterfac ]; if (array_key_exists(ucfirst($type), $sets)) { $order = (int) $this->getAccountsByType($sets[ucfirst($type)])->max('order'); - app('log')->debug(sprintf('Return max order of "%s" set: %d', $type, $order)); + Log::debug(sprintf('Return max order of "%s" set: %d', $type, $order)); return $order; } $specials = [AccountTypeEnum::CASH->value, AccountTypeEnum::INITIAL_BALANCE->value, AccountTypeEnum::IMPORT->value, AccountTypeEnum::RECONCILIATION->value]; $order = (int) $this->getAccountsByType($specials)->max('order'); - app('log')->debug(sprintf('Return max order of "%s" set (specials!): %d', $type, $order)); + Log::debug(sprintf('Return max order of "%s" set (specials!): %d', $type, $order)); return $order; } @@ -599,7 +600,7 @@ class AccountRepository implements AccountRepositoryInterface, UserGroupInterfac continue; } if ($index !== (int) $account->order) { - app('log')->debug(sprintf('Account #%d ("%s"): order should %d be but is %d.', $account->id, $account->name, $index, $account->order)); + Log::debug(sprintf('Account #%d ("%s"): order should %d be but is %d.', $account->id, $account->name, $index, $account->order)); $account->order = $index; $account->save(); } diff --git a/app/Services/Internal/Support/JournalServiceTrait.php b/app/Services/Internal/Support/JournalServiceTrait.php index d0de5db9df..9d7cedad90 100644 --- a/app/Services/Internal/Support/JournalServiceTrait.php +++ b/app/Services/Internal/Support/JournalServiceTrait.php @@ -54,7 +54,7 @@ trait JournalServiceTrait /** * @throws FireflyException */ - protected function getAccount(string $transactionType, string $direction, array $data): ?Account + protected function getAccount(string $transactionType, string $direction, array $data, ?Account $opposite = null): ?Account { // some debug logging: Log::debug(sprintf('Now in getAccount(%s)', $direction), $data); @@ -69,12 +69,12 @@ trait JournalServiceTrait $message = 'Transaction = %s, %s account should be in: %s. Direction is %s.'; Log::debug(sprintf($message, $transactionType, $direction, implode(', ', $expectedTypes[$transactionType] ?? ['UNKNOWN']), $direction)); - $result = $this->findAccountById($data, $expectedTypes[$transactionType]); - $result = $this->findAccountByIban($result, $data, $expectedTypes[$transactionType]); + $result = $this->findAccountById($data, $expectedTypes[$transactionType], $opposite); + $result = $this->findAccountByIban($result, $data, $expectedTypes[$transactionType], $opposite); $ibanResult = $result; - $result = $this->findAccountByNumber($result, $data, $expectedTypes[$transactionType]); + $result = $this->findAccountByNumber($result, $data, $expectedTypes[$transactionType], $opposite); $numberResult = $result; - $result = $this->findAccountByName($result, $data, $expectedTypes[$transactionType]); + $result = $this->findAccountByName($result, $data, $expectedTypes[$transactionType], $opposite); $nameResult = $result; // if $result (find by name) is NULL, but IBAN is set, any result of the search by NAME can't overrule @@ -82,7 +82,7 @@ trait JournalServiceTrait if (null !== $nameResult && null === $numberResult && null === $ibanResult && '' !== (string) $data['iban'] && '' !== (string) $nameResult->iban) { $data['name'] = sprintf('%s (%s)', $data['name'], $data['iban']); Log::debug(sprintf('Search again using the new name, "%s".', $data['name'])); - $result = $this->findAccountByName(null, $data, $expectedTypes[$transactionType]); + $result = $this->findAccountByName(null, $data, $expectedTypes[$transactionType], $opposite); } // the account that Firefly III creates must be "creatable", aka select the one we can create from the list just in case @@ -115,15 +115,18 @@ trait JournalServiceTrait return $result; } - private function findAccountById(array $data, array $types): ?Account + private function findAccountById(array $data, array $types, ?Account $opposite = null): ?Account { // first attempt, find by ID. if (null !== $data['id']) { $search = $this->accountRepository->find((int) $data['id']); if (null !== $search && in_array($search->accountType->type, $types, true)) { - Log::debug( - sprintf('Found "account_id" object: #%d, "%s" of type %s (1)', $search->id, $search->name, $search->accountType->type) - ); + Log::debug(sprintf('Found "account_id" object: #%d, "%s" of type %s (1)', $search->id, $search->name, $search->accountType->type)); + + if($opposite?->id === $search->id) { + Log::debug(sprintf('Account #%d is the same as opposite account #%d, returning NULL.', $search->id, $opposite->id)); + return null; + } return $search; } @@ -140,7 +143,7 @@ trait JournalServiceTrait return null; } - private function findAccountByIban(?Account $account, array $data, array $types): ?Account + private function findAccountByIban(?Account $account, array $data, array $types, ?Account $opposite = null): ?Account { if ($account instanceof Account) { Log::debug(sprintf('Already have account #%d ("%s"), return that.', $account->id, $account->name)); @@ -153,21 +156,26 @@ trait JournalServiceTrait return null; } // find by preferred type. - $source = $this->accountRepository->findByIbanNull($data['iban'], [$types[0]]); + $result = $this->accountRepository->findByIbanNull($data['iban'], [$types[0]]); // or any expected type. - $source ??= $this->accountRepository->findByIbanNull($data['iban'], $types); + $result ??= $this->accountRepository->findByIbanNull($data['iban'], $types); - if (null !== $source) { - Log::debug(sprintf('Found "account_iban" object: #%d, %s', $source->id, $source->name)); + if (null !== $result) { + Log::debug(sprintf('Found "account_iban" object: #%d, %s', $result->id, $result->name)); - return $source; + if($opposite?->id === $result->id) { + Log::debug(sprintf('Account #%d is the same as opposite account #%d, returning NULL.', $result->id, $opposite->id)); + return null; + } + + return $result; } Log::debug(sprintf('Found no account with IBAN "%s" of expected types', $data['iban']), $types); return null; } - private function findAccountByNumber(?Account $account, array $data, array $types): ?Account + private function findAccountByNumber(?Account $account, array $data, array $types, ?Account $opposite = null): ?Account { if ($account instanceof Account) { Log::debug(sprintf('Already have account #%d ("%s"), return that.', $account->id, $account->name)); @@ -180,15 +188,20 @@ trait JournalServiceTrait return null; } // find by preferred type. - $source = $this->accountRepository->findByAccountNumber((string) $data['number'], [$types[0]]); + $result = $this->accountRepository->findByAccountNumber((string) $data['number'], [$types[0]]); // or any expected type. - $source ??= $this->accountRepository->findByAccountNumber((string) $data['number'], $types); + $result ??= $this->accountRepository->findByAccountNumber((string) $data['number'], $types); - if (null !== $source) { - Log::debug(sprintf('Found account: #%d, %s', $source->id, $source->name)); + if (null !== $result) { + Log::debug(sprintf('Found account: #%d, %s', $result->id, $result->name)); - return $source; + if($opposite?->id === $result->id) { + Log::debug(sprintf('Account #%d is the same as opposite account #%d, returning NULL.', $result->id, $opposite->id)); + return null; + } + + return $result; } Log::debug(sprintf('Found no account with account number "%s" of expected types', $data['number']), $types); @@ -196,7 +209,7 @@ trait JournalServiceTrait return null; } - private function findAccountByName(?Account $account, array $data, array $types): ?Account + private function findAccountByName(?Account $account, array $data, array $types, ?Account $opposite = null): ?Account { if ($account instanceof Account) { Log::debug(sprintf('Already have account #%d ("%s"), return that.', $account->id, $account->name)); @@ -210,15 +223,20 @@ trait JournalServiceTrait } // find by preferred type. - $source = $this->accountRepository->findByName($data['name'], [$types[0]]); + $result = $this->accountRepository->findByName($data['name'], [$types[0]]); // or any expected type. - $source ??= $this->accountRepository->findByName($data['name'], $types); + $result ??= $this->accountRepository->findByName($data['name'], $types); - if (null !== $source) { - Log::debug(sprintf('Found "account_name" object: #%d, %s', $source->id, $source->name)); + if (null !== $result) { + Log::debug(sprintf('Found "account_name" object: #%d, %s', $result->id, $result->name)); - return $source; + if($opposite?->id === $result->id) { + Log::debug(sprintf('Account #%d is the same as opposite account #%d, returning NULL.', $result->id, $opposite->id)); + return null; + } + + return $result; } Log::debug(sprintf('Found no account with account name "%s" of expected types', $data['name']), $types); diff --git a/app/Validation/Account/DepositValidation.php b/app/Validation/Account/DepositValidation.php index 87e2e69f43..42a38447b6 100644 --- a/app/Validation/Account/DepositValidation.php +++ b/app/Validation/Account/DepositValidation.php @@ -27,6 +27,7 @@ namespace FireflyIII\Validation\Account; use FireflyIII\Enums\AccountTypeEnum; use FireflyIII\Models\Account; use FireflyIII\Models\AccountType; +use Illuminate\Support\Facades\Log; /** * Trait DepositValidation @@ -40,7 +41,7 @@ trait DepositValidation $accountName = array_key_exists('name', $array) ? $array['name'] : null; $accountIban = array_key_exists('iban', $array) ? $array['iban'] : null; - app('log')->debug('Now in validateDepositDestination', $array); + Log::debug('Now in validateDepositDestination', $array); // source can be any of the following types. $validTypes = $this->combinations[$this->transactionType][$this->source->accountType->type] ?? []; @@ -48,12 +49,12 @@ trait DepositValidation // if both values are NULL we return false, // because the destination of a deposit can't be created. $this->destError = (string) trans('validation.deposit_dest_need_data'); - app('log')->error('Both values are NULL, cant create deposit destination.'); + Log::error('Both values are NULL, cant create deposit destination.'); $result = false; } // if the account can be created anyway we don't need to search. if (null === $result && true === $this->canCreateTypes($validTypes)) { - app('log')->debug('Can create some of these types, so return true.'); + Log::debug('Can create some of these types, so return true.'); $result = true; } @@ -61,17 +62,17 @@ trait DepositValidation // otherwise try to find the account: $search = $this->findExistingAccount($validTypes, $array); if (null === $search) { - app('log')->debug('findExistingAccount() returned NULL, so the result is false.'); + Log::debug('findExistingAccount() returned NULL, so the result is false.'); $this->destError = (string) trans('validation.deposit_dest_bad_data', ['id' => $accountId, 'name' => $accountName]); $result = false; } if (null !== $search) { - app('log')->debug(sprintf('findExistingAccount() returned #%d ("%s"), so the result is true.', $search->id, $search->name)); + Log::debug(sprintf('findExistingAccount() returned #%d ("%s"), so the result is true.', $search->id, $search->name)); $this->setDestination($search); $result = true; } } - app('log')->debug(sprintf('validateDepositDestination will return %s', var_export($result, true))); + Log::debug(sprintf('validateDepositDestination will return %s', var_export($result, true))); return $result; } @@ -92,7 +93,7 @@ trait DepositValidation $accountName = array_key_exists('name', $array) ? $array['name'] : null; $accountIban = array_key_exists('iban', $array) ? $array['iban'] : null; $accountNumber = array_key_exists('number', $array) ? $array['number'] : null; - app('log')->debug('Now in validateDepositSource', $array); + Log::debug('Now in validateDepositSource', $array); // null = we found nothing at all or didn't even search // false = invalid results @@ -114,7 +115,7 @@ trait DepositValidation // if there is an iban, it can only be in use by a valid source type, or we will fail. if (null !== $accountIban && '' !== $accountIban) { - app('log')->debug('Check if there is not already another account with this IBAN'); + Log::debug('Check if there is not already another account with this IBAN'); $existing = $this->findExistingAccount($validTypes, ['iban' => $accountIban], true); if (null !== $existing) { $this->sourceError = (string) trans('validation.deposit_src_iban_exists'); @@ -128,11 +129,14 @@ trait DepositValidation if (null !== $accountId) { $search = $this->accountRepository->find($accountId); if (null !== $search && !in_array($search->accountType->type, $validTypes, true)) { - app('log')->debug(sprintf('User submitted an ID (#%d), which is a "%s", so this is not a valid source.', $accountId, $search->accountType->type)); - app('log')->debug(sprintf('Firefly III accepts ID #%d as valid account data.', $accountId)); + Log::debug(sprintf('User submitted an ID (#%d), which is a "%s", so this is not a valid source.', $accountId, $search->accountType->type)); + Log::debug(sprintf('Firefly III does not accept ID #%d as valid account data.', $accountId)); + // #10921 Set result false + $this->sourceError = (string) trans('validation.withdrawal_source_bad_data', ['id' => $accountId, 'name' => $accountName]); + $result = false; } if (null !== $search && in_array($search->accountType->type, $validTypes, true)) { - app('log')->debug('ID result is not null and seems valid, save as source account.'); + Log::debug('ID result is not null and seems valid, save as source account.'); $this->setSource($search); $result = true; } @@ -142,11 +146,11 @@ trait DepositValidation if (null !== $accountIban) { $search = $this->accountRepository->findByIbanNull($accountIban, $validTypes); if (null !== $search && !in_array($search->accountType->type, $validTypes, true)) { - app('log')->debug(sprintf('User submitted IBAN ("%s"), which is a "%s", so this is not a valid source.', $accountIban, $search->accountType->type)); + Log::debug(sprintf('User submitted IBAN ("%s"), which is a "%s", so this is not a valid source.', $accountIban, $search->accountType->type)); $result = false; } if (null !== $search && in_array($search->accountType->type, $validTypes, true)) { - app('log')->debug('IBAN result is not null and seems valid, save as source account.'); + Log::debug('IBAN result is not null and seems valid, save as source account.'); $this->setSource($search); $result = true; } @@ -156,13 +160,13 @@ trait DepositValidation if (null !== $accountNumber && '' !== $accountNumber) { $search = $this->accountRepository->findByAccountNumber($accountNumber, $validTypes); if (null !== $search && !in_array($search->accountType->type, $validTypes, true)) { - app('log')->debug( + Log::debug( sprintf('User submitted number ("%s"), which is a "%s", so this is not a valid source.', $accountNumber, $search->accountType->type) ); $result = false; } if (null !== $search && in_array($search->accountType->type, $validTypes, true)) { - app('log')->debug('Number result is not null and seems valid, save as source account.'); + Log::debug('Number result is not null and seems valid, save as source account.'); $this->setSource($search); $result = true; } diff --git a/app/Validation/Account/OBValidation.php b/app/Validation/Account/OBValidation.php index a3f32afeb0..b0633c63f9 100644 --- a/app/Validation/Account/OBValidation.php +++ b/app/Validation/Account/OBValidation.php @@ -27,6 +27,7 @@ namespace FireflyIII\Validation\Account; use FireflyIII\Enums\AccountTypeEnum; use FireflyIII\Models\Account; use FireflyIII\Models\AccountType; +use Illuminate\Support\Facades\Log; /** * Trait OBValidation @@ -38,7 +39,7 @@ trait OBValidation $result = null; $accountId = array_key_exists('id', $array) ? $array['id'] : null; $accountName = array_key_exists('name', $array) ? $array['name'] : null; - app('log')->debug('Now in validateOBDestination', $array); + Log::debug('Now in validateOBDestination', $array); // source can be any of the following types. $validTypes = $this->combinations[$this->transactionType][$this->source?->accountType->type] ?? []; @@ -46,12 +47,12 @@ trait OBValidation // if both values are NULL we return false, // because the destination of a deposit can't be created. $this->destError = (string) trans('validation.ob_dest_need_data'); - app('log')->error('Both values are NULL, cant create OB destination.'); + Log::error('Both values are NULL, cant create OB destination.'); $result = false; } // if the account can be created anyway we don't need to search. if (null === $result && true === $this->canCreateTypes($validTypes)) { - app('log')->debug('Can create some of these types, so return true.'); + Log::debug('Can create some of these types, so return true.'); $result = true; } @@ -59,17 +60,17 @@ trait OBValidation // otherwise try to find the account: $search = $this->findExistingAccount($validTypes, $array); if (null === $search) { - app('log')->debug('findExistingAccount() returned NULL, so the result is false.', $validTypes); + Log::debug('findExistingAccount() returned NULL, so the result is false.', $validTypes); $this->destError = (string) trans('validation.ob_dest_bad_data', ['id' => $accountId, 'name' => $accountName]); $result = false; } if (null !== $search) { - app('log')->debug(sprintf('findExistingAccount() returned #%d ("%s"), so the result is true.', $search->id, $search->name)); + Log::debug(sprintf('findExistingAccount() returned #%d ("%s"), so the result is true.', $search->id, $search->name)); $this->setDestination($search); $result = true; } } - app('log')->debug(sprintf('validateOBDestination(%d, "%s") will return %s', $accountId, $accountName, var_export($result, true))); + Log::debug(sprintf('validateOBDestination(%d, "%s") will return %s', $accountId, $accountName, var_export($result, true))); return $result; } @@ -84,7 +85,7 @@ trait OBValidation { $accountId = array_key_exists('id', $array) ? $array['id'] : null; $accountName = array_key_exists('name', $array) ? $array['name'] : null; - app('log')->debug('Now in validateOBSource', $array); + Log::debug('Now in validateOBSource', $array); $result = null; // source can be any of the following types. $validTypes = array_keys($this->combinations[$this->transactionType]); @@ -100,19 +101,19 @@ trait OBValidation // if the user submits an ID only but that ID is not of the correct type, // return false. if (null !== $accountId && null === $accountName) { - app('log')->debug('Source ID is not null, but name is null.'); + Log::debug('Source ID is not null, but name is null.'); $search = $this->accountRepository->find($accountId); // the source resulted in an account, but it's not of a valid type. if (null !== $search && !in_array($search->accountType->type, $validTypes, true)) { $message = sprintf('User submitted only an ID (#%d), which is a "%s", so this is not a valid source.', $accountId, $search->accountType->type); - app('log')->debug($message); + Log::debug($message); $this->sourceError = $message; $result = false; } // the source resulted in an account, AND it's of a valid type. if (null !== $search && in_array($search->accountType->type, $validTypes, true)) { - app('log')->debug(sprintf('Found account of correct type: #%d, "%s"', $search->id, $search->name)); + Log::debug(sprintf('Found account of correct type: #%d, "%s"', $search->id, $search->name)); $this->setSource($search); $result = true; } @@ -120,7 +121,7 @@ trait OBValidation // if the account can be created anyway we don't need to search. if (null === $result && true === $this->canCreateTypes($validTypes)) { - app('log')->debug('Result is still null.'); + Log::debug('Result is still null.'); $result = true; // set the source to be a (dummy) initial balance account. diff --git a/app/Validation/Account/WithdrawalValidation.php b/app/Validation/Account/WithdrawalValidation.php index 9456e4ecf9..ac2a06d825 100644 --- a/app/Validation/Account/WithdrawalValidation.php +++ b/app/Validation/Account/WithdrawalValidation.php @@ -26,6 +26,7 @@ namespace FireflyIII\Validation\Account; use FireflyIII\Enums\AccountTypeEnum; use FireflyIII\Models\Account; +use Illuminate\Support\Facades\Log; /** * Trait WithdrawalValidation @@ -37,14 +38,14 @@ trait WithdrawalValidation $accountId = array_key_exists('id', $array) ? $array['id'] : null; $accountName = array_key_exists('name', $array) ? $array['name'] : null; $accountIban = array_key_exists('iban', $array) ? $array['iban'] : null; - app('log')->debug('Now in validateGenericSource', $array); + Log::debug('Now in validateGenericSource', $array); // source can be any of the following types. $validTypes = [AccountTypeEnum::ASSET->value, AccountTypeEnum::REVENUE->value, AccountTypeEnum::LOAN->value, AccountTypeEnum::DEBT->value, AccountTypeEnum::MORTGAGE->value]; if (null === $accountId && null === $accountName && null === $accountIban && false === $this->canCreateTypes($validTypes)) { // if both values are NULL we return TRUE // because we assume the user doesn't want to submit / change anything. $this->sourceError = (string) trans('validation.withdrawal_source_need_data'); - app('log')->warning('[a] Not a valid source. Need more data.'); + Log::warning('[a] Not a valid source. Need more data.'); return false; } @@ -53,12 +54,12 @@ trait WithdrawalValidation $search = $this->findExistingAccount($validTypes, $array); if (null === $search) { $this->sourceError = (string) trans('validation.withdrawal_source_bad_data', ['id' => $accountId, 'name' => $accountName]); - app('log')->warning('Not a valid source. Cant find it.', $validTypes); + Log::warning('Not a valid source. Cant find it.', $validTypes); return false; } $this->setSource($search); - app('log')->debug('Valid source account!'); + Log::debug('Valid source account!'); return true; } @@ -73,10 +74,10 @@ trait WithdrawalValidation $accountName = array_key_exists('name', $array) ? $array['name'] : null; $accountIban = array_key_exists('iban', $array) ? $array['iban'] : null; $accountNumber = array_key_exists('number', $array) ? $array['number'] : null; - app('log')->debug('Now in validateWithdrawalDestination()', $array); + Log::debug('Now in validateWithdrawalDestination()', $array); // source can be any of the following types. $validTypes = $this->combinations[$this->transactionType][$this->source->accountType->type] ?? []; - app('log')->debug('Source type can be: ', $validTypes); + Log::debug('Source type can be: ', $validTypes); if (null === $accountId && null === $accountName && null === $accountIban && null === $accountNumber && false === $this->canCreateTypes($validTypes)) { // if both values are NULL return false, // because the destination of a withdrawal can never be created automatically. @@ -86,7 +87,7 @@ trait WithdrawalValidation } // if there's an ID it must be of the "validTypes". - if (null !== $accountId && 0 !== $accountId) { + if (null !== $accountId && 0 !== $accountId && $accountId !== $this->source->id) { $found = $this->accountRepository->find($accountId); if (null !== $found) { $type = $found->accountType->type; @@ -104,7 +105,7 @@ trait WithdrawalValidation // if there is an iban, it can only be in use by a valid destination type, or we will fail. // the inverse of $validTypes is if (null !== $accountIban && '' !== $accountIban) { - app('log')->debug('Check if there is not already an account with this IBAN'); + Log::debug('Check if there is not already an account with this IBAN'); // the inverse flag reverses the search, searching for everything that is NOT a valid type. $existing = $this->findExistingAccount($validTypes, ['iban' => $accountIban], true); if (null !== $existing) { @@ -125,14 +126,14 @@ trait WithdrawalValidation $accountIban = array_key_exists('iban', $array) ? $array['iban'] : null; $accountNumber = array_key_exists('number', $array) ? $array['number'] : null; - app('log')->debug('Now in validateWithdrawalSource', $array); + Log::debug('Now in validateWithdrawalSource', $array); // source can be any of the following types. $validTypes = array_keys($this->combinations[$this->transactionType]); if (null === $accountId && null === $accountName && null === $accountNumber && null === $accountIban && false === $this->canCreateTypes($validTypes)) { // if both values are NULL we return false, // because the source of a withdrawal can't be created. $this->sourceError = (string) trans('validation.withdrawal_source_need_data'); - app('log')->warning('[b] Not a valid source. Need more data.'); + Log::warning('[b] Not a valid source. Need more data.'); return false; } @@ -141,12 +142,12 @@ trait WithdrawalValidation $search = $this->findExistingAccount($validTypes, $array); if (null === $search) { $this->sourceError = (string) trans('validation.withdrawal_source_bad_data', ['id' => $accountId, 'name' => $accountName]); - app('log')->warning('Not a valid source. Cant find it.', $validTypes); + Log::warning('Not a valid source. Cant find it.', $validTypes); return false; } $this->setSource($search); - app('log')->debug('Valid source account!'); + Log::debug('Valid source account!'); return true; } diff --git a/app/Validation/AccountValidator.php b/app/Validation/AccountValidator.php index 44c9ad9d7b..0605b82bf0 100644 --- a/app/Validation/AccountValidator.php +++ b/app/Validation/AccountValidator.php @@ -36,6 +36,7 @@ use FireflyIII\Validation\Account\OBValidation; use FireflyIII\Validation\Account\ReconciliationValidation; use FireflyIII\Validation\Account\TransferValidation; use FireflyIII\Validation\Account\WithdrawalValidation; +use Illuminate\Support\Facades\Log; /** * Class AccountValidator @@ -80,10 +81,10 @@ class AccountValidator public function setSource(?Account $account): void { if (!$account instanceof Account) { - app('log')->debug('AccountValidator source is set to NULL'); + Log::debug('AccountValidator source is set to NULL'); } if ($account instanceof Account) { - app('log')->debug(sprintf('AccountValidator source is set to #%d: "%s" (%s)', $account->id, $account->name, $account->accountType?->type)); + Log::debug(sprintf('AccountValidator source is set to #%d: "%s" (%s)', $account->id, $account->name, $account->accountType?->type)); } $this->source = $account; } @@ -91,17 +92,17 @@ class AccountValidator public function setDestination(?Account $account): void { if (!$account instanceof Account) { - app('log')->debug('AccountValidator destination is set to NULL'); + Log::debug('AccountValidator destination is set to NULL'); } if ($account instanceof Account) { - app('log')->debug(sprintf('AccountValidator destination is set to #%d: "%s" (%s)', $account->id, $account->name, $account->accountType->type)); + Log::debug(sprintf('AccountValidator destination is set to #%d: "%s" (%s)', $account->id, $account->name, $account->accountType->type)); } $this->destination = $account; } public function setTransactionType(string $transactionType): void { - app('log')->debug(sprintf('Transaction type for validator is now "%s".', ucfirst($transactionType))); + Log::debug(sprintf('Transaction type for validator is now "%s".', ucfirst($transactionType))); $this->transactionType = ucfirst($transactionType); } @@ -117,9 +118,9 @@ class AccountValidator public function validateDestination(array $array): bool { - app('log')->debug('Now in AccountValidator::validateDestination()', $array); + Log::debug('Now in AccountValidator::validateDestination()', $array); if (!$this->source instanceof Account) { - app('log')->error('Source is NULL, always FALSE.'); + Log::error('Source is NULL, always FALSE.'); $this->destError = 'No source account validation has taken place yet. Please do this first or overrule the object.'; return false; @@ -128,7 +129,7 @@ class AccountValidator switch ($this->transactionType) { default: $this->destError = sprintf('AccountValidator::validateDestination cannot handle "%s", so it will always return false.', $this->transactionType); - app('log')->error(sprintf('AccountValidator::validateDestination cannot handle "%s", so it will always return false.', $this->transactionType)); + Log::error(sprintf('AccountValidator::validateDestination cannot handle "%s", so it will always return false.', $this->transactionType)); $result = false; @@ -170,11 +171,11 @@ class AccountValidator public function validateSource(array $array): bool { - app('log')->debug('Now in AccountValidator::validateSource()', $array); + Log::debug('Now in AccountValidator::validateSource()', $array); switch ($this->transactionType) { default: - app('log')->error(sprintf('AccountValidator::validateSource cannot handle "%s", so it will do a generic check.', $this->transactionType)); + Log::error(sprintf('AccountValidator::validateSource cannot handle "%s", so it will do a generic check.', $this->transactionType)); $result = $this->validateGenericSource($array); break; @@ -205,7 +206,7 @@ class AccountValidator break; case TransactionTypeEnum::RECONCILIATION->value: - app('log')->debug('Calling validateReconciliationSource'); + Log::debug('Calling validateReconciliationSource'); $result = $this->validateReconciliationSource($array); break; @@ -216,17 +217,17 @@ class AccountValidator protected function canCreateTypes(array $accountTypes): bool { - app('log')->debug('Can we create any of these types?', $accountTypes); + Log::debug('Can we create any of these types?', $accountTypes); /** @var string $accountType */ foreach ($accountTypes as $accountType) { if ($this->canCreateType($accountType)) { - app('log')->debug(sprintf('YES, we can create a %s', $accountType)); + Log::debug(sprintf('YES, we can create a %s', $accountType)); return true; } } - app('log')->debug('NO, we cant create any of those.'); + Log::debug('NO, we cant create any of those.'); return false; } @@ -250,8 +251,8 @@ class AccountValidator */ protected function findExistingAccount(array $validTypes, array $data, bool $inverse = false): ?Account { - app('log')->debug('Now in findExistingAccount', [$validTypes, $data]); - app('log')->debug('The search will be reversed!'); + Log::debug('Now in findExistingAccount', [$validTypes, $data]); + Log::debug('The search will be reversed!'); $accountId = array_key_exists('id', $data) ? $data['id'] : null; $accountIban = array_key_exists('iban', $data) ? $data['iban'] : null; $accountNumber = array_key_exists('number', $data) ? $data['number'] : null; @@ -264,7 +265,7 @@ class AccountValidator $check = in_array($accountType, $validTypes, true); $check = $inverse ? !$check : $check; // reverse the validation check if necessary. if (($first instanceof Account) && $check) { - app('log')->debug(sprintf('ID: Found %s account #%d ("%s", IBAN "%s")', $first->accountType->type, $first->id, $first->name, $first->iban ?? 'no iban')); + Log::debug(sprintf('ID: Found %s account #%d ("%s", IBAN "%s")', $first->accountType->type, $first->id, $first->name, $first->iban ?? 'no iban')); return $first; } @@ -277,7 +278,7 @@ class AccountValidator $check = in_array($accountType, $validTypes, true); $check = $inverse ? !$check : $check; // reverse the validation check if necessary. if (($first instanceof Account) && $check) { - app('log')->debug(sprintf('Iban: Found %s account #%d ("%s", IBAN "%s")', $first->accountType->type, $first->id, $first->name, $first->iban ?? 'no iban')); + Log::debug(sprintf('Iban: Found %s account #%d ("%s", IBAN "%s")', $first->accountType->type, $first->id, $first->name, $first->iban ?? 'no iban')); return $first; } @@ -290,7 +291,7 @@ class AccountValidator $check = in_array($accountType, $validTypes, true); $check = $inverse ? !$check : $check; // reverse the validation check if necessary. if (($first instanceof Account) && $check) { - app('log')->debug(sprintf('Number: Found %s account #%d ("%s", IBAN "%s")', $first->accountType->type, $first->id, $first->name, $first->iban ?? 'no iban')); + Log::debug(sprintf('Number: Found %s account #%d ("%s", IBAN "%s")', $first->accountType->type, $first->id, $first->name, $first->iban ?? 'no iban')); return $first; } @@ -300,12 +301,12 @@ class AccountValidator if ('' !== (string) $accountName) { $first = $this->accountRepository->findByName($accountName, $validTypes); if ($first instanceof Account) { - app('log')->debug(sprintf('Name: Found %s account #%d ("%s", IBAN "%s")', $first->accountType->type, $first->id, $first->name, $first->iban ?? 'no iban')); + Log::debug(sprintf('Name: Found %s account #%d ("%s", IBAN "%s")', $first->accountType->type, $first->id, $first->name, $first->iban ?? 'no iban')); return $first; } } - app('log')->debug('Found nothing in findExistingAccount()'); + Log::debug('Found nothing in findExistingAccount()'); return null; } diff --git a/resources/lang/en_US/validation.php b/resources/lang/en_US/validation.php index 7c2134f5a2..f7b27c0da6 100644 --- a/resources/lang/en_US/validation.php +++ b/resources/lang/en_US/validation.php @@ -24,10 +24,10 @@ declare(strict_types=1); return [ - 'limit_exists' => 'There is already a budget limit (amount) for this budget and currency in the given period.', + 'limit_exists' => 'There is already a budget limit (amount) for this budget and currency in the given period.', 'invalid_sort_instruction' => 'The sort instruction is invalid for an object of type ":object".', - 'invalid_sort_instruction_index' => 'The sort instruction at index #:index is invalid for an object of type ":object".', - 'no_sort_instructions' => 'There are no sort instructions defined for an object of type ":object".', + 'invalid_sort_instruction_index' => 'The sort instruction at index #:index is invalid for an object of type ":object".', + 'no_sort_instructions' => 'There are no sort instructions defined for an object of type ":object".', 'webhook_budget_info' => 'Cannot deliver budget information for transaction related webhooks.', 'webhook_account_info' => 'Cannot deliver account information for budget related webhooks.', 'webhook_transaction_info' => 'Cannot deliver transaction information for budget related webhooks.', @@ -40,8 +40,8 @@ return [ 'nog_logged_in' => 'You are not logged in.', 'prohibited' => 'You must not submit anything in field.', 'bad_webhook_combination' => 'Webhook trigger ":trigger" cannot be combined with webhook response ":response".', - 'unknown_webhook_trigger' => 'Unknown webhook trigger ":trigger".', - 'only_any_trigger' => 'If you select the "Any event"-trigger, you may not select any other triggers.', + 'unknown_webhook_trigger' => 'Unknown webhook trigger ":trigger".', + 'only_any_trigger' => 'If you select the "Any event"-trigger, you may not select any other triggers.', 'bad_type_source' => 'Firefly III can\'t determine the transaction type based on this source account.', 'bad_type_destination' => 'Firefly III can\'t determine the transaction type based on this destination account.', 'missing_where' => 'Array is missing "where"-clause', @@ -123,7 +123,7 @@ return [ 'between.file' => 'The :attribute must be between :min and :max kilobytes.', 'between.string' => 'The :attribute must be between :min and :max characters.', 'between.array' => 'The :attribute must have between :min and :max items.', - 'between_date' => 'The date must be between the given start and end date.', + 'between_date' => 'The date must be between the given start and end date.', 'boolean' => 'The :attribute field must be true or false.', 'confirmed' => 'The :attribute confirmation does not match.', 'date' => 'The :attribute is not a valid date.', From 65813f290dcd9e4a5ed915f0a559e0e2313435c8 Mon Sep 17 00:00:00 2001 From: James Cole Date: Wed, 17 Sep 2025 13:48:56 +0200 Subject: [PATCH 18/58] Expand changelog. --- changelog.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/changelog.md b/changelog.md index 93c2a96ae5..aecba981e0 100644 --- a/changelog.md +++ b/changelog.md @@ -8,6 +8,15 @@ This project adheres to [Semantic Versioning](http://semver.org/). ### Fixed - Fixed a missing filter from [issue 10803](https://github.com/firefly-iii/firefly-iii/issues/10803). +- #10891 +- #10920 +- #10921 +- #10833 + +### API + +- #10908 + ## 6.4.0 - 2025-09-14 From dbf7dba421cfeee972f1e98779bdf4b80ad95f06 Mon Sep 17 00:00:00 2001 From: James Cole Date: Wed, 17 Sep 2025 20:04:24 +0200 Subject: [PATCH 19/58] Fix #10916 --- .../Correction/CorrectsAccountTypes.php | 77 +++++++++++++------ 1 file changed, 55 insertions(+), 22 deletions(-) diff --git a/app/Console/Commands/Correction/CorrectsAccountTypes.php b/app/Console/Commands/Correction/CorrectsAccountTypes.php index c56fc6f79a..dd0e83e889 100644 --- a/app/Console/Commands/Correction/CorrectsAccountTypes.php +++ b/app/Console/Commands/Correction/CorrectsAccountTypes.php @@ -29,12 +29,15 @@ use FireflyIII\Enums\AccountTypeEnum; use FireflyIII\Enums\TransactionTypeEnum; use FireflyIII\Exceptions\FireflyException; use FireflyIII\Factory\AccountFactory; +use FireflyIII\Models\Account; use FireflyIII\Models\Transaction; use FireflyIII\Models\TransactionJournal; use FireflyIII\Models\TransactionType; +use FireflyIII\Repositories\Account\AccountRepositoryInterface; use Illuminate\Console\Command; use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Query\JoinClause; +use Illuminate\Support\Facades\Log; class CorrectsAccountTypes extends Command { @@ -45,6 +48,7 @@ class CorrectsAccountTypes extends Command private int $count; private array $expected; private AccountFactory $factory; + private AccountRepositoryInterface $repository; /** * Execute the console command. @@ -110,7 +114,7 @@ class CorrectsAccountTypes extends Command if ($resultSet->count() > 0) { $this->friendlyLine(sprintf('Found %d journals that need to be fixed.', $resultSet->count())); foreach ($resultSet as $entry) { - app('log')->debug(sprintf('Now fixing journal #%d', $entry->id)); + Log::debug(sprintf('Now fixing journal #%d', $entry->id)); /** @var null|TransactionJournal $journal */ $journal = TransactionJournal::find($entry->id); @@ -120,7 +124,7 @@ class CorrectsAccountTypes extends Command } } if (0 !== $this->count) { - app('log')->debug(sprintf('%d journals had to be fixed.', $this->count)); + Log::debug(sprintf('%d journals had to be fixed.', $this->count)); $this->friendlyInfo(sprintf('Acted on %d transaction(s)', $this->count)); } @@ -134,10 +138,10 @@ class CorrectsAccountTypes extends Command private function inspectJournal(TransactionJournal $journal): void { - app('log')->debug(sprintf('Now inspecting journal #%d', $journal->id)); + Log::debug(sprintf('Now inspecting journal #%d', $journal->id)); $transactions = $journal->transactions()->count(); if (2 !== $transactions) { - app('log')->debug(sprintf('Journal has %d transactions, so can\'t fix.', $transactions)); + Log::debug(sprintf('Journal has %d transactions, so can\'t fix.', $transactions)); $this->friendlyError(sprintf('Cannot inspect transaction journal #%d because it has %d transaction(s) instead of 2.', $journal->id, $transactions)); return; @@ -151,20 +155,20 @@ class CorrectsAccountTypes extends Command $destAccountType = $destAccount->accountType->type; if (!array_key_exists($type, $this->expected)) { - app('log')->info(sprintf('No source/destination info for transaction type %s.', $type)); + Log::info(sprintf('No source/destination info for transaction type %s.', $type)); $this->friendlyError(sprintf('No source/destination info for transaction type %s.', $type)); return; } if (!array_key_exists($sourceAccountType, $this->expected[$type])) { - app('log')->debug(sprintf('[a] Going to fix journal #%d', $journal->id)); + Log::debug(sprintf('[a] Going to fix journal #%d', $journal->id)); $this->fixJournal($journal, $type, $sourceTransaction, $destTransaction); return; } $expectedTypes = $this->expected[$type][$sourceAccountType]; if (!in_array($destAccountType, $expectedTypes, true)) { - app('log')->debug(sprintf('[b] Going to fix journal #%d', $journal->id)); + Log::debug(sprintf('[b] Going to fix journal #%d', $journal->id)); $this->fixJournal($journal, $type, $sourceTransaction, $destTransaction); } } @@ -181,13 +185,15 @@ class CorrectsAccountTypes extends Command private function fixJournal(TransactionJournal $journal, string $transactionType, Transaction $source, Transaction $dest): void { - app('log')->debug(sprintf('Going to fix journal #%d', $journal->id)); + Log::debug(sprintf('Going to fix journal #%d', $journal->id)); + $this->repository = app(AccountRepositoryInterface::class); + $this->repository->setUser($journal->user); ++$this->count; // variables: $sourceType = $source->account->accountType->type; $destinationType = $dest->account->accountType->type; $combination = sprintf('%s%s%s', $transactionType, $source->account->accountType->type, $dest->account->accountType->type); - app('log')->debug(sprintf('Combination is "%s"', $combination)); + Log::debug(sprintf('Combination is "%s"', $combination)); if ($this->shouldBeTransfer($transactionType, $sourceType, $destinationType)) { $this->makeTransfer($journal); @@ -220,10 +226,10 @@ class CorrectsAccountTypes extends Command return; } if (!$canCreateSource && !$hasValidSource) { - app('log')->debug('This transaction type has no source we can create. Just give error.'); + Log::debug('This transaction type has no source we can create. Just give error.'); $message = sprintf('The source account of %s #%d cannot be of type "%s". Firefly III cannot fix this. You may have to remove the transaction yourself.', $transactionType, $journal->id, $source->account->accountType->type); $this->friendlyError($message); - app('log')->debug($message); + Log::debug($message); return; } @@ -232,16 +238,24 @@ class CorrectsAccountTypes extends Command $validDestinations = $this->expected[$transactionType][$sourceType] ?? []; $canCreateDestination = $this->canCreateDestination($validDestinations); $hasValidDestination = $this->hasValidAccountType($validDestinations, $destinationType); + $alternativeDestination = $this->repository->findByName($dest->account->name, $validDestinations); if (!$hasValidDestination && $canCreateDestination) { $this->giveNewExpense($journal, $dest); return; } - if (!$canCreateDestination && !$hasValidDestination) { - app('log')->debug('This transaction type has no destination we can create. Just give error.'); + if (!$canCreateDestination && !$hasValidDestination && null === $alternativeDestination) { + Log::debug('This transaction type has no destination we can create. Just give error.'); $message = sprintf('The destination account of %s #%d cannot be of type "%s". Firefly III cannot fix this. You may have to remove the transaction yourself.', $transactionType, $journal->id, $dest->account->accountType->type); $this->friendlyError($message); - app('log')->debug($message); + Log::debug($message); + } + if (!$canCreateDestination && !$hasValidDestination && null !== $alternativeDestination) { + Log::debug('This transaction type has no destination we can create, but found alternative with the same name.'); + $message = sprintf('The destination account of %s #%d cannot be of type "%s". Firefly III found an alternative account. Please make sure this transaction is correct.', $transactionType, $journal->transaction_group_id, $dest->account->accountType->type); + $this->friendlyInfo($message); + Log::debug($message); + $this->giveNewDestinationAccount($journal, $alternativeDestination); } } @@ -263,7 +277,7 @@ class CorrectsAccountTypes extends Command $journal->save(); $message = sprintf('Converted transaction #%d from a transfer to a withdrawal.', $journal->id); $this->friendlyInfo($message); - app('log')->debug($message); + Log::debug($message); // check it again: $this->inspectJournal($journal); } @@ -281,7 +295,7 @@ class CorrectsAccountTypes extends Command $journal->save(); $message = sprintf('Converted transaction #%d from a transfer to a deposit.', $journal->id); $this->friendlyInfo($message); - app('log')->debug($message); + Log::debug($message); // check it again: $this->inspectJournal($journal); } @@ -308,7 +322,7 @@ class CorrectsAccountTypes extends Command $result->name ); $this->friendlyWarning($message); - app('log')->debug($message); + Log::debug($message); $this->inspectJournal($journal); } @@ -335,7 +349,7 @@ class CorrectsAccountTypes extends Command $result->name ); $this->friendlyWarning($message); - app('log')->debug($message); + Log::debug($message); $this->inspectJournal($journal); } @@ -354,14 +368,14 @@ class CorrectsAccountTypes extends Command private function giveNewRevenue(TransactionJournal $journal, Transaction $source): void { - app('log')->debug(sprintf('An account of type "%s" could be a valid source.', AccountTypeEnum::REVENUE->value)); + Log::debug(sprintf('An account of type "%s" could be a valid source.', AccountTypeEnum::REVENUE->value)); $this->factory->setUser($journal->user); $name = $source->account->name; $newSource = $this->factory->findOrCreate($name, AccountTypeEnum::REVENUE->value); $source->account()->associate($newSource); $source->save(); $this->friendlyPositive(sprintf('Firefly III gave transaction #%d a new source %s: #%d ("%s").', $journal->transaction_group_id, AccountTypeEnum::REVENUE->value, $newSource->id, $newSource->name)); - app('log')->debug(sprintf('Associated account #%d with transaction #%d', $newSource->id, $source->id)); + Log::debug(sprintf('Associated account #%d with transaction #%d', $newSource->id, $source->id)); $this->inspectJournal($journal); } @@ -372,14 +386,33 @@ class CorrectsAccountTypes extends Command private function giveNewExpense(TransactionJournal $journal, Transaction $destination): void { - app('log')->debug(sprintf('An account of type "%s" could be a valid destination.', AccountTypeEnum::EXPENSE->value)); + Log::debug(sprintf('An account of type "%s" could be a valid destination.', AccountTypeEnum::EXPENSE->value)); $this->factory->setUser($journal->user); $name = $destination->account->name; $newDestination = $this->factory->findOrCreate($name, AccountTypeEnum::EXPENSE->value); $destination->account()->associate($newDestination); $destination->save(); $this->friendlyPositive(sprintf('Firefly III gave transaction #%d a new destination %s: #%d ("%s").', $journal->transaction_group_id, AccountTypeEnum::EXPENSE->value, $newDestination->id, $newDestination->name)); - app('log')->debug(sprintf('Associated account #%d with transaction #%d', $newDestination->id, $destination->id)); + Log::debug(sprintf('Associated account #%d with transaction #%d', $newDestination->id, $destination->id)); $this->inspectJournal($journal); } + + private function giveNewDestinationAccount(TransactionJournal $journal, Account $newDestination): void + { + $destTransaction = $this->getDestinationTransaction($journal); + $oldDest = $destTransaction->account; + $destTransaction->account_id = $newDestination->id; + $destTransaction->save(); + $message = sprintf( + 'Transaction journal #%d, destination account changed from #%d ("%s") to #%d ("%s").', + $journal->id, + $oldDest->id, + $oldDest->name, + $newDestination->id, + $newDestination->name + ); + $this->friendlyInfo($message); + $journal->refresh(); + Log::debug($message); + } } From acc3c294d8bb9489aa382cc58044c241f152ca3e Mon Sep 17 00:00:00 2001 From: James Cole Date: Wed, 17 Sep 2025 20:46:03 +0200 Subject: [PATCH 20/58] Fix #10924 --- resources/views/recurring/edit.twig | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/resources/views/recurring/edit.twig b/resources/views/recurring/edit.twig index 725337de76..37df0d26b6 100644 --- a/resources/views/recurring/edit.twig +++ b/resources/views/recurring/edit.twig @@ -134,9 +134,9 @@ {# BILL ONLY WHEN CREATING A WITHDRAWAL #} {% if bills|length > 1 %} - {{ ExpandedForm.select('bill_id', bills, array.transactions[0].bill_id) }} + {{ ExpandedForm.select('bill_id', bills, array.transactions[0].subscription_id) }} {% else %} - {{ ExpandedForm.select('bill_id', bills, array.transactions[0].bill_id, {helpText: trans('firefly.no_bill_pointer', {link: route('subscriptions.index')})}) }} + {{ ExpandedForm.select('bill_id', bills, array.transactions[0].subscription_id, {helpText: trans('firefly.no_bill_pointer', {link: route('subscriptions.index')})}) }} {% endif %} {# TAGS #} From 8a062983856b7613bc2642542185b774fd867c26 Mon Sep 17 00:00:00 2001 From: James Cole Date: Sun, 21 Sep 2025 07:35:46 +0200 Subject: [PATCH 21/58] Repair charts and balances. --- .../assets/v2/src/pages/dashboard/accounts.js | 15 +++++--------- .../v2/src/pages/dashboard/categories.js | 20 +++++++++++-------- .../partials/dashboard/account-list.blade.php | 12 +++-------- 3 files changed, 20 insertions(+), 27 deletions(-) diff --git a/resources/assets/v2/src/pages/dashboard/accounts.js b/resources/assets/v2/src/pages/dashboard/accounts.js index 5aab5eba4d..1361b0008c 100644 --- a/resources/assets/v2/src/pages/dashboard/accounts.js +++ b/resources/assets/v2/src/pages/dashboard/accounts.js @@ -211,14 +211,6 @@ export default () => ({ (new Get).show(accountId, new Date(window.store.get('end'))).then((response) => { let parent = response.data.data; - // apply function to each element of parent: - // parent.attributes.balances = parent.attributes.balances.map((balance) => { - // balance.amount_formatted = formatMoney(balance.amount, balance.currency_code); - // return balance; - // }); - // console.log(parent); - - // get groups for account: const params = { page: 1, @@ -261,11 +253,14 @@ export default () => ({ accounts.push({ name: parent.attributes.name, order: parent.attributes.order, + + current_balance: formatMoney(parent.attributes.current_balance, parent.attributes.currency_code), + pc_current_balance: null === parent.attributes.pc_current_balance ? null : formatMoney(parent.attributes.pc_current_balance, parent.attributes.primary_currency_code), + id: parent.id, - balances: parent.attributes.balances, + //balances: parent.attributes.balances, groups: groups, }); - // console.log(parent.attributes); count++; if (count === totalAccounts) { accounts.sort((a, b) => a.order - b.order); // b - a for reverse sort diff --git a/resources/assets/v2/src/pages/dashboard/categories.js b/resources/assets/v2/src/pages/dashboard/categories.js index d75da3acfb..9af9c42d75 100644 --- a/resources/assets/v2/src/pages/dashboard/categories.js +++ b/resources/assets/v2/src/pages/dashboard/categories.js @@ -64,13 +64,17 @@ export default () => ({ } } } - // loop data again to add amounts to each series. for (const i in data) { if (data.hasOwnProperty(i)) { let yAxis = 'y'; let current = data[i]; + + // allow switch to primary currency. let code = current.currency_code; + if(this.convertToPrimary) { + code = current.primary_currency_code; + } // loop series, add 0 if not present or add actual amount. for (const ii in series) { @@ -78,8 +82,11 @@ export default () => ({ let amount = 0.0; if (code === ii) { // this series' currency matches this column's currency. - amount = parseFloat(current.amount); - yAxis = 'y' + current.currency_code; + amount = parseFloat(current.entries.spent); + if(this.convertToPrimary) { + amount = parseFloat(current.entries.pc_entries.spent); + } + yAxis = 'y' + code; } if (series[ii].data.hasOwnProperty(current.label)) { // there is a value for this particular currency. The amount from this column will be added. @@ -103,7 +110,6 @@ export default () => ({ // loop the series and create ChartJS-compatible data sets. let count = 0; for (const i in series) { - // console.log('series'); let yAxisID = 'y' + i; let dataset = { label: i, @@ -148,16 +154,15 @@ export default () => ({ const end = new Date(window.store.get('end')); const cacheKey = getCacheKey('ds_ct_chart', {convertToPrimary: this.convertToPrimary, start: start, end: end}); - const cacheValid = window.store.get('cacheValid'); + // const cacheValid = window.store.get('cacheValid'); + const cacheValid = false; let cachedData = window.store.get(cacheKey); - if (cacheValid && typeof cachedData !== 'undefined') { chartData = cachedData; // save chart data for later. this.drawChart(this.generateOptions(chartData)); this.loading = false; return; } - const dashboard = new Dashboard(); dashboard.dashboard(start, end, null).then((response) => { chartData = response.data; // save chart data for later. @@ -181,7 +186,6 @@ export default () => ({ this.getFreshData(); }, init() { - // console.log('categories init'); Promise.all([getVariable('convert_to_primary', false),]).then((values) => { this.convertToPrimary = values[0]; afterPromises = true; diff --git a/resources/views/v2/partials/dashboard/account-list.blade.php b/resources/views/v2/partials/dashboard/account-list.blade.php index 9934551588..55170d2466 100644 --- a/resources/views/v2/partials/dashboard/account-list.blade.php +++ b/resources/views/v2/partials/dashboard/account-list.blade.php @@ -9,16 +9,10 @@

- - + - + () +

From 69cae3ae550706fda705876d49f8be7ef1ea8af7 Mon Sep 17 00:00:00 2001 From: James Cole Date: Sun, 21 Sep 2025 08:13:13 +0200 Subject: [PATCH 22/58] Fix autocomplete. --- .../assets/v2/src/pages/transactions/edit.js | 15 ++++++--------- .../pages/transactions/shared/load-currencies.js | 8 ++++---- 2 files changed, 10 insertions(+), 13 deletions(-) diff --git a/resources/assets/v2/src/pages/transactions/edit.js b/resources/assets/v2/src/pages/transactions/edit.js index e95eca4ca4..cae648d243 100644 --- a/resources/assets/v2/src/pages/transactions/edit.js +++ b/resources/assets/v2/src/pages/transactions/edit.js @@ -72,8 +72,6 @@ let transactions = function () { resetButton: true, rulesButton: true, webhooksButton: true, - - }, // form behaviour during transaction @@ -85,7 +83,7 @@ let transactions = function () { // form data (except transactions) is stored in formData formData: { - defaultCurrency: null, + primaryCurrency: null, enabledCurrencies: [], primaryCurrencies: [], foreignCurrencies: [], @@ -200,8 +198,7 @@ let transactions = function () { // addedSplit, is called from the HTML // for source account const renderAccount = function (item, b, c) { - console.log(item); - return item.title + '
' + i18next.t('firefly.account_type_' + item.meta.type) + ''; + return item.name_with_balance + '
' + i18next.t('firefly.account_type_' + item.type) + ''; }; addAutocomplete({ selector: 'input.ac-source', @@ -209,7 +206,7 @@ let transactions = function () { account_types: this.filters.source, onRenderItem: renderAccount, valueField: 'id', - labelField: 'title', + labelField: 'name', onChange: changeSourceAccount, onSelectItem: selectSourceAccount }); @@ -217,7 +214,7 @@ let transactions = function () { selector: 'input.ac-dest', serverUrl: urls.account, valueField: 'id', - labelField: 'title', + labelField: 'name', account_types: this.filters.destination, onRenderItem: renderAccount, onChange: changeDestinationAccount, @@ -227,7 +224,7 @@ let transactions = function () { selector: 'input.ac-category', serverUrl: urls.category, valueField: 'id', - labelField: 'title', + labelField: 'name', onChange: changeCategory, onSelectItem: changeCategory }); @@ -330,7 +327,7 @@ let transactions = function () { // load meta data. loadCurrencies().then(data => { this.formStates.loadingCurrencies = false; - this.formData.defaultCurrency = data.defaultCurrency; + this.formData.primaryCurrency = data.primaryCurrency; this.formData.enabledCurrencies = data.enabledCurrencies; this.formData.primaryCurrencies = data.primaryCurrencies; this.formData.foreignCurrencies = data.foreignCurrencies; diff --git a/resources/assets/v2/src/pages/transactions/shared/load-currencies.js b/resources/assets/v2/src/pages/transactions/shared/load-currencies.js index 11c05baf67..2580a1389b 100644 --- a/resources/assets/v2/src/pages/transactions/shared/load-currencies.js +++ b/resources/assets/v2/src/pages/transactions/shared/load-currencies.js @@ -28,7 +28,7 @@ export function loadCurrencies() { let getter = new Get(); return getter.list(params).then((response) => { let returnData = { - defaultCurrency: {}, + primaryCurrency: {}, primaryCurrencies: [], foreignCurrencies: [], enabledCurrencies: [], @@ -46,13 +46,13 @@ export function loadCurrencies() { id: current.id, name: current.attributes.name, code: current.attributes.code, - default: current.attributes.default, + primary: current.attributes.primary, symbol: current.attributes.symbol, decimal_places: current.attributes.decimal_places, }; - if (obj.default) { - returnData.defaultCurrency = obj; + if (obj.primary) { + returnData.primaryCurrency = obj; } returnData.enabledCurrencies.push(obj); returnData.primaryCurrencies.push(obj); From 8c0ee8f024bbc690995274de307fda90752c5ead Mon Sep 17 00:00:00 2001 From: JC5 Date: Sun, 21 Sep 2025 08:28:09 +0200 Subject: [PATCH 23/58] =?UTF-8?q?=F0=9F=A4=96=20Auto=20commit=20for=20rele?= =?UTF-8?q?ase=20'develop'=20on=202025-09-21?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Correction/CorrectsAccountTypes.php | 28 +- app/Models/CurrencyExchangeRate.php | 2 +- .../Internal/Support/JournalServiceTrait.php | 12 +- app/Validation/Account/DepositValidation.php | 2 +- composer.lock | 74 +-- config/firefly.php | 4 +- package-lock.json | 433 +++++++++--------- resources/assets/v1/src/locales/cs.json | 2 +- resources/assets/v1/src/locales/it.json | 22 +- 9 files changed, 299 insertions(+), 280 deletions(-) diff --git a/app/Console/Commands/Correction/CorrectsAccountTypes.php b/app/Console/Commands/Correction/CorrectsAccountTypes.php index dd0e83e889..f554197c18 100644 --- a/app/Console/Commands/Correction/CorrectsAccountTypes.php +++ b/app/Console/Commands/Correction/CorrectsAccountTypes.php @@ -186,13 +186,13 @@ class CorrectsAccountTypes extends Command private function fixJournal(TransactionJournal $journal, string $transactionType, Transaction $source, Transaction $dest): void { Log::debug(sprintf('Going to fix journal #%d', $journal->id)); - $this->repository = app(AccountRepositoryInterface::class); + $this->repository = app(AccountRepositoryInterface::class); $this->repository->setUser($journal->user); ++$this->count; // variables: - $sourceType = $source->account->accountType->type; - $destinationType = $dest->account->accountType->type; - $combination = sprintf('%s%s%s', $transactionType, $source->account->accountType->type, $dest->account->accountType->type); + $sourceType = $source->account->accountType->type; + $destinationType = $dest->account->accountType->type; + $combination = sprintf('%s%s%s', $transactionType, $source->account->accountType->type, $dest->account->accountType->type); Log::debug(sprintf('Combination is "%s"', $combination)); if ($this->shouldBeTransfer($transactionType, $sourceType, $destinationType)) { @@ -217,9 +217,9 @@ class CorrectsAccountTypes extends Command } // transaction has no valid source. - $validSources = array_keys($this->expected[$transactionType]); - $canCreateSource = $this->canCreateSource($validSources); - $hasValidSource = $this->hasValidAccountType($validSources, $sourceType); + $validSources = array_keys($this->expected[$transactionType]); + $canCreateSource = $this->canCreateSource($validSources); + $hasValidSource = $this->hasValidAccountType($validSources, $sourceType); if (!$hasValidSource && $canCreateSource) { $this->giveNewRevenue($journal, $source); @@ -235,9 +235,9 @@ class CorrectsAccountTypes extends Command } /** @var array $validDestinations */ - $validDestinations = $this->expected[$transactionType][$sourceType] ?? []; - $canCreateDestination = $this->canCreateDestination($validDestinations); - $hasValidDestination = $this->hasValidAccountType($validDestinations, $destinationType); + $validDestinations = $this->expected[$transactionType][$sourceType] ?? []; + $canCreateDestination = $this->canCreateDestination($validDestinations); + $hasValidDestination = $this->hasValidAccountType($validDestinations, $destinationType); $alternativeDestination = $this->repository->findByName($dest->account->name, $validDestinations); if (!$hasValidDestination && $canCreateDestination) { $this->giveNewExpense($journal, $dest); @@ -255,7 +255,7 @@ class CorrectsAccountTypes extends Command $message = sprintf('The destination account of %s #%d cannot be of type "%s". Firefly III found an alternative account. Please make sure this transaction is correct.', $transactionType, $journal->transaction_group_id, $dest->account->accountType->type); $this->friendlyInfo($message); Log::debug($message); - $this->giveNewDestinationAccount($journal, $alternativeDestination); + $this->giveNewDestinationAccount($journal, $alternativeDestination); } } @@ -399,11 +399,11 @@ class CorrectsAccountTypes extends Command private function giveNewDestinationAccount(TransactionJournal $journal, Account $newDestination): void { - $destTransaction = $this->getDestinationTransaction($journal); - $oldDest = $destTransaction->account; + $destTransaction = $this->getDestinationTransaction($journal); + $oldDest = $destTransaction->account; $destTransaction->account_id = $newDestination->id; $destTransaction->save(); - $message = sprintf( + $message = sprintf( 'Transaction journal #%d, destination account changed from #%d ("%s") to #%d ("%s").', $journal->id, $oldDest->id, diff --git a/app/Models/CurrencyExchangeRate.php b/app/Models/CurrencyExchangeRate.php index 7edec3b797..a1bfa4a013 100644 --- a/app/Models/CurrencyExchangeRate.php +++ b/app/Models/CurrencyExchangeRate.php @@ -38,7 +38,7 @@ class CurrencyExchangeRate extends Model use ReturnsIntegerUserIdTrait; use SoftDeletes; - protected $fillable = ['user_id','user_group_id', 'from_currency_id', 'to_currency_id', 'date', 'date_tz', 'rate']; + protected $fillable = ['user_id', 'user_group_id', 'from_currency_id', 'to_currency_id', 'date', 'date_tz', 'rate']; public function fromCurrency(): BelongsTo { diff --git a/app/Services/Internal/Support/JournalServiceTrait.php b/app/Services/Internal/Support/JournalServiceTrait.php index 9d7cedad90..8cd15203ca 100644 --- a/app/Services/Internal/Support/JournalServiceTrait.php +++ b/app/Services/Internal/Support/JournalServiceTrait.php @@ -123,8 +123,9 @@ trait JournalServiceTrait if (null !== $search && in_array($search->accountType->type, $types, true)) { Log::debug(sprintf('Found "account_id" object: #%d, "%s" of type %s (1)', $search->id, $search->name, $search->accountType->type)); - if($opposite?->id === $search->id) { + if ($opposite?->id === $search->id) { Log::debug(sprintf('Account #%d is the same as opposite account #%d, returning NULL.', $search->id, $opposite->id)); + return null; } @@ -163,8 +164,9 @@ trait JournalServiceTrait if (null !== $result) { Log::debug(sprintf('Found "account_iban" object: #%d, %s', $result->id, $result->name)); - if($opposite?->id === $result->id) { + if ($opposite?->id === $result->id) { Log::debug(sprintf('Account #%d is the same as opposite account #%d, returning NULL.', $result->id, $opposite->id)); + return null; } @@ -196,8 +198,9 @@ trait JournalServiceTrait if (null !== $result) { Log::debug(sprintf('Found account: #%d, %s', $result->id, $result->name)); - if($opposite?->id === $result->id) { + if ($opposite?->id === $result->id) { Log::debug(sprintf('Account #%d is the same as opposite account #%d, returning NULL.', $result->id, $opposite->id)); + return null; } @@ -231,8 +234,9 @@ trait JournalServiceTrait if (null !== $result) { Log::debug(sprintf('Found "account_name" object: #%d, %s', $result->id, $result->name)); - if($opposite?->id === $result->id) { + if ($opposite?->id === $result->id) { Log::debug(sprintf('Account #%d is the same as opposite account #%d, returning NULL.', $result->id, $opposite->id)); + return null; } diff --git a/app/Validation/Account/DepositValidation.php b/app/Validation/Account/DepositValidation.php index 42a38447b6..36cd7e7d35 100644 --- a/app/Validation/Account/DepositValidation.php +++ b/app/Validation/Account/DepositValidation.php @@ -133,7 +133,7 @@ trait DepositValidation Log::debug(sprintf('Firefly III does not accept ID #%d as valid account data.', $accountId)); // #10921 Set result false $this->sourceError = (string) trans('validation.withdrawal_source_bad_data', ['id' => $accountId, 'name' => $accountName]); - $result = false; + $result = false; } if (null !== $search && in_array($search->accountType->type, $validTypes, true)) { Log::debug('ID result is not null and seems valid, save as source account.'); diff --git a/composer.lock b/composer.lock index ac2158c967..dce4114021 100644 --- a/composer.lock +++ b/composer.lock @@ -1878,16 +1878,16 @@ }, { "name": "laravel/framework", - "version": "v12.29.0", + "version": "v12.30.1", "source": { "type": "git", "url": "https://github.com/laravel/framework.git", - "reference": "a9e4c73086f5ba38383e9c1d74b84fe46aac730b" + "reference": "7f61e8679f9142f282a0184ac7ef9e3834bfd023" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/framework/zipball/a9e4c73086f5ba38383e9c1d74b84fe46aac730b", - "reference": "a9e4c73086f5ba38383e9c1d74b84fe46aac730b", + "url": "https://api.github.com/repos/laravel/framework/zipball/7f61e8679f9142f282a0184ac7ef9e3834bfd023", + "reference": "7f61e8679f9142f282a0184ac7ef9e3834bfd023", "shasum": "" }, "require": { @@ -1915,7 +1915,7 @@ "monolog/monolog": "^3.0", "nesbot/carbon": "^3.8.4", "nunomaduro/termwind": "^2.0", - "phiki/phiki": "v2.0.0", + "phiki/phiki": "^2.0.0", "php": "^8.2", "psr/container": "^1.1.1|^2.0.1", "psr/log": "^1.0|^2.0|^3.0", @@ -2094,7 +2094,7 @@ "issues": "https://github.com/laravel/framework/issues", "source": "https://github.com/laravel/framework" }, - "time": "2025-09-16T14:15:03+00:00" + "time": "2025-09-18T21:07:07+00:00" }, { "name": "laravel/passport", @@ -4352,16 +4352,16 @@ }, { "name": "phiki/phiki", - "version": "v2.0.0", + "version": "v2.0.3", "source": { "type": "git", "url": "https://github.com/phikiphp/phiki.git", - "reference": "461f6dd7e91dc3a95463b42f549ac7d0aab4702f" + "reference": "fe51fe6dc31856cd776fd1b04ee74053a4271644" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phikiphp/phiki/zipball/461f6dd7e91dc3a95463b42f549ac7d0aab4702f", - "reference": "461f6dd7e91dc3a95463b42f549ac7d0aab4702f", + "url": "https://api.github.com/repos/phikiphp/phiki/zipball/fe51fe6dc31856cd776fd1b04ee74053a4271644", + "reference": "fe51fe6dc31856cd776fd1b04ee74053a4271644", "shasum": "" }, "require": { @@ -4407,7 +4407,7 @@ "description": "Syntax highlighting using TextMate grammars in PHP.", "support": { "issues": "https://github.com/phikiphp/phiki/issues", - "source": "https://github.com/phikiphp/phiki/tree/v2.0.0" + "source": "https://github.com/phikiphp/phiki/tree/v2.0.3" }, "funding": [ { @@ -4419,7 +4419,7 @@ "type": "other" } ], - "time": "2025-08-28T18:20:27+00:00" + "time": "2025-09-19T11:50:41+00:00" }, { "name": "php-http/client-common", @@ -5045,21 +5045,21 @@ }, { "name": "pragmarx/google2fa-qrcode", - "version": "v3.0.0", + "version": "v3.0.1", "source": { "type": "git", "url": "https://github.com/antonioribeiro/google2fa-qrcode.git", - "reference": "ce4d8a729b6c93741c607cfb2217acfffb5bf76b" + "reference": "c23ebcc3a50de0d1566016a6dd1486e183bb78e1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/antonioribeiro/google2fa-qrcode/zipball/ce4d8a729b6c93741c607cfb2217acfffb5bf76b", - "reference": "ce4d8a729b6c93741c607cfb2217acfffb5bf76b", + "url": "https://api.github.com/repos/antonioribeiro/google2fa-qrcode/zipball/c23ebcc3a50de0d1566016a6dd1486e183bb78e1", + "reference": "c23ebcc3a50de0d1566016a6dd1486e183bb78e1", "shasum": "" }, "require": { "php": ">=7.1", - "pragmarx/google2fa": ">=4.0" + "pragmarx/google2fa": "^4.0|^5.0|^6.0|^7.0|^8.0" }, "require-dev": { "bacon/bacon-qr-code": "^2.0", @@ -5106,9 +5106,9 @@ ], "support": { "issues": "https://github.com/antonioribeiro/google2fa-qrcode/issues", - "source": "https://github.com/antonioribeiro/google2fa-qrcode/tree/v3.0.0" + "source": "https://github.com/antonioribeiro/google2fa-qrcode/tree/v3.0.1" }, - "time": "2021-08-15T12:53:48+00:00" + "time": "2025-09-19T23:02:26+00:00" }, { "name": "pragmarx/random", @@ -10810,16 +10810,16 @@ }, { "name": "larastan/larastan", - "version": "v3.7.1", + "version": "v3.7.2", "source": { "type": "git", "url": "https://github.com/larastan/larastan.git", - "reference": "2e653fd19585a825e283b42f38378b21ae481cc7" + "reference": "a761859a7487bd7d0cb8b662a7538a234d5bb5ae" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/larastan/larastan/zipball/2e653fd19585a825e283b42f38378b21ae481cc7", - "reference": "2e653fd19585a825e283b42f38378b21ae481cc7", + "url": "https://api.github.com/repos/larastan/larastan/zipball/a761859a7487bd7d0cb8b662a7538a234d5bb5ae", + "reference": "a761859a7487bd7d0cb8b662a7538a234d5bb5ae", "shasum": "" }, "require": { @@ -10833,7 +10833,7 @@ "illuminate/pipeline": "^11.44.2 || ^12.4.1", "illuminate/support": "^11.44.2 || ^12.4.1", "php": "^8.2", - "phpstan/phpstan": "^2.1.23" + "phpstan/phpstan": "^2.1.28" }, "require-dev": { "doctrine/coding-standard": "^13", @@ -10887,7 +10887,7 @@ ], "support": { "issues": "https://github.com/larastan/larastan/issues", - "source": "https://github.com/larastan/larastan/tree/v3.7.1" + "source": "https://github.com/larastan/larastan/tree/v3.7.2" }, "funding": [ { @@ -10895,7 +10895,7 @@ "type": "github" } ], - "time": "2025-09-10T19:42:11+00:00" + "time": "2025-09-19T09:03:05+00:00" }, { "name": "laravel-json-api/testing", @@ -11404,16 +11404,16 @@ }, { "name": "phpstan/phpstan", - "version": "2.1.26", + "version": "2.1.28", "source": { "type": "git", "url": "https://github.com/phpstan/phpstan.git", - "reference": "b13345001a8553ec405b7741be0c6b8d7f8b5bf5" + "reference": "578fa296a166605d97b94091f724f1257185d278" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan/zipball/b13345001a8553ec405b7741be0c6b8d7f8b5bf5", - "reference": "b13345001a8553ec405b7741be0c6b8d7f8b5bf5", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/578fa296a166605d97b94091f724f1257185d278", + "reference": "578fa296a166605d97b94091f724f1257185d278", "shasum": "" }, "require": { @@ -11458,7 +11458,7 @@ "type": "github" } ], - "time": "2025-09-16T11:33:46+00:00" + "time": "2025-09-19T08:58:49+00:00" }, { "name": "phpstan/phpstan-deprecation-rules", @@ -11557,16 +11557,16 @@ }, { "name": "phpunit/php-code-coverage", - "version": "12.3.7", + "version": "12.3.8", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-code-coverage.git", - "reference": "bbede0f5593dad37af3be6a6f8e6ae1885e8a0a9" + "reference": "99e692c6a84708211f7536ba322bbbaef57ac7fc" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/bbede0f5593dad37af3be6a6f8e6ae1885e8a0a9", - "reference": "bbede0f5593dad37af3be6a6f8e6ae1885e8a0a9", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/99e692c6a84708211f7536ba322bbbaef57ac7fc", + "reference": "99e692c6a84708211f7536ba322bbbaef57ac7fc", "shasum": "" }, "require": { @@ -11622,7 +11622,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/php-code-coverage/issues", "security": "https://github.com/sebastianbergmann/php-code-coverage/security/policy", - "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/12.3.7" + "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/12.3.8" }, "funding": [ { @@ -11642,7 +11642,7 @@ "type": "tidelift" } ], - "time": "2025-09-10T09:59:06+00:00" + "time": "2025-09-17T11:31:43+00:00" }, { "name": "phpunit/php-file-iterator", diff --git a/config/firefly.php b/config/firefly.php index d5c69546c3..6bbe1f0ea2 100644 --- a/config/firefly.php +++ b/config/firefly.php @@ -78,8 +78,8 @@ return [ 'running_balance_column' => env('USE_RUNNING_BALANCE', false), // see cer.php for exchange rates feature flag. ], - 'version' => 'develop/2025-09-16', - 'build_time' => 1758048808, + 'version' => 'develop/2025-09-21', + 'build_time' => 1758435980, 'api_version' => '2.1.0', // field is no longer used. 'db_version' => 26, diff --git a/package-lock.json b/package-lock.json index 671dd29016..a12d8faaf8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1693,9 +1693,9 @@ "license": "MIT" }, "node_modules/@esbuild/aix-ppc64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.9.tgz", - "integrity": "sha512-OaGtL73Jck6pBKjNIe24BnFE6agGl+6KxDtTfHhy1HmhthfKouEcOhqpSL64K4/0WCtbKFLOdzD/44cJ4k9opA==", + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.10.tgz", + "integrity": "sha512-0NFWnA+7l41irNuaSVlLfgNT12caWJVLzp5eAVhZ0z1qpxbockccEt3s+149rE64VUI3Ml2zt8Nv5JVc4QXTsw==", "cpu": [ "ppc64" ], @@ -1710,9 +1710,9 @@ } }, "node_modules/@esbuild/android-arm": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.9.tgz", - "integrity": "sha512-5WNI1DaMtxQ7t7B6xa572XMXpHAaI/9Hnhk8lcxF4zVN4xstUgTlvuGDorBguKEnZO70qwEcLpfifMLoxiPqHQ==", + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.10.tgz", + "integrity": "sha512-dQAxF1dW1C3zpeCDc5KqIYuZ1tgAdRXNoZP7vkBIRtKZPYe2xVr/d3SkirklCHudW1B45tGiUlz2pUWDfbDD4w==", "cpu": [ "arm" ], @@ -1727,9 +1727,9 @@ } }, "node_modules/@esbuild/android-arm64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.9.tgz", - "integrity": "sha512-IDrddSmpSv51ftWslJMvl3Q2ZT98fUSL2/rlUXuVqRXHCs5EUF1/f+jbjF5+NG9UffUDMCiTyh8iec7u8RlTLg==", + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.10.tgz", + "integrity": "sha512-LSQa7eDahypv/VO6WKohZGPSJDq5OVOo3UoFR1E4t4Gj1W7zEQMUhI+lo81H+DtB+kP+tDgBp+M4oNCwp6kffg==", "cpu": [ "arm64" ], @@ -1744,9 +1744,9 @@ } }, "node_modules/@esbuild/android-x64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.9.tgz", - "integrity": "sha512-I853iMZ1hWZdNllhVZKm34f4wErd4lMyeV7BLzEExGEIZYsOzqDWDf+y082izYUE8gtJnYHdeDpN/6tUdwvfiw==", + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.10.tgz", + "integrity": "sha512-MiC9CWdPrfhibcXwr39p9ha1x0lZJ9KaVfvzA0Wxwz9ETX4v5CHfF09bx935nHlhi+MxhA63dKRRQLiVgSUtEg==", "cpu": [ "x64" ], @@ -1761,9 +1761,9 @@ } }, "node_modules/@esbuild/darwin-arm64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.9.tgz", - "integrity": "sha512-XIpIDMAjOELi/9PB30vEbVMs3GV1v2zkkPnuyRRURbhqjyzIINwj+nbQATh4H9GxUgH1kFsEyQMxwiLFKUS6Rg==", + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.10.tgz", + "integrity": "sha512-JC74bdXcQEpW9KkV326WpZZjLguSZ3DfS8wrrvPMHgQOIEIG/sPXEN/V8IssoJhbefLRcRqw6RQH2NnpdprtMA==", "cpu": [ "arm64" ], @@ -1778,9 +1778,9 @@ } }, "node_modules/@esbuild/darwin-x64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.9.tgz", - "integrity": "sha512-jhHfBzjYTA1IQu8VyrjCX4ApJDnH+ez+IYVEoJHeqJm9VhG9Dh2BYaJritkYK3vMaXrf7Ogr/0MQ8/MeIefsPQ==", + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.10.tgz", + "integrity": "sha512-tguWg1olF6DGqzws97pKZ8G2L7Ig1vjDmGTwcTuYHbuU6TTjJe5FXbgs5C1BBzHbJ2bo1m3WkQDbWO2PvamRcg==", "cpu": [ "x64" ], @@ -1795,9 +1795,9 @@ } }, "node_modules/@esbuild/freebsd-arm64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.9.tgz", - "integrity": "sha512-z93DmbnY6fX9+KdD4Ue/H6sYs+bhFQJNCPZsi4XWJoYblUqT06MQUdBCpcSfuiN72AbqeBFu5LVQTjfXDE2A6Q==", + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.10.tgz", + "integrity": "sha512-3ZioSQSg1HT2N05YxeJWYR+Libe3bREVSdWhEEgExWaDtyFbbXWb49QgPvFH8u03vUPX10JhJPcz7s9t9+boWg==", "cpu": [ "arm64" ], @@ -1812,9 +1812,9 @@ } }, "node_modules/@esbuild/freebsd-x64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.9.tgz", - "integrity": "sha512-mrKX6H/vOyo5v71YfXWJxLVxgy1kyt1MQaD8wZJgJfG4gq4DpQGpgTB74e5yBeQdyMTbgxp0YtNj7NuHN0PoZg==", + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.10.tgz", + "integrity": "sha512-LLgJfHJk014Aa4anGDbh8bmI5Lk+QidDmGzuC2D+vP7mv/GeSN+H39zOf7pN5N8p059FcOfs2bVlrRr4SK9WxA==", "cpu": [ "x64" ], @@ -1829,9 +1829,9 @@ } }, "node_modules/@esbuild/linux-arm": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.9.tgz", - "integrity": "sha512-HBU2Xv78SMgaydBmdor38lg8YDnFKSARg1Q6AT0/y2ezUAKiZvc211RDFHlEZRFNRVhcMamiToo7bDx3VEOYQw==", + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.10.tgz", + "integrity": "sha512-oR31GtBTFYCqEBALI9r6WxoU/ZofZl962pouZRTEYECvNF/dtXKku8YXcJkhgK/beU+zedXfIzHijSRapJY3vg==", "cpu": [ "arm" ], @@ -1846,9 +1846,9 @@ } }, "node_modules/@esbuild/linux-arm64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.9.tgz", - "integrity": "sha512-BlB7bIcLT3G26urh5Dmse7fiLmLXnRlopw4s8DalgZ8ef79Jj4aUcYbk90g8iCa2467HX8SAIidbL7gsqXHdRw==", + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.10.tgz", + "integrity": "sha512-5luJWN6YKBsawd5f9i4+c+geYiVEw20FVW5x0v1kEMWNq8UctFjDiMATBxLvmmHA4bf7F6hTRaJgtghFr9iziQ==", "cpu": [ "arm64" ], @@ -1863,9 +1863,9 @@ } }, "node_modules/@esbuild/linux-ia32": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.9.tgz", - "integrity": "sha512-e7S3MOJPZGp2QW6AK6+Ly81rC7oOSerQ+P8L0ta4FhVi+/j/v2yZzx5CqqDaWjtPFfYz21Vi1S0auHrap3Ma3A==", + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.10.tgz", + "integrity": "sha512-NrSCx2Kim3EnnWgS4Txn0QGt0Xipoumb6z6sUtl5bOEZIVKhzfyp/Lyw4C1DIYvzeW/5mWYPBFJU3a/8Yr75DQ==", "cpu": [ "ia32" ], @@ -1880,9 +1880,9 @@ } }, "node_modules/@esbuild/linux-loong64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.9.tgz", - "integrity": "sha512-Sbe10Bnn0oUAB2AalYztvGcK+o6YFFA/9829PhOCUS9vkJElXGdphz0A3DbMdP8gmKkqPmPcMJmJOrI3VYB1JQ==", + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.10.tgz", + "integrity": "sha512-xoSphrd4AZda8+rUDDfD9J6FUMjrkTz8itpTITM4/xgerAZZcFW7Dv+sun7333IfKxGG8gAq+3NbfEMJfiY+Eg==", "cpu": [ "loong64" ], @@ -1897,9 +1897,9 @@ } }, "node_modules/@esbuild/linux-mips64el": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.9.tgz", - "integrity": "sha512-YcM5br0mVyZw2jcQeLIkhWtKPeVfAerES5PvOzaDxVtIyZ2NUBZKNLjC5z3/fUlDgT6w89VsxP2qzNipOaaDyA==", + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.10.tgz", + "integrity": "sha512-ab6eiuCwoMmYDyTnyptoKkVS3k8fy/1Uvq7Dj5czXI6DF2GqD2ToInBI0SHOp5/X1BdZ26RKc5+qjQNGRBelRA==", "cpu": [ "mips64el" ], @@ -1914,9 +1914,9 @@ } }, "node_modules/@esbuild/linux-ppc64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.9.tgz", - "integrity": "sha512-++0HQvasdo20JytyDpFvQtNrEsAgNG2CY1CLMwGXfFTKGBGQT3bOeLSYE2l1fYdvML5KUuwn9Z8L1EWe2tzs1w==", + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.10.tgz", + "integrity": "sha512-NLinzzOgZQsGpsTkEbdJTCanwA5/wozN9dSgEl12haXJBzMTpssebuXR42bthOF3z7zXFWH1AmvWunUCkBE4EA==", "cpu": [ "ppc64" ], @@ -1931,9 +1931,9 @@ } }, "node_modules/@esbuild/linux-riscv64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.9.tgz", - "integrity": "sha512-uNIBa279Y3fkjV+2cUjx36xkx7eSjb8IvnL01eXUKXez/CBHNRw5ekCGMPM0BcmqBxBcdgUWuUXmVWwm4CH9kg==", + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.10.tgz", + "integrity": "sha512-FE557XdZDrtX8NMIeA8LBJX3dC2M8VGXwfrQWU7LB5SLOajfJIxmSdyL/gU1m64Zs9CBKvm4UAuBp5aJ8OgnrA==", "cpu": [ "riscv64" ], @@ -1948,9 +1948,9 @@ } }, "node_modules/@esbuild/linux-s390x": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.9.tgz", - "integrity": "sha512-Mfiphvp3MjC/lctb+7D287Xw1DGzqJPb/J2aHHcHxflUo+8tmN/6d4k6I2yFR7BVo5/g7x2Monq4+Yew0EHRIA==", + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.10.tgz", + "integrity": "sha512-3BBSbgzuB9ajLoVZk0mGu+EHlBwkusRmeNYdqmznmMc9zGASFjSsxgkNsqmXugpPk00gJ0JNKh/97nxmjctdew==", "cpu": [ "s390x" ], @@ -1965,9 +1965,9 @@ } }, "node_modules/@esbuild/linux-x64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.9.tgz", - "integrity": "sha512-iSwByxzRe48YVkmpbgoxVzn76BXjlYFXC7NvLYq+b+kDjyyk30J0JY47DIn8z1MO3K0oSl9fZoRmZPQI4Hklzg==", + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.10.tgz", + "integrity": "sha512-QSX81KhFoZGwenVyPoberggdW1nrQZSvfVDAIUXr3WqLRZGZqWk/P4T8p2SP+de2Sr5HPcvjhcJzEiulKgnxtA==", "cpu": [ "x64" ], @@ -1982,9 +1982,9 @@ } }, "node_modules/@esbuild/netbsd-arm64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.9.tgz", - "integrity": "sha512-9jNJl6FqaUG+COdQMjSCGW4QiMHH88xWbvZ+kRVblZsWrkXlABuGdFJ1E9L7HK+T0Yqd4akKNa/lO0+jDxQD4Q==", + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.10.tgz", + "integrity": "sha512-AKQM3gfYfSW8XRk8DdMCzaLUFB15dTrZfnX8WXQoOUpUBQ+NaAFCP1kPS/ykbbGYz7rxn0WS48/81l9hFl3u4A==", "cpu": [ "arm64" ], @@ -1999,9 +1999,9 @@ } }, "node_modules/@esbuild/netbsd-x64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.9.tgz", - "integrity": "sha512-RLLdkflmqRG8KanPGOU7Rpg829ZHu8nFy5Pqdi9U01VYtG9Y0zOG6Vr2z4/S+/3zIyOxiK6cCeYNWOFR9QP87g==", + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.10.tgz", + "integrity": "sha512-7RTytDPGU6fek/hWuN9qQpeGPBZFfB4zZgcz2VK2Z5VpdUxEI8JKYsg3JfO0n/Z1E/6l05n0unDCNc4HnhQGig==", "cpu": [ "x64" ], @@ -2016,9 +2016,9 @@ } }, "node_modules/@esbuild/openbsd-arm64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.9.tgz", - "integrity": "sha512-YaFBlPGeDasft5IIM+CQAhJAqS3St3nJzDEgsgFixcfZeyGPCd6eJBWzke5piZuZ7CtL656eOSYKk4Ls2C0FRQ==", + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.10.tgz", + "integrity": "sha512-5Se0VM9Wtq797YFn+dLimf2Zx6McttsH2olUBsDml+lm0GOCRVebRWUvDtkY4BWYv/3NgzS8b/UM3jQNh5hYyw==", "cpu": [ "arm64" ], @@ -2033,9 +2033,9 @@ } }, "node_modules/@esbuild/openbsd-x64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.9.tgz", - "integrity": "sha512-1MkgTCuvMGWuqVtAvkpkXFmtL8XhWy+j4jaSO2wxfJtilVCi0ZE37b8uOdMItIHz4I6z1bWWtEX4CJwcKYLcuA==", + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.10.tgz", + "integrity": "sha512-XkA4frq1TLj4bEMB+2HnI0+4RnjbuGZfet2gs/LNs5Hc7D89ZQBHQ0gL2ND6Lzu1+QVkjp3x1gIcPKzRNP8bXw==", "cpu": [ "x64" ], @@ -2050,9 +2050,9 @@ } }, "node_modules/@esbuild/openharmony-arm64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.9.tgz", - "integrity": "sha512-4Xd0xNiMVXKh6Fa7HEJQbrpP3m3DDn43jKxMjxLLRjWnRsfxjORYJlXPO4JNcXtOyfajXorRKY9NkOpTHptErg==", + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.10.tgz", + "integrity": "sha512-AVTSBhTX8Y/Fz6OmIVBip9tJzZEUcY8WLh7I59+upa5/GPhh2/aM6bvOMQySspnCCHvFi79kMtdJS1w0DXAeag==", "cpu": [ "arm64" ], @@ -2067,9 +2067,9 @@ } }, "node_modules/@esbuild/sunos-x64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.9.tgz", - "integrity": "sha512-WjH4s6hzo00nNezhp3wFIAfmGZ8U7KtrJNlFMRKxiI9mxEK1scOMAaa9i4crUtu+tBr+0IN6JCuAcSBJZfnphw==", + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.10.tgz", + "integrity": "sha512-fswk3XT0Uf2pGJmOpDB7yknqhVkJQkAQOcW/ccVOtfx05LkbWOaRAtn5SaqXypeKQra1QaEa841PgrSL9ubSPQ==", "cpu": [ "x64" ], @@ -2084,9 +2084,9 @@ } }, "node_modules/@esbuild/win32-arm64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.9.tgz", - "integrity": "sha512-mGFrVJHmZiRqmP8xFOc6b84/7xa5y5YvR1x8djzXpJBSv/UsNK6aqec+6JDjConTgvvQefdGhFDAs2DLAds6gQ==", + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.10.tgz", + "integrity": "sha512-ah+9b59KDTSfpaCg6VdJoOQvKjI33nTaQr4UluQwW7aEwZQsbMCfTmfEO4VyewOxx4RaDT/xCy9ra2GPWmO7Kw==", "cpu": [ "arm64" ], @@ -2101,9 +2101,9 @@ } }, "node_modules/@esbuild/win32-ia32": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.9.tgz", - "integrity": "sha512-b33gLVU2k11nVx1OhX3C8QQP6UHQK4ZtN56oFWvVXvz2VkDoe6fbG8TOgHFxEvqeqohmRnIHe5A1+HADk4OQww==", + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.10.tgz", + "integrity": "sha512-QHPDbKkrGO8/cz9LKVnJU22HOi4pxZnZhhA2HYHez5Pz4JeffhDjf85E57Oyco163GnzNCVkZK0b/n4Y0UHcSw==", "cpu": [ "ia32" ], @@ -2118,9 +2118,9 @@ } }, "node_modules/@esbuild/win32-x64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.9.tgz", - "integrity": "sha512-PPOl1mi6lpLNQxnGoyAfschAodRFYXJ+9fs6WHXz7CSWKbOqiMZsubC+BQsVKuul+3vKLuwTHsS2c2y9EoKwxQ==", + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.10.tgz", + "integrity": "sha512-9KpxSVFCu0iK1owoez6aC/s/EdUQLDN3adTxGCqxMVhrPDj6bt5dbrHDXUuq+Bs2vATFBBrQS5vdQ/Ed2P+nbw==", "cpu": [ "x64" ], @@ -2589,9 +2589,9 @@ } }, "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.50.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.50.2.tgz", - "integrity": "sha512-uLN8NAiFVIRKX9ZQha8wy6UUs06UNSZ32xj6giK/rmMXAgKahwExvK6SsmgU5/brh4w/nSgj8e0k3c1HBQpa0A==", + "version": "4.52.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.52.0.tgz", + "integrity": "sha512-VxDYCDqOaR7NXzAtvRx7G1u54d2kEHopb28YH/pKzY6y0qmogP3gG7CSiWsq9WvDFxOQMpNEyjVAHZFXfH3o/A==", "cpu": [ "arm" ], @@ -2603,9 +2603,9 @@ ] }, "node_modules/@rollup/rollup-android-arm64": { - "version": "4.50.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.50.2.tgz", - "integrity": "sha512-oEouqQk2/zxxj22PNcGSskya+3kV0ZKH+nQxuCCOGJ4oTXBdNTbv+f/E3c74cNLeMO1S5wVWacSws10TTSB77g==", + "version": "4.52.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.52.0.tgz", + "integrity": "sha512-pqDirm8koABIKvzL59YI9W9DWbRlTX7RWhN+auR8HXJxo89m4mjqbah7nJZjeKNTNYopqL+yGg+0mhCpf3xZtQ==", "cpu": [ "arm64" ], @@ -2617,9 +2617,9 @@ ] }, "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.50.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.50.2.tgz", - "integrity": "sha512-OZuTVTpj3CDSIxmPgGH8en/XtirV5nfljHZ3wrNwvgkT5DQLhIKAeuFSiwtbMto6oVexV0k1F1zqURPKf5rI1Q==", + "version": "4.52.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.52.0.tgz", + "integrity": "sha512-YCdWlY/8ltN6H78HnMsRHYlPiKvqKagBP1r+D7SSylxX+HnsgXGCmLiV3Y4nSyY9hW8qr8U9LDUx/Lo7M6MfmQ==", "cpu": [ "arm64" ], @@ -2631,9 +2631,9 @@ ] }, "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.50.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.50.2.tgz", - "integrity": "sha512-Wa/Wn8RFkIkr1vy1k1PB//VYhLnlnn5eaJkfTQKivirOvzu5uVd2It01ukeQstMursuz7S1bU+8WW+1UPXpa8A==", + "version": "4.52.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.52.0.tgz", + "integrity": "sha512-z4nw6y1j+OOSGzuVbSWdIp1IUks9qNw4dc7z7lWuWDKojY38VMWBlEN7F9jk5UXOkUcp97vA1N213DF+Lz8BRg==", "cpu": [ "x64" ], @@ -2645,9 +2645,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.50.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.50.2.tgz", - "integrity": "sha512-QkzxvH3kYN9J1w7D1A+yIMdI1pPekD+pWx7G5rXgnIlQ1TVYVC6hLl7SOV9pi5q9uIDF9AuIGkuzcbF7+fAhow==", + "version": "4.52.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.52.0.tgz", + "integrity": "sha512-Q/dv9Yvyr5rKlK8WQJZVrp5g2SOYeZUs9u/t2f9cQ2E0gJjYB/BWoedXfUT0EcDJefi2zzVfhcOj8drWCzTviw==", "cpu": [ "arm64" ], @@ -2659,9 +2659,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.50.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.50.2.tgz", - "integrity": "sha512-dkYXB0c2XAS3a3jmyDkX4Jk0m7gWLFzq1C3qUnJJ38AyxIF5G/dyS4N9B30nvFseCfgtCEdbYFhk0ChoCGxPog==", + "version": "4.52.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.52.0.tgz", + "integrity": "sha512-kdBsLs4Uile/fbjZVvCRcKB4q64R+1mUq0Yd7oU1CMm1Av336ajIFqNFovByipciuUQjBCPMxwJhCgfG2re3rg==", "cpu": [ "x64" ], @@ -2673,9 +2673,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.50.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.50.2.tgz", - "integrity": "sha512-9VlPY/BN3AgbukfVHAB8zNFWB/lKEuvzRo1NKev0Po8sYFKx0i+AQlCYftgEjcL43F2h9Ui1ZSdVBc4En/sP2w==", + "version": "4.52.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.52.0.tgz", + "integrity": "sha512-aL6hRwu0k7MTUESgkg7QHY6CoqPgr6gdQXRJI1/VbFlUMwsSzPGSR7sG5d+MCbYnJmJwThc2ol3nixj1fvI/zQ==", "cpu": [ "arm" ], @@ -2687,9 +2687,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.50.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.50.2.tgz", - "integrity": "sha512-+GdKWOvsifaYNlIVf07QYan1J5F141+vGm5/Y8b9uCZnG/nxoGqgCmR24mv0koIWWuqvFYnbURRqw1lv7IBINw==", + "version": "4.52.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.52.0.tgz", + "integrity": "sha512-BTs0M5s1EJejgIBJhCeiFo7GZZ2IXWkFGcyZhxX4+8usnIo5Mti57108vjXFIQmmJaRyDwmV59Tw64Ap1dkwMw==", "cpu": [ "arm" ], @@ -2701,9 +2701,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.50.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.50.2.tgz", - "integrity": "sha512-df0Eou14ojtUdLQdPFnymEQteENwSJAdLf5KCDrmZNsy1c3YaCNaJvYsEUHnrg+/DLBH612/R0xd3dD03uz2dg==", + "version": "4.52.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.52.0.tgz", + "integrity": "sha512-uj672IVOU9m08DBGvoPKPi/J8jlVgjh12C9GmjjBxCTQc3XtVmRkRKyeHSmIKQpvJ7fIm1EJieBUcnGSzDVFyw==", "cpu": [ "arm64" ], @@ -2715,9 +2715,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.50.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.50.2.tgz", - "integrity": "sha512-iPeouV0UIDtz8j1YFR4OJ/zf7evjauqv7jQ/EFs0ClIyL+by++hiaDAfFipjOgyz6y6xbDvJuiU4HwpVMpRFDQ==", + "version": "4.52.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.52.0.tgz", + "integrity": "sha512-/+IVbeDMDCtB/HP/wiWsSzduD10SEGzIZX2945KSgZRNi4TSkjHqRJtNTVtVb8IRwhJ65ssI56krlLik+zFWkw==", "cpu": [ "arm64" ], @@ -2729,9 +2729,9 @@ ] }, "node_modules/@rollup/rollup-linux-loong64-gnu": { - "version": "4.50.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.50.2.tgz", - "integrity": "sha512-OL6KaNvBopLlj5fTa5D5bau4W82f+1TyTZRr2BdnfsrnQnmdxh4okMxR2DcDkJuh4KeoQZVuvHvzuD/lyLn2Kw==", + "version": "4.52.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.52.0.tgz", + "integrity": "sha512-U1vVzvSWtSMWKKrGoROPBXMh3Vwn93TA9V35PldokHGqiUbF6erSzox/5qrSMKp6SzakvyjcPiVF8yB1xKr9Pg==", "cpu": [ "loong64" ], @@ -2743,9 +2743,9 @@ ] }, "node_modules/@rollup/rollup-linux-ppc64-gnu": { - "version": "4.50.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.50.2.tgz", - "integrity": "sha512-I21VJl1w6z/K5OTRl6aS9DDsqezEZ/yKpbqlvfHbW0CEF5IL8ATBMuUx6/mp683rKTK8thjs/0BaNrZLXetLag==", + "version": "4.52.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.52.0.tgz", + "integrity": "sha512-X/4WfuBAdQRH8cK3DYl8zC00XEE6aM472W+QCycpQJeLWVnHfkv7RyBFVaTqNUMsTgIX8ihMjCvFF9OUgeABzw==", "cpu": [ "ppc64" ], @@ -2757,9 +2757,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.50.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.50.2.tgz", - "integrity": "sha512-Hq6aQJT/qFFHrYMjS20nV+9SKrXL2lvFBENZoKfoTH2kKDOJqff5OSJr4x72ZaG/uUn+XmBnGhfr4lwMRrmqCQ==", + "version": "4.52.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.52.0.tgz", + "integrity": "sha512-xIRYc58HfWDBZoLmWfWXg2Sq8VCa2iJ32B7mqfWnkx5mekekl0tMe7FHpY8I72RXEcUkaWawRvl3qA55og+cwQ==", "cpu": [ "riscv64" ], @@ -2771,9 +2771,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-musl": { - "version": "4.50.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.50.2.tgz", - "integrity": "sha512-82rBSEXRv5qtKyr0xZ/YMF531oj2AIpLZkeNYxmKNN6I2sVE9PGegN99tYDLK2fYHJITL1P2Lgb4ZXnv0PjQvw==", + "version": "4.52.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.52.0.tgz", + "integrity": "sha512-mbsoUey05WJIOz8U1WzNdf+6UMYGwE3fZZnQqsM22FZ3wh1N887HT6jAOjXs6CNEK3Ntu2OBsyQDXfIjouI4dw==", "cpu": [ "riscv64" ], @@ -2785,9 +2785,9 @@ ] }, "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.50.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.50.2.tgz", - "integrity": "sha512-4Q3S3Hy7pC6uaRo9gtXUTJ+EKo9AKs3BXKc2jYypEcMQ49gDPFU2P1ariX9SEtBzE5egIX6fSUmbmGazwBVF9w==", + "version": "4.52.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.52.0.tgz", + "integrity": "sha512-qP6aP970bucEi5KKKR4AuPFd8aTx9EF6BvutvYxmZuWLJHmnq4LvBfp0U+yFDMGwJ+AIJEH5sIP+SNypauMWzg==", "cpu": [ "s390x" ], @@ -2799,9 +2799,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.50.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.50.2.tgz", - "integrity": "sha512-9Jie/At6qk70dNIcopcL4p+1UirusEtznpNtcq/u/C5cC4HBX7qSGsYIcG6bdxj15EYWhHiu02YvmdPzylIZlA==", + "version": "4.52.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.52.0.tgz", + "integrity": "sha512-nmSVN+F2i1yKZ7rJNKO3G7ZzmxJgoQBQZ/6c4MuS553Grmr7WqR7LLDcYG53Z2m9409z3JLt4sCOhLdbKQ3HmA==", "cpu": [ "x64" ], @@ -2813,9 +2813,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.50.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.50.2.tgz", - "integrity": "sha512-HPNJwxPL3EmhzeAnsWQCM3DcoqOz3/IC6de9rWfGR8ZCuEHETi9km66bH/wG3YH0V3nyzyFEGUZeL5PKyy4xvw==", + "version": "4.52.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.52.0.tgz", + "integrity": "sha512-2d0qRo33G6TfQVjaMR71P+yJVGODrt5V6+T0BDYH4EMfGgdC/2HWDVjSSFw888GSzAZUwuska3+zxNUCDco6rQ==", "cpu": [ "x64" ], @@ -2827,9 +2827,9 @@ ] }, "node_modules/@rollup/rollup-openharmony-arm64": { - "version": "4.50.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.50.2.tgz", - "integrity": "sha512-nMKvq6FRHSzYfKLHZ+cChowlEkR2lj/V0jYj9JnGUVPL2/mIeFGmVM2mLaFeNa5Jev7W7TovXqXIG2d39y1KYA==", + "version": "4.52.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.52.0.tgz", + "integrity": "sha512-A1JalX4MOaFAAyGgpO7XP5khquv/7xKzLIyLmhNrbiCxWpMlnsTYr8dnsWM7sEeotNmxvSOEL7F65j0HXFcFsw==", "cpu": [ "arm64" ], @@ -2841,9 +2841,9 @@ ] }, "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.50.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.50.2.tgz", - "integrity": "sha512-eFUvvnTYEKeTyHEijQKz81bLrUQOXKZqECeiWH6tb8eXXbZk+CXSG2aFrig2BQ/pjiVRj36zysjgILkqarS2YA==", + "version": "4.52.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.52.0.tgz", + "integrity": "sha512-YQugafP/rH0eOOHGjmNgDURrpYHrIX0yuojOI8bwCyXwxC9ZdTd3vYkmddPX0oHONLXu9Rb1dDmT0VNpjkzGGw==", "cpu": [ "arm64" ], @@ -2855,9 +2855,9 @@ ] }, "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.50.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.50.2.tgz", - "integrity": "sha512-cBaWmXqyfRhH8zmUxK3d3sAhEWLrtMjWBRwdMMHJIXSjvjLKvv49adxiEz+FJ8AP90apSDDBx2Tyd/WylV6ikA==", + "version": "4.52.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.52.0.tgz", + "integrity": "sha512-zYdUYhi3Qe2fndujBqL5FjAFzvNeLxtIqfzNEVKD1I7C37/chv1VxhscWSQHTNfjPCrBFQMnynwA3kpZpZ8w4A==", "cpu": [ "ia32" ], @@ -2868,10 +2868,24 @@ "win32" ] }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.52.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.52.0.tgz", + "integrity": "sha512-fGk03kQylNaCOQ96HDMeT7E2n91EqvCDd3RwvT5k+xNdFCeMGnj5b5hEgTGrQuyidqSsD3zJDQ21QIaxXqTBJw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.50.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.50.2.tgz", - "integrity": "sha512-APwKy6YUhvZaEoHyM+9xqmTpviEI+9eL7LoCH+aLcvWYHJ663qG5zx7WzWZY+a9qkg5JtzcMyJ9z0WtQBMDmgA==", + "version": "4.52.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.52.0.tgz", + "integrity": "sha512-6iKDCVSIUQ8jPMoIV0OytRKniaYyy5EbY/RRydmLW8ZR3cEBhxbWl5ro0rkUNe0ef6sScvhbY79HrjRm8i3vDQ==", "cpu": [ "x64" ], @@ -3159,9 +3173,9 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "24.5.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-24.5.0.tgz", - "integrity": "sha512-y1dMvuvJspJiPSDZUQ+WMBvF7dpnEqN4x9DDC9ie5Fs/HUZJA3wFp7EhHoVaKX/iI0cRoECV8X2jL8zi0xrHCg==", + "version": "24.5.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.5.2.tgz", + "integrity": "sha512-FYxk1I7wPv3K2XBaoyH2cTnocQEu8AOZ60hPbsyukMPLv5/5qr7V1i8PLHdl6Zf87I+xZXFvPCXYjiTFq+YSDQ==", "dev": true, "license": "MIT", "dependencies": { @@ -4061,9 +4075,9 @@ "license": "MIT" }, "node_modules/baseline-browser-mapping": { - "version": "2.8.4", - "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.4.tgz", - "integrity": "sha512-L+YvJwGAgwJBV1p6ffpSTa2KRc69EeeYGYjRVWKs0GKrK+LON0GC0gV+rKSNtALEDvMDqkvCFq9r1r94/Gjwxw==", + "version": "2.8.6", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.6.tgz", + "integrity": "sha512-wrH5NNqren/QMtKUEEJf7z86YjfqW/2uw3IL3/xpqZUC95SSVIFXYQeeGjL6FT/X68IROu6RMehZQS5foy2BXw==", "dev": true, "license": "Apache-2.0", "bin": { @@ -5722,9 +5736,9 @@ "license": "MIT" }, "node_modules/electron-to-chromium": { - "version": "1.5.218", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.218.tgz", - "integrity": "sha512-uwwdN0TUHs8u6iRgN8vKeWZMRll4gBkz+QMqdS7DDe49uiK68/UX92lFb61oiFPrpYZNeZIqa4bA7O6Aiasnzg==", + "version": "1.5.222", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.222.tgz", + "integrity": "sha512-gA7psSwSwQRE60CEoLz6JBCQPIxNeuzB2nL8vE03GK/OHxlvykbLyeiumQy1iH5C2f3YbRAZpGCMT12a/9ih9w==", "dev": true, "license": "ISC" }, @@ -5885,9 +5899,9 @@ } }, "node_modules/esbuild": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.9.tgz", - "integrity": "sha512-CRbODhYyQx3qp7ZEwzxOk4JBqmD/seJrzPa/cGjY1VtIn5E09Oi9/dB4JwctnfZ8Q8iT7rioVv5k/FNT/uf54g==", + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.10.tgz", + "integrity": "sha512-9RiGKvCwaqxO2owP61uQ4BgNborAQskMR6QusfWzQqv7AZOg5oGehdY2pRJMTKuwxd1IDBP4rSbI5lHzU7SMsQ==", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -5898,32 +5912,32 @@ "node": ">=18" }, "optionalDependencies": { - "@esbuild/aix-ppc64": "0.25.9", - "@esbuild/android-arm": "0.25.9", - "@esbuild/android-arm64": "0.25.9", - "@esbuild/android-x64": "0.25.9", - "@esbuild/darwin-arm64": "0.25.9", - "@esbuild/darwin-x64": "0.25.9", - "@esbuild/freebsd-arm64": "0.25.9", - "@esbuild/freebsd-x64": "0.25.9", - "@esbuild/linux-arm": "0.25.9", - "@esbuild/linux-arm64": "0.25.9", - "@esbuild/linux-ia32": "0.25.9", - "@esbuild/linux-loong64": "0.25.9", - "@esbuild/linux-mips64el": "0.25.9", - "@esbuild/linux-ppc64": "0.25.9", - "@esbuild/linux-riscv64": "0.25.9", - "@esbuild/linux-s390x": "0.25.9", - "@esbuild/linux-x64": "0.25.9", - "@esbuild/netbsd-arm64": "0.25.9", - "@esbuild/netbsd-x64": "0.25.9", - "@esbuild/openbsd-arm64": "0.25.9", - "@esbuild/openbsd-x64": "0.25.9", - "@esbuild/openharmony-arm64": "0.25.9", - "@esbuild/sunos-x64": "0.25.9", - "@esbuild/win32-arm64": "0.25.9", - "@esbuild/win32-ia32": "0.25.9", - "@esbuild/win32-x64": "0.25.9" + "@esbuild/aix-ppc64": "0.25.10", + "@esbuild/android-arm": "0.25.10", + "@esbuild/android-arm64": "0.25.10", + "@esbuild/android-x64": "0.25.10", + "@esbuild/darwin-arm64": "0.25.10", + "@esbuild/darwin-x64": "0.25.10", + "@esbuild/freebsd-arm64": "0.25.10", + "@esbuild/freebsd-x64": "0.25.10", + "@esbuild/linux-arm": "0.25.10", + "@esbuild/linux-arm64": "0.25.10", + "@esbuild/linux-ia32": "0.25.10", + "@esbuild/linux-loong64": "0.25.10", + "@esbuild/linux-mips64el": "0.25.10", + "@esbuild/linux-ppc64": "0.25.10", + "@esbuild/linux-riscv64": "0.25.10", + "@esbuild/linux-s390x": "0.25.10", + "@esbuild/linux-x64": "0.25.10", + "@esbuild/netbsd-arm64": "0.25.10", + "@esbuild/netbsd-x64": "0.25.10", + "@esbuild/openbsd-arm64": "0.25.10", + "@esbuild/openbsd-x64": "0.25.10", + "@esbuild/openharmony-arm64": "0.25.10", + "@esbuild/sunos-x64": "0.25.10", + "@esbuild/win32-arm64": "0.25.10", + "@esbuild/win32-ia32": "0.25.10", + "@esbuild/win32-x64": "0.25.10" } }, "node_modules/escalade": { @@ -10145,9 +10159,9 @@ } }, "node_modules/rollup": { - "version": "4.50.2", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.50.2.tgz", - "integrity": "sha512-BgLRGy7tNS9H66aIMASq1qSYbAAJV6Z6WR4QYTvj5FgF15rZ/ympT1uixHXwzbZUBDbkvqUI1KR0fH1FhMaQ9w==", + "version": "4.52.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.52.0.tgz", + "integrity": "sha512-+IuescNkTJQgX7AkIDtITipZdIGcWF0pnVvZTWStiazUmcGA2ag8dfg0urest2XlXUi9kuhfQ+qmdc5Stc3z7g==", "dev": true, "license": "MIT", "dependencies": { @@ -10161,27 +10175,28 @@ "npm": ">=8.0.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.50.2", - "@rollup/rollup-android-arm64": "4.50.2", - "@rollup/rollup-darwin-arm64": "4.50.2", - "@rollup/rollup-darwin-x64": "4.50.2", - "@rollup/rollup-freebsd-arm64": "4.50.2", - "@rollup/rollup-freebsd-x64": "4.50.2", - "@rollup/rollup-linux-arm-gnueabihf": "4.50.2", - "@rollup/rollup-linux-arm-musleabihf": "4.50.2", - "@rollup/rollup-linux-arm64-gnu": "4.50.2", - "@rollup/rollup-linux-arm64-musl": "4.50.2", - "@rollup/rollup-linux-loong64-gnu": "4.50.2", - "@rollup/rollup-linux-ppc64-gnu": "4.50.2", - "@rollup/rollup-linux-riscv64-gnu": "4.50.2", - "@rollup/rollup-linux-riscv64-musl": "4.50.2", - "@rollup/rollup-linux-s390x-gnu": "4.50.2", - "@rollup/rollup-linux-x64-gnu": "4.50.2", - "@rollup/rollup-linux-x64-musl": "4.50.2", - "@rollup/rollup-openharmony-arm64": "4.50.2", - "@rollup/rollup-win32-arm64-msvc": "4.50.2", - "@rollup/rollup-win32-ia32-msvc": "4.50.2", - "@rollup/rollup-win32-x64-msvc": "4.50.2", + "@rollup/rollup-android-arm-eabi": "4.52.0", + "@rollup/rollup-android-arm64": "4.52.0", + "@rollup/rollup-darwin-arm64": "4.52.0", + "@rollup/rollup-darwin-x64": "4.52.0", + "@rollup/rollup-freebsd-arm64": "4.52.0", + "@rollup/rollup-freebsd-x64": "4.52.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.52.0", + "@rollup/rollup-linux-arm-musleabihf": "4.52.0", + "@rollup/rollup-linux-arm64-gnu": "4.52.0", + "@rollup/rollup-linux-arm64-musl": "4.52.0", + "@rollup/rollup-linux-loong64-gnu": "4.52.0", + "@rollup/rollup-linux-ppc64-gnu": "4.52.0", + "@rollup/rollup-linux-riscv64-gnu": "4.52.0", + "@rollup/rollup-linux-riscv64-musl": "4.52.0", + "@rollup/rollup-linux-s390x-gnu": "4.52.0", + "@rollup/rollup-linux-x64-gnu": "4.52.0", + "@rollup/rollup-linux-x64-musl": "4.52.0", + "@rollup/rollup-openharmony-arm64": "4.52.0", + "@rollup/rollup-win32-arm64-msvc": "4.52.0", + "@rollup/rollup-win32-ia32-msvc": "4.52.0", + "@rollup/rollup-win32-x64-gnu": "4.52.0", + "@rollup/rollup-win32-x64-msvc": "4.52.0", "fsevents": "~2.3.2" } }, @@ -10237,9 +10252,9 @@ "license": "MIT" }, "node_modules/sass": { - "version": "1.92.1", - "resolved": "https://registry.npmjs.org/sass/-/sass-1.92.1.tgz", - "integrity": "sha512-ffmsdbwqb3XeyR8jJR6KelIXARM9bFQe8A6Q3W4Klmwy5Ckd5gz7jgUNHo4UOqutU5Sk1DtKLbpDP0nLCg1xqQ==", + "version": "1.93.0", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.93.0.tgz", + "integrity": "sha512-CQi5/AzCwiubU3dSqRDJ93RfOfg/hhpW1l6wCIvolmehfwgCI35R/0QDs1+R+Ygrl8jFawwwIojE2w47/mf94A==", "dev": true, "license": "MIT", "dependencies": { @@ -11554,9 +11569,9 @@ } }, "node_modules/vite": { - "version": "7.1.5", - "resolved": "https://registry.npmjs.org/vite/-/vite-7.1.5.tgz", - "integrity": "sha512-4cKBO9wR75r0BeIWWWId9XK9Lj6La5X846Zw9dFfzMRw38IlTk2iCcUt6hsyiDRcPidc55ZParFYDXi0nXOeLQ==", + "version": "7.1.6", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.1.6.tgz", + "integrity": "sha512-SRYIB8t/isTwNn8vMB3MR6E+EQZM/WG1aKmmIUCfDXfVvKfc20ZpamngWHKzAmmu9ppsgxsg4b2I7c90JZudIQ==", "dev": true, "license": "MIT", "dependencies": { diff --git a/resources/assets/v1/src/locales/cs.json b/resources/assets/v1/src/locales/cs.json index 66c89e114d..f1040e3ae7 100644 --- a/resources/assets/v1/src/locales/cs.json +++ b/resources/assets/v1/src/locales/cs.json @@ -53,7 +53,7 @@ "external_url": "Extern\u00ed URL adresa", "update_transaction": "Aktualizovat transakci", "after_update_create_another": "Po aktualizaci se vr\u00e1tit sem pro pokra\u010dov\u00e1n\u00ed v \u00faprav\u00e1ch.", - "store_as_new": "Store as a new transaction instead of updating.", + "store_as_new": "Vytvo\u0159it novou transakci m\u00edsto aktualizov\u00e1n\u00ed t\u00e9 sou\u010dasn\u00e9.", "split_title_help": "Pokud vytvo\u0159\u00edte roz\u00fa\u010dtov\u00e1n\u00ed, je t\u0159eba, aby zde byl celkov\u00fd popis pro v\u0161echna roz\u00fa\u010dtov\u00e1n\u00ed dan\u00e9 transakce.", "none_in_select_list": "(\u017e\u00e1dn\u00e9)", "no_piggy_bank": "(\u017e\u00e1dn\u00e1 pokladni\u010dka)", diff --git a/resources/assets/v1/src/locales/it.json b/resources/assets/v1/src/locales/it.json index 08b853b545..e297e8cb29 100644 --- a/resources/assets/v1/src/locales/it.json +++ b/resources/assets/v1/src/locales/it.json @@ -2,9 +2,9 @@ "firefly": { "administrations_page_title": "Amministrazioni finanziarie", "administrations_index_menu": "Amministrazioni finanziarie", - "expires_at": "Expires at", - "temp_administrations_introduction": "Firefly III will soon get the ability to manage multiple financial administrations. Right now, you only have the one. You can set the title of this administration and its primary currency. This replaces the previous setting where you would set your \"default currency\". This setting is now tied to the financial administration and can be different per administration.", - "administration_currency_form_help": "It may take a long time for the page to load if you change the primary currency because transaction may need to be converted to your (new) primary currency.", + "expires_at": "Scade il", + "temp_administrations_introduction": "Firefly III avr\u00e0 presto la possibilit\u00e0 di gestire pi\u00f9 amministrazioni finanziarie. Al momento, ne hai solo una. Puoi impostare il titolo di questa amministrazione e la sua valuta principale. Questa impostazione sostituisce la precedente, che prevedeva di impostare la \"valuta predefinita\". Questa impostazione \u00e8 ora legata all'amministrazione finanziaria e pu\u00f2 essere diversa per ogni amministrazione.", + "administration_currency_form_help": "Se modifichi la valuta principale, il caricamento della pagina potrebbe richiedere molto tempo, poich\u00e9 potrebbe essere necessario convertire la transazione nella (nuova) valuta principale.", "administrations_page_edit_sub_title_js": "Modifica amministrazione finanziaria \"{title}\"", "table": "Tabella", "welcome_back": "La tua situazione finanziaria", @@ -102,23 +102,23 @@ "profile_oauth_client_secret_title": "Segreto del client", "profile_oauth_client_secret_expl": "Ecco il segreto del nuovo client. Questa \u00e8 l'unica occasione in cui viene mostrato pertanto non perderlo! Ora puoi usare questo segreto per effettuare delle richieste alle API.", "profile_oauth_confidential": "Riservato", - "profile_oauth_confidential_help": "Require the client to authenticate with a secret. Confidential clients can hold credentials in a secure way without exposing them to unauthorized parties. Public applications, such as native desktop or JavaScript SPA applications, are unable to hold secrets securely.", + "profile_oauth_confidential_help": "Richiedere al client di autenticarsi con un segreto. I client riservati possono conservare le credenziali in modo sicuro senza esporle a soggetti non autorizzati. Le applicazioni pubbliche, come le applicazioni desktop native o le applicazioni SPA JavaScript, non sono in grado di conservare i segreti in modo sicuro.", "multi_account_warning_unknown": "A seconda del tipo di transazione che hai creato, il conto di origine e\/o destinazione delle successive suddivisioni pu\u00f2 essere sovrascritto da qualsiasi cosa sia definita nella prima suddivisione della transazione.", "multi_account_warning_withdrawal": "Ricorda che il conto di origine delle successive suddivisioni verr\u00e0 sovrascritto da quello definito nella prima suddivisione del prelievo.", "multi_account_warning_deposit": "Ricorda che il conto di destinazione delle successive suddivisioni verr\u00e0 sovrascritto da quello definito nella prima suddivisione del deposito.", "multi_account_warning_transfer": "Ricorda che il conto di origine e il conto di destinazione delle successive suddivisioni verranno sovrascritti da quelli definiti nella prima suddivisione del trasferimento.", - "webhook_trigger_ANY": "After any event", + "webhook_trigger_ANY": "Dopo ogni evento", "webhook_trigger_STORE_TRANSACTION": "Dopo aver creato la transazione", "webhook_trigger_UPDATE_TRANSACTION": "Dopo aver aggiornato la transazione", "webhook_trigger_DESTROY_TRANSACTION": "Dopo aver eliminato la transazione", - "webhook_trigger_STORE_BUDGET": "After budget creation", - "webhook_trigger_UPDATE_BUDGET": "After budget update", - "webhook_trigger_DESTROY_BUDGET": "After budget delete", - "webhook_trigger_STORE_UPDATE_BUDGET_LIMIT": "After budgeted amount change", + "webhook_trigger_STORE_BUDGET": "Dopo la creazione del budget", + "webhook_trigger_UPDATE_BUDGET": "Dopo l'aggiornamento del budget", + "webhook_trigger_DESTROY_BUDGET": "Dopo l'eliminazione del budget", + "webhook_trigger_STORE_UPDATE_BUDGET_LIMIT": "Dopo la modifica dell'importo preventivato", "webhook_response_TRANSACTIONS": "Dettagli transazione", - "webhook_response_RELEVANT": "Relevant details", + "webhook_response_RELEVANT": "Dettagli rilevanti", "webhook_response_ACCOUNTS": "Dettagli conto", - "webhook_response_NONE": "No details", + "webhook_response_NONE": "Nessun dettaglio", "webhook_delivery_JSON": "JSON", "actions": "Azioni", "meta_data": "Meta dati", From 90623101a39999dc740d2335557df13f219ffe1a Mon Sep 17 00:00:00 2001 From: James Cole Date: Sun, 21 Sep 2025 08:54:26 +0200 Subject: [PATCH 24/58] Add earned + spent, needs cleaning up still. --- .../v2/src/pages/dashboard/categories.js | 71 ++++++++++++------- 1 file changed, 44 insertions(+), 27 deletions(-) diff --git a/resources/assets/v2/src/pages/dashboard/categories.js b/resources/assets/v2/src/pages/dashboard/categories.js index 9af9c42d75..bc2707e805 100644 --- a/resources/assets/v2/src/pages/dashboard/categories.js +++ b/resources/assets/v2/src/pages/dashboard/categories.js @@ -54,12 +54,21 @@ export default () => ({ if (data.hasOwnProperty(i)) { let current = data[i]; let code = current.currency_code; - if (!series.hasOwnProperty(code)) { - series[code] = { - name: code, - yAxisID: '', - data: {}, - }; + + // create two series, "spent" and "earned". + for(const type of ['spent', 'earned']) { + let typeCode = code + '_' + type; + if (!series.hasOwnProperty(typeCode)) { + series[typeCode] = { + name: typeCode, + code: code, + type: type, + yAxisID: '', + data: {}, + }; + } + } + if (!currencies.includes(code)) { currencies.push(code); } } @@ -76,31 +85,38 @@ export default () => ({ code = current.primary_currency_code; } - // loop series, add 0 if not present or add actual amount. - for (const ii in series) { - if (series.hasOwnProperty(ii)) { - let amount = 0.0; - if (code === ii) { - // this series' currency matches this column's currency. - amount = parseFloat(current.entries.spent); - if(this.convertToPrimary) { - amount = parseFloat(current.entries.pc_entries.spent); + // twice again, for speny AND earned. + for(const type of ['spent', 'earned']) { + let typeCode = code + '_' + type; + // loop series, add 0 if not present or add actual amount. + for (const ii in series) { + if (series.hasOwnProperty(typeCode)) { + let amount = 0.0; + if (typeCode === ii) { + // this series' currency matches this column's currency. + amount = parseFloat(current.entries[type]); + if(this.convertToPrimary) { + amount = parseFloat(current.entries.pc_entries[type]); + } + yAxis = 'y' + typeCode; + } + if (series[typeCode].data.hasOwnProperty(current.label)) { + // there is a value for this particular currency. The amount from this column will be added. + // (even if this column isn't recorded in this currency and a new filler value is written) + // this is so currency conversion works. + series[typeCode].data[current.label] = series[typeCode].data[current.label] + amount; } - yAxis = 'y' + code; - } - if (series[ii].data.hasOwnProperty(current.label)) { - // there is a value for this particular currency. The amount from this column will be added. - // (even if this column isn't recorded in this currency and a new filler value is written) - // this is so currency conversion works. - series[ii].data[current.label] = series[ii].data[current.label] + amount; - } - if (!series[ii].data.hasOwnProperty(current.label)) { - // this column's amount is not yet set in this series. - series[ii].data[current.label] = amount; + if (!series[typeCode].data.hasOwnProperty(current.label)) { + // this column's amount is not yet set in this series. + series[typeCode].data[current.label] = amount; + } } } } + + + // add label to x-axis, not unimportant. if (!options.data.labels.includes(current.label)) { options.data.labels.push(current.label); @@ -111,9 +127,10 @@ export default () => ({ let count = 0; for (const i in series) { let yAxisID = 'y' + i; + let currencyCode = i.replace('_spent', '').replace('_earned', ''); let dataset = { label: i, - currency_code: i, + currency_code: currencyCode, yAxisID: yAxisID, data: [], // backgroundColor: getColors(null, 'background'), From 7d3b993b9851a9313e33051f828c83d367b5ecd5 Mon Sep 17 00:00:00 2001 From: JC5 Date: Sun, 21 Sep 2025 08:58:33 +0200 Subject: [PATCH 25/58] =?UTF-8?q?=F0=9F=A4=96=20Auto=20commit=20for=20rele?= =?UTF-8?q?ase=20'develop'=20on=202025-09-21?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- config/firefly.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/firefly.php b/config/firefly.php index 6bbe1f0ea2..c1c9db8f92 100644 --- a/config/firefly.php +++ b/config/firefly.php @@ -79,7 +79,7 @@ return [ // see cer.php for exchange rates feature flag. ], 'version' => 'develop/2025-09-21', - 'build_time' => 1758435980, + 'build_time' => 1758437799, 'api_version' => '2.1.0', // field is no longer used. 'db_version' => 26, From 7e08a1f33c78ceca161f2a84dcdaba8b743a2a3b Mon Sep 17 00:00:00 2001 From: James Cole Date: Sun, 21 Sep 2025 15:11:16 +0200 Subject: [PATCH 26/58] Possible fix for #10940, not sure. --- app/Support/JsonApi/Enrichments/PiggyBankEnrichment.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/Support/JsonApi/Enrichments/PiggyBankEnrichment.php b/app/Support/JsonApi/Enrichments/PiggyBankEnrichment.php index 9dd940801a..bacf8d12c3 100644 --- a/app/Support/JsonApi/Enrichments/PiggyBankEnrichment.php +++ b/app/Support/JsonApi/Enrichments/PiggyBankEnrichment.php @@ -130,7 +130,7 @@ class PiggyBankEnrichment implements EnrichmentInterface } $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); + $this->amounts[$id][$accountId]['pc_current_amount'] = bcadd($this->amounts[$id][$accountId]['pc_current_amount'], (string) $item->native_current_amount); } } From 013c43f9f214aa7e4d7a3220fd4610154b3b0239 Mon Sep 17 00:00:00 2001 From: JC5 Date: Sun, 21 Sep 2025 15:15:08 +0200 Subject: [PATCH 27/58] =?UTF-8?q?=F0=9F=A4=96=20Auto=20commit=20for=20rele?= =?UTF-8?q?ase=20'develop'=20on=202025-09-21?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- composer.lock | 16 ++++++++-------- config/firefly.php | 2 +- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/composer.lock b/composer.lock index dce4114021..dc09cc5a05 100644 --- a/composer.lock +++ b/composer.lock @@ -11891,16 +11891,16 @@ }, { "name": "phpunit/phpunit", - "version": "12.3.11", + "version": "12.3.12", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "6a62f2b394e042884e4997ddc8b8db1ce56a0009" + "reference": "729861f66944204f5b446ee1cb156f02f2a439a6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/6a62f2b394e042884e4997ddc8b8db1ce56a0009", - "reference": "6a62f2b394e042884e4997ddc8b8db1ce56a0009", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/729861f66944204f5b446ee1cb156f02f2a439a6", + "reference": "729861f66944204f5b446ee1cb156f02f2a439a6", "shasum": "" }, "require": { @@ -11914,12 +11914,12 @@ "phar-io/manifest": "^2.0.4", "phar-io/version": "^3.2.1", "php": ">=8.3", - "phpunit/php-code-coverage": "^12.3.7", + "phpunit/php-code-coverage": "^12.3.8", "phpunit/php-file-iterator": "^6.0.0", "phpunit/php-invoker": "^6.0.0", "phpunit/php-text-template": "^5.0.0", "phpunit/php-timer": "^8.0.0", - "sebastian/cli-parser": "^4.1.0", + "sebastian/cli-parser": "^4.2.0", "sebastian/comparator": "^7.1.3", "sebastian/diff": "^7.0.0", "sebastian/environment": "^8.0.3", @@ -11968,7 +11968,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/phpunit/issues", "security": "https://github.com/sebastianbergmann/phpunit/security/policy", - "source": "https://github.com/sebastianbergmann/phpunit/tree/12.3.11" + "source": "https://github.com/sebastianbergmann/phpunit/tree/12.3.12" }, "funding": [ { @@ -11992,7 +11992,7 @@ "type": "tidelift" } ], - "time": "2025-09-14T06:21:44+00:00" + "time": "2025-09-21T12:23:01+00:00" }, { "name": "rector/rector", diff --git a/config/firefly.php b/config/firefly.php index c1c9db8f92..6aac08185a 100644 --- a/config/firefly.php +++ b/config/firefly.php @@ -79,7 +79,7 @@ return [ // see cer.php for exchange rates feature flag. ], 'version' => 'develop/2025-09-21', - 'build_time' => 1758437799, + 'build_time' => 1758460398, 'api_version' => '2.1.0', // field is no longer used. 'db_version' => 26, From a751218d53ddd65ad423e6ad5d5f0eb0f21479bd Mon Sep 17 00:00:00 2001 From: James Cole Date: Sun, 21 Sep 2025 17:52:07 +0200 Subject: [PATCH 28/58] Fix rule order for #10938 --- app/TransactionRules/Engine/SearchRuleEngine.php | 5 +++-- config/firefly.php | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/app/TransactionRules/Engine/SearchRuleEngine.php b/app/TransactionRules/Engine/SearchRuleEngine.php index c4b4822ba4..a2164cf2fe 100644 --- a/app/TransactionRules/Engine/SearchRuleEngine.php +++ b/app/TransactionRules/Engine/SearchRuleEngine.php @@ -508,9 +508,10 @@ class SearchRuleEngine implements RuleEngineInterface { Log::debug(sprintf('Going to fire group #%d with %d rule(s)', $group->id, $group->rules->count())); + $rules = $group->rules()->orderBy('order', 'ASC')->get(); /** @var Rule $rule */ - foreach ($group->rules as $rule) { - Log::debug(sprintf('Going to fire rule #%d from group #%d', $rule->id, $group->id)); + foreach ($rules as $rule) { + Log::debug(sprintf('Going to fire rule #%d with order #%d from group #%d', $rule->id, $rule->order, $group->id)); $result = $this->fireRule($rule); if (true === $result && true === $rule->stop_processing) { Log::debug(sprintf('The rule was triggered and rule->stop_processing = true, so group #%d will stop processing further rules.', $group->id)); diff --git a/config/firefly.php b/config/firefly.php index c1c9db8f92..8f26b11816 100644 --- a/config/firefly.php +++ b/config/firefly.php @@ -81,7 +81,7 @@ return [ 'version' => 'develop/2025-09-21', 'build_time' => 1758437799, 'api_version' => '2.1.0', // field is no longer used. - 'db_version' => 26, + 'db_version' => 27, // generic settings 'maxUploadSize' => 1073741824, // 1 GB From e6b6a3cee5306cc3083d3414160c011d6c1632ad Mon Sep 17 00:00:00 2001 From: JC5 Date: Sun, 21 Sep 2025 17:57:23 +0200 Subject: [PATCH 29/58] =?UTF-8?q?=F0=9F=A4=96=20Auto=20commit=20for=20rele?= =?UTF-8?q?ase=20'develop'=20on=202025-09-21?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/TransactionRules/Engine/SearchRuleEngine.php | 1 + config/firefly.php | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/app/TransactionRules/Engine/SearchRuleEngine.php b/app/TransactionRules/Engine/SearchRuleEngine.php index a2164cf2fe..7843d1ffca 100644 --- a/app/TransactionRules/Engine/SearchRuleEngine.php +++ b/app/TransactionRules/Engine/SearchRuleEngine.php @@ -509,6 +509,7 @@ class SearchRuleEngine implements RuleEngineInterface Log::debug(sprintf('Going to fire group #%d with %d rule(s)', $group->id, $group->rules->count())); $rules = $group->rules()->orderBy('order', 'ASC')->get(); + /** @var Rule $rule */ foreach ($rules as $rule) { Log::debug(sprintf('Going to fire rule #%d with order #%d from group #%d', $rule->id, $rule->order, $group->id)); diff --git a/config/firefly.php b/config/firefly.php index cf8f2be9a7..d84b93a551 100644 --- a/config/firefly.php +++ b/config/firefly.php @@ -79,7 +79,7 @@ return [ // see cer.php for exchange rates feature flag. ], 'version' => 'develop/2025-09-21', - 'build_time' => 1758460398, + 'build_time' => 1758470121, 'api_version' => '2.1.0', // field is no longer used. 'db_version' => 27, From beecf9c22983246bf3bec632a63e4244812ff91d Mon Sep 17 00:00:00 2001 From: James Cole Date: Sun, 21 Sep 2025 18:00:23 +0200 Subject: [PATCH 30/58] Make sure demo user cannot send notifications. --- app/Http/Controllers/Admin/NotificationController.php | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/app/Http/Controllers/Admin/NotificationController.php b/app/Http/Controllers/Admin/NotificationController.php index b8e52ebbe6..b2d6a40b62 100644 --- a/app/Http/Controllers/Admin/NotificationController.php +++ b/app/Http/Controllers/Admin/NotificationController.php @@ -29,6 +29,7 @@ use FireflyIII\Http\Controllers\Controller; use FireflyIII\Http\Requests\NotificationRequest; use FireflyIII\Notifications\Notifiables\OwnerNotifiable; use FireflyIII\Support\Facades\FireflyConfig; +use FireflyIII\User; use Illuminate\Http\RedirectResponse; use Illuminate\Http\Request; use Illuminate\Support\Facades\Log; @@ -122,6 +123,10 @@ 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'] ?? ''; From d868dc0945629cf1c473a885fa89666222cece22 Mon Sep 17 00:00:00 2001 From: JC5 Date: Mon, 22 Sep 2025 05:22:58 +0200 Subject: [PATCH 31/58] =?UTF-8?q?=F0=9F=A4=96=20Auto=20commit=20for=20rele?= =?UTF-8?q?ase=20'develop'=20on=202025-09-22?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/Http/Controllers/Admin/NotificationController.php | 4 ++-- config/firefly.php | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/app/Http/Controllers/Admin/NotificationController.php b/app/Http/Controllers/Admin/NotificationController.php index b2d6a40b62..233dc7f66a 100644 --- a/app/Http/Controllers/Admin/NotificationController.php +++ b/app/Http/Controllers/Admin/NotificationController.php @@ -29,7 +29,6 @@ use FireflyIII\Http\Controllers\Controller; use FireflyIII\Http\Requests\NotificationRequest; use FireflyIII\Notifications\Notifiables\OwnerNotifiable; use FireflyIII\Support\Facades\FireflyConfig; -use FireflyIII\User; use Illuminate\Http\RedirectResponse; use Illuminate\Http\Request; use Illuminate\Support\Facades\Log; @@ -124,7 +123,8 @@ 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' )); + session()->flash('error', (string) trans('firefly.not_available_demo_user')); + return redirect(route('settings.notification.index')); } diff --git a/config/firefly.php b/config/firefly.php index d84b93a551..8248d97eb6 100644 --- a/config/firefly.php +++ b/config/firefly.php @@ -78,8 +78,8 @@ return [ 'running_balance_column' => env('USE_RUNNING_BALANCE', false), // see cer.php for exchange rates feature flag. ], - 'version' => 'develop/2025-09-21', - 'build_time' => 1758470121, + 'version' => 'develop/2025-09-22', + 'build_time' => 1758511276, 'api_version' => '2.1.0', // field is no longer used. 'db_version' => 27, From 5a1413e758b5e6987f2c52c0488d5e4e9119331b Mon Sep 17 00:00:00 2001 From: James Cole Date: Tue, 23 Sep 2025 20:22:58 +0200 Subject: [PATCH 32/58] Fix #10954 --- resources/views/list/groups.twig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resources/views/list/groups.twig b/resources/views/list/groups.twig index 460462e684..a53f271fd8 100644 --- a/resources/views/list/groups.twig +++ b/resources/views/list/groups.twig @@ -268,7 +268,7 @@ {% if config('firefly.feature_flags.running_balance_column') %} - {% if null == transaction.balance_dirty or false == transaction.balance_dirty and null != transaction.destination_balance_after %} + {% if (null == transaction.balance_dirty or false == transaction.balance_dirty) and null != transaction.destination_balance_after and null != transaction.source_balance_after %} {% if transaction.transaction_type_type == 'Deposit' %} {{ formatAmountBySymbol(transaction.destination_balance_after, transaction.currency_symbol, transaction.currency_decimal_places) }} {% elseif transaction.transaction_type_type == 'Withdrawal' %} From 4a264f34fa30e9cfe031f13954d9576ec1bbd0d6 Mon Sep 17 00:00:00 2001 From: JC5 Date: Tue, 23 Sep 2025 20:41:36 +0200 Subject: [PATCH 33/58] =?UTF-8?q?=F0=9F=A4=96=20Auto=20commit=20for=20rele?= =?UTF-8?q?ase=20'develop'=20on=202025-09-23?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- composer.lock | 102 ++++++++------ config/firefly.php | 4 +- package-lock.json | 330 +++++++++++++++++++++------------------------ 3 files changed, 210 insertions(+), 226 deletions(-) diff --git a/composer.lock b/composer.lock index dc09cc5a05..de8828fde5 100644 --- a/composer.lock +++ b/composer.lock @@ -1878,16 +1878,16 @@ }, { "name": "laravel/framework", - "version": "v12.30.1", + "version": "v12.31.1", "source": { "type": "git", "url": "https://github.com/laravel/framework.git", - "reference": "7f61e8679f9142f282a0184ac7ef9e3834bfd023" + "reference": "281b711710c245dd8275d73132e92635be3094df" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/framework/zipball/7f61e8679f9142f282a0184ac7ef9e3834bfd023", - "reference": "7f61e8679f9142f282a0184ac7ef9e3834bfd023", + "url": "https://api.github.com/repos/laravel/framework/zipball/281b711710c245dd8275d73132e92635be3094df", + "reference": "281b711710c245dd8275d73132e92635be3094df", "shasum": "" }, "require": { @@ -2094,7 +2094,7 @@ "issues": "https://github.com/laravel/framework/issues", "source": "https://github.com/laravel/framework" }, - "time": "2025-09-18T21:07:07+00:00" + "time": "2025-09-23T15:33:04+00:00" }, { "name": "laravel/passport", @@ -2174,16 +2174,16 @@ }, { "name": "laravel/prompts", - "version": "v0.3.6", + "version": "v0.3.7", "source": { "type": "git", "url": "https://github.com/laravel/prompts.git", - "reference": "86a8b692e8661d0fb308cec64f3d176821323077" + "reference": "a1891d362714bc40c8d23b0b1d7090f022ea27cc" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/prompts/zipball/86a8b692e8661d0fb308cec64f3d176821323077", - "reference": "86a8b692e8661d0fb308cec64f3d176821323077", + "url": "https://api.github.com/repos/laravel/prompts/zipball/a1891d362714bc40c8d23b0b1d7090f022ea27cc", + "reference": "a1891d362714bc40c8d23b0b1d7090f022ea27cc", "shasum": "" }, "require": { @@ -2200,8 +2200,8 @@ "illuminate/collections": "^10.0|^11.0|^12.0", "mockery/mockery": "^1.5", "pestphp/pest": "^2.3|^3.4", - "phpstan/phpstan": "^1.11", - "phpstan/phpstan-mockery": "^1.1" + "phpstan/phpstan": "^1.12.28", + "phpstan/phpstan-mockery": "^1.1.3" }, "suggest": { "ext-pcntl": "Required for the spinner to be animated." @@ -2227,9 +2227,9 @@ "description": "Add beautiful and user-friendly forms to your command-line applications.", "support": { "issues": "https://github.com/laravel/prompts/issues", - "source": "https://github.com/laravel/prompts/tree/v0.3.6" + "source": "https://github.com/laravel/prompts/tree/v0.3.7" }, - "time": "2025-07-07T14:17:42+00:00" + "time": "2025-09-19T13:47:56+00:00" }, { "name": "laravel/sanctum", @@ -2297,16 +2297,16 @@ }, { "name": "laravel/serializable-closure", - "version": "v2.0.4", + "version": "v2.0.5", "source": { "type": "git", "url": "https://github.com/laravel/serializable-closure.git", - "reference": "b352cf0534aa1ae6b4d825d1e762e35d43f8a841" + "reference": "3832547db6e0e2f8bb03d4093857b378c66eceed" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/serializable-closure/zipball/b352cf0534aa1ae6b4d825d1e762e35d43f8a841", - "reference": "b352cf0534aa1ae6b4d825d1e762e35d43f8a841", + "url": "https://api.github.com/repos/laravel/serializable-closure/zipball/3832547db6e0e2f8bb03d4093857b378c66eceed", + "reference": "3832547db6e0e2f8bb03d4093857b378c66eceed", "shasum": "" }, "require": { @@ -2354,7 +2354,7 @@ "issues": "https://github.com/laravel/serializable-closure/issues", "source": "https://github.com/laravel/serializable-closure" }, - "time": "2025-03-19T13:51:03+00:00" + "time": "2025-09-22T17:29:40+00:00" }, { "name": "laravel/slack-notification-channel", @@ -4235,24 +4235,26 @@ }, { "name": "paragonie/constant_time_encoding", - "version": "v3.0.0", + "version": "v3.1.1", "source": { "type": "git", "url": "https://github.com/paragonie/constant_time_encoding.git", - "reference": "df1e7fde177501eee2037dd159cf04f5f301a512" + "reference": "5e9b582660b997de205a84c02a3aac7c060900c9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/paragonie/constant_time_encoding/zipball/df1e7fde177501eee2037dd159cf04f5f301a512", - "reference": "df1e7fde177501eee2037dd159cf04f5f301a512", + "url": "https://api.github.com/repos/paragonie/constant_time_encoding/zipball/5e9b582660b997de205a84c02a3aac7c060900c9", + "reference": "5e9b582660b997de205a84c02a3aac7c060900c9", "shasum": "" }, "require": { "php": "^8" }, "require-dev": { - "phpunit/phpunit": "^9", - "vimeo/psalm": "^4|^5" + "infection/infection": "^0", + "nikic/php-fuzzer": "^0", + "phpunit/phpunit": "^9|^10|^11", + "vimeo/psalm": "^4|^5|^6" }, "type": "library", "autoload": { @@ -4298,7 +4300,7 @@ "issues": "https://github.com/paragonie/constant_time_encoding/issues", "source": "https://github.com/paragonie/constant_time_encoding" }, - "time": "2024-05-08T12:36:18+00:00" + "time": "2025-09-22T21:00:33+00:00" }, { "name": "paragonie/random_compat", @@ -4352,16 +4354,16 @@ }, { "name": "phiki/phiki", - "version": "v2.0.3", + "version": "v2.0.4", "source": { "type": "git", "url": "https://github.com/phikiphp/phiki.git", - "reference": "fe51fe6dc31856cd776fd1b04ee74053a4271644" + "reference": "160785c50c01077780ab217e5808f00ab8f05a13" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phikiphp/phiki/zipball/fe51fe6dc31856cd776fd1b04ee74053a4271644", - "reference": "fe51fe6dc31856cd776fd1b04ee74053a4271644", + "url": "https://api.github.com/repos/phikiphp/phiki/zipball/160785c50c01077780ab217e5808f00ab8f05a13", + "reference": "160785c50c01077780ab217e5808f00ab8f05a13", "shasum": "" }, "require": { @@ -4407,7 +4409,7 @@ "description": "Syntax highlighting using TextMate grammars in PHP.", "support": { "issues": "https://github.com/phikiphp/phiki/issues", - "source": "https://github.com/phikiphp/phiki/tree/v2.0.3" + "source": "https://github.com/phikiphp/phiki/tree/v2.0.4" }, "funding": [ { @@ -4419,7 +4421,7 @@ "type": "other" } ], - "time": "2025-09-19T11:50:41+00:00" + "time": "2025-09-20T17:21:02+00:00" }, { "name": "php-http/client-common", @@ -11891,16 +11893,16 @@ }, { "name": "phpunit/phpunit", - "version": "12.3.12", + "version": "12.3.13", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "729861f66944204f5b446ee1cb156f02f2a439a6" + "reference": "44f15312c4968fa8102e491fc6f3746410819c16" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/729861f66944204f5b446ee1cb156f02f2a439a6", - "reference": "729861f66944204f5b446ee1cb156f02f2a439a6", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/44f15312c4968fa8102e491fc6f3746410819c16", + "reference": "44f15312c4968fa8102e491fc6f3746410819c16", "shasum": "" }, "require": { @@ -11923,7 +11925,7 @@ "sebastian/comparator": "^7.1.3", "sebastian/diff": "^7.0.0", "sebastian/environment": "^8.0.3", - "sebastian/exporter": "^7.0.0", + "sebastian/exporter": "^7.0.1", "sebastian/global-state": "^8.0.2", "sebastian/object-enumerator": "^7.0.0", "sebastian/type": "^6.0.3", @@ -11968,7 +11970,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/phpunit/issues", "security": "https://github.com/sebastianbergmann/phpunit/security/policy", - "source": "https://github.com/sebastianbergmann/phpunit/tree/12.3.12" + "source": "https://github.com/sebastianbergmann/phpunit/tree/12.3.13" }, "funding": [ { @@ -11992,7 +11994,7 @@ "type": "tidelift" } ], - "time": "2025-09-21T12:23:01+00:00" + "time": "2025-09-23T06:25:02+00:00" }, { "name": "rector/rector", @@ -12418,16 +12420,16 @@ }, { "name": "sebastian/exporter", - "version": "7.0.0", + "version": "7.0.1", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/exporter.git", - "reference": "76432aafc58d50691a00d86d0632f1217a47b688" + "reference": "b759164a8e02263784b662889cc6cbb686077af6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/76432aafc58d50691a00d86d0632f1217a47b688", - "reference": "76432aafc58d50691a00d86d0632f1217a47b688", + "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/b759164a8e02263784b662889cc6cbb686077af6", + "reference": "b759164a8e02263784b662889cc6cbb686077af6", "shasum": "" }, "require": { @@ -12484,15 +12486,27 @@ "support": { "issues": "https://github.com/sebastianbergmann/exporter/issues", "security": "https://github.com/sebastianbergmann/exporter/security/policy", - "source": "https://github.com/sebastianbergmann/exporter/tree/7.0.0" + "source": "https://github.com/sebastianbergmann/exporter/tree/7.0.1" }, "funding": [ { "url": "https://github.com/sebastianbergmann", "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/exporter", + "type": "tidelift" } ], - "time": "2025-02-07T04:56:42+00:00" + "time": "2025-09-22T05:39:29+00:00" }, { "name": "sebastian/global-state", diff --git a/config/firefly.php b/config/firefly.php index 8248d97eb6..4dbb9dfde7 100644 --- a/config/firefly.php +++ b/config/firefly.php @@ -78,8 +78,8 @@ return [ 'running_balance_column' => env('USE_RUNNING_BALANCE', false), // see cer.php for exchange rates feature flag. ], - 'version' => 'develop/2025-09-22', - 'build_time' => 1758511276, + 'version' => 'develop/2025-09-23', + 'build_time' => 1758652788, 'api_version' => '2.1.0', // field is no longer used. 'db_version' => 27, diff --git a/package-lock.json b/package-lock.json index a12d8faaf8..e0ac00f144 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2589,9 +2589,9 @@ } }, "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.52.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.52.0.tgz", - "integrity": "sha512-VxDYCDqOaR7NXzAtvRx7G1u54d2kEHopb28YH/pKzY6y0qmogP3gG7CSiWsq9WvDFxOQMpNEyjVAHZFXfH3o/A==", + "version": "4.52.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.52.2.tgz", + "integrity": "sha512-o3pcKzJgSGt4d74lSZ+OCnHwkKBeAbFDmbEm5gg70eA8VkyCuC/zV9TwBnmw6VjDlRdF4Pshfb+WE9E6XY1PoQ==", "cpu": [ "arm" ], @@ -2603,9 +2603,9 @@ ] }, "node_modules/@rollup/rollup-android-arm64": { - "version": "4.52.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.52.0.tgz", - "integrity": "sha512-pqDirm8koABIKvzL59YI9W9DWbRlTX7RWhN+auR8HXJxo89m4mjqbah7nJZjeKNTNYopqL+yGg+0mhCpf3xZtQ==", + "version": "4.52.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.52.2.tgz", + "integrity": "sha512-cqFSWO5tX2vhC9hJTK8WAiPIm4Q8q/cU8j2HQA0L3E1uXvBYbOZMhE2oFL8n2pKB5sOCHY6bBuHaRwG7TkfJyw==", "cpu": [ "arm64" ], @@ -2617,9 +2617,9 @@ ] }, "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.52.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.52.0.tgz", - "integrity": "sha512-YCdWlY/8ltN6H78HnMsRHYlPiKvqKagBP1r+D7SSylxX+HnsgXGCmLiV3Y4nSyY9hW8qr8U9LDUx/Lo7M6MfmQ==", + "version": "4.52.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.52.2.tgz", + "integrity": "sha512-vngduywkkv8Fkh3wIZf5nFPXzWsNsVu1kvtLETWxTFf/5opZmflgVSeLgdHR56RQh71xhPhWoOkEBvbehwTlVA==", "cpu": [ "arm64" ], @@ -2631,9 +2631,9 @@ ] }, "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.52.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.52.0.tgz", - "integrity": "sha512-z4nw6y1j+OOSGzuVbSWdIp1IUks9qNw4dc7z7lWuWDKojY38VMWBlEN7F9jk5UXOkUcp97vA1N213DF+Lz8BRg==", + "version": "4.52.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.52.2.tgz", + "integrity": "sha512-h11KikYrUCYTrDj6h939hhMNlqU2fo/X4NB0OZcys3fya49o1hmFaczAiJWVAFgrM1NCP6RrO7lQKeVYSKBPSQ==", "cpu": [ "x64" ], @@ -2645,9 +2645,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.52.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.52.0.tgz", - "integrity": "sha512-Q/dv9Yvyr5rKlK8WQJZVrp5g2SOYeZUs9u/t2f9cQ2E0gJjYB/BWoedXfUT0EcDJefi2zzVfhcOj8drWCzTviw==", + "version": "4.52.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.52.2.tgz", + "integrity": "sha512-/eg4CI61ZUkLXxMHyVlmlGrSQZ34xqWlZNW43IAU4RmdzWEx0mQJ2mN/Cx4IHLVZFL6UBGAh+/GXhgvGb+nVxw==", "cpu": [ "arm64" ], @@ -2659,9 +2659,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.52.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.52.0.tgz", - "integrity": "sha512-kdBsLs4Uile/fbjZVvCRcKB4q64R+1mUq0Yd7oU1CMm1Av336ajIFqNFovByipciuUQjBCPMxwJhCgfG2re3rg==", + "version": "4.52.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.52.2.tgz", + "integrity": "sha512-QOWgFH5X9+p+S1NAfOqc0z8qEpJIoUHf7OWjNUGOeW18Mx22lAUOiA9b6r2/vpzLdfxi/f+VWsYjUOMCcYh0Ng==", "cpu": [ "x64" ], @@ -2673,9 +2673,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.52.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.52.0.tgz", - "integrity": "sha512-aL6hRwu0k7MTUESgkg7QHY6CoqPgr6gdQXRJI1/VbFlUMwsSzPGSR7sG5d+MCbYnJmJwThc2ol3nixj1fvI/zQ==", + "version": "4.52.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.52.2.tgz", + "integrity": "sha512-kDWSPafToDd8LcBYd1t5jw7bD5Ojcu12S3uT372e5HKPzQt532vW+rGFFOaiR0opxePyUkHrwz8iWYEyH1IIQA==", "cpu": [ "arm" ], @@ -2687,9 +2687,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.52.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.52.0.tgz", - "integrity": "sha512-BTs0M5s1EJejgIBJhCeiFo7GZZ2IXWkFGcyZhxX4+8usnIo5Mti57108vjXFIQmmJaRyDwmV59Tw64Ap1dkwMw==", + "version": "4.52.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.52.2.tgz", + "integrity": "sha512-gKm7Mk9wCv6/rkzwCiUC4KnevYhlf8ztBrDRT9g/u//1fZLapSRc+eDZj2Eu2wpJ+0RzUKgtNijnVIB4ZxyL+w==", "cpu": [ "arm" ], @@ -2701,9 +2701,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.52.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.52.0.tgz", - "integrity": "sha512-uj672IVOU9m08DBGvoPKPi/J8jlVgjh12C9GmjjBxCTQc3XtVmRkRKyeHSmIKQpvJ7fIm1EJieBUcnGSzDVFyw==", + "version": "4.52.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.52.2.tgz", + "integrity": "sha512-66lA8vnj5mB/rtDNwPgrrKUOtCLVQypkyDa2gMfOefXK6rcZAxKLO9Fy3GkW8VkPnENv9hBkNOFfGLf6rNKGUg==", "cpu": [ "arm64" ], @@ -2715,9 +2715,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.52.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.52.0.tgz", - "integrity": "sha512-/+IVbeDMDCtB/HP/wiWsSzduD10SEGzIZX2945KSgZRNi4TSkjHqRJtNTVtVb8IRwhJ65ssI56krlLik+zFWkw==", + "version": "4.52.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.52.2.tgz", + "integrity": "sha512-s+OPucLNdJHvuZHuIz2WwncJ+SfWHFEmlC5nKMUgAelUeBUnlB4wt7rXWiyG4Zn07uY2Dd+SGyVa9oyLkVGOjA==", "cpu": [ "arm64" ], @@ -2729,9 +2729,9 @@ ] }, "node_modules/@rollup/rollup-linux-loong64-gnu": { - "version": "4.52.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.52.0.tgz", - "integrity": "sha512-U1vVzvSWtSMWKKrGoROPBXMh3Vwn93TA9V35PldokHGqiUbF6erSzox/5qrSMKp6SzakvyjcPiVF8yB1xKr9Pg==", + "version": "4.52.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.52.2.tgz", + "integrity": "sha512-8wTRM3+gVMDLLDdaT6tKmOE3lJyRy9NpJUS/ZRWmLCmOPIJhVyXwjBo+XbrrwtV33Em1/eCTd5TuGJm4+DmYjw==", "cpu": [ "loong64" ], @@ -2743,9 +2743,9 @@ ] }, "node_modules/@rollup/rollup-linux-ppc64-gnu": { - "version": "4.52.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.52.0.tgz", - "integrity": "sha512-X/4WfuBAdQRH8cK3DYl8zC00XEE6aM472W+QCycpQJeLWVnHfkv7RyBFVaTqNUMsTgIX8ihMjCvFF9OUgeABzw==", + "version": "4.52.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.52.2.tgz", + "integrity": "sha512-6yqEfgJ1anIeuP2P/zhtfBlDpXUb80t8DpbYwXQ3bQd95JMvUaqiX+fKqYqUwZXqdJDd8xdilNtsHM2N0cFm6A==", "cpu": [ "ppc64" ], @@ -2757,9 +2757,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.52.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.52.0.tgz", - "integrity": "sha512-xIRYc58HfWDBZoLmWfWXg2Sq8VCa2iJ32B7mqfWnkx5mekekl0tMe7FHpY8I72RXEcUkaWawRvl3qA55og+cwQ==", + "version": "4.52.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.52.2.tgz", + "integrity": "sha512-sshYUiYVSEI2B6dp4jMncwxbrUqRdNApF2c3bhtLAU0qA8Lrri0p0NauOsTWh3yCCCDyBOjESHMExonp7Nzc0w==", "cpu": [ "riscv64" ], @@ -2771,9 +2771,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-musl": { - "version": "4.52.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.52.0.tgz", - "integrity": "sha512-mbsoUey05WJIOz8U1WzNdf+6UMYGwE3fZZnQqsM22FZ3wh1N887HT6jAOjXs6CNEK3Ntu2OBsyQDXfIjouI4dw==", + "version": "4.52.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.52.2.tgz", + "integrity": "sha512-duBLgd+3pqC4MMwBrKkFxaZerUxZcYApQVC5SdbF5/e/589GwVvlRUnyqMFbM8iUSb1BaoX/3fRL7hB9m2Pj8Q==", "cpu": [ "riscv64" ], @@ -2785,9 +2785,9 @@ ] }, "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.52.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.52.0.tgz", - "integrity": "sha512-qP6aP970bucEi5KKKR4AuPFd8aTx9EF6BvutvYxmZuWLJHmnq4LvBfp0U+yFDMGwJ+AIJEH5sIP+SNypauMWzg==", + "version": "4.52.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.52.2.tgz", + "integrity": "sha512-tzhYJJidDUVGMgVyE+PmxENPHlvvqm1KILjjZhB8/xHYqAGeizh3GBGf9u6WdJpZrz1aCpIIHG0LgJgH9rVjHQ==", "cpu": [ "s390x" ], @@ -2799,9 +2799,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.52.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.52.0.tgz", - "integrity": "sha512-nmSVN+F2i1yKZ7rJNKO3G7ZzmxJgoQBQZ/6c4MuS553Grmr7WqR7LLDcYG53Z2m9409z3JLt4sCOhLdbKQ3HmA==", + "version": "4.52.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.52.2.tgz", + "integrity": "sha512-opH8GSUuVcCSSyHHcl5hELrmnk4waZoVpgn/4FDao9iyE4WpQhyWJ5ryl5M3ocp4qkRuHfyXnGqg8M9oKCEKRA==", "cpu": [ "x64" ], @@ -2813,9 +2813,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.52.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.52.0.tgz", - "integrity": "sha512-2d0qRo33G6TfQVjaMR71P+yJVGODrt5V6+T0BDYH4EMfGgdC/2HWDVjSSFw888GSzAZUwuska3+zxNUCDco6rQ==", + "version": "4.52.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.52.2.tgz", + "integrity": "sha512-LSeBHnGli1pPKVJ79ZVJgeZWWZXkEe/5o8kcn23M8eMKCUANejchJbF/JqzM4RRjOJfNRhKJk8FuqL1GKjF5oQ==", "cpu": [ "x64" ], @@ -2827,9 +2827,9 @@ ] }, "node_modules/@rollup/rollup-openharmony-arm64": { - "version": "4.52.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.52.0.tgz", - "integrity": "sha512-A1JalX4MOaFAAyGgpO7XP5khquv/7xKzLIyLmhNrbiCxWpMlnsTYr8dnsWM7sEeotNmxvSOEL7F65j0HXFcFsw==", + "version": "4.52.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.52.2.tgz", + "integrity": "sha512-uPj7MQ6/s+/GOpolavm6BPo+6CbhbKYyZHUDvZ/SmJM7pfDBgdGisFX3bY/CBDMg2ZO4utfhlApkSfZ92yXw7Q==", "cpu": [ "arm64" ], @@ -2841,9 +2841,9 @@ ] }, "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.52.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.52.0.tgz", - "integrity": "sha512-YQugafP/rH0eOOHGjmNgDURrpYHrIX0yuojOI8bwCyXwxC9ZdTd3vYkmddPX0oHONLXu9Rb1dDmT0VNpjkzGGw==", + "version": "4.52.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.52.2.tgz", + "integrity": "sha512-Z9MUCrSgIaUeeHAiNkm3cQyst2UhzjPraR3gYYfOjAuZI7tcFRTOD+4cHLPoS/3qinchth+V56vtqz1Tv+6KPA==", "cpu": [ "arm64" ], @@ -2855,9 +2855,9 @@ ] }, "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.52.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.52.0.tgz", - "integrity": "sha512-zYdUYhi3Qe2fndujBqL5FjAFzvNeLxtIqfzNEVKD1I7C37/chv1VxhscWSQHTNfjPCrBFQMnynwA3kpZpZ8w4A==", + "version": "4.52.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.52.2.tgz", + "integrity": "sha512-+GnYBmpjldD3XQd+HMejo+0gJGwYIOfFeoBQv32xF/RUIvccUz20/V6Otdv+57NE70D5pa8W/jVGDoGq0oON4A==", "cpu": [ "ia32" ], @@ -2869,9 +2869,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-gnu": { - "version": "4.52.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.52.0.tgz", - "integrity": "sha512-fGk03kQylNaCOQ96HDMeT7E2n91EqvCDd3RwvT5k+xNdFCeMGnj5b5hEgTGrQuyidqSsD3zJDQ21QIaxXqTBJw==", + "version": "4.52.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.52.2.tgz", + "integrity": "sha512-ApXFKluSB6kDQkAqZOKXBjiaqdF1BlKi+/eqnYe9Ee7U2K3pUDKsIyr8EYm/QDHTJIM+4X+lI0gJc3TTRhd+dA==", "cpu": [ "x64" ], @@ -2883,9 +2883,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.52.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.52.0.tgz", - "integrity": "sha512-6iKDCVSIUQ8jPMoIV0OytRKniaYyy5EbY/RRydmLW8ZR3cEBhxbWl5ro0rkUNe0ef6sScvhbY79HrjRm8i3vDQ==", + "version": "4.52.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.52.2.tgz", + "integrity": "sha512-ARz+Bs8kY6FtitYM96PqPEVvPXqEZmPZsSkXvyX19YzDqkCaIlhCieLLMI5hxO9SRZ2XtCtm8wxhy0iJ2jxNfw==", "cpu": [ "x64" ], @@ -4329,18 +4329,17 @@ } }, "node_modules/browserify-sign": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/browserify-sign/-/browserify-sign-4.2.3.tgz", - "integrity": "sha512-JWCZW6SKhfhjJxO8Tyiiy+XYB7cqd2S5/+WeYHsKdNKFlCBhKbblba1A/HN/90YwtxKc8tCErjffZl++UNmGiw==", + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/browserify-sign/-/browserify-sign-4.2.4.tgz", + "integrity": "sha512-pbZw0FHibrwXcpLQlXwHp21A5undDBo+RaGNL0K3KOm8nK8uP6PThhS301VDzoMgURZPiVRWRrVHlo6NyU57kA==", "dev": true, "license": "ISC", "dependencies": { - "bn.js": "^5.2.1", - "browserify-rsa": "^4.1.0", + "bn.js": "^5.2.2", + "browserify-rsa": "^4.1.1", "create-hash": "^1.2.0", "create-hmac": "^1.1.7", - "elliptic": "^6.5.5", - "hash-base": "~3.0", + "elliptic": "^6.6.1", "inherits": "^2.0.4", "parse-asn1": "^5.1.7", "readable-stream": "^2.3.8", @@ -5736,9 +5735,9 @@ "license": "MIT" }, "node_modules/electron-to-chromium": { - "version": "1.5.222", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.222.tgz", - "integrity": "sha512-gA7psSwSwQRE60CEoLz6JBCQPIxNeuzB2nL8vE03GK/OHxlvykbLyeiumQy1iH5C2f3YbRAZpGCMT12a/9ih9w==", + "version": "1.5.223", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.223.tgz", + "integrity": "sha512-qKm55ic6nbEmagFlTFczML33rF90aU+WtrJ9MdTCThrcvDNdUHN4p6QfVN78U06ZmguqXIyMPyYhw2TrbDUwPQ==", "dev": true, "license": "ISC" }, @@ -8761,21 +8760,20 @@ } }, "node_modules/parse-asn1": { - "version": "5.1.7", - "resolved": "https://registry.npmjs.org/parse-asn1/-/parse-asn1-5.1.7.tgz", - "integrity": "sha512-CTM5kuWR3sx9IFamcl5ErfPl6ea/N8IYwiJ+vpeB2g+1iknv7zBl5uPwbMbRVznRVbrNY6lGuDoE5b30grmbqg==", + "version": "5.1.8", + "resolved": "https://registry.npmjs.org/parse-asn1/-/parse-asn1-5.1.8.tgz", + "integrity": "sha512-e90aVPe/1q/g7BrNeYvbJy++5tln4ShE+I3qZ5LxFpUbu+uavfKMuzH2R3SH141O7Pvruwif0BZRwKoVf6vW6w==", "dev": true, "license": "ISC", "dependencies": { "asn1.js": "^4.10.1", "browserify-aes": "^1.2.0", "evp_bytestokey": "^1.0.3", - "hash-base": "~3.0", - "pbkdf2": "^3.1.2", + "pbkdf2": "^3.1.3", "safe-buffer": "^5.2.1" }, "engines": { - "node": ">= 0.10" + "node": ">= 0.12" } }, "node_modules/parse-json": { @@ -8937,57 +8935,23 @@ } }, "node_modules/pbkdf2": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/pbkdf2/-/pbkdf2-3.1.3.tgz", - "integrity": "sha512-wfRLBZ0feWRhCIkoMB6ete7czJcnNnqRpcoWQBLqatqXXmelSRqfdDK4F3u9T2s2cXas/hQJcryI/4lAL+XTlA==", + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/pbkdf2/-/pbkdf2-3.1.4.tgz", + "integrity": "sha512-0yPXNT01PxSUdkIxL85Fd+yPdeCcvGwFPAAHbR3Z2ukMERcRrJFfLUKK3oglbQ9eUPeX6qDY3QiELqiDarZYUQ==", "dev": true, "license": "MIT", "dependencies": { - "create-hash": "~1.1.3", + "create-hash": "^1.2.0", "create-hmac": "^1.1.7", - "ripemd160": "=2.0.1", + "ripemd160": "^2.0.3", "safe-buffer": "^5.2.1", - "sha.js": "^2.4.11", - "to-buffer": "^1.2.0" + "sha.js": "^2.4.12", + "to-buffer": "^1.2.1" }, "engines": { "node": ">=0.12" } }, - "node_modules/pbkdf2/node_modules/create-hash": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/create-hash/-/create-hash-1.1.3.tgz", - "integrity": "sha512-snRpch/kwQhcdlnZKYanNF1m0RDlrCdSKQaH87w1FCFPVPNCQ/Il9QJKAX2jVBZddRdaHBMC+zXa9Gw9tmkNUA==", - "dev": true, - "license": "MIT", - "dependencies": { - "cipher-base": "^1.0.1", - "inherits": "^2.0.1", - "ripemd160": "^2.0.0", - "sha.js": "^2.4.0" - } - }, - "node_modules/pbkdf2/node_modules/hash-base": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/hash-base/-/hash-base-2.0.2.tgz", - "integrity": "sha512-0TROgQ1/SxE6KmxWSvXHvRj90/Xo1JvZShofnYF+f6ZsGtR4eES7WfrQzPalmyagfKZCXpVnitiRebZulWsbiw==", - "dev": true, - "license": "MIT", - "dependencies": { - "inherits": "^2.0.1" - } - }, - "node_modules/pbkdf2/node_modules/ripemd160": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/ripemd160/-/ripemd160-2.0.1.tgz", - "integrity": "sha512-J7f4wutN8mdbV08MJnXibYpCOPHR+yzy+iQ/AsjMv2j8cLavQ8VGagDFUwwTAdF8FmRKVeNpbTTEwNHCW1g94w==", - "dev": true, - "license": "MIT", - "dependencies": { - "hash-base": "^2.0.0", - "inherits": "^2.0.1" - } - }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -9961,16 +9925,16 @@ } }, "node_modules/regexpu-core": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-6.3.1.tgz", - "integrity": "sha512-DzcswPr252wEr7Qz8AyAVbfyBDKLoYp6eRA1We2Fa9qirRFSdtkP5sHr3yglDKy2BbA0fd2T+j/CUSKes3FeVQ==", + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-6.4.0.tgz", + "integrity": "sha512-0ghuzq67LI9bLXpOX/ISfve/Mq33a4aFRzoQYhnnok1JOFpmE/A2TBGkNVenOGEeSBCjIiWcc6MVOG5HEQv0sA==", "dev": true, "license": "MIT", "dependencies": { "regenerate": "^1.4.2", "regenerate-unicode-properties": "^10.2.2", "regjsgen": "^0.8.0", - "regjsparser": "^0.12.0", + "regjsparser": "^0.13.0", "unicode-match-property-ecmascript": "^2.0.0", "unicode-match-property-value-ecmascript": "^2.2.1" }, @@ -9986,31 +9950,18 @@ "license": "MIT" }, "node_modules/regjsparser": { - "version": "0.12.0", - "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.12.0.tgz", - "integrity": "sha512-cnE+y8bz4NhMjISKbgeVJtqNbtf5QpjZP+Bslo+UqkIt9QPnX9q095eiRRASJG1/tz6dlNr6Z5NsBiWYokp6EQ==", + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.13.0.tgz", + "integrity": "sha512-NZQZdC5wOE/H3UT28fVGL+ikOZcEzfMGk/c3iN9UGxzWHMa1op7274oyiUVrAG4B2EuFhus8SvkaYnhvW92p9Q==", "dev": true, "license": "BSD-2-Clause", "dependencies": { - "jsesc": "~3.0.2" + "jsesc": "~3.1.0" }, "bin": { "regjsparser": "bin/parser" } }, - "node_modules/regjsparser/node_modules/jsesc": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.0.2.tgz", - "integrity": "sha512-xKqzzWXDttJuOcawBt4KnKHHIf5oQ/Cxax+0PWFG+DFDgHNAdi+TXECADI+RYiFUMmx8792xsMbbgXj4CwnP4g==", - "dev": true, - "license": "MIT", - "bin": { - "jsesc": "bin/jsesc" - }, - "engines": { - "node": ">=6" - } - }, "node_modules/relateurl": { "version": "0.2.7", "resolved": "https://registry.npmjs.org/relateurl/-/relateurl-0.2.7.tgz", @@ -10148,20 +10099,39 @@ } }, "node_modules/ripemd160": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/ripemd160/-/ripemd160-2.0.2.tgz", - "integrity": "sha512-ii4iagi25WusVoiC4B4lq7pbXfAp3D9v5CwfkY33vffw2+pkDjY1D8GaN7spsxvCSx8dkPqOZCEZyfxcmJG2IA==", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/ripemd160/-/ripemd160-2.0.3.tgz", + "integrity": "sha512-5Di9UC0+8h1L6ZD2d7awM7E/T4uA1fJRlx6zk/NvdCCVEoAnFqvHmCuNeIKoCeIixBX/q8uM+6ycDvF8woqosA==", "dev": true, "license": "MIT", "dependencies": { - "hash-base": "^3.0.0", - "inherits": "^2.0.1" + "hash-base": "^3.1.2", + "inherits": "^2.0.4" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/ripemd160/node_modules/hash-base": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/hash-base/-/hash-base-3.1.2.tgz", + "integrity": "sha512-Bb33KbowVTIj5s7Ked1OsqHUeCpz//tPwR+E2zJgJKo9Z5XolZ9b6bdUgjmYlwnWhoOQKoTd1TYToZGn5mAYOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "inherits": "^2.0.4", + "readable-stream": "^2.3.8", + "safe-buffer": "^5.2.1", + "to-buffer": "^1.2.1" + }, + "engines": { + "node": ">= 0.8" } }, "node_modules/rollup": { - "version": "4.52.0", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.52.0.tgz", - "integrity": "sha512-+IuescNkTJQgX7AkIDtITipZdIGcWF0pnVvZTWStiazUmcGA2ag8dfg0urest2XlXUi9kuhfQ+qmdc5Stc3z7g==", + "version": "4.52.2", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.52.2.tgz", + "integrity": "sha512-I25/2QgoROE1vYV+NQ1En9T9UFB9Cmfm2CJ83zZOlaDpvz29wGQSZXWKw7MiNXau7wYgB/T9fVIdIuEQ+KbiiA==", "dev": true, "license": "MIT", "dependencies": { @@ -10175,28 +10145,28 @@ "npm": ">=8.0.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.52.0", - "@rollup/rollup-android-arm64": "4.52.0", - "@rollup/rollup-darwin-arm64": "4.52.0", - "@rollup/rollup-darwin-x64": "4.52.0", - "@rollup/rollup-freebsd-arm64": "4.52.0", - "@rollup/rollup-freebsd-x64": "4.52.0", - "@rollup/rollup-linux-arm-gnueabihf": "4.52.0", - "@rollup/rollup-linux-arm-musleabihf": "4.52.0", - "@rollup/rollup-linux-arm64-gnu": "4.52.0", - "@rollup/rollup-linux-arm64-musl": "4.52.0", - "@rollup/rollup-linux-loong64-gnu": "4.52.0", - "@rollup/rollup-linux-ppc64-gnu": "4.52.0", - "@rollup/rollup-linux-riscv64-gnu": "4.52.0", - "@rollup/rollup-linux-riscv64-musl": "4.52.0", - "@rollup/rollup-linux-s390x-gnu": "4.52.0", - "@rollup/rollup-linux-x64-gnu": "4.52.0", - "@rollup/rollup-linux-x64-musl": "4.52.0", - "@rollup/rollup-openharmony-arm64": "4.52.0", - "@rollup/rollup-win32-arm64-msvc": "4.52.0", - "@rollup/rollup-win32-ia32-msvc": "4.52.0", - "@rollup/rollup-win32-x64-gnu": "4.52.0", - "@rollup/rollup-win32-x64-msvc": "4.52.0", + "@rollup/rollup-android-arm-eabi": "4.52.2", + "@rollup/rollup-android-arm64": "4.52.2", + "@rollup/rollup-darwin-arm64": "4.52.2", + "@rollup/rollup-darwin-x64": "4.52.2", + "@rollup/rollup-freebsd-arm64": "4.52.2", + "@rollup/rollup-freebsd-x64": "4.52.2", + "@rollup/rollup-linux-arm-gnueabihf": "4.52.2", + "@rollup/rollup-linux-arm-musleabihf": "4.52.2", + "@rollup/rollup-linux-arm64-gnu": "4.52.2", + "@rollup/rollup-linux-arm64-musl": "4.52.2", + "@rollup/rollup-linux-loong64-gnu": "4.52.2", + "@rollup/rollup-linux-ppc64-gnu": "4.52.2", + "@rollup/rollup-linux-riscv64-gnu": "4.52.2", + "@rollup/rollup-linux-riscv64-musl": "4.52.2", + "@rollup/rollup-linux-s390x-gnu": "4.52.2", + "@rollup/rollup-linux-x64-gnu": "4.52.2", + "@rollup/rollup-linux-x64-musl": "4.52.2", + "@rollup/rollup-openharmony-arm64": "4.52.2", + "@rollup/rollup-win32-arm64-msvc": "4.52.2", + "@rollup/rollup-win32-ia32-msvc": "4.52.2", + "@rollup/rollup-win32-x64-gnu": "4.52.2", + "@rollup/rollup-win32-x64-msvc": "4.52.2", "fsevents": "~2.3.2" } }, @@ -10252,9 +10222,9 @@ "license": "MIT" }, "node_modules/sass": { - "version": "1.93.0", - "resolved": "https://registry.npmjs.org/sass/-/sass-1.93.0.tgz", - "integrity": "sha512-CQi5/AzCwiubU3dSqRDJ93RfOfg/hhpW1l6wCIvolmehfwgCI35R/0QDs1+R+Ygrl8jFawwwIojE2w47/mf94A==", + "version": "1.93.1", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.93.1.tgz", + "integrity": "sha512-wLAeLB7IksO2u+cCfhHqcy7/2ZUMPp/X2oV6+LjmweTqgjhOKrkaE/Q1wljxtco5EcOcupZ4c981X0gpk5Tiag==", "dev": true, "license": "MIT", "dependencies": { @@ -11569,9 +11539,9 @@ } }, "node_modules/vite": { - "version": "7.1.6", - "resolved": "https://registry.npmjs.org/vite/-/vite-7.1.6.tgz", - "integrity": "sha512-SRYIB8t/isTwNn8vMB3MR6E+EQZM/WG1aKmmIUCfDXfVvKfc20ZpamngWHKzAmmu9ppsgxsg4b2I7c90JZudIQ==", + "version": "7.1.7", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.1.7.tgz", + "integrity": "sha512-VbA8ScMvAISJNJVbRDTJdCwqQoAareR/wutevKanhR2/1EkoXVZVkkORaYm/tNVCjP/UDTKtcw3bAkwOUdedmA==", "dev": true, "license": "MIT", "dependencies": { From 855bc2f8e742f5f54e6a6ebb777e6980ef279824 Mon Sep 17 00:00:00 2001 From: Sander Dorigo Date: Wed, 24 Sep 2025 14:39:54 +0200 Subject: [PATCH 34/58] attempted fix for #10956 --- .../V1/Controllers/Webhook/ShowController.php | 27 +++++++++++-------- 1 file changed, 16 insertions(+), 11 deletions(-) diff --git a/app/Api/V1/Controllers/Webhook/ShowController.php b/app/Api/V1/Controllers/Webhook/ShowController.php index c0a27d5fce..a24d5806f4 100644 --- a/app/Api/V1/Controllers/Webhook/ShowController.php +++ b/app/Api/V1/Controllers/Webhook/ShowController.php @@ -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()'); From 0aa90b945395180f7048a218b8f0b496457ab2d1 Mon Sep 17 00:00:00 2001 From: James Cole Date: Wed, 24 Sep 2025 19:51:39 +0200 Subject: [PATCH 35/58] Fix #10960 --- app/Exceptions/GracefulNotFoundHandler.php | 1 + 1 file changed, 1 insertion(+) diff --git a/app/Exceptions/GracefulNotFoundHandler.php b/app/Exceptions/GracefulNotFoundHandler.php index fe0ca9c896..b2feef6ea6 100644 --- a/app/Exceptions/GracefulNotFoundHandler.php +++ b/app/Exceptions/GracefulNotFoundHandler.php @@ -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')); From 62c5440605347895cdcf2480026bead32d6a8139 Mon Sep 17 00:00:00 2001 From: James Cole Date: Thu, 25 Sep 2025 19:07:02 +0200 Subject: [PATCH 36/58] Add table for period statistics, and see what happens re: performance. --- app/Models/Account.php | 8 + app/Models/PeriodStatistic.php | 56 +++ app/Providers/FireflyServiceProvider.php | 3 + .../PeriodStatisticRepository.php | 65 +++ .../PeriodStatisticRepositoryInterface.php | 38 ++ .../Http/Controllers/PeriodOverview.php | 387 ++++++++++-------- ..._09_25_175248_create_period_statistics.php | 47 +++ 7 files changed, 433 insertions(+), 171 deletions(-) create mode 100644 app/Models/PeriodStatistic.php create mode 100644 app/Repositories/PeriodStatistic/PeriodStatisticRepository.php create mode 100644 app/Repositories/PeriodStatistic/PeriodStatisticRepositoryInterface.php create mode 100644 database/migrations/2025_09_25_175248_create_period_statistics.php diff --git a/app/Models/Account.php b/app/Models/Account.php index e76ba418ee..82c6369aff 100644 --- a/app/Models/Account.php +++ b/app/Models/Account.php @@ -241,4 +241,12 @@ class Account extends Model get: static fn ($value) => (string)$value, ); } + + public function primaryPeriodStatistics(): MorphMany + + { + + return $this->morphMany(PeriodStatistic::class, 'primary_statable'); + + } } diff --git a/app/Models/PeriodStatistic.php b/app/Models/PeriodStatistic.php new file mode 100644 index 0000000000..0eb7d1f9b7 --- /dev/null +++ b/app/Models/PeriodStatistic.php @@ -0,0 +1,56 @@ + 'datetime', + 'updated_at' => 'datetime', + 'deleted_at' => 'datetime', + 'date' => 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(); + + } + + // +} diff --git a/app/Providers/FireflyServiceProvider.php b/app/Providers/FireflyServiceProvider.php index 02ff3d729b..cff9567090 100644 --- a/app/Providers/FireflyServiceProvider.php +++ b/app/Providers/FireflyServiceProvider.php @@ -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 { diff --git a/app/Repositories/PeriodStatistic/PeriodStatisticRepository.php b/app/Repositories/PeriodStatistic/PeriodStatisticRepository.php new file mode 100644 index 0000000000..f7665f2b6d --- /dev/null +++ b/app/Repositories/PeriodStatistic/PeriodStatisticRepository.php @@ -0,0 +1,65 @@ +. + */ + +namespace FireflyIII\Repositories\PeriodStatistic; + +use Carbon\Carbon; +use FireflyIII\Models\PeriodStatistic; +use Illuminate\Database\Eloquent\Model; +use Illuminate\Support\Collection; + +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(); + return $stat; + } +} diff --git a/app/Repositories/PeriodStatistic/PeriodStatisticRepositoryInterface.php b/app/Repositories/PeriodStatistic/PeriodStatisticRepositoryInterface.php new file mode 100644 index 0000000000..3d6245022a --- /dev/null +++ b/app/Repositories/PeriodStatistic/PeriodStatisticRepositoryInterface.php @@ -0,0 +1,38 @@ +. + */ + +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; + +} diff --git a/app/Support/Http/Controllers/PeriodOverview.php b/app/Support/Http/Controllers/PeriodOverview.php index afbc198ce2..6944eb5f26 100644 --- a/app/Support/Http/Controllers/PeriodOverview.php +++ b/app/Support/Http/Controllers/PeriodOverview.php @@ -30,11 +30,13 @@ 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\Facades\Log; @@ -67,8 +69,9 @@ use Illuminate\Support\Facades\Log; */ trait PeriodOverview { - protected AccountRepositoryInterface $accountRepository; - protected JournalRepositoryInterface $journalRepos; + protected AccountRepositoryInterface $accountRepository; + protected JournalRepositoryInterface $journalRepos; + protected PeriodStatisticRepositoryInterface $periodStatisticRepo; /** * This method returns "period entries", so nov-2015, dec-2015, etc. (this depends on the users session range) @@ -79,71 +82,107 @@ 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'), $end->format('Y-m-d'))); + $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 = []; - - // 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 + $dates = Navigation::blockPeriods($start, $end, $range); + $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['start'], $currentDate['end']); } - $timer->stop('account-period-loop'); - $cache->store($entries); - $timer->stop('account-period-total'); Log::debug('End of getAccountPeriodOverview()'); return $entries; } + protected function getSingleAccountPeriod(Account $account, 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, $end), + 'route' => route('accounts.show', [$account->id, $start->format('Y-m-d'), $start->format('Y-m-d')]), + 'total_transactions' => 0, + ]; + 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 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->periodStatisticRepo->findPeriodStatistic($account, $start, $end, $type); + + // nothing found, regenerate them. + if (0 === $statistics->count()) { + $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, $transactions, $start, $end); + break; + case 'earned': + $result = $this->filterTransactionsByType(TransactionTypeEnum::DEPOSIT, $transactions, $start, $end); + break; + case 'transferred_in': + $result = $this->filterTransfers('in', $transactions, $start, $end); + break; + case 'transferred_away': + $result = $this->filterTransfers('away', $transactions, $start, $end); + break; + } + // each result must be grouped by currency, then saved as period statistic. + $grouped = $this->groupByCurrency($result); + + // TODO save as statistic. + $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; + } + + protected function saveGroupedAsStatistics(Account $account, Carbon $start, Carbon $end, string $type, array $array): void + { + unset($array['count']); + foreach ($array as $entry) { + $this->periodStatisticRepo->saveStatistic($account, $entry['currency_id'], $start, $end, $type, $entry['count'], $entry['amount']); + } + } + private function filterTransactionsByType(TransactionTypeEnum $type, array $transactions, Carbon $start, Carbon $end): array { - $result = []; - $filtered = []; + $result = []; /** - * @var int $index + * @var int $index * @var array $item */ foreach ($transactions as $index => $item) { @@ -153,25 +192,21 @@ trait PeriodOverview $result[] = $item; unset($transactions[$index]); } - if (!$fits) { - $filtered[] = $item; - } } - return [$filtered, $result]; + return $result; } private function filterTransfers(string $direction, array $transactions, Carbon $start, Carbon $end): array { - $result = []; - $filtered = []; + $result = []; /** - * @var int $index + * @var int $index * @var array $item */ foreach ($transactions as $index => $item) { - $date = Carbon::parse($item['date']); + $date = Carbon::parse($item['date']); if ($date >= $start && $date <= $end) { if ('away' === $direction && -1 === bccomp((string)$item['amount'], '0')) { $result[] = $item; @@ -184,25 +219,34 @@ trait PeriodOverview continue; } } - $filtered[] = $item; } - return [$filtered, $result]; + return $result; } private function groupByCurrency(array $journals): array { - $return = []; + Log::debug('groupByCurrency()'); + $return = [ + 'count' => 0, + ]; /** @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 (!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'; @@ -233,6 +277,7 @@ trait PeriodOverview $return[$currencyId]['amount'] = bcadd($return[$currencyId]['amount'], $amount); ++$return[$currencyId]['count']; + ++$return['count']; } return $return; @@ -245,11 +290,11 @@ trait PeriodOverview */ protected function getCategoryPeriodOverview(Category $category, Carbon $start, Carbon $end): array { - $range = Navigation::getViewRange(true); + $range = Navigation::getViewRange(true); [$start, $end] = $end < $start ? [$end, $start] : [$start, $end]; // properties for entries with their amounts. - $cache = new CacheProperties(); + $cache = new CacheProperties(); $cache->addProperty($start); $cache->addProperty($end); $cache->addProperty($range); @@ -261,32 +306,32 @@ trait PeriodOverview } /** @var array $dates */ - $dates = Navigation::blockPeriods($start, $end, $range); - $entries = []; + $dates = Navigation::blockPeriods($start, $end, $range); + $entries = []; // collect all expenses in this period: /** @var GroupCollectorInterface $collector */ - $collector = app(GroupCollectorInterface::class); + $collector = app(GroupCollectorInterface::class); $collector->setCategory($category); $collector->setRange($start, $end); $collector->setTypes([TransactionTypeEnum::DEPOSIT->value]); - $earnedSet = $collector->getExtractedJournals(); + $earnedSet = $collector->getExtractedJournals(); // collect all income in this period: /** @var GroupCollectorInterface $collector */ - $collector = app(GroupCollectorInterface::class); + $collector = app(GroupCollectorInterface::class); $collector->setCategory($category); $collector->setRange($start, $end); $collector->setTypes([TransactionTypeEnum::WITHDRAWAL->value]); - $spentSet = $collector->getExtractedJournals(); + $spentSet = $collector->getExtractedJournals(); // collect all transfers in this period: /** @var GroupCollectorInterface $collector */ - $collector = app(GroupCollectorInterface::class); + $collector = app(GroupCollectorInterface::class); $collector->setCategory($category); $collector->setRange($start, $end); $collector->setTypes([TransactionTypeEnum::TRANSFER->value]); - $transferSet = $collector->getExtractedJournals(); + $transferSet = $collector->getExtractedJournals(); foreach ($dates as $currentDate) { $spent = $this->filterJournalsByDate($spentSet, $currentDate['start'], $currentDate['end']); $earned = $this->filterJournalsByDate($earnedSet, $currentDate['start'], $currentDate['end']); @@ -294,17 +339,17 @@ trait PeriodOverview $title = Navigation::periodShow($currentDate['end'], $currentDate['period']); $entries[] = [ - 'transactions' => 0, - 'title' => $title, - 'route' => route( - 'categories.show', - [$category->id, $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), - ]; + 'transactions' => 0, + 'title' => $title, + 'route' => route( + 'categories.show', + [$category->id, $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), + ]; } $cache->store($entries); @@ -337,11 +382,11 @@ trait PeriodOverview */ protected function getNoBudgetPeriodOverview(Carbon $start, Carbon $end): array { - $range = Navigation::getViewRange(true); + $range = Navigation::getViewRange(true); [$start, $end] = $end < $start ? [$end, $start] : [$start, $end]; - $cache = new CacheProperties(); + $cache = new CacheProperties(); $cache->addProperty($start); $cache->addProperty($end); $cache->addProperty($this->convertToPrimary); @@ -352,28 +397,28 @@ trait PeriodOverview } /** @var array $dates */ - $dates = Navigation::blockPeriods($start, $end, $range); - $entries = []; + $dates = Navigation::blockPeriods($start, $end, $range); + $entries = []; // get all expenses without a budget. /** @var GroupCollectorInterface $collector */ - $collector = app(GroupCollectorInterface::class); + $collector = app(GroupCollectorInterface::class); $collector->setRange($start, $end)->withoutBudget()->withAccountInformation()->setTypes([TransactionTypeEnum::WITHDRAWAL->value]); - $journals = $collector->getExtractedJournals(); + $journals = $collector->getExtractedJournals(); foreach ($dates as $currentDate) { $set = $this->filterJournalsByDate($journals, $currentDate['start'], $currentDate['end']); $title = Navigation::periodShow($currentDate['end'], $currentDate['period']); $entries[] = [ - 'title' => $title, - 'route' => route('budgets.no-budget', [$currentDate['start']->format('Y-m-d'), $currentDate['end']->format('Y-m-d')]), - 'total_transactions' => count($set), - 'spent' => $this->groupByCurrency($set), - 'earned' => [], - 'transferred_away' => [], - 'transferred_in' => [], - ]; + 'title' => $title, + 'route' => route('budgets.no-budget', [$currentDate['start']->format('Y-m-d'), $currentDate['end']->format('Y-m-d')]), + 'total_transactions' => count($set), + 'spent' => $this->groupByCurrency($set), + 'earned' => [], + 'transferred_away' => [], + 'transferred_in' => [], + ]; } $cache->store($entries); @@ -390,38 +435,38 @@ trait PeriodOverview protected function getNoCategoryPeriodOverview(Carbon $theDate): array { app('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); + $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'))); // properties for cache - $dates = Navigation::blockPeriods($start, $end, $range); - $entries = []; + $dates = Navigation::blockPeriods($start, $end, $range); + $entries = []; // collect all expenses in this period: /** @var GroupCollectorInterface $collector */ - $collector = app(GroupCollectorInterface::class); + $collector = app(GroupCollectorInterface::class); $collector->withoutCategory(); $collector->setRange($start, $end); $collector->setTypes([TransactionTypeEnum::DEPOSIT->value]); - $earnedSet = $collector->getExtractedJournals(); + $earnedSet = $collector->getExtractedJournals(); // collect all income in this period: /** @var GroupCollectorInterface $collector */ - $collector = app(GroupCollectorInterface::class); + $collector = app(GroupCollectorInterface::class); $collector->withoutCategory(); $collector->setRange($start, $end); $collector->setTypes([TransactionTypeEnum::WITHDRAWAL->value]); - $spentSet = $collector->getExtractedJournals(); + $spentSet = $collector->getExtractedJournals(); // collect all transfers in this period: /** @var GroupCollectorInterface $collector */ - $collector = app(GroupCollectorInterface::class); + $collector = app(GroupCollectorInterface::class); $collector->withoutCategory(); $collector->setRange($start, $end); $collector->setTypes([TransactionTypeEnum::TRANSFER->value]); @@ -435,13 +480,13 @@ trait PeriodOverview $title = Navigation::periodShow($currentDate['end'], $currentDate['period']); $entries[] = [ - 'title' => $title, - 'route' => route('categories.no-category', [$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('categories.no-category', [$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), + ]; } app('log')->debug('End of loops'); @@ -455,11 +500,11 @@ trait PeriodOverview */ protected function getTagPeriodOverview(Tag $tag, Carbon $start, Carbon $end): array // period overview for tags. { - $range = Navigation::getViewRange(true); + $range = Navigation::getViewRange(true); [$start, $end] = $end < $start ? [$end, $start] : [$start, $end]; // properties for cache - $cache = new CacheProperties(); + $cache = new CacheProperties(); $cache->addProperty($start); $cache->addProperty($end); $cache->addProperty('tag-period-entries'); @@ -469,37 +514,37 @@ trait PeriodOverview } /** @var array $dates */ - $dates = Navigation::blockPeriods($start, $end, $range); - $entries = []; + $dates = Navigation::blockPeriods($start, $end, $range); + $entries = []; // collect all expenses in this period: /** @var GroupCollectorInterface $collector */ - $collector = app(GroupCollectorInterface::class); + $collector = app(GroupCollectorInterface::class); $collector->setTag($tag); $collector->setRange($start, $end); $collector->setTypes([TransactionTypeEnum::DEPOSIT->value]); - $earnedSet = $collector->getExtractedJournals(); + $earnedSet = $collector->getExtractedJournals(); // collect all income in this period: /** @var GroupCollectorInterface $collector */ - $collector = app(GroupCollectorInterface::class); + $collector = app(GroupCollectorInterface::class); $collector->setTag($tag); $collector->setRange($start, $end); $collector->setTypes([TransactionTypeEnum::WITHDRAWAL->value]); - $spentSet = $collector->getExtractedJournals(); + $spentSet = $collector->getExtractedJournals(); // collect all transfers in this period: /** @var GroupCollectorInterface $collector */ - $collector = app(GroupCollectorInterface::class); + $collector = app(GroupCollectorInterface::class); $collector->setTag($tag); $collector->setRange($start, $end); $collector->setTypes([TransactionTypeEnum::TRANSFER->value]); - $transferSet = $collector->getExtractedJournals(); + $transferSet = $collector->getExtractedJournals(); // filer all of them: - $earnedSet = $this->filterJournalsByTag($earnedSet, $tag); - $spentSet = $this->filterJournalsByTag($spentSet, $tag); - $transferSet = $this->filterJournalsByTag($transferSet, $tag); + $earnedSet = $this->filterJournalsByTag($earnedSet, $tag); + $spentSet = $this->filterJournalsByTag($spentSet, $tag); + $transferSet = $this->filterJournalsByTag($transferSet, $tag); foreach ($dates as $currentDate) { $spent = $this->filterJournalsByDate($spentSet, $currentDate['start'], $currentDate['end']); @@ -508,17 +553,17 @@ trait PeriodOverview $title = Navigation::periodShow($currentDate['end'], $currentDate['period']); $entries[] = [ - 'transactions' => 0, - 'title' => $title, - 'route' => route( - 'tags.show', - [$tag->id, $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), - ]; + 'transactions' => 0, + 'title' => $title, + 'route' => route( + 'tags.show', + [$tag->id, $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), + ]; } return $entries; @@ -528,7 +573,7 @@ trait PeriodOverview { $return = []; foreach ($set as $entry) { - $found = false; + $found = false; /** @var array $localTag */ foreach ($entry['tags'] as $localTag) { @@ -550,12 +595,12 @@ trait PeriodOverview */ protected function getTransactionPeriodOverview(string $transactionType, Carbon $start, Carbon $end): array { - $range = Navigation::getViewRange(true); - $types = config(sprintf('firefly.transactionTypesByType.%s', $transactionType)); + $range = Navigation::getViewRange(true); + $types = config(sprintf('firefly.transactionTypesByType.%s', $transactionType)); [$start, $end] = $end < $start ? [$end, $start] : [$start, $end]; // properties for cache - $cache = new CacheProperties(); + $cache = new CacheProperties(); $cache->addProperty($start); $cache->addProperty($end); $cache->addProperty('transactions-period-entries'); @@ -565,16 +610,16 @@ trait PeriodOverview } /** @var array $dates */ - $dates = Navigation::blockPeriods($start, $end, $range); - $entries = []; - $spent = []; - $earned = []; - $transferred = []; + $dates = Navigation::blockPeriods($start, $end, $range); + $entries = []; + $spent = []; + $earned = []; + $transferred = []; // collect all journals in this period (regardless of type) - $collector = app(GroupCollectorInterface::class); + $collector = app(GroupCollectorInterface::class); $collector->setTypes($types)->setRange($start, $end); - $genericSet = $collector->getExtractedJournals(); - $loops = 0; + $genericSet = $collector->getExtractedJournals(); + $loops = 0; foreach ($dates as $currentDate) { $title = Navigation::periodShow($currentDate['end'], $currentDate['period']); @@ -592,14 +637,14 @@ 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; } diff --git a/database/migrations/2025_09_25_175248_create_period_statistics.php b/database/migrations/2025_09_25_175248_create_period_statistics.php new file mode 100644 index 0000000000..e04afaa9de --- /dev/null +++ b/database/migrations/2025_09_25_175248_create_period_statistics.php @@ -0,0 +1,47 @@ +id(); + $table->timestamps(); + $table->softDeletes(); + $table->integer('primary_statable_id', false, true)->nullable(); + $table->string('primary_statable_type', 255)->nullable(); + + $table->integer('secondary_statable_id', false, true)->nullable(); + $table->string('secondary_statable_type', 255)->nullable(); + + $table->integer('tertiary_statable_id', false, true)->nullable(); + $table->string('tertiary_statable_type', 255)->nullable(); + + $table->integer('transaction_currency_id', false, true); + $table->foreign('transaction_currency_id')->references('id')->on('transaction_currencies')->onDelete('cascade'); + + $table->dateTime('start')->nullable(); + $table->string('start_tz', 50)->nullable(); + $table->dateTime('end')->nullable(); + $table->string('end_tz', 50)->nullable(); + $table->string('type',255); + $table->integer('count', false, true)->default(0); + $table->decimal('amount', 32, 12); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('period_statistics'); + } +}; From 61e8d7d7a2afb096d06933b89f93a6e91c2e94c1 Mon Sep 17 00:00:00 2001 From: JC5 Date: Thu, 25 Sep 2025 19:11:11 +0200 Subject: [PATCH 37/58] =?UTF-8?q?=F0=9F=A4=96=20Auto=20commit=20for=20rele?= =?UTF-8?q?ase=20'develop'=20on=202025-09-25?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .ci/php-cs-fixer/composer.lock | 96 ++++++- .../V1/Controllers/Webhook/ShowController.php | 2 +- app/Models/Account.php | 1 - app/Models/PeriodStatistic.php | 8 +- .../PeriodStatisticRepository.php | 24 +- .../PeriodStatisticRepositoryInterface.php | 4 +- .../Http/Controllers/PeriodOverview.php | 247 ++++++++++-------- composer.lock | 62 ++--- config/firefly.php | 4 +- package-lock.json | 115 ++++---- 10 files changed, 332 insertions(+), 231 deletions(-) diff --git a/.ci/php-cs-fixer/composer.lock b/.ci/php-cs-fixer/composer.lock index a7ad9afcb3..18ee8f03de 100644 --- a/.ci/php-cs-fixer/composer.lock +++ b/.ci/php-cs-fixer/composer.lock @@ -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", diff --git a/app/Api/V1/Controllers/Webhook/ShowController.php b/app/Api/V1/Controllers/Webhook/ShowController.php index a24d5806f4..03249281c3 100644 --- a/app/Api/V1/Controllers/Webhook/ShowController.php +++ b/app/Api/V1/Controllers/Webhook/ShowController.php @@ -160,7 +160,7 @@ class ShowController extends Controller /** @var \FireflyIII\Models\WebhookTrigger $trigger */ - foreach($webhook->webhookTriggers as $trigger) { + foreach ($webhook->webhookTriggers as $trigger) { /** @var MessageGeneratorInterface $engine */ $engine = app(MessageGeneratorInterface::class); $engine->setUser(auth()->user()); diff --git a/app/Models/Account.php b/app/Models/Account.php index 82c6369aff..76d520856d 100644 --- a/app/Models/Account.php +++ b/app/Models/Account.php @@ -243,7 +243,6 @@ class Account extends Model } public function primaryPeriodStatistics(): MorphMany - { return $this->morphMany(PeriodStatistic::class, 'primary_statable'); diff --git a/app/Models/PeriodStatistic.php b/app/Models/PeriodStatistic.php index 0eb7d1f9b7..030735a00f 100644 --- a/app/Models/PeriodStatistic.php +++ b/app/Models/PeriodStatistic.php @@ -1,5 +1,7 @@ morphTo(); } + public function tertiaryStatable(): MorphTo { @@ -52,5 +54,5 @@ class PeriodStatistic extends Model } - // + } diff --git a/app/Repositories/PeriodStatistic/PeriodStatisticRepository.php b/app/Repositories/PeriodStatistic/PeriodStatisticRepository.php index f7665f2b6d..ae8914c0c0 100644 --- a/app/Repositories/PeriodStatistic/PeriodStatisticRepository.php +++ b/app/Repositories/PeriodStatistic/PeriodStatisticRepository.php @@ -1,4 +1,6 @@ primaryPeriodStatistics() - ->where('start', $start) - ->where('end', $end) - ->whereIn('type', $types) - ->get(); + ->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(); + ->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 = new PeriodStatistic(); $stat->primaryStatable()->associate($model); $stat->transaction_currency_id = $currencyId; $stat->start = $start; @@ -60,6 +63,7 @@ class PeriodStatisticRepository implements PeriodStatisticRepositoryInterface $stat->count = $count; $stat->type = $type; $stat->save(); + return $stat; } } diff --git a/app/Repositories/PeriodStatistic/PeriodStatisticRepositoryInterface.php b/app/Repositories/PeriodStatistic/PeriodStatisticRepositoryInterface.php index 3d6245022a..0b9a6bfbc0 100644 --- a/app/Repositories/PeriodStatistic/PeriodStatisticRepositoryInterface.php +++ b/app/Repositories/PeriodStatistic/PeriodStatisticRepositoryInterface.php @@ -1,4 +1,6 @@ accountRepository = app(AccountRepositoryInterface::class); $this->periodStatisticRepo = app(PeriodStatisticRepositoryInterface::class); $range = Navigation::getViewRange(true); - [$start, $end] = $end < $start ? [$end, $start] : [$start, $end]; + [$start, $end] = $end < $start ? [$end, $start] : [$start, $end]; + /** @var array $dates */ - $dates = Navigation::blockPeriods($start, $end, $range); - $entries = []; + $dates = Navigation::blockPeriods($start, $end, $range); + $entries = []; Log::debug(sprintf('Count of loops: %d', count($dates))); foreach ($dates as $currentDate) { $entries[] = $this->getSingleAccountPeriod($account, $currentDate['start'], $currentDate['end']); @@ -109,11 +110,12 @@ trait PeriodOverview 'total_transactions' => 0, ]; foreach ($types as $type) { - $set = $this->getSingleAccountPeriodByType($account, $start, $end, $type); + $set = $this->getSingleAccountPeriodByType($account, $start, $end, $type); $return['total_transactions'] += $set['count']; unset($set['count']); $return[$type] = $set; } + return $return; } @@ -125,36 +127,47 @@ trait PeriodOverview // nothing found, regenerate them. if (0 === $statistics->count()) { $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, $transactions, $start, $end); + break; + case 'earned': $result = $this->filterTransactionsByType(TransactionTypeEnum::DEPOSIT, $transactions, $start, $end); + break; + case 'transferred_in': $result = $this->filterTransfers('in', $transactions, $start, $end); + break; + case 'transferred_away': $result = $this->filterTransfers('away', $transactions, $start, $end); + break; } // each result must be grouped by currency, then saved as period statistic. - $grouped = $this->groupByCurrency($result); + $grouped = $this->groupByCurrency($result); // TODO save as statistic. $this->saveGroupedAsStatistics($account, $start, $end, $type, $grouped); + return $grouped; } - $grouped = [ + $grouped = [ 'count' => 0, ]; + /** @var PeriodStatistic $statistic */ - foreach($statistics as $statistic) { - $id = (int) $statistic->transaction_currency_id; - $currency = Amount::getTransactionCurrencyById($id); + foreach ($statistics as $statistic) { + $id = (int) $statistic->transaction_currency_id; + $currency = Amount::getTransactionCurrencyById($id); $grouped[$id] = [ 'amount' => (string) $statistic->amount, 'count' => (int) $statistic->count, @@ -166,6 +179,7 @@ trait PeriodOverview ]; $grouped['count'] += (int) $statistic->count; } + return $grouped; } @@ -182,7 +196,7 @@ trait PeriodOverview $result = []; /** - * @var int $index + * @var int $index * @var array $item */ foreach ($transactions as $index => $item) { @@ -202,7 +216,7 @@ trait PeriodOverview $result = []; /** - * @var int $index + * @var int $index * @var array $item */ foreach ($transactions as $index => $item) { @@ -237,16 +251,17 @@ trait PeriodOverview 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'; + $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'; @@ -290,11 +305,11 @@ trait PeriodOverview */ protected function getCategoryPeriodOverview(Category $category, Carbon $start, Carbon $end): array { - $range = Navigation::getViewRange(true); + $range = Navigation::getViewRange(true); [$start, $end] = $end < $start ? [$end, $start] : [$start, $end]; // properties for entries with their amounts. - $cache = new CacheProperties(); + $cache = new CacheProperties(); $cache->addProperty($start); $cache->addProperty($end); $cache->addProperty($range); @@ -306,32 +321,32 @@ trait PeriodOverview } /** @var array $dates */ - $dates = Navigation::blockPeriods($start, $end, $range); - $entries = []; + $dates = Navigation::blockPeriods($start, $end, $range); + $entries = []; // collect all expenses in this period: /** @var GroupCollectorInterface $collector */ - $collector = app(GroupCollectorInterface::class); + $collector = app(GroupCollectorInterface::class); $collector->setCategory($category); $collector->setRange($start, $end); $collector->setTypes([TransactionTypeEnum::DEPOSIT->value]); - $earnedSet = $collector->getExtractedJournals(); + $earnedSet = $collector->getExtractedJournals(); // collect all income in this period: /** @var GroupCollectorInterface $collector */ - $collector = app(GroupCollectorInterface::class); + $collector = app(GroupCollectorInterface::class); $collector->setCategory($category); $collector->setRange($start, $end); $collector->setTypes([TransactionTypeEnum::WITHDRAWAL->value]); - $spentSet = $collector->getExtractedJournals(); + $spentSet = $collector->getExtractedJournals(); // collect all transfers in this period: /** @var GroupCollectorInterface $collector */ - $collector = app(GroupCollectorInterface::class); + $collector = app(GroupCollectorInterface::class); $collector->setCategory($category); $collector->setRange($start, $end); $collector->setTypes([TransactionTypeEnum::TRANSFER->value]); - $transferSet = $collector->getExtractedJournals(); + $transferSet = $collector->getExtractedJournals(); foreach ($dates as $currentDate) { $spent = $this->filterJournalsByDate($spentSet, $currentDate['start'], $currentDate['end']); $earned = $this->filterJournalsByDate($earnedSet, $currentDate['start'], $currentDate['end']); @@ -339,17 +354,17 @@ trait PeriodOverview $title = Navigation::periodShow($currentDate['end'], $currentDate['period']); $entries[] = [ - 'transactions' => 0, - 'title' => $title, - 'route' => route( - 'categories.show', - [$category->id, $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), - ]; + 'transactions' => 0, + 'title' => $title, + 'route' => route( + 'categories.show', + [$category->id, $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), + ]; } $cache->store($entries); @@ -382,11 +397,11 @@ trait PeriodOverview */ protected function getNoBudgetPeriodOverview(Carbon $start, Carbon $end): array { - $range = Navigation::getViewRange(true); + $range = Navigation::getViewRange(true); [$start, $end] = $end < $start ? [$end, $start] : [$start, $end]; - $cache = new CacheProperties(); + $cache = new CacheProperties(); $cache->addProperty($start); $cache->addProperty($end); $cache->addProperty($this->convertToPrimary); @@ -397,28 +412,28 @@ trait PeriodOverview } /** @var array $dates */ - $dates = Navigation::blockPeriods($start, $end, $range); - $entries = []; + $dates = Navigation::blockPeriods($start, $end, $range); + $entries = []; // get all expenses without a budget. /** @var GroupCollectorInterface $collector */ - $collector = app(GroupCollectorInterface::class); + $collector = app(GroupCollectorInterface::class); $collector->setRange($start, $end)->withoutBudget()->withAccountInformation()->setTypes([TransactionTypeEnum::WITHDRAWAL->value]); - $journals = $collector->getExtractedJournals(); + $journals = $collector->getExtractedJournals(); foreach ($dates as $currentDate) { $set = $this->filterJournalsByDate($journals, $currentDate['start'], $currentDate['end']); $title = Navigation::periodShow($currentDate['end'], $currentDate['period']); $entries[] = [ - 'title' => $title, - 'route' => route('budgets.no-budget', [$currentDate['start']->format('Y-m-d'), $currentDate['end']->format('Y-m-d')]), - 'total_transactions' => count($set), - 'spent' => $this->groupByCurrency($set), - 'earned' => [], - 'transferred_away' => [], - 'transferred_in' => [], - ]; + 'title' => $title, + 'route' => route('budgets.no-budget', [$currentDate['start']->format('Y-m-d'), $currentDate['end']->format('Y-m-d')]), + 'total_transactions' => count($set), + 'spent' => $this->groupByCurrency($set), + 'earned' => [], + 'transferred_away' => [], + 'transferred_in' => [], + ]; } $cache->store($entries); @@ -435,38 +450,38 @@ trait PeriodOverview protected function getNoCategoryPeriodOverview(Carbon $theDate): array { app('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); + $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'))); // properties for cache - $dates = Navigation::blockPeriods($start, $end, $range); - $entries = []; + $dates = Navigation::blockPeriods($start, $end, $range); + $entries = []; // collect all expenses in this period: /** @var GroupCollectorInterface $collector */ - $collector = app(GroupCollectorInterface::class); + $collector = app(GroupCollectorInterface::class); $collector->withoutCategory(); $collector->setRange($start, $end); $collector->setTypes([TransactionTypeEnum::DEPOSIT->value]); - $earnedSet = $collector->getExtractedJournals(); + $earnedSet = $collector->getExtractedJournals(); // collect all income in this period: /** @var GroupCollectorInterface $collector */ - $collector = app(GroupCollectorInterface::class); + $collector = app(GroupCollectorInterface::class); $collector->withoutCategory(); $collector->setRange($start, $end); $collector->setTypes([TransactionTypeEnum::WITHDRAWAL->value]); - $spentSet = $collector->getExtractedJournals(); + $spentSet = $collector->getExtractedJournals(); // collect all transfers in this period: /** @var GroupCollectorInterface $collector */ - $collector = app(GroupCollectorInterface::class); + $collector = app(GroupCollectorInterface::class); $collector->withoutCategory(); $collector->setRange($start, $end); $collector->setTypes([TransactionTypeEnum::TRANSFER->value]); @@ -480,13 +495,13 @@ trait PeriodOverview $title = Navigation::periodShow($currentDate['end'], $currentDate['period']); $entries[] = [ - 'title' => $title, - 'route' => route('categories.no-category', [$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('categories.no-category', [$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), + ]; } app('log')->debug('End of loops'); @@ -500,11 +515,11 @@ trait PeriodOverview */ protected function getTagPeriodOverview(Tag $tag, Carbon $start, Carbon $end): array // period overview for tags. { - $range = Navigation::getViewRange(true); + $range = Navigation::getViewRange(true); [$start, $end] = $end < $start ? [$end, $start] : [$start, $end]; // properties for cache - $cache = new CacheProperties(); + $cache = new CacheProperties(); $cache->addProperty($start); $cache->addProperty($end); $cache->addProperty('tag-period-entries'); @@ -514,37 +529,37 @@ trait PeriodOverview } /** @var array $dates */ - $dates = Navigation::blockPeriods($start, $end, $range); - $entries = []; + $dates = Navigation::blockPeriods($start, $end, $range); + $entries = []; // collect all expenses in this period: /** @var GroupCollectorInterface $collector */ - $collector = app(GroupCollectorInterface::class); + $collector = app(GroupCollectorInterface::class); $collector->setTag($tag); $collector->setRange($start, $end); $collector->setTypes([TransactionTypeEnum::DEPOSIT->value]); - $earnedSet = $collector->getExtractedJournals(); + $earnedSet = $collector->getExtractedJournals(); // collect all income in this period: /** @var GroupCollectorInterface $collector */ - $collector = app(GroupCollectorInterface::class); + $collector = app(GroupCollectorInterface::class); $collector->setTag($tag); $collector->setRange($start, $end); $collector->setTypes([TransactionTypeEnum::WITHDRAWAL->value]); - $spentSet = $collector->getExtractedJournals(); + $spentSet = $collector->getExtractedJournals(); // collect all transfers in this period: /** @var GroupCollectorInterface $collector */ - $collector = app(GroupCollectorInterface::class); + $collector = app(GroupCollectorInterface::class); $collector->setTag($tag); $collector->setRange($start, $end); $collector->setTypes([TransactionTypeEnum::TRANSFER->value]); - $transferSet = $collector->getExtractedJournals(); + $transferSet = $collector->getExtractedJournals(); // filer all of them: - $earnedSet = $this->filterJournalsByTag($earnedSet, $tag); - $spentSet = $this->filterJournalsByTag($spentSet, $tag); - $transferSet = $this->filterJournalsByTag($transferSet, $tag); + $earnedSet = $this->filterJournalsByTag($earnedSet, $tag); + $spentSet = $this->filterJournalsByTag($spentSet, $tag); + $transferSet = $this->filterJournalsByTag($transferSet, $tag); foreach ($dates as $currentDate) { $spent = $this->filterJournalsByDate($spentSet, $currentDate['start'], $currentDate['end']); @@ -553,17 +568,17 @@ trait PeriodOverview $title = Navigation::periodShow($currentDate['end'], $currentDate['period']); $entries[] = [ - 'transactions' => 0, - 'title' => $title, - 'route' => route( - 'tags.show', - [$tag->id, $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), - ]; + 'transactions' => 0, + 'title' => $title, + 'route' => route( + 'tags.show', + [$tag->id, $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), + ]; } return $entries; @@ -573,7 +588,7 @@ trait PeriodOverview { $return = []; foreach ($set as $entry) { - $found = false; + $found = false; /** @var array $localTag */ foreach ($entry['tags'] as $localTag) { @@ -595,12 +610,12 @@ trait PeriodOverview */ protected function getTransactionPeriodOverview(string $transactionType, Carbon $start, Carbon $end): array { - $range = Navigation::getViewRange(true); - $types = config(sprintf('firefly.transactionTypesByType.%s', $transactionType)); + $range = Navigation::getViewRange(true); + $types = config(sprintf('firefly.transactionTypesByType.%s', $transactionType)); [$start, $end] = $end < $start ? [$end, $start] : [$start, $end]; // properties for cache - $cache = new CacheProperties(); + $cache = new CacheProperties(); $cache->addProperty($start); $cache->addProperty($end); $cache->addProperty('transactions-period-entries'); @@ -610,16 +625,16 @@ trait PeriodOverview } /** @var array $dates */ - $dates = Navigation::blockPeriods($start, $end, $range); - $entries = []; - $spent = []; - $earned = []; - $transferred = []; + $dates = Navigation::blockPeriods($start, $end, $range); + $entries = []; + $spent = []; + $earned = []; + $transferred = []; // collect all journals in this period (regardless of type) - $collector = app(GroupCollectorInterface::class); + $collector = app(GroupCollectorInterface::class); $collector->setTypes($types)->setRange($start, $end); - $genericSet = $collector->getExtractedJournals(); - $loops = 0; + $genericSet = $collector->getExtractedJournals(); + $loops = 0; foreach ($dates as $currentDate) { $title = Navigation::periodShow($currentDate['end'], $currentDate['period']); @@ -637,14 +652,14 @@ 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; } diff --git a/composer.lock b/composer.lock index de8828fde5..18cb3ef987 100644 --- a/composer.lock +++ b/composer.lock @@ -4235,16 +4235,16 @@ }, { "name": "paragonie/constant_time_encoding", - "version": "v3.1.1", + "version": "v3.1.3", "source": { "type": "git", "url": "https://github.com/paragonie/constant_time_encoding.git", - "reference": "5e9b582660b997de205a84c02a3aac7c060900c9" + "reference": "d5b01a39b3415c2cd581d3bd3a3575c1ebbd8e77" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/paragonie/constant_time_encoding/zipball/5e9b582660b997de205a84c02a3aac7c060900c9", - "reference": "5e9b582660b997de205a84c02a3aac7c060900c9", + "url": "https://api.github.com/repos/paragonie/constant_time_encoding/zipball/d5b01a39b3415c2cd581d3bd3a3575c1ebbd8e77", + "reference": "d5b01a39b3415c2cd581d3bd3a3575c1ebbd8e77", "shasum": "" }, "require": { @@ -4300,7 +4300,7 @@ "issues": "https://github.com/paragonie/constant_time_encoding/issues", "source": "https://github.com/paragonie/constant_time_encoding" }, - "time": "2025-09-22T21:00:33+00:00" + "time": "2025-09-24T15:06:41+00:00" }, { "name": "paragonie/random_compat", @@ -11406,16 +11406,16 @@ }, { "name": "phpstan/phpstan", - "version": "2.1.28", + "version": "2.1.29", "source": { "type": "git", - "url": "https://github.com/phpstan/phpstan.git", - "reference": "578fa296a166605d97b94091f724f1257185d278" + "url": "https://github.com/phpstan/phpstan-phar-composer-source.git", + "reference": "git" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan/zipball/578fa296a166605d97b94091f724f1257185d278", - "reference": "578fa296a166605d97b94091f724f1257185d278", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/d618573eed4a1b6b75e37b2e0b65ac65c885d88e", + "reference": "d618573eed4a1b6b75e37b2e0b65ac65c885d88e", "shasum": "" }, "require": { @@ -11460,7 +11460,7 @@ "type": "github" } ], - "time": "2025-09-19T08:58:49+00:00" + "time": "2025-09-25T06:58:18+00:00" }, { "name": "phpstan/phpstan-deprecation-rules", @@ -11559,16 +11559,16 @@ }, { "name": "phpunit/php-code-coverage", - "version": "12.3.8", + "version": "12.4.0", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-code-coverage.git", - "reference": "99e692c6a84708211f7536ba322bbbaef57ac7fc" + "reference": "67e8aed88f93d0e6e1cb7effe1a2dfc2fee6022c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/99e692c6a84708211f7536ba322bbbaef57ac7fc", - "reference": "99e692c6a84708211f7536ba322bbbaef57ac7fc", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/67e8aed88f93d0e6e1cb7effe1a2dfc2fee6022c", + "reference": "67e8aed88f93d0e6e1cb7effe1a2dfc2fee6022c", "shasum": "" }, "require": { @@ -11595,7 +11595,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "12.3.x-dev" + "dev-main": "12.4.x-dev" } }, "autoload": { @@ -11624,7 +11624,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/php-code-coverage/issues", "security": "https://github.com/sebastianbergmann/php-code-coverage/security/policy", - "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/12.3.8" + "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/12.4.0" }, "funding": [ { @@ -11644,7 +11644,7 @@ "type": "tidelift" } ], - "time": "2025-09-17T11:31:43+00:00" + "time": "2025-09-24T13:44:41+00:00" }, { "name": "phpunit/php-file-iterator", @@ -11893,16 +11893,16 @@ }, { "name": "phpunit/phpunit", - "version": "12.3.13", + "version": "12.3.14", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "44f15312c4968fa8102e491fc6f3746410819c16" + "reference": "13e9b2bea9327b094176147250d2c10319a10f5b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/44f15312c4968fa8102e491fc6f3746410819c16", - "reference": "44f15312c4968fa8102e491fc6f3746410819c16", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/13e9b2bea9327b094176147250d2c10319a10f5b", + "reference": "13e9b2bea9327b094176147250d2c10319a10f5b", "shasum": "" }, "require": { @@ -11925,7 +11925,7 @@ "sebastian/comparator": "^7.1.3", "sebastian/diff": "^7.0.0", "sebastian/environment": "^8.0.3", - "sebastian/exporter": "^7.0.1", + "sebastian/exporter": "^7.0.2", "sebastian/global-state": "^8.0.2", "sebastian/object-enumerator": "^7.0.0", "sebastian/type": "^6.0.3", @@ -11970,7 +11970,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/phpunit/issues", "security": "https://github.com/sebastianbergmann/phpunit/security/policy", - "source": "https://github.com/sebastianbergmann/phpunit/tree/12.3.13" + "source": "https://github.com/sebastianbergmann/phpunit/tree/12.3.14" }, "funding": [ { @@ -11994,7 +11994,7 @@ "type": "tidelift" } ], - "time": "2025-09-23T06:25:02+00:00" + "time": "2025-09-24T06:34:27+00:00" }, { "name": "rector/rector", @@ -12420,16 +12420,16 @@ }, { "name": "sebastian/exporter", - "version": "7.0.1", + "version": "7.0.2", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/exporter.git", - "reference": "b759164a8e02263784b662889cc6cbb686077af6" + "reference": "016951ae10980765e4e7aee491eb288c64e505b7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/b759164a8e02263784b662889cc6cbb686077af6", - "reference": "b759164a8e02263784b662889cc6cbb686077af6", + "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/016951ae10980765e4e7aee491eb288c64e505b7", + "reference": "016951ae10980765e4e7aee491eb288c64e505b7", "shasum": "" }, "require": { @@ -12486,7 +12486,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/exporter/issues", "security": "https://github.com/sebastianbergmann/exporter/security/policy", - "source": "https://github.com/sebastianbergmann/exporter/tree/7.0.1" + "source": "https://github.com/sebastianbergmann/exporter/tree/7.0.2" }, "funding": [ { @@ -12506,7 +12506,7 @@ "type": "tidelift" } ], - "time": "2025-09-22T05:39:29+00:00" + "time": "2025-09-24T06:16:11+00:00" }, { "name": "sebastian/global-state", diff --git a/config/firefly.php b/config/firefly.php index 4dbb9dfde7..3139bb0be9 100644 --- a/config/firefly.php +++ b/config/firefly.php @@ -78,8 +78,8 @@ return [ 'running_balance_column' => env('USE_RUNNING_BALANCE', false), // see cer.php for exchange rates feature flag. ], - 'version' => 'develop/2025-09-23', - 'build_time' => 1758652788, + 'version' => 'develop/2025-09-25', + 'build_time' => 1758820163, 'api_version' => '2.1.0', // field is no longer used. 'db_version' => 27, diff --git a/package-lock.json b/package-lock.json index e0ac00f144..d69416dc1e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3281,57 +3281,57 @@ } }, "node_modules/@vue/compiler-core": { - "version": "3.5.21", - "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.21.tgz", - "integrity": "sha512-8i+LZ0vf6ZgII5Z9XmUvrCyEzocvWT+TeR2VBUVlzIH6Tyv57E20mPZ1bCS+tbejgUgmjrEh7q/0F0bibskAmw==", + "version": "3.5.22", + "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.22.tgz", + "integrity": "sha512-jQ0pFPmZwTEiRNSb+i9Ow/I/cHv2tXYqsnHKKyCQ08irI2kdF5qmYedmF8si8mA7zepUFmJ2hqzS8CQmNOWOkQ==", "dev": true, "license": "MIT", "dependencies": { - "@babel/parser": "^7.28.3", - "@vue/shared": "3.5.21", + "@babel/parser": "^7.28.4", + "@vue/shared": "3.5.22", "entities": "^4.5.0", "estree-walker": "^2.0.2", "source-map-js": "^1.2.1" } }, "node_modules/@vue/compiler-dom": { - "version": "3.5.21", - "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.21.tgz", - "integrity": "sha512-jNtbu/u97wiyEBJlJ9kmdw7tAr5Vy0Aj5CgQmo+6pxWNQhXZDPsRr1UWPN4v3Zf82s2H3kF51IbzZ4jMWAgPlQ==", + "version": "3.5.22", + "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.22.tgz", + "integrity": "sha512-W8RknzUM1BLkypvdz10OVsGxnMAuSIZs9Wdx1vzA3mL5fNMN15rhrSCLiTm6blWeACwUwizzPVqGJgOGBEN/hA==", "dev": true, "license": "MIT", "dependencies": { - "@vue/compiler-core": "3.5.21", - "@vue/shared": "3.5.21" + "@vue/compiler-core": "3.5.22", + "@vue/shared": "3.5.22" } }, "node_modules/@vue/compiler-sfc": { - "version": "3.5.21", - "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.21.tgz", - "integrity": "sha512-SXlyk6I5eUGBd2v8Ie7tF6ADHE9kCR6mBEuPyH1nUZ0h6Xx6nZI29i12sJKQmzbDyr2tUHMhhTt51Z6blbkTTQ==", + "version": "3.5.22", + "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.22.tgz", + "integrity": "sha512-tbTR1zKGce4Lj+JLzFXDq36K4vcSZbJ1RBu8FxcDv1IGRz//Dh2EBqksyGVypz3kXpshIfWKGOCcqpSbyGWRJQ==", "dev": true, "license": "MIT", "dependencies": { - "@babel/parser": "^7.28.3", - "@vue/compiler-core": "3.5.21", - "@vue/compiler-dom": "3.5.21", - "@vue/compiler-ssr": "3.5.21", - "@vue/shared": "3.5.21", + "@babel/parser": "^7.28.4", + "@vue/compiler-core": "3.5.22", + "@vue/compiler-dom": "3.5.22", + "@vue/compiler-ssr": "3.5.22", + "@vue/shared": "3.5.22", "estree-walker": "^2.0.2", - "magic-string": "^0.30.18", + "magic-string": "^0.30.19", "postcss": "^8.5.6", "source-map-js": "^1.2.1" } }, "node_modules/@vue/compiler-ssr": { - "version": "3.5.21", - "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.21.tgz", - "integrity": "sha512-vKQ5olH5edFZdf5ZrlEgSO1j1DMA4u23TVK5XR1uMhvwnYvVdDF0nHXJUblL/GvzlShQbjhZZ2uvYmDlAbgo9w==", + "version": "3.5.22", + "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.22.tgz", + "integrity": "sha512-GdgyLvg4R+7T8Nk2Mlighx7XGxq/fJf9jaVofc3IL0EPesTE86cP/8DD1lT3h1JeZr2ySBvyqKQJgbS54IX1Ww==", "dev": true, "license": "MIT", "dependencies": { - "@vue/compiler-dom": "3.5.21", - "@vue/shared": "3.5.21" + "@vue/compiler-dom": "3.5.22", + "@vue/shared": "3.5.22" } }, "node_modules/@vue/component-compiler-utils": { @@ -3413,9 +3413,9 @@ "license": "MIT" }, "node_modules/@vue/shared": { - "version": "3.5.21", - "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.21.tgz", - "integrity": "sha512-+2k1EQpnYuVuu3N7atWyG3/xoFWIVJZq4Mz8XNOdScFI0etES75fbny/oU4lKWk/577P1zmg0ioYvpGEDZ3DLw==", + "version": "3.5.22", + "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.22.tgz", + "integrity": "sha512-F4yc6palwq3TT0u+FYf0Ns4Tfl9GRFURDN2gWG7L1ecIaS/4fCIuFOjMTnCyjsu/OK6vaDKLCrGAa+KvvH+h4w==", "dev": true, "license": "MIT" }, @@ -4075,9 +4075,9 @@ "license": "MIT" }, "node_modules/baseline-browser-mapping": { - "version": "2.8.6", - "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.6.tgz", - "integrity": "sha512-wrH5NNqren/QMtKUEEJf7z86YjfqW/2uw3IL3/xpqZUC95SSVIFXYQeeGjL6FT/X68IROu6RMehZQS5foy2BXw==", + "version": "2.8.7", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.7.tgz", + "integrity": "sha512-bxxN2M3a4d1CRoQC//IqsR5XrLh0IJ8TCv2x6Y9N0nckNz/rTjZB3//GGscZziZOxmjP55rzxg/ze7usFI9FqQ==", "dev": true, "license": "Apache-2.0", "bin": { @@ -4329,9 +4329,9 @@ } }, "node_modules/browserify-sign": { - "version": "4.2.4", - "resolved": "https://registry.npmjs.org/browserify-sign/-/browserify-sign-4.2.4.tgz", - "integrity": "sha512-pbZw0FHibrwXcpLQlXwHp21A5undDBo+RaGNL0K3KOm8nK8uP6PThhS301VDzoMgURZPiVRWRrVHlo6NyU57kA==", + "version": "4.2.5", + "resolved": "https://registry.npmjs.org/browserify-sign/-/browserify-sign-4.2.5.tgz", + "integrity": "sha512-C2AUdAJg6rlM2W5QMp2Q4KGQMVBwR1lIimTsUnutJ8bMpW5B52pGpR2gEnNBNwijumDo5FojQ0L9JrXA8m4YEw==", "dev": true, "license": "ISC", "dependencies": { @@ -4341,12 +4341,12 @@ "create-hmac": "^1.1.7", "elliptic": "^6.6.1", "inherits": "^2.0.4", - "parse-asn1": "^5.1.7", + "parse-asn1": "^5.1.9", "readable-stream": "^2.3.8", "safe-buffer": "^5.2.1" }, "engines": { - "node": ">= 0.12" + "node": ">= 0.10" } }, "node_modules/browserify-zlib": { @@ -4521,9 +4521,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001743", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001743.tgz", - "integrity": "sha512-e6Ojr7RV14Un7dz6ASD0aZDmQPT/A+eZU+nuTNfjqmRrmkmQlnTNWH0SKmqagx9PeW87UVqapSurtAXifmtdmw==", + "version": "1.0.30001745", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001745.tgz", + "integrity": "sha512-ywt6i8FzvdgrrrGbr1jZVObnVv6adj+0if2/omv9cmR2oiZs30zL4DIyaptKcbOrBdOIc74QTMoJvSE2QHh5UQ==", "dev": true, "funding": [ { @@ -4651,14 +4651,15 @@ } }, "node_modules/cipher-base": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/cipher-base/-/cipher-base-1.0.6.tgz", - "integrity": "sha512-3Ek9H3X6pj5TgenXYtNWdaBon1tgYCaebd+XPg0keyjEbEfkD4KkmAxkQ/i1vYvxdcT5nscLBfq9VJRmCBcFSw==", + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/cipher-base/-/cipher-base-1.0.7.tgz", + "integrity": "sha512-Mz9QMT5fJe7bKI7MH31UilT5cEK5EHHRCccw/YRFsRY47AuNgaV6HY3rscp0/I4Q+tTW/5zoqpSeRRI54TkDWA==", "dev": true, "license": "MIT", "dependencies": { "inherits": "^2.0.4", - "safe-buffer": "^5.2.1" + "safe-buffer": "^5.2.1", + "to-buffer": "^1.2.2" }, "engines": { "node": ">= 0.10" @@ -8760,20 +8761,20 @@ } }, "node_modules/parse-asn1": { - "version": "5.1.8", - "resolved": "https://registry.npmjs.org/parse-asn1/-/parse-asn1-5.1.8.tgz", - "integrity": "sha512-e90aVPe/1q/g7BrNeYvbJy++5tln4ShE+I3qZ5LxFpUbu+uavfKMuzH2R3SH141O7Pvruwif0BZRwKoVf6vW6w==", + "version": "5.1.9", + "resolved": "https://registry.npmjs.org/parse-asn1/-/parse-asn1-5.1.9.tgz", + "integrity": "sha512-fIYNuZ/HastSb80baGOuPRo1O9cf4baWw5WsAp7dBuUzeTD/BoaG8sVTdlPFksBE2lF21dN+A1AnrpIjSWqHHg==", "dev": true, "license": "ISC", "dependencies": { "asn1.js": "^4.10.1", "browserify-aes": "^1.2.0", "evp_bytestokey": "^1.0.3", - "pbkdf2": "^3.1.3", + "pbkdf2": "^3.1.5", "safe-buffer": "^5.2.1" }, "engines": { - "node": ">= 0.12" + "node": ">= 0.10" } }, "node_modules/parse-json": { @@ -8935,9 +8936,9 @@ } }, "node_modules/pbkdf2": { - "version": "3.1.4", - "resolved": "https://registry.npmjs.org/pbkdf2/-/pbkdf2-3.1.4.tgz", - "integrity": "sha512-0yPXNT01PxSUdkIxL85Fd+yPdeCcvGwFPAAHbR3Z2ukMERcRrJFfLUKK3oglbQ9eUPeX6qDY3QiELqiDarZYUQ==", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/pbkdf2/-/pbkdf2-3.1.5.tgz", + "integrity": "sha512-Q3CG/cYvCO1ye4QKkuH7EXxs3VC/rI1/trd+qX2+PolbaKG0H+bgcZzrTt96mMyRtejk+JMCiLUn3y29W8qmFQ==", "dev": true, "license": "MIT", "dependencies": { @@ -8949,7 +8950,7 @@ "to-buffer": "^1.2.1" }, "engines": { - "node": ">=0.12" + "node": ">= 0.10" } }, "node_modules/picocolors": { @@ -10222,9 +10223,9 @@ "license": "MIT" }, "node_modules/sass": { - "version": "1.93.1", - "resolved": "https://registry.npmjs.org/sass/-/sass-1.93.1.tgz", - "integrity": "sha512-wLAeLB7IksO2u+cCfhHqcy7/2ZUMPp/X2oV6+LjmweTqgjhOKrkaE/Q1wljxtco5EcOcupZ4c981X0gpk5Tiag==", + "version": "1.93.2", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.93.2.tgz", + "integrity": "sha512-t+YPtOQHpGW1QWsh1CHQ5cPIr9lbbGZLZnbihP/D/qZj/yuV68m8qarcV17nvkOX81BCrvzAlq2klCQFZghyTg==", "dev": true, "license": "MIT", "dependencies": { @@ -11233,9 +11234,9 @@ "license": "MIT" }, "node_modules/to-buffer": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/to-buffer/-/to-buffer-1.2.1.tgz", - "integrity": "sha512-tB82LpAIWjhLYbqjx3X4zEeHN6M8CiuOEy2JY8SEQVdYRe3CCHOFaqrBW1doLDrfpWhplcW7BL+bO3/6S3pcDQ==", + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/to-buffer/-/to-buffer-1.2.2.tgz", + "integrity": "sha512-db0E3UJjcFhpDhAF4tLo03oli3pwl3dbnzXOUIlRKrp+ldk/VUxzpWYZENsw2SZiuBjHAk7DfB0VU7NKdpb6sw==", "dev": true, "license": "MIT", "dependencies": { From 74f7c07a76471377ebc3e976dedb03c6fb289888 Mon Sep 17 00:00:00 2001 From: James Cole Date: Thu, 25 Sep 2025 19:17:10 +0200 Subject: [PATCH 38/58] Add filter for transfer type. --- app/Support/Http/Controllers/PeriodOverview.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/Support/Http/Controllers/PeriodOverview.php b/app/Support/Http/Controllers/PeriodOverview.php index 6944eb5f26..1515581961 100644 --- a/app/Support/Http/Controllers/PeriodOverview.php +++ b/app/Support/Http/Controllers/PeriodOverview.php @@ -208,12 +208,12 @@ trait PeriodOverview foreach ($transactions as $index => $item) { $date = Carbon::parse($item['date']); if ($date >= $start && $date <= $end) { - if ('away' === $direction && -1 === bccomp((string)$item['amount'], '0')) { + if ('Transfer' === $item['type'] && 'away' === $direction && -1 === bccomp((string)$item['amount'], '0')) { $result[] = $item; continue; } - if ('in' === $direction && 1 === bccomp((string)$item['amount'], '0')) { + if ('Transfer' === $item['type'] && 'in' === $direction && 1 === bccomp((string)$item['amount'], '0')) { $result[] = $item; continue; From 08879d31ba20f25e5a9e05c06bfe0b760b85905e Mon Sep 17 00:00:00 2001 From: James Cole Date: Fri, 26 Sep 2025 05:33:35 +0200 Subject: [PATCH 39/58] Rearrange some code. --- .../Http/Api/AccountBalanceGrouped.php | 74 ++--- app/Support/Http/Api/CleansChartData.php | 2 +- .../Http/Api/CollectsAccountsFromFilter.php | 2 +- .../Http/Api/ExchangeRateConverter.php | 37 ++- .../Http/Api/SummaryBalanceGrouped.php | 32 +-- .../Http/Api/ValidatesUserGroupTrait.php | 24 +- app/Support/Http/Controllers/AugumentData.php | 56 ++-- .../Http/Controllers/ChartGeneration.php | 32 +-- app/Support/Http/Controllers/CreateStuff.php | 9 +- app/Support/Http/Controllers/CronRunner.php | 52 ++-- .../Http/Controllers/DateCalculation.php | 14 +- .../Http/Controllers/GetConfigurationData.php | 60 ++--- .../Http/Controllers/ModelInformation.php | 46 ++-- .../Http/Controllers/PeriodOverview.php | 254 +++++++++--------- .../Http/Controllers/RenderPartialViews.php | 46 ++-- .../Http/Controllers/RequestInformation.php | 25 +- .../Http/Controllers/RuleManagement.php | 22 +- .../Controllers/TransactionCalculation.php | 26 +- .../Http/Controllers/UserNavigation.php | 10 +- 19 files changed, 407 insertions(+), 416 deletions(-) diff --git a/app/Support/Http/Api/AccountBalanceGrouped.php b/app/Support/Http/Api/AccountBalanceGrouped.php index 839c87bfb0..d1f7915d23 100644 --- a/app/Support/Http/Api/AccountBalanceGrouped.php +++ b/app/Support/Http/Api/AccountBalanceGrouped.php @@ -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() @@ -67,7 +67,7 @@ class AccountBalanceGrouped /** @var array $currency */ foreach ($this->data as $currency) { // income and expense array prepped: - $income = [ + $income = [ 'label' => 'earned', 'currency_id' => (string)$currency['currency_id'], 'currency_symbol' => $currency['currency_symbol'], @@ -86,7 +86,7 @@ class AccountBalanceGrouped 'entries' => [], 'pc_entries' => [], ]; - $expense = [ + $expense = [ 'label' => 'spent', 'currency_id' => (string)$currency['currency_id'], 'currency_symbol' => $currency['currency_symbol'], @@ -108,22 +108,22 @@ class AccountBalanceGrouped // loop all possible periods between $start and $end, and add them to the correct dataset. $currentStart = clone $this->start; while ($currentStart <= $this->end) { - $key = $currentStart->format($this->carbonFormat); - $label = $currentStart->toAtomString(); + $key = $currentStart->format($this->carbonFormat); + $label = $currentStart->toAtomString(); // normal entries - $income['entries'][$label] = Steam::bcround($currency[$key]['earned'] ?? '0', $currency['currency_decimal_places']); - $expense['entries'][$label] = Steam::bcround($currency[$key]['spent'] ?? '0', $currency['currency_decimal_places']); + $income['entries'][$label] = Steam::bcround($currency[$key]['earned'] ?? '0', $currency['currency_decimal_places']); + $expense['entries'][$label] = Steam::bcround($currency[$key]['spent'] ?? '0', $currency['currency_decimal_places']); // converted entries $income['pc_entries'][$label] = Steam::bcround($currency[$key]['pc_earned'] ?? '0', $currency['primary_currency_decimal_places']); $expense['pc_entries'][$label] = Steam::bcround($currency[$key]['pc_spent'] ?? '0', $currency['primary_currency_decimal_places']); // next loop - $currentStart = Navigation::addPeriod($currentStart, $this->preferredRange, 0); + $currentStart = Navigation::addPeriod($currentStart, $this->preferredRange, 0); } - $chartData[] = $income; - $chartData[] = $expense; + $chartData[] = $income; + $chartData[] = $expense; } return $chartData; @@ -149,9 +149,9 @@ class AccountBalanceGrouped private function processJournal(array $journal): void { // format the date according to the period - $period = $journal['date']->format($this->carbonFormat); - $currencyId = (int)$journal['currency_id']; - $currency = $this->findCurrency($currencyId); + $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); @@ -159,12 +159,12 @@ class AccountBalanceGrouped $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']); + $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); + $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) { @@ -173,7 +173,7 @@ class AccountBalanceGrouped } // add normal entry - $this->data[$currencyId][$period][$key] = bcadd((string)$this->data[$currencyId][$period][$key], $amount); + $this->data[$currencyId][$period][$key] = bcadd((string)$this->data[$currencyId][$period][$key], $amount); // add converted entry $convertedKey = sprintf('pc_%s', $key); @@ -192,7 +192,7 @@ class AccountBalanceGrouped private function createDefaultDataEntry(array $journal): void { - $currencyId = (int)$journal['currency_id']; + $currencyId = (int)$journal['currency_id']; $this->data[$currencyId] ??= [ 'currency_id' => (string)$currencyId, 'currency_symbol' => $journal['currency_symbol'], @@ -209,8 +209,8 @@ class AccountBalanceGrouped private function createDefaultPeriodEntry(array $journal): void { - $currencyId = (int)$journal['currency_id']; - $period = $journal['date']->format($this->carbonFormat); + $currencyId = (int)$journal['currency_id']; + $period = $journal['date']->format($this->carbonFormat); $this->data[$currencyId][$period] ??= [ 'period' => $period, 'spent' => '0', @@ -259,6 +259,22 @@ class AccountBalanceGrouped $this->accountIds = $accounts->pluck('id')->toArray(); } + public function setEnd(Carbon $end): void + { + $this->end = $end; + } + + 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; @@ -278,22 +294,6 @@ class AccountBalanceGrouped ]; } - public function setEnd(Carbon $end): void - { - $this->end = $end; - } - - 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 setStart(Carbon $start): void { $this->start = $start; diff --git a/app/Support/Http/Api/CleansChartData.php b/app/Support/Http/Api/CleansChartData.php index aef2ec443f..a3d54974cf 100644 --- a/app/Support/Http/Api/CleansChartData.php +++ b/app/Support/Http/Api/CleansChartData.php @@ -43,7 +43,7 @@ trait CleansChartData $return = []; /** - * @var int $index + * @var int $index * @var array $array */ foreach ($data as $index => $array) { diff --git a/app/Support/Http/Api/CollectsAccountsFromFilter.php b/app/Support/Http/Api/CollectsAccountsFromFilter.php index 7c10a6c1e1..2911ed61f6 100644 --- a/app/Support/Http/Api/CollectsAccountsFromFilter.php +++ b/app/Support/Http/Api/CollectsAccountsFromFilter.php @@ -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); } diff --git a/app/Support/Http/Api/ExchangeRateConverter.php b/app/Support/Http/Api/ExchangeRateConverter.php index 8abb75fe76..c78b7afce8 100644 --- a/app/Support/Http/Api/ExchangeRateConverter.php +++ b/app/Support/Http/Api/ExchangeRateConverter.php @@ -99,8 +99,8 @@ class ExchangeRateConverter */ private function getRate(TransactionCurrency $from, TransactionCurrency $to, Carbon $date): string { - $key = $this->getCacheKey($from, $to, $date); - $res = Cache::get($key, null); + $key = $this->getCacheKey($from, $to, $date); + $res = Cache::get($key, null); // find in cache if (null !== $res) { @@ -110,7 +110,7 @@ class ExchangeRateConverter } // find in database - $rate = $this->getFromDB($from->id, $to->id, $date->format('Y-m-d')); + $rate = $this->getFromDB($from->id, $to->id, $date->format('Y-m-d')); if (null !== $rate) { Cache::forever($key, $rate); Log::debug(sprintf('ExchangeRateConverter: Return DB rate from %s to %s on %s.', $from->code, $to->code, $date->format('Y-m-d'))); @@ -119,7 +119,7 @@ class ExchangeRateConverter } // find reverse in database - $rate = $this->getFromDB($to->id, $from->id, $date->format('Y-m-d')); + $rate = $this->getFromDB($to->id, $from->id, $date->format('Y-m-d')); if (null !== $rate) { $rate = bcdiv('1', $rate); Cache::forever($key, $rate); @@ -159,7 +159,7 @@ class ExchangeRateConverter return '1'; } - $key = sprintf('cer-%d-%d-%s', $from, $to, $date); + $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; @@ -169,7 +169,7 @@ class ExchangeRateConverter return $preparedRate; } - $cache = new CacheProperties(); + $cache = new CacheProperties(); $cache->addProperty($key); if ($cache->has()) { $rate = $cache->get(); @@ -182,15 +182,14 @@ class ExchangeRateConverter } /** @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() - ; + $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; + $rate = (string)$result?->rate; if ('' === $rate) { app('log')->debug(sprintf('ExchangeRateConverter: Found no rate for #%d->#%d (%s) in the DB.', $from, $to, $date)); @@ -230,13 +229,13 @@ class ExchangeRateConverter if ($euroId === $currency->id) { return '1'; } - $rate = $this->getFromDB($currency->id, $euroId, $date->format('Y-m-d')); + $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')); + $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)); @@ -245,7 +244,7 @@ class ExchangeRateConverter // grab backup values from config file: $backup = config(sprintf('cer.rates.%s', $currency->code)); if (null !== $backup) { - return bcdiv('1', (string) $backup); + return bcdiv('1', (string)$backup); // app('log')->debug(sprintf('Backup rate for %s to EUR is %s.', $currency->code, $backup)); // return $backup; } @@ -263,9 +262,9 @@ class ExchangeRateConverter $cache = new CacheProperties(); $cache->addProperty('cer-euro-id'); if ($cache->has()) { - return (int) $cache->get(); + return (int)$cache->get(); } - $euro = Amount::getTransactionCurrencyByCode('EUR'); + $euro = Amount::getTransactionCurrencyByCode('EUR'); ++$this->queryCount; $cache->store($euro->id); diff --git a/app/Support/Http/Api/SummaryBalanceGrouped.php b/app/Support/Http/Api/SummaryBalanceGrouped.php index b50ab2160e..8f88dba7d5 100644 --- a/app/Support/Http/Api/SummaryBalanceGrouped.php +++ b/app/Support/Http/Api/SummaryBalanceGrouped.php @@ -31,7 +31,7 @@ use Illuminate\Support\Facades\Log; class SummaryBalanceGrouped { - private const string SUM = 'sum'; + private const string SUM = 'sum'; private array $amounts = []; private array $currencies; private readonly CurrencyRepositoryInterface $currencyRepository; @@ -48,9 +48,9 @@ class SummaryBalanceGrouped public function groupData(): array { Log::debug('Now going to group data.'); - $return = []; + $return = []; foreach ($this->keys as $key) { - $title = match ($key) { + $title = match ($key) { 'sum' => 'balance', 'expense' => 'spent', 'income' => 'earned', @@ -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); - $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) { + $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) { // 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(); } diff --git a/app/Support/Http/Api/ValidatesUserGroupTrait.php b/app/Support/Http/Api/ValidatesUserGroupTrait.php index 6ce611396d..0ce08c97ed 100644 --- a/app/Support/Http/Api/ValidatesUserGroupTrait.php +++ b/app/Support/Http/Api/ValidatesUserGroupTrait.php @@ -38,8 +38,8 @@ use Illuminate\Support\Facades\Log; */ trait ValidatesUserGroupTrait { + protected User $user; protected UserGroup $userGroup; - protected User $user; /** * An "undocumented" filter @@ -59,41 +59,41 @@ trait ValidatesUserGroupTrait } /** @var User $user */ - $user = auth()->user(); - $groupId = 0; + $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)); } /** @var UserGroupRepositoryInterface $repository */ - $repository = app(UserGroupRepositoryInterface::class); + $repository = app(UserGroupRepositoryInterface::class); $repository->setUser($user); $memberships = $repository->getMembershipsFromGroupId($groupId); 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: - $group = $repository->getById($groupId); + $group = $repository->getById($groupId); 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 + $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')); } } diff --git a/app/Support/Http/Controllers/AugumentData.php b/app/Support/Http/Controllers/AugumentData.php index d882ce2b5b..aa3883244f 100644 --- a/app/Support/Http/Controllers/AugumentData.php +++ b/app/Support/Http/Controllers/AugumentData.php @@ -56,10 +56,10 @@ trait AugumentData /** @var Account $expenseAccount */ foreach ($accounts as $expenseAccount) { - $collection = new Collection(); + $collection = new Collection(); $collection->push($expenseAccount); - $revenue = $repository->findByName($expenseAccount->name, [AccountTypeEnum::REVENUE->value]); + $revenue = $repository->findByName($expenseAccount->name, [AccountTypeEnum::REVENUE->value]); if (null !== $revenue) { $collection->push($revenue); } @@ -110,13 +110,13 @@ 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']; } } - $return[0] = '(no name)'; + $return[0] = '(no name)'; return $return; } @@ -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; } @@ -171,14 +171,14 @@ trait AugumentData Log::debug('In getLimits'); /** @var OperationsRepositoryInterface $opsRepository */ - $opsRepository = app(OperationsRepositoryInterface::class); + $opsRepository = app(OperationsRepositoryInterface::class); /** @var BudgetLimitRepositoryInterface $blRepository */ - $blRepository = app(BudgetLimitRepositoryInterface::class); + $blRepository = app(BudgetLimitRepositoryInterface::class); $end->endOfMonth(); // properties for cache - $cache = new CacheProperties(); + $cache = new CacheProperties(); $cache->addProperty($start); $cache->addProperty($end); $cache->addProperty($budget->id); @@ -189,25 +189,25 @@ trait AugumentData return $cache->get(); } - $set = $blRepository->getBudgetLimits($budget, $start, $end); + $set = $blRepository->getBudgetLimits($budget, $start, $end); $budgetCollection = new Collection()->push($budget); // merge sets based on a key, in case of convert to primary currency - $limits = new Collection(); + $limits = new Collection(); /** @var BudgetLimit $entry */ foreach ($set as $entry) { Log::debug(sprintf('Now at budget limit #%d', $entry->id)); - $currency = $entry->transactionCurrency; + $currency = $entry->transactionCurrency; if ($this->convertToPrimary) { // the sumExpenses method already handles this. $currency = $this->primaryCurrency; } // clone because these objects change each other. - $currentStart = clone $entry->start_date; - $currentEnd = null === $entry->end_date ? null : clone $entry->end_date; + $currentStart = clone $entry->start_date; + $currentEnd = null === $entry->end_date ? null : clone $entry->end_date; if (null === $currentEnd) { $currentEnd = clone $currentStart; @@ -219,9 +219,9 @@ trait AugumentData $entry->pc_spent = $spent; // normal amount: - $expenses = $opsRepository->sumExpenses($currentStart, $currentEnd, null, $budgetCollection, $entry->transactionCurrency, false); - $spent = $expenses[$entry->transactionCurrency->id]['sum'] ?? '0'; - $entry->spent = $spent; + $expenses = $opsRepository->sumExpenses($currentStart, $currentEnd, null, $budgetCollection, $entry->transactionCurrency, false); + $spent = $expenses[$entry->transactionCurrency->id]['sum'] ?? '0'; + $entry->spent = $spent; $limits->push($entry); } @@ -240,7 +240,7 @@ trait AugumentData /** @var array $journal */ foreach ($array as $journal) { - $name = '(no name)'; + $name = '(no name)'; if (TransactionTypeEnum::WITHDRAWAL->value === $journal['transaction_type_type']) { $name = $journal['destination_account_name']; } @@ -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; @@ -263,16 +263,16 @@ trait AugumentData /** @var GroupCollectorInterface $collector */ $collector = app(GroupCollectorInterface::class); - $total = $assets->merge($opposing); + $total = $assets->merge($opposing); $collector->setRange($start, $end)->setTypes([TransactionTypeEnum::WITHDRAWAL->value])->setAccounts($total); - $journals = $collector->getExtractedJournals(); - $sum = [ + $journals = $collector->getExtractedJournals(); + $sum = [ 'grand_sum' => '0', 'per_currency' => [], ]; // 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; diff --git a/app/Support/Http/Controllers/ChartGeneration.php b/app/Support/Http/Controllers/ChartGeneration.php index 47a85c5c83..7fe3b3ab96 100644 --- a/app/Support/Http/Controllers/ChartGeneration.php +++ b/app/Support/Http/Controllers/ChartGeneration.php @@ -59,28 +59,28 @@ trait ChartGeneration return $cache->get(); } Log::debug('Regenerate chart.account.account-balance-chart from scratch.'); - $locale = app('steam')->getLocale(); + $locale = app('steam')->getLocale(); /** @var GeneratorInterface $generator */ - $generator = app(GeneratorInterface::class); + $generator = app(GeneratorInterface::class); /** @var AccountRepositoryInterface $accountRepos */ - $accountRepos = app(AccountRepositoryInterface::class); + $accountRepos = app(AccountRepositoryInterface::class); - $primary = app('amount')->getPrimaryCurrency(); - $chartData = []; + $primary = app('amount')->getPrimaryCurrency(); + $chartData = []; Log::debug(sprintf('Start of accountBalanceChart(list, %s, %s)', $start->format('Y-m-d H:i:s'), $end->format('Y-m-d H:i:s'))); /** @var Account $account */ foreach ($accounts as $account) { Log::debug(sprintf('Now at account #%d ("%s)', $account->id, $account->name)); - $currency = $accountRepos->getAccountCurrency($account) ?? $primary; - $usePrimary = $convertToPrimary && $primary->id !== $currency->id; - $field = $convertToPrimary ? 'pc_balance' : 'balance'; - $currency = $usePrimary ? $primary : $currency; + $currency = $accountRepos->getAccountCurrency($account) ?? $primary; + $usePrimary = $convertToPrimary && $primary->id !== $currency->id; + $field = $convertToPrimary ? 'pc_balance' : 'balance'; + $currency = $usePrimary ? $primary : $currency; Log::debug(sprintf('Will use field %s', $field)); - $currentSet = [ + $currentSet = [ 'label' => $account->name, 'currency_symbol' => $currency->symbol, 'entries' => [], @@ -91,16 +91,16 @@ trait ChartGeneration $previous = array_values($range)[0]; 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))); - $balance = $range[$format] ?? $previous; - $previous = $balance; + $format = $currentStart->format('Y-m-d'); + $label = trim($currentStart->isoFormat((string)trans('config.month_and_day_js', [], $locale))); + $balance = $range[$format] ?? $previous; + $previous = $balance; $currentStart->addDay(); $currentSet['entries'][$label] = $balance[$field] ?? '0'; } - $chartData[] = $currentSet; + $chartData[] = $currentSet; } - $data = $generator->multiSet($chartData); + $data = $generator->multiSet($chartData); $cache->store($data); return $data; diff --git a/app/Support/Http/Controllers/CreateStuff.php b/app/Support/Http/Controllers/CreateStuff.php index 6ce33985d3..c54d585328 100644 --- a/app/Support/Http/Controllers/CreateStuff.php +++ b/app/Support/Http/Controllers/CreateStuff.php @@ -32,7 +32,6 @@ use FireflyIII\User; use Illuminate\Support\Facades\Log; use Laravel\Passport\Passport; use phpseclib3\Crypt\RSA; - use function Safe\file_put_contents; /** @@ -73,7 +72,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, @@ -104,11 +103,11 @@ trait CreateStuff return; } - $key = RSA::createKey(4096); + $key = RSA::createKey(4096); 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 +119,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, diff --git a/app/Support/Http/Controllers/CronRunner.php b/app/Support/Http/Controllers/CronRunner.php index c8cf03cfd6..ed3e950024 100644 --- a/app/Support/Http/Controllers/CronRunner.php +++ b/app/Support/Http/Controllers/CronRunner.php @@ -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, + ]; + } } diff --git a/app/Support/Http/Controllers/DateCalculation.php b/app/Support/Http/Controllers/DateCalculation.php index 5027a4c69f..83baefcf8b 100644 --- a/app/Support/Http/Controllers/DateCalculation.php +++ b/app/Support/Http/Controllers/DateCalculation.php @@ -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 @@ -90,19 +90,19 @@ trait DateCalculation protected function getNextPeriods(Carbon $date, string $range): array { // select thing for next 12 periods: - $loop = []; + $loop = []; /** @var Carbon $current */ $current = app('navigation')->startOfPeriod($date, $range); $current = app('navigation')->endOfPeriod($current, $range); $current->addDay(); - $count = 0; + $count = 0; while ($count < 12) { $current = app('navigation')->endOfPeriod($current, $range); $currentStart = app('navigation')->startOfPeriod($current, $range); - $loop[] = [ + $loop[] = [ 'label' => $current->format('Y-m-d'), 'title' => app('navigation')->periodShow($current, $range), 'start' => clone $currentStart, @@ -122,7 +122,7 @@ trait DateCalculation protected function getPreviousPeriods(Carbon $date, string $range): array { // select thing for last 12 periods: - $loop = []; + $loop = []; /** @var Carbon $current */ $current = app('navigation')->startOfPeriod($date, $range); diff --git a/app/Support/Http/Controllers/GetConfigurationData.php b/app/Support/Http/Controllers/GetConfigurationData.php index 52ce494418..d1005bfdcb 100644 --- a/app/Support/Http/Controllers/GetConfigurationData.php +++ b/app/Support/Http/Controllers/GetConfigurationData.php @@ -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; } /** @@ -61,13 +61,13 @@ trait GetConfigurationData $steps = []; if (is_array($elements) && count($elements) > 0) { foreach ($elements as $key => $options) { - $currentStep = $options; + $currentStep = $options; // get the text: - $currentStep['intro'] = (string) trans('intro.'.$route.'_'.$key); + $currentStep['intro'] = (string)trans('intro.' . $route . '_' . $key); // save in array: - $steps[] = $currentStep; + $steps[] = $currentStep; } } app('log')->debug(sprintf('Total basic steps for %s is %d', $routeKey, count($steps))); @@ -82,22 +82,22 @@ trait GetConfigurationData */ protected function getDateRangeConfig(): array // get configuration + get preferences. { - $viewRange = app('navigation')->getViewRange(false); + $viewRange = app('navigation')->getViewRange(false); Log::debug(sprintf('dateRange: the view range is "%s"', $viewRange)); /** @var Carbon $start */ - $start = session('start'); + $start = session('start'); /** @var Carbon $end */ - $end = session('end'); + $end = session('end'); /** @var Carbon $first */ - $first = session('first'); - $title = sprintf('%s - %s', $start->isoFormat($this->monthAndDayFormat), $end->isoFormat($this->monthAndDayFormat)); - $isCustom = true === session('is_custom_range', false); - $today = today(config('app.timezone')); - $ranges = [ + $first = session('first'); + $title = sprintf('%s - %s', $start->isoFormat($this->monthAndDayFormat), $end->isoFormat($this->monthAndDayFormat)); + $isCustom = true === session('is_custom_range', false); + $today = today(config('app.timezone')); + $ranges = [ // first range is the current range: $title => [$start, $end], ]; @@ -127,47 +127,47 @@ trait GetConfigurationData // today: /** @var Carbon $todayStart */ - $todayStart = app('navigation')->startOfPeriod($today, $viewRange); + $todayStart = app('navigation')->startOfPeriod($today, $viewRange); /** @var Carbon $todayEnd */ - $todayEnd = app('navigation')->endOfPeriod($todayStart, $viewRange); + $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, @@ -186,16 +186,16 @@ trait GetConfigurationData // user is on page with specific instructions: if ('' !== $specificPage) { $routeKey = str_replace('.', '_', $route); - $elements = config(sprintf('intro.%s', $routeKey.'_'.$specificPage)); + $elements = config(sprintf('intro.%s', $routeKey . '_' . $specificPage)); if (is_array($elements) && count($elements) > 0) { foreach ($elements as $key => $options) { - $currentStep = $options; + $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; + $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) { diff --git a/app/Support/Http/Controllers/ModelInformation.php b/app/Support/Http/Controllers/ModelInformation.php index 65fd660211..152c9671ef 100644 --- a/app/Support/Http/Controllers/ModelInformation.php +++ b/app/Support/Http/Controllers/ModelInformation.php @@ -75,21 +75,21 @@ trait ModelInformation protected function getLiabilityTypes(): array { /** @var AccountRepositoryInterface $repository */ - $repository = app(AccountRepositoryInterface::class); + $repository = app(AccountRepositoryInterface::class); // types of liability: /** @var AccountType $debt */ - $debt = $repository->getAccountTypeByType(AccountTypeEnum::DEBT->value); + $debt = $repository->getAccountTypeByType(AccountTypeEnum::DEBT->value); /** @var AccountType $loan */ - $loan = $repository->getAccountTypeByType(AccountTypeEnum::LOAN->value); + $loan = $repository->getAccountTypeByType(AccountTypeEnum::LOAN->value); /** @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; @@ -114,11 +114,11 @@ trait ModelInformation protected function getTriggersForBill(Bill $bill): array // get info and argument { // TODO duplicate code - $operators = config('search.operators'); - $triggers = []; + $operators = config('search.operators'); + $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); @@ -165,27 +165,27 @@ trait ModelInformation private function getTriggersForJournal(TransactionJournal $journal): array { // TODO duplicated code. - $operators = config('search.operators'); - $triggers = []; + $operators = config('search.operators'); + $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); - $result = []; - $journalTriggers = []; - $values = []; - $index = 0; + $result = []; + $journalTriggers = []; + $values = []; + $index = 0; // amount, description, category, budget, tags, source, destination, notes, currency type // ,type /** @var null|Transaction $source */ - $source = $journal->transactions()->where('amount', '<', 0)->first(); + $source = $journal->transactions()->where('amount', '<', 0)->first(); /** @var null|Transaction $destination */ - $destination = $journal->transactions()->where('amount', '>', 0)->first(); + $destination = $journal->transactions()->where('amount', '>', 0)->first(); if (null === $destination || null === $source) { return $result; } @@ -220,21 +220,21 @@ trait ModelInformation ++$index; // category (if) - $category = $journal->categories()->first(); + $category = $journal->categories()->first(); if (null !== $category) { $journalTriggers[$index] = 'category_is'; $values[$index] = $category->name; ++$index; } // budget (if) - $budget = $journal->budgets()->first(); + $budget = $journal->budgets()->first(); if (null !== $budget) { $journalTriggers[$index] = 'budget_is'; $values[$index] = $budget->name; ++$index; } // tags (if) - $tags = $journal->tags()->get(); + $tags = $journal->tags()->get(); /** @var Tag $tag */ foreach ($tags as $tag) { @@ -243,7 +243,7 @@ trait ModelInformation ++$index; } // notes (if) - $notes = $journal->notes()->first(); + $notes = $journal->notes()->first(); if (null !== $notes) { $journalTriggers[$index] = 'notes_is'; $values[$index] = $notes->text; diff --git a/app/Support/Http/Controllers/PeriodOverview.php b/app/Support/Http/Controllers/PeriodOverview.php index 52b7a0c240..7371b997bd 100644 --- a/app/Support/Http/Controllers/PeriodOverview.php +++ b/app/Support/Http/Controllers/PeriodOverview.php @@ -86,11 +86,11 @@ trait PeriodOverview $this->accountRepository = app(AccountRepositoryInterface::class); $this->periodStatisticRepo = app(PeriodStatisticRepositoryInterface::class); $range = Navigation::getViewRange(true); - [$start, $end] = $end < $start ? [$end, $start] : [$start, $end]; + [$start, $end] = $end < $start ? [$end, $start] : [$start, $end]; /** @var array $dates */ - $dates = Navigation::blockPeriods($start, $end, $range); - $entries = []; + $dates = Navigation::blockPeriods($start, $end, $range); + $entries = []; Log::debug(sprintf('Count of loops: %d', count($dates))); foreach ($dates as $currentDate) { $entries[] = $this->getSingleAccountPeriod($account, $currentDate['start'], $currentDate['end']); @@ -110,7 +110,7 @@ trait PeriodOverview 'total_transactions' => 0, ]; foreach ($types as $type) { - $set = $this->getSingleAccountPeriodByType($account, $start, $end, $type); + $set = $this->getSingleAccountPeriodByType($account, $start, $end, $type); $return['total_transactions'] += $set['count']; unset($set['count']); $return[$type] = $set; @@ -153,50 +153,42 @@ trait PeriodOverview break; } // each result must be grouped by currency, then saved as period statistic. - $grouped = $this->groupByCurrency($result); + $grouped = $this->groupByCurrency($result); // TODO save as statistic. $this->saveGroupedAsStatistics($account, $start, $end, $type, $grouped); return $grouped; } - $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, + $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; + $grouped['count'] += (int)$statistic->count; } return $grouped; } - protected function saveGroupedAsStatistics(Account $account, Carbon $start, Carbon $end, string $type, array $array): void - { - unset($array['count']); - foreach ($array as $entry) { - $this->periodStatisticRepo->saveStatistic($account, $entry['currency_id'], $start, $end, $type, $entry['count'], $entry['amount']); - } - } - private function filterTransactionsByType(TransactionTypeEnum $type, array $transactions, Carbon $start, Carbon $end): array { $result = []; /** - * @var int $index + * @var int $index * @var array $item */ foreach ($transactions as $index => $item) { @@ -216,7 +208,7 @@ trait PeriodOverview $result = []; /** - * @var int $index + * @var int $index * @var array $item */ foreach ($transactions as $index => $item) { @@ -255,13 +247,13 @@ trait PeriodOverview 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'; + $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'; @@ -298,6 +290,14 @@ trait PeriodOverview return $return; } + protected function saveGroupedAsStatistics(Account $account, Carbon $start, Carbon $end, string $type, array $array): void + { + unset($array['count']); + foreach ($array as $entry) { + $this->periodStatisticRepo->saveStatistic($account, $entry['currency_id'], $start, $end, $type, $entry['count'], $entry['amount']); + } + } + /** * Overview for single category. Has been refactored recently. * @@ -305,11 +305,11 @@ trait PeriodOverview */ protected function getCategoryPeriodOverview(Category $category, Carbon $start, Carbon $end): array { - $range = Navigation::getViewRange(true); + $range = Navigation::getViewRange(true); [$start, $end] = $end < $start ? [$end, $start] : [$start, $end]; // properties for entries with their amounts. - $cache = new CacheProperties(); + $cache = new CacheProperties(); $cache->addProperty($start); $cache->addProperty($end); $cache->addProperty($range); @@ -321,32 +321,32 @@ trait PeriodOverview } /** @var array $dates */ - $dates = Navigation::blockPeriods($start, $end, $range); - $entries = []; + $dates = Navigation::blockPeriods($start, $end, $range); + $entries = []; // collect all expenses in this period: /** @var GroupCollectorInterface $collector */ - $collector = app(GroupCollectorInterface::class); + $collector = app(GroupCollectorInterface::class); $collector->setCategory($category); $collector->setRange($start, $end); $collector->setTypes([TransactionTypeEnum::DEPOSIT->value]); - $earnedSet = $collector->getExtractedJournals(); + $earnedSet = $collector->getExtractedJournals(); // collect all income in this period: /** @var GroupCollectorInterface $collector */ - $collector = app(GroupCollectorInterface::class); + $collector = app(GroupCollectorInterface::class); $collector->setCategory($category); $collector->setRange($start, $end); $collector->setTypes([TransactionTypeEnum::WITHDRAWAL->value]); - $spentSet = $collector->getExtractedJournals(); + $spentSet = $collector->getExtractedJournals(); // collect all transfers in this period: /** @var GroupCollectorInterface $collector */ - $collector = app(GroupCollectorInterface::class); + $collector = app(GroupCollectorInterface::class); $collector->setCategory($category); $collector->setRange($start, $end); $collector->setTypes([TransactionTypeEnum::TRANSFER->value]); - $transferSet = $collector->getExtractedJournals(); + $transferSet = $collector->getExtractedJournals(); foreach ($dates as $currentDate) { $spent = $this->filterJournalsByDate($spentSet, $currentDate['start'], $currentDate['end']); $earned = $this->filterJournalsByDate($earnedSet, $currentDate['start'], $currentDate['end']); @@ -354,17 +354,17 @@ trait PeriodOverview $title = Navigation::periodShow($currentDate['end'], $currentDate['period']); $entries[] = [ - 'transactions' => 0, - 'title' => $title, - 'route' => route( - 'categories.show', - [$category->id, $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), - ]; + 'transactions' => 0, + 'title' => $title, + 'route' => route( + 'categories.show', + [$category->id, $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), + ]; } $cache->store($entries); @@ -397,11 +397,11 @@ trait PeriodOverview */ protected function getNoBudgetPeriodOverview(Carbon $start, Carbon $end): array { - $range = Navigation::getViewRange(true); + $range = Navigation::getViewRange(true); [$start, $end] = $end < $start ? [$end, $start] : [$start, $end]; - $cache = new CacheProperties(); + $cache = new CacheProperties(); $cache->addProperty($start); $cache->addProperty($end); $cache->addProperty($this->convertToPrimary); @@ -412,28 +412,28 @@ trait PeriodOverview } /** @var array $dates */ - $dates = Navigation::blockPeriods($start, $end, $range); - $entries = []; + $dates = Navigation::blockPeriods($start, $end, $range); + $entries = []; // get all expenses without a budget. /** @var GroupCollectorInterface $collector */ - $collector = app(GroupCollectorInterface::class); + $collector = app(GroupCollectorInterface::class); $collector->setRange($start, $end)->withoutBudget()->withAccountInformation()->setTypes([TransactionTypeEnum::WITHDRAWAL->value]); - $journals = $collector->getExtractedJournals(); + $journals = $collector->getExtractedJournals(); foreach ($dates as $currentDate) { $set = $this->filterJournalsByDate($journals, $currentDate['start'], $currentDate['end']); $title = Navigation::periodShow($currentDate['end'], $currentDate['period']); $entries[] = [ - 'title' => $title, - 'route' => route('budgets.no-budget', [$currentDate['start']->format('Y-m-d'), $currentDate['end']->format('Y-m-d')]), - 'total_transactions' => count($set), - 'spent' => $this->groupByCurrency($set), - 'earned' => [], - 'transferred_away' => [], - 'transferred_in' => [], - ]; + 'title' => $title, + 'route' => route('budgets.no-budget', [$currentDate['start']->format('Y-m-d'), $currentDate['end']->format('Y-m-d')]), + 'total_transactions' => count($set), + 'spent' => $this->groupByCurrency($set), + 'earned' => [], + 'transferred_away' => [], + 'transferred_in' => [], + ]; } $cache->store($entries); @@ -450,38 +450,38 @@ trait PeriodOverview protected function getNoCategoryPeriodOverview(Carbon $theDate): array { app('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); + $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'))); // properties for cache - $dates = Navigation::blockPeriods($start, $end, $range); - $entries = []; + $dates = Navigation::blockPeriods($start, $end, $range); + $entries = []; // collect all expenses in this period: /** @var GroupCollectorInterface $collector */ - $collector = app(GroupCollectorInterface::class); + $collector = app(GroupCollectorInterface::class); $collector->withoutCategory(); $collector->setRange($start, $end); $collector->setTypes([TransactionTypeEnum::DEPOSIT->value]); - $earnedSet = $collector->getExtractedJournals(); + $earnedSet = $collector->getExtractedJournals(); // collect all income in this period: /** @var GroupCollectorInterface $collector */ - $collector = app(GroupCollectorInterface::class); + $collector = app(GroupCollectorInterface::class); $collector->withoutCategory(); $collector->setRange($start, $end); $collector->setTypes([TransactionTypeEnum::WITHDRAWAL->value]); - $spentSet = $collector->getExtractedJournals(); + $spentSet = $collector->getExtractedJournals(); // collect all transfers in this period: /** @var GroupCollectorInterface $collector */ - $collector = app(GroupCollectorInterface::class); + $collector = app(GroupCollectorInterface::class); $collector->withoutCategory(); $collector->setRange($start, $end); $collector->setTypes([TransactionTypeEnum::TRANSFER->value]); @@ -495,13 +495,13 @@ trait PeriodOverview $title = Navigation::periodShow($currentDate['end'], $currentDate['period']); $entries[] = [ - 'title' => $title, - 'route' => route('categories.no-category', [$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('categories.no-category', [$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), + ]; } app('log')->debug('End of loops'); @@ -515,11 +515,11 @@ trait PeriodOverview */ protected function getTagPeriodOverview(Tag $tag, Carbon $start, Carbon $end): array // period overview for tags. { - $range = Navigation::getViewRange(true); + $range = Navigation::getViewRange(true); [$start, $end] = $end < $start ? [$end, $start] : [$start, $end]; // properties for cache - $cache = new CacheProperties(); + $cache = new CacheProperties(); $cache->addProperty($start); $cache->addProperty($end); $cache->addProperty('tag-period-entries'); @@ -529,37 +529,37 @@ trait PeriodOverview } /** @var array $dates */ - $dates = Navigation::blockPeriods($start, $end, $range); - $entries = []; + $dates = Navigation::blockPeriods($start, $end, $range); + $entries = []; // collect all expenses in this period: /** @var GroupCollectorInterface $collector */ - $collector = app(GroupCollectorInterface::class); + $collector = app(GroupCollectorInterface::class); $collector->setTag($tag); $collector->setRange($start, $end); $collector->setTypes([TransactionTypeEnum::DEPOSIT->value]); - $earnedSet = $collector->getExtractedJournals(); + $earnedSet = $collector->getExtractedJournals(); // collect all income in this period: /** @var GroupCollectorInterface $collector */ - $collector = app(GroupCollectorInterface::class); + $collector = app(GroupCollectorInterface::class); $collector->setTag($tag); $collector->setRange($start, $end); $collector->setTypes([TransactionTypeEnum::WITHDRAWAL->value]); - $spentSet = $collector->getExtractedJournals(); + $spentSet = $collector->getExtractedJournals(); // collect all transfers in this period: /** @var GroupCollectorInterface $collector */ - $collector = app(GroupCollectorInterface::class); + $collector = app(GroupCollectorInterface::class); $collector->setTag($tag); $collector->setRange($start, $end); $collector->setTypes([TransactionTypeEnum::TRANSFER->value]); - $transferSet = $collector->getExtractedJournals(); + $transferSet = $collector->getExtractedJournals(); // filer all of them: - $earnedSet = $this->filterJournalsByTag($earnedSet, $tag); - $spentSet = $this->filterJournalsByTag($spentSet, $tag); - $transferSet = $this->filterJournalsByTag($transferSet, $tag); + $earnedSet = $this->filterJournalsByTag($earnedSet, $tag); + $spentSet = $this->filterJournalsByTag($spentSet, $tag); + $transferSet = $this->filterJournalsByTag($transferSet, $tag); foreach ($dates as $currentDate) { $spent = $this->filterJournalsByDate($spentSet, $currentDate['start'], $currentDate['end']); @@ -568,17 +568,17 @@ trait PeriodOverview $title = Navigation::periodShow($currentDate['end'], $currentDate['period']); $entries[] = [ - 'transactions' => 0, - 'title' => $title, - 'route' => route( - 'tags.show', - [$tag->id, $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), - ]; + 'transactions' => 0, + 'title' => $title, + 'route' => route( + 'tags.show', + [$tag->id, $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), + ]; } return $entries; @@ -588,7 +588,7 @@ trait PeriodOverview { $return = []; foreach ($set as $entry) { - $found = false; + $found = false; /** @var array $localTag */ foreach ($entry['tags'] as $localTag) { @@ -610,12 +610,12 @@ trait PeriodOverview */ protected function getTransactionPeriodOverview(string $transactionType, Carbon $start, Carbon $end): array { - $range = Navigation::getViewRange(true); - $types = config(sprintf('firefly.transactionTypesByType.%s', $transactionType)); + $range = Navigation::getViewRange(true); + $types = config(sprintf('firefly.transactionTypesByType.%s', $transactionType)); [$start, $end] = $end < $start ? [$end, $start] : [$start, $end]; // properties for cache - $cache = new CacheProperties(); + $cache = new CacheProperties(); $cache->addProperty($start); $cache->addProperty($end); $cache->addProperty('transactions-period-entries'); @@ -625,16 +625,16 @@ trait PeriodOverview } /** @var array $dates */ - $dates = Navigation::blockPeriods($start, $end, $range); - $entries = []; - $spent = []; - $earned = []; - $transferred = []; + $dates = Navigation::blockPeriods($start, $end, $range); + $entries = []; + $spent = []; + $earned = []; + $transferred = []; // collect all journals in this period (regardless of type) - $collector = app(GroupCollectorInterface::class); + $collector = app(GroupCollectorInterface::class); $collector->setTypes($types)->setRange($start, $end); - $genericSet = $collector->getExtractedJournals(); - $loops = 0; + $genericSet = $collector->getExtractedJournals(); + $loops = 0; foreach ($dates as $currentDate) { $title = Navigation::periodShow($currentDate['end'], $currentDate['period']); @@ -652,14 +652,14 @@ 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; } diff --git a/app/Support/Http/Controllers/RenderPartialViews.php b/app/Support/Http/Controllers/RenderPartialViews.php index 4c5f8348c6..05ff99e38b 100644 --- a/app/Support/Http/Controllers/RenderPartialViews.php +++ b/app/Support/Http/Controllers/RenderPartialViews.php @@ -52,20 +52,20 @@ trait RenderPartialViews protected function budgetEntry(array $attributes): string // generate view for report. { /** @var PopupReportInterface $popupHelper */ - $popupHelper = app(PopupReportInterface::class); + $popupHelper = app(PopupReportInterface::class); /** @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']); + $accountRepos = app(AccountRepositoryInterface::class); + $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.'); } - $journals = $popupHelper->balanceForBudget($budget, $account, $attributes); + $journals = $popupHelper->balanceForBudget($budget, $account, $attributes); try { $view = view('popup.report.balance-amount', compact('journals', 'budget', 'account'))->render(); @@ -113,14 +113,14 @@ trait RenderPartialViews $budgetRepository = app(BudgetRepositoryInterface::class); /** @var PopupReportInterface $popupHelper */ - $popupHelper = app(PopupReportInterface::class); + $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(); } - $journals = $popupHelper->byBudget($budget, $attributes); + $journals = $popupHelper->byBudget($budget, $attributes); try { $view = view('popup.report.budget-spent-amount', compact('journals', 'budget'))->render(); @@ -142,11 +142,11 @@ trait RenderPartialViews protected function categoryEntry(array $attributes): string // generate view for report. { /** @var PopupReportInterface $popupHelper */ - $popupHelper = app(PopupReportInterface::class); + $popupHelper = app(PopupReportInterface::class); /** @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 { @@ -237,15 +237,15 @@ trait RenderPartialViews $accountRepository = app(AccountRepositoryInterface::class); /** @var PopupReportInterface $popupHelper */ - $popupHelper = app(PopupReportInterface::class); + $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.'; } - $journals = $popupHelper->byExpenses($account, $attributes); + $journals = $popupHelper->byExpenses($account, $attributes); try { $view = view('popup.report.expense-entry', compact('journals', 'account'))->render(); @@ -266,8 +266,8 @@ trait RenderPartialViews */ protected function getCurrentActions(Rule $rule): array // get info from object and present. { - $index = 0; - $actions = []; + $index = 0; + $actions = []; // must be repos $currentActions = $rule->ruleActions()->orderBy('order', 'ASC')->get(); @@ -306,11 +306,11 @@ trait RenderPartialViews protected function getCurrentTriggers(Rule $rule): array // get info from object and present. { // TODO duplicated code. - $operators = config('search.operators'); - $triggers = []; + $operators = config('search.operators'); + $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, ] @@ -365,14 +365,14 @@ trait RenderPartialViews $accountRepository = app(AccountRepositoryInterface::class); /** @var PopupReportInterface $popupHelper */ - $popupHelper = app(PopupReportInterface::class); - $account = $accountRepository->find((int) $attributes['accountId']); + $popupHelper = app(PopupReportInterface::class); + $account = $accountRepository->find((int)$attributes['accountId']); if (null === $account) { return 'This is an unknown category. Apologies.'; } - $journals = $popupHelper->byIncome($account, $attributes); + $journals = $popupHelper->byIncome($account, $attributes); try { $view = view('popup.report.income-entry', compact('journals', 'account'))->render(); diff --git a/app/Support/Http/Controllers/RequestInformation.php b/app/Support/Http/Controllers/RequestInformation.php index 6c7731b68f..73a39655e6 100644 --- a/app/Support/Http/Controllers/RequestInformation.php +++ b/app/Support/Http/Controllers/RequestInformation.php @@ -32,10 +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; /** @@ -67,7 +66,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; @@ -85,13 +84,13 @@ trait RequestInformation $page = $this->getPageName(); $specificPage = $this->getSpecificPageName(); // indicator if user has seen the help for this page ( + special page): - $key = sprintf('shown_demo_%s%s', $page, $specificPage); + $key = sprintf('shown_demo_%s%s', $page, $specificPage); // is there an intro for this route? $intro = config(sprintf('intro.%s', $page)) ?? []; $specialIntro = config(sprintf('intro.%s%s', $page, $specificPage)) ?? []; // some routes have a "what" parameter, which indicates a special page: - $shownDemo = true; + $shownDemo = true; // both must be array and either must be > 0 if (count($intro) > 0 || count($specialIntro) > 0) { $shownDemo = app('preferences')->get($key, false)->data; @@ -128,7 +127,7 @@ trait RequestInformation $start = session('start', today(config('app.timezone'))->startOfMonth()); /** @var Carbon $end */ - $end = session('end', today(config('app.timezone'))->endOfMonth()); + $end = session('end', today(config('app.timezone'))->endOfMonth()); if ($start->greaterThanOrEqualTo($date) && $end->greaterThanOrEqualTo($date)) { return true; } @@ -146,20 +145,20 @@ trait RequestInformation final protected function parseAttributes(array $attributes): array // parse input + return result { $attributes['location'] ??= ''; - $attributes['accounts'] = AccountList::routeBinder($attributes['accounts'] ?? '', new Route('get', '', [])); - $date = Carbon::createFromFormat('Ymd', $attributes['startDate']); + $attributes['accounts'] = AccountList::routeBinder($attributes['accounts'] ?? '', new Route('get', '', [])); + $date = Carbon::createFromFormat('Ymd', $attributes['startDate']); if (!$date instanceof Carbon) { $date = today(config('app.timezone')); } $date->startOfMonth(); $attributes['startDate'] = $date; - $date2 = Carbon::createFromFormat('Ymd', $attributes['endDate']); + $date2 = Carbon::createFromFormat('Ymd', $attributes['endDate']); if (!$date2 instanceof Carbon) { $date2 = today(config('app.timezone')); } $date2->endOfDay(); - $attributes['endDate'] = $date2; + $attributes['endDate'] = $date2; return $attributes; } @@ -172,11 +171,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; diff --git a/app/Support/Http/Controllers/RuleManagement.php b/app/Support/Http/Controllers/RuleManagement.php index 6b88c3524c..9903873484 100644 --- a/app/Support/Http/Controllers/RuleManagement.php +++ b/app/Support/Http/Controllers/RuleManagement.php @@ -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(); @@ -74,11 +74,11 @@ trait RuleManagement protected function getPreviousTriggers(Request $request): array { // TODO duplicated code. - $operators = config('search.operators'); - $triggers = []; + $operators = config('search.operators'); + $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,15 +124,15 @@ 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); - $index = 0; + $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, ]; diff --git a/app/Support/Http/Controllers/TransactionCalculation.php b/app/Support/Http/Controllers/TransactionCalculation.php index 17df264ce8..a872b53807 100644 --- a/app/Support/Http/Controllers/TransactionCalculation.php +++ b/app/Support/Http/Controllers/TransactionCalculation.php @@ -39,15 +39,14 @@ trait TransactionCalculation */ protected function getExpensesForOpposing(Collection $accounts, Collection $opposing, Carbon $start, Carbon $end): array { - $total = $accounts->merge($opposing); + $total = $accounts->merge($opposing); /** @var GroupCollectorInterface $collector */ $collector = app(GroupCollectorInterface::class); $collector->setAccounts($total) - ->setRange($start, $end) - ->withAccountInformation() - ->setTypes([TransactionTypeEnum::WITHDRAWAL->value]) - ; + ->setRange($start, $end) + ->withAccountInformation() + ->setTypes([TransactionTypeEnum::WITHDRAWAL->value]); return $collector->getExtractedJournals(); } @@ -61,8 +60,7 @@ trait TransactionCalculation $collector = app(GroupCollectorInterface::class); $collector->setAccounts($accounts)->setRange($start, $end)->setTypes([TransactionTypeEnum::WITHDRAWAL->value, TransactionTypeEnum::TRANSFER->value]) - ->setTags($tags)->withAccountInformation() - ; + ->setTags($tags)->withAccountInformation(); return $collector->getExtractedJournals(); } @@ -75,8 +73,7 @@ trait TransactionCalculation /** @var GroupCollectorInterface $collector */ $collector = app(GroupCollectorInterface::class); $collector->setAccounts($accounts)->setRange($start, $end)->setTypes([TransactionTypeEnum::WITHDRAWAL->value, TransactionTypeEnum::TRANSFER->value]) - ->setBudgets($budgets)->withAccountInformation() - ; + ->setBudgets($budgets)->withAccountInformation(); return $collector->getExtractedJournals(); } @@ -93,8 +90,7 @@ trait TransactionCalculation ->setRange($start, $end) ->setTypes([TransactionTypeEnum::WITHDRAWAL->value, TransactionTypeEnum::TRANSFER->value]) ->setCategories($categories) - ->withAccountInformation() - ; + ->withAccountInformation(); return $collector->getExtractedJournals(); } @@ -107,8 +103,7 @@ trait TransactionCalculation /** @var GroupCollectorInterface $collector */ $collector = app(GroupCollectorInterface::class); $collector->setAccounts($accounts)->setRange($start, $end)->setTypes([TransactionTypeEnum::DEPOSIT->value, TransactionTypeEnum::TRANSFER->value]) - ->setCategories($categories)->withAccountInformation() - ; + ->setCategories($categories)->withAccountInformation(); return $collector->getExtractedJournals(); } @@ -118,7 +113,7 @@ trait TransactionCalculation */ protected function getIncomeForOpposing(Collection $accounts, Collection $opposing, Carbon $start, Carbon $end): array { - $total = $accounts->merge($opposing); + $total = $accounts->merge($opposing); /** @var GroupCollectorInterface $collector */ $collector = app(GroupCollectorInterface::class); @@ -135,8 +130,7 @@ trait TransactionCalculation /** @var GroupCollectorInterface $collector */ $collector = app(GroupCollectorInterface::class); $collector->setAccounts($accounts)->setRange($start, $end)->setTypes([TransactionTypeEnum::DEPOSIT->value, TransactionTypeEnum::TRANSFER->value]) - ->setTags($tags)->withAccountInformation() - ; + ->setTags($tags)->withAccountInformation(); return $collector->getExtractedJournals(); } diff --git a/app/Support/Http/Controllers/UserNavigation.php b/app/Support/Http/Controllers/UserNavigation.php index 8fac232ad0..fc34201443 100644 --- a/app/Support/Http/Controllers/UserNavigation.php +++ b/app/Support/Http/Controllers/UserNavigation.php @@ -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')); @@ -69,7 +69,7 @@ trait UserNavigation final protected function isEditableGroup(TransactionGroup $group): bool { /** @var null|TransactionJournal $journal */ - $journal = $group->transactionJournals()->first(); + $journal = $group->transactionJournals()->first(); if (null === $journal) { return false; } @@ -96,10 +96,10 @@ trait UserNavigation return redirect(route('index')); } - $journal = $transaction->transactionJournal; + $journal = $transaction->transactionJournal; /** @var null|Transaction $other */ - $other = $journal->transactions()->where('id', '!=', $transaction->id)->first(); + $other = $journal->transactions()->where('id', '!=', $transaction->id)->first(); if (null === $other) { app('log')->error(sprintf('Account #%d has no valid journals. Dont know where it belongs.', $account->id)); session()->flash('error', trans('firefly.cant_find_redirect_account')); @@ -119,7 +119,7 @@ trait UserNavigation final protected function redirectGroupToAccount(TransactionGroup $group) { /** @var null|TransactionJournal $journal */ - $journal = $group->transactionJournals()->first(); + $journal = $group->transactionJournals()->first(); if (null === $journal) { app('log')->error(sprintf('No journals in group #%d', $group->id)); From 4ec2fcdb8a07e789d93ca54fc57f08dd66ef24d5 Mon Sep 17 00:00:00 2001 From: James Cole Date: Fri, 26 Sep 2025 06:05:37 +0200 Subject: [PATCH 40/58] Optimize queries for statistics. --- .../Controllers/Account/ShowController.php | 2 +- app/Models/PeriodStatistic.php | 3 +- .../PeriodStatisticRepository.php | 5 + .../PeriodStatisticRepositoryInterface.php | 2 + app/Support/Amount.php | 562 ++-- .../Authentication/RemoteUserGuard.php | 72 +- app/Support/Balance.php | 21 +- app/Support/Binder/AccountList.php | 22 +- app/Support/Binder/BudgetList.php | 16 +- app/Support/Binder/CategoryList.php | 12 +- app/Support/Binder/Date.php | 20 +- app/Support/Binder/JournalList.php | 4 +- app/Support/Binder/TagList.php | 11 +- app/Support/Binder/TagOrId.php | 4 +- app/Support/Binder/UserGroupAccount.php | 7 +- app/Support/Binder/UserGroupBill.php | 7 +- app/Support/Binder/UserGroupExchangeRate.php | 7 +- app/Support/Binder/UserGroupTransaction.php | 7 +- app/Support/CacheProperties.php | 29 +- app/Support/Calendar/Calculator.php | 44 +- .../Chart/Budget/FrontpageChartGenerator.php | 138 +- .../Category/FrontpageChartGenerator.php | 61 +- .../Category/WholePeriodChartGenerator.php | 36 +- app/Support/Chart/ChartData.php | 6 +- app/Support/ChartColour.php | 2 +- app/Support/Cronjobs/AutoBudgetCronjob.php | 6 +- app/Support/Cronjobs/BillWarningCronjob.php | 8 +- app/Support/Cronjobs/ExchangeRatesCronjob.php | 6 +- app/Support/Cronjobs/RecurringCronjob.php | 8 +- app/Support/Cronjobs/UpdateCheckCronjob.php | 12 +- app/Support/Cronjobs/WebhookCronjob.php | 6 +- app/Support/Debug/Timer.php | 2 +- app/Support/ExpandedForm.php | 30 +- app/Support/Export/ExportDataGenerator.php | 1400 +++++----- app/Support/FireflyConfig.php | 96 +- app/Support/Form/AccountForm.php | 82 +- app/Support/Form/CurrencyForm.php | 186 +- app/Support/Form/FormSupport.php | 90 +- app/Support/Form/PiggyBankForm.php | 12 +- app/Support/Form/RuleForm.php | 16 +- .../Http/Api/AccountBalanceGrouped.php | 216 +- app/Support/Http/Api/CleansChartData.php | 2 +- .../Http/Api/ExchangeRateConverter.php | 198 +- .../Http/Controllers/PeriodOverview.php | 865 ++++--- .../Http/Controllers/RequestInformation.php | 32 +- .../JsonApi/Enrichments/AccountEnrichment.php | 351 ++- .../Enrichments/AvailableBudgetEnrichment.php | 78 +- .../JsonApi/Enrichments/BudgetEnrichment.php | 98 +- .../Enrichments/BudgetLimitEnrichment.php | 105 +- .../Enrichments/CategoryEnrichment.php | 63 +- .../Enrichments/EnrichmentInterface.php | 2 +- .../Enrichments/PiggyBankEnrichment.php | 194 +- .../Enrichments/PiggyBankEventEnrichment.php | 102 +- .../Enrichments/RecurringEnrichment.php | 654 +++-- .../Enrichments/SubscriptionEnrichment.php | 227 +- .../TransactionGroupEnrichment.php | 246 +- .../JsonApi/Enrichments/WebhookEnrichment.php | 49 +- .../Models/AccountBalanceCalculator.php | 102 +- app/Support/Models/BillDateCalculator.php | 16 +- app/Support/Models/ReturnsIntegerIdTrait.php | 2 +- .../Models/ReturnsIntegerUserIdTrait.php | 4 +- app/Support/Navigation.php | 417 +-- .../RecalculatesAvailableBudgetsTrait.php | 194 +- app/Support/ParseDateString.php | 341 ++- app/Support/Preferences.php | 254 +- .../Report/Budget/BudgetReportGenerator.php | 414 +-- .../Category/CategoryReportGenerator.php | 154 +- .../Summarizer/TransactionSummarizer.php | 48 +- .../Recurring/CalculateRangeOccurrences.php | 16 +- .../Recurring/CalculateXOccurrences.php | 22 +- .../Recurring/CalculateXOccurrencesSince.php | 24 +- .../Recurring/FiltersWeekends.php | 6 +- .../UserGroup/UserGroupInterface.php | 2 +- .../Repositories/UserGroup/UserGroupTrait.php | 11 +- app/Support/Request/AppendsLocationData.php | 186 +- app/Support/Request/ChecksLogin.php | 10 +- app/Support/Request/ConvertsDataTypes.php | 113 +- app/Support/Request/GetRecurrenceData.php | 4 +- app/Support/Request/ValidatesWebhooks.php | 8 +- app/Support/Search/AccountSearch.php | 20 +- app/Support/Search/OperatorQuerySearch.php | 2302 ++++++++--------- .../Search/QueryParser/GdbotsQueryParser.php | 11 +- .../Search/QueryParser/QueryParser.php | 36 +- .../Singleton/PreferencesSingleton.php | 12 +- app/Support/Steam.php | 602 +++-- .../System/GeneratesInstallationId.php | 2 +- app/Support/System/OAuthKeys.php | 145 +- app/Support/Twig/AmountFormat.php | 138 +- app/Support/Twig/General.php | 459 ++-- app/Support/Twig/Rule.php | 60 +- app/Support/Twig/TransactionGroupTwig.php | 332 ++- app/Support/Twig/Translation.php | 4 +- 92 files changed, 6499 insertions(+), 6514 deletions(-) diff --git a/app/Http/Controllers/Account/ShowController.php b/app/Http/Controllers/Account/ShowController.php index 4b77635b4b..8c9ac76727 100644 --- a/app/Http/Controllers/Account/ShowController.php +++ b/app/Http/Controllers/Account/ShowController.php @@ -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); diff --git a/app/Models/PeriodStatistic.php b/app/Models/PeriodStatistic.php index 030735a00f..194073bc88 100644 --- a/app/Models/PeriodStatistic.php +++ b/app/Models/PeriodStatistic.php @@ -22,7 +22,8 @@ class PeriodStatistic extends Model 'created_at' => 'datetime', 'updated_at' => 'datetime', 'deleted_at' => 'datetime', - 'date' => SeparateTimezoneCaster::class, + 'start' => SeparateTimezoneCaster::class, + 'end' => SeparateTimezoneCaster::class, ]; } diff --git a/app/Repositories/PeriodStatistic/PeriodStatisticRepository.php b/app/Repositories/PeriodStatistic/PeriodStatisticRepository.php index ae8914c0c0..1762329f12 100644 --- a/app/Repositories/PeriodStatistic/PeriodStatisticRepository.php +++ b/app/Repositories/PeriodStatistic/PeriodStatisticRepository.php @@ -66,4 +66,9 @@ class PeriodStatisticRepository implements PeriodStatisticRepositoryInterface return $stat; } + + public function allInRangeForModel(Model $model, Carbon $start, Carbon $end): Collection + { + return $model->primaryPeriodStatistics()->where('start','>=', $start)->where('end','<=', $end)->get(); + } } diff --git a/app/Repositories/PeriodStatistic/PeriodStatisticRepositoryInterface.php b/app/Repositories/PeriodStatistic/PeriodStatisticRepositoryInterface.php index 0b9a6bfbc0..d26d85d101 100644 --- a/app/Repositories/PeriodStatistic/PeriodStatisticRepositoryInterface.php +++ b/app/Repositories/PeriodStatistic/PeriodStatisticRepositoryInterface.php @@ -35,4 +35,6 @@ interface PeriodStatisticRepositoryInterface 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; } diff --git a/app/Support/Amount.php b/app/Support/Amount.php index c3d82f0caf..4b59c3a4d9 100644 --- a/app/Support/Amount.php +++ b/app/Support/Amount.php @@ -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('%s', $result); - } - if (-1 === bccomp($rounded, '0')) { - return sprintf('%s', $result); - } - - return sprintf('%s', $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'] @@ -333,11 +59,11 @@ class Amount // there are five possible positions for the "+" or "-" sign (if it is even used) // pos_a and pos_e could be the ( and ) symbol. - $posA = ''; // before everything - $posB = ''; // before currency symbol - $posC = ''; // after currency symbol - $posD = ''; // before amount - $posE = ''; // after everything + $posA = ''; // before everything + $posB = ''; // before currency symbol + $posC = ''; // after currency symbol + $posD = ''; // before amount + $posE = ''; // after everything // format would be (currency before amount) // AB%sC_D%vE @@ -379,9 +105,283 @@ class Amount } if ($csPrecedes) { - return $posA.$posB.'%s'.$posC.$space.$posD.'%v'.$posE; + return $posA . $posB . '%s' . $posC . $space . $posD . '%v' . $posE; } - return $posA.$posD.'%v'.$space.$posB.'%s'.$posC.$posE; + 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('%s', $result); + } + if (-1 === bccomp($rounded, '0')) { + return sprintf('%s', $result); + } + + return sprintf('%s', $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; } } diff --git a/app/Support/Authentication/RemoteUserGuard.php b/app/Support/Authentication/RemoteUserGuard.php index c2e534184b..353765af94 100644 --- a/app/Support/Authentication/RemoteUserGuard.php +++ b/app/Support/Authentication/RemoteUserGuard.php @@ -48,7 +48,7 @@ class RemoteUserGuard implements Guard public function __construct(protected UserProvider $provider, Application $app) { /** @var null|Request $request */ - $request = $app->get('request'); + $request = $app->get('request'); Log::debug(sprintf('Created RemoteUserGuard for %s "%s"', $request?->getMethod(), $request?->getRequestUri())); $this->application = $app; $this->user = null; @@ -63,8 +63,8 @@ class RemoteUserGuard implements Guard return; } // Get the user identifier from $_SERVER or apache filtered headers - $header = config('auth.guard_header', 'REMOTE_USER'); - $userID = request()->server($header) ?? null; + $header = config('auth.guard_header', 'REMOTE_USER'); + $userID = request()->server($header) ?? null; if (function_exists('apache_request_headers')) { Log::debug('Use apache_request_headers to find user ID.'); @@ -83,10 +83,10 @@ class RemoteUserGuard implements Guard $retrievedUser = $this->provider->retrieveById($userID); // store email address if present in header and not already set. - $header = config('auth.guard_email'); + $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) { @@ -99,7 +99,14 @@ class RemoteUserGuard implements Guard } Log::debug(sprintf('Result of getting user from provider: %s', $retrievedUser->email)); - $this->user = $retrievedUser; + $this->user = $retrievedUser; + } + + public function check(): bool + { + Log::debug(sprintf('Now at %s', __METHOD__)); + + return $this->user() instanceof User; } public function guest(): bool @@ -109,11 +116,32 @@ class RemoteUserGuard implements Guard return !$this->check(); } - public function check(): bool + public function hasUser(): bool { Log::debug(sprintf('Now at %s', __METHOD__)); - return $this->user() instanceof User; + throw new FireflyException('Did not implement RemoteUserGuard::hasUser()'); + } + + /** + * @SuppressWarnings("PHPMD.ShortMethodName") + */ + public function id(): int | string | null + { + Log::debug(sprintf('Now at %s', __METHOD__)); + + return $this->user?->id; + } + + public function setUser(Authenticatable | User | null $user): void // @phpstan-ignore-line + { + Log::debug(sprintf('Now at %s', __METHOD__)); + if ($user instanceof User) { + $this->user = $user; + + return; + } + Log::error(sprintf('Did not set user at %s', __METHOD__)); } public function user(): ?User @@ -129,34 +157,6 @@ class RemoteUserGuard implements Guard return $user; } - public function hasUser(): bool - { - Log::debug(sprintf('Now at %s', __METHOD__)); - - throw new FireflyException('Did not implement RemoteUserGuard::hasUser()'); - } - - /** - * @SuppressWarnings("PHPMD.ShortMethodName") - */ - public function id(): int|string|null - { - Log::debug(sprintf('Now at %s', __METHOD__)); - - return $this->user?->id; - } - - public function setUser(Authenticatable|User|null $user): void // @phpstan-ignore-line - { - Log::debug(sprintf('Now at %s', __METHOD__)); - if ($user instanceof User) { - $this->user = $user; - - return; - } - Log::error(sprintf('Did not set user at %s', __METHOD__)); - } - /** * @throws FireflyException * diff --git a/app/Support/Balance.php b/app/Support/Balance.php index 6b97a04628..f9d684c5ef 100644 --- a/app/Support/Balance.php +++ b/app/Support/Balance.php @@ -48,19 +48,18 @@ class Balance return $cache->get(); } - $query = Transaction::whereIn('transactions.account_id', $accounts->pluck('id')->toArray()) - ->leftJoin('transaction_journals', 'transactions.transaction_journal_id', '=', 'transaction_journals.id') - ->orderBy('transaction_journals.date', 'desc') - ->orderBy('transaction_journals.order', 'asc') - ->orderBy('transaction_journals.description', 'desc') - ->orderBy('transactions.amount', 'desc') - ->where('transaction_journals.date', '<=', $date) - ; + $query = Transaction::whereIn('transactions.account_id', $accounts->pluck('id')->toArray()) + ->leftJoin('transaction_journals', 'transactions.transaction_journal_id', '=', 'transaction_journals.id') + ->orderBy('transaction_journals.date', 'desc') + ->orderBy('transaction_journals.order', 'asc') + ->orderBy('transaction_journals.description', 'desc') + ->orderBy('transactions.amount', 'desc') + ->where('transaction_journals.date', '<=', $date); - $result = $query->get(['transactions.account_id', 'transactions.transaction_currency_id', 'transactions.balance_after']); + $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])) { diff --git a/app/Support/Binder/AccountList.php b/app/Support/Binder/AccountList.php index 314a0c025f..3d6c48728e 100644 --- a/app/Support/Binder/AccountList.php +++ b/app/Support/Binder/AccountList.php @@ -43,23 +43,21 @@ class AccountList implements BinderInterface if ('allAssetAccounts' === $value) { /** @var Collection $collection */ $collection = auth()->user()->accounts() - ->leftJoin('account_types', 'account_types.id', '=', 'accounts.account_type_id') - ->whereIn('account_types.type', [AccountTypeEnum::ASSET->value, AccountTypeEnum::LOAN->value, AccountTypeEnum::DEBT->value, AccountTypeEnum::MORTGAGE->value]) - ->orderBy('accounts.name', 'ASC') - ->get(['accounts.*']) - ; + ->leftJoin('account_types', 'account_types.id', '=', 'accounts.account_type_id') + ->whereIn('account_types.type', [AccountTypeEnum::ASSET->value, AccountTypeEnum::LOAN->value, AccountTypeEnum::DEBT->value, AccountTypeEnum::MORTGAGE->value]) + ->orderBy('accounts.name', 'ASC') + ->get(['accounts.*']); } if ('allAssetAccounts' !== $value) { - $incoming = array_map('\intval', explode(',', $value)); - $list = array_merge(array_unique($incoming), [0]); + $incoming = array_map('\intval', explode(',', $value)); + $list = array_merge(array_unique($incoming), [0]); /** @var Collection $collection */ $collection = auth()->user()->accounts() - ->leftJoin('account_types', 'account_types.id', '=', 'accounts.account_type_id') - ->whereIn('accounts.id', $list) - ->orderBy('accounts.name', 'ASC') - ->get(['accounts.*']) - ; + ->leftJoin('account_types', 'account_types.id', '=', 'accounts.account_type_id') + ->whereIn('accounts.id', $list) + ->orderBy('accounts.name', 'ASC') + ->get(['accounts.*']); } if ($collection->count() > 0) { diff --git a/app/Support/Binder/BudgetList.php b/app/Support/Binder/BudgetList.php index 6526ebd38a..917885a7d0 100644 --- a/app/Support/Binder/BudgetList.php +++ b/app/Support/Binder/BudgetList.php @@ -41,13 +41,12 @@ class BudgetList implements BinderInterface if (auth()->check()) { if ('allBudgets' === $value) { return auth()->user()->budgets()->where('active', true) - ->orderBy('order', 'ASC') - ->orderBy('name', 'ASC') - ->get() - ; + ->orderBy('order', 'ASC') + ->orderBy('name', 'ASC') + ->get(); } - $list = array_unique(array_map('\intval', explode(',', $value))); + $list = array_unique(array_map('\intval', explode(',', $value))); if (0 === count($list)) { // @phpstan-ignore-line app('log')->warning('Budget list count is zero, return 404.'); @@ -57,10 +56,9 @@ class BudgetList implements BinderInterface /** @var Collection $collection */ $collection = auth()->user()->budgets() - ->where('active', true) - ->whereIn('id', $list) - ->get() - ; + ->where('active', true) + ->whereIn('id', $list) + ->get(); // add empty budget if applicable. if (in_array(0, $list, true)) { diff --git a/app/Support/Binder/CategoryList.php b/app/Support/Binder/CategoryList.php index 1275481fa3..cde58f228f 100644 --- a/app/Support/Binder/CategoryList.php +++ b/app/Support/Binder/CategoryList.php @@ -41,21 +41,19 @@ class CategoryList implements BinderInterface if (auth()->check()) { if ('allCategories' === $value) { return auth()->user()->categories() - ->orderBy('name', 'ASC') - ->get() - ; + ->orderBy('name', 'ASC') + ->get(); } - $list = array_unique(array_map('\intval', explode(',', $value))); + $list = array_unique(array_map('\intval', explode(',', $value))); if (0 === count($list)) { // @phpstan-ignore-line throw new NotFoundHttpException(); } /** @var Collection $collection */ $collection = auth()->user()->categories() - ->whereIn('id', $list) - ->get() - ; + ->whereIn('id', $list) + ->get(); // add empty category if applicable. if (in_array(0, $list, true)) { diff --git a/app/Support/Binder/Date.php b/app/Support/Binder/Date.php index 99c0ce4c17..4dcfb314c8 100644 --- a/app/Support/Binder/Date.php +++ b/app/Support/Binder/Date.php @@ -43,16 +43,16 @@ class Date implements BinderInterface /** @var FiscalHelperInterface $fiscalHelper */ $fiscalHelper = app(FiscalHelperInterface::class); - $magicWords = [ - 'currentMonthStart' => today(config('app.timezone'))->startOfMonth(), - 'currentMonthEnd' => today(config('app.timezone'))->endOfMonth(), - 'currentYearStart' => today(config('app.timezone'))->startOfYear(), - 'currentYearEnd' => today(config('app.timezone'))->endOfYear(), + $magicWords = [ + 'currentMonthStart' => today(config('app.timezone'))->startOfMonth(), + 'currentMonthEnd' => today(config('app.timezone'))->endOfMonth(), + 'currentYearStart' => today(config('app.timezone'))->startOfYear(), + 'currentYearEnd' => today(config('app.timezone'))->endOfYear(), - 'previousMonthStart' => today(config('app.timezone'))->startOfMonth()->subDay()->startOfMonth(), - 'previousMonthEnd' => today(config('app.timezone'))->startOfMonth()->subDay()->endOfMonth(), - 'previousYearStart' => today(config('app.timezone'))->startOfYear()->subDay()->startOfYear(), - 'previousYearEnd' => today(config('app.timezone'))->startOfYear()->subDay()->endOfYear(), + 'previousMonthStart' => today(config('app.timezone'))->startOfMonth()->subDay()->startOfMonth(), + 'previousMonthEnd' => today(config('app.timezone'))->startOfMonth()->subDay()->endOfMonth(), + 'previousYearStart' => today(config('app.timezone'))->startOfYear()->subDay()->startOfYear(), + 'previousYearEnd' => today(config('app.timezone'))->startOfYear()->subDay()->endOfYear(), 'currentFiscalYearStart' => $fiscalHelper->startOfFiscalYear(today(config('app.timezone'))), 'currentFiscalYearEnd' => $fiscalHelper->endOfFiscalYear(today(config('app.timezone'))), @@ -68,7 +68,7 @@ class Date implements BinderInterface try { $result = new Carbon($value); - } catch (InvalidDateException|InvalidFormatException $e) { // @phpstan-ignore-line + } catch (InvalidDateException | InvalidFormatException $e) { // @phpstan-ignore-line $message = sprintf('Could not parse date "%s" for user #%d: %s', $value, auth()->user()->id, $e->getMessage()); app('log')->error($message); diff --git a/app/Support/Binder/JournalList.php b/app/Support/Binder/JournalList.php index 5eadcc587a..217dd565ed 100644 --- a/app/Support/Binder/JournalList.php +++ b/app/Support/Binder/JournalList.php @@ -39,7 +39,7 @@ class JournalList implements BinderInterface public static function routeBinder(string $value, Route $route): array { if (auth()->check()) { - $list = self::parseList($value); + $list = self::parseList($value); // get the journals by using the collector. /** @var GroupCollectorInterface $collector */ @@ -47,7 +47,7 @@ class JournalList implements BinderInterface $collector->setTypes([TransactionTypeEnum::WITHDRAWAL->value, TransactionTypeEnum::DEPOSIT->value, TransactionTypeEnum::TRANSFER->value, TransactionTypeEnum::RECONCILIATION->value]); $collector->withCategoryInformation()->withBudgetInformation()->withTagInformation()->withAccountInformation(); $collector->setJournalIds($list); - $result = $collector->getExtractedJournals(); + $result = $collector->getExtractedJournals(); if (0 === count($result)) { throw new NotFoundHttpException(); } diff --git a/app/Support/Binder/TagList.php b/app/Support/Binder/TagList.php index 3dd4835f54..685087da75 100644 --- a/app/Support/Binder/TagList.php +++ b/app/Support/Binder/TagList.php @@ -43,11 +43,10 @@ class TagList implements BinderInterface if (auth()->check()) { if ('allTags' === $value) { return auth()->user()->tags() - ->orderBy('tag', 'ASC') - ->get() - ; + ->orderBy('tag', 'ASC') + ->get(); } - $list = array_unique(array_map('\strtolower', explode(',', $value))); + $list = array_unique(array_map('\strtolower', explode(',', $value))); app('log')->debug('List of tags is', $list); if (0 === count($list)) { // @phpstan-ignore-line @@ -59,7 +58,7 @@ class TagList implements BinderInterface /** @var TagRepositoryInterface $repository */ $repository = app(TagRepositoryInterface::class); $repository->setUser(auth()->user()); - $allTags = $repository->get(); + $allTags = $repository->get(); $collection = $allTags->filter( static function (Tag $tag) use ($list) { @@ -68,7 +67,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; diff --git a/app/Support/Binder/TagOrId.php b/app/Support/Binder/TagOrId.php index bc511e5018..ad3a866e1a 100644 --- a/app/Support/Binder/TagOrId.php +++ b/app/Support/Binder/TagOrId.php @@ -40,9 +40,9 @@ class TagOrId implements BinderInterface $repository = app(TagRepositoryInterface::class); $repository->setUser(auth()->user()); - $result = $repository->findByTag($value); + $result = $repository->findByTag($value); if (null === $result) { - $result = $repository->find((int) $value); + $result = $repository->find((int)$value); } if (null !== $result) { return $result; diff --git a/app/Support/Binder/UserGroupAccount.php b/app/Support/Binder/UserGroupAccount.php index 12d7eff4a2..47a7af5541 100644 --- a/app/Support/Binder/UserGroupAccount.php +++ b/app/Support/Binder/UserGroupAccount.php @@ -41,10 +41,9 @@ class UserGroupAccount implements BinderInterface if (auth()->check()) { /** @var User $user */ $user = auth()->user(); - $account = Account::where('id', (int) $value) - ->where('user_group_id', $user->user_group_id) - ->first() - ; + $account = Account::where('id', (int)$value) + ->where('user_group_id', $user->user_group_id) + ->first(); if (null !== $account) { return $account; } diff --git a/app/Support/Binder/UserGroupBill.php b/app/Support/Binder/UserGroupBill.php index 551846d693..05eff73b6e 100644 --- a/app/Support/Binder/UserGroupBill.php +++ b/app/Support/Binder/UserGroupBill.php @@ -41,10 +41,9 @@ class UserGroupBill implements BinderInterface if (auth()->check()) { /** @var User $user */ $user = auth()->user(); - $currency = Bill::where('id', (int) $value) - ->where('user_group_id', $user->user_group_id) - ->first() - ; + $currency = Bill::where('id', (int)$value) + ->where('user_group_id', $user->user_group_id) + ->first(); if (null !== $currency) { return $currency; } diff --git a/app/Support/Binder/UserGroupExchangeRate.php b/app/Support/Binder/UserGroupExchangeRate.php index 74a65c9348..1bb8fcc374 100644 --- a/app/Support/Binder/UserGroupExchangeRate.php +++ b/app/Support/Binder/UserGroupExchangeRate.php @@ -38,10 +38,9 @@ class UserGroupExchangeRate implements BinderInterface if (auth()->check()) { /** @var User $user */ $user = auth()->user(); - $rate = CurrencyExchangeRate::where('id', (int) $value) - ->where('user_group_id', $user->user_group_id) - ->first() - ; + $rate = CurrencyExchangeRate::where('id', (int)$value) + ->where('user_group_id', $user->user_group_id) + ->first(); if (null !== $rate) { return $rate; } diff --git a/app/Support/Binder/UserGroupTransaction.php b/app/Support/Binder/UserGroupTransaction.php index d9131400f3..61add59c73 100644 --- a/app/Support/Binder/UserGroupTransaction.php +++ b/app/Support/Binder/UserGroupTransaction.php @@ -38,10 +38,9 @@ class UserGroupTransaction implements BinderInterface if (auth()->check()) { /** @var User $user */ $user = auth()->user(); - $group = TransactionGroup::where('id', (int) $value) - ->where('user_group_id', $user->user_group_id) - ->first() - ; + $group = TransactionGroup::where('id', (int)$value) + ->where('user_group_id', $user->user_group_id) + ->first(); if (null !== $group) { return $group; } diff --git a/app/Support/CacheProperties.php b/app/Support/CacheProperties.php index b81f040467..38f2863e92 100644 --- a/app/Support/CacheProperties.php +++ b/app/Support/CacheProperties.php @@ -27,7 +27,6 @@ use Carbon\Carbon; use Illuminate\Support\Collection; use Illuminate\Support\Facades\Cache; use JsonException; - use function Safe\json_encode; /** @@ -78,20 +77,6 @@ class CacheProperties return Cache::has($this->hash); } - private function hash(): void - { - $content = ''; - foreach ($this->properties as $property) { - try { - $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())); - } - } - $this->hash = substr(hash('sha256', $content), 0, 16); - } - /** * @param mixed $data */ @@ -99,4 +84,18 @@ class CacheProperties { Cache::forever($this->hash, $data); } + + private function hash(): void + { + $content = ''; + foreach ($this->properties as $property) { + try { + $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())); + } + } + $this->hash = substr(hash('sha256', $content), 0, 16); + } } diff --git a/app/Support/Calendar/Calculator.php b/app/Support/Calendar/Calculator.php index 3dff9dff07..b6ee2ceebb 100644 --- a/app/Support/Calendar/Calculator.php +++ b/app/Support/Calendar/Calculator.php @@ -33,31 +33,10 @@ use SplObjectStorage; */ class Calculator { - public const int DEFAULT_INTERVAL = 1; + public const int DEFAULT_INTERVAL = 1; 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; diff --git a/app/Support/Chart/Budget/FrontpageChartGenerator.php b/app/Support/Chart/Budget/FrontpageChartGenerator.php index 9e351afc78..e48cc65e42 100644 --- a/app/Support/Chart/Budget/FrontpageChartGenerator.php +++ b/app/Support/Chart/Budget/FrontpageChartGenerator.php @@ -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. @@ -158,7 +181,7 @@ class FrontpageChartGenerator Log::debug(sprintf('Processing limit #%d with %s %s', $limit->id, $limit->transactionCurrency->code, $limit->amount)); } - $spent = $this->opsRepository->sumExpenses($limit->start_date, $limit->end_date, null, new Collection()->push($budget), $currency); + $spent = $this->opsRepository->sumExpenses($limit->start_date, $limit->end_date, null, new Collection()->push($budget), $currency); Log::debug(sprintf('Spent array has %d entries.', count($spent))); /** @var array $entry */ @@ -185,7 +208,7 @@ class FrontpageChartGenerator */ private function processRow(array $data, Budget $budget, BudgetLimit $limit, array $entry): array { - $title = sprintf('%s (%s)', $budget->name, $entry['currency_name']); + $title = sprintf('%s (%s)', $budget->name, $entry['currency_name']); Log::debug(sprintf('Title is "%s"', $title)); if ($limit->start_date->startOfDay()->ne($this->start->startOfDay()) || $limit->end_date->startOfDay()->ne($this->end->startOfDay())) { $title = sprintf( @@ -196,22 +219,22 @@ class FrontpageChartGenerator $limit->end_date->isoFormat($this->monthAndDayFormat) ); } - $usePrimary = $this->convertToPrimary && $this->default->id !== $limit->transaction_currency_id; - $amount = $limit->amount; + $usePrimary = $this->convertToPrimary && $this->default->id !== $limit->transaction_currency_id; + $amount = $limit->amount; Log::debug(sprintf('Amount is "%s".', $amount)); if ($usePrimary && $limit->transaction_currency_id !== $this->default->id) { $amount = $limit->native_amount; 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); - } } diff --git a/app/Support/Chart/Category/FrontpageChartGenerator.php b/app/Support/Chart/Category/FrontpageChartGenerator.php index c27f13c686..b106deafce 100644 --- a/app/Support/Chart/Category/FrontpageChartGenerator.php +++ b/app/Support/Chart/Category/FrontpageChartGenerator.php @@ -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; @@ -66,16 +65,16 @@ class FrontpageChartGenerator public function generate(): array { Log::debug(sprintf('Now in %s', __METHOD__)); - $categories = $this->repository->getCategories(); - $accounts = $this->accountRepos->getAccountsByType([AccountTypeEnum::DEBT->value, AccountTypeEnum::LOAN->value, AccountTypeEnum::MORTGAGE->value, AccountTypeEnum::ASSET->value, AccountTypeEnum::DEFAULT->value]); - $collection = $this->collectExpensesAll($categories, $accounts); + $categories = $this->repository->getCategories(); + $accounts = $this->accountRepos->getAccountsByType([AccountTypeEnum::DEBT->value, AccountTypeEnum::LOAN->value, AccountTypeEnum::MORTGAGE->value, AccountTypeEnum::ASSET->value, AccountTypeEnum::DEFAULT->value]); + $collection = $this->collectExpensesAll($categories, $accounts); // collect for no-category: - $noCategory = $this->collectNoCatExpenses($accounts); - $collection = array_merge($collection, $noCategory); + $noCategory = $this->collectNoCatExpenses($accounts); + $collection = array_merge($collection, $noCategory); // sort temp array by amount. - $amounts = array_column($collection, 'sum_float'); + $amounts = array_column($collection, 'sum_float'); array_multisort($amounts, SORT_ASC, $collection); $currencyData = $this->createCurrencyGroups($collection); @@ -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; - } } diff --git a/app/Support/Chart/Category/WholePeriodChartGenerator.php b/app/Support/Chart/Category/WholePeriodChartGenerator.php index 2a28cb4d62..044b7f28a2 100644 --- a/app/Support/Chart/Category/WholePeriodChartGenerator.php +++ b/app/Support/Chart/Category/WholePeriodChartGenerator.php @@ -40,22 +40,22 @@ class WholePeriodChartGenerator public function generate(Category $category, Carbon $start, Carbon $end): array { - $collection = new Collection()->push($category); + $collection = new Collection()->push($category); /** @var OperationsRepositoryInterface $opsRepository */ - $opsRepository = app(OperationsRepositoryInterface::class); + $opsRepository = app(OperationsRepositoryInterface::class); /** @var AccountRepositoryInterface $accountRepository */ $accountRepository = app(AccountRepositoryInterface::class); - $types = [AccountTypeEnum::DEFAULT->value, AccountTypeEnum::ASSET->value, AccountTypeEnum::LOAN->value, AccountTypeEnum::DEBT->value, AccountTypeEnum::MORTGAGE->value]; - $accounts = $accountRepository->getAccountsByType($types); - $step = $this->calculateStep($start, $end); - $chartData = []; - $spent = []; - $earned = []; + $types = [AccountTypeEnum::DEFAULT->value, AccountTypeEnum::ASSET->value, AccountTypeEnum::LOAN->value, AccountTypeEnum::DEBT->value, AccountTypeEnum::MORTGAGE->value]; + $accounts = $accountRepository->getAccountsByType($types); + $step = $this->calculateStep($start, $end); + $chartData = []; + $spent = []; + $earned = []; - $current = clone $start; + $current = clone $start; while ($current <= $end) { $key = $current->format('Y-m-d'); @@ -65,33 +65,33 @@ class WholePeriodChartGenerator $current = app('navigation')->addPeriod($current, $step, 0); } - $currencies = $this->extractCurrencies($spent) + $this->extractCurrencies($earned); + $currencies = $this->extractCurrencies($spent) + $this->extractCurrencies($earned); // generate chart data (for each currency) /** @var array $currency */ foreach ($currencies as $currency) { - $code = $currency['currency_code']; - $name = $currency['currency_name']; - $chartData[sprintf('spent-in-%s', $code)] = [ - 'label' => (string) trans('firefly.box_spent_in_currency', ['currency' => $name]), + $code = $currency['currency_code']; + $name = $currency['currency_name']; + $chartData[sprintf('spent-in-%s', $code)] = [ + '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 ]; } - $current = clone $start; + $current = clone $start; while ($current <= $end) { - $key = $current->format('Y-m-d'); - $label = app('navigation')->periodShow($current, $step); + $key = $current->format('Y-m-d'); + $label = app('navigation')->periodShow($current, $step); /** @var array $currency */ foreach ($currencies as $currency) { diff --git a/app/Support/Chart/ChartData.php b/app/Support/Chart/ChartData.php index ab03e3dd31..8d58c6a304 100644 --- a/app/Support/Chart/ChartData.php +++ b/app/Support/Chart/ChartData.php @@ -44,12 +44,12 @@ 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']; + $required = ['start', 'date', 'end', 'entries']; foreach ($required as $field) { if (!array_key_exists($field, $data)) { throw new FireflyException(sprintf('Data-set is missing the "%s"-variable.', $field)); diff --git a/app/Support/ChartColour.php b/app/Support/ChartColour.php index 9e938b9946..f08de5c258 100644 --- a/app/Support/ChartColour.php +++ b/app/Support/ChartColour.php @@ -55,7 +55,7 @@ class ChartColour public static function getColour(int $index): string { $index %= count(self::$colours); - $row = self::$colours[$index]; + $row = self::$colours[$index]; return sprintf('rgba(%d, %d, %d, 0.7)', $row[0], $row[1], $row[2]); } diff --git a/app/Support/Cronjobs/AutoBudgetCronjob.php b/app/Support/Cronjobs/AutoBudgetCronjob.php index 9a2a5a0d66..6855884d66 100644 --- a/app/Support/Cronjobs/AutoBudgetCronjob.php +++ b/app/Support/Cronjobs/AutoBudgetCronjob.php @@ -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) { @@ -70,7 +70,7 @@ class AutoBudgetCronjob extends AbstractCronjob Log::info(sprintf('Will now fire auto budget cron job task for date "%s".', $this->date->format('Y-m-d'))); /** @var CreateAutoBudgetLimits $job */ - $job = app(CreateAutoBudgetLimits::class, [$this->date]); + $job = app(CreateAutoBudgetLimits::class, [$this->date]); $job->setDate($this->date); $job->handle(); @@ -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.'); } } diff --git a/app/Support/Cronjobs/BillWarningCronjob.php b/app/Support/Cronjobs/BillWarningCronjob.php index 8a30bb9c0a..f192aa1224 100644 --- a/app/Support/Cronjobs/BillWarningCronjob.php +++ b/app/Support/Cronjobs/BillWarningCronjob.php @@ -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); @@ -82,7 +82,7 @@ class BillWarningCronjob extends AbstractCronjob Log::info(sprintf('Will now fire bill notification job task for date "%s".', $this->date->format('Y-m-d H:i:s'))); /** @var WarnAboutBills $job */ - $job = app(WarnAboutBills::class); + $job = app(WarnAboutBills::class); $job->setDate($this->date); $job->setForce($this->force); $job->handle(); @@ -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.'); } } diff --git a/app/Support/Cronjobs/ExchangeRatesCronjob.php b/app/Support/Cronjobs/ExchangeRatesCronjob.php index 71c9a8e587..57cb788bc7 100644 --- a/app/Support/Cronjobs/ExchangeRatesCronjob.php +++ b/app/Support/Cronjobs/ExchangeRatesCronjob.php @@ -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) { @@ -71,7 +71,7 @@ class ExchangeRatesCronjob extends AbstractCronjob Log::info(sprintf('Will now fire exchange rates cron job task for date "%s".', $this->date->format('Y-m-d'))); /** @var DownloadExchangeRates $job */ - $job = app(DownloadExchangeRates::class); + $job = app(DownloadExchangeRates::class); $job->setDate($this->date); $job->handle(); @@ -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.'); } } diff --git a/app/Support/Cronjobs/RecurringCronjob.php b/app/Support/Cronjobs/RecurringCronjob.php index c722733253..1f8654b9a7 100644 --- a/app/Support/Cronjobs/RecurringCronjob.php +++ b/app/Support/Cronjobs/RecurringCronjob.php @@ -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); @@ -80,7 +80,7 @@ class RecurringCronjob extends AbstractCronjob { Log::info(sprintf('Will now fire recurring cron job task for date "%s".', $this->date->format('Y-m-d H:i:s'))); - $job = new CreateRecurringTransactions($this->date); + $job = new CreateRecurringTransactions($this->date); $job->setForce($this->force); $job->handle(); @@ -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.'); } } diff --git a/app/Support/Cronjobs/UpdateCheckCronjob.php b/app/Support/Cronjobs/UpdateCheckCronjob.php index 6d3cea13ab..c7681037dd 100644 --- a/app/Support/Cronjobs/UpdateCheckCronjob.php +++ b/app/Support/Cronjobs/UpdateCheckCronjob.php @@ -41,8 +41,8 @@ class UpdateCheckCronjob extends AbstractCronjob Log::debug('Now in checkForUpdates()'); // should not check for updates: - $permission = FireflyConfig::get('permission_update_check', -1); - $value = (int) $permission->data; + $permission = FireflyConfig::get('permission_update_check', -1); + $value = (int)$permission->data; if (1 !== $value) { Log::debug('Update check is not enabled.'); // get stuff from job: @@ -56,9 +56,9 @@ class UpdateCheckCronjob extends AbstractCronjob // TODO this is duplicate. /** @var Configuration $lastCheckTime */ - $lastCheckTime = FireflyConfig::get('last_update_check', Carbon::now()->getTimestamp()); - $now = Carbon::now()->getTimestamp(); - $diff = $now - $lastCheckTime->data; + $lastCheckTime = FireflyConfig::get('last_update_check', Carbon::now()->getTimestamp()); + $now = Carbon::now()->getTimestamp(); + $diff = $now - $lastCheckTime->data; Log::debug(sprintf('Last check time is %d, current time is %d, difference is %d', $lastCheckTime->data, $now, $diff)); if ($diff < 604800 && false === $this->force) { // get stuff from job: @@ -71,7 +71,7 @@ class UpdateCheckCronjob extends AbstractCronjob } // last check time was more than a week ago. Log::debug('Have not checked for a new version in a week!'); - $release = $this->getLatestRelease(); + $release = $this->getLatestRelease(); if ('error' === $release['level']) { // get stuff from job: $this->jobFired = true; diff --git a/app/Support/Cronjobs/WebhookCronjob.php b/app/Support/Cronjobs/WebhookCronjob.php index 0cd1899380..84ea6676f5 100644 --- a/app/Support/Cronjobs/WebhookCronjob.php +++ b/app/Support/Cronjobs/WebhookCronjob.php @@ -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.'); } } diff --git a/app/Support/Debug/Timer.php b/app/Support/Debug/Timer.php index 94e48187b7..b23e941a0c 100644 --- a/app/Support/Debug/Timer.php +++ b/app/Support/Debug/Timer.php @@ -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() { diff --git a/app/Support/ExpandedForm.php b/app/Support/ExpandedForm.php index e460353ac2..1dbaeb7d8e 100644 --- a/app/Support/ExpandedForm.php +++ b/app/Support/ExpandedForm.php @@ -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; @@ -43,7 +43,7 @@ class ExpandedForm */ public function amountNoCurrency(string $name, $value = null, ?array $options = null): string { - $options ??= []; + $options ??= []; $label = $this->label($name, $options); $options = $this->expandOptionArray($name, $label, $options); $classes = $this->getHolderClasses($name); @@ -74,8 +74,8 @@ class ExpandedForm */ public function checkbox(string $name, ?int $value = null, $checked = null, ?array $options = null): string { - $options ??= []; - $value ??= 1; + $options ??= []; + $value ??= 1; $options['checked'] = true === $checked; if (app('session')->has('preFilled')) { @@ -83,10 +83,10 @@ class ExpandedForm $options['checked'] = $preFilled[$name] ?? $options['checked']; } - $label = $this->label($name, $options); - $options = $this->expandOptionArray($name, $label, $options); - $classes = $this->getHolderClasses($name); - $value = $this->fillFieldValue($name, $value); + $label = $this->label($name, $options); + $options = $this->expandOptionArray($name, $label, $options); + $classes = $this->getHolderClasses($name); + $value = $this->fillFieldValue($name, $value); unset($options['placeholder'], $options['autocomplete'], $options['class']); @@ -157,10 +157,10 @@ class ExpandedForm public function integer(string $name, $value = null, ?array $options = null): string { $options ??= []; - $label = $this->label($name, $options); - $options = $this->expandOptionArray($name, $label, $options); - $classes = $this->getHolderClasses($name); - $value = $this->fillFieldValue($name, $value); + $label = $this->label($name, $options); + $options = $this->expandOptionArray($name, $label, $options); + $classes = $this->getHolderClasses($name); + $value = $this->fillFieldValue($name, $value); $options['step'] ??= '1'; try { @@ -209,9 +209,9 @@ class ExpandedForm /** @var Model $entry */ foreach ($set as $entry) { // All Eloquent models have an ID - $entryId = $entry->id; - $current = $entry->toArray(); - $title = null; + $entryId = $entry->id; + $current = $entry->toArray(); + $title = null; foreach ($fields as $field) { if (array_key_exists($field, $current) && null === $title) { $title = $current[$field]; diff --git a/app/Support/Export/ExportDataGenerator.php b/app/Support/Export/ExportDataGenerator.php index d4a44e4e6a..b70f2a6615 100644 --- a/app/Support/Export/ExportDataGenerator.php +++ b/app/Support/Export/ExportDataGenerator.php @@ -85,12 +85,12 @@ 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() { - $this->accounts = new Collection(); - $this->start = today(config('app.timezone')); + $this->accounts = new Collection(); + $this->start = today(config('app.timezone')); $this->start->subYear(); $this->end = today(config('app.timezone')); $this->exportTransactions = false; @@ -141,453 +141,6 @@ class ExportDataGenerator return $return; } - /** - * @throws CannotInsertRecord - * @throws Exception - * @throws FireflyException - */ - private function exportAccounts(): string - { - $header = [ - 'user_id', - 'account_id', - 'created_at', - 'updated_at', - 'type', - 'name', - 'virtual_balance', - 'iban', - 'number', - 'active', - 'currency_code', - 'role', - 'cc_type', - 'cc_payment_date', - 'in_net_worth', - 'interest', - 'interest_period', - ]; - - /** @var AccountRepositoryInterface $repository */ - $repository = app(AccountRepositoryInterface::class); - $repository->setUser($this->user); - $allAccounts = $repository->getAccountsByType([]); - $records = []; - - /** @var Account $account */ - foreach ($allAccounts as $account) { - $currency = $repository->getAccountCurrency($account); - $records[] = [ - $this->user->id, - $account->id, - $account->created_at->toAtomString(), - $account->updated_at->toAtomString(), - $account->accountType->type, - $account->name, - $account->virtual_balance, - $account->iban, - $account->account_number, - $account->active, - $currency?->code, - $repository->getMetaValue($account, 'account_role'), - $repository->getMetaValue($account, 'cc_type'), - $repository->getMetaValue($account, 'cc_monthly_payment_date'), - $repository->getMetaValue($account, 'include_net_worth'), - $repository->getMetaValue($account, 'interest'), - $repository->getMetaValue($account, 'interest_period'), - ]; - } - - // load the CSV document from a string - $csv = Writer::createFromString(); - - // insert the header - try { - $csv->insertOne($header); - } catch (CannotInsertRecord $e) { - throw new FireflyException(sprintf(self::ADD_RECORD_ERR, $e->getMessage()), 0, $e); - } - - // insert all the records - $csv->insertAll($records); - - try { - $string = $csv->toString(); - } catch (Exception $e) { // intentional generic exception - app('log')->error($e->getMessage()); - - throw new FireflyException(sprintf(self::EXPORT_ERR, $e->getMessage()), 0, $e); - } - - return $string; - } - - public function setUser(User $user): void - { - $this->user = $user; - } - - /** - * @throws CannotInsertRecord - * @throws Exception - * @throws FireflyException - */ - private function exportBills(): string - { - /** @var BillRepositoryInterface $repository */ - $repository = app(BillRepositoryInterface::class); - $repository->setUser($this->user); - $bills = $repository->getBills(); - $header = [ - 'user_id', - 'bill_id', - 'created_at', - 'updated_at', - 'currency_code', - 'name', - 'amount_min', - 'amount_max', - 'date', - 'repeat_freq', - 'skip', - 'active', - ]; - $records = []; - - /** @var Bill $bill */ - foreach ($bills as $bill) { - $records[] = [ - $this->user->id, - $bill->id, - $bill->created_at->toAtomString(), - $bill->updated_at->toAtomString(), - $bill->transactionCurrency->code, - $bill->name, - $bill->amount_min, - $bill->amount_max, - $bill->date->format('Y-m-d'), - $bill->repeat_freq, - $bill->skip, - $bill->active, - ]; - } - - // load the CSV document from a string - $csv = Writer::createFromString(); - - // insert the header - try { - $csv->insertOne($header); - } catch (CannotInsertRecord $e) { - throw new FireflyException(sprintf(self::ADD_RECORD_ERR, $e->getMessage()), 0, $e); - } - - // insert all the records - $csv->insertAll($records); - - try { - $string = $csv->toString(); - } catch (Exception $e) { // intentional generic exception - app('log')->error($e->getMessage()); - - throw new FireflyException(sprintf(self::EXPORT_ERR, $e->getMessage()), 0, $e); - } - - return $string; - } - - /** - * @throws CannotInsertRecord - * @throws Exception - * @throws FireflyException - */ - private function exportBudgets(): string - { - $header = [ - 'user_id', - 'budget_id', - 'name', - 'active', - 'order', - 'start_date', - 'end_date', - 'currency_code', - 'amount', - ]; - - $budgetRepos = app(BudgetRepositoryInterface::class); - $budgetRepos->setUser($this->user); - $limitRepos = app(BudgetLimitRepositoryInterface::class); - $budgets = $budgetRepos->getBudgets(); - $records = []; - - /** @var Budget $budget */ - foreach ($budgets as $budget) { - $limits = $limitRepos->getBudgetLimits($budget); - - /** @var BudgetLimit $limit */ - foreach ($limits as $limit) { - $records[] = [ - $this->user->id, - $budget->id, - $budget->name, - $budget->active, - $budget->order, - $limit->start_date->format('Y-m-d'), - $limit->end_date->format('Y-m-d'), - $limit->transactionCurrency->code, - $limit->amount, - ]; - } - } - - // load the CSV document from a string - $csv = Writer::createFromString(); - - // insert the header - try { - $csv->insertOne($header); - } catch (CannotInsertRecord $e) { - throw new FireflyException(sprintf(self::ADD_RECORD_ERR, $e->getMessage()), 0, $e); - } - - // insert all the records - $csv->insertAll($records); - - try { - $string = $csv->toString(); - } catch (Exception $e) { // intentional generic exception - app('log')->error($e->getMessage()); - - throw new FireflyException(sprintf(self::EXPORT_ERR, $e->getMessage()), 0, $e); - } - - return $string; - } - - /** - * @throws CannotInsertRecord - * @throws Exception - * @throws FireflyException - */ - private function exportCategories(): string - { - $header = ['user_id', 'category_id', 'created_at', 'updated_at', 'name']; - - /** @var CategoryRepositoryInterface $catRepos */ - $catRepos = app(CategoryRepositoryInterface::class); - $catRepos->setUser($this->user); - - $records = []; - $categories = $catRepos->getCategories(); - - /** @var Category $category */ - foreach ($categories as $category) { - $records[] = [ - $this->user->id, - $category->id, - $category->created_at->toAtomString(), - $category->updated_at->toAtomString(), - $category->name, - ]; - } - - // load the CSV document from a string - $csv = Writer::createFromString(); - - // insert the header - try { - $csv->insertOne($header); - } catch (CannotInsertRecord $e) { - throw new FireflyException(sprintf(self::ADD_RECORD_ERR, $e->getMessage()), 0, $e); - } - - // insert all the records - $csv->insertAll($records); - - try { - $string = $csv->toString(); - } catch (Exception $e) { // intentional generic exception - app('log')->error($e->getMessage()); - - throw new FireflyException(sprintf(self::EXPORT_ERR, $e->getMessage()), 0, $e); - } - - return $string; - } - - /** - * @throws CannotInsertRecord - * @throws Exception - * @throws FireflyException - */ - private function exportPiggies(): string - { - /** @var PiggyBankRepositoryInterface $piggyRepos */ - $piggyRepos = app(PiggyBankRepositoryInterface::class); - $piggyRepos->setUser($this->user); - - /** @var AccountRepositoryInterface $accountRepos */ - $accountRepos = app(AccountRepositoryInterface::class); - $accountRepos->setUser($this->user); - - $header = [ - 'user_id', - 'piggy_bank_id', - 'created_at', - 'updated_at', - 'account_name', - 'account_type', - 'name', - 'currency_code', - 'target_amount', - 'current_amount', - 'start_date', - 'target_date', - 'order', - 'active', - ]; - $records = []; - $piggies = $piggyRepos->getPiggyBanks(); - - /** @var PiggyBank $piggy */ - foreach ($piggies as $piggy) { - $repetition = $piggyRepos->getRepetition($piggy); - $currency = $accountRepos->getAccountCurrency($piggy->account); - $records[] = [ - $this->user->id, - $piggy->id, - $piggy->created_at->toAtomString(), - $piggy->updated_at->toAtomString(), - $piggy->account->name, - $piggy->account->accountType->type, - $piggy->name, - $currency?->code, - $piggy->target_amount, - $repetition?->current_amount, - $piggy->start_date?->format('Y-m-d'), - $piggy->target_date?->format('Y-m-d'), - $piggy->order, - $piggy->active, - ]; - } - - // load the CSV document from a string - $csv = Writer::createFromString(); - - // insert the header - try { - $csv->insertOne($header); - } catch (CannotInsertRecord $e) { - throw new FireflyException(sprintf(self::ADD_RECORD_ERR, $e->getMessage()), 0, $e); - } - - // insert all the records - $csv->insertAll($records); - - try { - $string = $csv->toString(); - } catch (Exception $e) { // intentional generic exception - app('log')->error($e->getMessage()); - - throw new FireflyException(sprintf(self::EXPORT_ERR, $e->getMessage()), 0, $e); - } - - return $string; - } - - /** - * @throws CannotInsertRecord - * @throws Exception - * @throws FireflyException - */ - private function exportRecurring(): string - { - /** @var RecurringRepositoryInterface $recurringRepos */ - $recurringRepos = app(RecurringRepositoryInterface::class); - $recurringRepos->setUser($this->user); - $header = [ - // recurrence: - 'user_id', 'recurrence_id', 'row_contains', 'created_at', 'updated_at', 'type', 'title', 'description', 'first_date', 'repeat_until', 'latest_date', 'repetitions', 'apply_rules', 'active', - - // repetition info: - 'type', 'moment', 'skip', 'weekend', - // transactions + meta: - 'currency_code', 'foreign_currency_code', 'source_name', 'source_type', 'destination_name', 'destination_type', 'amount', 'foreign_amount', 'category', 'budget', 'piggy_bank', 'tags', - ]; - $records = []; - $recurrences = $recurringRepos->get(); - - /** @var Recurrence $recurrence */ - foreach ($recurrences as $recurrence) { - // add recurrence: - $records[] = [ - $this->user->id, $recurrence->id, - 'recurrence', - $recurrence->created_at->toAtomString(), $recurrence->updated_at->toAtomString(), $recurrence->transactionType->type, $recurrence->title, $recurrence->description, $recurrence->first_date?->format('Y-m-d'), $recurrence->repeat_until?->format('Y-m-d'), $recurrence->latest_date?->format('Y-m-d'), $recurrence->repetitions, $recurrence->apply_rules, $recurrence->active, - ]; - - // add new row for each repetition - /** @var RecurrenceRepetition $repetition */ - foreach ($recurrence->recurrenceRepetitions as $repetition) { - $records[] = [ - // recurrence - $this->user->id, - $recurrence->id, - 'repetition', - null, null, null, null, null, null, null, null, null, null, null, - - // repetition: - $repetition->repetition_type, $repetition->repetition_moment, $repetition->repetition_skip, $repetition->weekend, - ]; - } - - /** @var RecurrenceTransaction $transaction */ - foreach ($recurrence->recurrenceTransactions as $transaction) { - $categoryName = $recurringRepos->getCategoryName($transaction); - $budgetId = $recurringRepos->getBudget($transaction); - $piggyBankId = $recurringRepos->getPiggyBank($transaction); - $tags = $recurringRepos->getTags($transaction); - - $records[] = [ - // recurrence - $this->user->id, - $recurrence->id, - 'transaction', - null, null, null, null, null, null, null, null, null, null, null, - - // repetition: - null, null, null, null, - - // transaction: - $transaction->transactionCurrency->code, $transaction->foreignCurrency?->code, $transaction->sourceAccount->name, $transaction->sourceAccount->accountType->type, $transaction->destinationAccount->name, $transaction->destinationAccount->accountType->type, $transaction->amount, $transaction->foreign_amount, $categoryName, $budgetId, $piggyBankId, implode(',', $tags), - ]; - } - } - // load the CSV document from a string - $csv = Writer::createFromString(); - - // insert the header - try { - $csv->insertOne($header); - } catch (CannotInsertRecord $e) { - throw new FireflyException(sprintf(self::ADD_RECORD_ERR, $e->getMessage()), 0, $e); - } - - // insert all the records - $csv->insertAll($records); - - try { - $string = $csv->toString(); - } catch (Exception $e) { // intentional generic exception - app('log')->error($e->getMessage()); - - throw new FireflyException(sprintf(self::EXPORT_ERR, $e->getMessage()), 0, $e); - } - - return $string; - } - /** * @SuppressWarnings("PHPMD.UnusedFormalParameter") */ @@ -596,256 +149,6 @@ class ExportDataGenerator return null; } - /** - * @throws CannotInsertRecord - * @throws Exception - * @throws FireflyException - */ - private function exportRules(): string - { - $header = [ - 'user_id', 'rule_id', 'row_contains', - 'created_at', 'updated_at', 'group_id', 'title', 'description', 'order', 'active', 'stop_processing', 'strict', - 'trigger_type', 'trigger_value', 'trigger_order', 'trigger_active', 'trigger_stop_processing', - 'action_type', 'action_value', 'action_order', 'action_active', 'action_stop_processing']; - $ruleRepos = app(RuleRepositoryInterface::class); - $ruleRepos->setUser($this->user); - $rules = $ruleRepos->getAll(); - $records = []; - - /** @var Rule $rule */ - foreach ($rules as $rule) { - $entry = [ - $this->user->id, $rule->id, - 'rule', - $rule->created_at->toAtomString(), $rule->updated_at->toAtomString(), $rule->ruleGroup->id, $rule->ruleGroup->title, $rule->title, $rule->description, $rule->order, $rule->active, $rule->stop_processing, $rule->strict, - null, null, null, null, null, null, null, null, null, - ]; - $records[] = $entry; - - /** @var RuleTrigger $trigger */ - foreach ($rule->ruleTriggers as $trigger) { - $entry = [ - $this->user->id, - $rule->id, - 'trigger', - null, null, null, null, null, null, null, null, null, - $trigger->trigger_type, $trigger->trigger_value, $trigger->order, $trigger->active, $trigger->stop_processing, - null, null, null, null, null, - ]; - $records[] = $entry; - } - - /** @var RuleAction $action */ - foreach ($rule->ruleActions as $action) { - $entry = [ - $this->user->id, - $rule->id, - 'action', - null, null, null, null, null, null, null, null, null, null, null, null, null, null, - $action->action_type, $action->action_value, $action->order, $action->active, $action->stop_processing, - ]; - $records[] = $entry; - } - } - - // load the CSV document from a string - $csv = Writer::createFromString(); - - // insert the header - try { - $csv->insertOne($header); - } catch (CannotInsertRecord $e) { - throw new FireflyException(sprintf(self::ADD_RECORD_ERR, $e->getMessage()), 0, $e); - } - - // insert all the records - $csv->insertAll($records); - - try { - $string = $csv->toString(); - } catch (Exception $e) { // intentional generic exception - app('log')->error($e->getMessage()); - - throw new FireflyException(sprintf(self::EXPORT_ERR, $e->getMessage()), 0, $e); - } - - return $string; - } - - /** - * @throws CannotInsertRecord - * @throws Exception - * @throws FireflyException - */ - private function exportTags(): string - { - $header = ['user_id', 'tag_id', 'created_at', 'updated_at', 'tag', 'date', 'description', 'latitude', 'longitude', 'zoom_level']; - - $tagRepos = app(TagRepositoryInterface::class); - $tagRepos->setUser($this->user); - $tags = $tagRepos->get(); - $records = []; - - /** @var Tag $tag */ - foreach ($tags as $tag) { - $records[] = [ - $this->user->id, - $tag->id, - $tag->created_at->toAtomString(), - $tag->updated_at->toAtomString(), - $tag->tag, - $tag->date?->format('Y-m-d'), - $tag->description, - $tag->latitude, - $tag->longitude, - $tag->zoomLevel, - ]; - } - - // load the CSV document from a string - $csv = Writer::createFromString(); - - // insert the header - try { - $csv->insertOne($header); - } catch (CannotInsertRecord $e) { - throw new FireflyException(sprintf(self::ADD_RECORD_ERR, $e->getMessage()), 0, $e); - } - - // insert all the records - $csv->insertAll($records); - - try { - $string = $csv->toString(); - } catch (Exception $e) { // intentional generic exception - app('log')->error($e->getMessage()); - - throw new FireflyException(sprintf(self::EXPORT_ERR, $e->getMessage()), 0, $e); - } - - return $string; - } - - /** - * @throws CannotInsertRecord - * @throws Exception - * @throws FireflyException - */ - private function exportTransactions(): string - { - Log::debug('Will now export transactions.'); - // TODO better place for keys? - $header = ['user_id', 'group_id', 'journal_id', 'created_at', 'updated_at', 'group_title', 'type', 'currency_code', 'amount', 'foreign_currency_code', 'foreign_amount', 'primary_currency_code', 'pc_amount', 'pc_foreign_amount', 'description', 'date', 'source_name', 'source_iban', 'source_type', 'destination_name', 'destination_iban', 'destination_type', 'reconciled', 'category', 'budget', 'bill', 'tags', 'notes']; - - $metaFields = config('firefly.journal_meta_fields'); - $header = array_merge($header, $metaFields); - $primary = Amount::getPrimaryCurrency(); - - $collector = app(GroupCollectorInterface::class); - $collector->setUser($this->user); - $collector->setRange($this->start, $this->end)->withAccountInformation()->withCategoryInformation()->withBillInformation()->withBudgetInformation()->withTagInformation()->withNotes(); - if (0 !== $this->accounts->count()) { - $collector->setAccounts($this->accounts); - } - - $journals = $collector->getExtractedJournals(); - - // get repository for meta data: - $repository = app(TransactionGroupRepositoryInterface::class); - $repository->setUser($this->user); - - $records = []; - - /** @var array $journal */ - foreach ($journals as $journal) { - $metaData = $repository->getMetaFields($journal['transaction_journal_id'], $metaFields); - $amount = Steam::bcround(Steam::negative($journal['amount']), $journal['currency_decimal_places']); - $foreignAmount = null === $journal['foreign_amount'] ? null : Steam::bcround(Steam::negative($journal['foreign_amount']), $journal['foreign_currency_decimal_places']); - $pcAmount = null === $journal['pc_amount'] ? null : Steam::bcround(Steam::negative($journal['pc_amount']), $primary->decimal_places); - $pcForeignAmount = null === $journal['pc_foreign_amount'] ? null : Steam::bcround(Steam::negative($journal['pc_foreign_amount']), $primary->decimal_places); - - if (TransactionTypeEnum::WITHDRAWAL->value !== $journal['transaction_type_type']) { - $amount = Steam::bcround(Steam::positive($journal['amount']), $journal['currency_decimal_places']); - $foreignAmount = null === $journal['foreign_amount'] ? null : Steam::bcround(Steam::positive($journal['foreign_amount']), $journal['foreign_currency_decimal_places']); - $pcAmount = null === $journal['pc_amount'] ? null : Steam::bcround(Steam::positive($journal['pc_amount']), $primary->decimal_places); - $pcForeignAmount = null === $journal['pc_foreign_amount'] ? null : Steam::bcround(Steam::positive($journal['pc_foreign_amount']), $primary->decimal_places); - } - - // opening balance depends on source account type. - if (TransactionTypeEnum::OPENING_BALANCE->value === $journal['transaction_type_type'] && AccountTypeEnum::ASSET->value === $journal['source_account_type']) { - $amount = Steam::bcround(Steam::negative($journal['amount']), $journal['currency_decimal_places']); - $foreignAmount = null === $journal['foreign_amount'] ? null : Steam::bcround(Steam::negative($journal['foreign_amount']), $journal['foreign_currency_decimal_places']); - $pcAmount = null === $journal['pc_amount'] ? null : Steam::bcround(Steam::negative($journal['pc_amount']), $primary->decimal_places); - $pcForeignAmount = null === $journal['pc_foreign_amount'] ? null : Steam::bcround(Steam::negative($journal['pc_foreign_amount']), $primary->decimal_places); - } - - $records[] = [ - $journal['user_id'], $journal['transaction_group_id'], $journal['transaction_journal_id'], $journal['created_at']->toAtomString(), $journal['updated_at']->toAtomString(), $journal['transaction_group_title'], $journal['transaction_type_type'], - // amounts and currencies - $journal['currency_code'], $amount, $journal['foreign_currency_code'], $foreignAmount, $primary->code, $pcAmount, $pcForeignAmount, - - // more fields - $journal['description'], $journal['date']->toAtomString(), $journal['source_account_name'], $journal['source_account_iban'], $journal['source_account_type'], $journal['destination_account_name'], $journal['destination_account_iban'], $journal['destination_account_type'], $journal['reconciled'], $journal['category_name'], $journal['budget_name'], $journal['bill_name'], - $this->mergeTags($journal['tags']), - $this->clearStringKeepNewlines($journal['notes']), - - // sepa - $metaData['sepa_cc'], $metaData['sepa_ct_op'], $metaData['sepa_ct_id'], $metaData['sepa_db'], $metaData['sepa_country'], $metaData['sepa_ep'], $metaData['sepa_ci'], $metaData['sepa_batch_id'], $metaData['external_url'], - - // dates - $metaData['interest_date'], $metaData['book_date'], $metaData['process_date'], $metaData['due_date'], $metaData['payment_date'], $metaData['invoice_date'], - - // others - $metaData['recurrence_id'], $metaData['internal_reference'], $metaData['bunq_payment_id'], $metaData['import_hash'], $metaData['import_hash_v2'], $metaData['external_id'], $metaData['original_source'], - - // recurring transactions - $metaData['recurrence_total'], $metaData['recurrence_count'], - ]; - } - - // load the CSV document from a string - $csv = Writer::createFromString(); - - // insert the header - try { - $csv->insertOne($header); - } catch (CannotInsertRecord $e) { - throw new FireflyException(sprintf(self::ADD_RECORD_ERR, $e->getMessage()), 0, $e); - } - - // insert all the records - $csv->insertAll($records); - - try { - $string = $csv->toString(); - } catch (Exception $e) { // intentional generic exception - app('log')->error($e->getMessage()); - - throw new FireflyException(sprintf(self::EXPORT_ERR, $e->getMessage()), 0, $e); - } - - return $string; - } - - public function setAccounts(Collection $accounts): void - { - $this->accounts = $accounts; - } - - private function mergeTags(array $tags): string - { - if (0 === count($tags)) { - return ''; - } - $smol = []; - foreach ($tags as $tag) { - $smol[] = $tag['name']; - } - - return implode(',', $smol); - } - /** * @SuppressWarnings("PHPMD.UnusedFormalParameter") */ @@ -854,6 +157,11 @@ class ExportDataGenerator return null; } + public function setAccounts(Collection $accounts): void + { + $this->accounts = $accounts; + } + public function setEnd(Carbon $end): void { $this->end = $end; @@ -909,8 +217,700 @@ class ExportDataGenerator $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 + * @throws FireflyException + */ + private function exportAccounts(): string + { + $header = [ + 'user_id', + 'account_id', + 'created_at', + 'updated_at', + 'type', + 'name', + 'virtual_balance', + 'iban', + 'number', + 'active', + 'currency_code', + 'role', + 'cc_type', + 'cc_payment_date', + 'in_net_worth', + 'interest', + 'interest_period', + ]; + + /** @var AccountRepositoryInterface $repository */ + $repository = app(AccountRepositoryInterface::class); + $repository->setUser($this->user); + $allAccounts = $repository->getAccountsByType([]); + $records = []; + + /** @var Account $account */ + foreach ($allAccounts as $account) { + $currency = $repository->getAccountCurrency($account); + $records[] = [ + $this->user->id, + $account->id, + $account->created_at->toAtomString(), + $account->updated_at->toAtomString(), + $account->accountType->type, + $account->name, + $account->virtual_balance, + $account->iban, + $account->account_number, + $account->active, + $currency?->code, + $repository->getMetaValue($account, 'account_role'), + $repository->getMetaValue($account, 'cc_type'), + $repository->getMetaValue($account, 'cc_monthly_payment_date'), + $repository->getMetaValue($account, 'include_net_worth'), + $repository->getMetaValue($account, 'interest'), + $repository->getMetaValue($account, 'interest_period'), + ]; + } + + // load the CSV document from a string + $csv = Writer::createFromString(); + + // insert the header + try { + $csv->insertOne($header); + } catch (CannotInsertRecord $e) { + throw new FireflyException(sprintf(self::ADD_RECORD_ERR, $e->getMessage()), 0, $e); + } + + // insert all the records + $csv->insertAll($records); + + try { + $string = $csv->toString(); + } catch (Exception $e) { // intentional generic exception + app('log')->error($e->getMessage()); + + throw new FireflyException(sprintf(self::EXPORT_ERR, $e->getMessage()), 0, $e); + } + + return $string; + } + + /** + * @throws CannotInsertRecord + * @throws Exception + * @throws FireflyException + */ + private function exportBills(): string + { + /** @var BillRepositoryInterface $repository */ + $repository = app(BillRepositoryInterface::class); + $repository->setUser($this->user); + $bills = $repository->getBills(); + $header = [ + 'user_id', + 'bill_id', + 'created_at', + 'updated_at', + 'currency_code', + 'name', + 'amount_min', + 'amount_max', + 'date', + 'repeat_freq', + 'skip', + 'active', + ]; + $records = []; + + /** @var Bill $bill */ + foreach ($bills as $bill) { + $records[] = [ + $this->user->id, + $bill->id, + $bill->created_at->toAtomString(), + $bill->updated_at->toAtomString(), + $bill->transactionCurrency->code, + $bill->name, + $bill->amount_min, + $bill->amount_max, + $bill->date->format('Y-m-d'), + $bill->repeat_freq, + $bill->skip, + $bill->active, + ]; + } + + // load the CSV document from a string + $csv = Writer::createFromString(); + + // insert the header + try { + $csv->insertOne($header); + } catch (CannotInsertRecord $e) { + throw new FireflyException(sprintf(self::ADD_RECORD_ERR, $e->getMessage()), 0, $e); + } + + // insert all the records + $csv->insertAll($records); + + try { + $string = $csv->toString(); + } catch (Exception $e) { // intentional generic exception + app('log')->error($e->getMessage()); + + throw new FireflyException(sprintf(self::EXPORT_ERR, $e->getMessage()), 0, $e); + } + + return $string; + } + + /** + * @throws CannotInsertRecord + * @throws Exception + * @throws FireflyException + */ + private function exportBudgets(): string + { + $header = [ + 'user_id', + 'budget_id', + 'name', + 'active', + 'order', + 'start_date', + 'end_date', + 'currency_code', + 'amount', + ]; + + $budgetRepos = app(BudgetRepositoryInterface::class); + $budgetRepos->setUser($this->user); + $limitRepos = app(BudgetLimitRepositoryInterface::class); + $budgets = $budgetRepos->getBudgets(); + $records = []; + + /** @var Budget $budget */ + foreach ($budgets as $budget) { + $limits = $limitRepos->getBudgetLimits($budget); + + /** @var BudgetLimit $limit */ + foreach ($limits as $limit) { + $records[] = [ + $this->user->id, + $budget->id, + $budget->name, + $budget->active, + $budget->order, + $limit->start_date->format('Y-m-d'), + $limit->end_date->format('Y-m-d'), + $limit->transactionCurrency->code, + $limit->amount, + ]; + } + } + + // load the CSV document from a string + $csv = Writer::createFromString(); + + // insert the header + try { + $csv->insertOne($header); + } catch (CannotInsertRecord $e) { + throw new FireflyException(sprintf(self::ADD_RECORD_ERR, $e->getMessage()), 0, $e); + } + + // insert all the records + $csv->insertAll($records); + + try { + $string = $csv->toString(); + } catch (Exception $e) { // intentional generic exception + app('log')->error($e->getMessage()); + + throw new FireflyException(sprintf(self::EXPORT_ERR, $e->getMessage()), 0, $e); + } + + return $string; + } + + /** + * @throws CannotInsertRecord + * @throws Exception + * @throws FireflyException + */ + private function exportCategories(): string + { + $header = ['user_id', 'category_id', 'created_at', 'updated_at', 'name']; + + /** @var CategoryRepositoryInterface $catRepos */ + $catRepos = app(CategoryRepositoryInterface::class); + $catRepos->setUser($this->user); + + $records = []; + $categories = $catRepos->getCategories(); + + /** @var Category $category */ + foreach ($categories as $category) { + $records[] = [ + $this->user->id, + $category->id, + $category->created_at->toAtomString(), + $category->updated_at->toAtomString(), + $category->name, + ]; + } + + // load the CSV document from a string + $csv = Writer::createFromString(); + + // insert the header + try { + $csv->insertOne($header); + } catch (CannotInsertRecord $e) { + throw new FireflyException(sprintf(self::ADD_RECORD_ERR, $e->getMessage()), 0, $e); + } + + // insert all the records + $csv->insertAll($records); + + try { + $string = $csv->toString(); + } catch (Exception $e) { // intentional generic exception + app('log')->error($e->getMessage()); + + throw new FireflyException(sprintf(self::EXPORT_ERR, $e->getMessage()), 0, $e); + } + + return $string; + } + + /** + * @throws CannotInsertRecord + * @throws Exception + * @throws FireflyException + */ + private function exportPiggies(): string + { + /** @var PiggyBankRepositoryInterface $piggyRepos */ + $piggyRepos = app(PiggyBankRepositoryInterface::class); + $piggyRepos->setUser($this->user); + + /** @var AccountRepositoryInterface $accountRepos */ + $accountRepos = app(AccountRepositoryInterface::class); + $accountRepos->setUser($this->user); + + $header = [ + 'user_id', + 'piggy_bank_id', + 'created_at', + 'updated_at', + 'account_name', + 'account_type', + 'name', + 'currency_code', + 'target_amount', + 'current_amount', + 'start_date', + 'target_date', + 'order', + 'active', + ]; + $records = []; + $piggies = $piggyRepos->getPiggyBanks(); + + /** @var PiggyBank $piggy */ + foreach ($piggies as $piggy) { + $repetition = $piggyRepos->getRepetition($piggy); + $currency = $accountRepos->getAccountCurrency($piggy->account); + $records[] = [ + $this->user->id, + $piggy->id, + $piggy->created_at->toAtomString(), + $piggy->updated_at->toAtomString(), + $piggy->account->name, + $piggy->account->accountType->type, + $piggy->name, + $currency?->code, + $piggy->target_amount, + $repetition?->current_amount, + $piggy->start_date?->format('Y-m-d'), + $piggy->target_date?->format('Y-m-d'), + $piggy->order, + $piggy->active, + ]; + } + + // load the CSV document from a string + $csv = Writer::createFromString(); + + // insert the header + try { + $csv->insertOne($header); + } catch (CannotInsertRecord $e) { + throw new FireflyException(sprintf(self::ADD_RECORD_ERR, $e->getMessage()), 0, $e); + } + + // insert all the records + $csv->insertAll($records); + + try { + $string = $csv->toString(); + } catch (Exception $e) { // intentional generic exception + app('log')->error($e->getMessage()); + + throw new FireflyException(sprintf(self::EXPORT_ERR, $e->getMessage()), 0, $e); + } + + return $string; + } + + /** + * @throws CannotInsertRecord + * @throws Exception + * @throws FireflyException + */ + private function exportRecurring(): string + { + /** @var RecurringRepositoryInterface $recurringRepos */ + $recurringRepos = app(RecurringRepositoryInterface::class); + $recurringRepos->setUser($this->user); + $header = [ + // recurrence: + 'user_id', 'recurrence_id', 'row_contains', 'created_at', 'updated_at', 'type', 'title', 'description', 'first_date', 'repeat_until', 'latest_date', 'repetitions', 'apply_rules', 'active', + + // repetition info: + 'type', 'moment', 'skip', 'weekend', + // transactions + meta: + 'currency_code', 'foreign_currency_code', 'source_name', 'source_type', 'destination_name', 'destination_type', 'amount', 'foreign_amount', 'category', 'budget', 'piggy_bank', 'tags', + ]; + $records = []; + $recurrences = $recurringRepos->get(); + + /** @var Recurrence $recurrence */ + foreach ($recurrences as $recurrence) { + // add recurrence: + $records[] = [ + $this->user->id, $recurrence->id, + 'recurrence', + $recurrence->created_at->toAtomString(), $recurrence->updated_at->toAtomString(), $recurrence->transactionType->type, $recurrence->title, $recurrence->description, $recurrence->first_date?->format('Y-m-d'), $recurrence->repeat_until?->format('Y-m-d'), $recurrence->latest_date?->format('Y-m-d'), $recurrence->repetitions, $recurrence->apply_rules, $recurrence->active, + ]; + + // add new row for each repetition + /** @var RecurrenceRepetition $repetition */ + foreach ($recurrence->recurrenceRepetitions as $repetition) { + $records[] = [ + // recurrence + $this->user->id, + $recurrence->id, + 'repetition', + null, null, null, null, null, null, null, null, null, null, null, + + // repetition: + $repetition->repetition_type, $repetition->repetition_moment, $repetition->repetition_skip, $repetition->weekend, + ]; + } + + /** @var RecurrenceTransaction $transaction */ + foreach ($recurrence->recurrenceTransactions as $transaction) { + $categoryName = $recurringRepos->getCategoryName($transaction); + $budgetId = $recurringRepos->getBudget($transaction); + $piggyBankId = $recurringRepos->getPiggyBank($transaction); + $tags = $recurringRepos->getTags($transaction); + + $records[] = [ + // recurrence + $this->user->id, + $recurrence->id, + 'transaction', + null, null, null, null, null, null, null, null, null, null, null, + + // repetition: + null, null, null, null, + + // transaction: + $transaction->transactionCurrency->code, $transaction->foreignCurrency?->code, $transaction->sourceAccount->name, $transaction->sourceAccount->accountType->type, $transaction->destinationAccount->name, $transaction->destinationAccount->accountType->type, $transaction->amount, $transaction->foreign_amount, $categoryName, $budgetId, $piggyBankId, implode(',', $tags), + ]; + } + } + // load the CSV document from a string + $csv = Writer::createFromString(); + + // insert the header + try { + $csv->insertOne($header); + } catch (CannotInsertRecord $e) { + throw new FireflyException(sprintf(self::ADD_RECORD_ERR, $e->getMessage()), 0, $e); + } + + // insert all the records + $csv->insertAll($records); + + try { + $string = $csv->toString(); + } catch (Exception $e) { // intentional generic exception + app('log')->error($e->getMessage()); + + throw new FireflyException(sprintf(self::EXPORT_ERR, $e->getMessage()), 0, $e); + } + + return $string; + } + + /** + * @throws CannotInsertRecord + * @throws Exception + * @throws FireflyException + */ + private function exportRules(): string + { + $header = [ + 'user_id', 'rule_id', 'row_contains', + 'created_at', 'updated_at', 'group_id', 'title', 'description', 'order', 'active', 'stop_processing', 'strict', + 'trigger_type', 'trigger_value', 'trigger_order', 'trigger_active', 'trigger_stop_processing', + 'action_type', 'action_value', 'action_order', 'action_active', 'action_stop_processing']; + $ruleRepos = app(RuleRepositoryInterface::class); + $ruleRepos->setUser($this->user); + $rules = $ruleRepos->getAll(); + $records = []; + + /** @var Rule $rule */ + foreach ($rules as $rule) { + $entry = [ + $this->user->id, $rule->id, + 'rule', + $rule->created_at->toAtomString(), $rule->updated_at->toAtomString(), $rule->ruleGroup->id, $rule->ruleGroup->title, $rule->title, $rule->description, $rule->order, $rule->active, $rule->stop_processing, $rule->strict, + null, null, null, null, null, null, null, null, null, + ]; + $records[] = $entry; + + /** @var RuleTrigger $trigger */ + foreach ($rule->ruleTriggers as $trigger) { + $entry = [ + $this->user->id, + $rule->id, + 'trigger', + null, null, null, null, null, null, null, null, null, + $trigger->trigger_type, $trigger->trigger_value, $trigger->order, $trigger->active, $trigger->stop_processing, + null, null, null, null, null, + ]; + $records[] = $entry; + } + + /** @var RuleAction $action */ + foreach ($rule->ruleActions as $action) { + $entry = [ + $this->user->id, + $rule->id, + 'action', + null, null, null, null, null, null, null, null, null, null, null, null, null, null, + $action->action_type, $action->action_value, $action->order, $action->active, $action->stop_processing, + ]; + $records[] = $entry; + } + } + + // load the CSV document from a string + $csv = Writer::createFromString(); + + // insert the header + try { + $csv->insertOne($header); + } catch (CannotInsertRecord $e) { + throw new FireflyException(sprintf(self::ADD_RECORD_ERR, $e->getMessage()), 0, $e); + } + + // insert all the records + $csv->insertAll($records); + + try { + $string = $csv->toString(); + } catch (Exception $e) { // intentional generic exception + app('log')->error($e->getMessage()); + + throw new FireflyException(sprintf(self::EXPORT_ERR, $e->getMessage()), 0, $e); + } + + return $string; + } + + /** + * @throws CannotInsertRecord + * @throws Exception + * @throws FireflyException + */ + private function exportTags(): string + { + $header = ['user_id', 'tag_id', 'created_at', 'updated_at', 'tag', 'date', 'description', 'latitude', 'longitude', 'zoom_level']; + + $tagRepos = app(TagRepositoryInterface::class); + $tagRepos->setUser($this->user); + $tags = $tagRepos->get(); + $records = []; + + /** @var Tag $tag */ + foreach ($tags as $tag) { + $records[] = [ + $this->user->id, + $tag->id, + $tag->created_at->toAtomString(), + $tag->updated_at->toAtomString(), + $tag->tag, + $tag->date?->format('Y-m-d'), + $tag->description, + $tag->latitude, + $tag->longitude, + $tag->zoomLevel, + ]; + } + + // load the CSV document from a string + $csv = Writer::createFromString(); + + // insert the header + try { + $csv->insertOne($header); + } catch (CannotInsertRecord $e) { + throw new FireflyException(sprintf(self::ADD_RECORD_ERR, $e->getMessage()), 0, $e); + } + + // insert all the records + $csv->insertAll($records); + + try { + $string = $csv->toString(); + } catch (Exception $e) { // intentional generic exception + app('log')->error($e->getMessage()); + + throw new FireflyException(sprintf(self::EXPORT_ERR, $e->getMessage()), 0, $e); + } + + return $string; + } + + /** + * @throws CannotInsertRecord + * @throws Exception + * @throws FireflyException + */ + private function exportTransactions(): string + { + Log::debug('Will now export transactions.'); + // TODO better place for keys? + $header = ['user_id', 'group_id', 'journal_id', 'created_at', 'updated_at', 'group_title', 'type', 'currency_code', 'amount', 'foreign_currency_code', 'foreign_amount', 'primary_currency_code', 'pc_amount', 'pc_foreign_amount', 'description', 'date', 'source_name', 'source_iban', 'source_type', 'destination_name', 'destination_iban', 'destination_type', 'reconciled', 'category', 'budget', 'bill', 'tags', 'notes']; + + $metaFields = config('firefly.journal_meta_fields'); + $header = array_merge($header, $metaFields); + $primary = Amount::getPrimaryCurrency(); + + $collector = app(GroupCollectorInterface::class); + $collector->setUser($this->user); + $collector->setRange($this->start, $this->end)->withAccountInformation()->withCategoryInformation()->withBillInformation()->withBudgetInformation()->withTagInformation()->withNotes(); + if (0 !== $this->accounts->count()) { + $collector->setAccounts($this->accounts); + } + + $journals = $collector->getExtractedJournals(); + + // get repository for meta data: + $repository = app(TransactionGroupRepositoryInterface::class); + $repository->setUser($this->user); + + $records = []; + + /** @var array $journal */ + foreach ($journals as $journal) { + $metaData = $repository->getMetaFields($journal['transaction_journal_id'], $metaFields); + $amount = Steam::bcround(Steam::negative($journal['amount']), $journal['currency_decimal_places']); + $foreignAmount = null === $journal['foreign_amount'] ? null : Steam::bcround(Steam::negative($journal['foreign_amount']), $journal['foreign_currency_decimal_places']); + $pcAmount = null === $journal['pc_amount'] ? null : Steam::bcround(Steam::negative($journal['pc_amount']), $primary->decimal_places); + $pcForeignAmount = null === $journal['pc_foreign_amount'] ? null : Steam::bcround(Steam::negative($journal['pc_foreign_amount']), $primary->decimal_places); + + if (TransactionTypeEnum::WITHDRAWAL->value !== $journal['transaction_type_type']) { + $amount = Steam::bcround(Steam::positive($journal['amount']), $journal['currency_decimal_places']); + $foreignAmount = null === $journal['foreign_amount'] ? null : Steam::bcround(Steam::positive($journal['foreign_amount']), $journal['foreign_currency_decimal_places']); + $pcAmount = null === $journal['pc_amount'] ? null : Steam::bcround(Steam::positive($journal['pc_amount']), $primary->decimal_places); + $pcForeignAmount = null === $journal['pc_foreign_amount'] ? null : Steam::bcround(Steam::positive($journal['pc_foreign_amount']), $primary->decimal_places); + } + + // opening balance depends on source account type. + if (TransactionTypeEnum::OPENING_BALANCE->value === $journal['transaction_type_type'] && AccountTypeEnum::ASSET->value === $journal['source_account_type']) { + $amount = Steam::bcround(Steam::negative($journal['amount']), $journal['currency_decimal_places']); + $foreignAmount = null === $journal['foreign_amount'] ? null : Steam::bcround(Steam::negative($journal['foreign_amount']), $journal['foreign_currency_decimal_places']); + $pcAmount = null === $journal['pc_amount'] ? null : Steam::bcround(Steam::negative($journal['pc_amount']), $primary->decimal_places); + $pcForeignAmount = null === $journal['pc_foreign_amount'] ? null : Steam::bcround(Steam::negative($journal['pc_foreign_amount']), $primary->decimal_places); + } + + $records[] = [ + $journal['user_id'], $journal['transaction_group_id'], $journal['transaction_journal_id'], $journal['created_at']->toAtomString(), $journal['updated_at']->toAtomString(), $journal['transaction_group_title'], $journal['transaction_type_type'], + // amounts and currencies + $journal['currency_code'], $amount, $journal['foreign_currency_code'], $foreignAmount, $primary->code, $pcAmount, $pcForeignAmount, + + // more fields + $journal['description'], $journal['date']->toAtomString(), $journal['source_account_name'], $journal['source_account_iban'], $journal['source_account_type'], $journal['destination_account_name'], $journal['destination_account_iban'], $journal['destination_account_type'], $journal['reconciled'], $journal['category_name'], $journal['budget_name'], $journal['bill_name'], + $this->mergeTags($journal['tags']), + $this->clearStringKeepNewlines($journal['notes']), + + // sepa + $metaData['sepa_cc'], $metaData['sepa_ct_op'], $metaData['sepa_ct_id'], $metaData['sepa_db'], $metaData['sepa_country'], $metaData['sepa_ep'], $metaData['sepa_ci'], $metaData['sepa_batch_id'], $metaData['external_url'], + + // dates + $metaData['interest_date'], $metaData['book_date'], $metaData['process_date'], $metaData['due_date'], $metaData['payment_date'], $metaData['invoice_date'], + + // others + $metaData['recurrence_id'], $metaData['internal_reference'], $metaData['bunq_payment_id'], $metaData['import_hash'], $metaData['import_hash_v2'], $metaData['external_id'], $metaData['original_source'], + + // recurring transactions + $metaData['recurrence_total'], $metaData['recurrence_count'], + ]; + } + + // load the CSV document from a string + $csv = Writer::createFromString(); + + // insert the header + try { + $csv->insertOne($header); + } catch (CannotInsertRecord $e) { + throw new FireflyException(sprintf(self::ADD_RECORD_ERR, $e->getMessage()), 0, $e); + } + + // insert all the records + $csv->insertAll($records); + + try { + $string = $csv->toString(); + } catch (Exception $e) { // intentional generic exception + app('log')->error($e->getMessage()); + + throw new FireflyException(sprintf(self::EXPORT_ERR, $e->getMessage()), 0, $e); + } + + return $string; + } + + private function mergeTags(array $tags): string + { + if (0 === count($tags)) { + return ''; + } + $smol = []; + foreach ($tags as $tag) { + $smol[] = $tag['name']; + } + + return implode(',', $smol); + } } diff --git a/app/Support/FireflyConfig.php b/app/Support/FireflyConfig.php index 499b123602..b79967f8cb 100644 --- a/app/Support/FireflyConfig.php +++ b/app/Support/FireflyConfig.php @@ -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. @@ -39,16 +39,43 @@ class FireflyConfig { public function delete(string $name): void { - $fullName = 'ff3-config-'.$name; + $fullName = 'ff3-config-' . $name; if (Cache::has($fullName)) { Cache::forget($fullName); } Configuration::where('name', $name)->forceDelete(); } - public function has(string $name): bool + /** + * @param null|bool|int|string $default + * + * @throws FireflyException + */ + public function get(string $name, mixed $default = null): ?Configuration { - return 1 === Configuration::where('name', $name)->count(); + $fullName = 'ff3-config-' . $name; + if (Cache::has($fullName)) { + return Cache::get($fullName); + } + + try { + /** @var null|Configuration $config */ + $config = Configuration::where('name', $name)->first(['id', 'name', 'data']); + } catch (Exception | QueryException $e) { + throw new FireflyException(sprintf('Could not poll the database: %s', $e->getMessage()), 0, $e); + } + + if (null !== $config) { + Cache::forever($fullName, $config); + + return $config; + } + // no preference found and default is null: + if (null === $default) { + return null; + } + + return $this->set($name, $default); } public function getEncrypted(string $name, mixed $default = null): ?Configuration @@ -74,28 +101,10 @@ class FireflyConfig return $result; } - /** - * @param null|bool|int|string $default - * - * @throws FireflyException - */ - public function get(string $name, mixed $default = null): ?Configuration + public function getFresh(string $name, mixed $default = null): ?Configuration { - $fullName = 'ff3-config-'.$name; - if (Cache::has($fullName)) { - return Cache::get($fullName); - } - - try { - /** @var null|Configuration $config */ - $config = Configuration::where('name', $name)->first(['id', 'name', 'data']); - } catch (Exception|QueryException $e) { - throw new FireflyException(sprintf('Could not poll the database: %s', $e->getMessage()), 0, $e); - } - + $config = Configuration::where('name', $name)->first(['id', 'name', 'data']); if (null !== $config) { - Cache::forever($fullName, $config); - return $config; } // no preference found and default is null: @@ -106,6 +115,19 @@ class FireflyConfig 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 { @@ -124,39 +146,17 @@ class FireflyConfig $item->name = $name; $item->data = $value; $item->save(); - Cache::forget('ff3-config-'.$name); + Cache::forget('ff3-config-' . $name); return $item; } $config->data = $value; $config->save(); - Cache::forget('ff3-config-'.$name); + Cache::forget('ff3-config-' . $name); 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 { diff --git a/app/Support/Form/AccountForm.php b/app/Support/Form/AccountForm.php index 19a043c76d..c7a7685061 100644 --- a/app/Support/Form/AccountForm.php +++ b/app/Support/Form/AccountForm.php @@ -51,55 +51,24 @@ 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. */ public function activeWithdrawalDestinations(string $name, mixed $value = null, ?array $options = null): string { - $types = [AccountTypeEnum::MORTGAGE->value, AccountTypeEnum::DEBT->value, AccountTypeEnum::CREDITCARD->value, AccountTypeEnum::LOAN->value, AccountTypeEnum::EXPENSE->value]; - $repository = $this->getAccountRepository(); - $grouped = $this->getAccountsGrouped($types, $repository); + $types = [AccountTypeEnum::MORTGAGE->value, AccountTypeEnum::DEBT->value, AccountTypeEnum::CREDITCARD->value, AccountTypeEnum::LOAN->value, AccountTypeEnum::EXPENSE->value]; + $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); } @@ -111,15 +80,15 @@ class AccountForm */ public function assetAccountCheckList(string $name, ?array $options = null): string { - $options ??= []; + $options ??= []; $label = $this->label($name, $options); $options = $this->expandOptionArray($name, $label, $options); $classes = $this->getHolderClasses($name); $selected = request()->old($name) ?? []; // get all asset accounts: - $types = [AccountTypeEnum::ASSET->value, AccountTypeEnum::DEFAULT->value, AccountTypeEnum::LOAN->value, AccountTypeEnum::MORTGAGE->value, AccountTypeEnum::DEBT->value]; - $grouped = $this->getAccountsGrouped($types); + $types = [AccountTypeEnum::ASSET->value, AccountTypeEnum::DEFAULT->value, AccountTypeEnum::LOAN->value, AccountTypeEnum::MORTGAGE->value, AccountTypeEnum::DEBT->value]; + $grouped = $this->getAccountsGrouped($types); unset($options['class']); @@ -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; + } } diff --git a/app/Support/Form/CurrencyForm.php b/app/Support/Form/CurrencyForm.php index 88816667d4..6d24780f19 100644 --- a/app/Support/Form/CurrencyForm.php +++ b/app/Support/Form/CurrencyForm.php @@ -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,6 +61,52 @@ class CurrencyForm return $this->allCurrencyField($name, 'balance', $value, $options); } + /** + * TODO cleanup and describe + * + * @param mixed $value + */ + public function currencyList(string $name, $value = null, ?array $options = null): string + { + /** @var CurrencyRepositoryInterface $currencyRepos */ + $currencyRepos = app(CurrencyRepositoryInterface::class); + + // get all currencies: + $list = $currencyRepos->get(); + $array = []; + + /** @var TransactionCurrency $currency */ + foreach ($list as $currency) { + $array[$currency->id] = $currency->name . ' (' . $currency->symbol . ')'; + } + + return $this->select($name, $array, $value, $options); + } + + /** + * TODO cleanup and describe + * + * @param mixed $value + */ + public function currencyListEmpty(string $name, $value = null, ?array $options = null): string + { + /** @var CurrencyRepositoryInterface $currencyRepos */ + $currencyRepos = app(CurrencyRepositoryInterface::class); + + // get all currencies: + $list = $currencyRepos->get(); + $array = [ + 0 => (string)trans('firefly.no_currency'), + ]; + + /** @var TransactionCurrency $currency */ + foreach ($list as $currency) { + $array[$currency->id] = $currency->name . ' (' . $currency->symbol . ')'; + } + + return $this->select($name, $array, $value, $options); + } + /** * TODO describe and cleanup * @@ -132,16 +124,16 @@ class CurrencyForm $primaryCurrency = $options['currency'] ?? app('amount')->getPrimaryCurrency(); /** @var Collection $currencies */ - $currencies = app('amount')->getAllCurrencies(); + $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'); + $preFilled = session('preFilled'); if (!is_array($preFilled)) { $preFilled = []; } - $key = 'amount_currency_id_'.$name; - $sentCurrencyId = array_key_exists($key, $preFilled) ? (int) $preFilled[$key] : $primaryCurrency->id; + $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)); @@ -161,7 +153,7 @@ class CurrencyForm } try { - $html = view('form.'.$view, compact('primaryCurrency', 'currencies', 'classes', 'name', 'label', 'value', 'options'))->render(); + $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.'; @@ -173,48 +165,56 @@ class CurrencyForm } /** - * TODO cleanup and describe + * @phpstan-param view-string $view * - * @param mixed $value + * @throws FireflyException */ - public function currencyList(string $name, $value = null, ?array $options = null): string + protected function currencyField(string $name, string $view, mixed $value = null, ?array $options = null): string { - /** @var CurrencyRepositoryInterface $currencyRepos */ - $currencyRepos = app(CurrencyRepositoryInterface::class); + $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(); - // get all currencies: - $list = $currencyRepos->get(); - $array = []; + /** @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; - /** @var TransactionCurrency $currency */ - foreach ($list as $currency) { - $array[$currency->id] = $currency->name.' ('.$currency->symbol.')'; + 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; + } } - return $this->select($name, $array, $value, $options); - } - - /** - * TODO cleanup and describe - * - * @param mixed $value - */ - public function currencyListEmpty(string $name, $value = null, ?array $options = null): string - { - /** @var CurrencyRepositoryInterface $currencyRepos */ - $currencyRepos = app(CurrencyRepositoryInterface::class); - - // get all currencies: - $list = $currencyRepos->get(); - $array = [ - 0 => (string) trans('firefly.no_currency'), - ]; - - /** @var TransactionCurrency $currency */ - foreach ($list as $currency) { - $array[$currency->id] = $currency->name.' ('.$currency->symbol.')'; + // make sure value is formatted nicely: + if (null !== $value && '' !== $value) { + $value = app('steam')->bcround($value, $primaryCurrency->decimal_places); } - return $this->select($name, $array, $value, $options); + 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; } } diff --git a/app/Support/Form/FormSupport.php b/app/Support/Form/FormSupport.php index 4bcc0fcb87..e41c785f00 100644 --- a/app/Support/Form/FormSupport.php +++ b/app/Support/Form/FormSupport.php @@ -36,7 +36,7 @@ trait FormSupport { public function multiSelect(string $name, ?array $list = null, mixed $selected = null, ?array $options = null): string { - $list ??= []; + $list ??= []; $label = $this->label($name, $options); $options = $this->expandOptionArray($name, $label, $options); $classes = $this->getHolderClasses($name); @@ -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; } /** @@ -70,29 +81,16 @@ trait FormSupport */ protected function expandOptionArray(string $name, $label, ?array $options = null): array { - $options ??= []; + $options ??= []; $name = str_replace('[]', '', $name); $options['class'] = 'form-control'; - $options['id'] = 'ffInput_'.$name; + $options['id'] = 'ffInput_' . $name; $options['autocomplete'] = 'off'; $options['placeholder'] = ucfirst((string)$label); 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); + } } diff --git a/app/Support/Form/PiggyBankForm.php b/app/Support/Form/PiggyBankForm.php index 78919b30bf..b6233fd0d6 100644 --- a/app/Support/Form/PiggyBankForm.php +++ b/app/Support/Form/PiggyBankForm.php @@ -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,21 +55,21 @@ class PiggyBankForm 'title' => $title, ], 'piggies' => [ - (string) trans('firefly.none_in_select_list'), + (string)trans('firefly.none_in_select_list'), ], ], ]; /** @var PiggyBank $piggy */ foreach ($piggyBanks as $piggy) { - $group = $piggy->objectGroups->first(); - $groupTitle = null; - $groupOrder = 0; + $group = $piggy->objectGroups->first(); + $groupTitle = null; + $groupOrder = 0; if (null !== $group) { $groupTitle = $group->title; $groupOrder = $group->order; } - $subList[$groupOrder] ??= [ + $subList[$groupOrder] ??= [ 'group' => [ 'title' => $groupTitle, ], diff --git a/app/Support/Form/RuleForm.php b/app/Support/Form/RuleForm.php index 6baee553be..f635ed9b86 100644 --- a/app/Support/Form/RuleForm.php +++ b/app/Support/Form/RuleForm.php @@ -41,8 +41,8 @@ class RuleForm $groupRepos = app(RuleGroupRepositoryInterface::class); // get all currencies: - $list = $groupRepos->get(); - $array = []; + $list = $groupRepos->get(); + $array = []; /** @var RuleGroup $group */ foreach ($list as $group) { @@ -57,21 +57,21 @@ class RuleForm */ public function ruleGroupListWithEmpty(string $name, $value = null, ?array $options = null): string { - $options ??= []; + $options ??= []; $options['class'] = 'form-control'; /** @var RuleGroupRepositoryInterface $groupRepos */ - $groupRepos = app(RuleGroupRepositoryInterface::class); + $groupRepos = app(RuleGroupRepositoryInterface::class); // get all currencies: - $list = $groupRepos->get(); - $array = [ - 0 => (string) trans('firefly.none_in_select_list'), + $list = $groupRepos->get(); + $array = [ + 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; } } diff --git a/app/Support/Http/Api/AccountBalanceGrouped.php b/app/Support/Http/Api/AccountBalanceGrouped.php index d1f7915d23..4c60e00870 100644 --- a/app/Support/Http/Api/AccountBalanceGrouped.php +++ b/app/Support/Http/Api/AccountBalanceGrouped.php @@ -146,114 +146,6 @@ class AccountBalanceGrouped $converter->summarize(); } - private function processJournal(array $journal): 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); - } - - 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 createDefaultDataEntry(array $journal): void - { - $currencyId = (int)$journal['currency_id']; - $this->data[$currencyId] ??= [ - 'currency_id' => (string)$currencyId, - 'currency_symbol' => $journal['currency_symbol'], - 'currency_code' => $journal['currency_code'], - 'currency_name' => $journal['currency_name'], - 'currency_decimal_places' => $journal['currency_decimal_places'], - // primary currency info (could be the same) - 'primary_currency_id' => (string)$this->primary->id, - 'primary_currency_code' => $this->primary->code, - 'primary_currency_symbol' => $this->primary->symbol, - 'primary_currency_decimal_places' => $this->primary->decimal_places, - ]; - } - - private function createDefaultPeriodEntry(array $journal): void - { - $currencyId = (int)$journal['currency_id']; - $period = $journal['date']->format($this->carbonFormat); - $this->data[$currencyId][$period] ??= [ - 'period' => $period, - 'spent' => '0', - 'earned' => '0', - 'pc_spent' => '0', - 'pc_earned' => '0', - ]; - } - - private function getDataKey(array $journal): string - { - // deposit = incoming - // transfer or reconcile or opening balance, and these accounts are the destination. - if ( - TransactionTypeEnum::DEPOSIT->value === $journal['transaction_type_type'] - - || ( - ( - TransactionTypeEnum::TRANSFER->value === $journal['transaction_type_type'] - || TransactionTypeEnum::RECONCILIATION->value === $journal['transaction_type_type'] - || TransactionTypeEnum::OPENING_BALANCE->value === $journal['transaction_type_type'] - ) - && in_array($journal['destination_account_id'], $this->accountIds, true) - ) - ) { - return 'earned'; - } - - return 'spent'; - } - - private function getRate(TransactionCurrency $currency, Carbon $date): string - { - try { - $rate = $this->converter->getCurrencyRate($currency, $this->primary, $date); - } catch (FireflyException $e) { - app('log')->error($e->getMessage()); - $rate = '1'; - } - - return $rate; - } - public function setAccounts(Collection $accounts): void { $this->accountIds = $accounts->pluck('id')->toArray(); @@ -298,4 +190,112 @@ class AccountBalanceGrouped { $this->start = $start; } + + private function createDefaultDataEntry(array $journal): void + { + $currencyId = (int)$journal['currency_id']; + $this->data[$currencyId] ??= [ + 'currency_id' => (string)$currencyId, + 'currency_symbol' => $journal['currency_symbol'], + 'currency_code' => $journal['currency_code'], + 'currency_name' => $journal['currency_name'], + 'currency_decimal_places' => $journal['currency_decimal_places'], + // primary currency info (could be the same) + 'primary_currency_id' => (string)$this->primary->id, + 'primary_currency_code' => $this->primary->code, + 'primary_currency_symbol' => $this->primary->symbol, + 'primary_currency_decimal_places' => $this->primary->decimal_places, + ]; + } + + private function createDefaultPeriodEntry(array $journal): void + { + $currencyId = (int)$journal['currency_id']; + $period = $journal['date']->format($this->carbonFormat); + $this->data[$currencyId][$period] ??= [ + 'period' => $period, + 'spent' => '0', + 'earned' => '0', + 'pc_spent' => '0', + 'pc_earned' => '0', + ]; + } + + 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 + // transfer or reconcile or opening balance, and these accounts are the destination. + if ( + TransactionTypeEnum::DEPOSIT->value === $journal['transaction_type_type'] + + || ( + ( + TransactionTypeEnum::TRANSFER->value === $journal['transaction_type_type'] + || TransactionTypeEnum::RECONCILIATION->value === $journal['transaction_type_type'] + || TransactionTypeEnum::OPENING_BALANCE->value === $journal['transaction_type_type'] + ) + && in_array($journal['destination_account_id'], $this->accountIds, true) + ) + ) { + return 'earned'; + } + + return 'spent'; + } + + private function getRate(TransactionCurrency $currency, Carbon $date): string + { + try { + $rate = $this->converter->getCurrencyRate($currency, $this->primary, $date); + } catch (FireflyException $e) { + app('log')->error($e->getMessage()); + $rate = '1'; + } + + return $rate; + } + + private function processJournal(array $journal): 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); + } } diff --git a/app/Support/Http/Api/CleansChartData.php b/app/Support/Http/Api/CleansChartData.php index a3d54974cf..aef2ec443f 100644 --- a/app/Support/Http/Api/CleansChartData.php +++ b/app/Support/Http/Api/CleansChartData.php @@ -43,7 +43,7 @@ trait CleansChartData $return = []; /** - * @var int $index + * @var int $index * @var array $array */ foreach ($data as $index => $array) { diff --git a/app/Support/Http/Api/ExchangeRateConverter.php b/app/Support/Http/Api/ExchangeRateConverter.php index c78b7afce8..84d57a4649 100644 --- a/app/Support/Http/Api/ExchangeRateConverter.php +++ b/app/Support/Http/Api/ExchangeRateConverter.php @@ -94,57 +94,22 @@ class ExchangeRateConverter return '0' === $rate ? '1' : $rate; } - /** - * @throws FireflyException - */ - private function getRate(TransactionCurrency $from, TransactionCurrency $to, Carbon $date): string + public function setIgnoreSettings(bool $ignoreSettings): void { - $key = $this->getCacheKey($from, $to, $date); - $res = Cache::get($key, null); + $this->ignoreSettings = $ignoreSettings; + } - // find in cache - if (null !== $res) { - Log::debug(sprintf('ExchangeRateConverter: Return cached rate (%s) from %s to %s on %s.', $res, $from->code, $to->code, $date->format('Y-m-d'))); + public function setUserGroup(UserGroup $userGroup): void + { + $this->userGroup = $userGroup; + } - return $res; + public function summarize(): void + { + if (false === $this->enabled()) { + return; } - - // find in database - $rate = $this->getFromDB($from->id, $to->id, $date->format('Y-m-d')); - if (null !== $rate) { - Cache::forever($key, $rate); - Log::debug(sprintf('ExchangeRateConverter: Return DB rate from %s to %s on %s.', $from->code, $to->code, $date->format('Y-m-d'))); - - return $rate; - } - - // find reverse in database - $rate = $this->getFromDB($to->id, $from->id, $date->format('Y-m-d')); - if (null !== $rate) { - $rate = bcdiv('1', $rate); - Cache::forever($key, $rate); - Log::debug(sprintf('ExchangeRateConverter: Return inverse DB rate from %s to %s on %s.', $from->code, $to->code, $date->format('Y-m-d'))); - - return $rate; - } - - // fallback scenario. - $first = $this->getEuroRate($from, $date); - $second = $this->getEuroRate($to, $date); - - // combined (if present), they can be used to calculate the necessary conversion rate. - if (0 === bccomp('0', $first) || 0 === bccomp('0', $second)) { - Log::warning(sprintf('There is not enough information to convert %s to %s on date %s', $from->code, $to->code, $date->format('Y-m-d'))); - - return '1'; - } - - $second = bcdiv('1', $second); - $rate = bcmul($first, $second); - Log::debug(sprintf('ExchangeRateConverter: Return DB rate from %s to %s on %s.', $from->code, $to->code, $date->format('Y-m-d'))); - Cache::forever($key, $rate); - - return $rate; + Log::debug(sprintf('ExchangeRateConverter ran %d queries.', $this->queryCount)); } private function getCacheKey(TransactionCurrency $from, TransactionCurrency $to, Carbon $date): string @@ -152,6 +117,57 @@ class ExchangeRateConverter 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) { @@ -223,69 +239,53 @@ class ExchangeRateConverter /** * @throws FireflyException */ - private function getEuroRate(TransactionCurrency $currency, Carbon $date): string + private function getRate(TransactionCurrency $from, TransactionCurrency $to, Carbon $date): string { - $euroId = $this->getEuroId(); - if ($euroId === $currency->id) { - return '1'; - } - $rate = $this->getFromDB($currency->id, $euroId, $date->format('Y-m-d')); + $key = $this->getCacheKey($from, $to, $date); + $res = Cache::get($key, null); + // find in cache + if (null !== $res) { + Log::debug(sprintf('ExchangeRateConverter: Return cached rate (%s) from %s to %s on %s.', $res, $from->code, $to->code, $date->format('Y-m-d'))); + + return $res; + } + + // find in database + $rate = $this->getFromDB($from->id, $to->id, $date->format('Y-m-d')); if (null !== $rate) { - // app('log')->debug(sprintf('Rate for %s to EUR is %s.', $currency->code, $rate)); + Cache::forever($key, $rate); + Log::debug(sprintf('ExchangeRateConverter: Return DB rate from %s to %s on %s.', $from->code, $to->code, $date->format('Y-m-d'))); + return $rate; } - $rate = $this->getFromDB($euroId, $currency->id, $date->format('Y-m-d')); + + // find reverse in database + $rate = $this->getFromDB($to->id, $from->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; + $rate = bcdiv('1', $rate); + Cache::forever($key, $rate); + Log::debug(sprintf('ExchangeRateConverter: Return inverse DB rate from %s to %s on %s.', $from->code, $to->code, $date->format('Y-m-d'))); + + return $rate; } - // app('log')->debug(sprintf('No rate for %s to EUR.', $currency->code)); - return '0'; - } + // fallback scenario. + $first = $this->getEuroRate($from, $date); + $second = $this->getEuroRate($to, $date); - /** - * @throws FireflyException - */ - private function getEuroId(): int - { - Log::debug('getEuroId()'); - $cache = new CacheProperties(); - $cache->addProperty('cer-euro-id'); - if ($cache->has()) { - return (int)$cache->get(); + // combined (if present), they can be used to calculate the necessary conversion rate. + if (0 === bccomp('0', $first) || 0 === bccomp('0', $second)) { + Log::warning(sprintf('There is not enough information to convert %s to %s on date %s', $from->code, $to->code, $date->format('Y-m-d'))); + + return '1'; } - $euro = Amount::getTransactionCurrencyByCode('EUR'); - ++$this->queryCount; - $cache->store($euro->id); - return $euro->id; - } + $second = bcdiv('1', $second); + $rate = bcmul($first, $second); + Log::debug(sprintf('ExchangeRateConverter: Return DB rate from %s to %s on %s.', $from->code, $to->code, $date->format('Y-m-d'))); + Cache::forever($key, $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)); + return $rate; } } diff --git a/app/Support/Http/Controllers/PeriodOverview.php b/app/Support/Http/Controllers/PeriodOverview.php index 7371b997bd..1dda34bf8d 100644 --- a/app/Support/Http/Controllers/PeriodOverview.php +++ b/app/Support/Http/Controllers/PeriodOverview.php @@ -38,6 +38,7 @@ use FireflyIII\Repositories\PeriodStatistic\PeriodStatisticRepositoryInterface; use FireflyIII\Support\CacheProperties; use FireflyIII\Support\Facades\Amount; use FireflyIII\Support\Facades\Navigation; +use Illuminate\Support\Collection; use Illuminate\Support\Facades\Log; /** @@ -72,6 +73,7 @@ trait PeriodOverview protected AccountRepositoryInterface $accountRepository; protected JournalRepositoryInterface $journalRepos; protected PeriodStatisticRepositoryInterface $periodStatisticRepo; + private Collection $statistics; /** * This method returns "period entries", so nov-2015, dec-2015, etc. (this depends on the users session range) @@ -82,30 +84,231 @@ trait PeriodOverview */ protected function getAccountPeriodOverview(Account $account, Carbon $start, Carbon $end): array { - Log::debug(sprintf('Now in getAccountPeriodOverview(#%d, %s %s)', $account->id, $start->format('Y-m-d'), $end->format('Y-m-d'))); + 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]; + $this->statistics = $this->periodStatisticRepo->allInRangeForModel($account, $start, $end); + + // 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. + /** @var array $dates */ $dates = Navigation::blockPeriods($start, $end, $range); $entries = []; + $types = ['spent', 'earned', 'transferred_in', 'transferred_away']; Log::debug(sprintf('Count of loops: %d', count($dates))); foreach ($dates as $currentDate) { - $entries[] = $this->getSingleAccountPeriod($account, $currentDate['start'], $currentDate['end']); + $entries[] = $this->getSingleAccountPeriod($account, $currentDate['period'], $currentDate['start'], $currentDate['end']); } Log::debug('End of getAccountPeriodOverview()'); return $entries; } - protected function getSingleAccountPeriod(Account $account, Carbon $start, Carbon $end): array + /** + * Overview for single category. Has been refactored recently. + * + * @throws FireflyException + */ + protected function getCategoryPeriodOverview(Category $category, Carbon $start, Carbon $end): array + { + $range = Navigation::getViewRange(true); + [$start, $end] = $end < $start ? [$end, $start] : [$start, $end]; + + // properties for entries with their amounts. + $cache = new CacheProperties(); + $cache->addProperty($start); + $cache->addProperty($end); + $cache->addProperty($range); + $cache->addProperty('category-show-period-entries'); + $cache->addProperty($category->id); + + if ($cache->has()) { + return $cache->get(); + } + + /** @var array $dates */ + $dates = Navigation::blockPeriods($start, $end, $range); + $entries = []; + + // collect all expenses in this period: + /** @var GroupCollectorInterface $collector */ + $collector = app(GroupCollectorInterface::class); + $collector->setCategory($category); + $collector->setRange($start, $end); + $collector->setTypes([TransactionTypeEnum::DEPOSIT->value]); + $earnedSet = $collector->getExtractedJournals(); + + // collect all income in this period: + /** @var GroupCollectorInterface $collector */ + $collector = app(GroupCollectorInterface::class); + $collector->setCategory($category); + $collector->setRange($start, $end); + $collector->setTypes([TransactionTypeEnum::WITHDRAWAL->value]); + $spentSet = $collector->getExtractedJournals(); + + // collect all transfers in this period: + /** @var GroupCollectorInterface $collector */ + $collector = app(GroupCollectorInterface::class); + $collector->setCategory($category); + $collector->setRange($start, $end); + $collector->setTypes([TransactionTypeEnum::TRANSFER->value]); + $transferSet = $collector->getExtractedJournals(); + foreach ($dates as $currentDate) { + $spent = $this->filterJournalsByDate($spentSet, $currentDate['start'], $currentDate['end']); + $earned = $this->filterJournalsByDate($earnedSet, $currentDate['start'], $currentDate['end']); + $transferred = $this->filterJournalsByDate($transferSet, $currentDate['start'], $currentDate['end']); + $title = Navigation::periodShow($currentDate['end'], $currentDate['period']); + $entries[] + = [ + 'transactions' => 0, + 'title' => $title, + 'route' => route( + 'categories.show', + [$category->id, $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), + ]; + } + $cache->store($entries); + + return $entries; + } + + /** + * Same as above, but for lists that involve transactions without a budget. + * + * This method has been refactored recently. + * + * @throws FireflyException + */ + protected function getNoBudgetPeriodOverview(Carbon $start, Carbon $end): array + { + $range = Navigation::getViewRange(true); + + [$start, $end] = $end < $start ? [$end, $start] : [$start, $end]; + + $cache = new CacheProperties(); + $cache->addProperty($start); + $cache->addProperty($end); + $cache->addProperty($this->convertToPrimary); + $cache->addProperty('no-budget-period-entries'); + + if ($cache->has()) { + return $cache->get(); + } + + /** @var array $dates */ + $dates = Navigation::blockPeriods($start, $end, $range); + $entries = []; + + // get all expenses without a budget. + /** @var GroupCollectorInterface $collector */ + $collector = app(GroupCollectorInterface::class); + $collector->setRange($start, $end)->withoutBudget()->withAccountInformation()->setTypes([TransactionTypeEnum::WITHDRAWAL->value]); + $journals = $collector->getExtractedJournals(); + + foreach ($dates as $currentDate) { + $set = $this->filterJournalsByDate($journals, $currentDate['start'], $currentDate['end']); + $title = Navigation::periodShow($currentDate['end'], $currentDate['period']); + $entries[] + = [ + 'title' => $title, + 'route' => route('budgets.no-budget', [$currentDate['start']->format('Y-m-d'), $currentDate['end']->format('Y-m-d')]), + 'total_transactions' => count($set), + 'spent' => $this->groupByCurrency($set), + 'earned' => [], + 'transferred_away' => [], + 'transferred_in' => [], + ]; + } + $cache->store($entries); + + return $entries; + } + + /** + * TODO fix the date. + * + * Show period overview for no category view. + * + * @throws FireflyException + */ + protected function getNoCategoryPeriodOverview(Carbon $theDate): array + { + 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); + + 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); + $entries = []; + + // collect all expenses in this period: + /** @var GroupCollectorInterface $collector */ + $collector = app(GroupCollectorInterface::class); + $collector->withoutCategory(); + $collector->setRange($start, $end); + $collector->setTypes([TransactionTypeEnum::DEPOSIT->value]); + $earnedSet = $collector->getExtractedJournals(); + + // collect all income in this period: + /** @var GroupCollectorInterface $collector */ + $collector = app(GroupCollectorInterface::class); + $collector->withoutCategory(); + $collector->setRange($start, $end); + $collector->setTypes([TransactionTypeEnum::WITHDRAWAL->value]); + $spentSet = $collector->getExtractedJournals(); + + // collect all transfers in this period: + /** @var GroupCollectorInterface $collector */ + $collector = app(GroupCollectorInterface::class); + $collector->withoutCategory(); + $collector->setRange($start, $end); + $collector->setTypes([TransactionTypeEnum::TRANSFER->value]); + $transferSet = $collector->getExtractedJournals(); + + /** @var array $currentDate */ + foreach ($dates as $currentDate) { + $spent = $this->filterJournalsByDate($spentSet, $currentDate['start'], $currentDate['end']); + $earned = $this->filterJournalsByDate($earnedSet, $currentDate['start'], $currentDate['end']); + $transferred = $this->filterJournalsByDate($transferSet, $currentDate['start'], $currentDate['end']); + $title = Navigation::periodShow($currentDate['end'], $currentDate['period']); + $entries[] + = [ + 'title' => $title, + 'route' => route('categories.no-category', [$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), + ]; + } + 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, $end), + 'title' => Navigation::periodShow($start, $period), 'route' => route('accounts.show', [$account->id, $start->format('Y-m-d'), $start->format('Y-m-d')]), 'total_transactions' => 0, ]; @@ -119,13 +322,34 @@ trait PeriodOverview 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->periodStatisticRepo->findPeriodStatistic($account, $start, $end, $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)); $transactions = $this->accountRepository->periodCollection($account, $start, $end); switch ($type) { @@ -183,12 +407,195 @@ trait PeriodOverview return $grouped; } + /** + * This shows a period overview for a tag. It goes back in time and lists all relevant transactions and sums. + * + * @throws FireflyException + */ + protected function getTagPeriodOverview(Tag $tag, Carbon $start, Carbon $end): array // period overview for tags. + { + $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('tag-period-entries'); + $cache->addProperty($tag->id); + if ($cache->has()) { + return $cache->get(); + } + + /** @var array $dates */ + $dates = Navigation::blockPeriods($start, $end, $range); + $entries = []; + + // collect all expenses in this period: + /** @var GroupCollectorInterface $collector */ + $collector = app(GroupCollectorInterface::class); + $collector->setTag($tag); + $collector->setRange($start, $end); + $collector->setTypes([TransactionTypeEnum::DEPOSIT->value]); + $earnedSet = $collector->getExtractedJournals(); + + // collect all income in this period: + /** @var GroupCollectorInterface $collector */ + $collector = app(GroupCollectorInterface::class); + $collector->setTag($tag); + $collector->setRange($start, $end); + $collector->setTypes([TransactionTypeEnum::WITHDRAWAL->value]); + $spentSet = $collector->getExtractedJournals(); + + // collect all transfers in this period: + /** @var GroupCollectorInterface $collector */ + $collector = app(GroupCollectorInterface::class); + $collector->setTag($tag); + $collector->setRange($start, $end); + $collector->setTypes([TransactionTypeEnum::TRANSFER->value]); + $transferSet = $collector->getExtractedJournals(); + + // filer all of them: + $earnedSet = $this->filterJournalsByTag($earnedSet, $tag); + $spentSet = $this->filterJournalsByTag($spentSet, $tag); + $transferSet = $this->filterJournalsByTag($transferSet, $tag); + + foreach ($dates as $currentDate) { + $spent = $this->filterJournalsByDate($spentSet, $currentDate['start'], $currentDate['end']); + $earned = $this->filterJournalsByDate($earnedSet, $currentDate['start'], $currentDate['end']); + $transferred = $this->filterJournalsByDate($transferSet, $currentDate['start'], $currentDate['end']); + $title = Navigation::periodShow($currentDate['end'], $currentDate['period']); + $entries[] + = [ + 'transactions' => 0, + 'title' => $title, + 'route' => route( + 'tags.show', + [$tag->id, $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), + ]; + } + + return $entries; + } + + /** + * @throws FireflyException + */ + protected function getTransactionPeriodOverview(string $transactionType, Carbon $start, Carbon $end): array + { + $range = Navigation::getViewRange(true); + $types = config(sprintf('firefly.transactionTypesByType.%s', $transactionType)); + [$start, $end] = $end < $start ? [$end, $start] : [$start, $end]; + + // properties for cache + $cache = new CacheProperties(); + $cache->addProperty($start); + $cache->addProperty($end); + $cache->addProperty('transactions-period-entries'); + $cache->addProperty($transactionType); + if ($cache->has()) { + return $cache->get(); + } + + /** @var array $dates */ + $dates = Navigation::blockPeriods($start, $end, $range); + $entries = []; + $spent = []; + $earned = []; + $transferred = []; + // collect all journals in this period (regardless of type) + $collector = app(GroupCollectorInterface::class); + $collector->setTypes($types)->setRange($start, $end); + $genericSet = $collector->getExtractedJournals(); + $loops = 0; + + foreach ($dates as $currentDate) { + $title = Navigation::periodShow($currentDate['end'], $currentDate['period']); + + if ($loops < 10) { + // set to correct array + if ('expenses' === $transactionType || 'withdrawal' === $transactionType) { + $spent = $this->filterJournalsByDate($genericSet, $currentDate['start'], $currentDate['end']); + } + if ('revenue' === $transactionType || 'deposit' === $transactionType) { + $earned = $this->filterJournalsByDate($genericSet, $currentDate['start'], $currentDate['end']); + } + if ('transfer' === $transactionType || 'transfers' === $transactionType) { + $transferred = $this->filterJournalsByDate($genericSet, $currentDate['start'], $currentDate['end']); + } + } + $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), + ]; + ++$loops; + } + + return $entries; + } + + protected function saveGroupedAsStatistics(Account $account, Carbon $start, Carbon $end, string $type, array $array): void + { + unset($array['count']); + foreach ($array as $entry) { + $this->periodStatisticRepo->saveStatistic($account, $entry['currency_id'], $start, $end, $type, $entry['count'], $entry['amount']); + } + } + + /** + * 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, array $transactions, Carbon $start, Carbon $end): array { $result = []; /** - * @var int $index + * @var int $index * @var array $item */ foreach ($transactions as $index => $item) { @@ -203,12 +610,46 @@ trait PeriodOverview return $result; } + /** + * Return only transactions where $account is the source. + */ + private function filterTransferredAway(Account $account, array $journals): array + { + $return = []; + + /** @var array $journal */ + foreach ($journals as $journal) { + if ($account->id === (int)$journal['source_account_id']) { + $return[] = $journal; + } + } + + return $return; + } + + /** + * Return only transactions where $account is the source. + */ + private function filterTransferredIn(Account $account, array $journals): array + { + $return = []; + + /** @var array $journal */ + foreach ($journals as $journal) { + if ($account->id === (int)$journal['destination_account_id']) { + $return[] = $journal; + } + } + + return $return; + } + private function filterTransfers(string $direction, array $transactions, Carbon $start, Carbon $end): array { $result = []; /** - * @var int $index + * @var int $index * @var array $item */ foreach ($transactions as $index => $item) { @@ -289,414 +730,4 @@ trait PeriodOverview return $return; } - - protected function saveGroupedAsStatistics(Account $account, Carbon $start, Carbon $end, string $type, array $array): void - { - unset($array['count']); - foreach ($array as $entry) { - $this->periodStatisticRepo->saveStatistic($account, $entry['currency_id'], $start, $end, $type, $entry['count'], $entry['amount']); - } - } - - /** - * Overview for single category. Has been refactored recently. - * - * @throws FireflyException - */ - protected function getCategoryPeriodOverview(Category $category, Carbon $start, Carbon $end): array - { - $range = Navigation::getViewRange(true); - [$start, $end] = $end < $start ? [$end, $start] : [$start, $end]; - - // properties for entries with their amounts. - $cache = new CacheProperties(); - $cache->addProperty($start); - $cache->addProperty($end); - $cache->addProperty($range); - $cache->addProperty('category-show-period-entries'); - $cache->addProperty($category->id); - - if ($cache->has()) { - return $cache->get(); - } - - /** @var array $dates */ - $dates = Navigation::blockPeriods($start, $end, $range); - $entries = []; - - // collect all expenses in this period: - /** @var GroupCollectorInterface $collector */ - $collector = app(GroupCollectorInterface::class); - $collector->setCategory($category); - $collector->setRange($start, $end); - $collector->setTypes([TransactionTypeEnum::DEPOSIT->value]); - $earnedSet = $collector->getExtractedJournals(); - - // collect all income in this period: - /** @var GroupCollectorInterface $collector */ - $collector = app(GroupCollectorInterface::class); - $collector->setCategory($category); - $collector->setRange($start, $end); - $collector->setTypes([TransactionTypeEnum::WITHDRAWAL->value]); - $spentSet = $collector->getExtractedJournals(); - - // collect all transfers in this period: - /** @var GroupCollectorInterface $collector */ - $collector = app(GroupCollectorInterface::class); - $collector->setCategory($category); - $collector->setRange($start, $end); - $collector->setTypes([TransactionTypeEnum::TRANSFER->value]); - $transferSet = $collector->getExtractedJournals(); - foreach ($dates as $currentDate) { - $spent = $this->filterJournalsByDate($spentSet, $currentDate['start'], $currentDate['end']); - $earned = $this->filterJournalsByDate($earnedSet, $currentDate['start'], $currentDate['end']); - $transferred = $this->filterJournalsByDate($transferSet, $currentDate['start'], $currentDate['end']); - $title = Navigation::periodShow($currentDate['end'], $currentDate['period']); - $entries[] - = [ - 'transactions' => 0, - 'title' => $title, - 'route' => route( - 'categories.show', - [$category->id, $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), - ]; - } - $cache->store($entries); - - 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. - * - * This method has been refactored recently. - * - * @throws FireflyException - */ - protected function getNoBudgetPeriodOverview(Carbon $start, Carbon $end): array - { - $range = Navigation::getViewRange(true); - - [$start, $end] = $end < $start ? [$end, $start] : [$start, $end]; - - $cache = new CacheProperties(); - $cache->addProperty($start); - $cache->addProperty($end); - $cache->addProperty($this->convertToPrimary); - $cache->addProperty('no-budget-period-entries'); - - if ($cache->has()) { - return $cache->get(); - } - - /** @var array $dates */ - $dates = Navigation::blockPeriods($start, $end, $range); - $entries = []; - - // get all expenses without a budget. - /** @var GroupCollectorInterface $collector */ - $collector = app(GroupCollectorInterface::class); - $collector->setRange($start, $end)->withoutBudget()->withAccountInformation()->setTypes([TransactionTypeEnum::WITHDRAWAL->value]); - $journals = $collector->getExtractedJournals(); - - foreach ($dates as $currentDate) { - $set = $this->filterJournalsByDate($journals, $currentDate['start'], $currentDate['end']); - $title = Navigation::periodShow($currentDate['end'], $currentDate['period']); - $entries[] - = [ - 'title' => $title, - 'route' => route('budgets.no-budget', [$currentDate['start']->format('Y-m-d'), $currentDate['end']->format('Y-m-d')]), - 'total_transactions' => count($set), - 'spent' => $this->groupByCurrency($set), - 'earned' => [], - 'transferred_away' => [], - 'transferred_in' => [], - ]; - } - $cache->store($entries); - - return $entries; - } - - /** - * TODO fix the date. - * - * Show period overview for no category view. - * - * @throws FireflyException - */ - protected function getNoCategoryPeriodOverview(Carbon $theDate): array - { - app('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'))); - - // properties for cache - $dates = Navigation::blockPeriods($start, $end, $range); - $entries = []; - - // collect all expenses in this period: - /** @var GroupCollectorInterface $collector */ - $collector = app(GroupCollectorInterface::class); - $collector->withoutCategory(); - $collector->setRange($start, $end); - $collector->setTypes([TransactionTypeEnum::DEPOSIT->value]); - $earnedSet = $collector->getExtractedJournals(); - - // collect all income in this period: - /** @var GroupCollectorInterface $collector */ - $collector = app(GroupCollectorInterface::class); - $collector->withoutCategory(); - $collector->setRange($start, $end); - $collector->setTypes([TransactionTypeEnum::WITHDRAWAL->value]); - $spentSet = $collector->getExtractedJournals(); - - // collect all transfers in this period: - /** @var GroupCollectorInterface $collector */ - $collector = app(GroupCollectorInterface::class); - $collector->withoutCategory(); - $collector->setRange($start, $end); - $collector->setTypes([TransactionTypeEnum::TRANSFER->value]); - $transferSet = $collector->getExtractedJournals(); - - /** @var array $currentDate */ - foreach ($dates as $currentDate) { - $spent = $this->filterJournalsByDate($spentSet, $currentDate['start'], $currentDate['end']); - $earned = $this->filterJournalsByDate($earnedSet, $currentDate['start'], $currentDate['end']); - $transferred = $this->filterJournalsByDate($transferSet, $currentDate['start'], $currentDate['end']); - $title = Navigation::periodShow($currentDate['end'], $currentDate['period']); - $entries[] - = [ - 'title' => $title, - 'route' => route('categories.no-category', [$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), - ]; - } - app('log')->debug('End of loops'); - - return $entries; - } - - /** - * This shows a period overview for a tag. It goes back in time and lists all relevant transactions and sums. - * - * @throws FireflyException - */ - protected function getTagPeriodOverview(Tag $tag, Carbon $start, Carbon $end): array // period overview for tags. - { - $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('tag-period-entries'); - $cache->addProperty($tag->id); - if ($cache->has()) { - return $cache->get(); - } - - /** @var array $dates */ - $dates = Navigation::blockPeriods($start, $end, $range); - $entries = []; - - // collect all expenses in this period: - /** @var GroupCollectorInterface $collector */ - $collector = app(GroupCollectorInterface::class); - $collector->setTag($tag); - $collector->setRange($start, $end); - $collector->setTypes([TransactionTypeEnum::DEPOSIT->value]); - $earnedSet = $collector->getExtractedJournals(); - - // collect all income in this period: - /** @var GroupCollectorInterface $collector */ - $collector = app(GroupCollectorInterface::class); - $collector->setTag($tag); - $collector->setRange($start, $end); - $collector->setTypes([TransactionTypeEnum::WITHDRAWAL->value]); - $spentSet = $collector->getExtractedJournals(); - - // collect all transfers in this period: - /** @var GroupCollectorInterface $collector */ - $collector = app(GroupCollectorInterface::class); - $collector->setTag($tag); - $collector->setRange($start, $end); - $collector->setTypes([TransactionTypeEnum::TRANSFER->value]); - $transferSet = $collector->getExtractedJournals(); - - // filer all of them: - $earnedSet = $this->filterJournalsByTag($earnedSet, $tag); - $spentSet = $this->filterJournalsByTag($spentSet, $tag); - $transferSet = $this->filterJournalsByTag($transferSet, $tag); - - foreach ($dates as $currentDate) { - $spent = $this->filterJournalsByDate($spentSet, $currentDate['start'], $currentDate['end']); - $earned = $this->filterJournalsByDate($earnedSet, $currentDate['start'], $currentDate['end']); - $transferred = $this->filterJournalsByDate($transferSet, $currentDate['start'], $currentDate['end']); - $title = Navigation::periodShow($currentDate['end'], $currentDate['period']); - $entries[] - = [ - 'transactions' => 0, - 'title' => $title, - 'route' => route( - 'tags.show', - [$tag->id, $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), - ]; - } - - 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 - */ - protected function getTransactionPeriodOverview(string $transactionType, Carbon $start, Carbon $end): array - { - $range = Navigation::getViewRange(true); - $types = config(sprintf('firefly.transactionTypesByType.%s', $transactionType)); - [$start, $end] = $end < $start ? [$end, $start] : [$start, $end]; - - // properties for cache - $cache = new CacheProperties(); - $cache->addProperty($start); - $cache->addProperty($end); - $cache->addProperty('transactions-period-entries'); - $cache->addProperty($transactionType); - if ($cache->has()) { - return $cache->get(); - } - - /** @var array $dates */ - $dates = Navigation::blockPeriods($start, $end, $range); - $entries = []; - $spent = []; - $earned = []; - $transferred = []; - // collect all journals in this period (regardless of type) - $collector = app(GroupCollectorInterface::class); - $collector->setTypes($types)->setRange($start, $end); - $genericSet = $collector->getExtractedJournals(); - $loops = 0; - - foreach ($dates as $currentDate) { - $title = Navigation::periodShow($currentDate['end'], $currentDate['period']); - - if ($loops < 10) { - // set to correct array - if ('expenses' === $transactionType || 'withdrawal' === $transactionType) { - $spent = $this->filterJournalsByDate($genericSet, $currentDate['start'], $currentDate['end']); - } - if ('revenue' === $transactionType || 'deposit' === $transactionType) { - $earned = $this->filterJournalsByDate($genericSet, $currentDate['start'], $currentDate['end']); - } - if ('transfer' === $transactionType || 'transfers' === $transactionType) { - $transferred = $this->filterJournalsByDate($genericSet, $currentDate['start'], $currentDate['end']); - } - } - $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), - ]; - ++$loops; - } - - return $entries; - } - - /** - * Return only transactions where $account is the source. - */ - private function filterTransferredAway(Account $account, array $journals): array - { - $return = []; - - /** @var array $journal */ - foreach ($journals as $journal) { - if ($account->id === (int)$journal['source_account_id']) { - $return[] = $journal; - } - } - - return $return; - } - - /** - * Return only transactions where $account is the source. - */ - private function filterTransferredIn(Account $account, array $journals): array - { - $return = []; - - /** @var array $journal */ - foreach ($journals as $journal) { - if ($account->id === (int)$journal['destination_account_id']) { - $return[] = $journal; - } - } - - return $return; - } } diff --git a/app/Support/Http/Controllers/RequestInformation.php b/app/Support/Http/Controllers/RequestInformation.php index 73a39655e6..b7600f778a 100644 --- a/app/Support/Http/Controllers/RequestInformation.php +++ b/app/Support/Http/Controllers/RequestInformation.php @@ -53,6 +53,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. */ @@ -102,22 +118,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. */ diff --git a/app/Support/JsonApi/Enrichments/AccountEnrichment.php b/app/Support/JsonApi/Enrichments/AccountEnrichment.php index 62dee47082..7dc3cb444c 100644 --- a/app/Support/JsonApi/Enrichments/AccountEnrichment.php +++ b/app/Support/JsonApi/Enrichments/AccountEnrichment.php @@ -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,12 +160,17 @@ 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) { - $id = (int)$item->id; - $item->full_account_type = $this->accountTypes[(int)$item->account_type_id] ?? null; - $meta = [ + $id = (int)$item->id; + $item->full_account_type = $this->accountTypes[(int)$item->account_type_id] ?? null; + $meta = [ 'currency' => null, 'location' => [ 'latitude' => null, @@ -289,30 +217,30 @@ class AccountEnrichment implements EnrichmentInterface // add balances // get currencies: - $currency = $this->primaryCurrency; // assume primary currency + $currency = $this->primaryCurrency; // assume primary currency if (null !== $meta['currency']) { $currency = $meta['currency']; } // get the current balance: - $date = $this->getDate(); + $date = $this->getDate(); // $finalBalance = Steam::finalAccountBalance($item, $date, $this->primaryCurrency, $this->convertToPrimary); - $finalBalance = $this->balances[$id]; - $balanceDifference = $this->getBalanceDifference($id, $currency); + $finalBalance = $this->balances[$id]; + $balanceDifference = $this->getBalanceDifference($id, $currency); Log::debug(sprintf('Call finalAccountBalance(%s) with date/time "%s"', var_export($this->convertToPrimary, true), $date->toIso8601String()), $finalBalance); // collect current balances: - $currentBalance = Steam::bcround($finalBalance[$currency->code] ?? '0', $currency->decimal_places); - $openingBalance = Steam::bcround($meta['opening_balance_amount'] ?? '0', $currency->decimal_places); - $virtualBalance = Steam::bcround($item->virtual_balance ?? '0', $currency->decimal_places); - $debtAmount = $meta['current_debt'] ?? null; + $currentBalance = Steam::bcround($finalBalance[$currency->code] ?? '0', $currency->decimal_places); + $openingBalance = Steam::bcround($meta['opening_balance_amount'] ?? '0', $currency->decimal_places); + $virtualBalance = Steam::bcround($item->virtual_balance ?? '0', $currency->decimal_places); + $debtAmount = $meta['current_debt'] ?? null; // set some pc_ default values to NULL: - $pcCurrentBalance = null; - $pcOpeningBalance = null; - $pcVirtualBalance = null; - $pcDebtAmount = null; - $pcBalanceDifference = null; + $pcCurrentBalance = null; + $pcOpeningBalance = null; + $pcVirtualBalance = null; + $pcDebtAmount = null; + $pcBalanceDifference = null; // convert to primary currency if needed: if ($this->convertToPrimary && $currency->id !== $this->primaryCurrency->id) { @@ -351,17 +279,12 @@ class AccountEnrichment implements EnrichmentInterface 'pc_balance_difference' => $pcBalanceDifference, ]; // end add balances - $item->meta = $meta; + $item->meta = $meta; return $item; }); } - 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,15 +294,84 @@ 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') - ->whereIn('object_groupable_id', $this->ids) - ->where('object_groupable_type', Account::class) - ->get(['object_groupable_id', 'object_group_id']) - ; + $set = DB::table('object_groupables') + ->whereIn('object_groupable_id', $this->ids) + ->where('object_groupable_type', Account::class) + ->get(['object_groupable_id', 'object_group_id']); - $ids = array_unique($set->pluck('object_group_id')->toArray()); + $ids = array_unique($set->pluck('object_group_id')->toArray()); foreach ($set as $entry) { $this->mappedObjects[(int)$entry->object_groupable_id] = (int)$entry->object_group_id; @@ -393,32 +385,40 @@ 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 @@ -431,17 +431,12 @@ class AccountEnrichment implements EnrichmentInterface if (0 === count($startBalance) || 0 === count($endBalance)) { return null; } - $start = $startBalance[$currency->code] ?? '0'; - $end = $endBalance[$currency->code] ?? '0'; + $start = $startBalance[$currency->code] ?? '0'; + $end = $endBalance[$currency->code] ?? '0'; 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', []); @@ -458,7 +453,7 @@ class AccountEnrichment implements EnrichmentInterface case 'current_balance': case 'pc_current_balance': - $this->collection = $this->collection->sortBy(static fn (Account $account) => $account->meta['balances'][$parameter[0]] ?? '0', SORT_NUMERIC, 'desc' === $parameter[1]); + $this->collection = $this->collection->sortBy(static fn(Account $account) => $account->meta['balances'][$parameter[0]] ?? '0', SORT_NUMERIC, 'desc' === $parameter[1]); break; } diff --git a/app/Support/JsonApi/Enrichments/AvailableBudgetEnrichment.php b/app/Support/JsonApi/Enrichments/AvailableBudgetEnrichment.php index 6154c941aa..85711c7efb 100644 --- a/app/Support/JsonApi/Enrichments/AvailableBudgetEnrichment.php +++ b/app/Support/JsonApi/Enrichments/AvailableBudgetEnrichment.php @@ -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() { @@ -79,7 +79,7 @@ class AvailableBudgetEnrichment implements EnrichmentInterface } #[Override] - public function enrichSingle(array|Model $model): array|Model + public function enrichSingle(array | Model $model): array | Model { Log::debug(__METHOD__); $collection = new Collection()->push($model); @@ -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; - } - } } diff --git a/app/Support/JsonApi/Enrichments/BudgetEnrichment.php b/app/Support/JsonApi/Enrichments/BudgetEnrichment.php index 2aa21bc9cf..71e4ff160b 100644 --- a/app/Support/JsonApi/Enrichments/BudgetEnrichment.php +++ b/app/Support/JsonApi/Enrichments/BudgetEnrichment.php @@ -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() {} @@ -70,7 +70,7 @@ class BudgetEnrichment implements EnrichmentInterface return $this->collection; } - public function enrichSingle(array|Model $model): array|Model + public function enrichSingle(array | Model $model): array | Model { Log::debug(__METHOD__); $collection = new Collection()->push($model); @@ -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,33 +100,11 @@ 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) { - $id = (int)$item->id; - $meta = [ + $id = (int)$item->id; + $meta = [ 'object_group_id' => null, 'object_group_order' => null, 'object_group_title' => null, @@ -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']; } @@ -168,7 +156,7 @@ class BudgetEnrichment implements EnrichmentInterface $opsRepository->setUserGroup($this->userGroup); // $spent = $this->beautify(); // $set = $this->opsRepository->sumExpenses($start, $end, null, new Collection()->push($budget)) - $expenses = $opsRepository->collectExpenses($this->start, $this->end, null, $this->collection, null); + $expenses = $opsRepository->collectExpenses($this->start, $this->end, null, $this->collection, null); foreach ($this->collection as $item) { $id = (int)$item->id; $this->spent[$id] = array_values($opsRepository->sumCollectedExpensesByBudget($expenses, $item, false)); @@ -177,25 +165,35 @@ 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 { - $set = DB::table('object_groupables') - ->whereIn('object_groupable_id', $this->ids) - ->where('object_groupable_type', Budget::class) - ->get(['object_groupable_id', 'object_group_id']) - ; + $set = DB::table('object_groupables') + ->whereIn('object_groupable_id', $this->ids) + ->where('object_groupable_type', Budget::class) + ->get(['object_groupable_id', 'object_group_id']); - $ids = array_unique($set->pluck('object_group_id')->toArray()); + $ids = array_unique($set->pluck('object_group_id')->toArray()); foreach ($set as $entry) { $this->mappedObjects[(int)$entry->object_groupable_id] = (int)$entry->object_group_id; diff --git a/app/Support/JsonApi/Enrichments/BudgetLimitEnrichment.php b/app/Support/JsonApi/Enrichments/BudgetLimitEnrichment.php index 0ba9e1f1a3..f0a7fd3479 100644 --- a/app/Support/JsonApi/Enrichments/BudgetLimitEnrichment.php +++ b/app/Support/JsonApi/Enrichments/BudgetLimitEnrichment.php @@ -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() { @@ -73,7 +73,7 @@ class BudgetLimitEnrichment implements EnrichmentInterface return $this->collection; } - public function enrichSingle(array|Model $model): array|Model + public function enrichSingle(array | Model $model): array | Model { Log::debug(__METHOD__); $collection = new Collection()->push($model); @@ -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) { @@ -145,12 +115,12 @@ class BudgetLimitEnrichment implements EnrichmentInterface private function collectBudgets(): void { - $budgetIds = $this->collection->pluck('budget_id')->unique()->toArray(); - $budgets = Budget::whereIn('id', $budgetIds)->get(); + $budgetIds = $this->collection->pluck('budget_id')->unique()->toArray(); + $budgets = Budget::whereIn('id', $budgetIds)->get(); $repository = app(OperationsRepository::class); $repository->setUser($this->user); - $expenses = $repository->collectExpenses($this->start, $this->end, null, $budgets, null); + $expenses = $repository->collectExpenses($this->start, $this->end, null, $budgets, null); /** @var BudgetLimit $budgetLimit */ foreach ($this->collection as $budgetLimit) { @@ -179,26 +149,55 @@ class BudgetLimitEnrichment implements EnrichmentInterface } } - private function stringifyIds(): void + private function collectIds(): void { - $this->expenses = array_map(fn ($first) => array_map(function ($second) { - $second['currency_id'] = (string)($second['currency_id'] ?? 0); + $this->start = $this->collection->min('start_date') ?? Carbon::now()->startOfMonth(); + $this->end = $this->collection->max('end_date') ?? Carbon::now()->endOfMonth(); - return $second; - }, $first), $this->expenses); + /** @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); + } - $this->pcExpenses = array_map(fn ($first) => array_map(function ($second) { - $second['currency_id'] = (string)($second['currency_id'] ?? 0); - - return $second; - }, $first), $this->expenses); + 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); + $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) { + $second['currency_id'] = (string)($second['currency_id'] ?? 0); + + return $second; + }, $first), $this->expenses); + + $this->pcExpenses = array_map(fn($first) => array_map(function ($second) { + $second['currency_id'] = (string)($second['currency_id'] ?? 0); + + return $second; + }, $first), $this->expenses); + } } diff --git a/app/Support/JsonApi/Enrichments/CategoryEnrichment.php b/app/Support/JsonApi/Enrichments/CategoryEnrichment.php index bbe24f94c2..074747011b 100644 --- a/app/Support/JsonApi/Enrichments/CategoryEnrichment.php +++ b/app/Support/JsonApi/Enrichments/CategoryEnrichment.php @@ -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 { @@ -62,7 +62,7 @@ class CategoryEnrichment implements EnrichmentInterface return $collection; } - public function enrichSingle(array|Model $model): array|Model + public function enrichSingle(array | Model $model): array | Model { Log::debug(__METHOD__); $collection = new Collection()->push($model); @@ -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,23 +111,21 @@ 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 { $notes = Note::query()->whereIn('noteable_id', $this->ids) - ->whereNotNull('notes.text') - ->where('notes.text', '!=', '') - ->where('noteable_type', Category::class)->get(['notes.noteable_id', 'notes.text'])->toArray() - ; + ->whereNotNull('notes.text') + ->where('notes.text', '!=', '') + ->where('noteable_type', Category::class)->get(['notes.noteable_id', 'notes.text'])->toArray(); foreach ($notes as $note) { $this->notes[(int)$note['noteable_id']] = (string)$note['text']; } @@ -140,9 +139,9 @@ class CategoryEnrichment implements EnrichmentInterface $opsRepository = app(OperationsRepositoryInterface::class); $opsRepository->setUser($this->user); $opsRepository->setUserGroup($this->userGroup); - $expenses = $opsRepository->collectExpenses($this->start, $this->end, null, $this->collection); - $income = $opsRepository->collectIncome($this->start, $this->end, null, $this->collection); - $transfers = $opsRepository->collectTransfers($this->start, $this->end, null, $this->collection); + $expenses = $opsRepository->collectExpenses($this->start, $this->end, null, $this->collection); + $income = $opsRepository->collectIncome($this->start, $this->end, null, $this->collection); + $transfers = $opsRepository->collectTransfers($this->start, $this->end, null, $this->collection); foreach ($this->collection as $item) { $id = (int)$item->id; $this->spent[$id] = array_values($opsRepository->sumCollectedTransactionsByCategory($expenses, $item, 'negative', false)); diff --git a/app/Support/JsonApi/Enrichments/EnrichmentInterface.php b/app/Support/JsonApi/Enrichments/EnrichmentInterface.php index 0ccfb7c060..e93ddcf283 100644 --- a/app/Support/JsonApi/Enrichments/EnrichmentInterface.php +++ b/app/Support/JsonApi/Enrichments/EnrichmentInterface.php @@ -33,7 +33,7 @@ interface EnrichmentInterface { public function enrich(Collection $collection): Collection; - public function enrichSingle(array|Model $model): array|Model; + public function enrichSingle(array | Model $model): array | Model; public function setUser(User $user): void; diff --git a/app/Support/JsonApi/Enrichments/PiggyBankEnrichment.php b/app/Support/JsonApi/Enrichments/PiggyBankEnrichment.php index bacf8d12c3..aebdde8f67 100644 --- a/app/Support/JsonApi/Enrichments/PiggyBankEnrichment.php +++ b/app/Support/JsonApi/Enrichments/PiggyBankEnrichment.php @@ -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() { @@ -77,7 +77,7 @@ class PiggyBankEnrichment implements EnrichmentInterface return $this->collection; } - public function enrichSingle(array|Model $model): array|Model + public function enrichSingle(array | Model $model): array | Model { Log::debug(__METHOD__); $collection = new Collection()->push($model); @@ -97,80 +97,17 @@ 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'], (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 appendCollectedData(): void { $this->collection = $this->collection->map(function (PiggyBank $item) { - $id = (int)$item->id; - $currencyId = (int)$item->transaction_currency_id; - $currency = $this->currencies[$currencyId] ?? $this->primaryCurrency; - $targetAmount = null; + $id = (int)$item->id; + $currencyId = (int)$item->transaction_currency_id; + $currency = $this->currencies[$currencyId] ?? $this->primaryCurrency; + $targetAmount = null; if (0 !== bccomp($item->target_amount, '0')) { $targetAmount = $item->target_amount; } - $meta = [ + $meta = [ 'notes' => $this->notes[$id] ?? null, 'currency' => $this->currencies[$currencyId] ?? null, // 'auto_budget' => $this->autoBudgets[$id] ?? null, @@ -193,23 +130,23 @@ 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']; } // add current amount(s). foreach ($this->amounts[$id] as $accountId => $row) { - $meta['accounts'][] = [ + $meta['accounts'][] = [ 'account_id' => (string)$accountId, 'name' => $this->accounts[$accountId]['name'] ?? '', 'current_amount' => Steam::bcround($row['current_amount'], $currency->decimal_places), 'pc_current_amount' => Steam::bcround($row['pc_current_amount'], $this->primaryCurrency->decimal_places), ]; - $meta['current_amount'] = bcadd($meta['current_amount'], $row['current_amount']); + $meta['current_amount'] = bcadd($meta['current_amount'], $row['current_amount']); // only add pc_current_amount when the pc_current_amount is set $meta['pc_current_amount'] = null === $row['pc_current_amount'] ? null : bcadd($meta['pc_current_amount'], $row['pc_current_amount']); } - $meta['current_amount'] = Steam::bcround($meta['current_amount'], $currency->decimal_places); + $meta['current_amount'] = Steam::bcround($meta['current_amount'], $currency->decimal_places); // only round this number when pc_current_amount is set. $meta['pc_current_amount'] = null === $meta['pc_current_amount'] ? null : Steam::bcround($meta['pc_current_amount'], $this->primaryCurrency->decimal_places); @@ -223,19 +160,83 @@ class PiggyBankEnrichment implements EnrichmentInterface $meta['save_per_month'] = Steam::bcround($this->getSuggestedMonthlyAmount($item->start_date, $item->target_date, $meta['target_amount'], $meta['current_amount']), $currency->decimal_places); $meta['pc_save_per_month'] = Steam::bcround($this->getSuggestedMonthlyAmount($item->start_date, $item->target_date, $meta['pc_target_amount'], $meta['pc_current_amount']), $currency->decimal_places); - $item->meta = $meta; + $item->meta = $meta; return $item; }); } + 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) - ->whereNotNull('notes.text') - ->where('notes.text', '!=', '') - ->where('noteable_type', PiggyBank::class)->get(['notes.noteable_id', 'notes.text'])->toArray() - ; + ->whereNotNull('notes.text') + ->where('notes.text', '!=', '') + ->where('noteable_type', PiggyBank::class)->get(['notes.noteable_id', 'notes.text'])->toArray(); foreach ($notes as $note) { $this->notes[(int)$note['noteable_id']] = (string)$note['text']; } @@ -244,13 +245,12 @@ class PiggyBankEnrichment implements EnrichmentInterface private function collectObjectGroups(): void { - $set = DB::table('object_groupables') - ->whereIn('object_groupable_id', $this->ids) - ->where('object_groupable_type', PiggyBank::class) - ->get(['object_groupable_id', 'object_group_id']) - ; + $set = DB::table('object_groupables') + ->whereIn('object_groupable_id', $this->ids) + ->where('object_groupable_type', PiggyBank::class) + ->get(['object_groupable_id', 'object_group_id']); - $ids = array_unique($set->pluck('object_group_id')->toArray()); + $ids = array_unique($set->pluck('object_group_id')->toArray()); foreach ($set as $entry) { $this->mappedObjects[(int)$entry->object_groupable_id] = (int)$entry->object_group_id; @@ -264,8 +264,6 @@ class PiggyBankEnrichment implements EnrichmentInterface } } - private function collectCurrentAmounts(): void {} - /** * Returns the suggested amount the user should save per month, or "". */ diff --git a/app/Support/JsonApi/Enrichments/PiggyBankEventEnrichment.php b/app/Support/JsonApi/Enrichments/PiggyBankEventEnrichment.php index fad6293f90..8758dfe94d 100644 --- a/app/Support/JsonApi/Enrichments/PiggyBankEventEnrichment.php +++ b/app/Support/JsonApi/Enrichments/PiggyBankEventEnrichment.php @@ -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; @@ -66,7 +66,7 @@ class PiggyBankEventEnrichment implements EnrichmentInterface return $this->collection; } - public function enrichSingle(array|Model $model): array|Model + public function enrichSingle(array | Model $model): array | Model { Log::debug(__METHOD__); $collection = new Collection()->push($model); @@ -86,53 +86,13 @@ class PiggyBankEventEnrichment implements EnrichmentInterface $this->userGroup = $userGroup; } - private function collectIds(): void - { - /** @var PiggyBankEvent $event */ - foreach ($this->collection as $event) { - $this->ids[] = (int)$event->id; - $this->journalIds[(int)$event->id] = (int)$event->transaction_journal_id; - $this->piggyBankIds[(int)$event->id] = (int)$event->piggy_bank_id; - } - $this->ids = array_unique($this->ids); - // collect groups with journal info. - $set = TransactionJournal::whereIn('id', $this->journalIds)->get(['id', 'transaction_group_id']); - - /** @var TransactionJournal $item */ - foreach ($set as $item) { - $this->groupIds[(int)$item->id] = (int)$item->transaction_group_id; - } - - // collect account info. - $set = DB::table('account_piggy_bank')->whereIn('piggy_bank_id', $this->piggyBankIds)->get(['piggy_bank_id', 'account_id']); - foreach ($set as $item) { - $id = (int)$item->piggy_bank_id; - if (!array_key_exists($id, $this->accountIds)) { - $this->accountIds[$id] = (int)$item->account_id; - } - } - - // 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]; - } - } - 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; + $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)) { @@ -149,4 +109,44 @@ class PiggyBankEventEnrichment implements EnrichmentInterface }); } + + private function collectIds(): void + { + /** @var PiggyBankEvent $event */ + foreach ($this->collection as $event) { + $this->ids[] = (int)$event->id; + $this->journalIds[(int)$event->id] = (int)$event->transaction_journal_id; + $this->piggyBankIds[(int)$event->id] = (int)$event->piggy_bank_id; + } + $this->ids = array_unique($this->ids); + // collect groups with journal info. + $set = TransactionJournal::whereIn('id', $this->journalIds)->get(['id', 'transaction_group_id']); + + /** @var TransactionJournal $item */ + foreach ($set as $item) { + $this->groupIds[(int)$item->id] = (int)$item->transaction_group_id; + } + + // collect account info. + $set = DB::table('account_piggy_bank')->whereIn('piggy_bank_id', $this->piggyBankIds)->get(['piggy_bank_id', 'account_id']); + foreach ($set as $item) { + $id = (int)$item->piggy_bank_id; + if (!array_key_exists($id, $this->accountIds)) { + $this->accountIds[$id] = (int)$item->account_id; + } + } + + // 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]; + } + } } diff --git a/app/Support/JsonApi/Enrichments/RecurringEnrichment.php b/app/Support/JsonApi/Enrichments/RecurringEnrichment.php index c8a3d8707a..30484f5388 100644 --- a/app/Support/JsonApi/Enrichments/RecurringEnrichment.php +++ b/app/Support/JsonApi/Enrichments/RecurringEnrichment.php @@ -51,30 +51,29 @@ use Illuminate\Database\Eloquent\Model; use Illuminate\Support\Arr; use Illuminate\Support\Collection; use Illuminate\Support\Facades\Log; - 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() { @@ -98,7 +97,7 @@ class RecurringEnrichment implements EnrichmentInterface return $this->collection; } - public function enrichSingle(array|Model $model): array|Model + public function enrichSingle(array | Model $model): array | Model { Log::debug(__METHOD__); $collection = new Collection()->push($model); @@ -107,139 +106,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. @@ -265,7 +131,7 @@ class RecurringEnrichment implements EnrichmentInterface return (string)trans('firefly.recurring_monthly', ['dayOfMonth' => $repetition->repetition_moment, 'skip' => $repetition->repetition_skip - 1], $this->language); } if ('ndom' === $repetition->repetition_type) { - $parts = explode(',', $repetition->repetition_moment); + $parts = explode(',', $repetition->repetition_moment); // first part is number of week, second is weekday. $dayOfWeek = trans(sprintf('config.dow_%s', $parts[1]), [], $this->language); if ($repetition->repetition_skip > 0) { @@ -282,7 +148,7 @@ class RecurringEnrichment implements EnrichmentInterface } // $diffInYears = (int)$today->diffInYears($repDate, true); // $repDate->addYears($diffInYears); // technically not necessary. - $string = $repDate->isoFormat((string)trans('config.month_and_day_no_year_js')); + $string = $repDate->isoFormat((string)trans('config.month_and_day_no_year_js')); return (string)trans('firefly.recurring_yearly', ['date' => $string], $this->language); } @@ -290,96 +156,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,10 +196,183 @@ 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); - $meta = RecurrenceTransactionMeta::whereNull('deleted_at')->whereIn('rt_id', $ids)->get(); + $ids = array_keys($this->transactions); + $meta = RecurrenceTransactionMeta::whereNull('deleted_at')->whereIn('rt_id', $ids)->get(); // other meta-data to be collected: $billIds = []; $piggyBankIds = []; @@ -409,8 +384,8 @@ class RecurringEnrichment implements EnrichmentInterface $transactionId = (int)$entry->rt_id; // this should refer to another array, were rtIds can be used to find the recurrence. - $recurrenceId = $this->recurrenceIds[$transactionId] ?? 0; - $name = (string)($entry->name ?? ''); + $recurrenceId = $this->recurrenceIds[$transactionId] ?? 0; + $name = (string)($entry->name ?? ''); if (0 === $recurrenceId) { Log::error(sprintf('Could not find recurrence ID for recurrence transaction ID %d', $transactionId)); @@ -504,109 +479,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; } } diff --git a/app/Support/JsonApi/Enrichments/SubscriptionEnrichment.php b/app/Support/JsonApi/Enrichments/SubscriptionEnrichment.php index 06388a80bc..285e0ad37e 100644 --- a/app/Support/JsonApi/Enrichments/SubscriptionEnrichment.php +++ b/app/Support/JsonApi/Enrichments/SubscriptionEnrichment.php @@ -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() { @@ -86,11 +86,11 @@ class SubscriptionEnrichment implements EnrichmentInterface $paidDates = $this->paidDates; $payDates = $this->payDates; $this->collection = $this->collection->map(function (Bill $item) use ($notes, $objectGroups, $paidDates, $payDates) { - $id = (int)$item->id; - $currency = $item->transactionCurrency; - $nem = $this->getNextExpectedMatch($payDates[$id] ?? []); + $id = (int)$item->id; + $currency = $item->transactionCurrency; + $nem = $this->getNextExpectedMatch($payDates[$id] ?? []); - $meta = [ + $meta = [ 'notes' => null, 'object_group_id' => null, 'object_group_title' => null, @@ -101,7 +101,7 @@ class SubscriptionEnrichment implements EnrichmentInterface 'nem' => $nem, 'nem_diff' => $this->getNextExpectedMatchDiff($nem, $payDates[$id] ?? []), ]; - $amounts = [ + $amounts = [ 'amount_min' => Steam::bcround($item->amount_min, $currency->decimal_places), 'amount_max' => Steam::bcround($item->amount_max, $currency->decimal_places), 'average' => Steam::bcround(bcdiv(bcadd($item->amount_min, $item->amount_max), '2'), $currency->decimal_places), @@ -142,7 +142,7 @@ class SubscriptionEnrichment implements EnrichmentInterface return $collection; } - public function enrichSingle(array|Model $model): array|Model + public function enrichSingle(array | Model $model): array | Model { Log::debug(__METHOD__); $collection = new Collection()->push($model); @@ -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,24 +172,49 @@ 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 { - $set = DB::table('object_groupables') - ->whereIn('object_groupable_id', $this->subscriptionIds) - ->where('object_groupable_type', Bill::class) - ->get(['object_groupable_id', 'object_group_id']) - ; + $set = DB::table('object_groupables') + ->whereIn('object_groupable_id', $this->subscriptionIds) + ->where('object_groupable_type', Bill::class) + ->get(['object_groupable_id', 'object_group_id']); - $ids = array_unique($set->pluck('object_group_id')->toArray()); + $ids = array_unique($set->pluck('object_group_id')->toArray()); foreach ($set as $entry) { $this->mappedObjects[(int)$entry->object_groupable_id] = (int)$entry->object_group_id; @@ -220,13 +242,13 @@ class SubscriptionEnrichment implements EnrichmentInterface // 2023-07-18 this particular date is used to search for the last paid date. // 2023-07-18 the cloned $searchDate is used to grab the correct transactions. /** @var Carbon $start */ - $start = clone $this->start; - $searchStart = clone $start; + $start = clone $this->start; + $searchStart = clone $start; $start->subDay(); /** @var Carbon $end */ - $end = clone $this->end; - $searchEnd = clone $end; + $end = clone $this->end; + $searchEnd = clone $end; // move the search dates to the start of the day. $searchStart->startOfDay(); @@ -235,13 +257,13 @@ class SubscriptionEnrichment implements EnrichmentInterface Log::debug(sprintf('Search parameters are: start: %s, end: %s', $searchStart->format('Y-m-d H:i:s'), $searchEnd->format('Y-m-d H:i:s'))); // Get from database when bills were paid. - $set = $this->user->transactionJournals() - ->whereIn('bill_id', $this->subscriptionIds) - ->leftJoin('transactions', 'transactions.transaction_journal_id', '=', 'transaction_journals.id') - ->leftJoin('transaction_currencies AS currency', 'currency.id', '=', 'transactions.transaction_currency_id') - ->leftJoin('transaction_currencies AS foreign_currency', 'foreign_currency.id', '=', 'transactions.foreign_currency_id') - ->where('transactions.amount', '>', 0) - ->before($searchEnd)->after($searchStart)->get( + $set = $this->user->transactionJournals() + ->whereIn('bill_id', $this->subscriptionIds) + ->leftJoin('transactions', 'transactions.transaction_journal_id', '=', 'transaction_journals.id') + ->leftJoin('transaction_currencies AS currency', 'currency.id', '=', 'transactions.transaction_currency_id') + ->leftJoin('transaction_currencies AS foreign_currency', 'foreign_currency.id', '=', 'transactions.foreign_currency_id') + ->where('transactions.amount', '>', 0) + ->before($searchEnd)->after($searchStart)->get( [ 'transaction_journals.id', 'transaction_journals.date', @@ -258,25 +280,24 @@ class SubscriptionEnrichment implements EnrichmentInterface 'transactions.amount', 'transactions.foreign_amount', ] - ) - ; + ); Log::debug(sprintf('Count %d entries in set', $set->count())); // for each bill, do a loop. - $converter = new ExchangeRateConverter(); + $converter = new ExchangeRateConverter(); /** @var Bill $subscription */ foreach ($this->collection as $subscription) { // Grab from array the most recent payment. If none exist, fall back to the start date and pretend *that* was the last paid date. Log::debug(sprintf('Grab last paid date from function, return %s if it comes up with nothing.', $start->format('Y-m-d'))); - $lastPaidDate = $this->lastPaidDate($subscription, $set, $start); + $lastPaidDate = $this->lastPaidDate($subscription, $set, $start); Log::debug(sprintf('Result of lastPaidDate is %s', $lastPaidDate->format('Y-m-d'))); // At this point the "next match" is exactly after the last time the bill was paid. - $result = []; - $filtered = $set->filter(fn (TransactionJournal $journal) => (int)$journal->bill_id === (int)$subscription->id); + $result = []; + $filtered = $set->filter(fn(TransactionJournal $journal) => (int)$journal->bill_id === (int)$subscription->id); foreach ($filtered as $entry) { - $array = [ + $array = [ 'transaction_group_id' => (string)$entry->transaction_group_id, 'transaction_journal_id' => (string)$entry->id, 'date' => $entry->date->toAtomString(), @@ -329,37 +350,47 @@ class SubscriptionEnrichment implements EnrichmentInterface } - public function setStart(?Carbon $start): void + private function collectPayDates(): void { - $this->start = $start; - } + if (!$this->start instanceof Carbon || !$this->end instanceof Carbon) { + Log::debug('Parameters are NULL, set empty array'); - 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; + return; } - $latest = $filtered->first()->date; - - /** @var TransactionJournal $journal */ - foreach ($filtered as $journal) { - if ($journal->date->gte($latest)) { - $latest = $journal->date; + /** @var Bill $subscription */ + foreach ($this->collection as $subscription) { + $id = (int)$subscription->id; + $lastPaidDate = $this->getLastPaidDate($this->paidDates[$id] ?? []); + $payDates = $this->calculator->getPayDates($this->start, $this->end, $subscription->date, $subscription->repeat_freq, $subscription->skip, $lastPaidDate); + $payDatesFormatted = []; + foreach ($payDates as $string) { + $date = Carbon::createFromFormat('!Y-m-d', $string, config('app.timezone')); + if (!$date instanceof Carbon) { + $date = today(config('app.timezone')); + } + $payDatesFormatted[] = $date->toAtomString(); } + $this->payDates[$id] = $payDatesFormatted; } + } - return $latest; + 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) { + unset($entry['date_object']); + + return $entry; + }, $entries); } private function getLastPaidDate(array $paidData): ?Carbon @@ -386,40 +417,6 @@ class SubscriptionEnrichment implements EnrichmentInterface return $return; } - private function collectPayDates(): void - { - if (!$this->start instanceof Carbon || !$this->end instanceof Carbon) { - Log::debug('Parameters are NULL, set empty array'); - - return; - } - - /** @var Bill $subscription */ - foreach ($this->collection as $subscription) { - $id = (int)$subscription->id; - $lastPaidDate = $this->getLastPaidDate($this->paidDates[$id] ?? []); - $payDates = $this->calculator->getPayDates($this->start, $this->end, $subscription->date, $subscription->repeat_freq, $subscription->skip, $lastPaidDate); - $payDatesFormatted = []; - foreach ($payDates as $string) { - $date = Carbon::createFromFormat('!Y-m-d', $string, config('app.timezone')); - if (!$date instanceof Carbon) { - $date = today(config('app.timezone')); - } - $payDatesFormatted[] = $date->toAtomString(); - } - $this->payDates[$id] = $payDatesFormatted; - } - } - - private function filterPaidDates(array $entries): array - { - return array_map(function (array $entry) { - unset($entry['date_object']); - - return $entry; - }, $entries); - } - private function getNextExpectedMatch(array $payDates): ?Carbon { // next expected match diff --git a/app/Support/JsonApi/Enrichments/TransactionGroupEnrichment.php b/app/Support/JsonApi/Enrichments/TransactionGroupEnrichment.php index d322331135..014aff8e68 100644 --- a/app/Support/JsonApi/Enrichments/TransactionGroupEnrichment.php +++ b/app/Support/JsonApi/Enrichments/TransactionGroupEnrichment.php @@ -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,119 +82,55 @@ 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 { - $notes = $this->notes; - $tags = $this->tags; - $metaData = $this->metaData; - $locations = $this->locations; - $attachmentCount = $this->attachmentCount; - $primaryCurrency = $this->primaryCurrency; + $notes = $this->notes; + $tags = $this->tags; + $metaData = $this->metaData; + $locations = $this->locations; + $attachmentCount = $this->attachmentCount; + $primaryCurrency = $this->primaryCurrency; $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; + $item['transactions'][$index]['notes'] = array_key_exists($journalId, $notes) ? $notes[$journalId] : null; // attach tags if they exist: - $item['transactions'][$index]['tags'] = array_key_exists($journalId, $tags) ? $tags[$journalId] : []; + $item['transactions'][$index]['tags'] = array_key_exists($journalId, $tags) ? $tags[$journalId] : []; // attachment count $item['transactions'][$index]['attachment_count'] = array_key_exists($journalId, $attachmentCount) ? $attachmentCount[$journalId] : 0; // default location data - $item['transactions'][$index]['location'] = [ + $item['transactions'][$index]['location'] = [ 'latitude' => null, 'longitude' => null, 'zoom_level' => null, @@ -216,16 +138,16 @@ 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 - $item['transactions'][$index]['meta'] = []; - $item['transactions'][$index]['meta_date'] = []; + $item['transactions'][$index]['meta'] = []; + $item['transactions'][$index]['meta_date'] = []; if (array_key_exists($journalId, $metaData)) { // loop al meta data: foreach ($metaData[$journalId] as $name => $value) { @@ -248,14 +170,88 @@ 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']; + } } } diff --git a/app/Support/JsonApi/Enrichments/WebhookEnrichment.php b/app/Support/JsonApi/Enrichments/WebhookEnrichment.php index 516705892a..0004c291c8 100644 --- a/app/Support/JsonApi/Enrichments/WebhookEnrichment.php +++ b/app/Support/JsonApi/Enrichments/WebhookEnrichment.php @@ -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 { @@ -67,7 +66,7 @@ class WebhookEnrichment implements EnrichmentInterface return $this->collection; } - public function enrichSingle(array|Model $model): array|Model + public function enrichSingle(array | Model $model): array | Model { Log::debug(__METHOD__); $collection = new Collection()->push($model); @@ -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; - }); - } } diff --git a/app/Support/Models/AccountBalanceCalculator.php b/app/Support/Models/AccountBalanceCalculator.php index d2a3572b7d..d9e616f9b9 100644 --- a/app/Support/Models/AccountBalanceCalculator.php +++ b/app/Support/Models/AccountBalanceCalculator.php @@ -62,6 +62,46 @@ 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'); @@ -72,15 +112,14 @@ class AccountBalanceCalculator $balances = []; $count = 0; $query = Transaction::leftJoin('transaction_journals', 'transaction_journals.id', '=', 'transactions.transaction_journal_id') - ->whereNull('transactions.deleted_at') - ->whereNull('transaction_journals.deleted_at') + ->whereNull('transactions.deleted_at') + ->whereNull('transaction_journals.deleted_at') // this order is the same as GroupCollector, but in the exact reverse. - ->orderBy('transaction_journals.date', 'asc') - ->orderBy('transaction_journals.order', 'desc') - ->orderBy('transaction_journals.id', 'asc') - ->orderBy('transaction_journals.description', 'asc') - ->orderBy('transactions.amount', 'asc') - ; + ->orderBy('transaction_journals.date', 'asc') + ->orderBy('transaction_journals.order', 'desc') + ->orderBy('transaction_journals.id', 'asc') + ->orderBy('transaction_journals.description', 'asc') + ->orderBy('transactions.amount', 'asc'); if ($accounts->count() > 0) { $query->whereIn('transactions.account_id', $accounts->pluck('id')->toArray()); } @@ -89,7 +128,7 @@ class AccountBalanceCalculator $query->where('transaction_journals.date', '>=', $notBefore); } - $set = $query->get(['transactions.id', 'transactions.balance_dirty', 'transactions.transaction_currency_id', 'transaction_journals.date', 'transactions.account_id', 'transactions.amount']); + $set = $query->get(['transactions.id', 'transactions.balance_dirty', 'transactions.transaction_currency_id', 'transaction_journals.date', 'transactions.account_id', 'transactions.amount']); Log::debug(sprintf('Counted %d transaction(s)', $set->count())); // the balance value is an array. @@ -102,8 +141,8 @@ class AccountBalanceCalculator $balances[$entry->account_id][$entry->transaction_currency_id] ??= [$this->getLatestBalance($entry->account_id, $entry->transaction_currency_id, $notBefore), null]; // before and after are easy: - $before = $balances[$entry->account_id][$entry->transaction_currency_id][0]; - $after = bcadd($before, (string)$entry->amount); + $before = $balances[$entry->account_id][$entry->transaction_currency_id][0]; + $after = bcadd($before, (string)$entry->amount); if (true === $entry->balance_dirty || $accounts->count() > 0) { // update the transaction: $entry->balance_before = $before; @@ -123,34 +162,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 +207,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); - } } diff --git a/app/Support/Models/BillDateCalculator.php b/app/Support/Models/BillDateCalculator.php index 3d49c867a3..9f40348777 100644 --- a/app/Support/Models/BillDateCalculator.php +++ b/app/Support/Models/BillDateCalculator.php @@ -49,15 +49,15 @@ class BillDateCalculator Log::debug(sprintf('Dates must be between %s and %s.', $earliest->format('Y-m-d'), $latest->format('Y-m-d'))); Log::debug(sprintf('Bill started on %s, period is "%s", skip is %d, last paid = "%s".', $billStart->format('Y-m-d'), $period, $skip, $lastPaid?->format('Y-m-d'))); - $daysUntilEOM = app('navigation')->daysUntilEndOfMonth($billStart); + $daysUntilEOM = app('navigation')->daysUntilEndOfMonth($billStart); Log::debug(sprintf('For bill start, days until end of month is %d', $daysUntilEOM)); - $set = new Collection(); - $currentStart = clone $earliest; + $set = new Collection(); + $currentStart = clone $earliest; // 2023-06-23 subDay to fix 7655 $currentStart->subDay(); - $loop = 0; + $loop = 0; Log::debug('Start of loop'); while ($currentStart <= $latest) { @@ -107,7 +107,7 @@ class BillDateCalculator // for the next loop, go to end of period, THEN add day. Log::debug('Add one day to nextExpectedMatch/currentStart.'); $nextExpectedMatch->addDay(); - $currentStart = clone $nextExpectedMatch; + $currentStart = clone $nextExpectedMatch; ++$loop; if ($loop > 31) { @@ -117,8 +117,8 @@ class BillDateCalculator } } Log::debug('end of loop'); - $simple = $set->map( // @phpstan-ignore-line - static fn (Carbon $date) => $date->format('Y-m-d') + $simple = $set->map( // @phpstan-ignore-line + static fn(Carbon $date) => $date->format('Y-m-d') ); Log::debug(sprintf('Found %d pay dates', $set->count()), $simple->toArray()); @@ -140,7 +140,7 @@ class BillDateCalculator return $billStartDate; } - $steps = app('navigation')->diffInPeriods($period, $skip, $earliest, $billStartDate); + $steps = app('navigation')->diffInPeriods($period, $skip, $earliest, $billStartDate); if ($steps === $this->diffInMonths) { Log::debug(sprintf('Steps is %d, which is the same as diffInMonths (%d), so we add another 1.', $steps, $this->diffInMonths)); ++$steps; diff --git a/app/Support/Models/ReturnsIntegerIdTrait.php b/app/Support/Models/ReturnsIntegerIdTrait.php index 804b9be384..d8178e07a5 100644 --- a/app/Support/Models/ReturnsIntegerIdTrait.php +++ b/app/Support/Models/ReturnsIntegerIdTrait.php @@ -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, ); } } diff --git a/app/Support/Models/ReturnsIntegerUserIdTrait.php b/app/Support/Models/ReturnsIntegerUserIdTrait.php index a0d2bc79e9..8eca6e943c 100644 --- a/app/Support/Models/ReturnsIntegerUserIdTrait.php +++ b/app/Support/Models/ReturnsIntegerUserIdTrait.php @@ -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, ); } } diff --git a/app/Support/Navigation.php b/app/Support/Navigation.php index d88b01ba38..a1e2c256c7 100644 --- a/app/Support/Navigation.php +++ b/app/Support/Navigation.php @@ -77,10 +77,10 @@ class Navigation if (!array_key_exists($repeatFreq, $functionMap)) { Log::error(sprintf( - 'The periodicity %s is unknown. Choose one of available periodicity: %s', - $repeatFreq, - implode(', ', array_keys($functionMap)) - )); + 'The periodicity %s is unknown. Choose one of available periodicity: %s', + $repeatFreq, + implode(', ', array_keys($functionMap)) + )); return $theDate; } @@ -88,30 +88,12 @@ 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) { [$start, $end] = [$end, $start]; } - $periods = []; + $periods = []; // first, 13 periods of [range] $loopCount = 0; $loopDate = clone $end; @@ -159,86 +141,71 @@ class Navigation return $periods; } - public function startOfPeriod(Carbon $theDate, string $repeatFreq): Carbon + public function daysUntilEndOfMonth(Carbon $date): int { - $date = clone $theDate; - // Log::debug(sprintf('Now in startOfPeriod("%s", "%s")', $date->toIso8601String(), $repeatFreq)); - $functionMap = [ - '1D' => 'startOfDay', - 'daily' => 'startOfDay', - '1W' => 'startOfWeek', - 'week' => 'startOfWeek', - 'weekly' => 'startOfWeek', - 'month' => 'startOfMonth', - '1M' => 'startOfMonth', - 'monthly' => 'startOfMonth', - '3M' => 'firstOfQuarter', - 'quarter' => 'firstOfQuarter', - 'quarterly' => 'firstOfQuarter', - 'year' => 'startOfYear', - 'yearly' => 'startOfYear', - '1Y' => 'startOfYear', - 'MTD' => 'startOfMonth', + $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)); - $parameterMap = [ - 'startOfWeek' => [Carbon::MONDAY], - ]; - - if (array_key_exists($repeatFreq, $functionMap)) { - $function = $functionMap[$repeatFreq]; - // Log::debug(sprintf('Function is ->%s()', $function)); - if (array_key_exists($function, $parameterMap)) { - // Log::debug(sprintf('Parameter map, function becomes ->%s(%s)', $function, implode(', ', $parameterMap[$function]))); - $date->{$function}($parameterMap[$function][0]); // @phpstan-ignore-line - // Log::debug(sprintf('Result is "%s"', $date->toIso8601String())); - - return $date; - } - - $date->{$function}(); // @phpstan-ignore-line - // Log::debug(sprintf('Result is "%s"', $date->toIso8601String())); - - return $date; + return 1; } - if ('half-year' === $repeatFreq || '6M' === $repeatFreq) { - $skipTo = $date->month > 7 ? 6 : 0; - $date->startOfYear()->addMonths($skipTo); - // Log::debug(sprintf('Custom call for "%s": addMonths(%d)', $repeatFreq, $skipTo)); - // Log::debug(sprintf('Result is "%s"', $date->toIso8601String())); + $func = $map[$period]; + // first do the diff + $floatDiff = $beginning->{$func}($end, true); // @phpstan-ignore-line - return $date; + // 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; } - $result = match ($repeatFreq) { - 'last7' => $date->subDays(7)->startOfDay(), - 'last30' => $date->subDays(30)->startOfDay(), - 'last90' => $date->subDays(90)->startOfDay(), - 'last365' => $date->subDays(365)->startOfDay(), - 'MTD' => $date->startOfMonth()->startOfDay(), - 'QTD' => $date->firstOfQuarter()->startOfDay(), - 'YTD' => $date->startOfYear()->startOfDay(), - default => null, - }; - if (null !== $result) { - // Log::debug(sprintf('Result is "%s"', $date->toIso8601String())); + // then do ceil() + $diff = ceil($floatDiff); - return $result; + 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 + )); } - if ('custom' === $repeatFreq) { - // Log::debug(sprintf('Custom, result is "%s"', $date->toIso8601String())); - - return $date; // the date is already at the start. - } - Log::error(sprintf('Cannot do startOfPeriod for $repeat_freq "%s"', $repeatFreq)); - - return $theDate; + return (int)$diff; } public function endOfPeriod(Carbon $end, string $repeatFreq): Carbon { - $currentEnd = clone $end; + $currentEnd = clone $end; // Log::debug(sprintf('Now in endOfPeriod("%s", "%s").', $currentEnd->toIso8601String(), $repeatFreq)); $functionMap = [ @@ -272,11 +239,11 @@ class Navigation Log::debug('Session data available.'); /** @var Carbon $tStart */ - $tStart = session('start', today(config('app.timezone'))->startOfMonth()); + $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); + $diffInDays = (int)$tStart->diffInDays($tEnd, true); } Log::debug(sprintf('Diff in days is %d', $diffInDays)); $currentEnd->addDays($diffInDays); @@ -286,13 +253,13 @@ class Navigation if ('MTD' === $repeatFreq) { $today = today(); if ($today->isSameMonth($end)) { - return $today->endOfDay(); + return $today->endOfDay()->milli(0); } return $end->endOfMonth(); } - $result = match ($repeatFreq) { + $result = match ($repeatFreq) { 'last7' => $currentEnd->addDays(7)->startOfDay(), 'last30' => $currentEnd->addDays(30)->startOfDay(), 'last90' => $currentEnd->addDays(90)->startOfDay(), @@ -312,19 +279,19 @@ class Navigation return $end; } - $function = $functionMap[$repeatFreq]; + $function = $functionMap[$repeatFreq]; if (array_key_exists($repeatFreq, $modifierMap)) { - $currentEnd->{$function}($modifierMap[$repeatFreq]); // @phpstan-ignore-line + $currentEnd->{$function}($modifierMap[$repeatFreq])->milli(0); // @phpstan-ignore-line if (in_array($repeatFreq, $subDay, true)) { $currentEnd->subDay(); } - $currentEnd->endOfDay(); + $currentEnd->endOfDay()->milli(0); return $currentEnd; } $currentEnd->{$function}(); // @phpstan-ignore-line - $currentEnd->endOfDay(); + $currentEnd->endOfDay()->milli(0); if (in_array($repeatFreq, $subDay, true)) { $currentEnd->subDay(); } @@ -333,68 +300,6 @@ class Navigation 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 = [ @@ -414,7 +319,7 @@ class Navigation 'yearly' => 'endOfYear', ]; - $currentEnd = clone $theCurrentEnd; + $currentEnd = clone $theCurrentEnd; if (array_key_exists($repeatFreq, $functionMap)) { $function = $functionMap[$repeatFreq]; @@ -438,7 +343,7 @@ class Navigation if (is_array($range)) { $range = '1M'; } - $range = (string) $range; + $range = (string)$range; if (!$correct) { return $range; } @@ -457,25 +362,25 @@ class Navigation */ public function listOfPeriods(Carbon $start, Carbon $end): array { - $locale = app('steam')->getLocale(); + $locale = app('steam')->getLocale(); // define period to increment $increment = 'addDay'; $format = $this->preferredCarbonFormat($start, $end); - $displayFormat = (string) trans('config.month_and_day_js', [], $locale); + $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'); + $displayFormat = (string)trans('config.month_js'); } // increment by year (for multi-year) if ($diff >= 12.0001) { $increment = 'addYear'; - $displayFormat = (string) trans('config.year_js'); + $displayFormat = (string)trans('config.year_js'); } - $begin = clone $start; - $entries = []; + $begin = clone $start; + $entries = []; while ($begin < $end) { $formatted = $begin->format($format); $displayed = $begin->isoFormat($displayFormat); @@ -486,6 +391,59 @@ class Navigation 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". @@ -508,40 +466,6 @@ class Navigation 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 */ @@ -567,14 +491,14 @@ class Navigation $locale = app('steam')->getLocale(); $diff = $start->diffInMonths($end, true); if ($diff >= 1.001 && $diff < 12.001) { - return (string) trans('config.month_js', [], $locale); + return (string)trans('config.month_js', [], $locale); } if ($diff >= 12.001) { - return (string) trans('config.year_js', [], $locale); + return (string)trans('config.year_js', [], $locale); } - return (string) trans('config.month_and_day_js', [], $locale); + return (string)trans('config.month_and_day_js', [], $locale); } /** @@ -631,13 +555,90 @@ class Navigation return '%Y-%m-%d'; } + public function startOfPeriod(Carbon $theDate, string $repeatFreq): Carbon + { + $date = clone $theDate; + // Log::debug(sprintf('Now in startOfPeriod("%s", "%s")', $date->toIso8601String(), $repeatFreq)); + $functionMap = [ + '1D' => 'startOfDay', + 'daily' => 'startOfDay', + '1W' => 'startOfWeek', + 'week' => 'startOfWeek', + 'weekly' => 'startOfWeek', + 'month' => 'startOfMonth', + '1M' => 'startOfMonth', + 'monthly' => 'startOfMonth', + '3M' => 'firstOfQuarter', + 'quarter' => 'firstOfQuarter', + 'quarterly' => 'firstOfQuarter', + 'year' => 'startOfYear', + 'yearly' => 'startOfYear', + '1Y' => 'startOfYear', + 'MTD' => 'startOfMonth', + ]; + + $parameterMap = [ + 'startOfWeek' => [Carbon::MONDAY], + ]; + + if (array_key_exists($repeatFreq, $functionMap)) { + $function = $functionMap[$repeatFreq]; + // Log::debug(sprintf('Function is ->%s()', $function)); + if (array_key_exists($function, $parameterMap)) { + // Log::debug(sprintf('Parameter map, function becomes ->%s(%s)', $function, implode(', ', $parameterMap[$function]))); + $date->{$function}($parameterMap[$function][0]); // @phpstan-ignore-line + // Log::debug(sprintf('Result is "%s"', $date->toIso8601String())); + + return $date; + } + + $date->{$function}(); // @phpstan-ignore-line + // Log::debug(sprintf('Result is "%s"', $date->toIso8601String())); + + return $date; + } + if ('half-year' === $repeatFreq || '6M' === $repeatFreq) { + $skipTo = $date->month > 7 ? 6 : 0; + $date->startOfYear()->addMonths($skipTo); + // Log::debug(sprintf('Custom call for "%s": addMonths(%d)', $repeatFreq, $skipTo)); + // Log::debug(sprintf('Result is "%s"', $date->toIso8601String())); + + return $date; + } + + $result = match ($repeatFreq) { + 'last7' => $date->subDays(7)->startOfDay(), + 'last30' => $date->subDays(30)->startOfDay(), + 'last90' => $date->subDays(90)->startOfDay(), + 'last365' => $date->subDays(365)->startOfDay(), + 'MTD' => $date->startOfMonth()->startOfDay(), + 'QTD' => $date->firstOfQuarter()->startOfDay(), + 'YTD' => $date->startOfYear()->startOfDay(), + default => null, + }; + if (null !== $result) { + // Log::debug(sprintf('Result is "%s"', $date->toIso8601String())); + + return $result; + } + + if ('custom' === $repeatFreq) { + // Log::debug(sprintf('Custom, result is "%s"', $date->toIso8601String())); + + return $date; // the date is already at the start. + } + Log::error(sprintf('Cannot do startOfPeriod for $repeat_freq "%s"', $repeatFreq)); + + return $theDate; + } + /** * @throws FireflyException */ public function subtractPeriod(Carbon $theDate, string $repeatFreq, ?int $subtract = null): Carbon { $subtract ??= 1; - $date = clone $theDate; + $date = clone $theDate; // 1D 1W 1M 3M 6M 1Y $functionMap = [ '1D' => 'subDays', @@ -676,11 +677,11 @@ class Navigation // this is then subtracted from $theDate (* $subtract). if ('custom' === $repeatFreq) { /** @var Carbon $tStart */ - $tStart = session('start', today(config('app.timezone'))->startOfMonth()); + $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); + $diffInDays = (int)$tStart->diffInDays($tEnd, true); $date->subDays($diffInDays * $subtract); return $date; @@ -770,7 +771,7 @@ class Navigation return $fiscalHelper->endOfFiscalYear($end); } - $list = [ + $list = [ 'last7', 'last30', 'last90', diff --git a/app/Support/Observers/RecalculatesAvailableBudgetsTrait.php b/app/Support/Observers/RecalculatesAvailableBudgetsTrait.php index 3f50036ab1..d71872971c 100644 --- a/app/Support/Observers/RecalculatesAvailableBudgetsTrait.php +++ b/app/Support/Observers/RecalculatesAvailableBudgetsTrait.php @@ -39,12 +39,98 @@ 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)); /** @var null|Budget $budget */ - $budget = Budget::find($budgetLimit->budget_id); + $budget = Budget::find($budgetLimit->budget_id); if (null === $budget) { Log::warning('Budget is null, probably deleted, find deleted version.'); @@ -59,7 +145,7 @@ trait RecalculatesAvailableBudgetsTrait } /** @var null|User $user */ - $user = $budget->user; + $user = $budget->user; // sanity check. It happens when the budget has been deleted so the original user is unknown. if (null === $user) { @@ -75,7 +161,7 @@ trait RecalculatesAvailableBudgetsTrait // all have to be created or updated. try { $viewRange = app('preferences')->getForUser($user, 'viewRange', '1M')->data; - } catch (ContainerExceptionInterface|NotFoundExceptionInterface $e) { + } catch (ContainerExceptionInterface | NotFoundExceptionInterface $e) { Log::error($e->getMessage()); $viewRange = '1M'; } @@ -83,20 +169,20 @@ 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); - $end = app('navigation')->endOfPeriod($end, $viewRange); + $start = app('navigation')->startOfPeriod($budgetLimit->start_date, $viewRange); + $end = app('navigation')->startOfPeriod($budgetLimit->end_date, $viewRange); + $end = app('navigation')->endOfPeriod($end, $viewRange); // limit period in total is: $limitPeriod = Period::make($start, $end, precision: Precision::DAY(), boundaries: Boundaries::EXCLUDE_NONE()); Log::debug(sprintf('Limit period is from %s to %s', $start->format('Y-m-d'), $end->format('Y-m-d'))); // from the start until the end of the budget limit, need to loop! - $current = clone $start; + $current = clone $start; while ($current <= $end) { - $currentEnd = app('navigation')->endOfPeriod($current, $viewRange); + $currentEnd = app('navigation')->endOfPeriod($current, $viewRange); // create or find AB for this particular period, and set the amount accordingly. /** @var null|AvailableBudget $availableBudget */ @@ -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)) { @@ -141,93 +227,7 @@ trait RecalculatesAvailableBudgetsTrait } // prep for next loop - $current = app('navigation')->addPeriod($current, $viewRange, 0); + $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; - } } diff --git a/app/Support/ParseDateString.php b/app/Support/ParseDateString.php index f2ab22a672..bd91585f8b 100644 --- a/app/Support/ParseDateString.php +++ b/app/Support/ParseDateString.php @@ -29,7 +29,6 @@ use Carbon\CarbonInterface; use Carbon\Exceptions\InvalidFormatException; use FireflyIII\Exceptions\FireflyException; use Illuminate\Support\Facades\Log; - use function Safe\preg_match; /** @@ -79,15 +78,15 @@ class ParseDateString public function parseDate(string $date): Carbon { Log::debug(sprintf('parseDate("%s")', $date)); - $date = strtolower($date); + $date = strtolower($date); // parse keywords: if (in_array($date, $this->keywords, true)) { return $this->parseKeyword($date); } // if regex for YYYY-MM-DD: - $pattern = '/^(19|20)\d\d-(0[1-9]|1[012])-(0[1-9]|[12]\d|3[01])$/'; - $result = preg_match($pattern, $date); + $pattern = '/^(19|20)\d\d-(0[1-9]|1[012])-(0[1-9]|[12]\d|3[01])$/'; + $result = preg_match($pattern, $date); if (0 !== $result) { return $this->parseDefaultDate($date); } @@ -114,99 +113,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 +182,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 +227,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 +257,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 +395,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 +409,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], ]; } } diff --git a/app/Support/Preferences.php b/app/Support/Preferences.php index 4a068cae06..f8fc423cc7 100644 --- a/app/Support/Preferences.php +++ b/app/Support/Preferences.php @@ -48,73 +48,19 @@ class Preferences } return Preference::where('user_id', $user->id) - ->where('name', '!=', 'currencyPreference') - ->where(function (Builder $q) use ($user): void { - $q->whereNull('user_group_id'); - $q->orWhere('user_group_id', $user->user_group_id); - }) - ->get() - ; + ->where('name', '!=', 'currencyPreference') + ->where(function (Builder $q) use ($user): void { + $q->whereNull('user_group_id'); + $q->orWhere('user_group_id', $user->user_group_id); + }) + ->get(); } - 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,58 +74,6 @@ class Preferences return true; } - public function forget(User $user, string $name): void - { - $key = sprintf('preference%s%s', $user->id, $name); - Cache::forget($key); - Cache::put($key, '', 5); - } - - 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; - } - - 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. */ @@ -188,17 +82,37 @@ class Preferences return Preference::where('name', $name)->get(); } + public function forget(User $user, string $name): void + { + $key = sprintf('preference%s%s', $user->id, $name); + Cache::forget($key); + Cache::put($key, '', 5); + } + + public function get(string $name, array | bool | int | string | null $default = null): ?Preference + { + /** @var null|User $user */ + $user = auth()->user(); + if (null === $user) { + $preference = new Preference(); + $preference->data = $default; + + return $preference; + } + + return $this->getForUser($user, $name, $default); + } + public function getArrayForUser(User $user, array $list): array { $result = []; $preferences = Preference::where('user_id', $user->id) - ->where(function (Builder $q) use ($user): void { - $q->whereNull('user_group_id'); - $q->orWhere('user_group_id', $user->user_group_id); - }) - ->whereIn('name', $list) - ->get(['id', 'name', 'data']) - ; + ->where(function (Builder $q) use ($user): void { + $q->whereNull('user_group_id'); + $q->orWhere('user_group_id', $user->user_group_id); + }) + ->whereIn('name', $list) + ->get(['id', 'name', 'data']); /** @var Preference $preference */ foreach ($preferences as $preference) { @@ -240,7 +154,7 @@ class Preferences return $result; } - public function getEncryptedForUser(User $user, string $name, array|bool|int|string|null $default = null): ?Preference + public function getEncryptedForUser(User $user, string $name, array | bool | int | string | null $default = null): ?Preference { $result = $this->getForUser($user, $name, $default); if ('' === $result->data) { @@ -265,7 +179,42 @@ class Preferences return $result; } - public function getFresh(string $name, array|bool|int|string|null $default = null): ?Preference + 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 */ $user = auth()->user(); @@ -284,8 +233,8 @@ class Preferences */ public function lastActivity(): string { - $instance = PreferencesSingleton::getInstance(); - $pref = $instance->getPreference('last_activity'); + $instance = PreferencesSingleton::getInstance(); + $pref = $instance->getPreference('last_activity'); if (null !== $pref) { // Log::debug(sprintf('Found last activity in singleton: %s', $pref)); return $pref; @@ -299,7 +248,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; @@ -313,7 +262,7 @@ class Preferences Session::forget('first'); } - public function set(string $name, array|bool|int|string|null $value): Preference + public function set(string $name, array | bool | int | string | null $value): Preference { /** @var null|User $user */ $user = auth()->user(); @@ -341,4 +290,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; + } } diff --git a/app/Support/Report/Budget/BudgetReportGenerator.php b/app/Support/Report/Budget/BudgetReportGenerator.php index c478e3cce4..b859847748 100644 --- a/app/Support/Report/Budget/BudgetReportGenerator.php +++ b/app/Support/Report/Budget/BudgetReportGenerator.php @@ -76,7 +76,7 @@ class BudgetReportGenerator /** @var Account $account */ foreach ($this->accounts as $account) { - $accountId = $account->id; + $accountId = $account->id; $this->report[$accountId] ??= [ 'name' => $account->name, 'id' => $account->id, @@ -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,175 +107,6 @@ class BudgetReportGenerator $this->percentageReport(); } - /** - * Start the budgets block on the default report by processing every budget. - */ - private function generalBudgetReport(): void - { - $budgetList = $this->repository->getBudgets(); - - /** @var Budget $budget */ - foreach ($budgetList as $budget) { - $this->processBudget($budget); - } - } - - /** - * Process expenses etc. for a single budget for the budgets block on the default report. - */ - private function processBudget(Budget $budget): void - { - $budgetId = $budget->id; - $this->report['budgets'][$budgetId] ??= [ - 'budget_id' => $budgetId, - 'budget_name' => $budget->name, - 'no_budget' => false, - 'budget_limits' => [], - ]; - - // get all budget limits for budget in period: - $limits = $this->blRepository->getBudgetLimits($budget, $this->start, $this->end); - - /** @var BudgetLimit $limit */ - foreach ($limits as $limit) { - $this->processLimit($budget, $limit); - } - } - - /** - * Process a single budget limit for the budgets block on the default report. - */ - private function processLimit(Budget $budget, BudgetLimit $limit): void - { - $budgetId = $budget->id; - $limitId = $limit->id; - $limitCurrency = $limit->transactionCurrency ?? $this->currency; - $currencyId = $limitCurrency->id; - $expenses = $this->opsRepository->sumExpenses($limit->start_date, $limit->end_date, $this->accounts, new Collection()->push($budget)); - $spent = $expenses[$currencyId]['sum'] ?? '0'; - $left = -1 === bccomp(bcadd($limit->amount, $spent), '0') ? '0' : bcadd($limit->amount, $spent); - $overspent = 1 === bccomp(bcmul($spent, '-1'), $limit->amount) ? bcadd($spent, $limit->amount) : '0'; - - $this->report['budgets'][$budgetId]['budget_limits'][$limitId] ??= [ - 'budget_limit_id' => $limitId, - 'start_date' => $limit->start_date, - 'end_date' => $limit->end_date, - 'budgeted' => $limit->amount, - 'budgeted_pct' => '0', - 'spent' => $spent, - 'spent_pct' => '0', - 'left' => $left, - 'overspent' => $overspent, - 'currency_id' => $currencyId, - 'currency_code' => $limitCurrency->code, - 'currency_name' => $limitCurrency->name, - 'currency_symbol' => $limitCurrency->symbol, - 'currency_decimal_places' => $limitCurrency->decimal_places, - ]; - - // make sum information: - $this->report['sums'][$currencyId] - ??= [ - 'budgeted' => '0', - 'spent' => '0', - 'left' => '0', - 'overspent' => '0', - 'currency_id' => $currencyId, - 'currency_code' => $limitCurrency->code, - 'currency_name' => $limitCurrency->name, - '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; @@ -349,4 +143,210 @@ class BudgetReportGenerator $this->nbRepository->setUser($user); $this->currency = app('amount')->getPrimaryCurrencyByUserGroup($user->userGroup); } + + /** + * Start the budgets block on the default report by processing every budget. + */ + private function generalBudgetReport(): void + { + $budgetList = $this->repository->getBudgets(); + + /** @var Budget $budget */ + foreach ($budgetList as $budget) { + $this->processBudget($budget); + } + } + + /** + * 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. + */ + private function processBudget(Budget $budget): void + { + $budgetId = $budget->id; + $this->report['budgets'][$budgetId] ??= [ + 'budget_id' => $budgetId, + 'budget_name' => $budget->name, + 'no_budget' => false, + 'budget_limits' => [], + ]; + + // get all budget limits for budget in period: + $limits = $this->blRepository->getBudgetLimits($budget, $this->start, $this->end); + + /** @var BudgetLimit $limit */ + foreach ($limits as $limit) { + $this->processLimit($budget, $limit); + } + } + + /** + * 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. + */ + private function processLimit(Budget $budget, BudgetLimit $limit): void + { + $budgetId = $budget->id; + $limitId = $limit->id; + $limitCurrency = $limit->transactionCurrency ?? $this->currency; + $currencyId = $limitCurrency->id; + $expenses = $this->opsRepository->sumExpenses($limit->start_date, $limit->end_date, $this->accounts, new Collection()->push($budget)); + $spent = $expenses[$currencyId]['sum'] ?? '0'; + $left = -1 === bccomp(bcadd($limit->amount, $spent), '0') ? '0' : bcadd($limit->amount, $spent); + $overspent = 1 === bccomp(bcmul($spent, '-1'), $limit->amount) ? bcadd($spent, $limit->amount) : '0'; + + $this->report['budgets'][$budgetId]['budget_limits'][$limitId] ??= [ + 'budget_limit_id' => $limitId, + 'start_date' => $limit->start_date, + 'end_date' => $limit->end_date, + 'budgeted' => $limit->amount, + 'budgeted_pct' => '0', + 'spent' => $spent, + 'spent_pct' => '0', + 'left' => $left, + 'overspent' => $overspent, + 'currency_id' => $currencyId, + 'currency_code' => $limitCurrency->code, + 'currency_name' => $limitCurrency->name, + 'currency_symbol' => $limitCurrency->symbol, + 'currency_decimal_places' => $limitCurrency->decimal_places, + ]; + + // make sum information: + $this->report['sums'][$currencyId] + ??= [ + 'budgeted' => '0', + 'spent' => '0', + 'left' => '0', + 'overspent' => '0', + 'currency_id' => $currencyId, + 'currency_code' => $limitCurrency->code, + 'currency_name' => $limitCurrency->name, + '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); + } } diff --git a/app/Support/Report/Category/CategoryReportGenerator.php b/app/Support/Report/Category/CategoryReportGenerator.php index 91f1470bb8..8f800d411d 100644 --- a/app/Support/Report/Category/CategoryReportGenerator.php +++ b/app/Support/Report/Category/CategoryReportGenerator.php @@ -62,17 +62,17 @@ class CategoryReportGenerator */ public function operations(): void { - $earnedWith = $this->opsRepository->listIncome($this->start, $this->end, $this->accounts); - $spentWith = $this->opsRepository->listExpenses($this->start, $this->end, $this->accounts); + $earnedWith = $this->opsRepository->listIncome($this->start, $this->end, $this->accounts); + $spentWith = $this->opsRepository->listExpenses($this->start, $this->end, $this->accounts); // also transferred out and transferred into these accounts in this category: $transferredIn = $this->opsRepository->listTransferredIn($this->start, $this->end, $this->accounts); $transferredOut = $this->opsRepository->listTransferredOut($this->start, $this->end, $this->accounts); - $earnedWithout = $this->noCatRepository->listIncome($this->start, $this->end, $this->accounts); - $spentWithout = $this->noCatRepository->listExpenses($this->start, $this->end, $this->accounts); + $earnedWithout = $this->noCatRepository->listIncome($this->start, $this->end, $this->accounts); + $spentWithout = $this->noCatRepository->listExpenses($this->start, $this->end, $this->accounts); - $this->report = [ + $this->report = [ 'categories' => [], 'sums' => [], ]; @@ -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); - } } diff --git a/app/Support/Report/Summarizer/TransactionSummarizer.php b/app/Support/Report/Summarizer/TransactionSummarizer.php index aea25a5663..84e0ec3231 100644 --- a/app/Support/Report/Summarizer/TransactionSummarizer.php +++ b/app/Support/Report/Summarizer/TransactionSummarizer.php @@ -43,26 +43,19 @@ 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))); $array = []; foreach ($journals as $journal) { - $field = 'amount'; + $field = 'amount'; // grab default currency information. - $currencyId = (int) $journal['currency_id']; - $currencyName = $journal['currency_name']; - $currencySymbol = $journal['currency_symbol']; - $currencyCode = $journal['currency_code']; - $currencyDecimalPlaces = $journal['currency_decimal_places']; + $currencyId = (int)$journal['currency_id']; + $currencyName = $journal['currency_name']; + $currencySymbol = $journal['currency_symbol']; + $currencyCode = $journal['currency_code']; + $currencyDecimalPlaces = $journal['currency_decimal_places']; // prepare foreign currency info: $foreignCurrencyId = 0; @@ -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, @@ -186,7 +179,7 @@ class TransactionSummarizer if ($convertToPrimary && $journal['currency_id'] !== $primary->id && $primary->id === $journal['foreign_currency_id']) { $field = 'foreign_amount'; } - $key = sprintf('%s-%s', $journal[$idKey], $currencyId); + $key = sprintf('%s-%s', $journal[$idKey], $currencyId); // sum it all up or create a new array. $array[$key] ??= [ 'id' => $journal[$idKey], @@ -200,15 +193,15 @@ 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] ??= [ + $array[$key] ??= [ 'id' => $journal[$idKey], 'name' => $journal[$nameKey], 'sum' => '0', @@ -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); + } } diff --git a/app/Support/Repositories/Recurring/CalculateRangeOccurrences.php b/app/Support/Repositories/Recurring/CalculateRangeOccurrences.php index 439fd1f50a..4ca2c8c29e 100644 --- a/app/Support/Repositories/Recurring/CalculateRangeOccurrences.php +++ b/app/Support/Repositories/Recurring/CalculateRangeOccurrences.php @@ -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(); @@ -82,8 +82,8 @@ trait CalculateRangeOccurrences */ protected function getNdomInRange(Carbon $start, Carbon $end, int $skipMod, string $moment): array { - $return = []; - $attempts = 0; + $return = []; + $attempts = 0; $start->startOfMonth(); // this feels a bit like a cop out but why reinvent the wheel? $counters = [1 => 'first', 2 => 'second', 3 => 'third', 4 => 'fourth', 5 => 'fifth']; @@ -108,12 +108,12 @@ trait CalculateRangeOccurrences */ protected function getWeeklyInRange(Carbon $start, Carbon $end, int $skipMod, string $moment): array { - $return = []; - $attempts = 0; + $return = []; + $attempts = 0; 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: @@ -154,8 +154,8 @@ trait CalculateRangeOccurrences } // is $date between $start and $end? - $obj = clone $date; - $count = 0; + $obj = clone $date; + $count = 0; while ($obj <= $end && $obj >= $start && $count < 10) { if (0 === $attempts % $skipMod) { $return[] = clone $obj; diff --git a/app/Support/Repositories/Recurring/CalculateXOccurrences.php b/app/Support/Repositories/Recurring/CalculateXOccurrences.php index 04f07046d4..602cb03d02 100644 --- a/app/Support/Repositories/Recurring/CalculateXOccurrences.php +++ b/app/Support/Repositories/Recurring/CalculateXOccurrences.php @@ -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(); @@ -89,10 +89,10 @@ trait CalculateXOccurrences */ protected function getXNDomOccurrences(Carbon $date, int $count, int $skipMod, string $moment): array { - $return = []; - $total = 0; - $attempts = 0; - $mutator = clone $date; + $return = []; + $total = 0; + $attempts = 0; + $mutator = clone $date; $mutator->addDay(); // always assume today has passed. $mutator->startOfMonth(); // this feels a bit like a cop out but why reinvent the wheel? @@ -120,14 +120,14 @@ trait CalculateXOccurrences */ protected function getXWeeklyOccurrences(Carbon $date, int $count, int $skipMod, string $moment): array { - $return = []; - $total = 0; - $attempts = 0; - $mutator = clone $date; + $return = []; + $total = 0; + $attempts = 0; + $mutator = clone $date; // 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(); @@ -164,7 +164,7 @@ trait CalculateXOccurrences if ($mutator > $date) { $date->addYear(); } - $obj = clone $date; + $obj = clone $date; while ($total < $count) { if (0 === $attempts % $skipMod) { $return[] = clone $obj; diff --git a/app/Support/Repositories/Recurring/CalculateXOccurrencesSince.php b/app/Support/Repositories/Recurring/CalculateXOccurrencesSince.php index 215c11bf0a..bd4b44fd7d 100644 --- a/app/Support/Repositories/Recurring/CalculateXOccurrencesSince.php +++ b/app/Support/Repositories/Recurring/CalculateXOccurrencesSince.php @@ -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)); @@ -87,7 +87,7 @@ trait CalculateXOccurrencesSince ++$total; } ++$attempts; - $mutator = $mutator->endOfMonth()->addDay(); + $mutator = $mutator->endOfMonth()->addDay(); } Log::debug('Collected enough occurrences.'); @@ -103,10 +103,10 @@ trait CalculateXOccurrencesSince protected function getXNDomOccurrencesSince(Carbon $date, Carbon $afterDate, int $count, int $skipMod, string $moment): array { Log::debug(sprintf('Now in %s', __METHOD__)); - $return = []; - $total = 0; - $attempts = 0; - $mutator = clone $date; + $return = []; + $total = 0; + $attempts = 0; + $mutator = clone $date; $mutator->addDay(); // always assume today has passed. $mutator->startOfMonth(); // this feels a bit like a cop out but why reinvent the wheel? @@ -137,15 +137,15 @@ trait CalculateXOccurrencesSince protected function getXWeeklyOccurrencesSince(Carbon $date, Carbon $afterDate, int $count, int $skipMod, string $moment): array { Log::debug(sprintf('Now in %s', __METHOD__)); - $return = []; - $total = 0; - $attempts = 0; - $mutator = clone $date; + $return = []; + $total = 0; + $attempts = 0; + $mutator = clone $date; // monday = 1 // 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(); @@ -189,7 +189,7 @@ trait CalculateXOccurrencesSince $date->addYear(); Log::debug(sprintf('Date is now %s', $date->format('Y-m-d'))); } - $obj = clone $date; + $obj = clone $date; while ($total < $count) { Log::debug(sprintf('total (%d) < count (%d) so go.', $total, $count)); Log::debug(sprintf('attempts (%d) %% skipmod (%d) === %d', $attempts, $skipMod, $attempts % $skipMod)); diff --git a/app/Support/Repositories/Recurring/FiltersWeekends.php b/app/Support/Repositories/Recurring/FiltersWeekends.php index 508e13f638..886679ced1 100644 --- a/app/Support/Repositories/Recurring/FiltersWeekends.php +++ b/app/Support/Repositories/Recurring/FiltersWeekends.php @@ -46,7 +46,7 @@ trait FiltersWeekends return $dates; } - $return = []; + $return = []; /** @var Carbon $date */ foreach ($dates as $date) { @@ -60,7 +60,7 @@ trait FiltersWeekends // is weekend and must set back to Friday? if (RecurrenceRepetitionWeekend::WEEKEND_TO_FRIDAY->value === $repetition->weekend) { - $clone = clone $date; + $clone = clone $date; $clone->addDays(5 - $date->dayOfWeekIso); Log::debug( sprintf('Date is %s, and this is in the weekend, so corrected to %s (Friday).', $date->format('D d M Y'), $clone->format('D d M Y')) @@ -72,7 +72,7 @@ trait FiltersWeekends // postpone to Monday? if (RecurrenceRepetitionWeekend::WEEKEND_TO_MONDAY->value === $repetition->weekend) { - $clone = clone $date; + $clone = clone $date; $clone->addDays(8 - $date->dayOfWeekIso); Log::debug( sprintf('Date is %s, and this is in the weekend, so corrected to %s (Monday).', $date->format('D d M Y'), $clone->format('D d M Y')) diff --git a/app/Support/Repositories/UserGroup/UserGroupInterface.php b/app/Support/Repositories/UserGroup/UserGroupInterface.php index d7a737b919..67e7fe3ea3 100644 --- a/app/Support/Repositories/UserGroup/UserGroupInterface.php +++ b/app/Support/Repositories/UserGroup/UserGroupInterface.php @@ -37,7 +37,7 @@ interface UserGroupInterface public function getUserGroup(): ?UserGroup; - public function setUser(Authenticatable|User|null $user): void; + public function setUser(Authenticatable | User | null $user): void; public function setUserGroup(UserGroup $userGroup): void; diff --git a/app/Support/Repositories/UserGroup/UserGroupTrait.php b/app/Support/Repositories/UserGroup/UserGroupTrait.php index 98781e5596..b6a1c94f5a 100644 --- a/app/Support/Repositories/UserGroup/UserGroupTrait.php +++ b/app/Support/Repositories/UserGroup/UserGroupTrait.php @@ -61,10 +61,10 @@ trait UserGroupTrait /** * @throws FireflyException */ - public function setUser(Authenticatable|User|null $user): void + public function setUser(Authenticatable | User | null $user): void { if ($user instanceof User) { - $this->user = $user; + $this->user = $user; if (null === $user->userGroup) { throw new FireflyException(sprintf('User #%d ("%s") has no user group.', $user->id, $user->email)); } @@ -99,15 +99,14 @@ trait UserGroupTrait public function setUserGroupById(int $userGroupId): void { $memberships = GroupMembership::where('user_id', $this->user->id) - ->where('user_group_id', $userGroupId) - ->count() - ; + ->where('user_group_id', $userGroupId) + ->count(); if (0 === $memberships) { throw new FireflyException(sprintf('User #%d has no access to administration #%d', $this->user->id, $userGroupId)); } /** @var null|UserGroup $userGroup */ - $userGroup = UserGroup::find($userGroupId); + $userGroup = UserGroup::find($userGroupId); if (null === $userGroup) { throw new FireflyException(sprintf('Cannot find administration for user #%d', $this->user->id)); } diff --git a/app/Support/Request/AppendsLocationData.php b/app/Support/Request/AppendsLocationData.php index 149898f986..01a6de40d3 100644 --- a/app/Support/Request/AppendsLocationData.php +++ b/app/Support/Request/AppendsLocationData.php @@ -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. */ @@ -82,12 +96,12 @@ trait AppendsLocationData $data['latitude'] = null; $data['zoom_level'] = null; - $longitudeKey = $this->getLocationKey($prefix, 'longitude'); - $latitudeKey = $this->getLocationKey($prefix, 'latitude'); - $zoomLevelKey = $this->getLocationKey($prefix, 'zoom_level'); - $isValidPOST = $this->isValidPost($prefix); - $isValidPUT = $this->isValidPUT($prefix); - $isValidEmptyPUT = $this->isValidEmptyPUT($prefix); + $longitudeKey = $this->getLocationKey($prefix, 'longitude'); + $latitudeKey = $this->getLocationKey($prefix, 'latitude'); + $zoomLevelKey = $this->getLocationKey($prefix, 'zoom_level'); + $isValidPOST = $this->isValidPost($prefix); + $isValidPUT = $this->isValidPUT($prefix); + $isValidEmptyPUT = $this->isValidEmptyPUT($prefix); // for a POST (store), all fields must be present and not NULL. if ($isValidPOST) { @@ -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; } } diff --git a/app/Support/Request/ChecksLogin.php b/app/Support/Request/ChecksLogin.php index 9fd2e11883..8576b17473 100644 --- a/app/Support/Request/ChecksLogin.php +++ b/app/Support/Request/ChecksLogin.php @@ -40,7 +40,7 @@ trait ChecksLogin { app('log')->debug(sprintf('Now in %s', __METHOD__)); // Only allow logged-in users - $check = auth()->check(); + $check = auth()->check(); if (!$check) { return false; } @@ -79,19 +79,19 @@ trait ChecksLogin public function getUserGroup(): ?UserGroup { /** @var User $user */ - $user = auth()->user(); + $user = auth()->user(); app('log')->debug('Now in getUserGroup()'); /** @var null|UserGroup $userGroup */ $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); + $userGroup = UserGroup::find($userGroupId); if (null === $userGroup) { app('log')->error(sprintf('Request class has user_group_id (#%d), but group does not exist.', $userGroupId)); diff --git a/app/Support/Request/ConvertsDataTypes.php b/app/Support/Request/ConvertsDataTypes.php index bc8da96efb..aa3896eb72 100644 --- a/app/Support/Request/ConvertsDataTypes.php +++ b/app/Support/Request/ConvertsDataTypes.php @@ -31,7 +31,6 @@ use FireflyIII\Repositories\Account\AccountRepositoryInterface; use FireflyIII\Support\Facades\Steam; use Illuminate\Support\Collection; use Illuminate\Support\Facades\Log; - use function Safe\preg_replace; /** @@ -99,28 +98,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 +136,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 +185,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 +217,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 +395,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 +417,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. */ @@ -445,7 +459,7 @@ trait ConvertsDataTypes if (!is_array($entry)) { continue; } - $amount = null; + $amount = null; if (array_key_exists('current_amount', $entry)) { $amount = $this->clearString((string)($entry['current_amount'] ?? '0')); if (null === $entry['current_amount']) { @@ -463,19 +477,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; - } } diff --git a/app/Support/Request/GetRecurrenceData.php b/app/Support/Request/GetRecurrenceData.php index 50dc0cd8f2..40f23738dd 100644 --- a/app/Support/Request/GetRecurrenceData.php +++ b/app/Support/Request/GetRecurrenceData.php @@ -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) { diff --git a/app/Support/Request/ValidatesWebhooks.php b/app/Support/Request/ValidatesWebhooks.php index 5647184ef4..dff1541fde 100644 --- a/app/Support/Request/ValidatesWebhooks.php +++ b/app/Support/Request/ValidatesWebhooks.php @@ -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 { @@ -40,9 +40,9 @@ trait ValidatesWebhooks if (count($validator->failed()) > 0) { return; } - $data = $validator->getData(); - $triggers = $data['triggers'] ?? []; - $responses = $data['responses'] ?? []; + $data = $validator->getData(); + $triggers = $data['triggers'] ?? []; + $responses = $data['responses'] ?? []; if (0 === count($triggers) || 0 === count($responses)) { Log::debug('No trigger or response, return.'); diff --git a/app/Support/Search/AccountSearch.php b/app/Support/Search/AccountSearch.php index 99b89f9c99..fe6e817c72 100644 --- a/app/Support/Search/AccountSearch.php +++ b/app/Support/Search/AccountSearch.php @@ -28,7 +28,6 @@ use FireflyIII\User; use Illuminate\Contracts\Auth\Authenticatable; use Illuminate\Database\Eloquent\Builder; use Illuminate\Support\Collection; - use function Safe\json_encode; /** @@ -37,16 +36,16 @@ use function Safe\json_encode; class AccountSearch implements GenericSearchInterface { /** @var string */ - public const string SEARCH_ALL = 'all'; + public const string SEARCH_ALL = 'all'; /** @var string */ - public const string SEARCH_IBAN = 'iban'; + public const string SEARCH_IBAN = 'iban'; /** @var string */ - public const string SEARCH_ID = 'id'; + public const string SEARCH_ID = 'id'; /** @var string */ - public const string SEARCH_NAME = 'name'; + public const string SEARCH_NAME = 'name'; /** @var string */ public const string SEARCH_NUMBER = 'number'; @@ -63,10 +62,9 @@ class AccountSearch implements GenericSearchInterface public function search(): Collection { $searchQuery = $this->user->accounts() - ->leftJoin('account_types', 'accounts.account_type_id', '=', 'account_types.id') - ->leftJoin('account_meta', 'accounts.id', '=', 'account_meta.account_id') - ->whereIn('account_types.type', $this->types) - ; + ->leftJoin('account_types', 'accounts.account_type_id', '=', 'account_types.id') + ->leftJoin('account_meta', 'accounts.id', '=', 'account_meta.account_id') + ->whereIn('account_types.type', $this->types); $like = sprintf('%%%s%%', $this->query); $originalQuery = $this->query; @@ -92,7 +90,7 @@ class AccountSearch implements GenericSearchInterface break; case self::SEARCH_ID: - $searchQuery->where('accounts.id', '=', (int) $originalQuery); + $searchQuery->where('accounts.id', '=', (int)$originalQuery); break; @@ -137,7 +135,7 @@ class AccountSearch implements GenericSearchInterface $this->types = $types; } - public function setUser(Authenticatable|User|null $user): void + public function setUser(Authenticatable | User | null $user): void { if ($user instanceof User) { $this->user = $user; diff --git a/app/Support/Search/OperatorQuerySearch.php b/app/Support/Search/OperatorQuerySearch.php index dc975609f0..2e813bd125 100644 --- a/app/Support/Search/OperatorQuerySearch.php +++ b/app/Support/Search/OperatorQuerySearch.php @@ -105,6 +105,41 @@ class OperatorQuerySearch implements SearchInterface $this->currencyRepository = app(CurrencyRepositoryInterface::class); } + /** + * @throws FireflyException + */ + public static function getRootOperator(string $operator): string + { + $original = $operator; + // if the string starts with "-" (not), we can remove it and recycle + // the configuration from the original operator. + if (str_starts_with($operator, '-')) { + $operator = substr($operator, 1); + } + + $config = config(sprintf('search.operators.%s', $operator)); + if (null === $config) { + throw new FireflyException(sprintf('No configuration for search operator "%s"', $operator)); + } + if (true === $config['alias']) { + $return = $config['alias_for']; + if (str_starts_with($original, '-')) { + $return = sprintf('-%s', $config['alias_for']); + } + Log::debug(sprintf('"%s" is an alias for "%s", so return that instead.', $original, $return)); + + return $return; + } + Log::debug(sprintf('"%s" is not an alias.', $operator)); + + return $original; + } + + public function getExcludedWords(): array + { + return $this->prohibitedWords; + } + public function getInvalidOperators(): array { return $this->invalidOperators; @@ -120,6 +155,11 @@ class OperatorQuerySearch implements SearchInterface return $this->operators; } + public function getWords(): array + { + return $this->words; + } + public function getWordsAsString(): string { return implode(' ', $this->words); @@ -146,7 +186,7 @@ class OperatorQuerySearch implements SearchInterface try { $parsedQuery = $parser->parse($query); - } catch (LogicException|TypeError $e) { + } catch (LogicException | TypeError $e) { Log::error($e->getMessage()); Log::error(sprintf('Could not parse search: "%s".', $query)); @@ -163,6 +203,124 @@ class OperatorQuerySearch implements SearchInterface $this->collector->excludeSearchWords($this->prohibitedWords); } + public function searchTime(): float + { + return microtime(true) - $this->startTime; + } + + public function searchTransactions(): LengthAwarePaginator + { + $this->parseTagInstructions(); + if (0 === count($this->getWords()) && 0 === count($this->getExcludedWords()) && 0 === count($this->getOperators())) { + return new LengthAwarePaginator([], 0, 5, 1); + } + + return $this->collector->getPaginatedGroups(); + } + + public function setDate(Carbon $date): void + { + $this->date = $date; + } + + public function setLimit(int $limit): void + { + $this->limit = $limit; + $this->collector->setLimit($this->limit); + } + + public function setPage(int $page): void + { + $this->page = $page; + $this->collector->setPage($this->page); + } + + public function setUser(User $user): void + { + $this->accountRepository->setUser($user); + $this->billRepository->setUser($user); + $this->categoryRepository->setUser($user); + $this->budgetRepository->setUser($user); + $this->tagRepository->setUser($user); + $this->collector = app(GroupCollectorInterface::class); + $this->collector->setUser($user); + $this->collector->withAccountInformation()->withCategoryInformation()->withBudgetInformation(); + + $this->setLimit((int)app('preferences')->getForUser($user, 'listPageSize', 50)->data); + } + + private function findCurrency(string $value): ?TransactionCurrency + { + if (str_contains($value, '(') && str_contains($value, ')')) { + // bad method to split and get the currency code: + $parts = explode(' ', $value); + $value = trim($parts[count($parts) - 1], "() \t\n\r\0\x0B"); + } + $result = $this->currencyRepository->findByCode($value); + if (null === $result) { + return $this->currencyRepository->findByName($value); + } + + return $result; + } + + private function getCashAccount(): Account + { + return $this->accountRepository->getCashAccount(); + } + + /** + * @throws FireflyException + */ + private function handleFieldNode(FieldNode $node, bool $flipProhibitedFlag): void + { + $operator = strtolower($node->getOperator()); + $value = $node->getValue(); + $prohibited = $node->isProhibited($flipProhibitedFlag); + + $context = config(sprintf('search.operators.%s.needs_context', $operator)); + + // is an operator that needs no context, and value is false, then prohibited = true. + if ('false' === $value && in_array($operator, $this->validOperators, true) && false === $context && !$prohibited) { + $prohibited = true; + $value = 'true'; + } + // if the operator is prohibited, but the value is false, do an uno reverse + if ('false' === $value && $prohibited && in_array($operator, $this->validOperators, true) && false === $context) { + $prohibited = false; + $value = 'true'; + } + + // must be valid operator: + $inArray = in_array($operator, $this->validOperators, true); + if ($inArray) { + if ($this->updateCollector($operator, $value, $prohibited)) { + $this->operators->push([ + 'type' => self::getRootOperator($operator), + 'value' => $value, + 'prohibited' => $prohibited, + ]); + Log::debug(sprintf('Added operator type "%s"', $operator)); + } + } + if (!$inArray) { + Log::debug(sprintf('Added INVALID operator type "%s"', $operator)); + $this->invalidOperators[] = [ + 'type' => $operator, + 'value' => $value, + ]; + } + } + + private function handleNodeGroup(NodeGroup $node, bool $flipProhibitedFlag): void + { + $prohibited = $node->isProhibited($flipProhibitedFlag); + + foreach ($node->getNodes() as $subNode) { + $this->handleSearchNode($subNode, $prohibited); + } + } + /** * @throws FireflyException * @@ -197,7 +355,7 @@ class OperatorQuerySearch implements SearchInterface private function handleStringNode(StringNode $node, bool $flipProhibitedFlag): void { - $string = $node->getValue(); + $string = $node->getValue(); $prohibited = $node->isProhibited($flipProhibitedFlag); @@ -214,43 +372,857 @@ class OperatorQuerySearch implements SearchInterface /** * @throws FireflyException */ - private function handleFieldNode(FieldNode $node, bool $flipProhibitedFlag): void + private function parseDateRange(string $type, string $value): array { - $operator = strtolower($node->getOperator()); - $value = $node->getValue(); - $prohibited = $node->isProhibited($flipProhibitedFlag); - - $context = config(sprintf('search.operators.%s.needs_context', $operator)); - - // is an operator that needs no context, and value is false, then prohibited = true. - if ('false' === $value && in_array($operator, $this->validOperators, true) && false === $context && !$prohibited) { - $prohibited = true; - $value = 'true'; - } - // if the operator is prohibited, but the value is false, do an uno reverse - if ('false' === $value && $prohibited && in_array($operator, $this->validOperators, true) && false === $context) { - $prohibited = false; - $value = 'true'; + $parser = new ParseDateString(); + if ($parser->isDateRange($value)) { + return $parser->parseRange($value); } - // must be valid operator: - $inArray = in_array($operator, $this->validOperators, true); - if ($inArray) { - if ($this->updateCollector($operator, $value, $prohibited)) { - $this->operators->push([ - 'type' => self::getRootOperator($operator), - 'value' => $value, - 'prohibited' => $prohibited, - ]); - Log::debug(sprintf('Added operator type "%s"', $operator)); - } - } - if (!$inArray) { - Log::debug(sprintf('Added INVALID operator type "%s"', $operator)); + try { + $parsedDate = $parser->parseDate($value); + } catch (FireflyException) { + Log::debug(sprintf('Could not parse date "%s", will return empty array.', $value)); $this->invalidOperators[] = [ - 'type' => $operator, + 'type' => $type, 'value' => $value, ]; + + return []; + } + + return [ + 'exact' => $parsedDate, + ]; + } + + private function parseTagInstructions(): void + { + Log::debug('Now in parseTagInstructions()'); + // if exclude tags, remove excluded tags. + if (count($this->excludeTags) > 0) { + Log::debug(sprintf('%d exclude tag(s)', count($this->excludeTags))); + $collection = new Collection(); + foreach ($this->excludeTags as $tagId) { + $tag = $this->tagRepository->find($tagId); + if (null !== $tag) { + Log::debug(sprintf('Exclude tag "%s"', $tag->tag)); + $collection->push($tag); + } + } + Log::debug(sprintf('Selecting all tags except %d excluded tag(s).', $collection->count())); + $this->collector->setWithoutSpecificTags($collection); + } + // if include tags, include them: + if (count($this->includeTags) > 0) { + Log::debug(sprintf('%d include tag(s)', count($this->includeTags))); + $collection = new Collection(); + foreach ($this->includeTags as $tagId) { + $tag = $this->tagRepository->find($tagId); + if (null !== $tag) { + Log::debug(sprintf('Include tag "%s"', $tag->tag)); + $collection->push($tag); + } + } + $this->collector->setAllTags($collection); + } + // if include ANY tags, include them: (see #8632) + if (count($this->includeAnyTags) > 0) { + Log::debug(sprintf('%d include ANY tag(s)', count($this->includeAnyTags))); + $collection = new Collection(); + foreach ($this->includeAnyTags as $tagId) { + $tag = $this->tagRepository->find($tagId); + if (null !== $tag) { + Log::debug(sprintf('Include ANY tag "%s"', $tag->tag)); + $collection->push($tag); + } + } + $this->collector->setTags($collection); + } + } + + /** + * searchDirection: 1 = source (default), 2 = destination, 3 = both + * stringPosition: 1 = start (default), 2 = end, 3 = contains, 4 = is + * + * @SuppressWarnings("PHPMD.BooleanArgumentFlag") + * @SuppressWarnings("PHPMD.NPathComplexity") + */ + private function searchAccount(string $value, SearchDirection $searchDirection, StringPosition $stringPosition, bool $prohibited = false): void + { + Log::debug(sprintf('searchAccount("%s", %s, %s)', $value, $stringPosition->name, $searchDirection->name)); + + // search direction (default): for source accounts + $searchTypes = [AccountTypeEnum::ASSET->value, AccountTypeEnum::MORTGAGE->value, AccountTypeEnum::LOAN->value, AccountTypeEnum::DEBT->value, AccountTypeEnum::REVENUE->value]; + $collectorMethod = 'setSourceAccounts'; + if ($prohibited) { + $collectorMethod = 'excludeSourceAccounts'; + } + + // search direction: for destination accounts + if (SearchDirection::DESTINATION === $searchDirection) { // destination + // destination can be + $searchTypes = [AccountTypeEnum::ASSET->value, AccountTypeEnum::MORTGAGE->value, AccountTypeEnum::LOAN->value, AccountTypeEnum::DEBT->value, AccountTypeEnum::EXPENSE->value]; + $collectorMethod = 'setDestinationAccounts'; + if ($prohibited) { + $collectorMethod = 'excludeDestinationAccounts'; + } + } + // either account could be: + if (SearchDirection::BOTH === $searchDirection) { + $searchTypes = [AccountTypeEnum::ASSET->value, AccountTypeEnum::MORTGAGE->value, AccountTypeEnum::LOAN->value, AccountTypeEnum::DEBT->value, AccountTypeEnum::EXPENSE->value, AccountTypeEnum::REVENUE->value]; + $collectorMethod = 'setAccounts'; + if ($prohibited) { + $collectorMethod = 'excludeAccounts'; + } + } + // string position (default): starts with: + $stringMethod = 'str_starts_with'; + + // string position: ends with: + if (StringPosition::ENDS === $stringPosition) { + $stringMethod = 'str_ends_with'; + } + if (StringPosition::CONTAINS === $stringPosition) { + $stringMethod = 'str_contains'; + } + if (StringPosition::IS === $stringPosition) { + $stringMethod = 'stringIsEqual'; + } + + // get accounts: + $accounts = $this->accountRepository->searchAccount($value, $searchTypes, 1337); + if (0 === $accounts->count() && false === $prohibited) { + Log::warning('Found zero accounts, search for non existing account, NO results will be returned.'); + $this->collector->findNothing(); + + return; + } + if (0 === $accounts->count() && true === $prohibited) { + Log::debug('Found zero accounts, but the search is negated, so effectively we ignore the search parameter.'); + + return; + } + Log::debug(sprintf('Found %d accounts, will filter.', $accounts->count())); + $filtered = $accounts->filter( + static fn(Account $account) => $stringMethod(strtolower($account->name), strtolower($value)) + ); + + if (0 === $filtered->count()) { + Log::warning('Left with zero accounts, so cannot find anything, NO results will be returned.'); + $this->collector->findNothing(); + + return; + } + Log::debug(sprintf('Left with %d, set as %s().', $filtered->count(), $collectorMethod)); + $this->collector->{$collectorMethod}($filtered); // @phpstan-ignore-line + } + + /** + * TODO make enums + * searchDirection: 1 = source (default), 2 = destination, 3 = both + * stringPosition: 1 = start (default), 2 = end, 3 = contains, 4 = is + * + * @SuppressWarnings("PHPMD.BooleanArgumentFlag") + * @SuppressWarnings("PHPMD.NPathComplexity") + */ + private function searchAccountNr(string $value, SearchDirection $searchDirection, StringPosition $stringPosition, bool $prohibited = false): void + { + Log::debug(sprintf('searchAccountNr(%s, %d, %d)', $value, $searchDirection->name, $stringPosition->name)); + + // search direction (default): for source accounts + $searchTypes = [AccountTypeEnum::ASSET->value, AccountTypeEnum::MORTGAGE->value, AccountTypeEnum::LOAN->value, AccountTypeEnum::DEBT->value, AccountTypeEnum::REVENUE->value]; + $collectorMethod = 'setSourceAccounts'; + if (true === $prohibited) { + $collectorMethod = 'excludeSourceAccounts'; + } + + // search direction: for destination accounts + if (SearchDirection::DESTINATION === $searchDirection) { + // destination can be + $searchTypes = [AccountTypeEnum::ASSET->value, AccountTypeEnum::MORTGAGE->value, AccountTypeEnum::LOAN->value, AccountTypeEnum::DEBT->value, AccountTypeEnum::EXPENSE->value]; + $collectorMethod = 'setDestinationAccounts'; + if (true === $prohibited) { + $collectorMethod = 'excludeDestinationAccounts'; + } + } + + // either account could be: + if (SearchDirection::BOTH === $searchDirection) { + $searchTypes = [AccountTypeEnum::ASSET->value, AccountTypeEnum::MORTGAGE->value, AccountTypeEnum::LOAN->value, AccountTypeEnum::DEBT->value, AccountTypeEnum::EXPENSE->value, AccountTypeEnum::REVENUE->value]; + $collectorMethod = 'setAccounts'; + if (true === $prohibited) { + $collectorMethod = 'excludeAccounts'; + } + } + + // string position (default): starts with: + $stringMethod = 'str_starts_with'; + + // string position: ends with: + if (StringPosition::ENDS === $stringPosition) { + $stringMethod = 'str_ends_with'; + } + if (StringPosition::CONTAINS === $stringPosition) { + $stringMethod = 'str_contains'; + } + if (StringPosition::IS === $stringPosition) { + $stringMethod = 'stringIsEqual'; + } + + // search for accounts: + $accounts = $this->accountRepository->searchAccountNr($value, $searchTypes, 1337); + if (0 === $accounts->count()) { + Log::debug('Found zero accounts, search for invalid account.'); + Log::warning('Call to findNothing() from searchAccountNr().'); + $this->collector->findNothing(); + + return; + } + + // if found, do filter + Log::debug(sprintf('Found %d accounts, will filter.', $accounts->count())); + $filtered = $accounts->filter( + static function (Account $account) use ($value, $stringMethod) { + // either IBAN or account number + $ibanMatch = $stringMethod(strtolower((string)$account->iban), strtolower($value)); + $accountNrMatch = false; + + /** @var AccountMeta $meta */ + foreach ($account->accountMeta as $meta) { + if ('account_number' === $meta->name && $stringMethod(strtolower((string)$meta->data), strtolower($value))) { + $accountNrMatch = true; + } + } + + return $ibanMatch || $accountNrMatch; + } + ); + + if (0 === $filtered->count()) { + Log::debug('Left with zero, search for invalid account'); + Log::warning('Call to findNothing() from searchAccountNr().'); + $this->collector->findNothing(); + + return; + } + Log::debug(sprintf('Left with %d, set as %s().', $filtered->count(), $collectorMethod)); + $this->collector->{$collectorMethod}($filtered); // @phpstan-ignore-line + } + + /** + * @throws FireflyException + * + * @SuppressWarnings("PHPMD.BooleanArgumentFlag") + */ + private function setDateAfterParams(array $range, bool $prohibited = false): void + { + /** + * @var string $key + * @var Carbon|string $value + */ + foreach ($range as $key => $value) { + $key = $prohibited ? sprintf('%s_not', $key) : $key; + + switch ($key) { + default: + throw new FireflyException(sprintf('Cannot handle key "%s" in setDateAfterParams()', $key)); + + case 'exact': + if ($value instanceof Carbon) { + $this->collector->setAfter($value); + $this->operators->push(['type' => 'date_after', 'value' => $value->format('Y-m-d')]); + } + + break; + + case 'year': + if (is_string($value)) { + Log::debug(sprintf('Set date_is_after YEAR value "%s"', $value)); + $this->collector->yearAfter($value); + $this->operators->push(['type' => 'date_after_year', 'value' => $value]); + } + + break; + + case 'month': + if (is_string($value)) { + Log::debug(sprintf('Set date_is_after MONTH value "%s"', $value)); + $this->collector->monthAfter($value); + $this->operators->push(['type' => 'date_after_month', 'value' => $value]); + } + + break; + + case 'day': + if (is_string($value)) { + Log::debug(sprintf('Set date_is_after DAY value "%s"', $value)); + $this->collector->dayAfter($value); + $this->operators->push(['type' => 'date_after_day', 'value' => $value]); + } + + break; + } + } + } + + /** + * @throws FireflyException + * + * @SuppressWarnings("PHPMD.BooleanArgumentFlag") + */ + private function setDateBeforeParams(array $range, bool $prohibited = false): void + { + /** + * @var string $key + * @var Carbon|string $value + */ + foreach ($range as $key => $value) { + $key = $prohibited ? sprintf('%s_not', $key) : $key; + + switch ($key) { + default: + throw new FireflyException(sprintf('Cannot handle key "%s" in setDateBeforeParams()', $key)); + + case 'exact': + if ($value instanceof Carbon) { + $this->collector->setBefore($value); + $this->operators->push(['type' => 'date_before', 'value' => $value->format('Y-m-d')]); + } + + break; + + case 'year': + if (is_string($value)) { + Log::debug(sprintf('Set date_is_before YEAR value "%s"', $value)); + $this->collector->yearBefore($value); + $this->operators->push(['type' => 'date_before_year', 'value' => $value]); + } + + break; + + case 'month': + if (is_string($value)) { + Log::debug(sprintf('Set date_is_before MONTH value "%s"', $value)); + $this->collector->monthBefore($value); + $this->operators->push(['type' => 'date_before_month', 'value' => $value]); + } + + break; + + case 'day': + if (is_string($value)) { + Log::debug(sprintf('Set date_is_before DAY value "%s"', $value)); + $this->collector->dayBefore($value); + $this->operators->push(['type' => 'date_before_day', 'value' => $value]); + } + + break; + } + } + } + + /** + * @throws FireflyException + * + * @SuppressWarnings("PHPMD.BooleanArgumentFlag") + */ + private function setExactDateParams(array $range, bool $prohibited = false): void + { + /** + * @var string $key + * @var Carbon|string $value + */ + foreach ($range as $key => $value) { + $key = $prohibited ? sprintf('%s_not', $key) : $key; + + switch ($key) { + default: + throw new FireflyException(sprintf('Cannot handle key "%s" in setExactParameters()', $key)); + + case 'exact': + if ($value instanceof Carbon) { + Log::debug(sprintf('Set date_is_exact value "%s"', $value->format('Y-m-d'))); + $this->collector->setRange($value, $value); + $this->operators->push(['type' => 'date_on', 'value' => $value->format('Y-m-d')]); + } + + break; + + case 'exact_not': + if ($value instanceof Carbon) { + $this->collector->excludeRange($value, $value); + $this->operators->push(['type' => 'not_date_on', 'value' => $value->format('Y-m-d')]); + } + + break; + + case 'year': + if (is_string($value)) { + Log::debug(sprintf('Set date_is_exact YEAR value "%s"', $value)); + $this->collector->yearIs($value); + $this->operators->push(['type' => 'date_on_year', 'value' => $value]); + } + + break; + + case 'year_not': + if (is_string($value)) { + Log::debug(sprintf('Set date_is_exact_not YEAR value "%s"', $value)); + $this->collector->yearIsNot($value); + $this->operators->push(['type' => 'not_date_on_year', 'value' => $value]); + } + + break; + + case 'month': + if (is_string($value)) { + Log::debug(sprintf('Set date_is_exact MONTH value "%s"', $value)); + $this->collector->monthIs($value); + $this->operators->push(['type' => 'date_on_month', 'value' => $value]); + } + + break; + + case 'month_not': + if (is_string($value)) { + Log::debug(sprintf('Set date_is_exact not MONTH value "%s"', $value)); + $this->collector->monthIsNot($value); + $this->operators->push(['type' => 'not_date_on_month', 'value' => $value]); + } + + break; + + case 'day': + if (is_string($value)) { + Log::debug(sprintf('Set date_is_exact DAY value "%s"', $value)); + $this->collector->dayIs($value); + $this->operators->push(['type' => 'date_on_day', 'value' => $value]); + } + + break; + + case 'day_not': + if (is_string($value)) { + Log::debug(sprintf('Set not date_is_exact DAY value "%s"', $value)); + $this->collector->dayIsNot($value); + $this->operators->push(['type' => 'not_date_on_day', 'value' => $value]); + } + + break; + } + } + } + + /** + * @throws FireflyException + * + * @SuppressWarnings("PHPMD.BooleanArgumentFlag") + */ + private function setExactMetaDateParams(string $field, array $range, bool $prohibited = false): void + { + Log::debug('Now in setExactMetaDateParams()'); + + /** + * @var string $key + * @var Carbon|string $value + */ + foreach ($range as $key => $value) { + $key = $prohibited ? sprintf('%s_not', $key) : $key; + + switch ($key) { + default: + throw new FireflyException(sprintf('Cannot handle key "%s" in setExactMetaDateParams()', $key)); + + case 'exact': + if ($value instanceof Carbon) { + Log::debug(sprintf('Set %s_is_exact value "%s"', $field, $value->format('Y-m-d'))); + $this->collector->setMetaDateRange($value, $value, $field); + $this->operators->push(['type' => sprintf('%s_on', $field), 'value' => $value->format('Y-m-d')]); + } + + break; + + case 'exact_not': + if ($value instanceof Carbon) { + Log::debug(sprintf('Set NOT %s_is_exact value "%s"', $field, $value->format('Y-m-d'))); + $this->collector->excludeMetaDateRange($value, $value, $field); + $this->operators->push(['type' => sprintf('not_%s_on', $field), 'value' => $value->format('Y-m-d')]); + } + + break; + + case 'year': + if (is_string($value)) { + Log::debug(sprintf('Set %s_is_exact YEAR value "%s"', $field, $value)); + $this->collector->metaYearIs($value, $field); + $this->operators->push(['type' => sprintf('%s_on_year', $field), 'value' => $value]); + } + + break; + + case 'year_not': + if (is_string($value)) { + Log::debug(sprintf('Set NOT %s_is_exact YEAR value "%s"', $field, $value)); + $this->collector->metaYearIsNot($value, $field); + $this->operators->push(['type' => sprintf('not_%s_on_year', $field), 'value' => $value]); + } + + break; + + case 'month': + if (is_string($value)) { + Log::debug(sprintf('Set %s_is_exact MONTH value "%s"', $field, $value)); + $this->collector->metaMonthIs($value, $field); + $this->operators->push(['type' => sprintf('%s_on_month', $field), 'value' => $value]); + } + + break; + + case 'month_not': + if (is_string($value)) { + Log::debug(sprintf('Set NOT %s_is_exact MONTH value "%s"', $field, $value)); + $this->collector->metaMonthIsNot($value, $field); + $this->operators->push(['type' => sprintf('not_%s_on_month', $field), 'value' => $value]); + } + + break; + + case 'day': + if (is_string($value)) { + Log::debug(sprintf('Set %s_is_exact DAY value "%s"', $field, $value)); + $this->collector->metaDayIs($value, $field); + $this->operators->push(['type' => sprintf('%s_on_day', $field), 'value' => $value]); + } + + break; + + case 'day_not': + if (is_string($value)) { + Log::debug(sprintf('Set NOT %s_is_exact DAY value "%s"', $field, $value)); + $this->collector->metaDayIsNot($value, $field); + $this->operators->push(['type' => sprintf('not_%s_on_day', $field), 'value' => $value]); + } + + break; + } + } + } + + /** + * @throws FireflyException + * + * @SuppressWarnings("PHPMD.BooleanArgumentFlag") + */ + private function setExactObjectDateParams(string $field, array $range, bool $prohibited = false): void + { + /** + * @var string $key + * @var Carbon|string $value + */ + foreach ($range as $key => $value) { + $key = $prohibited ? sprintf('%s_not', $key) : $key; + + switch ($key) { + default: + throw new FireflyException(sprintf('Cannot handle key "%s" in setExactObjectDateParams()', $key)); + + case 'exact': + if ($value instanceof Carbon) { + Log::debug(sprintf('Set %s_is_exact value "%s"', $field, $value->format('Y-m-d'))); + $this->collector->setObjectRange($value, clone $value, $field); + $this->operators->push(['type' => sprintf('%s_on', $field), 'value' => $value->format('Y-m-d')]); + } + + break; + + case 'exact_not': + if ($value instanceof Carbon) { + Log::debug(sprintf('Set NOT %s_is_exact value "%s"', $field, $value->format('Y-m-d'))); + $this->collector->excludeObjectRange($value, clone $value, $field); + $this->operators->push(['type' => sprintf('not_%s_on', $field), 'value' => $value->format('Y-m-d')]); + } + + break; + + case 'year': + if (is_string($value)) { + Log::debug(sprintf('Set %s_is_exact YEAR value "%s"', $field, $value)); + $this->collector->objectYearIs($value, $field); + $this->operators->push(['type' => sprintf('%s_on_year', $field), 'value' => $value]); + } + + break; + + case 'year_not': + if (is_string($value)) { + Log::debug(sprintf('Set NOT %s_is_exact YEAR value "%s"', $field, $value)); + $this->collector->objectYearIsNot($value, $field); + $this->operators->push(['type' => sprintf('not_%s_on_year', $field), 'value' => $value]); + } + + break; + + case 'month': + if (is_string($value)) { + Log::debug(sprintf('Set %s_is_exact MONTH value "%s"', $field, $value)); + $this->collector->objectMonthIs($value, $field); + $this->operators->push(['type' => sprintf('%s_on_month', $field), 'value' => $value]); + } + + break; + + case 'month_not': + if (is_string($value)) { + Log::debug(sprintf('Set NOT %s_is_exact MONTH value "%s"', $field, $value)); + $this->collector->objectMonthIsNot($value, $field); + $this->operators->push(['type' => sprintf('not_%s_on_month', $field), 'value' => $value]); + } + + break; + + case 'day': + if (is_string($value)) { + Log::debug(sprintf('Set %s_is_exact DAY value "%s"', $field, $value)); + $this->collector->objectDayIs($value, $field); + $this->operators->push(['type' => sprintf('%s_on_day', $field), 'value' => $value]); + } + + break; + + case 'day_not': + if (is_string($value)) { + Log::debug(sprintf('Set NOT %s_is_exact DAY value "%s"', $field, $value)); + $this->collector->objectDayIsNot($value, $field); + $this->operators->push(['type' => sprintf('not_%s_on_day', $field), 'value' => $value]); + } + + break; + } + } + } + + /** + * @throws FireflyException + * + * @SuppressWarnings("PHPMD.BooleanArgumentFlag") + */ + private function setMetaDateAfterParams(string $field, array $range, bool $prohibited = false): void + { + /** + * @var string $key + * @var Carbon|string $value + */ + foreach ($range as $key => $value) { + $key = $prohibited ? sprintf('%s_not', $key) : $key; + + switch ($key) { + default: + throw new FireflyException(sprintf('Cannot handle key "%s" in setMetaDateAfterParams()', $key)); + + case 'exact': + if ($value instanceof Carbon) { + $this->collector->setMetaAfter($value, $field); + $this->operators->push(['type' => sprintf('%s_after', $field), 'value' => $value->format('Y-m-d')]); + } + + break; + + case 'year': + if (is_string($value)) { + Log::debug(sprintf('Set %s_is_after YEAR value "%s"', $field, $value)); + $this->collector->metaYearAfter($value, $field); + $this->operators->push(['type' => sprintf('%s_after_year', $field), 'value' => $value]); + } + + break; + + case 'month': + if (is_string($value)) { + Log::debug(sprintf('Set %s_is_after MONTH value "%s"', $field, $value)); + $this->collector->metaMonthAfter($value, $field); + $this->operators->push(['type' => sprintf('%s_after_month', $field), 'value' => $value]); + } + + break; + + case 'day': + if (is_string($value)) { + Log::debug(sprintf('Set %s_is_after DAY value "%s"', $field, $value)); + $this->collector->metaDayAfter($value, $field); + $this->operators->push(['type' => sprintf('%s_after_day', $field), 'value' => $value]); + } + + break; + } + } + } + + /** + * @throws FireflyException + * + * @SuppressWarnings("PHPMD.BooleanArgumentFlag") + */ + private function setMetaDateBeforeParams(string $field, array $range, bool $prohibited = false): void + { + /** + * @var string $key + * @var Carbon|string $value + */ + foreach ($range as $key => $value) { + $key = $prohibited ? sprintf('%s_not', $key) : $key; + + switch ($key) { + default: + throw new FireflyException(sprintf('Cannot handle key "%s" in setMetaDateBeforeParams()', $key)); + + case 'exact': + if ($value instanceof Carbon) { + $this->collector->setMetaBefore($value, $field); + $this->operators->push(['type' => sprintf('%s_before', $field), 'value' => $value->format('Y-m-d')]); + } + + break; + + case 'year': + if (is_string($value)) { + Log::debug(sprintf('Set %s_is_before YEAR value "%s"', $field, $value)); + $this->collector->metaYearBefore($value, $field); + $this->operators->push(['type' => sprintf('%s_before_year', $field), 'value' => $value]); + } + + break; + + case 'month': + if (is_string($value)) { + Log::debug(sprintf('Set %s_is_before MONTH value "%s"', $field, $value)); + $this->collector->metaMonthBefore($value, $field); + $this->operators->push(['type' => sprintf('%s_before_month', $field), 'value' => $value]); + } + + break; + + case 'day': + if (is_string($value)) { + Log::debug(sprintf('Set %s_is_before DAY value "%s"', $field, $value)); + $this->collector->metaDayBefore($value, $field); + $this->operators->push(['type' => sprintf('%s_before_day', $field), 'value' => $value]); + } + + break; + } + } + } + + /** + * @throws FireflyException + * + * @SuppressWarnings("PHPMD.BooleanArgumentFlag") + */ + private function setObjectDateAfterParams(string $field, array $range, bool $prohibited = false): void + { + /** + * @var string $key + * @var Carbon|string $value + */ + foreach ($range as $key => $value) { + $key = $prohibited ? sprintf('%s_not', $key) : $key; + + switch ($key) { + default: + throw new FireflyException(sprintf('Cannot handle key "%s" in setObjectDateAfterParams()', $key)); + + case 'exact': + if ($value instanceof Carbon) { + $this->collector->setObjectAfter($value, $field); + $this->operators->push(['type' => sprintf('%s_after', $field), 'value' => $value->format('Y-m-d')]); + } + + break; + + case 'year': + if (is_string($value)) { + Log::debug(sprintf('Set date_is_after YEAR value "%s"', $value)); + $this->collector->objectYearAfter($value, $field); + $this->operators->push(['type' => sprintf('%s_after_year', $field), 'value' => $value]); + } + + break; + + case 'month': + if (is_string($value)) { + Log::debug(sprintf('Set date_is_after MONTH value "%s"', $value)); + $this->collector->objectMonthAfter($value, $field); + $this->operators->push(['type' => sprintf('%s_after_month', $field), 'value' => $value]); + } + + break; + + case 'day': + if (is_string($value)) { + Log::debug(sprintf('Set date_is_after DAY value "%s"', $value)); + $this->collector->objectDayAfter($value, $field); + $this->operators->push(['type' => sprintf('%s_after_day', $field), 'value' => $value]); + } + + break; + } + } + } + + /** + * @throws FireflyException + * + * @SuppressWarnings("PHPMD.BooleanArgumentFlag") + */ + private function setObjectDateBeforeParams(string $field, array $range, bool $prohibited = false): void + { + /** + * @var string $key + * @var Carbon|string $value + */ + foreach ($range as $key => $value) { + $key = $prohibited ? sprintf('%s_not', $key) : $key; + + switch ($key) { + default: + throw new FireflyException(sprintf('Cannot handle key "%s" in setObjectDateBeforeParams()', $key)); + + case 'exact': + if ($value instanceof Carbon) { + $this->collector->setObjectBefore($value, $field); + $this->operators->push(['type' => sprintf('%s_before', $field), 'value' => $value->format('Y-m-d')]); + } + + break; + + case 'year': + if (is_string($value)) { + Log::debug(sprintf('Set date_is_before YEAR value "%s"', $value)); + $this->collector->objectYearBefore($value, $field); + $this->operators->push(['type' => sprintf('%s_before_year', $field), 'value' => $value]); + } + + break; + + case 'month': + if (is_string($value)) { + Log::debug(sprintf('Set date_is_before MONTH value "%s"', $value)); + $this->collector->objectMonthBefore($value, $field); + $this->operators->push(['type' => sprintf('%s_before_month', $field), 'value' => $value]); + } + + break; + + case 'day': + if (is_string($value)) { + Log::debug(sprintf('Set date_is_before DAY value "%s"', $value)); + $this->collector->objectDayBefore($value, $field); + $this->operators->push(['type' => sprintf('%s_before_day', $field), 'value' => $value]); + } + + break; + } } } @@ -278,15 +1250,15 @@ class OperatorQuerySearch implements SearchInterface throw new FireflyException(sprintf('Unsupported search operator: "%s"', $operator)); - // some search operators are ignored, basically: + // some search operators are ignored, basically: case 'user_action': Log::info(sprintf('Ignore search operator "%s"', $operator)); return false; - // - // all account related searches: - // + // + // all account related searches: + // case 'account_is': $this->searchAccount($value, SearchDirection::BOTH, StringPosition::IS); @@ -448,7 +1420,7 @@ class OperatorQuerySearch implements SearchInterface break; case 'source_account_id': - $account = $this->accountRepository->find((int) $value); + $account = $this->accountRepository->find((int)$value); if (null !== $account) { $this->collector->setSourceAccounts(new Collection()->push($account)); } @@ -461,7 +1433,7 @@ class OperatorQuerySearch implements SearchInterface break; case '-source_account_id': - $account = $this->accountRepository->find((int) $value); + $account = $this->accountRepository->find((int)$value); if (null !== $account) { $this->collector->excludeSourceAccounts(new Collection()->push($account)); } @@ -474,25 +1446,25 @@ class OperatorQuerySearch implements SearchInterface break; case 'journal_id': - $parts = explode(',', $value); + $parts = explode(',', $value); $this->collector->setJournalIds($parts); break; case '-journal_id': - $parts = explode(',', $value); + $parts = explode(',', $value); $this->collector->excludeJournalIds($parts); break; case 'id': - $parts = explode(',', $value); + $parts = explode(',', $value); $this->collector->setIds($parts); break; case '-id': - $parts = explode(',', $value); + $parts = explode(',', $value); $this->collector->excludeIds($parts); break; @@ -578,7 +1550,7 @@ class OperatorQuerySearch implements SearchInterface break; case 'destination_account_id': - $account = $this->accountRepository->find((int) $value); + $account = $this->accountRepository->find((int)$value); if (null !== $account) { $this->collector->setDestinationAccounts(new Collection()->push($account)); } @@ -590,7 +1562,7 @@ class OperatorQuerySearch implements SearchInterface break; case '-destination_account_id': - $account = $this->accountRepository->find((int) $value); + $account = $this->accountRepository->find((int)$value); if (null !== $account) { $this->collector->excludeDestinationAccounts(new Collection()->push($account)); } @@ -603,12 +1575,12 @@ class OperatorQuerySearch implements SearchInterface case 'account_id': Log::debug(sprintf('Now in "account_id" with value "%s"', $value)); - $parts = explode(',', $value); - $collection = new Collection(); + $parts = explode(',', $value); + $collection = new Collection(); foreach ($parts as $accountId) { - $accountId = (int) $accountId; + $accountId = (int)$accountId; Log::debug(sprintf('Searching for account with ID #%d', $accountId)); - $account = $this->accountRepository->find($accountId); + $account = $this->accountRepository->find($accountId); if (null !== $account) { Log::debug(sprintf('Found account with ID #%d ("%s")', $accountId, $account->name)); $collection->push($account); @@ -629,10 +1601,10 @@ class OperatorQuerySearch implements SearchInterface break; case '-account_id': - $parts = explode(',', $value); - $collection = new Collection(); + $parts = explode(',', $value); + $collection = new Collection(); foreach ($parts as $accountId) { - $account = $this->accountRepository->find((int) $accountId); + $account = $this->accountRepository->find((int)$accountId); if (null !== $account) { $collection->push($account); } @@ -647,48 +1619,48 @@ class OperatorQuerySearch implements SearchInterface break; - // - // cash account - // + // + // cash account + // case 'source_is_cash': - $account = $this->getCashAccount(); + $account = $this->getCashAccount(); $this->collector->setSourceAccounts(new Collection()->push($account)); break; case '-source_is_cash': - $account = $this->getCashAccount(); + $account = $this->getCashAccount(); $this->collector->excludeSourceAccounts(new Collection()->push($account)); break; case 'destination_is_cash': - $account = $this->getCashAccount(); + $account = $this->getCashAccount(); $this->collector->setDestinationAccounts(new Collection()->push($account)); break; case '-destination_is_cash': - $account = $this->getCashAccount(); + $account = $this->getCashAccount(); $this->collector->excludeDestinationAccounts(new Collection()->push($account)); break; case 'account_is_cash': - $account = $this->getCashAccount(); + $account = $this->getCashAccount(); $this->collector->setAccounts(new Collection()->push($account)); break; case '-account_is_cash': - $account = $this->getCashAccount(); + $account = $this->getCashAccount(); $this->collector->excludeAccounts(new Collection()->push($account)); break; - // - // description - // + // + // description + // case 'description_starts': $this->collector->descriptionStarts([$value]); @@ -710,7 +1682,7 @@ class OperatorQuerySearch implements SearchInterface break; case 'description_contains': - $this->words[] = $value; + $this->words[] = $value; return false; @@ -729,11 +1701,11 @@ class OperatorQuerySearch implements SearchInterface break; - // - // currency - // + // + // currency + // case 'currency_is': - $currency = $this->findCurrency($value); + $currency = $this->findCurrency($value); if ($currency instanceof TransactionCurrency) { $this->collector->setCurrency($currency); } @@ -745,7 +1717,7 @@ class OperatorQuerySearch implements SearchInterface break; case '-currency_is': - $currency = $this->findCurrency($value); + $currency = $this->findCurrency($value); if ($currency instanceof TransactionCurrency) { $this->collector->excludeCurrency($currency); } @@ -757,7 +1729,7 @@ class OperatorQuerySearch implements SearchInterface break; case 'foreign_currency_is': - $currency = $this->findCurrency($value); + $currency = $this->findCurrency($value); if ($currency instanceof TransactionCurrency) { $this->collector->setForeignCurrency($currency); } @@ -769,7 +1741,7 @@ class OperatorQuerySearch implements SearchInterface break; case '-foreign_currency_is': - $currency = $this->findCurrency($value); + $currency = $this->findCurrency($value); if ($currency instanceof TransactionCurrency) { $this->collector->excludeForeignCurrency($currency); } @@ -780,9 +1752,9 @@ class OperatorQuerySearch implements SearchInterface break; - // - // attachments - // + // + // attachments + // case 'has_attachments': case '-has_no_attachments': Log::debug('Set collector to filter on attachments.'); @@ -797,8 +1769,8 @@ class OperatorQuerySearch implements SearchInterface break; - // - // categories + // + // categories case '-has_any_category': case 'has_no_category': $this->collector->withoutCategory(); @@ -812,7 +1784,7 @@ class OperatorQuerySearch implements SearchInterface break; case 'category_is': - $category = $this->categoryRepository->findByName($value); + $category = $this->categoryRepository->findByName($value); if (null !== $category) { $this->collector->setCategory($category); @@ -824,7 +1796,7 @@ class OperatorQuerySearch implements SearchInterface break; case '-category_is': - $category = $this->categoryRepository->findByName($value); + $category = $this->categoryRepository->findByName($value); if (null !== $category) { $this->collector->excludeCategory($category); @@ -834,7 +1806,7 @@ class OperatorQuerySearch implements SearchInterface break; case 'category_ends': - $result = $this->categoryRepository->categoryEndsWith($value, 1337); + $result = $this->categoryRepository->categoryEndsWith($value, 1337); if ($result->count() > 0) { $this->collector->setCategories($result); } @@ -846,7 +1818,7 @@ class OperatorQuerySearch implements SearchInterface break; case '-category_ends': - $result = $this->categoryRepository->categoryEndsWith($value, 1337); + $result = $this->categoryRepository->categoryEndsWith($value, 1337); if ($result->count() > 0) { $this->collector->excludeCategories($result); } @@ -858,7 +1830,7 @@ class OperatorQuerySearch implements SearchInterface break; case 'category_starts': - $result = $this->categoryRepository->categoryStartsWith($value, 1337); + $result = $this->categoryRepository->categoryStartsWith($value, 1337); if ($result->count() > 0) { $this->collector->setCategories($result); } @@ -870,7 +1842,7 @@ class OperatorQuerySearch implements SearchInterface break; case '-category_starts': - $result = $this->categoryRepository->categoryStartsWith($value, 1337); + $result = $this->categoryRepository->categoryStartsWith($value, 1337); if ($result->count() > 0) { $this->collector->excludeCategories($result); } @@ -882,7 +1854,7 @@ class OperatorQuerySearch implements SearchInterface break; case 'category_contains': - $result = $this->categoryRepository->searchCategory($value, 1337); + $result = $this->categoryRepository->searchCategory($value, 1337); if ($result->count() > 0) { $this->collector->setCategories($result); } @@ -894,7 +1866,7 @@ class OperatorQuerySearch implements SearchInterface break; case '-category_contains': - $result = $this->categoryRepository->searchCategory($value, 1337); + $result = $this->categoryRepository->searchCategory($value, 1337); if ($result->count() > 0) { $this->collector->excludeCategories($result); } @@ -905,9 +1877,9 @@ class OperatorQuerySearch implements SearchInterface break; - // - // budgets - // + // + // budgets + // case '-has_any_budget': case 'has_no_budget': $this->collector->withoutBudget(); @@ -921,7 +1893,7 @@ class OperatorQuerySearch implements SearchInterface break; case 'budget_contains': - $result = $this->budgetRepository->searchBudget($value, 1337); + $result = $this->budgetRepository->searchBudget($value, 1337); if ($result->count() > 0) { $this->collector->setBudgets($result); } @@ -933,7 +1905,7 @@ class OperatorQuerySearch implements SearchInterface break; case '-budget_contains': - $result = $this->budgetRepository->searchBudget($value, 1337); + $result = $this->budgetRepository->searchBudget($value, 1337); if ($result->count() > 0) { $this->collector->excludeBudgets($result); } @@ -945,7 +1917,7 @@ class OperatorQuerySearch implements SearchInterface break; case 'budget_is': - $budget = $this->budgetRepository->findByName($value); + $budget = $this->budgetRepository->findByName($value); if (null !== $budget) { $this->collector->setBudget($budget); @@ -957,7 +1929,7 @@ class OperatorQuerySearch implements SearchInterface break; case '-budget_is': - $budget = $this->budgetRepository->findByName($value); + $budget = $this->budgetRepository->findByName($value); if (null !== $budget) { $this->collector->excludeBudget($budget); @@ -969,7 +1941,7 @@ class OperatorQuerySearch implements SearchInterface break; case 'budget_ends': - $result = $this->budgetRepository->budgetEndsWith($value, 1337); + $result = $this->budgetRepository->budgetEndsWith($value, 1337); if ($result->count() > 0) { $this->collector->setBudgets($result); } @@ -981,7 +1953,7 @@ class OperatorQuerySearch implements SearchInterface break; case '-budget_ends': - $result = $this->budgetRepository->budgetEndsWith($value, 1337); + $result = $this->budgetRepository->budgetEndsWith($value, 1337); if ($result->count() > 0) { $this->collector->excludeBudgets($result); } @@ -993,7 +1965,7 @@ class OperatorQuerySearch implements SearchInterface break; case 'budget_starts': - $result = $this->budgetRepository->budgetStartsWith($value, 1337); + $result = $this->budgetRepository->budgetStartsWith($value, 1337); if ($result->count() > 0) { $this->collector->setBudgets($result); } @@ -1005,7 +1977,7 @@ class OperatorQuerySearch implements SearchInterface break; case '-budget_starts': - $result = $this->budgetRepository->budgetStartsWith($value, 1337); + $result = $this->budgetRepository->budgetStartsWith($value, 1337); if ($result->count() > 0) { $this->collector->excludeBudgets($result); } @@ -1016,9 +1988,9 @@ class OperatorQuerySearch implements SearchInterface break; - // - // bill - // + // + // bill + // case '-has_any_bill': case 'has_no_bill': $this->collector->withoutBill(); @@ -1032,7 +2004,7 @@ class OperatorQuerySearch implements SearchInterface break; case 'bill_contains': - $result = $this->billRepository->searchBill($value, 1337); + $result = $this->billRepository->searchBill($value, 1337); if ($result->count() > 0) { $this->collector->setBills($result); @@ -1044,7 +2016,7 @@ class OperatorQuerySearch implements SearchInterface break; case '-bill_contains': - $result = $this->billRepository->searchBill($value, 1337); + $result = $this->billRepository->searchBill($value, 1337); if ($result->count() > 0) { $this->collector->excludeBills($result); @@ -1056,7 +2028,7 @@ class OperatorQuerySearch implements SearchInterface break; case 'bill_is': - $bill = $this->billRepository->findByName($value); + $bill = $this->billRepository->findByName($value); if (null !== $bill) { $this->collector->setBill($bill); @@ -1068,7 +2040,7 @@ class OperatorQuerySearch implements SearchInterface break; case '-bill_is': - $bill = $this->billRepository->findByName($value); + $bill = $this->billRepository->findByName($value); if (null !== $bill) { $this->collector->excludeBills(new Collection()->push($bill)); @@ -1080,7 +2052,7 @@ class OperatorQuerySearch implements SearchInterface break; case 'bill_ends': - $result = $this->billRepository->billEndsWith($value, 1337); + $result = $this->billRepository->billEndsWith($value, 1337); if ($result->count() > 0) { $this->collector->setBills($result); } @@ -1092,7 +2064,7 @@ class OperatorQuerySearch implements SearchInterface break; case '-bill_ends': - $result = $this->billRepository->billEndsWith($value, 1337); + $result = $this->billRepository->billEndsWith($value, 1337); if ($result->count() > 0) { $this->collector->excludeBills($result); } @@ -1104,7 +2076,7 @@ class OperatorQuerySearch implements SearchInterface break; case 'bill_starts': - $result = $this->billRepository->billStartsWith($value, 1337); + $result = $this->billRepository->billStartsWith($value, 1337); if ($result->count() > 0) { $this->collector->setBills($result); } @@ -1116,7 +2088,7 @@ class OperatorQuerySearch implements SearchInterface break; case '-bill_starts': - $result = $this->billRepository->billStartsWith($value, 1337); + $result = $this->billRepository->billStartsWith($value, 1337); if ($result->count() > 0) { $this->collector->excludeBills($result); } @@ -1127,9 +2099,9 @@ class OperatorQuerySearch implements SearchInterface break; - // - // tags - // + // + // tags + // case '-has_any_tag': case 'has_no_tag': $this->collector->withoutTags(); @@ -1144,7 +2116,7 @@ class OperatorQuerySearch implements SearchInterface case '-tag_is_not': case 'tag_is': - $result = $this->tagRepository->findByTag($value); + $result = $this->tagRepository->findByTag($value); if (null !== $result) { $this->includeTags[] = $result->id; $this->includeTags = array_unique($this->includeTags); @@ -1159,7 +2131,7 @@ class OperatorQuerySearch implements SearchInterface break; case 'tag_contains': - $tags = $this->tagRepository->searchTag($value); + $tags = $this->tagRepository->searchTag($value); if (0 === $tags->count()) { Log::info(sprintf('No valid tags in "%s"-operator, so search will not return ANY results.', $operator)); Log::warning(sprintf('Call to findNothing() from %s.', $operator)); @@ -1174,7 +2146,7 @@ class OperatorQuerySearch implements SearchInterface break; case 'tag_starts': - $tags = $this->tagRepository->tagStartsWith($value); + $tags = $this->tagRepository->tagStartsWith($value); if (0 === $tags->count()) { Log::info(sprintf('No valid tags in "%s"-operator, so search will not return ANY results.', $operator)); Log::warning(sprintf('Call to findNothing() from %s.', $operator)); @@ -1189,7 +2161,7 @@ class OperatorQuerySearch implements SearchInterface break; case '-tag_starts': - $tags = $this->tagRepository->tagStartsWith($value); + $tags = $this->tagRepository->tagStartsWith($value); if (0 === $tags->count()) { Log::info(sprintf('No valid tags in "%s"-operator, so search will not return ANY results.', $operator)); Log::warning(sprintf('Call to findNothing() from %s.', $operator)); @@ -1203,7 +2175,7 @@ class OperatorQuerySearch implements SearchInterface break; case 'tag_ends': - $tags = $this->tagRepository->tagEndsWith($value); + $tags = $this->tagRepository->tagEndsWith($value); if (0 === $tags->count()) { Log::info(sprintf('No valid tags in "%s"-operator, so search will not return ANY results.', $operator)); Log::warning(sprintf('Call to findNothing() from %s.', $operator)); @@ -1217,7 +2189,7 @@ class OperatorQuerySearch implements SearchInterface break; case '-tag_ends': - $tags = $this->tagRepository->tagEndsWith($value); + $tags = $this->tagRepository->tagEndsWith($value); if (0 === $tags->count()) { Log::info(sprintf('No valid tags in "%s"-operator, so search will not return ANY results.', $operator)); Log::warning(sprintf('Call to findNothing() from %s.', $operator)); @@ -1231,7 +2203,7 @@ class OperatorQuerySearch implements SearchInterface break; case '-tag_contains': - $tags = $this->tagRepository->searchTag($value)->keyBy('id'); + $tags = $this->tagRepository->searchTag($value)->keyBy('id'); if (0 === $tags->count()) { Log::info(sprintf('No valid tags in "%s"-operator, so search will not return ANY results.', $operator)); @@ -1247,7 +2219,7 @@ class OperatorQuerySearch implements SearchInterface case '-tag_is': case 'tag_is_not': - $result = $this->tagRepository->findByTag($value); + $result = $this->tagRepository->findByTag($value); if (null !== $result) { $this->excludeTags[] = $result->id; $this->excludeTags = array_unique($this->excludeTags); @@ -1255,9 +2227,9 @@ class OperatorQuerySearch implements SearchInterface break; - // - // notes - // + // + // notes + // case 'notes_contains': $this->collector->notesContain($value); @@ -1320,14 +2292,14 @@ class OperatorQuerySearch implements SearchInterface break; - // - // amount - // + // + // amount + // case 'amount_is': // strip comma's, make dots. Log::debug(sprintf('Original value "%s"', $value)); - $value = str_replace(',', '.', $value); - $amount = app('steam')->positive($value); + $value = str_replace(',', '.', $value); + $amount = app('steam')->positive($value); Log::debug(sprintf('Set "%s" using collector with value "%s"', $operator, $amount)); $this->collector->amountIs($amount); @@ -1336,8 +2308,8 @@ class OperatorQuerySearch implements SearchInterface case '-amount_is': // strip comma's, make dots. Log::debug(sprintf('Original value "%s"', $value)); - $value = str_replace(',', '.', $value); - $amount = app('steam')->positive($value); + $value = str_replace(',', '.', $value); + $amount = app('steam')->positive($value); Log::debug(sprintf('Set "%s" using collector with value "%s"', $operator, $amount)); $this->collector->amountIsNot($amount); @@ -1345,9 +2317,9 @@ class OperatorQuerySearch implements SearchInterface case 'foreign_amount_is': // strip comma's, make dots. - $value = str_replace(',', '.', $value); + $value = str_replace(',', '.', $value); - $amount = app('steam')->positive($value); + $amount = app('steam')->positive($value); Log::debug(sprintf('Set "%s" using collector with value "%s"', $operator, $amount)); $this->collector->foreignAmountIs($amount); @@ -1355,9 +2327,9 @@ class OperatorQuerySearch implements SearchInterface case '-foreign_amount_is': // strip comma's, make dots. - $value = str_replace(',', '.', $value); + $value = str_replace(',', '.', $value); - $amount = app('steam')->positive($value); + $amount = app('steam')->positive($value); Log::debug(sprintf('Set "%s" using collector with value "%s"', $operator, $amount)); $this->collector->foreignAmountIsNot($amount); @@ -1366,9 +2338,9 @@ class OperatorQuerySearch implements SearchInterface case '-amount_more': case 'amount_less': // strip comma's, make dots. - $value = str_replace(',', '.', $value); + $value = str_replace(',', '.', $value); - $amount = app('steam')->positive($value); + $amount = app('steam')->positive($value); Log::debug(sprintf('Set "%s" using collector with value "%s"', $operator, $amount)); $this->collector->amountLess($amount); @@ -1377,9 +2349,9 @@ class OperatorQuerySearch implements SearchInterface case '-foreign_amount_more': case 'foreign_amount_less': // strip comma's, make dots. - $value = str_replace(',', '.', $value); + $value = str_replace(',', '.', $value); - $amount = app('steam')->positive($value); + $amount = app('steam')->positive($value); Log::debug(sprintf('Set "%s" using collector with value "%s"', $operator, $amount)); $this->collector->foreignAmountLess($amount); @@ -1389,8 +2361,8 @@ class OperatorQuerySearch implements SearchInterface case 'amount_more': Log::debug(sprintf('Now handling operator "%s"', $operator)); // strip comma's, make dots. - $value = str_replace(',', '.', $value); - $amount = app('steam')->positive($value); + $value = str_replace(',', '.', $value); + $amount = app('steam')->positive($value); Log::debug(sprintf('Set "%s" using collector with value "%s"', $operator, $amount)); $this->collector->amountMore($amount); @@ -1400,16 +2372,16 @@ class OperatorQuerySearch implements SearchInterface case 'foreign_amount_more': Log::debug(sprintf('Now handling operator "%s"', $operator)); // strip comma's, make dots. - $value = str_replace(',', '.', $value); - $amount = app('steam')->positive($value); + $value = str_replace(',', '.', $value); + $amount = app('steam')->positive($value); Log::debug(sprintf('Set "%s" using collector with value "%s"', $operator, $amount)); $this->collector->foreignAmountMore($amount); break; - // - // transaction type - // + // + // transaction type + // case 'transaction_type': $this->collector->setTypes([ucfirst($value)]); Log::debug(sprintf('Set "%s" using collector with value "%s"', $operator, $value)); @@ -1422,152 +2394,152 @@ class OperatorQuerySearch implements SearchInterface break; - // - // dates - // + // + // dates + // case '-date_on': case 'date_on': - $range = $this->parseDateRange($operator, $value); + $range = $this->parseDateRange($operator, $value); $this->setExactDateParams($range, $prohibited); return false; case 'date_before': case '-date_after': - $range = $this->parseDateRange($operator, $value); + $range = $this->parseDateRange($operator, $value); $this->setDateBeforeParams($range); return false; case 'date_after': case '-date_before': - $range = $this->parseDateRange($operator, $value); + $range = $this->parseDateRange($operator, $value); $this->setDateAfterParams($range); return false; case 'interest_date_on': case '-interest_date_on': - $range = $this->parseDateRange($operator, $value); + $range = $this->parseDateRange($operator, $value); $this->setExactMetaDateParams('interest_date', $range, $prohibited); return false; case 'interest_date_before': case '-interest_date_after': - $range = $this->parseDateRange($operator, $value); + $range = $this->parseDateRange($operator, $value); $this->setMetaDateBeforeParams('interest_date', $range); return false; case 'interest_date_after': case '-interest_date_before': - $range = $this->parseDateRange($operator, $value); + $range = $this->parseDateRange($operator, $value); $this->setMetaDateAfterParams('interest_date', $range); return false; case 'book_date_on': case '-book_date_on': - $range = $this->parseDateRange($operator, $value); + $range = $this->parseDateRange($operator, $value); $this->setExactMetaDateParams('book_date', $range, $prohibited); return false; case 'book_date_before': case '-book_date_after': - $range = $this->parseDateRange($operator, $value); + $range = $this->parseDateRange($operator, $value); $this->setMetaDateBeforeParams('book_date', $range); return false; case 'book_date_after': case '-book_date_before': - $range = $this->parseDateRange($operator, $value); + $range = $this->parseDateRange($operator, $value); $this->setMetaDateAfterParams('book_date', $range); return false; case 'process_date_on': case '-process_date_on': - $range = $this->parseDateRange($operator, $value); + $range = $this->parseDateRange($operator, $value); $this->setExactMetaDateParams('process_date', $range, $prohibited); return false; case 'process_date_before': case '-process_date_after': - $range = $this->parseDateRange($operator, $value); + $range = $this->parseDateRange($operator, $value); $this->setMetaDateBeforeParams('process_date', $range); return false; case 'process_date_after': case '-process_date_before': - $range = $this->parseDateRange($operator, $value); + $range = $this->parseDateRange($operator, $value); $this->setMetaDateAfterParams('process_date', $range); return false; case 'due_date_on': case '-due_date_on': - $range = $this->parseDateRange($operator, $value); + $range = $this->parseDateRange($operator, $value); $this->setExactMetaDateParams('due_date', $range, $prohibited); return false; case 'due_date_before': case '-due_date_after': - $range = $this->parseDateRange($operator, $value); + $range = $this->parseDateRange($operator, $value); $this->setMetaDateBeforeParams('due_date', $range); return false; case 'due_date_after': case '-due_date_before': - $range = $this->parseDateRange($operator, $value); + $range = $this->parseDateRange($operator, $value); $this->setMetaDateAfterParams('due_date', $range); return false; case 'payment_date_on': case '-payment_date_on': - $range = $this->parseDateRange($operator, $value); + $range = $this->parseDateRange($operator, $value); $this->setExactMetaDateParams('payment_date', $range, $prohibited); return false; case 'payment_date_before': case '-payment_date_after': - $range = $this->parseDateRange($operator, $value); + $range = $this->parseDateRange($operator, $value); $this->setMetaDateBeforeParams('payment_date', $range); return false; case 'payment_date_after': case '-payment_date_before': - $range = $this->parseDateRange($operator, $value); + $range = $this->parseDateRange($operator, $value); $this->setMetaDateAfterParams('payment_date', $range); return false; case 'invoice_date_on': case '-invoice_date_on': - $range = $this->parseDateRange($operator, $value); + $range = $this->parseDateRange($operator, $value); $this->setExactMetaDateParams('invoice_date', $range, $prohibited); return false; case 'invoice_date_before': case '-invoice_date_after': - $range = $this->parseDateRange($operator, $value); + $range = $this->parseDateRange($operator, $value); $this->setMetaDateBeforeParams('invoice_date', $range); return false; case 'invoice_date_after': case '-invoice_date_before': - $range = $this->parseDateRange($operator, $value); + $range = $this->parseDateRange($operator, $value); $this->setMetaDateAfterParams('invoice_date', $range); return false; @@ -1575,7 +2547,7 @@ class OperatorQuerySearch implements SearchInterface case 'created_at_on': case '-created_at_on': Log::debug(sprintf('Set "%s" using collector with value "%s"', $operator, $value)); - $range = $this->parseDateRange($operator, $value); + $range = $this->parseDateRange($operator, $value); $this->setExactObjectDateParams('created_at', $range, $prohibited); return false; @@ -1583,7 +2555,7 @@ class OperatorQuerySearch implements SearchInterface case 'created_at_before': case '-created_at_after': Log::debug(sprintf('Set "%s" using collector with value "%s"', $operator, $value)); - $range = $this->parseDateRange($operator, $value); + $range = $this->parseDateRange($operator, $value); $this->setObjectDateBeforeParams('created_at', $range); return false; @@ -1591,7 +2563,7 @@ class OperatorQuerySearch implements SearchInterface case 'created_at_after': case '-created_at_before': Log::debug(sprintf('Set "%s" using collector with value "%s"', $operator, $value)); - $range = $this->parseDateRange($operator, $value); + $range = $this->parseDateRange($operator, $value); $this->setObjectDateAfterParams('created_at', $range); return false; @@ -1599,7 +2571,7 @@ class OperatorQuerySearch implements SearchInterface case 'updated_at_on': case '-updated_at_on': Log::debug(sprintf('Set "%s" using collector with value "%s"', $operator, $value)); - $range = $this->parseDateRange($operator, $value); + $range = $this->parseDateRange($operator, $value); $this->setExactObjectDateParams('updated_at', $range, $prohibited); return false; @@ -1607,7 +2579,7 @@ class OperatorQuerySearch implements SearchInterface case 'updated_at_before': case '-updated_at_after': Log::debug(sprintf('Set "%s" using collector with value "%s"', $operator, $value)); - $range = $this->parseDateRange($operator, $value); + $range = $this->parseDateRange($operator, $value); $this->setObjectDateBeforeParams('updated_at', $range); return false; @@ -1615,14 +2587,14 @@ class OperatorQuerySearch implements SearchInterface case 'updated_at_after': case '-updated_at_before': Log::debug(sprintf('Set "%s" using collector with value "%s"', $operator, $value)); - $range = $this->parseDateRange($operator, $value); + $range = $this->parseDateRange($operator, $value); $this->setObjectDateAfterParams('updated_at', $range); return false; - // - // external URL - // + // + // external URL + // case '-any_external_url': case 'no_external_url': $this->collector->withoutExternalUrl(); @@ -1687,9 +2659,9 @@ class OperatorQuerySearch implements SearchInterface break; - // - // other fields - // + // + // other fields + // case 'external_id_is': $this->collector->setExternalId($value); @@ -1947,976 +2919,4 @@ class OperatorQuerySearch implements SearchInterface return true; } - - /** - * @throws FireflyException - */ - public static function getRootOperator(string $operator): string - { - $original = $operator; - // if the string starts with "-" (not), we can remove it and recycle - // the configuration from the original operator. - if (str_starts_with($operator, '-')) { - $operator = substr($operator, 1); - } - - $config = config(sprintf('search.operators.%s', $operator)); - if (null === $config) { - throw new FireflyException(sprintf('No configuration for search operator "%s"', $operator)); - } - if (true === $config['alias']) { - $return = $config['alias_for']; - if (str_starts_with($original, '-')) { - $return = sprintf('-%s', $config['alias_for']); - } - Log::debug(sprintf('"%s" is an alias for "%s", so return that instead.', $original, $return)); - - return $return; - } - Log::debug(sprintf('"%s" is not an alias.', $operator)); - - return $original; - } - - /** - * searchDirection: 1 = source (default), 2 = destination, 3 = both - * stringPosition: 1 = start (default), 2 = end, 3 = contains, 4 = is - * - * @SuppressWarnings("PHPMD.BooleanArgumentFlag") - * @SuppressWarnings("PHPMD.NPathComplexity") - */ - private function searchAccount(string $value, SearchDirection $searchDirection, StringPosition $stringPosition, bool $prohibited = false): void - { - Log::debug(sprintf('searchAccount("%s", %s, %s)', $value, $stringPosition->name, $searchDirection->name)); - - // search direction (default): for source accounts - $searchTypes = [AccountTypeEnum::ASSET->value, AccountTypeEnum::MORTGAGE->value, AccountTypeEnum::LOAN->value, AccountTypeEnum::DEBT->value, AccountTypeEnum::REVENUE->value]; - $collectorMethod = 'setSourceAccounts'; - if ($prohibited) { - $collectorMethod = 'excludeSourceAccounts'; - } - - // search direction: for destination accounts - if (SearchDirection::DESTINATION === $searchDirection) { // destination - // destination can be - $searchTypes = [AccountTypeEnum::ASSET->value, AccountTypeEnum::MORTGAGE->value, AccountTypeEnum::LOAN->value, AccountTypeEnum::DEBT->value, AccountTypeEnum::EXPENSE->value]; - $collectorMethod = 'setDestinationAccounts'; - if ($prohibited) { - $collectorMethod = 'excludeDestinationAccounts'; - } - } - // either account could be: - if (SearchDirection::BOTH === $searchDirection) { - $searchTypes = [AccountTypeEnum::ASSET->value, AccountTypeEnum::MORTGAGE->value, AccountTypeEnum::LOAN->value, AccountTypeEnum::DEBT->value, AccountTypeEnum::EXPENSE->value, AccountTypeEnum::REVENUE->value]; - $collectorMethod = 'setAccounts'; - if ($prohibited) { - $collectorMethod = 'excludeAccounts'; - } - } - // string position (default): starts with: - $stringMethod = 'str_starts_with'; - - // string position: ends with: - if (StringPosition::ENDS === $stringPosition) { - $stringMethod = 'str_ends_with'; - } - if (StringPosition::CONTAINS === $stringPosition) { - $stringMethod = 'str_contains'; - } - if (StringPosition::IS === $stringPosition) { - $stringMethod = 'stringIsEqual'; - } - - // get accounts: - $accounts = $this->accountRepository->searchAccount($value, $searchTypes, 1337); - if (0 === $accounts->count() && false === $prohibited) { - Log::warning('Found zero accounts, search for non existing account, NO results will be returned.'); - $this->collector->findNothing(); - - return; - } - if (0 === $accounts->count() && true === $prohibited) { - Log::debug('Found zero accounts, but the search is negated, so effectively we ignore the search parameter.'); - - return; - } - Log::debug(sprintf('Found %d accounts, will filter.', $accounts->count())); - $filtered = $accounts->filter( - static fn (Account $account) => $stringMethod(strtolower($account->name), strtolower($value)) - ); - - if (0 === $filtered->count()) { - Log::warning('Left with zero accounts, so cannot find anything, NO results will be returned.'); - $this->collector->findNothing(); - - return; - } - Log::debug(sprintf('Left with %d, set as %s().', $filtered->count(), $collectorMethod)); - $this->collector->{$collectorMethod}($filtered); // @phpstan-ignore-line - } - - /** - * TODO make enums - * searchDirection: 1 = source (default), 2 = destination, 3 = both - * stringPosition: 1 = start (default), 2 = end, 3 = contains, 4 = is - * - * @SuppressWarnings("PHPMD.BooleanArgumentFlag") - * @SuppressWarnings("PHPMD.NPathComplexity") - */ - private function searchAccountNr(string $value, SearchDirection $searchDirection, StringPosition $stringPosition, bool $prohibited = false): void - { - Log::debug(sprintf('searchAccountNr(%s, %d, %d)', $value, $searchDirection->name, $stringPosition->name)); - - // search direction (default): for source accounts - $searchTypes = [AccountTypeEnum::ASSET->value, AccountTypeEnum::MORTGAGE->value, AccountTypeEnum::LOAN->value, AccountTypeEnum::DEBT->value, AccountTypeEnum::REVENUE->value]; - $collectorMethod = 'setSourceAccounts'; - if (true === $prohibited) { - $collectorMethod = 'excludeSourceAccounts'; - } - - // search direction: for destination accounts - if (SearchDirection::DESTINATION === $searchDirection) { - // destination can be - $searchTypes = [AccountTypeEnum::ASSET->value, AccountTypeEnum::MORTGAGE->value, AccountTypeEnum::LOAN->value, AccountTypeEnum::DEBT->value, AccountTypeEnum::EXPENSE->value]; - $collectorMethod = 'setDestinationAccounts'; - if (true === $prohibited) { - $collectorMethod = 'excludeDestinationAccounts'; - } - } - - // either account could be: - if (SearchDirection::BOTH === $searchDirection) { - $searchTypes = [AccountTypeEnum::ASSET->value, AccountTypeEnum::MORTGAGE->value, AccountTypeEnum::LOAN->value, AccountTypeEnum::DEBT->value, AccountTypeEnum::EXPENSE->value, AccountTypeEnum::REVENUE->value]; - $collectorMethod = 'setAccounts'; - if (true === $prohibited) { - $collectorMethod = 'excludeAccounts'; - } - } - - // string position (default): starts with: - $stringMethod = 'str_starts_with'; - - // string position: ends with: - if (StringPosition::ENDS === $stringPosition) { - $stringMethod = 'str_ends_with'; - } - if (StringPosition::CONTAINS === $stringPosition) { - $stringMethod = 'str_contains'; - } - if (StringPosition::IS === $stringPosition) { - $stringMethod = 'stringIsEqual'; - } - - // search for accounts: - $accounts = $this->accountRepository->searchAccountNr($value, $searchTypes, 1337); - if (0 === $accounts->count()) { - Log::debug('Found zero accounts, search for invalid account.'); - Log::warning('Call to findNothing() from searchAccountNr().'); - $this->collector->findNothing(); - - return; - } - - // if found, do filter - Log::debug(sprintf('Found %d accounts, will filter.', $accounts->count())); - $filtered = $accounts->filter( - static function (Account $account) use ($value, $stringMethod) { - // either IBAN or account number - $ibanMatch = $stringMethod(strtolower((string) $account->iban), strtolower($value)); - $accountNrMatch = false; - - /** @var AccountMeta $meta */ - foreach ($account->accountMeta as $meta) { - if ('account_number' === $meta->name && $stringMethod(strtolower((string) $meta->data), strtolower($value))) { - $accountNrMatch = true; - } - } - - return $ibanMatch || $accountNrMatch; - } - ); - - if (0 === $filtered->count()) { - Log::debug('Left with zero, search for invalid account'); - Log::warning('Call to findNothing() from searchAccountNr().'); - $this->collector->findNothing(); - - return; - } - Log::debug(sprintf('Left with %d, set as %s().', $filtered->count(), $collectorMethod)); - $this->collector->{$collectorMethod}($filtered); // @phpstan-ignore-line - } - - private function getCashAccount(): Account - { - return $this->accountRepository->getCashAccount(); - } - - private function findCurrency(string $value): ?TransactionCurrency - { - if (str_contains($value, '(') && str_contains($value, ')')) { - // bad method to split and get the currency code: - $parts = explode(' ', $value); - $value = trim($parts[count($parts) - 1], "() \t\n\r\0\x0B"); - } - $result = $this->currencyRepository->findByCode($value); - if (null === $result) { - return $this->currencyRepository->findByName($value); - } - - return $result; - } - - /** - * @throws FireflyException - */ - private function parseDateRange(string $type, string $value): array - { - $parser = new ParseDateString(); - if ($parser->isDateRange($value)) { - return $parser->parseRange($value); - } - - try { - $parsedDate = $parser->parseDate($value); - } catch (FireflyException) { - Log::debug(sprintf('Could not parse date "%s", will return empty array.', $value)); - $this->invalidOperators[] = [ - 'type' => $type, - 'value' => $value, - ]; - - return []; - } - - return [ - 'exact' => $parsedDate, - ]; - } - - /** - * @throws FireflyException - * - * @SuppressWarnings("PHPMD.BooleanArgumentFlag") - */ - private function setExactDateParams(array $range, bool $prohibited = false): void - { - /** - * @var string $key - * @var Carbon|string $value - */ - foreach ($range as $key => $value) { - $key = $prohibited ? sprintf('%s_not', $key) : $key; - - switch ($key) { - default: - throw new FireflyException(sprintf('Cannot handle key "%s" in setExactParameters()', $key)); - - case 'exact': - if ($value instanceof Carbon) { - Log::debug(sprintf('Set date_is_exact value "%s"', $value->format('Y-m-d'))); - $this->collector->setRange($value, $value); - $this->operators->push(['type' => 'date_on', 'value' => $value->format('Y-m-d')]); - } - - break; - - case 'exact_not': - if ($value instanceof Carbon) { - $this->collector->excludeRange($value, $value); - $this->operators->push(['type' => 'not_date_on', 'value' => $value->format('Y-m-d')]); - } - - break; - - case 'year': - if (is_string($value)) { - Log::debug(sprintf('Set date_is_exact YEAR value "%s"', $value)); - $this->collector->yearIs($value); - $this->operators->push(['type' => 'date_on_year', 'value' => $value]); - } - - break; - - case 'year_not': - if (is_string($value)) { - Log::debug(sprintf('Set date_is_exact_not YEAR value "%s"', $value)); - $this->collector->yearIsNot($value); - $this->operators->push(['type' => 'not_date_on_year', 'value' => $value]); - } - - break; - - case 'month': - if (is_string($value)) { - Log::debug(sprintf('Set date_is_exact MONTH value "%s"', $value)); - $this->collector->monthIs($value); - $this->operators->push(['type' => 'date_on_month', 'value' => $value]); - } - - break; - - case 'month_not': - if (is_string($value)) { - Log::debug(sprintf('Set date_is_exact not MONTH value "%s"', $value)); - $this->collector->monthIsNot($value); - $this->operators->push(['type' => 'not_date_on_month', 'value' => $value]); - } - - break; - - case 'day': - if (is_string($value)) { - Log::debug(sprintf('Set date_is_exact DAY value "%s"', $value)); - $this->collector->dayIs($value); - $this->operators->push(['type' => 'date_on_day', 'value' => $value]); - } - - break; - - case 'day_not': - if (is_string($value)) { - Log::debug(sprintf('Set not date_is_exact DAY value "%s"', $value)); - $this->collector->dayIsNot($value); - $this->operators->push(['type' => 'not_date_on_day', 'value' => $value]); - } - - break; - } - } - } - - /** - * @throws FireflyException - * - * @SuppressWarnings("PHPMD.BooleanArgumentFlag") - */ - private function setDateBeforeParams(array $range, bool $prohibited = false): void - { - /** - * @var string $key - * @var Carbon|string $value - */ - foreach ($range as $key => $value) { - $key = $prohibited ? sprintf('%s_not', $key) : $key; - - switch ($key) { - default: - throw new FireflyException(sprintf('Cannot handle key "%s" in setDateBeforeParams()', $key)); - - case 'exact': - if ($value instanceof Carbon) { - $this->collector->setBefore($value); - $this->operators->push(['type' => 'date_before', 'value' => $value->format('Y-m-d')]); - } - - break; - - case 'year': - if (is_string($value)) { - Log::debug(sprintf('Set date_is_before YEAR value "%s"', $value)); - $this->collector->yearBefore($value); - $this->operators->push(['type' => 'date_before_year', 'value' => $value]); - } - - break; - - case 'month': - if (is_string($value)) { - Log::debug(sprintf('Set date_is_before MONTH value "%s"', $value)); - $this->collector->monthBefore($value); - $this->operators->push(['type' => 'date_before_month', 'value' => $value]); - } - - break; - - case 'day': - if (is_string($value)) { - Log::debug(sprintf('Set date_is_before DAY value "%s"', $value)); - $this->collector->dayBefore($value); - $this->operators->push(['type' => 'date_before_day', 'value' => $value]); - } - - break; - } - } - } - - /** - * @throws FireflyException - * - * @SuppressWarnings("PHPMD.BooleanArgumentFlag") - */ - private function setDateAfterParams(array $range, bool $prohibited = false): void - { - /** - * @var string $key - * @var Carbon|string $value - */ - foreach ($range as $key => $value) { - $key = $prohibited ? sprintf('%s_not', $key) : $key; - - switch ($key) { - default: - throw new FireflyException(sprintf('Cannot handle key "%s" in setDateAfterParams()', $key)); - - case 'exact': - if ($value instanceof Carbon) { - $this->collector->setAfter($value); - $this->operators->push(['type' => 'date_after', 'value' => $value->format('Y-m-d')]); - } - - break; - - case 'year': - if (is_string($value)) { - Log::debug(sprintf('Set date_is_after YEAR value "%s"', $value)); - $this->collector->yearAfter($value); - $this->operators->push(['type' => 'date_after_year', 'value' => $value]); - } - - break; - - case 'month': - if (is_string($value)) { - Log::debug(sprintf('Set date_is_after MONTH value "%s"', $value)); - $this->collector->monthAfter($value); - $this->operators->push(['type' => 'date_after_month', 'value' => $value]); - } - - break; - - case 'day': - if (is_string($value)) { - Log::debug(sprintf('Set date_is_after DAY value "%s"', $value)); - $this->collector->dayAfter($value); - $this->operators->push(['type' => 'date_after_day', 'value' => $value]); - } - - break; - } - } - } - - /** - * @throws FireflyException - * - * @SuppressWarnings("PHPMD.BooleanArgumentFlag") - */ - private function setExactMetaDateParams(string $field, array $range, bool $prohibited = false): void - { - Log::debug('Now in setExactMetaDateParams()'); - - /** - * @var string $key - * @var Carbon|string $value - */ - foreach ($range as $key => $value) { - $key = $prohibited ? sprintf('%s_not', $key) : $key; - - switch ($key) { - default: - throw new FireflyException(sprintf('Cannot handle key "%s" in setExactMetaDateParams()', $key)); - - case 'exact': - if ($value instanceof Carbon) { - Log::debug(sprintf('Set %s_is_exact value "%s"', $field, $value->format('Y-m-d'))); - $this->collector->setMetaDateRange($value, $value, $field); - $this->operators->push(['type' => sprintf('%s_on', $field), 'value' => $value->format('Y-m-d')]); - } - - break; - - case 'exact_not': - if ($value instanceof Carbon) { - Log::debug(sprintf('Set NOT %s_is_exact value "%s"', $field, $value->format('Y-m-d'))); - $this->collector->excludeMetaDateRange($value, $value, $field); - $this->operators->push(['type' => sprintf('not_%s_on', $field), 'value' => $value->format('Y-m-d')]); - } - - break; - - case 'year': - if (is_string($value)) { - Log::debug(sprintf('Set %s_is_exact YEAR value "%s"', $field, $value)); - $this->collector->metaYearIs($value, $field); - $this->operators->push(['type' => sprintf('%s_on_year', $field), 'value' => $value]); - } - - break; - - case 'year_not': - if (is_string($value)) { - Log::debug(sprintf('Set NOT %s_is_exact YEAR value "%s"', $field, $value)); - $this->collector->metaYearIsNot($value, $field); - $this->operators->push(['type' => sprintf('not_%s_on_year', $field), 'value' => $value]); - } - - break; - - case 'month': - if (is_string($value)) { - Log::debug(sprintf('Set %s_is_exact MONTH value "%s"', $field, $value)); - $this->collector->metaMonthIs($value, $field); - $this->operators->push(['type' => sprintf('%s_on_month', $field), 'value' => $value]); - } - - break; - - case 'month_not': - if (is_string($value)) { - Log::debug(sprintf('Set NOT %s_is_exact MONTH value "%s"', $field, $value)); - $this->collector->metaMonthIsNot($value, $field); - $this->operators->push(['type' => sprintf('not_%s_on_month', $field), 'value' => $value]); - } - - break; - - case 'day': - if (is_string($value)) { - Log::debug(sprintf('Set %s_is_exact DAY value "%s"', $field, $value)); - $this->collector->metaDayIs($value, $field); - $this->operators->push(['type' => sprintf('%s_on_day', $field), 'value' => $value]); - } - - break; - - case 'day_not': - if (is_string($value)) { - Log::debug(sprintf('Set NOT %s_is_exact DAY value "%s"', $field, $value)); - $this->collector->metaDayIsNot($value, $field); - $this->operators->push(['type' => sprintf('not_%s_on_day', $field), 'value' => $value]); - } - - break; - } - } - } - - /** - * @throws FireflyException - * - * @SuppressWarnings("PHPMD.BooleanArgumentFlag") - */ - private function setMetaDateBeforeParams(string $field, array $range, bool $prohibited = false): void - { - /** - * @var string $key - * @var Carbon|string $value - */ - foreach ($range as $key => $value) { - $key = $prohibited ? sprintf('%s_not', $key) : $key; - - switch ($key) { - default: - throw new FireflyException(sprintf('Cannot handle key "%s" in setMetaDateBeforeParams()', $key)); - - case 'exact': - if ($value instanceof Carbon) { - $this->collector->setMetaBefore($value, $field); - $this->operators->push(['type' => sprintf('%s_before', $field), 'value' => $value->format('Y-m-d')]); - } - - break; - - case 'year': - if (is_string($value)) { - Log::debug(sprintf('Set %s_is_before YEAR value "%s"', $field, $value)); - $this->collector->metaYearBefore($value, $field); - $this->operators->push(['type' => sprintf('%s_before_year', $field), 'value' => $value]); - } - - break; - - case 'month': - if (is_string($value)) { - Log::debug(sprintf('Set %s_is_before MONTH value "%s"', $field, $value)); - $this->collector->metaMonthBefore($value, $field); - $this->operators->push(['type' => sprintf('%s_before_month', $field), 'value' => $value]); - } - - break; - - case 'day': - if (is_string($value)) { - Log::debug(sprintf('Set %s_is_before DAY value "%s"', $field, $value)); - $this->collector->metaDayBefore($value, $field); - $this->operators->push(['type' => sprintf('%s_before_day', $field), 'value' => $value]); - } - - break; - } - } - } - - /** - * @throws FireflyException - * - * @SuppressWarnings("PHPMD.BooleanArgumentFlag") - */ - private function setMetaDateAfterParams(string $field, array $range, bool $prohibited = false): void - { - /** - * @var string $key - * @var Carbon|string $value - */ - foreach ($range as $key => $value) { - $key = $prohibited ? sprintf('%s_not', $key) : $key; - - switch ($key) { - default: - throw new FireflyException(sprintf('Cannot handle key "%s" in setMetaDateAfterParams()', $key)); - - case 'exact': - if ($value instanceof Carbon) { - $this->collector->setMetaAfter($value, $field); - $this->operators->push(['type' => sprintf('%s_after', $field), 'value' => $value->format('Y-m-d')]); - } - - break; - - case 'year': - if (is_string($value)) { - Log::debug(sprintf('Set %s_is_after YEAR value "%s"', $field, $value)); - $this->collector->metaYearAfter($value, $field); - $this->operators->push(['type' => sprintf('%s_after_year', $field), 'value' => $value]); - } - - break; - - case 'month': - if (is_string($value)) { - Log::debug(sprintf('Set %s_is_after MONTH value "%s"', $field, $value)); - $this->collector->metaMonthAfter($value, $field); - $this->operators->push(['type' => sprintf('%s_after_month', $field), 'value' => $value]); - } - - break; - - case 'day': - if (is_string($value)) { - Log::debug(sprintf('Set %s_is_after DAY value "%s"', $field, $value)); - $this->collector->metaDayAfter($value, $field); - $this->operators->push(['type' => sprintf('%s_after_day', $field), 'value' => $value]); - } - - break; - } - } - } - - /** - * @throws FireflyException - * - * @SuppressWarnings("PHPMD.BooleanArgumentFlag") - */ - private function setExactObjectDateParams(string $field, array $range, bool $prohibited = false): void - { - /** - * @var string $key - * @var Carbon|string $value - */ - foreach ($range as $key => $value) { - $key = $prohibited ? sprintf('%s_not', $key) : $key; - - switch ($key) { - default: - throw new FireflyException(sprintf('Cannot handle key "%s" in setExactObjectDateParams()', $key)); - - case 'exact': - if ($value instanceof Carbon) { - Log::debug(sprintf('Set %s_is_exact value "%s"', $field, $value->format('Y-m-d'))); - $this->collector->setObjectRange($value, clone $value, $field); - $this->operators->push(['type' => sprintf('%s_on', $field), 'value' => $value->format('Y-m-d')]); - } - - break; - - case 'exact_not': - if ($value instanceof Carbon) { - Log::debug(sprintf('Set NOT %s_is_exact value "%s"', $field, $value->format('Y-m-d'))); - $this->collector->excludeObjectRange($value, clone $value, $field); - $this->operators->push(['type' => sprintf('not_%s_on', $field), 'value' => $value->format('Y-m-d')]); - } - - break; - - case 'year': - if (is_string($value)) { - Log::debug(sprintf('Set %s_is_exact YEAR value "%s"', $field, $value)); - $this->collector->objectYearIs($value, $field); - $this->operators->push(['type' => sprintf('%s_on_year', $field), 'value' => $value]); - } - - break; - - case 'year_not': - if (is_string($value)) { - Log::debug(sprintf('Set NOT %s_is_exact YEAR value "%s"', $field, $value)); - $this->collector->objectYearIsNot($value, $field); - $this->operators->push(['type' => sprintf('not_%s_on_year', $field), 'value' => $value]); - } - - break; - - case 'month': - if (is_string($value)) { - Log::debug(sprintf('Set %s_is_exact MONTH value "%s"', $field, $value)); - $this->collector->objectMonthIs($value, $field); - $this->operators->push(['type' => sprintf('%s_on_month', $field), 'value' => $value]); - } - - break; - - case 'month_not': - if (is_string($value)) { - Log::debug(sprintf('Set NOT %s_is_exact MONTH value "%s"', $field, $value)); - $this->collector->objectMonthIsNot($value, $field); - $this->operators->push(['type' => sprintf('not_%s_on_month', $field), 'value' => $value]); - } - - break; - - case 'day': - if (is_string($value)) { - Log::debug(sprintf('Set %s_is_exact DAY value "%s"', $field, $value)); - $this->collector->objectDayIs($value, $field); - $this->operators->push(['type' => sprintf('%s_on_day', $field), 'value' => $value]); - } - - break; - - case 'day_not': - if (is_string($value)) { - Log::debug(sprintf('Set NOT %s_is_exact DAY value "%s"', $field, $value)); - $this->collector->objectDayIsNot($value, $field); - $this->operators->push(['type' => sprintf('not_%s_on_day', $field), 'value' => $value]); - } - - break; - } - } - } - - /** - * @throws FireflyException - * - * @SuppressWarnings("PHPMD.BooleanArgumentFlag") - */ - private function setObjectDateBeforeParams(string $field, array $range, bool $prohibited = false): void - { - /** - * @var string $key - * @var Carbon|string $value - */ - foreach ($range as $key => $value) { - $key = $prohibited ? sprintf('%s_not', $key) : $key; - - switch ($key) { - default: - throw new FireflyException(sprintf('Cannot handle key "%s" in setObjectDateBeforeParams()', $key)); - - case 'exact': - if ($value instanceof Carbon) { - $this->collector->setObjectBefore($value, $field); - $this->operators->push(['type' => sprintf('%s_before', $field), 'value' => $value->format('Y-m-d')]); - } - - break; - - case 'year': - if (is_string($value)) { - Log::debug(sprintf('Set date_is_before YEAR value "%s"', $value)); - $this->collector->objectYearBefore($value, $field); - $this->operators->push(['type' => sprintf('%s_before_year', $field), 'value' => $value]); - } - - break; - - case 'month': - if (is_string($value)) { - Log::debug(sprintf('Set date_is_before MONTH value "%s"', $value)); - $this->collector->objectMonthBefore($value, $field); - $this->operators->push(['type' => sprintf('%s_before_month', $field), 'value' => $value]); - } - - break; - - case 'day': - if (is_string($value)) { - Log::debug(sprintf('Set date_is_before DAY value "%s"', $value)); - $this->collector->objectDayBefore($value, $field); - $this->operators->push(['type' => sprintf('%s_before_day', $field), 'value' => $value]); - } - - break; - } - } - } - - /** - * @throws FireflyException - * - * @SuppressWarnings("PHPMD.BooleanArgumentFlag") - */ - private function setObjectDateAfterParams(string $field, array $range, bool $prohibited = false): void - { - /** - * @var string $key - * @var Carbon|string $value - */ - foreach ($range as $key => $value) { - $key = $prohibited ? sprintf('%s_not', $key) : $key; - - switch ($key) { - default: - throw new FireflyException(sprintf('Cannot handle key "%s" in setObjectDateAfterParams()', $key)); - - case 'exact': - if ($value instanceof Carbon) { - $this->collector->setObjectAfter($value, $field); - $this->operators->push(['type' => sprintf('%s_after', $field), 'value' => $value->format('Y-m-d')]); - } - - break; - - case 'year': - if (is_string($value)) { - Log::debug(sprintf('Set date_is_after YEAR value "%s"', $value)); - $this->collector->objectYearAfter($value, $field); - $this->operators->push(['type' => sprintf('%s_after_year', $field), 'value' => $value]); - } - - break; - - case 'month': - if (is_string($value)) { - Log::debug(sprintf('Set date_is_after MONTH value "%s"', $value)); - $this->collector->objectMonthAfter($value, $field); - $this->operators->push(['type' => sprintf('%s_after_month', $field), 'value' => $value]); - } - - break; - - case 'day': - if (is_string($value)) { - Log::debug(sprintf('Set date_is_after DAY value "%s"', $value)); - $this->collector->objectDayAfter($value, $field); - $this->operators->push(['type' => sprintf('%s_after_day', $field), 'value' => $value]); - } - - break; - } - } - } - - private function handleNodeGroup(NodeGroup $node, bool $flipProhibitedFlag): void - { - $prohibited = $node->isProhibited($flipProhibitedFlag); - - foreach ($node->getNodes() as $subNode) { - $this->handleSearchNode($subNode, $prohibited); - } - } - - public function searchTime(): float - { - return microtime(true) - $this->startTime; - } - - public function searchTransactions(): LengthAwarePaginator - { - $this->parseTagInstructions(); - if (0 === count($this->getWords()) && 0 === count($this->getExcludedWords()) && 0 === count($this->getOperators())) { - return new LengthAwarePaginator([], 0, 5, 1); - } - - return $this->collector->getPaginatedGroups(); - } - - private function parseTagInstructions(): void - { - Log::debug('Now in parseTagInstructions()'); - // if exclude tags, remove excluded tags. - if (count($this->excludeTags) > 0) { - Log::debug(sprintf('%d exclude tag(s)', count($this->excludeTags))); - $collection = new Collection(); - foreach ($this->excludeTags as $tagId) { - $tag = $this->tagRepository->find($tagId); - if (null !== $tag) { - Log::debug(sprintf('Exclude tag "%s"', $tag->tag)); - $collection->push($tag); - } - } - Log::debug(sprintf('Selecting all tags except %d excluded tag(s).', $collection->count())); - $this->collector->setWithoutSpecificTags($collection); - } - // if include tags, include them: - if (count($this->includeTags) > 0) { - Log::debug(sprintf('%d include tag(s)', count($this->includeTags))); - $collection = new Collection(); - foreach ($this->includeTags as $tagId) { - $tag = $this->tagRepository->find($tagId); - if (null !== $tag) { - Log::debug(sprintf('Include tag "%s"', $tag->tag)); - $collection->push($tag); - } - } - $this->collector->setAllTags($collection); - } - // if include ANY tags, include them: (see #8632) - if (count($this->includeAnyTags) > 0) { - Log::debug(sprintf('%d include ANY tag(s)', count($this->includeAnyTags))); - $collection = new Collection(); - foreach ($this->includeAnyTags as $tagId) { - $tag = $this->tagRepository->find($tagId); - if (null !== $tag) { - Log::debug(sprintf('Include ANY tag "%s"', $tag->tag)); - $collection->push($tag); - } - } - $this->collector->setTags($collection); - } - } - - public function getWords(): array - { - return $this->words; - } - - public function getExcludedWords(): array - { - return $this->prohibitedWords; - } - - public function setDate(Carbon $date): void - { - $this->date = $date; - } - - public function setPage(int $page): void - { - $this->page = $page; - $this->collector->setPage($this->page); - } - - public function setUser(User $user): void - { - $this->accountRepository->setUser($user); - $this->billRepository->setUser($user); - $this->categoryRepository->setUser($user); - $this->budgetRepository->setUser($user); - $this->tagRepository->setUser($user); - $this->collector = app(GroupCollectorInterface::class); - $this->collector->setUser($user); - $this->collector->withAccountInformation()->withCategoryInformation()->withBudgetInformation(); - - $this->setLimit((int) app('preferences')->getForUser($user, 'listPageSize', 50)->data); - } - - public function setLimit(int $limit): void - { - $this->limit = $limit; - $this->collector->setLimit($this->limit); - } } diff --git a/app/Support/Search/QueryParser/GdbotsQueryParser.php b/app/Support/Search/QueryParser/GdbotsQueryParser.php index a402013e48..0e670a21df 100644 --- a/app/Support/Search/QueryParser/GdbotsQueryParser.php +++ b/app/Support/Search/QueryParser/GdbotsQueryParser.php @@ -32,7 +32,6 @@ use Gdbots\QueryParser\QueryParser as BaseQueryParser; use Illuminate\Support\Facades\Log; use LogicException; use TypeError; - use function Safe\fwrite; class GdbotsQueryParser implements QueryParserInterface @@ -52,12 +51,12 @@ class GdbotsQueryParser implements QueryParserInterface try { $result = $this->parser->parse($query); $nodes = array_map( - fn (GdbotsNode\Node $node) => $this->convertNode($node), + fn(GdbotsNode\Node $node) => $this->convertNode($node), $result->getNodes() ); return new NodeGroup($nodes); - } catch (LogicException|TypeError $e) { + } catch (LogicException | TypeError $e) { fwrite(STDERR, "Setting up GdbotsQueryParserTest\n"); app('log')->error($e->getMessage()); app('log')->error(sprintf('Could not parse search: "%s".', $query)); @@ -76,7 +75,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() ); @@ -85,7 +84,7 @@ class GdbotsQueryParser implements QueryParserInterface return new NodeGroup( array_map( - fn (GdbotsNode\Node $subNode) => $this->convertNode($subNode), + fn(GdbotsNode\Node $subNode) => $this->convertNode($subNode), $node->getNodes() ) ); @@ -98,7 +97,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( diff --git a/app/Support/Search/QueryParser/QueryParser.php b/app/Support/Search/QueryParser/QueryParser.php index c9072f970b..2533bcc3a8 100644 --- a/app/Support/Search/QueryParser/QueryParser.php +++ b/app/Support/Search/QueryParser/QueryParser.php @@ -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 = ''; @@ -155,7 +139,7 @@ class QueryParser implements QueryParserInterface if ('' === $tokenUnderConstruction) { // In any other location, it's just a normal character $tokenUnderConstruction .= $char; - $skipNext = true; + $skipNext = true; } if ('' !== $tokenUnderConstruction && !$skipNext) { // @phpstan-ignore-line Log::debug(sprintf('Turns out that "%s" is a field name. Reset the token.', $tokenUnderConstruction)); @@ -187,13 +171,29 @@ class QueryParser implements QueryParserInterface ++$this->position; } - $finalNode = '' !== $tokenUnderConstruction || '' !== $fieldName + $finalNode = '' !== $tokenUnderConstruction || '' !== $fieldName ? $this->createNode($tokenUnderConstruction, $fieldName, $prohibited) : null; 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) { diff --git a/app/Support/Singleton/PreferencesSingleton.php b/app/Support/Singleton/PreferencesSingleton.php index 32b9bb94f6..e8ff779c5e 100644 --- a/app/Support/Singleton/PreferencesSingleton.php +++ b/app/Support/Singleton/PreferencesSingleton.php @@ -29,7 +29,7 @@ class PreferencesSingleton { private static ?PreferencesSingleton $instance = null; - private array $preferences = []; + private array $preferences = []; private function __construct() { @@ -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; - } } diff --git a/app/Support/Steam.php b/app/Support/Steam.php index e103f19ece..c9a13be8b7 100644 --- a/app/Support/Steam.php +++ b/app/Support/Steam.php @@ -38,7 +38,6 @@ use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Log; use Illuminate\Support\Str; use ValueError; - use function Safe\parse_url; use function Safe\preg_replace; @@ -47,6 +46,81 @@ 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 */ @@ -66,27 +140,15 @@ class Steam // Log::debug(sprintf('Trying bcround("%s",%d)', $number, $precision)); if (str_contains($number, '.')) { if ('-' !== $number[0]) { - return bcadd($number, '0.'.str_repeat('0', $precision).'5', $precision); + return bcadd($number, '0.' . str_repeat('0', $precision) . '5', $precision); } - return bcsub($number, '0.'.str_repeat('0', $precision).'5', $precision); + return bcsub($number, '0.' . str_repeat('0', $precision) . '5', $precision); } 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 +200,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 +271,94 @@ 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. @@ -205,7 +367,7 @@ class Steam Log::debug(sprintf('finalAccountBalanceInRange(#%d, %s, %s)', $account->id, $start->format('Y-m-d H:i:s'), $end->format('Y-m-d H:i:s'))); // set up cache - $cache = new CacheProperties(); + $cache = new CacheProperties(); $cache->addProperty($account->id); $cache->addProperty('final-balance-in-range'); $cache->addProperty($start); @@ -215,22 +377,22 @@ class Steam return $cache->get(); } - $balances = []; - $formatted = $start->format('Y-m-d'); + $balances = []; + $formatted = $start->format('Y-m-d'); /* * To make sure the start balance is correct, we need to get the balance at the exact end of the previous day. * Since we just did "startOfDay" we can do subDay()->endOfDay() to get the correct moment. * THAT will be the start balance. */ - $request = clone $start; + $request = clone $start; $request->subDay()->endOfDay(); Log::debug('Get first balance to start.'); Log::debug(sprintf('finalAccountBalanceInRange: Call finalAccountBalance with date/time "%s"', $request->toIso8601String())); - $startBalance = $this->finalAccountBalance($account, $request); - $primaryCurrency = Amount::getPrimaryCurrencyByUserGroup($account->user->userGroup); - $accountCurrency = $this->getAccountCurrency($account); - $hasCurrency = $accountCurrency instanceof TransactionCurrency; - $currency = $accountCurrency ?? $primaryCurrency; + $startBalance = $this->finalAccountBalance($account, $request); + $primaryCurrency = Amount::getPrimaryCurrencyByUserGroup($account->user->userGroup); + $accountCurrency = $this->getAccountCurrency($account); + $hasCurrency = $accountCurrency instanceof TransactionCurrency; + $currency = $accountCurrency ?? $primaryCurrency; Log::debug(sprintf('Currency is %s', $currency->code)); @@ -243,7 +405,7 @@ class Steam Log::debug(sprintf('Also set start balance in %s', $primaryCurrency->code)); $startBalance[$primaryCurrency->code] ??= '0'; } - $currencies = [ + $currencies = [ $currency->id => $currency, $primaryCurrency->id => $primaryCurrency, ]; @@ -253,48 +415,47 @@ class Steam // sums up the balance changes per day. Log::debug(sprintf('Date >= %s and <= %s', $start->format('Y-m-d H:i:s'), $end->format('Y-m-d H:i:s'))); - $set = $account->transactions() - ->leftJoin('transaction_journals', 'transactions.transaction_journal_id', '=', 'transaction_journals.id') - ->where('transaction_journals.date', '>=', $start->format('Y-m-d H:i:s')) - ->where('transaction_journals.date', '<=', $end->format('Y-m-d H:i:s')) - ->groupBy('transaction_journals.date') - ->groupBy('transactions.transaction_currency_id') - ->orderBy('transaction_journals.date', 'ASC') - ->whereNull('transaction_journals.deleted_at') - ->get( - [ // @phpstan-ignore-line - 'transaction_journals.date', - 'transactions.transaction_currency_id', - DB::raw('SUM(transactions.amount) AS sum_of_day'), - ] - ) - ; + $set = $account->transactions() + ->leftJoin('transaction_journals', 'transactions.transaction_journal_id', '=', 'transaction_journals.id') + ->where('transaction_journals.date', '>=', $start->format('Y-m-d H:i:s')) + ->where('transaction_journals.date', '<=', $end->format('Y-m-d H:i:s')) + ->groupBy('transaction_journals.date') + ->groupBy('transactions.transaction_currency_id') + ->orderBy('transaction_journals.date', 'ASC') + ->whereNull('transaction_journals.deleted_at') + ->get( + [ // @phpstan-ignore-line + 'transaction_journals.date', + 'transactions.transaction_currency_id', + DB::raw('SUM(transactions.amount) AS sum_of_day'), + ] + ); - $currentBalance = $startBalance; - $converter = new ExchangeRateConverter(); + $currentBalance = $startBalance; + $converter = new ExchangeRateConverter(); /** @var Transaction $entry */ foreach ($set as $entry) { // get date object - $carbon = new Carbon($entry->date, $entry->date_tz); - $carbonKey = $carbon->format('Y-m-d'); + $carbon = new Carbon($entry->date, $entry->date_tz); + $carbonKey = $carbon->format('Y-m-d'); // make sure sum is a string: - $sumOfDay = (string)($entry->sum_of_day ?? '0'); + $sumOfDay = (string)($entry->sum_of_day ?? '0'); // #10426 make sure sum is not in scientific notation. - $sumOfDay = $this->floatalize($sumOfDay); + $sumOfDay = $this->floatalize($sumOfDay); // find currency of this entry, does not have to exist. $currencies[$entry->transaction_currency_id] ??= Amount::getTransactionCurrencyById($entry->transaction_currency_id); // make sure this $entry has its own $entryCurrency /** @var TransactionCurrency $entryCurrency */ - $entryCurrency = $currencies[$entry->transaction_currency_id]; + $entryCurrency = $currencies[$entry->transaction_currency_id]; Log::debug(sprintf('Processing transaction(s) on moment %s', $carbon->format('Y-m-d H:i:s'))); // add amount to current balance in currency code. - $currentBalance[$entryCurrency->code] ??= '0'; + $currentBalance[$entryCurrency->code] ??= '0'; $currentBalance[$entryCurrency->code] = bcadd($sumOfDay, (string)$currentBalance[$entryCurrency->code]); // if not requested to convert to primary currency, add the amount to "balance", do nothing else. @@ -312,7 +473,7 @@ class Steam } } // add to final array. - $balances[$carbonKey] = $currentBalance; + $balances[$carbonKey] = $currentBalance; Log::debug(sprintf('Updated entry [%s]', $carbonKey), $currentBalance); } $cache->store($balances); @@ -321,175 +482,40 @@ 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 { - $type = $account->accountType->type; - $list = config('firefly.valid_currency_account_types'); + $type = $account->accountType->type; + $list = config('firefly.valid_currency_account_types'); // return null if not in this list. if (!in_array($type, $list, true)) { @@ -503,45 +529,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,19 +550,34 @@ 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 = []; - $set = auth()->user()->transactions() - ->whereIn('transactions.account_id', $accounts) - ->groupBy(['transactions.account_id', 'transaction_journals.user_id']) - ->get(['transactions.account_id', DB::raw('MAX(transaction_journals.date) AS max_date')]) // @phpstan-ignore-line + $set = auth()->user()->transactions() + ->whereIn('transactions.account_id', $accounts) + ->groupBy(['transactions.account_id', 'transaction_journals.user_id']) + ->get(['transactions.account_id', DB::raw('MAX(transaction_journals.date) AS max_date')]) // @phpstan-ignore-line ; /** @var Transaction $entry */ foreach ($set as $entry) { - $date = new Carbon($entry->max_date, config('app.timezone')); + $date = new Carbon($entry->max_date, config('app.timezone')); $date->setTimezone(config('app.timezone')); $list[(int)$entry->account_id] = $date; } @@ -605,21 +607,6 @@ class Steam 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 [ @@ -650,9 +637,9 @@ class Steam public function getSafeUrl(string $unknownUrl, string $safeUrl): string { // Log::debug(sprintf('getSafeUrl(%s, %s)', $unknownUrl, $safeUrl)); - $returnUrl = $safeUrl; - $unknownHost = parse_url($unknownUrl, PHP_URL_HOST); - $safeHost = parse_url($safeUrl, PHP_URL_HOST); + $returnUrl = $safeUrl; + $unknownHost = parse_url($unknownUrl, PHP_URL_HOST); + $safeHost = parse_url($safeUrl, PHP_URL_HOST); if (null !== $unknownHost && $unknownHost === $safeHost) { $returnUrl = $unknownUrl; @@ -681,36 +668,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 +725,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 = []; @@ -776,8 +760,8 @@ class Steam $primary = Amount::getPrimaryCurrency(); $currencies[$primary->id] = $primary; - $ids = $accounts->pluck('id')->toArray(); - $result = AccountMeta::whereIn('account_id', $ids)->where('name', 'currency_id')->get(); + $ids = $accounts->pluck('id')->toArray(); + $result = AccountMeta::whereIn('account_id', $ids)->where('name', 'currency_id')->get(); /** @var AccountMeta $item */ foreach ($result as $item) { @@ -787,7 +771,7 @@ class Steam } } // collect those currencies, skip primary because we already have it. - $set = TransactionCurrency::whereIn('id', $accountPreferences)->where('id', '!=', $primary->id)->get(); + $set = TransactionCurrency::whereIn('id', $accountPreferences)->where('id', '!=', $primary->id)->get(); foreach ($set as $item) { $currencies[$item->id] = $item; } @@ -798,7 +782,7 @@ class Steam $currencyPresent = isset($account->meta) && array_key_exists('currency', $account->meta) && null !== $account->meta['currency']; if ($currencyPresent) { $currencyId = $account->meta['currency']->id; - $currencies[$currencyId] ??= $account->meta['currency']; + $currencies[$currencyId] ??= $account->meta['currency']; $accountCurrencies[$accountId] = $account->meta['currency']; } if (!$currencyPresent && !array_key_exists($accountId, $accountPreferences)) { @@ -811,4 +795,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; + } } diff --git a/app/Support/System/GeneratesInstallationId.php b/app/Support/System/GeneratesInstallationId.php index 20cd0a303c..732237214f 100644 --- a/app/Support/System/GeneratesInstallationId.php +++ b/app/Support/System/GeneratesInstallationId.php @@ -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); } diff --git a/app/Support/System/OAuthKeys.php b/app/Support/System/OAuthKeys.php index 53e353481f..1c1f2276cf 100644 --- a/app/Support/System/OAuthKeys.php +++ b/app/Support/System/OAuthKeys.php @@ -31,7 +31,6 @@ use Illuminate\Support\Facades\Crypt; use Laravel\Passport\Console\KeysCommand; use Psr\Container\ContainerExceptionInterface; use Psr\Container\NotFoundExceptionInterface; - use function Safe\file_get_contents; use function Safe\file_put_contents; @@ -43,6 +42,78 @@ class OAuthKeys private const string PRIVATE_KEY = 'oauth_private_key'; private const string PUBLIC_KEY = 'oauth_public_key'; + public static function generateKeys(): void + { + Artisan::registerCommand(new KeysCommand()); + Artisan::call('firefly-iii:laravel-passport-keys'); + } + + 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 keysInDatabase(): bool + { + $privateKey = ''; + $publicKey = ''; + // 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; + } catch (ContainerExceptionInterface | FireflyException | NotFoundExceptionInterface $e) { + app('log')->error(sprintf('Could not validate keysInDatabase(): %s', $e->getMessage())); + app('log')->error($e->getTraceAsString()); + } + } + if ('' !== $privateKey && '' !== $publicKey) { + return true; + } + + return false; + } + + /** + * @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; + + try { + $privateContent = Crypt::decrypt($privateKey); + $publicContent = Crypt::decrypt($publicKey); + } catch (DecryptException $e) { + app('log')->error('Could not decrypt pub/private keypair.'); + app('log')->error($e->getMessage()); + + // delete config vars from DB: + app('fireflyconfig')->delete(self::PRIVATE_KEY); + app('fireflyconfig')->delete(self::PUBLIC_KEY); + + return false; + } + $private = storage_path('oauth-private.key'); + $public = storage_path('oauth-public.key'); + file_put_contents($private, $privateContent); + file_put_contents($public, $publicContent); + + 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()) { @@ -60,76 +131,4 @@ class OAuthKeys self::storeKeysInDB(); } } - - public static function keysInDatabase(): bool - { - $privateKey = ''; - $publicKey = ''; - // 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; - } catch (ContainerExceptionInterface|FireflyException|NotFoundExceptionInterface $e) { - app('log')->error(sprintf('Could not validate keysInDatabase(): %s', $e->getMessage())); - app('log')->error($e->getTraceAsString()); - } - } - if ('' !== $privateKey && '' !== $publicKey) { - return true; - } - - 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; - - try { - $privateContent = Crypt::decrypt($privateKey); - $publicContent = Crypt::decrypt($publicKey); - } catch (DecryptException $e) { - app('log')->error('Could not decrypt pub/private keypair.'); - app('log')->error($e->getMessage()); - - // delete config vars from DB: - app('fireflyconfig')->delete(self::PRIVATE_KEY); - app('fireflyconfig')->delete(self::PUBLIC_KEY); - - return false; - } - $private = storage_path('oauth-private.key'); - $public = storage_path('oauth-public.key'); - file_put_contents($private, $privateContent); - file_put_contents($public, $publicContent); - - return true; - } } diff --git a/app/Support/Twig/AmountFormat.php b/app/Support/Twig/AmountFormat.php index 39cc1594db..49c9a6d11e 100644 --- a/app/Support/Twig/AmountFormat.php +++ b/app/Support/Twig/AmountFormat.php @@ -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,50 +94,6 @@ class AmountFormat extends AbstractExtension ); } - /** - * Will format the amount by the currency related to the given account. - */ - protected function formatAmountBySymbol(): TwigFunction - { - return new TwigFunction( - 'formatAmountBySymbol', - static function (string $amount, ?string $symbol = null, ?int $decimalPlaces = null, ?bool $coloured = null): string { - - if (null === $symbol) { - $message = sprintf('formatAmountBySymbol("%s", %s, %d, %s) was called without a symbol. Please browse to /flush to clear your cache.', $amount, var_export($symbol, true), $decimalPlaces, var_export($coloured, true)); - Log::error($message); - $currency = Amount::getPrimaryCurrency(); - } - if (null !== $symbol) { - $decimalPlaces ??= 2; - $coloured ??= true; - $currency = new TransactionCurrency(); - $currency->symbol = $symbol; - $currency->decimal_places = $decimalPlaces; - } - - 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']] - ); - } - /** * Use the code to format a currency. */ @@ -175,4 +118,61 @@ class AmountFormat extends AbstractExtension ['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. + */ + protected function formatAmountBySymbol(): TwigFunction + { + return new TwigFunction( + 'formatAmountBySymbol', + static function (string $amount, ?string $symbol = null, ?int $decimalPlaces = null, ?bool $coloured = null): string { + + if (null === $symbol) { + $message = sprintf('formatAmountBySymbol("%s", %s, %d, %s) was called without a symbol. Please browse to /flush to clear your cache.', $amount, var_export($symbol, true), $decimalPlaces, var_export($coloured, true)); + Log::error($message); + $currency = Amount::getPrimaryCurrency(); + } + if (null !== $symbol) { + $decimalPlaces ??= 2; + $coloured ??= true; + $currency = new TransactionCurrency(); + $currency->symbol = $symbol; + $currency->decimal_places = $decimalPlaces; + } + + return Amount::formatAnything($currency, $amount, $coloured); + }, + ['is_safe' => ['html']] + ); + } + + 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']] + ); + } } diff --git a/app/Support/Twig/General.php b/app/Support/Twig/General.php index 6f71d6f578..337e832312 100644 --- a/app/Support/Twig/General.php +++ b/app/Support/Twig/General.php @@ -37,7 +37,6 @@ use Override; use Twig\Extension\AbstractExtension; use Twig\TwigFilter; use Twig\TwigFunction; - use function Safe\parse_url; /** @@ -57,144 +56,6 @@ class General extends AbstractExtension ]; } - /** - * Show account balance. Only used on the front page of Firefly III. - */ - protected function balance(): TwigFilter - { - return new TwigFilter( - 'balance', - static function (?Account $account): string { - if (!$account instanceof Account) { - return '0'; - } - - /** @var Carbon $date */ - $date = session('end', today(config('app.timezone'))->endOfMonth()); - Log::debug(sprintf('twig balance: Call finalAccountBalance with date/time "%s"', $date->toIso8601String())); - $info = Steam::finalAccountBalance($account, $date); - $currency = Steam::getAccountCurrency($account); - $primary = Amount::getPrimaryCurrency(); - $convertToPrimary = Amount::convertToPrimary(); - $usePrimary = $convertToPrimary && $primary->id !== $currency->id; - $currency ??= $primary; - $strings = []; - foreach ($info as $key => $balance) { - if ('balance' === $key) { - // balance in account currency. - if (!$usePrimary) { - $strings[] = app('amount')->formatAnything($currency, $balance, false); - } - - continue; - } - if ('pc_balance' === $key) { - // balance in primary currency. - if ($usePrimary) { - $strings[] = app('amount')->formatAnything($primary, $balance, false); - } - - continue; - } - // for multi currency accounts. - if ($usePrimary && $key !== $primary->code) { - $strings[] = app('amount')->formatAnything(Amount::getTransactionCurrencyByCode($key), $balance, false); - } - } - - return implode(', ', $strings); - // return app('steam')->balance($account, $date); - } - ); - } - - /** - * Used to convert 1024 to 1kb etc. - */ - protected function formatFilesize(): TwigFilter - { - return new TwigFilter( - 'filesize', - static function (int $size): string { - // less than one GB, more than one MB - if ($size < (1024 * 1024 * 2014) && $size >= (1024 * 1024)) { - return round($size / (1024 * 1024), 2).' MB'; - } - - // less than one MB - if ($size < (1024 * 1024)) { - return round($size / 1024, 2).' KB'; - } - - return $size.' bytes'; - } - ); - } - - /** - * Show icon with attachment. - * - * @SuppressWarnings("PHPMD.CyclomaticComplexity") - */ - protected function mimeIcon(): TwigFilter - { - return new TwigFilter( - 'mimeIcon', - static fn (string $string): string => match ($string) { - 'application/pdf' => 'fa-file-pdf-o', - 'image/webp', 'image/png', 'image/jpeg', 'image/svg+xml', 'image/heic', 'image/heic-sequence', 'application/vnd.oasis.opendocument.image' => 'fa-file-image-o', - 'application/msword', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', 'application/vnd.openxmlformats-officedocument.wordprocessingml.template', 'application/x-iwork-pages-sffpages', 'application/vnd.sun.xml.writer', 'application/vnd.sun.xml.writer.template', 'application/vnd.sun.xml.writer.global', 'application/vnd.stardivision.writer', 'application/vnd.stardivision.writer-global', 'application/vnd.oasis.opendocument.text', 'application/vnd.oasis.opendocument.text-template', 'application/vnd.oasis.opendocument.text-web', 'application/vnd.oasis.opendocument.text-master' => 'fa-file-word-o', - 'application/vnd.ms-excel', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', 'application/vnd.openxmlformats-officedocument.spreadsheetml.template', 'application/vnd.sun.xml.calc', 'application/vnd.sun.xml.calc.template', 'application/vnd.stardivision.calc', 'application/vnd.oasis.opendocument.spreadsheet', 'application/vnd.oasis.opendocument.spreadsheet-template' => 'fa-file-excel-o', - 'application/vnd.ms-powerpoint', 'application/vnd.openxmlformats-officedocument.presentationml.presentation', 'application/vnd.openxmlformats-officedocument.presentationml.template', 'application/vnd.openxmlformats-officedocument.presentationml.slideshow', 'application/vnd.sun.xml.impress', 'application/vnd.sun.xml.impress.template', 'application/vnd.stardivision.impress', 'application/vnd.oasis.opendocument.presentation', 'application/vnd.oasis.opendocument.presentation-template' => 'fa-file-powerpoint-o', - 'application/vnd.sun.xml.draw', 'application/vnd.sun.xml.draw.template', 'application/vnd.stardivision.draw', 'application/vnd.oasis.opendocument.chart' => 'fa-paint-brush', - 'application/vnd.oasis.opendocument.graphics', 'application/vnd.oasis.opendocument.graphics-template', 'application/vnd.sun.xml.math', 'application/vnd.stardivision.math', 'application/vnd.oasis.opendocument.formula', 'application/vnd.oasis.opendocument.database' => 'fa-calculator', - default => 'fa-file-o', - }, - ['is_safe' => ['html']] - ); - } - - protected function markdown(): TwigFilter - { - return new TwigFilter( - 'markdown', - static function (string $text): string { - $converter = new GithubFlavoredMarkdownConverter( - [ - 'allow_unsafe_links' => false, - 'max_nesting_level' => 5, - 'html_input' => 'escape', - ] - ); - - return (string)$converter->convert($text); - }, - ['is_safe' => ['html']] - ); - } - - /** - * Show URL host name - */ - protected function phpHostName(): TwigFilter - { - return new TwigFilter( - 'phphost', - static function (string $string): string { - $proto = parse_url($string, PHP_URL_SCHEME); - $host = parse_url($string, PHP_URL_HOST); - if (is_array($host)) { - $host = implode(' ', $host); - } - if (is_array($proto)) { - $proto = implode(' ', $proto); - } - - return e(sprintf('%s://%s', $proto, $host)); - } - ); - } - #[Override] public function getFunctions(): array { @@ -212,38 +73,6 @@ class General extends AbstractExtension ]; } - /** - * Basic example thing for some views. - */ - protected function phpdate(): TwigFunction - { - return new TwigFunction( - 'phpdate', - static fn (string $str): string => date($str) - ); - } - - /** - * Will return "active" when the current route matches the given argument - * exactly. - */ - protected function activeRouteStrict(): TwigFunction - { - return new TwigFunction( - 'activeRouteStrict', - static function (): string { - $args = func_get_args(); - $route = $args[0]; // name of the route. - - if (\Route::getCurrentRoute()->getName() === $route) { - return 'active'; - } - - return ''; - } - ); - } - /** * Will return "active" when a part of the route matches the argument. * ie. "accounts" will match "accounts.index". @@ -275,7 +104,7 @@ class General extends AbstractExtension 'activeRoutePartialObjectType', static function ($context): string { [, $route, $objectType] = func_get_args(); - $activeObjectType = $context['objectType'] ?? false; + $activeObjectType = $context['objectType'] ?? false; if ($objectType === $activeObjectType && false !== stripos( @@ -291,6 +120,193 @@ class General extends AbstractExtension ); } + /** + * Will return "active" when the current route matches the given argument + * exactly. + */ + protected function activeRouteStrict(): TwigFunction + { + return new TwigFunction( + 'activeRouteStrict', + static function (): string { + $args = func_get_args(); + $route = $args[0]; // name of the route. + + if (\Route::getCurrentRoute()->getName() === $route) { + return 'active'; + } + + return ''; + } + ); + } + + /** + * Show account balance. Only used on the front page of Firefly III. + */ + protected function balance(): TwigFilter + { + return new TwigFilter( + 'balance', + static function (?Account $account): string { + if (!$account instanceof Account) { + return '0'; + } + + /** @var Carbon $date */ + $date = session('end', today(config('app.timezone'))->endOfMonth()); + Log::debug(sprintf('twig balance: Call finalAccountBalance with date/time "%s"', $date->toIso8601String())); + $info = Steam::finalAccountBalance($account, $date); + $currency = Steam::getAccountCurrency($account); + $primary = Amount::getPrimaryCurrency(); + $convertToPrimary = Amount::convertToPrimary(); + $usePrimary = $convertToPrimary && $primary->id !== $currency->id; + $currency ??= $primary; + $strings = []; + foreach ($info as $key => $balance) { + if ('balance' === $key) { + // balance in account currency. + if (!$usePrimary) { + $strings[] = app('amount')->formatAnything($currency, $balance, false); + } + + continue; + } + if ('pc_balance' === $key) { + // balance in primary currency. + if ($usePrimary) { + $strings[] = app('amount')->formatAnything($primary, $balance, false); + } + + continue; + } + // for multi currency accounts. + if ($usePrimary && $key !== $primary->code) { + $strings[] = app('amount')->formatAnything(Amount::getTransactionCurrencyByCode($key), $balance, false); + } + } + + return implode(', ', $strings); + // return app('steam')->balance($account, $date); + } + ); + } + + protected function carbonize(): TwigFunction + { + return new TwigFunction( + 'carbonize', + static fn(string $date): Carbon => new Carbon($date, config('app.timezone')) + ); + } + + /** + * Formats a string as a thing by converting it to a Carbon first. + */ + protected function formatDate(): TwigFunction + { + return new TwigFunction( + 'formatDate', + static function (string $date, string $format): string { + $carbon = new Carbon($date); + + return $carbon->isoFormat($format); + } + ); + } + + /** + * Used to convert 1024 to 1kb etc. + */ + protected function formatFilesize(): TwigFilter + { + return new TwigFilter( + 'filesize', + static function (int $size): string { + // less than one GB, more than one MB + if ($size < (1024 * 1024 * 2014) && $size >= (1024 * 1024)) { + return round($size / (1024 * 1024), 2) . ' MB'; + } + + // less than one MB + if ($size < (1024 * 1024)) { + return round($size / 1024, 2) . ' KB'; + } + + return $size . ' bytes'; + } + ); + } + + /** + * TODO Remove me when v2 hits. + */ + protected function getMetaField(): TwigFunction + { + return new TwigFunction( + 'accountGetMetaField', + static function (Account $account, string $field): string { + /** @var AccountRepositoryInterface $repository */ + $repository = app(AccountRepositoryInterface::class); + $result = $repository->getMetaValue($account, $field); + if (null === $result) { + return ''; + } + + return $result; + } + ); + } + + protected function getRootSearchOperator(): TwigFunction + { + return new TwigFunction( + 'getRootSearchOperator', + static function (string $operator): string { + $result = OperatorQuerySearch::getRootOperator($operator); + + return str_replace('-', 'not_', $result); + } + ); + } + + /** + * Will return true if the user is of role X. + */ + protected function hasRole(): TwigFunction + { + return new TwigFunction( + 'hasRole', + static function (string $role): bool { + $repository = app(UserRepositoryInterface::class); + if ($repository->hasRole(auth()->user(), $role)) { + return true; + } + + return false; + } + ); + } + + protected function markdown(): TwigFilter + { + return new TwigFilter( + 'markdown', + static function (string $text): string { + $converter = new GithubFlavoredMarkdownConverter( + [ + 'allow_unsafe_links' => false, + 'max_nesting_level' => 5, + 'html_input' => 'escape', + ] + ); + + return (string)$converter->convert($text); + }, + ['is_safe' => ['html']] + ); + } + /** * Will return "menu-open" when a part of the route matches the argument. * ie. "accounts" will match "accounts.index". @@ -313,75 +329,58 @@ class General extends AbstractExtension } /** - * Formats a string as a thing by converting it to a Carbon first. + * Show icon with attachment. + * + * @SuppressWarnings("PHPMD.CyclomaticComplexity") */ - protected function formatDate(): TwigFunction + protected function mimeIcon(): TwigFilter { - return new TwigFunction( - 'formatDate', - static function (string $date, string $format): string { - $carbon = new Carbon($date); + return new TwigFilter( + 'mimeIcon', + static fn(string $string): string => match ($string) { + 'application/pdf' => 'fa-file-pdf-o', + 'image/webp', 'image/png', 'image/jpeg', 'image/svg+xml', 'image/heic', 'image/heic-sequence', 'application/vnd.oasis.opendocument.image' => 'fa-file-image-o', + 'application/msword', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', 'application/vnd.openxmlformats-officedocument.wordprocessingml.template', 'application/x-iwork-pages-sffpages', 'application/vnd.sun.xml.writer', 'application/vnd.sun.xml.writer.template', 'application/vnd.sun.xml.writer.global', 'application/vnd.stardivision.writer', 'application/vnd.stardivision.writer-global', 'application/vnd.oasis.opendocument.text', 'application/vnd.oasis.opendocument.text-template', 'application/vnd.oasis.opendocument.text-web', 'application/vnd.oasis.opendocument.text-master' => 'fa-file-word-o', + 'application/vnd.ms-excel', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', 'application/vnd.openxmlformats-officedocument.spreadsheetml.template', 'application/vnd.sun.xml.calc', 'application/vnd.sun.xml.calc.template', 'application/vnd.stardivision.calc', 'application/vnd.oasis.opendocument.spreadsheet', 'application/vnd.oasis.opendocument.spreadsheet-template' => 'fa-file-excel-o', + 'application/vnd.ms-powerpoint', 'application/vnd.openxmlformats-officedocument.presentationml.presentation', 'application/vnd.openxmlformats-officedocument.presentationml.template', 'application/vnd.openxmlformats-officedocument.presentationml.slideshow', 'application/vnd.sun.xml.impress', 'application/vnd.sun.xml.impress.template', 'application/vnd.stardivision.impress', 'application/vnd.oasis.opendocument.presentation', 'application/vnd.oasis.opendocument.presentation-template' => 'fa-file-powerpoint-o', + 'application/vnd.sun.xml.draw', 'application/vnd.sun.xml.draw.template', 'application/vnd.stardivision.draw', 'application/vnd.oasis.opendocument.chart' => 'fa-paint-brush', + 'application/vnd.oasis.opendocument.graphics', 'application/vnd.oasis.opendocument.graphics-template', 'application/vnd.sun.xml.math', 'application/vnd.stardivision.math', 'application/vnd.oasis.opendocument.formula', 'application/vnd.oasis.opendocument.database' => 'fa-calculator', + default => 'fa-file-o', + }, + ['is_safe' => ['html']] + ); + } - return $carbon->isoFormat($format); + /** + * Show URL host name + */ + protected function phpHostName(): TwigFilter + { + return new TwigFilter( + 'phphost', + static function (string $string): string { + $proto = parse_url($string, PHP_URL_SCHEME); + $host = parse_url($string, PHP_URL_HOST); + if (is_array($host)) { + $host = implode(' ', $host); + } + if (is_array($proto)) { + $proto = implode(' ', $proto); + } + + return e(sprintf('%s://%s', $proto, $host)); } ); } /** - * TODO Remove me when v2 hits. + * Basic example thing for some views. */ - protected function getMetaField(): TwigFunction + protected function phpdate(): TwigFunction { return new TwigFunction( - 'accountGetMetaField', - static function (Account $account, string $field): string { - /** @var AccountRepositoryInterface $repository */ - $repository = app(AccountRepositoryInterface::class); - $result = $repository->getMetaValue($account, $field); - if (null === $result) { - return ''; - } - - return $result; - } - ); - } - - /** - * Will return true if the user is of role X. - */ - protected function hasRole(): TwigFunction - { - return new TwigFunction( - 'hasRole', - static function (string $role): bool { - $repository = app(UserRepositoryInterface::class); - if ($repository->hasRole(auth()->user(), $role)) { - return true; - } - - return false; - } - ); - } - - protected function getRootSearchOperator(): TwigFunction - { - return new TwigFunction( - 'getRootSearchOperator', - static function (string $operator): string { - $result = OperatorQuerySearch::getRootOperator($operator); - - return str_replace('-', 'not_', $result); - } - ); - } - - protected function carbonize(): TwigFunction - { - return new TwigFunction( - 'carbonize', - static fn (string $date): Carbon => new Carbon($date, config('app.timezone')) + 'phpdate', + static fn(string $str): string => date($str) ); } } diff --git a/app/Support/Twig/Rule.php b/app/Support/Twig/Rule.php index c833745d40..7ed872df8b 100644 --- a/app/Support/Twig/Rule.php +++ b/app/Support/Twig/Rule.php @@ -23,34 +23,43 @@ declare(strict_types=1); namespace FireflyIII\Support\Twig; -use Twig\Extension\AbstractExtension; -use Twig\TwigFunction; use Config; use Override; +use Twig\Extension\AbstractExtension; +use Twig\TwigFunction; /** * Class Rule. */ class Rule extends AbstractExtension { - #[Override] - public function getFunctions(): array + public function allActionTriggers(): TwigFunction { - return [ - $this->allJournalTriggers(), - $this->allRuleTriggers(), - $this->allActionTriggers(), - ]; + return new TwigFunction( + 'allRuleActions', + static function () { + // array of valid values for actions + $ruleActions = array_keys(Config::get('firefly.rule-actions')); + $possibleActions = []; + foreach ($ruleActions as $key) { + $possibleActions[$key] = (string)trans('firefly.rule_action_' . $key . '_choice'); + } + unset($ruleActions); + asort($possibleActions); + + return $possibleActions; + } + ); } public function allJournalTriggers(): TwigFunction { return new TwigFunction( 'allJournalTriggers', - static fn () => [ - 'store-journal' => (string) trans('firefly.rule_trigger_store_journal'), - 'update-journal' => (string) trans('firefly.rule_trigger_update_journal'), - 'manual-activation' => (string) trans('firefly.rule_trigger_manual'), + static fn() => [ + 'store-journal' => (string)trans('firefly.rule_trigger_store_journal'), + 'update-journal' => (string)trans('firefly.rule_trigger_update_journal'), + 'manual-activation' => (string)trans('firefly.rule_trigger_manual'), ] ); } @@ -64,7 +73,7 @@ class Rule extends AbstractExtension $possibleTriggers = []; foreach ($ruleTriggers as $key) { if ('user_action' !== $key) { - $possibleTriggers[$key] = (string) trans('firefly.rule_trigger_'.$key.'_choice'); + $possibleTriggers[$key] = (string)trans('firefly.rule_trigger_' . $key . '_choice'); } } unset($ruleTriggers); @@ -75,22 +84,13 @@ class Rule extends AbstractExtension ); } - public function allActionTriggers(): TwigFunction + #[Override] + public function getFunctions(): array { - return new TwigFunction( - 'allRuleActions', - static function () { - // array of valid values for actions - $ruleActions = array_keys(Config::get('firefly.rule-actions')); - $possibleActions = []; - foreach ($ruleActions as $key) { - $possibleActions[$key] = (string) trans('firefly.rule_action_'.$key.'_choice'); - } - unset($ruleActions); - asort($possibleActions); - - return $possibleActions; - } - ); + return [ + $this->allJournalTriggers(), + $this->allRuleTriggers(), + $this->allActionTriggers(), + ]; } } diff --git a/app/Support/Twig/TransactionGroupTwig.php b/app/Support/Twig/TransactionGroupTwig.php index 81cf8db231..e2957ec3a1 100644 --- a/app/Support/Twig/TransactionGroupTwig.php +++ b/app/Support/Twig/TransactionGroupTwig.php @@ -31,10 +31,9 @@ use FireflyIII\Models\Transaction; use FireflyIII\Models\TransactionJournal; use FireflyIII\Models\TransactionJournalMeta; use Illuminate\Support\Facades\DB; +use Override; use Twig\Extension\AbstractExtension; use Twig\TwigFunction; -use Override; - use function Safe\json_decode; /** @@ -76,6 +75,141 @@ class TransactionGroupTwig extends AbstractExtension ); } + public function journalGetMetaDate(): TwigFunction + { + return new TwigFunction( + 'journalGetMetaDate', + static function (int $journalId, string $metaField) { + /** @var null|TransactionJournalMeta $entry */ + $entry = DB::table('journal_meta') + ->where('name', $metaField) + ->where('transaction_journal_id', $journalId) + ->whereNull('deleted_at') + ->first(); + if (null === $entry) { + return today(config('app.timezone')); + } + + return new Carbon(json_decode((string)$entry->data, false)); + } + ); + } + + public function journalGetMetaField(): TwigFunction + { + return new TwigFunction( + 'journalGetMetaField', + static function (int $journalId, string $metaField) { + /** @var null|TransactionJournalMeta $entry */ + $entry = DB::table('journal_meta') + ->where('name', $metaField) + ->where('transaction_journal_id', $journalId) + ->whereNull('deleted_at') + ->first(); + if (null === $entry) { + return ''; + } + + return json_decode((string)$entry->data, true); + } + ); + } + + public function journalHasMeta(): TwigFunction + { + return new TwigFunction( + 'journalHasMeta', + static function (int $journalId, string $metaField) { + $count = DB::table('journal_meta') + ->where('name', $metaField) + ->where('transaction_journal_id', $journalId) + ->whereNull('deleted_at') + ->count(); + + return 1 === $count; + } + ); + } + + /** + * Shows the amount for a single journal object. + */ + public function journalObjectAmount(): TwigFunction + { + return new TwigFunction( + 'journalObjectAmount', + function (TransactionJournal $journal): string { + $result = $this->normalJournalObjectAmount($journal); + // now append foreign amount, if any. + if ($this->journalObjectHasForeign($journal)) { + $foreign = $this->foreignJournalObjectAmount($journal); + $result = sprintf('%s (%s)', $result, $foreign); + } + + return $result; + }, + ['is_safe' => ['html']] + ); + } + + /** + * Generate foreign amount for transaction from a transaction group. + */ + private function foreignJournalArrayAmount(array $array): string + { + $type = $array['transaction_type_type'] ?? TransactionTypeEnum::WITHDRAWAL->value; + $amount = $array['foreign_amount'] ?? '0'; + $colored = true; + + $sourceType = $array['source_account_type'] ?? 'invalid'; + $amount = $this->signAmount($amount, $type, $sourceType); + + if (TransactionTypeEnum::TRANSFER->value === $type) { + $colored = false; + } + $result = app('amount')->formatFlat($array['foreign_currency_symbol'], (int)$array['foreign_currency_decimal_places'], $amount, $colored); + if (TransactionTypeEnum::TRANSFER->value === $type) { + return sprintf('%s', $result); + } + + return $result; + } + + /** + * Generate foreign amount for journal from a transaction group. + */ + private function foreignJournalObjectAmount(TransactionJournal $journal): string + { + $type = $journal->transactionType->type; + + /** @var Transaction $first */ + $first = $journal->transactions()->where('amount', '<', 0)->first(); + $currency = $first->foreignCurrency; + $amount = '' === $first->foreign_amount ? '0' : $first->foreign_amount; + $colored = true; + $sourceType = $first->account->accountType()->first()->type; + + $amount = $this->signAmount($amount, $type, $sourceType); + + if (TransactionTypeEnum::TRANSFER->value === $type) { + $colored = false; + } + $result = app('amount')->formatFlat($currency->symbol, $currency->decimal_places, $amount, $colored); + if (TransactionTypeEnum::TRANSFER->value === $type) { + return sprintf('%s', $result); + } + + return $result; + } + + private function journalObjectHasForeign(TransactionJournal $journal): bool + { + /** @var Transaction $first */ + $first = $journal->transactions()->where('amount', '<', 0)->first(); + + return '' !== $first->foreign_amount; + } + /** * Generate normal amount for transaction from a transaction group. */ @@ -91,7 +225,34 @@ class TransactionGroupTwig extends AbstractExtension $colored = false; } - $result = app('amount')->formatFlat($array['currency_symbol'], (int) $array['currency_decimal_places'], $amount, $colored); + $result = app('amount')->formatFlat($array['currency_symbol'], (int)$array['currency_decimal_places'], $amount, $colored); + if (TransactionTypeEnum::TRANSFER->value === $type) { + return sprintf('%s', $result); + } + + return $result; + } + + /** + * Generate normal amount for transaction from a transaction group. + */ + private function normalJournalObjectAmount(TransactionJournal $journal): string + { + $type = $journal->transactionType->type; + + /** @var Transaction $first */ + $first = $journal->transactions()->where('amount', '<', 0)->first(); + $currency = $journal->transactionCurrency; + $amount = $first->amount ?? '0'; + $colored = true; + $sourceType = $first->account->accountType()->first()->type; + + $amount = $this->signAmount($amount, $type, $sourceType); + + if (TransactionTypeEnum::TRANSFER->value === $type) { + $colored = false; + } + $result = app('amount')->formatFlat($currency->symbol, $currency->decimal_places, $amount, $colored); if (TransactionTypeEnum::TRANSFER->value === $type) { return sprintf('%s', $result); } @@ -118,169 +279,4 @@ class TransactionGroupTwig extends AbstractExtension return $amount; } - - /** - * Generate foreign amount for transaction from a transaction group. - */ - private function foreignJournalArrayAmount(array $array): string - { - $type = $array['transaction_type_type'] ?? TransactionTypeEnum::WITHDRAWAL->value; - $amount = $array['foreign_amount'] ?? '0'; - $colored = true; - - $sourceType = $array['source_account_type'] ?? 'invalid'; - $amount = $this->signAmount($amount, $type, $sourceType); - - if (TransactionTypeEnum::TRANSFER->value === $type) { - $colored = false; - } - $result = app('amount')->formatFlat($array['foreign_currency_symbol'], (int) $array['foreign_currency_decimal_places'], $amount, $colored); - if (TransactionTypeEnum::TRANSFER->value === $type) { - return sprintf('%s', $result); - } - - return $result; - } - - /** - * Shows the amount for a single journal object. - */ - public function journalObjectAmount(): TwigFunction - { - return new TwigFunction( - 'journalObjectAmount', - function (TransactionJournal $journal): string { - $result = $this->normalJournalObjectAmount($journal); - // now append foreign amount, if any. - if ($this->journalObjectHasForeign($journal)) { - $foreign = $this->foreignJournalObjectAmount($journal); - $result = sprintf('%s (%s)', $result, $foreign); - } - - return $result; - }, - ['is_safe' => ['html']] - ); - } - - /** - * Generate normal amount for transaction from a transaction group. - */ - private function normalJournalObjectAmount(TransactionJournal $journal): string - { - $type = $journal->transactionType->type; - - /** @var Transaction $first */ - $first = $journal->transactions()->where('amount', '<', 0)->first(); - $currency = $journal->transactionCurrency; - $amount = $first->amount ?? '0'; - $colored = true; - $sourceType = $first->account->accountType()->first()->type; - - $amount = $this->signAmount($amount, $type, $sourceType); - - if (TransactionTypeEnum::TRANSFER->value === $type) { - $colored = false; - } - $result = app('amount')->formatFlat($currency->symbol, $currency->decimal_places, $amount, $colored); - if (TransactionTypeEnum::TRANSFER->value === $type) { - return sprintf('%s', $result); - } - - return $result; - } - - private function journalObjectHasForeign(TransactionJournal $journal): bool - { - /** @var Transaction $first */ - $first = $journal->transactions()->where('amount', '<', 0)->first(); - - return '' !== $first->foreign_amount; - } - - /** - * Generate foreign amount for journal from a transaction group. - */ - private function foreignJournalObjectAmount(TransactionJournal $journal): string - { - $type = $journal->transactionType->type; - - /** @var Transaction $first */ - $first = $journal->transactions()->where('amount', '<', 0)->first(); - $currency = $first->foreignCurrency; - $amount = '' === $first->foreign_amount ? '0' : $first->foreign_amount; - $colored = true; - $sourceType = $first->account->accountType()->first()->type; - - $amount = $this->signAmount($amount, $type, $sourceType); - - if (TransactionTypeEnum::TRANSFER->value === $type) { - $colored = false; - } - $result = app('amount')->formatFlat($currency->symbol, $currency->decimal_places, $amount, $colored); - if (TransactionTypeEnum::TRANSFER->value === $type) { - return sprintf('%s', $result); - } - - return $result; - } - - public function journalHasMeta(): TwigFunction - { - return new TwigFunction( - 'journalHasMeta', - static function (int $journalId, string $metaField) { - $count = DB::table('journal_meta') - ->where('name', $metaField) - ->where('transaction_journal_id', $journalId) - ->whereNull('deleted_at') - ->count() - ; - - return 1 === $count; - } - ); - } - - public function journalGetMetaDate(): TwigFunction - { - return new TwigFunction( - 'journalGetMetaDate', - static function (int $journalId, string $metaField) { - /** @var null|TransactionJournalMeta $entry */ - $entry = DB::table('journal_meta') - ->where('name', $metaField) - ->where('transaction_journal_id', $journalId) - ->whereNull('deleted_at') - ->first() - ; - if (null === $entry) { - return today(config('app.timezone')); - } - - return new Carbon(json_decode((string) $entry->data, false)); - } - ); - } - - public function journalGetMetaField(): TwigFunction - { - return new TwigFunction( - 'journalGetMetaField', - static function (int $journalId, string $metaField) { - /** @var null|TransactionJournalMeta $entry */ - $entry = DB::table('journal_meta') - ->where('name', $metaField) - ->where('transaction_journal_id', $journalId) - ->whereNull('deleted_at') - ->first() - ; - if (null === $entry) { - return ''; - } - - return json_decode((string) $entry->data, true); - } - ); - } } diff --git a/app/Support/Twig/Translation.php b/app/Support/Twig/Translation.php index bb19890ff9..e4f429f07e 100644 --- a/app/Support/Twig/Translation.php +++ b/app/Support/Twig/Translation.php @@ -23,10 +23,10 @@ declare(strict_types=1); namespace FireflyIII\Support\Twig; +use Override; use Twig\Extension\AbstractExtension; use Twig\TwigFilter; use Twig\TwigFunction; -use Override; /** * Class Budget. @@ -39,7 +39,7 @@ class Translation extends AbstractExtension return [ new TwigFilter( '_', - static fn ($name) => (string) trans(sprintf('firefly.%s', $name)), + static fn($name) => (string)trans(sprintf('firefly.%s', $name)), ['is_safe' => ['html']] ), ]; From 69dfbda847f9d141bfe4bea391b99ea1728af090 Mon Sep 17 00:00:00 2001 From: James Cole Date: Fri, 26 Sep 2025 06:06:43 +0200 Subject: [PATCH 41/58] Add empty statistic if necessary. --- app/Support/Http/Controllers/PeriodOverview.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/app/Support/Http/Controllers/PeriodOverview.php b/app/Support/Http/Controllers/PeriodOverview.php index 1dda34bf8d..1914b7ce0b 100644 --- a/app/Support/Http/Controllers/PeriodOverview.php +++ b/app/Support/Http/Controllers/PeriodOverview.php @@ -550,6 +550,9 @@ trait PeriodOverview foreach ($array as $entry) { $this->periodStatisticRepo->saveStatistic($account, $entry['currency_id'], $start, $end, $type, $entry['count'], $entry['amount']); } + if(0 === count($array)) { + $this->periodStatisticRepo->saveStatistic($account, $this->primaryCurrency->id, $start, $end, $type, 0, '0'); + } } /** From 18ae950d2ef861a8aef9463bfa59d867729cc0b6 Mon Sep 17 00:00:00 2001 From: James Cole Date: Fri, 26 Sep 2025 06:09:44 +0200 Subject: [PATCH 42/58] Optimize queries. --- app/Support/Steam.php | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/app/Support/Steam.php b/app/Support/Steam.php index c9a13be8b7..32b7c160bd 100644 --- a/app/Support/Steam.php +++ b/app/Support/Steam.php @@ -590,6 +590,11 @@ class Steam */ public function getLocale(): string // get preference { + $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'; @@ -601,9 +606,9 @@ class Steam // 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; } From 8b09cfb8c97ee5d25dfc7f1dc39df51bef1c6b74 Mon Sep 17 00:00:00 2001 From: James Cole Date: Fri, 26 Sep 2025 19:32:53 +0200 Subject: [PATCH 43/58] Optimize query for period statistics. --- .../Account/AccountRepository.php | 1 + .../PeriodStatisticRepository.php | 27 ++++++++------- .../Http/Controllers/PeriodOverview.php | 34 +++++++++++++++---- 3 files changed, 44 insertions(+), 18 deletions(-) diff --git a/app/Repositories/Account/AccountRepository.php b/app/Repositories/Account/AccountRepository.php index c2a14d8d81..caf928b771 100644 --- a/app/Repositories/Account/AccountRepository.php +++ b/app/Repositories/Account/AccountRepository.php @@ -546,6 +546,7 @@ 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') diff --git a/app/Repositories/PeriodStatistic/PeriodStatisticRepository.php b/app/Repositories/PeriodStatistic/PeriodStatisticRepository.php index 1762329f12..00da6827ee 100644 --- a/app/Repositories/PeriodStatistic/PeriodStatisticRepository.php +++ b/app/Repositories/PeriodStatistic/PeriodStatisticRepository.php @@ -27,32 +27,31 @@ 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() - ; + ->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() - ; + ->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 = new PeriodStatistic(); $stat->primaryStatable()->associate($model); $stat->transaction_currency_id = $currencyId; $stat->start = $start; @@ -64,11 +63,15 @@ class PeriodStatisticRepository implements PeriodStatisticRepositoryInterface $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(); + return $model->primaryPeriodStatistics()->where('start', '>=', $start)->where('end', '<=', $end)->get(); } } diff --git a/app/Support/Http/Controllers/PeriodOverview.php b/app/Support/Http/Controllers/PeriodOverview.php index 1914b7ce0b..9012e471b9 100644 --- a/app/Support/Http/Controllers/PeriodOverview.php +++ b/app/Support/Http/Controllers/PeriodOverview.php @@ -90,6 +90,9 @@ trait PeriodOverview $range = Navigation::getViewRange(true); [$start, $end] = $end < $start ? [$end, $start] : [$start, $end]; + /** @var array $dates */ + $dates = Navigation::blockPeriods($start, $end, $range); + [$start, $end] = $this->getPeriodFromBlocks($dates, $start, $end); $this->statistics = $this->periodStatisticRepo->allInRangeForModel($account, $start, $end); // TODO needs to be re-arranged: @@ -97,10 +100,8 @@ trait PeriodOverview // loop blocks, an loop the types, and select the missing ones. // create new ones, or use collected. - /** @var array $dates */ - $dates = Navigation::blockPeriods($start, $end, $range); + $entries = []; - $types = ['spent', 'earned', 'transferred_in', 'transferred_away']; Log::debug(sprintf('Count of loops: %d', count($dates))); foreach ($dates as $currentDate) { $entries[] = $this->getSingleAccountPeriod($account, $currentDate['period'], $currentDate['start'], $currentDate['end']); @@ -110,6 +111,25 @@ trait PeriodOverview return $entries; } + private function getPeriodFromBlocks(array $dates, Carbon $start, Carbon $end): array + { + 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 ($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 [$start, $end]; + } + /** * Overview for single category. Has been refactored recently. * @@ -326,7 +346,7 @@ trait PeriodOverview { return $this->statistics->filter( function (PeriodStatistic $statistic) use ($start, $end, $type) { - if( + if ( !$statistic->end->equalTo($end) && $statistic->end->format('Y-m-d H:i:s') === $end->format('Y-m-d H:i:s') ) { @@ -377,9 +397,9 @@ trait PeriodOverview 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); - // TODO save as statistic. $this->saveGroupedAsStatistics($account, $start, $end, $type, $grouped); return $grouped; @@ -547,10 +567,12 @@ trait PeriodOverview 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)) { + if (0 === count($array)) { + Log::debug('Save empty statistic.'); $this->periodStatisticRepo->saveStatistic($account, $this->primaryCurrency->id, $start, $end, $type, 0, '0'); } } From 8f24ac4fcd5cea4054f81479a41cf0369c508e7d Mon Sep 17 00:00:00 2001 From: James Cole Date: Fri, 26 Sep 2025 19:38:26 +0200 Subject: [PATCH 44/58] Remove statistics when creating a new journal. --- .../Events/StoredGroupEventHandler.php | 33 ++++++++++++++----- .../PeriodStatisticRepository.php | 5 +++ .../PeriodStatisticRepositoryInterface.php | 2 ++ 3 files changed, 32 insertions(+), 8 deletions(-) diff --git a/app/Handlers/Events/StoredGroupEventHandler.php b/app/Handlers/Events/StoredGroupEventHandler.php index 367c4b4d09..b5912ad2b9 100644 --- a/app/Handlers/Events/StoredGroupEventHandler.php +++ b/app/Handlers/Events/StoredGroupEventHandler.php @@ -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); } /** @@ -58,14 +62,14 @@ class StoredGroupEventHandler } Log::debug('Now in StoredGroupEventHandler::processRules()'); - $journals = $storedGroupEvent->transactionGroup->transactionJournals; - $array = []; + $journals = $storedGroupEvent->transactionGroup->transactionJournals; + $array = []; /** @var TransactionJournal $journal */ foreach ($journals as $journal) { $array[] = $journal->id; } - $journalIds = implode(',', $array); + $journalIds = implode(',', $array); Log::debug(sprintf('Add local operator for journal(s): %s', $journalIds)); // collect rules: @@ -74,10 +78,10 @@ class StoredGroupEventHandler // add the groups to the rule engine. // it should run the rules in the group and cancel the group if necessary. - $groups = $ruleGroupRepository->getRuleGroupsWithRules('store-journal'); + $groups = $ruleGroupRepository->getRuleGroupsWithRules('store-journal'); // create and fire rule engine. - $newRuleEngine = app(RuleEngineInterface::class); + $newRuleEngine = app(RuleEngineInterface::class); $newRuleEngine->setUser($storedGroupEvent->transactionGroup->user); $newRuleEngine->addOperator(['type' => 'journal_id', 'value' => $journalIds]); $newRuleEngine->setRuleGroups($groups); @@ -86,7 +90,7 @@ class StoredGroupEventHandler private function recalculateCredit(StoredTransactionGroup $event): void { - $group = $event->transactionGroup; + $group = $event->transactionGroup; /** @var CreditRecalculateService $object */ $object = app(CreditRecalculateService::class); @@ -94,20 +98,33 @@ 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) */ private function triggerWebhooks(StoredTransactionGroup $storedGroupEvent): void { Log::debug(__METHOD__); - $group = $storedGroupEvent->transactionGroup; + $group = $storedGroupEvent->transactionGroup; if (false === $storedGroupEvent->fireWebhooks) { Log::info(sprintf('Will not fire webhooks for transaction group #%d', $group->id)); return; } - $user = $group->user; + $user = $group->user; /** @var MessageGeneratorInterface $engine */ $engine = app(MessageGeneratorInterface::class); diff --git a/app/Repositories/PeriodStatistic/PeriodStatisticRepository.php b/app/Repositories/PeriodStatistic/PeriodStatisticRepository.php index 00da6827ee..9036fa1d7f 100644 --- a/app/Repositories/PeriodStatistic/PeriodStatisticRepository.php +++ b/app/Repositories/PeriodStatistic/PeriodStatisticRepository.php @@ -74,4 +74,9 @@ class PeriodStatisticRepository implements PeriodStatisticRepositoryInterface { 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(); + } } diff --git a/app/Repositories/PeriodStatistic/PeriodStatisticRepositoryInterface.php b/app/Repositories/PeriodStatistic/PeriodStatisticRepositoryInterface.php index d26d85d101..6e4f7cf422 100644 --- a/app/Repositories/PeriodStatistic/PeriodStatisticRepositoryInterface.php +++ b/app/Repositories/PeriodStatistic/PeriodStatisticRepositoryInterface.php @@ -37,4 +37,6 @@ interface PeriodStatisticRepositoryInterface 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; } From 853a99852ee8b108305bbbb2099b3704e6f53ca3 Mon Sep 17 00:00:00 2001 From: James Cole Date: Fri, 26 Sep 2025 19:39:18 +0200 Subject: [PATCH 45/58] Also remove them when updating transactions. --- .../Events/UpdatedGroupEventHandler.php | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/app/Handlers/Events/UpdatedGroupEventHandler.php b/app/Handlers/Events/UpdatedGroupEventHandler.php index 973442abb3..eedb842a10 100644 --- a/app/Handlers/Events/UpdatedGroupEventHandler.php +++ b/app/Handlers/Events/UpdatedGroupEventHandler.php @@ -26,11 +26,13 @@ namespace FireflyIII\Handlers\Events; use FireflyIII\Enums\TransactionTypeEnum; use FireflyIII\Enums\WebhookTrigger; use FireflyIII\Events\RequestedSendWebhookMessages; +use FireflyIII\Events\StoredTransactionGroup; use FireflyIII\Events\UpdatedTransactionGroup; use FireflyIII\Generator\Webhook\MessageGeneratorInterface; use FireflyIII\Models\Account; use FireflyIII\Models\Transaction; use FireflyIII\Models\TransactionJournal; +use FireflyIII\Repositories\PeriodStatistic\PeriodStatisticRepositoryInterface; use FireflyIII\Repositories\RuleGroup\RuleGroupRepositoryInterface; use FireflyIII\Services\Internal\Support\CreditRecalculateService; use FireflyIII\Support\Models\AccountBalanceCalculator; @@ -49,10 +51,25 @@ 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); + } } /** From d3c557ca2255ef09ef1fdb3b9d34e08b665f90b9 Mon Sep 17 00:00:00 2001 From: JC5 Date: Fri, 26 Sep 2025 19:43:39 +0200 Subject: [PATCH 46/58] =?UTF-8?q?=F0=9F=A4=96=20Auto=20commit=20for=20rele?= =?UTF-8?q?ase=20'develop'=20on=202025-09-26?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Events/StoredGroupEventHandler.php | 17 +- .../Events/UpdatedGroupEventHandler.php | 2 +- app/Models/PeriodStatistic.php | 6 +- .../Account/AccountRepository.php | 1 + .../PeriodStatisticRepository.php | 34 +- app/Support/Amount.php | 56 +-- .../Authentication/RemoteUserGuard.php | 14 +- app/Support/Balance.php | 21 +- app/Support/Binder/AccountList.php | 22 +- app/Support/Binder/BudgetList.php | 16 +- app/Support/Binder/CategoryList.php | 12 +- app/Support/Binder/Date.php | 20 +- app/Support/Binder/JournalList.php | 4 +- app/Support/Binder/TagList.php | 9 +- app/Support/Binder/TagOrId.php | 2 +- app/Support/Binder/UserGroupAccount.php | 5 +- app/Support/Binder/UserGroupBill.php | 5 +- app/Support/Binder/UserGroupExchangeRate.php | 5 +- app/Support/Binder/UserGroupTransaction.php | 5 +- app/Support/CacheProperties.php | 3 +- app/Support/Calendar/Calculator.php | 2 +- .../Chart/Budget/FrontpageChartGenerator.php | 8 +- .../Category/FrontpageChartGenerator.php | 12 +- .../Category/WholePeriodChartGenerator.php | 32 +- app/Support/Chart/ChartData.php | 2 +- app/Support/ChartColour.php | 2 +- app/Support/Cronjobs/AutoBudgetCronjob.php | 2 +- app/Support/Cronjobs/BillWarningCronjob.php | 2 +- app/Support/Cronjobs/ExchangeRatesCronjob.php | 2 +- app/Support/Cronjobs/RecurringCronjob.php | 2 +- app/Support/Cronjobs/UpdateCheckCronjob.php | 12 +- app/Support/ExpandedForm.php | 28 +- app/Support/Export/ExportDataGenerator.php | 80 ++--- app/Support/FireflyConfig.php | 10 +- app/Support/Form/AccountForm.php | 14 +- app/Support/Form/CurrencyForm.php | 32 +- app/Support/Form/FormSupport.php | 10 +- app/Support/Form/PiggyBankForm.php | 8 +- app/Support/Form/RuleForm.php | 12 +- .../Http/Api/AccountBalanceGrouped.php | 40 +-- .../Http/Api/ExchangeRateConverter.php | 33 +- .../Http/Api/SummaryBalanceGrouped.php | 16 +- .../Http/Api/ValidatesUserGroupTrait.php | 10 +- app/Support/Http/Controllers/AugumentData.php | 42 +-- .../Http/Controllers/ChartGeneration.php | 32 +- app/Support/Http/Controllers/CreateStuff.php | 3 +- .../Http/Controllers/DateCalculation.php | 8 +- .../Http/Controllers/GetConfigurationData.php | 34 +- .../Http/Controllers/ModelInformation.php | 34 +- .../Http/Controllers/PeriodOverview.php | 233 ++++++------- .../Http/Controllers/RenderPartialViews.php | 38 +-- .../Http/Controllers/RequestInformation.php | 15 +- .../Http/Controllers/RuleManagement.php | 6 +- .../Controllers/TransactionCalculation.php | 26 +- .../Http/Controllers/UserNavigation.php | 8 +- .../JsonApi/Enrichments/AccountEnrichment.php | 95 +++--- .../Enrichments/AvailableBudgetEnrichment.php | 2 +- .../JsonApi/Enrichments/BudgetEnrichment.php | 26 +- .../Enrichments/BudgetLimitEnrichment.php | 25 +- .../Enrichments/CategoryEnrichment.php | 15 +- .../Enrichments/EnrichmentInterface.php | 2 +- .../Enrichments/PiggyBankEnrichment.php | 62 ++-- .../Enrichments/PiggyBankEventEnrichment.php | 20 +- .../Enrichments/RecurringEnrichment.php | 106 +++--- .../Enrichments/SubscriptionEnrichment.php | 79 ++--- .../TransactionGroupEnrichment.php | 66 ++-- .../JsonApi/Enrichments/WebhookEnrichment.php | 2 +- .../Models/AccountBalanceCalculator.php | 46 +-- app/Support/Models/BillDateCalculator.php | 16 +- app/Support/Models/ReturnsIntegerIdTrait.php | 2 +- .../Models/ReturnsIntegerUserIdTrait.php | 4 +- app/Support/Navigation.php | 67 ++-- .../RecalculatesAvailableBudgetsTrait.php | 36 +- app/Support/ParseDateString.php | 15 +- app/Support/Preferences.php | 56 +-- .../Report/Budget/BudgetReportGenerator.php | 94 ++--- .../Category/CategoryReportGenerator.php | 22 +- .../Summarizer/TransactionSummarizer.php | 22 +- .../Recurring/CalculateRangeOccurrences.php | 14 +- .../Recurring/CalculateXOccurrences.php | 20 +- .../Recurring/CalculateXOccurrencesSince.php | 22 +- .../Recurring/FiltersWeekends.php | 6 +- .../UserGroup/UserGroupInterface.php | 2 +- .../Repositories/UserGroup/UserGroupTrait.php | 11 +- app/Support/Request/AppendsLocationData.php | 18 +- app/Support/Request/ChecksLogin.php | 6 +- app/Support/Request/ConvertsDataTypes.php | 9 +- app/Support/Request/ValidatesWebhooks.php | 6 +- app/Support/Search/AccountSearch.php | 18 +- app/Support/Search/OperatorQuerySearch.php | 320 +++++++++--------- .../Search/QueryParser/GdbotsQueryParser.php | 7 +- .../Search/QueryParser/QueryParser.php | 6 +- .../Singleton/PreferencesSingleton.php | 2 +- app/Support/Steam.php | 177 +++++----- app/Support/System/OAuthKeys.php | 7 +- app/Support/Twig/AmountFormat.php | 6 +- app/Support/Twig/General.php | 19 +- app/Support/Twig/Rule.php | 6 +- app/Support/Twig/TransactionGroupTwig.php | 50 +-- app/Support/Twig/Translation.php | 2 +- composer.lock | 14 +- config/firefly.php | 4 +- package-lock.json | 6 +- 103 files changed, 1411 insertions(+), 1336 deletions(-) diff --git a/app/Handlers/Events/StoredGroupEventHandler.php b/app/Handlers/Events/StoredGroupEventHandler.php index b5912ad2b9..9b4f75b4b3 100644 --- a/app/Handlers/Events/StoredGroupEventHandler.php +++ b/app/Handlers/Events/StoredGroupEventHandler.php @@ -62,14 +62,14 @@ class StoredGroupEventHandler } Log::debug('Now in StoredGroupEventHandler::processRules()'); - $journals = $storedGroupEvent->transactionGroup->transactionJournals; - $array = []; + $journals = $storedGroupEvent->transactionGroup->transactionJournals; + $array = []; /** @var TransactionJournal $journal */ foreach ($journals as $journal) { $array[] = $journal->id; } - $journalIds = implode(',', $array); + $journalIds = implode(',', $array); Log::debug(sprintf('Add local operator for journal(s): %s', $journalIds)); // collect rules: @@ -78,10 +78,10 @@ class StoredGroupEventHandler // add the groups to the rule engine. // it should run the rules in the group and cancel the group if necessary. - $groups = $ruleGroupRepository->getRuleGroupsWithRules('store-journal'); + $groups = $ruleGroupRepository->getRuleGroupsWithRules('store-journal'); // create and fire rule engine. - $newRuleEngine = app(RuleEngineInterface::class); + $newRuleEngine = app(RuleEngineInterface::class); $newRuleEngine->setUser($storedGroupEvent->transactionGroup->user); $newRuleEngine->addOperator(['type' => 'journal_id', 'value' => $journalIds]); $newRuleEngine->setRuleGroups($groups); @@ -90,7 +90,7 @@ class StoredGroupEventHandler private function recalculateCredit(StoredTransactionGroup $event): void { - $group = $event->transactionGroup; + $group = $event->transactionGroup; /** @var CreditRecalculateService $object */ $object = app(CreditRecalculateService::class); @@ -102,6 +102,7 @@ class StoredGroupEventHandler { /** @var PeriodStatisticRepositoryInterface $repository */ $repository = app(PeriodStatisticRepositoryInterface::class); + /** @var TransactionJournal $journal */ foreach ($event->transactionGroup->transactionJournals as $journal) { $source = $journal->transactions()->where('amount', '<', '0')->first(); @@ -117,14 +118,14 @@ class StoredGroupEventHandler private function triggerWebhooks(StoredTransactionGroup $storedGroupEvent): void { Log::debug(__METHOD__); - $group = $storedGroupEvent->transactionGroup; + $group = $storedGroupEvent->transactionGroup; if (false === $storedGroupEvent->fireWebhooks) { Log::info(sprintf('Will not fire webhooks for transaction group #%d', $group->id)); return; } - $user = $group->user; + $user = $group->user; /** @var MessageGeneratorInterface $engine */ $engine = app(MessageGeneratorInterface::class); diff --git a/app/Handlers/Events/UpdatedGroupEventHandler.php b/app/Handlers/Events/UpdatedGroupEventHandler.php index eedb842a10..ec5ada671d 100644 --- a/app/Handlers/Events/UpdatedGroupEventHandler.php +++ b/app/Handlers/Events/UpdatedGroupEventHandler.php @@ -26,7 +26,6 @@ namespace FireflyIII\Handlers\Events; use FireflyIII\Enums\TransactionTypeEnum; use FireflyIII\Enums\WebhookTrigger; use FireflyIII\Events\RequestedSendWebhookMessages; -use FireflyIII\Events\StoredTransactionGroup; use FireflyIII\Events\UpdatedTransactionGroup; use FireflyIII\Generator\Webhook\MessageGeneratorInterface; use FireflyIII\Models\Account; @@ -63,6 +62,7 @@ class UpdatedGroupEventHandler { /** @var PeriodStatisticRepositoryInterface $repository */ $repository = app(PeriodStatisticRepositoryInterface::class); + /** @var TransactionJournal $journal */ foreach ($event->transactionGroup->transactionJournals as $journal) { $source = $journal->transactions()->where('amount', '<', '0')->first(); diff --git a/app/Models/PeriodStatistic.php b/app/Models/PeriodStatistic.php index 194073bc88..ca166993ac 100644 --- a/app/Models/PeriodStatistic.php +++ b/app/Models/PeriodStatistic.php @@ -22,8 +22,8 @@ class PeriodStatistic extends Model 'created_at' => 'datetime', 'updated_at' => 'datetime', 'deleted_at' => 'datetime', - 'start' => SeparateTimezoneCaster::class, - 'end' => SeparateTimezoneCaster::class, + 'start' => SeparateTimezoneCaster::class, + 'end' => SeparateTimezoneCaster::class, ]; } @@ -54,6 +54,4 @@ class PeriodStatistic extends Model return $this->morphTo(); } - - } diff --git a/app/Repositories/Account/AccountRepository.php b/app/Repositories/Account/AccountRepository.php index caf928b771..8eacb438b4 100644 --- a/app/Repositories/Account/AccountRepository.php +++ b/app/Repositories/Account/AccountRepository.php @@ -547,6 +547,7 @@ class AccountRepository implements AccountRepositoryInterface, UserGroupInterfac 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') diff --git a/app/Repositories/PeriodStatistic/PeriodStatisticRepository.php b/app/Repositories/PeriodStatistic/PeriodStatisticRepository.php index 9036fa1d7f..612773a9e2 100644 --- a/app/Repositories/PeriodStatistic/PeriodStatisticRepository.php +++ b/app/Repositories/PeriodStatistic/PeriodStatisticRepository.php @@ -34,24 +34,26 @@ 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(); + ->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(); + ->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 = new PeriodStatistic(); $stat->primaryStatable()->associate($model); $stat->transaction_currency_id = $currencyId; $stat->start = $start; @@ -63,9 +65,17 @@ class PeriodStatisticRepository implements PeriodStatisticRepositoryInterface $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 - )); + 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; } diff --git a/app/Support/Amount.php b/app/Support/Amount.php index 4b59c3a4d9..98e8aa284c 100644 --- a/app/Support/Amount.php +++ b/app/Support/Amount.php @@ -59,11 +59,11 @@ class Amount // there are five possible positions for the "+" or "-" sign (if it is even used) // pos_a and pos_e could be the ( and ) symbol. - $posA = ''; // before everything - $posB = ''; // before currency symbol - $posC = ''; // after currency symbol - $posD = ''; // before amount - $posE = ''; // after everything + $posA = ''; // before everything + $posB = ''; // before currency symbol + $posC = ''; // after currency symbol + $posD = ''; // before amount + $posE = ''; // after everything // format would be (currency before amount) // AB%sC_D%vE @@ -105,10 +105,10 @@ class Amount } if ($csPrecedes) { - return $posA . $posB . '%s' . $posC . $space . $posD . '%v' . $posE; + return $posA.$posB.'%s'.$posC.$space.$posD.'%v'.$posE; } - return $posA . $posD . '%v' . $space . $posB . '%s' . $posC . $posE; + return $posA.$posD.'%v'.$space.$posB.'%s'.$posC.$posE; } public function convertToPrimary(?User $user = null): bool @@ -125,8 +125,8 @@ class Amount return $pref; } - $key = sprintf('convert_to_primary_%d', $user->id); - $pref = $instance->getPreference($key); + $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); @@ -163,15 +163,15 @@ class Amount */ public function formatFlat(string $symbol, int $decimalPlaces, string $amount, ?bool $coloured = null): string { - $locale = Steam::getLocale(); - $rounded = Steam::bcround($amount, $decimalPlaces); + $locale = Steam::getLocale(); + $rounded = Steam::bcround($amount, $decimalPlaces); $coloured ??= true; - $fmt = new NumberFormatter($locale, NumberFormatter::CURRENCY); + $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 + $result = (string)$fmt->format((float)$rounded); // intentional float if (true === $coloured) { if (1 === bccomp($rounded, '0')) { @@ -218,16 +218,16 @@ class Amount */ public function getAmountFromJournalObject(TransactionJournal $journal): string { - $convertToPrimary = $this->convertToPrimary(); - $currency = $this->getPrimaryCurrency(); - $field = $convertToPrimary && $currency->id !== $journal->transaction_currency_id ? 'pc_amount' : 'amount'; + $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'; + $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. @@ -284,7 +284,7 @@ class Amount public function getPrimaryCurrencyByUserGroup(UserGroup $userGroup): TransactionCurrency { - $cache = new CacheProperties(); + $cache = new CacheProperties(); $cache->addProperty('getPrimaryCurrencyByGroup'); $cache->addProperty($userGroup->id); if ($cache->has()) { @@ -314,7 +314,7 @@ class Amount $key = sprintf('transaction_currency_%s', $code); /** @var null|TransactionCurrency $pref */ - $pref = $instance->getPreference($key); + $pref = $instance->getPreference($key); if (null !== $pref) { return $pref; } @@ -336,7 +336,7 @@ class Amount $key = sprintf('transaction_currency_%d', $currencyId); /** @var null|TransactionCurrency $pref */ - $pref = $instance->getPreference($key); + $pref = $instance->getPreference($key); if (null !== $pref) { return $pref; } @@ -364,20 +364,20 @@ class Amount private function getLocaleInfo(): array { // get config from preference, not from translation: - $locale = Steam::getLocale(); - $array = Steam::getLocaleArray($locale); + $locale = Steam::getLocale(); + $array = Steam::getLocaleArray($locale); setlocale(LC_MONETARY, $array); - $info = localeconv(); + $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_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'); + $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); + $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); diff --git a/app/Support/Authentication/RemoteUserGuard.php b/app/Support/Authentication/RemoteUserGuard.php index 353765af94..1c87806e00 100644 --- a/app/Support/Authentication/RemoteUserGuard.php +++ b/app/Support/Authentication/RemoteUserGuard.php @@ -48,7 +48,7 @@ class RemoteUserGuard implements Guard public function __construct(protected UserProvider $provider, Application $app) { /** @var null|Request $request */ - $request = $app->get('request'); + $request = $app->get('request'); Log::debug(sprintf('Created RemoteUserGuard for %s "%s"', $request?->getMethod(), $request?->getRequestUri())); $this->application = $app; $this->user = null; @@ -63,8 +63,8 @@ class RemoteUserGuard implements Guard return; } // Get the user identifier from $_SERVER or apache filtered headers - $header = config('auth.guard_header', 'REMOTE_USER'); - $userID = request()->server($header) ?? null; + $header = config('auth.guard_header', 'REMOTE_USER'); + $userID = request()->server($header) ?? null; if (function_exists('apache_request_headers')) { Log::debug('Use apache_request_headers to find user ID.'); @@ -83,7 +83,7 @@ class RemoteUserGuard implements Guard $retrievedUser = $this->provider->retrieveById($userID); // store email address if present in header and not already set. - $header = config('auth.guard_email'); + $header = config('auth.guard_email'); if (null !== $header) { $emailAddress = (string)(request()->server($header) ?? apache_request_headers()[$header] ?? null); @@ -99,7 +99,7 @@ class RemoteUserGuard implements Guard } Log::debug(sprintf('Result of getting user from provider: %s', $retrievedUser->email)); - $this->user = $retrievedUser; + $this->user = $retrievedUser; } public function check(): bool @@ -126,14 +126,14 @@ class RemoteUserGuard implements Guard /** * @SuppressWarnings("PHPMD.ShortMethodName") */ - public function id(): int | string | null + public function id(): int|string|null { Log::debug(sprintf('Now at %s', __METHOD__)); return $this->user?->id; } - public function setUser(Authenticatable | User | null $user): void // @phpstan-ignore-line + public function setUser(Authenticatable|User|null $user): void // @phpstan-ignore-line { Log::debug(sprintf('Now at %s', __METHOD__)); if ($user instanceof User) { diff --git a/app/Support/Balance.php b/app/Support/Balance.php index f9d684c5ef..0a9a97d0ee 100644 --- a/app/Support/Balance.php +++ b/app/Support/Balance.php @@ -48,18 +48,19 @@ class Balance return $cache->get(); } - $query = Transaction::whereIn('transactions.account_id', $accounts->pluck('id')->toArray()) - ->leftJoin('transaction_journals', 'transactions.transaction_journal_id', '=', 'transaction_journals.id') - ->orderBy('transaction_journals.date', 'desc') - ->orderBy('transaction_journals.order', 'asc') - ->orderBy('transaction_journals.description', 'desc') - ->orderBy('transactions.amount', 'desc') - ->where('transaction_journals.date', '<=', $date); + $query = Transaction::whereIn('transactions.account_id', $accounts->pluck('id')->toArray()) + ->leftJoin('transaction_journals', 'transactions.transaction_journal_id', '=', 'transaction_journals.id') + ->orderBy('transaction_journals.date', 'desc') + ->orderBy('transaction_journals.order', 'asc') + ->orderBy('transaction_journals.description', 'desc') + ->orderBy('transactions.amount', 'desc') + ->where('transaction_journals.date', '<=', $date) + ; - $result = $query->get(['transactions.account_id', 'transactions.transaction_currency_id', 'transactions.balance_after']); + $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])) { diff --git a/app/Support/Binder/AccountList.php b/app/Support/Binder/AccountList.php index 3d6c48728e..314a0c025f 100644 --- a/app/Support/Binder/AccountList.php +++ b/app/Support/Binder/AccountList.php @@ -43,21 +43,23 @@ class AccountList implements BinderInterface if ('allAssetAccounts' === $value) { /** @var Collection $collection */ $collection = auth()->user()->accounts() - ->leftJoin('account_types', 'account_types.id', '=', 'accounts.account_type_id') - ->whereIn('account_types.type', [AccountTypeEnum::ASSET->value, AccountTypeEnum::LOAN->value, AccountTypeEnum::DEBT->value, AccountTypeEnum::MORTGAGE->value]) - ->orderBy('accounts.name', 'ASC') - ->get(['accounts.*']); + ->leftJoin('account_types', 'account_types.id', '=', 'accounts.account_type_id') + ->whereIn('account_types.type', [AccountTypeEnum::ASSET->value, AccountTypeEnum::LOAN->value, AccountTypeEnum::DEBT->value, AccountTypeEnum::MORTGAGE->value]) + ->orderBy('accounts.name', 'ASC') + ->get(['accounts.*']) + ; } if ('allAssetAccounts' !== $value) { - $incoming = array_map('\intval', explode(',', $value)); - $list = array_merge(array_unique($incoming), [0]); + $incoming = array_map('\intval', explode(',', $value)); + $list = array_merge(array_unique($incoming), [0]); /** @var Collection $collection */ $collection = auth()->user()->accounts() - ->leftJoin('account_types', 'account_types.id', '=', 'accounts.account_type_id') - ->whereIn('accounts.id', $list) - ->orderBy('accounts.name', 'ASC') - ->get(['accounts.*']); + ->leftJoin('account_types', 'account_types.id', '=', 'accounts.account_type_id') + ->whereIn('accounts.id', $list) + ->orderBy('accounts.name', 'ASC') + ->get(['accounts.*']) + ; } if ($collection->count() > 0) { diff --git a/app/Support/Binder/BudgetList.php b/app/Support/Binder/BudgetList.php index 917885a7d0..6526ebd38a 100644 --- a/app/Support/Binder/BudgetList.php +++ b/app/Support/Binder/BudgetList.php @@ -41,12 +41,13 @@ class BudgetList implements BinderInterface if (auth()->check()) { if ('allBudgets' === $value) { return auth()->user()->budgets()->where('active', true) - ->orderBy('order', 'ASC') - ->orderBy('name', 'ASC') - ->get(); + ->orderBy('order', 'ASC') + ->orderBy('name', 'ASC') + ->get() + ; } - $list = array_unique(array_map('\intval', explode(',', $value))); + $list = array_unique(array_map('\intval', explode(',', $value))); if (0 === count($list)) { // @phpstan-ignore-line app('log')->warning('Budget list count is zero, return 404.'); @@ -56,9 +57,10 @@ class BudgetList implements BinderInterface /** @var Collection $collection */ $collection = auth()->user()->budgets() - ->where('active', true) - ->whereIn('id', $list) - ->get(); + ->where('active', true) + ->whereIn('id', $list) + ->get() + ; // add empty budget if applicable. if (in_array(0, $list, true)) { diff --git a/app/Support/Binder/CategoryList.php b/app/Support/Binder/CategoryList.php index cde58f228f..1275481fa3 100644 --- a/app/Support/Binder/CategoryList.php +++ b/app/Support/Binder/CategoryList.php @@ -41,19 +41,21 @@ class CategoryList implements BinderInterface if (auth()->check()) { if ('allCategories' === $value) { return auth()->user()->categories() - ->orderBy('name', 'ASC') - ->get(); + ->orderBy('name', 'ASC') + ->get() + ; } - $list = array_unique(array_map('\intval', explode(',', $value))); + $list = array_unique(array_map('\intval', explode(',', $value))); if (0 === count($list)) { // @phpstan-ignore-line throw new NotFoundHttpException(); } /** @var Collection $collection */ $collection = auth()->user()->categories() - ->whereIn('id', $list) - ->get(); + ->whereIn('id', $list) + ->get() + ; // add empty category if applicable. if (in_array(0, $list, true)) { diff --git a/app/Support/Binder/Date.php b/app/Support/Binder/Date.php index 4dcfb314c8..99c0ce4c17 100644 --- a/app/Support/Binder/Date.php +++ b/app/Support/Binder/Date.php @@ -43,16 +43,16 @@ class Date implements BinderInterface /** @var FiscalHelperInterface $fiscalHelper */ $fiscalHelper = app(FiscalHelperInterface::class); - $magicWords = [ - 'currentMonthStart' => today(config('app.timezone'))->startOfMonth(), - 'currentMonthEnd' => today(config('app.timezone'))->endOfMonth(), - 'currentYearStart' => today(config('app.timezone'))->startOfYear(), - 'currentYearEnd' => today(config('app.timezone'))->endOfYear(), + $magicWords = [ + 'currentMonthStart' => today(config('app.timezone'))->startOfMonth(), + 'currentMonthEnd' => today(config('app.timezone'))->endOfMonth(), + 'currentYearStart' => today(config('app.timezone'))->startOfYear(), + 'currentYearEnd' => today(config('app.timezone'))->endOfYear(), - 'previousMonthStart' => today(config('app.timezone'))->startOfMonth()->subDay()->startOfMonth(), - 'previousMonthEnd' => today(config('app.timezone'))->startOfMonth()->subDay()->endOfMonth(), - 'previousYearStart' => today(config('app.timezone'))->startOfYear()->subDay()->startOfYear(), - 'previousYearEnd' => today(config('app.timezone'))->startOfYear()->subDay()->endOfYear(), + 'previousMonthStart' => today(config('app.timezone'))->startOfMonth()->subDay()->startOfMonth(), + 'previousMonthEnd' => today(config('app.timezone'))->startOfMonth()->subDay()->endOfMonth(), + 'previousYearStart' => today(config('app.timezone'))->startOfYear()->subDay()->startOfYear(), + 'previousYearEnd' => today(config('app.timezone'))->startOfYear()->subDay()->endOfYear(), 'currentFiscalYearStart' => $fiscalHelper->startOfFiscalYear(today(config('app.timezone'))), 'currentFiscalYearEnd' => $fiscalHelper->endOfFiscalYear(today(config('app.timezone'))), @@ -68,7 +68,7 @@ class Date implements BinderInterface try { $result = new Carbon($value); - } catch (InvalidDateException | InvalidFormatException $e) { // @phpstan-ignore-line + } catch (InvalidDateException|InvalidFormatException $e) { // @phpstan-ignore-line $message = sprintf('Could not parse date "%s" for user #%d: %s', $value, auth()->user()->id, $e->getMessage()); app('log')->error($message); diff --git a/app/Support/Binder/JournalList.php b/app/Support/Binder/JournalList.php index 217dd565ed..5eadcc587a 100644 --- a/app/Support/Binder/JournalList.php +++ b/app/Support/Binder/JournalList.php @@ -39,7 +39,7 @@ class JournalList implements BinderInterface public static function routeBinder(string $value, Route $route): array { if (auth()->check()) { - $list = self::parseList($value); + $list = self::parseList($value); // get the journals by using the collector. /** @var GroupCollectorInterface $collector */ @@ -47,7 +47,7 @@ class JournalList implements BinderInterface $collector->setTypes([TransactionTypeEnum::WITHDRAWAL->value, TransactionTypeEnum::DEPOSIT->value, TransactionTypeEnum::TRANSFER->value, TransactionTypeEnum::RECONCILIATION->value]); $collector->withCategoryInformation()->withBudgetInformation()->withTagInformation()->withAccountInformation(); $collector->setJournalIds($list); - $result = $collector->getExtractedJournals(); + $result = $collector->getExtractedJournals(); if (0 === count($result)) { throw new NotFoundHttpException(); } diff --git a/app/Support/Binder/TagList.php b/app/Support/Binder/TagList.php index 685087da75..d87c8c69b9 100644 --- a/app/Support/Binder/TagList.php +++ b/app/Support/Binder/TagList.php @@ -43,10 +43,11 @@ class TagList implements BinderInterface if (auth()->check()) { if ('allTags' === $value) { return auth()->user()->tags() - ->orderBy('tag', 'ASC') - ->get(); + ->orderBy('tag', 'ASC') + ->get() + ; } - $list = array_unique(array_map('\strtolower', explode(',', $value))); + $list = array_unique(array_map('\strtolower', explode(',', $value))); app('log')->debug('List of tags is', $list); if (0 === count($list)) { // @phpstan-ignore-line @@ -58,7 +59,7 @@ class TagList implements BinderInterface /** @var TagRepositoryInterface $repository */ $repository = app(TagRepositoryInterface::class); $repository->setUser(auth()->user()); - $allTags = $repository->get(); + $allTags = $repository->get(); $collection = $allTags->filter( static function (Tag $tag) use ($list) { diff --git a/app/Support/Binder/TagOrId.php b/app/Support/Binder/TagOrId.php index ad3a866e1a..e742fb674d 100644 --- a/app/Support/Binder/TagOrId.php +++ b/app/Support/Binder/TagOrId.php @@ -40,7 +40,7 @@ class TagOrId implements BinderInterface $repository = app(TagRepositoryInterface::class); $repository->setUser(auth()->user()); - $result = $repository->findByTag($value); + $result = $repository->findByTag($value); if (null === $result) { $result = $repository->find((int)$value); } diff --git a/app/Support/Binder/UserGroupAccount.php b/app/Support/Binder/UserGroupAccount.php index 47a7af5541..c395655e87 100644 --- a/app/Support/Binder/UserGroupAccount.php +++ b/app/Support/Binder/UserGroupAccount.php @@ -42,8 +42,9 @@ class UserGroupAccount implements BinderInterface /** @var User $user */ $user = auth()->user(); $account = Account::where('id', (int)$value) - ->where('user_group_id', $user->user_group_id) - ->first(); + ->where('user_group_id', $user->user_group_id) + ->first() + ; if (null !== $account) { return $account; } diff --git a/app/Support/Binder/UserGroupBill.php b/app/Support/Binder/UserGroupBill.php index 05eff73b6e..bd2489965e 100644 --- a/app/Support/Binder/UserGroupBill.php +++ b/app/Support/Binder/UserGroupBill.php @@ -42,8 +42,9 @@ class UserGroupBill implements BinderInterface /** @var User $user */ $user = auth()->user(); $currency = Bill::where('id', (int)$value) - ->where('user_group_id', $user->user_group_id) - ->first(); + ->where('user_group_id', $user->user_group_id) + ->first() + ; if (null !== $currency) { return $currency; } diff --git a/app/Support/Binder/UserGroupExchangeRate.php b/app/Support/Binder/UserGroupExchangeRate.php index 1bb8fcc374..862564fde1 100644 --- a/app/Support/Binder/UserGroupExchangeRate.php +++ b/app/Support/Binder/UserGroupExchangeRate.php @@ -39,8 +39,9 @@ class UserGroupExchangeRate implements BinderInterface /** @var User $user */ $user = auth()->user(); $rate = CurrencyExchangeRate::where('id', (int)$value) - ->where('user_group_id', $user->user_group_id) - ->first(); + ->where('user_group_id', $user->user_group_id) + ->first() + ; if (null !== $rate) { return $rate; } diff --git a/app/Support/Binder/UserGroupTransaction.php b/app/Support/Binder/UserGroupTransaction.php index 61add59c73..fbbf5c1f43 100644 --- a/app/Support/Binder/UserGroupTransaction.php +++ b/app/Support/Binder/UserGroupTransaction.php @@ -39,8 +39,9 @@ class UserGroupTransaction implements BinderInterface /** @var User $user */ $user = auth()->user(); $group = TransactionGroup::where('id', (int)$value) - ->where('user_group_id', $user->user_group_id) - ->first(); + ->where('user_group_id', $user->user_group_id) + ->first() + ; if (null !== $group) { return $group; } diff --git a/app/Support/CacheProperties.php b/app/Support/CacheProperties.php index 38f2863e92..e22808ea06 100644 --- a/app/Support/CacheProperties.php +++ b/app/Support/CacheProperties.php @@ -27,6 +27,7 @@ use Carbon\Carbon; use Illuminate\Support\Collection; use Illuminate\Support\Facades\Cache; use JsonException; + use function Safe\json_encode; /** @@ -87,7 +88,7 @@ class CacheProperties private function hash(): void { - $content = ''; + $content = ''; foreach ($this->properties as $property) { try { $content = sprintf('%s%s', $content, json_encode($property, JSON_THROW_ON_ERROR)); diff --git a/app/Support/Calendar/Calculator.php b/app/Support/Calendar/Calculator.php index b6ee2ceebb..ae5c1d7f72 100644 --- a/app/Support/Calendar/Calculator.php +++ b/app/Support/Calendar/Calculator.php @@ -33,7 +33,7 @@ use SplObjectStorage; */ class Calculator { - public const int DEFAULT_INTERVAL = 1; + public const int DEFAULT_INTERVAL = 1; private static ?SplObjectStorage $intervalMap = null; // @phpstan-ignore-line private static array $intervals = []; diff --git a/app/Support/Chart/Budget/FrontpageChartGenerator.php b/app/Support/Chart/Budget/FrontpageChartGenerator.php index e48cc65e42..b5af64fedd 100644 --- a/app/Support/Chart/Budget/FrontpageChartGenerator.php +++ b/app/Support/Chart/Budget/FrontpageChartGenerator.php @@ -181,7 +181,7 @@ class FrontpageChartGenerator Log::debug(sprintf('Processing limit #%d with %s %s', $limit->id, $limit->transactionCurrency->code, $limit->amount)); } - $spent = $this->opsRepository->sumExpenses($limit->start_date, $limit->end_date, null, new Collection()->push($budget), $currency); + $spent = $this->opsRepository->sumExpenses($limit->start_date, $limit->end_date, null, new Collection()->push($budget), $currency); Log::debug(sprintf('Spent array has %d entries.', count($spent))); /** @var array $entry */ @@ -208,7 +208,7 @@ class FrontpageChartGenerator */ private function processRow(array $data, Budget $budget, BudgetLimit $limit, array $entry): array { - $title = sprintf('%s (%s)', $budget->name, $entry['currency_name']); + $title = sprintf('%s (%s)', $budget->name, $entry['currency_name']); Log::debug(sprintf('Title is "%s"', $title)); if ($limit->start_date->startOfDay()->ne($this->start->startOfDay()) || $limit->end_date->startOfDay()->ne($this->end->startOfDay())) { $title = sprintf( @@ -219,8 +219,8 @@ class FrontpageChartGenerator $limit->end_date->isoFormat($this->monthAndDayFormat) ); } - $usePrimary = $this->convertToPrimary && $this->default->id !== $limit->transaction_currency_id; - $amount = $limit->amount; + $usePrimary = $this->convertToPrimary && $this->default->id !== $limit->transaction_currency_id; + $amount = $limit->amount; Log::debug(sprintf('Amount is "%s".', $amount)); if ($usePrimary && $limit->transaction_currency_id !== $this->default->id) { $amount = $limit->native_amount; diff --git a/app/Support/Chart/Category/FrontpageChartGenerator.php b/app/Support/Chart/Category/FrontpageChartGenerator.php index b106deafce..cc9b249235 100644 --- a/app/Support/Chart/Category/FrontpageChartGenerator.php +++ b/app/Support/Chart/Category/FrontpageChartGenerator.php @@ -65,16 +65,16 @@ class FrontpageChartGenerator public function generate(): array { Log::debug(sprintf('Now in %s', __METHOD__)); - $categories = $this->repository->getCategories(); - $accounts = $this->accountRepos->getAccountsByType([AccountTypeEnum::DEBT->value, AccountTypeEnum::LOAN->value, AccountTypeEnum::MORTGAGE->value, AccountTypeEnum::ASSET->value, AccountTypeEnum::DEFAULT->value]); - $collection = $this->collectExpensesAll($categories, $accounts); + $categories = $this->repository->getCategories(); + $accounts = $this->accountRepos->getAccountsByType([AccountTypeEnum::DEBT->value, AccountTypeEnum::LOAN->value, AccountTypeEnum::MORTGAGE->value, AccountTypeEnum::ASSET->value, AccountTypeEnum::DEFAULT->value]); + $collection = $this->collectExpensesAll($categories, $accounts); // collect for no-category: - $noCategory = $this->collectNoCatExpenses($accounts); - $collection = array_merge($collection, $noCategory); + $noCategory = $this->collectNoCatExpenses($accounts); + $collection = array_merge($collection, $noCategory); // sort temp array by amount. - $amounts = array_column($collection, 'sum_float'); + $amounts = array_column($collection, 'sum_float'); array_multisort($amounts, SORT_ASC, $collection); $currencyData = $this->createCurrencyGroups($collection); diff --git a/app/Support/Chart/Category/WholePeriodChartGenerator.php b/app/Support/Chart/Category/WholePeriodChartGenerator.php index 044b7f28a2..ce43b1d16e 100644 --- a/app/Support/Chart/Category/WholePeriodChartGenerator.php +++ b/app/Support/Chart/Category/WholePeriodChartGenerator.php @@ -40,22 +40,22 @@ class WholePeriodChartGenerator public function generate(Category $category, Carbon $start, Carbon $end): array { - $collection = new Collection()->push($category); + $collection = new Collection()->push($category); /** @var OperationsRepositoryInterface $opsRepository */ - $opsRepository = app(OperationsRepositoryInterface::class); + $opsRepository = app(OperationsRepositoryInterface::class); /** @var AccountRepositoryInterface $accountRepository */ $accountRepository = app(AccountRepositoryInterface::class); - $types = [AccountTypeEnum::DEFAULT->value, AccountTypeEnum::ASSET->value, AccountTypeEnum::LOAN->value, AccountTypeEnum::DEBT->value, AccountTypeEnum::MORTGAGE->value]; - $accounts = $accountRepository->getAccountsByType($types); - $step = $this->calculateStep($start, $end); - $chartData = []; - $spent = []; - $earned = []; + $types = [AccountTypeEnum::DEFAULT->value, AccountTypeEnum::ASSET->value, AccountTypeEnum::LOAN->value, AccountTypeEnum::DEBT->value, AccountTypeEnum::MORTGAGE->value]; + $accounts = $accountRepository->getAccountsByType($types); + $step = $this->calculateStep($start, $end); + $chartData = []; + $spent = []; + $earned = []; - $current = clone $start; + $current = clone $start; while ($current <= $end) { $key = $current->format('Y-m-d'); @@ -65,14 +65,14 @@ class WholePeriodChartGenerator $current = app('navigation')->addPeriod($current, $step, 0); } - $currencies = $this->extractCurrencies($spent) + $this->extractCurrencies($earned); + $currencies = $this->extractCurrencies($spent) + $this->extractCurrencies($earned); // generate chart data (for each currency) /** @var array $currency */ foreach ($currencies as $currency) { - $code = $currency['currency_code']; - $name = $currency['currency_name']; - $chartData[sprintf('spent-in-%s', $code)] = [ + $code = $currency['currency_code']; + $name = $currency['currency_name']; + $chartData[sprintf('spent-in-%s', $code)] = [ 'label' => (string)trans('firefly.box_spent_in_currency', ['currency' => $name]), 'entries' => [], 'type' => 'bar', @@ -87,11 +87,11 @@ class WholePeriodChartGenerator ]; } - $current = clone $start; + $current = clone $start; while ($current <= $end) { - $key = $current->format('Y-m-d'); - $label = app('navigation')->periodShow($current, $step); + $key = $current->format('Y-m-d'); + $label = app('navigation')->periodShow($current, $step); /** @var array $currency */ foreach ($currencies as $currency) { diff --git a/app/Support/Chart/ChartData.php b/app/Support/Chart/ChartData.php index 8d58c6a304..d35242c4d6 100644 --- a/app/Support/Chart/ChartData.php +++ b/app/Support/Chart/ChartData.php @@ -49,7 +49,7 @@ class ChartData if (array_key_exists('primary_currency_id', $data)) { $data['primary_currency_id'] = (string)$data['primary_currency_id']; } - $required = ['start', 'date', 'end', 'entries']; + $required = ['start', 'date', 'end', 'entries']; foreach ($required as $field) { if (!array_key_exists($field, $data)) { throw new FireflyException(sprintf('Data-set is missing the "%s"-variable.', $field)); diff --git a/app/Support/ChartColour.php b/app/Support/ChartColour.php index f08de5c258..9e938b9946 100644 --- a/app/Support/ChartColour.php +++ b/app/Support/ChartColour.php @@ -55,7 +55,7 @@ class ChartColour public static function getColour(int $index): string { $index %= count(self::$colours); - $row = self::$colours[$index]; + $row = self::$colours[$index]; return sprintf('rgba(%d, %d, %d, 0.7)', $row[0], $row[1], $row[2]); } diff --git a/app/Support/Cronjobs/AutoBudgetCronjob.php b/app/Support/Cronjobs/AutoBudgetCronjob.php index 6855884d66..e4e82d2376 100644 --- a/app/Support/Cronjobs/AutoBudgetCronjob.php +++ b/app/Support/Cronjobs/AutoBudgetCronjob.php @@ -70,7 +70,7 @@ class AutoBudgetCronjob extends AbstractCronjob Log::info(sprintf('Will now fire auto budget cron job task for date "%s".', $this->date->format('Y-m-d'))); /** @var CreateAutoBudgetLimits $job */ - $job = app(CreateAutoBudgetLimits::class, [$this->date]); + $job = app(CreateAutoBudgetLimits::class, [$this->date]); $job->setDate($this->date); $job->handle(); diff --git a/app/Support/Cronjobs/BillWarningCronjob.php b/app/Support/Cronjobs/BillWarningCronjob.php index f192aa1224..a358d5879e 100644 --- a/app/Support/Cronjobs/BillWarningCronjob.php +++ b/app/Support/Cronjobs/BillWarningCronjob.php @@ -82,7 +82,7 @@ class BillWarningCronjob extends AbstractCronjob Log::info(sprintf('Will now fire bill notification job task for date "%s".', $this->date->format('Y-m-d H:i:s'))); /** @var WarnAboutBills $job */ - $job = app(WarnAboutBills::class); + $job = app(WarnAboutBills::class); $job->setDate($this->date); $job->setForce($this->force); $job->handle(); diff --git a/app/Support/Cronjobs/ExchangeRatesCronjob.php b/app/Support/Cronjobs/ExchangeRatesCronjob.php index 57cb788bc7..889d6e57de 100644 --- a/app/Support/Cronjobs/ExchangeRatesCronjob.php +++ b/app/Support/Cronjobs/ExchangeRatesCronjob.php @@ -71,7 +71,7 @@ class ExchangeRatesCronjob extends AbstractCronjob Log::info(sprintf('Will now fire exchange rates cron job task for date "%s".', $this->date->format('Y-m-d'))); /** @var DownloadExchangeRates $job */ - $job = app(DownloadExchangeRates::class); + $job = app(DownloadExchangeRates::class); $job->setDate($this->date); $job->handle(); diff --git a/app/Support/Cronjobs/RecurringCronjob.php b/app/Support/Cronjobs/RecurringCronjob.php index 1f8654b9a7..5f4e11a4c2 100644 --- a/app/Support/Cronjobs/RecurringCronjob.php +++ b/app/Support/Cronjobs/RecurringCronjob.php @@ -80,7 +80,7 @@ class RecurringCronjob extends AbstractCronjob { Log::info(sprintf('Will now fire recurring cron job task for date "%s".', $this->date->format('Y-m-d H:i:s'))); - $job = new CreateRecurringTransactions($this->date); + $job = new CreateRecurringTransactions($this->date); $job->setForce($this->force); $job->handle(); diff --git a/app/Support/Cronjobs/UpdateCheckCronjob.php b/app/Support/Cronjobs/UpdateCheckCronjob.php index c7681037dd..d9987a73ad 100644 --- a/app/Support/Cronjobs/UpdateCheckCronjob.php +++ b/app/Support/Cronjobs/UpdateCheckCronjob.php @@ -41,8 +41,8 @@ class UpdateCheckCronjob extends AbstractCronjob Log::debug('Now in checkForUpdates()'); // should not check for updates: - $permission = FireflyConfig::get('permission_update_check', -1); - $value = (int)$permission->data; + $permission = FireflyConfig::get('permission_update_check', -1); + $value = (int)$permission->data; if (1 !== $value) { Log::debug('Update check is not enabled.'); // get stuff from job: @@ -56,9 +56,9 @@ class UpdateCheckCronjob extends AbstractCronjob // TODO this is duplicate. /** @var Configuration $lastCheckTime */ - $lastCheckTime = FireflyConfig::get('last_update_check', Carbon::now()->getTimestamp()); - $now = Carbon::now()->getTimestamp(); - $diff = $now - $lastCheckTime->data; + $lastCheckTime = FireflyConfig::get('last_update_check', Carbon::now()->getTimestamp()); + $now = Carbon::now()->getTimestamp(); + $diff = $now - $lastCheckTime->data; Log::debug(sprintf('Last check time is %d, current time is %d, difference is %d', $lastCheckTime->data, $now, $diff)); if ($diff < 604800 && false === $this->force) { // get stuff from job: @@ -71,7 +71,7 @@ class UpdateCheckCronjob extends AbstractCronjob } // last check time was more than a week ago. Log::debug('Have not checked for a new version in a week!'); - $release = $this->getLatestRelease(); + $release = $this->getLatestRelease(); if ('error' === $release['level']) { // get stuff from job: $this->jobFired = true; diff --git a/app/Support/ExpandedForm.php b/app/Support/ExpandedForm.php index 1dbaeb7d8e..fc464fae22 100644 --- a/app/Support/ExpandedForm.php +++ b/app/Support/ExpandedForm.php @@ -43,7 +43,7 @@ class ExpandedForm */ public function amountNoCurrency(string $name, $value = null, ?array $options = null): string { - $options ??= []; + $options ??= []; $label = $this->label($name, $options); $options = $this->expandOptionArray($name, $label, $options); $classes = $this->getHolderClasses($name); @@ -74,8 +74,8 @@ class ExpandedForm */ public function checkbox(string $name, ?int $value = null, $checked = null, ?array $options = null): string { - $options ??= []; - $value ??= 1; + $options ??= []; + $value ??= 1; $options['checked'] = true === $checked; if (app('session')->has('preFilled')) { @@ -83,10 +83,10 @@ class ExpandedForm $options['checked'] = $preFilled[$name] ?? $options['checked']; } - $label = $this->label($name, $options); - $options = $this->expandOptionArray($name, $label, $options); - $classes = $this->getHolderClasses($name); - $value = $this->fillFieldValue($name, $value); + $label = $this->label($name, $options); + $options = $this->expandOptionArray($name, $label, $options); + $classes = $this->getHolderClasses($name); + $value = $this->fillFieldValue($name, $value); unset($options['placeholder'], $options['autocomplete'], $options['class']); @@ -157,10 +157,10 @@ class ExpandedForm public function integer(string $name, $value = null, ?array $options = null): string { $options ??= []; - $label = $this->label($name, $options); - $options = $this->expandOptionArray($name, $label, $options); - $classes = $this->getHolderClasses($name); - $value = $this->fillFieldValue($name, $value); + $label = $this->label($name, $options); + $options = $this->expandOptionArray($name, $label, $options); + $classes = $this->getHolderClasses($name); + $value = $this->fillFieldValue($name, $value); $options['step'] ??= '1'; try { @@ -209,9 +209,9 @@ class ExpandedForm /** @var Model $entry */ foreach ($set as $entry) { // All Eloquent models have an ID - $entryId = $entry->id; - $current = $entry->toArray(); - $title = null; + $entryId = $entry->id; + $current = $entry->toArray(); + $title = null; foreach ($fields as $field) { if (array_key_exists($field, $current) && null === $title) { $title = $current[$field]; diff --git a/app/Support/Export/ExportDataGenerator.php b/app/Support/Export/ExportDataGenerator.php index b70f2a6615..926f371108 100644 --- a/app/Support/Export/ExportDataGenerator.php +++ b/app/Support/Export/ExportDataGenerator.php @@ -89,8 +89,8 @@ class ExportDataGenerator public function __construct() { - $this->accounts = new Collection(); - $this->start = today(config('app.timezone')); + $this->accounts = new Collection(); + $this->start = today(config('app.timezone')); $this->start->subYear(); $this->end = today(config('app.timezone')); $this->exportTransactions = false; @@ -234,7 +234,7 @@ class ExportDataGenerator */ private function exportAccounts(): string { - $header = [ + $header = [ 'user_id', 'account_id', 'created_at', @@ -255,7 +255,7 @@ class ExportDataGenerator ]; /** @var AccountRepositoryInterface $repository */ - $repository = app(AccountRepositoryInterface::class); + $repository = app(AccountRepositoryInterface::class); $repository->setUser($this->user); $allAccounts = $repository->getAccountsByType([]); $records = []; @@ -285,7 +285,7 @@ class ExportDataGenerator } // load the CSV document from a string - $csv = Writer::createFromString(); + $csv = Writer::createFromString(); // insert the header try { @@ -318,8 +318,8 @@ class ExportDataGenerator /** @var BillRepositoryInterface $repository */ $repository = app(BillRepositoryInterface::class); $repository->setUser($this->user); - $bills = $repository->getBills(); - $header = [ + $bills = $repository->getBills(); + $header = [ 'user_id', 'bill_id', 'created_at', @@ -333,7 +333,7 @@ class ExportDataGenerator 'skip', 'active', ]; - $records = []; + $records = []; /** @var Bill $bill */ foreach ($bills as $bill) { @@ -354,7 +354,7 @@ class ExportDataGenerator } // load the CSV document from a string - $csv = Writer::createFromString(); + $csv = Writer::createFromString(); // insert the header try { @@ -384,7 +384,7 @@ class ExportDataGenerator */ private function exportBudgets(): string { - $header = [ + $header = [ 'user_id', 'budget_id', 'name', @@ -398,9 +398,9 @@ class ExportDataGenerator $budgetRepos = app(BudgetRepositoryInterface::class); $budgetRepos->setUser($this->user); - $limitRepos = app(BudgetLimitRepositoryInterface::class); - $budgets = $budgetRepos->getBudgets(); - $records = []; + $limitRepos = app(BudgetLimitRepositoryInterface::class); + $budgets = $budgetRepos->getBudgets(); + $records = []; /** @var Budget $budget */ foreach ($budgets as $budget) { @@ -423,7 +423,7 @@ class ExportDataGenerator } // load the CSV document from a string - $csv = Writer::createFromString(); + $csv = Writer::createFromString(); // insert the header try { @@ -453,10 +453,10 @@ class ExportDataGenerator */ private function exportCategories(): string { - $header = ['user_id', 'category_id', 'created_at', 'updated_at', 'name']; + $header = ['user_id', 'category_id', 'created_at', 'updated_at', 'name']; /** @var CategoryRepositoryInterface $catRepos */ - $catRepos = app(CategoryRepositoryInterface::class); + $catRepos = app(CategoryRepositoryInterface::class); $catRepos->setUser($this->user); $records = []; @@ -474,7 +474,7 @@ class ExportDataGenerator } // load the CSV document from a string - $csv = Writer::createFromString(); + $csv = Writer::createFromString(); // insert the header try { @@ -505,14 +505,14 @@ class ExportDataGenerator private function exportPiggies(): string { /** @var PiggyBankRepositoryInterface $piggyRepos */ - $piggyRepos = app(PiggyBankRepositoryInterface::class); + $piggyRepos = app(PiggyBankRepositoryInterface::class); $piggyRepos->setUser($this->user); /** @var AccountRepositoryInterface $accountRepos */ $accountRepos = app(AccountRepositoryInterface::class); $accountRepos->setUser($this->user); - $header = [ + $header = [ 'user_id', 'piggy_bank_id', 'created_at', @@ -528,8 +528,8 @@ class ExportDataGenerator 'order', 'active', ]; - $records = []; - $piggies = $piggyRepos->getPiggyBanks(); + $records = []; + $piggies = $piggyRepos->getPiggyBanks(); /** @var PiggyBank $piggy */ foreach ($piggies as $piggy) { @@ -554,7 +554,7 @@ class ExportDataGenerator } // load the CSV document from a string - $csv = Writer::createFromString(); + $csv = Writer::createFromString(); // insert the header try { @@ -587,7 +587,7 @@ class ExportDataGenerator /** @var RecurringRepositoryInterface $recurringRepos */ $recurringRepos = app(RecurringRepositoryInterface::class); $recurringRepos->setUser($this->user); - $header = [ + $header = [ // recurrence: 'user_id', 'recurrence_id', 'row_contains', 'created_at', 'updated_at', 'type', 'title', 'description', 'first_date', 'repeat_until', 'latest_date', 'repetitions', 'apply_rules', 'active', @@ -596,8 +596,8 @@ class ExportDataGenerator // transactions + meta: 'currency_code', 'foreign_currency_code', 'source_name', 'source_type', 'destination_name', 'destination_type', 'amount', 'foreign_amount', 'category', 'budget', 'piggy_bank', 'tags', ]; - $records = []; - $recurrences = $recurringRepos->get(); + $records = []; + $recurrences = $recurringRepos->get(); /** @var Recurrence $recurrence */ foreach ($recurrences as $recurrence) { @@ -630,7 +630,7 @@ class ExportDataGenerator $piggyBankId = $recurringRepos->getPiggyBank($transaction); $tags = $recurringRepos->getTags($transaction); - $records[] = [ + $records[] = [ // recurrence $this->user->id, $recurrence->id, @@ -646,7 +646,7 @@ class ExportDataGenerator } } // load the CSV document from a string - $csv = Writer::createFromString(); + $csv = Writer::createFromString(); // insert the header try { @@ -683,8 +683,8 @@ class ExportDataGenerator 'action_type', 'action_value', 'action_order', 'action_active', 'action_stop_processing']; $ruleRepos = app(RuleRepositoryInterface::class); $ruleRepos->setUser($this->user); - $rules = $ruleRepos->getAll(); - $records = []; + $rules = $ruleRepos->getAll(); + $records = []; /** @var Rule $rule */ foreach ($rules as $rule) { @@ -723,7 +723,7 @@ class ExportDataGenerator } // load the CSV document from a string - $csv = Writer::createFromString(); + $csv = Writer::createFromString(); // insert the header try { @@ -753,12 +753,12 @@ class ExportDataGenerator */ private function exportTags(): string { - $header = ['user_id', 'tag_id', 'created_at', 'updated_at', 'tag', 'date', 'description', 'latitude', 'longitude', 'zoom_level']; + $header = ['user_id', 'tag_id', 'created_at', 'updated_at', 'tag', 'date', 'description', 'latitude', 'longitude', 'zoom_level']; $tagRepos = app(TagRepositoryInterface::class); $tagRepos->setUser($this->user); - $tags = $tagRepos->get(); - $records = []; + $tags = $tagRepos->get(); + $records = []; /** @var Tag $tag */ foreach ($tags as $tag) { @@ -777,7 +777,7 @@ class ExportDataGenerator } // load the CSV document from a string - $csv = Writer::createFromString(); + $csv = Writer::createFromString(); // insert the header try { @@ -809,26 +809,26 @@ class ExportDataGenerator { Log::debug('Will now export transactions.'); // TODO better place for keys? - $header = ['user_id', 'group_id', 'journal_id', 'created_at', 'updated_at', 'group_title', 'type', 'currency_code', 'amount', 'foreign_currency_code', 'foreign_amount', 'primary_currency_code', 'pc_amount', 'pc_foreign_amount', 'description', 'date', 'source_name', 'source_iban', 'source_type', 'destination_name', 'destination_iban', 'destination_type', 'reconciled', 'category', 'budget', 'bill', 'tags', 'notes']; + $header = ['user_id', 'group_id', 'journal_id', 'created_at', 'updated_at', 'group_title', 'type', 'currency_code', 'amount', 'foreign_currency_code', 'foreign_amount', 'primary_currency_code', 'pc_amount', 'pc_foreign_amount', 'description', 'date', 'source_name', 'source_iban', 'source_type', 'destination_name', 'destination_iban', 'destination_type', 'reconciled', 'category', 'budget', 'bill', 'tags', 'notes']; $metaFields = config('firefly.journal_meta_fields'); $header = array_merge($header, $metaFields); $primary = Amount::getPrimaryCurrency(); - $collector = app(GroupCollectorInterface::class); + $collector = app(GroupCollectorInterface::class); $collector->setUser($this->user); $collector->setRange($this->start, $this->end)->withAccountInformation()->withCategoryInformation()->withBillInformation()->withBudgetInformation()->withTagInformation()->withNotes(); if (0 !== $this->accounts->count()) { $collector->setAccounts($this->accounts); } - $journals = $collector->getExtractedJournals(); + $journals = $collector->getExtractedJournals(); // get repository for meta data: $repository = app(TransactionGroupRepositoryInterface::class); $repository->setUser($this->user); - $records = []; + $records = []; /** @var array $journal */ foreach ($journals as $journal) { @@ -853,7 +853,7 @@ class ExportDataGenerator $pcForeignAmount = null === $journal['pc_foreign_amount'] ? null : Steam::bcround(Steam::negative($journal['pc_foreign_amount']), $primary->decimal_places); } - $records[] = [ + $records[] = [ $journal['user_id'], $journal['transaction_group_id'], $journal['transaction_journal_id'], $journal['created_at']->toAtomString(), $journal['updated_at']->toAtomString(), $journal['transaction_group_title'], $journal['transaction_type_type'], // amounts and currencies $journal['currency_code'], $amount, $journal['foreign_currency_code'], $foreignAmount, $primary->code, $pcAmount, $pcForeignAmount, @@ -878,7 +878,7 @@ class ExportDataGenerator } // load the CSV document from a string - $csv = Writer::createFromString(); + $csv = Writer::createFromString(); // insert the header try { diff --git a/app/Support/FireflyConfig.php b/app/Support/FireflyConfig.php index b79967f8cb..70d4650c6c 100644 --- a/app/Support/FireflyConfig.php +++ b/app/Support/FireflyConfig.php @@ -39,7 +39,7 @@ class FireflyConfig { public function delete(string $name): void { - $fullName = 'ff3-config-' . $name; + $fullName = 'ff3-config-'.$name; if (Cache::has($fullName)) { Cache::forget($fullName); } @@ -53,7 +53,7 @@ class FireflyConfig */ public function get(string $name, mixed $default = null): ?Configuration { - $fullName = 'ff3-config-' . $name; + $fullName = 'ff3-config-'.$name; if (Cache::has($fullName)) { return Cache::get($fullName); } @@ -61,7 +61,7 @@ class FireflyConfig try { /** @var null|Configuration $config */ $config = Configuration::where('name', $name)->first(['id', 'name', 'data']); - } catch (Exception | QueryException $e) { + } catch (Exception|QueryException $e) { throw new FireflyException(sprintf('Could not poll the database: %s', $e->getMessage()), 0, $e); } @@ -146,13 +146,13 @@ class FireflyConfig $item->name = $name; $item->data = $value; $item->save(); - Cache::forget('ff3-config-' . $name); + Cache::forget('ff3-config-'.$name); return $item; } $config->data = $value; $config->save(); - Cache::forget('ff3-config-' . $name); + Cache::forget('ff3-config-'.$name); return $config; } diff --git a/app/Support/Form/AccountForm.php b/app/Support/Form/AccountForm.php index c7a7685061..9b72eb285a 100644 --- a/app/Support/Form/AccountForm.php +++ b/app/Support/Form/AccountForm.php @@ -62,9 +62,9 @@ class AccountForm */ public function activeWithdrawalDestinations(string $name, mixed $value = null, ?array $options = null): string { - $types = [AccountTypeEnum::MORTGAGE->value, AccountTypeEnum::DEBT->value, AccountTypeEnum::CREDITCARD->value, AccountTypeEnum::LOAN->value, AccountTypeEnum::EXPENSE->value]; - $repository = $this->getAccountRepository(); - $grouped = $this->getAccountsGrouped($types, $repository); + $types = [AccountTypeEnum::MORTGAGE->value, AccountTypeEnum::DEBT->value, AccountTypeEnum::CREDITCARD->value, AccountTypeEnum::LOAN->value, AccountTypeEnum::EXPENSE->value]; + $repository = $this->getAccountRepository(); + $grouped = $this->getAccountsGrouped($types, $repository); $cash = $repository->getCashAccount(); $key = (string)trans('firefly.cash_account_type'); @@ -80,15 +80,15 @@ class AccountForm */ public function assetAccountCheckList(string $name, ?array $options = null): string { - $options ??= []; + $options ??= []; $label = $this->label($name, $options); $options = $this->expandOptionArray($name, $label, $options); $classes = $this->getHolderClasses($name); $selected = request()->old($name) ?? []; // get all asset accounts: - $types = [AccountTypeEnum::ASSET->value, AccountTypeEnum::DEFAULT->value, AccountTypeEnum::LOAN->value, AccountTypeEnum::MORTGAGE->value, AccountTypeEnum::DEBT->value]; - $grouped = $this->getAccountsGrouped($types); + $types = [AccountTypeEnum::ASSET->value, AccountTypeEnum::DEFAULT->value, AccountTypeEnum::LOAN->value, AccountTypeEnum::MORTGAGE->value, AccountTypeEnum::DEBT->value]; + $grouped = $this->getAccountsGrouped($types); unset($options['class']); @@ -154,7 +154,7 @@ class AccountForm /** @var Account $account */ foreach ($accountList as $account) { - $role = (string)$repository->getMetaValue($account, 'account_role'); + $role = (string)$repository->getMetaValue($account, 'account_role'); if (in_array($account->accountType->type, $liabilityTypes, true)) { $role = sprintf('l_%s', $account->accountType->type); } diff --git a/app/Support/Form/CurrencyForm.php b/app/Support/Form/CurrencyForm.php index 6d24780f19..bf748c6097 100644 --- a/app/Support/Form/CurrencyForm.php +++ b/app/Support/Form/CurrencyForm.php @@ -72,12 +72,12 @@ class CurrencyForm $currencyRepos = app(CurrencyRepositoryInterface::class); // get all currencies: - $list = $currencyRepos->get(); - $array = []; + $list = $currencyRepos->get(); + $array = []; /** @var TransactionCurrency $currency */ foreach ($list as $currency) { - $array[$currency->id] = $currency->name . ' (' . $currency->symbol . ')'; + $array[$currency->id] = $currency->name.' ('.$currency->symbol.')'; } return $this->select($name, $array, $value, $options); @@ -94,14 +94,14 @@ class CurrencyForm $currencyRepos = app(CurrencyRepositoryInterface::class); // get all currencies: - $list = $currencyRepos->get(); - $array = [ + $list = $currencyRepos->get(); + $array = [ 0 => (string)trans('firefly.no_currency'), ]; /** @var TransactionCurrency $currency */ foreach ($list as $currency) { - $array[$currency->id] = $currency->name . ' (' . $currency->symbol . ')'; + $array[$currency->id] = $currency->name.' ('.$currency->symbol.')'; } return $this->select($name, $array, $value, $options); @@ -124,16 +124,16 @@ class CurrencyForm $primaryCurrency = $options['currency'] ?? app('amount')->getPrimaryCurrency(); /** @var Collection $currencies */ - $currencies = app('amount')->getAllCurrencies(); + $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'); + $preFilled = session('preFilled'); if (!is_array($preFilled)) { $preFilled = []; } - $key = 'amount_currency_id_' . $name; - $sentCurrencyId = array_key_exists($key, $preFilled) ? (int)$preFilled[$key] : $primaryCurrency->id; + $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)); @@ -153,7 +153,7 @@ class CurrencyForm } try { - $html = view('form.' . $view, compact('primaryCurrency', 'currencies', 'classes', 'name', 'label', 'value', 'options'))->render(); + $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.'; @@ -179,15 +179,15 @@ class CurrencyForm $primaryCurrency = $options['currency'] ?? app('amount')->getPrimaryCurrency(); /** @var Collection $currencies */ - $currencies = app('amount')->getCurrencies(); + $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'); + $preFilled = session('preFilled'); if (!is_array($preFilled)) { $preFilled = []; } - $key = 'amount_currency_id_' . $name; - $sentCurrencyId = array_key_exists($key, $preFilled) ? (int)$preFilled[$key] : $primaryCurrency->id; + $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)); @@ -207,7 +207,7 @@ class CurrencyForm } try { - $html = view('form.' . $view, compact('primaryCurrency', 'currencies', 'classes', 'name', 'label', 'value', 'options'))->render(); + $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.'; diff --git a/app/Support/Form/FormSupport.php b/app/Support/Form/FormSupport.php index e41c785f00..22a580c295 100644 --- a/app/Support/Form/FormSupport.php +++ b/app/Support/Form/FormSupport.php @@ -36,7 +36,7 @@ trait FormSupport { public function multiSelect(string $name, ?array $list = null, mixed $selected = null, ?array $options = null): string { - $list ??= []; + $list ??= []; $label = $this->label($name, $options); $options = $this->expandOptionArray($name, $label, $options); $classes = $this->getHolderClasses($name); @@ -59,7 +59,7 @@ trait FormSupport */ public function select(string $name, ?array $list = null, $selected = null, ?array $options = null): string { - $list ??= []; + $list ??= []; $label = $this->label($name, $options); $options = $this->expandOptionArray($name, $label, $options); $classes = $this->getHolderClasses($name); @@ -81,10 +81,10 @@ trait FormSupport */ protected function expandOptionArray(string $name, $label, ?array $options = null): array { - $options ??= []; + $options ??= []; $name = str_replace('[]', '', $name); $options['class'] = 'form-control'; - $options['id'] = 'ffInput_' . $name; + $options['id'] = 'ffInput_'.$name; $options['autocomplete'] = 'off'; $options['placeholder'] = ucfirst((string)$label); @@ -145,6 +145,6 @@ trait FormSupport } $name = str_replace('[]', '', $name); - return (string)trans('form.' . $name); + return (string)trans('form.'.$name); } } diff --git a/app/Support/Form/PiggyBankForm.php b/app/Support/Form/PiggyBankForm.php index b6233fd0d6..0818d96157 100644 --- a/app/Support/Form/PiggyBankForm.php +++ b/app/Support/Form/PiggyBankForm.php @@ -62,14 +62,14 @@ class PiggyBankForm /** @var PiggyBank $piggy */ foreach ($piggyBanks as $piggy) { - $group = $piggy->objectGroups->first(); - $groupTitle = null; - $groupOrder = 0; + $group = $piggy->objectGroups->first(); + $groupTitle = null; + $groupOrder = 0; if (null !== $group) { $groupTitle = $group->title; $groupOrder = $group->order; } - $subList[$groupOrder] ??= [ + $subList[$groupOrder] ??= [ 'group' => [ 'title' => $groupTitle, ], diff --git a/app/Support/Form/RuleForm.php b/app/Support/Form/RuleForm.php index f635ed9b86..9566f0301f 100644 --- a/app/Support/Form/RuleForm.php +++ b/app/Support/Form/RuleForm.php @@ -41,8 +41,8 @@ class RuleForm $groupRepos = app(RuleGroupRepositoryInterface::class); // get all currencies: - $list = $groupRepos->get(); - $array = []; + $list = $groupRepos->get(); + $array = []; /** @var RuleGroup $group */ foreach ($list as $group) { @@ -57,15 +57,15 @@ class RuleForm */ public function ruleGroupListWithEmpty(string $name, $value = null, ?array $options = null): string { - $options ??= []; + $options ??= []; $options['class'] = 'form-control'; /** @var RuleGroupRepositoryInterface $groupRepos */ - $groupRepos = app(RuleGroupRepositoryInterface::class); + $groupRepos = app(RuleGroupRepositoryInterface::class); // get all currencies: - $list = $groupRepos->get(); - $array = [ + $list = $groupRepos->get(); + $array = [ 0 => (string)trans('firefly.none_in_select_list'), ]; diff --git a/app/Support/Http/Api/AccountBalanceGrouped.php b/app/Support/Http/Api/AccountBalanceGrouped.php index 4c60e00870..e335da23eb 100644 --- a/app/Support/Http/Api/AccountBalanceGrouped.php +++ b/app/Support/Http/Api/AccountBalanceGrouped.php @@ -67,7 +67,7 @@ class AccountBalanceGrouped /** @var array $currency */ foreach ($this->data as $currency) { // income and expense array prepped: - $income = [ + $income = [ 'label' => 'earned', 'currency_id' => (string)$currency['currency_id'], 'currency_symbol' => $currency['currency_symbol'], @@ -86,7 +86,7 @@ class AccountBalanceGrouped 'entries' => [], 'pc_entries' => [], ]; - $expense = [ + $expense = [ 'label' => 'spent', 'currency_id' => (string)$currency['currency_id'], 'currency_symbol' => $currency['currency_symbol'], @@ -108,22 +108,22 @@ class AccountBalanceGrouped // loop all possible periods between $start and $end, and add them to the correct dataset. $currentStart = clone $this->start; while ($currentStart <= $this->end) { - $key = $currentStart->format($this->carbonFormat); - $label = $currentStart->toAtomString(); + $key = $currentStart->format($this->carbonFormat); + $label = $currentStart->toAtomString(); // normal entries - $income['entries'][$label] = Steam::bcround($currency[$key]['earned'] ?? '0', $currency['currency_decimal_places']); - $expense['entries'][$label] = Steam::bcround($currency[$key]['spent'] ?? '0', $currency['currency_decimal_places']); + $income['entries'][$label] = Steam::bcround($currency[$key]['earned'] ?? '0', $currency['currency_decimal_places']); + $expense['entries'][$label] = Steam::bcround($currency[$key]['spent'] ?? '0', $currency['currency_decimal_places']); // converted entries $income['pc_entries'][$label] = Steam::bcround($currency[$key]['pc_earned'] ?? '0', $currency['primary_currency_decimal_places']); $expense['pc_entries'][$label] = Steam::bcround($currency[$key]['pc_spent'] ?? '0', $currency['primary_currency_decimal_places']); // next loop - $currentStart = Navigation::addPeriod($currentStart, $this->preferredRange, 0); + $currentStart = Navigation::addPeriod($currentStart, $this->preferredRange, 0); } - $chartData[] = $income; - $chartData[] = $expense; + $chartData[] = $income; + $chartData[] = $expense; } return $chartData; @@ -193,7 +193,7 @@ class AccountBalanceGrouped private function createDefaultDataEntry(array $journal): void { - $currencyId = (int)$journal['currency_id']; + $currencyId = (int)$journal['currency_id']; $this->data[$currencyId] ??= [ 'currency_id' => (string)$currencyId, 'currency_symbol' => $journal['currency_symbol'], @@ -210,8 +210,8 @@ class AccountBalanceGrouped private function createDefaultPeriodEntry(array $journal): void { - $currencyId = (int)$journal['currency_id']; - $period = $journal['date']->format($this->carbonFormat); + $currencyId = (int)$journal['currency_id']; + $period = $journal['date']->format($this->carbonFormat); $this->data[$currencyId][$period] ??= [ 'period' => $period, 'spent' => '0', @@ -268,9 +268,9 @@ class AccountBalanceGrouped private function processJournal(array $journal): void { // format the date according to the period - $period = $journal['date']->format($this->carbonFormat); - $currencyId = (int)$journal['currency_id']; - $currency = $this->findCurrency($currencyId); + $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); @@ -278,12 +278,12 @@ class AccountBalanceGrouped $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']); + $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); + $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) { @@ -292,7 +292,7 @@ class AccountBalanceGrouped } // add normal entry - $this->data[$currencyId][$period][$key] = bcadd((string)$this->data[$currencyId][$period][$key], $amount); + $this->data[$currencyId][$period][$key] = bcadd((string)$this->data[$currencyId][$period][$key], $amount); // add converted entry $convertedKey = sprintf('pc_%s', $key); diff --git a/app/Support/Http/Api/ExchangeRateConverter.php b/app/Support/Http/Api/ExchangeRateConverter.php index 84d57a4649..d92313907a 100644 --- a/app/Support/Http/Api/ExchangeRateConverter.php +++ b/app/Support/Http/Api/ExchangeRateConverter.php @@ -128,7 +128,7 @@ class ExchangeRateConverter if ($cache->has()) { return (int)$cache->get(); } - $euro = Amount::getTransactionCurrencyByCode('EUR'); + $euro = Amount::getTransactionCurrencyByCode('EUR'); ++$this->queryCount; $cache->store($euro->id); @@ -144,13 +144,13 @@ class ExchangeRateConverter if ($euroId === $currency->id) { return '1'; } - $rate = $this->getFromDB($currency->id, $euroId, $date->format('Y-m-d')); + $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')); + $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)); @@ -175,7 +175,7 @@ class ExchangeRateConverter return '1'; } - $key = sprintf('cer-%d-%d-%s', $from, $to, $date); + $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; @@ -185,7 +185,7 @@ class ExchangeRateConverter return $preparedRate; } - $cache = new CacheProperties(); + $cache = new CacheProperties(); $cache->addProperty($key); if ($cache->has()) { $rate = $cache->get(); @@ -198,14 +198,15 @@ class ExchangeRateConverter } /** @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(); + $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; + $rate = (string)$result?->rate; if ('' === $rate) { app('log')->debug(sprintf('ExchangeRateConverter: Found no rate for #%d->#%d (%s) in the DB.', $from, $to, $date)); @@ -241,8 +242,8 @@ class ExchangeRateConverter */ private function getRate(TransactionCurrency $from, TransactionCurrency $to, Carbon $date): string { - $key = $this->getCacheKey($from, $to, $date); - $res = Cache::get($key, null); + $key = $this->getCacheKey($from, $to, $date); + $res = Cache::get($key, null); // find in cache if (null !== $res) { @@ -252,7 +253,7 @@ class ExchangeRateConverter } // find in database - $rate = $this->getFromDB($from->id, $to->id, $date->format('Y-m-d')); + $rate = $this->getFromDB($from->id, $to->id, $date->format('Y-m-d')); if (null !== $rate) { Cache::forever($key, $rate); Log::debug(sprintf('ExchangeRateConverter: Return DB rate from %s to %s on %s.', $from->code, $to->code, $date->format('Y-m-d'))); @@ -261,7 +262,7 @@ class ExchangeRateConverter } // find reverse in database - $rate = $this->getFromDB($to->id, $from->id, $date->format('Y-m-d')); + $rate = $this->getFromDB($to->id, $from->id, $date->format('Y-m-d')); if (null !== $rate) { $rate = bcdiv('1', $rate); Cache::forever($key, $rate); diff --git a/app/Support/Http/Api/SummaryBalanceGrouped.php b/app/Support/Http/Api/SummaryBalanceGrouped.php index 8f88dba7d5..e4fdb41b68 100644 --- a/app/Support/Http/Api/SummaryBalanceGrouped.php +++ b/app/Support/Http/Api/SummaryBalanceGrouped.php @@ -31,7 +31,7 @@ use Illuminate\Support\Facades\Log; class SummaryBalanceGrouped { - private const string SUM = 'sum'; + private const string SUM = 'sum'; private array $amounts = []; private array $currencies; private readonly CurrencyRepositoryInterface $currencyRepository; @@ -48,9 +48,9 @@ class SummaryBalanceGrouped public function groupData(): array { Log::debug('Now going to group data.'); - $return = []; + $return = []; foreach ($this->keys as $key) { - $title = match ($key) { + $title = match ($key) { 'sum' => 'balance', 'expense' => 'spent', 'income' => 'earned', @@ -109,11 +109,11 @@ class SummaryBalanceGrouped /** @var array $journal */ foreach ($journals as $journal) { // transaction info: - $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); + $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) { // use foreign amount instead $pcAmount = $journal['foreign_amount']; diff --git a/app/Support/Http/Api/ValidatesUserGroupTrait.php b/app/Support/Http/Api/ValidatesUserGroupTrait.php index 0ce08c97ed..3d17a7b42c 100644 --- a/app/Support/Http/Api/ValidatesUserGroupTrait.php +++ b/app/Support/Http/Api/ValidatesUserGroupTrait.php @@ -59,8 +59,8 @@ trait ValidatesUserGroupTrait } /** @var User $user */ - $user = auth()->user(); - $groupId = 0; + $user = auth()->user(); + $groupId = 0; if (!$request->has('user_group_id')) { $groupId = (int)$user->user_group_id; Log::debug(sprintf('validateUserGroup: no user group submitted, use default group #%d.', $groupId)); @@ -71,7 +71,7 @@ trait ValidatesUserGroupTrait } /** @var UserGroupRepositoryInterface $repository */ - $repository = app(UserGroupRepositoryInterface::class); + $repository = app(UserGroupRepositoryInterface::class); $repository->setUser($user); $memberships = $repository->getMembershipsFromGroupId($groupId); @@ -82,14 +82,14 @@ trait ValidatesUserGroupTrait } // need to get the group from the membership: - $group = $repository->getById($groupId); + $group = $repository->getById($groupId); if (null === $group) { Log::debug(sprintf('validateUserGroup: group #%d does not exist.', $groupId)); 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 + $roles = property_exists($this, 'acceptedRoles') ? $this->acceptedRoles : []; // @phpstan-ignore-line if (0 === count($roles)) { Log::debug('validateUserGroup: no roles defined, so no access.'); diff --git a/app/Support/Http/Controllers/AugumentData.php b/app/Support/Http/Controllers/AugumentData.php index aa3883244f..2046b7e5b2 100644 --- a/app/Support/Http/Controllers/AugumentData.php +++ b/app/Support/Http/Controllers/AugumentData.php @@ -56,10 +56,10 @@ trait AugumentData /** @var Account $expenseAccount */ foreach ($accounts as $expenseAccount) { - $collection = new Collection(); + $collection = new Collection(); $collection->push($expenseAccount); - $revenue = $repository->findByName($expenseAccount->name, [AccountTypeEnum::REVENUE->value]); + $revenue = $repository->findByName($expenseAccount->name, [AccountTypeEnum::REVENUE->value]); if (null !== $revenue) { $collection->push($revenue); } @@ -116,7 +116,7 @@ trait AugumentData $return[$accountId] = $grouped[$accountId][0]['name']; } } - $return[0] = '(no name)'; + $return[0] = '(no name)'; return $return; } @@ -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; } @@ -158,7 +158,7 @@ trait AugumentData $return[$categoryId] = $grouped[$categoryId][0]['name']; } } - $return[0] = (string)trans('firefly.no_category'); + $return[0] = (string)trans('firefly.no_category'); return $return; } @@ -171,14 +171,14 @@ trait AugumentData Log::debug('In getLimits'); /** @var OperationsRepositoryInterface $opsRepository */ - $opsRepository = app(OperationsRepositoryInterface::class); + $opsRepository = app(OperationsRepositoryInterface::class); /** @var BudgetLimitRepositoryInterface $blRepository */ - $blRepository = app(BudgetLimitRepositoryInterface::class); + $blRepository = app(BudgetLimitRepositoryInterface::class); $end->endOfMonth(); // properties for cache - $cache = new CacheProperties(); + $cache = new CacheProperties(); $cache->addProperty($start); $cache->addProperty($end); $cache->addProperty($budget->id); @@ -189,25 +189,25 @@ trait AugumentData return $cache->get(); } - $set = $blRepository->getBudgetLimits($budget, $start, $end); + $set = $blRepository->getBudgetLimits($budget, $start, $end); $budgetCollection = new Collection()->push($budget); // merge sets based on a key, in case of convert to primary currency - $limits = new Collection(); + $limits = new Collection(); /** @var BudgetLimit $entry */ foreach ($set as $entry) { Log::debug(sprintf('Now at budget limit #%d', $entry->id)); - $currency = $entry->transactionCurrency; + $currency = $entry->transactionCurrency; if ($this->convertToPrimary) { // the sumExpenses method already handles this. $currency = $this->primaryCurrency; } // clone because these objects change each other. - $currentStart = clone $entry->start_date; - $currentEnd = null === $entry->end_date ? null : clone $entry->end_date; + $currentStart = clone $entry->start_date; + $currentEnd = null === $entry->end_date ? null : clone $entry->end_date; if (null === $currentEnd) { $currentEnd = clone $currentStart; @@ -219,9 +219,9 @@ trait AugumentData $entry->pc_spent = $spent; // normal amount: - $expenses = $opsRepository->sumExpenses($currentStart, $currentEnd, null, $budgetCollection, $entry->transactionCurrency, false); - $spent = $expenses[$entry->transactionCurrency->id]['sum'] ?? '0'; - $entry->spent = $spent; + $expenses = $opsRepository->sumExpenses($currentStart, $currentEnd, null, $budgetCollection, $entry->transactionCurrency, false); + $spent = $expenses[$entry->transactionCurrency->id]['sum'] ?? '0'; + $entry->spent = $spent; $limits->push($entry); } @@ -240,7 +240,7 @@ trait AugumentData /** @var array $journal */ foreach ($array as $journal) { - $name = '(no name)'; + $name = '(no name)'; if (TransactionTypeEnum::WITHDRAWAL->value === $journal['transaction_type_type']) { $name = $journal['destination_account_name']; } @@ -263,16 +263,16 @@ trait AugumentData /** @var GroupCollectorInterface $collector */ $collector = app(GroupCollectorInterface::class); - $total = $assets->merge($opposing); + $total = $assets->merge($opposing); $collector->setRange($start, $end)->setTypes([TransactionTypeEnum::WITHDRAWAL->value])->setAccounts($total); - $journals = $collector->getExtractedJournals(); - $sum = [ + $journals = $collector->getExtractedJournals(); + $sum = [ 'grand_sum' => '0', 'per_currency' => [], ]; // 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'])) { diff --git a/app/Support/Http/Controllers/ChartGeneration.php b/app/Support/Http/Controllers/ChartGeneration.php index 7fe3b3ab96..c117c51171 100644 --- a/app/Support/Http/Controllers/ChartGeneration.php +++ b/app/Support/Http/Controllers/ChartGeneration.php @@ -59,28 +59,28 @@ trait ChartGeneration return $cache->get(); } Log::debug('Regenerate chart.account.account-balance-chart from scratch.'); - $locale = app('steam')->getLocale(); + $locale = app('steam')->getLocale(); /** @var GeneratorInterface $generator */ - $generator = app(GeneratorInterface::class); + $generator = app(GeneratorInterface::class); /** @var AccountRepositoryInterface $accountRepos */ - $accountRepos = app(AccountRepositoryInterface::class); + $accountRepos = app(AccountRepositoryInterface::class); - $primary = app('amount')->getPrimaryCurrency(); - $chartData = []; + $primary = app('amount')->getPrimaryCurrency(); + $chartData = []; Log::debug(sprintf('Start of accountBalanceChart(list, %s, %s)', $start->format('Y-m-d H:i:s'), $end->format('Y-m-d H:i:s'))); /** @var Account $account */ foreach ($accounts as $account) { Log::debug(sprintf('Now at account #%d ("%s)', $account->id, $account->name)); - $currency = $accountRepos->getAccountCurrency($account) ?? $primary; - $usePrimary = $convertToPrimary && $primary->id !== $currency->id; - $field = $convertToPrimary ? 'pc_balance' : 'balance'; - $currency = $usePrimary ? $primary : $currency; + $currency = $accountRepos->getAccountCurrency($account) ?? $primary; + $usePrimary = $convertToPrimary && $primary->id !== $currency->id; + $field = $convertToPrimary ? 'pc_balance' : 'balance'; + $currency = $usePrimary ? $primary : $currency; Log::debug(sprintf('Will use field %s', $field)); - $currentSet = [ + $currentSet = [ 'label' => $account->name, 'currency_symbol' => $currency->symbol, 'entries' => [], @@ -91,16 +91,16 @@ trait ChartGeneration $previous = array_values($range)[0]; 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))); - $balance = $range[$format] ?? $previous; - $previous = $balance; + $format = $currentStart->format('Y-m-d'); + $label = trim($currentStart->isoFormat((string)trans('config.month_and_day_js', [], $locale))); + $balance = $range[$format] ?? $previous; + $previous = $balance; $currentStart->addDay(); $currentSet['entries'][$label] = $balance[$field] ?? '0'; } - $chartData[] = $currentSet; + $chartData[] = $currentSet; } - $data = $generator->multiSet($chartData); + $data = $generator->multiSet($chartData); $cache->store($data); return $data; diff --git a/app/Support/Http/Controllers/CreateStuff.php b/app/Support/Http/Controllers/CreateStuff.php index c54d585328..a69c44ac54 100644 --- a/app/Support/Http/Controllers/CreateStuff.php +++ b/app/Support/Http/Controllers/CreateStuff.php @@ -32,6 +32,7 @@ use FireflyIII\User; use Illuminate\Support\Facades\Log; use Laravel\Passport\Passport; use phpseclib3\Crypt\RSA; + use function Safe\file_put_contents; /** @@ -103,7 +104,7 @@ trait CreateStuff return; } - $key = RSA::createKey(4096); + $key = RSA::createKey(4096); Log::alert('NO OAuth keys were found. They have been created.'); diff --git a/app/Support/Http/Controllers/DateCalculation.php b/app/Support/Http/Controllers/DateCalculation.php index 83baefcf8b..69c30c77d3 100644 --- a/app/Support/Http/Controllers/DateCalculation.php +++ b/app/Support/Http/Controllers/DateCalculation.php @@ -90,19 +90,19 @@ trait DateCalculation protected function getNextPeriods(Carbon $date, string $range): array { // select thing for next 12 periods: - $loop = []; + $loop = []; /** @var Carbon $current */ $current = app('navigation')->startOfPeriod($date, $range); $current = app('navigation')->endOfPeriod($current, $range); $current->addDay(); - $count = 0; + $count = 0; while ($count < 12) { $current = app('navigation')->endOfPeriod($current, $range); $currentStart = app('navigation')->startOfPeriod($current, $range); - $loop[] = [ + $loop[] = [ 'label' => $current->format('Y-m-d'), 'title' => app('navigation')->periodShow($current, $range), 'start' => clone $currentStart, @@ -122,7 +122,7 @@ trait DateCalculation protected function getPreviousPeriods(Carbon $date, string $range): array { // select thing for last 12 periods: - $loop = []; + $loop = []; /** @var Carbon $current */ $current = app('navigation')->startOfPeriod($date, $range); diff --git a/app/Support/Http/Controllers/GetConfigurationData.php b/app/Support/Http/Controllers/GetConfigurationData.php index d1005bfdcb..b0bb16b3f1 100644 --- a/app/Support/Http/Controllers/GetConfigurationData.php +++ b/app/Support/Http/Controllers/GetConfigurationData.php @@ -61,13 +61,13 @@ trait GetConfigurationData $steps = []; if (is_array($elements) && count($elements) > 0) { foreach ($elements as $key => $options) { - $currentStep = $options; + $currentStep = $options; // get the text: - $currentStep['intro'] = (string)trans('intro.' . $route . '_' . $key); + $currentStep['intro'] = (string)trans('intro.'.$route.'_'.$key); // save in array: - $steps[] = $currentStep; + $steps[] = $currentStep; } } app('log')->debug(sprintf('Total basic steps for %s is %d', $routeKey, count($steps))); @@ -82,22 +82,22 @@ trait GetConfigurationData */ protected function getDateRangeConfig(): array // get configuration + get preferences. { - $viewRange = app('navigation')->getViewRange(false); + $viewRange = app('navigation')->getViewRange(false); Log::debug(sprintf('dateRange: the view range is "%s"', $viewRange)); /** @var Carbon $start */ - $start = session('start'); + $start = session('start'); /** @var Carbon $end */ - $end = session('end'); + $end = session('end'); /** @var Carbon $first */ - $first = session('first'); - $title = sprintf('%s - %s', $start->isoFormat($this->monthAndDayFormat), $end->isoFormat($this->monthAndDayFormat)); - $isCustom = true === session('is_custom_range', false); - $today = today(config('app.timezone')); - $ranges = [ + $first = session('first'); + $title = sprintf('%s - %s', $start->isoFormat($this->monthAndDayFormat), $end->isoFormat($this->monthAndDayFormat)); + $isCustom = true === session('is_custom_range', false); + $today = today(config('app.timezone')); + $ranges = [ // first range is the current range: $title => [$start, $end], ]; @@ -127,10 +127,10 @@ trait GetConfigurationData // today: /** @var Carbon $todayStart */ - $todayStart = app('navigation')->startOfPeriod($today, $viewRange); + $todayStart = app('navigation')->startOfPeriod($today, $viewRange); /** @var Carbon $todayEnd */ - $todayEnd = app('navigation')->endOfPeriod($todayStart, $viewRange); + $todayEnd = app('navigation')->endOfPeriod($todayStart, $viewRange); if ($todayStart->ne($start) || $todayEnd->ne($end)) { $ranges[ucfirst((string)trans('firefly.today'))] = [$todayStart, $todayEnd]; @@ -186,16 +186,16 @@ trait GetConfigurationData // user is on page with specific instructions: if ('' !== $specificPage) { $routeKey = str_replace('.', '_', $route); - $elements = config(sprintf('intro.%s', $routeKey . '_' . $specificPage)); + $elements = config(sprintf('intro.%s', $routeKey.'_'.$specificPage)); if (is_array($elements) && count($elements) > 0) { foreach ($elements as $key => $options) { - $currentStep = $options; + $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; + $steps[] = $currentStep; } } } diff --git a/app/Support/Http/Controllers/ModelInformation.php b/app/Support/Http/Controllers/ModelInformation.php index 152c9671ef..093120cf7a 100644 --- a/app/Support/Http/Controllers/ModelInformation.php +++ b/app/Support/Http/Controllers/ModelInformation.php @@ -75,14 +75,14 @@ trait ModelInformation protected function getLiabilityTypes(): array { /** @var AccountRepositoryInterface $repository */ - $repository = app(AccountRepositoryInterface::class); + $repository = app(AccountRepositoryInterface::class); // types of liability: /** @var AccountType $debt */ - $debt = $repository->getAccountTypeByType(AccountTypeEnum::DEBT->value); + $debt = $repository->getAccountTypeByType(AccountTypeEnum::DEBT->value); /** @var AccountType $loan */ - $loan = $repository->getAccountTypeByType(AccountTypeEnum::LOAN->value); + $loan = $repository->getAccountTypeByType(AccountTypeEnum::LOAN->value); /** @var AccountType $mortgage */ $mortgage = $repository->getAccountTypeByType(AccountTypeEnum::MORTGAGE->value); @@ -114,8 +114,8 @@ trait ModelInformation protected function getTriggersForBill(Bill $bill): array // get info and argument { // TODO duplicate code - $operators = config('search.operators'); - $triggers = []; + $operators = config('search.operators'); + $triggers = []; foreach ($operators as $key => $operator) { if ('user_action' !== $key && false === $operator['alias']) { $triggers[$key] = (string)trans(sprintf('firefly.rule_trigger_%s_choice', $key)); @@ -165,8 +165,8 @@ trait ModelInformation private function getTriggersForJournal(TransactionJournal $journal): array { // TODO duplicated code. - $operators = config('search.operators'); - $triggers = []; + $operators = config('search.operators'); + $triggers = []; foreach ($operators as $key => $operator) { if ('user_action' !== $key && false === $operator['alias']) { $triggers[$key] = (string)trans(sprintf('firefly.rule_trigger_%s_choice', $key)); @@ -174,18 +174,18 @@ trait ModelInformation } asort($triggers); - $result = []; - $journalTriggers = []; - $values = []; - $index = 0; + $result = []; + $journalTriggers = []; + $values = []; + $index = 0; // amount, description, category, budget, tags, source, destination, notes, currency type // ,type /** @var null|Transaction $source */ - $source = $journal->transactions()->where('amount', '<', 0)->first(); + $source = $journal->transactions()->where('amount', '<', 0)->first(); /** @var null|Transaction $destination */ - $destination = $journal->transactions()->where('amount', '>', 0)->first(); + $destination = $journal->transactions()->where('amount', '>', 0)->first(); if (null === $destination || null === $source) { return $result; } @@ -220,21 +220,21 @@ trait ModelInformation ++$index; // category (if) - $category = $journal->categories()->first(); + $category = $journal->categories()->first(); if (null !== $category) { $journalTriggers[$index] = 'category_is'; $values[$index] = $category->name; ++$index; } // budget (if) - $budget = $journal->budgets()->first(); + $budget = $journal->budgets()->first(); if (null !== $budget) { $journalTriggers[$index] = 'budget_is'; $values[$index] = $budget->name; ++$index; } // tags (if) - $tags = $journal->tags()->get(); + $tags = $journal->tags()->get(); /** @var Tag $tag */ foreach ($tags as $tag) { @@ -243,7 +243,7 @@ trait ModelInformation ++$index; } // notes (if) - $notes = $journal->notes()->first(); + $notes = $journal->notes()->first(); if (null !== $notes) { $journalTriggers[$index] = 'notes_is'; $values[$index] = $notes->text; diff --git a/app/Support/Http/Controllers/PeriodOverview.php b/app/Support/Http/Controllers/PeriodOverview.php index 9012e471b9..6af3f30b6f 100644 --- a/app/Support/Http/Controllers/PeriodOverview.php +++ b/app/Support/Http/Controllers/PeriodOverview.php @@ -88,12 +88,12 @@ trait PeriodOverview $this->accountRepository = app(AccountRepositoryInterface::class); $this->periodStatisticRepo = app(PeriodStatisticRepositoryInterface::class); $range = Navigation::getViewRange(true); - [$start, $end] = $end < $start ? [$end, $start] : [$start, $end]; + [$start, $end] = $end < $start ? [$end, $start] : [$start, $end]; /** @var array $dates */ - $dates = Navigation::blockPeriods($start, $end, $range); - [$start, $end] = $this->getPeriodFromBlocks($dates, $start, $end); - $this->statistics = $this->periodStatisticRepo->allInRangeForModel($account, $start, $end); + $dates = Navigation::blockPeriods($start, $end, $range); + [$start, $end] = $this->getPeriodFromBlocks($dates, $start, $end); + $this->statistics = $this->periodStatisticRepo->allInRangeForModel($account, $start, $end); // TODO needs to be re-arranged: // get all period stats for entire range. @@ -101,7 +101,7 @@ trait PeriodOverview // create new ones, or use collected. - $entries = []; + $entries = []; Log::debug(sprintf('Count of loops: %d', count($dates))); foreach ($dates as $currentDate) { $entries[] = $this->getSingleAccountPeriod($account, $currentDate['period'], $currentDate['start'], $currentDate['end']); @@ -137,11 +137,11 @@ trait PeriodOverview */ protected function getCategoryPeriodOverview(Category $category, Carbon $start, Carbon $end): array { - $range = Navigation::getViewRange(true); + $range = Navigation::getViewRange(true); [$start, $end] = $end < $start ? [$end, $start] : [$start, $end]; // properties for entries with their amounts. - $cache = new CacheProperties(); + $cache = new CacheProperties(); $cache->addProperty($start); $cache->addProperty($end); $cache->addProperty($range); @@ -153,32 +153,32 @@ trait PeriodOverview } /** @var array $dates */ - $dates = Navigation::blockPeriods($start, $end, $range); - $entries = []; + $dates = Navigation::blockPeriods($start, $end, $range); + $entries = []; // collect all expenses in this period: /** @var GroupCollectorInterface $collector */ - $collector = app(GroupCollectorInterface::class); + $collector = app(GroupCollectorInterface::class); $collector->setCategory($category); $collector->setRange($start, $end); $collector->setTypes([TransactionTypeEnum::DEPOSIT->value]); - $earnedSet = $collector->getExtractedJournals(); + $earnedSet = $collector->getExtractedJournals(); // collect all income in this period: /** @var GroupCollectorInterface $collector */ - $collector = app(GroupCollectorInterface::class); + $collector = app(GroupCollectorInterface::class); $collector->setCategory($category); $collector->setRange($start, $end); $collector->setTypes([TransactionTypeEnum::WITHDRAWAL->value]); - $spentSet = $collector->getExtractedJournals(); + $spentSet = $collector->getExtractedJournals(); // collect all transfers in this period: /** @var GroupCollectorInterface $collector */ - $collector = app(GroupCollectorInterface::class); + $collector = app(GroupCollectorInterface::class); $collector->setCategory($category); $collector->setRange($start, $end); $collector->setTypes([TransactionTypeEnum::TRANSFER->value]); - $transferSet = $collector->getExtractedJournals(); + $transferSet = $collector->getExtractedJournals(); foreach ($dates as $currentDate) { $spent = $this->filterJournalsByDate($spentSet, $currentDate['start'], $currentDate['end']); $earned = $this->filterJournalsByDate($earnedSet, $currentDate['start'], $currentDate['end']); @@ -186,17 +186,17 @@ trait PeriodOverview $title = Navigation::periodShow($currentDate['end'], $currentDate['period']); $entries[] = [ - 'transactions' => 0, - 'title' => $title, - 'route' => route( - 'categories.show', - [$category->id, $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), - ]; + 'transactions' => 0, + 'title' => $title, + 'route' => route( + 'categories.show', + [$category->id, $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), + ]; } $cache->store($entries); @@ -212,11 +212,11 @@ trait PeriodOverview */ protected function getNoBudgetPeriodOverview(Carbon $start, Carbon $end): array { - $range = Navigation::getViewRange(true); + $range = Navigation::getViewRange(true); [$start, $end] = $end < $start ? [$end, $start] : [$start, $end]; - $cache = new CacheProperties(); + $cache = new CacheProperties(); $cache->addProperty($start); $cache->addProperty($end); $cache->addProperty($this->convertToPrimary); @@ -227,28 +227,28 @@ trait PeriodOverview } /** @var array $dates */ - $dates = Navigation::blockPeriods($start, $end, $range); - $entries = []; + $dates = Navigation::blockPeriods($start, $end, $range); + $entries = []; // get all expenses without a budget. /** @var GroupCollectorInterface $collector */ - $collector = app(GroupCollectorInterface::class); + $collector = app(GroupCollectorInterface::class); $collector->setRange($start, $end)->withoutBudget()->withAccountInformation()->setTypes([TransactionTypeEnum::WITHDRAWAL->value]); - $journals = $collector->getExtractedJournals(); + $journals = $collector->getExtractedJournals(); foreach ($dates as $currentDate) { $set = $this->filterJournalsByDate($journals, $currentDate['start'], $currentDate['end']); $title = Navigation::periodShow($currentDate['end'], $currentDate['period']); $entries[] = [ - 'title' => $title, - 'route' => route('budgets.no-budget', [$currentDate['start']->format('Y-m-d'), $currentDate['end']->format('Y-m-d')]), - 'total_transactions' => count($set), - 'spent' => $this->groupByCurrency($set), - 'earned' => [], - 'transferred_away' => [], - 'transferred_in' => [], - ]; + 'title' => $title, + 'route' => route('budgets.no-budget', [$currentDate['start']->format('Y-m-d'), $currentDate['end']->format('Y-m-d')]), + 'total_transactions' => count($set), + 'spent' => $this->groupByCurrency($set), + 'earned' => [], + 'transferred_away' => [], + 'transferred_in' => [], + ]; } $cache->store($entries); @@ -265,38 +265,38 @@ trait PeriodOverview protected function getNoCategoryPeriodOverview(Carbon $theDate): array { 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); + $range = Navigation::getViewRange(true); + $first = $this->journalRepos->firstNull(); + $start = null === $first ? new Carbon() : $first->date; + $end = clone $theDate; + $end = Navigation::endOfPeriod($end, $range); 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); - $entries = []; + $dates = Navigation::blockPeriods($start, $end, $range); + $entries = []; // collect all expenses in this period: /** @var GroupCollectorInterface $collector */ - $collector = app(GroupCollectorInterface::class); + $collector = app(GroupCollectorInterface::class); $collector->withoutCategory(); $collector->setRange($start, $end); $collector->setTypes([TransactionTypeEnum::DEPOSIT->value]); - $earnedSet = $collector->getExtractedJournals(); + $earnedSet = $collector->getExtractedJournals(); // collect all income in this period: /** @var GroupCollectorInterface $collector */ - $collector = app(GroupCollectorInterface::class); + $collector = app(GroupCollectorInterface::class); $collector->withoutCategory(); $collector->setRange($start, $end); $collector->setTypes([TransactionTypeEnum::WITHDRAWAL->value]); - $spentSet = $collector->getExtractedJournals(); + $spentSet = $collector->getExtractedJournals(); // collect all transfers in this period: /** @var GroupCollectorInterface $collector */ - $collector = app(GroupCollectorInterface::class); + $collector = app(GroupCollectorInterface::class); $collector->withoutCategory(); $collector->setRange($start, $end); $collector->setTypes([TransactionTypeEnum::TRANSFER->value]); @@ -310,13 +310,13 @@ trait PeriodOverview $title = Navigation::periodShow($currentDate['end'], $currentDate['period']); $entries[] = [ - 'title' => $title, - 'route' => route('categories.no-category', [$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('categories.no-category', [$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), + ]; } Log::debug('End of loops'); @@ -333,7 +333,7 @@ trait PeriodOverview 'total_transactions' => 0, ]; foreach ($types as $type) { - $set = $this->getSingleAccountPeriodByType($account, $start, $end, $type); + $set = $this->getSingleAccountPeriodByType($account, $start, $end, $type); $return['total_transactions'] += $set['count']; unset($set['count']); $return[$type] = $set; @@ -353,6 +353,7 @@ trait PeriodOverview 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; } @@ -398,21 +399,21 @@ trait PeriodOverview } // 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); + $grouped = $this->groupByCurrency($result); $this->saveGroupedAsStatistics($account, $start, $end, $type, $grouped); return $grouped; } - $grouped = [ + $grouped = [ 'count' => 0, ]; /** @var PeriodStatistic $statistic */ foreach ($statistics as $statistic) { - $id = (int)$statistic->transaction_currency_id; - $currency = Amount::getTransactionCurrencyById($id); - $grouped[$id] = [ + $id = (int)$statistic->transaction_currency_id; + $currency = Amount::getTransactionCurrencyById($id); + $grouped[$id] = [ 'amount' => (string)$statistic->amount, 'count' => (int)$statistic->count, 'currency_id' => $currency->id, @@ -434,11 +435,11 @@ trait PeriodOverview */ protected function getTagPeriodOverview(Tag $tag, Carbon $start, Carbon $end): array // period overview for tags. { - $range = Navigation::getViewRange(true); + $range = Navigation::getViewRange(true); [$start, $end] = $end < $start ? [$end, $start] : [$start, $end]; // properties for cache - $cache = new CacheProperties(); + $cache = new CacheProperties(); $cache->addProperty($start); $cache->addProperty($end); $cache->addProperty('tag-period-entries'); @@ -448,37 +449,37 @@ trait PeriodOverview } /** @var array $dates */ - $dates = Navigation::blockPeriods($start, $end, $range); - $entries = []; + $dates = Navigation::blockPeriods($start, $end, $range); + $entries = []; // collect all expenses in this period: /** @var GroupCollectorInterface $collector */ - $collector = app(GroupCollectorInterface::class); + $collector = app(GroupCollectorInterface::class); $collector->setTag($tag); $collector->setRange($start, $end); $collector->setTypes([TransactionTypeEnum::DEPOSIT->value]); - $earnedSet = $collector->getExtractedJournals(); + $earnedSet = $collector->getExtractedJournals(); // collect all income in this period: /** @var GroupCollectorInterface $collector */ - $collector = app(GroupCollectorInterface::class); + $collector = app(GroupCollectorInterface::class); $collector->setTag($tag); $collector->setRange($start, $end); $collector->setTypes([TransactionTypeEnum::WITHDRAWAL->value]); - $spentSet = $collector->getExtractedJournals(); + $spentSet = $collector->getExtractedJournals(); // collect all transfers in this period: /** @var GroupCollectorInterface $collector */ - $collector = app(GroupCollectorInterface::class); + $collector = app(GroupCollectorInterface::class); $collector->setTag($tag); $collector->setRange($start, $end); $collector->setTypes([TransactionTypeEnum::TRANSFER->value]); - $transferSet = $collector->getExtractedJournals(); + $transferSet = $collector->getExtractedJournals(); // filer all of them: - $earnedSet = $this->filterJournalsByTag($earnedSet, $tag); - $spentSet = $this->filterJournalsByTag($spentSet, $tag); - $transferSet = $this->filterJournalsByTag($transferSet, $tag); + $earnedSet = $this->filterJournalsByTag($earnedSet, $tag); + $spentSet = $this->filterJournalsByTag($spentSet, $tag); + $transferSet = $this->filterJournalsByTag($transferSet, $tag); foreach ($dates as $currentDate) { $spent = $this->filterJournalsByDate($spentSet, $currentDate['start'], $currentDate['end']); @@ -487,17 +488,17 @@ trait PeriodOverview $title = Navigation::periodShow($currentDate['end'], $currentDate['period']); $entries[] = [ - 'transactions' => 0, - 'title' => $title, - 'route' => route( - 'tags.show', - [$tag->id, $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), - ]; + 'transactions' => 0, + 'title' => $title, + 'route' => route( + 'tags.show', + [$tag->id, $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), + ]; } return $entries; @@ -508,12 +509,12 @@ trait PeriodOverview */ protected function getTransactionPeriodOverview(string $transactionType, Carbon $start, Carbon $end): array { - $range = Navigation::getViewRange(true); - $types = config(sprintf('firefly.transactionTypesByType.%s', $transactionType)); + $range = Navigation::getViewRange(true); + $types = config(sprintf('firefly.transactionTypesByType.%s', $transactionType)); [$start, $end] = $end < $start ? [$end, $start] : [$start, $end]; // properties for cache - $cache = new CacheProperties(); + $cache = new CacheProperties(); $cache->addProperty($start); $cache->addProperty($end); $cache->addProperty('transactions-period-entries'); @@ -523,16 +524,16 @@ trait PeriodOverview } /** @var array $dates */ - $dates = Navigation::blockPeriods($start, $end, $range); - $entries = []; - $spent = []; - $earned = []; - $transferred = []; + $dates = Navigation::blockPeriods($start, $end, $range); + $entries = []; + $spent = []; + $earned = []; + $transferred = []; // collect all journals in this period (regardless of type) - $collector = app(GroupCollectorInterface::class); + $collector = app(GroupCollectorInterface::class); $collector->setTypes($types)->setRange($start, $end); - $genericSet = $collector->getExtractedJournals(); - $loops = 0; + $genericSet = $collector->getExtractedJournals(); + $loops = 0; foreach ($dates as $currentDate) { $title = Navigation::periodShow($currentDate['end'], $currentDate['period']); @@ -550,14 +551,14 @@ 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; } @@ -598,7 +599,7 @@ trait PeriodOverview { $return = []; foreach ($set as $entry) { - $found = false; + $found = false; /** @var array $localTag */ foreach ($entry['tags'] as $localTag) { @@ -713,13 +714,13 @@ trait PeriodOverview 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'; + $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'; diff --git a/app/Support/Http/Controllers/RenderPartialViews.php b/app/Support/Http/Controllers/RenderPartialViews.php index 05ff99e38b..f7d5df7cb8 100644 --- a/app/Support/Http/Controllers/RenderPartialViews.php +++ b/app/Support/Http/Controllers/RenderPartialViews.php @@ -52,20 +52,20 @@ trait RenderPartialViews protected function budgetEntry(array $attributes): string // generate view for report. { /** @var PopupReportInterface $popupHelper */ - $popupHelper = app(PopupReportInterface::class); + $popupHelper = app(PopupReportInterface::class); /** @var BudgetRepositoryInterface $budgetRepository */ $budgetRepository = app(BudgetRepositoryInterface::class); $budget = $budgetRepository->find((int)$attributes['budgetId']); - $accountRepos = app(AccountRepositoryInterface::class); - $account = $accountRepos->find((int)$attributes['accountId']); + $accountRepos = app(AccountRepositoryInterface::class); + $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.'); } - $journals = $popupHelper->balanceForBudget($budget, $account, $attributes); + $journals = $popupHelper->balanceForBudget($budget, $account, $attributes); try { $view = view('popup.report.balance-amount', compact('journals', 'budget', 'account'))->render(); @@ -113,14 +113,14 @@ trait RenderPartialViews $budgetRepository = app(BudgetRepositoryInterface::class); /** @var PopupReportInterface $popupHelper */ - $popupHelper = app(PopupReportInterface::class); + $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(); } - $journals = $popupHelper->byBudget($budget, $attributes); + $journals = $popupHelper->byBudget($budget, $attributes); try { $view = view('popup.report.budget-spent-amount', compact('journals', 'budget'))->render(); @@ -142,7 +142,7 @@ trait RenderPartialViews protected function categoryEntry(array $attributes): string // generate view for report. { /** @var PopupReportInterface $popupHelper */ - $popupHelper = app(PopupReportInterface::class); + $popupHelper = app(PopupReportInterface::class); /** @var CategoryRepositoryInterface $categoryRepository */ $categoryRepository = app(CategoryRepositoryInterface::class); @@ -237,15 +237,15 @@ trait RenderPartialViews $accountRepository = app(AccountRepositoryInterface::class); /** @var PopupReportInterface $popupHelper */ - $popupHelper = app(PopupReportInterface::class); + $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.'; } - $journals = $popupHelper->byExpenses($account, $attributes); + $journals = $popupHelper->byExpenses($account, $attributes); try { $view = view('popup.report.expense-entry', compact('journals', 'account'))->render(); @@ -266,8 +266,8 @@ trait RenderPartialViews */ protected function getCurrentActions(Rule $rule): array // get info from object and present. { - $index = 0; - $actions = []; + $index = 0; + $actions = []; // must be repos $currentActions = $rule->ruleActions()->orderBy('order', 'ASC')->get(); @@ -306,8 +306,8 @@ trait RenderPartialViews protected function getCurrentTriggers(Rule $rule): array // get info from object and present. { // TODO duplicated code. - $operators = config('search.operators'); - $triggers = []; + $operators = config('search.operators'); + $triggers = []; foreach ($operators as $key => $operator) { if ('user_action' !== $key && false === $operator['alias']) { $triggers[$key] = (string)trans(sprintf('firefly.rule_trigger_%s_choice', $key)); @@ -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); } @@ -365,14 +365,14 @@ trait RenderPartialViews $accountRepository = app(AccountRepositoryInterface::class); /** @var PopupReportInterface $popupHelper */ - $popupHelper = app(PopupReportInterface::class); - $account = $accountRepository->find((int)$attributes['accountId']); + $popupHelper = app(PopupReportInterface::class); + $account = $accountRepository->find((int)$attributes['accountId']); if (null === $account) { return 'This is an unknown category. Apologies.'; } - $journals = $popupHelper->byIncome($account, $attributes); + $journals = $popupHelper->byIncome($account, $attributes); try { $view = view('popup.report.income-entry', compact('journals', 'account'))->render(); diff --git a/app/Support/Http/Controllers/RequestInformation.php b/app/Support/Http/Controllers/RequestInformation.php index b7600f778a..39a6df3aff 100644 --- a/app/Support/Http/Controllers/RequestInformation.php +++ b/app/Support/Http/Controllers/RequestInformation.php @@ -35,6 +35,7 @@ use Illuminate\Routing\Route; use Illuminate\Support\Facades\Hash; use Illuminate\Support\Facades\Route as RouteFacade; use Illuminate\Support\Facades\Validator; + use function Safe\parse_url; /** @@ -100,13 +101,13 @@ trait RequestInformation $page = $this->getPageName(); $specificPage = $this->getSpecificPageName(); // indicator if user has seen the help for this page ( + special page): - $key = sprintf('shown_demo_%s%s', $page, $specificPage); + $key = sprintf('shown_demo_%s%s', $page, $specificPage); // is there an intro for this route? $intro = config(sprintf('intro.%s', $page)) ?? []; $specialIntro = config(sprintf('intro.%s%s', $page, $specificPage)) ?? []; // some routes have a "what" parameter, which indicates a special page: - $shownDemo = true; + $shownDemo = true; // both must be array and either must be > 0 if (count($intro) > 0 || count($specialIntro) > 0) { $shownDemo = app('preferences')->get($key, false)->data; @@ -127,7 +128,7 @@ trait RequestInformation $start = session('start', today(config('app.timezone'))->startOfMonth()); /** @var Carbon $end */ - $end = session('end', today(config('app.timezone'))->endOfMonth()); + $end = session('end', today(config('app.timezone'))->endOfMonth()); if ($start->greaterThanOrEqualTo($date) && $end->greaterThanOrEqualTo($date)) { return true; } @@ -145,20 +146,20 @@ trait RequestInformation final protected function parseAttributes(array $attributes): array // parse input + return result { $attributes['location'] ??= ''; - $attributes['accounts'] = AccountList::routeBinder($attributes['accounts'] ?? '', new Route('get', '', [])); - $date = Carbon::createFromFormat('Ymd', $attributes['startDate']); + $attributes['accounts'] = AccountList::routeBinder($attributes['accounts'] ?? '', new Route('get', '', [])); + $date = Carbon::createFromFormat('Ymd', $attributes['startDate']); if (!$date instanceof Carbon) { $date = today(config('app.timezone')); } $date->startOfMonth(); $attributes['startDate'] = $date; - $date2 = Carbon::createFromFormat('Ymd', $attributes['endDate']); + $date2 = Carbon::createFromFormat('Ymd', $attributes['endDate']); if (!$date2 instanceof Carbon) { $date2 = today(config('app.timezone')); } $date2->endOfDay(); - $attributes['endDate'] = $date2; + $attributes['endDate'] = $date2; return $attributes; } diff --git a/app/Support/Http/Controllers/RuleManagement.php b/app/Support/Http/Controllers/RuleManagement.php index 9903873484..081d1c44d0 100644 --- a/app/Support/Http/Controllers/RuleManagement.php +++ b/app/Support/Http/Controllers/RuleManagement.php @@ -74,8 +74,8 @@ trait RuleManagement protected function getPreviousTriggers(Request $request): array { // TODO duplicated code. - $operators = config('search.operators'); - $triggers = []; + $operators = config('search.operators'); + $triggers = []; foreach ($operators as $key => $operator) { if ('user_action' !== $key && false === $operator['alias']) { $triggers[$key] = (string)trans(sprintf('firefly.rule_trigger_%s_choice', $key)); @@ -129,7 +129,7 @@ trait RuleManagement } asort($triggers); - $index = 0; + $index = 0; foreach ($submittedOperators as $operator) { $rootOperator = OperatorQuerySearch::getRootOperator($operator['type']); $needsContext = (bool)config(sprintf('search.operators.%s.needs_context', $rootOperator)); diff --git a/app/Support/Http/Controllers/TransactionCalculation.php b/app/Support/Http/Controllers/TransactionCalculation.php index a872b53807..17df264ce8 100644 --- a/app/Support/Http/Controllers/TransactionCalculation.php +++ b/app/Support/Http/Controllers/TransactionCalculation.php @@ -39,14 +39,15 @@ trait TransactionCalculation */ protected function getExpensesForOpposing(Collection $accounts, Collection $opposing, Carbon $start, Carbon $end): array { - $total = $accounts->merge($opposing); + $total = $accounts->merge($opposing); /** @var GroupCollectorInterface $collector */ $collector = app(GroupCollectorInterface::class); $collector->setAccounts($total) - ->setRange($start, $end) - ->withAccountInformation() - ->setTypes([TransactionTypeEnum::WITHDRAWAL->value]); + ->setRange($start, $end) + ->withAccountInformation() + ->setTypes([TransactionTypeEnum::WITHDRAWAL->value]) + ; return $collector->getExtractedJournals(); } @@ -60,7 +61,8 @@ trait TransactionCalculation $collector = app(GroupCollectorInterface::class); $collector->setAccounts($accounts)->setRange($start, $end)->setTypes([TransactionTypeEnum::WITHDRAWAL->value, TransactionTypeEnum::TRANSFER->value]) - ->setTags($tags)->withAccountInformation(); + ->setTags($tags)->withAccountInformation() + ; return $collector->getExtractedJournals(); } @@ -73,7 +75,8 @@ trait TransactionCalculation /** @var GroupCollectorInterface $collector */ $collector = app(GroupCollectorInterface::class); $collector->setAccounts($accounts)->setRange($start, $end)->setTypes([TransactionTypeEnum::WITHDRAWAL->value, TransactionTypeEnum::TRANSFER->value]) - ->setBudgets($budgets)->withAccountInformation(); + ->setBudgets($budgets)->withAccountInformation() + ; return $collector->getExtractedJournals(); } @@ -90,7 +93,8 @@ trait TransactionCalculation ->setRange($start, $end) ->setTypes([TransactionTypeEnum::WITHDRAWAL->value, TransactionTypeEnum::TRANSFER->value]) ->setCategories($categories) - ->withAccountInformation(); + ->withAccountInformation() + ; return $collector->getExtractedJournals(); } @@ -103,7 +107,8 @@ trait TransactionCalculation /** @var GroupCollectorInterface $collector */ $collector = app(GroupCollectorInterface::class); $collector->setAccounts($accounts)->setRange($start, $end)->setTypes([TransactionTypeEnum::DEPOSIT->value, TransactionTypeEnum::TRANSFER->value]) - ->setCategories($categories)->withAccountInformation(); + ->setCategories($categories)->withAccountInformation() + ; return $collector->getExtractedJournals(); } @@ -113,7 +118,7 @@ trait TransactionCalculation */ protected function getIncomeForOpposing(Collection $accounts, Collection $opposing, Carbon $start, Carbon $end): array { - $total = $accounts->merge($opposing); + $total = $accounts->merge($opposing); /** @var GroupCollectorInterface $collector */ $collector = app(GroupCollectorInterface::class); @@ -130,7 +135,8 @@ trait TransactionCalculation /** @var GroupCollectorInterface $collector */ $collector = app(GroupCollectorInterface::class); $collector->setAccounts($accounts)->setRange($start, $end)->setTypes([TransactionTypeEnum::DEPOSIT->value, TransactionTypeEnum::TRANSFER->value]) - ->setTags($tags)->withAccountInformation(); + ->setTags($tags)->withAccountInformation() + ; return $collector->getExtractedJournals(); } diff --git a/app/Support/Http/Controllers/UserNavigation.php b/app/Support/Http/Controllers/UserNavigation.php index fc34201443..bee31f429e 100644 --- a/app/Support/Http/Controllers/UserNavigation.php +++ b/app/Support/Http/Controllers/UserNavigation.php @@ -69,7 +69,7 @@ trait UserNavigation final protected function isEditableGroup(TransactionGroup $group): bool { /** @var null|TransactionJournal $journal */ - $journal = $group->transactionJournals()->first(); + $journal = $group->transactionJournals()->first(); if (null === $journal) { return false; } @@ -96,10 +96,10 @@ trait UserNavigation return redirect(route('index')); } - $journal = $transaction->transactionJournal; + $journal = $transaction->transactionJournal; /** @var null|Transaction $other */ - $other = $journal->transactions()->where('id', '!=', $transaction->id)->first(); + $other = $journal->transactions()->where('id', '!=', $transaction->id)->first(); if (null === $other) { app('log')->error(sprintf('Account #%d has no valid journals. Dont know where it belongs.', $account->id)); session()->flash('error', trans('firefly.cant_find_redirect_account')); @@ -119,7 +119,7 @@ trait UserNavigation final protected function redirectGroupToAccount(TransactionGroup $group) { /** @var null|TransactionJournal $journal */ - $journal = $group->transactionJournals()->first(); + $journal = $group->transactionJournals()->first(); if (null === $journal) { app('log')->error(sprintf('No journals in group #%d', $group->id)); diff --git a/app/Support/JsonApi/Enrichments/AccountEnrichment.php b/app/Support/JsonApi/Enrichments/AccountEnrichment.php index 7dc3cb444c..7aedeb6880 100644 --- a/app/Support/JsonApi/Enrichments/AccountEnrichment.php +++ b/app/Support/JsonApi/Enrichments/AccountEnrichment.php @@ -112,7 +112,7 @@ class AccountEnrichment implements EnrichmentInterface } #[Override] - public function enrichSingle(array | Model $model): Account | array + public function enrichSingle(array|Model $model): Account|array { Log::debug(__METHOD__); $collection = new Collection()->push($model); @@ -168,9 +168,9 @@ class AccountEnrichment implements EnrichmentInterface private function appendCollectedData(): void { $this->collection = $this->collection->map(function (Account $item) { - $id = (int)$item->id; - $item->full_account_type = $this->accountTypes[(int)$item->account_type_id] ?? null; - $meta = [ + $id = (int)$item->id; + $item->full_account_type = $this->accountTypes[(int)$item->account_type_id] ?? null; + $meta = [ 'currency' => null, 'location' => [ 'latitude' => null, @@ -217,30 +217,30 @@ class AccountEnrichment implements EnrichmentInterface // add balances // get currencies: - $currency = $this->primaryCurrency; // assume primary currency + $currency = $this->primaryCurrency; // assume primary currency if (null !== $meta['currency']) { $currency = $meta['currency']; } // get the current balance: - $date = $this->getDate(); + $date = $this->getDate(); // $finalBalance = Steam::finalAccountBalance($item, $date, $this->primaryCurrency, $this->convertToPrimary); - $finalBalance = $this->balances[$id]; - $balanceDifference = $this->getBalanceDifference($id, $currency); + $finalBalance = $this->balances[$id]; + $balanceDifference = $this->getBalanceDifference($id, $currency); Log::debug(sprintf('Call finalAccountBalance(%s) with date/time "%s"', var_export($this->convertToPrimary, true), $date->toIso8601String()), $finalBalance); // collect current balances: - $currentBalance = Steam::bcround($finalBalance[$currency->code] ?? '0', $currency->decimal_places); - $openingBalance = Steam::bcround($meta['opening_balance_amount'] ?? '0', $currency->decimal_places); - $virtualBalance = Steam::bcround($item->virtual_balance ?? '0', $currency->decimal_places); - $debtAmount = $meta['current_debt'] ?? null; + $currentBalance = Steam::bcround($finalBalance[$currency->code] ?? '0', $currency->decimal_places); + $openingBalance = Steam::bcround($meta['opening_balance_amount'] ?? '0', $currency->decimal_places); + $virtualBalance = Steam::bcround($item->virtual_balance ?? '0', $currency->decimal_places); + $debtAmount = $meta['current_debt'] ?? null; // set some pc_ default values to NULL: - $pcCurrentBalance = null; - $pcOpeningBalance = null; - $pcVirtualBalance = null; - $pcDebtAmount = null; - $pcBalanceDifference = null; + $pcCurrentBalance = null; + $pcOpeningBalance = null; + $pcVirtualBalance = null; + $pcDebtAmount = null; + $pcBalanceDifference = null; // convert to primary currency if needed: if ($this->convertToPrimary && $currency->id !== $this->primaryCurrency->id) { @@ -279,7 +279,7 @@ class AccountEnrichment implements EnrichmentInterface 'pc_balance_difference' => $pcBalanceDifference, ]; // end add balances - $item->meta = $meta; + $item->meta = $meta; return $item; }); @@ -313,23 +313,25 @@ class AccountEnrichment implements EnrichmentInterface 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(); + ->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'], - ]; + '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(); + $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) { @@ -355,9 +357,10 @@ class AccountEnrichment implements EnrichmentInterface 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(); + ->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']; } @@ -366,12 +369,13 @@ class AccountEnrichment implements EnrichmentInterface private function collectObjectGroups(): void { - $set = DB::table('object_groupables') - ->whereIn('object_groupable_id', $this->ids) - ->where('object_groupable_type', Account::class) - ->get(['object_groupable_id', 'object_group_id']); + $set = DB::table('object_groupables') + ->whereIn('object_groupable_id', $this->ids) + ->where('object_groupable_type', Account::class) + ->get(['object_groupable_id', 'object_group_id']) + ; - $ids = array_unique($set->pluck('object_group_id')->toArray()); + $ids = array_unique($set->pluck('object_group_id')->toArray()); foreach ($set as $entry) { $this->mappedObjects[(int)$entry->object_groupable_id] = (int)$entry->object_group_id; @@ -395,19 +399,20 @@ class AccountEnrichment implements EnrichmentInterface ->setUserGroup($this->userGroup) ->setAccounts($this->collection) ->withAccountInformation() - ->setTypes([TransactionTypeEnum::OPENING_BALANCE->value]); - $journals = $collector->getExtractedJournals(); + ->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'], - ]; + 'amount' => Steam::negative($journal['amount']), + 'date' => $journal['date'], + ]; $this->openingBalances[(int)$journal['destination_account_id']] = [ - 'amount' => Steam::positive($journal['amount']), - 'date' => $journal['date'], - ]; + 'amount' => Steam::positive($journal['amount']), + 'date' => $journal['date'], + ]; } } @@ -431,8 +436,8 @@ class AccountEnrichment implements EnrichmentInterface if (0 === count($startBalance) || 0 === count($endBalance)) { return null; } - $start = $startBalance[$currency->code] ?? '0'; - $end = $endBalance[$currency->code] ?? '0'; + $start = $startBalance[$currency->code] ?? '0'; + $end = $endBalance[$currency->code] ?? '0'; return bcsub($end, $start); } @@ -453,7 +458,7 @@ class AccountEnrichment implements EnrichmentInterface case 'current_balance': case 'pc_current_balance': - $this->collection = $this->collection->sortBy(static fn(Account $account) => $account->meta['balances'][$parameter[0]] ?? '0', SORT_NUMERIC, 'desc' === $parameter[1]); + $this->collection = $this->collection->sortBy(static fn (Account $account) => $account->meta['balances'][$parameter[0]] ?? '0', SORT_NUMERIC, 'desc' === $parameter[1]); break; } diff --git a/app/Support/JsonApi/Enrichments/AvailableBudgetEnrichment.php b/app/Support/JsonApi/Enrichments/AvailableBudgetEnrichment.php index 85711c7efb..a6a8823368 100644 --- a/app/Support/JsonApi/Enrichments/AvailableBudgetEnrichment.php +++ b/app/Support/JsonApi/Enrichments/AvailableBudgetEnrichment.php @@ -79,7 +79,7 @@ class AvailableBudgetEnrichment implements EnrichmentInterface } #[Override] - public function enrichSingle(array | Model $model): array | Model + public function enrichSingle(array|Model $model): array|Model { Log::debug(__METHOD__); $collection = new Collection()->push($model); diff --git a/app/Support/JsonApi/Enrichments/BudgetEnrichment.php b/app/Support/JsonApi/Enrichments/BudgetEnrichment.php index 71e4ff160b..cb875aca78 100644 --- a/app/Support/JsonApi/Enrichments/BudgetEnrichment.php +++ b/app/Support/JsonApi/Enrichments/BudgetEnrichment.php @@ -70,7 +70,7 @@ class BudgetEnrichment implements EnrichmentInterface return $this->collection; } - public function enrichSingle(array | Model $model): array | Model + public function enrichSingle(array|Model $model): array|Model { Log::debug(__METHOD__); $collection = new Collection()->push($model); @@ -103,8 +103,8 @@ class BudgetEnrichment implements EnrichmentInterface private function appendCollectedData(): void { $this->collection = $this->collection->map(function (Budget $item) { - $id = (int)$item->id; - $meta = [ + $id = (int)$item->id; + $meta = [ 'object_group_id' => null, 'object_group_order' => null, 'object_group_title' => null, @@ -156,7 +156,7 @@ class BudgetEnrichment implements EnrichmentInterface $opsRepository->setUserGroup($this->userGroup); // $spent = $this->beautify(); // $set = $this->opsRepository->sumExpenses($start, $end, null, new Collection()->push($budget)) - $expenses = $opsRepository->collectExpenses($this->start, $this->end, null, $this->collection, null); + $expenses = $opsRepository->collectExpenses($this->start, $this->end, null, $this->collection, null); foreach ($this->collection as $item) { $id = (int)$item->id; $this->spent[$id] = array_values($opsRepository->sumCollectedExpensesByBudget($expenses, $item, false)); @@ -177,9 +177,10 @@ class BudgetEnrichment implements EnrichmentInterface 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(); + ->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']; } @@ -188,12 +189,13 @@ class BudgetEnrichment implements EnrichmentInterface private function collectObjectGroups(): void { - $set = DB::table('object_groupables') - ->whereIn('object_groupable_id', $this->ids) - ->where('object_groupable_type', Budget::class) - ->get(['object_groupable_id', 'object_group_id']); + $set = DB::table('object_groupables') + ->whereIn('object_groupable_id', $this->ids) + ->where('object_groupable_type', Budget::class) + ->get(['object_groupable_id', 'object_group_id']) + ; - $ids = array_unique($set->pluck('object_group_id')->toArray()); + $ids = array_unique($set->pluck('object_group_id')->toArray()); foreach ($set as $entry) { $this->mappedObjects[(int)$entry->object_groupable_id] = (int)$entry->object_group_id; diff --git a/app/Support/JsonApi/Enrichments/BudgetLimitEnrichment.php b/app/Support/JsonApi/Enrichments/BudgetLimitEnrichment.php index f0a7fd3479..db1e248708 100644 --- a/app/Support/JsonApi/Enrichments/BudgetLimitEnrichment.php +++ b/app/Support/JsonApi/Enrichments/BudgetLimitEnrichment.php @@ -73,7 +73,7 @@ class BudgetLimitEnrichment implements EnrichmentInterface return $this->collection; } - public function enrichSingle(array | Model $model): array | Model + public function enrichSingle(array|Model $model): array|Model { Log::debug(__METHOD__); $collection = new Collection()->push($model); @@ -115,12 +115,12 @@ class BudgetLimitEnrichment implements EnrichmentInterface private function collectBudgets(): void { - $budgetIds = $this->collection->pluck('budget_id')->unique()->toArray(); - $budgets = Budget::whereIn('id', $budgetIds)->get(); + $budgetIds = $this->collection->pluck('budget_id')->unique()->toArray(); + $budgets = Budget::whereIn('id', $budgetIds)->get(); $repository = app(OperationsRepository::class); $repository->setUser($this->user); - $expenses = $repository->collectExpenses($this->start, $this->end, null, $budgets, null); + $expenses = $repository->collectExpenses($this->start, $this->end, null, $budgets, null); /** @var BudgetLimit $budgetLimit */ foreach ($this->collection as $budgetLimit) { @@ -151,8 +151,8 @@ 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(); + $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) { @@ -169,9 +169,10 @@ class BudgetLimitEnrichment implements EnrichmentInterface 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(); + ->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']; } @@ -180,7 +181,7 @@ class BudgetLimitEnrichment implements EnrichmentInterface private function filterToBudget(array $expenses, int $budget): array { - $result = array_filter($expenses, fn(array $item) => (int)$item['budget_id'] === $budget); + $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; @@ -188,13 +189,13 @@ class BudgetLimitEnrichment implements EnrichmentInterface private function stringifyIds(): void { - $this->expenses = array_map(fn($first) => array_map(function ($second) { + $this->expenses = array_map(fn ($first) => array_map(function ($second) { $second['currency_id'] = (string)($second['currency_id'] ?? 0); return $second; }, $first), $this->expenses); - $this->pcExpenses = array_map(fn($first) => array_map(function ($second) { + $this->pcExpenses = array_map(fn ($first) => array_map(function ($second) { $second['currency_id'] = (string)($second['currency_id'] ?? 0); return $second; diff --git a/app/Support/JsonApi/Enrichments/CategoryEnrichment.php b/app/Support/JsonApi/Enrichments/CategoryEnrichment.php index 074747011b..975227844f 100644 --- a/app/Support/JsonApi/Enrichments/CategoryEnrichment.php +++ b/app/Support/JsonApi/Enrichments/CategoryEnrichment.php @@ -62,7 +62,7 @@ class CategoryEnrichment implements EnrichmentInterface return $collection; } - public function enrichSingle(array | Model $model): array | Model + public function enrichSingle(array|Model $model): array|Model { Log::debug(__METHOD__); $collection = new Collection()->push($model); @@ -123,9 +123,10 @@ class CategoryEnrichment implements EnrichmentInterface private function collectNotes(): void { $notes = Note::query()->whereIn('noteable_id', $this->ids) - ->whereNotNull('notes.text') - ->where('notes.text', '!=', '') - ->where('noteable_type', Category::class)->get(['notes.noteable_id', 'notes.text'])->toArray(); + ->whereNotNull('notes.text') + ->where('notes.text', '!=', '') + ->where('noteable_type', Category::class)->get(['notes.noteable_id', 'notes.text'])->toArray() + ; foreach ($notes as $note) { $this->notes[(int)$note['noteable_id']] = (string)$note['text']; } @@ -139,9 +140,9 @@ class CategoryEnrichment implements EnrichmentInterface $opsRepository = app(OperationsRepositoryInterface::class); $opsRepository->setUser($this->user); $opsRepository->setUserGroup($this->userGroup); - $expenses = $opsRepository->collectExpenses($this->start, $this->end, null, $this->collection); - $income = $opsRepository->collectIncome($this->start, $this->end, null, $this->collection); - $transfers = $opsRepository->collectTransfers($this->start, $this->end, null, $this->collection); + $expenses = $opsRepository->collectExpenses($this->start, $this->end, null, $this->collection); + $income = $opsRepository->collectIncome($this->start, $this->end, null, $this->collection); + $transfers = $opsRepository->collectTransfers($this->start, $this->end, null, $this->collection); foreach ($this->collection as $item) { $id = (int)$item->id; $this->spent[$id] = array_values($opsRepository->sumCollectedTransactionsByCategory($expenses, $item, 'negative', false)); diff --git a/app/Support/JsonApi/Enrichments/EnrichmentInterface.php b/app/Support/JsonApi/Enrichments/EnrichmentInterface.php index e93ddcf283..0ccfb7c060 100644 --- a/app/Support/JsonApi/Enrichments/EnrichmentInterface.php +++ b/app/Support/JsonApi/Enrichments/EnrichmentInterface.php @@ -33,7 +33,7 @@ interface EnrichmentInterface { public function enrich(Collection $collection): Collection; - public function enrichSingle(array | Model $model): array | Model; + public function enrichSingle(array|Model $model): array|Model; public function setUser(User $user): void; diff --git a/app/Support/JsonApi/Enrichments/PiggyBankEnrichment.php b/app/Support/JsonApi/Enrichments/PiggyBankEnrichment.php index aebdde8f67..e1afbe0042 100644 --- a/app/Support/JsonApi/Enrichments/PiggyBankEnrichment.php +++ b/app/Support/JsonApi/Enrichments/PiggyBankEnrichment.php @@ -43,13 +43,13 @@ use Illuminate\Support\Facades\Log; class PiggyBankEnrichment implements EnrichmentInterface { - private array $accountIds = []; // @phpstan-ignore-line - private array $accounts = []; // @phpstan-ignore-line - private array $amounts = []; + 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 $currencies = []; + private array $currencyIds = []; + private array $ids = []; // private array $accountCurrencies = []; private array $mappedObjects = []; private array $notes = []; @@ -77,7 +77,7 @@ class PiggyBankEnrichment implements EnrichmentInterface return $this->collection; } - public function enrichSingle(array | Model $model): array | Model + public function enrichSingle(array|Model $model): array|Model { Log::debug(__METHOD__); $collection = new Collection()->push($model); @@ -100,14 +100,14 @@ class PiggyBankEnrichment implements EnrichmentInterface private function appendCollectedData(): void { $this->collection = $this->collection->map(function (PiggyBank $item) { - $id = (int)$item->id; - $currencyId = (int)$item->transaction_currency_id; - $currency = $this->currencies[$currencyId] ?? $this->primaryCurrency; - $targetAmount = null; + $id = (int)$item->id; + $currencyId = (int)$item->transaction_currency_id; + $currency = $this->currencies[$currencyId] ?? $this->primaryCurrency; + $targetAmount = null; if (0 !== bccomp($item->target_amount, '0')) { $targetAmount = $item->target_amount; } - $meta = [ + $meta = [ 'notes' => $this->notes[$id] ?? null, 'currency' => $this->currencies[$currencyId] ?? null, // 'auto_budget' => $this->autoBudgets[$id] ?? null, @@ -136,17 +136,17 @@ class PiggyBankEnrichment implements EnrichmentInterface } // add current amount(s). foreach ($this->amounts[$id] as $accountId => $row) { - $meta['accounts'][] = [ + $meta['accounts'][] = [ 'account_id' => (string)$accountId, 'name' => $this->accounts[$accountId]['name'] ?? '', 'current_amount' => Steam::bcround($row['current_amount'], $currency->decimal_places), 'pc_current_amount' => Steam::bcround($row['pc_current_amount'], $this->primaryCurrency->decimal_places), ]; - $meta['current_amount'] = bcadd($meta['current_amount'], $row['current_amount']); + $meta['current_amount'] = bcadd($meta['current_amount'], $row['current_amount']); // only add pc_current_amount when the pc_current_amount is set $meta['pc_current_amount'] = null === $row['pc_current_amount'] ? null : bcadd($meta['pc_current_amount'], $row['pc_current_amount']); } - $meta['current_amount'] = Steam::bcround($meta['current_amount'], $currency->decimal_places); + $meta['current_amount'] = Steam::bcround($meta['current_amount'], $currency->decimal_places); // only round this number when pc_current_amount is set. $meta['pc_current_amount'] = null === $meta['pc_current_amount'] ? null : Steam::bcround($meta['pc_current_amount'], $this->primaryCurrency->decimal_places); @@ -160,7 +160,7 @@ class PiggyBankEnrichment implements EnrichmentInterface $meta['save_per_month'] = Steam::bcround($this->getSuggestedMonthlyAmount($item->start_date, $item->target_date, $meta['target_amount'], $meta['current_amount']), $currency->decimal_places); $meta['pc_save_per_month'] = Steam::bcround($this->getSuggestedMonthlyAmount($item->start_date, $item->target_date, $meta['pc_target_amount'], $meta['pc_current_amount']), $currency->decimal_places); - $item->meta = $meta; + $item->meta = $meta; return $item; }); @@ -176,7 +176,7 @@ class PiggyBankEnrichment implements EnrichmentInterface $this->ids[] = $id; $this->currencyIds[$id] = (int)$piggy->transaction_currency_id; } - $this->ids = array_unique($this->ids); + $this->ids = array_unique($this->ids); // collect currencies. $currencies = TransactionCurrency::whereIn('id', $this->currencyIds)->get(); @@ -185,10 +185,10 @@ class PiggyBankEnrichment implements EnrichmentInterface } // 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']); + $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; + $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; @@ -206,7 +206,7 @@ class PiggyBankEnrichment implements EnrichmentInterface } // get account currency preference for ALL. - $set = AccountMeta::whereIn('account_id', array_values($this->accountIds))->where('name', 'currency_id')->get(); + $set = AccountMeta::whereIn('account_id', array_values($this->accountIds))->where('name', 'currency_id')->get(); /** @var AccountMeta $item */ foreach ($set as $item) { @@ -219,7 +219,7 @@ class PiggyBankEnrichment implements EnrichmentInterface } // get account info. - $set = Account::whereIn('id', array_values($this->accountIds))->get(); + $set = Account::whereIn('id', array_values($this->accountIds))->get(); /** @var Account $item */ foreach ($set as $item) { @@ -234,9 +234,10 @@ class PiggyBankEnrichment implements EnrichmentInterface private function collectNotes(): void { $notes = Note::query()->whereIn('noteable_id', $this->ids) - ->whereNotNull('notes.text') - ->where('notes.text', '!=', '') - ->where('noteable_type', PiggyBank::class)->get(['notes.noteable_id', 'notes.text'])->toArray(); + ->whereNotNull('notes.text') + ->where('notes.text', '!=', '') + ->where('noteable_type', PiggyBank::class)->get(['notes.noteable_id', 'notes.text'])->toArray() + ; foreach ($notes as $note) { $this->notes[(int)$note['noteable_id']] = (string)$note['text']; } @@ -245,12 +246,13 @@ class PiggyBankEnrichment implements EnrichmentInterface private function collectObjectGroups(): void { - $set = DB::table('object_groupables') - ->whereIn('object_groupable_id', $this->ids) - ->where('object_groupable_type', PiggyBank::class) - ->get(['object_groupable_id', 'object_group_id']); + $set = DB::table('object_groupables') + ->whereIn('object_groupable_id', $this->ids) + ->where('object_groupable_type', PiggyBank::class) + ->get(['object_groupable_id', 'object_group_id']) + ; - $ids = array_unique($set->pluck('object_group_id')->toArray()); + $ids = array_unique($set->pluck('object_group_id')->toArray()); foreach ($set as $entry) { $this->mappedObjects[(int)$entry->object_groupable_id] = (int)$entry->object_group_id; diff --git a/app/Support/JsonApi/Enrichments/PiggyBankEventEnrichment.php b/app/Support/JsonApi/Enrichments/PiggyBankEventEnrichment.php index 8758dfe94d..56a0714466 100644 --- a/app/Support/JsonApi/Enrichments/PiggyBankEventEnrichment.php +++ b/app/Support/JsonApi/Enrichments/PiggyBankEventEnrichment.php @@ -66,7 +66,7 @@ class PiggyBankEventEnrichment implements EnrichmentInterface return $this->collection; } - public function enrichSingle(array | Model $model): array | Model + public function enrichSingle(array|Model $model): array|Model { Log::debug(__METHOD__); $collection = new Collection()->push($model); @@ -89,10 +89,10 @@ class PiggyBankEventEnrichment implements EnrichmentInterface 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; + $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)) { @@ -120,7 +120,7 @@ class PiggyBankEventEnrichment implements EnrichmentInterface } $this->ids = array_unique($this->ids); // collect groups with journal info. - $set = TransactionJournal::whereIn('id', $this->journalIds)->get(['id', 'transaction_group_id']); + $set = TransactionJournal::whereIn('id', $this->journalIds)->get(['id', 'transaction_group_id']); /** @var TransactionJournal $item */ foreach ($set as $item) { @@ -128,7 +128,7 @@ class PiggyBankEventEnrichment implements EnrichmentInterface } // collect account info. - $set = DB::table('account_piggy_bank')->whereIn('piggy_bank_id', $this->piggyBankIds)->get(['piggy_bank_id', 'account_id']); + $set = DB::table('account_piggy_bank')->whereIn('piggy_bank_id', $this->piggyBankIds)->get(['piggy_bank_id', 'account_id']); foreach ($set as $item) { $id = (int)$item->piggy_bank_id; if (!array_key_exists($id, $this->accountIds)) { @@ -137,12 +137,12 @@ class PiggyBankEventEnrichment implements EnrichmentInterface } // get account currency preference for ALL. - $set = AccountMeta::whereIn('account_id', array_values($this->accountIds))->where('name', 'currency_id')->get(); + $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; + $accountId = (int)$item->account_id; + $currencyId = (int)$item->data; if (!array_key_exists($currencyId, $this->currencies)) { $this->currencies[$currencyId] = Amount::getTransactionCurrencyById($currencyId); } diff --git a/app/Support/JsonApi/Enrichments/RecurringEnrichment.php b/app/Support/JsonApi/Enrichments/RecurringEnrichment.php index 30484f5388..7fdfc1c5dc 100644 --- a/app/Support/JsonApi/Enrichments/RecurringEnrichment.php +++ b/app/Support/JsonApi/Enrichments/RecurringEnrichment.php @@ -51,11 +51,12 @@ use Illuminate\Database\Eloquent\Model; use Illuminate\Support\Arr; use Illuminate\Support\Collection; use Illuminate\Support\Facades\Log; + use function Safe\json_decode; class RecurringEnrichment implements EnrichmentInterface { - private array $accounts = []; + private array $accounts = []; private Collection $collection; // private array $transactionTypeIds = []; // private array $transactionTypes = []; @@ -97,7 +98,7 @@ class RecurringEnrichment implements EnrichmentInterface return $this->collection; } - public function enrichSingle(array | Model $model): array | Model + public function enrichSingle(array|Model $model): array|Model { Log::debug(__METHOD__); $collection = new Collection()->push($model); @@ -131,7 +132,7 @@ class RecurringEnrichment implements EnrichmentInterface return (string)trans('firefly.recurring_monthly', ['dayOfMonth' => $repetition->repetition_moment, 'skip' => $repetition->repetition_skip - 1], $this->language); } if ('ndom' === $repetition->repetition_type) { - $parts = explode(',', $repetition->repetition_moment); + $parts = explode(',', $repetition->repetition_moment); // first part is number of week, second is weekday. $dayOfWeek = trans(sprintf('config.dow_%s', $parts[1]), [], $this->language); if ($repetition->repetition_skip > 0) { @@ -148,7 +149,7 @@ class RecurringEnrichment implements EnrichmentInterface } // $diffInYears = (int)$today->diffInYears($repDate, true); // $repDate->addYears($diffInYears); // technically not necessary. - $string = $repDate->isoFormat((string)trans('config.month_and_day_no_year_js')); + $string = $repDate->isoFormat((string)trans('config.month_and_day_no_year_js')); return (string)trans('firefly.recurring_yearly', ['date' => $string], $this->language); } @@ -171,8 +172,8 @@ class RecurringEnrichment implements EnrichmentInterface private function appendCollectedData(): void { $this->collection = $this->collection->map(function (Recurrence $item) { - $id = (int)$item->id; - $meta = [ + $id = (int)$item->id; + $meta = [ 'notes' => $this->notes[$id] ?? null, 'repetitions' => array_values($this->repetitions[$id] ?? []), 'transactions' => $this->processTransactions(array_values($this->transactions[$id] ?? [])), @@ -285,7 +286,7 @@ class RecurringEnrichment implements EnrichmentInterface { /** @var Recurrence $recurrence */ foreach ($this->collection as $recurrence) { - $id = (int)$recurrence->id; + $id = (int)$recurrence->id; // $typeId = (int)$recurrence->transaction_type_id; $this->ids[] = $id; // $this->transactionTypeIds[$id] = $typeId; @@ -303,9 +304,10 @@ class RecurringEnrichment implements EnrichmentInterface 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(); + ->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']; } @@ -335,20 +337,20 @@ class RecurringEnrichment implements EnrichmentInterface Log::debug('Start of enrichment: collectRepetitions()'); $repository = app(RecurringRepositoryInterface::class); $repository->setUserGroup($this->userGroup); - $set = RecurrenceRepetition::whereIn('recurrence_id', $this->ids)->get(); + $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; + $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 = []; + $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) { @@ -371,8 +373,8 @@ class RecurringEnrichment implements EnrichmentInterface private function collectTransactionMetaData(): void { - $ids = array_keys($this->transactions); - $meta = RecurrenceTransactionMeta::whereNull('deleted_at')->whereIn('rt_id', $ids)->get(); + $ids = array_keys($this->transactions); + $meta = RecurrenceTransactionMeta::whereNull('deleted_at')->whereIn('rt_id', $ids)->get(); // other meta-data to be collected: $billIds = []; $piggyBankIds = []; @@ -384,8 +386,8 @@ class RecurringEnrichment implements EnrichmentInterface $transactionId = (int)$entry->rt_id; // this should refer to another array, were rtIds can be used to find the recurrence. - $recurrenceId = $this->recurrenceIds[$transactionId] ?? 0; - $name = (string)($entry->name ?? ''); + $recurrenceId = $this->recurrenceIds[$transactionId] ?? 0; + $name = (string)($entry->name ?? ''); if (0 === $recurrenceId) { Log::error(sprintf('Could not find recurrence ID for recurrence transaction ID %d', $transactionId)); @@ -485,14 +487,14 @@ class RecurringEnrichment implements EnrichmentInterface /** @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; + $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] = [ + $this->transactions[$id][$transactionId] = [ 'id' => (string)$transactionId, // 'recurrence_id' => $id, 'transaction_currency_id' => (int)$transaction->transaction_currency_id, @@ -529,8 +531,8 @@ class RecurringEnrichment implements EnrichmentInterface private function getLanguage(): void { /** @var Preference $preference */ - $preference = Preferences::getForUser($this->user, 'language', config('firefly.default_language', 'en_US')); - $language = $preference->data; + $preference = Preferences::getForUser($this->user, 'language', config('firefly.default_language', 'en_US')); + $language = $preference->data; if (is_array($language)) { $language = 'en_US'; } @@ -543,9 +545,9 @@ class RecurringEnrichment implements EnrichmentInterface $return = []; $converter = new ExchangeRateConverter(); foreach ($transactions as $transaction) { - $currencyId = $transaction['transaction_currency_id']; - $pcAmount = null; - $pcForeignAmount = null; + $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']; @@ -561,26 +563,26 @@ class RecurringEnrichment implements EnrichmentInterface } } - $transaction['pc_amount'] = $pcAmount; - $transaction['pc_foreign_amount'] = $pcForeignAmount; + $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']; + $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']; + $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['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; @@ -602,7 +604,7 @@ class RecurringEnrichment implements EnrichmentInterface $transaction['foreign_currency_decimal_places'] = $this->currencies[$currencyId]->decimal_places; } unset($transaction['transaction_currency_id']); - $return[] = $transaction; + $return[] = $transaction; } return $return; diff --git a/app/Support/JsonApi/Enrichments/SubscriptionEnrichment.php b/app/Support/JsonApi/Enrichments/SubscriptionEnrichment.php index 285e0ad37e..68c3dc0f12 100644 --- a/app/Support/JsonApi/Enrichments/SubscriptionEnrichment.php +++ b/app/Support/JsonApi/Enrichments/SubscriptionEnrichment.php @@ -86,11 +86,11 @@ class SubscriptionEnrichment implements EnrichmentInterface $paidDates = $this->paidDates; $payDates = $this->payDates; $this->collection = $this->collection->map(function (Bill $item) use ($notes, $objectGroups, $paidDates, $payDates) { - $id = (int)$item->id; - $currency = $item->transactionCurrency; - $nem = $this->getNextExpectedMatch($payDates[$id] ?? []); + $id = (int)$item->id; + $currency = $item->transactionCurrency; + $nem = $this->getNextExpectedMatch($payDates[$id] ?? []); - $meta = [ + $meta = [ 'notes' => null, 'object_group_id' => null, 'object_group_title' => null, @@ -101,7 +101,7 @@ class SubscriptionEnrichment implements EnrichmentInterface 'nem' => $nem, 'nem_diff' => $this->getNextExpectedMatchDiff($nem, $payDates[$id] ?? []), ]; - $amounts = [ + $amounts = [ 'amount_min' => Steam::bcround($item->amount_min, $currency->decimal_places), 'amount_max' => Steam::bcround($item->amount_max, $currency->decimal_places), 'average' => Steam::bcround(bcdiv(bcadd($item->amount_min, $item->amount_max), '2'), $currency->decimal_places), @@ -142,7 +142,7 @@ class SubscriptionEnrichment implements EnrichmentInterface return $collection; } - public function enrichSingle(array | Model $model): array | Model + public function enrichSingle(array|Model $model): array|Model { Log::debug(__METHOD__); $collection = new Collection()->push($model); @@ -177,13 +177,13 @@ class SubscriptionEnrichment implements EnrichmentInterface */ protected function lastPaidDate(Bill $subscription, Collection $dates, Carbon $default): Carbon { - $filtered = $dates->filter(fn(TransactionJournal $journal) => (int)$journal->bill_id === (int)$subscription->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; } - $latest = $filtered->first()->date; + $latest = $filtered->first()->date; /** @var TransactionJournal $journal */ foreach ($filtered as $journal) { @@ -198,9 +198,10 @@ class SubscriptionEnrichment implements EnrichmentInterface 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(); + ->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']; } @@ -209,12 +210,13 @@ class SubscriptionEnrichment implements EnrichmentInterface private function collectObjectGroups(): void { - $set = DB::table('object_groupables') - ->whereIn('object_groupable_id', $this->subscriptionIds) - ->where('object_groupable_type', Bill::class) - ->get(['object_groupable_id', 'object_group_id']); + $set = DB::table('object_groupables') + ->whereIn('object_groupable_id', $this->subscriptionIds) + ->where('object_groupable_type', Bill::class) + ->get(['object_groupable_id', 'object_group_id']) + ; - $ids = array_unique($set->pluck('object_group_id')->toArray()); + $ids = array_unique($set->pluck('object_group_id')->toArray()); foreach ($set as $entry) { $this->mappedObjects[(int)$entry->object_groupable_id] = (int)$entry->object_group_id; @@ -242,13 +244,13 @@ class SubscriptionEnrichment implements EnrichmentInterface // 2023-07-18 this particular date is used to search for the last paid date. // 2023-07-18 the cloned $searchDate is used to grab the correct transactions. /** @var Carbon $start */ - $start = clone $this->start; - $searchStart = clone $start; + $start = clone $this->start; + $searchStart = clone $start; $start->subDay(); /** @var Carbon $end */ - $end = clone $this->end; - $searchEnd = clone $end; + $end = clone $this->end; + $searchEnd = clone $end; // move the search dates to the start of the day. $searchStart->startOfDay(); @@ -257,13 +259,13 @@ class SubscriptionEnrichment implements EnrichmentInterface Log::debug(sprintf('Search parameters are: start: %s, end: %s', $searchStart->format('Y-m-d H:i:s'), $searchEnd->format('Y-m-d H:i:s'))); // Get from database when bills were paid. - $set = $this->user->transactionJournals() - ->whereIn('bill_id', $this->subscriptionIds) - ->leftJoin('transactions', 'transactions.transaction_journal_id', '=', 'transaction_journals.id') - ->leftJoin('transaction_currencies AS currency', 'currency.id', '=', 'transactions.transaction_currency_id') - ->leftJoin('transaction_currencies AS foreign_currency', 'foreign_currency.id', '=', 'transactions.foreign_currency_id') - ->where('transactions.amount', '>', 0) - ->before($searchEnd)->after($searchStart)->get( + $set = $this->user->transactionJournals() + ->whereIn('bill_id', $this->subscriptionIds) + ->leftJoin('transactions', 'transactions.transaction_journal_id', '=', 'transaction_journals.id') + ->leftJoin('transaction_currencies AS currency', 'currency.id', '=', 'transactions.transaction_currency_id') + ->leftJoin('transaction_currencies AS foreign_currency', 'foreign_currency.id', '=', 'transactions.foreign_currency_id') + ->where('transactions.amount', '>', 0) + ->before($searchEnd)->after($searchStart)->get( [ 'transaction_journals.id', 'transaction_journals.date', @@ -280,24 +282,25 @@ class SubscriptionEnrichment implements EnrichmentInterface 'transactions.amount', 'transactions.foreign_amount', ] - ); + ) + ; Log::debug(sprintf('Count %d entries in set', $set->count())); // for each bill, do a loop. - $converter = new ExchangeRateConverter(); + $converter = new ExchangeRateConverter(); /** @var Bill $subscription */ foreach ($this->collection as $subscription) { // Grab from array the most recent payment. If none exist, fall back to the start date and pretend *that* was the last paid date. Log::debug(sprintf('Grab last paid date from function, return %s if it comes up with nothing.', $start->format('Y-m-d'))); - $lastPaidDate = $this->lastPaidDate($subscription, $set, $start); + $lastPaidDate = $this->lastPaidDate($subscription, $set, $start); Log::debug(sprintf('Result of lastPaidDate is %s', $lastPaidDate->format('Y-m-d'))); // At this point the "next match" is exactly after the last time the bill was paid. - $result = []; - $filtered = $set->filter(fn(TransactionJournal $journal) => (int)$journal->bill_id === (int)$subscription->id); + $result = []; + $filtered = $set->filter(fn (TransactionJournal $journal) => (int)$journal->bill_id === (int)$subscription->id); foreach ($filtered as $entry) { - $array = [ + $array = [ 'transaction_group_id' => (string)$entry->transaction_group_id, 'transaction_journal_id' => (string)$entry->id, 'date' => $entry->date->toAtomString(), @@ -360,12 +363,12 @@ class SubscriptionEnrichment implements EnrichmentInterface /** @var Bill $subscription */ foreach ($this->collection as $subscription) { - $id = (int)$subscription->id; - $lastPaidDate = $this->getLastPaidDate($this->paidDates[$id] ?? []); - $payDates = $this->calculator->getPayDates($this->start, $this->end, $subscription->date, $subscription->repeat_freq, $subscription->skip, $lastPaidDate); - $payDatesFormatted = []; + $id = (int)$subscription->id; + $lastPaidDate = $this->getLastPaidDate($this->paidDates[$id] ?? []); + $payDates = $this->calculator->getPayDates($this->start, $this->end, $subscription->date, $subscription->repeat_freq, $subscription->skip, $lastPaidDate); + $payDatesFormatted = []; foreach ($payDates as $string) { - $date = Carbon::createFromFormat('!Y-m-d', $string, config('app.timezone')); + $date = Carbon::createFromFormat('!Y-m-d', $string, config('app.timezone')); if (!$date instanceof Carbon) { $date = today(config('app.timezone')); } diff --git a/app/Support/JsonApi/Enrichments/TransactionGroupEnrichment.php b/app/Support/JsonApi/Enrichments/TransactionGroupEnrichment.php index 014aff8e68..a8dccfe9fc 100644 --- a/app/Support/JsonApi/Enrichments/TransactionGroupEnrichment.php +++ b/app/Support/JsonApi/Enrichments/TransactionGroupEnrichment.php @@ -83,7 +83,7 @@ class TransactionGroupEnrichment implements EnrichmentInterface } #[Override] - public function enrichSingle(array | Model $model): array | TransactionGroup + public function enrichSingle(array|Model $model): array|TransactionGroup { Log::debug(__METHOD__); if (is_array($model)) { @@ -109,28 +109,28 @@ class TransactionGroupEnrichment implements EnrichmentInterface private function appendCollectedData(): void { - $notes = $this->notes; - $tags = $this->tags; - $metaData = $this->metaData; - $locations = $this->locations; - $attachmentCount = $this->attachmentCount; - $primaryCurrency = $this->primaryCurrency; + $notes = $this->notes; + $tags = $this->tags; + $metaData = $this->metaData; + $locations = $this->locations; + $attachmentCount = $this->attachmentCount; + $primaryCurrency = $this->primaryCurrency; $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; + $item['transactions'][$index]['notes'] = array_key_exists($journalId, $notes) ? $notes[$journalId] : null; // attach tags if they exist: - $item['transactions'][$index]['tags'] = array_key_exists($journalId, $tags) ? $tags[$journalId] : []; + $item['transactions'][$index]['tags'] = array_key_exists($journalId, $tags) ? $tags[$journalId] : []; // attachment count $item['transactions'][$index]['attachment_count'] = array_key_exists($journalId, $attachmentCount) ? $attachmentCount[$journalId] : 0; // default location data - $item['transactions'][$index]['location'] = [ + $item['transactions'][$index]['location'] = [ 'latitude' => null, 'longitude' => null, 'zoom_level' => null, @@ -146,8 +146,8 @@ class TransactionGroupEnrichment implements EnrichmentInterface ]; // append meta data - $item['transactions'][$index]['meta'] = []; - $item['transactions'][$index]['meta_date'] = []; + $item['transactions'][$index]['meta'] = []; + $item['transactions'][$index]['meta_date'] = []; if (array_key_exists($journalId, $metaData)) { // loop al meta data: foreach ($metaData[$journalId] as $name => $value) { @@ -175,11 +175,12 @@ class TransactionGroupEnrichment implements EnrichmentInterface // 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(); + ->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']; } @@ -199,14 +200,15 @@ class TransactionGroupEnrichment implements EnrichmentInterface 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(); + ->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'], - ]; + '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))); } @@ -215,8 +217,8 @@ class TransactionGroupEnrichment implements EnrichmentInterface { $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']; + $name = $entry['name']; + $data = (string)$entry['data']; if ('' === $data) { continue; } @@ -234,9 +236,10 @@ class TransactionGroupEnrichment implements EnrichmentInterface 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(); + ->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']; } @@ -246,11 +249,12 @@ class TransactionGroupEnrichment implements EnrichmentInterface 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(); + ->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] ??= []; $this->tags[$journalId][] = $item['tag']; } } diff --git a/app/Support/JsonApi/Enrichments/WebhookEnrichment.php b/app/Support/JsonApi/Enrichments/WebhookEnrichment.php index 0004c291c8..e847a16b0f 100644 --- a/app/Support/JsonApi/Enrichments/WebhookEnrichment.php +++ b/app/Support/JsonApi/Enrichments/WebhookEnrichment.php @@ -66,7 +66,7 @@ class WebhookEnrichment implements EnrichmentInterface return $this->collection; } - public function enrichSingle(array | Model $model): array | Model + public function enrichSingle(array|Model $model): array|Model { Log::debug(__METHOD__); $collection = new Collection()->push($model); diff --git a/app/Support/Models/AccountBalanceCalculator.php b/app/Support/Models/AccountBalanceCalculator.php index d9e616f9b9..5675f92adc 100644 --- a/app/Support/Models/AccountBalanceCalculator.php +++ b/app/Support/Models/AccountBalanceCalculator.php @@ -65,9 +65,9 @@ class AccountBalanceCalculator public static function recalculateForJournal(TransactionJournal $transactionJournal): void { Log::debug(__METHOD__); - $object = new self(); + $object = new self(); - $set = []; + $set = []; foreach ($transactionJournal->transactions as $transaction) { $set[$transaction->account_id] = $transaction->account; } @@ -81,17 +81,18 @@ class AccountBalanceCalculator 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') + $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); + ->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); @@ -112,14 +113,15 @@ class AccountBalanceCalculator $balances = []; $count = 0; $query = Transaction::leftJoin('transaction_journals', 'transaction_journals.id', '=', 'transactions.transaction_journal_id') - ->whereNull('transactions.deleted_at') - ->whereNull('transaction_journals.deleted_at') + ->whereNull('transactions.deleted_at') + ->whereNull('transaction_journals.deleted_at') // this order is the same as GroupCollector, but in the exact reverse. - ->orderBy('transaction_journals.date', 'asc') - ->orderBy('transaction_journals.order', 'desc') - ->orderBy('transaction_journals.id', 'asc') - ->orderBy('transaction_journals.description', 'asc') - ->orderBy('transactions.amount', 'asc'); + ->orderBy('transaction_journals.date', 'asc') + ->orderBy('transaction_journals.order', 'desc') + ->orderBy('transaction_journals.id', 'asc') + ->orderBy('transaction_journals.description', 'asc') + ->orderBy('transactions.amount', 'asc') + ; if ($accounts->count() > 0) { $query->whereIn('transactions.account_id', $accounts->pluck('id')->toArray()); } @@ -128,7 +130,7 @@ class AccountBalanceCalculator $query->where('transaction_journals.date', '>=', $notBefore); } - $set = $query->get(['transactions.id', 'transactions.balance_dirty', 'transactions.transaction_currency_id', 'transaction_journals.date', 'transactions.account_id', 'transactions.amount']); + $set = $query->get(['transactions.id', 'transactions.balance_dirty', 'transactions.transaction_currency_id', 'transaction_journals.date', 'transactions.account_id', 'transactions.amount']); Log::debug(sprintf('Counted %d transaction(s)', $set->count())); // the balance value is an array. @@ -141,8 +143,8 @@ class AccountBalanceCalculator $balances[$entry->account_id][$entry->transaction_currency_id] ??= [$this->getLatestBalance($entry->account_id, $entry->transaction_currency_id, $notBefore), null]; // before and after are easy: - $before = $balances[$entry->account_id][$entry->transaction_currency_id][0]; - $after = bcadd($before, (string)$entry->amount); + $before = $balances[$entry->account_id][$entry->transaction_currency_id][0]; + $after = bcadd($before, (string)$entry->amount); if (true === $entry->balance_dirty || $accounts->count() > 0) { // update the transaction: $entry->balance_before = $before; diff --git a/app/Support/Models/BillDateCalculator.php b/app/Support/Models/BillDateCalculator.php index 9f40348777..3d49c867a3 100644 --- a/app/Support/Models/BillDateCalculator.php +++ b/app/Support/Models/BillDateCalculator.php @@ -49,15 +49,15 @@ class BillDateCalculator Log::debug(sprintf('Dates must be between %s and %s.', $earliest->format('Y-m-d'), $latest->format('Y-m-d'))); Log::debug(sprintf('Bill started on %s, period is "%s", skip is %d, last paid = "%s".', $billStart->format('Y-m-d'), $period, $skip, $lastPaid?->format('Y-m-d'))); - $daysUntilEOM = app('navigation')->daysUntilEndOfMonth($billStart); + $daysUntilEOM = app('navigation')->daysUntilEndOfMonth($billStart); Log::debug(sprintf('For bill start, days until end of month is %d', $daysUntilEOM)); - $set = new Collection(); - $currentStart = clone $earliest; + $set = new Collection(); + $currentStart = clone $earliest; // 2023-06-23 subDay to fix 7655 $currentStart->subDay(); - $loop = 0; + $loop = 0; Log::debug('Start of loop'); while ($currentStart <= $latest) { @@ -107,7 +107,7 @@ class BillDateCalculator // for the next loop, go to end of period, THEN add day. Log::debug('Add one day to nextExpectedMatch/currentStart.'); $nextExpectedMatch->addDay(); - $currentStart = clone $nextExpectedMatch; + $currentStart = clone $nextExpectedMatch; ++$loop; if ($loop > 31) { @@ -117,8 +117,8 @@ class BillDateCalculator } } Log::debug('end of loop'); - $simple = $set->map( // @phpstan-ignore-line - static fn(Carbon $date) => $date->format('Y-m-d') + $simple = $set->map( // @phpstan-ignore-line + static fn (Carbon $date) => $date->format('Y-m-d') ); Log::debug(sprintf('Found %d pay dates', $set->count()), $simple->toArray()); @@ -140,7 +140,7 @@ class BillDateCalculator return $billStartDate; } - $steps = app('navigation')->diffInPeriods($period, $skip, $earliest, $billStartDate); + $steps = app('navigation')->diffInPeriods($period, $skip, $earliest, $billStartDate); if ($steps === $this->diffInMonths) { Log::debug(sprintf('Steps is %d, which is the same as diffInMonths (%d), so we add another 1.', $steps, $this->diffInMonths)); ++$steps; diff --git a/app/Support/Models/ReturnsIntegerIdTrait.php b/app/Support/Models/ReturnsIntegerIdTrait.php index d8178e07a5..f3fac096ce 100644 --- a/app/Support/Models/ReturnsIntegerIdTrait.php +++ b/app/Support/Models/ReturnsIntegerIdTrait.php @@ -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, ); } } diff --git a/app/Support/Models/ReturnsIntegerUserIdTrait.php b/app/Support/Models/ReturnsIntegerUserIdTrait.php index 8eca6e943c..3f1808923d 100644 --- a/app/Support/Models/ReturnsIntegerUserIdTrait.php +++ b/app/Support/Models/ReturnsIntegerUserIdTrait.php @@ -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, ); } } diff --git a/app/Support/Navigation.php b/app/Support/Navigation.php index a1e2c256c7..cb9ac29d9f 100644 --- a/app/Support/Navigation.php +++ b/app/Support/Navigation.php @@ -77,10 +77,10 @@ class Navigation if (!array_key_exists($repeatFreq, $functionMap)) { Log::error(sprintf( - 'The periodicity %s is unknown. Choose one of available periodicity: %s', - $repeatFreq, - implode(', ', array_keys($functionMap)) - )); + 'The periodicity %s is unknown. Choose one of available periodicity: %s', + $repeatFreq, + implode(', ', array_keys($functionMap)) + )); return $theDate; } @@ -93,7 +93,7 @@ class Navigation if ($end < $start) { [$start, $end] = [$end, $start]; } - $periods = []; + $periods = []; // first, 13 periods of [range] $loopCount = 0; $loopDate = clone $end; @@ -151,13 +151,13 @@ class Navigation 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 = [ + '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', @@ -170,7 +170,7 @@ class Navigation return 1; } - $func = $map[$period]; + $func = $map[$period]; // first do the diff $floatDiff = $beginning->{$func}($end, true); // @phpstan-ignore-line @@ -185,7 +185,7 @@ class Navigation } // then do ceil() - $diff = ceil($floatDiff); + $diff = ceil($floatDiff); Log::debug(sprintf('Diff is %f periods (%d rounded up)', $floatDiff, $diff)); @@ -193,11 +193,11 @@ class Navigation $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 - )); + 'diffInPeriods: skip is %d, so param is %d, and diff becomes %d', + $skip, + $parameter, + $diff + )); } return (int)$diff; @@ -205,7 +205,7 @@ class Navigation public function endOfPeriod(Carbon $end, string $repeatFreq): Carbon { - $currentEnd = clone $end; + $currentEnd = clone $end; // Log::debug(sprintf('Now in endOfPeriod("%s", "%s").', $currentEnd->toIso8601String(), $repeatFreq)); $functionMap = [ @@ -239,7 +239,7 @@ class Navigation Log::debug('Session data available.'); /** @var Carbon $tStart */ - $tStart = session('start', today(config('app.timezone'))->startOfMonth()); + $tStart = session('start', today(config('app.timezone'))->startOfMonth()); /** @var Carbon $tEnd */ $tEnd = session('end', today(config('app.timezone'))->endOfMonth()); @@ -259,7 +259,7 @@ class Navigation return $end->endOfMonth(); } - $result = match ($repeatFreq) { + $result = match ($repeatFreq) { 'last7' => $currentEnd->addDays(7)->startOfDay(), 'last30' => $currentEnd->addDays(30)->startOfDay(), 'last90' => $currentEnd->addDays(90)->startOfDay(), @@ -279,7 +279,7 @@ class Navigation return $end; } - $function = $functionMap[$repeatFreq]; + $function = $functionMap[$repeatFreq]; if (array_key_exists($repeatFreq, $modifierMap)) { $currentEnd->{$function}($modifierMap[$repeatFreq])->milli(0); // @phpstan-ignore-line @@ -319,7 +319,7 @@ class Navigation 'yearly' => 'endOfYear', ]; - $currentEnd = clone $theCurrentEnd; + $currentEnd = clone $theCurrentEnd; if (array_key_exists($repeatFreq, $functionMap)) { $function = $functionMap[$repeatFreq]; @@ -362,7 +362,7 @@ class Navigation */ public function listOfPeriods(Carbon $start, Carbon $end): array { - $locale = app('steam')->getLocale(); + $locale = app('steam')->getLocale(); // define period to increment $increment = 'addDay'; $format = $this->preferredCarbonFormat($start, $end); @@ -379,8 +379,8 @@ class Navigation $increment = 'addYear'; $displayFormat = (string)trans('config.year_js'); } - $begin = clone $start; - $entries = []; + $begin = clone $start; + $entries = []; while ($begin < $end) { $formatted = $begin->format($format); $displayed = $begin->isoFormat($displayFormat); @@ -439,6 +439,7 @@ class Navigation // 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'); @@ -557,9 +558,9 @@ class Navigation public function startOfPeriod(Carbon $theDate, string $repeatFreq): Carbon { - $date = clone $theDate; + $date = clone $theDate; // Log::debug(sprintf('Now in startOfPeriod("%s", "%s")', $date->toIso8601String(), $repeatFreq)); - $functionMap = [ + $functionMap = [ '1D' => 'startOfDay', 'daily' => 'startOfDay', '1W' => 'startOfWeek', @@ -606,7 +607,7 @@ class Navigation return $date; } - $result = match ($repeatFreq) { + $result = match ($repeatFreq) { 'last7' => $date->subDays(7)->startOfDay(), 'last30' => $date->subDays(30)->startOfDay(), 'last90' => $date->subDays(90)->startOfDay(), @@ -638,7 +639,7 @@ class Navigation public function subtractPeriod(Carbon $theDate, string $repeatFreq, ?int $subtract = null): Carbon { $subtract ??= 1; - $date = clone $theDate; + $date = clone $theDate; // 1D 1W 1M 3M 6M 1Y $functionMap = [ '1D' => 'subDays', @@ -677,7 +678,7 @@ class Navigation // this is then subtracted from $theDate (* $subtract). if ('custom' === $repeatFreq) { /** @var Carbon $tStart */ - $tStart = session('start', today(config('app.timezone'))->startOfMonth()); + $tStart = session('start', today(config('app.timezone'))->startOfMonth()); /** @var Carbon $tEnd */ $tEnd = session('end', today(config('app.timezone'))->endOfMonth()); @@ -771,7 +772,7 @@ class Navigation return $fiscalHelper->endOfFiscalYear($end); } - $list = [ + $list = [ 'last7', 'last30', 'last90', diff --git a/app/Support/Observers/RecalculatesAvailableBudgetsTrait.php b/app/Support/Observers/RecalculatesAvailableBudgetsTrait.php index d71872971c..82f513a44e 100644 --- a/app/Support/Observers/RecalculatesAvailableBudgetsTrait.php +++ b/app/Support/Observers/RecalculatesAvailableBudgetsTrait.php @@ -41,10 +41,10 @@ trait RecalculatesAvailableBudgetsTrait { private function calculateAmount(AvailableBudget $availableBudget): void { - $repository = app(BudgetLimitRepositoryInterface::class); + $repository = app(BudgetLimitRepositoryInterface::class); $repository->setUser($availableBudget->user); - $newAmount = '0'; - $abPeriod = Period::make($availableBudget->start_date, $availableBudget->end_date, Precision::DAY()); + $newAmount = '0'; + $abPeriod = Period::make($availableBudget->start_date, $availableBudget->end_date, Precision::DAY()); Log::debug( sprintf( 'Now at AB #%d, ("%s" to "%s")', @@ -54,7 +54,7 @@ trait RecalculatesAvailableBudgetsTrait ) ); // have to recalculate everything just in case. - $set = $repository->getAllBudgetLimitsByCurrency($availableBudget->transactionCurrency, $availableBudget->start_date, $availableBudget->end_date); + $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 */ @@ -69,8 +69,8 @@ trait RecalculatesAvailableBudgetsTrait ); // overlap in days: $limitPeriod = Period::make( - $budgetLimit->start_date, - $budgetLimit->end_date, + $budgetLimit->start_date, + $budgetLimit->end_date, precision : Precision::DAY(), boundaries: Boundaries::EXCLUDE_NONE() ); @@ -111,8 +111,8 @@ trait RecalculatesAvailableBudgetsTrait return '0'; } $limitPeriod = Period::make( - $budgetLimit->start_date, - $budgetLimit->end_date, + $budgetLimit->start_date, + $budgetLimit->end_date, precision : Precision::DAY(), boundaries: Boundaries::EXCLUDE_NONE() ); @@ -130,7 +130,7 @@ trait RecalculatesAvailableBudgetsTrait Log::debug(sprintf('Now in updateAvailableBudget(limit #%d)', $budgetLimit->id)); /** @var null|Budget $budget */ - $budget = Budget::find($budgetLimit->budget_id); + $budget = Budget::find($budgetLimit->budget_id); if (null === $budget) { Log::warning('Budget is null, probably deleted, find deleted version.'); @@ -145,7 +145,7 @@ trait RecalculatesAvailableBudgetsTrait } /** @var null|User $user */ - $user = $budget->user; + $user = $budget->user; // sanity check. It happens when the budget has been deleted so the original user is unknown. if (null === $user) { @@ -161,7 +161,7 @@ trait RecalculatesAvailableBudgetsTrait // all have to be created or updated. try { $viewRange = app('preferences')->getForUser($user, 'viewRange', '1M')->data; - } catch (ContainerExceptionInterface | NotFoundExceptionInterface $e) { + } catch (ContainerExceptionInterface|NotFoundExceptionInterface $e) { Log::error($e->getMessage()); $viewRange = '1M'; } @@ -169,20 +169,20 @@ 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); - $end = app('navigation')->endOfPeriod($end, $viewRange); + $start = app('navigation')->startOfPeriod($budgetLimit->start_date, $viewRange); + $end = app('navigation')->startOfPeriod($budgetLimit->end_date, $viewRange); + $end = app('navigation')->endOfPeriod($end, $viewRange); // limit period in total is: $limitPeriod = Period::make($start, $end, precision: Precision::DAY(), boundaries: Boundaries::EXCLUDE_NONE()); Log::debug(sprintf('Limit period is from %s to %s', $start->format('Y-m-d'), $end->format('Y-m-d'))); // from the start until the end of the budget limit, need to loop! - $current = clone $start; + $current = clone $start; while ($current <= $end) { - $currentEnd = app('navigation')->endOfPeriod($current, $viewRange); + $currentEnd = app('navigation')->endOfPeriod($current, $viewRange); // create or find AB for this particular period, and set the amount accordingly. /** @var null|AvailableBudget $availableBudget */ @@ -227,7 +227,7 @@ trait RecalculatesAvailableBudgetsTrait } // prep for next loop - $current = app('navigation')->addPeriod($current, $viewRange, 0); + $current = app('navigation')->addPeriod($current, $viewRange, 0); } } } diff --git a/app/Support/ParseDateString.php b/app/Support/ParseDateString.php index bd91585f8b..df60be3d1d 100644 --- a/app/Support/ParseDateString.php +++ b/app/Support/ParseDateString.php @@ -29,6 +29,7 @@ use Carbon\CarbonInterface; use Carbon\Exceptions\InvalidFormatException; use FireflyIII\Exceptions\FireflyException; use Illuminate\Support\Facades\Log; + use function Safe\preg_match; /** @@ -78,15 +79,15 @@ class ParseDateString public function parseDate(string $date): Carbon { Log::debug(sprintf('parseDate("%s")', $date)); - $date = strtolower($date); + $date = strtolower($date); // parse keywords: if (in_array($date, $this->keywords, true)) { return $this->parseKeyword($date); } // if regex for YYYY-MM-DD: - $pattern = '/^(19|20)\d\d-(0[1-9]|1[012])-(0[1-9]|[12]\d|3[01])$/'; - $result = preg_match($pattern, $date); + $pattern = '/^(19|20)\d\d-(0[1-9]|1[012])-(0[1-9]|[12]\d|3[01])$/'; + $result = preg_match($pattern, $date); if (0 !== $result) { return $this->parseDefaultDate($date); } @@ -355,11 +356,11 @@ class ParseDateString foreach ($parts as $part) { Log::debug(sprintf('Now parsing part "%s"', $part)); - $part = trim($part); + $part = trim($part); // verify if correct - $pattern = '/[+-]\d+[wqmdy]/'; - $result = preg_match($pattern, $part); + $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)); @@ -373,7 +374,7 @@ class ParseDateString continue; } - $func = $functions[$direction][$period]; + $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'))); diff --git a/app/Support/Preferences.php b/app/Support/Preferences.php index f8fc423cc7..d622f1f66f 100644 --- a/app/Support/Preferences.php +++ b/app/Support/Preferences.php @@ -48,12 +48,13 @@ class Preferences } return Preference::where('user_id', $user->id) - ->where('name', '!=', 'currencyPreference') - ->where(function (Builder $q) use ($user): void { - $q->whereNull('user_group_id'); - $q->orWhere('user_group_id', $user->user_group_id); - }) - ->get(); + ->where('name', '!=', 'currencyPreference') + ->where(function (Builder $q) use ($user): void { + $q->whereNull('user_group_id'); + $q->orWhere('user_group_id', $user->user_group_id); + }) + ->get() + ; } public function beginsWith(User $user, string $search): Collection @@ -89,7 +90,7 @@ class Preferences Cache::put($key, '', 5); } - public function get(string $name, array | bool | int | string | null $default = null): ?Preference + public function get(string $name, array|bool|int|string|null $default = null): ?Preference { /** @var null|User $user */ $user = auth()->user(); @@ -107,12 +108,13 @@ class Preferences { $result = []; $preferences = Preference::where('user_id', $user->id) - ->where(function (Builder $q) use ($user): void { - $q->whereNull('user_group_id'); - $q->orWhere('user_group_id', $user->user_group_id); - }) - ->whereIn('name', $list) - ->get(['id', 'name', 'data']); + ->where(function (Builder $q) use ($user): void { + $q->whereNull('user_group_id'); + $q->orWhere('user_group_id', $user->user_group_id); + }) + ->whereIn('name', $list) + ->get(['id', 'name', 'data']) + ; /** @var Preference $preference */ foreach ($preferences as $preference) { @@ -154,7 +156,7 @@ class Preferences return $result; } - public function getEncryptedForUser(User $user, string $name, array | bool | int | string | null $default = null): ?Preference + public function getEncryptedForUser(User $user, string $name, array|bool|int|string|null $default = null): ?Preference { $result = $this->getForUser($user, $name, $default); if ('' === $result->data) { @@ -179,7 +181,7 @@ class Preferences return $result; } - public function getForUser(User $user, string $name, array | bool | int | string | null $default = null): ?Preference + 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. @@ -190,7 +192,7 @@ class Preferences $query->where('user_group_id', $userGroupId); } - $preference = $query->first(['id', 'user_id', 'user_group_id', 'name', 'data', 'updated_at', 'created_at']); + $preference = $query->first(['id', 'user_id', 'user_group_id', 'name', 'data', 'updated_at', 'created_at']); if (null !== $preference && null === $preference->data) { $preference->delete(); @@ -214,7 +216,7 @@ class Preferences return $this->setForUser($user, $name, $default); } - public function getFresh(string $name, array | bool | int | string | null $default = null): ?Preference + public function getFresh(string $name, array|bool|int|string|null $default = null): ?Preference { /** @var null|User $user */ $user = auth()->user(); @@ -233,8 +235,8 @@ class Preferences */ public function lastActivity(): string { - $instance = PreferencesSingleton::getInstance(); - $pref = $instance->getPreference('last_activity'); + $instance = PreferencesSingleton::getInstance(); + $pref = $instance->getPreference('last_activity'); if (null !== $pref) { // Log::debug(sprintf('Found last activity in singleton: %s', $pref)); return $pref; @@ -248,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; @@ -262,7 +264,7 @@ class Preferences Session::forget('first'); } - public function set(string $name, array | bool | int | string | null $value): Preference + public function set(string $name, array|bool|int|string|null $value): Preference { /** @var null|User $user */ $user = auth()->user(); @@ -291,21 +293,21 @@ class Preferences return $this->set($name, $encrypted); } - public function setForUser(User $user, string $name, array | bool | int | string | null $value): Preference + 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; + $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); + $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']); + $preference = $query->first(['id', 'user_id', 'user_group_id', 'name', 'data', 'updated_at', 'created_at']); if (null !== $preference && null === $value) { $preference->delete(); diff --git a/app/Support/Report/Budget/BudgetReportGenerator.php b/app/Support/Report/Budget/BudgetReportGenerator.php index b859847748..2fd81217bb 100644 --- a/app/Support/Report/Budget/BudgetReportGenerator.php +++ b/app/Support/Report/Budget/BudgetReportGenerator.php @@ -76,7 +76,7 @@ class BudgetReportGenerator /** @var Account $account */ foreach ($this->accounts as $account) { - $accountId = $account->id; + $accountId = $account->id; $this->report[$accountId] ??= [ 'name' => $account->name, 'id' => $account->id, @@ -170,16 +170,16 @@ class BudgetReportGenerator 'budget_limits' => [], ]; - $noBudget = $this->nbRepository->sumExpenses($this->start, $this->end, $this->accounts); + $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; + $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'][] = [ + $this->report['budgets'][0]['budget_limits'][] = [ 'budget_limit_id' => null, 'start_date' => $this->start, 'end_date' => $this->end, @@ -195,7 +195,7 @@ class BudgetReportGenerator 'currency_symbol' => $nbCurrencySymbol, 'currency_decimal_places' => $nbCurrencyDp, ]; - $this->report['sums'][$nbCurrencyId]['spent'] = bcadd($this->report['sums'][$nbCurrencyId]['spent'] ?? '0', (string)$noBudgetEntry['sum']); + $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; @@ -218,15 +218,15 @@ class BudgetReportGenerator // 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'; + $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')); @@ -234,7 +234,7 @@ class BudgetReportGenerator 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['sums'][$currencyId]['budgeted'] ??= '0'; $this->report['budgets'][$budgetId]['budget_limits'][$limitId]['spent_pct'] = $spentPct; $this->report['budgets'][$budgetId]['budget_limits'][$limitId]['budgeted_pct'] = $budgetedPct; } @@ -246,7 +246,7 @@ class BudgetReportGenerator */ private function processBudget(Budget $budget): void { - $budgetId = $budget->id; + $budgetId = $budget->id; $this->report['budgets'][$budgetId] ??= [ 'budget_id' => $budgetId, 'budget_name' => $budget->name, @@ -255,7 +255,7 @@ class BudgetReportGenerator ]; // get all budget limits for budget in period: - $limits = $this->blRepository->getBudgetLimits($budget, $this->start, $this->end); + $limits = $this->blRepository->getBudgetLimits($budget, $this->start, $this->end); /** @var BudgetLimit $limit */ foreach ($limits as $limit) { @@ -275,18 +275,18 @@ class BudgetReportGenerator $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' => [], - ]; + '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']); + = bcadd($this->report[$sourceAccountId]['currencies'][$currencyId]['budgets'][$budgetId], (string)$journal['amount']); } } @@ -305,14 +305,14 @@ class BudgetReportGenerator */ private function processLimit(Budget $budget, BudgetLimit $limit): void { - $budgetId = $budget->id; - $limitId = $limit->id; - $limitCurrency = $limit->transactionCurrency ?? $this->currency; - $currencyId = $limitCurrency->id; - $expenses = $this->opsRepository->sumExpenses($limit->start_date, $limit->end_date, $this->accounts, new Collection()->push($budget)); - $spent = $expenses[$currencyId]['sum'] ?? '0'; - $left = -1 === bccomp(bcadd($limit->amount, $spent), '0') ? '0' : bcadd($limit->amount, $spent); - $overspent = 1 === bccomp(bcmul($spent, '-1'), $limit->amount) ? bcadd($spent, $limit->amount) : '0'; + $budgetId = $budget->id; + $limitId = $limit->id; + $limitCurrency = $limit->transactionCurrency ?? $this->currency; + $currencyId = $limitCurrency->id; + $expenses = $this->opsRepository->sumExpenses($limit->start_date, $limit->end_date, $this->accounts, new Collection()->push($budget)); + $spent = $expenses[$currencyId]['sum'] ?? '0'; + $left = -1 === bccomp(bcadd($limit->amount, $spent), '0') ? '0' : bcadd($limit->amount, $spent); + $overspent = 1 === bccomp(bcmul($spent, '-1'), $limit->amount) ? bcadd($spent, $limit->amount) : '0'; $this->report['budgets'][$budgetId]['budget_limits'][$limitId] ??= [ 'budget_limit_id' => $limitId, @@ -333,17 +333,17 @@ class BudgetReportGenerator // make sum information: $this->report['sums'][$currencyId] - ??= [ - 'budgeted' => '0', - 'spent' => '0', - 'left' => '0', - 'overspent' => '0', - 'currency_id' => $currencyId, - 'currency_code' => $limitCurrency->code, - 'currency_name' => $limitCurrency->name, - 'currency_symbol' => $limitCurrency->symbol, - 'currency_decimal_places' => $limitCurrency->decimal_places, - ]; + ??= [ + 'budgeted' => '0', + 'spent' => '0', + 'left' => '0', + 'overspent' => '0', + 'currency_id' => $currencyId, + 'currency_code' => $limitCurrency->code, + 'currency_name' => $limitCurrency->name, + '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)); diff --git a/app/Support/Report/Category/CategoryReportGenerator.php b/app/Support/Report/Category/CategoryReportGenerator.php index 8f800d411d..51570e7696 100644 --- a/app/Support/Report/Category/CategoryReportGenerator.php +++ b/app/Support/Report/Category/CategoryReportGenerator.php @@ -62,17 +62,17 @@ class CategoryReportGenerator */ public function operations(): void { - $earnedWith = $this->opsRepository->listIncome($this->start, $this->end, $this->accounts); - $spentWith = $this->opsRepository->listExpenses($this->start, $this->end, $this->accounts); + $earnedWith = $this->opsRepository->listIncome($this->start, $this->end, $this->accounts); + $spentWith = $this->opsRepository->listExpenses($this->start, $this->end, $this->accounts); // also transferred out and transferred into these accounts in this category: $transferredIn = $this->opsRepository->listTransferredIn($this->start, $this->end, $this->accounts); $transferredOut = $this->opsRepository->listTransferredOut($this->start, $this->end, $this->accounts); - $earnedWithout = $this->noCatRepository->listIncome($this->start, $this->end, $this->accounts); - $spentWithout = $this->noCatRepository->listExpenses($this->start, $this->end, $this->accounts); + $earnedWithout = $this->noCatRepository->listIncome($this->start, $this->end, $this->accounts); + $spentWithout = $this->noCatRepository->listExpenses($this->start, $this->end, $this->accounts); - $this->report = [ + $this->report = [ 'categories' => [], 'sums' => [], ]; @@ -106,7 +106,7 @@ class CategoryReportGenerator private function processCategoryRow(int $currencyId, array $currencyRow, int $categoryId, array $categoryRow): void { - $key = sprintf('%s-%s', $currencyId, $categoryId); + $key = sprintf('%s-%s', $currencyId, $categoryId); $this->report['categories'][$key] ??= [ 'id' => $categoryId, 'title' => $categoryRow['name'], @@ -122,9 +122,9 @@ class CategoryReportGenerator // 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']); + $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( + $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']; @@ -135,14 +135,14 @@ class CategoryReportGenerator ) : $this->report['sums'][$currencyId]['earned']; // sum of category - $this->report['categories'][$key]['sum'] = bcadd((string)$this->report['categories'][$key]['sum'], (string)$journal['amount']); + $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( + $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( + $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']; diff --git a/app/Support/Report/Summarizer/TransactionSummarizer.php b/app/Support/Report/Summarizer/TransactionSummarizer.php index 84e0ec3231..5660a239f0 100644 --- a/app/Support/Report/Summarizer/TransactionSummarizer.php +++ b/app/Support/Report/Summarizer/TransactionSummarizer.php @@ -48,14 +48,14 @@ class TransactionSummarizer Log::debug(sprintf('Now in groupByCurrencyId([%d journals], "%s", %s)', count($journals), $method, var_export($includeForeign, true))); $array = []; foreach ($journals as $journal) { - $field = 'amount'; + $field = 'amount'; // grab default currency information. - $currencyId = (int)$journal['currency_id']; - $currencyName = $journal['currency_name']; - $currencySymbol = $journal['currency_symbol']; - $currencyCode = $journal['currency_code']; - $currencyDecimalPlaces = $journal['currency_decimal_places']; + $currencyId = (int)$journal['currency_id']; + $currencyName = $journal['currency_name']; + $currencySymbol = $journal['currency_symbol']; + $currencyCode = $journal['currency_code']; + $currencyDecimalPlaces = $journal['currency_decimal_places']; // prepare foreign currency info: $foreignCurrencyId = 0; @@ -102,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, @@ -121,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, @@ -179,7 +179,7 @@ class TransactionSummarizer if ($convertToPrimary && $journal['currency_id'] !== $primary->id && $primary->id === $journal['foreign_currency_id']) { $field = 'foreign_amount'; } - $key = sprintf('%s-%s', $journal[$idKey], $currencyId); + $key = sprintf('%s-%s', $journal[$idKey], $currencyId); // sum it all up or create a new array. $array[$key] ??= [ 'id' => $journal[$idKey], @@ -193,7 +193,7 @@ 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) @@ -201,7 +201,7 @@ class TransactionSummarizer 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] ??= [ + $array[$key] ??= [ 'id' => $journal[$idKey], 'name' => $journal[$nameKey], 'sum' => '0', diff --git a/app/Support/Repositories/Recurring/CalculateRangeOccurrences.php b/app/Support/Repositories/Recurring/CalculateRangeOccurrences.php index 4ca2c8c29e..df73a72f60 100644 --- a/app/Support/Repositories/Recurring/CalculateRangeOccurrences.php +++ b/app/Support/Repositories/Recurring/CalculateRangeOccurrences.php @@ -82,8 +82,8 @@ trait CalculateRangeOccurrences */ protected function getNdomInRange(Carbon $start, Carbon $end, int $skipMod, string $moment): array { - $return = []; - $attempts = 0; + $return = []; + $attempts = 0; $start->startOfMonth(); // this feels a bit like a cop out but why reinvent the wheel? $counters = [1 => 'first', 2 => 'second', 3 => 'third', 4 => 'fourth', 5 => 'fifth']; @@ -108,12 +108,12 @@ trait CalculateRangeOccurrences */ protected function getWeeklyInRange(Carbon $start, Carbon $end, int $skipMod, string $moment): array { - $return = []; - $attempts = 0; + $return = []; + $attempts = 0; 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: @@ -154,8 +154,8 @@ trait CalculateRangeOccurrences } // is $date between $start and $end? - $obj = clone $date; - $count = 0; + $obj = clone $date; + $count = 0; while ($obj <= $end && $obj >= $start && $count < 10) { if (0 === $attempts % $skipMod) { $return[] = clone $obj; diff --git a/app/Support/Repositories/Recurring/CalculateXOccurrences.php b/app/Support/Repositories/Recurring/CalculateXOccurrences.php index 602cb03d02..f31171810f 100644 --- a/app/Support/Repositories/Recurring/CalculateXOccurrences.php +++ b/app/Support/Repositories/Recurring/CalculateXOccurrences.php @@ -89,10 +89,10 @@ trait CalculateXOccurrences */ protected function getXNDomOccurrences(Carbon $date, int $count, int $skipMod, string $moment): array { - $return = []; - $total = 0; - $attempts = 0; - $mutator = clone $date; + $return = []; + $total = 0; + $attempts = 0; + $mutator = clone $date; $mutator->addDay(); // always assume today has passed. $mutator->startOfMonth(); // this feels a bit like a cop out but why reinvent the wheel? @@ -120,14 +120,14 @@ trait CalculateXOccurrences */ protected function getXWeeklyOccurrences(Carbon $date, int $count, int $skipMod, string $moment): array { - $return = []; - $total = 0; - $attempts = 0; - $mutator = clone $date; + $return = []; + $total = 0; + $attempts = 0; + $mutator = clone $date; // 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(); @@ -164,7 +164,7 @@ trait CalculateXOccurrences if ($mutator > $date) { $date->addYear(); } - $obj = clone $date; + $obj = clone $date; while ($total < $count) { if (0 === $attempts % $skipMod) { $return[] = clone $obj; diff --git a/app/Support/Repositories/Recurring/CalculateXOccurrencesSince.php b/app/Support/Repositories/Recurring/CalculateXOccurrencesSince.php index bd4b44fd7d..2fc216493d 100644 --- a/app/Support/Repositories/Recurring/CalculateXOccurrencesSince.php +++ b/app/Support/Repositories/Recurring/CalculateXOccurrencesSince.php @@ -87,7 +87,7 @@ trait CalculateXOccurrencesSince ++$total; } ++$attempts; - $mutator = $mutator->endOfMonth()->addDay(); + $mutator = $mutator->endOfMonth()->addDay(); } Log::debug('Collected enough occurrences.'); @@ -103,10 +103,10 @@ trait CalculateXOccurrencesSince protected function getXNDomOccurrencesSince(Carbon $date, Carbon $afterDate, int $count, int $skipMod, string $moment): array { Log::debug(sprintf('Now in %s', __METHOD__)); - $return = []; - $total = 0; - $attempts = 0; - $mutator = clone $date; + $return = []; + $total = 0; + $attempts = 0; + $mutator = clone $date; $mutator->addDay(); // always assume today has passed. $mutator->startOfMonth(); // this feels a bit like a cop out but why reinvent the wheel? @@ -137,15 +137,15 @@ trait CalculateXOccurrencesSince protected function getXWeeklyOccurrencesSince(Carbon $date, Carbon $afterDate, int $count, int $skipMod, string $moment): array { Log::debug(sprintf('Now in %s', __METHOD__)); - $return = []; - $total = 0; - $attempts = 0; - $mutator = clone $date; + $return = []; + $total = 0; + $attempts = 0; + $mutator = clone $date; // monday = 1 // 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(); @@ -189,7 +189,7 @@ trait CalculateXOccurrencesSince $date->addYear(); Log::debug(sprintf('Date is now %s', $date->format('Y-m-d'))); } - $obj = clone $date; + $obj = clone $date; while ($total < $count) { Log::debug(sprintf('total (%d) < count (%d) so go.', $total, $count)); Log::debug(sprintf('attempts (%d) %% skipmod (%d) === %d', $attempts, $skipMod, $attempts % $skipMod)); diff --git a/app/Support/Repositories/Recurring/FiltersWeekends.php b/app/Support/Repositories/Recurring/FiltersWeekends.php index 886679ced1..508e13f638 100644 --- a/app/Support/Repositories/Recurring/FiltersWeekends.php +++ b/app/Support/Repositories/Recurring/FiltersWeekends.php @@ -46,7 +46,7 @@ trait FiltersWeekends return $dates; } - $return = []; + $return = []; /** @var Carbon $date */ foreach ($dates as $date) { @@ -60,7 +60,7 @@ trait FiltersWeekends // is weekend and must set back to Friday? if (RecurrenceRepetitionWeekend::WEEKEND_TO_FRIDAY->value === $repetition->weekend) { - $clone = clone $date; + $clone = clone $date; $clone->addDays(5 - $date->dayOfWeekIso); Log::debug( sprintf('Date is %s, and this is in the weekend, so corrected to %s (Friday).', $date->format('D d M Y'), $clone->format('D d M Y')) @@ -72,7 +72,7 @@ trait FiltersWeekends // postpone to Monday? if (RecurrenceRepetitionWeekend::WEEKEND_TO_MONDAY->value === $repetition->weekend) { - $clone = clone $date; + $clone = clone $date; $clone->addDays(8 - $date->dayOfWeekIso); Log::debug( sprintf('Date is %s, and this is in the weekend, so corrected to %s (Monday).', $date->format('D d M Y'), $clone->format('D d M Y')) diff --git a/app/Support/Repositories/UserGroup/UserGroupInterface.php b/app/Support/Repositories/UserGroup/UserGroupInterface.php index 67e7fe3ea3..d7a737b919 100644 --- a/app/Support/Repositories/UserGroup/UserGroupInterface.php +++ b/app/Support/Repositories/UserGroup/UserGroupInterface.php @@ -37,7 +37,7 @@ interface UserGroupInterface public function getUserGroup(): ?UserGroup; - public function setUser(Authenticatable | User | null $user): void; + public function setUser(Authenticatable|User|null $user): void; public function setUserGroup(UserGroup $userGroup): void; diff --git a/app/Support/Repositories/UserGroup/UserGroupTrait.php b/app/Support/Repositories/UserGroup/UserGroupTrait.php index b6a1c94f5a..98781e5596 100644 --- a/app/Support/Repositories/UserGroup/UserGroupTrait.php +++ b/app/Support/Repositories/UserGroup/UserGroupTrait.php @@ -61,10 +61,10 @@ trait UserGroupTrait /** * @throws FireflyException */ - public function setUser(Authenticatable | User | null $user): void + public function setUser(Authenticatable|User|null $user): void { if ($user instanceof User) { - $this->user = $user; + $this->user = $user; if (null === $user->userGroup) { throw new FireflyException(sprintf('User #%d ("%s") has no user group.', $user->id, $user->email)); } @@ -99,14 +99,15 @@ trait UserGroupTrait public function setUserGroupById(int $userGroupId): void { $memberships = GroupMembership::where('user_id', $this->user->id) - ->where('user_group_id', $userGroupId) - ->count(); + ->where('user_group_id', $userGroupId) + ->count() + ; if (0 === $memberships) { throw new FireflyException(sprintf('User #%d has no access to administration #%d', $this->user->id, $userGroupId)); } /** @var null|UserGroup $userGroup */ - $userGroup = UserGroup::find($userGroupId); + $userGroup = UserGroup::find($userGroupId); if (null === $userGroup) { throw new FireflyException(sprintf('Cannot find administration for user #%d', $this->user->id)); } diff --git a/app/Support/Request/AppendsLocationData.php b/app/Support/Request/AppendsLocationData.php index 01a6de40d3..b77cd9ee41 100644 --- a/app/Support/Request/AppendsLocationData.php +++ b/app/Support/Request/AppendsLocationData.php @@ -96,12 +96,12 @@ trait AppendsLocationData $data['latitude'] = null; $data['zoom_level'] = null; - $longitudeKey = $this->getLocationKey($prefix, 'longitude'); - $latitudeKey = $this->getLocationKey($prefix, 'latitude'); - $zoomLevelKey = $this->getLocationKey($prefix, 'zoom_level'); - $isValidPOST = $this->isValidPost($prefix); - $isValidPUT = $this->isValidPUT($prefix); - $isValidEmptyPUT = $this->isValidEmptyPUT($prefix); + $longitudeKey = $this->getLocationKey($prefix, 'longitude'); + $latitudeKey = $this->getLocationKey($prefix, 'latitude'); + $zoomLevelKey = $this->getLocationKey($prefix, 'zoom_level'); + $isValidPOST = $this->isValidPost($prefix); + $isValidPUT = $this->isValidPUT($prefix); + $isValidEmptyPUT = $this->isValidEmptyPUT($prefix); // for a POST (store), all fields must be present and not NULL. if ($isValidPOST) { @@ -153,9 +153,9 @@ trait AppendsLocationData $zoomLevelKey = $this->getLocationKey($prefix, 'zoom_level'); return ( - null === $this->get($longitudeKey) - && null === $this->get($latitudeKey) - && null === $this->get($zoomLevelKey)) + null === $this->get($longitudeKey) + && null === $this->get($latitudeKey) + && null === $this->get($zoomLevelKey)) && ( 'PUT' === $this->method() || ('POST' === $this->method() && $this->routeIs('*.update')) diff --git a/app/Support/Request/ChecksLogin.php b/app/Support/Request/ChecksLogin.php index 8576b17473..0f9753ee62 100644 --- a/app/Support/Request/ChecksLogin.php +++ b/app/Support/Request/ChecksLogin.php @@ -40,7 +40,7 @@ trait ChecksLogin { app('log')->debug(sprintf('Now in %s', __METHOD__)); // Only allow logged-in users - $check = auth()->check(); + $check = auth()->check(); if (!$check) { return false; } @@ -79,7 +79,7 @@ trait ChecksLogin public function getUserGroup(): ?UserGroup { /** @var User $user */ - $user = auth()->user(); + $user = auth()->user(); app('log')->debug('Now in getUserGroup()'); /** @var null|UserGroup $userGroup */ @@ -91,7 +91,7 @@ trait ChecksLogin 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; } - $userGroup = UserGroup::find($userGroupId); + $userGroup = UserGroup::find($userGroupId); if (null === $userGroup) { app('log')->error(sprintf('Request class has user_group_id (#%d), but group does not exist.', $userGroupId)); diff --git a/app/Support/Request/ConvertsDataTypes.php b/app/Support/Request/ConvertsDataTypes.php index aa3896eb72..5293a6f804 100644 --- a/app/Support/Request/ConvertsDataTypes.php +++ b/app/Support/Request/ConvertsDataTypes.php @@ -31,6 +31,7 @@ use FireflyIII\Repositories\Account\AccountRepositoryInterface; use FireflyIII\Support\Facades\Steam; use Illuminate\Support\Collection; use Illuminate\Support\Facades\Log; + use function Safe\preg_replace; /** @@ -147,15 +148,15 @@ trait ConvertsDataTypes 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); + $parameter = (string)request()->query->get($field); if ('' === $parameter) { return []; } $parts = explode(',', $parameter); $sortParameters = []; foreach ($parts as $part) { - $part = trim($part); - $direction = 'asc'; + $part = trim($part); + $direction = 'asc'; if ('-' === $part[0]) { $part = substr($part, 1); $direction = 'desc'; @@ -459,7 +460,7 @@ trait ConvertsDataTypes if (!is_array($entry)) { continue; } - $amount = null; + $amount = null; if (array_key_exists('current_amount', $entry)) { $amount = $this->clearString((string)($entry['current_amount'] ?? '0')); if (null === $entry['current_amount']) { diff --git a/app/Support/Request/ValidatesWebhooks.php b/app/Support/Request/ValidatesWebhooks.php index dff1541fde..15d41f41ef 100644 --- a/app/Support/Request/ValidatesWebhooks.php +++ b/app/Support/Request/ValidatesWebhooks.php @@ -40,9 +40,9 @@ trait ValidatesWebhooks if (count($validator->failed()) > 0) { return; } - $data = $validator->getData(); - $triggers = $data['triggers'] ?? []; - $responses = $data['responses'] ?? []; + $data = $validator->getData(); + $triggers = $data['triggers'] ?? []; + $responses = $data['responses'] ?? []; if (0 === count($triggers) || 0 === count($responses)) { Log::debug('No trigger or response, return.'); diff --git a/app/Support/Search/AccountSearch.php b/app/Support/Search/AccountSearch.php index fe6e817c72..44bb34e893 100644 --- a/app/Support/Search/AccountSearch.php +++ b/app/Support/Search/AccountSearch.php @@ -28,6 +28,7 @@ use FireflyIII\User; use Illuminate\Contracts\Auth\Authenticatable; use Illuminate\Database\Eloquent\Builder; use Illuminate\Support\Collection; + use function Safe\json_encode; /** @@ -36,16 +37,16 @@ use function Safe\json_encode; class AccountSearch implements GenericSearchInterface { /** @var string */ - public const string SEARCH_ALL = 'all'; + public const string SEARCH_ALL = 'all'; /** @var string */ - public const string SEARCH_IBAN = 'iban'; + public const string SEARCH_IBAN = 'iban'; /** @var string */ - public const string SEARCH_ID = 'id'; + public const string SEARCH_ID = 'id'; /** @var string */ - public const string SEARCH_NAME = 'name'; + public const string SEARCH_NAME = 'name'; /** @var string */ public const string SEARCH_NUMBER = 'number'; @@ -62,9 +63,10 @@ class AccountSearch implements GenericSearchInterface public function search(): Collection { $searchQuery = $this->user->accounts() - ->leftJoin('account_types', 'accounts.account_type_id', '=', 'account_types.id') - ->leftJoin('account_meta', 'accounts.id', '=', 'account_meta.account_id') - ->whereIn('account_types.type', $this->types); + ->leftJoin('account_types', 'accounts.account_type_id', '=', 'account_types.id') + ->leftJoin('account_meta', 'accounts.id', '=', 'account_meta.account_id') + ->whereIn('account_types.type', $this->types) + ; $like = sprintf('%%%s%%', $this->query); $originalQuery = $this->query; @@ -135,7 +137,7 @@ class AccountSearch implements GenericSearchInterface $this->types = $types; } - public function setUser(Authenticatable | User | null $user): void + public function setUser(Authenticatable|User|null $user): void { if ($user instanceof User) { $this->user = $user; diff --git a/app/Support/Search/OperatorQuerySearch.php b/app/Support/Search/OperatorQuerySearch.php index 2e813bd125..5b2cc0776c 100644 --- a/app/Support/Search/OperatorQuerySearch.php +++ b/app/Support/Search/OperatorQuerySearch.php @@ -117,7 +117,7 @@ class OperatorQuerySearch implements SearchInterface $operator = substr($operator, 1); } - $config = config(sprintf('search.operators.%s', $operator)); + $config = config(sprintf('search.operators.%s', $operator)); if (null === $config) { throw new FireflyException(sprintf('No configuration for search operator "%s"', $operator)); } @@ -186,7 +186,7 @@ class OperatorQuerySearch implements SearchInterface try { $parsedQuery = $parser->parse($query); - } catch (LogicException | TypeError $e) { + } catch (LogicException|TypeError $e) { Log::error($e->getMessage()); Log::error(sprintf('Could not parse search: "%s".', $query)); @@ -278,7 +278,7 @@ class OperatorQuerySearch implements SearchInterface $value = $node->getValue(); $prohibited = $node->isProhibited($flipProhibitedFlag); - $context = config(sprintf('search.operators.%s.needs_context', $operator)); + $context = config(sprintf('search.operators.%s.needs_context', $operator)); // is an operator that needs no context, and value is false, then prohibited = true. if ('false' === $value && in_array($operator, $this->validOperators, true) && false === $context && !$prohibited) { @@ -292,14 +292,14 @@ class OperatorQuerySearch implements SearchInterface } // must be valid operator: - $inArray = in_array($operator, $this->validOperators, true); + $inArray = in_array($operator, $this->validOperators, true); if ($inArray) { if ($this->updateCollector($operator, $value, $prohibited)) { $this->operators->push([ - 'type' => self::getRootOperator($operator), - 'value' => $value, - 'prohibited' => $prohibited, - ]); + 'type' => self::getRootOperator($operator), + 'value' => $value, + 'prohibited' => $prohibited, + ]); Log::debug(sprintf('Added operator type "%s"', $operator)); } } @@ -355,7 +355,7 @@ class OperatorQuerySearch implements SearchInterface private function handleStringNode(StringNode $node, bool $flipProhibitedFlag): void { - $string = $node->getValue(); + $string = $node->getValue(); $prohibited = $node->isProhibited($flipProhibitedFlag); @@ -477,7 +477,7 @@ class OperatorQuerySearch implements SearchInterface } } // string position (default): starts with: - $stringMethod = 'str_starts_with'; + $stringMethod = 'str_starts_with'; // string position: ends with: if (StringPosition::ENDS === $stringPosition) { @@ -491,7 +491,7 @@ class OperatorQuerySearch implements SearchInterface } // get accounts: - $accounts = $this->accountRepository->searchAccount($value, $searchTypes, 1337); + $accounts = $this->accountRepository->searchAccount($value, $searchTypes, 1337); if (0 === $accounts->count() && false === $prohibited) { Log::warning('Found zero accounts, search for non existing account, NO results will be returned.'); $this->collector->findNothing(); @@ -504,8 +504,8 @@ class OperatorQuerySearch implements SearchInterface return; } Log::debug(sprintf('Found %d accounts, will filter.', $accounts->count())); - $filtered = $accounts->filter( - static fn(Account $account) => $stringMethod(strtolower($account->name), strtolower($value)) + $filtered = $accounts->filter( + static fn (Account $account) => $stringMethod(strtolower($account->name), strtolower($value)) ); if (0 === $filtered->count()) { @@ -557,7 +557,7 @@ class OperatorQuerySearch implements SearchInterface } // string position (default): starts with: - $stringMethod = 'str_starts_with'; + $stringMethod = 'str_starts_with'; // string position: ends with: if (StringPosition::ENDS === $stringPosition) { @@ -571,7 +571,7 @@ class OperatorQuerySearch implements SearchInterface } // search for accounts: - $accounts = $this->accountRepository->searchAccountNr($value, $searchTypes, 1337); + $accounts = $this->accountRepository->searchAccountNr($value, $searchTypes, 1337); if (0 === $accounts->count()) { Log::debug('Found zero accounts, search for invalid account.'); Log::warning('Call to findNothing() from searchAccountNr().'); @@ -582,7 +582,7 @@ class OperatorQuerySearch implements SearchInterface // if found, do filter Log::debug(sprintf('Found %d accounts, will filter.', $accounts->count())); - $filtered = $accounts->filter( + $filtered = $accounts->filter( static function (Account $account) use ($value, $stringMethod) { // either IBAN or account number $ibanMatch = $stringMethod(strtolower((string)$account->iban), strtolower($value)); @@ -1250,15 +1250,15 @@ class OperatorQuerySearch implements SearchInterface throw new FireflyException(sprintf('Unsupported search operator: "%s"', $operator)); - // some search operators are ignored, basically: + // some search operators are ignored, basically: case 'user_action': Log::info(sprintf('Ignore search operator "%s"', $operator)); return false; - // - // all account related searches: - // + // + // all account related searches: + // case 'account_is': $this->searchAccount($value, SearchDirection::BOTH, StringPosition::IS); @@ -1420,7 +1420,7 @@ class OperatorQuerySearch implements SearchInterface break; case 'source_account_id': - $account = $this->accountRepository->find((int)$value); + $account = $this->accountRepository->find((int)$value); if (null !== $account) { $this->collector->setSourceAccounts(new Collection()->push($account)); } @@ -1433,7 +1433,7 @@ class OperatorQuerySearch implements SearchInterface break; case '-source_account_id': - $account = $this->accountRepository->find((int)$value); + $account = $this->accountRepository->find((int)$value); if (null !== $account) { $this->collector->excludeSourceAccounts(new Collection()->push($account)); } @@ -1446,25 +1446,25 @@ class OperatorQuerySearch implements SearchInterface break; case 'journal_id': - $parts = explode(',', $value); + $parts = explode(',', $value); $this->collector->setJournalIds($parts); break; case '-journal_id': - $parts = explode(',', $value); + $parts = explode(',', $value); $this->collector->excludeJournalIds($parts); break; case 'id': - $parts = explode(',', $value); + $parts = explode(',', $value); $this->collector->setIds($parts); break; case '-id': - $parts = explode(',', $value); + $parts = explode(',', $value); $this->collector->excludeIds($parts); break; @@ -1550,7 +1550,7 @@ class OperatorQuerySearch implements SearchInterface break; case 'destination_account_id': - $account = $this->accountRepository->find((int)$value); + $account = $this->accountRepository->find((int)$value); if (null !== $account) { $this->collector->setDestinationAccounts(new Collection()->push($account)); } @@ -1562,7 +1562,7 @@ class OperatorQuerySearch implements SearchInterface break; case '-destination_account_id': - $account = $this->accountRepository->find((int)$value); + $account = $this->accountRepository->find((int)$value); if (null !== $account) { $this->collector->excludeDestinationAccounts(new Collection()->push($account)); } @@ -1575,12 +1575,12 @@ class OperatorQuerySearch implements SearchInterface case 'account_id': Log::debug(sprintf('Now in "account_id" with value "%s"', $value)); - $parts = explode(',', $value); - $collection = new Collection(); + $parts = explode(',', $value); + $collection = new Collection(); foreach ($parts as $accountId) { $accountId = (int)$accountId; Log::debug(sprintf('Searching for account with ID #%d', $accountId)); - $account = $this->accountRepository->find($accountId); + $account = $this->accountRepository->find($accountId); if (null !== $account) { Log::debug(sprintf('Found account with ID #%d ("%s")', $accountId, $account->name)); $collection->push($account); @@ -1601,8 +1601,8 @@ class OperatorQuerySearch implements SearchInterface break; case '-account_id': - $parts = explode(',', $value); - $collection = new Collection(); + $parts = explode(',', $value); + $collection = new Collection(); foreach ($parts as $accountId) { $account = $this->accountRepository->find((int)$accountId); if (null !== $account) { @@ -1619,48 +1619,48 @@ class OperatorQuerySearch implements SearchInterface break; - // - // cash account - // + // + // cash account + // case 'source_is_cash': - $account = $this->getCashAccount(); + $account = $this->getCashAccount(); $this->collector->setSourceAccounts(new Collection()->push($account)); break; case '-source_is_cash': - $account = $this->getCashAccount(); + $account = $this->getCashAccount(); $this->collector->excludeSourceAccounts(new Collection()->push($account)); break; case 'destination_is_cash': - $account = $this->getCashAccount(); + $account = $this->getCashAccount(); $this->collector->setDestinationAccounts(new Collection()->push($account)); break; case '-destination_is_cash': - $account = $this->getCashAccount(); + $account = $this->getCashAccount(); $this->collector->excludeDestinationAccounts(new Collection()->push($account)); break; case 'account_is_cash': - $account = $this->getCashAccount(); + $account = $this->getCashAccount(); $this->collector->setAccounts(new Collection()->push($account)); break; case '-account_is_cash': - $account = $this->getCashAccount(); + $account = $this->getCashAccount(); $this->collector->excludeAccounts(new Collection()->push($account)); break; - // - // description - // + // + // description + // case 'description_starts': $this->collector->descriptionStarts([$value]); @@ -1682,7 +1682,7 @@ class OperatorQuerySearch implements SearchInterface break; case 'description_contains': - $this->words[] = $value; + $this->words[] = $value; return false; @@ -1701,11 +1701,11 @@ class OperatorQuerySearch implements SearchInterface break; - // - // currency - // + // + // currency + // case 'currency_is': - $currency = $this->findCurrency($value); + $currency = $this->findCurrency($value); if ($currency instanceof TransactionCurrency) { $this->collector->setCurrency($currency); } @@ -1717,7 +1717,7 @@ class OperatorQuerySearch implements SearchInterface break; case '-currency_is': - $currency = $this->findCurrency($value); + $currency = $this->findCurrency($value); if ($currency instanceof TransactionCurrency) { $this->collector->excludeCurrency($currency); } @@ -1729,7 +1729,7 @@ class OperatorQuerySearch implements SearchInterface break; case 'foreign_currency_is': - $currency = $this->findCurrency($value); + $currency = $this->findCurrency($value); if ($currency instanceof TransactionCurrency) { $this->collector->setForeignCurrency($currency); } @@ -1741,7 +1741,7 @@ class OperatorQuerySearch implements SearchInterface break; case '-foreign_currency_is': - $currency = $this->findCurrency($value); + $currency = $this->findCurrency($value); if ($currency instanceof TransactionCurrency) { $this->collector->excludeForeignCurrency($currency); } @@ -1752,9 +1752,9 @@ class OperatorQuerySearch implements SearchInterface break; - // - // attachments - // + // + // attachments + // case 'has_attachments': case '-has_no_attachments': Log::debug('Set collector to filter on attachments.'); @@ -1769,8 +1769,8 @@ class OperatorQuerySearch implements SearchInterface break; - // - // categories + // + // categories case '-has_any_category': case 'has_no_category': $this->collector->withoutCategory(); @@ -1784,7 +1784,7 @@ class OperatorQuerySearch implements SearchInterface break; case 'category_is': - $category = $this->categoryRepository->findByName($value); + $category = $this->categoryRepository->findByName($value); if (null !== $category) { $this->collector->setCategory($category); @@ -1796,7 +1796,7 @@ class OperatorQuerySearch implements SearchInterface break; case '-category_is': - $category = $this->categoryRepository->findByName($value); + $category = $this->categoryRepository->findByName($value); if (null !== $category) { $this->collector->excludeCategory($category); @@ -1806,7 +1806,7 @@ class OperatorQuerySearch implements SearchInterface break; case 'category_ends': - $result = $this->categoryRepository->categoryEndsWith($value, 1337); + $result = $this->categoryRepository->categoryEndsWith($value, 1337); if ($result->count() > 0) { $this->collector->setCategories($result); } @@ -1818,7 +1818,7 @@ class OperatorQuerySearch implements SearchInterface break; case '-category_ends': - $result = $this->categoryRepository->categoryEndsWith($value, 1337); + $result = $this->categoryRepository->categoryEndsWith($value, 1337); if ($result->count() > 0) { $this->collector->excludeCategories($result); } @@ -1830,7 +1830,7 @@ class OperatorQuerySearch implements SearchInterface break; case 'category_starts': - $result = $this->categoryRepository->categoryStartsWith($value, 1337); + $result = $this->categoryRepository->categoryStartsWith($value, 1337); if ($result->count() > 0) { $this->collector->setCategories($result); } @@ -1842,7 +1842,7 @@ class OperatorQuerySearch implements SearchInterface break; case '-category_starts': - $result = $this->categoryRepository->categoryStartsWith($value, 1337); + $result = $this->categoryRepository->categoryStartsWith($value, 1337); if ($result->count() > 0) { $this->collector->excludeCategories($result); } @@ -1854,7 +1854,7 @@ class OperatorQuerySearch implements SearchInterface break; case 'category_contains': - $result = $this->categoryRepository->searchCategory($value, 1337); + $result = $this->categoryRepository->searchCategory($value, 1337); if ($result->count() > 0) { $this->collector->setCategories($result); } @@ -1866,7 +1866,7 @@ class OperatorQuerySearch implements SearchInterface break; case '-category_contains': - $result = $this->categoryRepository->searchCategory($value, 1337); + $result = $this->categoryRepository->searchCategory($value, 1337); if ($result->count() > 0) { $this->collector->excludeCategories($result); } @@ -1877,9 +1877,9 @@ class OperatorQuerySearch implements SearchInterface break; - // - // budgets - // + // + // budgets + // case '-has_any_budget': case 'has_no_budget': $this->collector->withoutBudget(); @@ -1893,7 +1893,7 @@ class OperatorQuerySearch implements SearchInterface break; case 'budget_contains': - $result = $this->budgetRepository->searchBudget($value, 1337); + $result = $this->budgetRepository->searchBudget($value, 1337); if ($result->count() > 0) { $this->collector->setBudgets($result); } @@ -1905,7 +1905,7 @@ class OperatorQuerySearch implements SearchInterface break; case '-budget_contains': - $result = $this->budgetRepository->searchBudget($value, 1337); + $result = $this->budgetRepository->searchBudget($value, 1337); if ($result->count() > 0) { $this->collector->excludeBudgets($result); } @@ -1917,7 +1917,7 @@ class OperatorQuerySearch implements SearchInterface break; case 'budget_is': - $budget = $this->budgetRepository->findByName($value); + $budget = $this->budgetRepository->findByName($value); if (null !== $budget) { $this->collector->setBudget($budget); @@ -1929,7 +1929,7 @@ class OperatorQuerySearch implements SearchInterface break; case '-budget_is': - $budget = $this->budgetRepository->findByName($value); + $budget = $this->budgetRepository->findByName($value); if (null !== $budget) { $this->collector->excludeBudget($budget); @@ -1941,7 +1941,7 @@ class OperatorQuerySearch implements SearchInterface break; case 'budget_ends': - $result = $this->budgetRepository->budgetEndsWith($value, 1337); + $result = $this->budgetRepository->budgetEndsWith($value, 1337); if ($result->count() > 0) { $this->collector->setBudgets($result); } @@ -1953,7 +1953,7 @@ class OperatorQuerySearch implements SearchInterface break; case '-budget_ends': - $result = $this->budgetRepository->budgetEndsWith($value, 1337); + $result = $this->budgetRepository->budgetEndsWith($value, 1337); if ($result->count() > 0) { $this->collector->excludeBudgets($result); } @@ -1965,7 +1965,7 @@ class OperatorQuerySearch implements SearchInterface break; case 'budget_starts': - $result = $this->budgetRepository->budgetStartsWith($value, 1337); + $result = $this->budgetRepository->budgetStartsWith($value, 1337); if ($result->count() > 0) { $this->collector->setBudgets($result); } @@ -1977,7 +1977,7 @@ class OperatorQuerySearch implements SearchInterface break; case '-budget_starts': - $result = $this->budgetRepository->budgetStartsWith($value, 1337); + $result = $this->budgetRepository->budgetStartsWith($value, 1337); if ($result->count() > 0) { $this->collector->excludeBudgets($result); } @@ -1988,9 +1988,9 @@ class OperatorQuerySearch implements SearchInterface break; - // - // bill - // + // + // bill + // case '-has_any_bill': case 'has_no_bill': $this->collector->withoutBill(); @@ -2004,7 +2004,7 @@ class OperatorQuerySearch implements SearchInterface break; case 'bill_contains': - $result = $this->billRepository->searchBill($value, 1337); + $result = $this->billRepository->searchBill($value, 1337); if ($result->count() > 0) { $this->collector->setBills($result); @@ -2016,7 +2016,7 @@ class OperatorQuerySearch implements SearchInterface break; case '-bill_contains': - $result = $this->billRepository->searchBill($value, 1337); + $result = $this->billRepository->searchBill($value, 1337); if ($result->count() > 0) { $this->collector->excludeBills($result); @@ -2028,7 +2028,7 @@ class OperatorQuerySearch implements SearchInterface break; case 'bill_is': - $bill = $this->billRepository->findByName($value); + $bill = $this->billRepository->findByName($value); if (null !== $bill) { $this->collector->setBill($bill); @@ -2040,7 +2040,7 @@ class OperatorQuerySearch implements SearchInterface break; case '-bill_is': - $bill = $this->billRepository->findByName($value); + $bill = $this->billRepository->findByName($value); if (null !== $bill) { $this->collector->excludeBills(new Collection()->push($bill)); @@ -2052,7 +2052,7 @@ class OperatorQuerySearch implements SearchInterface break; case 'bill_ends': - $result = $this->billRepository->billEndsWith($value, 1337); + $result = $this->billRepository->billEndsWith($value, 1337); if ($result->count() > 0) { $this->collector->setBills($result); } @@ -2064,7 +2064,7 @@ class OperatorQuerySearch implements SearchInterface break; case '-bill_ends': - $result = $this->billRepository->billEndsWith($value, 1337); + $result = $this->billRepository->billEndsWith($value, 1337); if ($result->count() > 0) { $this->collector->excludeBills($result); } @@ -2076,7 +2076,7 @@ class OperatorQuerySearch implements SearchInterface break; case 'bill_starts': - $result = $this->billRepository->billStartsWith($value, 1337); + $result = $this->billRepository->billStartsWith($value, 1337); if ($result->count() > 0) { $this->collector->setBills($result); } @@ -2088,7 +2088,7 @@ class OperatorQuerySearch implements SearchInterface break; case '-bill_starts': - $result = $this->billRepository->billStartsWith($value, 1337); + $result = $this->billRepository->billStartsWith($value, 1337); if ($result->count() > 0) { $this->collector->excludeBills($result); } @@ -2099,9 +2099,9 @@ class OperatorQuerySearch implements SearchInterface break; - // - // tags - // + // + // tags + // case '-has_any_tag': case 'has_no_tag': $this->collector->withoutTags(); @@ -2116,7 +2116,7 @@ class OperatorQuerySearch implements SearchInterface case '-tag_is_not': case 'tag_is': - $result = $this->tagRepository->findByTag($value); + $result = $this->tagRepository->findByTag($value); if (null !== $result) { $this->includeTags[] = $result->id; $this->includeTags = array_unique($this->includeTags); @@ -2131,7 +2131,7 @@ class OperatorQuerySearch implements SearchInterface break; case 'tag_contains': - $tags = $this->tagRepository->searchTag($value); + $tags = $this->tagRepository->searchTag($value); if (0 === $tags->count()) { Log::info(sprintf('No valid tags in "%s"-operator, so search will not return ANY results.', $operator)); Log::warning(sprintf('Call to findNothing() from %s.', $operator)); @@ -2146,7 +2146,7 @@ class OperatorQuerySearch implements SearchInterface break; case 'tag_starts': - $tags = $this->tagRepository->tagStartsWith($value); + $tags = $this->tagRepository->tagStartsWith($value); if (0 === $tags->count()) { Log::info(sprintf('No valid tags in "%s"-operator, so search will not return ANY results.', $operator)); Log::warning(sprintf('Call to findNothing() from %s.', $operator)); @@ -2161,7 +2161,7 @@ class OperatorQuerySearch implements SearchInterface break; case '-tag_starts': - $tags = $this->tagRepository->tagStartsWith($value); + $tags = $this->tagRepository->tagStartsWith($value); if (0 === $tags->count()) { Log::info(sprintf('No valid tags in "%s"-operator, so search will not return ANY results.', $operator)); Log::warning(sprintf('Call to findNothing() from %s.', $operator)); @@ -2175,7 +2175,7 @@ class OperatorQuerySearch implements SearchInterface break; case 'tag_ends': - $tags = $this->tagRepository->tagEndsWith($value); + $tags = $this->tagRepository->tagEndsWith($value); if (0 === $tags->count()) { Log::info(sprintf('No valid tags in "%s"-operator, so search will not return ANY results.', $operator)); Log::warning(sprintf('Call to findNothing() from %s.', $operator)); @@ -2189,7 +2189,7 @@ class OperatorQuerySearch implements SearchInterface break; case '-tag_ends': - $tags = $this->tagRepository->tagEndsWith($value); + $tags = $this->tagRepository->tagEndsWith($value); if (0 === $tags->count()) { Log::info(sprintf('No valid tags in "%s"-operator, so search will not return ANY results.', $operator)); Log::warning(sprintf('Call to findNothing() from %s.', $operator)); @@ -2203,7 +2203,7 @@ class OperatorQuerySearch implements SearchInterface break; case '-tag_contains': - $tags = $this->tagRepository->searchTag($value)->keyBy('id'); + $tags = $this->tagRepository->searchTag($value)->keyBy('id'); if (0 === $tags->count()) { Log::info(sprintf('No valid tags in "%s"-operator, so search will not return ANY results.', $operator)); @@ -2219,7 +2219,7 @@ class OperatorQuerySearch implements SearchInterface case '-tag_is': case 'tag_is_not': - $result = $this->tagRepository->findByTag($value); + $result = $this->tagRepository->findByTag($value); if (null !== $result) { $this->excludeTags[] = $result->id; $this->excludeTags = array_unique($this->excludeTags); @@ -2227,9 +2227,9 @@ class OperatorQuerySearch implements SearchInterface break; - // - // notes - // + // + // notes + // case 'notes_contains': $this->collector->notesContain($value); @@ -2292,14 +2292,14 @@ class OperatorQuerySearch implements SearchInterface break; - // - // amount - // + // + // amount + // case 'amount_is': // strip comma's, make dots. Log::debug(sprintf('Original value "%s"', $value)); - $value = str_replace(',', '.', $value); - $amount = app('steam')->positive($value); + $value = str_replace(',', '.', $value); + $amount = app('steam')->positive($value); Log::debug(sprintf('Set "%s" using collector with value "%s"', $operator, $amount)); $this->collector->amountIs($amount); @@ -2308,8 +2308,8 @@ class OperatorQuerySearch implements SearchInterface case '-amount_is': // strip comma's, make dots. Log::debug(sprintf('Original value "%s"', $value)); - $value = str_replace(',', '.', $value); - $amount = app('steam')->positive($value); + $value = str_replace(',', '.', $value); + $amount = app('steam')->positive($value); Log::debug(sprintf('Set "%s" using collector with value "%s"', $operator, $amount)); $this->collector->amountIsNot($amount); @@ -2317,9 +2317,9 @@ class OperatorQuerySearch implements SearchInterface case 'foreign_amount_is': // strip comma's, make dots. - $value = str_replace(',', '.', $value); + $value = str_replace(',', '.', $value); - $amount = app('steam')->positive($value); + $amount = app('steam')->positive($value); Log::debug(sprintf('Set "%s" using collector with value "%s"', $operator, $amount)); $this->collector->foreignAmountIs($amount); @@ -2327,9 +2327,9 @@ class OperatorQuerySearch implements SearchInterface case '-foreign_amount_is': // strip comma's, make dots. - $value = str_replace(',', '.', $value); + $value = str_replace(',', '.', $value); - $amount = app('steam')->positive($value); + $amount = app('steam')->positive($value); Log::debug(sprintf('Set "%s" using collector with value "%s"', $operator, $amount)); $this->collector->foreignAmountIsNot($amount); @@ -2338,9 +2338,9 @@ class OperatorQuerySearch implements SearchInterface case '-amount_more': case 'amount_less': // strip comma's, make dots. - $value = str_replace(',', '.', $value); + $value = str_replace(',', '.', $value); - $amount = app('steam')->positive($value); + $amount = app('steam')->positive($value); Log::debug(sprintf('Set "%s" using collector with value "%s"', $operator, $amount)); $this->collector->amountLess($amount); @@ -2349,9 +2349,9 @@ class OperatorQuerySearch implements SearchInterface case '-foreign_amount_more': case 'foreign_amount_less': // strip comma's, make dots. - $value = str_replace(',', '.', $value); + $value = str_replace(',', '.', $value); - $amount = app('steam')->positive($value); + $amount = app('steam')->positive($value); Log::debug(sprintf('Set "%s" using collector with value "%s"', $operator, $amount)); $this->collector->foreignAmountLess($amount); @@ -2361,8 +2361,8 @@ class OperatorQuerySearch implements SearchInterface case 'amount_more': Log::debug(sprintf('Now handling operator "%s"', $operator)); // strip comma's, make dots. - $value = str_replace(',', '.', $value); - $amount = app('steam')->positive($value); + $value = str_replace(',', '.', $value); + $amount = app('steam')->positive($value); Log::debug(sprintf('Set "%s" using collector with value "%s"', $operator, $amount)); $this->collector->amountMore($amount); @@ -2372,16 +2372,16 @@ class OperatorQuerySearch implements SearchInterface case 'foreign_amount_more': Log::debug(sprintf('Now handling operator "%s"', $operator)); // strip comma's, make dots. - $value = str_replace(',', '.', $value); - $amount = app('steam')->positive($value); + $value = str_replace(',', '.', $value); + $amount = app('steam')->positive($value); Log::debug(sprintf('Set "%s" using collector with value "%s"', $operator, $amount)); $this->collector->foreignAmountMore($amount); break; - // - // transaction type - // + // + // transaction type + // case 'transaction_type': $this->collector->setTypes([ucfirst($value)]); Log::debug(sprintf('Set "%s" using collector with value "%s"', $operator, $value)); @@ -2394,152 +2394,152 @@ class OperatorQuerySearch implements SearchInterface break; - // - // dates - // + // + // dates + // case '-date_on': case 'date_on': - $range = $this->parseDateRange($operator, $value); + $range = $this->parseDateRange($operator, $value); $this->setExactDateParams($range, $prohibited); return false; case 'date_before': case '-date_after': - $range = $this->parseDateRange($operator, $value); + $range = $this->parseDateRange($operator, $value); $this->setDateBeforeParams($range); return false; case 'date_after': case '-date_before': - $range = $this->parseDateRange($operator, $value); + $range = $this->parseDateRange($operator, $value); $this->setDateAfterParams($range); return false; case 'interest_date_on': case '-interest_date_on': - $range = $this->parseDateRange($operator, $value); + $range = $this->parseDateRange($operator, $value); $this->setExactMetaDateParams('interest_date', $range, $prohibited); return false; case 'interest_date_before': case '-interest_date_after': - $range = $this->parseDateRange($operator, $value); + $range = $this->parseDateRange($operator, $value); $this->setMetaDateBeforeParams('interest_date', $range); return false; case 'interest_date_after': case '-interest_date_before': - $range = $this->parseDateRange($operator, $value); + $range = $this->parseDateRange($operator, $value); $this->setMetaDateAfterParams('interest_date', $range); return false; case 'book_date_on': case '-book_date_on': - $range = $this->parseDateRange($operator, $value); + $range = $this->parseDateRange($operator, $value); $this->setExactMetaDateParams('book_date', $range, $prohibited); return false; case 'book_date_before': case '-book_date_after': - $range = $this->parseDateRange($operator, $value); + $range = $this->parseDateRange($operator, $value); $this->setMetaDateBeforeParams('book_date', $range); return false; case 'book_date_after': case '-book_date_before': - $range = $this->parseDateRange($operator, $value); + $range = $this->parseDateRange($operator, $value); $this->setMetaDateAfterParams('book_date', $range); return false; case 'process_date_on': case '-process_date_on': - $range = $this->parseDateRange($operator, $value); + $range = $this->parseDateRange($operator, $value); $this->setExactMetaDateParams('process_date', $range, $prohibited); return false; case 'process_date_before': case '-process_date_after': - $range = $this->parseDateRange($operator, $value); + $range = $this->parseDateRange($operator, $value); $this->setMetaDateBeforeParams('process_date', $range); return false; case 'process_date_after': case '-process_date_before': - $range = $this->parseDateRange($operator, $value); + $range = $this->parseDateRange($operator, $value); $this->setMetaDateAfterParams('process_date', $range); return false; case 'due_date_on': case '-due_date_on': - $range = $this->parseDateRange($operator, $value); + $range = $this->parseDateRange($operator, $value); $this->setExactMetaDateParams('due_date', $range, $prohibited); return false; case 'due_date_before': case '-due_date_after': - $range = $this->parseDateRange($operator, $value); + $range = $this->parseDateRange($operator, $value); $this->setMetaDateBeforeParams('due_date', $range); return false; case 'due_date_after': case '-due_date_before': - $range = $this->parseDateRange($operator, $value); + $range = $this->parseDateRange($operator, $value); $this->setMetaDateAfterParams('due_date', $range); return false; case 'payment_date_on': case '-payment_date_on': - $range = $this->parseDateRange($operator, $value); + $range = $this->parseDateRange($operator, $value); $this->setExactMetaDateParams('payment_date', $range, $prohibited); return false; case 'payment_date_before': case '-payment_date_after': - $range = $this->parseDateRange($operator, $value); + $range = $this->parseDateRange($operator, $value); $this->setMetaDateBeforeParams('payment_date', $range); return false; case 'payment_date_after': case '-payment_date_before': - $range = $this->parseDateRange($operator, $value); + $range = $this->parseDateRange($operator, $value); $this->setMetaDateAfterParams('payment_date', $range); return false; case 'invoice_date_on': case '-invoice_date_on': - $range = $this->parseDateRange($operator, $value); + $range = $this->parseDateRange($operator, $value); $this->setExactMetaDateParams('invoice_date', $range, $prohibited); return false; case 'invoice_date_before': case '-invoice_date_after': - $range = $this->parseDateRange($operator, $value); + $range = $this->parseDateRange($operator, $value); $this->setMetaDateBeforeParams('invoice_date', $range); return false; case 'invoice_date_after': case '-invoice_date_before': - $range = $this->parseDateRange($operator, $value); + $range = $this->parseDateRange($operator, $value); $this->setMetaDateAfterParams('invoice_date', $range); return false; @@ -2547,7 +2547,7 @@ class OperatorQuerySearch implements SearchInterface case 'created_at_on': case '-created_at_on': Log::debug(sprintf('Set "%s" using collector with value "%s"', $operator, $value)); - $range = $this->parseDateRange($operator, $value); + $range = $this->parseDateRange($operator, $value); $this->setExactObjectDateParams('created_at', $range, $prohibited); return false; @@ -2555,7 +2555,7 @@ class OperatorQuerySearch implements SearchInterface case 'created_at_before': case '-created_at_after': Log::debug(sprintf('Set "%s" using collector with value "%s"', $operator, $value)); - $range = $this->parseDateRange($operator, $value); + $range = $this->parseDateRange($operator, $value); $this->setObjectDateBeforeParams('created_at', $range); return false; @@ -2563,7 +2563,7 @@ class OperatorQuerySearch implements SearchInterface case 'created_at_after': case '-created_at_before': Log::debug(sprintf('Set "%s" using collector with value "%s"', $operator, $value)); - $range = $this->parseDateRange($operator, $value); + $range = $this->parseDateRange($operator, $value); $this->setObjectDateAfterParams('created_at', $range); return false; @@ -2571,7 +2571,7 @@ class OperatorQuerySearch implements SearchInterface case 'updated_at_on': case '-updated_at_on': Log::debug(sprintf('Set "%s" using collector with value "%s"', $operator, $value)); - $range = $this->parseDateRange($operator, $value); + $range = $this->parseDateRange($operator, $value); $this->setExactObjectDateParams('updated_at', $range, $prohibited); return false; @@ -2579,7 +2579,7 @@ class OperatorQuerySearch implements SearchInterface case 'updated_at_before': case '-updated_at_after': Log::debug(sprintf('Set "%s" using collector with value "%s"', $operator, $value)); - $range = $this->parseDateRange($operator, $value); + $range = $this->parseDateRange($operator, $value); $this->setObjectDateBeforeParams('updated_at', $range); return false; @@ -2587,14 +2587,14 @@ class OperatorQuerySearch implements SearchInterface case 'updated_at_after': case '-updated_at_before': Log::debug(sprintf('Set "%s" using collector with value "%s"', $operator, $value)); - $range = $this->parseDateRange($operator, $value); + $range = $this->parseDateRange($operator, $value); $this->setObjectDateAfterParams('updated_at', $range); return false; - // - // external URL - // + // + // external URL + // case '-any_external_url': case 'no_external_url': $this->collector->withoutExternalUrl(); @@ -2659,9 +2659,9 @@ class OperatorQuerySearch implements SearchInterface break; - // - // other fields - // + // + // other fields + // case 'external_id_is': $this->collector->setExternalId($value); diff --git a/app/Support/Search/QueryParser/GdbotsQueryParser.php b/app/Support/Search/QueryParser/GdbotsQueryParser.php index 0e670a21df..e532901696 100644 --- a/app/Support/Search/QueryParser/GdbotsQueryParser.php +++ b/app/Support/Search/QueryParser/GdbotsQueryParser.php @@ -32,6 +32,7 @@ use Gdbots\QueryParser\QueryParser as BaseQueryParser; use Illuminate\Support\Facades\Log; use LogicException; use TypeError; + use function Safe\fwrite; class GdbotsQueryParser implements QueryParserInterface @@ -51,12 +52,12 @@ class GdbotsQueryParser implements QueryParserInterface try { $result = $this->parser->parse($query); $nodes = array_map( - fn(GdbotsNode\Node $node) => $this->convertNode($node), + fn (GdbotsNode\Node $node) => $this->convertNode($node), $result->getNodes() ); return new NodeGroup($nodes); - } catch (LogicException | TypeError $e) { + } catch (LogicException|TypeError $e) { fwrite(STDERR, "Setting up GdbotsQueryParserTest\n"); app('log')->error($e->getMessage()); app('log')->error(sprintf('Could not parse search: "%s".', $query)); @@ -84,7 +85,7 @@ class GdbotsQueryParser implements QueryParserInterface return new NodeGroup( array_map( - fn(GdbotsNode\Node $subNode) => $this->convertNode($subNode), + fn (GdbotsNode\Node $subNode) => $this->convertNode($subNode), $node->getNodes() ) ); diff --git a/app/Support/Search/QueryParser/QueryParser.php b/app/Support/Search/QueryParser/QueryParser.php index 2533bcc3a8..bef27a0ec9 100644 --- a/app/Support/Search/QueryParser/QueryParser.php +++ b/app/Support/Search/QueryParser/QueryParser.php @@ -139,7 +139,7 @@ class QueryParser implements QueryParserInterface if ('' === $tokenUnderConstruction) { // In any other location, it's just a normal character $tokenUnderConstruction .= $char; - $skipNext = true; + $skipNext = true; } if ('' !== $tokenUnderConstruction && !$skipNext) { // @phpstan-ignore-line Log::debug(sprintf('Turns out that "%s" is a field name. Reset the token.', $tokenUnderConstruction)); @@ -171,7 +171,7 @@ class QueryParser implements QueryParserInterface ++$this->position; } - $finalNode = '' !== $tokenUnderConstruction || '' !== $fieldName + $finalNode = '' !== $tokenUnderConstruction || '' !== $fieldName ? $this->createNode($tokenUnderConstruction, $fieldName, $prohibited) : null; @@ -184,7 +184,7 @@ class QueryParser implements QueryParserInterface $nodeResult = $this->buildNextNode($isSubquery); while ($nodeResult->node instanceof Node) { - $nodes[] = $nodeResult->node; + $nodes[] = $nodeResult->node; if ($nodeResult->isSubqueryEnd) { break; } diff --git a/app/Support/Singleton/PreferencesSingleton.php b/app/Support/Singleton/PreferencesSingleton.php index e8ff779c5e..287a964361 100644 --- a/app/Support/Singleton/PreferencesSingleton.php +++ b/app/Support/Singleton/PreferencesSingleton.php @@ -29,7 +29,7 @@ class PreferencesSingleton { private static ?PreferencesSingleton $instance = null; - private array $preferences = []; + private array $preferences = []; private function __construct() { diff --git a/app/Support/Steam.php b/app/Support/Steam.php index 32b7c160bd..46119a268a 100644 --- a/app/Support/Steam.php +++ b/app/Support/Steam.php @@ -38,6 +38,7 @@ use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Log; use Illuminate\Support\Str; use ValueError; + use function Safe\parse_url; use function Safe\preg_replace; @@ -49,45 +50,46 @@ 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 = []; + $result = []; $convertToPrimary ??= Amount::convertToPrimary(); $primary ??= Amount::getPrimaryCurrency(); - $currencies = $this->getCurrencies($accounts); + $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(); + ->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 = [ + $return = [ 'pc_balance' => '0', 'balance' => '0', // this key is overwritten right away, but I must remember it is always created. ]; - $currency = $currencies[$account->id]; + $currency = $currencies[$account->id]; // second array - $accountSum = array_filter($arrayOfSums, fn($entry) => $entry['account_id'] === $account->id); + $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 = 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'; + $return['balance'] = $sumsByCode[$currency->code] ?? '0'; if (!$convertToPrimary) { unset($return['pc_balance']); // Log::debug(sprintf('Set balance to %s, unset pc_balance', $return['balance'])); @@ -99,7 +101,7 @@ class Steam } // either way, the balance is always combined with the virtual balance: - $virtualBalance = (string)('' === (string)$account->virtual_balance ? '0' : $account->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: @@ -140,10 +142,10 @@ class Steam // Log::debug(sprintf('Trying bcround("%s",%d)', $number, $precision)); if (str_contains($number, '.')) { if ('-' !== $number[0]) { - return bcadd($number, '0.' . str_repeat('0', $precision) . '5', $precision); + return bcadd($number, '0.'.str_repeat('0', $precision).'5', $precision); } - return bcsub($number, '0.' . str_repeat('0', $precision) . '5', $precision); + return bcsub($number, '0.'.str_repeat('0', $precision).'5', $precision); } return $number; @@ -287,7 +289,7 @@ class Steam public function finalAccountBalance(Account $account, Carbon $date, ?TransactionCurrency $primary = null, ?bool $convertToPrimary = null): array { - $cache = new CacheProperties(); + $cache = new CacheProperties(); $cache->addProperty($account->id); $cache->addProperty($date); if ($cache->has()) { @@ -303,7 +305,7 @@ class Steam $primary = Amount::getPrimaryCurrencyByUserGroup($account->user->userGroup); } // account balance thing. - $currencyPresent = isset($account->meta) && array_key_exists('currency', $account->meta) && null !== $account->meta['currency']; + $currencyPresent = isset($account->meta) && array_key_exists('currency', $account->meta) && null !== $account->meta['currency']; if ($currencyPresent) { $accountCurrency = $account->meta['currency']; } @@ -311,19 +313,20 @@ class Steam $accountCurrency = $this->getAccountCurrency($account); } - $hasCurrency = null !== $accountCurrency; - $currency = $hasCurrency ? $accountCurrency : $primary; - $return = [ + $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'); + $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'; @@ -338,7 +341,7 @@ class Steam } // either way, the balance is always combined with the virtual balance: - $virtualBalance = (string)('' === (string)$account->virtual_balance ? '0' : $account->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: @@ -352,7 +355,7 @@ class Steam $return['balance'] = bcadd($return['balance'], $virtualBalance); // Log::debug(sprintf('Virtual balance makes the (primary currency) total %s', $return['balance'])); } - $final = array_merge($return, $others); + $final = array_merge($return, $others); // Log::debug('Final balance is', $final); $cache->store($final); @@ -367,7 +370,7 @@ class Steam Log::debug(sprintf('finalAccountBalanceInRange(#%d, %s, %s)', $account->id, $start->format('Y-m-d H:i:s'), $end->format('Y-m-d H:i:s'))); // set up cache - $cache = new CacheProperties(); + $cache = new CacheProperties(); $cache->addProperty($account->id); $cache->addProperty('final-balance-in-range'); $cache->addProperty($start); @@ -377,22 +380,22 @@ class Steam return $cache->get(); } - $balances = []; - $formatted = $start->format('Y-m-d'); + $balances = []; + $formatted = $start->format('Y-m-d'); /* * To make sure the start balance is correct, we need to get the balance at the exact end of the previous day. * Since we just did "startOfDay" we can do subDay()->endOfDay() to get the correct moment. * THAT will be the start balance. */ - $request = clone $start; + $request = clone $start; $request->subDay()->endOfDay(); Log::debug('Get first balance to start.'); Log::debug(sprintf('finalAccountBalanceInRange: Call finalAccountBalance with date/time "%s"', $request->toIso8601String())); - $startBalance = $this->finalAccountBalance($account, $request); - $primaryCurrency = Amount::getPrimaryCurrencyByUserGroup($account->user->userGroup); - $accountCurrency = $this->getAccountCurrency($account); - $hasCurrency = $accountCurrency instanceof TransactionCurrency; - $currency = $accountCurrency ?? $primaryCurrency; + $startBalance = $this->finalAccountBalance($account, $request); + $primaryCurrency = Amount::getPrimaryCurrencyByUserGroup($account->user->userGroup); + $accountCurrency = $this->getAccountCurrency($account); + $hasCurrency = $accountCurrency instanceof TransactionCurrency; + $currency = $accountCurrency ?? $primaryCurrency; Log::debug(sprintf('Currency is %s', $currency->code)); @@ -405,7 +408,7 @@ class Steam Log::debug(sprintf('Also set start balance in %s', $primaryCurrency->code)); $startBalance[$primaryCurrency->code] ??= '0'; } - $currencies = [ + $currencies = [ $currency->id => $currency, $primaryCurrency->id => $primaryCurrency, ]; @@ -415,47 +418,48 @@ class Steam // sums up the balance changes per day. Log::debug(sprintf('Date >= %s and <= %s', $start->format('Y-m-d H:i:s'), $end->format('Y-m-d H:i:s'))); - $set = $account->transactions() - ->leftJoin('transaction_journals', 'transactions.transaction_journal_id', '=', 'transaction_journals.id') - ->where('transaction_journals.date', '>=', $start->format('Y-m-d H:i:s')) - ->where('transaction_journals.date', '<=', $end->format('Y-m-d H:i:s')) - ->groupBy('transaction_journals.date') - ->groupBy('transactions.transaction_currency_id') - ->orderBy('transaction_journals.date', 'ASC') - ->whereNull('transaction_journals.deleted_at') - ->get( - [ // @phpstan-ignore-line - 'transaction_journals.date', - 'transactions.transaction_currency_id', - DB::raw('SUM(transactions.amount) AS sum_of_day'), - ] - ); + $set = $account->transactions() + ->leftJoin('transaction_journals', 'transactions.transaction_journal_id', '=', 'transaction_journals.id') + ->where('transaction_journals.date', '>=', $start->format('Y-m-d H:i:s')) + ->where('transaction_journals.date', '<=', $end->format('Y-m-d H:i:s')) + ->groupBy('transaction_journals.date') + ->groupBy('transactions.transaction_currency_id') + ->orderBy('transaction_journals.date', 'ASC') + ->whereNull('transaction_journals.deleted_at') + ->get( + [ // @phpstan-ignore-line + 'transaction_journals.date', + 'transactions.transaction_currency_id', + DB::raw('SUM(transactions.amount) AS sum_of_day'), + ] + ) + ; - $currentBalance = $startBalance; - $converter = new ExchangeRateConverter(); + $currentBalance = $startBalance; + $converter = new ExchangeRateConverter(); /** @var Transaction $entry */ foreach ($set as $entry) { // get date object - $carbon = new Carbon($entry->date, $entry->date_tz); - $carbonKey = $carbon->format('Y-m-d'); + $carbon = new Carbon($entry->date, $entry->date_tz); + $carbonKey = $carbon->format('Y-m-d'); // make sure sum is a string: - $sumOfDay = (string)($entry->sum_of_day ?? '0'); + $sumOfDay = (string)($entry->sum_of_day ?? '0'); // #10426 make sure sum is not in scientific notation. - $sumOfDay = $this->floatalize($sumOfDay); + $sumOfDay = $this->floatalize($sumOfDay); // find currency of this entry, does not have to exist. $currencies[$entry->transaction_currency_id] ??= Amount::getTransactionCurrencyById($entry->transaction_currency_id); // make sure this $entry has its own $entryCurrency /** @var TransactionCurrency $entryCurrency */ - $entryCurrency = $currencies[$entry->transaction_currency_id]; + $entryCurrency = $currencies[$entry->transaction_currency_id]; Log::debug(sprintf('Processing transaction(s) on moment %s', $carbon->format('Y-m-d H:i:s'))); // add amount to current balance in currency code. - $currentBalance[$entryCurrency->code] ??= '0'; + $currentBalance[$entryCurrency->code] ??= '0'; $currentBalance[$entryCurrency->code] = bcadd($sumOfDay, (string)$currentBalance[$entryCurrency->code]); // if not requested to convert to primary currency, add the amount to "balance", do nothing else. @@ -473,7 +477,7 @@ class Steam } } // add to final array. - $balances[$carbonKey] = $currentBalance; + $balances[$carbonKey] = $currentBalance; Log::debug(sprintf('Updated entry [%s]', $carbonKey), $currentBalance); } $cache->store($balances); @@ -490,7 +494,7 @@ class Steam */ public function floatalize(string $value): string { - $value = strtoupper($value); + $value = strtoupper($value); if (!str_contains($value, 'E')) { return $value; } @@ -514,8 +518,8 @@ class Steam public function getAccountCurrency(Account $account): ?TransactionCurrency { - $type = $account->accountType->type; - $list = config('firefly.valid_currency_account_types'); + $type = $account->accountType->type; + $list = config('firefly.valid_currency_account_types'); // return null if not in this list. if (!in_array($type, $list, true)) { @@ -569,15 +573,15 @@ class Steam { $list = []; - $set = auth()->user()->transactions() - ->whereIn('transactions.account_id', $accounts) - ->groupBy(['transactions.account_id', 'transaction_journals.user_id']) - ->get(['transactions.account_id', DB::raw('MAX(transaction_journals.date) AS max_date')]) // @phpstan-ignore-line + $set = auth()->user()->transactions() + ->whereIn('transactions.account_id', $accounts) + ->groupBy(['transactions.account_id', 'transaction_journals.user_id']) + ->get(['transactions.account_id', DB::raw('MAX(transaction_journals.date) AS max_date')]) // @phpstan-ignore-line ; /** @var Transaction $entry */ foreach ($set as $entry) { - $date = new Carbon($entry->max_date, config('app.timezone')); + $date = new Carbon($entry->max_date, config('app.timezone')); $date->setTimezone(config('app.timezone')); $list[(int)$entry->account_id] = $date; } @@ -591,24 +595,25 @@ class Steam public function getLocale(): string // get preference { $singleton = PreferencesSingleton::getInstance(); - $cached = $singleton->getPreference('locale'); - if(null !== $cached) { + $cached = $singleton->getPreference('locale'); + if (null !== $cached) { return $cached; } - $locale = app('preferences')->get('locale', config('firefly.default_locale', 'equal'))->data; + $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))) { $locale = str_replace('_', '-', $locale); } $singleton->setPreference('locale', $locale); + return $locale; } @@ -642,9 +647,9 @@ class Steam public function getSafeUrl(string $unknownUrl, string $safeUrl): string { // Log::debug(sprintf('getSafeUrl(%s, %s)', $unknownUrl, $safeUrl)); - $returnUrl = $safeUrl; - $unknownHost = parse_url($unknownUrl, PHP_URL_HOST); - $safeHost = parse_url($safeUrl, PHP_URL_HOST); + $returnUrl = $safeUrl; + $unknownHost = parse_url($unknownUrl, PHP_URL_HOST); + $safeHost = parse_url($safeUrl, PHP_URL_HOST); if (null !== $unknownHost && $unknownHost === $safeHost) { $returnUrl = $unknownUrl; @@ -746,12 +751,12 @@ class Steam if (null === $preference) { $singleton->setPreference($key, $currency); } - $current = $amount; + $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); + $total = bcadd($current, $total); } return $total; @@ -765,8 +770,8 @@ class Steam $primary = Amount::getPrimaryCurrency(); $currencies[$primary->id] = $primary; - $ids = $accounts->pluck('id')->toArray(); - $result = AccountMeta::whereIn('account_id', $ids)->where('name', 'currency_id')->get(); + $ids = $accounts->pluck('id')->toArray(); + $result = AccountMeta::whereIn('account_id', $ids)->where('name', 'currency_id')->get(); /** @var AccountMeta $item */ foreach ($result as $item) { @@ -776,7 +781,7 @@ class Steam } } // collect those currencies, skip primary because we already have it. - $set = TransactionCurrency::whereIn('id', $accountPreferences)->where('id', '!=', $primary->id)->get(); + $set = TransactionCurrency::whereIn('id', $accountPreferences)->where('id', '!=', $primary->id)->get(); foreach ($set as $item) { $currencies[$item->id] = $item; } @@ -787,7 +792,7 @@ class Steam $currencyPresent = isset($account->meta) && array_key_exists('currency', $account->meta) && null !== $account->meta['currency']; if ($currencyPresent) { $currencyId = $account->meta['currency']->id; - $currencies[$currencyId] ??= $account->meta['currency']; + $currencies[$currencyId] ??= $account->meta['currency']; $accountCurrencies[$accountId] = $account->meta['currency']; } if (!$currencyPresent && !array_key_exists($accountId, $accountPreferences)) { diff --git a/app/Support/System/OAuthKeys.php b/app/Support/System/OAuthKeys.php index 1c1f2276cf..97bd74bd19 100644 --- a/app/Support/System/OAuthKeys.php +++ b/app/Support/System/OAuthKeys.php @@ -31,6 +31,7 @@ use Illuminate\Support\Facades\Crypt; use Laravel\Passport\Console\KeysCommand; use Psr\Container\ContainerExceptionInterface; use Psr\Container\NotFoundExceptionInterface; + use function Safe\file_get_contents; use function Safe\file_put_contents; @@ -65,7 +66,7 @@ class OAuthKeys try { $privateKey = (string)app('fireflyconfig')->get(self::PRIVATE_KEY)?->data; $publicKey = (string)app('fireflyconfig')->get(self::PUBLIC_KEY)?->data; - } catch (ContainerExceptionInterface | FireflyException | NotFoundExceptionInterface $e) { + } catch (ContainerExceptionInterface|FireflyException|NotFoundExceptionInterface $e) { app('log')->error(sprintf('Could not validate keysInDatabase(): %s', $e->getMessage())); app('log')->error($e->getTraceAsString()); } @@ -98,8 +99,8 @@ class OAuthKeys return false; } - $private = storage_path('oauth-private.key'); - $public = storage_path('oauth-public.key'); + $private = storage_path('oauth-private.key'); + $public = storage_path('oauth-public.key'); file_put_contents($private, $privateContent); file_put_contents($public, $publicContent); diff --git a/app/Support/Twig/AmountFormat.php b/app/Support/Twig/AmountFormat.php index 49c9a6d11e..dbf22254b3 100644 --- a/app/Support/Twig/AmountFormat.php +++ b/app/Support/Twig/AmountFormat.php @@ -145,13 +145,13 @@ class AmountFormat extends AbstractExtension static function (string $amount, ?string $symbol = null, ?int $decimalPlaces = null, ?bool $coloured = null): string { if (null === $symbol) { - $message = sprintf('formatAmountBySymbol("%s", %s, %d, %s) was called without a symbol. Please browse to /flush to clear your cache.', $amount, var_export($symbol, true), $decimalPlaces, var_export($coloured, true)); + $message = sprintf('formatAmountBySymbol("%s", %s, %d, %s) was called without a symbol. Please browse to /flush to clear your cache.', $amount, var_export($symbol, true), $decimalPlaces, var_export($coloured, true)); Log::error($message); $currency = Amount::getPrimaryCurrency(); } if (null !== $symbol) { - $decimalPlaces ??= 2; - $coloured ??= true; + $decimalPlaces ??= 2; + $coloured ??= true; $currency = new TransactionCurrency(); $currency->symbol = $symbol; $currency->decimal_places = $decimalPlaces; diff --git a/app/Support/Twig/General.php b/app/Support/Twig/General.php index 337e832312..485dfe1bd9 100644 --- a/app/Support/Twig/General.php +++ b/app/Support/Twig/General.php @@ -37,6 +37,7 @@ use Override; use Twig\Extension\AbstractExtension; use Twig\TwigFilter; use Twig\TwigFunction; + use function Safe\parse_url; /** @@ -104,7 +105,7 @@ class General extends AbstractExtension 'activeRoutePartialObjectType', static function ($context): string { [, $route, $objectType] = func_get_args(); - $activeObjectType = $context['objectType'] ?? false; + $activeObjectType = $context['objectType'] ?? false; if ($objectType === $activeObjectType && false !== stripos( @@ -154,14 +155,14 @@ class General extends AbstractExtension } /** @var Carbon $date */ - $date = session('end', today(config('app.timezone'))->endOfMonth()); + $date = session('end', today(config('app.timezone'))->endOfMonth()); Log::debug(sprintf('twig balance: Call finalAccountBalance with date/time "%s"', $date->toIso8601String())); $info = Steam::finalAccountBalance($account, $date); $currency = Steam::getAccountCurrency($account); $primary = Amount::getPrimaryCurrency(); $convertToPrimary = Amount::convertToPrimary(); $usePrimary = $convertToPrimary && $primary->id !== $currency->id; - $currency ??= $primary; + $currency ??= $primary; $strings = []; foreach ($info as $key => $balance) { if ('balance' === $key) { @@ -196,7 +197,7 @@ class General extends AbstractExtension { return new TwigFunction( 'carbonize', - static fn(string $date): Carbon => new Carbon($date, config('app.timezone')) + static fn (string $date): Carbon => new Carbon($date, config('app.timezone')) ); } @@ -225,15 +226,15 @@ class General extends AbstractExtension static function (int $size): string { // less than one GB, more than one MB if ($size < (1024 * 1024 * 2014) && $size >= (1024 * 1024)) { - return round($size / (1024 * 1024), 2) . ' MB'; + return round($size / (1024 * 1024), 2).' MB'; } // less than one MB if ($size < (1024 * 1024)) { - return round($size / 1024, 2) . ' KB'; + return round($size / 1024, 2).' KB'; } - return $size . ' bytes'; + return $size.' bytes'; } ); } @@ -337,7 +338,7 @@ class General extends AbstractExtension { return new TwigFilter( 'mimeIcon', - static fn(string $string): string => match ($string) { + static fn (string $string): string => match ($string) { 'application/pdf' => 'fa-file-pdf-o', 'image/webp', 'image/png', 'image/jpeg', 'image/svg+xml', 'image/heic', 'image/heic-sequence', 'application/vnd.oasis.opendocument.image' => 'fa-file-image-o', 'application/msword', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', 'application/vnd.openxmlformats-officedocument.wordprocessingml.template', 'application/x-iwork-pages-sffpages', 'application/vnd.sun.xml.writer', 'application/vnd.sun.xml.writer.template', 'application/vnd.sun.xml.writer.global', 'application/vnd.stardivision.writer', 'application/vnd.stardivision.writer-global', 'application/vnd.oasis.opendocument.text', 'application/vnd.oasis.opendocument.text-template', 'application/vnd.oasis.opendocument.text-web', 'application/vnd.oasis.opendocument.text-master' => 'fa-file-word-o', @@ -380,7 +381,7 @@ class General extends AbstractExtension { return new TwigFunction( 'phpdate', - static fn(string $str): string => date($str) + static fn (string $str): string => date($str) ); } } diff --git a/app/Support/Twig/Rule.php b/app/Support/Twig/Rule.php index 7ed872df8b..ea60a67a3e 100644 --- a/app/Support/Twig/Rule.php +++ b/app/Support/Twig/Rule.php @@ -42,7 +42,7 @@ class Rule extends AbstractExtension $ruleActions = array_keys(Config::get('firefly.rule-actions')); $possibleActions = []; foreach ($ruleActions as $key) { - $possibleActions[$key] = (string)trans('firefly.rule_action_' . $key . '_choice'); + $possibleActions[$key] = (string)trans('firefly.rule_action_'.$key.'_choice'); } unset($ruleActions); asort($possibleActions); @@ -56,7 +56,7 @@ class Rule extends AbstractExtension { return new TwigFunction( 'allJournalTriggers', - static fn() => [ + static fn () => [ 'store-journal' => (string)trans('firefly.rule_trigger_store_journal'), 'update-journal' => (string)trans('firefly.rule_trigger_update_journal'), 'manual-activation' => (string)trans('firefly.rule_trigger_manual'), @@ -73,7 +73,7 @@ class Rule extends AbstractExtension $possibleTriggers = []; foreach ($ruleTriggers as $key) { if ('user_action' !== $key) { - $possibleTriggers[$key] = (string)trans('firefly.rule_trigger_' . $key . '_choice'); + $possibleTriggers[$key] = (string)trans('firefly.rule_trigger_'.$key.'_choice'); } } unset($ruleTriggers); diff --git a/app/Support/Twig/TransactionGroupTwig.php b/app/Support/Twig/TransactionGroupTwig.php index e2957ec3a1..3033a84872 100644 --- a/app/Support/Twig/TransactionGroupTwig.php +++ b/app/Support/Twig/TransactionGroupTwig.php @@ -34,6 +34,7 @@ use Illuminate\Support\Facades\DB; use Override; use Twig\Extension\AbstractExtension; use Twig\TwigFunction; + use function Safe\json_decode; /** @@ -82,10 +83,11 @@ class TransactionGroupTwig extends AbstractExtension static function (int $journalId, string $metaField) { /** @var null|TransactionJournalMeta $entry */ $entry = DB::table('journal_meta') - ->where('name', $metaField) - ->where('transaction_journal_id', $journalId) - ->whereNull('deleted_at') - ->first(); + ->where('name', $metaField) + ->where('transaction_journal_id', $journalId) + ->whereNull('deleted_at') + ->first() + ; if (null === $entry) { return today(config('app.timezone')); } @@ -102,10 +104,11 @@ class TransactionGroupTwig extends AbstractExtension static function (int $journalId, string $metaField) { /** @var null|TransactionJournalMeta $entry */ $entry = DB::table('journal_meta') - ->where('name', $metaField) - ->where('transaction_journal_id', $journalId) - ->whereNull('deleted_at') - ->first(); + ->where('name', $metaField) + ->where('transaction_journal_id', $journalId) + ->whereNull('deleted_at') + ->first() + ; if (null === $entry) { return ''; } @@ -121,10 +124,11 @@ class TransactionGroupTwig extends AbstractExtension 'journalHasMeta', static function (int $journalId, string $metaField) { $count = DB::table('journal_meta') - ->where('name', $metaField) - ->where('transaction_journal_id', $journalId) - ->whereNull('deleted_at') - ->count(); + ->where('name', $metaField) + ->where('transaction_journal_id', $journalId) + ->whereNull('deleted_at') + ->count() + ; return 1 === $count; } @@ -157,9 +161,9 @@ class TransactionGroupTwig extends AbstractExtension */ private function foreignJournalArrayAmount(array $array): string { - $type = $array['transaction_type_type'] ?? TransactionTypeEnum::WITHDRAWAL->value; - $amount = $array['foreign_amount'] ?? '0'; - $colored = true; + $type = $array['transaction_type_type'] ?? TransactionTypeEnum::WITHDRAWAL->value; + $amount = $array['foreign_amount'] ?? '0'; + $colored = true; $sourceType = $array['source_account_type'] ?? 'invalid'; $amount = $this->signAmount($amount, $type, $sourceType); @@ -167,7 +171,7 @@ class TransactionGroupTwig extends AbstractExtension if (TransactionTypeEnum::TRANSFER->value === $type) { $colored = false; } - $result = app('amount')->formatFlat($array['foreign_currency_symbol'], (int)$array['foreign_currency_decimal_places'], $amount, $colored); + $result = app('amount')->formatFlat($array['foreign_currency_symbol'], (int)$array['foreign_currency_decimal_places'], $amount, $colored); if (TransactionTypeEnum::TRANSFER->value === $type) { return sprintf('%s', $result); } @@ -180,7 +184,7 @@ class TransactionGroupTwig extends AbstractExtension */ private function foreignJournalObjectAmount(TransactionJournal $journal): string { - $type = $journal->transactionType->type; + $type = $journal->transactionType->type; /** @var Transaction $first */ $first = $journal->transactions()->where('amount', '<', 0)->first(); @@ -189,12 +193,12 @@ class TransactionGroupTwig extends AbstractExtension $colored = true; $sourceType = $first->account->accountType()->first()->type; - $amount = $this->signAmount($amount, $type, $sourceType); + $amount = $this->signAmount($amount, $type, $sourceType); if (TransactionTypeEnum::TRANSFER->value === $type) { $colored = false; } - $result = app('amount')->formatFlat($currency->symbol, $currency->decimal_places, $amount, $colored); + $result = app('amount')->formatFlat($currency->symbol, $currency->decimal_places, $amount, $colored); if (TransactionTypeEnum::TRANSFER->value === $type) { return sprintf('%s', $result); } @@ -225,7 +229,7 @@ class TransactionGroupTwig extends AbstractExtension $colored = false; } - $result = app('amount')->formatFlat($array['currency_symbol'], (int)$array['currency_decimal_places'], $amount, $colored); + $result = app('amount')->formatFlat($array['currency_symbol'], (int)$array['currency_decimal_places'], $amount, $colored); if (TransactionTypeEnum::TRANSFER->value === $type) { return sprintf('%s', $result); } @@ -238,7 +242,7 @@ class TransactionGroupTwig extends AbstractExtension */ private function normalJournalObjectAmount(TransactionJournal $journal): string { - $type = $journal->transactionType->type; + $type = $journal->transactionType->type; /** @var Transaction $first */ $first = $journal->transactions()->where('amount', '<', 0)->first(); @@ -247,12 +251,12 @@ class TransactionGroupTwig extends AbstractExtension $colored = true; $sourceType = $first->account->accountType()->first()->type; - $amount = $this->signAmount($amount, $type, $sourceType); + $amount = $this->signAmount($amount, $type, $sourceType); if (TransactionTypeEnum::TRANSFER->value === $type) { $colored = false; } - $result = app('amount')->formatFlat($currency->symbol, $currency->decimal_places, $amount, $colored); + $result = app('amount')->formatFlat($currency->symbol, $currency->decimal_places, $amount, $colored); if (TransactionTypeEnum::TRANSFER->value === $type) { return sprintf('%s', $result); } diff --git a/app/Support/Twig/Translation.php b/app/Support/Twig/Translation.php index e4f429f07e..d316895ed7 100644 --- a/app/Support/Twig/Translation.php +++ b/app/Support/Twig/Translation.php @@ -39,7 +39,7 @@ class Translation extends AbstractExtension return [ new TwigFilter( '_', - static fn($name) => (string)trans(sprintf('firefly.%s', $name)), + static fn ($name) => (string)trans(sprintf('firefly.%s', $name)), ['is_safe' => ['html']] ), ]; diff --git a/composer.lock b/composer.lock index 18cb3ef987..f3717d8cb4 100644 --- a/composer.lock +++ b/composer.lock @@ -11511,21 +11511,21 @@ }, { "name": "phpstan/phpstan-strict-rules", - "version": "2.0.6", + "version": "2.0.7", "source": { "type": "git", "url": "https://github.com/phpstan/phpstan-strict-rules.git", - "reference": "f9f77efa9de31992a832ff77ea52eb42d675b094" + "reference": "d6211c46213d4181054b3d77b10a5c5cb0d59538" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan-strict-rules/zipball/f9f77efa9de31992a832ff77ea52eb42d675b094", - "reference": "f9f77efa9de31992a832ff77ea52eb42d675b094", + "url": "https://api.github.com/repos/phpstan/phpstan-strict-rules/zipball/d6211c46213d4181054b3d77b10a5c5cb0d59538", + "reference": "d6211c46213d4181054b3d77b10a5c5cb0d59538", "shasum": "" }, "require": { "php": "^7.4 || ^8.0", - "phpstan/phpstan": "^2.0.4" + "phpstan/phpstan": "^2.1.29" }, "require-dev": { "php-parallel-lint/php-parallel-lint": "^1.2", @@ -11553,9 +11553,9 @@ "description": "Extra strict and opinionated rules for PHPStan", "support": { "issues": "https://github.com/phpstan/phpstan-strict-rules/issues", - "source": "https://github.com/phpstan/phpstan-strict-rules/tree/2.0.6" + "source": "https://github.com/phpstan/phpstan-strict-rules/tree/2.0.7" }, - "time": "2025-07-21T12:19:29+00:00" + "time": "2025-09-26T11:19:08+00:00" }, { "name": "phpunit/php-code-coverage", diff --git a/config/firefly.php b/config/firefly.php index 3139bb0be9..cf0424ddc6 100644 --- a/config/firefly.php +++ b/config/firefly.php @@ -78,8 +78,8 @@ return [ 'running_balance_column' => env('USE_RUNNING_BALANCE', false), // see cer.php for exchange rates feature flag. ], - 'version' => 'develop/2025-09-25', - 'build_time' => 1758820163, + 'version' => 'develop/2025-09-26', + 'build_time' => 1758908498, 'api_version' => '2.1.0', // field is no longer used. 'db_version' => 27, diff --git a/package-lock.json b/package-lock.json index d69416dc1e..09bb10c0a6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5736,9 +5736,9 @@ "license": "MIT" }, "node_modules/electron-to-chromium": { - "version": "1.5.223", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.223.tgz", - "integrity": "sha512-qKm55ic6nbEmagFlTFczML33rF90aU+WtrJ9MdTCThrcvDNdUHN4p6QfVN78U06ZmguqXIyMPyYhw2TrbDUwPQ==", + "version": "1.5.224", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.224.tgz", + "integrity": "sha512-kWAoUu/bwzvnhpdZSIc6KUyvkI1rbRXMT0Eq8pKReyOyaPZcctMli+EgvcN1PAvwVc7Tdo4Fxi2PsLNDU05mdg==", "dev": true, "license": "ISC" }, From 822dee6e70fd86035f211d47105a4b82d651e411 Mon Sep 17 00:00:00 2001 From: James Cole Date: Fri, 26 Sep 2025 19:48:20 +0200 Subject: [PATCH 47/58] Allow statistics to be removed from /flush --- app/Http/Controllers/Admin/UpdateController.php | 11 ++++++----- app/Http/Controllers/DebugController.php | 3 +++ app/Models/PeriodStatistic.php | 2 -- .../2025_09_25_175248_create_period_statistics.php | 1 - 4 files changed, 9 insertions(+), 8 deletions(-) diff --git a/app/Http/Controllers/Admin/UpdateController.php b/app/Http/Controllers/Admin/UpdateController.php index 8502d43ce8..90729abe56 100644 --- a/app/Http/Controllers/Admin/UpdateController.php +++ b/app/Http/Controllers/Admin/UpdateController.php @@ -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')); diff --git a/app/Http/Controllers/DebugController.php b/app/Http/Controllers/DebugController.php index 73141d0b8c..f599788536 100644 --- a/app/Http/Controllers/DebugController.php +++ b/app/Http/Controllers/DebugController.php @@ -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); diff --git a/app/Models/PeriodStatistic.php b/app/Models/PeriodStatistic.php index 194073bc88..519df3c79c 100644 --- a/app/Models/PeriodStatistic.php +++ b/app/Models/PeriodStatistic.php @@ -14,14 +14,12 @@ use Illuminate\Database\Eloquent\SoftDeletes; class PeriodStatistic extends Model { use ReturnsIntegerUserIdTrait; - use SoftDeletes; protected function casts(): array { return [ 'created_at' => 'datetime', 'updated_at' => 'datetime', - 'deleted_at' => 'datetime', 'start' => SeparateTimezoneCaster::class, 'end' => SeparateTimezoneCaster::class, ]; diff --git a/database/migrations/2025_09_25_175248_create_period_statistics.php b/database/migrations/2025_09_25_175248_create_period_statistics.php index e04afaa9de..0a5bf8d86b 100644 --- a/database/migrations/2025_09_25_175248_create_period_statistics.php +++ b/database/migrations/2025_09_25_175248_create_period_statistics.php @@ -14,7 +14,6 @@ return new class extends Migration Schema::create('period_statistics', function (Blueprint $table) { $table->id(); $table->timestamps(); - $table->softDeletes(); $table->integer('primary_statable_id', false, true)->nullable(); $table->string('primary_statable_type', 255)->nullable(); From 33dcce752510889801e2055d0d25cdea5f1c448f Mon Sep 17 00:00:00 2001 From: James Cole Date: Fri, 26 Sep 2025 20:46:23 +0200 Subject: [PATCH 48/58] Optimize query for collecting transactions. --- .../Http/Controllers/PeriodOverview.php | 263 +++++++++--------- 1 file changed, 132 insertions(+), 131 deletions(-) diff --git a/app/Support/Http/Controllers/PeriodOverview.php b/app/Support/Http/Controllers/PeriodOverview.php index 6af3f30b6f..8e0d68f206 100644 --- a/app/Support/Http/Controllers/PeriodOverview.php +++ b/app/Support/Http/Controllers/PeriodOverview.php @@ -73,7 +73,8 @@ trait PeriodOverview protected AccountRepositoryInterface $accountRepository; protected JournalRepositoryInterface $journalRepos; protected PeriodStatisticRepositoryInterface $periodStatisticRepo; - private Collection $statistics; + 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) @@ -88,12 +89,12 @@ trait PeriodOverview $this->accountRepository = app(AccountRepositoryInterface::class); $this->periodStatisticRepo = app(PeriodStatisticRepositoryInterface::class); $range = Navigation::getViewRange(true); - [$start, $end] = $end < $start ? [$end, $start] : [$start, $end]; + [$start, $end] = $end < $start ? [$end, $start] : [$start, $end]; /** @var array $dates */ - $dates = Navigation::blockPeriods($start, $end, $range); - [$start, $end] = $this->getPeriodFromBlocks($dates, $start, $end); - $this->statistics = $this->periodStatisticRepo->allInRangeForModel($account, $start, $end); + $dates = Navigation::blockPeriods($start, $end, $range); + [$start, $end] = $this->getPeriodFromBlocks($dates, $start, $end); + $this->statistics = $this->periodStatisticRepo->allInRangeForModel($account, $start, $end); // TODO needs to be re-arranged: // get all period stats for entire range. @@ -101,7 +102,7 @@ trait PeriodOverview // create new ones, or use collected. - $entries = []; + $entries = []; Log::debug(sprintf('Count of loops: %d', count($dates))); foreach ($dates as $currentDate) { $entries[] = $this->getSingleAccountPeriod($account, $currentDate['period'], $currentDate['start'], $currentDate['end']); @@ -137,11 +138,11 @@ trait PeriodOverview */ protected function getCategoryPeriodOverview(Category $category, Carbon $start, Carbon $end): array { - $range = Navigation::getViewRange(true); + $range = Navigation::getViewRange(true); [$start, $end] = $end < $start ? [$end, $start] : [$start, $end]; // properties for entries with their amounts. - $cache = new CacheProperties(); + $cache = new CacheProperties(); $cache->addProperty($start); $cache->addProperty($end); $cache->addProperty($range); @@ -153,32 +154,32 @@ trait PeriodOverview } /** @var array $dates */ - $dates = Navigation::blockPeriods($start, $end, $range); - $entries = []; + $dates = Navigation::blockPeriods($start, $end, $range); + $entries = []; // collect all expenses in this period: /** @var GroupCollectorInterface $collector */ - $collector = app(GroupCollectorInterface::class); + $collector = app(GroupCollectorInterface::class); $collector->setCategory($category); $collector->setRange($start, $end); $collector->setTypes([TransactionTypeEnum::DEPOSIT->value]); - $earnedSet = $collector->getExtractedJournals(); + $earnedSet = $collector->getExtractedJournals(); // collect all income in this period: /** @var GroupCollectorInterface $collector */ - $collector = app(GroupCollectorInterface::class); + $collector = app(GroupCollectorInterface::class); $collector->setCategory($category); $collector->setRange($start, $end); $collector->setTypes([TransactionTypeEnum::WITHDRAWAL->value]); - $spentSet = $collector->getExtractedJournals(); + $spentSet = $collector->getExtractedJournals(); // collect all transfers in this period: /** @var GroupCollectorInterface $collector */ - $collector = app(GroupCollectorInterface::class); + $collector = app(GroupCollectorInterface::class); $collector->setCategory($category); $collector->setRange($start, $end); $collector->setTypes([TransactionTypeEnum::TRANSFER->value]); - $transferSet = $collector->getExtractedJournals(); + $transferSet = $collector->getExtractedJournals(); foreach ($dates as $currentDate) { $spent = $this->filterJournalsByDate($spentSet, $currentDate['start'], $currentDate['end']); $earned = $this->filterJournalsByDate($earnedSet, $currentDate['start'], $currentDate['end']); @@ -186,17 +187,17 @@ trait PeriodOverview $title = Navigation::periodShow($currentDate['end'], $currentDate['period']); $entries[] = [ - 'transactions' => 0, - 'title' => $title, - 'route' => route( - 'categories.show', - [$category->id, $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), - ]; + 'transactions' => 0, + 'title' => $title, + 'route' => route( + 'categories.show', + [$category->id, $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), + ]; } $cache->store($entries); @@ -212,11 +213,11 @@ trait PeriodOverview */ protected function getNoBudgetPeriodOverview(Carbon $start, Carbon $end): array { - $range = Navigation::getViewRange(true); + $range = Navigation::getViewRange(true); [$start, $end] = $end < $start ? [$end, $start] : [$start, $end]; - $cache = new CacheProperties(); + $cache = new CacheProperties(); $cache->addProperty($start); $cache->addProperty($end); $cache->addProperty($this->convertToPrimary); @@ -227,28 +228,28 @@ trait PeriodOverview } /** @var array $dates */ - $dates = Navigation::blockPeriods($start, $end, $range); - $entries = []; + $dates = Navigation::blockPeriods($start, $end, $range); + $entries = []; // get all expenses without a budget. /** @var GroupCollectorInterface $collector */ - $collector = app(GroupCollectorInterface::class); + $collector = app(GroupCollectorInterface::class); $collector->setRange($start, $end)->withoutBudget()->withAccountInformation()->setTypes([TransactionTypeEnum::WITHDRAWAL->value]); - $journals = $collector->getExtractedJournals(); + $journals = $collector->getExtractedJournals(); foreach ($dates as $currentDate) { $set = $this->filterJournalsByDate($journals, $currentDate['start'], $currentDate['end']); $title = Navigation::periodShow($currentDate['end'], $currentDate['period']); $entries[] = [ - 'title' => $title, - 'route' => route('budgets.no-budget', [$currentDate['start']->format('Y-m-d'), $currentDate['end']->format('Y-m-d')]), - 'total_transactions' => count($set), - 'spent' => $this->groupByCurrency($set), - 'earned' => [], - 'transferred_away' => [], - 'transferred_in' => [], - ]; + 'title' => $title, + 'route' => route('budgets.no-budget', [$currentDate['start']->format('Y-m-d'), $currentDate['end']->format('Y-m-d')]), + 'total_transactions' => count($set), + 'spent' => $this->groupByCurrency($set), + 'earned' => [], + 'transferred_away' => [], + 'transferred_in' => [], + ]; } $cache->store($entries); @@ -265,38 +266,38 @@ trait PeriodOverview protected function getNoCategoryPeriodOverview(Carbon $theDate): array { 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); + $range = Navigation::getViewRange(true); + $first = $this->journalRepos->firstNull(); + $start = null === $first ? new Carbon() : $first->date; + $end = clone $theDate; + $end = Navigation::endOfPeriod($end, $range); 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); - $entries = []; + $dates = Navigation::blockPeriods($start, $end, $range); + $entries = []; // collect all expenses in this period: /** @var GroupCollectorInterface $collector */ - $collector = app(GroupCollectorInterface::class); + $collector = app(GroupCollectorInterface::class); $collector->withoutCategory(); $collector->setRange($start, $end); $collector->setTypes([TransactionTypeEnum::DEPOSIT->value]); - $earnedSet = $collector->getExtractedJournals(); + $earnedSet = $collector->getExtractedJournals(); // collect all income in this period: /** @var GroupCollectorInterface $collector */ - $collector = app(GroupCollectorInterface::class); + $collector = app(GroupCollectorInterface::class); $collector->withoutCategory(); $collector->setRange($start, $end); $collector->setTypes([TransactionTypeEnum::WITHDRAWAL->value]); - $spentSet = $collector->getExtractedJournals(); + $spentSet = $collector->getExtractedJournals(); // collect all transfers in this period: /** @var GroupCollectorInterface $collector */ - $collector = app(GroupCollectorInterface::class); + $collector = app(GroupCollectorInterface::class); $collector->withoutCategory(); $collector->setRange($start, $end); $collector->setTypes([TransactionTypeEnum::TRANSFER->value]); @@ -310,13 +311,13 @@ trait PeriodOverview $title = Navigation::periodShow($currentDate['end'], $currentDate['period']); $entries[] = [ - 'title' => $title, - 'route' => route('categories.no-category', [$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('categories.no-category', [$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), + ]; } Log::debug('End of loops'); @@ -326,14 +327,15 @@ trait PeriodOverview 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 = [ + $types = ['spent', 'earned', 'transferred_in', 'transferred_away']; + $return = [ 'title' => Navigation::periodShow($start, $period), 'route' => route('accounts.show', [$account->id, $start->format('Y-m-d'), $start->format('Y-m-d')]), 'total_transactions' => 0, ]; + $this->transactions = []; foreach ($types as $type) { - $set = $this->getSingleAccountPeriodByType($account, $start, $end, $type); + $set = $this->getSingleAccountPeriodByType($account, $start, $end, $type); $return['total_transactions'] += $set['count']; unset($set['count']); $return[$type] = $set; @@ -371,49 +373,51 @@ trait PeriodOverview // nothing found, regenerate them. if (0 === $statistics->count()) { Log::debug(sprintf('Found nothing in this period for type "%s"', $type)); - $transactions = $this->accountRepository->periodCollection($account, $start, $end); + 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, $transactions, $start, $end); + $result = $this->filterTransactionsByType(TransactionTypeEnum::WITHDRAWAL, $start, $end); break; case 'earned': - $result = $this->filterTransactionsByType(TransactionTypeEnum::DEPOSIT, $transactions, $start, $end); + $result = $this->filterTransactionsByType(TransactionTypeEnum::DEPOSIT, $start, $end); break; case 'transferred_in': - $result = $this->filterTransfers('in', $transactions, $start, $end); + $result = $this->filterTransfers('in', $start, $end); break; case 'transferred_away': - $result = $this->filterTransfers('away', $transactions, $start, $end); + $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); + $grouped = $this->groupByCurrency($result); $this->saveGroupedAsStatistics($account, $start, $end, $type, $grouped); return $grouped; } - $grouped = [ + $grouped = [ 'count' => 0, ]; /** @var PeriodStatistic $statistic */ foreach ($statistics as $statistic) { - $id = (int)$statistic->transaction_currency_id; - $currency = Amount::getTransactionCurrencyById($id); - $grouped[$id] = [ + $id = (int)$statistic->transaction_currency_id; + $currency = Amount::getTransactionCurrencyById($id); + $grouped[$id] = [ 'amount' => (string)$statistic->amount, 'count' => (int)$statistic->count, 'currency_id' => $currency->id, @@ -435,11 +439,11 @@ trait PeriodOverview */ protected function getTagPeriodOverview(Tag $tag, Carbon $start, Carbon $end): array // period overview for tags. { - $range = Navigation::getViewRange(true); + $range = Navigation::getViewRange(true); [$start, $end] = $end < $start ? [$end, $start] : [$start, $end]; // properties for cache - $cache = new CacheProperties(); + $cache = new CacheProperties(); $cache->addProperty($start); $cache->addProperty($end); $cache->addProperty('tag-period-entries'); @@ -449,37 +453,37 @@ trait PeriodOverview } /** @var array $dates */ - $dates = Navigation::blockPeriods($start, $end, $range); - $entries = []; + $dates = Navigation::blockPeriods($start, $end, $range); + $entries = []; // collect all expenses in this period: /** @var GroupCollectorInterface $collector */ - $collector = app(GroupCollectorInterface::class); + $collector = app(GroupCollectorInterface::class); $collector->setTag($tag); $collector->setRange($start, $end); $collector->setTypes([TransactionTypeEnum::DEPOSIT->value]); - $earnedSet = $collector->getExtractedJournals(); + $earnedSet = $collector->getExtractedJournals(); // collect all income in this period: /** @var GroupCollectorInterface $collector */ - $collector = app(GroupCollectorInterface::class); + $collector = app(GroupCollectorInterface::class); $collector->setTag($tag); $collector->setRange($start, $end); $collector->setTypes([TransactionTypeEnum::WITHDRAWAL->value]); - $spentSet = $collector->getExtractedJournals(); + $spentSet = $collector->getExtractedJournals(); // collect all transfers in this period: /** @var GroupCollectorInterface $collector */ - $collector = app(GroupCollectorInterface::class); + $collector = app(GroupCollectorInterface::class); $collector->setTag($tag); $collector->setRange($start, $end); $collector->setTypes([TransactionTypeEnum::TRANSFER->value]); - $transferSet = $collector->getExtractedJournals(); + $transferSet = $collector->getExtractedJournals(); // filer all of them: - $earnedSet = $this->filterJournalsByTag($earnedSet, $tag); - $spentSet = $this->filterJournalsByTag($spentSet, $tag); - $transferSet = $this->filterJournalsByTag($transferSet, $tag); + $earnedSet = $this->filterJournalsByTag($earnedSet, $tag); + $spentSet = $this->filterJournalsByTag($spentSet, $tag); + $transferSet = $this->filterJournalsByTag($transferSet, $tag); foreach ($dates as $currentDate) { $spent = $this->filterJournalsByDate($spentSet, $currentDate['start'], $currentDate['end']); @@ -488,17 +492,17 @@ trait PeriodOverview $title = Navigation::periodShow($currentDate['end'], $currentDate['period']); $entries[] = [ - 'transactions' => 0, - 'title' => $title, - 'route' => route( - 'tags.show', - [$tag->id, $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), - ]; + 'transactions' => 0, + 'title' => $title, + 'route' => route( + 'tags.show', + [$tag->id, $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), + ]; } return $entries; @@ -509,12 +513,12 @@ trait PeriodOverview */ protected function getTransactionPeriodOverview(string $transactionType, Carbon $start, Carbon $end): array { - $range = Navigation::getViewRange(true); - $types = config(sprintf('firefly.transactionTypesByType.%s', $transactionType)); + $range = Navigation::getViewRange(true); + $types = config(sprintf('firefly.transactionTypesByType.%s', $transactionType)); [$start, $end] = $end < $start ? [$end, $start] : [$start, $end]; // properties for cache - $cache = new CacheProperties(); + $cache = new CacheProperties(); $cache->addProperty($start); $cache->addProperty($end); $cache->addProperty('transactions-period-entries'); @@ -524,16 +528,16 @@ trait PeriodOverview } /** @var array $dates */ - $dates = Navigation::blockPeriods($start, $end, $range); - $entries = []; - $spent = []; - $earned = []; - $transferred = []; + $dates = Navigation::blockPeriods($start, $end, $range); + $entries = []; + $spent = []; + $earned = []; + $transferred = []; // collect all journals in this period (regardless of type) - $collector = app(GroupCollectorInterface::class); + $collector = app(GroupCollectorInterface::class); $collector->setTypes($types)->setRange($start, $end); - $genericSet = $collector->getExtractedJournals(); - $loops = 0; + $genericSet = $collector->getExtractedJournals(); + $loops = 0; foreach ($dates as $currentDate) { $title = Navigation::periodShow($currentDate['end'], $currentDate['period']); @@ -551,14 +555,14 @@ 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; } @@ -599,7 +603,7 @@ trait PeriodOverview { $return = []; foreach ($set as $entry) { - $found = false; + $found = false; /** @var array $localTag */ foreach ($entry['tags'] as $localTag) { @@ -616,7 +620,7 @@ trait PeriodOverview return $return; } - private function filterTransactionsByType(TransactionTypeEnum $type, array $transactions, Carbon $start, Carbon $end): array + private function filterTransactionsByType(TransactionTypeEnum $type, Carbon $start, Carbon $end): array { $result = []; @@ -624,12 +628,11 @@ trait PeriodOverview * @var int $index * @var array $item */ - foreach ($transactions as $index => $item) { + foreach ($this->transactions as $item) { $date = Carbon::parse($item['date']); $fits = $item['type'] === $type->value && $date >= $start && $date <= $end; if ($fits) { $result[] = $item; - unset($transactions[$index]); } } @@ -670,7 +673,7 @@ trait PeriodOverview return $return; } - private function filterTransfers(string $direction, array $transactions, Carbon $start, Carbon $end): array + private function filterTransfers(string $direction, Carbon $start, Carbon $end): array { $result = []; @@ -678,7 +681,7 @@ trait PeriodOverview * @var int $index * @var array $item */ - foreach ($transactions as $index => $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')) { @@ -688,8 +691,6 @@ trait PeriodOverview } if ('Transfer' === $item['type'] && 'in' === $direction && 1 === bccomp((string)$item['amount'], '0')) { $result[] = $item; - - continue; } } } @@ -714,13 +715,13 @@ trait PeriodOverview 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'; + $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'; From d61f87f64994b52110d244fc55a39cdbce74b712 Mon Sep 17 00:00:00 2001 From: James Cole Date: Fri, 26 Sep 2025 20:47:44 +0200 Subject: [PATCH 49/58] Fix URL --- app/Support/Http/Controllers/PeriodOverview.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/Support/Http/Controllers/PeriodOverview.php b/app/Support/Http/Controllers/PeriodOverview.php index 8e0d68f206..4081faba0c 100644 --- a/app/Support/Http/Controllers/PeriodOverview.php +++ b/app/Support/Http/Controllers/PeriodOverview.php @@ -330,7 +330,7 @@ trait PeriodOverview $types = ['spent', 'earned', 'transferred_in', 'transferred_away']; $return = [ 'title' => Navigation::periodShow($start, $period), - 'route' => route('accounts.show', [$account->id, $start->format('Y-m-d'), $start->format('Y-m-d')]), + 'route' => route('accounts.show', [$account->id, $start->format('Y-m-d'), $end->format('Y-m-d')]), 'total_transactions' => 0, ]; $this->transactions = []; From eb6f78406e71753ee4b2cee68fb8546e73853d6d Mon Sep 17 00:00:00 2001 From: JC5 Date: Fri, 26 Sep 2025 21:25:46 +0200 Subject: [PATCH 50/58] =?UTF-8?q?=F0=9F=A4=96=20Auto=20commit=20for=20rele?= =?UTF-8?q?ase=20'develop'=20on=202025-09-26?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/Http/Controllers/DebugController.php | 2 +- app/Models/PeriodStatistic.php | 5 +- .../Http/Controllers/PeriodOverview.php | 230 +++++++++--------- .../Report/Budget/BudgetReportGenerator.php | 20 +- config/firefly.php | 2 +- 5 files changed, 129 insertions(+), 130 deletions(-) diff --git a/app/Http/Controllers/DebugController.php b/app/Http/Controllers/DebugController.php index f599788536..377bacd735 100644 --- a/app/Http/Controllers/DebugController.php +++ b/app/Http/Controllers/DebugController.php @@ -109,7 +109,7 @@ class DebugController extends Controller Artisan::call('route:clear'); Artisan::call('view:clear'); - PeriodStatistic::where('id','>',0)->delete(); + PeriodStatistic::where('id', '>', 0)->delete(); // also do some recalculations. Artisan::call('correction:recalculates-liabilities'); diff --git a/app/Models/PeriodStatistic.php b/app/Models/PeriodStatistic.php index af29deb02a..9a167fafe0 100644 --- a/app/Models/PeriodStatistic.php +++ b/app/Models/PeriodStatistic.php @@ -9,7 +9,6 @@ use FireflyIII\Support\Models\ReturnsIntegerUserIdTrait; use Illuminate\Database\Eloquent\Casts\Attribute; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\MorphTo; -use Illuminate\Database\Eloquent\SoftDeletes; class PeriodStatistic extends Model { @@ -20,8 +19,8 @@ class PeriodStatistic extends Model return [ 'created_at' => 'datetime', 'updated_at' => 'datetime', - 'start' => SeparateTimezoneCaster::class, - 'end' => SeparateTimezoneCaster::class, + 'start' => SeparateTimezoneCaster::class, + 'end' => SeparateTimezoneCaster::class, ]; } diff --git a/app/Support/Http/Controllers/PeriodOverview.php b/app/Support/Http/Controllers/PeriodOverview.php index 4081faba0c..b87dda9531 100644 --- a/app/Support/Http/Controllers/PeriodOverview.php +++ b/app/Support/Http/Controllers/PeriodOverview.php @@ -89,12 +89,12 @@ trait PeriodOverview $this->accountRepository = app(AccountRepositoryInterface::class); $this->periodStatisticRepo = app(PeriodStatisticRepositoryInterface::class); $range = Navigation::getViewRange(true); - [$start, $end] = $end < $start ? [$end, $start] : [$start, $end]; + [$start, $end] = $end < $start ? [$end, $start] : [$start, $end]; /** @var array $dates */ - $dates = Navigation::blockPeriods($start, $end, $range); - [$start, $end] = $this->getPeriodFromBlocks($dates, $start, $end); - $this->statistics = $this->periodStatisticRepo->allInRangeForModel($account, $start, $end); + $dates = Navigation::blockPeriods($start, $end, $range); + [$start, $end] = $this->getPeriodFromBlocks($dates, $start, $end); + $this->statistics = $this->periodStatisticRepo->allInRangeForModel($account, $start, $end); // TODO needs to be re-arranged: // get all period stats for entire range. @@ -102,7 +102,7 @@ trait PeriodOverview // create new ones, or use collected. - $entries = []; + $entries = []; Log::debug(sprintf('Count of loops: %d', count($dates))); foreach ($dates as $currentDate) { $entries[] = $this->getSingleAccountPeriod($account, $currentDate['period'], $currentDate['start'], $currentDate['end']); @@ -138,11 +138,11 @@ trait PeriodOverview */ protected function getCategoryPeriodOverview(Category $category, Carbon $start, Carbon $end): array { - $range = Navigation::getViewRange(true); + $range = Navigation::getViewRange(true); [$start, $end] = $end < $start ? [$end, $start] : [$start, $end]; // properties for entries with their amounts. - $cache = new CacheProperties(); + $cache = new CacheProperties(); $cache->addProperty($start); $cache->addProperty($end); $cache->addProperty($range); @@ -154,32 +154,32 @@ trait PeriodOverview } /** @var array $dates */ - $dates = Navigation::blockPeriods($start, $end, $range); - $entries = []; + $dates = Navigation::blockPeriods($start, $end, $range); + $entries = []; // collect all expenses in this period: /** @var GroupCollectorInterface $collector */ - $collector = app(GroupCollectorInterface::class); + $collector = app(GroupCollectorInterface::class); $collector->setCategory($category); $collector->setRange($start, $end); $collector->setTypes([TransactionTypeEnum::DEPOSIT->value]); - $earnedSet = $collector->getExtractedJournals(); + $earnedSet = $collector->getExtractedJournals(); // collect all income in this period: /** @var GroupCollectorInterface $collector */ - $collector = app(GroupCollectorInterface::class); + $collector = app(GroupCollectorInterface::class); $collector->setCategory($category); $collector->setRange($start, $end); $collector->setTypes([TransactionTypeEnum::WITHDRAWAL->value]); - $spentSet = $collector->getExtractedJournals(); + $spentSet = $collector->getExtractedJournals(); // collect all transfers in this period: /** @var GroupCollectorInterface $collector */ - $collector = app(GroupCollectorInterface::class); + $collector = app(GroupCollectorInterface::class); $collector->setCategory($category); $collector->setRange($start, $end); $collector->setTypes([TransactionTypeEnum::TRANSFER->value]); - $transferSet = $collector->getExtractedJournals(); + $transferSet = $collector->getExtractedJournals(); foreach ($dates as $currentDate) { $spent = $this->filterJournalsByDate($spentSet, $currentDate['start'], $currentDate['end']); $earned = $this->filterJournalsByDate($earnedSet, $currentDate['start'], $currentDate['end']); @@ -187,17 +187,17 @@ trait PeriodOverview $title = Navigation::periodShow($currentDate['end'], $currentDate['period']); $entries[] = [ - 'transactions' => 0, - 'title' => $title, - 'route' => route( - 'categories.show', - [$category->id, $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), - ]; + 'transactions' => 0, + 'title' => $title, + 'route' => route( + 'categories.show', + [$category->id, $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), + ]; } $cache->store($entries); @@ -213,11 +213,11 @@ trait PeriodOverview */ protected function getNoBudgetPeriodOverview(Carbon $start, Carbon $end): array { - $range = Navigation::getViewRange(true); + $range = Navigation::getViewRange(true); [$start, $end] = $end < $start ? [$end, $start] : [$start, $end]; - $cache = new CacheProperties(); + $cache = new CacheProperties(); $cache->addProperty($start); $cache->addProperty($end); $cache->addProperty($this->convertToPrimary); @@ -228,28 +228,28 @@ trait PeriodOverview } /** @var array $dates */ - $dates = Navigation::blockPeriods($start, $end, $range); - $entries = []; + $dates = Navigation::blockPeriods($start, $end, $range); + $entries = []; // get all expenses without a budget. /** @var GroupCollectorInterface $collector */ - $collector = app(GroupCollectorInterface::class); + $collector = app(GroupCollectorInterface::class); $collector->setRange($start, $end)->withoutBudget()->withAccountInformation()->setTypes([TransactionTypeEnum::WITHDRAWAL->value]); - $journals = $collector->getExtractedJournals(); + $journals = $collector->getExtractedJournals(); foreach ($dates as $currentDate) { $set = $this->filterJournalsByDate($journals, $currentDate['start'], $currentDate['end']); $title = Navigation::periodShow($currentDate['end'], $currentDate['period']); $entries[] = [ - 'title' => $title, - 'route' => route('budgets.no-budget', [$currentDate['start']->format('Y-m-d'), $currentDate['end']->format('Y-m-d')]), - 'total_transactions' => count($set), - 'spent' => $this->groupByCurrency($set), - 'earned' => [], - 'transferred_away' => [], - 'transferred_in' => [], - ]; + 'title' => $title, + 'route' => route('budgets.no-budget', [$currentDate['start']->format('Y-m-d'), $currentDate['end']->format('Y-m-d')]), + 'total_transactions' => count($set), + 'spent' => $this->groupByCurrency($set), + 'earned' => [], + 'transferred_away' => [], + 'transferred_in' => [], + ]; } $cache->store($entries); @@ -266,38 +266,38 @@ trait PeriodOverview protected function getNoCategoryPeriodOverview(Carbon $theDate): array { 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); + $range = Navigation::getViewRange(true); + $first = $this->journalRepos->firstNull(); + $start = null === $first ? new Carbon() : $first->date; + $end = clone $theDate; + $end = Navigation::endOfPeriod($end, $range); 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); - $entries = []; + $dates = Navigation::blockPeriods($start, $end, $range); + $entries = []; // collect all expenses in this period: /** @var GroupCollectorInterface $collector */ - $collector = app(GroupCollectorInterface::class); + $collector = app(GroupCollectorInterface::class); $collector->withoutCategory(); $collector->setRange($start, $end); $collector->setTypes([TransactionTypeEnum::DEPOSIT->value]); - $earnedSet = $collector->getExtractedJournals(); + $earnedSet = $collector->getExtractedJournals(); // collect all income in this period: /** @var GroupCollectorInterface $collector */ - $collector = app(GroupCollectorInterface::class); + $collector = app(GroupCollectorInterface::class); $collector->withoutCategory(); $collector->setRange($start, $end); $collector->setTypes([TransactionTypeEnum::WITHDRAWAL->value]); - $spentSet = $collector->getExtractedJournals(); + $spentSet = $collector->getExtractedJournals(); // collect all transfers in this period: /** @var GroupCollectorInterface $collector */ - $collector = app(GroupCollectorInterface::class); + $collector = app(GroupCollectorInterface::class); $collector->withoutCategory(); $collector->setRange($start, $end); $collector->setTypes([TransactionTypeEnum::TRANSFER->value]); @@ -311,13 +311,13 @@ trait PeriodOverview $title = Navigation::periodShow($currentDate['end'], $currentDate['period']); $entries[] = [ - 'title' => $title, - 'route' => route('categories.no-category', [$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('categories.no-category', [$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), + ]; } Log::debug('End of loops'); @@ -335,7 +335,7 @@ trait PeriodOverview ]; $this->transactions = []; foreach ($types as $type) { - $set = $this->getSingleAccountPeriodByType($account, $start, $end, $type); + $set = $this->getSingleAccountPeriodByType($account, $start, $end, $type); $return['total_transactions'] += $set['count']; unset($set['count']); $return[$type] = $set; @@ -409,15 +409,15 @@ trait PeriodOverview return $grouped; } - $grouped = [ + $grouped = [ 'count' => 0, ]; /** @var PeriodStatistic $statistic */ foreach ($statistics as $statistic) { - $id = (int)$statistic->transaction_currency_id; - $currency = Amount::getTransactionCurrencyById($id); - $grouped[$id] = [ + $id = (int)$statistic->transaction_currency_id; + $currency = Amount::getTransactionCurrencyById($id); + $grouped[$id] = [ 'amount' => (string)$statistic->amount, 'count' => (int)$statistic->count, 'currency_id' => $currency->id, @@ -439,11 +439,11 @@ trait PeriodOverview */ protected function getTagPeriodOverview(Tag $tag, Carbon $start, Carbon $end): array // period overview for tags. { - $range = Navigation::getViewRange(true); + $range = Navigation::getViewRange(true); [$start, $end] = $end < $start ? [$end, $start] : [$start, $end]; // properties for cache - $cache = new CacheProperties(); + $cache = new CacheProperties(); $cache->addProperty($start); $cache->addProperty($end); $cache->addProperty('tag-period-entries'); @@ -453,37 +453,37 @@ trait PeriodOverview } /** @var array $dates */ - $dates = Navigation::blockPeriods($start, $end, $range); - $entries = []; + $dates = Navigation::blockPeriods($start, $end, $range); + $entries = []; // collect all expenses in this period: /** @var GroupCollectorInterface $collector */ - $collector = app(GroupCollectorInterface::class); + $collector = app(GroupCollectorInterface::class); $collector->setTag($tag); $collector->setRange($start, $end); $collector->setTypes([TransactionTypeEnum::DEPOSIT->value]); - $earnedSet = $collector->getExtractedJournals(); + $earnedSet = $collector->getExtractedJournals(); // collect all income in this period: /** @var GroupCollectorInterface $collector */ - $collector = app(GroupCollectorInterface::class); + $collector = app(GroupCollectorInterface::class); $collector->setTag($tag); $collector->setRange($start, $end); $collector->setTypes([TransactionTypeEnum::WITHDRAWAL->value]); - $spentSet = $collector->getExtractedJournals(); + $spentSet = $collector->getExtractedJournals(); // collect all transfers in this period: /** @var GroupCollectorInterface $collector */ - $collector = app(GroupCollectorInterface::class); + $collector = app(GroupCollectorInterface::class); $collector->setTag($tag); $collector->setRange($start, $end); $collector->setTypes([TransactionTypeEnum::TRANSFER->value]); - $transferSet = $collector->getExtractedJournals(); + $transferSet = $collector->getExtractedJournals(); // filer all of them: - $earnedSet = $this->filterJournalsByTag($earnedSet, $tag); - $spentSet = $this->filterJournalsByTag($spentSet, $tag); - $transferSet = $this->filterJournalsByTag($transferSet, $tag); + $earnedSet = $this->filterJournalsByTag($earnedSet, $tag); + $spentSet = $this->filterJournalsByTag($spentSet, $tag); + $transferSet = $this->filterJournalsByTag($transferSet, $tag); foreach ($dates as $currentDate) { $spent = $this->filterJournalsByDate($spentSet, $currentDate['start'], $currentDate['end']); @@ -492,17 +492,17 @@ trait PeriodOverview $title = Navigation::periodShow($currentDate['end'], $currentDate['period']); $entries[] = [ - 'transactions' => 0, - 'title' => $title, - 'route' => route( - 'tags.show', - [$tag->id, $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), - ]; + 'transactions' => 0, + 'title' => $title, + 'route' => route( + 'tags.show', + [$tag->id, $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), + ]; } return $entries; @@ -513,12 +513,12 @@ trait PeriodOverview */ protected function getTransactionPeriodOverview(string $transactionType, Carbon $start, Carbon $end): array { - $range = Navigation::getViewRange(true); - $types = config(sprintf('firefly.transactionTypesByType.%s', $transactionType)); + $range = Navigation::getViewRange(true); + $types = config(sprintf('firefly.transactionTypesByType.%s', $transactionType)); [$start, $end] = $end < $start ? [$end, $start] : [$start, $end]; // properties for cache - $cache = new CacheProperties(); + $cache = new CacheProperties(); $cache->addProperty($start); $cache->addProperty($end); $cache->addProperty('transactions-period-entries'); @@ -528,16 +528,16 @@ trait PeriodOverview } /** @var array $dates */ - $dates = Navigation::blockPeriods($start, $end, $range); - $entries = []; - $spent = []; - $earned = []; - $transferred = []; + $dates = Navigation::blockPeriods($start, $end, $range); + $entries = []; + $spent = []; + $earned = []; + $transferred = []; // collect all journals in this period (regardless of type) - $collector = app(GroupCollectorInterface::class); + $collector = app(GroupCollectorInterface::class); $collector->setTypes($types)->setRange($start, $end); - $genericSet = $collector->getExtractedJournals(); - $loops = 0; + $genericSet = $collector->getExtractedJournals(); + $loops = 0; foreach ($dates as $currentDate) { $title = Navigation::periodShow($currentDate['end'], $currentDate['period']); @@ -555,14 +555,14 @@ 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; } @@ -603,7 +603,7 @@ trait PeriodOverview { $return = []; foreach ($set as $entry) { - $found = false; + $found = false; /** @var array $localTag */ foreach ($entry['tags'] as $localTag) { @@ -715,13 +715,13 @@ trait PeriodOverview 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'; + $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'; diff --git a/app/Support/Report/Budget/BudgetReportGenerator.php b/app/Support/Report/Budget/BudgetReportGenerator.php index 2fd81217bb..4de1c9d9a6 100644 --- a/app/Support/Report/Budget/BudgetReportGenerator.php +++ b/app/Support/Report/Budget/BudgetReportGenerator.php @@ -334,16 +334,16 @@ class BudgetReportGenerator // make sum information: $this->report['sums'][$currencyId] ??= [ - 'budgeted' => '0', - 'spent' => '0', - 'left' => '0', - 'overspent' => '0', - 'currency_id' => $currencyId, - 'currency_code' => $limitCurrency->code, - 'currency_name' => $limitCurrency->name, - 'currency_symbol' => $limitCurrency->symbol, - 'currency_decimal_places' => $limitCurrency->decimal_places, - ]; + 'budgeted' => '0', + 'spent' => '0', + 'left' => '0', + 'overspent' => '0', + 'currency_id' => $currencyId, + 'currency_code' => $limitCurrency->code, + 'currency_name' => $limitCurrency->name, + '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)); diff --git a/config/firefly.php b/config/firefly.php index cf0424ddc6..c990ec4a8e 100644 --- a/config/firefly.php +++ b/config/firefly.php @@ -79,7 +79,7 @@ return [ // see cer.php for exchange rates feature flag. ], 'version' => 'develop/2025-09-26', - 'build_time' => 1758908498, + 'build_time' => 1758914637, 'api_version' => '2.1.0', // field is no longer used. 'db_version' => 27, From c54da6200599d26b842a263457c8ba74d7b7359e Mon Sep 17 00:00:00 2001 From: James Cole Date: Sat, 27 Sep 2025 05:31:26 +0200 Subject: [PATCH 51/58] Enable period statistics for category. --- app/Models/Category.php | 5 + .../Category/CategoryRepository.php | 39 ++++++ .../Category/CategoryRepositoryInterface.php | 2 + .../Http/Controllers/PeriodOverview.php | 129 +++++++++++++++--- 4 files changed, 158 insertions(+), 17 deletions(-) diff --git a/app/Models/Category.php b/app/Models/Category.php index 018fbdbef4..55c9c7ebcf 100644 --- a/app/Models/Category.php +++ b/app/Models/Category.php @@ -109,4 +109,9 @@ class Category extends Model 'user_group_id' => 'integer', ]; } + + public function primaryPeriodStatistics(): MorphMany + { + return $this->morphMany(PeriodStatistic::class, 'primary_statable'); + } } diff --git a/app/Repositories/Category/CategoryRepository.php b/app/Repositories/Category/CategoryRepository.php index 487a467c20..f644c57070 100644 --- a/app/Repositories/Category/CategoryRepository.php +++ b/app/Repositories/Category/CategoryRepository.php @@ -358,4 +358,43 @@ class CategoryRepository implements CategoryRepositoryInterface, UserGroupInterf return $service->update($category, $data); } + + public function periodCollection(Category $category, Carbon $start, Carbon $end): array + { + Log::debug(sprintf('periodCollection(#%d, %s, %s)', $category->id, $start->format('Y-m-d'), $end->format('Y-m-d'))); + + return $category->transactionJournals() + ->leftJoin('transactions','transactions.transaction_journal_id', '=', 'transaction_journals.id') + ->leftJoin('transaction_types', 'transaction_types.id', '=', 'transaction_journals.transaction_type_id') + ->leftJoin('transaction_currencies', 'transaction_currencies.id', '=', 'transactions.transaction_currency_id') + ->leftJoin('transaction_currencies as foreign_currencies', 'foreign_currencies.id', '=', 'transactions.foreign_currency_id') + ->where('transaction_journals.date', '>=', $start) + ->where('transaction_journals.date', '<=', $end) + ->where('transactions.amount', '>', 0) + ->get([ + // currencies + 'transaction_currencies.id as currency_id', + 'transaction_currencies.code as currency_code', + 'transaction_currencies.name as currency_name', + 'transaction_currencies.symbol as currency_symbol', + 'transaction_currencies.decimal_places as currency_decimal_places', + + // foreign + 'foreign_currencies.id as foreign_currency_id', + 'foreign_currencies.code as foreign_currency_code', + 'foreign_currencies.name as foreign_currency_name', + 'foreign_currencies.symbol as foreign_currency_symbol', + 'foreign_currencies.decimal_places as foreign_currency_decimal_places', + + // fields + 'transaction_journals.date', + 'transaction_types.type', + 'transaction_journals.transaction_currency_id', + 'transactions.amount', + 'transactions.native_amount as pc_amount', + 'transactions.foreign_amount', + ]) + ->toArray() + ; + } } diff --git a/app/Repositories/Category/CategoryRepositoryInterface.php b/app/Repositories/Category/CategoryRepositoryInterface.php index 263c11c716..cef58d2d17 100644 --- a/app/Repositories/Category/CategoryRepositoryInterface.php +++ b/app/Repositories/Category/CategoryRepositoryInterface.php @@ -48,6 +48,8 @@ interface CategoryRepositoryInterface public function categoryStartsWith(string $query, int $limit): Collection; + public function periodCollection(Category $category, Carbon $start, Carbon $end): array; + public function destroy(Category $category): bool; /** diff --git a/app/Support/Http/Controllers/PeriodOverview.php b/app/Support/Http/Controllers/PeriodOverview.php index 4081faba0c..1995bc15c8 100644 --- a/app/Support/Http/Controllers/PeriodOverview.php +++ b/app/Support/Http/Controllers/PeriodOverview.php @@ -27,17 +27,20 @@ namespace FireflyIII\Support\Http\Controllers; use Carbon\Carbon; use FireflyIII\Enums\TransactionTypeEnum; use FireflyIII\Exceptions\FireflyException; +use FireflyIII\Generator\Report\Category\YearReportGenerator; 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\Category\CategoryRepositoryInterface; use FireflyIII\Repositories\Journal\JournalRepositoryInterface; use FireflyIII\Repositories\PeriodStatistic\PeriodStatisticRepositoryInterface; use FireflyIII\Support\CacheProperties; use FireflyIII\Support\Facades\Amount; use FireflyIII\Support\Facades\Navigation; +use Illuminate\Database\Eloquent\Model; use Illuminate\Support\Collection; use Illuminate\Support\Facades\Log; @@ -71,6 +74,7 @@ use Illuminate\Support\Facades\Log; trait PeriodOverview { protected AccountRepositoryInterface $accountRepository; + protected CategoryRepositoryInterface $categoryRepository; protected JournalRepositoryInterface $journalRepos; protected PeriodStatisticRepositoryInterface $periodStatisticRepo; private Collection $statistics; // temp data holder @@ -87,6 +91,7 @@ trait PeriodOverview { 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->accountRepository->setUser($account->user); $this->periodStatisticRepo = app(PeriodStatisticRepositoryInterface::class); $range = Navigation::getViewRange(true); [$start, $end] = $end < $start ? [$end, $start] : [$start, $end]; @@ -138,24 +143,27 @@ trait PeriodOverview */ protected function getCategoryPeriodOverview(Category $category, Carbon $start, Carbon $end): array { + $this->categoryRepository = app(CategoryRepositoryInterface::class); + $this->categoryRepository->setUser($category->user); + $this->periodStatisticRepo = app(PeriodStatisticRepositoryInterface::class); + $range = Navigation::getViewRange(true); [$start, $end] = $end < $start ? [$end, $start] : [$start, $end]; - // properties for entries with their amounts. - $cache = new CacheProperties(); - $cache->addProperty($start); - $cache->addProperty($end); - $cache->addProperty($range); - $cache->addProperty('category-show-period-entries'); - $cache->addProperty($category->id); - - if ($cache->has()) { - return $cache->get(); - } - /** @var array $dates */ $dates = Navigation::blockPeriods($start, $end, $range); $entries = []; + [$start, $end] = $this->getPeriodFromBlocks($dates, $start, $end); + $this->statistics = $this->periodStatisticRepo->allInRangeForModel($category, $start, $end); + + + Log::debug(sprintf('Count of loops: %d', count($dates))); + foreach ($dates as $currentDate) { + $entries[] = $this->getSingleCategoryPeriod($category, $currentDate['period'], $currentDate['start'], $currentDate['end']); + } + + return $entries; + // collect all expenses in this period: /** @var GroupCollectorInterface $collector */ @@ -199,7 +207,6 @@ trait PeriodOverview 'transferred' => $this->groupByCurrency($transferred), ]; } - $cache->store($entries); return $entries; } @@ -324,6 +331,7 @@ trait PeriodOverview 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'))); @@ -344,6 +352,26 @@ trait PeriodOverview return $return; } + protected function getSingleCategoryPeriod(Category $category, string $period, Carbon $start, Carbon $end): array + { + Log::debug(sprintf('Now in getSingleCategoryPeriod(#%d, %s %s)', $category->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('categories.show', [$category->id, $start->format('Y-m-d'), $end->format('Y-m-d')]), + 'total_transactions' => 0, + ]; + $this->transactions = []; + foreach ($types as $type) { + $set = $this->getSingleCategoryPeriodByType($category, $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( @@ -432,6 +460,73 @@ trait PeriodOverview return $grouped; } + protected function getSingleCategoryPeriodByType(Category $category, Carbon $start, Carbon $end, string $type): array + { + Log::debug(sprintf('Now in getSingleCategoryPeriodByType(#%d, %s %s, %s)', $category->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->categoryRepository->periodCollection($category, $start, $end); + } + + switch ($type) { + default: + throw new FireflyException(sprintf('Cannot deal with category 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($category, $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. * @@ -569,16 +664,16 @@ trait PeriodOverview return $entries; } - protected function saveGroupedAsStatistics(Account $account, Carbon $start, Carbon $end, string $type, array $array): void + protected function saveGroupedAsStatistics(Model $model, 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))); + Log::debug(sprintf('saveGroupedAsStatistics(%s #%d, %s, %s, "%s", array(%d))',get_class($model), $model->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']); + $this->periodStatisticRepo->saveStatistic($model, $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'); + $this->periodStatisticRepo->saveStatistic($model, $this->primaryCurrency->id, $start, $end, $type, 0, '0'); } } From 79f2d7021122de653f4c401b4e87e6601c3c6b3d Mon Sep 17 00:00:00 2001 From: James Cole Date: Sat, 27 Sep 2025 05:35:20 +0200 Subject: [PATCH 52/58] Fix #10974 --- app/Handlers/Events/WebhookEventHandler.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/app/Handlers/Events/WebhookEventHandler.php b/app/Handlers/Events/WebhookEventHandler.php index adad80b942..bab7d51363 100644 --- a/app/Handlers/Events/WebhookEventHandler.php +++ b/app/Handlers/Events/WebhookEventHandler.php @@ -62,5 +62,8 @@ class WebhookEventHandler Log::debug(sprintf('Skip message #%d', $message->id)); } } + + // clean up sent messages table: + WebhookMessage::where('webhook_messages.sent', true)->where('webhook_messages.created_at', '<', now()->subDays(30))->delete(); } } From 0f0a28c3d9d03221509da878aaa7678f1a4ab412 Mon Sep 17 00:00:00 2001 From: James Cole Date: Sat, 27 Sep 2025 05:49:22 +0200 Subject: [PATCH 53/58] Add cached periods for tags. --- app/Models/Tag.php | 5 + app/Repositories/Tag/TagRepository.php | 40 ++ .../Tag/TagRepositoryInterface.php | 1 + .../Http/Controllers/PeriodOverview.php | 373 ++++++++++-------- 4 files changed, 260 insertions(+), 159 deletions(-) diff --git a/app/Models/Tag.php b/app/Models/Tag.php index c9fc2f134b..f348bea48a 100644 --- a/app/Models/Tag.php +++ b/app/Models/Tag.php @@ -104,4 +104,9 @@ class Tag extends Model 'user_group_id' => 'integer', ]; } + + public function primaryPeriodStatistics(): MorphMany + { + return $this->morphMany(PeriodStatistic::class, 'primary_statable'); + } } diff --git a/app/Repositories/Tag/TagRepository.php b/app/Repositories/Tag/TagRepository.php index 58c998e760..06a41da184 100644 --- a/app/Repositories/Tag/TagRepository.php +++ b/app/Repositories/Tag/TagRepository.php @@ -379,4 +379,44 @@ class TagRepository implements TagRepositoryInterface, UserGroupInterface /** @var null|Location */ return $tag->locations()->first(); } + + #[\Override] + public function periodCollection(Tag $tag, Carbon $start, Carbon $end): array + { + Log::debug(sprintf('periodCollection(#%d, %s, %s)', $tag->id, $start->format('Y-m-d'), $end->format('Y-m-d'))); + + return $tag->transactionJournals() + ->leftJoin('transactions','transactions.transaction_journal_id', '=', 'transaction_journals.id') + ->leftJoin('transaction_types', 'transaction_types.id', '=', 'transaction_journals.transaction_type_id') + ->leftJoin('transaction_currencies', 'transaction_currencies.id', '=', 'transactions.transaction_currency_id') + ->leftJoin('transaction_currencies as foreign_currencies', 'foreign_currencies.id', '=', 'transactions.foreign_currency_id') + ->where('transaction_journals.date', '>=', $start) + ->where('transaction_journals.date', '<=', $end) + ->where('transactions.amount', '>', 0) + ->get([ + // currencies + 'transaction_currencies.id as currency_id', + 'transaction_currencies.code as currency_code', + 'transaction_currencies.name as currency_name', + 'transaction_currencies.symbol as currency_symbol', + 'transaction_currencies.decimal_places as currency_decimal_places', + + // foreign + 'foreign_currencies.id as foreign_currency_id', + 'foreign_currencies.code as foreign_currency_code', + 'foreign_currencies.name as foreign_currency_name', + 'foreign_currencies.symbol as foreign_currency_symbol', + 'foreign_currencies.decimal_places as foreign_currency_decimal_places', + + // fields + 'transaction_journals.date', + 'transaction_types.type', + 'transaction_journals.transaction_currency_id', + 'transactions.amount', + 'transactions.native_amount as pc_amount', + 'transactions.foreign_amount', + ]) + ->toArray() + ; + } } diff --git a/app/Repositories/Tag/TagRepositoryInterface.php b/app/Repositories/Tag/TagRepositoryInterface.php index 2052dff88c..18e45fb9ce 100644 --- a/app/Repositories/Tag/TagRepositoryInterface.php +++ b/app/Repositories/Tag/TagRepositoryInterface.php @@ -50,6 +50,7 @@ interface TagRepositoryInterface * This method destroys a tag. */ public function destroy(Tag $tag): bool; + public function periodCollection(Tag $tag, Carbon $start, Carbon $end): array; /** * Destroy all tags. diff --git a/app/Support/Http/Controllers/PeriodOverview.php b/app/Support/Http/Controllers/PeriodOverview.php index 9cdf54d6e3..3b9a708d88 100644 --- a/app/Support/Http/Controllers/PeriodOverview.php +++ b/app/Support/Http/Controllers/PeriodOverview.php @@ -27,7 +27,6 @@ namespace FireflyIII\Support\Http\Controllers; use Carbon\Carbon; use FireflyIII\Enums\TransactionTypeEnum; use FireflyIII\Exceptions\FireflyException; -use FireflyIII\Generator\Report\Category\YearReportGenerator; use FireflyIII\Helpers\Collector\GroupCollectorInterface; use FireflyIII\Models\Account; use FireflyIII\Models\Category; @@ -37,9 +36,11 @@ use FireflyIII\Repositories\Account\AccountRepositoryInterface; use FireflyIII\Repositories\Category\CategoryRepositoryInterface; use FireflyIII\Repositories\Journal\JournalRepositoryInterface; use FireflyIII\Repositories\PeriodStatistic\PeriodStatisticRepositoryInterface; +use FireflyIII\Repositories\Tag\TagRepositoryInterface; use FireflyIII\Support\CacheProperties; use FireflyIII\Support\Facades\Amount; use FireflyIII\Support\Facades\Navigation; +use FireflyIII\Support\Facades\Steam; use Illuminate\Database\Eloquent\Model; use Illuminate\Support\Collection; use Illuminate\Support\Facades\Log; @@ -74,7 +75,8 @@ use Illuminate\Support\Facades\Log; trait PeriodOverview { protected AccountRepositoryInterface $accountRepository; - protected CategoryRepositoryInterface $categoryRepository; + protected CategoryRepositoryInterface $categoryRepository; + protected TagRepositoryInterface $tagRepository; protected JournalRepositoryInterface $journalRepos; protected PeriodStatisticRepositoryInterface $periodStatisticRepo; private Collection $statistics; // temp data holder @@ -90,16 +92,16 @@ trait PeriodOverview protected function getAccountPeriodOverview(Account $account, Carbon $start, Carbon $end): array { 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->accountRepository = app(AccountRepositoryInterface::class); $this->accountRepository->setUser($account->user); $this->periodStatisticRepo = app(PeriodStatisticRepositoryInterface::class); $range = Navigation::getViewRange(true); - [$start, $end] = $end < $start ? [$end, $start] : [$start, $end]; + [$start, $end] = $end < $start ? [$end, $start] : [$start, $end]; /** @var array $dates */ - $dates = Navigation::blockPeriods($start, $end, $range); - [$start, $end] = $this->getPeriodFromBlocks($dates, $start, $end); - $this->statistics = $this->periodStatisticRepo->allInRangeForModel($account, $start, $end); + $dates = Navigation::blockPeriods($start, $end, $range); + [$start, $end] = $this->getPeriodFromBlocks($dates, $start, $end); + $this->statistics = $this->periodStatisticRepo->allInRangeForModel($account, $start, $end); // TODO needs to be re-arranged: // get all period stats for entire range. @@ -107,7 +109,7 @@ trait PeriodOverview // create new ones, or use collected. - $entries = []; + $entries = []; Log::debug(sprintf('Count of loops: %d', count($dates))); foreach ($dates as $currentDate) { $entries[] = $this->getSingleAccountPeriod($account, $currentDate['period'], $currentDate['start'], $currentDate['end']); @@ -143,7 +145,7 @@ trait PeriodOverview */ protected function getCategoryPeriodOverview(Category $category, Carbon $start, Carbon $end): array { - $this->categoryRepository = app(CategoryRepositoryInterface::class); + $this->categoryRepository = app(CategoryRepositoryInterface::class); $this->categoryRepository->setUser($category->user); $this->periodStatisticRepo = app(PeriodStatisticRepositoryInterface::class); @@ -163,51 +165,6 @@ trait PeriodOverview } return $entries; - - // collect all expenses in this period: - /** @var GroupCollectorInterface $collector */ - $collector = app(GroupCollectorInterface::class); - $collector->setCategory($category); - $collector->setRange($start, $end); - $collector->setTypes([TransactionTypeEnum::DEPOSIT->value]); - $earnedSet = $collector->getExtractedJournals(); - - // collect all income in this period: - /** @var GroupCollectorInterface $collector */ - $collector = app(GroupCollectorInterface::class); - $collector->setCategory($category); - $collector->setRange($start, $end); - $collector->setTypes([TransactionTypeEnum::WITHDRAWAL->value]); - $spentSet = $collector->getExtractedJournals(); - - // collect all transfers in this period: - /** @var GroupCollectorInterface $collector */ - $collector = app(GroupCollectorInterface::class); - $collector->setCategory($category); - $collector->setRange($start, $end); - $collector->setTypes([TransactionTypeEnum::TRANSFER->value]); - $transferSet = $collector->getExtractedJournals(); - foreach ($dates as $currentDate) { - $spent = $this->filterJournalsByDate($spentSet, $currentDate['start'], $currentDate['end']); - $earned = $this->filterJournalsByDate($earnedSet, $currentDate['start'], $currentDate['end']); - $transferred = $this->filterJournalsByDate($transferSet, $currentDate['start'], $currentDate['end']); - $title = Navigation::periodShow($currentDate['end'], $currentDate['period']); - $entries[] - = [ - 'transactions' => 0, - 'title' => $title, - 'route' => route( - 'categories.show', - [$category->id, $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), - ]; - } - - return $entries; } /** @@ -219,11 +176,11 @@ trait PeriodOverview */ protected function getNoBudgetPeriodOverview(Carbon $start, Carbon $end): array { - $range = Navigation::getViewRange(true); + $range = Navigation::getViewRange(true); [$start, $end] = $end < $start ? [$end, $start] : [$start, $end]; - $cache = new CacheProperties(); + $cache = new CacheProperties(); $cache->addProperty($start); $cache->addProperty($end); $cache->addProperty($this->convertToPrimary); @@ -234,28 +191,28 @@ trait PeriodOverview } /** @var array $dates */ - $dates = Navigation::blockPeriods($start, $end, $range); - $entries = []; + $dates = Navigation::blockPeriods($start, $end, $range); + $entries = []; // get all expenses without a budget. /** @var GroupCollectorInterface $collector */ - $collector = app(GroupCollectorInterface::class); + $collector = app(GroupCollectorInterface::class); $collector->setRange($start, $end)->withoutBudget()->withAccountInformation()->setTypes([TransactionTypeEnum::WITHDRAWAL->value]); - $journals = $collector->getExtractedJournals(); + $journals = $collector->getExtractedJournals(); foreach ($dates as $currentDate) { $set = $this->filterJournalsByDate($journals, $currentDate['start'], $currentDate['end']); $title = Navigation::periodShow($currentDate['end'], $currentDate['period']); $entries[] = [ - 'title' => $title, - 'route' => route('budgets.no-budget', [$currentDate['start']->format('Y-m-d'), $currentDate['end']->format('Y-m-d')]), - 'total_transactions' => count($set), - 'spent' => $this->groupByCurrency($set), - 'earned' => [], - 'transferred_away' => [], - 'transferred_in' => [], - ]; + 'title' => $title, + 'route' => route('budgets.no-budget', [$currentDate['start']->format('Y-m-d'), $currentDate['end']->format('Y-m-d')]), + 'total_transactions' => count($set), + 'spent' => $this->groupByCurrency($set), + 'earned' => [], + 'transferred_away' => [], + 'transferred_in' => [], + ]; } $cache->store($entries); @@ -272,38 +229,38 @@ trait PeriodOverview protected function getNoCategoryPeriodOverview(Carbon $theDate): array { 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); + $range = Navigation::getViewRange(true); + $first = $this->journalRepos->firstNull(); + $start = null === $first ? new Carbon() : $first->date; + $end = clone $theDate; + $end = Navigation::endOfPeriod($end, $range); 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); - $entries = []; + $dates = Navigation::blockPeriods($start, $end, $range); + $entries = []; // collect all expenses in this period: /** @var GroupCollectorInterface $collector */ - $collector = app(GroupCollectorInterface::class); + $collector = app(GroupCollectorInterface::class); $collector->withoutCategory(); $collector->setRange($start, $end); $collector->setTypes([TransactionTypeEnum::DEPOSIT->value]); - $earnedSet = $collector->getExtractedJournals(); + $earnedSet = $collector->getExtractedJournals(); // collect all income in this period: /** @var GroupCollectorInterface $collector */ - $collector = app(GroupCollectorInterface::class); + $collector = app(GroupCollectorInterface::class); $collector->withoutCategory(); $collector->setRange($start, $end); $collector->setTypes([TransactionTypeEnum::WITHDRAWAL->value]); - $spentSet = $collector->getExtractedJournals(); + $spentSet = $collector->getExtractedJournals(); // collect all transfers in this period: /** @var GroupCollectorInterface $collector */ - $collector = app(GroupCollectorInterface::class); + $collector = app(GroupCollectorInterface::class); $collector->withoutCategory(); $collector->setRange($start, $end); $collector->setTypes([TransactionTypeEnum::TRANSFER->value]); @@ -317,13 +274,13 @@ trait PeriodOverview $title = Navigation::periodShow($currentDate['end'], $currentDate['period']); $entries[] = [ - 'title' => $title, - 'route' => route('categories.no-category', [$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('categories.no-category', [$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), + ]; } Log::debug('End of loops'); @@ -342,7 +299,7 @@ trait PeriodOverview ]; $this->transactions = []; foreach ($types as $type) { - $set = $this->getSingleAccountPeriodByType($account, $start, $end, $type); + $set = $this->getSingleAccountPeriodByType($account, $start, $end, $type); $return['total_transactions'] += $set['count']; unset($set['count']); $return[$type] = $set; @@ -371,6 +328,26 @@ trait PeriodOverview return $return; } + protected function getSingleTagPeriod(Tag $tag, string $period, Carbon $start, Carbon $end): array + { + Log::debug(sprintf('Now in getSingleTagPeriod(#%d, %s %s)', $tag->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('tags.show', [$tag->id, $start->format('Y-m-d'), $end->format('Y-m-d')]), + 'total_transactions' => 0, + ]; + $this->transactions = []; + foreach ($types as $type) { + $set = $this->getSingleTagPeriodByType($tag, $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( @@ -436,15 +413,15 @@ trait PeriodOverview return $grouped; } - $grouped = [ + $grouped = [ 'count' => 0, ]; /** @var PeriodStatistic $statistic */ foreach ($statistics as $statistic) { - $id = (int)$statistic->transaction_currency_id; - $currency = Amount::getTransactionCurrencyById($id); - $grouped[$id] = [ + $id = (int)$statistic->transaction_currency_id; + $currency = Amount::getTransactionCurrencyById($id); + $grouped[$id] = [ 'amount' => (string)$statistic->amount, 'count' => (int)$statistic->count, 'currency_id' => $currency->id, @@ -476,6 +453,7 @@ trait PeriodOverview throw new FireflyException(sprintf('Cannot deal with category period type %s', $type)); case 'spent': + $result = $this->filterTransactionsByType(TransactionTypeEnum::WITHDRAWAL, $start, $end); break; @@ -526,6 +504,74 @@ trait PeriodOverview return $grouped; } + protected function getSingleTagPeriodByType(Tag $tag, Carbon $start, Carbon $end, string $type): array + { + Log::debug(sprintf('Now in getSingleTagPeriodByType(#%d, %s %s, %s)', $tag->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->tagRepository->periodCollection($tag, $start, $end); + } + + switch ($type) { + default: + throw new FireflyException(sprintf('Cannot deal with tag 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($tag, $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. * @@ -533,51 +579,63 @@ trait PeriodOverview */ protected function getTagPeriodOverview(Tag $tag, Carbon $start, Carbon $end): array // period overview for tags. { - $range = Navigation::getViewRange(true); + $this->tagRepository = app(TagRepositoryInterface::class); + $this->tagRepository->setUser($tag->user); + $this->periodStatisticRepo = app(PeriodStatisticRepositoryInterface::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('tag-period-entries'); - $cache->addProperty($tag->id); - if ($cache->has()) { - return $cache->get(); + /** @var array $dates */ + $dates = Navigation::blockPeriods($start, $end, $range); + $entries = []; + [$start, $end] = $this->getPeriodFromBlocks($dates, $start, $end); + $this->statistics = $this->periodStatisticRepo->allInRangeForModel($tag, $start, $end); + + + Log::debug(sprintf('Count of loops: %d', count($dates))); + foreach ($dates as $currentDate) { + $entries[] = $this->getSingleTagPeriod($tag, $currentDate['period'], $currentDate['start'], $currentDate['end']); } + return $entries; + + + $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); + $entries = []; // collect all expenses in this period: /** @var GroupCollectorInterface $collector */ - $collector = app(GroupCollectorInterface::class); + $collector = app(GroupCollectorInterface::class); $collector->setTag($tag); $collector->setRange($start, $end); $collector->setTypes([TransactionTypeEnum::DEPOSIT->value]); - $earnedSet = $collector->getExtractedJournals(); + $earnedSet = $collector->getExtractedJournals(); // collect all income in this period: /** @var GroupCollectorInterface $collector */ - $collector = app(GroupCollectorInterface::class); + $collector = app(GroupCollectorInterface::class); $collector->setTag($tag); $collector->setRange($start, $end); $collector->setTypes([TransactionTypeEnum::WITHDRAWAL->value]); - $spentSet = $collector->getExtractedJournals(); + $spentSet = $collector->getExtractedJournals(); // collect all transfers in this period: /** @var GroupCollectorInterface $collector */ - $collector = app(GroupCollectorInterface::class); + $collector = app(GroupCollectorInterface::class); $collector->setTag($tag); $collector->setRange($start, $end); $collector->setTypes([TransactionTypeEnum::TRANSFER->value]); - $transferSet = $collector->getExtractedJournals(); + $transferSet = $collector->getExtractedJournals(); // filer all of them: - $earnedSet = $this->filterJournalsByTag($earnedSet, $tag); - $spentSet = $this->filterJournalsByTag($spentSet, $tag); - $transferSet = $this->filterJournalsByTag($transferSet, $tag); + $earnedSet = $this->filterJournalsByTag($earnedSet, $tag); + $spentSet = $this->filterJournalsByTag($spentSet, $tag); + $transferSet = $this->filterJournalsByTag($transferSet, $tag); foreach ($dates as $currentDate) { $spent = $this->filterJournalsByDate($spentSet, $currentDate['start'], $currentDate['end']); @@ -586,17 +644,17 @@ trait PeriodOverview $title = Navigation::periodShow($currentDate['end'], $currentDate['period']); $entries[] = [ - 'transactions' => 0, - 'title' => $title, - 'route' => route( - 'tags.show', - [$tag->id, $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), - ]; + 'transactions' => 0, + 'title' => $title, + 'route' => route( + 'tags.show', + [$tag->id, $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), + ]; } return $entries; @@ -607,12 +665,12 @@ trait PeriodOverview */ protected function getTransactionPeriodOverview(string $transactionType, Carbon $start, Carbon $end): array { - $range = Navigation::getViewRange(true); - $types = config(sprintf('firefly.transactionTypesByType.%s', $transactionType)); + $range = Navigation::getViewRange(true); + $types = config(sprintf('firefly.transactionTypesByType.%s', $transactionType)); [$start, $end] = $end < $start ? [$end, $start] : [$start, $end]; // properties for cache - $cache = new CacheProperties(); + $cache = new CacheProperties(); $cache->addProperty($start); $cache->addProperty($end); $cache->addProperty('transactions-period-entries'); @@ -622,16 +680,16 @@ trait PeriodOverview } /** @var array $dates */ - $dates = Navigation::blockPeriods($start, $end, $range); - $entries = []; - $spent = []; - $earned = []; - $transferred = []; + $dates = Navigation::blockPeriods($start, $end, $range); + $entries = []; + $spent = []; + $earned = []; + $transferred = []; // collect all journals in this period (regardless of type) - $collector = app(GroupCollectorInterface::class); + $collector = app(GroupCollectorInterface::class); $collector->setTypes($types)->setRange($start, $end); - $genericSet = $collector->getExtractedJournals(); - $loops = 0; + $genericSet = $collector->getExtractedJournals(); + $loops = 0; foreach ($dates as $currentDate) { $title = Navigation::periodShow($currentDate['end'], $currentDate['period']); @@ -649,14 +707,14 @@ 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; } @@ -666,7 +724,7 @@ trait PeriodOverview protected function saveGroupedAsStatistics(Model $model, Carbon $start, Carbon $end, string $type, array $array): void { unset($array['count']); - Log::debug(sprintf('saveGroupedAsStatistics(%s #%d, %s, %s, "%s", array(%d))',get_class($model), $model->id, $start->format('Y-m-d'), $end->format('Y-m-d'), $type, count($array))); + Log::debug(sprintf('saveGroupedAsStatistics(%s #%d, %s, %s, "%s", array(%d))', get_class($model), $model->id, $start->format('Y-m-d'), $end->format('Y-m-d'), $type, count($array))); foreach ($array as $entry) { $this->periodStatisticRepo->saveStatistic($model, $entry['currency_id'], $start, $end, $type, $entry['count'], $entry['amount']); } @@ -697,7 +755,7 @@ trait PeriodOverview { $return = []; foreach ($set as $entry) { - $found = false; + $found = false; /** @var array $localTag */ foreach ($entry['tags'] as $localTag) { @@ -726,6 +784,11 @@ trait PeriodOverview $date = Carbon::parse($item['date']); $fits = $item['type'] === $type->value && $date >= $start && $date <= $end; if ($fits) { + + // if type is withdrawal, negative amount: + if (TransactionTypeEnum::WITHDRAWAL === $type && 1 === bccomp((string)$item['amount'], '0')) { + $item['amount'] = Steam::negative($item['amount']); + } $result[] = $item; } } @@ -801,21 +864,13 @@ trait PeriodOverview /** @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'; + $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'; From ac0113e445c3b9754d41fae333055613742e6ac6 Mon Sep 17 00:00:00 2001 From: James Cole Date: Sat, 27 Sep 2025 05:57:58 +0200 Subject: [PATCH 54/58] Fix some rector code. --- .../Autocomplete/PiggyBankController.php | 7 ++++-- .../DestroyController.php | 2 +- .../CurrencyExchangeRate/ShowController.php | 2 +- .../CurrencyExchangeRate/UpdateController.php | 2 +- app/Casts/SeparateTimezoneCaster.php | 1 - app/Console/Commands/Tools/ApplyRules.php | 3 ++- .../Transaction/CreateController.php | 8 ++++--- .../Category/OperationsRepository.php | 4 +--- .../PeriodStatisticRepository.php | 2 +- .../PiggyBank/ModifiesPiggyBanks.php | 16 +++++++------- app/Repositories/Tag/TagRepository.php | 3 ++- .../Support/CreditRecalculateService.php | 22 +++++++++---------- app/Support/Debug/Timer.php | 2 +- .../Http/Controllers/PeriodOverview.php | 4 ++-- .../Enrichments/CategoryEnrichment.php | 2 +- .../Enrichments/PiggyBankEnrichment.php | 10 ++++----- .../RecalculatesAvailableBudgetsTrait.php | 2 +- .../Summarizer/TransactionSummarizer.php | 4 ++-- .../Singleton/PreferencesSingleton.php | 2 +- app/Support/Steam.php | 2 +- .../PiggyBankEventTransformer.php | 2 +- 21 files changed, 53 insertions(+), 49 deletions(-) diff --git a/app/Api/V1/Controllers/Autocomplete/PiggyBankController.php b/app/Api/V1/Controllers/Autocomplete/PiggyBankController.php index de08a181b5..25fc1f145e 100644 --- a/app/Api/V1/Controllers/Autocomplete/PiggyBankController.php +++ b/app/Api/V1/Controllers/Autocomplete/PiggyBankController.php @@ -28,8 +28,10 @@ use FireflyIII\Api\V1\Controllers\Controller; use FireflyIII\Api\V1\Requests\Autocomplete\AutocompleteRequest; use FireflyIII\Enums\UserRoleEnum; use FireflyIII\Models\PiggyBank; +use FireflyIII\Models\TransactionCurrency; use FireflyIII\Repositories\Account\AccountRepositoryInterface; use FireflyIII\Repositories\PiggyBank\PiggyBankRepositoryInterface; +use FireflyIII\Support\Facades\Amount; use Illuminate\Http\JsonResponse; /** @@ -96,6 +98,7 @@ class PiggyBankController extends Controller /** @var PiggyBank $piggy */ foreach ($piggies as $piggy) { + /** @var TransactionCurrency $currency */ $currency = $piggy->transactionCurrency; $currentAmount = $this->piggyRepository->getCurrentAmount($piggy); $objectGroup = $piggy->objectGroups()->first(); @@ -105,8 +108,8 @@ class PiggyBankController extends Controller 'name_with_balance' => sprintf( '%s (%s / %s)', $piggy->name, - app('amount')->formatAnything($currency, $currentAmount, false), - app('amount')->formatAnything($currency, $piggy->target_amount, false), + Amount::formatAnything($currency, $currentAmount, false), + Amount::formatAnything($currency, $piggy->target_amount, false), ), 'currency_id' => (string) $currency->id, 'currency_name' => $currency->name, diff --git a/app/Api/V1/Controllers/Models/CurrencyExchangeRate/DestroyController.php b/app/Api/V1/Controllers/Models/CurrencyExchangeRate/DestroyController.php index 265a72b616..733517a506 100644 --- a/app/Api/V1/Controllers/Models/CurrencyExchangeRate/DestroyController.php +++ b/app/Api/V1/Controllers/Models/CurrencyExchangeRate/DestroyController.php @@ -72,7 +72,7 @@ class DestroyController extends Controller public function destroySingleByDate(TransactionCurrency $from, TransactionCurrency $to, Carbon $date): JsonResponse { $exchangeRate = $this->repository->getSpecificRateOnDate($from, $to, $date); - if (null !== $exchangeRate) { + if ($exchangeRate instanceof CurrencyExchangeRate) { $this->repository->deleteRate($exchangeRate); } diff --git a/app/Api/V1/Controllers/Models/CurrencyExchangeRate/ShowController.php b/app/Api/V1/Controllers/Models/CurrencyExchangeRate/ShowController.php index 3b2f8c6e4d..22a9756ab7 100644 --- a/app/Api/V1/Controllers/Models/CurrencyExchangeRate/ShowController.php +++ b/app/Api/V1/Controllers/Models/CurrencyExchangeRate/ShowController.php @@ -95,7 +95,7 @@ class ShowController extends Controller $transformer->setParameters($this->parameters); $exchangeRate = $this->repository->getSpecificRateOnDate($from, $to, $date); - if (null === $exchangeRate) { + if (!$exchangeRate instanceof CurrencyExchangeRate) { throw new NotFoundHttpException(); } diff --git a/app/Api/V1/Controllers/Models/CurrencyExchangeRate/UpdateController.php b/app/Api/V1/Controllers/Models/CurrencyExchangeRate/UpdateController.php index 8844407f7d..8326f44c52 100644 --- a/app/Api/V1/Controllers/Models/CurrencyExchangeRate/UpdateController.php +++ b/app/Api/V1/Controllers/Models/CurrencyExchangeRate/UpdateController.php @@ -74,7 +74,7 @@ class UpdateController extends Controller public function updateByDate(UpdateRequest $request, TransactionCurrency $from, TransactionCurrency $to, Carbon $date): JsonResponse { $exchangeRate = $this->repository->getSpecificRateOnDate($from, $to, $date); - if (null === $exchangeRate) { + if (!$exchangeRate instanceof CurrencyExchangeRate) { throw new NotFoundHttpException(); } $date = $request->getDate(); diff --git a/app/Casts/SeparateTimezoneCaster.php b/app/Casts/SeparateTimezoneCaster.php index 67a56ae83e..608f5fc1c9 100644 --- a/app/Casts/SeparateTimezoneCaster.php +++ b/app/Casts/SeparateTimezoneCaster.php @@ -28,7 +28,6 @@ namespace FireflyIII\Casts; use Carbon\Carbon; use Illuminate\Contracts\Database\Eloquent\CastsAttributes; use Illuminate\Database\Eloquent\Model; -use Illuminate\Support\Facades\Log; /** * Class SeparateTimezoneCaster diff --git a/app/Console/Commands/Tools/ApplyRules.php b/app/Console/Commands/Tools/ApplyRules.php index b2a891b5d6..c0ea4ed2db 100644 --- a/app/Console/Commands/Tools/ApplyRules.php +++ b/app/Console/Commands/Tools/ApplyRules.php @@ -24,6 +24,7 @@ declare(strict_types=1); namespace FireflyIII\Console\Commands\Tools; +use Carbon\CarbonInterface; use Carbon\Carbon; use FireflyIII\Console\Commands\ShowsFriendlyMessages; use FireflyIII\Console\Commands\VerifiesAccessToken; @@ -283,7 +284,7 @@ class ApplyRules extends Command if (null !== $endString && '' !== $endString) { $inputEnd = Carbon::createFromFormat('Y-m-d', $endString); } - if (null === $inputEnd || null === $inputStart) { + if (!$inputEnd instanceof Carbon || null === $inputStart) { Log::error('Could not parse start or end date in verifyInputDate().'); return; diff --git a/app/Http/Controllers/Transaction/CreateController.php b/app/Http/Controllers/Transaction/CreateController.php index 519ff3d538..21c573565e 100644 --- a/app/Http/Controllers/Transaction/CreateController.php +++ b/app/Http/Controllers/Transaction/CreateController.php @@ -31,6 +31,7 @@ use FireflyIII\Http\Controllers\Controller; use FireflyIII\Repositories\Account\AccountRepositoryInterface; use FireflyIII\Repositories\TransactionGroup\TransactionGroupRepositoryInterface; use FireflyIII\Services\Internal\Update\GroupCloneService; +use FireflyIII\Support\Facades\Preferences; use Illuminate\Contracts\View\Factory; use Illuminate\Contracts\View\View; use Illuminate\Http\JsonResponse; @@ -76,7 +77,7 @@ class CreateController extends Controller // event! event(new StoredTransactionGroup($newGroup, true, true)); - app('preferences')->mark(); + Preferences::mark(); $title = $newGroup->title ?? $newGroup->transactionJournals->first()->description; $link = route('transactions.show', [$newGroup->id]); @@ -103,7 +104,7 @@ class CreateController extends Controller * */ public function create(?string $objectType) { - app('preferences')->mark(); + Preferences::mark(); $sourceId = (int) request()->get('source'); $destinationId = (int) request()->get('destination'); @@ -114,7 +115,8 @@ class CreateController extends Controller $preFilled = session()->has('preFilled') ? session('preFilled') : []; $subTitle = (string) trans(sprintf('breadcrumbs.create_%s', strtolower((string) $objectType))); $subTitleIcon = 'fa-plus'; - $optionalFields = app('preferences')->get('transaction_journal_optional_fields', [])->data; + /** @var array|null $optionalFields */ + $optionalFields = Preferences::get('transaction_journal_optional_fields', [])->data; $allowedOpposingTypes = config('firefly.allowed_opposing_types'); $accountToTypes = config('firefly.account_to_transaction'); $previousUrl = $this->rememberPreviousUrl('transactions.create.url'); diff --git a/app/Repositories/Category/OperationsRepository.php b/app/Repositories/Category/OperationsRepository.php index e020782f40..d88bc16d0b 100644 --- a/app/Repositories/Category/OperationsRepository.php +++ b/app/Repositories/Category/OperationsRepository.php @@ -510,9 +510,7 @@ class OperationsRepository implements OperationsRepositoryInterface, UserGroupIn $summarizer->setConvertToPrimary($convertToPrimary); // filter $journals by range AND currency if it is present. - $expenses = array_filter($expenses, static function (array $expense) use ($category): bool { - return $expense['category_id'] === $category->id; - }); + $expenses = array_filter($expenses, static fn(array $expense): bool => $expense['category_id'] === $category->id); return $summarizer->groupByCurrencyId($expenses, $method, false); } diff --git a/app/Repositories/PeriodStatistic/PeriodStatisticRepository.php b/app/Repositories/PeriodStatistic/PeriodStatisticRepository.php index 612773a9e2..07523c5348 100644 --- a/app/Repositories/PeriodStatistic/PeriodStatisticRepository.php +++ b/app/Repositories/PeriodStatistic/PeriodStatisticRepository.php @@ -68,7 +68,7 @@ class PeriodStatisticRepository implements PeriodStatisticRepositoryInterface Log::debug(sprintf( 'Saved #%d [currency #%d, Model %s #%d, %s to %s, %d, %s] as new statistic.', $stat->id, - get_class($model), + $model::class, $model->id, $stat->transaction_currency_id, $stat->start->toW3cString(), diff --git a/app/Repositories/PiggyBank/ModifiesPiggyBanks.php b/app/Repositories/PiggyBank/ModifiesPiggyBanks.php index 0acb4b7ed8..e63da28927 100644 --- a/app/Repositories/PiggyBank/ModifiesPiggyBanks.php +++ b/app/Repositories/PiggyBank/ModifiesPiggyBanks.php @@ -67,7 +67,7 @@ trait ModifiesPiggyBanks { $currentAmount = $this->getCurrentAmount($piggyBank, $account); $pivot = $piggyBank->accounts()->where('accounts.id', $account->id)->first()->pivot; - $pivot->current_amount = bcsub($currentAmount, $amount); + $pivot->current_amount = bcsub((string) $currentAmount, $amount); $pivot->native_current_amount = null; // also update native_current_amount. @@ -90,7 +90,7 @@ trait ModifiesPiggyBanks { $currentAmount = $this->getCurrentAmount($piggyBank, $account); $pivot = $piggyBank->accounts()->where('accounts.id', $account->id)->first()->pivot; - $pivot->current_amount = bcadd($currentAmount, $amount); + $pivot->current_amount = bcadd((string) $currentAmount, $amount); $pivot->native_current_amount = null; // also update native_current_amount. @@ -122,13 +122,13 @@ trait ModifiesPiggyBanks if (0 !== bccomp($piggyBank->target_amount, '0')) { - $leftToSave = bcsub($piggyBank->target_amount, $savedSoFar); - $maxAmount = 1 === bccomp($leftOnAccount, $leftToSave) ? $leftToSave : $leftOnAccount; + $leftToSave = bcsub($piggyBank->target_amount, (string) $savedSoFar); + $maxAmount = 1 === bccomp((string) $leftOnAccount, $leftToSave) ? $leftToSave : $leftOnAccount; Log::debug(sprintf('Left to save: %s', $leftToSave)); Log::debug(sprintf('Maximum amount: %s', $maxAmount)); } - $compare = bccomp($amount, $maxAmount); + $compare = bccomp($amount, (string) $maxAmount); $result = $compare <= 0; Log::debug(sprintf('Compare <= 0? %d, so canAddAmount is %s', $compare, var_export($result, true))); @@ -140,7 +140,7 @@ trait ModifiesPiggyBanks { $savedSoFar = $this->getCurrentAmount($piggyBank, $account); - return bccomp($amount, $savedSoFar) <= 0; + return bccomp($amount, (string) $savedSoFar) <= 0; } /** @@ -234,9 +234,9 @@ trait ModifiesPiggyBanks // if the piggy bank is now smaller than the sum of the money saved, // remove money from all accounts until the piggy bank is the right amount. $currentAmount = $this->getCurrentAmount($piggyBank); - if (1 === bccomp($currentAmount, (string)$piggyBank->target_amount) && 0 !== bccomp((string)$piggyBank->target_amount, '0')) { + if (1 === bccomp((string) $currentAmount, (string)$piggyBank->target_amount) && 0 !== bccomp((string)$piggyBank->target_amount, '0')) { Log::debug(sprintf('Current amount is %s, target amount is %s', $currentAmount, $piggyBank->target_amount)); - $difference = bcsub((string)$piggyBank->target_amount, $currentAmount); + $difference = bcsub((string)$piggyBank->target_amount, (string) $currentAmount); // an amount will be removed, create "negative" event: // Log::debug(sprintf('ChangedAmount: is triggered with difference "%s"', $difference)); diff --git a/app/Repositories/Tag/TagRepository.php b/app/Repositories/Tag/TagRepository.php index 06a41da184..576f6ec804 100644 --- a/app/Repositories/Tag/TagRepository.php +++ b/app/Repositories/Tag/TagRepository.php @@ -23,6 +23,7 @@ declare(strict_types=1); namespace FireflyIII\Repositories\Tag; +use Override; use Carbon\Carbon; use FireflyIII\Enums\TransactionTypeEnum; use FireflyIII\Factory\TagFactory; @@ -380,7 +381,7 @@ class TagRepository implements TagRepositoryInterface, UserGroupInterface return $tag->locations()->first(); } - #[\Override] + #[Override] public function periodCollection(Tag $tag, Carbon $start, Carbon $end): array { Log::debug(sprintf('periodCollection(#%d, %s, %s)', $tag->id, $start->format('Y-m-d'), $end->format('Y-m-d'))); diff --git a/app/Services/Internal/Support/CreditRecalculateService.php b/app/Services/Internal/Support/CreditRecalculateService.php index eb6b33b8d4..66b5fc359c 100644 --- a/app/Services/Internal/Support/CreditRecalculateService.php +++ b/app/Services/Internal/Support/CreditRecalculateService.php @@ -276,66 +276,66 @@ class CreditRecalculateService if ($isSameAccount && $isCredit && $this->isWithdrawalIn($usedAmount, $type)) { // case 1 $usedAmount = app('steam')->positive($usedAmount); - return bcadd($leftOfDebt, $usedAmount); + return bcadd($leftOfDebt, (string) $usedAmount); // Log::debug(sprintf('Case 1 (withdrawal into credit liability): %s + %s = %s', app('steam')->bcround($leftOfDebt, 2), app('steam')->bcround($usedAmount, 2), app('steam')->bcround($result, 2))); } if ($isSameAccount && $isCredit && $this->isWithdrawalOut($usedAmount, $type)) { // case 2 $usedAmount = app('steam')->positive($usedAmount); - return bcsub($leftOfDebt, $usedAmount); + return bcsub($leftOfDebt, (string) $usedAmount); // Log::debug(sprintf('Case 2 (withdrawal away from liability): %s - %s = %s', app('steam')->bcround($leftOfDebt, 2), app('steam')->bcround($usedAmount, 2), app('steam')->bcround($result, 2))); } if ($isSameAccount && $isCredit && $this->isDepositOut($usedAmount, $type)) { // case 3 $usedAmount = app('steam')->positive($usedAmount); - return bcsub($leftOfDebt, $usedAmount); + return bcsub($leftOfDebt, (string) $usedAmount); // Log::debug(sprintf('Case 3 (deposit away from liability): %s - %s = %s', app('steam')->bcround($leftOfDebt, 2), app('steam')->bcround($usedAmount, 2), app('steam')->bcround($result, 2))); } if ($isSameAccount && $isCredit && $this->isDepositIn($usedAmount, $type)) { // case 4 $usedAmount = app('steam')->positive($usedAmount); - return bcadd($leftOfDebt, $usedAmount); + return bcadd($leftOfDebt, (string) $usedAmount); // Log::debug(sprintf('Case 4 (deposit into credit liability): %s + %s = %s', app('steam')->bcround($leftOfDebt, 2), app('steam')->bcround($usedAmount, 2), app('steam')->bcround($result, 2))); } if ($isSameAccount && $isCredit && $this->isTransferIn($usedAmount, $type)) { // case 5 $usedAmount = app('steam')->positive($usedAmount); - return bcadd($leftOfDebt, $usedAmount); + return bcadd($leftOfDebt, (string) $usedAmount); // Log::debug(sprintf('Case 5 (transfer into credit liability): %s + %s = %s', app('steam')->bcround($leftOfDebt, 2), app('steam')->bcround($usedAmount, 2), app('steam')->bcround($result, 2))); } if ($isSameAccount && $isDebit && $this->isWithdrawalIn($usedAmount, $type)) { // case 6 $usedAmount = app('steam')->positive($usedAmount); - return bcsub($leftOfDebt, $usedAmount); + return bcsub($leftOfDebt, (string) $usedAmount); // Log::debug(sprintf('Case 6 (withdrawal into debit liability): %s - %s = %s', app('steam')->bcround($leftOfDebt, 2), app('steam')->bcround($usedAmount, 2), app('steam')->bcround($result, 2))); } if ($isSameAccount && $isDebit && $this->isDepositOut($usedAmount, $type)) { // case 7 $usedAmount = app('steam')->positive($usedAmount); - return bcadd($leftOfDebt, $usedAmount); + return bcadd($leftOfDebt, (string) $usedAmount); // Log::debug(sprintf('Case 7 (deposit away from liability): %s + %s = %s', app('steam')->bcround($leftOfDebt, 2), app('steam')->bcround($usedAmount, 2), app('steam')->bcround($result, 2))); } if ($isSameAccount && $isDebit && $this->isWithdrawalOut($usedAmount, $type)) { // case 8 $usedAmount = app('steam')->positive($usedAmount); - return bcadd($leftOfDebt, $usedAmount); + return bcadd($leftOfDebt, (string) $usedAmount); // Log::debug(sprintf('Case 8 (withdrawal away from liability): %s + %s = %s', app('steam')->bcround($leftOfDebt, 2), app('steam')->bcround($usedAmount, 2), app('steam')->bcround($result, 2))); } if ($isSameAccount && $isDebit && $this->isTransferIn($usedAmount, $type)) { // case 9 $usedAmount = app('steam')->positive($usedAmount); - return bcsub($leftOfDebt, $usedAmount); + return bcsub($leftOfDebt, (string) $usedAmount); // 2024-10-05, #9225 this used to say you would owe more, but a transfer INTO a debit from wherever means you owe LESS. // Log::debug(sprintf('Case 9 (transfer into debit liability, means you owe LESS): %s - %s = %s', app('steam')->bcround($leftOfDebt, 2), app('steam')->bcround($usedAmount, 2), app('steam')->bcround($result, 2))); } if ($isSameAccount && $isDebit && $this->isTransferOut($usedAmount, $type)) { // case 10 $usedAmount = app('steam')->positive($usedAmount); - return bcadd($leftOfDebt, $usedAmount); + return bcadd($leftOfDebt, (string) $usedAmount); // 2024-10-05, #9225 this used to say you would owe less, but a transfer OUT OF a debit from wherever means you owe MORE. // Log::debug(sprintf('Case 10 (transfer out of debit liability, means you owe MORE): %s + %s = %s', app('steam')->bcround($leftOfDebt, 2), app('steam')->bcround($usedAmount, 2), app('steam')->bcround($result, 2))); } @@ -344,7 +344,7 @@ class CreditRecalculateService if (in_array($type, [TransactionTypeEnum::WITHDRAWAL->value, TransactionTypeEnum::DEPOSIT->value, TransactionTypeEnum::TRANSFER->value], true)) { $usedAmount = app('steam')->negative($usedAmount); - return bcadd($leftOfDebt, $usedAmount); + return bcadd($leftOfDebt, (string) $usedAmount); // Log::debug(sprintf('Case X (all other cases): %s + %s = %s', app('steam')->bcround($leftOfDebt, 2), app('steam')->bcround($usedAmount, 2), app('steam')->bcround($result, 2))); } diff --git a/app/Support/Debug/Timer.php b/app/Support/Debug/Timer.php index b23e941a0c..5f2d6bc283 100644 --- a/app/Support/Debug/Timer.php +++ b/app/Support/Debug/Timer.php @@ -38,7 +38,7 @@ class Timer public static function getInstance(): self { - if (null === self::$instance) { + if (!self::$instance instanceof \FireflyIII\Support\Debug\Timer) { self::$instance = new self(); } diff --git a/app/Support/Http/Controllers/PeriodOverview.php b/app/Support/Http/Controllers/PeriodOverview.php index 3b9a708d88..98206c429e 100644 --- a/app/Support/Http/Controllers/PeriodOverview.php +++ b/app/Support/Http/Controllers/PeriodOverview.php @@ -724,7 +724,7 @@ trait PeriodOverview protected function saveGroupedAsStatistics(Model $model, Carbon $start, Carbon $end, string $type, array $array): void { unset($array['count']); - Log::debug(sprintf('saveGroupedAsStatistics(%s #%d, %s, %s, "%s", array(%d))', get_class($model), $model->id, $start->format('Y-m-d'), $end->format('Y-m-d'), $type, count($array))); + Log::debug(sprintf('saveGroupedAsStatistics(%s #%d, %s, %s, "%s", array(%d))', $model::class, $model->id, $start->format('Y-m-d'), $end->format('Y-m-d'), $type, count($array))); foreach ($array as $entry) { $this->periodStatisticRepo->saveStatistic($model, $entry['currency_id'], $start, $end, $type, $entry['count'], $entry['amount']); } @@ -899,7 +899,7 @@ trait PeriodOverview ]; - $return[$currencyId]['amount'] = bcadd($return[$currencyId]['amount'], $amount); + $return[$currencyId]['amount'] = bcadd((string) $return[$currencyId]['amount'], $amount); ++$return[$currencyId]['count']; ++$return['count']; } diff --git a/app/Support/JsonApi/Enrichments/CategoryEnrichment.php b/app/Support/JsonApi/Enrichments/CategoryEnrichment.php index 975227844f..b5ddd22c52 100644 --- a/app/Support/JsonApi/Enrichments/CategoryEnrichment.php +++ b/app/Support/JsonApi/Enrichments/CategoryEnrichment.php @@ -135,7 +135,7 @@ class CategoryEnrichment implements EnrichmentInterface private function collectTransactions(): void { - if (null !== $this->start && null !== $this->end) { + if ($this->start instanceof Carbon && $this->end instanceof Carbon) { /** @var OperationsRepositoryInterface $opsRepository */ $opsRepository = app(OperationsRepositoryInterface::class); $opsRepository->setUser($this->user); diff --git a/app/Support/JsonApi/Enrichments/PiggyBankEnrichment.php b/app/Support/JsonApi/Enrichments/PiggyBankEnrichment.php index e1afbe0042..7c6a8a8a5b 100644 --- a/app/Support/JsonApi/Enrichments/PiggyBankEnrichment.php +++ b/app/Support/JsonApi/Enrichments/PiggyBankEnrichment.php @@ -142,9 +142,9 @@ class PiggyBankEnrichment implements EnrichmentInterface 'current_amount' => Steam::bcround($row['current_amount'], $currency->decimal_places), 'pc_current_amount' => Steam::bcround($row['pc_current_amount'], $this->primaryCurrency->decimal_places), ]; - $meta['current_amount'] = bcadd($meta['current_amount'], $row['current_amount']); + $meta['current_amount'] = bcadd($meta['current_amount'], (string) $row['current_amount']); // only add pc_current_amount when the pc_current_amount is set - $meta['pc_current_amount'] = null === $row['pc_current_amount'] ? null : bcadd($meta['pc_current_amount'], $row['pc_current_amount']); + $meta['pc_current_amount'] = null === $row['pc_current_amount'] ? null : bcadd((string) $meta['pc_current_amount'], (string) $row['pc_current_amount']); } $meta['current_amount'] = Steam::bcround($meta['current_amount'], $currency->decimal_places); // only round this number when pc_current_amount is set. @@ -152,8 +152,8 @@ class PiggyBankEnrichment implements EnrichmentInterface // calculate left to save, only when there is a target amount. if (null !== $targetAmount) { - $meta['left_to_save'] = bcsub($meta['target_amount'], $meta['current_amount']); - $meta['pc_left_to_save'] = null === $meta['pc_target_amount'] ? null : bcsub($meta['pc_target_amount'], $meta['pc_current_amount']); + $meta['left_to_save'] = bcsub((string) $meta['target_amount'], (string) $meta['current_amount']); + $meta['pc_left_to_save'] = null === $meta['pc_target_amount'] ? null : bcsub((string) $meta['pc_target_amount'], (string) $meta['pc_current_amount']); } // get suggested per month. @@ -199,7 +199,7 @@ class PiggyBankEnrichment implements EnrichmentInterface 'pc_current_amount' => '0', ]; } - $this->amounts[$id][$accountId]['current_amount'] = bcadd($this->amounts[$id][$accountId]['current_amount'], (string)$item->current_amount); + $this->amounts[$id][$accountId]['current_amount'] = bcadd((string) $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); } diff --git a/app/Support/Observers/RecalculatesAvailableBudgetsTrait.php b/app/Support/Observers/RecalculatesAvailableBudgetsTrait.php index 82f513a44e..0c77493667 100644 --- a/app/Support/Observers/RecalculatesAvailableBudgetsTrait.php +++ b/app/Support/Observers/RecalculatesAvailableBudgetsTrait.php @@ -197,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((string) $daily, (string)$currentPeriod->length(), 12); // no need to calculate if period is equal. if ($currentPeriod->equals($limitPeriod)) { diff --git a/app/Support/Report/Summarizer/TransactionSummarizer.php b/app/Support/Report/Summarizer/TransactionSummarizer.php index 5660a239f0..83f057c68c 100644 --- a/app/Support/Report/Summarizer/TransactionSummarizer.php +++ b/app/Support/Report/Summarizer/TransactionSummarizer.php @@ -193,7 +193,7 @@ 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'], (string) 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) @@ -211,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'], (string) Steam::{$method}((string)$journal['foreign_amount'])); // @phpstan-ignore-line } } diff --git a/app/Support/Singleton/PreferencesSingleton.php b/app/Support/Singleton/PreferencesSingleton.php index 287a964361..716eddc1e8 100644 --- a/app/Support/Singleton/PreferencesSingleton.php +++ b/app/Support/Singleton/PreferencesSingleton.php @@ -38,7 +38,7 @@ class PreferencesSingleton public static function getInstance(): self { - if (null === self::$instance) { + if (!self::$instance instanceof PreferencesSingleton) { self::$instance = new self(); } diff --git a/app/Support/Steam.php b/app/Support/Steam.php index 46119a268a..71d5e788f3 100644 --- a/app/Support/Steam.php +++ b/app/Support/Steam.php @@ -756,7 +756,7 @@ class Steam $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); + $total = bcadd((string) $current, $total); } return $total; diff --git a/app/Transformers/PiggyBankEventTransformer.php b/app/Transformers/PiggyBankEventTransformer.php index a9724ddc57..a0a43ecc59 100644 --- a/app/Transformers/PiggyBankEventTransformer.php +++ b/app/Transformers/PiggyBankEventTransformer.php @@ -35,7 +35,7 @@ use FireflyIII\Support\Facades\Steam; */ class PiggyBankEventTransformer extends AbstractTransformer { - private TransactionCurrency $primaryCurrency; + private readonly TransactionCurrency $primaryCurrency; private bool $convertToPrimary = false; /** From 6743b3fe833c3ac19f3095df92f077566ba84a07 Mon Sep 17 00:00:00 2001 From: James Cole Date: Sat, 27 Sep 2025 06:01:14 +0200 Subject: [PATCH 55/58] Remove stats for other objects as well. --- app/Handlers/Events/StoredGroupEventHandler.php | 6 ++++++ app/Handlers/Events/UpdatedGroupEventHandler.php | 13 +++++++++++++ 2 files changed, 19 insertions(+) diff --git a/app/Handlers/Events/StoredGroupEventHandler.php b/app/Handlers/Events/StoredGroupEventHandler.php index 9b4f75b4b3..531ef4a433 100644 --- a/app/Handlers/Events/StoredGroupEventHandler.php +++ b/app/Handlers/Events/StoredGroupEventHandler.php @@ -109,6 +109,12 @@ class StoredGroupEventHandler $dest = $journal->transactions()->where('amount', '>', '0')->first(); $repository->deleteStatisticsForModel($source->account, $journal->date); $repository->deleteStatisticsForModel($dest->account, $journal->date); + foreach($journal->categories as $category) { + $repository->deleteStatisticsForModel($category, $journal->date); + } + foreach($journal->tags as $tag) { + $repository->deleteStatisticsForModel($tag, $journal->date); + } } } diff --git a/app/Handlers/Events/UpdatedGroupEventHandler.php b/app/Handlers/Events/UpdatedGroupEventHandler.php index ec5ada671d..c8dc416b76 100644 --- a/app/Handlers/Events/UpdatedGroupEventHandler.php +++ b/app/Handlers/Events/UpdatedGroupEventHandler.php @@ -58,6 +58,13 @@ class UpdatedGroupEventHandler } + /** + * TODO duplicate + * + * @param UpdatedTransactionGroup $event + * + * @return void + */ private function removePeriodStatistics(UpdatedTransactionGroup $event): void { /** @var PeriodStatisticRepositoryInterface $repository */ @@ -69,6 +76,12 @@ class UpdatedGroupEventHandler $dest = $journal->transactions()->where('amount', '>', '0')->first(); $repository->deleteStatisticsForModel($source->account, $journal->date); $repository->deleteStatisticsForModel($dest->account, $journal->date); + foreach($journal->categories as $category) { + $repository->deleteStatisticsForModel($category, $journal->date); + } + foreach($journal->tags as $tag) { + $repository->deleteStatisticsForModel($tag, $journal->date); + } } } From 308abffb0bd4e8220e9374da78a161a98768dc50 Mon Sep 17 00:00:00 2001 From: JC5 Date: Sat, 27 Sep 2025 06:04:46 +0200 Subject: [PATCH 56/58] =?UTF-8?q?=F0=9F=A4=96=20Auto=20commit=20for=20rele?= =?UTF-8?q?ase=20'develop'=20on=202025-09-27?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .ci/php-cs-fixer/composer.lock | 12 +- app/Console/Commands/Tools/ApplyRules.php | 1 - .../Events/StoredGroupEventHandler.php | 4 +- .../Events/UpdatedGroupEventHandler.php | 8 +- .../Transaction/CreateController.php | 3 +- .../Category/CategoryRepository.php | 60 ++--- .../Category/OperationsRepository.php | 2 +- app/Repositories/Tag/TagRepository.php | 60 ++--- .../Tag/TagRepositoryInterface.php | 1 + app/Support/Debug/Timer.php | 2 +- .../Http/Controllers/PeriodOverview.php | 239 +++++++++--------- .../Singleton/PreferencesSingleton.php | 2 +- config/firefly.php | 4 +- 13 files changed, 197 insertions(+), 201 deletions(-) diff --git a/.ci/php-cs-fixer/composer.lock b/.ci/php-cs-fixer/composer.lock index 18ee8f03de..f397529f05 100644 --- a/.ci/php-cs-fixer/composer.lock +++ b/.ci/php-cs-fixer/composer.lock @@ -402,16 +402,16 @@ }, { "name": "friendsofphp/php-cs-fixer", - "version": "v3.88.0", + "version": "v3.88.2", "source": { "type": "git", "url": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer.git", - "reference": "f23469674ae50d40e398bfff8018911a2a2b0dbe" + "reference": "a8d15584bafb0f0d9d938827840060fd4a3ebc99" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/PHP-CS-Fixer/PHP-CS-Fixer/zipball/f23469674ae50d40e398bfff8018911a2a2b0dbe", - "reference": "f23469674ae50d40e398bfff8018911a2a2b0dbe", + "url": "https://api.github.com/repos/PHP-CS-Fixer/PHP-CS-Fixer/zipball/a8d15584bafb0f0d9d938827840060fd4a3ebc99", + "reference": "a8d15584bafb0f0d9d938827840060fd4a3ebc99", "shasum": "" }, "require": { @@ -494,7 +494,7 @@ ], "support": { "issues": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/issues", - "source": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/tree/v3.88.0" + "source": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/tree/v3.88.2" }, "funding": [ { @@ -502,7 +502,7 @@ "type": "github" } ], - "time": "2025-09-24T21:31:42+00:00" + "time": "2025-09-27T00:24:15+00:00" }, { "name": "psr/container", diff --git a/app/Console/Commands/Tools/ApplyRules.php b/app/Console/Commands/Tools/ApplyRules.php index c0ea4ed2db..6a42ac0990 100644 --- a/app/Console/Commands/Tools/ApplyRules.php +++ b/app/Console/Commands/Tools/ApplyRules.php @@ -24,7 +24,6 @@ declare(strict_types=1); namespace FireflyIII\Console\Commands\Tools; -use Carbon\CarbonInterface; use Carbon\Carbon; use FireflyIII\Console\Commands\ShowsFriendlyMessages; use FireflyIII\Console\Commands\VerifiesAccessToken; diff --git a/app/Handlers/Events/StoredGroupEventHandler.php b/app/Handlers/Events/StoredGroupEventHandler.php index 531ef4a433..50ce62f742 100644 --- a/app/Handlers/Events/StoredGroupEventHandler.php +++ b/app/Handlers/Events/StoredGroupEventHandler.php @@ -109,10 +109,10 @@ class StoredGroupEventHandler $dest = $journal->transactions()->where('amount', '>', '0')->first(); $repository->deleteStatisticsForModel($source->account, $journal->date); $repository->deleteStatisticsForModel($dest->account, $journal->date); - foreach($journal->categories as $category) { + foreach ($journal->categories as $category) { $repository->deleteStatisticsForModel($category, $journal->date); } - foreach($journal->tags as $tag) { + foreach ($journal->tags as $tag) { $repository->deleteStatisticsForModel($tag, $journal->date); } } diff --git a/app/Handlers/Events/UpdatedGroupEventHandler.php b/app/Handlers/Events/UpdatedGroupEventHandler.php index c8dc416b76..e1393a3355 100644 --- a/app/Handlers/Events/UpdatedGroupEventHandler.php +++ b/app/Handlers/Events/UpdatedGroupEventHandler.php @@ -60,10 +60,6 @@ class UpdatedGroupEventHandler /** * TODO duplicate - * - * @param UpdatedTransactionGroup $event - * - * @return void */ private function removePeriodStatistics(UpdatedTransactionGroup $event): void { @@ -76,10 +72,10 @@ class UpdatedGroupEventHandler $dest = $journal->transactions()->where('amount', '>', '0')->first(); $repository->deleteStatisticsForModel($source->account, $journal->date); $repository->deleteStatisticsForModel($dest->account, $journal->date); - foreach($journal->categories as $category) { + foreach ($journal->categories as $category) { $repository->deleteStatisticsForModel($category, $journal->date); } - foreach($journal->tags as $tag) { + foreach ($journal->tags as $tag) { $repository->deleteStatisticsForModel($tag, $journal->date); } } diff --git a/app/Http/Controllers/Transaction/CreateController.php b/app/Http/Controllers/Transaction/CreateController.php index 21c573565e..c69ddfc2ce 100644 --- a/app/Http/Controllers/Transaction/CreateController.php +++ b/app/Http/Controllers/Transaction/CreateController.php @@ -115,7 +115,8 @@ class CreateController extends Controller $preFilled = session()->has('preFilled') ? session('preFilled') : []; $subTitle = (string) trans(sprintf('breadcrumbs.create_%s', strtolower((string) $objectType))); $subTitleIcon = 'fa-plus'; - /** @var array|null $optionalFields */ + + /** @var null|array $optionalFields */ $optionalFields = Preferences::get('transaction_journal_optional_fields', [])->data; $allowedOpposingTypes = config('firefly.allowed_opposing_types'); $accountToTypes = config('firefly.account_to_transaction'); diff --git a/app/Repositories/Category/CategoryRepository.php b/app/Repositories/Category/CategoryRepository.php index f644c57070..894690e9e6 100644 --- a/app/Repositories/Category/CategoryRepository.php +++ b/app/Repositories/Category/CategoryRepository.php @@ -364,37 +364,37 @@ class CategoryRepository implements CategoryRepositoryInterface, UserGroupInterf Log::debug(sprintf('periodCollection(#%d, %s, %s)', $category->id, $start->format('Y-m-d'), $end->format('Y-m-d'))); return $category->transactionJournals() - ->leftJoin('transactions','transactions.transaction_journal_id', '=', 'transaction_journals.id') - ->leftJoin('transaction_types', 'transaction_types.id', '=', 'transaction_journals.transaction_type_id') - ->leftJoin('transaction_currencies', 'transaction_currencies.id', '=', 'transactions.transaction_currency_id') - ->leftJoin('transaction_currencies as foreign_currencies', 'foreign_currencies.id', '=', 'transactions.foreign_currency_id') - ->where('transaction_journals.date', '>=', $start) - ->where('transaction_journals.date', '<=', $end) - ->where('transactions.amount', '>', 0) - ->get([ - // currencies - 'transaction_currencies.id as currency_id', - 'transaction_currencies.code as currency_code', - 'transaction_currencies.name as currency_name', - 'transaction_currencies.symbol as currency_symbol', - 'transaction_currencies.decimal_places as currency_decimal_places', + ->leftJoin('transactions', 'transactions.transaction_journal_id', '=', 'transaction_journals.id') + ->leftJoin('transaction_types', 'transaction_types.id', '=', 'transaction_journals.transaction_type_id') + ->leftJoin('transaction_currencies', 'transaction_currencies.id', '=', 'transactions.transaction_currency_id') + ->leftJoin('transaction_currencies as foreign_currencies', 'foreign_currencies.id', '=', 'transactions.foreign_currency_id') + ->where('transaction_journals.date', '>=', $start) + ->where('transaction_journals.date', '<=', $end) + ->where('transactions.amount', '>', 0) + ->get([ + // currencies + 'transaction_currencies.id as currency_id', + 'transaction_currencies.code as currency_code', + 'transaction_currencies.name as currency_name', + 'transaction_currencies.symbol as currency_symbol', + 'transaction_currencies.decimal_places as currency_decimal_places', - // foreign - 'foreign_currencies.id as foreign_currency_id', - 'foreign_currencies.code as foreign_currency_code', - 'foreign_currencies.name as foreign_currency_name', - 'foreign_currencies.symbol as foreign_currency_symbol', - 'foreign_currencies.decimal_places as foreign_currency_decimal_places', + // foreign + 'foreign_currencies.id as foreign_currency_id', + 'foreign_currencies.code as foreign_currency_code', + 'foreign_currencies.name as foreign_currency_name', + 'foreign_currencies.symbol as foreign_currency_symbol', + 'foreign_currencies.decimal_places as foreign_currency_decimal_places', - // fields - 'transaction_journals.date', - 'transaction_types.type', - 'transaction_journals.transaction_currency_id', - 'transactions.amount', - 'transactions.native_amount as pc_amount', - 'transactions.foreign_amount', - ]) - ->toArray() - ; + // fields + 'transaction_journals.date', + 'transaction_types.type', + 'transaction_journals.transaction_currency_id', + 'transactions.amount', + 'transactions.native_amount as pc_amount', + 'transactions.foreign_amount', + ]) + ->toArray() + ; } } diff --git a/app/Repositories/Category/OperationsRepository.php b/app/Repositories/Category/OperationsRepository.php index d88bc16d0b..a1fe709f4c 100644 --- a/app/Repositories/Category/OperationsRepository.php +++ b/app/Repositories/Category/OperationsRepository.php @@ -510,7 +510,7 @@ class OperationsRepository implements OperationsRepositoryInterface, UserGroupIn $summarizer->setConvertToPrimary($convertToPrimary); // filter $journals by range AND currency if it is present. - $expenses = array_filter($expenses, static fn(array $expense): bool => $expense['category_id'] === $category->id); + $expenses = array_filter($expenses, static fn (array $expense): bool => $expense['category_id'] === $category->id); return $summarizer->groupByCurrencyId($expenses, $method, false); } diff --git a/app/Repositories/Tag/TagRepository.php b/app/Repositories/Tag/TagRepository.php index 576f6ec804..eb50e2ef8f 100644 --- a/app/Repositories/Tag/TagRepository.php +++ b/app/Repositories/Tag/TagRepository.php @@ -387,37 +387,37 @@ class TagRepository implements TagRepositoryInterface, UserGroupInterface Log::debug(sprintf('periodCollection(#%d, %s, %s)', $tag->id, $start->format('Y-m-d'), $end->format('Y-m-d'))); return $tag->transactionJournals() - ->leftJoin('transactions','transactions.transaction_journal_id', '=', 'transaction_journals.id') - ->leftJoin('transaction_types', 'transaction_types.id', '=', 'transaction_journals.transaction_type_id') - ->leftJoin('transaction_currencies', 'transaction_currencies.id', '=', 'transactions.transaction_currency_id') - ->leftJoin('transaction_currencies as foreign_currencies', 'foreign_currencies.id', '=', 'transactions.foreign_currency_id') - ->where('transaction_journals.date', '>=', $start) - ->where('transaction_journals.date', '<=', $end) - ->where('transactions.amount', '>', 0) - ->get([ - // currencies - 'transaction_currencies.id as currency_id', - 'transaction_currencies.code as currency_code', - 'transaction_currencies.name as currency_name', - 'transaction_currencies.symbol as currency_symbol', - 'transaction_currencies.decimal_places as currency_decimal_places', + ->leftJoin('transactions', 'transactions.transaction_journal_id', '=', 'transaction_journals.id') + ->leftJoin('transaction_types', 'transaction_types.id', '=', 'transaction_journals.transaction_type_id') + ->leftJoin('transaction_currencies', 'transaction_currencies.id', '=', 'transactions.transaction_currency_id') + ->leftJoin('transaction_currencies as foreign_currencies', 'foreign_currencies.id', '=', 'transactions.foreign_currency_id') + ->where('transaction_journals.date', '>=', $start) + ->where('transaction_journals.date', '<=', $end) + ->where('transactions.amount', '>', 0) + ->get([ + // currencies + 'transaction_currencies.id as currency_id', + 'transaction_currencies.code as currency_code', + 'transaction_currencies.name as currency_name', + 'transaction_currencies.symbol as currency_symbol', + 'transaction_currencies.decimal_places as currency_decimal_places', - // foreign - 'foreign_currencies.id as foreign_currency_id', - 'foreign_currencies.code as foreign_currency_code', - 'foreign_currencies.name as foreign_currency_name', - 'foreign_currencies.symbol as foreign_currency_symbol', - 'foreign_currencies.decimal_places as foreign_currency_decimal_places', + // foreign + 'foreign_currencies.id as foreign_currency_id', + 'foreign_currencies.code as foreign_currency_code', + 'foreign_currencies.name as foreign_currency_name', + 'foreign_currencies.symbol as foreign_currency_symbol', + 'foreign_currencies.decimal_places as foreign_currency_decimal_places', - // fields - 'transaction_journals.date', - 'transaction_types.type', - 'transaction_journals.transaction_currency_id', - 'transactions.amount', - 'transactions.native_amount as pc_amount', - 'transactions.foreign_amount', - ]) - ->toArray() - ; + // fields + 'transaction_journals.date', + 'transaction_types.type', + 'transaction_journals.transaction_currency_id', + 'transactions.amount', + 'transactions.native_amount as pc_amount', + 'transactions.foreign_amount', + ]) + ->toArray() + ; } } diff --git a/app/Repositories/Tag/TagRepositoryInterface.php b/app/Repositories/Tag/TagRepositoryInterface.php index 18e45fb9ce..5a53efe7fe 100644 --- a/app/Repositories/Tag/TagRepositoryInterface.php +++ b/app/Repositories/Tag/TagRepositoryInterface.php @@ -50,6 +50,7 @@ interface TagRepositoryInterface * This method destroys a tag. */ public function destroy(Tag $tag): bool; + public function periodCollection(Tag $tag, Carbon $start, Carbon $end): array; /** diff --git a/app/Support/Debug/Timer.php b/app/Support/Debug/Timer.php index 5f2d6bc283..31119addc4 100644 --- a/app/Support/Debug/Timer.php +++ b/app/Support/Debug/Timer.php @@ -38,7 +38,7 @@ class Timer public static function getInstance(): self { - if (!self::$instance instanceof \FireflyIII\Support\Debug\Timer) { + if (!self::$instance instanceof self) { self::$instance = new self(); } diff --git a/app/Support/Http/Controllers/PeriodOverview.php b/app/Support/Http/Controllers/PeriodOverview.php index 98206c429e..dcf3d574ad 100644 --- a/app/Support/Http/Controllers/PeriodOverview.php +++ b/app/Support/Http/Controllers/PeriodOverview.php @@ -92,16 +92,16 @@ trait PeriodOverview protected function getAccountPeriodOverview(Account $account, Carbon $start, Carbon $end): array { 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->accountRepository = app(AccountRepositoryInterface::class); $this->accountRepository->setUser($account->user); $this->periodStatisticRepo = app(PeriodStatisticRepositoryInterface::class); $range = Navigation::getViewRange(true); - [$start, $end] = $end < $start ? [$end, $start] : [$start, $end]; + [$start, $end] = $end < $start ? [$end, $start] : [$start, $end]; /** @var array $dates */ - $dates = Navigation::blockPeriods($start, $end, $range); - [$start, $end] = $this->getPeriodFromBlocks($dates, $start, $end); - $this->statistics = $this->periodStatisticRepo->allInRangeForModel($account, $start, $end); + $dates = Navigation::blockPeriods($start, $end, $range); + [$start, $end] = $this->getPeriodFromBlocks($dates, $start, $end); + $this->statistics = $this->periodStatisticRepo->allInRangeForModel($account, $start, $end); // TODO needs to be re-arranged: // get all period stats for entire range. @@ -109,7 +109,7 @@ trait PeriodOverview // create new ones, or use collected. - $entries = []; + $entries = []; Log::debug(sprintf('Count of loops: %d', count($dates))); foreach ($dates as $currentDate) { $entries[] = $this->getSingleAccountPeriod($account, $currentDate['period'], $currentDate['start'], $currentDate['end']); @@ -145,18 +145,18 @@ trait PeriodOverview */ protected function getCategoryPeriodOverview(Category $category, Carbon $start, Carbon $end): array { - $this->categoryRepository = app(CategoryRepositoryInterface::class); + $this->categoryRepository = app(CategoryRepositoryInterface::class); $this->categoryRepository->setUser($category->user); $this->periodStatisticRepo = app(PeriodStatisticRepositoryInterface::class); - $range = Navigation::getViewRange(true); - [$start, $end] = $end < $start ? [$end, $start] : [$start, $end]; + $range = Navigation::getViewRange(true); + [$start, $end] = $end < $start ? [$end, $start] : [$start, $end]; /** @var array $dates */ - $dates = Navigation::blockPeriods($start, $end, $range); - $entries = []; - [$start, $end] = $this->getPeriodFromBlocks($dates, $start, $end); - $this->statistics = $this->periodStatisticRepo->allInRangeForModel($category, $start, $end); + $dates = Navigation::blockPeriods($start, $end, $range); + $entries = []; + [$start, $end] = $this->getPeriodFromBlocks($dates, $start, $end); + $this->statistics = $this->periodStatisticRepo->allInRangeForModel($category, $start, $end); Log::debug(sprintf('Count of loops: %d', count($dates))); @@ -176,11 +176,11 @@ trait PeriodOverview */ protected function getNoBudgetPeriodOverview(Carbon $start, Carbon $end): array { - $range = Navigation::getViewRange(true); + $range = Navigation::getViewRange(true); [$start, $end] = $end < $start ? [$end, $start] : [$start, $end]; - $cache = new CacheProperties(); + $cache = new CacheProperties(); $cache->addProperty($start); $cache->addProperty($end); $cache->addProperty($this->convertToPrimary); @@ -191,28 +191,28 @@ trait PeriodOverview } /** @var array $dates */ - $dates = Navigation::blockPeriods($start, $end, $range); - $entries = []; + $dates = Navigation::blockPeriods($start, $end, $range); + $entries = []; // get all expenses without a budget. /** @var GroupCollectorInterface $collector */ - $collector = app(GroupCollectorInterface::class); + $collector = app(GroupCollectorInterface::class); $collector->setRange($start, $end)->withoutBudget()->withAccountInformation()->setTypes([TransactionTypeEnum::WITHDRAWAL->value]); - $journals = $collector->getExtractedJournals(); + $journals = $collector->getExtractedJournals(); foreach ($dates as $currentDate) { $set = $this->filterJournalsByDate($journals, $currentDate['start'], $currentDate['end']); $title = Navigation::periodShow($currentDate['end'], $currentDate['period']); $entries[] = [ - 'title' => $title, - 'route' => route('budgets.no-budget', [$currentDate['start']->format('Y-m-d'), $currentDate['end']->format('Y-m-d')]), - 'total_transactions' => count($set), - 'spent' => $this->groupByCurrency($set), - 'earned' => [], - 'transferred_away' => [], - 'transferred_in' => [], - ]; + 'title' => $title, + 'route' => route('budgets.no-budget', [$currentDate['start']->format('Y-m-d'), $currentDate['end']->format('Y-m-d')]), + 'total_transactions' => count($set), + 'spent' => $this->groupByCurrency($set), + 'earned' => [], + 'transferred_away' => [], + 'transferred_in' => [], + ]; } $cache->store($entries); @@ -229,38 +229,38 @@ trait PeriodOverview protected function getNoCategoryPeriodOverview(Carbon $theDate): array { 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); + $range = Navigation::getViewRange(true); + $first = $this->journalRepos->firstNull(); + $start = null === $first ? new Carbon() : $first->date; + $end = clone $theDate; + $end = Navigation::endOfPeriod($end, $range); 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); - $entries = []; + $dates = Navigation::blockPeriods($start, $end, $range); + $entries = []; // collect all expenses in this period: /** @var GroupCollectorInterface $collector */ - $collector = app(GroupCollectorInterface::class); + $collector = app(GroupCollectorInterface::class); $collector->withoutCategory(); $collector->setRange($start, $end); $collector->setTypes([TransactionTypeEnum::DEPOSIT->value]); - $earnedSet = $collector->getExtractedJournals(); + $earnedSet = $collector->getExtractedJournals(); // collect all income in this period: /** @var GroupCollectorInterface $collector */ - $collector = app(GroupCollectorInterface::class); + $collector = app(GroupCollectorInterface::class); $collector->withoutCategory(); $collector->setRange($start, $end); $collector->setTypes([TransactionTypeEnum::WITHDRAWAL->value]); - $spentSet = $collector->getExtractedJournals(); + $spentSet = $collector->getExtractedJournals(); // collect all transfers in this period: /** @var GroupCollectorInterface $collector */ - $collector = app(GroupCollectorInterface::class); + $collector = app(GroupCollectorInterface::class); $collector->withoutCategory(); $collector->setRange($start, $end); $collector->setTypes([TransactionTypeEnum::TRANSFER->value]); @@ -274,20 +274,19 @@ trait PeriodOverview $title = Navigation::periodShow($currentDate['end'], $currentDate['period']); $entries[] = [ - 'title' => $title, - 'route' => route('categories.no-category', [$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('categories.no-category', [$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), + ]; } 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'))); @@ -299,7 +298,7 @@ trait PeriodOverview ]; $this->transactions = []; foreach ($types as $type) { - $set = $this->getSingleAccountPeriodByType($account, $start, $end, $type); + $set = $this->getSingleAccountPeriodByType($account, $start, $end, $type); $return['total_transactions'] += $set['count']; unset($set['count']); $return[$type] = $set; @@ -319,7 +318,7 @@ trait PeriodOverview ]; $this->transactions = []; foreach ($types as $type) { - $set = $this->getSingleCategoryPeriodByType($category, $start, $end, $type); + $set = $this->getSingleCategoryPeriodByType($category, $start, $end, $type); $return['total_transactions'] += $set['count']; unset($set['count']); $return[$type] = $set; @@ -339,7 +338,7 @@ trait PeriodOverview ]; $this->transactions = []; foreach ($types as $type) { - $set = $this->getSingleTagPeriodByType($tag, $start, $end, $type); + $set = $this->getSingleTagPeriodByType($tag, $start, $end, $type); $return['total_transactions'] += $set['count']; unset($set['count']); $return[$type] = $set; @@ -413,15 +412,15 @@ trait PeriodOverview return $grouped; } - $grouped = [ + $grouped = [ 'count' => 0, ]; /** @var PeriodStatistic $statistic */ foreach ($statistics as $statistic) { - $id = (int)$statistic->transaction_currency_id; - $currency = Amount::getTransactionCurrencyById($id); - $grouped[$id] = [ + $id = (int)$statistic->transaction_currency_id; + $currency = Amount::getTransactionCurrencyById($id); + $grouped[$id] = [ 'amount' => (string)$statistic->amount, 'count' => (int)$statistic->count, 'currency_id' => $currency->id, @@ -481,15 +480,15 @@ trait PeriodOverview return $grouped; } - $grouped = [ + $grouped = [ 'count' => 0, ]; /** @var PeriodStatistic $statistic */ foreach ($statistics as $statistic) { - $id = (int)$statistic->transaction_currency_id; - $currency = Amount::getTransactionCurrencyById($id); - $grouped[$id] = [ + $id = (int)$statistic->transaction_currency_id; + $currency = Amount::getTransactionCurrencyById($id); + $grouped[$id] = [ 'amount' => (string)$statistic->amount, 'count' => (int)$statistic->count, 'currency_id' => $currency->id, @@ -549,15 +548,15 @@ trait PeriodOverview return $grouped; } - $grouped = [ + $grouped = [ 'count' => 0, ]; /** @var PeriodStatistic $statistic */ foreach ($statistics as $statistic) { - $id = (int)$statistic->transaction_currency_id; - $currency = Amount::getTransactionCurrencyById($id); - $grouped[$id] = [ + $id = (int)$statistic->transaction_currency_id; + $currency = Amount::getTransactionCurrencyById($id); + $grouped[$id] = [ 'amount' => (string)$statistic->amount, 'count' => (int)$statistic->count, 'currency_id' => $currency->id, @@ -579,18 +578,18 @@ trait PeriodOverview */ protected function getTagPeriodOverview(Tag $tag, Carbon $start, Carbon $end): array // period overview for tags. { - $this->tagRepository = app(TagRepositoryInterface::class); + $this->tagRepository = app(TagRepositoryInterface::class); $this->tagRepository->setUser($tag->user); $this->periodStatisticRepo = app(PeriodStatisticRepositoryInterface::class); - $range = Navigation::getViewRange(true); - [$start, $end] = $end < $start ? [$end, $start] : [$start, $end]; + $range = Navigation::getViewRange(true); + [$start, $end] = $end < $start ? [$end, $start] : [$start, $end]; /** @var array $dates */ - $dates = Navigation::blockPeriods($start, $end, $range); - $entries = []; - [$start, $end] = $this->getPeriodFromBlocks($dates, $start, $end); - $this->statistics = $this->periodStatisticRepo->allInRangeForModel($tag, $start, $end); + $dates = Navigation::blockPeriods($start, $end, $range); + $entries = []; + [$start, $end] = $this->getPeriodFromBlocks($dates, $start, $end); + $this->statistics = $this->periodStatisticRepo->allInRangeForModel($tag, $start, $end); Log::debug(sprintf('Count of loops: %d', count($dates))); @@ -601,41 +600,41 @@ trait PeriodOverview return $entries; - $range = Navigation::getViewRange(true); - [$start, $end] = $end < $start ? [$end, $start] : [$start, $end]; + $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); + $entries = []; // collect all expenses in this period: /** @var GroupCollectorInterface $collector */ - $collector = app(GroupCollectorInterface::class); + $collector = app(GroupCollectorInterface::class); $collector->setTag($tag); $collector->setRange($start, $end); $collector->setTypes([TransactionTypeEnum::DEPOSIT->value]); - $earnedSet = $collector->getExtractedJournals(); + $earnedSet = $collector->getExtractedJournals(); // collect all income in this period: /** @var GroupCollectorInterface $collector */ - $collector = app(GroupCollectorInterface::class); + $collector = app(GroupCollectorInterface::class); $collector->setTag($tag); $collector->setRange($start, $end); $collector->setTypes([TransactionTypeEnum::WITHDRAWAL->value]); - $spentSet = $collector->getExtractedJournals(); + $spentSet = $collector->getExtractedJournals(); // collect all transfers in this period: /** @var GroupCollectorInterface $collector */ - $collector = app(GroupCollectorInterface::class); + $collector = app(GroupCollectorInterface::class); $collector->setTag($tag); $collector->setRange($start, $end); $collector->setTypes([TransactionTypeEnum::TRANSFER->value]); - $transferSet = $collector->getExtractedJournals(); + $transferSet = $collector->getExtractedJournals(); // filer all of them: - $earnedSet = $this->filterJournalsByTag($earnedSet, $tag); - $spentSet = $this->filterJournalsByTag($spentSet, $tag); - $transferSet = $this->filterJournalsByTag($transferSet, $tag); + $earnedSet = $this->filterJournalsByTag($earnedSet, $tag); + $spentSet = $this->filterJournalsByTag($spentSet, $tag); + $transferSet = $this->filterJournalsByTag($transferSet, $tag); foreach ($dates as $currentDate) { $spent = $this->filterJournalsByDate($spentSet, $currentDate['start'], $currentDate['end']); @@ -644,17 +643,17 @@ trait PeriodOverview $title = Navigation::periodShow($currentDate['end'], $currentDate['period']); $entries[] = [ - 'transactions' => 0, - 'title' => $title, - 'route' => route( - 'tags.show', - [$tag->id, $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), - ]; + 'transactions' => 0, + 'title' => $title, + 'route' => route( + 'tags.show', + [$tag->id, $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), + ]; } return $entries; @@ -665,12 +664,12 @@ trait PeriodOverview */ protected function getTransactionPeriodOverview(string $transactionType, Carbon $start, Carbon $end): array { - $range = Navigation::getViewRange(true); - $types = config(sprintf('firefly.transactionTypesByType.%s', $transactionType)); + $range = Navigation::getViewRange(true); + $types = config(sprintf('firefly.transactionTypesByType.%s', $transactionType)); [$start, $end] = $end < $start ? [$end, $start] : [$start, $end]; // properties for cache - $cache = new CacheProperties(); + $cache = new CacheProperties(); $cache->addProperty($start); $cache->addProperty($end); $cache->addProperty('transactions-period-entries'); @@ -680,16 +679,16 @@ trait PeriodOverview } /** @var array $dates */ - $dates = Navigation::blockPeriods($start, $end, $range); - $entries = []; - $spent = []; - $earned = []; - $transferred = []; + $dates = Navigation::blockPeriods($start, $end, $range); + $entries = []; + $spent = []; + $earned = []; + $transferred = []; // collect all journals in this period (regardless of type) - $collector = app(GroupCollectorInterface::class); + $collector = app(GroupCollectorInterface::class); $collector->setTypes($types)->setRange($start, $end); - $genericSet = $collector->getExtractedJournals(); - $loops = 0; + $genericSet = $collector->getExtractedJournals(); + $loops = 0; foreach ($dates as $currentDate) { $title = Navigation::periodShow($currentDate['end'], $currentDate['period']); @@ -707,14 +706,14 @@ 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; } @@ -755,7 +754,7 @@ trait PeriodOverview { $return = []; foreach ($set as $entry) { - $found = false; + $found = false; /** @var array $localTag */ foreach ($entry['tags'] as $localTag) { @@ -864,13 +863,13 @@ trait PeriodOverview /** @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'; + $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'; diff --git a/app/Support/Singleton/PreferencesSingleton.php b/app/Support/Singleton/PreferencesSingleton.php index 716eddc1e8..646e1c60bf 100644 --- a/app/Support/Singleton/PreferencesSingleton.php +++ b/app/Support/Singleton/PreferencesSingleton.php @@ -38,7 +38,7 @@ class PreferencesSingleton public static function getInstance(): self { - if (!self::$instance instanceof PreferencesSingleton) { + if (!self::$instance instanceof self) { self::$instance = new self(); } diff --git a/config/firefly.php b/config/firefly.php index c990ec4a8e..da4d0d6233 100644 --- a/config/firefly.php +++ b/config/firefly.php @@ -78,8 +78,8 @@ return [ 'running_balance_column' => env('USE_RUNNING_BALANCE', false), // see cer.php for exchange rates feature flag. ], - 'version' => 'develop/2025-09-26', - 'build_time' => 1758914637, + 'version' => 'develop/2025-09-27', + 'build_time' => 1758945787, 'api_version' => '2.1.0', // field is no longer used. 'db_version' => 27, From 2cc8568077a3b9275bbc6d0215106abd2b0a42c3 Mon Sep 17 00:00:00 2001 From: James Cole Date: Sat, 27 Sep 2025 06:40:56 +0200 Subject: [PATCH 57/58] Clean up code. --- .../Http/Controllers/PeriodOverview.php | 498 ++++-------------- 1 file changed, 111 insertions(+), 387 deletions(-) diff --git a/app/Support/Http/Controllers/PeriodOverview.php b/app/Support/Http/Controllers/PeriodOverview.php index dcf3d574ad..e5b020e772 100644 --- a/app/Support/Http/Controllers/PeriodOverview.php +++ b/app/Support/Http/Controllers/PeriodOverview.php @@ -44,6 +44,7 @@ use FireflyIII\Support\Facades\Steam; use Illuminate\Database\Eloquent\Model; use Illuminate\Support\Collection; use Illuminate\Support\Facades\Log; +use Illuminate\Support\Str; /** * Trait PeriodOverview. @@ -76,7 +77,7 @@ trait PeriodOverview { protected AccountRepositoryInterface $accountRepository; protected CategoryRepositoryInterface $categoryRepository; - protected TagRepositoryInterface $tagRepository; + protected TagRepositoryInterface $tagRepository; protected JournalRepositoryInterface $journalRepos; protected PeriodStatisticRepositoryInterface $periodStatisticRepo; private Collection $statistics; // temp data holder @@ -92,27 +93,21 @@ trait PeriodOverview protected function getAccountPeriodOverview(Account $account, Carbon $start, Carbon $end): array { 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->accountRepository = app(AccountRepositoryInterface::class); $this->accountRepository->setUser($account->user); $this->periodStatisticRepo = app(PeriodStatisticRepositoryInterface::class); $range = Navigation::getViewRange(true); - [$start, $end] = $end < $start ? [$end, $start] : [$start, $end]; + [$start, $end] = $end < $start ? [$end, $start] : [$start, $end]; /** @var array $dates */ - $dates = Navigation::blockPeriods($start, $end, $range); - [$start, $end] = $this->getPeriodFromBlocks($dates, $start, $end); - $this->statistics = $this->periodStatisticRepo->allInRangeForModel($account, $start, $end); + $dates = Navigation::blockPeriods($start, $end, $range); + [$start, $end] = $this->getPeriodFromBlocks($dates, $start, $end); + $this->statistics = $this->periodStatisticRepo->allInRangeForModel($account, $start, $end); - // 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 = []; + $entries = []; Log::debug(sprintf('Count of loops: %d', count($dates))); foreach ($dates as $currentDate) { - $entries[] = $this->getSingleAccountPeriod($account, $currentDate['period'], $currentDate['start'], $currentDate['end']); + $entries[] = $this->getSingleModelPeriod($account, $currentDate['period'], $currentDate['start'], $currentDate['end']); } Log::debug('End of getAccountPeriodOverview()'); @@ -145,23 +140,23 @@ trait PeriodOverview */ protected function getCategoryPeriodOverview(Category $category, Carbon $start, Carbon $end): array { - $this->categoryRepository = app(CategoryRepositoryInterface::class); + $this->categoryRepository = app(CategoryRepositoryInterface::class); $this->categoryRepository->setUser($category->user); $this->periodStatisticRepo = app(PeriodStatisticRepositoryInterface::class); - $range = Navigation::getViewRange(true); - [$start, $end] = $end < $start ? [$end, $start] : [$start, $end]; + $range = Navigation::getViewRange(true); + [$start, $end] = $end < $start ? [$end, $start] : [$start, $end]; /** @var array $dates */ - $dates = Navigation::blockPeriods($start, $end, $range); - $entries = []; - [$start, $end] = $this->getPeriodFromBlocks($dates, $start, $end); - $this->statistics = $this->periodStatisticRepo->allInRangeForModel($category, $start, $end); + $dates = Navigation::blockPeriods($start, $end, $range); + $entries = []; + [$start, $end] = $this->getPeriodFromBlocks($dates, $start, $end); + $this->statistics = $this->periodStatisticRepo->allInRangeForModel($category, $start, $end); Log::debug(sprintf('Count of loops: %d', count($dates))); foreach ($dates as $currentDate) { - $entries[] = $this->getSingleCategoryPeriod($category, $currentDate['period'], $currentDate['start'], $currentDate['end']); + $entries[] = $this->getSingleModelPeriod($category, $currentDate['period'], $currentDate['start'], $currentDate['end']); } return $entries; @@ -176,11 +171,11 @@ trait PeriodOverview */ protected function getNoBudgetPeriodOverview(Carbon $start, Carbon $end): array { - $range = Navigation::getViewRange(true); + $range = Navigation::getViewRange(true); [$start, $end] = $end < $start ? [$end, $start] : [$start, $end]; - $cache = new CacheProperties(); + $cache = new CacheProperties(); $cache->addProperty($start); $cache->addProperty($end); $cache->addProperty($this->convertToPrimary); @@ -191,28 +186,28 @@ trait PeriodOverview } /** @var array $dates */ - $dates = Navigation::blockPeriods($start, $end, $range); - $entries = []; + $dates = Navigation::blockPeriods($start, $end, $range); + $entries = []; // get all expenses without a budget. /** @var GroupCollectorInterface $collector */ - $collector = app(GroupCollectorInterface::class); + $collector = app(GroupCollectorInterface::class); $collector->setRange($start, $end)->withoutBudget()->withAccountInformation()->setTypes([TransactionTypeEnum::WITHDRAWAL->value]); - $journals = $collector->getExtractedJournals(); + $journals = $collector->getExtractedJournals(); foreach ($dates as $currentDate) { $set = $this->filterJournalsByDate($journals, $currentDate['start'], $currentDate['end']); $title = Navigation::periodShow($currentDate['end'], $currentDate['period']); $entries[] = [ - 'title' => $title, - 'route' => route('budgets.no-budget', [$currentDate['start']->format('Y-m-d'), $currentDate['end']->format('Y-m-d')]), - 'total_transactions' => count($set), - 'spent' => $this->groupByCurrency($set), - 'earned' => [], - 'transferred_away' => [], - 'transferred_in' => [], - ]; + 'title' => $title, + 'route' => route('budgets.no-budget', [$currentDate['start']->format('Y-m-d'), $currentDate['end']->format('Y-m-d')]), + 'total_transactions' => count($set), + 'spent' => $this->groupByCurrency($set), + 'earned' => [], + 'transferred_away' => [], + 'transferred_in' => [], + ]; } $cache->store($entries); @@ -229,38 +224,38 @@ trait PeriodOverview protected function getNoCategoryPeriodOverview(Carbon $theDate): array { 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); + $range = Navigation::getViewRange(true); + $first = $this->journalRepos->firstNull(); + $start = null === $first ? new Carbon() : $first->date; + $end = clone $theDate; + $end = Navigation::endOfPeriod($end, $range); 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); - $entries = []; + $dates = Navigation::blockPeriods($start, $end, $range); + $entries = []; // collect all expenses in this period: /** @var GroupCollectorInterface $collector */ - $collector = app(GroupCollectorInterface::class); + $collector = app(GroupCollectorInterface::class); $collector->withoutCategory(); $collector->setRange($start, $end); $collector->setTypes([TransactionTypeEnum::DEPOSIT->value]); - $earnedSet = $collector->getExtractedJournals(); + $earnedSet = $collector->getExtractedJournals(); // collect all income in this period: /** @var GroupCollectorInterface $collector */ - $collector = app(GroupCollectorInterface::class); + $collector = app(GroupCollectorInterface::class); $collector->withoutCategory(); $collector->setRange($start, $end); $collector->setTypes([TransactionTypeEnum::WITHDRAWAL->value]); - $spentSet = $collector->getExtractedJournals(); + $spentSet = $collector->getExtractedJournals(); // collect all transfers in this period: /** @var GroupCollectorInterface $collector */ - $collector = app(GroupCollectorInterface::class); + $collector = app(GroupCollectorInterface::class); $collector->withoutCategory(); $collector->setRange($start, $end); $collector->setTypes([TransactionTypeEnum::TRANSFER->value]); @@ -274,31 +269,31 @@ trait PeriodOverview $title = Navigation::periodShow($currentDate['end'], $currentDate['period']); $entries[] = [ - 'title' => $title, - 'route' => route('categories.no-category', [$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('categories.no-category', [$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), + ]; } Log::debug('End of loops'); return $entries; } - protected function getSingleAccountPeriod(Account $account, string $period, Carbon $start, Carbon $end): array + protected function getSingleModelPeriod(Model $model, 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'))); + Log::debug(sprintf('Now in getSingleModelPeriod(%s #%d, %s %s)', $model::class, $model->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')]), + 'route' => route(sprintf('%s.show', strtolower(Str::plural(class_basename($model)))), [$model->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); + $set = $this->getSingleModelPeriodByType($model, $start, $end, $type); $return['total_transactions'] += $set['count']; unset($set['count']); $return[$type] = $set; @@ -307,45 +302,6 @@ trait PeriodOverview return $return; } - protected function getSingleCategoryPeriod(Category $category, string $period, Carbon $start, Carbon $end): array - { - Log::debug(sprintf('Now in getSingleCategoryPeriod(#%d, %s %s)', $category->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('categories.show', [$category->id, $start->format('Y-m-d'), $end->format('Y-m-d')]), - 'total_transactions' => 0, - ]; - $this->transactions = []; - foreach ($types as $type) { - $set = $this->getSingleCategoryPeriodByType($category, $start, $end, $type); - $return['total_transactions'] += $set['count']; - unset($set['count']); - $return[$type] = $set; - } - - return $return; - } - - protected function getSingleTagPeriod(Tag $tag, string $period, Carbon $start, Carbon $end): array - { - Log::debug(sprintf('Now in getSingleTagPeriod(#%d, %s %s)', $tag->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('tags.show', [$tag->id, $start->format('Y-m-d'), $end->format('Y-m-d')]), - 'total_transactions' => 0, - ]; - $this->transactions = []; - foreach ($types as $type) { - $set = $this->getSingleTagPeriodByType($tag, $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 { @@ -368,83 +324,30 @@ trait PeriodOverview ); } - protected function getSingleAccountPeriodByType(Account $account, Carbon $start, Carbon $end, string $type): array + + + protected function getSingleModelPeriodByType(Model $model, 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)); + Log::debug(sprintf('Now in getSingleModelPeriodByType(%s #%d, %s %s, %s)', $model::class, $model->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; - } - - protected function getSingleCategoryPeriodByType(Category $category, Carbon $start, Carbon $end, string $type): array - { - Log::debug(sprintf('Now in getSingleCategoryPeriodByType(#%d, %s %s, %s)', $category->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->categoryRepository->periodCollection($category, $start, $end); + switch ($model::class) { + default: + throw new FireflyException(sprintf('Cannot deal with model of type "%s"', $model::class)); + case Category::class: + $this->transactions = $this->categoryRepository->periodCollection($model, $start, $end); + break; + case Account::class: + $this->transactions = $this->accountRepository->periodCollection($model, $start, $end); + break; + case Tag::class: + $this->transactions = $this->tagRepository->periodCollection($model, $start, $end); + break; + } } switch ($type) { @@ -476,19 +379,19 @@ trait PeriodOverview Log::debug(sprintf('Going to group %d found journal(s)', count($result))); $grouped = $this->groupByCurrency($result); - $this->saveGroupedAsStatistics($category, $start, $end, $type, $grouped); + $this->saveGroupedAsStatistics($model, $start, $end, $type, $grouped); return $grouped; } - $grouped = [ + $grouped = [ 'count' => 0, ]; /** @var PeriodStatistic $statistic */ foreach ($statistics as $statistic) { - $id = (int)$statistic->transaction_currency_id; - $currency = Amount::getTransactionCurrencyById($id); - $grouped[$id] = [ + $id = (int)$statistic->transaction_currency_id; + $currency = Amount::getTransactionCurrencyById($id); + $grouped[$id] = [ 'amount' => (string)$statistic->amount, 'count' => (int)$statistic->count, 'currency_id' => $currency->id, @@ -503,73 +406,6 @@ trait PeriodOverview return $grouped; } - protected function getSingleTagPeriodByType(Tag $tag, Carbon $start, Carbon $end, string $type): array - { - Log::debug(sprintf('Now in getSingleTagPeriodByType(#%d, %s %s, %s)', $tag->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->tagRepository->periodCollection($tag, $start, $end); - } - - switch ($type) { - default: - throw new FireflyException(sprintf('Cannot deal with tag 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($tag, $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. @@ -578,82 +414,23 @@ trait PeriodOverview */ protected function getTagPeriodOverview(Tag $tag, Carbon $start, Carbon $end): array // period overview for tags. { - $this->tagRepository = app(TagRepositoryInterface::class); + $this->tagRepository = app(TagRepositoryInterface::class); $this->tagRepository->setUser($tag->user); $this->periodStatisticRepo = app(PeriodStatisticRepositoryInterface::class); - $range = Navigation::getViewRange(true); - [$start, $end] = $end < $start ? [$end, $start] : [$start, $end]; + $range = Navigation::getViewRange(true); + [$start, $end] = $end < $start ? [$end, $start] : [$start, $end]; /** @var array $dates */ - $dates = Navigation::blockPeriods($start, $end, $range); - $entries = []; - [$start, $end] = $this->getPeriodFromBlocks($dates, $start, $end); - $this->statistics = $this->periodStatisticRepo->allInRangeForModel($tag, $start, $end); + $dates = Navigation::blockPeriods($start, $end, $range); + $entries = []; + [$start, $end] = $this->getPeriodFromBlocks($dates, $start, $end); + $this->statistics = $this->periodStatisticRepo->allInRangeForModel($tag, $start, $end); Log::debug(sprintf('Count of loops: %d', count($dates))); foreach ($dates as $currentDate) { - $entries[] = $this->getSingleTagPeriod($tag, $currentDate['period'], $currentDate['start'], $currentDate['end']); - } - - return $entries; - - - $range = Navigation::getViewRange(true); - [$start, $end] = $end < $start ? [$end, $start] : [$start, $end]; - - /** @var array $dates */ - $dates = Navigation::blockPeriods($start, $end, $range); - $entries = []; - - // collect all expenses in this period: - /** @var GroupCollectorInterface $collector */ - $collector = app(GroupCollectorInterface::class); - $collector->setTag($tag); - $collector->setRange($start, $end); - $collector->setTypes([TransactionTypeEnum::DEPOSIT->value]); - $earnedSet = $collector->getExtractedJournals(); - - // collect all income in this period: - /** @var GroupCollectorInterface $collector */ - $collector = app(GroupCollectorInterface::class); - $collector->setTag($tag); - $collector->setRange($start, $end); - $collector->setTypes([TransactionTypeEnum::WITHDRAWAL->value]); - $spentSet = $collector->getExtractedJournals(); - - // collect all transfers in this period: - /** @var GroupCollectorInterface $collector */ - $collector = app(GroupCollectorInterface::class); - $collector->setTag($tag); - $collector->setRange($start, $end); - $collector->setTypes([TransactionTypeEnum::TRANSFER->value]); - $transferSet = $collector->getExtractedJournals(); - - // filer all of them: - $earnedSet = $this->filterJournalsByTag($earnedSet, $tag); - $spentSet = $this->filterJournalsByTag($spentSet, $tag); - $transferSet = $this->filterJournalsByTag($transferSet, $tag); - - foreach ($dates as $currentDate) { - $spent = $this->filterJournalsByDate($spentSet, $currentDate['start'], $currentDate['end']); - $earned = $this->filterJournalsByDate($earnedSet, $currentDate['start'], $currentDate['end']); - $transferred = $this->filterJournalsByDate($transferSet, $currentDate['start'], $currentDate['end']); - $title = Navigation::periodShow($currentDate['end'], $currentDate['period']); - $entries[] - = [ - 'transactions' => 0, - 'title' => $title, - 'route' => route( - 'tags.show', - [$tag->id, $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), - ]; + $entries[] = $this->getSingleModelPeriod($tag, $currentDate['period'], $currentDate['start'], $currentDate['end']); } return $entries; @@ -664,12 +441,12 @@ trait PeriodOverview */ protected function getTransactionPeriodOverview(string $transactionType, Carbon $start, Carbon $end): array { - $range = Navigation::getViewRange(true); - $types = config(sprintf('firefly.transactionTypesByType.%s', $transactionType)); + $range = Navigation::getViewRange(true); + $types = config(sprintf('firefly.transactionTypesByType.%s', $transactionType)); [$start, $end] = $end < $start ? [$end, $start] : [$start, $end]; // properties for cache - $cache = new CacheProperties(); + $cache = new CacheProperties(); $cache->addProperty($start); $cache->addProperty($end); $cache->addProperty('transactions-period-entries'); @@ -679,16 +456,16 @@ trait PeriodOverview } /** @var array $dates */ - $dates = Navigation::blockPeriods($start, $end, $range); - $entries = []; - $spent = []; - $earned = []; - $transferred = []; + $dates = Navigation::blockPeriods($start, $end, $range); + $entries = []; + $spent = []; + $earned = []; + $transferred = []; // collect all journals in this period (regardless of type) - $collector = app(GroupCollectorInterface::class); + $collector = app(GroupCollectorInterface::class); $collector->setTypes($types)->setRange($start, $end); - $genericSet = $collector->getExtractedJournals(); - $loops = 0; + $genericSet = $collector->getExtractedJournals(); + $loops = 0; foreach ($dates as $currentDate) { $title = Navigation::periodShow($currentDate['end'], $currentDate['period']); @@ -706,14 +483,14 @@ 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; } @@ -750,26 +527,6 @@ trait PeriodOverview 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 { @@ -795,39 +552,6 @@ trait PeriodOverview return $result; } - /** - * Return only transactions where $account is the source. - */ - private function filterTransferredAway(Account $account, array $journals): array - { - $return = []; - - /** @var array $journal */ - foreach ($journals as $journal) { - if ($account->id === (int)$journal['source_account_id']) { - $return[] = $journal; - } - } - - return $return; - } - - /** - * Return only transactions where $account is the source. - */ - private function filterTransferredIn(Account $account, array $journals): array - { - $return = []; - - /** @var array $journal */ - foreach ($journals as $journal) { - if ($account->id === (int)$journal['destination_account_id']) { - $return[] = $journal; - } - } - - return $return; - } private function filterTransfers(string $direction, Carbon $start, Carbon $end): array { @@ -863,13 +587,13 @@ trait PeriodOverview /** @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'; + $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'; @@ -898,7 +622,7 @@ trait PeriodOverview ]; - $return[$currencyId]['amount'] = bcadd((string) $return[$currencyId]['amount'], $amount); + $return[$currencyId]['amount'] = bcadd((string)$return[$currencyId]['amount'], $amount); ++$return[$currencyId]['count']; ++$return['count']; } From 1ff47441cefc131e8dec121bbce5eb635884081f Mon Sep 17 00:00:00 2001 From: James Cole Date: Sat, 27 Sep 2025 16:07:03 +0200 Subject: [PATCH 58/58] Also add no budget and no category overview. --- .../Controllers/Budget/ShowController.php | 2 +- .../Category/NoCategoryController.php | 21 +- app/Models/PeriodStatistic.php | 6 + app/Models/UserGroup.php | 8 + app/Providers/FireflyServiceProvider.php | 13 +- .../PeriodStatisticRepository.php | 84 ++++-- .../PeriodStatisticRepositoryInterface.php | 2 + .../Http/Controllers/PeriodOverview.php | 246 ++++++++++-------- ..._09_25_175248_create_period_statistics.php | 5 + 9 files changed, 245 insertions(+), 142 deletions(-) diff --git a/app/Http/Controllers/Budget/ShowController.php b/app/Http/Controllers/Budget/ShowController.php index 36815436a4..ce344f13e8 100644 --- a/app/Http/Controllers/Budget/ShowController.php +++ b/app/Http/Controllers/Budget/ShowController.php @@ -92,7 +92,7 @@ class ShowController extends Controller // get first journal ever to set off the budget period overview. $first = $this->journalRepos->firstNull(); $firstDate = $first instanceof TransactionJournal ? $first->date : $start; - $periods = $this->getNoBudgetPeriodOverview($firstDate, $end); + $periods = $this->getNoModelPeriodOverview('budget', $firstDate, $end); $page = (int) $request->get('page'); $pageSize = (int) app('preferences')->get('listPageSize', 50)->data; diff --git a/app/Http/Controllers/Category/NoCategoryController.php b/app/Http/Controllers/Category/NoCategoryController.php index be784e6aa6..4c12faeef3 100644 --- a/app/Http/Controllers/Category/NoCategoryController.php +++ b/app/Http/Controllers/Category/NoCategoryController.php @@ -35,6 +35,7 @@ use FireflyIII\Support\Http\Controllers\PeriodOverview; use Illuminate\Contracts\View\Factory; use Illuminate\Http\Request; use Illuminate\Support\Collection; +use Illuminate\Support\Facades\Log; use Illuminate\View\View; /** @@ -74,7 +75,7 @@ class NoCategoryController extends Controller */ public function show(Request $request, ?Carbon $start = null, ?Carbon $end = null) { - app('log')->debug('Start of noCategory()'); + Log::debug('Start of noCategory()'); $start ??= session('start'); $end ??= session('end'); @@ -82,14 +83,12 @@ class NoCategoryController extends Controller /** @var Carbon $end */ $page = (int) $request->get('page'); $pageSize = (int) app('preferences')->get('listPageSize', 50)->data; - $subTitle = trans( - 'firefly.without_category_between', - ['start' => $start->isoFormat($this->monthAndDayFormat), 'end' => $end->isoFormat($this->monthAndDayFormat)] - ); - $periods = $this->getNoCategoryPeriodOverview($start); + $subTitle = trans('firefly.without_category_between', ['start' => $start->isoFormat($this->monthAndDayFormat), 'end' => $end->isoFormat($this->monthAndDayFormat)]); + $first = $this->journalRepos->firstNull()->date ?? clone $start; + $periods = $this->getNoModelPeriodOverview('category', $first, $end); - app('log')->debug(sprintf('Start for noCategory() is %s', $start->format('Y-m-d'))); - app('log')->debug(sprintf('End for noCategory() is %s', $end->format('Y-m-d'))); + Log::debug(sprintf('Start for noCategory() is %s', $start->format('Y-m-d'))); + Log::debug(sprintf('End for noCategory() is %s', $end->format('Y-m-d'))); /** @var GroupCollectorInterface $collector */ $collector = app(GroupCollectorInterface::class); @@ -117,13 +116,13 @@ class NoCategoryController extends Controller $periods = new Collection(); $page = (int) $request->get('page'); $pageSize = (int) app('preferences')->get('listPageSize', 50)->data; - app('log')->debug('Start of noCategory()'); + Log::debug('Start of noCategory()'); $subTitle = (string) trans('firefly.all_journals_without_category'); $first = $this->journalRepos->firstNull(); $start = $first instanceof TransactionJournal ? $first->date : new Carbon(); $end = today(config('app.timezone')); - app('log')->debug(sprintf('Start for noCategory() is %s', $start->format('Y-m-d'))); - app('log')->debug(sprintf('End for noCategory() is %s', $end->format('Y-m-d'))); + Log::debug(sprintf('Start for noCategory() is %s', $start->format('Y-m-d'))); + Log::debug(sprintf('End for noCategory() is %s', $end->format('Y-m-d'))); /** @var GroupCollectorInterface $collector */ $collector = app(GroupCollectorInterface::class); diff --git a/app/Models/PeriodStatistic.php b/app/Models/PeriodStatistic.php index 9a167fafe0..c8878cfc9b 100644 --- a/app/Models/PeriodStatistic.php +++ b/app/Models/PeriodStatistic.php @@ -8,6 +8,7 @@ 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\BelongsTo; use Illuminate\Database\Eloquent\Relations\MorphTo; class PeriodStatistic extends Model @@ -24,6 +25,11 @@ class PeriodStatistic extends Model ]; } + public function userGroup(): BelongsTo + { + return $this->belongsTo(UserGroup::class); + } + protected function count(): Attribute { return Attribute::make( diff --git a/app/Models/UserGroup.php b/app/Models/UserGroup.php index 0c142b69cb..e0ff63268a 100644 --- a/app/Models/UserGroup.php +++ b/app/Models/UserGroup.php @@ -76,6 +76,14 @@ class UserGroup extends Model return $this->hasMany(Account::class); } + /** + * Link to accounts. + */ + public function periodStatistics(): HasMany + { + return $this->hasMany(PeriodStatistic::class); + } + /** * Link to attachments. */ diff --git a/app/Providers/FireflyServiceProvider.php b/app/Providers/FireflyServiceProvider.php index cff9567090..e900b72f9e 100644 --- a/app/Providers/FireflyServiceProvider.php +++ b/app/Providers/FireflyServiceProvider.php @@ -163,7 +163,6 @@ 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 { @@ -177,6 +176,18 @@ class FireflyServiceProvider extends ServiceProvider } ); + $this->app->bind( + static function (Application $app): PeriodStatisticRepositoryInterface { + /** @var PeriodStatisticRepository $repository */ + $repository = app(PeriodStatisticRepository::class); + if ($app->auth->check()) { // @phpstan-ignore-line (phpstan does not understand the reference to auth) + $repository->setUser(auth()->user()); + } + + return $repository; + } + ); + $this->app->bind( static function (Application $app): WebhookRepositoryInterface { /** @var WebhookRepository $repository */ diff --git a/app/Repositories/PeriodStatistic/PeriodStatisticRepository.php b/app/Repositories/PeriodStatistic/PeriodStatisticRepository.php index 07523c5348..3606ddf0d8 100644 --- a/app/Repositories/PeriodStatistic/PeriodStatisticRepository.php +++ b/app/Repositories/PeriodStatistic/PeriodStatisticRepository.php @@ -25,37 +25,40 @@ namespace FireflyIII\Repositories\PeriodStatistic; use Carbon\Carbon; use FireflyIII\Models\PeriodStatistic; +use FireflyIII\Support\Repositories\UserGroup\UserGroupInterface; +use FireflyIII\Support\Repositories\UserGroup\UserGroupTrait; use Illuminate\Database\Eloquent\Model; use Illuminate\Support\Collection; use Illuminate\Support\Facades\Log; -class PeriodStatisticRepository implements PeriodStatisticRepositoryInterface +class PeriodStatisticRepository implements PeriodStatisticRepositoryInterface, UserGroupInterface { + use UserGroupTrait; + 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() - ; + ->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() - ; + ->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 = new PeriodStatistic(); $stat->primaryStatable()->associate($model); $stat->transaction_currency_id = $currencyId; + $stat->user_group_id = $this->getUserGroup()->id; $stat->start = $start; $stat->start_tz = $start->format('e'); $stat->end = $end; @@ -66,16 +69,16 @@ class PeriodStatisticRepository implements PeriodStatisticRepositoryInterface $stat->save(); Log::debug(sprintf( - 'Saved #%d [currency #%d, Model %s #%d, %s to %s, %d, %s] as new statistic.', - $stat->id, - $model::class, - $model->id, - $stat->transaction_currency_id, - $stat->start->toW3cString(), - $stat->end->toW3cString(), - $count, - $amount - )); + 'Saved #%d [currency #%d, Model %s #%d, %s to %s, %d, %s] as new statistic.', + $stat->id, + $model::class, + $model->id, + $stat->transaction_currency_id, + $stat->start->toW3cString(), + $stat->end->toW3cString(), + $count, + $amount + )); return $stat; } @@ -89,4 +92,41 @@ class PeriodStatisticRepository implements PeriodStatisticRepositoryInterface { $model->primaryPeriodStatistics()->where('start', '<=', $date)->where('end', '>=', $date)->delete(); } + + #[\Override] + public function allInRangeForPrefix(string $prefix, Carbon $start, Carbon $end): Collection + { + return $this->userGroup->periodStatistics() + ->where('type', 'LIKE', sprintf('%s%%', $prefix)) + ->where('start', '>=', $start)->where('end', '<=', $end)->get(); + } + + #[\Override] + public function savePrefixedStatistic(string $prefix, int $currencyId, Carbon $start, Carbon $end, string $type, int $count, string $amount): PeriodStatistic + { + $stat = new PeriodStatistic(); + $stat->transaction_currency_id = $currencyId; + $stat->user_group_id = $this->getUserGroup()->id; + $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 = sprintf('%s_%s',$prefix, $type); + $stat->save(); + + Log::debug(sprintf( + 'Saved #%d [currency #%d, type "%s", %s to %s, %d, %s] as new statistic.', + $stat->id, + $stat->transaction_currency_id, + $stat->type, + $stat->start->toW3cString(), + $stat->end->toW3cString(), + $count, + $amount + )); + + return $stat; + } } diff --git a/app/Repositories/PeriodStatistic/PeriodStatisticRepositoryInterface.php b/app/Repositories/PeriodStatistic/PeriodStatisticRepositoryInterface.php index 6e4f7cf422..fb464e9794 100644 --- a/app/Repositories/PeriodStatistic/PeriodStatisticRepositoryInterface.php +++ b/app/Repositories/PeriodStatistic/PeriodStatisticRepositoryInterface.php @@ -35,8 +35,10 @@ interface PeriodStatisticRepositoryInterface 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 savePrefixedStatistic(string $prefix, 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 allInRangeForPrefix(string $prefix, Carbon $start, Carbon $end): Collection; public function deleteStatisticsForModel(Model $model, Carbon $date): void; } diff --git a/app/Support/Http/Controllers/PeriodOverview.php b/app/Support/Http/Controllers/PeriodOverview.php index e5b020e772..edb51c7ee3 100644 --- a/app/Support/Http/Controllers/PeriodOverview.php +++ b/app/Support/Http/Controllers/PeriodOverview.php @@ -169,117 +169,129 @@ trait PeriodOverview * * @throws FireflyException */ - protected function getNoBudgetPeriodOverview(Carbon $start, Carbon $end): array + protected function getNoModelPeriodOverview(string $model, Carbon $start, Carbon $end): array { - $range = Navigation::getViewRange(true); - + Log::debug(sprintf('Now in getNoModelPeriodOverview(%s, %s %s)', $model, $start->format('Y-m-d'), $end->format('Y-m-d'))); + $this->periodStatisticRepo = app(PeriodStatisticRepositoryInterface::class); + $range = Navigation::getViewRange(true); [$start, $end] = $end < $start ? [$end, $start] : [$start, $end]; - $cache = new CacheProperties(); - $cache->addProperty($start); - $cache->addProperty($end); - $cache->addProperty($this->convertToPrimary); - $cache->addProperty('no-budget-period-entries'); - - if ($cache->has()) { - return $cache->get(); - } - /** @var array $dates */ - $dates = Navigation::blockPeriods($start, $end, $range); - $entries = []; - - // get all expenses without a budget. - /** @var GroupCollectorInterface $collector */ - $collector = app(GroupCollectorInterface::class); - $collector->setRange($start, $end)->withoutBudget()->withAccountInformation()->setTypes([TransactionTypeEnum::WITHDRAWAL->value]); - $journals = $collector->getExtractedJournals(); + $dates = Navigation::blockPeriods($start, $end, $range); + [$start, $end] = $this->getPeriodFromBlocks($dates, $start, $end); + $entries = []; + $this->statistics = $this->periodStatisticRepo->allInRangeForPrefix(sprintf('no_%s', $model), $start, $end); + Log::debug(sprintf('Collected %d stats', $this->statistics->count())); foreach ($dates as $currentDate) { - $set = $this->filterJournalsByDate($journals, $currentDate['start'], $currentDate['end']); - $title = Navigation::periodShow($currentDate['end'], $currentDate['period']); - $entries[] - = [ - 'title' => $title, - 'route' => route('budgets.no-budget', [$currentDate['start']->format('Y-m-d'), $currentDate['end']->format('Y-m-d')]), - 'total_transactions' => count($set), - 'spent' => $this->groupByCurrency($set), - 'earned' => [], - 'transferred_away' => [], - 'transferred_in' => [], - ]; + $entries[] = $this->getSingleNoModelPeriodOverview($model, $currentDate['start'], $currentDate['end'], $currentDate['period']); } - $cache->store($entries); return $entries; } - /** - * TODO fix the date. - * - * Show period overview for no category view. - * - * @throws FireflyException - */ - protected function getNoCategoryPeriodOverview(Carbon $theDate): array + private function getSingleNoModelPeriodOverview(string $model, Carbon $start, Carbon $end, string $period): array { - 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); + Log::debug(sprintf('getSingleNoModelPeriodOverview(%s, %s, %s, %s)', $model, $start->format('Y-m-d'), $end->format('Y-m-d'), $period)); + $statistics = $this->filterPrefixedStatistics($start, $end, sprintf('no_%s', $model)); + $title = Navigation::periodShow($end, $period); - 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'))); + if (0 === $statistics->count()) { + Log::debug(sprintf('Found no statistics in period %s - %s, regenerating them.', $start->format('Y-m-d'), $end->format('Y-m-d'))); + switch ($model) { + default: + throw new FireflyException(sprintf('Cannot deal with model of type "%s"', $model)); + case 'budget': + // get all expenses without a budget. + /** @var GroupCollectorInterface $collector */ + $collector = app(GroupCollectorInterface::class); + $collector->setRange($start, $end)->withoutBudget()->withAccountInformation()->setTypes([TransactionTypeEnum::WITHDRAWAL->value]); + $spent = $collector->getExtractedJournals(); + $earned = []; + $transferred = []; + break; + case 'category': + // collect all expenses in this period: + /** @var GroupCollectorInterface $collector */ + $collector = app(GroupCollectorInterface::class); + $collector->withoutCategory(); + $collector->setRange($start, $end); + $collector->setTypes([TransactionTypeEnum::DEPOSIT->value]); + $earned = $collector->getExtractedJournals(); - // properties for cache - $dates = Navigation::blockPeriods($start, $end, $range); - $entries = []; + // collect all income in this period: + /** @var GroupCollectorInterface $collector */ + $collector = app(GroupCollectorInterface::class); + $collector->withoutCategory(); + $collector->setRange($start, $end); + $collector->setTypes([TransactionTypeEnum::WITHDRAWAL->value]); + $spent = $collector->getExtractedJournals(); - // collect all expenses in this period: - /** @var GroupCollectorInterface $collector */ - $collector = app(GroupCollectorInterface::class); - $collector->withoutCategory(); - $collector->setRange($start, $end); - $collector->setTypes([TransactionTypeEnum::DEPOSIT->value]); - $earnedSet = $collector->getExtractedJournals(); - - // collect all income in this period: - /** @var GroupCollectorInterface $collector */ - $collector = app(GroupCollectorInterface::class); - $collector->withoutCategory(); - $collector->setRange($start, $end); - $collector->setTypes([TransactionTypeEnum::WITHDRAWAL->value]); - $spentSet = $collector->getExtractedJournals(); - - // collect all transfers in this period: - /** @var GroupCollectorInterface $collector */ - $collector = app(GroupCollectorInterface::class); - $collector->withoutCategory(); - $collector->setRange($start, $end); - $collector->setTypes([TransactionTypeEnum::TRANSFER->value]); - $transferSet = $collector->getExtractedJournals(); - - /** @var array $currentDate */ - foreach ($dates as $currentDate) { - $spent = $this->filterJournalsByDate($spentSet, $currentDate['start'], $currentDate['end']); - $earned = $this->filterJournalsByDate($earnedSet, $currentDate['start'], $currentDate['end']); - $transferred = $this->filterJournalsByDate($transferSet, $currentDate['start'], $currentDate['end']); - $title = Navigation::periodShow($currentDate['end'], $currentDate['period']); - $entries[] - = [ + // collect all transfers in this period: + /** @var GroupCollectorInterface $collector */ + $collector = app(GroupCollectorInterface::class); + $collector->withoutCategory(); + $collector->setRange($start, $end); + $collector->setTypes([TransactionTypeEnum::TRANSFER->value]); + $transferred = $collector->getExtractedJournals(); + break; + } + $groupedSpent = $this->groupByCurrency($spent); + $groupedEarned = $this->groupByCurrency($earned); + $groupedTransferred = $this->groupByCurrency($transferred); + $entry + = [ 'title' => $title, - 'route' => route('categories.no-category', [$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), + 'route' => route(sprintf('%s.no-%s', Str::plural($model), $model), [$start->format('Y-m-d'), $end->format('Y-m-d')]), + 'total_transactions' => count($spent), + 'spent' => $groupedSpent, + 'earned' => $groupedEarned, + 'transferred' => $groupedTransferred, ]; + $this->saveGroupedForPrefix(sprintf('no_%s', $model), $start, $end, 'spent', $groupedSpent); + $this->saveGroupedForPrefix(sprintf('no_%s', $model), $start, $end, 'earned', $groupedEarned); + $this->saveGroupedForPrefix(sprintf('no_%s', $model), $start, $end, 'transferred', $groupedTransferred); + return $entry; } - Log::debug('End of loops'); + Log::debug(sprintf('Found %d statistics in period %s - %s.', count($statistics), $start->format('Y-m-d'), $end->format('Y-m-d'))); - return $entries; + $entry + = [ + 'title' => $title, + 'route' => route(sprintf('%s.no-%s', Str::plural($model), $model), [$start->format('Y-m-d'), $end->format('Y-m-d')]), + 'total_transactions' => 0, + 'spent' => [], + 'earned' => [], + 'transferred' => [], + ]; + $grouped = []; + /** @var PeriodStatistic $statistic */ + foreach ($statistics as $statistic) { + $type = str_replace(sprintf('no_%s_', $model), '', $statistic->type); + $id = (int)$statistic->transaction_currency_id; + $currency = Amount::getTransactionCurrencyById($id); + $grouped[$type]['count'] ??= 0; + $grouped[$type][$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[$type]['count'] += (int)$statistic->count; + } + $types = ['spent', 'earned', 'transferred']; + foreach ($types as $type) { + if (array_key_exists($type, $grouped)) { + $entry['total_transactions'] += $grouped[$type]['count']; + unset($grouped[$type]['count']); + $entry[$type] = $grouped[$type]; + } + + } + return $entry; } protected function getSingleModelPeriod(Model $model, string $period, Carbon $start, Carbon $end): array @@ -303,30 +315,34 @@ trait PeriodOverview } - protected function filterStatistics(Carbon $start, Carbon $end, string $type): Collection + private function filterStatistics(Carbon $start, Carbon $end, string $type): Collection { + if (0 === $this->statistics->count()) { + Log::warning('Have no statistic to filter!'); + return new 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; } ); } + private function filterPrefixedStatistics(Carbon $start, Carbon $end, string $prefix): Collection + { + if (0 === $this->statistics->count()) { + Log::warning('Have no statistic to filter!'); + return new Collection; + } + return $this->statistics->filter( + function (PeriodStatistic $statistic) use ($start, $end, $prefix) { + return $statistic->start->eq($start) && $statistic->end->eq($end) && str_starts_with($statistic->type, $prefix); + } + ); + } - protected function getSingleModelPeriodByType(Model $model, Carbon $start, Carbon $end, string $type): array + private function getSingleModelPeriodByType(Model $model, Carbon $start, Carbon $end, string $type): array { Log::debug(sprintf('Now in getSingleModelPeriodByType(%s #%d, %s %s, %s)', $model::class, $model->id, $start->format('Y-m-d'), $end->format('Y-m-d'), $type)); $statistics = $this->filterStatistics($start, $end, $type); @@ -497,7 +513,7 @@ trait PeriodOverview return $entries; } - protected function saveGroupedAsStatistics(Model $model, Carbon $start, Carbon $end, string $type, array $array): void + private function saveGroupedAsStatistics(Model $model, Carbon $start, Carbon $end, string $type, array $array): void { unset($array['count']); Log::debug(sprintf('saveGroupedAsStatistics(%s #%d, %s, %s, "%s", array(%d))', $model::class, $model->id, $start->format('Y-m-d'), $end->format('Y-m-d'), $type, count($array))); @@ -510,6 +526,19 @@ trait PeriodOverview } } + private function saveGroupedForPrefix(string $prefix, Carbon $start, Carbon $end, string $type, array $array): void + { + unset($array['count']); + Log::debug(sprintf('saveGroupedForPrefix("%s", %s, %s, "%s", array(%d))', $prefix, $start->format('Y-m-d'), $end->format('Y-m-d'), $type, count($array))); + foreach ($array as $entry) { + $this->periodStatisticRepo->savePrefixedStatistic($prefix, $entry['currency_id'], $start, $end, $type, $entry['count'], $entry['amount']); + } + if (0 === count($array)) { + Log::debug('Save empty statistic.'); + $this->periodStatisticRepo->savePrefixedStatistic($prefix, $this->primaryCurrency->id, $start, $end, $type, 0, '0'); + } + } + /** * Filter a list of journals by a set of dates, and then group them by currency. */ @@ -584,6 +613,9 @@ trait PeriodOverview $return = [ 'count' => 0, ]; + if (0 === count($journals)) { + return $return; + } /** @var array $journal */ foreach ($journals as $journal) { diff --git a/database/migrations/2025_09_25_175248_create_period_statistics.php b/database/migrations/2025_09_25_175248_create_period_statistics.php index 0a5bf8d86b..0cda62b0ef 100644 --- a/database/migrations/2025_09_25_175248_create_period_statistics.php +++ b/database/migrations/2025_09_25_175248_create_period_statistics.php @@ -14,6 +14,10 @@ return new class extends Migration Schema::create('period_statistics', function (Blueprint $table) { $table->id(); $table->timestamps(); + + // reference to user group id. + $table->bigInteger('user_group_id', false, true); + $table->integer('primary_statable_id', false, true)->nullable(); $table->string('primary_statable_type', 255)->nullable(); @@ -33,6 +37,7 @@ return new class extends Migration $table->string('type',255); $table->integer('count', false, true)->default(0); $table->decimal('amount', 32, 12); + $table->foreign('user_group_id')->references('id')->on('user_groups')->onDelete('cascade'); }); }