Passed
Push — master ( 0547e9...d89604 )
by James
10:02 queued 11s
created

Accounts::findByIban()   B

Complexity

Conditions 6
Paths 6

Size

Total Lines 38
Code Lines 23

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 23
c 0
b 0
f 0
dl 0
loc 38
rs 8.9297
cc 6
nc 6
nop 2
1
<?php
2
declare(strict_types=1);
3
/**
4
 * Accounts.php
5
 * Copyright (c) 2020 [email protected]
6
 *
7
 * This file is part of the Firefly III CSV importer
8
 * (https://github.com/firefly-iii/csv-importer).
9
 *
10
 * This program is free software: you can redistribute it and/or modify
11
 * it under the terms of the GNU Affero General Public License as
12
 * published by the Free Software Foundation, either version 3 of the
13
 * License, or (at your option) any later version.
14
 *
15
 * This program is distributed in the hope that it will be useful,
16
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
17
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
18
 * GNU Affero General Public License for more details.
19
 *
20
 * You should have received a copy of the GNU Affero General Public License
21
 * along with this program.  If not, see <https://www.gnu.org/licenses/>.
22
 */
23
24
namespace App\Services\Import\Task;
25
26
use App\Exceptions\ImportException;
27
use GrumpyDictator\FFIIIApiSupport\Exceptions\ApiException;
28
use GrumpyDictator\FFIIIApiSupport\Exceptions\ApiHttpException as GrumpyApiHttpException;
29
use GrumpyDictator\FFIIIApiSupport\Model\Account;
30
use GrumpyDictator\FFIIIApiSupport\Model\AccountType;
31
use GrumpyDictator\FFIIIApiSupport\Request\GetSearchAccountRequest;
32
use GrumpyDictator\FFIIIApiSupport\Response\GetAccountsResponse;
33
use Log;
34
35
/**
36
 * Class Accounts
37
 */
