Source: connectors/sfcc.js

/* * *  *  * *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  * */
/* Copyright (c) 2018 Mobify Research & Development Inc. All rights reserved. */
/* * *  *  * *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  * */
/** @module */

import ShopApi from 'commercecloud-ocapi-client'
import btoa from 'btoa'
import * as errors from '../errors'

const clean = (obj) => {
    Object.keys(obj).forEach(
        (key) => (obj[key] === null || obj[key] === undefined) && delete obj[key]
    )
    return obj
}

const filterAttributeDictionary = {
    categoryId: 'cgid'
}

/**
 * This function will take product search request as we use them in the commerce-integrations
 * and output them into a format usable by the specific connector (ocapi).
 *
 * @private
 */
const transformProductSearchRequest = ({query, count, start, sort, filters = {}}) => ({
    count,
    q: query,
    sort,
    start,
    refine: Object.keys(filters).map(
        (key) => `${filterAttributeDictionary[key] || key}=${filters[key]}`
    )
})

/**
 * Takes a OCAPI Image object and parses it into a commerce-integrations
 * Image type.
 *
 * @param {Object} data a OCAPI {@link https://documentation.demandware.com/DOC1/topic/com.demandware.dochelp/OCAPI/18.3/shop/Documents/Image.html|Image} document
 *
 * @return {module:types.Image}.
 */
/* eslint-disable camelcase */
const parseImage = ({alt, dis_base_link, link, title}) => ({
    alt,
    description: alt,
    src: dis_base_link || link,
    title
})
/* eslint-enable camelcase */

/**
 * Takes a OCAPI VariationAttributeValue object and parses it into a commerce-integrations
 * ProductSearchResult type.
 *
 * @param {Object} data a OCAPI {@link https://documentation.demandware.com/DOC1/topic/com.demandware.dochelp/OCAPI/18.3/shop/Documents/VariationAttributeValue.html|VariationAttributeValue} document
 * @return {module:types.VariationPropertyValue}
 * @private
 */
/* eslint-disable camelcase */
const parseVariationPropertyValue = ({image, image_swatch, name, value}) => ({
    name,
    value,
    mainImage: image ? parseImage(image) : undefined,
    swatchImage: image_swatch ? parseImage(image_swatch) : undefined
})
/* eslint-enable camelcase */

/**
 * Takes a OCAPI VariationAttribute object and parses it into a commerce-integrations
 * VariationProperty type.
 *
 * @param {Object} data a OCAPI {@link https://documentation.demandware.com/DOC1/topic/com.demandware.dochelp/OCAPI/18.3/shop/Documents/VariationAttribute.html|VariationAttribute} document
 * @return {module:types.VariationProperty}
 * @private
 */
const parseVariationProperty = ({id, name, values = []}) => ({
    id,
    label: name,
    values: values.map(parseVariationPropertyValue)
})

/**
 * Takes a OCAPI ProductSearchHit object and parses it into a commerce-integrations
 * ProductSearchResult type.
 *
 * @param {Object} data a OCAPI {@link https://documentation.demandware.com/DOC1/topic/com.demandware.dochelp/OCAPI/18.3/shop/Documents/ProductSearchHit.html|ProductSearchHit} document
 *
 * @returns {module:types.ProductSearchResult}.
 */
/* eslint-disable camelcase */
const parseProductSearchResult = ({
    product_id,
    product_name,
    image,
    orderable,
    price,
    variation_attributes
}) => ({
    available: orderable,
    productId: product_id,
    productName: product_name,
    price,
    defaultImage: image ? parseImage(image) : undefined,
    variationProperties: variation_attributes
        ? variation_attributes.map(parseVariationProperty)
        : []
})
/* eslint-enable camelcase */

/**
 * Takes a OCAPI ProductSearchRefinementValue object and parses it into a commerce-integrations
 * FilterValue type.
 *
 * @param {Object} data a OCAPI
 * {@link https://documentation.demandware.com/DOC1/topic/com.demandware.dochelp/OCAPI/18.3/shop/Documents/ProductSearchRefinementValue.html|ProductSearchRefinementValue} document
 *
 * @returns {module:types.FilterValue}.
 */
