Issues (20)

src/Responses/PurchaseResponse.php (2 issues)

1
<?php
2
3
namespace DansMaCulotte\Monetico\Responses;
4
5
use DansMaCulotte\Monetico\Exceptions\AuthenticationException;
6
use DansMaCulotte\Monetico\Exceptions\Exception;
7
use DansMaCulotte\Monetico\Exceptions\PurchaseException;
8
use DansMaCulotte\Monetico\Resources\AuthenticationResource;
9
use DateTime;
10
11
class PurchaseResponse extends AbstractResponse
12
{
13
    /** @var string */
14
    public $eptCode;
15
16
    /** @var \DateTime */
17
    public $dateTime;
18
19
    /** @var string */
20
    public $amount;
21
22
    /** @var string */
23
    public $seal;
24
25
    /** @var string */
26
    public $cardVerificationStatus;
27
28
    /** @var string */
29
    public $cardExpirationDate;
30
31
    /** @var string */
32
    public $cardBrand;
33
34
    /** @var string */
35
    public $cardCountry;
36
37
    /** @var string */
38
    public $cardBIN;
39
40
    /** @var string */
41
    public $cardHash;
42
43
    /** @var bool */
44
    public $cardSaved = null;
45
46
    /** @var string */
47
    public $cardMask = null;
48
49
    /** @var string */
50
    public $rejectReason = null;
51
52
    /** @var string */
53
    public $authNumber;
54
55
    /** @var string */
56
    public $clientIp;
57
58
    /** @var string */
59
    public $transactionCountry;
60
61
    /** @var string */
62
    public $paymentMethod = null;
63
64
    /** @var string */
65
    public $commitmentAmount = null;
66
67
    /** @var int */
68
    public $filteredReason = null;
69
70
    /** @var string */
71
    public $filteredValue = null;
72
73
    /** @var string */
74
    public $filteredStatus = null;
75
76
    /** @var AuthenticationResource */
77
    public $authentication = null;
78
79
    /** @var string */
80
    public $authenticationHash = null;
81
82
    /** @var string */
83
    public $cardType;
84
85
    /** @var string */
86
    public $accountType;
87
88
    /** @var string */
89
    public $virtualCard;
90
91
    /** @var string */
92
    const DATETIME_FORMAT = 'd/m/Y_\a_H:i:s';
93
94
    /** @var array */
95
    const RETURN_CODES = [
96
        'payetest',
97
        'paiement',
98
        'Annulation',
99
        'paiement_pf2',
100
        'paiement_pf3',
101
        'paiement_pf4',
102
        'Annulation_pf2',
103
        'Annulation_pf3',
104
        'Annulation_pf4',
105
    ];
106
107
    /** @var array */
108
    const CARD_VERIFICATION_STATUSES = [
109
        'oui',
110
        'non',
111
    ];
112
113
    /** @var array */
114
    const CARD_BRANDS = [
115
        'AM' => 'American Express',
116
        'CB' => 'GIE CB',
117
        'MC' => 'Mastercard',
118
        'VI' => 'Visa',
119
        'na' => 'Non disponible',
120
    ];
121
122
    /** @var array  */
123
    const REJECT_REASONS = [
124
        '',
125
        'Appel Phonie',
126
        'Refus',
127
        'Interdit',
128
        'filtrage',
129
        'scoring',
130
        '3DSecure',
131
    ];
132
133
    /** @var array  */
134
    const PAYMENT_METHODS = [
135
        'CB',
136
        'paypal',
137
        '1euro',
138
        '3xcb',
139
        '4cb',
140
        'audiotel',
141
    ];
142
143
    /** @var array */
144
    const FILTERED_REASONS = [
145
        1, 2, 3, 4, 5, 6, 7, 8, 9, 11, 12, 13, 14, 15, 16,
146
    ];
147
148
    /**
149
     * OutputPayload constructor.
150
     *
151
     * @param array $data
152
     * @throws \Exception
153
     */
154
    public function __construct(array $data = [])
155
    {
156
        parent::__construct($data);
157
158
        $this->dateTime = DateTime::createFromFormat(self::DATETIME_FORMAT, $this->dateTime);
0 ignored issues
show
$this->dateTime of type DateTime is incompatible with the type string expected by parameter $time of DateTime::createFromFormat(). ( Ignorable by Annotation )

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

158
        $this->dateTime = DateTime::createFromFormat(self::DATETIME_FORMAT, /** @scrutinizer ignore-type */ $this->dateTime);
Loading history...
Documentation Bug introduced by
It seems like DateTime::createFromForm...ORMAT, $this->dateTime) can also be of type false. However, the property $dateTime is declared as type DateTime. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
159
        if (!$this->dateTime instanceof DateTime) {
160
            throw Exception::invalidResponseDateTime();
161
        }
162
163
        if (!in_array($this->returnCode, self::RETURN_CODES, true)) {
164
            throw PurchaseException::invalidResponseReturnCode($this->returnCode);
165
        }
166
167
        if (!in_array($this->cardVerificationStatus, self::CARD_VERIFICATION_STATUSES, true)) {
168
            throw PurchaseException::invalidResponseCardVerificationStatus($this->cardVerificationStatus);
169
        }
170
171
        if (!array_key_exists($this->cardBrand, self::CARD_BRANDS)) {
172
            throw PurchaseException::invalidResponseCardBrand($this->cardBrand);
173
        }
174
175
        $this->setAuthentication($this->authenticationHash);
176
        $this->setOptions($data);
177
        $this->setErrorsOptions($data);
178
    }
