menu

SiteGenesis / Server-side JS / Source: app_storefront_controllers/cartridge/scripts/models/CartModel.js

'use strict';
/**
 * Model for cart functionality. Creates a CartModel class with payment, shipping, and product
 * helper methods.
 * @module models/CartModel
 */
var Transaction = require('dw/system/Transaction');

/* API Includes */
var AbstractModel = require('./AbstractModel');
var ArrayList = require('dw/util/ArrayList');
var BasketMgr = require('dw/order/BasketMgr');
var Money = require('dw/value/Money');
var MultiShippingLogger = dw.system.Logger.getLogger('multishipping');
var OrderMgr = require('dw/order/OrderMgr');
var PaymentInstrument = require('dw/order/PaymentInstrument');
var Product = require('~/cartridge/scripts/models/ProductModel');
var ProductInventoryMgr = require('dw/catalog/ProductInventoryMgr');
var ProductListMgr = require('dw/customer/ProductListMgr');
var QuantityLineItem = require('~/cartridge/scripts/models/QuantityLineItemModel');
var Resource = require('dw/web/Resource');
var ShippingMgr = require('dw/order/ShippingMgr');
var StoreMgr = require('dw/catalog/StoreMgr');
var TransientAddress = require('~/cartridge/scripts/models/TransientAddressModel');
var UUIDUtils = require('dw/util/UUIDUtils');

var lineItem;
var app = require('~/cartridge/scripts/app');
var ProductList = app.getModel('ProductList');

/**
 * Cart helper providing enhanced cart functionality
 * @class module:models/CartModel~CartModel
 * @extends module:models/AbstractModel
 *
 * @param {dw.order.Basket} obj The basket object to enhance/wrap.
 */
