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
2023-12-10 06:57:41 +01:00
protected $signature
2024-01-01 14:43:56 +01:00
= '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}' ;
2020-08-23 16:37:08 +02:00
private array $acceptedAccounts ;
private Collection $accounts ;
private bool $allRules ;
private Carbon $endDate ;
private Collection $groups ;
private RuleGroupRepositoryInterface $ruleGroupRepository ;
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
{
2024-01-01 14:43:56 +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 ());
2024-01-01 14:43:56 +01:00
$result = $this -> verifyInput ();
2019-05-29 18:28:28 +02:00
if ( false === $result ) {
return 1 ;
}
2024-01-01 14:43:56 +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:
2024-01-01 14:43:56 +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 */
2024-01-01 14:43:56 +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
}
2024-01-01 14:43:56 +01:00
$list = implode ( ',' , $filterAccountList );
2020-08-23 17:00:47 +02:00
$ruleEngine -> addOperator ([ 'type' => 'account_id' , 'value' => $list ]);
// add the date as a filter:
2025-01-03 14:56:06 +01:00
$ruleEngine -> addOperator ([ 'type' => 'date_after' , 'value' => $this -> startDate -> format ( 'Y-m-d' )]);
2020-08-23 17:00:47 +02:00
$ruleEngine -> addOperator ([ 'type' => 'date_before' , 'value' => $this -> endDate -> format ( 'Y-m-d' )]);
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 ( '' );
2024-01-01 14:43:56 +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 ;
}
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
{
$this -> allRules = false ;
2022-10-30 12:23:16 +01:00
$this -> accounts = new Collection ();
2019-06-13 15:48:35 +02:00
$this -> ruleSelection = [];
$this -> ruleGroupSelection = [];
$this -> ruleRepository = app ( RuleRepositoryInterface :: class );
$this -> ruleGroupRepository = app ( RuleGroupRepositoryInterface :: class );
2025-01-03 09:15:52 +01:00
$this -> acceptedAccounts = [ AccountTypeEnum :: DEFAULT -> value , AccountTypeEnum :: DEBT -> value , AccountTypeEnum :: ASSET -> value , AccountTypeEnum :: LOAN -> value , AccountTypeEnum :: MORTGAGE -> value ];
2022-10-30 12:23:16 +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
{
2024-01-01 14:43:56 +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 ;
}
2024-01-01 14:43:56 +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 ) {
2024-12-22 08:43:12 +01:00
$accountId = ( int ) $accountId ;
2021-06-30 06:17:38 +02:00
$account = $accountRepository -> find ( $accountId );
2019-05-29 18:28:28 +02:00
if ( null !== $account && in_array ( $account -> accountType -> type , $this -> acceptedAccounts , true )) {
$finalList -> push ( $account );
}
}
if ( 0 === $finalList -> count ()) {
2023-06-20 07:16:56 +02:00
$this -> friendlyError ( 'Please make sure all accounts in --accounts are asset accounts or liabilities.' );
2019-05-29 18:28:28 +02:00
return false ;
}
2024-01-01 14:43:56 +01:00
$this -> accounts = $finalList ;
2019-05-29 18:28:28 +02:00
return true ;
}
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 ;
}
2024-01-01 14:43:56 +01:00
$ruleGroupList = explode ( ',' , $ruleGroupString );
2023-06-21 12:34:58 +02:00
foreach ( $ruleGroupList as $ruleGroupId ) {
2024-12-22 08:43:12 +01:00
$ruleGroup = $this -> ruleGroupRepository -> find (( int ) $ruleGroupId );
2025-01-03 14:56:06 +01:00
if ( true === $ruleGroup -> active ) {
2023-06-21 12:34:58 +02:00
$this -> ruleGroupSelection [] = $ruleGroup -> id ;
}
if ( false === $ruleGroup -> active ) {
$this -> friendlyWarning ( sprintf ( 'Will ignore inactive rule group #%d ("%s")' , $ruleGroup -> id , $ruleGroup -> title ));
}
}
return true ;
}
private function verifyInputRules () : bool
{
$ruleString = $this -> option ( 'rules' );
if ( null === $ruleString || '' === $ruleString ) {
// can be empty.
return true ;
}
2024-01-01 14:43:56 +01:00
$ruleList = explode ( ',' , $ruleString );
2023-06-21 12:34:58 +02:00
foreach ( $ruleList as $ruleId ) {
2024-12-22 08:43:12 +01:00
$rule = $this -> ruleRepository -> find (( int ) $ruleId );
2025-05-27 17:02:18 +02:00
if ( $rule instanceof Rule && true === $rule -> active ) {
2023-06-21 12:34:58 +02:00
$this -> ruleSelection [] = $rule -> id ;
}
}
return true ;
}
2021-03-12 06:30:40 +01:00
/**
* @throws FireflyException
*/
private function verifyInputDates () : void
{
// parse start date.
2025-01-04 07:31:25 +01:00
$inputStart = today ( config ( 'app.timezone' )) -> startOfMonth ();
$startString = $this -> option ( 'start_date' );
2021-03-12 06:30:40 +01:00
if ( null === $startString ) {
/** @var JournalRepositoryInterface $repository */
$repository = app ( JournalRepositoryInterface :: class );
$repository -> setUser ( $this -> getUser ());
2024-01-01 14:43:56 +01:00
$first = $repository -> firstNull ();
2021-03-12 06:30:40 +01:00
if ( null !== $first ) {
$inputStart = $first -> date ;
}
}
if ( null !== $startString && '' !== $startString ) {
$inputStart = Carbon :: createFromFormat ( 'Y-m-d' , $startString );
}
// parse end date
2025-01-04 07:31:25 +01:00
$inputEnd = today ( config ( 'app.timezone' ));
$endString = $this -> option ( 'end_date' );
2021-03-12 06:30:40 +01:00
if ( null !== $endString && '' !== $endString ) {
$inputEnd = Carbon :: createFromFormat ( 'Y-m-d' , $endString );
}
2024-04-02 15:40:33 +02:00
if ( null === $inputEnd || null === $inputStart ) {
2023-11-26 12:10:42 +01:00
Log :: error ( 'Could not parse start or end date in verifyInputDate().' );
2023-12-20 19:35:52 +01:00
2023-11-26 12:10:42 +01:00
return ;
}
2021-03-12 06:30:40 +01:00
if ( $inputStart > $inputEnd ) {
[ $inputEnd , $inputStart ] = [ $inputStart , $inputEnd ];
}
2025-01-03 14:56:06 +01:00
$this -> startDate = $inputStart ;
2025-01-04 07:31:25 +01:00
$this -> endDate = $inputEnd ;
2021-03-12 06:30:40 +01:00
}
2023-06-21 12:34:58 +02:00
private function grabAllRules () : void
2021-03-12 06:30:40 +01:00
{
2023-06-21 12:34:58 +02:00
$this -> groups = $this -> ruleGroupRepository -> getActiveGroups ();
}
2021-03-12 06:30:40 +01:00
2023-06-21 12:34:58 +02:00
private function getRulesToApply () : Collection
{
$rulesToApply = new Collection ();
2023-12-20 19:35:52 +01:00
2023-06-21 12:34:58 +02:00
/** @var RuleGroup $group */
foreach ( $this -> groups as $group ) {
$rules = $this -> ruleGroupRepository -> getActiveStoreRules ( $group );
2023-12-20 19:35:52 +01:00
2023-06-21 12:34:58 +02:00
/** @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 ( true === $test ) {
2023-10-29 06:33:43 +01:00
app ( 'log' ) -> debug ( sprintf ( 'Will include rule #%d "%s"' , $rule -> id , $rule -> title ));
2023-06-21 12:34:58 +02:00
$rulesToApply -> push ( $rule );
}
2021-03-12 06:30:40 +01:00
}
}
2023-06-21 12:34:58 +02:00
return $rulesToApply ;
2021-03-12 06:30:40 +01:00
}
2023-06-21 12:34:58 +02:00
private function includeRule ( Rule $rule , RuleGroup $group ) : bool
2021-03-12 06:30:40 +01:00
{
2023-06-21 12:34:58 +02:00
return in_array ( $group -> id , $this -> ruleGroupSelection , true )
|| in_array ( $rule -> id , $this -> ruleSelection , true )
|| $this -> allRules ;
2021-03-12 06:30:40 +01:00
}
2019-05-29 18:28:28 +02:00
}