179
180
    public function getRequiredKeys(): array
181
    {
182
        return [
183
            'TPE' => 'eptCode',
184
            'date' => 'dateTime',
185
            'montant' => 'amount',
186
            'reference' => 'reference',
187
            'MAC' => 'seal',
188
            'authentification' => 'authenticationHash',
189
            'texte-libre' => 'description',
190
            'code-retour' => 'returnCode',
191
            'cvx' => 'cardVerificationStatus',
192
            'vld' => 'cardExpirationDate',
193
            'brand' => 'cardBrand',
194
            'originecb' => 'cardCountry',
195
            'bincb' => 'cardBIN',
196
            'hpancb' => 'cardHash',
197
            'ipclient' => 'clientIp',
198
            'originetr' => 'transactionCountry',
199
            'usage' => 'cardType',
200
            'typecompte' => 'accountType',
201
            'ecard' => 'virtualCard',
202
        ];
203
    }
204
205
    /**
206
     * @param string $authentication
207
     * @throws AuthenticationException
208
     */
209
    private function setAuthentication(string $authentication): void
210
    {
211
        $authentication = base64_decode($authentication);
212
        $authentication = json_decode($authentication, true);
213
214
        $this->authentication = new AuthenticationResource(
215
            $authentication['protocol'],
216
            $authentication['status'],
217
            $authentication['version'],
218
            $authentication->details ?? []
219
        );
220
    }
221
222
    /**
223
     * @param array $data
224
     * @throws PurchaseException
225
     */
226
    private function setOptions(array $data): void
227
    {
228
        if (isset($data['numauto'])) {
229
            $this->authNumber = $data['numauto'];
230
        }
231
232
        if (isset($data['modepaiement'])) {
233
            $this->paymentMethod = $data['modepaiement'];
234
            if (!in_array($this->paymentMethod, self::PAYMENT_METHODS, true)) {
235
                throw PurchaseException::invalidResponsePaymentMethod($this->paymentMethod);
236
            }
237
        }
238
239
        // ToDo: Split amount and currency with ISO4217
240
        if (isset($data['montantech'])) {
241
            $this->commitmentAmount = $data['montantech'];
242
        }
243
244
        if (isset($data['cbenregistree'])) {
245
            $this->cardSaved = (bool) $data['cbenregistree'];
246
        }
247
248
        if (isset($data['cbmasquee'])) {
249
            $this->cardMask = $data['cbmasquee'];
250
        }
251
    }
252
253
    /**
254
     * @param array $data
255
     * @throws PurchaseException
256
     */
257
    private function setErrorsOptions(array $data): void
