Passed
Push — master ( 40d218...d516cb )
by James
09:17 queued 11s
created

Accounts::findById()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 30
Code Lines 19

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 19
c 0
b 0
f 0
dl 0
loc 30
rs 9.6333
cc 4
nc 4
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\ApiHttpException;
27
use App\Exceptions\ImportException;
28
use GrumpyDictator\FFIIIApiSupport\Exceptions\ApiException;
29
use GrumpyDictator\FFIIIApiSupport\Exceptions\ApiHttpException as GrumpyApiHttpException;
30
use GrumpyDictator\FFIIIApiSupport\Model\Account;
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
        Log::debug('Found no account or haven\'t searched for one.');
110
111
        // append an empty type to the array for consistency's sake.
112
        $array['type'] = $array['type'] ?? null;
113
        $array['bic']  = $array['bic'] ?? null;
114
115
        // Return ID or name if not null
116
        if (null !== $array['id'] || null !== $array['name']) {
117
            Log::debug('Array with account has some info, return that.', $array);
118
119
            return $array;
120
        }
121
122
        // if the default account is not NULL, return that one instead:
123
        if (null !== $defaultAccount) {
124
            $default = $defaultAccount->toArray();
125
            Log::debug('Default account is not null, so will return:', $default);
126
127
            return $default;
128
        }
129
        Log::debug('Default account is NULL, so will return: ', $array);
130
131
        return $array;
132
    }
133
134
    /**
135
     * @param array $transaction
136
     *
137
     * @return array
138
     */
139
    private function getDestinationArray(array $transaction): array
140
    {
141
        return [
142
            'transaction_type' => $transaction['type'],
143
            'id'               => $transaction['destination_id'],
144
            'name'             => $transaction['destination_name'],
145
            'iban'             => $transaction['destination_iban'] ?? null,
146
            'number'           => $transaction['destination_number'] ?? null,
147
            'bic'              => $transaction['destination_bic'] ?? null,
148
        ];
149
    }
150
151
    /**
152
     * @param array $transaction
153
     *
154
     * @return array
155
     */
156
    private function getSourceArray(array $transaction): array
157
    {
158
        return [
159
            'transaction_type' => $transaction['type'],
160
            'id'               => $transaction['source_id'],
161
            'name'             => $transaction['source_name'],
162
            'iban'             => $transaction['source_iban'] ?? null,
163
            'number'           => $transaction['source_number'] ?? null,
164
            'bic'              => $transaction['source_bic'] ?? null,
165
        ];
166
    }
167
168
    /**
169
     * @param string $value
170
     *
171
     * @throws ImportException
172
     * @return Account|null
173
     */
174
    private function findById(string $value): ?Account
175
    {
176
        Log::debug(sprintf('Going to search account with ID "%s"', $value));
177
        $uri     = (string) config('csv_importer.uri');
178
        $token   = (string) config('csv_importer.access_token');
179
        $request = new GetSearchAccountRequest($uri, $token);
180
        $request->setField('id');
181
        $request->setQuery($value);
182
        /** @var GetAccountsResponse $response */
183
        try {
184
            $response = $request->get();
185
        } catch (GrumpyApiHttpException $e) {
186
            throw new ImportException($e->getMessage());
187
        }
188
        if (1 === count($response)) {
189
            /** @var Account $account */
190
            try {
191
                $account = $response->current();
192
            } catch (ApiException $e) {
193
                throw new ImportException($e->getMessage());
194
            }
195
196
            Log::debug(sprintf('[a] Found %s account #%d based on ID "%s"', $account->type, $account->id, $value));
197
198
            return $account;
199
        }
200
201
        Log::debug('Found NOTHING.');
202
203
        return null;
204
    }
205
206
207
    /**
208
     * @param array $transaction
209
     * @param array $source
210
     *
211
     * @return array
212
     */
213
    private function setSource(array $transaction, array $source): array
214
    {
215
        return $this->setTransactionAccount('source', $transaction, $source);
216
    }
217
218
    /**
219
     * @param array $transaction
220
     * @param array $source
221
     *
222
     * @return array
223
     */
224
    private function setDestination(array $transaction, array $source): array
225
    {
226
        return $this->setTransactionAccount('destination', $transaction, $source);
227
    }
228
229
    /**
230
     * @param string $direction
231
     * @param array  $transaction
232
     * @param array  $account
233
     *
234
     * @return array
235
     */
236
    private function setTransactionAccount(string $direction, array $transaction, array $account): array
237
    {
238
        $transaction[sprintf('%s_id', $direction)]     = $account['id'];
239
        $transaction[sprintf('%s_name', $direction)]   = $account['name'];
240
        $transaction[sprintf('%s_iban', $direction)]   = $account['iban'];
241
        $transaction[sprintf('%s_number', $direction)] = $account['number'];
242
        $transaction[sprintf('%s_bic', $direction)]    = $account['bic'];
243
244
        return $transaction;
245
    }
