Completed
Push — master ( 42a773...a9c19a )
by Marcel
18:32 queued 08:31
created

VatCalculator::getTaxRateForCountry()   B

Complexity

Conditions 6
Paths 4

Size

Total Lines 12
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

Changes 5
Bugs 1 Features 1
Metric Value
cc 6
eloc 7
c 5
b 1
f 1
nc 4
nop 2
dl 0
loc 12
rs 8.8571
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.
26
     *
27
     * @var array
28
     */
29
    protected $taxRules = [
30
        'AT' => 0.20,
31
        'BE' => 0.21,
32
        'BG' => 0.20,
33
        'CY' => 0.19,
34
        'CZ' => 0.21,
35
        'DE' => 0.19,
36
        'DK' => 0.25,
37
        'EE' => 0.20,
38
        'EL' => 0.23,
39
        'ES' => 0.21,
40
        'FI' => 0.24,
41
        'FR' => 0.20,
42
        'GB' => 0.20,
43
        'GR' => 0.23,
44
        'IE' => 0.23,
45
        'IT' => 0.22,
46
        'HR' => 0.25,
47
        'HU' => 0.27,
48
        'LV' => 0.21,
49
        'LT' => 0.21,
50
        'LU' => 0.17,
51
        'MT' => 0.18,
52
        'NL' => 0.21,
53
        'NO' => 0.25,
54
        'PL' => 0.23,
55
        'PT' => 0.23,
56
        'RO' => 0.20,
57
        'SE' => 0.25,
58
        'SK' => 0.20,
59
        'SI' => 0.22,
60
    ];
61
62
    /**
63
     * @var float
64
     */
65
    protected $netPrice = 0.0;
66
67
    /**
68
     * @var string
69
     */
70
    protected $countryCode;
71
72
    /**
73
     * @var Repository
74
     */
75
    protected $config;
76
77
    /**
78
     * @var float
79
     */
80
    protected $taxValue = 0;
81
82
    /**
83
     * @var float
84
     */
85
    protected $taxRate = 0;
86
87
    /**
88
     * The calculate net + tax value.
89
     *
90
     * @var float
91
     */
92
    protected $value = 0;
93
94
    /**
95
     * @var bool
96
     */
97
    protected $company = false;
98
99
    /**
100
     * @var string
101
     */
102
    protected $businessCountryCode;
103
104
    /**
105
     * @param \Illuminate\Contracts\Config\Repository
106
     */
107
    public function __construct($config = null)
108
    {
109
        $this->config = $config;
110
111
        $businessCountryKey = 'vat_calculator.business_country_code';
112
        if (isset($this->config) && $this->config->has($businessCountryKey)) {
113
            $this->setBusinessCountryCode($this->config->get($businessCountryKey, ''));
114
        }
115
116
        try {
117
            $this->soapClient = new SoapClient(self::VAT_SERVICE_URL);
118
        } catch (SoapFault $e) {
119
            $this->soapClient = false;
120
        }
121
    }
122
123
    /**
124
     * Finds the client IP address.
125
     *
126
     * @return mixed
127
     */
128
    private function getClientIP()
0 ignored issues
show
Coding Style introduced by
getClientIP uses the super-global variable $_SERVER which is generally not recommended.

Instead of super-globals, we recommend to explicitly inject the dependencies of your class. This makes your code less dependent on global state and it becomes generally more testable:

// Bad
class Router
{
    public function generate($path)
    {
        return $_SERVER['HOST'].$path;
    }
}

// Better
class Router
{
    private $host;

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

    public function generate($path)
    {
        return $this->host.$path;
    }
}

class Controller
{
    public function myAction(Request $request)
    {
        // Instead of
        $page = isset($_GET['page']) ? intval($_GET['page']) : 1;

        // Better (assuming you use the Symfony2 request)
        $page = $request->query->get('page', 1);
    }
}
Loading history...
129
    {
130
        if (isset($_SERVER['HTTP_X_FORWARDED_FOR']) && $_SERVER['HTTP_X_FORWARDED_FOR']) {
131
            $clientIpAddress = $_SERVER['HTTP_X_FORWARDED_FOR'];
132
        } elseif (isset($_SERVER['REMOTE_ADDR']) && $_SERVER['REMOTE_ADDR']) {
133
            $clientIpAddress = $_SERVER['REMOTE_ADDR'];
134
        } else {
135
            $clientIpAddress = '';
136
        }
137
138
        return $clientIpAddress;
139
    }
140
141
    /**
142
     * Returns the ISO 3166-1 alpha-2 two letter
143
     * country code for the client IP. If the
144
     * IP can't be resolved it returns false.
145
     *
146
     * @return bool|string
147
     */
148
    public function getIPBasedCountry()
