Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
94.23% covered (success)
94.23%
49 / 52
85.71% covered (warning)
85.71%
6 / 7
CRAP
0.00% covered (danger)
0.00%
0 / 1
Checkout_Shortcode
94.23% covered (success)
94.23%
49 / 52
85.71% covered (warning)
85.71%
6 / 7
31.18
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 enqueue_scripts
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
3
 parse_post_on_update_order_review
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
6
 process_address_update
88.00% covered (warning)
88.00%
22 / 25
0.00% covered (danger)
0.00%
0 / 1
15.39
 rerender_billing_fields_fragment
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 rerender_shipping_fields_fragment
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 get_fragment
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
2
1<?php
2/**
3 * Handle functions specific to the WooCommerce shortcode (traditional/PHP) checkout.
4 *
5 * @package brianhenryie/bh-wc-postcode-address-autofill
6 */
7
8namespace BrianHenryIE\WC_Postcode_Address_Autofill\WooCommerce;
9
10use BrianHenryIE\WC_Postcode_Address_Autofill\API_Interface;
11use BrianHenryIE\WC_Postcode_Address_Autofill\Settings_Interface;
12
13/**
14 * Hook onto WooCommerce checkout functions to reorder the fields and autofill the values.
15 */
16class Checkout_Shortcode {
17
18    /**
19     * The core plugin functions.
20     *
21     * @uses API_Interface::get_locations_for_postcode()
22     */
23    protected API_Interface $api;
24
25    /**
26     * Plugin settings for assets URL and caching version.
27     *
28     * @uses Settings_Interface::get_plugin_basename()
29     * @uses Settings_Interface::get_plugin_version()
30     */
31    protected Settings_Interface $settings;
32
33    /**
34     * Constructor
35     *
36     * @param API_Interface      $api The main plugin functions.
37     * @param Settings_Interface $settings The plugin settings.
38     */
39    public function __construct( API_Interface $api, Settings_Interface $settings ) {
40        $this->settings = $settings;
41        $this->api      = $api;
42    }
43
44    /**
45     * Register the JavaScript files used for WooCommerce.
46     *
47     * This JS fires the `update_checkout` trigger when a postcode is entered.
48     *
49     * @hooked wp_enqueue_scripts
50     */
51    public function enqueue_scripts(): void {
52
53        if ( ! function_exists( 'is_checkout' ) || ! is_checkout() ) {
54            return;
55        }
56
57        $version = $this->settings->get_plugin_version();
58
59        wp_enqueue_script( 'bh-wc-postcode-address-autofill-checkout', plugin_dir_url( $this->settings->get_plugin_basename() ) . 'assets/bh-wc-postcode-address-autofill-checkout.js', array( 'jquery' ), $version, true );
60    }
61
62    /**
63     * Parse the $_POST 'post_data' string for the zip code and set the city in the checkout object.
64     *
65     * @see WC_Ajax::update_order_review()
66     * @see assets/js/frontend/checkout.js
67     *
68     * @hooked woocommerce_checkout_update_order_review
69     *
70     * @param string $posted_data `posted_data` key of array posted by checkout.js.
71     */
72    public function parse_post_on_update_order_review( string $posted_data ): void {
73
74        $post_array = array();
75        parse_str( $posted_data, $post_array );
76
77        foreach ( array( 'billing', 'shipping' ) as $address_type ) {
78            if ( ! empty( $post_array[ "{$address_type}_postcode" ] )
79                && ! empty( $post_array[ "{$address_type}_country" ] )
80                && is_string( $post_array[ "{$address_type}_postcode" ] )
81                && is_string( $post_array[ "{$address_type}_country" ] )
82            ) {
83                $this->process_address_update( $address_type, $post_array );
84            }
85        }
86    }
87
88    /**
89     * Given a non-empty postcode and country, update the state and city. And add a filter to re-render the fields.
90     *
91     * @param string               $address_type Shipping or billing.
92     * @param array<string,string> $post_array The $_POST array.
93     */
94    protected function process_address_update( string $address_type, array $post_array ): void {
95        $postcode = $post_array[ "{$address_type}_postcode" ];
96        $country  = $post_array[ "{$address_type}_country" ];
97
98        $customer_session_data_prefix = 'shipping' === $address_type ? 'shipping_' : '';
99
100        // If the postcode did not change on this request, do not alter the address.
101        $customer_session_data = WC()->session->get( 'customer' );
102        if ( ! is_null( $customer_session_data ) && isset( $customer_session_data[ "{$customer_session_data_prefix}postcode" ] ) && $postcode === $customer_session_data[ "{$customer_session_data_prefix}postcode" ] ) {
103            return;
104        }
105
106        $locations = $this->api->get_locations_for_postcode( $country, $postcode );
107
108        if ( empty( $locations ) ) {
109            return;
110        }
111
112        // Crude! One postcode could represent multiple towns/cities but for v1 we only work with one.
113        $location = $locations->get_first();
114
115        if ( empty( $location ) ) {
116            return;
117        }
118
119        $new_state = $location->get_state();
120        $new_city  = $location->get_city();
121
122        // If the correct city and state are already set, there is nothing to do.
123        if (
124            isset( $post_array[ "{$address_type}_state" ] ) && $post_array[ "{$address_type}_state" ] === $new_state
125            && isset( $post_array[ "{$address_type}_city" ] ) && is_string( $post_array[ "{$address_type}_city" ] ) && stripos( $post_array[ "{$address_type}_city" ], $new_city ) === 0
126        ) {
127            return;
128        }
129
130        $post_prefix = 'shipping' === $address_type ? 's_' : '';
131
132        // Handle Puerto Rico edge case.
133        if ( 'PR' === $new_state ) {
134            $_POST[ "{$post_prefix}country" ] = $new_state;
135        } else {
136            $_POST[ "{$post_prefix}state" ] = $new_state;
137        }
138        $_POST[ "{$post_prefix}city" ] = $new_city;
139
140        'shipping' === $address_type
141            ? add_filter( 'woocommerce_update_order_review_fragments', array( $this, 'rerender_shipping_fields_fragment' ) )
142            : add_filter( 'woocommerce_update_order_review_fragments', array( $this, 'rerender_billing_fields_fragment' ) );
143    }
144
145    /**
146     * `.woocommerce-billing-fields` is not re-rendered by default.
147     *
148     * @see WC_Ajax::update_order_review()
149     * @hooked woocommerce_update_order_review_fragments
150     *
151     * @param array<string, string> $fragments Associative array of DOM selectors => HTML to be replaced.
152     *
153     * @return array<string, string>
154     */
155    public function rerender_billing_fields_fragment( array $fragments ): array {
156
157        $billing_fragment = $this->get_fragment( 'billing' );
158
159        if ( ! empty( $billing_fragment ) ) {
160            $fragments['.woocommerce-billing-fields'] = $billing_fragment;
161        }
162
163        return $fragments;
164    }
165
166    /**
167     * `.woocommerce-shipping-fields` is not re-rendered by default.
168     *
169     * @see WC_Ajax::update_order_review()
170     * @hooked woocommerce_update_order_review_fragments
171     *
172     * @param array<string, string> $fragments Associative array of DOM selectors => HTML to be replaced.
173     *
174     * @return array<string, string>
175     */
176    public function rerender_shipping_fields_fragment( array $fragments ): array {
177
178        $shipping_fragment = $this->get_fragment( 'shipping' );
179
180        if ( ! empty( $shipping_fragment ) ) {
181            $fragments['.woocommerce-shipping-fields'] = $shipping_fragment;
182        }
183
184        return $fragments;
185    }
186
187    /**
188     * Get the HTML for the billing or shipping fields of the shortcode checkout.
189     *
190     * @param string $address_type Billing or shipping.
191     *
192     * @return ?string
193     */
194    protected function get_fragment( string $address_type ): ?string {
195
196        $checkout = WC()->checkout();
197
198        ob_start();
199
200        wc_get_template( "checkout/form-{$address_type}.php", array( 'checkout' => $checkout ) );
201
202        $woocommerce_address_fields = ob_get_clean();
203
204        return is_string( $woocommerce_address_fields ) ? $woocommerce_address_fields : null;
205    }
206}