All files / src/app/extensions/copilot-embedded/models/copilot-embedded-product copilot-embedded-product.mapper.ts

100% Statements 15/15
88.88% Branches 16/18
100% Functions 9/9
100% Lines 13/13

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                              8x   8x                 15x 9x 6x 18x       9x                       6x 6x 5x 6x   9x   1x        
import {
  CopilotEmbeddedToolCall,
  CopilotEmbeddedToolName,
} from '../copilot-embedded-tool-call/copilot-embedded-tool-call.model';
 
import { CopilotEmbeddedProductData } from './copilot-embedded-product.interface';
import { CopilotEmbeddedProduct } from './copilot-embedded-product.model';
 
/**
 * Raw `icmSearch` tool output: products are grouped by promotions under `output`.
 */
interface CopilotEmbeddedProductSearchOutput {
  output?: { products?: CopilotEmbeddedProductData[] }[];
}
 
const SEARCH_TOOL: CopilotEmbeddedToolName = 'icmSearch';
 
export class CopilotEmbeddedProductMapper {
  /**
   * Maps the `icmSearch` tool outputs of a chatflow response (JSON strings with promotion-grouped products)
   * to a flat product list. Invalid output is ignored. Products found by several searches are only listed
   * once (first occurrence wins).
   *
   * @param tools The tool calls of a chatflow response (`usedTools`).
   */
  static fromToolCalls(tools: CopilotEmbeddedToolCall[]): CopilotEmbeddedProduct[] {
    const products = (tools ?? [])
      .filter(tool => tool?.tool === SEARCH_TOOL && !!tool.toolOutput)
      .flatMap(tool => CopilotEmbeddedProductMapper.fromSearchOutput(tool.toolOutput));
    return products.filter((product, index) => products.findIndex(p => p.sku === product.sku) === index);
  }
 
  private static fromData(data: CopilotEmbeddedProductData): CopilotEmbeddedProduct {
    return {
      sku: data?.sku,
      title: data?.title,
      price: typeof data?.price === 'number' ? data.price : undefined,
      currency: data?.currency ?? undefined,
      imageUrl: data?.mainImage ?? undefined,
      shortDescription: data?.shortDescription ?? undefined,
      manufacturer: data?.manufacturer ?? undefined,
    };
  }
 
  private static fromSearchOutput(toolOutput: string): CopilotEmbeddedProduct[] {
    try {
      const parsed = JSON.parse(toolOutput) as CopilotEmbeddedProductSearchOutput;
      return (parsed?.output ?? [])
        .flatMap(group => group?.products ?? [])
        .map(CopilotEmbeddedProductMapper.fromData)
        .filter(product => !!product.sku);
    } catch {
      return [];
    }
  }
}