diff --git a/app/Http/Controllers/RuleController.php b/app/Http/Controllers/RuleController.php index b8feba57e4..3b15ca91f9 100644 --- a/app/Http/Controllers/RuleController.php +++ b/app/Http/Controllers/RuleController.php @@ -25,6 +25,7 @@ use Response; use Session; use URL; use View; +use FireflyIII\Rules\TransactionMatcher; /** * Class RuleController @@ -333,6 +334,74 @@ class RuleController extends Controller return redirect(session('rules.rule.edit.url')); } + /** + * @return \Illuminate\View\View + */ + public function testTriggers() { + // Create a list of triggers + $triggers = $this->getTriggerList(); + + // We start searching for transactions. For performance reasons, there are limits + // to the search: a maximum number of results and a maximum number of transactions + // to search in + // TODO: Make these values configurable + $maxResults = 50; + $maxTransactionsToSearchIn = 1000; + + // Dispatch the actual work to a matched object + $matchingTransactions = + (new TransactionMatcher($triggers)) + ->setTransactionLimit($maxTransactionsToSearchIn) + ->findMatchingTransactions($maxResults); + + // Warn the user if only a subset of transactions is returned + if(count( $matchingTransactions ) == $maxResults) { + $warning = trans('firefly.warning_transaction_subset', [ 'max_num_transactions' => $maxResults ] ); + } else if(count($matchingTransactions) == 0){ + $warning = trans('firefly.warning_no_matching_transactions', [ 'num_transactions' => $maxTransactionsToSearchIn ] ); + } else { + $warning = ""; + } + + // Return json response + $view = view('list.journals-tiny', [ 'transactions' => $matchingTransactions ])->render(); + + return Response::json(['html' => $view, 'warning' => $warning ]); + } + + /** + * Returns a list of triggers as provided in the URL + * @return array + */ + protected function getTriggerList() { + $triggers = []; + $order = 1; + $data = [ + 'rule-triggers' => Input::get('rule-trigger'), + 'rule-trigger-values' => Input::get('rule-trigger-value'), + 'rule-trigger-stop' => Input::get('rule-trigger-stop'), + ]; + + foreach ($data['rule-triggers'] as $index => $trigger) { + $value = $data['rule-trigger-values'][$index]; + $stopProcessing = isset($data['rule-trigger-stop'][$index]) ? true : false; + + // Create a new trigger object + $ruleTrigger = new RuleTrigger; + $ruleTrigger->order = $order; + $ruleTrigger->active = 1; + $ruleTrigger->stop_processing = $stopProcessing; + $ruleTrigger->trigger_type = $trigger; + $ruleTrigger->trigger_value = $value; + + // Store in list + $triggers[] = $ruleTrigger; + $order++; + } + + return $triggers; + } + private function createDefaultRule() { /** @var RuleRepositoryInterface $repository */ diff --git a/app/Http/routes.php b/app/Http/routes.php index 5e16686d9a..c930d03514 100644 --- a/app/Http/routes.php +++ b/app/Http/routes.php @@ -272,7 +272,8 @@ Route::group( Route::get('/rules/rules/down/{rule}', ['uses' => 'RuleController@down', 'as' => 'rules.rule.down']); Route::get('/rules/rules/edit/{rule}', ['uses' => 'RuleController@edit', 'as' => 'rules.rule.edit']); Route::get('/rules/rules/delete/{rule}', ['uses' => 'RuleController@delete', 'as' => 'rules.rule.delete']); - + Route::get('/rules/rules/test_triggers', ['uses' => 'RuleController@testTriggers', 'as' => 'rules.rule.test_triggers']); + // rules POST: Route::post('/rules/rules/trigger/reorder/{rule}', ['uses' => 'RuleController@reorderRuleTriggers']); Route::post('/rules/rules/action/reorder/{rule}', ['uses' => 'RuleController@reorderRuleActions']); diff --git a/app/Rules/TransactionMatcher.php b/app/Rules/TransactionMatcher.php new file mode 100644 index 0000000000..629cdaa112 --- /dev/null +++ b/app/Rules/TransactionMatcher.php @@ -0,0 +1,148 @@ +setTriggers($triggers); + } + + /** + * Find matching transactions for the current set of triggers + * @param number $maxResults The maximum number of transactions returned + */ + public function findMatchingTransactions($maxResults = 50) { + /** @var JournalRepositoryInterface $repository */ + $repository = app('FireflyIII\Repositories\Journal\JournalRepositoryInterface'); + + // We don't know the number of transaction to fetch from the database, in + // order to return the proper number of matching transactions. Since we don't want + // to fetch all transactions (as the first transactions already match, or the last + // transactions are irrelevant), we will fetch data in pages. + + // The optimal pagesize is somewhere between the maximum number of results to be returned + // and the maximum number of transactions to consider. + $pagesize = min($this->maxTransactionsToSearchIn / 2, $maxResults * 2); + + // Variables used within the loop + $numTransactionsProcessed = 0; + $page = 1; + $matchingTransactions = []; + + // Flags to indicate the end of the loop + $reachedEndOfList = false; + $foundEnoughTransactions = false; + $searchedEnoughTransactions = false; + + // Start a loop to fetch batches of transactions. The loop will finish if: + // - all transactions have been fetched from the database + // - the maximum number of transactions to return has been found + // - the maximum number of transactions to search in have been searched + do { + // Fetch a batch of transactions from the database + $offset = $page > 0 ? ($page - 1) * $pagesize : 0; + $transactions = $repository->getJournalsOfTypes( $this->transactionTypes, $offset, $page, $pagesize)->getCollection()->all(); + + // Filter transactions that match the rule + $matchingTransactions += array_filter( $transactions, function($transaction) { + $processor = new Processor(new Rule, $transaction); + return $processor->isTriggeredBy($this->triggers); + }); + + // Update counters + $page++; + $numTransactionsProcessed += count($transactions); + + // Check for conditions to finish the loop + $reachedEndOfList = (count($transactions) < $pagesize); + $foundEnoughTransactions = (count($matchingTransactions) >= $maxResults); + $searchedEnoughTransactions = ($numTransactionsProcessed >= $this->maxTransactionsToSearchIn); + } while( !$reachedEndOfList && !$foundEnoughTransactions && !$searchedEnoughTransactions); + + // If the list of matchingTransactions is larger than the maximum number of results + // (e.g. if a large percentage of the transactions match), truncate the list + $matchingTransactions = array_slice($matchingTransactions, 0, $maxResults); + + return $matchingTransactions; + } + + /** + * @return array + */ + public function getTriggers() { + return $this->triggers; + } + + /** + * @param array $triggers + */ + public function setTriggers($triggers) { + $this->triggers = $triggers; + return $this; + } + + /** + * @return array + */ + public function getTransactionLimit() { + return $this->maxTransactionsToSearchIn; + } + + /** + * @param int $limit + */ + public function setTransactionLimit(int $limit) { + $this->maxTransactionsToSearchIn = $limit; + return $this; + } + + /** + * @return array + */ + public function getTransactionTypes() { + return $this->transactionTypes; + } + + /** + * @param array $transactionTypes + */ + public function setTransactionTypes(array $transactionTypes) { + $this->transactionTypes = $transactionTypes; + return $this; + } + +} diff --git a/resources/views/rules/partials/test-trigger-modal.twig b/resources/views/rules/partials/test-trigger-modal.twig new file mode 100644 index 0000000000..8239d0d88f --- /dev/null +++ b/resources/views/rules/partials/test-trigger-modal.twig @@ -0,0 +1,22 @@ + +
{{ 'add_rule_trigger'|_ }}
+ {{ 'test_rule_triggers'|_ }}