<?php

namespace Hilco\Models;

use Arr;
use Auth;
use DB;
use Debugbar;
use Exception;
use Hilco\Pricing\PriceHelper;
use HilcoB2B\M3Request;
use Hilco\GuzzleWrappers\APIGuzzleWrapper;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Str;
use Log;

class WebCart extends WebModel
{
    protected $table = 'WebCarts';
    protected $fillable = ['quantity', 'comment'];

    public function webPart() {
        return $this->hasOne(WebPart::class, 'part_number', 'part_number');
    }

    public function webUser() {
        return $this->hasOne(WebUser::class, 'webuser_id', 'id');
    }

    public function priceList() {
//        return $this->hasOne(PriceList::class)
    }

    public function scopeForUser($query, $webuserId) {
        return $query->where('webuser_id', $webuserId);
    }

    public function scopePartNumber($query, $partNumber) {
        return $query->where('part_number', $partNumber);
    }

    public function getExtendedPriceAttribute() {
        return $this->webPart->getCustomerPrice($this->quantity) * $this->quantity;
    }

    /**
     * @return bool TRUE if cart item is linked to a valid, web-visible webpart to the end-user, FALSE otherwise
     */
    public function isAvailable() {
        return isset($this->webPart) && $this->webPart->visibleInSilo(b2b()->activeWebSilo());
    }

    public static function add(WebPart $webPart, $quantity = 1)
    {
        $instance = new static;
        $instance->webuser_id = Auth::id();
        $instance->part_number = $webPart->part_number;
        $instance->quantity = $quantity;
        $instance->save();
        return $instance;
    }

    public static function remove($userId, $partNumber)
    {
        $instance = new static;
        return $instance::where(['part_number' => $partNumber, 'webuser_id' => $userId])->delete();
    }

    /**
     * @param $webUser
     * @param $withTranslations
     * @return array|WebCart[]|\Illuminate\Database\Eloquent\Builder[]|\Illuminate\Database\Eloquent\Collection
     */
    public static function getWebCartItems($webUser = null, $withTranslations = true) {
        if (is_null($webUser)) $webUser = auth()->user();
        return
            self
            ::where('webuser_id', ($webUser->id))
            ->orderBy('part_number', 'asc')
            ->with(['webPart' => function ($query) use ($withTranslations) {
                if ($withTranslations) {
                    $query = $query->with('translations');
                }
                return $query->with(['part' => function ($query) {
                    return $query->with('inventoryItems')->with('productFamily.productCategory.productCategoryGroup');
                }]);
            }])
            ->get()
        ;
    }

    /* Calculates and keeps track of the overall status of the active user's cart, including all discounts and pricing */
    public static function cartStatus(WebSilo $webSilo = null, WebUser $webUser = null, Customer $soldToCustomer = null, $useDummy = true) {
        if (is_null($webSilo)) $webSilo = b2b()->activeWebSilo();
        if (is_null($webUser)) $webUser = auth()->user();
        if (is_null($soldToCustomer)) $soldToCustomer = b2b()->activeCustomer();

        if (is_null($webSilo) || is_null($webUser) || is_null($soldToCustomer)) return [];

        // Initialize 'status' object
        $status = [
            'itemsCount' => 0,
            'quantityCount' => 0,
            'baseTotal' => 0,
            'discountedTotal' => 0,
            'discounts' => [
                'rewards' => 0,
                'customer' => 0,
            ],
            'productCategoryTotals' => [],
            'rewardsUpgradeSpending' => 0,
            'rewardsTier' => rewards()->tier($soldToCustomer),
            'taxAmount' => 0,
            'shippingAmount' => 0,
            'shippingDiscountAmount' => 0,
            'shippingTaxAmount' => 0,
            'requireApproval' => false,
            'approvalReasons' => [],
            'priceList' => '',
            'missingPrices' => false,
            'noStock' => false,
            'hasMedicalPriority' => false,
            'hasRegulatedItem' => false,
        ];

        $items = [];

        $allCartItemsAreAvailable = true;

        // get all current items in the web cart - [array of WebCart objects]
        $webCartItems = self::getWebCartItems($webUser);
        if (is_countable($webCartItems)) {
            $numCartLines = count($webCartItems);
        } else {
            $numCartLines = 0;
        }


        // match part ids to relevant info (base price, total qty, price list, is_contract)
        // From this we get two lists; one with the individual lines, one with the altogether parts.
        if($numCartLines > 0) {
            $pricelistConnection = PriceListSchema::getActivePriceListSchema()->getPriceListConnection();
            $distincts = self::getDistinctPartInfo($pricelistConnection, $webCartItems, $soldToCustomer, $webUser, $useDummy);
            $distinctPartInfo = $distincts['distinctPartInfo'];
            $distinctLineInfo = $distincts['distinctLineInfo'];

            $atpData = [];
            foreach ($webCartItems as $webCartItem) {
                $atpData[] = $webCartItem->part_number;
            }

            $atps = [];
            try {
                $atps = WebPart::fetchAllItemsStockInfo($atpData, null, $soldToCustomer);
                foreach($atps as $atp){
                    if(($atp['status'] == Part::LOW_STOCK && $atp['stock'] == 0) || $atp['status'] == Part::UNKNOWN){
                        $status['noStock'] = true;
                        break;
                    }
                }
            }catch(Exception $e){
                Log::error("Exception caught while fetching stock info: " . $e->getMessage());
            }
        }else{
            $distinctPartInfo = [];
            $distinctLineInfo = [];
            $atps = [];
        }

        $promotions = Promotion::getActivePromotions($webSilo);
        // get all "almost qualifying" promotions (used to conditionally show cart and checkout headers)
        $almostQualifyingPromotionsInfo = self::getAlmostQualifiedPromotionsInfo($promotions, $soldToCustomer, $webCartItems, $distinctPartInfo);
        $status['almostQualifiedPromotionsInfo'] = $almostQualifyingPromotionsInfo;

        $status['customerTriggeredPromotions'] = self::getCustomerTriggeredPromotions($promotions, $soldToCustomer, $webCartItems, $distinctPartInfo);

        // get relevant discounts and other info from all triggered promotions
        $triggeredPromotionsInfo = self::getTriggeredPromotionsInfo($promotions, $soldToCustomer, $webCartItems, $distinctPartInfo);

        // set individual promo info objects and lists for use later
        $orderHeaderDiscounts = $triggeredPromotionsInfo['orderHeaderDiscounts'];
        $triggeredPromotions = $triggeredPromotionsInfo['triggeredPromotions'];
        $discountNotes = $triggeredPromotionsInfo['discountNotes'];
        $freeItemActionsInfo = $triggeredPromotionsInfo['freeItemActionsInfo'];

        // get the current cheapest item in the cart that qualifies for a free-item promotion
//        $cheapestItemAndPromo = self::getCheapestWebCartItemsByProduct($freeItemActionsInfo, $webCartItems, $customerPrices, $siloPrices, $distinctPartQuantities, $priceListNameToUse, $soldToCustomer);
//        $cheapestItem = $cheapestItemAndPromo['cheapestItem'];
//        $freeItemPromoUsed = $cheapestItemAndPromo['promotionUsed'];
//        $hasAppliedFreeItem = false;

        foreach ($webCartItems as $key=>$webCartItem) {
            // Let's catch any cart items that have become invalid/unavailable and mark them as such
            if (!$webCartItem->isAvailable()) {
                $webPart = $webCartItem->webPart()->withTrashed()->first();
                if (isset($webPart)) {
                    $part = $webPart->part()->withTrashed()->first();
                } else {
                    $part = null;
                }
                $items[$webCartItem->id] = collect([
                    'id' => $webCartItem->id,
                    'webCart' => $webCartItem,
                    'webPart' => $webPart,
                    'part' => $part,
                    'quantity' => $webCartItem->quantity,
                    'available' => false,
                ]);

                $allCartItemsAreAvailable = false;
                continue;
            }
            $part = $webCartItem->webPart->part;
            $partInfo = [];
            if (!empty($distinctLineInfo)) {
                foreach ($distinctLineInfo as $lineInfo) {
                    if ($lineInfo['partId'] == $part->id && $lineInfo['totalQuantity'] == $webCartItem->quantity) {
                        $partInfo = $lineInfo;
                        break;
                    }
                }
            }

            $appliedPromos = [];

            $basePrice = empty($partInfo['basePrice']) ? 0 : $partInfo['basePrice'];
            if($basePrice == trans('messages.temporarily_unavailable')){
                $status['missingPrices'] = true;
            }

            $isContractPrice = 0;
            $priceListUsed = null;
            if (!empty($partInfo['basePrice'])) {
                if (isset($partInfo['isContractPrice']) && $partInfo['isContractPrice']) {
                    $isContractPrice = 1;
                }
                if (!empty($partInfo['priceListUsed'])) {
                    $priceListUsed = $partInfo['priceListUsed'];
                }
            }

            $itemDiscounts = !empty($partInfo['discounts']) ? $partInfo['discounts'] : [];

            $discountPercent = 0;
            $totalDiscAmount = 0;
            $baseDiscountAmount = 0;
            $extendedDiscountAmount = 0;
            $m3AppliedDiscountType = null;
            $m3AppliedDiscountNdx = 1;
            $discounts = [];
            if(!empty($partInfo['basePrice']) && $basePrice != trans('messages.temporarily_unavailable')) {
                $discountedPrice = $basePrice;
                $extendedBasePrice = $basePrice * $webCartItem->quantity;

                // As long as this is non-contract pricing, calculate all the applicable discounts for the part
                if (!$isContractPrice) {
                    // if there are still free item actions to process..
                    if (is_countable($freeItemActionsInfo) && count($freeItemActionsInfo)) {
                        // loop thru each action's list of cheapest parts...
                        foreach ($freeItemActionsInfo as $key => $value) {
                            $freeItemAction = $value['freeItemAction'];
                            $cheapestPartList = $value['cheapestPartList'];
                            // if they still have parts to discount...
                            if (is_countable($cheapestPartList) && count($cheapestPartList)) {
                                foreach ($cheapestPartList as $partId => $numFree) {
                                    // if this part is still on the list to discount...
                                    if ($numFree > 0) {
                                        // and the current item matches...
                                        if ($part->id == $partId) {
                                            // if the number of times this item should be set to free is less than the current
                                            // qty on this cart line, then use that number, otherwise, just use the cart line qty
                                            $numToDiscount = ($numFree < $webCartItem->quantity) ? $numFree : $webCartItem->quantity;
                                            // calculate the actual discount percentage for the line to set that number of items as 'free'
                                            $freePercent = round($numToDiscount * 100 * (1 / $webCartItem->quantity), 2);
                                            // make sure the cart line's discount percentage is capped at 100%
                                            if ($discountPercent + $freePercent >= 100) {
                                                $freePercent = 100 - $discountPercent;
                                            }
                                            // if there is still a discount to be applied..
                                            if ($freePercent !== 0) {
                                                // keep track of current discount percentage
                                                $discountPercent += $freePercent;
                                                // increment the percent, amt, and extAmt values for the promo disc type
                                                $discountAmount = round($extendedBasePrice * ($freePercent / 100), 2);
                                                $totalDiscAmount += $discountAmount;

                                                $appliedPromos[] = [
                                                    'promoActionType' => 'freeItemAction',
                                                    'promoActionId' => $freeItemAction->id,
                                                    'percent' => $freePercent,
                                                    'amount' => $discountAmount
                                                ];

                                                if($totalDiscAmount >= $extendedBasePrice){
                                                    foreach ($itemDiscounts as $discType => $discount) {
                                                        if($discType == 'promotion') {
                                                            array_increment($discounts, "promotion.percent", $freePercent);
                                                            array_increment($discounts, "promotion.baseAmount", round($discountAmount / $webCartItem->quantity, 2));
                                                            array_increment($discounts, "promotion.extendedAmount", $discountAmount);
                                                            array_increment($discounts, "promotion.isUsed", 1);

                                                            foreach ($discount as $discAttr => $attrValue) {
                                                                if (Str::contains($discAttr, 'TX8')) {
                                                                    $discounts['promotion']['m3Type'] = trim($attrValue, ' ');
                                                                    $discounts['promotion']['m3Ndx'] = $discAttr[3];
                                                                }
                                                            }
                                                        }
                                                    }
                                                }

                                                // make sure the promo that had this free item action is updated to 'applied'
                                                $triggeredPromotions = self::updateAppliedPromotions($triggeredPromotions, $freeItemAction->promotion()->first());
                                                // decrement the number on the cheapest part list to keep track...
                                                $cheapestPartList[$partId] = $numFree - $numToDiscount;
                                                // ...and remove the item from the list once they've all been discounted
                                                if ($cheapestPartList[$partId] == 0) {
                                                    unset($cheapestPartList[$partId]);
                                                }
                                                $freeItemActionsInfo[$key] = [
                                                    'freeItemAction' => $freeItemAction,
                                                    'cheapestPartList' => $cheapestPartList
                                                ];
                                            } else {
                                                // otherwise stop calculating for this free item actions part list, it's already at 100%
                                                break;
                                            }
                                        }
                                    } else {
                                        unset($cheapestPartList[$partId]);
                                        $freeItemActionsInfo[$key] = [
                                            'freeItemAction' => $freeItemAction,
                                            'cheapestPartList' => $cheapestPartList
                                        ];
                                    }
                                }
                            } else {
                                unset($freeItemActionsInfo[$key]);
                            }
                        }
                    }

                    if ($totalDiscAmount < $extendedBasePrice) {
                        $totalDiscAmount = 0;
                        $discountPercent = 0;

                        $hasCustomerDiscounts = false;
                        $hasRewardsDiscounts = false;
                        $hasPromotionalDiscounts = false;
                        $hasM3Discounts = false;
                        $hasRewardsExclusion = false;

                        $customerDiscPercent = 0;
                        $customerDiscAmnt = 0;
                        $rewardsDiscPercent = 0;
                        $rewardsDiscAmnt = 0;
                        $promotionalDiscPercent = 0;
                        $promotionalDiscAmnt = 0;
                        $m3DiscPercent = 0;
                        $m3DiscAmnt = 0;

                        foreach ($itemDiscounts as $discType => $discount) {
                            $m3Ndx = intval($discount["bucketNo"]);
                            $dPercent = floatval(trim($discount["DIP$m3Ndx"], ' '));
                            $dAmount = floatval(trim($discount["DIA$m3Ndx"], ' '));
                            $dType = $discType;
                            $m3Type = trim($discount["TX8$m3Ndx"], ' ');

                            // keep track of customer and rewards discounts (see if-check after for-loop)
                            if ($dType === 'customer' && ($dPercent > 0 || $dAmount > 0)) {
                                $customerDiscPercent = $dPercent;
                                $customerDiscAmnt = $dAmount;
                                $hasCustomerDiscounts = true;
                            } else if ($dType === 'rewards' && ($dPercent > 0 || $dAmount > 0)) {
                                $rewardsDiscPercent = $dPercent;
                                $rewardsDiscAmnt = $dAmount;
                                $hasRewardsDiscounts = true;
                            } else if ($dType === 'promotion'){
                                $appliedPromos = array_merge($appliedPromos, $part->getApplicableDiscounts($soldToCustomer, $triggeredPromotions, $partInfo));
                                if(!empty($appliedPromos)){
                                    foreach($appliedPromos as $appliedPromo){
                                        $dPercent += $appliedPromo['percent'];
                                        $dAmount += $appliedPromo['amount'];
                                    }

                                    $hasPromotionalDiscounts = true;
                                }else{
                                    $dPercent = 0;
                                    $dAmount = 0;
                                }
                                $promotionalDiscPercent = $dPercent;
                                $promotionalDiscAmnt = $dAmount;
                            }else{
                                $m3DiscPercent += $dPercent;
                                $m3DiscAmnt += $dAmount;
                                $hasM3Discounts = true;
                            }

                            if(!\Illuminate\Support\Arr::has($discounts, $dType)){
                                $discounts[$dType] = [];
                            }

                            array_increment($discounts[$dType], "percent", $dPercent);
                            array_increment($discounts[$dType], "baseAmount", round($dAmount / $webCartItem->quantity, 2));
                            array_increment($discounts[$dType], "extendedAmount", $dAmount);
                            array_increment($discounts[$dType], "isUsed", 1);
                            $discounts[$dType]['m3Type'] = $m3Type;
                            $discounts[$dType]['m3Ndx'] = $m3Ndx;
                        }

                        // if the item has both customer and rewards discounts, only add the greater of the two, not both
                        if ($hasCustomerDiscounts && $hasRewardsDiscounts) {
                            if ($customerDiscPercent > $rewardsDiscPercent) {
                                $discountPercent += $customerDiscPercent;
                                $totalDiscAmount += $customerDiscAmnt;
                                $discounts['rewards']['isUsed'] = 0;
                                $m3AppliedDiscountType = $discounts['customer']['m3Type'] ;
                                $m3AppliedDiscountNdx =$discounts['customer']['m3Ndx'] ;
                            } else {
                                $discountPercent += $rewardsDiscPercent;
                                $totalDiscAmount += $rewardsDiscAmnt;
                                $discounts['customer']['isUsed'] = 0;
                                $m3AppliedDiscountType = $discounts['rewards']['m3Type'] ;
                                $m3AppliedDiscountNdx =$discounts['rewards']['m3Ndx'] ;
                            }
                        } else if ($hasCustomerDiscounts) {
                            $discountPercent += $customerDiscPercent;
                            $totalDiscAmount += $customerDiscAmnt;
                            $m3AppliedDiscountType = $discounts['customer']['m3Type'] ;
                            $m3AppliedDiscountNdx =$discounts['customer']['m3Ndx'] ;
                        } else if ($hasRewardsDiscounts) {
                            $discountPercent += $rewardsDiscPercent;
                            $totalDiscAmount += $rewardsDiscAmnt;
                            $m3AppliedDiscountType = $discounts['rewards']['m3Type'] ;
                            $m3AppliedDiscountNdx =$discounts['rewards']['m3Ndx'] ;
                        }

                        if ($hasPromotionalDiscounts){
                            $discountPercent += $promotionalDiscPercent;
                            $totalDiscAmount += $promotionalDiscAmnt;
                        }

                        if($hasM3Discounts){
                            $discountPercent += $m3DiscPercent;
                            $totalDiscAmount += $m3DiscAmnt;
                        }
                    }else{
                        foreach ($itemDiscounts as $discType => $discount) {
                            if ($discType != 'promotion') {
                                if(!\Illuminate\Support\Arr::has($discounts, $discType)){
                                    $discounts[$discType] = [];
                                }

                                array_increment($discounts[$discType], "percent", 0);
                                array_increment($discounts[$discType], "baseAmount", 0);
                                array_increment($discounts[$discType], "extendedAmount", 0);
                                array_increment($discounts[$discType], "isUsed", 0);

                                foreach ($discount as $discAttr => $attrValue) {
                                    if (Str::contains($discAttr, 'TX8')) {
                                        $discounts[$discType]['m3Type'] = trim($attrValue, ' ');
                                        $discounts[$discType]['m3Ndx'] = $discAttr[3];
                                    }
                                }
                            }
                        }
                    }


                    if ($totalDiscAmount >= $extendedBasePrice){
                        $totalDiscAmount = $extendedBasePrice;
                        $discountPercent = 100;
                    }

                    $baseDiscountAmount = round($totalDiscAmount / $webCartItem->quantity, 2);
                    $extendedDiscountAmount = $totalDiscAmount;
                    $discountedPrice = round($basePrice - $baseDiscountAmount, 2);
                }
            }else{
                $discountedPrice = 0;
                $extendedBasePrice = 0;
                $baseDiscountAmount = 0;
                $discountPercent = 0;
                $extendedDiscountAmount = 0;
            }

            $extendedPrice = $extendedBasePrice - $extendedDiscountAmount;

            // If applicable, calculate the tax amount
            $taxInfo = [];
            try {
                $taxInfo = self::calculateTaxAmount($webCartItem->webPart, $extendedPrice);
            }catch(Exception $e){
//                Log::error("Exception caught while fetching tax info: " . $e->getMessage());
                $taxInfo['isTaxable'] = 0;
                $taxInfo['taxAmount'] = 0;
            }

            // If any part requires approval to be ordered, set the status of the cart to require approval
            $itemRequiresApproval = $webCartItem->webPart->requireApproval;
            if ($itemRequiresApproval) {
                $status['requireApproval'] = true;
                $status['approvalReasons'][] = "Item {$webCartItem->part_number} in microsite list requires approval.";
            }

            if ($webCartItem->webPart->part->is_medical_priority) {
                $status['hasMedicalPriority'] = true;
            }

            if ($webCartItem->webPart->part->isRegulatedItem()) {
                $status['hasRegulatedItem'] = true;
            }

            // Add the part and its relevant details to the $items list
            $items[$webCartItem->id] = collect([
                'id' => $webCartItem->id,
                'webCart' => $webCartItem,
                'webPart' => $webCartItem->webPart,
                'atp' => \Illuminate\Support\Arr::get($atps, $webCartItem->part_number, ['status' => Part::UNKNOWN, 'stock' => 0]),
                'part' => $webCartItem->webPart->part()->with('productFamily.productCategory.productCategoryGroup')->first(),
                'quantity' => $webCartItem->quantity,
                'basePrice' => $basePrice,
                'discountedPrice' => $discountedPrice,
                'baseDiscountAmount' => $baseDiscountAmount,
                'discountPercent' => $discountPercent,
                'extendedBasePrice' => $extendedBasePrice,
                'extendedPrice' => $extendedPrice,
                'extendedDiscountAmount' => $extendedDiscountAmount,
                'discounts' => $discounts,
                'appliedPromos' => $appliedPromos,
                'm3DiscountUsed' => $m3AppliedDiscountType,
                'm3DiscountNdx' => $m3AppliedDiscountNdx,
                'isContractPrice' => $isContractPrice,
                'priceListUsed' => $priceListUsed,
                'isTaxable' => $taxInfo['isTaxable'],
                'taxAmount' => $taxInfo['taxAmount'],
                'requireApproval' => $itemRequiresApproval,
                'available' => true,
            ]);
            array_increment($status, 'itemsCount');
            array_increment($status, 'quantityCount', $webCartItem->quantity);
            array_increment($status, 'baseTotal', $extendedBasePrice);
            array_increment($status, 'taxAmount', $taxInfo['taxAmount']);

            foreach ($discounts as $discountType => $discountInfo) {
                if (isset($discountInfo['isUsed']) && $discountInfo['isUsed']) {
                    if(in_array($discountType, ['customer', 'rewards', 'promotion'])) {
                        array_increment($status, "discounts.$discountType", $discountInfo['extendedAmount']);
                    }else{
                        array_increment($status, "discounts.hilco", $discountInfo['extendedAmount']);
                    }
                }
            }

            $productCategoryGroup = $part->productFamily->productCategory->productCategoryGroup;
            if(!is_null($productCategoryGroup)){
                array_increment($status, "productCategoryTotals.$productCategoryGroup->id", $extendedPrice);
            }
        }

        // set the ones that won't change and are necessary for other uses in $status
        $status['triggeredPromotions'] = $triggeredPromotions;
        $status['discountNotes'] = $discountNotes;
        $status['orderHeaderDiscounts'] = $orderHeaderDiscounts;

        // Check to see if this order will qualify the active user's customer account for a Rewards Program tier upgrade
        if (rewards()->isRegistered() && b2b()->isBillingDirect() && rewards()->isEligible($soldToCustomer, false)) {
            $rewardsUpgradeSpending = rewards()->calculateUpgradeSpending($status, $soldToCustomer);
            $status['rewardsUpgradeSpending'] = $rewardsUpgradeSpending;

            // If the order will cause a tier upgrade, recalculate the 'discounts.rewards' discount type in $status
            if ($status['rewardsUpgradeSpending'] < 0) { //Tier Upgrade
                $status['baseRewardsTier'] = $status['rewardsTier'];
                $tierJump = $status['rewardsUpgradeSpending'] * -1;
                $status['rewardsTier'] = min(4, rewards()->tier() + $tierJump);
                $status['upgradedTier'] = min(4, rewards()->tier() + $tierJump);

                $rewardsDiscountTotal = 0;
                foreach ($items as $itemKey => $item) {
                    if (\Illuminate\Support\Arr::has($item, 'discounts.rewards')) {
                        $discountPercent = rewards()->discountForPartTier($item['part'], $status['upgradedTier'], $soldToCustomer);
                        $discountAmount = round($item['basePrice'] * ($discountPercent / 100), 2);
                        $extendedDiscountAmount = $discountAmount * $item['quantity'];
                        $baseDiscountAmount = round($item['basePrice'] * ($discountPercent / 100), 2);
                        $discountedPrice = $item['basePrice'] - $baseDiscountAmount;
                        $extendedPrice = $discountedPrice * $item['quantity'];
                        $rewardsDiscountTotal += $extendedDiscountAmount;
                        //I don't think this is right...this shouldn't be applied until we know for sure whether the reward discounts should be applied

                        /**
                         *  The old method didn't actually add any objects that would be considered in the checkout controller.
                         * It might never have actually worked.
                         * This new method will actually inject the new discount values (replacing them because these are
                         * discounts acquainted with the new upgrade tier.
                         */
                        $discounts = $item->get('discounts');
                        $discounts['rewards']['percent'] = $discountPercent;
                        $discounts['rewards']['baseAmount'] = $discountAmount;
                        $discounts['rewards']['extendedAmount'] = $extendedDiscountAmount;
                        $item->put('discounts', $discounts);

                        // pretty sure these fields are vestigial and unused now, leaving until we clean up web cart someday
                        $item->put('baseDiscountAmount', $discountAmount);
                        $item->put('extendedDiscountAmount', $extendedDiscountAmount);
                        $item->put('discountedPrice', $discountedPrice);
                        $item->put('extendedPrice', $extendedPrice);

                        // m3DiscountUsed is crucial here because it's how CheckoutController::placeOrder() determines
                        // if it should be creating a SalesOrderDiscLine row for the rewards discount on a line item
                        $item->put('m3DiscountUsed', $discounts['rewards']['m3Type']);
                        $item->put('m3DiscountNdx', $discounts['rewards']['m3Ndx']);
                    }
                }
                $status['discounts']['rewards'] = $rewardsDiscountTotal;
            }
        }

        // Calculate any "header-level" discounts applied via promotion(s)
        $calculatedOrderHeaderDiscountMax = 0;
        foreach ($orderHeaderDiscounts as $orderHeadDiscount) {
            $discountMax = \Illuminate\Support\Arr::get($orderHeadDiscount, 'maximum_dollar_amount');
            $calculatedOrderHeaderDiscountMax = max($calculatedOrderHeaderDiscountMax, $discountMax);
        }

        $calculatedOrderHeaderDiscountAmount = 0;
        foreach ($orderHeaderDiscounts as $headerDiscount) {
            $discountPercent = \Illuminate\Support\Arr::get($headerDiscount, 'discount_percent');
            $discountMaxAmount = \Illuminate\Support\Arr::get($headerDiscount, 'maximum_dollar_amount');
            $discountAmount = round($status['baseTotal'] * ($discountPercent/100), 2);
            if ($discountMaxAmount > 0) {
                $discountAmount = min($discountMaxAmount, $discountAmount);
            }
            $calculatedOrderHeaderDiscountAmount += $discountAmount;
        }

        if ($calculatedOrderHeaderDiscountMax > 0) {
            $calculatedOrderHeaderDiscountAmount = min($calculatedOrderHeaderDiscountAmount, $calculatedOrderHeaderDiscountMax);
        }

        array_increment($status, "discounts.promotion", $calculatedOrderHeaderDiscountAmount);

        // Assign 'discountedTotal' to be the combined values of all discount types in $status
        $status['discountedTotal'] = \Illuminate\Support\Arr::get($status, 'baseTotal');
        foreach (\Illuminate\Support\Arr::get($status, 'discounts') as $type => $amt) {
            $status['discountedTotal'] = \Illuminate\Support\Arr::get($status, 'discountedTotal') - $amt;
        }
        if($status['discountedTotal'] < 0 ){
            $status['discountedTotal'] = 0;
        }

        // Rate Shop the order
        $rate = b2b()->activeRate();
        if ($rate && !$rate->isError()) {
            $status['shippingAmount'] = $rate->rate();
            $status['shippingDiscountAmount'] = $rate->discountAmount();
            $status['shippingTaxAmount'] = $rate->taxAmount();
        }

        // Get the actual order total (discounted total plus any taxes)
        $status['orderTotal'] = $status['discountedTotal'] + $status['taxAmount'];

        if (b2b()->activeSpendingLimit() > 0 && $status['discountedTotal'] > b2b()->activeSpendingLimit()) {
            $status['requireApproval'] = true;
            $status['approvalReasons'][] = "Microsite spending limit has been exceeded, approval required.";
        }
        if (b2b()->activeMinimumOrder() > 0 && $status['discountedTotal'] < b2b()->activeMinimumOrder()) {
            $status['requireApproval'] = true;
            $status['approvalReasons'][] = "Microsite order minimum has not been met, approval required.";
        }

        if (self::getQuantityNeedsApproval($webCartItems)) {
            $status['requireApproval'] = true;
            $status['approvalReasons'][] = "Item(s) in cart do not meet quantity requirements as defined in microsite limited list, approval required.";
        }

        $status['allCartItemsAreAvailable'] = $allCartItemsAreAvailable;
        $status['items'] = $items;

        return $status;
    }

