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 ( 37b02e...ebbbe1 )
by James
08:59
created

app/Import/Routine/BunqRoutine.php (1 issue)

Labels
Severity
1
<?php
2
/**
3
 * BunqRoutine.php
4
 * Copyright (c) 2018 [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
22
declare(strict_types=1);
23
24
namespace FireflyIII\Import\Routine;
25
26
use Carbon\Carbon;
27
use DB;
28
use Exception;
29
use FireflyIII\Exceptions\FireflyException;
30
use FireflyIII\Factory\AccountFactory;
31
use FireflyIII\Factory\TransactionJournalFactory;
32
use FireflyIII\Models\Account;
33
use FireflyIII\Models\AccountType;
34
use FireflyIII\Models\ImportJob;
35
use FireflyIII\Models\TransactionJournalMeta;
36
use FireflyIII\Models\TransactionType;
37
use FireflyIII\Repositories\Account\AccountRepositoryInterface;
38
use FireflyIII\Repositories\ImportJob\ImportJobRepositoryInterface;
39
use FireflyIII\Repositories\Tag\TagRepositoryInterface;
40
use FireflyIII\Services\Bunq\Id\DeviceServerId;
41
use FireflyIII\Services\Bunq\Object\DeviceServer;
42
use FireflyIII\Services\Bunq\Object\LabelMonetaryAccount;
43
use FireflyIII\Services\Bunq\Object\MonetaryAccountBank;
44
use FireflyIII\Services\Bunq\Object\Payment;
45
use FireflyIII\Services\Bunq\Object\ServerPublicKey;
46
use FireflyIII\Services\Bunq\Object\UserCompany;
47
use FireflyIII\Services\Bunq\Object\UserPerson;
48
use FireflyIII\Services\Bunq\Request\DeviceServerRequest;
49
use FireflyIII\Services\Bunq\Request\DeviceSessionRequest;
50
use FireflyIII\Services\Bunq\Request\InstallationTokenRequest;
51
use FireflyIII\Services\Bunq\Request\ListDeviceServerRequest;
52
use FireflyIII\Services\Bunq\Request\ListMonetaryAccountRequest;
53
use FireflyIII\Services\Bunq\Request\ListPaymentRequest;
54
use FireflyIII\Services\Bunq\Token\InstallationToken;
55
use FireflyIII\Services\Bunq\Token\SessionToken;
56
use Illuminate\Support\Collection;
57
use Log;
58
use Preferences;
59
use Requests;
60
61
/**
62
 * Class BunqRoutine
63
 *
64
 * Steps before import:
65
 * 1) register device complete.
66
 *
67
 * Stage: 'initial'.
68
 *
69
 * 1) Get an installation token (if not present)
70
 * 2) Register device (if not found)
71
 *
72
 * Stage 'registered'
73
 *
74
 * 1) Get a session token. (new session)
75
 * 2) store user person / user company
76
 *
77
 * Stage 'logged-in'
78
 *
79
 * Get list of bank accounts
80
 *
81
 * Stage 'have-accounts'
82
 *
83
 * Map accounts to existing accounts
84
 *
85
 * Stage 'do-import'?
86
 */
