Skip to content
Closed
Show file tree
Hide file tree
Changes from 66 commits
Commits
Show all changes
82 commits
Select commit Hold shift + click to select a range
5cac626
feat: cal.ai self serve
Udit-takkar Jun 15, 2025
e38f713
chore: progress
Udit-takkar Jun 16, 2025
f52c497
Merge branch 'main' into feat/cal-ai-self-serve
Udit-takkar Jun 24, 2025
b6be1fd
fix: form
Udit-takkar Jun 24, 2025
e7a61c4
fix: links[endpoint] error
Udit-takkar Jun 25, 2025
d2ad1cf
chore: form
Udit-takkar Jun 26, 2025
ca041da
Merge branch 'main' into feat/cal-ai-self-serve
Udit-takkar Jun 26, 2025
049c1bb
Merge branch 'main' into feat/cal-ai-self-serve
Udit-takkar Jul 2, 2025
3b9cf80
feat: finish setup
Udit-takkar Jul 3, 2025
def1186
chore: fix
Udit-takkar Jul 3, 2025
8a5967b
fix: trpc hang bug
Udit-takkar Jul 4, 2025
b1c0524
Merge branch 'main' into feat/cal-ai-self-serve
Udit-takkar Jul 4, 2025
436301c
chore: save progress
Udit-takkar Jul 4, 2025
e9c3c05
Merge branch 'main' into feat/cal-ai-self-serve
Udit-takkar Jul 8, 2025
254a2e0
feat: add phone number billing
Udit-takkar Jul 9, 2025
807d515
feat: retell ai webhook
Udit-takkar Jul 9, 2025
6771fa7
Merge branch 'main' into feat/cal-ai-self-serve
Udit-takkar Jul 9, 2025
39cd886
fix: type error
Udit-takkar Jul 9, 2025
a27a800
feat: remove api key input
Udit-takkar Jul 9, 2025
9676ef3
feat: add delete
Udit-takkar Jul 10, 2025
fb852c2
feat: add delete logic and assign phone number
Udit-takkar Jul 10, 2025
6d06d74
refactor: use design pattern
Udit-takkar Jul 10, 2025
7106fca
chore: remove comment
Udit-takkar Jul 10, 2025
bf7b9c9
Merge branch 'main' into feat/cal-ai-self-serve
Udit-takkar Jul 11, 2025
610bd75
fix: type errror
Udit-takkar Jul 11, 2025
95f82aa
refactor: decouple retell ai
Udit-takkar Jul 11, 2025
b79c4b5
fix: name
Udit-takkar Jul 14, 2025
130e38c
chore: comment unued
Udit-takkar Jul 14, 2025
7ff4667
fix: type errors
Udit-takkar Jul 14, 2025
d29eeb9
fix: type error
Udit-takkar Jul 14, 2025
e6700d2
feat: import phone number functionality
Udit-takkar Jul 15, 2025
47d4ad7
Merge branch 'main' into feat/cal-ai-self-serve
Udit-takkar Jul 16, 2025
cdaeae4
feat: wwebhook handler
Udit-takkar Jul 17, 2025
8791002
fix: type errors
Udit-takkar Jul 17, 2025
d9baa63
fix: type error
Udit-takkar Jul 17, 2025
443fa77
fix: 404 new trpc endpoint bug
Udit-takkar Jul 17, 2025
82cca61
fix: impor
Udit-takkar Jul 18, 2025
b1b9aac
chore
Udit-takkar Jul 18, 2025
9be39b8
feat: new version (wip)
Udit-takkar Jul 28, 2025
0754d7b
chore: improvements
Udit-takkar Jul 29, 2025
2701675
tests: add unit tests
Udit-takkar Jul 29, 2025
470f034
fix: types
Udit-takkar Jul 30, 2025
20bcd41
feat: new desing
Udit-takkar Jul 31, 2025
0b9f2b8
refactor: move everything to service
Udit-takkar Jul 31, 2025
351c4db
tests: add unit tests for service and client
Udit-takkar Jul 31, 2025
0ad600a
fix: deleting cal ai action and workflow
Udit-takkar Jul 31, 2025
b0d50a7
chore: update
Udit-takkar Jul 31, 2025
e7a804d
refactor: improve code
Udit-takkar Aug 1, 2025
9b9ef44
chore: use new app route
Udit-takkar Aug 1, 2025
326621a
fix: infinite rendering bug
Udit-takkar Aug 1, 2025
feb6236
feat: add team support completely
Udit-takkar Aug 1, 2025
1df875e
chore: remove old design and unused
Udit-takkar Aug 4, 2025
3146566
perf: improve agent repository query
Udit-takkar Aug 4, 2025
f50049e
perf: improve query
Udit-takkar Aug 4, 2025
faebeb1
chore: improvements and rate limiting
Udit-takkar Aug 4, 2025
4c62e65
fix: credit reposiotory
Udit-takkar Aug 4, 2025
8d8ff2d
fix: update unit tests and type error
Udit-takkar Aug 4, 2025
62bbd3f
fix: type
Udit-takkar Aug 4, 2025
f9886a2
chore: types
Udit-takkar Aug 4, 2025
5253927
fix: use types from retell sdk
Udit-takkar Aug 4, 2025
9301519
chore: remove PhoneData
Udit-takkar Aug 4, 2025
81f0145
chore: code improvements
Udit-takkar Aug 4, 2025
e97d2ed
chore: code improvements
Udit-takkar Aug 5, 2025
c1791b8
fix: tests and types
Udit-takkar Aug 5, 2025
b34588e
chore: improve UI
Udit-takkar Aug 5, 2025
a34a25e
chore: type error
Udit-takkar Aug 5, 2025
fe7ec30
fix: improvements and i18n
Udit-takkar Aug 5, 2025
69f1ec1
chore: remove unused
Udit-takkar Aug 5, 2025
c153c8b
chore: more i18n
Udit-takkar Aug 5, 2025
b9c2de2
chore: i18n
Udit-takkar Aug 5, 2025
fcfb4f6
fix: type err
Udit-takkar Aug 5, 2025
13ba93a
chore: missing check
Udit-takkar Aug 5, 2025
990c9c8
chore: update docs
Udit-takkar Aug 5, 2025
85878ec
nit
Udit-takkar Aug 5, 2025
8d0d762
chore: i18n, other improvements
Udit-takkar Aug 5, 2025
265e536
fix: formatting
Udit-takkar Aug 5, 2025
70c18b8
fix: type
Udit-takkar Aug 5, 2025
953001f
refactor: stripe related webhooks
Udit-takkar Aug 6, 2025
49bd22a
Merge branch 'main' into feat/cal-ai-self-serve
emrysal Aug 6, 2025
2cdbfa7
chore: imporovements
Udit-takkar Aug 7, 2025
2f52003
fix: types
Udit-takkar Aug 7, 2025
15ddc3d
chore
Udit-takkar Aug 7, 2025
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
223 changes: 223 additions & 0 deletions apps/web/app/api/phone-numbers/subscription/success/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,223 @@
import { defaultResponderForAppDir } from "app/api/defaultResponderForAppDir";
import type { NextRequest } from "next/server";
import { NextResponse } from "next/server";
import type Stripe from "stripe";
import { z } from "zod";

