import fetch from 'node-fetch'; import { TimeManager } from '../utils/time.utils.js'; import { RedisService } from './redis.service.js'; export class CampaignsService { constructor(apiKey, apiRevision) { this.apiKey = apiKey; this.apiRevision = apiRevision; this.baseUrl = 'https://a.klaviyo.com/api'; this.timeManager = new TimeManager(); this.redisService = new RedisService(); } async getCampaigns(params = {}) { try { // Add request debouncing const requestKey = JSON.stringify(params); if (this._pendingRequests && this._pendingRequests[requestKey]) { return this._pendingRequests[requestKey]; } // Try to get from cache first const cacheKey = this.redisService._getCacheKey('campaigns', params); let cachedData = null; try { cachedData = await this.redisService.get(`${cacheKey}:raw`); if (cachedData) { return cachedData; } } catch (cacheError) { console.warn('[CampaignsService] Cache error:', cacheError); } this._pendingRequests = this._pendingRequests || {}; this._pendingRequests[requestKey] = (async () => { let allCampaigns = []; let nextCursor = params.pageCursor; let pageCount = 0; const filter = params.filter || this._buildFilter(params); do { const queryParams = new URLSearchParams(); if (filter) { queryParams.append('filter', filter); } queryParams.append('sort', params.sort || '-send_time'); if (nextCursor) { queryParams.append('page[cursor]', nextCursor); } const url = `${this.baseUrl}/campaigns?${queryParams.toString()}`; try { const response = await fetch(url, { method: 'GET', headers: { 'Accept': 'application/json', 'Content-Type': 'application/json', 'Authorization': `Klaviyo-API-Key ${this.apiKey}`, 'revision': this.apiRevision } }); if (!response.ok) { const errorData = await response.json(); console.error('[CampaignsService] API Error:', errorData); throw new Error(`Klaviyo API error: ${response.status} ${response.statusText}`); } const responseData = await response.json(); allCampaigns = allCampaigns.concat(responseData.data || []); pageCount++; nextCursor = responseData.links?.next ? new URL(responseData.links.next).searchParams.get('page[cursor]') : null; if (nextCursor) { await new Promise(resolve => setTimeout(resolve, 50)); } } catch (fetchError) { console.error('[CampaignsService] Fetch error:', fetchError); throw fetchError; } } while (nextCursor); const transformedCampaigns = this._transformCampaigns(allCampaigns); const result = { data: transformedCampaigns, meta: { total_count: transformedCampaigns.length, page_count: pageCount } }; try { const ttl = this.redisService._getTTL(params.timeRange); await this.redisService.set(`${cacheKey}:raw`, result, ttl); } catch (cacheError) { console.warn('[CampaignsService] Cache set error:', cacheError); } delete this._pendingRequests[requestKey]; return result; })(); return await this._pendingRequests[requestKey]; } catch (error) { console.error('[CampaignsService] Error fetching campaigns:', error); throw error; } } _buildFilter(params) { const filters = []; if (params.startDate && params.endDate) { const startUtc = this.timeManager.formatForAPI(params.startDate); const endUtc = this.timeManager.formatForAPI(params.endDate); filters.push(`greater-or-equal(send_time,${startUtc})`); filters.push(`less-than(send_time,${endUtc})`); } if (params.status) { filters.push(`equals(status,"${params.status}")`); } if (params.customFilters) { filters.push(...params.customFilters); } return filters.length > 0 ? (filters.length > 1 ? `and(${filters.join(',')})` : filters[0]) : null; } async getCampaignsByTimeRange(timeRange, options = {}) { const range = this.timeManager.getDateRange(timeRange); if (!range) { throw new Error('Invalid time range specified'); } const params = { timeRange, startDate: range.start.toISO(), endDate: range.end.toISO(), ...options }; // Try to get from cache first const cacheKey = this.redisService._getCacheKey('campaigns', params); let cachedData = null; try { cachedData = await this.redisService.get(`${cacheKey}:raw`); if (cachedData) { return cachedData; } } catch (cacheError) { console.warn('[CampaignsService] Cache error:', cacheError); } return this.getCampaigns(params); } _transformCampaigns(campaigns) { if (!Array.isArray(campaigns)) { console.warn('[CampaignsService] Campaigns is not an array:', campaigns); return []; } return campaigns.map(campaign => { try { const stats = campaign.attributes?.campaign_message?.stats || {}; return { id: campaign.id, name: campaign.attributes?.name || "Unnamed Campaign", subject: campaign.attributes?.campaign_message?.subject || "", send_time: campaign.attributes?.send_time, stats: { delivery_rate: stats.delivery_rate || 0, delivered: stats.delivered || 0, recipients: stats.recipients || 0, open_rate: stats.open_rate || 0, opens_unique: stats.opens_unique || 0, opens: stats.opens || 0, clicks_unique: stats.clicks_unique || 0, click_rate: stats.click_rate || 0, click_to_open_rate: stats.click_to_open_rate || 0, conversion_value: stats.conversion_value || 0, conversion_uniques: stats.conversion_uniques || 0 } }; } catch (error) { console.error('[CampaignsService] Error transforming campaign:', error, campaign); return { id: campaign.id || 'unknown', name: 'Error Processing Campaign', stats: {} }; } }); } }