    /**
     * Get the list of all triggered promotions and their relevant info (discounts, etc)
     * @param $promotions
     * @param $customer
     * @param $webCartItems (These could also be SubscriptionOrderLines, as far as I can tell this isn't actually used anywhere)
     * @param $distinctPartInfo (This is what we actually use)
     * @return array
     * [
     *     'triggeredPromotions' => [array of Promotion objects, with extra 'applied' field],
     *     'discountNotes'=> [array of Discount Notes - DEPRECATED],
     *     'orderHeaderDiscounts' => [array of the following arrays, for each Order Discount action: ['discount_percent' => ?, 'maximum_dollar_amount' => ?]],
     *     'freeItemActions' => [array of PromotionFreeItemAction objects]
     * ]
     */
    public static function getTriggeredPromotionsInfo($promotions, $customer, $webCartItems, $distinctPartInfo) {
        if ($customer == null) {
            $customer = b2b()->activeCustomer();
        }
        $custDivision = $customer->getDivisionByFacility();
        if (!$customer->billsDirect || is_null($custDivision) || $custDivision->division_trimmed == Division::DE_CODE) {
            return [
                'triggeredPromotions' => [],
                'discountNotes'=> [],
                'orderHeaderDiscounts' => [],
                'freeItemActionsInfo' => []
            ];
        }

        $orderHeaderDiscounts = [];
        $triggeredPromotions = [];
        $discountNotes = [];
        $freeItemActionsInfo = [];

        foreach($promotions as $promotion) {
            $triggeredAllTriggers = true;
            $triggers = $promotion->triggers;
            foreach($triggers as $trigger) {
                $triggeredAllTriggers = $triggeredAllTriggers && $trigger->details->isTriggered($customer, $webCartItems, $distinctPartInfo);
            }

            if($triggeredAllTriggers) {
                foreach ($promotion->orderDiscountActions as $discountAction) {
                    $orderHeaderDiscounts[] = [
                        'discount_percent' => \Illuminate\Support\Arr::get($discountAction, 'discount_percent'),
                        'maximum_dollar_amount' => \Illuminate\Support\Arr::get($discountAction, 'maximum_dollar_amount'),
                        'action_id' => $discountAction->id
                    ];
                }

                foreach ($promotion->freeItemActions as $freeItemAction) {
                    $freeItemActionsInfo[] = [
                        'freeItemAction' => $freeItemAction,
                        'cheapestPartList' => self::getCheapestWebCartItemsForAFreeItemPromo($freeItemAction, $distinctPartInfo)
                    ];
                }

                if (count(\Illuminate\Support\Arr::get($promotion, 'orderDiscountActions', [])) ||
                    count(\Illuminate\Support\Arr::get($promotion, 'shippingDiscountActions', [])) ||
                    count(\Illuminate\Support\Arr::get($promotion, 'orderCommentActions', []))) {
                    $promotion['applied'] = true;
                }

                $triggeredPromotions[] = $promotion;
            }
        }

        // if multiple price list actions are set across multiple promotions
//        if (count($priceListActions) !== 1) $priceListActions = [];

        return
        [
            'triggeredPromotions' => $triggeredPromotions,
            'discountNotes'=> $discountNotes,
            'orderHeaderDiscounts' => $orderHeaderDiscounts,
//            'priceListActions' => $priceListActions,
            'freeItemActionsInfo' => $freeItemActionsInfo
        ];
    }

