Billing licenses working

This commit is contained in:
Owen
2026-02-04 18:15:46 -08:00
parent 72d46b7352
commit 48dd4d5913
5 changed files with 139 additions and 47 deletions

View File

@@ -1436,6 +1436,15 @@
"billingUsersInfo": "You're charged for each user in the organization. Billing is calculated daily based on the number of active user accounts in your org.",
"billingDomainInfo": "You're charged for each domain in the organization. Billing is calculated daily based on the number of active domain accounts in your org.",
"billingRemoteExitNodesInfo": "You're charged for each managed Node in the organization. Billing is calculated daily based on the number of active managed Nodes in your org.",
"billingLicenseKeys": "License Keys",
"billingLicenseKeysDescription": "Manage your license key subscriptions",
"billingLicenseSubscription": "License Subscription",
"billingInactive": "Inactive",
"billingLicenseItem": "License Item",
"billingQuantity": "Quantity",
"billingTotal": "total",
"billingModifyLicenses": "Modify License Subscription",
"billingPricingCalculatorLink": "View Pricing Calculator",
"domainNotFound": "Domain Not Found",
"domainNotFoundDescription": "This resource is disabled because the domain no longer exists our system. Please set a new domain for this resource.",
"failed": "Failed",

View File

@@ -37,17 +37,6 @@ const getOrgSchema = z.strictObject({
orgId: z.string()
});
registry.registerPath({
method: "get",
path: "/org/{orgId}/billing/subscription",
description: "Get an organization",
tags: [OpenAPITags.Org],
request: {
params: getOrgSchema
},
responses: {}
});
export async function getOrgSubscriptions(
req: Request,
res: Response,

View File

@@ -86,7 +86,7 @@ export default async function OrgLayout(props: {
try {
const getSubscription = cache(() =>
internal.get<AxiosResponse<GetOrgSubscriptionResponse>>(
`/org/${orgId}/billing/subscription`,
`/org/${orgId}/billing/subscriptions`,
cookie
)
);

View File

@@ -43,15 +43,18 @@ import Link from "next/link";
export default function GeneralPage() {
const { org } = useOrgContext();
const api = createApiClient(useEnvContext());
const envContext = useEnvContext();
const api = createApiClient(envContext);
const t = useTranslations();
// Subscription state
const [subscription, setSubscription] =
useState<GetOrgSubscriptionResponse["subscription"]>(null);
const [subscriptionItems, setSubscriptionItems] = useState<
GetOrgSubscriptionResponse["items"]
// Subscription state - now handling multiple subscriptions
const [allSubscriptions, setAllSubscriptions] = useState<
GetOrgSubscriptionResponse["subscriptions"]
>([]);
const [tierSubscription, setTierSubscription] =
useState<GetOrgSubscriptionResponse["subscriptions"][0] | null>(null);
const [licenseSubscription, setLicenseSubscription] =
useState<GetOrgSubscriptionResponse["subscriptions"][0] | null>(null);
const [subscriptionLoading, setSubscriptionLoading] = useState(true);
// Example usage data (replace with real usage data if available)
@@ -68,12 +71,41 @@ export default function GeneralPage() {
try {
const res = await api.get<
AxiosResponse<GetOrgSubscriptionResponse>
>(`/org/${org.org.orgId}/billing/subscription`);
const { subscription, items } = res.data.data;
setSubscription(subscription);
setSubscriptionItems(items);
>(`/org/${org.org.orgId}/billing/subscriptions`);
const { subscriptions } = res.data.data;
setAllSubscriptions(subscriptions);
// Import tier and license price sets
const { getTierPriceSet } = await import("@server/lib/billing/tiers");
const { getLicensePriceSet } = await import("@server/lib/billing/licenses");
const tierPriceSet = getTierPriceSet(
envContext.env.app.environment,
envContext.env.app.sandbox_mode
);
const licensePriceSet = getLicensePriceSet(
envContext.env.app.environment,
envContext.env.app.sandbox_mode
);
// Find tier subscription (subscription with items matching tier prices)
const tierSub = subscriptions.find(({ items }) =>
items.some((item) =>
item.priceId && Object.values(tierPriceSet).includes(item.priceId)
)
);
setTierSubscription(tierSub || null);
// Find license subscription (subscription with items matching license prices)
const licenseSub = subscriptions.find(({ items }) =>
items.some((item) =>
item.priceId && Object.values(licensePriceSet).includes(item.priceId)
)
);
setLicenseSubscription(licenseSub || null);
setHasSubscription(
!!subscription && subscription.status === "active"
!!tierSub?.subscription && tierSub.subscription.status === "active"
);
} catch (error) {
toast({
@@ -302,6 +334,10 @@ export default function GeneralPage() {
return { usage: usage ?? 0, item, limit };
}
// Get tier subscription items
const tierSubscriptionItems = tierSubscription?.items || [];
const tierSubscriptionData = tierSubscription?.subscription || null;
// Helper to check if usage exceeds limit
function isOverLimit(usage: any, limit: any, usageType: any) {
if (!limit || !usage) return false;
@@ -388,15 +424,15 @@ export default function GeneralPage() {
<div className="flex items-center justify-between mb-6">
<Badge
variant={
subscription?.status === "active" ? "green" : "outline"
tierSubscriptionData?.status === "active" ? "green" : "outline"
}
>
{subscription?.status === "active" && (
{tierSubscriptionData?.status === "active" && (
<CheckCircle className="h-3 w-3 mr-1" />
)}
{subscription
? subscription.status.charAt(0).toUpperCase() +
subscription.status.slice(1)
{tierSubscriptionData
? tierSubscriptionData.status.charAt(0).toUpperCase() +
tierSubscriptionData.status.slice(1)
: t("billingFreeTier")}
</Badge>
<Link
@@ -413,7 +449,7 @@ export default function GeneralPage() {
{usageTypes.some((type) => {
const { usage, limit } = getUsageItemAndLimit(
usageData,
subscriptionItems,
tierSubscriptionItems,
limitsData,
type.id
);
@@ -441,7 +477,7 @@ export default function GeneralPage() {
{usageTypes.map((type) => {
const { usage, limit } = getUsageItemAndLimit(
usageData,
subscriptionItems,
tierSubscriptionItems,
limitsData,
type.id
);
@@ -530,7 +566,7 @@ export default function GeneralPage() {
{usageTypes.map((type) => {
const { item, limit } = getUsageItemAndLimit(
usageData,
subscriptionItems,
tierSubscriptionItems,
limitsData,
type.id
);
@@ -614,7 +650,7 @@ export default function GeneralPage() {
const { usage, item } =
getUsageItemAndLimit(
usageData,
subscriptionItems,
tierSubscriptionItems,
limitsData,
type.id
);
@@ -636,7 +672,7 @@ export default function GeneralPage() {
);
})}
{/* Show recurring charges (items with unitAmount but no tiers/meterId) */}
{subscriptionItems
{tierSubscriptionItems
.filter(
(item) =>
item.unitAmount &&
@@ -672,7 +708,7 @@ export default function GeneralPage() {
const { usage, item } =
getUsageItemAndLimit(
usageData,
subscriptionItems,
tierSubscriptionItems,
limitsData,
type.id
);
@@ -687,7 +723,7 @@ export default function GeneralPage() {
return sum + cost;
}, 0) +
// Add recurring charges
subscriptionItems
tierSubscriptionItems
.filter(
(item) =>
item.unitAmount &&
@@ -749,6 +785,56 @@ export default function GeneralPage() {
</SettingsSectionBody>
</SettingsSection>
)}
{/* License Keys Section */}
{licenseSubscription && (
<SettingsSection>
<SettingsSectionHeader>
<SettingsSectionTitle>
{t("billingLicenseKeys") || "License Keys"}
</SettingsSectionTitle>
<SettingsSectionDescription>
{t("billingLicenseKeysDescription") || "Manage your license key subscriptions"}
</SettingsSectionDescription>
</SettingsSectionHeader>
<SettingsSectionBody>
<div className="flex items-center justify-between p-4 border rounded-lg">
<div className="flex items-center gap-2">
<CreditCard className="h-5 w-5 text-primary" />
<span className="font-semibold">
{t("billingLicenseSubscription") || "License Subscription"}
</span>
</div>
<Badge
variant={
licenseSubscription.subscription?.status === "active"
? "green"
: "outline"
}
>
{licenseSubscription.subscription?.status === "active" && (
<CheckCircle className="h-3 w-3 mr-1" />
)}
{licenseSubscription.subscription?.status
? licenseSubscription.subscription.status
.charAt(0)
.toUpperCase() +
licenseSubscription.subscription.status.slice(1)
: t("billingInactive") || "Inactive"}
</Badge>
</div>
<SettingsSectionFooter>
<Button
variant="secondary"
onClick={() => handleModifySubscription()}
disabled={isLoading}
>
{t("billingModifyLicenses") || "Modify License Subscription"}
</Button>
</SettingsSectionFooter>
</SettingsSectionBody>
</SettingsSection>
)}
</SettingsContainer>
);
}

View File

@@ -33,8 +33,11 @@ export function SubscriptionStatusProvider({
};
const isActive = () => {
if (subscriptionStatus?.subscription?.status === "active") {
return true;
if (subscriptionStatus?.subscriptions) {
// Check if any subscription is active
return subscriptionStatus.subscriptions.some(
(sub) => sub.subscription?.status === "active"
);
}
return false;
};
@@ -42,11 +45,14 @@ export function SubscriptionStatusProvider({
const getTier = () => {
const tierPriceSet = getTierPriceSet(env, sandbox_mode);
if (subscriptionStatus?.items && subscriptionStatus.items.length > 0) {
if (subscriptionStatus?.subscriptions) {
// Iterate through all subscriptions
for (const { subscription, items } of subscriptionStatus.subscriptions) {
if (items && items.length > 0) {
// Iterate through tiers in order (earlier keys are higher tiers)
for (const [tierId, priceId] of Object.entries(tierPriceSet)) {
// Check if any subscription item matches this tier's price ID
const matchingItem = subscriptionStatus.items.find(
const matchingItem = items.find(
(item) => item.priceId === priceId
);
if (matchingItem) {
@@ -54,6 +60,8 @@ export function SubscriptionStatusProvider({
}
}
}
}
}
return null;
};