Add viator-collector plugin (Hop On Hop Off front-end reskin)
Consolidate the plugin into the TourWebsite repository (its separate nested git repo has been removed). Includes the full new-design reskin: scoped design stylesheet, SVG icon sprite, rebuilt listing/single/section templates, and homepage hero/FAQ/testimonials shortcodes. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01J4hNL1PSHxXbu94SCenpKj
This commit is contained in:
518
viator-collector/includes/class-vc-importer.php
Normal file
518
viator-collector/includes/class-vc-importer.php
Normal file
@@ -0,0 +1,518 @@
|
||||
<?php
|
||||
defined( 'ABSPATH' ) || exit;
|
||||
|
||||
class VC_Importer {
|
||||
private $api;
|
||||
private $destination_map = array();
|
||||
private $tag_map = array();
|
||||
|
||||
public function __construct( VC_API $api = null ) {
|
||||
$this->api = $api ? $api : new VC_API();
|
||||
}
|
||||
|
||||
/**
|
||||
* Force-refresh the destination and tag dictionaries and cache them.
|
||||
* Returns a WP_Error if either lookup fails (so a run can abort early).
|
||||
*/
|
||||
public function prime_dictionaries() {
|
||||
$dest = $this->api->get_destination_map();
|
||||
if ( is_wp_error( $dest ) ) {
|
||||
return $dest;
|
||||
}
|
||||
$tags = $this->api->get_tag_map();
|
||||
if ( is_wp_error( $tags ) ) {
|
||||
return $tags;
|
||||
}
|
||||
set_transient( 'vc_destination_map', $dest, 25 * HOUR_IN_SECONDS );
|
||||
set_transient( 'vc_tag_map', $tags, 25 * HOUR_IN_SECONDS );
|
||||
$this->destination_map = $dest;
|
||||
$this->tag_map = $tags;
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Load dictionaries from cache, fetching them if the cache is cold.
|
||||
*/
|
||||
public function load_dictionaries() {
|
||||
$dest = get_transient( 'vc_destination_map' );
|
||||
if ( false === $dest ) {
|
||||
$dest = $this->api->get_destination_map();
|
||||
$dest = is_wp_error( $dest ) ? array() : $dest;
|
||||
set_transient( 'vc_destination_map', $dest, 25 * HOUR_IN_SECONDS );
|
||||
}
|
||||
$tags = get_transient( 'vc_tag_map' );
|
||||
if ( false === $tags ) {
|
||||
$tags = $this->api->get_tag_map();
|
||||
$tags = is_wp_error( $tags ) ? array() : $tags;
|
||||
set_transient( 'vc_tag_map', $tags, 25 * HOUR_IN_SECONDS );
|
||||
}
|
||||
$this->destination_map = is_array( $dest ) ? $dest : array();
|
||||
$this->tag_map = is_array( $tags ) ? $tags : array();
|
||||
}
|
||||
|
||||
/**
|
||||
* Crawl one page of products for a single destination.
|
||||
*
|
||||
* Each product is deduplicated by code within a run (via $run_id) so its
|
||||
* full detail is only fetched once even though it surfaces under several
|
||||
* destinations.
|
||||
*
|
||||
* @return array|WP_Error Per-page summary including totalCount and page size.
|
||||
*/
|
||||
public function crawl_destination( $destination_id, $start, $count, $run_id ) {
|
||||
$currency = vc_get_option( 'currency', 'USD' );
|
||||
$response = $this->api->search_products( $destination_id, $start, $count, $currency );
|
||||
if ( is_wp_error( $response ) ) {
|
||||
return $response;
|
||||
}
|
||||
|
||||
$products = vc_array_get( $response, 'products', array() );
|
||||
$summary = array(
|
||||
'count' => 0,
|
||||
'imported' => 0,
|
||||
'updated' => 0,
|
||||
'duplicate' => 0,
|
||||
'deactivated' => 0,
|
||||
'failed' => 0,
|
||||
'total' => (int) vc_first_non_empty( array( vc_array_get( $response, 'totalCount' ), vc_array_get( $response, 'total' ) ), 0 ),
|
||||
);
|
||||
|
||||
foreach ( (array) $products as $product ) {
|
||||
if ( ! is_array( $product ) ) {
|
||||
continue;
|
||||
}
|
||||
$summary['count']++;
|
||||
$outcome = $this->process_search_product( $product, $destination_id, $run_id );
|
||||
if ( isset( $summary[ $outcome ] ) ) {
|
||||
$summary[ $outcome ]++;
|
||||
}
|
||||
}
|
||||
|
||||
return $summary;
|
||||
}
|
||||
|
||||
/**
|
||||
* Process one product returned by search: skip if already handled this
|
||||
* run, otherwise optionally fetch full detail and create/update the post.
|
||||
*
|
||||
* @return string One of imported|updated|duplicate|deactivated|failed.
|
||||
*/
|
||||
private function process_search_product( array $product, $destination_id, $run_id ) {
|
||||
$code = (string) vc_first_non_empty( array( vc_array_get( $product, 'productCode' ), vc_array_get( $product, 'code' ) ) );
|
||||
if ( '' === $code ) {
|
||||
return 'failed';
|
||||
}
|
||||
|
||||
$existing = $this->find_existing_tour( $code );
|
||||
if ( $existing && (string) get_post_meta( $existing, '_vc_synced_run', true ) === (string) $run_id ) {
|
||||
// Already fully processed under another destination during this run.
|
||||
return 'duplicate';
|
||||
}
|
||||
|
||||
// Optionally enrich the summary with full product detail.
|
||||
if ( vc_get_option( 'fetch_full_detail', 0 ) ) {
|
||||
$detail = $this->api->get_product( $code );
|
||||
if ( ! is_wp_error( $detail ) && is_array( $detail ) ) {
|
||||
$product = $this->merge_detail( $product, $detail );
|
||||
} elseif ( is_wp_error( $detail ) ) {
|
||||
VC_Logger::log( 'Detail fetch failed (' . $code . '): ' . $detail->get_error_message() );
|
||||
}
|
||||
}
|
||||
|
||||
$product = $this->ensure_destination( $product, $destination_id );
|
||||
$inactive = $this->is_product_inactive( $product );
|
||||
|
||||
$result = $this->create_or_update_tour( $product, $inactive );
|
||||
if ( is_wp_error( $result ) ) {
|
||||
VC_Logger::error( 'Product import failed (' . $code . '): ' . $result->get_error_message() );
|
||||
return 'failed';
|
||||
}
|
||||
|
||||
update_post_meta( (int) $result, '_vc_synced_run', (string) $run_id );
|
||||
|
||||
if ( $inactive ) {
|
||||
return 'deactivated';
|
||||
}
|
||||
return $existing ? 'updated' : 'imported';
|
||||
}
|
||||
|
||||
/**
|
||||
* Overlay full-detail fields onto the search summary while keeping
|
||||
* summary-only fields (pricing/destinations/tags) when detail omits them.
|
||||
*/
|
||||
private function merge_detail( array $summary, array $detail ) {
|
||||
$merged = array_replace( $summary, $detail );
|
||||
foreach ( array( 'pricing', 'destinations', 'tags', 'reviews', 'images' ) as $key ) {
|
||||
if ( empty( $merged[ $key ] ) && ! empty( $summary[ $key ] ) ) {
|
||||
$merged[ $key ] = $summary[ $key ];
|
||||
}
|
||||
}
|
||||
return $merged;
|
||||
}
|
||||
|
||||
/**
|
||||
* Make sure the destination we searched is present on the product so its
|
||||
* taxonomy term is assigned even if the payload omits it.
|
||||
*/
|
||||
private function ensure_destination( array $product, $destination_id ) {
|
||||
$destinations = vc_array_get( $product, 'destinations', array() );
|
||||
if ( ! is_array( $destinations ) ) {
|
||||
$destinations = array();
|
||||
}
|
||||
foreach ( $destinations as $destination ) {
|
||||
$ref = vc_first_non_empty( array( vc_array_get( $destination, 'ref' ), vc_array_get( $destination, 'destinationId' ), is_scalar( $destination ) ? $destination : null ) );
|
||||
if ( (string) $ref === (string) $destination_id ) {
|
||||
return $product;
|
||||
}
|
||||
}
|
||||
$destinations[] = array( 'ref' => (string) $destination_id );
|
||||
$product['destinations'] = $destinations;
|
||||
return $product;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch full product detail for an already-imported post and merge it into
|
||||
* the stored summary. Used for lazy, on-view enrichment so we only pay the
|
||||
* per-product request for tours that are actually viewed.
|
||||
*
|
||||
* @return bool True on success.
|
||||
*/
|
||||
public function enrich_from_detail( $post_id, $product_code ) {
|
||||
$product_code = sanitize_text_field( (string) $product_code );
|
||||
if ( '' === $product_code ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$detail = $this->api->get_product( $product_code );
|
||||
if ( is_wp_error( $detail ) || ! is_array( $detail ) ) {
|
||||
if ( is_wp_error( $detail ) ) {
|
||||
VC_Logger::log( 'Detail enrichment failed (' . $product_code . '): ' . $detail->get_error_message() );
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
$this->load_dictionaries();
|
||||
|
||||
$summary = vc_json_decode_meta( get_post_meta( $post_id, '_vc_raw_response', true ), array() );
|
||||
$product = is_array( $summary ) && ! empty( $summary ) ? $this->merge_detail( $summary, $detail ) : $detail;
|
||||
|
||||
$inactive = $this->is_product_inactive( $product );
|
||||
$this->save_tour_meta( $post_id, $product, $inactive );
|
||||
$this->assign_taxonomies( $post_id, $product );
|
||||
$this->save_image_urls( $post_id, $product );
|
||||
update_post_meta( $post_id, '_vc_detail_fetched', current_time( 'mysql' ) );
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public function import_product_by_code( $product_code ) {
|
||||
$product_code = sanitize_text_field( $product_code );
|
||||
if ( '' === $product_code ) {
|
||||
return new WP_Error( 'vc_missing_product_code', __( 'Product code is required.', 'viator-collector' ) );
|
||||
}
|
||||
$product = $this->api->get_product( $product_code );
|
||||
if ( is_wp_error( $product ) ) {
|
||||
return $product;
|
||||
}
|
||||
$this->load_dictionaries();
|
||||
return $this->create_or_update_tour( $product, $this->is_product_inactive( $product ) );
|
||||
}
|
||||
|
||||
public function find_existing_tour( $product_code ) {
|
||||
$query = new WP_Query(
|
||||
array(
|
||||
'post_type' => 'viator_tour',
|
||||
'post_status' => 'any',
|
||||
'posts_per_page' => 1,
|
||||
'fields' => 'ids',
|
||||
'no_found_rows' => true,
|
||||
'update_post_meta_cache' => false,
|
||||
'update_post_term_cache' => false,
|
||||
'meta_query' => array(
|
||||
array(
|
||||
'key' => '_vc_viator_product_code',
|
||||
'value' => sanitize_text_field( (string) $product_code ),
|
||||
),
|
||||
),
|
||||
)
|
||||
);
|
||||
|
||||
return ! empty( $query->posts ) ? (int) $query->posts[0] : 0;
|
||||
}
|
||||
|
||||
public function create_or_update_tour( array $product, $inactive = false ) {
|
||||
$product_code = vc_first_non_empty( array( vc_array_get( $product, 'productCode' ), vc_array_get( $product, 'code' ) ) );
|
||||
if ( '' === (string) $product_code ) {
|
||||
return new WP_Error( 'vc_missing_product_code', __( 'Product data is missing a Viator product code.', 'viator-collector' ) );
|
||||
}
|
||||
|
||||
$title = vc_first_non_empty( array( vc_array_get( $product, 'title' ), vc_array_get( $product, 'name' ), $product_code ) );
|
||||
$desc = vc_first_non_empty( array( vc_array_get( $product, 'description' ), vc_array_get( $product, 'overview' ) ) );
|
||||
$short = vc_first_non_empty( array( vc_array_get( $product, 'shortDescription' ), vc_array_get( $product, 'summary' ) ) );
|
||||
|
||||
if ( $inactive ) {
|
||||
$status = 'draft';
|
||||
} else {
|
||||
$status = vc_get_option( 'default_post_status', 'publish' );
|
||||
if ( ! in_array( $status, array( 'publish', 'draft', 'pending' ), true ) ) {
|
||||
$status = 'publish';
|
||||
}
|
||||
}
|
||||
|
||||
$postarr = array(
|
||||
'post_type' => 'viator_tour',
|
||||
'post_title' => wp_strip_all_tags( $title ),
|
||||
'post_content' => wp_kses_post( $desc ),
|
||||
'post_excerpt' => wp_kses_post( $short ),
|
||||
'post_status' => $status,
|
||||
);
|
||||
|
||||
$post_id = $this->find_existing_tour( $product_code );
|
||||
if ( $post_id ) {
|
||||
$postarr['ID'] = $post_id;
|
||||
$result = wp_update_post( wp_slash( $postarr ), true );
|
||||
} else {
|
||||
$result = wp_insert_post( wp_slash( $postarr ), true );
|
||||
}
|
||||
|
||||
if ( is_wp_error( $result ) ) {
|
||||
return $result;
|
||||
}
|
||||
|
||||
$post_id = (int) $result;
|
||||
update_post_meta( $post_id, '_vc_viator_product_code', sanitize_text_field( (string) $product_code ) );
|
||||
$this->save_tour_meta( $post_id, $product, $inactive );
|
||||
$this->assign_taxonomies( $post_id, $product );
|
||||
$this->save_image_urls( $post_id, $product );
|
||||
|
||||
return $post_id;
|
||||
}
|
||||
|
||||
public function save_tour_meta( $post_id, array $product, $inactive = false ) {
|
||||
$price = vc_first_non_empty(
|
||||
array(
|
||||
vc_array_get( $product, 'pricing.summary.fromPrice' ),
|
||||
vc_array_get( $product, 'price.fromPrice' ),
|
||||
vc_array_get( $product, 'fromPrice' ),
|
||||
)
|
||||
);
|
||||
$currency = vc_first_non_empty(
|
||||
array(
|
||||
vc_array_get( $product, 'pricing.currency' ),
|
||||
vc_array_get( $product, 'currency' ),
|
||||
vc_get_option( 'currency', 'USD' ),
|
||||
)
|
||||
);
|
||||
$duration_minutes = vc_first_non_empty(
|
||||
array(
|
||||
vc_array_get( $product, 'duration.fixedDurationInMinutes' ),
|
||||
vc_array_get( $product, 'duration.variableDurationFromMinutes' ),
|
||||
vc_array_get( $product, 'durationMinutes' ),
|
||||
)
|
||||
);
|
||||
$duration_text = vc_first_non_empty(
|
||||
array(
|
||||
vc_array_get( $product, 'duration.displayDuration' ),
|
||||
vc_array_get( $product, 'durationText' ),
|
||||
)
|
||||
);
|
||||
$booking_url = vc_first_non_empty(
|
||||
array(
|
||||
vc_array_get( $product, 'productUrl' ),
|
||||
vc_array_get( $product, 'bookingUrl' ),
|
||||
vc_array_get( $product, 'bookingLink' ),
|
||||
)
|
||||
);
|
||||
$supplier = vc_first_non_empty(
|
||||
array(
|
||||
vc_array_get( $product, 'supplier.name' ),
|
||||
vc_array_get( $product, 'supplierName' ),
|
||||
)
|
||||
);
|
||||
$rating = vc_first_non_empty(
|
||||
array(
|
||||
vc_array_get( $product, 'reviews.combinedAverageRating' ),
|
||||
vc_array_get( $product, 'rating' ),
|
||||
)
|
||||
);
|
||||
$review_count = vc_first_non_empty(
|
||||
array(
|
||||
vc_array_get( $product, 'reviews.totalReviews' ),
|
||||
vc_array_get( $product, 'reviewCount' ),
|
||||
)
|
||||
);
|
||||
|
||||
// Marketing flags (FREE_CANCELLATION, SPECIAL_OFFER, LIKELY_TO_SELL_OUT, …).
|
||||
$flags = vc_array_get( $product, 'flags', array() );
|
||||
$flags = is_array( $flags ) ? array_values( array_map( 'sanitize_text_field', $flags ) ) : array();
|
||||
|
||||
// Maximum group size (present in full product detail only).
|
||||
$max_travelers = vc_first_non_empty(
|
||||
array(
|
||||
vc_array_get( $product, 'bookingRequirements.maxTravelersPerBooking' ),
|
||||
vc_array_get( $product, 'bookingRequirements.maxTravelers' ),
|
||||
vc_array_get( $product, 'maxTravelers' ),
|
||||
)
|
||||
);
|
||||
|
||||
$meta = array(
|
||||
'_vc_booking_url' => esc_url_raw( $booking_url ),
|
||||
'_vc_price_from' => $price,
|
||||
'_vc_currency' => sanitize_text_field( $currency ),
|
||||
'_vc_duration_minutes' => absint( $duration_minutes ),
|
||||
'_vc_duration_text' => sanitize_text_field( $duration_text ),
|
||||
'_vc_itinerary' => wp_json_encode( vc_array_get( $product, 'itinerary', array() ) ),
|
||||
'_vc_inclusions' => wp_json_encode( vc_array_get( $product, 'inclusions', array() ) ),
|
||||
'_vc_exclusions' => wp_json_encode( vc_array_get( $product, 'exclusions', array() ) ),
|
||||
'_vc_additional_info' => wp_json_encode( vc_array_get( $product, 'additionalInfo', array() ) ),
|
||||
'_vc_cancellation_policy' => wp_json_encode( vc_first_non_empty( array( vc_array_get( $product, 'cancellationPolicy' ), vc_array_get( $product, 'cancellation' ) ), array() ) ),
|
||||
'_vc_supplier' => sanitize_text_field( $supplier ),
|
||||
'_vc_destinations' => wp_json_encode( vc_array_get( $product, 'destinations', array() ) ),
|
||||
'_vc_tags' => wp_json_encode( vc_array_get( $product, 'tags', array() ) ),
|
||||
'_vc_rating' => sanitize_text_field( (string) $rating ),
|
||||
'_vc_review_count' => absint( $review_count ),
|
||||
'_vc_flags' => wp_json_encode( $flags ),
|
||||
'_vc_free_cancellation' => in_array( 'FREE_CANCELLATION', $flags, true ) ? 1 : 0,
|
||||
'_vc_max_travelers' => absint( $max_travelers ),
|
||||
'_vc_is_active' => $inactive ? 0 : 1,
|
||||
'_vc_last_synced_at' => current_time( 'mysql' ),
|
||||
);
|
||||
|
||||
foreach ( $meta as $key => $value ) {
|
||||
update_post_meta( $post_id, $key, $value );
|
||||
}
|
||||
|
||||
// The full API response is always stored so any field can be surfaced later.
|
||||
update_post_meta( $post_id, '_vc_raw_response', wp_json_encode( $product ) );
|
||||
}
|
||||
|
||||
public function assign_taxonomies( $post_id, array $product ) {
|
||||
$destination_ids = array();
|
||||
foreach ( (array) vc_array_get( $product, 'destinations', array() ) as $destination ) {
|
||||
$ref = vc_first_non_empty(
|
||||
array(
|
||||
vc_array_get( $destination, 'ref' ),
|
||||
vc_array_get( $destination, 'destinationId' ),
|
||||
is_scalar( $destination ) ? $destination : null,
|
||||
)
|
||||
);
|
||||
if ( '' === (string) $ref ) {
|
||||
continue;
|
||||
}
|
||||
$name = isset( $this->destination_map[ (string) $ref ] ) ? $this->destination_map[ (string) $ref ] : '';
|
||||
$term_id = VC_Taxonomies::get_or_create_destination_by_id( $ref, $name );
|
||||
if ( $term_id ) {
|
||||
$destination_ids[] = (int) $term_id;
|
||||
}
|
||||
}
|
||||
|
||||
$tag_ids = array();
|
||||
foreach ( (array) vc_array_get( $product, 'tags', array() ) as $tag ) {
|
||||
$id = is_scalar( $tag ) ? $tag : vc_first_non_empty( array( vc_array_get( $tag, 'tagId' ), vc_array_get( $tag, 'id' ) ) );
|
||||
if ( '' === (string) $id ) {
|
||||
continue;
|
||||
}
|
||||
$name = isset( $this->tag_map[ (string) $id ] ) ? $this->tag_map[ (string) $id ] : '';
|
||||
$term_id = VC_Taxonomies::get_or_create_tag_by_id( $id, $name );
|
||||
if ( $term_id ) {
|
||||
$tag_ids[] = (int) $term_id;
|
||||
}
|
||||
}
|
||||
|
||||
wp_set_object_terms( $post_id, array_values( array_unique( $destination_ids ) ), 'viator_destination', false );
|
||||
wp_set_object_terms( $post_id, array_values( array_unique( $tag_ids ) ), 'viator_tag', false );
|
||||
}
|
||||
|
||||
/**
|
||||
* Store image URLs (and a primary URL) without downloading anything.
|
||||
*/
|
||||
public function save_image_urls( $post_id, array $product ) {
|
||||
$images = vc_first_non_empty(
|
||||
array(
|
||||
vc_array_get( $product, 'images', array() ),
|
||||
vc_array_get( $product, 'media.images', array() ),
|
||||
),
|
||||
array()
|
||||
);
|
||||
|
||||
$urls = array();
|
||||
$primary = '';
|
||||
foreach ( (array) $images as $image ) {
|
||||
$url = $this->best_image_url( $image );
|
||||
if ( '' === $url ) {
|
||||
continue;
|
||||
}
|
||||
$urls[] = $url;
|
||||
if ( '' === $primary && ( is_scalar( $image ) || vc_array_get( $image, 'isCover' ) ) ) {
|
||||
$primary = $url;
|
||||
}
|
||||
}
|
||||
|
||||
$urls = array_values( array_unique( array_filter( $urls ) ) );
|
||||
if ( '' === $primary && ! empty( $urls ) ) {
|
||||
$primary = $urls[0];
|
||||
}
|
||||
|
||||
update_post_meta( $post_id, '_vc_gallery_image_urls', wp_json_encode( $urls ) );
|
||||
update_post_meta( $post_id, '_vc_primary_image_url', esc_url_raw( $primary ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Pick the highest-resolution variant available for an image entry.
|
||||
*/
|
||||
private function best_image_url( $image ) {
|
||||
if ( is_string( $image ) ) {
|
||||
return esc_url_raw( $image );
|
||||
}
|
||||
if ( ! is_array( $image ) ) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$variants = vc_array_get( $image, 'variants', array() );
|
||||
if ( is_array( $variants ) && ! empty( $variants ) ) {
|
||||
$best = '';
|
||||
$best_width = -1;
|
||||
foreach ( $variants as $variant ) {
|
||||
$url = vc_array_get( $variant, 'url' );
|
||||
if ( ! $url ) {
|
||||
continue;
|
||||
}
|
||||
$width = (int) vc_array_get( $variant, 'width', 0 );
|
||||
if ( $width >= $best_width ) {
|
||||
$best_width = $width;
|
||||
$best = $url;
|
||||
}
|
||||
}
|
||||
if ( '' !== $best ) {
|
||||
return esc_url_raw( $best );
|
||||
}
|
||||
}
|
||||
|
||||
$url = vc_first_non_empty(
|
||||
array(
|
||||
vc_array_get( $image, 'url' ),
|
||||
vc_array_get( $image, 'large' ),
|
||||
vc_array_get( $image, 'medium' ),
|
||||
vc_array_get( $image, 'imageSource' ),
|
||||
)
|
||||
);
|
||||
return $url ? esc_url_raw( $url ) : '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect whether the feed reports this product as no longer bookable.
|
||||
*/
|
||||
private function is_product_inactive( array $product ) {
|
||||
$status = strtoupper( (string) vc_first_non_empty( array( vc_array_get( $product, 'status' ), vc_array_get( $product, 'productStatus' ) ) ) );
|
||||
if ( '' !== $status && in_array( $status, array( 'INACTIVE', 'DEACTIVATED', 'EXPIRED', 'REMOVED', 'DELETED' ), true ) ) {
|
||||
return true;
|
||||
}
|
||||
if ( false === vc_array_get( $product, 'active', null ) ) {
|
||||
return true;
|
||||
}
|
||||
if ( vc_array_get( $product, 'isDeactivated', false ) ) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user