Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
80 / 80
100.00% covered (success)
100.00%
18 / 18
CRAP
100.00% covered (success)
100.00%
1 / 1
Google
100.00% covered (success)
100.00%
80 / 80
100.00% covered (success)
100.00%
18 / 18
40
100.00% covered (success)
100.00%
1 / 1
 __construct
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
1
 getApiUrl
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setApiKey
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 getApiKey
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 hasApiKey
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setClient
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 getClient
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 hasClient
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setOriginalAddress
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
2
 getOriginalAddress
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 hasOriginalAddress
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setSuggestedAddress
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
2
 getSuggestedAddress
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 hasSuggestedAddress
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getResponse
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 isConfirmed
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 validate
100.00% covered (success)
100.00%
41 / 41
100.00% covered (success)
100.00%
1 / 1
19
 parseAddress
100.00% covered (success)
100.00%
16 / 16
100.00% covered (success)
100.00%
1 / 1
3
1<?php
2declare(strict_types=1);
3/**
4 * Pop PHP Framework (https://www.popphp.org/)
5 *
6 * @link       https://github.com/popphp/popphp-framework
7 * @author     Nick Sagona, III <dev@noladev.com>
8 * @copyright  Copyright (c) 2009-2027 NOLA Interactive, LLC.
9 * @license    https://www.popphp.org/license     New BSD License
10 */
11
12/**
13 * @namespace
14 */
15namespace Pop\Shipping\Adapter;
16
17use Pop\Http\Client;
18use Pop\Parser\Address\AddressParser;
19use Pop\Shipping\Address;
20
21/**
22 * Pop shipping Google adapter class
23 *
24 * @category   Pop
25 * @package    Pop\Shipping
26 * @author     Nick Sagona, III <dev@noladev.com>
27 * @copyright  Copyright (c) 2009-2027 NOLA Interactive, LLC.
28 * @license    https://www.popphp.org/license     New BSD License
29 * @version    4.0.0
30 */
31class Google
32{
33
34    /**
35     * Google Maps API address validation URL
36     * @var string
37     */
38    protected string $apiUrl = 'https://addressvalidation.googleapis.com/v1:validateAddress?key=';
39
40    /**
41     * Google Maps API Key
42     * @var ?string
43     */
44    protected ?string $apiKey = null;
45
46    /**
47     * HTTP Client
48     * @var ?Client
49     */
50    protected ?Client $client = null;
51
52    /**
53     * Original address to validate
54     * @var ?Address
55     */
56    protected ?Address $originalAddress = null;
57
58    /**
59     * Suggested address returned from the validation API request
60     * @var ?Address
61     */
62    protected ?Address $suggestedAddress = null;
63
64    /**
65     * Validation response
66     * @var array
67     */
68    protected array $response = [];
69
70    /**
71     * Confirmed
72     * @var bool
73     */
74    protected bool $confirmed = false;
75
76    /**
77     * Constructor
78     *
79     * Instantiate the shipping object
80     *
81     */
82    public function __construct(string $apiKey)
83    {
84        $this->setApiKey($apiKey);
85
86        $client = new Client($this->apiUrl . $this->apiKey);
87        $client->setType(Client\Request::JSON);
88
89        $this->setClient($client);
90    }
91
92    /**
93     * Get API URL
94     *
95     * @return string
96     */
97    public function getApiUrl(): string
98    {
99        return $this->apiUrl;
100    }
101
102    /**
103     * Set API key
104     *
105     * @param  string $apiKey
106     * @return static
107     */
108    public function setApiKey(string $apiKey): static
109    {
110        $this->apiKey = $apiKey;
111        return $this;
112    }
113
114    /**
115     * Get API key
116     *
117     * @return ?string
118     */
119    public function getApiKey(): ?string
120    {
121        return $this->apiKey;
122    }
123
124    /**
125     * Has API key
126     *
127     * @return bool
128     */
129    public function hasApiKey(): bool
130    {
131        return (!empty($this->apiKey));
132    }
133
134    /**
135     * Set client
136     *
137     * @param  Client $client
138     * @return static
139     */
140    public function setClient(Client $client): static
141    {
142        $this->client = $client;
143        return $this;
144    }
145
146    /**
147     * Get client
148     *
149     * @return ?Client
150     */
151    public function getClient(): ?Client
152    {
153        return $this->client;
154    }
155
156    /**
157     * Has client
158     *
159     * @return bool
160     */
161    public function hasClient(): bool
162    {
163        return (!empty($this->client));
164    }
165
166    /**
167     * Set original address
168     *
169     * @param  array|Address $address
170     * @return static
171     */
172    public function setOriginalAddress(array|Address $address): static
173    {
174        $this->originalAddress = !($address instanceof Address) ? new Address($address) : $address;
175        return $this;
176    }
177
178    /**
179     * Get original address
180     *
181     * @return ?Address
182     */
183    public function getOriginalAddress(): ?Address
184    {
185        return $this->originalAddress;
186    }
187
188    /**
189     * Has original address
190     *
191     * @return bool
192     */
193    public function hasOriginalAddress(): bool
194    {
195        return (!empty($this->originalAddress));
196    }
197
198    /**
199     * Set suggested address
200     *
201     * @param  array|Address $address
202     * @return static
203     */
204    public function setSuggestedAddress(array|Address $address): static
205    {
206        $this->suggestedAddress = !($address instanceof Address) ? new Address($address) : $address;
207        return $this;
208    }
209
210    /**
211     * Get suggested address
212     *
213     * @return ?Address
214     */
215    public function getSuggestedAddress(): ?Address
216    {
217        return $this->suggestedAddress;
218    }
219
220    /**
221     * Has suggested address
222     *
223     * @return bool
224     */
225    public function hasSuggestedAddress(): bool
226    {
227        return (!empty($this->suggestedAddress));
228    }
229
230    /**
231     * Get response
232     *
233     * @return array
234     */
235    public function getResponse(): array
236    {
237        return $this->response;
238    }
239
240    /**
241     * Is confirmed
242     *
243     * @return bool
244     */
245    public function isConfirmed(): bool
246    {
247        return $this->confirmed;
248    }
249
250    /**
251     * Validate address
252     *
253     * @param  array|Address|null $address
254     * @throws Exception
255     * @return bool
256     */
257    public function validate(array|Address|null $address = null): bool
258    {
259        if ($address !== null) {
260            $this->setOriginalAddress($address);
261        }
262
263        if (!$this->hasOriginalAddress()) {
264            throw new Exception('Error: No original address was provided.');
265        }
266        if (!$this->originalAddress->hasPostalCode()) {
267            throw new Exception('Error: The original address postal code is required.');
268        }
269
270        $addressData = [
271            'address' => [
272                'addressLines' => [],
273                'postalCode'   => $this->originalAddress['postal_code']
274            ]
275        ];
276
277        if (!empty($this->originalAddress['address1'])) {
278            $addressData['address']['addressLines'][] = $this->originalAddress['address1'];
279        }
280        if (!empty($this->originalAddress['address2'])) {
281            $addressData['address']['addressLines'][] = $this->originalAddress['address2'];
282        }
283        if (!empty($this->originalAddress['city']) && !empty($this->originalAddress['state'])) {
284            $addressData['address']['addressLines'][] = $this->originalAddress['city'] . ', ' . $this->originalAddress['state'];
285        }
286
287        $response = $this->client->setData($addressData)->post();
288
289        if ($response->isSuccess()) {
290            $this->response = $response->getParsedResponse();
291            if (isset($this->response['result']['verdict']) && isset($this->response['result']['verdict']['possibleNextAction'])) {
292                switch ($this->response['result']['verdict']['possibleNextAction']) {
293                    // A standardized/corrected address exists and differs from the input -
294                    // ask the caller to confirm it via the suggested address.
295                    case 'CONFIRM':
296                    case 'CONFIRM_ADD_SUBPREMISES':
297                        $addressString = ucwords(strtolower($this->response['result']['uspsData']['standardizedAddress']['firstAddressLine']));
298                        if (!empty($this->response['result']['uspsData']['standardizedAddress']['city'])) {
299                            $addressString .= ', ' . ucwords(strtolower($this->response['result']['uspsData']['standardizedAddress']['city']));
300                        }
301                        if (!empty($this->response['result']['uspsData']['standardizedAddress']['state'])) {
302                            $addressString .= ', ' . $this->response['result']['uspsData']['standardizedAddress']['state'];
303                        }
304                        if (!empty($this->response['result']['uspsData']['standardizedAddress']['zipCode'])) {
305                            $addressString .= ' ' . $this->response['result']['uspsData']['standardizedAddress']['zipCode'];
306                        }
307                        if (!empty($this->response['result']['uspsData']['standardizedAddress']['zipCodeExtension'])) {
308                            $addressString .= '-' . $this->response['result']['uspsData']['standardizedAddress']['zipCodeExtension'];
309                        }
310
311                        $this->setSuggestedAddress($this->parseAddress($addressString));
312                        break;
313                    // The address is valid as given - nothing to confirm or suggest.
314                    case 'ACCEPT':
315                        $this->confirmed = true;
316                        break;
317                    // 'FIX' (and any other/unknown value) means the address has unresolved
318                    // issues that a caller must correct themselves - it is neither confirmed
319                    // nor does Google provide a reliable standardized replacement for it.
320                    default:
321                        break;
322                }
323            }
324        }
325
326        return $this->isConfirmed();
327    }
328
329    /**
330     * Parse address
331     *
332     * @param  string $address
333     * @return array
334     */
335    protected function parseAddress(string $address): array
336    {
337        $parser = new AddressParser();
338        $parser->parse($address);
339
340        $address1 = trim(trim((string)$parser->getStreetNumber()) . ' ' .
341            (($parser->hasRouteType()) ? trim((string)$parser->getStreetName()) . ' ' .
342            trim((string)$parser->getRouteType()) : trim((string)$parser->getStreetName())));
343
344        $postalCode = trim((string)$parser->getPostalCode());
345        $zip4       = trim((string)$parser->getZip4());
346
347        if (!empty($zip4)) {
348            $postalCode .= '-' . $zip4;
349        }
350
351        return array_filter([
352            'address1'      => $address1,
353            'address2' => trim((string)$parser->getUnit()),
354            'state'         => trim((string)$parser->getStateCode()),
355            'city'          => trim((string)$parser->getCity()),
356            'postal_code'   => $postalCode,
357        ]);
358    }
359
360}