Skip to content
Open
Show file tree
Hide file tree
Changes from 3 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
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -50,4 +50,4 @@ packages/stripe-app/.build/*
playwright-report/
**/playwright/.auth/
test-results/
blob-report/
blob-report/
Original file line number Diff line number Diff line change
@@ -1,119 +1,18 @@
import { getSharedPartnerPlatforms } from "@/lib/api/partners/get-shared-partner-platforms";
import { withAdmin } from "@/lib/auth";
import { prisma } from "@/lib/prisma";
import { partnerSharedPlatformSchema } from "@/lib/zod/schemas/partners";
import { getDomainWithoutWWW } from "@dub/utils";
import { Prisma } from "@prisma/client";
import { NextResponse } from "next/server";
import * as z from "zod/v4";

// GET /api/admin/partners/:partnerId/shared-platforms – other partners in the network with the same verified platform identifiers
export const GET = withAdmin(async ({ params }) => {
const { partnerId } = params;

const partner = await prisma.partner.findUnique({
where: { id: partnerId },
include: {
platforms: true,
},
});
const sharedPlatforms = await getSharedPartnerPlatforms({ partnerId });

if (!partner) {
if (sharedPlatforms === null) {
return new Response("Partner not found.", { status: 404 });
}
const verifiedPlatforms = partner.platforms.filter(
(platform) => platform.verifiedAt !== null,
);

// a partner has at most one platform per type, so there is at most one website
const websitePlatform = verifiedPlatforms.find(
(platform) => platform.type === "website",
);

const websiteDomain = websitePlatform
? getDomainWithoutWWW(websitePlatform.identifier) ?? null
: null;

const matchConditions: Prisma.PartnerPlatformWhereInput[] = [];

for (const platform of verifiedPlatforms) {
if (platform.type === "website") {
// website identifiers are full URLs with inconsistent normalization,
// so match on the domain instead of the exact identifier
if (websiteDomain) {
matchConditions.push({
identifier: {
contains: websiteDomain,
},
type: platform.type,
});
}
continue;
}

matchConditions.push({
identifier: platform.identifier,
type: platform.type,
});
}

if (matchConditions.length === 0) {
return NextResponse.json([]);
}

const sharedPlatformMatches = await prisma.partnerPlatform.findMany({
where: {
OR: matchConditions,
partnerId: {
not: partner.id,
},
verifiedAt: {
not: null,
},
},
include: {
partner: {
select: {
id: true,
name: true,
email: true,
image: true,
},
},
},
orderBy: {
createdAt: "asc",
},
take: 10,
});

const sharedPlatforms = verifiedPlatforms
.map((platform) => {
let platformMatches = sharedPlatformMatches.filter(
(match) => match.type === platform.type,
);

// "contains" matches on the domain need exact verification
// (e.g. contains "example.com" would also match "notexample.com")
if (platform.type === "website") {
platformMatches = platformMatches.filter(
(match) =>
websiteDomain !== null &&
getDomainWithoutWWW(match.identifier) === websiteDomain,
);
}

return {
type: platform.type,
identifier: platform.identifier,
partners: platformMatches.map((match) => ({
id: match.partner.id,
name: match.partner.name,
email: match.partner.email,
image: match.partner.image,
})),
};
})
.filter((platform) => platform.partners.length > 0);

return NextResponse.json(
z.array(partnerSharedPlatformSchema).parse(sharedPlatforms),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { DubApiError } from "@/lib/api/errors";
import { getSharedPartnerPlatforms } from "@/lib/api/partners/get-shared-partner-platforms";
import { getDefaultProgramIdOrThrow } from "@/lib/api/programs/get-default-program-id-or-throw";
import { getProgramEnrollmentOrThrow } from "@/lib/api/programs/get-program-enrollment-or-throw";
import { withWorkspace } from "@/lib/auth";
import { partnerProgramSharedPlatformSchema } from "@/lib/zod/schemas/partners";
import { NextResponse } from "next/server";
import * as z from "zod/v4";

// GET /api/partners/:partnerId/shared-platforms – other partners in this program with the same verified platform identifiers
export const GET = withWorkspace(
async ({ workspace, params }) => {
const { partnerId } = params;
const programId = getDefaultProgramIdOrThrow(workspace);

await getProgramEnrollmentOrThrow({
partnerId,
programId,
include: {},
});

const sharedPlatforms = await getSharedPartnerPlatforms({
partnerId,
programId,
});

if (sharedPlatforms === null) {
throw new DubApiError({
code: "not_found",
message: "Partner not found.",
});
}

return NextResponse.json(
z.array(partnerProgramSharedPlatformSchema).parse(sharedPlatforms),
);
},
{
requiredPlan: ["business", "advanced", "enterprise"],
},
);
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ export function ProgramPartnersRejectedApplicationsPageClient() {
status: "rejected",
sortBy,
sortOrder,
includePartnerPlatformPropss: true,
includePartnerPlatforms: true,
},
{ exclude: ["partnerId"] },
)}`,
Expand Down
170 changes: 170 additions & 0 deletions apps/web/lib/api/partners/get-shared-partner-platforms.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
import { prisma } from "@/lib/prisma";
import { getDomainWithoutWWW } from "@dub/utils";
import {
Partner,
PartnerPlatform,
Prisma,
ProgramEnrollmentStatus,
} from "@prisma/client";

type SharedPlatformMatch = PartnerPlatform & {
partner: Pick<Partner, "id" | "name" | "email" | "image"> & {
// only selected when programId is provided
programs?: { status: ProgramEnrollmentStatus }[];
};
};

// Finds other partners with the same verified platform identifiers.
// When programId is provided, only matches partners enrolled in that program
// and includes each matched partner's enrollment status.
export async function getSharedPartnerPlatforms({
partnerId,
programId,
}: {
partnerId: string;
programId?: string;
}) {
const partner = await prisma.partner.findUnique({
where: { id: partnerId },
include: {
platforms: true,
},
});

if (!partner) {
return null;
}

const verifiedPlatforms = partner.platforms.filter(
(platform) => platform.verifiedAt !== null,
);

// a partner has at most one platform per type, so there is at most one website
const websitePlatform = verifiedPlatforms.find(
(platform) => platform.type === "website",
);

const websiteDomain = websitePlatform
? getDomainWithoutWWW(websitePlatform.identifier) ?? null
: null;

const matchConditions: Prisma.PartnerPlatformWhereInput[] = [];

for (const platform of verifiedPlatforms) {
if (platform.type === "website") {
// website identifiers are full URLs with inconsistent normalization,
// so match on the domain instead of the exact identifier
if (websiteDomain) {
matchConditions.push({
identifier: {
contains: websiteDomain,
},
type: platform.type,
});
}
continue;
}

matchConditions.push({
identifier: platform.identifier,
type: platform.type,
});
}

if (matchConditions.length === 0) {
return [];
}

const sharedPlatformMatches = (await prisma.partnerPlatform.findMany({
where: {
OR: matchConditions,
partnerId: {
not: partner.id,
},
verifiedAt: {
not: null,
},
...(programId && {
partner: {
programs: {
some: {
programId,
},
},
},
}),
},
include: {
partner: {
select: {
id: true,
name: true,
email: true,
image: true,
...(programId && {
programs: {
where: {
programId,
},
select: {
status: true,
},
},
}),
},
},
},
orderBy: {
createdAt: "asc",
},
take: 100,
})) as SharedPlatformMatch[];
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated

const sharedPlatforms = verifiedPlatforms
.map((platform) => {
let platformMatches = sharedPlatformMatches.filter(
(match) => match.type === platform.type,
);

// "contains" matches on the domain need exact verification
// (e.g. contains "example.com" would also match "notexample.com")
if (platform.type === "website") {
platformMatches = platformMatches.filter(
(match) =>
websiteDomain !== null &&
getDomainWithoutWWW(match.identifier) === websiteDomain,
);
}

const partners = platformMatches
.flatMap((match) => {
const status = programId
? match.partner.programs?.[0]?.status
: undefined;

if (programId && !status) {
return [];
}

return [
{
id: match.partner.id,
name: match.partner.name,
email: match.partner.email,
image: match.partner.image,
...(status && { status }),
},
];
})
.slice(0, 10);

return {
type: platform.type,
identifier: platform.identifier,
partners,
};
})
.filter((platform) => platform.partners.length > 0);

return sharedPlatforms;
}
5 changes: 5 additions & 0 deletions apps/web/lib/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,7 @@ import {
EnrolledPartnerSchema,
EnrolledPartnerSchemaExtended,
partnerPlatformSchema,
partnerProgramSharedPlatformSchema,
PartnerRewindSchema,
PartnerSchema,
partnerSharedPlatformSchema,
Expand Down Expand Up @@ -516,6 +517,10 @@ export type PartnerSharedPlatformProps = z.infer<
typeof partnerSharedPlatformSchema
>;

export type PartnerProgramSharedPlatformProps = z.infer<
typeof partnerProgramSharedPlatformSchema
>;

export type PartnerProps = z.infer<typeof PartnerSchema> & {
role: PartnerRole;
userId: string;
Expand Down
11 changes: 11 additions & 0 deletions apps/web/lib/zod/schemas/partners.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1073,3 +1073,14 @@ export const partnerSharedPlatformSchema = z.object({
}),
),
});

// program-scoped variant: matched partners are enrolled in the program,
// so each one carries its enrollment status
export const partnerProgramSharedPlatformSchema =
partnerSharedPlatformSchema.extend({
partners: z.array(
partnerSharedPlatformSchema.shape.partners.element.extend({
status: z.enum(ProgramEnrollmentStatus),
}),
),
});
Loading
Loading