Completed
Pull Request — master (#95)
by
unknown
09:17
created

VatCalculator   F

Complexity

Total Complexity 67

Size/Duplication

Total Lines 755
Duplicated Lines 0.79 %

Coupling/Cohesion

Components 1
Dependencies 2

Importance

Changes 0
Metric Value
wmc 67
lcom 1
cbo 2
dl 6
loc 755
rs 2.8849
c 0
b 0
f 0

23 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 11 3
A getClientIP() 0 12 5
A getIPBasedCountry() 0 15 2
A shouldCollectVAT() 0 6 3
A calculate() 0 18 5
A calculateNet() 0 19 6
A getNetPrice() 0 4 1
A getCountryCode() 0 4 1
A setCountryCode() 0 4 1
A getPostalCode() 0 4 1
A setPostalCode() 0 4 1
A getTaxRate() 0 4 1
A isCompany() 0 4 1
A setCompany() 0 4 1
A setBusinessCountryCode() 0 4 1
A getTaxRateForCountry() 0 4 1
C getTaxRateForLocation() 0 29 12
A getRules() 0 14 6
A getTaxValue() 0 4 1
A isValidVATNumber() 0 10 2
A getVATDetails() 3 24 5
A initSoapClient() 3 15 6
A setSoapClient() 0 4 1

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complex Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

Complex classes like VatCalculator 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. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

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 VatCalculator, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
namespace Mpociot\VatCalculator;
4
5
use Illuminate\Contracts\Config\Repository;
6
use Mpociot\VatCalculator\Exceptions\VATCheckUnavailableException;
7
use SoapClient;
8
use SoapFault;
9
10
class VatCalculator
11
{
12
    /**
13
     * VAT Service check URL provided by the EU.
14
     */
15
    const VAT_SERVICE_URL = 'http://ec.europa.eu/taxation_customs/vies/checkVatService.wsdl';
16
17
    /**
18
     * We're using the free ip2c service to lookup IP 2 country.
19
     */
20
    const GEOCODE_SERVICE_URL = 'http://ip2c.org/';
21
22
    protected $soapClient;
23
24
    /**
25
     * All available tax rules and their exceptions.
26
     *
27
     * Taken from: http://ec.europa.eu/taxation_customs/resources/documents/taxation/vat/how_vat_works/rates/vat_rates_en.pdf
28
     *
29
     * @var array
30
     */
31
    protected $taxRules = [
32
        'AT' => [ // Austria
33
            'rate'       => 0.20,
34
            'exceptions' => [
35
                'Jungholz'   => 0.19,
36
                'Mittelberg' => 0.19,
37
            ],
38
        ],
39
        'BE' => [ // Belgium
40
            'rate' => 0.21,
41
        ],
42
        'BG' => [ // Bulgaria
43
            'rate' => 0.20,
44
        ],
45
        'CY' => [ // Cyprus
46
            'rate' => 0.19,
47
        ],
48
        'CZ' => [ // Czech Republic
49
            'rate' => 0.21,
50
        ],
51
        'DE' => [ // Germany
52
            'rate'  => 0.19,
53
            'since' => [
54
                '2021-01-01 00:00:00 Europe/Berlin' => [
55
                    'rate' => 0.19,
56
                ],
57
                '2020-07-01 00:00:00 Europe/Berlin' => [
58
                    'rate' => 0.16,
59
                ],
60
            ],
61
            'exceptions' => [
62
                'Heligoland'            => 0,
63
                'Büsingen am Hochrhein' => 0,
64
            ],
65
        ],
66
        'DK' => [ // Denmark
67
            'rate' => 0.25,
68
        ],
69
        'EE' => [ // Estonia
70
            'rate' => 0.20,
71
        ],
72
        'EL' => [ // Hellenic Republic (Greece)
73
            'rate'       => 0.24,
74
            'exceptions' => [
75
                'Mount Athos' => 0,
76
            ],
77
        ],
78
        'ES' => [ // Spain
79
            'rate'       => 0.21,
80
            'exceptions' => [
81
                'Canary Islands' => 0,
82
                'Ceuta'          => 0,
83
                'Melilla'        => 0,
84
            ],
85
        ],
86
        'FI' => [ // Finland
87
            'rate' => 0.24,
88
        ],
89
        'FR' => [ // France
90
            'rate'       => 0.20,
91
            'exceptions' => [
92
                // Overseas France
93
                'Reunion'    => 0.085,
94
                'Martinique' => 0.085,
95
                'Guadeloupe' => 0.085,
96
                'Guyane'     => 0,
97
                'Mayotte'    => 0,
98
            ],
99
        ],
100
        'GB' => [ // United Kingdom
101
            'rate'       => 0.20,
102
            'exceptions' => [
103
                // UK RAF Bases in Cyprus are taxed at Cyprus rate
104
                'Akrotiri' => 0.19,
105
                'Dhekelia' => 0.19,
106
            ],
107
        ],
108
        'GR' => [ // Greece
109
            'rate'       => 0.24,
110
            'exceptions' => [
111
                'Mount Athos' => 0,
112
            ],
113
        ],
114
        'HR' => [ // Croatia
115
            'rate' => 0.25,
116
        ],
117
        'HU' => [ // Hungary
118
            'rate' => 0.27,
119
        ],
120
        'IE' => [ // Ireland
121
            'rate' => 0.23,
122
        ],
123
        'IT' => [ // Italy
124
            'rate'       => 0.22,
125
            'exceptions' => [
126
                'Campione d\'Italia' => 0,
127
                'Livigno'            => 0,
128
            ],
129
        ],
130
        'LT' => [ // Lithuania
131
            'rate' => 0.21,
132
        ],
133
        'LU' => [ // Luxembourg
134
            'rate' => 0.17,
135
        ],
136
        'LV' => [ // Latvia
137
            'rate' => 0.21,
138
        ],
139
        'MT' => [ // Malta
140
            'rate' => 0.18,
141
        ],
142
        'NL' => [ // Netherlands
143
            'rate' => 0.21,
144
            'rates' => [
145
                'high' => 0.21,
146
                'low' => 0.09,
147
            ],
148
        ],
149
        'PL' => [ // Poland
150
            'rate' => 0.23,
151
        ],
152
        'PT' => [ // Portugal
153
            'rate'       => 0.23,
154
            'exceptions' => [
155
                'Azores'  => 0.18,
156
                'Madeira' => 0.22,
157
            ],
158
        ],
159
        'RO' => [ // Romania
160
            'rate' => 0.19,
161
        ],
162
        'SE' => [ // Sweden
163
            'rate' => 0.25,
164
        ],
165
        'SI' => [ // Slovenia
166
            'rate' => 0.22,
167
        ],
168
        'SK' => [ // Slovakia
169
            'rate' => 0.20,
170
        ],
171
172
        // Countries associated with EU countries that have a special VAT rate
173
        'MC' => [ // Monaco France
174
            'rate' => 0.20,
175
        ],
176
        'IM' => [ // Isle of Man - United Kingdom
177
            'rate' => 0.20,
178
        ],
179
180
        // Non-EU with their own VAT requirements
181
        'CH' => [ // Switzerland
182
            'rate' => 0.077,
183
            'rates' => [
184
                'high' => 0.077,
185
                'low' => 0.025,
186
            ],
187
        ],
188
        'TR' => [ // Turkey
189
            'rate' => 0.18,
190
        ],
191
        'NO' => [ // Norway
192
            'rate' => 0.25,
193
        ],
194
    ];
195
196
    /**
197
     * All possible postal code exceptions.
198
     *
199
     * @var array
200
     */
201
    protected $postalCodeExceptions = [
202
        'AT' => [
203
            [
204
                'postalCode' => '/^6691$/',
205
                'code'       => 'AT',
206
                'name'       => 'Jungholz',
207
            ],
208
            [
209
                'postalCode' => '/^699[123]$/',
210
                'city'       => '/\bmittelberg\b/i',
211
                'code'       => 'AT',
212
                'name'       => 'Mittelberg',
213
            ],
214
        ],
215
        'CH' => [
216
            [
217
                'postalCode' => '/^8238$/',
218
                'code'       => 'DE',
219
                'name'       => 'Büsingen am Hochrhein',
220
            ],
221
            [
222
                'postalCode' => '/^6911$/',
223
                'code'       => 'IT',
224
                'name'       => "Campione d'Italia",
225
            ],
226
            // The Italian city of Domodossola has a Swiss post office also
227
            [
228
                'postalCode' => '/^3907$/',
229
                'code'       => 'IT',
230
            ],
231
        ],
232
        'DE' => [
233
            [
234
                'postalCode' => '/^87491$/',
235
                'code'       => 'AT',
236
                'name'       => 'Jungholz',
237
            ],
238
            [
239
                'postalCode' => '/^8756[789]$/',
240
                'city'       => '/\bmittelberg\b/i',
241
                'code'       => 'AT',
242
                'name'       => 'Mittelberg',
243
            ],
244
            [
245
                'postalCode' => '/^78266$/',
246
                'code'       => 'DE',
247
                'name'       => 'Büsingen am Hochrhein',
248
            ],
249
            [
250
                'postalCode' => '/^27498$/',
251
                'code'       => 'DE',
252
                'name'       => 'Heligoland',
253
            ],
254
        ],
255
        'ES' => [
256
            [
257
                'postalCode' => '/^(5100[1-5]|5107[0-1]|51081)$/',
258
                'code'       => 'ES',
259
                'name'       => 'Ceuta',
260
            ],
261
            [
262
                'postalCode' => '/^(5200[0-6]|5207[0-1]|52081)$/',
263
                'code'       => 'ES',
264
                'name'       => 'Melilla',
265
            ],
266
            [
267
                'postalCode' => '/^(35\d{3}|38\d{3})$/',
268
                'code'       => 'ES',
269
                'name'       => 'Canary Islands',
270
            ],
271
        ],
272
        'FR' => [
273
            [
274
                'postalCode' => '/^971\d{2,}$/',
275
                'code'       => 'FR',
276
                'name'       => 'Guadeloupe',
277
            ],
278
            [
279
                'postalCode' => '/^972\d{2,}$/',
280
                'code'       => 'FR',
281
                'name'       => 'Martinique',
282
            ],
283
            [
284
                'postalCode' => '/^973\d{2,}$/',
285
                'code'       => 'FR',
286
                'name'       => 'Guyane',
287
            ],
288
            [
289
                'postalCode' => '/^974\d{2,}$/',
290
                'code'       => 'FR',
291
                'name'       => 'Reunion',
292
            ],
293
            [
294
                'postalCode' => '/^976\d{2,}$/',
295
                'code'       => 'FR',
296
                'name'       => 'Mayotte',
297
            ],
298
        ],
299
        'GB' => [
300
            // Akrotiri
301
            [
302
                'postalCode' => '/^BFPO57|BF12AT$/',
303
                'code'       => 'CY',
304
            ],
305
            // Dhekelia
306
            [
307
                'postalCode' => '/^BFPO58|BF12AU$/',
308
                'code'       => 'CY',
309
            ],
310
        ],
311
        'GR' => [
312
            [
313
                'postalCode' => '/^63086$/',
314
                'code'       => 'GR',
315
                'name'       => 'Mount Athos',
316
            ],
317
        ],
318
        'IT' => [
319
            [
320
                'postalCode' => '/^22060$/',
321
                'city'       => '/\bcampione\b/i',
322
                'code'       => 'IT',
323
                'name'       => "Campione d'Italia",
324
            ],
325
            [
326
                'postalCode' => '/^23030$/',
327
                'city'       => '/\blivigno\b/i',
328
                'code'       => 'IT',
329
                'name'       => 'Livigno',
330
            ],
331
        ],
332
        'PT' => [
333
            [
334
                'postalCode' => '/^9[0-4]\d{2,}$/',
335
                'code'       => 'PT',
336
                'name'       => 'Madeira',
337
            ],
338
            [
339
                'postalCode' => '/^9[5-9]\d{2,}$/',
340
                'code'       => 'PT',
341
                'name'       => 'Azores',
342
            ],
343
        ],
344
    ];
345
346
    /**
347
     * @var float
348
     */
349
    protected $netPrice = 0.0;
350
351
    /**
352
     * @var string
353
     */
354
    protected $countryCode;
355
356
    /**
357
     * @var string
358
     */
359
    protected $postalCode;
360
361
    /**
362
     * @var Repository
363
     */
364
    protected $config;
365
366
    /**
367
     * @var float
368
     */
369
    protected $taxValue = 0;
370
371
    /**
372
     * @var float
373
     */
374
    protected $taxRate = 0;
375
376
    /**
377
     * The calculate net + tax value.
378
     *
379
     * @var float
380
     */
381
    protected $value = 0;
382
383
    /**
384
     * @var bool
385
     */
386
    protected $company = false;
387
388
    /**
389
     * @var string
390
     */
391
    protected $businessCountryCode;
392
393
    /**
394
     * @var \DateTimeImmutable
395
     */
396
    private $now;
397
398
    /**
399
     * @param \Illuminate\Contracts\Config\Repository
400
     */
401
    public function __construct($config = null)
402
    {
403
        $this->config = $config;
404
405
        $businessCountryKey = 'vat_calculator.business_country_code';
406
        if (isset($this->config) && $this->config->has($businessCountryKey)) {
407
            $this->setBusinessCountryCode($this->config->get($businessCountryKey, ''));
408
        }
409
410
        $this->now = new \DateTimeImmutable();
411
    }
412
413
    /**
414
     * Finds the client IP address.
415
     *
416
     * @return mixed
417
     */
418
    private function getClientIP()
419
    {
420
        if (isset($_SERVER['HTTP_X_FORWARDED_FOR']) && $_SERVER['HTTP_X_FORWARDED_FOR']) {
421
            $clientIpAddress = $_SERVER['HTTP_X_FORWARDED_FOR'];
422
        } elseif (isset($_SERVER['REMOTE_ADDR']) && $_SERVER['REMOTE_ADDR']) {
423
            $clientIpAddress = $_SERVER['REMOTE_ADDR'];
424
        } else {
425
            $clientIpAddress = '';
426
        }
427
428
        return $clientIpAddress;
429
    }
430
431
    /**
432
     * Returns the ISO 3166-1 alpha-2 two letter
433
     * country code for the client IP. If the
434
     * IP can't be resolved it returns false.
435
     *
436
     * @return bool|string
437
     */
438
    public function getIPBasedCountry()
439
    {
440
        $ip = $this->getClientIP();
441
        $url = self::GEOCODE_SERVICE_URL.$ip;
442
        $result = file_get_contents($url);
443
        switch ($result[0]) {
444
            case '1':
445
                $data = explode(';', $result);
446
447
                return $data[1];
448
                break;
0 ignored issues
show
Unused Code introduced by
break is not strictly necessary here and could be removed.

The break statement is not necessary if it is preceded for example by a return statement:

switch ($x) {
    case 1:
        return 'foo';
        break; // This break is not necessary and can be left off.
}

If you would like to keep this construct to be consistent with other case statements, you can safely mark this issue as a false-positive.

Loading history...
449
            default:
450
                return false;
451
        }
452
    }
453
454
    /**
455
     * Determines if you need to collect VAT for the given country code.
456
     *
457
     * @param $countryCode
458
     *
459
     * @return bool
460
     */
461
    public function shouldCollectVAT($countryCode)
462
    {
463
        $taxKey = 'vat_calculator.rules.'.strtoupper($countryCode);
464
465
        return isset($this->taxRules[strtoupper($countryCode)]) || (isset($this->config) && $this->config->has($taxKey));
466
    }
467
468
    /**
469
     * Calculate the VAT based on the net price, country code and indication if the
470
     * customer is a company or not.
471
     *
472
     * @param int|float   $netPrice    The net price to use for the calculation
473
     * @param null|string $countryCode The country code to use for the rate lookup
474
     * @param null|string $postalCode  The postal code to use for the rate exception lookup
475
     * @param null|bool   $company
476
     * @param null|string $type        The type can be low or high
477
     * @param \DateTimeInterface|null $date Date to use the VAT rate for, null for current date
478
     *
479
     * @return float
480
     */
481
    public function calculate($netPrice, $countryCode = null, $postalCode = null, $company = null, $type = null, $date = null)
482
    {
483
        if ($countryCode) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $countryCode of type null|string is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
484
            $this->setCountryCode($countryCode);
485
        }
486
        if ($postalCode) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $postalCode of type null|string is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
487
            $this->setPostalCode($postalCode);
488
        }
489
        if (!is_null($company) && $company !== $this->isCompany()) {
490
            $this->setCompany($company);
491
        }
492
        $this->netPrice = floatval($netPrice);
493
        $this->taxRate = $this->getTaxRateForLocation($this->getCountryCode(), $this->getPostalCode(), $this->isCompany(), $type, $date);
494
        $this->taxValue = $this->taxRate * $this->netPrice;
495
        $this->value = $this->netPrice + $this->taxValue;
496
497
        return $this->value;
498
    }
