From 3ce7e400a1fc65ae54a62495f29c40df1bf6656d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kevin=20Gr=C3=BCneberg?= Date: Fri, 11 Apr 2025 12:40:59 +0800 Subject: [PATCH] feat: revamped invoice breakdown (#34861) - Read Replica and Branching Compute is now explicitly listed (previously Read Replica compute was not distinguishable) - All usage items have a link to their respective docs - Branching Compute is aggregated into a single line item to avoid listing dozens of branches (they are listed in the tooltip still) - Displays basic discounts with a strike-through price - For Replica+Compute, it specifically lists the projects as sub line items, whereas other line items are not broken down, it's shown on the tooltip - Compute credits are a sub line item under compute, rather than having it separate - Pricing is displayed on the tooltip - Breakdown focuses on costs, rather than mixing cost, usage and line item pricing descriptions - Avoid confusing users in the breakdown with hourly usage (for add-ons) - Fixed number formatting to properly work with negative number and 0 (use 2 decimals) - Upcoming Invoice Section is hidden for Free Plan users --- .../content/guides/platform/billing-faq.mdx | 2 +- .../content/guides/platform/cost-control.mdx | 2 +- .../BillingBreakdown/BillingBreakdown.tsx | 51 +- .../BillingBreakdown/UpcomingInvoice.tsx | 608 ++++++++++-------- .../BillingSettings/BillingSettings.tsx | 10 +- .../CostControl/CostControl.tsx | 29 +- .../Subscription/Subscription.tsx | 52 +- .../interfaces/Organization/Usage/Usage.tsx | 25 +- .../invoices/org-invoice-upcoming-query.ts | 9 +- apps/studio/lib/helpers.ts | 2 +- 10 files changed, 437 insertions(+), 353 deletions(-) diff --git a/apps/docs/content/guides/platform/billing-faq.mdx b/apps/docs/content/guides/platform/billing-faq.mdx index 68640711a88..0f6e77226fb 100644 --- a/apps/docs/content/guides/platform/billing-faq.mdx +++ b/apps/docs/content/guides/platform/billing-faq.mdx @@ -129,7 +129,7 @@ The Fair Use Policy is generally applied to all projects of the restricted organ To remove restrictions, you will need to address the issue that caused the restriction. This could be reducing your usage, paying overdue invoices, updating your payment method, or any other issue that caused the restriction. Once the issue is resolved, the restriction will be lifted. -Restrictions due to usage limits are lifted with the next billing cycle as your quota refills at the beginning of each cycle. You can see when your current billing cycle ends on the [billing page](https://supabase.com/dashboard/org/_/usage) under "Billing Breakdown". If your organization is on the Free Plan, you can also lift restrictions immediately by [upgrading](https://supabase.com/dashboard/org/_/billing?panel=subscriptionPlan) to Pro. +Restrictions due to usage limits are lifted with the next billing cycle as your quota refills at the beginning of each cycle. You can see when your current billing cycle ends on the [billing page](https://supabase.com/dashboard/org/_/billing) under "Upcoming Invoice". If your organization is on the Free Plan, you can also lift restrictions immediately by [upgrading](https://supabase.com/dashboard/org/_/billing?panel=subscriptionPlan) to Pro. ## Reports and invoices diff --git a/apps/docs/content/guides/platform/cost-control.mdx b/apps/docs/content/guides/platform/cost-control.mdx index b490855e98c..52f10d07913 100644 --- a/apps/docs/content/guides/platform/cost-control.mdx +++ b/apps/docs/content/guides/platform/cost-control.mdx @@ -65,4 +65,4 @@ You can configure the Spend Cap when creating an organization on the Pro Plan or ## Keep track of your usage and costs -You can monitor your usage on the [organization's usage page](https://supabase.com/dashboard/org/_/usage). The Billing Breakdown section of the [organization's billing page](https://supabase.com/dashboard/org/_/billing) shows your current spending and provides an estimate of your total costs for the billing cycle based on your usage. +You can monitor your usage on the [organization's usage page](https://supabase.com/dashboard/org/_/usage). The Upcoming Invoice section of the [organization's billing page](https://supabase.com/dashboard/org/_/billing) shows your current spending and provides an estimate of your total costs for the billing cycle based on your usage. diff --git a/apps/studio/components/interfaces/Organization/BillingSettings/BillingBreakdown/BillingBreakdown.tsx b/apps/studio/components/interfaces/Organization/BillingSettings/BillingBreakdown/BillingBreakdown.tsx index cb0489e80ba..89db61d64a1 100644 --- a/apps/studio/components/interfaces/Organization/BillingSettings/BillingBreakdown/BillingBreakdown.tsx +++ b/apps/studio/components/interfaces/Organization/BillingSettings/BillingBreakdown/BillingBreakdown.tsx @@ -15,6 +15,7 @@ import { useOrgSubscriptionQuery } from 'data/subscriptions/org-subscription-que import { useCheckPermissions } from 'hooks/misc/useCheckPermissions' import { useIsFeatureEnabled } from 'hooks/misc/useIsFeatureEnabled' import UpcomingInvoice from './UpcomingInvoice' +import SparkBar from 'components/ui/SparkBar' const BillingBreakdown = () => { const { slug: orgSlug } = useParams() @@ -36,23 +37,45 @@ const BillingBreakdown = () => { const billingCycleStart = dayjs.unix(subscription?.current_period_start ?? 0).utc() const billingCycleEnd = dayjs.unix(subscription?.current_period_end ?? 0).utc() + const daysToCycleEnd = billingCycleEnd.diff(dayjs(), 'days') + const daysWithinCycle = billingCycleEnd.diff(billingCycleStart, 'days') + return (
-

Billing Breakdown

-

- Current billing cycle: {billingCycleStart.format('MMM DD')} -{' '} - {billingCycleEnd.format('MMM DD')} +

Upcoming Invoice

+
+ +
+

+ Your upcoming invoice (excluding credits) will continue to update until the end of your + billing cycle on {billingCycleEnd.format('MMMM DD')}. For a more detailed breakdown, + visit the usage page.

-

- It may take up to an hour for addon changes or new projects to show up. +
+

+ Add-on changes or new projects may take up to an hour to appear.

{!canReadSubscriptions ? ( - + ) : ( <> {isLoadingSubscription && ( @@ -67,19 +90,7 @@ const BillingBreakdown = () => { )} - {invoiceFeatureEnabled && ( - <> -

- The table shows your upcoming invoice, excluding credits. This invoice will - continue updating until the end of your billing period on{' '} - {billingCycleEnd.format('MMMM DD')}. See{' '} - usage page for a more detailed usage - breakdown. -

- - - - )} + {invoiceFeatureEnabled && } )}
diff --git a/apps/studio/components/interfaces/Organization/BillingSettings/BillingBreakdown/UpcomingInvoice.tsx b/apps/studio/components/interfaces/Organization/BillingSettings/BillingBreakdown/UpcomingInvoice.tsx index 7f5de33cc79..39e0c66037f 100644 --- a/apps/studio/components/interfaces/Organization/BillingSettings/BillingBreakdown/UpcomingInvoice.tsx +++ b/apps/studio/components/interfaces/Organization/BillingSettings/BillingBreakdown/UpcomingInvoice.tsx @@ -1,31 +1,58 @@ -import { ChevronRight, Info } from 'lucide-react' import Link from 'next/link' -import { useMemo, useState } from 'react' import AlertError from 'components/ui/AlertError' import ShimmeringLoader from 'components/ui/ShimmeringLoader' -import { useOrgUpcomingInvoiceQuery } from 'data/invoices/org-invoice-upcoming-query' +import { + UpcomingInvoiceResponse, + useOrgUpcomingInvoiceQuery, +} from 'data/invoices/org-invoice-upcoming-query' import { formatCurrency } from 'lib/helpers' -import { Button, cn, Collapsible, Tooltip, TooltipContent, TooltipTrigger } from 'ui' +import { Table, TableBody, TableCell, TableFooter, TableRow } from 'ui' import { billingMetricUnit, formatUsage } from '../helpers' +import { InfoTooltip } from 'ui-patterns/info-tooltip' +import { PricingMetric } from 'data/analytics/org-daily-stats-query' +import _ from 'lodash' +import React from 'react' export interface UpcomingInvoiceProps { slug?: string } -interface TooltipData { - identifier: string - text: string - linkRef?: string -} +const usageBillingDocsLink: { [K in PricingMetric]?: string } = { + [PricingMetric.MONTHLY_ACTIVE_USERS]: + '/docs/guides/platform/manage-your-usage/monthly-active-users', + [PricingMetric.MONTHLY_ACTIVE_SSO_USERS]: + '/docs/guides/platform/manage-your-usage/monthly-active-users-sso', + [PricingMetric.MONTHLY_ACTIVE_THIRD_PARTY_USERS]: + '/docs/guides/platform/manage-your-usage/monthly-active-users-third-party', + [PricingMetric.AUTH_MFA_PHONE]: '/docs/guides/platform/manage-your-usage/advanced-mfa-phone', -const feeTooltipData: TooltipData[] = [ - { - identifier: 'COMPUTE', - text: 'Every project is a dedicated server and database. For every hour your project is active, it incurs compute costs based on the compute size of your project. Paused projects do not incur compute costs.', - linkRef: 'https://supabase.com/docs/guides/platform/manage-your-usage/compute', - }, -] + [PricingMetric.EGRESS]: '/docs/guides/platform/manage-your-usage/egress', + + [PricingMetric.FUNCTION_INVOCATIONS]: + '/docs/guides/platform/manage-your-usage/edge-function-invocations', + + [PricingMetric.STORAGE_SIZE]: '/docs/guides/platform/manage-your-usage/storage-size', + [PricingMetric.STORAGE_IMAGES_TRANSFORMED]: + '/docs/guides/platform/manage-your-usage/storage-image-transformations', + + [PricingMetric.REALTIME_MESSAGE_COUNT]: + '/docs/guides/platform/manage-your-usage/realtime-messages', + [PricingMetric.REALTIME_PEAK_CONNECTIONS]: + '/docs/guides/platform/manage-your-usage/realtime-peak-connections', + + [PricingMetric.CUSTOM_DOMAIN]: '/docs/guides/platform/manage-your-usage/custom-domains', + [PricingMetric.IPV4]: '/docs/guides/platform/manage-your-usage/ipv4', + [PricingMetric.PITR_7]: '/docs/guides/platform/manage-your-usage/point-in-time-recovery', + [PricingMetric.PITR_14]: '/docs/guides/platform/manage-your-usage/point-in-time-recovery', + [PricingMetric.PITR_28]: '/docs/guides/platform/manage-your-usage/point-in-time-recovery', + [PricingMetric.DISK_SIZE_GB_HOURS_GP3]: '/docs/guides/platform/manage-your-usage/disk-size', + [PricingMetric.DISK_SIZE_GB_HOURS_IO2]: '/docs/guides/platform/manage-your-usage/disk-size', + [PricingMetric.DISK_IOPS_GP3]: '/docs/guides/platform/manage-your-usage/disk-iops', + [PricingMetric.DISK_IOPS_IO2]: '/docs/guides/platform/manage-your-usage/disk-iops', + [PricingMetric.DISK_THROUGHPUT_GP3]: '/docs/guides/platform/manage-your-usage/disk-throughput', + [PricingMetric.LOG_DRAIN]: '/docs/guides/platform/manage-your-usage/log-drains', +} const UpcomingInvoice = ({ slug }: UpcomingInvoiceProps) => { const { @@ -36,48 +63,33 @@ const UpcomingInvoice = ({ slug }: UpcomingInvoiceProps) => { isSuccess, } = useOrgUpcomingInvoiceQuery({ orgSlug: slug }) - const [usageFeesExpanded, setUsageFeesExpanded] = useState([]) + const computeItems = + upcomingInvoice?.lines?.filter( + (item) => + item.description?.toLowerCase().includes('compute') && + item.breakdown && + item.breakdown?.length > 0 + ) || [] - const computeCredits = useMemo(() => { - return (upcomingInvoice?.lines || []).find((item) => - item.description.startsWith('Compute Credits') - ) - }, [upcomingInvoice]) + const computeCreditsItem = + upcomingInvoice?.lines?.find((item) => item.description.startsWith('Compute Credits')) ?? null - const fixedFees = useMemo(() => { - return (upcomingInvoice?.lines || []) - .filter((item) => item !== computeCredits && (!item.breakdown || !item.breakdown.length)) - .sort((a, b) => { - // Prorations should be below regular usage fees - return Number(a.proration) - Number(b.proration) - }) - }, [upcomingInvoice, computeCredits]) + const planItem = upcomingInvoice?.lines?.find((item) => + item.description?.toLowerCase().includes('plan') + ) - const feesWithBreakdown = useMemo(() => { - return (upcomingInvoice?.lines || []) - .filter((item) => item.breakdown?.length) - .sort((a, b) => Number(a.usage_based) - Number(b.usage_based) || b.amount - a.amount) - }, [upcomingInvoice]) + const regularComputeItems = computeItems.filter( + (it) => !it.metadata?.is_branch && !it.metadata?.is_read_replica + ) + const branchingComputeItems = computeItems.filter((it) => it.metadata?.is_branch) + const replicaComputeItems = computeItems.filter((it) => it.metadata?.is_read_replica) - const expandUsageFee = (fee: string) => { - setUsageFeesExpanded([...usageFeesExpanded, fee]) - } - - const collapseUsageFee = (fee: string) => { - setUsageFeesExpanded(usageFeesExpanded.filter((item) => item !== fee)) - } - - const allFeesExpanded = useMemo(() => { - return feesWithBreakdown.length === usageFeesExpanded.length - }, [feesWithBreakdown, usageFeesExpanded]) - - const toggleAllFees = () => { - if (allFeesExpanded) { - setUsageFeesExpanded([]) - } else { - setUsageFeesExpanded(feesWithBreakdown.map((item) => item.description)) - } - } + const otherItems = + upcomingInvoice?.lines?.filter( + (item) => + !item.description?.toLowerCase().includes('compute') && + !item.description?.toLowerCase().includes('plan') + ) || [] return ( <> @@ -93,218 +105,306 @@ const UpcomingInvoice = ({ slug }: UpcomingInvoiceProps) => { {isSuccess && (
- {feesWithBreakdown.length > 0 && ( -
- -
- )} +
+ + + + {planItem?.description} + + {planItem == null ? ( + '-' + ) : ( + + )} + + -
- - - - - - - - - - {fixedFees.map((item) => ( - - - {!item.proration && ( - - )} - {!item.proration && ( - - )} - - - ))} - - - {feesWithBreakdown.length > 0 && - feesWithBreakdown.map((fee) => ( - - open ? expandUsageFee(fee.description) : collapseUsageFee(fee.description) - } - key={fee.description} - > - - - + The first project is covered by Compute Credits. Additional projects incur + compute costs starting at $10/month, independent of activity. See{' '} + - - - - - - + docs + + . +

+ } + /> - - <> - {fee.breakdown?.map((breakdown) => ( - - - - - ))} - - - - - ))} + {/* Read Replica compute */} + + Each Read Replica is a dedicated database. You are charged for its resources: + Compute, Disk Size, provisioned Disk IOPS, provisioned Disk Throughput, and + IPv4. See{' '} + + docs + + . +

+ } + /> - {computeCredits && ( -
- - - - - - )} + {/* Branching compute */} + {branchingComputeItems.length > 0 && ( + + +
+ Branching + +
    + {branchingComputeItems + .flatMap((it) => it.breakdown) + .map((breakdown) => ( +
  • + {breakdown!.project_name} ({breakdown!.usage} Hours) +
  • + ))} +
-
- - - - - - - - - -
- Item - - Usage - - Unit price - Cost
- {item.description ?? 'Unknown'} - {item.usage_metric && - billingMetricUnit(item.usage_metric) && - ` (${billingMetricUnit(item.usage_metric)})`} - - {item.quantity?.toLocaleString()} - - {item.unit_price === 0 - ? 'FREE' - : item.unit_price - ? formatCurrency(item.unit_price) - : `${item.unit_price_desc}`} - {formatCurrency(item.amount)}
- - {fee.usage_original - ? `${formatUsage(fee.usage_metric!, { usage: fee.usage_original })}` - : fee.quantity - ? fee.quantity - : null} - - {fee.unit_price_desc - ? `${fee.unit_price_desc}` - : fee.unit_price - ? formatCurrency(fee.unit_price) - : null} - - {formatCurrency(fee.amount) ?? formatCurrency(0)} -
- {breakdown.project_name} - - {formatUsage(fee.usage_metric!, breakdown)} - - -
- {computeCredits.description} - - - {formatCurrency(computeCredits.amount)} -
- Current Costs - - - {formatCurrency(upcomingInvoice?.amount_total) ?? '-'} -
- Projected Costs - - - {formatCurrency(upcomingInvoice?.amount_projected) ?? '-'} -
+

+ See{' '} + + docs + {' '} + on how billing for Branching works. +

+ +
+ + + prev + cur.amount, 0)} + amountBeforeDiscount={branchingComputeItems.reduce( + (prev, cur) => prev + (cur.amount_before_discount ?? 0), + 0 + )} + /> + + + )} + + {/* Non-compute items */} + {otherItems.map((item) => ( + + +
+ {item.description ?? 'Unknown'} + {((item.breakdown && item.breakdown.length > 0) || + item.usage_metric != null) && ( + + {item.unit_price_desc && ( +

Pricing: {item.unit_price_desc}

+ )} + + {item.breakdown && item.breakdown.length > 0 && ( + <> +

Projects using {item.description}:

+
    + {item.breakdown.map((breakdown) => ( +
  • + + {breakdown.project_name} + {' '} + {item.usage_metric && ( + + ({formatUsage(item.usage_metric, breakdown)}{' '} + {billingMetricUnit(item.usage_metric)}) + + )} +
  • + ))} +
+ + )} + + {item.usage_metric && + usageBillingDocsLink[item.usage_metric] != null && ( +

+ See{' '} + + docs + {' '} + on how billing for {item.description} works and{' '} + + usage page + {' '} + for a detailed breakdown. +

+ )} +
+ )} +
+
+ + + +
+ ))} + + + + + + Current Costs + + Costs accumulated from the beginning of the billing cycle up to now. + + + + {formatCurrency(upcomingInvoice?.amount_total) ?? '-'} + + + + + Projected Costs + + Projected costs at the end of the billing cycle. Includes predictable costs + for Compute Hours, IPv4, Custom Domain and Point-In-Time-Recovery, but no + costs for metrics like MAU, storage or function invocations. Final amounts may + vary depending on your usage. + + + + {formatCurrency(upcomingInvoice?.amount_projected) ?? '-'} + + + + +
)} ) } -const InvoiceTooltip = ({ text, linkRef }: { text: string; linkRef?: string }) => { +function InvoiceLineItemAmount({ + amountBeforeDiscount, + amount, +}: { + amountBeforeDiscount?: number + amount: number +}) { + if (amountBeforeDiscount && amount < amountBeforeDiscount) { + return ( +
+ + {formatCurrency(amountBeforeDiscount)} + + {formatCurrency(amount)} +
+ ) + } else { + return formatCurrency(amount) + } +} + +function ComputeLineItem({ + computeItems, + tooltip, + title, + computeCredits, +}: { + title: string + tooltip: React.ReactElement + computeItems: UpcomingInvoiceResponse['lines'] + computeCredits?: UpcomingInvoiceResponse['lines'][number] | null +}) { + const replicaProjects = computeItems + .flatMap((item) => + item.breakdown!.map((project) => ({ + ...project, + computeType: item.description, + computeCosts: project.amount!, + })) + ) + // descending by cost + .sort((a, b) => b.computeCosts - a.computeCosts) + + const replicaComputeCosts = + Math.max( + 0, + computeItems.reduce((prev, cur) => prev + cur.amount, 0) + ) + (computeCredits?.amount ?? 0) + + const discountedReplicaComputeCosts = computeItems.reduce( + (prev, cur) => prev + (cur.amount_before_discount ?? 0), + 0 + ) + + if (!computeItems.length) return null + return ( - - - - - -

- {text}{' '} - {linkRef && ( - - Read more - - )} -

-
-
+ <> + + + {title} + {tooltip} + + + + + + {replicaProjects.map((project) => ( + + + {project.project_name} ({project.computeType} - {project.usage} Hours) + + + + {formatCurrency(project.computeCosts)} + + + ))} + + {computeCredits && ( + + Compute Credits + + {formatCurrency(computeCredits.amount)} + + + )} + ) } diff --git a/apps/studio/components/interfaces/Organization/BillingSettings/BillingSettings.tsx b/apps/studio/components/interfaces/Organization/BillingSettings/BillingSettings.tsx index 1cb8cfc1e4c..30fc4fe09a2 100644 --- a/apps/studio/components/interfaces/Organization/BillingSettings/BillingSettings.tsx +++ b/apps/studio/components/interfaces/Organization/BillingSettings/BillingSettings.tsx @@ -20,7 +20,7 @@ const BillingSettings = () => { const { slug: orgSlug } = useParams() const { data: subscription } = useOrgSubscriptionQuery({ orgSlug }) - const isNotOrgWithPartnerBilling = !subscription?.billing_via_partner ?? true + const isNotOrgWithPartnerBilling = !subscription?.billing_via_partner const billingAccountDataEnabled = isBillingAccountDataEnabledOnProfileLevel && isNotOrgWithPartnerBilling @@ -41,9 +41,11 @@ const BillingSettings = () => { - - - + {subscription?.plan.id !== 'free' && ( + + + + )} {isBillingCreditsEnabledOnProfileLevel && ( <> diff --git a/apps/studio/components/interfaces/Organization/BillingSettings/CostControl/CostControl.tsx b/apps/studio/components/interfaces/Organization/BillingSettings/CostControl/CostControl.tsx index be8b10ff81c..4b63791d318 100644 --- a/apps/studio/components/interfaces/Organization/BillingSettings/CostControl/CostControl.tsx +++ b/apps/studio/components/interfaces/Organization/BillingSettings/CostControl/CostControl.tsx @@ -57,7 +57,14 @@ const CostControl = ({}: CostControlProps) => {

Cost Control

- Control whether to use beyond your plans included quota + Allow scaling beyond your plan's{' '} + + included quota + + .

@@ -114,16 +121,8 @@ const CostControl = ({}: CostControlProps) => { ) : (

- You can control whether your organization is charged for additional usage - beyond the{' '} - - included quota - {' '} - of your subscription plan. If you need to go beyond the included quota, simply - switch off your spend cap to pay for additional usage. + If you need to go beyond the included quota, simply switch off your spend cap + to pay for additional usage.

)} @@ -152,7 +151,7 @@ const CostControl = ({}: CostControlProps) => {

{isUsageBillingEnabled ? ( - You will be charged for any usage above the included quota. + You will be charged for usage beyond the included quota. ) : ( You won't be charged any extra for usage. However, your projects could @@ -161,12 +160,6 @@ const CostControl = ({}: CostControlProps) => { )}

- {isUsageBillingEnabled && ( -

- Your projects will never become unresponsive. Only when your usage reaches - the quota limit will you be charged for any excess usage. -

- )}
-

Subscription Plan

-
-

More information

-
- -
-

Pricing

- -
- -
+
+

Subscription Plan

+

+ Each organization has it's own subscription plan, billing cycle, payment methods and + usage quotas. +

@@ -90,10 +81,7 @@ const Subscription = () => { {isSuccess && (
-

This organization is currently on the plan:

-

- {currentPlan?.name ?? 'Unknown'} -

+

{currentPlan?.name ?? 'Unknown'} Plan

@@ -149,37 +137,13 @@ const Subscription = () => {
Projects may become unresponsive when this organization exceeds its{' '} included usage quota. To scale - seamlessly and pay for over-usage,{' '} + seamlessly,{' '} {currentPlan?.id === 'free' ? 'upgrade to a paid plan.' : 'you can disable Spend Cap under the Cost Control settings.'}
- )} - -
)} diff --git a/apps/studio/components/interfaces/Organization/Usage/Usage.tsx b/apps/studio/components/interfaces/Organization/Usage/Usage.tsx index 4dda95390e2..9729e8fed47 100644 --- a/apps/studio/components/interfaces/Organization/Usage/Usage.tsx +++ b/apps/studio/components/interfaces/Organization/Usage/Usage.tsx @@ -28,6 +28,7 @@ import Bandwidth from './Bandwidth' import Compute from './Compute' import SizeAndCounts from './SizeAndCounts' import TotalUsage from './TotalUsage' +import Link from 'next/link' const Usage = () => { const newLayoutPreview = useNewLayout() @@ -199,15 +200,21 @@ const Usage = () => { - - + description={ +
+ You are currently viewing usage for the "$ + {selectedProject?.name || selectedProjectRef}" project. Supabase uses{' '} + + organization-level billing + {' '} + and quotas. For billing purposes, we sum up usage from all your projects. To view + your usage quota, set the project filter above back to "All Projects". +
+ } + /> ) : ( diff --git a/apps/studio/data/invoices/org-invoice-upcoming-query.ts b/apps/studio/data/invoices/org-invoice-upcoming-query.ts index b92a7f6eb87..0a2d42cfb9a 100644 --- a/apps/studio/data/invoices/org-invoice-upcoming-query.ts +++ b/apps/studio/data/invoices/org-invoice-upcoming-query.ts @@ -18,6 +18,7 @@ export type UpcomingInvoiceResponse = { billing_cycle_start: string lines: { amount: number + amount_before_discount: number description: string proration: boolean period: { start: string; end: string } @@ -27,11 +28,16 @@ export type UpcomingInvoiceResponse = { usage_based: boolean usage_metric?: PricingMetric usage_original?: number - breakdown: { + breakdown?: { project_ref: string project_name: string usage: number + amount?: number }[] + metadata?: { + is_branch: boolean + is_read_replica: boolean + } }[] } @@ -50,6 +56,7 @@ export async function getUpcomingInvoice( }) if (error) handleError(error) + return data as unknown as UpcomingInvoiceResponse } diff --git a/apps/studio/lib/helpers.ts b/apps/studio/lib/helpers.ts index 424be253d6f..792c46bd043 100644 --- a/apps/studio/lib/helpers.ts +++ b/apps/studio/lib/helpers.ts @@ -292,7 +292,7 @@ const currencyFormatterSmallValues = Intl.NumberFormat('en-US', { export const formatCurrency = (amount: number | undefined | null): string | null => { if (amount === undefined || amount === null) { return null - } else if (amount < 0.01) { + } else if (amount > 0 && amount < 0.01) { return currencyFormatterSmallValues.format(amount) } else { return currencyFormatterDefault.format(amount)