fix: withdraw flow fixes (#5296)
* fix: dev-741 currency exchanging bug * fix: remove redundant balance available check * fix: lint/fmt * fix: #5245 * fix: hide max if it's less than min
This commit is contained in:
@@ -90,66 +90,41 @@
|
|||||||
</Combobox>
|
</Combobox>
|
||||||
</div>
|
</div>
|
||||||
<span v-if="selectedMethodDetails" class="text-secondary">
|
<span v-if="selectedMethodDetails" class="text-secondary">
|
||||||
{{
|
{{ formatMoney(displayMinUsd)
|
||||||
formatMoney(
|
}}<template v-if="selectedMethodCurrencyCode && selectedMethodCurrencyCode !== 'USD'"
|
||||||
selectedMethodCurrencyCode &&
|
>({{
|
||||||
selectedMethodCurrencyCode !== 'USD' &&
|
|
||||||
selectedMethodExchangeRate
|
|
||||||
? (fixedDenominationMin ?? effectiveMinAmount) / selectedMethodExchangeRate
|
|
||||||
: (fixedDenominationMin ?? effectiveMinAmount),
|
|
||||||
)
|
|
||||||
}}<template v-if="selectedMethodCurrencyCode && selectedMethodCurrencyCode !== 'USD'">
|
|
||||||
({{
|
|
||||||
formatAmountForDisplay(
|
formatAmountForDisplay(
|
||||||
fixedDenominationMin ?? effectiveMinAmount,
|
displayMinLocal,
|
||||||
selectedMethodCurrencyCode,
|
selectedMethodCurrencyCode,
|
||||||
selectedMethodExchangeRate,
|
selectedMethodExchangeRate,
|
||||||
)
|
)
|
||||||
}})</template
|
}})</template
|
||||||
>
|
>
|
||||||
min,
|
min<template v-if="displayMinUsd <= roundedMaxAmount"
|
||||||
{{
|
>, {{ formatMoney(displayMaxUsd)
|
||||||
formatMoney(
|
}}<template v-if="selectedMethodCurrencyCode && selectedMethodCurrencyCode !== 'USD'"
|
||||||
selectedMethodCurrencyCode &&
|
>({{
|
||||||
selectedMethodCurrencyCode !== 'USD' &&
|
formatAmountForDisplay(
|
||||||
selectedMethodExchangeRate
|
displayMaxLocal,
|
||||||
? (fixedDenominationMax ??
|
selectedMethodCurrencyCode,
|
||||||
selectedMethodDetails.interval?.standard?.max ??
|
selectedMethodExchangeRate,
|
||||||
effectiveMaxAmount) / selectedMethodExchangeRate
|
)
|
||||||
: (fixedDenominationMax ??
|
}})</template
|
||||||
selectedMethodDetails.interval?.standard?.max ??
|
>
|
||||||
effectiveMaxAmount),
|
max</template
|
||||||
)
|
|
||||||
}}<template v-if="selectedMethodCurrencyCode && selectedMethodCurrencyCode !== 'USD'">
|
|
||||||
({{
|
|
||||||
formatAmountForDisplay(
|
|
||||||
fixedDenominationMax ??
|
|
||||||
selectedMethodDetails.interval?.standard?.max ??
|
|
||||||
effectiveMaxAmount,
|
|
||||||
selectedMethodCurrencyCode,
|
|
||||||
selectedMethodExchangeRate,
|
|
||||||
)
|
|
||||||
}})</template
|
|
||||||
>
|
>
|
||||||
max withdrawal amount.
|
withdrawal amount.
|
||||||
</span>
|
</span>
|
||||||
<span
|
<span
|
||||||
v-if="selectedMethodDetails && effectiveMinAmount > roundedMaxAmount"
|
v-if="selectedMethodDetails && effectiveMinAmount > roundedMaxAmount"
|
||||||
class="text-sm text-red"
|
class="text-sm text-red"
|
||||||
>
|
>
|
||||||
You need at least
|
You need at least
|
||||||
{{
|
{{ formatMoney(displayMinUsd)
|
||||||
formatMoney(
|
}}<template v-if="selectedMethodCurrencyCode && selectedMethodCurrencyCode !== 'USD'"
|
||||||
selectedMethodCurrencyCode &&
|
>({{
|
||||||
selectedMethodCurrencyCode !== 'USD' &&
|
|
||||||
selectedMethodExchangeRate
|
|
||||||
? effectiveMinAmount / selectedMethodExchangeRate
|
|
||||||
: effectiveMinAmount,
|
|
||||||
)
|
|
||||||
}}<template v-if="selectedMethodCurrencyCode && selectedMethodCurrencyCode !== 'USD'">
|
|
||||||
({{
|
|
||||||
formatAmountForDisplay(
|
formatAmountForDisplay(
|
||||||
effectiveMinAmount,
|
displayMinLocal,
|
||||||
selectedMethodCurrencyCode,
|
selectedMethodCurrencyCode,
|
||||||
selectedMethodExchangeRate,
|
selectedMethodExchangeRate,
|
||||||
)
|
)
|
||||||
@@ -307,7 +282,19 @@
|
|||||||
v-if="!useDenominationSuggestions && denominationOptions.length === 0"
|
v-if="!useDenominationSuggestions && denominationOptions.length === 0"
|
||||||
class="text-error text-sm"
|
class="text-error text-sm"
|
||||||
>
|
>
|
||||||
No denominations available for your current balance
|
<template v-if="rawFixedDenominationMin !== null">
|
||||||
|
The minimum denomination is
|
||||||
|
{{
|
||||||
|
formatAmountForDisplay(
|
||||||
|
rawFixedDenominationMin,
|
||||||
|
selectedMethodCurrencyCode,
|
||||||
|
selectedMethodExchangeRate,
|
||||||
|
)
|
||||||
|
}}<template v-if="selectedMethodCurrencyCode && selectedMethodCurrencyCode !== 'USD'">
|
||||||
|
({{ formatMoney(convertToUsd(rawFixedDenominationMin)) }})</template
|
||||||
|
>, which exceeds your balance of {{ formatMoney(roundedMaxAmount) }}.
|
||||||
|
</template>
|
||||||
|
<template v-else>No denominations available for your current balance</template>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -324,7 +311,7 @@
|
|||||||
|
|
||||||
<WithdrawFeeBreakdown
|
<WithdrawFeeBreakdown
|
||||||
v-if="allRequiredFieldsFilled && formData.amount && formData.amount > 0"
|
v-if="allRequiredFieldsFilled && formData.amount && formData.amount > 0"
|
||||||
:amount="formData.amount || 0"
|
:amount="amountForFeeBreakdown"
|
||||||
:fee="calculatedFee"
|
:fee="calculatedFee"
|
||||||
:fee-loading="feeLoading"
|
:fee-loading="feeLoading"
|
||||||
:exchange-rate="showGiftCardSelector ? selectedMethodExchangeRate : giftCardExchangeRate"
|
:exchange-rate="showGiftCardSelector ? selectedMethodExchangeRate : giftCardExchangeRate"
|
||||||
@@ -720,6 +707,34 @@ const hasSelectedDenomination = computed(() => {
|
|||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Convert local currency amount to USD using the selected method's exchange rate
|
||||||
|
const convertToUsd = (localAmount: number): number => {
|
||||||
|
const exchangeRate = selectedMethodExchangeRate.value
|
||||||
|
if (
|
||||||
|
selectedMethodCurrencyCode.value &&
|
||||||
|
selectedMethodCurrencyCode.value !== 'USD' &&
|
||||||
|
exchangeRate &&
|
||||||
|
exchangeRate > 0
|
||||||
|
) {
|
||||||
|
return localAmount / exchangeRate
|
||||||
|
}
|
||||||
|
return localAmount
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert USD amount to local currency using the selected method's exchange rate
|
||||||
|
const convertToLocalCurrency = (usdAmount: number): number => {
|
||||||
|
const exchangeRate = selectedMethodExchangeRate.value
|
||||||
|
if (
|
||||||
|
selectedMethodCurrencyCode.value &&
|
||||||
|
selectedMethodCurrencyCode.value !== 'USD' &&
|
||||||
|
exchangeRate &&
|
||||||
|
exchangeRate > 0
|
||||||
|
) {
|
||||||
|
return usdAmount * exchangeRate
|
||||||
|
}
|
||||||
|
return usdAmount
|
||||||
|
}
|
||||||
|
|
||||||
const denominationOptions = computed(() => {
|
const denominationOptions = computed(() => {
|
||||||
const interval = selectedMethodDetails.value?.interval
|
const interval = selectedMethodDetails.value?.interval
|
||||||
if (!interval) return []
|
if (!interval) return []
|
||||||
@@ -735,30 +750,56 @@ const denominationOptions = computed(() => {
|
|||||||
|
|
||||||
if (values.length === 0) return []
|
if (values.length === 0) return []
|
||||||
|
|
||||||
const filtered = values.filter((amount) => amount <= roundedMaxAmount.value).sort((a, b) => a - b)
|
// Convert USD balance to local currency for comparison with denomination values
|
||||||
|
// (denomination values are in local currency, e.g., 50 INR, 45000 IDR)
|
||||||
|
const maxInLocalCurrency = convertToLocalCurrency(roundedMaxAmount.value)
|
||||||
|
|
||||||
|
const filtered = values.filter((amount) => amount <= maxInLocalCurrency).sort((a, b) => a - b)
|
||||||
debug(
|
debug(
|
||||||
'Denomination options (filtered by max):',
|
'Denomination options (filtered by max):',
|
||||||
filtered,
|
filtered,
|
||||||
'from',
|
'from',
|
||||||
values,
|
values,
|
||||||
'max:',
|
'max (local currency):',
|
||||||
|
maxInLocalCurrency,
|
||||||
|
'max (USD):',
|
||||||
roundedMaxAmount.value,
|
roundedMaxAmount.value,
|
||||||
)
|
)
|
||||||
return filtered
|
return filtered
|
||||||
})
|
})
|
||||||
|
|
||||||
const effectiveMinAmount = computed(() => {
|
const effectiveMinAmount = computed(() => {
|
||||||
return selectedMethodDetails.value?.interval?.standard?.min || 0.01
|
const min = selectedMethodDetails.value?.interval?.standard?.min || 0.01
|
||||||
|
// Convert from local currency to USD for display/validation
|
||||||
|
return convertToUsd(min)
|
||||||
})
|
})
|
||||||
|
|
||||||
const effectiveMaxAmount = computed(() => {
|
const effectiveMaxAmount = computed(() => {
|
||||||
const methodMax = selectedMethodDetails.value?.interval?.standard?.max
|
const methodMax = selectedMethodDetails.value?.interval?.standard?.max
|
||||||
if (methodMax !== undefined && methodMax !== null) {
|
if (methodMax !== undefined && methodMax !== null) {
|
||||||
return Math.min(roundedMaxAmount.value, methodMax)
|
// Convert method max from local currency to USD, then compare with USD balance
|
||||||
|
const methodMaxInUsd = convertToUsd(methodMax)
|
||||||
|
return Math.min(roundedMaxAmount.value, methodMaxInUsd)
|
||||||
}
|
}
|
||||||
return roundedMaxAmount.value
|
return roundedMaxAmount.value
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Get the minimum fixed denomination from the full list (not filtered by user balance)
|
||||||
|
const rawFixedDenominationMin = computed(() => {
|
||||||
|
const interval = selectedMethodDetails.value?.interval
|
||||||
|
if (!interval) return null
|
||||||
|
|
||||||
|
let values: number[] = []
|
||||||
|
if (interval.fixed?.values) {
|
||||||
|
values = [...interval.fixed.values]
|
||||||
|
} else if (interval.standard && interval.standard.min === interval.standard.max) {
|
||||||
|
values = [interval.standard.min]
|
||||||
|
}
|
||||||
|
|
||||||
|
if (values.length === 0) return null
|
||||||
|
return Math.min(...values)
|
||||||
|
})
|
||||||
|
|
||||||
const fixedDenominationMin = computed(() => {
|
const fixedDenominationMin = computed(() => {
|
||||||
if (!useFixedDenominations.value) return null
|
if (!useFixedDenominations.value) return null
|
||||||
const options = denominationOptions.value
|
const options = denominationOptions.value
|
||||||
@@ -773,6 +814,63 @@ const fixedDenominationMax = computed(() => {
|
|||||||
return options[options.length - 1]
|
return options[options.length - 1]
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// - Fixed denominations: convert from local currency to USD
|
||||||
|
// - Variable amounts: effectiveMinAmount/effectiveMaxAmount are already in USD
|
||||||
|
const displayMinUsd = computed(() => {
|
||||||
|
if (fixedDenominationMin.value !== null) {
|
||||||
|
// Fixed denomination is in local currency, convert to USD
|
||||||
|
return convertToUsd(fixedDenominationMin.value)
|
||||||
|
}
|
||||||
|
// If no affordable denominations but there are fixed values, show the raw minimum
|
||||||
|
if (rawFixedDenominationMin.value !== null) {
|
||||||
|
return convertToUsd(rawFixedDenominationMin.value)
|
||||||
|
}
|
||||||
|
// effectiveMinAmount is already in USD
|
||||||
|
return effectiveMinAmount.value
|
||||||
|
})
|
||||||
|
|
||||||
|
const displayMaxUsd = computed(() => {
|
||||||
|
if (fixedDenominationMax.value !== null) {
|
||||||
|
// Fixed denomination is in local currency, convert to USD
|
||||||
|
return convertToUsd(fixedDenominationMax.value)
|
||||||
|
}
|
||||||
|
// For variable amounts, use effectiveMaxAmount (already in USD)
|
||||||
|
// But also check if there's a method max from the interval
|
||||||
|
const methodMax = selectedMethodDetails.value?.interval?.standard?.max
|
||||||
|
if (methodMax !== undefined && methodMax !== null) {
|
||||||
|
const methodMaxUsd = convertToUsd(methodMax)
|
||||||
|
return Math.min(effectiveMaxAmount.value, methodMaxUsd)
|
||||||
|
}
|
||||||
|
return effectiveMaxAmount.value
|
||||||
|
})
|
||||||
|
|
||||||
|
// Display values in local currency (for showing in parentheses)
|
||||||
|
const displayMinLocal = computed(() => {
|
||||||
|
if (fixedDenominationMin.value !== null) {
|
||||||
|
// Fixed denomination is already in local currency
|
||||||
|
return fixedDenominationMin.value
|
||||||
|
}
|
||||||
|
// If no affordable denominations but there are fixed values, show the raw minimum
|
||||||
|
if (rawFixedDenominationMin.value !== null) {
|
||||||
|
return rawFixedDenominationMin.value
|
||||||
|
}
|
||||||
|
// Convert USD to local currency
|
||||||
|
return convertToLocalCurrency(effectiveMinAmount.value)
|
||||||
|
})
|
||||||
|
|
||||||
|
const displayMaxLocal = computed(() => {
|
||||||
|
if (fixedDenominationMax.value !== null) {
|
||||||
|
// Fixed denomination is already in local currency
|
||||||
|
return fixedDenominationMax.value
|
||||||
|
}
|
||||||
|
// Check for method max
|
||||||
|
const methodMax = selectedMethodDetails.value?.interval?.standard?.max
|
||||||
|
if (methodMax !== undefined && methodMax !== null) {
|
||||||
|
return Math.min(convertToLocalCurrency(effectiveMaxAmount.value), methodMax)
|
||||||
|
}
|
||||||
|
return convertToLocalCurrency(effectiveMaxAmount.value)
|
||||||
|
})
|
||||||
|
|
||||||
const selectedDenomination = computed({
|
const selectedDenomination = computed({
|
||||||
get: () => formData.value.amount,
|
get: () => formData.value.amount,
|
||||||
set: (value) => {
|
set: (value) => {
|
||||||
@@ -791,6 +889,21 @@ const allRequiredFieldsFilled = computed(() => {
|
|||||||
return true
|
return true
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Amount to display in WithdrawFeeBreakdown (expects local currency for gift cards)
|
||||||
|
const amountForFeeBreakdown = computed(() => {
|
||||||
|
const amount = formData.value.amount ?? 0
|
||||||
|
if (!showGiftCardSelector.value) {
|
||||||
|
// Non-gift-card: amount is in USD
|
||||||
|
return amount
|
||||||
|
}
|
||||||
|
if (useFixedDenominations.value) {
|
||||||
|
// Fixed denominations: amount is already in local currency
|
||||||
|
return amount
|
||||||
|
}
|
||||||
|
// Variable amount gift card: amount is in USD, convert to local currency
|
||||||
|
return convertToLocalCurrency(amount)
|
||||||
|
})
|
||||||
|
|
||||||
const calculateFeesDebounced = useDebounceFn(async () => {
|
const calculateFeesDebounced = useDebounceFn(async () => {
|
||||||
const amount = formData.value.amount
|
const amount = formData.value.amount
|
||||||
if (!amount || amount <= 0) {
|
if (!amount || amount <= 0) {
|
||||||
@@ -852,7 +965,15 @@ watch(
|
|||||||
watch(
|
watch(
|
||||||
[() => formData.value.amount, selectedGiftCardId, deliveryEmail, selectedCurrency],
|
[() => formData.value.amount, selectedGiftCardId, deliveryEmail, selectedCurrency],
|
||||||
() => {
|
() => {
|
||||||
withdrawData.value.calculation.amount = formData.value.amount ?? 0
|
let amountForBackend = formData.value.amount ?? 0
|
||||||
|
|
||||||
|
// - Fixed denominations (chips): formData.amount is already in local currency
|
||||||
|
// - Variable amounts (RevenueInputField): formData.amount is in USD, needs conversion
|
||||||
|
if (showGiftCardSelector.value && !useFixedDenominations.value && amountForBackend > 0) {
|
||||||
|
amountForBackend = convertToLocalCurrency(amountForBackend)
|
||||||
|
}
|
||||||
|
|
||||||
|
withdrawData.value.calculation.amount = amountForBackend
|
||||||
|
|
||||||
if (showGiftCardSelector.value && selectedGiftCardId.value) {
|
if (showGiftCardSelector.value && selectedGiftCardId.value) {
|
||||||
withdrawData.value.selection.methodId = selectedGiftCardId.value
|
withdrawData.value.selection.methodId = selectedGiftCardId.value
|
||||||
|
|||||||
@@ -651,15 +651,27 @@ export function createWithdrawContext(
|
|||||||
)
|
)
|
||||||
|
|
||||||
if (selectedMethod?.interval) {
|
if (selectedMethod?.interval) {
|
||||||
const userMax = Math.floor(maxWithdrawAmount.value * 100) / 100
|
const userMaxUsd = Math.floor(maxWithdrawAmount.value * 100) / 100
|
||||||
|
|
||||||
|
const exchangeRate = selectedMethod.exchange_rate
|
||||||
|
const isNonUsdCurrency =
|
||||||
|
selectedMethod.currency_code &&
|
||||||
|
selectedMethod.currency_code !== 'USD' &&
|
||||||
|
exchangeRate &&
|
||||||
|
exchangeRate > 0
|
||||||
|
|
||||||
|
const userMaxInLocalCurrency = isNonUsdCurrency ? userMaxUsd * exchangeRate : userMaxUsd
|
||||||
|
|
||||||
if (selectedMethod.interval.standard) {
|
if (selectedMethod.interval.standard) {
|
||||||
const { min, max } = selectedMethod.interval.standard
|
const { min, max } = selectedMethod.interval.standard
|
||||||
const effectiveMax = Math.min(userMax, max)
|
const effectiveMax = Math.min(userMaxInLocalCurrency, max)
|
||||||
const effectiveMin = Math.min(min, effectiveMax)
|
const effectiveMin = Math.min(min, effectiveMax)
|
||||||
if (amount < effectiveMin || amount > effectiveMax) return false
|
if (amount < effectiveMin || amount > effectiveMax) return false
|
||||||
}
|
}
|
||||||
if (selectedMethod.interval.fixed) {
|
if (selectedMethod.interval.fixed) {
|
||||||
const validValues = selectedMethod.interval.fixed.values.filter((v) => v <= userMax)
|
const validValues = selectedMethod.interval.fixed.values.filter(
|
||||||
|
(v) => v <= userMaxInLocalCurrency,
|
||||||
|
)
|
||||||
if (!validValues.includes(amount)) return false
|
if (!validValues.includes(amount)) return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -817,6 +829,7 @@ export function createWithdrawContext(
|
|||||||
calculation: {
|
calculation: {
|
||||||
amount: 0,
|
amount: 0,
|
||||||
fee: null,
|
fee: null,
|
||||||
|
netUsd: null,
|
||||||
exchangeRate: null,
|
exchangeRate: null,
|
||||||
},
|
},
|
||||||
providerData: {
|
providerData: {
|
||||||
|
|||||||
@@ -498,9 +498,13 @@ pub async fn create_payout(
|
|||||||
let balance = get_user_balance(user.id, &pool)
|
let balance = get_user_balance(user.id, &pool)
|
||||||
.await
|
.await
|
||||||
.wrap_internal_err("failed to calculate user balance")?;
|
.wrap_internal_err("failed to calculate user balance")?;
|
||||||
if balance.available < body.amount || body.amount < Decimal::ZERO {
|
|
||||||
|
// Note: We only check for negative amounts here. The full balance validation
|
||||||
|
// happens later in payout_flow.validate() which correctly handles currency
|
||||||
|
// conversion (body.amount may be in local currency for gift cards, not USD).
|
||||||
|
if body.amount < Decimal::ZERO {
|
||||||
return Err(ApiError::InvalidInput(
|
return Err(ApiError::InvalidInput(
|
||||||
"You do not have enough funds to make this payout!".to_string(),
|
"Amount must be positive!".to_string(),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user