feat(backend): complete Module 5 (drop-off points + auto-assign)
drop_off_points (with SPATIAL INDEX on coordinates) +
drop_off_capacity_logs. Public nearby query via ST_Distance_Sphere
returns DOPs sorted by distance with distance_meters in payload.
Admin CRUD plus capacity-log endpoint. Household creation auto-assigns
to the nearest active DOP within 25km; resident can re-run the lookup
via POST /households/{uuid}/reassign-drop-off.
97 feature tests passing.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
62
app/Services/DropOff/DropOffPointFinder.php
Normal file
62
app/Services/DropOff/DropOffPointFinder.php
Normal file
@@ -0,0 +1,62 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\DropOff;
|
||||
|
||||
use App\Models\DropOffPoint;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class DropOffPointFinder
|
||||
{
|
||||
/**
|
||||
* Return active DOPs within $radiusKm of the given coordinates,
|
||||
* ordered by distance ascending. Each result has a `distance_meters`
|
||||
* attribute attached.
|
||||
*
|
||||
* @return Collection<int, DropOffPoint>
|
||||
*/
|
||||
public function nearby(float $latitude, float $longitude, float $radiusKm = 5.0, int $limit = 25): Collection
|
||||
{
|
||||
$radiusMeters = $radiusKm * 1000;
|
||||
|
||||
$rows = DB::table('drop_off_points')
|
||||
->whereNull('deleted_at')
|
||||
->where('status', DropOffPoint::STATUS_ACTIVE)
|
||||
->select('id')
|
||||
->selectRaw(
|
||||
'ST_Distance_Sphere(coordinates, ST_SRID(POINT(?, ?), 4326)) AS distance_meters',
|
||||
[$longitude, $latitude],
|
||||
)
|
||||
->whereRaw(
|
||||
'ST_Distance_Sphere(coordinates, ST_SRID(POINT(?, ?), 4326)) <= ?',
|
||||
[$longitude, $latitude, $radiusMeters],
|
||||
)
|
||||
->orderBy('distance_meters')
|
||||
->limit($limit)
|
||||
->get();
|
||||
|
||||
if ($rows->isEmpty()) {
|
||||
return collect();
|
||||
}
|
||||
|
||||
$distancesById = $rows->pluck('distance_meters', 'id');
|
||||
$points = DropOffPoint::with('barangay')
|
||||
->whereIn('id', $rows->pluck('id'))
|
||||
->get()
|
||||
->keyBy('id');
|
||||
|
||||
return $rows->map(function ($r) use ($points, $distancesById) {
|
||||
$p = $points->get($r->id);
|
||||
if ($p) {
|
||||
$p->distance_meters = (float) $distancesById->get($r->id);
|
||||
}
|
||||
|
||||
return $p;
|
||||
})->filter()->values();
|
||||
}
|
||||
|
||||
public function nearest(float $latitude, float $longitude, float $maxKm = 25.0): ?DropOffPoint
|
||||
{
|
||||
return $this->nearby($latitude, $longitude, $maxKm, 1)->first();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user