From 1a999bb2572fe7d0d6da6d020b929cb587ec805e Mon Sep 17 00:00:00 2001 From: vincanger <70215737+vincanger@users.noreply.github.com> Date: Tue, 14 Nov 2023 16:54:46 +0100 Subject: [PATCH] create job to calc stripe and user stats --- main.wasp | 29 ++++ .../20231114083725_is_admin/migration.sql | 4 + .../migration.sql | 2 + .../20231114094917_daily_stats/migration.sql | 11 ++ migrations/20231114102636_names/migration.sql | 18 +++ .../migration.sql | 8 + .../migration.sql | 3 + src/client/admin/common/icons.tsx | 35 +++++ src/client/admin/common/types.ts | 3 + .../components/DailyActiveUsersChart.tsx | 148 ++++++++++++------ .../admin/components/TotalPaidViewsCard.tsx | 4 +- .../admin/components/TotalPayingUsersCard.tsx | 62 ++++---- .../admin/components/TotalProfitCard.tsx | 57 ------- .../admin/components/TotalRevenueCard.tsx | 62 ++++++++ .../admin/components/TotalSignupsCard.tsx | 66 ++++---- .../admin/pages/Dashboard/ECommerce.tsx | 21 +-- src/server/actions.ts | 1 + src/server/queries.ts | 35 ++++- src/server/webhooks.ts | 32 ++-- src/server/workers/calculateDailyStats.ts | 125 +++++++++++++++ 20 files changed, 525 insertions(+), 201 deletions(-) create mode 100644 migrations/20231114083725_is_admin/migration.sql create mode 100644 migrations/20231114084236_active_timestamp_default/migration.sql create mode 100644 migrations/20231114094917_daily_stats/migration.sql create mode 100644 migrations/20231114102636_names/migration.sql create mode 100644 migrations/20231114112446_unique_daily_stats/migration.sql create mode 100644 migrations/20231114120939_total_revenue/migration.sql create mode 100644 src/client/admin/common/icons.tsx create mode 100644 src/client/admin/common/types.ts delete mode 100644 src/client/admin/components/TotalProfitCard.tsx create mode 100644 src/client/admin/components/TotalRevenueCard.tsx create mode 100644 src/server/workers/calculateDailyStats.ts diff --git a/main.wasp b/main.wasp index 105deaa..a2bb651 100644 --- a/main.wasp +++ b/main.wasp @@ -78,7 +78,9 @@ entity User {=psl id Int @id @default(autoincrement()) email String? @unique password String? + createdAt DateTime @default(now()) lastActiveTimestamp DateTime @default(now()) + isAdmin Boolean @default(false) isEmailVerified Boolean @default(false) emailVerificationSentAt DateTime? passwordResetSentAt DateTime? @@ -125,6 +127,17 @@ entity ContactFormMessage {=psl repliedAt DateTime? psl=} +entity DailyStats {=psl + id Int @id @default(autoincrement()) + date DateTime @default(now()) @unique + userCount Int @default(0) + paidUserCount Int @default(0) + userDelta Int @default(0) + paidUserDelta Int @default(0) + totalRevenue Int @default(0) + totalProfit Int @default(0) +psl=} + /* 📡 These are the Wasp Routes (You can protect them easily w/ 'authRequired: true'); * https://wasp-lang.dev/docs/tutorial/pages */ @@ -273,6 +286,11 @@ query getRelatedObjects { entities: [User, RelatedObject] } +query getDailyStats { + fn: import { getDailyStats } from "@server/queries.js", + entities: [User, DailyStats] +} + /* * 📡 These are custom Wasp API Endpoints. Use them for callbacks, webhooks, etc. * https://wasp-lang.dev/docs/advanced/apis @@ -298,3 +316,14 @@ job emailChecker { }, entities: [User] } + +job dailyStats { + executor: PgBoss, + perform: { + fn: import { calculateDailyStats } from "@server/workers/calculateDailyStats.js" + }, + schedule: { + cron: "0 * * * *" // every hour + }, + entities: [User, DailyStats] +} diff --git a/migrations/20231114083725_is_admin/migration.sql b/migrations/20231114083725_is_admin/migration.sql new file mode 100644 index 0000000..45ad717 --- /dev/null +++ b/migrations/20231114083725_is_admin/migration.sql @@ -0,0 +1,4 @@ +-- AlterTable +ALTER TABLE "User" ADD COLUMN "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, +ADD COLUMN "isAdmin" BOOLEAN NOT NULL DEFAULT false, +ALTER COLUMN "lastActiveTimestamp" DROP DEFAULT; diff --git a/migrations/20231114084236_active_timestamp_default/migration.sql b/migrations/20231114084236_active_timestamp_default/migration.sql new file mode 100644 index 0000000..8dabad5 --- /dev/null +++ b/migrations/20231114084236_active_timestamp_default/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "User" ALTER COLUMN "lastActiveTimestamp" SET DEFAULT CURRENT_TIMESTAMP; diff --git a/migrations/20231114094917_daily_stats/migration.sql b/migrations/20231114094917_daily_stats/migration.sql new file mode 100644 index 0000000..c923f02 --- /dev/null +++ b/migrations/20231114094917_daily_stats/migration.sql @@ -0,0 +1,11 @@ +-- CreateTable +CREATE TABLE "DailyStats" ( + "id" SERIAL NOT NULL, + "date" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "newUsers" INTEGER NOT NULL DEFAULT 0, + "newPaidUsers" INTEGER NOT NULL DEFAULT 0, + "newUsersDelta" INTEGER NOT NULL DEFAULT 0, + "newPaidUsersDelta" INTEGER NOT NULL DEFAULT 0, + + CONSTRAINT "DailyStats_pkey" PRIMARY KEY ("id") +); diff --git a/migrations/20231114102636_names/migration.sql b/migrations/20231114102636_names/migration.sql new file mode 100644 index 0000000..a28a489 --- /dev/null +++ b/migrations/20231114102636_names/migration.sql @@ -0,0 +1,18 @@ +/* + Warnings: + + - You are about to drop the column `newPaidUsers` on the `DailyStats` table. All the data in the column will be lost. + - You are about to drop the column `newPaidUsersDelta` on the `DailyStats` table. All the data in the column will be lost. + - You are about to drop the column `newUsers` on the `DailyStats` table. All the data in the column will be lost. + - You are about to drop the column `newUsersDelta` on the `DailyStats` table. All the data in the column will be lost. + +*/ +-- AlterTable +ALTER TABLE "DailyStats" DROP COLUMN "newPaidUsers", +DROP COLUMN "newPaidUsersDelta", +DROP COLUMN "newUsers", +DROP COLUMN "newUsersDelta", +ADD COLUMN "paidUserCount" INTEGER NOT NULL DEFAULT 0, +ADD COLUMN "paidUserDelta" INTEGER NOT NULL DEFAULT 0, +ADD COLUMN "userCount" INTEGER NOT NULL DEFAULT 0, +ADD COLUMN "userDelta" INTEGER NOT NULL DEFAULT 0; diff --git a/migrations/20231114112446_unique_daily_stats/migration.sql b/migrations/20231114112446_unique_daily_stats/migration.sql new file mode 100644 index 0000000..790df55 --- /dev/null +++ b/migrations/20231114112446_unique_daily_stats/migration.sql @@ -0,0 +1,8 @@ +/* + Warnings: + + - A unique constraint covering the columns `[date]` on the table `DailyStats` will be added. If there are existing duplicate values, this will fail. + +*/ +-- CreateIndex +CREATE UNIQUE INDEX "DailyStats_date_key" ON "DailyStats"("date"); diff --git a/migrations/20231114120939_total_revenue/migration.sql b/migrations/20231114120939_total_revenue/migration.sql new file mode 100644 index 0000000..1ec91e7 --- /dev/null +++ b/migrations/20231114120939_total_revenue/migration.sql @@ -0,0 +1,3 @@ +-- AlterTable +ALTER TABLE "DailyStats" ADD COLUMN "totalProfit" INTEGER NOT NULL DEFAULT 0, +ADD COLUMN "totalRevenue" INTEGER NOT NULL DEFAULT 0; diff --git a/src/client/admin/common/icons.tsx b/src/client/admin/common/icons.tsx new file mode 100644 index 0000000..f2629fa --- /dev/null +++ b/src/client/admin/common/icons.tsx @@ -0,0 +1,35 @@ +export function UpArrow() { + return ( + + + + ); +} + +export function DownArrow() { + return ( + + + + ); +} diff --git a/src/client/admin/common/types.ts b/src/client/admin/common/types.ts new file mode 100644 index 0000000..58748c9 --- /dev/null +++ b/src/client/admin/common/types.ts @@ -0,0 +1,3 @@ +import { DailyStats } from "@wasp/entities"; + +export type DailyStatsProps = { dailyStats?: DailyStats; weeklyStats?:DailyStats[], isLoading?: Boolean } \ No newline at end of file diff --git a/src/client/admin/components/DailyActiveUsersChart.tsx b/src/client/admin/components/DailyActiveUsersChart.tsx index 93085ea..2ea3458 100644 --- a/src/client/admin/components/DailyActiveUsersChart.tsx +++ b/src/client/admin/components/DailyActiveUsersChart.tsx @@ -1,6 +1,7 @@ import { ApexOptions } from 'apexcharts'; -import React, { useState } from 'react'; +import React, { useState, useMemo, useEffect } from 'react'; import ReactApexChart from 'react-apexcharts'; +import { DailyStatsProps } from '../common/types'; const options: ApexOptions = { legend: { @@ -10,7 +11,7 @@ const options: ApexOptions = { }, colors: ['#3C50E0', '#80CAEE'], chart: { - fontFamily: 'Satoshi, sans-serif', + fontFamily: 'Satoshi, sans-serif', height: 335, type: 'area', dropShadow: { @@ -83,20 +84,6 @@ const options: ApexOptions = { }, xaxis: { type: 'category', - categories: [ - 'Sep', - 'Oct', - 'Nov', - 'Dec', - 'Jan', - 'Feb', - 'Mar', - 'Apr', - 'May', - 'Jun', - 'Jul', - 'Aug', - ], axisBorder: { show: false, }, @@ -122,53 +109,117 @@ interface ChartOneState { }[]; } -const DailyActiveUsersChart: React.FC = () => { +const DailyActiveUsersChart = ({ weeklyStats, isLoading }: DailyStatsProps) => { + const dailyRevenueArray = useMemo(() => { + if (!!weeklyStats && weeklyStats?.length > 0) { + const sortedWeeks = weeklyStats?.sort((a, b) => { + return new Date(a.date).getTime() - new Date(b.date).getTime(); + }); + return sortedWeeks.map((stat) => stat.totalRevenue); + } + }, [weeklyStats]); + + const daysOfWeekArr = useMemo(() => { + if (!!weeklyStats && weeklyStats?.length > 0) { + const datesArr = weeklyStats?.map((stat) => { + // get day of week, month, and day of month + const dateArr = stat.date.toString().split(' '); + return dateArr.slice(0, 3).join(' '); + }); + return datesArr; + } + }, [weeklyStats]); + const [state, setState] = useState({ series: [ { - name: 'Product One', - data: [23, 0], - }, - - { - name: 'Product Two', - data: [30, 25, 36, 30, 45, 35, 64, 52, 59, 36, 39, 51], + name: 'Profit', + data: [4, 7, 10, 11, 13, 14, 17], }, ], }); + const [chartOptions, setChartOptions] = useState(options); + + useEffect(() => { + if (dailyRevenueArray && dailyRevenueArray.length > 0) { + setState((prevState) => { + // Check if a "Revenue" series already exists + const existingSeriesIndex = prevState.series.findIndex((series) => series.name === 'Revenue'); + + if (existingSeriesIndex >= 0) { + // Update existing "Revenue" series data + return { + ...prevState, + series: prevState.series.map((serie, index) => { + if (index === existingSeriesIndex) { + return { ...serie, data: dailyRevenueArray }; + } + return serie; + }), + }; + } else { + // Add "Revenue" series as it does not exist yet + return { + ...prevState, + series: [ + ...prevState.series, + { + name: 'Revenue', + data: dailyRevenueArray, + }, + ], + }; + } + }); + } + }, [dailyRevenueArray]); + + useEffect(() => { + console.log('ooptions categories: ', options?.xaxis?.categories); + console.log('days of week arr: ', daysOfWeekArr); + if (!!daysOfWeekArr && daysOfWeekArr?.length > 0) { + setChartOptions({ + ...options, + xaxis: { + ...options.xaxis, + categories: daysOfWeekArr, + }, + }); + } + }, [daysOfWeekArr]); return ( -
-
-
-
- - +
+
+
+
+ + -
-

Total Revenue

-

12.04.2022 - 12.05.2022

+
+

Total Profit

+

Last 7 Days

-
- - +
+ + -
-

Total Sales

-

12.04.2022 - 12.05.2022

+
+

Total Revenue

+

Last 7 Days

-
-
- - -
@@ -176,13 +227,8 @@ const DailyActiveUsersChart: React.FC = () => {
-
- +
+
diff --git a/src/client/admin/components/TotalPaidViewsCard.tsx b/src/client/admin/components/TotalPaidViewsCard.tsx index eb7cd7c..ab7a606 100644 --- a/src/client/admin/components/TotalPaidViewsCard.tsx +++ b/src/client/admin/components/TotalPaidViewsCard.tsx @@ -24,9 +24,9 @@ const TotalPageViewsCard = () => {

- $3.456K + 3.456K

- Total views + Total page views
diff --git a/src/client/admin/components/TotalPayingUsersCard.tsx b/src/client/admin/components/TotalPayingUsersCard.tsx index 082e735..39e8311 100644 --- a/src/client/admin/components/TotalPayingUsersCard.tsx +++ b/src/client/admin/components/TotalPayingUsersCard.tsx @@ -1,49 +1,45 @@ -const TotalPayingUsersCard = () => { +import { useMemo } from 'react'; +import { UpArrow, DownArrow } from '../common/icons'; +import type { DailyStatsProps } from '../common/types'; + +const TotalPayingUsersCard = ({ dailyStats, isLoading }: DailyStatsProps) => { + const isDeltaPositive = useMemo(() => { + return !!dailyStats?.paidUserDelta && dailyStats?.paidUserDelta > 0; + }, [dailyStats]); + return ( -
-
+
+
-
+
-

- 2.450 -

- Total Product +

{dailyStats?.paidUserCount}

+ Total Paying Users
- - 2.59% - - - + + {isLoading ? '...' : dailyStats?.paidUserDelta !== 0 ? dailyStats?.paidUserDelta : '-'} + {dailyStats?.paidUserDelta !== 0 ? isDeltaPositive ? : : null}
diff --git a/src/client/admin/components/TotalProfitCard.tsx b/src/client/admin/components/TotalProfitCard.tsx deleted file mode 100644 index 337dd62..0000000 --- a/src/client/admin/components/TotalProfitCard.tsx +++ /dev/null @@ -1,57 +0,0 @@ -const TotalProfitCard = () => { - return ( -
-
- - - - - -
- -
-
-

- $45,2K -

- Total Profit -
- - - 4.35% - - - - -
-
- ); -}; - -export default TotalProfitCard; diff --git a/src/client/admin/components/TotalRevenueCard.tsx b/src/client/admin/components/TotalRevenueCard.tsx new file mode 100644 index 0000000..4a2b057 --- /dev/null +++ b/src/client/admin/components/TotalRevenueCard.tsx @@ -0,0 +1,62 @@ +import { useMemo, useEffect } from 'react'; +import { UpArrow, DownArrow } from '../common/icons'; +import type { DailyStatsProps } from '../common/types'; + +const TotalRevenueCard = ({dailyStats, weeklyStats, isLoading}: DailyStatsProps) => { + const isDeltaPositive = useMemo(() => { + if (!weeklyStats) return false; + return (weeklyStats[0].totalRevenue - weeklyStats[1]?.totalRevenue) > 0; + }, [weeklyStats]); + + const delta = useMemo(() => { + if (!weeklyStats) return; + return weeklyStats[0].totalRevenue - weeklyStats[1]?.totalRevenue; + }, [weeklyStats]); + + const deltaPercentage = useMemo(() => { + if (!weeklyStats || !weeklyStats[1]?.totalRevenue) return; + return ((weeklyStats[0].totalRevenue - weeklyStats[1]?.totalRevenue) / weeklyStats[1]?.totalRevenue) * 100; + }, [weeklyStats]); + + return ( +
+
+ + + + + +
+ +
+
+

${dailyStats?.totalRevenue}

+ Total Revenue +
+ + + {isLoading ? '...' : !!deltaPercentage ? deltaPercentage + '%' : '-'} + {!!deltaPercentage ? isDeltaPositive ? : : null} + +
+
+ ); +}; + +export default TotalRevenueCard; diff --git a/src/client/admin/components/TotalSignupsCard.tsx b/src/client/admin/components/TotalSignupsCard.tsx index d60191b..5235472 100644 --- a/src/client/admin/components/TotalSignupsCard.tsx +++ b/src/client/admin/components/TotalSignupsCard.tsx @@ -1,53 +1,49 @@ -const TotalSignupsCard = () => { +import { useMemo } from 'react'; +import { UpArrow } from '../common/icons'; +import type { DailyStatsProps } from '../common/types'; + +const TotalSignupsCard = ({ dailyStats, isLoading }: DailyStatsProps) => { + const isDeltaPositive = useMemo(() => { + return !!dailyStats?.userDelta && dailyStats.userDelta > 0; + }, [dailyStats]); + return ( -
-
+
+
-
+
-

- 3.456 -

- Total Users +

{dailyStats?.userCount}

+ Total Signups
- - 0.95% - - - + + {isLoading ? '...' : isDeltaPositive ? dailyStats?.userDelta : '-'} + {!!dailyStats && isDeltaPositive && }
diff --git a/src/client/admin/pages/Dashboard/ECommerce.tsx b/src/client/admin/pages/Dashboard/ECommerce.tsx index 9bc2d6a..1fccfd0 100644 --- a/src/client/admin/pages/Dashboard/ECommerce.tsx +++ b/src/client/admin/pages/Dashboard/ECommerce.tsx @@ -1,26 +1,29 @@ import TotalSignupsCard from '../../components/TotalSignupsCard'; import TotalPageViewsCard from '../../components/TotalPaidViewsCard'; import TotalPayingUsersCard from '../../components/TotalPayingUsersCard'; -import TotalProfitCard from '../../components/TotalProfitCard'; +import TotalRevenueCard from '../../components/TotalRevenueCard'; import DailyActiveUsersChart from '../../components/DailyActiveUsersChart'; import ReferrerTable from '../../components/ReferrerTable'; import DefaultLayout from '../../layout/DefaultLayout'; +import { useQuery } from '@wasp/queries'; +import getDailyStats from '@wasp/queries/getDailyStats'; const ECommerce = () => { + const { data: stats, isLoading, error } = useQuery(getDailyStats); + return ( -
+
- - - + + +
-
- - {/* */} +
+ -
+
diff --git a/src/server/actions.ts b/src/server/actions.ts index 66a8aad..321ada4 100644 --- a/src/server/actions.ts +++ b/src/server/actions.ts @@ -161,3 +161,4 @@ export const updateUser: UpdateUser, User> = async (user, context) data: user }); } + diff --git a/src/server/queries.ts b/src/server/queries.ts index cd88827..06203c6 100644 --- a/src/server/queries.ts +++ b/src/server/queries.ts @@ -1,6 +1,11 @@ import HttpError from '@wasp/core/HttpError.js'; -import type { RelatedObject } from '@wasp/entities'; -import type { GetRelatedObjects } from '@wasp/queries/types'; +import type { DailyStats, RelatedObject } from '@wasp/entities'; +import type { GetRelatedObjects, GetDailyStats } from '@wasp/queries/types'; + +type DailyStatsValues = { + dailyStats: DailyStats; + weeklyStats: DailyStats[]; +}; export const getRelatedObjects: GetRelatedObjects = async (args, context) => { if (!context.user) { @@ -9,8 +14,28 @@ export const getRelatedObjects: GetRelatedObjects = async return context.entities.RelatedObject.findMany({ where: { user: { - id: context.user.id - } + id: context.user.id, + }, }, - }) + }); +}; + +export const getDailyStats: GetDailyStats = async (_args, context) => { + if (!context.user?.isAdmin) { + throw new HttpError(401); + } + const dailyStats = await context.entities.DailyStats.findFirstOrThrow({ + orderBy: { + date: 'desc', + }, + }); + + const weeklyStats = await context.entities.DailyStats.findMany({ + orderBy: { + date: 'desc', + }, + take: 7, + }); + + return {dailyStats, weeklyStats}; } \ No newline at end of file diff --git a/src/server/webhooks.ts b/src/server/webhooks.ts index 4d49656..54bfc6b 100644 --- a/src/server/webhooks.ts +++ b/src/server/webhooks.ts @@ -19,8 +19,9 @@ export const STRIPE_WEBHOOK_IPS = [ '54.187.216.72', ]; +// make sure the api version matches the version in the Stripe dashboard const stripe = new Stripe(process.env.STRIPE_KEY!, { - apiVersion: '2022-11-15', + apiVersion: '2022-11-15', // TODO find out where this is in the Stripe dashboard and document }); export const stripeWebhook: StripeWebhook = async (request, response, context) => { @@ -132,25 +133,37 @@ export const stripeWebhook: StripeWebhook = async (request, response, context) = if (subscription.cancel_at_period_end) { console.log('Subscription canceled at period end'); - const customer = await context.entities.User.findFirst({ + let customer = await context.entities.User.findFirst({ where: { stripeId: userStripeId, }, select: { + id: true, email: true, }, }); - if (customer?.email) { - await emailSender.send({ - to: customer.email, - subject: 'We hate to see you go :(', - text: 'We hate to see you go. Here is a sweet offer...', - html: 'We hate to see you go. Here is a sweet offer...', + if (customer) { + await context.entities.User.update({ + where: { + id: customer.id, + }, + data: { + subscriptionStatus: 'canceled', + }, }); + + if (customer.email) { + await emailSender.send({ + to: customer.email, + subject: 'We hate to see you go :(', + text: 'We hate to see you go. Here is a sweet offer...', + html: 'We hate to see you go. Here is a sweet offer...', + }); + } } } - } else if (event.type === 'customer.subscription.deleted' || event.type === 'customer.subscription.canceled') { + } else if (event.type === 'customer.subscription.deleted') { const subscription = event.data.object as Stripe.Subscription; userStripeId = subscription.customer as string; @@ -165,6 +178,7 @@ export const stripeWebhook: StripeWebhook = async (request, response, context) = }, data: { hasPaid: false, + subscriptionStatus: 'deleted', }, }); } else { diff --git a/src/server/workers/calculateDailyStats.ts b/src/server/workers/calculateDailyStats.ts new file mode 100644 index 0000000..0e63788 --- /dev/null +++ b/src/server/workers/calculateDailyStats.ts @@ -0,0 +1,125 @@ +import type { DailyStats } from '@wasp/jobs/dailyStats'; +import Stripe from 'stripe'; + +const stripe = new Stripe(process.env.STRIPE_KEY!, { + apiVersion: '2022-11-15', // TODO find out where this is in the Stripe dashboard and document +}); + +export const calculateDailyStats: DailyStats = async (_args, context) => { + const currentDate = new Date(); + const yesterdaysDate = new Date(new Date().setDate(currentDate.getDate() - 1)); + + try { + const yesterdaysStats = await context.entities.DailyStats.findFirst({ + where: { + date: { + equals: yesterdaysDate, + }, + }, + }); + + const userCount = await context.entities.User.count({}); + // users can have paid but canceled subscriptions which terminate at the end of the period + // we don't want to count those users as current paying users + const paidUserCount = await context.entities.User.count({ + where: { + hasPaid: true, + subscriptionStatus: 'active', + }, + }); + + let userDelta = userCount; + let paidUserDelta = paidUserCount; + if (yesterdaysStats) { + userDelta -= yesterdaysStats.userCount; + paidUserDelta -= yesterdaysStats.paidUserCount; + } + + const newRunningTotal = await calculateTotalRevenue(context); + + await context.entities.DailyStats.upsert({ + where: { + date: currentDate, + }, + create: { + date: currentDate, + userCount, + paidUserCount, + userDelta, + paidUserDelta, + totalRevenue: newRunningTotal, + }, + update: { + userCount, + paidUserCount, + userDelta, + paidUserDelta, + totalRevenue: newRunningTotal, + }, + }); + } catch (error) { + console.error('Error calculating daily stats: ', error); + } +}; + +async function fetchDailyStripeRevenue() { + const startOfDay = new Date(); + startOfDay.setHours(0, 0, 0, 0); // Sets to beginning of day + const startOfDayTimestamp = Math.floor(startOfDay.getTime() / 1000); // Convert to Unix timestamp in seconds + + const endOfDay = new Date(); + endOfDay.setHours(23, 59, 59, 999); // Sets to end of day + const endOfDayTimestamp = Math.floor(endOfDay.getTime() / 1000); // Convert to Unix timestamp in seconds + + let nextPageCursor = undefined; + const allPayments = [] as Stripe.Invoice[]; + + while (true) { + const params = { + query: `created>=${startOfDayTimestamp} AND created<=${endOfDayTimestamp} AND status:"paid"`, + limit: 100, + page: nextPageCursor, + }; + const payments = await stripe.invoices.search(params); + + if (payments.next_page) { + nextPageCursor = payments.next_page; + } + + console.log('\n\nstripe invoice payments: ', payments, '\n\n'); + + payments.data.forEach((invoice) => allPayments.push(invoice)); + + if (!payments.has_more) { + break; + } + } + + const dailyTotalInCents = allPayments.reduce((total, invoice) => { + return total + invoice.amount_paid; + }, 0); + + return dailyTotalInCents; +} + +async function calculateTotalRevenue(context: any) { + const revenueInCents = await fetchDailyStripeRevenue(); + + const revenueInDollars = revenueInCents / 100; + + const lastTotalEntry = await context.entities.DailyStats.find({ + where: { + // date is yesterday + date: { + equals: new Date(new Date().setDate(new Date().getDate() - 1)), + }, + }, + }); + + let newRunningTotal = revenueInDollars; + if (lastTotalEntry) { + newRunningTotal += lastTotalEntry.totalRevenue; + } + + return newRunningTotal; +}