graycoreio/daffodil

View on GitHub
libs/product-configurable/state/src/selectors/configurable-product/configurable-product.selectors.ts

Summary

Maintainability
C
7 hrs
Test Coverage
import { Dictionary } from '@ngrx/entity';
import {
  createSelector,
  MemoizedSelector,
  defaultMemoize,
} from '@ngrx/store';

import { daffSubtract } from '@daffodil/core';
import {
  DaffProductTypeEnum,
  DaffProduct,
} from '@daffodil/product';
import { getDaffProductEntitiesSelectors } from '@daffodil/product/state';
import {
  DaffConfigurableProductVariant,
  DaffConfigurableProduct,
  DaffConfigurableProductAttribute,
} from '@daffodil/product-configurable';

import { DaffConfigurableProductEntityAttribute } from '../../reducers/configurable-product-entities/configurable-product-entity';
import { DaffConfigurableProductStateRootSlice } from '../../reducers/configurable-product-reducers-state.interface';
import { getDaffConfigurableProductEntitiesSelectors } from '../configurable-product-entities/configurable-product-entities.selectors';

/**
 * An interface describing all selectors unique to configurable products including ranged pricing, configurable attributes, and product variants.
 */
export interface DaffConfigurableProductMemoizedSelectors<T extends DaffProduct = DaffProduct> {
  /**
   * Selects all possible attributes of a configurable product.
   *
   * @param productId the id of the configurable product.
   */
  selectAllConfigurableProductAttributes: (productId: T['id']) => MemoizedSelector<DaffConfigurableProductStateRootSlice<T>, Dictionary<string[]>>;
  /**
   * Selects all variants of the configurable product.
   *
   * @param productId the id of the configurable product.
   */
  selectAllConfigurableProductVariants: (productId: T['id']) => MemoizedSelector<DaffConfigurableProductStateRootSlice<T>, DaffConfigurableProductVariant[]>;
  /**
   * Selects the configurable product variants that match the currently applied attributes.
   *
   * @param productId the id of the configurable product.
   */
  selectMatchingConfigurableProductVariants: (productId: T['id']) => MemoizedSelector<DaffConfigurableProductStateRootSlice<T>, DaffConfigurableProductVariant[]>;
  /**
   * Selects all prices for the configurable product variants that match the currently applied attributes.
   *
   * @param productId the id of the configurable product.
   */
  selectConfigurableProductPrices: (productId: T['id']) => MemoizedSelector<DaffConfigurableProductStateRootSlice<T>, number[]>;
  /**
   * Selects all discounted prices for the configurable product variant that match the currently applied attributes.
   *
   * @param productId the id of the configurable product.
   */
  selectConfigurableProductDiscountedPrices: (productId: T['id']) => MemoizedSelector<DaffConfigurableProductStateRootSlice<T>, number[]>;
  /**
   * Selects all percent discounts for the configurable product variants that match the currently applied attributes.
   *
   * @param productId the id of the configurable product.
   */
  selectConfigurableProductPercentDiscounts: (productId: T['id']) => MemoizedSelector<DaffConfigurableProductStateRootSlice<T>, number[]>;
  /**
   * Selects whether or not any variants that match the currently applied attributes have a discount.
   *
   * @param productId the id of the configurable product.
   */
  selectConfigurableProductHasDiscount: (productId: T['id']) => MemoizedSelector<DaffConfigurableProductStateRootSlice<T>, boolean>;
  /**
   * Selects the minimum possible price of the configurable product variants that match the currently applied attributes.
   *
   * @param productId the id of the configurable product.
   */
  selectConfigurableProductMinimumPrice: (productId: T['id']) => MemoizedSelector<DaffConfigurableProductStateRootSlice<T>, number>;
  /**
   * Selects the maximum possible price of the configurable product variants that match the currently applied attributes.
   *
   * @param productId the id of the configurable product.
   */
  selectConfigurableProductMaximumPrice: (productId: T['id']) => MemoizedSelector<DaffConfigurableProductStateRootSlice<T>, number>;
  /**
   * Selects the minimum possible discounted price of the configurable product variants that match the currently applied attributes.
   */
  selectConfigurableProductMinimumDiscountedPrice: (productId: T['id']) => MemoizedSelector<DaffConfigurableProductStateRootSlice<T>, number>;
  /**
   * Selects the maximum possible discounted price of the configurable product variants that match the currently applied attributes.
   *
   * @param productId the id of the configurable product.
   */
  selectConfigurableProductMaximumDiscountedPrice: (productId: T['id']) => MemoizedSelector<DaffConfigurableProductStateRootSlice<T>, number>;
  /**
   * Selects the minimum possible percent discount of the configurable product variants that match the currently applied attributes.
   *
   * @param productId the id of the configurable product.
   */
  selectConfigurableProductMinimumPercentDiscount: (productId: T['id']) => MemoizedSelector<DaffConfigurableProductStateRootSlice<T>, number>;
  /**
   * Selects the maximum possible percent discount of the configurable product variants that match the currently applied attributes.
   *
   * @param productId the id of the configurable product.
   */
  selectConfigurableProductMaximumPercentDiscount: (productId: T['id']) => MemoizedSelector<DaffConfigurableProductStateRootSlice<T>, number>;
  /**
   * Selects whether or not the currently applied attributes result in more than one possible price.
   *
   * @param productId the id of the configurable product.
   */
  isConfigurablePriceRanged: (productId: T['id']) => MemoizedSelector<DaffConfigurableProductStateRootSlice<T>, boolean>;
  /**
   * Selects the available/selectable configurable product attributes derived from the order of currently applied attributes and the remaining variants
   * (determined by the currently applied attributes). An attribute might not be selectable if none of the matching variants have that particular attribute.
   */
  selectSelectableConfigurableProductAttributes: (productId: T['id']) => MemoizedSelector<DaffConfigurableProductStateRootSlice<T>, Dictionary<string[]>>;
}

