Bitrefill Search and Shop
Official
by bitrefill
- src
- services
import { BitrefillSearchResponse, SearchOptions, SearchResults } from "../types/index.js";
import { logError } from "../utils/index.js";
/**
* Service for searching Bitrefill products
*/
export class SearchService {
private static readonly BASE_URL = "https://www.bitrefill.com/api/omni";
private static readonly WEBSITE_BASE_URL = "https://www.bitrefill.com";
/**
* Search for products by query with optional parameters
* @param query - Search query string
* @param options - Additional search options
* @returns Search results
*/
public static async search(query: string, options: Partial<SearchOptions> = {}): Promise<SearchResults> {
try {
const url = this.buildSearchUrl(query, options);
const response = await fetch(url, {
method: "GET",
headers: this.getDefaultHeaders(),
});
if (!response.ok) {
throw new Error(`API request failed with status ${response.status}`);
}
const data = await response.json() as BitrefillSearchResponse;
return this.transformApiResponse(data, options);
} catch (error) {
logError(error instanceof Error ? error : new Error(String(error)), "SearchService");
return { results: [] };
}
}
/**
* Build the search URL with query parameters
* @param query - Search query
* @param options - Additional search options
* @returns Formatted URL string
*/
private static buildSearchUrl(query: string, options: Partial<SearchOptions>): string {
// Start with default parameters
const params = new URLSearchParams({
q: query,
limit: String(options.limit || 6),
skip: String(options.skip || 0),
src: 'mcp',
col: '1',
prefcc: '1',
});
// Add optional parameters if provided
if (options.category) params.set('category', options.category);
if (options.country) params.set('country', options.country);
if (options.language) params.set('hl', options.language);
return `${this.BASE_URL}?${params.toString()}`;
}
/**
* Get default headers for API requests
* @returns Headers object
*/
private static getDefaultHeaders(): HeadersInit {
return {
"accept": "application/json",
"accept-language": "en-US,en;q=0.9",
"sec-fetch-dest": "empty",
"sec-fetch-mode": "cors",
"sec-fetch-site": "same-origin"
};
}
/**
* Generate product URL based on product data and search options
* Format: {country}/{language}/{type}/{id}
* @param product - Product data
* @param options - Search options
* @returns Product URL
*/
private static generateProductUrl(product: any, options: Partial<SearchOptions> = {}): string {
// Default values
const country = product.countryCode?.toLowerCase() || options.country?.toLowerCase() || 'us';
const language = options.language?.toLowerCase() || 'en';
const type = product.type?.toLowerCase() || 'gift-cards';
const slug = product.slug || product.id;
return `${this.WEBSITE_BASE_URL}/${country}/${language}/${type}/${slug}/`;
}
/**
* Transform API response to SearchResults format
* @param apiResponse - Raw API response
* @param options - Search options used for the request
* @returns Formatted search results
*/
private static transformApiResponse(apiResponse: BitrefillSearchResponse, options: Partial<SearchOptions> = {}): SearchResults {
if (!apiResponse.products || !Array.isArray(apiResponse.products)) {
return { results: [] };
}
return {
results: apiResponse.products.map((product) => ({
id: product.id,
name: product.name,
type: product.type || "",
url: this.generateProductUrl(product, options),
_priceRange: product._priceRange,
_ratingValue: product._ratingValue,
_reviewCount: product._reviewCount,
baseName: product.baseName || "",
billCategories: product.billCategories || [],
cashbackDisabled: product.cashbackDisabled,
cashbackPercentage: product.cashbackPercentage,
cashbackPercentageFinal: product.cashbackPercentageFinal,
categories: product.categories || [],
countries: product.countries || [],
countryCode: product.countryCode || "",
currency: product.currency || "",
isRanged: product.isRanged,
range: product.range,
redemptionMethods: product.redemptionMethods || [],
slug: product.slug || "",
usageMethods: product.usageMethods || [],
usps: product.usps || [],
})),
};
}
}