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.
Completed
Push — master ( f7eb78...e47b21 )
by James
13:02 queued 08:20
created

ImportSupport::matchBills()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 17
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 17
rs 9.4285
c 0
b 0
f 0
cc 3
eloc 10
nc 3
nop 1
1
<?php
2
/**
3
 * ImportSupport.php
4
 * Copyright (c) 2017 [email protected]
5
 *
6
 * This file is part of Firefly III.
7
 *
8
 * Firefly III is free software: you can redistribute it and/or modify
9
 * it under the terms of the GNU General Public License as published by
10
 * the Free Software Foundation, either version 3 of the License, or
11
 * (at your option) any later version.
12
 *
13
 * Firefly III 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 General Public License for more details.
17
 *
18
 * You should have received a copy of the GNU General Public License
19
 * along with Firefly III.  If not, see <http://www.gnu.org/licenses/>.
20
 */
21
declare(strict_types=1);
22
23
namespace FireflyIII\Import\Storage;
24
25
use Carbon\Carbon;
26
use Exception;
27
use FireflyIII\Exceptions\FireflyException;
28
use FireflyIII\Import\Object\ImportAccount;
29
use FireflyIII\Import\Object\ImportJournal;
30
use FireflyIII\Models\Account;
31
use FireflyIII\Models\AccountType;
32
use FireflyIII\Models\Bill;
33
use FireflyIII\Models\Budget;
34
use FireflyIII\Models\Category;
35
use FireflyIII\Models\ImportJob;
36
use FireflyIII\Models\Rule;
37
use FireflyIII\Models\Transaction;
38
use FireflyIII\Models\TransactionJournal;
39
use FireflyIII\Models\TransactionJournalMeta;
40
use FireflyIII\Models\TransactionType;
41
use FireflyIII\Repositories\Bill\BillRepositoryInterface;
42
use FireflyIII\Repositories\Tag\TagRepositoryInterface;
43
use FireflyIII\TransactionRules\Processor;
44
use Illuminate\Database\Query\JoinClause;
45
use Illuminate\Support\Collection;
46
use Log;
47
48
/**
49
 * Trait ImportSupport.
50
 */
