Jelajahi Sumber

feat(analytics) replace daily analytics report generated event with email requested

Karol Sójko 2 tahun lalu
induk
melakukan
3096cd98d5

+ 30 - 12
packages/analytics/bin/report.ts

@@ -4,6 +4,7 @@ import 'newrelic'
 
 import { Logger } from 'winston'
 
+import { EmailLevel } from '@standardnotes/domain-core'
 import { DomainEventPublisherInterface } from '@standardnotes/domain-events'
 import { AnalyticsActivity } from '../src/Domain/Analytics/AnalyticsActivity'
 import { Period } from '../src/Domain/Time/Period'
@@ -16,6 +17,8 @@ import TYPES from '../src/Bootstrap/Types'
 import { Env } from '../src/Bootstrap/Env'
 import { DomainEventFactoryInterface } from '../src/Domain/Event/DomainEventFactoryInterface'
 import { CalculateMonthlyRecurringRevenue } from '../src/Domain/UseCase/CalculateMonthlyRecurringRevenue/CalculateMonthlyRecurringRevenue'
+import { getBody, getSubject } from '../src/Domain/Email/DailyAnalyticsReport'
+import { TimerInterface } from '@standardnotes/time'
 
 const requestReport = async (
   analyticsStore: AnalyticsStoreInterface,
@@ -24,6 +27,8 @@ const requestReport = async (
   domainEventPublisher: DomainEventPublisherInterface,
   periodKeyGenerator: PeriodKeyGeneratorInterface,
   calculateMonthlyRecurringRevenue: CalculateMonthlyRecurringRevenue,
+  timer: TimerInterface,
+  adminEmails: string[],
 ): Promise<void> => {
   await calculateMonthlyRecurringRevenue.execute({})
 
@@ -213,18 +218,28 @@ const requestReport = async (
     })
   }
 
-  const event = domainEventFactory.createDailyAnalyticsReportGeneratedEvent({
-    activityStatistics: yesterdayActivityStatistics,
-    activityStatisticsOverTime: analyticsOverTime,
-    statisticsOverTime,
-    statisticMeasures,
-    churn: {
-      periodKeys: monthlyPeriodKeys,
-      values: churnRates,
-    },
-  })
-
-  await domainEventPublisher.publish(event)
+  for (const adminEmail of adminEmails) {
+    const event = domainEventFactory.createEmailRequestedEvent({
+      messageIdentifier: 'VERSION_ADOPTION_REPORT',
+      subject: getSubject(),
+      body: getBody(
+        {
+          activityStatistics: yesterdayActivityStatistics,
+          activityStatisticsOverTime: analyticsOverTime,
+          statisticsOverTime,
+          statisticMeasures,
+          churn: {
+            periodKeys: monthlyPeriodKeys,
+            values: churnRates,
+          },
+        },
+        timer,
+      ),
+      level: EmailLevel.LEVELS.System,
+      userEmail: adminEmail,
+    })
+    await domainEventPublisher.publish(event)
+  }
 }
 
 const container = new ContainerConfigLoader()
