Skip to content

Commit

Permalink
feat: 쿠폰 발급 요청 및 대기열 사용자 쿠폰 발급 처리 구현 (#146)
Browse files Browse the repository at this point in the history
* style : Schedule 어노테이션 위치 변경

* refactor: 쿠폰 발행 기간 하루로 통일 및 쿠폰 정보 오픈 날짜 추가

* feat: 쿠폰 발행 가능 날짜 중복 체크 기능 추가

* refactor: Builder 삭제

* test: 쿠폰 관련 테스트 수정

* feat: 쿠폰 발행 관련 레포지토리 기능 구현 및 테스트

* test: 쿠폰 발행 관련 문자열 레디스 기능 구현 및 테스트

* feat: 쿠폰 발행 관련 ZSET 레디스 기능 구현 및 테스트

* test: 쿠폰 발행 컨트롤러 기능 테스트

* test: RestDoc 업데이트

* test: Github Actions 시, Redis ZSET 명령어 못찾는 테스트 Disable
  • Loading branch information
hongdosan authored Nov 26, 2023
1 parent 4b1c2bb commit 0756c4d
Show file tree
Hide file tree
Showing 29 changed files with 797 additions and 395 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ public PurchaseProductResponse purchaseBugProduct(Long memberId, Long productId,
Payment payment = PaymentMapper.toPayment(memberId, product);

if (!isNull(request.couponWalletId())) {
Coupon coupon = couponService.getByWallet(request.couponWalletId(), memberId);
Coupon coupon = couponService.getByWalletIdAndMemberId(request.couponWalletId(), memberId);
payment.applyCoupon(coupon, request.couponWalletId());
}
paymentRepository.save(payment);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
package com.moabam.api.application.coupon;

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

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

import com.moabam.api.domain.coupon.Coupon;
import com.moabam.api.domain.coupon.CouponWallet;
import com.moabam.api.domain.coupon.repository.CouponManageRepository;
import com.moabam.api.domain.coupon.repository.CouponRepository;
import com.moabam.api.domain.coupon.repository.CouponWalletRepository;
import com.moabam.global.auth.model.AuthMember;
import com.moabam.global.common.util.ClockHolder;
import com.moabam.global.error.exception.BadRequestException;
import com.moabam.global.error.model.ErrorMessage;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;

@Slf4j
@Service
@RequiredArgsConstructor
public class CouponManageService {

private static final long ISSUE_SIZE = 10;

private final ClockHolder clockHolder;

private final CouponRepository couponRepository;
private final CouponManageRepository couponManageRepository;
private final CouponWalletRepository couponWalletRepository;

@Scheduled(fixedDelay = 1000)
public void issue() {
LocalDate now = LocalDate.from(clockHolder.times());
Optional<Coupon> isCoupon = couponRepository.findByStartAt(now);

if (!canIssue(isCoupon)) {
return;
}

Coupon coupon = isCoupon.get();
Set<Long> membersId = couponManageRepository.popMinQueue(coupon.getName(), ISSUE_SIZE);

membersId.forEach(memberId -> {
int nextStock = couponManageRepository.increaseIssuedStock(coupon.getName());

if (coupon.getStock() < nextStock) {
return;
}

CouponWallet couponWallet = CouponWallet.create(memberId, coupon);
couponWalletRepository.save(couponWallet);
});
}

public void register(AuthMember authMember, String couponName) {
double registerTime = System.currentTimeMillis();
validateRegister(couponName);
couponManageRepository.addIfAbsentQueue(couponName, authMember.id(), registerTime);
}

public void deleteCouponManage(String couponName) {
couponManageRepository.deleteQueue(couponName);
couponManageRepository.deleteIssuedStock(couponName);
}

private void validateRegister(String couponName) {
LocalDate now = LocalDate.from(clockHolder.times());
Optional<Coupon> coupon = couponRepository.findByStartAt(now);

if (coupon.isEmpty() || !coupon.get().getName().equals(couponName)) {
throw new BadRequestException(ErrorMessage.INVALID_COUPON_PERIOD);
}
}

private boolean canIssue(Optional<Coupon> coupon) {
if (coupon.isEmpty()) {
return false;
}

Coupon currentCoupon = coupon.get();
int currentStock = couponManageRepository.getIssuedStock(currentCoupon.getName());
int maxStock = currentCoupon.getStock();

return currentStock < maxStock;
}
}

This file was deleted.

26 changes: 12 additions & 14 deletions src/main/java/com/moabam/api/application/coupon/CouponService.java
Original file line number Diff line number Diff line change
Expand Up @@ -28,15 +28,18 @@
@Transactional(readOnly = true)
public class CouponService {

private final ClockHolder clockHolder;
private final CouponManageService couponManageService;

private final CouponRepository couponRepository;
private final CouponSearchRepository couponSearchRepository;
private final CouponWalletSearchRepository couponWalletSearchRepository;
private final ClockHolder clockHolder;

@Transactional
public void create(AuthMember admin, CreateCouponRequest request) {
validateAdminRole(admin);
validateConflictName(request.name());
validateConflictStartAt(request.startAt());
validatePeriod(request.startAt(), request.openAt());

Coupon coupon = CouponMapper.toEntity(admin.id(), request);
Expand All @@ -49,6 +52,7 @@ public void delete(AuthMember admin, Long couponId) {
Coupon coupon = couponRepository.findById(couponId)
.orElseThrow(() -> new NotFoundException(ErrorMessage.NOT_FOUND_COUPON));
couponRepository.delete(coupon);
couponManageService.deleteCouponManage(coupon.getName());
}

public CouponResponse getById(Long couponId) {
Expand All @@ -58,7 +62,7 @@ public CouponResponse getById(Long couponId) {
return CouponMapper.toDto(coupon);
}

public Coupon getByWallet(Long couponWalletId, Long memberId) {
public Coupon getByWalletIdAndMemberId(Long couponWalletId, Long memberId) {
return couponWalletSearchRepository.findByIdAndMemberId(couponWalletId, memberId)
.orElseThrow(() -> new NotFoundException(ErrorMessage.NOT_FOUND_COUPON_WALLET))
.getCoupon();
Expand All @@ -73,18 +77,6 @@ public List<CouponResponse> getAllByStatus(CouponStatusRequest request) {
.toList();
}

public Coupon validatePeriod(String couponName) {
LocalDate now = LocalDate.from(clockHolder.times());
Coupon coupon = couponRepository.findByName(couponName)
.orElseThrow(() -> new NotFoundException(ErrorMessage.NOT_FOUND_COUPON));

if (!now.equals(coupon.getStartAt())) {
throw new BadRequestException(ErrorMessage.INVALID_COUPON_PERIOD);
}

return coupon;
}

private void validatePeriod(LocalDate startAt, LocalDate openAt) {
LocalDate now = LocalDate.from(clockHolder.times());

Expand All @@ -108,4 +100,10 @@ private void validateConflictName(String couponName) {
throw new ConflictException(ErrorMessage.CONFLICT_COUPON_NAME);
}
}

private void validateConflictStartAt(LocalDate startAt) {
if (couponRepository.existsByStartAt(startAt)) {
throw new ConflictException(ErrorMessage.CONFLICT_COUPON_START_AT);
}
}
}
2 changes: 1 addition & 1 deletion src/main/java/com/moabam/api/domain/coupon/Coupon.java
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ public class Coupon extends BaseTimeEntity {
@Column(name = "stock", nullable = false)
private int stock;

@Column(name = "start_at", nullable = false)
@Column(name = "start_at", unique = true, nullable = false)
private LocalDate startAt;

@Column(name = "open_at", nullable = false)
Expand Down
6 changes: 4 additions & 2 deletions src/main/java/com/moabam/api/domain/coupon/CouponWallet.java
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@
import jakarta.persistence.ManyToOne;
import jakarta.persistence.Table;
import lombok.AccessLevel;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

Expand All @@ -34,9 +33,12 @@ public class CouponWallet extends BaseTimeEntity {
@ManyToOne(fetch = FetchType.LAZY)
private Coupon coupon;

@Builder
private CouponWallet(Long memberId, Coupon coupon) {
this.memberId = memberId;
this.coupon = coupon;
}

public static CouponWallet create(Long memberId, Coupon coupon) {
return new CouponWallet(memberId, coupon);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
package com.moabam.api.domain.coupon.repository;

import static java.util.Objects.*;

import java.util.Set;
import java.util.stream.Collectors;

import org.springframework.stereotype.Repository;

import com.moabam.api.infrastructure.redis.StringRedisRepository;
import com.moabam.api.infrastructure.redis.ZSetRedisRepository;

import lombok.RequiredArgsConstructor;

@Repository
@RequiredArgsConstructor
public class CouponManageRepository {

private static final String STOCK_KEY = "%s_INCR";

private final ZSetRedisRepository zSetRedisRepository;
private final StringRedisRepository stringRedisRepository;

public void addIfAbsentQueue(String couponName, Long memberId, double registerTime) {
zSetRedisRepository.addIfAbsent(requireNonNull(couponName), requireNonNull(memberId), registerTime);
}

public Set<Long> popMinQueue(String couponName, long count) {
return zSetRedisRepository
.popMin(requireNonNull(couponName), count)
.stream()
.map(tuple -> (Long)tuple.getValue())
.collect(Collectors.toSet());
}

public void deleteQueue(String couponName) {
stringRedisRepository.delete(requireNonNull(couponName));
}

public int increaseIssuedStock(String couponName) {
String stockKey = String.format(STOCK_KEY, requireNonNull(couponName));

return stringRedisRepository
.increment(requireNonNull(stockKey))
.intValue();
}

public int getIssuedStock(String couponName) {
String stockKey = String.format(STOCK_KEY, requireNonNull(couponName));
String stockValue = stringRedisRepository.get(requireNonNull(stockKey));

if (stockValue == null) {
return 0;
}

return Integer.parseInt(stockValue);
}

public void deleteIssuedStock(String couponName) {
String stockKey = String.format(STOCK_KEY, requireNonNull(couponName));
stringRedisRepository.delete(requireNonNull(stockKey));
}
}

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.moabam.api.domain.coupon.repository;

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

import org.springframework.data.jpa.repository.JpaRepository;
Expand All @@ -8,7 +9,9 @@

public interface CouponRepository extends JpaRepository<Coupon, Long> {

Optional<Coupon> findByName(String couponName);

boolean existsByName(String name);

boolean existsByStartAt(LocalDate startAt);

Optional<Coupon> findByStartAt(LocalDate startAt);
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,10 @@ public void save(String key, String value, Duration timeout) {
.set(key, value, timeout);
}

public void delete(String key) {
redisTemplate.delete(key);
public Long increment(String key) {
return redisTemplate
.opsForValue()
.increment(key);
}

public String get(String key) {
Expand All @@ -32,4 +34,8 @@ public String get(String key) {
public Boolean hasKey(String key) {
return redisTemplate.hasKey(key);
}

public void delete(String key) {
redisTemplate.delete(key);
}
}
Loading

0 comments on commit 0756c4d

Please sign in to comment.