. */ declare(strict_types=1); namespace FireflyIII\Support\Http\Controllers; use Carbon\Carbon; use FireflyIII\Exceptions\FireflyException; use FireflyIII\Helpers\Collector\GroupCollectorInterface; use FireflyIII\Models\Account; use FireflyIII\Models\Category; use FireflyIII\Models\Tag; use FireflyIII\Models\TransactionType; use FireflyIII\Repositories\Journal\JournalRepositoryInterface; use FireflyIII\Support\CacheProperties; use Illuminate\Support\Collection; use Log; use Psr\Container\ContainerExceptionInterface; use Psr\Container\NotFoundExceptionInterface; /** * Trait PeriodOverview. * * TODO verify this all works as expected. * * - Always request start date and end date. * - Group expenses, income, etc. under this period. * - Returns collection of arrays. Fields * title (string), * route (string) * total_transactions (int) * spent (array), * earned (array), * transferred_away (array) * transferred_in (array) * transferred (array) * * each array has the following format: * currency_id => [ * currency_id : 1, (int) * currency_symbol : X (str) * currency_name: Euro (str) * currency_code: EUR (str) * amount: -1234 (str) * count: 23 * ] * */ trait PeriodOverview { protected JournalRepositoryInterface $journalRepos; /** * This method returns "period entries", so nov-2015, dec-2015, etc etc (this depends on the users session range) * and for each period, the amount of money spent and earned. This is a complex operation which is cached for * performance reasons. * * @param Account $account * @param Carbon $start * @param Carbon $end * * @return array * @throws FireflyException * @throws ContainerExceptionInterface * @throws NotFoundExceptionInterface */ protected function getAccountPeriodOverview(Account $account, Carbon $start, Carbon $end): array { $range = app('preferences')->get('viewRange', '1M')->data; [$start, $end] = $end < $start ? [$end, $start] : [$start, $end]; // properties for cache $cache = new CacheProperties(); $cache->addProperty($start); $cache->addProperty($end); $cache->addProperty('account-show-period-entries'); $cache->addProperty($account->id); if ($cache->has()) { // return $cache->get(); } /** @var array $dates */ $dates = app('navigation')->blockPeriods($start, $end, $range); $entries = []; // collect all expenses in this period: /** @var GroupCollectorInterface $collector */ $collector = app(GroupCollectorInterface::class); $collector->setAccounts(new Collection([$account])); $collector->setRange($start, $end); $collector->setTypes([TransactionType::DEPOSIT]); $earnedSet = $collector->getExtractedJournals(); // collect all income in this period: /** @var GroupCollectorInterface $collector */ $collector = app(GroupCollectorInterface::class); $collector->setAccounts(new Collection([$account])); $collector->setRange($start, $end); $collector->setTypes([TransactionType::WITHDRAWAL]); $spentSet = $collector->getExtractedJournals(); // collect all transfers in this period: /** @var GroupCollectorInterface $collector */ $collector = app(GroupCollectorInterface::class); $collector->setAccounts(new Collection([$account])); $collector->setRange($start, $end); $collector->setTypes([TransactionType::TRANSFER]); $transferSet = $collector->getExtractedJournals(); // loop dates foreach ($dates as $currentDate) { $title = app('navigation')->periodShow($currentDate['start'], $currentDate['period']); $earned = $this->filterJournalsByDate($earnedSet, $currentDate['start'], $currentDate['end']); $spent = $this->filterJournalsByDate($spentSet, $currentDate['start'], $currentDate['end']); $transferredAway = $this->filterTransferredAway($account, $this->filterJournalsByDate($transferSet, $currentDate['start'], $currentDate['end'])); $transferredIn = $this->filterTransferredIn($account, $this->filterJournalsByDate($transferSet, $currentDate['start'], $currentDate['end'])); $entries[] = [ 'title' => $title, 'route' => route('accounts.show', [$account->id, $currentDate['start']->format('Y-m-d'), $currentDate['end']->format('Y-m-d')]), 'total_transactions' => count($spent) + count($earned) + count($transferredAway) + count($transferredIn), 'spent' => $this->groupByCurrency($spent), 'earned' => $this->groupByCurrency($earned), 'transferred_away' => $this->groupByCurrency($transferredAway), 'transferred_in' => $this->groupByCurrency($transferredIn), ]; } $cache->store($entries); return $entries; } /** * Filter a list of journals by a set of dates, and then group them by currency. * * @param array $array * @param Carbon $start * @param Carbon $end * * @return array */ private function filterJournalsByDate(array $array, Carbon $start, Carbon $end): array { $result = []; /** @var array $journal */ foreach ($array as $journal) { if ($journal['date'] <= $end && $journal['date'] >= $start) { $result[] = $journal; } } return $result; } /** * Return only transactions where $account is the source. * * @param Account $account * @param array $journals * * @return array */ private function filterTransferredAway(Account $account, array $journals): array { $return = []; /** @var array $journal */ foreach ($journals as $journal) { if ($account->id === (int)$journal['source_account_id']) { $return[] = $journal; } } return $return; } /** * Return only transactions where $account is the source. * * @param Account $account * @param array $journals * * @return array * @codeCoverageIgnore */ private function filterTransferredIn(Account $account, array $journals): array { $return = []; /** @var array $journal */ foreach ($journals as $journal) { if ($account->id === (int)$journal['destination_account_id']) { $return[] = $journal; } } return $return; } /** * @param array $journals * * @return array * @codeCoverageIgnore */ private function groupByCurrency(array $journals): array { $return = []; /** @var array $journal */ foreach ($journals as $journal) { $currencyId = (int)$journal['currency_id']; $foreignCurrencyId = $journal['foreign_currency_id']; if (!array_key_exists($currencyId, $return)) { $return[$currencyId] = [ 'amount' => '0', 'count' => 0, 'currency_id' => $currencyId, 'currency_name' => $journal['currency_name'], 'currency_code' => $journal['currency_code'], 'currency_symbol' => $journal['currency_symbol'], 'currency_decimal_places' => $journal['currency_decimal_places'], ]; } $return[$currencyId]['amount'] = bcadd($return[$currencyId]['amount'], $journal['amount'] ?? '0'); $return[$currencyId]['count']++; if (null !== $foreignCurrencyId && null !== $journal['foreign_amount']) { if (!array_key_exists($foreignCurrencyId, $return)) { $return[$foreignCurrencyId] = [ 'amount' => '0', 'count' => 0, 'currency_id' => (int)$foreignCurrencyId, 'currency_name' => $journal['foreign_currency_name'], 'currency_code' => $journal['foreign_currency_code'], 'currency_symbol' => $journal['foreign_currency_symbol'], 'currency_decimal_places' => $journal['foreign_currency_decimal_places'], ]; } $return[$foreignCurrencyId]['count']++; $return[$foreignCurrencyId]['amount'] = bcadd($return[$foreignCurrencyId]['amount'], $journal['foreign_amount']); } } return $return; } /** * Overview for single category. Has been refactored recently. * * @param Category $category * @param Carbon $start * @param Carbon $end * * @return array * @throws FireflyException * @throws ContainerExceptionInterface * @throws NotFoundExceptionInterface */ protected function getCategoryPeriodOverview(Category $category, Carbon $start, Carbon $end): array { $range = app('preferences')->get('viewRange', '1M')->data; [$start, $end] = $end < $start ? [$end, $start] : [$start, $end]; // properties for entries with their amounts. $cache = new CacheProperties(); $cache->addProperty($start); $cache->addProperty($end); $cache->addProperty($range); $cache->addProperty('category-show-period-entries'); $cache->addProperty($category->id); if ($cache->has()) { return $cache->get(); } /** @var array $dates */ $dates = app('navigation')->blockPeriods($start, $end, $range); $entries = []; // collect all expenses in this period: /** @var GroupCollectorInterface $collector */ $collector = app(GroupCollectorInterface::class); $collector->setCategory($category); $collector->setRange($start, $end); $collector->setTypes([TransactionType::DEPOSIT]); $earnedSet = $collector->getExtractedJournals(); // collect all income in this period: /** @var GroupCollectorInterface $collector */ $collector = app(GroupCollectorInterface::class); $collector->setCategory($category); $collector->setRange($start, $end); $collector->setTypes([TransactionType::WITHDRAWAL]); $spentSet = $collector->getExtractedJournals(); // collect all transfers in this period: /** @var GroupCollectorInterface $collector */ $collector = app(GroupCollectorInterface::class); $collector->setCategory($category); $collector->setRange($start, $end); $collector->setTypes([TransactionType::TRANSFER]); $transferSet = $collector->getExtractedJournals(); foreach ($dates as $currentDate) { $spent = $this->filterJournalsByDate($spentSet, $currentDate['start'], $currentDate['end']); $earned = $this->filterJournalsByDate($earnedSet, $currentDate['start'], $currentDate['end']); $transferred = $this->filterJournalsByDate($transferSet, $currentDate['start'], $currentDate['end']); $title = app('navigation')->periodShow($currentDate['end'], $currentDate['period']); $entries[] = [ 'transactions' => 0, 'title' => $title, 'route' => route( 'categories.show', [$category->id, $currentDate['start']->format('Y-m-d'), $currentDate['end']->format('Y-m-d')] ), 'total_transactions' => count($spent) + count($earned) + count($transferred), 'spent' => $this->groupByCurrency($spent), 'earned' => $this->groupByCurrency($earned), 'transferred' => $this->groupByCurrency($transferred), ]; } $cache->store($entries); return $entries; } /** * Same as above, but for lists that involve transactions without a budget. * * This method has been refactored recently. * * @param Carbon $start * @param Carbon $end * * @return array * @throws FireflyException * @throws ContainerExceptionInterface * @throws NotFoundExceptionInterface */ protected function getNoBudgetPeriodOverview(Carbon $start, Carbon $end): array { $range = app('preferences')->get('viewRange', '1M')->data; [$start, $end] = $end < $start ? [$end, $start] : [$start, $end]; $cache = new CacheProperties(); $cache->addProperty($start); $cache->addProperty($end); $cache->addProperty('no-budget-period-entries'); if ($cache->has()) { return $cache->get(); } /** @var array $dates */ $dates = app('navigation')->blockPeriods($start, $end, $range); $entries = []; // get all expenses without a budget. /** @var GroupCollectorInterface $collector */ $collector = app(GroupCollectorInterface::class); $collector->setRange($start, $end)->withoutBudget()->withAccountInformation()->setTypes([TransactionType::WITHDRAWAL]); $journals = $collector->getExtractedJournals(); foreach ($dates as $currentDate) { $set = $this->filterJournalsByDate($journals, $currentDate['start'], $currentDate['end']); $title = app('navigation')->periodShow($currentDate['end'], $currentDate['period']); $entries[] = [ 'title' => $title, 'route' => route('budgets.no-budget', [$currentDate['start']->format('Y-m-d'), $currentDate['end']->format('Y-m-d')]), 'total_transactions' => count($set), 'spent' => $this->groupByCurrency($set), 'earned' => [], 'transferred_away' => [], 'transferred_in' => [], ]; } $cache->store($entries); return $entries; } /** * TODO fix the date. * * Show period overview for no category view. * * @param Carbon $theDate * * @return array * @throws FireflyException * @throws ContainerExceptionInterface * @throws NotFoundExceptionInterface */ protected function getNoCategoryPeriodOverview(Carbon $theDate): array { Log::debug(sprintf('Now in getNoCategoryPeriodOverview(%s)', $theDate->format('Y-m-d'))); $range = app('preferences')->get('viewRange', '1M')->data; $first = $this->journalRepos->firstNull(); $start = null === $first ? new Carbon() : $first->date; $end = clone $theDate; Log::debug(sprintf('Start for getNoCategoryPeriodOverview() is %s', $start->format('Y-m-d'))); Log::debug(sprintf('End for getNoCategoryPeriodOverview() is %s', $end->format('Y-m-d'))); // properties for cache $dates = app('navigation')->blockPeriods($start, $end, $range); $entries = []; // collect all expenses in this period: /** @var GroupCollectorInterface $collector */ $collector = app(GroupCollectorInterface::class); $collector->withoutCategory(); $collector->setRange($start, $end); $collector->setTypes([TransactionType::DEPOSIT]); $earnedSet = $collector->getExtractedJournals(); // collect all income in this period: /** @var GroupCollectorInterface $collector */ $collector = app(GroupCollectorInterface::class); $collector->withoutCategory(); $collector->setRange($start, $end); $collector->setTypes([TransactionType::WITHDRAWAL]); $spentSet = $collector->getExtractedJournals(); // collect all transfers in this period: /** @var GroupCollectorInterface $collector */ $collector = app(GroupCollectorInterface::class); $collector->withoutCategory(); $collector->setRange($start, $end); $collector->setTypes([TransactionType::TRANSFER]); $transferSet = $collector->getExtractedJournals(); /** @var array $currentDate */ foreach ($dates as $currentDate) { $spent = $this->filterJournalsByDate($spentSet, $currentDate['start'], $currentDate['end']); $earned = $this->filterJournalsByDate($earnedSet, $currentDate['start'], $currentDate['end']); $transferred = $this->filterJournalsByDate($transferSet, $currentDate['start'], $currentDate['end']); $title = app('navigation')->periodShow($currentDate['end'], $currentDate['period']); $entries[] = [ 'title' => $title, 'route' => route('categories.no-category', [$currentDate['start']->format('Y-m-d'), $currentDate['end']->format('Y-m-d')]), 'total_transactions' => count($spent) + count($earned) + count($transferred), 'spent' => $this->groupByCurrency($spent), 'earned' => $this->groupByCurrency($earned), 'transferred' => $this->groupByCurrency($transferred), ]; } Log::debug('End of loops'); return $entries; } /** * This shows a period overview for a tag. It goes back in time and lists all relevant transactions and sums. * * @param Tag $tag * @param Carbon $start * @param Carbon $end * * @return array * @throws FireflyException * @throws ContainerExceptionInterface * @throws NotFoundExceptionInterface */ protected function getTagPeriodOverview(Tag $tag, Carbon $start, Carbon $end): array // period overview for tags. { $range = app('preferences')->get('viewRange', '1M')->data; [$start, $end] = $end < $start ? [$end, $start] : [$start, $end]; // properties for cache $cache = new CacheProperties(); $cache->addProperty($start); $cache->addProperty($end); $cache->addProperty('tag-period-entries'); $cache->addProperty($tag->id); if ($cache->has()) { return $cache->get(); } /** @var array $dates */ $dates = app('navigation')->blockPeriods($start, $end, $range); $entries = []; // collect all expenses in this period: /** @var GroupCollectorInterface $collector */ $collector = app(GroupCollectorInterface::class); $collector->setTag($tag); $collector->setRange($start, $end); $collector->setTypes([TransactionType::DEPOSIT]); $earnedSet = $collector->getExtractedJournals(); // collect all income in this period: /** @var GroupCollectorInterface $collector */ $collector = app(GroupCollectorInterface::class); $collector->setTag($tag); $collector->setRange($start, $end); $collector->setTypes([TransactionType::WITHDRAWAL]); $spentSet = $collector->getExtractedJournals(); // collect all transfers in this period: /** @var GroupCollectorInterface $collector */ $collector = app(GroupCollectorInterface::class); $collector->setTag($tag); $collector->setRange($start, $end); $collector->setTypes([TransactionType::TRANSFER]); $transferSet = $collector->getExtractedJournals(); foreach ($dates as $currentDate) { $spent = $this->filterJournalsByDate($spentSet, $currentDate['start'], $currentDate['end']); $earned = $this->filterJournalsByDate($earnedSet, $currentDate['start'], $currentDate['end']); $transferred = $this->filterJournalsByDate($transferSet, $currentDate['start'], $currentDate['end']); $title = app('navigation')->periodShow($currentDate['end'], $currentDate['period']); $entries[] = [ 'transactions' => 0, 'title' => $title, 'route' => route( 'tags.show', [$tag->id, $currentDate['start']->format('Y-m-d'), $currentDate['end']->format('Y-m-d')] ), 'total_transactions' => count($spent) + count($earned) + count($transferred), 'spent' => $this->groupByCurrency($spent), 'earned' => $this->groupByCurrency($earned), 'transferred' => $this->groupByCurrency($transferred), ]; } return $entries; } /** * @param string $transactionType * @param Carbon $start * @param Carbon $end * * @return array * @throws FireflyException * @throws ContainerExceptionInterface * @throws NotFoundExceptionInterface */ protected function getTransactionPeriodOverview(string $transactionType, Carbon $start, Carbon $end): array { $range = app('preferences')->get('viewRange', '1M')->data; $types = config(sprintf('firefly.transactionTypesByType.%s', $transactionType)); [$start, $end] = $end < $start ? [$end, $start] : [$start, $end]; // properties for cache $cache = new CacheProperties(); $cache->addProperty($start); $cache->addProperty($end); $cache->addProperty('transactions-period-entries'); $cache->addProperty($transactionType); if ($cache->has()) { return $cache->get(); } /** @var array $dates */ $dates = app('navigation')->blockPeriods($start, $end, $range); $entries = []; // collect all journals in this period (regardless of type) $collector = app(GroupCollectorInterface::class); $collector->setTypes($types)->setRange($start, $end); $genericSet = $collector->getExtractedJournals(); foreach ($dates as $currentDate) { $spent = []; $earned = []; $transferred = []; $title = app('navigation')->periodShow($currentDate['end'], $currentDate['period']); // set to correct array if ('expenses' === $transactionType || 'withdrawal' === $transactionType) { $spent = $this->filterJournalsByDate($genericSet, $currentDate['start'], $currentDate['end']); } if ('revenue' === $transactionType || 'deposit' === $transactionType) { $earned = $this->filterJournalsByDate($genericSet, $currentDate['start'], $currentDate['end']); } if ('transfer' === $transactionType || 'transfers' === $transactionType) { $transferred = $this->filterJournalsByDate($genericSet, $currentDate['start'], $currentDate['end']); } $entries[] = [ 'title' => $title, 'route' => route('transactions.index', [$transactionType, $currentDate['start']->format('Y-m-d'), $currentDate['end']->format('Y-m-d')]), 'total_transactions' => count($spent) + count($earned) + count($transferred), 'spent' => $this->groupByCurrency($spent), 'earned' => $this->groupByCurrency($earned), 'transferred' => $this->groupByCurrency($transferred), ]; } return $entries; } }