mirror of
https://github.com/supabase/supabase.git
synced 2026-10-06 01:45:10 +03:00
chore: remove unused billing page/queries (#19412)
This commit is contained in:
1 parent
c43f0e74a3
commit
598c1ef866
7 files changed
-453
No files matched your search
@@ -1,159 +0,0 @@
|
||||
import { useParams } from 'common'
|
||||
import Link from 'next/link'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { Button, IconChevronLeft, IconChevronRight, IconDownload, IconFileText } from 'ui'
|
||||
|
||||
import Table from 'components/to-be-cleaned/Table'
|
||||
import AlertError from 'components/ui/AlertError'
|
||||
import { GenericSkeletonLoader } from 'components/ui/ShimmeringLoader'
|
||||
import { getInvoice } from 'data/invoices/invoice-query'
|
||||
import { useProjectInvoicesCountQuery } from 'data/invoices/project-invoices-count-query'
|
||||
import { useProjectInvoicesQuery } from 'data/invoices/project-invoices-query'
|
||||
import { useStore } from 'hooks'
|
||||
import { InvoiceStatus } from './Invoices.types'
|
||||
import InvoiceStatusBadge from './InvoiceStatusBadge'
|
||||
|
||||
const PAGE_LIMIT = 10
|
||||
|
||||
const Invoices = () => {
|
||||
const { ui } = useStore()
|
||||
const { ref: projectRef } = useParams()
|
||||
const [page, setPage] = useState(1)
|
||||
|
||||
const offset = (page - 1) * PAGE_LIMIT
|
||||
const { data: count, isError: isErrorCount } = useProjectInvoicesCountQuery({ projectRef })
|
||||
const { data, error, isLoading, isError, isSuccess } = useProjectInvoicesQuery({
|
||||
projectRef,
|
||||
limit: PAGE_LIMIT,
|
||||
offset,
|
||||
})
|
||||
const invoices = data || []
|
||||
|
||||
useEffect(() => {
|
||||
setPage(1)
|
||||
}, [projectRef])
|
||||
|
||||
const fetchInvoice = async (id: string) => {
|
||||
try {
|
||||
const invoice = await getInvoice({ id })
|
||||
if (invoice?.invoice_pdf) window.open(invoice.invoice_pdf, '_blank')
|
||||
} catch (error: any) {
|
||||
ui.setNotification({
|
||||
category: 'info',
|
||||
message: `Failed to fetch the selected invoice: ${error.message}`,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="container my-4 max-w-4xl space-y-1">
|
||||
{isLoading && <GenericSkeletonLoader />}
|
||||
|
||||
{isError && <AlertError error={error} subject="Failed to retrieve project invoices" />}
|
||||
|
||||
{isSuccess && (
|
||||
<Table
|
||||
head={[
|
||||
<Table.th key="header-icon"></Table.th>,
|
||||
<Table.th key="header-date">Date</Table.th>,
|
||||
<Table.th key="header-amount">Amount due</Table.th>,
|
||||
<Table.th key="header-invoice">Invoice number</Table.th>,
|
||||
<Table.th key="header-status" className="flex items-center">
|
||||
Status
|
||||
</Table.th>,
|
||||
<Table.th key="header-download" className="text-right"></Table.th>,
|
||||
]}
|
||||
body={
|
||||
invoices.length === 0 ? (
|
||||
<Table.tr>
|
||||
<Table.td colSpan={6} className="p-3 py-12 text-center">
|
||||
<p className="text-foreground-light">No invoices for this project yet</p>
|
||||
</Table.td>
|
||||
</Table.tr>
|
||||
) : (
|
||||
<>
|
||||
{invoices.map((x) => {
|
||||
return (
|
||||
<Table.tr key={x.id}>
|
||||
<Table.td>
|
||||
<IconFileText size="xxl" />
|
||||
</Table.td>
|
||||
<Table.td>
|
||||
<p>{new Date(x.period_end * 1000).toLocaleString()}</p>
|
||||
</Table.td>
|
||||
<Table.td>
|
||||
<p>
|
||||
{x.subtotal >= 0
|
||||
? `$${x.subtotal / 100}`
|
||||
: `-$${Math.abs(x.subtotal / 100)}`}
|
||||
</p>
|
||||
</Table.td>
|
||||
<Table.td>
|
||||
<p>{x.number}</p>
|
||||
</Table.td>
|
||||
<Table.td>
|
||||
<InvoiceStatusBadge status={x.status} />
|
||||
</Table.td>
|
||||
<Table.td className="align-right">
|
||||
<div className="flex items-center justify-end space-x-2">
|
||||
{[InvoiceStatus.UNCOLLECTIBLE, InvoiceStatus.OPEN].includes(x.status) && (
|
||||
<Button asChild>
|
||||
<Link
|
||||
href={`https://redirect.revops.supabase.com/pay-invoice/${x.id}`}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
Pay Now
|
||||
</Link>
|
||||
</Button>
|
||||
)}
|
||||
|
||||
<Button
|
||||
type="outline"
|
||||
icon={<IconDownload size={16} strokeWidth={1.5} />}
|
||||
onClick={() => fetchInvoice(x.id)}
|
||||
/>
|
||||
</div>
|
||||
</Table.td>
|
||||
</Table.tr>
|
||||
)
|
||||
})}
|
||||
<Table.tr key="navigation">
|
||||
<Table.td colSpan={6}>
|
||||
<div className="flex items-center justify-between">
|
||||
<p className="text-sm opacity-50">
|
||||
{isErrorCount
|
||||
? 'Failed to retrieve total number of invoices'
|
||||
: `Showing ${offset + 1} to ${
|
||||
offset + invoices.length
|
||||
} out of ${count} invoices`}
|
||||
</p>
|
||||
<div className="flex items-center space-x-2">
|
||||
<Button
|
||||
icon={<IconChevronLeft />}
|
||||
type="default"
|
||||
size="tiny"
|
||||
disabled={page === 1}
|
||||
onClick={async () => setPage(page - 1)}
|
||||
/>
|
||||
<Button
|
||||
icon={<IconChevronRight />}
|
||||
type="default"
|
||||
size="tiny"
|
||||
disabled={page * PAGE_LIMIT >= (count ?? 0)}
|
||||
onClick={async () => setPage(page + 1)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Table.td>
|
||||
</Table.tr>
|
||||
</>
|
||||
)
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Invoices
|
||||
@@ -57,31 +57,6 @@ export const analyticsKeys = {
|
||||
},
|
||||
] as const,
|
||||
|
||||
dailyStats: (
|
||||
projectRef: string | undefined,
|
||||
{
|
||||
attribute,
|
||||
startDate,
|
||||
endDate,
|
||||
interval,
|
||||
}: {
|
||||
attribute?: string
|
||||
startDate?: string
|
||||
endDate?: string
|
||||
interval?: string
|
||||
}
|
||||
) =>
|
||||
[
|
||||
'projects',
|
||||
projectRef,
|
||||
'daily-stats',
|
||||
{
|
||||
attribute,
|
||||
startDate: isoDateStringToDate(startDate),
|
||||
endDate: isoDateStringToDate(endDate),
|
||||
interval,
|
||||
},
|
||||
] as const,
|
||||
orgDailyStats: (
|
||||
orgSlug: string | undefined,
|
||||
{
|
||||
|
||||
@@ -1,43 +0,0 @@
|
||||
import { useQuery, UseQueryOptions } from '@tanstack/react-query'
|
||||
import { head } from 'data/fetchers'
|
||||
import { ResponseError } from 'types'
|
||||
import { invoicesKeys } from './keys'
|
||||
|
||||
export type ProjectInvoicesCountVariables = {
|
||||
projectRef?: string
|
||||
}
|
||||
|
||||
export async function getProjectInvoicesCount(
|
||||
{ projectRef }: ProjectInvoicesCountVariables,
|
||||
signal?: AbortSignal
|
||||
) {
|
||||
if (!projectRef) throw new Error('Project ref is required')
|
||||
|
||||
const res = await head(`/platform/projects/{ref}/invoices`, {
|
||||
params: { path: { ref: projectRef } },
|
||||
signal,
|
||||
parseAs: 'text',
|
||||
})
|
||||
|
||||
if (res.error) throw (res as any).error
|
||||
return Number(res.response.headers.get('X-Total-Count'))
|
||||
}
|
||||
|
||||
export type ProjectInvoicesCountData = Awaited<ReturnType<typeof getProjectInvoicesCount>>
|
||||
export type ProjectInvoicesCountError = ResponseError
|
||||
|
||||
export const useProjectInvoicesCountQuery = <TData = ProjectInvoicesCountData>(
|
||||
{ projectRef }: ProjectInvoicesCountVariables,
|
||||
{
|
||||
enabled = true,
|
||||
...options
|
||||
}: UseQueryOptions<ProjectInvoicesCountData, ProjectInvoicesCountError, TData> = {}
|
||||
) =>
|
||||
useQuery<ProjectInvoicesCountData, ProjectInvoicesCountError, TData>(
|
||||
invoicesKeys.projectInvoicesCount(projectRef),
|
||||
({ signal }) => getProjectInvoicesCount({ projectRef }, signal),
|
||||
{
|
||||
enabled: enabled && typeof projectRef !== 'undefined',
|
||||
...options,
|
||||
}
|
||||
)
|
||||
@@ -1,48 +0,0 @@
|
||||
import { useQuery, UseQueryOptions } from '@tanstack/react-query'
|
||||
import { get } from 'data/fetchers'
|
||||
import { ResponseError } from 'types'
|
||||
import { invoicesKeys } from './keys'
|
||||
|
||||
export type ProjectInvoicesVariables = {
|
||||
projectRef?: string
|
||||
offset?: number
|
||||
limit?: number
|
||||
}
|
||||
|
||||
export async function getProjectInvoices(
|
||||
{ projectRef, offset = 0, limit = 10 }: ProjectInvoicesVariables,
|
||||
signal?: AbortSignal
|
||||
) {
|
||||
if (!projectRef) throw new Error('Project ref is required')
|
||||
|
||||
const { data, error } = await get(`/platform/projects/{ref}/invoices`, {
|
||||
params: {
|
||||
path: { ref: projectRef },
|
||||
query: { offset: offset.toString(), limit: limit.toString() },
|
||||
},
|
||||
signal,
|
||||
})
|
||||
|
||||
if (error) throw error
|
||||
return data
|
||||
}
|
||||
|
||||
export type ProjectInvoicesData = Awaited<ReturnType<typeof getProjectInvoices>>
|
||||
export type ProjectInvoicesError = ResponseError
|
||||
|
||||
export const useProjectInvoicesQuery = <TData = ProjectInvoicesData>(
|
||||
{ projectRef, offset, limit }: ProjectInvoicesVariables,
|
||||
{
|
||||
enabled = true,
|
||||
...options
|
||||
}: UseQueryOptions<ProjectInvoicesData, ProjectInvoicesError, TData> = {}
|
||||
) =>
|
||||
// [Joshen] Switch to useInfiniteQuery
|
||||
useQuery<ProjectInvoicesData, ProjectInvoicesError, TData>(
|
||||
invoicesKeys.projectInvoices(projectRef, offset),
|
||||
({ signal }) => getProjectInvoices({ projectRef, offset, limit }, signal),
|
||||
{
|
||||
enabled: enabled && typeof projectRef !== 'undefined',
|
||||
...options,
|
||||
}
|
||||
)
|
||||
@@ -11,9 +11,6 @@ export const organizationKeys = {
|
||||
slug: string | undefined,
|
||||
{ date_start, date_end }: { date_start: string | undefined; date_end: string | undefined }
|
||||
) => ['organizations', slug, 'audit-logs', { date_start, date_end }] as const,
|
||||
migrateBilling: (slug: string | undefined) => ['organizations', slug, 'migrate-billing'] as const,
|
||||
migrateBillingPreview: (slug: string | undefined) =>
|
||||
['organizations', slug, 'migrate-billing', 'preview'] as const,
|
||||
subscriptionPreview: (slug: string | undefined, tier: string | undefined) =>
|
||||
['organizations', slug, 'subscription', 'preview', tier] as const,
|
||||
taxIds: (slug: string | undefined) => ['organizations', slug, 'tax-ids'] as const,
|
||||
|
||||
@@ -1,76 +0,0 @@
|
||||
import { useMutation, UseMutationOptions, useQueryClient } from '@tanstack/react-query'
|
||||
import { toast } from 'react-hot-toast'
|
||||
|
||||
import { post } from 'lib/common/fetch'
|
||||
import { API_URL } from 'lib/constants'
|
||||
import { ResponseError } from 'types'
|
||||
import { organizationKeys } from './keys'
|
||||
|
||||
export type OrganizationBillingMigrationVariables = {
|
||||
organizationSlug?: string
|
||||
tier?: string
|
||||
paymentMethodId?: string
|
||||
}
|
||||
|
||||
export async function migrateBilling({
|
||||
organizationSlug,
|
||||
tier,
|
||||
paymentMethodId,
|
||||
}: OrganizationBillingMigrationVariables) {
|
||||
if (!organizationSlug) throw new Error('organizationSlug is required')
|
||||
if (!tier) throw new Error('tier is required')
|
||||
|
||||
const payload: { tier: string; payment_method_id?: string } = {
|
||||
tier,
|
||||
payment_method_id: paymentMethodId,
|
||||
}
|
||||
|
||||
const response = await post(
|
||||
`${API_URL}/organizations/${organizationSlug}/org-billing-migration`,
|
||||
payload
|
||||
)
|
||||
if (response.error) throw response.error
|
||||
|
||||
return response
|
||||
}
|
||||
|
||||
type OrganizationBillingMigrationData = Awaited<ReturnType<typeof migrateBilling>>
|
||||
|
||||
export const useOrganizationBillingMigrationMutation = ({
|
||||
onSuccess,
|
||||
onError,
|
||||
...options
|
||||
}: Omit<
|
||||
UseMutationOptions<
|
||||
OrganizationBillingMigrationData,
|
||||
ResponseError,
|
||||
OrganizationBillingMigrationVariables
|
||||
>,
|
||||
'mutationFn'
|
||||
> = {}) => {
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
return useMutation<
|
||||
OrganizationBillingMigrationData,
|
||||
ResponseError,
|
||||
OrganizationBillingMigrationVariables
|
||||
>((vars) => migrateBilling(vars), {
|
||||
async onSuccess(data, variables, context) {
|
||||
const { organizationSlug } = variables
|
||||
await Promise.all([
|
||||
queryClient.invalidateQueries(organizationKeys.members(organizationSlug)),
|
||||
queryClient.invalidateQueries(organizationKeys.list()),
|
||||
queryClient.invalidateQueries(organizationKeys.freeProjectLimitCheck(organizationSlug)),
|
||||
])
|
||||
await onSuccess?.(data, variables, context)
|
||||
},
|
||||
async onError(data, variables, context) {
|
||||
if (onError === undefined) {
|
||||
toast.error(`Failed to migrate billing: ${data.message}`)
|
||||
} else {
|
||||
onError(data, variables, context)
|
||||
}
|
||||
},
|
||||
...options,
|
||||
})
|
||||
}
|
||||
@@ -1,99 +0,0 @@
|
||||
import { useQuery, UseQueryOptions } from '@tanstack/react-query'
|
||||
import { post } from 'lib/common/fetch'
|
||||
import { API_URL } from 'lib/constants'
|
||||
import { organizationKeys } from './keys'
|
||||
import { SubscriptionTier } from 'data/subscriptions/types'
|
||||
|
||||
export type OrganizationBillingMigrationPreviewVariables = {
|
||||
organizationSlug?: string
|
||||
tier?: string
|
||||
}
|
||||
|
||||
export type OrganizationBillingMigrationPreviewResponse = {
|
||||
added_credits: number
|
||||
addons_to_be_removed: {
|
||||
projectRef: string
|
||||
projectName: string
|
||||
addons: { variant: string; name: string; type: string }[]
|
||||
}[]
|
||||
monthly_invoice_breakdown: {
|
||||
description: string
|
||||
unit_price: number
|
||||
quantity: number
|
||||
total_price: number
|
||||
}[]
|
||||
old_tiers: SubscriptionTier[]
|
||||
}
|
||||
|
||||
export async function previewOrganizationBillingMigration(
|
||||
{ organizationSlug, tier }: OrganizationBillingMigrationPreviewVariables,
|
||||
signal?: AbortSignal
|
||||
) {
|
||||
if (!organizationSlug) throw new Error('organizationSlug is required')
|
||||
if (!tier) throw new Error('tier is required')
|
||||
|
||||
const payload: { tier: string } = {
|
||||
tier,
|
||||
}
|
||||
|
||||
const response = await post(
|
||||
`${API_URL}/organizations/${organizationSlug}/org-billing-migration/preview`,
|
||||
payload,
|
||||
{ signal }
|
||||
)
|
||||
|
||||
if (response.error) throw response.error
|
||||
|
||||
return response as OrganizationBillingMigrationPreviewResponse
|
||||
}
|
||||
|
||||
export type OrganizationBillingMigrationPreviewData = Awaited<
|
||||
ReturnType<typeof previewOrganizationBillingMigration>
|
||||
>
|
||||
export type OrganizationBillingMigrationPreviewError = {
|
||||
message: string
|
||||
}
|
||||
|
||||
export const useOrganizationBillingMigrationPreview = <
|
||||
TData = OrganizationBillingMigrationPreviewData
|
||||
>(
|
||||
{ organizationSlug, tier }: OrganizationBillingMigrationPreviewVariables,
|
||||
{
|
||||
enabled = true,
|
||||
...options
|
||||
}: UseQueryOptions<
|
||||
OrganizationBillingMigrationPreviewData,
|
||||
OrganizationBillingMigrationPreviewError,
|
||||
TData
|
||||
> = {}
|
||||
) =>
|
||||
useQuery<
|
||||
OrganizationBillingMigrationPreviewData,
|
||||
OrganizationBillingMigrationPreviewError,
|
||||
TData
|
||||
>(
|
||||
organizationKeys.migrateBillingPreview(organizationSlug),
|
||||
({ signal }) => previewOrganizationBillingMigration({ organizationSlug, tier }, signal),
|
||||
{
|
||||
enabled: enabled && typeof organizationSlug !== 'undefined' && typeof tier !== 'undefined',
|
||||
...options,
|
||||
|
||||
retry: (failureCount, error) => {
|
||||
// Don't retry on 400s
|
||||
if (
|
||||
typeof error === 'object' &&
|
||||
error !== null &&
|
||||
'code' in error &&
|
||||
(error as any).code === 400
|
||||
) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (failureCount < 3) {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
},
|
||||
}
|
||||
)
|
||||
Reference in new issue
Block a user