Payout flows in backend - fix Tremendous forex cards (#5001)

* wip: payouts flow api

* working

* Finish up flow migration

* vibe-coded frontend changes

* fix typos and vue

* fix: types

---------

Co-authored-by: Calum H. (IMB11) <contact@cal.engineer>
This commit is contained in:
aecsocket
2026-01-14 10:53:35 +00:00
committed by GitHub
parent 50a87ba933
commit d055dc68dc
17 changed files with 1224 additions and 873 deletions

View File

@@ -12,14 +12,14 @@
<div class="flex items-center justify-between">
<span class="text-primary">{{ formatMessage(messages.feeBreakdownGiftCardValue) }}</span>
<span class="font-semibold text-contrast"
>{{ formatMoney(amount || 0) }} ({{ formattedLocalCurrency }})</span
>{{ formatMoney(amountInUsd) }} ({{ formattedLocalCurrencyAmount }})</span
>
</div>
</template>
<template v-else>
<div class="flex items-center justify-between">
<span class="text-primary">{{ formatMessage(messages.feeBreakdownAmount) }}</span>
<span class="font-semibold text-contrast">{{ formatMoney(amount || 0) }}</span>
<span class="font-semibold text-contrast">{{ formatMoney(amountInUsd) }}</span>
</div>
</template>
@@ -29,7 +29,7 @@
<template v-if="feeLoading">
<LoaderCircleIcon class="size-5 animate-spin !text-secondary" />
</template>
<template v-else>-{{ formatMoney(fee || 0) }}</template>
<template v-else>-{{ formatMoney(feeInUsd) }}</template>
</span>
</div>
@@ -79,9 +79,23 @@ const props = withDefaults(
const { formatMessage } = useVIntl()
const amountInUsd = computed(() => {
if (props.isGiftCard && shouldShowExchangeRate.value) {
return (props.amount || 0) / (props.exchangeRate || 1)
}
return props.amount || 0
})
const feeInUsd = computed(() => {
if (props.isGiftCard && shouldShowExchangeRate.value) {
return (props.fee || 0) / (props.exchangeRate || 1)
}
return props.fee || 0
})
const netAmount = computed(() => {
const amount = props.amount || 0
const fee = props.fee || 0
const amount = amountInUsd.value
const fee = feeInUsd.value
return Math.max(0, amount - fee)
})
@@ -96,6 +110,11 @@ const netAmountInLocalCurrency = computed(() => {
return netAmount.value * (props.exchangeRate || 0)
})
const localCurrencyAmount = computed(() => {
if (!shouldShowExchangeRate.value) return null
return props.amount || 0
})
const formattedLocalCurrency = computed(() => {
if (!shouldShowExchangeRate.value || !netAmountInLocalCurrency.value || !props.localCurrency)
return ''
@@ -112,6 +131,21 @@ const formattedLocalCurrency = computed(() => {
}
})
const formattedLocalCurrencyAmount = computed(() => {
if (!shouldShowExchangeRate.value || !localCurrencyAmount.value || !props.localCurrency) return ''
try {
return new Intl.NumberFormat('en-US', {
style: 'currency',
currency: props.localCurrency,
minimumFractionDigits: 2,
maximumFractionDigits: 2,
}).format(localCurrencyAmount.value)
} catch {
return `${props.localCurrency} ${localCurrencyAmount.value.toFixed(2)}`
}
})
const messages = defineMessages({
feeBreakdownAmount: {
id: 'dashboard.creator-withdraw-modal.fee-breakdown-amount',

View File

@@ -90,7 +90,14 @@
</Combobox>
</div>
<span v-if="selectedMethodDetails" class="text-secondary">
{{ formatMoney(fixedDenominationMin ?? effectiveMinAmount)
{{
formatMoney(
selectedMethodCurrencyCode &&
selectedMethodCurrencyCode !== 'USD' &&
selectedMethodExchangeRate
? (fixedDenominationMin ?? effectiveMinAmount) / selectedMethodExchangeRate
: (fixedDenominationMin ?? effectiveMinAmount),
)
}}<template v-if="selectedMethodCurrencyCode && selectedMethodCurrencyCode !== 'USD'">
({{
formatAmountForDisplay(
@@ -103,9 +110,15 @@
min,
{{
formatMoney(
fixedDenominationMax ??
selectedMethodDetails.interval?.standard?.max ??
effectiveMaxAmount,
selectedMethodCurrencyCode &&
selectedMethodCurrencyCode !== 'USD' &&
selectedMethodExchangeRate
? (fixedDenominationMax ??
selectedMethodDetails.interval?.standard?.max ??
effectiveMaxAmount) / selectedMethodExchangeRate
: (fixedDenominationMax ??
selectedMethodDetails.interval?.standard?.max ??
effectiveMaxAmount),
)
}}<template v-if="selectedMethodCurrencyCode && selectedMethodCurrencyCode !== 'USD'">
({{
@@ -124,7 +137,15 @@
v-if="selectedMethodDetails && effectiveMinAmount > roundedMaxAmount"
class="text-sm text-red"
>
You need at least {{ formatMoney(effectiveMinAmount)
You need at least
{{
formatMoney(
selectedMethodCurrencyCode &&
selectedMethodCurrencyCode !== 'USD' &&
selectedMethodExchangeRate
? effectiveMinAmount / selectedMethodExchangeRate
: effectiveMinAmount,
)
}}<template v-if="selectedMethodCurrencyCode && selectedMethodCurrencyCode !== 'USD'">
({{
formatAmountForDisplay(
@@ -186,7 +207,7 @@
formatMessage(messages.balanceWorthHint, {
usdBalance: formatMoney(roundedMaxAmount),
localBalance: formatAmountForDisplay(
roundedMaxAmount,
roundedMaxAmount * selectedMethodExchangeRate,
selectedMethodCurrencyCode,
selectedMethodExchangeRate,
),
@@ -252,7 +273,7 @@
formatMessage(messages.balanceWorthHint, {
usdBalance: formatMoney(roundedMaxAmount),
localBalance: formatAmountForDisplay(
roundedMaxAmount,
roundedMaxAmount * selectedMethodExchangeRate,
selectedMethodCurrencyCode,
selectedMethodExchangeRate,
),
@@ -573,14 +594,13 @@ const giftCardExchangeRate = computed(() => {
})
function formatAmountForDisplay(
usdAmount: number,
localAmount: number,
currencyCode: string | null | undefined,
rate: number | null | undefined,
): string {
if (!currencyCode || currencyCode === 'USD' || !rate) {
return formatMoney(usdAmount)
return formatMoney(localAmount)
}
const localAmount = usdAmount * rate
try {
return new Intl.NumberFormat('en-US', {
style: 'currency',

View File

@@ -40,11 +40,6 @@ export interface PayoutMethod {
category?: string
image_url: string | null
image_logo_url: string | null
fee: {
percentage: number
min: number
max: number | null
}
interval: {
standard: {
min: number
@@ -130,6 +125,7 @@ export interface TaxData {
export interface CalculationData {
amount: number
fee: number | null
netUsd: number | null
exchangeRate: number | null
}
@@ -400,6 +396,7 @@ export function createWithdrawContext(
calculation: {
amount: 0,
fee: null,
netUsd: null,
exchangeRate: null,
},
providerData: {
@@ -841,14 +838,20 @@ export function createWithdrawContext(
apiVersion: 3,
method: 'POST',
body: payload,
})) as { fee: number | string | null; exchange_rate: number | string | null }
})) as {
net_usd: number | string | null
fee: number | string | null
exchange_rate: number | string | null
}
const parsedFee = response.fee ? Number.parseFloat(String(response.fee)) : 0
const parsedNetUsd = response.net_usd ? Number.parseFloat(String(response.net_usd)) : null
const parsedExchangeRate = response.exchange_rate
? Number.parseFloat(String(response.exchange_rate))
: null
withdrawData.value.calculation.fee = parsedFee
withdrawData.value.calculation.netUsd = parsedNetUsd
withdrawData.value.calculation.exchangeRate = parsedExchangeRate
return {
@@ -872,7 +875,9 @@ export function createWithdrawContext(
created: new Date(),
amount: withdrawData.value.calculation.amount,
fee: withdrawData.value.calculation.fee || 0,
netAmount: withdrawData.value.calculation.amount - (withdrawData.value.calculation.fee || 0),
netAmount:
withdrawData.value.calculation.netUsd ??
withdrawData.value.calculation.amount - (withdrawData.value.calculation.fee || 0),
methodType: getMethodDisplayName(withdrawData.value.selection.method),
recipientDisplay: getRecipientDisplay(withdrawData.value),
}