You've already forked AstralRinth
forked from xxxOFFxxx/AstralRinth
Improve support for non-USD Tremendous gift cards (#4887)
* Improve support for non-USD Tremendous gift cards * add forex info to tremendous payout methods * fix: partially fix DEV-535 * feat: wip * eur/usd to usd/eur * feat: better denom picking * feat: qa changes * fix: intl --------- Co-authored-by: Calum H. (IMB11) <contact@cal.engineer> Co-authored-by: Prospector <6166773+Prospector@users.noreply.github.com>
This commit is contained in:
@@ -111,28 +111,163 @@
|
||||
</Combobox>
|
||||
</div>
|
||||
<span v-if="selectedMethodDetails" class="text-secondary">
|
||||
{{ formatMoney(effectiveMinAmount) }} min,
|
||||
{{ formatMoney(selectedMethodDetails.interval?.standard?.max ?? effectiveMaxAmount) }}
|
||||
{{ formatMoney(fixedDenominationMin ?? effectiveMinAmount)
|
||||
}}<template v-if="selectedMethodCurrencyCode && selectedMethodCurrencyCode !== 'USD'">
|
||||
({{
|
||||
formatAmountForDisplay(
|
||||
fixedDenominationMin ?? effectiveMinAmount,
|
||||
selectedMethodCurrencyCode,
|
||||
selectedMethodExchangeRate,
|
||||
)
|
||||
}})</template
|
||||
>
|
||||
min,
|
||||
{{
|
||||
formatMoney(
|
||||
fixedDenominationMax ??
|
||||
selectedMethodDetails.interval?.standard?.max ??
|
||||
effectiveMaxAmount,
|
||||
)
|
||||
}}<template v-if="selectedMethodCurrencyCode && selectedMethodCurrencyCode !== 'USD'">
|
||||
({{
|
||||
formatAmountForDisplay(
|
||||
fixedDenominationMax ??
|
||||
selectedMethodDetails.interval?.standard?.max ??
|
||||
effectiveMaxAmount,
|
||||
selectedMethodCurrencyCode,
|
||||
selectedMethodExchangeRate,
|
||||
)
|
||||
}})</template
|
||||
>
|
||||
max withdrawal amount.
|
||||
</span>
|
||||
<span
|
||||
v-if="selectedMethodDetails && effectiveMinAmount > roundedMaxAmount"
|
||||
class="text-sm text-red"
|
||||
>
|
||||
You need at least {{ formatMoney(effectiveMinAmount)
|
||||
}}<template v-if="selectedMethodCurrencyCode && selectedMethodCurrencyCode !== 'USD'">
|
||||
({{
|
||||
formatAmountForDisplay(
|
||||
effectiveMinAmount,
|
||||
selectedMethodCurrencyCode,
|
||||
selectedMethodExchangeRate,
|
||||
)
|
||||
}})</template
|
||||
>
|
||||
to use this gift card.
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-2.5">
|
||||
<label>
|
||||
<span class="text-md font-semibold text-contrast"
|
||||
>{{ formatMessage(formFieldLabels.amount) }} <span class="text-red">*</span></span
|
||||
>{{ formatMessage(formFieldLabels.amount) }}
|
||||
<template v-if="useDenominationSuggestions"> ({{ selectedMethodCurrencyCode }})</template>
|
||||
<span class="text-red">*</span></span
|
||||
>
|
||||
</label>
|
||||
|
||||
<div v-if="showGiftCardSelector && useFixedDenominations" class="flex flex-col gap-2.5">
|
||||
<Chips
|
||||
v-model="selectedDenomination"
|
||||
:items="denominationOptions"
|
||||
:format-label="(amt: number) => formatMoney(amt)"
|
||||
:never-empty="false"
|
||||
:capitalize="false"
|
||||
/>
|
||||
<span v-if="denominationOptions.length === 0" class="text-error text-sm">
|
||||
<template v-if="useDenominationSuggestions">
|
||||
<input
|
||||
v-model.number="denominationSearchInput"
|
||||
type="number"
|
||||
step="0.01"
|
||||
:min="0"
|
||||
:placeholder="formatMessage(messages.enterDenominationPlaceholder)"
|
||||
class="w-full rounded-[14px] bg-surface-4 px-4 py-3 text-contrast placeholder:text-secondary sm:py-2.5"
|
||||
@input="hasTouchedSuggestions = true"
|
||||
/>
|
||||
<Transition
|
||||
enter-active-class="transition-opacity duration-200 ease-out"
|
||||
enter-from-class="opacity-0"
|
||||
enter-to-class="opacity-100"
|
||||
leave-active-class="transition-opacity duration-150 ease-in"
|
||||
leave-from-class="opacity-100"
|
||||
leave-to-class="opacity-0"
|
||||
>
|
||||
<span v-if="!denominationSearchInput" class="text-sm text-secondary">
|
||||
{{ formatMessage(messages.enterAmountHint) }}
|
||||
</span>
|
||||
</Transition>
|
||||
</template>
|
||||
|
||||
<Transition
|
||||
enter-active-class="transition-all duration-300 ease-out"
|
||||
enter-from-class="opacity-0 max-h-0"
|
||||
enter-to-class="opacity-100 max-h-96"
|
||||
leave-active-class="transition-all duration-200 ease-in"
|
||||
leave-from-class="opacity-100 max-h-96"
|
||||
leave-to-class="opacity-0 max-h-0"
|
||||
>
|
||||
<div
|
||||
v-if="!useDenominationSuggestions || displayedSuggestions.length > 0"
|
||||
class="overflow-hidden"
|
||||
:class="[useDenominationSuggestions ? 'p-[2px]' : '']"
|
||||
>
|
||||
<Chips
|
||||
v-model="selectedDenomination"
|
||||
:items="useDenominationSuggestions ? displayedSuggestions : denominationOptions"
|
||||
:format-label="
|
||||
(amt: number) =>
|
||||
formatAmountForDisplay(
|
||||
amt,
|
||||
selectedMethodCurrencyCode,
|
||||
selectedMethodExchangeRate,
|
||||
)
|
||||
"
|
||||
:never-empty="false"
|
||||
:capitalize="false"
|
||||
/>
|
||||
</div>
|
||||
</Transition>
|
||||
|
||||
<Transition
|
||||
enter-active-class="transition-opacity duration-200 ease-out"
|
||||
enter-from-class="opacity-0"
|
||||
enter-to-class="opacity-100"
|
||||
leave-active-class="transition-opacity duration-150 ease-in"
|
||||
leave-from-class="opacity-100"
|
||||
leave-to-class="opacity-0"
|
||||
>
|
||||
<span
|
||||
v-if="
|
||||
useDenominationSuggestions &&
|
||||
denominationSearchInput &&
|
||||
displayedSuggestions.length === 0
|
||||
"
|
||||
class="text-sm text-secondary"
|
||||
>
|
||||
{{ noSuggestionsMessage }}
|
||||
</span>
|
||||
</Transition>
|
||||
|
||||
<Transition
|
||||
enter-active-class="transition-opacity duration-200 ease-out"
|
||||
enter-from-class="opacity-0"
|
||||
enter-to-class="opacity-100"
|
||||
leave-active-class="transition-opacity duration-150 ease-in"
|
||||
leave-from-class="opacity-100"
|
||||
leave-to-class="opacity-0"
|
||||
>
|
||||
<span
|
||||
v-if="
|
||||
useDenominationSuggestions &&
|
||||
hasTouchedSuggestions &&
|
||||
displayedSuggestions.length > 0 &&
|
||||
!hasSelectedDenomination
|
||||
"
|
||||
class="text-sm text-orange"
|
||||
>
|
||||
{{ formatMessage(messages.selectDenominationRequired) }}
|
||||
</span>
|
||||
</Transition>
|
||||
|
||||
<span
|
||||
v-if="!useDenominationSuggestions && denominationOptions.length === 0"
|
||||
class="text-error text-sm"
|
||||
>
|
||||
No denominations available for your current balance
|
||||
</span>
|
||||
</div>
|
||||
@@ -153,8 +288,8 @@
|
||||
:amount="formData.amount || 0"
|
||||
:fee="calculatedFee"
|
||||
:fee-loading="feeLoading"
|
||||
:exchange-rate="exchangeRate"
|
||||
:local-currency="showPayPalCurrencySelector ? selectedCurrency : undefined"
|
||||
:exchange-rate="giftCardExchangeRate"
|
||||
:local-currency="giftCardCurrencyCode"
|
||||
/>
|
||||
|
||||
<Checkbox v-model="agreedTerms">
|
||||
@@ -285,6 +420,9 @@ const formData = ref<Record<string, any>>({
|
||||
|
||||
const selectedGiftCardId = ref<string | null>(withdrawData.value.selection.methodId || null)
|
||||
|
||||
const denominationSearchInput = ref<number | undefined>(undefined)
|
||||
const hasTouchedSuggestions = ref(false)
|
||||
|
||||
const currencyOptions = [
|
||||
{ value: 'USD', label: 'USD' },
|
||||
{ value: 'AUD', label: 'AUD' },
|
||||
@@ -373,6 +511,8 @@ const rewardOptions = ref<
|
||||
fixed?: { values: number[] }
|
||||
standard?: { min: number; max: number }
|
||||
}
|
||||
currencyCode?: string | null
|
||||
exchangeRate?: number | null
|
||||
}
|
||||
}>
|
||||
>([])
|
||||
@@ -390,24 +530,187 @@ const selectedMethodDetails = computed(() => {
|
||||
return option?.methodDetails || null
|
||||
})
|
||||
|
||||
const selectedMethodCurrencyCode = computed(() => selectedMethodDetails.value?.currencyCode || null)
|
||||
const selectedMethodExchangeRate = computed(() => selectedMethodDetails.value?.exchangeRate || null)
|
||||
|
||||
const giftCardCurrencyCode = computed(() => {
|
||||
if (showPayPalCurrencySelector.value) {
|
||||
return selectedCurrency.value !== 'USD' ? selectedCurrency.value : undefined
|
||||
}
|
||||
|
||||
if (
|
||||
showGiftCardSelector.value &&
|
||||
selectedMethodCurrencyCode.value &&
|
||||
selectedMethodCurrencyCode.value !== 'USD'
|
||||
) {
|
||||
return selectedMethodCurrencyCode.value
|
||||
}
|
||||
return undefined
|
||||
})
|
||||
|
||||
const giftCardExchangeRate = computed(() => {
|
||||
if (showPayPalCurrencySelector.value) {
|
||||
return exchangeRate.value
|
||||
}
|
||||
|
||||
if (
|
||||
showGiftCardSelector.value &&
|
||||
selectedMethodCurrencyCode.value &&
|
||||
selectedMethodCurrencyCode.value !== 'USD'
|
||||
) {
|
||||
return selectedMethodExchangeRate.value
|
||||
}
|
||||
return exchangeRate.value
|
||||
})
|
||||
|
||||
function formatAmountForDisplay(
|
||||
usdAmount: number,
|
||||
currencyCode: string | null | undefined,
|
||||
rate: number | null | undefined,
|
||||
): string {
|
||||
if (!currencyCode || currencyCode === 'USD' || !rate) {
|
||||
return formatMoney(usdAmount)
|
||||
}
|
||||
const localAmount = usdAmount * rate
|
||||
try {
|
||||
return new Intl.NumberFormat('en-US', {
|
||||
style: 'currency',
|
||||
currency: currencyCode,
|
||||
minimumFractionDigits: 2,
|
||||
maximumFractionDigits: 2,
|
||||
}).format(localAmount)
|
||||
} catch {
|
||||
return `${currencyCode} ${localAmount.toFixed(2)}`
|
||||
}
|
||||
}
|
||||
|
||||
const useFixedDenominations = computed(() => {
|
||||
const hasFixed = !!selectedMethodDetails.value?.interval?.fixed?.values
|
||||
debug('Use fixed denominations:', hasFixed, selectedMethodDetails.value?.interval)
|
||||
return hasFixed
|
||||
const interval = selectedMethodDetails.value?.interval
|
||||
if (!interval) return false
|
||||
|
||||
if (interval.fixed?.values?.length) {
|
||||
debug('Use fixed denominations: true (has fixed values)')
|
||||
return true
|
||||
}
|
||||
|
||||
// treat min=max as single fixed value
|
||||
if (interval.standard) {
|
||||
const { min, max } = interval.standard
|
||||
const isSingleValue = min === max
|
||||
debug('Use fixed denominations:', isSingleValue, '(min=max:', min, '=', max, ')')
|
||||
return isSingleValue
|
||||
}
|
||||
return false
|
||||
})
|
||||
|
||||
const useDenominationSuggestions = computed(() => {
|
||||
if (!useFixedDenominations.value) return false
|
||||
const interval = selectedMethodDetails.value?.interval
|
||||
if (!interval?.fixed?.values) return false
|
||||
return interval.fixed.values.length > 35
|
||||
})
|
||||
|
||||
const denominationSuggestions = computed(() => {
|
||||
const input = denominationSearchInput.value
|
||||
if (!input || input <= 0) return []
|
||||
|
||||
const allDenominations = denominationOptions.value
|
||||
if (allDenominations.length === 0) return []
|
||||
|
||||
// convert local currency input to USD for filtering (denominations are stored in USD)
|
||||
const exchangeRate = selectedMethodExchangeRate.value
|
||||
const inputInUsd = exchangeRate ? input / exchangeRate : input
|
||||
|
||||
const rangeSize = inputInUsd * 0.2
|
||||
let lowerBound = inputInUsd - rangeSize / 2
|
||||
let upperBound = inputInUsd + rangeSize / 2
|
||||
|
||||
const minAvailable = allDenominations[0]
|
||||
const maxAvailable = allDenominations[allDenominations.length - 1]
|
||||
|
||||
// shift range when hitting boundaries to maintain ~20% total range
|
||||
if (upperBound > maxAvailable) {
|
||||
const overflow = upperBound - maxAvailable
|
||||
upperBound = maxAvailable
|
||||
lowerBound = Math.max(minAvailable, lowerBound - overflow)
|
||||
} else if (lowerBound < minAvailable) {
|
||||
const underflow = minAvailable - lowerBound
|
||||
lowerBound = minAvailable
|
||||
upperBound = Math.min(maxAvailable, upperBound + underflow)
|
||||
}
|
||||
|
||||
return allDenominations
|
||||
.filter((amt) => amt >= lowerBound && amt <= upperBound)
|
||||
.sort((a, b) => a - b)
|
||||
})
|
||||
|
||||
const maxDisplayedSuggestions = 10
|
||||
const displayedSuggestions = computed(() => {
|
||||
const all = denominationSuggestions.value
|
||||
if (all.length <= maxDisplayedSuggestions) return all
|
||||
|
||||
const input = denominationSearchInput.value
|
||||
if (!input) return all.slice(0, maxDisplayedSuggestions)
|
||||
|
||||
const exchangeRate = selectedMethodExchangeRate.value
|
||||
const inputInUsd = exchangeRate ? input / exchangeRate : input
|
||||
|
||||
// select values closest to input, then sort ascending for display
|
||||
const closest = [...all]
|
||||
.sort((a, b) => Math.abs(a - inputInUsd) - Math.abs(b - inputInUsd))
|
||||
.slice(0, maxDisplayedSuggestions)
|
||||
|
||||
return closest.sort((a, b) => a - b)
|
||||
})
|
||||
|
||||
const noSuggestionsMessage = computed(() => {
|
||||
if (!denominationSearchInput.value || denominationSearchInput.value <= 0) {
|
||||
return null
|
||||
}
|
||||
if (denominationSuggestions.value.length === 0) {
|
||||
const maxDenom = fixedDenominationMax.value
|
||||
if (maxDenom) {
|
||||
const maxInLocal = formatAmountForDisplay(
|
||||
maxDenom,
|
||||
selectedMethodCurrencyCode.value,
|
||||
selectedMethodExchangeRate.value,
|
||||
)
|
||||
return `No denominations near this amount. The highest available is ${maxInLocal}.`
|
||||
}
|
||||
return 'No denominations near this amount'
|
||||
}
|
||||
return null
|
||||
})
|
||||
|
||||
const hasSelectedDenomination = computed(() => {
|
||||
return (
|
||||
formData.value.amount !== undefined &&
|
||||
formData.value.amount > 0 &&
|
||||
denominationOptions.value.includes(formData.value.amount)
|
||||
)
|
||||
})
|
||||
|
||||
const denominationOptions = computed(() => {
|
||||
const fixedValues = selectedMethodDetails.value?.interval?.fixed?.values
|
||||
if (!fixedValues) return []
|
||||
const interval = selectedMethodDetails.value?.interval
|
||||
if (!interval) return []
|
||||
|
||||
const filtered = fixedValues
|
||||
.filter((amount) => amount <= roundedMaxAmount.value)
|
||||
.sort((a, b) => a - b)
|
||||
let values: number[] = []
|
||||
|
||||
if (interval.fixed?.values) {
|
||||
values = [...interval.fixed.values]
|
||||
} else if (interval.standard && interval.standard.min === interval.standard.max) {
|
||||
// min=max case: treat as single fixed value
|
||||
values = [interval.standard.min]
|
||||
}
|
||||
|
||||
if (values.length === 0) return []
|
||||
|
||||
const filtered = values.filter((amount) => amount <= roundedMaxAmount.value).sort((a, b) => a - b)
|
||||
debug(
|
||||
'Denomination options (filtered by max):',
|
||||
filtered,
|
||||
'from',
|
||||
fixedValues,
|
||||
values,
|
||||
'max:',
|
||||
roundedMaxAmount.value,
|
||||
)
|
||||
@@ -426,6 +729,20 @@ const effectiveMaxAmount = computed(() => {
|
||||
return roundedMaxAmount.value
|
||||
})
|
||||
|
||||
const fixedDenominationMin = computed(() => {
|
||||
if (!useFixedDenominations.value) return null
|
||||
const options = denominationOptions.value
|
||||
if (options.length === 0) return null
|
||||
return options[0]
|
||||
})
|
||||
|
||||
const fixedDenominationMax = computed(() => {
|
||||
if (!useFixedDenominations.value) return null
|
||||
const options = denominationOptions.value
|
||||
if (options.length === 0) return null
|
||||
return options[options.length - 1]
|
||||
})
|
||||
|
||||
const selectedDenomination = computed({
|
||||
get: () => formData.value.amount,
|
||||
set: (value) => {
|
||||
@@ -542,6 +859,8 @@ onMounted(async () => {
|
||||
id: m.id,
|
||||
name: m.name,
|
||||
interval: m.interval,
|
||||
currencyCode: m.currency_code,
|
||||
exchangeRate: m.exchange_rate,
|
||||
},
|
||||
}))
|
||||
|
||||
@@ -564,6 +883,8 @@ watch(
|
||||
selectedGiftCardId.value = null
|
||||
calculatedFee.value = 0
|
||||
exchangeRate.value = null
|
||||
denominationSearchInput.value = undefined
|
||||
hasTouchedSuggestions.value = false
|
||||
|
||||
// Clear currency when switching away from PayPal International
|
||||
if (newMethod !== 'paypal' && withdrawData.value.providerData.type === 'tremendous') {
|
||||
@@ -573,6 +894,15 @@ watch(
|
||||
},
|
||||
)
|
||||
|
||||
watch(selectedGiftCardId, (newId, oldId) => {
|
||||
if (oldId && newId !== oldId) {
|
||||
// Reset denomination search when gift card changes
|
||||
denominationSearchInput.value = undefined
|
||||
hasTouchedSuggestions.value = false
|
||||
formData.value.amount = undefined
|
||||
}
|
||||
})
|
||||
|
||||
async function switchToDirectPaypal() {
|
||||
withdrawData.value.selection.country = {
|
||||
id: 'US',
|
||||
@@ -649,5 +979,21 @@ const messages = defineMessages({
|
||||
defaultMessage:
|
||||
'You selected USD for PayPal International. <direct-paypal-link>Switch to direct PayPal</direct-paypal-link> for better fees (≈2% instead of ≈6%).',
|
||||
},
|
||||
enterDenominationPlaceholder: {
|
||||
id: 'dashboard.creator-withdraw-modal.tremendous-details.enter-denomination-placeholder',
|
||||
defaultMessage: 'Enter amount',
|
||||
},
|
||||
enterAmountHint: {
|
||||
id: 'dashboard.creator-withdraw-modal.tremendous-details.enter-amount-hint',
|
||||
defaultMessage: 'Enter an amount to see available gift card denominations',
|
||||
},
|
||||
selectDenominationHint: {
|
||||
id: 'dashboard.creator-withdraw-modal.tremendous-details.select-denomination-hint',
|
||||
defaultMessage: 'Select a denomination:',
|
||||
},
|
||||
selectDenominationRequired: {
|
||||
id: 'dashboard.creator-withdraw-modal.tremendous-details.select-denomination-required',
|
||||
defaultMessage: 'Please select a denomination to continue',
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -812,6 +812,12 @@
|
||||
"dashboard.creator-withdraw-modal.tax-form-required.header": {
|
||||
"message": "Tax form required"
|
||||
},
|
||||
"dashboard.creator-withdraw-modal.tremendous-details.enter-amount-hint": {
|
||||
"message": "Enter an amount to see available gift card denominations"
|
||||
},
|
||||
"dashboard.creator-withdraw-modal.tremendous-details.enter-denomination-placeholder": {
|
||||
"message": "Enter amount"
|
||||
},
|
||||
"dashboard.creator-withdraw-modal.tremendous-details.payment-method": {
|
||||
"message": "Payment method"
|
||||
},
|
||||
@@ -824,6 +830,12 @@
|
||||
"dashboard.creator-withdraw-modal.tremendous-details.reward-plural": {
|
||||
"message": "Rewards"
|
||||
},
|
||||
"dashboard.creator-withdraw-modal.tremendous-details.select-denomination-hint": {
|
||||
"message": "Select a denomination:"
|
||||
},
|
||||
"dashboard.creator-withdraw-modal.tremendous-details.select-denomination-required": {
|
||||
"message": "Please select a denomination to continue"
|
||||
},
|
||||
"dashboard.creator-withdraw-modal.tremendous-details.unverified-email-header": {
|
||||
"message": "Unverified email"
|
||||
},
|
||||
|
||||
@@ -58,6 +58,8 @@ export interface PayoutMethod {
|
||||
fiat?: string | null
|
||||
blockchain?: string[]
|
||||
}
|
||||
currency_code?: string | null
|
||||
exchange_rate?: number | null
|
||||
}
|
||||
|
||||
export interface PaymentOption {
|
||||
@@ -284,9 +286,12 @@ interface PayoutPayload {
|
||||
}
|
||||
|
||||
function buildPayoutPayload(data: WithdrawData): PayoutPayload {
|
||||
// Round amount to 2 decimal places for API
|
||||
const amount = Math.round(data.calculation.amount * 100) / 100
|
||||
|
||||
if (data.selection.provider === 'paypal' || data.selection.provider === 'venmo') {
|
||||
return {
|
||||
amount: data.calculation.amount,
|
||||
amount,
|
||||
method: data.selection.provider,
|
||||
method_id: data.selection.methodId!,
|
||||
}
|
||||
@@ -301,7 +306,7 @@ function buildPayoutPayload(data: WithdrawData): PayoutPayload {
|
||||
methodDetails.currency = data.providerData.currency
|
||||
}
|
||||
return {
|
||||
amount: data.calculation.amount,
|
||||
amount,
|
||||
method: 'tremendous',
|
||||
method_id: data.selection.methodId!,
|
||||
method_details: methodDetails,
|
||||
@@ -317,7 +322,7 @@ function buildPayoutPayload(data: WithdrawData): PayoutPayload {
|
||||
|
||||
if (rail.type === 'crypto') {
|
||||
return {
|
||||
amount: data.calculation.amount,
|
||||
amount,
|
||||
method: 'muralpay',
|
||||
method_id: data.selection.methodId!,
|
||||
method_details: {
|
||||
@@ -346,7 +351,7 @@ function buildPayoutPayload(data: WithdrawData): PayoutPayload {
|
||||
}
|
||||
|
||||
return {
|
||||
amount: data.calculation.amount,
|
||||
amount,
|
||||
method: 'muralpay',
|
||||
method_id: data.selection.methodId!,
|
||||
method_details: {
|
||||
@@ -480,7 +485,7 @@ export function createWithdrawContext(
|
||||
label: paymentMethodMessages.paypalInternational,
|
||||
icon: PayPalColorIcon,
|
||||
methodId: internationalPaypalMethod.id,
|
||||
fee: '≈ 3.84%',
|
||||
fee: '≈ 3.84%, min $0.25',
|
||||
type: 'tremendous',
|
||||
})
|
||||
}
|
||||
@@ -642,12 +647,16 @@ export function createWithdrawContext(
|
||||
)
|
||||
|
||||
if (selectedMethod?.interval) {
|
||||
const userMax = Math.floor(maxWithdrawAmount.value * 100) / 100
|
||||
if (selectedMethod.interval.standard) {
|
||||
const { min, max } = selectedMethod.interval.standard
|
||||
if (amount < min || amount > max) return false
|
||||
const effectiveMax = Math.min(userMax, max)
|
||||
const effectiveMin = Math.min(min, effectiveMax)
|
||||
if (amount < effectiveMin || amount > effectiveMax) return false
|
||||
}
|
||||
if (selectedMethod.interval.fixed) {
|
||||
if (!selectedMethod.interval.fixed.values.includes(amount)) return false
|
||||
const validValues = selectedMethod.interval.fixed.values.filter((v) => v <= userMax)
|
||||
if (!validValues.includes(amount)) return false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -711,7 +720,11 @@ export function createWithdrawContext(
|
||||
)
|
||||
if (selectedMethod?.interval?.standard) {
|
||||
const { min, max } = selectedMethod.interval.standard
|
||||
if (amount < min || amount > max) return false
|
||||
// Use effective limits that account for user's available balance
|
||||
const userMax = Math.floor(maxWithdrawAmount.value * 100) / 100
|
||||
const effectiveMax = Math.min(userMax, max)
|
||||
const effectiveMin = Math.min(min, effectiveMax)
|
||||
if (amount < effectiveMin || amount > effectiveMax) return false
|
||||
}
|
||||
|
||||
const accountDetails = withdrawData.value.providerData.accountDetails
|
||||
@@ -736,7 +749,11 @@ export function createWithdrawContext(
|
||||
)
|
||||
if (selectedMethod?.interval?.standard) {
|
||||
const { min, max } = selectedMethod.interval.standard
|
||||
if (amount < min || amount > max) return false
|
||||
// Use effective limits that account for user's available balance
|
||||
const userMax = Math.floor(maxWithdrawAmount.value * 100) / 100
|
||||
const effectiveMax = Math.min(userMax, max)
|
||||
const effectiveMin = Math.min(min, effectiveMax)
|
||||
if (amount < effectiveMin || amount > effectiveMax) return false
|
||||
}
|
||||
|
||||
return !!withdrawData.value.stageValidation?.paypalDetails
|
||||
|
||||
@@ -239,6 +239,10 @@ pub struct PayoutMethod {
|
||||
pub image_logo_url: Option<String>,
|
||||
pub interval: PayoutInterval,
|
||||
pub fee: PayoutMethodFee,
|
||||
pub currency_code: Option<String>,
|
||||
/// USD to the given `currency_code`.
|
||||
#[serde(with = "rust_decimal::serde::float_option")]
|
||||
pub exchange_rate: Option<Decimal>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone)]
|
||||
|
||||
@@ -157,6 +157,8 @@ fn create_muralpay_methods() -> Vec<PayoutMethod> {
|
||||
min: Decimal::ZERO,
|
||||
max: Some(Decimal::ZERO),
|
||||
},
|
||||
currency_code: None,
|
||||
exchange_rate: None,
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
@@ -450,6 +452,8 @@ impl PayoutsQueue {
|
||||
min: Decimal::from(1) / Decimal::from(4),
|
||||
max: Some(Decimal::from(1)),
|
||||
},
|
||||
currency_code: None,
|
||||
exchange_rate: None,
|
||||
};
|
||||
|
||||
let mut venmo = paypal_us.clone();
|
||||
@@ -817,13 +821,19 @@ async fn get_tremendous_payout_methods(
|
||||
products: Vec<Product>,
|
||||
}
|
||||
|
||||
let forex: TremendousForexResponse = queue
|
||||
.make_tremendous_request(Method::GET, "forex", None::<()>)
|
||||
.await
|
||||
.wrap_err("failed to fetch Tremendous forex data")?;
|
||||
|
||||
let response = queue
|
||||
.make_tremendous_request::<(), TremendousResponse>(
|
||||
Method::GET,
|
||||
"products",
|
||||
None,
|
||||
)
|
||||
.await?;
|
||||
.await
|
||||
.wrap_err("failed to fetch Tremendous products data")?;
|
||||
|
||||
let mut methods = Vec::new();
|
||||
|
||||
@@ -903,6 +913,16 @@ async fn get_tremendous_payout_methods(
|
||||
},
|
||||
};
|
||||
|
||||
let Some(currency) = product.currency_codes.first() else {
|
||||
// cards with multiple currencies are not supported
|
||||
continue;
|
||||
};
|
||||
let Some(&usd_to_currency) = forex.forex.get(currency) else {
|
||||
warn!("No Tremendous forex data for {currency}");
|
||||
continue;
|
||||
};
|
||||
let currency_to_usd = dec!(1) / usd_to_currency;
|
||||
|
||||
let method = PayoutMethod {
|
||||
id: product.id,
|
||||
type_: PayoutMethodType::Tremendous,
|
||||
@@ -927,15 +947,15 @@ async fn get_tremendous_payout_methods(
|
||||
let mut values = product
|
||||
.skus
|
||||
.into_iter()
|
||||
.map(|x| PayoutDecimal(x.min))
|
||||
.map(|x| PayoutDecimal(x.min * currency_to_usd))
|
||||
.collect::<Vec<_>>();
|
||||
values.sort_by(|a, b| a.0.cmp(&b.0));
|
||||
|
||||
PayoutInterval::Fixed { values }
|
||||
} else if let Some(first) = product.skus.first() {
|
||||
PayoutInterval::Standard {
|
||||
min: first.min,
|
||||
max: first.max,
|
||||
min: first.min * currency_to_usd,
|
||||
max: first.max * currency_to_usd,
|
||||
}
|
||||
} else {
|
||||
PayoutInterval::Standard {
|
||||
@@ -944,15 +964,10 @@ async fn get_tremendous_payout_methods(
|
||||
}
|
||||
},
|
||||
fee,
|
||||
currency_code: Some(currency.clone()),
|
||||
exchange_rate: Some(usd_to_currency),
|
||||
};
|
||||
|
||||
// we do not support interval gift cards with non US based currencies since we cannot do currency conversions properly
|
||||
if let PayoutInterval::Fixed { .. } = method.interval
|
||||
&& !product.currency_codes.contains(&"USD".to_string())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
methods.push(method);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user