Skip to content

Commit 2df2868

Browse files
Udit-takkarCarinaWolli
andauthored
fix: cal ai webhook (calcom#24368)
* fix: cal ai email * fix: remove * fix: org * replace Cal AI with Cal.ai * fix: use * fix: feedback * fix: types * fix: types * fix: types * fix: tests * Merge branch 'main' into fix/cal-ai-credits * refactor: feedback * refactor: imporvement * fix: type * refactor: feedback * fix: tests * fix: use pbac --------- Co-authored-by: CarinaWolli <wollencarina@gmail.com>
1 parent 7f48c7f commit 2df2868

23 files changed

Lines changed: 589 additions & 74 deletions

File tree

apps/web/app/(use-page-wrapper)/workflow/new/page.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -130,7 +130,7 @@ const Page = async ({ searchParams }: PageProps) => {
130130
throw error;
131131
}
132132

133-
console.error("Failed to create Cal AI workflow:", error);
133+
console.error("Failed to create Cal.ai workflow:", error);
134134
redirect("/workflows?error=failed-to-create-workflow");
135135
}
136136
};

apps/web/app/api/webhooks/retell-ai/__tests__/route.test.ts

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { Retell } from "retell-sdk";
33
import { describe, it, expect, vi, beforeEach } from "vitest";
44

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

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

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

7273
const mockHasAvailableCredits = vi.fn();
7374
const mockChargeCredits = vi.fn();
75+
const mockSendCreditBalanceLimitReachedEmails = vi.fn();
76+
const mockSendCreditBalanceLowWarningEmails = vi.fn();
7477

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

85+
vi.mock("@calcom/emails/email-manager", () => ({
86+
sendCreditBalanceLimitReachedEmails: (...args: unknown[]) =>
87+
mockSendCreditBalanceLimitReachedEmails(...args),
88+
sendCreditBalanceLowWarningEmails: (...args: unknown[]) => mockSendCreditBalanceLowWarningEmails(...args),
89+
}));
90+
8291
const mockFindByPhoneNumber = vi.fn();
8392
const mockFindByProviderAgentId = vi.fn();
8493

@@ -231,6 +240,7 @@ describe("Retell AI Webhook Handler", () => {
231240
credits: 58, // 120 seconds = 2 minutes * $0.29 = $0.58 = 58 credits
232241
callDuration: 120,
233242
externalRef: "retell:test-call-id",
243+
creditFor: CreditUsageType.CAL_AI_PHONE_CALL,
234244
})
235245
);
236246
});
@@ -288,6 +298,7 @@ describe("Retell AI Webhook Handler", () => {
288298
credits: 87, // 180 seconds = 3 minutes * $0.29 = $0.87 = 87 credits
289299
callDuration: 180,
290300
externalRef: "retell:test-call-id",
301+
creditFor: CreditUsageType.CAL_AI_PHONE_CALL,
291302
})
292303
);
293304
});
@@ -471,6 +482,7 @@ describe("Retell AI Webhook Handler", () => {
471482
credits: expectedCredits,
472483
callDuration: durationSeconds,
473484
externalRef: expect.stringMatching(/^retell:test-call-/),
485+
creditFor: CreditUsageType.CAL_AI_PHONE_CALL,
474486
})
475487
);
476488
}
@@ -517,7 +529,12 @@ describe("Retell AI Webhook Handler", () => {
517529
const response = await callPOST(createMockRequest(body, "valid-signature"));
518530
expect(response.status).toBe(200);
519531
expect(mockChargeCredits).toHaveBeenCalledWith(
520-
expect.objectContaining({ userId: 42, credits: 61, callDuration: 125 }) // 125s = 2.083 minutes * $0.29 = $0.604 = 61 credits (rounded up)
532+
expect.objectContaining({
533+
userId: 42,
534+
credits: 61,
535+
callDuration: 125,
536+
creditFor: CreditUsageType.CAL_AI_PHONE_CALL,
537+
}) // 125s = 2.083 minutes * $0.29 = $0.604 = 61 credits (rounded up)
521538
);
522539
});
523540