/* eslint-disable camelcase */
const parseFilterValue = ({hit_count, label, value}) => ({
    count: hit_count,
    label,
    value
})
/* eslint-enable camelcase */

/**
 * Takes a OCAPI ProductSearchRefinement object and parses it into a commerce-integrations
 * Filter type.
 *
 * @param {Object} data a OCAPI
 * {@link https://documentation.demandware.com/DOC1/topic/com.demandware.dochelp/OCAPI/18.3/shop/Documents/ProductSearchRefinement.html|ProductSearchRefinement} document // eslint-disable-line
 *
 * @returns {module:types.Filter}.
 */
/* eslint-disable camelcase */
const parseFilters = ({label, attribute_id, values}) => ({
    propertyId: attribute_id,
    label,
    values: values ? values.map((value) => parseFilterValue(value)) : undefined
})
/* eslint-enable camelcase */

/**
 * Takes a OCAPI ProductSearchResult object and parses it into a commerce-integrations
 * ProductSearch type.
 *
 * @param {Object} data a OCAPI
 * {@link https://documentation.demandware.com/DOC1/topic/com.demandware.dochelp/OCAPI/18.3/shop/Documents/ProductSearchResult.html?cp=0_12_5_96|ProductSearchResult} document // eslint-disable-line
 *
 * @returns {module:types.ProductSearch}
 */
/* eslint-disable camelcase */
const parseProductSearch = ({
    count,
    hits = [],
    query,
    refinements = [],
    selected_sorting_option,
    selected_refinements,
    sorting_options,
    start,
    total
}) => ({
    count,
    filters: refinements.map((refinement) => parseFilters(refinement)),
    query,
    results: hits.map((hit) => parseProductSearchResult(hit)),
    selectedFilters: selected_refinements,
    selectedSortingOption: selected_sorting_option,
    sortingOptions: sorting_options,
    start,
    total
})
/* eslint-enable camelcase */

/**
 * Takes a OCAPI Variant object and parses it into a commerce-integrations Variation type.
 * @param {Object} data a OCAPI {@link https://documentation.demandware.com/DOC1/topic/com.demandware.dochelp/OCAPI/18.3/shop/Documents/Variant.html|Variant} document
 *
 * @returns {Promise<module:types.Variation>}
 */
/* eslint-disable camelcase */
const parseVariation = ({product_id, price, orderable, variation_values}) => ({
    id: product_id,
    price,
    orderable,
    values: variation_values
})
/* eslint-enable camelcase */

/**
 * Takes a OCAPI ImageGroup object and parses it into a commerce-integrations ImageSet type.
 * @param {Object} data a OCAPI {@link https://documentation.demandware.com/DOC1/topic/com.demandware.dochelp/OCAPI/18.3/shop/Documents/ImageGroup.html|ImageGroup} document
 *
 * @returns {Promise<module:types.ImageSet>}
 */
/* eslint-disable camelcase */
const parseImageSet = ({images, view_type, variation_attributes}) => ({
    images: images.map(parseImage),
    variationProperties: variation_attributes && variation_attributes.map(parseVariationProperty),
    sizeType: view_type
})
/* eslint-enable camelcase */

/**
 * 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:types.Product>}
 */
/* eslint-disable camelcase */
const parseProduct = ({
    id,
    image_groups = [],
    name,
    long_description,
    primary_category_id,
    brand,
    min_order_quantity,
    step_quantity,
    upc,
    unit,
    price,
    prices,
    variants = [],
    variation_attributes = [],
    variation_values = {}
}) => ({
    id,
    name,
    imageSets: image_groups.map(parseImageSet),
    description: long_description,
    categoryId: primary_category_id,
    brand,
    minimumOrderQuantity: min_order_quantity,
    stepQuantity: step_quantity,
    upc,
    unit,
    price,
    prices,
    variations: variants.map(parseVariation),
    variationProperties: variation_attributes.map(parseVariationProperty),
    variationValues: variation_values
})
/* eslint-enable camelcase */

