getType() || ! $catalog_object->getItemData() ) { throw new \InvalidArgumentException( 'Type of $catalog_object must be an ITEM' ); } $product->update_meta_data( self::SQUARE_ID_META_KEY, $catalog_object->getId() ); $product->update_meta_data( self::SQUARE_VERSION_META_KEY, $catalog_object->getVersion() ); $product->update_meta_data( self::SQUARE_IMAGE_ID_META_KEY, self::get_catalog_item_thumbnail_id( $catalog_object ) ); $product->save(); } /** * @param \WC_Product $product * @param \Square\Models\CatalogObject $catalog_object */ public static function update_variation( \WC_Product $product, \Square\Models\CatalogObject $catalog_object ) { if ( 'ITEM_VARIATION' !== $catalog_object->getType() || ! $catalog_object->getItemVariationData() ) { throw new \InvalidArgumentException( 'Type of $catalog_object must be an ITEM_VARIATION' ); } $product->update_meta_data( self::SQUARE_VARIATION_ID_META_KEY, $catalog_object->getId() ); $product->update_meta_data( self::SQUARE_VARIATION_VERSION_META_KEY, $catalog_object->getVersion() ); $product->save(); } /** * Updates a WooCommerce product from Square data. * * @since 2.0.0 * * @param \WC_Product $product product object * @param \Square\Models\CatalogItem $catalog_item Square API catalog item data * @param bool $with_inventory whether to pull the latest product inventory from Square * @throws \Exception */ public static function update_from_square( \WC_Product $product, \Square\Models\CatalogItem $catalog_item, $with_inventory = true ) { $catalog_id = null; $catalog_variations = $catalog_item->getVariations(); if ( $product instanceof \WC_Product_Variable ) { foreach ( $catalog_variations as $catalog_variation ) { // sanity check to ensure the correct data structure if ( ! $catalog_variation->getItemVariationData() instanceof \Square\Models\CatalogItemVariation ) { continue; } $catalog_id = $catalog_variation->getItemVariationData()->getItemId(); $variation = wc_get_product( wc_get_product_id_by_sku( $catalog_variation->getItemVariationData()->getSku() ) ); if ( $variation ) { if ( ! $variation instanceof \WC_Product_Variation || $variation->get_parent_id() !== $product->get_id() ) { continue; } $variation->update_meta_data( self::SQUARE_VARIATION_ID_META_KEY, $catalog_variation->getId() ); /** * Allow overriding variation name during product import from Square * * @since 3.3.0 * * @param string $variation_name Variation name to update. * @param \SquareConnect\Model\CatalogObject $catalog_variation Catalog item variation being imported. * @param \SquareConnect\Model\CatalogItem $catalog_item Catalog item being imported. * @param \WC_Product_Variation $variation Variation being updated. * @return false|string String to override the variation name, false to disable updating * and keep existing name. * @since 3.3.0 */ $variation_name = apply_filters( 'wc_square_update_product_set_variation_name', $catalog_variation->getItemVariationData()->getName(), $catalog_variation, $catalog_item, $variation ); if ( false !== $variation_name ) { $variation->set_name( $variation_name ); } self::update_price_money( $variation, $catalog_variation ); if ( $with_inventory && wc_square()->get_settings_handler()->is_inventory_sync_enabled() ) { self::update_stock_from_square( $variation, false ); } $variation->save(); /** * Fires after updating a WooCommerce variation product from Square data. * * @since 2.0.0 * * @param \WC_Product_Variation $variation variation object * @param \Square\Models\CatalogItemVariation $catalog_variation Square API catalog variation item object */ do_action( 'wc_square_updated_product_variation_from_square', $variation, $catalog_variation ); } } } else { $catalog_variation = current( $catalog_variations ); if ( $product->get_sku() !== $catalog_variation->getItemVariationData()->getSku() ) { throw new \Exception( 'The WooCommerce SKU and Square SKU do not match' ); } $catalog_id = $catalog_variation->getItemVariationData()->getItemId(); $product->update_meta_data( self::SQUARE_VARIATION_ID_META_KEY, $catalog_variation->getId() ); self::update_price_money( $product, $catalog_variation ); if ( $with_inventory && wc_square()->get_settings_handler()->is_inventory_sync_enabled() ) { self::update_stock_from_square( $product, false ); } } /** * Allow overriding product name during import from Square * * @since 3.3.0 * * @param string $product_name Product name to update. * @param \SquareConnect\Model\CatalogItem $catalog_item Catalog item being imported. * @param \WC_Product $product Product being updated. * @return false|string String to override the product name, false to disable updating * and keep existing name. * @since 3.3.0 */ $product_name = apply_filters( 'wc_square_update_product_set_name', $catalog_item->getName(), $catalog_item, $product ); if ( false !== $product_name ) { $product->set_name( wc_clean( $product_name ) ); } $product_description = self::get_catalog_item_description( $catalog_item ); /** * Allow overriding product description during import from Square * * @since 3.3.0 * * @param string $product_description Product description to update. * @param \SquareConnect\Model\CatalogItem $catalog_item Catalog item being imported. * @param \WC_Product $product Product being updated. * * @return false|string String to override the product description, false to disable updating * and keep existing description. * @since 3.3.0 */ $product_description = apply_filters( 'wc_square_update_product_set_description', $product_description, $catalog_item, $product ); if ( false !== $product_description ) { $product->set_description( $product_description ); } $square_category_id = Category::get_square_category_id( $catalog_item ); $category_id = Category::get_category_id_by_square_id( $square_category_id ); if ( $category_id ) { wp_set_object_terms( $product->get_id(), intval( $category_id ), 'product_cat' ); } else { $message = sprintf( /* translators: Placeholder: %s category ID */ __( 'Square category with id (%s) was not imported to your Store. Please run Import Products from Square settings.', 'woocommerce-square' ), $square_category_id ); $records = Records::get_records(); foreach ( $records as $record ) { if ( $record->get_message() === $message ) { $is_recorded = true; } } if ( ! isset( $is_recorded ) ) { Records::set_record( array( 'type' => 'alert', 'message' => $message, ) ); } } if ( $catalog_id ) { $product->update_meta_data( self::SQUARE_ID_META_KEY, $catalog_id ); } $product->save(); /** * Fires after updating a WooCommerce product from Square data. * * @since 2.0.0 * * @param \WC_Product $product product object * @param \Square\Models\CatalogItem $catalog_item Square API catalog item object */ do_action( 'wc_square_updated_product_from_square', $product, $catalog_item ); } /** * Returns description of a catalog item. * * @since 3.9.1 * * @param \Square\Models\CatalogItem $catalog_item * @return string */ public static function get_catalog_item_description( $catalog_item ) { /** * Filter to import HTML description with HTML. * Enabled by default. * * @since 3.9.1 * * @param boolean 'is_enabled' Boolean to toggle support for HTML descriptions. */ if ( apply_filters( 'wc_square_enable_html_description', true ) ) { $product_description = wp_specialchars_decode( $catalog_item->getDescriptionHtml() ); } else { // For some reason, `getDescriptionPlaintext` returns description with HTML. // We use wp_strip_all_tags to strip HTML and preserve white spaces. // Not sure if this is a bug. $product_description = wp_strip_all_tags( $catalog_item->getDescriptionPlaintext() ); } return $product_description; } /** * Updates a product image from a URL provided by Square (helper method). * * Note: does not save the product for persistence. If opening to public, consider changing this behavior. * This function handles its own exceptions and logs them. * * @since 2.0.0 * * @param \WC_Product|int $given_product product object or product ID * @param string $image_id * @param bool $force_update If true, always import and update the product image from Square. If false, check if the image already exists on the given product before uploading a possible * @todo Look at ussages of this function. Does it even need to return anything? * @return \WC_Product|int The product id of object that was passed in. */ public static function update_image_from_square( $given_product, $image_id, $force_update = false ) { $product = is_numeric( $given_product ) ? wc_get_product( $given_product ) : $given_product; $image_override = wc_square()->get_settings_handler()->is_override_product_images_enabled() || $force_update; $image_url = ''; if ( ! $product instanceof \WC_Product ) { wc_square()->log( sprintf( 'Could not import image from Square for attaching to product: Invalid product.' ) ); return $given_product; } try { if ( ! function_exists( 'media_sideload_image' ) ) { require_once ABSPATH . 'wp-admin/includes/media.php'; require_once ABSPATH . 'wp-admin/includes/file.php'; require_once ABSPATH . 'wp-admin/includes/image.php'; } $product_image_id = $product->get_image_id(); /** * If WooCommerce product has an image but Square product doesn't, then * delete `_square_item_image_id` meta. */ if ( $product_image_id && ! $image_id && $image_override ) { $product->delete_meta_data( '_square_item_image_id' ); $product->set_image_id( '' ); } elseif ( $image_override && $image_id ) { $old_square_image_id = $product->get_meta( '_square_item_image_id' ); $image_response = wc_square()->get_api()->retrieve_catalog_object( $image_id ); $image_url = $image_response->get_data()->getObject()->getImageData()->getUrl(); if ( $old_square_image_id !== $image_id ) { // grab remote image to upload into WordPress before attaching to product $attachment_id = media_sideload_image( $image_url, $product->get_id(), $product->get_title(), 'id' ); if ( is_wp_error( $attachment_id ) ) { throw new \Exception( $attachment_id->get_error_message() ); } self::set_square_image_id( $product, $image_id ); // attach the newly updated image to product $product->set_image_id( $attachment_id ); } } $product->save(); // if the product has an image but doesn't have any Square image ID meta set, check we're not uploading a duplicate image src from Square if ( ! $force_update && $product_image_id && ! $product->get_meta( '_square_item_image_id' ) ) { $product_image_src = get_post_meta( $product_image_id, '_source_url', true ); if ( empty( $product_image_src ) ) { throw new \Exception( 'Cannot compare existing product image src with new upload. Exiting to avoid uploading duplicate' ); } elseif ( $product_image_src === $image_url ) { $product->update_meta_data( '_square_item_image_id', $image_id ); throw new \Exception( 'This image has already been uploaded to WordPress and is now set on the product' ); } } return $product; } catch ( \Exception $e ) { /* Translators: Placeholder: %1$s - product ID, %2$s - Exception message */ $message = sprintf( esc_html__( 'Image not updated from Square for product #%1$s. %2$s.', 'woocommerce-square' ), $product->get_id(), $e->getMessage() ); wc_square()->log( $message ); Records::set_record( array( 'type' => 'alert', 'message' => $message, ) ); } $product->save(); return $product; } /** * Updates a product's stock by getting the latest values from Square. * * @since 2.0.0 * * @param \WC_Product $product product object * @param bool $save whether to save the product object * @return \WC_Product * @throws \Exception */ public static function update_stock_from_square( \WC_Product $product, $save = true ) { $square_id = $product->get_meta( self::SQUARE_VARIATION_ID_META_KEY ); if ( ! $square_id ) { throw new \Exception( esc_html__( 'Product not synced with Square', 'woocommerce-square' ) ); } // if saving the product, flag as syncing so updating the stock won't trigger another sync if ( $save && ( ! defined( 'DOING_SQUARE_SYNC' ) || false === DOING_SQUARE_SYNC ) ) { define( 'DOING_SQUARE_SYNC', true ); } $response = wc_square()->get_api()->retrieve_inventory_count( $square_id ); $result = wc_square()->get_api()->retrieve_catalog_object( $square_id ); if ( ! $result->get_data() || ! $result->get_data()->getObject() ) { throw new \Exception( 'No object data present' ); } $inventory_tracking = Helper::get_catalog_inventory_tracking( array( $result->get_data()->getObject() ) ); $stock = 0; if ( $response->get_data() && $response->get_data()->getCounts() ) { /** @var \Square\Models\InventoryCount $count */ foreach ( $response->get_data()->getCounts() as $count ) { if ( 'IN_STOCK' === $count->getState() ) { $stock += (float) $count->getQuantity(); } } } $is_inventory_tracking = isset( $inventory_tracking[ $square_id ] ) ? $inventory_tracking[ $square_id ] : true; if ( $is_inventory_tracking ) { $product->set_manage_stock( true ); $product->set_stock_quantity( $stock ); } else { $product->set_stock_status( 'instock' ); $product->set_manage_stock( false ); } if ( $save ) { $product->save(); } return $product; } /** * Updates a product's stock by getting the latest values from Square. * * @since 4.1.0 * * @param int[] $product_ids Product IDs. * @param bool $save Whether to save the product object. * @return void * @throws \Exception */ public static function update_products_stock_from_square( $product_ids ) { $products_map = self::get_square_meta( $product_ids, 'square_item_variation_id' ); $square_ids = array_keys( $products_map ); if ( empty( $square_ids ) ) { return; } // Flag as syncing so updating the stock won't trigger another sync if ( ! defined( 'DOING_SQUARE_SYNC' ) || false === DOING_SQUARE_SYNC ) { define( 'DOING_SQUARE_SYNC', true ); } $response = wc_square()->get_api()->batch_retrieve_catalog_objects( $square_ids ); if ( ! $response->get_data() instanceof BatchRetrieveCatalogObjectsResponse ) { throw new \Exception( 'Response data is missing' ); } if ( is_array( $response->get_data()->getObjects() ) ) { $inventory_hash = Helper::get_catalog_objects_inventory_stats( $square_ids ); $inventory_tracking = Helper::get_catalog_inventory_tracking( $response->get_data()->getObjects() ); foreach ( $response->get_data()->getObjects() as $catalog_object ) { $square_id = $catalog_object->getId(); $stock = $inventory_hash[ $square_id ] ?? 0; $is_inventory_tracking = isset( $inventory_tracking[ $square_id ] ) ? $inventory_tracking[ $square_id ] : true; $product_id = $products_map[ $square_id ]['product_id']; $product = wc_get_product( $product_id ); if ( ! $product ) { continue; } if ( $is_inventory_tracking ) { $product->set_manage_stock( true ); $product->set_stock_quantity( $stock ); } else { $product->set_stock_status( 'instock' ); $product->set_manage_stock( false ); } $product->save(); } } } /** * Initializes custom product taxonomies. * * @since 2.0.0 */ public static function init_taxonomies() { register_taxonomy( self::SYNCED_WITH_SQUARE_TAXONOMY, array( 'product' ), array( 'hierarchical' => false, 'update_count_callback' => '_update_generic_term_count', 'show_ui' => false, 'show_in_nav_menus' => false, 'query_var' => is_admin(), 'rewrite' => false, ) ); } /** * Sets a product's synced with Square status. * * @since 2.0.0 * * @param \WC_Product|int $product a valid product object or product ID * @param string $synced either 'yes' (default) or 'no' * @return bool */ public static function set_synced_with_square( $product, $synced = 'yes' ) { $product = is_numeric( $product ) ? wc_get_product( $product ) : $product; if ( ! $product instanceof \WC_Product || ! in_array( $synced, array( 'yes', 'no' ), true ) ) { return false; } // ensure only one term is associated with the product at any time wp_delete_object_term_relationships( $product->get_id(), array( self::SYNCED_WITH_SQUARE_TAXONOMY ) ); // we have already set the value to "no" above by deleting the term relationship // so it is safe to return with true. if ( 'no' === $synced ) { return true; } $set_term = wp_set_post_terms( $product->get_id(), array( $synced ), self::SYNCED_WITH_SQUARE_TAXONOMY ); $success = is_array( $set_term ); if ( wc_square()->get_settings_handler()->is_inventory_sync_enabled() && 'external' !== $product->get_type() ) { // Trigger a sync inventory from Woo to Square if product stock is updated from admin. $product->save(); } return $success; } /** * Removes a product flag from being synced with Square. * * @since 2.0.0 * * @param \WC_Product $product a valid product object * @return bool */ public static function unset_synced_with_square( $product ) { return self::set_synced_with_square( $product, 'no' ); } /** * Determines whether a product is set to be synced with Square. * * @since 2.0.0 * * @param false|\WC_Product $product a valid product object * @return bool */ public static function is_synced_with_square( $product ) { if ( $product instanceof \WC_Product ) { // if this is a variation, check its parent. $parent_product = wc_get_product( $product->get_parent_id() ); if ( $parent_product instanceof \WC_Product ) { $product = $parent_product; } $terms = wp_get_post_terms( $product->get_id(), self::SYNCED_WITH_SQUARE_TAXONOMY, array( 'fields' => 'names' ) ); } return ! empty( $terms ) && 'yes' === $terms[0]; } /** * Determines if a product can be synced with Square. * * SKUs and single-dimension attributes are required, so this helps us validate that in case a product has been * marked as "Sync with Square" manually. * * @since 2.0.2 * * @param \WC_Product $product product object * @return bool */ public static function can_sync_with_square( \WC_Product $product ) { $can_sync = self::has_sku( $product ); if ( $can_sync && $product->is_type( 'variable' ) ) { $can_sync = ! self::has_multiple_variation_attributes( $product ); } /** * Hook to filter whether a product can sync with Square. * * @since 2.0.2 * * @param boolean $can_sync Boolean to set if product can sync with Square. * @param \WC_Product $product WooCommerce product. */ return (bool) apply_filters( 'wc_square_product_can_sync_with_square', $can_sync, $product ); } /** * Return a link to the product's edit page * * @since 2.0.8 * * @param \WC_Product $product product object * @return string */ public static function get_product_edit_link( \WC_Product $product ) { return '' . esc_html( $product->get_formatted_name() ) . ''; } /** * Determines if a product has a SKU set. * * For variable products, this checks if all of its variations have a SKU. * * @since 2.0.2 * * @param \WC_Product $product product object * @return bool */ public static function has_sku( \WC_Product $product ) { if ( $product->is_type( 'variable' ) && $product->has_child() ) { foreach ( $product->get_children() as $variation_id ) { $variation = wc_get_product( $variation_id ); if ( $variation instanceof \WC_Product && empty( $variation->get_sku( 'edit' ) ) ) { return false; } } return true; } return ! empty( $product->get_sku() ); } /** * Determines if a product has multiple variation attributes. * * @since 2.0.2 * * @param \WC_Product $product product object * @return bool */ public static function has_multiple_variation_attributes( \WC_Product $product ) { $has_attributes = false; if ( $product->is_type( 'variable' ) ) { $variation_attributes = array(); foreach ( $product->get_attributes() as $attribute ) { if ( $attribute instanceof \WC_Product_Attribute && $attribute->get_variation() ) { $variation_attributes[] = $attribute; } } if ( count( $variation_attributes ) > 1 ) { $has_attributes = true; } } return $has_attributes; } /** * Gets an ID list of products that have a synced with Square status set. * * @since 2.0.0 * * @param string $status either 'yes' or 'no' * @return int[] array of product IDs */ private static function get_products_synced_status( $status ) { $sync_status_term = get_term_by( 'name', 'yes', self::SYNCED_WITH_SQUARE_TAXONOMY ); $product_ids = array(); if ( $sync_status_term instanceof \WP_Term && in_array( $status, array( 'yes', 'no' ), true ) ) { $tax_query_args = array( 'taxonomy' => self::SYNCED_WITH_SQUARE_TAXONOMY, 'field' => 'id', 'terms' => $sync_status_term->term_id, 'include_children' => false, ); if ( 'no' === $status ) { $tax_query_args['operator'] = 'NOT IN'; } $product_ids = get_posts( array( 'post_type' => array( 'product', 'product_variation' ), 'post_status' => array( 'private', 'publish' ), 'fields' => 'ids', 'nopaging' => true, 'tax_query' => array( $tax_query_args ), // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_tax_query ) ); } return $product_ids; } /** * Gets a list of products explicitly not set to be synced with Square. * * @since 2.0.0 * * @return int[] */ public static function get_products_not_synced_with_square() { return self::get_products_synced_status( 'no' ); } /** * Gets a list of products that are set to be synced with Square. * * @since 2.0.0 * * @return int[] array of product IDs */ public static function get_products_synced_with_square() { return self::get_products_synced_status( 'yes' ); } /** * Gets a product ID from a Square API variation ID. * * @since 2.0.0 * * @param string $variation_id Square API variation item ID * @return int|null */ public static function get_product_id_by_square_variation_id( $variation_id ) { global $wpdb; return $wpdb->get_var( $wpdb->prepare( " SELECT pm.post_id FROM {$wpdb->prefix}postmeta AS pm INNER JOIN {$wpdb->prefix}posts AS p ON pm.post_id = p.ID WHERE pm.meta_key = %s AND pm.meta_value = %s ", self::SQUARE_VARIATION_ID_META_KEY, $variation_id ) ); } /** * Gets a product from a Square API variation ID. * * @since 2.0.0 * * @param string $variation_id Square API variation item ID * @return \WC_Product|null */ public static function get_product_by_square_variation_id( $variation_id ) { $product = wc_get_product( self::get_product_id_by_square_variation_id( $variation_id ) ); if ( ! $product ) { $product = null; } return $product; } /** * Gets a product ID from a Square API ID. * * @since 2.0.0 * * @param string $square_id Square API item ID * @return int|null */ public static function get_product_id_by_square_id( $square_id ) { global $wpdb; return $wpdb->get_var( $wpdb->prepare( " SELECT pm.post_id FROM {$wpdb->prefix}postmeta AS pm INNER JOIN {$wpdb->prefix}posts AS p ON pm.post_id = p.ID WHERE pm.meta_key = %s AND pm.meta_value = %s ", self::SQUARE_ID_META_KEY, $square_id ) ); } /** * Gets a product from a Square API ID. * * @since 2.0.0 * * @param string $square_id Square API item ID * @return \WC_Product|null */ public static function get_product_by_square_id( $square_id ) { $product = wc_get_product( self::get_product_id_by_square_id( $square_id ) ); // ensure we have a parent product if ( ! $product || $product instanceof \WC_Product_Variation ) { $product = null; } return $product; } /** * Converts a WC_Product to a Square CatalogObject. * * @since 2.0.0 * * @param \WC_Product $product * @return null|\Square\Models\CatalogObject */ public static function convert_to_catalog_object( \WC_Product $product ) { if ( ! $product ) { return null; } $parent_id = $product->get_parent_id(); if ( 0 !== $parent_id ) { return self::convert_to_catalog_object( wc_get_product( $parent_id ) ); } $variations = array(); if ( $product->has_child() ) { foreach ( $product->get_children() as $child_product_id ) { $child_product = wc_get_product( $child_product_id ); if ( $child_product ) { $variation = self::extract_catalog_item_variation_data( $child_product ); if ( $variation ) { $variations[] = $variation; } } } } else { $variation = self::extract_catalog_item_variation_data( $product ); $variations = $variation ? array( $variation ) : array(); } if ( empty( $variations ) ) { return null; } $catalog_object = new \Square\Models\CatalogObject( 'ITEM', self::get_square_item_id( $product ) ); $catalog_object->setVersion( self::get_square_version( $product ) ); $catalog_object->setPresentAtLocationIds( array( wc_square()->get_settings_handler()->get_location_id() ) ); $catalog_item = new \Square\Models\CatalogItem(); $catalog_item->setName( $product->get_name() ); $catalog_item->setVariations( $variations ); $catalog_object->setItemData( $catalog_item ); // TODO: Handle categories return $catalog_object; } /** * Extracts the data for a catalog item from a \WC_Product. * * @since 2.0.0 * * @param \WC_Product $product the product object * @param \Square\Models\CatalogItemVariation[] $variations (optional) array of variations to include * @param bool $is_soft_delete whether or not this item data is for a soft-delete * @return array */ public static function extract_catalog_item_data( \WC_Product $product, array $variations = array(), $is_soft_delete = false ) { if ( ! $product ) { return null; } $data = array( 'type' => 'ITEM', 'id' => self::get_square_item_id( $product ), 'version' => self::get_square_version( $product ), 'present_at_location_ids' => array( wc_square()->get_settings_handler()->get_location_id() ), 'item_data' => array( 'name' => $product->get_name(), 'variations' => $variations, ), ); $square_category_id = 0; foreach ( $product->get_category_ids() as $category_id ) { $map = Category::get_mapping( $category_id ); if ( ! empty( $map['square_id'] ) ) { $square_category_id = $map['square_id']; break; } } // if a category with a Square ID was found if ( $square_category_id ) { $data['item_data']['category_id'] = $square_category_id; } if ( $is_soft_delete ) { $data['present_at_all_locations'] = false; $data['present_at_location_ids'] = array(); } return $data; } /** * Extracts the data for a catalog item variation from a \WC_Product. * * @since 2.0.0 * * @param \WC_Product $product the product to get the variation data for * @param \WC_Product $parent_product (optional) the parent product - prevents additional calls to wc_get_product() * * @param bool $is_soft_delete whether or not this item data is for a soft-delete * @return array */ public static function extract_catalog_item_variation_data( \WC_Product $product, \WC_Product $parent_product = null, $is_soft_delete = false ) { if ( ! $product ) { return null; } $parent_product_id = $product->get_parent_id(); if ( 0 === $parent_product_id ) { $parent_product = $product; } elseif ( null === $parent_product || $parent_product_id !== $parent_product->get_id() ) { $parent_product = wc_get_product( $parent_product_id ); } if ( $parent_product instanceof \WC_Product ) { $item_id = self::get_square_item_id( $parent_product ); $data = array( 'type' => 'ITEM_VARIATION', 'id' => self::get_square_item_variation_id( $product ), 'version' => self::get_square_variation_version( $product ), 'item_variation_data' => array( 'item_id' => $item_id, 'name' => $product->get_name(), 'sku' => $product->get_sku(), 'pricing_type' => 'FIXED_PRICING', 'price_money' => self::price_to_money( $product->get_regular_price() ), 'track_inventory' => true, ), ); if ( $is_soft_delete ) { $data['present_at_all_locations'] = false; $data['present_at_location_ids'] = array(); } } return $data; } /** * Converts a product price to a Money object. * * @since 2.0.0 * * @param int|float $price * @return \Square\Models\Money */ public static function price_to_money( $price ) { return Money_Utility::amount_to_money( $price, get_woocommerce_currency() ); } /** * Returns the square item ID (if known) or generates one based on local data. * * @since 2.0.0 * * @param int|\WC_Product $product_id the product ID or product object * @param bool $generate_if_not_found whether a temporary ID should be returned if an ID is not found * @return string */ public static function get_square_item_id( $product_id, $generate_if_not_found = true ) { if ( $product_id instanceof \WC_Product ) { $product_id = $product_id->get_id(); } $square_item_id = get_post_meta( $product_id, self::SQUARE_ID_META_KEY, true ); $square_item_id = $square_item_id ? $square_item_id : null; if ( ! $square_item_id && true === $generate_if_not_found ) { $square_item_id = '#item_' . $product_id; } return $square_item_id; } /** * Sets the Square item ID for a given product. * * @since 2.0.0 * * @param int|false|\WC_Product $product the product object or ID * @param string $item_id the Square item ID */ public static function set_square_item_id( $product, $item_id ) { $product = is_numeric( $product ) ? wc_get_product( $product ) : $product; if ( $product instanceof \WC_Product ) { $product->update_meta_data( self::SQUARE_ID_META_KEY, $item_id ); $product->save(); } } /** * Returns the square item variation ID (if known) or generates one based on local data. * * @since 2.0.0 * * @param int|\WC_Product $product_id the product ID or product object * @param bool $generate_if_not_found whether a temporary ID should be returned if an ID is not found * @return string|null */ public static function get_square_item_variation_id( $product_id, $generate_if_not_found = true ) { if ( $product_id instanceof \WC_Product ) { $product_id = $product_id->get_id(); } $square_item_variation_id = get_post_meta( $product_id, self::SQUARE_VARIATION_ID_META_KEY, true ); $square_item_variation_id = $square_item_variation_id ? $square_item_variation_id : null; if ( ! $square_item_variation_id && true === $generate_if_not_found ) { $square_item_variation_id = '#item_variation_' . $product_id; } return $square_item_variation_id; } /** * Sets the Square item variation ID for a given product. * * @since 2.0.0 * * @param int|false|\WC_Product $product the product object or ID * @param string $item_variation_id the Square item variation ID */ public static function set_square_item_variation_id( $product, $item_variation_id ) { $product = is_numeric( $product ) ? wc_get_product( $product ) : $product; if ( $product instanceof \WC_Product ) { $product->update_meta_data( self::SQUARE_VARIATION_ID_META_KEY, $item_variation_id ); $product->save(); } } /** * Returns the Square item version (if known) for the given product. * * @since 2.0.0 * * @param int|\WC_Product $product_id the product ID or product object * @return int */ public static function get_square_version( $product_id ) { if ( $product_id instanceof \WC_Product ) { $product_id = $product_id->get_id(); } $square_version = get_post_meta( $product_id, self::SQUARE_VERSION_META_KEY, true ); return $square_version ? (int) $square_version : 0; } /** * Sets the Square item version for a given product. * * @since 2.0.0 * * @param int|false|\WC_Product $product the product object or ID * @param int $version the Square item version */ public static function set_square_version( $product, $version ) { $product = is_numeric( $product ) ? wc_get_product( $product ) : $product; if ( $product instanceof \WC_Product ) { $product->update_meta_data( self::SQUARE_VERSION_META_KEY, $version ); $product->save(); } } /** * Returns the Square item variation version (if known) for the given product. * * @since 2.0.0 * * @param int|\WC_Product $product_id the product ID or product object * @return int */ public static function get_square_variation_version( $product_id ) { if ( $product_id instanceof \WC_Product ) { $product_id = $product_id->get_id(); } $square_variation_version = get_post_meta( $product_id, self::SQUARE_VARIATION_VERSION_META_KEY, true ); return $square_variation_version ? (int) $square_variation_version : 0; } /** * Sets the Square item ID for a given product. * * @since 2.0.0 * * @param int|false|\WC_Product $product the product object or ID * @param int $variation_version the Square item variation version */ public static function set_square_variation_version( $product, $variation_version ) { $product = is_numeric( $product ) ? wc_get_product( $product ) : $product; if ( $product instanceof \WC_Product ) { $product->update_meta_data( self::SQUARE_VARIATION_VERSION_META_KEY, $variation_version ); $product->save(); } } /** * Gets a product's Square image ID. * * @since 2.0.0 * * @param \WC_Product|int $product product object or ID * @return string */ public static function get_square_image_id( $product ) { $image_id = ''; if ( is_numeric( $product ) ) { $product = wc_get_product( $product ); } if ( $product instanceof \WC_Product ) { $image_id = $product->get_meta( self::SQUARE_IMAGE_ID_META_KEY ); } return $image_id; } /** * Sets a product's Square image ID. * * @since 2.0.0 * * @param \WC_Product|int $product product object or ID * @param string $image_id Square image ID */ public static function set_square_image_id( $product, $image_id ) { $product = is_numeric( $product ) ? wc_get_product( $product ) : $product; if ( $product instanceof \WC_Product ) { $product->update_meta_data( self::SQUARE_IMAGE_ID_META_KEY, $image_id ); $product->save_meta_data(); } } /** * Gets all the Square meta data for the given product IDs. * * @see Product::get_square_meta_single() * * @since 2.0.0 * * @param int[] $product_ids the product IDs to look up * @param string $array_key the variable to use as the array key in the resulting array * @return array associative array of arrays of data, indexed by $array_key found values (e.g. product ID or square ID, etc.) */ public static function get_square_meta( $product_ids, $array_key = 'product_id' ) { global $wpdb; $results = $square_meta = array(); if ( ! empty( $product_ids ) ) { $meta_keys = array( 'square_item_id' => self::SQUARE_ID_META_KEY, 'square_item_variation_id' => self::SQUARE_VARIATION_ID_META_KEY, 'square_version' => self::SQUARE_VERSION_META_KEY, 'square_variation_version' => self::SQUARE_VARIATION_VERSION_META_KEY, ); $array_key = array_key_exists( $array_key, $meta_keys ) ? $array_key : 'product_id'; $post_ids_in = '(' . implode( ',', array_map( 'absint', array_merge( array( 0 ), $product_ids ) ) ) . ')'; $meta_key_in = "('" . self::SQUARE_ID_META_KEY . "','" . self::SQUARE_VARIATION_ID_META_KEY . "','" . self::SQUARE_VERSION_META_KEY . "','" . self::SQUARE_VARIATION_VERSION_META_KEY . "')"; // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared $products_meta = $wpdb->get_results( " SELECT post_id AS product_id, meta_key, meta_value FROM $wpdb->postmeta WHERE post_id IN $post_ids_in AND meta_key IN $meta_key_in ", ARRAY_A ); // phpcs:enable foreach ( $products_meta as $post_meta ) { if ( ! array_key_exists( (string) $post_meta['product_id'], $square_meta ) ) { $square_meta[ (string) $post_meta['product_id'] ] = array( 'product_id' => (int) $post_meta['product_id'], 'square_item_id' => false, 'square_item_variation_id' => false, 'square_version' => false, 'square_variation_version' => false, ); } foreach ( $meta_keys as $square_meta_key => $post_meta_key ) { if ( isset( $post_meta['meta_key'] ) && $post_meta_key === $post_meta['meta_key'] ) { $square_meta[ $post_meta['product_id'] ][ $square_meta_key ] = $post_meta['meta_value']; break; } } } foreach ( $product_ids as $product_id ) { // sanity checks: cannot build index without a valid key if ( ! array_key_exists( $product_id, $square_meta ) || ! isset( $square_meta[ $product_id ][ $array_key ] ) || ! $square_meta[ (string) $product_id ][ $array_key ] ) { continue; } $results[ (string) $square_meta[ (string) $product_id ][ $array_key ] ] = $square_meta[ (string) $product_id ]; } } return $results; } /** * Gets all the Square meta data for the given single product ID. * * @see Product::get_square_meta() for getting meta data for all products * * @since 2.0.0 * * @param int|\WC_Product $product_id the product ID or object * @return array associative array */ public static function get_square_meta_single( $product_id ) { if ( $product_id instanceof \WC_Product ) { $product_id = $product_id->get_id(); } return array( 'product_id' => $product_id, 'square_item_id' => self::get_square_item_id( $product_id ), 'square_item_variation_id' => self::get_square_item_variation_id( $product_id ), 'square_version' => self::get_square_version( $product_id ), ); } /** * Checks if a product is mapped to a Square Item. * * @since 2.0.0 * * @param int|\WC_Product $product_id the product ID or product object * @return bool */ public static function is_mapped( $product_id ) { $item_id = self::get_square_item_id( $product_id ); return ! empty( $item_id ) && false === strpos( $item_id, '#' ); } /** * Updates square meta for a given product ID. * * @since 2.0.0 * * @param int|\WC_Product $product_id the product ID or the product object * @param array $meta_data the meta data to update * @type string $item_id the Square Item ID * @type int $item_version the Square Item version * @type string $item_variation_id the Square Item Variation ID * @type int $item_variation_version the Square Item Variation Version */ public static function update_square_meta( $product_id, $meta_data ) { foreach ( $meta_data as $meta_key => $meta_value ) { switch ( $meta_key ) { case 'item_id': self::set_square_item_id( $product_id, $meta_value ); break; case 'item_version': self::set_square_version( $product_id, $meta_value ); break; case 'item_variation_id': self::set_square_item_variation_id( $product_id, $meta_value ); break; case 'item_variation_version': self::set_square_variation_version( $product_id, $meta_value ); break; case 'item_image_id': self::set_square_image_id( $product_id, $meta_value ); break; } } } /** * Clears the Square meta for a given product. * * @since 2.0.0 * * @param int[] $product_ids array of product IDs */ public static function clear_square_meta( $product_ids ) { global $wpdb; $product_ids = is_array( $product_ids ) ? $product_ids : array( $product_ids ); $meta_keys = array( self::SQUARE_ID_META_KEY, self::SQUARE_VERSION_META_KEY, self::SQUARE_VARIATION_ID_META_KEY, self::SQUARE_VARIATION_VERSION_META_KEY, self::SQUARE_IMAGE_ID_META_KEY, ); $meta_key_in = '("' . implode( '","', $meta_keys ) . '")'; $post_ids_in = '(' . implode( ',', array_map( 'absint', array_merge( array( 0 ), $product_ids ) ) ) . ')'; // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared $wpdb->query( " UPDATE $wpdb->postmeta SET meta_value = '' WHERE meta_key IN $meta_key_in AND post_id IN $post_ids_in; " ); // phpcs:enable } /** * Imports meta data from a remote product to the given local product ID. * * @since 2.0.0 * * @param int|false|\WC_Product $product the product object or ID * @param \Square\Models\CatalogObject $remote_product the remote catalog object */ public static function import_remote_meta( $product, $remote_product ) { $product = is_numeric( $product ) ? wc_get_product( $product ) : $product; $item_data = $remote_product->getItemData(); $image_ids = $item_data->getImageIds(); if ( $product ) { self::update_square_meta( $product->get_id(), array( 'item_id' => $remote_product->getId(), 'item_version' => $remote_product->getVersion(), 'item_image_id' => self::get_catalog_item_thumbnail_id( $remote_product ), ) ); } } /** * Returns the thumbnail ID of a CatalogItem. * * @param \Square\Models\CatalogObject $catalog_object * @return string */ public static function get_catalog_item_thumbnail_id( $catalog_object ) { $catalog_item = $catalog_object->getItemData(); $image_ids = $catalog_item->getImageIds(); if ( is_array( $image_ids ) && count( $image_ids ) > 0 ) { return $image_ids[0]; } return ''; } /** * Gets an InventoryChange object filled with a \Square\Models\InventoryPhysicalCount object for a given product. * * @since 2.0.0 * * @param \WC_Product $product the product object * @return \Square\Models\InventoryChange|null */ public static function get_inventory_change_physical_count_type( \WC_Product $product ) { $inventory_change = null; $square_variation_id = self::get_square_item_variation_id( $product->get_id(), false ); if ( $square_variation_id ) { $inventory_physical_count = new \Square\Models\InventoryPhysicalCount(); $inventory_physical_count->setCatalogObjectId( $square_variation_id ); $inventory_physical_count->setQuantity( '' . max( 0, $product->get_stock_quantity() ) ); $inventory_physical_count->setLocationId( wc_square()->get_settings_handler()->get_location_id() ); $inventory_physical_count->setState( 'IN_STOCK' ); $inventory_physical_count->setOccurredAt( gmdate( 'Y-m-d\TH:i:sP' ) ); $inventory_change = new \Square\Models\InventoryChange(); $inventory_change->setType( 'PHYSICAL_COUNT' ); $inventory_change->setPhysicalCount( $inventory_physical_count ); } return $inventory_change; } /** * Gets an InventoryChange object filled with a \Square\Models\InventoryAdjustment object for a given product. * * @since 2.0.8 * * @param \WC_Product $product the product object * @param int $adjustment Value can negative or positive. * * @return \Square\Models\InventoryChange|null */ public static function get_inventory_change_adjustment_type( \WC_Product $product, $adjustment ) { $square_variation_id = self::get_square_item_variation_id( $product->get_id(), false ); if ( empty( $square_variation_id ) || 0 === $adjustment ) { return null; } if ( 0 > $adjustment ) { $from = 'IN_STOCK'; $to = 'SOLD'; } else { $from = 'NONE'; $to = 'IN_STOCK'; } $inventory_adjustment = new \Square\Models\InventoryAdjustment(); $inventory_adjustment->setCatalogObjectId( $square_variation_id ); $inventory_adjustment->setLocationId( wc_square()->get_settings_handler()->get_location_id() ); $inventory_adjustment->setQuantity( '' . absint( $adjustment ) ); $inventory_adjustment->setFromState( $from ); $inventory_adjustment->setToState( $to ); $inventory_adjustment->setOccurredAt( gmdate( 'Y-m-d\TH:i:sP' ) ); $inventory_change = new \Square\Models\InventoryChange(); $inventory_change->setType( 'ADJUSTMENT' ); $inventory_change->setAdjustment( $inventory_adjustment ); return $inventory_change; } /** * Checks for location overrides and sets the product/variation's price based on the location * * @since 3.3.1 * * @param \WC_Product|\WC_Product_Variation $product * @param \Square\Models\CatalogObject $catalog_variation */ private static function update_price_money( $product, \Square\Models\CatalogObject $catalog_variation ) { $location_overrides = $catalog_variation->getItemVariationData()->getLocationOverrides(); if ( is_null( $location_overrides ) ) { return; } $location_id = wc_square()->get_settings_handler()->get_location_id(); foreach ( $location_overrides as $location_override ) { if ( $location_id === $location_override->getLocationId() ) { // If there is a price override set, then use that amount. if ( $location_override->getPriceMoney() ) { $product->set_regular_price( Money_Utility::cents_to_float( $location_override->getPriceMoney()->getAmount() ) ); } elseif ( $catalog_variation->getItemVariationData()->getPriceMoney() ) { // No price override amount set; fall back on the base price of item variation. $product->set_regular_price( Money_Utility::cents_to_float( $catalog_variation->getItemVariationData()->getPriceMoney()->getAmount() ) ); } } } } /** Helper function to get the variation product's parent ID from posts table but only if the parent product still exists. * * @since 2.5.2 * @param int|object $variation_id * @return string|null */ public static function get_parent_product_id_by_variation_id( $variation_id ) { global $wpdb; return $wpdb->get_var( $wpdb->prepare( " SELECT pr.post_parent FROM {$wpdb->prefix}posts pr INNER JOIN {$wpdb->prefix}posts pp ON pp.ID = pr.post_parent WHERE pr.ID=%d AND pr.post_type IN ('product', 'product_variation') AND pp.post_type = 'product'; ", $variation_id ) ); } /** * Check if product is a gift card. * * @since 4.2.0 * * @param \WC_Product $product WooCommerce product. * * @return bool */ public static function is_gift_card( $product ) { if ( ! is_a( $product, 'WC_Product' ) ) { return false; } if ( $product->is_type( 'variation' ) ) { $product = wc_get_product( $product->get_parent_id() ); } return $product->meta_exists( self::SQUARE_GIFT_CARD_KEY ) && 'yes' === $product->get_meta( self::SQUARE_GIFT_CARD_KEY, true ); } }