Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
213 changes: 194 additions & 19 deletions apps/web/app/(ee)/api/commissions/analytics/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,12 @@ type CommissionGroupIdQueryRow = {
count: bigint;
};

type CommissionPartnerTagQueryRow = {
partnerTagId: string;
earnings: bigint;
count: bigint;
};

const excludedStatuses = [
CommissionStatus.duplicate,
CommissionStatus.fraud,
Expand All @@ -41,6 +47,7 @@ function commissionSqlConditions({
partnerFilter,
typeFilter,
groupIdParam,
partnerTagIdParam,
}: {
programId: string;
startDate: Date;
Expand All @@ -49,6 +56,7 @@ function commissionSqlConditions({
partnerFilter: ReturnType<typeof parseFilterValue>;
typeFilter: ReturnType<typeof parseFilterValue> | null;
groupIdParam: string | undefined;
partnerTagIdParam: string | undefined;
}): Prisma.Sql[] {
const conditions: Prisma.Sql[] = [
Prisma.sql`c.programId = ${programId}`,
Expand Down Expand Up @@ -94,6 +102,30 @@ function commissionSqlConditions({
}
}

if (partnerTagIdParam) {
const partnerTagFilter = parseFilterValue(partnerTagIdParam);
if (partnerTagFilter) {
const list = Prisma.join(
partnerTagFilter.values.map((v) => Prisma.sql`${v}`),
);
if (partnerTagFilter.sqlOperator === "NOT IN") {
conditions.push(Prisma.sql`NOT EXISTS (
SELECT 1 FROM ProgramPartnerTag ppt
WHERE ppt.programId = c.programId
AND ppt.partnerId = c.partnerId
AND ppt.partnerTagId IN (${list})
)`);
} else {
conditions.push(Prisma.sql`EXISTS (
SELECT 1 FROM ProgramPartnerTag ppt
WHERE ppt.programId = c.programId
AND ppt.partnerId = c.partnerId
AND ppt.partnerTagId IN (${list})
)`);
}
}
}

return conditions;
}

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

if (groupBy === "partnerTagId") {
return byPartnerTagIdId({ programId, parsed, startDate, endDate });
}

if (groupBy === "partnerId") {
return byPartnerId({ programId, parsed, startDate, endDate });
}
Expand All @@ -141,9 +177,10 @@ async function byType({
startDate: Date;
endDate: Date;
}) {
const { status, partnerId, groupId, type } = parsed;
const { status, partnerId, groupId, partnerTagId, type } = parsed;
const partnerFilter = parseFilterValue(partnerId);
const groupFilter = parseFilterValue(groupId);
const partnerTagFilter = parseFilterValue(partnerTagId);

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

const programEnrollmentFilter = {
...(groupFilter && {
groupId:
groupFilter.sqlOperator === "NOT IN"
? { notIn: groupFilter.values }
: { in: groupFilter.values },
}),
...(partnerTagFilter && {
programPartnerTags:
partnerTagFilter.sqlOperator === "NOT IN"
? { none: { partnerTagId: { in: partnerTagFilter.values } } }
: { some: { partnerTagId: { in: partnerTagFilter.values } } },
}),
};

const baseWhere: Prisma.CommissionWhereInput = {
programId,
createdAt: { gte: startDate, lt: endDate },
Expand All @@ -183,13 +235,8 @@ async function byType({
? { notIn: typeFilter.values }
: { in: typeFilter.values },
}),
...(groupFilter && {
programEnrollment: {
groupId:
groupFilter.sqlOperator === "NOT IN"
? { notIn: groupFilter.values }
: { in: groupFilter.values },
},
...(Object.keys(programEnrollmentFilter).length > 0 && {
programEnrollment: programEnrollmentFilter,
}),
};

Expand Down Expand Up @@ -224,7 +271,7 @@ async function byGroupId({
startDate: Date;
endDate: Date;
}) {
const { status, partnerId, groupId, type } = parsed;
const { status, partnerId, groupId, partnerTagId, type } = parsed;
const partnerFilter = parseFilterValue(partnerId);

const rawTypeFilter = parseFilterValue(type);
Expand Down Expand Up @@ -257,6 +304,7 @@ async function byGroupId({
partnerFilter,
typeFilter,
groupIdParam: groupId,
partnerTagIdParam: partnerTagId,
});

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

async function byPartnerTagIdId({
programId,
parsed,
startDate,
endDate,
}: {
programId: string;
parsed: CommissionAnalyticsQuery;
startDate: Date;
endDate: Date;
}) {
const { status, partnerId, groupId, partnerTagId, type } = parsed;
const partnerFilter = parseFilterValue(partnerId);

const rawTypeFilter = parseFilterValue(type);
const validCommissionTypes = new Set(Object.values(CommissionType));

const validTypeValues = rawTypeFilter
? (rawTypeFilter.values.filter((v) =>
validCommissionTypes.has(v as CommissionType),
) as CommissionType[])
: [];

if (
rawTypeFilter?.sqlOperator === "IN" &&
rawTypeFilter.values.length > 0 &&
validTypeValues.length === 0
) {
return NextResponse.json(commissionAnalyticsSchema.partnerTagId.parse([]));
}

const typeFilter =
rawTypeFilter && validTypeValues.length > 0
? { ...rawTypeFilter, values: validTypeValues }
: null;

const partnerTagFilter = parseFilterValue(partnerTagId);

const conditions = commissionSqlConditions({
programId,
startDate,
endDate,
status,
partnerFilter,
typeFilter,
groupIdParam: groupId,
partnerTagIdParam: undefined,
});

if (partnerTagFilter) {
const list = Prisma.join(
partnerTagFilter.values.map((v) => Prisma.sql`${v}`),
);
if (partnerTagFilter.sqlOperator === "IN") {
conditions.push(Prisma.sql`ppt.partnerTagId IN (${list})`);
} else {
conditions.push(Prisma.sql`NOT EXISTS (
SELECT 1 FROM ProgramPartnerTag ppt_excl
WHERE ppt_excl.programId = c.programId
AND ppt_excl.partnerId = c.partnerId
AND ppt_excl.partnerTagId IN (${list})
)`);
}
}

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

const rows = await prisma.$queryRaw<CommissionPartnerTagQueryRow[]>(
Prisma.sql`
SELECT
ppt.partnerTagId AS partnerTagId,
SUM(c.earnings) AS earnings,
COUNT(c.id) AS count
FROM Commission c
JOIN ProgramPartnerTag ppt
ON ppt.programId = c.programId
AND ppt.partnerId = c.partnerId
WHERE ${whereClause}
GROUP BY ppt.partnerTagId
ORDER BY earnings DESC`,
);

const partnerTagIds = rows.map((r) => r.partnerTagId);

const partnerTags =
partnerTagIds.length > 0
? await prisma.partnerTag.findMany({
where: { id: { in: partnerTagIds } },
select: { id: true, name: true },
})
: [];

const partnerTagById = new Map(partnerTags.map((t) => [t.id, t]));

const result = rows.map((row) => ({
key: row.partnerTagId,
label: partnerTagById.get(row.partnerTagId)?.name ?? row.partnerTagId,
earnings: Number(row.earnings),
count: Number(row.count),
}));

return NextResponse.json(
commissionAnalyticsSchema.partnerTagId.parse(result),
);
}

async function byPartnerId({
programId,
parsed,
Expand All @@ -319,9 +473,10 @@ async function byPartnerId({
startDate: Date;
endDate: Date;
}) {
const { status, partnerId, groupId, type } = parsed;
const { status, partnerId, groupId, partnerTagId, type } = parsed;
const partnerFilter = parseFilterValue(partnerId);
const groupFilter = parseFilterValue(groupId);
const partnerTagFilter = parseFilterValue(partnerTagId);

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

const programEnrollmentFilter = {
...(groupFilter && {
groupId:
groupFilter.sqlOperator === "NOT IN"
? { notIn: groupFilter.values }
: { in: groupFilter.values },
}),
...(partnerTagFilter && {
programPartnerTags:
partnerTagFilter.sqlOperator === "NOT IN"
? { none: { partnerTagId: { in: partnerTagFilter.values } } }
: { some: { partnerTagId: { in: partnerTagFilter.values } } },
}),
};

const grouped = await prisma.commission.groupBy({
by: ["partnerId"],
where: {
Expand All @@ -363,13 +533,8 @@ async function byPartnerId({
? { notIn: typeFilter.values }
: { in: typeFilter.values },
}),
...(groupFilter && {
programEnrollment: {
groupId:
groupFilter.sqlOperator === "NOT IN"
? { notIn: groupFilter.values }
: { in: groupFilter.values },
},
...(Object.keys(programEnrollmentFilter).length > 0 && {
programEnrollment: programEnrollmentFilter,
}),
},
_sum: { earnings: true },
Expand Down Expand Up @@ -418,8 +583,17 @@ async function byTimeseries({
programId: string;
parsed: z.infer<typeof commissionAnalyticsQuerySchema>;
}) {
const { start, end, interval, timezone, status, partnerId, groupId, type } =
parsed;
const {
start,
end,
interval,
timezone,
status,
partnerId,
groupId,
partnerTagId,
type,
} = parsed;

const { startDate, endDate, granularity } = getStartEndDates({
interval,
Expand Down Expand Up @@ -449,6 +623,7 @@ async function byTimeseries({
partnerFilter,
typeFilter,
groupIdParam: groupId,
partnerTagIdParam: partnerTagId,
});

const whereClause = Prisma.join(conditions, " AND ");
Expand Down
12 changes: 10 additions & 2 deletions apps/web/app/(ee)/api/partners/applications/reject/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,23 @@ import { NextResponse } from "next/server";
// POST /api/partners/applications/reject – Reject a pending partner application
export const POST = withWorkspace(
async ({ workspace, req, session }) => {
const { partnerId, rejectionReason, rejectionNote, allowImmediateReapply } =
rejectPartnerSchema.parse(await parseRequestBody(req));
const {
partnerId,
rejectionReason,
rejectionNote,
allowImmediateReapply,
flagForFraud,
flagForFraudReason,
} = rejectPartnerSchema.parse(await parseRequestBody(req));

await rejectPartner({
workspace,
partnerId,
rejectionReason,
rejectionNote,
allowImmediateReapply,
flagForFraud,
flagForFraudReason,
userId: session.user.id,
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
nFormatter,
parseFilterValue,
} from "@dub/utils";
import { useParams } from "next/navigation";
import { useCallback, useMemo } from "react";
import { ApplicationReferralSourceIcon } from "./application-referral-source-icon";
import { useApplicationsAnalytics } from "./use-applications-analytics";
Expand All @@ -20,6 +21,7 @@ const FILTER_KEYS = ["partnerId", "country", "referralSource"] as const;

export function useApplicationAnalyticsFilters() {
const { slug } = useWorkspace();
const { tab } = useParams() as { tab?: string };
const { stage } = useApplicationsAnalyticsQuery();
const { searchParamsObj, queryParams } = useRouterStuff();

Expand All @@ -35,16 +37,19 @@ export function useApplicationAnalyticsFilters() {
const { data: partners } = useApplicationsAnalytics({
groupBy: "partnerId",
exclude: ["partnerId"],
enabled: tab === "applications",
});

const { data: referralSources } = useApplicationsAnalytics({
groupBy: "referralSource",
exclude: ["referralSource"],
enabled: tab === "applications",
});

const { data: countries } = useApplicationsAnalytics({
groupBy: "country",
exclude: ["country"],
enabled: tab === "applications",
});

const filters = useMemo(
Expand Down
Loading
Loading