import { createDefaultAIPhoneServiceProvider } from "@calcom/features/ee/cal-ai-phone";
import stripe from "@calcom/features/ee/payments/server/stripe";
import { WEBAPP_URL } from "@calcom/lib/constants";
import { HttpError } from "@calcom/lib/http-error";
import { prisma } from "@calcom/prisma";
import { PhoneNumberSubscriptionStatus } from "@calcom/prisma/enums";

const querySchema = z.object({
session_id: z.string().min(1),
});

const checkoutSessionMetadataSchema = z.object({
userId: z.string().transform(Number),
teamId: z
.string()
.optional()
.transform((val) => (val ? Number(val) : undefined)),
eventTypeId: z
.string()
.optional()
.transform((val) => (val ? Number(val) : undefined)),
agentId: z.string().optional(),
workflowId: z.string().optional(),
type: z.literal("phone_number_subscription"),
});

type CheckoutSessionMetadata = z.infer<typeof checkoutSessionMetadataSchema>;

async function handler(request: NextRequest) {
try {
const { session_id } = querySchema.parse(Object.fromEntries(request.nextUrl.searchParams));

const checkoutSession = await getCheckoutSession(session_id);
validateCheckoutSession(checkoutSession);
const checkoutSessionMetadata = getCheckoutSessionMetadata(checkoutSession);
// Check if phone number already exists (in case webhook already processed it)
const subscriptionId =
typeof checkoutSession.subscription === "string"
? checkoutSession.subscription
: checkoutSession.subscription?.id;

if (!subscriptionId) {
throw new HttpError({ statusCode: 400, message: "Invalid subscription data" });
}

const existingPhoneNumber = await prisma.calAiPhoneNumber.findFirst({
where: {
stripeSubscriptionId: subscriptionId,
},
});

let phoneNumber = existingPhoneNumber;

if (!phoneNumber) {
// Create the phone number if it doesn't exist yet
phoneNumber = await createPhoneNumber(checkoutSession, checkoutSessionMetadata);
}

// If agentId is provided, link it to the agent
if (checkoutSessionMetadata.agentId && phoneNumber) {
try {
// Verify the agent exists and user has permission
const agent = await prisma.agent.findFirst({
where: {
id: checkoutSessionMetadata.agentId,
OR: [
{ userId: checkoutSessionMetadata.userId },
{
team: {
members: {
some: {
userId: checkoutSessionMetadata.userId,
accepted: true,
},
},
},
},
],
// If teamId is provided, ensure agent belongs to the same team
...(checkoutSessionMetadata.teamId ? { teamId: checkoutSessionMetadata.teamId } : {}),
},
});

if (agent) {
const aiService = createDefaultAIPhoneServiceProvider();

// Assign agent to the new number via Retell API
await aiService.updatePhoneNumber(phoneNumber.phoneNumber, {
outbound_agent_id: agent.retellAgentId,
});

// Link the new number to the agent in our database
await prisma.calAiPhoneNumber.update({
where: { id: phoneNumber.id },
data: {
outboundAgent: {
connect: { id: checkoutSessionMetadata.agentId },
},
},
});
}
} catch (error) {
console.error("Failed to link phone number to agent:", error);
// Don't fail the success page if agent linking fails
}
}

// Redirect based on context
let successUrl: URL;
if (checkoutSessionMetadata.agentId) {
// If this was part of a workflow setup, redirect back to workflow
if (checkoutSessionMetadata.workflowId) {
successUrl = new URL(`${WEBAPP_URL}/workflows/${checkoutSessionMetadata.workflowId}`);
} else {
successUrl = new URL(`${WEBAPP_URL}/workflows`);
}
} else {
// Otherwise redirect to phone numbers page
successUrl = new URL(`${WEBAPP_URL}/settings/my-account/phone-numbers`);
}

successUrl.searchParams.set("success", "true");
successUrl.searchParams.set("phone_number", phoneNumber?.phoneNumber || "");

return NextResponse.redirect(successUrl.toString());
} catch (error) {
console.error("Error handling phone number subscription success:", error);

// Redirect to phone numbers page with error
const errorUrl = new URL(`${WEBAPP_URL}/settings/my-account/phone-numbers`);
errorUrl.searchParams.set("error", "true");

if (error instanceof HttpError) {
errorUrl.searchParams.set("message", error.message);
return NextResponse.redirect(errorUrl.toString());
}

errorUrl.searchParams.set("message", "An error occurred while processing your subscription");
return NextResponse.redirect(errorUrl.toString());
}
}