const createConfigurableProductSelectors = <T extends DaffProduct = DaffProduct>(): DaffConfigurableProductMemoizedSelectors<T> => {

  const {
    selectConfigurableProductAppliedAttributes,
  } = getDaffConfigurableProductEntitiesSelectors<T>();

  const {
    selectProduct,
  } = getDaffProductEntitiesSelectors<T>();

  const selectAllConfigurableProductVariants = defaultMemoize((productId: T['id']) => createSelector<DaffConfigurableProductStateRootSlice<T>, [T], DaffConfigurableProductVariant[]>(
    selectProduct(productId),
    (product: T) => {
      if(!product || product.type !== DaffProductTypeEnum.Configurable) {
        return [];
      }
      return (<DaffConfigurableProduct><any>product).variants;
    },
  )).memoized;

  const selectMatchingConfigurableProductVariants = defaultMemoize((productId: T['id']) => createSelector<DaffConfigurableProductStateRootSlice<T>, [T, DaffConfigurableProductEntityAttribute[]], DaffConfigurableProductVariant[]>(
    selectProduct(productId),
    selectConfigurableProductAppliedAttributes(productId),
    (product: T, appliedAttributes) => {
      if(!product || product.type !== DaffProductTypeEnum.Configurable) {
        return [];
      }
      return (<DaffConfigurableProduct><any>product).variants.filter(variant => isVariantAvailable(appliedAttributes, variant));
    },
  )).memoized;

  const selectConfigurableProductPrices = defaultMemoize((productId: T['id']) => createSelector(
    selectMatchingConfigurableProductVariants(productId),
    (variants: DaffConfigurableProductVariant[]) => variants.map(variant => variant.price),
  )).memoized;

  const selectConfigurableProductDiscountedPrices = defaultMemoize((productId: T['id']) => createSelector(
    selectMatchingConfigurableProductVariants(productId),
    (variants: DaffConfigurableProductVariant[]) => variants.map(variant =>
      variant.discount ? daffSubtract(variant.price, variant.discount.amount) : variant.price,
    ),
  )).memoized;

  const selectConfigurableProductPercentDiscounts = defaultMemoize((productId: T['id']) => createSelector(
    selectMatchingConfigurableProductVariants(productId),
    (variants: DaffConfigurableProductVariant[]) => variants.map(variant => variant.discount?.percent),
  )).memoized;

  const selectConfigurableProductHasDiscount = defaultMemoize((productId: T['id']) => createSelector(
    selectMatchingConfigurableProductVariants(productId),
    (variants: DaffConfigurableProductVariant[]) => variants.reduce((acc, variant) =>
      acc || (variant.discount?.amount > 0), false,
    ),
  )).memoized;

  const selectConfigurableProductMinimumPrice = defaultMemoize((productId: T['id']) => createSelector(
    selectConfigurableProductPrices(productId),
    getMinimumPrice,
  )).memoized;

  const selectConfigurableProductMaximumPrice = defaultMemoize((productId: T['id']) => createSelector(
    selectConfigurableProductPrices(productId),
    getMaximumPrice,
  )).memoized;

  const selectConfigurableProductMinimumDiscountedPrice = defaultMemoize((productId: T['id']) => createSelector(
    selectConfigurableProductDiscountedPrices(productId),
    getMinimumPrice,
  )).memoized;

  const selectConfigurableProductMaximumDiscountedPrice = defaultMemoize((productId: T['id']) => createSelector(
    selectConfigurableProductDiscountedPrices(productId),
    getMaximumPrice,
  )).memoized;

  const selectConfigurableProductMinimumPercentDiscount = defaultMemoize((productId: T['id']) => createSelector(
    selectConfigurableProductPercentDiscounts(productId),
    getMinimumPrice,
  )).memoized;

  const selectConfigurableProductMaximumPercentDiscount = defaultMemoize((productId: T['id']) => createSelector(
    selectConfigurableProductPercentDiscounts(productId),
    getMaximumPrice,
  )).memoized;

  const isConfigurablePriceRanged = defaultMemoize((productId: T['id']) => createSelector(
    selectConfigurableProductMinimumPrice(productId),
    selectConfigurableProductMaximumPrice(productId),
    (minPrice: number, maxPrice: number) => minPrice !== maxPrice,
  )).memoized;

  const selectAllConfigurableProductAttributes = defaultMemoize((productId: T['id']) => createSelector(
    selectProduct(productId),
    (product: T) => {
      if(product.type !== DaffProductTypeEnum.Configurable) {
        return {};
      }
      return (<DaffConfigurableProduct><any>product).configurableAttributes.reduce((acc, attribute) => ({
        ...acc,
        [attribute.code]: attribute.values.map(value => value.value),
      }), {});
    },
  )).memoized;

  const selectSelectableConfigurableProductAttributes = defaultMemoize((productId: T['id']) => createSelector(
    selectProduct(productId),
    selectConfigurableProductAppliedAttributes(productId),
    (product: T, appliedAttributes: DaffConfigurableProductEntityAttribute[]) => {
      if(product.type !== DaffProductTypeEnum.Configurable) {
        return {};
      }

      const selectableAttributes = initializeSelectableAttributes((<DaffConfigurableProduct><any>product).configurableAttributes);

      // Set which values of applied attribute codes should be set as selectable based on the order that they were selected
      const matchedVariants = appliedAttributes.reduce((matchingVariants, appliedAttribute, i) => {
        const filteredVariants = matchingVariants.filter(variant => isVariantAvailable(appliedAttributes.slice(0, i), variant));

        selectableAttributes[appliedAttribute.code] = getSelectableAttributesFromVariants(selectableAttributes, filteredVariants, appliedAttribute.code);

        return filteredVariants;
      }, (<DaffConfigurableProduct><any>product).variants).filter(variant =>
        isVariantAvailable(appliedAttributes, variant),
      );

      // Set which values of the unapplied attribute codes should be set as selectable based on the matching variants of all
      // applied attributes.
      (<DaffConfigurableProduct><any>product).configurableAttributes.forEach(attribute => {
        if (!selectableAttributes[attribute.code].length) {
          selectableAttributes[attribute.code] = getSelectableAttributesFromVariants(selectableAttributes, matchedVariants, attribute.code);
        }
      });

      return selectableAttributes;
    },
  )).memoized;

  return {
    selectAllConfigurableProductAttributes,
    selectAllConfigurableProductVariants,
    selectConfigurableProductPrices,
    selectConfigurableProductDiscountedPrices,
    selectConfigurableProductPercentDiscounts,
    selectConfigurableProductHasDiscount,
    selectConfigurableProductMinimumPrice,
    selectConfigurableProductMaximumPrice,
    selectConfigurableProductMinimumDiscountedPrice,
    selectConfigurableProductMaximumDiscountedPrice,
    selectConfigurableProductMinimumPercentDiscount,
    selectConfigurableProductMaximumPercentDiscount,
    isConfigurablePriceRanged,
    selectMatchingConfigurableProductVariants,
    selectSelectableConfigurableProductAttributes,
  };
};

