$rate ) { $pickup = self::is_pickup( $rate_id ); if ( 'in' === $ev['status'] ) { if ( $pickup && $keep_pickup ) { continue; } if ( ! Zones::allows_method( $ev['zone'], $rate_id ) ) { unset( $rates[ $rate_id ] ); } elseif ( ! $pickup && ! empty( $ev['zone']['eta'] ) && method_exists( $rate, 'set_label' ) ) { $rates[ $rate_id ]->set_label( self::label_with_eta( $rate->get_label(), $ev['zone']['eta'] ) ); } } elseif ( ! ( $pickup && $keep_pickup ) ) { unset( $rates[ $rate_id ] ); } } return $rates; } /** * Shipping option name with the zone's delivery time, e.g. "Delivery (30 min)". * * The Block cart only prints the name of the selected option, so this is the one place that shows the ETA there. Rates * are cached in the session, so a label that already carries the ETA is left alone. * * @param string $label Label. * @param string $eta ETA text from the zone. * @return string */ public static function label_with_eta( $label, $eta ) { $eta = trim( (string) $eta ); if ( '' === $eta || false !== strpos( (string) $label, '(' . $eta . ')' ) ) { return (string) $label; } return trim( (string) $label ) . ' (' . $eta . ')'; } /** * Message shown when no shipping option is left. * * @param string $html Default HTML. * @return string */ public static function no_shipping_html( $html ) { $ev = Resolver::evaluate_destination( self::customer_destination() ); if ( 'out' === $ev['status'] ) { return '
' . esc_html( Settings::message( 'msg_out' ) ) . '
'; } if ( 'unknown' === $ev['status'] ) { return '' . esc_html( Settings::message( 'msg_unverified' ) ) . '
'; } return $html; } /** * Add the zone's delivery fee to the cart. * * @param \WC_Cart $cart Cart. */ public static function add_fee( $cart ) { if ( ( is_admin() && ! wp_doing_ajax() ) || ! $cart->needs_shipping() ) { return; } $ev = Resolver::evaluate_destination( self::cart_destination( $cart ) ); if ( 'in' !== $ev['status'] || self::pickup_chosen() ) { return; } $fee = Zones::fee( $ev['zone'] ); if ( $fee > 0 ) { $label = ! empty( $ev['zone']['label'] ) ? $ev['zone']['label'] : __( 'Delivery', 'reachzone' ); /* translators: %s: delivery zone name */ $cart->add_fee( sprintf( __( 'Delivery fee (%s)', 'reachzone' ), $label ), $fee, false ); } } /** * Classic checkout: block orders outside the zones and below the minimum. * * @param array $data Posted checkout data. * @param \WP_Error $errors Errors. */ public static function validate_classic( $data, $errors ) { if ( ! WC()->cart || ! WC()->cart->needs_shipping() ) { return; } $chosen = isset( $data['shipping_method'] ) ? (array) $data['shipping_method'] : array(); if ( ! empty( $chosen ) && self::is_pickup( reset( $chosen ) ) ) { return; } $prefix = ! empty( $data['ship_to_different_address'] ) ? 'shipping_' : 'billing_'; $dest = array( 'address_1' => isset( $data[ $prefix . 'address_1' ] ) ? $data[ $prefix . 'address_1' ] : '', 'city' => isset( $data[ $prefix . 'city' ] ) ? $data[ $prefix . 'city' ] : '', 'state' => isset( $data[ $prefix . 'state' ] ) ? $data[ $prefix . 'state' ] : '', 'postcode' => isset( $data[ $prefix . 'postcode' ] ) ? $data[ $prefix . 'postcode' ] : '', 'country' => isset( $data[ $prefix . 'country' ] ) ? $data[ $prefix . 'country' ] : '', ); $problem = self::problem_for( Resolver::evaluate_destination( $dest ), WC()->cart ); if ( $problem ) { if ( 'out' === $problem['status'] ) { $res = Resolver::evaluate_destination( $dest ); if ( null !== $res['lat'] ) { Demand::record( $res['lat'], $res['lon'], 'checkout' ); } } $errors->add( 'reachzone_' . $problem['status'], $problem['message'] ); } } /** * Show a minimum-order or outside-the-area notice on the classic cart and checkout pages. */ public static function cart_notice() { if ( did_action( 'woocommerce_before_checkout_process' ) || ! WC()->cart || ! WC()->cart->needs_shipping() ) { return; } $ev = Resolver::evaluate_destination( self::customer_destination() ); // Only the minimum order here. "Outside" is already explained by the shipping section, and the address may be // incomplete while the shopper is still typing. if ( 'in' !== $ev['status'] || self::pickup_chosen() ) { return; } $problem = self::problem_for( $ev, WC()->cart ); if ( $problem && 'min_order' === $problem['status'] && ! wc_has_notice( $problem['message'], 'error' ) ) { wc_add_notice( $problem['message'], 'error' ); } } /** * Block checkout: add Store API cart errors (shown as notices and refusing the order). * * @param \WP_Error $cart_errors Errors. * @param \WC_Cart $cart Cart. */ public static function store_api_errors( $cart_errors, $cart ) { if ( ! $cart->needs_shipping() || self::pickup_chosen() ) { return; } $dest = self::cart_destination( $cart ); $ev = Resolver::evaluate_destination( $dest ); $problem = self::problem_for( $ev, $cart ); if ( $problem ) { // Older Checkout blocks validate while the shopper is still typing: only a complete address (with a city or postcode) counts as demand. $complete = '' !== trim( (string) ( $dest['city'] ?? '' ) ) || '' !== trim( (string) ( $dest['postcode'] ?? '' ) ); if ( 'out' === $problem['status'] && null !== $ev['lat'] && $complete && WC()->session ) { // The Store API validates the cart on every refresh: count one attempt per shopper and spot per hour. Demand::record_once( $ev['lat'], $ev['lon'], 'checkout', WC()->session->get_customer_id() ); } $cart_errors->add( 'reachzone_' . $problem['status'], $problem['message'] ); } } /** * What is wrong with this evaluation, if anything? * * @param array $ev Evaluation from the Resolver. * @param \WC_Cart $cart Cart. * @return array|null array( status => out|unknown|min_order, message => string ). */ public static function problem_for( array $ev, $cart ) { $settings = Settings::get(); if ( 'out' === $ev['status'] ) { return array( 'status' => 'out', 'message' => Settings::message( 'msg_out' ), ); } if ( 'unknown' === $ev['status'] && 'block' === $settings['unverified_policy'] ) { return array( 'status' => 'unknown', 'message' => Settings::message( 'msg_unverified' ), ); } if ( 'in' === $ev['status'] ) { $check = Zones::evaluate_cart( $ev['zone'], (float) $cart->get_cart_contents_total() ); if ( ! $check['min_met'] ) { return array( 'status' => 'min_order', 'message' => str_replace( array( '{zone}', '{min}', '{missing}' ), array( isset( $ev['zone']['label'] ) ? $ev['zone']['label'] : '', Checker::money_text( $check['min_order'] ), Checker::money_text( $check['shortfall'] ) ), Settings::message( 'msg_min_order' ) ), ); } } return null; } /** * Is a pickup method currently selected in the session? * * @return bool */ private static function pickup_chosen() { $chosen = WC()->session ? WC()->session->get( 'chosen_shipping_methods' ) : array(); return ! empty( $chosen ) && self::is_pickup( reset( $chosen ) ); } /** * Destination of the first shipping package of a cart. * * @param \WC_Cart $cart Cart. * @return array */ private static function cart_destination( $cart ) { $packages = $cart->get_shipping_packages(); $first = is_array( $packages ) ? reset( $packages ) : false; return $first && isset( $first['destination'] ) ? (array) $first['destination'] : array(); } /** * The customer's current shipping address. * * @return array */ private static function customer_destination() { $c = WC()->customer; if ( ! $c ) { return array(); } return array( 'address_1' => $c->get_shipping_address_1(), 'city' => $c->get_shipping_city(), 'state' => $c->get_shipping_state(), 'postcode' => $c->get_shipping_postcode(), 'country' => $c->get_shipping_country(), ); } }