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;
use Carbon\Carbon;
use FireflyIII\Api\V1\Requests\RuleRequest;
use FireflyIII\Exceptions\FireflyException;
use FireflyIII\Jobs\ExecuteRuleOnExistingTransactions;
use FireflyIII\Models\AccountType;
use FireflyIII\Models\Rule;
use FireflyIII\Repositories\Account\AccountRepositoryInterface;
use FireflyIII\Repositories\Journal\JournalRepositoryInterface;
use FireflyIII\Repositories\Rule\RuleRepositoryInterface;
use FireflyIII\TransactionRules\TransactionMatcher;
use FireflyIII\Transformers\RuleTransformer;
use FireflyIII\Transformers\TransactionTransformer;
use FireflyIII\User;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Pagination\LengthAwarePaginator;
use Illuminate\Support\Collection;
use League\Fractal\Manager;
use League\Fractal\Pagination\IlluminatePaginatorAdapter;
use League\Fractal\Resource\Collection as FractalCollection;
use League\Fractal\Resource\Item;
use League\Fractal\Serializer\JsonApiSerializer;
use Log;
/**
* Class RuleController
*/
class RuleController extends Controller
{
/** @var AccountRepositoryInterface Account repository */
private $accountRepository;
/** @var RuleRepositoryInterface The rule repository */
private $ruleRepository;
@ -59,6 +71,9 @@ class RuleController extends Controller
$this->ruleRepository = app(RuleRepositoryInterface::class);
$this->ruleRepository->setUser($user);
$this->accountRepository = app(AccountRepositoryInterface::class);
$this->accountRepository->setUser($user);
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');
}
/**
* @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.
*

View File

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

View File

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

View File

@ -301,9 +301,9 @@ class RuleRepository implements RuleRepositoryInterface
$rule->rule_group_id = $data['rule_group_id'];
$rule->order = ($order + 1);
$rule->active = true;
$rule->strict = $data['strict'] ?? false;
$rule->stop_processing = 1 === (int)$data['stop_processing'];
$rule->active = $data['active'];
$rule->strict = $data['strict'];
$rule->stop_processing = $data['stop_processing'];
$rule->title = $data['title'];
$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
{
$order = 1;
foreach ($data['rule_actions'] as $action) {
foreach ($data['actions'] as $action) {
$value = $action['value'] ?? '';
$stopProcessing = $action['stop_processing'] ?? false;
@ -435,7 +435,7 @@ class RuleRepository implements RuleRepositoryInterface
];
$this->storeTrigger($rule, $triggerValues);
foreach ($data['rule_triggers'] as $trigger) {
foreach ($data['triggers'] as $trigger) {
$value = $trigger['value'] ?? '';
$stopProcessing = $trigger['stop_processing'] ?? false;

View File

@ -257,7 +257,7 @@ class FireflyValidator extends Validator
$index = (int)($parts[1] ?? '0');
// 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 ('invalid' === $actionType) {
@ -320,7 +320,7 @@ class FireflyValidator extends Validator
$index = (int)($parts[1] ?? '0');
// 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:
if ('invalid' === $triggerType) {

View File

@ -216,6 +216,8 @@ Route::group(
Route::get('{rule}', ['uses' => 'RuleController@show', 'as' => 'show']);
Route::put('{rule}', ['uses' => 'RuleController@update', 'as' => 'update']);
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::put('{ruleGroup}', ['uses' => 'RuleGroupController@update', 'as' => 'update']);
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']);
}
);