Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

상위 입찰자 발생 시 기존 마지막 입찰자에게 알림을 전송하는 기능 추가 #438

Merged
merged 7 commits into from
Sep 22, 2023
Original file line number Diff line number Diff line change
Expand Up @@ -169,4 +169,12 @@ public Optional<User> findWinner(final LocalDateTime targetTime) {
private boolean isWinnerExist(final LocalDateTime targetTime) {
return auctioneerCount != 0 && isClosed(targetTime);
}

public Optional<User> findLastBidder() {
if (lastBid == null) {
return Optional.empty();
}

return Optional.of(lastBid.getBidder());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,5 @@
import com.ddang.ddang.auction.domain.Auction;
import org.springframework.data.jpa.repository.JpaRepository;

public interface JpaAuctionRepository extends JpaRepository<Auction, Long>, QuerydslAuctionRepository {
public interface JpaAuctionRepository extends JpaRepository<Auction, Long>, QuerydslAuctionRepository, QuerydslAuctionAndImageRepository {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.ddang.ddang.auction.infrastructure.persistence;

import com.ddang.ddang.auction.infrastructure.persistence.dto.AuctionAndImageDto;

import java.util.Optional;

public interface QuerydslAuctionAndImageRepository {

Optional<AuctionAndImageDto> findDtoByAuctionId(final Long auctionId);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package com.ddang.ddang.auction.infrastructure.persistence;

import com.ddang.ddang.auction.infrastructure.persistence.dto.AuctionAndImageDto;
import com.ddang.ddang.auction.infrastructure.persistence.dto.AuctionAndImageQueryProjectionDto;
import com.ddang.ddang.auction.infrastructure.persistence.dto.QAuctionAndImageQueryProjectionDto;
import com.querydsl.jpa.JPAExpressions;
import com.querydsl.jpa.impl.JPAQueryFactory;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Repository;

import java.util.Optional;

import static com.ddang.ddang.auction.domain.QAuction.auction;
import static com.ddang.ddang.bid.domain.QBid.bid;
import static com.ddang.ddang.image.domain.QAuctionImage.auctionImage;

@Repository
@RequiredArgsConstructor
public class QuerydslAuctionAndImageRepositoryImpl implements QuerydslAuctionAndImageRepository {

private final JPAQueryFactory queryFactory;

@Override
public Optional<AuctionAndImageDto> findDtoByAuctionId(final Long auctionId) {
final AuctionAndImageQueryProjectionDto auctionAndImageQueryProjectionDto =
queryFactory.select(new QAuctionAndImageQueryProjectionDto(auction, auctionImage))
.from(auction)
.leftJoin(auction.lastBid, bid).fetchJoin()
.leftJoin(bid.bidder).fetchJoin()
.leftJoin(auctionImage).on(auctionImage.id.eq(
JPAExpressions
.select(auctionImage.id.min())
.from(auctionImage)
.where(auctionImage.auction.id.eq(auction.id))
.groupBy(auctionImage.auction.id)
)).fetchJoin()
.where(auction.id.eq(auctionId))
.fetchOne();

if (auctionAndImageQueryProjectionDto == null) {
return Optional.empty();
}

return Optional.of(auctionAndImageQueryProjectionDto.toDto());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.ddang.ddang.auction.infrastructure.persistence.dto;

import com.ddang.ddang.auction.domain.Auction;
import com.ddang.ddang.image.domain.AuctionImage;

public record AuctionAndImageDto(Auction auction, AuctionImage auctionImage) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package com.ddang.ddang.auction.infrastructure.persistence.dto;

import com.ddang.ddang.auction.domain.Auction;
import com.ddang.ddang.image.domain.AuctionImage;
import com.querydsl.core.annotations.QueryProjection;

public record AuctionAndImageQueryProjectionDto(Auction auction, AuctionImage auctionImage) {

@QueryProjection
public AuctionAndImageQueryProjectionDto {
}

public AuctionAndImageDto toDto() {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

다른 dto로 변경하는 코드니까 toAuctionAndImageDto로 변경해야 할 것 같아요!

return new AuctionAndImageDto(this.auction, this.auctionImage);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import com.ddang.ddang.auction.application.exception.AuctionNotFoundException;
import com.ddang.ddang.auction.domain.Auction;
import com.ddang.ddang.auction.infrastructure.persistence.JpaAuctionRepository;
import com.ddang.ddang.auction.infrastructure.persistence.dto.AuctionAndImageDto;
import com.ddang.ddang.bid.application.dto.CreateBidDto;
import com.ddang.ddang.bid.application.dto.ReadBidDto;
import com.ddang.ddang.bid.application.exception.InvalidAuctionToBidException;
Expand All @@ -11,35 +12,60 @@
import com.ddang.ddang.bid.domain.Bid;
import com.ddang.ddang.bid.domain.BidPrice;
import com.ddang.ddang.bid.infrastructure.persistence.JpaBidRepository;
import com.ddang.ddang.image.domain.AuctionImage;
import com.ddang.ddang.notification.application.NotificationService;
import com.ddang.ddang.notification.application.dto.CreateNotificationDto;
import com.ddang.ddang.notification.domain.NotificationType;
import com.ddang.ddang.user.application.exception.UserNotFoundException;
import com.ddang.ddang.user.domain.User;
import com.ddang.ddang.user.infrastructure.persistence.JpaUserRepository;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.time.LocalDateTime;
import java.util.List;
import java.util.Optional;

@Service
@Transactional(readOnly = true)
@RequiredArgsConstructor
@Slf4j
public class BidService {

private final NotificationService notificationService;
private final JpaAuctionRepository auctionRepository;
private final JpaUserRepository userRepository;
private final JpaBidRepository bidRepository;

@Transactional
public Long create(final CreateBidDto bidDto) {
public Long create(final CreateBidDto bidDto, final String baseUrl) {
final User bidder = userRepository.findById(bidDto.userId())
.orElseThrow(() -> new UserNotFoundException("해당 사용자를 찾을 수 없습니다."));
final Auction auction = auctionRepository.findById(bidDto.auctionId())
.orElseThrow(() -> new AuctionNotFoundException("해당 경매를 찾을 수 없습니다."));
final AuctionAndImageDto auctionAndImageDto =
auctionRepository.findDtoByAuctionId(bidDto.auctionId())
.orElseThrow(() -> new AuctionNotFoundException("해당 경매를 찾을 수 없습니다."));

final Auction auction = auctionAndImageDto.auction();
checkInvalidAuction(auction);
checkInvalidBid(auction, bidder, bidDto);

final Optional<User> previousBidder = auction.findLastBidder();

final Bid saveBid = saveBid(bidDto, auction, bidder);

if (previousBidder.isEmpty()) {
return saveBid.getId();
}

try {
final String sendNotificationMessage = sendNotification(auctionAndImageDto, previousBidder.get(), baseUrl);
log.info(sendNotificationMessage);
} catch (Exception ex) {
log.error("exception type : {}, ", ex.getClass().getSimpleName(), ex);
}

return saveBid.getId();
}

Expand Down Expand Up @@ -109,6 +135,33 @@ private Bid saveBid(final CreateBidDto bidDto, final Auction auction, final User
return saveBid;
}

private String sendNotification(
final AuctionAndImageDto auctionAndImageDto,
final User previousBidder,
final String baseUrl
) {
final Auction auction = auctionAndImageDto.auction();
final AuctionImage auctionImage = auctionAndImageDto.auctionImage();
final CreateNotificationDto dto = new CreateNotificationDto(
NotificationType.BID,
previousBidder.getId(),
auction.getTitle(),
String.valueOf(auction.getLastBid().getPrice()),
calculateRedirectUrl(auction.getId()),
calculateAuctionImageUrl(auctionImage, baseUrl)
);

return notificationService.send(dto);
}

private String calculateRedirectUrl(final Long auctionId) {
return "/auctions/" + auctionId;
}

private String calculateAuctionImageUrl(final AuctionImage thumbnailImage, final String baseUrl) {
return baseUrl.concat(String.valueOf(thumbnailImage.getId()));
}

public List<ReadBidDto> readAllByAuctionId(final Long auctionId) {
if (auctionRepository.existsById(auctionId)) {
final List<Bid> bids = bidRepository.findByAuctionId(auctionId);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import com.ddang.ddang.bid.presentation.dto.request.CreateBidRequest;
import com.ddang.ddang.bid.presentation.dto.response.ReadBidResponse;
import com.ddang.ddang.bid.presentation.dto.response.ReadBidsResponse;
import com.ddang.ddang.image.presentation.util.ImageBaseUrl;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
Expand All @@ -33,7 +34,7 @@ public ResponseEntity<Void> create(
@AuthenticateUser AuthenticationUserInfo userInfo,
@RequestBody @Valid final CreateBidRequest bidRequest
) {
bidService.create(CreateBidDto.of(bidRequest, userInfo.userId()));
bidService.create(CreateBidDto.of(bidRequest, userInfo.userId()), ImageBaseUrl.AUCTION.getBaseUrl());

return ResponseEntity.created(URI.create("/auctions/" + bidRequest.auctionId()))
.build();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@
import com.ddang.ddang.bid.domain.BidPrice;
import com.ddang.ddang.configuration.JpaConfiguration;
import com.ddang.ddang.configuration.QuerydslConfiguration;
import com.ddang.ddang.image.domain.ProfileImage;
import com.ddang.ddang.image.domain.AuctionImage;
import com.ddang.ddang.image.domain.ProfileImage;
import com.ddang.ddang.region.domain.AuctionRegion;
import com.ddang.ddang.region.domain.Region;
import com.ddang.ddang.user.domain.User;
Expand Down Expand Up @@ -489,4 +489,66 @@ class AuctionTest {
// then
assertThat(actual).isEmpty();
}

@Test
void 마지막_입찰자를_반환한다() {
// given
final User seller = User.builder()
.name("회원1")
.profileImage(new ProfileImage("upload.png", "store.png"))
.reliability(4.7d)
.oauthId("12345")
.build();
final User bidder = User.builder()
.name("회원2")
.profileImage(new ProfileImage("upload.png", "store.png"))
.reliability(4.7d)
.oauthId("12346")
.build();
userRepository.save(seller);
userRepository.save(bidder);

final LocalDateTime pastTime = LocalDateTime.now().minusDays(3L);

final Auction auction = Auction.builder()
.title("경매")
.seller(seller)
.closingTime(pastTime)
.startPrice(new Price(1_000))
.bidUnit(new BidUnit(1_000))
.build();
auction.updateLastBid(new Bid(auction, bidder, new BidPrice(10_000)));

// when
final Optional<User> actual = auction.findLastBidder();

// then
assertThat(actual).contains(bidder);
}

@Test
void 마지막_입찰자가_없다면_빈_Optional을_반환한다() {
// given
final User seller = User.builder()
.name("회원1")
.profileImage(new ProfileImage("upload.png", "store.png"))
.reliability(4.7d)
.oauthId("12345")
.build();
userRepository.save(seller);

final LocalDateTime pastTime = LocalDateTime.now().minusDays(3L);

final Auction auction = Auction.builder()
.title("경매")
.seller(seller)
.closingTime(pastTime)
.build();

// when
final Optional<User> actual = auction.findLastBidder();

// then
assertThat(actual).isEmpty();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
package com.ddang.ddang.auction.infrastructure.persistence;

import com.ddang.ddang.auction.domain.Auction;
import com.ddang.ddang.auction.domain.BidUnit;
import com.ddang.ddang.auction.domain.Price;
import com.ddang.ddang.auction.infrastructure.persistence.dto.AuctionAndImageDto;
import com.ddang.ddang.configuration.JpaConfiguration;
import com.ddang.ddang.configuration.QuerydslConfiguration;
import com.ddang.ddang.image.domain.AuctionImage;
import com.ddang.ddang.image.domain.ProfileImage;
import com.ddang.ddang.image.infrastructure.persistence.JpaAuctionImageRepository;
import com.ddang.ddang.user.domain.User;
import com.ddang.ddang.user.infrastructure.persistence.JpaUserRepository;
import jakarta.persistence.EntityManager;
import jakarta.persistence.PersistenceContext;
import org.junit.jupiter.api.DisplayNameGeneration;
import org.junit.jupiter.api.DisplayNameGenerator;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
import org.springframework.context.annotation.Import;

import java.time.LocalDateTime;
import java.util.List;
import java.util.Optional;

import static org.assertj.core.api.Assertions.assertThat;

@DataJpaTest
@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class)
@SuppressWarnings("NonAsciiCharacters")
@Import({JpaConfiguration.class, QuerydslConfiguration.class})
class QuerydslAuctionAndImageRepositoryImplTest {

@PersistenceContext
EntityManager em;

@Autowired
JpaAuctionRepository auctionRepository;

@Autowired
JpaAuctionImageRepository auctionImageRepository;

@Autowired
JpaUserRepository userRepository;

@Test
void 경매와_경매_대표이미지를_조회한다() {
// given
final Auction auction = Auction.builder()
.title("경매 상품 1")
.description("이것은 경매 상품 1 입니다.")
.bidUnit(new BidUnit(1_000))
.startPrice(new Price(1_000))
.closingTime(LocalDateTime.now())
.build();
final AuctionImage auctionImage = new AuctionImage("upload.png", "store.png");
auctionImageRepository.save(auctionImage);
auction.addAuctionImages(List.of(auctionImage));

final User user = User.builder()
.name("사용자")
.profileImage(new ProfileImage("upload.png", "store.png"))
.reliability(4.7d)
.oauthId("12345")
.build();

auctionRepository.save(auction);
userRepository.save(user);

final AuctionAndImageDto expect = new AuctionAndImageDto(auction, auction.getAuctionImages().get(0));

em.flush();
em.clear();

// when
final Optional<AuctionAndImageDto> actual = auctionRepository.findDtoByAuctionId(auction.getId());

// then
assertThat(actual).contains(expect);
}
}
Loading