Source: app/actions/resources/shopActions.js

/* global WP_DEFINE_IS_NODE */
import debugLib from 'debug';
import { formValueSelector, change } from 'redux-form';
import createAction from 'redux-actions/lib/createAction';
import Configuration from 'common/src/app/config/Configuration';
import { makeCollectionSelector } from '../../selectors/collectionSelector';
import {
  COUNTRY_REGIONS,
  productCollectionId,
  packageCollectionId,
} from '../../data/collectionIds';
import authenticate from '../../util/auth/authenticate';
import ProductType from '../../data/enum/ProductType';
import { setEntity } from '../entities/entityActions';
import { getAccount, updateAccount } from './accountActions';
import FormNames from '../../data/enum/FormNames';
import BasketFields from '../../data/enum/FieldNames/BasketFields';
import HifiRequirements from '../../data/enum/HifiRequirements';
import { userAccountSelector, userIdSelector } from '../../selectors/userAccountSelectors';
import { createCountryRegionSelector } from '../../selectors/shopSelectors';
import { regionCode as region } from '../../data/enum/Regions';
import { GATEWAY_SHOP, GATEWAY_SHOP_AUTH } from '../../data/Injectables';
import {
  PRODUCT,
  SHIPPING_COST,
  COUNTRY_REGION,
  PACKAGE,
  PURCHASE,
  DISCOUNT_ITEM,
} from '../../data/entityTypes';
import dedupeAsyncThunk from '../../util/dedupeAsyncThunk';
import {
  clearValidation,
  HANDLE_SUBMIT_ERRORS,
} from '../../enhanced-redux-form/actions/enhancedFormActions';
import { BASKET_CANNOT_GET_SHIPPING } from '../../data/validationErrorCodes';
import { hasFlag } from '../../util/bitwiseUtils';
import AccountState from '../../data/enum/AccountState';
import ProductCategory from '../../data/enum/ProductCategory';
import { shopFields } from '../../data/enum/FieldNames/AccountFieldNames';
import {
  discountItemSelector,
  makePackagesWithDiscountSelector,
  voucherSelector,
} from '../../selectors/packageVoucherSelectors';
import { apiGet, apiPatch, apiPost } from './apiActions/apiRequest';
import apiGetEntity from './apiActions/apiGetEntity';
import apiGetCollection, { collectionCachingNoPagination } from './apiActions/apiGetCollection';

export const GET_PRODUCT_DETAIL = 'shopActions/GET_PRODUCT_DETAIL';
export const GET_PRODUCTS = 'shopActions/GET_PRODUCTS';
export const GET_BASKET_CONTENT = 'shopActions/GET_BASKET_CONTENT';
export const GET_SHIPPING_COSTS = 'shopActions/GET_SHIPPING_COSTS';
export const GET_ORDER_LIMITS = 'shopActions/GET_ORDER_LIMITS';
export const REQUEST_NOTIFICATION = 'shopActions/REQUEST_NOTIFICATION';

const basketValueSelector = formValueSelector(FormNames.SHOP_BASKET);
const debug = debugLib('SlimmingWorld:shopActions');

const DEFAULT_LIMIT = 20;

let btoaNode = null;
if (WP_DEFINE_IS_NODE) {
  btoaNode = require('btoa'); // eslint-disable-line global-require
}

/**
 * Encodes basket content object to Base64 string
 * @param itemsDictionary
 */
const encodeBase64 = itemsDictionary =>
  (btoaNode || btoa)(
    typeof itemsDictionary === 'string' ? itemsDictionary : JSON.stringify(itemsDictionary),
  );

export const getProducts = (limit = DEFAULT_LIMIT, category, regionCode) => (
  dispatch,
  getState,
) => {
  const state = getState();
  const account = userAccountSelector(state);
  const accountState = getState().authentication?.userInfo?.account_state; // eslint-disable-line camelcase
  const hasValidSubscription =
    account &&
    (hasFlag(accountState, AccountState.ONLINE_SUBSCRIPTION_VALID) ||
      hasFlag(accountState, AccountState.GROUP_MEMBERSHIP_VALID));

  const isGroupMember =
    account && hasFlag(account.accountState, AccountState.GROUP_MEMBERSHIP_VALID);

  return dispatch(
    apiGetCollection(
      GET_PRODUCTS,
      GATEWAY_SHOP_AUTH,
      isGroupMember ? '/products/catalogue' : '/products',
      productCollectionId({ limit, category, regionCode }),
      {
        limit,
        category: hasValidSubscription ? category : ProductCategory.SUBSCRIPTION,
        regionCode,
      },
      {
        requestData: {
          category: hasValidSubscription ? category : ProductCategory.SUBSCRIPTION,
          regionCode,
        },
        entityType: PRODUCT,
        caching: collectionCachingNoPagination,
      },
    ),
  ).catch(error => {
    debug(error);
  });
};

