2019-05-29 18:28:28 +02:00
< ? php
2024-11-25 04:18:55 +01:00
2024-03-20 17:48:13 +01:00
/*
2019-10-02 06:37:26 +02:00
* ApplyRules.php
2024-03-20 17:48:13 +01:00
* Copyright (c) 2024 james@firefly-iii.org.
2019-10-02 06:37:26 +02:00
*
* This file is part of Firefly III (https://github.com/firefly-iii).
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
2024-03-20 17:48:13 +01:00
* along with this program. If not, see https://www.gnu.org/licenses/.
2019-10-02 06:37:26 +02:00
*/
2019-08-17 12:09:03 +02:00
declare ( strict_types = 1 );
2019-05-29 18:28:28 +02:00
namespace FireflyIII\Console\Commands\Tools ;
use Carbon\Carbon ;
2023-06-20 07:16:56 +02:00
use FireflyIII\Console\Commands\ShowsFriendlyMessages ;
2019-05-29 18:28:28 +02:00
use FireflyIII\Console\Commands\VerifiesAccessToken ;
2025-01-03 09:15:52 +01:00
use FireflyIII\Enums\AccountTypeEnum ;
2019-05-29 18:28:28 +02:00
use FireflyIII\Exceptions\FireflyException ;
use FireflyIII\Models\Rule ;
use FireflyIII\Models\RuleGroup ;
use FireflyIII\Repositories\Account\AccountRepositoryInterface ;
use FireflyIII\Repositories\Journal\JournalRepositoryInterface ;
use FireflyIII\Repositories\Rule\RuleRepositoryInterface ;
use FireflyIII\Repositories\RuleGroup\RuleGroupRepositoryInterface ;
2020-08-23 16:37:08 +02:00
use FireflyIII\TransactionRules\Engine\RuleEngineInterface ;
2019-05-29 18:28:28 +02:00
use Illuminate\Console\Command ;
use Illuminate\Support\Collection ;
2023-11-26 12:10:42 +01:00
use Illuminate\Support\Facades\Log ;
2019-05-29 18:28:28 +02:00
class ApplyRules extends Command
{
2023-06-20 07:16:56 +02:00
use ShowsFriendlyMessages ;
2019-05-29 18:28:28 +02:00
use VerifiesAccessToken ;
protected $description = 'This command will apply your rules and rule groups on a selection of your transactions.' ;
2023-11-05 09:54:53 +01:00
2026-01-23 15:14:29 +01:00
protected $signature = 'firefly-iii:apply-rules
2020-06-06 21:23:26 +02:00
{--user=1 : The user ID.}
2019-05-29 18:28:28 +02:00
{--token= : The user\'s access token.}
{--accounts= : A comma-separated list of asset accounts or liabilities to apply your rules to.}
{--rule_groups= : A comma-separated list of rule groups to apply. Take the ID\'s of these rule groups from the Firefly III interface.}
{--rules= : A comma-separated list of rules to apply. Take the ID\'s of these rules from the Firefly III interface. Using this option overrules the option that selects rule groups.}
{--all_rules : If set, will overrule both settings and simply apply ALL of your rules.}
{--start_date= : The date of the earliest transaction to be included (inclusive). If omitted, will be your very first transaction ever. Format: YYYY-MM-DD}
{--end_date= : The date of the latest transaction to be included (inclusive). If omitted, will be your latest transaction ever. Format: YYYY-MM-DD}' ;
2026-01-23 15:09:50 +01:00
private array $acceptedAccounts ;
private Collection $accounts ;
private bool $allRules ;
private Carbon $endDate ;
private Collection $groups ;
2020-08-23 16:37:08 +02:00
private RuleGroupRepositoryInterface $ruleGroupRepository ;
2026-01-23 15:09:50 +01:00
private array $ruleGroupSelection ;
private RuleRepositoryInterface $ruleRepository ;
private array $ruleSelection ;
private Carbon $startDate ;
2019-05-29 18:28:28 +02:00
/**
* Execute the console command.
*
2020-08-23 16:37:08 +02:00
* @throws FireflyException
2019-05-29 18:28:28 +02:00
*/
public function handle () : int
{
2026-01-23 15:14:29 +01:00
$start = microtime ( true );
2019-06-13 15:48:35 +02:00
$this -> stupidLaravel ();
2019-05-29 18:28:28 +02:00
if ( ! $this -> verifyAccessToken ()) {
2023-06-20 07:16:56 +02:00
$this -> friendlyError ( 'Invalid access token.' );
2019-05-29 18:28:28 +02:00
return 1 ;
}
2019-06-10 20:14:00 +02:00
2019-05-29 18:28:28 +02:00
// set user:
$this -> ruleRepository -> setUser ( $this -> getUser ());
$this -> ruleGroupRepository -> setUser ( $this -> getUser ());
2026-01-23 15:14:29 +01:00
$result = $this -> verifyInput ();
2019-05-29 18:28:28 +02:00
if ( false === $result ) {
return 1 ;
}
2026-01-23 15:14:29 +01:00
$this -> allRules = $this -> option ( 'all_rules' );
2019-05-29 18:28:28 +02:00
2019-06-07 17:57:46 +02:00
// always get all the rules of the user.
2019-05-29 18:28:28 +02:00
$this -> grabAllRules ();
// loop all groups and rules and indicate if they're included:
2026-01-23 15:14:29 +01:00
$rulesToApply = $this -> getRulesToApply ();
$count = $rulesToApply -> count ();
2019-05-29 18:28:28 +02:00
if ( 0 === $count ) {
2023-06-20 07:16:56 +02:00
$this -> friendlyError ( 'No rules or rule groups have been included.' );
$this -> friendlyWarning ( 'Make a selection using:' );
$this -> friendlyWarning ( ' --rules=1,2,...' );
$this -> friendlyWarning ( ' --rule_groups=1,2,...' );
$this -> friendlyWarning ( ' --all_rules' );
2020-03-21 15:43:41 +01:00
2020-03-21 15:42:37 +01:00
return 1 ;
2019-05-29 18:28:28 +02:00
}
2020-08-23 16:37:08 +02:00
// create new rule engine:
/** @var RuleEngineInterface $ruleEngine */
2026-01-23 15:14:29 +01:00
$ruleEngine = app ( RuleEngineInterface :: class );
2020-08-23 16:37:08 +02:00
$ruleEngine -> setRules ( $rulesToApply );
$ruleEngine -> setUser ( $this -> getUser ());
2019-05-29 18:28:28 +02:00
2020-08-23 17:00:47 +02:00
// add the accounts as filter:
2020-10-13 06:35:33 +02:00
$filterAccountList = [];
2021-03-12 06:30:40 +01:00
foreach ( $this -> accounts as $account ) {
2020-10-13 06:35:33 +02:00
$filterAccountList [] = $account -> id ;
2020-08-23 17:00:47 +02:00
}
2026-01-23 15:14:29 +01:00
$list = implode ( ',' , $filterAccountList );
2026-03-13 03:55:47 +01:00
$ruleEngine -> addOperator ([ 'type' => 'account_id' , 'value' => $list ]);
2020-08-23 17:00:47 +02:00
// add the date as a filter:
2026-03-13 03:55:47 +01:00
$ruleEngine -> addOperator ([ 'type' => 'date_after' , 'value' => $this -> startDate -> format ( 'Y-m-d' )]);
$ruleEngine -> addOperator ([ 'type' => 'date_before' , 'value' => $this -> endDate -> format ( 'Y-m-d' )]);
2020-08-23 17:00:47 +02:00
2019-05-29 18:28:28 +02:00
// start running rules.
2023-06-20 07:16:56 +02:00
$this -> friendlyLine ( sprintf ( 'Will apply %d rule(s) to your transaction(s).' , $count ));
2019-05-29 18:28:28 +02:00
2023-12-25 06:32:43 +01:00
// fire the rule(s)
2020-08-23 16:37:08 +02:00
$ruleEngine -> fire ();
2019-06-07 17:57:46 +02:00
2023-06-20 07:16:56 +02:00
$this -> friendlyLine ( '' );
2026-01-23 15:14:29 +01:00
$end = round ( microtime ( true ) - $start , 2 );
2023-06-20 07:16:56 +02:00
$this -> friendlyPositive ( sprintf ( 'Done in %s seconds!' , $end ));
2020-03-21 15:43:41 +01:00
2019-05-29 18:28:28 +02:00
return 0 ;
}
2026-02-06 13:55:17 +01:00
private function getRulesToApply () : Collection
{
Log :: debug ( 'getRulesToApply()' );
$rulesToApply = new Collection ();
/** @var RuleGroup $group */
foreach ( $this -> groups as $group ) {
Log :: debug ( sprintf ( 'Scanning rule group #%d' , $group -> id ));
$rules = $this -> ruleGroupRepository -> getActiveStoreRules ( $group );
/** @var Rule $rule */
foreach ( $rules as $rule ) {
// if in rule selection, or group in selection or all rules, it's included.
$test = $this -> includeRule ( $rule , $group );
if ( $test ) {
Log :: debug ( sprintf ( 'Will include rule #%d "%s"' , $rule -> id , $rule -> title ));
$rulesToApply -> push ( $rule );
}
if ( ! $test ) {
Log :: debug ( sprintf ( 'Will not include rule #%d' , $rule -> id ));
}
}
}
Log :: debug ( sprintf ( 'Found %d rules to apply.' , $rulesToApply -> count ()));
return $rulesToApply ;
}
private function grabAllRules () : void
{
$this -> groups = $this -> ruleGroupRepository -> getActiveGroups ();
}
private function includeRule ( Rule $rule , RuleGroup $group ) : bool
{
return in_array (( int ) $group -> id , $this -> ruleGroupSelection , true ) || in_array (( int ) $rule -> id , $this -> ruleSelection , true ) || $this -> allRules ;
}
2019-06-13 15:48:35 +02:00
/**
* Laravel will execute ALL __construct() methods for ALL commands whenever a SINGLE command is
* executed. This leads to noticeable slow-downs and class calls. To prevent this, this method should
* be called from the handle method instead of using the constructor to initialize the command.
*/
private function stupidLaravel () : void
{
2026-01-23 15:14:29 +01:00
$this -> allRules = false ;
$this -> accounts = new Collection ();
$this -> ruleSelection = [];
$this -> ruleGroupSelection = [];
$this -> ruleRepository = app ( RuleRepositoryInterface :: class );
2019-06-13 15:48:35 +02:00
$this -> ruleGroupRepository = app ( RuleGroupRepositoryInterface :: class );
2026-01-23 15:14:29 +01:00
$this -> acceptedAccounts = [
2026-01-23 15:09:50 +01:00
AccountTypeEnum :: DEFAULT -> value ,
AccountTypeEnum :: DEBT -> value ,
AccountTypeEnum :: ASSET -> value ,
AccountTypeEnum :: LOAN -> value ,
2026-01-23 15:14:29 +01:00
AccountTypeEnum :: MORTGAGE -> value ,
2026-01-23 15:09:50 +01:00
];
2026-01-23 15:14:29 +01:00
$this -> groups = new Collection ();
2019-06-13 15:48:35 +02:00
}
2019-05-29 18:28:28 +02:00
/**
2020-08-23 16:37:08 +02:00
* @throws FireflyException
2019-05-29 18:28:28 +02:00
*/
private function verifyInput () : bool
{
// verify account.
$result = $this -> verifyInputAccounts ();
if ( false === $result ) {
2021-09-19 08:28:01 +02:00
return false ;
2019-05-29 18:28:28 +02:00
}
// verify rule groups.
2019-06-10 20:14:00 +02:00
$this -> verifyInputRuleGroups ();
2019-05-29 18:28:28 +02:00
// verify rules.
2019-06-10 20:14:00 +02:00
$this -> verifyInputRules ();
2019-05-29 18:28:28 +02:00
$this -> verifyInputDates ();
return true ;
}
/**
2020-08-23 16:37:08 +02:00
* @throws FireflyException
2019-05-29 18:28:28 +02:00
*/
private function verifyInputAccounts () : bool
{
2026-01-23 15:14:29 +01:00
$accountString = $this -> option ( 'accounts' );
2019-05-29 18:28:28 +02:00
if ( null === $accountString || '' === $accountString ) {
2023-06-20 07:16:56 +02:00
$this -> friendlyError ( 'Please use the --accounts option to indicate the accounts to apply rules to.' );
2019-05-29 18:28:28 +02:00
return false ;
}
2026-01-23 15:14:29 +01:00
$finalList = new Collection ();
$accountList = explode ( ',' , $accountString );
2019-05-29 18:28:28 +02:00
/** @var AccountRepositoryInterface $accountRepository */
$accountRepository = app ( AccountRepositoryInterface :: class );
$accountRepository -> setUser ( $this -> getUser ());
foreach ( $accountList as $accountId ) {
2026-01-23 15:09:50 +01:00
$accountId = ( int ) $accountId ;
2026-01-10 17:42:51 +01:00
if ( 0 === $accountId ) {
$this -> friendlyWarning ( 'You provided an account with ID 0 (zero). It will be ignored.' );
2026-01-10 17:58:51 +01:00
2026-01-10 17:42:51 +01:00
continue ;
}
2026-01-23 15:14:29 +01:00
$account = $accountRepository -> find ( $accountId );
2026-01-10 17:42:51 +01:00
if ( null === $account ) {
$this -> friendlyWarning ( sprintf ( 'There is no account with ID #%d, it cannot be added.' , $accountId ));
2026-01-10 17:58:51 +01:00
2026-01-10 17:42:51 +01:00
continue ;
2019-05-29 18:28:28 +02:00
}
2026-01-23 15:14:29 +01:00
$type = $account -> accountType -> type ;
2026-01-10 17:42:51 +01:00
if ( ! in_array ( $account -> accountType -> type , $this -> acceptedAccounts , true )) {
$this -> friendlyWarning ( sprintf ( 'Account "%s" with ID #%d is of type "%s" and cannot be added.' , $account -> name , $accountId , $type ));
2026-01-10 17:58:51 +01:00
2026-01-10 17:42:51 +01:00
continue ;
}
$finalList -> push ( $account );
2019-05-29 18:28:28 +02:00
}
if ( 0 === $finalList -> count ()) {
2026-01-10 17:42:51 +01:00
$this -> friendlyError ( 'There are no accounts in the selection. Please make sure all accounts in --accounts are asset accounts or liabilities.' );
2019-05-29 18:28:28 +02:00
return false ;
}
2026-01-23 15:14:29 +01:00
$this -> accounts = $finalList ;
2019-05-29 18:28:28 +02:00
return true ;
}
2026-02-06 13:55:17 +01:00
/**
* @throws FireflyException
*/
private function verifyInputDates () : void
{
// parse start date.
$inputStart = today ( config ( 'app.timezone' )) -> startOfMonth ();
$startString = $this -> option ( 'start_date' );
if ( null === $startString ) {
/** @var JournalRepositoryInterface $repository */
$repository = app ( JournalRepositoryInterface :: class );
$repository -> setUser ( $this -> getUser ());
$first = $repository -> firstNull ();
if ( null !== $first ) {
$inputStart = $first -> date ;
}
}
if ( null !== $startString && '' !== $startString ) {
$inputStart = Carbon :: createFromFormat ( 'Y-m-d' , $startString );
}
// parse end date
$inputEnd = today ( config ( 'app.timezone' ));
$endString = $this -> option ( 'end_date' );
if ( null !== $endString && '' !== $endString ) {
$inputEnd = Carbon :: createFromFormat ( 'Y-m-d' , $endString );
}
if ( ! $inputEnd instanceof Carbon || null === $inputStart ) {
Log :: error ( 'Could not parse start or end date in verifyInputDate().' );
return ;
}
if ( $inputStart > $inputEnd ) {
[ $inputEnd , $inputStart ] = [ $inputStart , $inputEnd ];
}
$this -> startDate = $inputStart ;
$this -> endDate = $inputEnd ;
}
2023-06-21 12:34:58 +02:00
private function verifyInputRuleGroups () : bool
{
$ruleGroupString = $this -> option ( 'rule_groups' );
if ( null === $ruleGroupString || '' === $ruleGroupString ) {
// can be empty.
return true ;
}
2026-01-23 15:14:29 +01:00
$ruleGroupList = explode ( ',' , $ruleGroupString );
2023-06-21 12:34:58 +02:00
foreach ( $ruleGroupList as $ruleGroupId ) {
2026-01-23 15:14:29 +01:00
$ruleGroupId = ( int ) $ruleGroupId ;
2026-01-10 17:42:51 +01:00
if ( 0 === $ruleGroupId ) {
$this -> friendlyWarning ( 'You added a rule group with ID 0 (zero). It will be skipped.' );
2026-01-10 17:58:51 +01:00
2026-01-10 17:42:51 +01:00
continue ;
}
2026-01-23 15:14:29 +01:00
$ruleGroup = $this -> ruleGroupRepository -> find ( $ruleGroupId );
2026-01-10 17:42:51 +01:00
if ( null === $ruleGroup ) {
$this -> friendlyWarning ( sprintf ( 'There is no rule group with ID #%d, this ID will be ignored.' , $ruleGroupId ));
2026-01-10 17:58:51 +01:00
2026-01-10 17:42:51 +01:00
continue ;
2023-06-21 12:34:58 +02:00
}
if ( false === $ruleGroup -> active ) {
2026-01-10 17:42:51 +01:00
$this -> friendlyWarning ( sprintf ( 'Rule group with ID #%d is not active, so this ID will be ignored.' , $ruleGroupId ));
2026-01-10 17:58:51 +01:00
2026-01-10 17:42:51 +01:00
continue ;
2023-06-21 12:34:58 +02:00
}
2026-01-10 17:50:47 +01:00
$this -> ruleGroupSelection [] = $ruleGroupId ;
2023-06-21 12:34:58 +02:00
}
return true ;
}
private function verifyInputRules () : bool
{
$ruleString = $this -> option ( 'rules' );
if ( null === $ruleString || '' === $ruleString ) {
// can be empty.
return true ;
}
2026-01-23 15:14:29 +01:00
$ruleList = explode ( ',' , $ruleString );
2023-06-21 12:34:58 +02:00
foreach ( $ruleList as $ruleId ) {
2026-01-23 15:14:29 +01:00
$ruleId = ( int ) $ruleId ;
2026-01-10 17:42:51 +01:00
if ( 0 === $ruleId ) {
2026-01-10 17:38:03 +01:00
$this -> friendlyWarning ( 'You added a rule with ID 0 (zero). It will be skipped.' );
2026-01-10 17:58:51 +01:00
2026-01-10 17:38:03 +01:00
continue ;
}
2026-01-23 15:14:29 +01:00
$rule = $this -> ruleRepository -> find ( $ruleId );
2026-01-10 17:42:51 +01:00
if ( null === $rule ) {
2026-01-10 17:38:03 +01:00
$this -> friendlyWarning ( sprintf ( 'There is no rule with ID #%d, this ID will be ignored.' , $ruleId ));
2026-01-10 17:58:51 +01:00
2026-01-10 17:38:03 +01:00
continue ;
}
2026-01-10 17:42:51 +01:00
if ( false === $rule -> active ) {
2026-01-10 17:38:03 +01:00
$this -> friendlyWarning ( sprintf ( 'Rule with ID #%d is not active, so this ID will be ignored.' , $ruleId ));
2026-01-10 17:58:51 +01:00
2026-01-10 17:42:51 +01:00
continue ;
2026-01-10 17:38:03 +01:00
}
2026-01-10 17:50:47 +01:00
$this -> ruleSelection [] = $ruleId ;
2023-06-21 12:34:58 +02:00
}
return true ;
}
2019-05-29 18:28:28 +02:00
}