Skip to content
Draft
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
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
import { getPartners } from "@/lib/api/partners/get-partners";
import { partnersExportQuerySchema } from "@/lib/zod/schemas/partners";
import * as z from "zod/v4";

type PartnerFilters = Omit<
z.infer<typeof partnersExportQuerySchema>,
"columns"
Parameters<typeof getPartners>[0],
"page" | "pageSize"
> & {
programId: string;
};
Expand Down
29 changes: 25 additions & 4 deletions apps/web/app/(ee)/api/cron/export/partners/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,16 @@ import * as z from "zod/v4";
import { logAndRespond } from "../../utils";
import { fetchPartnersBatch } from "./fetch-partners-batch";

const sqlOperatorSchema = z.enum(["IN", "NOT IN"]);

const payloadSchema = partnersExportQuerySchema.extend({
programId: z.string(),
userId: z.string(),
partnerTagIdOperator: sqlOperatorSchema.optional(),
groupIdOperator: sqlOperatorSchema.optional(),
countryOperator: sqlOperatorSchema.optional(),
statusOperator: sqlOperatorSchema.optional(),
referredByPartnerIdOperator: sqlOperatorSchema.optional(),
});

// POST /api/cron/export/partners - QStash worker for processing large partner exports
Expand All @@ -29,9 +36,19 @@ export async function POST(req: Request) {
rawBody,
});

let { programId, columns, userId, ...filters } = payloadSchema.parse(
JSON.parse(rawBody),
);
const payload = payloadSchema.parse(JSON.parse(rawBody));

const {
programId,
columns,
userId,
partnerTagIdOperator,
groupIdOperator,
countryOperator,
statusOperator,
referredByPartnerIdOperator,
...filters
} = payload;

