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
2 changes: 1 addition & 1 deletion apps/web/app/api/callback/plain/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -173,7 +173,7 @@ export async function POST(req: NextRequest) {
plan === "enterprise"
? "RED"
: plan === "advanced"
? "ORANGE"
? "YELLOW"
: plan.startsWith("business")
? "GREEN"
: plan === "pro"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,12 @@ import { Customer } from "@dub/prisma/client";
import { nanoid } from "@dub/utils";
import { waitUntil } from "@vercel/functions";
import type Stripe from "stripe";
import { getConnectedCustomer } from "./utils";

// Handle event "checkout.session.completed"
export async function checkoutSessionCompleted(event: Stripe.Event) {
let charge = event.data.object as Stripe.Checkout.Session;
const dubCustomerId = charge.metadata?.dubCustomerId;
let dubCustomerId = charge.metadata?.dubCustomerId;
const clientReferenceId = charge.client_reference_id;
const stripeAccountId = event.account as string;
const stripeCustomerId = charge.customer as string;
Expand All @@ -42,45 +43,12 @@ export async function checkoutSessionCompleted(event: Stripe.Event) {
let linkId: string;

/*
for regular stripe checkout setup:
- if dubCustomerId is found, we update the customer with the stripe customerId
- we then find the lead event using the customer's unique ID on Dub
- the lead event will then be passed to the remaining logic to record a sale
*/
if (dubCustomerId) {
try {
// Update customer with stripe customerId if exists
customer = await prisma.customer.update({
where: {
projectConnectId_externalId: {
projectConnectId: stripeAccountId,
externalId: dubCustomerId,
},
},
data: {
stripeCustomerId,
},
});
} catch (error) {
// Skip if customer not found
console.log(error);
return `Customer with dubCustomerId ${dubCustomerId} not found, skipping...`;
}

// Find lead
leadEvent = await getLeadEvent({ customerId: customer.id }).then(
(res) => res.data[0],
);

linkId = leadEvent.link_id;

/*
for stripe checkout links:
- if client_reference_id is a dub_id, we find the click event
- the click event will be used to create a lead event + customer
- the lead event will then be passed to the remaining logic to record a sale
*/
} else if (clientReferenceId?.startsWith("dub_id_")) {
if (clientReferenceId?.startsWith("dub_id_")) {
const dubClickId = clientReferenceId.split("dub_id_")[1];

clickEvent = await getClickEvent({ clickId: dubClickId }).then(
Expand Down Expand Up @@ -169,7 +137,55 @@ export async function checkoutSessionCompleted(event: Stripe.Event) {
// if it's not either a regular stripe checkout setup or a stripe checkout link,
// we skip the event
} else {
return `Customer ID not found in Stripe checkout session metadata and client_reference_id is not a dub_id, skipping...`;
/*
for regular stripe checkout setup:
- if dubCustomerId not provided, we try to find the customer on the connected account
- if present:
- we update the customer with the stripe customerId
- we then find the lead event using the customer's unique ID on Dub
- the lead event will then be passed to the remaining logic to record a sale
- if not present, we skip the event
*/

if (!dubCustomerId) {
const connectedCustomer = await getConnectedCustomer({
stripeCustomerId,
stripeAccountId,
livemode: event.livemode,
});

if (!connectedCustomer || !connectedCustomer.metadata.dubCustomerId) {
return `dubCustomerId not found in Stripe checkout session metadata (nor is it available on the connected customer ${stripeCustomerId}) and client_reference_id is not a dub_id, skipping...`;
}

dubCustomerId = connectedCustomer.metadata.dubCustomerId;
}

try {
// Update customer with stripeCustomerId if exists – for future events
customer = await prisma.customer.update({
where: {
projectConnectId_externalId: {
projectConnectId: stripeAccountId,
externalId: dubCustomerId,
},
},
data: {
stripeCustomerId,
},
});
} catch (error) {
// Skip if customer not found
console.log(error);
return `Customer with dubCustomerId ${dubCustomerId} not found, skipping...`;
}

// Find lead
leadEvent = await getLeadEvent({ customerId: customer.id }).then(
(res) => res.data[0],
);

linkId = leadEvent.link_id;
}

if (charge.amount_total === 0) {
Expand Down
37 changes: 35 additions & 2 deletions apps/web/app/api/stripe/integration/webhook/invoice-paid.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,22 +10,55 @@ import { prisma } from "@dub/prisma";
import { nanoid } from "@dub/utils";
import { waitUntil } from "@vercel/functions";
import type Stripe from "stripe";
import { getConnectedCustomer } from "./utils";

// Handle event "invoice.paid"
export async function invoicePaid(event: Stripe.Event) {
const invoice = event.data.object as Stripe.Invoice;
const stripeAccountId = event.account as string;
const stripeCustomerId = invoice.customer as string;
const invoiceId = invoice.id;

// Find customer using projectConnectId and stripeCustomerId
const customer = await prisma.customer.findUnique({
let customer = await prisma.customer.findUnique({
where: {
stripeCustomerId,
},
});

// if customer is not found, we check if the connected customer has a dubCustomerId
if (!customer) {
return `Customer with stripeCustomerId ${stripeCustomerId} not found, skipping...`;
const connectedCustomer = await getConnectedCustomer({
stripeCustomerId,
stripeAccountId,
livemode: event.livemode,
});
const dubCustomerId = connectedCustomer?.metadata.dubCustomerId;

if (dubCustomerId) {
try {
// Update customer with stripeCustomerId if exists – for future events
customer = await prisma.customer.update({
where: {
projectConnectId_externalId: {
projectConnectId: stripeAccountId,
externalId: dubCustomerId,
},
},
data: {
stripeCustomerId,
},
});
} catch (error) {
console.log(error);
return `Customer with dubCustomerId ${dubCustomerId} not found, skipping...`;
}
}
}

// if customer is still not found, we skip the event
if (!customer) {
return `Customer with stripeCustomerId ${stripeCustomerId} not found on Dub (nor does the connected customer ${stripeCustomerId} have a valid dubCustomerId), skipping...`;
}

// Skip if invoice id is already processed
Expand Down
28 changes: 28 additions & 0 deletions apps/web/app/api/stripe/integration/webhook/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { createId } from "@/lib/api/create-id";
import { includeTags } from "@/lib/api/links/include-tags";
import { generateRandomName } from "@/lib/names";
import { createPartnerCommission } from "@/lib/partners/create-partner-commission";
import { stripeAppClient } from "@/lib/stripe";
import { getClickEvent, recordLead } from "@/lib/tinybird";
import { sendWorkspaceWebhook } from "@/lib/webhook/publish";
import { transformLeadEventData } from "@/lib/webhook/transform";
Expand Down Expand Up @@ -124,3 +125,30 @@ export async function createNewCustomer(event: Stripe.Event) {

return `New Dub customer created: ${customer.id}. Lead event recorded: ${leadData.event_id}`;
}

export async function getConnectedCustomer({
stripeCustomerId,
stripeAccountId,
livemode = true,
}: {
stripeCustomerId?: string | null;
stripeAccountId?: string | null;
livemode?: boolean;
}) {
// if stripeCustomerId or stripeAccountId is not provided, return null
if (!stripeCustomerId || !stripeAccountId) {
return null;
}

const connectedCustomer = await stripeAppClient({
livemode,
}).customers.retrieve(stripeCustomerId, {
stripeAccount: stripeAccountId,
});

if (connectedCustomer.deleted) {
return null;
}

return connectedCustomer;
}
12 changes: 12 additions & 0 deletions apps/web/lib/actions/send-otp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,18 @@ export const sendOtpAction = actionClient
}
}

const isExistingUser = await prisma.user.findUnique({
where: {
email,
},
});

if (isExistingUser) {
throw new Error(
"User already exists. Please login instead of requesting a new OTP.",
);
}

const code = generateOTP();

await prisma.emailVerificationToken.deleteMany({
Expand Down
17 changes: 17 additions & 0 deletions apps/web/lib/stripe/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,20 @@ export const stripe = new Stripe(`${process.env.STRIPE_SECRET_KEY}`, {
version: "0.1.0",
},
});

// Stripe Integration App client
export const stripeAppClient = ({
livemode,
}: {
livemode?: boolean;
} = {}) =>
new Stripe(
`${!livemode ? process.env.STRIPE_APP_SECRET_KEY_TEST : process.env.STRIPE_APP_SECRET_KEY}`,
{
apiVersion: "2022-11-15",
appInfo: {
name: "Dub.co",
version: "0.1.0",
},
},
);
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { prisma } from "@dub/prisma";
import "dotenv-flow/config";
import { stripe } from "../lib/stripe";
import { stripeConnectClient } from "./stripe";

async function main() {
const partners = await prisma.partner.findMany({
Expand All @@ -19,7 +19,7 @@ async function main() {
await Promise.allSettled(
partners.map(async (partner) => {
const [firstName, lastName] = partner.name.split(" ");
const res = await stripe.accounts.create({
const res = await stripeConnectClient.accounts.create({
type: "express",
business_type: "individual",
email: partner.email!,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,19 +1,6 @@
import { prisma } from "@dub/prisma";
import "dotenv-flow/config";
import Stripe from "stripe";

/*
One time script to backfill webhook events for connected customers
that don't have a stripe customer id
*/

export const stripe = new Stripe(`${process.env.STRIPE_APP_SECRET_KEY}`, {
apiVersion: "2022-11-15",
appInfo: {
name: "Dub.co",
version: "0.1.0",
},
});
import { stripeAppClient } from "../../lib/stripe";

const stripeAccountId = "xxx";

Expand All @@ -39,7 +26,9 @@ async function main() {
if (!customer.email) return;
if (customer.stripeCustomerId) return;

const stripeCustomer = await stripe.customers.list(
const stripeCustomer = await stripeAppClient({
livemode: false,
}).customers.list(
{
email: customer.email,
},
Expand Down
14 changes: 14 additions & 0 deletions apps/web/scripts/stripe/get-connected-customer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import "dotenv-flow/config";
import { stripeAppClient } from "../../lib/stripe";

async function main() {
const connectedCustomer = await stripeAppClient({
livemode: false,
}).customers.retrieve("cus_xxx", {
stripeAccount: "acct_xxx",
});

console.log(connectedCustomer);
}

main();