499
500
    /**
501
     * Calculate the net price on the gross price, country code and indication if the
502
     * customer is a company or not.
503
     *
504
     * @param int|float   $gross       The gross price to use for the calculation
505
     * @param null|string $countryCode The country code to use for the rate lookup
506
     * @param null|string $postalCode  The postal code to use for the rate exception lookup
507
     * @param null|bool   $company
508
     * @param null|string $type        The type can be low or high
509
     * @param \DateTimeInterface|null $date Date to use the VAT rate for, null for current date
510
     *
511
     * @return float
512
     */
513
    public function calculateNet($gross, $countryCode = null, $postalCode = null, $company = null, $type = null, $date = null)
514
    {
515
        if ($countryCode) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $countryCode of type null|string is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
516
            $this->setCountryCode($countryCode);
517
        }
518
        if ($postalCode) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $postalCode of type null|string is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
519
            $this->setPostalCode($postalCode);
520
        }
521
        if (!is_null($company) && $company !== $this->isCompany()) {
522
            $this->setCompany($company);
523
        }
524
525
        $this->value = floatval($gross);
526
        $this->taxRate = $this->getTaxRateForLocation($this->getCountryCode(), $this->getPostalCode(), $this->isCompany(), $type, $date);
527
        $this->taxValue = $this->taxRate > 0 ? $this->value / (1 + $this->taxRate) * $this->taxRate : 0;
