diff --git a/backend/src/ee/services/license/license-service.ts b/backend/src/ee/services/license/license-service.ts index 45f74fd8c..9b6df4eaf 100644 --- a/backend/src/ee/services/license/license-service.ts +++ b/backend/src/ee/services/license/license-service.ts @@ -40,6 +40,7 @@ import { TOrgPlanDTO, TOrgPlansTableDTO, TOrgPmtMethodsDTO, + TPlanBillingInfo, TStartOrgTrialDTO, TUpdateOrgBillingDetailsDTO } from "./license-types"; @@ -465,6 +466,21 @@ export const licenseServiceFactory = ({ return { url }; }; + const getUsageMetrics = async (orgId: string) => { + const [orgMembersUsed, identityUsed, projectCount] = await Promise.all([ + orgDAL.countAllOrgMembers(orgId), + licenseDAL.countOfOrgIdentities(orgId), + projectDAL.countOfOrgProjects(orgId) + ]); + + return { + orgMembersUsed, + identityUsed, + projectCount, + totalIdentities: identityUsed + orgMembersUsed + }; + }; + const getOrgBillingInfo = async ({ orgId, actor, actorId, actorAuthMethod, actorOrgId }: TGetOrgBillInfoDTO) => { const { permission } = await permissionService.getOrgPermission({ actorId, @@ -483,10 +499,16 @@ export const licenseServiceFactory = ({ }); } if (instanceType === InstanceType.Cloud) { - const { data } = await licenseServerCloudApi.request.get( + const { data } = await licenseServerCloudApi.request.get( `/api/license-server/v1/customers/${organization.customerId}/cloud-plan/billing` ); - return data; + const { identityUsed, orgMembersUsed } = await getUsageMetrics(orgId); + + return { + ...data, + users: orgMembersUsed, + identities: identityUsed + }; } return { @@ -495,7 +517,9 @@ export const licenseServiceFactory = ({ interval: "month", intervalCount: 1, amount: 0, - quantity: 1 + quantity: 1, + users: 0, + identities: 0 }; }; @@ -539,21 +563,6 @@ export const licenseServiceFactory = ({ throw new Error(`Unsupported instance type for server-based plan table: ${instanceType}`); }; - const getUsageMetrics = async (orgId: string) => { - const [orgMembersUsed, identityUsed, projectCount] = await Promise.all([ - orgDAL.countAllOrgMembers(orgId), - licenseDAL.countOfOrgIdentities(orgId), - projectDAL.countOfOrgProjects(orgId) - ]); - - return { - orgMembersUsed, - identityUsed, - projectCount, - totalIdentities: identityUsed + orgMembersUsed - }; - }; - // returns org current plan feature table const getOrgPlanTable = async ({ orgId, actor, actorId, actorAuthMethod, actorOrgId }: TGetOrgBillInfoDTO) => { const { permission } = await permissionService.getOrgPermission({ diff --git a/backend/src/ee/services/license/license-types.ts b/backend/src/ee/services/license/license-types.ts index 93f40ae6e..c4ff6a8fa 100644 --- a/backend/src/ee/services/license/license-types.ts +++ b/backend/src/ee/services/license/license-types.ts @@ -22,6 +22,15 @@ export type TOfflineLicense = { features: TFeatureSet; }; +export type TPlanBillingInfo = { + currentPeriodStart: number; + currentPeriodEnd: number; + interval: "month" | "year"; + intervalCount: number; + amount: number; + quantity: number; +}; + export type TFeatureSet = { _id: null; slug: string | null; diff --git a/frontend/src/hooks/api/organization/types.ts b/frontend/src/hooks/api/organization/types.ts index e9c36fada..867450830 100644 --- a/frontend/src/hooks/api/organization/types.ts +++ b/frontend/src/hooks/api/organization/types.ts @@ -66,6 +66,8 @@ export type PlanBillingInfo = { interval: "month" | "year"; intervalCount: number; quantity: number; + users: number; + identities: number; }; export type Invoice = { diff --git a/frontend/src/pages/organization/BillingPage/components/BillingCloudTab/PreviewSection.tsx b/frontend/src/pages/organization/BillingPage/components/BillingCloudTab/PreviewSection.tsx index d22eab08b..7edc8b0db 100644 --- a/frontend/src/pages/organization/BillingPage/components/BillingCloudTab/PreviewSection.tsx +++ b/frontend/src/pages/organization/BillingPage/components/BillingCloudTab/PreviewSection.tsx @@ -1,10 +1,10 @@ import { useEffect } from "react"; -import { faArrowUpRightFromSquare } from "@fortawesome/free-solid-svg-icons"; +import { faArrowUpRightFromSquare, faInfoCircle } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { useQueryClient } from "@tanstack/react-query"; import { OrgPermissionCan } from "@app/components/permissions"; -import { Button } from "@app/components/v2"; +import { Button, Tooltip } from "@app/components/v2"; import { OrgPermissionBillingActions, OrgPermissionSubjects, @@ -27,6 +27,9 @@ export const PreviewSection = () => { const { subscription } = useSubscription(true); const queryClient = useQueryClient(); const { data, isPending } = useGetOrgPlanBillingInfo(currentOrg?.id ?? ""); + + const totalAmount = data?.amount ? data.amount * (data.users + data.identities) : 0; + const getOrgTrialUrl = useGetOrgTrialUrl(); const createCustomerPortalSession = useCreateCustomerPortalSession(); @@ -190,14 +193,28 @@ export const PreviewSection = () => { )} -
-

Price

-

- {subscription.status === "trialing" - ? "$0.00 / month" - : `${formatAmount(data.amount)} / ${data.interval}`} -

-
+ {subscription.slug !== "enterprise" ? ( +
+

Price

+

+ {subscription.status === "trialing" ? ( + "$0.00 / month" + ) : ( + <> + {formatAmount(totalAmount)} / {data.interval} + {(subscription.slug === "pro" || subscription.slug === "pro-annual") && ( + 1 ? "users" : "user"} and ${data.identities} ${data.identities > 1 ? "machine identities" : "machine identity"}.`} + className="max-w-lg" + > + + + )} + + )} +

+
+ ) : null}

Subscription renews on