I know my answer will be a bit late to the party, but I hope it can save someone a bit of hair on their head, since it took me a good long evening to figure it out when I faced the same problem.
What we want to achieve here is not possible with an Eloquent builder only, we have to go a level deeper - to a DB query builder and just a touch of raw SQL.
Basically, when we say whereHas('projectstatuses', function...) we're creating an exists subquery. This subquery will be executed for each of the elements of the result of the main query. A result of the main query will be returned to us only if the result of its subquery is not empty. Hope it makes sense.
So, we want to write a subquery that will return something only when the latest projectstatus has a status_id we are asking.
Now, where('status_id, 5) is pretty obvious. We want to add another where clause to work with the latest entry related to the project in question. To get one specific entry out of a table we usually use one of the aggregate SQL functions, like max(), avg() or count(). We can go for the latest entry by incorporating max(created_at) for example. But I would suggest going with max(id) since it is basically the same by the logic of the table we're working with and has zero likelihood of ever being non-unique. Also, you'll see why it makes more sense in a second.
So, here's how you get an ID of the latest project status for a given project for example:
DB::table('projectstatuses')
->selectRaw('max(id)')
->where('project_id', $project->id)
->get();
Last thing we need to know is, while it isn't 100% obvious from the documentation, we can pass a closure to other where statements the same way we do it for whereHas, which will create the same sort of subquery. So, since we now know how to get the id of the latest status of the project, lets use a whereIn('id', ...) statement like so:
->whereIn('id', function (QueryBuilder $query) {
$query
->selectRaw('max(id)')
->from('projectstatuses')
->whereColumn('project_id', 'projects.id');
});
Note, that we're using whereColumn because we're now comparing a column to another column, not to a value.
Finally, let's combine all of that into a single query:
use Illuminate\Database\Eloquent\Builder as EloquentBuilder;
use Illuminate\Database\Query\Builder as QueryBuilder;
...
Project::whereHas('projectstatuses', function (EloquentBuilder $query) {
$query
->where('status_id', 5)
->whereIn('id', function (QueryBuilder $query) {
$query
->selectRaw('max(id)')
->from('projectstatuses')
->whereColumn('project_id', 'projects.id');
});
})->get();
How to make it into a scope is trivial from here.
Hope my explanation wasn't too convoluted. I want you to actually understand the direction of thought that brings you to solving this sort of problems.
Bonus:
Try changing ->get() to ->dd(). This dumps a full query how it will be sent to a DB with all the bindings. Very helpful to actually understand what Laravel is trying to do behind the scenes. Laravel can be a bit unintuitive sometimes.