246
247
    /**
248
     * @param array $transaction
249
     *
250
     * @return array
251
     */
252
    private function processTransaction(array $transaction): array
253
    {
254
        Log::debug('Now in Accounts::processTransaction()');
255
256
        /*
257
         * Try to find the source and destination accounts in the transaction.
258
         *
259
         * The source account will default back to the user's submitted default account.
260
         * So when everything fails, the transaction will be a deposit for amount X.
261
         */
262
        $sourceArray = $this->getSourceArray($transaction);
263
        $destArray   = $this->getDestinationArray($transaction);
264
        $source      = $this->findAccount($sourceArray, $this->account);
265
        $destination = $this->findAccount($destArray, null);
266
267
        /*
268
         * First, set source and destination in the transaction array:
269
         */
270
        $transaction         = $this->setSource($transaction, $source);
271
        $transaction         = $this->setDestination($transaction, $destination);
272
        $transaction['type'] = $this->determineType($source['type'], $destination['type']);
273
274
        $amount = (string) $transaction['amount'];
275
        $amount = '' === $amount ? '0' : $amount;
276
277
        if('0'===$amount) {
278
            Log::error('Amount is ZERO. This will give trouble further down the line.');
279
        }
280
281
        /*
282
         * If the amount is positive, the transaction is a deposit. We switch Source
283
         * and Destination and see if we can still handle the transaction:
284
         */
285
        if (1 === bccomp($amount, '0')) {
286
            // amount is positive
287
            Log::debug(sprintf('%s is positive.', $amount));
288
            $transaction         = $this->setSource($transaction, $destination);
289
            $transaction         = $this->setDestination($transaction, $source);
290
            $transaction['type'] = $this->determineType($destination['type'], $source['type']);
291
        }
292
293
        /*
294
         * Final check. If the type is "withdrawal" but the destination account found is "revenue"
295
         * we found the wrong one. Just submit the name and hope for the best.
296
         */
297
        if ('revenue' === $destination['type'] && 'withdrawal' === $transaction['type']) {
298
            Log::warning('The found destination account is of type revenue but this is a withdrawal. Out of cheese error.');
299
            $transaction['destination_id']   = null;
300
            $transaction['destination_name'] = $destination['name'];
301
            $transaction['destination_iban'] = $destination['iban'];
302
        }
303
304
        /*
305
         * Same but for the other way around.
306
         * If type is "deposit" but the source account is an expense account.
307
         * Submit just the name.
308
         */
309
        if ('expense' === $source['type'] && 'deposit' === $transaction['type']) {
310
            Log::warning('The found source account is of type expense but this is a deposit. Out of cheese error.');
311
            $transaction['source_id']   = null;
312
            $transaction['source_name'] = $source['name'];
313
            $transaction['source_iban'] = $source['iban'];
314
        }
315
316
        /*
317
         * if new source or destination ID is filled in, drop the other fields:
318
         */
319
        if (0 !== $transaction['source_id'] && null !== $transaction['source_id']) {
320
            $transaction['source_name']   = null;
321
            $transaction['source_iban']   = null;
322
            $transaction['source_number'] = null;
323
        }
324
        if (0 !== $transaction['destination_id'] && null !== $transaction['destination_id']) {
325
            $transaction['destination_name']   = null;
326
            $transaction['destination_iban']   = null;
327
            $transaction['destination_number'] = null;
328
        }
329
330
        return $transaction;
331
    }
332
333
334
    /**
335
     * @param string|null $sourceType
336
     * @param string|null $destinationType
337
     *
338
     * @return string
339
     */
340
    private function determineType(?string $sourceType, ?string $destinationType): string
341
    {
342
        Log::debug(sprintf('Now in determineType("%s", "%s")', $sourceType, $destinationType));
343
        if (null === $sourceType && null === $destinationType) {
344
            Log::debug('Return withdrawal, both are NULL');
345
346
            return 'withdrawal';
347
        }
348
349
        // if source is a asset and dest is NULL, its a withdrawal
350
        if ('asset' === $sourceType && null === $destinationType) {
351
            Log::debug('Return withdrawal, source is asset');
352
353
            return 'withdrawal';
354
        }
355
        // if destination is asset and source is NULL, its a deposit
356
        if (null === $sourceType && 'asset' === $destinationType) {
357
            Log::debug('Return deposit, dest is asset');
358
359
            return 'deposit';
360
        }
361
362
        $key   = sprintf('transaction_types.account_to_transaction.%s.%s', $sourceType, $destinationType);
363
        $type  = config($key);
364
        $value = $type ?? 'withdrawal';
365
        Log::debug(sprintf('Check config for "%s" and found "%s". Returning "%s"', $key, $type, $value));
366
367
        return $value;
368
    }
369
}
370