. */ declare(strict_types=1); namespace FireflyIII\Support\Http\Controllers; use Carbon\Carbon; use FireflyIII\Helpers\Collector\GroupCollectorInterface; use FireflyIII\Helpers\Collector\GroupSumCollectorInterface; use FireflyIII\Models\Account; use FireflyIII\Models\Category; use FireflyIII\Models\Tag; use FireflyIII\Models\Transaction; use FireflyIII\Models\TransactionCurrency; use FireflyIII\Models\TransactionType; use FireflyIII\Repositories\Account\AccountRepositoryInterface; use FireflyIII\Repositories\Category\CategoryRepositoryInterface; use FireflyIII\Repositories\Journal\JournalRepositoryInterface; use FireflyIII\Repositories\Tag\TagRepositoryInterface; use FireflyIII\Support\CacheProperties; use Illuminate\Support\Collection; use Log; /** * Trait PeriodOverview. * * TODO verify this all works as expected. * * - Group expenses, income, etc. under this period. * - Returns collection of arrays. Possible fields are: * - start (string), * end (string), * title (string), * spent (string), * earned (string), * transferred (string) * * */ trait PeriodOverview { /** * 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. * * The method has been refactored recently for better performance. * * @param Account $account The account involved * @param Carbon $date The start date. * * @return Collection */ protected function getAccountPeriodOverview(Account $account, Carbon $date): Collection { /** @var AccountRepositoryInterface $repository */ $repository = app(AccountRepositoryInterface::class); $range = app('preferences')->get('viewRange', '1M')->data; $end = $repository->oldestJournalDate($account) ?? Carbon::now()->subMonth()->startOfMonth(); $start = clone $date; if ($end < $start) { [$start, $end] = [$end, $start]; // @codeCoverageIgnore } // 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(); // @codeCoverageIgnore } /** @var array $dates */ $dates = app('navigation')->blockPeriods($start, $end, $range); $entries = new Collection; // loop dates foreach ($dates as $currentDate) { // collect from start to end: /** @var GroupCollectorInterface $collector */ $collector = app(GroupCollectorInterface::class); $collector->setAccounts(new Collection([$account])); $collector->setRange($currentDate['start'], $currentDate['end']); $collector->setTypes([TransactionType::DEPOSIT]); $earnedSet = $collector->getExtractedJournals(); $earned = $this->groupByCurrency($earnedSet); /** @var GroupCollectorInterface $collector */ $collector = app(GroupCollectorInterface::class); $collector->setAccounts(new Collection([$account])); $collector->setRange($currentDate['start'], $currentDate['end']); $collector->setTypes([TransactionType::WITHDRAWAL]); $spentSet = $collector->getExtractedJournals(); $spent = $this->groupByCurrency($spentSet); $title = app('navigation')->periodShow($currentDate['start'], $currentDate['period']); /** @noinspection PhpUndefinedMethodInspection */ $entries->push( [ 'transactions' => count($spentSet) + count($earnedSet), 'title' => $title, 'spent' => $spent, 'earned' => $earned, 'transferred' => '0', 'route' => route('accounts.show', [$account->id, $currentDate['start']->format('Y-m-d'), $currentDate['end']->format('Y-m-d')]), ] ); } //$cache->store($entries); return $entries; } /** * @param array $journals * * @return array */ private function groupByCurrency(array $journals): array { $return = []; /** @var array $journal */ foreach ($journals as $journal) { $currencyId = (int)$journal['currency_id']; if (!isset($return[$currencyId])) { $currency = new TransactionCurrency; $currency->symbol = $journal['currency_symbol']; $currency->decimal_places = $journal['currency_decimal_places']; $currency->name = $journal['currency_name']; $return[$currencyId] = [ 'amount' => '0', 'currency' => $currency, //'currency' => 'x',//$currency, ]; } $return[$currencyId]['amount'] = bcadd($return[$currencyId]['amount'], $journal['amount']); } return $return; } /** * Overview for single category. Has been refactored recently. * * @param Category $category * @param Carbon $date * * @return Collection */ protected function getCategoryPeriodOverview(Category $category, Carbon $date): Collection { /** @var JournalRepositoryInterface $journalRepository */ $journalRepository = app(JournalRepositoryInterface::class); $range = app('preferences')->get('viewRange', '1M')->data; $first = $journalRepository->firstNull(); $end = null === $first ? new Carbon : $first->date; $start = clone $date; if ($end < $start) { [$start, $end] = [$end, $start]; // @codeCoverageIgnore } // 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(); // @codeCoverageIgnore } /** @var array $dates */ $dates = app('navigation')->blockPeriods($start, $end, $range); $entries = new Collection; /** @var CategoryRepositoryInterface $categoryRepository */ $categoryRepository = app(CategoryRepositoryInterface::class); foreach ($dates as $currentDate) { $spent = $categoryRepository->spentInPeriodCollection(new Collection([$category]), new Collection, $currentDate['start'], $currentDate['end']); $earned = $categoryRepository->earnedInPeriodCollection(new Collection([$category]), new Collection, $currentDate['start'], $currentDate['end']); $spent = $this->groupByCurrency($spent); $earned = $this->groupByCurrency($earned); // amount transferred /** @var GroupCollectorInterface $collector */ $collector = app(GroupCollectorInterface::class); $collector->setRange($currentDate['start'], $currentDate['end'])->setCategory($category) ->setTypes([TransactionType::TRANSFER]); $transferred = $this->groupByCurrency($collector->getExtractedJournals()); $title = app('navigation')->periodShow($currentDate['end'], $currentDate['period']); $entries->push( [ 'transactions' => 0, 'title' => $title, 'spent' => $spent, 'earned' => $earned, 'transferred' => $transferred, 'route' => route('categories.show', [$category->id, $currentDate['start']->format('Y-m-d'), $currentDate['end']->format('Y-m-d')]), ] ); } $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 $date * * @return Collection */ protected function getNoBudgetPeriodOverview(Carbon $date): Collection { /** @var JournalRepositoryInterface $repository */ $repository = app(JournalRepositoryInterface::class); $first = $repository->firstNull(); $end = null === $first ? new Carbon : $first->date; $start = clone $date; $range = app('preferences')->get('viewRange', '1M')->data; if ($end < $start) { [$start, $end] = [$end, $start]; // @codeCoverageIgnore } $cache = new CacheProperties; $cache->addProperty($start); $cache->addProperty($end); $cache->addProperty('no-budget-period-entries'); if ($cache->has()) { return $cache->get(); // @codeCoverageIgnore } /** @var array $dates */ $dates = app('navigation')->blockPeriods($start, $end, $range); $entries = new Collection; foreach ($dates as $currentDate) { /** @var GroupCollectorInterface $collector */ $collector = app(GroupCollectorInterface::class); $collector->setRange($currentDate['start'], $currentDate['end'])->withoutBudget()->withAccountInformation()->setTypes( [TransactionType::WITHDRAWAL] ); $journals = $collector->getExtractedJournals(); $count = count($journals); $spent = $this->groupByCurrency($journals); $title = app('navigation')->periodShow($currentDate['end'], $currentDate['period']); $entries->push( [ 'transactions' => $count, 'title' => $title, 'spent' => $spent, 'earned' => '0', 'transferred' => '0', 'route' => route('budgets.no-budget', [$currentDate['start']->format('Y-m-d'), $currentDate['end']->format('Y-m-d')]), ] ); } $cache->store($entries); return $entries; } /** * TODO has to be synced with the others. * * Show period overview for no category view. * * @param Carbon $theDate * * @return Collection * @SuppressWarnings(PHPMD.ExcessiveMethodLength) */ protected function getNoCategoryPeriodOverview(Carbon $theDate): Collection // period overview method. { 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 = $theDate ?? new Carbon; 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 $cache = new CacheProperties; $cache->addProperty($start); $cache->addProperty($end); $cache->addProperty('no-category-period-entries'); if ($cache->has()) { return $cache->get(); // @codeCoverageIgnore } $dates = app('navigation')->blockPeriods($start, $end, $range); $entries = new Collection; foreach ($dates as $date) { // count journals without category in this period: /** @var GroupCollectorInterface $collector */ $collector = app(GroupCollectorInterface::class); $collector->setRange($date['start'], $date['end'])->withoutCategory() ->setTypes([TransactionType::WITHDRAWAL, TransactionType::DEPOSIT, TransactionType::TRANSFER]); $count = count($collector->getExtractedJournals()); // amount transferred /** @var GroupCollectorInterface $collector */ $collector = app(GroupCollectorInterface::class); $collector->setRange($date['start'], $date['end'])->withoutCategory() ->setTypes([TransactionType::TRANSFER]); $transferred = app('steam')->positive((string)$collector->getSum()); // amount spent /** @var GroupCollectorInterface $collector */ $collector = app(GroupCollectorInterface::class); $collector->setRange($date['start'], $date['end'])->withoutCategory()->setTypes( [TransactionType::WITHDRAWAL] ); $spent = $collector->getSum(); // amount earned /** @var GroupCollectorInterface $collector */ $collector = app(GroupCollectorInterface::class); $collector->setRange($date['start'], $date['end'])->withoutCategory()->setTypes( [TransactionType::DEPOSIT] ); $earned = $collector->getSum(); /** @noinspection PhpUndefinedMethodInspection */ $dateStr = $date['end']->format('Y-m-d'); $dateName = app('navigation')->periodShow($date['end'], $date['period']); $entries->push( [ 'string' => $dateStr, 'name' => $dateName, 'count' => $count, 'spent' => $spent, 'earned' => $earned, 'transferred' => $transferred, 'start' => clone $date['start'], 'end' => clone $date['end'], ] ); } Log::debug('End of loops'); $cache->store($entries); 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 $date * * @return Collection */ protected function getTagPeriodOverview(Tag $tag, Carbon $date): Collection // period overview for tags. { /** @var TagRepositoryInterface $repository */ $repository = app(TagRepositoryInterface::class); $range = app('preferences')->get('viewRange', '1M')->data; /** @var Carbon $end */ $start = clone $date; $end = $repository->firstUseDate($tag) ?? new Carbon; if ($end < $start) { [$start, $end] = [$end, $start]; // @codeCoverageIgnore } // properties for entries with their amounts. $cache = new CacheProperties; $cache->addProperty($start); $cache->addProperty($end); $cache->addProperty('tag-period-entries'); $cache->addProperty($tag->id); if ($cache->has()) { return $cache->get(); // @codeCoverageIgnore } /** @var array $dates */ $dates = app('navigation')->blockPeriods($start, $end, $range); $entries = new Collection; // while end larger or equal to start foreach ($dates as $currentDate) { $spentSet = $repository->expenseInPeriod($tag, $currentDate['start'], $currentDate['end']); $spent = $this->groupByCurrency($spentSet); $earnedSet = $repository->incomeInPeriod($tag, $currentDate['start'], $currentDate['end']); $earned = $this->groupByCurrency($earnedSet); $transferredSet = $repository->transferredInPeriod($tag, $currentDate['start'], $currentDate['end']); $transferred = $this->groupByCurrency($transferredSet); $title = app('navigation')->periodShow($currentDate['end'], $currentDate['period']); $entries->push( [ 'transactions' => count($spentSet) + count($earnedSet) + count($transferredSet), 'title' => $title, 'spent' => $spent, 'earned' => $earned, 'transferred' => $transferred, 'route' => route('tags.show', [$tag->id, $currentDate['start']->format('Y-m-d'), $currentDate['end']->format('Y-m-d')]), ] ); } $cache->store($entries); return $entries; } /** * @param string $transactionType * @param Carbon $endDate * * @return Collection */ protected function getTransactionPeriodOverview(string $transactionType, Carbon $endDate): Collection { /** @var JournalRepositoryInterface $repository */ $repository = app(JournalRepositoryInterface::class); $range = app('preferences')->get('viewRange', '1M')->data; $endJournal = $repository->firstNull(); $end = null === $endJournal ? new Carbon : $endJournal->date; $start = clone $endDate; $types = config('firefly.transactionTypesByType.' . $transactionType); if ($end < $start) { [$start, $end] = [$end, $start]; // @codeCoverageIgnore } /** @var array $dates */ $dates = app('navigation')->blockPeriods($start, $end, $range); $entries = new Collection; foreach ($dates as $currentDate) { /** @var GroupSumCollectorInterface $sumCollector */ $sumCollector = app(GroupSumCollectorInterface::class); $sumCollector->setTypes($types)->setRange($currentDate['start'], $currentDate['end']); $amounts = $sumCollector->getSum(); $spent = []; $earned = []; $transferred = []; // set to correct array if ('expenses' === $transactionType || 'withdrawal' === $transactionType) { $spent = $amounts; } if ('revenue' === $transactionType || 'deposit' === $transactionType) { $earned = $amounts; } if ('transfer' === $transactionType || 'transfers' === $transactionType) { $transferred = $amounts; } $title = app('navigation')->periodShow($currentDate['end'], $currentDate['period']); $entries->push( [ 'transactions' => $amounts['count'], 'title' => $title, 'spent' => $spent, 'earned' => $earned, 'transferred' => $transferred, 'route' => route( 'transactions.index', [$transactionType, $currentDate['start']->format('Y-m-d'), $currentDate['end']->format('Y-m-d')] ), ] ); } return $entries; } /** * Collect the sum per currency. * * @param Collection $collection * * @return array */ protected function sumPerCurrency(Collection $collection): array // helper for transactions (math, calculations) { $return = []; /** @var Transaction $transaction */ foreach ($collection as $transaction) { $currencyId = (int)$transaction->transaction_currency_id; // save currency information: if (!isset($return[$currencyId])) { $currencySymbol = $transaction->transaction_currency_symbol; $decimalPlaces = $transaction->transaction_currency_dp; $currencyCode = $transaction->transaction_currency_code; $return[$currencyId] = [ 'currency' => [ 'id' => $currencyId, 'code' => $currencyCode, 'symbol' => $currencySymbol, 'dp' => $decimalPlaces, ], 'sum' => '0', 'count' => 0, ]; } // save amount: $return[$currencyId]['sum'] = bcadd($return[$currencyId]['sum'], $transaction->transaction_amount); ++$return[$currencyId]['count']; } asort($return); return $return; } }