function getSelectableAttributesFromVariants(selectableAttributes: Dictionary<string[]>, variants: DaffConfigurableProductVariant[], code: string) {
  return variants.reduce((selectedAttributes, variant) =>
    isVariantAttributeMarkedAsSelectable(selectedAttributes, variant.appliedAttributes[code])
      ? selectedAttributes
      : [
        ...selectedAttributes,
        variant.appliedAttributes[code],
      ],
  selectableAttributes[code],
  );
}

/**
 * A function that returns all configurable product selectors.
 * Returns {@link DaffConfigurableProductMemoizedSelectors}.
 */
export const getDaffConfigurableProductSelectors = (() => {
  let cache;
  return <T extends DaffProduct = DaffProduct>(): DaffConfigurableProductMemoizedSelectors<T> => cache = cache
    ? cache
    : createConfigurableProductSelectors();
})();

function isVariantAvailable(
  appliedAttributes: DaffConfigurableProductEntityAttribute[],
  variant: DaffConfigurableProductVariant,
): boolean {
  return variant.in_stock &&
        appliedAttributes.reduce((acc, attribute) =>
          acc && attribute.value === variant.appliedAttributes[attribute.code],
        true,
        );
}

function getMinimumPrice(prices: number[]): number {
  return prices.reduce(
    (acc, price) => price < acc ? price : acc,
    prices[0],
  );
}

function getMaximumPrice(prices: number[]): number {
  return prices.reduce(
    (acc, price) => price > acc ? price : acc,
    prices[0],
  );
}

function initializeSelectableAttributes(attributes: DaffConfigurableProductAttribute[]): Dictionary<string[]> {
  return attributes.reduce((acc, attribute) => ({
    ...acc,
    [attribute.code]: [],
  }), {});
}

function isVariantAttributeMarkedAsSelectable(attributeArray: string[], variantValue: string) {
  return attributeArray.indexOf(variantValue) > -1;
}