Skip to content

Commit e14eb63

Browse files
Partner tags improvements (dubinc#3855)
Co-authored-by: Steven Tey <stevensteel97@gmail.com>
1 parent a0f8ebc commit e14eb63

24 files changed

Lines changed: 777 additions & 299 deletions

apps/web/app/(ee)/api/commissions/analytics/route.ts

Lines changed: 194 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,12 @@ type CommissionGroupIdQueryRow = {
2727
count: bigint;
2828
};
2929

30+
type CommissionPartnerTagQueryRow = {
31+
partnerTagId: string;
32+
earnings: bigint;
33+
count: bigint;
34+
};
35+
3036
const excludedStatuses = [
3137
CommissionStatus.duplicate,
3238
CommissionStatus.fraud,
@@ -41,6 +47,7 @@ function commissionSqlConditions({
4147
partnerFilter,
4248
typeFilter,
4349
groupIdParam,
50+
partnerTagIdParam,
4451
}: {
4552
programId: string;
4653
startDate: Date;
@@ -49,6 +56,7 @@ function commissionSqlConditions({
4956
partnerFilter: ReturnType<typeof parseFilterValue>;
5057
typeFilter: ReturnType<typeof parseFilterValue> | null;
5158
groupIdParam: string | undefined;
59+
partnerTagIdParam: string | undefined;
5260
}): Prisma.Sql[] {
5361
const conditions: Prisma.Sql[] = [
5462
Prisma.sql`c.programId = ${programId}`,
@@ -94,6 +102,30 @@ function commissionSqlConditions({
94102
}
95103
}
96104

105+
if (partnerTagIdParam) {
106+
const partnerTagFilter = parseFilterValue(partnerTagIdParam);
107+
if (partnerTagFilter) {
108+
const list = Prisma.join(
109+
partnerTagFilter.values.map((v) => Prisma.sql`${v}`),
110+
);
111+
if (partnerTagFilter.sqlOperator === "NOT IN") {
112+
conditions.push(Prisma.sql`NOT EXISTS (
113+
SELECT 1 FROM ProgramPartnerTag ppt
114+
WHERE ppt.programId = c.programId
115+
AND ppt.partnerId = c.partnerId
116+
AND ppt.partnerTagId IN (${list})
117+
)`);
118+
} else {
119+
conditions.push(Prisma.sql`EXISTS (
120+
SELECT 1 FROM ProgramPartnerTag ppt
121+
WHERE ppt.programId = c.programId
122+
AND ppt.partnerId = c.partnerId
123+
AND ppt.partnerTagId IN (${list})
124+
)`);
125+
}
126+
}
127+
}
128+
97129
return conditions;
98130
}
99131

@@ -123,6 +155,10 @@ export const GET = withWorkspace(async ({ workspace, searchParams }) => {
123155
return byGroupId({ programId, parsed, startDate, endDate });
124156
}
125157

158+
if (groupBy === "partnerTagId") {
159+
return byPartnerTagIdId({ programId, parsed, startDate, endDate });
160+
}
161+
126162
if (groupBy === "partnerId") {
127163
return byPartnerId({ programId, parsed, startDate, endDate });
128164
}
@@ -141,9 +177,10 @@ async function byType({
141177
startDate: Date;
142178
endDate: Date;
143179
}) {
144-
const { status, partnerId, groupId, type } = parsed;
180+
const { status, partnerId, groupId, partnerTagId, type } = parsed;
145181
const partnerFilter = parseFilterValue(partnerId);
146182
const groupFilter = parseFilterValue(groupId);
183+
const partnerTagFilter = parseFilterValue(partnerTagId);
147184

148185
const rawTypeFilter = parseFilterValue(type);
149186
const validCommissionTypes = new Set(Object.values(CommissionType));
@@ -167,6 +204,21 @@ async function byType({
167204
? { ...rawTypeFilter, values: validTypeValues }
168205
: null;
169206

207+
const programEnrollmentFilter = {
208+
...(groupFilter && {
209+
groupId:
210+
groupFilter.sqlOperator === "NOT IN"
211+
? { notIn: groupFilter.values }
212+
: { in: groupFilter.values },
213+
}),
214+
...(partnerTagFilter && {
215+
programPartnerTags:
216+
partnerTagFilter.sqlOperator === "NOT IN"
217+
? { none: { partnerTagId: { in: partnerTagFilter.values } } }
218+
: { some: { partnerTagId: { in: partnerTagFilter.values } } },
219+
}),
220+
};
221+
170222
const baseWhere: Prisma.CommissionWhereInput = {
171223
programId,
172224
createdAt: { gte: startDate, lt: endDate },
@@ -183,13 +235,8 @@ async function byType({
183235
? { notIn: typeFilter.values }
184236
: { in: typeFilter.values },
185237
}),
186-
...(groupFilter && {
187-
programEnrollment: {
188-
groupId:
189-
groupFilter.sqlOperator === "NOT IN"
190-
? { notIn: groupFilter.values }
191-
: { in: groupFilter.values },
192-
},
238+
...(Object.keys(programEnrollmentFilter).length > 0 && {
239+
programEnrollment: programEnrollmentFilter,
193240
}),
194241
};
195242

@@ -224,7 +271,7 @@ async function byGroupId({
224271
startDate: Date;
225272
endDate: Date;
226273
}) {
227-
const { status, partnerId, groupId, type } = parsed;
274+
const { status, partnerId, groupId, partnerTagId, type } = parsed;
228275
const partnerFilter = parseFilterValue(partnerId);
229276

230277
const rawTypeFilter = parseFilterValue(type);
@@ -257,6 +304,7 @@ async function byGroupId({
257304
partnerFilter,
258305
typeFilter,
259306
groupIdParam: groupId,
307+
partnerTagIdParam: partnerTagId,
260308
});
261309

262310
const whereClause = Prisma.join(conditions, " AND ");
@@ -308,6 +356,112 @@ async function byGroupId({
308356
return NextResponse.json(commissionAnalyticsSchema.groupId.parse(result));
309357
}
310358

359+
async function byPartnerTagIdId({
360+
programId,
361+
parsed,
362+
startDate,
363+
endDate,
364+
}: {
365+
programId: string;
366+
parsed: CommissionAnalyticsQuery;
367+
startDate: Date;
368+
endDate: Date;
369+
}) {
370+
const { status, partnerId, groupId, partnerTagId, type } = parsed;
371+
const partnerFilter = parseFilterValue(partnerId);
372+
373+
const rawTypeFilter = parseFilterValue(type);
374+
const validCommissionTypes = new Set(Object.values(CommissionType));
375+
376+
const validTypeValues = rawTypeFilter
377+
? (rawTypeFilter.values.filter((v) =>
378+
validCommissionTypes.has(v as CommissionType),
379+
) as CommissionType[])
380+
: [];
381+
382+
if (
383+
rawTypeFilter?.sqlOperator === "IN" &&
384+
rawTypeFilter.values.length > 0 &&
385+
validTypeValues.length === 0
386+
) {
387+
return NextResponse.json(commissionAnalyticsSchema.partnerTagId.parse([]));
388+
}
389+
390+
const typeFilter =
391+
rawTypeFilter && validTypeValues.length > 0
392+
? { ...rawTypeFilter, values: validTypeValues }
393+
: null;
394+
395+
const partnerTagFilter = parseFilterValue(partnerTagId);
396+
397+
const conditions = commissionSqlConditions({
398+
programId,
399+
startDate,
400+
endDate,
401+
status,
402+
partnerFilter,
403+
typeFilter,
404+
groupIdParam: groupId,
405+
partnerTagIdParam: undefined,
406+
});
407+
408+
if (partnerTagFilter) {
409+
const list = Prisma.join(
410+
partnerTagFilter.values.map((v) => Prisma.sql`${v}`),
411+
);
412+
if (partnerTagFilter.sqlOperator === "IN") {
413+
conditions.push(Prisma.sql`ppt.partnerTagId IN (${list})`);
414+
} else {
415+
conditions.push(Prisma.sql`NOT EXISTS (
416+
SELECT 1 FROM ProgramPartnerTag ppt_excl
417+
WHERE ppt_excl.programId = c.programId
418+
AND ppt_excl.partnerId = c.partnerId
419+
AND ppt_excl.partnerTagId IN (${list})
420+
)`);
421+
}
422+
}
423+
424+
const whereClause = Prisma.join(conditions, " AND ");
425+
426+
const rows = await prisma.$queryRaw<CommissionPartnerTagQueryRow[]>(
427+
Prisma.sql`
428+
SELECT
429+
ppt.partnerTagId AS partnerTagId,
430+
SUM(c.earnings) AS earnings,
431+
COUNT(c.id) AS count
432+
FROM Commission c
433+
JOIN ProgramPartnerTag ppt
434+
ON ppt.programId = c.programId
435+
AND ppt.partnerId = c.partnerId
436+
WHERE ${whereClause}
437+
GROUP BY ppt.partnerTagId
438+
ORDER BY earnings DESC`,
439+
);
440+
441+
const partnerTagIds = rows.map((r) => r.partnerTagId);
442+
443+
const partnerTags =
444+
partnerTagIds.length > 0
445+
? await prisma.partnerTag.findMany({
446+
where: { id: { in: partnerTagIds } },
447+
select: { id: true, name: true },
448+
})
449+
: [];
450+
451+
const partnerTagById = new Map(partnerTags.map((t) => [t.id, t]));
452+
453+
const result = rows.map((row) => ({
454+
key: row.partnerTagId,
455+
label: partnerTagById.get(row.partnerTagId)?.name ?? row.partnerTagId,
456+
earnings: Number(row.earnings),
457+
count: Number(row.count),
458+
}));
459+
460+
return NextResponse.json(
461+
commissionAnalyticsSchema.partnerTagId.parse(result),
462+
);
463+
}
464+
311465
async function byPartnerId({
312466
programId,
313467
parsed,
@@ -319,9 +473,10 @@ async function byPartnerId({
319473
startDate: Date;
320474
endDate: Date;
321475
}) {
322-
const { status, partnerId, groupId, type } = parsed;
476+
const { status, partnerId, groupId, partnerTagId, type } = parsed;
323477
const partnerFilter = parseFilterValue(partnerId);
324478
const groupFilter = parseFilterValue(groupId);
479+
const partnerTagFilter = parseFilterValue(partnerTagId);
325480

326481
const rawTypeFilter = parseFilterValue(type);
327482
const validCommissionTypes = new Set(Object.values(CommissionType));
@@ -345,6 +500,21 @@ async function byPartnerId({
345500
? { ...rawTypeFilter, values: validTypeValues }
346501
: null;
347502

503+
const programEnrollmentFilter = {
504+
...(groupFilter && {
505+
groupId:
506+
groupFilter.sqlOperator === "NOT IN"
507+
? { notIn: groupFilter.values }
508+
: { in: groupFilter.values },
509+
}),
510+
...(partnerTagFilter && {
511+
programPartnerTags:
512+
partnerTagFilter.sqlOperator === "NOT IN"
513+
? { none: { partnerTagId: { in: partnerTagFilter.values } } }
514+
: { some: { partnerTagId: { in: partnerTagFilter.values } } },
515+
}),
516+
};
517+
348518
const grouped = await prisma.commission.groupBy({
349519
by: ["partnerId"],
350520
where: {
@@ -363,13 +533,8 @@ async function byPartnerId({
363533
? { notIn: typeFilter.values }
364534
: { in: typeFilter.values },
365535
}),
366-
...(groupFilter && {
367-
programEnrollment: {
368-
groupId:
369-
groupFilter.sqlOperator === "NOT IN"
370-
? { notIn: groupFilter.values }
371-
: { in: groupFilter.values },
372-
},
536+
...(Object.keys(programEnrollmentFilter).length > 0 && {
537+
programEnrollment: programEnrollmentFilter,
373538
}),
374539
},
375540
_sum: { earnings: true },
@@ -418,8 +583,17 @@ async function byTimeseries({
418583
programId: string;
419584
parsed: z.infer<typeof commissionAnalyticsQuerySchema>;
420585
}) {
421-
const { start, end, interval, timezone, status, partnerId, groupId, type } =
422-
parsed;
586+
const {
587+
start,
588+
end,
589+
interval,
590+
timezone,
591+
status,
592+
partnerId,
593+
groupId,
594+
partnerTagId,
595+
type,
596+
} = parsed;
423597

424598
const { startDate, endDate, granularity } = getStartEndDates({
425599
interval,
@@ -449,6 +623,7 @@ async function byTimeseries({
449623
partnerFilter,
450624
typeFilter,
451625
groupIdParam: groupId,
626+
partnerTagIdParam: partnerTagId,
452627
});
453628

454629
const whereClause = Prisma.join(conditions, " AND ");

apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/analytics/applications/use-applications-analytics-filters.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import {
1111
nFormatter,
1212
parseFilterValue,
1313
} from "@dub/utils";
14+
import { useParams } from "next/navigation";
1415
import { useCallback, useMemo } from "react";
1516
import { ApplicationReferralSourceIcon } from "./application-referral-source-icon";
1617
import { useApplicationsAnalytics } from "./use-applications-analytics";
@@ -20,6 +21,7 @@ const FILTER_KEYS = ["partnerId", "country", "referralSource"] as const;
2021

2122
export function useApplicationAnalyticsFilters() {
2223
const { slug } = useWorkspace();
24+
const { tab } = useParams() as { tab?: string };
2325
const { stage } = useApplicationsAnalyticsQuery();
2426
const { searchParamsObj, queryParams } = useRouterStuff();
2527

@@ -35,16 +37,19 @@ export function useApplicationAnalyticsFilters() {
3537
const { data: partners } = useApplicationsAnalytics({
3638
groupBy: "partnerId",
3739
exclude: ["partnerId"],
40+
enabled: tab === "applications",
3841
});
3942

4043
const { data: referralSources } = useApplicationsAnalytics({
4144
groupBy: "referralSource",
4245
exclude: ["referralSource"],
46+
enabled: tab === "applications",
4347
});
4448

4549
const { data: countries } = useApplicationsAnalytics({
4650
groupBy: "country",
4751
exclude: ["country"],
52+
enabled: tab === "applications",
4853
});
4954

5055
const filters = useMemo(

0 commit comments

Comments
 (0)