Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
75.51% |
74 / 98 |
|
50.00% |
2 / 4 |
CRAP | |
0.00% |
0 / 1 |
API | |
75.51% |
74 / 98 |
|
50.00% |
2 / 4 |
29.11 | |
0.00% |
0 / 1 |
__construct | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
1 | |||
check_address_for_order | |
84.15% |
69 / 82 |
|
0.00% |
0 / 1 |
18.15 | |||
validate_address | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
recheck_bad_address_orders | |
0.00% |
0 / 11 |
|
0.00% |
0 / 1 |
12 |
1 | <?php |
2 | /** |
3 | * |
4 | */ |
5 | |
6 | namespace BrianHenryIE\WC_Address_Validation\API; |
7 | |
8 | use BrianHenryIE\WC_Address_Validation\API\Validators\No_Validator_Exception; |
9 | use BrianHenryIE\WC_Address_Validation\API_Interface; |
10 | use BrianHenryIE\WC_Address_Validation\Settings_Interface; |
11 | use BrianHenryIE\WC_Address_Validation\WP_Includes\Cron; |
12 | use BrianHenryIE\WC_Address_Validation\WP_Includes\Deactivator; |
13 | use BrianHenryIE\WC_Address_Validation\WooCommerce\Order_Status; |
14 | use Psr\Log\LoggerAwareTrait; |
15 | use Psr\Log\LoggerInterface; |
16 | use WC_Data_Exception; |
17 | use WC_Order; |
18 | |
19 | /** |
20 | * Class API |
21 | * |
22 | * @package BrianHenryIE\WC_Address_Validation\API |
23 | */ |
24 | class API implements API_Interface { |
25 | |
26 | use LoggerAwareTrait; |
27 | |
28 | const BH_WC_ADDRESS_VALIDATION_CHECKED_META = 'bh_wc_address_validation_checked'; |
29 | |
30 | protected Settings_Interface $settings; |
31 | |
32 | /** |
33 | * An interface to the APIs. |
34 | */ |
35 | protected Address_Validator_Interface $address_validator; |
36 | |
37 | /** |
38 | * API constructor. |
39 | * |
40 | * @param Settings_Interface $settings |
41 | * @param LoggerInterface $logger |
42 | */ |
43 | public function __construct( Address_Validator_Interface $address_validator, Settings_Interface $settings, LoggerInterface $logger ) { |
44 | |
45 | $this->setLogger( $logger ); |
46 | $this->settings = $settings; |
47 | $this->address_validator = $address_validator; |
48 | } |
49 | |
50 | /** |
51 | * Adds the +4 zip code or marks the order with 'bad-address' status. |
52 | * |
53 | * @param WC_Order $order |
54 | * @throws WC_Data_Exception |
55 | */ |
56 | public function check_address_for_order( WC_Order $order, bool $is_manual = false ): void { |
57 | |
58 | $checked_meta = (array) $order->get_meta( self::BH_WC_ADDRESS_VALIDATION_CHECKED_META, true ); |
59 | $reactivating = $order->get_meta( Deactivator::DEACTIVATED_BAD_ADDRESS_META_KEY, true ); |
60 | |
61 | // Only automatically run once, except when reactivating. |
62 | // Always run when manually run. |
63 | // array_filter is here because casting the meta to (array) results `in array( 0 => "" )` rather than a truly empty array. |
64 | if ( ! empty( array_filter( $checked_meta ) ) && false === $is_manual && empty( $reactivating ) ) { |
65 | return; |
66 | } |
67 | |
68 | // Clear the reactivating meta key so it only kicks in once and doesn't interfere later. |
69 | if ( ! empty( $reactivating ) ) { |
70 | $order->delete_meta_data( Deactivator::DEACTIVATED_BAD_ADDRESS_META_KEY ); |
71 | $order->save(); |
72 | } |
73 | |
74 | $this->logger->debug( 'Checking address for order ' . $order->get_id(), array( 'order_id', $order->get_id() ) ); |
75 | |
76 | $order_shipping_address = array(); |
77 | $order_shipping_address['address_1'] = $order->get_shipping_address_1(); |
78 | $order_shipping_address['address_2'] = $order->get_shipping_address_2(); |
79 | $order_shipping_address['city'] = $order->get_shipping_city(); |
80 | $order_shipping_address['state'] = $order->get_shipping_state(); |
81 | $order_shipping_address['postcode'] = $order->get_shipping_postcode(); |
82 | $order_shipping_address['country'] = $order->get_shipping_country(); |
83 | |
84 | try { |
85 | $result = $this->validate_address( $order_shipping_address ); |
86 | } catch ( No_Validator_Exception $e ) { |
87 | $this->logger->info( 'No address validator available for address. ' . implode( ',', $order_shipping_address ), array( 'address' => $order_shipping_address ) ); |
88 | $order->add_order_note( 'No address validator available for address.' . implode( ',', $order_shipping_address ) ); |
89 | $order->save(); |
90 | return; |
91 | } |
92 | |
93 | if ( $result['success'] ) { // Address is valid. |
94 | |
95 | $order_shipping_address = array_map( 'strtoupper', array_map( 'trim', $order_shipping_address ) ); |
96 | |
97 | /** @var array $updated_address */ |
98 | $updated_address = $result['updated_address']; |
99 | |
100 | // Fatal error here where $updated_address was a string '' |
101 | |
102 | $address_was_changed = implode( ',', $order_shipping_address ) !== implode( ',', $updated_address ); |
103 | |
104 | if ( $address_was_changed ) { |
105 | // If the billing address was the same as the shipping address, update it too. |
106 | $order_billing_address = array(); |
107 | $order_billing_address['address_1'] = $order->get_billing_address_1(); |
108 | $order_billing_address['address_2'] = $order->get_billing_address_2(); |
109 | $order_billing_address['city'] = $order->get_billing_city(); |
110 | $order_billing_address['state'] = $order->get_billing_state(); |
111 | $order_billing_address['postcode'] = $order->get_billing_postcode(); |
112 | $order_billing_address['country'] = $order->get_billing_country(); |
113 | $order_billing_address = array_map( 'strtoupper', array_map( 'trim', $order_billing_address ) ); |
114 | |
115 | // Compare the addresses. |
116 | $billing_equals_shipping = implode( ',', $order_shipping_address ) === implode( ',', $order_billing_address ); |
117 | |
118 | $customer = null; |
119 | $customer_id = $order->get_customer_id(); |
120 | if ( 0 !== $customer_id ) { |
121 | $customer = new \WC_Customer( $customer_id ); |
122 | } |
123 | |
124 | $order->set_shipping_address_1( $updated_address['address_1'] ); |
125 | $order->set_shipping_address_2( $updated_address['address_2'] ); |
126 | $order->set_shipping_city( $updated_address['city'] ); |
127 | $order->set_shipping_state( $updated_address['state'] ); |
128 | $order->set_shipping_postcode( $updated_address['postcode'] ); |
129 | |
130 | if ( $billing_equals_shipping ) { |
131 | $order->set_billing_address_1( $updated_address['address_1'] ); |
132 | $order->set_billing_address_2( $updated_address['address_2'] ); |
133 | $order->set_billing_city( $updated_address['city'] ); |
134 | $order->set_billing_state( $updated_address['state'] ); |
135 | $order->set_billing_postcode( $updated_address['postcode'] ); |
136 | } |
137 | |
138 | if ( $billing_equals_shipping && ( $customer instanceof \WC_Customer ) ) { |
139 | $customer->set_billing_address_1( $updated_address['address_1'] ); |
140 | $customer->set_billing_address_2( $updated_address['address_2'] ); |
141 | $customer->set_billing_city( $updated_address['city'] ); |
142 | $customer->set_billing_state( $updated_address['state'] ); |
143 | $customer->set_billing_postcode( $updated_address['postcode'] ); |
144 | } |
145 | |
146 | if ( $customer instanceof \WC_Customer ) { |
147 | $customer->set_shipping_address_1( $updated_address['address_1'] ); |
148 | $customer->set_shipping_address_2( $updated_address['address_2'] ); |
149 | $customer->set_shipping_city( $updated_address['city'] ); |
150 | $customer->set_shipping_state( $updated_address['state'] ); |
151 | $customer->set_shipping_postcode( $updated_address['postcode'] ); |
152 | $customer->save(); |
153 | } |
154 | } |
155 | |
156 | // If this is a re-check, update order status from bad-address to processing. |
157 | if ( Order_Status::BAD_ADDRESS_STATUS === $order->get_status() ) { |
158 | |
159 | // TODO: Use previous status from meta. |
160 | |
161 | $new_status = 'processing'; |
162 | if ( ! empty( $checked_meta ) ) { |
163 | $most_recent = array_pop( $checked_meta ); |
164 | if ( isset( $most_recent['previous_status'] ) ) { |
165 | $new_status = $most_recent['previous_status']; |
166 | } |
167 | } |
168 | |
169 | $order->set_status( $new_status ); |
170 | } |
171 | |
172 | $message = $result['message']; |
173 | $order->add_order_note( $message ); |
174 | |
175 | } else { |
176 | |
177 | $error_message = $result['error_message']; |
178 | |
179 | if ( Order_Status::BAD_ADDRESS_STATUS !== $order->get_status() ) { |
180 | $result['previous_status'] = $order->get_status(); |
181 | } |
182 | |
183 | $order->set_status( Order_Status::BAD_ADDRESS_STATUS ); |
184 | $order->add_order_note( $error_message ); |
185 | |
186 | // Try again in a few hours. |
187 | $args = array( $order->get_id() ); |
188 | wp_schedule_single_event( time() + HOUR_IN_SECONDS * 6, Cron::CHECK_SINGLE_ADDRESS_CRON_JOB, $args ); |
189 | |
190 | } |
191 | |
192 | $checked_meta[ gmdate( DATE_ATOM ) ] = $result; |
193 | $order->update_meta_data( self::BH_WC_ADDRESS_VALIDATION_CHECKED_META, $checked_meta ); |
194 | |
195 | $order->save(); |
196 | } |
197 | |
198 | /** |
199 | * @param array{address_1: string, address_2: string, city: string, state: string, postcode: string, country: string} $address_array |
200 | * @return array{success: bool, original_address: array, updated_address: ?array, message: ?string, error_message: ?string} |
201 | */ |
202 | public function validate_address( array $address_array ): array { |
203 | |
204 | $result = $this->address_validator->validate( $address_array ); |
205 | |
206 | return $result; |
207 | } |
208 | |
209 | /** |
210 | * Often the USPS API returns "no address" but later a manual invocation will validate the address. |
211 | * |
212 | * This function is intended to be hooked on a regular cron (~4 hours) to re-run the check. |
213 | */ |
214 | public function recheck_bad_address_orders(): void { |
215 | |
216 | $orders = wc_get_orders( |
217 | array( |
218 | 'limit' => -1, |
219 | 'type' => 'shop_order', |
220 | 'status' => array( 'wc-' . Order_Status::BAD_ADDRESS_STATUS ), |
221 | ) |
222 | ); |
223 | |
224 | // Probably enabled 'paginate' in the args. |
225 | if ( ! is_array( $orders ) ) { |
226 | return; |
227 | } |
228 | |
229 | foreach ( $orders as $order ) { |
230 | |
231 | $this->check_address_for_order( $order, true ); |
232 | } |
233 | } |
234 | } |