Skip to content

Commit 6b53689

Browse files
🎨 add safety condition for auto-recharge (#7648)
1 parent 0a9c2d0 commit 6b53689

File tree

2 files changed

+65
-23
lines changed

2 files changed

+65
-23
lines changed

services/payments/src/simcore_service_payments/services/auto_recharge_process_message.py

Lines changed: 36 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import logging
2-
from datetime import datetime, timedelta, timezone
2+
from datetime import UTC, datetime, timedelta
33
from decimal import Decimal
44
from typing import cast
55

@@ -57,24 +57,30 @@ async def process_message(app: FastAPI, data: bytes) -> bool:
5757
assert wallet_auto_recharge is not None # nosec
5858
assert wallet_auto_recharge.payment_method_id is not None # nosec
5959

60-
# Step 3: Get Payment method
61-
_payments_repo = PaymentsMethodsRepo(db_engine=app.state.engine)
62-
payment_method_db = await _payments_repo.get_payment_method_by_id(
63-
payment_method_id=wallet_auto_recharge.payment_method_id
64-
)
65-
66-
# Step 4: Check spending limits
60+
# Step 3: Check spending limits
6761
_payments_transactions_repo = PaymentsTransactionsRepo(db_engine=app.state.engine)
6862
if await _exceeds_monthly_limit(
6963
_payments_transactions_repo, rabbit_message.wallet_id, wallet_auto_recharge
7064
):
7165
return True # We do not auto recharge
7266

73-
# Step 5: Check last top-up time
74-
if await _recently_topped_up(_payments_transactions_repo, rabbit_message.wallet_id):
67+
# Step 4: Check last top-up time
68+
if await _was_wallet_topped_up_recently(
69+
_payments_transactions_repo, rabbit_message.wallet_id
70+
):
71+
return True # We do not auto recharge
72+
73+
# Step 5: Check if timestamp when message was created is not too old
74+
if await _is_message_too_old(rabbit_message.created_at):
7575
return True # We do not auto recharge
7676

77-
# Step 6: Perform auto-recharge
77+
# Step 6: Get Payment method
78+
_payments_repo = PaymentsMethodsRepo(db_engine=app.state.engine)
79+
payment_method_db = await _payments_repo.get_payment_method_by_id(
80+
payment_method_id=wallet_auto_recharge.payment_method_id
81+
)
82+
83+
# Step 7: Perform auto-recharge
7884
if settings.PAYMENTS_AUTORECHARGE_ENABLED:
7985
await _perform_auto_recharge(
8086
app, rabbit_message, payment_method_db, wallet_auto_recharge
@@ -114,16 +120,20 @@ async def _exceeds_monthly_limit(
114120
)
115121

116122

117-
async def _recently_topped_up(
123+
async def _was_wallet_topped_up_recently(
118124
payments_transactions_repo: PaymentsTransactionsRepo, wallet_id: WalletID
119125
):
126+
"""
127+
As safety, we check if the last transaction was initiated within the last 5 minutes
128+
in that case we do not auto recharge
129+
"""
120130
last_wallet_transaction = (
121131
await payments_transactions_repo.get_last_payment_transaction_for_wallet(
122132
wallet_id=wallet_id
123133
)
124134
)
125135

126-
current_timestamp = datetime.now(tz=timezone.utc)
136+
current_timestamp = datetime.now(tz=UTC)
127137
current_timestamp_minus_5_minutes = current_timestamp - timedelta(minutes=5)
128138

129139
return (
@@ -132,6 +142,19 @@ async def _recently_topped_up(
132142
)
133143

134144

145+
async def _is_message_too_old(
146+
message_timestamp: datetime,
147+
):
148+
"""
149+
As safety, we check if the message was created within the last 5 minutes
150+
if not we do not auto recharge
151+
"""
152+
current_timestamp = datetime.now(tz=UTC)
153+
current_timestamp_minus_5_minutes = current_timestamp - timedelta(minutes=5)
154+
155+
return message_timestamp < current_timestamp_minus_5_minutes
156+
157+
135158
async def _perform_auto_recharge(
136159
app: FastAPI,
137160
rabbit_message: WalletCreditsMessage,

services/payments/tests/unit/test_services_auto_recharge_listener.py

Lines changed: 29 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
# pylint: disable=unused-variable
66

77
from collections.abc import Awaitable, Callable, Iterator
8-
from datetime import datetime, timedelta, timezone
8+
from datetime import UTC, datetime, timedelta
99
from decimal import Decimal
1010
from unittest import mock
1111

@@ -51,7 +51,8 @@
5151
_check_autorecharge_conditions_not_met,
5252
_check_wallet_credits_above_threshold,
5353
_exceeds_monthly_limit,
54-
_recently_topped_up,
54+
_is_message_too_old,
55+
_was_wallet_topped_up_recently,
5556
)
5657
from tenacity.asyncio import AsyncRetrying
5758
from tenacity.retry import retry_if_exception_type
@@ -125,7 +126,7 @@ def populate_test_db(
125126
) -> Iterator[None]:
126127
with postgres_db.connect() as con:
127128
_primary_payment_method_id = faker.uuid4()
128-
_completed_at = datetime.now(tz=timezone.utc) + timedelta(minutes=1)
129+
_completed_at = datetime.now(tz=UTC) + timedelta(minutes=1)
129130

130131
con.execute(
131132
payments_methods.insert().values(
@@ -316,8 +317,8 @@ def populate_payment_transaction_db(
316317
price_dollars=Decimal(9500),
317318
wallet_id=wallet_id,
318319
state=PaymentTransactionState.SUCCESS,
319-
completed_at=datetime.now(tz=timezone.utc),
320-
initiated_at=datetime.now(tz=timezone.utc) - timedelta(seconds=10),
320+
completed_at=datetime.now(tz=UTC),
321+
initiated_at=datetime.now(tz=UTC) - timedelta(seconds=10),
321322
)
322323
)
323324
)
@@ -366,22 +367,25 @@ async def test_exceeds_monthly_limit(
366367
)
367368

368369

369-
async def test_recently_topped_up_true(
370+
async def test_was_wallet_topped_up_recently_true(
370371
app: FastAPI,
371372
wallet_id: int,
372373
populate_payment_transaction_db: None,
373374
):
374375
_payments_transactions_repo = PaymentsTransactionsRepo(db_engine=app.state.engine)
375376

376-
assert await _recently_topped_up(_payments_transactions_repo, wallet_id) is True
377+
assert (
378+
await _was_wallet_topped_up_recently(_payments_transactions_repo, wallet_id)
379+
is True
380+
)
377381

378382

379383
@pytest.fixture()
380384
def populate_payment_transaction_db_with_older_trans(
381385
postgres_db: sa.engine.Engine, wallet_id: int
382386
) -> Iterator[None]:
383387
with postgres_db.connect() as con:
384-
current_timestamp = datetime.now(tz=timezone.utc)
388+
current_timestamp = datetime.now(tz=UTC)
385389
current_timestamp_minus_10_minutes = current_timestamp - timedelta(minutes=10)
386390

387391
con.execute(
@@ -400,11 +404,26 @@ def populate_payment_transaction_db_with_older_trans(
400404
con.execute(payments_transactions.delete())
401405

402406

403-
async def test_recently_topped_up_false(
407+
async def test_was_wallet_topped_up_recently_false(
404408
app: FastAPI,
405409
wallet_id: int,
406410
populate_payment_transaction_db_with_older_trans: None,
407411
):
408412
_payments_transactions_repo = PaymentsTransactionsRepo(db_engine=app.state.engine)
409413

410-
assert await _recently_topped_up(_payments_transactions_repo, wallet_id) is False
414+
assert (
415+
await _was_wallet_topped_up_recently(_payments_transactions_repo, wallet_id)
416+
is False
417+
)
418+
419+
420+
async def test__is_message_too_old_true():
421+
_dummy_message_timestamp = datetime.now(tz=UTC) - timedelta(minutes=10)
422+
423+
assert await _is_message_too_old(_dummy_message_timestamp) is True
424+
425+
426+
async def test__is_message_too_old_false():
427+
_dummy_message_timestamp = datetime.now(tz=UTC) - timedelta(minutes=3)
428+
429+
assert await _is_message_too_old(_dummy_message_timestamp) is False

0 commit comments

Comments
 (0)