Failed Conditions
Push — master ( fb5ae8...a3d745 )
by Adrien
13:37
created

Importer::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 2
nc 1
nop 0
dl 0
loc 4
rs 10
c 0
b 0
f 0
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Application\Service;
6
7
use Application\Api\Exception;
8
use Application\Model\Account;
9
use Application\Model\Transaction;
10
use Application\Model\TransactionLine;
11
use Application\Model\User;
12
use Application\Repository\AccountRepository;
13
use Application\Repository\UserRepository;
14
use Cake\Chronos\Chronos;
15
use Genkgo\Camt\Config;
16
use Genkgo\Camt\DTO\Address;
17
use Genkgo\Camt\DTO\Entry;
18
use Genkgo\Camt\DTO\EntryTransactionDetail;
19
use Genkgo\Camt\DTO\Message;
20
use Genkgo\Camt\DTO\Record;
21
use Genkgo\Camt\DTO\RelatedPartyTypeInterface;
22
use Genkgo\Camt\Reader;
23
24
/**
25
 * This service allows to import a CAMT file as Transaction and TransactionLine
26
 *
27
 * @see https://www.six-group.com/interbank-clearing/dam/downloads/en/standardization/iso/swiss-recommendations/archives/implementation-guidelines-cm/standardization_isopayments_iso_20022_ch_implementation_guidelines_camt.pdf
28
 */
