From 3545d894fdca272fce5c442ac70d7b3d08ae8ae2 Mon Sep 17 00:00:00 2001 From: James Cole Date: Mon, 18 Mar 2019 16:52:49 +0100 Subject: [PATCH] Improve factories and tests. --- ...gradeDatabase.php => UpgradeDatabaseX.php} | 0 app/Factory/TransactionFactory.php | 182 +- app/Factory/TransactionJournalFactory.php | 73 +- app/Models/Account.php | 55 +- .../Journal/JournalRepository.php | 44 + .../Journal/JournalRepositoryInterface.php | 17 + .../Support/RecurringTransactionTrait.php | 21 +- tests/TestCase.php | 8 + .../Commands/Upgrade/MigrateToGroupsTest.php | 176 ++ tests/Unit/Factory/TransactionFactoryTest.php | 1664 ++++++----------- .../Factory/TransactionJournalFactoryTest.php | 1059 +++++++++-- 11 files changed, 1901 insertions(+), 1398 deletions(-) rename app/Console/Commands/{UpgradeDatabase.php => UpgradeDatabaseX.php} (100%) create mode 100644 tests/Unit/Console/Commands/Upgrade/MigrateToGroupsTest.php diff --git a/app/Console/Commands/UpgradeDatabase.php b/app/Console/Commands/UpgradeDatabaseX.php similarity index 100% rename from app/Console/Commands/UpgradeDatabase.php rename to app/Console/Commands/UpgradeDatabaseX.php diff --git a/app/Factory/TransactionFactory.php b/app/Factory/TransactionFactory.php index b9803457d5..fea1a9d7df 100644 --- a/app/Factory/TransactionFactory.php +++ b/app/Factory/TransactionFactory.php @@ -35,6 +35,7 @@ use FireflyIII\Models\TransactionType; use FireflyIII\Repositories\Account\AccountRepositoryInterface; use FireflyIII\Support\NullArrayObject; use FireflyIII\User; +use Illuminate\Database\QueryException; use Illuminate\Support\Collection; use Log; @@ -61,8 +62,6 @@ class TransactionFactory $this->accountRepository = app(AccountRepositoryInterface::class); } - //use TransactionServiceTrait; - /** * @param Account $account * @param TransactionCurrency $currency @@ -72,19 +71,23 @@ class TransactionFactory */ public function create(Account $account, TransactionCurrency $currency, string $amount): ?Transaction { - $result = Transaction::create( - [ - 'reconciled' => false, - 'account_id' => $account->id, - 'transaction_journal_id' => $this->journal->id, - 'description' => null, - 'transaction_currency_id' => $currency->id, - 'amount' => $amount, - 'foreign_amount' => null, - 'foreign_currency_id' => null, - 'identifier' => 0, - ] - ); + $result = null; + $data = [ + 'reconciled' => false, + 'account_id' => $account->id, + 'transaction_journal_id' => $this->journal->id, + 'description' => null, + 'transaction_currency_id' => $currency->id, + 'amount' => $amount, + 'foreign_amount' => null, + 'foreign_currency_id' => null, + 'identifier' => 0, + ]; + try { + $result = Transaction::create($data); + } catch (QueryException $e) { + Log::error(sprintf('Could not create transaction: %s', $e->getMessage()), $data); + } if (null !== $result) { Log::debug( sprintf( @@ -107,11 +110,14 @@ class TransactionFactory */ public function createPair(NullArrayObject $data, TransactionCurrency $currency, ?TransactionCurrency $foreignCurrency): Collection { - $sourceAccount = $this->getAccount('source', $data['source'], $data['source_id'], $data['source_name']); - $destinationAccount = $this->getAccount('destination', $data['destination'], $data['destination_id'], $data['destination_name']); + $sourceAccount = $this->getAccount('source', $data['source'], (int)$data['source_id'], $data['source_name']); + $destinationAccount = $this->getAccount('destination', $data['destination'], (int)$data['destination_id'], $data['destination_name']); $amount = $this->getAmount($data['amount']); $foreignAmount = $this->getForeignAmount($data['foreign_amount']); + $this->makeDramaOverAccountTypes($sourceAccount, $destinationAccount); + + $one = $this->create($sourceAccount, $currency, app('steam')->negative($amount)); $two = $this->create($destinationAccount, $currency, app('steam')->positive($amount)); @@ -134,24 +140,6 @@ class TransactionFactory } - - /** - * @param TransactionJournal $journal - */ - public function setJournal(TransactionJournal $journal): void - { - $this->journal = $journal; - } - - /** - * @param User $user - */ - public function setUser(User $user): void - { - $this->user = $user; - $this->accountRepository->setUser($user); - } - /** * @param string $direction * @param Account|null $source @@ -161,7 +149,7 @@ class TransactionFactory * @return Account * @throws FireflyException */ - private function getAccount(string $direction, ?Account $source, ?int $sourceId, ?string $sourceName): Account + public function getAccount(string $direction, ?Account $source, ?int $sourceId, ?string $sourceName): Account { Log::debug(sprintf('Now in getAccount(%s)', $direction)); Log::debug(sprintf('Parameters: ((account), %s, %s)', var_export($sourceId, true), var_export($sourceName, true))); @@ -169,21 +157,21 @@ class TransactionFactory $array = [ 'source' => [ TransactionType::WITHDRAWAL => [AccountType::ASSET, AccountType::LOAN, AccountType::DEBT, AccountType::MORTGAGE], - TransactionType::DEPOSIT => [AccountType::REVENUE, AccountType::LOAN, AccountType::DEBT, AccountType::MORTGAGE], + TransactionType::DEPOSIT => [AccountType::REVENUE, AccountType::CASH, AccountType::LOAN, AccountType::DEBT, AccountType::MORTGAGE, + AccountType::INITIAL_BALANCE, AccountType::RECONCILIATION], TransactionType::TRANSFER => [AccountType::ASSET, AccountType::LOAN, AccountType::DEBT, AccountType::MORTGAGE], TransactionType::OPENING_BALANCE => [AccountType::INITIAL_BALANCE, AccountType::ASSET, AccountType::LOAN, AccountType::DEBT, AccountType::MORTGAGE], - TransactionType::RECONCILIATION => [AccountType::RECONCILIATION, AccountType::ASSET, AccountType::LOAN, AccountType::DEBT, - AccountType::MORTGAGE], + TransactionType::RECONCILIATION => [AccountType::RECONCILIATION, AccountType::ASSET], ], 'destination' => [ - TransactionType::WITHDRAWAL => [AccountType::EXPENSE, AccountType::ASSET], + TransactionType::WITHDRAWAL => [AccountType::EXPENSE, AccountType::CASH, AccountType::LOAN, AccountType::DEBT, + AccountType::MORTGAGE], TransactionType::DEPOSIT => [AccountType::ASSET, AccountType::LOAN, AccountType::DEBT, AccountType::MORTGAGE], TransactionType::TRANSFER => [AccountType::ASSET, AccountType::LOAN, AccountType::DEBT, AccountType::MORTGAGE], TransactionType::OPENING_BALANCE => [AccountType::INITIAL_BALANCE, AccountType::ASSET, AccountType::LOAN, AccountType::DEBT, AccountType::MORTGAGE], - TransactionType::RECONCILIATION => [AccountType::RECONCILIATION, AccountType::ASSET, AccountType::LOAN, AccountType::DEBT, - AccountType::MORTGAGE], + TransactionType::RECONCILIATION => [AccountType::RECONCILIATION, AccountType::ASSET], ], ]; $expectedTypes = $array[$direction]; @@ -208,12 +196,10 @@ class TransactionFactory // second attempt, find by ID. if (null !== $sourceId) { $source = $this->accountRepository->findNull($sourceId); - if (null !== $source) { - Log::debug(sprintf('Found account #%d ("%s" of type "%s") based on #%d.', $source->id, $source->name, $source->accountType->type, $sourceId)); - } - if (null !== $source && \in_array($source->accountType->type, $expectedTypes[$transactionType], true)) { - Log::debug(sprintf('Found "account_id" object for %s: #%d, %s', $direction, $source->id, $source->name)); + Log::debug( + sprintf('Found "account_id" object for %s: #%d, "%s" of type %s', $direction, $source->id, $source->name, $source->accountType->type) + ); return $source; } @@ -232,6 +218,9 @@ class TransactionFactory return $source; } } + if (null === $sourceName && \in_array(AccountType::CASH, $expectedTypes[$transactionType], true)) { + return $this->accountRepository->getCashAccount(); + } $sourceName = $sourceName ?? '(no name)'; // final attempt, create it. $preferredType = $expectedTypes[$transactionType][0]; @@ -256,7 +245,7 @@ class TransactionFactory * @return string * @throws FireflyException */ - private function getAmount(string $amount): string + public function getAmount(string $amount): string { if ('' === $amount) { throw new FireflyException(sprintf('The amount cannot be an empty string: "%s"', $amount)); @@ -273,7 +262,7 @@ class TransactionFactory * * @return string */ - private function getForeignAmount(?string $amount): ?string + public function getForeignAmount(?string $amount): ?string { if (null === $amount) { Log::debug('No foreign amount info in array. Return NULL'); @@ -294,29 +283,78 @@ class TransactionFactory return $amount; } - // - // /** - // * @param string $sourceType - // * @param string $destinationType - // * @param string $transactionType - // * - // * @throws FireflyException - // */ - // private function validateTransaction(string $sourceType, string $destinationType, string $transactionType): void - // { - // // throw big fat error when source type === dest type and it's not a transfer or reconciliation. - // if ($sourceType === $destinationType && $transactionType !== TransactionType::TRANSFER) { - // throw new FireflyException(sprintf('Source and destination account cannot be both of the type "%s"', $destinationType)); - // } - // // source must be in this list AND dest must be in this list: - // $list = [AccountType::DEFAULT, AccountType::ASSET, AccountType::CREDITCARD, AccountType::CASH, AccountType::DEBT, AccountType::MORTGAGE, - // AccountType::LOAN, AccountType::MORTGAGE]; - // if ( - // !\in_array($sourceType, $list, true) - // && !\in_array($destinationType, $list, true)) { - // throw new FireflyException(sprintf('At least one of the accounts must be an asset account (%s, %s).', $sourceType, $destinationType)); - // } - // } + /** + * This method will throw a Firefly III Exception of the source and destination account types are not OK. + * + * @throws FireflyException + * + * @param Account $source + * @param Account $destination + */ + public function makeDramaOverAccountTypes(Account $source, Account $destination): void + { + // if the source is X, then Y is allowed as destination. + $combinations = [ + TransactionType::WITHDRAWAL => [ + AccountType::ASSET => [AccountType::EXPENSE, AccountType::LOAN, AccountType::DEBT, AccountType::MORTGAGE, AccountType::CASH], + AccountType::LOAN => [AccountType::EXPENSE], + AccountType::DEBT => [AccountType::EXPENSE], + AccountType::MORTGAGE => [AccountType::EXPENSE], + ], + TransactionType::DEPOSIT => [ + AccountType::REVENUE => [AccountType::ASSET, AccountType::LOAN, AccountType::DEBT, AccountType::MORTGAGE], + AccountType::CASH => [AccountType::ASSET], + AccountType::LOAN => [AccountType::ASSET], + AccountType::DEBT => [AccountType::ASSET], + AccountType::MORTGAGE => [AccountType::ASSET], + ], + TransactionType::TRANSFER => [ + AccountType::ASSET => [AccountType::ASSET], + AccountType::LOAN => [AccountType::LOAN, AccountType::DEBT, AccountType::MORTGAGE], + AccountType::DEBT => [AccountType::LOAN, AccountType::DEBT, AccountType::MORTGAGE], + AccountType::MORTGAGE => [AccountType::LOAN, AccountType::DEBT, AccountType::MORTGAGE], + ], + TransactionType::OPENING_BALANCE => [ + AccountType::ASSET => [AccountType::INITIAL_BALANCE], + AccountType::LOAN => [AccountType::INITIAL_BALANCE], + AccountType::DEBT => [AccountType::INITIAL_BALANCE], + AccountType::MORTGAGE => [AccountType::INITIAL_BALANCE], + AccountType::INITIAL_BALANCE => [AccountType::ASSET, AccountType::LOAN, AccountType::DEBT, AccountType::MORTGAGE], + ], + TransactionType::RECONCILIATION => [ + AccountType::RECONCILIATION => [AccountType::ASSET], + AccountType::ASSET => [AccountType::RECONCILIATION], + ], + ]; + $sourceType = $source->accountType->type; + $destType = $destination->accountType->type; + $journalType = $this->journal->transactionType->type; + $allowed = $combinations[$journalType][$sourceType] ?? []; + if (!\in_array($destType, $allowed, true)) { + throw new FireflyException( + sprintf( + 'Journal of type "%s" has a source account of type "%s" and cannot accept a "%s"-account as destination, but only accounts of: %s', $journalType, $sourceType, + $destType, implode(', ', $combinations[$journalType][$sourceType]) + ) + ); + } + } + /** + * @param TransactionJournal $journal + */ + public function setJournal(TransactionJournal $journal): void + { + $this->journal = $journal; + } + + /** + * @param User $user + */ + public function setUser(User $user): void + { + $this->user = $user; + $this->accountRepository->setUser($user); + } } diff --git a/app/Factory/TransactionJournalFactory.php b/app/Factory/TransactionJournalFactory.php index 2d69cad86a..5f26fcbc71 100644 --- a/app/Factory/TransactionJournalFactory.php +++ b/app/Factory/TransactionJournalFactory.php @@ -58,8 +58,12 @@ class TransactionJournalFactory private $currencyRepository; /** @var array */ private $fields; + /** @var PiggyBankEventFactory */ + private $piggyEventFactory; /** @var PiggyBankRepositoryInterface */ private $piggyRepository; + /** @var TagFactory */ + private $tagFactory; /** @var TransactionFactory */ private $transactionFactory; /** @var TransactionTypeRepositoryInterface */ @@ -89,6 +93,8 @@ class TransactionJournalFactory $this->budgetRepository = app(BudgetRepositoryInterface::class); $this->categoryRepository = app(CategoryRepositoryInterface::class); $this->piggyRepository = app(PiggyBankRepositoryInterface::class); + $this->piggyEventFactory = app(PiggyBankEventFactory::class); + $this->tagFactory = app(TagFactory::class); } /** @@ -101,6 +107,7 @@ class TransactionJournalFactory */ public function create(array $data): Collection { + $data = new NullArrayObject($data); Log::debug('Start of TransactionJournalFactory::create()'); $collection = new Collection; $transactions = $data['transactions'] ?? []; @@ -111,7 +118,7 @@ class TransactionJournalFactory Log::debug(sprintf('Going to store a %s.', $type->type)); if (0 === \count($transactions)) { - Log::error('There are no transactions in the array, cannot continue.'); + Log::error('There are no transactions in the array, the TransactionJournalFactory cannot continue.'); return new Collection; } @@ -122,12 +129,12 @@ class TransactionJournalFactory Log::debug(sprintf('Now creating journal %d/%d', $index + 1, \count($transactions))); /** Get basic fields */ - $currency = $this->currencyRepository->findCurrency($transaction['currency'], $transaction['currency_id'], $transaction['currency_code']); + $currency = $this->currencyRepository->findCurrency($transaction['currency'], (int)$transaction['currency_id'], $transaction['currency_code']); $foreignCurrency = $this->findForeignCurrency($transaction); - $bill = $this->billRepository->findBill($transaction['bill'], $transaction['bill_id'], $transaction['bill_name']); + $bill = $this->billRepository->findBill($transaction['bill'], (int)$transaction['bill_id'], $transaction['bill_name']); $billId = TransactionType::WITHDRAWAL === $type->type && null !== $bill ? $bill->id : null; - $description = app('steam')->cleanString($transaction['description']); + $description = app('steam')->cleanString((string)$transaction['description']); /** Create a basic journal. */ $journal = TransactionJournal::create( @@ -136,7 +143,7 @@ class TransactionJournalFactory 'transaction_type_id' => $type->id, 'bill_id' => $billId, 'transaction_currency_id' => $currency->id, - 'description' => $description, + 'description' => '' === $description ? '(empty description)' : $description, 'date' => $carbon->format('Y-m-d H:i:s'), 'order' => 0, 'tag_count' => 0, @@ -149,6 +156,17 @@ class TransactionJournalFactory $this->transactionFactory->setJournal($journal); $this->transactionFactory->createPair($transaction, $currency, $foreignCurrency); + // verify that journal has two transactions. Otherwise, delete and cancel. + $count = $journal->transactions()->count(); + if (2 !== $count) { + // @codeCoverageIgnoreStart + Log::error(sprintf('The journal unexpectedly has %d transaction(s). This is not OK. Cancel operation.', $count)); + $journal->delete(); + + return new Collection; + // @codeCoverageIgnoreEnd + } + /** Link all other data to the journal. */ /** Link budget */ @@ -206,7 +224,7 @@ class TransactionJournalFactory public function storeGroup(Collection $collection, ?string $title): ?TransactionGroup { if ($collection->count() < 2) { - return null; + return null; // @codeCoverageIgnore } /** @var TransactionJournal $first */ $first = $collection->first(); @@ -235,32 +253,11 @@ class TransactionJournalFactory return; } - $piggyBank = $this->piggyRepository->findPiggyBank($data['piggy_bank'], $data['piggy_bank_id'], $data['piggy_bank_name']); + $piggyBank = $this->piggyRepository->findPiggyBank($data['piggy_bank'], (int)$data['piggy_bank_id'], $data['piggy_bank_name']); if (null !== $piggyBank) { - /** @var PiggyBankEventFactory $factory */ - $factory = app(PiggyBankEventFactory::class); - $factory->create($journal, $piggyBank); + $this->piggyEventFactory->create($journal, $piggyBank); Log::debug('Create piggy event.'); - } - - - /** @var PiggyBankFactory $factory */ - $factory = app(PiggyBankFactory::class); - $factory->setUser($this->user); - $piggyBank = null; - - if (isset($data['piggy_bank']) && $data['piggy_bank'] instanceof PiggyBank && $data['piggy_bank']->account->user_id === $this->user->id) { - Log::debug('Piggy found and belongs to user'); - $piggyBank = $data['piggy_bank']; - } - if (null === $data['piggy_bank']) { - Log::debug('Piggy not found, search by piggy data.'); - $piggyBank = $factory->find($data['piggy_bank_id'], $data['piggy_bank_name']); - } - - if (null !== $piggyBank) { - return; } @@ -276,16 +273,14 @@ class TransactionJournalFactory */ public function storeTags(TransactionJournal $journal, ?array $tags): void { - /** @var TagFactory $factory */ - $factory = app(TagFactory::class); - $factory->setUser($journal->user); + $this->tagFactory->setUser($journal->user); $set = []; if (!\is_array($tags)) { - return; // @codeCoverageIgnore + return; } foreach ($tags as $string) { if ('' !== $string) { - $tag = $factory->findOrCreate($string); + $tag = $this->tagFactory->findOrCreate($string); if (null !== $tag) { $set[] = $tag->id; } @@ -333,14 +328,6 @@ class TransactionJournalFactory return; } - $note = $journal->notes()->first(); - if (null !== $note) { - try { - $note->delete(); - } catch (Exception $e) { - Log::debug(sprintf('Journal service trait could not delete note: %s', $e->getMessage())); - } - } } /** @@ -357,7 +344,7 @@ class TransactionJournalFactory } return $this->currencyRepository->findCurrency( - $transaction['foreign_currency'], $transaction['foreign_currency_id'], $transaction['foreign_currency_code'] + $transaction['foreign_currency'], (int)$transaction['foreign_currency_id'], $transaction['foreign_currency_code'] ); } diff --git a/app/Models/Account.php b/app/Models/Account.php index dee2a1574f..b9c7fffb1d 100644 --- a/app/Models/Account.php +++ b/app/Models/Account.php @@ -36,37 +36,31 @@ use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; /** * Class Account. * - * @property int $id - * @property string $name - * @property string $iban - * @property AccountType $accountType - * @property bool $active - * @property string $virtual_balance - * @property User $user - * @property string startBalance - * @property string endBalance - * @property string difference - * @property Carbon lastActivityDate - * @property Collection accountMeta - * @property bool encrypted - * @property int account_type_id - * @property Collection piggyBanks - * @property string $interest - * @property string $interestPeriod - * @property string accountTypeString - * @property Carbon created_at - * @property Carbon updated_at + * @property int $id + * @property string $name + * @property string $iban + * @property AccountType $accountType + * @property bool $active + * @property string $virtual_balance + * @property User $user + * @property string startBalance + * @property string endBalance + * @property string difference + * @property Carbon lastActivityDate + * @property Collection accountMeta + * @property bool encrypted + * @property int account_type_id + * @property Collection piggyBanks + * @property string $interest + * @property string $interestPeriod + * @property string accountTypeString + * @property Carbon created_at + * @property Carbon updated_at * @SuppressWarnings (PHPMD.CouplingBetweenObjects) - * @property \Illuminate\Support\Carbon|null $created_at - * @property \Illuminate\Support\Carbon|null $updated_at - * @property \Illuminate\Support\Carbon|null $deleted_at - * @property int $user_id - * @property int $account_type_id - * @property bool $encrypted - * @property-read \Illuminate\Database\Eloquent\Collection|\FireflyIII\Models\AccountMeta[] $accountMeta - * @property-read string $edit_name - * @property-read \Illuminate\Database\Eloquent\Collection|\FireflyIII\Models\Note[] $notes - * @property-read \Illuminate\Database\Eloquent\Collection|\FireflyIII\Models\PiggyBank[] $piggyBanks + * @property \Illuminate\Support\Carbon|null $deleted_at + * @property int $user_id + * @property-read string $edit_name + * @property-read \Illuminate\Database\Eloquent\Collection|\FireflyIII\Models\Note[] $notes * @property-read \Illuminate\Database\Eloquent\Collection|\FireflyIII\Models\Transaction[] $transactions * @method static \Illuminate\Database\Eloquent\Builder|\FireflyIII\Models\Account accountTypeIn($types) * @method static bool|null forceDelete() @@ -103,6 +97,7 @@ class Account extends Model = [ 'created_at' => 'datetime', 'updated_at' => 'datetime', + 'user_id' => 'integer', 'deleted_at' => 'datetime', 'active' => 'boolean', 'encrypted' => 'boolean', diff --git a/app/Repositories/Journal/JournalRepository.php b/app/Repositories/Journal/JournalRepository.php index 1f4bd93eed..9daf96f5bf 100644 --- a/app/Repositories/Journal/JournalRepository.php +++ b/app/Repositories/Journal/JournalRepository.php @@ -23,6 +23,7 @@ declare(strict_types=1); namespace FireflyIII\Repositories\Journal; use Carbon\Carbon; +use DB; use Exception; use FireflyIII\Exceptions\FireflyException; use FireflyIII\Factory\TransactionJournalFactory; @@ -320,6 +321,28 @@ class JournalRepository implements JournalRepositoryInterface return 0; } + /** + * Return the ID of the category linked to the journal (if any) or to the transactions (if any). + * + * @param TransactionJournal $journal + * + * @return int + */ + public function getJournalCategoryId(TransactionJournal $journal): int + { + $category = $journal->categories()->first(); + if (null !== $category) { + return $category->id; + } + /** @noinspection NullPointerExceptionInspection */ + $category = $journal->transactions()->first()->categories()->first(); + if (null !== $category) { + return $category->id; + } + + return 0; + } + /** * Return the name of the category linked to the journal (if any) or to the transactions (if any). * @@ -615,6 +638,27 @@ class JournalRepository implements JournalRepositoryInterface return $transaction->transactionJournal->piggyBankEvents()->get(); } + /** + * Returns all journals with more than 2 transactions. Should only return empty collections + * in Firefly III > v4.8.0. + * + * @return Collection + */ + public function getSplitJournals(): Collection + { + // grab all split transactions: + $all = Transaction::groupBy('transaction_journal_id')->get(['transaction_journal_id', DB::raw('COUNT(transaction_journal_id) as result')]); + /** @var Collection $filtered */ + $filtered = $all->filter( + function (Transaction $transaction) { + return (int)$transaction->result > 2; + } + ); + $journalIds = array_unique($filtered->pluck('transaction_journal_id')->toArray()); + + return TransactionJournal::whereIn('id', $journalIds)->get(); + } + /** * Return all tags as strings in an array. * diff --git a/app/Repositories/Journal/JournalRepositoryInterface.php b/app/Repositories/Journal/JournalRepositoryInterface.php index 13eaf8360f..c511acf8a7 100644 --- a/app/Repositories/Journal/JournalRepositoryInterface.php +++ b/app/Repositories/Journal/JournalRepositoryInterface.php @@ -149,6 +149,15 @@ interface JournalRepositoryInterface */ public function getJournalBudgetId(TransactionJournal $journal): int; + /** + * Return the ID of the category linked to the journal (if any) or to the transactions (if any). + * + * @param TransactionJournal $journal + * + * @return int + */ + public function getJournalCategoryId(TransactionJournal $journal): int; + /** * Return the name of the category linked to the journal (if any) or to the transactions (if any). * @@ -256,6 +265,14 @@ interface JournalRepositoryInterface */ public function getPiggyBankEventsbyTr(Transaction $transaction): Collection; + /** + * Returns all journals with more than 2 transactions. Should only return empty collections + * in Firefly III > v4.8.0. + * + * @return Collection + */ + public function getSplitJournals(): Collection; + /** * Return all tags as strings in an array. * diff --git a/app/Services/Internal/Support/RecurringTransactionTrait.php b/app/Services/Internal/Support/RecurringTransactionTrait.php index 894636e5bd..741f68a22c 100644 --- a/app/Services/Internal/Support/RecurringTransactionTrait.php +++ b/app/Services/Internal/Support/RecurringTransactionTrait.php @@ -81,16 +81,16 @@ trait RecurringTransactionTrait $destination = null; switch ($recurrence->transactionType->type) { case TransactionType::WITHDRAWAL: - $source = $this->findAccount(AccountType::ASSET, $array['source_id'], $array['source_name']); - $destination = $this->findAccount(AccountType::EXPENSE, $array['destination_id'], $array['destination_name']); + $source = $this->findAccount(AccountType::ASSET, null, $array['source_id'], $array['source_name']); + $destination = $this->findAccount(AccountType::EXPENSE,null, $array['destination_id'], $array['destination_name']); break; case TransactionType::DEPOSIT: - $source = $this->findAccount(AccountType::REVENUE, $array['source_id'], $array['source_name']); - $destination = $this->findAccount(AccountType::ASSET, $array['destination_id'], $array['destination_name']); + $source = $this->findAccount(AccountType::REVENUE, null, $array['source_id'], $array['source_name']); + $destination = $this->findAccount(AccountType::ASSET, null, $array['destination_id'], $array['destination_name']); break; case TransactionType::TRANSFER: - $source = $this->findAccount(AccountType::ASSET, $array['source_id'], $array['source_name']); - $destination = $this->findAccount(AccountType::ASSET, $array['destination_id'], $array['destination_name']); + $source = $this->findAccount(AccountType::ASSET,null, $array['source_id'], $array['source_name']); + $destination = $this->findAccount(AccountType::ASSET, null, $array['destination_id'], $array['destination_name']); break; } @@ -172,13 +172,14 @@ trait RecurringTransactionTrait } /** - * @param null|string $expectedType - * @param int|null $accountId - * @param null|string $accountName + * @param null|string $expectedType + * @param Account|null $account + * @param int|null $accountId + * @param null|string $accountName * * @return Account|null */ - abstract public function findAccount(?string $expectedType, ?int $accountId, ?string $accountName): ?Account; + abstract public function findAccount(?string $expectedType, ?Account $account, ?int $accountId, ?string $accountName): ?Account; /** * Update meta data for recurring transaction. diff --git a/tests/TestCase.php b/tests/TestCase.php index a107e03cb0..8bb3196227 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -122,6 +122,14 @@ abstract class TestCase extends BaseTestCase return $this->getRandomAccount(AccountType::ASSET); } + /** + * @return Account + */ + public function getRandomLoan(): Account + { + return $this->getRandomAccount(AccountType::LOAN); + } + /** * @return TransactionJournal */ diff --git a/tests/Unit/Console/Commands/Upgrade/MigrateToGroupsTest.php b/tests/Unit/Console/Commands/Upgrade/MigrateToGroupsTest.php new file mode 100644 index 0000000000..570af769fd --- /dev/null +++ b/tests/Unit/Console/Commands/Upgrade/MigrateToGroupsTest.php @@ -0,0 +1,176 @@ +. + */ + +declare(strict_types=1); + +namespace Tests\Unit\Console\Commands\Upgrade; + +use Carbon\Carbon; +use FireflyConfig; +use FireflyIII\Factory\TransactionJournalFactory; +use FireflyIII\Models\Configuration; +use FireflyIII\Models\Transaction; +use FireflyIII\Repositories\Journal\JournalRepositoryInterface; +use Illuminate\Support\Collection; +use Log; +use Mockery; +use Tests\TestCase; + +/** + * Class MigrateToGroupsTest + */ +class MigrateToGroupsTest extends TestCase +{ + /** + * + */ + public function setUp(): void + { + parent::setUp(); + Log::info(sprintf('Now in %s.', \get_class($this))); + } + + /** + * @covers \FireflyIII\Console\Commands\Upgrade\MigrateToGroups + */ + public function testAlreadyExecuted(): void + { + $this->mock(TransactionJournalFactory::class); + $this->mock(JournalRepositoryInterface::class); + + $configObject = new Configuration; + $configObject->data = true; + FireflyConfig::shouldReceive('get')->withArgs(['migrated_to_groups_4780', false])->andReturn($configObject)->once(); + + $this->artisan('firefly:migrate-to-groups') + ->expectsOutput('Database already seems to be migrated.') + ->assertExitCode(0); + } + + /** + * @covers \FireflyIII\Console\Commands\Upgrade\MigrateToGroups + */ + public function testBasic(): void + { + $journalFactory = $this->mock(TransactionJournalFactory::class); + $journalRepos = $this->mock(JournalRepositoryInterface::class); + $withdrawal = $this->getRandomSplitWithdrawal(); + $collection = new Collection([$withdrawal]); + $date = new Carbon; + $opposing = new Transaction; + $opposing->account_id = 13; + + // not yet executed: + $configObject = new Configuration; + $configObject->data = false; + FireflyConfig::shouldReceive('get')->withArgs(['migrated_to_groups_4780', false])->andReturn($configObject)->once(); + FireflyConfig::shouldReceive('set')->withArgs(['migrated_to_groups_4780', true])->once(); + + + // calls to repository: + $journalRepos->shouldReceive('setUser')->atLeast()->once(); + $journalRepos->shouldReceive('getJournalBudgetId')->atLeast()->once()->andReturn(1); + $journalRepos->shouldReceive('getJournalCategoryId')->atLeast()->once()->andReturn(2); + $journalRepos->shouldReceive('findOpposingTransaction')->atLeast()->once()->andReturn($opposing); + $journalRepos->shouldReceive('getNoteText')->atLeast()->once()->andReturn('I am some notes.'); + $journalRepos->shouldReceive('getTags')->atLeast()->once()->andReturn(['a', 'b']); + $journalRepos->shouldReceive('getSplitJournals')->once()->andReturn($collection); + + // all meta field calls. + $journalRepos->shouldReceive('getMetaField')->atLeast()->once()->withArgs([Mockery::any(), 'internal-reference'])->andReturn('ABC'); + $journalRepos->shouldReceive('getMetaField')->atLeast()->once()->withArgs([Mockery::any(), 'sepa-cc'])->andReturnNull(); + + $journalRepos->shouldReceive('getMetaField')->atLeast()->once()->withArgs([Mockery::any(), 'sepa-ct-op'])->andReturnNull(); + $journalRepos->shouldReceive('getMetaField')->atLeast()->once()->withArgs([Mockery::any(), 'sepa-ct-id'])->andReturnNull(); + $journalRepos->shouldReceive('getMetaField')->atLeast()->once()->withArgs([Mockery::any(), 'sepa-db'])->andReturnNull(); + $journalRepos->shouldReceive('getMetaField')->atLeast()->once()->withArgs([Mockery::any(), 'sepa-country'])->andReturnNull(); + $journalRepos->shouldReceive('getMetaField')->atLeast()->once()->withArgs([Mockery::any(), 'sepa-ep'])->andReturnNull(); + $journalRepos->shouldReceive('getMetaField')->atLeast()->once()->withArgs([Mockery::any(), 'sepa-ci'])->andReturnNull(); + $journalRepos->shouldReceive('getMetaField')->atLeast()->once()->withArgs([Mockery::any(), 'sepa-batch-id'])->andReturnNull(); + $journalRepos->shouldReceive('getMetaField')->atLeast()->once()->withArgs([Mockery::any(), 'external-id'])->andReturnNull(); + $journalRepos->shouldReceive('getMetaField')->atLeast()->once()->withArgs([Mockery::any(), 'original-source'])->andReturnNull(); + $journalRepos->shouldReceive('getMetaField')->atLeast()->once()->withArgs([Mockery::any(), 'recurrence_id'])->andReturnNull(); + $journalRepos->shouldReceive('getMetaField')->atLeast()->once()->withArgs([Mockery::any(), 'bunq_payment_id'])->andReturnNull(); + $journalRepos->shouldReceive('getMetaField')->atLeast()->once()->withArgs([Mockery::any(), 'importHash'])->andReturnNull(); + $journalRepos->shouldReceive('getMetaField')->atLeast()->once()->withArgs([Mockery::any(), 'importHashV2'])->andReturnNull(); + + $journalRepos->shouldReceive('getMetaDate')->atLeast()->once()->withArgs([Mockery::any(), 'interest_date'])->andReturn($date); + $journalRepos->shouldReceive('getMetaDate')->atLeast()->once()->withArgs([Mockery::any(), 'book_date'])->andReturnNull(); + $journalRepos->shouldReceive('getMetaDate')->atLeast()->once()->withArgs([Mockery::any(), 'process_date'])->andReturnNull(); + $journalRepos->shouldReceive('getMetaDate')->atLeast()->once()->withArgs([Mockery::any(), 'due_date'])->andReturnNull(); + $journalRepos->shouldReceive('getMetaDate')->atLeast()->once()->withArgs([Mockery::any(), 'payment_date'])->andReturnNull(); + $journalRepos->shouldReceive('getMetaDate')->atLeast()->once()->withArgs([Mockery::any(), 'invoice_date'])->andReturnNull(); + + // calls to factory + $journalFactory->shouldReceive('setUser')->atLeast()->once(); + $journalFactory->shouldReceive('create')->atLeast()->once()->withAnyArgs()->andReturn(new Collection()); + + + $this->artisan('firefly:migrate-to-groups') + ->expectsOutput('Going to un-split 1 transaction(s). This could take some time.') + ->assertExitCode(0); + } + + /** + * @covers \FireflyIII\Console\Commands\Upgrade\MigrateToGroups + */ + public function testForced(): void + { + $this->mock(TransactionJournalFactory::class); + $repository = $this->mock(JournalRepositoryInterface::class); + + $repository->shouldReceive('getSplitJournals')->andReturn(new Collection); + + + $configObject = new Configuration; + $configObject->data = true; + FireflyConfig::shouldReceive('get')->withArgs(['migrated_to_groups_4780', false])->andReturn($configObject)->once(); + FireflyConfig::shouldReceive('set')->withArgs(['migrated_to_groups_4780', true])->once(); + + $this->artisan('firefly:migrate-to-groups --force') + ->expectsOutput('Forcing the migration.') + ->expectsOutput('Found no split journals. Nothing to do.') + ->assertExitCode(0); + } + + /** + * @covers \FireflyIII\Console\Commands\Upgrade\MigrateToGroups + */ + public function testNotSplit(): void + { + $this->mock(TransactionJournalFactory::class); + $repository = $this->mock(JournalRepositoryInterface::class); + $withdrawal = $this->getRandomWithdrawal(); + + $repository->shouldReceive('getSplitJournals')->andReturn(new Collection([$withdrawal])); + + + $configObject = new Configuration; + $configObject->data = false; + FireflyConfig::shouldReceive('get')->withArgs(['migrated_to_groups_4780', false])->andReturn($configObject)->once(); + FireflyConfig::shouldReceive('set')->withArgs(['migrated_to_groups_4780', true])->once(); + + $this->artisan('firefly:migrate-to-groups') + ->expectsOutput('Going to un-split 1 transaction(s). This could take some time.') + ->assertExitCode(0); + } + +} \ No newline at end of file diff --git a/tests/Unit/Factory/TransactionFactoryTest.php b/tests/Unit/Factory/TransactionFactoryTest.php index b1b83ce22a..ad113b1500 100644 --- a/tests/Unit/Factory/TransactionFactoryTest.php +++ b/tests/Unit/Factory/TransactionFactoryTest.php @@ -25,16 +25,12 @@ namespace Tests\Unit\Factory; use FireflyIII\Exceptions\FireflyException; -use FireflyIII\Factory\AccountFactory; -use FireflyIII\Factory\BudgetFactory; -use FireflyIII\Factory\CategoryFactory; -use FireflyIII\Factory\TransactionCurrencyFactory; use FireflyIII\Factory\TransactionFactory; +use FireflyIII\Models\Account; use FireflyIII\Models\AccountType; -use FireflyIII\Models\Transaction; use FireflyIII\Models\TransactionCurrency; -use FireflyIII\Models\TransactionJournal; use FireflyIII\Repositories\Account\AccountRepositoryInterface; +use FireflyIII\Support\NullArrayObject; use Log; use Tests\TestCase; @@ -55,1144 +51,684 @@ class TransactionFactoryTest extends TestCase /** * @covers \FireflyIII\Factory\TransactionFactory - * @covers \FireflyIII\Services\Internal\Support\TransactionServiceTrait */ - public function testCreatePairBasic(): void + public function testCreateBasic(): void { - // objects: - $asset = $this->user()->accounts()->where('account_type_id', 3)->first(); - $expense = $this->user()->accounts()->where('account_type_id', 4)->first(); - $euro = TransactionCurrency::first(); + // mock classes + $accountRepos = $this->mock(AccountRepositoryInterface::class); - // mocked classes - $accountRepos = $this->mock(AccountRepositoryInterface::class); - $budgetFactory = $this->mock(BudgetFactory::class); - $categoryFactory = $this->mock(CategoryFactory::class); - $currencyFactory = $this->mock(TransactionCurrencyFactory::class); + // data used in calls. + $journal = $this->getRandomWithdrawal(); + $account = $this->getRandomAsset(); + $euro = TransactionCurrency::whereCode('EUR')->first(); + $amount = '10'; - $data = [ - 'currency_id' => 1, - 'currency_code' => null, - 'description' => null, - 'source_id' => $asset->id, - 'source_name' => null, - 'destination_id' => $expense->id, - 'destination_name' => null, - 'amount' => '10', - 'reconciled' => false, - 'identifier' => 0, - 'foreign_currency_id' => null, - 'foreign_currency_code' => null, - 'foreign_amount' => null, - 'budget_id' => null, - 'budget_name' => null, - 'category_id' => null, - 'category_name' => null, - ]; + // mock calls. + $accountRepos->shouldReceive('setUser')->once(); - // mock: - $accountRepos->shouldReceive('setUser'); - $budgetFactory->shouldReceive('setUser'); - $categoryFactory->shouldReceive('setUser'); - // first search action is for the asset account, second is for expense account. - $accountRepos->shouldReceive('findNull')->andReturn($asset, $expense); - - // factories return various stuff: - $budgetFactory->shouldReceive('find')->andReturn(null); - $categoryFactory->shouldReceive('findOrCreate')->andReturn(null); - $currencyFactory->shouldReceive('find')->andReturn($euro, null); - - /** @var TransactionJournal $withdrawal */ - $withdrawal = $this->user()->transactionJournals()->where('transaction_type_id', 1)->first(); - $count = $withdrawal->transactions()->count(); /** @var TransactionFactory $factory */ $factory = app(TransactionFactory::class); $factory->setUser($this->user()); - try { - $collection = $factory->createPair($withdrawal, $data); - } catch (FireflyException $e) { - $this->assertTrue(false, $e->getMessage()); - } + $factory->setJournal($journal); + $transaction = $factory->create($account, $euro, $amount); - $newCount = $withdrawal->transactions()->count(); - - $this->assertCount(2, $collection); - $this->assertEquals($count + 2, $newCount); - // find stuff in transaction #1 (should suffice): - /** @var Transaction $first */ - $first = $collection->first(); - $this->assertEquals($withdrawal->id, $first->transaction_journal_id); - $this->assertEquals(-10, $first->amount); - $this->assertEquals(false, $first->reconciled); - $this->assertEquals(0, $first->identifier); - $this->assertEquals($euro->id, $first->transaction_currency_id); - $this->assertNull($first->foreign_amount); - $this->assertNull($first->foreign_currency_id); + $this->assertEquals($transaction->account_id, $account->id); } /** * @covers \FireflyIII\Factory\TransactionFactory - * @covers \FireflyIII\Services\Internal\Support\TransactionServiceTrait */ - public function testCreatePairBasicByName(): void + public function testCreateNull(): void { - // objects: - $asset = $this->user()->accounts()->where('account_type_id', 3)->first(); - $expense = $this->user()->accounts()->where('account_type_id', 4)->first(); - $euro = TransactionCurrency::first(); + // mock classes + $accountRepos = $this->mock(AccountRepositoryInterface::class); - // mocked classes - $accountRepos = $this->mock(AccountRepositoryInterface::class); - $budgetFactory = $this->mock(BudgetFactory::class); - $categoryFactory = $this->mock(CategoryFactory::class); - $currencyFactory = $this->mock(TransactionCurrencyFactory::class); - $accountFactory = $this->mock(AccountFactory::class); + // data used in calls. + $journal = $this->getRandomWithdrawal(); + $account = new Account; + $euro = TransactionCurrency::whereCode('EUR')->first(); + $amount = '10'; - $data = [ - 'currency_id' => 1, - 'currency_code' => null, - 'description' => null, - 'source_id' => null, - 'source_name' => $asset->name, - 'destination_id' => null, - 'destination_name' => $expense->name, - 'amount' => '10', - 'reconciled' => false, - 'identifier' => 0, - 'foreign_currency_id' => null, - 'foreign_currency_code' => null, - 'foreign_amount' => null, - 'budget_id' => null, - 'budget_name' => null, - 'category_id' => null, - 'category_name' => null, - ]; + // mock calls. + $accountRepos->shouldReceive('setUser')->once(); - // mock: - $accountRepos->shouldReceive('setUser'); - $budgetFactory->shouldReceive('setUser'); - $categoryFactory->shouldReceive('setUser'); - $accountFactory->shouldReceive('setUser'); - // first search action is for the asset account - $accountRepos->shouldReceive('findByName')->withArgs([$asset->name, [AccountType::ASSET]])->andReturn($asset); - // second is for expense account. - $accountFactory->shouldReceive('findOrCreate')->andReturn($expense); - - // factories return various stuff: - $budgetFactory->shouldReceive('find')->andReturn(null); - $categoryFactory->shouldReceive('findOrCreate')->andReturn(null); - $currencyFactory->shouldReceive('find')->andReturn($euro, null); - - /** @var TransactionJournal $withdrawal */ - $withdrawal = $this->user()->transactionJournals()->where('transaction_type_id', 1)->first(); - $count = $withdrawal->transactions()->count(); /** @var TransactionFactory $factory */ $factory = app(TransactionFactory::class); $factory->setUser($this->user()); - try { - $collection = $factory->createPair($withdrawal, $data); - } catch (FireflyException $e) { - $this->assertTrue(false, $e->getMessage()); - } + $factory->setJournal($journal); + $transaction = $factory->create($account, $euro, $amount); - $newCount = $withdrawal->transactions()->count(); - - $this->assertCount(2, $collection); - $this->assertEquals($count + 2, $newCount); - // find stuff in transaction #1 (should suffice): - /** @var Transaction $first */ - $first = $collection->first(); - $this->assertEquals($withdrawal->id, $first->transaction_journal_id); - $this->assertEquals(-10, $first->amount); - $this->assertEquals(false, $first->reconciled); - $this->assertEquals(0, $first->identifier); - $this->assertEquals($euro->id, $first->transaction_currency_id); - $this->assertNull($first->foreign_amount); - $this->assertNull($first->foreign_currency_id); + $this->assertNull($transaction); } /** * @covers \FireflyIII\Factory\TransactionFactory - * @covers \FireflyIII\Services\Internal\Support\TransactionServiceTrait */ - public function testCreatePairBasicIntoCash(): void + public function testCreatePair(): void { - // objects: - $asset = $this->user()->accounts()->where('account_type_id', 3)->first(); - $expense = $this->user()->accounts()->where('account_type_id', 4)->first(); - $euro = TransactionCurrency::first(); + // mock classes + $accountRepos = $this->mock(AccountRepositoryInterface::class); - // mocked classes - $accountRepos = $this->mock(AccountRepositoryInterface::class); - $budgetFactory = $this->mock(BudgetFactory::class); - $categoryFactory = $this->mock(CategoryFactory::class); - $currencyFactory = $this->mock(TransactionCurrencyFactory::class); - $accountFactory = $this->mock(AccountFactory::class); + // used objects + $withdrawal = $this->getRandomWithdrawal(); + $asset = $this->getRandomAsset(); + $expense = $this->getRandomExpense(); + $currency = TransactionCurrency::whereCode('EUR')->first(); - $data = [ - 'currency_id' => 1, - 'currency_code' => null, - 'description' => null, - 'source_id' => null, - 'source_name' => $asset->name, - 'destination_id' => null, - 'destination_name' => '', - 'amount' => '10', - 'reconciled' => false, - 'identifier' => 0, - 'foreign_currency_id' => null, - 'foreign_currency_code' => null, - 'foreign_amount' => null, - 'budget_id' => null, - 'budget_name' => null, - 'category_id' => null, - 'category_name' => null, - ]; + // mock calls. + $accountRepos->shouldReceive('setUser')->once(); + $accountRepos->shouldReceive('findNull')->withArgs([1])->once()->andReturn($asset); + $accountRepos->shouldReceive('findByName')->withArgs(['Some destination', [AccountType::EXPENSE]])->once()->andReturn($expense); - // mock: - $accountRepos->shouldReceive('setUser'); - $budgetFactory->shouldReceive('setUser'); - $categoryFactory->shouldReceive('setUser'); - $accountFactory->shouldReceive('setUser'); - // first search action is for the asset account - $accountRepos->shouldReceive('findByName')->withArgs([$asset->name, [AccountType::ASSET]])->andReturn($asset); - // second is for expense account (cash) - $accountRepos->shouldReceive('getCashAccount')->andReturn($expense); - - // factories return various stuff: - $budgetFactory->shouldReceive('find')->andReturn(null); - $categoryFactory->shouldReceive('findOrCreate')->andReturn(null); - $currencyFactory->shouldReceive('find')->andReturn($euro, null); - - /** @var TransactionJournal $withdrawal */ - $withdrawal = $this->user()->transactionJournals()->where('transaction_type_id', 1)->first(); - $count = $withdrawal->transactions()->count(); + $data = new NullArrayObject( + [ + 'source_id' => 1, + 'destination_name' => 'Some destination', + 'amount' => '20', + ] + ); /** @var TransactionFactory $factory */ $factory = app(TransactionFactory::class); $factory->setUser($this->user()); - try { - $collection = $factory->createPair($withdrawal, $data); - } catch (FireflyException $e) { - $this->assertTrue(false, $e->getMessage()); - } - $newCount = $withdrawal->transactions()->count(); - - $this->assertCount(2, $collection); - $this->assertEquals($count + 2, $newCount); - // find stuff in transaction #1 (should suffice): - /** @var Transaction $first */ - $first = $collection->first(); - $this->assertEquals($withdrawal->id, $first->transaction_journal_id); - $this->assertEquals(-10, $first->amount); - $this->assertEquals(false, $first->reconciled); - $this->assertEquals(0, $first->identifier); - $this->assertEquals($euro->id, $first->transaction_currency_id); - $this->assertNull($first->foreign_amount); - $this->assertNull($first->foreign_currency_id); - } - - /** - * Add budget and category data. - * - * @covers \FireflyIII\Factory\TransactionFactory - * @covers \FireflyIII\Services\Internal\Support\TransactionServiceTrait - */ - public function testCreatePairBasicMeta(): void - { - // objects: - $asset = $this->user()->accounts()->where('account_type_id', 3)->first(); - $expense = $this->user()->accounts()->where('account_type_id', 4)->first(); - $budget = $this->user()->budgets()->first(); - $category = $this->user()->categories()->first(); - $euro = TransactionCurrency::first(); - - // mocked classes - $accountRepos = $this->mock(AccountRepositoryInterface::class); - $budgetFactory = $this->mock(BudgetFactory::class); - $categoryFactory = $this->mock(CategoryFactory::class); - $currencyFactory = $this->mock(TransactionCurrencyFactory::class); - - $data = [ - 'currency_id' => 1, - 'currency_code' => null, - 'description' => null, - 'source_id' => $asset->id, - 'source_name' => null, - 'destination_id' => $expense->id, - 'destination_name' => null, - 'amount' => '10', - 'reconciled' => false, - 'identifier' => 0, - 'foreign_currency_id' => null, - 'foreign_currency_code' => null, - 'foreign_amount' => null, - 'budget_id' => $budget->id, - 'budget_name' => null, - 'category_id' => $category->id, - 'category_name' => null, - ]; - - // mock: - $accountRepos->shouldReceive('setUser'); - $budgetFactory->shouldReceive('setUser'); - $categoryFactory->shouldReceive('setUser'); - // first search action is for the asset account, second is for expense account. - $accountRepos->shouldReceive('findNull')->andReturn($asset, $expense); - - // factories return various stuff: - $budgetFactory->shouldReceive('find')->andReturn($budget); - $categoryFactory->shouldReceive('findOrCreate')->andReturn($category); - $currencyFactory->shouldReceive('find')->andReturn($euro, null); - - /** @var TransactionJournal $withdrawal */ - $withdrawal = $this->user()->transactionJournals()->where('transaction_type_id', 1)->first(); - $count = $withdrawal->transactions()->count(); - - /** @var TransactionFactory $factory */ - $factory = app(TransactionFactory::class); - $factory->setUser($this->user()); - try { - $collection = $factory->createPair($withdrawal, $data); - } catch (FireflyException $e) { - $this->assertTrue(false, $e->getMessage()); - } - $newCount = $withdrawal->transactions()->count(); - - $this->assertCount(2, $collection); - $this->assertEquals($count + 2, $newCount); - // find stuff in transaction #1 (should suffice): - /** @var Transaction $first */ - $first = $collection->first(); - $this->assertEquals($withdrawal->id, $first->transaction_journal_id); - $this->assertEquals(-10, $first->amount); - $this->assertEquals(false, $first->reconciled); - $this->assertEquals(0, $first->identifier); - $this->assertEquals($euro->id, $first->transaction_currency_id); - $this->assertNull($first->foreign_amount); - $this->assertNull($first->foreign_currency_id); - $this->assertEquals($budget->name, $first->budgets()->first()->name); - $this->assertEquals($category->name, $first->categories()->first()->name); - } - - /** - * Create deposit using minimal data. - * - * @covers \FireflyIII\Factory\TransactionFactory - * @covers \FireflyIII\Services\Internal\Support\TransactionServiceTrait - */ - public function testCreatePairDeposit(): void - { - // objects: - $asset = $this->user()->accounts()->where('account_type_id', 3)->first(); - $revenue = $this->user()->accounts()->where('account_type_id', 5)->first(); - $euro = TransactionCurrency::first(); - $foreign = TransactionCurrency::where('id', '!=', $euro->id)->first(); - - // mocked classes - $accountRepos = $this->mock(AccountRepositoryInterface::class); - $budgetFactory = $this->mock(BudgetFactory::class); - $categoryFactory = $this->mock(CategoryFactory::class); - $currencyFactory = $this->mock(TransactionCurrencyFactory::class); - - $data = [ - 'currency_id' => 1, - 'currency_code' => null, - 'description' => null, - 'source_id' => $revenue->id, - 'source_name' => null, - 'destination_id' => $asset->id, - 'destination_name' => null, - 'amount' => '10', - 'reconciled' => false, - 'identifier' => 0, - 'foreign_currency_id' => null, - 'foreign_currency_code' => null, - 'foreign_amount' => null, - 'budget_id' => null, - 'budget_name' => null, - 'category_id' => null, - 'category_name' => null, - ]; - - // mock: - $accountRepos->shouldReceive('setUser'); - $budgetFactory->shouldReceive('setUser'); - $categoryFactory->shouldReceive('setUser'); - // first search action is for the asset account, second is for expense account. - $accountRepos->shouldReceive('findNull')->andReturn($revenue, $asset); - - // factories return various stuff: - $budgetFactory->shouldReceive('find')->andReturn(null); - $categoryFactory->shouldReceive('findOrCreate')->andReturn(null); - $currencyFactory->shouldReceive('find')->andReturn($euro, null); - - /** @var TransactionJournal $deposit */ - $deposit = $this->user()->transactionJournals()->where('transaction_type_id', 2)->first(); - $count = $deposit->transactions()->count(); - - /** @var TransactionFactory $factory */ - $factory = app(TransactionFactory::class); - $factory->setUser($this->user()); - try { - $collection = $factory->createPair($deposit, $data); - } catch (FireflyException $e) { - $this->assertTrue(false, $e->getMessage()); - } - $newCount = $deposit->transactions()->count(); - - $this->assertCount(2, $collection); - $this->assertEquals($count + 2, $newCount); - // find stuff in transaction #1 (should suffice): - /** @var Transaction $first */ - $first = $collection->first(); - $this->assertEquals($deposit->id, $first->transaction_journal_id); - $this->assertEquals(-10, $first->amount); - $this->assertEquals(false, $first->reconciled); - $this->assertEquals(0, $first->identifier); - $this->assertEquals($euro->id, $first->transaction_currency_id); - $this->assertNull($first->foreign_amount); - $this->assertNull($first->foreign_currency_id); - } - - /** - * Create deposit using minimal data. - * - * @covers \FireflyIII\Factory\TransactionFactory - * @covers \FireflyIII\Services\Internal\Support\TransactionServiceTrait - */ - public function testCreatePairDepositByName(): void - { - // objects: - $asset = $this->user()->accounts()->where('account_type_id', 3)->first(); - $revenue = $this->user()->accounts()->where('account_type_id', 5)->first(); - $euro = TransactionCurrency::first(); - $foreign = TransactionCurrency::where('id', '!=', $euro->id)->first(); - - // mocked classes - $accountRepos = $this->mock(AccountRepositoryInterface::class); - $budgetFactory = $this->mock(BudgetFactory::class); - $categoryFactory = $this->mock(CategoryFactory::class); - $currencyFactory = $this->mock(TransactionCurrencyFactory::class); - $accountFactory = $this->mock(AccountFactory::class); - - $data = [ - 'currency_id' => 1, - 'currency_code' => null, - 'description' => null, - 'source_id' => null, - 'source_name' => $revenue->name, - 'destination_id' => $asset->id, - 'destination_name' => null, - 'amount' => '10', - 'reconciled' => false, - 'identifier' => 0, - 'foreign_currency_id' => null, - 'foreign_currency_code' => null, - 'foreign_amount' => null, - 'budget_id' => null, - 'budget_name' => null, - 'category_id' => null, - 'category_name' => null, - ]; - - // mock: - $accountRepos->shouldReceive('setUser'); - $budgetFactory->shouldReceive('setUser'); - $categoryFactory->shouldReceive('setUser'); - $accountFactory->shouldReceive('setUser'); - // first search action is for the asset account - $accountRepos->shouldReceive('findNull')->andReturn($asset); - // second is for revenue account. - $accountFactory->shouldReceive('findOrCreate')->andReturn($revenue); - - // factories return various stuff: - $budgetFactory->shouldReceive('find')->andReturn(null); - $categoryFactory->shouldReceive('findOrCreate')->andReturn(null); - $currencyFactory->shouldReceive('find')->andReturn($euro, null); - - /** @var TransactionJournal $deposit */ - $deposit = $this->user()->transactionJournals()->where('transaction_type_id', 2)->first(); - $count = $deposit->transactions()->count(); - - /** @var TransactionFactory $factory */ - $factory = app(TransactionFactory::class); - $factory->setUser($this->user()); - try { - $collection = $factory->createPair($deposit, $data); - } catch (FireflyException $e) { - $this->assertTrue(false, $e->getMessage()); - } - - $newCount = $deposit->transactions()->count(); - - $this->assertCount(2, $collection); - $this->assertEquals($count + 2, $newCount); - // find stuff in transaction #1 (should suffice): - /** @var Transaction $first */ - $first = $collection->first(); - $this->assertEquals($deposit->id, $first->transaction_journal_id); - $this->assertEquals(-10, $first->amount); - $this->assertEquals(false, $first->reconciled); - $this->assertEquals(0, $first->identifier); - $this->assertEquals($euro->id, $first->transaction_currency_id); - $this->assertNull($first->foreign_amount); - $this->assertNull($first->foreign_currency_id); - } - - /** - * Create deposit using minimal data. - * - * @covers \FireflyIII\Factory\TransactionFactory - * @covers \FireflyIII\Services\Internal\Support\TransactionServiceTrait - */ - public function testCreatePairDepositCash(): void - { - // objects: - $asset = $this->user()->accounts()->where('account_type_id', 3)->first(); - $revenue = $this->user()->accounts()->where('account_type_id', 5)->first(); - $euro = TransactionCurrency::first(); - $foreign = TransactionCurrency::where('id', '!=', $euro->id)->first(); - - // mocked classes - $accountRepos = $this->mock(AccountRepositoryInterface::class); - $budgetFactory = $this->mock(BudgetFactory::class); - $categoryFactory = $this->mock(CategoryFactory::class); - $currencyFactory = $this->mock(TransactionCurrencyFactory::class); - $accountFactory = $this->mock(AccountFactory::class); - - $data = [ - 'currency_id' => 1, - 'currency_code' => null, - 'description' => null, - 'source_id' => null, - 'source_name' => '', - 'destination_id' => $asset->id, - 'destination_name' => null, - 'amount' => '10', - 'reconciled' => false, - 'identifier' => 0, - 'foreign_currency_id' => null, - 'foreign_currency_code' => null, - 'foreign_amount' => null, - 'budget_id' => null, - 'budget_name' => null, - 'category_id' => null, - 'category_name' => null, - ]; - - // mock: - $accountRepos->shouldReceive('setUser'); - $budgetFactory->shouldReceive('setUser'); - $categoryFactory->shouldReceive('setUser'); - $accountFactory->shouldReceive('setUser'); - // first search action is for the asset account - $accountRepos->shouldReceive('findNull')->andReturn($asset); - - // second is for revenue account. - $accountRepos->shouldReceive('getCashAccount')->andReturn($revenue)->once(); - - // factories return various stuff: - $budgetFactory->shouldReceive('find')->andReturn(null); - $categoryFactory->shouldReceive('findOrCreate')->andReturn(null); - $currencyFactory->shouldReceive('find')->andReturn($euro, null); - - /** @var TransactionJournal $deposit */ - $deposit = $this->user()->transactionJournals()->where('transaction_type_id', 2)->first(); - $count = $deposit->transactions()->count(); - - /** @var TransactionFactory $factory */ - $factory = app(TransactionFactory::class); - $factory->setUser($this->user()); - try { - $collection = $factory->createPair($deposit, $data); - } catch (FireflyException $e) { - $this->assertTrue(false, $e->getMessage()); - } - - $newCount = $deposit->transactions()->count(); - - $this->assertCount(2, $collection); - $this->assertEquals($count + 2, $newCount); - // find stuff in transaction #1 (should suffice): - /** @var Transaction $first */ - $first = $collection->first(); - $this->assertEquals($deposit->id, $first->transaction_journal_id); - $this->assertEquals(-10, $first->amount); - $this->assertEquals(false, $first->reconciled); - $this->assertEquals(0, $first->identifier); - $this->assertEquals($euro->id, $first->transaction_currency_id); - $this->assertNull($first->foreign_amount); - $this->assertNull($first->foreign_currency_id); + $factory->setJournal($withdrawal); + $pairs = $factory->createPair($data, $currency, null); + $first = $pairs->first(); + $this->assertCount(2, $pairs); + $this->assertEquals('-20', $first->amount); + $this->assertEquals($currency->id, $first->transaction_currency_id); } /** * @covers \FireflyIII\Factory\TransactionFactory - * @covers \FireflyIII\Services\Internal\Support\TransactionServiceTrait - */ - public function testCreatePairEmptyAmount(): void - { - // objects: - $asset = $this->user()->accounts()->where('account_type_id', 3)->first(); - $expense = $this->user()->accounts()->where('account_type_id', 4)->first(); - $euro = TransactionCurrency::first(); - - // mocked classes - $accountRepos = $this->mock(AccountRepositoryInterface::class); - $budgetFactory = $this->mock(BudgetFactory::class); - $categoryFactory = $this->mock(CategoryFactory::class); - $currencyFactory = $this->mock(TransactionCurrencyFactory::class); - - $data = [ - 'currency_id' => 1, - 'currency_code' => null, - 'description' => null, - 'source_id' => $asset->id, - 'source_name' => null, - 'destination_id' => $expense->id, - 'destination_name' => null, - 'amount' => '', - 'reconciled' => false, - 'identifier' => 0, - 'foreign_currency_id' => null, - 'foreign_currency_code' => null, - 'foreign_amount' => null, - 'budget_id' => null, - 'budget_name' => null, - 'category_id' => null, - 'category_name' => null, - ]; - - // mock: - $accountRepos->shouldReceive('setUser'); - $budgetFactory->shouldReceive('setUser'); - $categoryFactory->shouldReceive('setUser'); - // first search action is for the asset account, second is for expense account. - $accountRepos->shouldReceive('findNull')->andReturn($asset, $expense)->atLeast()->once(); - - // factories return various stuff: - $currencyFactory->shouldReceive('find')->andReturn($euro, null)->atLeast()->once(); - - /** @var TransactionJournal $withdrawal */ - $withdrawal = $this->user()->transactionJournals()->where('transaction_type_id', 1)->first(); - $count = $withdrawal->transactions()->count(); - - /** @var TransactionFactory $factory */ - $factory = app(TransactionFactory::class); - $factory->setUser($this->user()); - try { - $factory->createPair($withdrawal, $data); - } catch (FireflyException $e) { - $this->assertEquals('Amount is an empty string, which Firefly III cannot handle. Apologies.', $e->getMessage()); - } - - $newCount = $withdrawal->transactions()->count(); - $this->assertEquals($count, $newCount); - } - - /** - * @covers \FireflyIII\Factory\TransactionFactory - * @covers \FireflyIII\Services\Internal\Support\TransactionServiceTrait */ public function testCreatePairForeign(): void { - // objects: - $asset = $this->user()->accounts()->where('account_type_id', 3)->first(); - $expense = $this->user()->accounts()->where('account_type_id', 4)->first(); - $euro = TransactionCurrency::first(); - $foreign = TransactionCurrency::where('id', '!=', $euro->id)->first(); + // mock classes + $accountRepos = $this->mock(AccountRepositoryInterface::class); - // mocked classes - $accountRepos = $this->mock(AccountRepositoryInterface::class); - $budgetFactory = $this->mock(BudgetFactory::class); - $categoryFactory = $this->mock(CategoryFactory::class); - $currencyFactory = $this->mock(TransactionCurrencyFactory::class); + // used objects: + $withdrawal = $this->getRandomWithdrawal(); + $expense = $this->getRandomExpense(); + $asset = $this->getRandomAsset(); + $currency = TransactionCurrency::whereCode('EUR')->first(); + $foreign = TransactionCurrency::whereCode('USD')->first(); - $data = [ - 'currency_id' => $euro->id, - 'currency_code' => null, - 'description' => null, - 'source_id' => $asset->id, - 'source_name' => null, - 'destination_id' => $expense->id, - 'destination_name' => null, - 'amount' => '10', - 'reconciled' => false, - 'identifier' => 0, - 'foreign_currency_id' => $foreign->id, - 'foreign_currency_code' => null, - 'foreign_amount' => '10', - 'budget_id' => null, - 'budget_name' => null, - 'category_id' => null, - 'category_name' => null, - ]; + // mock calls. + $accountRepos->shouldReceive('setUser')->once(); + $accountRepos->shouldReceive('findNull')->withArgs([1])->once()->andReturn($asset); + $accountRepos->shouldReceive('findByName')->withArgs(['Some destination', [AccountType::EXPENSE]])->once()->andReturn($expense); - // mock: - $accountRepos->shouldReceive('setUser'); - $budgetFactory->shouldReceive('setUser'); - $categoryFactory->shouldReceive('setUser'); - // first search action is for the asset account, second is for expense account. - $accountRepos->shouldReceive('findNull')->andReturn($asset, $expense); - - // factories return various stuff: - $budgetFactory->shouldReceive('find')->andReturn(null); - $categoryFactory->shouldReceive('findOrCreate')->andReturn(null); - $currencyFactory->shouldReceive('find')->andReturn($euro, $foreign); - - /** @var TransactionJournal $withdrawal */ - $withdrawal = $this->user()->transactionJournals()->where('transaction_type_id', 1)->first(); - $count = $withdrawal->transactions()->count(); + $data = new NullArrayObject( + [ + 'source_id' => 1, + 'destination_name' => 'Some destination', + 'amount' => '20', + 'foreign_amount' => '20', + ] + ); /** @var TransactionFactory $factory */ $factory = app(TransactionFactory::class); $factory->setUser($this->user()); - try { - $collection = $factory->createPair($withdrawal, $data); - } catch (FireflyException $e) { - $this->assertTrue(false, $e->getMessage()); - } - $newCount = $withdrawal->transactions()->count(); - - $this->assertCount(2, $collection); - $this->assertEquals($count + 2, $newCount); - // find stuff in transaction #1 (should suffice): - /** @var Transaction $first */ - $first = $collection->first(); - $this->assertEquals($withdrawal->id, $first->transaction_journal_id); - $this->assertEquals(-10, $first->amount); - $this->assertEquals(-10, $first->foreign_amount); - $this->assertEquals(false, $first->reconciled); - $this->assertEquals(0, $first->identifier); - $this->assertEquals($euro->id, $first->transaction_currency_id); + $factory->setJournal($withdrawal); + $pairs = $factory->createPair($data, $currency, $foreign); + $first = $pairs->first(); + $this->assertCount(2, $pairs); + $this->assertEquals('-20', $first->amount); + $this->assertEquals($currency->id, $first->transaction_currency_id); $this->assertEquals($foreign->id, $first->foreign_currency_id); } /** - * @covers \FireflyIII\Factory\TransactionFactory - * @covers \FireflyIII\Services\Internal\Support\TransactionServiceTrait - */ - public function testCreatePairNoAccounts(): void - { - // objects: - $asset = $this->user()->accounts()->where('account_type_id', 3)->first(); - $expense = $this->user()->accounts()->where('account_type_id', 4)->first(); - $euro = TransactionCurrency::first(); - - // mocked classes - $accountRepos = $this->mock(AccountRepositoryInterface::class); - $budgetFactory = $this->mock(BudgetFactory::class); - $categoryFactory = $this->mock(CategoryFactory::class); - $currencyFactory = $this->mock(TransactionCurrencyFactory::class); - - $data = [ - 'currency_id' => 1, - 'currency_code' => null, - 'description' => null, - 'source_id' => $asset->id, - 'source_name' => null, - 'destination_id' => $expense->id, - 'destination_name' => null, - 'amount' => '10', - 'reconciled' => false, - 'identifier' => 0, - 'foreign_currency_id' => null, - 'foreign_currency_code' => null, - 'foreign_amount' => null, - 'budget_id' => null, - 'budget_name' => null, - 'category_id' => null, - 'category_name' => null, - ]; - - // mock: - $accountRepos->shouldReceive('setUser'); - $budgetFactory->shouldReceive('setUser'); - $categoryFactory->shouldReceive('setUser'); - // first search action is for the asset account, second is for expense account. - $accountRepos->shouldReceive('findNull')->andReturn(null, null)->atLeast()->once(); - - // factories return various stuff: - $currencyFactory->shouldReceive('find')->andReturn($euro, null)->atLeast()->once(); - - /** @var TransactionJournal $withdrawal */ - $withdrawal = $this->user()->transactionJournals()->where('transaction_type_id', 1)->first(); - $count = $withdrawal->transactions()->count(); - - /** @var TransactionFactory $factory */ - $factory = app(TransactionFactory::class); - $factory->setUser($this->user()); - try { - $factory->createPair($withdrawal, $data); - } catch (FireflyException $e) { - $this->assertEquals('Could not determine source or destination account.', $e->getMessage()); - } - - $newCount = $withdrawal->transactions()->count(); - - $this->assertEquals($count, $newCount); - } - - /** - * @covers \FireflyIII\Factory\TransactionFactory - * @covers \FireflyIII\Services\Internal\Support\TransactionServiceTrait - */ - public function testCreatePairNoCurrency(): void - { - // objects: - $asset = $this->user()->accounts()->where('account_type_id', 3)->first(); - $expense = $this->user()->accounts()->where('account_type_id', 4)->first(); - - // mocked classes - $accountRepos = $this->mock(AccountRepositoryInterface::class); - $budgetFactory = $this->mock(BudgetFactory::class); - $categoryFactory = $this->mock(CategoryFactory::class); - $currencyFactory = $this->mock(TransactionCurrencyFactory::class); - - $data = [ - 'description' => null, - 'source_id' => $asset->id, - 'source_name' => null, - 'destination_id' => $expense->id, - 'currency_id' => null, - 'currency_code' => null, - 'destination_name' => null, - 'amount' => '10', - 'reconciled' => false, - 'identifier' => 0, - 'foreign_currency_id' => null, - 'foreign_currency_code' => null, - 'foreign_amount' => null, - 'budget_id' => null, - 'budget_name' => null, - 'category_id' => null, - 'category_name' => null, - ]; - - // mock: - $accountRepos->shouldReceive('setUser'); - $budgetFactory->shouldReceive('setUser'); - $categoryFactory->shouldReceive('setUser'); - // first search action is for the asset account, second is for expense account. - $accountRepos->shouldReceive('findNull')->andReturn($asset, $expense)->atLeast()->once(); - - // Find budget, but based on null it returns null. - $budgetFactory->shouldReceive('find')->withArgs([null, null])->andReturnNull()->atLeast()->once(); - - // find category, but none are present so return null. - $categoryFactory->shouldReceive('findOrCreate')->withArgs([null, null])->andReturnNull()->atLeast()->once(); - - // factories return various stuff: - $currencyFactory->shouldReceive('find')->andReturn(null, null)->atLeast()->once(); - - /** @var TransactionJournal $withdrawal */ - $withdrawal = $this->user()->transactionJournals()->where('transaction_type_id', 1)->first(); - $count = $withdrawal->transactions()->count(); - - /** @var TransactionFactory $factory */ - $factory = app(TransactionFactory::class); - $factory->setUser($this->user()); - try { - $factory->createPair($withdrawal, $data); - } catch (FireflyException $e) { - $this->assertEquals('Cannot store transaction without currency information.', $e->getMessage()); - } - - $newCount = $withdrawal->transactions()->count(); - - $this->assertEquals($count, $newCount - 2); - } - - /** - * Create reconciliation using minimal data. + * To cover everything, test several combinations. + * + * For the source account, submit a Deposit and an Revenue account ID (this is OK). + * Expected result: the same revenue account. * * @covers \FireflyIII\Factory\TransactionFactory - * @covers \FireflyIII\Services\Internal\Support\TransactionServiceTrait */ - public function testCreatePairReconciliation(): void + public function testDepositSourceAsseRevenueId(): void { - // objects: - $asset = $this->user()->accounts()->where('account_type_id', 3)->first(); - $reconAccount = $this->user()->accounts()->where('account_type_id', 10)->first(); - $euro = TransactionCurrency::first(); - $foreign = TransactionCurrency::where('id', '!=', $euro->id)->first(); + // mock classes + $accountRepos = $this->mock(AccountRepositoryInterface::class); - // mocked classes - $accountRepos = $this->mock(AccountRepositoryInterface::class); - $budgetFactory = $this->mock(BudgetFactory::class); - $categoryFactory = $this->mock(CategoryFactory::class); - $currencyFactory = $this->mock(TransactionCurrencyFactory::class); + // data used in calls. + $deposit = $this->getRandomDeposit(); + $revenue = $this->getRandomRevenue(); - $data = [ - 'currency_id' => 1, - 'currency_code' => null, - 'description' => null, - 'source_id' => $asset->id, - 'source_name' => null, - 'destination_id' => $reconAccount->id, - 'destination_name' => null, - 'amount' => '10', - 'reconciled' => false, - 'identifier' => 0, - 'foreign_currency_id' => null, - 'foreign_currency_code' => null, - 'foreign_amount' => null, - 'budget_id' => null, - 'budget_name' => null, - 'category_id' => null, - 'category_name' => null, - ]; - - // mock: - $accountRepos->shouldReceive('setUser'); - $budgetFactory->shouldReceive('setUser'); - $categoryFactory->shouldReceive('setUser'); - // first search action is for the asset account, second is for expense account. - $accountRepos->shouldReceive('findNull')->andReturn($asset, $reconAccount); - - // factories return various stuff: - $budgetFactory->shouldReceive('find')->andReturn(null); - $categoryFactory->shouldReceive('findOrCreate')->andReturn(null); - $currencyFactory->shouldReceive('find')->andReturn($euro, null); - - /** @var TransactionJournal $recon */ - $recon = $this->user()->transactionJournals()->where('transaction_type_id', 5)->first(); - $count = $recon->transactions()->count(); + // mock calls. + $accountRepos->shouldReceive('setUser')->once(); + $accountRepos->shouldReceive('findNull')->once()->withArgs([$revenue->id])->andReturn($revenue); /** @var TransactionFactory $factory */ $factory = app(TransactionFactory::class); $factory->setUser($this->user()); - try { - $collection = $factory->createPair($recon, $data); - } catch (FireflyException $e) { - $this->assertTrue(false, $e->getMessage()); - } + $factory->setJournal($deposit); - $newCount = $recon->transactions()->count(); - - $this->assertCount(2, $collection); - $this->assertEquals($count + 2, $newCount); - // find stuff in transaction #1 (should suffice): - /** @var Transaction $first */ - $first = $collection->first(); - $this->assertEquals($recon->id, $first->transaction_journal_id); - $this->assertEquals(-10, $first->amount); - $this->assertEquals(false, $first->reconciled); - $this->assertEquals(0, $first->identifier); - $this->assertEquals($euro->id, $first->transaction_currency_id); - $this->assertNull($first->foreign_amount); - $this->assertNull($first->foreign_currency_id); + $result = $factory->getAccount('source', null, $revenue->id, null); + $this->assertEquals($revenue->id, $result->id); } /** - * @covers \FireflyIII\Factory\TransactionFactory - * @covers \FireflyIII\Services\Internal\Support\TransactionServiceTrait - */ - public function testCreatePairSameBadType(): void - { - // objects: - $expense = $this->user()->accounts()->where('account_type_id', 4)->first(); - $revenue = $this->user()->accounts()->where('account_type_id', 5)->first(); - $euro = TransactionCurrency::first(); - - // mocked classes - $accountRepos = $this->mock(AccountRepositoryInterface::class); - $budgetFactory = $this->mock(BudgetFactory::class); - $categoryFactory = $this->mock(CategoryFactory::class); - $currencyFactory = $this->mock(TransactionCurrencyFactory::class); - - $data = [ - 'currency_id' => 1, - 'currency_code' => null, - 'description' => null, - 'source_id' => $expense->id, - 'source_name' => null, - 'destination_id' => $revenue->id, - 'destination_name' => null, - 'amount' => '10', - 'reconciled' => false, - 'identifier' => 0, - 'foreign_currency_id' => null, - 'foreign_currency_code' => null, - 'foreign_amount' => null, - 'budget_id' => null, - 'budget_name' => null, - 'category_id' => null, - 'category_name' => null, - ]; - - // mock: - $accountRepos->shouldReceive('setUser'); - $budgetFactory->shouldReceive('setUser'); - $categoryFactory->shouldReceive('setUser'); - // first search action is for the asset account, second is for expense account. - $accountRepos->shouldReceive('findNull')->andReturn($expense, $revenue); - - // factories return various stuff: - $currencyFactory->shouldReceive('find')->andReturn($euro, null)->atLeast()->once(); - - /** @var TransactionJournal $withdrawal */ - $withdrawal = $this->user()->transactionJournals()->where('transaction_type_id', 1)->first(); - $count = $withdrawal->transactions()->count(); - - /** @var TransactionFactory $factory */ - $factory = app(TransactionFactory::class); - $factory->setUser($this->user()); - try { - $factory->createPair($withdrawal, $data); - } catch (FireflyException $e) { - $this->assertEquals('At least one of the accounts must be an asset account (Expense account, Revenue account).', $e->getMessage()); - } - - $newCount = $withdrawal->transactions()->count(); - $this->assertEquals($count, $newCount); - } - - /** - * @covers \FireflyIII\Factory\TransactionFactory - * @covers \FireflyIII\Services\Internal\Support\TransactionServiceTrait - */ - public function testCreatePairSameType(): void - { - // objects: - $asset = $this->user()->accounts()->where('account_type_id', 3)->first(); - $alsoAsset = $this->user()->accounts()->where('account_type_id', 3)->first(); - $euro = TransactionCurrency::first(); - - // mocked classes - $accountRepos = $this->mock(AccountRepositoryInterface::class); - $budgetFactory = $this->mock(BudgetFactory::class); - $categoryFactory = $this->mock(CategoryFactory::class); - $currencyFactory = $this->mock(TransactionCurrencyFactory::class); - - $data = [ - 'currency_id' => 1, - 'currency_code' => null, - 'description' => null, - 'source_id' => $asset->id, - 'source_name' => null, - 'destination_id' => $alsoAsset->id, - 'destination_name' => null, - 'amount' => '10', - 'reconciled' => false, - 'identifier' => 0, - 'foreign_currency_id' => null, - 'foreign_currency_code' => null, - 'foreign_amount' => null, - 'budget_id' => null, - 'budget_name' => null, - 'category_id' => null, - 'category_name' => null, - ]; - - // mock: - $accountRepos->shouldReceive('setUser'); - $budgetFactory->shouldReceive('setUser'); - $categoryFactory->shouldReceive('setUser'); - // first search action is for the asset account, second is for expense account. - $accountRepos->shouldReceive('findNull')->andReturn($asset, $alsoAsset); - - // factories return various stuff: - $currencyFactory->shouldReceive('find')->andReturn($euro, null)->atLeast()->once(); - - /** @var TransactionJournal $withdrawal */ - $withdrawal = $this->user()->transactionJournals()->where('transaction_type_id', 1)->first(); - $count = $withdrawal->transactions()->count(); - - /** @var TransactionFactory $factory */ - $factory = app(TransactionFactory::class); - $factory->setUser($this->user()); - try { - $factory->createPair($withdrawal, $data); - } catch (FireflyException $e) { - $this->assertEquals('Source and destination account cannot be both of the type "Asset account"', $e->getMessage()); - } - - $newCount = $withdrawal->transactions()->count(); - $this->assertEquals($count, $newCount); - } - - /** - * Create reconciliation using minimal (bad) data. + * To cover everything, test several combinations. + * + * For the source account, submit a Deposit and nothing else (this is OK). + * Expected result: a cash account * * @covers \FireflyIII\Factory\TransactionFactory - * @covers \FireflyIII\Services\Internal\Support\TransactionServiceTrait */ - public function testCreatePairTransfer(): void + public function testDepositSourceRevenueCash(): void { - // objects: - $asset = $this->user()->accounts()->where('account_type_id', 3)->first(); - $opposing = $this->user()->accounts()->where('id', '!=', $asset->id)->where('account_type_id', 3)->first(); - $euro = TransactionCurrency::first(); - $foreign = TransactionCurrency::where('id', '!=', $euro->id)->first(); + // mock classes + $accountRepos = $this->mock(AccountRepositoryInterface::class); - // mocked classes - $accountRepos = $this->mock(AccountRepositoryInterface::class); - $budgetFactory = $this->mock(BudgetFactory::class); - $categoryFactory = $this->mock(CategoryFactory::class); - $currencyFactory = $this->mock(TransactionCurrencyFactory::class); + // data used in calls. + $deposit = $this->getRandomDeposit(); + $revenue = $this->getRandomRevenue(); - $data = [ - 'currency_id' => 1, - 'currency_code' => null, - 'description' => null, - 'source_id' => $asset->id, - 'source_name' => null, - 'destination_id' => $opposing->id, - 'destination_name' => null, - 'amount' => '10', - 'reconciled' => false, - 'identifier' => 0, - 'foreign_currency_id' => null, - 'foreign_currency_code' => null, - 'foreign_amount' => null, - 'budget_id' => null, - 'budget_name' => null, - 'category_id' => null, - 'category_name' => null, - ]; - - // mock: - $accountRepos->shouldReceive('setUser'); - $budgetFactory->shouldReceive('setUser'); - $categoryFactory->shouldReceive('setUser'); - // first search action is for the asset account, second is for expense account. - $accountRepos->shouldReceive('findNull')->andReturn($asset, $opposing); - - // factories return various stuff: - $budgetFactory->shouldReceive('find')->andReturn(null); - $categoryFactory->shouldReceive('findOrCreate')->andReturn(null); - $currencyFactory->shouldReceive('find')->andReturn($euro, null); - - /** @var TransactionJournal $transfer */ - $transfer = $this->user()->transactionJournals()->where('transaction_type_id', 3)->first(); - $count = $transfer->transactions()->count(); + // mock calls. + $accountRepos->shouldReceive('setUser')->once(); + $accountRepos->shouldReceive('getCashAccount')->once()->andReturn($revenue); /** @var TransactionFactory $factory */ $factory = app(TransactionFactory::class); $factory->setUser($this->user()); - try { - $collection = $factory->createPair($transfer, $data); - } catch (FireflyException $e) { - $this->assertTrue(false, $e->getMessage()); - } + $factory->setJournal($deposit); - $newCount = $transfer->transactions()->count(); - - $this->assertCount(2, $collection); - $this->assertEquals($count + 2, $newCount); - // find stuff in transaction #1 (should suffice): - /** @var Transaction $first */ - $first = $collection->first(); - $this->assertEquals($transfer->id, $first->transaction_journal_id); - $this->assertEquals(-10, $first->amount); - $this->assertEquals(false, $first->reconciled); - $this->assertEquals(0, $first->identifier); - $this->assertEquals($euro->id, $first->transaction_currency_id); - $this->assertNull($first->foreign_amount); - $this->assertNull($first->foreign_currency_id); + $result = $factory->getAccount('source', null, null, null); + $this->assertEquals($revenue->name, $result->name); } + /** + * To cover everything, test several combinations. + * + * For the source account, submit a Deposit and an Revenue account name (this is OK). + * Expected result: a new revenue account. + * + * @covers \FireflyIII\Factory\TransactionFactory + */ + public function testDepositSourceRevenueNameNew(): void + { + // mock classes + $accountRepos = $this->mock(AccountRepositoryInterface::class); + + // data used in calls. + $deposit = $this->getRandomDeposit(); + $name = 'random rev name ' . random_int(1, 100000); + $revenue = $this->getRandomRevenue(); + + // mock calls. + $accountRepos->shouldReceive('setUser')->once(); + $accountRepos->shouldReceive('findByName')->once()->withArgs([$name, [AccountType::REVENUE]])->andReturnNull(); + // system will automatically expand search: + $accountRepos->shouldReceive('findByName')->once()->withArgs( + [$name, [AccountType::REVENUE, AccountType::CASH, AccountType::LOAN, AccountType::DEBT, AccountType::MORTGAGE, AccountType::INITIAL_BALANCE, + AccountType::RECONCILIATION]] + )->andReturnNull(); + + // then store new account: + $accountRepos->shouldReceive('store')->once()->withArgs( + [[ + 'account_type_id' => null, + 'accountType' => AccountType::REVENUE, + 'name' => $name, + 'active' => true, + 'iban' => null, + ]] + )->andReturn($revenue); + + /** @var TransactionFactory $factory */ + $factory = app(TransactionFactory::class); + $factory->setUser($this->user()); + $factory->setJournal($deposit); + + $result = $factory->getAccount('source', null, null, $name); + $this->assertEquals($revenue->name, $result->name); + } + + /** + * @throws FireflyException + */ + public function testDramaBasic(): void + { + // mock classes + $accountRepos = $this->mock(AccountRepositoryInterface::class); + + $withdrawal = $this->getRandomWithdrawal(); + $source = $withdrawal->transactions()->where('amount', '<', 0)->first(); + $dest = $withdrawal->transactions()->where('amount', '>', 0)->first(); + + // mock calls. + $accountRepos->shouldReceive('setUser')->once(); + + + /** @var TransactionFactory $factory */ + $factory = app(TransactionFactory::class); + $factory->setUser($this->user()); + $factory->setJournal($withdrawal); + $factory->makeDramaOverAccountTypes($source->account, $dest->account); + } + + /** + * @throws FireflyException + */ + public function testDramaNotAllowed(): void + { + // mock classes + $accountRepos = $this->mock(AccountRepositoryInterface::class); + + $withdrawal = $this->getRandomWithdrawal(); + + // this is an asset account. + $source = $withdrawal->transactions()->where('amount', '<', 0)->first(); + // so destiny cannot be also asset account + $dest = $this->getRandomAsset(); + + // mock calls. + $accountRepos->shouldReceive('setUser')->once(); + + + /** @var TransactionFactory $factory */ + $factory = app(TransactionFactory::class); + $factory->setUser($this->user()); + $factory->setJournal($withdrawal); + try { + $factory->makeDramaOverAccountTypes($source->account, $dest); + } catch (FireflyException $e) { + $this->assertEquals( + 'Journal of type "Withdrawal" has a source account of type "Asset account" and cannot accept a "Asset account"-account as destination, but only accounts of: Expense account, Loan, Debt, Mortgage', + $e->getMessage() + ); + } + } + + /** + * @covers \FireflyIII\Factory\TransactionFactory + */ + public function testGetAmountBasic(): void + { + // mock classes + $accountRepos = $this->mock(AccountRepositoryInterface::class); + + $amount = '10'; + // data used in calls. + $journal = $this->getRandomWithdrawal(); + + // mock calls. + $accountRepos->shouldReceive('setUser')->once(); + + /** @var TransactionFactory $factory */ + $factory = app(TransactionFactory::class); + $factory->setUser($this->user()); + $factory->setJournal($journal); + + $result = $factory->getAmount($amount); + $this->assertEquals($amount, $result); + } + + /** + * @covers \FireflyIII\Factory\TransactionFactory + */ + public function testGetAmountNull(): void + { + // mock classes + $accountRepos = $this->mock(AccountRepositoryInterface::class); + $amount = ''; + // data used in calls. + $journal = $this->getRandomWithdrawal(); + + // mock calls. + $accountRepos->shouldReceive('setUser')->once(); + + /** @var TransactionFactory $factory */ + $factory = app(TransactionFactory::class); + $factory->setUser($this->user()); + $factory->setJournal($journal); + + try { + $factory->getAmount($amount); + } catch (FireflyException $e) { + $this->assertEquals('The amount cannot be an empty string: ""', $e->getMessage()); + } + } + + /** + * @covers \FireflyIII\Factory\TransactionFactory + */ + public function testGetAmountZero(): void + { + // mock classes + $accountRepos = $this->mock(AccountRepositoryInterface::class); + $amount = '0.0'; + // data used in calls. + $journal = $this->getRandomWithdrawal(); + + // mock calls. + $accountRepos->shouldReceive('setUser')->once(); + + /** @var TransactionFactory $factory */ + $factory = app(TransactionFactory::class); + $factory->setUser($this->user()); + $factory->setJournal($journal); + + try { + $factory->getAmount($amount); + } catch (FireflyException $e) { + $this->assertEquals('The amount seems to be zero: "0.0"', $e->getMessage()); + } + } + + /** + * @covers \FireflyIII\Factory\TransactionFactory + */ + public function testGetForeignAmountBasic(): void + { + // mock classes + $accountRepos = $this->mock(AccountRepositoryInterface::class); + $amount = '10'; + // data used in calls. + $journal = $this->getRandomWithdrawal(); + + // mock calls. + $accountRepos->shouldReceive('setUser')->once(); + + /** @var TransactionFactory $factory */ + $factory = app(TransactionFactory::class); + $factory->setUser($this->user()); + $factory->setJournal($journal); + + $result = $factory->getForeignAmount($amount); + $this->assertEquals($amount, $result); + } + + /** + * @covers \FireflyIII\Factory\TransactionFactory + */ + public function testGetForeignAmountEmpty(): void + { + // mock classes + $accountRepos = $this->mock(AccountRepositoryInterface::class); + $amount = ''; + // data used in calls. + $journal = $this->getRandomWithdrawal(); + + // mock calls. + $accountRepos->shouldReceive('setUser')->once(); + + /** @var TransactionFactory $factory */ + $factory = app(TransactionFactory::class); + $factory->setUser($this->user()); + $factory->setJournal($journal); + + $result = $factory->getForeignAmount($amount); + $this->assertNull($result); + } + + /** + * @covers \FireflyIII\Factory\TransactionFactory + */ + public function testGetForeignAmountNull(): void + { + // mock classes + $accountRepos = $this->mock(AccountRepositoryInterface::class); + $amount = null; + // data used in calls. + $journal = $this->getRandomWithdrawal(); + + // mock calls. + $accountRepos->shouldReceive('setUser')->once(); + + /** @var TransactionFactory $factory */ + $factory = app(TransactionFactory::class); + $factory->setUser($this->user()); + $factory->setJournal($journal); + + $result = $factory->getForeignAmount($amount); + $this->assertNull($result); + } + + /** + * @covers \FireflyIII\Factory\TransactionFactory + */ + public function testGetForeignAmountZero(): void + { + // mock classes + $accountRepos = $this->mock(AccountRepositoryInterface::class); + $amount = '0.0'; + // data used in calls. + $journal = $this->getRandomWithdrawal(); + + // mock calls. + $accountRepos->shouldReceive('setUser')->once(); + + /** @var TransactionFactory $factory */ + $factory = app(TransactionFactory::class); + $factory->setUser($this->user()); + $factory->setJournal($journal); + + $result = $factory->getForeignAmount($amount); + $this->assertNull($result); + } + + /** + * To cover everything, test several combinations. + * + * For the source account, submit a Withdrawal and an Asset account ID (this is OK). + * Expected result: the same asset account. + * + * @covers \FireflyIII\Factory\TransactionFactory + */ + public function testWithdrawalSourceAssetId(): void + { + // mock classes + $accountRepos = $this->mock(AccountRepositoryInterface::class); + + // data used in calls. + $withdrawal = $this->getRandomWithdrawal(); + $asset = $this->getRandomAsset(); + + // mock calls. + $accountRepos->shouldReceive('setUser')->once(); + $accountRepos->shouldReceive('findNull')->once()->withArgs([$asset->id])->andReturn($asset); + + /** @var TransactionFactory $factory */ + $factory = app(TransactionFactory::class); + $factory->setUser($this->user()); + $factory->setJournal($withdrawal); + + $result = $factory->getAccount('source', null, $asset->id, null); + $this->assertEquals($asset->id, $result->id); + } + + /** + * To cover everything, test several combinations. + * + * For the source account, submit a Withdrawal and an Asset account ID (this is OK). + * Expected result: find won't return anything so we expect a big fat error. + * + * @covers \FireflyIII\Factory\TransactionFactory + */ + public function testWithdrawalSourceAssetIdNOK(): void + { + // mock classes + $accountRepos = $this->mock(AccountRepositoryInterface::class); + + // data used in calls. + $withdrawal = $this->getRandomWithdrawal(); + $asset = $this->getRandomAsset(); + + // mock calls. + $accountRepos->shouldReceive('setUser')->once(); + $accountRepos->shouldReceive('findNull')->once()->withArgs([$asset->id])->andReturn($asset); + + /** @var TransactionFactory $factory */ + $factory = app(TransactionFactory::class); + $factory->setUser($this->user()); + $factory->setJournal($withdrawal); + + try { + $factory->getAccount('source', null, $asset->id, null); + } catch (FireflyException $e) { + $this->assertEquals('TransactionFactory: Cannot create asset account with ID #0 or name "(no name)".', $e->getMessage()); + } + } + + /** + * To cover everything, test several combinations. + * + * For the source account, submit a Withdrawal and an Asset account name (this is OK). + * Expected result: the same asset account. + * + * @covers \FireflyIII\Factory\TransactionFactory + */ + public function testWithdrawalSourceAssetName(): void + { + // mock classes + $accountRepos = $this->mock(AccountRepositoryInterface::class); + + // data used in calls. + $withdrawal = $this->getRandomWithdrawal(); + $asset = $this->getRandomAsset(); + + // mock calls. + $accountRepos->shouldReceive('setUser')->once(); + $accountRepos->shouldReceive('findByName')->once()->withArgs([$asset->name, [AccountType::ASSET]])->andReturn($asset); + + /** @var TransactionFactory $factory */ + $factory = app(TransactionFactory::class); + $factory->setUser($this->user()); + $factory->setJournal($withdrawal); + + $result = $factory->getAccount('source', null, null, $asset->name); + $this->assertEquals($asset->id, $result->id); + } + + /** + * To cover everything, test several combinations. + * + * For the source account, submit a Withdrawal and an Asset account name (this is OK). + * Expected result: the same asset account. + * + * This will initially return NULL and then search again with all possible types. + * + * @covers \FireflyIII\Factory\TransactionFactory + */ + public function testWithdrawalSourceAssetName2(): void + { + // mock classes + $accountRepos = $this->mock(AccountRepositoryInterface::class); + + // data used in calls. + $withdrawal = $this->getRandomWithdrawal(); + $asset = $this->getRandomAsset(); + + // mock calls. + $accountRepos->shouldReceive('setUser')->once(); + $accountRepos->shouldReceive('findByName')->once()->withArgs([$asset->name, [AccountType::ASSET]])->andReturnNull(); + $accountRepos->shouldReceive('findByName')->once()->withArgs( + [$asset->name, [AccountType::ASSET, AccountType::LOAN, AccountType::DEBT, AccountType::MORTGAGE]] + )->andReturn($asset); + + /** @var TransactionFactory $factory */ + $factory = app(TransactionFactory::class); + $factory->setUser($this->user()); + $factory->setJournal($withdrawal); + + $result = $factory->getAccount('source', null, null, $asset->name); + $this->assertEquals($asset->id, $result->id); + } + + /** + * To cover everything, test several combinations. + * + * For the source account, submit a Withdrawal and an Asset account object (this is OK). + * Expected result: the same asset account. + * + * @covers \FireflyIII\Factory\TransactionFactory + */ + public function testWithdrawalSourceAssetObj(): void + { + // mock classes + $accountRepos = $this->mock(AccountRepositoryInterface::class); + + // data used in calls. + $withdrawal = $this->getRandomWithdrawal(); + $asset = $this->getRandomAsset(); + + // mock calls. + $accountRepos->shouldReceive('setUser')->once(); + + /** @var TransactionFactory $factory */ + $factory = app(TransactionFactory::class); + $factory->setUser($this->user()); + $factory->setJournal($withdrawal); + + $result = $factory->getAccount('source', $asset, null, null); + $this->assertEquals($asset->id, $result->id); + } + + /** + * To cover everything, test several combinations. + * + * For the source account, submit a Withdrawal and an Expense account object (this is not OK). + * Expected result: big fat error because of missing data. + * + * @covers \FireflyIII\Factory\TransactionFactory + */ + public function testWithdrawalSourceAssetObjNOK(): void + { + // mock classes + $accountRepos = $this->mock(AccountRepositoryInterface::class); + + // data used in calls. + $withdrawal = $this->getRandomWithdrawal(); + $expense = $this->getRandomExpense(); + + // mock calls. + $accountRepos->shouldReceive('setUser')->once(); + + /** @var TransactionFactory $factory */ + $factory = app(TransactionFactory::class); + $factory->setUser($this->user()); + $factory->setJournal($withdrawal); + try { + $factory->getAccount('source', $expense, null, null); + } catch (FireflyException $e) { + $this->assertEquals('TransactionFactory: Cannot create asset account with ID #0 or name "(no name)".', $e->getMessage()); + } + } + + /** + * To cover everything, test several combinations. + * + * For the source account, submit a Withdrawal and Loan account object (this is OK). + * Expected result: the same loan account. + * + * @covers \FireflyIII\Factory\TransactionFactory + */ + public function testWithdrawalSourceLoanObj(): void + { + // mock classes + $accountRepos = $this->mock(AccountRepositoryInterface::class); + + // data used in calls. + $withdrawal = $this->getRandomWithdrawal(); + $loan = $this->getRandomLoan(); + + // mock calls. + $accountRepos->shouldReceive('setUser')->once(); + + /** @var TransactionFactory $factory */ + $factory = app(TransactionFactory::class); + $factory->setUser($this->user()); + $factory->setJournal($withdrawal); + + $result = $factory->getAccount('source', $loan, null, null); + $this->assertEquals($loan->id, $result->id); + } + + } diff --git a/tests/Unit/Factory/TransactionJournalFactoryTest.php b/tests/Unit/Factory/TransactionJournalFactoryTest.php index f17966bb66..b6ee84c3f7 100644 --- a/tests/Unit/Factory/TransactionJournalFactoryTest.php +++ b/tests/Unit/Factory/TransactionJournalFactoryTest.php @@ -24,19 +24,30 @@ declare(strict_types=1); namespace Tests\Unit\Factory; -use Carbon\Carbon; -use FireflyIII\Exceptions\FireflyException; -use FireflyIII\Factory\BillFactory; +use FireflyIII\Factory\AccountFactory; use FireflyIII\Factory\PiggyBankEventFactory; -use FireflyIII\Factory\PiggyBankFactory; use FireflyIII\Factory\TagFactory; +use FireflyIII\Factory\TransactionCurrencyFactory; use FireflyIII\Factory\TransactionFactory; use FireflyIII\Factory\TransactionJournalFactory; use FireflyIII\Factory\TransactionJournalMetaFactory; -use FireflyIII\Factory\TransactionTypeFactory; +use FireflyIII\Models\Account; +use FireflyIII\Models\AccountType; +use FireflyIII\Models\Budget; +use FireflyIII\Models\Category; +use FireflyIII\Models\PiggyBank; +use FireflyIII\Models\Tag; use FireflyIII\Models\TransactionCurrency; +use FireflyIII\Models\TransactionGroup; +use FireflyIII\Models\TransactionJournal; use FireflyIII\Models\TransactionType; +use FireflyIII\Repositories\Account\AccountRepositoryInterface; +use FireflyIII\Repositories\Bill\BillRepositoryInterface; +use FireflyIII\Repositories\Budget\BudgetRepositoryInterface; +use FireflyIII\Repositories\Category\CategoryRepositoryInterface; use FireflyIII\Repositories\Currency\CurrencyRepositoryInterface; +use FireflyIII\Repositories\PiggyBank\PiggyBankRepositoryInterface; +use FireflyIII\Repositories\TransactionType\TransactionTypeRepositoryInterface; use Log; use Tests\TestCase; @@ -57,198 +68,888 @@ class TransactionJournalFactoryTest extends TestCase /** * @covers \FireflyIII\Factory\TransactionJournalFactory - * @covers \FireflyIII\Services\Internal\Support\JournalServiceTrait */ - public function testCreateBasic(): void + public function testBudget(): void { - // mock used classes: - $type = TransactionType::find(1); - $euro = TransactionCurrency::find(1); - $billFactory = $this->mock(BillFactory::class); - $tagFactory = $this->mock(TagFactory::class); - $metaFactory = $this->mock(TransactionJournalMetaFactory::class); - $typeFactory = $this->mock(TransactionTypeFactory::class); - $transactionFactory = $this->mock(TransactionFactory::class); - $piggyFactory = $this->mock(PiggyBankFactory::class); - $eventFactory = $this->mock(PiggyBankEventFactory::class); - $currencyRepos = $this->mock(CurrencyRepositoryInterface::class); + // mock used repositories. + $billRepos = $this->mock(BillRepositoryInterface::class); + $budgetRepos = $this->mock(BudgetRepositoryInterface::class); + $catRepos = $this->mock(CategoryRepositoryInterface::class); + $curRepos = $this->mock(CurrencyRepositoryInterface::class); + $piggyRepos = $this->mock(PiggyBankRepositoryInterface::class); + $typeRepos = $this->mock(TransactionTypeRepositoryInterface::class); + $eventFactory = $this->mock(PiggyBankEventFactory::class); + $tagFactory = $this->mock(TagFactory::class); + $accountFactory = $this->mock(AccountFactory::class); + $currencyFactory = $this->mock(TransactionCurrencyFactory::class); + $metaFactory = $this->mock(TransactionJournalMetaFactory::class); + $accountRepos = $this->mock(AccountRepositoryInterface::class); - // mock stuff: - $typeFactory->shouldReceive('find')->andReturn($type); - $currencyRepos->shouldReceive('find')->andReturn($euro); + // data to return from various calls: + $type = TransactionType::whereType(TransactionType::WITHDRAWAL)->first(); + $euro = TransactionCurrency::whereCode('EUR')->first(); + $usd = TransactionCurrency::whereCode('USD')->first(); + $asset = $this->getRandomAsset(); + $expense = $this->getRandomExpense(); + $budget = Budget::first(); - $metaFactory->shouldReceive('updateOrCreate'); - - // mock factories: - $transactionFactory->shouldReceive('setUser')->once(); - $billFactory->shouldReceive('setUser')->once(); - $piggyFactory->shouldReceive('setUser')->once(); - $tagFactory->shouldReceive('setUser')->once(); - - $transactionFactory->shouldReceive('createPair')->once(); - $billFactory->shouldReceive('find')->andReturn(null); - $piggyFactory->shouldReceive('find')->andReturn(null); + // data to submit. $data = [ - 'type' => 'withdrawal', - 'user' => $this->user()->id, - 'description' => 'I are journal', - 'date' => new Carbon('2018-01-01'), - 'bill_id' => null, - 'bill_name' => null, - 'piggy_bank_id' => null, - 'piggy_bank_name' => null, - 'notes' => 'Hello', - 'tags' => [], - 'transactions' => [[]], - ]; - - /** @var TransactionJournalFactory $factory */ - $factory = app(TransactionJournalFactory::class); - $factory->setUser($this->user()); - try { - $journal = $factory->create($data); - } catch (FireflyException $e) { - $this->assertTrue(false, $e->getMessage()); - } - $this->assertEquals($data['description'], $journal->description); - $this->assertEquals('2018-01-01', $journal->date->format('Y-m-d')); - $this->assertEquals(1, $journal->notes()->count()); - - } - - /** - * @covers \FireflyIII\Factory\TransactionJournalFactory - * @covers \FireflyIII\Services\Internal\Support\JournalServiceTrait - */ - public function testCreateBasicEmptyAmount(): void - { - // mock used classes: - $type = TransactionType::find(1); - $euro = TransactionCurrency::find(1); - $billFactory = $this->mock(BillFactory::class); - $tagFactory = $this->mock(TagFactory::class); - $metaFactory = $this->mock(TransactionJournalMetaFactory::class); - $typeFactory = $this->mock(TransactionTypeFactory::class); - $transactionFactory = $this->mock(TransactionFactory::class); - $piggyFactory = $this->mock(PiggyBankFactory::class); - $eventFactory = $this->mock(PiggyBankEventFactory::class); - $currencyRepos = $this->mock(CurrencyRepositoryInterface::class); - - // mock stuff: - $typeFactory->shouldReceive('find')->andReturn($type); - $currencyRepos->shouldReceive('find')->andReturn($euro); - - $metaFactory->shouldReceive('updateOrCreate'); - - // mock factories: - $transactionFactory->shouldReceive('setUser')->once(); - $billFactory->shouldReceive('setUser')->once(); - $piggyFactory->shouldReceive('setUser')->once(); - $tagFactory->shouldReceive('setUser')->once(); - - $transactionFactory->shouldReceive('createPair')->once(); - $billFactory->shouldReceive('find')->andReturn(null); - $piggyFactory->shouldReceive('find')->andReturn(null); - $data = [ - 'type' => 'withdrawal', - 'user' => $this->user()->id, - 'description' => 'I are journal', - 'date' => new Carbon('2018-01-01'), - 'bill_id' => null, - 'bill_name' => null, - 'piggy_bank_id' => null, - 'piggy_bank_name' => null, - 'notes' => 'Hello', - 'tags' => [], - 'transactions' => [ + 'transactions' => [ + // first transaction: [ - 'amount' => '', + 'source_id' => $asset->id, + 'amount' => '1', + 'foreign_currency_code' => 'USD', + 'foreign_amount' => '2', + 'notes' => 'I am some notes', + 'budget_id' => $budget->id, ], ], ]; - /** @var TransactionJournalFactory $factory */ - $factory = app(TransactionJournalFactory::class); - $factory->setUser($this->user()); - try { - $journal = $factory->create($data); - } catch (FireflyException $e) { - $this->assertTrue(false, $e->getMessage()); - } - $this->assertEquals($data['description'], $journal->description); - $this->assertEquals('2018-01-01', $journal->date->format('Y-m-d')); - $this->assertEquals(1, $journal->notes()->count()); - - } - - - /** - * Same but with added meta data - * - * @covers \FireflyIII\Factory\TransactionJournalFactory - * @covers \FireflyIII\Services\Internal\Support\JournalServiceTrait - */ - public function testCreateBasicMeta(): void - { - // mock used classes: - $type = TransactionType::find(1); - $euro = TransactionCurrency::find(1); - $piggy = $this->user()->piggyBanks()->first(); - $bill = $this->user()->bills()->first(); - $tag = $this->user()->tags()->first(); - $billFactory = $this->mock(BillFactory::class); - $tagFactory = $this->mock(TagFactory::class); - $metaFactory = $this->mock(TransactionJournalMetaFactory::class); - $typeFactory = $this->mock(TransactionTypeFactory::class); - $transactionFactory = $this->mock(TransactionFactory::class); - $piggyFactory = $this->mock(PiggyBankFactory::class); - $eventFactory = $this->mock(PiggyBankEventFactory::class); - $currencyRepos = $this->mock(CurrencyRepositoryInterface::class); - - // mock stuff: - $typeFactory->shouldReceive('find')->andReturn($type); - $currencyRepos->shouldReceive('find')->andReturn($euro); - - // mock factories: - $transactionFactory->shouldReceive('setUser')->once(); - $billFactory->shouldReceive('setUser')->once(); - $piggyFactory->shouldReceive('setUser')->once(); + // calls to setUser: + $curRepos->shouldReceive('setUser')->once(); + $billRepos->shouldReceive('setUser')->once(); + $budgetRepos->shouldReceive('setUser')->once(); + $catRepos->shouldReceive('setUser')->once(); + $piggyRepos->shouldReceive('setUser')->once(); + $accountRepos->shouldReceive('setUser')->once(); $tagFactory->shouldReceive('setUser')->once(); - $transactionFactory->shouldReceive('createPair')->once(); - $billFactory->shouldReceive('find')->andReturn($bill); - $piggyFactory->shouldReceive('find')->andReturn($piggy); - $eventFactory->shouldReceive('create')->once(); - $tagFactory->shouldReceive('findOrCreate')->andReturn($tag); - $metaFactory->shouldReceive('updateOrCreate'); + // calls to transaction type repository. + $typeRepos->shouldReceive('findTransactionType')->withArgs([null, null])->once()->andReturn($type); + + // calls to the currency repository: + $curRepos->shouldReceive('findCurrency')->withArgs([null, null, null])->once()->andReturn($euro); + $curRepos->shouldReceive('findCurrency')->withArgs([null, null, 'USD'])->once()->andReturn($usd); + + // calls to the bill repository: + $billRepos->shouldReceive('findBill')->withArgs([null, null, null])->once()->andReturnNull(); + + // calls to the budget repository + $budgetRepos->shouldReceive('findBudget')->withArgs([null, $budget->id, null])->once()->andReturn($budget); + + // calls to the category repository + $catRepos->shouldReceive('findCategory')->withArgs([null, null, null])->once()->andReturnNull(); + + // calls to the account repository + $accountRepos->shouldReceive('findNull')->withArgs([$asset->id])->once()->andReturn($asset); + $accountRepos->shouldReceive('getCashAccount')->once()->andReturn($expense); + + // calls to the meta factory: + $metaFactory->shouldReceive('updateOrCreate')->atLeast()->once()->andReturnNull(); - $data = [ - 'type' => 'withdrawal', - 'user' => $this->user()->id, - 'description' => 'I are journal', - 'date' => new Carbon('2018-01-01'), - 'bill_id' => $bill->id, - 'bill_name' => null, - 'piggy_bank_id' => $piggy->id, - 'piggy_bank_name' => null, - 'notes' => '', - 'tags' => ['a', 'b', 'c'], - 'transactions' => [[]], - 'interest_date' => '2018-01-01', - ]; /** @var TransactionJournalFactory $factory */ $factory = app(TransactionJournalFactory::class); $factory->setUser($this->user()); - try { - $journal = $factory->create($data); - } catch (FireflyException $e) { - $this->assertTrue(false, $e->getMessage()); - } - $this->assertEquals($data['description'], $journal->description); - $this->assertEquals('2018-01-01', $journal->date->format('Y-m-d')); - $this->assertEquals(0, $journal->notes()->count()); + $collection = $factory->create($data); + /** @var TransactionJournal $journal */ + $journal = $collection->first(); + // collection should have one journal. + $this->assertCount(1, $collection); + $this->assertInstanceOf(TransactionJournal::class, $journal); + $this->assertEquals('(empty description)', $journal->description); + $this->assertCount(1, $journal->budgets); + $this->assertCount(0, $journal->categories); + $this->assertCount(2, $journal->transactions); + $this->assertEquals('I am some notes', $journal->notes->first()->text); + $this->assertEquals('EUR', $journal->transactions->first()->transactionCurrency->code); + $this->assertEquals('USD', $journal->transactions->first()->foreignCurrency->code); + } + + /** + * @covers \FireflyIII\Factory\TransactionJournalFactory + */ + public function testCategory(): void + { + // mock used repositories. + $billRepos = $this->mock(BillRepositoryInterface::class); + $budgetRepos = $this->mock(BudgetRepositoryInterface::class); + $catRepos = $this->mock(CategoryRepositoryInterface::class); + $curRepos = $this->mock(CurrencyRepositoryInterface::class); + $piggyRepos = $this->mock(PiggyBankRepositoryInterface::class); + $typeRepos = $this->mock(TransactionTypeRepositoryInterface::class); + $eventFactory = $this->mock(PiggyBankEventFactory::class); + $tagFactory = $this->mock(TagFactory::class); + $accountFactory = $this->mock(AccountFactory::class); + $currencyFactory = $this->mock(TransactionCurrencyFactory::class); + $metaFactory = $this->mock(TransactionJournalMetaFactory::class); + $accountRepos = $this->mock(AccountRepositoryInterface::class); + + // data to return from various calls: + $type = TransactionType::whereType(TransactionType::WITHDRAWAL)->first(); + $euro = TransactionCurrency::whereCode('EUR')->first(); + $usd = TransactionCurrency::whereCode('USD')->first(); + $asset = $this->getRandomAsset(); + $category = Category::first(); + $expense = $this->getRandomExpense(); + $budget = Budget::first(); + + // data to submit. + $data = [ + 'transactions' => [ + // first transaction: + [ + 'source_id' => $asset->id, + 'amount' => '1', + 'foreign_currency_code' => 'USD', + 'foreign_amount' => '2', + 'notes' => 'I am some notes', + 'budget_id' => $budget->id, + 'category_name' => $category->name, + ], + ], + ]; + + // calls to setUser: + $curRepos->shouldReceive('setUser')->once(); + $billRepos->shouldReceive('setUser')->once(); + $budgetRepos->shouldReceive('setUser')->once(); + $catRepos->shouldReceive('setUser')->once(); + $piggyRepos->shouldReceive('setUser')->once(); + $accountRepos->shouldReceive('setUser')->once(); + $tagFactory->shouldReceive('setUser')->once(); + + // calls to transaction type repository. + $typeRepos->shouldReceive('findTransactionType')->withArgs([null, null])->once()->andReturn($type); + + // calls to the currency repository: + $curRepos->shouldReceive('findCurrency')->withArgs([null, null, null])->once()->andReturn($euro); + $curRepos->shouldReceive('findCurrency')->withArgs([null, null, 'USD'])->once()->andReturn($usd); + + // calls to the bill repository: + $billRepos->shouldReceive('findBill')->withArgs([null, null, null])->once()->andReturnNull(); + + // calls to the budget repository + $budgetRepos->shouldReceive('findBudget')->withArgs([null, $budget->id, null])->once()->andReturn($budget); + + // calls to the category repository + $catRepos->shouldReceive('findCategory')->withArgs([null, null, $category->name])->once()->andReturn($category); + + // calls to the account repository + $accountRepos->shouldReceive('findNull')->withArgs([$asset->id])->once()->andReturn($asset); + $accountRepos->shouldReceive('getCashAccount')->once()->andReturn($expense); + + // calls to the meta factory: + $metaFactory->shouldReceive('updateOrCreate')->atLeast()->once()->andReturnNull(); + + + /** @var TransactionJournalFactory $factory */ + $factory = app(TransactionJournalFactory::class); + $factory->setUser($this->user()); + $collection = $factory->create($data); + + /** @var TransactionJournal $journal */ + $journal = $collection->first(); + // collection should have one journal. + $this->assertCount(1, $collection); + $this->assertInstanceOf(TransactionJournal::class, $journal); + $this->assertEquals('(empty description)', $journal->description); + $this->assertCount(1, $journal->budgets); + $this->assertCount(1, $journal->categories); + $this->assertCount(2, $journal->transactions); + $this->assertEquals('I am some notes', $journal->notes->first()->text); + $this->assertEquals('EUR', $journal->transactions->first()->transactionCurrency->code); + $this->assertEquals('USD', $journal->transactions->first()->foreignCurrency->code); + } + + /** + * @covers \FireflyIII\Factory\TransactionJournalFactory + */ + public function testCreateAlmostEmpty(): void + { + // mock used repositories. + $billRepos = $this->mock(BillRepositoryInterface::class); + $budgetRepos = $this->mock(BudgetRepositoryInterface::class); + $catRepos = $this->mock(CategoryRepositoryInterface::class); + $curRepos = $this->mock(CurrencyRepositoryInterface::class); + $piggyRepos = $this->mock(PiggyBankRepositoryInterface::class); + $typeRepos = $this->mock(TransactionTypeRepositoryInterface::class); + $eventFactory = $this->mock(PiggyBankEventFactory::class); + $tagFactory = $this->mock(TagFactory::class); + $accountFactory = $this->mock(AccountFactory::class); + $currencyFactory = $this->mock(TransactionCurrencyFactory::class); + $metaFactory = $this->mock(TransactionJournalMetaFactory::class); + $accountRepos = $this->mock(AccountRepositoryInterface::class); + + // data to return from various calls: + $type = TransactionType::whereType(TransactionType::WITHDRAWAL)->first(); + $euro = TransactionCurrency::whereCode('EUR')->first(); + $asset = $this->getRandomAsset(); + $expense = $this->getRandomExpense(); + + // data to submit. + $data = [ + 'transactions' => [ + // first transaction: + [ + 'source_id' => $asset->id, + 'amount' => '1', + ], + ], + ]; + + // calls to setUser: + $curRepos->shouldReceive('setUser')->once(); + $billRepos->shouldReceive('setUser')->once(); + $budgetRepos->shouldReceive('setUser')->once(); + $catRepos->shouldReceive('setUser')->once(); + $piggyRepos->shouldReceive('setUser')->once(); + $accountRepos->shouldReceive('setUser')->once(); + $tagFactory->shouldReceive('setUser')->once(); + + // calls to transaction type repository. + $typeRepos->shouldReceive('findTransactionType')->withArgs([null, null])->once()->andReturn($type); + + // calls to the currency repository: + $curRepos->shouldReceive('findCurrency')->withArgs([null, null, null])->once()->andReturn($euro); + + // calls to the bill repository: + $billRepos->shouldReceive('findBill')->withArgs([null, null, null])->once()->andReturnNull(); + + // calls to the budget repository + $budgetRepos->shouldReceive('findBudget')->withArgs([null, null, null])->once()->andReturnNull(); + + // calls to the category repository + $catRepos->shouldReceive('findCategory')->withArgs([null, null, null])->once()->andReturnNull(); + + // calls to the account repository + $accountRepos->shouldReceive('findNull')->withArgs([$asset->id])->once()->andReturn($asset); + $accountRepos->shouldReceive('getCashAccount')->once()->andReturn($expense); + + // calls to the meta factory: + $metaFactory->shouldReceive('updateOrCreate')->atLeast()->once()->andReturnNull(); + + + /** @var TransactionJournalFactory $factory */ + $factory = app(TransactionJournalFactory::class); + $factory->setUser($this->user()); + $collection = $factory->create($data); + + /** @var TransactionJournal $journal */ + $journal = $collection->first(); + // collection should have one journal. + $this->assertCount(1, $collection); + $this->assertInstanceOf(TransactionJournal::class, $journal); + $this->assertEquals('(empty description)', $journal->description); + $this->assertCount(0, $journal->budgets); + $this->assertCount(0, $journal->categories); + $this->assertCount(2, $journal->transactions); + } + + /** + * @covers \FireflyIII\Factory\TransactionJournalFactory + */ + public function testCreateAlmostEmptyTransfer(): void + { + // mock used repositories. + $billRepos = $this->mock(BillRepositoryInterface::class); + $budgetRepos = $this->mock(BudgetRepositoryInterface::class); + $catRepos = $this->mock(CategoryRepositoryInterface::class); + $curRepos = $this->mock(CurrencyRepositoryInterface::class); + $piggyRepos = $this->mock(PiggyBankRepositoryInterface::class); + $typeRepos = $this->mock(TransactionTypeRepositoryInterface::class); + $eventFactory = $this->mock(PiggyBankEventFactory::class); + $tagFactory = $this->mock(TagFactory::class); + $accountFactory = $this->mock(AccountFactory::class); + $currencyFactory = $this->mock(TransactionCurrencyFactory::class); + $metaFactory = $this->mock(TransactionJournalMetaFactory::class); + $accountRepos = $this->mock(AccountRepositoryInterface::class); + + // data to return from various calls: + $type = TransactionType::whereType(TransactionType::TRANSFER)->first(); + $euro = TransactionCurrency::whereCode('EUR')->first(); + $source = $this->getRandomAsset(); + $destination = $this->getAnotherRandomAsset($source->id); + + // data to submit. + $data = [ + 'type' => 'transfer', + 'transactions' => [ + // first transaction: + [ + 'source_id' => $source->id, + 'destination_id' => $destination->id, + 'amount' => '1', + ], + ], + ]; + + // calls to setUser: + $curRepos->shouldReceive('setUser')->once(); + $billRepos->shouldReceive('setUser')->once(); + $budgetRepos->shouldReceive('setUser')->once(); + $catRepos->shouldReceive('setUser')->once(); + $piggyRepos->shouldReceive('setUser')->once(); + $accountRepos->shouldReceive('setUser')->once(); + $tagFactory->shouldReceive('setUser')->once(); + + // calls to transaction type repository. + $typeRepos->shouldReceive('findTransactionType')->withArgs([null, 'transfer'])->once()->andReturn($type); + + // calls to the currency repository: + $curRepos->shouldReceive('findCurrency')->withArgs([null, null, null])->once()->andReturn($euro); + + // calls to the bill repository: + $billRepos->shouldReceive('findBill')->withArgs([null, null, null])->once()->andReturnNull(); + + // calls to the budget repository + $budgetRepos->shouldReceive('findBudget')->withArgs([null, null, null])->once()->andReturnNull(); + + // calls to the category repository + $catRepos->shouldReceive('findCategory')->withArgs([null, null, null])->once()->andReturnNull(); + + // calls to the piggy bank repository: + $piggyRepos->shouldReceive('findPiggyBank')->withArgs([null, null, null])->once()->andReturnNull(); + + // calls to the account repository + $accountRepos->shouldReceive('findNull')->withArgs([$source->id])->once()->andReturn($source); + $accountRepos->shouldReceive('findNull')->withArgs([$destination->id])->once()->andReturn($destination); + + // calls to the meta factory: + $metaFactory->shouldReceive('updateOrCreate')->atLeast()->once()->andReturnNull(); + + /** @var TransactionJournalFactory $factory */ + $factory = app(TransactionJournalFactory::class); + $factory->setUser($this->user()); + $collection = $factory->create($data); + + /** @var TransactionJournal $journal */ + $journal = $collection->first(); + // collection should have one journal. + $this->assertCount(1, $collection); + $this->assertInstanceOf(TransactionJournal::class, $journal); + $this->assertEquals('(empty description)', $journal->description); + $this->assertCount(0, $journal->budgets); + $this->assertCount(0, $journal->categories); + $this->assertCount(2, $journal->transactions); + } + + /** + * @covers \FireflyIII\Factory\TransactionJournalFactory + */ + public function testCreateBasicGroup(): void + { + // mock used repositories. + $billRepos = $this->mock(BillRepositoryInterface::class); + $budgetRepos = $this->mock(BudgetRepositoryInterface::class); + $catRepos = $this->mock(CategoryRepositoryInterface::class); + $curRepos = $this->mock(CurrencyRepositoryInterface::class); + $piggyRepos = $this->mock(PiggyBankRepositoryInterface::class); + $typeRepos = $this->mock(TransactionTypeRepositoryInterface::class); + $eventFactory = $this->mock(PiggyBankEventFactory::class); + $tagFactory = $this->mock(TagFactory::class); + $accountFactory = $this->mock(AccountFactory::class); + $currencyFactory = $this->mock(TransactionCurrencyFactory::class); + $metaFactory = $this->mock(TransactionJournalMetaFactory::class); + $accountRepos = $this->mock(AccountRepositoryInterface::class); + + // data to return from various calls: + $type = TransactionType::whereType(TransactionType::WITHDRAWAL)->first(); + $euro = TransactionCurrency::whereCode('EUR')->first(); + $asset = $this->getRandomAsset(); + $expense = $this->getRandomExpense(); + + // data to submit. + $data = [ + 'transactions' => [ + // first transaction: + [ + 'source_id' => $asset->id, + 'amount' => '1', + ], + // second transaction: + [ + 'source_id' => $asset->id, + 'amount' => '1', + ], + ], + ]; + + // calls to setUser: + $curRepos->shouldReceive('setUser')->once(); + $billRepos->shouldReceive('setUser')->once(); + $budgetRepos->shouldReceive('setUser')->once(); + $catRepos->shouldReceive('setUser')->once(); + $piggyRepos->shouldReceive('setUser')->once(); + $accountRepos->shouldReceive('setUser')->once(); + $tagFactory->shouldReceive('setUser')->times(2); + + // calls to transaction type repository. + $typeRepos->shouldReceive('findTransactionType')->withArgs([null, null])->once()->andReturn($type); + + // calls to the currency repository: + $curRepos->shouldReceive('findCurrency')->withArgs([null, null, null])->times(2)->andReturn($euro); + + // calls to the bill repository: + $billRepos->shouldReceive('findBill')->withArgs([null, null, null])->times(2)->andReturnNull(); + + // calls to the budget repository + $budgetRepos->shouldReceive('findBudget')->withArgs([null, null, null])->times(2)->andReturnNull(); + + // calls to the category repository + $catRepos->shouldReceive('findCategory')->withArgs([null, null, null])->times(2)->andReturnNull(); + + // calls to the account repository + $accountRepos->shouldReceive('findNull')->withArgs([$asset->id])->times(2)->andReturn($asset); + $accountRepos->shouldReceive('getCashAccount')->times(2)->andReturn($expense); + + // calls to the meta factory: + $metaFactory->shouldReceive('updateOrCreate')->atLeast()->once()->andReturnNull(); + + /** @var TransactionJournalFactory $factory */ + $factory = app(TransactionJournalFactory::class); + $factory->setUser($this->user()); + $collection = $factory->create($data); + + /** @var TransactionJournal $journal */ + $journal = $collection->first(); + + // collection should have two journals. + $this->assertCount(2, $collection); + + // journal should have some props. + $this->assertInstanceOf(TransactionJournal::class, $journal); + $this->assertEquals('(empty description)', $journal->description); + $this->assertCount(0, $journal->budgets); + $this->assertCount(0, $journal->categories); + $this->assertCount(2, $journal->transactions); + + // group of journal should also have some props. + /** @var TransactionGroup $group */ + $group = $journal->transactionGroups()->first(); + $this->assertCount(2, $group->transactionJournals); + $this->assertEquals($journal->description, $group->title); + } + + /** + * @covers \FireflyIII\Factory\TransactionJournalFactory + */ + public function testCreateEmpty(): void + { + // mock used repositories. + $billRepos = $this->mock(BillRepositoryInterface::class); + $budgetRepos = $this->mock(BudgetRepositoryInterface::class); + $catRepos = $this->mock(CategoryRepositoryInterface::class); + $curRepos = $this->mock(CurrencyRepositoryInterface::class); + $piggyRepos = $this->mock(PiggyBankRepositoryInterface::class); + $transactionFactory = $this->mock(TransactionFactory::class); + $typeRepos = $this->mock(TransactionTypeRepositoryInterface::class); + $eventFactory = $this->mock(PiggyBankEventFactory::class); + $tagFactory = $this->mock(TagFactory::class); + $accountFactory = $this->mock(AccountFactory::class); + $currencyFactory = $this->mock(TransactionCurrencyFactory::class); + $metaFactory = $this->mock(TransactionJournalMetaFactory::class); + $accountRepos = $this->mock(AccountRepositoryInterface::class); + + // data to return from various calls: + $type = TransactionType::whereType(TransactionType::WITHDRAWAL)->first(); + + // data to submit. + $data = []; + + // calls to setUser: + $curRepos->shouldReceive('setUser')->once(); + $transactionFactory->shouldReceive('setUser')->once(); + $billRepos->shouldReceive('setUser')->once(); + $budgetRepos->shouldReceive('setUser')->once(); + $catRepos->shouldReceive('setUser')->once(); + $piggyRepos->shouldReceive('setUser')->once(); + + // calls to transaction type repository. + $typeRepos->shouldReceive('findTransactionType')->withArgs([null, null])->once()->andReturn($type); + + + /** @var TransactionJournalFactory $factory */ + $factory = app(TransactionJournalFactory::class); + $factory->setUser($this->user()); + $collection = $factory->create($data); + + $this->assertCount(0, $collection); + } + + /** + * @covers \FireflyIII\Factory\TransactionJournalFactory + */ + public function testCreatePiggyEvent(): void + { + // mock used repositories. + $billRepos = $this->mock(BillRepositoryInterface::class); + $budgetRepos = $this->mock(BudgetRepositoryInterface::class); + $catRepos = $this->mock(CategoryRepositoryInterface::class); + $curRepos = $this->mock(CurrencyRepositoryInterface::class); + $piggyRepos = $this->mock(PiggyBankRepositoryInterface::class); + $typeRepos = $this->mock(TransactionTypeRepositoryInterface::class); + $eventFactory = $this->mock(PiggyBankEventFactory::class); + $tagFactory = $this->mock(TagFactory::class); + $accountFactory = $this->mock(AccountFactory::class); + $currencyFactory = $this->mock(TransactionCurrencyFactory::class); + $metaFactory = $this->mock(TransactionJournalMetaFactory::class); + $accountRepos = $this->mock(AccountRepositoryInterface::class); + + // data to return from various calls: + $type = TransactionType::whereType(TransactionType::TRANSFER)->first(); + $euro = TransactionCurrency::whereCode('EUR')->first(); + $piggyBank = PiggyBank::first(); + $source = $this->getRandomAsset(); + $destination = $this->getAnotherRandomAsset($source->id); + + // data to submit. + $data = [ + 'type' => 'transfer', + 'transactions' => [ + // first transaction: + [ + 'source_id' => $source->id, + 'destination_id' => $destination->id, + 'amount' => '1', + 'piggy_bank_id' => '1', + 'piggy_bank_name' => 'Some name', + ], + ], + ]; + + // calls to setUser: + $curRepos->shouldReceive('setUser')->once(); + $billRepos->shouldReceive('setUser')->once(); + $budgetRepos->shouldReceive('setUser')->once(); + $catRepos->shouldReceive('setUser')->once(); + $piggyRepos->shouldReceive('setUser')->once(); + $accountRepos->shouldReceive('setUser')->once(); + $tagFactory->shouldReceive('setUser')->once(); + + // calls to transaction type repository. + $typeRepos->shouldReceive('findTransactionType')->withArgs([null, 'transfer'])->once()->andReturn($type); + + // calls to the currency repository: + $curRepos->shouldReceive('findCurrency')->withArgs([null, null, null])->once()->andReturn($euro); + + // calls to the bill repository: + $billRepos->shouldReceive('findBill')->withArgs([null, null, null])->once()->andReturnNull(); + + // calls to the budget repository + $budgetRepos->shouldReceive('findBudget')->withArgs([null, null, null])->once()->andReturnNull(); + + // calls to the category repository + $catRepos->shouldReceive('findCategory')->withArgs([null, null, null])->once()->andReturnNull(); + + // calls to the piggy bank repository: + $piggyRepos->shouldReceive('findPiggyBank')->withArgs([null, 1, 'Some name'])->once()->andReturn($piggyBank); + + // calls to the piggy factory + $eventFactory->shouldReceive('create')->once()->andReturnNull(); + + // calls to the account repository + $accountRepos->shouldReceive('findNull')->withArgs([$source->id])->once()->andReturn($source); + $accountRepos->shouldReceive('findNull')->withArgs([$destination->id])->once()->andReturn($destination); + + // calls to the meta factory: + $metaFactory->shouldReceive('updateOrCreate')->atLeast()->once()->andReturnNull(); + + /** @var TransactionJournalFactory $factory */ + $factory = app(TransactionJournalFactory::class); + $factory->setUser($this->user()); + $collection = $factory->create($data); + + /** @var TransactionJournal $journal */ + $journal = $collection->first(); + // collection should have one journal. + $this->assertCount(1, $collection); + $this->assertInstanceOf(TransactionJournal::class, $journal); + $this->assertEquals('(empty description)', $journal->description); + $this->assertCount(0, $journal->budgets); + $this->assertCount(0, $journal->categories); + $this->assertCount(2, $journal->transactions); + } + + /** + * @covers \FireflyIII\Factory\TransactionJournalFactory + */ + public function testForeignCurrency(): void + { + // mock used repositories. + $billRepos = $this->mock(BillRepositoryInterface::class); + $budgetRepos = $this->mock(BudgetRepositoryInterface::class); + $catRepos = $this->mock(CategoryRepositoryInterface::class); + $curRepos = $this->mock(CurrencyRepositoryInterface::class); + $piggyRepos = $this->mock(PiggyBankRepositoryInterface::class); + $typeRepos = $this->mock(TransactionTypeRepositoryInterface::class); + $eventFactory = $this->mock(PiggyBankEventFactory::class); + $tagFactory = $this->mock(TagFactory::class); + $accountFactory = $this->mock(AccountFactory::class); + $currencyFactory = $this->mock(TransactionCurrencyFactory::class); + $metaFactory = $this->mock(TransactionJournalMetaFactory::class); + $accountRepos = $this->mock(AccountRepositoryInterface::class); + + // data to return from various calls: + $type = TransactionType::whereType(TransactionType::WITHDRAWAL)->first(); + $euro = TransactionCurrency::whereCode('EUR')->first(); + $usd = TransactionCurrency::whereCode('USD')->first(); + $asset = $this->getRandomAsset(); + $expense = $this->getRandomExpense(); + + // data to submit. + $data = [ + 'transactions' => [ + // first transaction: + [ + 'source_id' => $asset->id, + 'amount' => '1', + 'foreign_currency_code' => 'USD', + 'foreign_amount' => '2', + 'notes' => 'I am some notes', + ], + ], + ]; + + // calls to setUser: + $curRepos->shouldReceive('setUser')->once(); + $billRepos->shouldReceive('setUser')->once(); + $budgetRepos->shouldReceive('setUser')->once(); + $catRepos->shouldReceive('setUser')->once(); + $piggyRepos->shouldReceive('setUser')->once(); + $accountRepos->shouldReceive('setUser')->once(); + $tagFactory->shouldReceive('setUser')->once(); + + // calls to transaction type repository. + $typeRepos->shouldReceive('findTransactionType')->withArgs([null, null])->once()->andReturn($type); + + // calls to the currency repository: + $curRepos->shouldReceive('findCurrency')->withArgs([null, null, null])->once()->andReturn($euro); + $curRepos->shouldReceive('findCurrency')->withArgs([null, null, 'USD'])->once()->andReturn($usd); + + // calls to the bill repository: + $billRepos->shouldReceive('findBill')->withArgs([null, null, null])->once()->andReturnNull(); + + // calls to the budget repository + $budgetRepos->shouldReceive('findBudget')->withArgs([null, null, null])->once()->andReturnNull(); + + // calls to the category repository + $catRepos->shouldReceive('findCategory')->withArgs([null, null, null])->once()->andReturnNull(); + + // calls to the account repository + $accountRepos->shouldReceive('findNull')->withArgs([$asset->id])->once()->andReturn($asset); + $accountRepos->shouldReceive('getCashAccount')->once()->andReturn($expense); + + // calls to the meta factory: + $metaFactory->shouldReceive('updateOrCreate')->atLeast()->once()->andReturnNull(); + + + /** @var TransactionJournalFactory $factory */ + $factory = app(TransactionJournalFactory::class); + $factory->setUser($this->user()); + $collection = $factory->create($data); + + /** @var TransactionJournal $journal */ + $journal = $collection->first(); + // collection should have one journal. + $this->assertCount(1, $collection); + $this->assertInstanceOf(TransactionJournal::class, $journal); + $this->assertEquals('(empty description)', $journal->description); + $this->assertCount(0, $journal->budgets); + $this->assertCount(0, $journal->categories); + $this->assertCount(2, $journal->transactions); + $this->assertEquals('I am some notes', $journal->notes->first()->text); + $this->assertEquals('EUR', $journal->transactions->first()->transactionCurrency->code); + $this->assertEquals('USD', $journal->transactions->first()->foreignCurrency->code); + } + + /** + * @covers \FireflyIII\Factory\TransactionJournalFactory + */ + public function testNotes(): void + { + // mock used repositories. + $billRepos = $this->mock(BillRepositoryInterface::class); + $budgetRepos = $this->mock(BudgetRepositoryInterface::class); + $catRepos = $this->mock(CategoryRepositoryInterface::class); + $curRepos = $this->mock(CurrencyRepositoryInterface::class); + $piggyRepos = $this->mock(PiggyBankRepositoryInterface::class); + $typeRepos = $this->mock(TransactionTypeRepositoryInterface::class); + $eventFactory = $this->mock(PiggyBankEventFactory::class); + $tagFactory = $this->mock(TagFactory::class); + $accountFactory = $this->mock(AccountFactory::class); + $currencyFactory = $this->mock(TransactionCurrencyFactory::class); + $metaFactory = $this->mock(TransactionJournalMetaFactory::class); + $accountRepos = $this->mock(AccountRepositoryInterface::class); + + // data to return from various calls: + $type = TransactionType::whereType(TransactionType::WITHDRAWAL)->first(); + $euro = TransactionCurrency::whereCode('EUR')->first(); + $asset = $this->getRandomAsset(); + $expense = $this->getRandomExpense(); + + // data to submit. + $data = [ + 'transactions' => [ + // first transaction: + [ + 'source_id' => $asset->id, + 'amount' => '1', + 'notes' => 'I am some notes', + ], + ], + ]; + + // calls to setUser: + $curRepos->shouldReceive('setUser')->once(); + $billRepos->shouldReceive('setUser')->once(); + $budgetRepos->shouldReceive('setUser')->once(); + $catRepos->shouldReceive('setUser')->once(); + $piggyRepos->shouldReceive('setUser')->once(); + $accountRepos->shouldReceive('setUser')->once(); + $tagFactory->shouldReceive('setUser')->once(); + + // calls to transaction type repository. + $typeRepos->shouldReceive('findTransactionType')->withArgs([null, null])->once()->andReturn($type); + + // calls to the currency repository: + $curRepos->shouldReceive('findCurrency')->withArgs([null, null, null])->once()->andReturn($euro); + + // calls to the bill repository: + $billRepos->shouldReceive('findBill')->withArgs([null, null, null])->once()->andReturnNull(); + + // calls to the budget repository + $budgetRepos->shouldReceive('findBudget')->withArgs([null, null, null])->once()->andReturnNull(); + + // calls to the category repository + $catRepos->shouldReceive('findCategory')->withArgs([null, null, null])->once()->andReturnNull(); + + // calls to the account repository + $accountRepos->shouldReceive('findNull')->withArgs([$asset->id])->once()->andReturn($asset); + $accountRepos->shouldReceive('getCashAccount')->once()->andReturn($expense); + + // calls to the meta factory: + $metaFactory->shouldReceive('updateOrCreate')->atLeast()->once()->andReturnNull(); + + + /** @var TransactionJournalFactory $factory */ + $factory = app(TransactionJournalFactory::class); + $factory->setUser($this->user()); + $collection = $factory->create($data); + + /** @var TransactionJournal $journal */ + $journal = $collection->first(); + // collection should have one journal. + $this->assertCount(1, $collection); + $this->assertInstanceOf(TransactionJournal::class, $journal); + $this->assertEquals('(empty description)', $journal->description); + $this->assertCount(0, $journal->budgets); + $this->assertCount(0, $journal->categories); + $this->assertCount(2, $journal->transactions); + $this->assertEquals('I am some notes', $journal->notes->first()->text); + } + + /** + * @covers \FireflyIII\Factory\TransactionJournalFactory + */ + public function testTags(): void + { + // mock used repositories. + $billRepos = $this->mock(BillRepositoryInterface::class); + $budgetRepos = $this->mock(BudgetRepositoryInterface::class); + $catRepos = $this->mock(CategoryRepositoryInterface::class); + $curRepos = $this->mock(CurrencyRepositoryInterface::class); + $piggyRepos = $this->mock(PiggyBankRepositoryInterface::class); + $typeRepos = $this->mock(TransactionTypeRepositoryInterface::class); + $eventFactory = $this->mock(PiggyBankEventFactory::class); + $tagFactory = $this->mock(TagFactory::class); + $accountFactory = $this->mock(AccountFactory::class); + $currencyFactory = $this->mock(TransactionCurrencyFactory::class); + $metaFactory = $this->mock(TransactionJournalMetaFactory::class); + $accountRepos = $this->mock(AccountRepositoryInterface::class); + + // data to return from various calls: + $type = TransactionType::whereType(TransactionType::WITHDRAWAL)->first(); + $euro = TransactionCurrency::whereCode('EUR')->first(); + $asset = $this->getRandomAsset(); + $expense = $this->getRandomExpense(); + $tag = Tag::first(); + + // data to submit. + $data = [ + 'transactions' => [ + // first transaction: + [ + 'source_id' => $asset->id, + 'amount' => '1', + 'tags' => ['tagA', 'B', '', 'C'], + ], + ], + ]; + + // calls to setUser: + $curRepos->shouldReceive('setUser')->once(); + $billRepos->shouldReceive('setUser')->once(); + $budgetRepos->shouldReceive('setUser')->once(); + $catRepos->shouldReceive('setUser')->once(); + $piggyRepos->shouldReceive('setUser')->once(); + $accountRepos->shouldReceive('setUser')->once(); + $tagFactory->shouldReceive('setUser')->once(); + + // calls to transaction type repository. + $typeRepos->shouldReceive('findTransactionType')->withArgs([null, null])->once()->andReturn($type); + + // calls to the currency repository: + $curRepos->shouldReceive('findCurrency')->withArgs([null, null, null])->once()->andReturn($euro); + + // calls to the bill repository: + $billRepos->shouldReceive('findBill')->withArgs([null, null, null])->once()->andReturnNull(); + + // calls to the budget repository + $budgetRepos->shouldReceive('findBudget')->withArgs([null, null, null])->once()->andReturnNull(); + + // calls to the category repository + $catRepos->shouldReceive('findCategory')->withArgs([null, null, null])->once()->andReturnNull(); + + // calls to the account repository + $accountRepos->shouldReceive('findNull')->withArgs([$asset->id])->once()->andReturn($asset); + $accountRepos->shouldReceive('getCashAccount')->once()->andReturn($expense); + + // calls to tag factory + $tagFactory->shouldReceive('findOrCreate')->once()->withArgs(['tagA'])->andReturn($tag); + $tagFactory->shouldReceive('findOrCreate')->once()->withArgs(['B'])->andReturn($tag); + $tagFactory->shouldReceive('findOrCreate')->once()->withArgs(['C'])->andReturnNull(); + + // calls to the meta factory: + $metaFactory->shouldReceive('updateOrCreate')->atLeast()->once()->andReturnNull(); + + /** @var TransactionJournalFactory $factory */ + $factory = app(TransactionJournalFactory::class); + $factory->setUser($this->user()); + $collection = $factory->create($data); + + /** @var TransactionJournal $journal */ + $journal = $collection->first(); + // collection should have one journal. + $this->assertCount(1, $collection); + $this->assertInstanceOf(TransactionJournal::class, $journal); + $this->assertEquals('(empty description)', $journal->description); + $this->assertCount(0, $journal->budgets); + $this->assertCount(0, $journal->categories); + $this->assertCount(2, $journal->transactions); + $this->assertCount(1, $journal->tags); // we return the same tag every time. + } + + /** + * @param int $id + * + * @return Account + */ + private function getAnotherRandomAsset(int $id): Account + { + $query = Account:: + leftJoin('account_types', 'account_types.id', '=', 'accounts.account_type_id') + ->whereNull('accounts.deleted_at') + ->where('accounts.user_id', $this->user()->id) + ->where('account_types.type', AccountType::ASSET) + ->where('accounts.id', '!=', $id) + ->inRandomOrder()->take(1); + + return $query->first(['accounts.*']); } }