@@ -574,6 +591,7 @@ describe("Retell AI Webhook Handler", () => {
574591
credits: 29, // 60 seconds = 1 minute * $0.29 = $0.29 = 29 credits
575592
callDuration: 60,
576593
externalRef: "retell:test-idempotency-call",
594+
creditFor: CreditUsageType.CAL_AI_PHONE_CALL,
577595
})
578596
);
579597
});
@@ -760,6 +778,7 @@ describe("Retell AI Webhook Handler", () => {
760778
credits: 4, // 7 seconds = 0.117 minutes * $0.29 = $0.034 = 4 credits (rounded up)
761779
callDuration: 7,
762780
externalRef: "retell:call_bcd94f5a50832873a5fd68cb1aa",
781+
creditFor: CreditUsageType.CAL_AI_PHONE_CALL,
763782
})
764783
);
765784
});
@@ -794,6 +813,7 @@ describe("Retell AI Webhook Handler", () => {
794813
teamId: 10,
795814
credits: 29, // 60 seconds = 1 minute * $0.29 = 29 credits
796815
callDuration: 60,
816+
creditFor: CreditUsageType.CAL_AI_PHONE_CALL,
797817
})
798818
);
799819
});

apps/web/app/api/webhooks/retell-ai/route.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,8 @@ const RetellWebhookSchema = z.object({
6363
.passthrough(),
6464
});
6565

