UNPKG

barneo-search-widget-lib

Version:

Библиотека для поиска по каталогу Barneo на Vue 3

754 lines (671 loc) 21.4 kB
// Импортируем типы из отдельных файлов 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, }); } }