Expand chart api

This commit is contained in:
James Cole
2024-05-20 06:49:42 +02:00
parent 7170931464
commit 79b91e25c2
8 changed files with 453 additions and 287 deletions

View File

@@ -24,14 +24,14 @@ declare(strict_types=1);
namespace FireflyIII\Api\V2\Controllers\Chart;
use Carbon\Carbon;
use FireflyIII\Api\V2\Controllers\Controller;
use FireflyIII\Api\V2\Request\Chart\DashboardChartRequest;
use FireflyIII\Api\V2\Request\Chart\ChartRequest;
use FireflyIII\Exceptions\FireflyException;
use FireflyIII\Models\Account;
use FireflyIII\Models\AccountType;
use FireflyIII\Models\TransactionCurrency;
use FireflyIII\Repositories\UserGroups\Account\AccountRepositoryInterface;
use FireflyIII\Support\Chart\ChartData;
use FireflyIII\Support\Http\Api\CleansChartData;
use FireflyIII\Support\Http\Api\ValidatesUserGroupTrait;
use Illuminate\Http\JsonResponse;
@@ -46,6 +46,8 @@ class AccountController extends Controller
use ValidatesUserGroupTrait;
private AccountRepositoryInterface $repository;
private ChartData $chartData;
private TransactionCurrency $default;
public function __construct()
{
@@ -54,6 +56,8 @@ class AccountController extends Controller
function ($request, $next) {
$this->repository = app(AccountRepositoryInterface::class);
$this->repository->setUserGroup($this->validateUserGroup($request));
$this->chartData = new ChartData();
$this->default = app('amount')->getDefaultCurrency();
return $next($request);
}
@@ -61,37 +65,49 @@ class AccountController extends Controller
}
/**
* This endpoint is documented at
* https://api-docs.firefly-iii.org/?urls.primaryName=2.0.0%20(v2)#/charts/getChartAccountOverview
*
* The native currency is the preferred currency on the page /currencies.
*
* If a transaction has foreign currency = native currency, the foreign amount will be used, no conversion
* will take place.
*
* TODO validate and set user_group_id from request
*
* TODO fix documentation
* @throws FireflyException
*/
public function dashboard(DashboardChartRequest $request): JsonResponse
public function dashboard(ChartRequest $request): JsonResponse
{
/** @var Carbon $start */
$start = $this->parameters->get('start');
$queryParameters = $request->getParameters();
$accounts = $this->getAccountList($queryParameters);
/** @var Carbon $end */
$end = $this->parameters->get('end');
$end->endOfDay();
// move date to end of day
$queryParameters['start']->startOfDay();
$queryParameters['end']->endOfDay();
/** @var TransactionCurrency $default */
$default = app('amount')->getDefaultCurrency();
$params = $request->getAll();
// loop each account, and collect info:
/** @var Account $account */
foreach ($accounts as $account) {
$this->renderAccountData($queryParameters, $account);
}
/** @var Collection $accounts */
$accounts = $params['accounts'];
$chartData = [];
return response()->json($this->chartData->render());
}
// user's preferences
if (0 === $accounts->count()) {
/**
* TODO Duplicate function but I think it belongs here or in a separate trait
*
*/
private function getAccountList(array $queryParameters): Collection
{
$collection = new Collection();
// always collect from the query parameter, even when it's empty.
foreach ($queryParameters['accounts'] as $accountId) {
$account = $this->repository->find((int) $accountId);
if (null !== $account) {
$collection->push($account);
}
}
// if no "preselected", and found accounts
if ('empty' === $queryParameters['preselected'] && $collection->count() > 0) {
return $collection;
}
// if no preselected, but no accounts:
if ('empty' === $queryParameters['preselected'] && 0 === $collection->count()) {
$defaultSet = $this->repository->getAccountsByType([AccountType::ASSET, AccountType::DEFAULT])->pluck('id')->toArray();
$frontpage = app('preferences')->get('frontpageAccounts', $defaultSet);
@@ -100,28 +116,34 @@ class AccountController extends Controller
$frontpage->save();
}
$accounts = $this->repository->getAccountsById($frontpage->data);
return $this->repository->getAccountsById($frontpage->data);
}
// both options are overruled by "preselected"
if ('all' === $params['preselected']) {
$accounts = $this->repository->getAccountsByType([AccountType::ASSET, AccountType::DEFAULT, AccountType::LOAN, AccountType::DEBT, AccountType::MORTGAGE]);
if ('all' === $queryParameters['preselected']) {
return $this->repository->getAccountsByType([AccountType::ASSET, AccountType::DEFAULT, AccountType::LOAN, AccountType::DEBT, AccountType::MORTGAGE]);
}
if ('assets' === $params['preselected']) {
$accounts = $this->repository->getAccountsByType([AccountType::ASSET, AccountType::DEFAULT]);
if ('assets' === $queryParameters['preselected']) {
return $this->repository->getAccountsByType([AccountType::ASSET, AccountType::DEFAULT]);
}
if ('liabilities' === $params['preselected']) {
$accounts = $this->repository->getAccountsByType([AccountType::LOAN, AccountType::DEBT, AccountType::MORTGAGE]);
if ('liabilities' === $queryParameters['preselected']) {
return $this->repository->getAccountsByType([AccountType::LOAN, AccountType::DEBT, AccountType::MORTGAGE]);
}
/** @var Account $account */
foreach ($accounts as $account) {
return $collection;
}
/**
* @throws FireflyException
*/
private function renderAccountData(array $params, Account $account): void {
$currency = $this->repository->getAccountCurrency($account);
if (null === $currency) {
$currency = $default;
$currency = $this->default;
}
$currentSet = [
'label' => $account->name,
// the currency that belongs to the account.
'currency_id' => (string) $currency->id,
'currency_code' => $currency->code,
@@ -129,23 +151,23 @@ class AccountController extends Controller
'currency_decimal_places' => $currency->decimal_places,
// the default currency of the user (could be the same!)
'native_currency_id' => (string)$default->id,
'native_currency_code' => $default->code,
'native_currency_symbol' => $default->symbol,
'native_currency_decimal_places' => $default->decimal_places,
'start' => $start->toAtomString(),
'end' => $end->toAtomString(),
'native_currency_id' => (string) $this->default->id,
'native_currency_code' => $this->default->code,
'native_currency_symbol' => $this->default->symbol,
'native_currency_decimal_places' => $this->default->decimal_places,
'start' => $params['start']->toAtomString(),
'end' => $params['end']->toAtomString(),
'period' => '1D',
'entries' => [],
'native_entries' => [],
];
$currentStart = clone $start;
$range = app('steam')->balanceInRange($account, $start, clone $end, $currency);
$rangeConverted = app('steam')->balanceInRangeConverted($account, $start, clone $end, $default);
$currentStart = clone $params['start'];
$range = app('steam')->balanceInRange($account, $params['start'], clone $params['end'], $currency);
$rangeConverted = app('steam')->balanceInRangeConverted($account, $params['start'], clone $params['end'], $this->default);
$previous = array_values($range)[0];
$previousConverted = array_values($rangeConverted)[0];
while ($currentStart <= $end) {
while ($currentStart <= $params['end']) {
$format = $currentStart->format('Y-m-d');
$label = $currentStart->toAtomString();
$balance = array_key_exists($format, $range) ? $range[$format] : $previous;
@@ -157,9 +179,6 @@ class AccountController extends Controller
$currentSet['entries'][$label] = $balance;
$currentSet['native_entries'][$label] = $balanceConverted;
}
$chartData[] = $currentSet;
}
return response()->json($this->clean($chartData));
$this->chartData->add($currentSet);
}
}

View File

@@ -25,6 +25,7 @@ namespace FireflyIII\Api\V2\Request\Chart;
use FireflyIII\Enums\UserRoleEnum;
use FireflyIII\JsonApi\Rules\IsValidFilter;
use FireflyIII\Rules\IsFilterValueIn;
use FireflyIII\Support\Http\Api\ParsesQueryFilters;
use FireflyIII\Support\Http\Api\ValidatesUserGroupTrait;
use FireflyIII\Support\Request\ChecksLogin;
@@ -50,17 +51,19 @@ class ChartRequest extends FormRequest
public function getParameters(): array
{
$queryParameters = QueryParameters::cast($this->all());
return [
'start' => $this->dateOrToday($queryParameters, 'start'),
'end' => $this->dateOrToday($queryParameters, 'end'),
// preselected heeft maar een paar toegestane waardes.
'preselected' => $this->stringFromQueryParams($queryParameters, 'preselected', 'empty'),
'accounts' => $this->arrayOfStrings($queryParameters, 'accounts'),
// preselected heeft maar een paar toegestane waardes, dat moet ook goed gaan.
// 'query' => $this->arrayOfStrings($queryParameters, 'query'),
// 'size' => $this->integerFromQueryParams($queryParameters,'size', 50),
// 'account_types' => $this->getAccountTypeParameter($this->arrayOfStrings($queryParameters, 'account_types')),
];
// collect accounts based on this list?
}
// return [
@@ -76,7 +79,12 @@ class ChartRequest extends FormRequest
{
return [
'fields' => JsonApiRule::notSupported(),
'filter' => ['nullable', 'array', new IsValidFilter(['start', 'end', 'preselected', 'accounts'])],
'filter' => ['nullable', 'array',
new IsValidFilter(['start', 'end', 'preselected', 'accounts']),
new IsFilterValueIn('preselected', config('firefly.preselected_accounts')),
],
'include' => JsonApiRule::notSupported(),
'page' => JsonApiRule::notSupported(),
'sort' => JsonApiRule::notSupported(),

View File

@@ -0,0 +1,58 @@
<?php
/*
* IsFilterValueIn.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 Illuminate\Contracts\Validation\ValidationRule;
class IsFilterValueIn implements ValidationRule
{
private string $key;
private array $values;
public function __construct(string $key, array $values) {
$this->key = $key;
$this->values = $values;
}
/**
* @SuppressWarnings(PHPMD.UnusedFormalParameter)
*/
public function validate(string $attribute, mixed $value, \Closure $fail): void
{
if(!is_array($value)) {
return;
}
if(!array_key_exists($this->key, $value)) {
return;
}
$value = $value[$this->key] ?? null;
if(!is_string($value) && !is_null($value)) {
$fail('validation.filter_not_string')->translate(['filter' => $this->key]);
}
if(!in_array($value, $this->values)) {
$fail('validation.filter_must_be_in')->translate(['filter' => $this->key,'values' => join(', ',$this->values)]);
}
//$fail('validation.filter_not_string')->translate(['filter' => $this->key]);
}
}

View File

@@ -0,0 +1,73 @@
<?php
/*
* ChartData.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\Chart;
use FireflyIII\Exceptions\FireflyException;
class ChartData
{
private array $series;
public function __construct()
{
$this->series = [];
}
public function render(): array
{
if (0 === count($this->series)) {
throw new FireflyException('No series added to chart');
}
return $this->series;
}
/**
* @param array $data
*
* @return void
* @throws FireflyException
*/
public function add(array $data): void
{
if (array_key_exists('currency_id', $data)) {
$data['currency_id'] = (string) $data['currency_id'];
}
if (array_key_exists('native_currency_id', $data)) {
$data['native_currency_id'] = (string) $data['native_currency_id'];
}
if (!array_key_exists('start', $data)) {
throw new FireflyException('Data-set is missing the "start"-variable.');
}
if (!array_key_exists('end', $data)) {
throw new FireflyException('Data-set is missing the "end"-variable.');
}
if (!array_key_exists('period', $data)) {
throw new FireflyException('Data-set is missing the "period"-variable.');
}
$this->series[] = $data;
}
}

View File

@@ -54,4 +54,8 @@ trait ParsesQueryFilters
{
return (int) ($parameters->page()[$field] ?? $default);
}
private function stringFromQueryParams(QueryParameters $parameters, string $field, string $default): string
{
return (string) ($parameters->page()[$field] ?? $default);
}
}

View File

@@ -108,8 +108,7 @@
"spatie/period": "^2.4",
"symfony/expression-language": "^7.0",
"symfony/http-client": "^7.0",
"symfony/mailgun-mailer": "^7.0",
"twig/twig": "3.8.0"
"symfony/mailgun-mailer": "^7.0"
},
"require-dev": {
"barryvdh/laravel-debugbar": "^3.9",

41
composer.lock generated
View File

@@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "64e54540b9a552493f83b9a46ac22b4e",
"content-hash": "a3648ab093343dd83bf7e728034ab46c",
"packages": [
{
"name": "bacon/bacon-qr-code",
@@ -5730,23 +5730,23 @@
},
{
"name": "rcrowe/twigbridge",
"version": "v0.14.2",
"version": "v0.14.3",
"source": {
"type": "git",
"url": "https://github.com/rcrowe/TwigBridge.git",
"reference": "6bf5a8fa48eb5d45de0bd5027936796947acfcbc"
"reference": "b8a5591ad79e53adab08841ec06ca11e814b51b4"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/rcrowe/TwigBridge/zipball/6bf5a8fa48eb5d45de0bd5027936796947acfcbc",
"reference": "6bf5a8fa48eb5d45de0bd5027936796947acfcbc",
"url": "https://api.github.com/repos/rcrowe/TwigBridge/zipball/b8a5591ad79e53adab08841ec06ca11e814b51b4",
"reference": "b8a5591ad79e53adab08841ec06ca11e814b51b4",
"shasum": ""
},
"require": {
"illuminate/support": "^9|^10|^11",
"illuminate/view": "^9|^10|^11",
"php": "^8.1",
"twig/twig": "~3.0"
"twig/twig": "~3.9"
},
"require-dev": {
"ext-json": "*",
@@ -5755,10 +5755,6 @@
"phpunit/phpunit": "^8.5.8 || ^9.3.7",
"squizlabs/php_codesniffer": "^3.6"
},
"suggest": {
"laravelcollective/html": "For bringing back html/form in Laravel",
"twig/extensions": "~1.0"
},
"type": "library",
"extra": {
"branch-alias": {
@@ -5800,9 +5796,9 @@
],
"support": {
"issues": "https://github.com/rcrowe/TwigBridge/issues",
"source": "https://github.com/rcrowe/TwigBridge/tree/v0.14.2"
"source": "https://github.com/rcrowe/TwigBridge/tree/v0.14.3"
},
"time": "2024-03-09T19:41:32+00:00"
"time": "2024-04-24T08:52:10+00:00"
},
{
"name": "spatie/backtrace",
@@ -9230,30 +9226,37 @@
},
{
"name": "twig/twig",
"version": "v3.8.0",
"version": "v3.10.3",
"source": {
"type": "git",
"url": "https://github.com/twigphp/Twig.git",
"reference": "9d15f0ac07f44dc4217883ec6ae02fd555c6f71d"
"reference": "67f29781ffafa520b0bbfbd8384674b42db04572"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/twigphp/Twig/zipball/9d15f0ac07f44dc4217883ec6ae02fd555c6f71d",
"reference": "9d15f0ac07f44dc4217883ec6ae02fd555c6f71d",
"url": "https://api.github.com/repos/twigphp/Twig/zipball/67f29781ffafa520b0bbfbd8384674b42db04572",
"reference": "67f29781ffafa520b0bbfbd8384674b42db04572",
"shasum": ""
},
"require": {
"php": ">=7.2.5",
"symfony/deprecation-contracts": "^2.5|^3",
"symfony/polyfill-ctype": "^1.8",
"symfony/polyfill-mbstring": "^1.3",
"symfony/polyfill-php80": "^1.22"
},
"require-dev": {
"psr/container": "^1.0|^2.0",
"symfony/phpunit-bridge": "^5.4.9|^6.3|^7.0"
"symfony/phpunit-bridge": "^5.4.9|^6.4|^7.0"
},
"type": "library",
"autoload": {
"files": [
"src/Resources/core.php",
"src/Resources/debug.php",
"src/Resources/escaper.php",
"src/Resources/string_loader.php"
],
"psr-4": {
"Twig\\": "src/"
}
@@ -9286,7 +9289,7 @@
],
"support": {
"issues": "https://github.com/twigphp/Twig/issues",
"source": "https://github.com/twigphp/Twig/tree/v3.8.0"
"source": "https://github.com/twigphp/Twig/tree/v3.10.3"
},
"funding": [
{
@@ -9298,7 +9301,7 @@
"type": "tidelift"
}
],
"time": "2023-11-21T18:54:41+00:00"
"time": "2024-05-16T10:04:27+00:00"
},
{
"name": "vlucas/phpdotenv",

View File

@@ -25,6 +25,8 @@
declare(strict_types=1);
return [
'filter_must_be_in' => 'Filter ":filter" must be one of: :values',
'filter_not_string' => 'Filter ":filter" is expected to be a string of text',
'bad_api_filter' => 'This API endpoint does not support ":filter" as a filter.',
'bad_type_source' => 'Firefly III can\'t determine the transaction type based on this source account.',
'bad_type_destination' => 'Firefly III can\'t determine the transaction type based on this destination account.',