Skip to content

Commit

Permalink
[feat] : 경매 입찰 종료 시 스케줄러로 경매, 입찰 상태 변경 (#107)
Browse files Browse the repository at this point in the history
* [feat] : 경매 최근 waiting 상태 bidding 조회 동적 쿼리 추가

* [test] : test auditing config 추가

* [test] : 경매 최근 waiting 상태 bidding 조회 동적 쿼리 추가 테스트

* [feat] : 변경된 메서드 서비스 로직에 반영

* [test] : 변경된 메서드 서비스 테스트에 반영

* [test] : 거래 취소 통합 테스트 구체화

* [refactor] : 불필요한 로직 삭제

* [feat] : 입찰 최초 생성 시 입찰 상태 바꾸는 로직 삭제

* [refactor] : 불필요한 로직 삭제

* [feat] : 마감일 지난 경매 최근 입찰 상태 준비중으로 변경 동적 쿼리 추가

* [test] : 마감일이 지난 경매의 최근 입찰 상태 준비중으로 변경 동적 쿼리 테스트

* [feat] : 입찰 종료시 최근 입찰 상태 준비중으로 변경 스케줄러 추가

* [feat] : 벌크 연산 jpql -> 동적 쿼리로 수정

* [test] : 벌크 연산 jpql -> 동적 쿼리로 수정 테스트 반영

* [feat] : auction 상태 완료 로직 추가

* [feat] : 거래 완료 시 Auction 상태 변경 로직 추가

* [test] : 거래 완료 시 Auction 상태 변경 로직 테스트 반영

* [style] : 코드 리포멧팅

* [refactor] : 교체로 안 쓰는 로직 삭제

* [feat] : 스케줄러에 경매 상태 변경 동적 쿼리 반영

* [test] : 테스트에 경매 상태 변경 동적 쿼리 반영
  • Loading branch information
hyun2371 authored Mar 17, 2024
1 parent 1af37f9 commit 36cd31d
Show file tree
Hide file tree
Showing 18 changed files with 314 additions and 75 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@

import java.util.List;

import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
Expand All @@ -20,6 +19,7 @@
import org.springframework.transaction.annotation.Transactional;

import dev.handsup.auction.domain.Auction;
import dev.handsup.auction.domain.auction_field.AuctionStatus;
import dev.handsup.auction.repository.product.ProductCategoryRepository;
import dev.handsup.auction.service.AuctionService;
import dev.handsup.auth.service.JwtProvider;
Expand Down Expand Up @@ -94,7 +94,7 @@ void registerBiddingTest() throws Exception {
jsonPath("$.auctionId").value(auction1.getId()),
jsonPath("$.bidderId").value(bidder.getId()),
jsonPath("$.bidderNickname").value(bidder.getNickname()),
jsonPath("$.tradingStatus").value(TradingStatus.PREPARING.getLabel()),
jsonPath("$.tradingStatus").value(TradingStatus.WAITING.getLabel()),
jsonPath("$.imgUrl").value(bidder.getProfileImageUrl())
);

Expand Down Expand Up @@ -161,10 +161,12 @@ void getTop3BidsForAuctionTest() throws Exception {
).andDo(MockMvcResultHandlers.print());
}

