Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
16 changes: 13 additions & 3 deletions apps/web/modules/settings/billing/components/BillingCredits.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
"use client";

import { useSession } from "next-auth/react";
import Link from "next/link";
import { useRouter } from "next/navigation";
import { useRouter, usePathname } from "next/navigation";
import { useState, useMemo } from "react";
import { useForm } from "react-hook-form";

Expand Down Expand Up @@ -53,6 +54,8 @@ const getMonthOptions = (): MonthOption[] => {
export default function BillingCredits() {
const { t } = useLocale();
const router = useRouter();
const pathname = usePathname();
const session = useSession();
const monthOptions = useMemo(() => getMonthOptions(), []);
const [selectedMonth, setSelectedMonth] = useState<MonthOption>(monthOptions[0]);
const [isDownloading, setIsDownloading] = useState(false);
Expand All @@ -66,7 +69,9 @@ export default function BillingCredits() {
} = useForm<{ quantity: number }>({ defaultValues: { quantity: 50 } });

const params = useParamsWithFallback();
const teamId = params.id ? Number(params.id) : undefined;
const orgId = session.data?.user?.org?.id;

const teamId = params.id ? Number(params.id) : orgId;

const { data: creditsData, isLoading } = trpc.viewer.credits.getAllCredits.useQuery({ teamId });

Expand Down Expand Up @@ -106,6 +111,11 @@ export default function BillingCredits() {
return null;
}

if (orgId && !pathname?.includes("/organizations/")) {
// Don't show credits on personal billing if user is an org member
return null;
}

if (isLoading && teamId) return <BillingCreditsSkeleton />;
if (!creditsData) return null;

Expand Down Expand Up @@ -206,7 +216,7 @@ export default function BillingCredits() {
<hr className="border-subtle mb-3 mt-3" />
</div>
<div className="flex">
<div className="mr-auto ">
<div className="mr-auto">
<Label className="mb-4">{t("download_expense_log")}</Label>
<div className="mt-2 flex flex-col">
<Select
Expand Down
28 changes: 28 additions & 0 deletions packages/features/ee/billing/credit-service.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -446,6 +446,34 @@ describe("CreditService", () => {
const result = await creditService.getMonthlyCredits(1);
expect(result).toBe(1500); // (3 members * 1000 price) / 2
});

it("should calculate credits with 20% multiplier for organizations", async () => {
const mockTeamRepo = {
findTeamWithMembers: vi.fn().mockResolvedValue({
id: 1,
isOrganization: true,
members: [{ accepted: true }, { accepted: true }],
}),
};
vi.mocked(TeamRepository).mockImplementation(() => mockTeamRepo as any);

const mockTeamBillingService = {
getSubscriptionStatus: vi.fn().mockResolvedValue("active"),
};
vi.spyOn(InternalTeamBilling.prototype, "getSubscriptionStatus").mockImplementation(
mockTeamBillingService.getSubscriptionStatus
);

const mockStripeBillingService = {
getPrice: vi.fn().mockResolvedValue({ unit_amount: 3700 }),
};
vi.spyOn(StripeBillingService.prototype, "getPrice").mockImplementation(
mockStripeBillingService.getPrice
);

const result = await creditService.getMonthlyCredits(1);
expect(result).toBe(1480); // (2 members * 3700 price) * 0.2
});
});

describe("getAllCreditsForTeam", () => {
Expand Down
15 changes: 11 additions & 4 deletions packages/features/ee/billing/credit-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -166,7 +166,7 @@ export class CreditService {
);
return true;
}
// limtReachedAt is set and still no available credits
// limitReachedAt is set and still no available credits
return false;
}

Expand Down Expand Up @@ -555,9 +555,16 @@ export class CreditService {

const billingService = new StripeBillingService();

const teamMonthlyPrice = await billingService.getPrice(process.env.STRIPE_TEAM_MONTHLY_PRICE_ID || "");
const pricePerSeat = teamMonthlyPrice.unit_amount ?? 0;
totalMonthlyCredits = (activeMembers * pricePerSeat) / 2;
const priceId = team.isOrganization
? process.env.STRIPE_ORG_MONTHLY_PRICE_ID
: process.env.STRIPE_TEAM_MONTHLY_PRICE_ID;

const monthlyPrice = await billingService.getPrice(priceId || "");
const pricePerSeat = monthlyPrice.unit_amount ?? 0;

// Teams get 50% of the price as credits, organizations get 20%
const creditMultiplier = team.isOrganization ? 0.2 : 0.5;
totalMonthlyCredits = activeMembers * pricePerSeat * creditMultiplier;

return totalMonthlyCredits;
}
Expand Down
19 changes: 16 additions & 3 deletions packages/trpc/server/routers/viewer/credits/buyCredits.handler.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { StripeBillingService } from "@calcom/features/ee/billing/stripe-billling-service";
import { WEBAPP_URL } from "@calcom/lib/constants";
import { MembershipRepository } from "@calcom/lib/server/repository/membership";
import { prisma } from "@calcom/prisma";
import type { TrpcSessionUser } from "@calcom/trpc/server/types";

import { TRPCError } from "@trpc/server";
Expand Down Expand Up @@ -43,9 +44,21 @@ export const buyCreditsHandler = async ({ ctx, input }: BuyCreditsOptions) => {
}
}

const redirect_uri = teamId
? `${WEBAPP_URL}/settings/teams/${teamId}/billing`
: `${WEBAPP_URL}/settings/billing`;
let redirect_uri = `${WEBAPP_URL}/settings/billing`;

if (teamId) {
// Check if the team is an organization
const team = await prisma.team.findUnique({
where: { id: teamId },
select: { isOrganization: true },
});

if (team?.isOrganization) {
redirect_uri = `${WEBAPP_URL}/settings/organizations/billing`;
} else {
redirect_uri = `${WEBAPP_URL}/settings/teams/${teamId}/billing`;
}
}

const billingService = new StripeBillingService();

Expand Down