Skip to content

Commit ff01827

Browse files
chore: analytic optimization (#70)
* added delay on load * types issue * code refactor * code refactored * code refactored * code refactored * code refactored
1 parent 8dd4a53 commit ff01827

File tree

16 files changed

+748
-173
lines changed

16 files changed

+748
-173
lines changed

actions/analytics/get-analytics.ts

Lines changed: 15 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,13 @@
33
import { PayoutRequest, PurchaseDetails, User } from '@prisma/client';
44
import groupBy from 'lodash.groupby';
55

6-
import { ONE_MINUTE_SEC } from '@/constants/common';
76
import { PayoutRequestStatus } from '@/constants/payments';
8-
import { fetchCachedData } from '@/lib/cache';
97
import { db } from '@/lib/db';
10-
import { stripe } from '@/server/stripe';
118

129
import { getStripeConnect, getStripeConnectPayouts } from './get-stripe-connect';
10+
import { getStripeData } from './get-stripe-data';
1311
import { getTotalProfit } from './get-total-profit';
12+
import { getTotalRevenueData } from './get-total-revenue-data';
1413
import { getTransactions, PurchaseWithCourse, Transaction } from './get-transactions';
1514

1615
type Sales = (ReturnType<typeof groupByCourse>[number][number]['details'] & { title: string })[];
@@ -72,27 +71,14 @@ export const getAnalytics = async (userId: string) => {
7271

7372
const userIds = [...new Set(purchases.map((ps) => ps.userId))];
7473
const users = await db.user.findMany({ where: { id: { in: userIds } } });
75-
const paymentIntents = [...new Set(purchases.map((ps) => ps.details?.paymentIntent))].filter(
76-
(pi) => pi,
77-
);
78-
79-
const stripeAccountId = await db.stripeConnectAccount.findUnique({ where: { userId } });
80-
const stripeAccount = stripeAccountId?.stripeAccountId
81-
? await stripe.accounts.retrieve(stripeAccountId.stripeAccountId)
82-
: null;
8374

84-
const stripeAccountBalance = stripeAccountId?.stripeAccountId
85-
? await stripe.balance.retrieve({ stripeAccount: stripeAccountId?.stripeAccountId })
86-
: null;
75+
const { account, accountBalance, accountBalanceTransactions, balanceTransactions, charges } =
76+
await getStripeData({ purchases, userId });
8777

88-
const stripeAccountBalanceTransactions = stripeAccount?.id
89-
? await stripe.balanceTransactions.list({ limit: 5 }, { stripeAccount: stripeAccount.id })
90-
: null;
91-
92-
const payouts = stripeAccount?.id
78+
const payouts = account?.id
9379
? await db.payoutRequest.findMany({
9480
where: {
95-
connectAccount: { stripeAccountId: stripeAccount.id },
81+
connectAccount: { stripeAccountId: account.id },
9682
},
9783
select: {
9884
amount: true,
@@ -114,44 +100,6 @@ export const getAnalytics = async (userId: string) => {
114100
.slice(0, 5);
115101
const successfulPayouts = payouts.filter((py) => py.status === PayoutRequestStatus.PAID);
116102

117-
const stripeCharges = (
118-
await Promise.all(
119-
paymentIntents.map(async (pi) => {
120-
const data = await fetchCachedData(
121-
`${userId}-${pi}`,
122-
async () => {
123-
const res = await stripe.charges.list({ payment_intent: pi as string });
124-
125-
return res.data.filter((ch) =>
126-
purchases.find((pc) => pc.details?.paymentIntent === ch.payment_intent),
127-
);
128-
},
129-
ONE_MINUTE_SEC,
130-
);
131-
132-
return data;
133-
}),
134-
)
135-
)
136-
.flat()
137-
.filter((sc) => sc?.balance_transaction);
138-
139-
const stripeBalanceTransactions = await Promise.all(
140-
stripeCharges.map(async (sc) => {
141-
const data = await fetchCachedData(
142-
`${userId}-${sc.id}`,
143-
async () => {
144-
const res = await stripe.balanceTransactions.retrieve(sc.balance_transaction);
145-
146-
return res;
147-
},
148-
ONE_MINUTE_SEC,
149-
);
150-
151-
return data;
152-
}),
153-
);
154-
155103
const fees = await db.fee.findMany();
156104

157105
const groupedEarnings = groupByCourse(purchases, users);
@@ -164,21 +112,22 @@ export const getAnalytics = async (userId: string) => {
164112
qty: others.length,
165113
}));
166114
const map = getMap(sales);
167-
const totalRevenue = stripeBalanceTransactions.reduce(
168-
(revenue, current) => revenue + current.amount,
115+
const totalRevenue = balanceTransactions.reduce(
116+
(revenue: number, current: { amount: number }) => revenue + current.amount,
169117
0,
170118
);
119+
const totalRevenueData = await getTotalRevenueData(balanceTransactions);
171120

172121
const totalProfit = await getTotalProfit(
173-
stripeBalanceTransactions,
122+
balanceTransactions,
174123
totalRevenue,
175124
fees,
176125
successfulPayouts,
177126
);
178-
const transactions = await getTransactions(stripeCharges, purchases, users);
179-
const stripeConnect = await getStripeConnect(stripeAccount, stripeAccountBalance);
127+
const transactions = await getTransactions(charges, purchases, users);
128+
const stripeConnect = await getStripeConnect(account, accountBalance);
180129
const stripeConnectPayouts = await getStripeConnectPayouts(
181-
stripeAccountBalanceTransactions,
130+
accountBalanceTransactions,
182131
declinedPayouts as PayoutRequest[],
183132
);
184133

@@ -191,6 +140,7 @@ export const getAnalytics = async (userId: string) => {
191140
stripeConnectPayouts,
192141
totalProfit,
193142
totalRevenue,
143+
totalRevenueData,
194144
transactions,
195145
};
196146
} catch (error) {
@@ -205,6 +155,7 @@ export const getAnalytics = async (userId: string) => {
205155
stripeConnectPayouts: [] as StripeConnectPayouts,
206156
totalProfit: null,
207157
totalRevenue: 0,
158+
totalRevenueData: [],
208159
transactions: [] as Transaction[],
209160
};
210161
}

actions/analytics/get-stripe-data.ts

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
'use server';
2+
3+
import { Course, Purchase, PurchaseDetails } from '@prisma/client';
4+
5+
import { TEN_MINUTE_SEC } from '@/constants/common';
6+
import { fetchCachedData } from '@/lib/cache';
7+
import { db } from '@/lib/db';
8+
import { sleep } from '@/lib/utils';
9+
import { stripe } from '@/server/stripe';
10+
11+
const BATCH_SIZE = 10;
12+
const DELAY_MS = 500;
13+
14+
type PurchaseType = Purchase & { course: Course | null; details: PurchaseDetails | null };
15+
16+
type GetStripeData = {
17+
purchases: PurchaseType[];
18+
userId: string;
19+
};
20+
21+
const getBatchedItems = <T>(items: T[]) =>
22+
items.reduce(
23+
(batches, item, index) => {
24+
const batchIndex = Math.floor(index / BATCH_SIZE);
25+
26+
batches[batchIndex] ??= [];
27+
28+
if (item) {
29+
batches[batchIndex].push(item);
30+
}
31+
32+
return batches;
33+
},
34+
[] as (typeof items)[],
35+
);
36+
37+
export const getStripeData = async ({ purchases, userId }: GetStripeData) => {
38+
const accountId = await db.stripeConnectAccount.findUnique({ where: { userId } });
39+
const account = accountId?.stripeAccountId
40+
? await stripe.accounts.retrieve(accountId.stripeAccountId)
41+
: null;
42+
43+
const accountBalance = accountId?.stripeAccountId
44+
? await stripe.balance.retrieve({ stripeAccount: accountId.stripeAccountId })
45+
: null;
46+
47+
const accountBalanceTransactions = account?.id
48+
? await stripe.balanceTransactions.list({ limit: 5 }, { stripeAccount: account.id })
49+
: null;
50+
51+
const paymentIntents = [...new Set(purchases.map((ps) => ps.details?.paymentIntent))].filter(
52+
(pi) => pi,
53+
);
54+
55+
const batchedPaymentIntents = getBatchedItems(paymentIntents);
56+
57+
const charges = (
58+
await batchedPaymentIntents.reduce(
59+
async (previousChargesPromise, batch, batchIndex) => {
60+
const previousCharges = await previousChargesPromise;
61+
62+
if (batchIndex > 0) {
63+
await sleep(DELAY_MS);
64+
}
65+
66+
const currentBatchCharges = await Promise.all(
67+
batch.map(async (pi) => {
68+
const data = await fetchCachedData(
69+
`${userId}-${pi}`,
70+
async () => {
71+
const res = await stripe.charges.list({ payment_intent: pi as string });
72+
73+
return res.data.filter((ch) =>
74+
purchases.find((pc) => pc.details?.paymentIntent === ch.payment_intent),
75+
);
76+
},
77+
TEN_MINUTE_SEC,
78+
);
79+
80+
return data;
81+
}),
82+
);
83+
84+
return previousCharges.concat(currentBatchCharges.flat());
85+
},
86+
Promise.resolve([] as any[]),
87+
)
88+
).filter((sc) => sc?.balance_transaction);
89+
90+
const batchedCharges = getBatchedItems(charges);
91+
92+
const balanceTransactions = await batchedCharges.reduce(
93+
async (previousTransactionsPromise: Promise<any[]>, batch: any[], batchIndex: number) => {
94+
const previousTransactions = await previousTransactionsPromise;
95+
96+
if (batchIndex > 0) {
97+
await sleep(DELAY_MS);
98+
}
99+
100+
const currentBatchTransactions = await Promise.all(
101+
batch.map(async (sc: any) => {
102+
const data = await fetchCachedData(
103+
`${userId}-${sc.id}`,
104+
async () => {
105+
const res = await stripe.balanceTransactions.retrieve(sc.balance_transaction);
106+
107+
return res;
108+
},
109+
TEN_MINUTE_SEC,
110+
);
111+
112+
return data;
113+
}),
114+
);
115+
116+
return previousTransactions.concat(currentBatchTransactions);
117+
},
118+
Promise.resolve([] as any[]),
119+
);
120+
121+
return {
122+
account,
123+
accountBalance,
124+
accountBalanceTransactions,
125+
balanceTransactions,
126+
charges,
127+
};
128+
};
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
'use server';
2+
3+
type MonthlyRevenueData = {
4+
totalRevenue: number;
5+
transactionCount: number;
6+
};
7+
8+
type TotalRevenueData = {
9+
average: number;
10+
diff: number | null;
11+
revenue: number;
12+
transactionCount: number;
13+
}[];
14+
15+
export const getTotalRevenueData = async (transactions: any[]) => {
16+
const monthlyData: Record<string, MonthlyRevenueData> = {};
17+
18+
transactions.forEach((transaction) => {
19+
const date = new Date(transaction.created * 1000);
20+
const yearMonth = `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}`;
21+
22+
if (!monthlyData[yearMonth]) {
23+
monthlyData[yearMonth] = { totalRevenue: 0, transactionCount: 0 };
24+
}
25+
26+
monthlyData[yearMonth].totalRevenue += transaction.net;
27+
monthlyData[yearMonth].transactionCount += 1;
28+
});
29+
30+
let previousRevenue: number | null = null;
31+
32+
const totalRevenueData: TotalRevenueData = Object.keys(monthlyData).map((month) => {
33+
const data = monthlyData[month];
34+
35+
const revenue = data.totalRevenue;
36+
const transactionCount = data.transactionCount;
37+
38+
const average = parseFloat((revenue / transactionCount).toFixed(0));
39+
const diff = previousRevenue
40+
? parseFloat((((revenue - previousRevenue) / previousRevenue) * 100).toFixed(2))
41+
: null;
42+
43+
previousRevenue = revenue;
44+
45+
return {
46+
average,
47+
diff,
48+
revenue,
49+
transactionCount,
50+
};
51+
});
52+
53+
return totalRevenueData;
54+
};

actions/analytics/get-transactions.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,10 +66,11 @@ export const getTransactions = async (
6666
userFreePurchases.push({
6767
amount: 0,
6868
billingDetails: {
69-
name: user?.name ?? null,
7069
address: null,
7170
email: user?.email ?? null,
71+
name: user?.name ?? null,
7272
phone: null,
73+
tax_id: null,
7374
},
7475
currency: DEFAULT_CURRENCY,
7576
id: pc.id,

actions/stripe/get-user-subscription.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,14 +47,14 @@ export const getUserSubscription = async (userId = '', noCache = false) => {
4747

4848
return {
4949
cancelAt: stripeSubscription.cancel_at ? fromUnixTime(stripeSubscription.cancel_at) : null,
50-
endPeriod: fromUnixTime(stripeSubscription.current_period_end),
50+
endPeriod: fromUnixTime(stripeSubscription.items.data[0].current_period_end),
5151
price: {
5252
currency: stripeSubscription.items.data[0].price.currency,
5353
unitAmount: stripeSubscription.items.data[0].price.unit_amount,
5454
},
5555
plan: stripeSubscription.items.data[0].plan,
5656
planName: planDescription?.name ?? 'Nova Plus',
57-
startPeriod: fromUnixTime(stripeSubscription.current_period_start),
57+
startPeriod: fromUnixTime(stripeSubscription.items.data[0].current_period_start),
5858
};
5959
};
6060

0 commit comments

Comments
 (0)