    /**
     * @param $promotions
     * @param $customer
     * @param $webCartItems (These could also be SubscriptionOrderLines, as far as I can tell this isn't actually used anywhere)
     * @param $distinctPartInfo (This is what we actually use)
     * This gathers promotions that might fit the customer.
     * @return array
     */
    public static function getCustomerTriggeredPromotions($promotions, $customer, $webCartItems, $distinctPartInfo){
        if ($customer == null) {
            $customer = b2b()->activeCustomer();
        }
        $custDivision = $customer->getDivisionByFacility();
        if (!$customer->billsDirect || is_null($custDivision) || $custDivision->division_trimmed == Division::DE_CODE) {
            return [];
        }

        $customerTriggeredPromotions = [];
        foreach($promotions as $promotion){
            $triggers = $promotion->triggers;
            foreach($triggers as $trigger){
                if($trigger->trigger_type != "customerTrigger"){
                    continue;
                }
                $details = $trigger->details;
                if(is_null($details) || !$details->isTriggered($customer, $webCartItems, $distinctPartInfo)){ //Technically, if this happens, something is wrong with the trigger instantiation.
                    continue 2;
                }
            }
            $customerTriggeredPromotions[] = $promotion;
        }

        return $customerTriggeredPromotions;
    }

    /**
     * @param $promotions
     * @param $customer
     * @param $webCartItems (These could also be SubscriptionOrderLines, as far as I can tell this isn't actually used anywhere)
     * @param $distinctPartInfo (This is what we actually use)
     * @return array
     */
    public static function getAlmostQualifiedPromotionsInfo($promotions, $customer, $webCartItems, $distinctPartInfo) {
        if ($customer == null) {
            $customer = b2b()->activeCustomer();
        }
        $custDivision = $customer->getDivisionByFacility();
        if (!$customer->billsDirect || is_null($custDivision) || $custDivision->division_trimmed == Division::DE_CODE) {
            return [];
        }
        /**
         *  Addendum [9-14-2018]:
         *  Promotions with Almost Qualified Triggers should only be considered if all of their other non-AQ triggers
         *      (if they have any others) have been satisfied/triggered.
         *  For example, if the promotion has a code trigger, then unless that code trigger has been activated, any
         *  AlmostQualified conditions will be disregarded.
         */
        $almostQualifyingPromotionsInfo = [];
//        TODO: Get rid the indexing, replace with basic appending
        $i = 0;
        foreach($promotions as $promotion) {
            $triggers = $promotion->triggers;
            $almostQualifiedValid = true; // This determines if there is a non-AQ trigger that is not satisfied.

            $j = 0;
            $promo = []; // Stores the Promotion and List of Triggers assosciated.
            $trigList = []; // Stores the Triggers w/ Qualifying Fields.
            foreach($triggers as $trigger) {
                if ($trigger->details->hasAlmostQualifyingField()) {
                    $trigDetails = [];
//                    For every promotion that has at least one trigger for an almost qualifying field, we create an object
//                      that stores the promotion, the triggerdetails, and the current cart status in terms of the relevant amounts.
                    $amountLeft = $trigger->details->isAlmostQualifying($customer, $webCartItems, $distinctPartInfo);
                    if ($amountLeft > 0) {
                        $trigDetails['trigger'] = $trigger; // Stores individual trigger and the amount progress their tied to.
                        if($trigger->details->minimum_quantity > 0 ) {
                            $trigDetails['type'] = 'minimum_quantity';
                        }else if($trigger->details->minimum_dollar > 0){
                            $trigDetails['type'] = 'minimum_dollar';
                        }
                        $trigDetails['amountLeft'] = $amountLeft;
                        $trigList[$j] = $trigDetails;        //Stores all Triggers that are tied to the individual promotion.
                        $j++;
                    }
                }else{
                    if(!$trigger->details->isTriggered($customer, $webCartItems, $distinctPartInfo)){
                        $almostQualifiedValid = false;
                    }
                }
            }

            if(count($trigList) && $almostQualifiedValid){
                $promo['triggers'] = $trigList;
                $promo['promotion'] = $promotion;
                $almostQualifyingPromotionsInfo[$i] = $promo;
                $i++;
            }
        }
        return $almostQualifyingPromotionsInfo;
    }

