Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
91 changes: 30 additions & 61 deletions .cursor/rules.json
Original file line number Diff line number Diff line change
Expand Up @@ -789,77 +789,46 @@
"enforcement": "This rule is NON-NEGOTIABLE. Any file created in the wrong location must be immediately moved to its correct organized directory."
},
"java_version_management": {
"description": "CRITICAL: This project requires Java 17, but the system may have Java 25 (or other versions) as default",
"description": "CRITICAL: This project requires Java 17, but the system has Java 25 as default which is INCOMPATIBLE",
"project_java_version": "17",
"java_17_path": "/Library/Java/JavaVirtualMachines/zulu-17.jdk/Contents/Home",
"mandatory_rules": [
"ALWAYS set JAVA_HOME to Java 17 before running ANY Maven commands",
"NEVER assume the default Java version is correct",
"ALWAYS verify Java version before building, testing, or compiling",
"The project uses Java 17 features and Lombok 1.18.36 which is NOT compatible with Java 25"
"ALWAYS use Java 17 for ANY Maven command - the default Java 25 will NOT work",
"NEVER run ./mvnw commands without prefixing with JAVA_HOME",
"The project uses Lombok 1.18.36 which is NOT compatible with Java 21+",
"If you see Lombok errors, it's almost certainly a Java version issue"
],
"required_java_home_setup": {
"command": "export JAVA_HOME=$(/usr/libexec/java_home -v 17)",
"explanation": "Sets JAVA_HOME to Java 17 on macOS",
"when_to_use": "At the start of EVERY terminal session before any Maven command"
},
"maven_commands_requiring_java_17": [
"./mvnw clean compile",
"./mvnw test-compile",
"./mvnw clean test",
"./mvnw test",
"./mvnw clean install",
"./mvnw clean package",
"./mvnw spring-boot:run"
],
"correct_command_pattern": {
"single_command": "export JAVA_HOME=$(/usr/libexec/java_home -v 17) && cd /Users/daggerpov/Documents/GitHub/Spawn-App-Back-End && ./mvnw clean test",
"explanation": "Always prefix Maven commands with JAVA_HOME setup in the same command to ensure correct Java version"
"simplest_command_pattern": {
"description": "Use inline JAVA_HOME assignment for simplicity",
"compile": "JAVA_HOME=/Library/Java/JavaVirtualMachines/zulu-17.jdk/Contents/Home ./mvnw clean compile",
"test_compile": "JAVA_HOME=/Library/Java/JavaVirtualMachines/zulu-17.jdk/Contents/Home ./mvnw test-compile",
"test": "JAVA_HOME=/Library/Java/JavaVirtualMachines/zulu-17.jdk/Contents/Home ./mvnw clean test",
"build_all": "JAVA_HOME=/Library/Java/JavaVirtualMachines/zulu-17.jdk/Contents/Home ./mvnw clean compile test-compile"
},
"error_indicators": {
"lombok_error": "java.lang.NoSuchFieldException: com.sun.tools.javac.code.TypeTag :: UNKNOWN",
"compilation_error": "Fatal error compiling: java.lang.ExceptionInInitializerError",
"wrong_java_message": "These errors typically indicate Java version mismatch (using Java 25 instead of Java 17)"
"lombok_typetag_error": "java.lang.NoSuchFieldException: com.sun.tools.javac.code.TypeTag :: UNKNOWN",
"initialization_error": "Fatal error compiling: java.lang.ExceptionInInitializerError",
"cause": "These errors mean you ran Maven with Java 25 instead of Java 17"
},
"debugging_workflow": {
"step_1": "Check current Java version: java -version",
"step_2": "If not Java 17, set JAVA_HOME: export JAVA_HOME=$(/usr/libexec/java_home -v 17)",
"step_3": "Verify change: java -version (should show Java 17)",
"step_4": "Re-run the Maven command"
},
"available_java_versions_on_system": [
"Java 25 (default, but INCOMPATIBLE)",
"Java 23 (available, but INCOMPATIBLE)",
"Java 17 (REQUIRED for this project)",
"Java 11 (available, but too old)"
"quick_fix_workflow": [
"1. If you see Lombok/TypeTag errors, DO NOT try to fix Lombok",
"2. Simply re-run the command with: JAVA_HOME=/Library/Java/JavaVirtualMachines/zulu-17.jdk/Contents/Home ./mvnw <command>",
"3. The build should succeed immediately"
],
"available_java_versions_on_system": {
"java_25": "/Users/daggerpov/Library/Java/JavaVirtualMachines/openjdk-25/Contents/Home (DEFAULT - DO NOT USE)",
"java_17": "/Library/Java/JavaVirtualMachines/zulu-17.jdk/Contents/Home (USE THIS)"
},
"why_java_17": [
"Project is configured for Java 17 in pom.xml (java.version=17)",
"Lombok 1.18.36 has compatibility issues with Java 21+",
"Spring Boot 3.3.5 works best with Java 17",
"All dependencies are tested with Java 17"
"pom.xml specifies java.version=17",
"Lombok 1.18.36 does not support Java 21+",
"Spring Boot 3.3.5 is optimized for Java 17"
],
"automation_principle": {
"rule": "ALWAYS include JAVA_HOME setup in the same command as Maven execution",
"reason": "Prevents forgetting to set JAVA_HOME and encountering compilation errors",
"pattern": "export JAVA_HOME=$(/usr/libexec/java_home -v 17) && cd <project-dir> && ./mvnw <command>"
},
"testing_workflow": {
"step_1_check_java": "export JAVA_HOME=$(/usr/libexec/java_home -v 17) && java -version",
"step_2_build": "cd /Users/daggerpov/Documents/GitHub/Spawn-App-Back-End && ./mvnw clean test",
"step_3_verify": "Ensure 'BUILD SUCCESS' with no Java version errors"
},
"forbidden_actions": [
"NEVER run Maven commands without setting JAVA_HOME first",
"NEVER assume java -version shows Java 17 by default",
"NEVER try to fix Lombok errors without checking Java version first",
"NEVER update Lombok version to work with Java 25 (use Java 17 instead)"
"NEVER run ./mvnw without JAVA_HOME prefix",
"NEVER try to upgrade Lombok to fix Java 25 compatibility",
"NEVER assume the default java -version is correct"
],
"quick_reference": {
"verify_java": "java -version (should show Java 17)",
"set_java_17": "export JAVA_HOME=$(/usr/libexec/java_home -v 17)",
"list_available": "/usr/libexec/java_home -V",
"build_with_correct_java": "export JAVA_HOME=$(/usr/libexec/java_home -v 17) && ./mvnw clean test"
},
"enforcement": "This is MANDATORY. Java version issues waste time and cause confusing errors. ALWAYS set JAVA_HOME to Java 17 before ANY Maven command."
"enforcement": "MANDATORY: Prefix ALL Maven commands with JAVA_HOME=/Library/Java/JavaVirtualMachines/zulu-17.jdk/Contents/Home"
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.danielagapov.spawn.activity.api;

import com.danielagapov.spawn.activity.api.dto.ActivityCreationResponseDTO;
import com.danielagapov.spawn.activity.api.dto.ActivityDTO;
import com.danielagapov.spawn.activity.api.dto.ActivityPartialUpdateDTO;
import com.danielagapov.spawn.activity.api.dto.FullFeedActivityDTO;
Expand Down Expand Up @@ -70,16 +71,18 @@ public ResponseEntity<?> getProfileActivities(@PathVariable UUID profileUserId,

// full path: /api/v1/activities
@PostMapping
public ResponseEntity<FullFeedActivityDTO> createActivity(@RequestBody ActivityDTO activityDTO) {
public ResponseEntity<ActivityCreationResponseDTO> createActivity(@RequestBody ActivityDTO activityDTO) {
try {
FullFeedActivityDTO response = activityService.createActivityWithSuggestions(activityDTO);
FullFeedActivityDTO createdActivity = activityService.createActivityWithSuggestions(activityDTO);
// Wrap in ActivityCreationResponseDTO to match iOS expected structure
ActivityCreationResponseDTO response = new ActivityCreationResponseDTO(createdActivity);
return new ResponseEntity<>(response, HttpStatus.CREATED);
} catch (IllegalArgumentException e) {
logger.error("Invalid request for activity creation: " + e.getMessage());
return new ResponseEntity<FullFeedActivityDTO>(HttpStatus.BAD_REQUEST);
return new ResponseEntity<ActivityCreationResponseDTO>(HttpStatus.BAD_REQUEST);
} catch (BaseNotFoundException e) {
logger.error("Entity not found during activity creation: " + e.getMessage());
return new ResponseEntity<FullFeedActivityDTO>(HttpStatus.NOT_FOUND);
return new ResponseEntity<ActivityCreationResponseDTO>(HttpStatus.NOT_FOUND);
} catch (Exception e) {
logger.error("Error creating activity: " + e.getMessage());
return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package com.danielagapov.spawn.activity.api.dto;

import com.fasterxml.jackson.annotation.JsonInclude;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;

/**
* Response DTO for activity creation that wraps the created activity
* and optionally includes friend suggestions for activity types.
*/
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@JsonInclude(JsonInclude.Include.NON_NULL)
public class ActivityCreationResponseDTO {
private FullFeedActivityDTO activity;
private ActivityTypeFriendSuggestionDTO friendSuggestion;

/**
* Create a response with just the activity (no friend suggestion)
*/
public ActivityCreationResponseDTO(FullFeedActivityDTO activity) {
this.activity = activity;
this.friendSuggestion = null;
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package com.danielagapov.spawn.activity.api.dto;

import com.danielagapov.spawn.user.api.dto.BaseUserDTO;
import com.danielagapov.spawn.user.api.dto.FriendUser.MinimalFriendDTO;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
Expand All @@ -10,14 +10,21 @@
import java.util.List;
import java.util.UUID;

/**
* DTO for activity types.
*
* Note: associatedFriends uses MinimalFriendDTO instead of BaseUserDTO to reduce memory usage.
* MinimalFriendDTO only contains essential fields (id, username, name, profilePicture)
* needed for displaying friends in activity type selection UI.
*/
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public class ActivityTypeDTO implements Serializable {
private UUID id;
private String title;
private List<BaseUserDTO> associatedFriends;
private List<MinimalFriendDTO> associatedFriends;
private String icon;
private int orderNum;
private UUID ownerUserId;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1324,8 +1324,8 @@ public List<ProfileActivityDTO> getPastActivitiesWhereUserInvited(UUID inviterUs
}

/**
* Gets feed Activities for a profile. If the profile user has no upcoming Activities, returns past Activities
* that the profile user invited the requesting user to, with a flag indicating they are past Activities.
* Gets Activities for a profile where the requesting user was invited or is participating.
* Includes both upcoming and past Activities, each flagged appropriately.
*
* @param profileUserId The user ID of the profile being viewed
* @param requestingUserId The user ID of the user viewing the profile
Expand All @@ -1334,32 +1334,88 @@ public List<ProfileActivityDTO> getPastActivitiesWhereUserInvited(UUID inviterUs
@Override
public List<ProfileActivityDTO> getProfileActivities(UUID profileUserId, UUID requestingUserId) {
try {
// Get upcoming Activities created by the profile user
List<ActivityDTO> upcomingActivities = getActivitiesByOwnerId(profileUserId);
List<FullFeedActivityDTO> upcomingFullActivities = convertActivitiesToFullFeedSelfOwnedActivities(upcomingActivities, requestingUserId);
// Get ALL Activities created by the profile user
List<ActivityDTO> allActivities = getActivitiesByOwnerId(profileUserId);
List<FullFeedActivityDTO> allFullActivities = convertActivitiesToFullFeedSelfOwnedActivities(allActivities, requestingUserId);

// Remove expired Activities
List<FullFeedActivityDTO> nonExpiredActivities = removeExpiredActivities(upcomingFullActivities);
// Filter to only include activities where the requesting user is invited or participating
List<FullFeedActivityDTO> filteredActivities = allFullActivities.stream()
.filter(activity -> isUserInvitedOrParticipating(activity, requestingUserId))
.collect(Collectors.toList());

// Convert to ProfileActivityDTO
// Convert to ProfileActivityDTO with proper past/upcoming flag
List<ProfileActivityDTO> result = new ArrayList<>();
List<ProfileActivityDTO> upcomingActivities = new ArrayList<>();
List<ProfileActivityDTO> pastActivities = new ArrayList<>();

// If there are upcoming Activities, return them as ProfileActivityDTOs
if (!nonExpiredActivities.isEmpty()) {
sortActivitiesByStartTime(nonExpiredActivities);
for (FullFeedActivityDTO Activity : nonExpiredActivities) {
result.add(ProfileActivityDTO.fromFullFeedActivityDTO(Activity, false)); // false = not past activity
for (FullFeedActivityDTO activity : filteredActivities) {
boolean isExpired = expirationService.isActivityExpired(
activity.getStartTime(),
activity.getEndTime(),
activity.getCreatedAt(),
activity.getClientTimezone()
);

ProfileActivityDTO profileActivity = ProfileActivityDTO.fromFullFeedActivityDTO(activity, isExpired);

if (isExpired) {
pastActivities.add(profileActivity);
} else {
upcomingActivities.add(profileActivity);
}
return result;
}

// If no upcoming Activities, get past Activities where the profile user invited the requesting user
return getPastActivitiesWhereUserInvited(profileUserId, requestingUserId);
// Sort upcoming activities by start time (soonest first)
upcomingActivities.sort(Comparator.comparing(
ProfileActivityDTO::getStartTime,
Comparator.nullsLast(Comparator.naturalOrder())
));

// Sort past activities by start time (most recent first)
pastActivities.sort(Comparator.comparing(
ProfileActivityDTO::getStartTime,
Comparator.nullsLast(Comparator.reverseOrder())
));

// Combine: upcoming first, then past
result.addAll(upcomingActivities);
result.addAll(pastActivities);

return result;
} catch (Exception e) {
logger.error("Error fetching profile Activities for user " + profileUserId +
" requested by " + requestingUserId + ": " + e.getMessage());
throw e;
}
}

/**
* Checks if the requesting user is invited to or participating in the activity.
*
* @param activity The activity to check
* @param requestingUserId The user ID to check for
* @return true if the user is in invitedUsers or participantUsers
*/
private boolean isUserInvitedOrParticipating(FullFeedActivityDTO activity, UUID requestingUserId) {
// Check if user is in invited users
if (activity.getInvitedUsers() != null) {
for (BaseUserDTO user : activity.getInvitedUsers()) {
if (user.getId().equals(requestingUserId)) {
return true;
}
}
}

// Check if user is in participant users
if (activity.getParticipantUsers() != null) {
for (BaseUserDTO user : activity.getParticipantUsers()) {
if (user.getId().equals(requestingUserId)) {
return true;
}
}
}

return false;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
import java.util.stream.Collectors;
import com.danielagapov.spawn.user.api.dto.BaseUserDTO;
import com.danielagapov.spawn.user.api.dto.AbstractUserDTO;
import com.danielagapov.spawn.user.api.dto.FriendUser.MinimalFriendDTO;

@Service
@AllArgsConstructor
Expand Down Expand Up @@ -425,7 +426,7 @@ private ActivityType convertDTOToEntityWithFriendLookup(ActivityTypeDTO dto, Use
// Get associated friends from database instead of creating detached entities
List<User> associatedFriends = new ArrayList<>();
if (dto.getAssociatedFriends() != null && !dto.getAssociatedFriends().isEmpty()) {
for (BaseUserDTO friendDTO : dto.getAssociatedFriends()) {
for (MinimalFriendDTO friendDTO : dto.getAssociatedFriends()) {
try {
User friend = userService.getUserEntityById(friendDTO.getId());
associatedFriends.add(friend);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -172,7 +172,23 @@ public boolean changePassword(String username, String currentPassword, String ne
public AuthResponseDTO getUserByToken(String token) {
final String username = jwtService.extractUsername(token);
User user = userService.getUserEntityByUsername(username);
return UserMapper.toAuthResponseDTO(user, oauthService.isOAuthUser(user.getId()));

// Determine the auth provider for this user
boolean isOAuthUser = oauthService.isOAuthUser(user.getId());
String provider;
if (isOAuthUser) {
try {
OAuthProvider oauthProvider = oauthService.getOAuthProvider(user.getId());
provider = oauthProvider.name(); // "google" or "apple"
} catch (Exception e) {
logger.warn("Could not determine OAuth provider for user: " + user.getId() + ". " + e.getMessage());
provider = null;
}
} else {
provider = "email";
}

return UserMapper.toAuthResponseDTO(user, isOAuthUser, provider);
}

@Override
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package com.danielagapov.spawn.shared.events;

import com.danielagapov.spawn.user.api.dto.BaseUserDTO;
import com.danielagapov.spawn.user.api.dto.RecommendedFriendUserDTO;
import com.danielagapov.spawn.user.api.dto.FriendUser.RecommendedFriendUserDTO;
import com.danielagapov.spawn.shared.util.SearchedUserResult;

import java.util.List;
Expand Down
Loading