From 4b6a1794eb42a9a26012ab3baf6ee190183afe39 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Robert?= Date: Mon, 20 Apr 2026 01:02:28 +0200 Subject: [PATCH 1/4] feat: improve request expiration logic --- app/core/mypayment/cruds_mypayment.py | 20 ++------ app/core/mypayment/endpoints_mypayment.py | 26 ++++------ app/core/mypayment/types_mypayment.py | 1 - migrations/versions/70-better_requests.py | 59 +++++++++++++++++++++++ tests/core/test_mypayment.py | 2 +- 5 files changed, 75 insertions(+), 33 deletions(-) create mode 100644 migrations/versions/70-better_requests.py diff --git a/app/core/mypayment/cruds_mypayment.py b/app/core/mypayment/cruds_mypayment.py index f3ca9d9e22..f0c21d19dc 100644 --- a/app/core/mypayment/cruds_mypayment.py +++ b/app/core/mypayment/cruds_mypayment.py @@ -1031,7 +1031,11 @@ async def get_requests_by_wallet_id( result = await db.execute( select(models_mypayment.Request).where( models_mypayment.Request.wallet_id == wallet_id, - models_mypayment.Request.status == RequestStatus.PROPOSED + and_( + models_mypayment.Request.status == RequestStatus.PROPOSED, + models_mypayment.Request.creation + > datetime.now(tz=UTC) - timedelta(minutes=REQUEST_EXPIRATION), + ) if not include_used else and_(True), ), @@ -1135,20 +1139,6 @@ async def create_request( db.add(request_db) -async def mark_expired_requests_as_expired( - db: AsyncSession, -) -> None: - await db.execute( - update(models_mypayment.Request) - .where( - models_mypayment.Request.status == RequestStatus.PROPOSED, - models_mypayment.Request.creation - <= datetime.now(tz=UTC) - timedelta(minutes=REQUEST_EXPIRATION), - ) - .values(status=RequestStatus.EXPIRED), - ) - - async def update_request( request_id: UUID, request_update: schemas_mypayment.RequestEdit, diff --git a/app/core/mypayment/endpoints_mypayment.py b/app/core/mypayment/endpoints_mypayment.py index f647fcc442..25a2504725 100644 --- a/app/core/mypayment/endpoints_mypayment.py +++ b/app/core/mypayment/endpoints_mypayment.py @@ -57,7 +57,6 @@ MYPAYMENT_STRUCTURE_S3_SUBFOLDER, MYPAYMENT_USERS_S3_SUBFOLDER, QRCODE_EXPIRATION, - REQUEST_EXPIRATION, RETENTION_DURATION, HistoryDirection, HistoryType, @@ -2796,10 +2795,6 @@ async def accept_request( **The user must be authenticated to use this endpoint** """ - await cruds_mypayment.mark_expired_requests_as_expired( - db=db, - ) - await db.flush() if request_id != request_validation.id: raise HTTPException( status_code=400, @@ -2819,6 +2814,16 @@ async def accept_request( status_code=400, detail="Request total in the body do not match the request total in the database", ) + if request.status != RequestStatus.PROPOSED: + raise HTTPException( + status_code=400, + detail="Only pending requests can be confirmed", + ) + if request.end_date < datetime.now(UTC): + raise HTTPException( + status_code=400, + detail="Request is expired", + ) user_payment = await cruds_mypayment.get_user_payment( user_id=user.id, @@ -2851,17 +2856,6 @@ async def accept_request( detail="Wallet device is not associated with the user wallet", ) - if request.status != RequestStatus.PROPOSED: - raise HTTPException( - status_code=400, - detail="Only pending requests can be confirmed", - ) - if request.creation < datetime.now(UTC) - timedelta(minutes=REQUEST_EXPIRATION): - raise HTTPException( - status_code=400, - detail="Request is expired", - ) - if not verify_signature( public_key_bytes=debited_wallet_device.ed25519_public_key, signature=request_validation.signature, diff --git a/app/core/mypayment/types_mypayment.py b/app/core/mypayment/types_mypayment.py index 251129b005..9703d5fe12 100644 --- a/app/core/mypayment/types_mypayment.py +++ b/app/core/mypayment/types_mypayment.py @@ -61,7 +61,6 @@ class RequestStatus(StrEnum): PROPOSED = "proposed" ACCEPTED = "accepted" REFUSED = "refused" - EXPIRED = "expired" class TransferOrigin(StrEnum): diff --git a/migrations/versions/70-better_requests.py b/migrations/versions/70-better_requests.py new file mode 100644 index 0000000000..cea7a7878f --- /dev/null +++ b/migrations/versions/70-better_requests.py @@ -0,0 +1,59 @@ +"""empty message + +Create Date: 2026-04-20 00:51:44.716180 +""" + +from collections.abc import Sequence +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from pytest_alembic import MigrationContext + +import sqlalchemy as sa +from alembic import op + +# revision identifiers, used by Alembic. +revision: str = "9bb79b2466f9" +down_revision: str | None = "c4d2aa4e6f1b" +branch_labels: str | Sequence[str] | None = None +depends_on: str | Sequence[str] | None = None + + +def upgrade() -> None: + conn = op.get_bind() + conn.execute( + sa.text( + """ + UPDATE mypayment_request + SET status = 'PROPOSED' + WHERE status = 'EXPIRED' + """, + ), + ) + conn.execute( + sa.text( + """ + DELETE FROM pg_enum WHERE enumtypid = ( + SELECT oid FROM pg_type WHERE typname = 'requeststatus' + ) AND enumlabel = 'EXPIRED' + """, + ), + ) + + +def downgrade() -> None: + pass + + +def pre_test_upgrade( + alembic_runner: "MigrationContext", + alembic_connection: sa.Connection, +) -> None: + pass + + +def test_upgrade( + alembic_runner: "MigrationContext", + alembic_connection: sa.Connection, +) -> None: + pass diff --git a/tests/core/test_mypayment.py b/tests/core/test_mypayment.py index a01a9f461e..ba085ffd53 100644 --- a/tests/core/test_mypayment.py +++ b/tests/core/test_mypayment.py @@ -692,7 +692,7 @@ async def init_objects() -> None: total=1000, name="Expired Request", store_note="Expired Request Note", - status=RequestStatus.EXPIRED, + status=RequestStatus.PROPOSED, module=TEST_MODULE_ROOT, object_id=uuid4(), transaction_id=None, From 61c437efe6207fd7642ddaf444b0779f748c6441 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Robert?= Date: Mon, 20 Apr 2026 01:08:53 +0200 Subject: [PATCH 2/4] refacto: renaming --- app/core/mypayment/cruds_mypayment.py | 6 +++--- app/core/mypayment/endpoints_mypayment.py | 2 +- app/core/mypayment/models_mypayment.py | 2 +- app/core/mypayment/schemas_mypayment.py | 2 +- app/core/mypayment/utils_mypayment.py | 2 +- 5 files changed, 7 insertions(+), 7 deletions(-) diff --git a/app/core/mypayment/cruds_mypayment.py b/app/core/mypayment/cruds_mypayment.py index f0c21d19dc..683f905afb 100644 --- a/app/core/mypayment/cruds_mypayment.py +++ b/app/core/mypayment/cruds_mypayment.py @@ -1046,7 +1046,7 @@ async def get_requests_by_wallet_id( wallet_id=request.wallet_id, status=request.status, creation=request.creation, - end_date=request.end_date, + expiration_date=request.expiration_date, total=request.total, store_note=request.store_note, store_id=request.store_id, @@ -1079,7 +1079,7 @@ async def get_request_by_id( wallet_id=request.wallet_id, status=request.status, creation=request.creation, - end_date=request.end_date, + expiration_date=request.expiration_date, total=request.total, store_note=request.store_note, store_id=request.store_id, @@ -1107,7 +1107,7 @@ async def get_request_by_store_id( wallet_id=request.wallet_id, status=request.status, creation=request.creation, - end_date=request.end_date, + expiration_date=request.expiration_date, total=request.total, store_note=request.store_note, store_id=request.store_id, diff --git a/app/core/mypayment/endpoints_mypayment.py b/app/core/mypayment/endpoints_mypayment.py index 25a2504725..41a04626d2 100644 --- a/app/core/mypayment/endpoints_mypayment.py +++ b/app/core/mypayment/endpoints_mypayment.py @@ -2819,7 +2819,7 @@ async def accept_request( status_code=400, detail="Only pending requests can be confirmed", ) - if request.end_date < datetime.now(UTC): + if request.expiration_date < datetime.now(UTC): raise HTTPException( status_code=400, detail="Request is expired", diff --git a/app/core/mypayment/models_mypayment.py b/app/core/mypayment/models_mypayment.py index 4f26974d60..504501d816 100644 --- a/app/core/mypayment/models_mypayment.py +++ b/app/core/mypayment/models_mypayment.py @@ -199,7 +199,7 @@ class Request(Base): ) @property - def end_date(self) -> datetime: + def expiration_date(self) -> datetime: return self.creation + timedelta(minutes=REQUEST_EXPIRATION) diff --git a/app/core/mypayment/schemas_mypayment.py b/app/core/mypayment/schemas_mypayment.py index 3d181dcd99..9f1881cc34 100644 --- a/app/core/mypayment/schemas_mypayment.py +++ b/app/core/mypayment/schemas_mypayment.py @@ -346,7 +346,7 @@ class Request(BaseModel): id: UUID wallet_id: UUID creation: datetime - end_date: datetime + expiration_date: datetime total: int # Stored in cents store_id: UUID name: str diff --git a/app/core/mypayment/utils_mypayment.py b/app/core/mypayment/utils_mypayment.py index 3492a8dae4..c799eec617 100644 --- a/app/core/mypayment/utils_mypayment.py +++ b/app/core/mypayment/utils_mypayment.py @@ -178,7 +178,7 @@ async def request_transaction( id=uuid4(), wallet_id=payment_user.wallet_id, creation=start_time, - end_date=start_time + timedelta(minutes=REQUEST_EXPIRATION), + expiration_date=start_time + timedelta(minutes=REQUEST_EXPIRATION), total=request_info.total, store_id=request_info.store_id, name=request_info.request_name, From b8df9463527ccd420bc4d3646513911343f60cac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Robert?= Date: Mon, 20 Apr 2026 01:11:44 +0200 Subject: [PATCH 3/4] refacto: remove duplicate --- app/core/mypayment/utils_mypayment.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/app/core/mypayment/utils_mypayment.py b/app/core/mypayment/utils_mypayment.py index c799eec617..d31e36bd87 100644 --- a/app/core/mypayment/utils_mypayment.py +++ b/app/core/mypayment/utils_mypayment.py @@ -172,13 +172,14 @@ async def request_transaction( if not payment_user: raise PaymentUserNotFoundError(user.id) start_time = datetime.now(UTC) + expiration_time = start_time + timedelta(minutes=REQUEST_EXPIRATION) await cruds_mypayment.create_request( db=db, request=schemas_mypayment.Request( id=uuid4(), wallet_id=payment_user.wallet_id, creation=start_time, - expiration_date=start_time + timedelta(minutes=REQUEST_EXPIRATION), + expiration_date=expiration_time, total=request_info.total, store_id=request_info.store_id, name=request_info.request_name, @@ -198,7 +199,7 @@ async def request_transaction( message=message, ) return schemas_mypayment.PaymentRequestInfo( - end_date=start_time + timedelta(minutes=REQUEST_EXPIRATION), + end_date=expiration_time, checkout_url=None, ) From b24268ab827705339a8a06ab87070c2cec053489 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Robert?= Date: Mon, 20 Apr 2026 01:31:04 +0200 Subject: [PATCH 4/4] fix: migration --- migrations/versions/70-better_requests.py | 18 ++++++++---------- tests/core/test_mypayment.py | 2 +- 2 files changed, 9 insertions(+), 11 deletions(-) diff --git a/migrations/versions/70-better_requests.py b/migrations/versions/70-better_requests.py index cea7a7878f..95bec48f44 100644 --- a/migrations/versions/70-better_requests.py +++ b/migrations/versions/70-better_requests.py @@ -21,15 +21,6 @@ def upgrade() -> None: conn = op.get_bind() - conn.execute( - sa.text( - """ - UPDATE mypayment_request - SET status = 'PROPOSED' - WHERE status = 'EXPIRED' - """, - ), - ) conn.execute( sa.text( """ @@ -42,7 +33,14 @@ def upgrade() -> None: def downgrade() -> None: - pass + conn = op.get_bind() + conn.execute( + sa.text( + """ + ALTER TYPE requeststatus ADD VALUE IF NOT EXISTS 'EXPIRED' + """, + ), + ) def pre_test_upgrade( diff --git a/tests/core/test_mypayment.py b/tests/core/test_mypayment.py index ba085ffd53..aca103ddbc 100644 --- a/tests/core/test_mypayment.py +++ b/tests/core/test_mypayment.py @@ -3970,7 +3970,7 @@ async def test_accept_expired_request( json=validation.model_dump(mode="json"), ) assert response.status_code == 400 - assert response.json()["detail"] == "Only pending requests can be confirmed" + assert response.json()["detail"] == "Request is expired" mocked_callback.assert_not_called()