feat: enhance payment processing with minimum balance checks and update documentation

This commit is contained in:
bohd4nx
2026-05-11 13:11:28 +03:00
parent 6dd9dcb5d4
commit 7412448f9c
11 changed files with 29 additions and 39 deletions
+3
View File
@@ -24,6 +24,9 @@ and this project uses [Calendar Versioning](https://calver.org/) (`YYYY.MINOR.MI
- `@username` / `username` / `https://t.me/username`
- `get_wallet()` now returns balances as separate fields: `ton_balance` and `usdt_balance`
- Wallet/system test output now prints TON and USDT balances on separate lines
- Balance checks are now method-aware with explicit thresholds:
- `ton`: minimum TON balance threshold via `MIN_TON_BALANCE` (based on current 50 Stars purchase amount)
- `usdt_ton`: minimum USDT balance threshold via `MIN_USDT_BALANCE` (based on current 50 Stars purchase amount)
### Tests
+1 -1
View File
@@ -2,7 +2,7 @@
Example: recharge your own Telegram Ads account with TON.
Amount must be an integer between 1 and 1 000 000 000 TON.
Your wallet must hold at least the recharge amount + ~0.056 TON for gas.
Your wallet must satisfy the current minimum TON threshold and transaction cost.
"""
import asyncio
+1 -1
View File
@@ -4,7 +4,7 @@ Example: top up TON to a recipient's Telegram balance.
For adding TON to a Telegram Ads account, use recharge_ads() instead.
Amount must be an integer between 1 and 1 000 000 000 TON.
Your wallet must hold at least the top-up amount + ~0.056 TON for gas.
Your wallet must satisfy the current minimum TON threshold and transaction cost.
"""
import asyncio
+1 -1
View File
@@ -77,7 +77,7 @@ async def giveaway_premium(
},
page_url=PREMIUM_GIVEAWAY_PAGE,
)
required_payment_amount = parse_required_payment_amount(result, payment_method)
required_payment_amount = parse_required_payment_amount(result)
req_id = result.get("req_id")
if not req_id:
raise FragmentAPIError(FragmentAPIError.NO_REQUEST_ID.format(context="Premium giveaway"))
+1 -1
View File
@@ -73,7 +73,7 @@ async def giveaway_stars(
},
page_url=STARS_GIVEAWAY_PAGE,
)
required_payment_amount = parse_required_payment_amount(result, payment_method)
required_payment_amount = parse_required_payment_amount(result)
req_id = result.get("req_id")
if not req_id:
raise FragmentAPIError(FragmentAPIError.NO_REQUEST_ID.format(context="Stars giveaway"))
+1 -1
View File
@@ -71,7 +71,7 @@ async def purchase_premium(
{"recipient": recipient, "months": months, "payment_method": payment_method},
page_url=PREMIUM_PAGE,
)
required_payment_amount = parse_required_payment_amount(result, payment_method)
required_payment_amount = parse_required_payment_amount(result)
req_id = result.get("req_id")
if not req_id:
raise FragmentAPIError(FragmentAPIError.NO_REQUEST_ID.format(context="Premium purchase"))
+1 -1
View File
@@ -67,7 +67,7 @@ async def purchase_stars(
{"recipient": recipient, "quantity": amount, "payment_method": payment_method},
page_url=STARS_PAGE,
)
required_payment_amount = parse_required_payment_amount(result, payment_method)
required_payment_amount = parse_required_payment_amount(result)
req_id = result.get("req_id")
if not req_id:
raise FragmentAPIError(FragmentAPIError.NO_REQUEST_ID.format(context="Stars purchase"))
+2 -2
View File
@@ -16,8 +16,8 @@ SUPPORTED_WALLET_VERSIONS: frozenset[str] = frozenset(get_args(WalletVersion))
# Wallet class map — used to resolve the correct contract from WALLET_VERSION
WALLET_CLASSES: dict[str, Any] = {"V4R2": WalletV4R2, "V5R1": WalletV5R1}
# Minimum minimum TON balance required for payments (transaction amount + gas reserve).
MIN_TON_BALANCE: float = 0.33 # 0.3 TON for transaction + 0.03 TON gas reserve
# Minimum TON balance threshold required for payment flows.
MIN_TON_BALANCE: float = 0.33
# USDT (TON) jetton metadata used for payment-method balance checks.
USDT_TON_MASTER_ADDRESS: str = "EQCxE6mUtQJKFnGfaROTKOt1lZbDiiX1kCixRv7Nw2Id_sDs"
+2 -8
View File
@@ -163,14 +163,8 @@ def parse_gift_items(html: str) -> tuple[list[dict[str, Any]], int | None]:
return items, next_offset
def parse_required_payment_amount(init_response: dict[str, Any], payment_method: str) -> float | None:
"""Extract required payment amount from init*Request response.
Fragment includes ``amount`` for both ``ton`` and ``usdt_ton`` init flows,
so this helper always reads ``amount``.
"""
del payment_method
def parse_required_payment_amount(init_response: dict[str, Any]) -> float | None:
"""Extract required payment amount from init*Request response."""
raw_amount = init_response.get("amount")
try:
return float(str(raw_amount))
+13 -20
View File
@@ -49,13 +49,8 @@ async def _check_ton_payment_balance(
balance_ton: float,
amount_ton: float,
required_payment_amount: float | None,
ton: Any,
wallet_address: str,
) -> None:
"""Validate balance requirements for TON payment method."""
del ton
del wallet_address
tx_price_ton = amount_ton
if required_payment_amount is not None and required_payment_amount > 0:
tx_price_ton = max(tx_price_ton, required_payment_amount)
@@ -72,14 +67,11 @@ async def _check_ton_payment_balance(
async def _check_usdt_payment_balance(
balance_ton: float,
amount_ton: float,
required_payment_amount: float | None,
ton: Any,
wallet_address: str,
) -> None:
"""Validate balance requirements for USDT payment method."""
del amount_ton
# USDT payment still needs TON for network fees.
if balance_ton < MIN_TON_BALANCE:
raise WalletError(
@@ -134,18 +126,19 @@ async def process_transaction(
await wallet.refresh()
balance_ton = wallet.balance / 1_000_000_000
wallet_address = wallet.address.to_str(False, False)
checkers = {
"ton": _check_ton_payment_balance,
"usdt_ton": _check_usdt_payment_balance,
}
checker = checkers[payment_method]
await checker(
balance_ton,
amount_ton,
required_payment_amount,
ton,
wallet_address,
)
if payment_method == "ton":
await _check_ton_payment_balance(
balance_ton,
amount_ton,
required_payment_amount,
)
else:
await _check_usdt_payment_balance(
balance_ton,
required_payment_amount,
ton,
wallet_address,
)
except WalletError:
raise
except Exception as exc:
+3 -3
View File
@@ -5,7 +5,7 @@ from pyfragment.utils.html import parse_required_payment_amount
def test_parse_required_payment_amount_ton_uses_amount() -> None:
init_response = {"amount": "0.326"}
assert parse_required_payment_amount(init_response, "ton") == 0.326
assert parse_required_payment_amount(init_response) == 0.326
def test_parse_required_payment_amount_usdt_uses_amount() -> None:
@@ -13,9 +13,9 @@ def test_parse_required_payment_amount_usdt_uses_amount() -> None:
"amount": "0.00075",
"content": '<span class="icon-before icon-usd">0.75</span>',
}
assert parse_required_payment_amount(init_response, "usdt_ton") == 0.00075
assert parse_required_payment_amount(init_response) == 0.00075
def test_parse_required_payment_amount_usdt_falls_back_to_amount() -> None:
init_response = {"amount": "1.25", "content": "<p>no usd icon</p>"}
assert parse_required_payment_amount(init_response, "usdt_ton") == 1.25
assert parse_required_payment_amount(init_response) == 1.25