+
+ {/* Credits title */}
+ {/* View and manage credits description */}
+
+
+
+ {/* Credits section */}
+
+ {/* Monthly credits row */}
+
+
+
-
-
{/* Remaining credits */}
+ {/* Additional credits row */}
+
+
+
+
+ {/* Remaining row */}
+
+
+
+
+ {/* Progress bar */}
+
+ {/* Credits per tip */}
+
+
+
+
+
+
+
+
+ {/* Additional Credits form */}
+
+
+
{/* Additional credits label */}
+
+ {/* Input field */}
+
+
+
+ {/* Buy button */}
+
+
+
+
+
+ {/* Download Expense Log */}
+
+
+
{/* Download expense log label */}
+
+ {/* Select dropdown */}
+
+
+
+ {/* Download button */}
- {/* Additional credits */}
-
-
- {/* Additional credits label */}
-
-
- {/* Additional credits value */}
-
-
-
-
-
- {/* Buy credits form */}
-
-
-
- {/* Buy credits label */}
-
-
- {/* Input field */}
-
-
-
-
- {/* Buy button */}
-
-
-
-
-
- {/* Download expense log */}
-
-
-
- {/* Label */}
-
-
- {/* Select */}
-
-
-
-
- {/* Download button */}
-
-
+
+ {/* Credit Worth Section */}
+
+
+
diff --git a/apps/web/public/static/locales/en/common.json b/apps/web/public/static/locales/en/common.json
index 90a32247a7..c3892521de 100644
--- a/apps/web/public/static/locales/en/common.json
+++ b/apps/web/public/static/locales/en/common.json
@@ -900,6 +900,7 @@
"yes_cancel_subscription": "Yes, Cancel Subscription",
"cancel_phone_number_subscription_confirmation": "Are you sure you want to cancel this phone number subscription? This action cannot be undone and you will lose access to this phone number.",
"add_members": "Add members...",
+ "add_members_no_ellipsis": "Add members",
"no_assigned_members": "No assigned members",
"assigned_to": "Assigned to",
"you_must_be_logged_in_to": "You must be logged in to {{url}}",
@@ -3234,9 +3235,15 @@
"verify_email_change": "Verify email change",
"buy_credits": "Buy Credits",
"credits": "Credits",
- "view_and_manage_credits": "View and manage credits",
+ "credits_used": "Credits used",
+ "total_credits_remaining": "Total remaining",
+ "credits_per_tip_org": "You receive 1000 credits per month, per team member",
+ "credits_per_tip_teams": "You receive 750 credits per month, per team member",
+ "view_and_manage_credits": "View and manage credits for sending SMS messages",
"view_and_manage_credits_description": "View and manage credits for sending SMS messages. One credit is worth 1¢ (USD). <0>Learn more0>",
+ "credit_worth_description": "One credit is worth 1¢ (USD). <0>Learn more0>",
"buy_additional_credits": "Buy additional credits ($0.01 per credit)",
+ "view_additional_credits_expense_tip": "You can view Additional credit spending in your expense log",
"overview": "Overview",
"organization_slug_taken": "Organization slug is already taken",
"you_cannot_create_an_organization_as_you_are_already_part_of_an_organization": "You cannot create an organization as you are already a part of an organization",
@@ -3417,10 +3424,13 @@
"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.",
"current_credit_balance": "Current balance: {{balance}} credits",
+ "current_balance": "Current balance:",
"notification_about_your_booking": "Notification about your booking",
"monthly_credits": "Monthly credits",
"total_credits": "Total credits: {{totalCredits}}",
"remaining_credits": "Remaining credits: {{remainingCredits}}",
+ "remaining": "Remaining",
+ "total": "Total",
"additional_credits": "Additional credits",
"routing_form_next_in_queue": "{{count}} next in queue",
"routing_form_select_members_to_email": "Send email responses to",
diff --git a/packages/features/ee/billing/credit-service.test.ts b/packages/features/ee/billing/credit-service.test.ts
index 702f93b9ca..e19106ab6a 100644
--- a/packages/features/ee/billing/credit-service.test.ts
+++ b/packages/features/ee/billing/credit-service.test.ts
@@ -59,14 +59,16 @@ vi.mock("@calcom/lib/server/repository/membership");
vi.mock("@calcom/lib/server/repository/team");
vi.mock("@calcom/emails/email-manager");
vi.mock("../workflows/lib/reminders/reminderScheduler", () => ({
- cancelScheduledMessagesAndScheduleEmails: vi.fn(),
+ cancelScheduledMessagesAndScheduleEmails: vi.fn().mockResolvedValue(undefined),
}));
const creditService = new CreditService();
vi.spyOn(creditService, "_getAllCreditsForTeam").mockResolvedValue({
+ totalMonthlyCredits: 10,
totalRemainingMonthlyCredits: 5,
additionalCredits: 0,
+ totalCreditsUsedThisMonth: 5,
});
vi.spyOn(creditService, "_getTeamWithAvailableCredits").mockResolvedValue({
@@ -233,6 +235,7 @@ describe("CreditService", () => {
totalMonthlyCredits: 500,
totalRemainingMonthlyCredits: 20,
additionalCredits: 60,
+ totalCreditsUsedThisMonth: 480,
});
await creditService.chargeCredits({
@@ -291,6 +294,7 @@ describe("CreditService", () => {
totalMonthlyCredits: 500,
totalRemainingMonthlyCredits: -1,
additionalCredits: 0,
+ totalCreditsUsedThisMonth: 501,
});
await creditService.chargeCredits({
@@ -321,6 +325,7 @@ describe("CreditService", () => {
totalMonthlyCredits: 500,
totalRemainingMonthlyCredits: 100,
additionalCredits: 50,
+ totalCreditsUsedThisMonth: 400,
});
const result = await creditService.getUserOrTeamToCharge({
@@ -340,6 +345,7 @@ describe("CreditService", () => {
totalMonthlyCredits: 500,
totalRemainingMonthlyCredits: 0,
additionalCredits: 50,
+ totalCreditsUsedThisMonth: 500,
});
const result = await creditService.getUserOrTeamToCharge({
@@ -483,13 +489,18 @@ describe("CreditService", () => {
describe("getAllCreditsForTeam", () => {
it("should calculate total and remaining credits correctly", async () => {
- vi.mocked(CreditsRepository.findCreditBalanceWithExpenseLogs).mockResolvedValue({
- additionalCredits: 100,
- expenseLogs: [
- { credits: 50, date: new Date() },
- { credits: 30, date: new Date() },
- ],
- });
+ vi.mocked(CreditsRepository.findCreditBalanceWithExpenseLogs)
+ .mockResolvedValueOnce({
+ additionalCredits: 100,
+ expenseLogs: [
+ { credits: 50, date: new Date() },
+ { credits: 30, date: new Date() },
+ ],
+ })
+ .mockResolvedValueOnce({
+ additionalCredits: 100,
+ expenseLogs: [{ credits: 80, date: new Date() }],
+ });
vi.spyOn(CreditService.prototype, "getMonthlyCredits").mockResolvedValue(500);
@@ -498,14 +509,20 @@ describe("CreditService", () => {
totalMonthlyCredits: 500,
totalRemainingMonthlyCredits: 420, // 500 - (50 + 30)
additionalCredits: 100,
+ totalCreditsUsedThisMonth: 80, // 50 + 30
});
});
it("should handle no expense logs", async () => {
- vi.mocked(CreditsRepository.findCreditBalanceWithExpenseLogs).mockResolvedValue({
- additionalCredits: 100,
- expenseLogs: [],
- });
+ vi.mocked(CreditsRepository.findCreditBalanceWithExpenseLogs)
+ .mockResolvedValueOnce({
+ additionalCredits: 100,
+ expenseLogs: [],
+ })
+ .mockResolvedValueOnce({
+ additionalCredits: 100,
+ expenseLogs: [],
+ });
vi.spyOn(CreditService.prototype, "getMonthlyCredits").mockResolvedValue(500);
@@ -514,6 +531,57 @@ describe("CreditService", () => {
totalMonthlyCredits: 500,
totalRemainingMonthlyCredits: 500,
additionalCredits: 100,
+ totalCreditsUsedThisMonth: 0, // no expenses
+ });
+ });
+
+ it("should calculate total credits including additional credits for the month", async () => {
+ vi.mocked(CreditsRepository.findCreditBalanceWithExpenseLogs)
+ .mockResolvedValueOnce({
+ additionalCredits: 150,
+ expenseLogs: [
+ { credits: 80, date: new Date() },
+ { credits: 40, date: new Date() },
+ ],
+ })
+ .mockResolvedValueOnce({
+ additionalCredits: 150,
+ expenseLogs: [
+ { credits: 25, date: new Date() },
+ { credits: 15, date: new Date() },
+ ],
+ });
+
+ vi.spyOn(CreditService.prototype, "getMonthlyCredits").mockResolvedValue(500);
+
+ const result = await creditService.getAllCreditsForTeam(1);
+ expect(result).toEqual({
+ totalMonthlyCredits: 500,
+ totalRemainingMonthlyCredits: 380, // 500 - (80 + 40)
+ additionalCredits: 150,
+ totalCreditsUsedThisMonth: 120, // 80 + 40
+ });
+ });
+
+ it("should handle zero additional credits", async () => {
+ vi.mocked(CreditsRepository.findCreditBalanceWithExpenseLogs)
+ .mockResolvedValueOnce({
+ additionalCredits: 0,
+ expenseLogs: [{ credits: 100, date: new Date() }],
+ })
+ .mockResolvedValueOnce({
+ additionalCredits: 0,
+ expenseLogs: [],
+ });
+
+ vi.spyOn(CreditService.prototype, "getMonthlyCredits").mockResolvedValue(500);
+
+ const result = await creditService.getAllCreditsForTeam(1);
+ expect(result).toEqual({
+ totalMonthlyCredits: 500,
+ totalRemainingMonthlyCredits: 400, // 500 - 100
+ additionalCredits: 0,
+ totalCreditsUsedThisMonth: 100,
});
});
});
@@ -704,6 +772,7 @@ describe("CreditService", () => {
totalMonthlyCredits: 500,
totalRemainingMonthlyCredits: 200,
additionalCredits: 100,
+ totalCreditsUsedThisMonth: 300,
});
const result = await creditService.getTeamWithAvailableCredits(1);
expect(result).toEqual({
@@ -827,6 +896,7 @@ describe("CreditService", () => {
totalMonthlyCredits: 500,
totalRemainingMonthlyCredits: 100,
additionalCredits: 100,
+ totalCreditsUsedThisMonth: 400,
});
await creditService.chargeCredits({
diff --git a/packages/features/ee/billing/credit-service.ts b/packages/features/ee/billing/credit-service.ts
index 067b3f3dde..3f80f16d81 100644
--- a/packages/features/ee/billing/credit-service.ts
+++ b/packages/features/ee/billing/credit-service.ts
@@ -651,6 +651,7 @@ export class CreditService {
totalMonthlyCredits: 0,
totalRemainingMonthlyCredits: 0,
additionalCredits: creditBalance?.additionalCredits ?? 0,
+ totalCreditsUsedThisMonth: 0,
};
}
@@ -658,6 +659,7 @@ export class CreditService {
totalMonthlyCredits: 0,
totalRemainingMonthlyCredits: 0,
additionalCredits: 0,
+ totalCreditsUsedThisMonth: 0,
};
}
@@ -677,10 +679,14 @@ export class CreditService {
const totalMonthlyCreditsUsed =
creditBalance?.expenseLogs.reduce((sum, log) => sum + (log?.credits ?? 0), 0) || 0;
+ const additionalCredits = creditBalance?.additionalCredits ?? 0;
+ const totalCreditsUsedThisMonth = totalMonthlyCreditsUsed;
+
return {
totalMonthlyCredits,
totalRemainingMonthlyCredits: Math.max(totalMonthlyCredits - totalMonthlyCreditsUsed, 0),
- additionalCredits: creditBalance?.additionalCredits ?? 0,
+ additionalCredits,
+ totalCreditsUsedThisMonth,
};
}
}