/** @module hybris */
import * as hybris from 'hybris-occ-client'
import * as errors from './errors'
import superagent from 'superagent'
const clone = (x) => JSON.parse(JSON.stringify(x))
/**
* A connector for the Hybris API.
*
* @implements {module:interfaces.CommerceConnector}
* @implements {module:interfaces.ParserHooks}
*/
export class HybrisConnector {
constructor({client, catalogId, catalogVersionId, authentication}) {
this.client = client
this.catalogId = catalogId
this.catalogVersionId = catalogVersionId
this.authentication = authentication
}
static fromConfig(config) {
return new this.prototype.constructor({
client: new hybris.ApiClient(config.clientConfig),
catalogId: config.catalogId,
catalogVersionId: config.catalogVersionId,
authentication: config.authentication
})
}
/**
* @inheritDoc
*/
login(username, password) {
const isRegisteredUser = username && password
const auth = this.authentication
const body = {
client_id: auth.clientId,
client_secret: auth.clientSecret,
grant_type: 'client_credentials'
}
if (isRegisteredUser) {
Object.assign(body, {
grant_type: 'password',
username,
password
})
}
return superagent.post(auth.authorizationUrl)
.set('Content-Type', 'application/x-www-form-urlencoded')
.send(body)
.then((res) => {
const updatedAuth = {
accessToken: res.body.access_token,
expiresIn: res.body.expires_in,
refreshToken: res.body.refresh_token
}
Object.assign(this.client.authentications.auth, updatedAuth)
return isRegisteredUser ? this.getCustomer('current') : this.getCustomer('anonymous')
})
.catch(() => {
throw new errors.ForbiddenError('Login failed')
})
}
/**
* @inheritDoc
*/
logout() {
const invalidatedAuth = {
accessToken: undefined,
expiresIn: undefined,
refreshToken: undefined
}
Object.assign(this.client.authentications.auth, invalidatedAuth)
return Promise.resolve(undefined)
}
/**
* @inheritDoc
*/
refreshSession() {
const refreshToken = this.client.authentications.auth.refreshToken
const grantType = refreshToken ? 'refresh_token' : 'client_credentials'
const auth = this.authentication
const body = {
client_id: auth.clientId,
client_secret: auth.clientSecret,
grant_type: grantType,
refresh_token: refreshToken
}
return superagent.post(auth.authorizationUrl)
.set('Content-Type', 'application/x-www-form-urlencoded')
.send(body)
.then((res) => {
const newAuth = {
accessToken: res.body.access_token,
expiresIn: res.body.expires_in,
refreshToken: res.body.refresh_token
}
Object.assign(this.client.authentications.auth, newAuth)
return (refreshToken ?
this.getCustomer('current') :
this.getCustomer('anonymous')
)
})
.catch(() => {
throw new errors.ForbiddenError('Failed To Refresh Session')
})
}
/**
* @inheritDoc
*/
getDefaultHeaders() {
return Object.assign({}, this.client.defaultHeaders)
}
/**
* @inheritDoc
*/
setDefaultHeaders(headers) {
this.client.defaultHeaders = Object.assign({}, headers)
}
/**
* Get current authentications object, which can be used to
* save/restore auth state, eg. for server-side usage.
*
* @return {Object}
*/
getAuthentications() {
return clone(this.client.authentications)
}
/**
* Set the current authentications object, as returned from getAuthentications.
* Use to save/restore auth state, eg. for server-side usage.
*
* @param {Object} authentications
*/
setAuthentications(authentications) {
this.client.authentications = clone(authentications)
}
/**
* @inheritDoc
*/
getCustomer(id, opts = {}) {
const api = new hybris.UsersApi(this.client)
return api.getUser(id, {fields: 'FULL', ...opts})
.then((res) => this.parseCustomer(res))
}
/**
* @inheritDoc
*/
parseCustomer(data) {
const d = data
return {
id: d.uid === 'anonymous' ? 'anonymous' : 'current',
firstName: d.firstName,
lastName: d.lastName,
email: d.defaultAddress ? d.defaultAddress.email : undefined
}
}
/**
* @inheritDoc
*/
searchProducts(query, opts = {}) {
const searchRequest = this.transformSearchParams(query)
const api = new hybris.ProductsApi(this.client)
return api.getProductSearch(searchRequest, opts)
.then((data) => this.parseSearchProducts(data, query))
}
transformSearchParams(params) {
const {
count = 20,
start = 0,
sort = 'relevance',
filters
} = params
return {
query: [].concat(...[
['', sort],
Object.keys(filters)
.filter((key) => !['categoryId', 'query'].includes(key))
.filter((key) => Boolean(filters[key]))
.map((key) => `${key}:${filters[key]}`),
[`allCategories:${filters.categoryId}`],
]).join(':'),
pageSize: count,
currentPage: start / count,
sort,
fields: 'FULL'
}
}
parseFilterValue({count, name, query}) {
// Parse query string to get filter value.
const value = query.query.value.split(':').slice(-1)[0]
return {
count,
label: name,
value
}
}
parseFilter({name, values}) {
// Get the filter identifier from the first values query string. There should
// always be a value or else the filter wouldn't be listed, but to be sure
// return undefiend if there isn't one.
let filterId
const firstValue = values[0]
if (firstValue) {
// Parse query string to get filter identifier.
filterId = firstValue.query.query.value.split(':').slice(-2)[0]
}
return {
id: filterId,
label: name,
values: values.map(this.parseFilterValue.bind(this))
}
}
parseSearchProducts(data) {
const {
products = [],
sorts = [],
pagination,
currentQuery,
facets = [],
breadcrumbs = [],
} = data
const getFilterCrumbs = () => {
const selectedFilterVals = ([].concat(
...facets.map((filter) => filter.values.filter((f) => f.selected))))
const selectedBreadcrumbs = ([].concat(
...selectedFilterVals.map(
(fv) => breadcrumbs.filter((bc) => bc.facetValueName === fv.name))))
const allCatsCrumb = breadcrumbs.find((bc) => bc.facetCode === 'allCategories')
allCatsCrumb && selectedBreadcrumbs.push(allCatsCrumb)
return selectedBreadcrumbs
}
return {
results: products.map(({stock = {}, images = [], ...product}) => ({
available: stock.stockLevelStatus === 'inStock',
productId: product.code,
productName: product.name,
link: product.url,
defaultImage: images[0] && {
alt: product.name,
src: images[0].url
},
price: product.price.value
})),
query: currentQuery.query.value || '',
count: pagination.pageSize,
start: pagination.currentPage * pagination.pageSize,
total: pagination.totalResults,
filters: facets.map(this.parseFilter.bind(this)),
selectedFilters: getFilterCrumbs().reduce((obj, curr) => {
obj[curr.facetCode] = curr.facetValueCode
return obj
}, {}),
sortingOptions: sorts.map(({code, name}) => ({
id: code,
label: name
})),
selectedSortingOption: pagination.sort
}
}
/**
* @inheritDoc
*/
getCategory(id, options = {}) {
const api = new hybris.CatalogsApi(this.client)
return api.getCategory(this.catalogId, this.catalogVersionId, id, options)
.then((data) => this.parseCategory(data))
}
/**
* @inheritDoc
*/
getCategories(ids, options = {}) {
const getCategoryPromises = ids.map((id) => this.getCategory(id, options).catch(() => null))
return Promise.all(getCategoryPromises)
.then((categories) => {
const validCategories = categories.filter((category) => !!category)
return {
data: validCategories,
count: Object.keys(categories).length,
total: ids.length
}
})
}
/**
* 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/hybris-occ-client/module-models_CategoryHierarchy.html|Category} document
*
* @returns {Promise<module:types.Category>}
*/
/* eslint-disable camelcase */
parseCategories(categories) {
const parsedCategories = []
categories.forEach((category) => parsedCategories.push(this.parseCategory(category)))
return parsedCategories
}
/**
* Takes an OCAPI Category objects and parses it into an
* commerce-integrations Category type.
* @param {Object[]} categories an OCAPI {@link https://mobify.github.io/hybris-occ-client/module-models_CategoryHierarchy.html|Category} document
*
* @returns {Promise<module:types.Category>}
*/
parseCategory({
id,
name,
subcategories
}) {
const category = {
id,
name
}
if (subcategories) {
category.categories = this.parseCategories(subcategories)
}
return category
}
/**
* @inheritDoc
*/
getProduct(id, opts = {}) {
let options = {}
const api = new hybris.ProductsApi(this.client)
const defaultOptions = {
fields: 'FULL'
}
// Assign options to new object. This allows the user to override
// the defaults we set.
options = {
...defaultOptions,
...opts
}
// Get the base product by creating a promise chain recursively.
const productCache = []
const getBaseProduct = (id, options) => (
api.getProduct(id, options)
.then((product) => {
const {baseProduct} = product
// Maintain a cache of all products requested in this command.
// It will be used lated to de-dup any fetches.
productCache.push(product)
return baseProduct ?
getBaseProduct(baseProduct, options) : product
})
)
return getBaseProduct(id, options)
.then((baseProduct) => {
// Overwrite the base product id with the id we initiality requested so it's parsed
// out correctly later on.
baseProduct.code = id
return Promise.all(baseProduct.variantOptions.map((variantOption) => {
// Search product cache and return products already fetched.
const cachedProduct = productCache.find(({code}) => code === variantOption.code)
return cachedProduct || api.getProduct(variantOption.code, options)
})).then((variations) => ({baseProduct, variations}))
})
.then(({baseProduct, variations}) => this.parseProduct(baseProduct, variations))
.then((parsedProduct) => {
// Get the variation values from the first product requested.
const {baseOptions = []} = productCache[0]
const {selected = {}} = baseOptions[0]
const {variantOptionQualifiers = []} = selected
const variationValues = variantOptionQualifiers.reduce((acc, {qualifier, value}) => ({
...acc,
[qualifier]: value
}), {})
// Set the products values.
parsedProduct.variationValues = variationValues
return parsedProduct
})
.catch(() => {
// TODO: Lets come up with a formal way of handling errors with it's
// own time and things like that. We'll need to map the OCAPI errors,
// to our own here.
throw new Error('Product Not Found')
})
}
/**
* @inheritDoc
*/
getProducts(ids, opts = {}) {
if (!ids || ids.length <= 0) { throw new Error('Please specify list of product ids to get.') }
const getProductPromises = ids.map((id) => this.getProduct(id, opts).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 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 */
parseImageSets(variation) {
const {images, variantOptions} = variation
const {variantOptionQualifiers} = variantOptions[0]
const variationProperties = variantOptionQualifiers.map(({qualifier, name, value}) => ({
id: qualifier,
lable: name,
values: [{
name: value,
value
}]
}))
return ['zoom', 'product', 'thumbnail'].map((type) => ({
images: images
.filter((image) => image.format === type)
.map(({altText, url}) => ({
alt: altText,
description: altText,
src: url,
title: altText
})),
variationProperties,
type
}))
}
/* eslint-enable camelcase */
/**
* Takes a OCC Product object and parses it into a commerce-integrations Product type.
* @param {Object} data a OCC product document
*
* @returns {Promise<module:types/Product>}
*/
/* eslint-disable camelcase */
parseProduct({
code,
name,
description,
categories,
price,
// images,
variantOptions
}, variations = []) {
// Parse all the image sets. We'll use these in our return value as well
// as for parsing property value images.
let allImageSets = []
variations.forEach((variation) => {
allImageSets = [...allImageSets, ...this.parseImageSets(variation)]
})
const allVariantOptions = [
...variantOptions,
...variations.reduce((acc, {variantOptions}) => [...acc, ...variantOptions], [])
]
const variationProperties = allVariantOptions
.reduce((acc, variantOption) => ([...acc, ...variantOption.variantOptionQualifiers]), [])
.reduce((acc, {qualifier, name, value}) => {
const variationProperty = acc.find((variationOptionQualifier) => variationOptionQualifier.id === qualifier)
// Lets get the first image of the correct type from the image sets.
const variationImageSets = allImageSets
.filter(({variationProperties}) => (variationProperties.find(({id, values}) => (id === qualifier && values[0].value === value))))
const filteredProductImageSets = variationImageSets
.filter(({type}) => (type === 'product'))
const filteredThumbnailImageSets = variationImageSets
.filter(({type}) => (type === 'thumbnail'))
if (variationProperty) {
const isNew = !variationProperty.values.find((v) => v.value === value)
if (isNew) {
variationProperty.values.push({
name: value,
value,
mainImage: filteredProductImageSets.length && filteredProductImageSets[0].images[0],
swatchImage: filteredThumbnailImageSets.length && filteredThumbnailImageSets[0].images[0],
})
}
} else {
acc.push({
id: qualifier,
label: name,
values: [{
name: qualifier,
value,
mainImage: filteredProductImageSets.length && filteredProductImageSets[0].images[0],
swatchImage: filteredThumbnailImageSets.length && filteredThumbnailImageSets[0].images[0],
}]
})
}
return acc
}, [])
const allVariations = variations
.map(({variantOptions}) => (
variantOptions.map(({code, priceData, stock, variantOptionQualifiers}) => ({
id: code,
price: priceData.value,
orderable: stock.stockLevelStatus === 'inStock',
values: variantOptionQualifiers.reduce((acc, curr) => {
acc[curr.qualifier] = curr.value
return acc
}, {})
}))
))
.reduce((acc, curr) => ([
...acc,
...curr
]), [])
return {
id: code,
name,
imageSets: allImageSets,
description,
categoryId: categories.length ? categories.pop().code : undefined,
minimumOrderQuantity: 1,
stepQuantity: 1,
price: price.value,
variations: allVariations,
variationProperties
}
}
/* eslint-enable camelcase */
/**
* @inheritDoc
*/
getStore(id) {
const api = new hybris.StoresApi(this.client)
return api.getStore(id, {fields: 'FULL'})
.then((data) => {
return this.parseGetStore({...data, id})
})
}
/**
* Takes a OCCAPI WeekdayOpeningDay object and parses it into a commerce-integrations Store.hours type.
* @param {Object} hours a OCCAPI {@link https://help.hybris.com/6.7.0/api/ycommercewebservices/index.html#_weekdayopeningdaywsdto|WeekdayOpneningDay} document
*
* @returns {Promise<module:types.Store.hours>}
*/
parseOpeningHours(hours) {
const parsedHours = {}
hours.forEach((h) => {
parsedHours[h.weekDay] = {
openingTime: h.openingTime ? h.openingTime.formattedHour : undefined,
closingTime: h.closingTime ? h.closingTime.formattedHour : undefined
}
})
return parsedHours
}
/**
* Takes a OCCAPI PointOfService object and parses it into a commerce-integrations Store type.
* @param {Object} data a OCCAPI {@link https://help.hybris.com/6.7.0/api/ycommercewebservices/index.html#_pointofservicewsdto|PointOfService} document
*
* @returns {Promise<module:types.Store>}
*/
parseGetStore(data) {
const d = data
const address = d.address
return {
addressLine1: address ? address.line1 : undefined,
addressLine2: address ? address.line2 : undefined,
city: address ? address.town : undefined,
country: address && address.country ? address.country.isocode : undefined,
description: d.description,
features: d.features ? d.features.entry.map((feature) => feature.value) : undefined,
email: address ? address.email : undefined,
name: d.name,
phone: address && address.phone ? address.phone.replace(new RegExp('[(]|[)]|[.]|[ ]|[-]|[#]|[x]', 'g'), '') : undefined,
postalCode: address ? address.postalCode : undefined,
hours: this.parseOpeningHours(d.openingHours.weekDayOpeningList),
images: d.storeImages ? d.storeImages.map((image) => {
return {src: image.url, alt: d.name}
}) : undefined,
id: d.id
}
}
transformSearchStoreParams(searchParams) {
const sp = searchParams
const count = sp.count > 0 ? sp.count : 20
const start = sp.start > 0 ? sp.start : 0
return {
pageSize: count.toString(),
currentPage: start.toString(),
sort: 'relevance',
latitude: !isNaN(sp.latlon.latitude) ? sp.latlon.latitude.toString() : undefined,
longitude: !isNaN(sp.latlon.longitude) ? sp.latlon.longitude.toString() : undefined,
fields: 'FULL'
}
}
/**
* @inheritDoc
*/
searchStores(storeSearchRequest, opts = {}) { // eslint-disable-line no-unused-vars
if (isNaN(storeSearchRequest.latlon.latitude) || isNaN(storeSearchRequest.latlon.longitude)) {
throw new Error('Provide Latitude and Longitude coordinates')
}
const api = new hybris.StoresApi(this.client)
const searchRequest = this.transformSearchStoreParams(storeSearchRequest)
return api.getStores(searchRequest)
.then((data) => {
if (data.stores.length <= 0) {
throw new Error('No stores found')
}
return this.parseSearchStores(data)
})
}
parseSearchStores(data) {
const d = data
let count
const start = d.pagination.currentPage
const total = d.pagination.totalResults
const pageSize = d.pagination.pageSize
const totalPages = d.pagination.totalPages
if (totalPages === start + 1) { // on last page
if (total > pageSize) {
const remainder = total % pageSize
count = (remainder !== 0) ? remainder : pageSize
} else {
count = total
}
} else { // middle pages
count = pageSize
}
return {
count,
start,
total,
stores: d.stores.length > 0 ? d.stores.map((store) => this.parseGetStore({...store, id: store.name})) : undefined
}
}
isLoggedIn() {
return this.getAuthentications().auth !== undefined
}
/**
* Returns customer id 'current' if a refresh token is set in the auth
* Otherwise, returns customer id 'anonymous'
*/
getCustomerIdFromAuth() {
if (!this.isLoggedIn()) { throw new errors.NotAuthenticatedError() }
return this.getAuthentications().auth.refreshToken === undefined ? 'anonymous' : 'current'
}
/**
* @inheritDoc
*/
createCart(oldCart, opts = {}) {
const api = new hybris.UsersApi(this.client)
const customerId = this.getCustomerIdFromAuth()
return api.postCart(customerId, {fields: 'FULL', ...opts})
.then((newCart) => {
if (customerId === 'current' && oldCart !== undefined) {
return api.postCart(customerId, { // postCart will merge and delete the old cart
fields: 'FULL',
toMergeCartGuid: newCart.guid,
oldCartId: oldCart.id
})
.catch(() => {
throw new errors.ServerError('Could not merge old cart')
})
}
return newCart
})
.then((cart) => this.parseCart(cart))
.catch(() => {
throw new errors.ServerError('Could not create cart')
})
}
/**
* @inheritDoc
*/
getCart(cartId, opts = {}) {
const api = new hybris.UsersApi(this.client)
if (!cartId) { throw new errors.InvalidArgumentError(`Parameter 'cartId' is required`) }
return api.getCart(this.getCustomerIdFromAuth(), cartId, {fields: 'FULL', ...opts})
.then((data) => this.parseCart(data))
.catch(() => { throw new errors.ServerError('Could not get cart') })
}
/**
* @inheritDoc
*/
deleteCart(cartId, opts = {}) { // eslint-disable-line no-unused-vars
if (!cartId) { throw new errors.InvalidArgumentError(`Parameter 'cartId' is required`) }
const api = new hybris.UsersApi(this.client)
return api.deleteCart(this.getCustomerIdFromAuth(), cartId)
.catch(() => { throw new errors.ServerError('Could not delete cart') })
}
/**
* @inheritDoc
*/
parseCart(data) {
const d = data
if (!d.guid && !d.code) {
throw new Error('Error parsing cart')
}
const customerId = this.getCustomerIdFromAuth()
const cartId = customerId === 'anonymous' ? d.guid : d.code
return {
id: cartId,
customerInfo: {
id: customerId,
email: d.email
},
items: d.entries ? d.entries.map(this.parseCartItem) : [],
shippingAddress: d.deliveryAddress ? this.parseOrderAddress(d.deliveryAddress) : undefined,
billingAddress: d.paymentInfo && d.paymentInfo.billingAddress ?
this.parseOrderAddress(d.paymentInfo.billingAddress)
: undefined,
shippingMethods: [],
paymentMethods: [],
selectedShippingMethodId: d.deliveryMode ? d.deliveryMode.code : undefined,
payments: d.paymentInfo ? [this.parsePayment({...d.paymentInfo, amount: d.totalPriceWithTax.value})] : [],
subtotal: d.subTotal ? d.subTotal.value : 0,
shipping: d.deliveryCost ? d.deliveryCost.value : 0,
discounts: d.totalDiscounts ? d.totalDiscounts.value : 0,
tax: d.totalTax ? d.totalTax.value : 0,
total: d.totalPriceWithTax ? d.totalPriceWithTax.value : 0
}
}
/**
* @inheritDoc
*/
parseCartItem(data) {
const d = data
const quantity = d.quantity || 0
return {
id: `${d.entryNumber}`,
productId: d.product ? d.product.code : undefined,
productName: d.product ? d.product.name : undefined,
quantity,
baseItemPrice: d.basePrice ? d.basePrice.value : 0,
baseLinePrice: d.basePrice ? d.basePrice.value * quantity : 0,
itemPrice: d.totalPrice && (quantity !== 0) ? d.totalPrice.value / quantity : 0,
linePrice: d.totalPrice ? d.totalPrice.value : 0
}
}
/**
* @inheritDoc
*/
parseShippingMethod(data) {
return {
id: data.code,
name: data.name,
cost: data.deliveryCost || 0
}
}
/**
* @inheritDoc
*/
parseOrderAddress(data) {
const d = data
return {
id: d.id,
titleCode: d.titleCode,
firstName: d.firstName,
lastName: d.lastName,
phone: d.phone,
addressLine1: d.line1,
addressLine2: d.line2,
countryCode: d.country ? d.country.isocode : undefined,
stateCode: d.region ? d.region.isocode : undefined,
city: d.town,
postalCode: d.postalCode,
}
}
/**
* @inheritDoc
*/
parsePayment(data) {
const d = data
return {
id: d.id,
amount: d.amount, // amount is always cart total for now.
methodId: 'CREDIT_CARD', // only 1 type of payment method, we'll call it 'CREDIT_CARD'
details: {
type: d.cardType.code,
holderName: d.accountHolderName,
number: undefined, // to add/update payment, provide full number.
username: undefined,
maskedNumber: d.cardNumber, // hybris will give back masked number.
expiryMonth: parseInt(d.expiryMonth),
expiryYear: parseInt(d.expiryYear),
ccv: undefined // hybris doesnt have ccv?
}
}
}
/**
* @inheritDoc
*/
parsePaymentMethod(data) {
return {
id: data.id,
name: data.name,
types: data.types.map((types) => ({id: types.code, name: types.name}))
}
}
/**
* Transform an orderAddress into hybris' models: billingAddress or shippingAddress
*/
transformOrderAddress(orderAddress) {
return {
country: {
isocode: orderAddress.countryCode
},
firstName: orderAddress.firstName,
lastName: orderAddress.lastName,
id: orderAddress.id,
line1: orderAddress.addressLine1,
line2: orderAddress.addressLine2,
phone: orderAddress.phone,
postalCode: orderAddress.postalCode,
region: {
isocode: orderAddress.stateCode,
},
titleCode: orderAddress.titleCode,
town: orderAddress.city,
}
}
/**
* Transform a shippingMethod into hybris' model deliveryMethod.
*/
transformShippingMethod(shippingMethod) {
return {
code: shippingMethod.id,
name: shippingMethod.name,
deliveryCost: shippingMethod.cost
}
}
/**
* Transform a cartItem into hybris' model OrderEntryOCC.
*/
transformCartItem(cartItem) {
return {
product: {
code: cartItem.productId
},
quantity: cartItem.quantity
}
}
/**
* Transform a payment into hybris' model paymentDetail.
*/
transformPayment(payment, billingAddress) {
const details = payment.details
return {
id: payment.id,
accountHolderName: details ? details.holderName : undefined,
billingAddress: this.transformOrderAddress(billingAddress),
cardNumber: details ? details.number : undefined,
cardType: {
code: details ? details.type : undefined
},
expiryMonth: details ? details.expiryMonth : undefined,
expiryYear: details ? details.expiryYear : undefined,
subscriptionId: details ? details.username : undefined
}
}
/**
* @inheritDoc
*/
addCartItem(cart, cartItem, opts = {}) {
if (!cart) { throw new errors.InvalidArgumentError(`Parameter 'cart' is required`) }
if (!cartItem) { throw new errors.InvalidArgumentError(`Parameter 'cartItem' is required`) }
const api = new hybris.UsersApi(this.client)
const entryOrder = this.transformCartItem(cartItem, opts)
return api.postCartEntries(cart.customerInfo.id, cart.id, entryOrder, {...opts, fields: 'FULL'})
.then(() => api.getCart(cart.customerInfo.id, cart.id, {...opts, fields: 'FULL'}))
.then((data) => this.parseCart({...data, email: cart.customerInfo.email}))
.catch(() => { throw new errors.ServerError('Could not add item to cart') })
}
/**
* @inheritDoc
*/
removeCartItem(cart, cartItemId, opts = {}) {
if (!cart) { throw new errors.InvalidArgumentError(`Parameter 'cart' is required`) }
if (!cartItemId) { throw new errors.InvalidArgumentError(`Parameter 'cartItemId' is required`) }
const api = new hybris.UsersApi(this.client)
return api.deleteCartEntry(cart.customerInfo.id, cart.id, cartItemId)
.then(() => api.getCart(cart.customerInfo.id, cart.id, {...opts, fields: 'FULL'}))
.then((data) => this.parseCart({...data, email: cart.customerInfo.email}))
.catch(() => { throw new errors.ServerError('Could not delete cart item') })
}
/**
* @inheritDoc
*/
updateCartItem(cart, cartItem, opts = {}) {
if (!cart) { throw new errors.InvalidArgumentError(`Parameter 'cart' is required`) }
if (!cartItem) { throw new errors.InvalidArgumentError(`Parameter 'cartItem' is required`) }
const api = new hybris.UsersApi(this.client)
const entryOrder = this.transformCartItem(cartItem, opts)
return api.patchCartEntry(cart.customerInfo.id, cart.id, cartItem.id, entryOrder, {fields: 'FULL'})
.then(() => api.getCart(cart.customerInfo.id, cart.id, {fields: 'FULL'}))
.then((data) => this.parseCart({...data, email: cart.customerInfo.email}))
.catch(() => { throw new errors.ServerError('Could not update cart item') })
}
/**
* @inheritDoc
* Need to set a billing address before setting a payment.
* Must call setCustomerInformation() to add an email before setting payment for an anonymous user.
* cart.paymentDetails.number will be returned as undefined and set as a masked number in cart.paymentDetails.maskedNumber.
*/
setPayment(cart, payment, opts = {}) {
if (!cart) { throw new errors.InvalidArgumentError(`Parameter 'cart' is required`) }
if (!payment) { throw new errors.InvalidArgumentError(`Parameter 'payment' is required`) }
const api = new hybris.UsersApi(this.client)
if (cart.customerInfo.id === 'anonymous' && cart.customerInfo.email === undefined) {
throw new errors.InvalidArgumentError('Please set email for guest user cart')
}
if (!cart.billingAddress) {
throw new errors.InvalidArgumentError('Please set billing address first')
}
const isMissingFields = !payment.details.type || !payment.details.expiryMonth || !payment.details.expiryYear || !payment.details.holderName || !payment.details.number
if (isMissingFields) { throw new errors.InvalidArgumentError('Missing hybris required fields') }
const paymentDetails = this.transformPayment(payment, cart.billingAddress)
// If the customer is a registered user and a payment already exists on the cart,
// then the payment is has also been added to the user account. So, update the
// corresponding payment on the user account to see the reflected changes on the cart.
const apiCommand = cart.payments.length > 0 && cart.customerInfo.id === 'current' ?
api.putPaymentDetail(cart.customerInfo.id, cart.payments[0].id, paymentDetails)
.then(() => this.getCart(cart.id))
: api.postCartPaymentDetail(cart.customerInfo.id, cart.id, paymentDetails, {...opts, fields: 'FULL'})
.then((data) => {
cart.payments[0] = this.parsePayment({...data, amount: cart.total})
return cart
})
return apiCommand
.catch(() => {
throw new errors.ServerError('Could not set payment')
})
}
/**
* @inheritDoc
* Email can be set for a guest user but not a registered user.
*/
setCustomerInformation(cart, customerInfo, opts = {}) {
if (!cart) { throw new errors.InvalidArgumentError(`Parameter 'cart' is required`) }
if (!customerInfo) { throw new errors.InvalidArgumentError(`Parameter 'customerInformation' is required`) }
const api = new hybris.UsersApi(this.client)
const customerId = cart.customerInfo.id
return api.putCartEmail(customerId, cart.id, {...opts, email: customerInfo.email})
.then(() => {
cart.customerInfo = {...customerInfo, id: customerId}
return cart
})
.catch(() => { throw new errors.ServerError('Could not set customer information') })
}
/**
* @inheritDoc
*/
getShippingMethods(cart, opts = {}) {
if (!cart) { throw new errors.InvalidArgumentError(`Parameter 'cart' is required`) }
const api = new hybris.UsersApi(this.client)
return api.getCartDeliveryModes(cart.customerInfo.id, cart.id, {...opts, fields: 'FULL'})
.then(({deliveryModes}) => deliveryModes.map(this.parseShippingMethod.bind(this)))
}
/**
* @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 hybris.UsersApi(this.client)
return api.putCartDeliveryMode(cart.customerInfo.id, cart.id, shippingMethod.id)
.then(() => {
cart.selectedShippingMethodId = shippingMethod.id
return cart
})
.catch(() => { throw new errors.ServerError('Could not set shipping method') })
}
/**
* @inheritDoc
* Billing Address model : this order address needs to have a titleCode defined.
* The default titleCodes are: ['mr', 'ms', 'miss', 'mrs', 'dr', 'rev']
* But these may change depending on your backend, please refer to your backend instance to confirm valid titleCodes.
*
* If the billing address is set before adding a payment, the billing address will not yet be saved in the hybris instance.
* Instead, the billing address will be saved in the hybris instance upon setting a payment.
*/
setBillingAddress(cart, billingAddress, opts = {}) {
if (!cart) { throw new errors.InvalidArgumentError(`Parameter 'cart' is required`) }
if (!billingAddress) { throw new errors.InvalidArgumentError(`Parameter 'billingAddress' is required`) }
const isMissingFields = !billingAddress.countryCode || !billingAddress.addressLine1 || !billingAddress.stateCode ||
!billingAddress.titleCode || !billingAddress.postalCode || !billingAddress.city || !billingAddress.firstName || !billingAddress.lastName
if (isMissingFields) { throw new errors.InvalidArgumentError('Missing hybris required fields') }
// if the cart does not have a payment yet, give back a cart with the billing address set.
// else, there a payment already exists and it's billing address needs to be updated
if (cart.payments.length <= 0) {
cart.billingAddress = billingAddress
return Promise.resolve(cart)
}
const api = new hybris.UsersApi(this.client)
// For a registered user, the existing payment can be patched to update it's billing address
if (cart.customerInfo.id === 'current') {
return api.patchPaymentDetail(cart.customerInfo.id, cart.payments[0].id, {billingAddress: this.transformOrderAddress(billingAddress)})
.then(() => {
cart.billingAddress = billingAddress
return cart
})
.catch(() => { throw new errors.ServerError('Could not set billing address') })
}
// For a guest user, the existing payment can only be updated if the payment detail number is re-injected.
if (cart.payments[0].details.number === undefined) {
throw new errors.InvalidArgumentError('As a guest user, please re-inject payment details number before updating billing address')
}
return api.postCartPaymentDetail(cart.customerInfo.id, cart.id, this.transformPayment(cart.payments[0], billingAddress), {...opts, fields: 'FULL'})
.then((data) => {
cart.billingAddress = billingAddress
cart.payments[0] = this.parsePayment({...data, amount: cart.total})
return cart
})
.catch(() => {
throw new errors.ServerError('Could not set billing address')
})
}
/**
* @inheritDoc
* Must call setCustomerInformation() to add an email before setting payment for a guest user.
*/
setShippingAddress(cart, shippingAddress, opts = {}) {
if (!cart) { throw new errors.InvalidArgumentError(`Parameter 'cart' is required`) }
if (!shippingAddress) { throw new errors.InvalidArgumentError(`Parameter 'shippingAddress' is required`) }
if (cart.customerInfo.id === 'anonymous' && cart.customerInfo.email === undefined) {
throw new errors.InvalidArgumentError('Please set email for guest user cart')
}
const isMissingFields = !shippingAddress.firstName || !shippingAddress.lastName || !shippingAddress.addressLine1 || !shippingAddress.city ||
!shippingAddress.countryCode || !shippingAddress.stateCode || !shippingAddress.postalCode
if (isMissingFields) { throw new errors.InvalidArgumentError('Missing hybris required fields') }
const api = new hybris.UsersApi(this.client)
// For a registered user, api.postCartDeliveryAddress() returns an unauthorized error (unknown reason)
// A workaround: post or put (if already exists a shipping address) the user address and then put that address to the cart by its id.
let apiCommand
if (cart.customerInfo.id === 'current') {
const userAddressApiCommand = cart.shippingAddress === undefined ?
api.postUserAddress(cart.customerInfo.id, this.transformOrderAddress(shippingAddress), {fields: 'FULL'})
: api.putUserAddress(cart.customerInfo.id, shippingAddress.id, {...this.transformOrderAddress(shippingAddress), id: shippingAddress.id})
apiCommand = userAddressApiCommand
.then((data) => {
return api.putCartDeliveryAddress(cart.customerInfo.id, cart.id, data.id)
})
} else {
apiCommand = api.postCartDeliveryAddress(cart.customerInfo.id, cart.id, this.transformOrderAddress(shippingAddress), {...opts, fields: 'FULL'})
}
return apiCommand
.then(() => {
cart.shippingAddress = shippingAddress
return cart
})
.catch(() => {
throw new errors.ServerError('Could not set shipping address')
})
}
/**
* @inheritDoc
* Hybris only supports payments by credit card attained from their cardtypes endpoint
* so this command will return only one payment method with the id 'CREDIT_CARD', name 'Credit Card'
* and all of it's valid credit card types.
*/
getPaymentMethods(cart, opts = {}) {
const api = new hybris.CardtypesApi(this.client)
return api.getCardTypes({...opts, fields: 'FULL'})
.then((data) => [this.parsePaymentMethod({id: 'CREDIT_CARD', name: 'Credit Card', types: data.cardTypes})])
.catch(() => { throw new errors.ServerError('Could not get payment methods') })
}
}