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.
Passed
Push — master ( 7cfa91...64a2f2 )
by James
31:27 queued 21:57
created

TransactionJournalFactory::errorIfDuplicate()   A

Complexity

Conditions 4
Paths 5

Size

Total Lines 14
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 9
c 0
b 0
f 0
dl 0
loc 14
rs 9.9666
cc 4
nc 5
nop 1
1
<?php
2
3
/**
4
 * TransactionJournalFactory.php
5
 * Copyright (c) 2019 [email protected]
6
 *
7
 * This file is part of Firefly III (https://github.com/firefly-iii).
8
 *
9
 * This program is free software: you can redistribute it and/or modify
10
 * it under the terms of the GNU Affero General Public License as
11
 * published by the Free Software Foundation, either version 3 of the
12
 * License, or (at your option) any later version.
13
 *
14
 * This program 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 Affero General Public License for more details.
18
 *
19
 * You should have received a copy of the GNU Affero General Public License
20
 * along with this program.  If not, see <https://www.gnu.org/licenses/>.
21
 */
22
23
declare(strict_types=1);
24
25
namespace FireflyIII\Factory;
26
27
use Carbon\Carbon;
28
use Exception;
29
use FireflyIII\Exceptions\DuplicateTransactionException;
30
use FireflyIII\Exceptions\FireflyException;
31
use FireflyIII\Models\Account;
32
use FireflyIII\Models\TransactionCurrency;
33
use FireflyIII\Models\TransactionJournal;
34
use FireflyIII\Models\TransactionJournalMeta;
35
use FireflyIII\Models\TransactionType;
36
use FireflyIII\Repositories\Account\AccountRepositoryInterface;
37
use FireflyIII\Repositories\Bill\BillRepositoryInterface;
38
use FireflyIII\Repositories\Budget\BudgetRepositoryInterface;
39
use FireflyIII\Repositories\Category\CategoryRepositoryInterface;
40
use FireflyIII\Repositories\Currency\CurrencyRepositoryInterface;
41
use FireflyIII\Repositories\PiggyBank\PiggyBankRepositoryInterface;
42
use FireflyIII\Repositories\TransactionType\TransactionTypeRepositoryInterface;
43
use FireflyIII\Services\Internal\Support\JournalServiceTrait;
44
use FireflyIII\Support\NullArrayObject;
45
use FireflyIII\User;
46
use FireflyIII\Validation\AccountValidator;
47
use Illuminate\Support\Collection;
48
use Log;
49
50
/**
51
 * Class TransactionJournalFactory
52
 */