export const getProductDetail = dedupeAsyncThunk(
  (productId, updateDetailView = true, productInfo, hiFiNoIngredients, isGroupMember) => {
    const getFresh = () => {
      /**
       * If the product is not already in state do the product detail call to get all info (mostly used when hitting the page directly)
       */
      if (productInfo === null) {
        return true;
      }

      /**
       * If we have already tried to get hifi ingredients before and there where
       * not any do not fire the call again
       */
      if (hiFiNoIngredients[productInfo.id]) {
        return false;
      }

      /**
       * If the product is a hifi and does not have ingredients & allergens
       * fire the product detail call to get them
       */
      return (
        productInfo.category === ProductType.HIFI &&
        (!productInfo.ingredients || !productInfo.allergens)
      );
    };

    return apiGetEntity(
      GET_PRODUCT_DETAIL,
      GATEWAY_SHOP_AUTH,
      isGroupMember ? `/products/catalogue/${productId}` : `/products/${productId}`,
      PRODUCT,
      productId,
      updateDetailView
        ? {
            caching: getFresh() ? false : undefined,
            updateEntityView: 'view.pages.productDetail.product',
          }
        : {},
    );
  },
  true,
);

/**
 * Validates that the products in the basket are up to date.
 */
export const validateBasket = () => (dispatch, getState) => {
  const state = getState();
  const userId = userIdSelector(state);
  const basketItems = basketValueSelector(state, BasketFields.ITEMS);
  const { currentDeliveryRegion } = state.view.pages.shop || {};
  const itemsDictionary = basketItems.reduce((acc, item) => {
    acc[item.id] = item.quantity; // eslint-disable-line no-param-reassign
    return acc;
  }, {});

  if (basketItems.length === 0) {
    return undefined;
  }

  // Let's pass the voucherCode to get the discounted price
  const voucherCodeObject = voucherSelector(
    state,
    basketValueSelector(state, shopFields.VOUCHER_CODE),
  );
  const voucherCode = voucherCodeObject?.code;

  dispatch(clearValidation(FormNames.SHOP_BASKET));
  return Promise.all(basketItems.map(({ id }) => dispatch(getProductDetail(id, false, null))))
    .then(() =>
      dispatch(
        apiGet(
          GET_BASKET_CONTENT,
          GATEWAY_SHOP_AUTH,
          `/baskets/${encodeBase64(itemsDictionary)}`,
          {
            userId,
            voucherCode,
          },
          {
            credentials: 'include',
          },
        ),
      ),
    )
    .then(({ items, shippingCost, discountItem, voucherAvailable }) => {
      const products = state.entities[PRODUCT];

      products &&
        items.forEach(item => {
          const currentProduct = state.entities[PRODUCT][item.productId];
          if (currentProduct.price.amount !== item.itemPrice) {
            debug(
              `product price outdated for product id ${item.productId}. Updating ${currentProduct.price.amount}=>${item.itemPrice}`,
            );
            dispatch(
              setEntity(
                PRODUCT,
                item.productId,
                {
                  price: {
                    amount: item.itemPrice,
                    currencyCode: item.currencyCode,
                  },
                  quantity: item.quantity,
                },
                true,
              ),
            );
          }
        });

      if (discountItem) {
        dispatch(setEntity(DISCOUNT_ITEM, voucherCode, discountItem));
      } else {
        // This boolean is used for showing the fallback copy
        dispatch(setEntity(DISCOUNT_ITEM, voucherCode, { hasItems: false }));
      }

      dispatch(setIsVoucherAvailable(voucherAvailable));

      if (shippingCost) {
        dispatch(setEntity(SHIPPING_COST, 0, shippingCost));
      } else {
        dispatch({
          type: HANDLE_SUBMIT_ERRORS,
          payload: {
            errors: {},
            generalError: { locale: 'basket.errors.shipping', code: BASKET_CANNOT_GET_SHIPPING },
          },
          meta: { form: FormNames.SHOP_BASKET },
        });
      }
    })
    .catch(e => {
      // eslint-disable-line consistent-return
      if (e.error && e.error.code && e.error.code === 'out-of-stock') {
        dispatch({
          type: HANDLE_SUBMIT_ERRORS,
          payload: {
            errors: {},
            generalError: { locale: 'basket.errors.outOfStock' },
          },
          meta: { form: FormNames.SHOP_BASKET },
        });
        dispatch(change(FormNames.SHOP_BASKET, BasketFields.ITEMS, []));
        return undefined;
      }

      if (e.error && e.error.code && e.error.code === 'quantity-overflow') {
        // some item has a larger quantity then the order limit. Get new order limits and
        // update the item quantities
        return dispatch(
          getUserShoppingDetails(
            basketItems.map(item => item.id),
            true,
            DEFAULT_LIMIT,
            null,
            currentDeliveryRegion,
          ),
        ).then(() => {
          basketItems.forEach(({ id, quantity }, index) => {
            const productEntity = getState().entities[PRODUCT][id];
            if (quantity > productEntity.personalOrderLimit) {
              dispatch(
                change(
                  FormNames.SHOP_BASKET,
                  `items[${index}].quantity`,
                  productEntity.personalOrderLimit,
                ),
              );
            }
          });
        });
      }

      // unknown error. empty the basket and set a general error
      dispatch({
        type: HANDLE_SUBMIT_ERRORS,
        payload: {
          errors: {},
          generalError: { locale: 'basket.errors.general' },
        },
        meta: { form: FormNames.SHOP_BASKET },
      });

      dispatch(change(FormNames.SHOP_BASKET, BasketFields.ITEMS, []));
      return undefined;
    });
};

