diff --git a/backend/routes/exchange.py b/backend/routes/exchange.py index 23a1264..e96f352 100644 --- a/backend/routes/exchange.py +++ b/backend/routes/exchange.py @@ -401,8 +401,8 @@ async def create_exchange( detail=f"EUR amount must be a multiple of €{EUR_TRADE_INCREMENT}", ) - # Validate slot timing - valid_minutes = (0, 15, 30, 45) + # Validate slot timing - compute valid boundaries from slot duration + valid_minutes = tuple(range(0, 60, SLOT_DURATION_MINUTES)) if request.slot_start.minute not in valid_minutes: raise HTTPException( status_code=400, diff --git a/backend/tests/test_exchange.py b/backend/tests/test_exchange.py index 5b0a019..e1095de 100644 --- a/backend/tests/test_exchange.py +++ b/backend/tests/test_exchange.py @@ -332,6 +332,27 @@ class TestCreateExchange: assert response.status_code == 400 assert "multiple" in response.json()["detail"] + @pytest.mark.asyncio + async def test_slot_not_on_minute_boundary_rejected( + self, client_factory, regular_user, admin_user + ): + """Slot start time not on slot duration boundary is rejected.""" + target_date = await setup_availability_and_price(client_factory, admin_user) + + with mock_price_fetcher(20000.0): + async with client_factory.create(cookies=regular_user["cookies"]) as client: + response = await client.post( + "/api/exchange", + json={ + "slot_start": f"{target_date}T09:07:00Z", # 07 is not on 15-min boundary + "direction": "buy", + "eur_amount": 10000, + }, + ) + + assert response.status_code == 400 + assert "boundary" in response.json()["detail"].lower() + @pytest.mark.asyncio async def test_stale_price_blocks_booking( self, client_factory, regular_user, admin_user