From b92bf7093837bed5452a23246bc3c862de5682a2 Mon Sep 17 00:00:00 2001 From: matusdrobuliak66 Date: Thu, 8 May 2025 14:47:09 +0200 Subject: [PATCH] add safety condition for autorecharge --- .../services/auto_recharge_process_message.py | 49 ++++++++++++++----- .../test_services_auto_recharge_listener.py | 39 +++++++++++---- 2 files changed, 65 insertions(+), 23 deletions(-) diff --git a/services/payments/src/simcore_service_payments/services/auto_recharge_process_message.py b/services/payments/src/simcore_service_payments/services/auto_recharge_process_message.py index d300bbf881b1..e87e77e5c442 100644 --- a/services/payments/src/simcore_service_payments/services/auto_recharge_process_message.py +++ b/services/payments/src/simcore_service_payments/services/auto_recharge_process_message.py @@ -1,5 +1,5 @@ import logging -from datetime import datetime, timedelta, timezone +from datetime import UTC, datetime, timedelta from decimal import Decimal from typing import cast @@ -57,24 +57,30 @@ async def process_message(app: FastAPI, data: bytes) -> bool: assert wallet_auto_recharge is not None # nosec assert wallet_auto_recharge.payment_method_id is not None # nosec - # Step 3: Get Payment method - _payments_repo = PaymentsMethodsRepo(db_engine=app.state.engine) - payment_method_db = await _payments_repo.get_payment_method_by_id( - payment_method_id=wallet_auto_recharge.payment_method_id - ) - - # Step 4: Check spending limits + # Step 3: Check spending limits _payments_transactions_repo = PaymentsTransactionsRepo(db_engine=app.state.engine) if await _exceeds_monthly_limit( _payments_transactions_repo, rabbit_message.wallet_id, wallet_auto_recharge ): return True # We do not auto recharge - # Step 5: Check last top-up time - if await _recently_topped_up(_payments_transactions_repo, rabbit_message.wallet_id): + # Step 4: Check last top-up time + if await _was_wallet_topped_up_recently( + _payments_transactions_repo, rabbit_message.wallet_id + ): + return True # We do not auto recharge + + # Step 5: Check if timestamp when message was created is not too old + if await _is_message_too_old(rabbit_message.created_at): return True # We do not auto recharge - # Step 6: Perform auto-recharge + # Step 6: Get Payment method + _payments_repo = PaymentsMethodsRepo(db_engine=app.state.engine) + payment_method_db = await _payments_repo.get_payment_method_by_id( + payment_method_id=wallet_auto_recharge.payment_method_id + ) + + # Step 7: Perform auto-recharge if settings.PAYMENTS_AUTORECHARGE_ENABLED: await _perform_auto_recharge( app, rabbit_message, payment_method_db, wallet_auto_recharge @@ -114,16 +120,20 @@ async def _exceeds_monthly_limit( ) -async def _recently_topped_up( +async def _was_wallet_topped_up_recently( payments_transactions_repo: PaymentsTransactionsRepo, wallet_id: WalletID ): + """ + As safety, we check if the last transaction was initiated within the last 5 minutes + in that case we do not auto recharge + """ last_wallet_transaction = ( await payments_transactions_repo.get_last_payment_transaction_for_wallet( wallet_id=wallet_id ) ) - current_timestamp = datetime.now(tz=timezone.utc) + current_timestamp = datetime.now(tz=UTC) current_timestamp_minus_5_minutes = current_timestamp - timedelta(minutes=5) return ( @@ -132,6 +142,19 @@ async def _recently_topped_up( ) +async def _is_message_too_old( + message_timestamp: datetime, +): + """ + As safety, we check if the message was created within the last 5 minutes + if not we do not auto recharge + """ + current_timestamp = datetime.now(tz=UTC) + current_timestamp_minus_5_minutes = current_timestamp - timedelta(minutes=5) + + return message_timestamp < current_timestamp_minus_5_minutes + + async def _perform_auto_recharge( app: FastAPI, rabbit_message: WalletCreditsMessage, diff --git a/services/payments/tests/unit/test_services_auto_recharge_listener.py b/services/payments/tests/unit/test_services_auto_recharge_listener.py index aaf143ad7b7a..a5e4115c9fe9 100644 --- a/services/payments/tests/unit/test_services_auto_recharge_listener.py +++ b/services/payments/tests/unit/test_services_auto_recharge_listener.py @@ -5,7 +5,7 @@ # pylint: disable=unused-variable from collections.abc import Awaitable, Callable, Iterator -from datetime import datetime, timedelta, timezone +from datetime import UTC, datetime, timedelta from decimal import Decimal from unittest import mock @@ -51,7 +51,8 @@ _check_autorecharge_conditions_not_met, _check_wallet_credits_above_threshold, _exceeds_monthly_limit, - _recently_topped_up, + _is_message_too_old, + _was_wallet_topped_up_recently, ) from tenacity.asyncio import AsyncRetrying from tenacity.retry import retry_if_exception_type @@ -125,7 +126,7 @@ def populate_test_db( ) -> Iterator[None]: with postgres_db.connect() as con: _primary_payment_method_id = faker.uuid4() - _completed_at = datetime.now(tz=timezone.utc) + timedelta(minutes=1) + _completed_at = datetime.now(tz=UTC) + timedelta(minutes=1) con.execute( payments_methods.insert().values( @@ -316,8 +317,8 @@ def populate_payment_transaction_db( price_dollars=Decimal(9500), wallet_id=wallet_id, state=PaymentTransactionState.SUCCESS, - completed_at=datetime.now(tz=timezone.utc), - initiated_at=datetime.now(tz=timezone.utc) - timedelta(seconds=10), + completed_at=datetime.now(tz=UTC), + initiated_at=datetime.now(tz=UTC) - timedelta(seconds=10), ) ) ) @@ -366,14 +367,17 @@ async def test_exceeds_monthly_limit( ) -async def test_recently_topped_up_true( +async def test_was_wallet_topped_up_recently_true( app: FastAPI, wallet_id: int, populate_payment_transaction_db: None, ): _payments_transactions_repo = PaymentsTransactionsRepo(db_engine=app.state.engine) - assert await _recently_topped_up(_payments_transactions_repo, wallet_id) is True + assert ( + await _was_wallet_topped_up_recently(_payments_transactions_repo, wallet_id) + is True + ) @pytest.fixture() @@ -381,7 +385,7 @@ def populate_payment_transaction_db_with_older_trans( postgres_db: sa.engine.Engine, wallet_id: int ) -> Iterator[None]: with postgres_db.connect() as con: - current_timestamp = datetime.now(tz=timezone.utc) + current_timestamp = datetime.now(tz=UTC) current_timestamp_minus_10_minutes = current_timestamp - timedelta(minutes=10) con.execute( @@ -400,11 +404,26 @@ def populate_payment_transaction_db_with_older_trans( con.execute(payments_transactions.delete()) -async def test_recently_topped_up_false( +async def test_was_wallet_topped_up_recently_false( app: FastAPI, wallet_id: int, populate_payment_transaction_db_with_older_trans: None, ): _payments_transactions_repo = PaymentsTransactionsRepo(db_engine=app.state.engine) - assert await _recently_topped_up(_payments_transactions_repo, wallet_id) is False + assert ( + await _was_wallet_topped_up_recently(_payments_transactions_repo, wallet_id) + is False + ) + + +async def test__is_message_too_old_true(): + _dummy_message_timestamp = datetime.now(tz=UTC) - timedelta(minutes=10) + + assert await _is_message_too_old(_dummy_message_timestamp) is True + + +async def test__is_message_too_old_false(): + _dummy_message_timestamp = datetime.now(tz=UTC) - timedelta(minutes=3) + + assert await _is_message_too_old(_dummy_message_timestamp) is False