diff --git a/backend/src/main/java/org/sejongisc/backend/betting/entity/BetRound.java b/backend/src/main/java/org/sejongisc/backend/betting/entity/BetRound.java index 9487110b..80f14156 100644 --- a/backend/src/main/java/org/sejongisc/backend/betting/entity/BetRound.java +++ b/backend/src/main/java/org/sejongisc/backend/betting/entity/BetRound.java @@ -3,6 +3,8 @@ import jakarta.persistence.*; import lombok.*; import org.sejongisc.backend.common.entity.postgres.BasePostgresEntity; +import org.sejongisc.backend.common.exception.CustomException; +import org.sejongisc.backend.common.exception.ErrorCode; import java.math.BigDecimal; import java.time.LocalDateTime; @@ -11,10 +13,12 @@ @Entity @Builder @Getter -@NoArgsConstructor @AllArgsConstructor +@NoArgsConstructor +@AllArgsConstructor public class BetRound extends BasePostgresEntity { - @Id @GeneratedValue(strategy = GenerationType.UUID) + @Id + @GeneratedValue(strategy = GenerationType.UUID) @Column(columnDefinition = "uuid") private UUID betRoundID; @@ -35,7 +39,7 @@ public class BetRound extends BasePostgresEntity { private BigDecimal baseMultiplier; @Column(nullable = false) - private boolean status; // enum 고려할 것 + private boolean status = false; // Todo : Enum 클래스로 수정 private LocalDateTime openAt; @@ -56,4 +60,49 @@ public class BetRound extends BasePostgresEntity { @Column(precision = 15, scale = 2) private BigDecimal settleClosePrice; + + public boolean isOpen() { + return this.status; + } + + public boolean isClosed() { + return !this.status; + } + + public void open() { + this.status = true; + } + + public void close() { + this.status = false; + } + + public void validate() { + if (isClosed() || (lockAt != null && LocalDateTime.now().isAfter(lockAt))) { + throw new CustomException(ErrorCode.BET_ROUND_CLOSED); + } + } + + public void settle(BigDecimal finalPrice) { + if (isOpen()) { + throw new CustomException(ErrorCode.BET_ROUND_NOT_CLOSED); + } + if (this.settleAt != null) { + return; + } + if (finalPrice == null) { + throw new IllegalArgumentException("finalPrice must not be null"); + } + this.settleClosePrice = finalPrice; + this.resultOption = determineResult(finalPrice); + this.settleAt = LocalDateTime.now(); + } + + private BetOption determineResult(BigDecimal finalPrice) { + int compare = finalPrice.compareTo(previousClosePrice); + + if (compare >= 0) return BetOption.RISE; + return BetOption.FALL; + } + } diff --git a/backend/src/main/java/org/sejongisc/backend/betting/entity/BetStatus.java b/backend/src/main/java/org/sejongisc/backend/betting/entity/BetStatus.java index 73ee274f..35ca6524 100644 --- a/backend/src/main/java/org/sejongisc/backend/betting/entity/BetStatus.java +++ b/backend/src/main/java/org/sejongisc/backend/betting/entity/BetStatus.java @@ -2,5 +2,6 @@ public enum BetStatus { ACTIVE, - DELETED + DELETED, // 삭제 + CLOSED // 정산 완료 } \ No newline at end of file diff --git a/backend/src/main/java/org/sejongisc/backend/betting/entity/Stock.java b/backend/src/main/java/org/sejongisc/backend/betting/entity/Stock.java index 827857ef..bbf4f9cf 100644 --- a/backend/src/main/java/org/sejongisc/backend/betting/entity/Stock.java +++ b/backend/src/main/java/org/sejongisc/backend/betting/entity/Stock.java @@ -21,7 +21,7 @@ public class Stock { @Column(nullable = false, length = 100) private String name; - @Column(nullable = false, length = 50) + @Column(nullable = false, length = 50, unique = true) private String symbol; @Enumerated(EnumType.STRING) @@ -30,4 +30,7 @@ public class Stock { @Column(precision = 15, scale = 2, nullable = false) private BigDecimal previousClosePrice; + + @Column(precision = 15, scale = 2) + private BigDecimal settleClosePrice; } diff --git a/backend/src/main/java/org/sejongisc/backend/betting/entity/UserBet.java b/backend/src/main/java/org/sejongisc/backend/betting/entity/UserBet.java index 693e84fc..bc934821 100644 --- a/backend/src/main/java/org/sejongisc/backend/betting/entity/UserBet.java +++ b/backend/src/main/java/org/sejongisc/backend/betting/entity/UserBet.java @@ -51,4 +51,19 @@ public class UserBet extends BasePostgresEntity { @Enumerated(EnumType.STRING) @Column(nullable = false) private BetStatus betStatus; + + private boolean isCollect; + + public void win(int reward) { + this.payoutPoints = reward; + this.isCollect = true; + this.betStatus = BetStatus.CLOSED; + } + + public void lose() { + this.payoutPoints = 0; + this.isCollect = false; + this.betStatus = BetStatus.CLOSED; + } + } diff --git a/backend/src/main/java/org/sejongisc/backend/betting/repository/BetRoundRepository.java b/backend/src/main/java/org/sejongisc/backend/betting/repository/BetRoundRepository.java index d06debfb..b9c26ed5 100644 --- a/backend/src/main/java/org/sejongisc/backend/betting/repository/BetRoundRepository.java +++ b/backend/src/main/java/org/sejongisc/backend/betting/repository/BetRoundRepository.java @@ -15,4 +15,6 @@ public interface BetRoundRepository extends JpaRepository { List findAllByOrderBySettleAtDesc(); List findByStatusTrueAndLockAtLessThanEqual(LocalDateTime now); + + List findByStatusFalseAndSettleAtIsNullAndLockAtLessThanEqual(LocalDateTime now); } diff --git a/backend/src/main/java/org/sejongisc/backend/betting/repository/StockRepository.java b/backend/src/main/java/org/sejongisc/backend/betting/repository/StockRepository.java index b35b5fcd..40c7ed88 100644 --- a/backend/src/main/java/org/sejongisc/backend/betting/repository/StockRepository.java +++ b/backend/src/main/java/org/sejongisc/backend/betting/repository/StockRepository.java @@ -3,5 +3,8 @@ import org.sejongisc.backend.betting.entity.Stock; import org.springframework.data.jpa.repository.JpaRepository; +import java.util.Optional; + public interface StockRepository extends JpaRepository { + Optional findBySymbol(String symbol); } diff --git a/backend/src/main/java/org/sejongisc/backend/betting/repository/UserBetRepository.java b/backend/src/main/java/org/sejongisc/backend/betting/repository/UserBetRepository.java index a1e81052..7e7e5d80 100644 --- a/backend/src/main/java/org/sejongisc/backend/betting/repository/UserBetRepository.java +++ b/backend/src/main/java/org/sejongisc/backend/betting/repository/UserBetRepository.java @@ -10,8 +10,10 @@ public interface UserBetRepository extends JpaRepository { boolean existsByRoundAndUserId(BetRound round, UUID userId); - Optional findByUserBetIdAndUserId(UUID userBetId, UUID userId); + Optional findByUserBetIdAndUserId(UUID userBetId, UUID userId); List findAllByUserIdOrderByRound_SettleAtDesc(UUID userId); + + List findAllByRound(BetRound round); } diff --git a/backend/src/main/java/org/sejongisc/backend/betting/service/BettingScheduler.java b/backend/src/main/java/org/sejongisc/backend/betting/service/BettingScheduler.java index ff43b59b..4d9d4260 100644 --- a/backend/src/main/java/org/sejongisc/backend/betting/service/BettingScheduler.java +++ b/backend/src/main/java/org/sejongisc/backend/betting/service/BettingScheduler.java @@ -1,33 +1,49 @@ package org.sejongisc.backend.betting.service; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; import org.sejongisc.backend.betting.entity.Scope; import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Component; +import java.time.LocalDateTime; + @Component +@Slf4j @RequiredArgsConstructor public class BettingScheduler { private final BettingService bettingService; - @Scheduled(cron = "0 0 9 * * MON-FRI") + @Scheduled(cron = "0 0 9 * * MON-FRI", zone = "Asia/Seoul") public void dailyOpenScheduler() { bettingService.createBetRound(Scope.DAILY); +// log.info("✅ 스케줄러 정상 작동 중: {}", LocalDateTime.now()); } - @Scheduled(cron = "0 0 9 * * MON") + @Scheduled(cron = "0 0 9 * * MON", zone = "Asia/Seoul") public void weeklyOpenScheduler() { bettingService.createBetRound(Scope.WEEKLY); } - @Scheduled(cron = "0 0 22 * * MON-FRI") + @Scheduled(cron = "0 0 22 * * MON-FRI", zone = "Asia/Seoul") public void dailyCloseScheduler() { bettingService.closeBetRound(); } - @Scheduled(cron = "0 0 22 * * FRI") + @Scheduled(cron = "0 0 22 * * FRI", zone = "Asia/Seoul") public void weeklyCloseScheduler() { bettingService.closeBetRound(); } + + @Scheduled(cron = "0 5 22 * * MON-FRI", zone = "Asia/Seoul") + public void dailySettleScheduler() { + bettingService.settleUserBets(); + } + + @Scheduled(cron = "0 5 22 * * FRI", zone = "Asia/Seoul") + public void weeklySettleScheduler() { + bettingService.settleUserBets(); + } + } diff --git a/backend/src/main/java/org/sejongisc/backend/betting/service/BettingService.java b/backend/src/main/java/org/sejongisc/backend/betting/service/BettingService.java index 68c5ac08..f1e1a42c 100644 --- a/backend/src/main/java/org/sejongisc/backend/betting/service/BettingService.java +++ b/backend/src/main/java/org/sejongisc/backend/betting/service/BettingService.java @@ -15,6 +15,7 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import java.math.BigDecimal; import java.time.LocalDateTime; import java.util.List; import java.util.Optional; @@ -32,7 +33,7 @@ public class BettingService { private final Random random = new Random(); - public Optional getActiveRound(Scope type){ + public Optional getActiveRound(Scope type) { return betRoundRepository.findByStatusTrueAndScope(type); } @@ -42,7 +43,7 @@ public List getAllBetRounds() { return betRoundRepository.findAllByOrderBySettleAtDesc(); } - public Stock getStock(){ + public Stock getStock() { List stocks = stockRepository.findAll(); if (stocks.isEmpty()) { throw new CustomException(ErrorCode.STOCK_NOT_FOUND); @@ -51,7 +52,7 @@ public Stock getStock(){ return stocks.get(random.nextInt(stocks.size())); } - public boolean setAllowFree(){ + public boolean setAllowFree() { return random.nextDouble() < 0.2; } @@ -61,28 +62,36 @@ public List getAllMyBets(UUID userId) { } public void createBetRound(Scope scope) { - Stock stock = getStock(); + LocalDateTime now = LocalDateTime.now(); + Stock stock = getStock(); + BetRound betRound = BetRound.builder() .scope(scope) .title(now.toLocalDate() + " " + stock.getName() + " " + scope.name() + " 라운드") .symbol(stock.getSymbol()) .allowFree(setAllowFree()) - .status(true) .openAt(scope.getOpenAt(now)) .lockAt(scope.getLockAt(now)) .market(stock.getMarket()) .previousClosePrice(stock.getPreviousClosePrice()) .build(); + betRound.open(); + betRoundRepository.save(betRound); } - public void closeBetRound(){ - // TODO : status를 false로 바꿔야함, 정산 로직 구현하면서 같이 할 것 + public void closeBetRound() { + LocalDateTime now = LocalDateTime.now(); + List toClose = betRoundRepository.findByStatusTrueAndLockAtLessThanEqual(now); + if (toClose.isEmpty()) return; + toClose.forEach(BetRound::close); + betRoundRepository.saveAll(toClose); } + @Transactional public UserBet postUserBet(UUID userId, UserBetRequest userBetRequest) { BetRound betRound = betRoundRepository.findById(userBetRequest.getRoundId()) @@ -92,12 +101,7 @@ public UserBet postUserBet(UUID userId, UserBetRequest userBetRequest) { throw new CustomException(ErrorCode.BET_DUPLICATE); } - LocalDateTime now = LocalDateTime.now(); - - // 허용 구간: [openAt, lockAt) - if (now.isBefore(betRound.getOpenAt()) || !now.isBefore(betRound.getLockAt())) { - throw new CustomException(ErrorCode.BET_TIME_INVALID); - } + betRound.validate(); int stake = 0; @@ -139,9 +143,7 @@ public void cancelUserBet(UUID userId, UUID userBetId) { BetRound betRound = userBet.getRound(); - if (!LocalDateTime.now().isBefore(betRound.getLockAt())){ - throw new CustomException(ErrorCode.BET_ROUND_CLOSED); - } + betRound.validate(); if (!userBet.isFree() && userBet.getStakePoints() > 0) { pointHistoryService.createPointHistory( @@ -155,4 +157,49 @@ public void cancelUserBet(UUID userId, UUID userBetId) { userBetRepository.delete(userBet); } + + @Transactional + public void settleUserBets() { + LocalDateTime now = LocalDateTime.now(); + + List activeRounds = + betRoundRepository.findByStatusFalseAndSettleAtIsNullAndLockAtLessThanEqual(now); + + for (BetRound round : activeRounds) { + Stock stock = stockRepository.findBySymbol(round.getSymbol()) + .orElseThrow(() -> new CustomException(ErrorCode.STOCK_NOT_FOUND)); + + BigDecimal finalPrice = stock.getSettleClosePrice(); + if (finalPrice == null) { + continue; + } + round.settle(finalPrice); + betRoundRepository.save(round); + + List userBets = userBetRepository.findAllByRound(round); + + for (UserBet bet : userBets) { + if (bet.getBetStatus() != BetStatus.ACTIVE) continue; + if (bet.getOption() == round.getResultOption()) { + int reward = calculateReward(bet); + bet.win(reward); + pointHistoryService.createPointHistory( + bet.getUserId(), + reward, + PointReason.BETTING_WIN, + PointOrigin.BETTING, + round.getBetRoundID() + ); + } else { + bet.lose(); + } + } + userBetRepository.saveAll(userBets); + } + } + + // TODO : 비율을 바탕으로한 reward 계산 로직 + private int calculateReward(UserBet bet) { + return 2; + } } diff --git a/backend/src/main/java/org/sejongisc/backend/common/exception/ErrorCode.java b/backend/src/main/java/org/sejongisc/backend/common/exception/ErrorCode.java index 96df555b..dc9a09cc 100644 --- a/backend/src/main/java/org/sejongisc/backend/common/exception/ErrorCode.java +++ b/backend/src/main/java/org/sejongisc/backend/common/exception/ErrorCode.java @@ -66,10 +66,10 @@ public enum ErrorCode { STOCK_NOT_FOUND(HttpStatus.NOT_FOUND, "주식 종목이 존재하지 않습니다."), BET_ROUND_NOT_FOUND(HttpStatus.NOT_FOUND, "존재하지 않는 라운드입니다."), BET_DUPLICATE(HttpStatus.CONFLICT, "이미 이 라운드에 베팅했습니다."), - BET_TIME_INVALID(HttpStatus.CONFLICT, "베팅 가능 시간이 아닙니다."), + BET_ROUND_CLOSED(HttpStatus.CONFLICT, "베팅 가능 시간이 아닙니다."), BET_NOT_FOUND(HttpStatus.NOT_FOUND, "해당 베팅을 찾을 수 없습니다."), - BET_ROUND_CLOSED(HttpStatus.CONFLICT, "이미 마감된 라운드입니다."), - BET_POINT_TOO_LOW(HttpStatus.CONFLICT, "베팅 포인트는 10 이상이어야 합니다."); + BET_POINT_TOO_LOW(HttpStatus.CONFLICT, "베팅 포인트는 10 이상이어야 합니다."), + BET_ROUND_NOT_CLOSED(HttpStatus.CONFLICT, "닫히지 않은 배팅입니다."); private final HttpStatus status; private final String message; diff --git a/backend/src/test/java/org/sejongisc/backend/betting/service/BettingServiceTest.java b/backend/src/test/java/org/sejongisc/backend/betting/service/BettingServiceTest.java index 26d26a20..3855e717 100644 --- a/backend/src/test/java/org/sejongisc/backend/betting/service/BettingServiceTest.java +++ b/backend/src/test/java/org/sejongisc/backend/betting/service/BettingServiceTest.java @@ -14,14 +14,10 @@ import org.sejongisc.backend.point.entity.PointOrigin; import org.sejongisc.backend.point.entity.PointReason; import org.sejongisc.backend.point.service.PointHistoryService; -import org.sejongisc.backend.user.dao.UserRepository; import java.math.BigDecimal; import java.time.LocalDateTime; -import java.util.Collections; -import java.util.List; -import java.util.Optional; -import java.util.UUID; +import java.util.*; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertThrows; @@ -46,7 +42,6 @@ void setUp() { userBetRepository = mock(UserBetRepository.class); pointHistoryService = mock(PointHistoryService.class); - // BettingService 생성자 시그니처에 맞춰 주입 bettingService = new BettingService( betRoundRepository, stockRepository, @@ -58,6 +53,9 @@ void setUp() { roundId = UUID.randomUUID(); } + // --------------------- + // Stock / util tests + // --------------------- @Test void getStock_빈리스트면_예외발생() { when(stockRepository.findAll()).thenReturn(List.of()); @@ -89,7 +87,7 @@ void setUp() { Stock chosen = bettingService.getStock(); - assertThat(List.of(s1, s2)).contains(chosen); // 둘 중 하나여야 함 + assertThat(List.of(s1, s2)).contains(chosen); verify(stockRepository, times(1)).findAll(); } @@ -99,8 +97,13 @@ void setUp() { assertThat(result).isInstanceOf(Boolean.class); } + // --------------------- + // 조회 관련 테스트 + // --------------------- @Test + @DisplayName("createBetRound_DAILY_정상저장") void createBetRound_DAILY_정상저장() { + // given Stock stock = Stock.builder() .name("삼성전자") .symbol("005930") @@ -110,21 +113,23 @@ void setUp() { when(stockRepository.findAll()).thenReturn(List.of(stock)); + // when bettingService.createBetRound(Scope.DAILY); + // then ArgumentCaptor captor = ArgumentCaptor.forClass(BetRound.class); verify(betRoundRepository, times(1)).save(captor.capture()); - BetRound saved = captor.getValue(); + assertThat(saved.getScope()).isEqualTo(Scope.DAILY); assertThat(saved.getSymbol()).isEqualTo("005930"); assertThat(saved.getTitle()).contains("삼성전자"); + assertThat(saved.isOpen()).isTrue(); // 스케줄러에서 open() 호출 후 저장이라면 true여야 함 } @Test @DisplayName("활성화된 DAILY BetRound 조회 성공") void findActiveRound_Success() { - // given BetRound dailyRound = BetRound.builder() .betRoundID(UUID.randomUUID()) .scope(Scope.DAILY) @@ -136,27 +141,22 @@ void findActiveRound_Success() { when(betRoundRepository.findByStatusTrueAndScope(Scope.DAILY)) .thenReturn(Optional.of(dailyRound)); - // when Optional result = bettingService.getActiveRound(Scope.DAILY); - // then assertThat(result).isPresent(); assertThat(result.get().getScope()).isEqualTo(Scope.DAILY); - assertThat(result.get().isStatus()).isTrue(); + assertThat(result.get().isOpen()).isTrue(); verify(betRoundRepository, times(1)).findByStatusTrueAndScope(Scope.DAILY); } @Test @DisplayName("활성화된 BetRound가 없을 때 빈 Optional 반환") void findActiveRound_NotFound() { - // given when(betRoundRepository.findByStatusTrueAndScope(Scope.DAILY)) .thenReturn(Optional.empty()); - // when Optional result = bettingService.getActiveRound(Scope.DAILY); - // then assertThat(result).isEmpty(); verify(betRoundRepository, times(1)).findByStatusTrueAndScope(Scope.DAILY); } @@ -164,7 +164,6 @@ void findActiveRound_NotFound() { @Test @DisplayName("모든 BetRound 최신순 조회 성공") void getAllBetRounds_Success() { - // given List betRounds = List.of( BetRound.builder() .betRoundID(UUID.randomUUID()) @@ -185,10 +184,8 @@ void getAllBetRounds_Success() { when(betRoundRepository.findAllByOrderBySettleAtDesc()) .thenReturn(betRounds); - // when List results = bettingService.getAllBetRounds(); - // then assertThat(results).hasSize(2); assertThat(results.get(0).getScope()).isEqualTo(Scope.DAILY); assertThat(results.get(1).getScope()).isEqualTo(Scope.WEEKLY); @@ -198,14 +195,11 @@ void getAllBetRounds_Success() { @Test @DisplayName("BetRound가 없을 때 빈 리스트 반환") void getAllBetRounds_Empty() { - // given when(betRoundRepository.findAllByOrderBySettleAtDesc()) .thenReturn(Collections.emptyList()); - // when List results = bettingService.getAllBetRounds(); - // then assertThat(results).isEmpty(); verify(betRoundRepository, times(1)).findAllByOrderBySettleAtDesc(); } @@ -213,7 +207,6 @@ void getAllBetRounds_Empty() { @Test @DisplayName("WEEKLY BetRound 조회 성공") void findActiveRound_Weekly_Success() { - // given BetRound weeklyRound = BetRound.builder() .betRoundID(UUID.randomUUID()) .scope(Scope.WEEKLY) @@ -225,10 +218,8 @@ void findActiveRound_Weekly_Success() { when(betRoundRepository.findByStatusTrueAndScope(Scope.WEEKLY)) .thenReturn(Optional.of(weeklyRound)); - // when Optional result = bettingService.getActiveRound(Scope.WEEKLY); - // then assertThat(result).isPresent(); assertThat(result.get().getScope()).isEqualTo(Scope.WEEKLY); verify(betRoundRepository, times(1)).findByStatusTrueAndScope(Scope.WEEKLY); @@ -237,8 +228,7 @@ void findActiveRound_Weekly_Success() { @Test @DisplayName("getAllMyBets() - 유저 ID로 조회 시 Repository 호출 및 결과 반환 확인") void getAllMyBets_Success() { - // given - UUID userId = UUID.randomUUID(); + UUID u = UUID.randomUUID(); BetRound round = BetRound.builder() .title("테스트 라운드") .openAt(LocalDateTime.now().minusHours(2)) @@ -249,7 +239,7 @@ void getAllMyBets_Success() { UserBet bet1 = UserBet.builder() .userBetId(UUID.randomUUID()) .round(round) - .userId(userId) + .userId(u) .option(BetOption.RISE) .stakePoints(100) .isFree(false) @@ -258,27 +248,28 @@ void getAllMyBets_Success() { UserBet bet2 = UserBet.builder() .userBetId(UUID.randomUUID()) .round(round) - .userId(userId) + .userId(u) .option(BetOption.FALL) .stakePoints(50) .isFree(true) .build(); List mockResult = List.of(bet1, bet2); - when(userBetRepository.findAllByUserIdOrderByRound_SettleAtDesc(userId)) + when(userBetRepository.findAllByUserIdOrderByRound_SettleAtDesc(u)) .thenReturn(mockResult); - // when - List result = bettingService.getAllMyBets(userId); + List result = bettingService.getAllMyBets(u); - // then verify(userBetRepository, times(1)) - .findAllByUserIdOrderByRound_SettleAtDesc(userId); + .findAllByUserIdOrderByRound_SettleAtDesc(u); assertThat(result).hasSize(2); - assertThat(result.get(0).getUserId()).isEqualTo(userId); + assertThat(result.get(0).getUserId()).isEqualTo(u); assertThat(result.get(1).getRound().getTitle()).isEqualTo("테스트 라운드"); } + // --------------------- + // Bet creation / posting tests + // --------------------- private BetRound openRoundNow() { LocalDateTime now = LocalDateTime.now(); return BetRound.builder() @@ -296,7 +287,7 @@ private BetRound closedRoundNow() { return BetRound.builder() .betRoundID(roundId) .scope(Scope.DAILY) - .status(true) + .status(false) .title("CLOSED") .openAt(now.minusMinutes(10)) .lockAt(now.minusMinutes(1)) @@ -317,12 +308,12 @@ private UserBetRequest freeReq() { .roundId(roundId) .option(BetOption.FALL) .free(true) - .stakePoints(999) // 무시되어야 함 + .stakePoints(999) .build(); } @Test - @DisplayName("postUserBet: 유료 베팅 성공 → 포인트 차감 호출 + 저장") + @DisplayName("postUserBet_paid_success") void postUserBet_paid_success() { BetRound round = openRoundNow(); @@ -347,7 +338,7 @@ void postUserBet_paid_success() { } @Test - @DisplayName("postUserBet: 무료 베팅 성공 → 포인트 차감 호출 안함, stake=0") + @DisplayName("postUserBet_free_success") void postUserBet_free_success() { BetRound round = openRoundNow(); @@ -367,7 +358,7 @@ void postUserBet_free_success() { } @Test - @DisplayName("postUserBet: 라운드 없음 → BET_ROUND_NOT_FOUND") + @DisplayName("postUserBet_round_not_found") void postUserBet_round_not_found() { when(betRoundRepository.findById(roundId)).thenReturn(Optional.empty()); @@ -379,7 +370,7 @@ void postUserBet_round_not_found() { } @Test - @DisplayName("postUserBet: 중복 베팅 → BET_DUPLICATE") + @DisplayName("postUserBet_duplicate") void postUserBet_duplicate() { BetRound round = openRoundNow(); when(betRoundRepository.findById(roundId)).thenReturn(Optional.of(round)); @@ -394,7 +385,7 @@ void postUserBet_duplicate() { } @Test - @DisplayName("postUserBet: 베팅 시간 아님 → BET_TIME_INVALID") + @DisplayName("postUserBet_time_invalid") void postUserBet_time_invalid() { BetRound closed = closedRoundNow(); when(betRoundRepository.findById(roundId)).thenReturn(Optional.of(closed)); @@ -403,13 +394,16 @@ void postUserBet_time_invalid() { CustomException ex = assertThrows(CustomException.class, () -> bettingService.postUserBet(userId, paidReq(100))); - assertThat(ex.getErrorCode()).isEqualTo(ErrorCode.BET_TIME_INVALID); + assertThat(ex.getErrorCode()).isEqualTo(ErrorCode.BET_ROUND_CLOSED); verifyNoInteractions(pointHistoryService); verify(userBetRepository, never()).save(any()); } + // --------------------- + // cancelUserBet tests + // --------------------- @Test - @DisplayName("cancelUserBet: 유료 베팅 취소 → 환불 호출 + 삭제") + @DisplayName("cancelUserBet_paid_refund_and_delete") void cancelUserBet_paid_refund_and_delete() { BetRound round = openRoundNow(); UUID userBetId = UUID.randomUUID(); @@ -438,7 +432,7 @@ void cancelUserBet_paid_refund_and_delete() { } @Test - @DisplayName("cancelUserBet: 본인 소유/존재 X → BET_NOT_FOUND") + @DisplayName("cancelUserBet_not_found") void cancelUserBet_not_found() { UUID userBetId = UUID.randomUUID(); when(userBetRepository.findByUserBetIdAndUserId(userBetId, userId)) @@ -453,7 +447,7 @@ void cancelUserBet_not_found() { } @Test - @DisplayName("cancelUserBet: 마감 이후 취소 → BET_ROUND_CLOSED") + @DisplayName("cancelUserBet_after_lock") void cancelUserBet_after_lock() { BetRound closed = closedRoundNow(); UUID userBetId = UUID.randomUUID(); @@ -477,4 +471,4 @@ void cancelUserBet_after_lock() { verify(pointHistoryService, never()).createPointHistory(any(), anyInt(), any(), any(), any()); verify(userBetRepository, never()).delete(any()); } -} \ No newline at end of file +}