| | 1 | = Advanced Application Development |
| | 2 | == Transactional |
| | 3 | === Adding a listing as a favorite |
| | 4 | {{{ |
| | 5 | @Transactional |
| | 6 | public void addFavorite(Long userId, Long listingId) { |
| | 7 | Client client = clientRepository.findByUserId(userId) |
| | 8 | .orElseThrow(() -> new RuntimeException("Client not found")); |
| | 9 | |
| | 10 | Listing listing = listingRepository.findById(listingId) |
| | 11 | .orElseThrow(() -> new RuntimeException("Listing not found")); |
| | 12 | |
| | 13 | FavoriteListing favorite = new FavoriteListing(client, listing); |
| | 14 | favoriteRepository.save(favorite); |
| | 15 | logger.info("Added favorite - User: {}, Listing: {}", userId, listingId); |
| | 16 | } |
| | 17 | }}} |
| | 18 | === Removing a listing from favorites |
| | 19 | {{{ |
| | 20 | @Transactional |
| | 21 | public void removeFavorite(Long userId, Long listingId) { |
| | 22 | Client client = clientRepository.findByUserId(userId) |
| | 23 | .orElseThrow(() -> new RuntimeException("Client not found")); |
| | 24 | |
| | 25 | Listing listing = listingRepository.findById(listingId) |
| | 26 | .orElseThrow(() -> new RuntimeException("Listing not found")); |
| | 27 | |
| | 28 | FavoriteListing favorite = favoriteRepository.findByClientAndListing(client, listing) |
| | 29 | .orElseThrow(() -> new RuntimeException("Favorite not found")); |
| | 30 | |
| | 31 | favoriteRepository.delete(favorite); |
| | 32 | logger.info("Removed favorite - User: {}, Listing: {}", userId, listingId); |
| | 33 | } |
| | 34 | }}} |
| | 35 | === Getting information from the favorite_listings table |
| | 36 | {{{ |
| | 37 | @Transactional(readOnly = true) |
| | 38 | public List<ListingDTO> getFavoritedListings(Long userId) { |
| | 39 | return favoriteRepository.findFavoritedListingDTOs(userId); |
| | 40 | } |
| | 41 | |
| | 42 | @Transactional(readOnly = true) |
| | 43 | public boolean isFavorited(Long userId, Long listingId) { |
| | 44 | Client client = clientRepository.findByUserId(userId) |
| | 45 | .orElse(null); |
| | 46 | |
| | 47 | if (client == null) return false; |
| | 48 | |
| | 49 | Listing listing = listingRepository.findById(listingId) |
| | 50 | .orElse(null); |
| | 51 | |
| | 52 | if (listing == null) return false; |
| | 53 | |
| | 54 | return favoriteRepository.findByClientAndListing(client, listing).isPresent(); |
| | 55 | } |
| | 56 | }}} |
| | 57 | === Creating a review |
| | 58 | {{{ |
| | 59 | @Transactional |
| | 60 | public ReviewDTO createReview(Long reviewerId, Long targetUserId, CreateReviewRequest request) { |
| | 61 | logger.info("==== START createReview ===="); |
| | 62 | // Validate rating |
| | 63 | if (request.getRating() == null || request.getRating() < 1 || request.getRating() > 5) { |
| | 64 | logger.error(" VALIDATION FAILED: Invalid rating: {}", request.getRating()); |
| | 65 | throw new RuntimeException("Rating must be between 1 and 5"); |
| | 66 | } |
| | 67 | |
| | 68 | |
| | 69 | // Check if reviewer exists |
| | 70 | User reviewer = userRepository.findById(reviewerId) |
| | 71 | .orElseThrow(() -> { |
| | 72 | logger.error(" Reviewer not found with ID: {}", reviewerId); |
| | 73 | return new RuntimeException("Reviewer not found"); |
| | 74 | }); |
| | 75 | |
| | 76 | // Check if target user exists |
| | 77 | User targetUser = userRepository.findById(targetUserId) |
| | 78 | .orElseThrow(() -> { |
| | 79 | logger.error(" Target user not found with ID: {}", targetUserId); |
| | 80 | return new RuntimeException("Target user not found"); |
| | 81 | }); |
| | 82 | |
| | 83 | // Check if review already exists and is not deleted |
| | 84 | logger.info("Checking if reviewer {} has already reviewed user {}", reviewerId, targetUserId); |
| | 85 | var existingReview = userReviewRepository.findTopByReviewReviewerUserIdAndTargetUserIdAndReviewIsDeletedFalseOrderByReviewCreatedAtDesc(reviewerId, targetUserId); |
| | 86 | |
| | 87 | if (existingReview.isPresent()) { |
| | 88 | Review existingReviewEntity = existingReview.get().getReview(); |
| | 89 | logger.info("Found existing review with ID: {}", existingReviewEntity.getReviewId()); |
| | 90 | logger.info("Existing review isDeleted status: {}", existingReviewEntity.getIsDeleted()); |
| | 91 | |
| | 92 | if (!existingReviewEntity.getIsDeleted()) { |
| | 93 | logger.error(" User {} has already reviewed user {} and review is NOT deleted", reviewerId, targetUserId); |
| | 94 | throw new RuntimeException("You have already reviewed this user"); |
| | 95 | } else { |
| | 96 | logger.info(" User {} has a deleted review for user {} - can create a new one", reviewerId, targetUserId); |
| | 97 | } |
| | 98 | } else { |
| | 99 | logger.info(" No existing review found - safe to create new review"); |
| | 100 | } |
| | 101 | |
| | 102 | // Create Review entity |
| | 103 | Review review = new Review(reviewer, request.getRating(), request.getComment()); |
| | 104 | |
| | 105 | |
| | 106 | // Save Review to database with flush |
| | 107 | review = reviewRepository.saveAndFlush(review); |
| | 108 | logger.info("Review ID after save: {}", review.getReviewId()); |
| | 109 | |
| | 110 | if (review.getReviewId() == null) { |
| | 111 | logger.error(" CRITICAL: Review ID is NULL after save!"); |
| | 112 | throw new RuntimeException("Failed to save review - ID is null"); |
| | 113 | } |
| | 114 | |
| | 115 | // Create UserReview entry |
| | 116 | UserReview userReview = new UserReview(); |
| | 117 | logger.info("Setting Review on UserReview (will copy ID via @MapsId)..."); |
| | 118 | userReview.setReview(review); |
| | 119 | logger.info("UserReview reviewId after setReview: {}", userReview.getReviewId()); |
| | 120 | |
| | 121 | userReview.setTargetUserId(targetUserId); |
| | 122 | |
| | 123 | // Save UserReview to database with flush |
| | 124 | userReview = userReviewRepository.saveAndFlush(userReview); |
| | 125 | logger.info(" UserReview saved successfully"); |
| | 126 | |
| | 127 | // Create and return DTO |
| | 128 | ReviewDTO reviewDTO = new ReviewDTO(review); |
| | 129 | logger.info(" ReviewDTO created successfully"); |
| | 130 | |
| | 131 | return reviewDTO; |
| | 132 | } |
| | 133 | }}} |
| | 134 | === Deleting a review |
| | 135 | {{{ |
| | 136 | @Transactional |
| | 137 | public void deleteReview(Long reviewId, Long userId) { |
| | 138 | logger.info("=== START deleteReview (SOFT DELETE) ==="); |
| | 139 | |
| | 140 | |
| | 141 | // Fetch review |
| | 142 | logger.info("Fetching review with ID: {}", reviewId); |
| | 143 | Review review = reviewRepository.findById(reviewId) |
| | 144 | .orElseThrow(() -> { |
| | 145 | logger.error(" Review not found with ID: {}", reviewId); |
| | 146 | return new RuntimeException("Review not found"); |
| | 147 | }); |
| | 148 | logger.info(" Review found: reviewer={}, rating={}", review.getReviewer().getUsername(), review.getRating()); |
| | 149 | |
| | 150 | // Check authorization |
| | 151 | if (!review.getReviewer().getUserId().equals(userId)) { |
| | 152 | logger.error(" User {} is not authorized to delete review {}. Reviewer is {}", userId, reviewId, review.getReviewer().getUserId()); |
| | 153 | throw new RuntimeException("You can only delete your own reviews"); |
| | 154 | } |
| | 155 | |
| | 156 | // Soft delete: mark as deleted instead of physically removing |
| | 157 | review.setIsDeleted(true); |
| | 158 | review.setUpdatedAt(LocalDateTime.now()); |
| | 159 | reviewRepository.save(review); |
| | 160 | |
| | 161 | logger.info("=== END deleteReview - SUCCESS ==="); |
| | 162 | } |
| | 163 | }}} |
| | 164 | === Creating a new pet |
| | 165 | {{{ |
| | 166 | @Transactional |
| | 167 | public AnimalResponseDTO addPet(Long userId,@RequestBody CreatePetRequest request) { |
| | 168 | |
| | 169 | // Validate required fields |
| | 170 | if (request.getName() == null || request.getName().isBlank()) { |
| | 171 | throw new RuntimeException("Pet name is required"); |
| | 172 | } |
| | 173 | |
| | 174 | if (request.getSex() == null || request.getSex().isBlank()) { |
| | 175 | throw new RuntimeException("Pet sex is required"); |
| | 176 | } |
| | 177 | |
| | 178 | if (request.getType() == null || request.getType().isBlank()) { |
| | 179 | throw new RuntimeException("Pet type is required"); |
| | 180 | } |
| | 181 | |
| | 182 | |
| | 183 | if (request.getSpecies() == null || request.getSpecies().isBlank()) { |
| | 184 | throw new RuntimeException("Pet species is required"); |
| | 185 | } |
| | 186 | |
| | 187 | // Get user |
| | 188 | User user = userRepository.findById(userId) |
| | 189 | .orElseThrow(() -> { |
| | 190 | logger.error(" User not found with ID: {}", userId); |
| | 191 | return new RuntimeException("User not found"); |
| | 192 | }); |
| | 193 | |
| | 194 | logger.info("Adding pet for user ID: {}", userId); |
| | 195 | |
| | 196 | // Check if user is already an owner, if not, promote them |
| | 197 | Owner owner = ownerRepository.findByUserId(userId) |
| | 198 | .orElseGet(() -> { |
| | 199 | logger.info("⚠User {} is a CLIENT, promoting to OWNER", userId); |
| | 200 | Owner newOwner = new Owner(user); |
| | 201 | Owner savedOwner = ownerRepository.save(newOwner); |
| | 202 | logger.info(" User promoted to OWNER with ID: {}", savedOwner.getUserId()); |
| | 203 | return savedOwner; |
| | 204 | }); |
| | 205 | |
| | 206 | // Create new pet with all schema fields |
| | 207 | |
| | 208 | Pet pet = new Pet( |
| | 209 | request.getName(), |
| | 210 | request.getSex(), |
| | 211 | request.getDateOfBirth(), |
| | 212 | request.getPhotoUrl(), |
| | 213 | request.getType(), |
| | 214 | request.getSpecies(), |
| | 215 | request.getBreed(), |
| | 216 | request.getLocatedName(), |
| | 217 | owner |
| | 218 | ); |
| | 219 | |
| | 220 | Pet savedPet = petRepository.save(pet); |
| | 221 | logger.info("Pet ID: {}, Owner ID: {}, Name: {}", |
| | 222 | savedPet.getAnimalId(), userId, savedPet.getName()); |
| | 223 | |
| | 224 | AnimalResponseDTO result = new AnimalResponseDTO(savedPet); |
| | 225 | |
| | 226 | |
| | 227 | return result; |
| | 228 | } |
| | 229 | }}} |
| | 230 | === Creating a new listing |
| | 231 | {{{ |
| | 232 | @Transactional |
| | 233 | public ListingDTO createListing(Long userId, CreateListingRequest request) { |
| | 234 | // Check if user is an owner |
| | 235 | Owner owner = ownerRepository.findByUserId(userId) |
| | 236 | .orElseThrow(() -> new RuntimeException("User is not an owner. Only owners can create listings.")); |
| | 237 | |
| | 238 | logger.info("Creating listing for owner ID: {}", userId); |
| | 239 | |
| | 240 | // Create new listing with Owner object |
| | 241 | Listing listing = new Listing( |
| | 242 | owner, |
| | 243 | request.getAnimalId(), |
| | 244 | request.getPrice(), |
| | 245 | request.getDescription() |
| | 246 | ); |
| | 247 | |
| | 248 | Listing savedListing = listingRepository.save(listing); |
| | 249 | |
| | 250 | logger.info("Listing created successfully - ID: {}, Owner ID: {}, Animal ID: {}", |
| | 251 | savedListing.getListingId(), userId, request.getAnimalId()); |
| | 252 | |
| | 253 | return mapToDTO(savedListing); |
| | 254 | } |
| | 255 | }}} |