Improve projected revenue
This commit is contained in:
@@ -149,6 +149,57 @@ export function createEventsRouter(apiKey, apiRevision) {
|
||||
}
|
||||
});
|
||||
|
||||
// Add new route for smart revenue projection
|
||||
router.get('/projection', async (req, res) => {
|
||||
try {
|
||||
const { timeRange, startDate, endDate } = req.query;
|
||||
console.log('[Events Route] Projection request:', {
|
||||
timeRange,
|
||||
startDate,
|
||||
endDate
|
||||
});
|
||||
|
||||
let range;
|
||||
if (startDate && endDate) {
|
||||
range = timeManager.getCustomRange(startDate, endDate);
|
||||
} else if (timeRange) {
|
||||
range = timeManager.getDateRange(timeRange);
|
||||
} else {
|
||||
return res.status(400).json({ error: 'Must provide either timeRange or startDate and endDate' });
|
||||
}
|
||||
|
||||
if (!range) {
|
||||
return res.status(400).json({ error: 'Invalid time range' });
|
||||
}
|
||||
|
||||
const params = {
|
||||
timeRange,
|
||||
startDate: range.start.toISO(),
|
||||
endDate: range.end.toISO()
|
||||
};
|
||||
|
||||
// Try to get from cache first with a short TTL
|
||||
const cacheKey = redisService._getCacheKey('projection', params);
|
||||
const cachedData = await redisService.get(cacheKey);
|
||||
|
||||
if (cachedData) {
|
||||
console.log('[Events Route] Cache hit for projection');
|
||||
return res.json(cachedData);
|
||||
}
|
||||
|
||||
console.log('[Events Route] Calculating smart projection with params:', params);
|
||||
const projection = await eventsService.calculateSmartProjection(params);
|
||||
|
||||
// Cache the results with a short TTL (5 minutes)
|
||||
await redisService.set(cacheKey, projection, 300);
|
||||
|
||||
res.json(projection);
|
||||
} catch (error) {
|
||||
console.error("[Events Route] Error calculating projection:", error);
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// Add new route for detailed stats
|
||||
router.get('/stats/details', async (req, res) => {
|
||||
try {
|
||||
|
||||
@@ -2022,4 +2022,160 @@ export class EventsService {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async calculateSmartProjection(params = {}) {
|
||||
try {
|
||||
const { timeRange, startDate, endDate } = params;
|
||||
|
||||
// Get current period dates
|
||||
let periodStart, periodEnd;
|
||||
if (startDate && endDate) {
|
||||
periodStart = this.timeManager.getDayStart(this.timeManager.toDateTime(startDate));
|
||||
periodEnd = this.timeManager.getDayEnd(this.timeManager.toDateTime(endDate));
|
||||
} else if (timeRange) {
|
||||
const range = this.timeManager.getDateRange(timeRange);
|
||||
periodStart = range.start;
|
||||
periodEnd = range.end;
|
||||
}
|
||||
|
||||
// Get the same day of week from the last 4 weeks for pattern matching
|
||||
const historicalPeriods = [];
|
||||
let historicalStart = periodStart.minus({ weeks: 4 });
|
||||
|
||||
for (let i = 0; i < 4; i++) {
|
||||
historicalPeriods.push({
|
||||
start: historicalStart.plus({ weeks: i }),
|
||||
end: historicalStart.plus({ weeks: i + 1 }).minus({ milliseconds: 1 })
|
||||
});
|
||||
}
|
||||
|
||||
// Fetch current period data
|
||||
const currentEvents = await this.getEvents({
|
||||
startDate: periodStart.toISO(),
|
||||
endDate: periodEnd.toISO(),
|
||||
metricId: METRIC_IDS.PLACED_ORDER
|
||||
});
|
||||
|
||||
// Fetch historical data for pattern matching
|
||||
const historicalPromises = historicalPeriods.map(period =>
|
||||
this.getEvents({
|
||||
startDate: period.start.toISO(),
|
||||
endDate: period.end.toISO(),
|
||||
metricId: METRIC_IDS.PLACED_ORDER
|
||||
})
|
||||
);
|
||||
|
||||
const historicalResults = await Promise.all(historicalPromises);
|
||||
|
||||
// Process current period data
|
||||
const currentData = this._transformEvents(currentEvents.data || []);
|
||||
const currentRevenue = currentData.reduce((sum, event) => {
|
||||
const props = event.event_properties || {};
|
||||
return sum + Number(props.TotalAmount || 0);
|
||||
}, 0);
|
||||
|
||||
// Build hourly patterns from historical data
|
||||
const hourlyPatterns = Array(24).fill(0).map(() => ({
|
||||
count: 0,
|
||||
revenue: 0,
|
||||
percentage: 0
|
||||
}));
|
||||
|
||||
let totalHistoricalRevenue = 0;
|
||||
let totalHistoricalOrders = 0;
|
||||
|
||||
historicalResults.forEach(result => {
|
||||
const events = this._transformEvents(result.data || []);
|
||||
events.forEach(event => {
|
||||
const datetime = this.timeManager.toDateTime(event.attributes?.datetime);
|
||||
if (!datetime) return;
|
||||
|
||||
const hour = datetime.hour;
|
||||
const props = event.event_properties || {};
|
||||
const amount = Number(props.TotalAmount || 0);
|
||||
|
||||
hourlyPatterns[hour].count++;
|
||||
hourlyPatterns[hour].revenue += amount;
|
||||
totalHistoricalRevenue += amount;
|
||||
totalHistoricalOrders++;
|
||||
});
|
||||
});
|
||||
|
||||
// Calculate percentages
|
||||
hourlyPatterns.forEach(pattern => {
|
||||
pattern.percentage = totalHistoricalRevenue > 0 ?
|
||||
(pattern.revenue / totalHistoricalRevenue) * 100 : 0;
|
||||
});
|
||||
|
||||
// Get current hour in the period's timezone
|
||||
const now = this.timeManager.getNow();
|
||||
const currentHour = now.hour;
|
||||
const currentMinute = now.minute;
|
||||
|
||||
// Calculate how much of the current hour has passed (0-1)
|
||||
const hourProgress = currentMinute / 60;
|
||||
|
||||
// Calculate how much of the expected daily revenue we've seen so far
|
||||
let expectedPercentageSeen = 0;
|
||||
for (let i = 0; i < currentHour; i++) {
|
||||
expectedPercentageSeen += hourlyPatterns[i].percentage;
|
||||
}
|
||||
// Add partial current hour
|
||||
expectedPercentageSeen += hourlyPatterns[currentHour].percentage * hourProgress;
|
||||
|
||||
// Calculate projection based on patterns
|
||||
let projectedRevenue = 0;
|
||||
if (expectedPercentageSeen > 0) {
|
||||
projectedRevenue = (currentRevenue / (expectedPercentageSeen / 100));
|
||||
}
|
||||
|
||||
// Calculate confidence score (0-1) based on:
|
||||
// 1. How much historical data we have
|
||||
// 2. How consistent the patterns are
|
||||
// 3. How far through the period we are
|
||||
const patternConsistency = this._calculatePatternConsistency(hourlyPatterns);
|
||||
const periodProgress = Math.min(100, Math.max(0, (now.diff(periodStart).milliseconds / periodEnd.diff(periodStart).milliseconds) * 100));
|
||||
const historicalDataAmount = Math.min(totalHistoricalOrders / 1000, 1); // Normalize to 0-1, considering 1000+ orders as maximum confidence
|
||||
|
||||
const confidence = (
|
||||
(patternConsistency * 0.4) +
|
||||
(periodProgress / 100 * 0.4) +
|
||||
(historicalDataAmount * 0.2)
|
||||
);
|
||||
|
||||
// Return both the simple and pattern-based projections with metadata
|
||||
return {
|
||||
currentRevenue,
|
||||
projectedRevenue,
|
||||
confidence,
|
||||
metadata: {
|
||||
periodProgress,
|
||||
patternConsistency,
|
||||
historicalOrders: totalHistoricalOrders,
|
||||
hourlyPatterns,
|
||||
expectedPercentageSeen,
|
||||
currentHour,
|
||||
currentMinute
|
||||
}
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('[EventsService] Error calculating smart projection:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
_calculatePatternConsistency(hourlyPatterns) {
|
||||
// Calculate the standard deviation of the percentage distribution
|
||||
const mean = hourlyPatterns.reduce((sum, pattern) => sum + pattern.percentage, 0) / 24;
|
||||
const variance = hourlyPatterns.reduce((sum, pattern) => {
|
||||
const diff = pattern.percentage - mean;
|
||||
return sum + (diff * diff);
|
||||
}, 0) / 24;
|
||||
const stdDev = Math.sqrt(variance);
|
||||
|
||||
// Normalize to a 0-1 scale where lower standard deviation means higher consistency
|
||||
// Using a sigmoid function to normalize
|
||||
const normalizedConsistency = 1 / (1 + Math.exp(stdDev / 10));
|
||||
return normalizedConsistency;
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user