29
class Importer
30
{
31
    /**
32
     * @var Message
33
     */
34
    private $message;
35
36
    /**
37
     * @var Transaction[]
38
     */
39
    private $transactions = [];
40
41
    /**
42
     * @var Account
43
     */
44
    private $bankAccount;
45
46
    /**
47
     * @var AccountRepository
48
     */
49
    private $accountRepository;
50
51
    /**
52
     * @var UserRepository
53
     */
54
    private $userRepository;
55
56
    public function __construct()
57
    {
58
        $this->accountRepository = _em()->getRepository(Account::class);
59
        $this->userRepository = _em()->getRepository(User::class);
60
    }
61
62
    /**
63
     * Import all transactions from a CAMT file
64
     *
65
     * @param string $file
66
     *
67
     * @return Transaction[]
68
     */
69
    public function import(string $file): array
70
    {
71
        $this->transactions = [];
72
        $reader = new Reader(Config::getDefault());
73
        $this->message = $reader->readFile($file);
74
75
        $this->userRepository->getAclFilter()->setEnabled(false);
76
77
        try {
78
            $records = $this->message->getRecords();
79
            foreach ($records as $record) {
80
                $this->bankAccount = $this->loadAccount($record);
81
82
                foreach ($record->getEntries() as $entry) {
83
                    $this->importTransaction($entry);
84
                }
85
            }
86
        } finally {
87
            $this->userRepository->getAclFilter()->setEnabled(true);
88
        }
89
90
        return $this->transactions;
91
    }
92
93
    /**
94
     * @param Entry $entry
95
     */
96
    private function importTransaction(Entry $entry): void
97
    {
98
        $nativeDate = $entry->getValueDate();
99
        $date = Chronos::instance($nativeDate);
100
101
        $transaction = new Transaction();
102
        $transaction->setName('Versement BVR');
103
        $transaction->setTransactionDate($date);
104
105
        $internalRemarks = [];
106
        foreach ($entry->getTransactionDetails() as $detail) {
107
            $internalRemarks[] = $this->importTransactionLine($transaction, $detail);
108
109
            // Use same owner for line and transaction
110
            $transaction->setOwner($transaction->getTransactionLines()->first()->getOwner());
111
        }
112
        $transaction->setInternalRemarks(implode(PHP_EOL . PHP_EOL, $internalRemarks));
113
114
        // Don't persist transaction that may not have any lines
115
        if ($transaction->getTransactionLines()->count()) {
116
            _em()->persist($transaction);
117
            $this->transactions[] = $transaction;
118
        }
119
    }
120
121
    private function importTransactionLine(Transaction $transaction, EntryTransactionDetail $detail): string
122
    {
123
        $referenceNumber = $detail->getRemittanceInformation()->getStructuredBlock()->getCreditorReferenceInformation()->getRef();
124
        $user = $this->loadUser($referenceNumber);
125
        $userAccount = $this->accountRepository->getOrCreate($user);
126
        $remarks = $this->getRemarks($detail, $referenceNumber);
127
        $amount = $detail->getAmount()->getAmount()->getAmount();
128
        $amount = bcdiv($amount, '100', 2);
129
130
        $accountServicerReference = $detail->getReference()->getAccountServiceReference();
131
        if (!$accountServicerReference) {
132
            throw new Exception('Cannot import a transaction without a account servicer reference to store a universal identifier.');
133
        }
134
135
        $line = new TransactionLine();
136
        $line->setTransaction($transaction);
137
        $line->setOwner($user);
138
        $line->setName('Versement BVR');
139
        $line->setTransactionDate($transaction->getTransactionDate());
140
        $line->setBalance($amount);
141
        $line->setCredit($userAccount);
142
        $line->setDebit($this->bankAccount);
143
        $line->setImportedId($accountServicerReference);
144
145
        _em()->persist($line);
146
147
        return $remarks;
148
    }
149
150
    private function partyToString(RelatedPartyTypeInterface $party): string
151
    {
152
        $parts = [];
153
        $parts[] = $this->partyLabel($party);
154
        $parts[] = $party->getName();
155
156
        $address = $party->getAddress();
157
        if ($address) {
158
            $parts[] = $this->addressToString($address);
159
        }
160
161
        return implode(PHP_EOL, $parts);
162
    }
163
164
    private function partyLabel(RelatedPartyTypeInterface $party): string
165
    {
166
        $class = get_class($party);
167
        switch ($class) {
168
            case \Genkgo\Camt\DTO\Recipient::class:
169
                return 'Récipient';
170
            case \Genkgo\Camt\DTO\Debtor::class:
171
                return 'Débiteur';
172
            case \Genkgo\Camt\DTO\Creditor::class:
173
                return 'Créancier';
174
            case \Genkgo\Camt\DTO\UltimateDebtor::class:
175
                return 'Débiteur final';
176
            case \Genkgo\Camt\DTO\UltimateCreditor::class:
177
                return 'Créancier final';
178
            default:
179
                throw new Exception('Non supported related party type: ' . $class);
180
        }
181
    }
182
183
    private function addressToString(Address $a): string
184
    {
185
        $lines = [];
186
        $lines[] = trim($a->getStreetName() . ' ' . $a->getBuildingNumber());
187
        $lines[] = trim($a->getPostCode() . ' ' . $a->getTownName());
188
        $lines[] = $a->getCountry();
189
        $lines = array_merge($lines, $a->getAddressLines());
190
191
        $nonEmptyLines = array_filter($lines);
192
193
        return implode(PHP_EOL, $nonEmptyLines);
194
    }
195
196
    /**
197
     * @param EntryTransactionDetail $detail
198
     * @param string $referenceNumber
199
     *
200
     * @return string
201
     */
202
    private function getRemarks(EntryTransactionDetail $detail, string $referenceNumber): string
203
    {
204
        $parts = [];
205
        $parts[] = 'Numéro de référence: ' . $referenceNumber;
206
207
        foreach ($detail->getRelatedParties() as $party) {
208
            $partyDetail = $party->getRelatedPartyType();
209
            $parts[] = $this->partyToString($partyDetail);
210
        }
211
212
        $remarks = implode(PHP_EOL . PHP_EOL, $parts);
213
214
        return $remarks;
215
    }
216
217
    /**
218
     * @param Record $record
219
     *
220
     * @return Account
221
     */
222
    private function loadAccount(Record $record): Account
223
    {
224
        $accountFromFile = $record->getAccount();
225
        $iban = $accountFromFile->getIdentification();
226
        $account = $this->accountRepository->findOneByIban($iban);
0 ignored issues
show
Bug introduced by
The method findOneByIban() does not exist on Application\Repository\AccountRepository. Since you implemented __call, consider adding a @method annotation. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

226
        /** @scrutinizer ignore-call */ 
227
        $account = $this->accountRepository->findOneByIban($iban);
Loading history...
227
228
        if (!$account) {
229
            throw new Exception('The CAMT file contains a statement for account with IBAN `' . $iban . '`, but no account exist for that IBAN in the database. Either create/update a corresponding account, or import a different CAMT file.');
230
        }
231
232
        return $account;
233
    }
234
235
    /**
236
     * @param string $referenceNumber
237
     *
238
     * @return User
239
     */
240
    private function loadUser(string $referenceNumber): User
241
    {
242
        $userId = (int) Bvr::extractCustomId($referenceNumber);
243
        $user = $this->userRepository->getOneById($userId);
244
245
        if (!$user) {
246
            throw new Exception('Could not find a matching user for reference number `' . $referenceNumber . '`.');
247
        }
248
249
        return $user;
250
    }
251
}
252