"use strict";

var _interopRequireWildcard = require("@babel/runtime/helpers/interopRequireWildcard");

var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");

Object.defineProperty(exports, "__esModule", {
  value: true
});
exports.HybrisConnector = void 0;

var _objectWithoutProperties2 = _interopRequireDefault(require("@babel/runtime/helpers/objectWithoutProperties"));

var _defineProperty2 = _interopRequireDefault(require("@babel/runtime/helpers/defineProperty"));

var _extends2 = _interopRequireDefault(require("@babel/runtime/helpers/extends"));

var hybris = _interopRequireWildcard(require("hybris-occ-client"));

var errors = _interopRequireWildcard(require("../errors"));

var _superagent = _interopRequireDefault(require("superagent"));

var _urlParse = _interopRequireDefault(require("url-parse"));

function ownKeys(object, enumerableOnly) { var keys = Object.keys(object); if (Object.getOwnPropertySymbols) { var symbols = Object.getOwnPropertySymbols(object); if (enumerableOnly) symbols = symbols.filter(function (sym) { return Object.getOwnPropertyDescriptor(object, sym).enumerable; }); keys.push.apply(keys, symbols); } return keys; }

function _objectSpread(target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i] != null ? arguments[i] : {}; if (i % 2) { ownKeys(source, true).forEach(function (key) { (0, _defineProperty2.default)(target, key, source[key]); }); } else if (Object.getOwnPropertyDescriptors) { Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)); } else { ownKeys(source).forEach(function (key) { Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key)); }); } } return target; }