0 ignored issues
show
Documentation Bug introduced by
It seems like $this->taxRate > 0 ? $th...e) * $this->taxRate : 0 can also be of type integer. However, the property $taxValue is declared as type double. 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...
528
        $this->netPrice = $this->value - $this->taxValue;
529
530
        return $this->netPrice;
531
    }
532
533
    /**
534
     * @return float
535
     */
536
    public function getNetPrice()
537
    {
538
        return $this->netPrice;
539
    }
540
541
    /**
542
     * @return string
543
     */
544
    public function getCountryCode()
545
    {
546
        return strtoupper($this->countryCode);
547
    }
548
549
    /**
550
     * @param mixed $countryCode
551
     */
552
    public function setCountryCode($countryCode)
553
    {
554
        $this->countryCode = $countryCode;
555
    }
556
557
    /**
558
     * @return string
559
     */
560
    public function getPostalCode()
561
    {
562
        return $this->postalCode;
563
    }
564
565
    /**
566
     * @param mixed $postalCode
567
     */
568
    public function setPostalCode($postalCode)
569
    {
570
        $this->postalCode = $postalCode;
571
    }
572
573
    /**
574
     * @return float
575
     */
576
    public function getTaxRate()
577
    {
578
        return $this->taxRate;
579
    }
580
581
    /**
582
     * @return bool
583
     */
