Clean up API endpoints.

This commit is contained in:
James Cole
2025-08-15 07:11:34 +02:00
parent 020c8ad933
commit fc9ef290f1
7 changed files with 119 additions and 180 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);
@@ -120,14 +117,20 @@ class AccountController extends Controller
// 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' => [],
]; ];
@@ -162,91 +165,6 @@ 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();

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

@@ -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;
@@ -75,11 +77,12 @@ class AccountBalanceGrouped
'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,
'period' => $this->preferredRange, 'period' => $this->preferredRange,
'entries' => [], 'entries' => [],
'primary_entries' => [], 'pc_entries' => [],
]; ];
$expense = [ $expense = [
'label' => 'spent', 'label' => 'spent',
@@ -92,8 +95,9 @@ class AccountBalanceGrouped
'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,
'period' => $this->preferredRange, 'period' => $this->preferredRange,
'entries' => [], 'entries' => [],
'pc_entries' => [], 'pc_entries' => [],
@@ -104,15 +108,15 @@ class AccountBalanceGrouped
$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;
@@ -153,7 +157,7 @@ class AccountBalanceGrouped
// 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']);
@@ -162,7 +166,7 @@ class AccountBalanceGrouped
// 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
@@ -284,7 +288,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,26 @@ trait CleansChartData
* @var array $array * @var array $array
*/ */
foreach ($data as $index => $array) { foreach ($data as $index => $array) {
$array = $this->cleanSingleArray($index, $array);
$return[] = $array;
}
return $return;
}
private function cleanSingleArray(mixed $index, array $array): array {
if (array_key_exists('currency_id', $array)) { if (array_key_exists('currency_id', $array)) {
$array['currency_id'] = (string)$array['currency_id']; $array['currency_id'] = (string)$array['currency_id'];
} }
if (array_key_exists('primary_currency_id', $array)) { if (array_key_exists('primary_currency_id', $array)) {
$array['primary_currency_id'] = (string)$array['primary_currency_id']; $array['primary_currency_id'] = (string)$array['primary_currency_id'];
} }
if (!array_key_exists('start', $array)) { $required = ['start_date', 'end_date', 'period', 'yAxisID'];
throw new FireflyException(sprintf('Data-set "%s" is missing the "start"-variable.', $index)); foreach ($required as $field) {
} if (!array_key_exists($field, $array)) {
if (!array_key_exists('end', $array)) { throw new FireflyException(sprintf('Data-set "%s" is missing the "%s"-variable.', $index, $field));
throw new FireflyException(sprintf('Data-set "%s" is missing the "end"-variable.', $index)); }
} }
if (!array_key_exists('period', $array)) { return $array;
throw new FireflyException(sprintf('Data-set "%s" is missing the "period"-variable.', $index));
}
$return[] = $array;
}
return $return;
} }
} }

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

@@ -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']);
// get list of rates
Route::get('rates/{fromCurrencyCode}/{toCurrencyCode}', ['uses' => 'ShowController@show', 'as' => 'show']); Route::get('rates/{fromCurrencyCode}/{toCurrencyCode}', ['uses' => 'ShowController@show', 'as' => 'show']);
Route::get('{userGroupExchangeRate}', ['uses' => 'ShowController@showSingle', 'as' => 'show.single']); // get single rate
Route::get('{userGroupExchangeRate}', ['uses' => 'ShowController@showSingleById', 'as' => 'show.single']);
Route::get('rates/{fromCurrencyCode}/{toCurrencyCode}/{date}', ['uses' => 'ShowController@showSingleByDate', 'as' => 'show.by-date'])->where(['start_date' => DATEFORMAT]);
// delete all rates
Route::delete('rates/{fromCurrencyCode}/{toCurrencyCode}', ['uses' => 'DestroyController@destroy', 'as' => 'destroy']); Route::delete('rates/{fromCurrencyCode}/{toCurrencyCode}', ['uses' => 'DestroyController@destroy', 'as' => 'destroy']);
Route::delete('{userGroupExchangeRate}', ['uses' => 'DestroyController@destroySingle', 'as' => 'destroy.single']); // delete single rate
Route::put('{userGroupExchangeRate}', ['uses' => 'UpdateController@update', 'as' => 'update']); Route::delete('{userGroupExchangeRate}', ['uses' => 'DestroyController@destroySingleById', 'as' => 'destroy.single']);
Route::delete('rates/{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('rates/{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']);
} }
); );