diff --git a/src/main/java/earlybird/earlybird/email/SendEmailService.java b/src/main/java/earlybird/earlybird/email/SendEmailService.java index 75e523b..894b2a1 100644 --- a/src/main/java/earlybird/earlybird/email/SendEmailService.java +++ b/src/main/java/earlybird/earlybird/email/SendEmailService.java @@ -1,20 +1,23 @@ package earlybird.earlybird.email; +import static earlybird.earlybird.promotion.email.entity.PromotionEmailMessageType.BERKELEY_6_MONTH_FREE; + import earlybird.earlybird.common.util.LocalDateTimeUtil; import earlybird.earlybird.promotion.email.entity.PromotionEmailMessageType; import earlybird.earlybird.promotion.email.entity.PromotionEmailVerification; + import jakarta.mail.Message; import jakarta.mail.internet.MimeMessage; + import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; + import org.springframework.mail.javamail.JavaMailSender; import org.springframework.retry.annotation.Backoff; import org.springframework.retry.annotation.Retryable; import org.springframework.scheduling.annotation.Async; import org.springframework.stereotype.Service; -import static earlybird.earlybird.promotion.email.entity.PromotionEmailMessageType.BERKELEY_6_MONTH_FREE; - @Slf4j @RequiredArgsConstructor @Service diff --git a/src/main/java/earlybird/earlybird/promotion/controller/request/CreatePromotionCampaignRequest.java b/src/main/java/earlybird/earlybird/promotion/controller/request/CreatePromotionCampaignRequest.java index 0324a9a..62bdd2f 100644 --- a/src/main/java/earlybird/earlybird/promotion/controller/request/CreatePromotionCampaignRequest.java +++ b/src/main/java/earlybird/earlybird/promotion/controller/request/CreatePromotionCampaignRequest.java @@ -7,11 +7,15 @@ import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotNull; +import lombok.AllArgsConstructor; +import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; import java.time.LocalDateTime; +@Builder +@AllArgsConstructor @Getter @NoArgsConstructor public class CreatePromotionCampaignRequest { @@ -34,9 +38,5 @@ public class CreatePromotionCampaignRequest { timezone = "Asia/Seoul") private LocalDateTime endTime; - @NotNull private Long perUserLimit; - - @NotNull private Long totalIssueLimit; - @NotNull private PromotionCampaignType promotionCampaignType; } diff --git a/src/main/java/earlybird/earlybird/promotion/controller/response/CreatePromotionCampaignResponse.java b/src/main/java/earlybird/earlybird/promotion/controller/response/CreatePromotionCampaignResponse.java index 1acbc0b..8674c44 100644 --- a/src/main/java/earlybird/earlybird/promotion/controller/response/CreatePromotionCampaignResponse.java +++ b/src/main/java/earlybird/earlybird/promotion/controller/response/CreatePromotionCampaignResponse.java @@ -15,7 +15,7 @@ public class CreatePromotionCampaignResponse { public static CreatePromotionCampaignResponse from( CreatePromotionCampaignServiceResponse response) { return CreatePromotionCampaignResponse.builder() - .promotionCampaignId(response.getPromotionCampaignId()) + .promotionCampaignId(response.promotionCampaignId()) .build(); } } diff --git a/src/main/java/earlybird/earlybird/promotion/email/repository/PromotionEmailVerificationRepository.java b/src/main/java/earlybird/earlybird/promotion/email/repository/PromotionEmailVerificationRepository.java index 72ccb1d..c6dc6e4 100644 --- a/src/main/java/earlybird/earlybird/promotion/email/repository/PromotionEmailVerificationRepository.java +++ b/src/main/java/earlybird/earlybird/promotion/email/repository/PromotionEmailVerificationRepository.java @@ -1,6 +1,7 @@ package earlybird.earlybird.promotion.email.repository; import earlybird.earlybird.promotion.email.entity.PromotionEmailVerification; +import earlybird.earlybird.promotion.entity.PromotionCampaign; import earlybird.earlybird.promotion.entity.PromotionUrlUuid; import org.springframework.data.jpa.repository.JpaRepository; @@ -11,4 +12,7 @@ public interface PromotionEmailVerificationRepository extends JpaRepository { Optional findByPromotionUrlUuid(PromotionUrlUuid promotionUrlUuid); + + Optional findByPromotionCampaignAndEmail( + PromotionCampaign promotionCampaign, String email); } diff --git a/src/main/java/earlybird/earlybird/promotion/email/service/SendPromotionEmailService.java b/src/main/java/earlybird/earlybird/promotion/email/service/SendPromotionEmailService.java index a0f6aed..fd9efe4 100644 --- a/src/main/java/earlybird/earlybird/promotion/email/service/SendPromotionEmailService.java +++ b/src/main/java/earlybird/earlybird/promotion/email/service/SendPromotionEmailService.java @@ -32,17 +32,29 @@ public class SendPromotionEmailService { @Transactional public void sendVerificationEmail(SendVerificationEmailServiceRequest request) { - /** - * TODO - 이메일 유효성 검사 (완) - URL을 구성할 UUID 발급 (완) - DB에서 애플 프로모션 URL 하나 조회 - 락을 걸었는데 이거 성능 문제 - * 좀 더 고민 필요 - 이메일로 인증 URL 전송 (여기에 UUID가 들어감) - 비동기 처리 필요 (완) - no-reply@earlybirdteam.com - * 이메일 적용 필요 - 인증 URL을 클릭하면 애플 프로모션 URL로 리다이렉션 (완) - 엣지 케이스 좀 더 고민 필요 - 스프링 트랜잭션에 대한 고민 필요 - - * Retry 로직 도입 고려 (완) - 메일 발송 부분 로직 문제 없나 다시 체크 - */ + /** TODO - 락을 걸었는데 이거 성능 문제 좀 더 고민 필요 - 스프링 트랜잭션에 대한 고민 필요 - 메일 발송 부분 로직 문제 없나 다시 체크 */ checkEmailAddress(request); PromotionCampaign promotionCampaign = getPromotionCampaignService.findById(request.getPromotionCampaignId()); + promotionEmailVerificationRepository + .findByPromotionCampaignAndEmail(promotionCampaign, request.getEmail()) + .ifPresentOrElse( + verification -> sendEmailIfSendBefore(verification, request), + () -> sendFirstEmail(promotionCampaign, request)); + } + + // 이전에 동일한 캠패인, 동일한 이메일로 인증 메일을 전송한 적이 있으면 이 함수 호출 + private void sendEmailIfSendBefore( + PromotionEmailVerification promotionEmailVerification, + SendVerificationEmailServiceRequest request) { + sendEmailService.send(promotionEmailVerification, request.getPromotionEmailMessageType()); + } + + // 처음 인증 요청하는 건 이 함수 호출 + private void sendFirstEmail( + PromotionCampaign promotionCampaign, SendVerificationEmailServiceRequest request) { PromotionUrlUuid promotionUrlUuid = createPromotionUrlUuidService.create(); String promotionUrl = getApplePromotionUrlService diff --git a/src/main/java/earlybird/earlybird/promotion/entity/PromotionCampaign.java b/src/main/java/earlybird/earlybird/promotion/entity/PromotionCampaign.java index 3802229..1246e01 100644 --- a/src/main/java/earlybird/earlybird/promotion/entity/PromotionCampaign.java +++ b/src/main/java/earlybird/earlybird/promotion/entity/PromotionCampaign.java @@ -37,14 +37,6 @@ public class PromotionCampaign extends BaseTimeEntity { @Column(name = "promotion_campaigns_ends_time", nullable = false) private LocalDateTime endTime; - // // 인당 발급 허용 개수 - // @Column(name = "promotion_campaigns_per_user_limit", nullable = false) - // private Long perUserLimit; - // - // // 캠페인 전체 발급 최대치 (없으면 NULL) - // @Column(name = "promotion_campaigns_total_issue_limit") - // private Long totalIssueLimit; - // 캠페인 타입 @Enumerated(EnumType.STRING) @Column(name = "promotion_campaigns_type", nullable = false) diff --git a/src/main/java/earlybird/earlybird/promotion/service/CreatePromotionCampaignService.java b/src/main/java/earlybird/earlybird/promotion/service/CreatePromotionCampaignService.java index 820807a..10c9a4e 100644 --- a/src/main/java/earlybird/earlybird/promotion/service/CreatePromotionCampaignService.java +++ b/src/main/java/earlybird/earlybird/promotion/service/CreatePromotionCampaignService.java @@ -27,13 +27,11 @@ public CreatePromotionCampaignServiceResponse create( private PromotionCampaign createPromotionCampaign( CreatePromotionCampaignServiceRequest request) { return PromotionCampaign.builder() - .name(request.getName()) - .description(request.getDescription()) - .startTime(request.getStartTime()) - .endTime(request.getEndTime()) - // .perUserLimit(request.getPerUserLimit()) - // .totalIssueLimit(request.getTotalIssueLimit()) - .promotionCampaignType(request.getPromotionCampaignType()) + .name(request.name()) + .description(request.description()) + .startTime(request.startTime()) + .endTime(request.endTime()) + .promotionCampaignType(request.promotionCampaignType()) .active(true) .build(); } diff --git a/src/main/java/earlybird/earlybird/promotion/service/request/CreatePromotionCampaignServiceRequest.java b/src/main/java/earlybird/earlybird/promotion/service/request/CreatePromotionCampaignServiceRequest.java index bee961f..9acf23f 100644 --- a/src/main/java/earlybird/earlybird/promotion/service/request/CreatePromotionCampaignServiceRequest.java +++ b/src/main/java/earlybird/earlybird/promotion/service/request/CreatePromotionCampaignServiceRequest.java @@ -4,22 +4,16 @@ import earlybird.earlybird.promotion.entity.PromotionCampaignType; import lombok.Builder; -import lombok.Getter; -import lombok.RequiredArgsConstructor; import java.time.LocalDateTime; @Builder -@RequiredArgsConstructor -@Getter -public class CreatePromotionCampaignServiceRequest { - private final String name; - private final String description; - private final LocalDateTime startTime; - private final LocalDateTime endTime; - private final Long perUserLimit; - private final Long totalIssueLimit; - private final PromotionCampaignType promotionCampaignType; +public record CreatePromotionCampaignServiceRequest( + String name, + String description, + LocalDateTime startTime, + LocalDateTime endTime, + PromotionCampaignType promotionCampaignType) { public static CreatePromotionCampaignServiceRequest from( CreatePromotionCampaignRequest request) { @@ -28,8 +22,6 @@ public static CreatePromotionCampaignServiceRequest from( .description(request.getPromotionCampaignDescription()) .startTime(request.getStartTime()) .endTime(request.getEndTime()) - .perUserLimit(request.getPerUserLimit()) - .totalIssueLimit(request.getTotalIssueLimit()) .promotionCampaignType(request.getPromotionCampaignType()) .build(); } diff --git a/src/main/java/earlybird/earlybird/promotion/service/response/CreatePromotionCampaignServiceResponse.java b/src/main/java/earlybird/earlybird/promotion/service/response/CreatePromotionCampaignServiceResponse.java index 00037e2..b931bef 100644 --- a/src/main/java/earlybird/earlybird/promotion/service/response/CreatePromotionCampaignServiceResponse.java +++ b/src/main/java/earlybird/earlybird/promotion/service/response/CreatePromotionCampaignServiceResponse.java @@ -3,18 +3,13 @@ import earlybird.earlybird.promotion.entity.PromotionCampaign; import lombok.Builder; -import lombok.Getter; -import lombok.RequiredArgsConstructor; @Builder -@Getter -@RequiredArgsConstructor -public class CreatePromotionCampaignServiceResponse { - private final Long PromotionCampaignId; +public record CreatePromotionCampaignServiceResponse(Long promotionCampaignId) { public static CreatePromotionCampaignServiceResponse from(PromotionCampaign promotionCampaign) { return CreatePromotionCampaignServiceResponse.builder() - .PromotionCampaignId(promotionCampaign.getId()) + .promotionCampaignId(promotionCampaign.getId()) .build(); } } diff --git a/src/test/java/earlybird/earlybird/promotion/service/CreatePromotionCampaignServiceTest.java b/src/test/java/earlybird/earlybird/promotion/service/CreatePromotionCampaignServiceTest.java new file mode 100644 index 0000000..72bd855 --- /dev/null +++ b/src/test/java/earlybird/earlybird/promotion/service/CreatePromotionCampaignServiceTest.java @@ -0,0 +1,66 @@ +package earlybird.earlybird.promotion.service; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import earlybird.earlybird.promotion.entity.PromotionCampaign; +import earlybird.earlybird.promotion.entity.PromotionCampaignType; +import earlybird.earlybird.promotion.repository.PromotionCampaignRepository; +import earlybird.earlybird.promotion.service.request.CreatePromotionCampaignServiceRequest; +import earlybird.earlybird.promotion.service.response.CreatePromotionCampaignServiceResponse; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.time.LocalDateTime; + +@ExtendWith(MockitoExtension.class) +class CreatePromotionCampaignServiceTest { + + @InjectMocks CreatePromotionCampaignService createPromotionCampaignService; + + @Mock PromotionCampaignRepository promotionCampaignRepository; + + @DisplayName("프로모션 캠페인을 생성하고 저장한다") + @Test + void create() { + // given + CreatePromotionCampaignServiceRequest request = + CreatePromotionCampaignServiceRequest.builder() + .name("테스트 캠페인") + .description("테스트 캠페인 설명") + .startTime(LocalDateTime.of(2024, 1, 1, 0, 0)) + .endTime(LocalDateTime.of(2024, 12, 31, 23, 59)) + .promotionCampaignType(PromotionCampaignType.EDU_EMAIL_6_MONTH_FREE) + .build(); + + PromotionCampaign savedCampaign = + PromotionCampaign.builder() + .id(1L) + .name("테스트 캠페인") + .description("테스트 캠페인 설명") + .startTime(LocalDateTime.of(2024, 1, 1, 0, 0)) + .endTime(LocalDateTime.of(2024, 12, 31, 23, 59)) + .promotionCampaignType(PromotionCampaignType.EDU_EMAIL_6_MONTH_FREE) + .active(true) + .build(); + + when(promotionCampaignRepository.save(any(PromotionCampaign.class))) + .thenReturn(savedCampaign); + + // when + CreatePromotionCampaignServiceResponse response = + createPromotionCampaignService.create(request); + + // then + assertThat(response).isNotNull(); + assertThat(response.promotionCampaignId()).isEqualTo(1L); + verify(promotionCampaignRepository).save(any(PromotionCampaign.class)); + } +} diff --git a/src/test/java/earlybird/earlybird/promotion/service/CreatePromotionUrlUuidServiceTest.java b/src/test/java/earlybird/earlybird/promotion/service/CreatePromotionUrlUuidServiceTest.java new file mode 100644 index 0000000..d73304c --- /dev/null +++ b/src/test/java/earlybird/earlybird/promotion/service/CreatePromotionUrlUuidServiceTest.java @@ -0,0 +1,43 @@ +package earlybird.earlybird.promotion.service; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import earlybird.earlybird.promotion.entity.PromotionUrlUuid; +import earlybird.earlybird.promotion.repository.PromotionUrlUuidRepository; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.util.UUID; + +@ExtendWith(MockitoExtension.class) +class CreatePromotionUrlUuidServiceTest { + + @InjectMocks CreatePromotionUrlUuidService createPromotionUrlUuidService; + + @Mock PromotionUrlUuidRepository promotionUrlUuidRepository; + + @DisplayName("UUID를 생성하고 저장한다") + @Test + void create() { + // given + PromotionUrlUuid savedEntity = PromotionUrlUuid.builder().uuid(UUID.randomUUID()).build(); + + when(promotionUrlUuidRepository.save(any(PromotionUrlUuid.class))).thenReturn(savedEntity); + + // when + PromotionUrlUuid result = createPromotionUrlUuidService.create(); + + // then + assertThat(result).isNotNull(); + assertThat(result.getUuid()).isNotNull(); + verify(promotionUrlUuidRepository).save(any(PromotionUrlUuid.class)); + } +} diff --git a/src/test/java/earlybird/earlybird/promotion/service/GetPromotionCampaignServiceTest.java b/src/test/java/earlybird/earlybird/promotion/service/GetPromotionCampaignServiceTest.java new file mode 100644 index 0000000..7d3063a --- /dev/null +++ b/src/test/java/earlybird/earlybird/promotion/service/GetPromotionCampaignServiceTest.java @@ -0,0 +1,58 @@ +package earlybird.earlybird.promotion.service; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +import earlybird.earlybird.promotion.entity.PromotionCampaign; +import earlybird.earlybird.promotion.repository.PromotionCampaignRepository; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.util.NoSuchElementException; +import java.util.Optional; + +@ExtendWith(MockitoExtension.class) +class GetPromotionCampaignServiceTest { + + @InjectMocks private GetPromotionCampaignService getPromotionCampaignService; + + @Mock private PromotionCampaignRepository promotionCampaignRepository; + + @DisplayName("프로모션 캠페인 ID가 주어지면 해당하는 프로모션 캠페인 객체를 반환한다.") + @Test + void findById() { + // given + Long promotionCampaignId = 1L; + PromotionCampaign promotionCampaign = + PromotionCampaign.builder().id(promotionCampaignId).build(); + when(promotionCampaignRepository.findById(promotionCampaignId)) + .thenReturn(Optional.of(promotionCampaign)); + + // when + PromotionCampaign result = getPromotionCampaignService.findById(promotionCampaignId); + + // then + assertThat(result.getId()).isEqualTo(promotionCampaignId); + assertThat(result).isEqualTo(promotionCampaign); + } + + @DisplayName("주어진 프로모션 캠페인 ID에 해당하는 프로모션 캠페인 객체가 없으면 예외를 던진다.") + @Test + void findByIdThrowException() { + // given + Long promotionCampaignId = 1L; + when(promotionCampaignRepository.findById(promotionCampaignId)) + .thenReturn(Optional.empty()); + + // when // then + assertThatThrownBy(() -> getPromotionCampaignService.findById(promotionCampaignId)) + .isInstanceOf(NoSuchElementException.class); + } +} diff --git a/src/test/java/earlybird/earlybird/promotion/service/request/CreatePromotionCampaignServiceRequestTest.java b/src/test/java/earlybird/earlybird/promotion/service/request/CreatePromotionCampaignServiceRequestTest.java new file mode 100644 index 0000000..b300cd6 --- /dev/null +++ b/src/test/java/earlybird/earlybird/promotion/service/request/CreatePromotionCampaignServiceRequestTest.java @@ -0,0 +1,45 @@ +package earlybird.earlybird.promotion.service.request; + +import static earlybird.earlybird.promotion.entity.PromotionCampaignType.EDU_EMAIL_6_MONTH_FREE; + +import static org.assertj.core.api.Assertions.assertThat; + +import earlybird.earlybird.promotion.controller.request.CreatePromotionCampaignRequest; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.time.LocalDateTime; + +class CreatePromotionCampaignServiceRequestTest { + + @DisplayName( + "CreatePromotionCampaignRequest 객체로부터 CreatePromotionCampaignServiceRequest 객체를 만든다.") + @Test + void from() { + // given + String promotionCampaignName = "promotionCampaignName"; + String promotionCampaignDescription = "promotionCampaignDescription"; + LocalDateTime localDateTime = LocalDateTime.of(2025, 8, 4, 0, 0, 0); + CreatePromotionCampaignRequest request = + CreatePromotionCampaignRequest.builder() + .promotionCampaignName(promotionCampaignName) + .promotionCampaignDescription(promotionCampaignDescription) + .startTime(localDateTime) + .endTime(localDateTime) + .promotionCampaignType(EDU_EMAIL_6_MONTH_FREE) + .build(); + + // when + CreatePromotionCampaignServiceRequest result = + CreatePromotionCampaignServiceRequest.from(request); + + // then + assertThat(result).isNotNull(); + assertThat(result.name()).isEqualTo(promotionCampaignName); + assertThat(result.description()).isEqualTo(promotionCampaignDescription); + assertThat(result.startTime()).isEqualTo(localDateTime); + assertThat(result.endTime()).isEqualTo(localDateTime); + assertThat(result.promotionCampaignType()).isEqualTo(EDU_EMAIL_6_MONTH_FREE); + } +} diff --git a/src/test/java/earlybird/earlybird/promotion/service/response/CreatePromotionCampaignServiceResponseTest.java b/src/test/java/earlybird/earlybird/promotion/service/response/CreatePromotionCampaignServiceResponseTest.java new file mode 100644 index 0000000..724202b --- /dev/null +++ b/src/test/java/earlybird/earlybird/promotion/service/response/CreatePromotionCampaignServiceResponseTest.java @@ -0,0 +1,27 @@ +package earlybird.earlybird.promotion.service.response; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.*; + +import earlybird.earlybird.promotion.entity.PromotionCampaign; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +class CreatePromotionCampaignServiceResponseTest { + + @DisplayName("PromotionCampaign 객체를 통해 CreatePromotionCampaignServiceResponse 객체를 만든다.") + @Test + void from() { + // given + PromotionCampaign promotionCampaign = PromotionCampaign.builder().id(1L).build(); + + // when + CreatePromotionCampaignServiceResponse response = + CreatePromotionCampaignServiceResponse.from(promotionCampaign); + + // then + assertThat(response).isNotNull(); + assertThat(response.promotionCampaignId()).isEqualTo(promotionCampaign.getId()); + } +}