Skip to content

Instantly share code, notes, and snippets.

@joshmoto
Created April 12, 2023 16:40
Show Gist options
  • Select an option

  • Save joshmoto/28adfa359834022cc6e30ff07b8ce494 to your computer and use it in GitHub Desktop.

Select an option

Save joshmoto/28adfa359834022cc6e30ff07b8ce494 to your computer and use it in GitHub Desktop.

Revisions

  1. joshmoto created this gist Apr 12, 2023.
    813 changes: 813 additions & 0 deletions PricingV2.lib.php
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,813 @@
    <?php

    use JetBrains\PhpStorm\NoReturn;

    /**
    * @author Josh Cranwell <[email protected]>
    * @copyright The Sweet People
    * @version 1.0
    * @link https://www.thesweetpeople.com/
    * @since March 2023
    */

    class PricingV2 {

    public static
    string $default_currency = 'gbp';

    public static
    array $pricing = [
    'uk' => 'United Kingdom',
    'eu' => 'Europe'
    ];

    public static
    array $currency = [
    'gbp' => 'GBP - British Pound',
    'eur' => 'EUR - Euro'
    ];

    public static
    array|bool $error_logs = false;

    public function __construct ()
    {

    // get default currency
    $this->default_currency();

    // loadding shipping form service
    add_action('wp_ajax_nopriv_load_pricing_table', [ $this, 'ajax_load_pricing_table' ], 20 );
    add_action('wp_ajax_load_pricing_table', [ $this, 'ajax_load_pricing_table' ], 20 );

    // loadding load json price breaks
    add_action('wp_ajax_nopriv_load_json_price_breaks', [ $this, 'ajax_load_json_price_breaks' ], 20 );
    add_action('wp_ajax_load_json_price_breaks', [ $this, 'ajax_load_json_price_breaks' ], 20 );

    }

    /**
    * @param mixed $field
    * @param string $message
    * @return void
    */
    public static function error_logger(mixed $field, string $message): void
    {

    // if field is empty
    if(!$field) {

    // set error log
    self::$error_logs[] = $message;

    }

    }

    /**
    * @return void
    */
    #[NoReturn] public function ajax_load_pricing_table(): void
    {

    // set data array
    $data = [
    'product_id' => $_GET['product_id'] ? (int)$_GET['product_id'] : false,
    'company_id' => $_GET['company_id'] ? (int)$_GET['company_id'] : false,
    'country' => $_GET['country'] ?? false,
    'service' => $_GET['service'] ?? false,
    'currency' => $_GET['currency'] ?? false,
    'markup_unit' => $_GET['markup_unit'] ? (float)$_GET['markup_unit'] : false,
    'markup_shipping' => $_GET['markup_shipping'] ? (float)$_GET['markup_shipping'] : false,
    'markup_origination' => $_GET['markup_origination'] ? (float)$_GET['markup_origination'] : false,
    'custom_qty' => $_GET['custom_qty'] ? (int)$_GET['custom_qty'] : false,
    'custom_unit' => $_GET['custom_unit'] ? (float)$_GET['custom_unit'] : false,
    'custom_origination' => $_GET['custom_origination'] ? (float)$_GET['custom_origination'] : false
    ];

    // update data with new data
    $data = self::data($data);

    // start our object buffer
    ob_start();

    // load our pricing table component
    CF::component('products/single/pricing-table-v2',[ 'data' => $data ],false);

    // get our buffer
    $html = ob_get_contents();

    // clean our buffer
    ob_end_clean();

    // if we have errors
    if(isset($data['error']) && is_array($data['error'])) {

    // send json error
    wp_send_json_error($html);

    } else {

    // else send json success
    wp_send_json_success($html);

    }

    // death is the beginning
    die();

    }

    /**
    * @param $data array
    * @return array
    */
    public static function data(array $data): array
    {

    // get pricing
    $data = self::get_pricing_data($data);

    // get unit weight
    $data = Shipping::get_unit_weight($data);

    // get packaging
    $data = Shipping::get_packaging_id($data);

    // get parcel data
    $data = Shipping::get_parcel_data($data);

    // get pallet data
    $data = Shipping::get_pallet_data($data);

    // get individual shipping
    //$data = Shipping::get_individual($data);

    // if we have errors return the logs
    if(self::$error_logs) {

    // return the error logs
    return [ 'error' => self::$error_logs ];

    }

    // calculate the data if no errors
    return self::calculate($data);

    }

    /**
    * @param array $data
    * @return array
    */
    public static function calculate(array $data): array
    {

    // if we have a price breaks
    if(isset($data['price_breaks']) && is_array($data['price_breaks'])) {

    // loop through price breaks and update price break
    foreach ($data['price_breaks'] as &$break) {

    // update the unit price to correct currency
    //$break['unit_price'] = self::currency($break['unit_price'],$data['currency']);

    // get shipping data
    $shipping = self::shipping($break['quantity'],$data);

    // set shipping data
    $break['shipping_weight_parcel'] = $shipping['weight_parcel'] ?? false;
    $break['shipping_weight_consignment'] = $shipping['weight_consignment'] ?? false;
    $break['shipping_parcels'] = $shipping['parcels'] ?? false;
    $break['shipping_pallets'] = $shipping['pallets'] ?? false;
    $break['shipping_method'] = $shipping['method'] ?? false;
    $break['shipping_price'] = $shipping['price'] ?? false;
    $break['shipping_price_consignment'] = $shipping['price_consignment'] ?? false;
    $break['shipping_price_total'] = $shipping['price_total'] ?? false;

    // set the total price
    $break['total_price'] = $break['shipping_price_total'] !== 'POA' ? self::round_up_currency(($break['unit_price'] * $break['quantity']) + $data['origination'] + $break['shipping_price_total']) : 'POA';

    }

    }

    // return data
    return $data;

    }

    /**
    * @param int|float $quantity
    * @param array $data
    * @return array
    */
    public static function shipping(int|float $quantity, array $data): array
    {

    // set default unit weight for qty unit measurement
    $unit_weight = $data['unit_weight'];

    // if unit measure is set and is equal too kg
    if(isset($data['unit_measure']) && $data['unit_measure'] === 'kg') {

    // convert quantity to weight kg units
    $quantity = ( $quantity * 1000 ) / $unit_weight;

    }

    // shipping data array
    $shipping = [];

    // get the number of parcels
    $parcels = ceil($quantity / $data['parcel_max_items']);

    // get parcel box est weight
    $parcel_box_est_weight = Shipping::$settings['globals'][$data['pricing']]['parcel_box_est_weight'];

    // set the shipping parcel weight
    $shipping['weight_parcel'] = $parcel_box_est_weight + (min($data['parcel_max_items'],$quantity) * $unit_weight);

    // set the total consignment weight
    $shipping['weight_consignment'] = ($parcels * $parcel_box_est_weight) + ($quantity * $unit_weight);

    // if we have a parcel outer id
    if(isset($data['parcel_outer_id']) && $data['parcel_outer_id']) {

    // if parcels is greater than 1
    if($parcels > 1) {

    // update the outer parcel est weight by 3
    $parcel_box_est_weight = $parcel_box_est_weight * 3;

    // get the shipping outer parcel weight
    $shipping['weight_parcel'] = $parcel_box_est_weight + (min(( $data['parcel_max_items'] * 2 ),$quantity) * $unit_weight);

    // get the number of outer parcels
    $parcels = ceil($parcels / 2);

    }

    }

    // set the number of parcels
    $shipping['parcels'] = $parcels;

    // get pallet min parcels
    $pallet_min_parcels = Shipping::$settings['globals'][$data['pricing']]['pallet_min_parcels'];

    // if parcels is greater than or equal to pallet min parcels
    if($parcels >= $pallet_min_parcels) {

    // get the number of pallets
    $pallets = ceil($parcels / $data['pallet_parcels_max']);

    // set the number of pallets
    $shipping['pallets'] = $pallets;

    // set the shipping method to pallet
    $shipping['method'] = 'pallet';

    // get the pallet shipping
    $pallet_service = Shipping::$settings['countries'][$data['country']]['services'][$data['service']]['method']['pallet'] ?? false;

    // get the pallet shipping price
    $pallet_price = $pallet_service['price_customer'] ?? false;

    // get the pallet consignment shipping price
    $pallet_consignment = $pallet_service['price_consignment'] ?? false;

    // if we have a pallet price and pallet consignment as strings
    if(is_string($pallet_price) && is_string($pallet_consignment)) {

    // set the pallet shipping price
    $shipping['price'] = self::currency($pallet_price,$data['currency']);

    // set the pallet consignment shipping price
    $shipping['price_consignment'] = self::currency($pallet_consignment,$data['currency']);

    // set the pallet shipping price
    $shipping['price_total'] = self::markup(($shipping['price'] * $pallets) + $shipping['price_consignment'],$data['markup_shipping']);

    // return shipping data
    return $shipping;

    }

    // return price on application
    $shipping['price_total'] = 'POA';

    // return shipping data
    return $shipping;

    }

    // set the number of pallets
    $shipping['pallets'] = 0;

    // set the shipping method to pallet
    $shipping['method'] = 'parcel';

    // get the parcel shipping
    $parcel_service = Shipping::$settings['countries'][$data['country']]['services'][$data['service']]['method']['parcel'] ?? false;

    // get the parcel shipping price
    $parcel_price = $parcel_service['price_customer'] ?? false;

    // get the parcel consignment shipping price
    $parcel_consignment = $parcel_service['price_consignment'] ?? false;

    // if we have a parcel price and parcel consignment as strings
    if(is_string($parcel_price) && is_string($parcel_consignment)) {

    // set the parcel shipping price
    $shipping['price'] = self::currency($parcel_price,$data['currency']);

    // set the parcel consignment shipping price
    $shipping['price_consignment'] = self::currency($parcel_consignment,$data['currency']);

    // set the parcel shipping price total
    $shipping['price_total'] = self::markup(($shipping['price'] * $parcels) + $shipping['price_consignment'],$data['markup_shipping']);

    // return shipping data
    return $shipping;

    }

    // return price on application
    $shipping['price_total'] = 'POA';

    // return shipping data
    return $shipping;

    }

    /**
    * @param array $price_breaks
    * @param array $data
    * @return array
    */
    public static function breaks_floatvals(array $price_breaks, array $data): array
    {

    // if we have custom qty and custom unit
    if($data['custom_qty'] && $data['custom_unit']) {

    // set our custom price break
    $price_breaks = [
    [
    "quantity" => $data['custom_qty'],
    "unit_price" => $data['custom_unit']
    ]
    ];

    } else {

    // loop through price breaks and convert break values to float
    foreach ($price_breaks as &$break) {
    $break["quantity"] = floatval($break["quantity"]);
    //$break["unit_price"] = floatval(self::markup($break["unit_price"],$data['markup_unit']));
    $break["unit_price"] = self::currency(self::markup($break["unit_price"],$data['markup_unit']),$data['currency']);
    }

    }

    // return price breaks
    return $price_breaks;

    }

    /**
    * @param array $data
    * @return array|bool
    */
    public static function get_pricing_data(array $data): array|bool
    {

    // if data country is set and is not empty
    if(isset($data['country']) && $data['country']) {

    // get the quanity measurement type
    $unit_measure = get_field('product_weight_kg',$data['product_id']);

    // if we have a measure
    $data['unit_measure'] = $unit_measure ? 'kg' : 'qty';

    // get the origination promo setting
    $promo_origination = get_field('promo_origination_charges','option');

    // if company id is set and is false
    if(isset($data['company_id']) && !$data['company_id']) {

    // get the company id
    $data['company_id'] = Company::id(User::id());

    }

    // get the shipping country pricing setting
    switch (Shipping::$settings['countries'][$data['country']]['pricing'])
    {
    case 'uk':

    // update data with pricing UK code
    $data['pricing'] = 'uk';

    // get the UK origination
    $uk_origination = get_field('product_origination',$data['product_id']);

    // if promo origination is on then set origination to 0 else set it to the UK origination including any markup
    $data['origination'] = $promo_origination ? 0 : self::currency(self::markup_add($uk_origination,$data['markup_origination']),$data['currency']);

    // get UK price breaks
    $uk_price_breaks = self::get_price_breaks($data);

    // if we have UK price breaks
    if($uk_price_breaks) {

    // set the price breaks
    $data['price_breaks'] = self::breaks_floatvals($uk_price_breaks,$data);

    } else {

    // no price breaks found
    self::error_logger(false,'No UK price breaks found for <a href="'.get_edit_post_link($data['product_id']).'" target="_blank">product</a>.');

    }

    break;

    case 'eu':

    // update data with pricing EU code
    $data['pricing'] = 'eu';

    // get the EU origination
    $eu_origination = get_field('product_eu_origination',$data['product_id']);

    // if promo origination is on then set origination to 0 else set it to the EU origination
    $data['origination'] = $promo_origination ? 0 : self::currency(self::markup_add($eu_origination,$data['markup_origination']),$data['currency']);

    // get EU price breaks
    $eu_price_breaks = self::get_price_breaks($data);

    // if we have EU price breaks
    if($eu_price_breaks) {

    // set the price breaks
    $data['price_breaks'] = self::breaks_floatvals($eu_price_breaks,$data);

    } else {

    // no price breaks found
    self::error_logger(false,'No EU price breaks found for <a href="'.get_edit_post_link($data['product_id']).'" target="_blank">product</a>.');

    }

    break;

    default:

    // no regional price code found
    self::error_logger(false,'No regional pricing code found in shipping settings.');

    }

    } else {

    // no product id is set in data
    self::error_logger(false,'No shipping country is set in data.');

    }

    // if our custom origination is set and is a float
    if(is_float($data['custom_origination'])) {

    // overide the origination with custom origination no matter what
    $data['origination'] = $data['custom_origination'];

    }

    // return data
    return $data;

    }

    /**
    * @param array $data
    * @return array|bool
    */
    public static function get_price_breaks(array $data): array|bool
    {

    // if product id is set and is not empty
    if(isset($data['product_id']) && $data['product_id']) {

    // get the customer pricing
    $customer_pricing = get_field('product_custom_prices',$data['product_id']);

    // get the pricing code
    switch ($data['pricing'])
    {
    case 'uk':

    // if we have customer pricing
    if($customer_pricing) {

    // loop through customer pricing
    foreach ($customer_pricing as $customer) {

    // if customer id is set and is not empty
    if(isset($customer['customer']) && $customer['customer']) {

    // if customer id is the same as the data customer id
    if($customer['customer'] === $data['company_id']) {

    // if we have customer UK price breaks
    if(isset($customer['product_prices']) && $customer['product_prices']) {

    // return the customer UK price breaks
    return $customer['product_prices'];

    }

    }

    }

    }

    }

    // get default UK price breaks
    return get_field('product_prices',$data['product_id']);

    break;

    case 'eu':

    // if we have customer pricing
    if($customer_pricing) {

    // loop through customer pricing
    foreach ($customer_pricing as $customer) {

    // if customer id is set and is not empty
    if(isset($customer['customer']) && $customer['customer']) {

    // if customer id is the same as the data customer id
    if($customer['customer'] === $data['company_id']) {

    // if we have customer EU price breaks
    if(isset($customer['product_eu_prices']) && $customer['product_eu_prices']) {

    // return the customer EU price breaks
    return $customer['product_eu_prices'];

    }

    }

    }

    }

    }

    // check for eu price breaks
    $eu_price_breaks = get_field('product_eu_prices',$data['product_id']);

    // if we have eu price breaks
    if($eu_price_breaks) {

    // return eu price breaks
    return $eu_price_breaks;

    }

    // else return uk price breaks as fallback
    return get_field('product_prices',$data['product_id']);

    break;

    default:

    // no regional price code found
    self::error_logger(false,'No regional pricing code found in pricing data.');

    }

    }

    // return false if no return
    return false;

    }

    /**
    * @return void
    */
    public static function render_currency_select(): void
    {
    ?>
    <div class="form-group ml-auto">
    <select class="form-control" style="padding-right:2.25rem;" name="pricing_currency">
    <option value="gbp" <?=self::$default_currency==='gbp'?'selected':null?>>(£) GBP <span class="fi fi-gb"></span></option>
    <option value="eur" <?=self::$default_currency==='eur'?'selected':null?>>(€) EUR <span class="fi fi-eu"></span></option>
    </select>
    </div>
    <?php
    }

    /**
    * @return void
    */
    public function default_currency(): void
    {

    // check if we have eu shipping cookie
    if (isset($_COOKIE['pricing_currency'])) {

    // set our currency variable
    self::$default_currency = $_COOKIE['pricing_currency'];

    }

    }

    /**
    * @param int|float $value
    * @return int|float
    */
    public static function round_up_currency(int|float $value): int|float
    {

    // round up currency value to 2 decimal places
    return ceil($value * 100) / 100;



    }

    /**
    * @param int|float $value
    * @param bool|string $currency
    * @return int|float
    */
    public static function currency(int|float $value, bool|string $currency = false): int|float
    {

    // if we have a currency
    switch($currency)
    {
    case 'gbp':

    // return GBP value
    return (float)number_format($value,2,'.','');

    break;

    case 'eur':

    // get the EUR rate from shipping options
    $eur_rate = get_field('currency_rate_eur','option');

    // return EUR value
    return self::round_up_currency($value * $eur_rate);

    break;

    default:

    // return GBP value
    return (float)number_format($value,2,'.','');

    }

    }

    /**
    * @param int|float|string $value
    * @param string $currency
    * @return string
    */
    public static function value(int|float|string $value, string $currency = 'gbp'): string
    {

    // if value is a string
    if(is_string($value)) {

    // if value is price on application
    if($value === 'POA') {

    // update abbrediated price on application value
    $value = '<abbr data-toggle="tooltip" data-placement="top" data-html="true" title="<strong>Price on application</strong>" class="font-weight-bold">'.$value.'</abbr>';

    }

    // return string value
    return $value;

    } else {

    // convert value to 2 decimal place add decimal point and thousands seperator
    $value_format = number_format($value,2);

    // return matched currency with value
    return match ($currency) {
    'eur' => $value_format . '',
    default => '£' . $value_format
    };

    }

    // return value
    return $value;

    }

    /**
    * @param int|float $value
    * @param bool|float $markup
    * @return float|int
    */
    public static function markup(int|float $value, bool|float $markup): float|int
    {

    // if we have a markup
    if($markup) {

    // convert markup percentage to decimal
    $markup = (float)($markup/100)+1;

    // price markup
    $value = $value * $markup;

    }

    // return price with parse markup
    return $value;

    }

    /**
    * @param int|float $value
    * @param bool|float $markup
    * @return float|int
    */
    public static function markup_add(int|float $value, bool|float $markup): float|int
    {

    // if we have a markup
    if($markup) {

    // add markup to value
    $value = $value + $markup;

    }

    // return price with parse markup
    return $value;

    }

    /**
    * @return void
    */
    #[NoReturn] public function ajax_load_json_price_breaks(): void
    {

    // set data array
    $data = [
    'product_id' => $_GET['product_id'] ? (int)$_GET['product_id'] : false,
    'company_id' => $_GET['company_id'] ? (int)$_GET['company_id'] : false,
    'country' => $_GET['country'] ?? false,
    'currency' => $_GET['currency'] ?? false,
    'markup_unit' => false,
    'markup_shipping' => false,
    'markup_origination' => false
    ];

    // get pricing
    $data = self::get_pricing_data($data);

    // if we have errors
    if(isset($data['error']) && is_array($data['error'])) {

    // send json error
    wp_send_json_error($data);

    } else {

    // else send json success
    wp_send_json_success($data);

    }

    // death is the beginning
    die();

    }

    }

    new PricingV2();
    411 changes: 411 additions & 0 deletions QuoteV2.lib.php
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,411 @@
    <?php
    /**
    * @author Josh Cranwell <[email protected]>
    * @copyright The Sweet People
    * @version 1.0
    * @link https://www.thesweetpeople.com/
    * @since April 2023
    *
    * @property int id
    * @property string quote_hash
    * @property int user_id
    * @property int product_id
    * @property bool|int company_id
    * @property string country
    * @property string service
    * @property string currency
    * @property bool|array quote_data
    * @property string created_at
    */

    class QuotesV2
    {

    public static
    string $expiryDuration = '30 DAYS';

    protected
    $data = [],
    $isLoaded = false,
    $filterDuration = '1 DAY';

    protected function tableName(): string
    {
    global $wpdb;
    return $wpdb->prefix . 'quotes_v2';
    }

    public function __get($name)
    {

    // check if we have a special method for formatting our data
    $getterAttribute = 'get' . ucfirst($name) . 'Attribute';

    // is this a valid property
    if (property_exists($this->data, $name)) {

    // return our default data
    return $this->data->{$name};
    }

    // is this a valid method?
    if (method_exists($this, $getterAttribute)) {
    return $this->$getterAttribute();
    }

    // not found, return false
    return false;

    }

    /**
    * Forms constructor.
    */
    public function __construct()
    {

    global $wpdb;

    // create table if not already created
    $wpdb->query("CREATE TABLE IF NOT EXISTS `{$this->tableName()}`
    (
    `id` INT(10) UNSIGNED NOT NULL AUTO_INCREMENT,
    `quote_hash` VARCHAR(32) NOT NULL ,
    `user_id` INT(10) UNSIGNED NOT NULL ,
    `company_id` INT(10) UNSIGNED,
    `product_id` INT(10) UNSIGNED NOT NULL ,
    `quote_data` LONGTEXT NULL,
    `country` VARCHAR(255) NULL ,
    `service` VARCHAR(255) NULL ,
    `currency` VARCHAR(255) NULL ,
    `created_at` DATETIME NOT NULL,
    PRIMARY KEY (`id`),
    INDEX (`quote_hash`, `user_id`, `product_id`, `company_id`),
    INDEX (`created_at`)
    ) ENGINE = InnoDB;
    ");

    }


    /**
    * @param int $user_id
    * @param $product_id
    * @param $company_id
    * @param $country
    * @param $service
    * @param $currency
    * @param array $quote_data
    * @return $this
    */
    public function newQuote(int $user_id, $product_id, $company_id, $country, $service, $currency, array $quote_data = []): static
    {

    global $wpdb;

    // calculate our quote hash
    $quoteHash = md5(implode(':', [

    // base on user id & post id
    $user_id, $product_id, $company_id,

    // and delivery country / type
    $country, $service,

    // and currency
    $currency,

    // also if our pricing has changed, generate new quote
    serialize($quote_data)

    ]));

    // get quotes with current quote hash created in the last day
    $quotes = $wpdb->get_results("
    SELECT * FROM `{$this->tableName()}`
    WHERE quote_hash = '$quoteHash'
    AND user_id = $user_id
    AND product_id = $product_id
    AND created_at >= DATE_SUB(NOW(), INTERVAL {$this->filterDuration})
    ");

    // check if we've got our quotes...
    if (is_array($quotes) && count($quotes) > 0) {
    $this->data = $quotes[0];
    return $this;
    }

    // create our new record
    $wpdb->insert($this->tableName(), [
    'quote_hash' => $quoteHash,
    'user_id' => $user_id,
    'product_id' => $product_id,
    'company_id' => $company_id,
    'country' => $country,
    'service' => $service,
    'currency' => $currency,
    'quote_data' => json_encode($quote_data),
    'created_at' => (new DateTime())->format('Y-m-d H:i:s'),
    ]);

    // reload the quote back on this object
    $this->loadQuote($wpdb->insert_id);

    // create action to let WordPress know, we've created a new quote
    do_action('quote_created_v2', $this);

    return $this;
    }

    /**
    * Get our quotes filename
    *
    * @return string
    */
    public function filename(): string
    {
    return sprintf('quote-%s.pdf', $this->quote_number());
    }


    /**
    * Is loaded
    * @return bool
    */
    public function isLoaded(): bool
    {
    return $this->isLoaded;
    }


    /**
    * Check if a quote has expired
    *
    * @return bool
    */
    public function hasExpired()
    {

    // get today's date and determine our expiry date
    $dateNow = \Carbon\Carbon::now('GMT');
    $expiryDate = $this->created_at()->add(self::$expiryDuration);

    // return our comparison
    return $dateNow->gt($expiryDate);
    }




    /**
    * Return our $pricing property
    *
    * @return mixed
    */
    protected function getPricingAttribute(): mixed
    {
    // return our json decoded pricing
    return json_decode($this->quote_data, true);
    }


    /**
    * Load quote
    * @param $quote_id
    * @return QuotesV2
    */
    public function loadQuote($quote_id): static
    {
    global $wpdb;

    // get our quote with id #$quote_id
    $result = $wpdb->get_row("SELECT * FROM {$this->tableName()} WHERE id = $quote_id");

    // check we have a result
    if ($result) {
    $this->saturate($result);
    }

    // return instance of our quote
    return $this;
    }

    /**
    * Saturate a Quote object
    * @param stdClass $data
    * @return QuotesV2
    */
    public function saturate(stdClass $data): static
    {

    // fill our model with our data
    $this->isLoaded = true;
    $this->data = $data;

    return $this;
    }

    /**
    * Retrieve all quotes from the database
    *
    * @param int $page The page to load
    * @param int $limit The number of pages to load
    * @param int $totalPages Referenced value, the total number of pages
    *
    * @return QuotesV2[]
    */
    public function allQuotes(int $page = 1, int $limit = 50, int &$totalPages = 1): array
    {
    global $wpdb;

    if ($limit > 0) {

    // get our total number of items
    $count = $wpdb->get_var("SELECT COUNT(*) FROM `{$this->tableName()}`");

    // determine our total number of pages
    $totalPages = $count > 0 ? 1 : ceil($count / $limit);

    // check our page is a valid positive integer
    $page = $page > 0 ? $page : 1;

    // if we are exceeding our page count, return empty
    if ($totalPages < $page) {
    return [];
    }

    // create our mysql limit string
    $limitString = 'LIMIT ' . ($page - 1) . ',' . $limit;

    } else {

    // we're not enforcing a limit...
    $limitString = null;

    }

    // get all quotes from the database
    $results = $wpdb->get_results("SELECT * FROM `{$this->tableName()}` WHERE created_at > NOW() - INTERVAL 30 DAY ORDER BY id DESC $limitString");

    //dd($results);

    $results = array_map(function ($result) {
    return (new QuotesV2())->saturate($result);
    }, $results);

    // return all of our results
    return $results;
    }


    /**
    * Get quotes for a specific user
    * @param $user_id
    * @param int $limit
    * @return QuotesV2[]
    */
    public function get_quotes($user_id, int $limit = 50): array
    {

    global $wpdb;

    // get quotes from current user, current product, created within the last day
    $results = $wpdb->get_results("SELECT id FROM `{$this->tableName()}` WHERE user_id = {$user_id} ORDER BY id DESC LIMIT {$limit}");

    // for each result, create a quote object
    $results = array_map(function ($result) {
    return (new QuotesV2())->loadQuote($result->id);
    }, $results);

    // return our results
    return $results;

    }


    /**
    * Get quotes number
    * @return string Number
    */
    public function quote_number(): string
    {
    // format our quote number
    return str_pad($this->data->id, 4, '0', STR_PAD_LEFT);
    }

    /**
    * Get quotes id
    * @return int
    */
    public function quote_id(): int
    {
    // format our quote number
    return $this->data->id;
    }


    /**
    * Get delivery type
    * @return string
    */
    public function country(): string
    {
    return Shipping::$countries[strtoupper($this->data->country)];
    }

    /**
    * Get delivery type
    * @return string
    */
    public function service(): string
    {
    return Shipping::$services[$this->data->service];
    }


    /**
    * Return our user object
    * @return WP_User
    */
    public function user(): WP_User
    {
    // get user object
    return get_user_by('ID',$this->data->user_id);
    }


    /**
    * Return our product object
    * @return WP_Post
    */
    public function product(): WP_Post
    {
    // get product object
    return get_post($this->data->product_id);
    }


    /**
    * Return our created at data
    * @return \Carbon\Carbon
    */
    public function created_at(): \Carbon\Carbon
    {
    return \Carbon\Carbon::make($this->data->created_at, 'GMT');
    }


    /**
    * Return our product object
    * @param $quote_id
    * @return string
    */
    public function download_url($quote_id): string
    {

    // quote url
    return get_bloginfo('url') . '/generate/quote?id=' . $quote_id;

    }

    }
    830 changes: 830 additions & 0 deletions Shipping.lib.php
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,830 @@
    <?php

    use JetBrains\PhpStorm\NoReturn;

    /**
    * @author Josh Cranwell <[email protected]>
    * @copyright The Sweet People
    * @version 1.0
    * @link https://www.thesweetpeople.com/
    * @since March 2023
    */

    class Shipping {

    /**
    * @return $
    */
    public static
    array $countries = [
    'AT' => 'Austria',
    'BE' => 'Belgium',
    'BG' => 'Bulgaria',
    'GB-CHA' => 'Channel Islands',
    'HR' => 'Croatia',
    'CY' => 'Republic of Cyprus',
    'CZ' => 'Czech Republic',
    'DK' => 'Denmark',
    'EE' => 'Estonia',
    'FI' => 'Finland',
    'FR' => 'France',
    'DE' => 'Germany',
    'GR' => 'Greece',
    'HU' => 'Hungary',
    'IE' => 'Ireland',
    'IT' => 'Italy',
    'GB-IOM' => 'Isle of Man',
    'GB-IOW' => 'Isle of Wight',
    'LV' => 'Latvia',
    'LT' => 'Lithuania',
    'LU' => 'Luxembourg',
    'MT' => 'Malta',
    'NL' => 'Netherlands',
    'PL' => 'Poland',
    'PT' => 'Portugal',
    'RO' => 'Romania',
    'SK' => 'Slovakia',
    'SI' => 'Slovenia',
    'ES' => 'Spain',
    'SE' => 'Sweden',
    'GB' => 'United Kingdom'
    ];

    public static
    array $uk_couriers = [
    "Royal Mail",
    "DHL Express UK",
    "Hermes",
    "DPD UK",
    "UPS UK",
    "TNT UK",
    "Parcelforce Worldwide",
    "Yodel",
    "UK Mail",
    "DX"
    ];

    public static
    array $eu_couriers = [
    "DHL Express",
    "UPS",
    "FedEx",
    "GLS",
    "DPD",
    "TNT",
    "Chronopost",
    "La Poste",
    "PostNord",
    "Bpost"
    ];

    public static
    array $services = [
    "standard" => "Standard",
    "pre-1200" => "Pre 12:00",
    "pre-1030" => "Pre 10:30"
    ];

    public static
    array $settings = [];

    public static
    array|bool $enabled = false;

    public static
    array|bool $individual = false;


    #[NoReturn] public function __construct ()
    {

    // load acf choices shipping settings
    add_filter('acf/load_field/key=field_6418de2d5394b', [ $this, 'load_country_choices' ]);
    add_filter('acf/load_field/key=field_6418e1805394e', [ $this, 'load_courier_choices' ]);
    add_filter('acf/load_field/key=field_6418fbf07a998', [ $this, 'load_courier_choices' ]);
    add_filter('acf/load_field/key=field_6418efa64b7c1', [ $this, 'load_service_choices' ]);
    add_filter('acf/load_field/key=field_6418fbf07a999', [ $this, 'load_service_choices' ]);

    // set our settings varible array
    $this->set_settings();

    // loadding shipping form service
    add_action('wp_ajax_nopriv_load_shipping_services', [ $this, 'ajax_load_shipping_services' ], 20 );
    add_action('wp_ajax_load_shipping_services', [ $this, 'ajax_load_shipping_services' ], 20 );

    }

    /**
    * @param array $data
    * @return array
    */
    public static function get_packaging_id(array $data): array
    {

    // check we have a product id
    if(isset($data['product_id']) && $data['product_id']) {

    // if product is pre packed
    $pre_packed = get_field('product_is_pre_packed',$data['product_id']);

    // if product is pre packed
    if($pre_packed) {

    // get confectionery id
    $data['packaging_id'] = get_field('product_confectionery',$data['product_id']);

    // if no confectionery packaging id is set the log error
    PricingV2::error_logger($data['packaging_id'],'No pre-packed confectionery selected for <a href="'.get_edit_post_link($data['product_id']).'" target="_blank">product</a>.');

    } else {

    // get packaging id
    $data['packaging_id'] = get_field('product_packaging',$data['product_id']);

    // if no packaging id is set the log error
    PricingV2::error_logger($data['packaging_id'],'No packaging selected for <a href="'.get_edit_post_link($data['product_id']).'" target="_blank">product</a>.');

    }

    } else {

    // no product id is set in data
    PricingV2::error_logger(false,'No product id is set in data.');

    }

    // return data aray
    return $data;

    }

    /**
    * @param array $data
    * @return array
    */
    public static function get_parcel_data(array $data): array
    {

    // check we have a packaging id
    if(isset($data['packaging_id']) && $data['packaging_id']) {

    // get parcel data
    $packaging = get_term_by('term_taxonomy_id',$data['packaging_id'],'','ARRAY_A');

    // check we have taxonomoy set
    if(isset($packaging['taxonomy']) && is_string($packaging['taxonomy'])) {

    // if we have a product id
    if (isset($data['pricing']) && $data['pricing']) {

    // get the shipping packaging setting
    switch ($data['pricing']) {
    case 'uk':

    // check if we have a custom shipping
    $custom_uk_shipping = get_field('product_custom_shipping',$data['product_id']);

    // if we have custom shipping
    if($custom_uk_shipping) {

    // get parcel box id from product
    $data['parcel_id'] = (int)get_field('packaging_shipping',$data['product_id'],false);

    // get parcel max items from product
    $data['parcel_max_items'] = (int)get_field('packaging_shipping_content',$data['product_id']);

    // get custom pallet max parcels
    $data['pallet_parcels_max'] = (int)get_field('shipping_pallet_qty',$data['product_id']);

    } else {

    // get parcel box id
    $data['parcel_id'] = (int)get_field('packaging_shipping',$packaging['taxonomy'] . '_' . $packaging['term_id'],false);

    // no uk parcel id is found log
    PricingV2::error_logger($data['parcel_id'],'No UK parcel found in <a href="'.get_edit_term_link($packaging['term_id'],$packaging['taxonomy']).'" target="_blank">'.$packaging['taxonomy'].'</a>.');

    // get parcel max items
    $data['parcel_max_items'] = (int)get_field('packaging_shipping_content',$packaging['taxonomy'] . '_' . $packaging['term_id']);

    // no uk parcel max items is found log
    PricingV2::error_logger($data['parcel_max_items'],'No UK parcel max items is found in <a href="'.get_edit_term_link($packaging['term_id'],$packaging['taxonomy']).'" target="_blank">'.$packaging['taxonomy'].'</a>.');

    }

    break;

    case 'eu':

    // check if we have a custom eu shipping
    $custom_eu_shipping = get_field('product_custom_eu_shipping',$data['product_id']);

    // if we have custom shipping
    if($custom_eu_shipping) {

    // get parcel box id from product
    $data['parcel_id'] = (int)get_field('packaging_eu_shipping',$data['product_id'],false);

    // get parcel max items from product
    $data['parcel_max_items'] = (int)get_field('packaging_eu_shipping_content',$data['product_id']);

    // get custom pallet max parcels
    $data['pallet_parcels_max'] = (int)get_field('eu_shipping_pallet_qty',$data['product_id']);

    } else {

    // get parcel box id
    $data['parcel_id'] = (int)get_field('packaging_eu_shipping',$packaging['taxonomy'] . '_' . $packaging['term_id'],false);

    // no eu parcel id is found log
    PricingV2::error_logger($data['parcel_id'],'No EU parcel found in <a href="'.get_edit_term_link($packaging['term_id'],$packaging['taxonomy']).'" target="_blank">'.$packaging['taxonomy'].'</a>.');

    // get parcel content
    $data['parcel_max_items'] = (int)get_field('packaging_eu_shipping_content',$packaging['taxonomy'] . '_' . $packaging['term_id']);

    // no eu parcel max items is found log
    PricingV2::error_logger($data['parcel_max_items'],'No EU parcel max items is found in <a href="'.get_edit_term_link($packaging['term_id'],$packaging['taxonomy']).'" target="_blank">'.$packaging['taxonomy'].'</a>.');

    }

    break;
    }
    }
    }
    }

    // return data aray
    return $data;

    }

    public static function get_pallet_data(array $data): array
    {

    // check we have a packaging id
    if(isset($data['parcel_id']) && $data['parcel_id']) {

    // check we have outer box
    $parcel_outer_parcel = get_field('shipping_outer_box','shipping_' . $data['parcel_id']);

    // if we have outer parcel
    if($parcel_outer_parcel) {

    // set outer parcel id
    $data['parcel_outer_id'] = $parcel_outer_parcel;

    // if is not set and is false
    if(!isset($data['pallet_parcels_max']) || !$data['pallet_parcels_max']) {

    // pallet max parcels
    $data['pallet_parcels_max'] = (int)get_field('shipping_pallet_qty','shipping_' . $parcel_outer_parcel);

    // max outer parcels per pallet error
    PricingV2::error_logger($data['pallet_parcels_max'],'Maximum outer boxes per pallet not set in <a href="'.get_edit_term_link($parcel_outer_parcel,'shipping').'" target="_blank">shipping</a>.');

    }

    // pallet max parcels
    //$data['pallet_parcels_layer'] = (int)get_field('shipping_pallet_qty_layer','shipping_' . $parcel_outer_parcel);

    // pallet outer parcel single layer error
    //PricingV2::error_logger($data['pallet_parcels_layer'],'Pallet single outer box layer quantity not set in <a href="'.get_edit_term_link($parcel_outer_parcel,'shipping').'" target="_blank">shipping</a>.');

    } else {

    // if is not set or is false
    if(!isset($data['pallet_parcels_max']) || !$data['pallet_parcels_max']) {

    // pallet max parcels
    $data['pallet_parcels_max'] = (int)get_field('shipping_pallet_qty','shipping_' . $data['parcel_id']);

    // max parcels per pallet error
    PricingV2::error_logger($data['pallet_parcels_max'],'Maximum boxes per pallet not set in <a href="'.get_edit_term_link($data['parcel_id'],'shipping').'" target="_blank">shipping</a>.');

    }

    // pallet max parcels
    //$data['pallet_parcels_layer'] = (int)get_field('shipping_pallet_qty_layer','shipping_' . $data['parcel_id']);

    // pallet parcel single layer error
    //PricingV2::error_logger($data['pallet_parcels_layer'],'Pallet single box layer quantity not set in <a href="'.get_edit_term_link($data['parcel_id'],'shipping').'" target="_blank">shipping</a>.');

    }

    }

    // return data aray
    return $data;

    }

    /**
    * @return mixed|void
    */
    public static function get_default()
    {

    // if uk pricing is enabled
    if(self::$enabled['uk']) {

    // set default services using gb code
    return self::$settings['countries']['gb']['services'];

    } else {

    // get the first country in list
    $default_country = reset(self::$settings['countries']);

    // set default services using gb code
    return $default_country['services'];

    }
    }

    /**
    * @param array $data
    * @return array
    */
    public static function get_unit_weight(array $data): array
    {

    // add unit weight to data
    $data['unit_weight'] = Product::unit_weight($data['product_id']);

    // return array
    return $data;

    }

    /**
    * @return no-return
    */
    public static function render_country_select()
    {
    // if countries is set and is an array
    if (isset(self::$settings['countries']) && is_array(self::$settings['countries'])) { ?>
    <div class="form-group mr-sm-3">
    <select class="form-control" style="padding-right:2.25rem;" name="shipping_country">
    <?php foreach (self::$settings['countries'] as $code => $country) { ?>
    <?php if (self::$enabled['uk'] && $country['pricing'] === 'uk') { ?>
    <option value="<?=$code?>" <?=$code==='gb'?'selected':null?>><?=$country['country']?></option>
    <?php } ?>
    <?php if (self::$enabled['eu'] && $country['pricing'] === 'eu') { ?>
    <option value="<?=$code?>" ><?=$country['country']?></option>
    <?php } ?>
    <?php } ?>
    </select>
    </div>
    <?php }
    }

    /**
    * @param bool|string $code
    * @return no-return
    */
    public static function render_service_radio(bool|string $code = false)
    {

    // if countries is set and is an array
    if (isset(self::$settings['countries']) && is_array(self::$settings['countries'])) {

    // if we have country code
    if ($code) {

    // set services using code
    $services = self::$settings['countries'][ $code ]['services'];

    } else {

    // get default
    $services = self::get_default();

    }

    // if we have services
    if (is_array($services)) { $uid = rand(); ?>
    <div data-group="services" class="form-group">
    <?php $i = 0; foreach ($services as $value => $service) { $i++ ?>
    <div class="form-check form-check-inline">
    <input class="form-check-input" type="radio" name="shipping_service" id="service_<?=$value?>_<?=$uid?>" value="<?=$value?>" <?=$i===1?'checked':false?>/>
    <label class="form-check-label" for="service_<?=$value?>_<?=$uid?>"><?=$service['service']?></label>
    </div>
    <?php } ?>
    </div>
    <?php }
    }
    }

    /**
    * @return void
    */
    #[NoReturn] public function ajax_load_shipping_services(): void
    {

    // get the country code
    $code = $_GET['country'];

    // render service radios for country
    self::render_service_radio($code);

    // die
    die();

    }

    /**
    * @param bool|int $post_id
    * @return void
    */
    #[NoReturn] public static function set_product_vars(bool|int $post_id = false): void
    {

    // check if post id is set
    if(!$post_id) {

    // global post
    global $post;

    // set post id
    $post_id = $post->ID;

    }

    // check if we are on the right post type and post is set
    if($post_id || is_singular('product') && isset($post)) {

    // check if product shipping has been enabled
    $enabled_uk = get_field('product_uk_shipping',$post_id);
    $enabled_eu = get_field('product_eu_shipping',$post_id);

    // update our indivual shipping var
    self::$enabled['uk'] = $enabled_uk ?? false;
    self::$enabled['eu'] = $enabled_eu ?? false;

    // let check for indiviual settings
    $individual = get_field('product_individual_shipping',$post_id);

    // if individual shipping
    if($individual) {

    // individual price fields
    $price_uk = get_field('product_individual_shipping_cost',$post_id);
    $price_eu = get_field('product_individual_eu_shipping_cost',$post_id);

    // update our indivual shipping var
    self::$individual['uk'] = $price_uk ?? false;
    self::$individual['eu'] = $price_eu ?? false;

    }
    }
    }

    /**
    * @return void
    */
    #[NoReturn] public function set_settings(): void
    {

    // get our shipping options
    $globals = get_field('shipping_globals','options');
    $countries = get_field('shipping_countries','options');

    // if globals is an array
    if( is_array($globals) ) {

    // for each globals as global
    foreach( $globals as $key => $value ) {

    // if key string starts with uk
    if( str_starts_with($key, 'uk') ) {

    // set uk global settings as integer
    self::$settings['globals']['uk'][ str_replace('uk_','',$key) ] = (int)$value;
    continue;

    }

    // if key string starts with eu
    if( str_starts_with($key, 'eu') ) {

    // set eu global settings as integer
    self::$settings['globals']['eu'][ str_replace('eu_','',$key) ] = (int)$value;

    }
    }
    }

    // if countries is an array
    if( is_array($countries) ) {

    // for each countries as country
    foreach( $countries as $country ) {

    // country code
    $code = $country['country']['value'];

    // set country in settings array
    self::$settings['countries'][ $code ]['country'] = $country['country']['label'];

    // set pricing in settings array
    self::$settings['countries'][ $code ]['pricing'] = $country['pricing']['value'];

    // if parcel services is an array
    if( is_array($country['services_parcel']) ) {

    // for each service's parcel as service parcel
    foreach( $country['services_parcel'] as $service_parcel ) {

    // parcel service value
    $service = $service_parcel['service']['value'];

    // set the service name label
    self::$settings['countries'][ $code ]['services'][ $service ]['service'] = $service_parcel['service']['label'];

    // set parcel service in settings array
    self::$settings['countries'][ $code ]['services'][ $service ]['method']['parcel'] = $service_parcel;

    // remove service array to label value
    unset(self::$settings['countries'][ $code ]['services'][ $service ]['method']['parcel']['service']);

    }
    }

    // if pallet services is an array
    if( is_array($country['services_pallet']) ) {

    // for each service's pallet as service pallet
    foreach( $country['services_pallet'] as $service_pallet ) {

    // pallet service value
    $service = $service_pallet['service']['value'];

    // set pallet service in settings array
    self::$settings['countries'][ $code ]['services'][ $service ]['method']['pallet'] = $service_pallet;

    // remove service array to label value
    unset(self::$settings['countries'][ $code ]['services'][ $service ]['method']['pallet']['service']);

    }
    }
    }
    }

    // get the countries sub-array
    $countries = self::$settings['countries'] ?? false;

    // if countries is an array
    if(is_array($countries)) {

    // extract the country sub-element as a separate array for sorting
    $country_list = array_column($countries,'country');

    // sort the country sub-element array in alphabetical order
    array_multisort($country_list,SORT_ASC, SORT_STRING, $countries);

    // update the original array with the sorted countries sub-array
    self::$settings['countries'] = $countries;

    }

    }

    /**
    * @param $field array
    * @return array
    */
    public function load_country_choices( array $field ): array
    {

    // reset choices
    $field['choices'] = [];

    // select country default null
    $field['choices'][''] = 'Select Country...';

    // loop through array and add to field 'choices'
    if( is_array(self::$countries) ) {

    // change keys to lower case
    $countries = array_change_key_case(self::$countries);

    // for each contries as code => country
    foreach( $countries as $code => $country ) {

    // add country and code to
    $field['choices'][ $code ] = $country;

    }
    }

    // return the field
    return $field;

    }

    /**
    * @param $field array
    * @return array
    */
    public function load_courier_choices( array $field ): array
    {

    // reset choices
    $field['choices'] = [];

    // select courier default null
    $field['choices'][''] = 'Select Courier...';

    // loop through array and add to field 'choices'
    if( is_array(self::$uk_couriers) ) {

    // for each uk couriers as uk courier
    foreach( self::$uk_couriers as $uk_courier ) {

    // add uk courier to option group
    $field['choices']['UK Couriers'][ $uk_courier ] = $uk_courier;

    }
    }

    // loop through array and add to field 'choices'
    if( is_array(self::$eu_couriers) ) {

    // for each eu couriers as eu courier
    foreach( self::$eu_couriers as $eu_courier ) {

    // add eu courier option group
    $field['choices']['EU Couriers'][ $eu_courier ] = $eu_courier;

    }
    }

    // return the field
    return $field;

    }

    /**
    * @param $field array
    * @return array
    */
    public function load_service_choices( array $field ): array
    {

    // reset choices
    $field['choices'] = [];

    // select services default null
    $field['choices'][''] = 'Select Service...';

    // loop through array and add to field 'choices'
    if( is_array(self::$services) ) {

    // for each service as value => label
    foreach( self::$services as $value => $label ) {

    // add value and label to choices
    $field['choices'][ $value ] = $label;

    }
    }

    // return the field
    return $field;

    }

    /**
    * @param int|float $value
    * @return string
    */
    public static function value_kg(int|float $value): string
    {

    // return kg value
    return (float)($value / 1000) . ' kg';

    }

    /**
    * @param int|float $value
    * @param string|bool $measure
    * @return string
    */
    public static function value_unit(int|float $value, string|bool $measure): string
    {

    // return matched measure with value
    return match ($measure) {
    'kg' => $value . ' kg',
    default => $value
    };

    }

    /**
    * @param array|bool $break
    * @return string
    */
    public static function packing_icon(array|bool $break): string
    {

    // set icon
    $icon = '';

    // if break is an array
    if(is_array($break)) {

    // tooltip html
    $tooltip = '';

    // get parcel weight
    $parcel_weight = $break['shipping_weight_parcel'] ?? false;

    // set tooltip parcel weight
    $tooltip .= $parcel_weight ? 'Parcel Weight: <strong>' . self::value_kg($parcel_weight). '</strong></br>' : '';

    // get consignment weight
    $consignment_weight = $break['shipping_weight_consignment'] ?? false;

    // set tooltip consignment weight
    $tooltip .= $consignment_weight ? 'Consignment Weight: <strong>' . self::value_kg($consignment_weight). '</strong></br>' : '';

    // if shipping method is set
    $method = $break['shipping_method'] ?? false;

    // set parcel count
    $parcels = $break['shipping_parcels'] ?? false;

    // set tooltip parcel count
    $tooltip .= $parcels ? 'Parcels: <strong>x' . $parcels . '</strong></br>' : '';

    // if method is set
    switch ($method) {
    case 'parcel':

    // if parcel count is 1
    if($parcels == 1) {

    // single parcel icon
    $icon = '<abbr data-toggle="tooltip" data-html="true" title="'.$tooltip.'" class="mr-2"><i class="fas fa-box fa-fw color-sweets"></i></abbr>';

    // else if parcel count is greater than 1
    } else if ($parcels > 1) {

    // multiple parcels icon
    $icon = '<abbr data-toggle="tooltip" data-html="true" title="'.$tooltip.'" class="mr-2"><i class="fas fa-boxes fa-fw color-sweets"></i></abbr>';

    }

    break;

    case 'pallet':

    // set pallet count
    $pallets = $break['shipping_pallets'] ?? false;

    // set tooltip pallet count
    $tooltip .= $pallets ? 'Pallets: <strong>x' . $pallets . '</strong></br>' : '';

    // if we have pallets
    if($pallets) {

    // single or multiple pallets icon
    $icon = '<abbr data-toggle="tooltip" data-html="true" title="'.$tooltip.'" class="mr-2"><i class="fas fa-pallet fa-fw color-sweets"></i></abbr>';

    }

    break;

    default:
    break;

    }

    }

    // return icon string
    return $icon;

    }

    /**
    * @param bool|string $service
    * @return string
    */
    public static function service_icon(bool|string $service): string
    {

    // return matched service icon
    return match ($service) {
    'standard' => '<abbr data-toggle="tooltip" data-placement="top" data-html="true" title="Service: <strong>'.self::$services[$service].'</strong>" class="mr-2"><i class="fas fa-shipping-fast fa-fw color-sweets"></i></abbr>',
    default => '<abbr data-toggle="tooltip" data-placement="top" data-html="true" title="Service: <strong>'.self::$services[$service].'</strong>" class="mr-2"><i class="fas fa-shipping-timed fa-fw color-sweets"></i></abbr>'
    };

    }

    }

    // initiate the shipping class
    $shipping = new Shipping();
    646 changes: 646 additions & 0 deletions pricing.js
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,646 @@
    /**
    * @author Josh Cranwell <[email protected]>
    * @copyright The Sweet People
    * @version 1.0
    * @link https://www.thesweetpeople.com/
    * @since March 2023
    */

    // jQuery on ready
    jQuery(function($) {

    /**
    * @param value
    */
    function format_money(value) {

    // Round the value to two decimal places
    value = parseFloat(value.toFixed(2));

    // Add a comma separator for the thousands place
    value = value.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",");

    // Add a leading zero if necessary
    if (value.indexOf('.') === -1) {
    value += '.00';
    } else if (value.indexOf('.') === value.length - 2) {
    value += '0';
    }

    // return the value
    return value;

    }

    // all pricing blocks
    let product_pricing = $('.product-pricing-v2', document);

    // run pricing form events
    pricing_form_events(product_pricing);

    // winodw loaded
    $(window).on('load',function() {

    // detect when a new pricing form is added to the DOM
    $(document).on('DOMNodeInserted','#quicklook_modal_body',function(e) {

    //console.log(e);

    // if the newly added element has the pri
    if($(e.target).hasClass('product-single')) {

    // get the newly added pricing form
    let product_pricing = $('.product-pricing-v2', e.target);

    // run pricing form events on newly added pricing form
    pricing_form_events(product_pricing);

    }

    });

    });

    /**
    * @param product_pricing
    */
    function pricing_form_events(product_pricing) {

    // pricing form object
    $(product_pricing)

    // pricing form country select on change
    .on('input','[name="shipping_country"]',function(e) {

    // load shipping servicess
    load_shipping_services(e.currentTarget);

    })

    // pricing form service selcect on change
    .on('input', '[name="shipping_service"]', function (e) {

    // load pricing table
    load_pricing_table(e.currentTarget);

    })

    // pricing form currency select on change
    .on('input', '[name="pricing_currency"]', function (e) {

    // get selected currecy value
    let currency = e.currentTarget.value;

    // set currency cookie
    Cookies.set('pricing_currency', currency, {expires: 365});

    // load pricing table
    load_pricing_table(e.currentTarget);

    })

    }

    /**
    * @param target
    */
    function load_shipping_services(target) {

    // get selected country code
    let code = target.value;

    // get the current pricing form
    let product_pricing = $(target).closest('.product-pricing-v2');

    // disable all shipping services
    $('[name="shipping_service"]', product_pricing).prop('disabled',true);

    // ajax call
    $.ajax({
    cache: false,
    timeout: 30000,
    url: admin_ajax_url,
    type: 'GET',
    data: {
    action: 'load_shipping_services',
    country: code
    }
    }).done(function (data) {

    // update shipping services group
    $('[data-group="services"]', product_pricing).replaceWith(data);

    }).then(function () {

    // load pricing table
    load_pricing_table(product_pricing);

    });

    }

    /**
    * @param target
    */
    window.load_pricing_table = function(target) {

    // get the current pricing form
    let product_pricing = $(target).closest('.product-pricing-v2');

    // get our shipping form variable
    let data = {
    product_id: $(product_pricing).data('product-id'),
    company_id: $(product_pricing).data('company-id'),
    country: $('[name="shipping_country"]', product_pricing).val(),
    service: $('[name="shipping_service"]:checked', product_pricing).val(),
    currency: $('[name="pricing_currency"]', product_pricing).val(),
    markup_unit: $(product_pricing).data('markup-unit'),
    markup_shipping: $(product_pricing).data('markup-shipping'),
    markup_origination: $(product_pricing).data('markup-origination'),
    };

    // enable quote download button
    $('.download-product-quote-v2', product_pricing)
    .addClass('disabled')
    .attr('href','#');

    // fade out pricing table
    $('.pricing-table-v2', product_pricing).find('.table-responsive')
    .fadeTo(0,0.5);

    // ajax call
    $.ajax({
    cache: false,
    timeout: 30000,
    url: admin_ajax_url,
    type: 'GET',
    data: {
    action: 'load_pricing_table',
    product_id: data.product_id,
    company_id: data.company_id,
    country: data.country,
    service: data.service,
    currency: data.currency,
    markup_unit: data.markup_unit,
    markup_shipping: data.markup_shipping,
    markup_origination: data.markup_origination
    }
    }).done(function (response) {

    // render the pricing table html
    $('.pricing-table-v2', product_pricing).find('.table-responsive')
    .html(response.data)
    .fadeTo(0,1);

    // initialize the tooltips again
    $('[data-toggle="tooltip"]').tooltip();

    // if success is true
    if(response.success) {

    // get the download quote button href params
    let params = $.param(data);

    // enable quote download button and set the href
    $('.download-product-quote-v2', product_pricing)
    .attr('href','/generate/quote?' + params)
    .removeClass('disabled');

    }

    }).then(function () {

    // do nothing

    });

    }


    // get custom pricing calculator
    let calculator_pricing = $('.custom-pricing-calculator', document);

    // pricing form object
    $(calculator_pricing)

    // pricing form country select on change
    .on('input','[name="shipping_country"]',function(e) {

    // load shipping servicess
    load_calculator_shipping_services(e.currentTarget);

    })

    // pricing form currency select on change
    .on('input', '[name="pricing_currency"]', function (e) {

    // get selected currecy value
    let currency = e.currentTarget.value;

    // set currency cookie
    Cookies.set('pricing_currency', currency, {expires: 365});

    // load calculator price breaks json
    load_calculator_json_price_breaks(calculator_pricing);

    })

    // calculator qty input on change
    .on('input', '[name="calculator_qty"]', function (e) {

    // get the add unit price input
    let add_unit = $('[name="add_calculator_unit"]', calculator_pricing);

    // if add unit price is not checked
    if(!add_unit.prop('checked')) {

    // get the current calculator pricing data
    let data = JSON.parse($(calculator_pricing).attr('data-pricing'));

    // check if json data has the price_breaks property
    if (data.hasOwnProperty('price_breaks')) {

    // set our quantity input and unit price input
    let qty_input = e.currentTarget.value;
    let unit_price_input = $('[name="calculator_unit"]', calculator_pricing);

    // set the entered quantity and unit price
    let entered_qty = parseInt(qty_input, 10);
    let unit_price;

    // if enter quanity is a number
    if (!isNaN(entered_qty)) {

    // set the unit_price to the lowest unit price initially
    unit_price = data.price_breaks[0].unit_price;

    // loop through the price_breaks array
    for (let i = 0; i < data.price_breaks.length; i++) {

    // check if the entered_qty is greater than or equal to the current price_break's quantity
    if (entered_qty >= data.price_breaks[i].quantity) {

    // update the unit_price variable with the unit_price of the current price_break
    unit_price = data.price_breaks[i].unit_price;

    } else {

    // else if the entered_qty is less than the current price_break's quantity,
    // stop looping and keep the last matched unit_price
    break;
    }
    }
    }

    // update the unit price input value
    unit_price_input.val(unit_price ? unit_price.toFixed(2) : '');

    }

    }

    })

    // check when the calculator qty input is blurred
    .on('blur', '[name="calculator_qty"]', function (e) {

    // lets get the min value
    let min = parseInt(e.currentTarget.min,10);

    // // if current value is empty
    if(!e.currentTarget.value) {

    // reset the value to min
    $(e.currentTarget).val(e.currentTarget.min).trigger('input');

    // if current value is less than min
    } else if (e.currentTarget.value < min) {

    // set the current value to the min
    $(e.currentTarget).val(e.currentTarget.min).trigger('input');

    }

    })

    // add calculator unit price checkbox on change
    .on('input', '[name="add_calculator_unit"]', function (e) {

    // get the calculator unit price input
    let input_unit = $('[name="calculator_unit"]', calculator_pricing);

    // if the custom add unit price is checked
    if(e.currentTarget.checked) {

    // disable the unit price input
    input_unit.prop('disabled',false);

    } else {

    // disable the unit price input
    input_unit.prop('disabled',true);

    }

    })

    // add calculator origination checkbox on change
    .on('input', '[name="add_calculator_origination"]', function (e) {

    // get the calculator origination input
    let input_unit = $('[name="calculator_origination"]', calculator_pricing);

    // if the custom add origination is checked
    if(e.currentTarget.checked) {

    // disable the origination input
    input_unit.prop('disabled',false);

    } else {

    // disable the origination input
    input_unit.prop('disabled',true);

    }

    })

    // check when the calculator unit and origination input is blurred
    .on('blur', '[name="calculator_unit"], [name="calculator_origination"]', function (e) {

    // lets get current target value
    let value = parseFloat(e.currentTarget.value);

    // format the value
    $(e.currentTarget).val(format_money(value));

    })

    /**
    * @param target
    */
    function load_calculator_shipping_services(target) {

    // get selected country code
    let code = target.value;

    // get the custom pricing calculator
    let calculator_pricing = $(target).closest('.custom-pricing-calculator');

    // disable all custom pricing calculator shipping services
    $('[name="shipping_service"]', calculator_pricing).prop('disabled',true);

    // ajax call
    $.ajax({
    cache: false,
    timeout: 30000,
    url: admin_ajax_url,
    type: 'GET',
    data: {
    action: 'load_shipping_services',
    country: code
    }
    }).done(function (data) {

    // update shipping services group
    $('[data-group="services"]', calculator_pricing).replaceWith(data);

    }).then(function () {

    // load calculor price breaks json
    load_calculator_json_price_breaks(calculator_pricing);

    });

    }

    /**
    * @param target
    */
    function load_calculator_json_price_breaks(target) {

    // get the custom pricing calculator
    let calculator_pricing = $(target);

    // set our data object
    let data = {
    product_id: $(calculator_pricing).data('product-id'),
    company_id: $(calculator_pricing).data('company-id'),
    country: $('[name="shipping_country"]', calculator_pricing).val(),
    currency: $('[name="pricing_currency"]', calculator_pricing).val()
    };

    // get our add values
    let add = {
    unit_price: $('[name="add_calculator_unit"]', calculator_pricing).prop('checked'),
    origination: $('[name="add_calculator_origination"]', calculator_pricing).prop('checked'),
    shipping: $('[name="add_calculator_shipping"]', calculator_pricing).prop('checked')
    }

    // disable all calculator unit inputs
    $('[name="calculator_qty"], [name="calculator_unit"], [name="calculator_origination"], [type="submit"]', calculator_pricing).prop('disabled',true);

    // ajax call
    $.ajax({
    cache: false,
    timeout: 30000,
    url: admin_ajax_url,
    type: 'GET',
    data: {
    action: 'load_json_price_breaks',
    product_id: data.product_id,
    company_id: data.company_id,
    country: data.country,
    currency: data.currency
    }
    }).done(function (response) {

    // if success is true
    if(response.success) {

    // set data json with stringified response data
    let data_json = JSON.stringify(response.data);

    // add json to calculator data pricing
    $(calculator_pricing).attr('data-pricing', data_json);

    // update calculator unit price and origination currency symbol
    $('[name="calculator_unit"], [name="calculator_origination"]', calculator_pricing)
    .closest('.input-group')
    .find('.input-group-text')
    .html(data.currency === 'eur' ? '€' : '£');

    // check if response.data has the price_breaks property
    if(response.data.hasOwnProperty('price_breaks')) {

    // set price breaks
    let price_breaks = response.data.price_breaks;

    // is array of price breaks
    if(Array.isArray(price_breaks) && price_breaks[0]) {

    // set calculation unit to first price break unit
    let first_price_break = price_breaks[0];

    // if we have first priced break unit
    if(first_price_break.unit_price) {

    // set quantity input
    let input_qty = $('[name="calculator_qty"]', calculator_pricing);

    // set value on quantity input and enable
    input_qty.val(first_price_break.quantity).prop('disabled',false);

    // if the input qty min is undefined
    if(!input_qty.attr('min')) {

    // set the min value
    input_qty.attr('min', first_price_break.quantity);

    }

    // set input unit price
    let input_unit = $('[name="calculator_unit"]', calculator_pricing);

    // enable all calculator unit inputs
    input_unit.val(format_money(first_price_break.unit_price));

    // if we have an add unit price
    if(add.unit_price) {

    // enable unit price input
    input_unit.prop('disabled',false);

    }

    }

    }

    }

    // check if response.data has the price_breaks property
    if(response.data.hasOwnProperty('origination')) {

    // set origination
    let orgination = response.data.origination;

    // if we have origination
    if(orgination) {

    // set input origination
    let input_origination = $('[name="calculator_origination"]', calculator_pricing);

    // enable all calculator unit inputs
    input_origination.val(format_money(orgination));

    // if we have an add unit price
    if(add.origination) {

    // enable input origination
    input_origination.prop('disabled',false);

    }

    }

    }

    }

    }).then(function (response) {

    // if success is true
    if(response.success) {

    // enable the calculator submit button for calcaltion
    $('[type="submit"]', calculator_pricing).prop('disabled',false);

    }

    });

    }

    // load calculator price breaks json
    load_calculator_json_price_breaks(calculator_pricing);

    // custom calculator form submit
    $('.custom-pricing-calculator-form').on('submit', function (e) {

    // prevent default
    e.preventDefault();

    // set our data object
    let data = {
    product_id: $(calculator_pricing).data('product-id'),
    company_id: $(calculator_pricing).data('company-id'),
    country: $('[name="shipping_country"]', calculator_pricing).val(),
    service: $('[name="shipping_service"]:checked', calculator_pricing).val(),
    currency: $('[name="pricing_currency"]', calculator_pricing).val(),
    custom_qty: $('[name="calculator_qty"]', calculator_pricing).val(),
    custom_unit: $('[name="calculator_unit"]', calculator_pricing).val(),
    custom_origination: $('[name="calculator_origination"]', calculator_pricing).val()
    }

    // remove the pricing calculation display nonw class
    $('.custom-pricing-calculation', calculator_pricing).removeClass('d-none');

    // enable quote download button
    $('.download-product-quote-v2', calculator_pricing)
    .addClass('disabled')
    .attr('href','#');

    // fade out pricing table
    $('.custom-pricing-calculation-table', calculator_pricing).find('.table-responsive')
    .fadeTo(0,0.5);

    // ajax call
    $.ajax({
    cache: false,
    timeout: 30000,
    url: admin_ajax_url,
    type: 'GET',
    data: {
    action: 'load_pricing_table',
    product_id: data.product_id,
    company_id: data.company_id,
    country: data.country,
    service: data.service,
    currency: data.currency,
    custom_qty: data.custom_qty,
    custom_unit: data.custom_unit,
    custom_origination: data.custom_origination
    }
    }).done(function (response) {

    // render the pricing table html
    $('.custom-pricing-calculation-table', calculator_pricing).find('.table-responsive')
    .html(response.data)
    .fadeTo(0,1);

    // initialize the tooltips again
    $('[data-toggle="tooltip"]').tooltip();

    // if success is true
    if(response.success) {

    // get the download quote button href params
    let params = $.param(data);

    // enable quote download button and set the href
    $('.download-product-quote-v2', calculator_pricing)
    .attr('href', '/generate/quote?' + params)
    .removeClass('disabled');

    }

    }).then(function (response) {

    // then

    });

    });

    })