Compare commits

...

6 Commits

Author SHA1 Message Date
James Cole
0e29e282df Fix translations. 2025-08-15 11:38:25 +02:00
James Cole
0b3fd335ad Fix other endpoint. 2025-08-15 11:35:16 +02:00
James Cole
9b2263c7bb Match exchange rate API with API docs. 2025-08-15 11:28:23 +02:00
James Cole
1305fafd38 Add multi-currency for budget chart 2025-08-15 07:50:13 +02:00
James Cole
844b8d48c4 Update category overview to be multi-currency aware. 2025-08-15 07:44:14 +02:00
James Cole
fc9ef290f1 Clean up API endpoints. 2025-08-15 07:11:34 +02:00
21 changed files with 752 additions and 477 deletions

View File

@@ -24,13 +24,11 @@ declare(strict_types=1);
namespace FireflyIII\Api\V1\Controllers\Chart; namespace FireflyIII\Api\V1\Controllers\Chart;
use Carbon\Carbon;
use FireflyIII\Api\V1\Controllers\Controller; use FireflyIII\Api\V1\Controllers\Controller;
use FireflyIII\Api\V1\Requests\Chart\ChartRequest; use FireflyIII\Api\V1\Requests\Chart\ChartRequest;
use FireflyIII\Api\V1\Requests\Data\DateRequest;
use FireflyIII\Enums\AccountTypeEnum; use FireflyIII\Enums\AccountTypeEnum;
use FireflyIII\Enums\UserRoleEnum;
use FireflyIII\Exceptions\FireflyException; use FireflyIII\Exceptions\FireflyException;
use FireflyIII\Exceptions\ValidationException;
use FireflyIII\Models\Account; use FireflyIII\Models\Account;
use FireflyIII\Models\Preference; use FireflyIII\Models\Preference;
use FireflyIII\Models\TransactionCurrency; use FireflyIII\Models\TransactionCurrency;
@@ -40,7 +38,6 @@ use FireflyIII\Support\Facades\Preferences;
use FireflyIII\Support\Facades\Steam; use FireflyIII\Support\Facades\Steam;
use FireflyIII\Support\Http\Api\ApiSupport; use FireflyIII\Support\Http\Api\ApiSupport;
use FireflyIII\Support\Http\Api\CollectsAccountsFromFilter; use FireflyIII\Support\Http\Api\CollectsAccountsFromFilter;
use FireflyIII\User;
use Illuminate\Http\JsonResponse; use Illuminate\Http\JsonResponse;
use Illuminate\Support\Facades\Log; use Illuminate\Support\Facades\Log;
@@ -52,6 +49,8 @@ class AccountController extends Controller
use ApiSupport; use ApiSupport;
use CollectsAccountsFromFilter; use CollectsAccountsFromFilter;
protected array $acceptedRoles = [UserRoleEnum::READ_ONLY];
private ChartData $chartData; private ChartData $chartData;
private AccountRepositoryInterface $repository; private AccountRepositoryInterface $repository;
@@ -63,11 +62,11 @@ class AccountController extends Controller
parent::__construct(); parent::__construct();
$this->middleware( $this->middleware(
function ($request, $next) { function ($request, $next) {
/** @var User $user */
$user = auth()->user();
$this->chartData = new ChartData(); $this->chartData = new ChartData();
$this->repository = app(AccountRepositoryInterface::class); $this->repository = app(AccountRepositoryInterface::class);
$this->repository->setUser($user);
$userGroup = $this->validateUserGroup($request);
$this->repository->setUserGroup($userGroup);
return $next($request); return $next($request);
} }
@@ -75,11 +74,9 @@ class AccountController extends Controller
} }
/** /**
* TODO fix documentation
*
* @throws FireflyException * @throws FireflyException
*/ */
public function dashboard(ChartRequest $request): JsonResponse public function overview(ChartRequest $request): JsonResponse
{ {
$queryParameters = $request->getParameters(); $queryParameters = $request->getParameters();
$accounts = $this->getAccountList($queryParameters); $accounts = $this->getAccountList($queryParameters);
@@ -110,24 +107,30 @@ class AccountController extends Controller
$range = Steam::finalAccountBalanceInRange($account, $params['start'], clone $params['end'], $this->convertToPrimary); $range = Steam::finalAccountBalanceInRange($account, $params['start'], clone $params['end'], $this->convertToPrimary);
$previous = array_values($range)[0]['balance']; $previous = array_values($range)[0]['balance'];
$pcPrevious = null; $pcPrevious = null;
if (!$currency instanceof TransactionCurrency) { if (!$currency instanceof TransactionCurrency) {
$currency = $this->default; $currency = $this->default;
} }
$currentSet = [ $currentSet = [
'label' => $account->name, 'label' => $account->name,
// the currency that belongs to the account. // the currency that belongs to the account.
'currency_id' => (string)$currency->id, 'currency_id' => (string)$currency->id,
'currency_name' => $currency->name,
'currency_code' => $currency->code, 'currency_code' => $currency->code,
'currency_symbol' => $currency->symbol, 'currency_symbol' => $currency->symbol,
'currency_decimal_places' => $currency->decimal_places, 'currency_decimal_places' => $currency->decimal_places,
// the primary currency
'primary_currency_id' => (string)$this->primaryCurrency->id,
// the default currency of the user (could be the same!) // the default currency of the user (could be the same!)
'date' => $params['start']->toAtomString(), 'date' => $params['start']->toAtomString(),
'start' => $params['start']->toAtomString(), 'start_date' => $params['start']->toAtomString(),
'end' => $params['end']->toAtomString(), 'end_date' => $params['end']->toAtomString(),
'type' => 'line',
'yAxisID' => 0,
'period' => '1D', 'period' => '1D',
'entries' => [], 'entries' => [],
]; ];
@@ -150,7 +153,7 @@ class AccountController extends Controller
// do the same for the primary currency balance, if relevant: // do the same for the primary currency balance, if relevant:
$pcBalance = null; $pcBalance = null;
if ($this->convertToPrimary) { if ($this->convertToPrimary) {
$pcBalance = array_key_exists($format, $range) ? $range[$format]['pc_balance'] : $pcPrevious; $pcBalance = array_key_exists($format, $range) ? $range[$format]['pc_balance'] : $pcPrevious;
$pcPrevious = $pcBalance; $pcPrevious = $pcBalance;
@@ -162,97 +165,12 @@ class AccountController extends Controller
$this->chartData->add($currentSet); $this->chartData->add($currentSet);
} }
/**
* This endpoint is documented at:
* https://api-docs.firefly-iii.org/?urls.primaryName=2.0.0%20(v1)#/charts/getChartAccountOverview
*
* @throws ValidationException
*/
public function overview(DateRequest $request): JsonResponse
{
// parameters for chart:
$dates = $request->getAll();
/** @var Carbon $start */
$start = $dates['start'];
/** @var Carbon $end */
$end = $dates['end'];
// set dates to end of day + start of day:
$start->startOfDay();
$end->endOfDay();
$frontPageIds = $this->getFrontPageAccountIds();
$accounts = $this->repository->getAccountsById($frontPageIds);
$chartData = [];
/** @var Account $account */
foreach ($accounts as $account) {
Log::debug(sprintf('Rendering chart data for account %s (%d)', $account->name, $account->id));
$currency = $this->repository->getAccountCurrency($account) ?? $this->primaryCurrency;
$currentStart = clone $start;
$range = Steam::finalAccountBalanceInRange($account, $start, clone $end, $this->convertToPrimary);
$previous = array_values($range)[0]['balance'];
$pcPrevious = null;
$currentSet = [
'label' => $account->name,
'currency_id' => (string)$currency->id,
'currency_code' => $currency->code,
'currency_symbol' => $currency->symbol,
'currency_decimal_places' => $currency->decimal_places,
'start_date' => $start->toAtomString(),
'end_date' => $end->toAtomString(),
'type' => 'line', // line, area or bar
'yAxisID' => 0, // 0, 1, 2
'entries' => [],
];
// add "pc_entries" if convertToPrimary is true:
if ($this->convertToPrimary) {
$currentSet['pc_entries'] = [];
$currentSet['primary_currency_id'] = (string)$this->primaryCurrency->id;
$currentSet['primary_currency_code'] = $this->primaryCurrency->code;
$currentSet['primary_currency_symbol'] = $this->primaryCurrency->symbol;
$currentSet['primary_currency_decimal_places'] = $this->primaryCurrency->decimal_places;
$pcPrevious = array_values($range)[0]['pc_balance'];
}
// also get the primary balance if convertToPrimary is true:
while ($currentStart <= $end) {
$format = $currentStart->format('Y-m-d');
$label = $currentStart->toAtomString();
// balance is based on "balance" from the $range variable.
$balance = array_key_exists($format, $range) ? $range[$format]['balance'] : $previous;
$previous = $balance;
$currentSet['entries'][$label] = $balance;
// do the same for the primary balance, if relevant:
$pcBalance = null;
if ($this->convertToPrimary) {
$pcBalance = array_key_exists($format, $range) ? $range[$format]['pc_balance'] : $pcPrevious;
$pcPrevious = $pcBalance;
$currentSet['pc_entries'][$label] = $pcBalance;
}
$currentStart->addDay();
}
$chartData[] = $currentSet;
}
return response()->json($chartData);
}
private function getFrontPageAccountIds(): array private function getFrontPageAccountIds(): array
{ {
$defaultSet = $this->repository->getAccountsByType([AccountTypeEnum::ASSET->value])->pluck('id')->toArray(); $defaultSet = $this->repository->getAccountsByType([AccountTypeEnum::ASSET->value])->pluck('id')->toArray();
/** @var Preference $frontpage */ /** @var Preference $frontpage */
$frontpage = Preferences::get('frontpageAccounts', $defaultSet); $frontpage = Preferences::get('frontpageAccounts', $defaultSet);
if (!(is_array($frontpage->data) && count($frontpage->data) > 0)) { if (!(is_array($frontpage->data) && count($frontpage->data) > 0)) {
$frontpage->data = $defaultSet; $frontpage->data = $defaultSet;

View File

@@ -7,6 +7,7 @@ namespace FireflyIII\Api\V1\Controllers\Chart;
use FireflyIII\Api\V1\Controllers\Controller; use FireflyIII\Api\V1\Controllers\Controller;
use FireflyIII\Api\V1\Requests\Chart\ChartRequest; use FireflyIII\Api\V1\Requests\Chart\ChartRequest;
use FireflyIII\Enums\TransactionTypeEnum; use FireflyIII\Enums\TransactionTypeEnum;
use FireflyIII\Enums\UserRoleEnum;
use FireflyIII\Exceptions\FireflyException; use FireflyIII\Exceptions\FireflyException;
use FireflyIII\Helpers\Collector\GroupCollectorInterface; use FireflyIII\Helpers\Collector\GroupCollectorInterface;
use FireflyIII\Models\TransactionCurrency; use FireflyIII\Models\TransactionCurrency;
@@ -25,8 +26,9 @@ class BalanceController extends Controller
{ {
use CleansChartData; use CleansChartData;
use CollectsAccountsFromFilter; use CollectsAccountsFromFilter;
protected array $acceptedRoles = [UserRoleEnum::READ_ONLY];
private ChartData $chartData; private array $chartData;
private GroupCollectorInterface $collector; private GroupCollectorInterface $collector;
private AccountRepositoryInterface $repository; private AccountRepositoryInterface $repository;
@@ -42,7 +44,7 @@ class BalanceController extends Controller
$userGroup = $this->validateUserGroup($request); $userGroup = $this->validateUserGroup($request);
$this->repository->setUserGroup($userGroup); $this->repository->setUserGroup($userGroup);
$this->collector->setUserGroup($userGroup); $this->collector->setUserGroup($userGroup);
$this->chartData = new ChartData(); $this->chartData = [];
// $this->default = app('amount')->getPrimaryCurrency(); // $this->default = app('amount')->getPrimaryCurrency();
return $next($request); return $next($request);
@@ -66,10 +68,6 @@ class BalanceController extends Controller
$queryParameters = $request->getParameters(); $queryParameters = $request->getParameters();
$accounts = $this->getAccountList($queryParameters); $accounts = $this->getAccountList($queryParameters);
// prepare for currency conversion and data collection:
/** @var TransactionCurrency $primary */
$primary = Amount::getPrimaryCurrency();
// get journals for entire period: // get journals for entire period:
$this->collector->setRange($queryParameters['start'], $queryParameters['end']) $this->collector->setRange($queryParameters['start'], $queryParameters['end'])
@@ -81,7 +79,7 @@ class BalanceController extends Controller
$object = new AccountBalanceGrouped(); $object = new AccountBalanceGrouped();
$object->setPreferredRange($queryParameters['period']); $object->setPreferredRange($queryParameters['period']);
$object->setPrimary($primary); $object->setPrimary($this->primaryCurrency);
$object->setAccounts($accounts); $object->setAccounts($accounts);
$object->setJournals($journals); $object->setJournals($journals);
$object->setStart($queryParameters['start']); $object->setStart($queryParameters['start']);
@@ -89,9 +87,10 @@ class BalanceController extends Controller
$object->groupByCurrencyAndPeriod(); $object->groupByCurrencyAndPeriod();
$data = $object->convertToChartData(); $data = $object->convertToChartData();
foreach ($data as $entry) { foreach ($data as $entry) {
$this->chartData->add($entry); $this->chartData[] = $entry;
} }
$this->chartData= $this->clean($this->chartData);
return response()->json($this->chartData->render()); return response()->json($this->chartData);
} }
} }

View File

@@ -50,7 +50,7 @@ class BudgetController extends Controller
use CleansChartData; use CleansChartData;
use ValidatesUserGroupTrait; use ValidatesUserGroupTrait;
protected array $acceptedRoles = [UserRoleEnum::READ_ONLY]; protected array $acceptedRoles = [UserRoleEnum::READ_ONLY];
protected OperationsRepositoryInterface $opsRepository; protected OperationsRepositoryInterface $opsRepository;
private BudgetLimitRepositoryInterface $blRepository; private BudgetLimitRepositoryInterface $blRepository;
@@ -81,15 +81,15 @@ class BudgetController extends Controller
* *
* @throws FireflyException * @throws FireflyException
*/ */
public function dashboard(DateRequest $request): JsonResponse public function overview(DateRequest $request): JsonResponse
{ {
$params = $request->getAll(); $params = $request->getAll();
/** @var Carbon $start */ /** @var Carbon $start */
$start = $params['start']; $start = $params['start'];
/** @var Carbon $end */ /** @var Carbon $end */
$end = $params['end']; $end = $params['end'];
// code from FrontpageChartGenerator, but not in separate class // code from FrontpageChartGenerator, but not in separate class
$budgets = $this->repository->getActiveBudgets(); $budgets = $this->repository->getActiveBudgets();
@@ -110,18 +110,27 @@ class BudgetController extends Controller
private function processBudget(Budget $budget, Carbon $start, Carbon $end): array private function processBudget(Budget $budget, Carbon $start, Carbon $end): array
{ {
// get all limits: // get all limits:
$limits = $this->blRepository->getBudgetLimits($budget, $start, $end); $limits = $this->blRepository->getBudgetLimits($budget, $start, $end);
$rows = []; $rows = [];
$spent = $this->opsRepository->listExpenses($start, $end, null, new Collection([$budget])); $spent = $this->opsRepository->listExpenses($start, $end, null, new Collection([$budget]));
$expenses = $this->processExpenses($budget->id, $spent, $start, $end); $expenses = $this->processExpenses($budget->id, $spent, $start, $end);
$converter = new ExchangeRateConverter();
$currencies = [$this->primaryCurrency->id => $this->primaryCurrency,];
/** /**
* @var int $currencyId * @var int $currencyId
* @var array $row * @var array $row
*/ */
foreach ($expenses as $currencyId => $row) { foreach ($expenses as $currencyId => $row) {
// budgeted, left and overspent are now 0. // budgeted, left and overspent are now 0.
$limit = $this->filterLimit($currencyId, $limits); $limit = $this->filterLimit($currencyId, $limits);
// primary currency entries
$row['pc_budgeted'] = '0';
$row['pc_spent'] = '0';
$row['pc_left'] = '0';
$row['pc_overspent'] = '0';
if (null !== $limit) { if (null !== $limit) {
$row['budgeted'] = $limit->amount; $row['budgeted'] = $limit->amount;
$row['left'] = bcsub($row['budgeted'], bcmul($row['spent'], '-1')); $row['left'] = bcsub($row['budgeted'], bcmul($row['spent'], '-1'));
@@ -129,6 +138,21 @@ class BudgetController extends Controller
$row['left'] = 1 === bccomp($row['left'], '0') ? $row['left'] : '0'; $row['left'] = 1 === bccomp($row['left'], '0') ? $row['left'] : '0';
$row['overspent'] = 1 === bccomp($row['overspent'], '0') ? $row['overspent'] : '0'; $row['overspent'] = 1 === bccomp($row['overspent'], '0') ? $row['overspent'] : '0';
} }
// convert data if necessary.
if (true === $this->convertToPrimary && $currencyId !== $this->primaryCurrency->id) {
$currencies[$currencyId] ??= TransactionCurrency::find($currencyId);
$row['pc_budgeted'] = $converter->convert($currencies[$currencyId], $this->primaryCurrency, $start, $row['budgeted']);
$row['pc_spent'] = $converter->convert($currencies[$currencyId], $this->primaryCurrency, $start, $row['spent']);
$row['pc_left'] = $converter->convert($currencies[$currencyId], $this->primaryCurrency, $start, $row['left']);
$row['pc_overspent'] = $converter->convert($currencies[$currencyId], $this->primaryCurrency, $start, $row['overspent']);
}
if (true === $this->convertToPrimary && $currencyId === $this->primaryCurrency->id) {
$row['pc_budgeted'] = $row['budgeted'];
$row['pc_spent'] = $row['spent'];
$row['pc_left'] = $row['left'];
$row['pc_overspent'] = $row['overspent'];
}
$rows[] = $row; $rows[] = $row;
} }
@@ -140,23 +164,39 @@ class BudgetController extends Controller
// } // }
// is always an array // is always an array
$return = []; $return = [];
foreach ($rows as $row) { foreach ($rows as $row) {
$current = [ $current = [
'label' => $budget->name, 'label' => $budget->name,
'currency_id' => (string)$row['currency_id'], 'currency_id' => (string)$row['currency_id'],
'currency_code' => $row['currency_code'],
'currency_name' => $row['currency_name'], 'currency_name' => $row['currency_name'],
'currency_code' => $row['currency_code'],
'currency_decimal_places' => $row['currency_decimal_places'], 'currency_decimal_places' => $row['currency_decimal_places'],
'period' => null,
'start' => $row['start'], 'primary_currency_id' => (string)$this->primaryCurrency->id,
'end' => $row['end'], 'primary_currency_name' => $this->primaryCurrency->name,
'entries' => [ 'primary_currency_code' => $this->primaryCurrency->code,
'primary_currency_symbol' => $this->primaryCurrency->symbol,
'primary_currency_decimal_places' => $this->primaryCurrency->decimal_places,
'period' => null,
'date' => $row['start'],
'start_date' => $row['start'],
'end_date' => $row['end'],
'yAxisID' => 0,
'type' => 'bar',
'entries' => [
'budgeted' => $row['budgeted'], 'budgeted' => $row['budgeted'],
'spent' => $row['spent'], 'spent' => $row['spent'],
'left' => $row['left'], 'left' => $row['left'],
'overspent' => $row['overspent'], 'overspent' => $row['overspent'],
], ],
'pc_entries' => [
'budgeted' => $row['pc_budgeted'],
'spent' => '0',
'left' => '0',
'overspent' => '0',
],
]; ];
$return[] = $current; $return[] = $current;
} }
@@ -191,7 +231,7 @@ class BudgetController extends Controller
* This array contains the expenses in this budget. Grouped per currency. * This array contains the expenses in this budget. Grouped per currency.
* The grouping is on the main currency only. * The grouping is on the main currency only.
* *
* @var int $currencyId * @var int $currencyId
* @var array $block * @var array $block
*/ */
foreach ($spent as $currencyId => $block) { foreach ($spent as $currencyId => $block) {
@@ -209,7 +249,7 @@ class BudgetController extends Controller
'left' => '0', 'left' => '0',
'overspent' => '0', 'overspent' => '0',
]; ];
$currentBudgetArray = $block['budgets'][$budgetId]; $currentBudgetArray = $block['budgets'][$budgetId];
// var_dump($return); // var_dump($return);
/** @var array $journal */ /** @var array $journal */
@@ -250,7 +290,7 @@ class BudgetController extends Controller
private function processLimit(Budget $budget, BudgetLimit $limit): array private function processLimit(Budget $budget, BudgetLimit $limit): array
{ {
Log::debug(sprintf('Created new ExchangeRateConverter in %s', __METHOD__)); Log::debug(sprintf('Created new ExchangeRateConverter in %s', __METHOD__));
$end = clone $limit->end_date; $end = clone $limit->end_date;
$end->endOfDay(); $end->endOfDay();
$spent = $this->opsRepository->listExpenses($limit->start_date, $end, null, new Collection([$budget])); $spent = $this->opsRepository->listExpenses($limit->start_date, $end, null, new Collection([$budget]));
$limitCurrencyId = $limit->transaction_currency_id; $limitCurrencyId = $limit->transaction_currency_id;
@@ -258,8 +298,8 @@ class BudgetController extends Controller
/** @var array $entry */ /** @var array $entry */
// only spent the entry where the entry's currency matches the budget limit's currency // only spent the entry where the entry's currency matches the budget limit's currency
// so $filtered will only have 1 or 0 entries // so $filtered will only have 1 or 0 entries
$filtered = array_filter($spent, fn ($entry) => $entry['currency_id'] === $limitCurrencyId); $filtered = array_filter($spent, fn($entry) => $entry['currency_id'] === $limitCurrencyId);
$result = $this->processExpenses($budget->id, $filtered, $limit->start_date, $end); $result = $this->processExpenses($budget->id, $filtered, $limit->start_date, $end);
if (1 === count($result)) { if (1 === count($result)) {
$compare = bccomp($limit->amount, (string)app('steam')->positive($result[$limitCurrencyId]['spent'])); $compare = bccomp($limit->amount, (string)app('steam')->positive($result[$limitCurrencyId]['spent']));
$result[$limitCurrencyId]['budgeted'] = $limit->amount; $result[$limitCurrencyId]['budgeted'] = $limit->amount;

View File

@@ -34,6 +34,7 @@ use FireflyIII\Exceptions\FireflyException;
use FireflyIII\Helpers\Collector\GroupCollectorInterface; use FireflyIII\Helpers\Collector\GroupCollectorInterface;
use FireflyIII\Repositories\Account\AccountRepositoryInterface; use FireflyIII\Repositories\Account\AccountRepositoryInterface;
use FireflyIII\Repositories\Currency\CurrencyRepositoryInterface; use FireflyIII\Repositories\Currency\CurrencyRepositoryInterface;
use FireflyIII\Support\Facades\Steam;
use FireflyIII\Support\Http\Api\CleansChartData; use FireflyIII\Support\Http\Api\CleansChartData;
use FireflyIII\Support\Http\Api\ExchangeRateConverter; use FireflyIII\Support\Http\Api\ExchangeRateConverter;
use FireflyIII\Support\Http\Api\ValidatesUserGroupTrait; use FireflyIII\Support\Http\Api\ValidatesUserGroupTrait;
@@ -77,10 +78,10 @@ class CategoryController extends Controller
* *
* @SuppressWarnings("PHPMD.UnusedFormalParameter") * @SuppressWarnings("PHPMD.UnusedFormalParameter")
*/ */
public function dashboard(DateRequest $request): JsonResponse public function overview(DateRequest $request): JsonResponse
{ {
/** @var Carbon $start */ /** @var Carbon $start */
$start = $this->parameters->get('start'); $start = $this->parameters->get('start');
/** @var Carbon $end */ /** @var Carbon $end */
$end = $this->parameters->get('end'); $end = $this->parameters->get('end');
@@ -91,11 +92,11 @@ class CategoryController extends Controller
// get journals for entire period: // get journals for entire period:
/** @var GroupCollectorInterface $collector */ /** @var GroupCollectorInterface $collector */
$collector = app(GroupCollectorInterface::class); $collector = app(GroupCollectorInterface::class);
$collector->setRange($start, $end)->withAccountInformation(); $collector->setRange($start, $end)->withAccountInformation();
$collector->setXorAccounts($accounts)->withCategoryInformation(); $collector->setXorAccounts($accounts)->withCategoryInformation();
$collector->setTypes([TransactionTypeEnum::WITHDRAWAL->value, TransactionTypeEnum::RECONCILIATION->value]); $collector->setTypes([TransactionTypeEnum::WITHDRAWAL->value, TransactionTypeEnum::RECONCILIATION->value]);
$journals = $collector->getExtractedJournals(); $journals = $collector->getExtractedJournals();
/** @var array $journal */ /** @var array $journal */
foreach ($journals as $journal) { foreach ($journals as $journal) {
@@ -108,44 +109,62 @@ class CategoryController extends Controller
$currencyCode = (string)$currency->code; $currencyCode = (string)$currency->code;
$currencySymbol = (string)$currency->symbol; $currencySymbol = (string)$currency->symbol;
$currencyDecimalPlaces = (int)$currency->decimal_places; $currencyDecimalPlaces = (int)$currency->decimal_places;
$amount = app('steam')->positive($journal['amount']); $amount = Steam::positive($journal['amount']);
$pcAmount = null;
// overrule if necessary: // overrule if necessary:
if ($this->convertToPrimary && $journalCurrencyId === $this->primaryCurrency->id) {
$pcAmount = $amount;
}
if ($this->convertToPrimary && $journalCurrencyId !== $this->primaryCurrency->id) { if ($this->convertToPrimary && $journalCurrencyId !== $this->primaryCurrency->id) {
$currencyId = (int)$this->primaryCurrency->id; $currencyId = (int)$this->primaryCurrency->id;
$currencyName = (string)$this->primaryCurrency->name; $currencyName = (string)$this->primaryCurrency->name;
$currencyCode = (string)$this->primaryCurrency->code; $currencyCode = (string)$this->primaryCurrency->code;
$currencySymbol = (string)$this->primaryCurrency->symbol; $currencySymbol = (string)$this->primaryCurrency->symbol;
$currencyDecimalPlaces = (int)$this->primaryCurrency->decimal_places; $currencyDecimalPlaces = (int)$this->primaryCurrency->decimal_places;
$convertedAmount = $converter->convert($currency, $this->primaryCurrency, $journal['date'], $amount); $pcAmount = $converter->convert($currency, $this->primaryCurrency, $journal['date'], $amount);
Log::debug(sprintf('Converted %s %s to %s %s', $journal['currency_code'], $amount, $this->primaryCurrency->code, $convertedAmount)); Log::debug(sprintf('Converted %s %s to %s %s', $journal['currency_code'], $amount, $this->primaryCurrency->code, $pcAmount));
$amount = $convertedAmount;
} }
$categoryName = $journal['category_name'] ?? (string)trans('firefly.no_category'); $categoryName = $journal['category_name'] ?? (string)trans('firefly.no_category');
$key = sprintf('%s-%s', $categoryName, $currencyCode); $key = sprintf('%s-%s', $categoryName, $currencyCode);
// create arrays // create arrays
$return[$key] ??= [ $return[$key] ??= [
'label' => $categoryName, 'label' => $categoryName,
'currency_id' => (string)$currencyId, 'currency_id' => (string)$currencyId,
'currency_code' => $currencyCode, 'currency_name' => $currencyName,
'currency_name' => $currencyName, 'currency_code' => $currencyCode,
'currency_symbol' => $currencySymbol, 'currency_symbol' => $currencySymbol,
'currency_decimal_places' => $currencyDecimalPlaces, 'currency_decimal_places' => $currencyDecimalPlaces,
'period' => null, 'primary_currency_id' => (string)$this->primaryCurrency->id,
'start' => $start->toAtomString(), 'primary_currency_name' => (string)$this->primaryCurrency->name,
'end' => $end->toAtomString(), 'primary_currency_code' => (string)$this->primaryCurrency->code,
'amount' => '0', 'primary_currency_symbol' => (string)$this->primaryCurrency->symbol,
'primary_currency_decimal_places' => (int)$this->primaryCurrency->decimal_places,
'period' => null,
'start_date' => $start->toAtomString(),
'end_date' => $end->toAtomString(),
'yAxisID' => 0,
'type' => 'bar',
'entries' => [
'spent' => '0'
],
'pc_entries' => [
'spent' => '0'
],
]; ];
// add monies // add monies
$return[$key]['amount'] = bcadd($return[$key]['amount'], (string)$amount); $return[$key]['entries']['spent'] = bcadd($return[$key]['entries']['spent'], (string)$amount);
if (null !== $pcAmount) {
$return[$key]['pc_entries']['spent'] = bcadd($return[$key]['pc_entries']['spent'], (string)$pcAmount);
}
} }
$return = array_values($return); $return = array_values($return);
// order by amount // order by amount
usort($return, static fn (array $a, array $b) => (float)$a['amount'] < (float)$b['amount'] ? 1 : -1); usort($return, static fn(array $a, array $b) => (float)$a['entries']['spent'] < (float)$b['entries']['spent'] ? 1 : -1);
return response()->json($this->clean($return)); return response()->json($this->clean($return));
} }

View File

@@ -59,23 +59,24 @@ class DestroyController extends Controller
public function destroy(DestroyRequest $request, TransactionCurrency $from, TransactionCurrency $to): JsonResponse public function destroy(DestroyRequest $request, TransactionCurrency $from, TransactionCurrency $to): JsonResponse
{ {
$date = $request->getDate(); $this->repository->deleteRates($from, $to);
if (!$date instanceof Carbon) {
throw new ValidationException('Date is required');
}
$rate = $this->repository->getSpecificRateOnDate($from, $to, $date);
if (!$rate instanceof CurrencyExchangeRate) {
throw new NotFoundHttpException();
}
$this->repository->deleteRate($rate);
return response()->json([], 204); return response()->json([], 204);
} }
public function destroySingle(CurrencyExchangeRate $exchangeRate): JsonResponse public function destroySingleById(CurrencyExchangeRate $exchangeRate): JsonResponse
{ {
$this->repository->deleteRate($exchangeRate); $this->repository->deleteRate($exchangeRate);
return response()->json([], 204);
}
public function destroySingleByDate(TransactionCurrency $from, TransactionCurrency $to, Carbon $date): JsonResponse
{
$exchangeRate = $this->repository->getSpecificRateOnDate($from, $to, $date);
if(null !== $exchangeRate) {
$this->repository->deleteRate($exchangeRate);
}
return response()->json([], 204); return response()->json([], 204);
} }
} }

View File

@@ -24,6 +24,7 @@ declare(strict_types=1);
namespace FireflyIII\Api\V1\Controllers\Models\CurrencyExchangeRate; namespace FireflyIII\Api\V1\Controllers\Models\CurrencyExchangeRate;
use Carbon\Carbon;
use FireflyIII\Api\V1\Controllers\Controller; use FireflyIII\Api\V1\Controllers\Controller;
use FireflyIII\Enums\UserRoleEnum; use FireflyIII\Enums\UserRoleEnum;
use FireflyIII\Models\CurrencyExchangeRate; use FireflyIII\Models\CurrencyExchangeRate;
@@ -33,6 +34,7 @@ use FireflyIII\Support\Http\Api\ValidatesUserGroupTrait;
use FireflyIII\Transformers\ExchangeRateTransformer; use FireflyIII\Transformers\ExchangeRateTransformer;
use Illuminate\Http\JsonResponse; use Illuminate\Http\JsonResponse;
use Illuminate\Pagination\LengthAwarePaginator; use Illuminate\Pagination\LengthAwarePaginator;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
/** /**
* Class ShowController * Class ShowController
@@ -76,7 +78,7 @@ class ShowController extends Controller
; ;
} }
public function showSingle(CurrencyExchangeRate $exchangeRate): JsonResponse public function showSingleById(CurrencyExchangeRate $exchangeRate): JsonResponse
{ {
$transformer = new ExchangeRateTransformer(); $transformer = new ExchangeRateTransformer();
$transformer->setParameters($this->parameters); $transformer->setParameters($this->parameters);
@@ -86,4 +88,20 @@ class ShowController extends Controller
->header('Content-Type', self::CONTENT_TYPE) ->header('Content-Type', self::CONTENT_TYPE)
; ;
} }
public function showSingleByDate(TransactionCurrency $from, TransactionCurrency $to, Carbon $date): JsonResponse
{
$transformer = new ExchangeRateTransformer();
$transformer->setParameters($this->parameters);
$exchangeRate = $this->repository->getSpecificRateOnDate($from, $to, $date);
if(null === $exchangeRate) {
throw new NotFoundHttpException();
}
return response()
->api($this->jsonApiObject(self::RESOURCE_KEY, $exchangeRate, $transformer))
->header('Content-Type', self::CONTENT_TYPE)
;
}
} }

View File

@@ -24,21 +24,27 @@ declare(strict_types=1);
namespace FireflyIII\Api\V1\Controllers\Models\CurrencyExchangeRate; namespace FireflyIII\Api\V1\Controllers\Models\CurrencyExchangeRate;
use Carbon\Carbon;
use FireflyIII\Api\V1\Controllers\Controller;
use FireflyIII\Api\V1\Requests\Models\CurrencyExchangeRate\StoreByCurrenciesRequest;
use FireflyIII\Api\V1\Requests\Models\CurrencyExchangeRate\StoreByDateRequest;
use FireflyIII\Api\V1\Requests\Models\CurrencyExchangeRate\StoreRequest;
use FireflyIII\Enums\UserRoleEnum; use FireflyIII\Enums\UserRoleEnum;
use FireflyIII\Models\CurrencyExchangeRate; use FireflyIII\Models\CurrencyExchangeRate;
use FireflyIII\Api\V1\Requests\Models\CurrencyExchangeRate\StoreRequest; use FireflyIII\Models\TransactionCurrency;
use FireflyIII\Api\V1\Controllers\Controller;
use FireflyIII\Repositories\ExchangeRate\ExchangeRateRepositoryInterface; use FireflyIII\Repositories\ExchangeRate\ExchangeRateRepositoryInterface;
use FireflyIII\Support\Http\Api\ValidatesUserGroupTrait; use FireflyIII\Support\Http\Api\ValidatesUserGroupTrait;
use FireflyIII\Transformers\ExchangeRateTransformer; use FireflyIII\Transformers\ExchangeRateTransformer;
use Illuminate\Http\JsonResponse; use Illuminate\Http\JsonResponse;
use Illuminate\Pagination\LengthAwarePaginator;
use Illuminate\Support\Collection;
class StoreController extends Controller class StoreController extends Controller
{ {
use ValidatesUserGroupTrait; use ValidatesUserGroupTrait;
public const string RESOURCE_KEY = 'exchange-rates'; public const string RESOURCE_KEY = 'exchange-rates';
protected array $acceptedRoles = [UserRoleEnum::OWNER]; protected array $acceptedRoles = [UserRoleEnum::OWNER];
private ExchangeRateRepositoryInterface $repository; private ExchangeRateRepositoryInterface $repository;
public function __construct() public function __construct()
@@ -54,15 +60,76 @@ class StoreController extends Controller
); );
} }
public function storeByCurrencies(StoreByCurrenciesRequest $request, TransactionCurrency $from, TransactionCurrency $to): JsonResponse
{
$data = $request->getAll();
$collection = new Collection();
foreach ($data as $date => $rate) {
$date = Carbon::createFromFormat('Y-m-d', $date);
$existing = $this->repository->getSpecificRateOnDate($from, $to, $date);
if (null !== $existing) {
// update existing rate.
$existing = $this->repository->updateExchangeRate($existing, $rate);
$collection->push($existing);
continue;
}
$new = $this->repository->storeExchangeRate($from, $to, $rate, $date);
$collection->push($new);
}
$count = $collection->count();
$paginator = new LengthAwarePaginator($collection, $count, $count, 1);
$transformer = new ExchangeRateTransformer();
$transformer->setParameters($this->parameters); // give params to transformer
return response()
->json($this->jsonApiList(self::RESOURCE_KEY, $paginator, $transformer))
->header('Content-Type', self::CONTENT_TYPE);
}
public function storeByDate(StoreByDateRequest $request, Carbon $date): JsonResponse
{
$data = $request->getAll();
$from = $request->getFromCurrency();
$collection = new Collection();
foreach ($data['rates'] as $key => $rate) {
$to = TransactionCurrency::where('code', $key)->first();
if (null === $to) {
continue; // should not happen.
}
$existing = $this->repository->getSpecificRateOnDate($from, $to, $date);
if (null !== $existing) {
// update existing rate.
$existing = $this->repository->updateExchangeRate($existing, $rate);
$collection->push($existing);
continue;
}
$new = $this->repository->storeExchangeRate($from, $to, $rate, $date);
$collection->push($new);
}
$count = $collection->count();
$paginator = new LengthAwarePaginator($collection, $count, $count, 1);
$transformer = new ExchangeRateTransformer();
$transformer->setParameters($this->parameters); // give params to transformer
return response()
->json($this->jsonApiList(self::RESOURCE_KEY, $paginator, $transformer))
->header('Content-Type', self::CONTENT_TYPE);
}
public function store(StoreRequest $request): JsonResponse public function store(StoreRequest $request): JsonResponse
{ {
$date = $request->getDate(); $date = $request->getDate();
$rate = $request->getRate(); $rate = $request->getRate();
$from = $request->getFromCurrency(); $from = $request->getFromCurrency();
$to = $request->getToCurrency(); $to = $request->getToCurrency();
// already has rate? // already has rate?
$object = $this->repository->getSpecificRateOnDate($from, $to, $date); $object = $this->repository->getSpecificRateOnDate($from, $to, $date);
if ($object instanceof CurrencyExchangeRate) { if ($object instanceof CurrencyExchangeRate) {
// just update it, no matter. // just update it, no matter.
$rate = $this->repository->updateExchangeRate($object, $rate, $date); $rate = $this->repository->updateExchangeRate($object, $rate, $date);
@@ -77,7 +144,6 @@ class StoreController extends Controller
return response() return response()
->api($this->jsonApiObject(self::RESOURCE_KEY, $rate, $transformer)) ->api($this->jsonApiObject(self::RESOURCE_KEY, $rate, $transformer))
->header('Content-Type', self::CONTENT_TYPE) ->header('Content-Type', self::CONTENT_TYPE);
;
} }
} }

View File

@@ -24,21 +24,24 @@ declare(strict_types=1);
namespace FireflyIII\Api\V1\Controllers\Models\CurrencyExchangeRate; namespace FireflyIII\Api\V1\Controllers\Models\CurrencyExchangeRate;
use FireflyIII\Api\V1\Requests\Models\CurrencyExchangeRate\UpdateRequest; use Carbon\Carbon;
use FireflyIII\Api\V1\Controllers\Controller; use FireflyIII\Api\V1\Controllers\Controller;
use FireflyIII\Api\V1\Requests\Models\CurrencyExchangeRate\UpdateRequest;
use FireflyIII\Enums\UserRoleEnum; use FireflyIII\Enums\UserRoleEnum;
use FireflyIII\Models\CurrencyExchangeRate; use FireflyIII\Models\CurrencyExchangeRate;
use FireflyIII\Models\TransactionCurrency;
use FireflyIII\Repositories\ExchangeRate\ExchangeRateRepositoryInterface; use FireflyIII\Repositories\ExchangeRate\ExchangeRateRepositoryInterface;
use FireflyIII\Support\Http\Api\ValidatesUserGroupTrait; use FireflyIII\Support\Http\Api\ValidatesUserGroupTrait;
use FireflyIII\Transformers\ExchangeRateTransformer; use FireflyIII\Transformers\ExchangeRateTransformer;
use Illuminate\Http\JsonResponse; use Illuminate\Http\JsonResponse;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
class UpdateController extends Controller class UpdateController extends Controller
{ {
use ValidatesUserGroupTrait; use ValidatesUserGroupTrait;
public const string RESOURCE_KEY = 'exchange-rates'; public const string RESOURCE_KEY = 'exchange-rates';
protected array $acceptedRoles = [UserRoleEnum::OWNER]; protected array $acceptedRoles = [UserRoleEnum::OWNER];
private ExchangeRateRepositoryInterface $repository; private ExchangeRateRepositoryInterface $repository;
public function __construct() public function __construct()
@@ -54,7 +57,7 @@ class UpdateController extends Controller
); );
} }
public function update(UpdateRequest $request, CurrencyExchangeRate $exchangeRate): JsonResponse public function updateById(UpdateRequest $request, CurrencyExchangeRate $exchangeRate): JsonResponse
{ {
$date = $request->getDate(); $date = $request->getDate();
$rate = $request->getRate(); $rate = $request->getRate();
@@ -64,7 +67,24 @@ class UpdateController extends Controller
return response() return response()
->api($this->jsonApiObject(self::RESOURCE_KEY, $exchangeRate, $transformer)) ->api($this->jsonApiObject(self::RESOURCE_KEY, $exchangeRate, $transformer))
->header('Content-Type', self::CONTENT_TYPE) ->header('Content-Type', self::CONTENT_TYPE);
; }
public function updateByDate(UpdateRequest $request, TransactionCurrency $from, TransactionCurrency $to, Carbon $date): JsonResponse
{
$exchangeRate = $this->repository->getSpecificRateOnDate($from, $to, $date);
if (null === $exchangeRate) {
throw new NotFoundHttpException();
}
$date = $request->getDate();
$rate = $request->getRate();
$exchangeRate = $this->repository->updateExchangeRate($exchangeRate, $rate, $date);
$transformer = new ExchangeRateTransformer();
$transformer->setParameters($this->parameters);
return response()
->api($this->jsonApiObject(self::RESOURCE_KEY, $exchangeRate, $transformer))
->header('Content-Type', self::CONTENT_TYPE);
} }
} }

View File

@@ -45,7 +45,7 @@ class DestroyRequest extends FormRequest
public function rules(): array public function rules(): array
{ {
return [ return [
'date' => 'required|date|after:1970-01-02|before:2038-01-17', // 'date' => 'required|date|after:1970-01-02|before:2038-01-17',
]; ];
} }
} }

View File

@@ -0,0 +1,73 @@
<?php
/*
* StoreRequest.php
* Copyright (c) 2025 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\Api\V1\Requests\Models\CurrencyExchangeRate;
use Carbon\Carbon;
use Carbon\Exceptions\InvalidFormatException;
use FireflyIII\Support\Request\ChecksLogin;
use FireflyIII\Support\Request\ConvertsDataTypes;
use Illuminate\Contracts\Validation\Validator;
use Illuminate\Foundation\Http\FormRequest;
class StoreByCurrenciesRequest extends FormRequest
{
use ChecksLogin;
use ConvertsDataTypes;
public function getAll(): array
{
return $this->all();
}
/**
* The rules that the incoming request must be matched against.
*/
public function rules(): array
{
return [
'*' => 'required|numeric|min:0.0000000001',
];
}
public function withValidator(Validator $validator): void
{
$validator->after(
static function (Validator $validator): void {
$data = $validator->getData() ?? [];
foreach ($data as $date => $rate) {
try {
$date = Carbon::createFromFormat('Y-m-d', $date);
} catch (InvalidFormatException $e) {
$validator->errors()->add('date', trans('validation.date',['attribute' => 'date']));
return;
}
if (!is_numeric($rate)) {
$validator->errors()->add('rate', trans('validation.number',['attribute' => 'rate']));
return;
}
}
});
}
}

View File

@@ -0,0 +1,87 @@
<?php
/*
* StoreRequest.php
* Copyright (c) 2025 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\Api\V1\Requests\Models\CurrencyExchangeRate;
use FireflyIII\Models\TransactionCurrency;
use FireflyIII\Support\Request\ChecksLogin;
use FireflyIII\Support\Request\ConvertsDataTypes;
use Illuminate\Contracts\Validation\Validator;
use Illuminate\Foundation\Http\FormRequest;
class StoreByDateRequest extends FormRequest
{
use ChecksLogin;
use ConvertsDataTypes;
public function getAll(): array
{
return [
'from' => $this->get('from'),
'rates' => $this->get('rates', []),
];
}
public function getFromCurrency(): TransactionCurrency
{
return TransactionCurrency::where('code', $this->get('from'))->first();
}
/**
* The rules that the incoming request must be matched against.
*/
public function rules(): array
{
return [
'from' => 'required|exists:transaction_currencies,code',
'rates' => 'required|array',
'rates.*' => 'required|numeric|min:0.0000000001',
];
}
public function withValidator(Validator $validator): void
{
$from = $this->getFromCurrency();
$validator->after(
static function (Validator $validator) use ($from): void {
$data = $validator->getData();
$rates = $data['rates'] ?? [];
if (0 === count($rates)) {
$validator->errors()->add('rates', 'No rates given.');
return;
}
foreach ($rates as $key => $entry) {
if ($key === $from->code) {
$validator->errors()->add(sprintf('rates.%s', $key), trans('validation.convert_to_itself', ['code' => $key]));
continue;
}
$to = TransactionCurrency::where('code', $key)->first();
if (null === $to) {
$validator->errors()->add(sprintf('rates.%s', $key), trans('validation.invalid_currency_code', ['code' => $key]));
}
}
});
}
}

View File

@@ -52,6 +52,8 @@ class UpdateRequest extends FormRequest
return [ return [
'date' => 'date|after:1970-01-02|before:2038-01-17', 'date' => 'date|after:1970-01-02|before:2038-01-17',
'rate' => 'required|numeric|gt:0', 'rate' => 'required|numeric|gt:0',
'from' => 'nullable|exists:transaction_currencies,code',
'to' => 'nullable|exists:transaction_currencies,code',
]; ];
} }
} }

View File

@@ -55,20 +55,17 @@ class ExchangeRateRepository implements ExchangeRateRepositoryInterface, UserGro
// orderBy('date', 'DESC')->toRawSql(); // orderBy('date', 'DESC')->toRawSql();
return return
$this->userGroup->currencyExchangeRates() $this->userGroup->currencyExchangeRates()
->where(function (Builder $q1) use ($from, $to): void { ->where(function (Builder $q1) use ($from, $to): void {
$q1->where(function (Builder $q) use ($from, $to): void { $q1->where(function (Builder $q) use ($from, $to): void {
$q->where('from_currency_id', $from->id) $q->where('from_currency_id', $from->id)
->where('to_currency_id', $to->id) ->where('to_currency_id', $to->id);
; })->orWhere(function (Builder $q) use ($from, $to): void {
})->orWhere(function (Builder $q) use ($from, $to): void { $q->where('from_currency_id', $to->id)
$q->where('from_currency_id', $to->id) ->where('to_currency_id', $from->id);
->where('to_currency_id', $from->id) });
; })
}); ->orderBy('date', 'DESC')
}) ->get(['currency_exchange_rates.*']);
->orderBy('date', 'DESC')
->get(['currency_exchange_rates.*'])
;
} }
@@ -78,11 +75,10 @@ class ExchangeRateRepository implements ExchangeRateRepositoryInterface, UserGro
/** @var null|CurrencyExchangeRate */ /** @var null|CurrencyExchangeRate */
return return
$this->userGroup->currencyExchangeRates() $this->userGroup->currencyExchangeRates()
->where('from_currency_id', $from->id) ->where('from_currency_id', $from->id)
->where('to_currency_id', $to->id) ->where('to_currency_id', $to->id)
->where('date', $date->format('Y-m-d')) ->where('date', $date->format('Y-m-d'))
->first() ->first();
;
} }
#[Override] #[Override]
@@ -112,4 +108,12 @@ class ExchangeRateRepository implements ExchangeRateRepositoryInterface, UserGro
return $object; return $object;
} }
public function deleteRates(TransactionCurrency $from, TransactionCurrency $to): void
{
$this->userGroup->currencyExchangeRates()
->where('from_currency_id', $from->id)
->where('to_currency_id', $to->id)
->delete();
}
} }

