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; namespace FireflyIII\Api\V2\Controllers\Chart;
use Carbon\Carbon;
use FireflyIII\Api\V2\Controllers\Controller; 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\Exceptions\FireflyException;
use FireflyIII\Models\Account; use FireflyIII\Models\Account;
use FireflyIII\Models\AccountType; use FireflyIII\Models\AccountType;
use FireflyIII\Models\TransactionCurrency; use FireflyIII\Models\TransactionCurrency;
use FireflyIII\Repositories\UserGroups\Account\AccountRepositoryInterface; use FireflyIII\Repositories\UserGroups\Account\AccountRepositoryInterface;
use FireflyIII\Support\Chart\ChartData;
use FireflyIII\Support\Http\Api\CleansChartData; use FireflyIII\Support\Http\Api\CleansChartData;
use FireflyIII\Support\Http\Api\ValidatesUserGroupTrait; use FireflyIII\Support\Http\Api\ValidatesUserGroupTrait;
use Illuminate\Http\JsonResponse; use Illuminate\Http\JsonResponse;
@@ -46,6 +46,8 @@ class AccountController extends Controller
use ValidatesUserGroupTrait; use ValidatesUserGroupTrait;
private AccountRepositoryInterface $repository; private AccountRepositoryInterface $repository;
private ChartData $chartData;
private TransactionCurrency $default;
public function __construct() public function __construct()
{ {
@@ -54,6 +56,8 @@ class AccountController extends Controller
function ($request, $next) { function ($request, $next) {
$this->repository = app(AccountRepositoryInterface::class); $this->repository = app(AccountRepositoryInterface::class);
$this->repository->setUserGroup($this->validateUserGroup($request)); $this->repository->setUserGroup($this->validateUserGroup($request));
$this->chartData = new ChartData();
$this->default = app('amount')->getDefaultCurrency();
return $next($request); return $next($request);
} }
@@ -61,37 +65,49 @@ class AccountController extends Controller
} }
/** /**
* This endpoint is documented at * TODO fix documentation
* 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
*
* @throws FireflyException * @throws FireflyException
*/ */
public function dashboard(DashboardChartRequest $request): JsonResponse public function dashboard(ChartRequest $request): JsonResponse
{ {
/** @var Carbon $start */ $queryParameters = $request->getParameters();
$start = $this->parameters->get('start'); $accounts = $this->getAccountList($queryParameters);
/** @var Carbon $end */ // move date to end of day
$end = $this->parameters->get('end'); $queryParameters['start']->startOfDay();
$end->endOfDay(); $queryParameters['end']->endOfDay();
/** @var TransactionCurrency $default */ // loop each account, and collect info:
$default = app('amount')->getDefaultCurrency(); /** @var Account $account */
$params = $request->getAll(); foreach ($accounts as $account) {
$this->renderAccountData($queryParameters, $account);
}
/** @var Collection $accounts */ return response()->json($this->chartData->render());
$accounts = $params['accounts']; }
$chartData = [];
// 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(); $defaultSet = $this->repository->getAccountsByType([AccountType::ASSET, AccountType::DEFAULT])->pluck('id')->toArray();
$frontpage = app('preferences')->get('frontpageAccounts', $defaultSet); $frontpage = app('preferences')->get('frontpageAccounts', $defaultSet);
@@ -100,66 +116,69 @@ class AccountController extends Controller
$frontpage->save(); $frontpage->save();
} }
$accounts = $this->repository->getAccountsById($frontpage->data); return $this->repository->getAccountsById($frontpage->data);
} }
// both options are overruled by "preselected" // both options are overruled by "preselected"
if ('all' === $params['preselected']) { if ('all' === $queryParameters['preselected']) {
$accounts = $this->repository->getAccountsByType([AccountType::ASSET, AccountType::DEFAULT, AccountType::LOAN, AccountType::DEBT, AccountType::MORTGAGE]); return $this->repository->getAccountsByType([AccountType::ASSET, AccountType::DEFAULT, AccountType::LOAN, AccountType::DEBT, AccountType::MORTGAGE]);
} }
if ('assets' === $params['preselected']) { if ('assets' === $queryParameters['preselected']) {
$accounts = $this->repository->getAccountsByType([AccountType::ASSET, AccountType::DEFAULT]); return $this->repository->getAccountsByType([AccountType::ASSET, AccountType::DEFAULT]);
} }
if ('liabilities' === $params['preselected']) { if ('liabilities' === $queryParameters['preselected']) {
$accounts = $this->repository->getAccountsByType([AccountType::LOAN, AccountType::DEBT, AccountType::MORTGAGE]); return $this->repository->getAccountsByType([AccountType::LOAN, AccountType::DEBT, AccountType::MORTGAGE]);
} }
/** @var Account $account */ return $collection;
foreach ($accounts as $account) { }
$currency = $this->repository->getAccountCurrency($account);
if (null === $currency) {
$currency = $default;
}
$currentSet = [
'label' => $account->name,
// the currency that belongs to the account.
'currency_id' => (string)$currency->id,
'currency_code' => $currency->code,
'currency_symbol' => $currency->symbol,
'currency_decimal_places' => $currency->decimal_places,
// the default currency of the user (could be the same!) /**
'native_currency_id' => (string)$default->id, * @throws FireflyException
'native_currency_code' => $default->code, */
'native_currency_symbol' => $default->symbol, private function renderAccountData(array $params, Account $account): void {
'native_currency_decimal_places' => $default->decimal_places, $currency = $this->repository->getAccountCurrency($account);
'start' => $start->toAtomString(), if (null === $currency) {
'end' => $end->toAtomString(), $currency = $this->default;
'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);
$previous = array_values($range)[0];
$previousConverted = array_values($rangeConverted)[0];
while ($currentStart <= $end) {
$format = $currentStart->format('Y-m-d');
$label = $currentStart->toAtomString();
$balance = array_key_exists($format, $range) ? $range[$format] : $previous;
$balanceConverted = array_key_exists($format, $rangeConverted) ? $rangeConverted[$format] : $previousConverted;
$previous = $balance;
$previousConverted = $balanceConverted;
$currentStart->addDay();
$currentSet['entries'][$label] = $balance;
$currentSet['native_entries'][$label] = $balanceConverted;
}
$chartData[] = $currentSet;
} }
$currentSet = [
'label' => $account->name,
return response()->json($this->clean($chartData)); // the currency that belongs to the account.
'currency_id' => (string) $currency->id,
'currency_code' => $currency->code,
'currency_symbol' => $currency->symbol,
'currency_decimal_places' => $currency->decimal_places,
// the default currency of the user (could be the same!)
'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 $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 <= $params['end']) {
$format = $currentStart->format('Y-m-d');
$label = $currentStart->toAtomString();
$balance = array_key_exists($format, $range) ? $range[$format] : $previous;
$balanceConverted = array_key_exists($format, $rangeConverted) ? $rangeConverted[$format] : $previousConverted;
$previous = $balance;
$previousConverted = $balanceConverted;
$currentStart->addDay();
$currentSet['entries'][$label] = $balance;
$currentSet['native_entries'][$label] = $balanceConverted;
}
$this->chartData->add($currentSet);
} }
} }

