* feat: Cal.ai Self Serve #2 * chore: fix import and remove logs * fix: update checkout session * fix: type errors and test * fix: imports * fix: type err * fix: type error * fix: tests * chore: save progress * fix: workflow flow * fix: workflow update bug * tests: add unit tests for retell ai webhoo * fix: status code * fix: test and delete bug * fix: add dynamic variables * fix: type err * chore: update unit test * fix: type error * chore: update default prompt * fix: type errors * fix: workflow permissions * fix: workflow page * fix: translations * feat: add call duration * chore: add booking uid * fix: button positioning * chore: update tests * chore: improvements * chore: some more improvements * refactor: improvements * refactor: code feedback * refactor: improvements * feat: enable credits for orgs (#23077) * Show credits UI for orgs * fix stripe callback url when buying credits * give orgs 20% credits * add test for calulating credits --------- Co-authored-by: CarinaWolli <wollencarina@gmail.com> Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com> * fix: types * fix: types * chore: error * fix: type error * fix: type error * chore: mock env * feat: add idempotency key to prevent double charging * chore: add userId and teamId * fix: skip inbound calls * chore: update tests * feat: add feature flag for voice agent * feat: finish test call and other improvements * chore: add alert * chore: update .env.example * chore: improvements * fix: update tests * refactor: remove un necessary * feat: add setup badge * chore: improvements * fix: use referene id * chore: improvements * fix: type error * fix: type * refactor: change pricing logic * refactor: update tests * fix: conflicts * fix: billing link for orgs * fix: types * refactor: move feature flag up * fix: alert and test call credit check * fix: update unit tests * fix: feedback * refactor: improvements * refactor: move handlers to separate files * fix: types * fix: missing import * fix: type * refactor: change general tools functions handling * refactor: use repository * refactor: improvements * fix: types * fix: type errorr * fix: auth check * feat: add creditFor * fix: update defualt prompt * fix: throw error on frontend * fix: update unit tests * fix: use deleteAllWorkflowReminders * refactor: add connect phone number * refactor: improvements * chore: translation * chore: update message * chore: translation * design improvements buy number dialog * add translation for error message * use translation key in error message * refactor: improve connect phone number tab * feat: support un saved workflow to tests * chore: remove un used * fix: remove un used * fix: remove un used * refactor: similify billing --------- Co-authored-by: Carina Wollendorfer <30310907+CarinaWolli@users.noreply.github.com> Co-authored-by: CarinaWolli <wollencarina@gmail.com> Co-authored-by: Peer Richelsen <peeroke@gmail.com> Co-authored-by: Keith Williams <keithwillcode@gmail.com>
93 lines
3.1 KiB
TypeScript
93 lines
3.1 KiB
TypeScript
import type { NextRequest } from "next/server";
|
|
import { NextResponse } from "next/server";
|
|
import type Stripe from "stripe";
|
|
import { z } from "zod";
|
|
|
|
import { CHECKOUT_SESSION_TYPES } from "@calcom/features/ee/billing/constants";
|
|
import stripe from "@calcom/features/ee/payments/server/stripe";
|
|
import { WEBAPP_URL } from "@calcom/lib/constants";
|
|
import { HttpError } from "@calcom/lib/http-error";
|
|
import logger from "@calcom/lib/logger";
|
|
import { safeStringify } from "@calcom/lib/safeStringify";
|
|
|
|
const querySchema = z.object({
|
|
session_id: z.string().min(1),
|
|
});
|
|
const log = logger.getSubLogger({ prefix: ["[calAIPhone] subscription/success"] });
|
|
|
|
const checkoutSessionMetadataSchema = z.object({
|
|
userId: z.coerce.number().int().positive(),
|
|
teamId: z.coerce.number().int().optional(),
|
|
eventTypeId: z.coerce.number().int().positive().optional(),
|
|
agentId: z.string().optional(),
|
|
workflowId: z.string().optional(),
|
|
type: z.literal(CHECKOUT_SESSION_TYPES.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);
|
|
const metadata = validateAndExtractMetadata(checkoutSession);
|
|
|
|
return redirectToSuccess(metadata);
|
|
} catch (error) {
|
|
return handleError(error);
|
|
}
|
|
}
|
|
|
|
async function getCheckoutSession(sessionId: string) {
|
|
const session = await stripe.checkout.sessions.retrieve(sessionId, { expand: ["subscription"] });
|
|
if (!session) {
|
|
throw new HttpError({ statusCode: 404, message: "Checkout session not found" });
|
|
}
|
|
return session;
|
|
}
|
|
|
|
function validateAndExtractMetadata(session: Stripe.Checkout.Session): CheckoutSessionMetadata {
|
|
if (session.payment_status !== "paid") {
|
|
throw new HttpError({ statusCode: 402, message: "Payment required" });
|
|
}
|
|
if (!session.subscription) {
|
|
throw new HttpError({ statusCode: 400, message: "No subscription found in checkout session" });
|
|
}
|
|
|
|
const result = checkoutSessionMetadataSchema.safeParse(session.metadata);
|
|
if (!result.success) {
|
|
log.error(`Invalid checkout session metadata: ${safeStringify(result.error.issues)}`);
|
|
throw new HttpError({
|
|
statusCode: 400,
|
|
message: "Invalid checkout session metadata",
|
|
});
|
|
}
|
|
|
|
return result.data;
|
|
}
|
|
|
|
function redirectToSuccess(metadata: CheckoutSessionMetadata) {
|
|
const basePath = metadata.workflowId
|
|
? `${WEBAPP_URL}/workflows/${metadata.workflowId}`
|
|
: `${WEBAPP_URL}/workflows`;
|
|
|
|
return NextResponse.redirect(basePath);
|
|
}
|
|
|
|
function handleError(error: unknown) {
|
|
log.error("Error handling phone number subscription success:", safeStringify(error));
|
|
|
|
const url = new URL(`${WEBAPP_URL}/workflows`);
|
|
url.searchParams.set("error", "true");
|
|
|
|
if (error instanceof HttpError) {
|
|
url.searchParams.set("message", error.message);
|
|
} else {
|
|
url.searchParams.set("message", "An error occurred while processing your subscription");
|
|
}
|
|
|
|
return NextResponse.redirect(url.toString());
|
|
}
|
|
|
|
export default handler;
|