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
12 changes: 9 additions & 3 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -90,11 +90,17 @@
</dependency>
<!-- Stripe Java SDK -->
<dependency>
<groupId>com.stripe</groupId>
<artifactId>stripe-java</artifactId>
<version>25.0.0</version>
<groupId>com.stripe</groupId>
<artifactId>stripe-java</artifactId>
<version>25.0.0</version>
</dependency>

<!-- AWS SDK for S3 -->
<dependency>
<groupId>software.amazon.awssdk</groupId>
<artifactId>s3</artifactId>
<version>2.20.26</version>
</dependency>
</dependencies>

<build>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,17 +1,16 @@
package fr.codecake.nbat.booking.application.dto;

import fr.codecake.nbat.listing.application.dto.sub.PictureDTO;
import fr.codecake.nbat.listing.application.dto.vo.PriceVO;
import jakarta.validation.Valid;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.NotNull;

import java.util.UUID;

public record BookedListingDTO(@Valid PictureDTO cover,
@NotEmpty String location,
@Valid BookedDateDTO dates,
@Valid PriceVO totalPrice,
@NotNull UUID bookingPublicId,
@NotNull UUID listingPublicId) {
public record BookedListingDTO(@Valid String cover,
@NotEmpty String location,
@Valid BookedDateDTO dates,
@Valid PriceVO totalPrice,
@NotNull UUID bookingPublicId,
@NotNull UUID listingPublicId) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package fr.codecake.nbat.infrastructure.config;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import software.amazon.awssdk.auth.credentials.AwsBasicCredentials;
import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider;
import software.amazon.awssdk.regions.Region;
import software.amazon.awssdk.services.s3.S3Client;

@Configuration
public class S3Configuration {

@Value("${aws.credentials.access-key}")
private String accessKey;

@Value("${aws.credentials.secret-key}")
private String secretKey;

@Value("${aws.s3.region}")
private String region;

@Bean
public S3Client S3Client() {
AwsBasicCredentials awsBasicCredentials = AwsBasicCredentials.create(accessKey, secretKey);

return S3Client.builder()
.region(Region.of(region))
.credentialsProvider(StaticCredentialsProvider.create(awsBasicCredentials))
.build();
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ public CreatedListingDTO create(SaveListingDTO saveListingDTO) {

@Transactional(readOnly = true)
public List<DisplayCardListingDTO> getAllProperties(ReadUserDTO landlord) {
List<Listing> properties = listingRepository.findAllByLandlordPublicIdFetchCoverPicture(landlord.publicId());
List<Listing> properties = listingRepository.findAllByLandlordPublicId(landlord.publicId());
return listingMapper.listingToDisplayCardListingDTOs(properties);
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,42 +1,74 @@
package fr.codecake.nbat.listing.application;

import fr.codecake.nbat.listing.application.dto.sub.PictureDTO;
import fr.codecake.nbat.listing.application.dto.sub.PictureUploadDTO;
import fr.codecake.nbat.listing.domain.Listing;
import fr.codecake.nbat.listing.domain.ListingPicture;
import fr.codecake.nbat.listing.mapper.ListingPictureMapper;
import fr.codecake.nbat.listing.repository.ListingPictureRepository;
import org.springframework.stereotype.Service;

import java.util.ArrayList;
import java.util.List;
import java.util.Set;

@Service
public class PictureService {

private final ListingPictureRepository listingPictureRepository;

private final ListingPictureMapper listingPictureMapper;
// NEW: Inject the S3Service so we can upload files
private final S3Service s3Service;

public PictureService(ListingPictureRepository listingPictureRepository, ListingPictureMapper listingPictureMapper) {
// CHANGED: Constructor now includes S3Service parameter
// Spring will automatically inject the S3Service bean
public PictureService(ListingPictureRepository listingPictureRepository,
ListingPictureMapper listingPictureMapper,
S3Service s3Service) {
this.listingPictureRepository = listingPictureRepository;
this.listingPictureMapper = listingPictureMapper;
this.s3Service = s3Service;
}

public List<PictureDTO> saveAll(List<PictureDTO> pictures, Listing listing) {
// CHANGE: accept PictureUploadDTO (has bytes) instead of PictureDTO
public List<PictureDTO> saveAll(List<PictureUploadDTO> pictures, Listing listing) {
if (pictures == null || pictures.isEmpty()) {
return List.of();
}
Set<ListingPicture> listingPictures = listingPictureMapper.pictureDTOsToListingPictures(pictures);

List<ListingPicture> listingPictures = new ArrayList<>();
String firstCoverUrl = null; // we'll use this for Change 4
boolean isFirst = true;

for (ListingPicture listingPicture : listingPictures) {
for (PictureUploadDTO pictureDTO : pictures) {
ListingPicture listingPicture = new ListingPicture();
listingPicture.setFileContentType(pictureDTO.fileContentType());
listingPicture.setCover(isFirst);
listingPicture.setListing(listing);

if (pictureDTO.file() != null) {
// NOW this compiles correctly because PictureUploadDTO has .file()
String s3Url = s3Service.uploadFile(
pictureDTO.file(),
pictureDTO.fileContentType(),
"listing-image");
listingPicture.setFileUrl(s3Url);

// Capture the cover URL to store directly on Listing (see Change 4)
if (isFirst) {
firstCoverUrl = s3Url;
}
}

listingPictures.add(listingPicture);
isFirst = false;
}

// Store the cover URL directly on the Listing entity (Change 4)
if (firstCoverUrl != null) {
listing.setCoverUrl(firstCoverUrl);
}

listingPictureRepository.saveAll(listingPictures);
return listingPictureMapper.listingPictureToPictureDTO(listingPictures.stream().toList());
return listingPictureMapper.listingPictureToPictureDTO(listingPictures);
}
}
}
69 changes: 69 additions & 0 deletions src/main/java/fr/codecake/nbat/listing/application/S3Service.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
package fr.codecake.nbat.listing.application;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import software.amazon.awssdk.core.sync.RequestBody;
import software.amazon.awssdk.services.s3.S3Client;
import software.amazon.awssdk.services.s3.model.PutObjectRequest;
import software.amazon.awssdk.services.s3.model.ObjectCannedACL;

import java.util.UUID;

@Service
public class S3Service {

private final S3Client s3Client;

@Value("${aws.s3.bucket-name}")
private String bucketName;

@Value("${aws.s3.region}")
private String region;

public S3Service(S3Client s3Client) {
this.s3Client = s3Client;
}

/**
* Uploads a file to S3 and returns the public URL where the file can be
* accessed.
*
* @param fileBytes The actual file data as a byte array
* @param contentType The MIME type of the file (e.g., "image/jpeg",
* "image/png")
* @param originalFileName The original name of the file (used to determine file
* extension)
* @return The public URL where the uploaded file can be accessed
*/
public String uploadFile(byte[] fileBytes, String contentType, String originalFileName) {
// Generate a unique filename to avoid collisions
// We use UUID to ensure each file has a unique name
String fileExtension = getFileExtension(originalFileName);
String uniqueFileName = "listings/" + UUID.randomUUID().toString() + fileExtension;

// Build the S3 upload request
PutObjectRequest putObjectRequest = PutObjectRequest.builder()
.bucket(bucketName)
.key(uniqueFileName)
.contentType(contentType)
.build();

// Actually upload the file to S3
s3Client.putObject(putObjectRequest, RequestBody.fromBytes(fileBytes));

// Construct and return the public URL
return String.format("https://%s.s3.%s.amazonaws.com/%s",
bucketName, region, uniqueFileName);
}

/**
* Extracts the file extension from a filename.
* For example: "house.jpg" returns ".jpg"
*/
private String getFileExtension(String fileName) {
if (fileName == null || !fileName.contains(".")) {
return "";
}
return fileName.substring(fileName.lastIndexOf("."));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,8 @@ public class TenantService {
private final UserService userService;
private final BookingService bookingService;


public TenantService(ListingRepository listingRepository, ListingMapper listingMapper, UserService userService, BookingService bookingService) {
public TenantService(ListingRepository listingRepository, ListingMapper listingMapper, UserService userService,
BookingService bookingService) {
this.listingRepository = listingRepository;
this.listingMapper = listingMapper;
this.userService = userService;
Expand All @@ -43,9 +43,9 @@ public TenantService(ListingRepository listingRepository, ListingMapper listingM
public Page<DisplayCardListingDTO> getAllByCategory(Pageable pageable, BookingCategory category) {
Page<Listing> allOrBookingCategory;
if (category == BookingCategory.ALL) {
allOrBookingCategory = listingRepository.findAllWithCoverOnly(pageable);
allOrBookingCategory = listingRepository.findAll(pageable);
} else {
allOrBookingCategory = listingRepository.findAllByBookingCategoryWithCoverOnly(pageable, category);
allOrBookingCategory = listingRepository.findAllByBookingCategory(pageable, category);
}

return allOrBookingCategory.map(listingMapper::listingToDisplayCardListingDTO);
Expand All @@ -62,29 +62,32 @@ public State<DisplayListingDTO, String> getOne(UUID publicId) {

DisplayListingDTO displayListingDTO = listingMapper.listingToDisplayListingDTO(listingByPublicIdOpt.get());

ReadUserDTO readUserDTO = userService.getByPublicId(listingByPublicIdOpt.get().getLandlordPublicId()).orElseThrow();
ReadUserDTO readUserDTO = userService.getByPublicId(listingByPublicIdOpt.get().getLandlordPublicId())
.orElseThrow();
LandlordListingDTO landlordListingDTO = new LandlordListingDTO(readUserDTO.firstName(), readUserDTO.imageUrl());
displayListingDTO.setLandlord(landlordListingDTO);

return State.<DisplayListingDTO, String>builder().forSuccess(displayListingDTO);
}


@SuppressWarnings("null")
@Transactional(readOnly = true)
public Page<DisplayCardListingDTO> search(Pageable pageable, SearchDTO newSearch) {

Page<Listing> allMatchedListings = listingRepository.findAllByLocationAndBathroomsAndBedroomsAndGuestsAndBeds(pageable, newSearch.location(),
Page<Listing> allMatchedListings = listingRepository.findAllByLocationAndBathroomsAndBedroomsAndGuestsAndBeds(
pageable, newSearch.location(),
newSearch.infos().baths().value(),
newSearch.infos().bedrooms().value(),
newSearch.infos().guests().value(),
newSearch.infos().beds().value());

List<UUID> listingUUIDs = allMatchedListings.stream().map(Listing::getPublicId).toList();

List<UUID> bookingUUIDs = bookingService.getBookingMatchByListingIdsAndBookedDate(listingUUIDs, newSearch.dates());
List<UUID> bookingUUIDs = bookingService.getBookingMatchByListingIdsAndBookedDate(listingUUIDs,
newSearch.dates());

List<DisplayCardListingDTO> listingsNotBooked = allMatchedListings.stream().filter(listing -> !bookingUUIDs.contains(listing.getPublicId()))
List<DisplayCardListingDTO> listingsNotBooked = allMatchedListings.stream()
.filter(listing -> !bookingUUIDs.contains(listing.getPublicId()))
.map(listingMapper::listingToDisplayCardListingDTO)
.toList();

Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,13 @@
package fr.codecake.nbat.listing.application.dto;

import fr.codecake.nbat.listing.application.dto.sub.PictureDTO;
import fr.codecake.nbat.listing.application.dto.vo.PriceVO;
import fr.codecake.nbat.listing.domain.BookingCategory;

import java.util.UUID;

public record DisplayCardListingDTO(PriceVO price,
String location,
PictureDTO cover,
BookingCategory bookingCategory,
UUID publicId) {
String location,
String cover,
BookingCategory bookingCategory,
UUID publicId) {
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import fr.codecake.nbat.listing.application.dto.sub.DescriptionDTO;
import fr.codecake.nbat.listing.application.dto.sub.ListingInfoDTO;
import fr.codecake.nbat.listing.application.dto.sub.PictureDTO;
import fr.codecake.nbat.listing.application.dto.sub.PictureUploadDTO;
import fr.codecake.nbat.listing.application.dto.vo.PriceVO;
import fr.codecake.nbat.listing.domain.BookingCategory;
import jakarta.validation.Valid;
Expand All @@ -15,19 +15,23 @@ public class SaveListingDTO {
@NotNull
BookingCategory category;

@NotNull String location;
@NotNull
String location;

@NotNull @Valid
@NotNull
@Valid
ListingInfoDTO infos;

@NotNull @Valid
@NotNull
@Valid
DescriptionDTO description;

@NotNull @Valid
@NotNull
@Valid
PriceVO price;

@NotNull
List<PictureDTO> pictures;
List<PictureUploadDTO> pictures;

public BookingCategory getCategory() {
return category;
Expand Down Expand Up @@ -69,11 +73,11 @@ public void setPrice(PriceVO price) {
this.price = price;
}

public List<PictureDTO> getPictures() {
public List<PictureUploadDTO> getPictures() {
return pictures;
}

public void setPictures(List<PictureDTO> pictures) {
public void setPictures(List<PictureUploadDTO> pictures) {
this.pictures = pictures;
}
}
Loading