1 | <?php |
||
2 | |||
3 | /** |
||
4 | * ApplyRules.php |
||
5 | * Copyright (c) 2018 [email protected] |
||
6 | * |
||
7 | * This file is part of Firefly III. |
||
8 | * |
||
9 | * Firefly III is free software: you can redistribute it and/or modify |
||
10 | * it under the terms of the GNU General Public License as published by |
||
11 | * the Free Software Foundation, either version 3 of the License, or |
||
12 | * (at your option) any later version. |
||
13 | * |
||
14 | * Firefly III is distributed in the hope that it will be useful, |
||
15 | * but WITHOUT ANY WARRANTY; without even the implied warranty of |
||
16 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
||
17 | * GNU General Public License for more details. |
||
18 | * |
||
19 | * You should have received a copy of the GNU General Public License |
||
20 | * along with Firefly III. If not, see <http://www.gnu.org/licenses/>. |
||
21 | */ |
||
22 | |||
23 | declare(strict_types=1); |
||
24 | |||
25 | namespace FireflyIII\Console\Commands; |
||
26 | |||
27 | use Carbon\Carbon; |
||
28 | use FireflyIII\Helpers\Collector\TransactionCollectorInterface; |
||
29 | use FireflyIII\Models\AccountType; |
||
30 | use FireflyIII\Models\Rule; |
||
31 | use FireflyIII\Models\RuleGroup; |
||
32 | use FireflyIII\Models\Transaction; |
||
33 | use FireflyIII\Repositories\Account\AccountRepositoryInterface; |
||
34 | use FireflyIII\Repositories\Journal\JournalRepositoryInterface; |
||
35 | use FireflyIII\Repositories\Rule\RuleRepositoryInterface; |
||
36 | use FireflyIII\Repositories\RuleGroup\RuleGroupRepositoryInterface; |
||
37 | use FireflyIII\TransactionRules\Processor; |
||
38 | use Illuminate\Console\Command; |
||
39 | use Illuminate\Support\Collection; |
||
40 | |||
41 | /** |
||
42 | * |
||
43 | * Class ApplyRules |
||
44 | * |
||
45 | * @codeCoverageIgnore |
||
46 | */ |
||
47 | class ApplyRules extends Command |
||
48 | { |
||
49 | use VerifiesAccessToken; |
||
50 | |||
51 | /** |
||
52 | * The console command description. |
||
53 | * |
||
54 | * @var string |
||
55 | */ |
||
56 | protected $description = 'This command will apply your rules and rule groups on a selection of your transactions.'; |
||
57 | /** |
||
58 | * The name and signature of the console command. |
||
59 | * |
||
60 | * @var string |
||
61 | */ |
||
62 | protected $signature |
||
63 | = 'firefly:apply-rules |
||
64 | {--user=1 : The user ID that the import should import for.} |
||
65 | {--token= : The user\'s access token.} |
||
66 | {--accounts= : A comma-separated list of asset accounts or liabilities to apply your rules to.} |
||
67 | {--rule_groups= : A comma-separated list of rule groups to apply. Take the ID\'s of these rule groups from the Firefly III interface.} |
||
68 | {--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.} |
||
69 | {--all_rules : If set, will overrule both settings and simply apply ALL of your rules.} |
||
70 | {--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} |
||
71 | {--end_date= : The date of the latest transaction to be included (inclusive). If omitted, will be your latest transaction ever. Format: YYYY-MM-DD}'; |
||
72 | /** @var Collection */ |
||
73 | private $accounts; |
||
74 | /** @var Carbon */ |
||
75 | private $endDate; |
||
76 | /** @var Collection */ |
||
77 | private $results; |
||
78 | /** @var Collection */ |
||
79 | private $ruleGroups; |
||
80 | /** @var Collection */ |
||
81 | private $rules; |
||
82 | /** @var Carbon */ |
||
83 | private $startDate; |
||
84 | |||
85 | /** |
||
86 | * Create a new command instance. |
||
87 | * |
||
88 | * @return void |
||
89 | */ |
||
90 | public function __construct() |
||
91 | { |
||
92 | parent::__construct(); |
||
93 | $this->accounts = new Collection; |
||
94 | $this->rules = new Collection; |
||
95 | $this->ruleGroups = new Collection; |
||
96 | $this->results = new Collection; |
||
97 | } |
||
98 | |||
99 | /** |
||
100 | * Execute the console command. |
||
101 | * |
||
102 | * @return int |
||
103 | * @throws \FireflyIII\Exceptions\FireflyException |
||
104 | */ |
||
105 | public function handle(): int |
||
106 | { |
||
107 | if (!$this->verifyAccessToken()) { |
||
108 | $this->error('Invalid access token.'); |
||
109 | |||
110 | return 1; |
||
111 | } |
||
112 | |||
113 | $result = $this->verifyInput(); |
||
114 | if (false === $result) { |
||
115 | return 1; |
||
116 | } |
||
117 | |||
118 | |||
119 | // get transactions from asset accounts. |
||
120 | /** @var TransactionCollectorInterface $collector */ |
||
121 | $collector = app(TransactionCollectorInterface::class); |
||
122 | $collector->setUser($this->getUser()); |
||
123 | $collector->setAccounts($this->accounts); |
||
124 | $collector->setRange($this->startDate, $this->endDate); |
||
125 | $transactions = $collector->getTransactions(); |
||
126 | $count = $transactions->count(); |
||
127 | |||
128 | // first run all rule groups: |
||
129 | /** @var RuleGroupRepositoryInterface $ruleGroupRepos */ |
||
130 | $ruleGroupRepos = app(RuleGroupRepositoryInterface::class); |
||
131 | $ruleGroupRepos->setUser($this->getUser()); |
||
132 | |||
133 | /** @var RuleGroup $ruleGroup */ |
||
134 | foreach ($this->ruleGroups as $ruleGroup) { |
||
135 | $this->line(sprintf('Going to apply rule group "%s" to %d transaction(s).', $ruleGroup->title, $count)); |
||
136 | $rules = $ruleGroupRepos->getActiveStoreRules($ruleGroup); |
||
137 | $this->applyRuleSelection($rules, $transactions, true); |
||
138 | } |
||
139 | |||
140 | // then run all rules (rule groups should be empty). |
||
141 | if ($this->rules->count() > 0) { |
||
142 | |||
143 | $this->line(sprintf('Will apply %d rule(s) to %d transaction(s)', $this->rules->count(), $transactions->count())); |
||
144 | $this->applyRuleSelection($this->rules, $transactions, false); |
||
145 | } |
||
146 | |||
147 | // filter results: |
||
148 | $this->results = $this->results->unique( |
||
149 | function (Transaction $transaction) { |
||
150 | return (int)$transaction->journal_id; |
||
151 | } |
||
152 | ); |
||
153 | |||
154 | $this->line(''); |
||
155 | if (0 === $this->results->count()) { |
||
156 | $this->line('The rules were fired but did not influence any transactions.'); |
||
157 | } |
||
158 | if ($this->results->count() > 0) { |
||
159 | $this->line(sprintf('The rule(s) was/were fired, and influenced %d transaction(s).', $this->results->count())); |
||
160 | foreach ($this->results as $result) { |
||
161 | $this->line( |
||
162 | vsprintf( |
||
163 | 'Transaction #%d: "%s" (%s %s)', |
||
164 | [ |
||
165 | $result->journal_id, |
||
166 | $result->description, |
||
167 | $result->transaction_currency_code, |
||
168 | round($result->transaction_amount, $result->transaction_currency_dp), |
||
169 | ] |
||
170 | ) |
||
171 | ); |
||
172 | } |
||
173 | } |
||
174 | |||
175 | return 0; |
||
176 | } |
||
177 | |||
178 | /** |
||
179 | * @param Collection $rules |
||
180 | * @param Collection $transactions |
||
181 | * @param bool $breakProcessing |
||
182 | * |
||
183 | * @throws \FireflyIII\Exceptions\FireflyException |
||
184 | */ |
||
185 | private function applyRuleSelection(Collection $rules, Collection $transactions, bool $breakProcessing): void |
||
186 | { |
||
187 | $bar = $this->output->createProgressBar($rules->count() * $transactions->count()); |
||
188 | |||
189 | /** @var Rule $rule */ |
||
190 | foreach ($rules as $rule) { |
||
191 | /** @var Processor $processor */ |
||
192 | $processor = app(Processor::class); |
||
193 | $processor->make($rule, true); |
||
194 | |||
195 | /** @var Transaction $transaction */ |
||
196 | foreach ($transactions as $transaction) { |
||
197 | /** @noinspection DisconnectedForeachInstructionInspection */ |
||
198 | $bar->advance(); |
||
199 | $result = $processor->handleTransaction($transaction); |
||
200 | if (true === $result) { |
||
201 | $this->results->push($transaction); |
||
202 | } |
||
203 | } |
||
204 | if (true === $rule->stop_processing && true === $breakProcessing) { |
||
205 | $this->line(''); |
||
206 | $this->line(sprintf('Rule #%d ("%s") says to stop processing.', $rule->id, $rule->title)); |
||
207 | |||
208 | return; |
||
209 | } |
||
210 | } |
||
211 | $this->line(''); |
||
212 | } |
||
213 | |||
214 | /** |
||
215 | * |
||
216 | * @throws \FireflyIII\Exceptions\FireflyException |
||
217 | */ |
||
218 | private function grabAllRules(): void |
||
219 | { |
||
220 | if (true === $this->option('all_rules')) { |
||
221 | /** @var RuleRepositoryInterface $ruleRepos */ |
||
222 | $ruleRepos = app(RuleRepositoryInterface::class); |
||
223 | $ruleRepos->setUser($this->getUser()); |
||
224 | $this->rules = $ruleRepos->getAll(); |
||
225 | |||
226 | // reset rule groups. |
||
227 | $this->ruleGroups = new Collection; |
||
228 | } |
||
229 | } |
||
230 | |||
231 | /** |
||
232 | * |
||
233 | * @throws \FireflyIII\Exceptions\FireflyException |
||
234 | */ |
||
235 | private function parseDates(): void |
||
236 | { |
||
237 | // parse start date. |
||
238 | $startDate = Carbon::now()->startOfMonth(); |
||
239 | $startString = $this->option('start_date'); |
||
240 | if (null === $startString) { |
||
241 | /** @var JournalRepositoryInterface $repository */ |
||
242 | $repository = app(JournalRepositoryInterface::class); |
||
243 | $repository->setUser($this->getUser()); |
||
244 | $first = $repository->firstNull(); |
||
245 | if (null !== $first) { |
||
246 | $startDate = $first->date; |
||
247 | } |
||
248 | } |
||
249 | if (null !== $startString && '' !== $startString) { |
||
250 | $startDate = Carbon::createFromFormat('Y-m-d', $startString); |
||
251 | } |
||
252 | |||
253 | // parse end date |
||
254 | $endDate = Carbon::now(); |
||
255 | $endString = $this->option('end_date'); |
||
256 | if (null !== $endString && '' !== $endString) { |
||
257 | $endDate = Carbon::createFromFormat('Y-m-d', $endString); |
||
258 | } |
||
259 | |||
260 | if ($startDate > $endDate) { |
||
261 | [$endDate, $startDate] = [$startDate, $endDate]; |
||
262 | } |
||
263 | |||
264 | $this->startDate = $startDate; |
||
0 ignored issues
–
show
|
|||
265 | $this->endDate = $endDate; |
||
0 ignored issues
–
show
It seems like
$endDate can also be of type false . However, the property $endDate is declared as type Carbon\Carbon . Maybe add an additional type check?
Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly. For example, imagine you have a variable Either this assignment is in error or a type check should be added for that assignment. class Id
{
public $id;
public function __construct($id)
{
$this->id = $id;
}
}
class Account
{
/** @var Id $id */
public $id;
}
$account_id = false;
if (starsAreRight()) {
$account_id = new Id(42);
}
$account = new Account();
if ($account instanceof Id)
{
$account->id = $account_id;
}
Loading history...
|
|||
266 | } |
||
267 | |||
268 | /** |
||
269 | * @return bool |
||
270 | * @throws \FireflyIII\Exceptions\FireflyException |
||
271 | */ |
||
272 | private function verifyInput(): bool |
||
273 | { |
||
274 | // verify account. |
||
275 | $result = $this->verifyInputAccounts(); |
||
276 | if (false === $result) { |
||
277 | return $result; |
||
278 | } |
||
279 | |||
280 | // verify rule groups. |
||
281 | $result = $this->verifyRuleGroups(); |
||
282 | if (false === $result) { |
||
283 | return $result; |
||
284 | } |
||
285 | |||
286 | // verify rules. |
||
287 | $result = $this->verifyRules(); |
||
288 | if (false === $result) { |
||
289 | return $result; |
||
290 | } |
||
291 | |||
292 | $this->grabAllRules(); |
||
293 | $this->parseDates(); |
||
294 | |||
295 | //$this->line('Number of rules found: ' . $this->rules->count()); |
||
296 | $this->line('Start date is ' . $this->startDate->format('Y-m-d')); |
||
297 | $this->line('End date is ' . $this->endDate->format('Y-m-d')); |
||
298 | |||
299 | return true; |
||
300 | } |
||
301 | |||
302 | /** |
||
303 | * @return bool |
||
304 | * @throws \FireflyIII\Exceptions\FireflyException |
||
305 | */ |
||
306 | private function verifyInputAccounts(): bool |
||
307 | { |
||
308 | $accountString = $this->option('accounts'); |
||
309 | if (null === $accountString || '' === $accountString) { |
||
310 | $this->error('Please use the --accounts to indicate the accounts to apply rules to.'); |
||
311 | |||
312 | return false; |
||
313 | } |
||
314 | $finalList = new Collection; |
||
315 | $accountList = explode(',', $accountString); |
||
316 | |||
317 | if (0 === \count($accountList)) { |
||
318 | $this->error('Please use the --accounts to indicate the accounts to apply rules to.'); |
||
319 | |||
320 | return false; |
||
321 | } |
||
322 | |||
323 | /** @var AccountRepositoryInterface $accountRepository */ |
||
324 | $accountRepository = app(AccountRepositoryInterface::class); |
||
325 | $accountRepository->setUser($this->getUser()); |
||
326 | |||
327 | foreach ($accountList as $accountId) { |
||
328 | $accountId = (int)$accountId; |
||
329 | $account = $accountRepository->findNull($accountId); |
||
330 | if (null !== $account |
||
331 | && \in_array( |
||
332 | $account->accountType->type, [AccountType::DEFAULT, AccountType::DEBT, AccountType::ASSET, AccountType::LOAN, AccountType::MORTGAGE], true |
||
333 | )) { |
||
334 | $finalList->push($account); |
||
335 | } |
||
336 | } |
||
337 | |||
338 | if (0 === $finalList->count()) { |
||
339 | $this->error('Please make sure all accounts in --accounts are asset accounts or liabilities.'); |
||
340 | |||
341 | return false; |
||
342 | } |
||
343 | $this->accounts = $finalList; |
||
344 | |||
345 | return true; |
||
346 | |||
347 | } |
||
348 | |||
349 | /** |
||
350 | * @return bool |
||
351 | * @throws \FireflyIII\Exceptions\FireflyException |
||
352 | */ |
||
353 | private function verifyRuleGroups(): bool |
||
354 | { |
||
355 | $ruleGroupString = $this->option('rule_groups'); |
||
356 | if (null === $ruleGroupString || '' === $ruleGroupString) { |
||
357 | // can be empty. |
||
358 | return true; |
||
359 | } |
||
360 | $ruleGroupList = explode(',', $ruleGroupString); |
||
361 | |||
362 | if (0 === \count($ruleGroupList)) { |
||
363 | // can be empty. |
||
364 | |||
365 | return true; |
||
366 | } |
||
367 | /** @var RuleGroupRepositoryInterface $ruleGroupRepos */ |
||
368 | $ruleGroupRepos = app(RuleGroupRepositoryInterface::class); |
||
369 | $ruleGroupRepos->setUser($this->getUser()); |
||
370 | |||
371 | foreach ($ruleGroupList as $ruleGroupId) { |
||
372 | $ruleGroupId = (int)$ruleGroupId; |
||
373 | $ruleGroup = $ruleGroupRepos->find($ruleGroupId); |
||
374 | $this->ruleGroups->push($ruleGroup); |
||
375 | } |
||
376 | |||
377 | return true; |
||
378 | } |
||
379 | |||
380 | /** |
||
381 | * @return bool |
||
382 | * @throws \FireflyIII\Exceptions\FireflyException |
||
383 | */ |
||
384 | private function verifyRules(): bool |
||
385 | { |
||
386 | $ruleString = $this->option('rules'); |
||
387 | if (null === $ruleString || '' === $ruleString) { |
||
388 | // can be empty. |
||
389 | return true; |
||
390 | } |
||
391 | $finalList = new Collection; |
||
392 | $ruleList = explode(',', $ruleString); |
||
393 | |||
394 | if (0 === \count($ruleList)) { |
||
395 | // can be empty. |
||
396 | |||
397 | return true; |
||
398 | } |
||
399 | /** @var RuleRepositoryInterface $ruleRepos */ |
||
400 | $ruleRepos = app(RuleRepositoryInterface::class); |
||
401 | $ruleRepos->setUser($this->getUser()); |
||
402 | |||
403 | foreach ($ruleList as $ruleId) { |
||
404 | $ruleId = (int)$ruleId; |
||
405 | $rule = $ruleRepos->find($ruleId); |
||
406 | if (null !== $rule) { |
||
407 | $finalList->push($rule); |
||
408 | } |
||
409 | } |
||
410 | if ($finalList->count() > 0) { |
||
411 | // reset rule groups. |
||
412 | $this->ruleGroups = new Collection; |
||
413 | $this->rules = $finalList; |
||
414 | } |
||
415 | |||
416 | return true; |
||
417 | } |
||
418 | |||
419 | |||
420 | } |
||
421 |
Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.
For example, imagine you have a variable
$accountId
that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to theid
property of an instance of theAccount
class. This class holds a proper account, so the id value must no longer be false.Either this assignment is in error or a type check should be added for that assignment.