Get some data in

This commit is contained in:
2024-12-22 14:51:13 -05:00
parent 4619d24e84
commit 03d56c5f51
3 changed files with 233 additions and 213 deletions

View File

@@ -1294,38 +1294,70 @@ export class EventsService {
return events.map(event => {
try {
// Extract metric ID from the relationships field if it exists
// Extract metric ID from all possible locations
const metricId = event.relationships?.metric?.data?.id ||
event.attributes?.metric?.id ||
event.attributes?.metric_id;
// Extract properties from all possible locations in the Klaviyo event structure
const eventProps = {
// Extract properties from all possible locations
const rawProps = {
...(event.attributes?.event_properties || {}),
...(event.attributes?.properties || {}),
...(event.attributes?.profile || {}),
value: event.attributes?.value,
datetime: event.attributes?.datetime
};
// Normalize shipping data
const shippingData = {
name: eventProps.ShippingName || eventProps.shipping_name || eventProps.shipping?.name,
street1: eventProps.ShippingStreet1 || eventProps.shipping_street1 || eventProps.shipping?.street1,
street2: eventProps.ShippingStreet2 || eventProps.shipping_street2 || eventProps.shipping?.street2,
city: eventProps.ShippingCity || eventProps.shipping_city || eventProps.shipping?.city,
state: eventProps.ShippingState || eventProps.shipping_state || eventProps.shipping?.state,
zip: eventProps.ShippingZip || eventProps.shipping_zip || eventProps.shipping?.zip,
country: eventProps.ShippingCountry || eventProps.shipping_country || eventProps.shipping?.country,
method: eventProps.ShipMethod || eventProps.shipping_method || eventProps.shipping?.method,
tracking: eventProps.TrackingNumber || eventProps.tracking_number
name: rawProps.ShippingName || rawProps.shipping_name || rawProps.shipping?.name,
street1: rawProps.ShippingStreet1 || rawProps.shipping_street1 || rawProps.shipping?.street1,
street2: rawProps.ShippingStreet2 || rawProps.shipping_street2 || rawProps.shipping?.street2,
city: rawProps.ShippingCity || rawProps.shipping_city || rawProps.shipping?.city,
state: rawProps.ShippingState || rawProps.shipping_state || rawProps.shipping?.state,
zip: rawProps.ShippingZip || rawProps.shipping_zip || rawProps.shipping?.zip,
country: rawProps.ShippingCountry || rawProps.shipping_country || rawProps.shipping?.country,
method: rawProps.ShipMethod || rawProps.shipping_method || rawProps.shipping?.method,
tracking: rawProps.TrackingNumber || rawProps.tracking_number
};
// Normalize payment data
const paymentData = {
method: rawProps.PaymentMethod || rawProps.payment_method || rawProps.payment?.method,
name: rawProps.PaymentName || rawProps.payment_name || rawProps.payment?.name,
amount: Number(rawProps.PaymentAmount || rawProps.payment_amount || rawProps.payment?.amount || 0)
};
// Normalize order flags
const orderFlags = {
type: rawProps.OrderType || rawProps.order_type || 'standard',
hasPreorder: Boolean(rawProps.HasPreorder || rawProps.has_preorder || rawProps.preorder),
localPickup: Boolean(rawProps.LocalPickup || rawProps.local_pickup || rawProps.pickup),
isOnHold: Boolean(rawProps.IsOnHold || rawProps.is_on_hold || rawProps.on_hold),
hasDigiItem: Boolean(rawProps.HasDigiItem || rawProps.has_digital_item || rawProps.digital_item),
hasNotions: Boolean(rawProps.HasNotions || rawProps.has_notions || rawProps.notions),
hasDigitalGC: Boolean(rawProps.HasDigitalGC || rawProps.has_digital_gc || rawProps.gift_card),
stillOwes: Boolean(rawProps.StillOwes || rawProps.still_owes || rawProps.balance_due)
};
// Normalize refund/cancel data
const refundData = {
reason: rawProps.CancelReason || rawProps.cancel_reason || rawProps.reason,
message: rawProps.CancelMessage || rawProps.cancel_message || rawProps.message,
orderMessage: rawProps.OrderMessage || rawProps.order_message || rawProps.note
};
// Transform items
const items = this._transformItems(rawProps.Items || rawProps.items || rawProps.line_items || []);
// Calculate totals
const totalAmount = Number(rawProps.TotalAmount || rawProps.PaymentAmount || rawProps.total_amount || rawProps.value || 0);
const itemCount = items.reduce((sum, item) => sum + Number(item.Quantity || item.QuantityOrdered || 1), 0);
const transformed = {
id: event.id,
type: event.type,
metric_id: metricId,
// Preserve the original attributes structure for compatibility
attributes: {
...event.attributes,
datetime: event.attributes?.datetime,
@@ -1337,42 +1369,29 @@ export class EventsService {
},
relationships: event.relationships,
event_properties: {
...eventProps,
// Transform common properties
EmailAddress: eventProps.EmailAddress || eventProps.email,
FirstName: eventProps.FirstName || eventProps.first_name,
LastName: eventProps.LastName || eventProps.last_name,
OrderId: eventProps.OrderId || eventProps.FromOrder || eventProps.order_id,
TotalAmount: Number(eventProps.TotalAmount || eventProps.PaymentAmount || eventProps.total_amount || eventProps.value || 0),
Items: this._transformItems(eventProps.Items || eventProps.items || eventProps.line_items || []),
// Add normalized shipping information
ShippingName: shippingData.name,
ShippingStreet1: shippingData.street1,
ShippingStreet2: shippingData.street2,
ShippingCity: shippingData.city,
ShippingState: shippingData.state,
ShippingZip: shippingData.zip,
ShippingCountry: shippingData.country,
ShippingMethod: shippingData.method,
TrackingNumber: shippingData.tracking,
ShipMethod: shippingData.method,
// Add payment information
PaymentMethod: eventProps.PaymentMethod || eventProps.payment_method || eventProps.payment?.method,
PaymentName: eventProps.PaymentName || eventProps.payment_name || eventProps.payment?.name,
PaymentAmount: Number(eventProps.PaymentAmount || eventProps.payment_amount || eventProps.payment?.amount || 0),
// Add order flags
OrderType: eventProps.OrderType || eventProps.order_type || 'standard',
HasPreorder: Boolean(eventProps.HasPreorder || eventProps.has_preorder || eventProps.preorder),
LocalPickup: Boolean(eventProps.LocalPickup || eventProps.local_pickup || eventProps.pickup),
IsOnHold: Boolean(eventProps.IsOnHold || eventProps.is_on_hold || eventProps.on_hold),
HasDigiItem: Boolean(eventProps.HasDigiItem || eventProps.has_digital_item || eventProps.digital_item),
HasNotions: Boolean(eventProps.HasNotions || eventProps.has_notions || eventProps.notions),
HasDigitalGC: Boolean(eventProps.HasDigitalGC || eventProps.has_digital_gc || eventProps.gift_card),
StillOwes: Boolean(eventProps.StillOwes || eventProps.still_owes || eventProps.balance_due),
// Add refund/cancel information
CancelReason: eventProps.CancelReason || eventProps.cancel_reason || eventProps.reason,
CancelMessage: eventProps.CancelMessage || eventProps.cancel_message || eventProps.message,
OrderMessage: eventProps.OrderMessage || eventProps.order_message || eventProps.note
// Basic properties
EmailAddress: rawProps.EmailAddress || rawProps.email,
FirstName: rawProps.FirstName || rawProps.first_name,
LastName: rawProps.LastName || rawProps.last_name,
OrderId: rawProps.OrderId || rawProps.FromOrder || rawProps.order_id,
TotalAmount: totalAmount,
ItemCount: itemCount,
Items: items,
// Shipping information
...shippingData,
// Payment information
...paymentData,
// Order flags
...orderFlags,
// Refund/cancel information
...refundData,
// Original properties (for backward compatibility)
...rawProps
}
};
@@ -1398,23 +1417,55 @@ export class EventsService {
}
return items.map(item => {
const transformed = {
...item,
ProductID: item.ProductID || item.product_id,
ProductName: item.ProductName || item.product_name,
SKU: item.SKU || item.sku,
Brand: item.Brand || item.brand,
Categories: item.Categories || item.categories || [],
ItemPrice: Number(item.ItemPrice || item.item_price || 0),
Quantity: Number(item.Quantity || item.QuantityOrdered || item.quantity || 1),
QuantityOrdered: Number(item.QuantityOrdered || item.Quantity || item.quantity_ordered || 1),
QuantitySent: Number(item.QuantitySent || item.quantity_sent || 0),
QuantityBackordered: Number(item.QuantityBackordered || item.quantity_backordered || 0),
RowTotal: Number(item.RowTotal || item.row_total || (item.ItemPrice * (item.Quantity || item.QuantityOrdered || 1))),
ItemStatus: item.ItemStatus || item.item_status || 'In Stock',
ImgThumb: item.ImgThumb || item.img_thumb
};
return transformed;
try {
const quantity = Number(item.Quantity || item.QuantityOrdered || item.quantity || item.quantity_ordered || 1);
const price = Number(item.ItemPrice || item.item_price || item.price || 0);
const rowTotal = Number(item.RowTotal || item.row_total || (price * quantity) || 0);
const transformed = {
// Basic item information
ProductID: item.ProductID || item.product_id || item.id,
ProductName: item.ProductName || item.product_name || item.name,
SKU: item.SKU || item.sku,
Brand: item.Brand || item.brand,
Categories: Array.isArray(item.Categories) ? item.Categories :
Array.isArray(item.categories) ? item.categories : [],
// Pricing
ItemPrice: price,
RowTotal: rowTotal,
// Quantities
Quantity: quantity,
QuantityOrdered: quantity,
QuantitySent: Number(item.QuantitySent || item.quantity_sent || 0),
QuantityBackordered: Number(item.QuantityBackordered || item.quantity_backordered || 0),
// Status and images
ItemStatus: item.ItemStatus || item.item_status || item.status || 'In Stock',
ImgThumb: item.ImgThumb || item.img_thumb || item.thumbnail,
// Additional properties
IsPreorder: Boolean(item.IsPreorder || item.is_preorder || item.preorder),
IsDigital: Boolean(item.IsDigital || item.is_digital || item.digital),
IsGiftCard: Boolean(item.IsGiftCard || item.is_gift_card || item.gift_card),
// Original properties (for backward compatibility)
...item
};
return transformed;
} catch (error) {
console.error('[EventsService] Error transforming item:', error, item);
// Return a minimal valid item structure
return {
ProductID: item.ProductID || item.product_id || 'unknown',
ProductName: item.ProductName || item.product_name || 'Unknown Product',
Quantity: 1,
ItemPrice: 0,
RowTotal: 0
};
}
});
}
@@ -1439,128 +1490,67 @@ export class EventsService {
prevPeriodEnd = periodStart.minus({ milliseconds: 1 });
prevPeriodStart = prevPeriodEnd.minus(duration);
} else if (params.timeRange) {
// Handle both current and previous period time ranges
const timeRange = params.timeRange;
const isPreviousPeriod = timeRange.startsWith('previous');
const normalizedTimeRange = isPreviousPeriod ? timeRange.replace('previous', 'last') : timeRange;
const range = this.timeManager.getDateRange(params.timeRange);
const prevRange = this.timeManager.getPreviousPeriod(params.timeRange);
console.log('[EventsService] Time range details:', {
originalTimeRange: timeRange,
isPreviousPeriod,
normalizedTimeRange
});
// Get current period range
const range = this.timeManager.getDateRange(normalizedTimeRange);
if (!range) {
throw new Error(`Invalid time range specified: ${timeRange}`);
}
// Get previous period range using TimeManager
const prevRange = this.timeManager.getPreviousPeriod(normalizedTimeRange);
if (!prevRange) {
throw new Error(`Could not calculate previous period for: ${timeRange}`);
if (!range || !prevRange) {
throw new Error(`Invalid time range specified: ${params.timeRange}`);
}
periodStart = range.start;
periodEnd = range.end;
prevPeriodStart = prevRange.start;
prevPeriodEnd = prevRange.end;
console.log('[EventsService] Calculated date ranges:', {
timeRange,
current: {
start: periodStart.toISO(),
end: periodEnd.toISO(),
duration: periodEnd.diff(periodStart).as('days')
},
previous: {
start: prevPeriodStart.toISO(),
end: prevPeriodEnd.toISO(),
duration: prevPeriodEnd.diff(prevPeriodStart).as('days')
}
});
}
// Load both current and previous period data
console.log('[EventsService] Fetching events with params:', {
current: {
startDate: periodStart.toISO(),
endDate: periodEnd.toISO(),
metricId: METRIC_IDS.PLACED_ORDER,
...params
},
previous: {
startDate: prevPeriodStart.toISO(),
endDate: prevPeriodEnd.toISO(),
metricId: METRIC_IDS.PLACED_ORDER
}
});
const [currentResponse, prevResponse] = await Promise.all([
this.getEvents({
...params,
startDate: periodStart.toISO(),
endDate: periodEnd.toISO(),
metricId: METRIC_IDS.PLACED_ORDER,
customFilters: params.metric === 'pre_orders' ? ['equals(event_properties.HasPreorder,true)'] :
params.metric === 'local_pickup' ? ['equals(event_properties.LocalPickup,true)'] :
params.metric === 'on_hold' ? ['equals(event_properties.IsOnHold,true)'] : undefined
metricId: METRIC_IDS.PLACED_ORDER
}),
this.getEvents({
..._.omit(params, ['timeRange', 'startDate', 'endDate']),
startDate: prevPeriodStart.toISO(),
endDate: prevPeriodEnd.toISO(),
metricId: METRIC_IDS.PLACED_ORDER,
timeRange: undefined,
isPreviousPeriod: true,
cacheKey: `prev_${prevPeriodStart.toISO()}_${prevPeriodEnd.toISO()}`,
customFilters: params.metric === 'pre_orders' ? ['equals(event_properties.HasPreorder,true)'] :
params.metric === 'local_pickup' ? ['equals(event_properties.LocalPickup,true)'] :
params.metric === 'on_hold' ? ['equals(event_properties.IsOnHold,true)'] : undefined
metricId: METRIC_IDS.PLACED_ORDER
})
]);
// Add debug logging for request params and filters
console.log('[EventsService] Request details with filters:', {
current: {
params: {
...params,
startDate: periodStart.toISO(),
endDate: periodEnd.toISO(),
customFilters: params.metric === 'pre_orders' ? ['equals(event_properties.HasPreorder,true)'] :
params.metric === 'local_pickup' ? ['equals(event_properties.LocalPickup,true)'] :
params.metric === 'on_hold' ? ['equals(event_properties.IsOnHold,true)'] : undefined
},
responseLength: currentResponse?.data?.length
},
previous: {
params: {
..._.omit(params, ['timeRange', 'startDate', 'endDate']),
startDate: prevPeriodStart.toISO(),
endDate: prevPeriodEnd.toISO(),
customFilters: params.metric === 'pre_orders' ? ['equals(event_properties.HasPreorder,true)'] :
params.metric === 'local_pickup' ? ['equals(event_properties.LocalPickup,true)'] :
params.metric === 'on_hold' ? ['equals(event_properties.IsOnHold,true)'] : undefined
},
responseLength: prevResponse?.data?.length
}
});
// Transform events
const currentEvents = this._transformEvents(currentResponse.data || []);
const prevEvents = this._transformEvents(prevResponse.data || []);
console.log('[EventsService] Transformed events:', {
current: {
count: currentEvents.length,
revenue: currentEvents.reduce((sum, event) => sum + (Number(event.event_properties?.TotalAmount) || 0), 0)
},
previous: {
count: prevEvents.length,
revenue: prevEvents.reduce((sum, event) => sum + (Number(event.event_properties?.TotalAmount) || 0), 0)
// Filter events based on metric type
const filterEvents = (events) => {
switch (metric) {
case 'pre_orders':
return events.filter(event =>
Boolean(event.event_properties?.HasPreorder ||
event.event_properties?.has_preorder ||
event.event_properties?.preorder)
);
case 'local_pickup':
return events.filter(event =>
Boolean(event.event_properties?.LocalPickup ||
event.event_properties?.local_pickup ||
event.event_properties?.pickup)
);
case 'on_hold':
return events.filter(event =>
Boolean(event.event_properties?.IsOnHold ||
event.event_properties?.is_on_hold ||
event.event_properties?.on_hold)
);
default:
return events;
}
});
};
const filteredCurrentEvents = filterEvents(currentEvents);
const filteredPrevEvents = filterEvents(prevEvents);
// Initialize daily stats map with all dates in range
const dailyStats = new Map();
@@ -1575,6 +1565,10 @@ export class EventsService {
itemCount: 0,
averageOrderValue: 0,
averageItemsPerOrder: 0,
count: 0,
value: 0,
percentage: 0,
totalOrders: 0,
prevRevenue: 0,
prevOrders: 0,
prevItemCount: 0,
@@ -1583,8 +1577,11 @@ export class EventsService {
currentDate = currentDate.plus({ days: 1 });
}
// Get total orders for the period (needed for percentages)
const totalOrders = currentEvents.length;
// Process current period events
for (const event of currentEvents) {
for (const event of filteredCurrentEvents) {
const datetime = this.timeManager.toDateTime(event.attributes?.datetime);
if (!datetime) continue;
@@ -1599,6 +1596,10 @@ export class EventsService {
dayStats.revenue += totalAmount;
dayStats.orders++;
dayStats.itemCount += items.length;
dayStats.value += totalAmount;
dayStats.count++;
dayStats.totalOrders = totalOrders;
dayStats.percentage = (dayStats.count / totalOrders) * 100;
dayStats.averageOrderValue = dayStats.revenue / dayStats.orders;
dayStats.averageItemsPerOrder = dayStats.itemCount / dayStats.orders;
}
@@ -1613,13 +1614,18 @@ export class EventsService {
timestamp: dateKey,
revenue: 0,
orders: 0,
itemCount: 0
itemCount: 0,
value: 0,
count: 0
});
prevDate = prevDate.plus({ days: 1 });
}
// Aggregate previous period data
for (const event of prevEvents) {
// Get total orders for previous period
const prevTotalOrders = prevEvents.length;
// Process previous period events
for (const event of filteredPrevEvents) {
const datetime = this.timeManager.toDateTime(event.attributes?.datetime);
if (!datetime) continue;
@@ -1634,21 +1640,15 @@ export class EventsService {
dayStats.revenue += totalAmount;
dayStats.orders++;
dayStats.itemCount += items.length;
prevDailyStats.set(dateKey, dayStats);
dayStats.value += totalAmount;
dayStats.count++;
dayStats.percentage = (dayStats.count / prevTotalOrders) * 100;
}
// Map previous period data to current period days
const prevPeriodDays = Array.from(prevDailyStats.values()).sort((a, b) => a.date.localeCompare(b.date));
const currentPeriodDays = Array.from(dailyStats.values()).sort((a, b) => a.date.localeCompare(b.date));
// Add debug logging for data before mapping
console.log('[EventsService] Data before mapping:', {
currentPeriod: currentPeriodDays.slice(0, 3),
previousPeriod: prevPeriodDays.slice(0, 3),
currentLength: currentPeriodDays.length,
prevLength: prevPeriodDays.length
});
// Map the data using array indices
for (let i = 0; i < currentPeriodDays.length && i < prevPeriodDays.length; i++) {
const currentDayStats = currentPeriodDays[i];
@@ -1660,19 +1660,15 @@ export class EventsService {
dayStats.prevRevenue = prevDayStats.revenue;
dayStats.prevOrders = prevDayStats.orders;
dayStats.prevItemCount = prevDayStats.itemCount;
dayStats.prevValue = prevDayStats.value;
dayStats.prevCount = prevDayStats.count;
dayStats.prevPercentage = prevDayStats.percentage;
dayStats.prevAvgOrderValue = prevDayStats.orders > 0 ? prevDayStats.revenue / prevDayStats.orders : 0;
dailyStats.set(currentDayStats.timestamp, dayStats);
}
}
}
// Add debug logging for mapped data
console.log('[EventsService] Sample of mapped data:', {
firstDay: dailyStats.get(currentPeriodDays[0]?.timestamp),
lastDay: dailyStats.get(currentPeriodDays[currentPeriodDays.length - 1]?.timestamp),
totalDays: dailyStats.size
});
// Convert to array and sort by date
const stats = Array.from(dailyStats.values())
.sort((a, b) => a.date.localeCompare(b.date))
@@ -1683,9 +1679,16 @@ export class EventsService {
itemCount: Number(day.itemCount || 0),
averageOrderValue: Number(day.averageOrderValue || 0),
averageItemsPerOrder: Number(day.averageItemsPerOrder || 0),
count: Number(day.count || 0),
value: Number(day.value || 0),
percentage: Number(day.percentage || 0),
totalOrders: Number(day.totalOrders || 0),
prevRevenue: Number(day.prevRevenue || 0),
prevOrders: Number(day.prevOrders || 0),
prevItemCount: Number(day.prevItemCount || 0),
prevValue: Number(day.prevValue || 0),
prevCount: Number(day.prevCount || 0),
prevPercentage: Number(day.prevPercentage || 0),
prevAvgOrderValue: Number(day.prevAvgOrderValue || 0)
}));