/* eslint-disable camelcase */
const parseCustomer = ({customer_id, first_name, last_name, email}) => ({
    id: customer_id,
    firstName: first_name,
    lastName: last_name,
    email
})
/* eslint-enable camelcase */

const transformOrderAddress = (shippingAddress) => ({
    first_name: shippingAddress.firstName,
    last_name: shippingAddress.lastName,
    phone: shippingAddress.phone,
    address1: shippingAddress.addressLine1,
    address2: shippingAddress.addressLine2,
    country_code: shippingAddress.countryCode,
    state_code: shippingAddress.stateCode,
    city: shippingAddress.city,
    postal_code: shippingAddress.postalCode
})

const transformPayment = (payment) => ({
    amount: payment.amount,
    payment_card: {
        issue_number: payment.details.number,
        security_code: payment.details.ccv,
        holder: payment.details.holderName,
        card_type: payment.details.type,
        expiration_month: payment.details.expiryMonth,
        expiration_year: payment.details.expiryYear
    },
    payment_method_id: payment.methodId
})

const transformShippingMethod = (shippingMethod) => ({
    id: shippingMethod.id
})

const transformCustomerInformation = (customerInfo) => ({
    customer_id: customerInfo.id,
    email: customerInfo.email
})

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

const transformCart = (cart) => {
    const transformedCart = {}
    const {billingAddress, customerInfo, items, payments, shippingAddress} = cart

    // Transform our mobify api schema to salesforce ocapi schema
    if (billingAddress) {
        transformedCart.billing_address = transformOrderAddress(billingAddress)
    }
    if (customerInfo) {
        transformedCart.customer_info = transformCustomerInformation(customerInfo)
    }
    if (items) {
        transformedCart.product_items = items.map(transformCartItem)
    }
    if (payments) {
        transformedCart.payment_instruments = payments.map(transformPayment)
    }
    if (shippingAddress) {
        transformedCart.shipments = [
            {
                shipment_id: 'me',
                shipping_address: transformOrderAddress(shippingAddress)
            }
        ]
    }

    return transformedCart
}

/**
 * A connector for the Salesforce Commerce Cloud API.
 *
 * @implements {module:connectors/interfaces.CommerceConnector}
 * @implements {module:connectors/interfaces.ParserHooks}
 */
export class SalesforceConnector {
    /**
     * @param client {ShopApi.ApiClient}
     */
    constructor(client) {
        this.client = client
    }

    /**
     * Given a configuration in the form of a plain object, this method returns
     * a new SalesforceConnector instance.
     *
     * @param {Object} config {@link https://github.com/mobify/commercecloud-ocapi-client/blob/develop/src/ApiClient.js#L44}
     * @returns {module:sfcc.SalesforceConnector} The new SalesforceConnector instance.
     */
    static fromConfig(config) {
        return new this.prototype.constructor(new ShopApi.ApiClient(config))
    }

    /**
     * @inheritDoc
     */
    createCart(oldCart) {
        const api = new ShopApi.BasketsApi(this.client)
        const options = {}

        // Append the cart to the body.
        if (oldCart) {
            options.body = transformCart(oldCart)
        }

        return api
            .postBaskets(options)
            .then((data) => this.parseCart(data))
            .catch(() => {
                throw new errors.ServerError('Could Not Create Cart')
            })
    }

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

        const api = new ShopApi.BasketsApi(this.client)

