. */ declare(strict_types=1); namespace FireflyIII\Repositories\Account; use Carbon\Carbon; use FireflyIII\Exceptions\FireflyException; use FireflyIII\Factory\AccountFactory; use FireflyIII\Models\Account; use FireflyIII\Models\AccountType; use FireflyIII\Models\TransactionJournal; use FireflyIII\Models\TransactionType; use FireflyIII\Services\Internal\Destroy\AccountDestroyService; use FireflyIII\Services\Internal\Update\AccountUpdateService; use FireflyIII\User; use Illuminate\Database\Eloquent\Relations\HasMany; use Illuminate\Support\Collection; use Log; /** * Class AccountRepository. * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class AccountRepository implements AccountRepositoryInterface { /** @var User */ private $user; /** * Constructor. */ public function __construct() { if ('testing' === env('APP_ENV')) { Log::warning(sprintf('%s should not be instantiated in the TEST environment!', \get_class($this))); } } /** * @param array $types * * @return int */ public function count(array $types): int { return $this->user->accounts()->accountTypeIn($types)->count(); } /** * Moved here from account CRUD. * * @param Account $account * @param Account|null $moveTo * * @return bool * */ public function destroy(Account $account, ?Account $moveTo): bool { /** @var AccountDestroyService $service */ $service = app(AccountDestroyService::class); $service->destroy($account, $moveTo); return true; } /** * @param string $number * @param array $types * * @return Account|null */ public function findByAccountNumber(string $number, array $types): ?Account { $query = $this->user->accounts() ->leftJoin('account_meta', 'account_meta.account_id', '=', 'accounts.id') ->where('account_meta.name', 'accountNumber') ->where('account_meta.data', json_encode($number)); if (\count($types) > 0) { $query->leftJoin('account_types', 'accounts.account_type_id', '=', 'account_types.id'); $query->whereIn('account_types.type', $types); } /** @var Collection $accounts */ $accounts = $query->get(['accounts.*']); if ($accounts->count() > 0) { return $accounts->first(); } return null; } /** * @param string $iban * @param array $types * * @return Account|null */ public function findByIbanNull(string $iban, array $types): ?Account { $query = $this->user->accounts()->where('iban', '!=', '')->whereNotNull('iban'); if (\count($types) > 0) { $query->leftJoin('account_types', 'accounts.account_type_id', '=', 'account_types.id'); $query->whereIn('account_types.type', $types); } $accounts = $query->get(['accounts.*']); /** @var Account $account */ foreach ($accounts as $account) { if ($account->iban === $iban) { return $account; } } return null; } /** * @param string $name * @param array $types * * @return Account|null */ public function findByName(string $name, array $types): ?Account { $query = $this->user->accounts(); if (\count($types) > 0) { $query->leftJoin('account_types', 'accounts.account_type_id', '=', 'account_types.id'); $query->whereIn('account_types.type', $types); } Log::debug(sprintf('Searching for account named "%s" (of user #%d) of the following type(s)', $name, $this->user->id), ['types' => $types]); $accounts = $query->get(['accounts.*']); /** @var Account $account */ foreach ($accounts as $account) { if ($account->name === $name) { Log::debug(sprintf('Found #%d (%s) with type id %d', $account->id, $account->name, $account->account_type_id)); return $account; } } Log::debug(sprintf('There is no account with name "%s" of types', $name), $types); return null; } /** * @param int $accountId * * @return Account|null */ public function findNull(int $accountId): ?Account { return $this->user->accounts()->find($accountId); } /** * Return account type or null if not found. * * @param string $type * * @return AccountType|null */ public function getAccountTypeByType(string $type): ?AccountType { return AccountType::whereType($type)->first(); } /** * @param array $accountIds * * @return Collection */ public function getAccountsById(array $accountIds): Collection { /** @var Collection $result */ $query = $this->user->accounts(); if (\count($accountIds) > 0) { $query->whereIn('accounts.id', $accountIds); } $result = $query->get(['accounts.*']); $result = $result->sortBy( function (Account $account) { return strtolower($account->name); } ); return $result; } /** * @param array $types * * @return Collection */ public function getAccountsByType(array $types): Collection { /** @var Collection $result */ $query = $this->user->accounts(); if (\count($types) > 0) { $query->accountTypeIn($types); } $result = $query->get(['accounts.*']); $result = $result->sortBy( function (Account $account) { return strtolower($account->name); } ); return $result; } /** * @param array $types * * @return Collection */ public function getActiveAccountsByType(array $types): Collection { /** @var Collection $result */ $query = $this->user->accounts()->with( ['accountmeta' => function (HasMany $query) { $query->where('name', 'accountRole'); }] ); if (\count($types) > 0) { $query->accountTypeIn($types); } $query->where('active', 1); $result = $query->get(['accounts.*']); $result = $result->sortBy( function (Account $account) { return sprintf('%02d', $account->account_type_id) . strtolower($account->name); } ); return $result; } /** * @return Account * * @throws FireflyException */ public function getCashAccount(): Account { /** @var AccountType $type */ $type = AccountType::where('type', AccountType::CASH)->first(); /** @var AccountFactory $factory */ $factory = app(AccountFactory::class); $factory->setUser($this->user); return $factory->findOrCreate('Cash account', $type->type); } /** * @param $account * * @return string */ public function getInterestPerDay(Account $account): string { $interest = $this->getMetaValue($account, 'interest'); $interestPeriod = $this->getMetaValue($account, 'interest_period'); Log::debug(sprintf('Start with interest of %s percent', $interest)); // calculate if ('monthly' === $interestPeriod) { $interest = bcdiv(bcmul($interest, '12'), '365'); // per year Log::debug(sprintf('Interest is now (monthly to daily) %s percent', $interest)); } if ('yearly' === $interestPeriod) { $interest = bcdiv($interest, '365'); // per year Log::debug(sprintf('Interest is now (yearly to daily) %s percent', $interest)); } return $interest; } /** * Return meta value for account. Null if not found. * * @param Account $account * @param string $field * * @return null|string */ public function getMetaValue(Account $account, string $field): ?string { foreach ($account->accountMeta as $meta) { if ($meta->name === $field) { return (string)$meta->data; } } return null; } /** * Get note text or null. * * @param Account $account * * @return null|string */ public function getNoteText(Account $account): ?string { $note = $account->notes()->first(); if (null === $note) { return null; } return $note->text; } /** * Returns the amount of the opening balance for this account. * * @param Account $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 (string)$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'); } /** * @param Account $account * * @return Account|null * * @throws FireflyException */ public function getReconciliation(Account $account): ?Account { if (AccountType::ASSET !== $account->accountType->type) { throw new FireflyException(sprintf('%s is not an asset account.', $account->name)); } $name = $account->name . ' reconciliation'; /** @var AccountType $type */ $type = AccountType::where('type', AccountType::RECONCILIATION)->first(); $accounts = $this->user->accounts()->where('account_type_id', $type->id)->get(); /** @var Account $current */ foreach ($accounts as $current) { if ($current->name === $name) { return $current; } } /** @var AccountFactory $factory */ $factory = app(AccountFactory::class); $factory->setUser($account->user); $account = $factory->findOrCreate($name, $type->type); return $account; } /** * @param Account $account * * @return bool */ public function isLiability(Account $account): bool { return \in_array($account->accountType->type, [AccountType::CREDITCARD, AccountType::LOAN, AccountType::DEBT, AccountType::MORTGAGE], true); } /** * Returns the date of the very last transaction in this account. * * @param Account $account * * @return TransactionJournal|null */ public function latestJournal(Account $account): ?TransactionJournal { $first = $account->transactions() ->leftJoin('transaction_journals', 'transaction_journals.id', '=', 'transactions.transaction_journal_id') ->orderBy('transaction_journals.date', 'DESC') ->orderBy('transaction_journals.order', 'ASC') ->where('transaction_journals.user_id', $this->user->id) ->orderBy('transaction_journals.id', 'DESC') ->first(['transaction_journals.id']); if (null !== $first) { return TransactionJournal::find((int)$first->id); } return null; } /** * Returns the date of the very last transaction in this account. * * @param Account $account * * @return Carbon|null */ public function latestJournalDate(Account $account): ?Carbon { $result = null; $journal = $this->latestJournal($account); if (null !== $journal) { $result = $journal->date; } return $result; } /** * Returns the date of the very first transaction in this account. * * @param Account $account * * @return TransactionJournal|null */ 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((int)$first->id); } return null; } /** * Returns the date of the very first transaction in this account. * * @param Account $account * * @return Carbon|null */ public function oldestJournalDate(Account $account): ?Carbon { $result = null; $journal = $this->oldestJournal($account); if (null !== $journal) { $result = $journal->date; } return $result; } /** * @param User $user */ public function setUser(User $user): void { $this->user = $user; } /** * @param array $data * * @return Account * @throws \FireflyIII\Exceptions\FireflyException */ public function store(array $data): Account { /** @var AccountFactory $factory */ $factory = app(AccountFactory::class); $factory->setUser($this->user); return $factory->create($data); } /** * @param Account $account * @param array $data * * @return Account * @throws \FireflyIII\Exceptions\FireflyException * @throws FireflyException * @throws FireflyException */ public function update(Account $account, array $data): Account { /** @var AccountUpdateService $service */ $service = app(AccountUpdateService::class); $account = $service->update($account, $data); return $account; } }