1 | <?php |
||
2 | /** |
||
3 | * ApplyRules.php |
||
4 | * Copyright (c) 2020 [email protected] |
||
5 | * |
||
6 | * This file is part of Firefly III (https://github.com/firefly-iii). |
||
7 | * |
||
8 | * This program is free software: you can redistribute it and/or modify |
||
9 | * it under the terms of the GNU Affero General Public License as |
||
10 | * published by the Free Software Foundation, either version 3 of the |
||
11 | * License, or (at your option) any later version. |
||
12 | * |
||
13 | * This program is distributed in the hope that it will be useful, |
||
14 | * but WITHOUT ANY WARRANTY; without even the implied warranty of |
||
15 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
||
16 | * GNU Affero General Public License for more details. |
||
17 | * |
||
18 | * You should have received a copy of the GNU Affero General Public License |
||
19 | * along with this program. If not, see <https://www.gnu.org/licenses/>. |
||
20 | */ |
||
21 | |||
22 | declare(strict_types=1); |
||
23 | |||
24 | namespace FireflyIII\Console\Commands\Tools; |
||
25 | |||
26 | |||
27 | use Carbon\Carbon; |
||
28 | use FireflyIII\Console\Commands\VerifiesAccessToken; |
||
29 | use FireflyIII\Exceptions\FireflyException; |
||
30 | use FireflyIII\Helpers\Collector\GroupCollectorInterface; |
||
31 | use FireflyIII\Models\AccountType; |
||
32 | use FireflyIII\Models\Rule; |
||
33 | use FireflyIII\Models\RuleGroup; |
||
34 | use FireflyIII\Repositories\Account\AccountRepositoryInterface; |
||
35 | use FireflyIII\Repositories\Journal\JournalRepositoryInterface; |
||
36 | use FireflyIII\Repositories\Rule\RuleRepositoryInterface; |
||
37 | use FireflyIII\Repositories\RuleGroup\RuleGroupRepositoryInterface; |
||
38 | use FireflyIII\TransactionRules\Engine\RuleEngine; |
||
39 | use Illuminate\Console\Command; |
||
40 | use Illuminate\Support\Collection; |
||
41 | use Log; |
||
42 | |||
43 | /** |
||
44 | * Class ApplyRules |
||
45 | */ |
||
46 | class ApplyRules extends Command |
||
47 | { |
||
48 | use VerifiesAccessToken; |
||
49 | |||
50 | /** |
||
51 | * The console command description. |
||
52 | * |
||
53 | * @var string |
||
54 | */ |
||
55 | protected $description = 'This command will apply your rules and rule groups on a selection of your transactions.'; |
||
56 | /** |
||
57 | * The name and signature of the console command. |
||
58 | * |
||
59 | * @var string |
||
60 | */ |
||
61 | protected $signature |
||
62 | = 'firefly-iii:apply-rules |
||
63 | {--user=1 : The user ID that the import should import for.} |
||
64 | {--token= : The user\'s access token.} |
||
65 | {--accounts= : A comma-separated list of asset accounts or liabilities to apply your rules to.} |
||
66 | {--rule_groups= : A comma-separated list of rule groups to apply. Take the ID\'s of these rule groups from the Firefly III interface.} |
||
67 | {--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.} |
||
68 | {--all_rules : If set, will overrule both settings and simply apply ALL of your rules.} |
||
69 | {--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} |
||
70 | {--end_date= : The date of the latest transaction to be included (inclusive). If omitted, will be your latest transaction ever. Format: YYYY-MM-DD}'; |
||
71 | /** @var array */ |
||
72 | private $acceptedAccounts; |
||
73 | /** @var Collection */ |
||
74 | private $accounts; |
||
75 | /** @var bool */ |
||
76 | private $allRules; |
||
77 | /** @var Carbon */ |
||
78 | private $endDate; |
||
79 | /** @var Collection */ |
||
80 | private $groups; |
||
81 | /** @var RuleGroupRepositoryInterface */ |
||
82 | private $ruleGroupRepository; |
||
83 | /** @var array */ |
||
84 | private $ruleGroupSelection; |
||
85 | /** @var RuleRepositoryInterface */ |
||
86 | private $ruleRepository; |
||
87 | /** @var array */ |
||
88 | private $ruleSelection; |
||
89 | /** @var Carbon */ |
||
90 | private $startDate; |
||
91 | |||
92 | /** |
||
93 | * Execute the console command. |
||
94 | * |
||
95 | * @throws FireflyException |
||
96 | * @return int |
||
97 | */ |
||
98 | public function handle(): int |
||
99 | { |
||
100 | $this->stupidLaravel(); |
||
101 | // @codeCoverageIgnoreStart |
||
102 | if (!$this->verifyAccessToken()) { |
||
103 | $this->error('Invalid access token.'); |
||
104 | |||
105 | return 1; |
||
106 | } |
||
107 | // @codeCoverageIgnoreEnd |
||
108 | |||
109 | // set user: |
||
110 | $this->ruleRepository->setUser($this->getUser()); |
||
111 | $this->ruleGroupRepository->setUser($this->getUser()); |
||
112 | |||
113 | $result = $this->verifyInput(); |
||
114 | if (false === $result) { |
||
115 | // app('telemetry')->feature('executed-command-with-error', $this->signature); |
||
116 | return 1; |
||
117 | } |
||
118 | |||
119 | $this->allRules = $this->option('all_rules'); |
||
0 ignored issues
–
show
|
|||
120 | |||
121 | // always get all the rules of the user. |
||
122 | $this->grabAllRules(); |
||
123 | |||
124 | // loop all groups and rules and indicate if they're included: |
||
125 | $rulesToApply = $this->getRulesToApply(); |
||
126 | $count = count($rulesToApply); |
||
127 | if (0 === $count) { |
||
128 | $this->error('No rules or rule groups have been included.'); |
||
129 | $this->warn('Make a selection using:'); |
||
130 | $this->warn(' --rules=1,2,...'); |
||
131 | $this->warn(' --rule_groups=1,2,...'); |
||
132 | $this->warn(' --all_rules'); |
||
133 | |||
134 | // app('telemetry')->feature('executed-command-with-error', $this->signature); |
||
135 | return 1; |
||
136 | } |
||
137 | |||
138 | /** @var GroupCollectorInterface $collector */ |
||
139 | $collector = app(GroupCollectorInterface::class); |
||
140 | $collector->setUser($this->getUser()); |
||
141 | $collector->setAccounts($this->accounts); |
||
142 | $collector->setRange($this->startDate, $this->endDate); |
||
143 | $journals = $collector->getExtractedJournals(); |
||
144 | |||
145 | // start running rules. |
||
146 | $this->line(sprintf('Will apply %d rule(s) to %d transaction(s).', $count, count($journals))); |
||
147 | |||
148 | // start looping. |
||
149 | /** @var RuleEngine $ruleEngine */ |
||
150 | $ruleEngine = app(RuleEngine::class); |
||
151 | $ruleEngine->setUser($this->getUser()); |
||
152 | $ruleEngine->setRulesToApply($rulesToApply); |
||
153 | |||
154 | // for this call, the rule engine only includes "store" rules: |
||
155 | $ruleEngine->setTriggerMode(RuleEngine::TRIGGER_STORE); |
||
156 | |||
157 | $bar = $this->output->createProgressBar(count($journals)); |
||
158 | Log::debug(sprintf('Now looping %d transactions.', count($journals))); |
||
159 | /** @var array $journal */ |
||
160 | foreach ($journals as $journal) { |
||
161 | Log::debug('Start of new journal.'); |
||
162 | $ruleEngine->processJournalArray($journal); |
||
163 | Log::debug('Done with all rules for this group + done with journal.'); |
||
164 | /** @noinspection DisconnectedForeachInstructionInspection */ |
||
165 | $bar->advance(); |
||
166 | } |
||
167 | $this->line(''); |
||
168 | $this->line('Done!'); |
||
169 | |||
170 | // app('telemetry')->feature('executed-command', $this->signature); |
||
171 | return 0; |
||
172 | } |
||
173 | |||
174 | /** |
||
175 | * @return array |
||
176 | */ |
||
177 | private function getRulesToApply(): array |
||
178 | { |
||
179 | $rulesToApply = []; |
||
180 | /** @var RuleGroup $group */ |
||
181 | foreach ($this->groups as $group) { |
||
182 | $rules = $this->ruleGroupRepository->getActiveStoreRules($group); |
||
183 | /** @var Rule $rule */ |
||
184 | foreach ($rules as $rule) { |
||
185 | // if in rule selection, or group in selection or all rules, it's included. |
||
186 | $test = $this->includeRule($rule, $group); |
||
187 | if (true === $test) { |
||
188 | Log::debug(sprintf('Will include rule #%d "%s"', $rule->id, $rule->title)); |
||
189 | $rulesToApply[] = $rule->id; |
||
190 | } |
||
191 | } |
||
192 | } |
||
193 | |||
194 | return $rulesToApply; |
||
195 | } |
||
196 | |||
197 | /** |
||
198 | */ |
||
199 | private function grabAllRules(): void |
||
200 | { |
||
201 | $this->groups = $this->ruleGroupRepository->getActiveGroups(); |
||
202 | } |
||
203 | |||
204 | /** |
||
205 | * @param Rule $rule |
||
206 | * @param RuleGroup $group |
||
207 | * |
||
208 | * @return bool |
||
209 | */ |
||
210 | private function includeRule(Rule $rule, RuleGroup $group): bool |
||
211 | { |
||
212 | return in_array($group->id, $this->ruleGroupSelection, true) |
||
213 | || in_array($rule->id, $this->ruleSelection, true) |
||
214 | || $this->allRules; |
||
215 | } |
||
216 | |||
217 | /** |
||
218 | * Laravel will execute ALL __construct() methods for ALL commands whenever a SINGLE command is |
||
219 | * executed. This leads to noticeable slow-downs and class calls. To prevent this, this method should |
||
220 | * be called from the handle method instead of using the constructor to initialize the command. |
||
221 | * |
||
222 | * @codeCoverageIgnore |
||
223 | */ |
||
224 | private function stupidLaravel(): void |
||
225 | { |
||
226 | $this->allRules = false; |
||
227 | $this->accounts = new Collection; |
||
228 | $this->ruleSelection = []; |
||
229 | $this->ruleGroupSelection = []; |
||
230 | $this->ruleRepository = app(RuleRepositoryInterface::class); |
||
231 | $this->ruleGroupRepository = app(RuleGroupRepositoryInterface::class); |
||
232 | $this->acceptedAccounts = [AccountType::DEFAULT, AccountType::DEBT, AccountType::ASSET, AccountType::LOAN, AccountType::MORTGAGE]; |
||
233 | $this->groups = new Collection; |
||
234 | } |
||
235 | |||
236 | /** |
||
237 | * @throws FireflyException |
||
238 | * @return bool |
||
239 | */ |
||
240 | private function verifyInput(): bool |
||
241 | { |
||
242 | // verify account. |
||
243 | $result = $this->verifyInputAccounts(); |
||
244 | if (false === $result) { |
||
245 | return $result; |
||
246 | } |
||
247 | |||
248 | // verify rule groups. |
||
249 | $this->verifyInputRuleGroups(); |
||
250 | |||
251 | // verify rules. |
||
252 | $this->verifyInputRules(); |
||
253 | |||
254 | $this->verifyInputDates(); |
||
255 | |||
256 | return true; |
||
257 | } |
||
258 | |||
259 | /** |
||
260 | * @throws FireflyException |
||
261 | * @return bool |
||
262 | */ |
||
263 | private function verifyInputAccounts(): bool |
||
264 | { |
||
265 | $accountString = $this->option('accounts'); |
||
266 | if (null === $accountString || '' === $accountString) { |
||
267 | $this->error('Please use the --accounts option to indicate the accounts to apply rules to.'); |
||
268 | |||
269 | return false; |
||
270 | } |
||
271 | $finalList = new Collection; |
||
272 | $accountList = explode(',', $accountString); |
||
273 | |||
274 | // @codeCoverageIgnoreStart |
||
275 | if (0 === count($accountList)) { |
||
276 | $this->error('Please use the --accounts option to indicate the accounts to apply rules to.'); |
||
277 | |||
278 | return false; |
||
279 | } |
||
280 | // @codeCoverageIgnoreEnd |
||
281 | |||
282 | /** @var AccountRepositoryInterface $accountRepository */ |
||
283 | $accountRepository = app(AccountRepositoryInterface::class); |
||
284 | $accountRepository->setUser($this->getUser()); |
||
285 | |||
286 | |||
287 | foreach ($accountList as $accountId) { |
||
288 | $accountId = (int) $accountId; |
||
289 | $account = $accountRepository->findNull($accountId); |
||
290 | if (null !== $account && in_array($account->accountType->type, $this->acceptedAccounts, true)) { |
||
291 | $finalList->push($account); |
||
292 | } |
||
293 | } |
||
294 | |||
295 | if (0 === $finalList->count()) { |
||
296 | $this->error('Please make sure all accounts in --accounts are asset accounts or liabilities.'); |
||
297 | |||
298 | return false; |
||
299 | } |
||
300 | $this->accounts = $finalList; |
||
301 | |||
302 | return true; |
||
303 | |||
304 | } |
||
305 | |||
306 | /** |
||
307 | * @throws FireflyException |
||
308 | */ |
||
309 | private function verifyInputDates(): void |
||
310 | { |
||
311 | // parse start date. |
||
312 | $startDate = Carbon::now()->startOfMonth(); |
||
313 | $startString = $this->option('start_date'); |
||
314 | if (null === $startString) { |
||
315 | /** @var JournalRepositoryInterface $repository */ |
||
316 | $repository = app(JournalRepositoryInterface::class); |
||
317 | $repository->setUser($this->getUser()); |
||
318 | $first = $repository->firstNull(); |
||
319 | if (null !== $first) { |
||
320 | $startDate = $first->date; |
||
321 | } |
||
322 | } |
||
323 | if (null !== $startString && '' !== $startString) { |
||
324 | $startDate = Carbon::createFromFormat('Y-m-d', $startString); |
||
325 | } |
||
326 | |||
327 | // parse end date |
||
328 | $endDate = Carbon::now(); |
||
329 | $endString = $this->option('end_date'); |
||
330 | if (null !== $endString && '' !== $endString) { |
||
331 | $endDate = Carbon::createFromFormat('Y-m-d', $endString); |
||
332 | } |
||
333 | |||
334 | if ($startDate > $endDate) { |
||
335 | [$endDate, $startDate] = [$startDate, $endDate]; |
||
336 | } |
||
337 | |||
338 | $this->startDate = $startDate; |
||
339 | $this->endDate = $endDate; |
||
340 | } |
||
341 | |||
342 | /** |
||
343 | * @return bool |
||
344 | */ |
||
345 | private function verifyInputRuleGroups(): bool |
||
346 | { |
||
347 | $ruleGroupString = $this->option('rule_groups'); |
||
348 | if (null === $ruleGroupString || '' === $ruleGroupString) { |
||
349 | // can be empty. |
||
350 | return true; |
||
351 | } |
||
352 | $ruleGroupList = explode(',', $ruleGroupString); |
||
353 | // @codeCoverageIgnoreStart |
||
354 | if (0 === count($ruleGroupList)) { |
||
355 | // can be empty. |
||
356 | return true; |
||
357 | } |
||
358 | // @codeCoverageIgnoreEnd |
||
359 | foreach ($ruleGroupList as $ruleGroupId) { |
||
360 | $ruleGroup = $this->ruleGroupRepository->find((int) $ruleGroupId); |
||
361 | if ($ruleGroup->active) { |
||
362 | $this->ruleGroupSelection[] = $ruleGroup->id; |
||
363 | } |
||
364 | if (false === $ruleGroup->active) { |
||
365 | $this->warn(sprintf('Will ignore inactive rule group #%d ("%s")', $ruleGroup->id, $ruleGroup->title)); |
||
366 | } |
||
367 | } |
||
368 | |||
369 | return true; |
||
370 | } |
||
371 | |||
372 | /** |
||
373 | * @return bool |
||
374 | */ |
||
375 | private function verifyInputRules(): bool |
||
376 | { |
||
377 | $ruleString = $this->option('rules'); |
||
378 | if (null === $ruleString || '' === $ruleString) { |
||
379 | // can be empty. |
||
380 | return true; |
||
381 | } |
||
382 | $ruleList = explode(',', $ruleString); |
||
383 | |||
384 | // @codeCoverageIgnoreStart |
||
385 | if (0 === count($ruleList)) { |
||
386 | // can be empty. |
||
387 | |||
388 | return true; |
||
389 | } |
||
390 | // @codeCoverageIgnoreEnd |
||
391 | |||
392 | foreach ($ruleList as $ruleId) { |
||
393 | $rule = $this->ruleRepository->find((int) $ruleId); |
||
394 | if (null !== $rule && $rule->active) { |
||
395 | $this->ruleSelection[] = $rule->id; |
||
396 | } |
||
397 | } |
||
398 | |||
399 | return true; |
||
400 | } |
||
401 | } |
||
402 |
This check looks for assignments to scalar types that may be of the wrong type.
To ensure the code behaves as expected, it may be a good idea to add an explicit type cast.