From 3141ec040635cdcecce918139a22e0ea6c1f73af Mon Sep 17 00:00:00 2001 From: James Cole Date: Mon, 24 Aug 2020 07:03:17 +0200 Subject: [PATCH] Expand search. --- .../Events/StoredGroupEventHandler.php | 25 ++- .../Events/UpdatedGroupEventHandler.php | 25 ++- app/Repositories/Rule/RuleRepository.php | 38 +++++ .../Rule/RuleRepositoryInterface.php | 14 ++ app/Support/Search/OperatorQuerySearch.php | 6 +- app/TransactionRules/Actions/AddTag.php | 4 + app/TransactionRules/Actions/SetCategory.php | 5 +- .../Engine/SearchRuleEngine.php | 150 ++++++++++++++---- config/firefly.php | 3 + .../Search/OperatorQuerySearchTest.php | 40 ++++- 10 files changed, 256 insertions(+), 54 deletions(-) diff --git a/app/Handlers/Events/StoredGroupEventHandler.php b/app/Handlers/Events/StoredGroupEventHandler.php index 60a0ad99a2..05b86d5fe4 100644 --- a/app/Handlers/Events/StoredGroupEventHandler.php +++ b/app/Handlers/Events/StoredGroupEventHandler.php @@ -24,7 +24,9 @@ namespace FireflyIII\Handlers\Events; use FireflyIII\Events\StoredTransactionGroup; use FireflyIII\Models\TransactionJournal; +use FireflyIII\Repositories\Rule\RuleRepositoryInterface; use FireflyIII\TransactionRules\Engine\RuleEngine; +use FireflyIII\TransactionRules\Engine\RuleEngineInterface; use Log; /** @@ -46,17 +48,26 @@ class StoredGroupEventHandler } Log::debug('Now in StoredGroupEventHandler::processRules()'); - /** @var RuleEngine $ruleEngine */ - $ruleEngine = app(RuleEngine::class); - $ruleEngine->setUser($storedGroupEvent->transactionGroup->user); - $ruleEngine->setAllRules(true); - $ruleEngine->setTriggerMode(RuleEngine::TRIGGER_STORE); $journals = $storedGroupEvent->transactionGroup->transactionJournals; - + $array = []; /** @var TransactionJournal $journal */ foreach ($journals as $journal) { - $ruleEngine->processTransactionJournal($journal); + $array[] = $journal->id; } + $journalIds = implode(',', $array); + Log::debug(sprintf('Add local operator for journal(s): %s', $journalIds)); + + // collect rules: + $ruleRepository = app(RuleRepositoryInterface::class); + $ruleRepository->setUser($storedGroupEvent->transactionGroup->user); + $rules = $ruleRepository->getStoreRules(); + + // file rule engine. + $newRuleEngine = app(RuleEngineInterface::class); + $newRuleEngine->setUser($storedGroupEvent->transactionGroup->user); + $newRuleEngine->addOperator(['type' => 'journal_id', 'value' => $journalIds]); + $newRuleEngine->setRules($rules); + $newRuleEngine->fire(); } } diff --git a/app/Handlers/Events/UpdatedGroupEventHandler.php b/app/Handlers/Events/UpdatedGroupEventHandler.php index 7c4292bca4..4d13bc672c 100644 --- a/app/Handlers/Events/UpdatedGroupEventHandler.php +++ b/app/Handlers/Events/UpdatedGroupEventHandler.php @@ -27,7 +27,9 @@ use FireflyIII\Models\Account; use FireflyIII\Models\Transaction; use FireflyIII\Models\TransactionJournal; use FireflyIII\Models\TransactionType; +use FireflyIII\Repositories\Rule\RuleRepositoryInterface; use FireflyIII\TransactionRules\Engine\RuleEngine; +use FireflyIII\TransactionRules\Engine\RuleEngineInterface; use Log; /** @@ -88,17 +90,26 @@ class UpdatedGroupEventHandler return; } - /** @var RuleEngine $ruleEngine */ - $ruleEngine = app(RuleEngine::class); - $ruleEngine->setUser($updatedGroupEvent->transactionGroup->user); - $ruleEngine->setAllRules(true); - $ruleEngine->setTriggerMode(RuleEngine::TRIGGER_UPDATE); $journals = $updatedGroupEvent->transactionGroup->transactionJournals; - + $array = []; /** @var TransactionJournal $journal */ foreach ($journals as $journal) { - $ruleEngine->processTransactionJournal($journal); + $array[] = $journal->id; } + $journalIds = implode(',', $array); + Log::debug(sprintf('Add local operator for journal(s): %s', $journalIds)); + + // collect rules: + $ruleRepository = app(RuleRepositoryInterface::class); + $ruleRepository->setUser($updatedGroupEvent->transactionGroup->user); + $rules = $ruleRepository->getUpdateRules(); + + // file rule engine. + $newRuleEngine = app(RuleEngineInterface::class); + $newRuleEngine->setUser($updatedGroupEvent->transactionGroup->user); + $newRuleEngine->addOperator(['type' => 'journal_id', 'value' => $journalIds]); + $newRuleEngine->setRules($rules); + $newRuleEngine->fire(); } } diff --git a/app/Repositories/Rule/RuleRepository.php b/app/Repositories/Rule/RuleRepository.php index cb39e7d1e6..b2e743bb4f 100644 --- a/app/Repositories/Rule/RuleRepository.php +++ b/app/Repositories/Rule/RuleRepository.php @@ -497,4 +497,42 @@ class RuleRepository implements RuleRepositoryInterface return $rule; } + + /** + * @inheritDoc + */ + public function getStoreRules(): Collection + { + $collection = $this->user->rules()->with(['ruleGroup','ruleTriggers'])->get(); + $filtered = new Collection; + /** @var Rule $rule */ + foreach($collection as $rule) { + /** @var RuleTrigger $ruleTrigger */ + foreach($rule->ruleTriggers as $ruleTrigger) { + if('user_action' === $ruleTrigger->trigger_type && 'store-journal' === $ruleTrigger->trigger_value) { + $filtered->push($rule); + } + } + } + return $filtered; + } + + /** + * @inheritDoc + */ + public function getUpdateRules(): Collection + { + $collection = $this->user->rules()->with(['ruleGroup','ruleTriggers'])->get(); + $filtered = new Collection; + /** @var Rule $rule */ + foreach($collection as $rule) { + /** @var RuleTrigger $ruleTrigger */ + foreach($rule->ruleTriggers as $ruleTrigger) { + if('user_action' === $ruleTrigger->trigger_type && 'update-journal' === $ruleTrigger->trigger_value) { + $filtered->push($rule); + } + } + } + return $filtered; + } } diff --git a/app/Repositories/Rule/RuleRepositoryInterface.php b/app/Repositories/Rule/RuleRepositoryInterface.php index a42e861126..178eb3953e 100644 --- a/app/Repositories/Rule/RuleRepositoryInterface.php +++ b/app/Repositories/Rule/RuleRepositoryInterface.php @@ -76,6 +76,20 @@ interface RuleRepositoryInterface */ public function getAll(): Collection; + /** + * Get all the users rules that trigger on storage. + * + * @return Collection + */ + public function getStoreRules(): Collection; + + /** + * Get all the users rules that trigger on update. + * + * @return Collection + */ + public function getUpdateRules(): Collection; + /** * @return RuleGroup */ diff --git a/app/Support/Search/OperatorQuerySearch.php b/app/Support/Search/OperatorQuerySearch.php index 83450a8c48..434160e27b 100644 --- a/app/Support/Search/OperatorQuerySearch.php +++ b/app/Support/Search/OperatorQuerySearch.php @@ -299,6 +299,10 @@ class OperatorQuerySearch implements SearchInterface $this->collector->setSourceAccounts(new Collection([$account])); } break; + case 'journal_id': + $parts = explode(',', $value); + $this->collector->setJournalIds($parts); + break; case 'destination_account_starts': $this->searchAccount($value, 2, 1); break; @@ -402,7 +406,6 @@ class OperatorQuerySearch implements SearchInterface case 'has_any_budget': $this->collector->withBudget(); break; - case 'budget': case 'budget_is': $result = $this->budgetRepository->searchBudget($value, 25); if ($result->count() > 0) { @@ -412,7 +415,6 @@ class OperatorQuerySearch implements SearchInterface // // bill // - case 'bill': case 'bill_is': $result = $this->billRepository->searchBill($value, 25); if ($result->count() > 0) { diff --git a/app/TransactionRules/Actions/AddTag.php b/app/TransactionRules/Actions/AddTag.php index a96caa041a..92ad271402 100644 --- a/app/TransactionRules/Actions/AddTag.php +++ b/app/TransactionRules/Actions/AddTag.php @@ -57,6 +57,10 @@ class AddTag implements ActionInterface /** @var TagFactory $factory */ $factory = app(TagFactory::class); $factory->setUser($journal->user); + + // TODO explode value on comma? + + $tag = $factory->findOrCreate($this->action->action_value); if (null === $tag) { diff --git a/app/TransactionRules/Actions/SetCategory.php b/app/TransactionRules/Actions/SetCategory.php index 0f0ac91866..ee8451d8b8 100644 --- a/app/TransactionRules/Actions/SetCategory.php +++ b/app/TransactionRules/Actions/SetCategory.php @@ -84,7 +84,10 @@ class SetCategory implements ActionInterface $user = User::find($journal['user_id']); $search = $this->action->action_value; - $category = $user->categories()->where('name', $search)->first(); + /** @var CategoryFactory $factory */ + $factory = app(CategoryFactory::class); + $factory->setUser($user); + $category = $factory->findOrCreate(null, $search); if (null === $category) { Log::debug(sprintf('RuleAction SetCategory could not set category of journal #%d to "%s" because no such category exists.', $journal['transaction_journal_id'], $search)); diff --git a/app/TransactionRules/Engine/SearchRuleEngine.php b/app/TransactionRules/Engine/SearchRuleEngine.php index 7fd9e3705d..7a4fbd3b86 100644 --- a/app/TransactionRules/Engine/SearchRuleEngine.php +++ b/app/TransactionRules/Engine/SearchRuleEngine.php @@ -81,7 +81,7 @@ class SearchRuleEngine implements RuleEngineInterface */ public function addOperator(array $operator): void { - Log::debug('Add operator: ', $operator); + Log::debug('Add extra operator: ', $operator); $this->operators[] = $operator; } @@ -104,40 +104,11 @@ class SearchRuleEngine implements RuleEngineInterface */ private function fireRule(Rule $rule): void { - Log::debug(sprintf('SearchRuleEngine::fireRule(%d)!', $rule->id)); - $searchArray = []; - /** @var RuleTrigger $ruleTrigger */ - foreach ($rule->ruleTriggers as $ruleTrigger) { - Log::debug(sprintf('SearchRuleEngine:: add a rule trigger: %s:"%s"', $ruleTrigger->trigger_type, $ruleTrigger->trigger_value)); - $searchArray[$ruleTrigger->trigger_type] = sprintf('"%s"', $ruleTrigger->trigger_value); + if (true === $rule->strict) { + $this->fileStrictRule($rule); + return; } - - // add local operators: - foreach ($this->operators as $operator) { - Log::debug(sprintf('SearchRuleEngine:: add local added operator: %s:"%s"', $operator['type'], $operator['value'])); - $searchArray[$operator['type']] = sprintf('"%s"', $operator['value']); - } - $toJoin = []; - foreach ($searchArray as $type => $value) { - $toJoin[] = sprintf('%s:%s', $type, $value); - } - - $searchQuery = join(' ', $toJoin); - Log::debug(sprintf('SearchRuleEngine:: Search query for rule #%d ("%s") = %s', $rule->id, $rule->title, $searchQuery)); - - // build and run the search engine. - $searchEngine = app(SearchInterface::class); - $searchEngine->setUser($this->user); - $searchEngine->setPage(1); - $searchEngine->setLimit(31337); - $searchEngine->parseQuery($searchQuery); - - $result = $searchEngine->searchTransactions(); - $collection = $result->getCollection(); - Log::debug(sprintf('SearchRuleEngine:: Found %d transactions using search engine with query "%s".', $collection->count(), $searchQuery)); - - $this->processResults($rule, $collection); - Log::debug(sprintf('SearchRuleEngine:: done processing rule #%d', $rule->id)); + $this->fileNonStrictRule($rule); } /** @@ -202,4 +173,115 @@ class SearchRuleEngine implements RuleEngineInterface } return false; } + + /** + * @param Rule $rule + * @throws FireflyException + */ + private function fileStrictRule(Rule $rule): void + { + Log::debug(sprintf('SearchRuleEngine::fileStrictRule(%d)!', $rule->id)); + $searchArray = []; + /** @var RuleTrigger $ruleTrigger */ + foreach ($rule->ruleTriggers as $ruleTrigger) { + Log::debug(sprintf('SearchRuleEngine:: add a rule trigger: %s:"%s"', $ruleTrigger->trigger_type, $ruleTrigger->trigger_value)); + $searchArray[$ruleTrigger->trigger_type] = sprintf('"%s"', $ruleTrigger->trigger_value); + } + + // add local operators: + foreach ($this->operators as $operator) { + Log::debug(sprintf('SearchRuleEngine:: add local added operator: %s:"%s"', $operator['type'], $operator['value'])); + $searchArray[$operator['type']] = sprintf('"%s"', $operator['value']); + } + $toJoin = []; + foreach ($searchArray as $type => $value) { + $toJoin[] = sprintf('%s:%s', $type, $value); + } + + $searchQuery = join(' ', $toJoin); + Log::debug(sprintf('SearchRuleEngine:: Search strict query for rule #%d ("%s") = %s', $rule->id, $rule->title, $searchQuery)); + + // build and run the search engine. + $searchEngine = app(SearchInterface::class); + $searchEngine->setUser($this->user); + $searchEngine->setPage(1); + $searchEngine->setLimit(31337); + $searchEngine->parseQuery($searchQuery); + + $result = $searchEngine->searchTransactions(); + $collection = $result->getCollection(); + Log::debug(sprintf('SearchRuleEngine:: Found %d transactions using search engine with query "%s".', $collection->count(), $searchQuery)); + + $this->processResults($rule, $collection); + Log::debug(sprintf('SearchRuleEngine:: done processing strict rule #%d', $rule->id)); + } + + /** + * @param Rule $rule + * @throws FireflyException + */ + private function fileNonStrictRule(Rule $rule): void + { + Log::debug(sprintf('SearchRuleEngine::fileNonStrictRule(%d)!', $rule->id)); + + // start a search query for individual each trigger: + $total = new Collection; + $count = 0; + /** @var RuleTrigger $ruleTrigger */ + foreach ($rule->ruleTriggers as $ruleTrigger) { + if ('user_action' === $ruleTrigger->trigger_type) { + Log::debug('Skip trigger type.'); + continue; + } + $searchArray = []; + Log::debug(sprintf('SearchRuleEngine:: non strict, will search for: %s:"%s"', $ruleTrigger->trigger_type, $ruleTrigger->trigger_value)); + $searchArray[$ruleTrigger->trigger_type] = sprintf('"%s"', $ruleTrigger->trigger_value); + + // then, add local operators as well: + foreach ($this->operators as $operator) { + Log::debug(sprintf('SearchRuleEngine:: add local added operator: %s:"%s"', $operator['type'], $operator['value'])); + $searchArray[$operator['type']] = sprintf('"%s"', $operator['value']); + } + $toJoin = []; + foreach ($searchArray as $type => $value) { + $toJoin[] = sprintf('%s:%s', $type, $value); + } + + $searchQuery = join(' ', $toJoin); + Log::debug(sprintf('SearchRuleEngine:: Search strict query for non-strict rule #%d ("%s") = %s', $rule->id, $rule->title, $searchQuery)); + + // build and run a search: + // build and run the search engine. + $searchEngine = app(SearchInterface::class); + $searchEngine->setUser($this->user); + $searchEngine->setPage(1); + $searchEngine->setLimit(31337); + $searchEngine->parseQuery($searchQuery); + + $result = $searchEngine->searchTransactions(); + $collection = $result->getCollection(); + Log::debug(sprintf('Found in this run, %d transactions', $collection->count())); + $total = $total->merge($collection); + Log::debug(sprintf('Total collection is now %d transactions', $total->count())); + $count++; + } + Log::debug(sprintf('Total collection is now %d transactions', $total->count())); + Log::debug(sprintf('Done running %d trigger(s)', $count)); + + // make collection unique + $unique = $total->unique(function (array $group) { + $str = ''; + foreach ($group['transactions'] as $transaction) { + $str = sprintf('%s%d', $str, $transaction['transaction_journal_id']); + } + $key = sprintf('%d%s', $group['id'], $str); + Log::debug(sprintf('Return key: %s ', $key)); + return $key; + }); + + Log::debug(sprintf('SearchRuleEngine:: Found %d transactions using search engine.', $unique->count())); + + $this->processResults($rule, $unique); + Log::debug(sprintf('SearchRuleEngine:: done processing non-strict rule #%d', $rule->id)); + } } \ No newline at end of file diff --git a/config/firefly.php b/config/firefly.php index 5a321f64c5..c2484554df 100644 --- a/config/firefly.php +++ b/config/firefly.php @@ -503,6 +503,9 @@ return [ 'no_notes' => ['alias' => false, 'trigger_class' => NotesEmpty::class, 'needs_context' => false,], 'any_notes' => ['alias' => false, 'trigger_class' => NotesAny::class, 'needs_context' => false,], + // one exact (or array of) journals: + 'journal_id' => ['alias' => false, 'trigger_class' => null, 'needs_context' => true,], + // exact amount 'amount_exactly' => ['alias' => false, 'trigger_class' => AmountExactly::class, 'needs_context' => true,], 'amount_is' => ['alias' => true, 'alias_for' => 'amount_exactly', 'needs_context' => true,], diff --git a/tests/Unit/Support/Search/OperatorQuerySearchTest.php b/tests/Unit/Support/Search/OperatorQuerySearchTest.php index 535ebb9453..44fb33ff71 100644 --- a/tests/Unit/Support/Search/OperatorQuerySearchTest.php +++ b/tests/Unit/Support/Search/OperatorQuerySearchTest.php @@ -100,6 +100,9 @@ class OperatorQuerySearchTest extends TestCase 'destination_account_id' => '1', 'to_account_id' => '1', + // journal test + 'journal_id' => '1', + // destination account nr 'to_account_nr_starts' => 'test', 'destination_account_nr_starts' => 'test', @@ -201,9 +204,6 @@ class OperatorQuerySearchTest extends TestCase } $this->assertTrue(true); } - - - //$groups = $object->searchTransactions(); } /** @@ -659,6 +659,40 @@ class OperatorQuerySearchTest extends TestCase $this->assertCount(1, $result); } + /** + * @covers \FireflyIII\Support\Search\OperatorQuerySearch + */ + public function testJournalId(): void + { + $this->be($this->user()); + + $object = new OperatorQuerySearch; + $object->setUser($this->user()); + $object->setPage(1); + $query = 'journal_id:1,2'; + Log::debug(sprintf('Trying to parse query "%s"', $query)); + try { + $object->parseQuery($query); + } catch (FireflyException $e) { + $this->assertTrue(false, $e->getMessage()); + } + $this->assertCount(0, $object->getWords()); + + // operator is assumed to be included. + $this->assertCount(1, $object->getOperators()); + + $result = ['transactions' => []]; + // execute search should work: + try { + $result = $object->searchTransactions(); + } catch (FireflyException $e) { + $this->assertTrue(false, $e->getMessage()); + } + + // two results + $this->assertCount(2, $result); + } + /** * @covers \FireflyIII\Support\Search\OperatorQuerySearch