From 0bbac9d83114d2bc852812fdd06a06d6cb1a7ee3 Mon Sep 17 00:00:00 2001 From: vendidero Date: Wed, 27 Mar 2024 17:59:48 +0100 Subject: [PATCH] Improve checkout impl + UI. Added validation and data structure for pickup locations. --- .../slotfills/pickup-location-select.js | 37 ++-- .../slotfills/style.scss | 122 +----------- assets/js/packages/checkout/utils/index.js | 40 +++- src/Admin/Admin.php | 30 +++ src/Blocks/Checkout.php | 160 ++++++++++------ src/DataStores/Shipment.php | 2 + src/Interfaces/ShippingProviderAuto.php | 19 +- src/Order.php | 38 ++++ src/Shipment.php | 124 +++++++++--- src/ShippingMethod/ShippingMethod.php | 3 + src/ShippingProvider/Auto.php | 34 ++-- src/ShippingProvider/PickupLocation.php | 180 ++++++++++++++++++ src/SimpleShipment.php | 26 +-- 13 files changed, 555 insertions(+), 260 deletions(-) create mode 100644 src/ShippingProvider/PickupLocation.php diff --git a/assets/js/blocks/checkout-pickup-location-select/slotfills/pickup-location-select.js b/assets/js/blocks/checkout-pickup-location-select/slotfills/pickup-location-select.js index 8629dec3..9a180e2c 100644 --- a/assets/js/blocks/checkout-pickup-location-select/slotfills/pickup-location-select.js +++ b/assets/js/blocks/checkout-pickup-location-select/slotfills/pickup-location-select.js @@ -2,14 +2,8 @@ import { ExperimentalOrderShippingPackages } from '@woocommerce/blocks-checkout' import { registerPlugin } from '@wordpress/plugins'; import { useEffect, useCallback, useState, useMemo } from "@wordpress/element"; import { useSelect, useDispatch, select, dispatch } from '@wordpress/data'; -import triggerFetch from '@wordpress/api-fetch'; -import { extensionCartUpdate } from '@woocommerce/blocks-checkout'; -import classnames from 'classnames'; -import { getSetting } from '@woocommerce/settings'; import { __, _x, sprintf } from '@wordpress/i18n'; -import { SVG } from '@wordpress/components'; -import _ from 'lodash'; -import { CART_STORE_KEY, CHECKOUT_STORE_KEY, PAYMENT_STORE_KEY, VALIDATION_STORE_KEY } from '@woocommerce/block-data'; +import { CART_STORE_KEY, CHECKOUT_STORE_KEY } from '@woocommerce/block-data'; import { ValidatedTextInput, @@ -17,15 +11,17 @@ import { } from '@woocommerce/blocks-checkout'; import { decodeEntities } from '@wordpress/html-entities'; -import { useDebouncedCallback, useDebounce } from 'use-debounce'; -import { getSelectedShippingProviders, Combobox, hasShippingProvider } from '@woocommerceGzdShipments/blocks-checkout'; +import { getSelectedShippingProviders, Combobox, hasShippingProvider, getCheckoutData, hasPickupLocation } from '@woocommerceGzdShipments/blocks-checkout'; + +import './style.scss'; const PickupLocationSelect = ({ extensions, cart, components }) => { - const [ needsCustomerNumber, setNeedsCustomerNumber ] = useState( false ); + const [ supportsCustomerNumber, setSupportsCustomerNumber ] = useState( false ); + const [ customerNumberIsMandatory, setCustomerNumberIsMandatory ] = useState( false ); const { shippingRates, @@ -54,16 +50,7 @@ const PickupLocationSelect = ({ }; } ); - const { checkoutOptions } = useSelect( ( select ) => { - const store = select( CHECKOUT_STORE_KEY ); - - const extensionsData = store.getExtensionData(); - const shipmentsData = extensionsData.hasOwnProperty( 'woocommerce-gzd-shipments' ) ? extensionsData['woocommerce-gzd-shipments'] : { 'pickup_location': '', 'pickup_location_customer_number': '' }; - - return { - checkoutOptions: shipmentsData - }; - } ); + const checkoutOptions = getCheckoutData(); const availableLocations = useMemo( () => @@ -113,7 +100,8 @@ const PickupLocationSelect = ({ const currentLocation = getLocationByCode( checkoutOptions.pickup_location ); if ( currentLocation ) { - setNeedsCustomerNumber( () => { return currentLocation.needs_customer_number } ); + setSupportsCustomerNumber( () => { return currentLocation.supports_customer_number } ); + setCustomerNumberIsMandatory( () => { return currentLocation.customer_number_is_mandatory } ); const newShippingAddress = { ...shippingAddress }; @@ -156,8 +144,6 @@ const PickupLocationSelect = ({ pickupLocationDeliveryAvailable ] ); - console.log(checkoutOptions); - if ( needsShipping && pickupLocationDeliveryAvailable ) { return (
@@ -167,6 +153,7 @@ const PickupLocationSelect = ({ - { needsCustomerNumber && ( + { supportsCustomerNumber && ( { setOption( 'pickup_location_customer_number', newValue ); diff --git a/assets/js/blocks/checkout-pickup-location-select/slotfills/style.scss b/assets/js/blocks/checkout-pickup-location-select/slotfills/style.scss index de880d8e..90f91d75 100644 --- a/assets/js/blocks/checkout-pickup-location-select/slotfills/style.scss +++ b/assets/js/blocks/checkout-pickup-location-select/slotfills/style.scss @@ -1,123 +1,5 @@ -.wc-gzd-checkout-dhl { - .wc-gzd-checkout-dhl-title { - display: flex; - flex-wrap: wrap; - align-items: center; +.wc-gzd-shipments-pickup-location-delivery { + h4 { font-size: 1em; - - .dhl-icon { - width: 130px; - height: auto; - margin-left: auto; - } - } - - .wc-block-components-radio-control-accordion-option { - position: relative; - - .wc-block-components-radio-control__option-layout { - font-size: .875em; - } - - .wc-block-components-radio-control-accordion-content { - font-size: .875em; - } - - .wc-block-components-radio-control__option { - border-bottom: none; - padding-left: 56px; - padding-right: 16px; - margin: 0; - padding-bottom: 1em; - padding-top: 1em; - - &::after { - border-style: solid; - border-width: 1px 1px 0; - bottom: 0; - content: ""; - display: block; - left: 0; - opacity: .3; - pointer-events: none; - position: absolute; - right: 0; - top: 0; - } - - .wc-block-components-radio-control__input { - left: 16px; - } - - &::after { - border-width: 0; - } - } - - &::after { - border-style: solid; - border-width: 1px 1px 0; - bottom: 0; - content: ""; - display: block; - left: 0; - opacity: .3; - pointer-events: none; - position: absolute; - right: 0; - top: 0; - } - - &:last-child { - &::after { - border-width: 1px; - } - } - } - - .wc-gzd-dhl-preferred-day-select { - display: flex; - flex-direction: row; - flex-wrap: wrap; - margin: 0; - padding: 0; - - .wc-gzd-dhl-preferred-day { - color: inherit; - flex-basis: 10%; - flex-grow: 1; - text-align: center; - padding: 10px 0 0; - margin: 0 8px 8px 0; - background-color: #e3e3e3; - border: none; - - &:last-child { - margin-right: 0; - } - - .inner { - position: relative; - display: flex; - flex-direction: column; - flex-wrap: wrap; - padding: 5px 10px; - font-size: 1em; - background-color: #eef4f2; - cursor: pointer; - margin: 0; - line-height: 1.8em; - - .day { - font-size: 1.4em; - } - } - - &.active { - .inner { - background-color: #FFCC00; - } - } - } } } \ No newline at end of file diff --git a/assets/js/packages/checkout/utils/index.js b/assets/js/packages/checkout/utils/index.js index 2739cbad..7fe18d96 100644 --- a/assets/js/packages/checkout/utils/index.js +++ b/assets/js/packages/checkout/utils/index.js @@ -1,6 +1,19 @@ +import { CHECKOUT_STORE_KEY, CART_STORE_KEY } from '@woocommerce/block-data'; +import { useSelect, useDispatch, select, dispatch } from '@wordpress/data'; +import {useCallback} from "@wordpress/element"; + export const getSelectedShippingProviders = ( - shippingRates + shippingRates = null ) => { + if ( null === shippingRates ) { + shippingRates = useSelect( ( select ) => { + const isEditor = !! select( 'core/editor' ); + const store = select( CART_STORE_KEY ); + + return isEditor ? [] : store.getShippingRates(); + } ); + } + return Object.fromEntries( shippingRates.map( ( { package_id: packageId, shipping_rates: packageRates } ) => { const meta_data = packageRates.find( ( rate ) => rate.selected )?.meta_data || []; let provider = ''; @@ -18,6 +31,29 @@ export const getSelectedShippingProviders = ( } ) ); }; -export const hasShippingProvider = ( shippingProviders, shippingProvider ) => { +export const hasShippingProvider = ( shippingProvider, shippingProviders = null ) => { + shippingProviders = null === shippingProviders ? getSelectedShippingProviders() : shippingProviders; + return Object.values( shippingProviders ).includes( shippingProvider ); +}; + +export const hasPickupLocation = () => { + const checkoutData = getCheckoutData(); + + return !!checkoutData.pickup_location; +}; + +export const getCheckoutData = () => { + const { checkoutOptions } = useSelect( ( select ) => { + const store = select( CHECKOUT_STORE_KEY ); + + const extensionsData = store.getExtensionData(); + const shipmentsData = extensionsData.hasOwnProperty( 'woocommerce-gzd-shipments' ) ? extensionsData['woocommerce-gzd-shipments'] : { 'pickup_location': '', 'pickup_location_customer_number': '' }; + + return { + checkoutOptions: shipmentsData + }; + } ); + + return checkoutOptions; }; \ No newline at end of file diff --git a/src/Admin/Admin.php b/src/Admin/Admin.php index 2d25519f..24452ff9 100644 --- a/src/Admin/Admin.php +++ b/src/Admin/Admin.php @@ -100,6 +100,36 @@ function() { add_action( 'woocommerce_admin_field_dimensions', array( __CLASS__, 'register_dimensions_field' ), 30 ); add_filter( 'woocommerce_admin_settings_sanitize_option', array( __CLASS__, 'sanitize_dimensions_field' ), 10, 3 ); + + add_filter( 'woocommerce_admin_shipping_fields', array( __CLASS__, 'register_pickup_location_admin_fields' ), 10, 3 ); + } + + public static function register_pickup_location_admin_fields( $fields, $order = null, $context = 'edit' ) { + if ( ! $order instanceof \WC_Order ) { + return $fields; + } + + $shipment_order = wc_gzd_get_shipment_order( $order ); + + if ( $shipment_order->supports_pickup_location() ) { + $fields['pickup_location_code'] = array( + 'label' => _x( 'Pickup location', 'shipments', 'woocommerce-germanized-shipments' ), + 'type' => 'text', + 'id' => '_pickup_location_code', + 'show' => false, + 'value' => $shipment_order->get_pickup_location_code(), + ); + + $fields['pickup_location_customer_number'] = array( + 'label' => _x( 'Pickup customer number', 'shipments', 'woocommerce-germanized-shipments' ), + 'show' => false, + 'id' => '_pickup_location_customer_number', + 'type' => 'text', + 'value' => $shipment_order->get_pickup_location_customer_number(), + ); + } + + return $fields; } public static function sanitize_toggle_field( $value, $option, $raw_value ) { diff --git a/src/Blocks/Checkout.php b/src/Blocks/Checkout.php index 2add381c..bc380573 100644 --- a/src/Blocks/Checkout.php +++ b/src/Blocks/Checkout.php @@ -5,6 +5,7 @@ use Automattic\WooCommerce\StoreApi\Schemas\ExtendSchema; use Automattic\WooCommerce\StoreApi\Schemas\V1\CartSchema; use Automattic\WooCommerce\StoreApi\Schemas\V1\CheckoutSchema; +use Automattic\WooCommerce\StoreApi\Utilities\CartController; use Vendidero\Germanized\Shipments\Package; final class Checkout { @@ -47,6 +48,8 @@ private function get_checkout_data_from_request( $request ) { ) ); + $data['pickup_location_customer_number'] = trim( preg_replace( '/\s+/', '', $data['pickup_location_customer_number'] ) ); + return $data; } @@ -62,7 +65,8 @@ private function validate_checkout_data( $order, $request ) { if ( $this->has_checkout_data( 'pickup_location', $request ) ) { $pickup_location_code = $gzd_data['pickup_location']; $pickup_location_customer_number = $gzd_data['pickup_location_customer_number']; - $needs_customer_number = false; + $supports_customer_number = false; + $customer_number_is_mandatory = false; $is_valid = false; $pickup_location = false; $address_data = array( @@ -74,32 +78,32 @@ private function validate_checkout_data( $order, $request ) { if ( $provider = wc_gzd_get_order_shipping_provider( $order ) ) { if ( is_a( $provider, 'Vendidero\Germanized\Shipments\Interfaces\ShippingProviderAuto' ) ) { - $pickup_location = $provider->get_pickup_location_by_code( $pickup_location_code, $address_data ); - $is_valid = $provider->is_valid_pickup_location( $pickup_location_code, $address_data ); - $needs_customer_number = $provider->pickup_location_needs_customer_number( $pickup_location_code, $address_data ); + $pickup_location = $provider->get_pickup_location_by_code( $pickup_location_code, $address_data ); + $is_valid = $provider->is_valid_pickup_location( $pickup_location_code, $address_data ); + $supports_customer_number = $pickup_location ? $pickup_location->supports_customer_number() : false; + $customer_number_is_mandatory = $pickup_location ? $pickup_location->customer_number_is_mandatory() : false; - if ( $is_valid && $needs_customer_number ) { - if ( ! $provider->is_valid_pickup_location_customer_number( $pickup_location_customer_number ) ) { + if ( $is_valid && $customer_number_is_mandatory ) { + if ( ! $pickup_location ) { throw new \Automattic\WooCommerce\StoreApi\Exceptions\RouteException( 'pickup_location_customer_number_invalid', _x( 'Sorry, your pickup location customer number is invalid.', 'shipments', 'woocommerce-germanized-shipments' ), 400 ); + } elseif ( ! $validation = $pickup_location->customer_number_is_valid( $pickup_location_customer_number ) ) { + if ( is_a( $validation, 'WP_Error' ) ) { + throw new \Automattic\WooCommerce\StoreApi\Exceptions\RouteException( 'pickup_location_customer_number_invalid', $validation->get_error_message(), 400 ); + } else { + throw new \Automattic\WooCommerce\StoreApi\Exceptions\RouteException( 'pickup_location_customer_number_invalid', _x( 'Sorry, your pickup location customer number is invalid.', 'shipments', 'woocommerce-germanized-shipments' ), 400 ); + } } } } } if ( $is_valid && $pickup_location ) { - $pickup_location_code = $pickup_location['code']; - - foreach ( $pickup_location['address_replacements'] as $address_field => $replacement ) { - $setter = "set_shipping_{$address_field}"; - - if ( is_callable( array( $order, $setter ) ) ) { - $order->{ $setter }( $replacement ); - } - } + $pickup_location_code = $pickup_location->get_code(); + $pickup_location->replace_address( $order ); $order->update_meta_data( '_pickup_location_code', $pickup_location_code ); - if ( $needs_customer_number ) { + if ( $supports_customer_number ) { $order->update_meta_data( '_pickup_location_customer_number', $pickup_location_customer_number ); } @@ -110,16 +114,9 @@ private function validate_checkout_data( $order, $request ) { $wc_customer = new \WC_Customer( $order->get_customer_id() ); $wc_customer->update_meta_data( 'pickup_location_code', $pickup_location_code ); + $pickup_location->replace_address( $wc_customer ); - foreach ( $pickup_location['address_replacements'] as $address_field => $replacement ) { - $setter = "set_shipping_{$address_field}"; - - if ( is_callable( array( $wc_customer, $setter ) ) ) { - $wc_customer->{ $setter }( $replacement ); - } - } - - if ( $needs_customer_number ) { + if ( $supports_customer_number ) { $wc_customer->update_meta_data( 'pickup_location_customer_number', $pickup_location_customer_number ); } @@ -128,16 +125,9 @@ private function validate_checkout_data( $order, $request ) { $customer = wc()->customer; $customer->update_meta_data( 'pickup_location_code', $pickup_location_code ); + $pickup_location->replace_address( $customer ); - foreach ( $pickup_location['address_replacements'] as $address_field => $replacement ) { - $setter = "set_shipping_{$address_field}"; - - if ( is_callable( array( $customer, $setter ) ) ) { - $customer->{ $setter }( $replacement ); - } - } - - if ( $needs_customer_number ) { + if ( $supports_customer_number ) { $customer->update_meta_data( 'pickup_location_customer_number', $pickup_location_customer_number ); } $customer->save(); @@ -221,55 +211,57 @@ private function get_cart_schema() { 'items' => array( 'type' => 'object', 'properties' => array( - 'code' => array( + 'code' => array( 'description' => _x( 'The location code.', 'shipments', 'woocommerce-germanized-shipments' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), - 'title' => array( - 'description' => _x( 'The location title.', 'shipments', 'woocommerce-germanized-shipments' ), + 'label' => array( + 'description' => _x( 'The location label.', 'shipments', 'woocommerce-germanized-shipments' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), - 'lat' => array( + 'latitude' => array( 'description' => _x( 'The location latitude.', 'shipments', 'woocommerce-germanized-shipments' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), - 'long' => array( + 'longitude' => array( 'description' => _x( 'The location longitude.', 'shipments', 'woocommerce-germanized-shipments' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), - 'needs_customer_number' => array( - 'description' => _x( 'Whether the location needs a customer number or not.', 'shipments', 'woocommerce-germanized-shipments' ), + 'supports_customer_number' => array( + 'description' => _x( 'Whether the location supports a customer number or not.', 'shipments', 'woocommerce-germanized-shipments' ), + 'type' => 'boolean', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + 'default' => false, + ), + 'customer_number_is_mandatory' => array( + 'description' => _x( 'Whether the customer number is mandatory or not.', 'shipments', 'woocommerce-germanized-shipments' ), 'type' => 'boolean', 'context' => array( 'view', 'edit' ), 'readonly' => true, 'default' => false, ), - 'type' => array( + 'type' => array( 'description' => _x( 'The location type, e.g. locker.', 'shipments', 'woocommerce-germanized-shipments' ), - 'type' => 'enum', - 'enum' => array( - 'locker', - 'shop', - 'servicepoint', - ), + 'type' => 'string', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), - 'formatted_address' => array( + 'formatted_address' => array( 'description' => _x( 'The location\'s formatted address.', 'shipments', 'woocommerce-germanized-shipments' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), - 'address_replacements' => array( + 'address_replacements' => array( 'description' => _x( 'The location\'s address replacements.', 'shipments', 'woocommerce-germanized-shipments' ), 'type' => 'array', 'context' => array( 'view', 'edit' ), @@ -319,12 +311,13 @@ private function get_cart_schema() { } private function get_cart_data() { - $customer = wc()->customer; - $provider = false; - $is_available = false; - $locations = array(); + $customer = wc()->customer; + $provider = false; + $is_available = false; + $locations = array(); + $shipping_method = wc_gzd_get_current_shipping_provider_method(); - if ( $shipping_method = wc_gzd_get_current_shipping_provider_method() ) { + if ( $shipping_method ) { $provider = $shipping_method->get_shipping_provider_instance(); } @@ -336,13 +329,64 @@ private function get_cart_data() { 'city' => $customer->get_shipping_city(), ); - $locations = $provider->get_pickup_locations( $address ); - $is_available = $provider->supports_pickup_location_delivery( $address ); + $locations = $provider->get_pickup_locations( $address ); + $max_weight = 0; + $max_dimensions = array( + 'length' => 0.0, + 'width' => 0.0, + 'height' => 0.0, + ); + + if ( ! empty( $locations ) && $shipping_method && is_a( $shipping_method->get_method(), 'Vendidero\Germanized\Shipments\ShippingMethod\ShippingMethod' ) ) { + $controller = new CartController(); + $cart = wc()->cart; + $has_calculated_shipping = $cart->show_shipping(); + $shipping_packages = $has_calculated_shipping ? $controller->get_shipping_packages() : array(); + $current_rate_id = wc_gzd_get_current_shipping_method_id(); + + if ( isset( $shipping_packages[0]['rates'][ $current_rate_id ] ) ) { + $rate = $shipping_packages[0]['rates'][ $current_rate_id ]; + + if ( is_a( $rate, 'WC_Shipping_Rate' ) ) { + $meta = $rate->get_meta_data(); + + if ( isset( $meta['_packages'] ) ) { + foreach ( (array) $meta['_packages'] as $package_data ) { + $packaging_id = $package_data['packaging_id']; + + if ( $packaging = wc_gzd_get_packaging( $packaging_id ) ) { + $package_weight = (float) wc_get_weight( $package_data['weight'], wc_gzd_get_packaging_weight_unit(), 'g' ); + + if ( (float) $packaging->get_length() > $max_dimensions['length'] ) { + $max_dimensions['length'] = (float) $packaging->get_length(); + } + if ( (float) $packaging->get_width() > $max_dimensions['width'] ) { + $max_dimensions['width'] = (float) $packaging->get_width(); + } + if ( (float) $packaging->get_height() > $max_dimensions['height'] ) { + $max_dimensions['height'] = (float) $packaging->get_height(); + } + + if ( $package_weight > $max_weight ) { + $max_weight = $package_weight; + } + } + } + } + } + } + } + + $is_available = $provider->supports_pickup_location_delivery( $address, $max_dimensions, $max_weight ); } return array( 'pickup_location_delivery_available' => $is_available && ! empty( $locations ), - 'pickup_locations' => $locations, + 'pickup_locations' => array_map( + function( $location ) { + return $location->get_data(); }, + $locations + ), ); } } diff --git a/src/DataStores/Shipment.php b/src/DataStores/Shipment.php index e30a9d57..28c01557 100644 --- a/src/DataStores/Shipment.php +++ b/src/DataStores/Shipment.php @@ -47,6 +47,8 @@ class Shipment extends WC_Data_Store_WP implements WC_Object_Data_Store_Interfac '_weight_unit', '_dimension_unit', '_is_customer_requested', + '_pickup_location_code', + '_pickup_location_customer_number', ); protected $core_props = array( diff --git a/src/Interfaces/ShippingProviderAuto.php b/src/Interfaces/ShippingProviderAuto.php index b187b6b0..4f36cadd 100644 --- a/src/Interfaces/ShippingProviderAuto.php +++ b/src/Interfaces/ShippingProviderAuto.php @@ -2,6 +2,7 @@ namespace Vendidero\Germanized\Shipments\Interfaces; use Vendidero\Germanized\Shipments\Labels\ConfigurationSet; +use Vendidero\Germanized\Shipments\ShippingProvider\PickupLocation; /** * Shipment Label Interface @@ -47,15 +48,23 @@ public function is_sandbox(); public function get_settings_help_pointers( $section = '' ); - public function supports_pickup_location_delivery( $address, $max_dimensions = array() ); + public function supports_pickup_location_delivery( $address, $max_dimensions = array(), $max_weight = 0.0 ); public function is_valid_pickup_location( $location_code, $address ); - public function is_valid_pickup_location_customer_number( $number ); - - public function pickup_location_needs_customer_number( $location_code, $address ); - + /** + * @param $location_code + * @param $address + * + * @return PickupLocation|false + */ public function get_pickup_location_by_code( $location_code, $address ); + /** + * @param $address + * @param $limit + * + * @return PickupLocation[] + */ public function get_pickup_locations( $address, $limit = 10 ); } diff --git a/src/Order.php b/src/Order.php index b0681848..94678f70 100644 --- a/src/Order.php +++ b/src/Order.php @@ -1124,6 +1124,44 @@ public function get_returnable_item_count() { return apply_filters( 'woocommerce_gzd_shipment_order_returnable_item_count', $count, $this ); } + public function supports_pickup_location() { + $supports_pickup_location = false; + + if ( $provider = wc_gzd_get_order_shipping_provider( $this ) ) { + if ( is_a( $provider, 'Vendidero\Germanized\Shipments\Interfaces\ShippingProviderAuto' ) ) { + $supports_pickup_location = $provider->supports_pickup_location_delivery( $this->get_order()->get_address( 'shipping' ) ); + } + } + + if ( $this->has_pickup_location() ) { + $supports_pickup_location = true; + } + + return apply_filters( 'woocommerce_gzd_shipment_order_supports_pickup_location', $supports_pickup_location, $this->get_order(), $this ); + } + + public function has_pickup_location() { + $pickup_location_code = $this->get_pickup_location_code(); + + return apply_filters( 'woocommerce_gzd_shipment_order_has_pickup_location', ! empty( $pickup_location_code ), $this->get_order(), $this ); + } + + public function get_pickup_location_customer_number() { + $customer_number = ''; + + if ( $this->has_pickup_location() ) { + $customer_number = $this->get_order()->get_meta( '_pickup_location_customer_number', true ); + } + + return apply_filters( 'woocommerce_gzd_shipment_order_pickup_location_customer_number', $customer_number, $this->get_order(), $this ); + } + + public function get_pickup_location_code() { + $pickup_location_code = $this->get_order()->get_meta( '_pickup_location_code', true ); + + return apply_filters( 'woocommerce_gzd_shipment_order_pickup_location_code', $pickup_location_code, $this->get_order(), $this ); + } + protected function has_local_pickup() { $shipping_methods = $this->get_order()->get_shipping_methods(); $has_pickup = false; diff --git a/src/Shipment.php b/src/Shipment.php index b97fa422..320c0772 100644 --- a/src/Shipment.php +++ b/src/Shipment.php @@ -13,6 +13,7 @@ use Vendidero\Germanized\Shipments\Interfaces\ShipmentReturnLabel; use Vendidero\Germanized\Shipments\Labels\Label; use Vendidero\Germanized\Shipments\ShippingMethod\ProviderMethod; +use Vendidero\Germanized\Shipments\ShippingProvider\PickupLocation; use WC_Data; use WC_Data_Store; use Exception; @@ -83,6 +84,8 @@ abstract class Shipment extends WC_Data { protected $items_to_pack = null; + protected $pickup_location = null; + /** * Item weights. * @@ -136,27 +139,29 @@ abstract class Shipment extends WC_Data { * @var array */ protected $data = array( - 'date_created' => null, - 'date_sent' => null, - 'status' => '', - 'weight' => '', - 'width' => '', - 'height' => '', - 'length' => '', - 'packaging_weight' => '', - 'weight_unit' => '', - 'dimension_unit' => '', - 'country' => '', - 'address' => array(), - 'tracking_id' => '', - 'shipping_provider' => '', - 'shipping_method' => '', - 'total' => 0, - 'subtotal' => 0, - 'additional_total' => 0, - 'est_delivery_date' => null, - 'packaging_id' => 0, - 'version' => '', + 'date_created' => null, + 'date_sent' => null, + 'status' => '', + 'weight' => '', + 'width' => '', + 'height' => '', + 'length' => '', + 'packaging_weight' => '', + 'weight_unit' => '', + 'dimension_unit' => '', + 'country' => '', + 'address' => array(), + 'tracking_id' => '', + 'shipping_provider' => '', + 'shipping_method' => '', + 'pickup_location_code' => '', + 'pickup_location_customer_number' => '', + 'total' => 0, + 'subtotal' => 0, + 'additional_total' => 0, + 'est_delivery_date' => null, + 'packaging_id' => 0, + 'version' => '', ); /** @@ -864,6 +869,51 @@ public function get_tracking_id( $context = 'view' ) { return $this->get_prop( 'tracking_id', $context ); } + /** + * Retrieves the pickup location code, in case existent. + * + * @param string $context What the value is for. Valid values are 'view' and 'edit'. + * @return string + */ + public function get_pickup_location_code( $context = 'view' ) { + return $this->get_prop( 'pickup_location_code', $context ); + } + + /** + * Retrieves the pickup location customer number, in case existent. + * + * @param string $context What the value is for. Valid values are 'view' and 'edit'. + * @return string + */ + public function get_pickup_location_customer_number( $context = 'view' ) { + return $this->get_prop( 'pickup_location_customer_number', $context ); + } + + public function has_pickup_location() { + $code = $this->get_pickup_location_code(); + + return apply_filters( "{$this->get_general_hook_prefix()}has_pickup_location", ! empty( $code ), $this ); + } + + /** + * @return false|PickupLocation + */ + public function get_pickup_location() { + if ( is_null( $this->pickup_location ) ) { + $this->pickup_location = false; + + if ( $this->has_pickup_location() ) { + if ( $provider = $this->get_shipping_provider_instance() ) { + if ( is_a( $provider, 'Vendidero\Germanized\Shipments\Interfaces\ShippingProviderAuto' ) ) { + $this->pickup_location = $provider->get_pickup_location_by_code( $this->get_pickup_location_code(), $this->get_address() ); + } + } + } + } + + return $this->pickup_location; + } + /** * Returns the shipment tracking URL. * @@ -1526,7 +1576,16 @@ public function get_est_delivery_date( $context = 'view' ) { * @return boolean */ public function send_to_external_pickup( $types = array() ) { - $types = is_array( $types ) ? $types : array( $types ); + $types = is_array( $types ) ? $types : array( $types ); + $has_pickup = $this->has_pickup_location(); + + if ( ! empty( $types ) ) { + $has_pickup = false; + + if ( $location = $this->get_pickup_location() ) { + $has_pickup = in_array( $location->get_type(), $types, true ); + } + } /** * Filter to decide whether a Shipment is to be sent to a external pickup location @@ -1539,7 +1598,7 @@ public function send_to_external_pickup( $types = array() ) { * @since 3.0.0 * @package Vendidero/Germanized/Shipments */ - return apply_filters( 'woocommerce_gzd_shipment_send_to_external_pickup', false, $types, $this ); + return apply_filters( 'woocommerce_gzd_shipment_send_to_external_pickup', $has_pickup, $types, $this ); } /** @@ -1945,6 +2004,25 @@ public function set_tracking_id( $tracking_id ) { $this->set_prop( 'tracking_id', $tracking_id ); } + /** + * Set shipment pickup location code. + * + * @param string $location_code The location code. + */ + public function set_pickup_location_code( $location_code ) { + $this->set_prop( 'pickup_location_code', $location_code ); + $this->pickup_location = null; + } + + /** + * Set shipment pickup location customer number. + * + * @param string $customer_number The customer number. + */ + public function set_pickup_location_customer_number( $customer_number ) { + $this->set_prop( 'pickup_location_customer_number', $customer_number ); + } + /** * Set shipment shipping provider. * diff --git a/src/ShippingMethod/ShippingMethod.php b/src/ShippingMethod/ShippingMethod.php index 269b4625..9a195f74 100644 --- a/src/ShippingMethod/ShippingMethod.php +++ b/src/ShippingMethod/ShippingMethod.php @@ -681,11 +681,13 @@ public function calculate_shipping( $package = array() ) { * included within the package. */ $item_map = array(); + $weight = 0.0; foreach ( $items as $item ) { $cart_item_wrapper = $item->getItem(); $product = $cart_item_wrapper->get_product(); $product_key = $product->get_parent_id() . '_' . $product->get_id(); + $weight += $cart_item_wrapper->getWeight(); if ( array_key_exists( $product_key, $item_map ) ) { $item_map[ $product_key ]++; @@ -706,6 +708,7 @@ public function calculate_shipping( $package = array() ) { 'packaging_id' => $packaging->get_id(), 'rules' => $package_applied_rules, 'items' => $item_map, + 'weight' => $weight + $packaging->getEmptyWeight(), ); $rule_ids = array_unique( array_merge( $rule_ids, $package_applied_rules ) ); diff --git a/src/ShippingProvider/Auto.php b/src/ShippingProvider/Auto.php index c1833b93..6bd40061 100644 --- a/src/ShippingProvider/Auto.php +++ b/src/ShippingProvider/Auto.php @@ -348,19 +348,33 @@ public function get_settings_help_pointers( $section = '' ) { return array(); } - public function supports_pickup_location_delivery( $address, $max_dimensions = array() ) { + public function supports_pickup_location_delivery( $address, $max_dimensions = array(), $max_weight = 0.0 ) { return false; } + /** + * @param $location_code + * @param $address + * + * @return PickupLocation|null + */ protected function fetch_pickup_location( $location_code, $address ) { return null; } + /** + * @param $location_code + * @param $address + * + * @return false|PickupLocation + */ public function get_pickup_location_by_code( $location_code, $address ) { $address = $this->parse_pickup_location_address_args( $address ); $cache_key = $this->get_pickup_location_cache_key( $location_code, $address ); $pickup_location = get_transient( $cache_key ); + $pickup_location = false; + if ( false === $pickup_location ) { $pickup_location = $this->fetch_pickup_location( $location_code, $address ); @@ -369,6 +383,8 @@ public function get_pickup_location_by_code( $location_code, $address ) { } else { $pickup_location = false; } + } elseif ( ! is_a( $pickup_location, 'Vendidero\Germanized\Shipments\ShippingProvider\PickupLocation' ) ) { + $pickup_location = false; } return $pickup_location; @@ -393,20 +409,6 @@ protected function get_pickup_locations_cache_key( $address ) { return $cache_key; } - public function is_valid_pickup_location_customer_number( $number ) { - return true; - } - - public function pickup_location_needs_customer_number( $location_code, $address ) { - $needs_customer_number = false; - - if ( $pickup_location = $this->get_pickup_location_by_code( $location_code, $address ) ) { - $needs_customer_number = $pickup_location['needs_customer_number']; - } - - return $needs_customer_number; - } - public function is_valid_pickup_location( $location_code, $address ) { if ( $this->get_pickup_location_by_code( $location_code, $address ) ) { return true; @@ -436,6 +438,8 @@ public function get_pickup_locations( $address, $limit = 10 ) { $pickup_locations = get_transient( $cache_key ); $address = $this->parse_pickup_location_address_args( $address ); + $pickup_locations = false; + if ( false === $pickup_locations ) { $pickup_locations = $this->fetch_pickup_locations( $address, $limit ); diff --git a/src/ShippingProvider/PickupLocation.php b/src/ShippingProvider/PickupLocation.php new file mode 100644 index 00000000..a1562164 --- /dev/null +++ b/src/ShippingProvider/PickupLocation.php @@ -0,0 +1,180 @@ + '', + 'type' => '', + 'label' => '', + 'latitude' => '', + 'longitude' => '', + 'supports_customer_number' => false, + 'customer_number_is_mandatory' => false, + 'customer_number_validation_cb' => null, + 'address' => array(), + 'address_replacement_map' => array(), + ) + ); + + if ( empty( $args['code'] ) ) { + $args['code'] = sanitize_key( $args['label'] ); + } + + if ( empty( $args['code'] ) ) { + throw new \Exception( 'A pickup location needs a code.', 500 ); + } + + $this->code = $args['code']; + $this->type = $args['type']; + $this->label = $args['label']; + $this->latitude = $args['latitude']; + $this->longitude = $args['longitude']; + $this->supports_customer_number = wc_string_to_bool( $args['supports_customer_number'] ); + $this->customer_number_is_mandatory = wc_string_to_bool( $args['customer_number_is_mandatory'] ); + $this->customer_number_validation_cb = $args['customer_number_validation_cb']; + $this->address = wp_parse_args( + (array) $args['address'], + array( + 'address_1', + 'city', + 'postcode', + 'country', + ) + ); + + if ( $this->customer_number_validation_cb instanceof \Closure ) { + throw new \Exception( 'Closures as callbacks for customer number validation are not supported', 500 ); + } + + $this->address_replacement_map = (array) $args['address_replacement_map']; + } + + public function get_code() { + return $this->code; + } + + public function get_label() { + return $this->label; + } + + public function get_type() { + return $this->type; + } + + public function get_address() { + return $this->address; + } + + public function supports_customer_number() { + return $this->supports_customer_number; + } + + public function customer_number_is_mandatory() { + return $this->customer_number_is_mandatory; + } + + /** + * @param $customer_number + * + * @return bool|\WP_Error + */ + public function customer_number_is_valid( $customer_number ) { + $is_valid = $this->customer_number_is_mandatory() ? ! empty( $customer_number ) : true; + + if ( null !== $this->customer_number_validation_cb ) { + $is_valid = call_user_func_array( $this->customer_number_validation_cb, array( $customer_number, $this ) ); + } + + return $is_valid; + } + + public function get_latitude() { + return $this->latitude; + } + + public function get_longitude() { + return $this->longitude; + } + + public function get_formatted_address( $separator = ', ' ) { + $address = $this->get_address(); + + if ( empty( $address['company'] ) ) { + $address['company'] = $this->get_label(); + } + + return WC()->countries->get_formatted_address( $address, $separator ); + } + + public function get_address_replacement_map() { + return $this->address_replacement_map; + } + + public function get_address_replacements() { + $replacements = array(); + $location_address = $this->get_address(); + $location_address['label'] = $this->get_label(); + $location_address['code'] = $this->get_code(); + + foreach ( $this->get_address_replacement_map() as $address_key => $location_address_key ) { + if ( isset( $location_address[ $location_address_key ] ) ) { + $replacements[ $address_key ] = $location_address[ $location_address_key ]; + } + } + + return $replacements; + } + + public function replace_address( $object ) { + foreach ( $this->get_address_replacements() as $address_field => $address_value ) { + $setter = "set_shipping_{$address_field}"; + + if ( is_callable( array( $object, $setter ) ) ) { + $object->{$setter}( $address_value ); + } + } + } + + public function get_data() { + return array( + 'code' => $this->get_code(), + 'type' => $this->get_type(), + 'label' => $this->get_label(), + 'latitude' => $this->get_latitude(), + 'longitude' => $this->get_longitude(), + 'supports_customer_number' => $this->supports_customer_number(), + 'customer_number_is_mandatory' => $this->customer_number_is_mandatory(), + 'address' => $this->get_address(), + 'address_replacement_map' => $this->get_address_replacement_map(), + 'address_replacements' => $this->get_address_replacements(), + 'formatted_address' => $this->get_formatted_address(), + ); + } +} diff --git a/src/SimpleShipment.php b/src/SimpleShipment.php index e83d05ea..cf8c8a52 100644 --- a/src/SimpleShipment.php +++ b/src/SimpleShipment.php @@ -173,18 +173,20 @@ public function sync( $args = array() ) { $args = wp_parse_args( $args, array( - 'order_id' => $order->get_id(), - 'shipping_method' => wc_gzd_get_shipment_order_shipping_method_id( $order ), - 'shipping_provider' => ( ! empty( $provider ) ) ? $provider : $default_provider, - 'packaging_id' => $this->get_packaging_id( 'edit' ), - 'address' => $address_data, - 'country' => $country, - 'weight' => $this->get_weight( 'edit' ), - 'packaging_weight' => $this->get_packaging_weight( 'edit' ), - 'length' => $dimensions['length'], - 'width' => $dimensions['width'], - 'height' => $dimensions['height'], - 'additional_total' => $order_shipment->calculate_shipment_additional_total( $this ), + 'order_id' => $order->get_id(), + 'shipping_method' => wc_gzd_get_shipment_order_shipping_method_id( $order ), + 'shipping_provider' => ( ! empty( $provider ) ) ? $provider : $default_provider, + 'packaging_id' => $this->get_packaging_id( 'edit' ), + 'address' => $address_data, + 'country' => $country, + 'weight' => $this->get_weight( 'edit' ), + 'packaging_weight' => $this->get_packaging_weight( 'edit' ), + 'pickup_location_code' => $order_shipment->has_pickup_location() ? $order_shipment->get_pickup_location_code() : '', + 'pickup_location_customer_number' => $order_shipment->has_pickup_location() ? $order_shipment->get_pickup_location_customer_number() : '', + 'length' => $dimensions['length'], + 'width' => $dimensions['width'], + 'height' => $dimensions['height'], + 'additional_total' => $order_shipment->calculate_shipment_additional_total( $this ), ) );