Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
0.00% |
0 / 143 |
|
0.00% |
0 / 4 |
CRAP | |
0.00% |
0 / 1 |
USPS_Address_Validator | |
0.00% |
0 / 143 |
|
0.00% |
0 / 4 |
756 | |
0.00% |
0 / 1 |
__construct | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
2 | |||
wc_address_array_to_usps_address_object | |
0.00% |
0 / 8 |
|
0.00% |
0 / 1 |
2 | |||
validate | |
0.00% |
0 / 69 |
|
0.00% |
0 / 1 |
110 | |||
add_address_common_mistakes | |
0.00% |
0 / 63 |
|
0.00% |
0 / 1 |
240 |
1 | <?php |
2 | /** |
3 | * @see https://www.usps.com/business/web-tools-apis/address-information-api.htm |
4 | */ |
5 | |
6 | namespace BrianHenryIE\WC_Address_Validation\API\Validators; |
7 | |
8 | use BrianHenryIE\WC_Address_Validation\API\Address_Validator_Interface; |
9 | use BrianHenryIE\WC_Address_Validation\USPS\Address; |
10 | use BrianHenryIE\WC_Address_Validation\USPS\AddressVerify; |
11 | use Psr\Log\LoggerAwareTrait; |
12 | use Psr\Log\LoggerInterface; |
13 | |
14 | class USPS_Address_Validator implements Address_Validator_Interface { |
15 | |
16 | use LoggerAwareTrait; |
17 | |
18 | /** |
19 | * @var AddressVerify |
20 | */ |
21 | protected AddressVerify $address_verify; |
22 | |
23 | public function __construct( AddressVerify $address_verify, LoggerInterface $logger ) { |
24 | |
25 | $this->setLogger( $logger ); |
26 | $this->address_verify = $address_verify; |
27 | |
28 | $this->address_verify->setRevision( 1 ); |
29 | } |
30 | |
31 | /** |
32 | * Given an address array from WooCommerce (i.e. keyed as WooCommerce does), this returns an object as expected by |
33 | * the USPS validator. In particular, the USPS Address object is sensitive to the order in which the address parts are added. |
34 | * |
35 | * @param array array{address_1: string, address_2: string, city: string, state: string, postcode: string, country: string} $address |
36 | * |
37 | * @return Address |
38 | */ |
39 | protected function wc_address_array_to_usps_address_object( array $address ): Address { |
40 | $address_to_validate = new Address(); |
41 | |
42 | // This needs to be set in order. |
43 | $address_to_validate->setApt( $address['address_2'] ); |
44 | $address_to_validate->setAddress( $address['address_1'] ); |
45 | $address_to_validate->setCity( $address['city'] ); |
46 | $address_to_validate->setState( $address['state'] ); |
47 | $address_to_validate->setZip5( substr( $address['postcode'], 0, 5 ) ); |
48 | $address_to_validate->setZip4( '' ); |
49 | |
50 | return $address_to_validate; |
51 | } |
52 | |
53 | /** |
54 | * TODO: AE (military) addresses cannot be validated. |
55 | * |
56 | * @param array{address_1: string, address_2: string, city: string, state: string, postcode: string, country: string} $address |
57 | * @return array{success: bool, original_address: array, updated_address: ?array, message: ?string, error_message: ?string} |
58 | */ |
59 | public function validate( array $address ): array { |
60 | |
61 | $result = array(); |
62 | $original_address = $address; |
63 | $result['original_address'] = $address; |
64 | |
65 | // Since USPS always returns the address in all-uppercase letters, let's convert the input to |
66 | // all uppercase so we are later comparing like to like. |
67 | $address = array_map( 'strtoupper', $address ); |
68 | |
69 | // Remove plural 's. |
70 | $address = str_replace( '\'S', 'S', $address ); |
71 | |
72 | // Let's clean up punctuation which USPS does not like. |
73 | // TODO: is "#" ok? |
74 | // forward slash is ok, e.g. 123 1/2 22nd st. |
75 | $address = preg_replace( '/[^a-zA-Z\d\s\/]+/', ' ', $address ); |
76 | // Trim and remove double spaces. |
77 | $address = array_map( 'normalize_whitespace', $address ); |
78 | |
79 | $address_to_validate = $this->wc_address_array_to_usps_address_object( $address ); |
80 | |
81 | if ( in_array( $address['state'], array( 'AE', 'AP', 'AA' ), true ) ) { |
82 | |
83 | $result['success'] = true; |
84 | |
85 | $message = 'Military address – cannot be validated, assumed to be correct. Sanitized from: `' . implode( ', ', array_filter( $original_address ) ) . '` to: `' . implode( ', ', array_filter( $address ) ) . '`'; |
86 | |
87 | $this->logger->debug( $message, $result ); |
88 | |
89 | $result['message'] = $message; |
90 | |
91 | return $result; |
92 | } |
93 | |
94 | // Add the address object to the address verify class |
95 | $this->address_verify->addAddress( $address_to_validate ); |
96 | |
97 | $this->add_address_common_mistakes( $address ); |
98 | |
99 | // Perform the request and return result |
100 | $xml_response = $this->address_verify->verify(); |
101 | $array_response = $this->address_verify->convertResponseToArray(); |
102 | |
103 | // See if it was successful |
104 | // AddressVerify::isSuccess() will return false if any are errors. |
105 | // success = true could still be everything error! |
106 | $success = isset( $array_response['AddressValidateResponse'] ) && isset( $array_response['AddressValidateResponse']['Address'] ); |
107 | |
108 | if ( ! $success ) { |
109 | |
110 | // TODO: Check the reason for failure... e.g. timeout rather than bad address. |
111 | |
112 | // "Multiple addresses were found" (not really here). |
113 | // "Peer’s Certificate has expired." |
114 | |
115 | $result['success'] = false; |
116 | |
117 | $error_message = 'USPS Address Information API failed validation: ' . $this->address_verify->getErrorMessage() . "\n" . implode( ', ', array_filter( $original_address ) ); |
118 | |
119 | $result['error_message'] = $error_message; |
120 | |
121 | $this->logger->error( |
122 | $error_message, |
123 | array( |
124 | 'request' => $this->address_verify->getPostData(), |
125 | 'error_message' => $this->address_verify->getErrorMessage(), |
126 | 'xml_response' => $xml_response, |
127 | 'array_response' => $array_response, |
128 | 'address' => $address, |
129 | ) |
130 | ); |
131 | |
132 | return $result; |
133 | |
134 | } else { |
135 | |
136 | $response_address = $array_response['AddressValidateResponse']['Address']; |
137 | |
138 | // If one address was returned. |
139 | if ( isset( $response_address['@attributes'] ) ) { |
140 | $returned_addresses = array( $response_address ); |
141 | } else { |
142 | // If many were found, especially when common errors were found in the input. |
143 | $returned_addresses = $response_address; |
144 | } |
145 | |
146 | foreach ( $returned_addresses as $index => $usps_address ) { |
147 | |
148 | // If it's an error, continue. |
149 | // Later, use $returned_addresses[0] for detailed error message if none found at all. |
150 | if ( isset( $usps_address['Error'] ) ) { |
151 | |
152 | continue; |
153 | |
154 | } else { |
155 | |
156 | $updated_address = array(); |
157 | |
158 | // WooCommerce address_1 is the building and street, whereas USPS Address1 is the apartment number. |
159 | $updated_address['address_1'] = $usps_address['Address2']; |
160 | $updated_address['address_2'] = isset( $usps_address['Address1'] ) ? $usps_address['Address1'] : ''; |
161 | $updated_address['city'] = $usps_address['City']; |
162 | $updated_address['state'] = $usps_address['State']; |
163 | $updated_address['country'] = 'US'; |
164 | |
165 | $zip = $usps_address['Zip5']; |
166 | |
167 | // Not all addresses have a +4 zip code. |
168 | if ( ! empty( $usps_address['Zip4'] ) ) { |
169 | $zip .= '-' . $usps_address['Zip4']; |
170 | } |
171 | $updated_address['postcode'] = $zip; |
172 | |
173 | $result['updated_address'] = $updated_address; |
174 | |
175 | $result['success'] = true; |
176 | |
177 | $message = "Shipping address validated by USPS Address Verification API. \n\nOld address was: \n`" . implode( ", \n", array_filter( $original_address ) ) . "`. \n\nNew address is: \n`" . implode( ", \n", array_filter( $updated_address ) ) . '`.'; |
178 | |
179 | $result['message'] = $message; |
180 | |
181 | $this->logger->debug( preg_replace( '/\n+/', '', $message ) ); |
182 | |
183 | return $result; |
184 | } |
185 | |
186 | // If it's a match, break. |
187 | |
188 | // TODO: This is a weird scenario, but it has happened. |
189 | // TODO: This shouldn't be in here... |
190 | if ( $address['state'] !== $usps_address['State'] ) { |
191 | $error_message = 'State returned from USPS ' . $usps_address['State'] . ' did not match customer supplied state ' . strtoupper( $address['state'] ); |
192 | $this->logger->notice( $error_message, array( 'array_response' => $array_response ) ); |
193 | |
194 | $result['error_message'] = $error_message; |
195 | $result['success'] = false; |
196 | |
197 | return $result; |
198 | } |
199 | } |
200 | |
201 | // No valid address found. |
202 | $usps_address = $returned_addresses[0]; |
203 | |
204 | $message = $usps_address['Error']['Description'] . ' for address: `' . implode( ', ', array_filter( $original_address ) ) . '`.'; |
205 | |
206 | $result['success'] = false; |
207 | |
208 | $this->logger->debug( $message, $result ); |
209 | |
210 | $result['error_message'] = $message; |
211 | |
212 | return $result; |
213 | |
214 | } |
215 | } |
216 | |
217 | |
218 | /** |
219 | * Probably unnecessary to apply every combination of these? |
220 | * |
221 | * TODO: document if the supplied address is assumed to be uppercase |
222 | * |
223 | * @param array $address |
224 | * |
225 | * @return void |
226 | */ |
227 | protected function add_address_common_mistakes( array $address ) { |
228 | |
229 | // Switch line one and line two |
230 | if ( ! empty( $address['address_1'] && ! empty( $address['address_2'] ) ) ) { |
231 | $new_address = $address; |
232 | $new_address['address_1'] = $address['address_2']; |
233 | $new_address['address_2'] = $address['address_1']; |
234 | $this->address_verify->addAddress( $this->wc_address_array_to_usps_address_object( $new_address ) ); |
235 | unset( $new_address ); |
236 | } |
237 | |
238 | $us_to_highway = preg_replace( '/(\d+\s)(US)(\s\d+.*)/', ' $1Highway$3', $address['address_1'] ); |
239 | if ( $us_to_highway !== $address['address_1'] ) { |
240 | $new_address = $address; |
241 | $new_address['address_1'] = $us_to_highway; |
242 | $this->address_verify->addAddress( $this->wc_address_array_to_usps_address_object( $new_address ) ); |
243 | unset( $new_address ); |
244 | } |
245 | |
246 | // Remove space in "W123 N123 ...". |
247 | $remove_space = preg_replace( '/(\w\d+)\s(\w\d+.*)/', '$1$2', $address['address_1'] ); |
248 | if ( $remove_space !== $address['address_1'] ) { |
249 | $new_address = $address; |
250 | $new_address['address_1'] = $remove_space; |
251 | $this->address_verify->addAddress( $this->wc_address_array_to_usps_address_object( $new_address ) ); |
252 | unset( $new_address ); |
253 | } |
254 | |
255 | // Remove an accidental space in the street number, e.g. 13 12 63st -> 1312 63rd st. |
256 | $remove_accidental_space = preg_replace( '/(\d+)\s(\d+\s.*)/', '$1$2', $address['address_1'] ); |
257 | if ( $remove_accidental_space !== $address['address_1'] ) { |
258 | $new_address = $address; |
259 | $new_address['address_1'] = $remove_accidental_space; |
260 | $this->address_verify->addAddress( $this->wc_address_array_to_usps_address_object( $new_address ) ); |
261 | unset( $new_address ); |
262 | } |
263 | |
264 | // Replace the word Interstate with just the initial I. |
265 | if ( false !== strpos( $address['address_1'], 'INTERSTATE' ) ) { |
266 | $new_address = $address; |
267 | $new_address['address_1'] = str_replace( 'INTERSTATE', 'I', $address['address_1'] ); |
268 | $this->address_verify->addAddress( $this->wc_address_array_to_usps_address_object( $new_address ) ); |
269 | unset( $new_address ); |
270 | } |
271 | |
272 | // Replace the word DRIVE with just DR. |
273 | if ( false !== strpos( $address['address_1'], 'DRIVE' ) ) { |
274 | $new_address = $address; |
275 | $new_address['address_1'] = str_replace( 'DRIVE', 'DR', $address['address_1'] ); |
276 | $this->address_verify->addAddress( $this->wc_address_array_to_usps_address_object( $new_address ) ); |
277 | unset( $new_address ); |
278 | } |
279 | |
280 | // If "1 APT" is written, swap it to be "APT 1". |
281 | $num_apt_swap = preg_replace( '/(\d+)\s(.+)/', '$2 $1', $address['address_2'] ); |
282 | if ( $num_apt_swap !== $address['address_2'] ) { |
283 | $new_address = $address; |
284 | $new_address['address_2'] = $num_apt_swap; |
285 | $this->address_verify->addAddress( $this->wc_address_array_to_usps_address_object( $new_address ) ); |
286 | unset( $new_address ); |
287 | } |
288 | |
289 | // Try merging address_1 and address_2, sometimes people write the house number on one line and the street on the second. |
290 | $new_address = $address; |
291 | $new_address['address_1'] = $new_address['address_1'] . ' ' . $new_address['address_2']; |
292 | $new_address['address_2'] = ''; |
293 | $this->address_verify->addAddress( $this->wc_address_array_to_usps_address_object( $new_address ) ); |
294 | unset( $new_address ); |
295 | |
296 | // Sometimes autofill fills the complete address on line one, then the rest of the address as normal. |
297 | $address_parts = array( 'postcode', 'state', 'city', 'address_2', 'address_1' ); |
298 | $new_address = $address; |
299 | foreach ( $address_parts as $part_to_search ) { |
300 | if ( empty( $part_to_search ) ) { |
301 | continue; |
302 | } |
303 | foreach ( $address_parts as $part_to_edit ) { |
304 | if ( $part_to_edit === $part_to_search ) { |
305 | continue; |
306 | } |
307 | $new_address[ $part_to_edit ] = normalize_whitespace( preg_replace( '/\b' . preg_quote( $new_address[ $part_to_search ], '/' ) . '\b/', '', $new_address[ $part_to_edit ] ) ); |
308 | } |
309 | } |
310 | // If changes have been made, use the new address. |
311 | if ( implode( '', $address ) !== implode( '', $new_address ) ) { |
312 | $this->address_verify->addAddress( $this->wc_address_array_to_usps_address_object( $new_address ) ); |
313 | } |
314 | unset( $new_address ); |
315 | |
316 | // If "RD" has been misspelled as "RE". |
317 | $re_to_rd = preg_replace( '/.*\bRE\b/', '$1RD', $address['address_1'] ); |
318 | if ( $re_to_rd !== $address['address_1'] ) { |
319 | $new_address = $address; |
320 | $new_address['address_1'] = $re_to_rd; |
321 | $this->address_verify->addAddress( $this->wc_address_array_to_usps_address_object( $new_address ) ); |
322 | unset( $new_address ); |
323 | } |
324 | } |
325 | } |