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
|
// Add new route for detailed stats
|
||||||
router.get('/stats/details', async (req, res) => {
|
router.get('/stats/details', async (req, res) => {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -2022,4 +2022,160 @@ export class EventsService {
|
|||||||
throw error;
|
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