diff --git a/backend/bot/payment_providers/platega.py b/backend/bot/payment_providers/platega.py index 0d7c28e..4a34c21 100644 --- a/backend/bot/payment_providers/platega.py +++ b/backend/bot/payment_providers/platega.py @@ -663,7 +663,6 @@ async def pay_platega_callback_handler( ) transaction_id = first_value(response_data, "transactionId", "id") redirect_url = first_value(response_data, "redirect", "url", "paymentUrl") - provider_status = str((response_data or {}).get("status") or payment_record.status) # Platega requires *both* a transaction id and a redirect url to count as a # usable payment — neither field is sufficient on its own. Skipping the # persistence step when the redirect is missing matches the pre-refactor @@ -680,7 +679,6 @@ async def pay_platega_callback_handler( api_success=success, payment_url=redirect_url, provider_payment_id=persistable_id, - new_status=provider_status if persistable_id else None, log_prefix=_LOG, ) @@ -766,7 +764,6 @@ async def _create_webapp_payment(ctx: WebAppPaymentContext, variant: str) -> web first_value(response_data, "redirect", "url", "paymentUrl") if success else None ), provider_payment_id=first_value(response_data, "transactionId", "id"), - new_status=str((response_data or {}).get("status") or payment.status), log_prefix="Platega", ) diff --git a/backend/bot/payment_providers/shared/callbacks.py b/backend/bot/payment_providers/shared/callbacks.py index d8523d0..0748469 100644 --- a/backend/bot/payment_providers/shared/callbacks.py +++ b/backend/bot/payment_providers/shared/callbacks.py @@ -356,7 +356,7 @@ async def render_link_or_fail( payment as ``failed_creation``. Every link-style provider used to inline this same sequence. """ - if api_success and provider_payment_id: + if api_success and provider_payment_id and payment_url: await safe_store_provider_payment_id( session, payment, diff --git a/backend/bot/payment_providers/shared/webapp.py b/backend/bot/payment_providers/shared/webapp.py index 7577957..45981f0 100644 --- a/backend/bot/payment_providers/shared/webapp.py +++ b/backend/bot/payment_providers/shared/webapp.py @@ -39,7 +39,9 @@ async def finalize_webapp_link_payment( log_prefix="Wata", ) """ - if api_success and provider_payment_id: + # Reuse logic needs both a provider id and a redirect URL; persisting only + # the id creates orphan records that match find_recent but fail verification. + if api_success and provider_payment_id and payment_url: try: await payment_dal.update_provider_payment_and_status( session, diff --git a/backend/bot/payment_providers/yookassa.py b/backend/bot/payment_providers/yookassa.py index 30b33b5..2c5520d 100644 --- a/backend/bot/payment_providers/yookassa.py +++ b/backend/bot/payment_providers/yookassa.py @@ -1609,7 +1609,7 @@ async def _initiate_yk_payment( await payment_dal.update_payment_status_by_db_id( session, payment_db_id=db_payment_record.payment_id, - new_status=payment_response_yk.get("status", "pending"), + new_status="pending_yookassa", yk_payment_id=payment_response_yk.get("id"), ) if selected_method_internal_id is not None: @@ -1677,12 +1677,11 @@ async def _initiate_yk_payment( return True if payment_response_yk and payment_method_id: - status_to_store = payment_response_yk.get("status", "pending") try: await payment_dal.update_payment_status_by_db_id( session, payment_db_id=db_payment_record.payment_id, - new_status=status_to_store, + new_status="pending_yookassa", yk_payment_id=payment_response_yk.get("id"), ) if selected_method_internal_id is not None: @@ -2934,7 +2933,7 @@ async def create_webapp_payment(ctx: WebAppPaymentContext) -> web.Response: await payment_dal.update_payment_status_by_db_id( ctx.session, payment.payment_id, - response.get("status", "pending"), + "pending_yookassa", yk_payment_id=response.get("id"), ) await ctx.session.commit() diff --git a/backend/db/dal/payment_dal.py b/backend/db/dal/payment_dal.py index 6c435f8..bdcf73c 100644 --- a/backend/db/dal/payment_dal.py +++ b/backend/db/dal/payment_dal.py @@ -129,13 +129,19 @@ async def find_recent_pending_provider_payment( Used to reuse an existing provider payment link instead of creating a new one on repeated user clicks. A generic or provider-specific payment id must be populated so the caller can verify the remote payment link. + + Status matching is case-insensitive and also accepts the generic ``pending`` + alias so legacy rows (e.g. Platega ``PENDING`` or YooKassa ``pending``) stay + reusable after provider APIs overwrite the internal pending status. """ from datetime import datetime, timedelta, timezone conditions = [ Payment.user_id == user_id, Payment.provider == provider, - Payment.status.in_((pending_status, "pending")), + func.lower(Payment.status).in_( + tuple({str(pending_status).lower(), "pending"}) + ), or_( Payment.provider_payment_id.isnot(None), Payment.yookassa_payment_id.isnot(None), diff --git a/tests/test_webapp_payment_status.py b/tests/test_webapp_payment_status.py index b044993..524889f 100644 --- a/tests/test_webapp_payment_status.py +++ b/tests/test_webapp_payment_status.py @@ -210,6 +210,85 @@ class WebAppPaymentStatusTests(IsolatedAsyncioTestCase): ) ) + async def test_platega_reuses_tariff_upgrade_pending_transaction(self): + payment = SimpleNamespace( + payment_id=35, + amount=150.0, + currency="RUB", + provider_payment_id="transaction-35", + provider_payment_url="https://platega.example/pay/35", + ) + service = object.__new__(PlategaService) + service.get_transaction = AsyncMock( + return_value=( + True, + { + "id": "transaction-35", + "status": "PENDING", + "paymentDetails": {"amount": 150.0, "currency": "RUB"}, + "payload": json.dumps( + { + "payment_db_id": 35, + "user_id": 734546943, + "months": 1, + "sale_mode": "tariff_upgrade@main", + "traffic_gb": None, + "hwid_devices": None, + "source": "webapp", + "platega_variant": "sbp", + } + ), + }, + ) + ) + + url = await service.try_reuse_pending_transaction( + payment, + user_id=734546943, + sale_mode="tariff_upgrade@main", + variant="sbp", + ) + + self.assertEqual(url, "https://platega.example/pay/35") + + async def test_render_link_or_fail_skips_id_without_payment_url(self): + from bot.payment_providers.shared.callbacks import render_link_or_fail + + payment = SimpleNamespace(payment_id=77, status="pending_platega") + session = AsyncMock() + callback = SimpleNamespace(message=SimpleNamespace(edit_text=AsyncMock()), answer=AsyncMock()) + + with patch( + "bot.payment_providers.shared.callbacks.safe_store_provider_payment_id", + AsyncMock(return_value=True), + ) as store_id, patch( + "bot.payment_providers.shared.callbacks.render_payment_link", + AsyncMock(), + ) as render_link, patch( + "bot.payment_providers.shared.callbacks.safe_mark_failed_creation", + AsyncMock(), + ) as mark_failed, patch( + "bot.payment_providers.shared.callbacks.notify_payment_gateway_failure", + AsyncMock(), + ): + await render_link_or_fail( + callback, + translator=lambda key, **kwargs: key, + current_lang="ru", + i18n=None, + parts=SimpleNamespace(), + session=session, + payment=payment, + api_success=True, + payment_url=None, + provider_payment_id="transaction-77", + log_prefix="Platega", + ) + + store_id.assert_not_awaited() + render_link.assert_not_awaited() + mark_failed.assert_awaited_once() + async def test_freekassa_reuses_matching_new_order(self): payment = SimpleNamespace( payment_id=77,