fix(payments): pedning status in transactions
This commit is contained in:
@@ -663,7 +663,6 @@ async def pay_platega_callback_handler(
|
|||||||
)
|
)
|
||||||
transaction_id = first_value(response_data, "transactionId", "id")
|
transaction_id = first_value(response_data, "transactionId", "id")
|
||||||
redirect_url = first_value(response_data, "redirect", "url", "paymentUrl")
|
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
|
# 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
|
# usable payment — neither field is sufficient on its own. Skipping the
|
||||||
# persistence step when the redirect is missing matches the pre-refactor
|
# persistence step when the redirect is missing matches the pre-refactor
|
||||||
@@ -680,7 +679,6 @@ async def pay_platega_callback_handler(
|
|||||||
api_success=success,
|
api_success=success,
|
||||||
payment_url=redirect_url,
|
payment_url=redirect_url,
|
||||||
provider_payment_id=persistable_id,
|
provider_payment_id=persistable_id,
|
||||||
new_status=provider_status if persistable_id else None,
|
|
||||||
log_prefix=_LOG,
|
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
|
first_value(response_data, "redirect", "url", "paymentUrl") if success else None
|
||||||
),
|
),
|
||||||
provider_payment_id=first_value(response_data, "transactionId", "id"),
|
provider_payment_id=first_value(response_data, "transactionId", "id"),
|
||||||
new_status=str((response_data or {}).get("status") or payment.status),
|
|
||||||
log_prefix="Platega",
|
log_prefix="Platega",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -356,7 +356,7 @@ async def render_link_or_fail(
|
|||||||
payment as ``failed_creation``. Every link-style provider used to inline
|
payment as ``failed_creation``. Every link-style provider used to inline
|
||||||
this same sequence.
|
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(
|
await safe_store_provider_payment_id(
|
||||||
session,
|
session,
|
||||||
payment,
|
payment,
|
||||||
|
|||||||
@@ -39,7 +39,9 @@ async def finalize_webapp_link_payment(
|
|||||||
log_prefix="Wata",
|
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:
|
try:
|
||||||
await payment_dal.update_provider_payment_and_status(
|
await payment_dal.update_provider_payment_and_status(
|
||||||
session,
|
session,
|
||||||
|
|||||||
@@ -1609,7 +1609,7 @@ async def _initiate_yk_payment(
|
|||||||
await payment_dal.update_payment_status_by_db_id(
|
await payment_dal.update_payment_status_by_db_id(
|
||||||
session,
|
session,
|
||||||
payment_db_id=db_payment_record.payment_id,
|
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"),
|
yk_payment_id=payment_response_yk.get("id"),
|
||||||
)
|
)
|
||||||
if selected_method_internal_id is not None:
|
if selected_method_internal_id is not None:
|
||||||
@@ -1677,12 +1677,11 @@ async def _initiate_yk_payment(
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
if payment_response_yk and payment_method_id:
|
if payment_response_yk and payment_method_id:
|
||||||
status_to_store = payment_response_yk.get("status", "pending")
|
|
||||||
try:
|
try:
|
||||||
await payment_dal.update_payment_status_by_db_id(
|
await payment_dal.update_payment_status_by_db_id(
|
||||||
session,
|
session,
|
||||||
payment_db_id=db_payment_record.payment_id,
|
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"),
|
yk_payment_id=payment_response_yk.get("id"),
|
||||||
)
|
)
|
||||||
if selected_method_internal_id is not None:
|
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(
|
await payment_dal.update_payment_status_by_db_id(
|
||||||
ctx.session,
|
ctx.session,
|
||||||
payment.payment_id,
|
payment.payment_id,
|
||||||
response.get("status", "pending"),
|
"pending_yookassa",
|
||||||
yk_payment_id=response.get("id"),
|
yk_payment_id=response.get("id"),
|
||||||
)
|
)
|
||||||
await ctx.session.commit()
|
await ctx.session.commit()
|
||||||
|
|||||||
@@ -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
|
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
|
on repeated user clicks. A generic or provider-specific payment id must be
|
||||||
populated so the caller can verify the remote payment link.
|
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
|
from datetime import datetime, timedelta, timezone
|
||||||
|
|
||||||
conditions = [
|
conditions = [
|
||||||
Payment.user_id == user_id,
|
Payment.user_id == user_id,
|
||||||
Payment.provider == provider,
|
Payment.provider == provider,
|
||||||
Payment.status.in_((pending_status, "pending")),
|
func.lower(Payment.status).in_(
|
||||||
|
tuple({str(pending_status).lower(), "pending"})
|
||||||
|
),
|
||||||
or_(
|
or_(
|
||||||
Payment.provider_payment_id.isnot(None),
|
Payment.provider_payment_id.isnot(None),
|
||||||
Payment.yookassa_payment_id.isnot(None),
|
Payment.yookassa_payment_id.isnot(None),
|
||||||
|
|||||||
@@ -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):
|
async def test_freekassa_reuses_matching_new_order(self):
|
||||||
payment = SimpleNamespace(
|
payment = SimpleNamespace(
|
||||||
payment_id=77,
|
payment_id=77,
|
||||||
|
|||||||
Reference in New Issue
Block a user