1
|
|
|
<?php |
2
|
|
|
/** |
3
|
|
|
* GroupCollector.php |
4
|
|
|
* Copyright (c) 2019 [email protected] |
5
|
|
|
* |
6
|
|
|
* This file is part of Firefly III (https://github.com/firefly-iii). |
7
|
|
|
* |
8
|
|
|
* This program is free software: you can redistribute it and/or modify |
9
|
|
|
* it under the terms of the GNU Affero General Public License as |
10
|
|
|
* published by the Free Software Foundation, either version 3 of the |
11
|
|
|
* License, or (at your option) any later version. |
12
|
|
|
* |
13
|
|
|
* This program 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 Affero General Public License for more details. |
17
|
|
|
* |
18
|
|
|
* You should have received a copy of the GNU Affero General Public License |
19
|
|
|
* along with this program. If not, see <https://www.gnu.org/licenses/>. |
20
|
|
|
*/ |
21
|
|
|
|
22
|
|
|
declare(strict_types=1); |
23
|
|
|
|
24
|
|
|
namespace FireflyIII\Helpers\Collector; |
25
|
|
|
|
26
|
|
|
use Carbon\Carbon; |
27
|
|
|
use Carbon\Exceptions\InvalidDateException; |
28
|
|
|
use Exception; |
29
|
|
|
use FireflyIII\Helpers\Collector\Extensions\AccountCollection; |
30
|
|
|
use FireflyIII\Helpers\Collector\Extensions\AmountCollection; |
31
|
|
|
use FireflyIII\Helpers\Collector\Extensions\CollectorProperties; |
32
|
|
|
use FireflyIII\Helpers\Collector\Extensions\MetaCollection; |
33
|
|
|
use FireflyIII\Helpers\Collector\Extensions\TimeCollection; |
34
|
|
|
use FireflyIII\Models\Bill; |
35
|
|
|
use FireflyIII\Models\Budget; |
36
|
|
|
use FireflyIII\Models\Category; |
37
|
|
|
use FireflyIII\Models\Tag; |
38
|
|
|
use FireflyIII\Models\TransactionCurrency; |
39
|
|
|
use FireflyIII\Models\TransactionGroup; |
40
|
|
|
use FireflyIII\Models\TransactionJournal; |
41
|
|
|
use FireflyIII\User; |
42
|
|
|
use Illuminate\Database\Eloquent\Builder as EloquentBuilder; |
43
|
|
|
use Illuminate\Database\Eloquent\Relations\HasMany; |
44
|
|
|
use Illuminate\Database\Query\JoinClause; |
45
|
|
|
use Illuminate\Pagination\LengthAwarePaginator; |
46
|
|
|
use Illuminate\Support\Collection; |
47
|
|
|
use Log; |
48
|
|
|
|
49
|
|
|
/** |
50
|
|
|
* Class GroupCollector |
51
|
|
|
* |
52
|
|
|
* @codeCoverageIgnore |
53
|
|
|
*/ |
54
|
|
|
class GroupCollector implements GroupCollectorInterface |
55
|
|
|
{ |
56
|
|
|
use CollectorProperties, AccountCollection, AmountCollection, TimeCollection, MetaCollection; |
|
|
|
|
57
|
|
|
|
58
|
|
|
/** |
59
|
|
|
* Group collector constructor. |
60
|
|
|
*/ |
61
|
|
|
public function __construct() |
62
|
|
|
{ |
63
|
|
|
if ('testing' === config('app.env')) { |
64
|
|
|
app('log')->warning(sprintf('%s should not be instantiated in the TEST environment!', get_class($this))); |
65
|
|
|
} |
66
|
|
|
$this->hasAccountInfo = false; |
67
|
|
|
$this->hasCatInformation = false; |
68
|
|
|
$this->hasBudgetInformation = false; |
69
|
|
|
$this->hasBillInformation = false; |
70
|
|
|
$this->hasJoinedTagTables = false; |
71
|
|
|
$this->hasJoinedAttTables = false; |
72
|
|
|
$this->integerFields = [ |
73
|
|
|
'transaction_group_id', |
74
|
|
|
'user_id', |
75
|
|
|
'transaction_journal_id', |
76
|
|
|
'transaction_type_id', |
77
|
|
|
'order', |
78
|
|
|
'source_transaction_id', |
79
|
|
|
'source_account_id', |
80
|
|
|
'currency_id', |
81
|
|
|
'currency_decimal_places', |
82
|
|
|
'foreign_currency_id', |
83
|
|
|
'foreign_currency_decimal_places', |
84
|
|
|
'destination_transaction_id', |
85
|
|
|
'destination_account_id', |
86
|
|
|
'category_id', |
87
|
|
|
'budget_id', |
88
|
|
|
]; |
89
|
|
|
$this->total = 0; |
90
|
|
|
$this->fields = [ |
91
|
|
|
# group |
92
|
|
|
'transaction_groups.id as transaction_group_id', |
93
|
|
|
'transaction_groups.user_id as user_id', |
94
|
|
|
'transaction_groups.created_at as created_at', |
95
|
|
|
'transaction_groups.updated_at as updated_at', |
96
|
|
|
'transaction_groups.title as transaction_group_title', |
97
|
|
|
|
98
|
|
|
# journal |
99
|
|
|
'transaction_journals.id as transaction_journal_id', |
100
|
|
|
'transaction_journals.transaction_type_id', |
101
|
|
|
'transaction_journals.description', |
102
|
|
|
'transaction_journals.date', |
103
|
|
|
'transaction_journals.order', |
104
|
|
|
|
105
|
|
|
# types |
106
|
|
|
'transaction_types.type as transaction_type_type', |
107
|
|
|
|
108
|
|
|
# source info (always present) |
109
|
|
|
'source.id as source_transaction_id', |
110
|
|
|
'source.account_id as source_account_id', |
111
|
|
|
'source.reconciled', |
112
|
|
|
|
113
|
|
|
# currency info: |
114
|
|
|
'source.amount as amount', |
115
|
|
|
'source.transaction_currency_id as currency_id', |
116
|
|
|
'currency.code as currency_code', |
117
|
|
|
'currency.name as currency_name', |
118
|
|
|
'currency.symbol as currency_symbol', |
119
|
|
|
'currency.decimal_places as currency_decimal_places', |
120
|
|
|
|
121
|
|
|
# foreign currency info |
122
|
|
|
'source.foreign_amount as foreign_amount', |
123
|
|
|
'source.foreign_currency_id as foreign_currency_id', |
124
|
|
|
'foreign_currency.code as foreign_currency_code', |
125
|
|
|
'foreign_currency.name as foreign_currency_name', |
126
|
|
|
'foreign_currency.symbol as foreign_currency_symbol', |
127
|
|
|
'foreign_currency.decimal_places as foreign_currency_decimal_places', |
128
|
|
|
|
129
|
|
|
# destination account info (always present) |
130
|
|
|
'destination.account_id as destination_account_id', |
131
|
|
|
]; |
132
|
|
|
} |
133
|
|
|
|
134
|
|
|
/** |
135
|
|
|
* |
136
|
|
|
*/ |
137
|
|
|
public function dumpQuery(): void |
138
|
|
|
{ |
139
|
|
|
echo $this->query->toSql(); |
140
|
|
|
echo '<pre>'; |
141
|
|
|
print_r($this->query->getBindings()); |
142
|
|
|
echo '</pre>'; |
143
|
|
|
} |
144
|
|
|
|
145
|
|
|
/** |
146
|
|
|
* Return the transaction journals without group information. Is useful in some instances. |
147
|
|
|
* |
148
|
|
|
* @return array |
149
|
|
|
*/ |
150
|
|
|
public function getExtractedJournals(): array |
151
|
|
|
{ |
152
|
|
|
$selection = $this->getGroups(); |
153
|
|
|
$return = []; |
154
|
|
|
/** @var array $group */ |
155
|
|
|
foreach ($selection as $group) { |
156
|
|
|
$count = count($group['transactions']); |
157
|
|
|
foreach ($group['transactions'] as $journalId => $journal) { |
158
|
|
|
$journal['group_title'] = $group['title']; |
159
|
|
|
$journal['journals_in_group'] = $count; |
160
|
|
|
$return[$journalId] = $journal; |
161
|
|
|
} |
162
|
|
|
} |
163
|
|
|
|
164
|
|
|
return $return; |
165
|
|
|
} |
166
|
|
|
|
167
|
|
|
/** |
168
|
|
|
* Return the groups. |
169
|
|
|
* |
170
|
|
|
* @return Collection |
171
|
|
|
*/ |
172
|
|
|
public function getGroups(): Collection |
173
|
|
|
{ |
174
|
|
|
//$start = microtime(true); |
175
|
|
|
/** @var Collection $result */ |
176
|
|
|
$result = $this->query->get($this->fields); |
177
|
|
|
//$end = round(microtime(true) - $start, 5); |
178
|
|
|
// log info about query time. |
179
|
|
|
//Log::info(sprintf('Query took Firefly III %s seconds', $end)); |
180
|
|
|
//Log::info($this->query->toSql(), $this->query->getBindings()); |
181
|
|
|
|
182
|
|
|
// now to parse this into an array. |
183
|
|
|
$collection = $this->parseArray($result); |
184
|
|
|
$this->total = $collection->count(); |
185
|
|
|
|
186
|
|
|
// now filter the array according to the page and the limit (if necessary) |
187
|
|
|
if (null !== $this->limit && null !== $this->page) { |
188
|
|
|
$offset = ($this->page - 1) * $this->limit; |
189
|
|
|
|
190
|
|
|
return $collection->slice($offset, $this->limit); |
191
|
|
|
} |
192
|
|
|
|
193
|
|
|
return $collection; |
194
|
|
|
} |
195
|
|
|
|
196
|
|
|
/** |
197
|
|
|
* Same as getGroups but everything is in a paginator. |
198
|
|
|
* |
199
|
|
|
* @return LengthAwarePaginator |
200
|
|
|
*/ |
201
|
|
|
public function getPaginatedGroups(): LengthAwarePaginator |
202
|
|
|
{ |
203
|
|
|
$set = $this->getGroups(); |
204
|
|
|
|
205
|
|
|
return new LengthAwarePaginator($set, $this->total, $this->limit, $this->page); |
206
|
|
|
} |
207
|
|
|
|
208
|
|
|
/** |
209
|
|
|
* Return the sum of all journals. |
210
|
|
|
* TODO ignores the currency. |
211
|
|
|
* |
212
|
|
|
* @return string |
213
|
|
|
*/ |
214
|
|
|
public function getSum(): string |
215
|
|
|
{ |
216
|
|
|
$journals = $this->getExtractedJournals(); |
217
|
|
|
$sum = '0'; |
218
|
|
|
/** @var array $journal */ |
219
|
|
|
foreach ($journals as $journal) { |
220
|
|
|
$amount = (string) $journal['amount']; |
221
|
|
|
$sum = bcadd($sum, $amount); |
222
|
|
|
} |
223
|
|
|
|
224
|
|
|
return $sum; |
225
|
|
|
} |
226
|
|
|
|
227
|
|
|
/** |
228
|
|
|
* Limit results to a specific currency, either foreign or normal one. |
229
|
|
|
* |
230
|
|
|
* @param TransactionCurrency $currency |
231
|
|
|
* |
232
|
|
|
* @return GroupCollectorInterface |
233
|
|
|
*/ |
234
|
|
|
public function setCurrency(TransactionCurrency $currency): GroupCollectorInterface |
235
|
|
|
{ |
236
|
|
|
$this->query->where( |
237
|
|
|
static function (EloquentBuilder $q) use ($currency) { |
238
|
|
|
$q->where('source.transaction_currency_id', $currency->id); |
239
|
|
|
$q->orWhere('source.foreign_currency_id', $currency->id); |
240
|
|
|
} |
241
|
|
|
); |
242
|
|
|
|
243
|
|
|
return $this; |
244
|
|
|
} |
245
|
|
|
|
246
|
|
|
/** |
247
|
|
|
* Limit the result to a specific transaction group. |
248
|
|
|
* |
249
|
|
|
* @param TransactionGroup $transactionGroup |
250
|
|
|
* |
251
|
|
|
* @return GroupCollectorInterface |
252
|
|
|
*/ |
253
|
|
|
public function setGroup(TransactionGroup $transactionGroup): GroupCollectorInterface |
254
|
|
|
{ |
255
|
|
|
$this->query->where('transaction_groups.id', $transactionGroup->id); |
256
|
|
|
|
257
|
|
|
return $this; |
258
|
|
|
} |
259
|
|
|
|
260
|
|
|
/** |
261
|
|
|
* Limit the result to a set of specific journals. |
262
|
|
|
* |
263
|
|
|
* @param array $journalIds |
264
|
|
|
* |
265
|
|
|
* @return GroupCollectorInterface |
266
|
|
|
*/ |
267
|
|
|
public function setJournalIds(array $journalIds): GroupCollectorInterface |
268
|
|
|
{ |
269
|
|
|
if (count($journalIds) > 0) { |
270
|
|
|
$this->query->whereIn('transaction_journals.id', $journalIds); |
271
|
|
|
} |
272
|
|
|
|
273
|
|
|
return $this; |
274
|
|
|
} |
275
|
|
|
|
276
|
|
|
/** |
277
|
|
|
* Limit the number of returned entries. |
278
|
|
|
* |
279
|
|
|
* @param int $limit |
280
|
|
|
* |
281
|
|
|
* @return GroupCollectorInterface |
282
|
|
|
*/ |
283
|
|
|
public function setLimit(int $limit): GroupCollectorInterface |
284
|
|
|
{ |
285
|
|
|
$this->limit = $limit; |
286
|
|
|
app('log')->debug(sprintf('GroupCollector: The limit is now %d', $limit)); |
287
|
|
|
|
288
|
|
|
return $this; |
289
|
|
|
} |
290
|
|
|
|
291
|
|
|
/** |
292
|
|
|
* Set the page to get. |
293
|
|
|
* |
294
|
|
|
* @param int $page |
295
|
|
|
* |
296
|
|
|
* @return GroupCollectorInterface |
297
|
|
|
*/ |
298
|
|
|
public function setPage(int $page): GroupCollectorInterface |
299
|
|
|
{ |
300
|
|
|
$page = 0 === $page ? 1 : $page; |
301
|
|
|
$this->page = $page; |
302
|
|
|
app('log')->debug(sprintf('GroupCollector: page is now %d', $page)); |
303
|
|
|
|
304
|
|
|
return $this; |
305
|
|
|
} |
306
|
|
|
|
307
|
|
|
/** |
308
|
|
|
* Search for words in descriptions. |
309
|
|
|
* |
310
|
|
|
* @param array $array |
311
|
|
|
* |
312
|
|
|
* @return GroupCollectorInterface |
313
|
|
|
*/ |
314
|
|
|
public function setSearchWords(array $array): GroupCollectorInterface |
315
|
|
|
{ |
316
|
|
|
$this->query->where( |
317
|
|
|
static function (EloquentBuilder $q) use ($array) { |
318
|
|
|
$q->where( |
319
|
|
|
function (EloquentBuilder $q1) use ($array) { |
320
|
|
|
foreach ($array as $word) { |
321
|
|
|
$keyword = sprintf('%%%s%%', $word); |
322
|
|
|
$q1->where('transaction_journals.description', 'LIKE', $keyword); |
323
|
|
|
} |
324
|
|
|
} |
325
|
|
|
); |
326
|
|
|
$q->orWhere( |
327
|
|
|
static function (EloquentBuilder $q2) use ($array) { |
328
|
|
|
foreach ($array as $word) { |
329
|
|
|
$keyword = sprintf('%%%s%%', $word); |
330
|
|
|
$q2->where('transaction_groups.title', 'LIKE', $keyword); |
331
|
|
|
} |
332
|
|
|
} |
333
|
|
|
); |
334
|
|
|
} |
335
|
|
|
); |
336
|
|
|
|
337
|
|
|
return $this; |
338
|
|
|
} |
339
|
|
|
|
340
|
|
|
|
341
|
|
|
/** |
342
|
|
|
* Limit the search to one specific transaction group. |
343
|
|
|
* |
344
|
|
|
* @param TransactionGroup $transactionGroup |
345
|
|
|
* |
346
|
|
|
* @return GroupCollectorInterface |
347
|
|
|
*/ |
348
|
|
|
public function setTransactionGroup(TransactionGroup $transactionGroup): GroupCollectorInterface |
349
|
|
|
{ |
350
|
|
|
$this->query->where('transaction_groups.id', $transactionGroup->id); |
351
|
|
|
|
352
|
|
|
return $this; |
353
|
|
|
} |
354
|
|
|
|
355
|
|
|
/** |
356
|
|
|
* Limit the included transaction types. |
357
|
|
|
* |
358
|
|
|
* @param array $types |
359
|
|
|
* |
360
|
|
|
* @return GroupCollectorInterface |
361
|
|
|
*/ |
362
|
|
|
public function setTypes(array $types): GroupCollectorInterface |
363
|
|
|
{ |
364
|
|
|
$this->query->whereIn('transaction_types.type', $types); |
365
|
|
|
|
366
|
|
|
return $this; |
367
|
|
|
} |
368
|
|
|
|
369
|
|
|
/** |
370
|
|
|
* Set the user object and start the query. |
371
|
|
|
* |
372
|
|
|
* @param User $user |
373
|
|
|
* |
374
|
|
|
* @return GroupCollectorInterface |
375
|
|
|
*/ |
376
|
|
|
public function setUser(User $user): GroupCollectorInterface |
377
|
|
|
{ |
378
|
|
|
$this->user = $user; |
379
|
|
|
$this->startQuery(); |
380
|
|
|
|
381
|
|
|
return $this; |
382
|
|
|
} |
383
|
|
|
|
384
|
|
|
/** |
385
|
|
|
* Automatically include all stuff required to make API calls work. |
386
|
|
|
* |
387
|
|
|
* @return GroupCollectorInterface |
388
|
|
|
*/ |
389
|
|
|
public function withAPIInformation(): GroupCollectorInterface |
390
|
|
|
{ |
391
|
|
|
// include source + destination account name and type. |
392
|
|
|
$this->withAccountInformation() |
393
|
|
|
// include category ID + name (if any) |
394
|
|
|
->withCategoryInformation() |
395
|
|
|
// include budget ID + name (if any) |
396
|
|
|
->withBudgetInformation() |
397
|
|
|
// include bill ID + name (if any) |
398
|
|
|
->withBillInformation(); |
399
|
|
|
|
400
|
|
|
return $this; |
401
|
|
|
} |
402
|
|
|
|
403
|
|
|
/** |
404
|
|
|
* @inheritDoc |
405
|
|
|
*/ |
406
|
|
|
public function withAttachmentInformation(): GroupCollectorInterface |
407
|
|
|
{ |
408
|
|
|
$this->fields[] = 'attachments.id as attachment_id'; |
409
|
|
|
$this->joinAttachmentTables(); |
410
|
|
|
|
411
|
|
|
return $this; |
412
|
|
|
} |
413
|
|
|
|
414
|
|
|
|
415
|
|
|
/** |
416
|
|
|
* Convert a selected set of fields to arrays. |
417
|
|
|
* |
418
|
|
|
* @param array $array |
419
|
|
|
* |
420
|
|
|
* @return array |
421
|
|
|
*/ |
422
|
|
|
private function convertToInteger(array $array): array |
423
|
|
|
{ |
424
|
|
|
foreach ($this->integerFields as $field) { |
425
|
|
|
$array[$field] = isset($array[$field]) ? (int) $array[$field] : null; |
426
|
|
|
} |
427
|
|
|
|
428
|
|
|
return $array; |
429
|
|
|
} |
430
|
|
|
|
431
|
|
|
/** |
432
|
|
|
* Join table to get attachment information. |
433
|
|
|
*/ |
434
|
|
|
private function joinAttachmentTables(): void |
435
|
|
|
{ |
436
|
|
|
if (false === $this->hasJoinedAttTables) { |
437
|
|
|
// join some extra tables: |
438
|
|
|
$this->hasJoinedAttTables = true; |
439
|
|
|
$this->query->leftJoin('attachments', 'attachments.attachable_id', '=', 'transaction_journals.id') |
440
|
|
|
->where( |
441
|
|
|
static function (EloquentBuilder $q1) { |
442
|
|
|
$q1->where('attachments.attachable_type', TransactionJournal::class); |
443
|
|
|
$q1->orWhereNull('attachments.attachable_type'); |
444
|
|
|
} |
445
|
|
|
); |
446
|
|
|
} |
447
|
|
|
} |
448
|
|
|
|
449
|
|
|
/** |
450
|
|
|
* @param array $existingJournal |
451
|
|
|
* @param TransactionJournal $newJournal |
452
|
|
|
* |
453
|
|
|
* @return array |
454
|
|
|
*/ |
455
|
|
|
private function mergeAttachments(array $existingJournal, TransactionJournal $newJournal): array |
456
|
|
|
{ |
457
|
|
|
$newArray = $newJournal->toArray(); |
458
|
|
|
if (isset($newArray['attachment_id'])) { |
459
|
|
|
$attachmentId = (int) $newJournal['tag_id']; |
460
|
|
|
$existingJournal['attachments'][$attachmentId] = [ |
461
|
|
|
'id' => $attachmentId, |
462
|
|
|
]; |
463
|
|
|
} |
464
|
|
|
|
465
|
|
|
return $existingJournal; |
466
|
|
|
} |
467
|
|
|
|
468
|
|
|
/** |
469
|
|
|
* @param array $existingJournal |
470
|
|
|
* @param TransactionJournal $newJournal |
471
|
|
|
* |
472
|
|
|
* @return array |
473
|
|
|
*/ |
474
|
|
|
private function mergeTags(array $existingJournal, TransactionJournal $newJournal): array |
475
|
|
|
{ |
476
|
|
|
$newArray = $newJournal->toArray(); |
477
|
|
|
if (isset($newArray['tag_id'])) { // assume the other fields are present as well. |
478
|
|
|
$tagId = (int) $newJournal['tag_id']; |
479
|
|
|
|
480
|
|
|
$tagDate = null; |
|
|
|
|
481
|
|
|
try { |
482
|
|
|
$tagDate = Carbon::parse($newArray['tag_date']); |
483
|
|
|
} catch (InvalidDateException $e) { |
484
|
|
|
Log::debug(sprintf('Could not parse date: %s', $e->getMessage())); |
485
|
|
|
} |
486
|
|
|
|
487
|
|
|
$existingJournal['tags'][$tagId] = [ |
488
|
|
|
'id' => (int) $newArray['tag_id'], |
489
|
|
|
'name' => $newArray['tag_name'], |
490
|
|
|
'date' => $tagDate, |
491
|
|
|
'description' => $newArray['tag_description'], |
492
|
|
|
]; |
493
|
|
|
} |
494
|
|
|
|
495
|
|
|
return $existingJournal; |
496
|
|
|
} |
497
|
|
|
|
498
|
|
|
/** |
499
|
|
|
* @param Collection $collection |
500
|
|
|
* |
501
|
|
|
* @return Collection |
502
|
|
|
*/ |
503
|
|
|
private function parseArray(Collection $collection): Collection |
504
|
|
|
{ |
505
|
|
|
$groups = []; |
506
|
|
|
/** @var TransactionJournal $augumentedJournal */ |
507
|
|
|
foreach ($collection as $augumentedJournal) { |
508
|
|
|
$groupId = $augumentedJournal->transaction_group_id; |
509
|
|
|
|
510
|
|
|
if (!isset($groups[$groupId])) { |
511
|
|
|
// make new array |
512
|
|
|
$parsedGroup = $this->parseAugmentedJournal($augumentedJournal); |
513
|
|
|
$groupArray = [ |
514
|
|
|
'id' => (int) $augumentedJournal->transaction_group_id, |
515
|
|
|
'user_id' => (int) $augumentedJournal->user_id, |
516
|
|
|
'title' => $augumentedJournal->transaction_group_title, |
517
|
|
|
'transaction_type' => $parsedGroup['transaction_type_type'], |
518
|
|
|
'count' => 1, |
519
|
|
|
'sums' => [], |
520
|
|
|
'transactions' => [], |
521
|
|
|
]; |
522
|
|
|
$journalId = (int) $augumentedJournal->transaction_journal_id; |
523
|
|
|
$groupArray['transactions'][$journalId] = $parsedGroup; |
524
|
|
|
$groups[$groupId] = $groupArray; |
525
|
|
|
continue; |
526
|
|
|
} |
527
|
|
|
// or parse the rest. |
528
|
|
|
$journalId = (int) $augumentedJournal->transaction_journal_id; |
529
|
|
|
$groups[$groupId]['count']++; |
530
|
|
|
|
531
|
|
|
if (isset($groups[$groupId]['transactions'][$journalId])) { |
532
|
|
|
// append data to existing group + journal (for multiple tags or multiple attachments) |
533
|
|
|
$groups[$groupId]['transactions'][$journalId] = $this->mergeTags($groups[$groupId]['transactions'][$journalId], $augumentedJournal); |
534
|
|
|
$groups[$groupId]['transactions'][$journalId] = $this->mergeAttachments($groups[$groupId]['transactions'][$journalId], $augumentedJournal); |
535
|
|
|
} |
536
|
|
|
|
537
|
|
|
if (!isset($groups[$groupId]['transactions'][$journalId])) { |
538
|
|
|
// create second, third, fourth split: |
539
|
|
|
$groups[$groupId]['transactions'][$journalId] = $this->parseAugmentedJournal($augumentedJournal); |
540
|
|
|
} |
541
|
|
|
} |
542
|
|
|
|
543
|
|
|
$groups = $this->parseSums($groups); |
544
|
|
|
|
545
|
|
|
return new Collection($groups); |
546
|
|
|
} |
547
|
|
|
|
548
|
|
|
/** |
549
|
|
|
* @param TransactionJournal $augumentedJournal |
550
|
|
|
* |
551
|
|
|
* @return array |
552
|
|
|
*/ |
553
|
|
|
private function parseAugmentedJournal(TransactionJournal $augumentedJournal): array |
554
|
|
|
{ |
555
|
|
|
$result = $augumentedJournal->toArray(); |
556
|
|
|
$result['tags'] = []; |
557
|
|
|
$result['attachments'] = []; |
558
|
|
|
try { |
559
|
|
|
$result['date'] = new Carbon($result['date']); |
560
|
|
|
$result['created_at'] = new Carbon($result['created_at']); |
561
|
|
|
$result['updated_at'] = new Carbon($result['updated_at']); |
562
|
|
|
} catch (Exception $e) { |
563
|
|
|
Log::error($e->getMessage()); |
564
|
|
|
} |
565
|
|
|
|
566
|
|
|
// convert values to integers: |
567
|
|
|
$result = $this->convertToInteger($result); |
568
|
|
|
|
569
|
|
|
$result['reconciled'] = 1 === (int) $result['reconciled']; |
570
|
|
|
if (isset($augumentedJournal['tag_id'])) { // assume the other fields are present as well. |
571
|
|
|
$tagId = (int) $augumentedJournal['tag_id']; |
572
|
|
|
$tagDate = null; |
|
|
|
|
573
|
|
|
try { |
574
|
|
|
$tagDate = Carbon::parse($augumentedJournal['tag_date']); |
575
|
|
|
} catch (InvalidDateException $e) { |
576
|
|
|
Log::debug(sprintf('Could not parse date: %s', $e->getMessage())); |
577
|
|
|
} |
578
|
|
|
|
579
|
|
|
$result['tags'][$tagId] = [ |
580
|
|
|
'id' => (int) $result['tag_id'], |
581
|
|
|
'name' => $result['tag_name'], |
582
|
|
|
'date' => $tagDate, |
583
|
|
|
'description' => $result['tag_description'], |
584
|
|
|
]; |
585
|
|
|
} |
586
|
|
|
|
587
|
|
|
// also merge attachments: |
588
|
|
|
if (isset($augumentedJournal['attachment_id'])) { |
589
|
|
|
$attachmentId = (int) $augumentedJournal['attachment_id']; |
590
|
|
|
$result['attachments'][$attachmentId] = [ |
591
|
|
|
'id' => $attachmentId, |
592
|
|
|
]; |
593
|
|
|
} |
594
|
|
|
|
595
|
|
|
return $result; |
596
|
|
|
} |
597
|
|
|
|
598
|
|
|
/** |
599
|
|
|
* @param array $groups |
600
|
|
|
* |
601
|
|
|
* @return array |
602
|
|
|
*/ |
603
|
|
|
private function parseSums(array $groups): array |
604
|
|
|
{ |
605
|
|
|
/** |
606
|
|
|
* @var int $groudId |
607
|
|
|
* @var array $group |
608
|
|
|
*/ |
609
|
|
|
foreach ($groups as $groudId => $group) { |
610
|
|
|
/** @var array $transaction */ |
611
|
|
|
foreach ($group['transactions'] as $transaction) { |
612
|
|
|
$currencyId = (int) $transaction['currency_id']; |
613
|
|
|
|
614
|
|
|
// set default: |
615
|
|
|
if (!isset($groups[$groudId]['sums'][$currencyId])) { |
616
|
|
|
$groups[$groudId]['sums'][$currencyId]['currency_id'] = $currencyId; |
617
|
|
|
$groups[$groudId]['sums'][$currencyId]['currency_code'] = $transaction['currency_code']; |
618
|
|
|
$groups[$groudId]['sums'][$currencyId]['currency_symbol'] = $transaction['currency_symbol']; |
619
|
|
|
$groups[$groudId]['sums'][$currencyId]['currency_decimal_places'] = $transaction['currency_decimal_places']; |
620
|
|
|
$groups[$groudId]['sums'][$currencyId]['amount'] = '0'; |
621
|
|
|
} |
622
|
|
|
$groups[$groudId]['sums'][$currencyId]['amount'] = bcadd($groups[$groudId]['sums'][$currencyId]['amount'], $transaction['amount'] ?? '0'); |
623
|
|
|
|
624
|
|
|
if (null !== $transaction['foreign_amount'] && null !== $transaction['foreign_currency_id']) { |
625
|
|
|
$currencyId = (int) $transaction['foreign_currency_id']; |
626
|
|
|
|
627
|
|
|
// set default: |
628
|
|
|
if (!isset($groups[$groudId]['sums'][$currencyId])) { |
629
|
|
|
$groups[$groudId]['sums'][$currencyId]['currency_id'] = $currencyId; |
630
|
|
|
$groups[$groudId]['sums'][$currencyId]['currency_code'] = $transaction['foreign_currency_code']; |
631
|
|
|
$groups[$groudId]['sums'][$currencyId]['currency_symbol'] = $transaction['foreign_currency_symbol']; |
632
|
|
|
$groups[$groudId]['sums'][$currencyId]['currency_decimal_places'] = $transaction['foreign_currency_decimal_places']; |
633
|
|
|
$groups[$groudId]['sums'][$currencyId]['amount'] = '0'; |
634
|
|
|
} |
635
|
|
|
$groups[$groudId]['sums'][$currencyId]['amount'] = bcadd( |
636
|
|
|
$groups[$groudId]['sums'][$currencyId]['amount'], |
637
|
|
|
$transaction['foreign_amount'] ?? '0' |
638
|
|
|
); |
639
|
|
|
} |
640
|
|
|
} |
641
|
|
|
} |
642
|
|
|
|
643
|
|
|
return $groups; |
644
|
|
|
} |
645
|
|
|
|
646
|
|
|
/** |
647
|
|
|
* Build the query. |
648
|
|
|
*/ |
649
|
|
|
private function startQuery(): void |
650
|
|
|
{ |
651
|
|
|
app('log')->debug('GroupCollector::startQuery'); |
652
|
|
|
$this->query = $this->user |
653
|
|
|
//->transactionGroups() |
654
|
|
|
//->leftJoin('transaction_journals', 'transaction_journals.transaction_group_id', 'transaction_groups.id') |
655
|
|
|
->transactionJournals() |
656
|
|
|
->leftJoin('transaction_groups', 'transaction_journals.transaction_group_id', 'transaction_groups.id') |
657
|
|
|
|
658
|
|
|
// join source transaction. |
659
|
|
|
->leftJoin( |
660
|
|
|
'transactions as source', |
661
|
|
|
function (JoinClause $join) { |
662
|
|
|
$join->on('source.transaction_journal_id', '=', 'transaction_journals.id') |
663
|
|
|
->where('source.amount', '<', 0); |
664
|
|
|
} |
665
|
|
|
) |
666
|
|
|
// join destination transaction |
667
|
|
|
->leftJoin( |
668
|
|
|
'transactions as destination', |
669
|
|
|
function (JoinClause $join) { |
670
|
|
|
$join->on('destination.transaction_journal_id', '=', 'transaction_journals.id') |
671
|
|
|
->where('destination.amount', '>', 0); |
672
|
|
|
} |
673
|
|
|
) |
674
|
|
|
// left join transaction type. |
675
|
|
|
->leftJoin('transaction_types', 'transaction_types.id', '=', 'transaction_journals.transaction_type_id') |
676
|
|
|
->leftJoin('transaction_currencies as currency', 'currency.id', '=', 'source.transaction_currency_id') |
677
|
|
|
->leftJoin('transaction_currencies as foreign_currency', 'foreign_currency.id', '=', 'source.foreign_currency_id') |
678
|
|
|
->whereNull('transaction_groups.deleted_at') |
679
|
|
|
->whereNull('transaction_journals.deleted_at') |
680
|
|
|
->whereNull('source.deleted_at') |
681
|
|
|
->whereNull('destination.deleted_at') |
682
|
|
|
->orderBy('transaction_journals.date', 'DESC') |
683
|
|
|
->orderBy('transaction_journals.order', 'ASC') |
684
|
|
|
->orderBy('transaction_journals.id', 'DESC') |
685
|
|
|
->orderBy('transaction_journals.description', 'DESC') |
686
|
|
|
->orderBy('source.amount', 'DESC'); |
687
|
|
|
} |
688
|
|
|
} |
689
|
|
|
|