    /**
     * @param $pricelistConnection, "hilco-pricelist-a" or "hilco-pricelist-b" (fetch from PriceListSchemas table)
     * @param $pricingData
     * @param $webUser
     * @param bool $includeDiscounts
     * @param $customer
     * @param bool $checkDummy
     * @return array
     */
    public static function fetchAllItemPrices ($pricelistConnection, $pricingData, $webUser, bool $includeDiscounts = true, $customer = null, bool $checkDummy = true): array {
        if (is_null($pricelistConnection)) $pricelistConnection = PriceListSchema::getActivePriceListSchema();

        $prices = [];
        $tempUnavailMsg = trans('messages.temporarily_unavailable');

        if ($customer == null) {
            $customer = b2b()->activeCustomer();
        }

        if ($checkDummy && b2b()->shouldUseDummyPricing()) {
            $customer = $customer->getDummyCustomer();
        }
        $customerSegment = $customer->customerSegment;
        $m3DiscountCodes = M3DiscountCode::all()->pluck('discount_type', 'discount_code')->toArray();
        $activeDivision = b2b()->activeDivision($customer);

        
        if (in_array($activeDivision->division_trimmed, [Division::US_CODE, Division::AU_CODE])) {

            /* We're going to figure out all the parts we need to query ahead of time
            *  so that we can eager load all the results and not do 7 queries per item
            */
            $partNumbers = [];
            foreach ($pricingData as $id => $pricingDataObj) {
                $partNumbers[] = $pricingDataObj['partNumber'];
            }

            $items = Part::with(['rewardsPartExclusion', 'rewardsFamilyExclusion'])
                ->with('productFamily.productCategory.productCategoryGroup')
                ->whereIn('part_no', $partNumbers)->whereNull('deleted_at')->get();

            $partMap = [];
            foreach ($items as $item) {
                $partMap[$item->part_no] = $item;
            }

            foreach ($pricingData as $id => $pricingDataObj) {
//                $start = microtime(true);
//                DB::connection('hilco-pricelist')->enableQueryLog();

                $priceRecord = PriceHelper::getPriceRecord($pricelistConnection, $customer, $partMap[$pricingDataObj['partNumber']], $pricingDataObj['quantity'], $customerSegment);

//                dump("checkpoint 1", microtime(true) - $start);

                if (isset($priceRecord)) {
                    $prices[$id]['basePrice'] = $priceRecord->getSalePrice();
                    $prices[$id]['isContractPrice'] = $priceRecord->isContractPrice();
                    $prices[$id]['priceListUsed'] = $priceRecord->getPriceList();
                } else {
                    $prices[$id]['basePrice'] = $tempUnavailMsg; // pretty sure if the price list table is right this shouldn't happen
                }

                $prices[$id]['totalQuantity'] = $pricingDataObj['quantity'];

                if ($prices[$id]['basePrice'] != $tempUnavailMsg && $includeDiscounts) {
                    $prices[$id]['discounts'] = PriceHelper::calculateDiscountDataForItem($pricelistConnection, $customer, $partMap[$pricingDataObj['partNumber']], $pricingDataObj['quantity'], $prices[$id]['basePrice'], $m3DiscountCodes, $customerSegment);
                }
//                dump("checkpoint 2", microtime(true) - $start);
                if (isset($pricingDataObj['partId'])) {
                    $prices[$id]['partId'] = $pricingDataObj['partId'];
                } else if (isset($pricingDataObj['familyId'])) {
                    $prices[$id]['familyId'] = $pricingDataObj['familyId'];
                } else if (isset($pricingDataObj['part'])) {
                    $prices[$id]['partNum'] = $pricingDataObj['part'];
                }
                $prices[$id]['partNumber'] = $pricingDataObj['partNumber'];
//                dd('loop total',  (microtime(true) - $start), DB::connection('hilco-pricelist')->getQueryLog(),  config('database.connections.hilco-pricelist'));
            }


        } else {
            if (BannedApiUser::byWebUserId($webUser->id)->count() > 0) {
                foreach ($pricingData as $id => $data) {
                    $data['basePrice'] = $tempUnavailMsg;
                    $data['totalQuantity'] = $data['quantity'];
                    $prices[$id] = $data;
                }
                return $prices;
            }

            $m3 = new M3Request();
            $m3Client = $m3->client;

            if (empty($m3Client)) {
                Log::error('Exception caught while performing ' . __FUNCTION__ . ': Guzzle Http Client does not exist. Base URI may be missing!');
                return $tempUnavailMsg;
            }

            $chunks = array_chunk($pricingData, 50);
            $response = [];

            foreach ($chunks as $chunk) {
                $chunkResponseRaw = $m3Client->post('v4/getItemPrice', [
                    'form_params' => [
                        'requestFrom' => 'b2b',
                        'webuser' => $webUser->id,
                        'customer_number' => $customer->getCustNo(),
                        'pricingData' => $chunk,
                        'facility' => $customerSegment->getFacilityCode(),
                    ],
                    'headers' => [
                        'Accept' => 'application/json',
                    ]
                ]);
                $chunkResponseBody = $chunkResponseRaw->getBody();
                $chunkResponseContents = $chunkResponseBody->getContents();
                $chunkResponse = json_decode($chunkResponseContents, true);
                $response = array_merge($response, $chunkResponse);
            }

            foreach ($response as $infoId => $partPriceInfo) {
                if (is_countable($partPriceInfo['details']) && count($partPriceInfo['details']) == 1) {
                    $prices[$infoId]['basePrice'] = $tempUnavailMsg;
                } else if (Arr::has($partPriceInfo['details'], 'MIRecord')) {
                    $results = $partPriceInfo['details']['MIRecord'][0]['NameValue'];
                    foreach ($results as $result) {
                        if ($result['Name'] == 'SAPR') {
                            $returnMe = trim($result['Value'], ' ');
                            if (empty($returnMe)) {
                                $prices[$infoId]['basePrice'] = $tempUnavailMsg;
                            } else {
                                $prices[$infoId]['basePrice'] = trim($result['Value'], ' ');
                            }
                        }

                        if (!empty($prices[$infoId])) {
                            if ($prices[$infoId]['basePrice'] != $tempUnavailMsg) {
                                if ($result['Name'] == 'NTCD') {
                                    $prices[$infoId]['isContractPrice'] = trim($result['Value'], ' ') ? true : false;
                                } else if ($result['Name'] == 'PRRO') {
                                    $prices[$infoId]['priceListUsed'] = trim($result['Value'], ' ');
                                }
                            }
                        }
                    }

                    if ($prices[$infoId]['basePrice'] != $tempUnavailMsg && $includeDiscounts) {
                        foreach ($partPriceInfo['discounts'] as $discountNum => $discountInfo) {
                            $discType = null;
                            $hasDiscount = false;
                            $bucketNo = '';
                            foreach ($discountInfo as $key => $value) {
                                if (Str::contains($key, 'TX8')) {
                                    $value = trim($value, ' ');
                                    if (!empty($m3DiscountCodes[$value])) {
                                        switch ($m3DiscountCodes[$value]) {
                                            case 'Other':
                                                $discType = 'customer';
                                                break;
                                            case 'Rewards':
                                                $discType = 'rewards';
                                                break;
                                            case 'One-Off':
                                                $discType = 'promotion';
                                                break;
                                        }
                                    } else {
                                        $discType = $value;
                                    }
                                } else if (Str::contains($key, 'DIC')) {
                                    if ($value > 0) {
                                        $hasDiscount = true;
                                    }
                                }
                                $bucketNo = substr($key, -1);
                            }
                            $discountInfo['bucketNo'] = $bucketNo;

                            if ($hasDiscount && ($discType != null || $discType != '')) {
                                if ($discType == 'customer') {
                                    $prices[$infoId]['discounts']['customer'] = $discountInfo;
                                } else if ($discType == 'rewards') {
                                    $quantity = !empty($pricingData[$infoId]['quantity']) ? $pricingData[$infoId]['quantity'] : 1;
                                    $price = round($prices[$infoId]['basePrice'] * $quantity, 2);
                                    $prices[$infoId]['discounts']['rewards'] = PriceHelper::populateRewardsDiscInfoWithPartNumber($partPriceInfo['part'], $price, $discountInfo, $customer);
                                } else if ($discType == 'promotion') {
                                    $prices[$infoId]['discounts']['promotion'] = $discountInfo;
                                } else {
                                    $prices[$infoId]['discounts'][$discType] = $discountInfo;
                                }
                            }
                        }
                    }

                } else {
                    $prices[$infoId]['basePrice'] = $tempUnavailMsg;
                }

                if (!empty($pricingData[$infoId]['quantity'])) {
                    $prices[$infoId]['totalQuantity'] = $pricingData[$infoId]['quantity'];
                } else {
                    foreach ($pricingData as $priceData) {
                        if (!empty($priceData['quantity'])) {
                            if ($priceData['partNumber'] == $partPriceInfo['part']) {
                                $prices[$infoId]['totalQuantity'] = $priceData['quantity'];
                            }
                        }
                    }
                }
                
                if (!empty($pricingData[$infoId]['partId'])) {
                    $prices[$infoId]['partId'] = $pricingData[$infoId]['partId'];
                } else if (!empty($pricingData[$infoId]['familyId'])) {
                    $prices[$infoId]['familyId'] = $pricingData[$infoId]['familyId'];
                } else if (!empty($partPriceInfo['part'])) {
                    $prices[$infoId]['partNum'] = $partPriceInfo['part'];
                }
            }
        }
//        dd((microtime(true) - $start), DB::getQueryLog());
        return $prices;
    }

