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
Original file line number Diff line number Diff line change
@@ -1,20 +1,24 @@
package org.sejongisc.backend.betting.controller;


import jakarta.validation.Valid;
import jakarta.validation.constraints.Pattern;
import lombok.RequiredArgsConstructor;
import org.sejongisc.backend.betting.dto.UserBetRequest;
import org.sejongisc.backend.betting.entity.BetRound;
import org.sejongisc.backend.betting.entity.Scope;
import org.sejongisc.backend.betting.entity.UserBet;
import org.sejongisc.backend.betting.service.BettingService;
import org.sejongisc.backend.common.auth.springsecurity.CustomUserDetails;
import org.sejongisc.backend.user.entity.User;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.bind.annotation.*;

import java.util.List;
import java.util.Optional;
import java.util.UUID;

@RestController
@RequiredArgsConstructor
Expand All @@ -24,7 +28,7 @@ public class BettingController {

private final BettingService bettingService;

@GetMapping("/bet-round/{scope}")
@GetMapping("/bet-rounds/{scope}")
public ResponseEntity<BetRound> getTodayBetRound(
@PathVariable @Pattern(regexp = "daily|weekly") String scope){

Expand All @@ -37,10 +41,30 @@ public ResponseEntity<BetRound> getTodayBetRound(
.orElseGet(() -> ResponseEntity.notFound().build());
}

@GetMapping("/bet-round/history")
@GetMapping("/bet-rounds/history")
public ResponseEntity<List<BetRound>> getAllBetRounds(){
List<BetRound> betRounds = bettingService.getAllBetRounds();

return ResponseEntity.ok(betRounds);
}

@PostMapping("/user-bets")
public ResponseEntity<UserBet> postUserBet(
@AuthenticationPrincipal CustomUserDetails principal,
@RequestBody @Valid UserBetRequest userBetRequest){

UserBet userBet = bettingService.postUserBet(principal.getUserId(), userBetRequest);

return ResponseEntity.ok(userBet);
}

@DeleteMapping("/user-bets/{userBetId}")
public ResponseEntity<Void> cancelUserBet(
@AuthenticationPrincipal CustomUserDetails principal,
@PathVariable UUID userBetId){

bettingService.cancelUserBet(principal.getUserId(), userBetId);
return ResponseEntity.noContent().build();
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package org.sejongisc.backend.betting.dto;

import com.fasterxml.jackson.annotation.JsonProperty;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotNull;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import org.sejongisc.backend.betting.entity.BetOption;

import java.util.UUID;

@Getter
@Builder @AllArgsConstructor @NoArgsConstructor
public class UserBetRequest {

@NotNull(message = "라운드 ID는 필수입니다.")
private UUID roundId;

@NotNull(message = "베팅 옵션은 필수입니다.")
private BetOption option;

@JsonProperty("isFree")
private boolean free;

@Min(value = 10, message = "베팅 포인트는 10 이상이어야 합니다.")
private Integer stakePoints;
}
Original file line number Diff line number Diff line change
@@ -1,11 +1,17 @@
package org.sejongisc.backend.betting.entity;

import jakarta.persistence.*;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import org.sejongisc.backend.common.entity.postgres.BasePostgresEntity;

import java.util.UUID;

@Entity
@Getter
@Builder @NoArgsConstructor @AllArgsConstructor
public class UserBet extends BasePostgresEntity {

@Id @GeneratedValue(strategy = GenerationType.UUID)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package org.sejongisc.backend.betting.repository;

import org.sejongisc.backend.betting.entity.BetRound;
import org.sejongisc.backend.betting.entity.UserBet;
import org.springframework.data.jpa.repository.JpaRepository;

import java.util.Optional;
import java.util.UUID;

public interface UserBetRepository extends JpaRepository<UserBet, UUID> {
boolean existsByRoundAndUserId(BetRound round, UUID userId);
Optional<UserBet> findByUserBetIdAndUserId(UUID userBetId, UUID userId);
}
Original file line number Diff line number Diff line change
@@ -1,26 +1,33 @@
package org.sejongisc.backend.betting.service;

import lombok.RequiredArgsConstructor;
import org.sejongisc.backend.betting.entity.BetRound;
import org.sejongisc.backend.betting.entity.Scope;
import org.sejongisc.backend.betting.entity.Stock;
import org.sejongisc.backend.betting.dto.UserBetRequest;
import org.sejongisc.backend.betting.entity.*;
import org.sejongisc.backend.betting.repository.BetRoundRepository;
import org.sejongisc.backend.betting.repository.StockRepository;
import org.sejongisc.backend.betting.repository.UserBetRepository;
import org.sejongisc.backend.common.exception.CustomException;
import org.sejongisc.backend.common.exception.ErrorCode;
import org.sejongisc.backend.point.entity.PointOrigin;
import org.sejongisc.backend.point.entity.PointReason;
import org.sejongisc.backend.point.service.PointHistoryService;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

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

@Service
@RequiredArgsConstructor
public class BettingService {

private final BetRoundRepository betRoundRepository;
private final StockRepository stockRepository;
private final UserBetRepository userBetRepository;
private final PointHistoryService pointHistoryService;

private final Random random = new Random();

Expand Down Expand Up @@ -70,4 +77,67 @@ public void closeBetRound(){
// TODO : status를 false로 바꿔야함, 정산 로직 구현하면서 같이 할 것
}

@Transactional
public UserBet postUserBet(UUID userId, UserBetRequest userBetRequest) {
BetRound betRound = betRoundRepository.findById(userBetRequest.getRoundId())
.orElseThrow(() -> new CustomException(ErrorCode.BET_ROUND_NOT_FOUND));

if (userBetRepository.existsByRoundAndUserId(betRound, userId)) {
throw new CustomException(ErrorCode.BET_DUPLICATE);
}

LocalDateTime now = LocalDateTime.now();

if (now.isBefore(betRound.getOpenAt()) || now.isAfter(betRound.getLockAt())) {
throw new CustomException(ErrorCode.BET_TIME_INVALID);
}

int stake = 0;

if (!userBetRequest.isFree()) {
pointHistoryService.createPointHistory(
userId,
-userBetRequest.getStakePoints(),
PointReason.BETTING,
PointOrigin.BETTING,
userBetRequest.getRoundId()
);
stake = userBetRequest.getStakePoints();
}

UserBet userBet = UserBet.builder()
.round(betRound)
.userId(userId)
.option(userBetRequest.getOption())
.isFree(userBetRequest.isFree())
.stakePoints(stake)
.betStatus(BetStatus.ACTIVE)
.build();

return userBetRepository.save(userBet);
}

@Transactional
public void cancelUserBet(UUID userId, UUID userBetId) {
UserBet userBet = userBetRepository.findByUserBetIdAndUserId(userBetId, userId)
.orElseThrow(() -> new CustomException(ErrorCode.BET_NOT_FOUND));

BetRound betRound = userBet.getRound();

if (LocalDateTime.now().isAfter(betRound.getLockAt())){
throw new CustomException(ErrorCode.BET_ROUND_CLOSED);
}

if (!userBet.isFree() && userBet.getStakePoints() > 0) {
pointHistoryService.createPointHistory(
userId,
userBet.getStakePoints(),
PointReason.BETTING,
PointOrigin.BETTING,
userBet.getUserBetId()
);
}

userBetRepository.delete(userBet);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,12 @@ public enum ErrorCode {

// BETTING

STOCK_NOT_FOUND(HttpStatus.NOT_FOUND, "주식 종목이 존재하지 않습니다.");
STOCK_NOT_FOUND(HttpStatus.NOT_FOUND, "주식 종목이 존재하지 않습니다."),
BET_ROUND_NOT_FOUND(HttpStatus.NOT_FOUND, "존재하지 않는 라운드입니다."),
BET_DUPLICATE(HttpStatus.CONFLICT, "이미 이 라운드에 베팅했습니다."),
BET_TIME_INVALID(HttpStatus.CONFLICT, "베팅 가능 시간이 아닙니다."),
BET_NOT_FOUND(HttpStatus.NOT_FOUND, "해당 베팅을 찾을 수 없습니다."),
BET_ROUND_CLOSED(HttpStatus.CONFLICT, "이미 마감된 라운드입니다.");

private final HttpStatus status;
private final String message;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
import org.springframework.retry.annotation.Recover;
import org.springframework.retry.annotation.Retryable;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;

import java.util.UUID;
Expand Down Expand Up @@ -56,7 +57,7 @@ public PointHistoryResponse getPointHistoryListByUserId(UUID userId, PageRequest
}

// 포인트 증감 기록 생성 및 유저 포인트 업데이트
@Transactional
@Transactional(propagation = Propagation.REQUIRES_NEW)
@Retryable(
// 어떤 예외가 발생했을 때 재시도할지 지정합니다.
include = {OptimisticLockingFailureException.class},
Expand Down
Loading