Expand v2 api

This commit is contained in:
James Cole 2024-07-31 20:19:17 +02:00
parent 3560f0388c
commit dafd99f155
No known key found for this signature in database
GPG Key ID: B49A324B7EAD6D80
13 changed files with 407 additions and 35 deletions

View File

@ -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');

View File

@ -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',

View File

@ -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,
];
}

View File

@ -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

View File

@ -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 () {

View File

@ -0,0 +1,79 @@
<?php
/*
* IsValidDateRange.php
* Copyright (c) 2024 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\Rules;
use Carbon\Carbon;
use Carbon\Exceptions\InvalidDateException;
use Carbon\Exceptions\InvalidFormatException;
use Illuminate\Contracts\Validation\ValidationRule;
class IsValidDateRange implements ValidationRule
{
/**
* @SuppressWarnings(PHPMD.UnusedFormalParameter)
*/
public function validate(string $attribute, mixed $value, \Closure $fail): void
{
$value = (string) $value;
if ('' === $value) {
$fail('validation.date_or_time')->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();
}
}
}

76
app/Support/Balance.php Normal file
View File

@ -0,0 +1,76 @@
<?php
/*
* Balance.php
* Copyright (c) 2024 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;
use Carbon\Carbon;
use FireflyIII\Models\Transaction;
use FireflyIII\Models\TransactionCurrency;
use Illuminate\Support\Collection;
class Balance
{
/**
* Returns the accounts balances as an array, on the account ID.
*
* @param Collection $accounts
* @param Carbon $date
*
* @return array
*/
public function getAccountBalances(Collection $accounts, Carbon $date): array
{
$return = [];
$currencies = [];
$cache = new CacheProperties();
$cache->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;
}
}

View File

@ -0,0 +1,37 @@
<?php
/*
* Balance.php
* Copyright (c) 2024 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\Facades;
use Illuminate\Support\Facades\Facade;
class Balance extends Facade
{
/**
* Get the registered name of the component.
*/
protected static function getFacadeAccessor(): string
{
return 'balance';
}
}

View File

@ -441,7 +441,7 @@ trait PeriodOverview
$cache->addProperty('tag-period-entries');
$cache->addProperty($tag->id);
if ($cache->has()) {
// return $cache->get();
return $cache->get();
}
/** @var array $dates */

View File

@ -0,0 +1,43 @@
<?php
/*
* CollectsCustomParameters.php
* Copyright (c) 2024 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;
use Carbon\Carbon;
trait CollectsCustomParameters
{
protected function getOtherParams(array $params): array
{
$return = [];
if (array_key_exists('startPeriod', $params)) {
$return['start'] = Carbon::parse($params['startPeriod']);
}
if (array_key_exists('endPeriod', $params)) {
$return['end'] = Carbon::parse($params['endPeriod']);
}
return $return;
}
}

View File

@ -24,11 +24,13 @@ declare(strict_types=1);
namespace FireflyIII\Support\JsonApi\Enrichments;
use Carbon\Carbon;
use FireflyIII\Exceptions\FireflyException;
use FireflyIII\Models\Account;
use FireflyIII\Models\AccountType;
use FireflyIII\Models\TransactionCurrency;
use FireflyIII\Repositories\UserGroups\Account\AccountRepositoryInterface;
use FireflyIII\Repositories\UserGroups\Currency\CurrencyRepositoryInterface;
use FireflyIII\Support\Facades\Balance;
use FireflyIII\Support\Http\Api\ExchangeRateConverter;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Log;
@ -40,8 +42,12 @@ use Illuminate\Support\Facades\Log;
*/
class AccountEnrichment implements EnrichmentInterface
{
private Collection $collection;
private array $currencies;
private Collection $collection;
private array $currencies;
private array $balances;
private TransactionCurrency $default;
private ?Carbon $start;
private ?Carbon $end;
private AccountRepositoryInterface $repository;
private CurrencyRepositoryInterface $currencyRepository;
@ -50,6 +56,8 @@ class AccountEnrichment implements EnrichmentInterface
{
$this->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;
}
}

View File

@ -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;

View File

@ -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);