Code and routes for rules.

This commit is contained in:
James Cole 2018-12-07 16:03:05 +01:00
parent 9a2e5c36a1
commit 8e4092e7d7
No known key found for this signature in database
GPG Key ID: C16961E655E74B5E
7 changed files with 206 additions and 41 deletions

View File

@ -23,25 +23,37 @@ declare(strict_types=1);
namespace FireflyIII\Api\V1\Controllers; namespace FireflyIII\Api\V1\Controllers;
use Carbon\Carbon;
use FireflyIII\Api\V1\Requests\RuleRequest; use FireflyIII\Api\V1\Requests\RuleRequest;
use FireflyIII\Exceptions\FireflyException;
use FireflyIII\Jobs\ExecuteRuleOnExistingTransactions;
use FireflyIII\Models\AccountType;
use FireflyIII\Models\Rule; use FireflyIII\Models\Rule;
use FireflyIII\Repositories\Account\AccountRepositoryInterface;
use FireflyIII\Repositories\Journal\JournalRepositoryInterface;
use FireflyIII\Repositories\Rule\RuleRepositoryInterface; use FireflyIII\Repositories\Rule\RuleRepositoryInterface;
use FireflyIII\TransactionRules\TransactionMatcher;
use FireflyIII\Transformers\RuleTransformer; use FireflyIII\Transformers\RuleTransformer;
use FireflyIII\Transformers\TransactionTransformer;
use FireflyIII\User; use FireflyIII\User;
use Illuminate\Http\JsonResponse; use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Pagination\LengthAwarePaginator; use Illuminate\Pagination\LengthAwarePaginator;
use Illuminate\Support\Collection;
use League\Fractal\Manager; use League\Fractal\Manager;
use League\Fractal\Pagination\IlluminatePaginatorAdapter; use League\Fractal\Pagination\IlluminatePaginatorAdapter;
use League\Fractal\Resource\Collection as FractalCollection; use League\Fractal\Resource\Collection as FractalCollection;
use League\Fractal\Resource\Item; use League\Fractal\Resource\Item;
use League\Fractal\Serializer\JsonApiSerializer; use League\Fractal\Serializer\JsonApiSerializer;
use Log;
/** /**
* Class RuleController * Class RuleController
*/ */
class RuleController extends Controller class RuleController extends Controller
{ {
/** @var AccountRepositoryInterface Account repository */
private $accountRepository;
/** @var RuleRepositoryInterface The rule repository */ /** @var RuleRepositoryInterface The rule repository */
private $ruleRepository; private $ruleRepository;
@ -59,6 +71,9 @@ class RuleController extends Controller
$this->ruleRepository = app(RuleRepositoryInterface::class); $this->ruleRepository = app(RuleRepositoryInterface::class);
$this->ruleRepository->setUser($user); $this->ruleRepository->setUser($user);
$this->accountRepository = app(AccountRepositoryInterface::class);
$this->accountRepository->setUser($user);
return $next($request); return $next($request);
} }
); );
@ -155,6 +170,115 @@ class RuleController extends Controller
return response()->json($manager->createData($resource)->toArray())->header('Content-Type', 'application/vnd.api+json'); return response()->json($manager->createData($resource)->toArray())->header('Content-Type', 'application/vnd.api+json');
} }
/**
* @param Request $request
* @param Rule $rule
*
* @return JsonResponse
* @throws FireflyException
*/
public function testRule(Request $request, Rule $rule): JsonResponse
{
$pageSize = (int)app('preferences')->getForUser(auth()->user(), 'listPageSize', 50)->data;
$page = 0 === (int)$request->query('page') ? 1 : (int)$request->query('page');
$startDate = null === $request->query('start_date') ? null : Carbon::createFromFormat('Y-m-d', $request->query('start_date'));
$endDate = null === $request->query('end_date') ? null : Carbon::createFromFormat('Y-m-d', $request->query('end_date'));
$searchLimit = 0 === (int)$request->query('search_limit') ? (int)config('firefly.test-triggers.limit') : (int)$request->query('search_limit');
$triggerLimit = 0 === (int)$request->query('triggered_limit') ? (int)config('firefly.test-triggers.range') : (int)$request->query('triggered_limit');
$accountList = '' === (string)$request->query('accounts') ? [] : explode(',', $request->query('accounts'));
$accounts = new Collection;
foreach ($accountList as $accountId) {
Log::debug(sprintf('Searching for asset account with id "%s"', $accountId));
$account = $this->accountRepository->findNull((int)$accountId);
if (null !== $account && AccountType::ASSET === $account->accountType->type) {
Log::debug(sprintf('Found account #%d ("%s") and its an asset account', $account->id, $account->name));
$accounts->push($account);
}
if (null === $account) {
Log::debug(sprintf('No asset account with id "%s"', $accountId));
}
}
/** @var Rule $rule */
Log::debug(sprintf('Now testing rule #%d, "%s"', $rule->id, $rule->title));
/** @var TransactionMatcher $matcher */
$matcher = app(TransactionMatcher::class);
// set all parameters:
$matcher->setRule($rule);
$matcher->setStartDate($startDate);
$matcher->setEndDate($endDate);
$matcher->setSearchLimit($searchLimit);
$matcher->setTriggeredLimit($triggerLimit);
$matcher->setAccounts($accounts);
$matchingTransactions = $matcher->findTransactionsByRule();
$matchingTransactions = $matchingTransactions->unique('id');
// make paginator out of results.
$count = $matchingTransactions->count();
$transactions = $matchingTransactions->slice(($page - 1) * $pageSize, $pageSize);
// make paginator:
$paginator = new LengthAwarePaginator($transactions, $count, $pageSize, $this->parameters->get('page'));
$paginator->setPath(route('api.v1.rules.test', [$rule->id]) . $this->buildParams());
// resulting list is presented as JSON thing.
$manager = new Manager();
$baseUrl = $request->getSchemeAndHttpHost() . '/api/v1';
$manager->setSerializer(new JsonApiSerializer($baseUrl));
$repository = app(JournalRepositoryInterface::class);
$resource = new FractalCollection($matchingTransactions, new TransactionTransformer($this->parameters, $repository), 'transactions');
$resource->setPaginator(new IlluminatePaginatorAdapter($paginator));
return response()->json($manager->createData($resource)->toArray())->header('Content-Type', 'application/vnd.api+json');
}
/**
* Execute the given rule group on a set of existing transactions.
*
* @param Request $request
* @param Rule $rule
*
* @return JsonResponse
*/
public function triggerRule(Request $request, Rule $rule): JsonResponse
{
// Get parameters specified by the user
/** @var User $user */
$user = auth()->user();
$startDate = new Carbon($request->get('start_date'));
$endDate = new Carbon($request->get('end_date'));
$accountList = '' === (string)$request->query('accounts') ? [] : explode(',', $request->query('accounts'));
$accounts = new Collection;
foreach ($accountList as $accountId) {
Log::debug(sprintf('Searching for asset account with id "%s"', $accountId));
$account = $this->accountRepository->findNull((int)$accountId);
if (null !== $account && AccountType::ASSET === $account->accountType->type) {
Log::debug(sprintf('Found account #%d ("%s") and its an asset account', $account->id, $account->name));
$accounts->push($account);
}
if (null === $account) {
Log::debug(sprintf('No asset account with id "%s"', $accountId));
}
}
// Create a job to do the work asynchronously
$job = new ExecuteRuleOnExistingTransactions($rule);
// Apply parameters to the job
$job->setUser($user);
$job->setAccounts($accounts);
$job->setStartDate($startDate);
$job->setEndDate($endDate);
// Dispatch a new job to execute it in a queue
$this->dispatch($job);
return response()->json([], 204);
}
/** /**
* Update a rule. * Update a rule.
* *

View File

@ -51,9 +51,7 @@ class RuleGroupRequest extends Request
*/ */
public function getAll(): array public function getAll(): array
{ {
if (null === $this->get('active')) { $active = true;
$active = true;
}
if (null !== $this->get('active')) { if (null !== $this->get('active')) {
$active = $this->boolean('active'); $active = $this->boolean('active');

View File

@ -23,6 +23,7 @@ declare(strict_types=1);
namespace FireflyIII\Api\V1\Requests; namespace FireflyIII\Api\V1\Requests;
use FireflyIII\Rules\IsBoolean;
use Illuminate\Validation\Validator; use Illuminate\Validation\Validator;
@ -49,17 +50,30 @@ class RuleRequest extends Request
*/ */
public function getAll(): array public function getAll(): array
{ {
$strict = true;
$active = true;
$stopProcessing = false;
if (null !== $this->get('active')) {
$active = $this->boolean('active');
}
if (null !== $this->get('strict')) {
$strict = $this->boolean('strict');
}
if (null !== $this->get('stop_processing')) {
$stopProcessing = $this->boolean('stop_processing');
}
$data = [ $data = [
'title' => $this->string('title'), 'title' => $this->string('title'),
'description' => $this->string('description'), 'description' => $this->string('description'),
'rule_group_id' => $this->integer('rule_group_id'), 'rule_group_id' => $this->integer('rule_group_id'),
'rule_group_title' => $this->string('rule_group_title'), 'rule_group_title' => $this->string('rule_group_title'),
'trigger' => $this->string('trigger'), 'trigger' => $this->string('trigger'),
'strict' => $this->boolean('strict'), 'strict' => $strict,
'stop_processing' => $this->boolean('stop_processing'), 'stop_processing' => $stopProcessing,
'active' => $this->boolean('active'), 'active' => $active,
'rule_triggers' => $this->getRuleTriggers(), 'triggers' => $this->getRuleTriggers(),
'rule_actions' => $this->getRuleActions(), 'actions' => $this->getRuleActions(),
]; ];
return $data; return $data;
@ -81,20 +95,20 @@ class RuleRequest extends Request
$rules = [ $rules = [
'title' => 'required|between:1,100|uniqueObjectForUser:rules,title', 'title' => 'required|between:1,100|uniqueObjectForUser:rules,title',
'description' => 'between:1,5000|nullable', 'description' => 'between:1,5000|nullable',
'rule_group_id' => 'required|belongsToUser:rule_groups|required_without:rule_group_title', 'rule_group_id' => 'required|belongsToUser:rule_groups|required_without:rule_group_title',
'rule_group_title' => 'nullable|between:1,255|required_without:rule_group_id|belongsToUser:rule_groups,title', 'rule_group_title' => 'nullable|between:1,255|required_without:rule_group_id|belongsToUser:rule_groups,title',
'trigger' => 'required|in:store-journal,update-journal', 'trigger' => 'required|in:store-journal,update-journal',
'rule_triggers.*.name' => 'required|in:' . implode(',', $validTriggers), 'triggers.*.name' => 'required|in:' . implode(',', $validTriggers),
'rule_triggers.*.stop_processing' => 'boolean', 'triggers.*.stop_processing' => [new IsBoolean],
'rule_triggers.*.value' => 'required_if:rule_actions.*.type,' . $contextTriggers . '|min:1|ruleTriggerValue', 'triggers.*.value' => 'required_if:rule_actions.*.type,' . $contextTriggers . '|min:1|ruleTriggerValue',
'rule_actions.*.name' => 'required|in:' . implode(',', $validActions), 'actions.*.name' => 'required|in:' . implode(',', $validActions),
'rule_actions.*.value' => 'required_if:rule_actions.*.type,' . $contextActions . '|ruleActionValue', 'actions.*.value' => 'required_if:rule_actions.*.type,' . $contextActions . '|ruleActionValue',
'rule_actions.*.stop_processing' => 'boolean', 'actions.*.stop_processing' => [new IsBoolean],
'strict' => 'required|boolean', 'strict' => [new IsBoolean],
'stop_processing' => 'required|boolean', 'stop_processing' => [new IsBoolean],
'active' => 'required|boolean', 'active' => [new IsBoolean],
]; ];
return $rules; return $rules;
@ -124,10 +138,10 @@ class RuleRequest extends Request
*/ */
protected function atLeastOneAction(Validator $validator): void protected function atLeastOneAction(Validator $validator): void
{ {
$data = $validator->getData(); $data = $validator->getData();
$repetitions = $data['rule_actions'] ?? []; $actions = $data['actions'] ?? [];
// need at least one transaction // need at least one trigger
if (0 === \count($repetitions)) { if (0 === \count($actions)) {
$validator->errors()->add('title', (string)trans('validation.at_least_one_action')); $validator->errors()->add('title', (string)trans('validation.at_least_one_action'));
} }
} }
@ -139,10 +153,10 @@ class RuleRequest extends Request
*/ */
protected function atLeastOneTrigger(Validator $validator): void protected function atLeastOneTrigger(Validator $validator): void
{ {
$data = $validator->getData(); $data = $validator->getData();
$repetitions = $data['rule_triggers'] ?? []; $triggers = $data['triggers'] ?? [];
// need at least one transaction // need at least one trugger
if (0 === \count($repetitions)) { if (0 === \count($triggers)) {
$validator->errors()->add('title', (string)trans('validation.at_least_one_trigger')); $validator->errors()->add('title', (string)trans('validation.at_least_one_trigger'));
} }
} }
@ -152,14 +166,14 @@ class RuleRequest extends Request
*/ */
private function getRuleActions(): array private function getRuleActions(): array
{ {
$actions = $this->get('rule_actions'); $actions = $this->get('actions');
$return = []; $return = [];
if (\is_array($actions)) { if (\is_array($actions)) {
foreach ($actions as $action) { foreach ($actions as $action) {
$return[] = [ $return[] = [
'name' => $action['name'], 'name' => $action['name'],
'value' => $action['value'], 'value' => $action['value'],
'stop_processing' => 1 === (int)($action['stop-processing'] ?? '0'), 'stop_processing' => $this->convertBoolean((string)($action['stop_processing'] ?? 'false')),
]; ];
} }
} }
@ -172,14 +186,14 @@ class RuleRequest extends Request
*/ */
private function getRuleTriggers(): array private function getRuleTriggers(): array
{ {
$triggers = $this->get('rule_triggers'); $triggers = $this->get('triggers');
$return = []; $return = [];
if (\is_array($triggers)) { if (\is_array($triggers)) {
foreach ($triggers as $trigger) { foreach ($triggers as $trigger) {
$return[] = [ $return[] = [
'name' => $trigger['name'], 'name' => $trigger['name'],
'value' => $trigger['value'], 'value' => $trigger['value'],
'stop_processing' => 1 === (int)($trigger['stop-processing'] ?? '0'), 'stop_processing' => $this->convertBoolean((string)($trigger['stop_processing'] ?? 'false')),
]; ];
} }
} }

View File

@ -27,6 +27,7 @@ use Illuminate\Foundation\Http\FormRequest;
/** /**
* Class Request. * Class Request.
*
* @codeCoverageIgnore * @codeCoverageIgnore
* *
* @SuppressWarnings(PHPMD.NumberOfChildren) * @SuppressWarnings(PHPMD.NumberOfChildren)
@ -52,6 +53,29 @@ class Request extends FormRequest
return 1 === (int)$this->input($field); return 1 === (int)$this->input($field);
} }
/**
* @param string $value
*
* @return bool
*/
public function convertBoolean(string $value): bool
{
if ('true' === $value) {
return true;
}
if (1 === $value) {
return true;
}
if ('1' === $value) {
return true;
}
if (true === $value) {
return true;
}
return false;
}
/** /**
* Return floating value. * Return floating value.
* *

View File

@ -301,9 +301,9 @@ class RuleRepository implements RuleRepositoryInterface
$rule->rule_group_id = $data['rule_group_id']; $rule->rule_group_id = $data['rule_group_id'];
$rule->order = ($order + 1); $rule->order = ($order + 1);
$rule->active = true; $rule->active = $data['active'];
$rule->strict = $data['strict'] ?? false; $rule->strict = $data['strict'];
$rule->stop_processing = 1 === (int)$data['stop_processing']; $rule->stop_processing = $data['stop_processing'];
$rule->title = $data['title']; $rule->title = $data['title'];
$rule->description = \strlen($data['description']) > 0 ? $data['description'] : null; $rule->description = \strlen($data['description']) > 0 ? $data['description'] : null;
@ -399,7 +399,7 @@ class RuleRepository implements RuleRepositoryInterface
private function storeActions(Rule $rule, array $data): bool private function storeActions(Rule $rule, array $data): bool
{ {
$order = 1; $order = 1;
foreach ($data['rule_actions'] as $action) { foreach ($data['actions'] as $action) {
$value = $action['value'] ?? ''; $value = $action['value'] ?? '';
$stopProcessing = $action['stop_processing'] ?? false; $stopProcessing = $action['stop_processing'] ?? false;
@ -435,7 +435,7 @@ class RuleRepository implements RuleRepositoryInterface
]; ];
$this->storeTrigger($rule, $triggerValues); $this->storeTrigger($rule, $triggerValues);
foreach ($data['rule_triggers'] as $trigger) { foreach ($data['triggers'] as $trigger) {
$value = $trigger['value'] ?? ''; $value = $trigger['value'] ?? '';
$stopProcessing = $trigger['stop_processing'] ?? false; $stopProcessing = $trigger['stop_processing'] ?? false;

View File

@ -257,7 +257,7 @@ class FireflyValidator extends Validator
$index = (int)($parts[1] ?? '0'); $index = (int)($parts[1] ?? '0');
// get the name of the trigger from the data array: // get the name of the trigger from the data array:
$actionType = $this->data['rule_actions'][$index]['name'] ?? 'invalid'; $actionType = $this->data['actions'][$index]['name'] ?? 'invalid';
// if it's "invalid" return false. // if it's "invalid" return false.
if ('invalid' === $actionType) { if ('invalid' === $actionType) {
@ -320,7 +320,7 @@ class FireflyValidator extends Validator
$index = (int)($parts[1] ?? '0'); $index = (int)($parts[1] ?? '0');
// get the name of the trigger from the data array: // get the name of the trigger from the data array:
$triggerType = $this->data['rule_triggers'][$index]['name'] ?? 'invalid'; $triggerType = $this->data['triggers'][$index]['name'] ?? 'invalid';
// invalid always returns false: // invalid always returns false:
if ('invalid' === $triggerType) { if ('invalid' === $triggerType) {

View File

@ -216,6 +216,8 @@ Route::group(
Route::get('{rule}', ['uses' => 'RuleController@show', 'as' => 'show']); Route::get('{rule}', ['uses' => 'RuleController@show', 'as' => 'show']);
Route::put('{rule}', ['uses' => 'RuleController@update', 'as' => 'update']); Route::put('{rule}', ['uses' => 'RuleController@update', 'as' => 'update']);
Route::delete('{rule}', ['uses' => 'RuleController@delete', 'as' => 'delete']); Route::delete('{rule}', ['uses' => 'RuleController@delete', 'as' => 'delete']);
Route::get('{rule}/test', ['uses' => 'RuleController@testRule', 'as' => 'test']);
Route::post('{rule}/trigger', ['uses' => 'RuleController@triggerRule', 'as' => 'trigger']);
} }
); );
@ -229,6 +231,9 @@ Route::group(
Route::get('{ruleGroup}', ['uses' => 'RuleGroupController@show', 'as' => 'show']); Route::get('{ruleGroup}', ['uses' => 'RuleGroupController@show', 'as' => 'show']);
Route::put('{ruleGroup}', ['uses' => 'RuleGroupController@update', 'as' => 'update']); Route::put('{ruleGroup}', ['uses' => 'RuleGroupController@update', 'as' => 'update']);
Route::delete('{ruleGroup}', ['uses' => 'RuleGroupController@delete', 'as' => 'delete']); Route::delete('{ruleGroup}', ['uses' => 'RuleGroupController@delete', 'as' => 'delete']);
Route::get('{ruleGroup}/test', ['uses' => 'RuleGroupController@testGroup', 'as' => 'test']);
Route::get('{ruleGroup}/rules', ['uses' => 'RuleGroupController@rules', 'as' => 'rules']);
Route::post('{ruleGroup}/trigger', ['uses' => 'RuleGroupController@triggerGroup', 'as' => 'trigger']);
} }
); );