mirror of
https://github.com/firefly-iii/firefly-iii.git
synced 2025-10-12 15:35:15 +00:00
Update API endpoints and account autocomplete.
This commit is contained in:
@@ -24,14 +24,16 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace FireflyIII\Api\V2\Controllers\Autocomplete;
|
namespace FireflyIII\Api\V2\Controllers\Autocomplete;
|
||||||
|
|
||||||
|
use Carbon\Carbon;
|
||||||
use FireflyIII\Api\V2\Controllers\Controller;
|
use FireflyIII\Api\V2\Controllers\Controller;
|
||||||
use FireflyIII\Api\V2\Request\Autocomplete\AutocompleteRequest;
|
use FireflyIII\Api\V2\Request\Autocomplete\AutocompleteRequest;
|
||||||
use FireflyIII\Exceptions\FireflyException;
|
use FireflyIII\Exceptions\FireflyException;
|
||||||
use FireflyIII\Models\Account;
|
use FireflyIII\Models\Account;
|
||||||
|
use FireflyIII\Models\AccountBalance;
|
||||||
use FireflyIII\Models\AccountType;
|
use FireflyIII\Models\AccountType;
|
||||||
use FireflyIII\Repositories\Account\AccountRepositoryInterface;
|
use FireflyIII\Models\TransactionCurrency;
|
||||||
use FireflyIII\Repositories\UserGroups\Account\AccountRepositoryInterface as AdminAccountRepositoryInterface;
|
use FireflyIII\Repositories\UserGroups\Account\AccountRepositoryInterface as AdminAccountRepositoryInterface;
|
||||||
use FireflyIII\Support\Http\Api\AccountFilter;
|
use FireflyIII\Support\Http\Api\ExchangeRateConverter;
|
||||||
use Illuminate\Http\JsonResponse;
|
use Illuminate\Http\JsonResponse;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -39,11 +41,14 @@ use Illuminate\Http\JsonResponse;
|
|||||||
*/
|
*/
|
||||||
class AccountController extends Controller
|
class AccountController extends Controller
|
||||||
{
|
{
|
||||||
use AccountFilter;
|
|
||||||
|
|
||||||
|
// use AccountFilter;
|
||||||
private AdminAccountRepositoryInterface $adminRepository;
|
private AdminAccountRepositoryInterface $adminRepository;
|
||||||
private array $balanceTypes;
|
private TransactionCurrency $default;
|
||||||
private AccountRepositoryInterface $repository;
|
private ExchangeRateConverter $converter;
|
||||||
|
|
||||||
|
// private array $balanceTypes;
|
||||||
|
// private AccountRepositoryInterface $repository;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* AccountController constructor.
|
* AccountController constructor.
|
||||||
@@ -53,14 +58,20 @@ class AccountController extends Controller
|
|||||||
parent::__construct();
|
parent::__construct();
|
||||||
$this->middleware(
|
$this->middleware(
|
||||||
function ($request, $next) {
|
function ($request, $next) {
|
||||||
$this->repository = app(AccountRepositoryInterface::class);
|
// new way of user group validation
|
||||||
|
$userGroup = $this->validateUserGroup($request);
|
||||||
$this->adminRepository = app(AdminAccountRepositoryInterface::class);
|
$this->adminRepository = app(AdminAccountRepositoryInterface::class);
|
||||||
$this->adminRepository->setUserGroup($this->validateUserGroup($request));
|
$this->adminRepository->setUserGroup($userGroup);
|
||||||
|
$this->default = app('amount')->getDefaultCurrency();
|
||||||
|
$this->converter = app(ExchangeRateConverter::class);
|
||||||
|
|
||||||
|
// $this->repository = app(AccountRepositoryInterface::class);
|
||||||
|
// $this->adminRepository->setUserGroup($this->validateUserGroup($request));
|
||||||
|
|
||||||
return $next($request);
|
return $next($request);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
$this->balanceTypes = [AccountType::ASSET, AccountType::LOAN, AccountType::DEBT, AccountType::MORTGAGE];
|
// $this->balanceTypes = [AccountType::ASSET, AccountType::LOAN, AccountType::DEBT, AccountType::MORTGAGE];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -73,59 +84,66 @@ class AccountController extends Controller
|
|||||||
* 5. Collector uses user_group_id
|
* 5. Collector uses user_group_id
|
||||||
*
|
*
|
||||||
* @throws FireflyException
|
* @throws FireflyException
|
||||||
* @throws FireflyException
|
|
||||||
*/
|
*/
|
||||||
public function accounts(AutocompleteRequest $request): JsonResponse
|
public function accounts(AutocompleteRequest $request): JsonResponse
|
||||||
{
|
{
|
||||||
$data = $request->getData();
|
$queryParameters = $request->getParameters();
|
||||||
$types = $data['types'];
|
$result = $this->adminRepository->searchAccount((string) $queryParameters['query'], $queryParameters['account_types'], $queryParameters['size']);
|
||||||
$query = $data['query'];
|
$return = [];
|
||||||
$date = $this->parameters->get('date') ?? today(config('app.timezone'));
|
|
||||||
$result = $this->adminRepository->searchAccount((string) $query, $types, $data['limit']);
|
|
||||||
$defaultCurrency = app('amount')->getDefaultCurrency();
|
|
||||||
$groupedResult = [];
|
|
||||||
$allItems = [];
|
|
||||||
|
|
||||||
/** @var Account $account */
|
/** @var Account $account */
|
||||||
foreach ($result as $account) {
|
foreach ($result as $account) {
|
||||||
$nameWithBalance = $account->name;
|
$return[] = $this->parseAccount($account);
|
||||||
$currency = $this->repository->getAccountCurrency($account) ?? $defaultCurrency;
|
|
||||||
|
|
||||||
if (in_array($account->accountType->type, $this->balanceTypes, true)) {
|
|
||||||
$balance = app('steam')->balance($account, $date);
|
|
||||||
$nameWithBalance = sprintf('%s (%s)', $account->name, app('amount')->formatAnything($currency, $balance, false));
|
|
||||||
}
|
|
||||||
$type = (string) trans(sprintf('firefly.%s', $account->accountType->type));
|
|
||||||
$groupedResult[$type] ??= [
|
|
||||||
'group ' => $type,
|
|
||||||
'items' => [],
|
|
||||||
];
|
|
||||||
$allItems[] = [
|
|
||||||
'id' => (string) $account->id,
|
|
||||||
'value' => (string) $account->id,
|
|
||||||
'name' => $account->name,
|
|
||||||
'name_with_balance' => $nameWithBalance,
|
|
||||||
'label' => $nameWithBalance,
|
|
||||||
'type' => $account->accountType->type,
|
|
||||||
'currency_id' => (string) $currency->id,
|
|
||||||
'currency_name' => $currency->name,
|
|
||||||
'currency_code' => $currency->code,
|
|
||||||
'currency_symbol' => $currency->symbol,
|
|
||||||
'currency_decimal_places' => $currency->decimal_places,
|
|
||||||
];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
usort(
|
return response()->json($return);
|
||||||
$allItems,
|
}
|
||||||
static function (array $left, array $right): int {
|
|
||||||
$order = [AccountType::ASSET, AccountType::REVENUE, AccountType::EXPENSE];
|
|
||||||
$posLeft = (int) array_search($left['type'], $order, true);
|
|
||||||
$posRight = (int) array_search($right['type'], $order, true);
|
|
||||||
|
|
||||||
return $posLeft - $posRight;
|
private function parseAccount(Account $account): array
|
||||||
}
|
{
|
||||||
);
|
return [
|
||||||
|
'id' => (string) $account->id,
|
||||||
|
'title' => $account->name,
|
||||||
|
'meta' => [
|
||||||
|
'type' => $account->accountType->type,
|
||||||
|
'account_balances' => $this->getAccountBalances($account),
|
||||||
|
],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
private function getAccountBalances(Account $account): array
|
||||||
|
{
|
||||||
|
$return = [];
|
||||||
|
$balances = $this->adminRepository->getAccountBalances($account);
|
||||||
|
/** @var AccountBalance $balance */
|
||||||
|
foreach ($balances as $balance) {
|
||||||
|
$return[] = $this->parseAccountBalance($balance);
|
||||||
|
}
|
||||||
|
return $return;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param AccountBalance $balance
|
||||||
|
*
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
private function parseAccountBalance(AccountBalance $balance): array
|
||||||
|
{
|
||||||
|
$currency = $balance->transactionCurrency;
|
||||||
|
return [
|
||||||
|
'title' => $balance->title,
|
||||||
|
'native_amount' => $this->converter->convert($currency, $this->default, today(), $balance->balance),
|
||||||
|
'amount' => app('steam')->bcround($balance->balance, $currency->decimal_places),
|
||||||
|
'currency_id' => (string) $currency->id,
|
||||||
|
'currency_code' => $currency->code,
|
||||||
|
'currency_symbol' => $currency->symbol,
|
||||||
|
'currency_decimal_places' => $currency->decimal_places,
|
||||||
|
'native_currency_id' => (string) $this->default->id,
|
||||||
|
'native_currency_code' => $this->default->code,
|
||||||
|
'native_currency_symbol' => $this->default->symbol,
|
||||||
|
'native_currency_decimal' => $this->default->decimal_places,
|
||||||
|
|
||||||
|
];
|
||||||
|
|
||||||
return response()->json($allItems);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -23,11 +23,16 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace FireflyIII\Api\V2\Request\Autocomplete;
|
namespace FireflyIII\Api\V2\Request\Autocomplete;
|
||||||
|
|
||||||
use FireflyIII\Enums\UserRoleEnum;
|
use Carbon\Carbon;
|
||||||
|
use FireflyIII\JsonApi\Rules\IsValidFilter;
|
||||||
|
use FireflyIII\JsonApi\Rules\IsValidPage;
|
||||||
use FireflyIII\Models\AccountType;
|
use FireflyIII\Models\AccountType;
|
||||||
|
use FireflyIII\Support\Http\Api\AccountFilter;
|
||||||
use FireflyIII\Support\Request\ChecksLogin;
|
use FireflyIII\Support\Request\ChecksLogin;
|
||||||
use FireflyIII\Support\Request\ConvertsDataTypes;
|
use FireflyIII\Support\Request\ConvertsDataTypes;
|
||||||
use Illuminate\Foundation\Http\FormRequest;
|
use Illuminate\Foundation\Http\FormRequest;
|
||||||
|
use LaravelJsonApi\Core\Query\QueryParameters;
|
||||||
|
use LaravelJsonApi\Validation\Rule as JsonApiRule;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Class AutocompleteRequest
|
* Class AutocompleteRequest
|
||||||
@@ -36,11 +41,39 @@ class AutocompleteRequest extends FormRequest
|
|||||||
{
|
{
|
||||||
use ChecksLogin;
|
use ChecksLogin;
|
||||||
use ConvertsDataTypes;
|
use ConvertsDataTypes;
|
||||||
|
use AccountFilter;
|
||||||
|
|
||||||
protected array $acceptedRoles = [UserRoleEnum::MANAGE_TRANSACTIONS];
|
/**
|
||||||
|
* Loops over all possible query parameters (these are shared over ALL auto complete requests)
|
||||||
|
* and returns a validated array of parameters.
|
||||||
|
*
|
||||||
|
* The advantage is a single class. But you may also submit "account types" to an endpoint that doesn't use these.
|
||||||
|
*
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public function getParameters(): array
|
||||||
|
{
|
||||||
|
$queryParameters = QueryParameters::cast($this->all());
|
||||||
|
$date = Carbon::createFromFormat('Y-m-d', $queryParameters->filter()->value('date', date('Y-m-d')), config('app.timezone'));
|
||||||
|
$query = $queryParameters->filter()->value('query', '');
|
||||||
|
$size = (int) ($queryParameters->page()['size'] ?? 50);
|
||||||
|
$accountTypes = $this->getAccountTypeParameter($queryParameters->filter()->value('account_types', ''));
|
||||||
|
|
||||||
|
|
||||||
|
return [
|
||||||
|
'date' => $date,
|
||||||
|
'query' => $query,
|
||||||
|
'size' => $size,
|
||||||
|
'account_types' => $accountTypes,
|
||||||
|
];
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
public function getData(): array
|
public function getData(): array
|
||||||
{
|
{
|
||||||
|
|
||||||
|
|
||||||
|
return [];
|
||||||
$types = $this->convertString('types');
|
$types = $this->convertString('types');
|
||||||
$array = [];
|
$array = [];
|
||||||
if ('' !== $types) {
|
if ('' !== $types) {
|
||||||
@@ -62,8 +95,28 @@ class AutocompleteRequest extends FormRequest
|
|||||||
|
|
||||||
public function rules(): array
|
public function rules(): array
|
||||||
{
|
{
|
||||||
|
|
||||||
return [
|
return [
|
||||||
'limit' => 'min:0|max:1337',
|
'fields' => JsonApiRule::notSupported(),
|
||||||
|
'filter' => ['nullable', 'array', new IsValidFilter(['query', 'date', 'account_types']),],
|
||||||
|
'include' => JsonApiRule::notSupported(),
|
||||||
|
'page' => ['nullable', 'array', new IsValidPage(['size']),],
|
||||||
|
'sort' => JsonApiRule::notSupported(),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function getAccountTypeParameter(mixed $types): array
|
||||||
|
{
|
||||||
|
if (is_string($types) && str_contains($types, ',')) {
|
||||||
|
$types = explode(',', $types);
|
||||||
|
}
|
||||||
|
if (!is_iterable($types)) {
|
||||||
|
$types = [$types];
|
||||||
|
}
|
||||||
|
$return = [];
|
||||||
|
foreach ($types as $type) {
|
||||||
|
$return = array_merge($return, $this->mapAccountTypes($type));
|
||||||
|
}
|
||||||
|
return array_unique($return);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -3,8 +3,10 @@
|
|||||||
namespace FireflyIII\Console\Commands\Correction;
|
namespace FireflyIII\Console\Commands\Correction;
|
||||||
|
|
||||||
use DB;
|
use DB;
|
||||||
|
use FireflyIII\Console\Commands\ShowsFriendlyMessages;
|
||||||
use FireflyIII\Models\AccountBalance;
|
use FireflyIII\Models\AccountBalance;
|
||||||
use FireflyIII\Models\Transaction;
|
use FireflyIII\Models\Transaction;
|
||||||
|
use FireflyIII\Support\Models\AccountBalanceCalculator;
|
||||||
use Illuminate\Console\Command;
|
use Illuminate\Console\Command;
|
||||||
use stdClass;
|
use stdClass;
|
||||||
|
|
||||||
@@ -16,6 +18,7 @@ class CorrectAccountBalance extends Command
|
|||||||
protected $description = 'Recalculate all account balance amounts';
|
protected $description = 'Recalculate all account balance amounts';
|
||||||
|
|
||||||
protected $signature = 'firefly-iii:correct-account-balance';
|
protected $signature = 'firefly-iii:correct-account-balance';
|
||||||
|
use ShowsFriendlyMessages;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return int
|
* @return int
|
||||||
@@ -30,19 +33,6 @@ class CorrectAccountBalance extends Command
|
|||||||
|
|
||||||
private function correctBalanceAmounts(): void
|
private function correctBalanceAmounts(): void
|
||||||
{
|
{
|
||||||
$result = Transaction
|
AccountBalanceCalculator::recalculate(null);
|
||||||
::groupBy(['account_id', 'transaction_currency_id'])
|
|
||||||
->get(['account_id', 'transaction_currency_id', DB::raw('SUM(amount) as amount_sum')]);
|
|
||||||
/** @var stdClass $entry */
|
|
||||||
foreach ($result as $entry) {
|
|
||||||
$account = (int) $entry->account_id;
|
|
||||||
$currency = (int) $entry->transaction_currency_id;
|
|
||||||
$sum = $entry->amount_sum;
|
|
||||||
|
|
||||||
AccountBalance::updateOrCreate(
|
|
||||||
['title' => 'balance', 'account_id' => $account, 'transaction_currency_id' => $currency],
|
|
||||||
['balance' => $sum]
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -26,6 +26,7 @@ namespace FireflyIII\Handlers\Observer;
|
|||||||
use DB;
|
use DB;
|
||||||
use FireflyIII\Models\AccountBalance;
|
use FireflyIII\Models\AccountBalance;
|
||||||
use FireflyIII\Models\Transaction;
|
use FireflyIII\Models\Transaction;
|
||||||
|
use FireflyIII\Support\Models\AccountBalanceCalculator;
|
||||||
use stdClass;
|
use stdClass;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -42,16 +43,6 @@ class TransactionObserver
|
|||||||
public function updated(Transaction $transaction): void
|
public function updated(Transaction $transaction): void
|
||||||
{
|
{
|
||||||
app('log')->debug('Observe "updated" of a transaction.');
|
app('log')->debug('Observe "updated" of a transaction.');
|
||||||
// refresh account balance:
|
AccountBalanceCalculator::recalculate($transaction->account);
|
||||||
/** @var stdClass $result */
|
|
||||||
$result = Transaction::groupBy(['account_id', 'transaction_currency_id'])->where('account_id', $transaction->account_id)->first(['account_id', 'transaction_currency_id', DB::raw('SUM(amount) as amount_sum')]);
|
|
||||||
if (null !== $result) {
|
|
||||||
$account = (int) $result->account_id;
|
|
||||||
$currency = (int) $result->transaction_currency_id;
|
|
||||||
$sum = $result->amount_sum;
|
|
||||||
|
|
||||||
AccountBalance::updateOrCreate(['title' => 'balance', 'account_id' => $account, 'transaction_currency_id' => $currency], ['balance' => $sum]);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
55
app/JsonApi/Rules/IsValidFilter.php
Normal file
55
app/JsonApi/Rules/IsValidFilter.php
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
<?php
|
||||||
|
/*
|
||||||
|
* IsValidFilter.php
|
||||||
|
* Copyright (c) 2024 james@firefly-iii.org.
|
||||||
|
*
|
||||||
|
* This file is part of Firefly III (https://github.com/firefly-iii).
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU Affero General Public License as
|
||||||
|
* published by the Free Software Foundation, either version 3 of the
|
||||||
|
* License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU Affero General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
|
* along with this program. If not, see https://www.gnu.org/licenses/.
|
||||||
|
*/
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace FireflyIII\JsonApi\Rules;
|
||||||
|
|
||||||
|
use Closure;
|
||||||
|
use Illuminate\Contracts\Validation\ValidationRule;
|
||||||
|
|
||||||
|
class IsValidFilter implements ValidationRule
|
||||||
|
{
|
||||||
|
private array $allowed;
|
||||||
|
|
||||||
|
public function __construct(array $keys)
|
||||||
|
{
|
||||||
|
$this->allowed = $keys;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritDoc
|
||||||
|
*/
|
||||||
|
#[\Override] public function validate(string $attribute, mixed $value, Closure $fail): void
|
||||||
|
{
|
||||||
|
if ('filter' !== $attribute) {
|
||||||
|
$fail('validation.bad_api_filter')->translate();
|
||||||
|
}
|
||||||
|
if (!is_array($value)) {
|
||||||
|
$value = explode(',', $value);
|
||||||
|
}
|
||||||
|
foreach ($value as $key => $val) {
|
||||||
|
if (!in_array($key, $this->allowed, true)) {
|
||||||
|
$fail('validation.bad_api_filter')->translate();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
55
app/JsonApi/Rules/IsValidPage.php
Normal file
55
app/JsonApi/Rules/IsValidPage.php
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
<?php
|
||||||
|
/*
|
||||||
|
* IsValidFilter.php
|
||||||
|
* Copyright (c) 2024 james@firefly-iii.org.
|
||||||
|
*
|
||||||
|
* This file is part of Firefly III (https://github.com/firefly-iii).
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU Affero General Public License as
|
||||||
|
* published by the Free Software Foundation, either version 3 of the
|
||||||
|
* License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU Affero General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
|
* along with this program. If not, see https://www.gnu.org/licenses/.
|
||||||
|
*/
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace FireflyIII\JsonApi\Rules;
|
||||||
|
|
||||||
|
use Closure;
|
||||||
|
use Illuminate\Contracts\Validation\ValidationRule;
|
||||||
|
|
||||||
|
class IsValidPage implements ValidationRule
|
||||||
|
{
|
||||||
|
private array $allowed;
|
||||||
|
|
||||||
|
public function __construct(array $keys)
|
||||||
|
{
|
||||||
|
$this->allowed = $keys;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritDoc
|
||||||
|
*/
|
||||||
|
#[\Override] public function validate(string $attribute, mixed $value, Closure $fail): void
|
||||||
|
{
|
||||||
|
if ('page' !== $attribute) {
|
||||||
|
$fail('validation.bad_api_filter')->translate();
|
||||||
|
}
|
||||||
|
if (!is_array($value)) {
|
||||||
|
$value = explode(',', $value);
|
||||||
|
}
|
||||||
|
foreach ($value as $key => $val) {
|
||||||
|
if (!in_array($key, $this->allowed, true)) {
|
||||||
|
$fail('validation.bad_api_page')->translate();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -8,7 +8,7 @@ use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
|||||||
|
|
||||||
class AccountBalance extends Model
|
class AccountBalance extends Model
|
||||||
{
|
{
|
||||||
protected $fillable = ['account_id', 'transaction_currency_id', 'balance'];
|
protected $fillable = ['account_id', 'title', 'transaction_currency_id', 'balance'];
|
||||||
use HasFactory;
|
use HasFactory;
|
||||||
|
|
||||||
|
|
||||||
@@ -16,4 +16,9 @@ class AccountBalance extends Model
|
|||||||
{
|
{
|
||||||
return $this->belongsTo(Account::class);
|
return $this->belongsTo(Account::class);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function transactionCurrency(): BelongsTo
|
||||||
|
{
|
||||||
|
return $this->belongsTo(TransactionCurrency::class);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -302,6 +302,7 @@ class AccountRepository implements AccountRepositoryInterface
|
|||||||
;
|
;
|
||||||
if ('' !== $query) {
|
if ('' !== $query) {
|
||||||
// split query on spaces just in case:
|
// split query on spaces just in case:
|
||||||
|
// TODO this will always fail because it searches for AND.
|
||||||
$parts = explode(' ', $query);
|
$parts = explode(' ', $query);
|
||||||
foreach ($parts as $part) {
|
foreach ($parts as $part) {
|
||||||
$search = sprintf('%%%s%%', $part);
|
$search = sprintf('%%%s%%', $part);
|
||||||
@@ -384,4 +385,9 @@ class AccountRepository implements AccountRepositoryInterface
|
|||||||
|
|
||||||
return $return;
|
return $return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[\Override] public function getAccountBalances(Account $account): Collection
|
||||||
|
{
|
||||||
|
return $account->accountBalances;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -53,6 +53,8 @@ interface AccountRepositoryInterface
|
|||||||
|
|
||||||
public function getAccountCurrency(Account $account): ?TransactionCurrency;
|
public function getAccountCurrency(Account $account): ?TransactionCurrency;
|
||||||
|
|
||||||
|
public function getAccountBalances(Account $account): Collection;
|
||||||
|
|
||||||
public function getAccountsById(array $accountIds): Collection;
|
public function getAccountsById(array $accountIds): Collection;
|
||||||
|
|
||||||
public function getAccountsByType(array $types, ?array $sort = [], ?array $filters = []): Collection;
|
public function getAccountsByType(array $types, ?array $sort = [], ?array $filters = []): Collection;
|
||||||
|
100
app/Support/Models/AccountBalanceCalculator.php
Normal file
100
app/Support/Models/AccountBalanceCalculator.php
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
<?php
|
||||||
|
/*
|
||||||
|
* AccountBalanceCalculator.php
|
||||||
|
* Copyright (c) 2024 james@firefly-iii.org.
|
||||||
|
*
|
||||||
|
* This file is part of Firefly III (https://github.com/firefly-iii).
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU Affero General Public License as
|
||||||
|
* published by the Free Software Foundation, either version 3 of the
|
||||||
|
* License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU Affero General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
|
* along with this program. If not, see https://www.gnu.org/licenses/.
|
||||||
|
*/
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace FireflyIII\Support\Models;
|
||||||
|
|
||||||
|
use DB;
|
||||||
|
use FireflyIII\Models\Account;
|
||||||
|
use FireflyIII\Models\AccountBalance;
|
||||||
|
use FireflyIII\Models\Transaction;
|
||||||
|
use Illuminate\Support\Facades\Log;
|
||||||
|
use stdClass;
|
||||||
|
|
||||||
|
class AccountBalanceCalculator
|
||||||
|
{
|
||||||
|
public static function recalculate(?Account $account): void
|
||||||
|
{
|
||||||
|
// first collect normal amounts (in whatever currency), and set them.
|
||||||
|
|
||||||
|
// select account_id, transaction_currency_id, foreign_currency_id, sum(amount), sum(foreign_amount) from transactions group by account_id, transaction_currency_id, foreign_currency_id
|
||||||
|
$result = Transaction
|
||||||
|
::groupBy(['account_id', 'transaction_currency_id', 'foreign_currency_id'])
|
||||||
|
->get(['account_id', 'transaction_currency_id', 'foreign_currency_id', DB::raw('SUM(amount) as sum_amount'), DB::raw('SUM(foreign_amount) as sum_foreign_amount')]);
|
||||||
|
|
||||||
|
// reset account balances:
|
||||||
|
self::resetAccountBalances($account);
|
||||||
|
|
||||||
|
/** @var stdClass $row */
|
||||||
|
foreach ($result as $row) {
|
||||||
|
$account = (int) $row->account_id;
|
||||||
|
$transactionCurrency = (int) $row->transaction_currency_id;
|
||||||
|
$foreignCurrency = (int) $row->foreign_currency_id;
|
||||||
|
$sumAmount = $row->sum_amount;
|
||||||
|
$sumForeignAmount = $row->sum_foreign_amount;
|
||||||
|
|
||||||
|
// first create for normal currency:
|
||||||
|
$entry = self::getBalance('balance', $account, $transactionCurrency);
|
||||||
|
$entry->balance = bcadd($entry->balance, $sumAmount);
|
||||||
|
$entry->save();
|
||||||
|
Log::debug(sprintf('Set balance entry #%d to amount %s', $entry->id, $entry->balance));
|
||||||
|
|
||||||
|
// then do foreign amount, if present:
|
||||||
|
if ($foreignCurrency > 0) {
|
||||||
|
$entry = self::getBalance('balance', $account, $foreignCurrency);
|
||||||
|
$entry->balance = bcadd($entry->balance, $sumForeignAmount);
|
||||||
|
$entry->save();
|
||||||
|
Log::debug(sprintf('Set balance entry #%d to amount %s', $entry->id, $entry->balance));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
private static function getBalance(string $title, int $account, int $currency): AccountBalance
|
||||||
|
{
|
||||||
|
$entry = AccountBalance::where('title', $title)->where('account_id', $account)->where('transaction_currency_id', $currency)->first();
|
||||||
|
if (null !== $entry) {
|
||||||
|
Log::debug(sprintf('Found account balance for account #%d and currency #%d: %s', $account, $currency, $entry->balance));
|
||||||
|
return $entry;
|
||||||
|
}
|
||||||
|
$entry = new AccountBalance;
|
||||||
|
$entry->title = $title;
|
||||||
|
$entry->account_id = $account;
|
||||||
|
$entry->transaction_currency_id = $currency;
|
||||||
|
$entry->balance = '0';
|
||||||
|
$entry->save();
|
||||||
|
Log::debug(sprintf('Created new account balance for account #%d and currency #%d: %s', $account, $currency, $entry->balance));
|
||||||
|
return $entry;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static function resetAccountBalances(?Account $account): void
|
||||||
|
{
|
||||||
|
if (null === $account) {
|
||||||
|
AccountBalance::whereNotNull('updated_at')->update(['balance' => '0']);
|
||||||
|
Log::debug('Set ALL balances to zero.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
AccountBalance::where('account_id', $account->id)->update(['balance' => '0']);
|
||||||
|
Log::debug(sprintf('Set balances of account #%d to zero.', $account->id));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
@@ -29,22 +29,44 @@ use LaravelJsonApi\Laravel\Routing\ActionRegistrar;
|
|||||||
use LaravelJsonApi\Laravel\Routing\Relationships;
|
use LaravelJsonApi\Laravel\Routing\Relationships;
|
||||||
use LaravelJsonApi\Laravel\Routing\ResourceRegistrar;
|
use LaravelJsonApi\Laravel\Routing\ResourceRegistrar;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* V2 auto complete controller(s)
|
||||||
|
*/
|
||||||
|
Route::group(
|
||||||
|
[
|
||||||
|
'namespace' => 'FireflyIII\Api\V2\Controllers\Autocomplete',
|
||||||
|
'prefix' => 'v2/autocomplete',
|
||||||
|
'as' => 'api.v2.autocomplete.',
|
||||||
|
],
|
||||||
|
static function (): void {
|
||||||
|
// Auto complete routes
|
||||||
|
Route::get('accounts', ['uses' => 'AccountController@accounts', 'as' => 'accounts']);
|
||||||
|
Route::get('transaction-descriptions', ['uses' => 'TransactionController@transactionDescriptions', 'as' => 'transaction-descriptions']);
|
||||||
|
Route::get('categories', ['uses' => 'CategoryController@categories', 'as' => 'categories']);
|
||||||
|
Route::get('tags', ['uses' => 'TagController@tags', 'as' => 'tags']);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
//JsonApiRoute::server('v3')
|
||||||
|
// ->prefix('v3')
|
||||||
|
// ->resources(function (ResourceRegistrar $server) {
|
||||||
|
// $server->resource('accounts', AccountController::class)->readOnly()->relationships(function (Relationships $relations) {
|
||||||
|
// $relations->hasOne('user')->readOnly();
|
||||||
|
// //$relations->hasMany('account_balances')->readOnly();
|
||||||
|
// })
|
||||||
|
// ->actions(function (ActionRegistrar $actions) {
|
||||||
|
// $actions->withId()->get('account-balances', 'readAccountBalances'); // non-eloquent pseudo relation
|
||||||
|
// });
|
||||||
|
// $server->resource('users', JsonApiController::class)->readOnly()->relationships(function (Relationships $relations) {
|
||||||
|
// $relations->hasMany('accounts')->readOnly();
|
||||||
|
// });
|
||||||
|
// $server->resource('account-balances', JsonApiController::class);
|
||||||
|
// });
|
||||||
|
|
||||||
JsonApiRoute::server('v3')
|
|
||||||
->prefix('v3')
|
|
||||||
->resources(function (ResourceRegistrar $server) {
|
|
||||||
$server->resource('accounts', AccountController::class)->readOnly()->relationships(function (Relationships $relations) {
|
|
||||||
$relations->hasOne('user')->readOnly();
|
|
||||||
//$relations->hasMany('account_balances')->readOnly();
|
|
||||||
})
|
|
||||||
->actions(function (ActionRegistrar $actions) {
|
|
||||||
$actions->withId()->get('account-balances', 'readAccountBalances'); // non-eloquent pseudo relation
|
|
||||||
});
|
|
||||||
$server->resource('users', JsonApiController::class)->readOnly()->relationships(function (Relationships $relations) {
|
|
||||||
$relations->hasMany('accounts')->readOnly();
|
|
||||||
});
|
|
||||||
$server->resource('account-balances', JsonApiController::class);
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
// V2 API route for Summary boxes
|
// V2 API route for Summary boxes
|
||||||
@@ -79,20 +101,7 @@ Route::group(
|
|||||||
);
|
);
|
||||||
|
|
||||||
// V2 API routes for auto complete
|
// V2 API routes for auto complete
|
||||||
Route::group(
|
|
||||||
[
|
|
||||||
'namespace' => 'FireflyIII\Api\V2\Controllers\Autocomplete',
|
|
||||||
'prefix' => 'v2/autocomplete',
|
|
||||||
'as' => 'api.v2.autocomplete.',
|
|
||||||
],
|
|
||||||
static function (): void {
|
|
||||||
// Auto complete routes
|
|
||||||
Route::get('accounts', ['uses' => 'AccountController@accounts', 'as' => 'accounts']);
|
|
||||||
Route::get('transaction-descriptions', ['uses' => 'TransactionController@transactionDescriptions', 'as' => 'transaction-descriptions']);
|
|
||||||
Route::get('categories', ['uses' => 'CategoryController@categories', 'as' => 'categories']);
|
|
||||||
Route::get('tags', ['uses' => 'TagController@tags', 'as' => 'tags']);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
// V2 API route for net worth endpoint(s);
|
// V2 API route for net worth endpoint(s);
|
||||||
Route::group(
|
Route::group(
|
||||||
|
Reference in New Issue
Block a user