584
    public function isCompany()
585
    {
586
        return $this->company;
587
    }
588
589
    /**
590
     * @param bool $company
591
     */
592
    public function setCompany($company)
593
    {
594
        $this->company = $company;
595
    }
596
597
    /**
598
     * @param string $businessCountryCode
599
     */
600
    public function setBusinessCountryCode($businessCountryCode)
601
    {
602
        $this->businessCountryCode = $businessCountryCode;
603
    }
604
605
    /**
606
     * Returns the tax rate for the given country code.
607
     * This method is used to allow backwards compatibility.
608
     *
609
     * @param $countryCode
610
     * @param bool $company
611
     * @param string $type
612
     *
613
     * @return float
614
     */
615
    public function getTaxRateForCountry($countryCode, $company = false, $type = null)
616
    {
617
        return $this->getTaxRateForLocation($countryCode, null, $company, $type);
618
    }
619
620
    /**
621
     * Returns the tax rate for the given country code.
622
     * If a postal code is provided, it will try to lookup the different
623
     * postal code exceptions that are possible.
624
     *
625
     * @param string      $countryCode
626
     * @param string|null $postalCode
627
     * @param bool|false  $company
628
     * @param string|null $type
629
     * @param \DateTimeInterface|null $date Date to use the VAT rate for, null for current date
630
     *
631
     * @return float
632
     */
