diff --git a/src/docs/asciidoc/coupon.adoc b/src/docs/asciidoc/coupon.adoc new file mode 100644 index 00000000..f06a91ef --- /dev/null +++ b/src/docs/asciidoc/coupon.adoc @@ -0,0 +1,35 @@ +== 쿠폰(Coupon) + + 쿠폰에 대해 생성/삭제/조회/발급/사용 기능을 제공합니다. + +=== 쿠폰 생성 + + 관리자가 쿠폰을 생성합니다. + +[discrete] +==== 요청 + +include::{snippets}/coupons/http-request.adoc[] + +[discrete] +==== 응답 + +include::{snippets}/coupons/http-response.adoc[] + +=== 쿠폰 삭제 (진행 중) + + 관리자가 쿠폰을 삭제합니다. + +=== 쿠폰 조회 (진행 중) + + 관리자 혹은 사용자가 쿠폰들을 조회합니다. + + 사용자가 자신의 보관함에 있는 쿠폰들을 조회합니다. + +=== 쿠폰 발급 (진행 중) + + 사용자가 발급 가능한 쿠폰을 선착순으로 발급 받습니다. + +=== 쿠폰 사용 (진행 중) + + 사용자가 자신의 보관함에 있는 쿠폰들을 사용합니다. diff --git a/src/main/java/com/moabam/api/application/CouponService.java b/src/main/java/com/moabam/api/application/CouponService.java new file mode 100644 index 00000000..7f8609bd --- /dev/null +++ b/src/main/java/com/moabam/api/application/CouponService.java @@ -0,0 +1,45 @@ +package com.moabam.api.application; + +import java.time.LocalDateTime; + +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import com.moabam.api.domain.entity.Coupon; +import com.moabam.api.domain.repository.CouponRepository; +import com.moabam.api.dto.CouponMapper; +import com.moabam.api.dto.CreateCouponRequest; +import com.moabam.global.error.exception.BadRequestException; +import com.moabam.global.error.exception.ConflictException; +import com.moabam.global.error.model.ErrorMessage; + +import lombok.RequiredArgsConstructor; + +@Service +@RequiredArgsConstructor +@Transactional(readOnly = true) +public class CouponService { + + private final CouponRepository couponRepository; + + @Transactional + public void createCoupon(Long adminId, CreateCouponRequest request) { + validateConflictCouponName(request.name()); + validateCouponPeriod(request.startAt(), request.endAt()); + + Coupon coupon = CouponMapper.toEntity(adminId, request); + couponRepository.save(coupon); + } + + private void validateConflictCouponName(String name) { + if (couponRepository.existsByName(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); + } + } +} diff --git a/src/main/java/com/moabam/api/domain/entity/Coupon.java b/src/main/java/com/moabam/api/domain/entity/Coupon.java new file mode 100644 index 00000000..102da46f --- /dev/null +++ b/src/main/java/com/moabam/api/domain/entity/Coupon.java @@ -0,0 +1,93 @@ +package com.moabam.api.domain.entity; + +import static com.moabam.global.error.model.ErrorMessage.*; +import static java.util.Objects.*; + +import java.time.LocalDateTime; + +import org.hibernate.annotations.ColumnDefault; + +import com.moabam.api.domain.entity.enums.CouponType; +import com.moabam.global.common.entity.BaseTimeEntity; +import com.moabam.global.error.exception.BadRequestException; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Entity +@Getter +@Table(name = "coupon") +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class Coupon extends BaseTimeEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "id") + private Long id; + + @Column(name = "name", nullable = false, unique = true, length = 20) + private String name; + + @ColumnDefault("1") + @Column(name = "point", nullable = false) + private int point; + + @Column(name = "description", length = 50) + private String description; + + @Enumerated(value = EnumType.STRING) + @Column(name = "type", nullable = false) + private CouponType type; + + @ColumnDefault("1") + @Column(name = "stock", nullable = false) + private int stock; + + @Column(name = "start_at", nullable = false) + private LocalDateTime startAt; + + @Column(name = "end_at", nullable = false) + private LocalDateTime endAt; + + @Column(name = "admin_id", updatable = false, nullable = false) + private Long adminId; + + @Builder + 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 = description; + this.type = requireNonNull(type); + this.stock = validateStock(stock); + this.startAt = requireNonNull(startAt); + this.endAt = requireNonNull(endAt); + this.adminId = requireNonNull(adminId); + } + + private int validatePoint(int point) { + if (point < 1) { + throw new BadRequestException(INVALID_COUPON_POINT); + } + + return point; + } + + private int validateStock(int stock) { + if (stock < 1) { + throw new BadRequestException(INVALID_COUPON_STOCK); + } + + return stock; + } +} diff --git a/src/main/java/com/moabam/api/domain/entity/enums/CouponType.java b/src/main/java/com/moabam/api/domain/entity/enums/CouponType.java new file mode 100644 index 00000000..9cdb78de --- /dev/null +++ b/src/main/java/com/moabam/api/domain/entity/enums/CouponType.java @@ -0,0 +1,38 @@ +package com.moabam.api.domain.entity.enums; + +import java.util.Arrays; +import java.util.Collections; +import java.util.Map; +import java.util.Optional; +import java.util.function.Function; +import java.util.stream.Collectors; + +import com.moabam.global.error.exception.NotFoundException; +import com.moabam.global.error.model.ErrorMessage; + +import lombok.AccessLevel; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@RequiredArgsConstructor(access = AccessLevel.PRIVATE) +public enum CouponType { + + MORNING_COUPON("아침"), + NIGHT_COUPON("저녁"), + GOLDEN_COUPON("황금"), + DISCOUNT_COUPON("할인"); + + private final String typeName; + private static final Map COUPON_TYPE_MAP; + + static { + COUPON_TYPE_MAP = Collections.unmodifiableMap(Arrays.stream(values()) + .collect(Collectors.toMap(CouponType::getTypeName, Function.identity()))); + } + + public static CouponType from(String typeName) { + return Optional.ofNullable(COUPON_TYPE_MAP.get(typeName)) + .orElseThrow(() -> new NotFoundException(ErrorMessage.NOT_FOUND_COUPON_TYPE)); + } +} diff --git a/src/main/java/com/moabam/api/domain/repository/CouponRepository.java b/src/main/java/com/moabam/api/domain/repository/CouponRepository.java new file mode 100644 index 00000000..1a262050 --- /dev/null +++ b/src/main/java/com/moabam/api/domain/repository/CouponRepository.java @@ -0,0 +1,10 @@ +package com.moabam.api.domain.repository; + +import org.springframework.data.jpa.repository.JpaRepository; + +import com.moabam.api.domain.entity.Coupon; + +public interface CouponRepository extends JpaRepository { + + boolean existsByName(String name); +} diff --git a/src/main/java/com/moabam/api/dto/CouponMapper.java b/src/main/java/com/moabam/api/dto/CouponMapper.java new file mode 100644 index 00000000..400f232d --- /dev/null +++ b/src/main/java/com/moabam/api/dto/CouponMapper.java @@ -0,0 +1,24 @@ +package com.moabam.api.dto; + +import com.moabam.api.domain.entity.Coupon; +import com.moabam.api.domain.entity.enums.CouponType; + +import lombok.AccessLevel; +import lombok.NoArgsConstructor; + +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public final class CouponMapper { + + public static Coupon toEntity(Long adminId, CreateCouponRequest request) { + return Coupon.builder() + .name(request.name()) + .description(request.description()) + .type(CouponType.from(request.type())) + .point(request.point()) + .stock(request.stock()) + .startAt(request.startAt()) + .endAt(request.endAt()) + .adminId(adminId) + .build(); + } +} diff --git a/src/main/java/com/moabam/api/dto/CreateCouponRequest.java b/src/main/java/com/moabam/api/dto/CreateCouponRequest.java new file mode 100644 index 00000000..710fffc3 --- /dev/null +++ b/src/main/java/com/moabam/api/dto/CreateCouponRequest.java @@ -0,0 +1,27 @@ +package com.moabam.api.dto; + +import java.time.LocalDateTime; + +import org.hibernate.validator.constraints.Length; + +import com.fasterxml.jackson.annotation.JsonFormat; + +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import lombok.Builder; + +@Builder +public record CreateCouponRequest( + @NotBlank(message = "쿠폰명이 입력되지 않았거나 20자를 넘었습니다.") @Length(max = 20) String name, + @Length(max = 50, message = "쿠폰 간단 소개는 최대 50자까지 가능합니다.") String description, + @NotBlank(message = "쿠폰 종류를 입력해주세요.") String type, + @Min(value = 1, message = "벌레 수 혹은 할인 금액은 1 이상이어야 합니다.") int point, + @Min(value = 1, message = "쿠폰 재고는 1 이상이어야 합니다.") int stock, + @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd'T'HH:mm") + @NotNull(message = "쿠폰 발급 시작 시각을 입력해주세요.") LocalDateTime startAt, + @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd'T'HH:mm") + @NotNull(message = "쿠폰 발급 종료 시각을 입력해주세요.") LocalDateTime endAt +) { + +} diff --git a/src/main/java/com/moabam/api/presentation/CouponController.java b/src/main/java/com/moabam/api/presentation/CouponController.java new file mode 100644 index 00000000..acac1ac8 --- /dev/null +++ b/src/main/java/com/moabam/api/presentation/CouponController.java @@ -0,0 +1,27 @@ +package com.moabam.api.presentation; + +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.ResponseStatus; +import org.springframework.web.bind.annotation.RestController; + +import com.moabam.api.application.CouponService; +import com.moabam.api.dto.CreateCouponRequest; + +import lombok.RequiredArgsConstructor; + +@RestController +@RequiredArgsConstructor +@RequestMapping("/admins/coupons") +public class CouponController { + + private final CouponService couponService; + + @PostMapping + @ResponseStatus(HttpStatus.OK) + public void createCoupon(@RequestBody CreateCouponRequest request) { + couponService.createCoupon(1L, request); + } +} diff --git a/src/main/java/com/moabam/global/error/model/ErrorMessage.java b/src/main/java/com/moabam/global/error/model/ErrorMessage.java index a5ee589a..c5843d25 100644 --- a/src/main/java/com/moabam/global/error/model/ErrorMessage.java +++ b/src/main/java/com/moabam/global/error/model/ErrorMessage.java @@ -39,7 +39,13 @@ public enum ErrorMessage { FAILED_FCM_INIT("파이어베이스 설정을 실패했습니다."), NOT_FOUND_FCM_TOKEN("해당 유저는 접속 중이 아닙니다."), - CONFLICT_KNOCK("이미 콕 알림을 보낸 대상입니다."); + CONFLICT_KNOCK("이미 콕 알림을 보낸 대상입니다."), + + INVALID_COUPON_POINT("쿠폰의 보너스 포인트는 0 이상이어야 합니다."), + INVALID_COUPON_STOCK("쿠폰의 재고는 0 이상이어야 합니다."), + CONFLICT_COUPON_NAME("쿠폰의 이름이 중복되었습니다."), + NOT_FOUND_COUPON_TYPE("존재하지 않는 쿠폰 종류입니다."), + INVALID_COUPON_PERIOD("쿠폰 발급 종료 시각은 시작 시각보다 이후여야 합니다."); private final String message; } diff --git a/src/main/resources/static/docs/coupon.html b/src/main/resources/static/docs/coupon.html new file mode 100644 index 00000000..a0074984 --- /dev/null +++ b/src/main/resources/static/docs/coupon.html @@ -0,0 +1,540 @@ + + + + + + + +쿠폰(Coupon) + + + + + +
+
+

