. */ declare(strict_types=1); namespace FireflyIII\Repositories\Account; use Carbon\Carbon; use FireflyIII\Helpers\Collector\GroupCollectorInterface; use FireflyIII\Models\TransactionCurrency; use FireflyIII\Models\TransactionType; use FireflyIII\User; use Illuminate\Support\Collection; /** * * Class OperationsRepository */ class OperationsRepository implements OperationsRepositoryInterface { private User $user; /** * This method returns a list of all the withdrawal transaction journals (as arrays) set in that period * which have the specified accounts. It's grouped per currency, with as few details in the array * as possible. Amounts are always negative. * * @param Carbon $start * @param Carbon $end * @param Collection $accounts * * @return array */ public function listExpenses(Carbon $start, Carbon $end, Collection $accounts): array { $journals = $this->getTransactions($start, $end, $accounts, TransactionType::WITHDRAWAL); return $this->sortByCurrency($journals, 'negative'); } /** * This method returns a list of all the deposit transaction journals (as arrays) set in that period * which have the specified accounts. It's grouped per currency, with as few details in the array * as possible. Amounts are always positive. * * @param Carbon $start * @param Carbon $end * @param Collection|null $accounts * * @return array */ public function listIncome(Carbon $start, Carbon $end, ?Collection $accounts = null): array { $journals = $this->getTransactions($start, $end, $accounts, TransactionType::DEPOSIT); return $this->sortByCurrency($journals, 'positive'); } /** * @param User $user */ public function setUser(User $user): void { $this->user = $user; } /** * @inheritDoc */ public function sumExpenses(Carbon $start, Carbon $end, ?Collection $accounts = null, ?Collection $expense = null, ?TransactionCurrency $currency = null ): array { $journals = $this->getTransactionsForSum($start, $end, $accounts, $expense, $currency, TransactionType::WITHDRAWAL); return $this->groupByCurrency($journals, 'negative'); } /** * @inheritDoc */ public function sumExpensesByDestination( Carbon $start, Carbon $end, ?Collection $accounts = null, ?Collection $expense = null, ?TransactionCurrency $currency = null ): array { $journals = $this->getTransactionsForSum($start, $end, $accounts, $expense, $currency, TransactionType::WITHDRAWAL); return $this->groupByDirection($journals, 'destination', 'negative'); } /** * @inheritDoc */ public function sumExpensesBySource( Carbon $start, Carbon $end, ?Collection $accounts = null, ?Collection $expense = null, ?TransactionCurrency $currency = null ): array { $journals = $this->getTransactionsForSum($start, $end, $accounts, $expense, $currency, TransactionType::WITHDRAWAL); return $this->groupByDirection($journals, 'source', 'negative'); } /** * @inheritDoc */ public function sumIncome(Carbon $start, Carbon $end, ?Collection $accounts = null, ?Collection $revenue = null, ?TransactionCurrency $currency = null ): array { $journals = $this->getTransactionsForSum($start, $end, $accounts, $revenue, $currency, TransactionType::DEPOSIT); return $this->groupByCurrency($journals, 'positive'); } /** * @inheritDoc */ public function sumIncomeByDestination( Carbon $start, Carbon $end, ?Collection $accounts = null, ?Collection $revenue = null, ?TransactionCurrency $currency = null ): array { $journals = $this->getTransactionsForSum($start, $end, $accounts, $revenue, $currency, TransactionType::DEPOSIT); return $this->groupByDirection($journals, 'destination', 'positive'); } /** * @inheritDoc */ public function sumIncomeBySource( Carbon $start, Carbon $end, ?Collection $accounts = null, ?Collection $revenue = null, ?TransactionCurrency $currency = null ): array { $journals = $this->getTransactionsForSum($start, $end, $accounts, $revenue, $currency, TransactionType::DEPOSIT); return $this->groupByDirection($journals, 'source', 'positive'); } /** * @inheritDoc */ public function sumTransfers(Carbon $start, Carbon $end, ?Collection $accounts = null, ?TransactionCurrency $currency = null): array { $journals = $this->getTransactionsForSum($start, $end, $accounts, null, $currency, TransactionType::TRANSFER); return $this->groupByEither($journals); } /** * Collect transactions with some parameters * * @param Carbon $start * @param Carbon $end * @param Collection $accounts * @param string $type * * @return array */ private function getTransactions(Carbon $start, Carbon $end, Collection $accounts, string $type): array { /** @var GroupCollectorInterface $collector */ $collector = app(GroupCollectorInterface::class); $collector->setUser($this->user)->setRange($start, $end)->setTypes([$type]); $collector->setBothAccounts($accounts); $collector->withCategoryInformation()->withAccountInformation()->withBudgetInformation()->withTagInformation(); return $collector->getExtractedJournals(); } /** * @param array $journals * @param string $direction * * @return array */ private function sortByCurrency(array $journals, string $direction): array { $array = []; foreach ($journals as $journal) { $currencyId = (int)$journal['currency_id']; $journalId = (int)$journal['transaction_journal_id']; $array[$currencyId] = $array[$currencyId] ?? [ 'currency_id' => $journal['currency_id'], 'currency_name' => $journal['currency_name'], 'currency_symbol' => $journal['currency_symbol'], 'currency_code' => $journal['currency_code'], 'currency_decimal_places' => $journal['currency_decimal_places'], 'transaction_journals' => [], ]; $array[$currencyId]['transaction_journals'][$journalId] = [ 'amount' => app('steam')->$direction((string)$journal['amount']), 'date' => $journal['date'], 'transaction_journal_id' => $journalId, 'budget_name' => $journal['budget_name'], 'category_name' => $journal['category_name'], 'source_account_id' => $journal['source_account_id'], 'source_account_name' => $journal['source_account_name'], 'source_account_iban' => $journal['source_account_iban'], 'destination_account_id' => $journal['destination_account_id'], 'destination_account_name' => $journal['destination_account_name'], 'destination_account_iban' => $journal['destination_account_iban'], 'tags' => $journal['tags'], 'description' => $journal['description'], 'transaction_group_id' => $journal['transaction_group_id'], ]; } return $array; } /** * @param Carbon $start * @param Carbon $end * @param Collection|null $accounts * @param Collection|null $opposing * @param TransactionCurrency|null $currency * @param string $type * * @return array */ private function getTransactionsForSum( Carbon $start, Carbon $end, ?Collection $accounts = null, ?Collection $opposing = null, ?TransactionCurrency $currency = null, string $type ): array { $start->startOfDay(); $end->endOfDay(); /** @var GroupCollectorInterface $collector */ $collector = app(GroupCollectorInterface::class); $collector->setUser($this->user)->setRange($start, $end)->setTypes([$type])->withAccountInformation(); // depends on transaction type: if (TransactionType::WITHDRAWAL === $type) { if (null !== $accounts) { $collector->setSourceAccounts($accounts); } if (null !== $opposing) { $collector->setDestinationAccounts($opposing); } } if (TransactionType::DEPOSIT === $type) { if (null !== $accounts) { $collector->setDestinationAccounts($accounts); } if (null !== $opposing) { $collector->setSourceAccounts($opposing); } } if (null !== $currency) { $collector->setCurrency($currency); } $journals = $collector->getExtractedJournals(); // same but for foreign currencies: if (null !== $currency) { /** @var GroupCollectorInterface $collector */ $collector = app(GroupCollectorInterface::class); $collector->setUser($this->user)->setRange($start, $end)->setTypes([$type])->withAccountInformation() ->setForeignCurrency($currency); if (TransactionType::WITHDRAWAL === $type) { if (null !== $accounts) { $collector->setSourceAccounts($accounts); } if (null !== $opposing) { $collector->setDestinationAccounts($opposing); } } if (TransactionType::DEPOSIT === $type) { if (null !== $accounts) { $collector->setDestinationAccounts($accounts); } if (null !== $opposing) { $collector->setSourceAccounts($opposing); } } $result = $collector->getExtractedJournals(); // do not use array_merge because you want keys to overwrite (otherwise you get double results): $journals = $result + $journals; } return $journals; } /** * @param array $journals * @param string $direction * * @return array */ private function groupByCurrency(array $journals, string $direction): array { $array = []; foreach ($journals as $journal) { $currencyId = (int)$journal['currency_id']; $array[$currencyId] = $array[$currencyId] ?? [ 'sum' => '0', 'currency_id' => $currencyId, 'currency_name' => $journal['currency_name'], 'currency_symbol' => $journal['currency_symbol'], 'currency_code' => $journal['currency_code'], 'currency_decimal_places' => $journal['currency_decimal_places'], ]; $array[$currencyId]['sum'] = bcadd($array[$currencyId]['sum'], app('steam')->$direction($journal['amount'])); // also do foreign amount: $foreignId = (int)$journal['foreign_currency_id']; if (0 !== $foreignId) { $array[$foreignId] = $array[$foreignId] ?? [ 'sum' => '0', 'currency_id' => $foreignId, 'currency_name' => $journal['foreign_currency_name'], 'currency_symbol' => $journal['foreign_currency_symbol'], 'currency_code' => $journal['foreign_currency_code'], 'currency_decimal_places' => $journal['foreign_currency_decimal_places'], ]; $array[$foreignId]['sum'] = bcadd($array[$foreignId]['sum'], app('steam')->$direction($journal['foreign_amount'])); } } return $array; } /** * @param array $journals * @param string $direction * @param string $method * * @return array */ private function groupByDirection(array $journals, string $direction, string $method): array { $array = []; $idKey = sprintf('%s_account_id', $direction); $nameKey = sprintf('%s_account_name', $direction); foreach ($journals as $journal) { $key = sprintf('%s-%s', $journal[$idKey], $journal['currency_id']); $array[$key] = $array[$key] ?? [ 'id' => $journal[$idKey], 'name' => $journal[$nameKey], 'sum' => '0', 'currency_id' => $journal['currency_id'], 'currency_name' => $journal['currency_name'], 'currency_symbol' => $journal['currency_symbol'], 'currency_code' => $journal['currency_code'], 'currency_decimal_places' => $journal['currency_decimal_places'], ]; $array[$key]['sum'] = bcadd($array[$key]['sum'], app('steam')->$method((string)$journal['amount'])); // also do foreign amount: if (0 !== (int)$journal['foreign_currency_id']) { $key = sprintf('%s-%s', $journal[$idKey], $journal['foreign_currency_id']); $array[$key] = $array[$key] ?? [ 'id' => $journal[$idKey], 'name' => $journal[$nameKey], 'sum' => '0', 'currency_id' => $journal['foreign_currency_id'], 'currency_name' => $journal['foreign_currency_name'], 'currency_symbol' => $journal['foreign_currency_symbol'], 'currency_code' => $journal['foreign_currency_code'], 'currency_decimal_places' => $journal['foreign_currency_decimal_places'], ]; $array[$key]['sum'] = bcadd($array[$key]['sum'], app('steam')->$method((string)$journal['foreign_amount'])); } } return $array; } /** * @param array $journals * * @return array */ private function groupByEither(array $journals): array { $return = []; /** @var array $journal */ foreach ($journals as $journal) { $return = $this->groupByEitherJournal($return, $journal); } $final = []; foreach ($return as $array) { $array['difference_float'] = (float)$array['difference']; $array['in_float'] = (float)$array['in']; $array['out_float'] = (float)$array['out']; $final[] = $array; } return $final; } /** * @param array $return * @param array $journal * * @return array */ private function groupByEitherJournal(array $return, array $journal): array { $sourceId = $journal['source_account_id']; $destinationId = $journal['destination_account_id']; $currencyId = $journal['currency_id']; $sourceKey = sprintf('%d-%d', $currencyId, $sourceId); $destKey = sprintf('%d-%d', $currencyId, $destinationId); $amount = app('steam')->positive($journal['amount']); // source first $return[$sourceKey] = $return[$sourceKey] ?? [ 'id' => (string)$sourceId, 'name' => $journal['source_account_name'], 'difference' => '0', 'difference_float' => 0, 'in' => '0', 'in_float' => 0, 'out' => '0', 'out_float' => 0, 'currency_id' => (string)$currencyId, 'currency_code' => $journal['currency_code'], ]; // dest next: $return[$destKey] = $return[$destKey] ?? [ 'id' => (string)$destinationId, 'name' => $journal['destination_account_name'], 'difference' => '0', 'difference_float' => 0, 'in' => '0', 'in_float' => 0, 'out' => '0', 'out_float' => 0, 'currency_id' => (string)$currencyId, 'currency_code' => $journal['currency_code'], ]; // source account? money goes out! $return[$sourceKey]['out'] = bcadd($return[$sourceKey]['out'], app('steam')->negative($amount)); $return[$sourceKey]['difference'] = bcadd($return[$sourceKey]['out'], $return[$sourceKey]['in']); // destination account? money comes in: $return[$destKey]['in'] = bcadd($return[$destKey]['in'], $amount); $return[$destKey]['difference'] = bcadd($return[$destKey]['out'], $return[$destKey]['in']); // foreign currency if (null !== $journal['foreign_currency_id'] && null !== $journal['foreign_amount']) { $currencyId = $journal['foreign_currency_id']; $sourceKey = sprintf('%d-%d', $currencyId, $sourceId); $destKey = sprintf('%d-%d', $currencyId, $destinationId); $amount = app('steam')->positive($journal['foreign_amount']); // same as above: // source first $return[$sourceKey] = $return[$sourceKey] ?? [ 'id' => (string)$sourceId, 'name' => $journal['source_account_name'], 'difference' => '0', 'difference_float' => 0, 'in' => '0', 'in_float' => 0, 'out' => '0', 'out_float' => 0, 'currency_id' => (string)$currencyId, 'currency_code' => $journal['foreign_currency_code'], ]; // dest next: $return[$destKey] = $return[$destKey] ?? [ 'id' => (string)$destinationId, 'name' => $journal['destination_account_name'], 'difference' => '0', 'difference_float' => 0, 'in' => '0', 'in_float' => 0, 'out' => '0', 'out_float' => 0, 'currency_id' => (string)$currencyId, 'currency_code' => $journal['foreign_currency_code'], ]; // source account? money goes out! (same as above) $return[$sourceKey]['out'] = bcadd($return[$sourceKey]['out'], app('steam')->negative($amount)); $return[$sourceKey]['difference'] = bcadd($return[$sourceKey]['out'], $return[$sourceKey]['in']); // destination account? money comes in: $return[$destKey]['in'] = bcadd($return[$destKey]['in'], $amount); $return[$destKey]['difference'] = bcadd($return[$destKey]['out'], $return[$destKey]['in']); } return $return; } }