@Transactional
@DisplayName("[판매자는 진행 중인 거래를 완료 상태로 변경할 수 있다.]")
@Test
void completeTrading() throws Exception {
//given
ReflectionTestUtils.setField(auction1, "status", AuctionStatus.TRADING); //변경 감지
Bidding bidding = BiddingFixture.bidding(bidder, auction1, TradingStatus.PROGRESSING);
biddingRepository.save(bidding);
//when, then
Expand Down Expand Up @@ -197,19 +199,22 @@ void completeTrading_fails() throws Exception {
@Test
void cancelTrading() throws Exception {
//given
Bidding bidding1 = BiddingFixture.bidding(bidder, auction1, TradingStatus.PROGRESSING);
Bidding bidding2 = BiddingFixture.bidding(bidder, auction1, TradingStatus.WAITING);
biddingRepository.saveAll(List.of(bidding1, bidding2));
Bidding waitingBidding1 = BiddingFixture.bidding(bidder, auction1, TradingStatus.WAITING, 200000);
Bidding waitingBidding2 = BiddingFixture.bidding(bidder, auction1, TradingStatus.WAITING, 300000);
Bidding anotherAuctionBidding = BiddingFixture.bidding(bidder, auction2, TradingStatus.WAITING, 400000);
Bidding progressingBidding = BiddingFixture.bidding(bidder, auction1, TradingStatus.PROGRESSING, 500000);
biddingRepository.saveAll(List.of(waitingBidding1, waitingBidding2, anotherAuctionBidding, progressingBidding));

//when, then
mockMvc.perform(patch("/api/auctions/bids/{biddingId}/cancel", bidding1.getId())
mockMvc.perform(patch("/api/auctions/bids/{biddingId}/cancel", progressingBidding.getId())
.contentType(APPLICATION_JSON)
.header(AUTHORIZATION, "Bearer " + sellerAccessToken))
.andExpect(status().isOk())
.andExpect(jsonPath("$.tradingStatus").value(TradingStatus.CANCELED.getLabel()))
.andExpect(jsonPath("$.auctionId").value(auction1.getId()))
.andExpect(jsonPath("$.bidderId").value(bidder.getId()));

assertThat(bidding2.getTradingStatus()).isEqualTo(TradingStatus.PREPARING); // 변경 감지 위해 @Transactional 필요
assertThat(waitingBidding2.getTradingStatus()).isEqualTo(TradingStatus.PREPARING); // 변경 감지 위해 @Transactional 필요
}

@DisplayName("[판매자는 거래가 진행중이 아니면 취소할 수 없다.]")
Expand Down
9 changes: 9 additions & 0 deletions core/src/main/java/dev/handsup/auction/domain/Auction.java
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,9 @@
import dev.handsup.auction.domain.product.Product;
import dev.handsup.auction.domain.product.ProductStatus;
import dev.handsup.auction.domain.product.product_category.ProductCategory;
import dev.handsup.auction.exception.AuctionErrorCode;
import dev.handsup.common.entity.TimeBaseEntity;
import dev.handsup.common.exception.ValidationException;
import dev.handsup.user.domain.User;
import jakarta.persistence.Column;
import jakarta.persistence.Embedded;
Expand Down Expand Up @@ -159,6 +161,13 @@ public void changeAuctionStatusTrading() {
status = AuctionStatus.TRADING;
}

public void changeAuctionStatusCompleted() {
if (status != TRADING) {
throw new ValidationException(AuctionErrorCode.CAN_NOT_COMPLETE_AUCTION);
}
status = COMPLETED;
}

public void increaseBookmarkCount() {
bookmarkCount++;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,9 @@ public enum AuctionErrorCode implements ErrorCode {
NOT_FOUND_TADE_METHOD("거래 방법을 올바르게 입력해주세요.", "AU_003"),
NOT_FOUND_PRODUCT_CATEGORY("상품 카테고리를 올바르게 입력해주세요.", "AU_004"),
INVALID_SORT_INPUT("정렬 기준을 올바르게 입력해주세요.", "AU_005"),
EMPTY_SORT_INPUT("정렬 기준을 입력해주세요.", "AU_006");
EMPTY_SORT_INPUT("정렬 기준을 입력해주세요.", "AU_006"),
CAN_NOT_COMPLETE_AUCTION("경매를 완료 상태로 변경할 수 없습니다.", "AU_007");

private final String message;
private final String code;
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,6 @@ public interface AuctionQueryRepository {
Slice<Auction> sortAuctionByCriteria(String si, String gu, String dong, Pageable pageable);

Slice<Auction> findByProductCategories(List<ProductCategory> productCategories, Pageable pageable);

void updateAuctionStatusTrading();
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,14 @@
import static dev.handsup.auction.domain.product.product_category.QProductCategory.*;
import static org.springframework.util.StringUtils.*;

import java.time.LocalDate;
import java.util.List;

import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Slice;
import org.springframework.data.domain.SliceImpl;
import org.springframework.stereotype.Repository;
import org.springframework.transaction.annotation.Transactional;

import com.querydsl.core.types.OrderSpecifier;
import com.querydsl.core.types.dsl.BooleanExpression;
Expand Down Expand Up @@ -94,6 +96,16 @@ public Slice<Auction> findByProductCategories(List<ProductCategory> productCateg
return new SliceImpl<>(content, pageable, hasNext);
}

@Override
@Transactional
public void updateAuctionStatusTrading() {
queryFactory
.update(auction)
.set(auction.status, AuctionStatus.TRADING)
.where(auction.endDate.eq(LocalDate.now().minusDays(1)))
.execute();
}

private OrderSpecifier<?> searchAuctionSort(Pageable pageable) {
return pageable.getSort().stream()
.findFirst()
Expand Down
Original file line number Diff line number Diff line change
@@ -1,33 +1,17 @@
package dev.handsup.auction.repository.auction;

import java.time.LocalDate;

import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Slice;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Modifying;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.springframework.transaction.annotation.Transactional;

import dev.handsup.auction.domain.Auction;
import dev.handsup.auction.domain.auction_field.AuctionStatus;
import dev.handsup.user.domain.User;

public interface AuctionRepository extends JpaRepository<Auction, Long> {
Boolean existsByStatus(AuctionStatus status);

@Query("select distinct b.auction from Bookmark b " +
"where b.user = :user")
Slice<Auction> findBookmarkAuction(@Param("user") User user, Pageable pageable);

@Transactional
@Modifying(clearAutomatically = true)
@Query("update Auction a set a.status = :newStatus "
+ "where a.status = :currentStatus and a.endDate < :todayDate")
void updateAuctionStatus(
@Param("currentStatus") AuctionStatus currentStatus,
@Param("newStatus") AuctionStatus newStatus,
@Param("todayDate") LocalDate todayDate
);
}
Original file line number Diff line number Diff line change
@@ -1,25 +1,20 @@
package dev.handsup.auction.scheduler;

import java.text.SimpleDateFormat;
import java.time.LocalDate;

import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;

import dev.handsup.auction.domain.auction_field.AuctionStatus;
import dev.handsup.auction.repository.auction.AuctionRepository;
import dev.handsup.auction.repository.auction.AuctionQueryRepository;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;

@Component
@RequiredArgsConstructor
@Slf4j
public class AuctionScheduler {
private final AuctionRepository auctionRepository;
private final SimpleDateFormat dateFormat = new SimpleDateFormat("HH:mm:ss");
private final AuctionQueryRepository auctionQueryRepository;

@Scheduled(cron = "0 0 0 * * *", zone = "Asia/Seoul")
public void updateAuctionStatus() {
auctionRepository.updateAuctionStatus(AuctionStatus.BIDDING, AuctionStatus.TRADING, LocalDate.now());
auctionQueryRepository.updateAuctionStatusTrading();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package dev.handsup.bidding.repository;

import java.util.Optional;

import dev.handsup.auction.domain.Auction;
import dev.handsup.bidding.domain.Bidding;

public interface BiddingQueryRepository {

Optional<Bidding> findWaitingBiddingLatest(Auction auction);

void updateBiddingTradingStatus();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package dev.handsup.bidding.repository;

import static dev.handsup.bidding.domain.QBidding.*;

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

import org.springframework.stereotype.Repository;
import org.springframework.transaction.annotation.Transactional;

import com.querydsl.jpa.impl.JPAQueryFactory;

import dev.handsup.auction.domain.Auction;
import dev.handsup.auction.domain.QAuction;
import dev.handsup.bidding.domain.Bidding;
import dev.handsup.bidding.domain.QBidding;
import dev.handsup.bidding.domain.TradingStatus;
import lombok.RequiredArgsConstructor;

@Repository
@RequiredArgsConstructor
public class BiddingQueryRepositoryImpl implements BiddingQueryRepository {
private final JPAQueryFactory queryFactory;

@Override
public Optional<Bidding> findWaitingBiddingLatest(Auction auction) {
Bidding bidding = queryFactory.select(QBidding.bidding)
.from(QBidding.bidding)
.where(
QAuction.auction.eq(auction),
QBidding.bidding.tradingStatus.eq(TradingStatus.WAITING)
)
.orderBy(QBidding.bidding.createdAt.desc())
.fetchFirst();
return Optional.ofNullable(bidding);
}

@Override
@Transactional
public void updateBiddingTradingStatus() {
//하루 지난 각 경매들에 대한 최신 입찰 조회
List<Long> latestBiddingIdsPerAuctions = queryFactory
.select(bidding.id.max())
.from(bidding)
.where(bidding.auction.endDate.eq(LocalDate.now().minusDays(1)))
.groupBy(bidding.auction)
.fetch();

// 해당 최신 입찰 상태를 준비중으로 업데이트
queryFactory
.update(bidding)
.set(bidding.tradingStatus, TradingStatus.PREPARING)
.where(bidding.id.in(latestBiddingIdsPerAuctions))
.execute();
}
}
Original file line number Diff line number Diff line change
@@ -1,21 +1,16 @@
package dev.handsup.bidding.repository;

import java.util.Optional;

import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Slice;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;

import dev.handsup.bidding.domain.Bidding;
import dev.handsup.bidding.domain.TradingStatus;

public interface BiddingRepository extends JpaRepository<Bidding, Long> {

@Query("SELECT MAX(b.biddingPrice) FROM Bidding b WHERE b.auction.id = :auctionId")
Integer findMaxBiddingPriceByAuctionId(Long auctionId);

Slice<Bidding> findByAuctionIdOrderByBiddingPriceDesc(Long auctionId, Pageable pageable);

Optional<Bidding> findFirstByTradingStatus(TradingStatus tradingStatus);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package dev.handsup.bidding.scheduler;

import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;

import dev.handsup.bidding.repository.BiddingQueryRepository;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;

@Component
@RequiredArgsConstructor
@Slf4j
public class BiddingScheduler {

private BiddingQueryRepository biddingQueryRepository;

@Scheduled(cron = "0 0 0 * * *", zone = "Asia/Seoul")
public void updateTradingStatus() {
biddingQueryRepository.updateBiddingTradingStatus();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,11 @@
import dev.handsup.auction.domain.Auction;
import dev.handsup.auction.service.AuctionService;
import dev.handsup.bidding.domain.Bidding;
import dev.handsup.bidding.domain.TradingStatus;
import dev.handsup.bidding.dto.BiddingMapper;
import dev.handsup.bidding.dto.request.RegisterBiddingRequest;
import dev.handsup.bidding.dto.response.BiddingResponse;
import dev.handsup.bidding.exception.BiddingErrorCode;
import dev.handsup.bidding.repository.BiddingQueryRepository;
import dev.handsup.bidding.repository.BiddingRepository;
import dev.handsup.common.dto.CommonMapper;
import dev.handsup.common.dto.PageResponse;
Expand All @@ -28,16 +28,14 @@
public class BiddingService {

private final BiddingRepository biddingRepository;
private final BiddingQueryRepository biddingQueryRepository;
private final AuctionService auctionService;
private boolean isFirstBidding;

private void validateBiddingPrice(int biddingPrice, Auction auction) {
isFirstBidding = false;
Integer maxBiddingPrice = biddingRepository.findMaxBiddingPriceByAuctionId(auction.getId());

if (maxBiddingPrice == null) {
// 입찰 내역이 없는 경우, 최소 입찰가부터 입찰 가능
isFirstBidding = true;
if (biddingPrice < auction.getInitPrice()) {
throw new ValidationException(BiddingErrorCode.BIDDING_PRICE_LESS_THAN_INIT_PRICE);
}
Expand All @@ -62,9 +60,7 @@ public BiddingResponse registerBidding(RegisterBiddingRequest request, Long auct
Bidding savedBidding = biddingRepository.save(
BiddingMapper.toBidding(request, auction, bidder)
);
if (isFirstBidding) {
savedBidding.updateTradingStatusPreparing(); // 첫 입찰일 경우 준비중 상태로 변경
}

auction.updateCurrentBiddingPrice(savedBidding.getBiddingPrice());
auction.increaseBiddingCount();

Expand All @@ -85,7 +81,7 @@ public BiddingResponse completeTrading(Long biddingId, User seller) {
validateAuthorization(bidding, seller);
bidding.updateTradingStatusComplete();
bidding.getAuction().updateBuyer(bidding.getBidder());

bidding.getAuction().changeAuctionStatusCompleted();
//

return BiddingMapper.toBiddingResponse(bidding);
Expand All @@ -96,7 +92,7 @@ public BiddingResponse cancelTrading(Long biddingId, User seller) {
Bidding bidding = findBiddingById(biddingId);
validateAuthorization(bidding, seller);
bidding.updateTradingStatusCanceled();
biddingRepository.findFirstByTradingStatus(TradingStatus.WAITING) // 다음 입찰 준비중 상태로 변경
biddingQueryRepository.findWaitingBiddingLatest(bidding.getAuction()) // 다음 입찰 준비중 상태로 변경
.ifPresent(Bidding::updateTradingStatusPreparing);
return BiddingMapper.toBiddingResponse(bidding);
}
Expand Down
Loading

0 comments on commit 36cd31d

Please sign in to comment.