All files / src/app/core/models/category category.mapper.ts

100% Statements 55/55
95.45% Branches 21/22
100% Functions 14/14
100% Lines 55/55

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 17034x   34x 34x   34x 34x     34x     34x 107x             83x 81x 127x 127x   2x               75x 2x       73x   73x     118x     45x 45x     45x               45x               84x 1x       83x   83x   4x   83x   37x   83x   2x   83x   1x     83x             79x 78x 78x   78x                           1x                 71x     70x 12x 34x 22x   58x       70x 70x     70x     70x     70x   1x                   78x 73x     5x       5x      
import { Injectable } from '@angular/core';
 
import { AttributeHelper } from 'ish-core/models/attribute/attribute.helper';
import { CategoryTreeHelper } from 'ish-core/models/category-tree/category-tree.helper';
import { CategoryTree } from 'ish-core/models/category-tree/category-tree.model';
import { ImageMapper } from 'ish-core/models/image/image.mapper';
import { SeoAttributesMapper } from 'ish-core/models/seo-attributes/seo-attributes.mapper';
 
import { CategoryData, CategoryPathElement } from './category.interface';
import { Category, CategoryHelper } from './category.model';
 
@Injectable({ providedIn: 'root' })
export class CategoryMapper {
  constructor(private imageMapper: ImageMapper) {}
 
  /**
   * Utility Method:
   * Maps the incoming raw category path to a path with unique IDs.
   */
  mapCategoryPath(path: CategoryPathElement[]) {
    if (path?.length) {
      return path
        .map(x => x.id)
        .reduce((acc, _, idx, arr) => [...acc, arr.slice(0, idx + 1).join(CategoryHelper.uniqueIdSeparator)], []);
    }
    throw new Error('input is falsy');
  }
 
  /**
   * Utility Method:
   * Creates Category stubs from the category path (excluding the last element)
   */
  categoriesFromCategoryPath(path: CategoryPathElement[]): CategoryTree {
    if (!path?.length) {
      return CategoryTreeHelper.empty();
    }
 
    let uniqueId: string;
    const newCategoryPath: string[] = [];
 
    return (
      path
        // remove the last
        .filter((_, idx, arr) => idx !== arr.length - 1)
        .map(pathElement => {
          // accumulate and construct uniqueId and categoryPath
          uniqueId = !uniqueId ? pathElement.id : uniqueId + CategoryHelper.uniqueIdSeparator + pathElement.id;
          newCategoryPath.push(uniqueId);
 
          // yield category stub
          return {
            uniqueId,
            name: pathElement.name,
            completenessLevel: 0,
            categoryPath: [...newCategoryPath],
          };
        })
        // construct a tree from it
        .reduce((tree, cat: Category) => CategoryTreeHelper.add(tree, cat), CategoryTreeHelper.empty())
    );
  }
 
  /**
   * Compute completeness level of incoming raw data.
   */
  computeCompleteness(categoryData: CategoryData): number {
    if (!categoryData) {
      return -1;
    }
 
    // adjust CategoryCompletenessLevel.Max accordingly
    let count = 0;
 
    if (categoryData.categoryRef) {
      // category path categories do not contain a categoryRef
      count++;
    }
    if (categoryData.categoryPath && categoryData.categoryPath.length === 1) {
      // root categories have no images but a single-entry categoryPath
      count++;
    }
    if (categoryData.images) {
      // images are supplied for sub categories in the category details call
      count++;
    }
    if (categoryData.seoAttributes) {
      // seo attributes are only supplied with the category details call
      count++;
    }
 
    return count;
  }
 
  /**
   * Maps a raw {@link CategoryData} element to a {@link Category} element ignoring subcategories.
   */
  fromDataSingle(categoryData: CategoryData): Category {
    if (categoryData) {
      const categoryPath = this.mapCategoryPath(categoryData.categoryPath);
      const uniqueId = categoryPath[categoryPath.length - 1];
 
      return {
        uniqueId,
        categoryRef: categoryData.categoryRef,
        categoryPath,
        name: categoryData.name,
        hasOnlineProducts: categoryData.hasOnlineProducts,
        description: categoryData.description,
        images: this.imageMapper.fromImages(categoryData.images),
        attributes: categoryData.attributes,
        hideInMenu: this.shouldHideInMenu(categoryData),
        completenessLevel: this.computeCompleteness(categoryData),
        seoAttributes: SeoAttributesMapper.fromData(categoryData.seoAttributes),
      };
    } else {
      throw new Error(`'categoryData' is required`);
    }
  }
 
  /**
   * Converts the tree of {@link CategoryData} to the model entity {@link CategoryTree}.
   * Inserts all sub categories accordingly.
   */
  fromData(categoryData: CategoryData): CategoryTree {
    if (categoryData) {
      // recurse into tree
      let subTrees: CategoryTree;
      if (categoryData.subCategories?.length) {
        subTrees = categoryData.subCategories
          .map(c => this.fromData(c) as CategoryTree)
          .reduce((a, b) => CategoryTreeHelper.merge(a, b));
      } else {
        subTrees = CategoryTreeHelper.empty();
      }
 
      // create tree from current category
      const rootCat = this.fromDataSingle(categoryData);
      const tree = CategoryTreeHelper.single(rootCat);
 
      // create tree from categoryPath stubs
      const categoryPathTree = this.categoriesFromCategoryPath(categoryData.categoryPath);
 
      // merge sub categories onto current tree
      const treeWithSubCategories = CategoryTreeHelper.merge(tree, subTrees);
 
      // merge categoryPath stubs onto current tree
      return CategoryTreeHelper.merge(treeWithSubCategories, categoryPathTree);
    } else {
      throw new Error(`'categoryData' is required`);
    }
  }
 
  /**
   * Determines if a category should be hidden in the menu based on the 'ShowInMenu' attribute.
   * Categories without the ShowInMenu attribute or with any value other than 'false' will be shown by default.
   * The 'hideInMenu' property is always set on mapped Category objects for consistent behavior.
   */
  private shouldHideInMenu(categoryData: CategoryData): boolean {
    if (!categoryData?.attributes?.length) {
      return false;
    }
 
    const showInMenuValue = AttributeHelper.getAttributeValueByAttributeName<string>(
      categoryData.attributes,
      'ShowInMenu'
    );
    return showInMenuValue?.toLowerCase() === 'false';
  }
}