Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
b0215ce
fix: cal ai email
Udit-takkar Oct 8, 2025
95659ca
fix: remove
Udit-takkar Oct 9, 2025
c2d3889
fix: org
Udit-takkar Oct 9, 2025
c95a63c
replace Cal AI with Cal.ai
Oct 9, 2025
5c06429
fix: use
Udit-takkar Oct 9, 2025
f413c63
fix: feedback
Udit-takkar Oct 9, 2025
5b8255b
Merge branch 'main' into fix/cal-ai-credits
Udit-takkar Oct 9, 2025
27f30f5
fix: types
Udit-takkar Oct 9, 2025
ea19c17
fix: types
Udit-takkar Oct 9, 2025
1dabea4
fix: types
Udit-takkar Oct 9, 2025
541984c
fix: tests
Udit-takkar Oct 9, 2025
fd9dfc7
Merge branch 'main' into fix/cal-ai-credits
Udit-takkar Oct 9, 2025
966e882
Merge branch 'main' into fix/cal-ai-credits
Udit-takkar Oct 21, 2025
37f59a6
Merge branch 'main' into fix/cal-ai-credits
Udit-takkar Oct 21, 2025
11d6497
refactor: feedback
Udit-takkar Oct 21, 2025
444271d
refactor: imporvement
Udit-takkar Oct 21, 2025
1996313
Merge branch 'main' into fix/cal-ai-credits
Udit-takkar Oct 22, 2025
7ffdb49
fix: type
Udit-takkar Oct 22, 2025
9579b95
Merge branch 'main' into fix/cal-ai-credits
Udit-takkar Oct 27, 2025
3a0c8d0
refactor: feedback
Udit-takkar Oct 27, 2025
0c39cec
fix: tests
Udit-takkar Oct 27, 2025
677df2d
Merge branch 'main' into fix/cal-ai-credits
Udit-takkar Oct 31, 2025
0a43978
fix: use pbac
Udit-takkar Oct 31, 2025
61cc13f
Merge branch 'main' into fix/cal-ai-credits
Udit-takkar Nov 3, 2025
7f4947f
Merge branch 'main' into fix/cal-ai-credits
Udit-takkar Nov 4, 2025
4a11b3b
Merge branch 'main' into fix/cal-ai-credits
Udit-takkar Nov 4, 2025
fde4705
Merge branch 'main' into fix/cal-ai-credits
volnei Nov 4, 2025
da71115
chore: add missing methof
Udit-takkar Nov 5, 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
2 changes: 1 addition & 1 deletion apps/web/app/(use-page-wrapper)/workflow/new/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@ const Page = async ({ searchParams }: PageProps) => {
throw error;
}

console.error("Failed to create Cal AI workflow:", error);
console.error("Failed to create Cal.ai workflow:", error);
redirect("/workflows?error=failed-to-create-workflow");
}
};
Expand Down
22 changes: 21 additions & 1 deletion apps/web/app/api/webhooks/retell-ai/__tests__/route.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { Retell } from "retell-sdk";
import { describe, it, expect, vi, beforeEach } from "vitest";

import type { CalAiPhoneNumber, User, Team, Agent } from "@calcom/prisma/client";
import { CreditUsageType } from "@calcom/prisma/enums";

import { POST } from "../route";

