Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
75.51% covered (warning)
75.51%
74 / 98
50.00% covered (danger)
50.00%
2 / 4
CRAP
0.00% covered (danger)
0.00%
0 / 1
API
75.51% covered (warning)
75.51%
74 / 98
50.00% covered (danger)
50.00%
2 / 4
29.11
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 check_address_for_order
84.15% covered (warning)
84.15%
69 / 82
0.00% covered (danger)
0.00%
0 / 1
18.15
 validate_address
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 recheck_bad_address_orders
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
0.00%
0 / 1
12
1<?php
2/**
3 *
4 */
5
6namespace BrianHenryIE\WC_Address_Validation\API;
7
8use BrianHenryIE\WC_Address_Validation\API\Validators\No_Validator_Exception;
9use BrianHenryIE\WC_Address_Validation\API_Interface;
10use BrianHenryIE\WC_Address_Validation\Settings_Interface;
11use BrianHenryIE\WC_Address_Validation\WP_Includes\Cron;
12use BrianHenryIE\WC_Address_Validation\WP_Includes\Deactivator;
13use BrianHenryIE\WC_Address_Validation\WooCommerce\Order_Status;
14use Psr\Log\LoggerAwareTrait;
15use Psr\Log\LoggerInterface;
16use WC_Data_Exception;
17use WC_Order;
18
19/**
20 * Class API
21 *
22 * @package BrianHenryIE\WC_Address_Validation\API
23 */
24class 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}