mirror of
https://github.com/chillpadclub/bedolaga-cabinet.git
synced 2026-07-28 17:43:47 +00:00
fix(topup): preserve canonical RUB amount to avoid FX round-trip shortfall
User report: a user in EN locale paid 1.65 USD for a 150₽ subscription, but the
bot received 14960 kopeks (149.60₽) — short by 40 kopeks, blocking the
subscription purchase.
Root cause: TopUpAmount.tsx displays the prefilled RUB amount converted via
`.toFixed(2)`. With an exchange rate like 90.66 RUB/USD, 150₽ → 1.6545 USD →
displayed "1.65" (rounded down by 0.0045 USD ≈ 0.4 RUB). When the user submits
without editing, `convertToRub("1.65")` runs again and returns 1.65 × 90.66 =
149.589₽ — less than the 150₽ the user is trying to pay. Math.round/Math.ceil
on this only handles sub-kopek IEEE-754 fractions, not the deeper FX display-
rounding direction.
Fix: when the user does not edit the prefilled amount (`amount === initialDisplayAmount`)
and `initialAmountRubles > 0` is known, bypass the FX round-trip and send
`Math.round(initialAmountRubles * 100)` directly. Math.ceil for non-RUB targets
when the user does type a custom amount still helps with sub-kopek fractions.
`?amount=150` from a renew CTA → "1.65 USD" displayed → user clicks pay → bot
receives 15000 kopeks instead of 14960. Subscription renews as expected.
This commit is contained in:
@@ -170,7 +170,8 @@ export default function TopUpAmount() {
|
|||||||
: converted.toFixed(2);
|
: converted.toFixed(2);
|
||||||
};
|
};
|
||||||
|
|
||||||
const [amount, setAmount] = useState(getInitialAmount);
|
const initialDisplayAmount = getInitialAmount();
|
||||||
|
const [amount, setAmount] = useState(initialDisplayAmount);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
const [selectedOption, setSelectedOption] = useState<string | null>(
|
const [selectedOption, setSelectedOption] = useState<string | null>(
|
||||||
getPreferredOptionId(method?.options),
|
getPreferredOptionId(method?.options),
|
||||||
@@ -334,7 +335,22 @@ export default function TopUpAmount() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const amountKopeks = Math.round(amountRubles * 100);
|
// Сохраняем canonical RUB amount если юзер НЕ редактировал префилл.
|
||||||
|
// Display-rounding в `.toFixed(2)` теряет точность: 150₽ при rate=90.66 → "1.65" USD
|
||||||
|
// (округление вниз с 1.6545), back-конвертация даёт 1.65 × 90.66 = 149.589₽ < 150₽
|
||||||
|
// → юзер не может купить подписку 150₽. С canonical RUB обходим FX round-trip.
|
||||||
|
//
|
||||||
|
// Math.ceil для не-RUB локалей покрывает остаточные sub-копеечные ошибки
|
||||||
|
// floating-point, когда юзер реально вводит свой amount.
|
||||||
|
const userEditedAmount = amount.trim() !== initialDisplayAmount.trim();
|
||||||
|
let amountKopeks: number;
|
||||||
|
if (!userEditedAmount && initialAmountRubles && initialAmountRubles > 0) {
|
||||||
|
amountKopeks = Math.round(initialAmountRubles * 100);
|
||||||
|
} else if (targetCurrency === 'RUB') {
|
||||||
|
amountKopeks = Math.round(amountRubles * 100);
|
||||||
|
} else {
|
||||||
|
amountKopeks = Math.ceil(amountRubles * 100);
|
||||||
|
}
|
||||||
if (isStarsMethod) {
|
if (isStarsMethod) {
|
||||||
starsPaymentMutation.mutate(amountKopeks);
|
starsPaymentMutation.mutate(amountKopeks);
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
Reference in New Issue
Block a user