Accounts::findByName()   A
last analyzed

Complexity

Conditions 5
Paths 5

Size

Total Lines 30
Code Lines 19

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 19
c 1
b 0
f 0
dl 0
loc 30
rs 9.3222
cc 5
nc 5
nop 1
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 App\Services\Import\DeterminesTransactionType;
28
use GrumpyDictator\FFIIIApiSupport\Exceptions\ApiException;
29
use GrumpyDictator\FFIIIApiSupport\Exceptions\ApiHttpException as GrumpyApiHttpException;
30
use GrumpyDictator\FFIIIApiSupport\Model\Account;
31
use GrumpyDictator\FFIIIApiSupport\Model\AccountType;
32
use GrumpyDictator\FFIIIApiSupport\Request\GetSearchAccountRequest;
33
use GrumpyDictator\FFIIIApiSupport\Response\GetAccountsResponse;
34
use Log;
35
36
/**
37
 * Class Accounts
38
 */
39
class Accounts extends AbstractTask
40
{
41
    use DeterminesTransactionType;
42
43
    /**
44
     * @param array $group
45
     *
46
     * @return array
47
     */
48
    public function process(array $group): array
49
    {
50
        Log::debug('Now in Accounts::process()');
51
        $total = count($group['transactions']);
52
        foreach ($group['transactions'] as $index => $transaction) {
53
            Log::debug(sprintf('Now processing transaction %d of %d', $index + 1, $total));
54
            $group['transactions'][$index] = $this->processTransaction($transaction);
55
        }
56
57
        return $group;
58
    }
59
60
    /**
61
     * Returns true if the task requires the default account.
62
     *
63
     * @return bool
64
     */
65
    public function requiresDefaultAccount(): bool
66
    {
67
        return true;
68
    }
69
70
    /**
71
     * Returns true if the task requires the default currency of the user.
72
     *
73
     * @return bool
74
     */
75
    public function requiresTransactionCurrency(): bool
76
    {
77
        return false;
78
    }
79
80
    /**
81
     * @param array        $array
82
     *
83
     * @param Account|null $defaultAccount
84
     *
85
     * @throws ImportException
86
     * @return array
87
     */
88
    private function findAccount(array $array, ?Account $defaultAccount): array
89
    {
90
        Log::debug('Now in findAccount', $array);
91
        if (null === $defaultAccount) {
92
            Log::debug('findAccount() default account is NULL.');
93
        }
94
        if (null !== $defaultAccount) {
95
            Log::debug(sprintf('Default account is #%d ("%s")', $defaultAccount->id, $defaultAccount->name));
96
        }
97
98
        $result = null;
99
        // if the ID is set, at least search for the ID.
100
        if (is_int($array['id']) && $array['id'] > 0) {
101
            Log::debug('Find by ID field.');
102
            $result = $this->findById((string) $array['id']);
103
        }
104
        if (null !== $result) {
105
            $return = $result->toArray();
106
            Log::debug('Result of findById is not null, returning:', $return);
107
108
            return $return;
109
        }
110
111
        // if the IBAN is set, search for the IBAN.
112
        if (isset($array['iban']) && '' !== (string) $array['iban']) {
113
            Log::debug('Find by IBAN.');
114
            $transactionType = (string) ($array['transaction_type'] ?? null);
115
            $result          = $this->findByIban((string) $array['iban'], $transactionType);
116
        }
117
        if (null !== $result) {
118
            $return = $result->toArray();
119
            Log::debug('Result of findByIBAN is not null, returning:', $return);
120
121
            return $return;
122
        }
123
124
        // find by name, return only if it's an asset or liability account.
125
        if (isset($array['name']) && '' !== (string) $array['name']) {
126
            Log::debug('Find by name.');
127
            $result = $this->findByName((string) $array['name']);
128
        }
129
        if (null !== $result) {
130
            $return = $result->toArray();
131
            Log::debug('Result of findByName is not null, returning:', $return);
132
133
            return $return;
134
        }
135
136
        Log::debug('Found no account or haven\'t searched for one.');
137
138
        // append an empty type to the array for consistency's sake.
139
        $array['type'] = $array['type'] ?? null;
140
        $array['bic']  = $array['bic'] ?? null;
141
142
        // Return ID or name if not null
143
        if (null !== $array['id'] || null !== $array['name']) {
144
            Log::debug('Array with account has some info, return that.', $array);
145
146
            return $array;
147
        }
148
149
        // if the default account is not NULL, return that one instead:
150
        if (null !== $defaultAccount) {
151
            $default = $defaultAccount->toArray();
152
            Log::debug('Default account is not null, so will return:', $default);
153
154
            return $default;
155
        }
156
        Log::debug('Default account is NULL, so will return: ', $array);
157
158
        return $array;
159
    }
160
161
    /**
162
     * @param array $transaction
163
     *
164
     * @return array
165
     */
166
    private function getDestinationArray(array $transaction): array
167
    {
168
        return [
169
            'transaction_type' => $transaction['type'],
170
            'id'               => $transaction['destination_id'],
171
            'name'             => $transaction['destination_name'],
172
            'iban'             => $transaction['destination_iban'] ?? null,
173
            'number'           => $transaction['destination_number'] ?? null,
174
            'bic'              => $transaction['destination_bic'] ?? null,
175
        ];
176
    }
177
178
    /**
179
     * @param array $transaction
180
     *
181
     * @return array
182
     */
183
    private function getSourceArray(array $transaction): array
184
    {
185
        return [
186
            'transaction_type' => $transaction['type'],
187
            'id'               => $transaction['source_id'],
188
            'name'             => $transaction['source_name'],
189
            'iban'             => $transaction['source_iban'] ?? null,
190
            'number'           => $transaction['source_number'] ?? null,
191
            'bic'              => $transaction['source_bic'] ?? null,
192
        ];
193
    }
194
195
    /**
196
     * @param string $value
197
     *
198
     * @throws ImportException
199
     * @return Account|null
200
     */
201
    private function findById(string $value): ?Account
202
    {
203
        Log::debug(sprintf('Going to search account with ID "%s"', $value));
204
        $uri     = (string) config('csv_importer.uri');
205
        $token   = (string) config('csv_importer.access_token');
206
        $request = new GetSearchAccountRequest($uri, $token);
207
        $request->setField('id');
208
        $request->setQuery($value);
209
        /** @var GetAccountsResponse $response */
210
        try {
211
            $response = $request->get();
212
        } catch (GrumpyApiHttpException $e) {
213
            throw new ImportException($e->getMessage());
214
        }
215
        if (1 === count($response)) {
216
            /** @var Account $account */
217
            try {
218
                $account = $response->current();
219
            } catch (ApiException $e) {
220
                throw new ImportException($e->getMessage());
221
            }
222
223
            Log::debug(sprintf('[a] Found %s account #%d based on ID "%s"', $account->type, $account->id, $value));
224
225
            return $account;
226
        }
227
228
        Log::debug('Found NOTHING.');
229
230
        return null;
231
    }
232
233
234
    /**
235
     * @param string $iban
236
     * @param string $transactionType
237
     *
238
     * @throws ImportException
239
     * @return Account|null
240
     */
241
    private function findByIban(string $iban, string $transactionType): ?Account
242
    {
243
        Log::debug(sprintf('Going to search account with IBAN "%s"', $iban));
244
        $uri     = (string) config('csv_importer.uri');
245
        $token   = (string) config('csv_importer.access_token');
246
        $request = new GetSearchAccountRequest($uri, $token);
247
        $request->setField('iban');
248
        $request->setQuery($iban);
249
        /** @var GetAccountsResponse $response */
250
        try {
251
            $response = $request->get();
252
        } catch (GrumpyApiHttpException $e) {
253
            throw new ImportException($e->getMessage());
254
        }
255
        if (0 === count($response)) {
256
            Log::debug('Found NOTHING.');
257
258
            return null;
259
        }
260
261
        if (1 === count($response)) {
262
            /** @var Account $account */
263
            try {
264
                $account = $response->current();
265
            } catch (ApiException $e) {
266
                throw new ImportException($e->getMessage());
267
            }
268
            // catch impossible combination "expense" with "deposit"
269
            if ('expense' === $account->type && 'deposit' === $transactionType) {
270
                Log::debug(
271
                    sprintf(
272
                        'Out of cheese error (IBAN). Found Found %s account #%d based on IBAN "%s". But not going to use expense/deposit combi.',
273
                        $account->type, $account->id, $iban
274
                    )
275
                );
276
                Log::debug('Firefly III will have to make the correct decision.');
277
278
                return null;
279
            }
280
            Log::debug(sprintf('[a] Found %s account #%d based on IBAN "%s"', $account->type, $account->id, $iban));
281
282
            return $account;
283
        }
284
285
        if (2 === count($response)) {
286
            Log::debug('Found 2 results, Firefly III will have to make the correct decision.');
287
            return null;
288
        }
289
        Log::debug(sprintf('Found %d result(s), Firefly III will have to make the correct decision.', count($response)));
290
291
        return null;
292
    }
293
294
    /**
295
     * @param string $name
296
     *
297
     * @throws ImportException
298
     * @return Account|null
299
     */
300
    private function findByName(string $name): ?Account
301
    {
302
        Log::debug(sprintf('Going to search account with name "%s"', $name));
303
        $uri     = (string) config('csv_importer.uri');
304
        $token   = (string) config('csv_importer.access_token');
305
        $request = new GetSearchAccountRequest($uri, $token);
306
        $request->setField('name');
307
        $request->setQuery($name);
308
        /** @var GetAccountsResponse $response */
309
        try {
310
            $response = $request->get();
311
        } catch (GrumpyApiHttpException $e) {
312
            throw new ImportException($e->getMessage());
313
        }
314
        if (0 === count($response)) {
315
            Log::debug('Found NOTHING.');
316
317
            return null;
318
        }
319
        /** @var Account $account */
320
        foreach ($response as $account) {
321
            if (in_array($account->type, [AccountType::ASSET, AccountType::LOAN, AccountType::DEBT, AccountType::MORTGAGE], true)) {
322
                Log::debug(sprintf('[b] Found "%s" account #%d based on name "%s"', $account->type, $account->id, $name));
323
324
                return $account;
325
            }
326
        }
327
        Log::debug(sprintf('Found %d account(s) searching for "%s" but not going to use them. Firefly III must handle the values.', count($response), $name));
328
329
        return null;
330
    }
331
332
333
    /**
334
     * @param array $transaction
335
     * @param array $source
336
     *
337
     * @return array
338
     */
339
    private function setSource(array $transaction, array $source): array
340
    {
341
        return $this->setTransactionAccount('source', $transaction, $source);
342
    }
343
344
    /**
345
     * @param array $transaction
346
     * @param array $source
347
     *
348
     * @return array
349
     */
350
    private function setDestination(array $transaction, array $source): array
351
    {
352
        return $this->setTransactionAccount('destination', $transaction, $source);
353
    }
354
355
    /**
356
     * @param string $direction
357
     * @param array  $transaction
358
     * @param array  $account
359
     *
360
     * @return array
361
     */
362
    private function setTransactionAccount(string $direction, array $transaction, array $account): array
363
    {
364
        $transaction[sprintf('%s_id', $direction)]     = $account['id'];
365
        $transaction[sprintf('%s_name', $direction)]   = $account['name'];
366
        $transaction[sprintf('%s_iban', $direction)]   = $account['iban'];
367
        $transaction[sprintf('%s_number', $direction)] = $account['number'];
368
        $transaction[sprintf('%s_bic', $direction)]    = $account['bic'];
369
370
        return $transaction;
371
    }
372
373
    /**
374
     * @param array $transaction
375
     *
376
     * @return array
377
     */
378
    private function processTransaction(array $transaction): array
379
    {
380
        Log::debug('Now in Accounts::processTransaction()');
381
382
        /*
383
         * Try to find the source and destination accounts in the transaction.
384
         *
385
         * The source account will default back to the user's submitted default account.
386
         * So when everything fails, the transaction will be a deposit for amount X.
387
         */
388
        $sourceArray = $this->getSourceArray($transaction);
389
        $destArray   = $this->getDestinationArray($transaction);
390
        $source      = $this->findAccount($sourceArray, $this->account);
391
        $destination = $this->findAccount($destArray, null);
392
393
        /*
394
         * First, set source and destination in the transaction array:
395
         */
396
        $transaction         = $this->setSource($transaction, $source);
397
        $transaction         = $this->setDestination($transaction, $destination);
398
        $transaction['type'] = $this->determineType($source['type'], $destination['type']);
399
400
        $amount = (string) $transaction['amount'];
401
        $amount = '' === $amount ? '0' : $amount;
402
403
        if ('0' === $amount) {
404
            Log::error('Amount is ZERO. This will give trouble further down the line.');
405
        }
406
407
        /*
408
         * If the amount is positive, the transaction is a deposit. We switch Source
409
         * and Destination and see if we can still handle the transaction, but only if the transaction
410
         * isn't already a deposit
411
         */
412
        if ('deposit' !== $transaction['type'] && 1 === bccomp($amount, '0')) {
413
            // amount is positive
414
            Log::debug(sprintf('%s is positive.', $amount));
415
            $transaction         = $this->setSource($transaction, $destination);
416
            $transaction         = $this->setDestination($transaction, $source);
417
            $transaction['type'] = $this->determineType($destination['type'], $source['type']);
418
        }
419
        if ('deposit' === $transaction['type'] && 1 === bccomp($amount, '0')) {
420
            Log::debug('Transaction is a deposit, and amount is positive. Will not change account types.');
421
        }
422
423
        /*
424
         * Final check. If the type is "withdrawal" but the destination account found is "revenue"
425
         * we found the wrong one. Just submit the name and hope for the best.
426
         */
427
        if ('revenue' === $destination['type'] && 'withdrawal' === $transaction['type']) {
428
            Log::warning('The found destination account is of type revenue but this is a withdrawal. Out of cheese error.');
429
            Log::debug(
430
                sprintf('CSV importer will submit name "%s" and IBAN "%s" and let Firefly III sort it out.', $destination['name'], $destination['iban'])
431
            );
432
            $transaction['destination_id']   = null;
433
            $transaction['destination_name'] = $destination['name'];
434
            $transaction['destination_iban'] = $destination['iban'];
435
        }
436
437
        /*
438
         * Same but for the other way around.
439
         * If type is "deposit" but the source account is an expense account.
440
         * Submit just the name.
441
         */
442
        if ('expense' === $source['type'] && 'deposit' === $transaction['type']) {
443
            Log::warning('The found source account is of type expense but this is a deposit. Out of cheese error.');
444
            Log::debug(sprintf('CSV importer will submit name "%s" and IBAN "%s" and let Firefly III sort it out.', $source['name'], $source['iban']));
445
            $transaction['source_id']   = null;
446
            $transaction['source_name'] = $source['name'];
447
            $transaction['source_iban'] = $source['iban'];
448
        }
449
450
        /*
451
         * if new source or destination ID is filled in, drop the other fields:
452
         */
453
        if (0 !== $transaction['source_id'] && null !== $transaction['source_id']) {
454
            $transaction['source_name']   = null;
455
            $transaction['source_iban']   = null;
456
            $transaction['source_number'] = null;
457
        }
458
        if (0 !== $transaction['destination_id'] && null !== $transaction['destination_id']) {
459
            $transaction['destination_name']   = null;
460
            $transaction['destination_iban']   = null;
461
            $transaction['destination_number'] = null;
462
        }
463
464
        return $transaction;
465
    }
466
467
468
}
469