fix: pricing calculation on billing page

This commit is contained in:
Piyush Gupta
2025-10-22 20:59:25 +05:30
parent 0c7a5cfe27
commit eb1bf79066
4 changed files with 65 additions and 28 deletions
@@ -40,6 +40,7 @@ import {
TOrgPlanDTO, TOrgPlanDTO,
TOrgPlansTableDTO, TOrgPlansTableDTO,
TOrgPmtMethodsDTO, TOrgPmtMethodsDTO,
TPlanBillingInfo,
TStartOrgTrialDTO, TStartOrgTrialDTO,
TUpdateOrgBillingDetailsDTO TUpdateOrgBillingDetailsDTO
} from "./license-types"; } from "./license-types";
@@ -465,6 +466,21 @@ export const licenseServiceFactory = ({
return { url }; 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 getOrgBillingInfo = async ({ orgId, actor, actorId, actorAuthMethod, actorOrgId }: TGetOrgBillInfoDTO) => {
const { permission } = await permissionService.getOrgPermission({ const { permission } = await permissionService.getOrgPermission({
actorId, actorId,
@@ -483,10 +499,16 @@ export const licenseServiceFactory = ({
}); });
} }
if (instanceType === InstanceType.Cloud) { if (instanceType === InstanceType.Cloud) {
const { data } = await licenseServerCloudApi.request.get( const { data } = await licenseServerCloudApi.request.get<TPlanBillingInfo>(
`/api/license-server/v1/customers/${organization.customerId}/cloud-plan/billing` `/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 { return {
@@ -495,7 +517,9 @@ export const licenseServiceFactory = ({
interval: "month", interval: "month",
intervalCount: 1, intervalCount: 1,
amount: 0, 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}`); 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 // returns org current plan feature table
const getOrgPlanTable = async ({ orgId, actor, actorId, actorAuthMethod, actorOrgId }: TGetOrgBillInfoDTO) => { const getOrgPlanTable = async ({ orgId, actor, actorId, actorAuthMethod, actorOrgId }: TGetOrgBillInfoDTO) => {
const { permission } = await permissionService.getOrgPermission({ const { permission } = await permissionService.getOrgPermission({
@@ -22,6 +22,15 @@ export type TOfflineLicense = {
features: TFeatureSet; features: TFeatureSet;
}; };
export type TPlanBillingInfo = {
currentPeriodStart: number;
currentPeriodEnd: number;
interval: "month" | "year";
intervalCount: number;
amount: number;
quantity: number;
};
export type TFeatureSet = { export type TFeatureSet = {
_id: null; _id: null;
slug: string | null; slug: string | null;
@@ -66,6 +66,8 @@ export type PlanBillingInfo = {
interval: "month" | "year"; interval: "month" | "year";
intervalCount: number; intervalCount: number;
quantity: number; quantity: number;
users: number;
identities: number;
}; };
export type Invoice = { export type Invoice = {
@@ -1,10 +1,10 @@
import { useEffect } from "react"; 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 { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { useQueryClient } from "@tanstack/react-query"; import { useQueryClient } from "@tanstack/react-query";
import { OrgPermissionCan } from "@app/components/permissions"; import { OrgPermissionCan } from "@app/components/permissions";
import { Button } from "@app/components/v2"; import { Button, Tooltip } from "@app/components/v2";
import { import {
OrgPermissionBillingActions, OrgPermissionBillingActions,
OrgPermissionSubjects, OrgPermissionSubjects,
@@ -27,6 +27,9 @@ export const PreviewSection = () => {
const { subscription } = useSubscription(true); const { subscription } = useSubscription(true);
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const { data, isPending } = useGetOrgPlanBillingInfo(currentOrg?.id ?? ""); const { data, isPending } = useGetOrgPlanBillingInfo(currentOrg?.id ?? "");
const totalAmount = data?.amount ? data.amount * (data.users + data.identities) : 0;
const getOrgTrialUrl = useGetOrgTrialUrl(); const getOrgTrialUrl = useGetOrgTrialUrl();
const createCustomerPortalSession = useCreateCustomerPortalSession(); const createCustomerPortalSession = useCreateCustomerPortalSession();
@@ -190,14 +193,28 @@ export const PreviewSection = () => {
</OrgPermissionCan> </OrgPermissionCan>
)} )}
</div> </div>
<div className="mr-4 flex-1 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4"> {subscription.slug !== "enterprise" ? (
<p className="mb-2 text-gray-400">Price</p> <div className="mr-4 flex-1 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
<p className="mb-8 text-2xl font-medium text-mineshaft-50"> <p className="mb-2 text-gray-400">Price</p>
{subscription.status === "trialing" <p className="mb-8 text-2xl font-medium text-mineshaft-50">
? "$0.00 / month" {subscription.status === "trialing" ? (
: `${formatAmount(data.amount)} / ${data.interval}`} "$0.00 / month"
</p> ) : (
</div> <>
{formatAmount(totalAmount)} / {data.interval}
{(subscription.slug === "pro" || subscription.slug === "pro-annual") && (
<Tooltip
content={`Total price is based on the number of users and machine identities at ${formatAmount(data.amount)} each. You have ${data.users} ${data.users > 1 ? "users" : "user"} and ${data.identities} ${data.identities > 1 ? "machine identities" : "machine identity"}.`}
className="max-w-lg"
>
<FontAwesomeIcon icon={faInfoCircle} className="ml-2" size="xs" />
</Tooltip>
)}
</>
)}
</p>
</div>
) : null}
<div className="flex-1 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4"> <div className="flex-1 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
<p className="mb-2 text-gray-400">Subscription renews on</p> <p className="mb-2 text-gray-400">Subscription renews on</p>
<p className="mb-8 text-2xl font-medium text-mineshaft-50"> <p className="mb-8 text-2xl font-medium text-mineshaft-50">