Skip to content

Commit 0160cd8

Browse files
authored
feat: 스터디 수료 시 회비 할인 쿠폰 발급 기능 구현 (#843)
* feat: 스터디 수료 이벤트 추가 * feat: 스터디 수료 철회 이벤트 추가 * feat: 스터디 수료 관련 이벤트 발행을 위한 명시적 save 호출 * docs: 오타 수정 * feat: 스터디 단일 수료 이벤트 제거 * feat: 스터디 다건 수료 이벤트 추가 * feat: 쿠폰 이름 생성 유틸리티 구현 * test: 쿠폰 이름 생성 유틸리티 테스트 추가 * feat: 쿠폰 이벤트 핸들러 추가 * feat: 스터디 수료 쿠폰 발급 로직 구현 * docs: 투두 추가 * fix: setId 테스트 로직 추가 * refactor: void 리턴하도록 변경
1 parent a12c395 commit 0160cd8

File tree

10 files changed

+137
-0
lines changed

10 files changed

+137
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
package com.gdschongik.gdsc.domain.coupon.application;
2+
3+
import com.gdschongik.gdsc.domain.study.domain.StudyHistoriesCompletedEvent;
4+
import lombok.RequiredArgsConstructor;
5+
import lombok.extern.slf4j.Slf4j;
6+
import org.springframework.stereotype.Component;
7+
import org.springframework.transaction.event.TransactionPhase;
8+
import org.springframework.transaction.event.TransactionalEventListener;
9+
10+
@Slf4j
11+
@Component
12+
@RequiredArgsConstructor
13+
public class CouponEventHandler {
14+
// TODO: 여기서는 쿠폰 외 도메인의 이벤트를 받아서 쿠폰 서비스를 호출. 다른 핸들러는 반대로 되어있으므로 수정 필요
15+
16+
private final CouponService couponService;
17+
18+
@TransactionalEventListener(phase = TransactionPhase.BEFORE_COMMIT)
19+
public void handleStudyHistoryCompletedEvent(StudyHistoriesCompletedEvent event) {
20+
log.info("[CouponEventHandler] 스터디 수료 이벤트 수신: studyHistoryIds={}", event.studyHistoryIds());
21+
couponService.createAndIssueCouponByStudyHistories(event.studyHistoryIds());
22+
}
23+
}

src/main/java/com/gdschongik/gdsc/domain/coupon/application/CouponService.java

+29
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,11 @@
1212
import com.gdschongik.gdsc.domain.coupon.dto.request.IssuedCouponQueryOption;
1313
import com.gdschongik.gdsc.domain.coupon.dto.response.CouponResponse;
1414
import com.gdschongik.gdsc.domain.coupon.dto.response.IssuedCouponResponse;
15+
import com.gdschongik.gdsc.domain.coupon.util.CouponNameUtil;
1516
import com.gdschongik.gdsc.domain.member.dao.MemberRepository;
1617
import com.gdschongik.gdsc.domain.member.domain.Member;
18+
import com.gdschongik.gdsc.domain.study.dao.StudyHistoryRepository;
19+
import com.gdschongik.gdsc.domain.study.domain.StudyHistory;
1720
import com.gdschongik.gdsc.global.exception.CustomException;
1821
import com.gdschongik.gdsc.global.util.MemberUtil;
1922
import java.util.List;
@@ -30,7 +33,9 @@
3033
@Transactional(readOnly = true)
3134
public class CouponService {
3235

36+
private final CouponNameUtil couponNameUtil;
3337
private final MemberUtil memberUtil;
38+
private final StudyHistoryRepository studyHistoryRepository;
3439
private final CouponRepository couponRepository;
3540
private final IssuedCouponRepository issuedCouponRepository;
3641
private final MemberRepository memberRepository;
@@ -87,4 +92,28 @@ public List<IssuedCouponResponse> findMyUsableIssuedCoupons() {
8792
.map(IssuedCouponResponse::from)
8893
.toList();
8994
}
95+
96+
@Transactional
97+
public void createAndIssueCouponByStudyHistories(List<Long> studyHistoryIds) {
98+
List<StudyHistory> studyHistories = studyHistoryRepository.findAllById(studyHistoryIds);
99+
List<Long> studentIds = studyHistories.stream()
100+
.map(studyHistory -> studyHistory.getStudent().getId())
101+
.toList();
102+
List<Member> students = memberRepository.findAllById(studentIds);
103+
104+
String couponName = couponNameUtil.generateStudyCompletionCouponName(
105+
studyHistories.get(0).getStudy());
106+
// TODO: 요청할 때마다 새로운 쿠폰 생성되는 문제 수정: 스터디마다 하나의 쿠폰만 존재하도록 쿠폰 타입 및 참조 식별자 추가
107+
Coupon coupon = Coupon.create(couponName, Money.from(5000L));
108+
couponRepository.save(coupon);
109+
110+
List<IssuedCoupon> issuedCoupons = students.stream()
111+
.map(student -> IssuedCoupon.create(coupon, student))
112+
.toList();
113+
issuedCouponRepository.saveAll(issuedCoupons);
114+
115+
log.info(
116+
"[CouponService] 스터디 수료 쿠폰 발급: issuedCouponIds={}",
117+
issuedCoupons.stream().map(IssuedCoupon::getId).toList());
118+
}
90119
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
package com.gdschongik.gdsc.domain.coupon.util;
2+
3+
import com.gdschongik.gdsc.domain.study.domain.Study;
4+
import com.gdschongik.gdsc.global.util.formatter.SemesterFormatter;
5+
import lombok.RequiredArgsConstructor;
6+
import org.springframework.stereotype.Component;
7+
8+
@Component
9+
@RequiredArgsConstructor
10+
public class CouponNameUtil {
11+
12+
public String generateStudyCompletionCouponName(Study study) {
13+
String academicYearAndSemesterName = SemesterFormatter.format(study);
14+
return academicYearAndSemesterName + " " + study.getTitle() + " 수료 쿠폰";
15+
}
16+
}

src/main/java/com/gdschongik/gdsc/domain/study/application/MentorStudyHistoryService.java

+8
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import com.gdschongik.gdsc.domain.study.dao.StudyHistoryRepository;
77
import com.gdschongik.gdsc.domain.study.dao.StudyRepository;
88
import com.gdschongik.gdsc.domain.study.domain.Study;
9+
import com.gdschongik.gdsc.domain.study.domain.StudyHistoriesCompletedEvent;
910
import com.gdschongik.gdsc.domain.study.domain.StudyHistory;
1011
import com.gdschongik.gdsc.domain.study.domain.StudyHistoryValidator;
1112
import com.gdschongik.gdsc.domain.study.domain.StudyValidator;
@@ -15,6 +16,7 @@
1516
import java.util.List;
1617
import lombok.RequiredArgsConstructor;
1718
import lombok.extern.slf4j.Slf4j;
19+
import org.springframework.context.ApplicationEventPublisher;
1820
import org.springframework.stereotype.Service;
1921
import org.springframework.transaction.annotation.Transactional;
2022

@@ -23,6 +25,7 @@
2325
@RequiredArgsConstructor
2426
public class MentorStudyHistoryService {
2527

28+
private final ApplicationEventPublisher applicationEventPublisher;
2629
private final MemberUtil memberUtil;
2730
private final StudyValidator studyValidator;
2831
private final StudyHistoryValidator studyHistoryValidator;
@@ -43,6 +46,9 @@ public void completeStudy(StudyCompleteRequest request) {
4346

4447
studyHistories.forEach(StudyHistory::complete);
4548

49+
applicationEventPublisher.publishEvent(new StudyHistoriesCompletedEvent(
50+
studyHistories.stream().map(StudyHistory::getId).toList()));
51+
4652
log.info(
4753
"[MentorStudyHistoryService] 스터디 수료 처리: studyId={}, studentIds={}",
4854
request.studyId(),
@@ -63,6 +69,8 @@ public void withdrawStudyCompletion(StudyCompleteRequest request) {
6369

6470
studyHistories.forEach(StudyHistory::withdrawCompletion);
6571

72+
studyHistoryRepository.saveAll(studyHistories);
73+
6674
log.info(
6775
"[MentorStudyHistoryService] 스터디 수료 철회: studyId={}, studentIds={}",
6876
request.studyId(),
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
package com.gdschongik.gdsc.domain.study.domain;
2+
3+
import java.util.List;
4+
import lombok.NonNull;
5+
6+
public record StudyHistoriesCompletedEvent(@NonNull List<Long> studyHistoryIds) {}

src/main/java/com/gdschongik/gdsc/domain/study/domain/StudyHistory.java

+1
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@ public void complete() {
8484
*/
8585
public void withdrawCompletion() {
8686
studyHistoryStatus = NONE;
87+
registerEvent(new StudyHistoryCompletionWithdrawnEvent(this.id));
8788
}
8889

8990
// 데이터 전달 로직
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
package com.gdschongik.gdsc.domain.study.domain;
2+
3+
public record StudyHistoryCompletionWithdrawnEvent(long studyHistoryId) {
4+
// TODO: 이벤트 내부 필드의 식별자 값은 항상 not null이므로, primitive 타입으로 사용하도록 변경
5+
// TODO: 스터디 철회 시 기존에 발행된 쿠폰 회수하는 로직 구현
6+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
package com.gdschongik.gdsc.domain.coupon.util;
2+
3+
import static com.gdschongik.gdsc.global.common.constant.RecruitmentConstant.*;
4+
import static com.gdschongik.gdsc.global.common.constant.StudyConstant.*;
5+
import static org.assertj.core.api.Assertions.*;
6+
7+
import com.gdschongik.gdsc.domain.common.model.SemesterType;
8+
import com.gdschongik.gdsc.domain.common.vo.Period;
9+
import com.gdschongik.gdsc.domain.member.domain.Member;
10+
import com.gdschongik.gdsc.domain.study.domain.Study;
11+
import com.gdschongik.gdsc.helper.FixtureHelper;
12+
import org.junit.jupiter.api.Test;
13+
14+
class CouponNameUtilTest {
15+
16+
CouponNameUtil couponNameUtil = new CouponNameUtil();
17+
FixtureHelper fixtureHelper = new FixtureHelper();
18+
19+
@Test
20+
void 스터디_수료_쿠폰_이름이_생성된다() {
21+
// given
22+
Member mentor = fixtureHelper.createMentor(1L);
23+
Study study = Study.create(
24+
2025,
25+
SemesterType.FIRST,
26+
"기초 백엔드 스터디",
27+
mentor,
28+
STUDY_ONGOING_PERIOD,
29+
Period.of(START_DATE.minusDays(10), START_DATE.minusDays(5)),
30+
TOTAL_WEEK,
31+
ONLINE_STUDY,
32+
DAY_OF_WEEK,
33+
STUDY_START_TIME,
34+
STUDY_END_TIME);
35+
36+
// when
37+
String couponName = couponNameUtil.generateStudyCompletionCouponName(study);
38+
39+
// then
40+
assertThat(couponName).isEqualTo("2025-1 기초 백엔드 스터디 수료 쿠폰");
41+
}
42+
}

src/test/java/com/gdschongik/gdsc/domain/study/domain/StudyHistoryTest.java

+1
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ class 스터디_수료_철회시 {
6969
mentor, Period.of(now.plusDays(5), now.plusDays(10)), Period.of(now.minusDays(5), now));
7070

7171
StudyHistory studyHistory = StudyHistory.create(student, study);
72+
fixtureHelper.setId(studyHistory, 1L); // TODO: 이벤트 ID 필드를 원시 타입으로 설정하는 것 vs setId를 테스트 사용 강제 간 trade-off 고민
7273
studyHistory.complete();
7374

7475
// when

src/test/java/com/gdschongik/gdsc/helper/FixtureHelper.java

+5
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import static com.gdschongik.gdsc.global.common.constant.StudyConstant.*;
99
import static com.gdschongik.gdsc.global.common.constant.TemporalConstant.*;
1010

11+
import com.gdschongik.gdsc.domain.common.model.BaseEntity;
1112
import com.gdschongik.gdsc.domain.common.model.SemesterType;
1213
import com.gdschongik.gdsc.domain.common.vo.Money;
1314
import com.gdschongik.gdsc.domain.common.vo.Period;
@@ -25,6 +26,10 @@
2526

2627
public class FixtureHelper {
2728

29+
public <T extends BaseEntity> void setId(T entity, Long id) {
30+
ReflectionTestUtils.setField(entity, "id", id);
31+
}
32+
2833
public Member createGuestMember(Long id) {
2934
Member member = Member.createGuest(OAUTH_ID);
3035
ReflectionTestUtils.setField(member, "id", id);

0 commit comments

Comments
 (0)