mirror of
				https://github.com/firefly-iii/firefly-iii.git
				synced 2025-10-31 02:36:28 +00:00 
			
		
		
		
	First start on optimized available balance enrichment.
This commit is contained in:
		| @@ -28,6 +28,7 @@ use FireflyIII\Api\V1\Controllers\Controller; | ||||
| use FireflyIII\Exceptions\FireflyException; | ||||
| use FireflyIII\Models\AvailableBudget; | ||||
| use FireflyIII\Repositories\Budget\AvailableBudgetRepositoryInterface; | ||||
| use FireflyIII\Support\JsonApi\Enrichments\AvailableBudgetEnrichment; | ||||
| use FireflyIII\Transformers\AvailableBudgetTransformer; | ||||
| use FireflyIII\User; | ||||
| use Illuminate\Http\JsonResponse; | ||||
| @@ -75,7 +76,6 @@ class ShowController extends Controller | ||||
| 
 | ||||
|         // types to get, page size:
 | ||||
|         $pageSize = $this->parameters->get('limit'); | ||||
| 
 | ||||
|         $start    = $this->parameters->get('start'); | ||||
|         $end      = $this->parameters->get('end'); | ||||
| 
 | ||||
| @@ -84,6 +84,15 @@ class ShowController extends Controller | ||||
|         $count            = $collection->count(); | ||||
|         $availableBudgets = $collection->slice(($this->parameters->get('page') - 1) * $pageSize, $pageSize); | ||||
| 
 | ||||
|         // enrich
 | ||||
|         /** @var User $admin */ | ||||
|         $admin      = auth()->user(); | ||||
|         $enrichment = new AvailableBudgetEnrichment(); | ||||
|         $enrichment->setUser($admin); | ||||
|         $enrichment->setStart($start); | ||||
|         $enrichment->setEnd($end); | ||||
|         $availableBudgets = $enrichment->enrich($availableBudgets); | ||||
| 
 | ||||
|         // make paginator:
 | ||||