    /**
     * Call M3 GetPriceLine API to fetch prices and discounts for the cart.
     * Returns array object with "distinctLineInfo" and "distinctPartInfo", each their own array objects.
     * "distinctLineInfo" contains the price calculated on a per-line basis, "distinctPartInfo" contains the price
     * calculated on a per-item basis (essentially the price for the total quantity of an item in the cart).
     * @param $pricelistConnection, "hilco-pricelist-a" or "hilco-pricelist-b" (fetch from PriceListSchemas table)
     * @param $webCartItems
     * @param $customer
     * @param $webuser
     * @param bool $useDummy
     * @return array
     * e.g.,
     * [
     *  'distinctLineInfo' => [
     *      0 => [
     *          'basePrice'         => "<price value from GetPriceLine>",
     *          'priceListUsed'     => "<price list value from GetPriceLine>",
     *          'isContractPrice'   => <true|false>,
     *          'discounts'         => [<discounts data from GetPriceLine>],
     *          'totalQuantity'     => <quantity on the line>,
     *          'partId'            => <part id>,
     *      ],
     *      1 => [...],
     *      ...
     *  ],
     *  'distinctPartInfo' => [
     *      0 => [
     *          'basePrice'         => "<price value from GetPriceLine>",
     *          'priceListUsed'     => "<price list value from GetPriceLine>",
     *          'isContractPrice'   => <true|false>,
     *          'totalQuantity'     => <quantity on the line>,
     *          'partNum'           => <part number>,
     *      ],
     *      1 => [...],
     *      ...
     *  ],
     * ]
     */
    public static function getDistinctPartInfo($pricelistConnection, $webCartItems, $customer = null, $webuser = null, bool $useDummy = true): array {
        if (is_null($pricelistConnection)) $pricelistConnection = PriceListSchema::getActivePriceListSchema();

        if ($customer == null) {
            $customer = b2b()->activeCustomer();
        }
        if ($webuser == null) {
            $webuser = auth()->user();
        }

        /* First, grab only the cart items that are still available */
        $webCartItems = $webCartItems->filter(function ($cartLine) {
            return $cartLine->isAvailable();
        });

        /* Next, determine if web cart has duplicate items on different lines */
        // Get all cart line part numbers
        $cartPartNumbers = $webCartItems->pluck('part_number')->toArray();
        // Use array_count_values() to populate array of duplicate part numbers
        $dupePartNumbers = [];
        foreach (array_count_values($cartPartNumbers) as $partNo => $count) {
            if ($count > 1) {
                $dupePartNumbers[] = $partNo;
            }
        }
        $hasDupes = count($dupePartNumbers) > 0;

        $distinctPartQuantities = [];
        $distinctLineQuantities = [];

        if ($hasDupes) {
            // If the cart has duplicate items then construct $distinctPartQuantities with only the duplicates
            $dupeCartItems = $webCartItems->filter(function ($cartLine) use ($dupePartNumbers) {
                return in_array($cartLine->part_number, $dupePartNumbers);
            });
            foreach ($dupeCartItems as $dupeCartItem) {
                $distinctPartQuantities[$dupeCartItem->webPart->part->id]['partNumber'] = $dupeCartItem->part_number;
                array_increment($distinctPartQuantities[$dupeCartItem->webPart->part->id],'quantity', $dupeCartItem->quantity);
            }
        }

        // Doesn't matter if the cart has duplicates, we always construct $distinctLineQuantities with all cart lines
        foreach ($webCartItems as $webCartItem) {
            $distinctLineQuantities[] = [
                'partId' => $webCartItem->webPart->part->id,
                'partNumber' => $webCartItem->part_number,
                'quantity' => $webCartItem->quantity,
            ];
        }

        // Prepare return arrays
        $distinctLineInfo = [];
        $distinctPartInfo = [];
        try {
            /*
             * No matter what we always do the price check for $distinctLineInfo
             */
            $distinctLineInfo = self::fetchAllItemPrices($pricelistConnection, $distinctLineQuantities, $webuser, true, $customer, $useDummy);
            if ($hasDupes) {
                /*
                 * If the cart has duplicate items then we also need to do a price check for the duplicate items 
                 * to initially populate $distinctPartInfo, and then after that we fill in the rest of the $distinctPartInfo entries 
                 * with the non-dupe entries from $distinctLineInfo.
                 */
                $distinctPartInfo = self::fetchAllItemPrices($pricelistConnection, $distinctPartQuantities, $webuser, false, $customer, $useDummy);
            }
            foreach ($distinctLineInfo as $lineNum => $lineInfo) {
                $partNum =  $distinctLineQuantities[$lineNum]['partNumber'];
                // If this is a no-duplicates situation OR if this entry is for a part that isn't duplicated, populate $distinctPartInfo with the entry
                if (!$hasDupes || !in_array($partNum, $dupePartNumbers)) {
                    $partInfo['basePrice'] = $lineInfo['basePrice']; // basePrice should always be populated, even if it's "Temporarily Unavailable"

                    $priceListUsed = Arr::get($lineInfo, 'priceListUsed');
                    if (isset($priceListUsed)) $partInfo['priceListUsed'] = $priceListUsed;
                    $isContractPrice = Arr::get($lineInfo, 'isContractPrice');
                    if (isset($isContractPrice)) $partInfo['isContractPrice'] = $isContractPrice;
                    $totalQuantity = Arr::get($lineInfo, 'totalQuantity');
                    if (isset($totalQuantity)) $partInfo['totalQuantity'] = $totalQuantity;

                    $partInfo['partNum'] = $partNum;

                    $distinctPartInfo[] = $partInfo;
                }
            }
        } catch (Exception $e) {
            Log::error("Exception caught while fetching prices: " . $e->getMessage());
        }

        $distincts = [
            'distinctLineInfo' => $distinctLineInfo,
            'distinctPartInfo' => $distinctPartInfo,
        ];
        return $distincts;
    }

