. */ declare(strict_types=1); namespace FireflyIII\Repositories\Account; use Carbon\Carbon; use DB; use FireflyIII\Factory\AccountFactory; use FireflyIII\Models\Account; use FireflyIII\Models\AccountType; use FireflyIII\Models\Category; use FireflyIII\Models\Note; use FireflyIII\Models\Tag; use FireflyIII\Models\Transaction; use FireflyIII\Models\TransactionJournal; use FireflyIII\Models\TransactionType; use FireflyIII\Repositories\Tag\TagRepositoryInterface; use FireflyIII\Services\Internal\Destroy\AccountDestroyService; use FireflyIII\Services\Internal\Update\AccountUpdateService; use FireflyIII\User; use Log; /** * Class AccountRepository. */ class AccountRepository implements AccountRepositoryInterface { /** @var User */ private $user; /** @var array */ private $validAssetFields = ['accountRole', 'accountNumber', 'currency_id', 'BIC']; use FindAccountsTrait; /** @var array */ private $validCCFields = ['accountRole', 'ccMonthlyPaymentDate', 'ccType', 'accountNumber', 'currency_id', 'BIC']; /** @var array */ private $validFields = ['accountNumber', 'currency_id', 'BIC']; /** * Moved here from account CRUD. * * @param array $types * * @return int */ public function count(array $types): int { $count = $this->user->accounts()->accountTypeIn($types)->count(); return $count; } /** * Moved here from account CRUD. * * @param Account $account * @param Account $moveTo * * @return bool * * @throws \Exception */ public function destroy(Account $account, Account $moveTo): bool { /** @var AccountDestroyService $service */ $service = app(AccountDestroyService::class); $service->destroy($account, $moveTo); return true; } /** * @param int $accountId * * @return Account|null */ public function findNull(int $accountId): ?Account { return $this->user->accounts()->find($accountId); } /** * Return account type by string. * * @param string $type * * @return AccountType|null */ public function getAccountType(string $type): ?AccountType { return AccountType::whereType($type)->first(); } /** * @param Account $account * * @return Note|null */ public function getNote(Account $account): ?Note { return $account->notes()->first(); } /** * Returns the amount of the opening balance for this account. * * @return string */ public function getOpeningBalanceAmount(Account $account): ?string { $journal = TransactionJournal::leftJoin('transactions', 'transactions.transaction_journal_id', '=', 'transaction_journals.id') ->where('transactions.account_id', $account->id) ->transactionTypes([TransactionType::OPENING_BALANCE]) ->first(['transaction_journals.*']); if (null === $journal) { return null; } $transaction = $journal->transactions()->where('account_id', $account->id)->first(); if (null === $transaction) { return null; } return strval($transaction->amount); } /** * Return date of opening balance as string or null. * * @param Account $account * * @return null|string */ public function getOpeningBalanceDate(Account $account): ?string { $journal = TransactionJournal::leftJoin('transactions', 'transactions.transaction_journal_id', '=', 'transaction_journals.id') ->where('transactions.account_id', $account->id) ->transactionTypes([TransactionType::OPENING_BALANCE]) ->first(['transaction_journals.*']); if (null === $journal) { return null; } return $journal->date->format('Y-m-d'); } /** * Returns the date of the very last transaction in this account. * * @param Account $account * * @return Carbon */ public function newestJournalDate(Account $account): Carbon { $last = new Carbon; $date = $account->transactions() ->leftJoin('transaction_journals', 'transaction_journals.id', '=', 'transactions.transaction_journal_id') ->orderBy('transaction_journals.date', 'DESC') ->orderBy('transaction_journals.order', 'ASC') ->orderBy('transaction_journals.id', 'DESC') ->first(['transaction_journals.date']); if (null !== $date) { $last = new Carbon($date->date); } return $last; } /** * Returns the date of the very first transaction in this account. * * @param Account $account * * @return TransactionJournal */ public function oldestJournal(Account $account): TransactionJournal { $first = $account->transactions() ->leftJoin('transaction_journals', 'transaction_journals.id', '=', 'transactions.transaction_journal_id') ->orderBy('transaction_journals.date', 'ASC') ->orderBy('transaction_journals.order', 'DESC') ->where('transaction_journals.user_id', $this->user->id) ->orderBy('transaction_journals.id', 'ASC') ->first(['transaction_journals.id']); if (null !== $first) { return TransactionJournal::find(intval($first->id)); } return new TransactionJournal(); } /** * Returns the date of the very first transaction in this account. * * @param Account $account * * @return Carbon */ public function oldestJournalDate(Account $account): Carbon { $journal = $this->oldestJournal($account); if (null === $journal->id) { return new Carbon; } return $journal->date; } /** * @param User $user */ public function setUser(User $user) { $this->user = $user; } /** * @param array $data * * @return Account */ public function store(array $data): Account { /** @var AccountFactory $factory */ $factory = app(AccountFactory::class); $factory->setUser($this->user); $account = $factory->create($data); return $account; } /** * @param Account $account * @param array $data * * @return Account */ public function update(Account $account, array $data): Account { /** @var AccountUpdateService $service */ $service = app(AccountUpdateService::class); $account = $service->update($account, $data); return $account; } /** * @param TransactionJournal $journal * @param array $data * * @return TransactionJournal */ public function updateReconciliation(TransactionJournal $journal, array $data): TransactionJournal { // update journal // update actual journal: $data['amount'] = strval($data['amount']); // unlink all categories, recreate them: $journal->categories()->detach(); $this->storeCategoryWithJournal($journal, strval($data['category'])); // update amounts /** @var Transaction $transaction */ foreach ($journal->transactions as $transaction) { $transaction->amount = bcmul($data['amount'], '-1'); if (AccountType::ASSET === $transaction->account->accountType->type) { $transaction->amount = $data['amount']; } $transaction->save(); } $journal->save(); // update tags: if (isset($data['tags']) && is_array($data['tags'])) { $this->updateTags($journal, $data['tags']); } return $journal; } /** * @param TransactionJournal $journal * @param string $category */ protected function storeCategoryWithJournal(TransactionJournal $journal, string $category) { if (strlen($category) > 0) { $category = Category::firstOrCreateEncrypted(['name' => $category, 'user_id' => $journal->user_id]); $journal->categories()->save($category); } } /** * @param TransactionJournal $journal * @param array $array * * @return bool */ protected function updateTags(TransactionJournal $journal, array $array): bool { // create tag repository /** @var TagRepositoryInterface $tagRepository */ $tagRepository = app(TagRepositoryInterface::class); // find or create all tags: $tags = []; $ids = []; foreach ($array as $name) { if (strlen(trim($name)) > 0) { $tag = Tag::firstOrCreateEncrypted(['tag' => $name, 'user_id' => $journal->user_id]); $tags[] = $tag; $ids[] = $tag->id; } } // delete all tags connected to journal not in this array: if (count($ids) > 0) { DB::table('tag_transaction_journal')->where('transaction_journal_id', $journal->id)->whereNotIn('tag_id', $ids)->delete(); } // if count is zero, delete them all: if (0 === count($ids)) { DB::table('tag_transaction_journal')->where('transaction_journal_id', $journal->id)->delete(); } // connect each tag to journal (if not yet connected): /** @var Tag $tag */ foreach ($tags as $tag) { Log::debug(sprintf('Will try to connect tag #%d to journal #%d.', $tag->id, $journal->id)); $tagRepository->connect($journal, $tag); } return true; } }