66+
type RetellCallData = z.infer<typeof RetellWebhookSchema>["call"];
67+
6668
async function chargeCreditsForCall({
6769
userId,
6870
teamId,
@@ -120,7 +122,7 @@ async function chargeCreditsForCall({
120122
}
121123
}
122124

123-
async function handleCallAnalyzed(callData: any) {
125+
async function handleCallAnalyzed(callData: RetellCallData) {
124126
const { from_number, call_id, call_cost, call_type, agent_id } = callData;
125127

126128
if (
@@ -165,7 +167,7 @@ async function handleCallAnalyzed(callData: any) {
165167
}
166168

167169
userId = agent.userId ?? undefined;
168-
teamId = agent.teamId ?? undefined;
170+
teamId = agent.team?.parentId ?? agent.teamId ?? undefined;
169171

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

183185
userId = phoneNumber.userId ?? undefined;
184-
teamId = phoneNumber.teamId ?? undefined;
186+
teamId = phoneNumber.team?.parentId ?? phoneNumber.teamId ?? undefined;
185187

186188
log.info(`Processing phone call ${call_id} from ${from_number}, user ${userId}, team ${teamId}`);
187189
}

apps/web/public/static/locales/en/common.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3490,6 +3490,10 @@
34903490
"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.",
34913491
"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.",
34923492
"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.",
3493+
"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.",
3494+
"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.",
3495+
"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.",
3496+
"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.",
34933497
"current_credit_balance": "Current balance: {{balance}} credits",
34943498
"current_balance": "Current balance:",
34953499
"notification_about_your_booking": "Notification about your booking",

packages/emails/email-manager.ts

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { formatCalEvent } from "@calcom/lib/formatCalendarEvent";
1010
import logger from "@calcom/lib/logger";
1111
import { safeStringify } from "@calcom/lib/safeStringify";
1212
import { withReporting } from "@calcom/lib/sentryWrapper";
13+
import type { CreditUsageType } from "@calcom/prisma/enums";
1314
import type { EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils";
1415
import type { CalendarEvent, Person } from "@calcom/types/Calendar";
1516

@@ -852,28 +853,32 @@ export const sendCreditBalanceLowWarningEmails = async (input: {
852853
t: TFunction;
853854
};
854855
balance: number;
856+
creditFor?: CreditUsageType;
855857
}) => {
856-
const { team, balance, user } = input;
858+
const { team, balance, user, creditFor } = input;
857859
if ((!team || !team.adminAndOwners.length) && !user) return;
858860

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

862864
for (const admin of team.adminAndOwners) {
863-
emailsToSend.push(sendEmail(() => new CreditBalanceLowWarningEmail({ user: admin, balance, team })));
865+
emailsToSend.push(
866+
sendEmail(() => new CreditBalanceLowWarningEmail({ user: admin, balance, team, creditFor }))
867+
);
864868
}
865869

866870
await Promise.all(emailsToSend);
867871
}
868872

869873
if (user) {
870-
await sendEmail(() => new CreditBalanceLowWarningEmail({ user, balance }));
874+
await sendEmail(() => new CreditBalanceLowWarningEmail({ user, balance, creditFor }));
871875
}
872876
};
873877

874878
export const sendCreditBalanceLimitReachedEmails = async ({
875879
team,
876880
user,
881+
creditFor,
877882
}: {
878883
team?: {
879884
name: string;
@@ -891,20 +896,23 @@ export const sendCreditBalanceLimitReachedEmails = async ({
891896
email: string;
892897
t: TFunction;
893898
};
899+
creditFor?: CreditUsageType;
894900
}) => {
895901
if ((!team || !team.adminAndOwners.length) && !user) return;
896902

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

900906
for (const admin of team.adminAndOwners) {
901-
emailsToSend.push(sendEmail(() => new CreditBalanceLimitReachedEmail({ user: admin, team })));
907+
emailsToSend.push(
908+
sendEmail(() => new CreditBalanceLimitReachedEmail({ user: admin, team, creditFor }))
909+
);
902910
}
903911
await Promise.all(emailsToSend);
904912
}
905913

906914
if (user) {
907-
await sendEmail(() => new CreditBalanceLimitReachedEmail({ user }));
915+
await sendEmail(() => new CreditBalanceLimitReachedEmail({ user, creditFor }));
908916
}
909917
};
910918

packages/emails/src/templates/CreditBalanceLimitReachedEmail.tsx

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import type { TFunction } from "i18next";
22

33
import { WEBAPP_URL } from "@calcom/lib/constants";
4+
import { CreditUsageType } from "@calcom/prisma/enums";
45

56
import { CallToAction, V2BaseEmailHtml } from "../components";
67
import type { BaseScheduledEmail } from "./BaseScheduledEmail";
@@ -17,9 +18,11 @@ export const CreditBalanceLimitReachedEmail = (
1718
email: string;
1819
t: TFunction;
1920
};
21+
creditFor?: CreditUsageType;
2022
} & Partial<React.ComponentProps<typeof BaseScheduledEmail>>
2123
) => {
22-
const { team, user } = props;
24+
const { team, user, creditFor } = props;
25+
const isCalAi = creditFor === CreditUsageType.CAL_AI_PHONE_CALL;
2326

2427
if (team) {
2528
return (
@@ -28,7 +31,11 @@ export const CreditBalanceLimitReachedEmail = (
2831
<> {user.t("hi_user_name", { name: user.name })},</>
2932
</p>
3033
<p style={{ fontWeight: 400, lineHeight: "24px", marginBottom: "20px" }}>
31-
<>{user.t("credit_limit_reached_message", { teamName: team.name })}</>
34+
<>
35+
{isCalAi
36+
? user.t("cal_ai_credit_limit_reached_message", { teamName: team.name })
37+
: user.t("credit_limit_reached_message", { teamName: team.name })}
38+
</>
3239
</p>
3340
<div style={{ textAlign: "center", marginTop: "24px" }}>
3441
<CallToAction
@@ -47,7 +54,11 @@ export const CreditBalanceLimitReachedEmail = (
4754
<> {user.t("hi_user_name", { name: user.name })},</>
4855
</p>
4956
<p style={{ fontWeight: 400, lineHeight: "24px", marginBottom: "20px" }}>
50-
<>{user.t("credit_limit_reached_message_user")}</>
57+
<>
58+
{isCalAi
59+
? user.t("cal_ai_credit_limit_reached_message_user")
60+
: user.t("credit_limit_reached_message_user")}
61+
</>
5162
</p>
5263
<div style={{ textAlign: "center", marginTop: "24px" }}>
5364
<CallToAction

packages/emails/src/templates/CreditBalanceLowWarningEmail.tsx

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import type { TFunction } from "i18next";
22

33
import { WEBAPP_URL } from "@calcom/lib/constants";
4+
import { CreditUsageType } from "@calcom/prisma/enums";
45

56
import { CallToAction, V2BaseEmailHtml } from "../components";
67
import type { BaseScheduledEmail } from "./BaseScheduledEmail";
@@ -18,9 +19,11 @@ export const CreditBalanceLowWarningEmail = (
1819
email: string;
1920
t: TFunction;
2021
};
22+
creditFor?: CreditUsageType;
2123
} & Partial<React.ComponentProps<typeof BaseScheduledEmail>>
2224
) => {
23-
const { team, balance, user } = props;
25+
const { team, balance, user, creditFor } = props;
26+
const isCalAi = creditFor === CreditUsageType.CAL_AI_PHONE_CALL;
2427

2528
if (team) {
2629
return (
@@ -29,7 +32,11 @@ export const CreditBalanceLowWarningEmail = (
2932
<> {user.t("hi_user_name", { name: user.name })},</>
3033
</p>
3134
<p style={{ fontWeight: 400, lineHeight: "24px", marginBottom: "20px" }}>
32-
<>{user.t("low_credits_warning_message", { teamName: team.name })}</>
35+
<>
36+
{isCalAi
37+
? user.t("cal_ai_low_credits_warning_message", { teamName: team.name })
38+
: user.t("low_credits_warning_message", { teamName: team.name })}
39+
</>
3340
</p>
3441
<p
3542
style={{
@@ -56,7 +63,11 @@ export const CreditBalanceLowWarningEmail = (
5663
<> {user.t("hi_user_name", { name: user.name })},</>
5764
</p>
5865
<p style={{ fontWeight: 400, lineHeight: "24px", marginBottom: "20px" }}>
59-
<>{user.t("low_credits_warning_message_user")}</>
66+
<>
67+
{isCalAi
68+
? user.t("cal_ai_low_credits_warning_message_user")
69+
: user.t("low_credits_warning_message_user")}
70+
</>
6071
</p>
6172
<div style={{ textAlign: "center", marginTop: "24px" }}>
6273
<CallToAction

packages/emails/templates/credit-balance-limit-reached-email.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import type { TFunction } from "i18next";
22

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

56
import renderEmail from "../src/renderEmail";
67
import BaseEmail from "./_base-email";
@@ -16,17 +17,21 @@ export default class CreditBalanceLimitReachedEmail extends BaseEmail {
1617
id: number;
1718
name: string;
1819
};
20+
creditFor?: CreditUsageType;
1921

2022
constructor({
2123
user,
2224
team,
25+
creditFor,
2326
}: {
2427
user: { id: number; name: string | null; email: string; t: TFunction };
2528
team?: { id: number; name: string | null };
29+
creditFor?: CreditUsageType;
2630
}) {
2731
super();
2832
this.user = { ...user, name: user.name || "" };
2933
this.team = team ? { ...team, name: team.name || "" } : undefined;
34+
this.creditFor = creditFor;
3035
}
3136

3237
protected async getNodeMailerPayload(): Promise<Record<string, unknown>> {
@@ -39,6 +44,7 @@ export default class CreditBalanceLimitReachedEmail extends BaseEmail {
3944
html: await renderEmail("CreditBalanceLimitReachedEmail", {
4045
team: this.team,
4146
user: this.user,
47+
creditFor: this.creditFor,
4248
}),
4349
text: this.getTextBody(),
4450
};

packages/emails/templates/credit-balance-low-warning-email.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import type { TFunction } from "i18next";
22

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

56
import renderEmail from "../src/renderEmail";
67
import BaseEmail from "./_base-email";
@@ -17,20 +18,24 @@ export default class CreditBalanceLowWarningEmail extends BaseEmail {
1718
name: string;
1819
};
1920
balance: number;
21+
creditFor?: CreditUsageType;
2022

2123
constructor({
2224
user,
2325
balance,
2426
team,
27+
creditFor,
2528
}: {
2629
user: { id: number; name: string | null; email: string; t: TFunction };
2730
balance: number;
2831
team?: { id: number; name: string | null };
32+
creditFor?: CreditUsageType;
2933
}) {
3034
super();
3135
this.user = { ...user, name: user.name || "" };
3236
this.team = team ? { ...team, name: team.name || "" } : undefined;
3337
this.balance = balance;
38+
this.creditFor = creditFor;
3439
}
3540

3641
protected async getNodeMailerPayload(): Promise<Record<string, unknown>> {
@@ -44,6 +49,7 @@ export default class CreditBalanceLowWarningEmail extends BaseEmail {
4449
balance: this.balance,
4550
team: this.team,
4651
user: this.user,
52+
creditFor: this.creditFor,
4753
}),
4854
text: this.getTextBody(),
4955
};

packages/features/calAIPhone/providers/retellAI/services/AgentService.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ export class AgentService {
3737
userId,
3838
teamId,
3939
expiresAt: null,
40-
note: `Cal AI Phone API Key for agent ${userId} ${teamId ? `for team ${teamId}` : ""}`,
40+
note: `Cal.ai Phone API Key for agent ${userId} ${teamId ? `for team ${teamId}` : ""}`,
4141
});
4242
}
4343

0 commit comments

Comments
 (0)