Skip to content

Commit 6718bd4

Browse files
joshuacadman01joshcadman-easyreplydriesvints
authored
Add uk vat checks (#193)
* Adding support for UK Vat * Updating tests to check UK VAT * Updating Readme * Fixing tests * Fix PHP 7.3 compatibility by removing typed property * wip --------- Co-authored-by: Josh Cadman <[email protected]> Co-authored-by: Dries Vints <[email protected]>
1 parent 49f9523 commit 6718bd4

File tree

5 files changed

+390
-19
lines changed

5 files changed

+390
-19
lines changed

README.md

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -154,11 +154,11 @@ try {
154154
}
155155
```
156156

157-
#### ~UK VAT Numbers~
157+
#### UK VAT Numbers
158158

159-
> Support for validating UK VAT numbers has been removed. [See the related PR.](https://github.com/driesvints/vat-calculator/pull/191)
159+
> Note: Validating UK VAT numbers requires registering your application with the HMRC Developer Hub. Please follow the official [HMRC API Documentation](https://developer.service.hmrc.gov.uk/api-documentation/docs/api/service/vat-registered-companies-api/2.0) for details on authentication and setup.
160160
161-
~UK VAT numbers are formatted a little differently:~
161+
UK VAT numbers are formatted a little differently:
162162

163163
```php
164164
try {
@@ -186,6 +186,15 @@ try {
186186
}
187187
```
188188

189+
> 🔐 Configuration
190+
191+
To use the UK VAT validation feature, you'll need to register your application with HMRC and set the following environment variables in your .env file:
192+
193+
```
194+
HMRC_CLIENT_ID="your-client-id"
195+
HMRC_CLIENT_SECRET="your-client-secret"
196+
```
197+
189198
## Laravel
190199

191200
### Configuration

config/vat_calculator.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,4 +70,9 @@
7070

7171
'soap_timeout' => 30,
7272

73+
'hmrc' => [
74+
'client_id' => env('HMRC_CLIENT_ID'),
75+
'client_secret' => env('HMRC_CLIENT_SECRET'),
76+
],
77+
7378
];

src/Http/CurlClient.php

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
<?php
2+
3+
namespace Mpociot\VatCalculator\Http;
4+
5+
class CurlClient
6+
{
7+
/**
8+
* Send a GET request.
9+
*
10+
* @param string $url
11+
* @param array $headers
12+
* @return string
13+
* @throws \RuntimeException on cURL error
14+
*/
15+
public function get(string $url, array $headers = []): string
16+
{
17+
$ch = curl_init($url);
18+
19+
curl_setopt_array($ch, [
20+
CURLOPT_RETURNTRANSFER => true,
21+
CURLOPT_HTTPHEADER => $headers,
22+
]);
23+
24+
$response = curl_exec($ch);
25+
26+
if ($response === false) {
27+
throw new \RuntimeException('cURL GET error: ' . curl_error($ch));
28+
}
29+
30+
curl_close($ch);
31+
32+
return $response;
33+
}
34+
35+
/**
36+
* Send a POST request with JSON body.
37+
*
38+
* @param string $url
39+
* @param array $headers
40+
* @param array|string $data
41+
* @param bool $json
42+
* @return string
43+
* @throws \RuntimeException on cURL error
44+
*/
45+
public function post(string $url, array $headers = [], $data = [], bool $json = true): string
46+
{
47+
$ch = curl_init($url);
48+
49+
$postFields = $json ? json_encode($data) : (is_array($data) ? http_build_query($data) : $data);
50+
51+
if ($json) {
52+
$headers = array_merge(['Content-Type: application/json'], $headers);
53+
}
54+
55+
curl_setopt_array($ch, [
56+
CURLOPT_RETURNTRANSFER => true,
57+
CURLOPT_HTTPHEADER => $headers,
58+
CURLOPT_POST => true,
59+
CURLOPT_POSTFIELDS => $postFields,
60+
]);
61+
62+
$response = curl_exec($ch);
63+
64+
if ($response === false) {
65+
throw new \RuntimeException('cURL POST error: ' . curl_error($ch));
66+
}
67+
68+
curl_close($ch);
69+
70+
return $response;
71+
}
72+
73+
/**
74+
* Send a GET request and return response with HTTP status code and headers.
75+
*
76+
* @param string $url The URL to send the GET request to
77+
* @param array $headers Optional array of HTTP headers to include in the request
78+
* @return array Associative array containing statusCode, headers, and body
79+
* @throws \RuntimeException on cURL error
80+
*/
81+
public function getWithStatus(string $url, array $headers = []): array
82+
{
83+
$ch = curl_init($url);
84+
85+
curl_setopt_array($ch, [
86+
CURLOPT_RETURNTRANSFER => true,
87+
CURLOPT_HTTPHEADER => $headers,
88+
CURLOPT_TIMEOUT => 30,
89+
CURLOPT_HEADER => true, // We want headers in output
90+
]);
91+
92+
$response = curl_exec($ch);
93+
94+
if ($response === false) {
95+
throw new \RuntimeException('cURL GET error: ' . curl_error($ch));
96+
}
97+
98+
$headerSize = curl_getinfo($ch, CURLINFO_HEADER_SIZE);
99+
$statusCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
100+
101+
curl_close($ch);
102+
103+
$header = substr($response, 0, $headerSize);
104+
$body = substr($response, $headerSize);
105+
106+
return [
107+
'statusCode' => $statusCode,
108+
'headers' => $header,
109+
'body' => $body,
110+
];
111+
}
112+
}

src/VatCalculator.php

Lines changed: 108 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@
22

33
namespace Mpociot\VatCalculator;
44

5+
use Exception;
56
use Illuminate\Contracts\Config\Repository;
7+
use Mpociot\VatCalculator\Http\CurlClient;
68
use Mpociot\VatCalculator\Exceptions\VATCheckUnavailableException;
79
use SoapClient;
810
use SoapFault;
@@ -19,6 +21,11 @@ class VatCalculator
1921
*/
2022
protected $soapClient;
2123

24+
/**
25+
* @var CurlClient
26+
*/
27+
protected $curlClient;
28+
2229
/**
2330
* All available tax rules and their exceptions.
2431
*
@@ -594,11 +601,23 @@ class VatCalculator
594601
*/
595602
protected $businessCountryCode = '';
596603

604+
/**
605+
* @var string
606+
*/
607+
protected $ukHmrcTokenEndpoint = 'https://api.service.hmrc.gov.uk/oauth/token';
608+
609+
/**
610+
* @var string
611+
*/
612+
protected $ukValidationEndpoint = 'https://api.service.hmrc.gov.uk';
613+
597614
/**
598615
* @param \Illuminate\Contracts\Config\Repository|array
599616
*/
600617
public function __construct($config = [])
601618
{
619+
$this->curlClient = new CurlClient();
620+
602621
$this->config = $config instanceof Repository ? $config->get('vat_calculator', []) : $config;
603622

604623
if (isset($this->config['business_country_code'])) {
@@ -883,6 +902,39 @@ public function isValidVATNumber($vatNumber)
883902
return false;
884903
}
885904

905+
/**
906+
* Get or refresh HMRC access token
907+
*/
908+
private function getHmrcAccessToken()
909+
{
910+
// Get token from HMRC
911+
$clientId = $this->config['hmrc']['client_id'];
912+
$clientSecret = $this->config['hmrc']['client_secret'];
913+
914+
if (! $clientId || ! $clientSecret) {
915+
throw new VATCheckUnavailableException("HMRC API credentials not configured");
916+
}
917+
918+
// Note: This endpoint requires x-www-form-urlencoded, so override the content-type.
919+
$headers = [
920+
'Content-Type: application/x-www-form-urlencoded',
921+
];
922+
923+
$response = $this->curlClient->post($this->ukHmrcTokenEndpoint, $headers, http_build_query([
924+
'grant_type' => 'client_credentials',
925+
'client_id' => $clientId,
926+
'client_secret' => $clientSecret,
927+
]), false);
928+
929+
$data = json_decode($response, true);
930+
931+
if (! isset($data['access_token'])) {
932+
throw new VATCheckUnavailableException("Failed to retrieve HMRC access token");
933+
}
934+
935+
return $data['access_token'];
936+
}
937+
886938
/**
887939
* @param string $vatNumber
888940
* @return object|false
@@ -896,7 +948,39 @@ public function getVATDetails($vatNumber)
896948
$vatNumber = substr($vatNumber, 2);
897949

898950
if (strtoupper($countryCode) === 'GB') {
899-
throw new VATCheckUnavailableException('UK VAT checks are no longer available. Please see https://github.com/driesvints/vat-calculator/pull/191.');
951+
try {
952+
$accessToken = $this->getHmrcAccessToken();
953+
954+
$responseData = $this->curlClient->getWithStatus(
955+
"$this->ukValidationEndpoint/organisations/vat/check-vat-number/lookup/$vatNumber",
956+
[
957+
"Authorization: Bearer $accessToken",
958+
"Accept: application/vnd.hmrc.2.0+json",
959+
]
960+
);
961+
962+
$apiStatusCode = $responseData['statusCode'];
963+
964+
if ($apiStatusCode === 400 || $apiStatusCode === 404) {
965+
return false;
966+
}
967+
968+
if ($apiStatusCode === 200) {
969+
$apiResponse = json_decode($responseData['body'], true);
970+
971+
if (json_last_error() !== JSON_ERROR_NONE) {
972+
throw new VATCheckUnavailableException("Invalid JSON response from UK VAT check service");
973+
}
974+
975+
return $apiResponse['target'] ?? false;
976+
}
977+
978+
throw new VATCheckUnavailableException("The UK VAT check service is currently unavailable (status code $apiStatusCode). Please try again later.");
979+
} catch (VATCheckUnavailableException $e) {
980+
throw $e;
981+
} catch (Exception $e) {
982+
throw new VATCheckUnavailableException("An unexpected error occurred while validating the VAT number");
983+
}
900984
} else {
901985
$this->initSoapClient();
902986
$client = $this->soapClient;
@@ -958,4 +1042,27 @@ public function setSoapClient($soapClient)
9581042
{
9591043
$this->soapClient = $soapClient;
9601044
}
1045+
1046+
public function setupCurlClient($curlClient)
1047+
{
1048+
$this->curlClient = $curlClient;
1049+
}
1050+
1051+
/**
1052+
* @return $this
1053+
*
1054+
* @internal This method is not covered by our BC policy.
1055+
*/
1056+
public function testing($curlClient)
1057+
{
1058+
$this->ukHmrcTokenEndpoint = 'https://test-api.service.hmrc.gov.uk/oauth/token';
1059+
$this->ukValidationEndpoint = 'https://test-api.service.hmrc.gov.uk';
1060+
1061+
$this->config['hmrc']['client_id'] = 'test-client-id';
1062+
$this->config['hmrc']['client_secret'] = 'test-client-secret';
1063+
1064+
$this->setupCurlClient($curlClient);
1065+
1066+
return $this;
1067+
}
9611068
}

0 commit comments

Comments
 (0)