if ( 'yes' !== $shipping_method['enabled'] ) { continue; } try { if ( 'free_shipping' === $shipping_method['id'] ) { $shipping_method_data[] = self::get_free_shipping_method_data( $zone, $shipping_method ); } if ( 'flat_rate' === $shipping_method['id'] ) { $shipping_method_data[] = self::get_flat_rate_shipping_method_data( $zone, $shipping_method ); } } catch ( \Exception $e ) { \WC_Facebookcommerce_Utils::log_exception_immediately_to_meta( $e, array( 'event' => FeedUploadUtils::SHIPPING_PROFILES_SYNC_LOGGING_FLOW_NAME, 'event_type' => 'get_shipping_method_data', ) ); } } $shipping_rates_per_shipping_class = []; foreach ( $shipping_method_data as $method_datum ) { if ( null === $method_datum ) { continue; } $shipping_rate = array( 'name' => $method_datum['name'], 'has_free_shipping' => $method_datum['has_free_shipping'], ); if ( key_exists( 'cart_minimum_for_free_shipping', $method_datum ) ) { $shipping_rate['cart_minimum_for_free_shipping'] = $method_datum['cart_minimum_for_free_shipping']; } if ( $method_datum['applies_to_all_products'] ) { $shipping_rates_per_shipping_class['all_products'][] = $shipping_rate; } else { foreach ( $method_datum['shipping_class_ids'] as $class_id ) { $shipping_rates_per_shipping_class[ $class_id ][] = $shipping_rate; } } } foreach ( $shipping_rates_per_shipping_class as $shipping_class => $shipping_rates ) { // Don't send shipping profile if there are no rates since the shipping profile won't be usable. if ( 0 === count( $shipping_rates ) ) { continue; } $shipping_class_string = (string) $shipping_class; $data = array( 'shipping_profile_id' => sprintf( '%s-%s', $zone['id'], $shipping_class_string ), 'name' => $zone['zone_name'], 'shipping_zones' => $countries_with_states, 'shipping_rates' => $shipping_rates, 'applies_to_rest_of_world' => 'false', ); if ( 'all_products' === $shipping_class ) { $data['applies_to_all_products'] = 'true'; } else { $data['applies_to_all_products'] = 'false'; $data['applicable_products_filter'] = sprintf( '{"tags":{"eq":"%s"}}', self::get_shipping_class_tag_for_class( $shipping_class_string ) ); } $shipping_profiles_data[] = $data; } } return $shipping_profiles_data; } catch ( \Exception $e ) { \WC_Facebookcommerce_Utils::log_exception_immediately_to_meta( $e, array( 'event' => FeedUploadUtils::SHIPPING_PROFILES_SYNC_LOGGING_FLOW_NAME, 'event_type' => 'get_shipping_profiles_data', ) ); throw $e; } } private static function get_free_shipping_method_data( array $zone, array $free_shipping_method ): ?array { $shipping_settings = $free_shipping_method['instance_settings']; $shipping_data = array( 'name' => $free_shipping_method['title'], 'has_free_shipping' => 'true', 'applies_to_all_products' => true, 'shipping_class_ids' => [], ); // Today free shipping via coupons is displayed solely through the discounts data model. This does not // need to be synced to Meta here, as display details will need to be synced through coupon sync. $requires_coupon = ( 'both' === $shipping_settings['requires'] ) || ( 'coupon' === $shipping_settings['requires'] ); if ( $requires_coupon ) { self::log_map_shipping_method_issue_to_meta( $zone, $free_shipping_method, 'Free shipping requires coupon', 'map_free_shipping_method' ); return null; } // Since we aren't syncing coupon based shipping profiles here, we just treat 'either' as a requirement for min_amount. $requires_min_spend = ( 'min_amount' === $shipping_settings['requires'] ) || ( 'either' === $shipping_settings['requires'] ); if ( $requires_min_spend ) { // Minimum spend requirements on Facebook and Instagram are determined by post-discount subtotals. // Don't sync rate if using pre-discount amounts if ( 'yes' === $shipping_settings['ignore_discounts'] ) { self::log_map_shipping_method_issue_to_meta( $zone, $free_shipping_method, 'Min spend free shipping ignores discounts', 'map_free_shipping_method' ); return null; } $min_spend = $free_shipping_method['instance_settings']['min_amount'] ?? 0; $shipping_data['cart_minimum_for_free_shipping'] = $min_spend . ' ' . get_woocommerce_currency(); } return $shipping_data; } /** * Flat rate shipping can still be configured to be free for all or a subset of products based on shipping classes. * TODO - Currently syncs only if free for all products, need to extract free and non-free products based on shipping class. * * @param array $zone * @param array $flat_rate_method * @return array|null */ private static function get_flat_rate_shipping_method_data( array $zone, array $flat_rate_method ): ?array { $shipping_settings = $flat_rate_method['instance_settings']; // If the base cost isn't free we don't need to bother syncing this shipping method. if ( ! self::is_zero_cost( $shipping_settings['cost'] ?? '0' ) ) { self::log_map_shipping_method_issue_to_meta( $zone, $flat_rate_method, 'Flat rate shipping has base cost', 'map_flat_rate_shipping_method' ); return null; } // For each shipping class, a new key is inserted into the methods settings with form 'class_cost_{class_id}' // The value is the additional cost to ship for products of that class when using the shipping method. If // // some classes have a cost, data will be synced as a separate shipping profile for any class that has a 0 cost. $shipping_class_ids_to_costs = []; $class_cost_prefix = 'class_cost_'; $prefix_length = strlen( $class_cost_prefix ); foreach ( $shipping_settings as $key => $value ) { if ( str_starts_with( $key, $class_cost_prefix ) ) { $shipping_class_id = substr( $key, $prefix_length ); $shipping_class_ids_to_costs[ $shipping_class_id ] = $value; } } $shipping_class_ids_to_costs[ self::NO_SHIPPING_CLASS_ID ] = $shipping_settings['no_class_cost'] ?? '0'; $free_shipping_class_ids = []; $paid_shipping_class_ids = []; foreach ( $shipping_class_ids_to_costs as $class_id => $cost ) { if ( self::is_zero_cost( $cost ) ) { $free_shipping_class_ids[] = $class_id; } else { $paid_shipping_class_ids[] = $class_id; } } if ( count( $free_shipping_class_ids ) === 0 ) { self::log_map_shipping_method_issue_to_meta( $zone, $flat_rate_method, 'Flat rate shipping has no free classes', 'map_flat_rate_shipping_method' ); return null; } $free_shipping_applies_to_all_products = empty( $paid_shipping_class_ids ); return array( 'name' => $flat_rate_method['title'], 'has_free_shipping' => 'true', 'applies_to_all_products' => $free_shipping_applies_to_all_products, 'shipping_class_ids' => $free_shipping_class_ids, ); } private static function log_map_shipping_method_issue_to_meta( array $zone, array $shipping_method, string $message, string $flow_step ): void { Logger::log( $message, array( 'flow_name' => FeedUploadUtils::SHIPPING_PROFILES_SYNC_LOGGING_FLOW_NAME, 'flow_step' => $flow_step, 'extra_data' => [ 'zone_id' => $zone['id'], 'zone_name' => $zone['zone_name'], 'method_id' => $shipping_method['instance_id'], ], ), array( 'should_send_log_to_meta' => true, 'should_save_log_in_woocommerce' => true, 'woocommerce_log_level' => \WC_Log_Levels::DEBUG, ) ); } private static function add_continent_location( string $continent_code, array $countries_to_states ): array { $country_codes = WC()->countries->get_continents()[ $continent_code ]['countries']; foreach ( $country_codes as $country_code ) { $countries_to_states = self::add_country_location( $country_code, $countries_to_states ); } return $countries_to_states; } private static function add_country_location( string $country_code, array $countries_to_states ): array { $countries_to_states[ $country_code ]['applies_to_entire_country'] = true; return $countries_to_states; } private static function is_zero_cost( string $cost_string ): bool { if ( empty( $cost_string ) ) { return true; } if ( is_numeric( $cost_string ) ) { return 0.0 === (float) $cost_string; } return false; } public static function get_shipping_class_tag_for_class( string $class_id ): string { if ( self::NO_SHIPPING_CLASS_ID === $class_id ) { return self::NO_SHIPPING_CLASS_TAG; } return self::SHIPPING_CLASS_TAG_PREFIX . $class_id; } }