diff --git a/app/Http/Controllers/StravaController.php b/app/Http/Controllers/StravaController.php index 40df113..9dead08 100644 --- a/app/Http/Controllers/StravaController.php +++ b/app/Http/Controllers/StravaController.php @@ -5,6 +5,7 @@ namespace App\Http\Controllers; use App\Enums\WorkoutSource; use App\Http\Requests\StravaCallbackRequest; use App\Http\Requests\StravaSyncRequest; +use App\Models\StravaConnection; use App\Models\WorkoutSessions; use App\Services\StravaClient; use Illuminate\Http\JsonResponse; @@ -111,11 +112,13 @@ class StravaController extends Controller ], 422); } - $activities = $strava->activities( + $activities = collect($strava->activities( $connection, $request->integer('page', 1), $request->integer('per_page', 30), - ); + )) + ->map(fn (array $activity): array => $this->activityForStorage($strava, $connection, $activity)) + ->all(); $importedCount = collect($activities) ->map(fn (array $activity): WorkoutSessions => $this->storeActivity($request, $activity)) @@ -144,6 +147,15 @@ class StravaController extends Controller ]); } + private function activityForStorage(StravaClient $strava, StravaConnection $connection, array $activity): array + { + if (array_key_exists('calories', $activity) || empty($activity['id'])) { + return $activity; + } + + return array_replace($activity, $strava->activity($connection, $activity['id'])); + } + private function canReadActivities(array $scopes): bool { return in_array('activity:read', $scopes, true) diff --git a/app/Services/StravaClient.php b/app/Services/StravaClient.php index 8d52e46..57042b0 100644 --- a/app/Services/StravaClient.php +++ b/app/Services/StravaClient.php @@ -79,6 +79,20 @@ class StravaClient ->json(); } + public function activity(StravaConnection $connection, int|string $activityId): array + { + return Http::baseUrl('https://www.strava.com/api/v3') + ->withToken($this->accessToken($connection)) + ->timeout(10) + ->connectTimeout(3) + ->retry([100, 500, 1000]) + ->get("/activities/{$activityId}", [ + 'include_all_efforts' => false, + ]) + ->throw() + ->json(); + } + private function accessToken(StravaConnection $connection): string { if (! $connection->expires_at || $connection->expires_at->lte(now()->addMinutes(5))) { diff --git a/tests/Feature/StravaConnectionTest.php b/tests/Feature/StravaConnectionTest.php index bb9b927..63b7945 100644 --- a/tests/Feature/StravaConnectionTest.php +++ b/tests/Feature/StravaConnectionTest.php @@ -144,6 +144,57 @@ it('syncs strava activities into workouts', function () { ]); }); +it('syncs strava calories from detailed activities when summaries omit them', function () { + Http::preventStrayRequests(); + Http::fake([ + 'https://www.strava.com/api/v3/athlete/activities*' => Http::response([ + [ + 'id' => 1122334455, + 'name' => 'Evening ride', + 'sport_type' => 'Ride', + 'moving_time' => 2410, + 'distance' => 20500.2, + 'start_date' => '2026-05-19T17:45:00Z', + ], + ]), + 'https://www.strava.com/api/v3/activities/1122334455*' => Http::response([ + 'id' => 1122334455, + 'name' => 'Evening ride', + 'sport_type' => 'Ride', + 'moving_time' => 2410, + 'calories' => 618.4, + 'distance' => 20500.2, + 'start_date' => '2026-05-19T17:45:00Z', + ]), + ]); + + $user = User::factory()->create(); + StravaConnection::create([ + 'user_id' => $user->id, + 'athlete_id' => 123456, + 'access_token' => 'access-token', + 'refresh_token' => 'refresh-token', + 'expires_at' => now()->addHour(), + 'scopes' => ['read', 'activity:read'], + 'athlete' => ['id' => 123456], + ]); + + Sanctum::actingAs($user); + + $this + ->postJson('/api/strava/sync') + ->assertOk() + ->assertJsonPath('data.importedCount', 1) + ->assertJsonPath('data.fetchedCount', 1); + + $this->assertDatabaseHas('workout_sessions', [ + 'user_id' => $user->id, + 'source' => WorkoutSource::STRAVA->value, + 'external_id' => '1122334455', + 'calories_burned' => 618, + ]); +}); + it('requires an activity read scope before syncing strava activities', function () { $user = User::factory()->create(); StravaConnection::create([