mirror of
https://github.com/firefly-iii/firefly-iii.git
synced 2025-02-25 18:45:27 -06:00
Code and routes for rules.
This commit is contained in:
parent
9a2e5c36a1
commit
8e4092e7d7
@ -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.
|
||||
*
|
||||
|
@ -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');
|
||||
|
@ -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')),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
@ -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.
|
||||
*
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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) {
|
||||
|
@ -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']);
|
||||
}
|
||||
);
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user