GitHub Access Token became invalid

It seems like the GitHub access token used for retrieving details about this repository from GitHub became invalid. This might prevent certain types of inspections from being run (in particular, everything related to pull requests).
Please ask an admin of your repository to re-new the access token on this website.

TransactionValidation   F
last analyzed

Complexity

Total Complexity 65

Size/Duplication

Total Lines 422
Duplicated Lines 0 %

Importance

Changes 12
Bugs 1 Features 0
Metric Value
wmc 65
eloc 184
c 12
b 1
f 0
dl 0
loc 422
rs 3.2

14 Methods

Rating   Name   Duplication   Size   Complexity  
A validateAccountInformation() 0 15 2
A validateTransactionTypesForUpdate() 0 17 3
A validateSingleAccount() 0 32 5
B validateSingleUpdate() 0 37 9
A validateTransactionTypes() 0 18 4
A arrayEqual() 0 3 1
A validateAccountInformationUpdate() 0 11 2
A getTransactionsArray() 0 17 3
A validateOneRecurrenceTransaction() 0 8 2
A getOriginalData() 0 25 4
B validateEqualAccounts() 0 35 10
A validateOneTransaction() 0 11 2
A getOriginalType() 0 12 3
C validateEqualAccountsForUpdate() 0 77 15

How to fix   Complexity   

Complex Class

Complex classes like TransactionValidation often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use TransactionValidation, and based on these observations, apply Extract Interface, too.

1
<?php
2
/**
3
 * TransactionValidation.php
4
 * Copyright (c) 2019 [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\Validation;
25
26
use FireflyIII\Models\Transaction;
27
use FireflyIII\Models\TransactionGroup;
28
use FireflyIII\Models\TransactionJournal;
29
use Illuminate\Validation\Validator;
30
use Log;
31
32
/**
33
 * Trait TransactionValidation
34
 */
