is_system_of_record_square() ) {
$next_steps = array(
'update_category_data',
'update_product_data',
);
}
// only pull latest inventory if enabled
if ( wc_square()->get_settings_handler()->is_inventory_sync_enabled() ) {
$next_steps[] = 'update_inventory_tracking';
$next_steps[] = 'update_inventory_counts';
}
$this->set_attr( 'next_steps', $next_steps );
}
/**
* Updates categories from Square.
*
* @since 2.0.8
*
* @throws \Exception
*/
protected function update_category_data() {
$date = new \DateTime();
$date->setTimestamp( $this->get_attr( 'catalog_last_synced_at', (int) wc_square()->get_sync_handler()->get_last_synced_at() ) );
$date->setTimezone( new \DateTimeZone( 'UTC' ) );
$count = 0;
$response = wc_square()->get_api()->search_catalog_objects(
array(
'object_types' => array( 'CATEGORY' ),
'begin_time' => $date->format( DATE_ATOM ),
)
);
if ( $response->get_data() instanceof SearchCatalogObjectsResponse ) {
$categories = $response->get_data()->getObjects();
if ( $categories && is_array( $categories ) ) {
foreach ( $categories as $category ) {
Category::import_or_update( $category );
}
$count = count( $categories );
Records::set_record(
array(
'type' => 'info',
'message' => sprintf(
/* translators: Placeholder %d number of categories. */
_n( 'Updated data for %d category.', 'Updated data for %d categories.', count( $categories ), 'woocommerce-square' ),
count( $categories )
),
)
);
}
} else {
Records::set_record(
array(
'type' => 'alert',
'message' => esc_html__( 'Product category data could not be updated from Square. Invalid API response.', 'woocommerce-square' ),
)
);
}
$this->set_attr( 'update_category_data_count', $count );
$this->complete_step( 'update_category_data' );
}
/**
* Updates products from Square.
*
* @since 2.0.0
*
* @throws \Exception
*/
protected function update_product_data() {
$date = new \DateTime();
$date->setTimestamp( $this->get_attr( 'catalog_last_synced_at', (int) wc_square()->get_sync_handler()->get_last_synced_at() ) );
$date->setTimezone( new \DateTimeZone( 'UTC' ) );
$products_updated = $this->get_attr( 'processed_product_ids', array() );
$cursor = $this->get_attr( 'update_product_data_cursor' );
$response = wc_square()->get_api()->search_catalog_objects(
array(
'object_types' => array( 'ITEM' ),
'include_deleted_objects' => true,
'begin_time' => $date->format( DATE_ATOM ),
'cursor' => $cursor,
)
);
// store the timestamp after this API request was completed
// we don't want to set it at the end, as counts may have changed in the time it takes to process the data
if ( ! $cursor ) {
wc_square()->get_sync_handler()->set_last_synced_at();
}
if ( $response->get_data() instanceof SearchCatalogObjectsResponse && is_array( $response->get_data()->getObjects() ) ) {
foreach ( $response->get_data()->getObjects() as $object ) {
// filter out objects that aren't at our configured location
if ( ! $object->getPresentAtAllLocations() && ( ! is_array( $object->getPresentAtLocationIds() ) || ! in_array( wc_square()->get_settings_handler()->get_location_id(), $object->getPresentAtLocationIds(), true ) ) ) {
continue;
}
$product = Product::get_product_by_square_id( $object->getId() );
if ( $product instanceof \WC_Product ) {
if ( ! in_array( $product->get_type(), wc_square()->get_sync_handler()->supported_product_types(), true ) ) {
Records::set_record(
array(
'type' => 'alert',
'message' => sprintf(
/* translators: %1$s - product edit page URL, %2$s - Product ID, %3$s - Product type. */
__( 'Product #%2$s is excluded from sync as the product type "%3$s" is unsupported.', 'woocommerce-square' ),
get_edit_post_link( $product->get_id() ),
$product->get_id(),
$product->get_type()
),
)
);
continue;
}
// deleted items won't have any data to set, so don't try and update the product
if ( $object->getIsDeleted() ) {
$record = array(
'type' => 'alert',
'product_id' => $product->get_id(),
);
// if enabled, hide the product from the catalog
if ( wc_square()->get_settings_handler()->hide_missing_square_products() ) {
try {
$product->set_catalog_visibility( 'hidden' );
$product->save();
$record['product_hidden'] = true;
} catch ( \Exception $e ) {
/* translators: Placeholder %1$s Product Name, %2$s Exception message */
$record['message'] = sprintf( esc_html__( '%1$s was deleted in Square but could not be hidden in WooCommerce. %2$s.', 'woocommerce-square' ), '' . $product->get_formatted_name() . '', $e->getMessage() );
}
}
Records::set_record( $record );
} else {
try {
$thumbnail_image_id = Product::get_catalog_item_thumbnail_id( $object );
Product::update_from_square( $product, $object->getItemData(), false );
Product::update_image_from_square( $product, $thumbnail_image_id );
$products_updated[] = $product->get_id();
} catch ( \Exception $exception ) {
Records::set_record(
array(
'type' => 'alert',
'product_id' => $product->get_id(),
/* translators: Placeholder %1$s Product Name, %2$s Exception message */
'message' => sprintf( esc_html__( 'Could not sync %1$s data from Square. %2$s.', 'woocommerce-square' ), '' . $product->get_formatted_name() . '', $exception->getMessage() ),
)
);
}
}
}
}
}
$cursor = $response->get_data() instanceof SearchCatalogObjectsResponse ? $response->get_data()->getCursor() : null;
$this->set_attr( 'update_product_data_cursor', $cursor );
$this->set_attr( 'processed_product_ids', array_unique( $products_updated ) );
$this->set_attr( 'update_product_data_count', count( array_unique( $products_updated ) ) );
if ( ! $cursor ) {
$this->complete_step( 'update_product_data' );
}
}
/**
* Updates the inventory tracking value from the latest in Square.
*
* Helper method, do not open to public.
*
* @since 3.8.2
*
* @throws \Exception
*/
protected function update_inventory_tracking() {
$products_updated = $this->get_attr( 'processed_product_ids' );
$cursor = $this->get_attr( 'update_inventory_tracking_cursor', null );
$last_synced_at = $this->get_attr( 'inventory_last_synced_at' );
$args = array(
'object_types' => array( 'ITEM_VARIATION' ),
'limit' => 100,
'cursor' => $cursor,
);
if ( $last_synced_at ) {
$date = new \DateTime();
$date->setTimestamp( $last_synced_at );
$date->setTimezone( new \DateTimeZone( 'UTC' ) );
$args['begin_time'] = $date->format( DATE_ATOM );
}
$search_result = wc_square()->get_api()->search_catalog_objects( $args );
if ( ! $search_result->get_data() instanceof SearchCatalogObjectsResponse ) {
throw new \Exception( 'API response data is invalid' );
}
$objects = $search_result->get_data()->getObjects() ? $search_result->get_data()->getObjects() : array();
$cursor = $search_result->get_data() instanceof SearchCatalogObjectsResponse ? $search_result->get_data()->getCursor() : null;
$catalog_objects_tracking_stats = Helper::get_catalog_inventory_tracking( $objects );
$catalog_objects_to_update = array();
foreach ( $catalog_objects_tracking_stats as $catalog_object_id => $inventory_data ) {
$is_tracking_inventory = $inventory_data['track_inventory'] ?? true;
$sold_out = $inventory_data['sold_out'] ?? false;
$product = Product::get_product_by_square_variation_id( $catalog_object_id );
if ( $product instanceof \WC_Product ) {
$manage_stock = $product->get_manage_stock();
$stock_status = $product->get_stock_status();
$out_of_stock = 'outofstock' === $stock_status;
// If Inventory tracking is the same as the product's manage stock setting and sold_old value same, skip.
if ( (bool) $is_tracking_inventory === (bool) $manage_stock && (bool) $sold_out === (bool) $out_of_stock ) {
continue;
}
$catalog_objects_to_update[] = $catalog_object_id;
}
}
if ( ! empty( $catalog_objects_to_update ) ) {
// Catalog Inventory data.
$inventory_hash = Helper::get_catalog_objects_inventory_stats( $catalog_objects_to_update );
foreach ( $catalog_objects_to_update as $catalog_object_id ) {
$product = Product::get_product_by_square_variation_id( $catalog_object_id );
if ( $product instanceof \WC_Product ) {
$inventory_data = $catalog_objects_tracking_stats[ $catalog_object_id ] ?? array();
$is_tracking_inventory = $inventory_data['track_inventory'] ?? true;
$sold_out = $inventory_data['sold_out'] ?? false;
/* If catalog object is tracked and has a quantity > 0 set in Square. */
if ( $is_tracking_inventory && isset( $inventory_hash[ $catalog_object_id ] ) ) {
$product->set_stock_quantity( (float) $inventory_hash[ $catalog_object_id ] );
$product->set_manage_stock( true );
/* If the catalog object is tracked but the quantity in Square is set to 0. */
} elseif ( $is_tracking_inventory ) {
$product->set_stock_quantity( 0 );
$product->set_manage_stock( true );
/* If the catalog object is not tracked in Square at all. */
} else {
$product->set_stock_status( $sold_out ? 'outofstock' : 'instock' );
$product->set_manage_stock( false );
}
$product->save();
$products_updated[] = $product->get_id();
}
}
}
$this->set_attr( 'update_inventory_tracking_cursor', $cursor );
$this->set_attr( 'processed_product_ids', array_unique( $products_updated ) );
if ( ! $cursor ) {
$this->complete_step( 'update_inventory_tracking' );
}
}
/**
* Updates the inventory counts from the latest in Square.
*
* Helper method, do not open to public.
*
* @since 2.0.0
*
* @throws \Exception
*/
protected function update_inventory_counts() {
$products_updated = $this->get_attr( 'processed_product_ids' );
$cursor = $this->get_attr( 'update_inventory_counts_cursor' );
$update_count = $this->get_attr( 'update_inventory_counts_count', 0 );
$args = array(
'location_ids' => array( wc_square()->get_settings_handler()->get_location_id() ),
'cursor' => $cursor,
);
$last_synced_at = $this->get_attr( 'inventory_last_synced_at' );
if ( $last_synced_at ) {
$date = new \DateTime();
$date->setTimestamp( $last_synced_at );
$date->setTimezone( new \DateTimeZone( 'UTC' ) );
$args['updated_after'] = $date->format( DATE_ATOM );
}
$response = wc_square()->get_api()->batch_retrieve_inventory_counts( $args );
$cursor = $response->get_data() instanceof BatchRetrieveInventoryCountsResponse ? $response->get_data()->getCursor() : null;
// store the start timestamp after the first API request was completed but do not save it now
// if cursor is present, then it is not the last page. So, use the inventory_last_synced_at time
// else use the current time
$last_sync_timestamp = $cursor ? $last_synced_at : current_time( 'timestamp', true ); // phpcs:disable WordPress.DateTime.CurrentTimeTimestamp.RequestedUTC
$catalog_objects_inventory_stats = array();
foreach ( $response->get_counts() as $count ) {
// If catalog stats array already contains the catalog object marked as IN_STOCK, then continue.
if ( isset( $catalog_objects_inventory_stats[ $count->getCatalogObjectId() ] ) && $catalog_objects_inventory_stats[ $count->getCatalogObjectId() ]['IN_STOCK'] ) {
continue;
// Else if the catalog object is IN_STOCK, then mark IN_STOCK as true and set the quantity for later use.
} elseif ( 'IN_STOCK' === $count->getState() ) {
$catalog_objects_inventory_stats[ $count->getCatalogObjectId() ] = array(
'IN_STOCK' => true,
'quantity' => $count->getQuantity(),
);
// Else if the catalog object doesn't have an IN_STOCK status, then mark IN_STOCK as false and set the quantity as 0 for later use.
} else {
$catalog_objects_inventory_stats[ $count->getCatalogObjectId() ] = array(
'IN_STOCK' => false,
'quantity' => 0,
);
}
}
// Get the inventory tracking for catalog objects.
$catalog_objects_tracking_stats = Helper::get_catalog_objects_tracking_stats(
array_keys( $catalog_objects_inventory_stats )
);
foreach ( $catalog_objects_inventory_stats as $catalog_object_id => $stats ) {
$product = Product::get_product_by_square_variation_id( $catalog_object_id );
// Square can return multiple "types" of counts, WooCommerce only distinguishes whether a product is in stock or not
if ( $product instanceof \WC_Product ) {
$inventory_data = $catalog_objects_tracking_stats[ $catalog_object_id ] ?? array();
$is_tracking_inventory = $inventory_data['track_inventory'] ?? true;
$sold_out = $inventory_data['sold_out'] ?? false;
if ( $is_tracking_inventory ) {
$product->set_manage_stock( true );
$product->set_stock_quantity( $stats['quantity'] );
} else {
$product->set_stock_status( $sold_out ? 'outofstock' : 'instock' );
$product->set_manage_stock( false );
}
$product->save();
$products_updated[] = $product->get_id();
}
}
$this->set_attr( 'update_inventory_counts_cursor', $cursor );
$this->set_attr( 'processed_product_ids', array_unique( $products_updated ) );
$this->set_attr( 'update_inventory_counts_count', $update_count + count( $catalog_objects_inventory_stats ) );
if ( ! $cursor ) {
// When all the inventory counts are synced then set the last sync time to the start time that was stored
wc_square()->get_sync_handler()->set_inventory_last_synced_at( $last_sync_timestamp );
$this->complete_step( 'update_inventory_counts' );
}
}
}