From 79b91e25c21f06d09bd724b14b4042573f1ebf31 Mon Sep 17 00:00:00 2001 From: James Cole Date: Mon, 20 May 2024 06:49:42 +0200 Subject: [PATCH] Expand chart api --- .../Controllers/Chart/AccountController.php | 177 +++++---- app/Api/V2/Request/Chart/ChartRequest.php | 22 +- app/Rules/IsFilterValueIn.php | 58 +++ app/Support/Chart/ChartData.php | 73 ++++ app/Support/Http/Api/ParsesQueryFilters.php | 4 + composer.json | 3 +- composer.lock | 41 +- resources/lang/en_US/validation.php | 366 +++++++++--------- 8 files changed, 455 insertions(+), 289 deletions(-) create mode 100644 app/Rules/IsFilterValueIn.php create mode 100644 app/Support/Chart/ChartData.php diff --git a/app/Api/V2/Controllers/Chart/AccountController.php b/app/Api/V2/Controllers/Chart/AccountController.php index df00351e91d..060cd7d317d 100644 --- a/app/Api/V2/Controllers/Chart/AccountController.php +++ b/app/Api/V2/Controllers/Chart/AccountController.php @@ -24,14 +24,14 @@ 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 @@ public function __construct() 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 @@ function ($request, $next) { } /** - * 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); + + // move date to end of day + $queryParameters['start']->startOfDay(); + $queryParameters['end']->endOfDay(); + + // loop each account, and collect info: + /** @var Account $account */ + foreach ($accounts as $account) { + $this->renderAccountData($queryParameters, $account); + } - /** @var Carbon $end */ - $end = $this->parameters->get('end'); - $end->endOfDay(); + return response()->json($this->chartData->render()); + } - /** @var TransactionCurrency $default */ - $default = app('amount')->getDefaultCurrency(); - $params = $request->getAll(); + /** + * TODO Duplicate function but I think it belongs here or in a separate trait + * + */ + private function getAccountList(array $queryParameters): Collection + { + $collection = new Collection(); - /** @var Collection $accounts */ - $accounts = $params['accounts']; - $chartData = []; + // 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); + } + } - // user's preferences - if (0 === $accounts->count()) { + // 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,66 +116,69 @@ public function dashboard(DashboardChartRequest $request): JsonResponse $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) { - $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, - 'native_currency_code' => $default->code, - 'native_currency_symbol' => $default->symbol, - 'native_currency_decimal_places' => $default->decimal_places, - 'start' => $start->toAtomString(), - 'end' => $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); - - $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; - } + return $collection; + } - return response()->json($this->clean($chartData)); + /** + * @throws FireflyException + */ + private function renderAccountData(array $params, Account $account): void { + $currency = $this->repository->getAccountCurrency($account); + if (null === $currency) { + $currency = $this->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) $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); } } diff --git a/app/Api/V2/Request/Chart/ChartRequest.php b/app/Api/V2/Request/Chart/ChartRequest.php index 6bc67a7cbaf..95f47421aab 100644 --- a/app/Api/V2/Request/Chart/ChartRequest.php +++ b/app/Api/V2/Request/Chart/ChartRequest.php @@ -25,6 +25,7 @@ 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. - + 'start' => $this->dateOrToday($queryParameters, 'start'), + 'end' => $this->dateOrToday($queryParameters, 'end'), + '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 @@ public function rules(): array { 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(), diff --git a/app/Rules/IsFilterValueIn.php b/app/Rules/IsFilterValueIn.php new file mode 100644 index 00000000000..ab7a3ac69ac --- /dev/null +++ b/app/Rules/IsFilterValueIn.php @@ -0,0 +1,58 @@ +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]); + } + +} diff --git a/app/Support/Chart/ChartData.php b/app/Support/Chart/ChartData.php new file mode 100644 index 00000000000..c6130aab857 --- /dev/null +++ b/app/Support/Chart/ChartData.php @@ -0,0 +1,73 @@ +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; + } + +} diff --git a/app/Support/Http/Api/ParsesQueryFilters.php b/app/Support/Http/Api/ParsesQueryFilters.php index 1136e3d9341..2d35c4e5ce5 100644 --- a/app/Support/Http/Api/ParsesQueryFilters.php +++ b/app/Support/Http/Api/ParsesQueryFilters.php @@ -54,4 +54,8 @@ private function integerFromQueryParams(QueryParameters $parameters, string $fie { return (int) ($parameters->page()[$field] ?? $default); } + private function stringFromQueryParams(QueryParameters $parameters, string $field, string $default): string + { + return (string) ($parameters->page()[$field] ?? $default); + } } diff --git a/composer.json b/composer.json index cddfce5d99f..8e721952af0 100644 --- a/composer.json +++ b/composer.json @@ -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", diff --git a/composer.lock b/composer.lock index dbd0e4fc6aa..91057ea157e 100644 --- a/composer.lock +++ b/composer.lock @@ -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", diff --git a/resources/lang/en_US/validation.php b/resources/lang/en_US/validation.php index e23106f780f..3b0f858f127 100644 --- a/resources/lang/en_US/validation.php +++ b/resources/lang/en_US/validation.php @@ -25,159 +25,161 @@ declare(strict_types=1); return [ - '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.', - 'missing_where' => 'Array is missing "where"-clause', - 'missing_update' => 'Array is missing "update"-clause', - 'invalid_where_key' => 'JSON contains an invalid key for the "where"-clause', - 'invalid_update_key' => 'JSON contains an invalid key for the "update"-clause', - 'invalid_query_data' => 'There is invalid data in the %s:%s field of your query.', - 'invalid_query_account_type' => 'Your query contains accounts of different types, which is not allowed.', - 'invalid_query_currency' => 'Your query contains accounts that have different currency settings, which is not allowed.', - 'iban' => 'This is not a valid IBAN.', - 'zero_or_more' => 'The value cannot be negative.', - 'more_than_zero' => 'The value must be more than zero.', - 'more_than_zero_correct' => 'The value must be zero or more.', - 'no_asset_account' => 'This is not an asset account.', - 'date_or_time' => 'The value must be a valid date or time value (ISO 8601).', - 'source_equals_destination' => 'The source account equals the destination account.', - 'unique_account_number_for_user' => 'It looks like this account number is already in use.', - 'unique_iban_for_user' => 'It looks like this IBAN is already in use.', - 'reconciled_forbidden_field' => 'This transaction is already reconciled, you cannot change the ":field"', - 'deleted_user' => 'Due to security constraints, you cannot register using this email address.', - 'rule_trigger_value' => 'This value is invalid for the selected trigger.', - 'rule_action_expression' => 'Invalid expression. :error', - 'rule_action_value' => 'This value is invalid for the selected action.', - 'file_already_attached' => 'Uploaded file ":name" is already attached to this object.', - 'file_attached' => 'Successfully uploaded file ":name".', - 'file_zero' => 'The file is zero bytes in size.', - 'must_exist' => 'The ID in field :attribute does not exist in the database.', - 'all_accounts_equal' => 'All accounts in this field must be equal.', - 'group_title_mandatory' => 'A group title is mandatory when there is more than one transaction.', - 'transaction_types_equal' => 'All splits must be of the same type.', - 'invalid_transaction_type' => 'Invalid transaction type.', - 'invalid_selection' => 'Your selection is invalid.', - 'belongs_user' => 'This value is linked to an object that does not seem to exist.', - 'belongs_user_or_user_group' => 'This value is linked to an object that does not seem to exist in your current financial administration.', - 'no_access_group' => 'The user has no access to this user group.', - 'no_accepted_roles_defined' => 'No access roles have been defined for this endpoint, access denied.', - 'at_least_one_transaction' => 'Need at least one transaction.', - 'recurring_transaction_id' => 'Need at least one transaction.', - 'need_id_to_match' => 'You need to submit this entry with an ID for the API to be able to match it.', - 'too_many_unmatched' => 'Too many submitted transactions cannot be matched to their respective database entries. Make sure existing entries have a valid ID.', - 'id_does_not_match' => 'Submitted ID #:id does not match expected ID. Make sure it matches or omit the field.', - 'at_least_one_repetition' => 'Need at least one repetition.', - 'require_repeat_until' => 'Require either a number of repetitions, or an end date (repeat_until). Not both.', - 'require_currency_info' => 'The content of this field is invalid without currency information.', - 'not_transfer_account' => 'This account is not an account that can be used for transfers.', - 'require_currency_amount' => 'The content of this field is invalid without foreign amount information.', - 'require_foreign_currency' => 'This field requires a number', - 'require_foreign_dest' => 'This field value must match the currency of the destination account.', - 'require_foreign_src' => 'This field value must match the currency of the source account.', - 'equal_description' => 'Transaction description should not equal global description.', - 'file_invalid_mime' => 'File ":name" is of type ":mime" which is not accepted as a new upload.', - 'file_too_large' => 'File ":name" is too large.', - 'belongs_to_user' => 'The value of :attribute is unknown.', - 'accepted' => 'The :attribute must be accepted.', - 'bic' => 'This is not a valid BIC.', - 'at_least_one_trigger' => 'Rule must have at least one trigger.', - 'at_least_one_active_trigger' => 'Rule must have at least one active trigger.', - 'at_least_one_action' => 'Rule must have at least one action.', - 'at_least_one_active_action' => 'Rule must have at least one active action.', - 'base64' => 'This is not valid base64 encoded data.', - 'model_id_invalid' => 'The given ID seems invalid for this model.', - 'less' => ':attribute must be less than 10,000,000', - 'active_url' => 'The :attribute is not a valid URL.', - 'after' => 'The :attribute must be a date after :date.', - 'date_after' => 'The start date must be before the end date.', - 'alpha' => 'The :attribute may only contain letters.', - 'alpha_dash' => 'The :attribute may only contain letters, numbers, and dashes.', - 'alpha_num' => 'The :attribute may only contain letters and numbers.', - 'array' => 'The :attribute must be an array.', - 'unique_for_user' => 'There already is an entry with this :attribute.', - '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.', + '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.', + 'missing_where' => 'Array is missing "where"-clause', + 'missing_update' => 'Array is missing "update"-clause', + 'invalid_where_key' => 'JSON contains an invalid key for the "where"-clause', + 'invalid_update_key' => 'JSON contains an invalid key for the "update"-clause', + 'invalid_query_data' => 'There is invalid data in the %s:%s field of your query.', + 'invalid_query_account_type' => 'Your query contains accounts of different types, which is not allowed.', + 'invalid_query_currency' => 'Your query contains accounts that have different currency settings, which is not allowed.', + 'iban' => 'This is not a valid IBAN.', + 'zero_or_more' => 'The value cannot be negative.', + 'more_than_zero' => 'The value must be more than zero.', + 'more_than_zero_correct' => 'The value must be zero or more.', + 'no_asset_account' => 'This is not an asset account.', + 'date_or_time' => 'The value must be a valid date or time value (ISO 8601).', + 'source_equals_destination' => 'The source account equals the destination account.', + 'unique_account_number_for_user' => 'It looks like this account number is already in use.', + 'unique_iban_for_user' => 'It looks like this IBAN is already in use.', + 'reconciled_forbidden_field' => 'This transaction is already reconciled, you cannot change the ":field"', + 'deleted_user' => 'Due to security constraints, you cannot register using this email address.', + 'rule_trigger_value' => 'This value is invalid for the selected trigger.', + 'rule_action_expression' => 'Invalid expression. :error', + 'rule_action_value' => 'This value is invalid for the selected action.', + 'file_already_attached' => 'Uploaded file ":name" is already attached to this object.', + 'file_attached' => 'Successfully uploaded file ":name".', + 'file_zero' => 'The file is zero bytes in size.', + 'must_exist' => 'The ID in field :attribute does not exist in the database.', + 'all_accounts_equal' => 'All accounts in this field must be equal.', + 'group_title_mandatory' => 'A group title is mandatory when there is more than one transaction.', + 'transaction_types_equal' => 'All splits must be of the same type.', + 'invalid_transaction_type' => 'Invalid transaction type.', + 'invalid_selection' => 'Your selection is invalid.', + 'belongs_user' => 'This value is linked to an object that does not seem to exist.', + 'belongs_user_or_user_group' => 'This value is linked to an object that does not seem to exist in your current financial administration.', + 'no_access_group' => 'The user has no access to this user group.', + 'no_accepted_roles_defined' => 'No access roles have been defined for this endpoint, access denied.', + 'at_least_one_transaction' => 'Need at least one transaction.', + 'recurring_transaction_id' => 'Need at least one transaction.', + 'need_id_to_match' => 'You need to submit this entry with an ID for the API to be able to match it.', + 'too_many_unmatched' => 'Too many submitted transactions cannot be matched to their respective database entries. Make sure existing entries have a valid ID.', + 'id_does_not_match' => 'Submitted ID #:id does not match expected ID. Make sure it matches or omit the field.', + 'at_least_one_repetition' => 'Need at least one repetition.', + 'require_repeat_until' => 'Require either a number of repetitions, or an end date (repeat_until). Not both.', + 'require_currency_info' => 'The content of this field is invalid without currency information.', + 'not_transfer_account' => 'This account is not an account that can be used for transfers.', + 'require_currency_amount' => 'The content of this field is invalid without foreign amount information.', + 'require_foreign_currency' => 'This field requires a number', + 'require_foreign_dest' => 'This field value must match the currency of the destination account.', + 'require_foreign_src' => 'This field value must match the currency of the source account.', + 'equal_description' => 'Transaction description should not equal global description.', + 'file_invalid_mime' => 'File ":name" is of type ":mime" which is not accepted as a new upload.', + 'file_too_large' => 'File ":name" is too large.', + 'belongs_to_user' => 'The value of :attribute is unknown.', + 'accepted' => 'The :attribute must be accepted.', + 'bic' => 'This is not a valid BIC.', + 'at_least_one_trigger' => 'Rule must have at least one trigger.', + 'at_least_one_active_trigger' => 'Rule must have at least one active trigger.', + 'at_least_one_action' => 'Rule must have at least one action.', + 'at_least_one_active_action' => 'Rule must have at least one active action.', + 'base64' => 'This is not valid base64 encoded data.', + 'model_id_invalid' => 'The given ID seems invalid for this model.', + 'less' => ':attribute must be less than 10,000,000', + 'active_url' => 'The :attribute is not a valid URL.', + 'after' => 'The :attribute must be a date after :date.', + 'date_after' => 'The start date must be before the end date.', + 'alpha' => 'The :attribute may only contain letters.', + 'alpha_dash' => 'The :attribute may only contain letters, numbers, and dashes.', + 'alpha_num' => 'The :attribute may only contain letters and numbers.', + 'array' => 'The :attribute must be an array.', + 'unique_for_user' => 'There already is an entry with this :attribute.', + '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 - 'between.numeric' => 'The :attribute must be between :min and :max.', - 'between.file' => 'The :attribute must be between :min and :max kilobytes.', - 'between.string' => 'The :attribute must be between :min and :max characters.', - 'between.array' => 'The :attribute must have between :min and :max items.', - 'boolean' => 'The :attribute field must be true or false.', - 'confirmed' => 'The :attribute confirmation does not match.', - 'date' => 'The :attribute is not a valid date.', - 'date_format' => 'The :attribute does not match the format :format.', - 'different' => 'The :attribute and :other must be different.', - 'digits' => 'The :attribute must be :digits digits.', - 'digits_between' => 'The :attribute must be between :min and :max digits.', - 'email' => 'The :attribute must be a valid email address.', - 'filled' => 'The :attribute field is required.', - 'exists' => 'The selected :attribute is invalid.', - 'image' => 'The :attribute must be an image.', - 'in' => 'The selected :attribute is invalid.', - 'integer' => 'The :attribute must be an integer.', - 'ip' => 'The :attribute must be a valid IP address.', - 'json' => 'The :attribute must be a valid JSON string.', - 'max.numeric' => 'The :attribute may not be greater than :max.', - 'max.file' => 'The :attribute may not be greater than :max kilobytes.', - 'max.string' => 'The :attribute may not be greater than :max characters.', - 'max.array' => 'The :attribute may not have more than :max items.', - 'mimes' => 'The :attribute must be a file of type: :values.', - 'min.numeric' => 'The :attribute must be at least :min.', - 'lte.numeric' => 'The :attribute must be less than or equal :value.', - 'min.file' => 'The :attribute must be at least :min kilobytes.', - 'min.string' => 'The :attribute must be at least :min characters.', - 'min.array' => 'The :attribute must have at least :min items.', - 'not_in' => 'The selected :attribute is invalid.', - 'numeric' => 'The :attribute must be a number.', - 'scientific_notation' => 'The :attribute cannot use the scientific notation.', - 'numeric_native' => 'The native amount must be a number.', - 'numeric_destination' => 'The destination amount must be a number.', - 'numeric_source' => 'The source amount must be a number.', - 'regex' => 'The :attribute format is invalid.', - 'required' => 'The :attribute field is required.', - 'required_if' => 'The :attribute field is required when :other is :value.', - '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_all' => 'The :attribute field is required when :values is 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.', - 'same' => 'The :attribute and :other must match.', - 'size.numeric' => 'The :attribute must be :size.', - 'amount_min_over_max' => 'The minimum amount cannot be larger than the maximum amount.', - 'size.file' => 'The :attribute must be :size kilobytes.', - 'size.string' => 'The :attribute must be :size characters.', - 'size.array' => 'The :attribute must contain :size items.', - 'unique' => 'The :attribute has already been taken.', - 'string' => 'The :attribute must be a string.', - 'url' => 'The :attribute format is invalid.', - 'timezone' => 'The :attribute must be a valid zone.', - '2fa_code' => 'The :attribute field is invalid.', - 'dimensions' => 'The :attribute has invalid image dimensions.', - 'distinct' => 'The :attribute field has a duplicate value.', - 'file' => 'The :attribute must be a file.', - 'in_array' => 'The :attribute field does not exist in :other.', - 'present' => 'The :attribute field must be present.', - 'amount_zero' => 'The total amount cannot be zero.', - '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_object_group' => 'The group name must be unique', - '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_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_currency' => 'Both accounts must have the same currency setting', + 'between.numeric' => 'The :attribute must be between :min and :max.', + 'between.file' => 'The :attribute must be between :min and :max kilobytes.', + 'between.string' => 'The :attribute must be between :min and :max characters.', + 'between.array' => 'The :attribute must have between :min and :max items.', + 'boolean' => 'The :attribute field must be true or false.', + 'confirmed' => 'The :attribute confirmation does not match.', + 'date' => 'The :attribute is not a valid date.', + 'date_format' => 'The :attribute does not match the format :format.', + 'different' => 'The :attribute and :other must be different.', + 'digits' => 'The :attribute must be :digits digits.', + 'digits_between' => 'The :attribute must be between :min and :max digits.', + 'email' => 'The :attribute must be a valid email address.', + 'filled' => 'The :attribute field is required.', + 'exists' => 'The selected :attribute is invalid.', + 'image' => 'The :attribute must be an image.', + 'in' => 'The selected :attribute is invalid.', + 'integer' => 'The :attribute must be an integer.', + 'ip' => 'The :attribute must be a valid IP address.', + 'json' => 'The :attribute must be a valid JSON string.', + 'max.numeric' => 'The :attribute may not be greater than :max.', + 'max.file' => 'The :attribute may not be greater than :max kilobytes.', + 'max.string' => 'The :attribute may not be greater than :max characters.', + 'max.array' => 'The :attribute may not have more than :max items.', + 'mimes' => 'The :attribute must be a file of type: :values.', + 'min.numeric' => 'The :attribute must be at least :min.', + 'lte.numeric' => 'The :attribute must be less than or equal :value.', + 'min.file' => 'The :attribute must be at least :min kilobytes.', + 'min.string' => 'The :attribute must be at least :min characters.', + 'min.array' => 'The :attribute must have at least :min items.', + 'not_in' => 'The selected :attribute is invalid.', + 'numeric' => 'The :attribute must be a number.', + 'scientific_notation' => 'The :attribute cannot use the scientific notation.', + 'numeric_native' => 'The native amount must be a number.', + 'numeric_destination' => 'The destination amount must be a number.', + 'numeric_source' => 'The source amount must be a number.', + 'regex' => 'The :attribute format is invalid.', + 'required' => 'The :attribute field is required.', + 'required_if' => 'The :attribute field is required when :other is :value.', + '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_all' => 'The :attribute field is required when :values is 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.', + 'same' => 'The :attribute and :other must match.', + 'size.numeric' => 'The :attribute must be :size.', + 'amount_min_over_max' => 'The minimum amount cannot be larger than the maximum amount.', + 'size.file' => 'The :attribute must be :size kilobytes.', + 'size.string' => 'The :attribute must be :size characters.', + 'size.array' => 'The :attribute must contain :size items.', + 'unique' => 'The :attribute has already been taken.', + 'string' => 'The :attribute must be a string.', + 'url' => 'The :attribute format is invalid.', + 'timezone' => 'The :attribute must be a valid zone.', + '2fa_code' => 'The :attribute field is invalid.', + 'dimensions' => 'The :attribute has invalid image dimensions.', + 'distinct' => 'The :attribute field has a duplicate value.', + 'file' => 'The :attribute must be a file.', + 'in_array' => 'The :attribute field does not exist in :other.', + 'present' => 'The :attribute field must be present.', + 'amount_zero' => 'The total amount cannot be zero.', + '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_object_group' => 'The group name must be unique', + '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_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_currency' => 'Both accounts must have the same currency setting', // Ignore this comment - '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_moment' => 'Invalid repetition moment for this type of repetition.', - 'invalid_account_info' => 'Invalid account information.', - 'attributes' => [ + '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_moment' => 'Invalid repetition moment for this type of repetition.', + 'invalid_account_info' => 'Invalid account information.', + 'attributes' => [ 'email' => 'email address', 'description' => 'description', 'amount' => 'amount', @@ -216,58 +218,58 @@ ], // 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_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_bad_data' => 'Could not find a valid destination account when searching for ID ":id" or name ":name".', + '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_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_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.', + '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.', - '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_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_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_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_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_wrong_type' => 'The submitted destination account is not of the right type.', // 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_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_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).', + '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_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".', + '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.', - '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_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.', + '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.', + '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".', + '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_destination' => 'You can\'t use this account as the destination 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_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_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.', - 'gte.numeric' => 'The :attribute must be greater than or equal to :value.', - 'gt.numeric' => 'The :attribute must be greater than :value.', - '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.array' => 'The :attribute must have :value items or more.', + 'gte.numeric' => 'The :attribute must be greater than or equal to :value.', + 'gt.numeric' => 'The :attribute must be greater than :value.', + '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.array' => 'The :attribute must have :value items or more.', 'amount_required_for_auto_budget' => 'The amount is required.', '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_user_group' => 'You do not have the correct access rights for this administration.', - 'administration_owner_rename' => 'You can\'t rename your standard 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.', ]; // Ignore this comment