# Source: connectors/merlins.js

    /**
     * @module @mobify/commerce-integrations/dist/connectors/merlins
     */
    import {ScrapingConnector} from './scraping-connector'
    import * as errors from '../errors'
    import * as cj from 'cookiejar'

    const ROOT_CATEGORY = 'root'

    /**
     * Takes commerce-integrations Integer type and returns the URL
     * representation used to view a product details page on the website.
     *
     * @param {module:@mobify/commerce-integrations/dist/types.Integer} id
     * @returns {String}
     * @private
     */
    const getProductUrl = (id) => {
        return `/${id.replace(/\s/g, '-').toLowerCase()}.html`
    }

    /**
     * Utility function, takes a string and caplitalizes each word.
     */
    const titleCase = (str) =>
        str
            .split(' ')
            .map((word) => `${word.charAt(0).toUpperCase()}${word.substring(1)}`)
            .join(' ')

    /**
     * Takes a Magento product attribute value object and parses out a
     * commerce-integrations VariationPropertyValue type
     *
     * @param {Object} data
     * @returns {module:@mobify/commerce-integrations/dist/types.VariationPropertyValue}
     * @private
     */
    const parseVariationPropertyValue = ({label, id}) => ({
        name: label,
        value: id
    })

    /**
     * Takes a Magento product variation attribute object and parses out a
     * commerce-integrations VariationProperty type
     *
     * @param {Object} data
     * @returns {module:@mobify/commerce-integrations/dist/types.VariationProperty}
     * @private
     */
    const parseVariationProperty = ({code, label, options = []}) => ({
        id: code,
        label,
        values: options.map(parseVariationPropertyValue)
    })

    /**
     * Takes a Magento product object and parses out an
     * commerce-integrations Variation type
     *
     * @param {Object} data
     * @returns {module:@mobify/commerce-integrations/dist/types.Variation}
     * @private
     */
    const parseVariation = (id, upc, value, config) => {
        const convertedValues = Object.keys(value).reduce((acc, curr) => {
            acc[config.attributes[curr].code] = value[curr]

            return acc
        }, {})

        // Id's in magento look something like this "Eye Of Newt-20 ml-Red-1" which can be broken
        // down to this template "{sku}-{varationPropertyLabel1}-{varationPropertyLabelN}-{NumericalMasterProductId}". The below
        // code makes sure we get these variation property labels to use in the construction of
        // the id value.
        const variationValueLabels = Object.keys(value)
            .map(
                (key) => config.attributes[key].options.find((option) => option.id === value[key]).label
            )
            .sort()

        return {
            id: `${[upc, ...variationValueLabels].join('-')}-${config.productId}`,
            price: parseFloat(config.optionPrices[id].basePrice.amount),
            orderable: true, // Magento doesn't show out of stock variations... I think...
            values: convertedValues
        }
    }

    /**
     * Takes a takes a Magento image object and returns a set of commerce-integrations
     * Image type.
     *
     * @param {Object} data
     * @returns {Array.<module:@mobify/commerce-integrations/dist/types.Image>}
     * @private
     */
    const parseImageSet = ({caption, full, thumb}) => {
        const sets = []

        // Parse `full` image
        sets.push({
            images: [{title: caption, src: full, alt: caption}],
            variationProperties: [],
            sizeType: 'full'
        })

        // Parse `thumb` image
        sets.push({
            images: [{title: caption, src: thumb, alt: caption}],
            variationProperties: [],
            sizeType: 'thumb'
        })

        return sets
    }

    /**
     * Takes a document object and parses out a commerce-integrations Product type
     *
     * @param {Object} htmlDoc a {@link https://developer.mozilla.org/en-US/docs/Web/API/Document|Document} object
     * @returns {Promise<module:@mobify/commerce-integrations/dist/types.Product>}
     * @private
     */
    const parseProduct = (htmlDoc) => {
        const name = htmlDoc.querySelector('.page-title').textContent
        const description = htmlDoc.querySelector('.product.description').textContent
        const categoryEl = htmlDoc.querySelector('.breadcrumbs .item:nth-last-child(2) a')
        const priceEl = htmlDoc.querySelector('meta[property="product:price:amount"]')
        const skuEl = htmlDoc.querySelector('.sku .value')

        // Parse the categoryId
        let categoryId
        if (categoryEl) {
            const categoryIdMatch = categoryEl.pathname.match(/\/([^.]+)\.html/)
            categoryId = categoryIdMatch && categoryIdMatch[1]
        }

        // Parse the currency, price, prices, upc
        let price
        let upc

        if (priceEl) {
            price = parseFloat(priceEl.getAttribute('content'))
        }
        if (skuEl) {
            upc = skuEl.textContent
        }

        // We'll get alot of the product information from an inline script which contains
        // product details in the form of JSON.
        let productConfig
        const productScriptEls = htmlDoc.querySelectorAll('.product-add-form script')
        if (productScriptEls && productScriptEls.length > 1) {
            // NOTE: If there are variations there will be more than 1 script
            const productScriptJSON = JSON.parse(productScriptEls[0].textContent)
            productConfig = productScriptJSON['#product_addtocart_form'].configurable.spConfig
        }

        let galleryConfig
        const galleryScriptEl = htmlDoc.querySelector('.product.media script')
        if (galleryScriptEl) {
            const galleryScriptJSON = JSON.parse(galleryScriptEl.textContent)
            galleryConfig =
                galleryScriptJSON['[data-gallery-role=gallery-placeholder]']['mage/gallery/gallery']
                    .data
        }

        return {
            id: upc,
            name,
            imageSets:
                galleryConfig &&
                galleryConfig.reduce((acc, curr) => [...acc, ...parseImageSet(curr)], []),
            description,
            categoryId, // NOTE: Without the category in the path, it won't show up in the bread crumbs.
            brand: `Merlin's Potions`,
            minimumOrderQuantity: 1,
            stepQuantity: 1,
            upc,
            unit: 'ea',
            price,
            prices:
                productConfig &&
                Object.keys(productConfig.prices).reduce(
                    (acc, curr) => ({...acc, ...{[curr]: productConfig.prices[curr].amount}}),
                    {}
                ),
            variations:
                productConfig &&
                Object.keys(productConfig.index).map((key) =>
                    parseVariation(key, upc, productConfig.index[key], productConfig)
                ),
            variationProperties:
                productConfig &&
                Object.keys(productConfig.attributes).map((attributeId) =>
                    parseVariationProperty(productConfig.attributes[attributeId])
                ),
            variationValues: productConfig && productConfig.defaultValues
        }
    }

    // Transform Methods
    const transformOrderAddress = (shippingAddress) => ({
        firstname: shippingAddress.firstName,
        lastname: shippingAddress.lastName,
        telephone: shippingAddress.phone,
        street: [shippingAddress.addressLine1, shippingAddress.addressLine2],
        countryId: shippingAddress.countryCode,
        regionId: shippingAddress.stateCode,
        city: shippingAddress.city,
        postcode: shippingAddress.postalCode
    })

    const transformShippingMethod = (shippingMethod) => ({
        amount: shippingMethod.cost,
        carrier_code: shippingMethod.id
    })

    const transformCartItem = (cartItem) => ({
        cartItem: {
            sku: cartItem.productId,
            qty: cartItem.quantity
        }
    })

    /**
     * A connector for www.merlinspotions.com, using screen-scraping.
     *
     * @implements {module:@mobify/commerce-integrations/dist/connectors/interfaces.CommerceConnector}
     */
    export class MerlinsConnector extends ScrapingConnector {
        constructor({window, basePath, dondeGeoBasePath, dondeApiBasePath, cookieDomainRewrites}) {
            super({window, cookieDomainRewrites})
            this.basePath = basePath
            this.dondeGeoBasePath = dondeGeoBasePath
            this.dondeApiBasePath = dondeApiBasePath
        }

        /**
         * @private
         */
        loginUrl() {
            return `${this.basePath}/customer/account/login/`
        }

        /**
         * @inheritDoc
         */
        login(username, password) {
            if (!(username && password)) {
                // This is a magic customer id and shouldn't passed to the server.
                return Promise.resolve({
                    id: 'anonymous'
                })
            }

            // Magento has separate logins for the API and the HTML site. Login to both.
            const apiLogin = this.agent
                .post(`${this.basePath}/rest/default/V1/integration/customer/token`)
                .send({username, password})
                .then((res) => this.agent.set('Authorization', `Bearer ${res.body}`))
                .catch(() => {
                    throw new errors.ServerError('Could not get customer token')
                })

            const htmlLogin = this.agent
                .get(this.loginUrl())
                .then((res) => this.buildDocument(res))
                .then((doc) => {
                    const titleEl = doc.querySelector('h1.page-title')
                    const title = (titleEl && titleEl.textContent) || undefined
                    const formKeyEl = doc.querySelector('#login-form input[name="form_key"]')
                    const formKey = (formKeyEl && formKeyEl.value) || undefined

                    if (title === 'My Dashboard') {
                        // If redirected to the dashboard, we're already logged in.
                        return Promise.resolve()
                    } else {
                        return this.agent
                            .post(`${this.basePath}/customer/account/loginPost/`)
                            .type('form')
                            .send({
                                form_key: formKey,
                                'login[username]': username,
                                'login[password]': password,
                                persistent_remember_me: 'on',
                                send: ''
                            })
                    }
                })

            return Promise.all([apiLogin, htmlLogin]).then(() => this.getCustomer('me'))
        }

        /**
         * Register a new customer account and login.
         *
         * @param {module:@mobify/commerce-integrations/dist/types.CustomerRegistration} data
         * @return {Promise<module:@mobify/commerce-integrations/dist/types.Customer>}
         */
        registerCustomer(data) {
            const defaults = {newLetterSignup: false, rememberMe: false}
            const fields = ['firstName', 'lastName', 'email', 'password']
            data = Object.assign({}, defaults, data)
            fields.forEach((field) => {
                if (!data.hasOwnProperty(field)) {
                    throw new errors.InvalidArgumentError(`Missing required field ${field}`)
                }
            })
            return this.agent
                .post(`${this.basePath}/customer/account/createpost/`)
                .type('form')
                .send({
                    success_url: '',
                    error_url: '',
                    firstname: data.firstName,
                    lastname: data.lastName,
                    email: data.email,
                    password: data.password,
                    password_confirmation: data.password,
                    ...(data.rememberMe ? {persistent_remember_me: 'on'} : {}),
                    ...(data.newLetterSignup ? {is_subscribed: 1} : {})
                })
                .then((res) => this.buildDocument(res))
                .then((doc) => {
                    // If we see the signup form again, there was an error.
                    const form = doc.querySelector('form.form-create-account')
                    return form
                        ? Promise.reject(new errors.ServerError('Could not create account'))
                        : data
                })
                .then(() => {
                    // After creating an account the server leaves the user in
                    // a logged-in state. This doesn't cover API login though, so
                    // do a part-redundant call to login here.
                    return this.login(data.email, data.password)
                })
        }

        /**
         * @inheritDoc
         */
        logout() {
            return this.agent.post(`${this.basePath}/customer/account/logout/`).then(() => {
                if (!this.inBrowser()) {
                    // Really kill all cookies, if not in the browser
                    this.agent.jar = cj.CookieJar()
                }
                const {agent} = this
                // Clear out Authorization headers if there are any.
                agent._defaults = agent._defaults.filter(
                    (def) => !(def.fn === 'set' && def.arguments[0] === 'Authorization')
                )
            })
        }

        /**
         * @inheritDoc
         */
        getCustomer(id) {
            if (id !== 'me') {
                return Promise.reject(
                    new Error(
                        'This backend only supports getting the current customer ' +
                            '(pass "me" as the ID)'
                    )
                )
            }
            return this.agent
                .get(`${this.basePath}/customer/account/edit/`)
                .then((res) => this.buildDocument(res))
                .then((doc) => {
                    // Selector is over-specific to ensure we are on the correct page
                    // (could have followed a redirect if logged-out).
                    const form = doc.querySelector('#form-validate.form-edit-account')

                    if (!form) {
                        return Promise.reject(new Error('logged out'))
                    } else {
                        return Promise.resolve({
                            id: 'me',
                            firstName: form.querySelector('input[name="firstname"]').value,
                            lastName: form.querySelector('input[name="lastname"]').value,
                            email: form.querySelector('input[name="email"]').value
                        })
                    }
                })
        }

        /**
         * @inheritDoc
         */
        refreshSession() {
            // This is for interface compatibility - it's not necessary
            // on a screen-scraping connector, generally.
            return this.getCustomer('me')
        }

        /**
         * Given a configuration in the form of a plain object, this method returns
         * a new MerlinsConnector instance.
         *
         * @param {Object} config
         *
         * @returns {module:@mobify/commerce-integrations/dist/types.MerlinsConnector} The new MerlinsConnector instance.
         */
        static fromConfig(config) {
            return new this.prototype.constructor(config)
        }

        /**
         * @inheritDoc
         */
        getProduct(id) {
            const url = `${this.basePath}${getProductUrl(id)}`
            return this.agent
                .get(url)
                .then((res) => this.buildDocument(res))
                .then((htmlDoc) => {
                    if (htmlDoc.title === '404 Not Found') {
                        throw new errors.NotFoundError('Product Not Found')
                    }
                    return htmlDoc
                })
                .then((htmlDoc) => this.parseProduct(htmlDoc))
        }

        /**
         * @inheritDoc
         */
        getProducts(ids = []) {
            const getProductPromises = ids.map((id) => this.getProduct(id).catch(() => null))
            return Promise.all(getProductPromises).then((products) => {
                const validProducts = products.filter((product) => !!product)
                return {
                    count: validProducts.length,
                    data: validProducts,
                    total: validProducts.length
                }
            })
        }

        /**
         * Takes a OCAPI Product object and parses it into a commerce-integrations Product type.
         * @param {Object} data a OCAPI {@link https://documentation.demandware.com/DOC1/topic/com.demandware.dochelp/OCAPI/18.3/shop/Documents/Product.html|Product} document
         *
         * @returns {Promise<module:@mobify/commerce-integrations/dist/types.Store>}
         */
        parseProduct(data) {
            return parseProduct(data)
        }

        /**
         * @inheritDoc
         */
        searchProducts(productSearchRequest) {
            const url = `${this.basePath}${this.getSearchUrl(productSearchRequest)}`
            if (!productSearchRequest.count || ![4, 8, 12].includes(productSearchRequest.count)) {
                console.warn(
                    `The Merlins backend only supports 'count' values of 4, 8, 12. Defaulting to 4.`
                )
                productSearchRequest.count = 4
            }

            return this.agent
                .get(url)
                .then((res) => this.buildDocument(res))
                .then((htmlDoc) => this.parseSearchProducts(htmlDoc, productSearchRequest))
        }

        parseGetStore(htmlDoc) {
            // Parse the page into the models unique parts and assign them to the result.
            const mainContent = htmlDoc.getElementById('maincontent')
            const name = mainContent.querySelector('.location-title').textContent
            const addressLine1 = mainContent
                .querySelector('[itemprop="streetAddress"]')
                .textContent.trim()
            const city = mainContent.querySelector('[itemprop="addressLocality"]').textContent

            const country = mainContent.querySelector('.location-crumb.first-crumb').textContent
            const phone = mainContent
                .querySelector('[itemprop="telephone"]')
                .textContent.replace(new RegExp('[(]|[)]|[.]|[ ]|[-]|[#]|[x]', 'g'), '')
            const postalCode = mainContent.querySelector('[itemprop="postalCode"]').textContent

            const hoursContent = this.window.document.createElement('div')
            hoursContent.innerHTML = mainContent.querySelector('#hoursBlock').textContent

            const hours = {}
            const hoursNodeList = hoursContent.querySelectorAll('.fh-item')
            hoursNodeList.forEach((hour) => {
                const content = hour.querySelector('[itemprop="name"]')
                if (content) {
                    const day = content.textContent.slice(0, -1)
                    const openingTime = hour.querySelector('[itemprop="opens"]').textContent
                    const closingTime = hour.querySelector('[itemprop="closes"]').textContent
                    hours[day] = {
                        openingTime,
                        closingTime
                    }
                }
            })
            return {
                addressLine1,
                city,
                country,
                name,
                phone: phone ? phone : undefined,
                postalCode: postalCode ? postalCode : undefined,
                hours: hours ? hours : undefined
            }
        }

        /**
         * @private
         */
        hasAuthorization() {
            const headers = this.getDefaultHeaders() || {}
            const keys = Object.keys(headers)

            return keys.filter((key) => headers[key].Authorization).length > 0
        }

        /**
         * Parse an order address
         */
        parseOrderAddress(data) {
            return {
                firstName: data.firstname,
                lastName: data.lastname,
                phone: data.telephone,
                addressLine1: data.street[0],
                addressLine2: data.street[1],
                city: data.city,
                countryCode: data.country_id,
                stateCode: data.region_id.toString(),
                postalCode: data.postcode
            }
        }

        /**
         * Parse a cart item
         * cart item will not have price until a payment is set.
         */
        parseCartItem(data) {
            return {
                id: data.item_id.toString(),
                baseItemPrice: data.price,
                baseLinePrice: data.price * data.qty,
                productId: data.sku,
                productName: data.name,
                quantity: data.qty,
                itemPrice: data.price,
                linePrice: data.price * data.qty
            }
        }

        /**
         * Parse a shipping method
         */
        parseShippingMethod(data) {
            return {
                id: data.carrier_code,
                cost: data.amount,
                label: `${data.method_title} - ${data.carrier_title}`
            }
        }

        /**
         * Parse a payment method
         */
        parsePaymentMethod(data) {
            return {
                id: data.code,
                name: data.title
            }
        }

        /**
         * Parse a coupon item
         */
        parseCouponEntry(data) {
            return {
                id: data.coupon_code,
                code: data.coupon_code
            }
        }

        /**
         * Parse a cart
         */
        /* eslint-disable camelcase */
        parseCart(data) {
            const {billing_address, customer} = data
            const hasBillingAddress = billing_address && billing_address.street[0] !== ''

            return {
                id: data.id,
                billingAddress: hasBillingAddress ? this.parseOrderAddress(billing_address) : undefined,
                customerInfo: {
                    id: customer.id ? customer.id.toString() : 'anonymous',
                    email: customer.email
                }
            }
        }
        /* eslint-enable camelcase */

        /**
         * @inheritDoc
         */
        // eslint-disable-next-line no-unused-vars
        createCart(oldCart, opts = {}) {
            return this.agent
                .post(
                    `${this.basePath}/rest/default/V1/${
                        this.hasAuthorization() ? 'carts/mine' : 'guest-carts'
                    }`
                )
                .then((res) => res.body)
                .then((cartId) => this.getCart(cartId))
                .then((cart) => {
                    if (oldCart && !cart.customerInfo.email && oldCart.customerInfo.email) {
                        cart.customerInfo.email = oldCart.customerInfo.email
                    }
                    // Have not found a way to retrieve shippingAddress from the backend. so lets persist it from the incoming cart parameter to the returned cart.
                    if (oldCart && oldCart.shippingAddress) {
                        cart.shippingAddress = oldCart.shippingAddress
                    }

                    // Have not found a way to retrieve selectedShippingMethodId from the backend. so lets persist it from the incoming cart parameter to the returned cart.
                    if (oldCart && oldCart.selectedShippingMethodId) {
                        cart.selectedShippingMethodId = oldCart.selectedShippingMethodId
                    }
                    return cart
                })
        }

        /**
         * @inheritDoc
         */
        getCart(cartId) {
            if (!cartId) {
                throw new errors.InvalidArgumentError(`Parameter 'cartId' is required`)
            }

            const basePath = `${this.basePath}/rest/default/V1/${
                this.hasAuthorization() ? `carts/mine` : `guest-carts/${cartId}`
            }`

            const cartPromise = this.agent
                .get(`${basePath}`)
                .then(({body}) => this.parseCart({...body, id: cartId}))

            const itemsPromise = this.agent
                .get(`${basePath}/items`)
                .then(({body}) => body && body.map(this.parseCartItem))
                .catch(() => [])

            const paymentsPromise = this.agent
                .get(`${basePath}/selected-payment-method`)
                .then(({body}) => {
                    return body.method ? [{id: 'mine', methodId: body.method}] : []
                })
                .catch(() => [])

            const totalsPromise = this.agent
                .get(`${basePath}/totals`)
                .then(({body}) => this.parseCartTotals(body))
                .catch(() => {
                    throw new errors.ServerError(`Could not get cart totals`)
                })

            return Promise.all([cartPromise, itemsPromise, paymentsPromise, totalsPromise])
                .then(([cart, items, payments, totals]) => {
                    // Apply payments, items, totals and payment amount to the cart.
                    cart.payments = payments
                    cart.items = items

                    if (cart.payments && cart.payments.length > 0) {
                        cart.payments[0].amount = totals.total
                    }

                    return {
                        ...cart,
                        ...totals
                    }
                })
                .catch(() => {
                    throw new errors.ServerError(`Could not retrieve cart`)
                })
        }

        /**
         * Parse the cart money totals
         * totals not calculated until shipping address/method is set.
         */
        parseCartTotals(data) {
            return {
                couponEntries: data.coupon_code ? [this.parseCouponEntry(data)] : [],
                subtotal: data.subtotal,
                shipping: data.shipping_amount,
                discounts: data.discount_amount,
                tax: data.tax_amount,
                total: data.base_grand_total
            }
        }

        /**
         * @inheritDoc
         */
        deleteCart() {
            throw new errors.UnsupportedError('This function is not supported by the Magento API')
        }

        /**
         * @inheritDoc
         */
        addCartItem(cart, cartItem) {
            if (!cart) {
                throw new errors.InvalidArgumentError(`Parameter 'cart' is required`)
            }
            if (!cartItem) {
                throw new errors.InvalidArgumentError(`Parameter 'cartItem' is required`)
            }

            // eslint-disable-next-line no-undef
            cartItem = transformCartItem(cartItem)
            cartItem.cartItem.quote_id = cart.id

            return (
                this.agent
                    .post(
                        `${this.basePath}/rest/default/V1/${
                            this.hasAuthorization() ? 'carts/mine' : `guest-carts/${cart.id}`
                        }/items`
                    )
                    .send(cartItem)
                    // Have not found a way to retrieve shippingAddress and selectedShippingMethodId from the backend and email is not saved in the backend for a guest user.
                    // So lets persist these fields on the incoming cart parameter to the returned cart.
                    .then(() => this.getCart(cart.id))
                    .then((updatedCart) => ({
                        ...updatedCart,
                        shippingAddress: cart.shippingAddress,
                        customerInfo: cart.customerInfo,
                        selectedShippingMethodId: cart.selectedShippingMethodId
                    }))
                    .catch(() => {
                        throw new errors.ServerError('Could Not Add Cart Item')
                    })
            )
        }

        /**
         * @inheritDoc
         */
        updateCartItem(cart, cartItem) {
            if (!cart) {
                throw new errors.InvalidArgumentError(`Parameter 'cart' is required`)
            }
            if (!cartItem) {
                throw new errors.InvalidArgumentError(`Parameter 'cartItem' is required`)
            }

            const cartItemId = cartItem.id
            // eslint-disable-next-line no-undef
            cartItem = transformCartItem(cartItem)
            cartItem.cartItem.quote_id = cart.id

            return (
                this.agent
                    .put(
                        `${this.basePath}/rest/default/V1/${
                            this.hasAuthorization() ? 'carts/mine' : `guest-carts/${cart.id}`
                        }/items/${cartItemId}`
                    )
                    .send(cartItem)
                    // Have not found a way to retrieve shippingAddress from the backend and email is not saved in the backend for a guest user.
                    // So lets persist these fields on the incoming cart parameter to the returned cart.
                    .then(() => this.getCart(cart.id))
                    .then((updatedCart) => ({
                        ...updatedCart,
                        shippingAddress: cart.shippingAddress,
                        customerInfo: cart.customerInfo
                    }))
                    .catch(() => {
                        throw new errors.ServerError('Could Not Update Cart Item')
                    })
            )
        }

        /**
         * @inheritDoc
         */
        removeCartItem(cart, cartItemId) {
            if (!cart) {
                throw new errors.InvalidArgumentError(`Parameter 'cart' is required`)
            }
            if (!cartItemId) {
                throw new errors.InvalidArgumentError(`Parameter 'cartItemId' is required`)
            }

            return (
                this.agent
                    .delete(
                        `${this.basePath}/rest/default/V1/${
                            this.hasAuthorization() ? 'carts/mine' : `guest-carts/${cart.id}`
                        }/items/${cartItemId}`
                    )
                    // Have not found a way to retrieve shippingAddress and selectedShippingMethodId from the backend and email is not saved in the backend for a guest user.
                    // So lets persist these fields on the incoming cart parameter to the returned cart.
                    .then(() => this.getCart(cart.id))
                    .then((updatedCart) => ({
                        ...updatedCart,
                        shippingAddress: cart.shippingAddress,
                        customerInfo: cart.customerInfo,
                        selectedShippingMethodId: cart.selectedShippingMethodId
                    }))
                    .catch(() => {
                        throw new errors.ServerError('Could Not Add Cart Item')
                    })
            )
        }

        /**
         * This updates the given cart object with the given customerInformation object and returns it.
         * It will not persist in the backend until a payment is added.
         */
        setCustomerInformation(cart, customerInformation) {
            if (!cart) {
                throw new errors.InvalidArgumentError(`Parameter 'cart' is required`)
            }
            if (!customerInformation) {
                throw new errors.InvalidArgumentError(`Parameter 'customerInformation' is required`)
            }

            const cartCopy = JSON.parse(JSON.stringify(cart))
            cartCopy.customerInfo.email = customerInformation.email

            return Promise.resolve(cartCopy)
        }

        /**
         * @inheritDoc
         */
        setBillingAddress(cart, billingAddress) {
            if (!cart) {
                throw new errors.InvalidArgumentError(`Parameter 'cart' is required`)
            }
            if (!billingAddress) {
                throw new errors.InvalidArgumentError(`Parameter 'billingAddress' is required`)
            }

            const originalBillingAddress = {
                ...billingAddress,
                titleCode: undefined
            }
            // Transform Mobify schema into OCAPI schema
            // eslint-disable-next-line no-undef
            billingAddress = transformOrderAddress(billingAddress)

            return this.agent
                .post(
                    `${this.basePath}/rest/default/V1/${
                        this.hasAuthorization() ? 'carts/mine' : `guest-carts/${cart.id}`
                    }/billing-address`
                )
                .send({address: billingAddress})
                .then(() => {
                    return {
                        ...cart,
                        billingAddress: originalBillingAddress
                    }
                })
                .catch(() => {
                    throw new errors.ServerError('Could Not Set Billing Address')
                })
        }

        /**
         * @inheritDoc
         */
        getShippingMethods(cart) {
            if (!cart) {
                throw new errors.InvalidArgumentError(`Parameter 'cart' is required`)
            }
            if (!cart.shippingAddress) {
                throw new errors.InvalidArgumentError(`Parameter 'cart.shippingAddress' is required`)
            }

            const hasItems = cart.items && cart.items.length
            if (!hasItems) {
                console.warn(`You must first add items to your cart to get valid shipping methods`)
            }

            const shippingAddress = transformOrderAddress(cart.shippingAddress)

            return this.agent
                .post(
                    `${this.basePath}/rest/default/V1/${
                        this.hasAuthorization() ? 'carts/mine' : `guest-carts/${cart.id}`
                    }/estimate-shipping-methods`
                )
                .send({
                    address: shippingAddress
                })
                .then(({body = []}) => body.map(this.parseShippingMethod))
                .catch(() => {
                    throw new errors.ServerError('Could Not Get Shipping Methods')
                })
        }

        /**
         * @private
         */
        setShippingInformation(cart, shippingAddress, shippingMethod) {
            // Set the shipping method id on the cart. We do this because we can't get
            // shipping information back from the server. If we figure out how to do this
            // we can remove this line.
            cart.selectedShippingMethodId = shippingMethod.id

            // Transform the shipping address and method into objects that the server
            // will understand.
            shippingAddress = transformOrderAddress(shippingAddress)
            shippingMethod = transformShippingMethod(shippingMethod)

            // Append the customer email to the shipping address if we have one.
            if (cart.customerInfo && cart.customerInfo.email) {
                shippingAddress.email = cart.customerInfo.email
            }

            return this.agent
                .post(
                    `${this.basePath}/rest/default/V1/${
                        this.hasAuthorization() ? 'carts/mine' : `guest-carts/${cart.id}`
                    }/shipping-information`
                )
                .send({
                    addressInformation: {
                        shippingMethodCode: shippingMethod.carrier_code,
                        shippingCarrierCode: shippingMethod.carrier_code,
                        shipping_address: shippingAddress
                    }
                })
                .then(({body}) => {
                    const totals = this.parseCartTotals(body.totals)

                    if (cart.payments && cart.payments.length > 0) {
                        cart.payments[0].amount = totals.total
                    }

                    return {
                        ...cart,
                        ...totals
                    }
                })
                .catch(() => {
                    throw new errors.ServerError('Could Not Set Shipping Information')
                })
        }

        /**
         * @inheritDoc
         */
        setShippingMethod(cart, shippingMethod) {
            if (!cart) {
                throw new errors.InvalidArgumentError(`Parameter 'cart' is required`)
            }
            if (!shippingMethod) {
                throw new errors.InvalidArgumentError(`Parameter 'shippingMethod' is required`)
            }
            if (!cart.shippingAddress) {
                throw new errors.InvalidArgumentError(
                    `Shipping Address must be set prior to setting shipping method`
                )
            }

            return this.setShippingInformation(cart, cart.shippingAddress, shippingMethod)
        }

        /**
         * @inheritDoc
         */
        setShippingAddress(cart, shippingAddress) {
            if (!cart) {
                throw new errors.InvalidArgumentError(`Parameter 'cart' is required`)
            }
            if (!shippingAddress) {
                throw new errors.InvalidArgumentError(`Parameter 'shippingAddress' is required`)
            }
            shippingAddress.titleCode = undefined

            const {shippingMethods, selectedShippingMethod} = cart

            // If we don't have the shipping methods set, fake it, and return the cart with the
            // shipping address merged in.
            if (!shippingMethods) {
                return Promise.resolve({
                    ...cart,
                    shippingAddress
                })
            }

            return this.setShippingInformation(
                cart,
                shippingAddress,
                selectedShippingMethod || shippingMethods[0]
            )
        }

        /**
         * @inheritDoc
         */
        getPaymentMethods(cart) {
            if (!cart) {
                throw new errors.InvalidArgumentError(`Parameter 'cart' is required`)
            }

            return this.agent
                .get(
                    `${this.basePath}/rest/default/V1/${
                        this.hasAuthorization() ? 'carts/mine' : `guest-carts/${cart.id}`
                    }/payment-methods`
                )
                .then(({body = []}) => body.map(this.parsePaymentMethod))
                .catch(() => {
                    throw new errors.ServerError('Could not get payment methods')
                })
        }

        /**
         * @inheritDoc
         */
        setPayment(cart, payment) {
            if (!cart) {
                throw new errors.InvalidArgumentError(`Parameter 'cart' is required`)
            }
            if (!payment) {
                throw new errors.InvalidArgumentError(`Parameter 'payment' is required`)
            }
            if (!payment.methodId) {
                throw new errors.InvalidArgumentError(
                    `Payment methodId address on parameter 'payment' is required`
                )
            }
            if (!cart.customerInfo.email) {
                throw new errors.InvalidArgumentError(`Email on cart is required`)
            }
            if (!cart.billingAddress) {
                throw new errors.InvalidArgumentError(`Billing address on cart is required`)
            }
            if (!cart.shippingAddress) {
                throw new errors.InvalidArgumentError(`Shipping address on cart is required`)
            }

            const billingAddress = transformOrderAddress(cart.billingAddress)
            const paymentInformation = {
                paymentMethod: {
                    method: payment.methodId
                },
                billingAddress,
                email: cart.customerInfo.email
            }

            const basePath = `${this.basePath}/rest/default/V1/${
                this.hasAuthorization() ? 'carts/mine' : `guest-carts/${cart.id}`
            }`

            return (
                this.agent
                    .post(`${basePath}/set-payment-information`)
                    .send(paymentInformation)
                    // Have not found a way to retrieve shippingAddress and selectedShippingMethodId from the backend and email is not saved in the backend for a guest user.
                    // So lets persist these fields on the incoming cart parameter to the returned cart.
                    .then(() => this.getCart(cart.id))
                    .then((updatedCart) => ({
                        ...updatedCart,
                        shippingAddress: cart.shippingAddress,
                        customerInfo: cart.customerInfo,
                        selectedShippingMethodId: cart.selectedShippingMethodId
                    }))
                    .catch(() => {
                        throw new errors.ServerError('Could not set payment')
                    })
            )
        }

        /**
         * @inheritDoc
         */
        getStore(id) {
            const storesBasePath = this.basePath.replace('www', 'locations')
            const url = `${storesBasePath}/${id}.html?utm_source=storelocator`
            return this.agent
                .get(url)
                .then((res) => this.buildDocument(res))
                .then((htmlDoc) => this.parseGetStore(htmlDoc))
                .then((data) => ({...data, id}))
        }

        /**
         * @inheritDoc
         */
        getCategory(id) {
            // Always request the basePath for getting all categories
            return this.agent
                .get(this.basePath)
                .then((res) => this.buildDocument(res))
                .then((htmlDoc) => {
                    let category

                    if (id === ROOT_CATEGORY) {
                        const categoryEls = [...htmlDoc.querySelectorAll('.navigation a')].map(
                            (el) => el.parentElement
                        )
                        const categories = categoryEls.map(this.parseCategory)

                        category = {
                            id: ROOT_CATEGORY,
                            name: 'Categories',
                            categories
                        }
                    } else {
                        const categoryAnchorEl = htmlDoc.querySelector(`.navigation a[href*="${id}"]`)
                        if (!categoryAnchorEl) {
                            throw new Error('Category Not Found')
                        }
                        category = this.parseCategory(categoryAnchorEl.parentElement)
                    }
                    return category
                })
        }

        /**
         * @inheritDoc
         */
        getCategories(ids = []) {
            const getCategoryPromises = ids.map((id) => this.getCategory(id).catch(() => null))

            return Promise.all(getCategoryPromises).then((categories) => {
                const validCategories = categories.filter((category) => !!category)

                return {
                    count: validCategories.length,
                    data: validCategories,
                    total: validCategories.length
                }
            })
        }

        /**
         * Takes a html element that represents a category and parses it into a commerce-integrations
         * Category type.
         *
         * @param {Object} htmlDoc a {@link https://developer.mozilla.org/en-US/docs/Web/API/Element|Element} object
         *
         * @returns {Promise<module:@mobify/commerce-integrations/dist/types.Category>}
         */
        parseCategory(categoryEl) {
            const anchorEl = categoryEl.querySelector('a')
            const idMatch = anchorEl.pathname.match(/[^\\](\w+).html$/)

            return {
                id: idMatch && idMatch[1],
                name: anchorEl.textContent
            }
        }

        /**
         * Takes commerce-integrations ProductSearchRequest type and returns the URL
         * representation used to query the website.
         *
         * @param {module:@mobify/commerce-integrations/dist/types.ProductSearchRequest} productSearchRequest
         *
         * @returns {String}
         */
        getSearchUrl({count, sort, start, total, query, filters = {}}) {
            const params = []
            const pathname = query ? '/catalogsearch/result/' : `/${filters.categoryId}.html`

            if (count) {
                params.push(`product_list_limit=${count}`)
            }
            if (total) {
                params.push(`product_list_total=${total}`)
            }
            if (sort) {
                params.push(`product_list_order=${sort}`)
            }
            if (start) {
                params.push(`p=${Math.ceil(total / start)}`)
            }
            if (query) {
                params.push(`q=${query}`)
            }

            // Compose the filters part of the url
            Object.keys(filters)
                .filter((key) => !['categoryId'].includes(key))
                .forEach((key) => {
                    params.push(`${key}=${filters[key]}`)
                })

            return `${pathname}?${params.sort().join('&')}`
        }

        /**
         * Takes a dom element representing a merlins search result product tile and parses out an
         * commerce-integrations ProductSearchResult type.
         *
         * @param {Object} productEl a {@link https://developer.mozilla.org/en-US/docs/Web/API/Element|Element} object
         *
         * @returns {module:@mobify/commerce-integrations/dist/types.ProductSearchResult}
         */
        parseProductSearchResult(productEl) {
            const linkEl = productEl.querySelector('.product-item-link')
            const imageEl = productEl.querySelector('.product-image-photo')
            const priceEl = productEl.querySelector('.price')
            const stockEl = productEl.querySelector('.stock')

            const productName = linkEl.textContent
            const productIdMatch = linkEl.href.match(/([^/]+).html$/)
            const productId = productIdMatch
                ? titleCase(productIdMatch[1].replace(/-/g, ' '))
                : undefined
            const available = !stockEl

            let price
            if (available) {
                const priceMatch = priceEl.textContent.match(/\$(.+)/)
                price = priceMatch ? parseFloat(priceEl.textContent.match(/\$(.+)/)[1]) : ''
            } else {
                price = -1 // Something obviously invalid
            }

            const image = {
                alt: imageEl.getAttribute('alt'),
                description: imageEl.getAttribute('alt'),
                src: imageEl.getAttribute('src'),
                title: imageEl.getAttribute('alt')
            }

            return {
                available,
                productId,
                productName,
                defaultImage: image,
                price,
                variationProperties: []
            }
        }

        /**
         * Takes a dom element representing a merlins sorting select and parses out an
         * array od commerce-integrations SortOption type.
         *
         * @param {Object} sortEl a {@link https://developer.mozilla.org/en-US/docs/Web/API/Element|Element} object
         *
         * @returns {Array.<module:@mobify/commerce-integrations/dist/types.SortOption>}
         */
        parseSortingOptions(sortEl) {
            const optionEls = sortEl.querySelectorAll('option')
            return [...optionEls].map((el) => ({
                id: el.value,
                label: el.innerHTML
            }))
        }

        /**
         * Takes a dom element representing a merlins filter and parses out an
         * commerce-integrations Filter type.
         *
         * @param {Object} optionEl a {@link https://developer.mozilla.org/en-US/docs/Web/API/Element|Element} object
         *
         * @returns {module:@mobify/commerce-integrations/dist/types.Filter}
         */
        parseFilter(optionEl) {
            const label = optionEl.querySelector('.filter-options-title').textContent
            const propertyId = label.toLowerCase()

            const valueEls = optionEl.querySelectorAll('.items .item')
            const values = [...valueEls].map((valueEl) => {
                const countEl = valueEl.querySelector('.count')
                const anchorEl = valueEl.querySelector('a')

                // Parse out the count and then remove the element so we can get the label text.
                const count = parseInt(countEl.textContent)
                countEl.remove()

                return {
                    count,
                    label: anchorEl.textContent.trim(),
                    value: anchorEl.search.match(/\w=([^&]+)/)[1]
                }
            })

            return {
                propertyId,
                label,
                values
            }
        }

        /**
         * Takes a dom element representing a merlins selected filter and parses out a
         * commerce-integrations Filter type.
         *
         * @param {Object} selectedEl a {@link https://developer.mozilla.org/en-US/docs/Web/API/Element|Element} object
         * @param {String} currentUrl The current page url used to help determine filter values.
         *
         * @returns {module:@mobify/commerce-integrations/dist/types.Filter}
         */
        parseSelectedFilter(selectedEl, currentUrl) {
            // Use an anchor to parse out the search string
            const anchorEl = this.window.document.createElement('a')
            anchorEl.href = currentUrl

            const label = selectedEl.querySelector('.filter-label').textContent
            const propertyId = label.toLowerCase()

            const valueLabel = selectedEl.querySelector('.filter-value').textContent
            const valueValue = anchorEl.search.match(new RegExp(`${propertyId}=([^&]+)`))[1]

            return {
                propertyId,
                label,
                values: [
                    {
                        label: valueLabel,
                        value: valueValue
                    }
                ]
            }
        }

        /**
         * Takes a dom element and parses out an array of commerce-integrations
         * Filter type representing those filters that are selected.
         *
         * @param {Object} selectedFiltersEl a {@link https://developer.mozilla.org/en-US/docs/Web/API/Element|Element} object
         * @param {String} currentUrl The current page url used to help determine filter values.
         *
         * @returns {Array.<module:@mobify/commerce-integrations/dist/types.Filter>}
         */
        parseSelectedFilters(selectedFiltersEl, currentUrl) {
            const selectedEls = selectedFiltersEl.querySelectorAll('.items .item')
            return [...selectedEls].map((el) => this.parseSelectedFilter(el, currentUrl))
        }

        /**
         * Takes a dom element and parses out an array of commerce-integrations
         * Filter type.
         *
         * @param {Object} filterOptionsEl a {@link https://developer.mozilla.org/en-US/docs/Web/API/Element|Element} object
         * @param {String} currentUrl The current page url used to help determine filter values.
         *
         * @returns {Array.<module:@mobify/commerce-integrations/dist/types.Filter>}
         */
        parseFilters(filterOptionsEl, currentUrl) {
            // Note: Merlin's Potion's is unique in the way that it handles filters, normally
            // you'd see checkboxes of all the filters with those that are selected being filled
            // in. That makes it easy to parse. But in this case they are split up in two sections,
            // one is the filters that aren't selected and a list of links, and the other is a
            // section of selected filters with it's value.
            // we'll have to parse these both to get as close to a complete list of all the filters.
            const optionsEls = filterOptionsEl.querySelectorAll('.filter-options-item')
            return [...optionsEls].map((el) => this.parseFilter(el, currentUrl))
        }

        /**
         * @inheritDoc
         */
        parseSearchProducts(htmlDoc, searchRequest) {
            const {filters, query} = searchRequest

            // Parse get the mainContent element and any product elements in it.
            const mainContent = htmlDoc.getElementById('maincontent')
            const resultEls = mainContent.querySelectorAll('.products.product-items > .product-item')

            const is404 = htmlDoc.title === '404 Not Found'
            const isEmpty = !resultEls.length

            if (is404 || isEmpty) {
                return {
                    count: 0,
                    filters: [],
                    results: [],
                    query,
                    selectedFilters: filters,
                    sortingOptions: [],
                    start: 0,
                    total: 0
                }
            }

            // This is the current url of the page, it'll be used to get filter values, etc.
            const currentUrl = JSON.parse(
                htmlDoc.querySelector('.toolbar-products').getAttribute('data-mage-init')
            ).productListToolbarForm.url

            // Parse other page information from document.
            const pageSize = parseInt(mainContent.querySelector('#limiter').value)
            const pageEl = mainContent.querySelector('.pages .current span:last-child')
            const page = pageEl ? parseInt(pageEl.textContent) - 1 : 0

            const filterOptionsEl = mainContent.querySelector('.filter-options')
            const selectedFiltersEl = mainContent.querySelector('.filter-current')

            const filterOptions = filterOptionsEl ? this.parseFilters(filterOptionsEl, currentUrl) : []
            const selectedFilters = selectedFiltersEl
                ? this.parseSelectedFilters(selectedFiltersEl, currentUrl)
                : []

            const sortEl = mainContent.querySelector('#sorter')
            const totalEl = mainContent.querySelector('#toolbar-amount .toolbar-number')

            return {
                count: resultEls.length,
                query: htmlDoc.querySelector('#search').value || query,
                filters: [...filterOptions, ...selectedFilters],
                results: [...resultEls].map(this.parseProductSearchResult),
                selectedFilters: selectedFilters.reduce((obj, curr) => {
                    obj[curr.propertyId] = curr.values[0].value
                    return obj
                }, {}),
                selectedSortingOption: sortEl ? sortEl.value : '',
                sortingOptions: sortEl ? this.parseSortingOptions(sortEl) : [],
                start: page * pageSize,
                total: totalEl ? parseInt(totalEl.textContent) : 0
            }
        }

        /**
         * @inheritDoc
         */
        searchStores(storeSearchRequest) {
            let count = storeSearchRequest.count > 0 ? storeSearchRequest.count : 20
            const start = storeSearchRequest.start > 0 ? storeSearchRequest.start : 0
            const latitude = storeSearchRequest.latlon.latitude
            const longitude = storeSearchRequest.latlon.longitude
            let total
            if (isNaN(latitude) || isNaN(longitude)) {
                throw new Error('Provide Latitude and Longitude coordinates')
            }

            const getStoresUrl = `${this.dondeApiBasePath}/near?center=${latitude},${longitude}&dondeKey=56c3681586312e000e000001&_=1527268304391`
            return this.agent
                .get(getStoresUrl)
                .then((res) => res.body)
                .then((jsonRes) => {
                    total = jsonRes.length
                    const stores = jsonRes.map((store) => {
                        const storeId = store.llp_url.replace('.html', '').substring(1)
                        return this.getStore(storeId).then((data) => {
                            return data
                        })
                    })
                    return Promise.all(stores).then((stores) => {
                        let storesToReturn
                        const totalPages = Math.ceil(total / count)
                        if (totalPages === start + 1) {
                            // on last page
                            const remainder = total % count
                            if (remainder > 0) {
                                count = remainder
                            }
                            storesToReturn = stores.slice(stores.length - remainder)
                        } else if (totalPages < start + 1) {
                            throw new Error('No stores found')
                        } else {
                            // middle pages
                            storesToReturn = stores.slice(start * count, (start + 1) * count)
                        }

                        return {
                            count,
                            start,
                            total,
                            stores: storesToReturn
                        }
                    })
                })
        }

        /**
         * @inheritDoc
         */
        parseOrder(htmlDoc, id) {
            const status = htmlDoc.querySelector('.order-status').textContent
            const creationDateText = htmlDoc.querySelector('.order-date date').textContent
            const creationDate = new Date(creationDateText)

            // Order Items and Totals
            const orderItemsEl = htmlDoc.getElementById('my-orders-table')

            // Shipping/Billing Addresses
            const shippingAddressEl = htmlDoc.querySelector('.box-order-shipping-address address')
            const billingAddressEl = htmlDoc.querySelector('.box-order-billing-address address')

            // Parse various sub units of order information.
            const parseElPrice = (el) => parseFloat(el.textContent.slice(1))
            const totals = {
                discounts: 0, // TODO: This will be completed when we add coupons to the API.
                shipping: parseElPrice(orderItemsEl.querySelector('.shipping .price')),
                subtotal: parseElPrice(orderItemsEl.querySelector('.subtotal .price')),
                tax: parseElPrice(orderItemsEl.querySelector('.totals-tax .price')),
                total: parseElPrice(orderItemsEl.querySelector('.grand_total .price'))
            }

            const parseItem = (itemEl) => ({
                id: itemEl.querySelector('.sku').textContent.trim(),
                productId: itemEl.querySelector('.sku').textContent.trim(),
                productName: itemEl.querySelector('.sku').textContent.trim(),
                baseItemPrice: parseElPrice(itemEl.querySelector('.price .price')),
                baseLinePrice: parseElPrice(itemEl.querySelector('.subtotal .price')),
                itemPrice: parseElPrice(itemEl.querySelector('.price .price')),
                linePrice: parseElPrice(itemEl.querySelector('.subtotal .price')),
                quantity: parseInt(itemEl.querySelector('.qty .content').textContent.trim())
            })

            const itemsEls = orderItemsEl.querySelectorAll('tbody tr')
            const items = itemsEls && [...itemsEls].map(parseItem)

            const shippingAddressParts = shippingAddressEl && shippingAddressEl.textContent.split('\n')
            const shippingAddress = shippingAddressParts && {
                firstName: shippingAddressParts[0].split(' ')[0],
                lastName: shippingAddressParts[0].split(' ')[1],
                phone: shippingAddressParts[8].replace('T: ', ''),
                addressLine1: shippingAddressParts[2],
                addressLine2: shippingAddressParts[3],
                city: shippingAddressParts[6].split(',')[0],
                countryCode: shippingAddressParts[7],
                stateCode: shippingAddressParts[6].split(',')[1],
                postalCode: shippingAddressParts[6].split(', ')[2]
            }

            // NOTE: Known issue, we can't get the proper code for country and state, only the
            // values.
            const billingAddressParts = billingAddressEl && billingAddressEl.textContent.split('\n')
            const billingAddress = billingAddressParts && {
                firstName: billingAddressParts[0].split(' ')[0],
                lastName: billingAddressParts[0].split(' ')[1],
                phone: billingAddressParts[8].replace('T: ', ''),
                addressLine1: billingAddressParts[2],
                addressLine2: billingAddressParts[3],
                city: billingAddressParts[6].split(',')[0],
                countryCode: billingAddressParts[7], // NOTE: Should be a code not a value.
                stateCode: billingAddressParts[6].split(',')[1], // NOTE: Should be a code not a value.
                postalCode: billingAddressParts[6].split(', ')[2]
            }

            // Create a fake payment method id to allow correct mapping of the payment name
            const payments = [{id: 'mine', amount: totals.total, methodId: 'checkmo'}]

            // Create a fake shipping method id to allow correct mapping of the shipping name
            const selectedShippingMethodId = 'mine'

            return {
                id,
                billingAddress,
                creationDate,
                items,
                status,
                payments,
                selectedShippingMethodId,
                shippingAddress,
                ...totals
            }
        }

        /**
         * @inheritDoc
         */
        // eslint-disable-next-line no-unused-vars
        createOrder(cart, opts = {}) {
            if (!cart) {
                throw new errors.InvalidArgumentError(`Parameter 'cart' is required`)
            }

            const paymentMethod = {
                method: cart.payments[0].methodId
            }

            return this.agent
                .put(
                    `${this.basePath}/rest/default/V1/${
                        this.hasAuthorization() ? 'carts/mine' : `guest-carts/${cart.id}`
                    }/order`
                )
                .send(paymentMethod)
                .then((res) => ({
                    ...cart,
                    id: res.body,
                    creationDate: new Date(),
                    status: 'Pending'
                }))
        }

        /**
         * @inheritDoc
         */
        // eslint-disable-next-line no-unused-vars
        getOrder(id, opts = {}) {
            const url = `${this.basePath}/sales/order/view/order_id/${id}/`

            return this.agent
                .get(url)
                .then((res) => this.buildDocument(res))
                .then((htmlDoc) => {
                    const isLoginPage = !!htmlDoc.querySelector('body.customer-account-login')
                    const isHistoryPage = !!htmlDoc.querySelector('body.sales-order-history')

                    // If we were redirected to the account order history page, it means that
                    // you need to log in to see this order.
                    if (isLoginPage) {
                        throw new errors.NotAuthenticatedError()
                    }

                    // If we were redirected to the account order history page, it means that
                    // the order we were looking for wasn't found.
                    if (isHistoryPage) {
                        throw new errors.NotFoundError('Order Not Found')
                    }

                    return htmlDoc
                })
                .then((htmlDoc) => this.parseOrder(htmlDoc, id))
        }

        /**
         * @inheritDoc
         */
        // eslint-disable-next-line no-unused-vars
        getOrders(ids, opts = {}) {
            const getOrderPromises = ids.map((id) => this.getOrder(id).catch(() => undefined))
            return Promise.all(getOrderPromises).then((orders) => {
                const validOrders = orders.filter((product) => !!product)
                return {
                    count: validOrders.length,
                    data: validOrders,
                    total: validOrders.length
                }
            })
        }

        /**
         * @inheritDoc
         */
        // eslint-disable-next-line no-unused-vars
        addCouponEntry(cart, couponCode, opts = {}) {
            if (!cart) {
                throw new errors.InvalidArgumentError(`Parameter 'cart' is required`)
            }
            if (!couponCode) {
                throw new errors.InvalidArgumentError(`Parameter 'couponCode' is required`)
            }

            return this.agent
                .put(
                    `${this.basePath}/rest/default/V1/${
                        this.hasAuthorization() ? 'carts/mine' : `guest-carts/${cart.id}`
                    }/coupons/${couponCode}`
                )
                .then(() => this.getCart(cart.id))
                .catch(() => {
                    throw new errors.ServerError('Could Not Add Coupon')
                })
        }

        /**
         * @inheritDoc
         */
        // eslint-disable-next-line no-unused-vars
        removeCouponEntry(cart, couponEntryId, opts = {}) {
            if (!cart) {
                throw new errors.InvalidArgumentError(`Parameter 'cart' is required`)
            }
            if (!couponEntryId) {
                throw new errors.InvalidArgumentError(`Parameter 'couponEntryId' is required`)
            }

            // Transform coupon into something the server understands.
            const coupon = {
                coupon_item_id: couponEntryId
            }

            return this.agent
                .delete(
                    `${this.basePath}/rest/default/V1/${
                        this.hasAuthorization() ? 'carts/mine' : `guest-carts/${cart.id}`
                    }/coupons`
                )
                .send(coupon)
                .then(() => this.getCart(cart.id))
                .catch(() => {
                    throw new errors.ServerError('Could Not Remove Coupon')
                })
        }
    }