= Advanced Application Development == Transactional === Adding a listing as a favorite {{{ @Transactional public void addFavorite(Long userId, Long listingId) { Client client = clientRepository.findByUserId(userId) .orElseThrow(() -> new RuntimeException("Client not found")); Listing listing = listingRepository.findById(listingId) .orElseThrow(() -> new RuntimeException("Listing not found")); FavoriteListing favorite = new FavoriteListing(client, listing); favoriteRepository.save(favorite); logger.info("Added favorite - User: {}, Listing: {}", userId, listingId); } }}} === Removing a listing from favorites {{{ @Transactional public void removeFavorite(Long userId, Long listingId) { Client client = clientRepository.findByUserId(userId) .orElseThrow(() -> new RuntimeException("Client not found")); Listing listing = listingRepository.findById(listingId) .orElseThrow(() -> new RuntimeException("Listing not found")); FavoriteListing favorite = favoriteRepository.findByClientAndListing(client, listing) .orElseThrow(() -> new RuntimeException("Favorite not found")); favoriteRepository.delete(favorite); logger.info("Removed favorite - User: {}, Listing: {}", userId, listingId); } }}} === Getting information from the favorite_listings table {{{ @Transactional(readOnly = true) public List getFavoritedListings(Long userId) { return favoriteRepository.findFavoritedListingDTOs(userId); } @Transactional(readOnly = true) public boolean isFavorited(Long userId, Long listingId) { Client client = clientRepository.findByUserId(userId) .orElse(null); if (client == null) return false; Listing listing = listingRepository.findById(listingId) .orElse(null); if (listing == null) return false; return favoriteRepository.findByClientAndListing(client, listing).isPresent(); } }}} === Creating a review {{{ @Transactional public ReviewDTO createReview(Long reviewerId, Long targetUserId, CreateReviewRequest request) { logger.info("==== START createReview ===="); // Validate rating if (request.getRating() == null || request.getRating() < 1 || request.getRating() > 5) { logger.error(" VALIDATION FAILED: Invalid rating: {}", request.getRating()); throw new RuntimeException("Rating must be between 1 and 5"); } // Check if reviewer exists User reviewer = userRepository.findById(reviewerId) .orElseThrow(() -> { logger.error(" Reviewer not found with ID: {}", reviewerId); return new RuntimeException("Reviewer not found"); }); // Check if target user exists User targetUser = userRepository.findById(targetUserId) .orElseThrow(() -> { logger.error(" Target user not found with ID: {}", targetUserId); return new RuntimeException("Target user not found"); }); // Check if review already exists and is not deleted logger.info("Checking if reviewer {} has already reviewed user {}", reviewerId, targetUserId); var existingReview = userReviewRepository.findTopByReviewReviewerUserIdAndTargetUserIdAndReviewIsDeletedFalseOrderByReviewCreatedAtDesc(reviewerId, targetUserId); if (existingReview.isPresent()) { Review existingReviewEntity = existingReview.get().getReview(); logger.info("Found existing review with ID: {}", existingReviewEntity.getReviewId()); logger.info("Existing review isDeleted status: {}", existingReviewEntity.getIsDeleted()); if (!existingReviewEntity.getIsDeleted()) { logger.error(" User {} has already reviewed user {} and review is NOT deleted", reviewerId, targetUserId); throw new RuntimeException("You have already reviewed this user"); } else { logger.info(" User {} has a deleted review for user {} - can create a new one", reviewerId, targetUserId); } } else { logger.info(" No existing review found - safe to create new review"); } // Create Review entity Review review = new Review(reviewer, request.getRating(), request.getComment()); // Save Review to database with flush review = reviewRepository.saveAndFlush(review); logger.info("Review ID after save: {}", review.getReviewId()); if (review.getReviewId() == null) { logger.error(" CRITICAL: Review ID is NULL after save!"); throw new RuntimeException("Failed to save review - ID is null"); } // Create UserReview entry UserReview userReview = new UserReview(); logger.info("Setting Review on UserReview (will copy ID via @MapsId)..."); userReview.setReview(review); logger.info("UserReview reviewId after setReview: {}", userReview.getReviewId()); userReview.setTargetUserId(targetUserId); // Save UserReview to database with flush userReview = userReviewRepository.saveAndFlush(userReview); logger.info(" UserReview saved successfully"); // Create and return DTO ReviewDTO reviewDTO = new ReviewDTO(review); logger.info(" ReviewDTO created successfully"); return reviewDTO; } }}} === Deleting a review {{{ @Transactional public void deleteReview(Long reviewId, Long userId) { logger.info("=== START deleteReview (SOFT DELETE) ==="); // Fetch review logger.info("Fetching review with ID: {}", reviewId); Review review = reviewRepository.findById(reviewId) .orElseThrow(() -> { logger.error(" Review not found with ID: {}", reviewId); return new RuntimeException("Review not found"); }); logger.info(" Review found: reviewer={}, rating={}", review.getReviewer().getUsername(), review.getRating()); // Check authorization if (!review.getReviewer().getUserId().equals(userId)) { logger.error(" User {} is not authorized to delete review {}. Reviewer is {}", userId, reviewId, review.getReviewer().getUserId()); throw new RuntimeException("You can only delete your own reviews"); } // Soft delete: mark as deleted instead of physically removing review.setIsDeleted(true); review.setUpdatedAt(LocalDateTime.now()); reviewRepository.save(review); logger.info("=== END deleteReview - SUCCESS ==="); } }}} === Creating a new pet {{{ @Transactional public AnimalResponseDTO addPet(Long userId,@RequestBody CreatePetRequest request) { // Validate required fields if (request.getName() == null || request.getName().isBlank()) { throw new RuntimeException("Pet name is required"); } if (request.getSex() == null || request.getSex().isBlank()) { throw new RuntimeException("Pet sex is required"); } if (request.getType() == null || request.getType().isBlank()) { throw new RuntimeException("Pet type is required"); } if (request.getSpecies() == null || request.getSpecies().isBlank()) { throw new RuntimeException("Pet species is required"); } // Get user User user = userRepository.findById(userId) .orElseThrow(() -> { logger.error(" User not found with ID: {}", userId); return new RuntimeException("User not found"); }); logger.info("Adding pet for user ID: {}", userId); // Check if user is already an owner, if not, promote them Owner owner = ownerRepository.findByUserId(userId) .orElseGet(() -> { logger.info("⚠User {} is a CLIENT, promoting to OWNER", userId); Owner newOwner = new Owner(user); Owner savedOwner = ownerRepository.save(newOwner); logger.info(" User promoted to OWNER with ID: {}", savedOwner.getUserId()); return savedOwner; }); // Create new pet with all schema fields Pet pet = new Pet( request.getName(), request.getSex(), request.getDateOfBirth(), request.getPhotoUrl(), request.getType(), request.getSpecies(), request.getBreed(), request.getLocatedName(), owner ); Pet savedPet = petRepository.save(pet); logger.info("Pet ID: {}, Owner ID: {}, Name: {}", savedPet.getAnimalId(), userId, savedPet.getName()); AnimalResponseDTO result = new AnimalResponseDTO(savedPet); return result; } }}} === Creating a new listing {{{ @Transactional public ListingDTO createListing(Long userId, CreateListingRequest request) { // Check if user is an owner Owner owner = ownerRepository.findByUserId(userId) .orElseThrow(() -> new RuntimeException("User is not an owner. Only owners can create listings.")); logger.info("Creating listing for owner ID: {}", userId); // Create new listing with Owner object Listing listing = new Listing( owner, request.getAnimalId(), request.getPrice(), request.getDescription() ); Listing savedListing = listingRepository.save(listing); logger.info("Listing created successfully - ID: {}, Owner ID: {}, Animal ID: {}", savedListing.getListingId(), userId, request.getAnimalId()); return mapToDTO(savedListing); } }}}