oont-contents/plugins/woocommerce-square/includes/Handlers/Product.php
2025-04-06 08:34:48 +02:00

1697 lines
50 KiB
PHP

<?php
/**
* WooCommerce Square
*
* This source file is subject to the GNU General Public License v3.0
* that is bundled with this package in the file license.txt.
* It is also available through the world-wide-web at this URL:
* http://www.gnu.org/licenses/gpl-3.0.html GNU General Public License v3.0 or later
* If you did not receive a copy of the license and are unable to
* obtain it through the world-wide-web, please send an email
* to license@woocommerce.com so we can send you a copy immediately.
*
* DISCLAIMER
*
* Do not edit or add to this file if you wish to upgrade WooCommerce Square to newer
* versions in the future. If you wish to customize WooCommerce Square for your
* needs please refer to https://docs.woocommerce.com/document/woocommerce-square/
*
* @author WooCommerce
* @copyright Copyright: (c) 2019, Automattic, Inc.
* @license http://www.gnu.org/licenses/gpl-3.0.html GNU General Public License v3.0 or later
*/
namespace WooCommerce\Square\Handlers;
use Square\Models\BatchRetrieveCatalogObjectsResponse;
use WooCommerce\Square\Utilities\Money_Utility;
use WooCommerce\Square\Sync\Records;
use WooCommerce\Square\Sync\Helper;
defined( 'ABSPATH' ) || exit;
/**
* Product handler class.
*
* @since 2.0.0
*/
class Product {
/**
* Meta key to store the taxonomy name that flags whether a product
* is marked as 'synced' with Square
* @var string
**/
const SYNCED_WITH_SQUARE_TAXONOMY = 'wc_square_synced';
/**
* Meta key to store catalog object ID.
*
* @var string
*/
const SQUARE_ID_META_KEY = '_square_item_id';
/**
* Meta key to store version of a catalog object.
*
* @var string
*/
const SQUARE_VERSION_META_KEY = '_square_item_version';
/**
* Meta key to store version of a catalog object variation.
*
* @var string
*/
const SQUARE_VARIATION_ID_META_KEY = '_square_item_variation_id';
/**
* Meta key to store version of a catalog object variation.
*
* @var string
*/
const SQUARE_VARIATION_VERSION_META_KEY = '_square_item_variation_version';
/**
* Meta key to store catalog object thumbnail ID.
*
* @var string
**/
const SQUARE_IMAGE_ID_META_KEY = '_square_item_image_id';
/**
* Meta key used to identify whether a product is a gift card.
*
* @var string
*/
const SQUARE_GIFT_CARD_KEY = '_square_gift_card';
/**
* @param \WC_Product $product
* @param \Square\Models\CatalogObject $catalog_object
*/
public static function update_product( \WC_Product $product, \Square\Models\CatalogObject $catalog_object ) {
if ( 'ITEM' !== $catalog_object->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();
}
}
}
$inventory_tracking_data = $inventory_tracking[ $square_id ] ?? array();
$is_inventory_tracking = $inventory_tracking_data['track_inventory'] ?? true;
$sold_out = $inventory_tracking_data['sold_out'] ?? false;
if ( $is_inventory_tracking ) {
$product->set_manage_stock( true );
$product->set_stock_quantity( $stock );
} else {
$product->set_stock_status( $sold_out ? 'outofstock' : '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;
$inventory_tracking_data = $inventory_tracking[ $square_id ] ?? array();
$is_inventory_tracking = $inventory_tracking_data['track_inventory'] ?? true;
$sold_out = $inventory_tracking_data['sold_out'] ?? false;
$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( $sold_out ? 'outofstock' : '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 '<a href="' . esc_url( get_edit_post_link( $product->get_id() ) ) . '">' . esc_html( $product->get_formatted_name() ) . '</a>';
}
/**
* 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 );
}
}