Skip to content

Commit

Permalink
feat: 쿠폰 발급 요청 기능 구현 (#114)
Browse files Browse the repository at this point in the history
* refactor: 쿠폰 및 토큰 패키지 및 클래스명 변경

* refactor: 알림 패키지 및 클래스명 변경, Fcm 로직 분리

* feat: 쿠폰 발급 요청 기능 구현

* test: 쿠폰 발급 요청 기능 테스트

* test: Syntax 에러로 쿠폰 발급 관련 테스트 임시 Disabled 처리

* fix: Redis Yaml 추가 설정

* test: 중복 저장에 대한 테스트 코드 추가

* refactor: SystemClockHolder -> ClockHolder 변경
  • Loading branch information
hongdosan authored Nov 20, 2023
1 parent bd73795 commit 5b7b46a
Show file tree
Hide file tree
Showing 31 changed files with 578 additions and 100 deletions.
17 changes: 13 additions & 4 deletions src/docs/asciidoc/coupon.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -66,15 +66,24 @@ include::{snippets}/coupons/search/http-response.adoc[]

---

=== 특정 사용자의 쿠폰 보관함을 조회
=== 특정 쿠폰에 대해 발급

사용자가 자신의 보관함에 있는 쿠폰들을 조회합니다.
사용자가 발급 가능한 쿠폰을 선착순으로 발급 받습니다.

==== 요청

include::{snippets}/coupons/http-request.adoc[]

[discrete]
==== 응답

include::{snippets}/coupons/http-response.adoc[]

---

=== 쿠폰 발급 (진행 중)
=== 특정 사용자의 쿠폰 보관함을 조회

사용자가 발급 가능한 쿠폰을 선착순으로 발급 받습니다.
사용자가 자신의 보관함에 있는 쿠폰들을 조회합니다.

---

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ public static Coupon toEntity(Long adminId, CreateCouponRequest request) {
return Coupon.builder()
.name(request.name())
.description(request.description())
.couponType(CouponType.from(request.couponType()))
.type(CouponType.from(request.couponType()))
.point(request.point())
.stock(request.stock())
.startAt(request.startAt())
Expand All @@ -33,7 +33,7 @@ public static CouponResponse toDto(Coupon coupon) {
.description(coupon.getDescription())
.point(coupon.getPoint())
.stock(coupon.getStock())
.couponType(coupon.getCouponType())
.couponType(coupon.getType())
.startAt(coupon.getStartAt())
.endAt(coupon.getEndAt())
.build();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package com.moabam.api.application.coupon;

import org.springframework.stereotype.Service;

import com.moabam.api.domain.coupon.Coupon;
import com.moabam.api.domain.coupon.repository.CouponQueueRepository;
import com.moabam.global.auth.model.AuthorizationMember;

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

@Slf4j
@Service
@RequiredArgsConstructor
public class CouponQueueService {

private final CouponService couponService;
private final CouponQueueRepository couponQueueRepository;

public void register(AuthorizationMember member, String couponName) {
double registerTime = System.currentTimeMillis();

if (canRegister(couponName)) {
log.info("{} 쿠폰이 모두 발급되었습니다.", couponName);
return;
}

couponQueueRepository.addQueue(couponName, member.nickname(), registerTime);
}

private boolean canRegister(String couponName) {
Coupon coupon = couponService.validateCouponPeriod(couponName);

return coupon.getStock() <= couponQueueRepository.queueSize(coupon.getName());
}
}
30 changes: 22 additions & 8 deletions src/main/java/com/moabam/api/application/coupon/CouponService.java
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
import com.moabam.api.dto.coupon.CouponSearchRequest;
import com.moabam.api.dto.coupon.CreateCouponRequest;
import com.moabam.global.auth.model.AuthorizationMember;
import com.moabam.global.common.util.ClockHolder;
import com.moabam.global.error.exception.BadRequestException;
import com.moabam.global.error.exception.ConflictException;
import com.moabam.global.error.exception.NotFoundException;
Expand All @@ -28,6 +29,7 @@ public class CouponService {

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

@Transactional
public void createCoupon(AuthorizationMember admin, CreateCouponRequest request) {
Expand All @@ -48,21 +50,39 @@ public void deleteCoupon(AuthorizationMember admin, Long couponId) {
}

public CouponResponse getCouponById(Long couponId) {
Coupon coupon = couponSearchRepository.findById(couponId)
Coupon coupon = couponRepository.findById(couponId)
.orElseThrow(() -> new NotFoundException(ErrorMessage.NOT_FOUND_COUPON));

return CouponMapper.toDto(coupon);
}

public List<CouponResponse> getCoupons(CouponSearchRequest request) {
LocalDateTime now = LocalDateTime.now();
LocalDateTime now = clockHolder.times();
List<Coupon> coupons = couponSearchRepository.findAllByStatus(now, request);

return coupons.stream()
.map(CouponMapper::toDto)
.toList();
}

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

if (!now.isBefore(coupon.getStartAt()) && !now.isAfter(coupon.getEndAt())) {
return coupon;
}

throw new BadRequestException(ErrorMessage.INVALID_COUPON_PERIOD_END);
}

private void validateCouponPeriod(LocalDateTime startAt, LocalDateTime endAt) {
if (startAt.isAfter(endAt)) {
throw new BadRequestException(ErrorMessage.INVALID_COUPON_PERIOD);
}
}

private void validateAdminRole(AuthorizationMember admin) {
if (!admin.role().equals(Role.ADMIN)) {
throw new NotFoundException(ErrorMessage.MEMBER_NOT_FOUND);
Expand All @@ -74,10 +94,4 @@ private void validateConflictCouponName(String name) {
throw new ConflictException(ErrorMessage.CONFLICT_COUPON_NAME);
}
}

private void validateCouponPeriod(LocalDateTime startAt, LocalDateTime endAt) {
if (startAt.isAfter(endAt)) {
throw new BadRequestException(ErrorMessage.INVALID_COUPON_PERIOD);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@

import static com.moabam.global.common.util.GlobalConstant.*;

import java.time.LocalDateTime;
import java.util.List;
import java.util.Map;
import java.util.function.Predicate;
Expand All @@ -19,6 +18,7 @@
import com.moabam.api.dto.notification.KnockNotificationStatusResponse;
import com.moabam.api.infrastructure.fcm.FcmService;
import com.moabam.global.auth.model.AuthorizationMember;
import com.moabam.global.common.util.ClockHolder;
import com.moabam.global.error.exception.ConflictException;
import com.moabam.global.error.exception.NotFoundException;
import com.moabam.global.error.model.ErrorMessage;
Expand All @@ -38,6 +38,7 @@ public class NotificationService {
private final RoomService roomService;
private final NotificationRepository notificationRepository;
private final ParticipantSearchRepository participantSearchRepository;
private final ClockHolder clockHolder;

@Transactional
public void sendKnockNotification(AuthorizationMember member, Long targetId, Long roomId) {
Expand All @@ -55,7 +56,7 @@ public void sendKnockNotification(AuthorizationMember member, Long targetId, Lon

@Scheduled(cron = "0 50 * * * *")
public void sendCertificationTimeNotification() {
int certificationTime = (LocalDateTime.now().getHour() + ONE_HOUR) % HOURS_IN_A_DAY;
int certificationTime = (clockHolder.times().getHour() + ONE_HOUR) % HOURS_IN_A_DAY;
List<Participant> participants = participantSearchRepository.findAllByRoomCertifyTime(certificationTime);

participants.parallelStream().forEach(participant -> {
Expand Down
8 changes: 4 additions & 4 deletions src/main/java/com/moabam/api/domain/coupon/Coupon.java
Original file line number Diff line number Diff line change
Expand Up @@ -48,8 +48,8 @@ public class Coupon extends BaseTimeEntity {
private String description;

@Enumerated(value = EnumType.STRING)
@Column(name = "coupon_type", nullable = false)
private CouponType couponType;
@Column(name = "type", nullable = false)
private CouponType type;

@ColumnDefault("1")
@Column(name = "stock", nullable = false)
Expand All @@ -66,12 +66,12 @@ public class Coupon extends BaseTimeEntity {
private Long adminId;

@Builder
private Coupon(String name, int point, String description, CouponType couponType, int stock, LocalDateTime startAt,
private Coupon(String name, int point, String description, CouponType type, int stock, LocalDateTime startAt,
LocalDateTime endAt, Long adminId) {
this.name = requireNonNull(name);
this.point = validatePoint(point);
this.description = Optional.ofNullable(description).orElse(BLANK);
this.couponType = requireNonNull(couponType);
this.type = requireNonNull(type);
this.stock = validateStock(stock);
this.startAt = requireNonNull(startAt);
this.endAt = requireNonNull(endAt);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package com.moabam.api.domain.coupon.repository;

import static java.util.Objects.*;

import org.springframework.stereotype.Repository;

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

import lombok.RequiredArgsConstructor;

@Repository
@RequiredArgsConstructor
public class CouponQueueRepository {

private final ZSetRedisRepository zSetRedisRepository;

public void addQueue(String couponName, String memberNickname, double score) {
zSetRedisRepository.addIfAbsent(requireNonNull(couponName), requireNonNull(memberNickname), score);
}

public Long queueSize(String couponName) {
return zSetRedisRepository.size(requireNonNull(couponName));
}
}
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
package com.moabam.api.domain.coupon.repository;

import java.util.Optional;

import org.springframework.data.jpa.repository.JpaRepository;

import com.moabam.api.domain.coupon.Coupon;

public interface CouponRepository extends JpaRepository<Coupon, Long> {

Optional<Coupon> findByName(String couponName);

boolean existsByName(String name);
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@

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

import org.springframework.stereotype.Repository;

Expand All @@ -22,14 +21,6 @@ public class CouponSearchRepository {

private final JPAQueryFactory jpaQueryFactory;

public Optional<Coupon> findById(Long couponId) {
return Optional.ofNullable(
jpaQueryFactory.selectFrom(coupon)
.where(coupon.id.eq(couponId))
.fetchOne()
);
}

public List<Coupon> findAllByStatus(LocalDateTime now, CouponSearchRequest request) {
return jpaQueryFactory.selectFrom(coupon)
.where(filterCouponStatus(now, request))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
@RequiredArgsConstructor
public class StringRedisRepository {

private final RedisTemplate<String, String> redisTemplate;
private final RedisTemplate<String, Object> redisTemplate;

public void save(String key, String value, Duration timeout) {
redisTemplate
Expand All @@ -24,7 +24,7 @@ public void delete(String key) {
}

public String get(String key) {
return redisTemplate
return (String)redisTemplate
.opsForValue()
.get(key);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package com.moabam.api.infrastructure.redis;

import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Repository;

import lombok.RequiredArgsConstructor;

@Repository
@RequiredArgsConstructor
public class ZSetRedisRepository {

private final RedisTemplate<String, Object> redisTemplate;

public void addIfAbsent(String key, String value, double score) {
if (redisTemplate.opsForZSet().score(key, value) == null) {
redisTemplate
.opsForZSet()
.add(key, value, score);
}
}

public Long size(String key) {
return redisTemplate
.opsForZSet()
.size(key);
}

public Boolean hasKey(String key) {
return redisTemplate.hasKey(key);
}

public void delete(String key) {
redisTemplate.delete(key);
}
}
13 changes: 11 additions & 2 deletions src/main/java/com/moabam/api/presentation/CouponController.java
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,11 @@
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestController;

import com.moabam.api.application.coupon.CouponQueueService;
import com.moabam.api.application.coupon.CouponService;
import com.moabam.api.dto.coupon.CouponResponse;
import com.moabam.api.dto.coupon.CouponSearchRequest;
Expand All @@ -26,6 +28,7 @@
public class CouponController {

private final CouponService couponService;
private final CouponQueueService couponQueueService;

@PostMapping("/admins/coupons")
@ResponseStatus(HttpStatus.CREATED)
Expand All @@ -36,13 +39,13 @@ public void createCoupon(@CurrentMember AuthorizationMember admin,

@DeleteMapping("/admins/coupons/{couponId}")
@ResponseStatus(HttpStatus.OK)
public void deleteCoupon(@CurrentMember AuthorizationMember admin, @PathVariable Long couponId) {
public void deleteCoupon(@CurrentMember AuthorizationMember admin, @PathVariable("couponId") Long couponId) {
couponService.deleteCoupon(admin, couponId);
}

@GetMapping("/coupons/{couponId}")
@ResponseStatus(HttpStatus.OK)
public CouponResponse getCouponById(@PathVariable Long couponId) {
public CouponResponse getCouponById(@PathVariable("couponId") Long couponId) {
return couponService.getCouponById(couponId);
}

Expand All @@ -51,4 +54,10 @@ public CouponResponse getCouponById(@PathVariable Long couponId) {
public List<CouponResponse> getCoupons(@Valid @RequestBody CouponSearchRequest request) {
return couponService.getCoupons(request);
}

@PostMapping("/coupons")
public void registerCouponQueue(@CurrentMember AuthorizationMember member,
@RequestParam("couponName") String couponName) {
couponQueueService.register(member, couponName);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,8 @@ public class NotificationController {
private final NotificationService notificationService;

@GetMapping("/rooms/{roomId}/members/{memberId}")
public void sendKnockNotification(@CurrentMember AuthorizationMember member, @PathVariable Long roomId,
@PathVariable Long memberId) {
public void sendKnockNotification(@CurrentMember AuthorizationMember member, @PathVariable("roomId") Long roomId,
@PathVariable("memberId") Long memberId) {
notificationService.sendKnockNotification(member, memberId, roomId);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,7 @@ public class GlobalConstant {
public static final String SPACE = " ";
public static final int ONE_HOUR = 1;
public static final int HOURS_IN_A_DAY = 24;
public static final String KNOCK_KEY = "room_%s_member_%s_knocks_%s";
public static final String FIREBASE_PATH = "config/moabam-firebase.json";

public static final int ROOM_FIXED_SEARCH_SIZE = 10;
public static final int LEVEL_DIVISOR = 10;
}
Loading

0 comments on commit 5b7b46a

Please sign in to comment.