35
trait TransactionValidation
36
{
37
    /**
38
     * Validates the given account information. Switches on given transaction type.
39
     *
40
     * @param Validator $validator
41
     */
42
    public function validateAccountInformation(Validator $validator): void
43
    {
44
        Log::debug('Now in validateAccountInformation()');
45
        $transactions = $this->getTransactionsArray($validator);
46
        $data         = $validator->getData();
47
48
        $transactionType = $data['type'] ?? 'invalid';
49
50
        Log::debug(sprintf('Going to loop %d transaction(s)', count($transactions)));
51
        /**
52
         * @var int   $index
53
         * @var array $transaction
54
         */
55
        foreach ($transactions as $index => $transaction) {
56
            $this->validateSingleAccount($validator, $index, $transactionType, $transaction);
57
        }
58
    }
59
60
    /**
61
     * @param Validator $validator
62
     * @param int       $index
63
     * @param string    $transactionType
64
     * @param array     $transaction
65
     */
66
    protected function validateSingleAccount(Validator $validator, int $index, string $transactionType, array $transaction): void
67
    {
68
        /** @var AccountValidator $accountValidator */
69
        $accountValidator = app(AccountValidator::class);
70
71
        $transactionType = $transaction['type'] ?? $transactionType;
72
        $accountValidator->setTransactionType($transactionType);
73
74
        // validate source account.
75
        $sourceId    = isset($transaction['source_id']) ? (int) $transaction['source_id'] : null;
76
        $sourceName  = $transaction['source_name'] ?? null;
77
        $sourceIban  = $transaction['source_iban'] ?? null;
78
        $validSource = $accountValidator->validateSource($sourceId, $sourceName, $sourceIban);
79
80
        // do something with result:
81
        if (false === $validSource) {
82
            $validator->errors()->add(sprintf('transactions.%d.source_id', $index), $accountValidator->sourceError);
83
            $validator->errors()->add(sprintf('transactions.%d.source_name', $index), $accountValidator->sourceError);
84
85
            return;
86
        }
87
        // validate destination account
88
        $destinationId    = isset($transaction['destination_id']) ? (int) $transaction['destination_id'] : null;
89
        $destinationName  = $transaction['destination_name'] ?? null;
90
        $destinationIban  = $transaction['destination_iban'] ?? null;
91
        $validDestination = $accountValidator->validateDestination($destinationId, $destinationName, $destinationIban);
92
        // do something with result:
93
        if (false === $validDestination) {
94
            $validator->errors()->add(sprintf('transactions.%d.destination_id', $index), $accountValidator->destError);
95
            $validator->errors()->add(sprintf('transactions.%d.destination_name', $index), $accountValidator->destError);
96
97
            return;
98
        }
99
    }
100
101
    /**
102
     * Validates the given account information. Switches on given transaction type.
103
     *
104
     * @param Validator $validator
105
     */
106
    public function validateAccountInformationUpdate(Validator $validator): void
107
    {
108
        Log::debug('Now in validateAccountInformationUpdate()');
109
        $transactions = $this->getTransactionsArray($validator);
110
111
        /**
112
         * @var int   $index
113
         * @var array $transaction
114
         */
115
        foreach ($transactions as $index => $transaction) {
116
            $this->validateSingleUpdate($validator, $index, $transaction);
117
        }
118
    }
119
120
    /**
121
     * @param Validator $validator
122
     * @param int       $index
123
     * @param array     $transaction
124
     */
125
    protected function validateSingleUpdate(Validator $validator, int $index, array $transaction): void
126
    {
127
        /** @var AccountValidator $accountValidator */
128
        $accountValidator = app(AccountValidator::class);
129
        $originalType     = $this->getOriginalType((int)($transaction['transaction_journal_id'] ?? 0));
130
        $originalData     = $this->getOriginalData((int)($transaction['transaction_journal_id'] ?? 0));
131
        $transactionType  = $transaction['type'] ?? $originalType;
132
        $accountValidator->setTransactionType($transactionType);
133
134
        // if no account types are given, just skip the check.
135
        if (!isset($transaction['source_id'])
136
            && !isset($transaction['source_name'])
137
            && !isset($transaction['destination_id'])
138
            && !isset($transaction['destination_name'])) {
139
            return;
140
        }
141
142
        // validate source account.
143
        $sourceId    = isset($transaction['source_id']) ? (int)$transaction['source_id'] : $originalData['source_id'];
144
        $sourceName  = $transaction['source_name'] ?? $originalData['source_name'];
145
        $validSource = $accountValidator->validateSource($sourceId, $sourceName, null);
146
147
        // do something with result:
148
        if (false === $validSource) {
149
            $validator->errors()->add(sprintf('transactions.%d.source_id', $index), $accountValidator->sourceError);
150
            $validator->errors()->add(sprintf('transactions.%d.source_name', $index), $accountValidator->sourceError);
151
152
            return;
153
        }
154
        // validate destination account
155
        $destinationId    = isset($transaction['destination_id']) ? (int)$transaction['destination_id'] : $originalData['destination_id'];
156
        $destinationName  = $transaction['destination_name'] ?? $originalData['destination_name'];
157
        $validDestination = $accountValidator->validateDestination($destinationId, $destinationName, null);
158
        // do something with result:
159
        if (false === $validDestination) {
160
            $validator->errors()->add(sprintf('transactions.%d.destination_id', $index), $accountValidator->destError);
161
            $validator->errors()->add(sprintf('transactions.%d.destination_name', $index), $accountValidator->destError);
162
        }
163
    }
164
165
    /**
166
     * Adds an error to the validator when there are no transactions in the array of data.
167
     *
168
     * @param Validator $validator
169
     */
170
    public function validateOneRecurrenceTransaction(Validator $validator): void
171
    {
172
        Log::debug('Now in validateOneRecurrenceTransaction()');
173
        $transactions = $this->getTransactionsArray($validator);
174
175
        // need at least one transaction
176
        if (0 === count($transactions)) {
177
            $validator->errors()->add('transactions', (string)trans('validation.at_least_one_transaction'));
178
        }
179
    }
180
181
    /**
182
     * Adds an error to the validator when there are no transactions in the array of data.
183
     *
184
     * @param Validator $validator
185
     */
186
    public function validateOneTransaction(Validator $validator): void
187
    {
188
        Log::debug('Now in validateOneTransaction()');
189
        $transactions = $this->getTransactionsArray($validator);
190
        // need at least one transaction
191
        if (0 === count($transactions)) {
192
            $validator->errors()->add('transactions.0.description', (string)trans('validation.at_least_one_transaction'));
193
            Log::debug('Added error: at_least_one_transaction.');
194
            return;
195
        }
196
        Log::debug('Added NO errors.');
197
    }
198
199
    /**
200
     * All types of splits must be equal.
201
     *
202
     * @param Validator $validator
203
     */
204
    public function validateTransactionTypes(Validator $validator): void
205
    {
206
        Log::debug('Now in validateTransactionTypes()');
207
        $transactions = $this->getTransactionsArray($validator);
208
209
        $types = [];
210
        foreach ($transactions as $transaction) {
211
            $types[] = $transaction['type'] ?? 'invalid';
212
        }
213
        $unique = array_unique($types);
214
        if (count($unique) > 1) {
215
            $validator->errors()->add('transactions.0.type', (string)trans('validation.transaction_types_equal'));
216
217
            return;
218
        }
219
        $first = $unique[0] ?? 'invalid';
220
        if ('invalid' === $first) {
221
            $validator->errors()->add('transactions.0.type', (string)trans('validation.invalid_transaction_type'));
222
        }
223
    }
224
225
    /**
226
     * All types of splits must be equal.
227
     *
228
     * @param Validator $validator
229
     */
230
    public function validateTransactionTypesForUpdate(Validator $validator): void
231
    {
232
        Log::debug('Now in validateTransactionTypesForUpdate()');
233
        $transactions = $this->getTransactionsArray($validator);
234
        $types        = [];
235
        foreach ($transactions as $transaction) {
236
            $originalType = $this->getOriginalType((int)($transaction['transaction_journal_id'] ?? 0));
237
            // if type is not set, fall back to the type of the journal, if one is given.
238
239
240
            $types[] = $transaction['type'] ?? $originalType;
241
        }
242
        $unique = array_unique($types);
243
        if (count($unique) > 1) {
244
            $validator->errors()->add('transactions.0.type', (string)trans('validation.transaction_types_equal'));
245
246
            return;
247
        }
248
    }
249
250
    /**
251
     * @param array $array
252
     *
253
     * @return bool
254
     */
255
    private function arrayEqual(array $array): bool
256
    {
257
        return 1 === count(array_unique($array));
258
    }
259
260
    /**
261
     * @param int $journalId
262
     *
263
     * @return array
264
     */
265
    private function getOriginalData(int $journalId): array
266
    {
267
        $return = [
268
            'source_id'        => 0,
269
            'source_name'      => '',
270
            'destination_id'   => 0,
271
            'destination_name' => '',
272
        ];
273
        if (0 === $journalId) {
274
            return $return;
275
        }
276
        /** @var Transaction $source */
277
        $source = Transaction::where('transaction_journal_id', $journalId)->where('amount', '<', 0)->with(['account'])->first();
278
        if (null !== $source) {
279
            $return['source_id']   = $source->account_id;
280
            $return['source_name'] = $source->account->name;
281
        }
282
        /** @var Transaction $destination */
283
        $destination = Transaction::where('transaction_journal_id', $journalId)->where('amount', '>', 0)->with(['account'])->first();
284
        if (null !== $source) {
285
            $return['destination_id']   = $destination->account_id;
286
            $return['destination_name'] = $destination->account->name;
287
        }
288
289
        return $return;
290
    }
291
292
    /**
293
     * @param int $journalId
294
     *
295
     * @return string
296
     */
297
    private function getOriginalType(int $journalId): string
298
    {
299
        if (0 === $journalId) {
300
            return 'invalid';
301
        }
302
        /** @var TransactionJournal $journal */
303
        $journal = TransactionJournal::with(['transactionType'])->find($journalId);
304
        if (null !== $journal) {
305
            return strtolower($journal->transactionType->type);
306
        }
307
308
        return 'invalid';
309
    }
310
311
    /**
312
     * @param Validator $validator
313
     *
314
     * @return array
315
     */
316
    protected function getTransactionsArray(Validator $validator): array
317
    {
318
        $data         = $validator->getData();
319
        $transactions = $data['transactions'] ?? [];
320
        if (!is_countable($transactions)) {
321
            Log::error(sprintf('Transactions array is not countable, because its a %s', gettype($transactions)));
322
323
            return [];
324
        }
325
        // a superfluous check but you never know.
326
        if (!is_array($transactions)) {
327
            Log::error(sprintf('Transactions array is not an array, because its a %s', gettype($transactions)));
328
329
            return [];
330
        }
331
332
        return $transactions;
333
    }
334
335
    /**
336
     * @param Validator $validator
337
     */
338
    private function validateEqualAccounts(Validator $validator): void
339
    {
340
        Log::debug('Now in validateEqualAccounts()');
341
        $transactions = $this->getTransactionsArray($validator);
342
343
        // needs to be split
344
        if (count($transactions) < 2) {
345
            return;
346
        }
347
        $type    = $transactions[0]['type'] ?? 'withdrawal';
348
        $sources = [];
349
        $dests   = [];
350
        foreach ($transactions as $transaction) {
351
            $sources[] = sprintf('%d-%s', $transaction['source_id'] ?? 0, $transaction['source_name'] ?? '');
352
            $dests[]   = sprintf('%d-%s', $transaction['destination_id'] ?? 0, $transaction['destination_name'] ?? '');
353
        }
354
        $sources = array_unique($sources);
355
        $dests   = array_unique($dests);
356
        switch ($type) {
357
            case 'withdrawal':
358
                if (count($sources) > 1) {
359
                    $validator->errors()->add('transactions.0.source_id', (string)trans('validation.all_accounts_equal'));
360
                }
361
                break;
362
            case 'deposit':
363
                if (count($dests) > 1) {
364
                    $validator->errors()->add('transactions.0.destination_id', (string)trans('validation.all_accounts_equal'));
365
                }
366
                break;
367
            case'transfer':
368
                if (count($sources) > 1 || count($dests) > 1) {
369
                    $validator->errors()->add('transactions.0.source_id', (string)trans('validation.all_accounts_equal'));
370
                    $validator->errors()->add('transactions.0.destination_id', (string)trans('validation.all_accounts_equal'));
371
                }
372
                break;
373
        }
374
    }
375
376
    /**
377
     * @param Validator        $validator
378
     * @param TransactionGroup $transactionGroup
379
     */
380
    private function validateEqualAccountsForUpdate(Validator $validator, TransactionGroup $transactionGroup): void
381
    {
382
        Log::debug('Now in validateEqualAccountsForUpdate()');
383
        $transactions = $this->getTransactionsArray($validator);
384
385
        // needs to be split
386
        if (count($transactions) < 2) {
387
            return;
388
        }
389
        $type = $transactions[0]['type'] ?? strtolower($transactionGroup->transactionJournals()->first()->transactionType->type);
390
391
        // compare source ID's, destination ID's, source names and destination names.
392
        // I think I can get away with one combination being equal, as long as the rest
393
        // of the code picks up on this as well.
394
        // either way all fields must be blank or all equal
395
        // but if ID's are equal don't bother with the names.
396
397
        $fields     = ['source_id', 'destination_id', 'source_name', 'destination_name'];
398
        $comparison = [];
399
        foreach ($fields as $field) {
400
            $comparison[$field] = [];
401
            /** @var array $transaction */
402
            foreach ($transactions as $transaction) {
403
                // source or destination may be omitted. If this is the case, use the original source / destination name + ID.
404
                $originalData = $this->getOriginalData((int)($transaction['transaction_journal_id'] ?? 0));
405
406
                // get field.
407
                $comparison[$field][] = $transaction[$field] ?? $originalData[$field];
408
            }
409
        }
410
        // TODO not the best way to loop this.
411
        switch ($type) {
412
            case 'withdrawal':
413
                if ($this->arrayEqual($comparison['source_id'])) {
414
                    // source ID's are equal, return void.
415
                    return;
416
                }
417
                if ($this->arrayEqual($comparison['source_name'])) {
418
                    // source names are equal, return void.
419
                    return;
420
                }
421
                // add error:
422
                $validator->errors()->add('transactions.0.source_id', (string)trans('validation.all_accounts_equal'));
423
                break;
424
            case 'deposit':
425
                if ($this->arrayEqual($comparison['destination_id'])) {
426
                    // destination ID's are equal, return void.
427
                    return;
428
                }
429
                if ($this->arrayEqual($comparison['destination_name'])) {
430
                    // destination names are equal, return void.
431
                    return;
432
                }
433
                // add error:
434
                $validator->errors()->add('transactions.0.destination_id', (string)trans('validation.all_accounts_equal'));
435
                break;
436
            case 'transfer':
437
                if ($this->arrayEqual($comparison['source_id'])) {
438
                    // source ID's are equal, return void.
439
                    return;
440
                }
441
                if ($this->arrayEqual($comparison['source_name'])) {
442
                    // source names are equal, return void.
443
                    return;
444
                }
445
                if ($this->arrayEqual($comparison['destination_id'])) {
446
                    // destination ID's are equal, return void.
447
                    return;
448
                }
449
                if ($this->arrayEqual($comparison['destination_name'])) {
450
                    // destination names are equal, return void.
451
                    return;
452
                }
453
                // add error:
454
                $validator->errors()->add('transactions.0.source_id', (string)trans('validation.all_accounts_equal'));
455
                $validator->errors()->add('transactions.0.destination_id', (string)trans('validation.all_accounts_equal'));
456
                break;
457
        }
458
    }
459
}
460