GenerateTransactions::collectTargetAccounts()   A
last analyzed

Complexity

Conditions 4
Paths 4

Size

Total Lines 28
Code Lines 20

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 20
c 0
b 0
f 0
dl 0
loc 28
rs 9.6
cc 4
nc 4
nop 0
1
<?php
2
3
declare(strict_types=1);
4
/**
5
 * GenerateTransactions.php
6
 * Copyright (c) 2020 [email protected].
7
 *
8
 * This file is part of the Firefly III bunq importer
9
 * (https://github.com/firefly-iii/bunq-importer).
10
 *
11
 * This program is free software: you can redistribute it and/or modify
12
 * it under the terms of the GNU Affero General Public License as
13
 * published by the Free Software Foundation, either version 3 of the
14
 * License, or (at your option) any later version.
15
 *
16
 * This program is distributed in the hope that it will be useful,
17
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
18
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
19
 * GNU Affero General Public License for more details.
20
 *
21
 * You should have received a copy of the GNU Affero General Public License
22
 * along with this program.  If not, see <https://www.gnu.org/licenses/>.
23
 */
24
25
namespace App\Services\Sync;
26
27
use App\Exceptions\ImportException;
28
use App\Services\Configuration\Configuration;
29
use App\Services\Sync\JobStatus\ProgressInformation;
30
use GrumpyDictator\FFIIIApiSupport\Exceptions\ApiHttpException;
31
use GrumpyDictator\FFIIIApiSupport\Model\Account;
32
use GrumpyDictator\FFIIIApiSupport\Request\GetAccountRequest;
33
use GrumpyDictator\FFIIIApiSupport\Request\GetAccountsRequest;
34
use GrumpyDictator\FFIIIApiSupport\Response\GetAccountResponse;
35
use GrumpyDictator\FFIIIApiSupport\Response\GetAccountsResponse;
36
use Log;
37
38
/**
39
 * Class GenerateTransactions.
40
 */