@@ -241,6 +256,7 @@ void container.load().then((container) => {
   const domainEventFactory: DomainEventFactoryInterface = container.get(TYPES.DomainEventFactory)
   const domainEventPublisher: DomainEventPublisherInterface = container.get(TYPES.DomainEventPublisher)
   const periodKeyGenerator: PeriodKeyGeneratorInterface = container.get(TYPES.PeriodKeyGenerator)
+  const timer: TimerInterface = container.get(TYPES.Timer)
   const calculateMonthlyRecurringRevenue: CalculateMonthlyRecurringRevenue = container.get(
     TYPES.CalculateMonthlyRecurringRevenue,
   )
@@ -253,6 +269,8 @@ void container.load().then((container) => {
       domainEventPublisher,
       periodKeyGenerator,
       calculateMonthlyRecurringRevenue,
+      timer,
+      container.get(TYPES.ADMIN_EMAILS),
     ),
   )
     .then(() => {

+ 1 - 1
packages/analytics/jest.config.js

@@ -7,5 +7,5 @@ module.exports = {
   transform: {
     ...tsjPreset.transform,
   },
-  coveragePathIgnorePatterns: ['/Infra/'],
+  coveragePathIgnorePatterns: ['/Infra/', '/Domain/Email/'],
 }

+ 1 - 1
packages/analytics/package.json

@@ -40,7 +40,7 @@
     "@newrelic/winston-enricher": "^4.0.0",
     "@sentry/node": "^7.19.0",
     "@standardnotes/common": "workspace:*",
-    "@standardnotes/domain-core": "workspace:*",
+    "@standardnotes/domain-core": "workspace:^",
     "@standardnotes/domain-events": "workspace:*",
     "@standardnotes/domain-events-infra": "workspace:*",
     "@standardnotes/time": "workspace:*",

+ 1 - 0
packages/analytics/src/Bootstrap/Container.ts

@@ -130,6 +130,7 @@ export class ContainerConfigLoader {
     container.bind(TYPES.SQS_QUEUE_URL).toConstantValue(env.get('SQS_QUEUE_URL', true))
     container.bind(TYPES.REDIS_EVENTS_CHANNEL).toConstantValue(env.get('REDIS_EVENTS_CHANNEL'))
     container.bind(TYPES.NEW_RELIC_ENABLED).toConstantValue(env.get('NEW_RELIC_ENABLED', true))
+    container.bind(TYPES.ADMIN_EMAILS).toConstantValue(env.get('ADMIN_EMAILS').split(','))
 
     // Repositories
     container

+ 1 - 0
packages/analytics/src/Bootstrap/Types.ts

@@ -11,6 +11,7 @@ const TYPES = {
   SQS_AWS_REGION: Symbol.for('SQS_AWS_REGION'),
   REDIS_EVENTS_CHANNEL: Symbol.for('REDIS_EVENTS_CHANNEL'),
   NEW_RELIC_ENABLED: Symbol.for('NEW_RELIC_ENABLED'),
+  ADMIN_EMAILS: Symbol.for('ADMIN_EMAILS'),
   // Repositories
   AnalyticsEntityRepository: Symbol.for('AnalyticsEntityRepository'),
   RevenueModificationRepository: Symbol.for('RevenueModificationRepository'),

+ 11 - 0
packages/analytics/src/Domain/Email/DailyAnalyticsReport.ts

@@ -0,0 +1,11 @@
+import { TimerInterface } from '@standardnotes/time'
+
+import { html } from './daily-analytics-report.html'
+
+export function getSubject(): string {
+  return `Daily analytics report ${new Date().toLocaleDateString('en-US')}`
+}
+
+export function getBody(data: unknown, timer: TimerInterface): string {
+  return html(data, timer)
+}

+ 966 - 0
packages/analytics/src/Domain/Email/daily-analytics-report.html.ts

@@ -0,0 +1,966 @@
+/* eslint-disable @typescript-eslint/no-explicit-any */
+import { TimerInterface } from '@standardnotes/time'
+
+import { AnalyticsActivity } from '../Analytics/AnalyticsActivity'
+import { StatisticsMeasure } from '../Statistics/StatisticsMeasure'
+import { Period } from '../Time/Period'
+
+const getChartUrls = (
+  data: any,
+): {
+  subscriptions: string
+  users: string
+  quarterlyPerformance: string
+  churn: string
+  mrr: string
+  mrrMonthly: string
+} => {
+  const subscriptionPurchasingOverTime = data.activityStatisticsOverTime.find(
+    (a: { name: AnalyticsActivity; period: Period }) =>
+      a.name === AnalyticsActivity.SubscriptionPurchased && a.period === Period.Last30Days,
+  )
+  const subscriptionRenewingOverTime = data.activityStatisticsOverTime.find(
+    (a: { name: AnalyticsActivity; period: Period }) =>
+      a.name === AnalyticsActivity.SubscriptionRenewed && a.period === Period.Last30Days,
+  )
+  const subscriptionRefundingOverTime = data.activityStatisticsOverTime.find(
+    (a: { name: AnalyticsActivity; period: Period }) =>
+      a.name === AnalyticsActivity.SubscriptionRefunded && a.period === Period.Last30Days,
+  )
+  const subscriptionCancelledOverTime = data.activityStatisticsOverTime.find(
+    (a: { name: AnalyticsActivity; period: Period }) =>
+      a.name === AnalyticsActivity.SubscriptionCancelled && a.period === Period.Last30Days,
+  )
+  const subscriptionReactivatedOverTime = data.activityStatisticsOverTime.find(
+    (a: { name: AnalyticsActivity; period: Period }) =>
+      a.name === AnalyticsActivity.SubscriptionReactivated && a.period === Period.Last30Days,
+  )
+
+  const subscriptionsLinerOverTimeConfig = {
+    type: 'line',
+    data: {
+      labels: subscriptionPurchasingOverTime?.counts.map((count: { periodKey: any }) => count.periodKey),
+      datasets: [
+        {
+          label: 'Subscription Purchases',
+          backgroundColor: 'rgb(25, 255, 140)',
+          borderColor: 'rgb(25, 255, 140)',
+          data: subscriptionPurchasingOverTime?.counts.map((count: { totalCount: any }) => count.totalCount),
+          fill: false,
+          pointRadius: 2,
+        },
+        {
+          label: 'Subscription Renewals',
+          backgroundColor: 'rgb(54, 162, 235)',
+          borderColor: 'rgb(54, 162, 235)',
+          data: subscriptionRenewingOverTime?.counts.map((count: { totalCount: any }) => count.totalCount),
+          fill: false,
+          pointRadius: 2,
+        },
+        {
+          label: 'Subscription Refunds',
+          backgroundColor: 'rgb(255, 221, 51)',
+          borderColor: 'rgb(255, 221, 51)',
+          data: subscriptionRefundingOverTime?.counts.map((count: { totalCount: any }) => count.totalCount),
+          fill: false,
+          pointRadius: 2,
+        },
+        {
+          label: 'Subscription Cancels',
+          backgroundColor: 'rgb(255, 99, 132)',
+          borderColor: 'rgb(255, 99, 132)',
+          data: subscriptionCancelledOverTime?.counts.map((count: { totalCount: any }) => count.totalCount),
+          fill: false,
+          pointRadius: 2,
+        },
+        {
+          label: 'Subscription Reactivations',
+          backgroundColor: 'rgb(221, 51, 255)',
+          borderColor: 'rgb(221, 51, 255)',
+          data: subscriptionReactivatedOverTime?.counts.map((count: { totalCount: any }) => count.totalCount),
+          fill: false,
+          pointRadius: 2,
+        },
+      ],
+    },
+  }
+
+  const userRegistrationOverTime = data.activityStatisticsOverTime.find(
+    (a: { name: AnalyticsActivity; period: Period }) =>
+      a.name === AnalyticsActivity.Register && a.period === Period.Last30Days,
+  )
+  const userDeletionOverTime = data.activityStatisticsOverTime.find(
+    (a: { name: AnalyticsActivity; period: Period }) =>
+      a.name === AnalyticsActivity.DeleteAccount && a.period === Period.Last30Days,
+  )
+
+  const usersLinerOverTimeConfig = {
+    type: 'line',
+    data: {
+      labels: userRegistrationOverTime?.counts.map((count: { periodKey: any }) => count.periodKey),
+      datasets: [
+        {
+          label: 'User Registrations',
+          backgroundColor: 'rgb(25, 255, 140)',
+          borderColor: 'rgb(25, 255, 140)',
+          data: userRegistrationOverTime?.counts.map((count: { totalCount: any }) => count.totalCount),
+          fill: false,
+          pointRadius: 2,
+        },
+        {
+          label: 'Account Deletions',
+          backgroundColor: 'rgb(255, 99, 132)',
+          borderColor: 'rgb(255, 99, 132)',
+          data: userDeletionOverTime?.counts.map((count: { totalCount: any }) => count.totalCount),
+          fill: false,
+          pointRadius: 2,
+        },
+      ],
+    },
+  }
+
+  const quarters = [Period.Q1ThisYear, Period.Q2ThisYear, Period.Q3ThisYear, Period.Q4ThisYear]
+  const quarterlyUserRegistrations = []
+  const quarterlySubscriptionPurchases = []
+  const quarterlySubscriptionRenewals = []
+  for (const quarter of quarters) {
+    const registrations =
+      data.activityStatisticsOverTime.find(
+        (a: { name: AnalyticsActivity; period: Period }) =>
+          a.name === AnalyticsActivity.Register && a.period === quarter,
+      )?.totalCount ?? 0
+    const purchases =
+      data.activityStatisticsOverTime.find(
+        (a: { name: AnalyticsActivity; period: Period }) =>
+          a.name === AnalyticsActivity.SubscriptionPurchased && a.period === quarter,
+      )?.totalCount ?? 0
+    const renewals =
+      data.activityStatisticsOverTime.find(
+        (a: { name: AnalyticsActivity; period: Period }) =>
+          a.name === AnalyticsActivity.SubscriptionRenewed && a.period === quarter,
+      )?.totalCount ?? 0
+    quarterlyUserRegistrations.push(registrations)
+    quarterlySubscriptionPurchases.push(purchases)
+    quarterlySubscriptionRenewals.push(renewals)
+  }
+
+  const quarterlyConfig = {
+    type: 'bar',
+    data: {
+      labels: ['Q1', 'Q2', 'Q3', 'Q4'],
+      datasets: [
+        {
+          label: 'User Registrations',
+          backgroundColor: 'rgba(255, 99, 132, 0.5)',
+          borderColor: 'rgb(255, 99, 132)',
+          borderWidth: 1,
+          data: quarterlyUserRegistrations,
+        },
+        {
+          label: 'Subscription Purchases',
+          backgroundColor: 'rgba(54, 162, 235, 0.5)',
+          borderColor: 'rgb(54, 162, 235)',
+          borderWidth: 1,
+          data: quarterlySubscriptionPurchases,
+        },
+        {
+          label: 'Subscription Renewals',
+          backgroundColor: 'rgb(25, 255, 140, 0.5)',
+          borderColor: 'rgb(25, 255, 140)',
+          borderWidth: 1,
+          data: quarterlySubscriptionRenewals,
+        },
+      ],
+    },
+    options: {
+      title: {
+        display: true,
+        text: 'Quarterly Performance',
+      },
+      plugins: {
+        datalabels: {
+          anchor: 'center',
+          align: 'center',
+          color: '#666',
+          font: {
+            weight: 'normal',
+          },
+        },
+      },
+    },
+  }
+
+  const monthlyChurnRates = data.churn.values.map((value: { rate: number }) => +value.rate.toFixed(2))
+
+  const churnConfig = {
+    type: 'bar',
+    data: {
+      labels: [
+        'January',
+        'February',
+        'March',
+        'April',
+        'May',
+        'June',
+        'July',
+        'August',
+        'September',
+        'October',
+        'November',
+        'December',
+      ],
+      datasets: [
+        {
+          label: 'Churn Percent',
+          backgroundColor: 'rgba(255, 99, 132, 0.5)',
+          borderColor: 'rgb(255, 99, 132)',
+          borderWidth: 1,
+          data: monthlyChurnRates,
+        },
+      ],
+    },
+    options: {
+      title: {
+        display: true,
+        text: 'Monthly Churn Rate',
+      },
+      plugins: {
+        datalabels: {
+          anchor: 'center',
+          align: 'center',
+          color: '#666',
+          font: {
+            weight: 'normal',
+          },
+        },
+      },
+    },
+  }
+
+  const mrrOverTime = data.statisticsOverTime.find(
+    (a: { name: string; period: number }) => a.name === 'mrr' && a.period === 27,
+  )
+  const monthlyPlansMrrOverTime = data.statisticsOverTime.find(
+    (a: { name: string; period: number }) => a.name === 'monthly-plans-mrr' && a.period === 27,
+  )
+  const annualPlansMrrOverTime = data.statisticsOverTime.find(
+    (a: { name: string; period: number }) => a.name === 'annual-plans-mrr' && a.period === 27,
+  )
+  const fiveYearPlansMrrOverTime = data.statisticsOverTime.find(
+    (a: { name: string; period: number }) => a.name === 'five-year-plans-mrr' && a.period === 27,
+  )
+  const proPlansMrrOverTime = data.statisticsOverTime.find(
+    (a: { name: string; period: number }) => a.name === 'pro-plans-mrr' && a.period === 27,
+  )
+  const plusPlansMrrOverTime = data.statisticsOverTime.find(
+    (a: { name: string; period: number }) => a.name === 'plus-plans-mrr' && a.period === 27,
+  )
+
+  const mrrOverTimeConfig = {
+    type: 'line',
+    data: {
+      labels: mrrOverTime?.counts.map((count: { periodKey: any }) => count.periodKey),
+      datasets: [
+        {
+          label: 'MRR',
+          backgroundColor: 'rgb(25, 255, 140)',
+          borderColor: 'rgb(25, 255, 140)',
+          data: mrrOverTime?.counts.map((count: { totalCount: any }) => count.totalCount),
+          fill: false,
+          pointRadius: 2,
+        },
+        {
+          label: 'MRR - Monthly Plans',
+          backgroundColor: 'rgb(54, 162, 235)',
+          borderColor: 'rgb(54, 162, 235)',
+          data: monthlyPlansMrrOverTime?.counts.map((count: { totalCount: any }) => count.totalCount),
+          fill: false,
+          pointRadius: 2,
+        },
+        {
+          label: 'MRR - Annual Plans',
+          backgroundColor: 'rgb(255, 221, 51)',
+          borderColor: 'rgb(255, 221, 51)',
+          data: annualPlansMrrOverTime?.counts.map((count: { totalCount: any }) => count.totalCount),
+          fill: false,
+          pointRadius: 2,
+        },
+        {
+          label: 'MRR - Five Year Plans',
+          backgroundColor: 'rgb(255, 120, 120)',
+          borderColor: 'rgb(255, 120, 120)',
+          data: fiveYearPlansMrrOverTime?.counts.map((count: { totalCount: any }) => count.totalCount),
+          fill: false,
+          pointRadius: 2,
+        },
+        {
+          label: 'MRR - PRO Plans',
+          backgroundColor: 'rgb(255, 99, 132)',
+          borderColor: 'rgb(255, 99, 132)',
+          data: proPlansMrrOverTime?.counts.map((count: { totalCount: any }) => count.totalCount),
+          fill: false,
+          pointRadius: 2,
+        },
+        {
+          label: 'MRR - PLUS Plans',
+          backgroundColor: 'rgb(221, 51, 255)',
+          borderColor: 'rgb(221, 51, 255)',
+          data: plusPlansMrrOverTime?.counts.map((count: { totalCount: any }) => count.totalCount),
+          fill: false,
+          pointRadius: 2,
+        },
+      ],
+    },
+  }
+
+  const mrrMonthlyOverTime = data.statisticsOverTime
+    .find((a: { name: string; period: Period }) => a.name === 'mrr' && a.period === Period.ThisYear)
+    ?.counts.map((count: { totalCount: number }) => +count.totalCount.toFixed(2))
+
+  const mrrMonthlyConfig = {
+    type: 'bar',
+    data: {
+      labels: [
+        'January',
+        'February',
+        'March',
+        'April',
+        'May',
+        'June',
+        'July',
+        'August',
+        'September',
+        'October',
+        'November',
+        'December',
+      ],
+      datasets: [
+        {
+          label: 'MRR',
+          backgroundColor: 'rgba(25, 255, 140, 0.5)',
+          borderColor: 'rgb(25, 255, 140)',
+          borderWidth: 1,
+          data: mrrMonthlyOverTime,
+        },
+      ],
+    },
+    options: {
+      title: {
+        display: true,
+        text: 'Monthly MRR',
+      },
+      plugins: {
+        datalabels: {
+          anchor: 'center',
+          align: 'center',
+          color: '#666',
+          font: {
+            weight: 'normal',
+          },
+        },
+      },
+    },
+  }
+
+  return {
+    subscriptions: `https://quickchart.io/chart?width=800&c=${encodeURIComponent(
+      JSON.stringify(subscriptionsLinerOverTimeConfig),
+    )}`,
+    users: `https://quickchart.io/chart?width=800&c=${encodeURIComponent(JSON.stringify(usersLinerOverTimeConfig))}`,
+    quarterlyPerformance: `https://quickchart.io/chart?width=800&c=${encodeURIComponent(
+      JSON.stringify(quarterlyConfig),
+    )}`,
+    churn: `https://quickchart.io/chart?width=800&c=${encodeURIComponent(JSON.stringify(churnConfig))}`,
+    mrr: `https://quickchart.io/chart?width=800&c=${encodeURIComponent(JSON.stringify(mrrOverTimeConfig))}`,
+    mrrMonthly: `https://quickchart.io/chart?width=800&c=${encodeURIComponent(JSON.stringify(mrrMonthlyConfig))}`,
+  }
+}
+
+export const html = (data: any, timer: TimerInterface) => {
+  const chartUrls = getChartUrls(event)
+
+  const successfullPaymentsActivity = data.activityStatistics.find(
+    (a: { name: AnalyticsActivity }) => a.name === AnalyticsActivity.PaymentSuccess && Period.Yesterday,
+  )
+  const failedPaymentsActivity = data.activityStatistics.find(
+    (a: { name: AnalyticsActivity }) => a.name === AnalyticsActivity.PaymentFailed && Period.Yesterday,
+  )
+  const limitedDiscountPurchasedActivity = data.activityStatistics.find(
+    (a: { name: AnalyticsActivity }) => a.name === AnalyticsActivity.LimitedDiscountOfferPurchased && Period.Yesterday,
+  )
+  const subscriptionPurchasingOverTime = data.activityStatisticsOverTime.find(
+    (a: { name: AnalyticsActivity; period: Period }) =>
+      a.name === AnalyticsActivity.SubscriptionPurchased && a.period === Period.Last30Days,
+  )
+  const subscriptionRenewingOverTime = data.activityStatisticsOverTime.find(
+    (a: { name: AnalyticsActivity; period: Period }) =>
+      a.name === AnalyticsActivity.SubscriptionRenewed && a.period === Period.Last30Days,
+  )
+  const subscriptionRefundingOverTime = data.activityStatisticsOverTime.find(
+    (a: { name: AnalyticsActivity; period: Period }) =>
+      a.name === AnalyticsActivity.SubscriptionRefunded && a.period === Period.Last30Days,
+  )
+  const subscriptionCancelledOverTime = data.activityStatisticsOverTime.find(
+    (a: { name: AnalyticsActivity; period: Period }) =>
+      a.name === AnalyticsActivity.SubscriptionCancelled && a.period === Period.Last30Days,
+  )
+  const subscriptionReactivatedOverTime = data.activityStatisticsOverTime.find(
+    (a: { name: AnalyticsActivity; period: Period }) =>
+      a.name === AnalyticsActivity.SubscriptionReactivated && a.period === Period.Last30Days,
+  )
+  const userRegistrationOverTime = data.activityStatisticsOverTime.find(
+    (a: { name: AnalyticsActivity; period: Period }) =>
+      a.name === AnalyticsActivity.Register && a.period === Period.Last30Days,
+  )
+  const userDeletionOverTime = data.activityStatisticsOverTime.find(
+    (a: { name: AnalyticsActivity; period: Period }) =>
+      a.name === AnalyticsActivity.DeleteAccount && a.period === Period.Last30Days,
+  )
+  const incomeMeasureYesterday = data.statisticMeasures.find(
+    (a: { name: StatisticsMeasure; period: Period }) =>
+      a.name === StatisticsMeasure.Income && a.period === Period.Yesterday,
+  )
+  const refundMeasureYesterday = data.statisticMeasures.find(
+    (a: { name: StatisticsMeasure; period: Period }) =>
+      a.name === StatisticsMeasure.Refunds && a.period === Period.Yesterday,
+  )
+  const incomeYesterday = incomeMeasureYesterday?.totalValue ?? 0
+  const refundsYesterday = refundMeasureYesterday?.totalValue ?? 0
+  const revenueYesterday = incomeYesterday - refundsYesterday
+
+  const subscriptionLengthMeasureYesterday = data.statisticMeasures.find(
+    (a: { name: StatisticsMeasure; period: Period }) =>
+      a.name === StatisticsMeasure.SubscriptionLength && a.period === Period.Yesterday,
+  )
+  const subscriptionLengthDurationYesterday = timer.convertMicrosecondsToTimeStructure(
+    Math.floor(subscriptionLengthMeasureYesterday?.average ?? 0),
+  )
+
+  const subscriptionRemainingTimePercentageMeasureYesterday = data.statisticMeasures.find(
+    (a: { name: StatisticsMeasure; period: Period }) =>
+      a.name === StatisticsMeasure.RemainingSubscriptionTimePercentage && a.period === Period.Yesterday,
+  )
+  const subscriptionRemainingTimePercentageYesterday = Math.floor(
+    subscriptionRemainingTimePercentageMeasureYesterday?.average ?? 0,
+  )
+
+  const registrationLengthMeasureYesterday = data.statisticMeasures.find(
+    (a: { name: StatisticsMeasure; period: Period }) =>
+      a.name === StatisticsMeasure.RegistrationLength && a.period === Period.Yesterday,
+  )
+  const registrationLengthDurationYesterday = timer.convertMicrosecondsToTimeStructure(
+    Math.floor(registrationLengthMeasureYesterday?.average ?? 0),
+  )
+
+  const registrationToSubscriptionMeasureYesterday = data.statisticMeasures.find(
+    (a: { name: StatisticsMeasure; period: Period }) =>
+      a.name === StatisticsMeasure.RegistrationToSubscriptionTime && a.period === Period.Yesterday,
+  )
+  const registrationToSubscriptionDurationYesterday = timer.convertMicrosecondsToTimeStructure(
+    Math.floor(registrationToSubscriptionMeasureYesterday?.average ?? 0),
+  )
+
+  const incomeMeasureThisMonth = data.statisticMeasures.find(
+    (a: { name: StatisticsMeasure; period: Period }) =>
+      a.name === StatisticsMeasure.Income && a.period === Period.ThisMonth,
+  )
+  const refundMeasureThisMonth = data.statisticMeasures.find(
+    (a: { name: StatisticsMeasure; period: Period }) =>
+      a.name === StatisticsMeasure.Refunds && a.period === Period.ThisMonth,
+  )
+  const incomeThisMonth = incomeMeasureThisMonth?.totalValue ?? 0
+  const refundsThisMonth = refundMeasureThisMonth?.totalValue ?? 0
+  const revenueThisMonth = incomeThisMonth - refundsThisMonth
+
+  const subscriptionLengthMeasureThisMonth = data.statisticMeasures.find(
+    (a: { name: StatisticsMeasure; period: Period }) =>
+      a.name === StatisticsMeasure.SubscriptionLength && a.period === Period.ThisMonth,
+  )
+  const subscriptionLengthDurationThisMonth = timer.convertMicrosecondsToTimeStructure(
+    Math.floor(subscriptionLengthMeasureThisMonth?.average ?? 0),
+  )
+
+  const subscriptionRemainingTimePercentageMeasureThisMonth = data.statisticMeasures.find(
+    (a: { name: StatisticsMeasure; period: Period }) =>
+      a.name === StatisticsMeasure.RemainingSubscriptionTimePercentage && a.period === Period.ThisMonth,
+  )
+  const subscriptionRemainingTimePercentageThisMonth = Math.floor(
+    subscriptionRemainingTimePercentageMeasureThisMonth?.average ?? 0,
+  )
+
+  const registrationLengthMeasureThisMonth = data.statisticMeasures.find(
+    (a: { name: StatisticsMeasure; period: Period }) =>
+      a.name === StatisticsMeasure.RegistrationLength && a.period === Period.ThisMonth,
+  )
+  const registrationLengthDurationThisMonth = timer.convertMicrosecondsToTimeStructure(
+    Math.floor(registrationLengthMeasureThisMonth?.average ?? 0),
+  )
+
+  const registrationToSubscriptionMeasureThisMonth = data.statisticMeasures.find(
+    (a: { name: StatisticsMeasure; period: Period }) =>
+      a.name === StatisticsMeasure.RegistrationToSubscriptionTime && a.period === Period.ThisMonth,
+  )
+  const registrationToSubscriptionDurationThisMonth = timer.convertMicrosecondsToTimeStructure(
+    Math.floor(registrationToSubscriptionMeasureThisMonth?.average ?? 0),
+  )
+
+  const plusSubscriptionsInitialAnnualPaymentsYesterday = data.statisticMeasures.find(
+    (a: { name: StatisticsMeasure; period: Period }) =>
+      a.name === StatisticsMeasure.PlusSubscriptionInitialAnnualPaymentsIncome && a.period === Period.Yesterday,
+  )
+  const plusSubscriptionsInitialMonthlyPaymentsYesterday = data.statisticMeasures.find(
+    (a: { name: StatisticsMeasure; period: Period }) =>
+      a.name === StatisticsMeasure.PlusSubscriptionInitialMonthlyPaymentsIncome && a.period === Period.Yesterday,
+  )
+  const plusSubscriptionsRenewingAnnualPaymentsYesterday = data.statisticMeasures.find(
+    (a: { name: StatisticsMeasure; period: Period }) =>
+      a.name === StatisticsMeasure.PlusSubscriptionRenewingAnnualPaymentsIncome && a.period === Period.Yesterday,
+  )
+  const plusSubscriptionsRenewingMonthlyPaymentsYesterday = data.statisticMeasures.find(
+    (a: { name: StatisticsMeasure; period: Period }) =>
+      a.name === StatisticsMeasure.PlusSubscriptionRenewingMonthlyPaymentsIncome && a.period === Period.Yesterday,
+  )
+  const proSubscriptionsInitialAnnualPaymentsYesterday = data.statisticMeasures.find(
+    (a: { name: StatisticsMeasure; period: Period }) =>
+      a.name === StatisticsMeasure.ProSubscriptionInitialAnnualPaymentsIncome && a.period === Period.Yesterday,
+  )
+  const proSubscriptionsInitialMonthlyPaymentsYesterday = data.statisticMeasures.find(
+    (a: { name: StatisticsMeasure; period: Period }) =>
+      a.name === StatisticsMeasure.ProSubscriptionInitialMonthlyPaymentsIncome && a.period === Period.Yesterday,
+  )
+  const proSubscriptionsRenewingAnnualPaymentsYesterday = data.statisticMeasures.find(
+    (a: { name: StatisticsMeasure; period: Period }) =>
+      a.name === StatisticsMeasure.ProSubscriptionRenewingAnnualPaymentsIncome && a.period === Period.Yesterday,
+  )
+  const proSubscriptionsRenewingMonthlyPaymentsYesterday = data.statisticMeasures.find(
+    (a: { name: StatisticsMeasure; period: Period }) =>
+      a.name === StatisticsMeasure.ProSubscriptionRenewingMonthlyPaymentsIncome && a.period === Period.Yesterday,
+  )
+  const plusSubscriptionsInitialAnnualPaymentsThisMonth = data.statisticMeasures.find(
+    (a: { name: StatisticsMeasure; period: Period }) =>
+      a.name === StatisticsMeasure.PlusSubscriptionInitialAnnualPaymentsIncome && a.period === Period.ThisMonth,
+  )
+  const plusSubscriptionsInitialMonthlyPaymentsThisMonth = data.statisticMeasures.find(
+    (a: { name: StatisticsMeasure; period: Period }) =>
+      a.name === StatisticsMeasure.PlusSubscriptionInitialMonthlyPaymentsIncome && a.period === Period.ThisMonth,
+  )
+  const plusSubscriptionsRenewingAnnualPaymentsThisMonth = data.statisticMeasures.find(
+    (a: { name: StatisticsMeasure; period: Period }) =>
+      a.name === StatisticsMeasure.PlusSubscriptionRenewingAnnualPaymentsIncome && a.period === Period.ThisMonth,
+  )
+  const plusSubscriptionsRenewingMonthlyPaymentsThisMonth = data.statisticMeasures.find(
+    (a: { name: StatisticsMeasure; period: Period }) =>
+      a.name === StatisticsMeasure.PlusSubscriptionRenewingMonthlyPaymentsIncome && a.period === Period.ThisMonth,
+  )
+  const proSubscriptionsInitialAnnualPaymentsThisMonth = data.statisticMeasures.find(
+    (a: { name: StatisticsMeasure; period: Period }) =>
+      a.name === StatisticsMeasure.ProSubscriptionInitialAnnualPaymentsIncome && a.period === Period.ThisMonth,
+  )
+  const proSubscriptionsInitialMonthlyPaymentsThisMonth = data.statisticMeasures.find(
+    (a: { name: StatisticsMeasure; period: Period }) =>
+      a.name === StatisticsMeasure.ProSubscriptionInitialMonthlyPaymentsIncome && a.period === Period.ThisMonth,
+  )
+  const proSubscriptionsRenewingAnnualPaymentsThisMonth = data.statisticMeasures.find(
+    (a: { name: StatisticsMeasure; period: Period }) =>
+      a.name === StatisticsMeasure.ProSubscriptionRenewingAnnualPaymentsIncome && a.period === Period.ThisMonth,
+  )
+  const proSubscriptionsRenewingMonthlyPaymentsThisMonth = data.statisticMeasures.find(
+    (a: { name: StatisticsMeasure; period: Period }) =>
+      a.name === StatisticsMeasure.ProSubscriptionRenewingMonthlyPaymentsIncome && a.period === Period.ThisMonth,
+  )
+
+  const mrrOverTime = data.statisticsOverTime.find(
+    (a: { name: string; period: number }) => a.name === 'mrr' && a.period === 27,
+  )
+  const monthlyPlansMrrOverTime = data.statisticsOverTime.find(
+    (a: { name: string; period: number }) => a.name === 'monthly-plans-mrr' && a.period === 27,
+  )
+  const annualPlansMrrOverTime = data.statisticsOverTime.find(
+    (a: { name: string; period: number }) => a.name === 'annual-plans-mrr' && a.period === 27,
+  )
+  const fiveYearPlansMrrOverTime = data.statisticsOverTime.find(
+    (a: { name: string; period: number }) => a.name === 'five-year-plans-mrr' && a.period === 27,
+  )
+  const proPlansMrrOverTime = data.statisticsOverTime.find(
+    (a: { name: string; period: number }) => a.name === 'pro-plans-mrr' && a.period === 27,
+  )
+  const plusPlansMrrOverTime = data.statisticsOverTime.find(
+    (a: { name: string; period: number }) => a.name === 'plus-plans-mrr' && a.period === 27,
+  )
+
+  const today = new Date()
+  const thisMonthPeriodKey = `${today.getFullYear().toString()}-${(today.getMonth() + 1).toString()}`
+  const thisMonthChurn = data.churn.values.find(
+    (value: { periodKey: string }) => value.periodKey === thisMonthPeriodKey,
+  )
+
+  return `      <div>
+<p>Hello,</p>
+<p>
+  <strong>Here are some statistics from yesterday:</strong>
+</p>
+<ul>
+  <li>
+    <b>Payments</b>
+    <ul>
+      <li>
+        Revenue: <b>$${revenueYesterday.toLocaleString('en-US')}</b> (Income: $
+        ${incomeYesterday.toLocaleString('en-US')}, Refunds: $${refundsYesterday.toLocaleString('en-US')})
+      </li>
+      <li>
+        Successfull payments: <b>${successfullPaymentsActivity?.totalCount.toLocaleString('en-US')}</b>
+      </li>
+      <li>
+        Failed payments: <b>${failedPaymentsActivity?.totalCount.toLocaleString('en-US')}</b>
+      </li>
+    </ul>
+  </li>
+  <li>
+    <b>MRR Breakdown</b>
+    <ul>
+      <li>
+        <b>Total:</b> $${mrrOverTime?.counts[mrrOverTime?.counts.length - 1].totalCount.toLocaleString('en-US')}
+      </li>
+      <li>
+        <b>By Subscription Type:</b>
+        <ul>
+          <li>
+            <b>PLUS:</b> $
+            ${plusPlansMrrOverTime?.counts[plusPlansMrrOverTime?.counts.length - 1].totalCount.toLocaleString('en-US')}
+          </li>
+          <li>
+            <b>PRO:</b> $
+            ${proPlansMrrOverTime?.counts[proPlansMrrOverTime?.counts.length - 1].totalCount.toLocaleString('en-US')}
+          </li>
+        </ul>
+      </li>
+      <li>
+        <b>By Billing Frequency:</b>
+        <ul>
+          <li>
+            <b>Monthly:</b> $
+            ${monthlyPlansMrrOverTime?.counts[monthlyPlansMrrOverTime?.counts.length - 1].totalCount.toLocaleString(
+              'en-US',
+            )}
+          </li>
+          <li>
+            <b>Annual:</b> $
+            ${annualPlansMrrOverTime?.counts[annualPlansMrrOverTime?.counts.length - 1].totalCount.toLocaleString(
+              'en-US',
+            )}
+          </li>
+          <li>
+            <b>5-year:</b> $
+            ${fiveYearPlansMrrOverTime?.counts[fiveYearPlansMrrOverTime?.counts.length - 1].totalCount.toLocaleString(
+              'en-US',
+            )}
+          </li>
+        </ul>
+      </li>
+    </ul>
+  </li>
+  <li>
+    <b>Income Breakdown</b>
+    <ul>
+      <li>
+        <b>Plus Subscription:</b>
+        <ul>
+          <li>
+            <b>${plusSubscriptionsInitialMonthlyPaymentsYesterday?.increments.toLocaleString('en-US')}</b>${' '}
+            <i>initial</i> payments on <u>monhtly</u> plan, totaling${' '}
+            <b>$${plusSubscriptionsInitialMonthlyPaymentsYesterday?.totalValue.toLocaleString('en-US')}</b>
+          </li>
+          <li>
+            <b>${plusSubscriptionsInitialAnnualPaymentsYesterday?.increments.toLocaleString('en-US')}</b>${' '}
+            <i>initial</i> payments on <u>annual</u> plan, totaling${' '}
+            <b>$${plusSubscriptionsInitialAnnualPaymentsYesterday?.totalValue.toLocaleString('en-US')}</b>
+          </li>
+          <li>
+            <b>${plusSubscriptionsRenewingMonthlyPaymentsYesterday?.increments.toLocaleString('en-US')}</b>${' '}
+            <i>renewing</i> payments on <u>monthly</u> plan, totaling${' '}
+            <b>$${plusSubscriptionsRenewingMonthlyPaymentsYesterday?.totalValue.toLocaleString('en-US')}</b>
+          </li>
+          <li>
+            <b>${plusSubscriptionsRenewingAnnualPaymentsYesterday?.increments.toLocaleString('en-US')}</b>${' '}
+            <i>renewing</i> payments on <u>annual</u> plan, totaling${' '}
+            <b>$${plusSubscriptionsRenewingAnnualPaymentsYesterday?.totalValue.toLocaleString('en-US')}</b>
+          </li>
+        </ul>
+      </li>
+      <li>
+        <b>Pro Subscription:</b>
+        <ul>
+          <li>
+            <b>${proSubscriptionsInitialMonthlyPaymentsYesterday?.increments.toLocaleString('en-US')}</b>${' '}
+            <i>initial</i> payments on <u>monhtly</u> plan, totaling${' '}
+            <b>$${proSubscriptionsInitialMonthlyPaymentsYesterday?.totalValue.toLocaleString('en-US')}</b>
+          </li>
+          <li>
+            <b>${proSubscriptionsInitialAnnualPaymentsYesterday?.increments.toLocaleString('en-US')}</b>${' '}
+            <i>initial</i> payments on <u>annual</u> plan, totaling${' '}
+            <b>$${proSubscriptionsInitialAnnualPaymentsYesterday?.totalValue.toLocaleString('en-US')}</b>
+          </li>
+          <li>
+            <b>${proSubscriptionsRenewingMonthlyPaymentsYesterday?.increments.toLocaleString('en-US')}</b>${' '}
+            <i>renewing</i> payments on <u>monthly</u> plan, totaling${' '}
+            <b>$${proSubscriptionsRenewingMonthlyPaymentsYesterday?.totalValue.toLocaleString('en-US')}</b>
+          </li>
+          <li>
+            <b>${proSubscriptionsRenewingAnnualPaymentsYesterday?.increments.toLocaleString('en-US')}</b>${' '}
+            <i>renewing</i> payments on <u>annual</u> plan, totaling${' '}
+            <b>$${proSubscriptionsRenewingAnnualPaymentsYesterday?.totalValue.toLocaleString('en-US')}</b>
+          </li>
+        </ul>
+      </li>
+    </ul>
+  </li>
+  <li>
+    <b>Users</b>
+    <ul>
+      <li>
+        Number of users registered:${' '}
+        <b>
+          ${userRegistrationOverTime?.counts[userRegistrationOverTime?.counts.length - 1]?.totalCount.toLocaleString(
+            'en-US',
+          )}
+        </b>
+      </li>
+      <li>
+        Number of users unregistered:${' '}
+        <b>
+          ${userDeletionOverTime?.counts[userDeletionOverTime?.counts.length - 1]?.totalCount.toLocaleString('en-US')}
+        </b>${' '}
+        (average account duration: ${registrationLengthDurationYesterday.days} days${' '}
+        ${registrationLengthDurationYesterday.hours} hours ${registrationLengthDurationYesterday.minutes} minutes)
+      </li>
+    </ul>
+  </li>
+  <li>
+    <b>Subscriptions</b>
+    <ul>
+      <li>
+        Number of subscriptions purchased:${' '}
+        <b>
+          ${subscriptionPurchasingOverTime?.counts[
+            subscriptionPurchasingOverTime?.counts.length - 1
+          ]?.totalCount.toLocaleString('en-US')}
+        </b>${' '}
+        (includes <b>${limitedDiscountPurchasedActivity?.totalCount.toLocaleString('en-US')}</b> limited time
+        offer purchases)
+      </li>
+      <li>
+        Number of subscriptions renewed:${' '}
+        <b>
+          ${subscriptionRenewingOverTime?.counts[
+            subscriptionRenewingOverTime?.counts.length - 1
+          ]?.totalCount.toLocaleString('en-US')}
+        </b>
+      </li>
+      <li>
+        Number of subscriptions refunded:${' '}
+        <b>
+          ${subscriptionRefundingOverTime?.counts[
+            subscriptionRefundingOverTime?.counts.length - 1
+          ]?.totalCount.toLocaleString('en-US')}
+        </b>
+      </li>
+      <li>
+        Number of subscriptions cancelled:${' '}
+        <b>
+          ${subscriptionCancelledOverTime?.counts[
+            subscriptionCancelledOverTime?.counts.length - 1
+          ]?.totalCount.toLocaleString('en-US')}
+        </b>${' '}
+        (average subscription duration: ${subscriptionLengthDurationYesterday.days} days${' '}
+        ${subscriptionLengthDurationYesterday.hours} hours ${subscriptionLengthDurationYesterday.minutes} minutes,
+        average remaining subscription percentage: ${subscriptionRemainingTimePercentageYesterday}%)
+      </li>
+      <li>
+        Number of subscriptions reactivated:${' '}
+        <b>
+          ${subscriptionReactivatedOverTime?.counts[
+            subscriptionReactivatedOverTime?.counts.length - 1
+          ]?.totalCount.toLocaleString('en-US')}
+        </b>
+      </li>
+      <li>
+        Average time from registration to subscription purchase:${' '}
+        <b>
+          ${registrationToSubscriptionDurationYesterday.days} days${' '}
+          ${registrationToSubscriptionDurationYesterday.hours} hours${' '}
+          ${registrationToSubscriptionDurationYesterday.minutes} minutes
+        </b>
+      </li>
+    </ul>
+  </li>
+</ul>
+<p>
+  <strong>Here are some statistics from last 30 days:</strong>
+</p>
+<ul>
+  <li>
+    <b>Payments (This Month)</b>
+    <ul>
+      <li>
+        Revenue: <b>$${revenueThisMonth.toLocaleString('en-US')}</b>
+      </li>
+      <li>
+        Income: <b>$${incomeThisMonth.toLocaleString('en-US')}</b>
+      </li>
+      <li>
+        Refunds: <b>$${refundsThisMonth.toLocaleString('en-US')}</b>
+      </li>
+    </ul>
+  </li>
+  <li>
+    <b>Income Breakdown (This Month)</b>
+    <ul>
+      <li>
+        <b>Plus Subscription:</b>
+        <ul>
+          <li>
+            <b>${plusSubscriptionsInitialMonthlyPaymentsThisMonth?.increments.toLocaleString('en-US')}</b>${' '}
+            <i>initial</i> payments on <u>monhtly</u> plan, totaling${' '}
+            <b>$${plusSubscriptionsInitialMonthlyPaymentsThisMonth?.totalValue.toLocaleString('en-US')}</b>
+          </li>
+          <li>
+            <b>${plusSubscriptionsInitialAnnualPaymentsThisMonth?.increments.toLocaleString('en-US')}</b>${' '}
+            <i>initial</i> payments on <u>annual</u> plan, totaling${' '}
+            <b>$${plusSubscriptionsInitialAnnualPaymentsThisMonth?.totalValue.toLocaleString('en-US')}</b>
+          </li>
+          <li>
+            <b>${plusSubscriptionsRenewingMonthlyPaymentsThisMonth?.increments.toLocaleString('en-US')}</b>${' '}
+            <i>renewing</i> payments on <u>monthly</u> plan, totaling${' '}
+            <b>$${plusSubscriptionsRenewingMonthlyPaymentsThisMonth?.totalValue.toLocaleString('en-US')}</b>
+          </li>
+          <li>
+            <b>${plusSubscriptionsRenewingAnnualPaymentsThisMonth?.increments.toLocaleString('en-US')}</b>${' '}
+            <i>renewing</i> payments on <u>annual</u> plan, totaling${' '}
+            <b>$${plusSubscriptionsRenewingAnnualPaymentsThisMonth?.totalValue.toLocaleString('en-US')}</b>
+          </li>
+        </ul>
+      </li>
+      <li>
+        <b>Pro Subscription:</b>
+        <ul>
+          <li>
+            <b>${proSubscriptionsInitialMonthlyPaymentsThisMonth?.increments.toLocaleString('en-US')}</b>${' '}
+            <i>initial</i> payments on <u>monhtly</u> plan, totaling${' '}
+            <b>$${proSubscriptionsInitialMonthlyPaymentsThisMonth?.totalValue.toLocaleString('en-US')}</b>
+          </li>
+          <li>
+            <b>${proSubscriptionsInitialAnnualPaymentsThisMonth?.increments.toLocaleString('en-US')}</b>${' '}
+            <i>initial</i> payments on <u>annual</u> plan, totaling${' '}
+            <b>$${proSubscriptionsInitialAnnualPaymentsThisMonth?.totalValue.toLocaleString('en-US')}</b>
+          </li>
+          <li>
+            <b>${proSubscriptionsRenewingMonthlyPaymentsThisMonth?.increments.toLocaleString('en-US')}</b>${' '}
+            <i>renewing</i> payments on <u>monthly</u> plan, totaling${' '}
+            <b>$${proSubscriptionsRenewingMonthlyPaymentsThisMonth?.totalValue.toLocaleString('en-US')}</b>
+          </li>
+          <li>
+            <b>${proSubscriptionsRenewingAnnualPaymentsThisMonth?.increments.toLocaleString('en-US')}</b>${' '}
+            <i>renewing</i> payments on <u>annual</u> plan, totaling${' '}
+            <b>$${proSubscriptionsRenewingAnnualPaymentsThisMonth?.totalValue.toLocaleString('en-US')}</b>
+          </li>
+        </ul>
+      </li>
+    </ul>
+  </li>
+  <li>
+    <b>Users</b>
+    <ul>
+      <li>
+        Number of users registered: <b>${userRegistrationOverTime?.totalCount.toLocaleString('en-US')}</b>
+      </li>
+      <li>
+        Number of users unregistered: <b>${userDeletionOverTime?.totalCount.toLocaleString('en-US')}</b>
+      </li>
+      <li>
+        Average account duration this month:${' '}
+        <b>
+          ${registrationLengthDurationThisMonth.days} days ${registrationLengthDurationThisMonth.hours} hours${' '}
+          ${registrationLengthDurationThisMonth.minutes} minutes
+        </b>
+      </li>
+    </ul>
+  </li>
+  <li>
+    <b>Subscriptions</b>
+    <ul>
+      <li>
+        Number of subscriptions purchased:${' '}
+        <b>${subscriptionPurchasingOverTime?.totalCount.toLocaleString('en-US')}</b>
+      </li>
+      <li>
+        Number of subscriptions renewed:${' '}
+        <b>${subscriptionRenewingOverTime?.totalCount.toLocaleString('en-US')}</b>
+      </li>
+      <li>
+        Number of subscriptions refunded:${' '}
+        <b>${subscriptionRefundingOverTime?.totalCount.toLocaleString('en-US')}</b>
+      </li>
+      <li>
+        Number of subscriptions cancelled:${' '}
+        <b>${subscriptionCancelledOverTime?.totalCount.toLocaleString('en-US')}</b>
+      </li>
+      <li>
+        Number of subscriptions reactivated:${' '}
+        <b>${subscriptionReactivatedOverTime?.totalCount.toLocaleString('en-US')}</b>
+      </li>
+      <li>
+        Average subscription duration this month:${' '}
+        <b>
+          ${subscriptionLengthDurationThisMonth.days} days ${subscriptionLengthDurationThisMonth.hours} hours${' '}
+          ${subscriptionLengthDurationThisMonth.minutes} minutes
+        </b>
+      </li>
+      <li>
+        Average subscription remaining percentage this month:${' '}
+        <b>${subscriptionRemainingTimePercentageThisMonth}%</b>
+      </li>
+      <li>
+        Average time from registration to subscription purchase this month:${' '}
+        <b>
+          ${registrationToSubscriptionDurationThisMonth.days} days${' '}
+          ${registrationToSubscriptionDurationThisMonth.hours} hours${' '}
+          ${registrationToSubscriptionDurationThisMonth.minutes} minutes
+        </b>
+      </li>
+    </ul>
+  </li>
+</ul>
+<p>
+  <strong>Here is the MRR chart over 30 days:</strong>
+</p>
+<img src=${chartUrls.mrr}></img>
+<p>
+  <strong>Here is the MRR Monthly chart this year:</strong>
+</p>
+<img src=${chartUrls.mrrMonthly}></img>
+<p>
+  <strong>Here is the subscription chart over 30 days:</strong>
+</p>
+<img src=${chartUrls.subscriptions}></img>
+<p>
+  <strong>Here is the users chart over 30 days:</strong>
+</p>
+<img src=${chartUrls.users}></img>
+<p>
+  <strong>Here is the monthly churn rate percentage:</strong>
+</p>
+<p>✅ GREAT! Up to 7% 🔶 OKAY: 8-10% 🩸 BAD: 11 -15 % 🚨 TERRIBLE! 16-20%</p>
+<p>Churn is calculated by the following formula:</p>
+<p>
+  ( Existing Customers Churn [${thisMonthChurn?.existingCustomersChurn}] + New Customers Churn [
+  ${thisMonthChurn?.newCustomersChurn}] ) * 100 / Average Customers Count This Month [
+  ${thisMonthChurn?.averageCustomersCount}]
+</p>
+<img src=${chartUrls.churn}></img>
+<p>
+  <strong>Here is quarterly performance chart:</strong>
+</p>
+<img src=${chartUrls.quarterlyPerformance}></img>
+<p>Thanks,SN</p>
+</div>`
+}

+ 11 - 46
packages/analytics/src/Domain/Event/DomainEventFactory.ts

@@ -1,6 +1,6 @@
 /* istanbul ignore file */
 
-import { DomainEventService, DailyAnalyticsReportGeneratedEvent } from '@standardnotes/domain-events'
+import { DomainEventService, EmailRequestedEvent } from '@standardnotes/domain-events'
 import { TimerInterface } from '@standardnotes/time'
 import { inject, injectable } from 'inversify'
 import TYPES from '../../Bootstrap/Types'
@@ -9,55 +9,20 @@ import { DomainEventFactoryInterface } from './DomainEventFactoryInterface'
 @injectable()
 export class DomainEventFactory implements DomainEventFactoryInterface {
   constructor(@inject(TYPES.Timer) private timer: TimerInterface) {}
-
-  createDailyAnalyticsReportGeneratedEvent(dto: {
-    activityStatistics: Array<{
-      name: string
-      retention: number
-      totalCount: number
-    }>
-    statisticMeasures: Array<{
-      name: string
-      totalValue: number
-      average: number
-      increments: number
-      period: number
-    }>
-    activityStatisticsOverTime: Array<{
-      name: string
-      period: number
-      counts: Array<{
-        periodKey: string
-        totalCount: number
-      }>
-      totalCount: number
-    }>
-    statisticsOverTime: Array<{
-      name: string
-      period: number
-      counts: Array<{
-        periodKey: string
-        totalCount: number
-      }>
-    }>
-    churn: {
-      periodKeys: Array<string>
-      values: Array<{
-        rate: number
-        periodKey: string
-        averageCustomersCount: number
-        existingCustomersChurn: number
-        newCustomersChurn: number
-      }>
-    }
-  }): DailyAnalyticsReportGeneratedEvent {
+  createEmailRequestedEvent(dto: {
+    userEmail: string
+    messageIdentifier: string
+    level: string
+    body: string
+    subject: string
+  }): EmailRequestedEvent {
     return {
-      type: 'DAILY_ANALYTICS_REPORT_GENERATED',
+      type: 'EMAIL_REQUESTED',
       createdAt: this.timer.getUTCDate(),
       meta: {
         correlation: {
-          userIdentifier: '',
-          userIdentifierType: 'uuid',
+          userIdentifier: dto.userEmail,
+          userIdentifierType: 'email',
         },
         origin: DomainEventService.Analytics,
       },

+ 8 - 42
packages/analytics/src/Domain/Event/DomainEventFactoryInterface.ts

@@ -1,45 +1,11 @@
-import { DailyAnalyticsReportGeneratedEvent } from '@standardnotes/domain-events'
+import { EmailRequestedEvent } from '@standardnotes/domain-events'
 
 export interface DomainEventFactoryInterface {
-  createDailyAnalyticsReportGeneratedEvent(dto: {
-    activityStatistics: Array<{
-      name: string
-      retention: number
-      totalCount: number
-    }>
-    statisticMeasures: Array<{
-      name: string
-      totalValue: number
-      average: number
-      increments: number
-      period: number
-    }>
-    activityStatisticsOverTime: Array<{
-      name: string
-      period: number
-      counts: Array<{
-        periodKey: string
-        totalCount: number
-      }>
-      totalCount: number
-    }>
-    statisticsOverTime: Array<{
-      name: string
-      period: number
-      counts: Array<{
-        periodKey: string
-        totalCount: number
-      }>
-    }>
-    churn: {
-      periodKeys: Array<string>
-      values: Array<{
-        rate: number
-        periodKey: string
-        averageCustomersCount: number
-        existingCustomersChurn: number
-        newCustomersChurn: number
-      }>
-    }
-  }): DailyAnalyticsReportGeneratedEvent
+  createEmailRequestedEvent(dto: {
+    userEmail: string
+    messageIdentifier: string
+    level: string
+    body: string
+    subject: string
+  }): EmailRequestedEvent
 }

+ 0 - 7
packages/domain-events/src/Domain/Event/DailyAnalyticsReportGeneratedEvent.ts

@@ -1,7 +0,0 @@
-import { DomainEventInterface } from './DomainEventInterface'
-import { DailyAnalyticsReportGeneratedEventPayload } from './DailyAnalyticsReportGeneratedEventPayload'
-
-export interface DailyAnalyticsReportGeneratedEvent extends DomainEventInterface {
-  type: 'DAILY_ANALYTICS_REPORT_GENERATED'
-  payload: DailyAnalyticsReportGeneratedEventPayload
-}

+ 0 - 41
packages/domain-events/src/Domain/Event/DailyAnalyticsReportGeneratedEventPayload.ts

@@ -1,41 +0,0 @@
-export interface DailyAnalyticsReportGeneratedEventPayload {
-  activityStatistics: Array<{
-    name: string
-    retention: number
-    totalCount: number
-  }>
-  statisticMeasures: Array<{
-    name: string
-    totalValue: number
-    average: number
-    increments: number
-    period: number
-  }>
-  activityStatisticsOverTime: Array<{
-    name: string
-    period: number
-    counts: Array<{
-      periodKey: string
-      totalCount: number
-    }>
-    totalCount: number
-  }>
-  statisticsOverTime: Array<{
-    name: string
-    period: number
-    counts: Array<{
-      periodKey: string
-      totalCount: number
-    }>
-  }>
-  churn: {
-    periodKeys: Array<string>
-    values: Array<{
-      rate: number
-      averageCustomersCount: number
-      existingCustomersChurn: number
-      newCustomersChurn: number
-      periodKey: string
-    }>
-  }
-}

+ 0 - 2
packages/domain-events/src/Domain/index.ts

@@ -2,8 +2,6 @@ export * from './Event/AccountDeletionRequestedEvent'
 export * from './Event/AccountDeletionRequestedEventPayload'
 export * from './Event/CloudBackupRequestedEvent'
 export * from './Event/CloudBackupRequestedEventPayload'
-export * from './Event/DailyAnalyticsReportGeneratedEvent'
-export * from './Event/DailyAnalyticsReportGeneratedEventPayload'
 export * from './Event/DiscountApplyRequestedEvent'
 export * from './Event/DiscountApplyRequestedEventPayload'
 export * from './Event/DiscountWithdrawRequestedEvent'

+ 2 - 2
yarn.lock

@@ -1795,7 +1795,7 @@ __metadata:
     "@newrelic/winston-enricher": "npm:^4.0.0"
     "@sentry/node": "npm:^7.19.0"
     "@standardnotes/common": "workspace:*"
-    "@standardnotes/domain-core": "workspace:*"
+    "@standardnotes/domain-core": "workspace:^"
     "@standardnotes/domain-events": "workspace:*"
     "@standardnotes/domain-events-infra": "workspace:*"
     "@standardnotes/time": "workspace:*"
@@ -1977,7 +1977,7 @@ __metadata:
   languageName: node
   linkType: hard
 
-"@standardnotes/domain-core@workspace:*, @standardnotes/domain-core@workspace:^, @standardnotes/domain-core@workspace:packages/domain-core":
+"@standardnotes/domain-core@workspace:^, @standardnotes/domain-core@workspace:packages/domain-core":
   version: 0.0.0-use.local
   resolution: "@standardnotes/domain-core@workspace:packages/domain-core"
   dependencies: