Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: 쿠폰 보관함 조회 기능 구현 #149

Merged
merged 21 commits into from
Nov 26, 2023
Merged
Show file tree
Hide file tree
Changes from 19 commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
136eff2
style : Schedule 어노테이션 위치 변경
hongdosan Nov 21, 2023
bc837c1
refactor: 쿠폰 발행 기간 하루로 통일 및 쿠폰 정보 오픈 날짜 추가
hongdosan Nov 21, 2023
cebc707
feat: 쿠폰 발행 가능 날짜 중복 체크 기능 추가
hongdosan Nov 22, 2023
9df5c45
refactor: Builder 삭제
hongdosan Nov 22, 2023
a5bb2a1
test: 쿠폰 관련 테스트 수정
hongdosan Nov 22, 2023
ab522fd
feat: 쿠폰 발행 및 등록 기능 구현 및 테스트
hongdosan Nov 23, 2023
d91d962
feat: 쿠폰 발행 관련 레포지토리 기능 구현 및 테스트
hongdosan Nov 23, 2023
a20a115
test: 쿠폰 발행 관련 문자열 레디스 기능 구현 및 테스트
hongdosan Nov 23, 2023
9e3476b
feat: 쿠폰 발행 관련 ZSET 레디스 기능 구현 및 테스트
hongdosan Nov 23, 2023
e48c48e
test: 쿠폰 발행 컨트롤러 기능 테스트
hongdosan Nov 23, 2023
82bcd37
test: RestDoc 업데이트
hongdosan Nov 23, 2023
2b39b27
merge develop
hongdosan Nov 23, 2023
74b5f93
merge develop
hongdosan Nov 23, 2023
fb5e3d0
test: Github Actions 시, Redis ZSET 명령어 못찾는 테스트 Disable
hongdosan Nov 23, 2023
2e85aa0
refactor: 알림 및 쿠폰 테스트 코드 메서드명 변경 및 알림 콕 알림 키 변경
hongdosan Nov 23, 2023
2084a27
feat: 쿠폰함 조회 서비스 기능 구현 및 테스트
hongdosan Nov 23, 2023
90780d9
feat: 쿠폰 보관함 저장소 조회 기능 구현 및 테스트
hongdosan Nov 23, 2023
ba12cde
feat: 쿠폰 보관함 조회 기능 구현 및 테스트
hongdosan Nov 23, 2023
82e54be
fix: temporal 에러 해결
hongdosan Nov 23, 2023
b9b7889
merge develop
hongdosan Nov 26, 2023
37ed033
refactor: Stream 코드 리뷰 반영
hongdosan Nov 26, 2023
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
9 changes: 9 additions & 0 deletions src/docs/asciidoc/coupon.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,15 @@ include::{snippets}/coupons/http-response.adoc[]

사용자가 자신의 보관함에 있는 쿠폰들을 조회합니다.

==== 요청

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

[discrete]
==== 응답

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

---