export const GET_PURCHASE = 'shopActions/GET_PURCHASE';

export const getPurchase = (id, fullSnapshot = false) => dispatch =>
  dispatch(
    apiGetEntity(GET_PURCHASE, GATEWAY_SHOP_AUTH, `/purchases/${id}`, PURCHASE, id, {
      requestData: {
        fullSnapshot,
      },
    }),
  );

export const getPurchaseDetail = ({
  firstName,
  lastName,
  phoneNumber,
  addressLine1,
  addressLine2,
  addressLine3,
  city,
  state: countryState,
  county,
  zip,
  country,
}) => {
  const address = {
    addressLine1,
    addressLine2,
    addressLine3,
    city,
    state: countryState,
    county,
    zip,
    country,
  };

  return {
    personalDetails: {
      firstName,
      lastName,
      phoneNumber,
    },
    shippingAddress: address,
    billingAddress: address,
  };
};

export const createPurchaseShop = purchaseDetail => (dispatch, getState) => {
  const state = getState();
  const items = basketValueSelector(state, BasketFields.ITEMS);

  return dispatch(createPurchase(purchaseDetail, items));
};

const CREATE_PACKAGE_PURCHASE = 'shopActions/CREATE_PACKAGE_PURCHASE';

/**
 * Create a purchase for the given package id. A package contains multiple products.
 *
 * @param {object} purchaseDetail
 * @param {string} packageId
 * @param {string} voucherCode
 */
export const createPackagePurchase = (_purchaseDetail, packageId, voucherCode) => (
  dispatch,
  getState,
) =>
  dispatch(getAccount()).then(() => {
    const state = getState();
    const account = userAccountSelector(state);
    const purchaseDetail = _purchaseDetail || getPurchaseDetail(account);

    return dispatch(
      apiPost(
        CREATE_PACKAGE_PURCHASE,
        GATEWAY_SHOP_AUTH,
        `/packages/${packageId}/purchase`,
        {
          purchaseDetail,
          voucherCode,
        },
        { credentials: 'include' },
      ),
    );
  });

/**
 * TODO: rename function to createProductPurchase
 * TODO: rename second argument from selectedPackage to product, this should then represent a single item. This will make this funciton reusable for any product
 *
 * @param _purchaseDetail
 * @param selectedPackage
 * @param voucherCode
 *
 * @returns {Promise}
 */
export const createPurchaseSubscription = (_purchaseDetail, selectedPackage) => (
  dispatch,
  getState,
) => {
  const state = getState();
  const account = userAccountSelector(state);
  const purchaseDetail = _purchaseDetail || getPurchaseDetail(account);

  const items = [{ id: selectedPackage, quantity: 1 }];

  return dispatch(createPurchase(purchaseDetail, items));
};