쿠폰(Coupon)

+
+
+
+
쿠폰에 대해 생성/삭제/조회/발급/사용 기능을 제공합니다.
+
+
+
+

쿠폰 생성

+
+
+
관리자가 쿠폰을 생성합니다.
+
+
+

요청

+
+
+
POST /admins/coupons HTTP/1.1
+Content-Type: application/json;charset=UTF-8
+Content-Length: 186
+Host: localhost:8080
+
+{
+  "name" : "couponName",
+  "description" : "coupon description",
+  "type" : "황금",
+  "point" : 10,
+  "stock" : 10,
+  "startAt" : "2000-01-22T10:30",
+  "endAt" : "2000-02-22T11:00"
+}
+
+
+

응답

+
+
+
HTTP/1.1 409 Conflict
+Vary: Origin
+Vary: Access-Control-Request-Method
+Vary: Access-Control-Request-Headers
+Content-Type: application/json
+Content-Length: 62
+
+{
+  "message" : "쿠폰의 이름이 중복되었습니다."
+}
+
+
+
+
+

쿠폰 삭제 (진행 중)

+
+
+
관리자가 쿠폰을 삭제합니다.
+
+
+
+
+

쿠폰 조회 (진행 중)

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

쿠폰 발급 (진행 중)

+
+
+
사용자가 발급 가능한 쿠폰을 선착순으로 발급 받습니다.
+
+
+
+
+