const user = await prisma.user.findUnique({
where: {
Expand Down Expand Up @@ -65,10 +82,14 @@ export async function POST(req: Request) {
);
}

// Fetch partners in batches and build CSV
const allPartners: any[] = [];
const partnersFilters = {
...filters,
partnerTagIdOperator,
groupIdOperator,
countryOperator,
statusOperator,
referredByPartnerIdOperator,
programId,
};

Expand Down
31 changes: 5 additions & 26 deletions apps/web/app/(ee)/api/partners/count/route.ts
Original file line number Diff line number Diff line change
@@ -1,42 +1,21 @@
import { getPartnersCount } from "@/lib/api/partners/get-partners-count";
import { parsePartnerListQuery } from "@/lib/api/partners/parse-partner-filter-params";
import { getDefaultProgramIdOrThrow } from "@/lib/api/programs/get-default-program-id-or-throw";
import { withWorkspace } from "@/lib/auth";
import { partnersCountQuerySchema } from "@/lib/zod/schemas/partners";
import { parseFilterValue } from "@dub/utils";
import { NextResponse } from "next/server";

function parsePartnerFilterParams(
searchParams: Record<string, string | undefined>,
) {
const partnerTagIdParsed = parseFilterValue(searchParams.partnerTagId);
const groupIdParsed = parseFilterValue(searchParams.groupId);
const countryParsed = parseFilterValue(searchParams.country);

return {
partnerTagId: partnerTagIdParsed?.values,
partnerTagIdOperator: partnerTagIdParsed?.sqlOperator,
groupId: groupIdParsed?.values,
groupIdOperator: groupIdParsed?.sqlOperator,
country: countryParsed?.values,
countryOperator: countryParsed?.sqlOperator,
};
}

// GET /api/partners/count - get the count of partners for a program
export const GET = withWorkspace(
async ({ workspace, searchParams }) => {
const programId = getDefaultProgramIdOrThrow(workspace);
const filterOverrides = parsePartnerFilterParams(searchParams);
const parsedParams = partnersCountQuerySchema.parse(searchParams);
const parsedParams = parsePartnerListQuery(
searchParams,
partnersCountQuerySchema,
);

const count = await getPartnersCount({
...parsedParams,
partnerTagId: filterOverrides.partnerTagId ?? parsedParams.partnerTagId,
partnerTagIdOperator: filterOverrides.partnerTagIdOperator,
groupId: filterOverrides.groupId ?? parsedParams.groupId,
groupIdOperator: filterOverrides.groupIdOperator,
country: filterOverrides.country ?? parsedParams.country,
countryOperator: filterOverrides.countryOperator,
programId: programId as string,
});

Expand Down
11 changes: 7 additions & 4 deletions apps/web/app/(ee)/api/partners/export/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { convertToCSV } from "@/lib/analytics/utils/convert-to-csv";
import { formatPartnersForExport } from "@/lib/api/partners/format-partners-for-export";
import { getPartners } from "@/lib/api/partners/get-partners";
import { getPartnersCount } from "@/lib/api/partners/get-partners-count";
import { parsePartnerListQuery } from "@/lib/api/partners/parse-partner-filter-params";
import { getDefaultProgramIdOrThrow } from "@/lib/api/programs/get-default-program-id-or-throw";
import { withWorkspace } from "@/lib/auth";
import { qstash } from "@/lib/cron";
Expand All @@ -16,21 +17,23 @@ export const GET = withWorkspace(
async ({ searchParams, workspace, session }) => {
const programId = getDefaultProgramIdOrThrow(workspace);

const parsedParams = partnersExportQuerySchema.parse(searchParams);
const { columns, ...filters } = parsedParams;
const params = parsePartnerListQuery(
searchParams,
partnersExportQuerySchema,
);
const { columns, ...filters } = params;

const partnersCount = await getPartnersCount<number>({
...filters,
groupBy: undefined,
programId,
});

// Process the export in the background if the number of partners is greater than MAX_PARTNERS_TO_EXPORT
if (partnersCount > MAX_PARTNERS_TO_EXPORT) {
await qstash.publishJSON({
url: `${APP_DOMAIN_WITH_NGROK}/api/cron/export/partners`,
body: {
...parsedParams,
...params,
columns: columns.join(","),
programId,
userId: session.user.id,
Expand Down
48 changes: 7 additions & 41 deletions apps/web/app/(ee)/api/partners/route.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { createAndEnrollPartner } from "@/lib/api/partners/create-and-enroll-partner";
import { getPartners } from "@/lib/api/partners/get-partners";
import { parsePartnerListQuery } from "@/lib/api/partners/parse-partner-filter-params";
import { getDefaultProgramIdOrThrow } from "@/lib/api/programs/get-default-program-id-or-throw";
import { getProgramOrThrow } from "@/lib/api/programs/get-program-or-throw";
import { parseRequestBody } from "@/lib/api/utils";
Expand All @@ -12,50 +13,21 @@ import {
getPartnersQuerySchemaExtended,
partnerPlatformSchema,
} from "@/lib/zod/schemas/partners";
import { parseFilterValue, toCentsNumber } from "@dub/utils";
import { toCentsNumber } from "@dub/utils";
import { NextResponse } from "next/server";
import * as z from "zod/v4";

function parsePartnerFilterParams(
searchParams: Record<string, string | undefined>,
) {
const partnerTagIdParsed = parseFilterValue(searchParams.partnerTagId);
const groupIdParsed = parseFilterValue(searchParams.groupId);
const countryParsed = parseFilterValue(searchParams.country);

return {
partnerTagId: partnerTagIdParsed?.values,
partnerTagIdOperator: partnerTagIdParsed?.sqlOperator,
groupId: groupIdParsed?.values,
groupIdOperator: groupIdParsed?.sqlOperator,
country: countryParsed?.values,
countryOperator: countryParsed?.sqlOperator,
};
}

// GET /api/partners - get all partners for a program
export const GET = withWorkspace(
async ({ workspace, searchParams }) => {
const programId = getDefaultProgramIdOrThrow(workspace);
const filterOverrides = parsePartnerFilterParams(searchParams);
const paramsToParse = {
...searchParams,
...(filterOverrides.partnerTagId && {
partnerTagId: filterOverrides.partnerTagId,
}),
...(filterOverrides.groupId !== undefined && {
groupId: filterOverrides.groupId,
}),
...(filterOverrides.country !== undefined && {
country: filterOverrides.country,
}),
};
const {
sortBy: sortByWithOldFields,
includePartnerPlatforms,
...parsedParams
} = getPartnersQuerySchemaExtended
.extend({
} = parsePartnerListQuery(
searchParams,
getPartnersQuerySchemaExtended.extend({
// add old fields for backward compatibility
sortBy: getPartnersQuerySchemaExtended.shape.sortBy.or(
z.enum([
Expand All @@ -67,8 +39,8 @@ export const GET = withWorkspace(
"totalSales",
]),
),
})
.parse(paramsToParse);
}),
);

// get the final sortBy field (replace old fields with new fields)
const sortBy =
Expand All @@ -84,12 +56,6 @@ export const GET = withWorkspace(
console.time("getPartners");
const partners = await getPartners({
...parsedParams,
partnerTagId: filterOverrides.partnerTagId ?? parsedParams.partnerTagId,
partnerTagIdOperator: filterOverrides.partnerTagIdOperator,
groupId: filterOverrides.groupId ?? parsedParams.groupId,
groupIdOperator: filterOverrides.groupIdOperator,
country: filterOverrides.country ?? parsedParams.country,
countryOperator: filterOverrides.countryOperator,
sortBy,
programId,
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -78,11 +78,20 @@ export function ProgramPartnersApplicationsPageClient() {
(key) => !["sortBy", "sortOrder", "page"].includes(key),
);

const { filters, activeFilters, onSelect, onRemove, onRemoveAll } =
usePartnerFilters({ sortBy, sortOrder, status: "pending" }, [
"groupId",
"country",
]);
const {
filters,
activeFilters,
onSelect,
onRemove,
onRemoveFilter,
onRemoveAll,
onToggleOperator,
onOpenFilter,
setSearch,
} = usePartnerFilters({ sortBy, sortOrder, status: "pending" }, [
"groupId",
"country",
]);

const { partnersCount, error: countError } = usePartnersCount<number>({
status: "pending",
Expand Down Expand Up @@ -463,6 +472,11 @@ export function ProgramPartnersApplicationsPageClient() {
activeFilters={activeFilters}
onSelect={onSelect}
onRemove={onRemove}
onRemoveFilter={onRemoveFilter}
isAdvancedFilter
onOpenFilter={onOpenFilter}
onSearchChange={setSearch}
onSelectedFilterChange={onOpenFilter}
/>
<div className="flex w-full grow items-center gap-2 md:w-auto">
<div className="min-w-0 flex-1">
Expand Down Expand Up @@ -497,7 +511,10 @@ export function ProgramPartnersApplicationsPageClient() {
activeFilters={activeFilters}
onSelect={onSelect}
onRemove={onRemove}
onRemoveFilter={onRemoveFilter}
onRemoveAll={onRemoveAll}
onToggleOperator={onToggleOperator}
isAdvancedFilter
/>
</div>
)}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,8 +70,17 @@ export function ProgramPartnersRejectedApplicationsPageClient() {
const sortBy = searchParams.get("sortBy") || "createdAt";
const sortOrder = searchParams.get("sortOrder") === "asc" ? "asc" : "desc";

const { filters, activeFilters, onSelect, onRemove, onRemoveAll } =
usePartnerFilters({ sortBy, sortOrder, status: "rejected" }, ["country"]);
const {
filters,
activeFilters,
onSelect,
onRemove,
onRemoveFilter,
onRemoveAll,
onToggleOperator,
onOpenFilter,
setSearch,
} = usePartnerFilters({ sortBy, sortOrder, status: "rejected" }, ["country"]);

const { partnersCount, error: countError } = usePartnersCount<number>({
status: "rejected",
Expand Down Expand Up @@ -401,6 +410,11 @@ export function ProgramPartnersRejectedApplicationsPageClient() {
activeFilters={activeFilters}
onSelect={onSelect}
onRemove={onRemove}
onRemoveFilter={onRemoveFilter}
isAdvancedFilter
onOpenFilter={onOpenFilter}
onSearchChange={setSearch}
onSelectedFilterChange={onOpenFilter}
/>
<SearchBoxPersisted
placeholder="Search by name, email, or company"
Expand All @@ -416,7 +430,10 @@ export function ProgramPartnersRejectedApplicationsPageClient() {
activeFilters={activeFilters}
onSelect={onSelect}
onRemove={onRemove}
onRemoveFilter={onRemoveFilter}
onRemoveAll={onRemoveAll}
onToggleOperator={onToggleOperator}
isAdvancedFilter
/>
</div>
)}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -557,6 +557,8 @@ function PartnersFilters({
onRemoveFilter,
onRemoveAll,
onToggleOperator,
onOpenFilter,
setSearch,
} = usePartnerFilters({ sortBy, sortOrder, status });

return (
Expand All @@ -569,6 +571,10 @@ function PartnersFilters({
onSelect={onSelect}
onRemove={onRemove}
onRemoveFilter={onRemoveFilter}
isAdvancedFilter
onOpenFilter={onOpenFilter}
onSearchChange={setSearch}
onSelectedFilterChange={onOpenFilter}
/>
<SearchBoxPersisted
placeholder="Search by name, email, or company"
Expand All @@ -587,6 +593,7 @@ function PartnersFilters({
onRemoveFilter={onRemoveFilter}
onRemoveAll={onRemoveAll}
onToggleOperator={onToggleOperator}
isAdvancedFilter
/>
</div>
)}
Expand Down
Loading
Loading