const createPurchase = (purchaseDetail, items) => (dispatch, getState) => {
  const state = getState();
  const itemsDictionary = items.reduce((acc, item) => {
    acc[item.id] = item.quantity; // eslint-disable-line no-param-reassign
    return acc;
  }, {});

  const voucherCodeObject = voucherSelector(
    state,
    basketValueSelector(state, shopFields.VOUCHER_CODE),
  );
  const voucherCode = voucherCodeObject?.code || null;
  const discountItem = discountItemSelector(state, voucherCode);
  const discountAmount = discountItem?.discountAmount || null;

  return dispatch(
    apiPost(
      GET_BASKET_CONTENT,
      GATEWAY_SHOP_AUTH,
      `/baskets/${encodeBase64(itemsDictionary)}/purchase`,
      {
        purchaseDetail,
        basket: {
          items: items.map(item => {
            const productEntity =
              state.entities[PRODUCT]?.[item.id] || state.entities.subscriptionPackage?.[item.id];

            return {
              productId: item.id,
              itemPrice: productEntity.price.amount,
              price: productEntity.price.amount * item.quantity,
              currencyCode: productEntity.price.currencyCode,
              quantity: item.quantity,
            };
          }),
          discountAmount,
        },
        voucherCode,
      },
      { credentials: 'include' },
    ),
  );
};

export const getUserShoppingDetails = (
  productIds,
  noCache = false,
  limit = 20,
  category,
  regionCode,
) => (dispatch, getState) => {
  const state = getState();
  const userId = userAccountSelector(state).id;
  const account = userAccountSelector(state);
  const collectionSelector = makeCollectionSelector();

  const hasValidSubscription =
    account &&
    (hasFlag(account.accountState, AccountState.ONLINE_SUBSCRIPTION_VALID) ||
      hasFlag(account.accountState, AccountState.GROUP_MEMBERSHIP_VALID));

  const products = collectionSelector(state, {
    collectionId: productCollectionId({
      limit,
      category: hasValidSubscription ? category : ProductCategory.SUBSCRIPTION,
      regionCode,
    }),
  }).entityRefs;
  const targetProductIds = productIds || products.map(ref => ref.id);
  const requestProductIds = noCache
    ? targetProductIds
    : targetProductIds.filter(id => {
        const product = products.find(p => p.id === id);
        if (!product) {
          return true;
        }

        return typeof product.data.personalOrderLimit !== 'number';
      });

  targetProductIds.filter(id => {
    const product = products.find(p => p.id === id);
    return product ? typeof product.data.personalOrderLimit !== 'number' : true;
  });

  if (!requestProductIds.length) {
    return Promise.resolve();
  }

  return dispatch(
    apiGet(GET_ORDER_LIMITS, GATEWAY_SHOP_AUTH, '/user-shopping-details', {
      userId,
      productIds: requestProductIds.join(','),
    }),
  ).then(({ data: result = [] }) =>
    result.forEach(({ productId, personalOrderLimit, states }) =>
      dispatch(
        setEntity(
          PRODUCT,
          productId,
          { personalOrderLimit, hasRequestedNotification: states === 1 },
          true,
        ),
      ),
    ),
  );
};

export const requestNotification = id => (dispatch, getState) => {
  const userId = userIdSelector(getState());

  // optimistic update: set hasRequestedNotification to make CartButton respond immediately
  dispatch(setEntity(PRODUCT, id, { hasRequestedNotification: true }, true));

  return dispatch(
    apiPatch(REQUEST_NOTIFICATION, GATEWAY_SHOP_AUTH, '/user-shopping-details', {
      productId: id,
      states: 1,
      userId,
    }),
  ).catch(e => {
    // something went wrong. restore the CartButton state
    dispatch(setEntity(PRODUCT, id, { hasRequestedNotification: false }, true));
    debug('Unable to request notification: ');
    debug(e);
  });
};

export const GET_COUNTRY_REGIONS = 'shopActions/GET_COUNTRY_REGIONS';
export const getCountryRegions = () => async (dispatch, getState) => {
  await dispatch(getAccount());
  const state = getState();
  const account = userAccountSelector(state);
  const countryRegionSelector = createCountryRegionSelector(account.country);

  // only fetch country codes if we don't already have them in state
  if (!state.entities.countryRegion) {
    await dispatch(
      apiGetCollection(
        GET_COUNTRY_REGIONS,
        GATEWAY_SHOP_AUTH,
        '/country-regions',
        COUNTRY_REGIONS,
        {},
        {
          cache: collectionCachingNoPagination,
          entityType: COUNTRY_REGION,
          getId: entity => entity.countryCode,
        },
      ),
    );
  }

  const initialRegion = countryRegionSelector(getState())?.deliveryRegion;
  const deliveryRegion =
    initialRegion || initialRegion === 0 ? initialRegion : region.OUTSIDE_EUROPE;

  return deliveryRegion;
};

export const UPDATE_DELIVERY_ADDRESS = 'shopActions/UPDATE_DELIVERY_ADDRESS';
export const deliveryRegionUpdated = createAction(UPDATE_DELIVERY_ADDRESS, regionUpdated => ({
  regionUpdated,
}));

