barneo-search-widget-lib
Version: 
Библиотека для поиска по каталогу Barneo на Vue 3
754 lines (671 loc) • 21.4 kB
text/typescript
// Импортируем типы из отдельных файлов
import type {
  ApiResponse,
  AuthorizeCustomerRequest,
  CatalogSearchRequest,
  CatalogSearchResponse,
  ProductClassificationSearchRequest,
  ProductClassificationSearchResponse,
  RecommendationProductsRequest,
  RecommendationQueryProductsRequest,
  RecommendationResponse,
  CrossSellProductsRequest,
  UpSellProductsRequest,
  RecentlyWatchedProductsRequest,
  HistoryQueriesSearchRequest,
  HistoryQueriesDeleteRequest,
  HistoryQueriesDeleteAllRequest,
  TopQueriesSearchRequest,
  TopQueriesResponse,
  PopularProductsSearchRequest,
  PopularProductsResponse,
  PopularBrandsSearchRequest,
  PopularBrandsResponse,
  FunctionalUseRequest,
  QueryUseRequest,
  HintsUseRequest,
  ProductsUseRequest,
  PopularCategoriesSearchRequest,
  PopularCategoriesResponse,
  CategoriesUseRequest,
  SimilarProductsSearchRequest,
  SimilarProductsResponse,
  SearchApiConfig,
  AbTestResponse,
} from "../modules/searchWidget/types";
/**
 * Сервис для работы с API поиска
 */