View File

@@ -25,6 +25,7 @@ namespace FireflyIII\Api\V2\Request\Chart;
use FireflyIII\Enums\UserRoleEnum; use FireflyIII\Enums\UserRoleEnum;
use FireflyIII\JsonApi\Rules\IsValidFilter; use FireflyIII\JsonApi\Rules\IsValidFilter;
use FireflyIII\Rules\IsFilterValueIn;
use FireflyIII\Support\Http\Api\ParsesQueryFilters; use FireflyIII\Support\Http\Api\ParsesQueryFilters;
use FireflyIII\Support\Http\Api\ValidatesUserGroupTrait; use FireflyIII\Support\Http\Api\ValidatesUserGroupTrait;
use FireflyIII\Support\Request\ChecksLogin; use FireflyIII\Support\Request\ChecksLogin;
@@ -50,17 +51,19 @@ class ChartRequest extends FormRequest
public function getParameters(): array public function getParameters(): array
{ {
$queryParameters = QueryParameters::cast($this->all()); $queryParameters = QueryParameters::cast($this->all());
return [ return [
'start' => $this->dateOrToday($queryParameters, 'start'), 'start' => $this->dateOrToday($queryParameters, 'start'),
'end' => $this->dateOrToday($queryParameters, 'end'), 'end' => $this->dateOrToday($queryParameters, 'end'),
'preselected' => $this->stringFromQueryParams($queryParameters, 'preselected', 'empty'),
// preselected heeft maar een paar toegestane waardes. 'accounts' => $this->arrayOfStrings($queryParameters, 'accounts'),
// preselected heeft maar een paar toegestane waardes, dat moet ook goed gaan.
// 'query' => $this->arrayOfStrings($queryParameters, 'query'), // 'query' => $this->arrayOfStrings($queryParameters, 'query'),
// 'size' => $this->integerFromQueryParams($queryParameters,'size', 50), // 'size' => $this->integerFromQueryParams($queryParameters,'size', 50),
// 'account_types' => $this->getAccountTypeParameter($this->arrayOfStrings($queryParameters, 'account_types')), // 'account_types' => $this->getAccountTypeParameter($this->arrayOfStrings($queryParameters, 'account_types')),
]; ];
// collect accounts based on this list?
} }
// return [ // return [
@@ -76,7 +79,12 @@ class ChartRequest extends FormRequest
{ {
return [ return [
'fields' => JsonApiRule::notSupported(), '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(), 'include' => JsonApiRule::notSupported(),
'page' => JsonApiRule::notSupported(), 'page' => JsonApiRule::notSupported(),
'sort' => 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); 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", "spatie/period": "^2.4",
"symfony/expression-language": "^7.0", "symfony/expression-language": "^7.0",
"symfony/http-client": "^7.0", "symfony/http-client": "^7.0",
"symfony/mailgun-mailer": "^7.0", "symfony/mailgun-mailer": "^7.0"
"twig/twig": "3.8.0"
}, },
"require-dev": { "require-dev": {
"barryvdh/laravel-debugbar": "^3.9", "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", "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically" "This file is @generated automatically"
], ],
"content-hash": "64e54540b9a552493f83b9a46ac22b4e", "content-hash": "a3648ab093343dd83bf7e728034ab46c",
"packages": [ "packages": [
{ {
"name": "bacon/bacon-qr-code", "name": "bacon/bacon-qr-code",
@@ -5730,23 +5730,23 @@
}, },
{ {
"name": "rcrowe/twigbridge", "name": "rcrowe/twigbridge",
"version": "v0.14.2", "version": "v0.14.3",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/rcrowe/TwigBridge.git", "url": "https://github.com/rcrowe/TwigBridge.git",
"reference": "6bf5a8fa48eb5d45de0bd5027936796947acfcbc" "reference": "b8a5591ad79e53adab08841ec06ca11e814b51b4"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/rcrowe/TwigBridge/zipball/6bf5a8fa48eb5d45de0bd5027936796947acfcbc", "url": "https://api.github.com/repos/rcrowe/TwigBridge/zipball/b8a5591ad79e53adab08841ec06ca11e814b51b4",
"reference": "6bf5a8fa48eb5d45de0bd5027936796947acfcbc", "reference": "b8a5591ad79e53adab08841ec06ca11e814b51b4",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
"illuminate/support": "^9|^10|^11", "illuminate/support": "^9|^10|^11",
"illuminate/view": "^9|^10|^11", "illuminate/view": "^9|^10|^11",
"php": "^8.1", "php": "^8.1",
"twig/twig": "~3.0" "twig/twig": "~3.9"
}, },
"require-dev": { "require-dev": {
"ext-json": "*", "ext-json": "*",
@@ -5755,10 +5755,6 @@
"phpunit/phpunit": "^8.5.8 || ^9.3.7", "phpunit/phpunit": "^8.5.8 || ^9.3.7",
"squizlabs/php_codesniffer": "^3.6" "squizlabs/php_codesniffer": "^3.6"
}, },
"suggest": {
"laravelcollective/html": "For bringing back html/form in Laravel",
"twig/extensions": "~1.0"
},
"type": "library", "type": "library",
"extra": { "extra": {
"branch-alias": { "branch-alias": {
@@ -5800,9 +5796,9 @@
], ],
"support": { "support": {
"issues": "https://github.com/rcrowe/TwigBridge/issues", "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", "name": "spatie/backtrace",
@@ -9230,30 +9226,37 @@
}, },
{ {
"name": "twig/twig", "name": "twig/twig",
"version": "v3.8.0", "version": "v3.10.3",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/twigphp/Twig.git", "url": "https://github.com/twigphp/Twig.git",
"reference": "9d15f0ac07f44dc4217883ec6ae02fd555c6f71d" "reference": "67f29781ffafa520b0bbfbd8384674b42db04572"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/twigphp/Twig/zipball/9d15f0ac07f44dc4217883ec6ae02fd555c6f71d", "url": "https://api.github.com/repos/twigphp/Twig/zipball/67f29781ffafa520b0bbfbd8384674b42db04572",
"reference": "9d15f0ac07f44dc4217883ec6ae02fd555c6f71d", "reference": "67f29781ffafa520b0bbfbd8384674b42db04572",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
"php": ">=7.2.5", "php": ">=7.2.5",
"symfony/deprecation-contracts": "^2.5|^3",
"symfony/polyfill-ctype": "^1.8", "symfony/polyfill-ctype": "^1.8",
"symfony/polyfill-mbstring": "^1.3", "symfony/polyfill-mbstring": "^1.3",
"symfony/polyfill-php80": "^1.22" "symfony/polyfill-php80": "^1.22"
}, },
"require-dev": { "require-dev": {
"psr/container": "^1.0|^2.0", "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", "type": "library",
"autoload": { "autoload": {
"files": [
"src/Resources/core.php",
"src/Resources/debug.php",
"src/Resources/escaper.php",
"src/Resources/string_loader.php"
],
"psr-4": { "psr-4": {
"Twig\\": "src/" "Twig\\": "src/"
} }
@@ -9286,7 +9289,7 @@
], ],
"support": { "support": {
"issues": "https://github.com/twigphp/Twig/issues", "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": [ "funding": [
{ {
@@ -9298,7 +9301,7 @@
"type": "tidelift" "type": "tidelift"
} }
], ],
"time": "2023-11-21T18:54:41+00:00" "time": "2024-05-16T10:04:27+00:00"
}, },
{ {
"name": "vlucas/phpdotenv", "name": "vlucas/phpdotenv",

View File

@@ -25,159 +25,161 @@
declare(strict_types=1); declare(strict_types=1);
return [ return [
'bad_api_filter' => 'This API endpoint does not support ":filter" as a filter.', 'filter_must_be_in' => 'Filter ":filter" must be one of: :values',
'bad_type_source' => 'Firefly III can\'t determine the transaction type based on this source account.', 'filter_not_string' => 'Filter ":filter" is expected to be a string of text',
'bad_type_destination' => 'Firefly III can\'t determine the transaction type based on this destination account.', 'bad_api_filter' => 'This API endpoint does not support ":filter" as a filter.',
'missing_where' => 'Array is missing "where"-clause', 'bad_type_source' => 'Firefly III can\'t determine the transaction type based on this source account.',
'missing_update' => 'Array is missing "update"-clause', 'bad_type_destination' => 'Firefly III can\'t determine the transaction type based on this destination account.',
'invalid_where_key' => 'JSON contains an invalid key for the "where"-clause', 'missing_where' => 'Array is missing "where"-clause',
'invalid_update_key' => 'JSON contains an invalid key for the "update"-clause', 'missing_update' => 'Array is missing "update"-clause',
'invalid_query_data' => 'There is invalid data in the %s:%s field of your query.', 'invalid_where_key' => 'JSON contains an invalid key for the "where"-clause',
'invalid_query_account_type' => 'Your query contains accounts of different types, which is not allowed.', 'invalid_update_key' => 'JSON contains an invalid key for the "update"-clause',
'invalid_query_currency' => 'Your query contains accounts that have different currency settings, which is not allowed.', 'invalid_query_data' => 'There is invalid data in the %s:%s field of your query.',
'iban' => 'This is not a valid IBAN.', 'invalid_query_account_type' => 'Your query contains accounts of different types, which is not allowed.',
'zero_or_more' => 'The value cannot be negative.', 'invalid_query_currency' => 'Your query contains accounts that have different currency settings, which is not allowed.',
'more_than_zero' => 'The value must be more than zero.', 'iban' => 'This is not a valid IBAN.',
'more_than_zero_correct' => 'The value must be zero or more.', 'zero_or_more' => 'The value cannot be negative.',
'no_asset_account' => 'This is not an asset account.', 'more_than_zero' => 'The value must be more than zero.',
'date_or_time' => 'The value must be a valid date or time value (ISO 8601).', 'more_than_zero_correct' => 'The value must be zero or more.',
'source_equals_destination' => 'The source account equals the destination account.', 'no_asset_account' => 'This is not an asset account.',
'unique_account_number_for_user' => 'It looks like this account number is already in use.', 'date_or_time' => 'The value must be a valid date or time value (ISO 8601).',
'unique_iban_for_user' => 'It looks like this IBAN is already in use.', 'source_equals_destination' => 'The source account equals the destination account.',
'reconciled_forbidden_field' => 'This transaction is already reconciled, you cannot change the ":field"', 'unique_account_number_for_user' => 'It looks like this account number is already in use.',
'deleted_user' => 'Due to security constraints, you cannot register using this email address.', 'unique_iban_for_user' => 'It looks like this IBAN is already in use.',
'rule_trigger_value' => 'This value is invalid for the selected trigger.', 'reconciled_forbidden_field' => 'This transaction is already reconciled, you cannot change the ":field"',
'rule_action_expression' => 'Invalid expression. :error', 'deleted_user' => 'Due to security constraints, you cannot register using this email address.',
'rule_action_value' => 'This value is invalid for the selected action.', 'rule_trigger_value' => 'This value is invalid for the selected trigger.',
'file_already_attached' => 'Uploaded file ":name" is already attached to this object.', 'rule_action_expression' => 'Invalid expression. :error',
'file_attached' => 'Successfully uploaded file ":name".', 'rule_action_value' => 'This value is invalid for the selected action.',
'file_zero' => 'The file is zero bytes in size.', 'file_already_attached' => 'Uploaded file ":name" is already attached to this object.',
'must_exist' => 'The ID in field :attribute does not exist in the database.', 'file_attached' => 'Successfully uploaded file ":name".',
'all_accounts_equal' => 'All accounts in this field must be equal.', 'file_zero' => 'The file is zero bytes in size.',
'group_title_mandatory' => 'A group title is mandatory when there is more than one transaction.', 'must_exist' => 'The ID in field :attribute does not exist in the database.',
'transaction_types_equal' => 'All splits must be of the same type.', 'all_accounts_equal' => 'All accounts in this field must be equal.',
'invalid_transaction_type' => 'Invalid transaction type.', 'group_title_mandatory' => 'A group title is mandatory when there is more than one transaction.',
'invalid_selection' => 'Your selection is invalid.', 'transaction_types_equal' => 'All splits must be of the same type.',
'belongs_user' => 'This value is linked to an object that does not seem to exist.', 'invalid_transaction_type' => 'Invalid transaction type.',
'belongs_user_or_user_group' => 'This value is linked to an object that does not seem to exist in your current financial administration.', 'invalid_selection' => 'Your selection is invalid.',
'no_access_group' => 'The user has no access to this user group.', 'belongs_user' => 'This value is linked to an object that does not seem to exist.',
'no_accepted_roles_defined' => 'No access roles have been defined for this endpoint, access denied.', 'belongs_user_or_user_group' => 'This value is linked to an object that does not seem to exist in your current financial administration.',
'at_least_one_transaction' => 'Need at least one transaction.', 'no_access_group' => 'The user has no access to this user group.',
'recurring_transaction_id' => 'Need at least one transaction.', 'no_accepted_roles_defined' => 'No access roles have been defined for this endpoint, access denied.',
'need_id_to_match' => 'You need to submit this entry with an ID for the API to be able to match it.', 'at_least_one_transaction' => 'Need at least one transaction.',
'too_many_unmatched' => 'Too many submitted transactions cannot be matched to their respective database entries. Make sure existing entries have a valid ID.', 'recurring_transaction_id' => 'Need at least one transaction.',
'id_does_not_match' => 'Submitted ID #:id does not match expected ID. Make sure it matches or omit the field.', 'need_id_to_match' => 'You need to submit this entry with an ID for the API to be able to match it.',
'at_least_one_repetition' => 'Need at least one repetition.', 'too_many_unmatched' => 'Too many submitted transactions cannot be matched to their respective database entries. Make sure existing entries have a valid ID.',
'require_repeat_until' => 'Require either a number of repetitions, or an end date (repeat_until). Not both.', 'id_does_not_match' => 'Submitted ID #:id does not match expected ID. Make sure it matches or omit the field.',
'require_currency_info' => 'The content of this field is invalid without currency information.', 'at_least_one_repetition' => 'Need at least one repetition.',
'not_transfer_account' => 'This account is not an account that can be used for transfers.', 'require_repeat_until' => 'Require either a number of repetitions, or an end date (repeat_until). Not both.',
'require_currency_amount' => 'The content of this field is invalid without foreign amount information.', 'require_currency_info' => 'The content of this field is invalid without currency information.',
'require_foreign_currency' => 'This field requires a number', 'not_transfer_account' => 'This account is not an account that can be used for transfers.',
'require_foreign_dest' => 'This field value must match the currency of the destination account.', 'require_currency_amount' => 'The content of this field is invalid without foreign amount information.',
'require_foreign_src' => 'This field value must match the currency of the source account.', 'require_foreign_currency' => 'This field requires a number',
'equal_description' => 'Transaction description should not equal global description.', 'require_foreign_dest' => 'This field value must match the currency of the destination account.',
'file_invalid_mime' => 'File ":name" is of type ":mime" which is not accepted as a new upload.', 'require_foreign_src' => 'This field value must match the currency of the source account.',
'file_too_large' => 'File ":name" is too large.', 'equal_description' => 'Transaction description should not equal global description.',
'belongs_to_user' => 'The value of :attribute is unknown.', 'file_invalid_mime' => 'File ":name" is of type ":mime" which is not accepted as a new upload.',
'accepted' => 'The :attribute must be accepted.', 'file_too_large' => 'File ":name" is too large.',
'bic' => 'This is not a valid BIC.', 'belongs_to_user' => 'The value of :attribute is unknown.',
'at_least_one_trigger' => 'Rule must have at least one trigger.', 'accepted' => 'The :attribute must be accepted.',
'at_least_one_active_trigger' => 'Rule must have at least one active trigger.', 'bic' => 'This is not a valid BIC.',
'at_least_one_action' => 'Rule must have at least one action.', 'at_least_one_trigger' => 'Rule must have at least one trigger.',
'at_least_one_active_action' => 'Rule must have at least one active action.', 'at_least_one_active_trigger' => 'Rule must have at least one active trigger.',
'base64' => 'This is not valid base64 encoded data.', 'at_least_one_action' => 'Rule must have at least one action.',
'model_id_invalid' => 'The given ID seems invalid for this model.', 'at_least_one_active_action' => 'Rule must have at least one active action.',
'less' => ':attribute must be less than 10,000,000', 'base64' => 'This is not valid base64 encoded data.',
'active_url' => 'The :attribute is not a valid URL.', 'model_id_invalid' => 'The given ID seems invalid for this model.',
'after' => 'The :attribute must be a date after :date.', 'less' => ':attribute must be less than 10,000,000',
'date_after' => 'The start date must be before the end date.', 'active_url' => 'The :attribute is not a valid URL.',
'alpha' => 'The :attribute may only contain letters.', 'after' => 'The :attribute must be a date after :date.',
'alpha_dash' => 'The :attribute may only contain letters, numbers, and dashes.', 'date_after' => 'The start date must be before the end date.',
'alpha_num' => 'The :attribute may only contain letters and numbers.', 'alpha' => 'The :attribute may only contain letters.',
'array' => 'The :attribute must be an array.', 'alpha_dash' => 'The :attribute may only contain letters, numbers, and dashes.',
'unique_for_user' => 'There already is an entry with this :attribute.', 'alpha_num' => 'The :attribute may only contain letters and numbers.',
'before' => 'The :attribute must be a date before :date.', 'array' => 'The :attribute must be an array.',
'unique_object_for_user' => 'This name is already in use.', 'unique_for_user' => 'There already is an entry with this :attribute.',
'unique_account_for_user' => 'This account name is already in use.', 'before' => 'The :attribute must be a date before :date.',
'unique_object_for_user' => 'This name is already in use.',
'unique_account_for_user' => 'This account name is already in use.',
// Ignore this comment // Ignore this comment
'between.numeric' => 'The :attribute must be between :min and :max.', 'between.numeric' => 'The :attribute must be between :min and :max.',
'between.file' => 'The :attribute must be between :min and :max kilobytes.', 'between.file' => 'The :attribute must be between :min and :max kilobytes.',
'between.string' => 'The :attribute must be between :min and :max characters.', 'between.string' => 'The :attribute must be between :min and :max characters.',
'between.array' => 'The :attribute must have between :min and :max items.', 'between.array' => 'The :attribute must have between :min and :max items.',
'boolean' => 'The :attribute field must be true or false.', 'boolean' => 'The :attribute field must be true or false.',
'confirmed' => 'The :attribute confirmation does not match.', 'confirmed' => 'The :attribute confirmation does not match.',
'date' => 'The :attribute is not a valid date.', 'date' => 'The :attribute is not a valid date.',
'date_format' => 'The :attribute does not match the format :format.', 'date_format' => 'The :attribute does not match the format :format.',
'different' => 'The :attribute and :other must be different.', 'different' => 'The :attribute and :other must be different.',
'digits' => 'The :attribute must be :digits digits.', 'digits' => 'The :attribute must be :digits digits.',
'digits_between' => 'The :attribute must be between :min and :max digits.', 'digits_between' => 'The :attribute must be between :min and :max digits.',
'email' => 'The :attribute must be a valid email address.', 'email' => 'The :attribute must be a valid email address.',
'filled' => 'The :attribute field is required.', 'filled' => 'The :attribute field is required.',
'exists' => 'The selected :attribute is invalid.', 'exists' => 'The selected :attribute is invalid.',
'image' => 'The :attribute must be an image.', 'image' => 'The :attribute must be an image.',
'in' => 'The selected :attribute is invalid.', 'in' => 'The selected :attribute is invalid.',
'integer' => 'The :attribute must be an integer.', 'integer' => 'The :attribute must be an integer.',
'ip' => 'The :attribute must be a valid IP address.', 'ip' => 'The :attribute must be a valid IP address.',
'json' => 'The :attribute must be a valid JSON string.', 'json' => 'The :attribute must be a valid JSON string.',
'max.numeric' => 'The :attribute may not be greater than :max.', 'max.numeric' => 'The :attribute may not be greater than :max.',
'max.file' => 'The :attribute may not be greater than :max kilobytes.', 'max.file' => 'The :attribute may not be greater than :max kilobytes.',
'max.string' => 'The :attribute may not be greater than :max characters.', 'max.string' => 'The :attribute may not be greater than :max characters.',
'max.array' => 'The :attribute may not have more than :max items.', 'max.array' => 'The :attribute may not have more than :max items.',
'mimes' => 'The :attribute must be a file of type: :values.', 'mimes' => 'The :attribute must be a file of type: :values.',
'min.numeric' => 'The :attribute must be at least :min.', 'min.numeric' => 'The :attribute must be at least :min.',
'lte.numeric' => 'The :attribute must be less than or equal :value.', 'lte.numeric' => 'The :attribute must be less than or equal :value.',
'min.file' => 'The :attribute must be at least :min kilobytes.', 'min.file' => 'The :attribute must be at least :min kilobytes.',
'min.string' => 'The :attribute must be at least :min characters.', 'min.string' => 'The :attribute must be at least :min characters.',
'min.array' => 'The :attribute must have at least :min items.', 'min.array' => 'The :attribute must have at least :min items.',
'not_in' => 'The selected :attribute is invalid.', 'not_in' => 'The selected :attribute is invalid.',
'numeric' => 'The :attribute must be a number.', 'numeric' => 'The :attribute must be a number.',
'scientific_notation' => 'The :attribute cannot use the scientific notation.', 'scientific_notation' => 'The :attribute cannot use the scientific notation.',
'numeric_native' => 'The native amount must be a number.', 'numeric_native' => 'The native amount must be a number.',
'numeric_destination' => 'The destination amount must be a number.', 'numeric_destination' => 'The destination amount must be a number.',
'numeric_source' => 'The source amount must be a number.', 'numeric_source' => 'The source amount must be a number.',
'regex' => 'The :attribute format is invalid.', 'regex' => 'The :attribute format is invalid.',
'required' => 'The :attribute field is required.', 'required' => 'The :attribute field is required.',
'required_if' => 'The :attribute field is required when :other is :value.', 'required_if' => 'The :attribute field is required when :other is :value.',
'required_unless' => 'The :attribute field is required unless :other is in :values.', 'required_unless' => 'The :attribute field is required unless :other is in :values.',
'required_with' => 'The :attribute field is required when :values is present.', 'required_with' => 'The :attribute field is required when :values is present.',
'required_with_all' => 'The :attribute field is required when :values is present.', 'required_with_all' => 'The :attribute field is required when :values is present.',
'required_without' => 'The :attribute field is required when :values is not present.', 'required_without' => 'The :attribute field is required when :values is not present.',
'required_without_all' => 'The :attribute field is required when none of :values are present.', 'required_without_all' => 'The :attribute field is required when none of :values are present.',
'same' => 'The :attribute and :other must match.', 'same' => 'The :attribute and :other must match.',
'size.numeric' => 'The :attribute must be :size.', 'size.numeric' => 'The :attribute must be :size.',
'amount_min_over_max' => 'The minimum amount cannot be larger than the maximum amount.', 'amount_min_over_max' => 'The minimum amount cannot be larger than the maximum amount.',
'size.file' => 'The :attribute must be :size kilobytes.', 'size.file' => 'The :attribute must be :size kilobytes.',
'size.string' => 'The :attribute must be :size characters.', 'size.string' => 'The :attribute must be :size characters.',
'size.array' => 'The :attribute must contain :size items.', 'size.array' => 'The :attribute must contain :size items.',
'unique' => 'The :attribute has already been taken.', 'unique' => 'The :attribute has already been taken.',
'string' => 'The :attribute must be a string.', 'string' => 'The :attribute must be a string.',
'url' => 'The :attribute format is invalid.', 'url' => 'The :attribute format is invalid.',
'timezone' => 'The :attribute must be a valid zone.', 'timezone' => 'The :attribute must be a valid zone.',
'2fa_code' => 'The :attribute field is invalid.', '2fa_code' => 'The :attribute field is invalid.',
'dimensions' => 'The :attribute has invalid image dimensions.', 'dimensions' => 'The :attribute has invalid image dimensions.',
'distinct' => 'The :attribute field has a duplicate value.', 'distinct' => 'The :attribute field has a duplicate value.',
'file' => 'The :attribute must be a file.', 'file' => 'The :attribute must be a file.',
'in_array' => 'The :attribute field does not exist in :other.', 'in_array' => 'The :attribute field does not exist in :other.',
'present' => 'The :attribute field must be present.', 'present' => 'The :attribute field must be present.',
'amount_zero' => 'The total amount cannot be zero.', 'amount_zero' => 'The total amount cannot be zero.',
'current_target_amount' => 'The current amount must be less than the target amount.', 'current_target_amount' => 'The current amount must be less than the target amount.',
'unique_piggy_bank_for_user' => 'The name of the piggy bank must be unique.', 'unique_piggy_bank_for_user' => 'The name of the piggy bank must be unique.',
'unique_object_group' => 'The group name must be unique', 'unique_object_group' => 'The group name must be unique',
'starts_with' => 'The value must start with :values.', 'starts_with' => 'The value must start with :values.',
'unique_webhook' => 'You already have a webhook with this combination of URL, trigger, response and delivery.', 'unique_webhook' => 'You already have a webhook with this combination of URL, trigger, response and delivery.',
'unique_existing_webhook' => 'You already have another webhook with this combination of URL, trigger, response and delivery.', 'unique_existing_webhook' => 'You already have another webhook with this combination of URL, trigger, response and delivery.',
'same_account_type' => 'Both accounts must be of the same account type', 'same_account_type' => 'Both accounts must be of the same account type',
'same_account_currency' => 'Both accounts must have the same currency setting', 'same_account_currency' => 'Both accounts must have the same currency setting',
// Ignore this comment // Ignore this comment
'secure_password' => 'This is not a secure password. Please try again. For more information, visit https://bit.ly/FF3-password', 'secure_password' => 'This is not a secure password. Please try again. For more information, visit https://bit.ly/FF3-password',
'valid_recurrence_rep_type' => 'Invalid repetition type for recurring transactions.', 'valid_recurrence_rep_type' => 'Invalid repetition type for recurring transactions.',
'valid_recurrence_rep_moment' => 'Invalid repetition moment for this type of repetition.', 'valid_recurrence_rep_moment' => 'Invalid repetition moment for this type of repetition.',
'invalid_account_info' => 'Invalid account information.', 'invalid_account_info' => 'Invalid account information.',
'attributes' => [ 'attributes' => [
'email' => 'email address', 'email' => 'email address',
'description' => 'description', 'description' => 'description',
'amount' => 'amount', 'amount' => 'amount',
@@ -216,58 +218,58 @@ return [
], ],
// validation of accounts: // validation of accounts:
'withdrawal_source_need_data' => 'Need to get a valid source account ID and/or valid source account name to continue.', 'withdrawal_source_need_data' => 'Need to get a valid source account ID and/or valid source account name to continue.',
'withdrawal_source_bad_data' => '[a] Could not find a valid source account when searching for ID ":id" or name ":name".', 'withdrawal_source_bad_data' => '[a] Could not find a valid source account when searching for ID ":id" or name ":name".',
'withdrawal_dest_need_data' => '[a] Need to get a valid destination account ID and/or valid destination account name to continue.', 'withdrawal_dest_need_data' => '[a] Need to get a valid destination account ID and/or valid destination account name to continue.',
'withdrawal_dest_bad_data' => 'Could not find a valid destination account when searching for ID ":id" or name ":name".', 'withdrawal_dest_bad_data' => 'Could not find a valid destination account when searching for ID ":id" or name ":name".',
'withdrawal_dest_iban_exists' => 'This destination account IBAN is already in use by an asset account or a liability and cannot be used as a withdrawal destination.', 'withdrawal_dest_iban_exists' => 'This destination account IBAN is already in use by an asset account or a liability and cannot be used as a withdrawal destination.',
'deposit_src_iban_exists' => 'This source account IBAN is already in use by an asset account or a liability and cannot be used as a deposit source.', 'deposit_src_iban_exists' => 'This source account IBAN is already in use by an asset account or a liability and cannot be used as a deposit source.',
'reconciliation_source_bad_data' => 'Could not find a valid reconciliation account when searching for ID ":id" or name ":name".', 'reconciliation_source_bad_data' => 'Could not find a valid reconciliation account when searching for ID ":id" or name ":name".',
'generic_source_bad_data' => '[e] Could not find a valid source account when searching for ID ":id" or name ":name".', 'generic_source_bad_data' => '[e] Could not find a valid source account when searching for ID ":id" or name ":name".',
'deposit_source_need_data' => 'Need to get a valid source account ID and/or valid source account name to continue.', 'deposit_source_need_data' => 'Need to get a valid source account ID and/or valid source account name to continue.',
'deposit_source_bad_data' => '[b] Could not find a valid source account when searching for ID ":id" or name ":name".', 'deposit_source_bad_data' => '[b] Could not find a valid source account when searching for ID ":id" or name ":name".',
'deposit_dest_need_data' => '[b] Need to get a valid destination account ID and/or valid destination account name to continue.', 'deposit_dest_need_data' => '[b] Need to get a valid destination account ID and/or valid destination account name to continue.',
'deposit_dest_bad_data' => 'Could not find a valid destination account when searching for ID ":id" or name ":name".', 'deposit_dest_bad_data' => 'Could not find a valid destination account when searching for ID ":id" or name ":name".',
'deposit_dest_wrong_type' => 'The submitted destination account is not of the right type.', 'deposit_dest_wrong_type' => 'The submitted destination account is not of the right type.',
// Ignore this comment // Ignore this comment
'transfer_source_need_data' => 'Need to get a valid source account ID and/or valid source account name to continue.', 'transfer_source_need_data' => 'Need to get a valid source account ID and/or valid source account name to continue.',
'transfer_source_bad_data' => '[c] Could not find a valid source account when searching for ID ":id" or name ":name".', 'transfer_source_bad_data' => '[c] Could not find a valid source account when searching for ID ":id" or name ":name".',
'transfer_dest_need_data' => '[c] Need to get a valid destination account ID and/or valid destination account name to continue.', 'transfer_dest_need_data' => '[c] Need to get a valid destination account ID and/or valid destination account name to continue.',
'transfer_dest_bad_data' => 'Could not find a valid destination account when searching for ID ":id" or name ":name".', 'transfer_dest_bad_data' => 'Could not find a valid destination account when searching for ID ":id" or name ":name".',
'need_id_in_edit' => 'Each split must have transaction_journal_id (either valid ID or 0).', 'need_id_in_edit' => 'Each split must have transaction_journal_id (either valid ID or 0).',
'ob_source_need_data' => 'Need to get a valid source account ID and/or valid source account name to continue.', 'ob_source_need_data' => 'Need to get a valid source account ID and/or valid source account name to continue.',
'lc_source_need_data' => 'Need to get a valid source account ID to continue.', 'lc_source_need_data' => 'Need to get a valid source account ID to continue.',
'ob_dest_need_data' => '[d] Need to get a valid destination account ID and/or valid destination account name to continue.', 'ob_dest_need_data' => '[d] Need to get a valid destination account ID and/or valid destination account name to continue.',
'ob_dest_bad_data' => 'Could not find a valid destination account when searching for ID ":id" or name ":name".', 'ob_dest_bad_data' => 'Could not find a valid destination account when searching for ID ":id" or name ":name".',
'reconciliation_either_account' => 'To submit a reconciliation, you must submit either a source or a destination account. Not both, not neither.', 'reconciliation_either_account' => 'To submit a reconciliation, you must submit either a source or a destination account. Not both, not neither.',
'generic_invalid_source' => 'You can\'t use this account as the source account.', 'generic_invalid_source' => 'You can\'t use this account as the source account.',
'generic_invalid_destination' => 'You can\'t use this account as the destination account.', 'generic_invalid_destination' => 'You can\'t use this account as the destination account.',
'generic_no_source' => 'You must submit source account information or submit a transaction journal ID.', 'generic_no_source' => 'You must submit source account information or submit a transaction journal ID.',
'generic_no_destination' => 'You must submit destination account information or submit a transaction journal ID.', 'generic_no_destination' => 'You must submit destination account information or submit a transaction journal ID.',
'gte.numeric' => 'The :attribute must be greater than or equal to :value.', 'gte.numeric' => 'The :attribute must be greater than or equal to :value.',
'gt.numeric' => 'The :attribute must be greater than :value.', 'gt.numeric' => 'The :attribute must be greater than :value.',
'gte.file' => 'The :attribute must be greater than or equal to :value kilobytes.', 'gte.file' => 'The :attribute must be greater than or equal to :value kilobytes.',
'gte.string' => 'The :attribute must be greater than or equal to :value characters.', 'gte.string' => 'The :attribute must be greater than or equal to :value characters.',
'gte.array' => 'The :attribute must have :value items or more.', 'gte.array' => 'The :attribute must have :value items or more.',
'amount_required_for_auto_budget' => 'The amount is required.', 'amount_required_for_auto_budget' => 'The amount is required.',
'auto_budget_amount_positive' => 'The amount must be more than zero.', 'auto_budget_amount_positive' => 'The amount must be more than zero.',
'auto_budget_period_mandatory' => 'The auto budget period is a mandatory field.', 'auto_budget_period_mandatory' => 'The auto budget period is a mandatory field.',
// no access to administration: // no access to administration:
'no_access_user_group' => 'You do not have the correct access rights for this administration.', 'no_access_user_group' => 'You do not have the correct access rights for this administration.',
'administration_owner_rename' => 'You can\'t rename your standard administration.', 'administration_owner_rename' => 'You can\'t rename your standard administration.',
]; ];
// Ignore this comment // Ignore this comment