diff --git a/app/Api/V2/Controllers/JsonApi/AccountController.php b/app/Api/V2/Controllers/JsonApi/AccountController.php index 5b34923741..2e8effdc6b 100644 --- a/app/Api/V2/Controllers/JsonApi/AccountController.php +++ b/app/Api/V2/Controllers/JsonApi/AccountController.php @@ -81,12 +81,13 @@ class AccountController extends Controller */ public function show(AccountSchema $schema, AccountSingleQuery $request, Account $account) { - $model = $schema - ->repository() + Log::debug(__METHOD__); + $model = $schema->repository() ->queryOne($account) ->withRequest($request) ->first() ; + Log::debug(sprintf('%s again!', __METHOD__)); // do something custom... diff --git a/app/JsonApi/V2/Accounts/AccountRepository.php b/app/JsonApi/V2/Accounts/AccountRepository.php index 0bc4c50fb7..6dc5d45ee6 100644 --- a/app/JsonApi/V2/Accounts/AccountRepository.php +++ b/app/JsonApi/V2/Accounts/AccountRepository.php @@ -28,8 +28,10 @@ use FireflyIII\Support\JsonApi\Concerns\UsergroupAware; use FireflyIII\Support\JsonApi\Enrichments\AccountEnrichment; use Illuminate\Support\Facades\Log; use LaravelJsonApi\Contracts\Store\QueriesAll; +use LaravelJsonApi\Contracts\Store\QueryOneBuilder; use LaravelJsonApi\NonEloquent\AbstractRepository; use LaravelJsonApi\NonEloquent\Capabilities\CrudRelations; +use LaravelJsonApi\NonEloquent\Capabilities\CrudResource; use LaravelJsonApi\NonEloquent\Concerns\HasCrudCapability; use LaravelJsonApi\NonEloquent\Concerns\HasRelationsCapability; @@ -52,17 +54,21 @@ class AccountRepository extends AbstractRepository implements QueriesAll /** * SiteRepository constructor. */ - public function __construct() {} + public function __construct() { + Log::debug(__METHOD__); + } public function exists(string $resourceId): bool { - Log::debug(__METHOD__); + $result = null !== Account::find((int) $resourceId); + Log::debug(sprintf('%s: %s',__METHOD__, var_export($result, true))); - return null !== Account::find((int) $resourceId); + return $result; } public function find(string $resourceId): ?object { + die(__METHOD__); Log::debug(__METHOD__); // throw new \RuntimeException('trace me'); $account = Account::find((int) $resourceId); @@ -88,11 +94,13 @@ class AccountRepository extends AbstractRepository implements QueriesAll protected function crud(): Capabilities\CrudAccount { + Log::debug(__METHOD__); return Capabilities\CrudAccount::make(); } protected function relations(): CrudRelations { + Log::debug(__METHOD__); return Capabilities\CrudAccountRelations::make(); } } diff --git a/app/JsonApi/V2/Accounts/AccountResource.php b/app/JsonApi/V2/Accounts/AccountResource.php index f137eda932..14553433b5 100644 --- a/app/JsonApi/V2/Accounts/AccountResource.php +++ b/app/JsonApi/V2/Accounts/AccountResource.php @@ -19,9 +19,10 @@ class AccountResource extends JsonApiResource */ public function id(): string { - Log::debug(__METHOD__); + $id = (string) $this->resource->id; + Log::debug(sprintf('%s: "%s"', __METHOD__, $id)); - return (string) $this->resource->id; + return $id; } /** @@ -39,7 +40,7 @@ class AccountResource extends JsonApiResource 'name' => $this->resource->name, 'active' => $this->resource->active, 'order' => $this->resource->order, - 'iban' => $this->resource->iban, + 'iban' => $this->resource->iban, 'type' => $this->resource->account_type_string, 'account_role' => $this->resource->account_role, 'account_number' => '' === $this->resource->account_number ? null : $this->resource->account_number, @@ -62,23 +63,14 @@ class AccountResource extends JsonApiResource 'interest_period' => $this->resource->interest_period, 'current_debt' => $this->resource->current_debt, // TODO may be removed in the future. - // other things - 'last_activity' => $this->resource->last_activity, + 'last_activity' => $this->resource->last_activity, - // still to do - - // balance difference -// 'balance_difference' => $balanceDiff, -// 'native_balance_difference' => $nativeBalanceDiff, -// 'balance_difference_start' => $diffStart, -// 'balance_difference_end' => $diffEnd, - // object group -// 'object_group_id' => null !== $objectGroupId ? (string) $objectGroupId : null, -// 'object_group_order' => $objectGroupOrder, -// 'object_group_title' => $objectGroupTitle, + 'object_group_id' => $this->resource->object_group_id, + 'object_group_title' => $this->resource->object_group_title, + 'object_group_order' => $this->resource->object_group_order, ]; } diff --git a/app/JsonApi/V2/Accounts/AccountSchema.php b/app/JsonApi/V2/Accounts/AccountSchema.php index 6aebb3be82..37eb25c0e5 100644 --- a/app/JsonApi/V2/Accounts/AccountSchema.php +++ b/app/JsonApi/V2/Accounts/AccountSchema.php @@ -76,13 +76,12 @@ class AccountSchema extends Schema public function repository(): AccountRepository { - Log::debug(__METHOD__); $this->setUserGroup($this->server->getUsergroup()); - - return AccountRepository::make() - ->withServer($this->server) - ->withSchema($this) - ->withUserGroup($this->userGroup) - ; + $repository = AccountRepository::make() + ->withServer($this->server) + ->withSchema($this) + ->withUserGroup($this->userGroup); + Log::debug(sprintf('%s: %s', __METHOD__, get_class($repository))); + return $repository; } } diff --git a/app/JsonApi/V2/Accounts/Capabilities/AccountQuery.php b/app/JsonApi/V2/Accounts/Capabilities/AccountQuery.php index ec1b5e6169..a0c40474d3 100644 --- a/app/JsonApi/V2/Accounts/Capabilities/AccountQuery.php +++ b/app/JsonApi/V2/Accounts/Capabilities/AccountQuery.php @@ -48,6 +48,8 @@ class AccountQuery extends QueryAll implements HasPagination #[\Override] /** * This method returns all accounts, given a bunch of filters and sort fields, together with pagination. + * + * It is only used on the index, and nowhere else. */ public function get(): iterable { @@ -77,6 +79,7 @@ class AccountQuery extends QueryAll implements HasPagination $query = $this->addSortParams($query, $sort); $query = $this->addFilterParams('account', $query, $filters); + // collect the result. $collection = $query->get(['accounts.*']); diff --git a/app/JsonApi/V2/Accounts/Capabilities/CrudAccount.php b/app/JsonApi/V2/Accounts/Capabilities/CrudAccount.php index 8a7658abb9..bc42100f75 100644 --- a/app/JsonApi/V2/Accounts/Capabilities/CrudAccount.php +++ b/app/JsonApi/V2/Accounts/Capabilities/CrudAccount.php @@ -24,19 +24,31 @@ declare(strict_types=1); namespace FireflyIII\JsonApi\V2\Accounts\Capabilities; use FireflyIII\Models\Account; +use FireflyIII\Support\JsonApi\CollectsCustomParameters; use FireflyIII\Support\JsonApi\Enrichments\AccountEnrichment; +use Illuminate\Support\Facades\Log; use LaravelJsonApi\NonEloquent\Capabilities\CrudResource; class CrudAccount extends CrudResource { + use CollectsCustomParameters; + /** * Read the supplied site. */ public function read(Account $account): ?Account { + $otherParams = $this->getOtherParams($this->request->query->all()); + + Log::debug(__METHOD__); // enrich the collected data $enrichment = new AccountEnrichment(); + // set start and date, if present. + $enrichment->setStart($otherParams['start'] ?? null); + $enrichment->setEnd($otherParams['end'] ?? null); + + return $enrichment->enrichSingle($account); } } diff --git a/app/Support/Balance.php b/app/Support/Balance.php index baf348ccf7..3864957e52 100644 --- a/app/Support/Balance.php +++ b/app/Support/Balance.php @@ -27,6 +27,7 @@ use Carbon\Carbon; use FireflyIII\Models\Transaction; use FireflyIII\Models\TransactionCurrency; use Illuminate\Support\Collection; +use Illuminate\Support\Facades\Log; class Balance { @@ -41,6 +42,7 @@ class Balance */ public function getAccountBalances(Collection $accounts, Carbon $date): array { + Log::debug(sprintf('getAccountBalances(, "%s")', $date->format('Y-m-d'))); $return = []; $currencies = []; $cache = new CacheProperties(); @@ -48,7 +50,7 @@ class Balance $cache->addProperty('getAccountBalances'); $cache->addProperty($date); if ($cache->has()) { - return $cache->get(); + return $cache->get(); } $query = Transaction:: @@ -57,7 +59,8 @@ class Balance ->orderBy('transaction_journals.date', 'desc') ->orderBy('transaction_journals.order', 'asc') ->orderBy('transaction_journals.description', 'desc') - ->orderBy('transactions.amount', 'desc'); + ->orderBy('transactions.amount', 'desc') + ->where('transaction_journals.date', '<=', $date); $result = $query->get(['transactions.account_id', 'transactions.transaction_currency_id', 'transactions.balance_after']); foreach ($result as $entry) { diff --git a/app/Support/Http/Api/ExchangeRateConverter.php b/app/Support/Http/Api/ExchangeRateConverter.php index 0cda38da77..4a930e0878 100644 --- a/app/Support/Http/Api/ExchangeRateConverter.php +++ b/app/Support/Http/Api/ExchangeRateConverter.php @@ -44,12 +44,18 @@ class ExchangeRateConverter private array $prepared = []; private int $queryCount = 0; + + public function enabled(): bool + { + return false !== config('cer.enabled'); + } + /** * @throws FireflyException */ public function convert(TransactionCurrency $from, TransactionCurrency $to, Carbon $date, string $amount): string { - if (false === config('cer.enabled')) { + if (false === $this->enabled()) { Log::debug('ExchangeRateConverter: disabled, return amount as is.'); return $amount; @@ -64,7 +70,7 @@ class ExchangeRateConverter */ public function getCurrencyRate(TransactionCurrency $from, TransactionCurrency $to, Carbon $date): string { - if (false === config('cer.enabled')) { + if (false === $this->enabled()) { Log::debug('ExchangeRateConverter: disabled, return "1".'); return '1'; @@ -79,8 +85,8 @@ class ExchangeRateConverter */ private function getRate(TransactionCurrency $from, TransactionCurrency $to, Carbon $date): string { - $key = $this->getCacheKey($from, $to, $date); - $res = Cache::get($key, null); + $key = $this->getCacheKey($from, $to, $date); + $res = Cache::get($key, null); // find in cache if (null !== $res) { @@ -90,7 +96,7 @@ class ExchangeRateConverter } // find in database - $rate = $this->getFromDB($from->id, $to->id, $date->format('Y-m-d')); + $rate = $this->getFromDB($from->id, $to->id, $date->format('Y-m-d')); if (null !== $rate) { Cache::forever($key, $rate); Log::debug(sprintf('ExchangeRateConverter: Return DB rate from #%d to #%d on %s.', $from->id, $to->id, $date->format('Y-m-d'))); @@ -99,7 +105,7 @@ class ExchangeRateConverter } // find reverse in database - $rate = $this->getFromDB($to->id, $from->id, $date->format('Y-m-d')); + $rate = $this->getFromDB($to->id, $from->id, $date->format('Y-m-d')); if (null !== $rate) { $rate = bcdiv('1', $rate); Cache::forever($key, $rate); @@ -132,7 +138,7 @@ class ExchangeRateConverter if ($from === $to) { return '1'; } - $key = sprintf('cer-%d-%d-%s', $from, $to, $date); + $key = sprintf('cer-%d-%d-%s', $from, $to, $date); // perhaps the rate has been cached during this particular run $preparedRate = $this->prepared[$date][$from][$to] ?? null; @@ -142,7 +148,7 @@ class ExchangeRateConverter return $preparedRate; } - $cache = new CacheProperties(); + $cache = new CacheProperties(); $cache->addProperty($key); if ($cache->has()) { $rate = $cache->get(); @@ -155,16 +161,15 @@ class ExchangeRateConverter } /** @var null|CurrencyExchangeRate $result */ - $result = auth()->user() - ->currencyExchangeRates() - ->where('from_currency_id', $from) - ->where('to_currency_id', $to) - ->where('date', '<=', $date) - ->orderBy('date', 'DESC') - ->first() - ; + $result = auth()->user() + ->currencyExchangeRates() + ->where('from_currency_id', $from) + ->where('to_currency_id', $to) + ->where('date', '<=', $date) + ->orderBy('date', 'DESC') + ->first(); ++$this->queryCount; - $rate = (string) $result?->rate; + $rate = (string) $result?->rate; if ('' === $rate) { app('log')->debug(sprintf('ExchangeRateConverter: Found no rate for #%d->#%d (%s) in the DB.', $from, $to, $date)); @@ -204,13 +209,13 @@ class ExchangeRateConverter if ($euroId === $currency->id) { return '1'; } - $rate = $this->getFromDB($currency->id, $euroId, $date->format('Y-m-d')); + $rate = $this->getFromDB($currency->id, $euroId, $date->format('Y-m-d')); if (null !== $rate) { // app('log')->debug(sprintf('Rate for %s to EUR is %s.', $currency->code, $rate)); return $rate; } - $rate = $this->getFromDB($euroId, $currency->id, $date->format('Y-m-d')); + $rate = $this->getFromDB($euroId, $currency->id, $date->format('Y-m-d')); if (null !== $rate) { return bcdiv('1', $rate); // app('log')->debug(sprintf('Inverted rate for %s to EUR is %s.', $currency->code, $rate)); @@ -239,7 +244,7 @@ class ExchangeRateConverter if ($cache->has()) { return (int) $cache->get(); } - $euro = TransactionCurrency::whereCode('EUR')->first(); + $euro = TransactionCurrency::whereCode('EUR')->first(); ++$this->queryCount; if (null === $euro) { throw new FireflyException('Cannot find EUR in system, cannot do currency conversion.'); @@ -254,21 +259,20 @@ class ExchangeRateConverter */ public function prepare(TransactionCurrency $from, TransactionCurrency $to, Carbon $start, Carbon $end): void { - if (false === config('cer.enabled')) { + if (false === $this->enabled()) { return; } Log::debug('prepare()'); $start->startOfDay(); $end->endOfDay(); Log::debug(sprintf('Preparing for %s to %s between %s and %s', $from->code, $to->code, $start->format('Y-m-d'), $end->format('Y-m-d'))); - $set = auth()->user() - ->currencyExchangeRates() - ->where('from_currency_id', $from->id) - ->where('to_currency_id', $to->id) - ->where('date', '<=', $end->format('Y-m-d')) - ->where('date', '>=', $start->format('Y-m-d')) - ->orderBy('date', 'DESC')->get() - ; + $set = auth()->user() + ->currencyExchangeRates() + ->where('from_currency_id', $from->id) + ->where('to_currency_id', $to->id) + ->where('date', '<=', $end->format('Y-m-d')) + ->where('date', '>=', $start->format('Y-m-d')) + ->orderBy('date', 'DESC')->get(); ++$this->queryCount; if (0 === $set->count()) { Log::debug('No prepared rates found in this period, use the fallback'); @@ -282,10 +286,10 @@ class ExchangeRateConverter $this->isPrepared = true; // so there is a fallback just in case. Now loop the set of rates we DO have. - $temp = []; - $count = 0; + $temp = []; + $count = 0; foreach ($set as $rate) { - $date = $rate->date->format('Y-m-d'); + $date = $rate->date->format('Y-m-d'); $temp[$date] ??= [ $from->id => [ $to->id => $rate->rate, @@ -294,11 +298,11 @@ class ExchangeRateConverter ++$count; } Log::debug(sprintf('Found %d rates in this period.', $count)); - $currentStart = clone $start; + $currentStart = clone $start; while ($currentStart->lte($end)) { - $currentDate = $currentStart->format('Y-m-d'); + $currentDate = $currentStart->format('Y-m-d'); $this->prepared[$currentDate] ??= []; - $fallback = $temp[$currentDate][$from->id][$to->id] ?? $this->fallback[$from->id][$to->id] ?? '0'; + $fallback = $temp[$currentDate][$from->id][$to->id] ?? $this->fallback[$from->id][$to->id] ?? '0'; if (0 === count($this->prepared[$currentDate]) && 0 !== bccomp('0', $fallback)) { // fill from temp or fallback or from temp (see before) $this->prepared[$currentDate][$from->id][$to->id] = $fallback; @@ -329,7 +333,7 @@ class ExchangeRateConverter public function summarize(): void { - if (false === config('cer.enabled')) { + if (false === $this->enabled()) { return; } Log::debug(sprintf('ExchangeRateConverter ran %d queries.', $this->queryCount)); diff --git a/app/Support/JsonApi/Enrichments/AccountEnrichment.php b/app/Support/JsonApi/Enrichments/AccountEnrichment.php index 4983f90e17..cfec8dfe05 100644 --- a/app/Support/JsonApi/Enrichments/AccountEnrichment.php +++ b/app/Support/JsonApi/Enrichments/AccountEnrichment.php @@ -26,6 +26,7 @@ namespace FireflyIII\Support\JsonApi\Enrichments; use Carbon\Carbon; use FireflyIII\Models\Account; use FireflyIII\Models\AccountType; +use FireflyIII\Models\ObjectGroup; use FireflyIII\Models\TransactionCurrency; use FireflyIII\Repositories\UserGroups\Account\AccountRepositoryInterface; use FireflyIII\Repositories\UserGroups\Currency\CurrencyRepositoryInterface; @@ -44,6 +45,8 @@ class AccountEnrichment implements EnrichmentInterface { private Collection $collection; private array $currencies; + private array $objectGroups; + private array $grouped; private array $balances; private TransactionCurrency $default; private ?Carbon $start; @@ -68,16 +71,19 @@ class AccountEnrichment implements EnrichmentInterface { Log::debug(sprintf('Now doing account enrichment for %d account(s)', $collection->count())); // prep local fields - $this->collection = $collection; - $this->default = app('amount')->getDefaultCurrency(); - $this->currencies = []; - $this->balances = []; + $this->collection = $collection; + $this->default = app('amount')->getDefaultCurrency(); + $this->currencies = []; + $this->balances = []; + $this->objectGroups = []; + $this->grouped = []; // do everything here: $this->getLastActivity(); $this->collectAccountTypes(); $this->collectMetaData(); $this->getMetaBalances(); + $this->getObjectGroups(); // $this->collection->transform(function (Account $account) { // $account->user_array = ['id' => 1, 'bla bla' => 'bla']; @@ -112,16 +118,15 @@ class AccountEnrichment implements EnrichmentInterface // get start and end, so the balance difference can be generated. $start = null; - $end = null; - if(null !== $this->start) { + $end = null; + if (null !== $this->start) { $start = Balance::getAccountBalances($this->collection, $this->start); } - if(null !== $this->end) { + if (null !== $this->end) { $end = Balance::getAccountBalances($this->collection, $this->end); } - - $this->collection->transform(function (Account $account) use ($balances, $default) { + $this->collection->transform(function (Account $account) use ($balances, $default, $start, $end) { $converter = new ExchangeRateConverter(); $native = [ 'currency_id' => $this->default->id, @@ -130,39 +135,50 @@ class AccountEnrichment implements EnrichmentInterface 'currency_symbol' => $this->default->symbol, 'currency_decimal_places' => $this->default->decimal_places, 'balance' => '0', + 'period_start_balance' => null, + 'period_end_balance' => null, + 'balance_difference' => null, ]; if (array_key_exists($account->id, $balances)) { $set = []; - foreach ($balances[$account->id] as $entry) { - $set[] = [ + foreach ($balances[$account->id] as $currencyId => $entry) { + $left = $start[$account->id][$currencyId]['balance'] ?? null; + $right = $end[$account->id][$currencyId]['balance'] ?? null; + $diff = null; + if (null !== $left && null !== $right) { + $diff = bcsub($right, $left); + } + + $item = [ 'currency_id' => $entry['currency']->id, 'currency_name' => $entry['currency']->name, 'currency_code' => $entry['currency']->code, 'currency_symbol' => $entry['currency']->symbol, 'currency_decimal_places' => $entry['currency']->decimal_places, 'balance' => $entry['balance'], + 'period_start_balance' => $left, + 'period_end_balance' => $right, + 'balance_difference' => $diff, ]; - $native['balance'] = bcadd($native['balance'], $converter->convert($entry['currency'], $default, today(), $entry['balance'])); + $set[] = $item; + if ($converter->enabled()) { + $native['balance'] = bcadd($native['balance'], $converter->convert($entry['currency'], $default, today(), $entry['balance'])); + if (null !== $diff) { + $native['period_start_balance'] = $converter->convert($entry['currency'], $default, today(), $item['period_start_balance']); + $native['period_end_balance'] = $converter->convert($entry['currency'], $default, today(), $item['period_end_balance']); + $native['balance_difference'] = bcsub($native['period_end_balance'], $native['period_start_balance']); + } + } + } + $account->balance = $set; + if ($converter->enabled()) { + $account->native_balance = $native; } - $account->balance = $set; - $account->native_balance = $native; } return $account; }); - -// try { -// $array = app('steam')->balancesByAccountsConverted($this->collection, today()); -// } catch (FireflyException $e) { -// Log::error(sprintf('Could not load balances: %s', $e->getMessage())); -// -// return; -// } -// foreach ($array as $accountId => $row) { -// //$this->collection->where('id', $accountId)->first()->balance = $row['balance']; -// //$this->collection->where('id', $accountId)->first()->native_balance = $row['native_balance']; -// } } /** @@ -213,7 +229,7 @@ class AccountEnrichment implements EnrichmentInterface } #[\Override] - public function enrichSingle(Model $model): Model + public function enrichSingle(Model $model): Account { Log::debug(__METHOD__); $collection = new Collection([$model]); @@ -232,5 +248,33 @@ class AccountEnrichment implements EnrichmentInterface $this->end = $end; } + private function getObjectGroups(): void + { + $set = \DB::table('object_groupables') + ->where('object_groupable_type', Account::class) + ->whereIn('object_groupable_id', $this->collection->pluck('id')->toArray()) + ->distinct() + ->get(['object_groupables.object_groupable_id', 'object_groupables.object_group_id']); + // get the groups: + $groupIds = $set->pluck('object_group_id')->toArray(); + $groups = ObjectGroup::whereIn('id', $groupIds)->get(); + /** @var ObjectGroup $group */ + foreach ($groups as $group) { + $this->objectGroups[$group->id] = $group; + } + /** @var \stdClass $entry */ + foreach ($set as $entry) { + $this->grouped[(int) $entry->object_groupable_id] = (int) $entry->object_group_id; + } + $this->collection->transform(function (Account $account) { + $account->object_group_id = $this->grouped[$account->id] ?? null; + if(null !== $account->object_group_id) { + $account->object_group_title = $this->objectGroups[$account->object_group_id]->title; + $account->object_group_order = $this->objectGroups[$account->object_group_id]->order; + } + return $account; + }); + } + }