Skip to content

feat: add WithdrawCalc method — withdrawal amount calculation - #42

Open
Flummy1 wants to merge 1 commit into
devfrom
feature/withdraw-calc
Open

feat: add WithdrawCalc method — withdrawal amount calculation#42
Flummy1 wants to merge 1 commit into
devfrom
feature/withdraw-calc

Conversation

@Flummy1

@Flummy1 Flummy1 commented Aug 20, 2026

Copy link
Copy Markdown
Contributor

Контекст

FunPay считает сумму вывода на стороне сервера: пользователь вводит одну из двух сумм,
вторую возвращает эндпоинт POST https://funpay.com/withdraw/calc. В движке этой возможности
не было — посчитать, сколько реально придёт на карту после комиссии (или сколько списать с
баланса, чтобы получить нужную сумму), было нечем.

Механика взята из обработчика формы вывода в app.bundle.js (функция инициализации
.withdraw-box):

function De(){ var R = o.serializeObject(); if (I && I.length) delete R[I.attr("name")]; return R }
// ...
$.ajax({ type:"POST", url: app.processRoute("/withdraw/calc"), data: R, dataType:"json",
         success: function(ae){ ... I.val(ae[le] || "") } })   // le = "amount_int" | "amount_ext"

Ключевое: поле суммы, которое пользователь не редактирует, помечается классом slave и
удаляется из тела запроса; именно его сайт и получает в ответе. Логика симметрична —
отправил amount_ext, получил amount_int, и наоборот.

Смысл величин:

  • amount_int — сумма, списываемая с баланса FunPay;
  • amount_ext — сумма, зачисляемая на карту / кошелёк (комиссия уже вычтена).

csrf_token отдельно передавать не нужно — его подставляет сессия
(AioHttpSession.make_request).

Что добавлено

Файл Что
funpaybotengine/types/withdraw.py WithdrawCalcResult — модель ответа (новый)
funpaybotengine/methods/withdraw_calc.py WithdrawCalc — метод withdraw/calc (новый)
funpaybotengine/types/__init__.py, funpaybotengine/methods/__init__.py реэкспорт
funpaybotengine/client/bot.py высокоуровневый Bot.calc_withdraw()

Метод построен по образцу уже существующих CalcLots / CalcChips: POST, заголовок
X-Requested-With: XMLHttpRequest, json.loads в parse_result, pydantic-модель в
__model_to_build__.

Инвариант «ровно одна из сумм» держит model_validator(mode='after') — вторая не попадает в
тело запроса вообще, как и в вебе. Передать обе или ни одной нельзя: ValidationError
до обращения к сети, а не пустой ответ после.

Тело запроса собирается callable-функцией make_data (как в GetTransactions), поэтому оно
всегда соответствует текущему состоянию модели:

{'preview': '1', 'currency_id': 'rub', 'ext_currency_id': 'card_rub',
 'wallet': '2202...', 'amount_ext': 500000.0}

WithdrawCalcResult терпим к формату: оба поля опциональны (в ответе всегда ровно одно),
строковые суммы нормализуются — обычный и неразрывный пробел как разделитель разрядов,
запятая как десятичный разделитель, пустая строка → None.

Использование

# сколько спишется с баланса, чтобы на карту пришло 500 000 ₽
res = await bot.calc_withdraw('rub', 'card_rub', '2202...', amount_ext=500_000)
print(res.amount_int)   # amount_ext остаётся None — его считал не сервер

# и наоборот: сколько придёт на карту, если списать 500 000 ₽
res = await bot.calc_withdraw('rub', 'card_rub', '2202...', amount_int=500_000)
print(res.amount_ext)

Границы изменения

Осознанно не входит в этот PR:

  • сам вывод средств (submit формы, 2FA-код, обработка error / twofactor_error) — нужен
    form[action] со страницы /account/balance, в бандле он не зашит;
  • список доступных ext_currency_id, комиссий и сохранённых кошельков — они лежат в
    .withdraw-box[data-data], а TransactionsPageParser этот блок не разбирает. Это задача для
    funpayparsers;
  • выбор банка для СБП (ext_currency_id == 'fps') — имя поля в бандле не видно (в ответе
    submit'а оно фигурирует как fps_bank_name).

Поэтому currency_id / ext_currency_id типизированы как str, а не enum: маппинг на
PaymentMethod из funpayparsers совпадает для card_rub / fps, но для WebMoney не проверен,
а гадать на публичном API не стоит.

Из-за validate_assignment=True у уже созданного WithdrawCalc нельзя переключить
«ведущее» поле присваиванием — под каждый расчёт создаётся новый объект, ровно как у
CalcLots / CalcChips.

Обратная совместимость

Изменений в существующем поведении нет — только новые сущности и один новый метод Bot.

Проверки

ruff check funpaybotengine        # новых замечаний нет (14 предсуществующих, как на dev)
ruff format --check <новые файлы> # 2 files already formatted
mypy funpaybotengine              # 8 ошибок, все предсуществующие; в новых файлах чисто

Сборка тела запроса и разбор ответа проверены локально на данных из реального сетевого лога
/withdraw/calc; живых запросов к FunPay в рамках PR не выполнялось.

Add support for the withdrawal amount calculation endpoint
(https://funpay.com/withdraw/calc), which FunPay uses to convert
between the amount debited from the balance and the amount credited
to the wallet.

Exactly one of `amount_int` / `amount_ext` must be passed: the one that
is omitted is the one FunPay calculates and returns.

New files:
- types/withdraw.py: WithdrawCalcResult pydantic model
- methods/withdraw_calc.py: WithdrawCalc method

Also adds the `Bot.calc_withdraw()` shortcut.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@Flummy1
Flummy1 requested a review from qvvonk as a code owner August 20, 2026 17:41
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant