mirror of
				https://github.com/firefly-iii/firefly-iii.git
				synced 2025-11-03 20:55:05 +00:00 
			
		
		
		
	
		
			
				
	
	
		
			520 lines
		
	
	
		
			16 KiB
		
	
	
	
		
			PHP
		
	
	
	
	
	
			
		
		
	
	
			520 lines
		
	
	
		
			16 KiB
		
	
	
	
		
			PHP
		
	
	
	
	
	
<?php
 | 
						|
/**
 | 
						|
 * AccountRepository.php
 | 
						|
 * Copyright (C) 2016 thegrumpydictator@gmail.com
 | 
						|
 *
 | 
						|
 * This software may be modified and distributed under the terms of the
 | 
						|
 * Creative Commons Attribution-ShareAlike 4.0 International License.
 | 
						|
 *
 | 
						|
 * See the LICENSE file for details.
 | 
						|
 */
 | 
						|
 | 
						|
declare(strict_types=1);
 | 
						|
 | 
						|
 | 
						|
namespace FireflyIII\Repositories\Account;
 | 
						|
 | 
						|
use Carbon\Carbon;
 | 
						|
use DB;
 | 
						|
use FireflyIII\Exceptions\FireflyException;
 | 
						|
use FireflyIII\Models\Account;
 | 
						|
use FireflyIII\Models\AccountMeta;
 | 
						|
use FireflyIII\Models\AccountType;
 | 
						|
use FireflyIII\Models\Transaction;
 | 
						|
use FireflyIII\Models\TransactionJournal;
 | 
						|
use FireflyIII\Models\TransactionType;
 | 
						|
use FireflyIII\User;
 | 
						|
use Log;
 | 
						|
use Validator;
 | 
						|
 | 
						|
 | 
						|
/**
 | 
						|
 *
 | 
						|
 * Class AccountRepository
 | 
						|
 *
 | 
						|
 * @package FireflyIII\Repositories\Account
 | 
						|
 */
 | 
						|
class AccountRepository implements AccountRepositoryInterface
 | 
						|
{
 | 
						|
    use FindAccountsTrait;
 | 
						|
 | 
						|
    /** @var User */
 | 
						|
    private $user;
 | 
						|
    /** @var array */
 | 
						|
    private $validFields = ['accountRole', 'ccMonthlyPaymentDate', 'ccType', '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
 | 
						|
     */
 | 
						|
    public function destroy(Account $account, Account $moveTo): bool
 | 
						|
    {
 | 
						|
        if (!is_null($moveTo->id)) {
 | 
						|
            DB::table('transactions')->where('account_id', $account->id)->update(['account_id' => $moveTo->id]);
 | 
						|
        }
 | 
						|
        if (!is_null($account)) {
 | 
						|
            $account->delete();
 | 
						|
        }
 | 
						|
 | 
						|
        return true;
 | 
						|
    }
 | 
						|
 | 
						|
    /**
 | 
						|
     * 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 (!is_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 (!is_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 (is_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
 | 
						|
    {
 | 
						|
        $newAccount = $this->storeAccount($data);
 | 
						|
        $this->updateMetadata($newAccount, $data);
 | 
						|
 | 
						|
        if ($this->validOpeningBalanceData($data)) {
 | 
						|
            $this->updateInitialBalance($newAccount, $data);
 | 
						|
 | 
						|
            return $newAccount;
 | 
						|
        }
 | 
						|
        $this->deleteInitialBalance($newAccount);
 | 
						|
 | 
						|
        return $newAccount;
 | 
						|
 | 
						|
    }
 | 
						|
 | 
						|
    /**
 | 
						|
     * @param Account $account
 | 
						|
     * @param array   $data
 | 
						|
     *
 | 
						|
     * @return Account
 | 
						|
     */
 | 
						|
    public function update(Account $account, array $data): Account
 | 
						|
    {
 | 
						|
        // update the account:
 | 
						|
        $account->name            = $data['name'];
 | 
						|
        $account->active          = $data['active'] === '1' ? true : false;
 | 
						|
        $account->virtual_balance = $data['virtualBalance'];
 | 
						|
        $account->iban            = $data['iban'];
 | 
						|
        $account->save();
 | 
						|
 | 
						|
        $this->updateMetadata($account, $data);
 | 
						|
        if ($this->validOpeningBalanceData($data)) {
 | 
						|
            $this->updateInitialBalance($account, $data);
 | 
						|
        }
 | 
						|
 | 
						|
        return $account;
 | 
						|
    }
 | 
						|
 | 
						|
    /**
 | 
						|
     * @param Account $account
 | 
						|
     */
 | 
						|
    protected function deleteInitialBalance(Account $account)
 | 
						|
    {
 | 
						|
        $journal = $this->openingBalanceTransaction($account);
 | 
						|
        if (!is_null($journal->id)) {
 | 
						|
            $journal->delete();
 | 
						|
        }
 | 
						|
 | 
						|
    }
 | 
						|
 | 
						|
    /**
 | 
						|
     * @param Account $account
 | 
						|
     *
 | 
						|
     * @return TransactionJournal|null
 | 
						|
     */
 | 
						|
    protected function openingBalanceTransaction(Account $account): TransactionJournal
 | 
						|
    {
 | 
						|
        $journal = TransactionJournal::sortCorrectly()
 | 
						|
                                     ->leftJoin('transactions', 'transactions.transaction_journal_id', '=', 'transaction_journals.id')
 | 
						|
                                     ->where('transactions.account_id', $account->id)
 | 
						|
                                     ->transactionTypes([TransactionType::OPENING_BALANCE])
 | 
						|
                                     ->first(['transaction_journals.*']);
 | 
						|
        if (is_null($journal)) {
 | 
						|
            Log::debug('Could not find a opening balance journal, return empty one.');
 | 
						|
 | 
						|
            return new TransactionJournal;
 | 
						|
        }
 | 
						|
        Log::debug(sprintf('Found opening balance: journal #%d.', $journal->id));
 | 
						|
 | 
						|
        return $journal;
 | 
						|
    }
 | 
						|
 | 
						|
    /**
 | 
						|
     * @param array $data
 | 
						|
     *
 | 
						|
     * @return Account
 | 
						|
     * @throws FireflyException
 | 
						|
     */
 | 
						|
    protected function storeAccount(array $data): Account
 | 
						|
    {
 | 
						|
        $data['accountType'] = $data['accountType'] ?? 'invalid';
 | 
						|
        $type                = config('firefly.accountTypeByIdentifier.' . $data['accountType']);
 | 
						|
        $accountType         = AccountType::whereType($type)->first();
 | 
						|
        $data['iban']        = $this->filterIban($data['iban']);
 | 
						|
        // verify account type
 | 
						|
        if (is_null($accountType)) {
 | 
						|
            throw new FireflyException(sprintf('Account type "%s" is invalid. Cannot create account.', $data['accountType']));
 | 
						|
        }
 | 
						|
 | 
						|
        // account may exist already:
 | 
						|
        $existingAccount = $this->findByName($data['name'], [$type]);
 | 
						|
        if (!is_null($existingAccount->id)) {
 | 
						|
            Log::warning(sprintf('There already is an account named "%s" of type "%s".', $data['name'], $type));
 | 
						|
 | 
						|
            return $existingAccount;
 | 
						|
        }
 | 
						|
 | 
						|
        // create it:
 | 
						|
        $databaseData
 | 
						|
                    = [
 | 
						|
            'user_id'         => $this->user->id,
 | 
						|
            'account_type_id' => $accountType->id,
 | 
						|
            'name'            => $data['name'],
 | 
						|
            'virtual_balance' => $data['virtualBalance'],
 | 
						|
            'active'          => $data['active'] === true ? true : false,
 | 
						|
            'iban'            => $data['iban'],
 | 
						|
        ];
 | 
						|
        $newAccount = new Account($databaseData);
 | 
						|
        Log::debug('Final account creation dataset', $databaseData);
 | 
						|
        $newAccount->save();
 | 
						|
        // verify its creation:
 | 
						|
        if (is_null($newAccount->id)) {
 | 
						|
            Log::error(
 | 
						|
                sprintf('Could not create account "%s" (%d error(s))', $data['name'], $newAccount->getErrors()->count()), $newAccount->getErrors()->toArray()
 | 
						|
            );
 | 
						|
            throw new FireflyException(sprintf('Tried to create account named "%s" but failed. The logs have more details.', $data['name']));
 | 
						|
 | 
						|
        }
 | 
						|
        Log::debug(sprintf('Created new account #%d named "%s" of type %s.', $newAccount->id, $newAccount->name, $accountType->type));
 | 
						|
 | 
						|
        return $newAccount;
 | 
						|
    }
 | 
						|
 | 
						|
    /**
 | 
						|
     * @param Account $account
 | 
						|
     * @param array   $data
 | 
						|
     *
 | 
						|
     * @return TransactionJournal
 | 
						|
     */
 | 
						|
    protected function storeInitialBalance(Account $account, array $data): TransactionJournal
 | 
						|
    {
 | 
						|
        $amount = strval($data['openingBalance']);
 | 
						|
 | 
						|
        if (bccomp($amount, '0') === 0) {
 | 
						|
            return new TransactionJournal;
 | 
						|
        }
 | 
						|
 | 
						|
        $name            = $data['name'];
 | 
						|
        $currencyId      = $data['currency_id'];
 | 
						|
        $opposing        = $this->storeOpposingAccount($name);
 | 
						|
        $transactionType = TransactionType::whereType(TransactionType::OPENING_BALANCE)->first();
 | 
						|
        /** @var TransactionJournal $journal */
 | 
						|
        $journal = TransactionJournal::create(
 | 
						|
            [
 | 
						|
                'user_id'                 => $this->user->id,
 | 
						|
                'transaction_type_id'     => $transactionType->id,
 | 
						|
                'transaction_currency_id' => $currencyId,
 | 
						|
                'description'             => 'Initial balance for "' . $account->name . '"',
 | 
						|
                'completed'               => true,
 | 
						|
                'date'                    => $data['openingBalanceDate'],
 | 
						|
            ]
 | 
						|
        );
 | 
						|
        Log::debug(sprintf('Created new opening balance journal: #%d', $journal->id));
 | 
						|
 | 
						|
        $firstAccount  = $account;
 | 
						|
        $secondAccount = $opposing;
 | 
						|
        $firstAmount   = $amount;
 | 
						|
        $secondAmount  = bcmul($amount, '-1');
 | 
						|
 | 
						|
        if ($data['openingBalance'] < 0) {
 | 
						|
            $firstAccount  = $opposing;
 | 
						|
            $secondAccount = $account;
 | 
						|
            $firstAmount   = bcmul($amount, '-1');
 | 
						|
            $secondAmount  = $amount;
 | 
						|
        }
 | 
						|
 | 
						|
        $one = new Transaction(
 | 
						|
            [
 | 
						|
                'account_id'              => $firstAccount->id,
 | 
						|
                'transaction_journal_id'  => $journal->id,
 | 
						|
                'amount'                  => $firstAmount,
 | 
						|
                'transaction_currency_id' => $currencyId,
 | 
						|
            ]
 | 
						|
        );
 | 
						|
        $one->save();// first transaction: from
 | 
						|
 | 
						|
        $two = new Transaction(
 | 
						|
            [
 | 
						|
                'account_id'              => $secondAccount->id,
 | 
						|
                'transaction_journal_id'  => $journal->id,
 | 
						|
                'amount'                  => $secondAmount,
 | 
						|
                'transaction_currency_id' => $currencyId,]
 | 
						|
        );
 | 
						|
        $two->save(); // second transaction: to
 | 
						|
 | 
						|
        Log::debug(sprintf('Stored two transactions, #%d and #%d', $one->id, $two->id));
 | 
						|
 | 
						|
        return $journal;
 | 
						|
    }
 | 
						|
 | 
						|
    /**
 | 
						|
     * @param string $name
 | 
						|
     *
 | 
						|
     * @return Account
 | 
						|
     */
 | 
						|
    protected function storeOpposingAccount(string $name): Account
 | 
						|
    {
 | 
						|
        $opposingData = [
 | 
						|
            'accountType'    => 'initial',
 | 
						|
            'name'           => $name . ' initial balance',
 | 
						|
            'active'         => false,
 | 
						|
            'iban'           => '',
 | 
						|
            'virtualBalance' => 0,
 | 
						|
        ];
 | 
						|
        Log::debug('Going to create an opening balance opposing account.');
 | 
						|
 | 
						|
        return $this->storeAccount($opposingData);
 | 
						|
    }
 | 
						|
 | 
						|
    /**
 | 
						|
     * @param Account $account
 | 
						|
     * @param array   $data
 | 
						|
     *
 | 
						|
     * @return bool
 | 
						|
     */
 | 
						|
    protected function updateInitialBalance(Account $account, array $data): bool
 | 
						|
    {
 | 
						|
        $openingBalance = $this->openingBalanceTransaction($account);
 | 
						|
 | 
						|
        // no opening balance journal? create it:
 | 
						|
        if (is_null($openingBalance->id)) {
 | 
						|
            Log::debug('No opening balance journal yet, create journal.');
 | 
						|
            $this->storeInitialBalance($account, $data);
 | 
						|
 | 
						|
            return true;
 | 
						|
        }
 | 
						|
        // opening balance data? update it!
 | 
						|
        if (!is_null($openingBalance->id)) {
 | 
						|
            Log::debug('Opening balance journal found, update journal.');
 | 
						|
            $this->updateOpeningBalanceJournal($account, $openingBalance, $data);
 | 
						|
 | 
						|
            return true;
 | 
						|
        }
 | 
						|
 | 
						|
        return true;
 | 
						|
    }
 | 
						|
 | 
						|
    /**
 | 
						|
     * @param Account $account
 | 
						|
     * @param array   $data
 | 
						|
     *
 | 
						|
     */
 | 
						|
    protected function updateMetadata(Account $account, array $data)
 | 
						|
    {
 | 
						|
        foreach ($this->validFields as $field) {
 | 
						|
            /** @var AccountMeta $entry */
 | 
						|
            $entry = $account->accountMeta()->where('name', $field)->first();
 | 
						|
 | 
						|
            // if $data has field and $entry is null, create new one:
 | 
						|
            if (isset($data[$field]) && is_null($entry)) {
 | 
						|
                Log::debug(
 | 
						|
                    sprintf(
 | 
						|
                        'Created meta-field "%s":"%s" for account #%d ("%s") ',
 | 
						|
                        $field, $data[$field], $account->id, $account->name
 | 
						|
                    )
 | 
						|
                );
 | 
						|
                AccountMeta::create(
 | 
						|
                    [
 | 
						|
                        'account_id' => $account->id,
 | 
						|
                        'name'       => $field,
 | 
						|
                        'data'       => $data[$field],
 | 
						|
                    ]
 | 
						|
                );
 | 
						|
            }
 | 
						|
 | 
						|
            // if $data has field and $entry is not null, update $entry:
 | 
						|
            if (isset($data[$field]) && !is_null($entry)) {
 | 
						|
                $entry->data = $data[$field];
 | 
						|
                $entry->save();
 | 
						|
                Log::debug(
 | 
						|
                    sprintf(
 | 
						|
                        'Updated meta-field "%s":"%s" for account #%d ("%s") ',
 | 
						|
                        $field, $data[$field], $account->id, $account->name
 | 
						|
                    )
 | 
						|
                );
 | 
						|
            }
 | 
						|
        }
 | 
						|
 | 
						|
    }
 | 
						|
 | 
						|
    /**
 | 
						|
     * @param Account            $account
 | 
						|
     * @param TransactionJournal $journal
 | 
						|
     * @param array              $data
 | 
						|
     *
 | 
						|
     * @return bool
 | 
						|
     */
 | 
						|
    protected function updateOpeningBalanceJournal(Account $account, TransactionJournal $journal, array $data): bool
 | 
						|
    {
 | 
						|
        $date       = $data['openingBalanceDate'];
 | 
						|
        $amount     = strval($data['openingBalance']);
 | 
						|
        $currencyId = intval($data['currency_id']);
 | 
						|
 | 
						|
        if (bccomp($amount, '0') === 0) {
 | 
						|
            $journal->delete();
 | 
						|
 | 
						|
            return true;
 | 
						|
        }
 | 
						|
 | 
						|
        // update date:
 | 
						|
        $journal->date                    = $date;
 | 
						|
        $journal->transaction_currency_id = $currencyId;
 | 
						|
        $journal->save();
 | 
						|
        // update transactions:
 | 
						|
        /** @var Transaction $transaction */
 | 
						|
        foreach ($journal->transactions()->get() as $transaction) {
 | 
						|
            if ($account->id === $transaction->account_id) {
 | 
						|
                $transaction->amount                  = $amount;
 | 
						|
                $transaction->transaction_currency_id = $currencyId;
 | 
						|
                $transaction->save();
 | 
						|
            }
 | 
						|
            if ($account->id !== $transaction->account_id) {
 | 
						|
                $transaction->amount                  = bcmul($amount, '-1');
 | 
						|
                $transaction->transaction_currency_id = $currencyId;
 | 
						|
                $transaction->save();
 | 
						|
            }
 | 
						|
        }
 | 
						|
        Log::debug('Updated opening balance journal.');
 | 
						|
 | 
						|
        return true;
 | 
						|
 | 
						|
    }
 | 
						|
 | 
						|
 | 
						|
    /**
 | 
						|
     * @param array $data
 | 
						|
     *
 | 
						|
     * @return bool
 | 
						|
     */
 | 
						|
    protected function validOpeningBalanceData(array $data): bool
 | 
						|
    {
 | 
						|
        if (isset($data['openingBalance']) && isset($data['openingBalanceDate'])) {
 | 
						|
            Log::debug('Array has valid opening balance data.');
 | 
						|
 | 
						|
            return true;
 | 
						|
        }
 | 
						|
        Log::debug('Array does not have valid opening balance data.');
 | 
						|
 | 
						|
        return false;
 | 
						|
    }
 | 
						|
 | 
						|
    /**
 | 
						|
     * @param string $iban
 | 
						|
     *
 | 
						|
     * @return null|string
 | 
						|
     */
 | 
						|
    private function filterIban(string $iban = null)
 | 
						|
    {
 | 
						|
        if (is_null($iban)) {
 | 
						|
            return null;
 | 
						|
        }
 | 
						|
        $data      = ['iban' => $iban];
 | 
						|
        $rules     = ['iban' => 'required|iban'];
 | 
						|
        $validator = Validator::make($data, $rules);
 | 
						|
        if ($validator->fails()) {
 | 
						|
            Log::error(sprintf('Detected invalid IBAN ("%s"). Return NULL instead.', $iban));
 | 
						|
 | 
						|
            return null;
 | 
						|
        }
 | 
						|
 | 
						|
        return $iban;
 | 
						|
    }
 | 
						|
}
 |