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:
Ignacio DobronichandClaude Opus 4.6 authored and GitHub committed 2026-04-10 09:49:57 -03:00
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)
}
@@ -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()
})
})
})