87
class BunqRoutine implements RoutineInterface
88
{
89
    /** @var Collection */
90
    public $errors;
91
    /** @var Collection */
92
    public $journals;
93
    /** @var int */
94
    public $lines = 0;
95
    /** @var AccountFactory */
96
    private $accountFactory;
97
    /** @var AccountRepositoryInterface */
98
    private $accountRepository;
99
    /** @var ImportJob */
100
    private $job;
101
    /** @var TransactionJournalFactory */
102
    private $journalFactory;
103
    /** @var ImportJobRepositoryInterface */
104
    private $repository;
105
106
    /**
107
     * ImportRoutine constructor.
108
     */
109
    public function __construct()
110
    {
111
        $this->journals = new Collection;
112
        $this->errors   = new Collection;
113
    }
114
115
    /**
116
     * @return Collection
117
     */
118
    public function getErrors(): Collection
119
    {
120
        return $this->errors;
121
    }
122
123
    /**
124
     * @return Collection
125
     */
126
    public function getJournals(): Collection
127
    {
128
        return $this->journals;
129
    }
130
131
    /**
132
     * @return int
133
     */
134
    public function getLines(): int
135
    {
136
        return $this->lines;
137
    }
138
139
    /**
140
     * @return bool
141
     *
142
     * @throws FireflyException
143
     */
144
    public function run(): bool
145
    {
146
        Log::info(sprintf('Start with import job %s using Bunq.', $this->job->key));
147
        set_time_limit(0);
148
        // this method continues with the job and is called by whenever a stage is
149
        // finished
150
        $this->continueJob();
151
152
        return true;
153
    }
154
155
    /**
156
     * @param ImportJob $job
157
     */
158
    public function setJob(ImportJob $job)
159
    {
160
        $this->job               = $job;
161
        $this->repository        = app(ImportJobRepositoryInterface::class);
162
        $this->accountRepository = app(AccountRepositoryInterface::class);
163
        $this->accountFactory    = app(AccountFactory::class);
164
        $this->journalFactory    = app(TransactionJournalFactory::class);
165
        $this->repository->setUser($job->user);
166
        $this->accountRepository->setUser($job->user);
167
        $this->accountFactory->setUser($job->user);
168
        $this->journalFactory->setUser($job->user);
169
    }
170
171
    /**
172
     * @throws FireflyException
173
     */
174
    protected function continueJob()
175
    {
176
        // if in "configuring"
177
        if ('configuring' === $this->getStatus()) {
178
            Log::debug('Job is in configuring stage, will do nothing.');
179
180
            return;
181
        }
182
        $stage = $this->getConfig()['stage'] ?? 'unknown';
183
        Log::debug(sprintf('Now in continueJob() for stage %s', $stage));
184
        switch ($stage) {
185
            case 'initial':
186
                // register device and get tokens.
187
                $this->runStageInitial();
188
                $this->continueJob();
189
                break;
190
            case 'registered':
191
                // get all bank accounts of user.
192
                $this->runStageRegistered();
193
                $this->continueJob();
194
                break;
195
            case 'logged-in':
196
                $this->runStageLoggedIn();
197
                break;
198
            case 'have-accounts':
199
                // do nothing in this stage. Job should revert to config routine.
200
                break;
201
            case 'have-account-mapping':
202
                $this->setStatus('running');
203
                $this->runStageHaveAccountMapping();
204
205
                break;
206
            default:
207
                throw new FireflyException(sprintf('No action for stage %s!', $stage));
208
                break;
209
        }
210
    }
211
212
    /**
213
     * @throws FireflyException
214
     */
215
    protected function runStageInitial()
216
    {
217
        $this->addStep();
218
        Log::debug('In runStageInitial()');
219
        $this->setStatus('running');
220
221
        // register the device at Bunq:
222
        $serverId = $this->registerDevice();
223
        Log::debug(sprintf('Found device server with id %d', $serverId->getId()));
224
225
        $config          = $this->getConfig();
226
        $config['stage'] = 'registered';
227
        $this->setConfig($config);
228
        $this->addStep();
229
    }
230
231
    /**
232
     * Get a session token + userperson + usercompany. Store it in the job.
233
     *
234
     * @throws FireflyException
235
     */
236
    protected function runStageRegistered(): void
237
    {
238
        $this->addStep();
239
        Log::debug('Now in runStageRegistered()');
240
        $apiKey            = Preferences::getForUser($this->job->user, 'bunq_api_key')->data;
241
        $serverPublicKey   = Preferences::getForUser($this->job->user, 'bunq_server_public_key')->data;
242
        $installationToken = $this->getInstallationToken();
243
        $request           = new DeviceSessionRequest;
244
        $request->setInstallationToken($installationToken);
245
        $request->setPrivateKey($this->getPrivateKey());
246
        $request->setServerPublicKey($serverPublicKey);
247
        $request->setSecret($apiKey);
248
        $request->call();
249
        $this->addStep();
250
251
        Log::debug('Requested new session.');
252
253
        $deviceSession = $request->getDeviceSessionId();
254
        $userPerson    = $request->getUserPerson();
255
        $userCompany   = $request->getUserCompany();
256
        $sessionToken  = $request->getSessionToken();
257
258
        $config                      = $this->getConfig();
259
        $config['device_session_id'] = $deviceSession->toArray();
260
        $config['user_person']       = $userPerson->toArray();
261
        $config['user_company']      = $userCompany->toArray();
262
        $config['session_token']     = $sessionToken->toArray();
263
        $config['stage']             = 'logged-in';
264
        $this->setConfig($config);
265
        $this->addStep();
266
267
        Log::debug('Session stored in job.');
268
269
        return;
270
    }
271
272
    /**
273
     * Shorthand method.
274
     */
275
    private function addStep()
276
    {
277
        $this->addSteps(1);
278
    }
279
280
    /**
281
     * Shorthand method.
282
     *
283
     * @param int $count
284
     */
285
    private function addSteps(int $count)
286
    {
287
        $this->repository->addStepsDone($this->job, $count);
288
    }
289
290
    /**
291
     * Shorthand
292
     *
293
     * @param int $steps
294
     */
295
    private function addTotalSteps(int $steps)
296
    {
297
        $this->repository->addTotalSteps($this->job, $steps);
298
    }
299
300
    /**
301
     * @param int $paymentId
302
     *
303
     * @return bool
304
     */
305
    private function alreadyImported(int $paymentId): bool
306
    {
307
        $count = TransactionJournalMeta::where('name', 'bunq_payment_id')
308
                                       ->where('data', json_encode($paymentId))->count();
309
310
        Log::debug(sprintf('Transaction #%d is %d time(s) in the database.', $paymentId, $count));
311
312
        return $count > 0;
313
    }
314
315
    /**
316
     * @param LabelMonetaryAccount $party
317
     * @param string               $expectedType
318
     *
319
     * @return Account
320
     * @throws \FireflyIII\Exceptions\FireflyException
321
     * @throws FireflyException
322
     */
323
    private function convertToAccount(LabelMonetaryAccount $party, string $expectedType): Account
324
    {
325
        Log::debug('in convertToAccount()');
326
        // find opposing party by IBAN first.
327
        $result = $this->accountRepository->findByIbanNull($party->getIban(), [$expectedType]);
328
        if (null !== $result) {
329
            Log::debug(sprintf('Search for %s resulted in account %s (#%d)', $party->getIban(), $result->name, $result->id));
330
331
            return $result;
332
        }
333
334
        // try to find asset account just in case:
335
        if ($expectedType !== AccountType::ASSET) {
336
            $result = $this->accountRepository->findByIbanNull($party->getIban(), [AccountType::ASSET]);
337
            if (nul !== $result) {
0 ignored issues
show
The constant FireflyIII\Import\Routine\nul was not found. Maybe you did not declare it correctly or list all dependencies?
Loading history...
338
                Log::debug(sprintf('Search for Asset "%s" resulted in account %s (#%d)', $party->getIban(), $result->name, $result->id));
339
340
                return $result;
341
            }
342
        }
343
        // create new account:
344
        $data    = [
345
            'user_id'         => $this->job->user_id,
346
            'iban'            => $party->getIban(),
347
            'name'            => $party->getLabelUser()->getDisplayName(),
348
            'account_type_id' => null,
349
            'accountType'     => $expectedType,
350
            'virtualBalance'  => null,
351
            'active'          => true,
352
353
        ];
354
        $account = $this->accountFactory->create($data);
355
        Log::debug(
356
            sprintf(
357
                'Converted label monetary account %s to %s account %s (#%d)',
358
                $party->getLabelUser()->getDisplayName(),
359
                $expectedType,
360
                $account->name, $account->id
361
            )
362
        );
363
364
        return $account;
365
    }
366
367
    /**
368
     * This method creates a new public/private keypair for the user. This isn't really secure, since the key is generated on the fly with
369
     * no regards for HSM's, smart cards or other things. It would require some low level programming to get this right. But the private key
370
     * is stored encrypted in the database so it's something.
371
     */
372
    private function createKeyPair(): void
373
    {
374
        Log::debug('Now in createKeyPair()');
375
        $private = Preferences::getForUser($this->job->user, 'bunq_private_key', null);
376
        $public  = Preferences::getForUser($this->job->user, 'bunq_public_key', null);
377
378
        if (!(null === $private && null === $public)) {
379
            Log::info('Already have public and private key, return NULL.');
380
381
            return;
382
        }
383
384
        Log::debug('Generate new key pair for user.');
385
        $keyConfig = [
386
            'digest_alg'       => 'sha512',
387
            'private_key_bits' => 2048,
388
            'private_key_type' => OPENSSL_KEYTYPE_RSA,
389
        ];
390
        // Create the private and public key
391
        $res = openssl_pkey_new($keyConfig);
392
393
        // Extract the private key from $res to $privKey
394
        $privKey = '';
395
        openssl_pkey_export($res, $privKey);
396
397
        // Extract the public key from $res to $pubKey
398
        $pubKey = openssl_pkey_get_details($res);
399
400
        Preferences::setForUser($this->job->user, 'bunq_private_key', $privKey);
401
        Preferences::setForUser($this->job->user, 'bunq_public_key', $pubKey['key']);
402
        Log::debug('Created and stored key pair');
403
    }
404
405
    /**
406
     * @return array
407
     */
408
    private function getConfig(): array
409
    {
410
        return $this->repository->getConfiguration($this->job);
411
    }
412
413
    /**
414
     * Try to detect the current device ID (in case this instance has been registered already.
415
     *
416
     * @return DeviceServerId
417
     *
418
     * @throws FireflyException
419
     */
420
    private function getExistingDevice(): ?DeviceServerId
421
    {
422
        Log::debug('Now in getExistingDevice()');
423
        $installationToken = $this->getInstallationToken();
424
        $serverPublicKey   = $this->getServerPublicKey();
425
        $request           = new ListDeviceServerRequest;
426
        $remoteIp          = $this->getRemoteIp();
427
        $request->setInstallationToken($installationToken);
428
        $request->setServerPublicKey($serverPublicKey);
429
        $request->setPrivateKey($this->getPrivateKey());
430
        $request->call();
431
        $devices = $request->getDevices();
432
        /** @var DeviceServer $device */
433
        foreach ($devices as $device) {
434
            if ($device->getIp() === $remoteIp) {
435
                Log::debug(sprintf('This instance is registered as device #%s', $device->getId()->getId()));
436
437
                return $device->getId();
438
            }
439
        }
440
        Log::info('This instance is not yet registered.');
441
442
        return null;
443
    }
444
445
    /**
446
     * Shorthand method.
447
     *
448
     * @return array
449
     */
450
    private function getExtendedStatus(): array
451
    {
452
        return $this->repository->getExtendedStatus($this->job);
453
    }
454
455
    /**
456
     * Get the installation token, either from the users preferences or from Bunq.
457
     *
458
     * @return InstallationToken
459
     *
460
     * @throws FireflyException
461
     */
462
    private function getInstallationToken(): InstallationToken
463
    {
464
        Log::debug('Now in getInstallationToken().');
465
        $token = Preferences::getForUser($this->job->user, 'bunq_installation_token', null);
466
        if (null !== $token) {
467
            Log::debug('Have installation token, return it.');
468
469
            return $token->data;
470
        }
471
        Log::debug('Have no installation token, request one.');
472
473
        // verify bunq api code:
474
        $publicKey = $this->getPublicKey();
475
        $request   = new InstallationTokenRequest;
476
        $request->setPublicKey($publicKey);
477
        $request->call();
478
        Log::debug('Sent request for installation token.');
479
480
        $installationToken = $request->getInstallationToken();
481
        $installationId    = $request->getInstallationId();
482
        $serverPublicKey   = $request->getServerPublicKey();
483
484
        Preferences::setForUser($this->job->user, 'bunq_installation_token', $installationToken);
485
        Preferences::setForUser($this->job->user, 'bunq_installation_id', $installationId);
486
        Preferences::setForUser($this->job->user, 'bunq_server_public_key', $serverPublicKey);
487
488
        Log::debug('Stored token, ID and pub key.');
489
490
        return $installationToken;
491
    }
492
493
    /**
494
     * Get the private key from the users preferences.
495
     *
496
     * @return string
497
     */
498
    private function getPrivateKey(): string
499
    {
500
        Log::debug('In getPrivateKey()');
501
        $preference = Preferences::getForUser($this->job->user, 'bunq_private_key', null);
502
        if (null === $preference) {
503
            Log::debug('private key is null');
504
            // create key pair
505
            $this->createKeyPair();
506
        }
507
        $preference = Preferences::getForUser($this->job->user, 'bunq_private_key', null);
508
        Log::debug('Return private key for user');
509
510
        return $preference->data;
511
    }
512
513
    /**
514
     * Get a public key from the users preferences.
515
     *
516
     * @return string
517
     */
518
    private function getPublicKey(): string
519
    {
520
        Log::debug('Now in getPublicKey()');
521
        $preference = Preferences::getForUser($this->job->user, 'bunq_public_key', null);
522
        if (null === $preference) {
523
            Log::debug('public key is NULL.');
524
            // create key pair
525
            $this->createKeyPair();
526
        }
527
        $preference = Preferences::getForUser($this->job->user, 'bunq_public_key', null);
528
        Log::debug('Return public key for user');
529
530
        return $preference->data;
531
    }
532
533
    /**
534
     * Request users server remote IP. Let's assume this value will not change any time soon.
535
     *
536
     * @return string
537
     *
538
     * @throws FireflyException
539
     */
540
    private function getRemoteIp(): string
541
    {
542
        $preference = Preferences::getForUser($this->job->user, 'external_ip', null);
543
        if (null === $preference) {
544
            try {
545
                $response = Requests::get('https://api.ipify.org');
546
            } catch (Exception $e) {
547
                throw new FireflyException(sprintf('Could not retrieve external IP: %s', $e->getMessage()));
548
            }
549
            if (200 !== $response->status_code) {
550
                throw new FireflyException(sprintf('Could not retrieve external IP: %d %s', $response->status_code, $response->body));
551
            }
552
            $serverIp = $response->body;
553
            Preferences::setForUser($this->job->user, 'external_ip', $serverIp);
554
555
            return $serverIp;
556
        }
557
558
        return $preference->data;
559
    }
560
561
    /**
562
     * Get the public key of the server, necessary to verify server signature.
563
     *
564
     * @return ServerPublicKey
565
     *
566
     * @throws FireflyException
567
     */
568
    private function getServerPublicKey(): ServerPublicKey
569
    {
570
        $pref = Preferences::getForUser($this->job->user, 'bunq_server_public_key', null)->data;
571
        if (null === $pref) {
572
            throw new FireflyException('Cannot determine bunq server public key, but should have it at this point.');
573
        }
574
575
        return $pref;
576
    }
577
578
    /**
579
     * Shorthand method.
580
     *
581
     * @return string
582
     */
583
    private function getStatus(): string
584
    {
585
        return $this->repository->getStatus($this->job);
586
    }
587
588
    /**
589
     * Import the transactions that were found.
590
     *
591
     * @param array $payments
592
     *
593
     * @throws FireflyException
594
     */
595
    private function importPayments(array $payments): void
596
    {
597
        Log::debug('Going to run importPayments()');
598
        $journals = new Collection;
599
        $config   = $this->getConfig();
600
        foreach ($payments as $accountId => $data) {
601
            Log::debug(sprintf('Now running for bunq account #%d with %d payment(s).', $accountId, count($data['payments'])));
602
            /** @var Payment $payment */
603
            foreach ($data['payments'] as $index => $payment) {
604
                Log::debug(sprintf('Now at payment #%d with ID #%d', $index, $payment->getId()));
605
                // store or find counter party:
606
                $counterParty = $payment->getCounterParty();
607
                $amount       = $payment->getAmount();
608
                $paymentId    = $payment->getId();
609
                if ($this->alreadyImported($paymentId)) {
610
                    Log::error(sprintf('Already imported bunq payment with id #%d', $paymentId));
611
612
                    // add three steps to keep up
613
                    $this->addSteps(3);
614
                    continue;
615
                }
616
                Log::debug(sprintf('Amount is %s %s', $amount->getCurrency(), $amount->getValue()));
617
                $expected = AccountType::EXPENSE;
618
                if (bccomp($amount->getValue(), '0') === 1) {
619
                    // amount + means that its a deposit.
620
                    $expected = AccountType::REVENUE;
621
                    Log::debug('Will make opposing account revenue.');
622
                }
623
                $opposing = $this->convertToAccount($counterParty, $expected);
624
                $account  = $this->accountRepository->findNull($config['accounts-mapped'][$accountId]);
625
                $type     = TransactionType::WITHDRAWAL;
626
627
                $this->addStep();
628
629
                Log::debug(sprintf('Will store withdrawal between "%s" (%d) and "%s" (%d)', $account->name, $account->id, $opposing->name, $opposing->id));
630
631
                // start storing stuff:
632
                $source      = $account;
633
                $destination = $opposing;
634
                if (bccomp($amount->getValue(), '0') === 1) {
635
                    // its a deposit:
636
                    $source      = $opposing;
637
                    $destination = $account;
638
                    $type        = TransactionType::DEPOSIT;
639
                    Log::debug('Will make it a deposit.');
640
                }
641
                if ($account->accountType->type === AccountType::ASSET && $opposing->accountType->type === AccountType::ASSET) {
642
                    $type = TransactionType::TRANSFER;
643
                    Log::debug('Both are assets, will make transfer.');
644
                }
645
646
                $storeData = [
647
                    'user'               => $this->job->user_id,
648
                    'type'               => $type,
649
                    'date'               => $payment->getCreated(),
650
                    'description'        => $payment->getDescription(),
651
                    'piggy_bank_id'      => null,
652
                    'piggy_bank_name'    => null,
653
                    'bill_id'            => null,
654
                    'bill_name'          => null,
655
                    'tags'               => [$payment->getType(), $payment->getSubType()],
656
                    'internal_reference' => $payment->getId(),
657
                    'notes'              => null,
658
                    'bunq_payment_id'    => $payment->getId(),
659
                    'transactions'       => [
660
                        // single transaction:
661
                        [
662
                            'description'           => null,
663
                            'amount'                => $amount->getValue(),
664
                            'currency_id'           => null,
665
                            'currency_code'         => $amount->getCurrency(),
666
                            'foreign_amount'        => null,
667
                            'foreign_currency_id'   => null,
668
                            'foreign_currency_code' => null,
669
                            'budget_id'             => null,
670
                            'budget_name'           => null,
671
                            'category_id'           => null,
672
                            'category_name'         => null,
673
                            'source_id'             => $source->id,
674
                            'source_name'           => null,
675
                            'destination_id'        => $destination->id,
676
                            'destination_name'      => null,
677
                            'reconciled'            => false,
678
                            'identifier'            => 0,
679
                        ],
680
                    ],
681
                ];
682
                $journal   = $this->journalFactory->create($storeData);
683
                Log::debug(sprintf('Stored journal with ID #%d', $journal->id));
684
                $this->addStep();
685
                $journals->push($journal);
686
687
            }
688
        }
689
690
        // link to tag
691
        /** @var TagRepositoryInterface $repository */
692
        $repository = app(TagRepositoryInterface::class);
693
        $repository->setUser($this->job->user);
694
        $data            = [
695
            'tag'         => trans('import.import_with_key', ['key' => $this->job->key]),
696
            'date'        => new Carbon,
697
            'description' => null,
698
            'latitude'    => null,
699
            'longitude'   => null,
700
            'zoomLevel'   => null,
701
            'tagMode'     => 'nothing',
702
        ];
703
        $tag             = $repository->store($data);
704
        $extended        = $this->getExtendedStatus();
705
        $extended['tag'] = $tag->id;
706
        $this->setExtendedStatus($extended);
707
708
        Log::debug(sprintf('Created tag #%d ("%s")', $tag->id, $tag->tag));
709
        Log::debug('Looping journals...');
710
        $tagId = $tag->id;
711
712
        foreach ($journals as $journal) {
713
            Log::debug(sprintf('Linking journal #%d to tag #%d...', $journal->id, $tagId));
714
            DB::table('tag_transaction_journal')->insert(['transaction_journal_id' => $journal->id, 'tag_id' => $tagId]);
715
            $this->addStep();
716
        }
717
        Log::info(sprintf('Linked %d journals to tag #%d ("%s")', $journals->count(), $tag->id, $tag->tag));
718
719
        // set status to "finished"?
720
        // update job:
721
        $this->setStatus('finished');
722
    }
723
724
    /**
725
     * To install Firefly III as a new device:
726
     * - Send an installation token request.
727
     * - Use this token to send a device server request
728
     * - Store the installation token
729
     * - Use the installation token each time we need a session.
730
     *
731
     * @throws FireflyException
732
     */
733
    private function registerDevice(): DeviceServerId
734
    {
735
        Log::debug('Now in registerDevice()');
736
        $deviceServerId = Preferences::getForUser($this->job->user, 'bunq_device_server_id', null);
737
        $serverIp       = $this->getRemoteIp();
738
        if (null !== $deviceServerId) {
739
            Log::debug('Already have device server ID.');
740
741
            return $deviceServerId->data;
742
        }
743
744
        Log::debug('Device server ID is null, we have to find an existing one or register a new one.');
745
        $installationToken = $this->getInstallationToken();
746
        $serverPublicKey   = $this->getServerPublicKey();
747
        $apiKey            = Preferences::getForUser($this->job->user, 'bunq_api_key', '');
748
        $this->addStep();
749
750
        // try get the current from a list:
751
        $deviceServerId = $this->getExistingDevice();
752
        $this->addStep();
753
        if (null !== $deviceServerId) {
754
            Log::debug('Found device server ID in existing devices list.');
755
756
            return $deviceServerId;
757
        }
758
759
        Log::debug('Going to create new DeviceServerRequest() because nothing found in existing list.');
760
        $request = new DeviceServerRequest;
761
        $request->setPrivateKey($this->getPrivateKey());
762
        $request->setDescription('Firefly III v' . config('firefly.version') . ' for ' . $this->job->user->email);
763
        $request->setSecret($apiKey->data);
764
        $request->setPermittedIps([$serverIp]);
765
        $request->setInstallationToken($installationToken);
766
        $request->setServerPublicKey($serverPublicKey);
767
        $deviceServerId = null;
768
        // try to register device:
769
        try {
770
            $request->call();
771
            $deviceServerId = $request->getDeviceServerId();
772
        } catch (FireflyException $e) {
773
            Log::error($e->getMessage());
774
            // we really have to quit at this point :(
775
            throw new FireflyException($e->getMessage());
776
        }
777
        if (null === $deviceServerId) {
778
            throw new FireflyException('Was not able to register server with bunq. Please see the log files.');
779
        }
780
781
        Preferences::setForUser($this->job->user, 'bunq_device_server_id', $deviceServerId);
782
        Log::debug(sprintf('Server ID: %s', serialize($deviceServerId)));
783
784
        return $deviceServerId;
785
    }
786
787
    /**
788
     * Will download the transactions for each account that is selected to be imported from.
789
     * Will of course also update the number of steps and what-not.
790
     *
791
     * @throws FireflyException
792
     */
793
    private function runStageHaveAccountMapping(): void
794
    {
795
        $config  = $this->getConfig();
796
        $user    = new UserPerson($config['user_person']);
797
        $mapping = $config['accounts-mapped'];
798
        $token   = new SessionToken($config['session_token']);
799
        $count   = 0;
800
        if (0 === $user->getId()) {
801
            $user = new UserCompany($config['user_company']);
802
            Log::debug(sprintf('Will try to get transactions for company #%d', $user->getId()));
803
        }
804
805
        $this->addTotalSteps(count($config['accounts']) * 2);
806
807
        foreach ($config['accounts'] as $accountData) {
808
            $this->addStep();
809
            $account  = new MonetaryAccountBank($accountData);
810
            $importId = $account->getId();
811
            if (1 === $mapping[$importId]) {
812
                Log::debug(sprintf('Will grab payments for account %s', $account->getDescription()));
813
                $request = new ListPaymentRequest();
814
                $request->setPrivateKey($this->getPrivateKey());
815
                $request->setServerPublicKey($this->getServerPublicKey());
816
                $request->setSessionToken($token);
817
                $request->setUserId($user->getId());
818
                $request->setAccount($account);
819
                $request->call();
820
                $payments = $request->getPayments();
821
822
                // store in array
823
                $all[$account->getId()] = [
824
                    'account'   => $account,
825
                    'import_id' => $importId,
826
                    'payments'  => $payments,
827
                ];
828
                $count                  += count($payments);
829
            }
830
            Log::debug(sprintf('Total number of payments: %d', $count));
831
            $this->addStep();
832
            // add steps for import:
833
            $this->addTotalSteps($count * 3);
834
            $this->importPayments($all);
835
        }
836
837
        // update job to be complete, I think?
838
    }
839
840
    /**
841
     * @throws FireflyException
842
     */
843
    private function runStageLoggedIn(): void
844
    {
845
        $this->addStep();
846
        // grab new session token:
847
        $config = $this->getConfig();
848
        $token  = new SessionToken($config['session_token']);
849
        $user   = new UserPerson($config['user_person']);
850
        if (0 === $user->getId()) {
851
            $user = new UserCompany($config['user_company']);
852
        }
853
854
        // list accounts request
855
        $request = new ListMonetaryAccountRequest();
856
        $request->setServerPublicKey($this->getServerPublicKey());
857
        $request->setPrivateKey($this->getPrivateKey());
858
        $request->setUserId($user->getId());
859
        $request->setSessionToken($token);
860
        $request->call();
861
        $accounts = $request->getMonetaryAccounts();
862
        $arr      = [];
863
        Log::debug(sprintf('Get monetary accounts, found %d accounts.', $accounts->count()));
864
        $this->addStep();
865
866
        /** @var MonetaryAccountBank $account */
867
        foreach ($accounts as $account) {
868
            $arr[] = $account->toArray();
869
        }
870
871
        $config             = $this->getConfig();
872
        $config['accounts'] = $arr;
873
        $config['stage']    = 'have-accounts';
874
        $this->setConfig($config);
875
876
        // once the accounts are stored, go to configuring stage:
877
        // update job, set status to "configuring".
878
        $this->setStatus('configuring');
879
        $this->addStep();
880
    }
881
882
    /**
883
     * Shorthand.
884
     *
885
     * @param array $config
886
     */
887
    private function setConfig(array $config): void
888
    {
889
        $this->repository->setConfiguration($this->job, $config);
890
    }
891
892
    /**
893
     * Shorthand method.
894
     *
895
     * @param array $extended
896
     */
897
    private function setExtendedStatus(array $extended): void
898
    {
899
        $this->repository->setExtendedStatus($this->job, $extended);
900
    }
901
902
    /**
903
     * Shorthand.
904
     *
905
     * @param string $status
906
     */
907
    private function setStatus(string $status): void
908
    {
909
        $this->repository->setStatus($this->job, $status);
910
    }
911
}
912