const clone = x => JSON.parse(JSON.stringify(x));
/**
 * A Commerce Integrations connector for SAP Hybris Commerce backends.
 *
 * @implements {module:@mobify/commerce-integrations/dist/connectors/interfaces.CommerceConnector}
 * @implements {module:@mobify/commerce-integrations/dist/connectors/interfaces.ParserHooks}
 */


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
    });
  }

  absoluteURL(path) {
    return new _urlParse.default(path, this.client.basePath || '').href;
  }
  /**
   * Register a new customer account and login.
   *
   * @param {module:@mobify/commerce-integrations/dist/types.HybrisCustomerRegistration} data
   * @return {Promise<module:@mobify/commerce-integrations/dist/types.Customer>}
   */


  registerCustomer(data) {
    const api = new hybris.UsersApi(this.client);
    return api.postUsers({
      firstName: data.firstName,
      lastName: data.lastName,
      password: data.password,
      titleCode: data.titleCode,
      uid: data.email // Must be an email

    }).then(() => this.login(data.email, data.password));
  }
  /**
   * @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) {
      (0, _extends2.default)(body, {
        grant_type: 'password',
        username,
        password
      });
    }

    return _superagent.default.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
      };
      (0, _extends2.default)(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
    };
    (0, _extends2.default)(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.default.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
      };
      (0, _extends2.default)(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 (0, _extends2.default)({}, this.client.defaultHeaders);
  }
  /**
   * @inheritDoc
   */


  setDefaultHeaders(headers) {
    this.client.defaultHeaders = (0, _extends2.default)({}, headers);
  }
  /**
   * Get the current authentications object, which can be used to
   * save and restore auth state.
   *
   * @return {Object}
   */


  getAuthentications() {
    return clone(this.client.authentications);
  }
  /**
   * Set the current authentications object, as returned from getAuthentications.
   * Use to save and restore auth state.
   *
   * @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, _objectSpread({
      fields: 'FULL'
    }, opts)).then(res => this.parseCustomer(res));
  }
  /**
   * @inheritDoc
   */


  parseCustomer(data) {
    // Hybris validates that uid is an email for registered users.
    const d = data;
    const isAnon = d.uid === 'anonymous';
    const anonEmail = d.defaultAddress ? d.defaultAddress.email : undefined;
    return {
      id: isAnon ? 'anonymous' : 'current',
      firstName: d.firstName,
      lastName: d.lastName,
      email: isAnon ? anonEmail : d.uid
    };
  }
  /**
   * @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 {
      query,
      count = 20,
      start = 0,
      sort = 'relevance',
      filters = {}
    } = params; // Please refer to Hybris documentation for notes on the shape of the request object.
    // https://help.hybris.com/6.4.0/api/ycommercewebservices/main.html#_products_search_get

    return {
      query: [].concat(...[query ? [`${query}:`] : [], ['', sort], Object.keys(filters).filter(key => !['categoryId', 'query'].includes(key)).filter(key => Boolean(filters[key])).map(key => `${key}:${filters[key]}`), filters.categoryId ? [`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,
      freeTextSearch,
      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((_ref) => {
        let {
          stock = {},
          images = []
        } = _ref,
            product = (0, _objectWithoutProperties2.default)(_ref, ["stock", "images"]);
        return {
          available: stock.stockLevelStatus === 'inStock',
          productId: product.code,
          productName: product.name,
          link: product.url,
          defaultImage: images[0] && {
            alt: product.name,
            src: this.absoluteURL(images[0].url)
          },
          price: product.price.value
        };
      }),
      query: freeTextSearch,
      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 OCC API Category objects and parses it into an
   * commerce-integrations Category type.
   * @param {Object[]} categories an array of OCC API {@link https://mobify.github.io/hybris-occ-client/module-models_CategoryHierarchy.html|Category} document
   *
   * @returns {Promise<module:@mobify/commerce-integrations/dist/types.Category>}
   */

  /* eslint-disable camelcase */


  parseCategories(categories) {
    const parsedCategories = [];
    categories.forEach(category => parsedCategories.push(this.parseCategory(category)));
    return parsedCategories;
  }
  /**
   * Takes an OCC API Category objects and parses it into an
   * commerce-integrations Category type.
   * @param {Object[]} categories An OCC API {@link https://mobify.github.io/hybris-occ-client/module-models_CategoryHierarchy.html|Category} document
   *
   * @returns {Promise<module:@mobify/commerce-integrations/dist/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 = _objectSpread({}, 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
      }) => _objectSpread({}, acc, {
        [qualifier]: value
      }), {}); // Set the products values.

      parsedProduct.variationValues = variationValues;
      return parsedProduct;
    }).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 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 an OCC API ImageGroup object and parses it into a commerce-integrations ImageSet type.
   * @param {Object} data An OCC API {@link https://documentation.demandware.com/DOC1/topic/com.demandware.dochelp/OCC API/18.3/shop/Documents/ImageGroup.html|ImageGroup} document
   *
   * @returns {Promise<module:@mobify/commerce-integrations/dist/types.ImageSet>}
   */

  /* eslint-disable camelcase */


  parseImageSets(variation) {
    const {
      images,
      variantOptions
    } = variation;
    const {
      variantOptionQualifiers
    } = variantOptions[0];
    const variationProperties = variantOptionQualifiers.map(({
      qualifier,
      name,
      value
    }) => ({
      id: qualifier,
      label: 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: this.absoluteURL(url),
        title: altText
      })),
      variationProperties,
      sizeType: type
    }));
  }
  /* eslint-enable camelcase */

  /**
   * Takes an OCC Product object and parses it into a commerce-integrations Product type.
   * @param {Object} data An OCC product document
   *
   * @returns {Promise<module:@mobify/commerce-integrations/dist/types.Product>}
   */

  /* eslint-disable camelcase */


  parseProduct({
    code,
    name,
    description,
    categories,
    price,
    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(({
        sizeType
      }) => sizeType === 'product');
      const filteredThumbnailImageSets = variationImageSets.filter(({
        sizeType
      }) => sizeType === '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: value,
            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(_objectSpread({}, data, {
        id
      }));
    });
  }
  /**
   * Takes an OCC API WeekdayOpeningDay object and parses it into a commerce-integrations Store.hours type.
   * @param {Object} hours An OCC API {@link https://help.hybris.com/6.7.0/api/ycommercewebservices/index.html#_weekdayopeningdaywsdto|WeekdayOpneningDay} document
   *
   * @returns {Promise<module:@mobify/commerce-integrations/dist/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 an OCC API PointOfService object and parses it into a commerce-integrations Store type.
   * @param {Object} data An OCC API {@link https://help.hybris.com/6.7.0/api/ycommercewebservices/index.html#_pointofservicewsdto|PointOfService} document
   *
   * @returns {Promise<module:@mobify/commerce-integrations/dist/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: this.absoluteURL(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
   */
  // 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 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(_objectSpread({}, 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'
   *
   * @private
   */


  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, _objectSpread({
      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, _objectSpread({
      fields: 'FULL'
    }, opts)).then(data => this.parseCart(data)).catch(() => {
      throw new errors.ServerError('Could not get cart');
    });
  }
  /**
   * @inheritDoc
   */
  // eslint-disable-next-line no-unused-vars


  deleteCart(cartId, opts = {}) {
    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');
    });
  }
  /**
   * Parse a coupon item
   */


  parseCouponEntry(data) {
    return {
      id: data.code,
      code: data.code
    };
  }
  /**
   * @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;

    const parseNumberValue = price => {
      return price ? price.value : undefined;
    };

    const parsedDiscounts = parseNumberValue(d.totalDiscounts);
    const discounts = parsedDiscounts === 0 ? parsedDiscounts : Math.abs(parsedDiscounts) * -1; // Our interface requires a negative value

    return {
      id: cartId,
      couponEntries: d.appliedVouchers ? d.appliedVouchers.map(this.parseCouponEntry) : [],
      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,
      selectedShippingMethodId: d.deliveryMode ? d.deliveryMode.code : undefined,
      payments: d.paymentInfo ? [this.parsePayment(_objectSpread({}, d.paymentInfo, {
        amount: d.totalPriceWithTax.value
      }))] : [],
      subtotal: parseNumberValue(d.subTotal),
      shipping: parseNumberValue(d.deliveryCost),
      discounts,
      tax: parseNumberValue(d.totalTax),
      total: parseNumberValue(d.totalPriceWithTax)
    };
  }
  /**
   * @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,
      // BUG: fixed in hybris 6.5, phone field is not saved and returned
      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, _objectSpread({}, opts, {
      fields: 'FULL'
    })).then(() => api.getCart(cart.customerInfo.id, cart.id, _objectSpread({}, opts, {
      fields: 'FULL'
    }))).then(data => {
      const updatedCart = this.parseCart(_objectSpread({}, data, {
        email: cart.customerInfo.email
      })); // BUG: does not save billing address region when sending billing address through a POST cart payment detail.
      // So lets persist this field on the incoming cart parameter to the returned order.

      if (updatedCart.billingAddress && !updatedCart.billingAddress.stateCode && cart.billingAddress.stateCode) {
        updatedCart.billingAddress.stateCode = cart.billingAddress.stateCode;
      }

      return updatedCart;
    }).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, _objectSpread({}, opts, {
      fields: 'FULL'
    }))).then(data => {
      const updatedCart = this.parseCart(_objectSpread({}, data, {
        email: cart.customerInfo.email
      })); // BUG: does not save billing address region when sending billing address through a POST cart payment detail.
      // So lets persist this field on the incoming cart parameter to the returned order.

      if (updatedCart.billingAddress && !updatedCart.billingAddress.stateCode && cart.billingAddress.stateCode) {
        updatedCart.billingAddress.stateCode = cart.billingAddress.stateCode;
      }

      return updatedCart;
    }).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 => {
      const updatedCart = this.parseCart(_objectSpread({}, data, {
        email: cart.customerInfo.email
      })); // BUG: does not save billing address region when sending billing address through a POST cart payment detail.
      // So lets persist this field on the incoming cart parameter to the returned order.

      if (updatedCart.billingAddress && !updatedCart.billingAddress.stateCode && cart.billingAddress.stateCode) {
        updatedCart.billingAddress.stateCode = cart.billingAddress.stateCode;
      }

      return updatedCart;
    }).catch(() => {
      throw new errors.ServerError('Could not update cart item');
    });
  }
  /**
   * Add a supported payment type to the cart
   * @param {module:@mobify/commerce-integrations/dist/types.Cart} cart The cart to add the payment to
   * @param {module:@mobify/commerce-integrations/dist/types.Payment} payment The payment to add
   * @param {Object} opts
   * @return {Promise<module:@mobify/commerce-integrations/dist/types.Cart>}
   *
   * Need to set a billing address before setting a payment.
   * Must call setCustomerInformation() to add an email before setting payment for an anonymous cart.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, _objectSpread({}, opts, {
      fields: 'FULL'
    })).then(data => {
      cart.payments[0] = this.parsePayment(_objectSpread({}, data, {
        amount: cart.total
      }));
      return cart;
    });
    return apiCommand.catch(() => {
      throw new errors.ServerError('Could not set payment');
    });
  }
  /**
   * Set the customer information.
   * @param {module:@mobify/commerce-integrations/dist/types.Cart} cart The customer's cart
   * @param {module:@mobify/commerce-integrations/dist/types.CustomerInformation} customerInformation The new or modified customer information
   * @param {Object} opts
   * @return {Promise<module:@mobify/commerce-integrations/dist/types.Cart>}
   *
   * 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, _objectSpread({}, opts, {
      email: customerInfo.email
    })).then(() => {
      cart.customerInfo = _objectSpread({}, 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, _objectSpread({}, 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');
    });
  }
  /**
   * Set the billing address on the cart.
   *
   * 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.
   * @param {module:@mobify/commerce-integrations/dist/types.Cart} cart The cart to update the billing address for
   * @param {module:@mobify/commerce-integrations/dist/types.OrderAddress} billingAddress The new or modified address
   * @param {Object} opts
   * @return {Promise<module:@mobify/commerce-integrations/dist/types.Cart>}
   */


  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), _objectSpread({}, opts, {
      fields: 'FULL'
    })).then(data => {
      cart.billingAddress = billingAddress;
      cart.payments[0] = this.parsePayment(_objectSpread({}, data, {
        amount: cart.total
      }));
      return cart;
    }).catch(() => {
      throw new errors.ServerError('Could not set billing address');
    });
  }
  /**
   * Set the shipping method for the cart.
   * @param {module:@mobify/commerce-integrations/dist/types.Cart} cart The cart to set the shipping method for
   * @param {module:@mobify/commerce-integrations/dist/types.ShippingMethod} shippingMethod The shipping method to setid
   * @param {Object} opts
   * @return {Promise<module:@mobify/commerce-integrations/dist/types.Cart>}
   *
   * 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, _objectSpread({}, 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), _objectSpread({}, opts, {
        fields: 'FULL'
      }));
    }

    return apiCommand.then(() => {
      cart.shippingAddress = shippingAddress;
      return cart;
    }).catch(() => {
      throw new errors.ServerError('Could not set shipping address');
    });
  }
  /**
   * Get the available payment methods for the cart
   * @param {module:@mobify/commerce-integrations/dist/types.Cart} cart The customer's cart
   * @param {Object} opts
   * @return {Promise<Array.module:@mobify/commerce-integrations/dist/types.PaymentMethod>}
   *
   * 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(_objectSpread({}, 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');
    });
  }
  /**
   * @inheritDoc
   */


  parseOrder(data) {
    const order = this.parseCart(data);
    return _objectSpread({}, order, {
      id: data.code,
      creationDate: new Date(data.created),
      status: data.statusDisplay,
      customerInfo: {
        id: data.guestCustomer ? 'anonymous' : 'current',
        email: data.paymentInfo.billingAddress.email
      }
    });
  }
  /**
   * @inheritDoc
   */


  getOrder(id, opts = {}) {
    if (!id) {
      throw new errors.InvalidArgumentError(`Parameter 'id' is required`);
    }

    let getOrderPromise; // BUG: does not save billing address region when sending billing address through a POST cart payment detail.
    // So when retrieving an order by id, region (stateCode field) may be missing in the returned order.

    if (this.getCustomerIdFromAuth() === 'current') {
      const api = new hybris.UsersApi(this.client);
      getOrderPromise = api.getOrder('current', id, _objectSpread({}, opts, {
        fields: 'FULL'
      }));
    } else {
      const api = new hybris.OrdersApi(this.client);
      getOrderPromise = api.getOrder(id, _objectSpread({}, opts, {
        fields: 'FULL'
      }));
    }

    return getOrderPromise.then(data => this.parseOrder(data)).catch(() => {
      throw new errors.ServerError('Order not found');
    });
  }
  /**
   * @inheritDoc
   */


  getOrders(ids, opts = {}) {
    if (!ids || ids.length <= 0) {
      throw new errors.InvalidArgumentError('Please specify list of order ids to get.');
    }

    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
   */


  createOrder(cart, opts = {}) {
    const api = new hybris.UsersApi(this.client);
    return api.postOrder(cart.customerInfo.id, cart.id, _objectSpread({}, opts, {
      fields: 'FULL'
    })).then(data => {
      const order = this.parseOrder(data); // BUG: does not save billing address region when sending billing address through a POST cart payment detail.
      // So lets persist this field on the incoming cart parameter to the returned order.

      if (order.billingAddress && !order.billingAddress.stateCode) {
        order.billingAddress.stateCode = cart.billingAddress.stateCode;
      }

      return order;
    }).catch(() => {
      throw new errors.ServerError('Could not create order');
    });
  }
  /**
   * @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 hybris.UsersApi(this.client);
    return api.postCartVoucher(cart.customerInfo.id, cart.id, 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`);
    }

    const api = new hybris.UsersApi(this.client);
    return api.deleteCartVouchers(cart.customerInfo.id, cart.id, couponEntryId).then(() => this.getCart(cart.id)).catch(() => {
      throw new errors.ServerError('Could Not Remove Coupon');
    });
  }

}

exports.HybrisConnector = HybrisConnector;