import fetch from 'node-fetch'; import { TimeManager } from '../utils/time.utils.js'; import { RedisService } from './redis.service.js'; const METRIC_IDS = { PLACED_ORDER: 'Y8cqcF' }; export class ReportingService { constructor(apiKey, apiRevision) { this.apiKey = apiKey; this.apiRevision = apiRevision; this.baseUrl = 'https://a.klaviyo.com/api'; this.timeManager = new TimeManager(); this.redisService = new RedisService(); this._pendingReportRequest = null; } async getCampaignReports(params = {}) { try { // Check if there's a pending request if (this._pendingReportRequest) { console.log('[ReportingService] Using pending campaign report request'); return this._pendingReportRequest; } // Try to get from cache first const cacheKey = this.redisService._getCacheKey('campaign_reports', params); let cachedData = null; try { cachedData = await this.redisService.get(`${cacheKey}:raw`); if (cachedData) { console.log('[ReportingService] Using cached campaign report data'); return cachedData; } } catch (cacheError) { console.warn('[ReportingService] Cache error:', cacheError); } // Create new request promise this._pendingReportRequest = (async () => { console.log('[ReportingService] Fetching fresh campaign report data'); const range = this.timeManager.getDateRange(params.timeRange || 'last30days'); const payload = { data: { type: "campaign-values-report", attributes: { timeframe: { start: range.start.toISO(), end: range.end.toISO() }, statistics: [ "delivery_rate", "delivered", "recipients", "open_rate", "opens_unique", "opens", "click_rate", "clicks_unique", "click_to_open_rate", "conversion_value", "conversion_uniques" ], conversion_metric_id: METRIC_IDS.PLACED_ORDER, filter: 'equals(send_channel,"email")' } } }; const response = await fetch(`${this.baseUrl}/campaign-values-reports`, { method: 'POST', headers: { 'Accept': 'application/json', 'Content-Type': 'application/json', 'Authorization': `Klaviyo-API-Key ${this.apiKey}`, 'revision': this.apiRevision }, body: JSON.stringify(payload) }); if (!response.ok) { const errorData = await response.json(); console.error('[ReportingService] API Error:', errorData); throw new Error(`Klaviyo API error: ${response.status} ${response.statusText}`); } const reportData = await response.json(); // Cache the response for 10 minutes try { await this.redisService.set(`${cacheKey}:raw`, reportData, 600); } catch (cacheError) { console.warn('[ReportingService] Cache set error:', cacheError); } return reportData; })(); const result = await this._pendingReportRequest; this._pendingReportRequest = null; return result; } catch (error) { console.error('[ReportingService] Error fetching campaign reports:', error); this._pendingReportRequest = null; throw error; } } async getCampaignDetails(campaignIds = []) { try { if (!Array.isArray(campaignIds) || campaignIds.length === 0) { return []; } // Process in batches of 5 to avoid rate limits const batchSize = 5; const campaignDetails = []; for (let i = 0; i < campaignIds.length; i += batchSize) { const batch = campaignIds.slice(i, i + batchSize); const batchPromises = batch.map(async (campaignId) => { try { // Try to get from cache first const cacheKey = this.redisService._getCacheKey('campaign_details', { campaignId }); const cachedData = await this.redisService.get(`${cacheKey}:raw`); if (cachedData) { return cachedData; } const response = await fetch( `${this.baseUrl}/campaigns/${campaignId}?include=campaign-messages`, { method: 'GET', headers: { 'Accept': 'application/json', 'Content-Type': 'application/json', 'Authorization': `Klaviyo-API-Key ${this.apiKey}`, 'revision': this.apiRevision } } ); if (!response.ok) { console.error(`Error fetching campaign ${campaignId}:`, response.statusText); return null; } const campaignData = await response.json(); // Cache individual campaign data for 1 hour await this.redisService.set(`${cacheKey}:raw`, campaignData, 3600); return campaignData; } catch (error) { console.error(`Error processing campaign ${campaignId}:`, error); return null; } }); const batchResults = await Promise.all(batchPromises); campaignDetails.push(...batchResults.filter(Boolean)); // Add delay between batches if not the last batch if (i + batchSize < campaignIds.length) { await new Promise(resolve => setTimeout(resolve, 1000)); } } return campaignDetails; } catch (error) { console.error('[ReportingService] Error fetching campaign details:', error); throw error; } } async getEnrichedCampaignReports(params = {}) { try { // Get campaign reports first const reportData = await this.getCampaignReports(params); // Extract campaign IDs from the report const campaignIds = reportData.data?.attributes?.results?.map( result => result.groupings?.campaign_id ).filter(Boolean) || []; // Get campaign details including messages const campaignDetails = await this.getCampaignDetails(campaignIds); // Merge report data with campaign details const enrichedData = reportData.data?.attributes?.results?.map(report => { const campaignId = report.groupings?.campaign_id; const campaignDetail = campaignDetails.find( detail => detail.data?.id === campaignId ); const campaignMessages = campaignDetail?.included?.filter( item => item.type === 'campaign-message' ) || []; return { id: campaignId, name: campaignDetail?.data?.attributes?.name || "Unnamed Campaign", subject: campaignMessages[0]?.attributes?.content?.subject || "", send_time: report.groupings?.send_time, stats: { delivery_rate: report.statistics?.delivery_rate || 0, delivered: report.statistics?.delivered || 0, recipients: report.statistics?.recipients || 0, open_rate: report.statistics?.open_rate || 0, opens_unique: report.statistics?.opens_unique || 0, opens: report.statistics?.opens || 0, click_rate: report.statistics?.click_rate || 0, clicks_unique: report.statistics?.clicks_unique || 0, click_to_open_rate: report.statistics?.click_to_open_rate || 0, conversion_value: report.statistics?.conversion_value || 0, conversion_uniques: report.statistics?.conversion_uniques || 0 } }; }).filter(Boolean) || []; return { data: enrichedData, meta: { total_count: enrichedData.length } }; } catch (error) { console.error('[ReportingService] Error getting enriched campaign reports:', error); throw error; } } }