refactor(komoot): align DTOs with Lombok class pattern

Signed-off-by: Marcus Fihlon <marcus@fihlon.swiss>
This commit is contained in:
Marcus Fihlon 2026-04-29 11:01:21 +02:00
parent 84735594f2
commit ea47cccdb1
Signed by: McPringle
GPG key ID: C6B7F469EE363E1F
8 changed files with 155 additions and 93 deletions

View file

@ -43,7 +43,7 @@ public class KomootImportController {
.getId(); .getId();
log.info("User {} requested Komoot activity preview for Komoot ID {}", log.info("User {} requested Komoot activity preview for Komoot ID {}",
authentication.getName(), request.userId()); authentication.getName(), request.getUserId());
KomootActivitiesResponse response = komootImportService.fetchCompletedActivities(request, fitPubUserId); KomootActivitiesResponse response = komootImportService.fetchCompletedActivities(request, fitPubUserId);
return ResponseEntity.ok(response); return ResponseEntity.ok(response);
} }
@ -58,7 +58,7 @@ public class KomootImportController {
.getId(); .getId();
log.info("User {} requested Komoot import for activity {}", log.info("User {} requested Komoot import for activity {}",
authentication.getName(), request.activityId()); authentication.getName(), request.getActivityId());
KomootImportExecutionResponse response = komootImportService.importActivity( KomootImportExecutionResponse response = komootImportService.importActivity(
request, request,

View file

@ -1,13 +1,22 @@
package net.javahippie.fitpub.model.dto; package net.javahippie.fitpub.model.dto;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.List; import java.util.List;
/** /**
* Response payload for the Komoot import preview. * Response payload for the Komoot import preview.
*/ */
public record KomootActivitiesResponse( @Data
String userId, @Builder
int totalCount, @NoArgsConstructor
List<KomootActivitySummaryDTO> activities @AllArgsConstructor
) { public class KomootActivitiesResponse {
private String userId;
private int totalCount;
private List<KomootActivitySummaryDTO> activities;
} }

View file

@ -4,25 +4,33 @@ import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Pattern; import jakarta.validation.constraints.Pattern;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
/** /**
* Request payload for importing one specific Komoot activity. * Request payload for importing one specific Komoot activity.
* *
* <p>The password is only used for the current request and is never persisted.</p> * <p>The password is only used for the current request and is never persisted.</p>
*/ */
public record KomootActivityImportRequest( @Data
@NotBlank @Builder
@Email @NoArgsConstructor
String email, @AllArgsConstructor
public class KomootActivityImportRequest {
@NotBlank @NotBlank
String password, @Email
private String email;
@NotBlank @NotBlank
@Pattern(regexp = "\\d+", message = "Komoot user ID must contain digits only") private String password;
String userId,
@NotNull @NotBlank
Long activityId @Pattern(regexp = "\\d+", message = "Komoot user ID must contain digits only")
) { private String userId;
@NotNull
private Long activityId;
} }

View file

@ -1,24 +1,33 @@
package net.javahippie.fitpub.model.dto; package net.javahippie.fitpub.model.dto;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.time.OffsetDateTime; import java.time.OffsetDateTime;
import java.util.UUID; import java.util.UUID;
/** /**
* Reduced activity representation returned by the Komoot import preview. * Reduced activity representation returned by the Komoot import preview.
*/ */
public record KomootActivitySummaryDTO( @Data
long id, @Builder
String name, @NoArgsConstructor
String sport, @AllArgsConstructor
String mappedActivityType, public class KomootActivitySummaryDTO {
String status,
String type, private long id;
OffsetDateTime date, private String name;
Double distanceMeters, private String sport;
Integer durationSeconds, private String mappedActivityType;
Integer timeInMotionSeconds, private String status;
Double elevationUp, private String type;
boolean imported, private OffsetDateTime date;
UUID fitPubActivityId private Double distanceMeters;
) { private Integer durationSeconds;
private Integer timeInMotionSeconds;
private Double elevationUp;
private boolean imported;
private UUID fitPubActivityId;
} }

View file

@ -1,14 +1,23 @@
package net.javahippie.fitpub.model.dto; package net.javahippie.fitpub.model.dto;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.UUID; import java.util.UUID;
/** /**
* Response for importing exactly one Komoot activity into FitPub. * Response for importing exactly one Komoot activity into FitPub.
*/ */
public record KomootImportExecutionResponse( @Data
UUID importedActivityId, @Builder
Long importedKomootActivityId, @NoArgsConstructor
String status, @AllArgsConstructor
String message public class KomootImportExecutionResponse {
) {
private UUID importedActivityId;
private Long importedKomootActivityId;
private String status;
private String message;
} }

View file

@ -1,8 +1,12 @@
package net.javahippie.fitpub.model.dto; package net.javahippie.fitpub.model.dto;
import jakarta.validation.constraints.AssertTrue;
import jakarta.validation.constraints.Email; import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Pattern; import jakarta.validation.constraints.Pattern;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.time.LocalDate; import java.time.LocalDate;
@ -11,23 +15,46 @@ import java.time.LocalDate;
* *
* <p>The password is only used for the current request and is never persisted.</p> * <p>The password is only used for the current request and is never persisted.</p>
*/ */
public record KomootImportRequest( @Data
@NotBlank @Builder
@Email @NoArgsConstructor
String email, public class KomootImportRequest {
@NotBlank @NotBlank
String password, @Email
private String email;
@NotBlank @NotBlank
@Pattern(regexp = "\\d+", message = "Komoot user ID must contain digits only") private String password;
String userId,
LocalDate startDate, @NotBlank
@Pattern(regexp = "\\d+", message = "Komoot user ID must contain digits only")
private String userId;
LocalDate endDate private LocalDate startDate;
) {
public KomootImportRequest { private LocalDate endDate;
public KomootImportRequest(String email, String password, String userId, LocalDate startDate, LocalDate endDate) {
this.email = email;
this.password = password;
this.userId = userId;
this.startDate = startDate;
this.endDate = endDate;
validateDateRange();
}
@AssertTrue(message = "Start date and end date must either both be set or both be empty, and start date must be before or equal to end date.")
public boolean isDateRangeConsistent() {
try {
validateDateRange();
return true;
} catch (IllegalArgumentException e) {
return false;
}
}
private void validateDateRange() {
boolean onlyOneDateProvided = (startDate == null) != (endDate == null); boolean onlyOneDateProvided = (startDate == null) != (endDate == null);
if (onlyOneDateProvided) { if (onlyOneDateProvided) {
throw new IllegalArgumentException("Start date and end date must either both be set or both be empty."); throw new IllegalArgumentException("Start date and end date must either both be set or both be empty.");

View file

@ -86,7 +86,7 @@ public class KomootImportService {
} }
URI nextUri = buildInitialUri(request); URI nextUri = buildInitialUri(request);
HttpEntity<Void> httpEntity = new HttpEntity<>(buildHeaders(request.email(), request.password())); HttpEntity<Void> httpEntity = new HttpEntity<>(buildHeaders(request.getEmail(), request.getPassword()));
try { try {
while (nextUri != null) { while (nextUri != null) {
@ -113,8 +113,8 @@ public class KomootImportService {
throw new IllegalStateException("Failed to parse Komoot activity list.", e); throw new IllegalStateException("Failed to parse Komoot activity list.", e);
} }
log.info("Fetched {} completed Komoot activities for user ID {}", activities.size(), request.userId()); log.info("Fetched {} completed Komoot activities for user ID {}", activities.size(), request.getUserId());
return new KomootActivitiesResponse(request.userId(), activities.size(), activities); return new KomootActivitiesResponse(request.getUserId(), activities.size(), activities);
} }
void pauseBeforeNextPageRequest() { void pauseBeforeNextPageRequest() {
@ -122,29 +122,29 @@ public class KomootImportService {
} }
public KomootImportExecutionResponse importActivity(KomootActivityImportRequest request, UUID fitPubUserId) { public KomootImportExecutionResponse importActivity(KomootActivityImportRequest request, UUID fitPubUserId) {
Activity existingActivity = activityRepository.findByUserIdAndKomootActivityId(fitPubUserId, request.activityId()).orElse(null); Activity existingActivity = activityRepository.findByUserIdAndKomootActivityId(fitPubUserId, request.getActivityId()).orElse(null);
if (existingActivity != null) { if (existingActivity != null) {
return new KomootImportExecutionResponse( return new KomootImportExecutionResponse(
existingActivity.getId(), existingActivity.getId(),
request.activityId(), request.getActivityId(),
"SKIPPED_ALREADY_IMPORTED", "SKIPPED_ALREADY_IMPORTED",
"Komoot activity " + request.activityId() + " was already imported." "Komoot activity " + request.getActivityId() + " was already imported."
); );
} }
JsonNode details = fetchActivityDetails(request.email(), request.password(), request.activityId()); JsonNode details = fetchActivityDetails(request.getEmail(), request.getPassword(), request.getActivityId());
pauseBetweenDetailAndGpxRequest(); pauseBetweenDetailAndGpxRequest();
byte[] gpxData = fetchActivityGpx(request.email(), request.password(), request.activityId()); byte[] gpxData = fetchActivityGpx(request.getEmail(), request.getPassword(), request.getActivityId());
ByteArrayMultipartFile gpxFile = new ByteArrayMultipartFile( ByteArrayMultipartFile gpxFile = new ByteArrayMultipartFile(
"file", "file",
"komoot-" + request.activityId() + ".gpx", "komoot-" + request.getActivityId() + ".gpx",
"application/gpx+xml", "application/gpx+xml",
gpxData gpxData
); );
Activity.Visibility mappedVisibility = mapVisibility(nullableText(details, "status")); Activity.Visibility mappedVisibility = mapVisibility(nullableText(details, "status"));
String mappedTitle = firstNonBlank(nullableText(details, "name"), null, "Komoot Activity " + request.activityId()); String mappedTitle = firstNonBlank(nullableText(details, "name"), null, "Komoot Activity " + request.getActivityId());
String mappedDescription = nullableText(details, "description"); String mappedDescription = nullableText(details, "description");
Activity.ActivityType mappedActivityType = mapKomootSportToActivityType(nullableText(details, "sport")); Activity.ActivityType mappedActivityType = mapKomootSportToActivityType(nullableText(details, "sport"));
@ -156,7 +156,7 @@ public class KomootImportService {
mappedVisibility mappedVisibility
); );
importedActivity.setKomootActivityId(request.activityId()); importedActivity.setKomootActivityId(request.getActivityId());
importedActivity.setTitle(mappedTitle); importedActivity.setTitle(mappedTitle);
importedActivity.setDescription(mappedDescription); importedActivity.setDescription(mappedDescription);
importedActivity.setVisibility(mappedVisibility); importedActivity.setVisibility(mappedVisibility);
@ -167,7 +167,7 @@ public class KomootImportService {
log.info( log.info(
"Imported Komoot activity {} into FitPub activity {} with visibility {} and type {}", "Imported Komoot activity {} into FitPub activity {} with visibility {} and type {}",
request.activityId(), request.getActivityId(),
importedActivity.getId(), importedActivity.getId(),
importedActivity.getVisibility(), importedActivity.getVisibility(),
importedActivity.getActivityType() importedActivity.getActivityType()
@ -177,9 +177,9 @@ public class KomootImportService {
return new KomootImportExecutionResponse( return new KomootImportExecutionResponse(
importedActivity.getId(), importedActivity.getId(),
request.activityId(), request.getActivityId(),
"IMPORTED", "IMPORTED",
"Imported Komoot activity " + request.activityId() + " into FitPub activity " + importedActivity.getId() "Imported Komoot activity " + request.getActivityId() + " into FitPub activity " + importedActivity.getId()
); );
} }
@ -193,15 +193,15 @@ public class KomootImportService {
private URI buildInitialUri(KomootImportRequest request) { private URI buildInitialUri(KomootImportRequest request) {
String normalizedBaseUrl = komootBaseUrl.endsWith("/") ? komootBaseUrl.substring(0, komootBaseUrl.length() - 1) : komootBaseUrl; String normalizedBaseUrl = komootBaseUrl.endsWith("/") ? komootBaseUrl.substring(0, komootBaseUrl.length() - 1) : komootBaseUrl;
UriComponentsBuilder builder = UriComponentsBuilder.fromUriString(normalizedBaseUrl + "/api/v007/users/" + request.userId() + "/tours/") UriComponentsBuilder builder = UriComponentsBuilder.fromUriString(normalizedBaseUrl + "/api/v007/users/" + request.getUserId() + "/tours/")
.queryParam("type", "tour_recorded") .queryParam("type", "tour_recorded")
.queryParam("sort_field", "date") .queryParam("sort_field", "date")
.queryParam("sort_direction", "desc") .queryParam("sort_direction", "desc")
.queryParam("limit", PAGE_SIZE); .queryParam("limit", PAGE_SIZE);
if (request.startDate() != null && request.endDate() != null) { if (request.getStartDate() != null && request.getEndDate() != null) {
builder.queryParam("start_date", formatKomootStartDate(request.startDate())) builder.queryParam("start_date", formatKomootStartDate(request.getStartDate()))
.queryParam("end_date", formatKomootEndDate(request.endDate())); .queryParam("end_date", formatKomootEndDate(request.getEndDate()));
} else { } else {
builder.queryParam("status", "private") builder.queryParam("status", "private")
.queryParam("name", "") .queryParam("name", "")

View file

@ -154,15 +154,15 @@ class KomootImportServiceTest {
new KomootImportRequest("user@example.com", "secret", "123456", null, null), new KomootImportRequest("user@example.com", "secret", "123456", null, null),
userId); userId);
assertThat(response.totalCount()).isEqualTo(2); assertThat(response.getTotalCount()).isEqualTo(2);
assertThat(response.activities()).hasSize(2); assertThat(response.getActivities()).hasSize(2);
assertThat(response.activities().get(0).id()).isEqualTo(1001L); assertThat(response.getActivities().get(0).getId()).isEqualTo(1001L);
assertThat(response.activities().get(0).imported()).isFalse(); assertThat(response.getActivities().get(0).isImported()).isFalse();
assertThat(response.activities().get(0).fitPubActivityId()).isNull(); assertThat(response.getActivities().get(0).getFitPubActivityId()).isNull();
assertThat(response.activities().get(0).timeInMotionSeconds()).isEqualTo(7800); assertThat(response.getActivities().get(0).getTimeInMotionSeconds()).isEqualTo(7800);
assertThat(response.activities().get(1).name()).isEqualTo("Lunch Walk"); assertThat(response.getActivities().get(1).getName()).isEqualTo("Lunch Walk");
assertThat(response.activities().get(1).imported()).isTrue(); assertThat(response.getActivities().get(1).isImported()).isTrue();
assertThat(response.activities().get(1).fitPubActivityId()).isEqualTo(existingActivityId); assertThat(response.getActivities().get(1).getFitPubActivityId()).isEqualTo(existingActivityId);
verify(throttledService).pauseBeforeNextPageRequest(); verify(throttledService).pauseBeforeNextPageRequest();
server.verify(); server.verify();
@ -219,11 +219,11 @@ class KomootImportServiceTest {
), ),
userId); userId);
assertThat(response.totalCount()).isEqualTo(2); assertThat(response.getTotalCount()).isEqualTo(2);
assertThat(response.activities()).extracting("id").containsExactly(1002L, 1003L); assertThat(response.getActivities()).extracting("id").containsExactly(1002L, 1003L);
assertThat(response.activities().get(0).imported()).isFalse(); assertThat(response.getActivities().get(0).isImported()).isFalse();
assertThat(response.activities().get(1).imported()).isTrue(); assertThat(response.getActivities().get(1).isImported()).isTrue();
assertThat(response.activities().get(1).fitPubActivityId()).isEqualTo(existingActivityId); assertThat(response.getActivities().get(1).getFitPubActivityId()).isEqualTo(existingActivityId);
server.verify(); server.verify();
} }
@ -296,9 +296,9 @@ class KomootImportServiceTest {
userId userId
); );
assertThat(response.importedActivityId()).isEqualTo(importedActivityId); assertThat(response.getImportedActivityId()).isEqualTo(importedActivityId);
assertThat(response.importedKomootActivityId()).isEqualTo(2880957035L); assertThat(response.getImportedKomootActivityId()).isEqualTo(2880957035L);
assertThat(response.status()).isEqualTo("IMPORTED"); assertThat(response.getStatus()).isEqualTo("IMPORTED");
assertThat(importedActivity.getKomootActivityId()).isEqualTo(2880957035L); assertThat(importedActivity.getKomootActivityId()).isEqualTo(2880957035L);
assertThat(importedActivity.getTitle()).isEqualTo("Latest Ride"); assertThat(importedActivity.getTitle()).isEqualTo("Latest Ride");
assertThat(importedActivity.getDescription()).isEqualTo("Imported from Komoot"); assertThat(importedActivity.getDescription()).isEqualTo("Imported from Komoot");
@ -326,9 +326,9 @@ class KomootImportServiceTest {
userId userId
); );
assertThat(response.importedActivityId()).isEqualTo(existingActivityId); assertThat(response.getImportedActivityId()).isEqualTo(existingActivityId);
assertThat(response.importedKomootActivityId()).isEqualTo(3002L); assertThat(response.getImportedKomootActivityId()).isEqualTo(3002L);
assertThat(response.status()).isEqualTo("SKIPPED_ALREADY_IMPORTED"); assertThat(response.getStatus()).isEqualTo("SKIPPED_ALREADY_IMPORTED");
} }
@Test @Test
@ -386,8 +386,8 @@ class KomootImportServiceTest {
userId userId
); );
assertThat(response.importedActivityId()).isEqualTo(importedActivityId); assertThat(response.getImportedActivityId()).isEqualTo(importedActivityId);
assertThat(response.status()).isEqualTo("IMPORTED"); assertThat(response.getStatus()).isEqualTo("IMPORTED");
assertThat(importedActivity.getActivityType()).isEqualTo(Activity.ActivityType.OTHER); assertThat(importedActivity.getActivityType()).isEqualTo(Activity.ActivityType.OTHER);
verify(throttledService).pauseBetweenDetailAndGpxRequest(); verify(throttledService).pauseBetweenDetailAndGpxRequest();