Skip to content

fix: preserve requested transaction filter across pagination#40

Open
Flummy1 wants to merge 3 commits into
chore/fix-ruff-mypy-strictfrom
fix/transaction-filter-pagination
Open

fix: preserve requested transaction filter across pagination#40
Flummy1 wants to merge 3 commits into
chore/fix-ruff-mypy-strictfrom
fix/transaction-filter-pagination

Conversation

@Flummy1

@Flummy1 Flummy1 commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

Проблема

TransactionPreviewsBatch.next_batch() терял фильтр транзакций: первая страница приходила
отфильтрованной, а каждая следующая — со всей лентой транзакций. Молча, без исключения
и предупреждения.

batch = await bot.get_transactions(filter=TransactionFilter.WITHDRAW)
# batch.transactions — только выводы ✅

nxt = await batch.next_batch()
# nxt.transactions — выводы, пополнения, заказы, возвраты ❌

Последствие для потребителя — тихая порча данных: любой код, который постранично читает
отфильтрованную ленту, начиная со второй страницы получает не то, что запросил. Аккаунты,
чья история помещается на одну страницу (~25 транзакций), не затронуты — это и есть
характерный признак.

Причина

next_batch() восстанавливал фильтр из self.filter, а self.filter — это не запрошенное
значение, а значение, вычитанное из HTML ответа:

# funpayparsers/parsers/transaction_previews_parser.py
filter_ = self.tree.css('input[type="hidden"][name="filter"]')
filter=filter_[0].attributes.get('value') if filter_ else None

FunPay в части ответов не отдаёт скрытый input filter. Тогда self.filter становится
None, и выражение self.filter or '' расширяет фильтр с «выводы» до «всё»:

return await self.get_bound_bot().get_transactions(
    from_transaction_id=self.next_transaction_id,
    filter=self.filter or '',   # ← '' == TransactionFilter.ALL
)

Усугубляет ситуацию то, что TransactionFilter.ALL == '', то есть ложно в булевом
контексте. Поэтому любой fallback через or на этом enum некорректен по построению: он не
способен отличить «фильтр не применялся» от «фильтр неизвестен».

При этом GetTransactions вообще не передавал запрошенный фильтр в собираемый объект —
context не заполнялся.

Решение

Инвариант в кодовой базе уже сформулирован — просто не был применён к транзакциям.
GetSales пробрасывает дискриминатор через context метода, а OrderPreviewsBatch.next_batch()
громко падает на неизвестном типе вместо подстановки значения по умолчанию.

Транзакции приведены к тому же контракту:

  1. GetTransactions передаёт context={'transaction_filter': method.filter} — запрошенное
    значение, уже нормализованное pydantic в enum (в том числе если пользователь передал строку).
  2. TransactionPreviewsBatch.model_post_init отдаёт приоритет фильтру из запроса; значение
    из HTML остаётся только как fallback (например, для объекта, собранного вручную).
  3. next_batch() передаёт фильтр явно и бросает ValueError, если фильтр неизвестен,
    вместо тихого расширения до ALL. Никаких проверок на «ложность» enum.
  4. GetTransactionsPage помечает свой батч как TransactionFilter.ALL — страница баланса
    всегда отдаёт нефильтрованный список, поэтому пагинация оттуда продолжает работать.

Сопутствующий дефект (обнаружен при работе над основным): GetTransactions() без аргументов
падал с ValidationError, потому что from_transaction_id по умолчанию был None при поле
типа int. Значение по умолчанию исправлено на 0.

Изменения

Файл Что
funpaybotengine/methods/get_transactions.py проброс context, исправлен дефолт from_transaction_id
funpaybotengine/types/finances.py приоритет запрошенного фильтра, громкий отказ в next_batch()
funpaybotengine/methods/get_transactions_page.py батч страницы баланса помечен как ALL
pyproject.toml секция [tool.pytest.ini_options], чтобы make test работал; pythonpath = ["."]
tests/test_transactions_pagination.py регрессионный набор (новый)

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

Меняется одно наблюдаемое поведение: next_batch() теперь бросает
ValueError('Unknown transaction filter.') вместо того, чтобы отдать неотфильтрованную
страницу. Это ровно тот случай, который раньше и приводил к порче данных, и он воспроизводится
только на батчах, собранных в обход GetTransactions / GetTransactionsPage.

batch.filter теперь при обычном использовании всегда содержит запрошенный enum, а не None.

Каллерский workaround (ручная пагинация с явным фильтром) продолжает работать и больше не нужен.

Проверки

uv run ruff check funpaybotengine tests     # passed
uv run ruff format --check funpaybotengine tests  # passed
uv run mypy funpaybotengine                 # Success: no issues found in 107 source files
uv run --extra tests pytest tests -q        # 10 passed

Регрессионный набор проверен на «мутацию»: против кода до фикса падают 7 из 10 тестов,
включая сквозной test_filtered_pagination_survives_a_response_without_filter_input, который
прогоняет настоящий TransactionPreviewsParser на ответе без скрытого input filter.

pythonpath = ["."] в конфиге pytest добавлен не для удобства: без него голый pytest
(вне uv run) импортирует funpaybotengine из установленного в окружении релиза, а не из
рабочего дерева, и тесты молча проверяют чужой код.

Зависимость

Ветка построена поверх #39 (chore/fix-ruff-mypy-strict) — база PR должна быть chore/fix-ruff-mypy-strict,
иначе в диффе окажутся чужие изменения. После вливания #39 базу можно переключить на dev.

`TransactionPreviewsBatch.next_batch()` re-derived the filter from `self.filter`,
which is scraped from the response HTML. FunPay omits the hidden `filter` input in
some responses, so `self.filter` became `None` and `self.filter or ''` silently
widened the filter to `TransactionFilter.ALL` — every page after the first returned
the entire transaction feed instead of the requested category.

`TransactionFilter.ALL` is `''` and therefore falsy, so no `or`-based fallback on
this enum can distinguish "no filter" from "unknown filter".

Thread the requested filter through the method context, the same way `GetSales`
threads `order_preview_type`, and let `next_batch()` fail loudly on an unknown
filter instead of defaulting to the widest one — mirroring
`OrderPreviewsBatch.next_batch()`.

- `GetTransactions` now passes `context={'transaction_filter': ...}`.
- `TransactionPreviewsBatch` prefers the requested filter over the scraped one.
- `next_batch()` raises `ValueError` when the filter is unknown.
- `GetTransactionsPage` marks its batch as `ALL` (the balance page is unfiltered).
- `GetTransactions()` no longer fails validation when called without arguments
  (`from_transaction_id` defaulted to `None` against an `int` field).

Adds a regression suite covering the end-to-end path with a response that has no
hidden `filter` input.
@Flummy1 Flummy1 requested a review from qvvonk as a code owner July 9, 2026 21:13
Flummy1 and others added 2 commits July 14, 2026 13:47
Requested by maintainer.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Requested by maintainer.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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