Skip to content

Commit

Permalink
feat: 벌레 상품 구매 기능 구현 (#107)
Browse files Browse the repository at this point in the history
* feat: 결제 엔티티 생성

* feat: 벌레 상품 구매 API 구현

* test: 벌레 상품 구매 통합 테스트

* test: 벌레 상품 구매 서비스 테스트

* test: 결제 쿠폰 적용 테스트

* test: 주문 생성 및 금액 할인 테스트

* test: 벌레 사용 및 증가 로직 검증 방식 수정

* chore: config 업데이트

* fix: 상품 구매 Response에 주문 id 제거

* feat: 상품 구매 Response에 결제 id 추가

* fix: Transactional 적용
  • Loading branch information
kmebin authored Nov 20, 2023
1 parent a4c2f2f commit 8d16e9d
Show file tree
Hide file tree
Showing 23 changed files with 472 additions and 14 deletions.
33 changes: 33 additions & 0 deletions src/main/java/com/moabam/api/application/bug/BugService.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,24 +3,34 @@
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;

import org.springframework.stereotype.Service;
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;

Expand All @@ -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) {
Expand All @@ -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> 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));
}
}
Original file line number Diff line number Diff line change
@@ -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();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -28,4 +30,12 @@ public static ProductsResponse toProductsResponse(List<Product> 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();
}
}
50 changes: 50 additions & 0 deletions src/main/java/com/moabam/api/domain/payment/Order.java
Original file line number Diff line number Diff line change
@@ -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);
}
}
73 changes: 73 additions & 0 deletions src/main/java/com/moabam/api/domain/payment/Payment.java
Original file line number Diff line number Diff line change
@@ -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;
}
}
10 changes: 10 additions & 0 deletions src/main/java/com/moabam/api/domain/payment/PaymentStatus.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.moabam.api.domain.payment;

public enum PaymentStatus {

// 임시
REQUEST,
DONE,
FAIL,
REFUND;
}
Original file line number Diff line number Diff line change
@@ -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<Payment, Long> {

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.moabam.api.dto.product;

import javax.annotation.Nullable;

public record PurchaseProductRequest(
@Nullable Long couponId
) {

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package com.moabam.api.dto.product;

import lombok.Builder;

@Builder
public record PurchaseProductResponse(
Long paymentId,
String orderName,
int price
) {

}
13 changes: 13 additions & 0 deletions src/main/java/com/moabam/api/presentation/BugController.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;

Expand Down Expand Up @@ -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);
}
}
3 changes: 3 additions & 0 deletions src/main/java/com/moabam/global/error/model/ErrorMessage.java
Original file line number Diff line number Diff line change
Expand Up @@ -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("해당 유저는 접속 중이 아닙니다."),
Expand Down
2 changes: 1 addition & 1 deletion src/main/resources/static/docs/coupon.html
Original file line number Diff line number Diff line change
Expand Up @@ -627,7 +627,7 @@ <h3 id="_쿠폰_사용_진행_중">쿠폰 사용 (진행 중)</h3>
<div id="footer">
<div id="footer-text">
Version 0.0.1-SNAPSHOT<br>
Last updated 2023-11-16 18:30:03 +0900
Last updated 2023-11-13 18:48:03 +0900
</div>
</div>
</body>
Expand Down
2 changes: 1 addition & 1 deletion src/main/resources/static/docs/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -616,7 +616,7 @@ <h4 id="_상태코드httpstatus"><a class="link" href="#_상태코드httpstatus"
<div id="footer">
<div id="footer-text">
Version 0.0.1-SNAPSHOT<br>
Last updated 2023-11-16 18:30:03 +0900
Last updated 2023-11-13 18:48:03 +0900
</div>
</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/9.18.3/highlight.min.js"></script>
Expand Down
2 changes: 1 addition & 1 deletion src/main/resources/static/docs/notification.html
Original file line number Diff line number Diff line change
Expand Up @@ -487,7 +487,7 @@ <h4 id="_응답" class="discrete">응답</h4>
<div id="footer">
<div id="footer-text">
Version 0.0.1-SNAPSHOT<br>
Last updated 2023-11-16 18:30:04 +0900
Last updated 2023-11-13 18:48:03 +0900
</div>
</div>
</body>
Expand Down
Loading

0 comments on commit 8d16e9d

Please sign in to comment.