Current File : //var/www/vinorea/modules/klaviyopsautomation/classes/KlaviyoUtils.php
<?php

/**
 * Klaviyo
 *
 * NOTICE OF LICENSE
 *
 * This source file is subject to the Commercial License
 * you can't distribute, modify or sell this code
 *
 * DISCLAIMER
 *
 * Do not edit or add to this file
 * If you need help please contact extensions@klaviyo.com
 *
 * @author    Klaviyo
 * @copyright Klaviyo
 * @license   commercial
 */

namespace KlaviyoPs\Classes;

if (!defined('_PS_VERSION_')) {
    exit;
}

use Cart;
use Configuration;
use DateTime;
use Context;
use Db;
use Exception;
use Image;
use Link;
use Product;
use Validate;
use KlaviyoV3Sdk\Exception\KlaviyoException;
use KlaviyoPs\Classes\BusinessLogicServices\OrderPayloadService;
use KlaviyoPs\Classes\BusinessLogicServices\ProductPayloadService;
use libphonenumber\PhoneNumberFormat;
use libphonenumber\PhoneNumberUtil;

/**
 * Class KlaviyoUtils is a collection of utility methods reused across multiple KlaviyoPs module classes.
 * @package KlaviyoPs\Classes
 */
class KlaviyoUtils
{
    /**
     * List of rewrite rules for Authorization header fix.
     * @var string[]
     */
    const HTACCESS_REWRITE_RULES = [
        'SetEnvIf Authorization "(.*)" HTTP_AUTHORIZATION=$1',
        'RewriteCond %{HTTP:Authorization} ^(.*)',
        'RewriteRule . - [E=HTTP_AUTHORIZATION:%1]',
    ];

    /**
     * Get full url for product image based on attribute ID, otherwise get cover image.
     *
     * @param $productId
     * @return string
     */
    public static function getProductImageLink($product_id, $product_attribute_id, $shop_id, $lang_id)
    {
        if ($product_attribute_id) {
            $image = Image::getBestImageAttribute(
                $shop_id,
                $lang_id,
                $product_id,
                $product_attribute_id
            );
        }
        // Some product attributes don't correspond to a different image e.g. size. Handle that here as well.
        if (!isset($image) || !$image) {
            $image = Image::getCover($product_id);
        }

        if (is_array($image) && isset($image['id_image'])) {
            $image = new Image($image['id_image']);
            return _PS_BASE_URL_ . _THEME_PROD_DIR_ . $image->getExistingImgPath() . ".jpg";
        }
    }

    /**
     * Build object containing values compiled from and including cart line items e.g. unique categories.
     *
     * @param Cart $cart
     * @return array
     * @throws \PrestaShopDatabaseException
     * @throws \PrestaShopException
     */
    public static function buildCartLineItemsArray($cart)
    {
        $langId = $cart->id_lang;
        $shopId = $cart->id_shop;

        // Define total cart variables.
        $itemCount = 0;
        $itemNames = array();
        $lineItems = array();
        $productCategories = array();
        $productTags = array();

        $products = $cart->getProducts();
        foreach ($products as $product) {
            $productId = $product['id_product'];
            $productObj = new Product($productId, false, $id_lang = $langId, $id_shop = $shopId);

            foreach (Product::getProductCategoriesFull($id_product = $productId, $id_lang = $langId) as $category) {
                $category_name = $category['name'];
                if (!in_array($category_name, $productCategories)) {
                    $productCategories[] = $category_name;
                }
            };

            $tags = ProductPayloadService::getProductTagsArray($productId, $langId);
            foreach ($tags as $tag) {
                if (!in_array($tag, $productTags)) {
                    $productTags[] = $tag;
                }
            }

            $itemNames[] = $product['name'];
            $lineItems[] = array(
                'Image' => KlaviyoUtils::getProductImageLink($productId, $product['id_product_attribute'], $shopId, $langId),
                'ProductURL' => ProductPayloadService::getProductUrl($productId, $langId, $shopId, $product['id_product_attribute']),
                'ProductID' => $productId,
                'Price' => number_format($product['price'], 2),
                'Quantity' => $product['quantity'],
                'ProductUniqueID' => $product['unique_id'],
                'ConstructedVariantID' => self::formatKlaviyoVariantIdentifier($product['id_product'], $product['id_product_attribute']),
                'ProductInfo' => $product,
                'Tags' => $tags,
            );

            $itemCount += (int) $product['quantity'];
        };

        return array(
            'lineItems' => $lineItems,
            'itemNames' => $itemNames,
            'itemCount' => $itemCount,
            'uniqueCategories' => $productCategories,
            'uniqueTags' => $productTags
        );
    }

    /**
     * Build a token to secure the recovery procedure of a cart
     *
     * @param int $idCart
     * @return string
     */
    public static function buildRecoverCartToken($idCart)
    {
        $key = _COOKIE_KEY_;
        return md5("{$key}recover_cart_{$idCart}");
    }

    /**
     * Build url to reclaim cart.
     *
     * @param Cart $cart
     * @return string
     */
    public static function buildReclaimCartUrl(Cart $cart)
    {
        $context = Context::getContext();

        $link = $context->link;
        if ($link === null) {
            $link = new Link();
        }

        $idCart = (int)$cart->id;
        $idShop = (int)$cart->id_shop;
        $idLang = (int)$cart->id_lang;
        $token = self::buildRecoverCartToken($idCart);

        return $link->getModuleLink(
            'klaviyo',
            'reclaim',
            [
                'id_cart' => $idCart,
                'id_shop' => $idShop,
                'token' => $token,
            ],
            null,
            ($idLang > 0) ? $idLang : null,
            ($idShop > 0) ? $idShop : null
        );
    }