Expand Down Expand Up @@ -71,6 +72,8 @@ vi.mock("retell-sdk", () => ({

const mockHasAvailableCredits = vi.fn();
const mockChargeCredits = vi.fn();
const mockSendCreditBalanceLimitReachedEmails = vi.fn();
const mockSendCreditBalanceLowWarningEmails = vi.fn();

vi.mock("@calcom/features/ee/billing/credit-service", () => ({
CreditService: vi.fn().mockImplementation(() => ({
Expand All @@ -79,6 +82,12 @@ vi.mock("@calcom/features/ee/billing/credit-service", () => ({
})),
}));

vi.mock("@calcom/emails/email-manager", () => ({
sendCreditBalanceLimitReachedEmails: (...args: unknown[]) =>
mockSendCreditBalanceLimitReachedEmails(...args),
sendCreditBalanceLowWarningEmails: (...args: unknown[]) => mockSendCreditBalanceLowWarningEmails(...args),
}));

const mockFindByPhoneNumber = vi.fn();
const mockFindByProviderAgentId = vi.fn();

Expand Down Expand Up @@ -231,6 +240,7 @@ describe("Retell AI Webhook Handler", () => {
credits: 58, // 120 seconds = 2 minutes * $0.29 = $0.58 = 58 credits
callDuration: 120,
externalRef: "retell:test-call-id",
creditFor: CreditUsageType.CAL_AI_PHONE_CALL,
})
);
});
Expand Down Expand Up @@ -288,6 +298,7 @@ describe("Retell AI Webhook Handler", () => {
credits: 87, // 180 seconds = 3 minutes * $0.29 = $0.87 = 87 credits
callDuration: 180,
externalRef: "retell:test-call-id",
creditFor: CreditUsageType.CAL_AI_PHONE_CALL,
})
);
});
Expand Down Expand Up @@ -471,6 +482,7 @@ describe("Retell AI Webhook Handler", () => {
credits: expectedCredits,
callDuration: durationSeconds,
externalRef: expect.stringMatching(/^retell:test-call-/),
creditFor: CreditUsageType.CAL_AI_PHONE_CALL,
})
);
}
Expand Down Expand Up @@ -517,7 +529,12 @@ describe("Retell AI Webhook Handler", () => {
const response = await callPOST(createMockRequest(body, "valid-signature"));
expect(response.status).toBe(200);
expect(mockChargeCredits).toHaveBeenCalledWith(
expect.objectContaining({ userId: 42, credits: 61, callDuration: 125 }) // 125s = 2.083 minutes * $0.29 = $0.604 = 61 credits (rounded up)
expect.objectContaining({
userId: 42,
credits: 61,
callDuration: 125,
creditFor: CreditUsageType.CAL_AI_PHONE_CALL,
}) // 125s = 2.083 minutes * $0.29 = $0.604 = 61 credits (rounded up)
);
});

Expand Down Expand Up @@ -574,6 +591,7 @@ describe("Retell AI Webhook Handler", () => {
credits: 29, // 60 seconds = 1 minute * $0.29 = $0.29 = 29 credits
callDuration: 60,
externalRef: "retell:test-idempotency-call",
creditFor: CreditUsageType.CAL_AI_PHONE_CALL,
})
);
});
Expand Down Expand Up @@ -760,6 +778,7 @@ describe("Retell AI Webhook Handler", () => {
credits: 4, // 7 seconds = 0.117 minutes * $0.29 = $0.034 = 4 credits (rounded up)
callDuration: 7,
externalRef: "retell:call_bcd94f5a50832873a5fd68cb1aa",
creditFor: CreditUsageType.CAL_AI_PHONE_CALL,
})
);
});
Expand Down Expand Up @@ -794,6 +813,7 @@ describe("Retell AI Webhook Handler", () => {
teamId: 10,
credits: 29, // 60 seconds = 1 minute * $0.29 = 29 credits
callDuration: 60,
creditFor: CreditUsageType.CAL_AI_PHONE_CALL,
})
);
});
Expand Down
8 changes: 5 additions & 3 deletions apps/web/app/api/webhooks/retell-ai/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,8 @@ const RetellWebhookSchema = z.object({
.passthrough(),
});

type RetellCallData = z.infer<typeof RetellWebhookSchema>["call"];

