From 90fc4b44f2e1125c797b445db55b8eaf3a7b09d8 Mon Sep 17 00:00:00 2001 From: James Cole Date: Sat, 15 Feb 2025 16:51:13 +0100 Subject: [PATCH] A lot less queries for the account transformer. --- .../Models/Account/ShowController.php | 22 ++- .../Models/Account/StoreController.php | 11 ++ .../Models/Account/UpdateController.php | 11 ++ .../Models/PiggyBank/ListController.php | 17 +- .../TransactionCurrency/ListController.php | 10 ++ .../Controllers/Search/AccountController.php | 11 ++ .../JsonApi/Enrichments/AccountEnrichment.php | 103 ++++++++++++- .../Enrichments/EnrichmentInterface.php | 9 ++ .../TransactionGroupEnrichment.php | 3 +- app/Support/Steam.php | 145 ++++++++++-------- app/Transformers/AccountTransformer.php | 133 +++++++--------- 11 files changed, 321 insertions(+), 154 deletions(-) diff --git a/app/Api/V1/Controllers/Models/Account/ShowController.php b/app/Api/V1/Controllers/Models/Account/ShowController.php index 675ff15713..c5680f2976 100644 --- a/app/Api/V1/Controllers/Models/Account/ShowController.php +++ b/app/Api/V1/Controllers/Models/Account/ShowController.php @@ -29,7 +29,9 @@ use FireflyIII\Exceptions\FireflyException; use FireflyIII\Models\Account; use FireflyIII\Repositories\Account\AccountRepositoryInterface; use FireflyIII\Support\Http\Api\AccountFilter; +use FireflyIII\Support\JsonApi\Enrichments\AccountEnrichment; use FireflyIII\Transformers\AccountTransformer; +use FireflyIII\User; use Illuminate\Http\JsonResponse; use Illuminate\Http\Request; use Illuminate\Pagination\LengthAwarePaginator; @@ -88,9 +90,17 @@ class ShowController extends Controller $count = $collection->count(); // continue sort: - $accounts = $collection->slice(($this->parameters->get('page') - 1) * $pageSize, $pageSize); + // enrich + /** @var User $admin */ + $admin = auth()->user(); + $enrichment = new AccountEnrichment(); + $enrichment->setUser($admin); + $enrichment->setConvertToNative($this->convertToNative); + $enrichment->setNative($this->nativeCurrency); + $accounts = $enrichment->enrich($accounts); + // make paginator: $paginator = new LengthAwarePaginator($accounts, $count, $pageSize, $this->parameters->get('page')); $paginator->setPath(route('api.v1.accounts.index').$this->buildParams()); @@ -118,6 +128,16 @@ class ShowController extends Controller $account->refresh(); $manager = $this->getManager(); + // enrich + /** @var User $admin */ + $admin = auth()->user(); + $enrichment = new AccountEnrichment(); + $enrichment->setUser($admin); + $enrichment->setConvertToNative($this->convertToNative); + $enrichment->setNative($this->nativeCurrency); + $account = $enrichment->enrichSingle($account); + + /** @var AccountTransformer $transformer */ $transformer = app(AccountTransformer::class); $transformer->setParameters($this->parameters); diff --git a/app/Api/V1/Controllers/Models/Account/StoreController.php b/app/Api/V1/Controllers/Models/Account/StoreController.php index 796620e289..ca327bda58 100644 --- a/app/Api/V1/Controllers/Models/Account/StoreController.php +++ b/app/Api/V1/Controllers/Models/Account/StoreController.php @@ -27,7 +27,9 @@ namespace FireflyIII\Api\V1\Controllers\Models\Account; use FireflyIII\Api\V1\Controllers\Controller; use FireflyIII\Api\V1\Requests\Models\Account\StoreRequest; use FireflyIII\Repositories\Account\AccountRepositoryInterface; +use FireflyIII\Support\JsonApi\Enrichments\AccountEnrichment; use FireflyIII\Transformers\AccountTransformer; +use FireflyIII\User; use Illuminate\Http\JsonResponse; use League\Fractal\Resource\Item; @@ -69,6 +71,15 @@ class StoreController extends Controller $account = $this->repository->store($data); $manager = $this->getManager(); + // enrich + /** @var User $admin */ + $admin = auth()->user(); + $enrichment = new AccountEnrichment(); + $enrichment->setUser($admin); + $enrichment->setConvertToNative($this->convertToNative); + $enrichment->setNative($this->nativeCurrency); + $account = $enrichment->enrichSingle($account); + /** @var AccountTransformer $transformer */ $transformer = app(AccountTransformer::class); $transformer->setParameters($this->parameters); diff --git a/app/Api/V1/Controllers/Models/Account/UpdateController.php b/app/Api/V1/Controllers/Models/Account/UpdateController.php index d6185bf021..03df429637 100644 --- a/app/Api/V1/Controllers/Models/Account/UpdateController.php +++ b/app/Api/V1/Controllers/Models/Account/UpdateController.php @@ -28,7 +28,9 @@ use FireflyIII\Api\V1\Controllers\Controller; use FireflyIII\Api\V1\Requests\Models\Account\UpdateRequest; use FireflyIII\Models\Account; use FireflyIII\Repositories\Account\AccountRepositoryInterface; +use FireflyIII\Support\JsonApi\Enrichments\AccountEnrichment; use FireflyIII\Transformers\AccountTransformer; +use FireflyIII\User; use Illuminate\Http\JsonResponse; use League\Fractal\Resource\Item; @@ -73,6 +75,15 @@ class UpdateController extends Controller $account->refresh(); app('preferences')->mark(); + // enrich + /** @var User $admin */ + $admin = auth()->user(); + $enrichment = new AccountEnrichment(); + $enrichment->setUser($admin); + $enrichment->setConvertToNative($this->convertToNative); + $enrichment->setNative($this->nativeCurrency); + $account = $enrichment->enrichSingle($account); + /** @var AccountTransformer $transformer */ $transformer = app(AccountTransformer::class); $transformer->setParameters($this->parameters); diff --git a/app/Api/V1/Controllers/Models/PiggyBank/ListController.php b/app/Api/V1/Controllers/Models/PiggyBank/ListController.php index a0b35ec09a..03d89911d4 100644 --- a/app/Api/V1/Controllers/Models/PiggyBank/ListController.php +++ b/app/Api/V1/Controllers/Models/PiggyBank/ListController.php @@ -28,9 +28,11 @@ use FireflyIII\Api\V1\Controllers\Controller; use FireflyIII\Exceptions\FireflyException; use FireflyIII\Models\PiggyBank; use FireflyIII\Repositories\PiggyBank\PiggyBankRepositoryInterface; +use FireflyIII\Support\JsonApi\Enrichments\AccountEnrichment; use FireflyIII\Transformers\AccountTransformer; use FireflyIII\Transformers\AttachmentTransformer; use FireflyIII\Transformers\PiggyBankEventTransformer; +use FireflyIII\User; use Illuminate\Http\JsonResponse; use Illuminate\Pagination\LengthAwarePaginator; use League\Fractal\Pagination\IlluminatePaginatorAdapter; @@ -75,17 +77,26 @@ class ListController extends Controller $collection = $piggyBank->accounts; $count = $collection->count(); - $events = $collection->slice(($this->parameters->get('page') - 1) * $pageSize, $pageSize); + $accounts = $collection->slice(($this->parameters->get('page') - 1) * $pageSize, $pageSize); + + // enrich + /** @var User $admin */ + $admin = auth()->user(); + $enrichment = new AccountEnrichment(); + $enrichment->setUser($admin); + $enrichment->setConvertToNative($this->convertToNative); + $enrichment->setNative($this->nativeCurrency); + $accounts = $enrichment->enrich($accounts); // make paginator: - $paginator = new LengthAwarePaginator($events, $count, $pageSize, $this->parameters->get('page')); + $paginator = new LengthAwarePaginator($accounts, $count, $pageSize, $this->parameters->get('page')); $paginator->setPath(route('api.v1.piggy-banks.accounts', [$piggyBank->id]).$this->buildParams()); /** @var AccountTransformer $transformer */ $transformer = app(AccountTransformer::class); $transformer->setParameters($this->parameters); - $resource = new FractalCollection($events, $transformer, 'accounts'); + $resource = new FractalCollection($accounts, $transformer, 'accounts'); $resource->setPaginator(new IlluminatePaginatorAdapter($paginator)); return response()->json($manager->createData($resource)->toArray())->header('Content-Type', self::CONTENT_TYPE); diff --git a/app/Api/V1/Controllers/Models/TransactionCurrency/ListController.php b/app/Api/V1/Controllers/Models/TransactionCurrency/ListController.php index 18e3a3126c..c2c7027132 100644 --- a/app/Api/V1/Controllers/Models/TransactionCurrency/ListController.php +++ b/app/Api/V1/Controllers/Models/TransactionCurrency/ListController.php @@ -42,6 +42,7 @@ use FireflyIII\Repositories\Recurring\RecurringRepositoryInterface; use FireflyIII\Repositories\Rule\RuleRepositoryInterface; use FireflyIII\Support\Http\Api\AccountFilter; use FireflyIII\Support\Http\Api\TransactionFilter; +use FireflyIII\Support\JsonApi\Enrichments\AccountEnrichment; use FireflyIII\Support\JsonApi\Enrichments\TransactionGroupEnrichment; use FireflyIII\Transformers\AccountTransformer; use FireflyIII\Transformers\AvailableBudgetTransformer; @@ -101,6 +102,15 @@ class ListController extends Controller $count = $collection->count(); $accounts = $collection->slice(($this->parameters->get('page') - 1) * $pageSize, $pageSize); + // enrich + /** @var User $admin */ + $admin = auth()->user(); + $enrichment = new AccountEnrichment(); + $enrichment->setUser($admin); + $enrichment->setConvertToNative($this->convertToNative); + $enrichment->setNative($this->nativeCurrency); + $accounts = $enrichment->enrich($accounts); + // make paginator: $paginator = new LengthAwarePaginator($accounts, $count, $pageSize, $this->parameters->get('page')); $paginator->setPath(route('api.v1.currencies.accounts', [$currency->code]).$this->buildParams()); diff --git a/app/Api/V1/Controllers/Search/AccountController.php b/app/Api/V1/Controllers/Search/AccountController.php index 74ed89c978..c7d292ffa9 100644 --- a/app/Api/V1/Controllers/Search/AccountController.php +++ b/app/Api/V1/Controllers/Search/AccountController.php @@ -26,8 +26,10 @@ namespace FireflyIII\Api\V1\Controllers\Search; use FireflyIII\Api\V1\Controllers\Controller; use FireflyIII\Support\Http\Api\AccountFilter; +use FireflyIII\Support\JsonApi\Enrichments\AccountEnrichment; use FireflyIII\Support\Search\AccountSearch; use FireflyIII\Transformers\AccountTransformer; +use FireflyIII\User; use Illuminate\Http\JsonResponse; use Illuminate\Http\Request; use Illuminate\Http\Response; @@ -81,6 +83,15 @@ class AccountController extends Controller $accounts = $search->search(); + // enrich + /** @var User $admin */ + $admin = auth()->user(); + $enrichment = new AccountEnrichment(); + $enrichment->setUser($admin); + $enrichment->setConvertToNative($this->convertToNative); + $enrichment->setNative($this->nativeCurrency); + $accounts = $enrichment->enrich($accounts); + /** @var AccountTransformer $transformer */ $transformer = app(AccountTransformer::class); $transformer->setParameters($this->parameters); diff --git a/app/Support/JsonApi/Enrichments/AccountEnrichment.php b/app/Support/JsonApi/Enrichments/AccountEnrichment.php index 18bbd9f8db..3ce7f71c8b 100644 --- a/app/Support/JsonApi/Enrichments/AccountEnrichment.php +++ b/app/Support/JsonApi/Enrichments/AccountEnrichment.php @@ -25,14 +25,20 @@ declare(strict_types=1); namespace FireflyIII\Support\JsonApi\Enrichments; use Carbon\Carbon; +use FireflyIII\Enums\TransactionTypeEnum; use FireflyIII\Exceptions\FireflyException; +use FireflyIII\Helpers\Collector\GroupCollectorInterface; use FireflyIII\Models\Account; use FireflyIII\Models\AccountMeta; use FireflyIII\Models\AccountType; +use FireflyIII\Models\Location; +use FireflyIII\Models\Note; use FireflyIII\Models\ObjectGroup; use FireflyIII\Models\TransactionCurrency; +use FireflyIII\Models\TransactionJournal; use FireflyIII\Models\UserGroup; use FireflyIII\Support\Facades\Balance; +use FireflyIII\Support\Facades\Steam; use FireflyIII\Support\Http\Api\ExchangeRateConverter; use FireflyIII\User; use Illuminate\Database\Eloquent\Model; @@ -67,15 +73,21 @@ class AccountEnrichment implements EnrichmentInterface private array $accountTypes; private array $currencies; private array $meta; + private array $openingBalances; + private array $notes; + private array $locations; public function __construct() { $this->convertToNative = false; $this->accountIds = []; + $this->openingBalances = []; $this->currencies = []; $this->accountTypeIds = []; $this->accountTypes = []; $this->meta = []; + $this->notes = []; + $this->locations = []; // $this->repository = app(AccountRepositoryInterface::class); // $this->currencyRepository = app(CurrencyRepositoryInterface::class); // $this->start = null; @@ -105,6 +117,8 @@ class AccountEnrichment implements EnrichmentInterface $this->collectAccountIds(); $this->getAccountTypes(); $this->collectMetaData(); + $this->collectLocations(); + $this->collectOpeningBalances(); // $this->default = app('amount')->getNativeCurrency(); // $this->currencies = []; // $this->balances = []; @@ -156,23 +170,90 @@ class AccountEnrichment implements EnrichmentInterface private function appendCollectedData(): void { - $accountTypes = $this->accountTypes; $meta = $this->meta; - $this->collection = $this->collection->map(function (Account $item) use ($accountTypes, $meta) { + $currencies = $this->currencies; + $notes = $this->notes; + $openingBalances = $this->openingBalances; + $locations = $this->locations; + $this->collection = $this->collection->map(function (Account $item) use ($accountTypes, $meta, $currencies, $notes, $openingBalances, $locations) { $item->full_account_type = $accountTypes[(int) $item->account_type_id] ?? null; - $meta = []; + $accountMeta = [ + 'currency' => null, + 'location' => [ + 'latitude' => null, + 'longitude' => null, + 'zoom_level' => null, + ], + ]; if (array_key_exists((int) $item->id, $meta)) { foreach ($meta[(int) $item->id] as $name => $value) { - $meta[$name] = $value; + $accountMeta[$name] = $value; } } - $item->meta = $meta; + // also add currency, if present. + if (array_key_exists('currency_id', $accountMeta)) { + $currencyId = (int) $accountMeta['currency_id']; + $accountMeta['currency'] = $currencies[$currencyId]; + } + + // if notes, add notes. + if (array_key_exists($item->id, $notes)) { + $accountMeta['notes'] = $notes[$item->id]; + } + // if opening balance, add opening balance + if (array_key_exists($item->id, $openingBalances)) { + $accountMeta['opening_balance_date'] = $openingBalances[$item->id]['date']; + $accountMeta['opening_balance_amount'] = $openingBalances[$item->id]['amount']; + } + + // if location, add location: + if (array_key_exists($item->id, $locations)) { + $accountMeta['location'] = $locations[$item->id]; + } + $item->meta = $accountMeta; return $item; }); } + private function collectOpeningBalances(): void + { + // use new group collector: + /** @var GroupCollectorInterface $collector */ + $collector = app(GroupCollectorInterface::class); + $collector->setUser($this->user)->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'], + ]; + } + } + + private function collectLocations(): void { + $locations = Location::query()->whereIn('locatable_id', $this->accountIds) + ->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))); + } + /** * TODO this method refers to a single-use method inside Steam that could be moved here. */ @@ -386,5 +467,17 @@ class AccountEnrichment implements EnrichmentInterface $this->native = $native; } + private function collectNotes(): void + { + $notes = Note::query()->whereIn('noteable_id', $this->accountIds) + ->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))); + } + } diff --git a/app/Support/JsonApi/Enrichments/EnrichmentInterface.php b/app/Support/JsonApi/Enrichments/EnrichmentInterface.php index 90d921f9e9..4f8c5f46ab 100644 --- a/app/Support/JsonApi/Enrichments/EnrichmentInterface.php +++ b/app/Support/JsonApi/Enrichments/EnrichmentInterface.php @@ -24,6 +24,8 @@ declare(strict_types=1); namespace FireflyIII\Support\JsonApi\Enrichments; +use FireflyIII\Models\UserGroup; +use FireflyIII\User; use Illuminate\Database\Eloquent\Model; use Illuminate\Support\Collection; @@ -32,4 +34,11 @@ interface EnrichmentInterface public function enrich(Collection $collection): Collection; public function enrichSingle(Model|array $model): Model|array; + + public function setUserGroup(UserGroup $userGroup): void; + + public function setUser(User $user): void; } + + + diff --git a/app/Support/JsonApi/Enrichments/TransactionGroupEnrichment.php b/app/Support/JsonApi/Enrichments/TransactionGroupEnrichment.php index e165f8b6d6..a48908b159 100644 --- a/app/Support/JsonApi/Enrichments/TransactionGroupEnrichment.php +++ b/app/Support/JsonApi/Enrichments/TransactionGroupEnrichment.php @@ -29,6 +29,7 @@ use FireflyIII\Models\Attachment; use FireflyIII\Models\Location; use FireflyIII\Models\Note; use FireflyIII\Models\Tag; +use FireflyIII\Models\TransactionGroup; use FireflyIII\Models\TransactionJournal; use FireflyIII\Models\TransactionJournalMeta; use FireflyIII\Models\UserGroup; @@ -80,7 +81,7 @@ class TransactionGroupEnrichment implements EnrichmentInterface return $this->collection; } - #[\Override] public function enrichSingle(Model|array $model): Model|array + #[\Override] public function enrichSingle(Model|array $model): TransactionGroup|array { Log::debug(__METHOD__); if(is_array($model)) { diff --git a/app/Support/Steam.php b/app/Support/Steam.php index c2ed20374b..33d3c12ac6 100644 --- a/app/Support/Steam.php +++ b/app/Support/Steam.php @@ -41,14 +41,14 @@ 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)) { return null; } - $result = $account->accountMeta->where('name', 'currency_id')->first(); + $result = $account->accountMeta()->where('name', 'currency_id')->first(); if (null === $result) { return null; } @@ -64,7 +64,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); @@ -74,21 +74,21 @@ 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(sprintf('finalAccountBalanceInRange: Call finalAccountBalance with date/time "%s"', $request->toIso8601String())); - $startBalance = $this->finalAccountBalance($account, $request); - $nativeCurrency = app('amount')->getNativeCurrencyByUserGroup($account->user->userGroup); - $accountCurrency = $this->getAccountCurrency($account); - $hasCurrency = null !== $accountCurrency; - $currency = $accountCurrency ?? $nativeCurrency; + $startBalance = $this->finalAccountBalance($account, $request); + $nativeCurrency = app('amount')->getNativeCurrencyByUserGroup($account->user->userGroup); + $accountCurrency = $this->getAccountCurrency($account); + $hasCurrency = null !== $accountCurrency; + $currency = $accountCurrency ?? $nativeCurrency; Log::debug(sprintf('Currency is %s', $currency->code)); @@ -101,7 +101,7 @@ class Steam Log::debug(sprintf('Also set start balance in %s', $nativeCurrency->code)); $startBalance[$nativeCurrency->code] ??= '0'; } - $currencies = [ + $currencies = [ $currency->id => $currency, $nativeCurrency->id => $nativeCurrency, ]; @@ -111,46 +111,45 @@ 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) (null === $entry->sum_of_day ? '0' : $entry->sum_of_day); + $sumOfDay = (string) (null === $entry->sum_of_day ? '0' : $entry->sum_of_day); // find currency of this entry, does not have to exist. $currencies[$entry->transaction_currency_id] ??= TransactionCurrency::find($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, $currentBalance[$entryCurrency->code]); // if not convert to native, add the amount to "balance", do nothing else. @@ -168,7 +167,7 @@ class Steam } // just set it. - $balances[$carbonKey] = $currentBalance; + $balances[$carbonKey] = $currentBalance; Log::debug(sprintf('Updated entry [%s]', $carbonKey), $currentBalance); } $cache->store($balances); @@ -207,10 +206,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; @@ -288,35 +287,45 @@ class Steam * --> "native_balance": balance in the user's native balance, with all amounts converted to native. * "EUR": balance in EUR (or whatever currencies the account has balance in) */ - public function finalAccountBalance(Account $account, Carbon $date): array + public function finalAccountBalance(Account $account, Carbon $date, ?TransactionCurrency $native = null, ?bool $convertToNative = null): array { - $cache = new CacheProperties(); + $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'))); + 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 === $convertToNative) { + $convertToNative = Amount::convertToNative($account->user); + } + if (null === $native) { + $native = Amount::getNativeCurrencyByUserGroup($account->user->userGroup); + } + // account balance thing. + $currencyPresent = isset($account->meta) && array_key_exists('currency', $account->meta) && $account->meta['currency'] !== null; + if($currencyPresent) { + $accountCurrency = $account->meta['currency']; + } + if(!$currencyPresent) { - $native = Amount::getNativeCurrencyByUserGroup($account->user->userGroup); - $convertToNative = Amount::convertToNative($account->user); - $accountCurrency = $this->getAccountCurrency($account); - $hasCurrency = null !== $accountCurrency; - $currency = $hasCurrency ? $accountCurrency : $native; - $return = [ + $accountCurrency = $this->getAccountCurrency($account); + } + $hasCurrency = null !== $accountCurrency; + $currency = $hasCurrency ? $accountCurrency : $native; + $return = [ 'native_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 "native_balance". $return['balance'] = $others[$currency->code] ?? '0'; @@ -331,7 +340,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 ($convertToNative) { // the native balance is combined with a converted virtual_balance: @@ -345,7 +354,7 @@ class Steam $return['balance'] = bcadd($return['balance'], $virtualBalance); // Log::debug(sprintf('Virtual balance makes the (native) total %s', $return['balance'])); } - $final = array_merge($return, $others); + $final = array_merge($return, $others); Log::debug('Final balance is', $final); $cache->store($final); @@ -452,15 +461,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; } @@ -535,9 +544,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; @@ -574,7 +583,7 @@ class Steam */ public function floatalize(string $value): string { - $value = strtoupper($value); + $value = strtoupper($value); if (!str_contains($value, 'E')) { return $value; } @@ -662,9 +671,9 @@ class Steam if (null === $currency) { continue; } - $current = $converter->convert($currency, $native, $date, $amount); + $current = $converter->convert($currency, $native, $date, $amount); Log::debug(sprintf('Convert %s %s to %s %s', $currency->code, $amount, $native->code, $current)); - $total = bcadd($current, $total); + $total = bcadd($current, $total); } return $total; diff --git a/app/Transformers/AccountTransformer.php b/app/Transformers/AccountTransformer.php index 07a62bd4c6..6017a377fa 100644 --- a/app/Transformers/AccountTransformer.php +++ b/app/Transformers/AccountTransformer.php @@ -31,6 +31,7 @@ use FireflyIII\Models\TransactionCurrency; use FireflyIII\Repositories\Account\AccountRepositoryInterface; use FireflyIII\Support\Facades\Amount; use FireflyIII\Support\Facades\Steam; +use FireflyIII\Support\Http\Api\ExchangeRateConverter; use Illuminate\Support\Facades\Log; use Symfony\Component\HttpFoundation\ParameterBag; @@ -61,60 +62,51 @@ class AccountTransformer extends AbstractTransformer */ public function transform(Account $account): array { - $this->repository->setUser($account->user); - // get account type: + $accountType = (string) config(sprintf('firefly.shortNamesByFullName.%s', $account->full_account_type)); + $liabilityType = (string) config(sprintf('firefly.shortLiabilityNameByFullName.%s', $account->full_account_type)); + $liabilityType = '' === $liabilityType ? null : strtolower($liabilityType); - $fullType = $account->accountType->type; - $accountType = (string) config(sprintf('firefly.shortNamesByFullName.%s', $fullType)); - $liabilityType = (string) config(sprintf('firefly.shortLiabilityNameByFullName.%s', $fullType)); - $liabilityType = '' === $liabilityType ? null : strtolower($liabilityType); - $liabilityDirection = $this->repository->getMetaValue($account, 'liability_direction'); - $convertToNative = Amount::convertToNative(); - + $liabilityDirection = $account->meta['liability_direction'] ?? null; // get account role (will only work if the type is asset). - $native = Amount::getNativeCurrency(); - $accountRole = $this->getAccountRole($account, $accountType); - $date = $this->getDate(); + $accountRole = $this->getAccountRole($account, $accountType); + + // date (for balance etc.) + $date = $this->getDate(); $date->endOfDay(); - [$currencyId, $currencyCode, $currencySymbol, $decimalPlaces] = $this->getCurrency($account); - [$creditCardType, $monthlyPaymentDate] = $this->getCCInfo($account, $accountRole, $accountType); - [$openingBalance, $nativeOpeningBalance, $openingBalanceDate] = $this->getOpeningBalance($account, $accountType, $convertToNative); - [$interest, $interestPeriod] = $this->getInterest($account, $accountType); + [$creditCardType, $monthlyPaymentDate] = $this->getCCInfo($account, $accountRole, $accountType); + [$openingBalance, $nativeOpeningBalance, $openingBalanceDate] = $this->getOpeningBalance($account, $accountType); + [$interest, $interestPeriod] = $this->getInterest($account, $accountType); - $native = $this->native; + $native = $this->native; if (!$this->convertToNative) { // reset native currency to NULL, not interesting. $native = null; } - - $openingBalance = app('steam')->bcround($openingBalance, $decimalPlaces); - $includeNetWorth = '0' !== $this->repository->getMetaValue($account, 'include_net_worth'); - $longitude = null; - $latitude = null; - $zoomLevel = null; - $location = $this->repository->getLocation($account); - if (null !== $location) { - $longitude = $location->longitude; - $latitude = $location->latitude; - $zoomLevel = (int) $location->zoom_level; - } +// + $decimalPlaces = (int) $account->meta['currency']?->decimal_places; + $decimalPlaces = 0 === $decimalPlaces ? 2 : $decimalPlaces; + $openingBalance = Steam::bcround($openingBalance, $decimalPlaces); + $includeNetWorth = '0' !== ($account->meta['include_net_worth'] ?? null); + $longitude = $account->meta['location']['longitude'] ?? null; + $latitude = $account->meta['location']['latitude'] ?? null; + $zoomLevel = $account->meta['location']['zoom_level'] ?? null; // no order for some accounts: - $order = $account->order; + $order = $account->order; if (!in_array(strtolower($accountType), ['liability', 'liabilities', 'asset'], true)) { $order = null; } // balance, native balance, virtual balance, native virtual balance? Log::debug(sprintf('transform: Call finalAccountBalance with date/time "%s"', $date->toIso8601String())); - $finalBalance = Steam::finalAccountBalance($account, $date); - if ($convertToNative) { - $finalBalance['balance'] = $finalBalance[$currencyCode] ?? '0'; + $finalBalance = Steam::finalAccountBalance($account, $date, $this->native, $this->convertToNative); + if ($this->convertToNative) { + $finalBalance['balance'] = $finalBalance[$account->meta['currency']?->code] ?? '0'; } - $currentBalance = app('steam')->bcround($finalBalance['balance'] ?? '0', $decimalPlaces); - $nativeCurrentBalance = $convertToNative ? app('steam')->bcround($finalBalance['native_balance'] ?? '0', $native->decimal_places) : null; + $currentBalance = Steam::bcround($finalBalance['balance'] ?? '0', $decimalPlaces); + $nativeCurrentBalance = $this->convertToNative ? Steam::bcround($finalBalance['native_balance'] ?? '0', $native->decimal_places) : null; return [ 'id' => (string) $account->id, @@ -125,10 +117,10 @@ class AccountTransformer extends AbstractTransformer 'name' => $account->name, 'type' => strtolower($accountType), 'account_role' => $accountRole, - 'currency_id' => $currencyId, - 'currency_code' => $currencyCode, - 'currency_symbol' => $currencySymbol, - 'currency_decimal_places' => $decimalPlaces, + 'currency_id' => $account->meta['currency_id'] ?? null, + 'currency_code' => $account->meta['currency']?->code, + 'currency_symbol' => $account->meta['currency']?->symbol, + 'currency_decimal_places' => $account->meta['currency']?->decimal_places, 'native_currency_id' => null === $native ? null : (string) $native->id, 'native_currency_code' => $native?->code, 'native_currency_symbol' => $native?->symbol, @@ -136,14 +128,14 @@ class AccountTransformer extends AbstractTransformer 'current_balance' => $currentBalance, 'native_current_balance' => $nativeCurrentBalance, 'current_balance_date' => $date->toAtomString(), - 'notes' => $this->repository->getNoteText($account), + 'notes' => $account->meta['notes'] ?? null, 'monthly_payment_date' => $monthlyPaymentDate, 'credit_card_type' => $creditCardType, - 'account_number' => $this->repository->getMetaValue($account, 'account_number'), + 'account_number' => $account->meta['account_number'] ?? null, 'iban' => '' === $account->iban ? null : $account->iban, - 'bic' => $this->repository->getMetaValue($account, 'BIC'), - 'virtual_balance' => app('steam')->bcround($account->virtual_balance, $decimalPlaces), - 'native_virtual_balance' => $this->convertToNative ? app('steam')->bcround($account->native_virtual_balance, $native->decimal_places) : null, + 'bic' => $account->meta['BIC'] ?? null, + 'virtual_balance' => Steam::bcround($account->virtual_balance, $decimalPlaces), + 'native_virtual_balance' => $this->convertToNative ? Steam::bcround($account->native_virtual_balance, $native->decimal_places) : null, 'opening_balance' => $openingBalance, 'native_opening_balance' => $nativeOpeningBalance, 'opening_balance_date' => $openingBalanceDate, @@ -151,7 +143,7 @@ class AccountTransformer extends AbstractTransformer 'liability_direction' => $liabilityDirection, 'interest' => $interest, 'interest_period' => $interestPeriod, - 'current_debt' => $this->repository->getMetaValue($account, 'current_debt'), + 'current_debt' => $account->meta['current_debt'] ?? null, 'include_net_worth' => $includeNetWorth, 'longitude' => $longitude, 'latitude' => $latitude, @@ -159,7 +151,7 @@ class AccountTransformer extends AbstractTransformer 'links' => [ [ 'rel' => 'self', - 'uri' => '/accounts/'.$account->id, + 'uri' => sprintf('/accounts/%d', $account->id), ], ], ]; @@ -167,7 +159,7 @@ class AccountTransformer extends AbstractTransformer private function getAccountRole(Account $account, string $accountType): ?string { - $accountRole = $this->repository->getMetaValue($account, 'account_role'); + $accountRole = $account->meta['account_role'] ?? null; if ('asset' !== $accountType || '' === (string) $accountRole) { $accountRole = null; } @@ -188,34 +180,18 @@ class AccountTransformer extends AbstractTransformer return $date; } - private function getCurrency(Account $account): array - { - $currency = $this->repository->getAccountCurrency($account); - - // only grab native when result is null: - if (null === $currency) { - $currency = $this->native; - } - $currencyId = (string) $currency->id; - $currencyCode = $currency->code; - $decimalPlaces = $currency->decimal_places; - $currencySymbol = $currency->symbol; - - return [$currencyId, $currencyCode, $currencySymbol, $decimalPlaces]; - } - private function getCCInfo(Account $account, ?string $accountRole, string $accountType): array { $monthlyPaymentDate = null; $creditCardType = null; if ('ccAsset' === $accountRole && 'asset' === $accountType) { - $creditCardType = $this->repository->getMetaValue($account, 'cc_type'); - $monthlyPaymentDate = $this->repository->getMetaValue($account, 'cc_monthly_payment_date'); + $creditCardType = $account->meta['cc_type'] ?? null; + $monthlyPaymentDate = $account->meta['cc_monthly_payment_date'] ?? null; } if (null !== $monthlyPaymentDate) { // try classic date: if (10 === strlen($monthlyPaymentDate)) { - $object = Carbon::createFromFormat('!Y-m-d', $monthlyPaymentDate, config('app.timezone')); + $object = Carbon::createFromFormat('!Y-m-d', $monthlyPaymentDate, config('app.timezone')); if (null === $object) { $object = today(config('app.timezone')); } @@ -229,25 +205,30 @@ class AccountTransformer extends AbstractTransformer return [$creditCardType, $monthlyPaymentDate]; } - /** - * TODO refactor call to get~OpeningBalanceAmount / Date because it is a lot of queries - */ - private function getOpeningBalance(Account $account, string $accountType, bool $convertToNative): array + private function getOpeningBalance(Account $account, string $accountType): array { $openingBalance = null; $openingBalanceDate = null; $nativeOpeningBalance = null; if (in_array($accountType, ['asset', 'liabilities'], true)) { - $openingBalance = $this->repository->getOpeningBalanceAmount($account, false); - $nativeOpeningBalance = $this->repository->getOpeningBalanceAmount($account, true); - $openingBalanceDate = $this->repository->getOpeningBalanceDate($account); + // grab from meta. + $openingBalance = $account->meta['opening_balance_amount'] ?? null; + $nativeOpeningBalance = null; + $openingBalanceDate = $account->meta['opening_balance_date'] ?? null; } if (null !== $openingBalanceDate) { - $object = Carbon::createFromFormat('Y-m-d H:i:s', $openingBalanceDate, config('app.timezone')); + $object = Carbon::createFromFormat('Y-m-d H:i:s', $openingBalanceDate, config('app.timezone')); if (null === $object) { $object = today(config('app.timezone')); } $openingBalanceDate = $object->toAtomString(); + + // NOW do conversion. + if ($this->convertToNative && null !== $account->meta['currency']) { + $converter = new ExchangeRateConverter(); + $nativeOpeningBalance = $converter->convert($account->meta['currency'], $this->native, $object, $openingBalance); + } + } return [$openingBalance, $nativeOpeningBalance, $openingBalanceDate]; @@ -258,8 +239,8 @@ class AccountTransformer extends AbstractTransformer $interest = null; $interestPeriod = null; if ('liabilities' === $accountType) { - $interest = $this->repository->getMetaValue($account, 'interest'); - $interestPeriod = $this->repository->getMetaValue($account, 'interest_period'); + $interest = $account->meta['interest'] ?? null; + $interestPeriod = $account->meta['interest_period'] ?? null; } return [$interest, $interestPeriod];