feat: dynamic tax thresholds from backend (#5342)

* feat: dynamic tax thresholds from backend

* fix: lint & i18n
This commit is contained in:
Calum H.
2026-02-09 14:42:38 +00:00
committed by GitHub
parent 45a397d52b
commit e962521492
10 changed files with 89 additions and 22 deletions

View File

@@ -1,11 +1,18 @@
<script setup lang="ts">
import { FileTextIcon } from '@modrinth/assets'
import { ButtonStyled, defineMessages, PagewideBanner, useVIntl } from '@modrinth/ui'
import { formatMoney } from '@modrinth/utils'
import { computed } from 'vue'
import { getTaxThreshold } from '@/providers/creator-withdraw.ts'
import CreatorTaxFormModal from '~/components/ui/dashboard/CreatorTaxFormModal.vue'
import { useGeneratedState } from '~/composables/generated'
const { formatMessage } = useVIntl()
const generatedState = useGeneratedState()
const taxThreshold = computed(() => getTaxThreshold(generatedState.value?.taxComplianceThresholds))
const modal = useTemplateRef('modal')
const messages = defineMessages({
@@ -16,7 +23,7 @@ const messages = defineMessages({
description: {
id: 'layout.banner.tax.description',
defaultMessage:
"You've already withdrawn over $600 from Modrinth this year. To comply with tax regulations, you need to complete a tax form. Your withdrawals are paused until this form is submitted.",
"You've already withdrawn over {threshold} from Modrinth this year. To comply with tax regulations, you need to complete a tax form. Your withdrawals are paused until this form is submitted.",
},
action: {
id: 'layout.banner.tax.action',
@@ -38,7 +45,9 @@ function openTaxForm(e: MouseEvent) {
<span>{{ formatMessage(messages.title) }}</span>
</template>
<template #description>
<span>{{ formatMessage(messages.description) }}</span>
<span>{{
formatMessage(messages.description, { threshold: formatMoney(taxThreshold) })
}}</span>
</template>
<template #actions>
<ButtonStyled color="orange">

View File

@@ -128,12 +128,14 @@ import { computed, nextTick, onMounted, ref, useTemplateRef, watch } from 'vue'
import {
createWithdrawContext,
getTaxThreshold,
getTaxThresholdActual,
type PaymentProvider,
type PayoutMethod,
provideWithdrawContext,
TAX_THRESHOLD_ACTUAL,
type WithdrawStage,
} from '@/providers/creator-withdraw.ts'
import { useGeneratedState } from '~/composables/generated'
import CreatorTaxFormModal from './CreatorTaxFormModal.vue'
import CompletionStage from './withdraw-stages/CompletionStage.vue'
@@ -191,9 +193,13 @@ defineExpose({
const { formatMessage } = useVIntl()
const { addNotification } = injectNotificationManager()
const generatedState = useGeneratedState()
const taxComplianceThresholds = computed(() => generatedState.value.taxComplianceThresholds)
const withdrawContext = createWithdrawContext(
props.balance,
props.preloadedPaymentData || undefined,
taxComplianceThresholds.value,
)
provideWithdrawContext(withdrawContext)
@@ -249,13 +255,13 @@ const needsTaxForm = computed(() => {
const ytd = props.balance.withdrawn_ytd ?? 0
const available = props.balance.available ?? 0
const status = props.balance.form_completion_status
return status !== 'complete' && ytd + available >= 600
return status !== 'complete' && ytd + available >= getTaxThreshold(taxComplianceThresholds.value)
})
const remainingLimit = computed(() => {
if (!props.balance) return 0
const ytd = props.balance.withdrawn_ytd ?? 0
const raw = TAX_THRESHOLD_ACTUAL - ytd
const raw = getTaxThresholdActual(taxComplianceThresholds.value) - ytd
if (raw <= 0) return 0
const cents = Math.floor(raw * 100)
return cents / 100

View File

@@ -94,8 +94,14 @@ import { formatMoney } from '@modrinth/utils'
import { useGeolocation } from '@vueuse/core'
import { useCountries, useFormattedCountries, useUserCountry } from '@/composables/country.ts'
import { type PayoutMethod, useWithdrawContext } from '@/providers/creator-withdraw.ts'
import {
getTaxThreshold,
type PayoutMethod,
useWithdrawContext,
} from '@/providers/creator-withdraw.ts'
import { useGeneratedState } from '~/composables/generated'
const generatedState = useGeneratedState()
const debug = useDebugLogger('MethodSelectionStage')
const {
withdrawData,
@@ -165,7 +171,9 @@ const shouldShowTaxLimitWarning = computed(() => {
if (!balanceValue) return false
const formIncomplete = balanceValue.form_completion_status !== 'complete'
const wouldHitLimit = (balanceValue.withdrawn_ytd ?? 0) + (balanceValue.available ?? 0) >= 600
const wouldHitLimit =
(balanceValue.withdrawn_ytd ?? 0) + (balanceValue.available ?? 0) >=
getTaxThreshold(generatedState.value.taxComplianceThresholds)
return formIncomplete && wouldHitLimit
})

View File

@@ -5,14 +5,14 @@
<span class="font-semibold text-contrast">{{ formatMessage(messages.withdrawLimit) }}</span>
<div>
<span class="text-orange">{{ formatMoney(usedLimit) }}</span> /
<span class="text-contrast">{{ formatMoney(600) }}</span>
<span class="text-contrast">{{ formatMoney(taxThreshold) }}</span>
</div>
</div>
<div class="flex h-2.5 w-full overflow-hidden rounded-full bg-surface-2">
<div
v-if="usedLimit > 0"
class="gradient-border bg-orange"
:style="{ width: `${(usedLimit / 600) * 100}%` }"
:style="{ width: `${(usedLimit / taxThreshold) * 100}%` }"
></div>
</div>
</div>
@@ -59,7 +59,7 @@
<span>
<IntlFormatted
:message-id="messages.withdrawLimitUsed"
:values="{ withdrawLimit: formatMoney(600) }"
:values="{ withdrawLimit: formatMoney(taxThreshold) }"
>
<template #b="{ children }">
<b>
@@ -85,7 +85,8 @@ import {
import { formatMoney } from '@modrinth/utils'
import { computed } from 'vue'
import { TAX_THRESHOLD_ACTUAL } from '@/providers/creator-withdraw.ts'
import { getTaxThreshold, getTaxThresholdActual } from '@/providers/creator-withdraw.ts'
import { useGeneratedState } from '~/composables/generated'
const props = defineProps<{
balance: any
@@ -94,9 +95,15 @@ const props = defineProps<{
const { formatMessage } = useVIntl()
const generatedState = useGeneratedState()
const taxThreshold = computed(() => getTaxThreshold(generatedState.value.taxComplianceThresholds))
const taxThresholdActual = computed(() =>
getTaxThresholdActual(generatedState.value.taxComplianceThresholds),
)
const usedLimit = computed(() => props.balance?.withdrawn_ytd ?? 0)
const remainingLimit = computed(() => {
const raw = TAX_THRESHOLD_ACTUAL - usedLimit.value
const raw = taxThresholdActual.value - usedLimit.value
if (raw <= 0) return 0
const cents = Math.floor(raw * 100)
return cents / 100