PluginProbe
Lasso Lite – Affiliate Link Manager & Product Displays / 158
Lasso Lite – Affiliate Link Manager & Product Displays v158
158 157 155 156 154 153 152 151 150 149 148 trunk 0.9.9 104 105 106 107 108 109 110 111 112 113 114 115 All 57 releases
← All changes | classes/class-amazon-api.php +669 -52 113158 View file →
@@ -6,8 +6,9 @@
6 6 */
7 7
8 8 namespace LassoLite\Classes;
9 9
10 +use LassoLite\Admin\Constant;
10 11 use LassoLite\Classes\Affiliate_Link;
11 12 use LassoLite\Classes\Cache_Per_Process;
12 13 use LassoLite\Classes\Helper;
13 14 use LassoLite\Classes\Setting;
@@ -13,8 +14,10 @@
13 14 use LassoLite\Classes\Setting;
14 15
15 16 use LassoLite\Libs\Amazon_Api_V5\AwsV5;
16 17
18 +use LassoLite\Models\Amazon_Products;
19 +
17 20 /**
18 21 * Config
19 22 */
20 23 class Amazon_Api {
@@ -25,8 +28,9 @@
25 28 const SHORT_LINK_DOMAINS = array( 'amzn.com', 'amzn.to' );
26 29 const FILTER_AMAZON_PRODUCT = 'filter_amazon_product';
27 30 const TRACKING_ID_REGEX = '^[a-zA-Z0-9-]+-\d{2,3}$';
28 31 const CURRENCY_ISO = array( 'USD', 'AUD', 'CAD', 'EUR', 'MXN', 'CNY', 'JPY', 'INR', 'SEK', 'BRL', 'TRY', 'GBP', 'PLN', 'EGP', 'SGD', 'AED' );
32 + const VARIATION_PAGE_LIMIT = 2;
29 33
30 34 /**
31 35 * Get amazon API countries
32 36 */
@@ -396,9 +400,9 @@
396 400 * Check whether a url is amazon link or not
397 401 *
398 402 * @param string $url Url.
399 403 */
400 - public static function is_amazon_shortented_url( $url ) {
404 + public static function is_amazon_shortened_url( $url ) {
401 405 $is_amazon_url = self::is_amazon_url( $url );
402 406 $is_shortented_url = strpos( $url, 'amzn.to' ) || strpos( $url, 'amzn.com' );
403 407
404 408 return $is_amazon_url && $is_shortented_url;
@@ -453,18 +457,38 @@
453 457 * @param string $product_id Amazon product id.
454 458 * @param bool $store_product Store product into DB or not. Default to false.
455 459 * @param bool|string $updated_at Set date time or not. Default to false.
456 460 * @param string $amz_link Amazon link. Default to empty.
461 + * @param bool $url_version_param Is add version param to request api url to ignore cache. Default to false.
462 + * @param bool $force_bls Force fetch product from BLS or not. Default to false.
463 + * @param bool $refresh_image Bypass cache and fetch fresh product image/metadata. Default to false.
457 464 * @return mixed
458 465 */
459 - public function fetch_product_info( $product_id, $store_product = false, $updated_at = false, $amz_link = '' ) {
466 + public function fetch_product_info( $product_id, $store_product = false, $updated_at = false, $amz_link = '', $url_version_param = false, $force_bls = false, $refresh_image = false ) {
460 467 $lasso_db = new Lasso_DB();
461 468
462 469 $lasso_settings = Setting::get_settings();
463 470 $is_amazon_configured = $lasso_settings['amazon_access_key_id'] && $lasso_settings['amazon_secret_key'] && $lasso_settings['amazon_tracking_id'];
464 - $result = $is_amazon_configured && $this->is_same_domain( $amz_link ) ? $this->get_product_by_id_v5( $product_id ) : false;
471 +
472 + if (
473 + self::is_amazon_creators_verified( $lasso_settings )
474 + && $this->is_same_domain( $amz_link )
475 + ) {
476 + $creators_result = $this->fetch_product_info_from_creators_api(
477 + $product_id,
478 + $store_product,
479 + $updated_at,
480 + $amz_link,
481 + $lasso_settings
482 + );
483 + if ( null !== $creators_result ) {
484 + return $creators_result;
485 + }
486 + }
487 +
488 + $result = ! $force_bls && $is_amazon_configured && $this->is_same_domain( $amz_link ) ? $this->get_product_by_id_v5( $product_id ) : false;
465 489 // phpcs:ignore
466 - if ( isset( $result->Errors[0] ) && (
490 + if ( is_object( $result ) && isset( $result->Errors[0] ) && (
467 491 "The ItemId $product_id provided in the request is invalid." === $result->Errors[0]->Message // phpcs:ignore
468 492 || "The value [$product_id] provided in the request for ItemIds is invalid." === $result->Errors[0]->Message // phpcs:ignore
469 493 )
470 494 ) {
@@ -474,9 +498,9 @@
474 498 'full_item' => array(),
475 499 'status' => 'failed',
476 500 'error_code' => 'NotFound',
477 501 );
478 - } elseif ( isset( $result->ItemsResult->Items[0] ) ) { // phpcs:ignore
502 + } elseif ( is_object( $result ) && isset( $result->ItemsResult->Items[0] ) ) { // phpcs:ignore
479 503 $item = $result->ItemsResult->Items[0]; // phpcs:ignore
480 504
481 505 $product = $this->extract_search_result_v5( $item, true );
482 506 $product_url = $amz_link ? $amz_link : $product['url'];
@@ -482,22 +506,18 @@
482 506 $product_url = $amz_link ? $amz_link : $product['url'];
483 507 $product['status_code'] = 200;
484 508 $product['url'] = self::get_amazon_product_url( $product_url );
485 509
486 - // ? Get Lasso ID
487 - $query = '
488 - SELECT post_id
489 - FROM ' . $lasso_db->postmeta . "
490 - WHERE meta_key = 'amazon_product_id' AND meta_value = '" . $product['product_id'] . "'
491 - ";
492 - $lasso_id = $lasso_db->get_var( $query );
493 - $product['lasso_id'] = ( isset( $lasso_id ) ) ? $lasso_id : 0;
510 + // ? If $item->Offers is missing, we try to get the quantity from variation data
511 + if ( ! isset( $item->Offers ) ) { // phpcs:ignore
512 + sleep( 1 ); // ? Delay for a while before call the next request
513 + $variation_product = $this->get_product_variation( $product_id, $product_url );
514 + if ( $variation_product && isset( $variation_product['quantity'] ) && $variation_product['quantity'] ) {
515 + $product = $variation_product;
516 + }
517 + }
494 518
495 - if ( $store_product ) {
496 - $amazon_tracking_id = Setting::get_setting( 'amazon_tracking_id', '' );
497 - $product['default_url'] = '' === $amazon_tracking_id ? $amz_link : $product['default_url'];
498 - $this->update_amazon_product_in_db( $product, $updated_at );
499 - }
519 + $product = $this->enrich_fetched_amazon_product( $product, $store_product, $updated_at, $amz_link );
500 520
501 521 return array(
502 522 'product' => $product,
503 523 'api' => 'yes',
@@ -504,20 +524,152 @@
504 524 'full_item' => $item,
505 525 'status' => 'success',
506 526 'error_code' => '',
507 527 );
528 + } elseif ( '' !== $lasso_settings['license_serial'] || $force_bls ) {
529 + list( $product, $status ) = $this->fetch_product_from_bls( $product_id, $store_product, $updated_at, $amz_link, $url_version_param, $force_bls, $refresh_image );
530 +
531 + return array(
532 + 'product' => $product,
533 + 'api' => 'no',
534 + 'full_item' => array(),
535 + 'status' => 200 === $status ? 'success' : 'fail',
536 + 'error_code' => 404 === $status ? 'NotFound' : '',
537 + );
538 + }
539 +
540 + return array(
541 + 'product' => array(),
542 + 'api' => 'no',
543 + 'full_item' => array(),
544 + 'status' => 'failed',
545 + 'error_code' => 'NotFound',
546 + );
547 + }
548 +
549 + /**
550 + * Fetch amazon product from BLS (Lambda)
551 + *
552 + * @param string $product_id Amazon product id.
553 + * @param bool $store_product Store product into DB or not. Default to false.
554 + * @param bool|string $updated_at Set date time or not. Default to false.
555 + * @param string $amz_link Amazon link. Default to empty.
556 + * @param bool $url_version_param Is add version param to request api url to ignore cache. Default to false.
557 + * @param bool $force_bls Force fetch product from BLS or not. Default to false.
558 + * @param bool $refresh_image Bypass cache and fetch fresh product image/metadata. Default to false.
559 + */
560 + public function fetch_product_from_bls( $product_id, $store_product = false, $updated_at = false, $amz_link = '', $url_version_param = false, $force_bls = false, $refresh_image = false ) {
561 + $url = strpos( $amz_link, 'amazon.' ) !== false ? $amz_link : $this->get_amazon_link_by_product_id( $product_id, $amz_link );
562 + $m_link = self::get_amazon_product_url( $url );
563 + $url = self::get_amazon_product_url( $url, false );
564 +
565 + $amazon_product = array(
566 + 'title' => '',
567 + 'image' => '',
568 + 'url' => $m_link,
569 + 'price' => '',
570 + 'currency' => '',
571 + 'savings_amount' => 0,
572 + 'savings_percent' => 0,
573 + 'savings_basis' => 0,
574 + );
575 +
576 + try {
577 + $res = Helper::get_url_status_code_by_broken_link_service( $url, true, false, $url_version_param, $force_bls, $refresh_image );
578 + } catch ( \Throwable $e ) {
579 + $res = array(
580 + 'status_code' => 500,
581 + 'response' => null,
582 + );
583 + }
584 +
585 + $bls_response = ( isset( $res['response'] ) && is_object( $res['response'] ) ) ? $res['response'] : null;
586 + $bls_status = ( null !== $bls_response && isset( $bls_response->status ) ) ? intval( $bls_response->status ) : 0;
587 +
588 + if ( 200 === $res['status_code'] && 200 === $bls_status ) {
589 + $img_url = $bls_response->imgUrl ?? '';
590 + $img_url = '' !== $img_url ? $img_url : '';
591 + $product_name = $bls_response->productName ?? '';
592 + $product_name = '' === $product_name ? ( $bls_response->pageTitle ?? '' ) : $product_name;
593 + $quantity = $bls_response->quantity ?? 200;
594 + $price = $bls_response->price ?? '';
595 + $product_id = self::get_product_id_by_url( $url );
596 +
597 + $temp_url = $bls_response->finalUrl ?? $url;
598 + $url = '' !== $temp_url ? $temp_url : $url;
599 + $m_link = self::get_amazon_product_url( $amz_link ? $amz_link : $url );
600 +
601 + if ( strpos( $product_name, 'Amazon.com:' ) === 0 ) {
602 + $product_name = str_replace( 'Amazon.com:', '', $product_name );
603 + $product_name = trim( $product_name );
604 + }
605 +
606 + if ( $product_id && $store_product ) {
607 + $store_data = array(
608 + 'product_id' => $product_id,
609 + 'title' => $product_name,
610 + 'price' => $price,
611 + 'default_url' => $url,
612 + 'url' => $m_link,
613 + 'image' => trim( $img_url ),
614 + 'quantity' => intval( $quantity ), // Manual checks won't show out of stock for now. TODO: Add BLS to out of stock checks.
615 + 'is_manual' => 1,
616 + );
617 +
618 + // ? Set additional data for amazon product
619 + if ( isset( $bls_response->additionalData ) && ! empty( $bls_response->additionalData ) ) {
620 + $basis_price = $bls_response->additionalData->basis_price ?? '';
621 + $basis_price = Helper::get_price_value_from_price_text( $basis_price );
622 + $savings_amount = $bls_response->additionalData->saving_amount ?? '';
623 + $savings_amount = Helper::get_price_value_from_price_text( $savings_amount );
624 +
625 + $store_data['currency'] = $bls_response->additionalData->currency_name ?? '';
626 + $store_data['savings_basis'] = $basis_price;
627 + $store_data['savings_amount'] = $savings_amount;
628 + $store_data['savings_percent'] = $bls_response->additionalData->saving_amount_percent ?? '';
629 +
630 + $amazon_product['currency'] = $store_data['currency'];
631 + $amazon_product['savings_basis'] = $store_data['savings_basis'];
632 + $amazon_product['savings_amount'] = $store_data['savings_amount'];
633 + $amazon_product['savings_percent'] = $store_data['savings_percent'];
634 + }
635 +
636 + $this->update_amazon_product_in_db( $store_data, $updated_at, true ); // allow_partial: BLS may omit imgUrl.
637 + }
638 +
639 + $amazon_product['title'] = $product_name;
640 + $amazon_product['image'] = $img_url;
641 + $amazon_product['url'] = $m_link;
642 + $amazon_product['price'] = $price;
643 + $amazon_product['quantity'] = $quantity;
644 + $amazon_product['status_code'] = $bls_status;
645 + }
646 + if ( 404 === $res['status_code'] || ( 200 === $res['status_code'] && 404 === $bls_status ) ) {
647 + $amz_model = new Amazon_Products();
648 + $last_updated = gmdate( 'Y-m-d H:i:s', time() );
649 + $amz_model->update_amazon_field( $product_id, 'last_updated', $last_updated );
650 + $amz_model->update_amazon_field( $product_id, 'out_of_stock', 0 );
651 + }
652 +
653 + if ( $bls_status ) {
654 + $status = $bls_status;
655 + } elseif ( 200 === intval( $res['status_code'] ?? 0 ) ) {
656 + $status = 500;
508 657 } else {
509 - return array();
658 + $status = intval( $res['status_code'] ?? 500 );
510 659 }
660 +
661 + return array( $amazon_product, intval( $status ) );
511 662 }
512 663
513 664 /**
514 665 * Insert or Update Amazon Product Data
515 666 *
516 - * @param array $product Amazon product.
517 - * @param bool|string $updated_at Set update date time. Default to false.
667 + * @param array $product Amazon product.
668 + * @param bool|string $updated_at Set update date time. Default to false.
669 + * @param bool $allow_partial When true, allow empty image (BLS may omit imgUrl). Default false.
518 670 */
519 - public function update_amazon_product_in_db( $product, $updated_at = false ) {
671 + public function update_amazon_product_in_db( $product, $updated_at = false, $allow_partial = false ) {
520 672 global $wpdb;
521 673
522 674 $lasso_db = new Lasso_DB();
523 675
@@ -539,9 +691,11 @@
539 691 $is_manual = $product['is_manual'] ?? 0;
540 692 $quantity = intval( $product['quantity'] ?? 200 );
541 693 $out_of_stock = 0 === $quantity ? 1 : 0;
542 694
543 - if ( '' === $amazon_id || '' === $default_product_name || '' === $default_image
695 + $image_required = ! $allow_partial;
696 + if ( '' === $amazon_id || '' === $default_product_name
697 + || ( $image_required && '' === $default_image )
544 698 || ( '' !== $default_image && Helper::validate_url( $default_image ) === false && strpos( $default_image, 'data:image' ) !== 0 )
545 699 ) {
546 700 return false;
547 701 }
@@ -663,13 +817,14 @@
663 817 * Extract result to an array
664 818 *
665 819 * @param object $response Data from Amazon.
666 820 * @param bool $large_image Get large image size. Default to false.
821 + * @param string $product_id Product Id. Default to empty.
822 + * @param string $product_url Product url. Default to empty.
667 823 *
668 824 * @return array
669 825 */
670 - private function extract_search_result_v5( $response, $large_image = false ) {
671 -
826 + private function extract_search_result_v5( $response, $large_image = false, $product_id = '', $product_url = '' ) {
672 827 $image = '';
673 828 if ( isset( $response->Images->Primary ) ) { // phpcs:ignore
674 829 $image = $large_image ? $response->Images->Primary->Large->URL : $response->Images->Primary->Small->URL; // phpcs:ignore
675 830 }
@@ -675,11 +830,11 @@
675 830 }
676 831
677 832 // @codingStandardsIgnoreStart
678 833 $result = array(
679 - 'product_id' => $response->ASIN ?? 0,
834 + 'product_id' => $product_id ? $product_id : ( $response->ASIN ?? 0 ),
680 835 'title' => $response->ItemInfo->Title->DisplayValue ?? '',
681 - 'url' => $response->DetailPageURL ?? '',
836 + 'url' => $product_url ? $product_url : ( $response->DetailPageURL ?? '' ),
682 837 'default_url' => $response->DetailPageURL ?? '',
683 838 'image' => $image,
684 839 'quantity' => $response->Offers->Summaries[0]->OfferCount ?? 0,
685 840 'is_prime' => $response->Offers->Listings[0]->DeliveryInfo->IsPrimeEligible ?? false,
@@ -869,8 +1024,44 @@
869 1024 return isset( $matches[1] ) && ! empty( $matches[1] ) ? $matches[1] : false;
870 1025 }
871 1026
872 1027 /**
1028 + * Keep URL arguments for Amazon URL
1029 + *
1030 + * @param string $product_url Amazon product url.
1031 + *
1032 + * @return array $results
1033 + */
1034 + private static function keep_args( $product_url ) {
1035 + // ? amazon link but it is not product url
1036 + if ( ! self::is_amazon_url( $product_url ) || self::is_amazon_shortened_url( $product_url ) ) {
1037 + return array();
1038 + }
1039 +
1040 + $results = array();
1041 + $url_params = array(
1042 + 'maas',
1043 + 'ref_',
1044 + 's',
1045 + 'aa_campaignid',
1046 + 'aa_creativeid',
1047 + 'aa_adgroupid',
1048 + 'campaignId',
1049 + 'linkCode',
1050 + 'linkId',
1051 + );
1052 +
1053 + foreach ( $url_params as $param_name ) {
1054 + $param_value = Helper::get_argument_from_url( $product_url, $param_name );
1055 + if ( $param_value ) {
1056 + $results[] = $param_name . '=' . $param_value;
1057 + }
1058 + }
1059 +
1060 + return $results;
1061 + }
1062 +
1063 + /**
873 1064 * Get amazon tracking id by url
874 1065 * This function will be deprecated in the future. Please use Helper::get_argument_from_url() instead
875 1066 *
876 1067 * @param string $link Amazon link.
@@ -884,8 +1075,18 @@
884 1075 return $matches[2] ?? '';
885 1076 }
886 1077
887 1078 /**
1079 + * Clean Amazon URL that contains maas parameter
1080 + *
1081 + * @param string $product_url Amazon product URL.
1082 + * @return string
1083 + */
1084 + public static function clean_maas_url( $product_url ) {
1085 + return $product_url;
1086 + }
1087 +
1088 + /**
888 1089 * Make product url to shorten url if this setting is enabled
889 1090 *
890 1091 * @param string $product_url Amazon product url.
891 1092 * @param bool $monetize Monetize link or not. Default to true.
@@ -893,62 +1094,67 @@
893 1094 * @param string $custom_tracking_id Allow to use custom tracking id.
894 1095 */
895 1096 public static function get_amazon_product_url( $product_url, $monetize = true, $check_lasso_post = true, $custom_tracking_id = '' ) {
896 1097 // ? amazon link but it is not product url
897 - if ( ! self::is_amazon_url( $product_url ) || self::is_amazon_shortented_url( $product_url ) ) {
1098 + if ( ! self::is_amazon_url( $product_url ) || self::is_amazon_shortened_url( $product_url ) ) {
898 1099 return $product_url;
899 1100 }
900 1101
1102 + // If MAAS param exists and the original tag is exactly "maas", leave the URL untouched
1103 + $__maas_arg = Helper::get_argument_from_url( $product_url, 'maas' );
1104 + $__orig_tag = Helper::get_argument_from_url( $product_url, 'tag' );
1105 + if ( $__maas_arg && strtolower( $__orig_tag ) === 'maas' ) {
1106 + return $product_url;
1107 + }
1108 +
901 1109 $product_id = self::get_product_id_by_url( $product_url );
902 1110
903 - $tag = Helper::get_argument_from_url( $product_url, 'tag' );
904 - $maas = Helper::get_argument_from_url( $product_url, 'maas' );
905 - $ref_ = Helper::get_argument_from_url( $product_url, 'ref_' );
906 - $s_args = Helper::get_argument_from_url( $product_url, 's' );
907 -
908 - $maas_args = $maas ? 'maas=' . $maas : '';
909 - $ref__args = $ref_ ? 'ref_=' . $ref_ : '';
910 - $s_args = $s_args ? 's=' . $s_args : '';
911 -
912 1111 // ? remove all url queries, just keep needed args
913 1112 $url_without_params = explode( '?', $product_url )[0];
914 1113 if ( $product_id && ! $monetize ) {
915 - $args = Helper::build_url_parameter_string( array( $maas_args, $ref__args, $s_args ) );
1114 + $keep_args = self::keep_args( $product_url );
1115 + $args = Helper::build_url_parameter_string( $keep_args );
916 1116
917 - return $args ? $url_without_params . '?' . $args : $url_without_params;
1117 + $product_url = $args ? $url_without_params . '?' . $args : $url_without_params;
1118 + $product_url = self::clean_maas_url( $product_url );
1119 +
1120 + return $product_url;
918 1121 }
919 1122
920 - $lasso_settings = Setting::get_settings();
921 - $amazon_multiple_tracking_id = $lasso_settings['amazon_multiple_tracking_id'] ?? true;
922 - $amazon_tracking_id_whitelist = $lasso_settings['amazon_tracking_id_whitelist'] ?? array();
923 - $amazon_tracking_id = trim( $lasso_settings['amazon_tracking_id'] ?? '' );
1123 + $lasso_settings = Setting::get_settings();
1124 + $amazon_tracking_id = trim( $lasso_settings['amazon_tracking_id'] ?? '' );
1125 + $amazon_add_tracking_id_to_attribution = $lasso_settings['amazon_add_tracking_id_to_attribution'] ?? true;
924 1126
925 - if ( ! $amazon_multiple_tracking_id ) {
926 - $amazon_tracking_id_whitelist = array();
927 - }
928 -
929 1127 $lasso_db = new Lasso_DB();
930 1128 $amz_cache_key = self::OBJECT_KEY . '_' . self::FUNCTION_NAME_GET_LASSO_ID_BY_PRODUCT_ID_AND_TYPE . '_' . $product_id . '_' . self::PRODUCT_TYPE;
931 1129 $lasso_id = Cache_Per_Process::get_instance()->get_cache( $amz_cache_key, null );
932 1130 if ( null === $lasso_id ) {
933 - $lasso_id = $lasso_db->get_lasso_id_by_product_id_and_type( $product_id );
1131 + $lasso_id = $lasso_db->get_lasso_id_by_product_id_and_type( $product_id, self::PRODUCT_TYPE );
934 1132 Cache_Per_Process::get_instance()->set_cache( $amz_cache_key, $lasso_id );
935 1133 }
936 1134
937 1135 if ( $check_lasso_post && ! $lasso_id ) {
1136 + $product_url = self::clean_maas_url( $product_url );
938 1137 return $product_url;
939 1138 }
940 1139
1140 + $tag = Helper::get_argument_from_url( $product_url, 'tag' );
941 1141 $tag = $custom_tracking_id ? $custom_tracking_id : $tag;
942 1142 $tag = $tag ? $tag : '';
943 - $tag = ! empty( $amazon_tracking_id ) && ! in_array( $tag, $amazon_tracking_id_whitelist, true )
944 - ? $amazon_tracking_id : $tag;
1143 + $tag = ! empty( $tag ) ? $tag : $amazon_tracking_id;
945 1144
946 1145 // ? Return the remove all url queries for product url
947 1146 if ( $product_id ) {
948 - $tag_args = $tag ? 'tag=' . $tag : '';
949 - $args = Helper::build_url_parameter_string( array( $tag_args, $maas_args, $ref__args, $s_args ) );
1147 + $tag_args = $tag ? 'tag=' . $tag : '';
1148 + $keep_args = self::keep_args( $product_url );
1149 + $maas = Helper::get_argument_from_url( $product_url, 'maas' );
1150 + $add_tag_to_url = ( $maas && $amazon_add_tracking_id_to_attribution ) || ! $maas;
950 1151
1152 + if ( $add_tag_to_url ) {
1153 + array_unshift( $keep_args, $tag_args );
1154 + }
1155 +
1156 + $args = Helper::build_url_parameter_string( $keep_args );
951 1157 $product_url = $args ? $url_without_params . '?' . $args : $url_without_params;
952 1158 } else {
953 1159 $product_url = str_replace( '&', '&', $product_url );
954 1160 $parse = wp_parse_url( $product_url );
@@ -970,8 +1176,10 @@
970 1176 $product_url = trim( $product_url );
971 1177 $product_url = trim( $product_url, '?' );
972 1178 }
973 1179
1180 + $product_url = self::clean_maas_url( $product_url );
1181 +
974 1182 return $product_url;
975 1183 }
976 1184
977 1185 /**
@@ -1181,6 +1389,415 @@
1181 1389 $currency_position = preg_match( '/[€]|R\$|TL|kr|zł/', $currency_symbol ) ? 'end' : 'begin';
1182 1390 $price_format = preg_match( '/[€]|R\$|TL|kr|zł/', $currency_symbol ) ? number_format( $price_value, 2, ',', '.' ) : number_format( $price_value, 2, '.', ',' );
1183 1391
1184 1392 return 'begin' === $currency_position ? $currency_symbol . $price_format : $price_format . $currency_symbol;
1393 + }
1394 +
1395 + /**
1396 + * Get the variation item in stock
1397 + *
1398 + * @param string $product_id Product id.
1399 + * @param string $product_url Product url.
1400 + * @param int $variation_page Variation page.
1401 + * @return array
1402 + */
1403 + public function get_product_variation( $product_id, $product_url, $variation_page = 1 ) {
1404 + $result = $this->get_product_variations_by_id_v5( $product_id, $variation_page );
1405 + $items = $result->VariationsResult->Items ?? array(); // phpcs:ignore
1406 +
1407 + if ( ! empty( $items ) ) {
1408 + $items = $result->VariationsResult->Items; // phpcs:ignore
1409 + $product_variations = array();
1410 +
1411 + // ? Get product variation list
1412 + foreach ( $items as $item ) {
1413 + $product_variations[] = $this->extract_search_result_v5( $item, true, $product_id, $product_url );
1414 + }
1415 +
1416 + // ? Sort price from lowest to highest
1417 + usort(
1418 + $product_variations,
1419 + function( $a, $b ) {
1420 + return strcmp( $a['amount'], $b['amount'] );
1421 + }
1422 + );
1423 +
1424 + // ? Get the in-stock product
1425 + foreach ( $product_variations as $product_variation ) {
1426 + if ( $product_variation['quantity'] && $product_variation['price'] ) {
1427 + return $product_variation;
1428 + }
1429 + }
1430 +
1431 + // ? If all variation products in this page unavailable, we request to the next page
1432 + $page_count = $result->VariationsResult->VariationSummary->PageCount ?? 1; // phpcs:ignore
1433 + if ( $variation_page < self::VARIATION_PAGE_LIMIT && $variation_page < $page_count ) {
1434 + sleep( 1 ); // ? Delay for a while before call the next request
1435 + return $this->get_product_variation( $product_id, $product_url, $variation_page + 1 );
1436 + }
1437 + }
1438 +
1439 + return array();
1440 + }
1441 +
1442 + /**
1443 + * Get Amazon product variations by product id
1444 + *
1445 + * @param string $product_id Amazon product id.
1446 + * @param int $variation_page Variation page.
1447 + *
1448 + * @return object
1449 + */
1450 + public function get_product_variations_by_id_v5( $product_id, $variation_page = 1 ) {
1451 + $parameters = array(
1452 + 'Operation' => 'GetVariations',
1453 + 'ASIN' => $product_id,
1454 + 'Condition' => 'New',
1455 + 'VariationPage' => $variation_page,
1456 + 'Resources' => array(
1457 + 'Images.Primary.Small',
1458 + 'Images.Primary.Large',
1459 + 'ItemInfo.Title',
1460 + 'ItemInfo.ContentRating',
1461 + 'ItemInfo.Features',
1462 + 'ItemInfo.ProductInfo',
1463 + 'ItemInfo.TechnicalInfo',
1464 + 'Offers.Listings.Price',
1465 + 'Offers.Listings.SavingBasis',
1466 + 'Offers.Summaries.OfferCount',
1467 + 'Offers.Listings.DeliveryInfo.IsPrimeEligible',
1468 + ),
1469 + );
1470 +
1471 + $json_response = $this->query_amazon_v5( $parameters );
1472 +
1473 + return $json_response;
1474 + }
1475 +
1476 + /**
1477 + * Build discount pricing html.
1478 + *
1479 + * @param string $latest_price Latest price.
1480 + * @param mixed $basis_price Basis price value.
1481 + * @param string $currency Currency ISO.
1482 + */
1483 + public static function build_discount_pricing_html( $latest_price, $basis_price, $currency ) {
1484 + $result = '';
1485 +
1486 + try {
1487 + $latest_price_value = Helper::get_price_value_from_price_text( $latest_price );
1488 + $basis_price_value = Helper::get_price_value_from_price_text( $basis_price );
1489 +
1490 + if ( $basis_price_value && ( round( $latest_price_value, 2 ) < round( $basis_price_value, 2 ) ) ) {
1491 + $currency_symbol = Helper::get_currency_symbol_from_iso_code( $currency );
1492 + $currency_position = preg_match( '/[€]|R\$|TL|kr|zł/', $latest_price ) ? 'end' : 'begin';
1493 + $basis_price_format = preg_match( '/[€]|R\$|TL|kr|zł/', $latest_price ) ? number_format( $basis_price_value, 2, ',', '.' ) : number_format( $basis_price_value, 2, '.', ',' );
1494 + $format_price = 'begin' === $currency_position ? $currency_symbol . $basis_price_format : $basis_price_format . ' ' . $currency_symbol;
1495 +
1496 + $result = "<strike>$format_price</strike>";
1497 + }
1498 + } catch ( \Exception $e ) {
1499 + $result = '';
1500 + }
1501 +
1502 + return $result;
1503 + }
1504 +
1505 + /**
1506 + * Check amazon setting is configured or not
1507 + *
1508 + * @return boolean
1509 + */
1510 + public static function is_amazon_setting_configured() {
1511 + $lasso_settings = Setting::get_settings();
1512 + $is_amazon_configured = $lasso_settings['amazon_access_key_id'] && $lasso_settings['amazon_secret_key'] && $lasso_settings['amazon_tracking_id'];
1513 +
1514 + return $is_amazon_configured || self::is_amazon_creators_verified( $lasso_settings );
1515 + }
1516 +
1517 + /**
1518 + * Whether Amazon Creators API credentials are complete in settings.
1519 + *
1520 + * @param array|false $lasso_settings Settings array. Default false loads current settings.
1521 + * @return bool
1522 + */
1523 + public static function is_amazon_creators_configured( $lasso_settings = false ) {
1524 + if ( ! is_array( $lasso_settings ) ) {
1525 + $lasso_settings = Setting::get_settings();
1526 + }
1527 +
1528 + $fields = array(
1529 + 'amazon_creators_credential_id',
1530 + 'amazon_creators_secret',
1531 + 'amazon_creators_version',
1532 + 'amazon_creators_partner_tag',
1533 + );
1534 +
1535 + foreach ( $fields as $field ) {
1536 + if ( '' === trim( (string) ( $lasso_settings[ $field ] ?? '' ) ) ) {
1537 + return false;
1538 + }
1539 + }
1540 +
1541 + return true;
1542 + }
1543 +
1544 + /**
1545 + * Whether Creators credentials were verified successfully (signature matches stored settings).
1546 + *
1547 + * @param array|false $lasso_settings Settings array. Default false loads current settings.
1548 + * @return bool
1549 + */
1550 + public static function is_amazon_creators_verified( $lasso_settings = false ) {
1551 + if ( ! self::is_amazon_creators_configured( $lasso_settings ) ) {
1552 + return false;
1553 + }
1554 +
1555 + if ( ! is_array( $lasso_settings ) ) {
1556 + $lasso_settings = Setting::get_settings();
1557 + }
1558 +
1559 + $current_signature = self::get_amazon_creators_signature( $lasso_settings );
1560 + $saved_signature = (string) Helper::get_option( Constant::LASSO_OPTION_AMAZON_CREATORS_VERIFIED_SIGNATURE, '' );
1561 +
1562 + return '' !== $current_signature && '' !== $saved_signature && hash_equals( $saved_signature, $current_signature );
1563 + }
1564 +
1565 + /**
1566 + * Build a stable signature for the Creators payload.
1567 + *
1568 + * @param array $settings Settings values.
1569 + * @return string Empty when Creators credentials are incomplete.
1570 + */
1571 + public static function get_amazon_creators_signature( $settings ) {
1572 + if ( ! self::is_amazon_creators_configured( $settings ) ) {
1573 + return '';
1574 + }
1575 +
1576 + return hash(
1577 + 'sha256',
1578 + wp_json_encode(
1579 + array(
1580 + 'credential_id' => (string) ( $settings['amazon_creators_credential_id'] ?? '' ),
1581 + 'secret' => (string) ( $settings['amazon_creators_secret'] ?? '' ),
1582 + 'credential_version' => (string) ( $settings['amazon_creators_version'] ?? '' ),
1583 + 'partner_tag' => (string) ( $settings['amazon_creators_partner_tag'] ?? '' ),
1584 + 'country' => (string) ( $settings['amazon_default_tracking_country'] ?? '' ),
1585 + )
1586 + )
1587 + );
1588 + }
1589 +
1590 + /**
1591 + * Resolve Creators API country from an Amazon URL or default tracking country.
1592 + *
1593 + * @param string $amazon_url Amazon product URL (optional).
1594 + * @param array|false $lasso_settings Settings array. Default false loads current settings.
1595 + * @return string
1596 + */
1597 + public static function get_country_for_creators_api( $amazon_url, $lasso_settings = false ) {
1598 + if ( ! is_array( $lasso_settings ) ) {
1599 + $lasso_settings = Setting::get_settings();
1600 + }
1601 +
1602 + if ( $amazon_url && self::is_amazon_url( $amazon_url ) ) {
1603 + $base_domain = strtolower( Helper::get_base_domain( $amazon_url ) );
1604 + if ( '' !== $base_domain ) {
1605 + foreach ( self::get_amazon_api_countries() as $country_code => $country_data ) {
1606 + $amazon_domain = strtolower( Helper::get_base_domain( (string) ( $country_data['amazon_domain'] ?? '' ) ) );
1607 + if ( $base_domain === $amazon_domain ) {
1608 + return (string) $country_code;
1609 + }
1610 + }
1611 + }
1612 + }
1613 +
1614 + return (string) ( $lasso_settings['amazon_default_tracking_country'] ?? '' );
1615 + }
1616 +
1617 + /**
1618 + * Whether the Lite install is linked to a Lasso account email.
1619 + *
1620 + * @return bool
1621 + */
1622 + public static function is_lite_account_connected() {
1623 + $email = Helper::get_option( Constant::LASSO_ACCOUNT_EMAIL, '' );
1624 +
1625 + return is_string( $email ) && '' !== trim( $email );
1626 + }
1627 +
1628 + /**
1629 + * Fetch product data via direct Amazon Creators API (replaces deprecated PA-API GetItems).
1630 + *
1631 + * @param string $product_id Amazon ASIN.
1632 + * @param bool $store_product Store in local DB.
1633 + * @param bool|string $updated_at Optional updated timestamp.
1634 + * @param string $amz_link Amazon URL.
1635 + * @param array $lasso_settings Plugin settings.
1636 + * @return array|null Same shape as fetch_product_info success/failure, or null to fall back.
1637 + */
1638 + private function fetch_product_info_from_creators_api( $product_id, $store_product, $updated_at, $amz_link, $lasso_settings ) {
1639 + if ( ! self::is_amazon_creators_configured( $lasso_settings ) ) {
1640 + return null;
1641 + }
1642 +
1643 + $api_result = Amazon_Creators_Api::fetch_product(
1644 + $product_id,
1645 + self::get_country_for_creators_api( $amz_link, $lasso_settings ),
1646 + array(
1647 + 'product_only' => false,
1648 + )
1649 + );
1650 +
1651 + if ( 'fail' === ( $api_result['status'] ?? '' ) ) {
1652 + $error_code = (string) ( $api_result['error_code'] ?? '' );
1653 +
1654 + if ( 'NotFound' === $error_code ) {
1655 + return $this->build_fetch_product_info_response( array(), 'yes', array(), 'failed', 'NotFound' );
1656 + }
1657 +
1658 + return null;
1659 + }
1660 +
1661 + $product_data = $api_result['product'] ?? array();
1662 + if ( ! is_array( $product_data ) || empty( $product_data ) ) {
1663 + return null;
1664 + }
1665 +
1666 + $full_item = $api_result['item'] ?? array();
1667 + $product = $this->finalize_fetch_product_info_product(
1668 + $product_data,
1669 + $product_id,
1670 + $store_product,
1671 + $updated_at,
1672 + $amz_link
1673 + );
1674 +
1675 + return $this->build_fetch_product_info_response( $product, 'yes', $full_item, 'success', '' );
1676 + }
1677 +
1678 + /**
1679 + * Build fetch_product_info() return array (same contract as get_product_by_id_v5 path).
1680 + *
1681 + * @param array $product Product fields.
1682 + * @param string $api API source flag.
1683 + * @param array|object $full_item Raw item payload.
1684 + * @param string $status success|failed.
1685 + * @param string $error_code Error code.
1686 + * @return array
1687 + */
1688 + private function build_fetch_product_info_response( $product, $api, $full_item, $status, $error_code ) {
1689 + return array(
1690 + 'product' => $product,
1691 + 'api' => $api,
1692 + 'full_item' => $full_item,
1693 + 'status' => $status,
1694 + 'error_code' => $error_code,
1695 + );
1696 + }
1697 +
1698 + /**
1699 + * Apply the same post-processing as the PA-API branch in fetch_product_info().
1700 + *
1701 + * @param array $product Product fields from API.
1702 + * @param string $product_id Amazon ASIN.
1703 + * @param bool $store_product Store in local DB.
1704 + * @param bool|string $updated_at Optional updated timestamp.
1705 + * @param string $amz_link Amazon URL.
1706 + * @return array
1707 + */
1708 + private function finalize_fetch_product_info_product( $product, $product_id, $store_product, $updated_at, $amz_link ) {
1709 + if ( empty( $product['product_id'] ) ) {
1710 + $product['product_id'] = $product_id;
1711 + }
1712 +
1713 + return $this->enrich_fetched_amazon_product( $product, $store_product, $updated_at, $amz_link );
1714 + }
1715 +
1716 + /**
1717 + * Shared post-fetch enrichment for PA-API and Creators product payloads.
1718 + *
1719 + * @param array $product Product fields.
1720 + * @param bool $store_product Store in local DB.
1721 + * @param bool|string $updated_at Optional updated timestamp.
1722 + * @param string $amz_link Amazon URL.
1723 + * @return array
1724 + */
1725 + private function enrich_fetched_amazon_product( $product, $store_product, $updated_at, $amz_link ) {
1726 + global $wpdb;
1727 +
1728 + $lasso_db = new Lasso_DB();
1729 + $product_url = $amz_link ? $amz_link : ( $product['url'] ?? '' );
1730 + $product['status_code'] = 200;
1731 + $product['url'] = self::get_amazon_product_url( $product_url );
1732 +
1733 + $amazon_product_id = (string) ( $product['product_id'] ?? '' );
1734 + $query = $wpdb->prepare(
1735 + 'SELECT post_id FROM ' . $lasso_db->postmeta . " WHERE meta_key = 'amazon_product_id' AND meta_value = %s",
1736 + $amazon_product_id
1737 + );
1738 + $lasso_id = $lasso_db->get_var( $query );
1739 + $product['lasso_id'] = ( isset( $lasso_id ) ) ? $lasso_id : 0;
1740 +
1741 + if ( $store_product ) {
1742 + $amazon_tracking_id = Setting::get_setting( 'amazon_tracking_id', '' );
1743 + $product['default_url'] = '' === $amazon_tracking_id ? $amz_link : ( $product['default_url'] ?? '' );
1744 + $this->update_amazon_product_in_db( $product, $updated_at );
1745 + }
1746 +
1747 + return $product;
1748 + }
1749 +
1750 + /**
1751 + * Get final url of the amazon shortlink from cache
1752 + *
1753 + * @param string $shortlink Amazon shortlink.
1754 + * @return string|null
1755 + */
1756 + public static function get_shortlink_final_url_cached( $shortlink ) {
1757 + if ( ! self::is_amazon_shortened_url( $shortlink ) ) {
1758 + return null;
1759 + }
1760 + $final_url_cache = get_option( self::build_shortlink_cache_key( $shortlink ) );
1761 +
1762 + if ( $final_url_cache === $shortlink ) {
1763 + return null;
1764 + }
1765 +
1766 + return $final_url_cache ? $final_url_cache : null;
1767 + }
1768 +
1769 + /**
1770 + * Build amazon shortlink cache key
1771 + *
1772 + * @param string $shortlink Amazon shortlink.
1773 + * @return string|null
1774 + */
1775 + public static function build_shortlink_cache_key( $shortlink ) {
1776 + if ( ! self::is_amazon_shortened_url( $shortlink ) ) {
1777 + return null;
1778 + }
1779 +
1780 + $parse = wp_parse_url( $shortlink );
1781 + $host = str_replace( '.', '_', $parse['host'] );
1782 + $id = trim( $parse['path'] ?? '', '/' );
1783 +
1784 + return $host . '_' . $id;
1785 + }
1786 +
1787 + /**
1788 + * Format Amazon URLs
1789 + *
1790 + * @param string $url Amazon product url.
1791 + * @return string URL.
1792 + */
1793 + public static function format_amazon_url( $url ) {
1794 + $is_amazon_link = self::is_amazon_url( $url );
1795 + $product_id = self::get_product_id_by_url( $url );
1796 +
1797 + if ( $is_amazon_link && $product_id && strpos( $url, 'smile.amazon.' ) !== false ) {
1798 + $url = str_replace( 'smile.amazon.', 'amazon.', $url );
1799 + }
1800 +
1801 + return $url;
1185 1802 }
1186 1803 }