633
    public function getTaxRateForLocation($countryCode, $postalCode = null, $company = false, $type = null, $date = null)
634
    {
635
        if ($company && strtoupper($countryCode) !== strtoupper($this->businessCountryCode)) {
636
            return 0;
637
        }
638
        $taxKey = 'vat_calculator.rules.'.strtoupper($countryCode);
639
        if (isset($this->config) && $this->config->has($taxKey)) {
640
            return $this->config->get($taxKey, 0);
641
        }
642
643
        if (isset($this->postalCodeExceptions[$countryCode]) && $postalCode !== null) {
644
            foreach ($this->postalCodeExceptions[$countryCode] as $postalCodeException) {
645
                if (!preg_match($postalCodeException['postalCode'], $postalCode)) {
646
                    continue;
647
                }
648
                if (isset($postalCodeException['name'])) {
649
                    return $this->taxRules[$postalCodeException['code']]['exceptions'][$postalCodeException['name']];
650
                }
651
652
                return $this->getRules($postalCodeException['code'], $date)['rate'];
653
            }
654
        }
655
656
        if ($type !== null) {
657
            return isset($this->taxRules[strtoupper($countryCode)]['rates'][$type]) ? $this->taxRules[strtoupper($countryCode)]['rates'][$type] : 0;
658
        }
659
660
        return $this->getRules(strtoupper($countryCode), $date)['rate'];
661
    }