    /**
     * Get order status mappings for all stores where configured.
     *
     * @return array
     * @throws \PrestaShopDatabaseException
     */
    public static function getAllOrderStatusMaps()
    {
        $sql = 'SELECT `id_shop`, `value`, `date_add`, `date_upd` FROM ' . _DB_PREFIX_ . 'configuration WHERE `name` = "KLAVIYO_ORDER_STATUS_MAP"';
        $result = Db::getInstance()->ExecuteS($sql);

        $statusMaps = array();
        foreach ($result as $statusMap) {
            $statusMaps[$statusMap['id_shop']] = array(
                'map' => json_decode($statusMap['value'], true),
                'date_add' => $statusMap['date_add'],
                'date_upd' => $statusMap['date_upd'],
            );
        }
        return $statusMaps;
    }

    /**
     * Returns the corresponding event type for an order state e.g. klaviyops-statuses-refunded.
     *
     * @param int $OrderStateId
     * @param int|null $shopId
     * @return string|null
     */
    public static function getMappedOrderStatusValue(
        $OrderStateId,
        $shopId = null
    ) {
        $json = Configuration::get(
            'KLAVIYO_ORDER_STATUS_MAP',
            null,
            null,
            $shopId
        );

        $statusMap = null;
        if ($json !== false) {
            $statusMap = json_decode($json, true);
        }

        if (!$statusMap) {
            $statusMap = OrderPayloadService::ORDER_STATUS_MAP_DEFAULT;
        }

        $mappedStatus = null;
        foreach ($statusMap as $eventType => $statusIds) {
            if (in_array($OrderStateId, $statusIds)) {
                $mappedStatus = $eventType;
                break;
            }
        }

        return $mappedStatus;
    }

    /**
     * Converts numeric value to formatted string. Non-numeric return "0.00".
     *
     * @param $price
     * @return string
     */
    public static function formatPrice($price)
    {
        return number_format(is_numeric($price) ? $price : 0, 2, '.', '');
    }

    /**
     * Format date from PrestaShop format to timestamp format to send it to Klaviyo
     *
     * @param string|null $date
     * @param int|null $defaultTime
     * @return int|null
     */
    public static function formatDate($date, $defaultTime = null)
    {
        try {
            if (
                $date === null ||
                !Validate::isDate($date) ||
                $date === '0000-00-00'
            ) {
                throw new KlaviyoException();
            }

            $date = new DateTime($date);

            $minimumDate = new DateTime('2000-01-01');
            if ($minimumDate > $date) {
                throw new KlaviyoException();
            }

            return $date->getTimestamp();
        } catch (Exception $e) {
            return $defaultTime;
        }
    }

    /**
     * Helper function to determine whether an array has non-integer keys.
     *
     * @param array $array
     * @return bool
     */
    public static function hasStringKeys(array $array)
    {
        return count(array_filter(array_keys($array), 'is_string')) > 0;
    }

    /**
     * Whether to warn a customer about the potential for php running as CGI to
     * cause API authentication issues from Authorization header being stripped.
     *
     * @return bool
     */
    public static function shouldWarnCGI()
    {
        // Check htaccess for HTTP_AUTHORIZATION
        $path = _PS_ROOT_DIR_ . '/.htaccess';
        if (!file_exists($path)) {
            return false;
        }

        $content = file_get_contents($path);
        foreach (self::HTACCESS_REWRITE_RULES as $rule) {
            if (is_int(strpos($content, $rule))) {
                return false;
            }
        }

        // Check if php is CGI
        $sapi_type = php_sapi_name();
        if (!is_string($sapi_type)) {
            return false;
        }
        // strpos will either return the position of the substring or false.
        // If the value is an int, we know cgi is in the sapi name and we should warn the user.
        return is_int(strpos($sapi_type, 'cgi'));
    }

    /**
     * Format phone number to international format like +12345678901
     *
     * @param  string $phone
     * @param  string $isoCode
     * @return string
     */
    public static function formatPhone($phone, $isoCode = '')
    {
        if (empty($phone) || empty($isoCode)) {
            return $phone;
        }

        try {
            $phoneUtil = PhoneNumberUtil::getInstance();
            $parsed = $phoneUtil->parse($phone, $isoCode);

            if (!$phoneUtil->isValidNumber($parsed)) {
                return $phone;
            }

            $formated = $phoneUtil->format($parsed, PhoneNumberFormat::E164);
            return $formated;
        } catch (Exception $e) {
            return $phone;
        }
    }

    /**
     * Format a unique identifier for products/variants to be referenced in Klaviyo's
     * catalog. Combination products will use the `id_product_attribute` value as the
     * variant ID e.g. '19-4' while simple products will use 0 e.g. '18-0'.
     *
     * @param $parentId
     * @param $variantId
     * @return string
     */
    public static function formatKlaviyoVariantIdentifier($parentId, $variantId = 0)
    {
        return "{$parentId}-{$variantId}";
    }

    /**
     * Format identifier for Klaviyo catalogs. This is used to link events to a
     * specific scoped catalog in Klaviyo, so that profile events can be
     * connected to a specific scoped product when building flow audiences.
     *
     * @param $shopId
     * @param $langId
     * @return string
     */
    public static function formatKlaviyoCatalogIdentifier($shopId, $langId)
    {
        return "{$shopId}:{$langId}";
    }
}