    public static function getQuantityNeedsApproval($webCartItems) {

        $distinctParts = [];
        foreach ($webCartItems as $webCartItem) {
            if (isset($webCartItem->webPart) && isset($webCartItem->webPart->part)) {
                array_increment($distinctParts, $webCartItem->webPart->part->id, $webCartItem->quantity);
            }
        }

        foreach ($distinctParts as $key => $value){
            $siloPart = WebSilo_Part::where('part_id', $key)->where('websilo_id', b2b()->activeWebSilo()->id)->first();
            if($siloPart){
                if(($siloPart->minimum_quantity != 0 && $value < $siloPart->minimum_quantity) || ($siloPart->maximum_quantity != 0 && $value > $siloPart->maximum_quantity)){
                    return true;
                }
            }
        }

        return false;
    }

    /**
     * @param $priceListName
     * @param $currencyCode
     * @param $partIds
     * @return array|PriceList[]|\Illuminate\Database\Eloquent\Collection
     */
    private static function getPartPricesByPriceList($priceListName, $currencyCode, $partIds) {
        return PriceList::where('price_list', '=', $priceListName)
                        ->where('currency', '=', $currencyCode)
                        ->whereIn('part_id', $partIds)
                        ->orderBy('quantity_level', 'DESC')
                        ->get();
    }

//    /* Returns the cheapest item in the web cart that matches the product_type hierarchy */
//    private static function getCheapestWebCartItemsByProduct($freeItemActions, $webCartItems, $customerPrices, $siloPrices, $partTotalQuantities, $priceListName, $soldToCustomer) {
//        $cheapestPrice = -1;
//        $cheapestItem = 0;
//        $promotionUsed = null;
//        foreach ($webCartItems as $webCartItem) {
//            $part = $webCartItem->webPart->part;
//
//            $qualifiesForFreeItem = false;
//            foreach ($freeItemActions as $freeItemAction) {
//                if ($freeItemAction->isApplicable($part)) {
//                    $qualifiesForFreeItem = true;
//                    $promotionUsed = $freeItemAction->promotion()->first();
//                    break;
//                }
//            }
//
//            if ($qualifiesForFreeItem) {
//                $partTotalQuantity = \Illuminate\Support\Arr::get($partTotalQuantities, $part->id, 0);
//                $currencyCode = $soldToCustomer->currency;
//                $siloPriceListName = b2b()->activeWebSilo()->priceList;
//                $partPriceInfo = self::getPartPriceInfo($customerPrices, $siloPrices, $part, $partTotalQuantity, $priceListName, $siloPriceListName, $currencyCode);
//                $basePrice = $partPriceInfo['basePrice'];
//                $cheapestPrice = $cheapestPrice == -1 ? $basePrice : min($cheapestPrice, $basePrice);
//                if ($cheapestPrice === $basePrice) $cheapestItem = $webCartItem;
//            }
//        }
//        return [
//            'cheapestItem' => $cheapestItem,
//            'promotionUsed' => $promotionUsed
//        ];
//    }

