. */ declare(strict_types=1); namespace tests\Unit\Import\Storage; use Carbon\Carbon; use FireflyIII\Exceptions\FireflyException; use FireflyIII\Helpers\Collector\JournalCollector; use FireflyIII\Helpers\Collector\JournalCollectorInterface; use FireflyIII\Helpers\Filter\InternalTransferFilter; use FireflyIII\Import\Storage\ImportArrayStorage; use FireflyIII\Models\ImportJob; use FireflyIII\Models\Rule; use FireflyIII\Models\TransactionJournal; use FireflyIII\Models\TransactionJournalMeta; use FireflyIII\Models\TransactionType; use FireflyIII\Repositories\ImportJob\ImportJobRepositoryInterface; use FireflyIII\Repositories\Journal\JournalRepositoryInterface; use FireflyIII\Repositories\Rule\RuleRepositoryInterface; use FireflyIII\Repositories\Tag\TagRepositoryInterface; use Illuminate\Support\Collection; use Mockery; use Tests\TestCase; /** * Class ImportArrayStorageTest */ class ImportArrayStorageTest extends TestCase { /** * Very basic storage routine. Doesn't call store() * * @covers \FireflyIII\Import\Storage\ImportArrayStorage */ public function testBasic(): void { // make fake job $job = new ImportJob; $job->user()->associate($this->user()); $job->key = 'a_storage' . random_int(1, 1000); $job->status = 'new'; $job->stage = 'new'; $job->provider = 'fake'; $job->file_type = ''; $job->configuration = []; $job->transactions = []; $job->save(); // mock stuff $repository = $this->mock(ImportJobRepositoryInterface::class); $journalRepos = $this->mock(JournalRepositoryInterface::class); // mock calls: $repository->shouldReceive('setUser')->once(); $journalRepos->shouldReceive('setUser')->once(); $storage = new ImportArrayStorage; $storage->setJob($job); } /** * Two withdrawals, one of which is duplicated. * * @covers \FireflyIII\Import\Storage\ImportArrayStorage */ public function testBasicStoreIsDouble(): void { // make fake job $transactions = [$this->singleWithdrawal(), $this->singleWithdrawal()]; $job = new ImportJob; $job->user()->associate($this->user()); $job->key = 'b_storage' . random_int(1, 1000); $job->status = 'new'; $job->stage = 'new'; $job->provider = 'fake'; $job->file_type = ''; $job->configuration = ['apply-rules' => true]; $job->transactions = $transactions; $job->save(); // get some stuff: $tag = $this->user()->tags()->inRandomOrder()->first(); $journal = $this->user()->transactionJournals()->inRandomOrder()->first(); $ruleOne = new Rule; $ruleOne->stop_processing = false; $ruleTwo = new Rule; $ruleTwo->stop_processing = true; $meta = new TransactionJournalMeta; $meta->transaction_journal_id = 3; // mock stuff $repository = $this->mock(ImportJobRepositoryInterface::class); $collector = $this->mock(JournalCollectorInterface::class); $tagRepos = $this->mock(TagRepositoryInterface::class); $ruleRepos = $this->mock(RuleRepositoryInterface::class); $journalRepos = $this->mock(JournalRepositoryInterface::class); // mock calls: $repository->shouldReceive('setUser')->once(); $repository->shouldReceive('setStatus')->withAnyArgs(); $ruleRepos->shouldReceive('setUser')->once(); $tagRepos->shouldReceive('setUser')->once(); $tagRepos->shouldReceive('store')->once()->andReturn($tag); $repository->shouldReceive('setTag')->once(); $ruleRepos->shouldReceive('getForImport')->andReturn(new Collection([$ruleOne, $ruleTwo])); $journalRepos->shouldReceive('setUser')->once(); $journalRepos->shouldReceive('store')->once()->andReturn($journal); $journalRepos->shouldReceive('findByHash')->andReturn(null, $meta)->twice(); $repository->shouldReceive('addErrorMessage')->once() ->withArgs([Mockery::any(), 'Entry #1 ("' . $transactions[1]['description'] . '") could not be imported. It already exists.']); $storage = new ImportArrayStorage; $storage->setJob($job); $result = new Collection; try { $result = $storage->store(); } catch (FireflyException $e) { $this->assertTrue(false, $e->getMessage()); } $this->assertCount(1, $result); } /** * Very basic storage routine. Call store with no data. * * @covers \FireflyIII\Import\Storage\ImportArrayStorage */ public function testBasicStoreNothing(): void { // make fake job $job = new ImportJob; $job->user()->associate($this->user()); $job->key = 'c_storage' . random_int(1, 1000); $job->status = 'new'; $job->stage = 'new'; $job->provider = 'fake'; $job->file_type = ''; $job->configuration = []; $job->transactions = []; $job->save(); // mock stuff $repository = $this->mock(ImportJobRepositoryInterface::class); $journalRepos = $this->mock(JournalRepositoryInterface::class); // mock calls: $repository->shouldReceive('setUser')->once(); $repository->shouldReceive('setStatus')->withAnyArgs(); $journalRepos->shouldReceive('setUser')->once(); $storage = new ImportArrayStorage; $storage->setJob($job); $result = new Collection; try { $result = $storage->store(); } catch (FireflyException $e) { $this->assertTrue(false, $e->getMessage()); } $this->assertCount(0, $result); } /** * Call store with no data, also assume rules. * * @covers \FireflyIII\Import\Storage\ImportArrayStorage */ public function testBasicStoreNothingWithRules(): void { // make fake job $job = new ImportJob; $job->user()->associate($this->user()); $job->key = 'd_storage' . random_int(1, 1000); $job->status = 'new'; $job->stage = 'new'; $job->provider = 'fake'; $job->file_type = ''; $job->configuration = ['apply-rules' => true]; $job->transactions = []; $job->save(); // mock stuff $repository = $this->mock(ImportJobRepositoryInterface::class); $collector = $this->mock(JournalCollectorInterface::class); $tagRepos = $this->mock(TagRepositoryInterface::class); $ruleRepos = $this->mock(RuleRepositoryInterface::class); $journalRepos = $this->mock(JournalRepositoryInterface::class); // mock calls: $repository->shouldReceive('setUser')->once(); $repository->shouldReceive('setStatus')->withAnyArgs(); $ruleRepos->shouldReceive('setUser')->once(); $ruleRepos->shouldReceive('getForImport')->andReturn(new Collection); $journalRepos->shouldReceive('setUser')->once(); $storage = new ImportArrayStorage; $storage->setJob($job); $result = new Collection; try { $result = $storage->store(); } catch (FireflyException $e) { $this->assertTrue(false, $e->getMessage()); } $this->assertCount(0, $result); } /** * @covers \FireflyIII\Import\Storage\ImportArrayStorage */ public function testBasicStoreSingleWithNoRules(): void { // make fake job $job = new ImportJob; $job->user()->associate($this->user()); $job->key = 'e_storage' . random_int(1, 1000); $job->status = 'new'; $job->stage = 'new'; $job->provider = 'fake'; $job->file_type = ''; $job->configuration = ['apply-rules' => true]; $job->transactions = [$this->singleWithdrawal()]; $job->save(); // get some stuff: $tag = $this->user()->tags()->inRandomOrder()->first(); $journal = $this->user()->transactionJournals()->inRandomOrder()->first(); // mock stuff $repository = $this->mock(ImportJobRepositoryInterface::class); $collector = $this->mock(JournalCollectorInterface::class); $tagRepos = $this->mock(TagRepositoryInterface::class); $ruleRepos = $this->mock(RuleRepositoryInterface::class); $journalRepos = $this->mock(JournalRepositoryInterface::class); // mock calls: $repository->shouldReceive('setUser')->once(); $repository->shouldReceive('setStatus')->withAnyArgs(); $ruleRepos->shouldReceive('setUser')->once(); $tagRepos->shouldReceive('setUser')->once(); $tagRepos->shouldReceive('store')->once()->andReturn($tag); $repository->shouldReceive('setTag')->once(); $ruleRepos->shouldReceive('getForImport')->andReturn(new Collection); $journalRepos->shouldReceive('setUser')->once(); $journalRepos->shouldReceive('store')->once()->andReturn($journal); $journalRepos->shouldReceive('findByHash')->andReturn(null)->once(); $storage = new ImportArrayStorage; $storage->setJob($job); $result = new Collection; try { $result = $storage->store(); } catch (FireflyException $e) { $this->assertTrue(false, $e->getMessage()); } $this->assertCount(1, $result); } /** * @covers \FireflyIII\Import\Storage\ImportArrayStorage */ public function testBasicStoreSingleWithRules(): void { // make fake job $job = new ImportJob; $job->user()->associate($this->user()); $job->key = 'f_storage' . random_int(1, 1000); $job->status = 'new'; $job->stage = 'new'; $job->provider = 'fake'; $job->file_type = ''; $job->configuration = ['apply-rules' => true]; $job->transactions = [$this->singleWithdrawal()]; $job->save(); // get some stuff: $tag = $this->user()->tags()->inRandomOrder()->first(); $journal = $this->user()->transactionJournals()->inRandomOrder()->first(); $ruleOne = new Rule; $ruleOne->stop_processing = false; $ruleTwo = new Rule; $ruleTwo->stop_processing = true; // mock stuff $repository = $this->mock(ImportJobRepositoryInterface::class); $collector = $this->mock(JournalCollectorInterface::class); $tagRepos = $this->mock(TagRepositoryInterface::class); $ruleRepos = $this->mock(RuleRepositoryInterface::class); $journalRepos = $this->mock(JournalRepositoryInterface::class); // mock calls: $repository->shouldReceive('setUser')->once(); $repository->shouldReceive('setStatus')->withAnyArgs(); $ruleRepos->shouldReceive('setUser')->once(); $tagRepos->shouldReceive('setUser')->once(); $tagRepos->shouldReceive('store')->once()->andReturn($tag); $repository->shouldReceive('setTag')->once(); $ruleRepos->shouldReceive('getForImport')->andReturn(new Collection([$ruleOne, $ruleTwo])); $journalRepos->shouldReceive('setUser')->once(); $journalRepos->shouldReceive('store')->once()->andReturn($journal); $journalRepos->shouldReceive('findByHash')->andReturn(null)->once(); $storage = new ImportArrayStorage; $storage->setJob($job); $result = new Collection; try { $result = $storage->store(); } catch (FireflyException $e) { $this->assertTrue(false, $e->getMessage()); } $this->assertCount(1, $result); } /** * @covers \FireflyIII\Import\Storage\ImportArrayStorage */ public function testBasicStoreTransferWithRules(): void { // make fake job $job = new ImportJob; $job->user()->associate($this->user()); $job->key = 'g_storage' . random_int(1, 1000); $job->status = 'new'; $job->stage = 'new'; $job->provider = 'fake'; $job->file_type = ''; $job->configuration = ['apply-rules' => true]; $job->transactions = [$this->singleTransfer(), $this->singleWithdrawal()]; $job->save(); // get a transfer: $transfer = $this->user()->transactionJournals() ->inRandomOrder()->where('transaction_type_id', 3) ->first(); // get transfer as a collection, so the compare routine works. $journalCollector = new JournalCollector(); $journalCollector->setUser($this->user()); $journalCollector->setJournals(new Collection([$transfer])); $transferCollection = $journalCollector->withOpposingAccount()->getJournals(); // get some stuff: $tag = $this->user()->tags()->inRandomOrder()->first(); $journal = $this->user()->transactionJournals()->inRandomOrder()->first(); $ruleOne = new Rule; $ruleOne->stop_processing = false; $ruleTwo = new Rule; $ruleTwo->stop_processing = true; // mock stuff $repository = $this->mock(ImportJobRepositoryInterface::class); $collector = $this->mock(JournalCollectorInterface::class); $tagRepos = $this->mock(TagRepositoryInterface::class); $ruleRepos = $this->mock(RuleRepositoryInterface::class); $journalRepos = $this->mock(JournalRepositoryInterface::class); // mock calls: $repository->shouldReceive('setUser')->once(); $repository->shouldReceive('setStatus')->withAnyArgs(); $ruleRepos->shouldReceive('setUser')->once(); $tagRepos->shouldReceive('setUser')->once(); $tagRepos->shouldReceive('store')->once()->andReturn($tag); $repository->shouldReceive('setTag')->once(); $ruleRepos->shouldReceive('getForImport')->andReturn(new Collection([$ruleOne, $ruleTwo])); $journalRepos->shouldReceive('setUser')->once(); $journalRepos->shouldReceive('store')->twice()->andReturn($journal); $journalRepos->shouldReceive('findByHash')->andReturn(null)->twice(); // mock collector so it will return some transfers: $collector->shouldReceive('setAllAssetAccounts')->once()->andReturnSelf(); $collector->shouldReceive('setTypes')->withArgs([[TransactionType::TRANSFER]])->once()->andReturnSelf(); $collector->shouldReceive('withOpposingAccount')->once()->andReturnSelf(); $collector->shouldReceive('removeFilter')->withArgs([InternalTransferFilter::class])->once()->andReturnSelf(); $collector->shouldReceive('getJournals')->andReturn($transferCollection); $storage = new ImportArrayStorage; $storage->setJob($job); $result = new Collection; try { $result = $storage->store(); } catch (FireflyException $e) { $this->assertTrue(false, $e->getMessage()); } $this->assertCount(2, $result); } /** * @covers \FireflyIII\Import\Storage\ImportArrayStorage */ public function testBasicStoreDoubleTransferWithRules(): void { // get a transfer: /** @var TransactionJournal $transfer */ $transfer = $this->user()->transactionJournals() ->inRandomOrder()->where('transaction_type_id', 3) ->first(); // get transfer as a collection, so the compare routine works. $journalCollector = new JournalCollector(); $journalCollector->setUser($this->user()); $journalCollector->setJournals(new Collection([$transfer])); $transferCollection = $journalCollector->withOpposingAccount()->getJournals(); // make fake job $job = new ImportJob; $job->user()->associate($this->user()); $job->key = 'h_storage' . random_int(1, 1000); $job->status = 'new'; $job->stage = 'new'; $job->provider = 'fake'; $job->file_type = ''; $job->configuration = ['apply-rules' => true]; $job->transactions = [$this->singleTransfer(), $this->singleWithdrawal(), $this->basedOnTransfer($transfer)]; $job->save(); // get some stuff: $tag = $this->user()->tags()->inRandomOrder()->first(); $journal = $this->user()->transactionJournals()->inRandomOrder()->first(); $ruleOne = new Rule; $ruleOne->stop_processing = false; $ruleTwo = new Rule; $ruleTwo->stop_processing = true; // mock stuff $repository = $this->mock(ImportJobRepositoryInterface::class); $collector = $this->mock(JournalCollectorInterface::class); $tagRepos = $this->mock(TagRepositoryInterface::class); $ruleRepos = $this->mock(RuleRepositoryInterface::class); $journalRepos = $this->mock(JournalRepositoryInterface::class); // mock calls: $repository->shouldReceive('setUser')->once(); $repository->shouldReceive('setStatus')->withAnyArgs(); $ruleRepos->shouldReceive('setUser')->once(); $tagRepos->shouldReceive('setUser')->once(); $tagRepos->shouldReceive('store')->once()->andReturn($tag); $repository->shouldReceive('setTag')->once(); $ruleRepos->shouldReceive('getForImport')->andReturn(new Collection([$ruleOne, $ruleTwo])); $journalRepos->shouldReceive('setUser')->once(); $journalRepos->shouldReceive('store')->twice()->andReturn($journal); $journalRepos->shouldReceive('findByHash')->andReturn(null)->times(3); $repository->shouldReceive('addErrorMessage')->withArgs( [Mockery::any(), 'Entry #2 ("' . $transfer->description . '") could not be imported. Such a transfer already exists.'] )->once(); // mock collector so it will return some transfers: $collector->shouldReceive('setAllAssetAccounts')->once()->andReturnSelf(); $collector->shouldReceive('setTypes')->withArgs([[TransactionType::TRANSFER]])->once()->andReturnSelf(); $collector->shouldReceive('withOpposingAccount')->once()->andReturnSelf(); $collector->shouldReceive('removeFilter')->withArgs([InternalTransferFilter::class])->once()->andReturnSelf(); $collector->shouldReceive('getJournals')->andReturn($transferCollection); $storage = new ImportArrayStorage; $storage->setJob($job); $result = new Collection; try { $result = $storage->store(); } catch (FireflyException $e) { $this->assertTrue(false, $e->getMessage()); } $this->assertCount(2, $result); } /** * @param TransactionJournal $transfer * * @return array */ private function basedOnTransfer(TransactionJournal $transfer): array { $destination = $transfer->transactions()->where('amount', '>', 0)->first(); $source = $transfer->transactions()->where('amount', '<', 0)->first(); $amount = $destination->amount; return [ 'type' => 'transfer', 'date' => $transfer->date->format('Y-m-d'), 'tags' => '', 'user' => $this->user()->id, // all custom fields: 'internal_reference' => null, 'notes' => null, // journal data: 'description' => $transfer->description, 'piggy_bank_id' => null, 'piggy_bank_name' => null, 'bill_id' => null, 'bill_name' => null, // transaction data: 'transactions' => [ [ 'currency_id' => null, 'currency_code' => 'EUR', 'description' => null, 'amount' => $amount, 'budget_id' => null, 'budget_name' => null, 'category_id' => null, 'category_name' => null, 'source_id' => $source->account_id, 'source_name' => null, 'destination_id' => $destination->account_id, 'destination_name' => null, 'foreign_currency_id' => null, 'foreign_currency_code' => null, 'foreign_amount' => null, 'reconciled' => false, 'identifier' => 0, ], ], ]; } /** * @return array */ private function singleTransfer(): array { return [ 'type' => 'transfer', 'date' => Carbon::create()->format('Y-m-d'), 'tags' => '', 'user' => $this->user()->id, // all custom fields: 'internal_reference' => null, 'notes' => null, // journal data: 'description' => 'Some TEST transfer #' . random_int(1, 10000), 'piggy_bank_id' => null, 'piggy_bank_name' => null, 'bill_id' => null, 'bill_name' => null, // transaction data: 'transactions' => [ [ 'currency_id' => null, 'currency_code' => 'EUR', 'description' => null, 'amount' => random_int(500, 5000) / 100, 'budget_id' => null, 'budget_name' => null, 'category_id' => null, 'category_name' => null, 'source_id' => 1, 'source_name' => null, 'destination_id' => 2, 'destination_name' => null, 'foreign_currency_id' => null, 'foreign_currency_code' => null, 'foreign_amount' => null, 'reconciled' => false, 'identifier' => 0, ], ], ]; } /** * @return array */ private function singleWithdrawal(): array { return [ 'type' => 'withdrawal', 'date' => Carbon::create()->format('Y-m-d'), 'tags' => '', 'user' => $this->user()->id, // all custom fields: 'internal_reference' => null, 'notes' => null, // journal data: 'description' => 'Some TEST withdrawal #' . random_int(1, 10000), 'piggy_bank_id' => null, 'piggy_bank_name' => null, 'bill_id' => null, 'bill_name' => null, // transaction data: 'transactions' => [ [ 'currency_id' => null, 'currency_code' => 'EUR', 'description' => null, 'amount' => random_int(500, 5000) / 100, 'budget_id' => null, 'budget_name' => null, 'category_id' => null, 'category_name' => null, 'source_id' => null, 'source_name' => 'Checking Account', 'destination_id' => null, 'destination_name' => 'Random TEST expense account #' . random_int(1, 10000), 'foreign_currency_id' => null, 'foreign_currency_code' => null, 'foreign_amount' => null, 'reconciled' => false, 'identifier' => 0, ], ], ]; } }