206 lines
6.5 KiB
JavaScript
206 lines
6.5 KiB
JavaScript
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: {}
|
|
};
|
|
}
|
|
});
|
|
}
|
|
}
|