View File

@@ -46,6 +46,7 @@ use Illuminate\Support\Collection;
interface ExchangeRateRepositoryInterface interface ExchangeRateRepositoryInterface
{ {
public function deleteRate(CurrencyExchangeRate $rate): void; public function deleteRate(CurrencyExchangeRate $rate): void;
public function deleteRates(TransactionCurrency $from, TransactionCurrency $to): void;
public function getAll(): Collection; public function getAll(): Collection;

View File

@@ -26,6 +26,9 @@ namespace FireflyIII\Support\Chart;
use FireflyIII\Exceptions\FireflyException; use FireflyIII\Exceptions\FireflyException;
/**
* @deprecated
*/
class ChartData class ChartData
{ {
private array $series; private array $series;

View File

@@ -28,6 +28,8 @@ use Carbon\Carbon;
use FireflyIII\Enums\TransactionTypeEnum; use FireflyIII\Enums\TransactionTypeEnum;
use FireflyIII\Exceptions\FireflyException; use FireflyIII\Exceptions\FireflyException;
use FireflyIII\Models\TransactionCurrency; use FireflyIII\Models\TransactionCurrency;
use FireflyIII\Support\Facades\Navigation;
use FireflyIII\Support\Facades\Steam;
use Illuminate\Support\Collection; use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Log; use Illuminate\Support\Facades\Log;
@@ -64,36 +66,40 @@ class AccountBalanceGrouped
/** @var array $currency */ /** @var array $currency */
foreach ($this->data as $currency) { foreach ($this->data as $currency) {
// income and expense array prepped: // income and expense array prepped:
$income = [ $income = [
'label' => 'earned', 'label' => 'earned',
'currency_id' => (string) $currency['currency_id'], 'currency_id' => (string)$currency['currency_id'],
'currency_symbol' => $currency['currency_symbol'], 'currency_symbol' => $currency['currency_symbol'],
'currency_code' => $currency['currency_code'], 'currency_code' => $currency['currency_code'],
'currency_decimal_places' => $currency['currency_decimal_places'], 'currency_decimal_places' => $currency['currency_decimal_places'],
'primary_currency_id' => (string) $currency['primary_currency_id'], 'primary_currency_id' => (string)$currency['primary_currency_id'],
'primary_currency_symbol' => $currency['primary_currency_symbol'], 'primary_currency_symbol' => $currency['primary_currency_symbol'],
'primary_currency_code' => $currency['primary_currency_code'], 'primary_currency_code' => $currency['primary_currency_code'],
'primary_currency_decimal_places' => $currency['primary_currency_decimal_places'], 'primary_currency_decimal_places' => $currency['primary_currency_decimal_places'],
'date' => $this->start->toAtomString(), 'date' => $this->start->toAtomString(),
'start' => $this->start->toAtomString(), 'start_date' => $this->start->toAtomString(),
'end' => $this->end->toAtomString(), 'end_date' => $this->end->toAtomString(),
'yAxisID' => 0,
'type' => 'line',
'period' => $this->preferredRange, 'period' => $this->preferredRange,
'entries' => [], 'entries' => [],
'primary_entries' => [], 'pc_entries' => [],
]; ];
$expense = [ $expense = [
'label' => 'spent', 'label' => 'spent',
'currency_id' => (string) $currency['currency_id'], 'currency_id' => (string)$currency['currency_id'],
'currency_symbol' => $currency['currency_symbol'], 'currency_symbol' => $currency['currency_symbol'],
'currency_code' => $currency['currency_code'], 'currency_code' => $currency['currency_code'],
'currency_decimal_places' => $currency['currency_decimal_places'], 'currency_decimal_places' => $currency['currency_decimal_places'],
'primary_currency_id' => (string) $currency['primary_currency_id'], 'primary_currency_id' => (string)$currency['primary_currency_id'],
'primary_currency_symbol' => $currency['primary_currency_symbol'], 'primary_currency_symbol' => $currency['primary_currency_symbol'],
'primary_currency_code' => $currency['primary_currency_code'], 'primary_currency_code' => $currency['primary_currency_code'],
'primary_currency_decimal_places' => $currency['primary_currency_decimal_places'], 'primary_currency_decimal_places' => $currency['primary_currency_decimal_places'],
'date' => $this->start->toAtomString(), 'date' => $this->start->toAtomString(),
'start' => $this->start->toAtomString(), 'start_date' => $this->start->toAtomString(),
'end' => $this->end->toAtomString(), 'end_date' => $this->end->toAtomString(),
'type' => 'line',
'yAxisID' => 0,
'period' => $this->preferredRange, 'period' => $this->preferredRange,
'entries' => [], 'entries' => [],
'pc_entries' => [], 'pc_entries' => [],
@@ -101,22 +107,22 @@ class AccountBalanceGrouped
// loop all possible periods between $start and $end, and add them to the correct dataset. // loop all possible periods between $start and $end, and add them to the correct dataset.
$currentStart = clone $this->start; $currentStart = clone $this->start;
while ($currentStart <= $this->end) { while ($currentStart <= $this->end) {
$key = $currentStart->format($this->carbonFormat); $key = $currentStart->format($this->carbonFormat);
$label = $currentStart->toAtomString(); $label = $currentStart->toAtomString();
// normal entries // normal entries
$income['entries'][$label] = app('steam')->bcround($currency[$key]['earned'] ?? '0', $currency['currency_decimal_places']); $income['entries'][$label] = Steam::bcround($currency[$key]['earned'] ?? '0', $currency['currency_decimal_places']);
$expense['entries'][$label] = app('steam')->bcround($currency[$key]['spent'] ?? '0', $currency['currency_decimal_places']); $expense['entries'][$label] = Steam::bcround($currency[$key]['spent'] ?? '0', $currency['currency_decimal_places']);
// converted entries // converted entries
$income['pc_entries'][$label] = app('steam')->bcround($currency[$key]['pc_earned'] ?? '0', $currency['primary_currency_decimal_places']); $income['pc_entries'][$label] = Steam::bcround($currency[$key]['pc_earned'] ?? '0', $currency['primary_currency_decimal_places']);
$expense['pc_entries'][$label] = app('steam')->bcround($currency[$key]['pc_spent'] ?? '0', $currency['primary_currency_decimal_places']); $expense['pc_entries'][$label] = Steam::bcround($currency[$key]['pc_spent'] ?? '0', $currency['primary_currency_decimal_places']);
// next loop // next loop
$currentStart = app('navigation')->addPeriod($currentStart, $this->preferredRange, 0); $currentStart = Navigation::addPeriod($currentStart, $this->preferredRange, 0);
} }
$chartData[] = $income; $chartData[] = $income;
$chartData[] = $expense; $chartData[] = $expense;
} }
return $chartData; return $chartData;
@@ -142,9 +148,9 @@ class AccountBalanceGrouped
private function processJournal(array $journal): void private function processJournal(array $journal): void
{ {
// format the date according to the period // format the date according to the period
$period = $journal['date']->format($this->carbonFormat); $period = $journal['date']->format($this->carbonFormat);
$currencyId = (int) $journal['currency_id']; $currencyId = (int)$journal['currency_id'];
$currency = $this->findCurrency($currencyId); $currency = $this->findCurrency($currencyId);
// set the array with monetary info, if it does not exist. // set the array with monetary info, if it does not exist.
$this->createDefaultDataEntry($journal); $this->createDefaultDataEntry($journal);
@@ -152,25 +158,25 @@ class AccountBalanceGrouped
$this->createDefaultPeriodEntry($journal); $this->createDefaultPeriodEntry($journal);
// is this journal's amount in- our outgoing? // is this journal's amount in- our outgoing?
$key = $this->getDataKey($journal); $key = $this->getDataKey($journal);
$amount = 'spent' === $key ? app('steam')->negative($journal['amount']) : app('steam')->positive($journal['amount']); $amount = 'spent' === $key ? Steam::negative($journal['amount']) : Steam::positive($journal['amount']);
// get conversion rate // get conversion rate
$rate = $this->getRate($currency, $journal['date']); $rate = $this->getRate($currency, $journal['date']);
$amountConverted = bcmul((string) $amount, $rate); $amountConverted = bcmul((string)$amount, $rate);
// perhaps transaction already has the foreign amount in the primary currency. // perhaps transaction already has the foreign amount in the primary currency.
if ((int) $journal['foreign_currency_id'] === $this->primary->id) { if ((int)$journal['foreign_currency_id'] === $this->primary->id) {
$amountConverted = $journal['foreign_amount'] ?? '0'; $amountConverted = $journal['foreign_amount'] ?? '0';
$amountConverted = 'earned' === $key ? app('steam')->positive($amountConverted) : app('steam')->negative($amountConverted); $amountConverted = 'earned' === $key ? Steam::positive($amountConverted) : Steam::negative($amountConverted);
} }
// add normal entry // add normal entry
$this->data[$currencyId][$period][$key] = bcadd((string) $this->data[$currencyId][$period][$key], (string) $amount); $this->data[$currencyId][$period][$key] = bcadd((string)$this->data[$currencyId][$period][$key], (string)$amount);
// add converted entry // add converted entry
$convertedKey = sprintf('pc_%s', $key); $convertedKey = sprintf('pc_%s', $key);
$this->data[$currencyId][$period][$convertedKey] = bcadd((string) $this->data[$currencyId][$period][$convertedKey], (string) $amountConverted); $this->data[$currencyId][$period][$convertedKey] = bcadd((string)$this->data[$currencyId][$period][$convertedKey], (string)$amountConverted);
} }
private function findCurrency(int $currencyId): TransactionCurrency private function findCurrency(int $currencyId): TransactionCurrency
@@ -185,15 +191,15 @@ class AccountBalanceGrouped
private function createDefaultDataEntry(array $journal): void private function createDefaultDataEntry(array $journal): void
{ {
$currencyId = (int) $journal['currency_id']; $currencyId = (int)$journal['currency_id'];
$this->data[$currencyId] ??= [ $this->data[$currencyId] ??= [
'currency_id' => (string) $currencyId, 'currency_id' => (string)$currencyId,
'currency_symbol' => $journal['currency_symbol'], 'currency_symbol' => $journal['currency_symbol'],
'currency_code' => $journal['currency_code'], 'currency_code' => $journal['currency_code'],
'currency_name' => $journal['currency_name'], 'currency_name' => $journal['currency_name'],
'currency_decimal_places' => $journal['currency_decimal_places'], 'currency_decimal_places' => $journal['currency_decimal_places'],
// primary currency info (could be the same) // primary currency info (could be the same)
'primary_currency_id' => (string) $this->primary->id, 'primary_currency_id' => (string)$this->primary->id,
'primary_currency_code' => $this->primary->code, 'primary_currency_code' => $this->primary->code,
'primary_currency_symbol' => $this->primary->symbol, 'primary_currency_symbol' => $this->primary->symbol,
'primary_currency_decimal_places' => $this->primary->decimal_places, 'primary_currency_decimal_places' => $this->primary->decimal_places,
@@ -202,14 +208,14 @@ class AccountBalanceGrouped
private function createDefaultPeriodEntry(array $journal): void private function createDefaultPeriodEntry(array $journal): void
{ {
$currencyId = (int) $journal['currency_id']; $currencyId = (int)$journal['currency_id'];
$period = $journal['date']->format($this->carbonFormat); $period = $journal['date']->format($this->carbonFormat);
$this->data[$currencyId][$period] ??= [ $this->data[$currencyId][$period] ??= [
'period' => $period, 'period' => $period,
'spent' => '0', 'spent' => '0',
'earned' => '0', 'earned' => '0',
'pc_spent' => '0', 'pc_spent' => '0',
'pc_earned' => '0', 'pc_earned' => '0',
]; ];
} }
@@ -258,12 +264,12 @@ class AccountBalanceGrouped
$primaryCurrencyId = $primary->id; $primaryCurrencyId = $primary->id;
$this->currencies = [$primary->id => $primary]; // currency cache $this->currencies = [$primary->id => $primary]; // currency cache
$this->data[$primaryCurrencyId] = [ $this->data[$primaryCurrencyId] = [
'currency_id' => (string) $primaryCurrencyId, 'currency_id' => (string)$primaryCurrencyId,
'currency_symbol' => $primary->symbol, 'currency_symbol' => $primary->symbol,
'currency_code' => $primary->code, 'currency_code' => $primary->code,
'currency_name' => $primary->name, 'currency_name' => $primary->name,
'currency_decimal_places' => $primary->decimal_places, 'currency_decimal_places' => $primary->decimal_places,
'primary_currency_id' => (string) $primaryCurrencyId, 'primary_currency_id' => (string)$primaryCurrencyId,
'primary_currency_symbol' => $primary->symbol, 'primary_currency_symbol' => $primary->symbol,
'primary_currency_code' => $primary->code, 'primary_currency_code' => $primary->code,
'primary_currency_name' => $primary->name, 'primary_currency_name' => $primary->name,
@@ -284,7 +290,7 @@ class AccountBalanceGrouped
public function setPreferredRange(string $preferredRange): void public function setPreferredRange(string $preferredRange): void
{ {
$this->preferredRange = $preferredRange; $this->preferredRange = $preferredRange;
$this->carbonFormat = app('navigation')->preferredCarbonFormatByPeriod($preferredRange); $this->carbonFormat = Navigation::preferredCarbonFormatByPeriod($preferredRange);
} }
public function setStart(Carbon $start): void public function setStart(Carbon $start): void

View File

@@ -47,24 +47,29 @@ trait CleansChartData
* @var array $array * @var array $array
*/ */
foreach ($data as $index => $array) { foreach ($data as $index => $array) {
if (array_key_exists('currency_id', $array)) { $array = $this->cleanSingleArray($index, $array);
$array['currency_id'] = (string) $array['currency_id'];
}
if (array_key_exists('primary_currency_id', $array)) {
$array['primary_currency_id'] = (string) $array['primary_currency_id'];
}
if (!array_key_exists('start', $array)) {
throw new FireflyException(sprintf('Data-set "%s" is missing the "start"-variable.', $index));
}
if (!array_key_exists('end', $array)) {
throw new FireflyException(sprintf('Data-set "%s" is missing the "end"-variable.', $index));
}
if (!array_key_exists('period', $array)) {
throw new FireflyException(sprintf('Data-set "%s" is missing the "period"-variable.', $index));
}
$return[] = $array; $return[] = $array;
} }
return $return; return $return;
} }
private function cleanSingleArray(mixed $index, array $array): array {
if (array_key_exists('currency_id', $array)) {
$array['currency_id'] = (string)$array['currency_id'];
}
if (array_key_exists('primary_currency_id', $array)) {
$array['primary_currency_id'] = (string)$array['primary_currency_id'];
}
$required = [
'start_date', 'end_date', 'period', 'yAxisID','type','entries','pc_entries',
'currency_id', 'primary_currency_id'
];
foreach ($required as $field) {
if (!array_key_exists($field, $array)) {
throw new FireflyException(sprintf('Data-set "%s" is missing the "%s"-variable.', $index, $field));
}
}
return $array;
}
} }

View File

@@ -67,7 +67,7 @@ trait CollectsAccountsFromFilter
if ('all' === $queryParameters['preselected']) { if ('all' === $queryParameters['preselected']) {
return $this->repository->getAccountsByType([AccountTypeEnum::ASSET->value, AccountTypeEnum::DEFAULT->value, AccountTypeEnum::LOAN->value, AccountTypeEnum::DEBT->value, AccountTypeEnum::MORTGAGE->value]); return $this->repository->getAccountsByType([AccountTypeEnum::ASSET->value, AccountTypeEnum::DEFAULT->value, AccountTypeEnum::LOAN->value, AccountTypeEnum::DEBT->value, AccountTypeEnum::MORTGAGE->value]);
} }
if ('assets' === $queryParameters['preselected']) { if ('assets' === $queryParameters['preselected'] || 'Asset account' === $queryParameters['preselected']) {
return $this->repository->getAccountsByType([AccountTypeEnum::ASSET->value, AccountTypeEnum::DEFAULT->value]); return $this->repository->getAccountsByType([AccountTypeEnum::ASSET->value, AccountTypeEnum::DEFAULT->value]);
} }
if ('liabilities' === $queryParameters['preselected']) { if ('liabilities' === $queryParameters['preselected']) {

View File

@@ -48,11 +48,13 @@ class ExchangeRateTransformer extends AbstractTransformer
'updated_at' => $rate->updated_at->toAtomString(), 'updated_at' => $rate->updated_at->toAtomString(),
'from_currency_id' => (string) $rate->fromCurrency->id, 'from_currency_id' => (string) $rate->fromCurrency->id,
'from_currency_name' => $rate->fromCurrency->name,
'from_currency_code' => $rate->fromCurrency->code, 'from_currency_code' => $rate->fromCurrency->code,
'from_currency_symbol' => $rate->fromCurrency->symbol, 'from_currency_symbol' => $rate->fromCurrency->symbol,
'from_currency_decimal_places' => $rate->fromCurrency->decimal_places, 'from_currency_decimal_places' => $rate->fromCurrency->decimal_places,
'to_currency_id' => (string) $rate->toCurrency->id, 'to_currency_id' => (string) $rate->toCurrency->id,
'to_currency_name' => $rate->toCurrency->name,
'to_currency_code' => $rate->toCurrency->code, 'to_currency_code' => $rate->toCurrency->code,
'to_currency_symbol' => $rate->toCurrency->symbol, 'to_currency_symbol' => $rate->toCurrency->symbol,
'to_currency_decimal_places' => $rate->toCurrency->decimal_places, 'to_currency_decimal_places' => $rate->toCurrency->decimal_places,

View File

@@ -21,174 +21,173 @@
*/ */
declare(strict_types=1); declare(strict_types=1);
return [ return [
'invalid_account_type' => 'A piggy bank can only be linked to asset accounts and liabilities', 'invalid_account_type' => 'A piggy bank can only be linked to asset accounts and liabilities',
'invalid_account_currency' => 'This account does not use the currency you have selected', 'invalid_account_currency' => 'This account does not use the currency you have selected',
'current_amount_too_much' => 'The combined amount in "current_amount" cannot exceed the "target_amount".', 'current_amount_too_much' => 'The combined amount in "current_amount" cannot exceed the "target_amount".',
'filter_must_be_in' => 'Filter ":filter" must be one of: :values', 'filter_must_be_in' => 'Filter ":filter" must be one of: :values',
'filter_not_string' => 'Filter ":filter" is expected to be a string of text', '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_api_filter' => 'This API endpoint does not support ":filter" as a filter.',
'nog_logged_in' => 'You are not logged in.', 'nog_logged_in' => 'You are not logged in.',
'bad_type_source' => 'Firefly III can\'t determine the transaction type based on this source account.', '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.', 'bad_type_destination' => 'Firefly III can\'t determine the transaction type based on this destination account.',
'missing_where' => 'Array is missing "where"-clause', 'missing_where' => 'Array is missing "where"-clause',
'missing_update' => 'Array is missing "update"-clause', 'missing_update' => 'Array is missing "update"-clause',
'invalid_where_key' => 'JSON contains an invalid key for the "where"-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_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_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_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.', 'invalid_query_currency' => 'Your query contains accounts that have different currency settings, which is not allowed.',
'iban' => 'This is not a valid IBAN.', 'iban' => 'This is not a valid IBAN.',
'zero_or_more' => 'The value cannot be negative.', 'zero_or_more' => 'The value cannot be negative.',
'more_than_zero' => 'The value must be more than zero.', 'more_than_zero' => 'The value must be more than zero.',
'more_than_zero_correct' => 'The value must be zero or more.', 'more_than_zero_correct' => 'The value must be zero or more.',
'no_asset_account' => 'This is not an asset account.', 'no_asset_account' => 'This is not an asset account.',
'date_or_time' => 'The value must be a valid date or time value (ISO 8601).', '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.', '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_account_number_for_user' => 'It looks like this account number is already in use.',
'unique_user_group_for_user' => 'It looks like this administration title is already in use.', 'unique_user_group_for_user' => 'It looks like this administration title is already in use.',
'unique_iban_for_user' => 'It looks like this IBAN 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"', '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.', '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_trigger_value' => 'This value is invalid for the selected trigger.',
'rule_action_expression' => 'Invalid expression. :error', 'rule_action_expression' => 'Invalid expression. :error',
'rule_action_value' => 'This value is invalid for the selected action.', 'rule_action_value' => 'This value is invalid for the selected action.',
'file_already_attached' => 'Uploaded file ":name" is already attached to this object.', 'file_already_attached' => 'Uploaded file ":name" is already attached to this object.',
'file_attached' => 'Successfully uploaded file ":name".', 'file_attached' => 'Successfully uploaded file ":name".',
'file_zero' => 'The file is zero bytes in size.', 'file_zero' => 'The file is zero bytes in size.',
'must_exist' => 'The ID in field :attribute does not exist in the database.', 'must_exist' => 'The ID in field :attribute does not exist in the database.',
'all_accounts_equal' => 'All accounts in this field must be equal.', '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.', '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.', 'transaction_types_equal' => 'All splits must be of the same type.',
'invalid_transaction_type' => 'Invalid transaction type.', 'invalid_transaction_type' => 'Invalid transaction type.',
'invalid_selection' => 'Your selection is invalid.', 'invalid_selection' => 'Your selection is invalid.',
'belongs_user' => 'This value is linked to an object that does not seem to exist.', '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.', '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 administration.', 'no_access_group' => 'The user has no access to this administration.',
'no_accepted_roles_defined' => 'No access roles have been defined for this endpoint, access denied.', 'no_accepted_roles_defined' => 'No access roles have been defined for this endpoint, access denied.',
'at_least_one_transaction' => 'Need at least one transaction.', 'at_least_one_transaction' => 'Need at least one transaction.',
'recurring_transaction_id' => '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.', '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.', '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.', '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.', '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_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.', 'require_currency_info' => 'The content of this field is invalid without currency information.',
'require_currency_id_code' => 'Please set either "transaction_currency_id" or "transaction_currency_code".', 'require_currency_id_code' => 'Please set either "transaction_currency_id" or "transaction_currency_code".',
'not_transfer_account' => 'This account is not an account that can be used for transfers.', '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_currency_amount' => 'The content of this field is invalid without foreign amount information.',
'require_foreign_currency' => 'This field requires a number', 'require_foreign_currency' => 'This field requires a number',
'require_foreign_dest' => 'This field value must match the currency of the destination account.', '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.', 'require_foreign_src' => 'This field value must match the currency of the source account.',
'equal_description' => 'Transaction description should not equal global description.', '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_invalid_mime' => 'File ":name" is of type ":mime" which is not accepted as a new upload.',
'file_too_large' => 'File ":name" is too large.', 'file_too_large' => 'File ":name" is too large.',
'belongs_to_user' => 'The value of :attribute is unknown.', 'belongs_to_user' => 'The value of :attribute is unknown.',
'accepted' => 'The :attribute must be accepted.', 'accepted' => 'The :attribute must be accepted.',
'bic' => 'This is not a valid BIC.', 'bic' => 'This is not a valid BIC.',
'at_least_one_trigger' => 'Rule must have at least one trigger.', '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_active_trigger' => 'Rule must have at least one active trigger.',
'at_least_one_action' => 'Rule must have at least one action.', 'at_least_one_action' => 'Rule must have at least one action.',
'at_least_one_active_action' => 'Rule must have at least one active action.', 'at_least_one_active_action' => 'Rule must have at least one active action.',
'base64' => 'This is not valid base64 encoded data.', 'base64' => 'This is not valid base64 encoded data.',
'model_id_invalid' => 'The given ID seems invalid for this model.', 'model_id_invalid' => 'The given ID seems invalid for this model.',
'less' => ':attribute must be less than 10,000,000', 'less' => ':attribute must be less than 10,000,000',
'active_url' => 'The :attribute is not a valid URL.', 'active_url' => 'The :attribute is not a valid URL.',
'after' => 'The :attribute must be a date after :date.', 'after' => 'The :attribute must be a date after :date.',
'date_after' => 'The start date must be before the end date.', 'date_after' => 'The start date must be before the end date.',
'alpha' => 'The :attribute may only contain letters.', 'alpha' => 'The :attribute may only contain letters.',
'alpha_dash' => 'The :attribute may only contain letters, numbers, and dashes.', 'alpha_dash' => 'The :attribute may only contain letters, numbers, and dashes.',
'alpha_num' => 'The :attribute may only contain letters and numbers.', 'alpha_num' => 'The :attribute may only contain letters and numbers.',
'array' => 'The :attribute must be an array.', 'array' => 'The :attribute must be an array.',
'unique_for_user' => 'There already is an entry with this :attribute.', 'unique_for_user' => 'There already is an entry with this :attribute.',
'before' => 'The :attribute must be a date before :date.', 'before' => 'The :attribute must be a date before :date.',
'unique_object_for_user' => 'This name is already in use.', 'unique_object_for_user' => 'This name is already in use.',
'unique_account_for_user' => 'This account name is already in use.', 'unique_account_for_user' => 'This account name is already in use.',
'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.', 'convert_to_itself' => 'Cannot store currency exchange rate for ":code", because from and to currency are the same.',
'scientific_notation' => 'The :attribute cannot use the scientific notation.', 'invalid_currency_code' => 'Currency code ":code" is invalid',
'numeric_primary' => 'The primary currency amount must be a number.', 'scientific_notation' => 'The :attribute cannot use the scientific notation.',
'numeric_destination' => 'The destination amount must be a number.', 'numeric_primary' => 'The primary currency amount must be a number.',
'numeric_source' => 'The source amount must be a number.', 'numeric_destination' => 'The destination amount must be a number.',
'generic_invalid' => 'This value is invalid.', 'numeric_source' => 'The source amount must be a number.',
'transaction_type_changed' => 'If you change the type of the transaction, make sure the correct source/destination accounts are set.', 'generic_invalid' => 'This value is invalid.',
'regex' => 'The :attribute format is invalid.', 'transaction_type_changed' => 'If you change the type of the transaction, make sure the correct source/destination accounts are set.',
'required' => 'The :attribute field is required.', 'regex' => 'The :attribute format is invalid.',
'required_if' => 'The :attribute field is required when :other is :value.', 'required' => 'The :attribute field is required.',
'required_unless' => 'The :attribute field is required unless :other is in :values.', 'required_if' => 'The :attribute field is required when :other is :value.',
'required_with' => 'The :attribute field is required when :values is present.', 'required_unless' => 'The :attribute field is required unless :other is in :values.',
'required_with_all' => 'The :attribute field is required when :values is present.', 'required_with' => 'The :attribute field is required when :values is present.',
'required_without' => 'The :attribute field is required when :values is not present.', 'required_with_all' => 'The :attribute field is required when :values is present.',
'required_without_all' => 'The :attribute field is required when none of :values are present.', 'required_without' => 'The :attribute field is required when :values is not present.',
'same' => 'The :attribute and :other must match.', 'required_without_all' => 'The :attribute field is required when none of :values are present.',
'size.numeric' => 'The :attribute must be :size.', 'same' => 'The :attribute and :other must match.',
'amount_min_over_max' => 'The minimum amount cannot be larger than the maximum amount.', 'size.numeric' => 'The :attribute must be :size.',
'size.file' => 'The :attribute must be :size kilobytes.', 'amount_min_over_max' => 'The minimum amount cannot be larger than the maximum amount.',
'size.string' => 'The :attribute must be :size characters.', 'size.file' => 'The :attribute must be :size kilobytes.',
'size.array' => 'The :attribute must contain :size items.', 'size.string' => 'The :attribute must be :size characters.',
'unique' => 'The :attribute has already been taken.', 'size.array' => 'The :attribute must contain :size items.',
'string' => 'The :attribute must be a string.', 'unique' => 'The :attribute has already been taken.',
'url' => 'The :attribute format is invalid.', 'string' => 'The :attribute must be a string.',
'timezone' => 'The :attribute must be a valid zone.', 'url' => 'The :attribute format is invalid.',
'2fa_code' => 'The :attribute field is invalid.', 'timezone' => 'The :attribute must be a valid zone.',
'dimensions' => 'The :attribute has invalid image dimensions.', '2fa_code' => 'The :attribute field is invalid.',
'distinct' => 'The :attribute field has a duplicate value.', 'dimensions' => 'The :attribute has invalid image dimensions.',
'file' => 'The :attribute must be a file.', 'distinct' => 'The :attribute field has a duplicate value.',
'in_array' => 'The :attribute field does not exist in :other.', 'file' => 'The :attribute must be a file.',
'present' => 'The :attribute field must be present.', 'in_array' => 'The :attribute field does not exist in :other.',
'amount_zero' => 'The total amount cannot be zero.', 'present' => 'The :attribute field must be present.',
'current_target_amount' => 'The current amount must be less than the target amount.', 'amount_zero' => 'The total amount cannot be zero.',
'unique_piggy_bank_for_user' => 'The name of the piggy bank must be unique.', 'current_target_amount' => 'The current amount must be less than the target amount.',
'unique_object_group' => 'The group name must be unique', 'unique_piggy_bank_for_user' => 'The name of the piggy bank must be unique.',
'starts_with' => 'The value must start with :values.', 'unique_object_group' => 'The group name must be unique',
'unique_webhook' => 'You already have a webhook with this combination of URL, trigger, response and delivery.', 'starts_with' => 'The value must start with :values.',
'unique_existing_webhook' => 'You already have another 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.',
'same_account_type' => 'Both accounts must be of the same account type', 'unique_existing_webhook' => 'You already have another webhook with this combination of URL, trigger, response and delivery.',
'same_account_currency' => 'Both accounts must have the same currency setting', 'same_account_type' => 'Both accounts must be of the same account type',
'piggy_no_change_currency' => 'Because there are piggy banks linked to this account, you cannot change the currency of the account.', 'same_account_currency' => 'Both accounts must have the same currency setting',
'piggy_no_change_currency' => 'Because there are piggy banks linked to this account, you cannot change the currency of the account.',
'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',
@@ -227,59 +226,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.',
'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.', 'ob_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".', 'lc_source_need_data' => 'Need to get a valid source account ID to continue.',
'transfer_dest_need_data' => '[c] 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.',
'transfer_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".',
'need_id_in_edit' => 'Each split must have transaction_journal_id (either valid ID or 0).', '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.', 'generic_invalid_source' => 'You can\'t use this account as the source account.',
'lc_source_need_data' => 'Need to get a valid source account ID to continue.', 'generic_invalid_destination' => 'You can\'t use this account as the destination account.',
'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_no_source' => 'You must submit source account information or submit a transaction journal ID.',
'generic_invalid_destination' => 'You can\'t use this account as the destination account.', '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.', 'gte.numeric' => 'The :attribute must be greater than or equal to :value.',
'generic_no_destination' => 'You must submit destination account information or submit a transaction journal ID.', '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.',
'missing_with' => 'The :attribute cannot be combined with another field.',
'gte.numeric' => 'The :attribute must be greater than or equal to :value.', 'amount_required_for_auto_budget' => 'The amount is required.',
'gt.numeric' => 'The :attribute must be greater than :value.', 'auto_budget_amount_positive' => 'The amount must be more than zero.',
'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.',
'missing_with' => 'The :attribute cannot be combined with another field.',
'amount_required_for_auto_budget' => 'The amount is required.', 'auto_budget_period_mandatory' => 'The auto budget period is a mandatory field.',
'auto_budget_amount_positive' => 'The amount must be more than zero.',
'auto_budget_period_mandatory' => 'The auto budget period is a mandatory field.',
// no access to administration: // no access to administration:
'no_auth_user_group' => 'You have to be logged in to access this administration.', 'no_auth_user_group' => 'You have to be logged in to access this 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.',
'existing_mfa_code' => 'Please enter a valid code', 'existing_mfa_code' => 'Please enter a valid code',
]; ];

View File

@@ -33,6 +33,10 @@ use Illuminate\Support\Facades\Route;
* \__/ |_| | _| `._____| \______/ \______/ |__| |_______|_______/ * \__/ |_| | _| `._____| \______/ \______/ |__| |_______|_______/
*/ */
if (!defined('DATEFORMAT')) {
define('DATEFORMAT', '(19|20)[0-9]{2}-?[0-9]{2}-?[0-9]{2}');
}
// Autocomplete controllers // Autocomplete controllers
Route::group( Route::group(
[ [
@@ -69,24 +73,34 @@ Route::group(
'as' => 'api.v1.exchange-rates.', 'as' => 'api.v1.exchange-rates.',
], ],
static function (): void { static function (): void {
// get all
Route::get('', ['uses' => 'IndexController@index', 'as' => 'index']); Route::get('', ['uses' => 'IndexController@index', 'as' => 'index']);
Route::get('rates/{fromCurrencyCode}/{toCurrencyCode}', ['uses' => 'ShowController@show', 'as' => 'show']); // get list of rates
Route::get('{userGroupExchangeRate}', ['uses' => 'ShowController@showSingle', 'as' => 'show.single']); Route::get('{userGroupExchangeRate}', ['uses' => 'ShowController@showSingleById', 'as' => 'show.single']);
Route::delete('rates/{fromCurrencyCode}/{toCurrencyCode}', ['uses' => 'DestroyController@destroy', 'as' => 'destroy']); Route::get('{fromCurrencyCode}/{toCurrencyCode}', ['uses' => 'ShowController@show', 'as' => 'show']);
Route::delete('{userGroupExchangeRate}', ['uses' => 'DestroyController@destroySingle', 'as' => 'destroy.single']); Route::get('{fromCurrencyCode}/{toCurrencyCode}/{date}', ['uses' => 'ShowController@showSingleByDate', 'as' => 'show.by-date'])->where(['start_date' => DATEFORMAT]);
Route::put('{userGroupExchangeRate}', ['uses' => 'UpdateController@update', 'as' => 'update']);
// delete all rates
Route::delete('{fromCurrencyCode}/{toCurrencyCode}', ['uses' => 'DestroyController@destroy', 'as' => 'destroy']);
// delete single rate
Route::delete('{userGroupExchangeRate}', ['uses' => 'DestroyController@destroySingleById', 'as' => 'destroy.single']);
Route::delete('{fromCurrencyCode}/{toCurrencyCode}/{date}', ['uses' => 'DestroyController@destroySingleByDate', 'as' => 'destroy.by-date'])->where(['start_date' => DATEFORMAT]);
// update single
Route::put('{userGroupExchangeRate}', ['uses' => 'UpdateController@updateById', 'as' => 'update']);
Route::put('{fromCurrencyCode}/{toCurrencyCode}/{date}', ['uses' => 'UpdateController@updateByDate', 'as' => 'update.by-date'])->where(['start_date' => DATEFORMAT]);
// post new rate
Route::post('', ['uses' => 'StoreController@store', 'as' => 'store']); Route::post('', ['uses' => 'StoreController@store', 'as' => 'store']);
Route::post('by-date/{date}', ['uses' => 'StoreController@storeByDate', 'as' => 'store.by-date'])->where(['start_date' => DATEFORMAT]);
Route::post('by-currencies/{fromCurrencyCode}/{toCurrencyCode}', ['uses' => 'StoreController@storeByCurrencies', 'as' => 'store.by-currencies']);
} }
); );
// CHART ROUTES.
// chart balance
// CHART ROUTES // CHART ROUTES
Route::group( Route::group(
[ [
'namespace' => 'FireflyIII\Api\V2\Controllers\Chart', 'namespace' => 'FireflyIII\Api\V1\Controllers\Chart',
'prefix' => 'v1/chart/balance', 'prefix' => 'v1/chart/balance',
'as' => 'api.v1.chart.balance', 'as' => 'api.v1.chart.balance',
], ],
@@ -104,7 +118,6 @@ Route::group(
], ],
static function (): void { static function (): void {
Route::get('overview', ['uses' => 'AccountController@overview', 'as' => 'overview']); Route::get('overview', ['uses' => 'AccountController@overview', 'as' => 'overview']);
Route::get('dashboard', ['uses' => 'AccountController@dashboard', 'as' => 'dashboard']);
} }
); );
@@ -115,7 +128,7 @@ Route::group(
'as' => 'api.v1.chart.budget.', 'as' => 'api.v1.chart.budget.',
], ],
static function (): void { static function (): void {
Route::get('dashboard', ['uses' => 'BudgetController@dashboard', 'as' => 'dashboard']); Route::get('overview', ['uses' => 'BudgetController@overview', 'as' => 'overview']);
} }
); );
@@ -126,7 +139,7 @@ Route::group(
'as' => 'api.v1.chart.category.', 'as' => 'api.v1.chart.category.',
], ],
static function (): void { static function (): void {
Route::get('dashboard', ['uses' => 'CategoryController@dashboard', 'as' => 'dashboard']); Route::get('overview', ['uses' => 'CategoryController@overview', 'as' => 'overview']);
} }
); );