41
class GenerateTransactions
42
{
43
    use ProgressInformation;
44
    /** @var array */
45
    private $accounts;
46
    /** @var Configuration */
47
    private $configuration;
48
49
    /** @var array */
50
    private $targetAccounts;
51
    /** @var array */
52
    private $targetTypes;
53
54
    /**
55
     * GenerateTransactions constructor.
56
     */
57
    public function __construct()
58
    {
59
        $this->targetAccounts = [];
60
        $this->targetTypes    = [];
61
    }
62
63
    /**
64
     *
65
     */
66
    public function collectTargetAccounts(): void
67
    {
68
        Log::debug('Going to collect all target accounts from Firefly III.');
69
        // send account list request to Firefly III.
70
        $token   = (string) config('bunq.access_token');
71
        $uri     = (string) config('bunq.uri');
72
        $request = new GetAccountsRequest($uri, $token);
73
        /** @var GetAccountsResponse $result */
74
        $result = $request->get();
75
        $return = [];
76
        $types  = [];
77
        /** @var Account $entry */
78
        foreach ($result as $entry) {
79
            $type = $entry->type;
80
            if (in_array($type, ['reconciliation', 'initial-balance', 'expense', 'revenue'], true)) {
81
                continue;
82
            }
83
            $iban = $entry->iban;
84
            if ('' === (string) $iban) {
85
                continue;
86
            }
87
            Log::debug(sprintf('Collected %s (%s) under ID #%d', $iban, $entry->type, $entry->id));
88
            $return[$iban] = $entry->id;
89
            $types[$iban]  = $entry->type;
90
        }
91
        $this->targetAccounts = $return;
92
        $this->targetTypes    = $types;
93
        Log::debug(sprintf('Collected %d accounts.', count($this->targetAccounts)));
94
    }
95
96
    /**
97
     * @param array $bunq
98
     *
99
     * @throws ImportException
100
     * @return array
101
     */
102
    public function getTransactions(array $bunq): array
103
    {
104
        $return = [];
105
        /** @var array $entry */
106
        foreach ($bunq as $bunqAccountId => $entries) {
107
            $bunqAccountId = (int) $bunqAccountId;
108
            app('log')->debug(sprintf('Going to parse account #%d', $bunqAccountId));
109
            foreach ($entries as $entry) {
110
                $return[] = $this->generateTransaction($bunqAccountId, $entry);
111
                // TODO error handling at this point.
112
            }
113
        }
114
        $this->addMessage(0, sprintf('Parsed %d bunq transactions for further processing.', count($return)));
115
116
        return $return;
117
    }
118
119
    /**
120
     * @param Configuration $configuration
121
     */
122
    public function setConfiguration(Configuration $configuration): void
123
    {
124
        $this->configuration = $configuration;
125
        $this->accounts      = $configuration->getAccounts();
126
    }
127
128
    /**
129
     * @param int   $bunqAccountId
130
     * @param array $entry
131
     *
132
     * @throws ImportException
133
     * @return array
134
     */
135
    private function generateTransaction(int $bunqAccountId, array $entry): array
136
    {
137
        $return = [
138
            'apply_rules'             => $this->configuration->isRules(),
139
            'error_if_duplicate_hash' => true,
140
            'transactions'            => [
141
                [
142
                    'type'          => 'withdrawal', // reverse
143
                    'date'          => substr($entry['created'], 0, 10),
144
                    'datetime'      => $entry['created'], // not used in API, only for transaction filtering.
145
                    'amount'        => 0,
146
                    'description'   => $entry['description'],
147
                    'order'         => 0,
148
                    'currency_code' => $entry['currency_code'],
149
                    'tags'          => [$entry['type'], $entry['sub_type']],
150
                ],
151
            ],
152
        ];
153
154
        // save meta:
155
        $return['transactions'][0]['bunq_payment_id']    = $entry['id'];
156
        $return['transactions'][0]['external_id']        = $entry['id'];
157
        $return['transactions'][0]['internal_reference'] = $bunqAccountId;
158
159
        // give "auto save" transactions a different description:
160
        if ('SAVINGS' === $entry['type'] && 'PAYMENT' === $entry['sub_type']) {
161
            $return['transactions'][0]['description'] = '(auto save transaction)';
162
        }
163
164
        if (1 === bccomp($entry['amount'], '0')) {
165
            // amount is positive: deposit or transfer. Bunq account is destination
166
            $return['transactions'][0]['type']   = 'deposit';
167
            $return['transactions'][0]['amount'] = $entry['amount'];
168
169
            // destination is bunq
170
            $return['transactions'][0]['destination_id'] = (int) $this->accounts[$bunqAccountId];
171
172
            // source is the other side:
173
            $return['transactions'][0]['source_iban'] = $entry['counter_party']['iban'];
174
            $return['transactions'][0]['source_name'] = $entry['counter_party']['display_name'];
175
176
            $mappedId = $this->getMappedId($entry['counter_party']['display_name'], (string) $entry['counter_party']['iban']);
177
            if (null !== $mappedId && 0 !== $mappedId) {
178
                $mappedType                             = $this->getMappedType($mappedId);
179
                $return['transactions'][0]['type']      = $this->getTransactionType($mappedType, 'asset');
180
                $return['transactions'][0]['source_id'] = $mappedId;
181
                unset($return['transactions'][0]['source_iban'], $return['transactions'][0]['source_name']);
182
            }
183
            //Log::debug(sprintf('Mapped ID is %s', var_export($mappedId, true)));
184
            // check target accounts as well:
185
            $iban = $entry['counter_party']['iban'];
186
            if ((null === $mappedId || 0 === $mappedId) && isset($this->targetAccounts[$iban])) {
187
                Log::debug(sprintf('Found IBAN %s in target accounts (ID %d). Type is %s', $iban, $this->targetAccounts[$iban], $this->targetTypes[$iban]));
188
189
                // type: source comes from $targetTypes, destination is asset (see above).
190
                $return['transactions'][0]['type']      = $this->getTransactionType($this->targetTypes[$iban] ?? '', 'asset');
191
                $return['transactions'][0]['source_id'] = $this->targetAccounts[$iban];
192
                unset($return['transactions'][0]['source_iban'], $return['transactions'][0]['source_name']);
193
                Log::debug(sprintf('Replaced source IBAN %s with ID #%d (type %s).', $iban, $this->targetAccounts[$iban], $this->targetTypes[$iban]));
194
            }
195
            unset($iban);
196
        }
197
198
        // TODO these two if statements are mirrors of each other.
199
200
        if (-1 === bccomp($entry['amount'], '0')) {
201
            // amount is negative: withdrawal or transfer.
202
            $return['transactions'][0]['amount'] = bcmul($entry['amount'], '-1');
203
204
            // source is bunq:
205
            $return['transactions'][0]['source_id'] = (int) $this->accounts[$bunqAccountId];
206
207
            // dest is shop
208
            $return['transactions'][0]['destination_iban'] = $entry['counter_party']['iban'];
209
            $return['transactions'][0]['destination_name'] = $entry['counter_party']['display_name'];
210
211
            $mappedId = $this->getMappedId($entry['counter_party']['display_name'], (string) $entry['counter_party']['iban']);
212
            //Log::debug(sprintf('Mapped ID is %s', var_export($mappedId, true)));
213
            if (null !== $mappedId && 0 !== $mappedId) {
214
                $return['transactions'][0]['destination_id'] = $mappedId;
215
                $mappedType                                  = $this->getMappedType($mappedId);
216
                $return['transactions'][0]['type']           = $this->getTransactionType('asset', $mappedType);
217
                unset($return['transactions'][0]['destination_iban'], $return['transactions'][0]['destination_name']);
218
            }
219
220
            // check target accounts as well:
221
            $iban = $entry['counter_party']['iban'];
222
            if ((null === $mappedId || 0 === $mappedId) && isset($this->targetAccounts[$iban])) {
223
                Log::debug(sprintf('Found IBAN %s in target accounts (ID %d). Type is %s', $iban, $this->targetAccounts[$iban], $this->targetTypes[$iban]));
224
225
                // source is always asset, destination depends on $targetType.
226
                $return['transactions'][0]['type']           = $this->getTransactionType('asset', $this->targetTypes[$iban] ?? '');
227
                $return['transactions'][0]['destination_id'] = $this->targetAccounts[$iban];
228
                unset($return['transactions'][0]['destination_iban'], $return['transactions'][0]['destination_name']);
229
                Log::debug(sprintf('Replaced source IBAN %s with ID #%d (type %s).', $iban, $this->targetAccounts[$iban], $this->targetTypes[$iban]));
230
            }
231
            unset($iban);
232
        }
233
        app('log')->debug(sprintf('Parsed bunq transaction #%d', $entry['id']));
234
235
        return $return;
236
    }
237
238
    /**
239
     * @param int $accountId
240
     *
241
     * @throws ApiHttpException
242
     * @return string
243
     */
244
    private function getAccountType(int $accountId): string
245
    {
246
        $uri   = (string) config('bunq.uri');
247
        $token = (string) config('bunq.access_token');
248
        app('log')->debug(sprintf('Going to download account #%d', $accountId));
249
        $request = new GetAccountRequest($uri, $token);
250
        $request->setId($accountId);
251
        /** @var GetAccountResponse $result */
252
        $result = $request->get();
253
        $type   = $result->getAccount()->type;
254
255
        app('log')->debug(sprintf('Discovered that account #%d is of type "%s"', $accountId, $type));
256
257
        return $type;
258
    }
259
260
    /**
261
     * @param string $name
262
     * @param string $iban
263
     *
264
     * @return int|null
265
     */
266
    private function getMappedId(string $name, string $iban): ?int
267
    {
268
        $fullName = $name;
269
        if ('' !== $iban) {
270
            $fullName = sprintf('%s (%s)', $name, $iban);
271
        }
272
        if (isset($this->configuration->getMapping()[$fullName])) {
273
            return (int) $this->configuration->getMapping()[$fullName];
274
        }
275
276
        return null;
277
    }
278
279
    /**
280
     * @param int $mappedId
281
     *
282
     * @return string
283
     */
284
    private function getMappedType(int $mappedId): string
285
    {
286
        if (!isset($this->configuration->getAccountTypes()[$mappedId])) {
287
            app('log')->warning(sprintf('Cannot find account type for Firefly III account #%d.', $mappedId));
288
            $accountType             = $this->getAccountType($mappedId);
289
            $accountTypes            = $this->configuration->getAccountTypes();
290
            $accountTypes[$mappedId] = $accountType;
291
            $this->configuration->setAccountTypes($accountTypes);
292
293
            return $accountType;
294
        }
295
296
        return $this->configuration->getAccountTypes()[$mappedId] ?? 'expense';
297
    }
298
299
    /**
300
     * @param string $source
301
     * @param string $destination
302
     *
303
     * @throws ImportException
304
     * @return string
305
     */
306
    private function getTransactionType(string $source, string $destination): string
307
    {
308
        $combination = sprintf('%s-%s', $source, $destination);
309
        switch ($combination) {
310
            default:
311
                throw new ImportException(sprintf('Unknown combination: %s and %s', $source, $destination));
312
            case 'asset-expense':
313
                return 'withdrawal';
314
            case 'asset-asset':
315
                return 'transfer';
316
            case 'revenue-asset':
317
                return 'deposit';
318
        }
319
    }
320
}
321