662
663
    private function getRules($countryCode, $date): array
664
    {
665
        if (!isset($this->taxRules[$countryCode])) {
666
            return ['rate' => 0];
667
        }
668
        if (isset($this->taxRules[$countryCode]['since'])) {
669
            foreach ($this->taxRules[$countryCode]['since'] as $since => $rates) {
670
                if (new \DateTimeImmutable($since) <= ($date !== null ? $date : $this->now)) {
671
                    return $rates;
672
                }
673
            }
674
        }
675
        return $this->taxRules[$countryCode];
676
    }
677
678
    /**
679
     * @return float
680
     */
681
    public function getTaxValue()
682
    {
683
        return $this->taxValue;
684
    }
685
686
    /**
687
     * @param $vatNumber
688
     *
689
     * @throws VATCheckUnavailableException
690
     *
691
     * @return bool
692
     */
693
    public function isValidVATNumber($vatNumber)
694
    {
695
        $details = self::getVATDetails($vatNumber);
696
697
        if ($details) {
698
            return $details->valid;
699
        } else {
700
            return false;
701
        }
702
    }
703
704
    /**
705
     * @param $vatNumber
706
     *
707
     * @throws VATCheckUnavailableException
708
     *
709
     * @return object|false
710
     */
711
    public function getVATDetails($vatNumber)
712
    {
713
        $vatNumber = str_replace([' ', "\xC2\xA0", "\xA0", '-', '.', ','], '', trim($vatNumber));
714
        $countryCode = substr($vatNumber, 0, 2);
715
        $vatNumber = substr($vatNumber, 2);
716
        $this->initSoapClient();
717
        $client = $this->soapClient;
718
        if ($client) {
719
            try {
720
                $result = $client->checkVat([
721
                    'countryCode' => $countryCode,
722
                    'vatNumber' => $vatNumber,
723
                ]);
724
                return $result;
725
            } catch (SoapFault $e) {
726 View Code Duplication
                if (isset($this->config) && $this->config->get('vat_calculator.forward_soap_faults')) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
727
                    throw new VATCheckUnavailableException($e->getMessage(), $e->getCode(), $e->getPrevious());
728
                }
729
730
                return false;
731
            }
732
        }
733
        throw new VATCheckUnavailableException('The VAT check service is currently unavailable. Please try again later.');
734
    }
735
736
    /**
737
     * @throws VATCheckUnavailableException
738
     *
739
     * @return void
740
     */
741
    public function initSoapClient()
742
    {
743
        if (is_object($this->soapClient) || $this->soapClient === false) {
744
            return;
745
        }
746
        try {
747
            $this->soapClient = new SoapClient(self::VAT_SERVICE_URL);
748
        } catch (SoapFault $e) {
749 View Code Duplication
            if (isset($this->config) && $this->config->get('vat_calculator.forward_soap_faults')) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
750
                throw new VATCheckUnavailableException($e->getMessage(), $e->getCode(), $e->getPrevious());
751
            }
752
753
            $this->soapClient = false;
754
        }
755
    }
756
757
    /**
758
     * @param SoapClient $soapClient
759
     */
760
    public function setSoapClient($soapClient)
761
    {
762
        $this->soapClient = $soapClient;
763
    }
764
}
765