GitHub Access Token became invalid

It seems like the GitHub access token used for retrieving details about this repository from GitHub became invalid. This might prevent certain types of inspections from being run (in particular, everything related to pull requests).
Please ask an admin of your repository to re-new the access token on this website.

ImportArrayStorage   C
last analyzed

Complexity

Total Complexity 56

Size/Duplication

Total Lines 572
Duplicated Lines 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
wmc 56
eloc 243
c 1
b 0
f 0
dl 0
loc 572
rs 5.5199

17 Methods

Rating   Name   Duplication   Size   Complexity  
A setImportJob() 0 20 1
A linkToTag() 0 43 5
A setStatus() 0 3 1
B storeGroup() 0 42 7
A applyRulesGroup() 0 6 2
A storeGroupArray() 0 20 3
A logDuplicateObject() 0 9 1
A logDuplicateTransfer() 0 8 1
A getTransfers() 0 14 1
A countTransfers() 0 23 5
C transferExists() 0 118 13
A store() 0 26 3
A applyRules() 0 16 2
A getTransactionFromJournal() 0 10 1
A duplicateDetected() 0 26 5
A hashExists() 0 11 2
A getHash() 0 21 3

How to fix   Complexity   

Complex Class

Complex classes like ImportArrayStorage often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use ImportArrayStorage, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
/**
4
 * ImportArrayStorage.php
5
 * Copyright (c) 2019 [email protected]
6
 *
7
 * This file is part of Firefly III (https://github.com/firefly-iii).
8
 *
9
 * This program is free software: you can redistribute it and/or modify
10
 * it under the terms of the GNU Affero General Public License as
11
 * published by the Free Software Foundation, either version 3 of the
12
 * License, or (at your option) any later version.
13
 *
14
 * This program is distributed in the hope that it will be useful,
15
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
16
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
17
 * GNU Affero General Public License for more details.
18
 *
19
 * You should have received a copy of the GNU Affero General Public License
20
 * along with this program.  If not, see <https://www.gnu.org/licenses/>.
21
 */
22
23
declare(strict_types=1);
24
25
namespace FireflyIII\Import\Storage;
26
27
use Carbon\Carbon;
28
use DB;
29
use Exception;
30
use FireflyIII\Events\RequestedReportOnJournals;
31
use FireflyIII\Exceptions\FireflyException;
32
use FireflyIII\Helpers\Collector\GroupCollectorInterface;
33
use FireflyIII\Models\ImportJob;
34
use FireflyIII\Models\Preference;
35
use FireflyIII\Models\TransactionGroup;
36
use FireflyIII\Models\TransactionJournal;
37
use FireflyIII\Models\TransactionType;
38
use FireflyIII\Repositories\ImportJob\ImportJobRepositoryInterface;
39
use FireflyIII\Repositories\Journal\JournalRepositoryInterface;
40
use FireflyIII\Repositories\Tag\TagRepositoryInterface;
41
use FireflyIII\Repositories\TransactionGroup\TransactionGroupRepositoryInterface;
42
use FireflyIII\TransactionRules\Engine\RuleEngine;
43
use Illuminate\Database\QueryException;
44
use Illuminate\Support\Collection;
45
use Log;
46
47
/**
48
 * Creates new transactions based on arrays.
49
 *
50
 * Class ImportArrayStorage
51
 *
52
 * @codeCoverageIgnore
53
 * @deprecated
54
 *
55
 */