38
class Accounts extends AbstractTask
39
{
40
41
    /**
42
     * @param array $group
43
     *
44
     * @return array
45
     */
46
    public function process(array $group): array
47
    {
48
        Log::debug('Now in Accounts::process()');
49
        $total = count($group['transactions']);
50
        foreach ($group['transactions'] as $index => $transaction) {
51
            Log::debug(sprintf('Now processing transaction %d of %d', $index + 1, $total));
52
            $group['transactions'][$index] = $this->processTransaction($transaction);
53
        }
54
55
        return $group;
56
    }
57
58
    /**
59
     * Returns true if the task requires the default account.
60
     *
61
     * @return bool
62
     */
63
    public function requiresDefaultAccount(): bool
64
    {
65
        return true;
66
    }
67
68
    /**
69
     * Returns true if the task requires the default currency of the user.
70
     *
71
     * @return bool
72
     */
73
    public function requiresTransactionCurrency(): bool
74
    {
75
        return false;
76
    }
77
78
    /**
79
     * @param array        $array
80
     *
81
     * @param Account|null $defaultAccount
82
     *
83
     * @throws ImportException
84
     * @return array
85
     */
86
    private function findAccount(array $array, ?Account $defaultAccount): array
87
    {
88
        Log::debug('Now in findAccount', $array);
89
        if (null === $defaultAccount) {
90
            Log::debug('findAccount() default account is NULL.');
91
        }
92
        if (null !== $defaultAccount) {
93
            Log::debug(sprintf('Default account is #%d ("%s")', $defaultAccount->id, $defaultAccount->name));
94
        }
95
96
        $result = null;
97
        // if the ID is set, at least search for the ID.
98
        if (is_int($array['id']) && $array['id'] > 0) {
99
            Log::debug('Find by ID field.');
100
            $result = $this->findById((string) $array['id']);
101
        }
102
        if (null !== $result) {
103
            $return = $result->toArray();
104
            Log::debug('Result of findById is not null, returning:', $return);
105
106
            return $return;
107
        }
108
109
        // if the IBAN is set, search for the IBAN.
110
        if (isset($array['iban']) && '' !== (string) $array['iban']) {
111
            Log::debug('Find by IBAN.');
112
            $transactionType = (string) ($array['transaction_type'] ?? null);
113
            $result          = $this->findByIban((string) $array['iban'], $transactionType);
114
        }
115
        if (null !== $result) {
116
            $return = $result->toArray();
117
            Log::debug('Result of findByIBAN is not null, returning:', $return);
118
119
            return $return;
120
        }
121
122
        // find by name, return only if it's an asset or liability account.
123
        if (isset($array['name']) && '' !== (string) $array['name']) {
124
            Log::debug('Find by name.');
125
            $result = $this->findByName((string) $array['name']);
126
        }
127
        if (null !== $result) {
128
            $return = $result->toArray();
129
            Log::debug('Result of findByName is not null, returning:', $return);
130
131
            return $return;
132
        }
133
134
        Log::debug('Found no account or haven\'t searched for one.');
135
136
        // append an empty type to the array for consistency's sake.
137
        $array['type'] = $array['type'] ?? null;
138
        $array['bic']  = $array['bic'] ?? null;
139
140
        // Return ID or name if not null
141
        if (null !== $array['id'] || null !== $array['name']) {
142
            Log::debug('Array with account has some info, return that.', $array);
143
144
            return $array;
145
        }
146
147
        // if the default account is not NULL, return that one instead:
148
        if (null !== $defaultAccount) {
149
            $default = $defaultAccount->toArray();
150
            Log::debug('Default account is not null, so will return:', $default);
151
152
            return $default;
153
        }
154
        Log::debug('Default account is NULL, so will return: ', $array);
155
156
        return $array;
157
    }
158
159
    /**
160
     * @param array $transaction
161
     *
162
     * @return array
163
     */
164
    private function getDestinationArray(array $transaction): array
165
    {
166
        return [
167
            'transaction_type' => $transaction['type'],
168
            'id'               => $transaction['destination_id'],
169
            'name'             => $transaction['destination_name'],
170
            'iban'             => $transaction['destination_iban'] ?? null,
171
            'number'           => $transaction['destination_number'] ?? null,
172
            'bic'              => $transaction['destination_bic'] ?? null,
173
        ];
174
    }
175
176
    /**
177
     * @param array $transaction
178
     *
179
     * @return array
180
     */
181
    private function getSourceArray(array $transaction): array
182
    {
183
        return [
184
            'transaction_type' => $transaction['type'],
185
            'id'               => $transaction['source_id'],
186
            'name'             => $transaction['source_name'],
187
            'iban'             => $transaction['source_iban'] ?? null,
188
            'number'           => $transaction['source_number'] ?? null,
189
            'bic'              => $transaction['source_bic'] ?? null,
190
        ];
191
    }
192
193
    /**
194
     * @param string $value
195
     *
196
     * @throws ImportException
197
     * @return Account|null
198
     */
199
    private function findById(string $value): ?Account
200
    {
201
        Log::debug(sprintf('Going to search account with ID "%s"', $value));
202
        $uri     = (string) config('csv_importer.uri');
203
        $token   = (string) config('csv_importer.access_token');
204
        $request = new GetSearchAccountRequest($uri, $token);
205
        $request->setField('id');
206
        $request->setQuery($value);
207
        /** @var GetAccountsResponse $response */
208
        try {
209
            $response = $request->get();
210
        } catch (GrumpyApiHttpException $e) {
211
            throw new ImportException($e->getMessage());
212
        }
213
        if (1 === count($response)) {
214
            /** @var Account $account */
215
            try {
216
                $account = $response->current();
217
            } catch (ApiException $e) {
218
                throw new ImportException($e->getMessage());
219
            }
220
221
            Log::debug(sprintf('[a] Found %s account #%d based on ID "%s"', $account->type, $account->id, $value));
222
223
            return $account;
224
        }
225
226
        Log::debug('Found NOTHING.');
227
228
        return null;
229
    }
230
231
232
    /**
233
     * @param string $iban
234
     * @param string $transactionType
235
     *
236
     * @throws ImportException
237
     * @return Account|null
238
     */
239
    private function findByIban(string $iban, string $transactionType): ?Account
0 ignored issues
show
Unused Code introduced by
The parameter $transactionType is not used and could be removed. ( Ignorable by Annotation )

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

239
    private function findByIban(string $iban, /** @scrutinizer ignore-unused */ string $transactionType): ?Account

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
240
    {
241
        Log::debug(sprintf('Going to search account with IBAN "%s"', $iban));
242
        $uri     = (string) config('csv_importer.uri');
243
        $token   = (string) config('csv_importer.access_token');
244
        $request = new GetSearchAccountRequest($uri, $token);
245
        $request->setField('iban');
246
        $request->setQuery($iban);
247
        /** @var GetAccountsResponse $response */
248
        try {
249
            $response = $request->get();
250
        } catch (GrumpyApiHttpException $e) {
251
            throw new ImportException($e->getMessage());
252
        }
253
        if (0 === count($response)) {
254
            Log::debug('Found NOTHING.');
255
256
            return null;
257
        }
258
259
        if (1 === count($response)) {
260
            /** @var Account $account */
261
            try {
262
                $account = $response->current();
263
            } catch (ApiException $e) {
264
                throw new ImportException($e->getMessage());
265
            }
266
267
            Log::debug(sprintf('[a] Found %s account #%d based on IBAN "%s"', $account->type, $account->id, $iban));
268
269
            return $account;
270
        }
271
272
        if (2 === count($response)) {
273
            Log::debug('Found 2 results, Firefly III will have to make the correct decision.');
274
        }
275
276
        return null;
277
    }
278
279
    /**
280
     * @param string $name
281
     *
282
     * @throws ImportException
283
     * @return Account|null
284
     */
285
    private function findByName(string $name): ?Account
286
    {
287
        Log::debug(sprintf('Going to search account with name "%s"', $name));
288
        $uri     = (string) config('csv_importer.uri');
289
        $token   = (string) config('csv_importer.access_token');
290
        $request = new GetSearchAccountRequest($uri, $token);
291
        $request->setField('name');
292
        $request->setQuery($name);
293
        /** @var GetAccountsResponse $response */
294
        try {
295
            $response = $request->get();
296
        } catch (GrumpyApiHttpException $e) {
297
            throw new ImportException($e->getMessage());
298
        }
299
        if (0 === count($response)) {
300
            Log::debug('Found NOTHING.');
301
302
            return null;
303
        }
304
        /** @var Account $account */
305
        foreach ($response as $account) {
306
            if (in_array($account->type, [AccountType::ASSET, AccountType::LOAN, AccountType::DEBT, AccountType::MORTGAGE], true)) {
307
                Log::debug(sprintf('[b] Found "%s" account #%d based on name "%s"', $account->type, $account->id, $name));
308
309
                return $account;
310
            }
311
        }
312
313
        return null;
314
    }
315
316
317
    /**
318
     * @param array $transaction
319
     * @param array $source
320
     *
321
     * @return array
322
     */
323
    private function setSource(array $transaction, array $source): array
324
    {
325
        return $this->setTransactionAccount('source', $transaction, $source);
326
    }
327
328
    /**
329
     * @param array $transaction
330
     * @param array $source
331
     *
332
     * @return array
333
     */
334
    private function setDestination(array $transaction, array $source): array
335
    {
336
        return $this->setTransactionAccount('destination', $transaction, $source);
337
    }
338
339
    /**
340
     * @param string $direction
341
     * @param array  $transaction
342
     * @param array  $account
343
     *
344
     * @return array
345
     */
346
    private function setTransactionAccount(string $direction, array $transaction, array $account): array
347
    {
348
        $transaction[sprintf('%s_id', $direction)]     = $account['id'];
349
        $transaction[sprintf('%s_name', $direction)]   = $account['name'];
350
        $transaction[sprintf('%s_iban', $direction)]   = $account['iban'];
351
        $transaction[sprintf('%s_number', $direction)] = $account['number'];
352
        $transaction[sprintf('%s_bic', $direction)]    = $account['bic'];
353
354
        return $transaction;
355
    }
356
357
    /**
358
     * @param array $transaction
359
     *
360
     * @return array
361
     */
362
    private function processTransaction(array $transaction): array
363
    {
364
        Log::debug('Now in Accounts::processTransaction()');
365
366
        /*
367
         * Try to find the source and destination accounts in the transaction.
368
         *
369
         * The source account will default back to the user's submitted default account.
370
         * So when everything fails, the transaction will be a deposit for amount X.
371
         */
372
        $sourceArray = $this->getSourceArray($transaction);
373
        $destArray   = $this->getDestinationArray($transaction);
374
        $source      = $this->findAccount($sourceArray, $this->account);
375
        $destination = $this->findAccount($destArray, null);
376
377
        /*
378
         * First, set source and destination in the transaction array:
379
         */
380
        $transaction         = $this->setSource($transaction, $source);
381
        $transaction         = $this->setDestination($transaction, $destination);
382
        $transaction['type'] = $this->determineType($source['type'], $destination['type']);
383
384
        $amount = (string) $transaction['amount'];
385
        $amount = '' === $amount ? '0' : $amount;
386
387
        if('0'===$amount) {
388
            Log::error('Amount is ZERO. This will give trouble further down the line.');
389
        }
390
391
        /*
392
         * If the amount is positive, the transaction is a deposit. We switch Source
393
         * and Destination and see if we can still handle the transaction:
394
         */
395
        if (1 === bccomp($amount, '0')) {
396
            // amount is positive
397
            Log::debug(sprintf('%s is positive.', $amount));
398
            $transaction         = $this->setSource($transaction, $destination);
399
            $transaction         = $this->setDestination($transaction, $source);
400
            $transaction['type'] = $this->determineType($destination['type'], $source['type']);
401
        }
402
403
        /*
404
         * Final check. If the type is "withdrawal" but the destination account found is "revenue"
405
         * we found the wrong one. Just submit the name and hope for the best.
406
         */
407
        if ('revenue' === $destination['type'] && 'withdrawal' === $transaction['type']) {
408
            Log::warning('The found destination account is of type revenue but this is a withdrawal. Out of cheese error.');
409
            Log::debug(
410
                sprintf('CSV importer will submit name "%s" and IBAN "%s" and let Firefly III sort it out.', $destination['name'], $destination['iban'])
411
            );
412
            $transaction['destination_id']   = null;
413
            $transaction['destination_name'] = $destination['name'];
414
            $transaction['destination_iban'] = $destination['iban'];
415
        }
416
417
        /*
418
         * Same but for the other way around.
419
         * If type is "deposit" but the source account is an expense account.
420
         * Submit just the name.
421
         */
422
        if ('expense' === $source['type'] && 'deposit' === $transaction['type']) {
423
            Log::warning('The found source account is of type expense but this is a deposit. Out of cheese error.');
424
            Log::debug(sprintf('CSV importer will submit name "%s" and IBAN "%s" and let Firefly III sort it out.', $source['name'], $source['iban']));
425
            $transaction['source_id']   = null;
426
            $transaction['source_name'] = $source['name'];
427
            $transaction['source_iban'] = $source['iban'];
428
        }
429
430
        /*
431
         * if new source or destination ID is filled in, drop the other fields:
432
         */
433
        if (0 !== $transaction['source_id'] && null !== $transaction['source_id']) {
434
            $transaction['source_name']   = null;
435
            $transaction['source_iban']   = null;
436
            $transaction['source_number'] = null;
437
        }
438
        if (0 !== $transaction['destination_id'] && null !== $transaction['destination_id']) {
439
            $transaction['destination_name']   = null;
440
            $transaction['destination_iban']   = null;
441
            $transaction['destination_number'] = null;
442
        }
443
444
        return $transaction;
445
    }
446
447
448
    /**
449
     * @param string|null $sourceType
450
     * @param string|null $destinationType
451
     *
452
     * @return string
453
     */
454
    private function determineType(?string $sourceType, ?string $destinationType): string
455
    {
456
        Log::debug(sprintf('Now in determineType("%s", "%s")', $sourceType, $destinationType));
457
        if (null === $sourceType && null === $destinationType) {
458
            Log::debug('Return withdrawal, both are NULL');
459
460
            return 'withdrawal';
461
        }
462
463
        // if source is a asset and dest is NULL, its a withdrawal
464
        if ('asset' === $sourceType && null === $destinationType) {
465
            Log::debug('Return withdrawal, source is asset');
466
467
            return 'withdrawal';
468
        }
469
        // if destination is asset and source is NULL, its a deposit
470
        if (null === $sourceType && 'asset' === $destinationType) {
471
            Log::debug('Return deposit, dest is asset');
472
473
            return 'deposit';
474
        }
475
476
        $key   = sprintf('transaction_types.account_to_transaction.%s.%s', $sourceType, $destinationType);
477
        $type  = config($key);
478
        $value = $type ?? 'withdrawal';
479
        Log::debug(sprintf('Check config for "%s" and found "%s". Returning "%s"', $key, $type, $value));
480
481
        return $value;
482
    }
483
}
484