149
    {
150
        $ip = $this->getClientIP();
151
        $url = self::GEOCODE_SERVICE_URL.$ip;
152
        $result = file_get_contents($url);
153
        switch ($result[0]) {
154
            case '1':
155
                $data = explode(';', $result);
156
157
                return $data[1];
158
                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...
159
            default:
160
                return false;
161
        }
162
    }
163
164
    /**
165
     * Determines if you need to collect VAT for the given country code.
166
     *
167
     * @param $countryCode
168
     *
169
     * @return bool
170
     */
171
    public function shouldCollectVAT($countryCode)
172
    {
173
        $taxKey = 'vat_calculator.rules.'.strtoupper($countryCode);
174
175
        return isset($this->taxRules[strtoupper($countryCode)]) || (isset($this->config) && $this->config->has($taxKey));
176
    }
177
178
    /**
179
     * Calculate the VAT based on the net price, country code and indication if the
180
     * customer is a company or not.
181
     *
182
     * @param int|float   $netPrice    The net price to use for the calculation
183
     * @param null|string $countryCode The country code to use for the rate lookup
184
     * @param null|bool   $company
185
     *
186
     * @return float
187
     */
188
    public function calculate($netPrice, $countryCode = null, $company = null)
189
    {
190
        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...
191
            $this->setCountryCode($countryCode);
192
        }
193
        if (!is_null($company) && $company !== $this->isCompany()) {
194
            $this->setCompany($company);
195
        }
196
        $this->netPrice = floatval($netPrice);
197
        $this->taxRate = $this->getTaxRateForCountry($this->getCountryCode(), $this->isCompany());
198
        $this->taxValue = $this->taxRate * $this->netPrice;
199
        $this->value = $this->netPrice + $this->taxValue;
200
201
        return $this->value;
202
    }
203
204
    /**
205
     * @return float
206
     */
207
    public function getNetPrice()
208
    {
209
        return $this->netPrice;
210
    }
211
212
    /**
213
     * @return string
214
     */
215
    public function getCountryCode()
216
    {
217
        return strtoupper($this->countryCode);
218
    }
219
220
    /**
221
     * @param mixed $countryCode
222
     */
223
    public function setCountryCode($countryCode)
224
    {
225
        $this->countryCode = $countryCode;
226
    }
227
228
    /**
229
     * @return float
230
     */
231
    public function getTaxRate()
232
    {
233
        return $this->taxRate;
234
    }
235
236
    /**
237
     * @return bool
238
     */
239
    public function isCompany()
240
    {
241
        return $this->company;
242
    }
243
244
    /**
245
     * @param bool $company
246
     */
247
    public function setCompany($company)
248
    {
249
        $this->company = $company;
250
    }
251
252
    /**
253
     * @param string $businessCountryCode
254
     */
255
    public function setBusinessCountryCode($businessCountryCode)
256
    {
257
        $this->businessCountryCode = $businessCountryCode;
258
    }
259
260
    /**
261
     * Returns the tax rate for the given country.
262
     *
263
     * @param string     $countryCode
264
     * @param bool|false $company
265
     *
266
     * @return float
267
     */
268
    public function getTaxRateForCountry($countryCode, $company = false)
269
    {
270
        if ($company && strtoupper($countryCode) !== strtoupper($this->businessCountryCode)) {
271
            return 0;
272
        }
273
        $taxKey = 'vat_calculator.rules.'.strtoupper($countryCode);
274
        if (isset($this->config) && $this->config->has($taxKey)) {
275
            return $this->config->get($taxKey, 0);
276
        }
277
278
        return isset($this->taxRules[strtoupper($countryCode)]) ? $this->taxRules[strtoupper($countryCode)] : 0;
279
    }
280
281
    /**
282
     * @return float
283
     */
284
    public function getTaxValue()
285
    {
286
        return $this->taxValue;
287
    }
288
289
    /**
290
     * @param $vatNumber
291
     *
292
     * @throws VATCheckUnavailableException
293
     *
294
     * @return bool
295
     */
296
    public function isValidVATNumber($vatNumber)
297
    {
298
        $vatNumber = str_replace([' ', '-', '.', ','], '', trim($vatNumber));
299
        $countryCode = substr($vatNumber, 0, 2);
300
        $vatNumber = substr($vatNumber, 2);
301
302
        $client = $this->soapClient;
303
        if ($client) {
304
            try {
305
                $result = $client->checkVat([
306
                    'countryCode' => $countryCode,
307
                    'vatNumber'   => $vatNumber,
308
                ]);
309
310
                return $result->valid;
311
            } catch (SoapFault $e) {
312
                return false;
313
            }
314
        }
315
        throw new VATCheckUnavailableException('The VAT check service is currently unavailable. Please try again later.');
316
    }
317
318
    /**
319
     * @param SoapClient $soapClient
320
     */
321
    public function setSoapClient($soapClient)
322
    {
323
        $this->soapClient = $soapClient;
324
    }
325
}
326