fix: allow payouts that go over the tax limit by prefilling form (#4478)

* feat: start on fix

* fix: withdraw btn

* fix: lint issues

* fix: use button rather than span

* fix: lint issues

---------

Co-authored-by: --global <--global>
This commit is contained in:
Calum H.
2025-10-07 17:33:50 +01:00
committed by GitHub
parent cb5600ad45
commit 3c578108de
3 changed files with 90 additions and 31 deletions

View File

@@ -239,6 +239,7 @@ async function continueForm() {
}
} catch (error) {
console.error('Error occurred while continuing tax form:', error)
handleCancel()
} finally {
manualLoading.value = false
}

View File

@@ -68,25 +68,15 @@
</div>
</div>
<div class="input-group mt-4">
<span
:class="{
'disabled-cursor-wrapper': userBalance.available < minWithdraw || blockedByTax,
}"
>
<ButtonStyled color="brand">
<nuxt-link
:aria-disabled="
userBalance.available < minWithdraw || blockedByTax ? 'true' : 'false'
"
:class="{ 'disabled-link': userBalance.available < minWithdraw || blockedByTax }"
:disabled="!!(userBalance.available < minWithdraw || blockedByTax) || null"
:tabindex="userBalance.available < minWithdraw || blockedByTax ? -1 : undefined"
to="/dashboard/revenue/withdraw"
>
<TransferIcon /> Withdraw
</nuxt-link>
</ButtonStyled>
</span>
<ButtonStyled color="brand">
<nuxt-link
v-if="!(userBalance.available < minWithdraw || blockedByTax)"
to="/dashboard/revenue/withdraw"
>
<TransferIcon /> Withdraw
</nuxt-link>
<button v-else class="disabled"><TransferIcon /> Withdraw</button>
</ButtonStyled>
<ButtonStyled>
<NuxtLink to="/dashboard/revenue/transfers">
<HistoryIcon />
@@ -176,9 +166,19 @@ const { addNotification, handleError } = injectNotificationManager()
const auth = await useAuth()
const minWithdraw = ref(0.01)
const { data: userBalance } = await useAsyncData(`payout/balance`, () =>
useBaseFetch(`payout/balance`, { apiVersion: 3 }),
)
const { data: userBalance } = await useAsyncData(`payout/balance`, async () => {
const response = await useBaseFetch(`payout/balance`, { apiVersion: 3 })
return {
...response,
available: parseFloat(response.available),
withdrawn_lifetime: parseFloat(response.withdrawn_lifetime),
withdrawn_ytd: parseFloat(response.withdrawn_ytd),
pending: parseFloat(response.pending),
dates: Object.fromEntries(
Object.entries(response.dates).map(([date, value]) => [date, parseFloat(value)]),
),
}
})
const blockedByTax = computed(() => {
const status = userBalance.value?.form_completion_status ?? 'unknown'
@@ -250,14 +250,6 @@ strong {
font-weight: 500;
}
.disabled-cursor-wrapper {
cursor: not-allowed;
}
.disabled-link {
pointer-events: none;
}
.grid-display {
grid-template-columns: repeat(auto-fit, minmax(16rem, 1fr));
}

View File

@@ -1,4 +1,9 @@
<template>
<CreatorTaxFormModal
ref="taxFormModalRef"
@success="onTaxFormSuccess"
@cancelled="onTaxFormCancelled"
/>
<section class="universal-card">
<Breadcrumbs
current-title="Withdraw"
@@ -135,6 +140,11 @@
</template>
</div>
<p v-if="willTriggerTaxForm" class="font-bold text-orange">
This withdrawal will exceed $600 for the year. You will be prompted to complete a tax form
before proceeding.
</p>
<p v-if="blockedByTax" class="font-bold text-orange">
You have withdrawn over $600 this year. To continue withdrawing, you must complete a tax form.
</p>
@@ -207,6 +217,7 @@ import { all } from 'iso-3166-1'
import { Multiselect } from 'vue-multiselect'
import VenmoIcon from '~/assets/images/external/venmo.svg?component'
import CreatorTaxFormModal from '~/components/ui/dashboard/CreatorTaxFormModal.vue'
const { addNotification } = injectNotificationManager()
const auth = await useAuth()
@@ -227,7 +238,19 @@ const country = ref(
const [{ data: userBalance }, { data: payoutMethods, refresh: refreshPayoutMethods }] =
await Promise.all([
useAsyncData(`payout/balance`, () => useBaseFetch(`payout/balance`, { apiVersion: 3 })),
useAsyncData(`payout/balance`, async () => {
const response = await useBaseFetch(`payout/balance`, { apiVersion: 3 })
return {
...response,
available: parseFloat(response.available),
withdrawn_lifetime: parseFloat(response.withdrawn_lifetime),
withdrawn_ytd: parseFloat(response.withdrawn_ytd),
pending: parseFloat(response.pending),
dates: Object.fromEntries(
Object.entries(response.dates).map(([date, value]) => [date, parseFloat(value)]),
),
}
}),
useAsyncData(`payout/methods?country=${country.value.id}`, () =>
useBaseFetch(`payout/methods?country=${country.value.id}`, { apiVersion: 3 }),
),
@@ -334,6 +357,13 @@ const blockedByTax = computed(() => {
return thresholdMet && status !== 'complete'
})
const willTriggerTaxForm = computed(() => {
const status = userBalance.value?.form_completion_status ?? 'unknown'
const currentWithdrawn = userBalance.value?.withdrawn_ytd ?? 0
const wouldExceedThreshold = currentWithdrawn + parsedAmount.value >= 600
return wouldExceedThreshold && status !== 'complete' && !blockedByTax.value
})
watch(country, async () => {
await refreshPayoutMethods()
if (payoutMethods.value && payoutMethods.value[0]) {
@@ -353,7 +383,18 @@ watch(selectedMethod, () => {
agreedTerms.value = false
})
const taxFormModalRef = ref(null)
const taxFormCancelled = ref(false)
async function withdraw() {
if (willTriggerTaxForm.value) {
taxFormCancelled.value = false
if (taxFormModalRef.value && taxFormModalRef.value.startTaxForm) {
await taxFormModalRef.value.startTaxForm(new MouseEvent('click'))
}
return
}
startLoading()
try {
const auth = await useAuth()
@@ -386,6 +427,31 @@ async function withdraw() {
}
stopLoading()
}
async function onTaxFormSuccess() {
// Refresh user balance to get updated form completion status
const updatedBalance = await useBaseFetch(`payout/balance`, { apiVersion: 3 })
userBalance.value = updatedBalance
if (updatedBalance?.form_completion_status === 'complete') {
await withdraw()
} else {
addNotification({
title: 'Tax form incomplete',
text: 'You must complete a tax form for this withdrawal.',
type: 'error',
})
}
}
function onTaxFormCancelled() {
taxFormCancelled.value = true
addNotification({
title: 'Withdrawal canceled',
text: 'You must complete a tax form for this withdrawal.',
type: 'error',
})
}
</script>
<style lang="scss" scoped>