258
    {
259
        if (isset($data['filtragecause'])) {
260
            $this->filteredReason = (int) $data['filtragecause'];
261
            if (!in_array($this->filteredReason, self::FILTERED_REASONS, true)) {
262
                throw PurchaseException::invalidResponseFilteredReason($this->filteredReason);
263
            }
264
        }
265
266
        if (isset($data['motifrefus'])) {
267
            $this->rejectReason = $data['motifrefus'];
268
            if (!in_array($this->rejectReason, self::REJECT_REASONS, true)) {
269
                throw PurchaseException::invalidResponseRejectReason($this->rejectReason);
270
            }
271
        }
272
273
        if (isset($data['filtragevaleur'])) {
274
            $this->filteredValue = $data['filtragevaleur'];
275
        }
276
277
        if (isset($data['filtrage_etat'])) {
278
            $this->filteredStatus = $data['filtrage_etat'];
279
        }
280
    }
281
282
    /**
283
     * @return array
284
     */
285
    public function toArray(): array
286
    {
287
        $fields = [
288
            'authentification' => $this->authenticationHash,
289
            'bincb' => $this->cardBIN,
290
            'brand' => $this->cardBrand,
291
            'code-retour' => $this->returnCode,
292
            'cvx' => $this->cardVerificationStatus,
293
            'date' => $this->dateTime->format(self::DATETIME_FORMAT),
294
            'hpancb' => $this->cardHash,
295
            'ipclient' => $this->clientIp,
296
            'modepaiement' => $this->paymentMethod,
297
            'montant' => $this->amount,
298
            'originecb' => $this->cardCountry,
299
            'originetr' => $this->transactionCountry,
300
            'reference' => $this->reference,
301
            'texte-libre' => $this->description,
302
            'vld' => $this->cardExpirationDate,
303
            'usage' => $this->cardType,
304
            'typecompte' => $this->accountType,
305
            'ecard' => $this->virtualCard,
306
        ];
307
308
        if (isset($this->authNumber)) {
309
            $fields['numauto'] = $this->authNumber;
310
        }
311
312
        if (isset($this->rejectReason)) {
313
            $fields['motifrefus'] = $this->rejectReason;
314
        }
315
316
317
        if (isset($this->commitmentAmount)) {
318
            $fields['montantech'] = $this->commitmentAmount;
319
        }
320
321
        if (isset($this->folderNumber)) {
322
            $fields['numerodossier'] = $this->folderNumber;
323
        }
324
325
        if (isset($this->invoiceType)) {
326
            $fields['typefacture'] = $this->invoiceType;
327
        }
328
329
        if (isset($this->filteredReason)) {
330
            $fields['filtragecause'] = $this->filteredReason;
331
        }
332
333
        if (isset($this->filteredValue)) {
334
            $fields['filtragevaleur'] = $this->filteredValue;
335
        }
336
337
        if (isset($this->filteredStatus)) {
338
            $fields['filtrage_etat'] = $this->filteredStatus;
339
        }
340
341
        if (isset($this->cardSaved)) {
342
            $fields['cbenregistree'] = $this->cardSaved;
343
        }
344
345
        if (isset($this->cardMask)) {
346
            $fields['cbmasquee'] = $this->cardMask;
347
        }
348
349
        if (isset($this->paymentMethod)) {
350
            $fields['modepaiement'] = $this->paymentMethod;
351
        }
352
353
        return $fields;
354
    }
355
356
    /**
357
     * Validate seal to verify payment
358
     *
359
     * @param string $eptCode
360
     * @param string $securityKey
361
     * @return bool
362
     */
363
    public function validateSeal(string $eptCode, string $securityKey): bool
364
    {
365
        $fields = array_merge(
366
            [
367
                'TPE' => $eptCode,
368
            ],
369
            $this->toArray()
370
        );
371
372
        ksort($fields);
373
374
        $query = http_build_query($fields, null, '*');
375
        $query = urldecode($query);
376
377
        $hash = strtoupper(hash_hmac(
378
            'sha1',
379
            $query,
380
            $securityKey
381
        ));
382
383
        return $hash === $this->seal;
384
    }
385
}
386