Get some data in
This commit is contained in:
@@ -1294,13 +1294,13 @@ export class EventsService {
|
|||||||
|
|
||||||
return events.map(event => {
|
return events.map(event => {
|
||||||
try {
|
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 ||
|
const metricId = event.relationships?.metric?.data?.id ||
|
||||||
event.attributes?.metric?.id ||
|
event.attributes?.metric?.id ||
|
||||||
event.attributes?.metric_id;
|
event.attributes?.metric_id;
|
||||||
|
|
||||||
// Extract properties from all possible locations in the Klaviyo event structure
|
// Extract properties from all possible locations
|
||||||
const eventProps = {
|
const rawProps = {
|
||||||
...(event.attributes?.event_properties || {}),
|
...(event.attributes?.event_properties || {}),
|
||||||
...(event.attributes?.properties || {}),
|
...(event.attributes?.properties || {}),
|
||||||
...(event.attributes?.profile || {}),
|
...(event.attributes?.profile || {}),
|
||||||
@@ -1310,22 +1310,54 @@ export class EventsService {
|
|||||||
|
|
||||||
// Normalize shipping data
|
// Normalize shipping data
|
||||||
const shippingData = {
|
const shippingData = {
|
||||||
name: eventProps.ShippingName || eventProps.shipping_name || eventProps.shipping?.name,
|
name: rawProps.ShippingName || rawProps.shipping_name || rawProps.shipping?.name,
|
||||||
street1: eventProps.ShippingStreet1 || eventProps.shipping_street1 || eventProps.shipping?.street1,
|
street1: rawProps.ShippingStreet1 || rawProps.shipping_street1 || rawProps.shipping?.street1,
|
||||||
street2: eventProps.ShippingStreet2 || eventProps.shipping_street2 || eventProps.shipping?.street2,
|
street2: rawProps.ShippingStreet2 || rawProps.shipping_street2 || rawProps.shipping?.street2,
|
||||||
city: eventProps.ShippingCity || eventProps.shipping_city || eventProps.shipping?.city,
|
city: rawProps.ShippingCity || rawProps.shipping_city || rawProps.shipping?.city,
|
||||||
state: eventProps.ShippingState || eventProps.shipping_state || eventProps.shipping?.state,
|
state: rawProps.ShippingState || rawProps.shipping_state || rawProps.shipping?.state,
|
||||||
zip: eventProps.ShippingZip || eventProps.shipping_zip || eventProps.shipping?.zip,
|
zip: rawProps.ShippingZip || rawProps.shipping_zip || rawProps.shipping?.zip,
|
||||||
country: eventProps.ShippingCountry || eventProps.shipping_country || eventProps.shipping?.country,
|
country: rawProps.ShippingCountry || rawProps.shipping_country || rawProps.shipping?.country,
|
||||||
method: eventProps.ShipMethod || eventProps.shipping_method || eventProps.shipping?.method,
|
method: rawProps.ShipMethod || rawProps.shipping_method || rawProps.shipping?.method,
|
||||||
tracking: eventProps.TrackingNumber || eventProps.tracking_number
|
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 = {
|
const transformed = {
|
||||||
id: event.id,
|
id: event.id,
|
||||||
type: event.type,
|
type: event.type,
|
||||||
metric_id: metricId,
|
metric_id: metricId,
|
||||||
// Preserve the original attributes structure for compatibility
|
|
||||||
attributes: {
|
attributes: {
|
||||||
...event.attributes,
|
...event.attributes,
|
||||||
datetime: event.attributes?.datetime,
|
datetime: event.attributes?.datetime,
|
||||||
@@ -1337,42 +1369,29 @@ export class EventsService {
|
|||||||
},
|
},
|
||||||
relationships: event.relationships,
|
relationships: event.relationships,
|
||||||
event_properties: {
|
event_properties: {
|
||||||
...eventProps,
|
// Basic properties
|
||||||
// Transform common properties
|
EmailAddress: rawProps.EmailAddress || rawProps.email,
|
||||||
EmailAddress: eventProps.EmailAddress || eventProps.email,
|
FirstName: rawProps.FirstName || rawProps.first_name,
|
||||||
FirstName: eventProps.FirstName || eventProps.first_name,
|
LastName: rawProps.LastName || rawProps.last_name,
|
||||||
LastName: eventProps.LastName || eventProps.last_name,
|
OrderId: rawProps.OrderId || rawProps.FromOrder || rawProps.order_id,
|
||||||
OrderId: eventProps.OrderId || eventProps.FromOrder || eventProps.order_id,
|
TotalAmount: totalAmount,
|
||||||
TotalAmount: Number(eventProps.TotalAmount || eventProps.PaymentAmount || eventProps.total_amount || eventProps.value || 0),
|
ItemCount: itemCount,
|
||||||
Items: this._transformItems(eventProps.Items || eventProps.items || eventProps.line_items || []),
|
Items: items,
|
||||||
// Add normalized shipping information
|
|
||||||
ShippingName: shippingData.name,
|
// Shipping information
|
||||||
ShippingStreet1: shippingData.street1,
|
...shippingData,
|
||||||
ShippingStreet2: shippingData.street2,
|
|
||||||
ShippingCity: shippingData.city,
|
// Payment information
|
||||||
ShippingState: shippingData.state,
|
...paymentData,
|
||||||
ShippingZip: shippingData.zip,
|
|
||||||
ShippingCountry: shippingData.country,
|
// Order flags
|
||||||
ShippingMethod: shippingData.method,
|
...orderFlags,
|
||||||
TrackingNumber: shippingData.tracking,
|
|
||||||
ShipMethod: shippingData.method,
|
// Refund/cancel information
|
||||||
// Add payment information
|
...refundData,
|
||||||
PaymentMethod: eventProps.PaymentMethod || eventProps.payment_method || eventProps.payment?.method,
|
|
||||||
PaymentName: eventProps.PaymentName || eventProps.payment_name || eventProps.payment?.name,
|
// Original properties (for backward compatibility)
|
||||||
PaymentAmount: Number(eventProps.PaymentAmount || eventProps.payment_amount || eventProps.payment?.amount || 0),
|
...rawProps
|
||||||
// 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
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -1398,23 +1417,55 @@ export class EventsService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return items.map(item => {
|
return items.map(item => {
|
||||||
const transformed = {
|
try {
|
||||||
...item,
|
const quantity = Number(item.Quantity || item.QuantityOrdered || item.quantity || item.quantity_ordered || 1);
|
||||||
ProductID: item.ProductID || item.product_id,
|
const price = Number(item.ItemPrice || item.item_price || item.price || 0);
|
||||||
ProductName: item.ProductName || item.product_name,
|
const rowTotal = Number(item.RowTotal || item.row_total || (price * quantity) || 0);
|
||||||
SKU: item.SKU || item.sku,
|
|
||||||
Brand: item.Brand || item.brand,
|
const transformed = {
|
||||||
Categories: item.Categories || item.categories || [],
|
// Basic item information
|
||||||
ItemPrice: Number(item.ItemPrice || item.item_price || 0),
|
ProductID: item.ProductID || item.product_id || item.id,
|
||||||
Quantity: Number(item.Quantity || item.QuantityOrdered || item.quantity || 1),
|
ProductName: item.ProductName || item.product_name || item.name,
|
||||||
QuantityOrdered: Number(item.QuantityOrdered || item.Quantity || item.quantity_ordered || 1),
|
SKU: item.SKU || item.sku,
|
||||||
QuantitySent: Number(item.QuantitySent || item.quantity_sent || 0),
|
Brand: item.Brand || item.brand,
|
||||||
QuantityBackordered: Number(item.QuantityBackordered || item.quantity_backordered || 0),
|
Categories: Array.isArray(item.Categories) ? item.Categories :
|
||||||
RowTotal: Number(item.RowTotal || item.row_total || (item.ItemPrice * (item.Quantity || item.QuantityOrdered || 1))),
|
Array.isArray(item.categories) ? item.categories : [],
|
||||||
ItemStatus: item.ItemStatus || item.item_status || 'In Stock',
|
|
||||||
ImgThumb: item.ImgThumb || item.img_thumb
|
// Pricing
|
||||||
};
|
ItemPrice: price,
|
||||||
return transformed;
|
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 });
|
prevPeriodEnd = periodStart.minus({ milliseconds: 1 });
|
||||||
prevPeriodStart = prevPeriodEnd.minus(duration);
|
prevPeriodStart = prevPeriodEnd.minus(duration);
|
||||||
} else if (params.timeRange) {
|
} else if (params.timeRange) {
|
||||||
// Handle both current and previous period time ranges
|
const range = this.timeManager.getDateRange(params.timeRange);
|
||||||
const timeRange = params.timeRange;
|
const prevRange = this.timeManager.getPreviousPeriod(params.timeRange);
|
||||||
const isPreviousPeriod = timeRange.startsWith('previous');
|
|
||||||
const normalizedTimeRange = isPreviousPeriod ? timeRange.replace('previous', 'last') : timeRange;
|
|
||||||
|
|
||||||
console.log('[EventsService] Time range details:', {
|
if (!range || !prevRange) {
|
||||||
originalTimeRange: timeRange,
|
throw new Error(`Invalid time range specified: ${params.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}`);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
periodStart = range.start;
|
periodStart = range.start;
|
||||||
periodEnd = range.end;
|
periodEnd = range.end;
|
||||||
prevPeriodStart = prevRange.start;
|
prevPeriodStart = prevRange.start;
|
||||||
prevPeriodEnd = prevRange.end;
|
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
|
// 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([
|
const [currentResponse, prevResponse] = await Promise.all([
|
||||||
this.getEvents({
|
this.getEvents({
|
||||||
...params,
|
...params,
|
||||||
startDate: periodStart.toISO(),
|
startDate: periodStart.toISO(),
|
||||||
endDate: periodEnd.toISO(),
|
endDate: periodEnd.toISO(),
|
||||||
metricId: METRIC_IDS.PLACED_ORDER,
|
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
|
|
||||||
}),
|
}),
|
||||||
this.getEvents({
|
this.getEvents({
|
||||||
..._.omit(params, ['timeRange', 'startDate', 'endDate']),
|
..._.omit(params, ['timeRange', 'startDate', 'endDate']),
|
||||||
startDate: prevPeriodStart.toISO(),
|
startDate: prevPeriodStart.toISO(),
|
||||||
endDate: prevPeriodEnd.toISO(),
|
endDate: prevPeriodEnd.toISO(),
|
||||||
metricId: METRIC_IDS.PLACED_ORDER,
|
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
|
|
||||||
})
|
})
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// 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
|
// Transform events
|
||||||
const currentEvents = this._transformEvents(currentResponse.data || []);
|
const currentEvents = this._transformEvents(currentResponse.data || []);
|
||||||
const prevEvents = this._transformEvents(prevResponse.data || []);
|
const prevEvents = this._transformEvents(prevResponse.data || []);
|
||||||
|
|
||||||
console.log('[EventsService] Transformed events:', {
|
// Filter events based on metric type
|
||||||
current: {
|
const filterEvents = (events) => {
|
||||||
count: currentEvents.length,
|
switch (metric) {
|
||||||
revenue: currentEvents.reduce((sum, event) => sum + (Number(event.event_properties?.TotalAmount) || 0), 0)
|
case 'pre_orders':
|
||||||
},
|
return events.filter(event =>
|
||||||
previous: {
|
Boolean(event.event_properties?.HasPreorder ||
|
||||||
count: prevEvents.length,
|
event.event_properties?.has_preorder ||
|
||||||
revenue: prevEvents.reduce((sum, event) => sum + (Number(event.event_properties?.TotalAmount) || 0), 0)
|
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
|
// Initialize daily stats map with all dates in range
|
||||||
const dailyStats = new Map();
|
const dailyStats = new Map();
|
||||||
@@ -1575,6 +1565,10 @@ export class EventsService {
|
|||||||
itemCount: 0,
|
itemCount: 0,
|
||||||
averageOrderValue: 0,
|
averageOrderValue: 0,
|
||||||
averageItemsPerOrder: 0,
|
averageItemsPerOrder: 0,
|
||||||
|
count: 0,
|
||||||
|
value: 0,
|
||||||
|
percentage: 0,
|
||||||
|
totalOrders: 0,
|
||||||
prevRevenue: 0,
|
prevRevenue: 0,
|
||||||
prevOrders: 0,
|
prevOrders: 0,
|
||||||
prevItemCount: 0,
|
prevItemCount: 0,
|
||||||
@@ -1583,8 +1577,11 @@ export class EventsService {
|
|||||||
currentDate = currentDate.plus({ days: 1 });
|
currentDate = currentDate.plus({ days: 1 });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Get total orders for the period (needed for percentages)
|
||||||
|
const totalOrders = currentEvents.length;
|
||||||
|
|
||||||
// Process current period events
|
// Process current period events
|
||||||
for (const event of currentEvents) {
|
for (const event of filteredCurrentEvents) {
|
||||||
const datetime = this.timeManager.toDateTime(event.attributes?.datetime);
|
const datetime = this.timeManager.toDateTime(event.attributes?.datetime);
|
||||||
if (!datetime) continue;
|
if (!datetime) continue;
|
||||||
|
|
||||||
@@ -1599,6 +1596,10 @@ export class EventsService {
|
|||||||
dayStats.revenue += totalAmount;
|
dayStats.revenue += totalAmount;
|
||||||
dayStats.orders++;
|
dayStats.orders++;
|
||||||
dayStats.itemCount += items.length;
|
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.averageOrderValue = dayStats.revenue / dayStats.orders;
|
||||||
dayStats.averageItemsPerOrder = dayStats.itemCount / dayStats.orders;
|
dayStats.averageItemsPerOrder = dayStats.itemCount / dayStats.orders;
|
||||||
}
|
}
|
||||||
@@ -1613,13 +1614,18 @@ export class EventsService {
|
|||||||
timestamp: dateKey,
|
timestamp: dateKey,
|
||||||
revenue: 0,
|
revenue: 0,
|
||||||
orders: 0,
|
orders: 0,
|
||||||
itemCount: 0
|
itemCount: 0,
|
||||||
|
value: 0,
|
||||||
|
count: 0
|
||||||
});
|
});
|
||||||
prevDate = prevDate.plus({ days: 1 });
|
prevDate = prevDate.plus({ days: 1 });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Aggregate previous period data
|
// Get total orders for previous period
|
||||||
for (const event of prevEvents) {
|
const prevTotalOrders = prevEvents.length;
|
||||||
|
|
||||||
|
// Process previous period events
|
||||||
|
for (const event of filteredPrevEvents) {
|
||||||
const datetime = this.timeManager.toDateTime(event.attributes?.datetime);
|
const datetime = this.timeManager.toDateTime(event.attributes?.datetime);
|
||||||
if (!datetime) continue;
|
if (!datetime) continue;
|
||||||
|
|
||||||
@@ -1634,21 +1640,15 @@ export class EventsService {
|
|||||||
dayStats.revenue += totalAmount;
|
dayStats.revenue += totalAmount;
|
||||||
dayStats.orders++;
|
dayStats.orders++;
|
||||||
dayStats.itemCount += items.length;
|
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
|
// Map previous period data to current period days
|
||||||
const prevPeriodDays = Array.from(prevDailyStats.values()).sort((a, b) => a.date.localeCompare(b.date));
|
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));
|
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
|
// Map the data using array indices
|
||||||
for (let i = 0; i < currentPeriodDays.length && i < prevPeriodDays.length; i++) {
|
for (let i = 0; i < currentPeriodDays.length && i < prevPeriodDays.length; i++) {
|
||||||
const currentDayStats = currentPeriodDays[i];
|
const currentDayStats = currentPeriodDays[i];
|
||||||
@@ -1660,19 +1660,15 @@ export class EventsService {
|
|||||||
dayStats.prevRevenue = prevDayStats.revenue;
|
dayStats.prevRevenue = prevDayStats.revenue;
|
||||||
dayStats.prevOrders = prevDayStats.orders;
|
dayStats.prevOrders = prevDayStats.orders;
|
||||||
dayStats.prevItemCount = prevDayStats.itemCount;
|
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;
|
dayStats.prevAvgOrderValue = prevDayStats.orders > 0 ? prevDayStats.revenue / prevDayStats.orders : 0;
|
||||||
dailyStats.set(currentDayStats.timestamp, dayStats);
|
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
|
// Convert to array and sort by date
|
||||||
const stats = Array.from(dailyStats.values())
|
const stats = Array.from(dailyStats.values())
|
||||||
.sort((a, b) => a.date.localeCompare(b.date))
|
.sort((a, b) => a.date.localeCompare(b.date))
|
||||||
@@ -1683,9 +1679,16 @@ export class EventsService {
|
|||||||
itemCount: Number(day.itemCount || 0),
|
itemCount: Number(day.itemCount || 0),
|
||||||
averageOrderValue: Number(day.averageOrderValue || 0),
|
averageOrderValue: Number(day.averageOrderValue || 0),
|
||||||
averageItemsPerOrder: Number(day.averageItemsPerOrder || 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),
|
prevRevenue: Number(day.prevRevenue || 0),
|
||||||
prevOrders: Number(day.prevOrders || 0),
|
prevOrders: Number(day.prevOrders || 0),
|
||||||
prevItemCount: Number(day.prevItemCount || 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)
|
prevAvgOrderValue: Number(day.prevAvgOrderValue || 0)
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
|||||||
@@ -141,14 +141,27 @@ _getCacheKey(type, params = {}) {
|
|||||||
metric,
|
metric,
|
||||||
daily,
|
daily,
|
||||||
cacheKey,
|
cacheKey,
|
||||||
isPreviousPeriod
|
isPreviousPeriod,
|
||||||
|
customFilters
|
||||||
} = params;
|
} = params;
|
||||||
|
|
||||||
let key = `klaviyo:${type}`;
|
let key = `klaviyo:${type}`;
|
||||||
|
|
||||||
// Handle "stats:details" for daily or metric-based keys
|
// Handle "stats:details" for daily or metric-based keys
|
||||||
if (type === 'stats:details') {
|
if (type === 'stats:details') {
|
||||||
key += `:${metric}${daily ? ':daily' : ''}`;
|
// Add metric to key
|
||||||
|
key += `:${metric || 'all'}`;
|
||||||
|
|
||||||
|
// Add daily flag if present
|
||||||
|
if (daily) {
|
||||||
|
key += ':daily';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add custom filters hash if present
|
||||||
|
if (customFilters?.length) {
|
||||||
|
const filterHash = customFilters.join('').replace(/[^a-zA-Z0-9]/g, '');
|
||||||
|
key += `:${filterHash}`;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// If a specific cache key is provided, use it (highest priority)
|
// If a specific cache key is provided, use it (highest priority)
|
||||||
@@ -157,17 +170,29 @@ _getCacheKey(type, params = {}) {
|
|||||||
}
|
}
|
||||||
// Otherwise, build a default cache key
|
// Otherwise, build a default cache key
|
||||||
else if (timeRange) {
|
else if (timeRange) {
|
||||||
key += `:${timeRange}${metricId ? `:${metricId}` : ''}`;
|
key += `:${timeRange}`;
|
||||||
|
if (metricId) {
|
||||||
|
key += `:${metricId}`;
|
||||||
|
}
|
||||||
if (isPreviousPeriod) {
|
if (isPreviousPeriod) {
|
||||||
key += ':prev';
|
key += ':prev';
|
||||||
}
|
}
|
||||||
} else if (startDate && endDate) {
|
} else if (startDate && endDate) {
|
||||||
key += `:custom:${startDate}:${endDate}${metricId ? `:${metricId}` : ''}`;
|
// For custom date ranges, include both dates in the key
|
||||||
|
key += `:custom:${startDate}:${endDate}`;
|
||||||
|
if (metricId) {
|
||||||
|
key += `:${metricId}`;
|
||||||
|
}
|
||||||
if (isPreviousPeriod) {
|
if (isPreviousPeriod) {
|
||||||
key += ':prev';
|
key += ':prev';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Add order type to key if present
|
||||||
|
if (['pre_orders', 'local_pickup', 'on_hold'].includes(metric)) {
|
||||||
|
key += `:${metric}`;
|
||||||
|
}
|
||||||
|
|
||||||
return key;
|
return key;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -163,8 +163,9 @@ export class TimeManager {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
case 'today': {
|
case 'today': {
|
||||||
|
const dayStart = this.getDayStart(now);
|
||||||
return {
|
return {
|
||||||
start: this.getDayStart(now),
|
start: dayStart,
|
||||||
end: this.getDayEnd(now)
|
end: this.getDayEnd(now)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -177,27 +178,29 @@ export class TimeManager {
|
|||||||
}
|
}
|
||||||
case 'last7days': {
|
case 'last7days': {
|
||||||
// For last 7 days, we want to include today and the previous 6 days
|
// For last 7 days, we want to include today and the previous 6 days
|
||||||
// So if today is 12/17, we want 12/11 1am to 12/17 12:59am
|
|
||||||
const dayStart = this.getDayStart(now);
|
const dayStart = this.getDayStart(now);
|
||||||
|
const weekStart = dayStart.minus({ days: 6 });
|
||||||
return {
|
return {
|
||||||
start: dayStart.minus({ days: 6 }), // 6 days ago from start of today
|
start: weekStart,
|
||||||
end: this.getDayEnd(now) // end of today
|
end: this.getDayEnd(now)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
case 'last30days': {
|
case 'last30days': {
|
||||||
// Include today and previous 29 days
|
// Include today and previous 29 days
|
||||||
const dayStart = this.getDayStart(now);
|
const dayStart = this.getDayStart(now);
|
||||||
|
const monthStart = dayStart.minus({ days: 29 });
|
||||||
return {
|
return {
|
||||||
start: dayStart.minus({ days: 29 }), // 29 days ago from start of today
|
start: monthStart,
|
||||||
end: this.getDayEnd(now) // end of today
|
end: this.getDayEnd(now)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
case 'last90days': {
|
case 'last90days': {
|
||||||
// Include today and previous 89 days
|
// Include today and previous 89 days
|
||||||
const dayStart = this.getDayStart(now);
|
const dayStart = this.getDayStart(now);
|
||||||
|
const start = dayStart.minus({ days: 89 });
|
||||||
return {
|
return {
|
||||||
start: dayStart.minus({ days: 89 }), // 89 days ago from start of today
|
start,
|
||||||
end: this.getDayEnd(now) // end of today
|
end: this.getDayEnd(now)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
case 'thisWeek': {
|
case 'thisWeek': {
|
||||||
@@ -212,7 +215,6 @@ export class TimeManager {
|
|||||||
const lastWeek = now.minus({ weeks: 1 });
|
const lastWeek = now.minus({ weeks: 1 });
|
||||||
const weekStart = this.getWeekStart(lastWeek);
|
const weekStart = this.getWeekStart(lastWeek);
|
||||||
const weekEnd = weekStart.plus({ days: 6 }); // 6 days after start = Saturday
|
const weekEnd = weekStart.plus({ days: 6 }); // 6 days after start = Saturday
|
||||||
|
|
||||||
return {
|
return {
|
||||||
start: weekStart,
|
start: weekStart,
|
||||||
end: this.getDayEnd(weekEnd)
|
end: this.getDayEnd(weekEnd)
|
||||||
@@ -221,7 +223,6 @@ export class TimeManager {
|
|||||||
case 'thisMonth': {
|
case 'thisMonth': {
|
||||||
const dayStart = this.getDayStart(now);
|
const dayStart = this.getDayStart(now);
|
||||||
const monthStart = dayStart.startOf('month').set({ hour: this.dayStartHour });
|
const monthStart = dayStart.startOf('month').set({ hour: this.dayStartHour });
|
||||||
|
|
||||||
return {
|
return {
|
||||||
start: monthStart,
|
start: monthStart,
|
||||||
end: this.getDayEnd(now)
|
end: this.getDayEnd(now)
|
||||||
@@ -231,7 +232,6 @@ export class TimeManager {
|
|||||||
const lastMonth = now.minus({ months: 1 });
|
const lastMonth = now.minus({ months: 1 });
|
||||||
const monthStart = lastMonth.startOf('month').set({ hour: this.dayStartHour });
|
const monthStart = lastMonth.startOf('month').set({ hour: this.dayStartHour });
|
||||||
const monthEnd = monthStart.plus({ months: 1 }).minus({ days: 1 });
|
const monthEnd = monthStart.plus({ months: 1 }).minus({ days: 1 });
|
||||||
|
|
||||||
return {
|
return {
|
||||||
start: monthStart,
|
start: monthStart,
|
||||||
end: this.getDayEnd(monthEnd)
|
end: this.getDayEnd(monthEnd)
|
||||||
@@ -298,7 +298,9 @@ export class TimeManager {
|
|||||||
* @returns {Object} Object with start and end DateTime objects
|
* @returns {Object} Object with start and end DateTime objects
|
||||||
*/
|
*/
|
||||||
getPreviousPeriod(period, now = this.getNow()) {
|
getPreviousPeriod(period, now = this.getNow()) {
|
||||||
switch (period) {
|
const normalizedPeriod = period.startsWith('previous') ? period.replace('previous', 'last') : period;
|
||||||
|
|
||||||
|
switch (normalizedPeriod) {
|
||||||
case 'today': {
|
case 'today': {
|
||||||
const yesterday = now.minus({ days: 1 });
|
const yesterday = now.minus({ days: 1 });
|
||||||
return {
|
return {
|
||||||
@@ -313,8 +315,7 @@ export class TimeManager {
|
|||||||
end: this.getDayEnd(twoDaysAgo)
|
end: this.getDayEnd(twoDaysAgo)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
case 'last7days':
|
case 'last7days': {
|
||||||
case 'previous7days': {
|
|
||||||
const dayStart = this.getDayStart(now);
|
const dayStart = this.getDayStart(now);
|
||||||
const currentStart = dayStart.minus({ days: 6 });
|
const currentStart = dayStart.minus({ days: 6 });
|
||||||
const prevEnd = currentStart.minus({ milliseconds: 1 });
|
const prevEnd = currentStart.minus({ milliseconds: 1 });
|
||||||
@@ -324,8 +325,7 @@ export class TimeManager {
|
|||||||
end: prevEnd
|
end: prevEnd
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
case 'last30days':
|
case 'last30days': {
|
||||||
case 'previous30days': {
|
|
||||||
const dayStart = this.getDayStart(now);
|
const dayStart = this.getDayStart(now);
|
||||||
const currentStart = dayStart.minus({ days: 29 });
|
const currentStart = dayStart.minus({ days: 29 });
|
||||||
const prevEnd = currentStart.minus({ milliseconds: 1 });
|
const prevEnd = currentStart.minus({ milliseconds: 1 });
|
||||||
@@ -335,8 +335,7 @@ export class TimeManager {
|
|||||||
end: prevEnd
|
end: prevEnd
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
case 'last90days':
|
case 'last90days': {
|
||||||
case 'previous90days': {
|
|
||||||
const dayStart = this.getDayStart(now);
|
const dayStart = this.getDayStart(now);
|
||||||
const currentStart = dayStart.minus({ days: 89 });
|
const currentStart = dayStart.minus({ days: 89 });
|
||||||
const prevEnd = currentStart.minus({ milliseconds: 1 });
|
const prevEnd = currentStart.minus({ milliseconds: 1 });
|
||||||
@@ -382,13 +381,6 @@ export class TimeManager {
|
|||||||
end: prevEnd
|
end: prevEnd
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
case 'twoDaysAgo': {
|
|
||||||
const twoDaysAgo = now.minus({ days: 2 });
|
|
||||||
return {
|
|
||||||
start: this.getDayStart(twoDaysAgo),
|
|
||||||
end: this.getDayEnd(twoDaysAgo)
|
|
||||||
};
|
|
||||||
}
|
|
||||||
default:
|
default:
|
||||||
console.warn(`[TimeManager] No previous period defined for: ${period}`);
|
console.warn(`[TimeManager] No previous period defined for: ${period}`);
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
Reference in New Issue
Block a user