Files
Verde-Web/app/Http/Controllers/Api/V1/Household/HouseholdController.php
admin 1150a518e8 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>
2026-04-30 00:11:22 +08:00

231 lines
7.9 KiB
PHP

<?php
namespace App\Http\Controllers\Api\V1\Household;
use App\Http\Controllers\Api\V1\ApiController;
use App\Http\Requests\Household\StoreHouseholdMemberRequest;
use App\Http\Requests\Household\StoreHouseholdRequest;
use App\Http\Requests\Household\UpdateHouseholdRequest;
use App\Http\Requests\Household\UploadProofRequest;
use App\Http\Resources\HouseholdResource;
use App\Models\Household;
use App\Models\HouseholdMember;
use App\Models\User;
use App\Services\DropOff\DropOffPointFinder;
use App\Services\Geo\GeoLocationService;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Storage;
use MatanYadaev\EloquentSpatial\Objects\Point;
class HouseholdController extends ApiController
{
public function store(
StoreHouseholdRequest $request,
GeoLocationService $geo,
DropOffPointFinder $dopFinder,
): JsonResponse {
$data = $request->validated();
$user = $request->user();
$existing = Household::where('head_user_id', $user->id)->first();
if ($existing) {
return $this->fail(
'You already have a household. Update it instead.',
['head_user_id' => ['already_has_household']],
409,
);
}
$point = new Point((float) $data['lat'], (float) $data['lng'], 4326);
$barangayId = $data['barangay_id'] ?? null;
if (! $barangayId) {
$resolved = $geo->findBarangayByCoordinates((float) $data['lat'], (float) $data['lng']);
$barangayId = $resolved?->id;
}
$nearestDop = $dopFinder->nearest((float) $data['lat'], (float) $data['lng']);
$household = DB::transaction(function () use ($data, $user, $point, $barangayId, $nearestDop) {
$h = Household::create([
'head_user_id' => $user->id,
'barangay_id' => $barangayId,
'address_line' => $data['address_line'],
'coordinates' => $point,
'household_size' => $data['household_size'],
'assigned_drop_off_point_id' => $nearestDop?->id,
'verification_status' => Household::VERIFICATION_PENDING,
]);
HouseholdMember::create([
'household_id' => $h->id,
'user_id' => $user->id,
'relationship' => HouseholdMember::RELATIONSHIP_HEAD,
'full_name' => $user->full_name,
]);
return $h;
});
return $this->created(
new HouseholdResource(
$household->fresh()
->load(['head', 'barangay', 'members.user', 'assignedDropOffPoint'])
->loadCount('members'),
),
'Household created. Upload proof of residency to begin verification.',
);
}
public function show(Request $request, Household $household): JsonResponse
{
$this->authorizeView($request->user(), $household);
$household->load(['head', 'barangay.cityMunicipality', 'members.user', 'assignedDropOffPoint'])
->loadCount('members');
return $this->ok(new HouseholdResource($household));
}
public function reassignDropOff(Request $request, Household $household, DropOffPointFinder $finder): JsonResponse
{
$this->authorizeOwn($request->user(), $household);
if (! $household->coordinates) {
return $this->fail('Household has no coordinates on file', null, 422);
}
$nearest = $finder->nearest(
$household->coordinates->latitude,
$household->coordinates->longitude,
);
$household->forceFill(['assigned_drop_off_point_id' => $nearest?->id])->save();
return $this->ok(
new HouseholdResource(
$household->fresh()->load(['assignedDropOffPoint', 'barangay'])->loadCount('members'),
),
$nearest ? 'Reassigned to nearest drop-off point' : 'No nearby drop-off point found',
);
}
public function update(UpdateHouseholdRequest $request, Household $household): JsonResponse
{
$this->authorizeOwn($request->user(), $household);
if ($household->verification_status === Household::VERIFICATION_APPROVED) {
return $this->fail(
'Approved households cannot be edited. Contact an admin.',
null,
422,
);
}
$data = $request->validated();
if (isset($data['lat'], $data['lng'])) {
$data['coordinates'] = new Point((float) $data['lat'], (float) $data['lng'], 4326);
}
unset($data['lat'], $data['lng']);
$household->update($data);
// Editing a rejected household resets it to pending.
if ($household->verification_status === Household::VERIFICATION_REJECTED) {
$household->forceFill([
'verification_status' => Household::VERIFICATION_PENDING,
'rejection_reason' => null,
])->save();
}
return $this->ok(
new HouseholdResource(
$household->fresh()->load(['head', 'barangay', 'members.user'])->loadCount('members'),
),
'Household updated',
);
}
public function uploadProof(UploadProofRequest $request, Household $household): JsonResponse
{
$this->authorizeOwn($request->user(), $household);
$path = $request->file('proof')->store('households/proofs', 'local');
$household->forceFill(['proof_of_residency_path' => $path]);
if ($household->verification_status === Household::VERIFICATION_REJECTED) {
$household->forceFill([
'verification_status' => Household::VERIFICATION_PENDING,
'rejection_reason' => null,
]);
}
$household->save();
return $this->ok(
new HouseholdResource($household->fresh()->load(['head', 'barangay'])->loadCount('members')),
'Proof of residency uploaded',
);
}
public function addMember(StoreHouseholdMemberRequest $request, Household $household): JsonResponse
{
$this->authorizeOwn($request->user(), $household);
$data = $request->validated();
$member = HouseholdMember::create([
'household_id' => $household->id,
'user_id' => $data['user_id'] ?? null,
'relationship' => $data['relationship'],
'full_name' => $data['full_name'] ?? null,
'date_of_birth' => $data['date_of_birth'] ?? null,
]);
return $this->created(
new HouseholdResource(
$household->fresh()->load(['head', 'barangay', 'members.user'])->loadCount('members'),
),
'Member added',
);
}
public function removeMember(Request $request, Household $household, HouseholdMember $member): JsonResponse
{
$this->authorizeOwn($request->user(), $household);
if ($member->household_id !== $household->id) {
return $this->notFound('Member not found in this household');
}
if ($member->relationship === HouseholdMember::RELATIONSHIP_HEAD) {
return $this->fail('Cannot remove the head of household', null, 422);
}
$member->delete();
return $this->ok(null, 'Member removed');
}
private function authorizeOwn(User $user, Household $household): void
{
if ($household->head_user_id !== $user->id) {
abort(403, 'You do not own this household');
}
}
private function authorizeView(User $user, Household $household): void
{
if ($household->head_user_id === $user->id) {
return;
}
if ($user->role === User::ROLE_ADMIN) {
return;
}
if ($household->members()->where('user_id', $user->id)->exists()) {
return;
}
abort(403, 'You do not have access to this household');
}
}