export class SearchApiService {
  private readonly baseUrl: string;
  private readonly token: string;
  private readonly customerId: string;
  private readonly locationId: string;
  private readonly apiVersion: string | undefined;
  private readonly defaultHeaders: Record<string, string>;
  constructor(config: SearchApiConfig) {
    this.baseUrl = config.baseUrl.replace(/\/$/, ""); // Убираем trailing slash
    this.token = config.token;
    this.customerId = config.customerId;
    this.locationId = config.locationId;
    this.apiVersion = config.apiVersion; // Может быть undefined
    // Инициализируем заголовки по умолчанию
    this.defaultHeaders = {
      Accept: "application/json",
      "Content-Type": "application/json",
      Authorization: `Bearer ${this.token}`,
      "X-Customer-Id": this.customerId,
    };
  }
  /**
   * Формирует полный URL для API запроса
   */
  private buildApiUrl(endpoint: string): string {
    // Убираем ведущий слеш из endpoint если есть
    const cleanEndpoint = endpoint.startsWith("/")
      ? endpoint.slice(1)
      : endpoint;
    // Если apiVersion не указан, используем только baseUrl
    if (!this.apiVersion) {
      return `${this.baseUrl}/${cleanEndpoint}`;
    }
    // Если apiVersion указан, добавляем его в путь
    return `${this.baseUrl}/${this.apiVersion}/${cleanEndpoint}`;
  }
  /**
   * Получить ID локации
   */
  getLocationId(): string {
    return this.locationId;
  }
  /**
   * Получить ID клиента
   */
  getCustomerId(): string {
    return this.customerId;
  }
  /**
   * Выполнить HTTP запрос к API
   */
  private async makeRequest<T>(
    endpoint: string,
    options: RequestInit = {}
  ): Promise<ApiResponse<T>> {
    const url = this.buildApiUrl(endpoint);
    const requestOptions: RequestInit = {
      ...options,
      headers: {
        ...this.defaultHeaders,
        ...options.headers,
      },
    };
    try {
      const response = await fetch(url, requestOptions);
      if (!response.ok) {
        await this.handleErrorResponse(response, endpoint);
      }
      const data = await response.json();
      return data;
    } catch (error) {
      this.handleRequestError(error, endpoint);
      throw error;
    }
  }
  /**
   * Обработка ошибок HTTP ответа
   */
  private async handleErrorResponse(
    response: Response,
    endpoint: string
  ): Promise<never> {
    const status = response.status;
    let errorMessage: string;
    switch (status) {
      case 401:
        errorMessage = `Ошибка авторизации (401): Проверьте правильность токена API и customer ID`;
        break;
      case 403:
        errorMessage = `Доступ запрещен (403): Недостаточно прав для выполнения операции`;
        break;
      case 404:
        errorMessage = `Эндпоинт не найден (404): ${endpoint}`;
        break;
      case 429:
        errorMessage = `Превышен лимит запросов (429): Попробуйте позже`;
        break;
      case 500:
        errorMessage = `Внутренняя ошибка сервера (500): Попробуйте позже`;
        break;
      default:
        const errorText = await response.text();
        let errorData;
        try {
          errorData = JSON.parse(errorText);
        } catch {
          errorData = { message: errorText };
        }
        errorMessage = `API Error: ${status} - ${
          errorData.message || response.statusText
        }`;
    }
    const error = new Error(errorMessage);
    (error as any).status = status;
    (error as any).endpoint = endpoint;
    throw error;
  }
  /**
   * Обработка ошибок сети
   */
  private handleRequestError(error: unknown, endpoint: string): void {
    console.error(`API Request Error for ${endpoint}:`, error);
    if (error instanceof TypeError && error.message.includes("fetch")) {
      throw new Error(
        `Ошибка сети: Не удается подключиться к API (${endpoint})`
      );
    }
  }
  /**
   * Объединение неавторизованного и авторизованного пользователя
   */
  async authorizeCustomer(
    request: AuthorizeCustomerRequest
  ): Promise<ApiResponse> {
    this.validateCustomerRequest(request);
    return this.makeRequest("customers/authorize", {
      method: "POST",
      body: JSON.stringify(request),
    });
  }
  /**
   * Поиск по каталогу
   */
  async searchCatalog(
    request: Partial<CatalogSearchRequest>
  ): Promise<ApiResponse<CatalogSearchResponse>> {
    const defaultRequest: CatalogSearchRequest = {
      is_fast_result: true,
      include: ["products"],
      sort: "relevance",
      filter: {
        location_id: this.locationId,
        query: request.filter?.query || "",
        auto_filter: true,
        use_query_correction: true,
      },
      pagination: {
        limit_products: 20,
        offset_products: 0,
      },
      load_full_products: {
        location_id: this.locationId,
        include: ["properties"],
      },
    };
    const finalRequest = { ...defaultRequest, ...request };
    this.validateCatalogRequest(finalRequest);
    return this.makeRequest("catalog/search", {
      method: "POST",
      body: JSON.stringify(finalRequest),
    });
  }
  /**
   * Валидация запроса авторизации клиента
   */
  private validateCustomerRequest(request: AuthorizeCustomerRequest): void {
    if (!request.guest_customer_id || !request.authorized_customer_id) {
      throw new Error(
        "Необходимо указать guest_customer_id и authorized_customer_id"
      );
    }
  }
  /**
   * Валидация запроса поиска по каталогу
   */
  private validateCatalogRequest(request: CatalogSearchRequest): void {
    if (!request.filter.query?.trim()) {
      throw new Error("Поисковый запрос не может быть пустым");
    }
    if (request.filter.query.length < 2) {
      throw new Error("Поисковый запрос должен содержать минимум 2 символа");
    }
  }
  /**
   * Поиск результатов классификации продуктов
   */
  async searchProductClassification(
    request: Partial<ProductClassificationSearchRequest>
  ): Promise<ApiResponse<ProductClassificationSearchResponse>> {
    const defaultRequest: ProductClassificationSearchRequest = {
      filter: {},
      pagination: {
        limit: 100,
        offset: 0,
      },
    };
    const finalRequest = { ...defaultRequest, ...request };
    return this.makeRequest("catalog/product-classification-results:search", {
      method: "POST",
      body: JSON.stringify(finalRequest),
    });
  }
  /**
   * Поиск рекомендаций "С этим товаром ищут"
   */
  async getRecommendationProducts(
    productId: string,
    request?: Partial<Omit<RecommendationProductsRequest, "filter">>
  ): Promise<ApiResponse<RecommendationResponse>> {
    const defaultRequest: RecommendationProductsRequest = {
      filter: {
        product_id: productId,
      },
      pagination: {
        limit: 5,
      },
      load_full_products: {
        location_id: this.locationId,
        include: ["properties"],
      },
    };
    const finalRequest = { ...defaultRequest, ...request };
    return this.makeRequest("adviser/recommendation-products:search", {
      method: "POST",
      body: JSON.stringify(finalRequest),
    });
  }
  /**
   * Поиск рекомендаций "Искавшие также смотрели"
   */
  async getRecommendationQueryProducts(
    query: string,
    request?: Partial<Omit<RecommendationQueryProductsRequest, "filter">>
  ): Promise<ApiResponse<RecommendationResponse>> {
    const defaultRequest: RecommendationQueryProductsRequest = {
      filter: {
        query: query,
      },
      pagination: {
        limit: 5,
      },
      load_full_products: {
        location_id: this.locationId,
        include: ["properties"],
      },
    };
    const finalRequest = { ...defaultRequest, ...request };
    return this.makeRequest("adviser/recommendation-query-products:search", {
      method: "POST",
      body: JSON.stringify(finalRequest),
    });
  }
  /**
   * Поиск рекомендаций "С этим товаром покупают" (Cross Sell)
   */
  async getCrossSellProducts(
    productId: string,
    request?: Partial<Omit<CrossSellProductsRequest, "filter">>
  ): Promise<ApiResponse<RecommendationResponse>> {
    const defaultRequest: CrossSellProductsRequest = {
      filter: {
        product_id: productId,
      },
      pagination: {
        limit: 5,
      },
      load_full_products: {
        location_id: this.locationId,
        include: ["properties"],
      },
    };
    const finalRequest = { ...defaultRequest, ...request };
    return this.makeRequest("adviser/cross-sell-products:search", {
      method: "POST",
      body: JSON.stringify(finalRequest),
    });
  }
  /**
   * Поиск рекомендаций "Up Sell" (более дорогие альтернативы)
   */
  async getUpSellProducts(
    productId: string,
    request?: Partial<Omit<UpSellProductsRequest, "filter">>
  ): Promise<ApiResponse<RecommendationResponse>> {
    const defaultRequest: UpSellProductsRequest = {
      filter: {
        product_id: productId,
        location_id: this.locationId,
      },
      pagination: {
        limit: 5,
      },
      load_full_products: {
        location_id: this.locationId,
        include: ["properties"],
      },
    };
    const finalRequest = { ...defaultRequest, ...request };
    return this.makeRequest("adviser/up-sell-products:search", {
      method: "POST",
      body: JSON.stringify(finalRequest),
    });
  }
  /**
   * Поиск недавно просмотренных товаров
   */
  async getRecentlyWatchedProducts(
    customerId: string,
    request?: Partial<Omit<RecentlyWatchedProductsRequest, "filter">>
  ): Promise<ApiResponse<RecommendationResponse>> {
    const defaultRequest: RecentlyWatchedProductsRequest = {
      filter: {
        customer_id: customerId,
      },
      pagination: {
        limit: 5,
      },
      load_full_products: {
        location_id: this.locationId,
        include: ["properties"],
      },
    };
    const finalRequest = { ...defaultRequest, ...request };
    return this.makeRequest("adviser/recently-watched-products:search", {
      method: "POST",
      body: JSON.stringify(finalRequest),
    });
  }
  /**
   * Проверка авторизации пользователя
   */
  async checkCustomerAuth(customerId: string): Promise<ApiResponse<any>> {
    return this.makeRequest("analytics/history-queries:search", {
      method: "POST",
      body: JSON.stringify({
        filter: {
          customer_id: customerId,
        },
      }),
    });
  }
  /**
   * Получение популярных товаров
   */
  async getPopularProducts(
    request?: Partial<PopularProductsSearchRequest>
  ): Promise<ApiResponse<PopularProductsResponse>> {
    const defaultRequest: PopularProductsSearchRequest = {
      include: ["automatic"],
      pagination: {
        limit_automatic: 5,
      },
      load_full_products: {
        location_id: this.locationId,
        include: ["properties"],
      },
    };
    const finalRequest = { ...defaultRequest, ...request };
    return this.makeRequest<PopularProductsResponse>(
      "analytics/popular-products:search",
      {
        method: "POST",
        body: JSON.stringify(finalRequest),
      }
    );
  }
  /**
   * Получение топ запросов
   */
  async getTopQueries(
    request?: Partial<TopQueriesSearchRequest>
  ): Promise<ApiResponse<TopQueriesResponse>> {
    const defaultRequest: TopQueriesSearchRequest = {
      include: ["popular-successful"],
      pagination: {
        limit_popular_automatic: 5,
      },
    };
    const finalRequest = { ...defaultRequest, ...request };
    return this.makeRequest<TopQueriesResponse>(
      "analytics/top-queries:search",
      {
        method: "POST",
        body: JSON.stringify(finalRequest),
      }
    );
  }
  /**
   * Поиск истории запросов пользователя
   */
  async getHistoryQueries(
    request: Partial<HistoryQueriesSearchRequest>
  ): Promise<ApiResponse<string[]>> {
    const defaultRequest: HistoryQueriesSearchRequest = {
      filter: {
        customer_id: this.customerId,
      },
      pagination: {
        limit: 10,
      },
    };
    const finalRequest = { ...defaultRequest, ...request };
    return this.makeRequest("analytics/history-queries:search", {
      method: "POST",
      body: JSON.stringify(finalRequest),
    });
  }
  /**
   * Удаление конкретного запроса из истории
   */
  async deleteHistoryQuery(
    request: HistoryQueriesDeleteRequest
  ): Promise<ApiResponse<string[]>> {
    return this.makeRequest("analytics/history-queries:one", {
      method: "DELETE",
      body: JSON.stringify(request),
    });
  }
  /**
   * Удаление всей истории запросов пользователя
   */
  async deleteAllHistoryQueries(
    request: HistoryQueriesDeleteAllRequest
  ): Promise<ApiResponse<null>> {
    return this.makeRequest("analytics/history-queries", {
      method: "DELETE",
      body: JSON.stringify(request),
    });
  }
  /**
   * Поиск похожих товаров
   */
  async getSimilarProducts(
    request: Partial<SimilarProductsSearchRequest>
  ): Promise<ApiResponse<SimilarProductsResponse>> {
    const defaultRequest: SimilarProductsSearchRequest = {
      filter: {
        parameter: 1, // По умолчанию используем параметр 1
        product_id: "",
        location_id: this.locationId,
      },
      pagination: {
        limit: 5,
      },
      load_full_products: {
        location_id: this.locationId,
        include: ["properties"],
      },
    };
    const finalRequest = { ...defaultRequest, ...request };
    return this.makeRequest("analytics/similar-products:search", {
      method: "POST",
      body: JSON.stringify(finalRequest),
    });
  }
  /**
   * Поиск популярных брендов
   */
  async getPopularBrands(
    request?: Partial<PopularBrandsSearchRequest>
  ): Promise<ApiResponse<PopularBrandsResponse>> {
    const defaultRequest: PopularBrandsSearchRequest = {
      include: ["automatic"],
      pagination: {
        limit_automatic: 5,
      },
    };
    const finalRequest = { ...defaultRequest, ...request };
    return this.makeRequest("analytics/popular-brands:search", {
      method: "POST",
      body: JSON.stringify(finalRequest),
    });
  }
  /**
   * Использование функции поиска
   */
  async useSearchFunction(functionName: string): Promise<ApiResponse<any>> {
    const request: FunctionalUseRequest = {
      location_id: this.locationId,
      customer_id: this.customerId,
      function: functionName,
    };
    return this.makeRequest("analytics/functional:use", {
      method: "POST",
      body: JSON.stringify(request),
    });
  }
  /**
   * Поисковый запрос клиента
   */
  async trackQueryUse(query: string): Promise<ApiResponse<any>> {
    const request: QueryUseRequest = {
      location_id: this.locationId,
      customer_id: this.customerId,
      query: query,
    };
    return this.makeRequest("analytics/query:use", {
      method: "POST",
      body: JSON.stringify(request),
    });
  }
  /**
   * Применение поисковой подсказки
   */
  async trackHintUse(query: string, hint: string): Promise<ApiResponse<any>> {
    const request: HintsUseRequest = {
      query: query,
      hint: hint,
    };
    return this.makeRequest("analytics/hints:use", {
      method: "POST",
      body: JSON.stringify(request),
    });
  }
  /**
   * Конверсии с товарами
   */
  async trackProductConversion(
    request: Partial<ProductsUseRequest>
  ): Promise<ApiResponse<any>> {
    const defaultRequest: ProductsUseRequest = {
      location_id: this.locationId,
      customer_id: this.customerId,
      product_id: "",
      conversion: "click",
    };
    const finalRequest = { ...defaultRequest, ...request };
    return this.makeRequest("analytics/products:use", {
      method: "POST",
      body: JSON.stringify(finalRequest),
    });
  }
  /**
   * Поиск популярных категорий
   */
  async getPopularCategories(
    request?: Partial<PopularCategoriesSearchRequest>
  ): Promise<ApiResponse<PopularCategoriesResponse>> {
    const defaultRequest: PopularCategoriesSearchRequest = {
      include: ["automatic"],
      pagination: {
        limit_automatic: 5,
      },
    };
    const finalRequest = { ...defaultRequest, ...request };
    return this.makeRequest("analytics/popular-categories:search", {
      method: "POST",
      body: JSON.stringify(finalRequest),
    });
  }
  /**
   * Конверсии с категориями
   */
  async trackCategoryConversion(
    request: Partial<CategoriesUseRequest>
  ): Promise<ApiResponse<any>> {
    const defaultRequest: CategoriesUseRequest = {
      location_id: this.locationId,
      category_id: "",
      customer_id: this.customerId,
      conversion: "click",
    };
    const finalRequest = { ...defaultRequest, ...request };
    return this.makeRequest("analytics/categories:use", {
      method: "POST",
      body: JSON.stringify(finalRequest),
    });
  }
  /**
   * Получение текущего A/B теста для клиента
   */
  async getCurrentAbTest(): Promise<ApiResponse<AbTestResponse>> {
    return this.makeRequest("analytics/ab-tests:current", {
      method: "POST",
      headers: {
        "X-Customer-Id": this.customerId,
      },
    });
  }
  /**
   * Проверить доступность API
   */
  async healthCheck(): Promise<boolean> {
    try {
      await this.makeRequest("health", { method: "GET" });
      return true;
    } catch {
      return false;
    }
  }
  /**
   * Получить информацию о конфигурации (без чувствительных данных)
   */
  getConfigInfo(): {
    baseUrl: string;
    customerId: string;
    locationId: string;
    apiVersion: string | undefined;
  } {
    return {
      baseUrl: this.baseUrl,
      customerId: this.customerId,
      locationId: this.locationId,
      apiVersion: this.apiVersion,
    };
  }
  /**
   * Создать новый экземпляр сервиса с обновленной конфигурацией
   */
  withConfig(updates: Partial<SearchApiConfig>): SearchApiService {
    const currentConfig = {
      baseUrl: this.baseUrl,
      token: this.token,
      customerId: this.customerId,
      locationId: this.locationId,
      apiVersion: this.apiVersion,
    };
    return new SearchApiService({
      ...currentConfig,
      ...updates,
    });
  }
}