mirror of
https://github.com/supabase/supabase.git
synced 2026-10-05 09:25:06 +03:00
refactor: save tax ID via customer update endpoint (#44728)
## Summary
- Consolidate tax ID saving into the `PUT
/organizations/{slug}/customer` endpoint instead of using a
separate `/tax-ids` endpoint
## Test plan
### Updating billing information
From the billing dashboard`/org/_/billing`:
- [ ] Manually test updating billing address + tax ID from org billing
settings - single API call should save both
- [ ] Clear the Tax ID in the org, save and assert that it has been
deleted
- [ ] Modify all the fields in the address form, including TaxID. Click
"Cancel" and assert that everything has returned to the previous values.
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit
* **Refactor**
* Billing address and tax ID updates are now sent in a single update
operation.
* Error messaging for billing updates consolidated into a single failure
notification.
* Tax ID handling clarified: you can clear tax ID explicitly or leave it
unchanged; omitting address fields no longer overwrites existing
address.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
a9fdb09c66
commit
cfce3c7efc
4 files changed
+49
-60
No files matched your search
@@ -28,7 +28,6 @@ import { useBillingCustomerDataForm } from '@/components/interfaces/Organization
|
||||
import { useOrganizationCustomerProfileQuery } from '@/data/organizations/organization-customer-profile-query'
|
||||
import { useOrganizationCustomerProfileUpdateMutation } from '@/data/organizations/organization-customer-profile-update-mutation'
|
||||
import { useOrganizationTaxIdQuery } from '@/data/organizations/organization-tax-id-query'
|
||||
import { useOrganizationTaxIdUpdateMutation } from '@/data/organizations/organization-tax-id-update-mutation'
|
||||
import { invalidateOrganizationsQuery } from '@/data/organizations/organizations-query'
|
||||
import { useAsyncCheckPermissions } from '@/hooks/misc/useCheckPermissions'
|
||||
import { useSelectedOrganizationQuery } from '@/hooks/misc/useSelectedOrganization'
|
||||
@@ -96,7 +95,6 @@ export function UpdateBillingAddressModal() {
|
||||
const { mutateAsync: updateCustomerProfile } = useOrganizationCustomerProfileUpdateMutation({
|
||||
onError: () => {},
|
||||
})
|
||||
const { mutateAsync: updateTaxId } = useOrganizationTaxIdUpdateMutation({ onError: () => {} })
|
||||
|
||||
const {
|
||||
form,
|
||||
@@ -116,27 +114,19 @@ export function UpdateBillingAddressModal() {
|
||||
setIsSubmitting(true)
|
||||
|
||||
try {
|
||||
try {
|
||||
await updateCustomerProfile({
|
||||
slug,
|
||||
address: data.address,
|
||||
billing_name: data.billing_name,
|
||||
})
|
||||
setDismissed(true)
|
||||
await invalidateOrganizationsQuery(queryClient)
|
||||
} catch (error: any) {
|
||||
toast.error(`Failed to update billing address: ${error.message}`)
|
||||
throw error
|
||||
}
|
||||
|
||||
try {
|
||||
await updateTaxId({ slug, taxId: data.tax_id })
|
||||
} catch (error: any) {
|
||||
toast.error(`Failed to update tax ID: ${error.message}`)
|
||||
throw error
|
||||
}
|
||||
await updateCustomerProfile({
|
||||
slug,
|
||||
address: data.address,
|
||||
billing_name: data.billing_name,
|
||||
tax_id: data.tax_id,
|
||||
})
|
||||
setDismissed(true)
|
||||
await invalidateOrganizationsQuery(queryClient)
|
||||
|
||||
toast.success('Successfully updated billing address')
|
||||
} catch (error: any) {
|
||||
toast.error(`Failed to update billing address: ${error.message}`)
|
||||
throw error
|
||||
} finally {
|
||||
setIsSubmitting(false)
|
||||
}
|
||||
|
||||
+18
-24
@@ -27,7 +27,6 @@ import { organizationKeys } from '@/data/organizations/keys'
|
||||
import { useOrganizationCustomerProfileQuery } from '@/data/organizations/organization-customer-profile-query'
|
||||
import { useOrganizationCustomerProfileUpdateMutation } from '@/data/organizations/organization-customer-profile-update-mutation'
|
||||
import { useOrganizationTaxIdQuery } from '@/data/organizations/organization-tax-id-query'
|
||||
import { useOrganizationTaxIdUpdateMutation } from '@/data/organizations/organization-tax-id-update-mutation'
|
||||
import { useAsyncCheckPermissions } from '@/hooks/misc/useCheckPermissions'
|
||||
import { useSelectedOrganizationQuery } from '@/hooks/misc/useSelectedOrganization'
|
||||
import { STRIPE_PUBLIC_KEY } from '@/lib/constants'
|
||||
@@ -64,7 +63,6 @@ export const BillingCustomerData = () => {
|
||||
const { mutateAsync: updateCustomerProfile } = useOrganizationCustomerProfileUpdateMutation({
|
||||
onError: () => {},
|
||||
})
|
||||
const { mutateAsync: updateTaxId } = useOrganizationTaxIdUpdateMutation({ onError: () => {} })
|
||||
|
||||
const [isSubmitting, setIsSubmitting] = useState(false)
|
||||
const addressElementRef = useRef<StripeAddressElement | null>(null)
|
||||
@@ -87,27 +85,12 @@ export const BillingCustomerData = () => {
|
||||
setIsSubmitting(true)
|
||||
|
||||
try {
|
||||
try {
|
||||
await updateCustomerProfile({
|
||||
slug,
|
||||
address: data.address,
|
||||
billing_name: data.billing_name,
|
||||
})
|
||||
} catch (error) {
|
||||
toast.error(
|
||||
`Failed updating billing address: ${error instanceof Error ? error.message : 'Unknown error'}`
|
||||
)
|
||||
throw error
|
||||
}
|
||||
|
||||
try {
|
||||
await updateTaxId({ slug, taxId: data.tax_id })
|
||||
} catch (error) {
|
||||
toast.error(
|
||||
`Failed updating tax ID: ${error instanceof Error ? error.message : 'Unknown error'}`
|
||||
)
|
||||
throw error
|
||||
}
|
||||
await updateCustomerProfile({
|
||||
slug,
|
||||
address: data.address,
|
||||
billing_name: data.billing_name,
|
||||
tax_id: data.tax_id,
|
||||
})
|
||||
|
||||
toast.success('Successfully updated billing data')
|
||||
|
||||
@@ -116,11 +99,22 @@ export const BillingCustomerData = () => {
|
||||
(prev) => {
|
||||
if (!prev) return prev
|
||||
return prev.map((org) =>
|
||||
org.slug === slug ? { ...org, organization_missing_tax_id: data.tax_id == null } : org
|
||||
org.slug === slug
|
||||
? {
|
||||
...org,
|
||||
...(data.address !== undefined ? { organization_missing_address: false } : {}),
|
||||
...(data.tax_id !== undefined
|
||||
? { organization_missing_tax_id: data.tax_id == null }
|
||||
: {}),
|
||||
}
|
||||
: org
|
||||
)
|
||||
}
|
||||
)
|
||||
} catch (error) {
|
||||
toast.error(
|
||||
`Failed updating billing data: ${error instanceof Error ? error.message : 'Unknown error'}`
|
||||
)
|
||||
throw error
|
||||
} finally {
|
||||
setIsSubmitting(false)
|
||||
|
||||
@@ -2,7 +2,7 @@ import { useMutation, useQueryClient } from '@tanstack/react-query'
|
||||
import { toast } from 'sonner'
|
||||
|
||||
import { organizationKeys } from './keys'
|
||||
import type { CustomerAddress } from './types'
|
||||
import type { CustomerAddress, CustomerTaxId } from './types'
|
||||
import { handleError, put } from '@/data/fetchers'
|
||||
import type { ResponseError, UseCustomMutationOptions } from '@/types'
|
||||
|
||||
@@ -10,25 +10,33 @@ export type OrganizationCustomerProfileUpdateVariables = {
|
||||
slug?: string
|
||||
address?: CustomerAddress
|
||||
billing_name: string
|
||||
/** Pass a tax ID object to set/update, `null` to clear, or `undefined` to leave unchanged */
|
||||
tax_id?: CustomerTaxId | null
|
||||
}
|
||||
|
||||
export async function updateOrganizationCustomerProfile({
|
||||
slug,
|
||||
address,
|
||||
billing_name,
|
||||
tax_id,
|
||||
}: OrganizationCustomerProfileUpdateVariables) {
|
||||
if (!slug) return console.error('Slug is required')
|
||||
|
||||
const payload: any = {}
|
||||
if (address) payload.address = address
|
||||
|
||||
const { data, error } = await put(`/platform/organizations/{slug}/customer`, {
|
||||
params: {
|
||||
path: {
|
||||
slug,
|
||||
},
|
||||
},
|
||||
body: { address: address != null ? address : undefined, billing_name },
|
||||
body: {
|
||||
address: address != null ? address : undefined,
|
||||
billing_name,
|
||||
...(tax_id === null
|
||||
? { clear_tax_id: true as const }
|
||||
: tax_id !== undefined
|
||||
? { tax_id }
|
||||
: {}),
|
||||
},
|
||||
})
|
||||
if (error) throw handleError(error)
|
||||
return data
|
||||
@@ -59,7 +67,7 @@ export const useOrganizationCustomerProfileUpdateMutation = ({
|
||||
>({
|
||||
mutationFn: (vars) => updateOrganizationCustomerProfile(vars),
|
||||
async onSuccess(data, variables, context) {
|
||||
const { address, slug, billing_name } = variables
|
||||
const { address, slug, billing_name, tax_id } = variables
|
||||
|
||||
// We do not invalidate here as GET endpoint data is stale for 1-2 seconds, so we handle state manually
|
||||
queryClient.setQueriesData(
|
||||
@@ -69,11 +77,16 @@ export const useOrganizationCustomerProfileUpdateMutation = ({
|
||||
return {
|
||||
...prev,
|
||||
billing_name,
|
||||
address,
|
||||
...(address !== undefined ? { address } : {}),
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
// Update tax ID cache if tax_id was part of this update
|
||||
if (tax_id !== undefined) {
|
||||
queryClient.setQueryData(organizationKeys.taxId(slug), tax_id)
|
||||
}
|
||||
|
||||
await onSuccess?.(data, variables, context)
|
||||
},
|
||||
async onError(data, variables, context) {
|
||||
|
||||
@@ -231,13 +231,6 @@ vi.mock('@/data/organizations/organization-customer-profile-update-mutation', ()
|
||||
}),
|
||||
}))
|
||||
|
||||
const mockUpdateTaxId = vi.fn(() => Promise.resolve())
|
||||
vi.mock('@/data/organizations/organization-tax-id-update-mutation', () => ({
|
||||
useOrganizationTaxIdUpdateMutation: () => ({
|
||||
mutateAsync: mockUpdateTaxId,
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock('@/data/organizations/organizations-query', () => ({
|
||||
invalidateOrganizationsQuery: vi.fn(() => Promise.resolve()),
|
||||
}))
|
||||
@@ -403,7 +396,6 @@ describe('UpdateBillingAddressModal', () => {
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockUpdateCustomerProfile).not.toHaveBeenCalled()
|
||||
expect(mockUpdateTaxId).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
})
|
||||
Reference in new issue
Block a user