Skip to content

Commit

Permalink
Merge branch 'main' into analytics-link-comments
Browse files Browse the repository at this point in the history
  • Loading branch information
steven-tey authored Feb 3, 2025
2 parents 45bacbf + 423580d commit 2442428
Show file tree
Hide file tree
Showing 5 changed files with 174 additions and 11 deletions.
103 changes: 103 additions & 0 deletions apps/web/app/api/partners/links/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
import { DubApiError, ErrorCodes } from "@/lib/api/errors";
import { createLink, processLink } from "@/lib/api/links";
import { getProgramOrThrow } from "@/lib/api/programs/get-program-or-throw";
import { parseRequestBody } from "@/lib/api/utils";
import { withWorkspace } from "@/lib/auth";
import { sendWorkspaceWebhook } from "@/lib/webhook/publish";
import { linkEventSchema } from "@/lib/zod/schemas/links";
import { createPartnerLinkSchema } from "@/lib/zod/schemas/partners";
import { prisma } from "@dub/prisma";
import { getApexDomain } from "@dub/utils";
import { waitUntil } from "@vercel/functions";
import { NextResponse } from "next/server";

// POST /api/partners/links - create a link for a partner
export const POST = withWorkspace(
async ({ workspace, req, session }) => {
const { programId, partnerId, tenantId, url, key, linkProps } =
createPartnerLinkSchema.parse(await parseRequestBody(req));

const program = await getProgramOrThrow({
workspaceId: workspace.id,
programId,
});

if (!program.domain || !program.url) {
throw new DubApiError({
code: "bad_request",
message:
"You need to set a domain and url for this program before creating a partner.",
});
}

if (getApexDomain(url) !== getApexDomain(program.url)) {
throw new DubApiError({
code: "bad_request",
message: `The provided URL domain (${getApexDomain(url)}) does not match the program's domain (${getApexDomain(program.url)}).`,
});
}

if (!partnerId && !tenantId) {
throw new DubApiError({
code: "bad_request",
message: "You must provide a partnerId or tenantId.",
});
}

const partner = await prisma.programEnrollment.findUnique({
where: partnerId
? { partnerId_programId: { partnerId, programId } }
: { tenantId_programId: { tenantId: tenantId!, programId } },
});

if (!partner) {
throw new DubApiError({
code: "not_found",
message: "Partner not found.",
});
}

const { link, error, code } = await processLink({
payload: {
...linkProps,
domain: program.domain,
key: key || undefined,
url,
programId,
tenantId,
partnerId,
trackConversion: true,
},
workspace,
userId: session.user.id,
});

if (error != null) {
throw new DubApiError({
code: code as ErrorCodes,
message: error,
});
}

const partnerLink = await createLink(link);

waitUntil(
sendWorkspaceWebhook({
trigger: "link.created",
workspace,
data: linkEventSchema.parse(partnerLink),
}),
);

return NextResponse.json(partnerLink, { status: 201 });
},
{
requiredPlan: [
"business",
"business extra",
"business max",
"business plus",
"enterprise",
],
},
);
10 changes: 0 additions & 10 deletions apps/web/app/api/partners/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import { enrollPartner } from "@/lib/api/partners/enroll-partner";
import { getProgramOrThrow } from "@/lib/api/programs/get-program-or-throw";
import { parseRequestBody } from "@/lib/api/utils";
import { withWorkspace } from "@/lib/auth";
import { checkIfKeyExists } from "@/lib/planetscale";
import { sendWorkspaceWebhook } from "@/lib/webhook/publish";
import { linkEventSchema } from "@/lib/zod/schemas/links";
import {
Expand Down Expand Up @@ -152,15 +151,6 @@ export const POST = withWorkspace(
});
}

const linkExists = await checkIfKeyExists(program.domain, username);

if (linkExists) {
throw new DubApiError({
code: "conflict",
message: "This username is already in use. Choose a different one.",
});
}

const { link, error, code } = await processLink({
payload: {
...linkProps,
Expand Down
32 changes: 32 additions & 0 deletions apps/web/lib/openapi/partners/create-partner-link.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { openApiErrorResponses } from "@/lib/openapi/responses";
import { LinkSchema } from "@/lib/zod/schemas/links";
import { createPartnerLinkSchema } from "@/lib/zod/schemas/partners";
import { ZodOpenApiOperationObject } from "zod-openapi";

export const createPartnerLink: ZodOpenApiOperationObject = {
operationId: "createPartnerLink",
"x-speakeasy-name-override": "createLink",
summary: "Create a link for a partner",
description:
"Create a new link for a partner that is enrolled in your program",
requestBody: {
content: {
"application/json": {
schema: createPartnerLinkSchema,
},
},
},
responses: {
"201": {
description: "The created partner",
content: {
"application/json": {
schema: LinkSchema,
},
},
},
...openApiErrorResponses,
},
tags: ["Partners"],
security: [{ token: [] }],
};
4 changes: 4 additions & 0 deletions apps/web/lib/openapi/partners/index.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
import { ZodOpenApiPathsObject } from "zod-openapi";
import { createPartner } from "./create-partner";
import { createPartnerLink } from "./create-partner-link";

export const partnersPaths: ZodOpenApiPathsObject = {
"/partners": {
post: createPartner,
},
"/partners/links": {
post: createPartnerLink,
},
};
36 changes: 35 additions & 1 deletion apps/web/lib/zod/schemas/partners.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import { CustomerSchema } from "./customers";
import { createLinkBodySchema } from "./links";
import { getPaginationQuerySchema } from "./misc";
import { ProgramEnrollmentSchema } from "./programs";
import { parseDateSchema } from "./utils";
import { parseDateSchema, parseUrlSchema } from "./utils";

export const PARTNERS_MAX_PAGE_SIZE = 100;
export const PAYOUTS_MAX_PAGE_SIZE = 100;
Expand Down Expand Up @@ -236,3 +236,37 @@ export const onboardPartnerSchema = createPartnerSchema
country: z.enum(COUNTRY_CODES),
}),
);

export const createPartnerLinkSchema = z
.object({
programId: z
.string()
.describe("The ID of the program that the partner is enrolled in."),
partnerId: z
.string()
.nullish()
.describe(
"The ID of the partner to create a link for. Will take precedence over `tenantId` if provided.",
),
tenantId: z
.string()
.nullish()
.describe(
"The ID of the partner in your system. If both `partnerId` and `tenantId` are not provided, an error will be thrown.",
),
url: parseUrlSchema.describe(
"The URL to shorten. Will throw an error if the domain doesn't match the program's default URL domain.",
),
key: z
.string()
.max(190)
.optional()
.describe(
"The short link slug. If not provided, a random 7-character slug will be generated.",
),
})
.merge(
createPartnerSchema.pick({
linkProps: true,
}),
);

0 comments on commit 2442428

Please sign in to comment.