metrics.service.ts 5.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158
  1. import { Injectable } from '@nestjs/common';
  2. import { CacheService, Logger, Order, RequestContext, TransactionalConnection } from '@vendure/core';
  3. import { createHash } from 'crypto';
  4. import { endOfDay, startOfDay } from 'date-fns';
  5. import {
  6. AverageOrderValueMetric,
  7. MetricCalculation,
  8. OrderCountMetric,
  9. OrderTotalMetric,
  10. } from '../config/metrics-strategies.js';
  11. import { loggerCtx } from '../constants.js';
  12. import {
  13. DashboardMetricSummary,
  14. DashboardMetricSummaryEntry,
  15. DashboardMetricSummaryInput,
  16. } from '../types.js';
  17. export type MetricData = {
  18. date: Date;
  19. orders: Order[];
  20. };
  21. @Injectable()
  22. export class MetricsService {
  23. metricCalculations: MetricCalculation[];
  24. constructor(
  25. private connection: TransactionalConnection,
  26. private cacheService: CacheService,
  27. ) {
  28. this.metricCalculations = [
  29. new AverageOrderValueMetric(),
  30. new OrderCountMetric(),
  31. new OrderTotalMetric(),
  32. ];
  33. }
  34. async getMetrics(
  35. ctx: RequestContext,
  36. { types, refresh, startDate, endDate }: DashboardMetricSummaryInput,
  37. ): Promise<DashboardMetricSummary[]> {
  38. const calculatedStartDate = startOfDay(new Date(startDate));
  39. const calculatedEndDate = endOfDay(new Date(endDate));
  40. // Check if we have cached result
  41. const hash = createHash('sha1')
  42. .update(
  43. JSON.stringify({
  44. startDate: calculatedStartDate,
  45. endDate: calculatedEndDate,
  46. types: types.sort(),
  47. channel: ctx.channel.token,
  48. }),
  49. )
  50. .digest('base64');
  51. const cacheKey = `MetricsService:${hash}`;
  52. const cachedMetricList = await this.cacheService.get<DashboardMetricSummary[]>(cacheKey);
  53. if (cachedMetricList && refresh !== true) {
  54. Logger.verbose(`Returning cached metrics for channel ${ctx.channel.token}`, loggerCtx);
  55. return cachedMetricList;
  56. }
  57. // No cache, calculating new metrics
  58. Logger.verbose(
  59. `No cache hit, calculating metrics from ${calculatedStartDate.toISOString()} to ${calculatedEndDate.toISOString()} for channel ${
  60. ctx.channel.token
  61. } for all orders`,
  62. loggerCtx,
  63. );
  64. const data = await this.loadData(ctx, calculatedStartDate, calculatedEndDate);
  65. const metrics: DashboardMetricSummary[] = [];
  66. for (const type of types) {
  67. const metric = this.metricCalculations.find(m => m.type === type);
  68. if (!metric) {
  69. continue;
  70. }
  71. // Calculate entries for each day
  72. const entries: DashboardMetricSummaryEntry[] = [];
  73. data.forEach(dataPerDay => {
  74. entries.push(metric.calculateEntry(ctx, dataPerDay));
  75. });
  76. // Create metric with calculated entries
  77. metrics.push({
  78. title: metric.getTitle(ctx),
  79. type: metric.type,
  80. entries,
  81. });
  82. }
  83. await this.cacheService.set(cacheKey, metrics, { ttl: 1000 * 60 * 60 * 2 }); // 2 hours
  84. return metrics;
  85. }
  86. async loadData(ctx: RequestContext, startDate: Date, endDate: Date): Promise<Map<string, MetricData>> {
  87. const orderRepo = this.connection.getRepository(ctx, Order);
  88. // Calculate number of days between start and end
  89. const diffTime = Math.abs(endDate.getTime() - startDate.getTime());
  90. const nrOfDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24)) + 1;
  91. // Get orders in a loop until we have all
  92. let skip = 0;
  93. const take = 1000;
  94. let hasMoreOrders = true;
  95. const orders: Order[] = [];
  96. while (hasMoreOrders) {
  97. const query = orderRepo
  98. .createQueryBuilder('order')
  99. .leftJoin('order.channels', 'orderChannel')
  100. .where('orderChannel.id=:channelId', { channelId: ctx.channelId })
  101. .andWhere('order.orderPlacedAt >= :startDate', {
  102. startDate: startDate.toISOString(),
  103. })
  104. .andWhere('order.orderPlacedAt <= :endDate', {
  105. endDate: endDate.toISOString(),
  106. })
  107. .skip(skip)
  108. .take(take);
  109. const [items, nrOfOrders] = await query.getManyAndCount();
  110. orders.push(...items);
  111. Logger.verbose(
  112. `Fetched orders ${skip}-${skip + take} for channel ${
  113. ctx.channel.token
  114. } for date range metrics`,
  115. loggerCtx,
  116. );
  117. skip += items.length;
  118. if (orders.length >= nrOfOrders) {
  119. hasMoreOrders = false;
  120. }
  121. }
  122. Logger.verbose(
  123. `Finished fetching all ${orders.length} orders for channel ${ctx.channel.token} for date range metrics`,
  124. loggerCtx,
  125. );
  126. const dataPerDay = new Map<string, MetricData>();
  127. // Create a map entry for each day in the range
  128. for (let i = 0; i < nrOfDays; i++) {
  129. const currentDate = new Date(startDate);
  130. currentDate.setDate(startDate.getDate() + i);
  131. const dateKey = currentDate.toISOString().split('T')[0]; // YYYY-MM-DD format
  132. // Filter orders for this specific day
  133. const ordersForDay = orders.filter(order => {
  134. if (!order.orderPlacedAt) return false;
  135. const orderDate = new Date(order.orderPlacedAt).toISOString().split('T')[0];
  136. return orderDate === dateKey;
  137. });
  138. dataPerDay.set(dateKey, {
  139. orders: ordersForDay,
  140. date: currentDate,
  141. });
  142. }
  143. return dataPerDay;
  144. }
  145. }