async function chargeCreditsForCall({
userId,
teamId,
Expand Down Expand Up @@ -120,7 +122,7 @@ async function chargeCreditsForCall({
}
}

async function handleCallAnalyzed(callData: any) {
async function handleCallAnalyzed(callData: RetellCallData) {
const { from_number, call_id, call_cost, call_type, agent_id } = callData;

if (
Expand Down Expand Up @@ -165,7 +167,7 @@ async function handleCallAnalyzed(callData: any) {
}

userId = agent.userId ?? undefined;
teamId = agent.teamId ?? undefined;
teamId = agent.team?.parentId ?? agent.teamId ?? undefined;

log.info(`Processing web call ${call_id} for agent ${agent_id}, user ${userId}, team ${teamId}`);
} else {
Expand All @@ -181,7 +183,7 @@ async function handleCallAnalyzed(callData: any) {
}

userId = phoneNumber.userId ?? undefined;
teamId = phoneNumber.teamId ?? undefined;
teamId = phoneNumber.team?.parentId ?? phoneNumber.teamId ?? undefined;

log.info(`Processing phone call ${call_id} from ${from_number}, user ${userId}, team ${teamId}`);
}
Expand Down
4 changes: 4 additions & 0 deletions apps/web/public/static/locales/en/common.json
Original file line number Diff line number Diff line change
Expand Up @@ -3490,6 +3490,10 @@
"low_credits_warning_message_user": "Your Cal.com account is running low on credits. To avoid any disruption in service, please purchase additional credits. If your balance runs out, SMS messages will stop sending and will be sent as emails instead.",
"credit_limit_reached_message": "Your Cal.com team {{teamName}} has run out of credits. As a result, SMS messages are now being sent via email instead. To resume sending SMS, please purchase additional credits.",
"credit_limit_reached_message_user": "Your Cal.com account has run out of credits. As a result, SMS messages are now being sent via email instead. To resume sending SMS, please purchase additional credits.",
"cal_ai_low_credits_warning_message": "Your Cal.com team {{teamName}} is running low on credits. To avoid any disruption in Cal.ai phone service, please purchase additional credits. If your balance runs out, Cal.ai phone calls will be disabled.",
"cal_ai_low_credits_warning_message_user": "Your Cal.com account is running low on credits. To avoid any disruption in Cal.ai phone service, please purchase additional credits. If your balance runs out, Cal.ai phone calls will be disabled.",
"cal_ai_credit_limit_reached_message": "Your Cal.com team {{teamName}} has run out of credits. As a result, Cal.ai phone calls are now disabled. To resume using Cal.ai phone calls, please purchase additional credits.",
"cal_ai_credit_limit_reached_message_user": "Your Cal.com account has run out of credits. As a result, Cal.ai phone calls are now disabled. To resume using Cal.ai phone calls, please purchase additional credits.",
"current_credit_balance": "Current balance: {{balance}} credits",
"current_balance": "Current balance:",
"notification_about_your_booking": "Notification about your booking",
Expand Down
18 changes: 13 additions & 5 deletions packages/emails/email-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { formatCalEvent } from "@calcom/lib/formatCalendarEvent";
import logger from "@calcom/lib/logger";
import { safeStringify } from "@calcom/lib/safeStringify";
import { withReporting } from "@calcom/lib/sentryWrapper";
import type { CreditUsageType } from "@calcom/prisma/enums";
import type { EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils";
import type { CalendarEvent, Person } from "@calcom/types/Calendar";

Expand Down Expand Up @@ -852,28 +853,32 @@ export const sendCreditBalanceLowWarningEmails = async (input: {
t: TFunction;
};
balance: number;
creditFor?: CreditUsageType;
}) => {
const { team, balance, user } = input;
const { team, balance, user, creditFor } = input;
if ((!team || !team.adminAndOwners.length) && !user) return;

if (team) {
const emailsToSend: Promise<unknown>[] = [];

for (const admin of team.adminAndOwners) {
emailsToSend.push(sendEmail(() => new CreditBalanceLowWarningEmail({ user: admin, balance, team })));
emailsToSend.push(
sendEmail(() => new CreditBalanceLowWarningEmail({ user: admin, balance, team, creditFor }))
);
}

await Promise.all(emailsToSend);
}

if (user) {
await sendEmail(() => new CreditBalanceLowWarningEmail({ user, balance }));
await sendEmail(() => new CreditBalanceLowWarningEmail({ user, balance, creditFor }));
}
};

export const sendCreditBalanceLimitReachedEmails = async ({
team,
user,
creditFor,
}: {
team?: {
name: string;
Expand All @@ -891,20 +896,23 @@ export const sendCreditBalanceLimitReachedEmails = async ({
email: string;
t: TFunction;
};
creditFor?: CreditUsageType;
}) => {
if ((!team || !team.adminAndOwners.length) && !user) return;

if (team) {
const emailsToSend: Promise<unknown>[] = [];

for (const admin of team.adminAndOwners) {
emailsToSend.push(sendEmail(() => new CreditBalanceLimitReachedEmail({ user: admin, team })));
emailsToSend.push(
sendEmail(() => new CreditBalanceLimitReachedEmail({ user: admin, team, creditFor }))
);
}
await Promise.all(emailsToSend);
}

if (user) {
await sendEmail(() => new CreditBalanceLimitReachedEmail({ user }));
await sendEmail(() => new CreditBalanceLimitReachedEmail({ user, creditFor }));
}
};

Expand Down
17 changes: 14 additions & 3 deletions packages/emails/src/templates/CreditBalanceLimitReachedEmail.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { TFunction } from "i18next";

import { WEBAPP_URL } from "@calcom/lib/constants";
import { CreditUsageType } from "@calcom/prisma/enums";

import { CallToAction, V2BaseEmailHtml } from "../components";
import type { BaseScheduledEmail } from "./BaseScheduledEmail";
Expand All @@ -17,9 +18,11 @@ export const CreditBalanceLimitReachedEmail = (
email: string;
t: TFunction;
};
creditFor?: CreditUsageType;
} & Partial<React.ComponentProps<typeof BaseScheduledEmail>>
) => {
const { team, user } = props;
const { team, user, creditFor } = props;
const isCalAi = creditFor === CreditUsageType.CAL_AI_PHONE_CALL;

if (team) {
return (
Expand All @@ -28,7 +31,11 @@ export const CreditBalanceLimitReachedEmail = (
<> {user.t("hi_user_name", { name: user.name })},</>
</p>
<p style={{ fontWeight: 400, lineHeight: "24px", marginBottom: "20px" }}>
<>{user.t("credit_limit_reached_message", { teamName: team.name })}</>
<>
{isCalAi
? user.t("cal_ai_credit_limit_reached_message", { teamName: team.name })
: user.t("credit_limit_reached_message", { teamName: team.name })}
</>
</p>
<div style={{ textAlign: "center", marginTop: "24px" }}>
<CallToAction
Expand All @@ -47,7 +54,11 @@ export const CreditBalanceLimitReachedEmail = (
<> {user.t("hi_user_name", { name: user.name })},</>
</p>
<p style={{ fontWeight: 400, lineHeight: "24px", marginBottom: "20px" }}>
<>{user.t("credit_limit_reached_message_user")}</>
<>
{isCalAi
? user.t("cal_ai_credit_limit_reached_message_user")
: user.t("credit_limit_reached_message_user")}
</>
</p>
<div style={{ textAlign: "center", marginTop: "24px" }}>
<CallToAction
Expand Down
17 changes: 14 additions & 3 deletions packages/emails/src/templates/CreditBalanceLowWarningEmail.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { TFunction } from "i18next";

import { WEBAPP_URL } from "@calcom/lib/constants";
import { CreditUsageType } from "@calcom/prisma/enums";

import { CallToAction, V2BaseEmailHtml } from "../components";
import type { BaseScheduledEmail } from "./BaseScheduledEmail";
Expand All @@ -18,9 +19,11 @@ export const CreditBalanceLowWarningEmail = (
email: string;
t: TFunction;
};
creditFor?: CreditUsageType;
} & Partial<React.ComponentProps<typeof BaseScheduledEmail>>
) => {
const { team, balance, user } = props;
const { team, balance, user, creditFor } = props;
const isCalAi = creditFor === CreditUsageType.CAL_AI_PHONE_CALL;

if (team) {
return (
Expand All @@ -29,7 +32,11 @@ export const CreditBalanceLowWarningEmail = (
<> {user.t("hi_user_name", { name: user.name })},</>
</p>
<p style={{ fontWeight: 400, lineHeight: "24px", marginBottom: "20px" }}>
<>{user.t("low_credits_warning_message", { teamName: team.name })}</>
<>
{isCalAi
? user.t("cal_ai_low_credits_warning_message", { teamName: team.name })
: user.t("low_credits_warning_message", { teamName: team.name })}
</>
</p>
<p
style={{
Expand All @@ -56,7 +63,11 @@ export const CreditBalanceLowWarningEmail = (
<> {user.t("hi_user_name", { name: user.name })},</>
</p>
<p style={{ fontWeight: 400, lineHeight: "24px", marginBottom: "20px" }}>
<>{user.t("low_credits_warning_message_user")}</>
<>
{isCalAi
? user.t("cal_ai_low_credits_warning_message_user")
: user.t("low_credits_warning_message_user")}
</>
</p>
<div style={{ textAlign: "center", marginTop: "24px" }}>
<CallToAction
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { TFunction } from "i18next";

import { EMAIL_FROM_NAME } from "@calcom/lib/constants";
import type { CreditUsageType } from "@calcom/prisma/enums";

import renderEmail from "../src/renderEmail";
import BaseEmail from "./_base-email";
Expand All @@ -16,17 +17,21 @@ export default class CreditBalanceLimitReachedEmail extends BaseEmail {
id: number;
name: string;
};
creditFor?: CreditUsageType;

constructor({
user,
team,
creditFor,
}: {
user: { id: number; name: string | null; email: string; t: TFunction };
team?: { id: number; name: string | null };
creditFor?: CreditUsageType;
}) {
super();
this.user = { ...user, name: user.name || "" };
this.team = team ? { ...team, name: team.name || "" } : undefined;
this.creditFor = creditFor;
}

protected async getNodeMailerPayload(): Promise<Record<string, unknown>> {
Expand All @@ -39,6 +44,7 @@ export default class CreditBalanceLimitReachedEmail extends BaseEmail {
html: await renderEmail("CreditBalanceLimitReachedEmail", {
team: this.team,
user: this.user,
creditFor: this.creditFor,
}),
text: this.getTextBody(),
};
Expand Down
6 changes: 6 additions & 0 deletions packages/emails/templates/credit-balance-low-warning-email.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { TFunction } from "i18next";

import { EMAIL_FROM_NAME } from "@calcom/lib/constants";
import type { CreditUsageType } from "@calcom/prisma/enums";

import renderEmail from "../src/renderEmail";
import BaseEmail from "./_base-email";
Expand All @@ -17,20 +18,24 @@ export default class CreditBalanceLowWarningEmail extends BaseEmail {
name: string;
};
balance: number;
creditFor?: CreditUsageType;

constructor({
user,
balance,
team,
creditFor,
}: {
user: { id: number; name: string | null; email: string; t: TFunction };
balance: number;
team?: { id: number; name: string | null };
creditFor?: CreditUsageType;
}) {
super();
this.user = { ...user, name: user.name || "" };
this.team = team ? { ...team, name: team.name || "" } : undefined;
this.balance = balance;
this.creditFor = creditFor;
}

protected async getNodeMailerPayload(): Promise<Record<string, unknown>> {
Expand All @@ -44,6 +49,7 @@ export default class CreditBalanceLowWarningEmail extends BaseEmail {
balance: this.balance,
team: this.team,
user: this.user,
creditFor: this.creditFor,
}),
text: this.getTextBody(),
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ export class AgentService {
userId,
teamId,
expiresAt: null,
note: `Cal AI Phone API Key for agent ${userId} ${teamId ? `for team ${teamId}` : ""}`,
note: `Cal.ai Phone API Key for agent ${userId} ${teamId ? `for team ${teamId}` : ""}`,
});
}

Expand Down
Loading
Loading