From dafd99f1553a3730af8a53b48d1224a914b72062 Mon Sep 17 00:00:00 2001 From: James Cole Date: Wed, 31 Jul 2024 20:19:17 +0200 Subject: [PATCH] Expand v2 api --- app/Http/Controllers/DebugController.php | 2 + .../V2/Accounts/AccountCollectionQuery.php | 25 ++++- app/JsonApi/V2/Accounts/AccountResource.php | 28 +++++- .../V2/Accounts/Capabilities/AccountQuery.php | 19 ++-- app/Providers/FireflyServiceProvider.php | 7 ++ app/Rules/IsValidDateRange.php | 79 +++++++++++++++ app/Support/Balance.php | 76 ++++++++++++++ app/Support/Facades/Balance.php | 37 +++++++ .../Http/Controllers/PeriodOverview.php | 2 +- .../JsonApi/CollectsCustomParameters.php | 43 ++++++++ .../JsonApi/Enrichments/AccountEnrichment.php | 99 +++++++++++++++---- .../Models/AccountBalanceCalculator.php | 7 +- app/Support/Steam.php | 18 +++- 13 files changed, 407 insertions(+), 35 deletions(-) create mode 100644 app/Rules/IsValidDateRange.php create mode 100644 app/Support/Balance.php create mode 100644 app/Support/Facades/Balance.php create mode 100644 app/Support/JsonApi/CollectsCustomParameters.php diff --git a/app/Http/Controllers/DebugController.php b/app/Http/Controllers/DebugController.php index 0e7111094d..896afc947f 100644 --- a/app/Http/Controllers/DebugController.php +++ b/app/Http/Controllers/DebugController.php @@ -30,6 +30,7 @@ use FireflyIII\Http\Middleware\IsDemoUser; use FireflyIII\Models\AccountType; use FireflyIII\Models\TransactionType; use FireflyIII\Support\Http\Controllers\GetConfigurationData; +use FireflyIII\Support\Models\AccountBalanceCalculator; use FireflyIII\User; use Illuminate\Contracts\View\Factory; use Illuminate\Http\RedirectResponse; @@ -94,6 +95,7 @@ class DebugController extends Controller // also do some recalculations. Artisan::call('firefly-iii:trigger-credit-recalculation'); + AccountBalanceCalculator::forceRecalculateAll(); try { Artisan::call('twig:clean'); diff --git a/app/JsonApi/V2/Accounts/AccountCollectionQuery.php b/app/JsonApi/V2/Accounts/AccountCollectionQuery.php index 2cb3d072cc..464256fe4a 100644 --- a/app/JsonApi/V2/Accounts/AccountCollectionQuery.php +++ b/app/JsonApi/V2/Accounts/AccountCollectionQuery.php @@ -6,6 +6,8 @@ namespace FireflyIII\JsonApi\V2\Accounts; use FireflyIII\Models\Account; use FireflyIII\Rules\IsAllowedGroupAction; +use FireflyIII\Rules\IsDateOrTime; +use FireflyIII\Rules\IsValidDateRange; use Illuminate\Support\Facades\Log; use LaravelJsonApi\Laravel\Http\Requests\ResourceQuery; use LaravelJsonApi\Validation\Rule as JsonApiRule; @@ -25,11 +27,23 @@ class AccountCollectionQuery extends ResourceQuery 'array', JsonApiRule::fieldSets(), ], - 'user_group_id' => [ + 'userGroupId' => [ 'nullable', 'integer', new IsAllowedGroupAction(Account::class, request()->method()), ], + 'startPeriod' => [ + 'nullable', + 'date', + new IsDateOrTime(), + new isValidDateRange(), + ], + 'endPeriod' => [ + 'nullable', + 'date', + new IsDateOrTime(), + new isValidDateRange(), + ], 'filter' => [ 'nullable', 'array', @@ -45,6 +59,15 @@ class AccountCollectionQuery extends ResourceQuery 'array', JsonApiRule::page(), ], + 'page.number' => [ + 'integer', + 'min:1', + ], + + 'page.size' => [ + 'integer', + 'min:1', + ], 'sort' => [ 'nullable', 'string', diff --git a/app/JsonApi/V2/Accounts/AccountResource.php b/app/JsonApi/V2/Accounts/AccountResource.php index bbdd2133bb..f137eda932 100644 --- a/app/JsonApi/V2/Accounts/AccountResource.php +++ b/app/JsonApi/V2/Accounts/AccountResource.php @@ -39,24 +39,46 @@ class AccountResource extends JsonApiResource 'name' => $this->resource->name, 'active' => $this->resource->active, 'order' => $this->resource->order, + 'iban' => $this->resource->iban, 'type' => $this->resource->account_type_string, 'account_role' => $this->resource->account_role, 'account_number' => '' === $this->resource->account_number ? null : $this->resource->account_number, - // currency + // currency (if the account has a currency setting, otherwise NULL). 'currency_id' => $this->resource->currency_id, 'currency_name' => $this->resource->currency_name, 'currency_code' => $this->resource->currency_code, 'currency_symbol' => $this->resource->currency_symbol, 'currency_decimal_places' => $this->resource->currency_decimal_places, + 'is_multi_currency' => '1' === $this->resource->is_multi_currency, + + // balances + 'balance' => $this->resource->balance, + 'native_balance' => $this->resource->native_balance, // liability things 'liability_direction' => $this->resource->liability_direction, 'interest' => $this->resource->interest, 'interest_period' => $this->resource->interest_period, - 'current_debt' => $this->resource->current_debt, + 'current_debt' => $this->resource->current_debt, // TODO may be removed in the future. - 'last_activity' => $this->resource->last_activity, + + // other things + 'last_activity' => $this->resource->last_activity, + + + // still to do + + // balance difference +// 'balance_difference' => $balanceDiff, +// 'native_balance_difference' => $nativeBalanceDiff, +// 'balance_difference_start' => $diffStart, +// 'balance_difference_end' => $diffEnd, + + // object group +// 'object_group_id' => null !== $objectGroupId ? (string) $objectGroupId : null, +// 'object_group_order' => $objectGroupOrder, +// 'object_group_title' => $objectGroupTitle, ]; } diff --git a/app/JsonApi/V2/Accounts/Capabilities/AccountQuery.php b/app/JsonApi/V2/Accounts/Capabilities/AccountQuery.php index 7afcca2a7e..ec1b5e6169 100644 --- a/app/JsonApi/V2/Accounts/Capabilities/AccountQuery.php +++ b/app/JsonApi/V2/Accounts/Capabilities/AccountQuery.php @@ -23,6 +23,7 @@ declare(strict_types=1); namespace FireflyIII\JsonApi\V2\Accounts\Capabilities; +use FireflyIII\Support\JsonApi\CollectsCustomParameters; use FireflyIII\Support\JsonApi\Concerns\UsergroupAware; use FireflyIII\Support\JsonApi\Enrichments\AccountEnrichment; use FireflyIII\Support\JsonApi\ExpandsQuery; @@ -42,6 +43,7 @@ class AccountQuery extends QueryAll implements HasPagination use SortsCollection; use UsergroupAware; use ValidateSortParameters; + use CollectsCustomParameters; #[\Override] /** @@ -51,17 +53,20 @@ class AccountQuery extends QueryAll implements HasPagination { Log::debug(__METHOD__); // collect filters - $filters = $this->queryParameters->filter(); + $filters = $this->queryParameters->filter(); // collect sort options - $sort = $this->queryParameters->sortFields(); + $sort = $this->queryParameters->sortFields(); // collect pagination based on the page $pagination = $this->filtersPagination($this->queryParameters->page()); // check if we need all accounts, regardless of pagination // This is necessary when the user wants to sort on specific params. - $needsAll = $this->needsFullDataset('account', $sort); + $needsAll = $this->needsFullDataset('account', $sort); + + // params that were not recognised, may be my own custom stuff. + $otherParams = $this->getOtherParams($this->queryParameters->unrecognisedParameters()); // start the query - $query = $this->userGroup->accounts(); + $query = $this->userGroup->accounts(); // add pagination to the query, limiting the results. if (!$needsAll) { @@ -69,14 +74,16 @@ class AccountQuery extends QueryAll implements HasPagination } // add sort and filter parameters to the query. - $query = $this->addSortParams($query, $sort); - $query = $this->addFilterParams('account', $query, $filters); + $query = $this->addSortParams($query, $sort); + $query = $this->addFilterParams('account', $query, $filters); // collect the result. $collection = $query->get(['accounts.*']); // enrich the collected data $enrichment = new AccountEnrichment(); + $enrichment->setStart($otherParams['start'] ?? null); + $enrichment->setEnd($otherParams['end'] ?? null); $collection = $enrichment->enrich($collection); // TODO add filters after the query, if there are filters that cannot be applied to the database but only diff --git a/app/Providers/FireflyServiceProvider.php b/app/Providers/FireflyServiceProvider.php index 681670511d..752fb21309 100644 --- a/app/Providers/FireflyServiceProvider.php +++ b/app/Providers/FireflyServiceProvider.php @@ -58,6 +58,7 @@ use FireflyIII\Services\Password\Verifier; use FireflyIII\Services\Webhook\StandardWebhookSender; use FireflyIII\Services\Webhook\WebhookSenderInterface; use FireflyIII\Support\Amount; +use FireflyIII\Support\Balance; use FireflyIII\Support\ExpandedForm; use FireflyIII\Support\FireflyConfig; use FireflyIII\Support\Form\AccountForm; @@ -133,6 +134,12 @@ class FireflyServiceProvider extends ServiceProvider return new Steam(); } ); + $this->app->bind( + 'balance', + static function () { + return new Balance(); + } + ); $this->app->bind( 'expandedform', static function () { diff --git a/app/Rules/IsValidDateRange.php b/app/Rules/IsValidDateRange.php new file mode 100644 index 0000000000..92f2fc249d --- /dev/null +++ b/app/Rules/IsValidDateRange.php @@ -0,0 +1,79 @@ +translate(); + + return; + } + $other = 'startPeriod'; + if ('startPeriod' === $attribute) { + $other = 'endPeriod'; + } + $otherValue = request()->get($other); + // parse date, twice. + try { + $left = Carbon::parse($value); + $right = Carbon::parse($otherValue); + } catch (InvalidDateException $e) { // @phpstan-ignore-line + app('log')->error(sprintf('"%s" or "%s" is not a valid date or time: %s', $value, $otherValue, $e->getMessage())); + + $fail('validation.date_or_time')->translate(); + + return; + } catch (InvalidFormatException $e) { + app('log')->error(sprintf('"%s" or "%s" is of an invalid format: %s', $value, $otherValue, $e->getMessage())); + + $fail('validation.date_or_time')->translate(); + + return; + } + // start must be before end. + if ('startPeriod' === $attribute) { + if ($left->gt($right)) { + $fail('validation.date_after')->translate(); + } + return; + } + // end must be after start + if ($left->lt($right)) { + $fail('validation.date_after')->translate(); + } + } +} + diff --git a/app/Support/Balance.php b/app/Support/Balance.php new file mode 100644 index 0000000000..baf348ccf7 --- /dev/null +++ b/app/Support/Balance.php @@ -0,0 +1,76 @@ +addProperty($accounts->pluck('id')->toArray()); + $cache->addProperty('getAccountBalances'); + $cache->addProperty($date); + if ($cache->has()) { + 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'); + + $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; + $currencies[$currencyId] ??= TransactionCurrency::find($currencyId); + $return[$accountId] ??= []; + if (array_key_exists($currencyId, $return[$accountId])) { + continue; + } + $return[$accountId][$currencyId] = ['currency' => $currencies[$currencyId], 'balance' => $entry->balance_after, 'date' => clone $date]; + } + return $return; + + } +} diff --git a/app/Support/Facades/Balance.php b/app/Support/Facades/Balance.php new file mode 100644 index 0000000000..a281f83b1a --- /dev/null +++ b/app/Support/Facades/Balance.php @@ -0,0 +1,37 @@ +addProperty('tag-period-entries'); $cache->addProperty($tag->id); if ($cache->has()) { - // return $cache->get(); + return $cache->get(); } /** @var array $dates */ diff --git a/app/Support/JsonApi/CollectsCustomParameters.php b/app/Support/JsonApi/CollectsCustomParameters.php new file mode 100644 index 0000000000..c45cffc1af --- /dev/null +++ b/app/Support/JsonApi/CollectsCustomParameters.php @@ -0,0 +1,43 @@ +repository = app(AccountRepositoryInterface::class); $this->currencyRepository = app(CurrencyRepositoryInterface::class); + $this->start = null; + $this->end = null; } #[\Override] @@ -61,13 +69,15 @@ class AccountEnrichment implements EnrichmentInterface Log::debug(sprintf('Now doing account enrichment for %d account(s)', $collection->count())); // prep local fields $this->collection = $collection; + $this->default = app('amount')->getDefaultCurrency(); $this->currencies = []; + $this->balances = []; // do everything here: $this->getLastActivity(); $this->collectAccountTypes(); $this->collectMetaData(); - // $this->getMetaBalances(); + $this->getMetaBalances(); // $this->collection->transform(function (Account $account) { // $account->user_array = ['id' => 1, 'bla bla' => 'bla']; @@ -94,22 +104,65 @@ class AccountEnrichment implements EnrichmentInterface } } - /** - * TODO this method refers to a single-use method inside Steam that could be moved here. - */ private function getMetaBalances(): void { - try { - $array = app('steam')->balancesByAccountsConverted($this->collection, today()); - } catch (FireflyException $e) { - Log::error(sprintf('Could not load balances: %s', $e->getMessage())); + $this->balances = Balance::getAccountBalances($this->collection, today()); + $balances = $this->balances; + $default = $this->default; - return; + // get start and end, so the balance difference can be generated. + $start = null; + $end = null; + if(null !== $this->start) { + $start = Balance::getAccountBalances($this->collection, $this->start); } - foreach ($array as $accountId => $row) { - $this->collection->where('id', $accountId)->first()->balance = $row['balance']; - $this->collection->where('id', $accountId)->first()->native_balance = $row['native_balance']; + if(null !== $this->end) { + $end = Balance::getAccountBalances($this->collection, $this->end); } + + + $this->collection->transform(function (Account $account) use ($balances, $default) { + $converter = new ExchangeRateConverter(); + $native = [ + 'currency_id' => $this->default->id, + 'currency_name' => $this->default->name, + 'currency_code' => $this->default->code, + 'currency_symbol' => $this->default->symbol, + 'currency_decimal_places' => $this->default->decimal_places, + 'balance' => '0', + ]; + if (array_key_exists($account->id, $balances)) { + $set = []; + foreach ($balances[$account->id] as $entry) { + $set[] = [ + 'currency_id' => $entry['currency']->id, + 'currency_name' => $entry['currency']->name, + 'currency_code' => $entry['currency']->code, + 'currency_symbol' => $entry['currency']->symbol, + 'currency_decimal_places' => $entry['currency']->decimal_places, + 'balance' => $entry['balance'], + ]; + $native['balance'] = bcadd($native['balance'], $converter->convert($entry['currency'], $default, today(), $entry['balance'])); + } + $account->balance = $set; + $account->native_balance = $native; + } + + + return $account; + }); + +// try { +// $array = app('steam')->balancesByAccountsConverted($this->collection, today()); +// } catch (FireflyException $e) { +// Log::error(sprintf('Could not load balances: %s', $e->getMessage())); +// +// return; +// } +// foreach ($array as $accountId => $row) { +// //$this->collection->where('id', $accountId)->first()->balance = $row['balance']; +// //$this->collection->where('id', $accountId)->first()->native_balance = $row['native_balance']; +// } } /** @@ -133,10 +186,10 @@ class AccountEnrichment implements EnrichmentInterface private function collectMetaData(): void { - $metaFields = $this->repository->getMetaValues($this->collection, ['currency_id', 'account_role', 'account_number', 'liability_direction', 'interest', 'interest_period', 'current_debt']); + $metaFields = $this->repository->getMetaValues($this->collection, ['is_multi_currency', 'currency_id', 'account_role', 'account_number', 'liability_direction', 'interest', 'interest_period', 'current_debt']); $currencyIds = $metaFields->where('name', 'currency_id')->pluck('data')->toArray(); - $currencies = []; + $currencies = []; foreach ($this->currencyRepository->getByIds($currencyIds) as $currency) { $id = $currency->id; $currencies[$id] = $currency; @@ -168,4 +221,16 @@ class AccountEnrichment implements EnrichmentInterface return $collection->first(); } + + public function setStart(?Carbon $start): void + { + $this->start = $start; + } + + public function setEnd(?Carbon $end): void + { + $this->end = $end; + } + + } diff --git a/app/Support/Models/AccountBalanceCalculator.php b/app/Support/Models/AccountBalanceCalculator.php index 386a922c8d..162e010ccf 100644 --- a/app/Support/Models/AccountBalanceCalculator.php +++ b/app/Support/Models/AccountBalanceCalculator.php @@ -108,7 +108,8 @@ 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') // this order is the same as GroupCollector, but in the exact reverse. ->orderBy('transaction_journals.date', 'asc') ->orderBy('transaction_journals.order', 'desc') @@ -116,7 +117,7 @@ class AccountBalanceCalculator ->orderBy('transaction_journals.description', 'asc') ->orderBy('transactions.amount', 'asc') ; - if (count($accounts) > 0) { + if ($accounts->count() > 0) { $query->whereIn('transactions.account_id', $accounts->pluck('id')->toArray()); } @@ -131,7 +132,7 @@ class AccountBalanceCalculator // before and after are easy: $before = $balances[$entry->account_id][$entry->transaction_currency_id]; $after = bcadd($before, $entry->amount); - if (true === $entry->balance_dirty) { + if (true === $entry->balance_dirty || $accounts->count() > 0) { // update the transaction: $entry->balance_before = $before; $entry->balance_after = $after; diff --git a/app/Support/Steam.php b/app/Support/Steam.php index ed69e351d9..00b50458f6 100644 --- a/app/Support/Steam.php +++ b/app/Support/Steam.php @@ -39,8 +39,12 @@ use Illuminate\Support\Facades\Log; */ class Steam { + /** + * @deprecated + */ public function balanceIgnoreVirtual(Account $account, Carbon $date): string { + Log::warning(sprintf('Deprecated method %s, do not use.', __METHOD__)); /** @var AccountRepositoryInterface $repository */ $repository = app(AccountRepositoryInterface::class); $repository->setUser($account->user); @@ -89,6 +93,7 @@ class Steam */ public function balanceInRange(Account $account, Carbon $start, Carbon $end, ?TransactionCurrency $currency = null): array { + Log::warning(sprintf('Deprecated method %s, do not use.', __METHOD__)); $cache = new CacheProperties(); $cache->addProperty($account->id); $cache->addProperty('balance-in-range'); @@ -209,8 +214,7 @@ class Steam */ public function balance(Account $account, Carbon $date, ?TransactionCurrency $currency = null): string { - //throw new FireflyException('This method is obsolete.'); - Log::warning('This method is obsolete.'); + Log::warning(sprintf('Deprecated method %s, do not use.', __METHOD__)); // abuse chart properties: $cache = new CacheProperties(); $cache->addProperty($account->id); @@ -257,6 +261,7 @@ class Steam */ public function balanceInRangeConverted(Account $account, Carbon $start, Carbon $end, TransactionCurrency $native): array { + Log::warning(sprintf('Deprecated method %s, do not use.', __METHOD__)); $cache = new CacheProperties(); $cache->addProperty($account->id); $cache->addProperty('balance-in-range-converted'); @@ -381,6 +386,7 @@ class Steam */ public function balanceConverted(Account $account, Carbon $date, TransactionCurrency $native): string { + Log::warning(sprintf('Deprecated method %s, do not use.', __METHOD__)); Log::debug(sprintf('Now in balanceConverted (%s) for account #%d, converting to %s', $date->format('Y-m-d'), $account->id, $native->code)); $cache = new CacheProperties(); $cache->addProperty($account->id); @@ -390,7 +396,7 @@ class Steam if ($cache->has()) { Log::debug('Cached!'); - // return $cache->get(); + return $cache->get(); } /** @var AccountRepositoryInterface $repository */ @@ -520,6 +526,7 @@ class Steam */ public function balancesByAccounts(Collection $accounts, Carbon $date): array { + Log::warning(sprintf('Deprecated method %s, do not use.', __METHOD__)); $ids = $accounts->pluck('id')->toArray(); // cache this property. $cache = new CacheProperties(); @@ -550,6 +557,7 @@ class Steam */ public function balancesByAccountsConverted(Collection $accounts, Carbon $date): array { + Log::warning(sprintf('Deprecated method %s, do not use.', __METHOD__)); $ids = $accounts->pluck('id')->toArray(); // cache this property. $cache = new CacheProperties(); @@ -557,7 +565,7 @@ class Steam $cache->addProperty('balances-converted'); $cache->addProperty($date); if ($cache->has()) { - // return $cache->get(); + return $cache->get(); } // need to do this per account. @@ -583,6 +591,7 @@ class Steam */ public function balancesPerCurrencyByAccounts(Collection $accounts, Carbon $date): array { + Log::warning(sprintf('Deprecated method %s, do not use.', __METHOD__)); $ids = $accounts->pluck('id')->toArray(); // cache this property. $cache = new CacheProperties(); @@ -608,6 +617,7 @@ class Steam public function balancePerCurrency(Account $account, Carbon $date): array { + Log::warning(sprintf('Deprecated method %s, do not use.', __METHOD__)); // abuse chart properties: $cache = new CacheProperties(); $cache->addProperty($account->id);