have_posts() return false on single post

Late answer but, one possible scenario this could be happening is if you register two custom post types with the same permalink slug.

You can debug this by doing a var_dump of the global variable $wp_query.

global $wp_query;
var_dump($wp_query);

Check to see if the post_type in the query matches the post type of the page you are currently visiting.

If you find that it does not match then your next step is to check out the arguments where both the post types are being registered. Take a look at the rewrite specifically. If the slug is set to the same value, you will have a conflict between the two custom post types.

Bad

// post type (a)
$args_a = array(
  'rewrite' => array(
    'slug' => 'hello'
  ),
);

// post type (b)
$args_b = array(
  'rewrite' => array(
    'slug' => 'hello'
  ),
);

Good

// post type (a)
$args_a = array(
  'rewrite' => array(
    'slug' => 'hello'
  ),
);

// post type (b)
$args_b = array(
  'rewrite' => array(
    'slug' => 'world' // <-- this is now different.
  ),
);

That alone may not fix your issue because you will need to flush the permalinks. To do that, you can just go to the Admin Dashboard > Settings > Permalinks page and click on the save changes button. That will flush the permalinks so they are now updated to be hello and world respectively for your custom post types.