53
class TransactionJournalFactory
54
{
55
    use JournalServiceTrait;
0 ignored issues
show
introduced by
The trait FireflyIII\Services\Inte...ort\JournalServiceTrait requires some properties which are not provided by FireflyIII\Factory\TransactionJournalFactory: $name, $transactionType, $id, $accountType, $type
Loading history...
56
57
    /** @var AccountRepositoryInterface */
58
    private $accountRepository;
59
    /** @var AccountValidator */
60
    private $accountValidator;
61
    /** @var BillRepositoryInterface */
62
    private $billRepository;
63
    /** @var CurrencyRepositoryInterface */
64
    private $currencyRepository;
65
    /** @var array */
66
    private $fields;
67
    /** @var PiggyBankEventFactory */
68
    private $piggyEventFactory;
69
    /** @var PiggyBankRepositoryInterface */
70
    private $piggyRepository;
71
    /** @var TransactionFactory */
72
    private $transactionFactory;
73
    /** @var TransactionTypeRepositoryInterface */
74
    private $typeRepository;
75
    /** @var User The user */
76
    private $user;
77
    /** @var bool */
78
    private $errorOnHash;
79
80
    /**
81
     * Constructor.
82
     *
83
     * @throws Exception
84
     * @codeCoverageIgnore
85
     */
86
    public function __construct()
87
    {
88
        $this->errorOnHash = false;
89
        $this->fields      = [
90
            // sepa
91
            'sepa_cc', 'sepa_ct_op', 'sepa_ct_id',
92
            'sepa_db', 'sepa_country', 'sepa_ep',
93
            'sepa_ci', 'sepa_batch_id',
94
95
            // dates
96
            'interest_date', 'book_date', 'process_date',
97
            'due_date', 'payment_date', 'invoice_date',
98
99
            // others
100
            'recurrence_id', 'internal_reference', 'bunq_payment_id',
101
            'import_hash', 'import_hash_v2', 'external_id', 'original_source'];
102
103
104
        if ('testing' === config('app.env')) {
105
            Log::warning(sprintf('%s should not be instantiated in the TEST environment!', get_class($this)));
106
        }
107
108
        $this->currencyRepository = app(CurrencyRepositoryInterface::class);
109
        $this->typeRepository     = app(TransactionTypeRepositoryInterface::class);
110
        $this->transactionFactory = app(TransactionFactory::class);
111
        $this->billRepository     = app(BillRepositoryInterface::class);
112
        $this->budgetRepository   = app(BudgetRepositoryInterface::class);
113
        $this->categoryRepository = app(CategoryRepositoryInterface::class);
114
        $this->piggyRepository    = app(PiggyBankRepositoryInterface::class);
115
        $this->piggyEventFactory  = app(PiggyBankEventFactory::class);
116
        $this->tagFactory         = app(TagFactory::class);
117
        $this->accountValidator   = app(AccountValidator::class);
118
        $this->accountRepository  = app(AccountRepositoryInterface::class);
119
    }
120
121
    /**
122
     * Store a new (set of) transaction journals.
123
     *
124
     * @param array $data
125
     *
126
     * @return Collection
127
     * @throws DuplicateTransactionException
128
     */
129
    public function create(array $data): Collection
130
    {
131
        // convert to special object.
132
        $data = new NullArrayObject($data);
133
134
        Log::debug('Start of TransactionJournalFactory::create()');
135
        $collection   = new Collection;
136
        $transactions = $data['transactions'] ?? [];
137
        if (0 === count($transactions)) {
138
            Log::error('There are no transactions in the array, the TransactionJournalFactory cannot continue.');
139
140
            return new Collection;
141
        }
142
143
        /** @var array $row */
144
        foreach ($transactions as $index => $row) {
145
            Log::debug(sprintf('Now creating journal %d/%d', $index + 1, count($transactions)));
146
147
            Log::debug('Going to call createJournal', $row);
148
            $journal = $this->createJournal(new NullArrayObject($row));
149
            if (null !== $journal) {
150
                $collection->push($journal);
151
            }
152
            if (null === $journal) {
153
                Log::error('The createJournal() method returned NULL. This may indicate an error.');
154
            }
155
        }
156
157
        return $collection;
158
    }
159
160
    /**
161
     * Set the user.
162
     *
163
     * @param User $user
164
     */
165
    public function setUser(User $user): void
166
    {
167
        $this->user = $user;
168
        $this->currencyRepository->setUser($this->user);
169
        $this->tagFactory->setUser($user);
170
        $this->transactionFactory->setUser($this->user);
171
        $this->billRepository->setUser($this->user);
172
        $this->budgetRepository->setUser($this->user);
173
        $this->categoryRepository->setUser($this->user);
174
        $this->piggyRepository->setUser($this->user);
175
        $this->accountRepository->setUser($this->user);
176
    }
177
178
    /**
179
     * @param TransactionJournal $journal
180
     * @param NullArrayObject    $data
181
     * @param string             $field
182
     */
183
    protected function storeMeta(TransactionJournal $journal, NullArrayObject $data, string $field): void
184
    {
185
        $set = [
186
            'journal' => $journal,
187
            'name'    => $field,
188
            'data'    => (string)($data[$field] ?? ''),
189
        ];
190
191
        Log::debug(sprintf('Going to store meta-field "%s", with value "%s".', $set['name'], $set['data']));
192
193
        /** @var TransactionJournalMetaFactory $factory */
194
        $factory = app(TransactionJournalMetaFactory::class);
195
        $factory->updateOrCreate($set);
196
    }
197
198
    /**
199
     * @param NullArrayObject $row
200
     *
201
     * @return TransactionJournal|null
202
     * @throws Exception
203
     * @throws DuplicateTransactionException
204
     */
205
    private function createJournal(NullArrayObject $row): ?TransactionJournal
206
    {
207
        $row['import_hash_v2'] = $this->hashArray($row);
208
209
        $this->errorIfDuplicate($row['import_hash_v2']);
0 ignored issues
show
Bug introduced by
It seems like $row['import_hash_v2'] can also be of type null; however, parameter $hash of FireflyIII\Factory\Trans...ory::errorIfDuplicate() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

209
        $this->errorIfDuplicate(/** @scrutinizer ignore-type */ $row['import_hash_v2']);
Loading history...
210
211
        /** Some basic fields */
212
        $type            = $this->typeRepository->findTransactionType(null, $row['type']);
213
        $carbon          = $row['date'] ?? new Carbon;
214
        $order           = $row['order'] ?? 0;
215
        $currency        = $this->currencyRepository->findCurrency((int)$row['currency_id'], $row['currency_code']);
216
        $foreignCurrency = $this->currencyRepository->findCurrencyNull($row['foreign_currency_id'], $row['foreign_currency_code']);
217
        $bill            = $this->billRepository->findBill((int)$row['bill_id'], $row['bill_name']);
218
        $billId          = TransactionType::WITHDRAWAL === $type->type && null !== $bill ? $bill->id : null;
219
        $description     = app('steam')->cleanString((string)$row['description']);
0 ignored issues
show
Bug introduced by
The method cleanString() does not exist on FireflyIII\Support\Facades\Steam. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

219
        $description     = app('steam')->/** @scrutinizer ignore-call */ cleanString((string)$row['description']);

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
220
221
        /** Manipulate basic fields */
222
        $carbon->setTimezone(config('app.timezone'));
223
224
        /** Get source + destination account */
225
        Log::debug(sprintf('Currency is #%d (%s)', $currency->id, $currency->code));
226
227
        try {
228
            // validate source and destination using a new Validator.
229
            $this->validateAccounts($row);
230
            /** create or get source and destination accounts  */
231
232
            $sourceInfo = [
233
                'id'     => (int)$row['source_id'],
234
                'name'   => $row['source_name'],
235
                'iban'   => $row['source_iban'],
236
                'number' => $row['source_number'],
237
                'bic'    => $row['source_bic'],
238
            ];
239
240
            $destInfo = [
241
                'id'     => (int)$row['destination_id'],
242
                'name'   => $row['destination_name'],
243
                'iban'   => $row['destination_iban'],
244
                'number' => $row['destination_number'],
245
                'bic'    => $row['destination_bic'],
246
            ];
247
            Log::debug('Source info:', $sourceInfo);
248
            Log::debug('Destination info:', $destInfo);
249
250
            $sourceAccount      = $this->getAccount($type->type, 'source', $sourceInfo);
251
            $destinationAccount = $this->getAccount($type->type, 'destination', $destInfo);
252
            // @codeCoverageIgnoreStart
253
        } catch (FireflyException $e) {
254
            Log::error('Could not validate source or destination.');
255
            Log::error($e->getMessage());
256
257
            return null;
258
        }
259
        // @codeCoverageIgnoreEnd
260
261
        // TODO AFTER 4.8,0 better handling below:
262
263
        /** double check currencies. */
264
        $sourceCurrency        = $currency;
265
        $destCurrency          = $currency;
266
        $sourceForeignCurrency = $foreignCurrency;
267
        $destForeignCurrency   = $foreignCurrency;
268
269
        if (TransactionType::WITHDRAWAL === $type->type) {
270
            // make sure currency is correct.
271
            $currency = $this->getCurrency($currency, $sourceAccount);
272
            // make sure foreign currency != currency.
273
            if (null !== $foreignCurrency && $foreignCurrency->id === $currency->id) {
274
                $foreignCurrency = null;
275
            }
276
            $sourceCurrency        = $currency;
277
            $destCurrency          = $currency;
278
            $sourceForeignCurrency = $foreignCurrency;
279
            $destForeignCurrency   = $foreignCurrency;
280
        }
281
        if (TransactionType::DEPOSIT === $type->type) {
282
            // make sure currency is correct.
283
            $currency = $this->getCurrency($currency, $destinationAccount);
284
            // make sure foreign currency != currency.
285
            if (null !== $foreignCurrency && $foreignCurrency->id === $currency->id) {
286
                $foreignCurrency = null;
287
            }
288
289
            $sourceCurrency        = $currency;
290
            $destCurrency          = $currency;
291
            $sourceForeignCurrency = $foreignCurrency;
292
            $destForeignCurrency   = $foreignCurrency;
293
        }
294
295
        if (TransactionType::TRANSFER === $type->type) {
296
            // get currencies
297
            $currency        = $this->getCurrency($currency, $sourceAccount);
298
            $foreignCurrency = $this->getCurrency($foreignCurrency, $destinationAccount);
299
300
            $sourceCurrency        = $currency;
301
            $destCurrency          = $currency;
302
            $sourceForeignCurrency = $foreignCurrency;
303
            $destForeignCurrency   = $foreignCurrency;
304
        }
305
306
        $description = '' === $description ? '(empty description)' : $description;
307
        $description = substr($description, 0, 255);
308
309
310
        /** Create a basic journal. */
311
        $journal = TransactionJournal::create(
312
            [
313
                'user_id'                 => $this->user->id,
314
                'transaction_type_id'     => $type->id,
315
                'bill_id'                 => $billId,
316
                'transaction_currency_id' => $currency->id,
317
                'description'             => substr($description,0,1000),
318
                'date'                    => $carbon->format('Y-m-d H:i:s'),
319
                'order'                   => $order,
320
                'tag_count'               => 0,
321
                'completed'               => 0,
322
            ]
323
        );
324
        Log::debug(sprintf('Created new journal #%d: "%s"', $journal->id, $journal->description));
325
326
        /** Create two transactions. */
327
        /** @var TransactionFactory $transactionFactory */
328
        $transactionFactory = app(TransactionFactory::class);
329
        $transactionFactory->setUser($this->user);
330
        $transactionFactory->setJournal($journal);
331
        $transactionFactory->setAccount($sourceAccount);
332
        $transactionFactory->setCurrency($sourceCurrency);
333
        $transactionFactory->setForeignCurrency($sourceForeignCurrency);
334
        $transactionFactory->setReconciled($row['reconciled'] ?? false);
335
        $transactionFactory->createNegative((string)$row['amount'], $row['foreign_amount']);
336
337
        // and the destination one:
338
        /** @var TransactionFactory $transactionFactory */
339
        $transactionFactory = app(TransactionFactory::class);
340
        $transactionFactory->setUser($this->user);
341
        $transactionFactory->setJournal($journal);
342
        $transactionFactory->setAccount($destinationAccount);
343
        $transactionFactory->setCurrency($destCurrency);
344
        $transactionFactory->setForeignCurrency($destForeignCurrency);
345
        $transactionFactory->setReconciled($row['reconciled'] ?? false);
346
        $transactionFactory->createPositive((string)$row['amount'], $row['foreign_amount']);
347
348
        // verify that journal has two transactions. Otherwise, delete and cancel.
349
        // TODO this can't be faked so it can't be tested.
350
        //        $count = $journal->transactions()->count();
351
        //        if (2 !== $count) {
352
        //            // @codeCoverageIgnoreStart
353
        //            Log::error(sprintf('The journal unexpectedly has %d transaction(s). This is not OK. Cancel operation.', $count));
354
        //            try {
355
        //                $journal->delete();
356
        //            } catch (Exception $e) {
357
        //                Log::debug(sprintf('Dont care: %s.', $e->getMessage()));
358
        //            }
359
        //
360
        //            return null;
361
        //            // @codeCoverageIgnoreEnd
362
        //        }
363
        $journal->completed = true;
364
        $journal->save();
365
366
        /** Link all other data to the journal. */
367
368
        /** Link budget */
369
        $this->storeBudget($journal, $row);
370
371
        /** Link category */
372
        $this->storeCategory($journal, $row);
373
374
        /** Set notes */
375
        $this->storeNotes($journal, $row['notes']);
376
377
        /** Set piggy bank */
378
        $this->storePiggyEvent($journal, $row);
379
380
        /** Set tags */
381
        $this->storeTags($journal, $row['tags']);
382
383
        /** set all meta fields */
384
        $this->storeMetaFields($journal, $row);
385
386
        return $journal;
387
    }
388
389
    /**
390
     * If this transaction already exists, throw an error.
391
     *
392
     * @param string $hash
393
     *
394
     * @throws DuplicateTransactionException
395
     */
396
    private function errorIfDuplicate(string $hash): void
397
    {
398
        if (false === $this->errorOnHash) {
399
            return;
400
        }
401
        $result = null;
402
        if ($this->errorOnHash) {
403
            /** @var TransactionJournalMeta $result */
404
            $result = TransactionJournalMeta::where('data', json_encode($hash, JSON_THROW_ON_ERROR))
405
                                            ->with(['transactionJournal', 'transactionJournal.transactionGroup'])
406
                                            ->first();
407
        }
408
        if (null !== $result) {
409
            throw new DuplicateTransactionException(sprintf('Duplicate of transaction #%d.', $result->transactionJournal->transaction_group_id));
410
        }
411
    }
412
413
    /**
414
     * @param TransactionCurrency|null $currency
415
     * @param Account                  $account
416
     *
417
     * @return TransactionCurrency
418
     */
419
    private function getCurrency(?TransactionCurrency $currency, Account $account): TransactionCurrency
420
    {
421
        $preference = $this->accountRepository->getAccountCurrency($account);
422
        if (null === $preference && null === $currency) {
423
            // return user's default:
424
            return app('amount')->getDefaultCurrencyByUser($this->user);
425
        }
426
        $result = $preference ?? $currency;
427
        Log::debug(sprintf('Currency is now #%d (%s) because of account #%d (%s)', $result->id, $result->code, $account->id, $account->name));
428
429
        return $result;
430
    }
431
432
    /**
433
     * @param NullArrayObject $row
434
     *
435
     * @return string
436
     */
437
    private function hashArray(NullArrayObject $row): string
438
    {
439
        $dataRow = $row->getArrayCopy();
440
441
        unset($dataRow['import_hash_v2'], $dataRow['original_source']);
442
        $json = json_encode($dataRow);
443
        if (false === $json) {
444
            // @codeCoverageIgnoreStart
445
            $json = json_encode((string)microtime());
446
            Log::error(sprintf('Could not hash the original row! %s', json_last_error_msg()), $dataRow);
447
            // @codeCoverageIgnoreEnd
448
        }
449
        $hash = hash('sha256', $json);
450
        Log::debug(sprintf('The hash is: %s', $hash));
451
452
        return $hash;
453
    }
454
455
    /**
456
     * @param TransactionJournal $journal
457
     * @param NullArrayObject    $transaction
458
     */
459
    private function storeMetaFields(TransactionJournal $journal, NullArrayObject $transaction): void
460
    {
461
        foreach ($this->fields as $field) {
462
            $this->storeMeta($journal, $transaction, $field);
463
        }
464
    }
465
466
    /**
467
     * Link a piggy bank to this journal.
468
     *
469
     * @param TransactionJournal $journal
470
     * @param NullArrayObject    $data
471
     */
472
    private function storePiggyEvent(TransactionJournal $journal, NullArrayObject $data): void
473
    {
474
        Log::debug('Will now store piggy event.');
475
        if (!$journal->isTransfer()) {
476
            Log::debug('Journal is not a transfer, do nothing.');
477
478
            return;
479
        }
480
481
        $piggyBank = $this->piggyRepository->findPiggyBank((int)$data['piggy_bank_id'], $data['piggy_bank_name']);
482
483
        if (null !== $piggyBank) {
484
            $this->piggyEventFactory->create($journal, $piggyBank);
485
            Log::debug('Create piggy event.');
486
487
            return;
488
        }
489
        Log::debug('Create no piggy event');
490
    }
491
492
    /**
493
     * @param NullArrayObject $data
494
     *
495
     * @throws FireflyException
496
     */
497
    private function validateAccounts(NullArrayObject $data): void
498
    {
499
        $transactionType = $data['type'] ?? 'invalid';
500
        $this->accountValidator->setUser($this->user);
501
        $this->accountValidator->setTransactionType($transactionType);
502
503
        // validate source account.
504
        $sourceId    = isset($data['source_id']) ? (int)$data['source_id'] : null;
505
        $sourceName  = $data['source_name'] ?? null;
506
        $validSource = $this->accountValidator->validateSource($sourceId, $sourceName);
507
508
        // do something with result:
509
        if (false === $validSource) {
510
            throw new FireflyException(sprintf('Source: %s', $this->accountValidator->sourceError)); // @codeCoverageIgnore
511
        }
512
        Log::debug('Source seems valid.');
513
        // validate destination account
514
        $destinationId    = isset($data['destination_id']) ? (int)$data['destination_id'] : null;
515
        $destinationName  = $data['destination_name'] ?? null;
516
        $validDestination = $this->accountValidator->validateDestination($destinationId, $destinationName);
517
        // do something with result:
518
        if (false === $validDestination) {
519
            throw new FireflyException(sprintf('Destination: %s', $this->accountValidator->destError)); // @codeCoverageIgnore
520
        }
521
    }
522
523
    /**
524
     * @param bool $errorOnHash
525
     */
526
    public function setErrorOnHash(bool $errorOnHash): void
527
    {
528
        $this->errorOnHash = $errorOnHash;
529
    }
530
531
532
}
533