/**
  * Called by AJAX from checkout for Calculate Shipping. Builds a grid of
  * shipping scenarios including shipping price and cart total prices. The
  * results are cached client-side in the browser allowing the user to click
  * through and see updated cart totals without initiating another AJAX
  * request.
  * @return string JSON encoded shipping options.
  */
 public function actionAjaxCalculateShipping()
 {
     if (isset($_POST['CheckoutForm']) === false) {
         return $this->renderJSON(array('result' => 'error', 'errormsg' => 'Must POST a CheckoutForm'));
     }
     Yii::log("Performing an AJAX Shipping Calculation Request", 'info', 'application.' . __CLASS__ . "." . __FUNCTION__);
     // We run the items through the model for verification.
     $model = new CheckoutForm();
     $model->attributes = $_POST['CheckoutForm'];
     if (Yii::app()->params['SHIP_SAME_BILLSHIP']) {
         $model->billingSameAsShipping = 1;
     }
     $model->scenario = 'CalculateShipping';
     // Copy address book to field if necessary.
     $model->fillFieldsFromPreselect();
     // Set up the exception handle for the remainder of this function.
     $handleException = function (Exception $e) {
         Yii::log($e->getMessage(), 'error', 'application.' . __CLASS__ . "." . __FUNCTION__);
         return $this->renderJSON(array('result' => 'error', 'errormsg' => $e->getMessage()));
     };
     try {
         self::validateCheckoutForm($model);
     } catch (Exception $e) {
         return $handleException($e);
     }
     Yii::log("Successfully validated shipping request", 'info', 'application.' . __CLASS__ . "." . __FUNCTION__);
     // Clone the model because we're going to make some changes we may
     // not want to retain.
     // TODO: Accommodate spaces in the postal code in the shipping modules themselves.
     $checkoutForm = clone $model;
     $checkoutForm->shippingPostal = str_replace(" ", "", $checkoutForm->shippingPostal);
     $hasPromoCodeBeforeUpdatingTaxCode = Yii::app()->shoppingcart->hasPromoCode;
     try {
         $arrCartScenario = Shipping::getCartScenarios($checkoutForm);
     } catch (Exception $e) {
         return $handleException($e);
     }
     // A promo code can be removed as a result of getting updated
     // cart scenarios if the tax code has changed and the promo
     // code theshold is no longer met.
     // TODO: Do not modify the shoppingcart tax code in getCartScenarios,
     // it is surprising. See a similar TODO in Shipping::getCartScenarios.
     $hasPromoCodeBeforeAfterUpdatingTaxCode = Yii::app()->shoppingcart->hasPromoCode;
     $wasPromoCodeRemoved = false;
     if ($hasPromoCodeBeforeUpdatingTaxCode === true && $hasPromoCodeBeforeAfterUpdatingTaxCode === false) {
         $wasPromoCodeRemoved = true;
     }
     // Sort the shipping options based on sort order, then on price.
     usort($arrCartScenario, function ($item1, $item2) {
         if ($item1['sortOrder'] === $item2['sortOrder'] && $item1['shippingPrice'] === $item2['shippingPrice']) {
             return 0;
         }
         if ($item1['sortOrder'] === $item2['sortOrder']) {
             return $item1['shippingPrice'] > $item2['shippingPrice'] ? 1 : -1;
         }
         return $item1['sortOrder'] > $item2['sortOrder'] ? 1 : -1;
     });
     $arrFormattedCartScenario = $this->formatCartScenarios($arrCartScenario);
     // Store the results in our session so we don't have to recalculate whatever they picked.
     // TODO: Much of this may be unnecessary with the refactoring completed
     // as part of advanced checkout.
     Yii::log("Populating caches", 'info', 'application.' . __CLASS__ . "." . __FUNCTION__);
     Yii::app()->session['ship.htmlCartItems.cache'] = $arrFormattedCartScenario['renderedCartItems'];
     Yii::app()->session['ship.prices.cache'] = $arrFormattedCartScenario['shippingOptionPrices'];
     Yii::app()->session['ship.formattedPrices.cache'] = $arrFormattedCartScenario['formattedShippingOptionPrices'];
     Yii::app()->session['ship.priorityRadio.cache'] = $arrFormattedCartScenario['renderedPriorityRadioButtonList'];
     Yii::app()->session['ship.providerLabels.cache'] = $arrFormattedCartScenario['shippingProviderLabels'];
     Yii::app()->session['ship.priorityLabels.cache'] = $arrFormattedCartScenario['shippingPriorityLabels'];
     Yii::app()->session['ship.providerRadio.cache'] = $arrFormattedCartScenario['renderedProviderRadioButtonList'];
     Yii::app()->session['ship.formattedCartTotals.cache'] = $arrFormattedCartScenario['formattedCartTotals'];
     Yii::app()->session['ship.taxes.cache'] = $arrFormattedCartScenario['renderedCartTaxes'];
     $errormsg = null;
     if (Yii::app()->user->hasFlash('error')) {
         // The error message may contain HTML. Since the JavaScript
         // displays it in an alert, we don't want that.
         $errormsg = strip_tags(Yii::app()->user->getFlash('error'));
     }
     $arrReturn = array('cartitems' => $arrFormattedCartScenario['renderedCartItems'], 'paymentmodules' => $model->getPaymentMethodsAjax(), 'prices' => $arrFormattedCartScenario['formattedShippingOptionPrices'], 'priority' => $arrFormattedCartScenario['renderedPriorityRadioButtonList'], 'errormsg' => $errormsg, 'provider' => $arrFormattedCartScenario['renderedProviderRadioButtonList'], 'result' => 'success', 'taxes' => $arrFormattedCartScenario['renderedCartTaxes'], 'totals' => $arrFormattedCartScenario['formattedCartTotals'], 'wasPromoCodeRemoved' => $wasPromoCodeRemoved);
     Yii::log("Returning JSON encoded shipping", 'info', 'application.' . __CLASS__ . "." . __FUNCTION__);
     return $this->renderJSON($arrReturn);
 }