export const SET_CURRENT_DELIVERY_REGION = 'shopActions/SET_CURRENT_DELIVERY_REGION';
export const setCurrentDeliveryRegion = createAction(SET_CURRENT_DELIVERY_REGION);

export const updateDeliveryAddress = (initialValues, values, addressField) => (
  dispatch,
  getState,
) => {
  const state = getState();
  const initialCountry = initialValues.country;
  const newCountry = values.country;

  const initialRegionSelector = createCountryRegionSelector(initialCountry);
  const newRegionSelector = createCountryRegionSelector(newCountry);

  const initialRegion = initialRegionSelector(state)?.deliveryRegion;
  const newRegion = newRegionSelector(state)?.deliveryRegion;
  const initialDeliveryRegion =
    initialRegion || initialRegion === 0 ? initialRegion : region.OUTSIDE_EUROPE;
  const newDeliveryRegion = newRegion || newRegion === 0 ? newRegion : region.OUTSIDE_EUROPE;

  dispatch(change(FormNames.SHOP_CHECKOUT, addressField, { ...values }));

  // Clear the basket when the region is not the same
  if (initialDeliveryRegion !== newDeliveryRegion) {
    dispatch(change(FormNames.SHOP_BASKET, BasketFields.ITEMS, []));
    // Show an error message about the region update
    dispatch(deliveryRegionUpdated(true));
    dispatch(getProducts(DEFAULT_LIMIT, null, newDeliveryRegion));
    dispatch(setCurrentDeliveryRegion(newDeliveryRegion));
  }

  // Save the mutated values
  if (values.updateProfile) {
    dispatch(updateAccount(values));
  }
};

export const GET_USER_HIFI_LIMITS = 'shopActions/GET_USER_HIFI_LIMITS';
export const ADD_USER_HIFI_LIMITS = 'shopActions/ADD_USER_HIFI_LIMITS';
const addUserHifiLimits = createAction(ADD_USER_HIFI_LIMITS);

export const getUserHifiLimits = () => async (dispatch, getState) => {
  await authenticate();
  const state = getState();
  const userId = userIdSelector(state);
  return dispatch(apiGet(GET_USER_HIFI_LIMITS, GATEWAY_SHOP_AUTH, `/user-hifi-limits/${userId}`))
    .then(({ data }) => dispatch(addUserHifiLimits(data)))
    .catch(() =>
      dispatch(
        addUserHifiLimits({ resetCountAtDateUTC: null, limit: HifiRequirements.MAXIMUM_AMOUNT }),
      ),
    );
};

export const GET_PACKAGE = 'shopActions/GET_PACKAGE';

export const getPackage = id => dispatch =>
  dispatch(apiGetEntity(GET_PACKAGE, GATEWAY_SHOP, `/packages/${id}`, PACKAGE, id));

export const GET_PACKAGES = 'shopActions/GET_PACKAGES';

/**
 * @param {Object} query
 * @param {Number} query.limit
 * @param {Object} query.regionCode
 * @param {Object} [query.sinceId]
 * @param {Object} [query.offsetId]
 */
export const getPackages = query => dispatch =>
  dispatch(
    apiGetCollection(
      GET_PACKAGES,
      GATEWAY_SHOP,
      `/packages`,
      packageCollectionId(query),
      {},
      {
        requestData: query,
        entityType: PACKAGE,
        caching: collectionCachingNoPagination,
      },
    ),
  );

export const clearPackageField = () => (dispatch, getState) => {
  const packagesSelector = makePackagesWithDiscountSelector();

  const checkoutFormSelector = formValueSelector(FormNames.CHECKOUT);
  const voucherCode = checkoutFormSelector(getState(), shopFields.VOUCHER_CODE);
  const regionCode = Configuration.hasRegionPicker
    ? checkoutFormSelector(getState(), shopFields.REGION)
    : Configuration.defaultRegion;

  const packages = packagesSelector(getState(), {
    regionCode,
    voucherCode,
  });

  dispatch(change(FormNames.CHECKOUT, shopFields.PACKAGE, packages?.[0]?.id));
};

export const SET_FIRST_LOAD = 'shopActions/SET_FIRST_LOAD';
export const setFirstLoad = createAction(SET_FIRST_LOAD);

export const SET_HIFI_NO_INGREDIENTS = 'shopActions/SET_HIFI_NO_INGREDIENTS';
export const setHifiNoIngredients = createAction(SET_HIFI_NO_INGREDIENTS);

export const SET_IS_VOUCHER_AVAILABLE = 'shopActions/SET_IS_VOUCHER_AVAILABLE';
export const setIsVoucherAvailable = createAction(SET_IS_VOUCHER_AVAILABLE);