async function getCheckoutSession(sessionId: string) {
const checkoutSession = await stripe.checkout.sessions.retrieve(sessionId, {
expand: ["subscription"],
});

if (!checkoutSession) {
throw new HttpError({ statusCode: 404, message: "Checkout session not found" });
}

return checkoutSession;
}

function validateCheckoutSession(checkoutSession: Stripe.Response<Stripe.Checkout.Session>) {
if (checkoutSession.payment_status !== "paid") {
throw new HttpError({ statusCode: 402, message: "Payment required" });
}

if (!checkoutSession.subscription) {
throw new HttpError({ statusCode: 400, message: "No subscription found in checkout session" });
}
}

function getCheckoutSessionMetadata(
checkoutSession: Stripe.Response<Stripe.Checkout.Session>
): CheckoutSessionMetadata {
const parseCheckoutSessionMetadata = checkoutSessionMetadataSchema.safeParse(checkoutSession.metadata);

if (!parseCheckoutSessionMetadata.success) {
throw new HttpError({
statusCode: 400,
message: `Incorrect metadata in checkout session. Error: ${parseCheckoutSessionMetadata.error}`,
});
}

return parseCheckoutSessionMetadata.data;
}

async function createPhoneNumber(
checkoutSession: Stripe.Response<Stripe.Checkout.Session>,
metadata: CheckoutSessionMetadata
) {
const aiService = createDefaultAIPhoneServiceProvider();

// Create the phone number through Retell API
const retellPhoneNumber = await aiService.createPhoneNumber({
nickname: `${metadata.userId}-${Date.now()}`,
});

// Extract subscription ID correctly
const subscriptionId =
typeof checkoutSession.subscription === "string"
? checkoutSession.subscription
: checkoutSession.subscription?.id;

if (!subscriptionId) {
throw new HttpError({ statusCode: 400, message: "Invalid subscription data" });
}

// Create the phone number in our database with subscription details
const newNumber = await prisma.calAiPhoneNumber.create({
data: {
userId: metadata.userId,
teamId: metadata.teamId,
phoneNumber: retellPhoneNumber.phone_number,
provider: "retell",
stripeCustomerId: checkoutSession.customer as string,
stripeSubscriptionId: subscriptionId,
subscriptionStatus: PhoneNumberSubscriptionStatus.ACTIVE,
},
});

return newNumber;
}

export const GET = defaultResponderForAppDir(handler);
Loading
Loading