user(); $team = CollectionTeam::query() ->with(['driver', 'scanner', 'truck', 'helpers.user']) ->where('scanner_id', $user->id) ->where('status', CollectionTeam::STATUS_ACTIVE) ->first(); if (! $team) { return $this->fail('No active collection team assigned to this scanner', null, 404); } $trip = Trip::query() ->with(['route.stops.dropOffPoint', 'stops.dropOffPoint']) ->where('team_id', $team->id) ->whereIn('status', [Trip::STATUS_SCHEDULED, Trip::STATUS_IN_PROGRESS, Trip::STATUS_AT_DUMPSITE]) ->orderBy('scheduled_date') ->first(); return $this->ok([ 'team' => CollectionTeamResource::make($team), 'active_trip' => $trip ? TripResource::make($trip) : null, ]); } public function scan(Request $request): JsonResponse { Log::info('DEBUG: Entering ScannerController::scan'); $data = $request->validate([ 'serial' => ['required', 'string', 'max:64'], 'drop_off_point_id' => ['required', 'integer', 'exists:drop_off_points,id'], 'lat' => ['required', 'numeric', 'between:-90,90'], 'lng' => ['required', 'numeric', 'between:-180,180'], 'trip_id' => ['nullable', 'string', 'exists:trips,uuid'], 'trip_stop_id' => ['nullable', 'integer', 'exists:trip_stops,id'], 'weight_kg' => ['nullable', 'integer', 'min:0'], 'waste_type' => ['nullable', 'string', 'max:50'], 'photo_path' => ['nullable', 'string', 'max:255'], 'notes' => ['nullable', 'string', 'max:500'], 'scanned_at' => ['nullable', 'date'], ]); $dop = DropOffPoint::findOrFail($data['drop_off_point_id']); $trip = isset($data['trip_id']) ? Trip::where('uuid', $data['trip_id'])->first() : null; $stop = isset($data['trip_stop_id']) ? TripStop::find($data['trip_stop_id']) : null; Log::info('Scan attempt', ['serial' => $data['serial'] ?? 'none']); try { $result = $this->scanner->scan( serial: $data['serial'], dropOff: $dop, scanner: $request->user(), lat: (float) $data['lat'], lng: (float) $data['lng'], trip: $trip, tripStop: $stop, weightKg: $data['weight_kg'] ?? null, wasteType: $data['waste_type'] ?? null, photoPath: $data['photo_path'] ?? null, notes: $data['notes'] ?? null, scannedAt: isset($data['scanned_at']) ? Carbon::parse($data['scanned_at']) : null, ); } catch (\Throwable $e) { Log::error('DEBUG: Scan Exception: '.$e->getMessage()."\n".$e->getTraceAsString()); throw $e; } Log::info('DEBUG: Scan completed', ['accepted' => $result->accepted, 'reason' => $result->rejectReason ?? 'none']); if (! $result->accepted) { Log::warning('Scan rejected internally', ['serial' => $data['serial'], 'reason' => $result->rejectReason]); return $this->fail( 'Scan rejected', ['reason' => [$result->rejectReason]], 422, ); } $res = [ 'serial' => $result->code->serial, 'log_id' => $result->log->id, 'status' => (string) $result->code->status, ]; Log::info('Scan Response OK', $res); return $this->ok($res, 'Scan recorded'); } /** * Bulk scan endpoint for offline sync. Each scan is processed * independently; failures don't abort the batch. */ public function bulk(Request $request): JsonResponse { $data = $request->validate([ 'scans' => ['required', 'array', 'min:1', 'max:200'], 'scans.*.serial' => ['required', 'string'], 'scans.*.drop_off_point_id' => ['required', 'integer', 'exists:drop_off_points,id'], 'scans.*.lat' => ['required', 'numeric'], 'scans.*.lng' => ['required', 'numeric'], 'scans.*.trip_id' => ['nullable', 'string'], 'scans.*.trip_stop_id' => ['nullable', 'integer'], 'scans.*.scanned_at' => ['nullable', 'date'], ]); $results = []; foreach ($data['scans'] as $i => $s) { try { $dop = DropOffPoint::find($s['drop_off_point_id']); if (! $dop) { $results[] = ['index' => $i, 'serial' => $s['serial'], 'accepted' => false, 'reason' => 'dop_not_found']; continue; } $trip = isset($s['trip_id']) ? Trip::where('uuid', $s['trip_id'])->first() : null; $stop = isset($s['trip_stop_id']) ? TripStop::find($s['trip_stop_id']) : null; $r = $this->scanner->scan( serial: $s['serial'], dropOff: $dop, scanner: $request->user(), lat: (float) $s['lat'], lng: (float) $s['lng'], trip: $trip, tripStop: $stop, scannedAt: isset($s['scanned_at']) ? Carbon::parse($s['scanned_at']) : null, ); $results[] = [ 'index' => $i, 'serial' => $s['serial'], 'accepted' => $r->accepted, 'reason' => $r->rejectReason, ]; } catch (\Throwable $e) { $results[] = ['index' => $i, 'serial' => $s['serial'], 'accepted' => false, 'reason' => 'exception']; } } $accepted = collect($results)->where('accepted', true)->count(); return $this->ok([ 'results' => $results, 'accepted_count' => $accepted, 'rejected_count' => count($results) - $accepted, ], "Processed {$accepted} of ".count($results).' scans'); } }