Skip to content

Commit

Permalink
Merge pull request #1819 from dubinc/customer-externalId
Browse files Browse the repository at this point in the history
Allow querying customers by externalId
  • Loading branch information
steven-tey authored Dec 21, 2024
2 parents e9706a0 + ce76850 commit 20f62fa
Show file tree
Hide file tree
Showing 8 changed files with 75 additions and 58 deletions.
62 changes: 25 additions & 37 deletions apps/web/app/api/customers/[id]/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,15 @@ export const GET = withWorkspace(
async ({ workspace, params }) => {
const { id } = params;

const customer = await getCustomerOrThrow({
id,
workspaceId: workspace.id,
});
const customer = await getCustomerOrThrow(
{
id,
workspaceId: workspace.id,
},
{
expand: ["link"],
},
);

return NextResponse.json(CustomerSchema.parse(customer));
},
Expand All @@ -35,32 +40,29 @@ export const PATCH = withWorkspace(
await parseRequestBody(req),
);

await getCustomerOrThrow({
const customer = await getCustomerOrThrow({
id,
workspaceId: workspace.id,
});

try {
const customer = await prisma.customer.update({
const updatedCustomer = await prisma.customer.update({
where: {
id,
id: customer.id,
},
data: { name, email, avatar, externalId },
include: {
link: true,
},
});

return NextResponse.json(CustomerSchema.parse(customer));
return NextResponse.json(CustomerSchema.parse(updatedCustomer));
} catch (error) {
if (error.code === "P2002") {
throw new DubApiError({
code: "conflict",
message: "A customer with this external ID already exists.",
});
} else if (error.code === "P2025") {
throw new DubApiError({
code: "not_found",
message:
"Customer not found. Make sure you're using the correct external ID.",
});
}

throw new DubApiError({
Expand All @@ -79,34 +81,20 @@ export const DELETE = withWorkspace(
async ({ workspace, params }) => {
const { id } = params;

await getCustomerOrThrow({
const customer = await getCustomerOrThrow({
id,
workspaceId: workspace.id,
});

try {
await prisma.customer.delete({
where: {
id,
},
});

return NextResponse.json({
id,
});
} catch (error) {
if (error.code === "P2025") {
throw new DubApiError({
code: "not_found",
message: "Customer not found",
});
}
await prisma.customer.delete({
where: {
id: customer.id,
},
});

throw new DubApiError({
code: "unprocessable_entity",
message: error.message,
});
}
return NextResponse.json({
id: customer.id,
});
},
{
requiredAddOn: "conversion",
Expand Down
3 changes: 3 additions & 0 deletions apps/web/app/api/customers/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,9 @@ export const POST = withWorkspace(
projectId: workspace.id,
projectConnectId: workspace.stripeConnectId,
},
include: {
link: true,
},
});

return NextResponse.json(CustomerSchema.parse(customer), {
Expand Down
10 changes: 3 additions & 7 deletions apps/web/app/api/stripe/integration/webhook/customer-created.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,8 @@ export async function customerCreated(event: Stripe.Event) {
},
});

if (!link) {
return `Link with ID ${linkId} not found, skipping...`;
if (!link || !link.projectId) {
return `Link with ID ${linkId} not found or does not have a project, skipping...`;
}

// Check the customer is not already created
Expand All @@ -61,15 +61,11 @@ export async function customerCreated(event: Stripe.Event) {
stripeCustomerId: stripeCustomer.id,
projectConnectId: stripeAccountId,
externalId,
projectId: link.projectId,
linkId,
clickId,
clickedAt: new Date(clickData.timestamp + "Z"),
country: clickData.country,
project: {
connect: {
stripeConnectId: stripeAccountId,
},
},
},
});

Expand Down
34 changes: 25 additions & 9 deletions apps/web/lib/api/customers/get-customer-or-throw.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,37 @@
import { prisma } from "@dub/prisma";
import { DubApiError } from "../errors";

export const getCustomerOrThrow = async ({
id,
workspaceId,
}: {
id: string;
workspaceId: string;
}) => {
export const getCustomerOrThrow = async (
{
id,
workspaceId,
}: {
id: string;
workspaceId: string;
},
{ expand }: { expand?: ("link" | "project")[] } = {},
) => {
const customer = await prisma.customer.findUnique({
where: { id },
where: {
...(id.startsWith("ext_")
? {
projectId_externalId: {
projectId: workspaceId,
externalId: id.replace("ext_", ""),
},
}
: { id }),
},
include: {
link: expand?.includes("link"),
},
});

if (!customer || customer.projectId !== workspaceId) {
throw new DubApiError({
code: "not_found",
message: "Customer not found.",
message:
"Customer not found. Make sure you're using the correct customer ID (e.g. `cus_3TagGjzRzmsFJdH8od2BNCsc`) or external ID (has to be prefixed with `ext_`).",
});
}

Expand Down
7 changes: 7 additions & 0 deletions apps/web/lib/zod/schemas/customers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,13 @@ export const CustomerSchema = z.object({
avatar: z.string().nullish().describe("Avatar URL of the customer."),
country: z.string().nullish().describe("Country of the customer."),
createdAt: z.date().describe("The date the customer was created."),
link: LinkSchema.pick({
id: true,
domain: true,
key: true,
shortLink: true,
programId: true,
}).nullish(),
});

export const CUSTOMERS_MAX_PAGE_SIZE = 100;
Expand Down
1 change: 1 addition & 0 deletions apps/web/tests/customers/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ const expectedCustomer = {
email: customerRecord.email,
avatar: customerRecord.avatar,
country: null,
link: null,
createdAt: expect.any(String),
};

Expand Down
15 changes: 10 additions & 5 deletions packages/prisma/schema/customer.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -4,22 +4,27 @@ model Customer {
email String?
avatar String? @db.LongText
externalId String?
stripeCustomerId String? @unique
linkId String?
clickId String?
clickedAt DateTime?
country String?
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
projectId String
projectConnectId String?
stripeCustomerId String? @unique
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
link Link? @relation(fields: [linkId], references: [id])
sales Sale[]
@@unique([projectId, externalId])
@@unique([projectConnectId, externalId])
@@index(projectId)
@@index(projectConnectId)
@@index(externalId)
@@index([projectId])
@@index([projectConnectId])
@@index([externalId])
@@index([linkId])
}
1 change: 1 addition & 0 deletions packages/prisma/schema/link.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ model Link {
programId String?
program Program? @relation(fields: [programId], references: [id])
customers Customer[]
@@unique([domain, key])
@@unique([projectId, externalId])
Expand Down

0 comments on commit 20f62fa

Please sign in to comment.