var CartModel = AbstractModel.extend({
    /** @lends module:models/CartModel~CartModel.prototype */
    /**
     * Triggers the cart calculation by executing the hook 'dw.ocapi.shop.basket.calculate'.
     *
     * @transactional
     * @alias module:models/CartModel~CartModel/calculate
     * @return {dw.system.Status} Returns OK if cart when the cart is recalculated.
     */
    calculate: function () {
        dw.system.HookMgr.callHook('dw.ocapi.shop.basket.calculate', 'calculate', this.object);
    },

    addProductToCart: function() {
        var cart = this;
        var params = request.httpParameterMap;
        var format = params.hasOwnProperty('format') && params.format.stringValue ? params.format.stringValue.toLowerCase() : '';
        var newBonusDiscountLineItem;
        var Product = app.getModel('Product');
        var productOptionModel;
        var productToAdd;
        var template = 'checkout/cart/minicart';

        // Edit details of a gift registry
        if (params.source && params.source.stringValue === 'giftregistry' && params.cartAction && params.cartAction.stringValue === 'update') {
            ProductList.replaceProductListItem();
            return {
                source: 'giftregistry'
            };
        }

        if (params.source && params.source.stringValue === 'wishlist' && params.cartAction && params.cartAction.stringValue === 'update') {
            app.getController('Wishlist').ReplaceProductListItem();
            return;
        }

        // Updates a product line item.
        if (params.uuid.stringValue) {
            var lineItem = cart.getProductLineItemByUUID(params.uuid.stringValue);
            if (lineItem) {
                var productModel = Product.get(params.pid.stringValue);
                var quantity = parseInt(params.Quantity.value);

                productToAdd = productModel.object;
                productOptionModel = productModel.updateOptionSelection(params);

                Transaction.wrap(function () {
                    cart.updateLineItem(lineItem, productToAdd, quantity, productOptionModel);
                });

                if (format === 'ajax') {
                    template = 'checkout/cart/refreshcart';
                }
            } else {
                return {
                    template: 'checkout/cart/cart'
                };
            }
        // Adds a product from a product list.
        } else if (params.plid.stringValue) {
            var productList = ProductListMgr.getProductList(params.plid.stringValue);
            if (productList) {
                cart.addProductListItem(productList.getItem(params.itemid.stringValue), params.Quantity.doubleValue);
            }

        // Adds a product.
        } else {
            var previousBonusDiscountLineItems = cart.getBonusDiscountLineItems();
            productToAdd = Product.get(params.pid.stringValue);

            if (productToAdd.object.isProductSet()) {
                var childPids = params.childPids.stringValue.split(',');
                var childQtys = params.childQtys.stringValue.split(',');
                var counter = 0;

                for (var i = 0; i < childPids.length; i++) {
                    var childProduct = Product.get(childPids[i]);

                    if (childProduct.object && !childProduct.isProductSet()) {
                        var childProductOptionModel = childProduct.updateOptionSelection(params);
                        cart.addProductItem(childProduct.object, parseInt(childQtys[counter]), childProductOptionModel);
                    }
                    counter++;
                }
            } else {
                productOptionModel = productToAdd.updateOptionSelection(params);
                cart.addProductItem(productToAdd.object, params.Quantity.doubleValue, productOptionModel);
            }

            // When adding a new product to the cart, check to see if it has triggered a new bonus discount line item.
            newBonusDiscountLineItem = cart.getNewBonusDiscountLineItem(previousBonusDiscountLineItems);
        }

        return {
            format: format,
            template: template,
            BonusDiscountLineItem: newBonusDiscountLineItem
        };
    },

    /**
     * Adds a product list item to the cart and recalculates the cart.
     *
     * @alias module:models/CartModel~CartModel/addProductListItem
     * @transactional
     * @param {dw.customer.ProductListItem} productListItem The product list item whose associated product is added to the basket.
     * @param {Number} quantity The quantity of the product.
     */
    addProductListItem: function (productListItem, quantity) {

        if (productListItem) {
            var cart = this;

            Transaction.wrap(function () {
                var shipment = cart.object.defaultShipment;
                cart.createProductLineItem(productListItem, shipment).setQuantityValue(quantity);

                cart.calculate();
            });
        }
    },

    /**
     * Adds a product to the cart and recalculates the cart.
     * By default, when a bundle is added to cart, all its child products are added too, but if those products are
     * variants then the code must replace the master products with the selected variants that are passed in the
     * HTTP params as childPids along with any options.
     * @params {request.httpParameterMap.childPids} - comma separated list of
     * product IDs of the bundled products that are variations.
     *
     * @transactional
     * @alias module:models/CartModel~CartModel/addProductItem
     * @param {String} pid - ID of the product that is to be added to the basket.
     * @param {Number} quantity - The quantity of the product.
     * @param {dw.catalog.ProductOptionModel} productOptionModel - The option model of the product that is to be added to the basket.
     */
    addProductItem: function (product, quantity, productOptionModel) {
        var cart = this;
        Transaction.wrap(function () {
            var i;
            if (product) {
                var productInCart;
                var productListItems = cart.object.productLineItems;
                var quantityInCart;
                var quantityToSet;
                var shipment = cart.object.defaultShipment;

                for (var q = 0; q < cart.object.productLineItems.length; q++) {
                    if (productListItems[q].productID === product.ID) {
                        productInCart = productListItems[q];
                        break;
                    }
                }

                if (productInCart) {
                    quantityInCart = productInCart.getQuantity();
                    quantityToSet = quantity ? quantity + quantityInCart : quantityInCart + 1;
                    productInCart.setQuantityValue(quantityToSet);
                } else {
                    var productLineItem = cart.createProductLineItem(product, productOptionModel, shipment);

                    if (quantity) {
                        productLineItem.setQuantityValue(quantity);
                    }
                }

                /**
                 * By default, when a bundle is added to cart, all its child products are added too, but if those products are
                 * variants then the code must replace the master products with the selected variants that get passed in the
                 * HTTP params as childPids along with any options. Params: CurrentHttpParameterMap.childPids - comma separated list of
                 * pids of the bundled products that are variations.
                 */
                if (request.httpParameterMap.childPids.stringValue && product.bundle) {
                    var childPids = request.httpParameterMap.childPids.stringValue.split(',');

                    for (i = 0; i < childPids.length; i++) {
                        var childProduct = Product.get(childPids[i]).object;

                        if (childProduct) {
                            childProduct.updateOptionSelection(request.httpParameterMap);

                            var foundLineItem = this.getBundledProductLineItemByPID(lineItem, childProduct.isVariant() ? childProduct.masterProduct.ID : childProduct.ID);

                            if (foundLineItem) {
                                foundLineItem.replaceProduct(childProduct);
                            }
                        }
                    }
                }
                cart.calculate();
            }
        });
    },

    /**
     * Validates the supplied coupon code and if the coupon code is valid, applies the coupon code to the basket.
     * While applying the coupon code the function adds a new CouponLineItem to the basket, based on the supplied coupon code.
     * The coupon code gets set at the CouponLineItem.
     *
     * A coupon code can be invalid for the following reasons:
     *  <ul>
     *  <li>The coupon code was already added to the basket.</li>
     *  <li>A coupon code of the same coupon is already in basket. Adding a single coupon code of this coupon is sufficient to enable a promotion.
     *  Adding another coupon code of the same coupon does not make sense, since the promotion is already enabled by the previously added code.</li>
     *  <li>The number of redemptions of this coupon code is 1 and the code was already redeemed.</li>
     *  <li>The number of redemptions of this coupon code is > 1 and the maximum numbers of redemptions of this coupon code was already reached.
     *  The calculation of the redemptions is based on the number of redemptions of this coupon code in past plus
     * the number of redemptions of other coupon codes of the same coupon in the past.</li>
     *  <li>The maximum number of times this coupon can be redeemed per customer was already reached.</li>
     *  <li>The maximum number of times this coupon can be redeemed by a customer within a given time period was already reached.
     *  The calculation of the redemptions is based on the the number of redemptions of this coupon code in past plus the number of redemptions
     *  of other coupon codes of the same coupon in the past.</li>
     *  <li>The coupon code is unknown to the system.</li>
     *  <li>The coupon is not enabled.</li>
     *  <li>There exists no active promotion to which the coupon is assigned.</li>
     *  </ul>
     * In this case, no CouponLineItem is added to the basket and the returned "Status" object contains the details of
     * why the coupon was invalid.
     *
     * Status: The status object representing a detailed result of the operation. The
     * status property (Status.status) is set to 0 if the coupon was successfully applied or 1 otherwise.
     *
     * The code property (Status.code) is set to one of the following values:
     *  <ul>
     *  <li>"OK" = The coupon was applied to the basket.</li>
     *  <li>CouponStatusCodes.COUPON_CODE_ALREADY_IN_BASKET = Indicates that coupon code was already added to the basket.</li>
     *  <li>CouponStatusCodes.COUPON_ALREADY_IN_BASKET = Indicates that another code of the same MultiCode/System coupon was already added to basket.</li>
     *  <li>CouponStatusCodes.COUPON_CODE_ALREADY_REDEEMED = Indicates that code of MultiCode/System coupon was already redeemed.</li>
     *  <li>CouponStatusCodes.COUPON_CODE_UNKNOWN = Indicates that coupon not found for given coupon code or that the code itself was not found.</li>
     *  <li>CouponStatusCodes.COUPON_DISABLED = Indicates that coupon is not enabled.</li>
     *  <li>CouponStatusCodes.REDEMPTION_LIMIT_EXCEEDED = Indicates that number of redemptions per code exceeded.</li>
     *  <li>CouponStatusCodes.CUSTOMER_REDEMPTION_LIMIT_EXCEEDED = Indicates that number of redemptions per code and customer exceeded.</li>
     *  <li>CouponStatusCodes.TIMEFRAME_REDEMPTION_LIMIT_EXCEEDED = Indicates that number of redemptions per code, customer and time exceeded.</li>
     *  <li>CouponStatusCodes.NO_ACTIVE_PROMOTION = Indicates that coupon is not assigned to an active promotion.</li>
     * </ul>
     *
     * @transactional
     * @alias module:models/CartModel~CartModel/addCoupon
     * @param {String} couponCode - The code of the coupon to add.
     * @returns {dw.system.Status} The "Status" object containing details of the add to cart action.
     */
    addCoupon: function (couponCode) {
        if (couponCode) {
            var cart = this;
            var campaignBased = true;
            var addCouponToBasketResult;
            
            try {
                addCouponToBasketResult = Transaction.wrap(function () {
                    return cart.object.createCouponLineItem(couponCode, campaignBased); 
                });
            } catch (e) {
                return {CouponStatus: e.errorCode};
            }
            
            Transaction.wrap(function (){
                cart.calculate();            	
            });
            return {CouponStatus: addCouponToBasketResult.statusCode};
        }
    },

    /**
     * Adds a bonus product to the cart associated with the specified BonusDiscountLineItem. The function creates
     * and returns a ProductLineItem by assigning the specified Product and Quantity to the cart. The function adds
     * the new ProductLineItem to the default shipment.
     * The product parameter must be one of the products associated with the BonusDiscountLineItem or the process
     * fails. The process does not validate if the number of bonus products exceeds the maximum allowed by the bonus
     * discount. This is the job of application logic.
     * The function always creates a new product line item, regardless of the value of the site preference
     * 'Add Product Behavior'.
     *
     * @transactional
     * @alias module:models/CartModel~CartModel/addBonusProduct
     * @param {dw.order.BonusDiscountLineItem} bonusDiscountLineItem - Line item representing an applied BonusChoiceDiscount in the basket. The
     * product must be in the bonus product list of this discount.
     * @param {dw.catalog.Product} product - The product that is to be added to the basket.
     * @param {dw.util.ArrayList} selectedOptions - Product option array of optionName/optionValue pairs.
     *
     *
     * @returns {dw.order.ProductLineItem} The newly created product line item.
     */
    addBonusProduct: function (bonusDiscountLineItem, product, selectedOptions, quantity) {
        // TODO: Should this actually be using the dw.catalog.ProductOptionModel.UpdateProductOptionSelections method instead?
        var UpdateProductOptionSelections = require('app_storefront_core/cartridge/scripts/cart/UpdateProductOptionSelections');
        var ScriptResult = UpdateProductOptionSelections.update({
            SelectedOptions: selectedOptions,
            Product: product
        });
        var shipment = null;
        var productLineItem = this.object.createBonusProductLineItem(bonusDiscountLineItem, product, ScriptResult, shipment);

        if (quantity) {
            productLineItem.setQuantityValue(quantity);
        }

        return productLineItem;
    },

    /**
     * Deletes all the products associated with a bonus discount line item.
     *
     * @transactional
     * @alias module:models/CartModel~CartModel/removeBonusDiscountLineItemProducts
     * @param {dw.order.BonusDiscountLineItem} bonusDiscountLineItem - The bonus discount line item to remove the
     * product line items for.
     */
    removeBonusDiscountLineItemProducts: function (bonusDiscountLineItem) {
        // TODO - add check whether the given line item actually belongs to this cart object.
        var plis = bonusDiscountLineItem.getBonusProductLineItems();

        for (var i = 0; i < plis.length; i++) {
            var pli = plis[i];
            if (pli.product) {
                this.removeProductLineItem(pli);
            }
        }
    },

    /**
     * Returns the product line item of the cart with a specific UUID.
     *
     * @alias module:models/CartModel~CartModel/getProductLineItemByUUID
     * @param {String} lineItemUUID - The UUID of the line item.
     * @returns {dw.order.ProductLineItem} The product line item or null if not found.
     */
    getProductLineItemByUUID: function (lineItemUUID) {
        var plis = this.getProductLineItems();
        var lineItem = null;

        for (var i = 0, il = plis.length; i < il; i++) {
            var item = plis[i];
            if ((lineItemUUID && item.UUID === lineItemUUID)) {
                lineItem = item;
                break;
            }
        }

        return lineItem;
    },

    /**
     * Searches a bundle line item for a product line item with a specific product ID.
     * @alias module:models/CartModel~CartModel/getBundledProductLineItemByPID
     * @param {dw.order.ProductLineItem} bundleLineItem - The bundle product line item, which contains two or more product line items.
     * @param {String} pid - The product identifier of the product line item to find.
     * @returns {dw.order.ProductLineItem} The product line item or null if not found.
     */
    getBundledProductLineItemByPID: function (bundleLineItem, pid) {
        // TODO - add check whether the given line item actually belongs to this cart object
        var plis = bundleLineItem.getBundledProductLineItems();
        var lineItem = null;

        for (var i = 0, il = plis.length; i < il; i++) {
            var item = plis[i];
            if ((pid && item.productID === pid)) {
                lineItem = item;
                break;
            }
        }

        return lineItem;
    },

    /**
     * Returns the bonus discount line item of the cart having the given UUID.
     *
     * @alias module:models/CartModel~CartModel/getBonusDiscountLineItemByUUID
     * @param {String} lineItemUUID - The UUID of the bonus discount line item.
     * @returns {dw.order.BonusDiscountLineItem} The bonus discount line item or null if not found.
     */
    getBonusDiscountLineItemByUUID: function (lineItemUUID) {
        var plis = this.getBonusDiscountLineItems();
        var lineItem = null;

        for (var i = 0, il = plis.length; i < il; i++) {
            var item = plis[i];
            if ((lineItemUUID && item.UUID === lineItemUUID)) {
                lineItem = item;
                break;
            }
        }

        return lineItem;
    },

    /**
     * Checks the in-store quantity of all product line items
     * against the store inventory in case the product line item's quantity was
     * updated. If the store inventory does not have as many of the item in stock as is being purchased,
     * the product line item is converted from in-store pickup to home delivery.
     * @transactional
     * @alias module:models/CartModel~CartModel/checkInStoreProducts
     */
    checkInStoreProducts: function () {
        if (dw.system.Site.getCurrent().getCustomPreferenceValue('enableStorePickUp')) {

            var allProductLineItems = this.getAllProductLineItems();
            for (var i = 0; i < allProductLineItems.length; i++) {
                var pli = allProductLineItems[i];

                //Skip product line items that are not in-store.
                if (pli.custom.fromStoreId) {
                    //Check the quantity against the inventory of the store with matching storeID,
                    //if the cart is being updated with a new quantity.
                    var store = StoreMgr.getStore(pli.custom.fromStoreId);
                    var storeinventory = ProductInventoryMgr.getInventoryList(store.custom.inventoryListId);

                    if (storeinventory.getRecord(pli.productID).ATS.value >= pli.quantityValue) {
                        pli.custom.fromStoreId = store.ID;
                        pli.setProductInventoryList(storeinventory);

                    } else {
                        //The in-store line item is reset to a regular home delivery item.
                        pli.custom.fromStoreId = '';
                        pli.setProductInventoryList(null);
                        pli.setShipment(this.getDefaultShipment());
                    }
                }
            }
        }
    },

    /**
     * Checks to see if a new bonus discount line item was created by providing a list of 'previous' discount
     * line items.
     *
     * @alias module:models/CartModel~CartModel/getNewBonusDiscountLineItems
     * @param {dw.util.Collection} previousBonusDiscountLineItems - Baseline collection of bonus discount line items.
     * @returns {dw.order.BonusDiscountLineItem} The newly created bonus discount line item.
     */
    getNewBonusDiscountLineItem: function (previousBonusDiscountLineItems) {
        var newBonusDiscountLineItems = this.getBonusDiscountLineItems();
        var newBonusDiscountLineItem;

        var iter = newBonusDiscountLineItems.iterator();
        while (iter.hasNext()) {
            var newItem = iter.next();
            // if there is a new discount line item, return it right away
            if (!previousBonusDiscountLineItems.contains(newItem)) {
                newBonusDiscountLineItem = newItem;
                break;
            }
        }
        return newBonusDiscountLineItem;
    },

    /**
     * Replaces the current product of the specified product line item with the specified product.
     *
     * By default, when a bundle is added to a cart, all its child products are added as well.
     * However, if those products are variants then the master products must be replaced
     * with the selected variants that get passed in the CurrentHttpParameterMap.childPids as comma separated list of
     * pids of the bundled products that are variations and any options.
     *
     * @transactional
     * @alias module:models/CartModel~CartModel/updateLineItem
     * @param {dw.order.ProductLineItem} lineItem - The product line item to replace.
     * @param {dw.catalog.Product} product - The new product.
     * @param {Number} quantity - The quantity of the product line item after replacement.
     * @param {dw.catalog.ProductOptionModel} productOptionModel - The option model of the product to add to the basket.
     */
    updateLineItem: function (lineItem, product, quantity, productOptionModel) {
        var optionValues = productOptionModel.getOptions();

        lineItem.replaceProduct(product);
        lineItem.setQuantityValue(quantity);

        if (optionValues.length) {
            lineItem.updateOptionValue(optionValues[0]);
        }

        if (product.isBundle()) {

            if (request.httpParameterMap.childPids.stringValue) {
                var childPids = request.httpParameterMap.childPids.stringValue.split(',');

                for (var i = 0; i < childPids.length; i++) {
                    var childProduct = Product.get(childPids[i]).object;

                    if (childProduct) {
                        // why is this needed ?
                        childProduct.updateOptionSelection(request.httpParameterMap);

                        var foundLineItem = null;
                        foundLineItem = this.getBundledProductLineItemByPID(lineItem, (childProduct.isVariant() ? childProduct.masterProduct.ID : childProduct.ID));

                        if (foundLineItem) {
                            foundLineItem.replaceProduct(childProduct);
                        }
                    }
                }
            }
        }
    },

    /**
     * Implements a typical shopping cart checkout validation. Some parts of the validation script are specific to
     * the reference application logic and might not be applicable to our customer's storefront applications.
     * However, the shopping cart validation script can be customized to meet specific needs and requirements.
     *
     * This function implements the validation of the shopping cart against specific
     * conditions in the following steps:
     * - validate that total price is not N/A
     * - validate that all products in the basket are still in site catalog and online
     * - validate that all coupon codes in the basket are valid
     * - validate that the taxes can be calculated for all products in the basket (if ValidateTax input parameter is true)
     *
     * @alias module:models/CartModel~CartModel/validateForCheckout
     * @returns {dw.system.Status} BasketStatus
     * @returns {Boolean} EnableCheckout
     */
    validateForCheckout: function () {
        var ValidateCartForCheckout = require('app_storefront_core/cartridge/scripts/cart/ValidateCartForCheckout');
        return ValidateCartForCheckout.validate({
            Basket: this.object,
            ValidateTax: false
        });
    },

    /**
     * The function removes all empty shipments of the current cart.
     *
     * @transactional
     * @alias module:models/CartModel~CartModel/removeEmptyShipments
     */
    removeEmptyShipments: function () {
        var that = this;
        // Gets the list of shipments.
        var shipments = that.getShipments();

        dw.system.Transaction.wrap(function () {
            for (var i = 0; i < shipments.length; i++) {
                var shipment = shipments[i];

                if (!shipment.isDefault()) {
                    if (shipment.getProductLineItems().isEmpty() && shipment.getGiftCertificateLineItems().isEmpty()) {
                        that.removeShipment(shipment);
                    }
                }
            }
        });
    },

    /**
     * Determines the physical shipments of the current cart. Physical shipments are shipments that contain at
     * least one product line item. A shipment that contains only gift certificates is not a physical shipment.
     * Product line items marked for in-store pickup are also not considered physical shipments.
     *
     * @alias module:models/CartModel~CartModel/getPhysicalShipments
     * @returns {dw.util.ArrayList} List of physical shipments.
     */
    getPhysicalShipments: function () {

        // list of physical shipments
        var physicalShipments = new ArrayList();

        // find physical shipments
        var shipments = this.getShipments();

        for (var i = 0; i < shipments.length; i++) {
            var shipment = shipments[i];
            if (!shipment.getProductLineItems().isEmpty() && shipment.custom.shipmentType !== 'instore') {
                physicalShipments.add(shipment);
            }
        }

        return physicalShipments;
    },

    /**
     * Cleans the shipments of the current basket by putting all gift certificate line items to single, possibly
     * new, shipments, with one shipment per gift certificate line item.
     *
     * @alias module:models/CartModel~CartModel/updateGiftCertificateShipments
     * @transactional
     */
    updateGiftCertificateShipments: function () {

        // List of line items.
        var giftCertificatesLI = new ArrayList();

        // Finds gift certificates in shipments that have
        // product line items and gift certificate line items merged.
        var shipments = this.getShipments();

        for (var i = 0; i < shipments.length; i++) {
            var shipment = shipments[i];

            // Skips shipment if no gift certificates are contained.
            if (shipment.giftCertificateLineItems.size() === 0) {
                continue;
            }

            // Skips shipment if it has no products and just one gift certificate is contained.
            if (shipment.productLineItems.size() === 0 && shipment.giftCertificateLineItems.size() === 1) {
                continue;
            }

            // If there are gift certificates, add them to the list.
            if (shipment.giftCertificateLineItems.size() > 0) {
                giftCertificatesLI.addAll(shipment.giftCertificateLineItems);
            }
        }

        // Create a shipment for each gift certificate line item.
        for (var n = 0; n < giftCertificatesLI.length; n++) {
            var newShipmentID = this.determineUniqueShipmentID('Shipment #');
            giftCertificatesLI[n].setShipment(this.createShipment(newShipmentID));
        }
    },

    /**
     * Determines a unique shipment ID for shipments in the current cart and the given base ID. The function appends
     * a counter to the base ID and checks the existence of the resulting ID. If the resulting ID is unique, this ID
     * is returned; if not, the counter is incremented and checked again.
     *
     * @alias module:models/CartModel~CartModel/determineUniqueShipmentID
     * @param {String} baseID - The base ID.
     * @returns {String} Calculated shipment ID.
     */
    determineUniqueShipmentID: function (baseID) {
        var counter = 1;
        var shipment = null;
        var candidateID = baseID + '' + counter;

        while (shipment === null) {
            shipment = this.getShipment(candidateID);
            if (shipment) {
                // This ID is already taken, increment the counter
                // and try the next one.
                counter++;
                candidateID = baseID + '' + counter;
                shipment = null;
            } else {
                return candidateID;
            }
        }

        // Should never go here
        return null;
    },

    /**
     * Creates a shipping address for the shipment with the given shipment ID.
     *
     * @transactional
     * @alias module:models/CartModel~CartModel/createShipmentShippingAddress
     * @param {String} shipmentID - The ID of the shipment to create the shipping address for.
     * @returns {dw.order.OrderAddress} The created shipping address.
     */
    createShipmentShippingAddress: function (shipmentID) {

        var shipment = this.getShipment(shipmentID);
        var shippingAddress = shipment.getShippingAddress();

        // If the shipment has no shipping address yet, create one.
        if (shippingAddress === null) {
            shippingAddress = shipment.createShippingAddress();
        }

        return shippingAddress;

    },

    /**
     * Scans the basket and consolidates items that are going to the same store. It also creates shipments
     * with shipment type and method for the rest of checkout.
     *
     * @transactional
     * @alias module:models/CartModel~CartModel/consolidateInStoreShipments
     * @returns {boolean} true if there is a home delivery found in the basket, false otherwise.
     */
    consolidateInStoreShipments: function () {
        var sliArrayList = new ArrayList();
        var homeDeliveries = false;
        var storeObject, shippingAddress, orderAddress;

        var plis = this.getAllProductLineItems();
        for (var i = 0; i < plis.length; i++) {
            var pli = plis[i];

            if (pli.custom.fromStoreId === null) {
                //Skips line items that are not in-store product line items.
                homeDeliveries = true;
                continue;
            }
            if (pli.shipment.shippingMethod && pli.shipment.shippingMethod.custom.storePickupEnabled) {
                //Checks to see if the store id changed.
                if (pli.custom.fromStoreId === pli.shipment.custom.fromStoreId) {
                    if (pli.shipment.shippingAddress) {
                        continue;
                    } else {
                        //Creates the shipment address to reflect the new store address.
                        shippingAddress = new TransientAddress();
                        storeObject = StoreMgr.getStore(pli.custom.fromStoreId);
                        orderAddress = pli.shipment.createShippingAddress();
                        shippingAddress.storeAddressTo(orderAddress, storeObject);
                        continue;
                    }
                } else {
                    storeObject = StoreMgr.getStore(pli.custom.fromStoreId);
                    //Changes the shipment address to reflect the new store address.
                    pli.shipment.shippingAddress.setFirstName('');
                    pli.shipment.shippingAddress.setLastName(storeObject.name);
                    pli.shipment.shippingAddress.setAddress1(storeObject.address1);
                    pli.shipment.shippingAddress.setAddress2(storeObject.address2);
                    pli.shipment.shippingAddress.setCity(storeObject.city);
                    pli.shipment.shippingAddress.setPostalCode(storeObject.postalCode);
                    pli.shipment.shippingAddress.setStateCode(storeObject.stateCode);
                    pli.shipment.shippingAddress.setCountryCode(storeObject.custom.countryCodeValue);
                    pli.shipment.shippingAddress.setPhone(storeObject.phone);
                    pli.shipment.custom.fromStoreId = pli.custom.fromStoreId;
                    continue;
                }
            }

            //Checks whether the function is creating a new shipment or adding to an existing one.
            if (sliArrayList.contains(pli.custom.fromStoreId)) {
                //Adds the product line item to the existing shipment.
                //Loops through to find the shipment with the storeid and set it as the shipment for the pli.
                var shipments = this.getShipments();
                for (var j = 0; j < this.getShipments().length; j++) {
                    var inStoreShipment = shipments[j];

                    if (inStoreShipment.custom.fromStoreId && (pli.custom.fromStoreId === inStoreShipment.custom.fromStoreId)) {
                        //If an existing shipment that has the correct address is found.
                        pli.setShipment(inStoreShipment);
                    }

                }

            } else {
                //create a new shipment to put this product line item in
                var shipment = null;
                shipment = this.createShipment(UUIDUtils.createUUID());
                shipment.custom.fromStoreId = pli.custom.fromStoreId;
                shipment.custom.shipmentType = 'instore';

                //Loops over the shipping methods and picks the in-store method.
                var shippingMethods = new ArrayList(ShippingMgr.getShipmentShippingModel(shipment).getApplicableShippingMethods());
                for (var k = 0; k < shippingMethods.length; k++) {
                    var shippingMethod = shippingMethods[k];

                    if (shippingMethod.custom.storePickupEnabled) {
                        shipment.setShippingMethod(shippingMethod);
                    }

                }

                shippingAddress = new TransientAddress();
                storeObject = StoreMgr.getStore(pli.custom.fromStoreId);
                orderAddress = shipment.createShippingAddress();
                shippingAddress.storeAddressTo(orderAddress, storeObject);
                pli.setShipment(shipment);

            }

            sliArrayList.add(pli.custom.fromStoreId);
        }

        return homeDeliveries;
    },

    /**
     * Updates the shipping method of the given shipment. If a shipping method ID is given, the given
     * shipping method is used to update the shipment.
     *
     * @alias module:models/CartModel~CartModel/preCalculateShipping
     * @param {dw.order.ShippingMethod} shippingMethod - A shipping method.
     * @returns {Object} Returns the following object:
     * <code><pre>
     * "shippingExclDiscounts"         : this.getShippingTotalPrice(),
     * "shippingInclDiscounts"         : this.getAdjustedShippingTotalPrice(),
     * "productShippingCosts"          : productShippingCosts,
     * "productShippingDiscounts"      : productShippingDiscounts,
     * "shippingPriceAdjustments"      : priceAdjArray,
     * "shippingPriceAdjustmentsTotal" : priceAdjTotal,
     * "surchargeAdjusted"             : adustedSurchargeTotal,
     * "baseShipping"                  : baseShipping,
     * "baseShippingAdjusted"          : baseShippingAdjusted
     * </pre></code>
     */
    preCalculateShipping: function (shippingMethod) {

        var shipment = this.getDefaultShipment();

        if (shipment) {
            var currencyCode = this.getCurrencyCode();
            var productShippingCosts     = [], // array to hold product level shipping costs (fixed or surcharges), each entry is an object containing product name and shipping cost
                productShippingDiscounts = new ArrayList(), // list holds all products shipping discounts NOT promotions e.g. fixed shipping discount or free shipping for individual products discount
                productIter              = this.getAllProductLineItems().iterator(),
                priceAdj,
                priceAdjArray            = [], // array to hold shipping price adjustments data (we have to create objects since price adjustments get lost after applying a shipping method
                priceAdjIter             = shipment.getShippingPriceAdjustments().iterator(),
                priceAdjTotal            = new Money(0.0, currencyCode), // total of all price adjustments
                surchargeTotal           = new Money(0.0, currencyCode), // total of all surcharges
                adustedSurchargeTotal    = new Money(0.0, currencyCode); // total of all surcharges minus price adjustments

            // Iterates over all products in the basket
            // and calculates their shipping cost and shipping discounts
            while (productIter.hasNext()) {
                var pli = productIter.next();
                var product = pli.product;
                if (product) {
                    var psc = ShippingMgr.getProductShippingModel(product).getShippingCost(shippingMethod);
                    productShippingCosts[productShippingCosts.length] = {
                        name: product.name,
                        shippingCost: psc,
                        qty: pli.getQuantity()
                    };
                    if (psc && psc.getAmount() && psc.isSurcharge()) {
                        // update the surcharge totals
                        surchargeTotal = surchargeTotal.add(psc.getAmount());
                        adustedSurchargeTotal = adustedSurchargeTotal.add(psc.getAmount());
                    }
                    //productShippingDiscounts.addAll(discountPlan.getProductShippingDiscounts(pli));
                    //productShippingDiscounts.addAll(pli.shippingLineItem.priceAdjustments);
                    if (pli.shippingLineItem) {
                        var pdiscountsiter = pli.shippingLineItem.priceAdjustments.iterator();
                        while (pdiscountsiter.hasNext()) {
                            priceAdj = pdiscountsiter.next();
                            if (priceAdj && priceAdj.promotion !== null) {
                                if (pli.shippingLineItem.isSurcharge()) {
                                    // adjust the surchage total value
                                    adustedSurchargeTotal = adustedSurchargeTotal.add(priceAdj.price);
                                }
                                productShippingDiscounts.add({
                                    price: priceAdj.price,
                                    calloutMsg: priceAdj.promotion.calloutMsg
                                });
                            }
                        }
                    }
                }
            }

            // Iterates over all shipping price adjustments and
            // grabs price and calloutMsg objects.
            while (priceAdjIter.hasNext()) {
                priceAdj = priceAdjIter.next();
                if (priceAdj && priceAdj.promotion !== null) {
                    priceAdjTotal = priceAdjTotal.add(priceAdj.price);
                    priceAdjArray[priceAdjArray.length] = {
                        price: priceAdj.price,
                        calloutMsg: priceAdj.promotion.calloutMsg
                    };
                }
            }

            var baseShipping = this.getShippingTotalPrice().subtract(surchargeTotal);
            var baseShippingAdjusted = null;
            if (priceAdjTotal >= 0) {
                baseShippingAdjusted = baseShipping.subtract(priceAdjTotal);
            } else {
                baseShippingAdjusted = baseShipping.add(priceAdjTotal);
            }

            return {
                shippingExclDiscounts: this.getShippingTotalPrice(),
                shippingInclDiscounts: this.getAdjustedShippingTotalPrice(),
                productShippingCosts: productShippingCosts,
                productShippingDiscounts: productShippingDiscounts,
                shippingPriceAdjustments: priceAdjArray,
                shippingPriceAdjustmentsTotal: priceAdjTotal,
                surchargeAdjusted: adustedSurchargeTotal,
                baseShipping: baseShipping,
                baseShippingAdjusted: baseShippingAdjusted
            };
        }
    },

    /**
     * Sets the shipping method of the given shipment to the passed method.  The list of allowed shipping
     * methods may be passed in as a parameter.  If not, then it is retrieved using ShipmentShippingModel.getApplicableShippingMetods().
     * If the passed shipping method is not in this list, then the function uses the default shipping method.
     * If the default shipping method is not in the list, the function uses the first method in the list.
     *
     * @transactional
     * @alias module:models/CartModel~CartModel/updateShipmentShippingMethod
     * @param {String} shipmentID - The ID of the shipment to update the shipping method for.
     * @param {String} shippingMethodID - The ID of the shipping method to set for the shipment.
     * @param {dw.order.ShippingMethod} shippingMethod - The shipping method to set for the shipment.
     * @param {dw.util.Collection} shippingMethods - The list of applicable shipping methods.
     */
    updateShipmentShippingMethod: function (shipmentID, shippingMethodID, shippingMethod, shippingMethods) {

        var shipment = this.getShipment(shipmentID);

        if (!shippingMethods) {
            shippingMethods = ShippingMgr.getShipmentShippingModel(shipment).getApplicableShippingMethods();
        }

        // Tries to set the shipment shipping method to the passed one.
        for (var i = 0; i < shippingMethods.length; i++) {
            var method = shippingMethods[i];

            if (!shippingMethod) {
                if (!method.ID.equals(shippingMethodID)) {
                    continue;
                }
            } else {
                if (method !== shippingMethod) {
                    continue;
                }

            }

            // Sets this shipping method.
            shipment.setShippingMethod(method);
            return;
        }

        var defaultShippingMethod = ShippingMgr.getDefaultShippingMethod();
        if (shippingMethods.contains(defaultShippingMethod)) {
            // Sets the default shipping method if it is applicable.
            shipment.setShippingMethod(defaultShippingMethod);
        } else if (shippingMethods.length > 0) {
            // Sets the first shipping method in the applicable list.
            shipment.setShippingMethod(shippingMethods.iterator().next());
        } else {
            // Invalidates the current shipping method selection.
            shipment.setShippingMethod(null);
        }

        return;
    },

    /**
     * Retrieves the list of applicable shipping methods for a given shipment and a full or partial shipping address.
     * A shipping method is applicable if it does not exclude any of the products in the shipment, and does not
     * exclude the specified address.
     *
     * @alias module:models/CartModel~CartModel/getApplicableShippingMethods
     * @param {module:models/TransientAddressModel~TransientAddressModel} address - The address to get the applicable shipping
     * methods for.
     * @returns {dw.util.Collection} The list of applicable shipping methods for the default shipment and the given address.
     */
    getApplicableShippingMethods: function (address) {
        // Modify as needed.
        if (!address.countryCode) {
            address.countryCode = 'US';
        }
        if (!address.stateCode) {
            address.stateCode = 'NY';
        }

        // Retrieves the list of applicable shipping methods for the given shipment and address.
        return ShippingMgr.getShipmentShippingModel(this.getDefaultShipment()).getApplicableShippingMethods(address);
    },

    /**
     * Calculates the amount to be paid by a non-gift certificate payment instrument based on the given basket.
     * The function subtracts the amount of all redeemed gift certificates from the order total and returns this
     * value.
     *
     * @alias module:models/CartModel~CartModel/getNonGiftCertificateAmount
     * @returns {dw.value.Money} The amount to be paid by a non-gift certificate payment instrument.
     */
    getNonGiftCertificateAmount: function () {
        // The total redemption amount of all gift certificate payment instruments in the basket.
        var giftCertTotal = new Money(0.0, this.getCurrencyCode());

        // Gets the list of all gift certificate payment instruments
        var gcPaymentInstrs = this.getGiftCertificatePaymentInstruments();
        var iter = gcPaymentInstrs.iterator();
        var orderPI = null;

        // Sums the total redemption amount.
        while (iter.hasNext()) {
            orderPI = iter.next();
            giftCertTotal = giftCertTotal.add(orderPI.getPaymentTransaction().getAmount());
        }

        // Gets the order total.
        var orderTotal = this.getTotalGrossPrice();

        // Calculates the amount to charge for the payment instrument.
        // This is the remaining open order total that must be paid.
        var amountOpen = orderTotal.subtract(giftCertTotal);

        // Returns the open amount to be paid.
        return amountOpen;
    },

    /**
     * Removes a gift certificate payment instrument with the given gift certificate ID
     * from the basket.
     *
     * @transactional
     * @alias module:models/CartModel~CartModel/removeGiftCertificatePaymentInstrument
     * @param {String} giftCertificateID - The ID of the gift certificate to remove the payment instrument for.
     */
    removeGiftCertificatePaymentInstrument: function (giftCertificateID) {

        // Iterates over the list of payment instruments.
        var gcPaymentInstrs = this.getGiftCertificatePaymentInstruments(giftCertificateID);
        var iter = gcPaymentInstrs.iterator();
        var existingPI = null;

        // Remove (one or more) gift certificate payment
        // instruments for this gift certificate ID.
        while (iter.hasNext()) {
            existingPI = iter.next();
            this.removePaymentInstrument(existingPI);
        }

        return;
    },

    /**
     * Deletes multiple payment instruments.
     *
     * @transactional
     * @alias module:models/CartModel~CartModel/removePaymentInstruments
     * @param {dw.util.Collection} paymentInstruments - The payment instruments to remove.
     */
    removePaymentInstruments: function (paymentInstruments) {

        for (var i = 0; i < paymentInstruments.length; i++) {
            var pi = paymentInstruments[i];
            this.removePaymentInstrument(pi);
        }

    },

    /**
     * Calculates the total amount of an order paid for by gift certificate payment
     * instruments. Any remaining open amount is applied to the non-gift certificate payment
     * instrument, such as a credit card. <b>Note:</b> this script assumes that only one non-gift certificate
     * payment instrument is used for the payment.
     *
     * @alias module:models/CartModel~CartModel/calculatePaymentTransactionTotal
     * @returns {boolean} false in the case of an error or if the amount of the transaction is not covered, true otherwise.
     */
    calculatePaymentTransactionTotal: function () {

        // Gets all payment instruments for the basket.
        var iter = this.getPaymentInstruments().iterator();
        var paymentInstrument = null;
        var nonGCPaymentInstrument = null;
        var giftCertTotal = new Money(0.0, this.getCurrencyCode());

        // Locates a non-gift certificate payment instrument if one exists.
        while (iter.hasNext()) {
            paymentInstrument = iter.next();
            if (PaymentInstrument.METHOD_GIFT_CERTIFICATE.equals(paymentInstrument.getPaymentMethod())) {
                giftCertTotal = giftCertTotal.add(paymentInstrument.getPaymentTransaction().getAmount());
                continue;
            }

            // Captures the non-gift certificate payment instrument.
            nonGCPaymentInstrument = paymentInstrument;
            break;
        }

        // Gets the order total.
        var orderTotal = this.getTotalGrossPrice();

        // If a gift certificate payment and non-gift certificate payment
        // instrument are found, the function returns true.
        if (!nonGCPaymentInstrument) {
            // If there are no other payment types and the gift certificate
            // does not cover the open amount, then return false.
            if (giftCertTotal < orderTotal) {
                return false;
            } else {
                return true;
            }
        }

        // Calculates the amount to be charged for the
        // non-gift certificate payment instrument.
        var amount = this.getNonGiftCertificateAmount();

        // now set the non-gift certificate payment instrument total.
        if (amount.value <= 0.0) {
            var zero = new Money(0, amount.getCurrencyCode());
            nonGCPaymentInstrument.getPaymentTransaction().setAmount(zero);
        } else {
            nonGCPaymentInstrument.getPaymentTransaction().setAmount(amount);
        }

        return true;
    },

    /**
     * Creates a list of gift certificate ids from gift certificate payment instruments.
     *
     * @alias module:models/CartModel~CartModel/getGiftCertIdList
     * @returns {dw.util.ArrayList} The list of gift certificate IDs.
     */
    getGiftCertIdList: function () {
        var gcIdList = new ArrayList();
        var gcPIIter = this.getGiftCertificatePaymentInstruments.iterator();

        while (gcPIIter.hasNext()) {
            gcIdList.add((gcPIIter.next()).getGiftCertificateCode());
        }

        return gcIdList;
    },

    /**
     * Verifies whether existing non-gift-certificate payment instrument methods or cards are still applicable.
     * Returns the collection of valid and invalid payment instruments.
     *
     * @alias module:models/CartModel~CartModel/validatePaymentInstruments
     * @param {dw.customer.Customer} customer - The current customer.
     * @param {String} countryCode - The country code.
     * @param {Number} amount - The payment amount.
     *
     * @returns {dw.util.Collection} ValidPaymentInstruments - The collection of valid payment instruments.
     * @returns {dw.util.Collection} InvalidPaymentInstruments - The collection of invalid payment instruments.
     */
    validatePaymentInstruments: function (customer, countryCode, amount) {
        var paymentHelpers = require('~/cartridge/scripts/payment/common');
        return paymentHelpers.validatePaymentInstruments(this, countryCode, amount);
    },

    /**
     * Creates a gift certificate payment instrument from the given gift certificate ID for the basket. The
     * method attempts to redeem the current balance of the gift certificate. If the current balance exceeds the
     * order total, this amount is redeemed and the balance is lowered.
     *
     * @transactional
     * @alias module:models/CartModel~CartModel/createGiftCertificatePaymentInstrument
     * @param {dw.order.GiftCertificate} giftCertificate - The gift certificate.
     * @returns {dw.order.PaymentInstrument} The created PaymentInstrument.
     */
    createGiftCertificatePaymentInstrument: function (giftCertificate) {

        // Removes any duplicates.
        // Iterates over the list of payment instruments to check.
        var gcPaymentInstrs = this.getGiftCertificatePaymentInstruments(giftCertificate.getGiftCertificateCode()).iterator();
        var existingPI = null;

        // Removes found gift certificates, to prevent duplicates.
        while (gcPaymentInstrs.hasNext()) {
            existingPI = gcPaymentInstrs.next();
            this.removePaymentInstrument(existingPI);
        }

        // Fetches the balance and the order total.
        var balance = giftCertificate.getBalance();
        var orderTotal = this.getTotalGrossPrice();

        // Sets the amount to redeem equal to the remaining balance.
        var amountToRedeem = balance;

        // Since there may be multiple gift certificates, adjusts the amount applied to the current
        // gift certificate based on the order total minus the aggregate amount of the current gift certificates.

        var giftCertTotal = new Money(0.0, this.getCurrencyCode());

        // Iterates over the list of gift certificate payment instruments
        // and updates the total redemption amount.
        gcPaymentInstrs = this.getGiftCertificatePaymentInstruments().iterator();
        var orderPI = null;

        while (gcPaymentInstrs.hasNext()) {
            orderPI = gcPaymentInstrs.next();
            giftCertTotal = giftCertTotal.add(orderPI.getPaymentTransaction().getAmount());
        }

        // Calculates the remaining order balance.
        // This is the remaining open order total that must be paid.
        var orderBalance = orderTotal.subtract(giftCertTotal);

        // The redemption amount exceeds the order balance.
        // use the order balance as maximum redemption amount.
        if (orderBalance < amountToRedeem) {
            // Sets the amount to redeem equal to the order balance.
            amountToRedeem = orderBalance;
        }

        // Creates a payment instrument from this gift certificate.
        return this.object.createGiftCertificatePaymentInstrument(giftCertificate.getGiftCertificateCode(), amountToRedeem);
    },

    /**
     * Creates a new QuantityLineItem helper object for each quantity of a ProductLineItem, if one does not already exist.
     * It does not create QuantityLineItems for products using in-store pickup as the shipping method.
     *
     * @alias module:models/CartModel~CartModel/separateQuantities
     * @param {dw.order.ProductLineItem} pli - The ProductLineItem.
     * @param {dw.util.ArrayList} quantityLineItems - The existing QuantityLineItems.
     * @returns {dw.util.ArrayList} A list of separated QuantityLineItems.
     */
    separateQuantities: function (pli, quantityLineItems) {

        var quantity = pli.quantityValue;

        // Creates new ArrayList if there are no QLIs
        if (!quantityLineItems) {
            quantityLineItems = new ArrayList();
        }

        // Creates for each quantity of the ProductLineItem a new QuantityLineItem.
        for (var i = 0; i < quantity; i++) {
            //Skips plis that are using the in-store pick up shipping method.
            if (empty(pli.custom.fromStoreId)) {
                quantityLineItems.add(new QuantityLineItem(pli));
            }
        }

        return quantityLineItems;
    },

    /**
     * Loads customer addresses and shipment addresses and stores them into the session address book attribute of the cart,
     * if they are available and configured in Business Manager.
     * @alias module:models/CartModel~CartModel/initAddressBook
     * @transactional
     *
     * @param {dw.customer.Customer} customer - The customer to load the addresses from.
     */
    initAddressBook: function (customer) {

        var shipments = this.getShipments();

        // Loads addresses from the customer address book.
        if (customer.registered && customer.addressBook) {
            for (var i = 0; i < customer.addressBook.addresses.length; i++) {
                this.addAddressToAddressBook(customer.addressBook.addresses[i]);
            }
        }

        // Loads addresses from Shipments excluding in-store shipments.
        if (shipments) {
            for (var k = 0; k < shipments.length; k++) {
                var shipment = shipments[k];
                if (shipment.shippingAddress && shipment.custom.shipmentType !== 'instore') {
                    this.addAddressToAddressBook(shipment.getShippingAddress());
                }
            }
        }
    },

    /**
     * Returns all addresses of the cart's address book. The addresses are stored as a JSON objects in a custom
     * attribute named 'sessionAddressBook'.
     *
     * @alias module:models/CartModel~CartModel/getAddressBookAddresses
     * @returns {dw.util.ArrayList} An ArrayList containing a addresses of the addresses stored in the carts address
     * book.
     */
    getAddressBookAddresses: function () {
        var addressBook = {};
        var addresses = new ArrayList();

        if (!empty(this.object.describe().getCustomAttributeDefinition('sessionAddressBook'))) {

            // Checks session addresses availability.
            if (this.object.custom.sessionAddressBook) {
                try {
                    addressBook = JSON.parse(this.object.custom.sessionAddressBook);
                    addresses.add(addressBook.addresses);
                } catch (error) {
                    MultiShippingLogger.error(Resource.msgf('multishipping.error.parsejson', 'checkout', null, error));
                    return null;
                }
            }

            return addresses;
        }

        return null;
    },

    /**
     * Adds the given address to the address book of the cart.
     *
     * @alias module:models/CartModel~CartModel/addAddressToAddressBook
     * @transactional
     * @param {module:models/TransientAddressModel~TransientAddressModel} addressToAdd - The address to add.
     */
    addAddressToAddressBook: function (addressToAdd) {
        var address = new TransientAddress();
        var addressBook = {};

        if (addressToAdd) {

            // Tries to parse incoming JSON string.
            if (this.object.custom.sessionAddressBook) {
                try {
                    addressBook = JSON.parse(this.object.custom.sessionAddressBook);
                } catch (error) {
                    MultiShippingLogger.error(Resource.msgf('multishipping.error.parsejson', 'checkout', null, error));
                    return;
                }
            }

            // Checks if JSON object already has addresses.
            if (!(addressBook.addresses instanceof Array)) {
                addressBook.addresses = [];
            }

            // Copies referenceAddress to address object to be stringified.
            address.copyFrom(addressToAdd);
            address.UUID = addressToAdd.UUID;
            // Adds address if not already existing.
            if (!address.addressExists(addressBook.addresses)) {
                addressBook.addresses.push(address);
            }
        }

        this.object.custom.sessionAddressBook = JSON.stringify(addressBook);
    },

    /**
     * Updates the given address in the cart's address book.
     *
     * @transactional
     * @alias module:models/CartModel~CartModel/updateAddressBookAddress
     * @param {module:models/TransientAddressModel~TransientAddressModel} addressToUpdate - The address to update.
     */
    updateAddressBookAddress: function (addressToUpdate) {
        var addresses = [];
        var addressBook = {};

        if (addressToUpdate && this.object.custom.sessionAddressBook) {
            try {
                addressBook = JSON.parse(this.object.custom.sessionAddressBook);
            } catch (error) {
                MultiShippingLogger.error(Resource.msgf('multishipping.error.parsejson', 'checkout', null, error));
                return;
            }

            addresses = addressBook.addresses;

            for (var i = 0; i < addresses.length; i++) {
                if (addresses[i].UUID === addressToUpdate.UUID) {
                    addressToUpdate.ID = addresses[i].ID;
                    addressToUpdate.referenceAddressUUID = addresses[i].referenceAddressUUID;
                    addressBook.addresses[i] = addressToUpdate;
                }
            }

            this.object.custom.sessionAddressBook = JSON.stringify(addressBook);
        }
    },

    /**
     * Creates an Order based on the cart. If the order is created successfully, it has status CREATED.
     * Once the order is created, the cart is removed from the session and marked for removal.
     *
     * If the order is not created successfully the function returns null, if any of the following conditions are encountered:
     * <ul>
     * <li>any of the totals (net, gross, tax) of the basket is N/A</li>
     * <li>any of the product items is not available</li>
     * <li>any campaign-based coupon in the basket is invalid (see dw.order.CouponLineItem.isValid())</li>
     * <li>the basket represents an order being edited, but the order has been already been replaced by another order</li>
     * <li>the basket represents an order being edited, but the customer associated with the original order is not the same as the current customer</li>
     * </ul>
     * All empty shipments of the basket are removed before creating the order. A shipment is empty if it contains
     * no product or gift certificate line items or all total prices (net, gross, tax) are 0.0.
     *
     * The function decrements inventory for all products contained in the order. This means the 'reserve inventory for
     * order' functionality must not be subsequently called. The function redeems all coupons contained in the order.
     *
     * If the cart contains product or gift certificate line items associated with product list items, the function
     * updates the purchase of the product list items. For example, if the basket contains an item added from a gift
     * registry, the purchase history of the respective gift registry item is updated.
     *
     * @transactional
     * @alias module:models/CartModel~CartModel/createOrder
     * @returns {dw.order.Order} The created order in status CREATED or null if an error occured.
     */
    createOrder: function () {
        var basket = this.object;
        var order;
        try {
            order = Transaction.wrap(function () {
                return OrderMgr.createOrder(basket);
            });
        } catch (error) {
            return;
        }

        return order;
    },

    /**
     * Determines if the cart already contains payment instruments of the given payment method and removes them
     * from the basket.
     *
     * @transactional
     * @alias module:models/CartModel~CartModel/removeExistingPaymentInstruments
     * @param {String} method - Name of the payment method.
     */
    removeExistingPaymentInstruments: function (method) {
        var iter = this.getPaymentInstruments(method).iterator();

        // Remove payment instruments.
        while (iter.hasNext()) {
            this.removePaymentInstrument(iter.next());
        }
    },

    /**
     * Gets a gift certificate line item.
     *
     * @alias module:models/CartModel~CartModel/removeExistingPaymentInstruments
     * @param {String} uuid - UUID of the gift certificate line item to retrieve.
     * @return {dw.order.GiftCertificate | null} giftCertificate object with the passed UUID or null if no gift certificate with the passed UUID exists in the cart.
     */
    getGiftCertificateLineItemByUUID: function (uuid) {
        for (var it = this.object.getGiftCertificateLineItems().iterator(); it.hasNext();) {
            var item = it.next();
            if (item.getUUID() === uuid) {
                return item;
            }
        }
        return null;
    }

});

/**
 * Gets a new instance for the current or a given basket.
 *
 * @alias module:models/CartModel~CartModel/get
 * @param parameter {dw.order.Basket=} The basket object to enhance/wrap. If NULL the basket is retrieved from
 * the current session, if existing.
 * @returns {module:models/CartModel~CartModel}
 */
CartModel.get = function (parameter) {
    var basket = null;

    if (!parameter) {

        var currentBasket = BasketMgr.getCurrentBasket();

        if (currentBasket !== null) {
            basket = currentBasket;
        }

    } else if (typeof parameter === 'object') {
        basket = parameter;
    }
    return (basket !== null) ? new CartModel(basket) : null;
};

/**
 * Gets or creates a new instance of a basket.
 *
 * @alias module:models/CartModel~CartModel/goc
 * @returns {module:models/CartModel~CartModel}
 */
CartModel.goc = function () {
    var obj = null;

    var basket = BasketMgr.getCurrentOrNewBasket();

    if (basket && basket !== null) {
        obj = basket;
    }

    return new CartModel(obj);
};

/** The cart class */
module.exports = CartModel;