51
trait ImportSupport
52
{
53
    /** @var BillRepositoryInterface */
54
    protected $billRepository;
55
    /** @var Collection */
56
    protected $bills;
57
    /** @var int */
58
    protected $defaultCurrencyId = 1;
59
    /** @var ImportJob */
60
    protected $job;
61
    /** @var Collection */
62
    protected $rules;
63
64
    /**
65
     * @param TransactionJournal $journal
66
     *
67
     * @return bool
68
     */
69
    protected function applyRules(TransactionJournal $journal): bool
70
    {
71
        if ($this->rules->count() > 0) {
72
            $this->rules->each(
73
                function (Rule $rule) use ($journal) {
74
                    Log::debug(sprintf('Going to apply rule #%d to journal %d.', $rule->id, $journal->id));
75
                    $processor = Processor::make($rule);
76
                    $processor->handleTransactionJournal($journal);
77
                    if ($rule->stop_processing) {
78
                        return false;
79
                    }
80
81
                    return true;
82
                }
83
            );
84
        }
85
86
        return true;
87
    }
88
89
    /**
90
     * @param TransactionJournal $journal
91
     *
92
     * @return bool
93
     */
94
    protected function matchBills(TransactionJournal $journal): bool
95
    {
96
        if(!is_null($journal->bill_id)) {
97
            Log::debug('Journal is already linked to a bill, will not scan.');
98
            return true;
99
        }
100
        if ($this->bills->count() > 0) {
101
            $this->bills->each(
102
                function (Bill $bill) use ($journal) {
103
                    Log::debug(sprintf('Going to match bill #%d to journal %d.', $bill->id, $journal->id));
104
                    $this->billRepository->scan($bill, $journal);
105
                }
106
            );
107
        }
108
109
        return true;
110
    }
111
112
    /**
113
     * @param array $parameters
114
     *
115
     * @return bool
116
     *
117
     * @throws FireflyException
118
     */
119
    private function createTransaction(array $parameters): bool
120
    {
121
        $transaction                          = new Transaction;
122
        $transaction->account_id              = $parameters['account'];
123
        $transaction->transaction_journal_id  = $parameters['id'];
124
        $transaction->transaction_currency_id = $parameters['currency'];
125
        $transaction->amount                  = $parameters['amount'];
126
        $transaction->foreign_currency_id     = $parameters['foreign_currency'];
127
        $transaction->foreign_amount          = $parameters['foreign_amount'];
128
        $transaction->save();
129
        if (null === $transaction->id) {
130
            $errorText = join(', ', $transaction->getErrors()->all());
131
            throw new FireflyException($errorText);
132
        }
133
        Log::debug(sprintf('Created transaction with ID #%d, account #%d, amount %s', $transaction->id, $parameters['account'], $parameters['amount']));
134
135
        return true;
136
    }
137
138
    /**
139
     * @return Collection
140
     */
141
    private function getBills(): Collection
142
    {
143
        $set = Bill::where('user_id', $this->job->user->id)->where('active', 1)->where('automatch', 1)->get(['bills.*']);
144
        Log::debug(sprintf('Found %d user bills.', $set->count()));
145
146
        return $set;
147
    }
148
149
    /**
150
     * This method finds out what the import journal's currency should be. The account itself
151
     * is favoured (and usually it stops there). If no preference is found, the journal has a say
152
     * and thirdly the default currency is used.
153
     *
154
     * @param ImportJournal $importJournal
155
     *
156
     * @return int
157
     */
158
    private function getCurrencyId(ImportJournal $importJournal): int
159
    {
160
        // start with currency pref of account, if any:
161
        $account    = $importJournal->asset->getAccount();
162
        $currencyId = intval($account->getMeta('currency_id'));
163
        if ($currencyId > 0) {
164
            return $currencyId;
165
        }
166
167
        // use given currency
168
        $currency = $importJournal->currency->getTransactionCurrency();
169
        if (null !== $currency->id) {
170
            return $currency->id;
171
        }
172
173
        // backup to default
174
        $currency = $this->defaultCurrencyId;
175
176
        return $currency;
177
    }
178
179
    /**
180
     * The foreign currency is only returned when the journal has a different value from the
181
     * currency id (see other method).
182
     *
183
     * @param ImportJournal $importJournal
184
     * @param int           $currencyId
185
     *
186
     * @see ImportSupport::getCurrencyId
187
     *
188
     * @return int|null
189
     */
190
    private function getForeignCurrencyId(ImportJournal $importJournal, int $currencyId): ?int
191
    {
192
        // use given currency by import journal.
193
        $currency = $importJournal->currency->getTransactionCurrency();
194
        if (null !== $currency->id && $currency->id !== $currencyId) {
195
            return $currency->id;
196
        }
197
198
        // return null, because no different:
199
        return null;
200
    }
201
202
    /**
203
     * The search for the opposing account is complex. Firstly, we forbid the ImportAccount to resolve into the asset
204
     * account to prevent a situation where the transaction flows from A to A. Given the amount, we "expect" the opposing
205
     * account to be an expense or a revenue account. However, the mapping given by the user may return something else
206
     * entirely (usually an asset account). So whatever the expectation, the result may be anything.
207
     *
208
     * When the result does not match the expected type (a negative amount cannot be linked to a revenue account) the next step
209
     * will return an error.
210
     *
211
     * @param ImportAccount $account
212
     * @param int           $forbiddenAccount
213
     * @param string        $amount
214
     *
215
     * @see ImportSupport::getTransactionType
216
     *
217
     * @return Account
218
     */
219
    private function getOpposingAccount(ImportAccount $account, int $forbiddenAccount, string $amount): Account
220
    {
221
        $account->setForbiddenAccountId($forbiddenAccount);
222
        if (bccomp($amount, '0') === -1) {
223
            Log::debug(sprintf('%s is negative, create opposing expense account.', $amount));
224
            $account->setExpectedType(AccountType::EXPENSE);
225
226
            return $account->getAccount();
227
        }
228
        Log::debug(sprintf('%s is positive, create opposing revenue account.', $amount));
229
        // amount is positive, it's a deposit, opposing is an revenue:
230
        $account->setExpectedType(AccountType::REVENUE);
231
232
        $databaseAccount = $account->getAccount();
233
234
        return $databaseAccount;
235
    }
236
237
    /**
238
     * @return Collection
239
     */
240
    private function getRules(): Collection
241
    {
242
        $set = Rule::distinct()
243
                   ->where('rules.user_id', $this->job->user->id)
244
                   ->leftJoin('rule_groups', 'rule_groups.id', '=', 'rules.rule_group_id')
245
                   ->leftJoin('rule_triggers', 'rules.id', '=', 'rule_triggers.rule_id')
246
                   ->where('rule_groups.active', 1)
247
                   ->where('rule_triggers.trigger_type', 'user_action')
248
                   ->where('rule_triggers.trigger_value', 'store-journal')
249
                   ->where('rules.active', 1)
250
                   ->orderBy('rule_groups.order', 'ASC')
251
                   ->orderBy('rules.order', 'ASC')
252
                   ->get(['rules.*', 'rule_groups.order']);
253
        Log::debug(sprintf('Found %d user rules.', $set->count()));
254
255
        return $set;
256
    }
257
258
    /**
259
     * Given the amount and the opposing account its easy to define which kind of transaction type should be associated with the new
260
     * import. This may however fail when there is an unexpected mismatch between the transaction type and the opposing account.
261
     *
262
     * @param string  $amount
263
     * @param Account $account
264
     *
265
     * @return string
266
     *x
267
     * @throws FireflyException
268
     *
269
     * @see ImportSupport::getOpposingAccount()
270
     */
271
    private function getTransactionType(string $amount, Account $account): string
272
    {
273
        $transactionType = TransactionType::WITHDRAWAL;
274
        // amount is negative, it's a withdrawal, opposing is an expense:
275
        if (bccomp($amount, '0') === -1) {
276
            $transactionType = TransactionType::WITHDRAWAL;
277
        }
278
279
        if (1 === bccomp($amount, '0')) {
280
            $transactionType = TransactionType::DEPOSIT;
281
        }
282
283
        // if opposing is an asset account, it's a transfer:
284
        if (AccountType::ASSET === $account->accountType->type) {
285
            Log::debug(sprintf('Opposing account #%d %s is an asset account, make transfer.', $account->id, $account->name));
286
            $transactionType = TransactionType::TRANSFER;
287
        }
288
289
        // verify that opposing account is of the correct type:
290
        if (AccountType::EXPENSE === $account->accountType->type && TransactionType::WITHDRAWAL !== $transactionType) {
291
            $message = 'This row is imported as a withdrawal but opposing is an expense account. This cannot be!';
292
            Log::error($message);
293
            throw new FireflyException($message);
294
        }
295
296
        return $transactionType;
297
    }
298
299
    /**
300
     * This method returns a collection of the current transfers in the system and some meta data for
301
     * this set. This can later be used to see if the journal that firefly is trying to import
302
     * is not already present.
303
     *
304
     * @return array
305
     */
306
    private function getTransfers(): array
307
    {
308
        $set   = TransactionJournal::leftJoin('transaction_types', 'transaction_types.id', '=', 'transaction_journals.transaction_type_id')
309
                                   ->leftJoin(
310
                                       'transactions AS source',
311
                                       function (JoinClause $join) {
312
                                           $join->on('transaction_journals.id', '=', 'source.transaction_journal_id')->where('source.amount', '<', 0);
313
                                       }
314
                                   )
315
                                   ->leftJoin(
316
                                       'transactions AS destination',
317
                                       function (JoinClause $join) {
318
                                           $join->on('transaction_journals.id', '=', 'destination.transaction_journal_id')->where(
319
                                               'destination.amount',
320
                                               '>',
321
                                               0
322
                                           );
323
                                       }
324
                                   )
325
                                   ->leftJoin('accounts as source_accounts', 'source.account_id', '=', 'source_accounts.id')
326
                                   ->leftJoin('accounts as destination_accounts', 'destination.account_id', '=', 'destination_accounts.id')
327
                                   ->where('transaction_journals.user_id', $this->job->user_id)
328
                                   ->where('transaction_types.type', TransactionType::TRANSFER)
329
                                   ->get(
330
                                       ['transaction_journals.id', 'transaction_journals.encrypted', 'transaction_journals.description',
331
                                        'source_accounts.name as source_name', 'destination_accounts.name as destination_name', 'destination.amount',
332
                                        'transaction_journals.date',]
333
                                   );
334
        $array = [];
335
        /** @var TransactionJournal $entry */
336
        foreach ($set as $entry) {
337
            $original = [app('steam')->tryDecrypt($entry->source_name), app('steam')->tryDecrypt($entry->destination_name)];
338
            sort($original);
339
            $array[] = [
340
                'names'       => $original,
341
                'amount'      => $entry->amount,
342
                'date'        => $entry->date->format('Y-m-d'),
343
                'description' => $entry->description,
344
            ];
345
        }
346
347
        return $array;
348
    }
349
350
    /**
351
     * Checks if the import journal has not been imported before.
352
     *
353
     * @param string $hash
354
     *
355
     * @return bool
356
     */
357
    private function hashAlreadyImported(string $hash): bool
358
    {
359
        $json = json_encode($hash);
360
        /** @var TransactionJournalMeta $entry */
361
        $entry = TransactionJournalMeta::leftJoin('transaction_journals', 'transaction_journals.id', '=', 'journal_meta.transaction_journal_id')
362
                                       ->where('data', $json)
363
                                       ->where('name', 'importHash')
364
                                       ->first();
365
        if (null !== $entry) {
366
            Log::error(sprintf('A journal with hash %s has already been imported (spoiler: it\'s journal #%d)', $hash, $entry->transaction_journal_id));
367
368
            return true;
369
        }
370
371
        return false;
372
    }
373
374
    /**
375
     * @param TransactionJournal $journal
376
     * @param Bill               $bill
377
     */
378
    private function storeBill(TransactionJournal $journal, Bill $bill)
379
    {
380
        if (null !== $bill->id) {
381
            Log::debug(sprintf('Linked bill #%d to journal #%d', $bill->id, $journal->id));
382
            $journal->bill()->associate($bill);
383
            $journal->save();
384
        }
385
    }
386
387
    /**
388
     * @param TransactionJournal $journal
389
     * @param Budget             $budget
390
     */
391
    private function storeBudget(TransactionJournal $journal, Budget $budget)
392
    {
393
        if (null !== $budget->id) {
394
            Log::debug(sprintf('Linked budget #%d to journal #%d', $budget->id, $journal->id));
395
            $journal->budgets()->save($budget);
396
        }
397
    }
398
399
    /**
400
     * @param TransactionJournal $journal
401
     * @param Category           $category
402
     */
403
    private function storeCategory(TransactionJournal $journal, Category $category)
404
    {
405
        if (null !== $category->id) {
406
            Log::debug(sprintf('Linked category #%d to journal #%d', $category->id, $journal->id));
407
            $journal->categories()->save($category);
408
        }
409
    }
410
411
    private function storeJournal(array $parameters): TransactionJournal
412
    {
413
        // find transaction type:
414
        $transactionType = TransactionType::whereType($parameters['type'])->first();
415
416
        // create a journal:
417
        $journal                          = new TransactionJournal;
418
        $journal->user_id                 = $this->job->user_id;
419
        $journal->transaction_type_id     = $transactionType->id;
420
        $journal->transaction_currency_id = $parameters['currency'];
421
        $journal->description             = $parameters['description'];
422
        $journal->date                    = $parameters['date'];
423
        $journal->order                   = 0;
424
        $journal->tag_count               = 0;
425
        $journal->completed               = false;
426
427
        if (!$journal->save()) {
428
            $errorText = join(', ', $journal->getErrors()->all());
429
            // add three steps:
430
            $this->job->addStepsDone(3);
431
            // throw error
432
            throw new FireflyException($errorText);
433
        }
434
        // save meta data:
435
        $journal->setMeta('importHash', $parameters['hash']);
436
        Log::debug(sprintf('Created journal with ID #%d', $journal->id));
437
438
        // create transactions:
439
        $one      = [
440
            'id'               => $journal->id,
441
            'account'          => $parameters['asset']->id,
442
            'currency'         => $parameters['currency'],
443
            'amount'           => $parameters['amount'],
444
            'foreign_currency' => $parameters['foreign_currency'],
445
            'foreign_amount'   => null === $parameters['foreign_currency'] ? null : $parameters['amount'],
446
        ];
447
        $opposite = app('steam')->opposite($parameters['amount']);
448
        $two      = [
449
            'id'               => $journal->id,
450
            'account'          => $parameters['opposing']->id,
451
            'currency'         => $parameters['currency'],
452
            'amount'           => $opposite,
453
            'foreign_currency' => $parameters['foreign_currency'],
454
            'foreign_amount'   => null === $parameters['foreign_currency'] ? null : $opposite,
455
        ];
456
        $this->createTransaction($one);
457
        $this->createTransaction($two);
458
459
        return $journal;
460
    }
461
462
    /**
463
     * @param TransactionJournal $journal
464
     * @param array              $dates
465
     */
466
    private function storeMeta(TransactionJournal $journal, array $dates)
467
    {
468
        // all other date fields as meta thing:
469
        foreach ($dates as $name => $value) {
470
            try {
471
                $date = new Carbon($value);
472
                $journal->setMeta($name, $date);
473
            } catch (Exception $e) {
474
                // don't care, ignore:
475
                Log::warning(sprintf('Could not parse "%s" into a valid Date object for field %s', $value, $name));
476
            }
477
        }
478
    }
479
480
    /**
481
     * @param array              $tags
482
     * @param TransactionJournal $journal
483
     */
484
    private function storeTags(array $tags, TransactionJournal $journal): void
485
    {
486
        $repository = app(TagRepositoryInterface::class);
487
        $repository->setUser($journal->user);
488
489
        foreach ($tags as $tag) {
490
            $dbTag = $repository->findByTag($tag);
491
            if (null === $dbTag->id) {
492
                $dbTag = $repository->store(
493
                    ['tag'       => $tag, 'date' => null, 'description' => null, 'latitude' => null, 'longitude' => null,
494
                     'zoomLevel' => null, 'tagMode' => 'nothing',]
495
                );
496
            }
497
            $journal->tags()->save($dbTag);
498
            Log::debug(sprintf('Linked tag %d ("%s") to journal #%d', $dbTag->id, $dbTag->tag, $journal->id));
499
        }
500
501
        return;
502
    }
503
}
504