From 8d16e9d96b11861bc50415427c89d97b51f737a5 Mon Sep 17 00:00:00 2001 From: Kim Heebin Date: Mon, 20 Nov 2023 14:20:42 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20=EB=B2=8C=EB=A0=88=20=EC=83=81=ED=92=88?= =?UTF-8?q?=20=EA=B5=AC=EB=A7=A4=20=EA=B8=B0=EB=8A=A5=20=EA=B5=AC=ED=98=84?= =?UTF-8?q?=20(#107)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: 결제 엔티티 생성 * feat: 벌레 상품 구매 API 구현 * test: 벌레 상품 구매 통합 테스트 * test: 벌레 상품 구매 서비스 테스트 * test: 결제 쿠폰 적용 테스트 * test: 주문 생성 및 금액 할인 테스트 * test: 벌레 사용 및 증가 로직 검증 방식 수정 * chore: config 업데이트 * fix: 상품 구매 Response에 주문 id 제거 * feat: 상품 구매 Response에 결제 id 추가 * fix: Transactional 적용 --- .../api/application/bug/BugService.java | 33 +++++++++ .../application/payment/PaymentMapper.java | 25 +++++++ .../application/product/ProductMapper.java | 10 +++ .../com/moabam/api/domain/payment/Order.java | 50 +++++++++++++ .../moabam/api/domain/payment/Payment.java | 73 +++++++++++++++++++ .../api/domain/payment/PaymentStatus.java | 10 +++ .../payment/repository/PaymentRepository.java | 9 +++ .../dto/product/PurchaseProductRequest.java | 9 +++ .../dto/product/PurchaseProductResponse.java | 12 +++ .../api/presentation/BugController.java | 13 ++++ .../global/error/model/ErrorMessage.java | 3 + src/main/resources/static/docs/coupon.html | 2 +- src/main/resources/static/docs/index.html | 2 +- .../resources/static/docs/notification.html | 2 +- .../api/application/bug/BugServiceTest.java | 24 ++++++ .../com/moabam/api/domain/bug/BugTest.java | 14 ++-- .../moabam/api/domain/payment/OrderTest.java | 57 +++++++++++++++ .../api/domain/payment/PaymentTest.java | 28 +++++++ .../api/domain/product/ProductTest.java | 4 +- .../api/presentation/BugControllerTest.java | 63 +++++++++++++++- .../moabam/support/fixture/CouponFixture.java | 17 +++++ .../support/fixture/PaymentFixture.java | 24 ++++++ .../support/fixture/ProductFixture.java | 2 +- 23 files changed, 472 insertions(+), 14 deletions(-) create mode 100644 src/main/java/com/moabam/api/application/payment/PaymentMapper.java create mode 100644 src/main/java/com/moabam/api/domain/payment/Order.java create mode 100644 src/main/java/com/moabam/api/domain/payment/Payment.java create mode 100644 src/main/java/com/moabam/api/domain/payment/PaymentStatus.java create mode 100644 src/main/java/com/moabam/api/domain/payment/repository/PaymentRepository.java create mode 100644 src/main/java/com/moabam/api/dto/product/PurchaseProductRequest.java create mode 100644 src/main/java/com/moabam/api/dto/product/PurchaseProductResponse.java create mode 100644 src/test/java/com/moabam/api/domain/payment/OrderTest.java create mode 100644 src/test/java/com/moabam/api/domain/payment/PaymentTest.java create mode 100644 src/test/java/com/moabam/support/fixture/PaymentFixture.java diff --git a/src/main/java/com/moabam/api/application/bug/BugService.java b/src/main/java/com/moabam/api/application/bug/BugService.java index 6093f51d..c7de536b 100644 --- a/src/main/java/com/moabam/api/application/bug/BugService.java +++ b/src/main/java/com/moabam/api/application/bug/BugService.java @@ -3,6 +3,8 @@ import static com.moabam.api.domain.bug.BugActionType.*; import static com.moabam.api.domain.bug.BugType.*; import static com.moabam.api.domain.product.ProductType.*; +import static com.moabam.global.error.model.ErrorMessage.*; +import static java.util.Objects.*; import java.util.List; @@ -10,17 +12,25 @@ import org.springframework.transaction.annotation.Transactional; import com.moabam.api.application.member.MemberService; +import com.moabam.api.application.payment.PaymentMapper; import com.moabam.api.application.product.ProductMapper; import com.moabam.api.domain.bug.BugHistory; import com.moabam.api.domain.bug.BugType; import com.moabam.api.domain.bug.repository.BugHistorySearchRepository; +import com.moabam.api.domain.coupon.Coupon; +import com.moabam.api.domain.coupon.repository.CouponRepository; import com.moabam.api.domain.member.Member; +import com.moabam.api.domain.payment.Payment; +import com.moabam.api.domain.payment.repository.PaymentRepository; import com.moabam.api.domain.product.Product; import com.moabam.api.domain.product.repository.ProductRepository; import com.moabam.api.dto.bug.BugResponse; import com.moabam.api.dto.bug.TodayBugResponse; import com.moabam.api.dto.product.ProductsResponse; +import com.moabam.api.dto.product.PurchaseProductRequest; +import com.moabam.api.dto.product.PurchaseProductResponse; import com.moabam.global.common.util.ClockHolder; +import com.moabam.global.error.exception.NotFoundException; import lombok.RequiredArgsConstructor; @@ -32,6 +42,8 @@ public class BugService { private final MemberService memberService; private final BugHistorySearchRepository bugHistorySearchRepository; private final ProductRepository productRepository; + private final PaymentRepository paymentRepository; + private final CouponRepository couponRepository; private final ClockHolder clockHolder; public BugResponse getBug(Long memberId) { @@ -54,10 +66,31 @@ public ProductsResponse getBugProducts() { return ProductMapper.toProductsResponse(products); } + @Transactional + public PurchaseProductResponse purchaseBugProduct(Long memberId, Long productId, PurchaseProductRequest request) { + Product product = getById(productId); + Payment payment = PaymentMapper.toEntity(memberId, product); + + if (!isNull(request.couponId())) { + // TODO: (임시) CouponWallet 에 존재하는 할인 쿠폰인지 확인 @홍혁준 + Coupon coupon = couponRepository.findById(request.couponId()) + .orElseThrow(() -> new NotFoundException(NOT_FOUND_COUPON)); + payment.applyCoupon(coupon); + } + paymentRepository.save(payment); + + return ProductMapper.toPurchaseProductResponse(payment); + } + private int calculateBugQuantity(List bugHistory, BugType bugType) { return bugHistory.stream() .filter(history -> bugType.equals(history.getBugType())) .mapToInt(BugHistory::getQuantity) .sum(); } + + private Product getById(Long productId) { + return productRepository.findById(productId) + .orElseThrow(() -> new NotFoundException(PRODUCT_NOT_FOUND)); + } } diff --git a/src/main/java/com/moabam/api/application/payment/PaymentMapper.java b/src/main/java/com/moabam/api/application/payment/PaymentMapper.java new file mode 100644 index 00000000..c68b78c6 --- /dev/null +++ b/src/main/java/com/moabam/api/application/payment/PaymentMapper.java @@ -0,0 +1,25 @@ +package com.moabam.api.application.payment; + +import com.moabam.api.domain.payment.Order; +import com.moabam.api.domain.payment.Payment; +import com.moabam.api.domain.product.Product; + +import lombok.AccessLevel; +import lombok.NoArgsConstructor; + +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public final class PaymentMapper { + + public static Payment toEntity(Long memberId, Product product) { + Order order = Order.builder() + .name(product.getName()) + .amount(product.getPrice()) + .build(); + + return Payment.builder() + .memberId(memberId) + .product(product) + .order(order) + .build(); + } +} diff --git a/src/main/java/com/moabam/api/application/product/ProductMapper.java b/src/main/java/com/moabam/api/application/product/ProductMapper.java index 6207cfc0..45434316 100644 --- a/src/main/java/com/moabam/api/application/product/ProductMapper.java +++ b/src/main/java/com/moabam/api/application/product/ProductMapper.java @@ -2,9 +2,11 @@ import java.util.List; +import com.moabam.api.domain.payment.Payment; import com.moabam.api.domain.product.Product; import com.moabam.api.dto.product.ProductResponse; import com.moabam.api.dto.product.ProductsResponse; +import com.moabam.api.dto.product.PurchaseProductResponse; import com.moabam.global.common.util.StreamUtils; import lombok.AccessLevel; @@ -28,4 +30,12 @@ public static ProductsResponse toProductsResponse(List products) { .products(StreamUtils.map(products, ProductMapper::toProductResponse)) .build(); } + + public static PurchaseProductResponse toPurchaseProductResponse(Payment payment) { + return PurchaseProductResponse.builder() + .paymentId(payment.getId()) + .orderName(payment.getOrder().getName()) + .price(payment.getOrder().getAmount()) + .build(); + } } diff --git a/src/main/java/com/moabam/api/domain/payment/Order.java b/src/main/java/com/moabam/api/domain/payment/Order.java new file mode 100644 index 00000000..98f78037 --- /dev/null +++ b/src/main/java/com/moabam/api/domain/payment/Order.java @@ -0,0 +1,50 @@ +package com.moabam.api.domain.payment; + +import static com.moabam.global.error.model.ErrorMessage.*; +import static java.lang.Math.*; +import static java.util.Objects.*; + +import com.moabam.global.error.exception.BadRequestException; + +import jakarta.persistence.Column; +import jakarta.persistence.Embeddable; +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Embeddable +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class Order { + + private static final int MIN_AMOUNT = 0; + + @Column(name = "order_id") + private String id; + + @Column(name = "order_name", nullable = false) + private String name; + + @Column(name = "amount", nullable = false) + private int amount; + + @Builder + private Order(String id, String name, int amount) { + this.id = id; + this.name = requireNonNull(name); + this.amount = validateAmount(amount); + } + + private int validateAmount(int amount) { + if (amount < MIN_AMOUNT) { + throw new BadRequestException(INVALID_ORDER_AMOUNT); + } + + return amount; + } + + public void discountAmount(int price) { + this.amount = max(MIN_AMOUNT, amount - price); + } +} diff --git a/src/main/java/com/moabam/api/domain/payment/Payment.java b/src/main/java/com/moabam/api/domain/payment/Payment.java new file mode 100644 index 00000000..8f5cfff0 --- /dev/null +++ b/src/main/java/com/moabam/api/domain/payment/Payment.java @@ -0,0 +1,73 @@ +package com.moabam.api.domain.payment; + +import static java.util.Objects.*; + +import com.moabam.api.domain.coupon.Coupon; +import com.moabam.api.domain.product.Product; +import com.moabam.global.common.entity.BaseTimeEntity; + +import jakarta.persistence.Column; +import jakarta.persistence.Embedded; +import jakarta.persistence.Entity; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.Table; +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Entity +@Getter +@Table(name = "payment") +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class Payment extends BaseTimeEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "id") + private Long id; + + @Column(name = "member_id", updatable = false, nullable = false) + private Long memberId; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "product_id", updatable = false, nullable = false) + private Product product; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "coupon_id") + private Coupon coupon; + + @Embedded + private Order order; + + @Column(name = "payment_key") + private String paymentKey; + + @Enumerated(value = EnumType.STRING) + @Column(name = "status", nullable = false) + private PaymentStatus status; + + @Builder + public Payment(Long memberId, Product product, Coupon coupon, Order order, String paymentKey, + PaymentStatus status) { + this.memberId = requireNonNull(memberId); + this.product = requireNonNull(product); + this.coupon = coupon; + this.order = requireNonNull(order); + this.paymentKey = paymentKey; + this.status = requireNonNullElse(status, PaymentStatus.REQUEST); + } + + public void applyCoupon(Coupon coupon) { + this.order.discountAmount(coupon.getPoint()); + this.coupon = coupon; + } +} diff --git a/src/main/java/com/moabam/api/domain/payment/PaymentStatus.java b/src/main/java/com/moabam/api/domain/payment/PaymentStatus.java new file mode 100644 index 00000000..37806cb3 --- /dev/null +++ b/src/main/java/com/moabam/api/domain/payment/PaymentStatus.java @@ -0,0 +1,10 @@ +package com.moabam.api.domain.payment; + +public enum PaymentStatus { + + // 임시 + REQUEST, + DONE, + FAIL, + REFUND; +} diff --git a/src/main/java/com/moabam/api/domain/payment/repository/PaymentRepository.java b/src/main/java/com/moabam/api/domain/payment/repository/PaymentRepository.java new file mode 100644 index 00000000..aca0dba9 --- /dev/null +++ b/src/main/java/com/moabam/api/domain/payment/repository/PaymentRepository.java @@ -0,0 +1,9 @@ +package com.moabam.api.domain.payment.repository; + +import org.springframework.data.jpa.repository.JpaRepository; + +import com.moabam.api.domain.payment.Payment; + +public interface PaymentRepository extends JpaRepository { + +} diff --git a/src/main/java/com/moabam/api/dto/product/PurchaseProductRequest.java b/src/main/java/com/moabam/api/dto/product/PurchaseProductRequest.java new file mode 100644 index 00000000..394bbeba --- /dev/null +++ b/src/main/java/com/moabam/api/dto/product/PurchaseProductRequest.java @@ -0,0 +1,9 @@ +package com.moabam.api.dto.product; + +import javax.annotation.Nullable; + +public record PurchaseProductRequest( + @Nullable Long couponId +) { + +} diff --git a/src/main/java/com/moabam/api/dto/product/PurchaseProductResponse.java b/src/main/java/com/moabam/api/dto/product/PurchaseProductResponse.java new file mode 100644 index 00000000..8d74cee1 --- /dev/null +++ b/src/main/java/com/moabam/api/dto/product/PurchaseProductResponse.java @@ -0,0 +1,12 @@ +package com.moabam.api.dto.product; + +import lombok.Builder; + +@Builder +public record PurchaseProductResponse( + Long paymentId, + String orderName, + int price +) { + +} diff --git a/src/main/java/com/moabam/api/presentation/BugController.java b/src/main/java/com/moabam/api/presentation/BugController.java index 95de7f5b..287ce4b8 100644 --- a/src/main/java/com/moabam/api/presentation/BugController.java +++ b/src/main/java/com/moabam/api/presentation/BugController.java @@ -2,6 +2,9 @@ import org.springframework.http.HttpStatus; import org.springframework.web.bind.annotation.GetMapping; +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.RequestMapping; import org.springframework.web.bind.annotation.ResponseStatus; import org.springframework.web.bind.annotation.RestController; @@ -10,6 +13,8 @@ import com.moabam.api.dto.bug.BugResponse; import com.moabam.api.dto.bug.TodayBugResponse; import com.moabam.api.dto.product.ProductsResponse; +import com.moabam.api.dto.product.PurchaseProductRequest; +import com.moabam.api.dto.product.PurchaseProductResponse; import com.moabam.global.auth.annotation.CurrentMember; import com.moabam.global.auth.model.AuthorizationMember; @@ -39,4 +44,12 @@ public TodayBugResponse getTodayBug(@CurrentMember AuthorizationMember member) { public ProductsResponse getBugProducts() { return bugService.getBugProducts(); } + + @PostMapping("/products/{productId}/purchase") + @ResponseStatus(HttpStatus.OK) + public PurchaseProductResponse purchaseBugProduct(@CurrentMember AuthorizationMember member, + @PathVariable Long productId, + @RequestBody PurchaseProductRequest request) { + return bugService.purchaseBugProduct(member.id(), productId, 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 90c493c0..ad826461 100644 --- a/src/main/java/com/moabam/global/error/model/ErrorMessage.java +++ b/src/main/java/com/moabam/global/error/model/ErrorMessage.java @@ -47,6 +47,9 @@ public enum ErrorMessage { INVALID_PRICE("가격은 0 이상이어야 합니다."), INVALID_QUANTITY("수량은 1 이상이어야 합니다."), INVALID_LEVEL("레벨은 1 이상이어야 합니다."), + INVALID_ORDER_AMOUNT("주문 금액은 0 이상이어야 합니다."), + + PRODUCT_NOT_FOUND("존재하지 않는 상품입니다."), FAILED_FCM_INIT("파이어베이스 설정을 실패했습니다."), NOT_FOUND_FCM_TOKEN("해당 유저는 접속 중이 아닙니다."), diff --git a/src/main/resources/static/docs/coupon.html b/src/main/resources/static/docs/coupon.html index 432d567d..dc7b0eb3 100644 --- a/src/main/resources/static/docs/coupon.html +++ b/src/main/resources/static/docs/coupon.html @@ -627,7 +627,7 @@

쿠폰 사용 (진행 중)

diff --git a/src/main/resources/static/docs/index.html b/src/main/resources/static/docs/index.html index b31d0579..3b245d88 100644 --- a/src/main/resources/static/docs/index.html +++ b/src/main/resources/static/docs/index.html @@ -616,7 +616,7 @@

diff --git a/src/main/resources/static/docs/notification.html b/src/main/resources/static/docs/notification.html index d84209a5..0e9f868c 100644 --- a/src/main/resources/static/docs/notification.html +++ b/src/main/resources/static/docs/notification.html @@ -487,7 +487,7 @@

응답

diff --git a/src/test/java/com/moabam/api/application/bug/BugServiceTest.java b/src/test/java/com/moabam/api/application/bug/BugServiceTest.java index 6d65a2e3..06e9a1c3 100644 --- a/src/test/java/com/moabam/api/application/bug/BugServiceTest.java +++ b/src/test/java/com/moabam/api/application/bug/BugServiceTest.java @@ -7,8 +7,10 @@ import static org.mockito.BDDMockito.*; import java.util.List; +import java.util.Optional; import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.InjectMocks; @@ -23,7 +25,9 @@ import com.moabam.api.dto.bug.BugResponse; import com.moabam.api.dto.product.ProductResponse; import com.moabam.api.dto.product.ProductsResponse; +import com.moabam.api.dto.product.PurchaseProductRequest; import com.moabam.global.common.util.StreamUtils; +import com.moabam.global.error.exception.NotFoundException; @ExtendWith(MockitoExtension.class) class BugServiceTest { @@ -71,4 +75,24 @@ void get_bug_products_success() { assertThat(response.products()).hasSize(2); assertThat(productNames).containsExactly(BUG_PRODUCT_NAME, BUG_PRODUCT_NAME); } + + @DisplayName("벌레 상품을 구매한다.") + @Nested + class PurchaseBugProduct { + + @DisplayName("해당 상품이 존재하지 않으면 예외가 발생한다.") + @Test + void product_not_found_exception() { + // given + Long memberId = 1L; + Long productId = 1L; + PurchaseProductRequest request = new PurchaseProductRequest(null); + given(productRepository.findById(productId)).willReturn(Optional.empty()); + + // when, then + assertThatThrownBy(() -> bugService.purchaseBugProduct(memberId, productId, request)) + .isInstanceOf(NotFoundException.class) + .hasMessage("존재하지 않는 상품입니다."); + } + } } diff --git a/src/test/java/com/moabam/api/domain/bug/BugTest.java b/src/test/java/com/moabam/api/domain/bug/BugTest.java index f2720165..7e1981f0 100644 --- a/src/test/java/com/moabam/api/domain/bug/BugTest.java +++ b/src/test/java/com/moabam/api/domain/bug/BugTest.java @@ -2,7 +2,6 @@ import static com.moabam.support.fixture.BugFixture.*; import static org.assertj.core.api.Assertions.*; -import static org.junit.jupiter.api.Assertions.*; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Nested; @@ -42,8 +41,11 @@ void success() { // given Bug bug = bug(); - // when, then - assertDoesNotThrow(() -> bug.use(BugType.MORNING, 5)); + // when + bug.use(BugType.MORNING, 5); + + // then + assertThat(bug.getMorningBug()).isEqualTo(MORNING_BUG - 5); } @DisplayName("벌레 개수가 부족하면 사용할 수 없다.") @@ -71,8 +73,8 @@ void increase_bug_success() { bug.increaseBug(BugType.GOLDEN, 5); // then - assertThat(bug.getMorningBug()).isEqualTo(15); - assertThat(bug.getNightBug()).isEqualTo(25); - assertThat(bug.getGoldenBug()).isEqualTo(35); + assertThat(bug.getMorningBug()).isEqualTo(MORNING_BUG + 5); + assertThat(bug.getNightBug()).isEqualTo(NIGHT_BUG + 5); + assertThat(bug.getGoldenBug()).isEqualTo(GOLDEN_BUG + 5); } } diff --git a/src/test/java/com/moabam/api/domain/payment/OrderTest.java b/src/test/java/com/moabam/api/domain/payment/OrderTest.java new file mode 100644 index 00000000..bf075c4f --- /dev/null +++ b/src/test/java/com/moabam/api/domain/payment/OrderTest.java @@ -0,0 +1,57 @@ +package com.moabam.api.domain.payment; + +import static com.moabam.support.fixture.PaymentFixture.*; +import static com.moabam.support.fixture.ProductFixture.*; +import static org.assertj.core.api.Assertions.*; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +import com.moabam.global.error.exception.BadRequestException; + +class OrderTest { + + @DisplayName("금액이 음수이면 예외가 발생한다.") + @Test + void validate_bug_count_exception() { + Order.OrderBuilder orderBuilder = Order.builder() + .name(BUG_PRODUCT_NAME) + .amount(-1000); + + assertThatThrownBy(orderBuilder::build) + .isInstanceOf(BadRequestException.class) + .hasMessage("주문 금액은 0 이상이어야 합니다."); + } + + @DisplayName("금액을 할인한다.") + @Nested + class Use { + + @DisplayName("성공한다.") + @Test + void success() { + // given + Order order = order(); + + // when + order.discountAmount(1000); + + // then + assertThat(order.getAmount()).isEqualTo(BUG_PRODUCT_PRICE - 1000); + } + + @DisplayName("할인 금액이 주문 금액보다 크면 0으로 처리한다.") + @Test + void discount_amount_greater_than_order_amount() { + // given + Order order = order(); + + // when + order.discountAmount(10000); + + // then + assertThat(order.getAmount()).isZero(); + } + } +} diff --git a/src/test/java/com/moabam/api/domain/payment/PaymentTest.java b/src/test/java/com/moabam/api/domain/payment/PaymentTest.java new file mode 100644 index 00000000..78a6eadd --- /dev/null +++ b/src/test/java/com/moabam/api/domain/payment/PaymentTest.java @@ -0,0 +1,28 @@ +package com.moabam.api.domain.payment; + +import static com.moabam.support.fixture.CouponFixture.*; +import static com.moabam.support.fixture.PaymentFixture.*; +import static org.assertj.core.api.Assertions.*; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import com.moabam.api.domain.coupon.Coupon; + +class PaymentTest { + + @DisplayName("쿠폰을 적용한다.") + @Test + void apply_coupon() { + // given + Payment payment = bugProductPayment(); + Coupon coupon = discount1000Coupon(); + + // when + payment.applyCoupon(coupon); + + // then + assertThat(payment.getOrder().getAmount()).isEqualTo(2000); + assertThat(payment.getCoupon()).isEqualTo(coupon); + } +} diff --git a/src/test/java/com/moabam/api/domain/product/ProductTest.java b/src/test/java/com/moabam/api/domain/product/ProductTest.java index 4e905697..4aa61a90 100644 --- a/src/test/java/com/moabam/api/domain/product/ProductTest.java +++ b/src/test/java/com/moabam/api/domain/product/ProductTest.java @@ -13,7 +13,7 @@ class ProductTest { @Test void validate_price_exception() { Product.ProductBuilder productBuilder = Product.builder() - .name("X10") + .name("황금벌레 10") .price(-10); assertThatThrownBy(productBuilder::build) @@ -25,7 +25,7 @@ void validate_price_exception() { @Test void validate_quantity_exception() { Product.ProductBuilder productBuilder = Product.builder() - .name("X10") + .name("황금벌레 10") .price(1000) .quantity(-1); diff --git a/src/test/java/com/moabam/api/presentation/BugControllerTest.java b/src/test/java/com/moabam/api/presentation/BugControllerTest.java index a2c66b1e..cc754567 100644 --- a/src/test/java/com/moabam/api/presentation/BugControllerTest.java +++ b/src/test/java/com/moabam/api/presentation/BugControllerTest.java @@ -3,6 +3,7 @@ import static com.moabam.global.auth.model.AuthorizationThreadLocal.*; import static com.moabam.support.fixture.BugFixture.*; import static com.moabam.support.fixture.BugHistoryFixture.*; +import static com.moabam.support.fixture.CouponFixture.*; import static com.moabam.support.fixture.MemberFixture.*; import static com.moabam.support.fixture.ProductFixture.*; import static java.nio.charset.StandardCharsets.*; @@ -16,6 +17,7 @@ import java.util.List; import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; @@ -30,11 +32,15 @@ import com.moabam.api.application.product.ProductMapper; import com.moabam.api.domain.bug.repository.BugHistoryRepository; import com.moabam.api.domain.bug.repository.BugHistorySearchRepository; +import com.moabam.api.domain.coupon.Coupon; +import com.moabam.api.domain.coupon.repository.CouponRepository; import com.moabam.api.domain.product.Product; import com.moabam.api.domain.product.repository.ProductRepository; import com.moabam.api.dto.bug.BugResponse; import com.moabam.api.dto.bug.TodayBugResponse; import com.moabam.api.dto.product.ProductsResponse; +import com.moabam.api.dto.product.PurchaseProductRequest; +import com.moabam.api.dto.product.PurchaseProductResponse; import com.moabam.support.annotation.WithMember; import com.moabam.support.common.WithoutFilterSupporter; @@ -61,6 +67,9 @@ class BugControllerTest extends WithoutFilterSupporter { @Autowired ProductRepository productRepository; + @Autowired + CouponRepository couponRepository; + @DisplayName("벌레를 조회한다.") @WithMember @Test @@ -108,12 +117,12 @@ void get_today_bug_success() throws Exception { @DisplayName("벌레 상품 목록을 조회한다.") @Test - void get_products_success() throws Exception { + void get_bug_products_success() throws Exception { // given List products = productRepository.saveAll(List.of(bugProduct(), bugProduct())); ProductsResponse expected = ProductMapper.toProductsResponse(products); - // when, then + // expected String content = mockMvc.perform(get("/bugs/products") .contentType(APPLICATION_JSON)) .andExpect(status().isOk()) @@ -124,4 +133,54 @@ void get_products_success() throws Exception { ProductsResponse actual = objectMapper.readValue(content, ProductsResponse.class); assertThat(actual).isEqualTo(expected); } + + @Nested + @DisplayName("벌레 상품을 구매한다.") + class PurchaseBugProduct { + + @DisplayName("성공한다.") + @WithMember + @Test + void success() throws Exception { + // given + Product product = productRepository.save(bugProduct()); + PurchaseProductRequest request = new PurchaseProductRequest(null); + + // expected + String content = mockMvc.perform(post("/bugs/products/{productId}/purchase", product.getId()) + .contentType(APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andExpect(status().isOk()) + .andDo(print()) + .andReturn() + .getResponse() + .getContentAsString(UTF_8); + PurchaseProductResponse actual = objectMapper.readValue(content, PurchaseProductResponse.class); + assertThat(actual.orderName()).isEqualTo(BUG_PRODUCT_NAME); + assertThat(actual.price()).isEqualTo(BUG_PRODUCT_PRICE); + } + + @DisplayName("쿠폰을 적용하여 성공한다.") + @WithMember + @Test + void with_coupon_success() throws Exception { + // given + Product product = productRepository.save(bugProduct()); + Coupon coupon = couponRepository.save(discount1000Coupon()); + PurchaseProductRequest request = new PurchaseProductRequest(coupon.getId()); + + // expected + String content = mockMvc.perform(post("/bugs/products/{productId}/purchase", product.getId()) + .contentType(APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andExpect(status().isOk()) + .andDo(print()) + .andReturn() + .getResponse() + .getContentAsString(UTF_8); + PurchaseProductResponse actual = objectMapper.readValue(content, PurchaseProductResponse.class); + assertThat(actual.orderName()).isEqualTo(BUG_PRODUCT_NAME); + assertThat(actual.price()).isEqualTo(BUG_PRODUCT_PRICE - 1000); + } + } } diff --git a/src/test/java/com/moabam/support/fixture/CouponFixture.java b/src/test/java/com/moabam/support/fixture/CouponFixture.java index 702d0a7c..5a26e621 100644 --- a/src/test/java/com/moabam/support/fixture/CouponFixture.java +++ b/src/test/java/com/moabam/support/fixture/CouponFixture.java @@ -13,6 +13,11 @@ public final class CouponFixture { + public static final String DISCOUNT_1000_COUPON_NAME = "황금벌레 1000원 할인"; + public static final int DISCOUNT_1000_COUPON_STOCK = 100; + public static final LocalDateTime DISCOUNT_1000_COUPON_START_AT = LocalDateTime.of(2023, 1, 1, 0, 0); + public static final LocalDateTime DISCOUNT_1000_COUPON_END_AT = LocalDateTime.of(2023, 1, 1, 0, 0); + public static Coupon coupon(int point, int stock) { return Coupon.builder() .name("couponName") @@ -37,6 +42,18 @@ public static Coupon coupon(String name, int startMonth, int endMonth) { .build(); } + public static Coupon discount1000Coupon() { + return Coupon.builder() + .name(DISCOUNT_1000_COUPON_NAME) + .point(1000) + .couponType(CouponType.DISCOUNT_COUPON) + .stock(DISCOUNT_1000_COUPON_STOCK) + .startAt(DISCOUNT_1000_COUPON_START_AT) + .endAt(DISCOUNT_1000_COUPON_END_AT) + .adminId(1L) + .build(); + } + public static CreateCouponRequest createCouponRequest(String couponType, int startMonth, int endMonth) { return CreateCouponRequest.builder() .name("couponName") diff --git a/src/test/java/com/moabam/support/fixture/PaymentFixture.java b/src/test/java/com/moabam/support/fixture/PaymentFixture.java new file mode 100644 index 00000000..91de1efe --- /dev/null +++ b/src/test/java/com/moabam/support/fixture/PaymentFixture.java @@ -0,0 +1,24 @@ +package com.moabam.support.fixture; + +import static com.moabam.support.fixture.ProductFixture.*; + +import com.moabam.api.domain.payment.Order; +import com.moabam.api.domain.payment.Payment; + +public final class PaymentFixture { + + public static Payment bugProductPayment() { + return Payment.builder() + .memberId(1L) + .product(bugProduct()) + .order(order()) + .build(); + } + + public static Order order() { + return Order.builder() + .name(BUG_PRODUCT_NAME) + .amount(BUG_PRODUCT_PRICE) + .build(); + } +} diff --git a/src/test/java/com/moabam/support/fixture/ProductFixture.java b/src/test/java/com/moabam/support/fixture/ProductFixture.java index de5bcff6..73cb81cd 100644 --- a/src/test/java/com/moabam/support/fixture/ProductFixture.java +++ b/src/test/java/com/moabam/support/fixture/ProductFixture.java @@ -5,7 +5,7 @@ public class ProductFixture { - public static final String BUG_PRODUCT_NAME = "X10"; + public static final String BUG_PRODUCT_NAME = "황금벌레 10"; public static final int BUG_PRODUCT_PRICE = 3000; public static final int BUG_PRODUCT_QUANTITY = 10;