쿠폰 사용 (진행 중)

+
+
+
사용자가 자신의 보관함에 있는 쿠폰들을 사용합니다.
+
+
+
+
+
+
+ + + \ No newline at end of file diff --git a/src/test/java/com/moabam/api/application/CouponServiceTest.java b/src/test/java/com/moabam/api/application/CouponServiceTest.java new file mode 100644 index 00000000..2759bec5 --- /dev/null +++ b/src/test/java/com/moabam/api/application/CouponServiceTest.java @@ -0,0 +1,89 @@ +package com.moabam.api.application; + +import static org.assertj.core.api.Assertions.*; +import static org.mockito.BDDMockito.*; + +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 com.moabam.api.domain.entity.Coupon; +import com.moabam.api.domain.entity.enums.CouponType; +import com.moabam.api.domain.repository.CouponRepository; +import com.moabam.api.dto.CreateCouponRequest; +import com.moabam.global.error.exception.BadRequestException; +import com.moabam.global.error.exception.ConflictException; +import com.moabam.global.error.exception.NotFoundException; +import com.moabam.global.error.model.ErrorMessage; +import com.moabam.support.fixture.CouponFixture; + +@ExtendWith(MockitoExtension.class) +class CouponServiceTest { + + @InjectMocks + private CouponService couponService; + + @Mock + private CouponRepository couponRepository; + + @DisplayName("쿠폰을 성공적으로 발행한다. - Void") + @Test + void couponService_createCoupon() { + // Given + String couponType = CouponType.GOLDEN_COUPON.getTypeName(); + CreateCouponRequest request = CouponFixture.createCouponRequest(couponType, 1, 2); + + given(couponRepository.existsByName(any(String.class))).willReturn(false); + + // When + couponService.createCoupon(1L, request); + + // Then + verify(couponRepository).save(any(Coupon.class)); + } + + @DisplayName("중복된 쿠폰명을 발행한다. - ConflictException") + @Test + void couponService_createCoupon_ConflictException() { + // Given + String couponType = CouponType.GOLDEN_COUPON.getTypeName(); + CreateCouponRequest request = CouponFixture.createCouponRequest(couponType, 1, 2); + + given(couponRepository.existsByName(any(String.class))).willReturn(true); + + // When & Then + assertThatThrownBy(() -> couponService.createCoupon(1L, request)) + .isInstanceOf(ConflictException.class) + .hasMessage(ErrorMessage.CONFLICT_COUPON_NAME.getMessage()); + } + + @DisplayName("존재하지 않는 쿠폰 종류를 발행한다. - NotFoundException") + @Test + void couponService_createCoupon_NotFoundException() { + // Given + CreateCouponRequest request = CouponFixture.createCouponRequest("UNKNOWN", 1, 2); + given(couponRepository.existsByName(any(String.class))).willReturn(false); + + // When & Then + assertThatThrownBy(() -> couponService.createCoupon(1L, request)) + .isInstanceOf(NotFoundException.class) + .hasMessage(ErrorMessage.NOT_FOUND_COUPON_TYPE.getMessage()); + } + + @DisplayName("쿠폰 발급 종료 기간이 시작 기간보다 더 이전인 쿠폰을 발행한다. - BadRequestException") + @Test + void couponService_createCoupon_BadRequestException() { + // Given + String couponType = CouponType.GOLDEN_COUPON.getTypeName(); + CreateCouponRequest request = CouponFixture.createCouponRequest(couponType, 2, 1); + given(couponRepository.existsByName(any(String.class))).willReturn(false); + + // When & Then + assertThatThrownBy(() -> couponService.createCoupon(1L, request)) + .isInstanceOf(BadRequestException.class) + .hasMessage(ErrorMessage.INVALID_COUPON_PERIOD.getMessage()); + } +} diff --git a/src/test/java/com/moabam/api/domain/entity/CouponTest.java b/src/test/java/com/moabam/api/domain/entity/CouponTest.java new file mode 100644 index 00000000..1f68c7de --- /dev/null +++ b/src/test/java/com/moabam/api/domain/entity/CouponTest.java @@ -0,0 +1,63 @@ +package com.moabam.api.domain.entity; + +import static org.assertj.core.api.Assertions.*; + +import java.time.LocalDateTime; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import com.moabam.api.domain.entity.enums.CouponType; +import com.moabam.global.error.exception.BadRequestException; +import com.moabam.global.error.model.ErrorMessage; +import com.moabam.support.fixture.CouponFixture; + +class CouponTest { + + @DisplayName("쿠폰이 정상적으로 생성된다. - Coupon") + @Test + void coupon() { + // Given + LocalDateTime startAt = LocalDateTime.of(2000, 1, 22, 10, 30, 0); + LocalDateTime endAt = LocalDateTime.of(2000, 1, 22, 11, 0, 0); + + // When + Coupon actual = Coupon.builder() + .name("couponName") + .point(10) + .type(CouponType.MORNING_COUPON) + .stock(100) + .startAt(startAt) + .endAt(endAt) + .adminId(1L) + .build(); + + // Then + assertThat(actual.getName()).isEqualTo("couponName"); + assertThat(actual.getDescription()).isNull(); + assertThat(actual.getPoint()).isEqualTo(10); + assertThat(actual.getStock()).isEqualTo(100); + assertThat(actual.getType()).isEqualTo(CouponType.MORNING_COUPON); + assertThat(actual.getStartAt()).isEqualTo(startAt); + assertThat(actual.getEndAt()).isEqualTo(endAt); + assertThat(actual.getAdminId()).isEqualTo(1L); + } + + @DisplayName("쿠폰 보너스 포인트가 1보다 작다. - BadRequestException") + @Test + void coupon_validatePoint_Point_BadRequestException() { + // When& Then + assertThatThrownBy(() -> CouponFixture.coupon(0, 1)) + .isInstanceOf(BadRequestException.class) + .hasMessage(ErrorMessage.INVALID_COUPON_POINT.getMessage()); + } + + @DisplayName("쿠폰 재고가 1보다 작다. - BadRequestException") + @Test + void coupon_validatePoint_Stock_BadRequestException() { + // When& Then + assertThatThrownBy(() -> CouponFixture.coupon(1, 0)) + .isInstanceOf(BadRequestException.class) + .hasMessage(ErrorMessage.INVALID_COUPON_STOCK.getMessage()); + } +} diff --git a/src/test/java/com/moabam/api/domain/entity/enums/CouponTypeTest.java b/src/test/java/com/moabam/api/domain/entity/enums/CouponTypeTest.java new file mode 100644 index 00000000..4ceed56f --- /dev/null +++ b/src/test/java/com/moabam/api/domain/entity/enums/CouponTypeTest.java @@ -0,0 +1,31 @@ +package com.moabam.api.domain.entity.enums; + +import static org.assertj.core.api.Assertions.*; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import com.moabam.global.error.exception.NotFoundException; +import com.moabam.global.error.model.ErrorMessage; + +class CouponTypeTest { + + @DisplayName("존재하는 쿠폰을 가져온다. - CouponType") + @Test + void couponType_from() { + // When + CouponType actual = CouponType.from("황금"); + + // Then + assertThat(actual).isEqualTo(CouponType.GOLDEN_COUPON); + } + + @DisplayName("존재하지 않는 쿠폰을 가져온다. - NotFoundException") + @Test + void couponType_from_NotFoundException() { + // When & Then + assertThatThrownBy(() -> CouponType.from("Not-Coupon")) + .isInstanceOf(NotFoundException.class) + .hasMessage(ErrorMessage.NOT_FOUND_COUPON_TYPE.getMessage()); + } +} diff --git a/src/test/java/com/moabam/api/dto/CreateCouponRequestTest.java b/src/test/java/com/moabam/api/dto/CreateCouponRequestTest.java new file mode 100644 index 00000000..18d2c481 --- /dev/null +++ b/src/test/java/com/moabam/api/dto/CreateCouponRequestTest.java @@ -0,0 +1,31 @@ +package com.moabam.api.dto; + +import static org.assertj.core.api.Assertions.*; + +import java.time.LocalDateTime; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; + +class CreateCouponRequestTest { + + @DisplayName("쿠폰 발급 가능 시작 날짜가 올바른 형식으로 입력된다. - yyyy-MM-dd'T'HH:mm") + @Test + void createCouponRequest_StartAt() throws JsonProcessingException { + // Given + ObjectMapper objectMapper = new ObjectMapper(); + objectMapper.registerModule(new JavaTimeModule()); + + String json = "{\"startAt\":\"2023-11-09T10:10\"}"; + + // When + CreateCouponRequest actual = objectMapper.readValue(json, CreateCouponRequest.class); + + // Then + assertThat(actual.startAt()).isEqualTo(LocalDateTime.of(2023, 11, 9, 10, 10)); + } +} diff --git a/src/test/java/com/moabam/api/presentation/CouponControllerTest.java b/src/test/java/com/moabam/api/presentation/CouponControllerTest.java new file mode 100644 index 00000000..215e0b85 --- /dev/null +++ b/src/test/java/com/moabam/api/presentation/CouponControllerTest.java @@ -0,0 +1,80 @@ +package com.moabam.api.presentation; + +import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.*; +import static org.springframework.restdocs.operation.preprocess.Preprocessors.*; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; +import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.*; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.restdocs.AutoConfigureRestDocs; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.transaction.annotation.Transactional; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.moabam.api.domain.entity.enums.CouponType; +import com.moabam.api.domain.repository.CouponRepository; +import com.moabam.api.dto.CouponMapper; +import com.moabam.api.dto.CreateCouponRequest; +import com.moabam.support.fixture.CouponFixture; +import com.moabam.support.fixture.CouponSnippetFixture; + +@Transactional +@SpringBootTest +@AutoConfigureMockMvc +@AutoConfigureRestDocs +class CouponControllerTest { + + @Autowired + private MockMvc mockMvc; + + @Autowired + private ObjectMapper objectMapper; + + @Autowired + private CouponRepository couponRepository; + + @DisplayName("쿠폰을 성공적으로 발행한다. - Void") + @Test + void couponController_createCoupon() throws Exception { + // Given + String couponType = CouponType.GOLDEN_COUPON.getTypeName(); + CreateCouponRequest request = CouponFixture.createCouponRequest(couponType, 1, 2); + + // When & Then + mockMvc.perform(post("/admins/coupons") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andDo(print()) + .andDo(document("coupons", + preprocessRequest(prettyPrint()), + preprocessResponse(prettyPrint()), + CouponSnippetFixture.CREATE_COUPON_REQUEST)) + .andExpect(status().isOk()); + } + + @DisplayName("쿠폰명이 중복된 쿠폰을 발행한다. - ConflictException") + @Test + void couponController_createCoupon_ConflictException() throws Exception { + // Given + String couponType = CouponType.GOLDEN_COUPON.getTypeName(); + CreateCouponRequest request = CouponFixture.createCouponRequest(couponType, 1, 2); + couponRepository.save(CouponMapper.toEntity(1L, request)); + + // When & Then + mockMvc.perform(post("/admins/coupons") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andDo(print()) + .andDo(document("coupons", + preprocessRequest(prettyPrint()), + preprocessResponse(prettyPrint()), + CouponSnippetFixture.CREATE_COUPON_REQUEST)) + .andExpect(status().isConflict()); + } +} diff --git a/src/test/java/com/moabam/support/fixture/CouponFixture.java b/src/test/java/com/moabam/support/fixture/CouponFixture.java new file mode 100644 index 00000000..6dbc68b8 --- /dev/null +++ b/src/test/java/com/moabam/support/fixture/CouponFixture.java @@ -0,0 +1,34 @@ +package com.moabam.support.fixture; + +import java.time.LocalDateTime; + +import com.moabam.api.domain.entity.Coupon; +import com.moabam.api.domain.entity.enums.CouponType; +import com.moabam.api.dto.CreateCouponRequest; + +public final class CouponFixture { + + public static Coupon coupon(int point, int stock) { + return Coupon.builder() + .name("couponName") + .point(point) + .type(CouponType.MORNING_COUPON) + .stock(stock) + .startAt(LocalDateTime.of(2000, 1, 22, 10, 30, 0)) + .endAt(LocalDateTime.of(2000, 1, 22, 11, 0, 0)) + .adminId(1L) + .build(); + } + + public static CreateCouponRequest createCouponRequest(String couponType, int startMonth, int endMonth) { + return CreateCouponRequest.builder() + .name("couponName") + .description("coupon description") + .point(10) + .type(couponType) + .stock(10) + .startAt(LocalDateTime.of(2000, startMonth, 22, 10, 30, 0)) + .endAt(LocalDateTime.of(2000, endMonth, 22, 11, 0, 0)) + .build(); + } +} diff --git a/src/test/java/com/moabam/support/fixture/CouponSnippetFixture.java b/src/test/java/com/moabam/support/fixture/CouponSnippetFixture.java new file mode 100644 index 00000000..92345c0e --- /dev/null +++ b/src/test/java/com/moabam/support/fixture/CouponSnippetFixture.java @@ -0,0 +1,19 @@ +package com.moabam.support.fixture; + +import static org.springframework.restdocs.payload.JsonFieldType.*; +import static org.springframework.restdocs.payload.PayloadDocumentation.*; + +import org.springframework.restdocs.payload.RequestFieldsSnippet; + +public final class CouponSnippetFixture { + + public static final RequestFieldsSnippet CREATE_COUPON_REQUEST = requestFields( + fieldWithPath("name").type(STRING).description("쿠폰명"), + fieldWithPath("description").type(STRING).description("쿠폰 간단 소개 (NULL 가능)"), + fieldWithPath("type").type(STRING).description("쿠폰 종류 (아침, 저녁, 황금, 할인)"), + fieldWithPath("point").type(NUMBER).description("쿠폰 사용 시, 제공하는 포인트량"), + fieldWithPath("stock").type(NUMBER).description("쿠폰을 발급 받을 수 있는 수"), + fieldWithPath("startAt").type(STRING).description("쿠폰 발급 시작 날짜 (Ex: yyyy-MM-dd'T'HH:mm)"), + fieldWithPath("endAt").type(STRING).description("쿠폰 발급 종료 날짜 (Ex: yyyy-MM-dd'T'HH:mm)") + ); +}