=== 쿠폰 사용 (진행 중)
Expand Down
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 {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Q: 맨처음 PR에도 있지 않았나요?

Copy link
Member Author

@hongdosan hongdosan Nov 26, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

연속으로 PR을 올려서 이전 것도 보이는 걸거에요 ㅎㅎ


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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@

import com.moabam.api.domain.coupon.Coupon;
import com.moabam.api.domain.coupon.CouponType;
import com.moabam.api.domain.coupon.CouponWallet;
import com.moabam.api.dto.coupon.CouponResponse;
import com.moabam.api.dto.coupon.CreateCouponRequest;
import com.moabam.api.dto.coupon.MyCouponResponse;

import lombok.AccessLevel;
import lombok.NoArgsConstructor;
Expand All @@ -25,7 +27,7 @@ public static Coupon toEntity(Long adminId, CreateCouponRequest coupon) {
}

// TODO : Admin Table 생성 시, 관리자 명 추가할 예정
public static CouponResponse toDto(Coupon coupon) {
public static CouponResponse toResponse(Coupon coupon) {
return CouponResponse.builder()
.id(coupon.getId())
.adminName(coupon.getAdminId() + "admin")
Expand All @@ -38,4 +40,16 @@ public static CouponResponse toDto(Coupon coupon) {
.openAt(coupon.getOpenAt())
.build();
}

public static MyCouponResponse toMyResponse(CouponWallet couponWallet) {
Coupon coupon = couponWallet.getCoupon();

return MyCouponResponse.builder()
.id(coupon.getId())
.name(coupon.getName())
.description(coupon.getDescription())
.point(coupon.getPoint())
.type(coupon.getType())
.build();
}
}

This file was deleted.

43 changes: 26 additions & 17 deletions src/main/java/com/moabam/api/application/coupon/CouponService.java
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,15 @@
import org.springframework.transaction.annotation.Transactional;

import com.moabam.api.domain.coupon.Coupon;
import com.moabam.api.domain.coupon.CouponWallet;
import com.moabam.api.domain.coupon.repository.CouponRepository;
import com.moabam.api.domain.coupon.repository.CouponSearchRepository;
import com.moabam.api.domain.coupon.repository.CouponWalletSearchRepository;
import com.moabam.api.domain.member.Role;
import com.moabam.api.dto.coupon.CouponResponse;
import com.moabam.api.dto.coupon.CouponStatusRequest;
import com.moabam.api.dto.coupon.CreateCouponRequest;
import com.moabam.api.dto.coupon.MyCouponResponse;
import com.moabam.global.auth.model.AuthMember;
import com.moabam.global.common.util.ClockHolder;
import com.moabam.global.error.exception.BadRequestException;
Expand All @@ -28,15 +30,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,40 +54,38 @@ 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) {
Coupon coupon = couponRepository.findById(couponId)
.orElseThrow(() -> new NotFoundException(ErrorMessage.NOT_FOUND_COUPON));

return CouponMapper.toDto(coupon);
}

public Coupon getByWallet(Long couponWalletId, Long memberId) {
return couponWalletSearchRepository.findByIdAndMemberId(couponWalletId, memberId)
.orElseThrow(() -> new NotFoundException(ErrorMessage.NOT_FOUND_COUPON_WALLET))
.getCoupon();
return CouponMapper.toResponse(coupon);
}

public List<CouponResponse> getAllByStatus(CouponStatusRequest request) {
LocalDate now = LocalDate.from(clockHolder.times());
List<Coupon> coupons = couponSearchRepository.findAllByStatus(now, request);

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

public Coupon validatePeriod(String couponName) {
LocalDate now = LocalDate.from(clockHolder.times());
Coupon coupon = couponRepository.findByName(couponName)
.orElseThrow(() -> new NotFoundException(ErrorMessage.NOT_FOUND_COUPON));
public List<MyCouponResponse> getWallet(Long couponId, AuthMember authMember) {
List<CouponWallet> couponWallets =
couponWalletSearchRepository.findAllByCouponIdAndMemberId(couponId, authMember.id());

if (!now.equals(coupon.getStartAt())) {
throw new BadRequestException(ErrorMessage.INVALID_COUPON_PERIOD);
}
return couponWallets.stream()
.map(CouponMapper::toMyResponse)
.toList();
}

return coupon;
public Coupon getByWalletIdAndMemberId(Long couponWalletId, Long memberId) {
return couponWalletSearchRepository.findByIdAndMemberId(couponWalletId, memberId)
.orElseThrow(() -> new NotFoundException(ErrorMessage.NOT_FOUND_COUPON_WALLET))
.getCoupon();
}

private void validatePeriod(LocalDate startAt, LocalDate openAt) {
Expand All @@ -108,4 +111,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);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,6 @@ public class NotificationService {

private static final String KNOCK_BODY = "%s님이 콕 찔렀습니다.";
private static final String CERTIFY_TIME_BODY = "%s방 인증 시간입니다.";
private static final String KNOCK_KEY = "room_%s_member_%s_knocks_%s";

private final FcmService fcmService;
private final RoomService roomService;
Expand All @@ -41,13 +40,11 @@ public class NotificationService {
@Transactional
public void sendKnock(AuthMember member, Long targetId, Long roomId) {
roomService.validateRoomById(roomId);

String knockKey = generateKnockKey(member.id(), targetId, roomId);
validateConflictKnock(knockKey);
validateConflictKnock(member.id(), targetId, roomId);

String fcmToken = fcmService.findTokenByMemberId(targetId);
fcmService.sendAsync(fcmToken, String.format(KNOCK_BODY, member.nickname()));
notificationRepository.saveKnock(knockKey);
notificationRepository.saveKnock(member.id(), targetId, roomId);
}

@Scheduled(cron = "0 50 * * * *")
Expand All @@ -68,8 +65,8 @@ public List<Long> getMyKnockStatusInRoom(Long memberId, Long roomId, List<Partic
.filter(participant -> !participant.getMemberId().equals(memberId))
.toList();

Predicate<Long> knockPredicate = targetId
-> notificationRepository.existsKnockByKey(generateKnockKey(memberId, targetId, roomId));
Predicate<Long> knockPredicate = targetId ->
notificationRepository.existsKnockByKey(memberId, targetId, roomId);

Map<Boolean, List<Long>> knockStatus = filteredParticipants.stream()
.map(Participant::getMemberId)
Expand All @@ -78,13 +75,9 @@ public List<Long> getMyKnockStatusInRoom(Long memberId, Long roomId, List<Partic
return knockStatus.get(true);
}

private void validateConflictKnock(String knockKey) {
if (notificationRepository.existsKnockByKey(knockKey)) {
private void validateConflictKnock(Long memberId, Long targetId, Long roomId) {
if (notificationRepository.existsKnockByKey(memberId, targetId, roomId)) {
throw new ConflictException(ErrorMessage.CONFLICT_KNOCK);
}
}

private String generateKnockKey(Long memberId, Long targetId, Long roomId) {
return String.format(KNOCK_KEY, roomId, memberId, targetId);
}
}
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);
}
}
Loading