import React from 'react' import { Table, TableBody, TableCell, TableFooter, TableRow } from 'ui' import { InfoTooltip } from 'ui-patterns/info-tooltip' import { ShimmeringLoader } from 'ui-patterns/ShimmeringLoader' import { billingMetricUnit, formatUsage } from '../helpers' import AlertError from '@/components/ui/AlertError' import { InlineLink } from '@/components/ui/InlineLink' import { PricingMetric } from '@/data/analytics/org-daily-stats-query' import { UpcomingInvoiceResponse, useOrgUpcomingInvoiceQuery, } from '@/data/invoices/org-invoice-upcoming-query' import { useOrganizationQuery } from '@/data/organizations/organization-query' import { DOCS_URL } from '@/lib/constants' import { formatCurrency } from '@/lib/helpers' export interface UpcomingInvoiceProps { slug?: string } const usageBillingDocsLink: { [K in PricingMetric]?: string } = { [PricingMetric.MONTHLY_ACTIVE_USERS]: `${DOCS_URL}/guides/platform/manage-your-usage/monthly-active-users`, [PricingMetric.MONTHLY_ACTIVE_SSO_USERS]: `${DOCS_URL}/guides/platform/manage-your-usage/monthly-active-users-sso`, [PricingMetric.MONTHLY_ACTIVE_THIRD_PARTY_USERS]: `${DOCS_URL}/guides/platform/manage-your-usage/monthly-active-users-third-party`, [PricingMetric.AUTH_MFA_PHONE]: `${DOCS_URL}/guides/platform/manage-your-usage/advanced-mfa-phone`, [PricingMetric.EGRESS]: `${DOCS_URL}/guides/platform/manage-your-usage/egress`, [PricingMetric.CACHED_EGRESS]: `${DOCS_URL}/guides/platform/manage-your-usage/egress`, [PricingMetric.FUNCTION_INVOCATIONS]: `${DOCS_URL}/guides/platform/manage-your-usage/edge-function-invocations`, [PricingMetric.STORAGE_SIZE]: `${DOCS_URL}/guides/platform/manage-your-usage/storage-size`, [PricingMetric.STORAGE_IMAGES_TRANSFORMED]: `${DOCS_URL}/guides/platform/manage-your-usage/storage-image-transformations`, [PricingMetric.REALTIME_MESSAGE_COUNT]: `${DOCS_URL}/guides/platform/manage-your-usage/realtime-messages`, [PricingMetric.REALTIME_PEAK_CONNECTIONS]: `${DOCS_URL}/guides/platform/manage-your-usage/realtime-peak-connections`, [PricingMetric.CUSTOM_DOMAIN]: `${DOCS_URL}/guides/platform/manage-your-usage/custom-domains`, [PricingMetric.IPV4]: `${DOCS_URL}/guides/platform/manage-your-usage/ipv4`, [PricingMetric.PITR_7]: `${DOCS_URL}/guides/platform/manage-your-usage/point-in-time-recovery`, [PricingMetric.PITR_14]: `${DOCS_URL}/guides/platform/manage-your-usage/point-in-time-recovery`, [PricingMetric.PITR_28]: `${DOCS_URL}/guides/platform/manage-your-usage/point-in-time-recovery`, [PricingMetric.DISK_SIZE_GB_HOURS_GP3]: `${DOCS_URL}/guides/platform/manage-your-usage/disk-size`, [PricingMetric.DISK_SIZE_GB_HOURS_IO2]: `${DOCS_URL}/guides/platform/manage-your-usage/disk-size`, [PricingMetric.DISK_IOPS_GP3]: `${DOCS_URL}/guides/platform/manage-your-usage/disk-iops`, [PricingMetric.DISK_IOPS_IO2]: `${DOCS_URL}/guides/platform/manage-your-usage/disk-iops`, [PricingMetric.DISK_THROUGHPUT_GP3]: `${DOCS_URL}/guides/platform/manage-your-usage/disk-throughput`, [PricingMetric.LOG_DRAIN]: `${DOCS_URL}/guides/platform/manage-your-usage/log-drains`, } export const UpcomingInvoice = ({ slug }: UpcomingInvoiceProps) => { const { data: upcomingInvoice, error: error, isPending: isLoading, isError, isSuccess, } = useOrgUpcomingInvoiceQuery({ orgSlug: slug }) const { data: organization } = useOrganizationQuery({ slug }) // For non-platform customers, compute is broken down per project and contains a breakdown array const computeItems = upcomingInvoice?.lines?.filter( (item) => item.description?.toLowerCase().includes('compute') && !item.description.startsWith('Compute Credits') ) || [] const computeCreditsItem = upcomingInvoice?.lines?.find((item) => item.description.startsWith('Compute Credits')) ?? null const planItem = upcomingInvoice?.lines?.find((item) => item.description?.toLowerCase().includes('plan') ) 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 branchingComputeItemsDisplay = branchingComputeItems .flatMap((it) => it.breakdown) .sort((a, b) => (a?.project_name ?? '').localeCompare(b?.project_name ?? '')) const otherItems = upcomingInvoice?.lines ?.filter( (item) => !item.description?.toLowerCase().includes('compute') && !item.description?.toLowerCase().includes('plan') && item.amount_before_discount > 0 ) .sort((a, b) => b.amount_before_discount - a.amount_before_discount) || [] const prepaidCreditsItem = upcomingInvoice?.lines?.find((item) => item.item_name === 'Prepaid Credits') ?? null const hasTax = upcomingInvoice?.tax_status === 'calculated' && (upcomingInvoice?.tax?.tax_amount ?? 0) > 0 const taxFailed = upcomingInvoice?.tax_status === 'failed' const planFeePaidInAdvance = !planItem && upcomingInvoice?.fixed_fees_billing_mode === 'in_arrears' return ( <> {isLoading && (
)} {isError && } {isSuccess && (
{planItem && !planFeePaidInAdvance && ( {planItem?.description} {!planItem ? ( '-' ) : ( )} )} {/* Compute section */} The first project is covered by Compute Credits. Additional projects incur compute costs starting at $10/month, independent of activity. See{' '} docs .

} /> {/* 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 .

} /> {/* Branching compute */} {branchingComputeItems.length > 0 && (
Branching
    {branchingComputeItemsDisplay.map((breakdown) => (
  • {breakdown!.project_name} ({breakdown!.usage} Hours)
  • ))}

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) => { const usageMetric = item.usage_metric as PricingMetric const sortedBreakdown = (item.breakdown ?? []).sort((a, b) => a.project_name.localeCompare(b.project_name) ) return (
{item.description ?? 'Unknown'} {(sortedBreakdown.length > 0 || item.usage_metric !== null) && ( {item.item_name === 'minimum_amount' && (

Minimum Fee - If your cost is below the minimum fee, you will be charged the difference as a floor fee

)} {item.unit_price_desc && (

Pricing: {item.unit_price_desc}

)} {sortedBreakdown.length > 0 && ( <>

Projects using {item.description}:

    {sortedBreakdown.map((breakdown) => { const unit = billingMetricUnit(usageMetric) return (
  • {breakdown.project_name} {' '} {usageMetric && ( ({formatUsage(usageMetric, breakdown)} {!!unit ? ` ${unit}` : ''}) )}
  • ) })}
)} {usageMetric && usageBillingDocsLink[usageMetric] != null && (

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

)}
)}
) })}
{prepaidCreditsItem && ( {prepaidCreditsItem.item_name} Prepaid credits purchased upfront, applied automatically against your invoice. Any remaining balance rolls over to the next billing cycle. {formatCurrency(prepaidCreditsItem.amount) ?? '-'} )} Current Costs Costs accumulated from the beginning of the billing cycle up to now. {formatCurrency(upcomingInvoice?.amount_total) ?? '-'} {(!!upcomingInvoice.amount_projected || hasTax || taxFailed) && ( 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.

{hasTax && (
Subtotal {formatCurrency(upcomingInvoice.tax!.total_amount_excluding_tax)}
Tax {upcomingInvoice.tax?.tax_rate_percentage != null && ` (${upcomingInvoice.tax.tax_rate_percentage}%)`} {formatCurrency(upcomingInvoice.tax!.tax_amount)}

Estimated based on your organization's billing address. The final amount may be adjusted at the end of the billing cycle.

)} {taxFailed && (

We were unable to estimate tax for your organization. Please verify your billing address in your organization settings.

)}
{formatCurrency( hasTax ? upcomingInvoice.tax!.total_amount_including_tax : upcomingInvoice.amount_projected ) ?? '-'}
)} {planFeePaidInAdvance && ( Your {organization?.plan?.name && `${organization.plan.name} `}Plan fee for this period has already been paid. This invoice will only reflect usage charges. )}
)} ) } 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 computeProjects = 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 computeItemsSortedByCost = computeItems // descending by cost .sort((a, b) => b.amount - a.amount) const computeCosts = Math.max( 0, computeItems.reduce((prev, cur) => prev + cur.amount_before_discount, 0) ) const discountedComputeCosts = Math.max( 0, computeItems.reduce((prev, cur) => prev + (cur.amount ?? 0), 0) ) if (!computeItems.length) return null return ( <> {title} {tooltip} {computeProjects.map((project) => ( {project.project_name} ({project.computeType} - {project.usage} Hours) {formatCurrency(project.computeCosts)} ))} {/* Fallback to breakdown by instance size if project breakdown not available */} {!computeProjects.length && computeItemsSortedByCost.map((computeItem) => ( {computeItem.description} - {computeItem.usage_original} Hours {formatCurrency(computeItem.amount)} ))} {computeCredits && ( Compute Credits {formatCurrency(computeCredits.amount)} )} ) }