        return api
            .getBasketsByID(cartId)
            .then((data) => this.parseCart(data))
            .catch(() => {
                throw new errors.NotFoundError('Cart Not Found')
            })
    }

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

        const api = new ShopApi.BasketsApi(this.client)
        return api.deleteBasketsByID(cartId).catch(() => {
            throw new errors.ServerError('Could Not Delete Cart')
        })
    }

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

        const api = new ShopApi.BasketsApi(this.client)

        // eslint-disable-next-line no-undef
        cartItem = transformCartItem(cartItem)

        return api
            .postBasketsByIDItems(cart.id, [cartItem])
            .then((data) => this.parseCart(data))
            .catch(() => {
                throw new errors.ServerError('Could Not Add 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`)
        }

        const api = new ShopApi.BasketsApi(this.client)

        return api
            .deleteBasketsByIDItemsByID(cart.id, cartItemId)
            .then((data) => this.parseCart(data))
            .catch(() => {
                throw new errors.ServerError('Could Not Remove Cart')
            })
    }

    /**
     * @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 api = new ShopApi.BasketsApi(this.client)
        const cartItemId = cartItem.id

        // eslint-disable-next-line no-undef
        cartItem = transformCartItem(cartItem)

        return api
            .patchBasketsByIDItemsByID(cart.id, cartItemId, cartItem)
            .then((data) => this.parseCart(data))
            .catch(() => {
                throw new errors.ServerError('Could Not Update Cart')
            })
    }

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

        const api = new ShopApi.BasketsApi(this.client)

        // Transform Mobify schema into OCAPI schema
        // eslint-disable-next-line no-undef
        shippingAddress = transformOrderAddress(shippingAddress)

        return api
            .putBasketsByIDShipmentsByIDShippingAddress(cart.id, 'me', shippingAddress)
            .then((data) => this.parseCart(data))
            .catch(() => {
                throw new errors.ServerError('Could Not Set Shipping Address')
            })
    }

    /**
     * @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 api = new ShopApi.BasketsApi(this.client)

        // Transform Mobify schema into OCAPI schema
        // eslint-disable-next-line no-undef
        billingAddress = transformOrderAddress(billingAddress)

        return api
            .putBasketsByIDBillingAddress(cart.id, {body: billingAddress})
            .then((data) => this.parseCart(data))
            .catch(() => {
                throw new errors.ServerError('Could Not Set Billing Address')
            })
    }

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

        const api = new ShopApi.BasketsApi(this.client)

        // Only allow one payment of a given type. If there is already a type in the cart,
        // then get it's id so we can `patch` it.
        const {payments = []} = cart
        const oldPayment = payments.find(({methodId}) => methodId === payment.methodId)

        // Transform Mobify schema into OCAPI schema
        // eslint-disable-next-line no-undef
        payment = transformPayment(payment)

        const request = !oldPayment
            ? api.postBasketsByIDPaymentInstruments(cart.id, payment)
            : api.patchBasketsByIDPaymentInstrumentsByID(cart.id, oldPayment.id, payment)

        return request
            .then((data) => this.parseCart(data))
            .catch(() => {
                throw new errors.ServerError('Could Not Add Payment')
            })
    }

    /**
     * Register a new customer account and login.
     *
     * @param {module:types/CustomerRegistration} data
     * @return {Promise<module:types/Customer>}
     */
    registerCustomer(data) {
        const body = {
            password: data.password,
            customer: {
                first_name: data.firstName,
                last_name: data.lastName,
                login: data.email,
                email: data.email
            }
        }
        const api = new ShopApi.CustomersApi(this.client)
        return api.postCustomers(body).then(() => this.login(data.email, data.password))
    }

    /**
     * @inheritDoc
     */
    login(username, password) {
        const type = username && password ? 'credentials' : 'guest'
        const api = new ShopApi.CustomersApi(this.client)

        const authParam =
            type === 'credentials'
                ? {authorization: `Basic ${btoa(`${username}:${password}`)}`}
                : undefined

        // Clear authorization from previous login
        delete api.apiClient.defaultHeaders.authorization

        return api
            .postCustomersAuth({type}, authParam)
            .then((data) => this.parseCustomer(data))
            .catch(() => {
                throw new errors.ForbiddenError('Login error.')
            })
    }

    /**
     * @inheritDoc
     */
    logout() {
        const api = new ShopApi.CustomersApi(this.client)
        return api
            .deleteCustomersAuth()
            .then(() => {
                // Clear authorization from current login
                delete api.apiClient.defaultHeaders.authorization
                return undefined
            })
            .catch(() => {
                throw new errors.ServerError('Logout error.')
            })
    }

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

        const api = new ShopApi.BasketsApi(this.client)

        return (
            api
                .getBasketsByIDShipmentsByIDShippingMethods(cart.id, 'me')
                // eslint-disable-next-line camelcase
                .then(({applicable_shipping_methods = []}) =>
                    applicable_shipping_methods.map(this.parseShippingMethod)
                )
                .catch(() => {
                    throw new errors.ServerError('Could Not Get Shipping Methods')
                })
        )
    }

    /**
     * @inheritDoc
     */
    refreshSession() {
        const api = new ShopApi.CustomersApi(this.client)

        return api
            .postCustomersAuth({type: 'refresh'})
            .then((data) => this.parseCustomer(data))
            .catch(() => {
                throw new errors.ForbiddenError('Unable to refresh session.')
            })
    }

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

        const api = new ShopApi.BasketsApi(this.client)

        return (
            api
                .getBasketsByIDPaymentMethods(cart.id, 'me')
                // eslint-disable-next-line camelcase
                .then(({applicable_payment_methods = []}) =>
                    applicable_payment_methods.map(this.parsePaymentMethod)
                )
        )
    }

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

        const api = new ShopApi.BasketsApi(this.client)

        // Transform Mobify schema into OCAPI schema
        // eslint-disable-next-line no-undef
        shippingMethod = transformShippingMethod(shippingMethod)

        return api
            .putBasketsByIDShipmentsByIDShippingMethod(cart.id, 'me', shippingMethod)
            .then((data) => this.parseCart(data))
            .catch(() => {
                throw new errors.ServerError('Could Not Set Shipping Method')
            })
    }

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

        const api = new ShopApi.BasketsApi(this.client)

        // Transform Mobify schema into OCAPI schema
        // eslint-disable-next-line no-undef
        customerInformation = transformCustomerInformation(customerInformation)

        return api
            .putBasketsByIDCustomer(cart.id, customerInformation)
            .then((data) => this.parseCart(data))
    }

    /**
     * Parse a cart item
     */
    parseCartItem(data) {
        return {
            id: data.item_id,
            baseItemPrice: data.base_price,
            baseLinePrice: data.base_price * data.quantity,
            productId: data.product_id,
            productName: data.product_name,
            quantity: data.quantity,
            itemPrice: data.price_after_order_discount / data.quantity,
            linePrice: data.price_after_order_discount
        }
    }

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

    /**
     * Parse an order address
     */
    parseOrderAddress(data) {
        return {
            firstName: data.first_name,
            lastName: data.last_name,
            phone: data.phone,
            addressLine1: data.address1,
            addressLine2: data.address2,
            city: data.city,
            countryCode: data.country_code,
            stateCode: data.state_code,
            postalCode: data.postal_code
        }
    }

    /**
     * Parse customer information
     */
    parseCustomerInformation(data) {
        return {
            id: data.customer_id,
            email: data.email || undefined
        }
    }

    /**
     * Parse a shipping method
     */
    parseShippingMethod(data) {
        return {
            id: data.id,
            cost: data.price,
            label: `${data.name} - ${data.description}`
        }
    }

    /**
     * Parse a payment method
     */
    parsePaymentMethod(data) {
        const {cards} = data
        const method = {
            id: data.id,
            name: data.name
        }

        if (cards) {
            method.types = cards.map((card) => ({
                id: card.card_type,
                name: card.name
            }))
        }

        return method
    }

    /**
     * Parse a payment.
     */
    parsePayment(data) {
        return {
            id: data.payment_instrument_id,
            amount: data.amount,
            methodId: data.payment_method_id,
            details: {
                type: data.payment_card.card_type,
                expiryMonth: data.payment_card.expiration_month,
                expiryYear: data.payment_card.expiration_year,
                holderName: data.payment_card.holder,
                number: data.payment_card.issue_number,
                maskedNumber: data.payment_card.masked_number
            }
        }
    }

    /**
     * Parse a cart
     */
    parseCart(data) {
        /* eslint-disable camelcase */
        const {
            billing_address,
            shipments = [],
            coupon_items = [],
            payment_instruments = [],
            product_items = [],
            customer_info
        } = data
        let shippingAddress
        let selectedShippingMethodId

        const shipment = shipments.length ? shipments[0] : undefined
        if (shipment) {
            shippingAddress = shipment.shipping_address
                ? this.parseOrderAddress(shipment.shipping_address)
                : undefined

            selectedShippingMethodId = shipment.shipping_method
                ? shipment.shipping_method.id
                : undefined
        }
        const billingAddress = billing_address ? this.parseOrderAddress(billing_address) : undefined

        const payments = payment_instruments.map(this.parsePayment)
        const items = product_items.map(this.parseCartItem)
        const couponEntries = coupon_items.map(this.parseCouponEntry)

        const parseNumberValue = (value) => {
            return value !== null ? value : undefined
        }
        const id = data.basket_id
        const customerInfo = customer_info
            ? this.parseCustomerInformation(customer_info)
            : undefined
        const subtotal = parseNumberValue(data.product_sub_total)
        const discounts = (data.order_price_adjustments || []).reduce(
            (acc, curr) => acc + curr.price,
            0
        )
        const shipping = parseNumberValue(data.shipping_total)
        const tax = parseNumberValue(data.tax_total || data.merchandize_total_tax)
        const total = parseNumberValue(data.order_total || data.product_total)

        return {
            id,
            discounts,
            billingAddress,
            couponEntries,
            customerInfo,
            items,
            payments,
            shippingAddress,
            selectedShippingMethodId,
            shipping,
            subtotal,
            tax,
            total
        }
        /* eslint-enable camelcase */
    }

    /**
     * @inheritDoc
     */
    getDefaultHeaders() {
        return Object.assign({}, this.client.defaultHeaders)
    }

    /**
     * @inheritDoc
     */
    setDefaultHeaders(headers) {
        this.client.defaultHeaders = Object.assign({}, headers)
    }

    /**
     * @inheritDoc
     */
    getCustomer(id, opts) {
        const api = new ShopApi.CustomersApi(this.client)
        const defaultOptions = {
            expand: ['addresses', 'paymentinstruments'],
            allImages: true
        }
        const options = {
            ...defaultOptions,
            ...opts
        }
        return api.getCustomersByID(id, options).then((data) => this.parseCustomer(data))
    }

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

    /**
     * @inheritDoc
     */
    getProduct(id, opts = {}) {
        const api = new ShopApi.ProductsApi(this.client)
        const defaultOptions = {
            expand: ['availability', 'prices', 'variations', 'images'],
            allImages: true
        }
        const options = {
            ...defaultOptions,
            ...opts
        }
        return api
            .getProductsByID(id, options)
            .then((data) => this.parseProduct(data))
            .catch(() => {
                throw new errors.NotFoundError('Product Not Found')
            })
    }

    /**
     * @inheritDoc
     */
    getProducts(ids, opts = {}) {
        if (!ids || ids.length <= 0) {
            throw new Error('Please specify list of product ids to get.')
        }

        const api = new ShopApi.ProductsApi(this.client)
        const defaultOptions = {
            expand: ['availability', 'prices', 'variations', 'images'],
            allImages: true
        }
        const options = {
            ...defaultOptions,
            ...opts
        }
        return api.getProductsByIDs(ids, options).then(({count, data = [], total}) => {
            return {
                count,
                data: data.map(this.parseProduct),
                total
            }
        })
    }

    /**
     * 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:types.Product>}
     */
    parseProduct(data) {
        return parseProduct(data)
    }

    /**
     * @inheritDoc
     */
    searchProducts(productSearchRequest, opts = {}) {
        let searchRequest = transformProductSearchRequest(productSearchRequest)
        const api = new ShopApi.ProductSearchApi(this.client)

        const defaultOptions = {
            expand: ['availability', 'prices', 'variations', 'images']
        }

        // Assign options to new object. This allows the user to override
        // and defaults we set.
        searchRequest = {
            ...searchRequest,
            ...defaultOptions,
            ...opts
        }

        return api
            .getProductSearch(searchRequest)
            .then((data) => this.parseSearchProducts(data, searchRequest))
    }

    /**
     * Takes a OCAPI ProductSearchResult object and parses it into a commerce-integrations
     * ProductSearch type.
     *
     * @param {Object} data a OCAPI
     * {@link https://documentation.demandware.com/DOC1/topic/com.demandware.dochelp/OCAPI/18.3/shop/Documents/ProductSearchResult.html?cp=0_12_5_96|ProductSearchResult} document
     *
     * @returns {Promise<module:types.ProductSearch>}
     */
    parseSearchProducts(data, searchParams) {
        return parseProductSearch(data, searchParams)
    }

    /**
     * @inheritDoc
     */
    getStore(id) {
        const api = new ShopApi.StoresApi(this.client)

        return api.getStoresByID(id).then((data) => this.parseGetStore(data))
    }

    /**
     * Takes a OCAPI Store object and parses it into a commerce-integrations Store type.
     * @param {Object} data a OCAPI {@link https://documentation.demandware.com/DOC1/topic/com.demandware.dochelp/OCAPI/18.3/shop/Documents/Store.html|Store} document
     *
     * @returns {Promise<module:types.Store>}
     */
    parseGetStore(data) {
        const d = data
        return {
            addressLine1: d.address1,
            addressLine2: d.address2,
            city: d.city,
            country: d.country_code,
            email: d.email,
            id: d.id,
            name: d.name,
            phone: d.phone
                ? d.phone.replace(new RegExp('[(]|[)]|[.]|[ ]|[-]|[#]|[x]', 'g'), '')
                : undefined,
            postalCode: d.postal_code,
            hours: d.store_hours,
            images: d.image ? {src: d.image, alt: d.name} : undefined
        }
    }

    /**
     * This function will take product search request as we use them in the commerce-integrations
     * and output them into a format usable by the specific connector (ocapi).
     */
    transformSearchStoreParams(searchParams) {
        const sp = searchParams
        return {
            count: sp.count > 0 ? sp.count : 20,
            start: sp.start > 0 ? sp.start : 0,
            distanceUnit: 'km', // only 'mi' and 'km' supported
            latitude: sp.latlon.latitude,
            longitude: sp.latlon.longitude
        }
    }

    /**
     * @inheritDoc
     */
    // eslint-disable-next-line no-unused-vars
    searchStores(storeSearchRequest, opts = {}) {
        if (
            isNaN(storeSearchRequest.latlon.latitude) ||
            isNaN(storeSearchRequest.latlon.longitude)
        ) {
            throw new Error('Provide Latitude and Longitude coordinates')
        }

        const api = new ShopApi.StoresApi(this.client)
        const searchRequest = this.transformSearchStoreParams(storeSearchRequest)
        return api.getStores(searchRequest).then((data) => this.parseSearchStores(data))
    }

    parseSearchStores(data) {
        const d = data
        return {
            count: d.count,
            start: d.start,
            total: d.total,
            stores: d.data ? d.data.map((store) => this.parseGetStore(store)) : undefined
        }
    }

    /**
     * @inheritDoc
     */
    getCategory(id, options = {}) {
        const api = new ShopApi.CategoriesApi(this.client)
        return api.getCategoriesByID(id, options).then((data) => this.parseCategory(data))
    }

    /**
     * @inheritDoc
     */
    getCategories(ids, options = {}) {
        const api = new ShopApi.CategoriesApi(this.client)
        return api.getCategoriesByIDs(ids, options).then(({data, count, total}) => ({
            data: this.parseCategories(data),
            count,
            total
        }))
    }

    /**
     * Takes an array of OCAPI Category objects and parses it into an
     * commerce-integrations Category type.
     * @param {Object[]} categories an array of OCAPI {@link https://mobify.github.io/commercecloud-ocapi-client/module-models_Category.html|Category} document
     *
     * @returns {Array<module:types.Category>}
     */
    parseCategories(categories) {
        return categories.map((c) => this.parseCategory(c))
    }

    /**
     * Takes a OCAPI Category objects and parses it into an
     * commerce-integrations Category type.
     * @param {Object[]} categories an OCAPI {@link https://mobify.github.io/commercecloud-ocapi-client/module-models_Category.html|Category} document
     *
     * @returns {module:types.Category}
     */
    /* eslint-disable camelcase */
    parseCategory({
        id,
        name,
        page_description,
        categories: subCategories,
        image: backgroundImage,
        thumbnail: thumbnailImage
    }) {
        const imageAttributes = {
            alt: name,
            description: page_description,
            title: name
        }
        const category = {
            id,
            name,
            description: page_description,
            thumbnailImage: thumbnailImage && this.parseImage(thumbnailImage, imageAttributes),
            backgroundImage: backgroundImage && this.parseImage(backgroundImage, imageAttributes)
        }

        if (subCategories) {
            category.categories = this.parseCategories(subCategories)
        }

        return clean(category)
    }
    /* eslint-enable camelcase */

    /**
     * Takes an image url and its attributes and return an
     * commerce-integrations Image type.
     * @param {string} imageUrl
     * @param {Object} attributes additional attributes of an image
     *
     * @returns {Promise<module:types.Image>}
     */
    parseImage(imageUrl, attributes) {
        return {
            ...attributes,
            src: imageUrl
        }
    }

    /**
     * @inheritDoc
     */
    parseOrder(data) {
        const order = this.parseCart(data)

        return {
            ...order,
            creationDate: new Date(data.creation_date),
            id: data.order_no,
            status: data.shipping_status
        }
    }

    /**
     * Create a new order using a given cart. This command will trigger the appropriate
     * OCAPI order authorization hooks. If orders placed through this command aren't progressing
     * from the `created` status, it's possible that your API hooks haven't been set up. Please
     * consult your Saleforce professional.
     * @param {module:types.Cart} cart The customer's cart.
     * @param {Object} opts
     * @return {Promise<module:types.Order>}.
     */
    // eslint-disable-next-line no-unused-vars
    createOrder(cart, opts = {}) {
        if (!cart) {
            throw new errors.InvalidArgumentError(`Parameter 'cart' is required`)
        }

        const api = new ShopApi.OrdersApi(this.client)

        // OCAPI only requires that you pass in the id of the basket, so we don't need
        // a complex transformation here.
        return api
            .postOrders({basket_id: cart.id})
            .then((data) => {
                // The OCAPI backend doesn't like having this value defined, so let's remove it.
                delete data.payment_instruments[0].payment_card.credit_card_expired

                // To ensure that the OCAPI backend triggers the payment authorization hook, we have
                // to either update the order payment intruments or add one. He you can see we are just
                // re-submitting the payments after it's creation.
                return api.patchOrdersByIDPaymentInstrumentsByID(
                    data.order_no,
                    data.payment_instruments[0].payment_instrument_id,
                    {
                        amount: data.payment_instruments[0].amount,
                        payment_card: data.payment_instruments[0].payment_card,
                        payment_method_id: data.payment_instruments[0].payment_method_id
                    }
                )
            })
            .then((data) => this.parseOrder(data))
    }

    /**
     * @inheritDoc
     */
    // eslint-disable-next-line no-unused-vars
    getOrder(id, opts = {}) {
        if (!id) {
            throw new errors.InvalidArgumentError(`Parameter 'id' is required`)
        }
        const api = new ShopApi.OrdersApi(this.client)

        // OCAPI only requires that you pass in the id of the basket, so we don't need
        // a complex transformation here.
        return api
            .getOrdersByID(id)
            .then((data) => this.parseOrder(data))
            .catch(() => {
                throw new errors.NotFoundError('Order Not Found')
            })
    }

    /**
     * @inheritDoc
     */
    // eslint-disable-next-line no-unused-vars
    getOrders(ids, opts = {}) {
        if (!ids || ids.length <= 0) {
            throw new errors.InvalidArgumentError(`Parameter 'ids' is required`)
        }

        const getOrderPromises = ids.map((id) => this.getOrder(id, opts).catch(() => null))

        return Promise.all(getOrderPromises).then((orders) => {
            const validOrders = orders.filter((order) => !!order)
            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`)
        }

        const api = new ShopApi.BasketsApi(this.client)

        return api
            .postBasketsByIDCoupons(cart.id, {code: couponCode})
            .then((data) => this.parseCart(data))
            .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`)
        }

        const api = new ShopApi.BasketsApi(this.client)

        return api
            .deleteBasketsByIDCouponsByID(cart.id, couponEntryId)
            .then((data) => this.parseCart(data))
            .catch(() => {
                throw new errors.ServerError('Could Not Remove Coupon')
            })
    }
}