|         $paginator = new LengthAwarePaginator($availableBudgets, $count, $pageSize, $this->parameters->get('page')); | ||||
|         $paginator->setPath(route('api.v1.available-budgets.index') . $this->buildParams()); | ||||
| @@ -107,11 +116,23 @@ class ShowController extends Controller | ||||
|     public function show(AvailableBudget $availableBudget): JsonResponse | ||||
|     { | ||||
|         $manager = $this->getManager(); | ||||
|         $start   = $this->parameters->get('start'); | ||||
|         $end     = $this->parameters->get('end'); | ||||
| 
 | ||||
|         /** @var AvailableBudgetTransformer $transformer */ | ||||
|         $transformer = app(AvailableBudgetTransformer::class); | ||||
|         $transformer->setParameters($this->parameters); | ||||
| 
 | ||||
|         // enrich
 | ||||
|         /** @var User $admin */ | ||||
|         $admin      = auth()->user(); | ||||
|         $enrichment = new AvailableBudgetEnrichment(); | ||||
|         $enrichment->setUser($admin); | ||||
|         $enrichment->setStart($start); | ||||
|         $enrichment->setEnd($end); | ||||
|         $availableBudget = $enrichment->enrichSingle($availableBudget); | ||||
| 
 | ||||
| 
 | ||||
|         $resource = new Item($availableBudget, $transformer, 'available_budgets'); | ||||
| 
 | ||||
|         return response()->json($manager->createData($resource)->toArray())->header('Content-Type', self::CONTENT_TYPE); | ||||
|   | ||||
| @@ -231,7 +231,8 @@ class OperationsRepository implements OperationsRepositoryInterface, UserGroupIn | ||||
|         ?Collection          $budgets = null, | ||||
|         ?TransactionCurrency $currency = null, | ||||
|         bool                 $convertToPrimary = false | ||||
|     ): array { | ||||
|     ): array | ||||
|     { | ||||
|         Log::debug(sprintf('Start of %s(date, date, array, array, "%s", %s).', __METHOD__, $currency?->code, var_export($convertToPrimary, true))); | ||||
|         // 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.
 | ||||
| @@ -254,8 +255,7 @@ class OperationsRepository implements OperationsRepositoryInterface, UserGroupIn | ||||
|         $collector->setUser($this->user) | ||||
|                   ->setRange($start, $end) | ||||
|             // ->excludeDestinationAccounts($selection)
 | ||||
|             ->setTypes([TransactionTypeEnum::WITHDRAWAL->value]) | ||||
|         ; | ||||
|                   ->setTypes([TransactionTypeEnum::WITHDRAWAL->value]); | ||||
| 
 | ||||
|         if ($accounts instanceof Collection) { | ||||
|             $collector->setAccounts($accounts); | ||||
| @@ -282,4 +282,61 @@ class OperationsRepository implements OperationsRepositoryInterface, UserGroupIn | ||||
| 
 | ||||
|         return $summarizer->groupByCurrencyId($journals, 'negative', false); | ||||
|     } | ||||
| 
 | ||||
|     public function sumCollectedExpenses(array $expenses, Carbon $start, Carbon $end, bool $convertToPrimary = false): array | ||||
|     { | ||||
|         Log::debug(sprintf('Start of %s.', __METHOD__)); | ||||
|         $summarizer = new TransactionSummarizer($this->user); | ||||
|         // 2025-04-21 overrule "convertToPrimary" because in this particular view, we never want to do this.
 | ||||
|         $summarizer->setConvertToPrimary($convertToPrimary); | ||||
| 
 | ||||
|         // filter $journals by range.
 | ||||
|         $expenses = array_filter($expenses, static function (array $expense) use ($start, $end): bool { | ||||
|             return $expense['date']->between($start, $end); | ||||
|         }); | ||||
| 
 | ||||
|         return $summarizer->groupByCurrencyId($expenses, 'negative', false); | ||||
|     } | ||||
| 
 | ||||
|     #[\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)); | ||||
|         // 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.
 | ||||
| 
 | ||||
|         $repository = app(AccountRepositoryInterface::class); | ||||
|         $repository->setUser($this->user); | ||||
|         $subset    = $repository->getAccountsByType(config('firefly.valid_liabilities')); | ||||
|         $selection = new Collection(); | ||||
| 
 | ||||
|         /** @var Account $account */ | ||||
|         foreach ($subset as $account) { | ||||
|             if ('credit' === $repository->getMetaValue($account, 'liability_direction')) { | ||||
|                 $selection->push($account); | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         /** @var GroupCollectorInterface $collector */ | ||||
|         $collector = app(GroupCollectorInterface::class); | ||||
|         $collector->setUser($this->user) | ||||
|                   ->setRange($start, $end) | ||||
|             // ->excludeDestinationAccounts($selection)
 | ||||
|                   ->setTypes([TransactionTypeEnum::WITHDRAWAL->value]); | ||||
| 
 | ||||
|         if ($accounts instanceof Collection) { | ||||
|             $collector->setAccounts($accounts); | ||||
|         } | ||||
|         if (!$budgets instanceof Collection) { | ||||
|             $budgets = $this->getBudgets(); | ||||
|         } | ||||
|         if ($currency instanceof TransactionCurrency) { | ||||
|             Log::debug(sprintf('Limit to normal currency %s', $currency->code)); | ||||
|             $collector->setNormalCurrency($currency); | ||||
|         } | ||||
|         if ($budgets->count() > 0) { | ||||
|             $collector->setBudgets($budgets); | ||||
|         } | ||||
|         return $collector->getExtractedJournals(); | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -24,8 +24,8 @@ declare(strict_types=1); | ||||
| 
 | ||||
| namespace FireflyIII\Repositories\Budget; | ||||
| 
 | ||||
| use Deprecated; | ||||
| use Carbon\Carbon; | ||||
| use Deprecated; | ||||
| use FireflyIII\Enums\UserRoleEnum; | ||||
| use FireflyIII\Models\Budget; | ||||
| use FireflyIII\Models\TransactionCurrency; | ||||
| @@ -73,4 +73,8 @@ interface OperationsRepositoryInterface | ||||
|         ?TransactionCurrency $currency = null, | ||||
|         bool                 $convertToPrimary = false | ||||
|     ): array; | ||||
| 
 | ||||
|     public function sumCollectedExpenses(array $expenses, Carbon $start, Carbon $end, bool $convertToPrimary = false): array; | ||||
| 
 | ||||
|     public function collectExpenses(Carbon $start, Carbon $end, ?Collection $accounts = null, ?Collection $budgets = null, ?TransactionCurrency $currency = null): array; | ||||
| } | ||||
|   | ||||
| @@ -62,6 +62,9 @@ class AccountEnrichment implements EnrichmentInterface | ||||
|     private UserGroup           $userGroup; | ||||
|     private array               $lastActivities; | ||||
| 
 | ||||
|     /** | ||||
|      * TODO Set primary currency using Amount::method, not through setter. | ||||
|      */ | ||||
|     public function __construct() | ||||
|     { | ||||
|         $this->accountIds      = []; | ||||
|   | ||||
							
								
								
									
										157
									
								
								app/Support/JsonApi/Enrichments/AvailableBudgetEnrichment.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										157
									
								
								app/Support/JsonApi/Enrichments/AvailableBudgetEnrichment.php
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,157 @@ | ||||
| <?php | ||||
| /* | ||||
|  * AvailableBudgetEnrichment.php | ||||
|  * Copyright (c) 2025 james@firefly-iii.org. | ||||
|  * | ||||
|  * This file is part of Firefly III (https://github.com/firefly-iii). | ||||
|  * | ||||
|  * This program is free software: you can redistribute it and/or modify | ||||
|  * it under the terms of the GNU Affero General Public License as | ||||
|  * published by the Free Software Foundation, either version 3 of the | ||||
|  * License, or (at your option) any later version. | ||||
|  * | ||||
|  * This program is distributed in the hope that it will be useful, | ||||
|  * but WITHOUT ANY WARRANTY; without even the implied warranty of | ||||
|  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the | ||||
|  * GNU Affero General Public License for more details. | ||||
|  * | ||||
|  * You should have received a copy of the GNU Affero General Public License | ||||
|  * along with this program.  If not, see https://www.gnu.org/licenses/. | ||||
|  */ | ||||
| 
 | ||||
| declare(strict_types=1); | ||||
| 
 | ||||
| namespace FireflyIII\Support\JsonApi\Enrichments; | ||||
| 
 | ||||
| use Carbon\Carbon; | ||||
| use FireflyIII\Models\AvailableBudget; | ||||
| use FireflyIII\Models\TransactionCurrency; | ||||
| use FireflyIII\Models\UserGroup; | ||||
| use FireflyIII\Repositories\Budget\BudgetRepositoryInterface; | ||||
| use FireflyIII\Repositories\Budget\NoBudgetRepositoryInterface; | ||||
| use FireflyIII\Repositories\Budget\OperationsRepositoryInterface; | ||||
| use FireflyIII\Support\Facades\Amount; | ||||
| use FireflyIII\User; | ||||
| use Illuminate\Database\Eloquent\Model; | ||||
| use Illuminate\Support\Collection; | ||||
| use Illuminate\Support\Facades\Log; | ||||
| 
 | ||||
| class AvailableBudgetEnrichment implements EnrichmentInterface | ||||
| { | ||||
|     private User                $user; | ||||
|     private UserGroup           $userGroup; | ||||
|     private TransactionCurrency $primaryCurrency; | ||||
|     private bool                $convertToPrimary      = false; | ||||
|     private array               $ids                   = []; | ||||
|     private Collection          $collection; | ||||
|     private array               $spentInBudgets        = []; | ||||
|     private array               $spentOutsideBudgets   = []; | ||||
|     private array               $pcSpentInBudgets      = []; | ||||
|     private array               $pcSpentOutsideBudgets = []; | ||||
|     private readonly NoBudgetRepositoryInterface   $noBudgetRepository; | ||||
|     private readonly OperationsRepositoryInterface $opsRepository; | ||||
|     private readonly BudgetRepositoryInterface     $repository; | ||||
| 
 | ||||
| 
 | ||||
|     private ?Carbon $start = null; | ||||
|     private ?Carbon $end   = null; | ||||
| 
 | ||||
|     public function __construct() | ||||
|     { | ||||
|         $this->primaryCurrency  = Amount::getPrimaryCurrency(); | ||||
|         $this->convertToPrimary = Amount::convertToPrimary(); | ||||
|         $this->noBudgetRepository = app(NoBudgetRepositoryInterface::class); | ||||
|         $this->opsRepository      = app(OperationsRepositoryInterface::class); | ||||
|         $this->repository         = app(BudgetRepositoryInterface::class); | ||||
|     } | ||||
| 
 | ||||
|     #[\Override] public function enrich(Collection $collection): Collection
 | ||||
|     { | ||||
|         $this->collection = $collection; | ||||
|         $this->collectIds(); | ||||
|         $this->collectSpentInfo(); | ||||
|         $this->appendCollectedData(); | ||||
| 
 | ||||
|         return $this->collection; | ||||
|     } | ||||
| 
 | ||||
|     #[\Override] public function enrichSingle(Model | array $model): array | Model
 | ||||
|     { | ||||
|         Log::debug(__METHOD__); | ||||
|         $collection = new Collection([$model]); | ||||
|         $collection = $this->enrich($collection); | ||||
| 
 | ||||
|         return $collection->first(); | ||||
|     } | ||||
| 
 | ||||
|     #[\Override] public function setUser(User $user): void
 | ||||
|     { | ||||
|         $this->user      = $user; | ||||
|         $this->setUserGroup($user->userGroup); | ||||
|     } | ||||
| 
 | ||||
|     #[\Override] public function setUserGroup(UserGroup $userGroup): void
 | ||||
|     { | ||||
|         $this->userGroup = $userGroup; | ||||
|         $this->noBudgetRepository->setUserGroup($userGroup); | ||||
|         $this->opsRepository->setUserGroup($userGroup); | ||||
|         $this->repository->setUserGroup($userGroup); | ||||
|     } | ||||
| 
 | ||||
|     private function collectIds(): void | ||||
|     { | ||||
|         /** @var AvailableBudget $availableBudget */ | ||||
|         foreach ($this->collection as $availableBudget) { | ||||
|             $this->ids[] = (int) $availableBudget->id; | ||||
|         } | ||||
|         $this->ids = array_unique($this->ids); | ||||
|     } | ||||
| 
 | ||||
|     public function setStart(?Carbon $start): void | ||||
|     { | ||||
|         $this->start = $start; | ||||
|     } | ||||
| 
 | ||||
|     public function setEnd(?Carbon $end): void | ||||
|     { | ||||
|         $this->end = $end; | ||||
|     } | ||||
| 
 | ||||
|     private function collectSpentInfo(): void { | ||||
|         $start = $this->collection->min('start_date'); | ||||
|         $end   = $this->collection->max('end_date'); | ||||
|         $allActive = $this->repository->getActiveBudgets(); | ||||
|         $spentInBudgets = $this->opsRepository->collectExpenses($start, $end, null, $allActive, null); | ||||
|         foreach($this->collection as $availableBudget) { | ||||
|             $filteredSpentInBudgets = $this->opsRepository->sumCollectedExpenses($spentInBudgets, $availableBudget->start_date, $availableBudget->end_date, $this->convertToPrimary); | ||||
|             $id = (int) $availableBudget->id; | ||||
|             $this->spentInBudgets[$id] = array_values($filteredSpentInBudgets); | ||||
|             // filter arrays on date.
 | ||||
|             // send them to sumCollection thing.
 | ||||
|             // save.
 | ||||
|         } | ||||
| 
 | ||||
|         // first collect, then filter and append.
 | ||||
|     } | ||||
| 
 | ||||
|     private function appendCollectedData(): void | ||||
|     { | ||||
|         $spentInsideBudgets    = $this->spentInBudgets; | ||||
|         $spentOutsideBudgets   = $this->spentOutsideBudgets; | ||||
|         $pcSpentInBudgets      = $this->pcSpentInBudgets; | ||||
|         $pcSpentOutsideBudgets = $this->pcSpentOutsideBudgets; | ||||
|         $this->collection      = $this->collection->map(function (AvailableBudget $item) use ($spentInsideBudgets, $spentOutsideBudgets, $pcSpentInBudgets, $pcSpentOutsideBudgets) { | ||||
|             $id         = (int) $item->id; | ||||
|             $meta       = [ | ||||
|                 'spent_in_budgets'         => $spentInsideBudgets[$id] ?? [], | ||||
|                 'spent_outside_budgets'    => $spentOutsideBudgets[$id] ?? [], | ||||
|                 'pc_spent_in_budgets'      => $pcSpentInBudgets[$id] ?? [], | ||||
|                 'pc_spent_outside_budgets' => $pcSpentOutsideBudgets ?? [], | ||||
|             ]; | ||||
|             $item->meta = $meta; | ||||
|             return $item; | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
| 
 | ||||
| } | ||||
| @@ -60,30 +60,39 @@ class AvailableBudgetTransformer extends AbstractTransformer | ||||
|     public function transform(AvailableBudget $availableBudget): array | ||||
|     { | ||||
|         $this->repository->setUser($availableBudget->user); | ||||
| 
 | ||||
|         $currency = $availableBudget->transactionCurrency; | ||||
|         $primary  = $this->primary; | ||||
|         if (!$this->convertToPrimary) { | ||||
|             $primary = null; | ||||
|         $amount   = app('steam')->bcround($availableBudget->amount, $currency->decimal_places); | ||||
|         $pcAmount = null; | ||||
| 
 | ||||
|         if ($this->convertToPrimary) { | ||||
|             $pcAmount = app('steam')->bcround($availableBudget->native_amount, $this->primary->decimal_places); | ||||
|         } | ||||
| 
 | ||||
|         $data  = [ | ||||
|             'id'                      => (string) $availableBudget->id, | ||||
|             'created_at'              => $availableBudget->created_at->toAtomString(), | ||||
|             'updated_at'              => $availableBudget->updated_at->toAtomString(), | ||||
| 
 | ||||
|             // currencies according to 6.3.0
 | ||||
|             'currency_id'             => (string) $currency->id, | ||||
|             'currency_code'           => $currency->code, | ||||
|             'currency_symbol'         => $currency->symbol, | ||||
|             'currency_decimal_places' => $currency->decimal_places, | ||||
|             'primary_currency_id'             => $primary instanceof TransactionCurrency ? (string)$primary->id : null, | ||||
|             'primary_currency_code'           => $primary?->code, | ||||
|             'primary_currency_symbol'         => $primary?->symbol, | ||||
|             'primary_currency_decimal_places' => $primary?->decimal_places, | ||||
|             'amount'                          => app('steam')->bcround($availableBudget->amount, $currency->decimal_places), | ||||
|             'pc_amount'                       => $this->convertToPrimary ? app('steam')->bcround($availableBudget->native_amount, $currency->decimal_places) : null, | ||||
| 
 | ||||
|             '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, | ||||
| 
 | ||||
| 
 | ||||
|             'amount'                   => $amount, | ||||
|             'pc_amount'                => $pcAmount, | ||||
|             'start'                    => $availableBudget->start_date->toAtomString(), | ||||
|             'end'                      => $availableBudget->end_date->endOfDay()->toAtomString(), | ||||
|             'spent_in_budgets'                => [], | ||||
|             'spent_no_budget'                 => [], | ||||
|             'spent_in_budgets'         => $availableBudget->meta['spent_in_budgets'], | ||||
|             'pc_spent_in_budgets'      => $availableBudget->meta['pc_spent_in_budgets'], | ||||
|             'spent_outside_budgets'    => $availableBudget->meta['spent_outside_budgets'], | ||||
|             'pc_spent_outside_budgets' => $availableBudget->meta['pc_spent_outside_budgets'], | ||||
|             'links'                    => [ | ||||
|                 [ | ||||
|                     'rel' => 'self', | ||||
| @@ -94,8 +103,8 @@ class AvailableBudgetTransformer extends AbstractTransformer | ||||
|         $start = $this->parameters->get('start'); | ||||
|         $end   = $this->parameters->get('end'); | ||||
|         if (null !== $start && null !== $end) { | ||||
|             $data['spent_in_budgets'] = $this->getSpentInBudgets(); | ||||
|             $data['spent_no_budget']  = $this->spentOutsideBudgets(); | ||||
|             $data['old_spent_in_budgets'] = $this->getSpentInBudgets(); | ||||
|             $data['old_spent_no_budget']  = $this->spentOutsideBudgets(); | ||||
|         } | ||||
| 
 | ||||
|         return $data; | ||||
|   | ||||
		Reference in New Issue
	
	Block a user