    /**
     * Determine the cheapest parts that are applicable to a free item promo action,
     * and how many of them can be marked as 'free' in the current cart.
     * @param $freeItemAction
     * @param $distinctPartInfo
     * @return array
     * [
     *     <part_id> => <number to mark as free>
     * ]
     */
    private static function getCheapestWebCartItemsForAFreeItemPromo($freeItemAction, $distinctPartInfo) {
        $cheapestList = [];
        $cheapestItemsFound = 0;
        $applicableItems = [];

        foreach ($distinctPartInfo as $partInfo) {
            $part = Part::wherePartNo($partInfo['partNum'])->first();
            if ($freeItemAction->isApplicable($part)) {
                $applicableItems[] = [
                    'partId' => $part->id,
                    'partInfo' => $partInfo
                ];
            }
        }

        usort($applicableItems, ['\Hilco\Models\WebCart', 'compareWebCartItemByPrice']);

        $maxFree = $freeItemAction->num_free;
        foreach($applicableItems as $key => $value) {
            $partId = $value['partId'];
            $partInfo = $value['partInfo'];
            if ($cheapestItemsFound < $maxFree) {
                $partQuantity = $partInfo['totalQuantity'];
                $quantityToDiscount = ($cheapestItemsFound + $partQuantity <= $maxFree ? $partQuantity : $maxFree - $cheapestItemsFound);
                array_increment($cheapestList, $partId, $quantityToDiscount);
                $cheapestItemsFound += $quantityToDiscount;
            }
        }

        return $cheapestList;
    }

    /**
     * Comparator function for sorting web cart items by their base price
     * Only used in getCheapestWebCartItemsForAFreeItemPromo() right now
     * @param $item1
     * @param $item2
     * @return int
     */
    static function compareWebCartItemByPrice($item1, $item2) {
        $price1 = \Illuminate\Support\Arr::get($item1, 'partInfo.basePrice');
        $price2 = \Illuminate\Support\Arr::get($item2, 'partInfo.basePrice');

        if ($price1 === $price2)
            return 0;

        return ($price1 < $price2) ? -1 : 1;
    }

    /**
     * Returns general price info for a given part, including its base price, whether it's under
     * contract pricing, and which price list was used. Tries to get the customer's price list first,
     * and uses the default WebSilo price list otherwise.
     * @param $customerPrices
     * @param $siloPrices
     * @param $part
     * @param $partTotalQuantity
     * @param $customerPriceListName
     * @param $siloPriceListName
     * @param $currencyCode
     * @return array
     * [
     *     'basePrice' => base price of the part from the price list used,
     *     'isContractPrice' => boolean value from PriceLists->contract_flag,
     *     'priceListUsed' => name of the price list that was used
     * ]
     */
    private static function getPartPriceInfo($customerPrices, $siloPrices, $part, $partTotalQuantity, $customerPriceListName, $siloPriceListName, $currencyCode) {
//        $customerPriceList = PriceList::filterForPartQuantity($customerPrices, $part->id, $partTotalQuantity);
//        $basePrice = WebCart::getItemPrice($part, $partTotalQuantity);
        $basePrice = null;
        $isContractPrice = true;
        $priceListUsed = false;
//        if ($customerPriceList) {
//            $basePrice = $customerPriceList->price;
//            $basePrice = WebCart::getItemPrice($part, $partTotalQuantity);
//            $isContractPrice = $customerPriceList->contract_flag;
//            $priceListUsed = $customerPriceListName;
//        } else {
//            $siloPriceList = PriceList::filterForPartQuantity($siloPrices, $part->id, $partTotalQuantity);
//            if ($siloPriceList) {
//                $basePrice = $siloPriceList->price;
//                $isContractPrice = false;
//                $priceListUsed = $siloPriceListName;
//            } else {
//                $basePrice = $part->getListPriceForCurrencyCode($currencyCode);
//                $isContractPrice = false;
//                $priceListUsed = 'PART_TABLE_LIST_PRICE';
//            }
//        }
        return [
            'basePrice' => $basePrice,
            'isContractPrice' => $isContractPrice,
            'priceListUsed' => $priceListUsed
        ];
    }

    /**
     * Update the triggered promotions list to mark one as 'applied'
     * @param $triggeredPromotions
     * @param $promoToUpdate
     * @return array
     */
    private static function updateAppliedPromotions($triggeredPromotions, $promoToUpdate) {
        $promos = [];
        foreach ($triggeredPromotions as $promo) {
            if ($promo->id === $promoToUpdate->id) {
                $promo['applied'] = true;
            }
            $promos[] = $promo;
        }
        return $promos;
    }

    /**
     * Calculate the part's tax amount, if applicable, based on the current customer and the calculated extended price
     * @param $part
     * @param $extendedPrice
     * @return float|int
     */
    public static function calculateTaxAmount(WebPart $webPart, $extendedPrice, $customer = null, $shipToAddress = null) {
        $itemTaxAmount = 0;
        $taxInfo = [];
        if (b2b()->isCustomerTaxable()) {
            if (b2b()->activeCountryVATTax()) {
                $vatTax = \Illuminate\Support\Arr::get($webPart->part->inventoryItems()->inPlant(b2b()->activePlant()->plant)->first(), 'VATTax', null);

                if(is_null($vatTax)){
                    $taxInfo['isTaxable'] = 0;
                }else{
                    $taxInfo['isTaxable'] = 1;
                    $itemTaxAmount = $extendedPrice * (\Illuminate\Support\Arr::get($vatTax, 'tax_percentage', 0) / 100);
                }
            }else{
                $partTaxability = $webPart->getPartTaxability($extendedPrice, $shipToAddress);
                if ($partTaxability['taxResult'] == 'TAXABLE') {
                    $taxInfo['isTaxable'] = 1;
                    $itemTaxAmount = $partTaxability['calculatedTax'];
                }else{
                    $taxInfo['isTaxable'] = 0;
                }
            }
        }else{
            $taxInfo['isTaxable'] = 0;
        }

        $taxInfo['taxAmount'] = $itemTaxAmount;
        return $taxInfo;
    }

    /**
     * Get the name of the Price List to use on the order -- exists because Croakies Adventures logic can "override"
     * a customer's default price list (see CroakiesHelper.php for more details)
     * @param $customerPriceListName
     * @param $webCartItems
     * @return false|int|null|string
     */
    public static function getPriceListNameToUse($customerPriceListName, $webCartItems) {
        // check Croakies Adventures Race logic if customer is qualifying...
        $priceListNameToUse = croakies()->getPriceListToUse($webCartItems);

        // DISABLED, DEFUNCT AS OF EARLY 2018
//         ...and check if there's a promotion that applies a price list...
//        $priceLists = $triggeredPromotionsInfo['priceListActions'];
//        if (count($priceLists)) $priceListNameToUse = array_values($priceLists)[0];//reset($priceLists);

        //...otherwise just use sold-to customer's default price list
        if (!$priceListNameToUse) $priceListNameToUse = $customerPriceListName;

        return $priceListNameToUse;
    }
}