56
class ImportArrayStorage
57
{
58
    /** @var int Number of hits required for a transfer to match. */
59
    private const REQUIRED_HITS = 4;
60
    /** @var bool Check for transfers during import. */
61
    private $checkForTransfers = false;
62
    /** @var TransactionGroupRepositoryInterface */
63
    private $groupRepos;
64
    /** @var ImportJob The import job */
65
    private $importJob;
66
    /** @var JournalRepositoryInterface Journal repository for storage. */
67
    private $journalRepos;
68
    /** @var string */
69
    private $language = 'en_US';
70
    /** @var ImportJobRepositoryInterface Import job repository */
71
    private $repository;
72
    /** @var array The transfers the user already has. */
73
    private $transfers;
74
75
    /**
76
     * Set job, count transfers in the array and create the repository.
77
     *
78
     * @param ImportJob $importJob
79
     */
80
    public function setImportJob(ImportJob $importJob): void
81
    {
82
        $this->importJob  = $importJob;
83
        $this->repository = app(ImportJobRepositoryInterface::class);
84
        $this->repository->setUser($importJob->user);
85
86
        $this->countTransfers();
87
88
        $this->journalRepos = app(JournalRepositoryInterface::class);
89
        $this->journalRepos->setUser($importJob->user);
90
91
        $this->groupRepos = app(TransactionGroupRepositoryInterface::class);
92
        $this->groupRepos->setUser($importJob->user);
93
94
        // get language of user.
95
        /** @var Preference $pref */
96
        $pref           = app('preferences')->getForUser($importJob->user, 'language', config('firefly.default_language', 'en_US'));
97
        $this->language = $pref->data;
98
99
        Log::debug('Constructed ImportArrayStorage()');
100
    }
101
102
    /**
103
     * Actually does the storing. Does three things.
104
     * - Store journals
105
     * - Link to tag
106
     * - Run rules (if set to)
107
     *
108
     * @throws FireflyException
109
     * @return Collection
110
     */
111
    public function store(): Collection
112
    {
113
        // store transactions
114
        $this->setStatus('storing_data');
115
        $collection = $this->storeGroupArray();
116
        $this->setStatus('stored_data');
117
118
        // link tag:
119
        $this->setStatus('linking_to_tag');
120
        $this->linkToTag($collection);
121
        $this->setStatus('linked_to_tag');
122
123
        // run rules, if configured to.
124
        $config = $this->importJob->configuration;
125
        if (isset($config['apply-rules']) && true === $config['apply-rules']) {
126
            $this->setStatus('applying_rules');
127
            $this->applyRules($collection);
128
            $this->setStatus('rules_applied');
129
        }
130
131
        app('preferences')->mark();
132
133
        // email about this:
134
        event(new RequestedReportOnJournals((int) $this->importJob->user_id, $collection));
135
136
        return $collection;
137
    }
138
139
    /**
140
     * Applies the users rules to the created journals.
141
     *
142
     * @param Collection $collection
143
     *
144
     */
145
    private function applyRules(Collection $collection): void
146
    {
147
        Log::debug('Now in applyRules()');
148
149
        /** @var RuleEngine $ruleEngine */
150
        $ruleEngine = app(RuleEngine::class);
151
        $ruleEngine->setUser($this->importJob->user);
152
        $ruleEngine->setAllRules(true);
153
154
        // for this call, the rule engine only includes "store" rules:
155
        $ruleEngine->setTriggerMode(RuleEngine::TRIGGER_STORE);
156
        Log::debug('Start of engine loop');
157
        foreach ($collection as $group) {
158
            $this->applyRulesGroup($ruleEngine, $group);
159
        }
160
        Log::debug('End of engine loop.');
161
    }
162
163
    /**
164
     * @param RuleEngine       $ruleEngine
165
     * @param TransactionGroup $group
166
     */
167
    private function applyRulesGroup(RuleEngine $ruleEngine, TransactionGroup $group): void
168
    {
169
        Log::debug(sprintf('Processing group #%d', $group->id));
170
        foreach ($group->transactionJournals as $journal) {
171
            Log::debug(sprintf('Processing journal #%d from group #%d', $journal->id, $group->id));
172
            $ruleEngine->processTransactionJournal($journal);
173
        }
174
    }
175
176
    /**
177
     * Count the number of transfers in the array. If this is zero, don't bother checking for double transfers.
178
     */
179
    private function countTransfers(): void
180
    {
181
        Log::debug('Now in countTransfers()');
182
        /** @var array $array */
183
        $array = $this->repository->getTransactions($this->importJob);
184
185
186
        $count = 0;
187
        foreach ($array as $index => $group) {
188
189
            foreach ($group['transactions'] as $transaction) {
190
                if (strtolower(TransactionType::TRANSFER) === strtolower($transaction['type'])) {
191
                    $count++;
192
                    Log::debug(sprintf('Row #%d is a transfer, increase count to %d', $index + 1, $count));
193
                }
194
            }
195
        }
196
        Log::debug(sprintf('Count of transfers in import array is %d.', $count));
197
        if ($count > 0) {
198
            $this->checkForTransfers = true;
199
            Log::debug('Will check for duplicate transfers.');
200
            // get users transfers. Needed for comparison.
201
            $this->getTransfers();
202
        }
203
    }
204
205
    /**
206
     * @param int   $index
207
     * @param array $group
208
     *
209
     * @return bool
210
     */
211
    private function duplicateDetected(int $index, array $group): bool
212
    {
213
        Log::debug(sprintf('Now in duplicateDetected(%d)', $index));
214
        $transactions = $group['transactions'] ?? [];
215
        foreach ($transactions as $transaction) {
216
            $hash       = $this->getHash($transaction);
217
            $existingId = $this->hashExists($hash);
218
            if (null !== $existingId) {
219
                $message = (string) trans('import.duplicate_row', ['row' => $index, 'description' => $transaction['description']]);
220
                $this->logDuplicateObject($transaction, $existingId);
221
                $this->repository->addErrorMessage($this->importJob, $message);
222
223
                return true;
224
            }
225
226
            // do transfer detection:
227
            if ($this->checkForTransfers && $this->transferExists($transaction)) {
228
                $message = (string) trans('import.duplicate_row', ['row' => $index, 'description' => $transaction['description']]);
229
                $this->logDuplicateTransfer($transaction);
230
                $this->repository->addErrorMessage($this->importJob, $message);
231
232
                return true;
233
            }
234
        }
235
236
        return false;
237
    }
238
239
    /**
240
     * Get hash of transaction.
241
     *
242
     * @param array $transaction
243
     *
244
     * @return string
245
     */
246
    private function getHash(array $transaction): string
247
    {
248
        unset($transaction['import_hash_v2'], $transaction['original_source']);
249
        $json = json_encode($transaction, JSON_THROW_ON_ERROR);
250
        if (false === $json) {
251
            // @codeCoverageIgnoreStart
252
            /** @noinspection ForgottenDebugOutputInspection */
253
            Log::error('Could not encode import array.', $transaction);
254
            try {
255
                $json = random_int(1, 10000);
256
            } catch (Exception $e) {
257
                // seriously?
258
                Log::error(sprintf('random_int() just failed. I want a medal: %s', $e->getMessage()));
259
            }
260
            // @codeCoverageIgnoreEnd
261
        }
262
263
        $hash = hash('sha256', $json);
264
        Log::debug(sprintf('The hash is: %s', $hash), $transaction);
265
266
        return $hash;
267
    }
268
269
    /**
270
     * @param TransactionGroup $transactionGroup
271
     *
272
     * @return array
273
     */
274
    private function getTransactionFromJournal(TransactionGroup $transactionGroup): array
275
    {
276
        // collect transactions using the journal collector
277
        /** @var GroupCollectorInterface $collector */
278
        $collector = app(GroupCollectorInterface::class);
279
280
        $collector->setUser($this->importJob->user);
281
        $collector->setGroup($transactionGroup);
282
283
        return $collector->getExtractedJournals();
284
    }
285
286
    /**
287
     * Get the users transfers, so they can be compared to whatever the user is trying to import.
288
     */
289
    private function getTransfers(): void
290
    {
291
        Log::debug('Now in getTransfers()');
292
        app('preferences')->mark();
293
294
        /** @var GroupCollectorInterface $collector */
295
        $collector = app(GroupCollectorInterface::class);
296
297
        $collector->setUser($this->importJob->user);
298
        $collector
299
            ->setTypes([TransactionType::TRANSFER])->setLimit(10000)->setPage(1)
300
            ->withAccountInformation();
301
        $this->transfers = $collector->getExtractedJournals();
302
        Log::debug(sprintf('Count of getTransfers() is %d', count($this->transfers)));
303
    }
304
305
    /**
306
     * Check if the hash exists for the array the user wants to import.
307
     *
308
     * @param string $hash
309
     *
310
     * @return int|null
311
     */
312
    private function hashExists(string $hash): ?int
313
    {
314
        $entry = $this->journalRepos->findByHash($hash);
315
        if (null === $entry) {
316
            Log::debug(sprintf('Found no transactions with hash %s.', $hash));
317
318
            return null;
319
        }
320
        Log::info(sprintf('Found a transaction journal with an existing hash: %s', $hash));
321
322
        return (int) $entry->transaction_journal_id;
323
    }
324
325
    /**
326
     * Link all imported journals to a tag.
327
     *
328
     * @param Collection $collection
329
     */
330
    private function linkToTag(Collection $collection): void
331
    {
332
        if (0 === $collection->count()) {
333
            return;
334
        }
335
        /** @var TagRepositoryInterface $repository */
336
        $repository = app(TagRepositoryInterface::class);
337
        $repository->setUser($this->importJob->user);
338
        $data = [
339
            'tag'         => (string) trans('import.import_with_key', ['key' => $this->importJob->key]),
340
            'date'        => new Carbon,
341
            'description' => null,
342
            'latitude'    => null,
343
            'longitude'   => null,
344
            'zoom_level'  => null,
345
            'tagMode'     => 'nothing',
346
        ];
347
        $tag  = $repository->store($data);
348
349
        Log::debug(sprintf('Created tag #%d ("%s")', $tag->id, $tag->tag));
350
        Log::debug('Looping groups...');
351
352
        // TODO double loop.
353
354
        /** @var TransactionGroup $group */
355
        foreach ($collection as $group) {
356
            Log::debug(sprintf('Looping journals in group #%d', $group->id));
357
            /** @var TransactionJournal $journal */
358
            $journalIds = $group->transactionJournals->pluck('id')->toArray();
359
            $tagId      = $tag->id;
360
            foreach ($journalIds as $journalId) {
361
                Log::debug(sprintf('Linking journal #%d to tag #%d...', $journalId, $tagId));
362
                // @codeCoverageIgnoreStart
363
                try {
364
                    DB::table('tag_transaction_journal')->insert(['transaction_journal_id' => $journalId, 'tag_id' => $tagId]);
365
                } catch (QueryException $e) {
366
                    Log::error(sprintf('Could not link journal #%d to tag #%d because: %s', $journalId, $tagId, $e->getMessage()));
367
                }
368
                // @codeCoverageIgnoreEnd
369
            }
370
            Log::info(sprintf('Linked %d journals to tag #%d ("%s")', $collection->count(), $tag->id, $tag->tag));
371
        }
372
        $this->repository->setTag($this->importJob, $tag);
373
374
    }
375
376
    /**
377
     * Log about a duplicate object (double hash).
378
     *
379
     * @param array $transaction
380
     * @param int   $existingId
381
     */
382
    private function logDuplicateObject(array $transaction, int $existingId): void
383
    {
384
        Log::info(
385
            'Transaction is a duplicate, and will not be imported (the hash exists).',
386
            [
387
                'existing'    => $existingId,
388
                'description' => $transaction['description'] ?? '',
389
                'amount'      => $transaction['transactions'][0]['amount'] ?? 0,
390
                'date'        => $transaction['date'] ?? '',
391
            ]
392
        );
393
394
    }
395
396
    /**
397
     * Log about a duplicate transfer.
398
     *
399
     * @param array $transaction
400
     */
401
    private function logDuplicateTransfer(array $transaction): void
402
    {
403
        Log::info(
404
            'Transaction is a duplicate transfer, and will not be imported (such a transfer exists already).',
405
            [
406
                'description' => $transaction['description'] ?? '',
407
                'amount'      => $transaction['transactions'][0]['amount'] ?? 0,
408
                'date'        => $transaction['date'] ?? '',
409
            ]
410
        );
411
    }
412
413
    /**
414
     * Shorthand method to quickly set job status
415
     *
416
     * @param string $status
417
     */
418
    private function setStatus(string $status): void
419
    {
420
        $this->repository->setStatus($this->importJob, $status);
421
    }
422
423
    /**
424
     * @param int   $index
425
     * @param array $group
426
     *
427
     * @return TransactionGroup|null
428
     */
429
    private function storeGroup(int $index, array $group): ?TransactionGroup
430
    {
431
        Log::debug(sprintf('Going to store entry #%d', $index + 1));
432
433
        // do some basic error catching.
434
        foreach ($group['transactions'] as $groupIndex => $transaction) {
435
            $group['transactions'][$groupIndex]['date']        = Carbon::parse($transaction['date'], config('app.timezone'));
436
            $group['transactions'][$groupIndex]['description'] = '' === $transaction['description'] ? '(empty description)' : $transaction['description'];
437
        }
438
439
        // do duplicate detection!
440
        if ($this->duplicateDetected($index, $group)) {
441
            Log::warning(sprintf('Row #%d seems to be a imported already and will be ignored.', $index));
442
443
            return null;
444
        }
445
446
        // store the group
447
        try {
448
            $newGroup = $this->groupRepos->store($group);
449
            // @codeCoverageIgnoreStart
450
        } catch (FireflyException $e) {
451
            Log::error($e->getMessage());
452
            Log::error($e->getTraceAsString());
453
            $this->repository->addErrorMessage($this->importJob, sprintf('Row #%d could not be imported. %s', $index, $e->getMessage()));
454
455
            return null;
456
        }
457
        // @codeCoverageIgnoreEnd
458
        Log::debug(sprintf('Stored as group #%d', $newGroup->id));
459
460
        // add to collection of transfers, if necessary:
461
        if ('transfer' === strtolower($group['transactions'][0]['type'])) {
462
            $journals = $this->getTransactionFromJournal($newGroup);
463
            Log::debug('We just stored a transfer, so add the journal to the list of transfers.');
464
            foreach ($journals as $newJournal) {
465
                $this->transfers[] = $newJournal;
466
            }
467
            Log::debug(sprintf('List length is now %d', count($this->transfers)));
468
        }
469
470
        return $newGroup;
471
    }
472
473
    /**
474
     * Store array as journals.
475
     *
476
     * @throws FireflyException
477
     *
478
     * @return Collection
479
     */
480
    private function storeGroupArray(): Collection
481
    {
482
        /** @var array $array */
483
        $array = $this->repository->getTransactions($this->importJob);
484
        $count = count($array);
485
486
        Log::notice(sprintf('Will now store the groups. Count of groups is %d.', $count));
487
        Log::notice('Going to store...');
488
489
        $collection = new Collection;
490
        foreach ($array as $index => $group) {
491
            Log::debug(sprintf('Now store #%d', $index + 1));
492
            $result = $this->storeGroup($index, $group);
493
            if (null !== $result) {
494
                $collection->push($result);
495
            }
496
        }
497
        Log::notice(sprintf('Done storing. Firefly III has stored %d transactions.', $collection->count()));
498
499
        return $collection;
500
    }
501
502
    /**
503
     * Check if a transfer exists.
504
     *
505
     * @param array $transaction
506
     *
507
     * @return bool
508
     *
509
     */
510
    private function transferExists(array $transaction): bool
511
    {
512
        Log::debug('transferExists() Check if transaction is a double transfer.');
513
514
        // how many hits do we need?
515
        Log::debug(sprintf('System has %d existing transfers', count($this->transfers)));
516
        // loop over each split:
517
518
        // check if is a transfer
519
        if (strtolower(TransactionType::TRANSFER) !== strtolower($transaction['type'])) {
520
            // @codeCoverageIgnoreStart
521
            Log::debug(sprintf('Is a %s, not a transfer so no.', $transaction['type']));
522
523
            return false;
524
            // @codeCoverageIgnoreEnd
525
        }
526
527
528
        Log::debug(sprintf('Required hits for transfer comparison is %d', self::REQUIRED_HITS));
529
530
        // get the amount:
531
        /** @noinspection UnnecessaryCastingInspection */
532
        $amount = (string) ($transaction['amount'] ?? '0');
533
        if (bccomp($amount, '0') === -1) {
534
            $amount = bcmul($amount, '-1'); // @codeCoverageIgnore
535
        }
536
537
        // get the description:
538
        //$description = '' === (string)$transaction['description'] ? $transaction['description'] : $transaction['description'];
539
        $description = (string) $transaction['description'];
540
541
        // get the source and destination ID's:
542
        $transactionSourceIDs = [(int) $transaction['source_id'], (int) $transaction['destination_id']];
543
        sort($transactionSourceIDs);
544
545
        // get the source and destination names:
546
        $transactionSourceNames = [(string) $transaction['source_name'], (string) $transaction['destination_name']];
547
        sort($transactionSourceNames);
548
549
        // then loop all transfers:
550
        /** @var array $transfer */
551
        foreach ($this->transfers as $transfer) {
552
            // number of hits for this split-transfer combination:
553
            $hits = 0;
554
            Log::debug(sprintf('Now looking at transaction journal #%d', $transfer['transaction_journal_id']));
555
            // compare amount:
556
            $originalAmount = app('steam')->positive($transfer['amount']);
557
            Log::debug(sprintf('Amount %s compared to %s', $amount, $originalAmount));
558
            if (0 !== bccomp($amount, $originalAmount)) {
559
                Log::debug('Amount is not a match, continue with next transfer.');
560
                continue;
561
            }
562
            ++$hits;
563
            Log::debug(sprintf('Comparison is a hit! (%s)', $hits));
564
565
            // compare description:
566
            // $comparison = '(empty description)' === $transfer['description'] ? '' : $transfer['description'];
567
            $comparison = $transfer['description'];
568
            Log::debug(sprintf('Comparing "%s" to "%s" (original: "%s")', $description, $transfer['description'], $comparison));
569
            if ($description !== $comparison) {
570
                Log::debug('Description is not a match, continue with next transfer.');
571
                continue; // @codeCoverageIgnore
572
            }
573
            ++$hits;
574
            Log::debug(sprintf('Comparison is a hit! (%s)', $hits));
575
576
            // compare date:
577
            $transferDate    = $transfer['date']->format('Y-m-d H:i:s');
578
            $transactionDate = $transaction['date']->format('Y-m-d H:i:s');
579
            Log::debug(sprintf('Comparing dates "%s" to "%s"', $transactionDate, $transferDate));
580
            if ($transactionDate !== $transferDate) {
581
                Log::debug('Date is not a match, continue with next transfer.');
582
                continue; // @codeCoverageIgnore
583
            }
584
            ++$hits;
585
            Log::debug(sprintf('Comparison is a hit! (%s)', $hits));
586
587
            // compare source and destination id's
588
            $transferSourceIDs = [(int) $transfer['source_account_id'], (int) $transfer['destination_account_id']];
589
            sort($transferSourceIDs);
590
            /** @noinspection DisconnectedForeachInstructionInspection */
591
            Log::debug('Comparing current transaction source+dest IDs', $transactionSourceIDs);
592
            Log::debug('.. with current transfer source+dest IDs', $transferSourceIDs);
593
            if ($transactionSourceIDs === $transferSourceIDs) {
594
                ++$hits;
595
                Log::debug(sprintf('Source IDs are the same! (%d)', $hits));
596
            }
597
            if ($transactionSourceIDs !== $transferSourceIDs) {
598
                Log::debug('Source IDs are not the same.');
599
            }
600
            unset($transferSourceIDs);
601
602
            // compare source and destination names
603
            $transferSource = [(string) ($transfer['source_account_name'] ?? ''), (string) ($transfer['destination_account_name'] ?? '')];
604
            sort($transferSource);
605
            /** @noinspection DisconnectedForeachInstructionInspection */
606
            Log::debug('Comparing current transaction source+dest names', $transactionSourceNames);
607
            Log::debug('.. with current transfer source+dest names', $transferSource);
608
            if ($transactionSourceNames === $transferSource && $transferSource !== ['', '']) {
609
                // @codeCoverageIgnoreStart
610
                ++$hits;
611
                Log::debug(sprintf('Source names are the same! (%d)', $hits));
612
                // @codeCoverageIgnoreEnd
613
            }
614
            if ($transactionSourceNames !== $transferSource) {
615
                Log::debug('Source names are not the same.');
616
            }
617
618
            Log::debug(sprintf('Number of hits is %d', $hits));
619
            if ($hits >= self::REQUIRED_HITS) {
620
                Log::debug(sprintf('Is more than %d, return true.', self::REQUIRED_HITS));
621
622
                return true;
623
            }
624
        }
625
        Log::debug('Is not an existing transfer, return false.');
626
627
        return false;
628
    }
629
630
}
631