Skip to content

Commit

Permalink
feat: 결제 승인 기능 구현 (#154)
Browse files Browse the repository at this point in the history
* feat: order_id 컬럼 인덱스 설정

* chore: webflux 의존성 추가

* feat: 토스 결제 위젯 승인 API 연동

* feat: 결제 승인 API 구현

* feat: 결제 테이블에 couponWalletId 컬럼 추가

* test: 결제 승인 통합 테스트

* feat: 벌레 상품 구매 시 couponWallet 검증 로직 적용

* fix: couponWalletId를 받도록 수정

* test: couponWallet 적용 테스트

* chore: 불필요한 fixture 제거

* feat: 결제 승인 시 쿠폰 차감 및 벌레 충전 로직 추가

* fix: 쿠폰이 적용된 경우 분기 처리

* chore: config 업데이트

* test: 결제 승인 컨트롤러 통합 테스트

* test: 결제 승인 서비스 테스트

* chore: MockWebServer 의존성 추가

* test: 토스 결제 승인 API 테스트

* fix: checkStyle 오류 수정

* chore: config 업데이트

* refactor: 결제 테이블 coupon_id 컬럼을 discount_amount로 변경

* refactor: 공통 메서드 분리

* feat: 벌레 충전 시 벌레 내역 저장 로직 추가

* style: 중복 메서드 제거
  • Loading branch information
kmebin authored Nov 27, 2023
1 parent 78d4738 commit 99e9afe
Show file tree
Hide file tree
Showing 32 changed files with 605 additions and 66 deletions.
4 changes: 4 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ dependencies {

// Test
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation 'com.squareup.okhttp3:mockwebserver:4.11.0'

// Querydsl
implementation 'com.querydsl:querydsl-jpa:5.0.0:jakarta'
Expand Down Expand Up @@ -92,6 +93,9 @@ dependencies {
// S3
implementation platform("io.awspring.cloud:spring-cloud-aws-dependencies:3.0.2")
implementation 'io.awspring.cloud:spring-cloud-aws-starter-s3'

// webflux
implementation 'org.springframework.boot:spring-boot-starter-webflux'
}

tasks.named('test') {
Expand Down
9 changes: 9 additions & 0 deletions src/main/java/com/moabam/api/application/bug/BugMapper.java
Original file line number Diff line number Diff line change
Expand Up @@ -28,4 +28,13 @@ public static BugHistory toUseBugHistory(Long memberId, BugType bugType, int qua
.quantity(quantity)
.build();
}

public static BugHistory toChargeBugHistory(Long memberId, int quantity) {
return BugHistory.builder()
.memberId(memberId)
.bugType(BugType.GOLDEN)
.actionType(BugActionType.CHARGE)
.quantity(quantity)
.build();
}
}
16 changes: 13 additions & 3 deletions src/main/java/com/moabam/api/application/bug/BugService.java
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,9 @@
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.Bug;
import com.moabam.api.domain.bug.repository.BugHistoryRepository;
import com.moabam.api.domain.coupon.Coupon;
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;
Expand All @@ -34,13 +35,14 @@ public class BugService {

private final MemberService memberService;
private final CouponService couponService;
private final BugHistoryRepository bugHistoryRepository;
private final ProductRepository productRepository;
private final PaymentRepository paymentRepository;

public BugResponse getBug(Long memberId) {
Member member = memberService.getById(memberId);
Bug bug = memberService.getById(memberId).getBug();

return BugMapper.toBugResponse(member.getBug());
return BugMapper.toBugResponse(bug);
}

public ProductsResponse getBugProducts() {
Expand All @@ -63,6 +65,14 @@ public PurchaseProductResponse purchaseBugProduct(Long memberId, Long productId,
return ProductMapper.toPurchaseProductResponse(payment);
}

@Transactional
public void charge(Long memberId, Product bugProduct) {
Bug bug = memberService.getById(memberId).getBug();

bug.charge(bugProduct.getQuantity());
bugHistoryRepository.save(BugMapper.toChargeBugHistory(memberId, bugProduct.getQuantity()));
}

private Product getById(Long productId) {
return productRepository.findById(productId)
.orElseThrow(() -> new NotFoundException(PRODUCT_NOT_FOUND));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
import com.moabam.api.domain.coupon.CouponWallet;
import com.moabam.api.domain.coupon.repository.CouponRepository;
import com.moabam.api.domain.coupon.repository.CouponSearchRepository;
import com.moabam.api.domain.coupon.repository.CouponWalletRepository;
import com.moabam.api.domain.coupon.repository.CouponWalletSearchRepository;
import com.moabam.api.domain.member.Role;
import com.moabam.api.dto.coupon.CouponResponse;
Expand All @@ -32,9 +33,9 @@ public class CouponService {

private final ClockHolder clockHolder;
private final CouponManageService couponManageService;

private final CouponRepository couponRepository;
private final CouponSearchRepository couponSearchRepository;
private final CouponWalletRepository couponWalletRepository;
private final CouponWalletSearchRepository couponWalletSearchRepository;

@Transactional
Expand All @@ -57,6 +58,12 @@ public void delete(AuthMember admin, Long couponId) {
couponManageService.deleteCouponManage(coupon.getName());
}

@Transactional
public void use(Long memberId, Long couponWalletId) {
Coupon coupon = getByWalletIdAndMemberId(couponWalletId, memberId);
couponRepository.delete(coupon);
}

public CouponResponse getById(Long couponId) {
Coupon coupon = couponRepository.findById(couponId)
.orElseThrow(() -> new NotFoundException(ErrorMessage.NOT_FOUND_COUPON));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ public static Payment toPayment(Long memberId, Product product) {
.memberId(memberId)
.product(product)
.order(order)
.amount(product.getPrice())
.totalAmount(product.getPrice())
.build();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,17 @@
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import com.moabam.api.application.bug.BugService;
import com.moabam.api.application.coupon.CouponService;
import com.moabam.api.domain.payment.Payment;
import com.moabam.api.domain.payment.repository.PaymentRepository;
import com.moabam.api.domain.payment.repository.PaymentSearchRepository;
import com.moabam.api.dto.payment.ConfirmPaymentRequest;
import com.moabam.api.dto.payment.ConfirmTossPaymentResponse;
import com.moabam.api.dto.payment.PaymentRequest;
import com.moabam.api.infrastructure.payment.TossPaymentMapper;
import com.moabam.api.infrastructure.payment.TossPaymentService;
import com.moabam.global.error.exception.MoabamException;
import com.moabam.global.error.exception.NotFoundException;

import lombok.RequiredArgsConstructor;
Expand All @@ -17,7 +25,11 @@
@RequiredArgsConstructor
public class PaymentService {

private final BugService bugService;
private final CouponService couponService;
private final TossPaymentService tossPaymentService;
private final PaymentRepository paymentRepository;
private final PaymentSearchRepository paymentSearchRepository;

@Transactional
public void request(Long memberId, Long paymentId, PaymentRequest request) {
Expand All @@ -26,8 +38,33 @@ public void request(Long memberId, Long paymentId, PaymentRequest request) {
payment.request(request.orderId());
}

@Transactional
public void confirm(Long memberId, ConfirmPaymentRequest request) {
Payment payment = getByOrderId(request.orderId());
payment.validateInfo(memberId, request.amount());

try {
ConfirmTossPaymentResponse response = tossPaymentService.confirm(
TossPaymentMapper.toConfirmRequest(request.paymentKey(), request.orderId(), request.amount())
);
payment.confirm(response.paymentKey(), response.approvedAt());

if (payment.isCouponApplied()) {
couponService.use(memberId, payment.getCouponWalletId());
}
bugService.charge(memberId, payment.getProduct());
} catch (MoabamException exception) {
payment.fail(request.paymentKey());
}
}

private Payment getById(Long paymentId) {
return paymentRepository.findById(paymentId)
.orElseThrow(() -> new NotFoundException(PAYMENT_NOT_FOUND));
}

private Payment getByOrderId(String orderId) {
return paymentSearchRepository.findByOrderId(orderId)
.orElseThrow(() -> new NotFoundException(PAYMENT_NOT_FOUND));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ public static PurchaseProductResponse toPurchaseProductResponse(Payment payment)
return PurchaseProductResponse.builder()
.paymentId(payment.getId())
.orderName(payment.getOrder().getName())
.price(payment.getAmount())
.price(payment.getTotalAmount())
.build();
}
}
Original file line number Diff line number Diff line change
@@ -1,9 +1,6 @@
package com.moabam.api.application.room;

import static com.moabam.global.error.model.ErrorMessage.DUPLICATED_DAILY_MEMBER_CERTIFICATION;
import static com.moabam.global.error.model.ErrorMessage.INVALID_CERTIFY_TIME;
import static com.moabam.global.error.model.ErrorMessage.PARTICIPANT_NOT_FOUND;
import static com.moabam.global.error.model.ErrorMessage.ROUTINE_NOT_FOUND;
import static com.moabam.global.error.model.ErrorMessage.*;

import java.time.LocalDate;
import java.time.LocalDateTime;
Expand Down Expand Up @@ -81,7 +78,7 @@ public void certifyRoom(Long memberId, Long roomId, List<String> imageUrls) {
return;
}

member.getBug().increaseBug(bugType, roomLevel);
member.getBug().increase(bugType, roomLevel);
}

public boolean existsMemberCertification(Long memberId, Long roomId, LocalDate date) {
Expand Down Expand Up @@ -175,6 +172,6 @@ private void provideBugToCompletedMembers(BugType bugType, List<DailyMemberCerti
.toList();

memberService.getRoomMembers(memberIds)
.forEach(completedMember -> completedMember.getBug().increaseBug(bugType, expAppliedRoomLevel));
.forEach(completedMember -> completedMember.getBug().increase(bugType, expAppliedRoomLevel));
}
}
10 changes: 7 additions & 3 deletions src/main/java/com/moabam/api/domain/bug/Bug.java
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ private int validateBugCount(int bug) {
public void use(BugType bugType, int price) {
int currentBug = getBug(bugType);
validateEnoughBug(currentBug, price);
decreaseBug(bugType, price);
decrease(bugType, price);
}

private int getBug(BugType bugType) {
Expand All @@ -65,19 +65,23 @@ private void validateEnoughBug(int currentBug, int price) {
}
}

private void decreaseBug(BugType bugType, int bug) {
private void decrease(BugType bugType, int bug) {
switch (bugType) {
case MORNING -> this.morningBug -= bug;
case NIGHT -> this.nightBug -= bug;
case GOLDEN -> this.goldenBug -= bug;
}
}

public void increaseBug(BugType bugType, int bug) {
public void increase(BugType bugType, int bug) {
switch (bugType) {
case MORNING -> this.morningBug += bug;
case NIGHT -> this.nightBug += bug;
case GOLDEN -> this.goldenBug += bug;
}
}

public void charge(int quantity) {
this.goldenBug += quantity;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,7 @@ public class BugHistorySearchRepository {
private final JPAQueryFactory jpaQueryFactory;

public List<BugHistory> find(Long memberId, BugActionType actionType, LocalDateTime dateTime) {
return jpaQueryFactory
.selectFrom(bugHistory)
return jpaQueryFactory.selectFrom(bugHistory)
.where(
DynamicQuery.generateEq(memberId, bugHistory.memberId::eq),
DynamicQuery.generateEq(actionType, bugHistory.actionType::eq),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,23 +23,23 @@ public class InventorySearchRepository {
private final JPAQueryFactory jpaQueryFactory;

public Optional<Inventory> findOne(Long memberId, Long itemId) {
return Optional.ofNullable(
jpaQueryFactory.selectFrom(inventory)
.where(
DynamicQuery.generateEq(memberId, inventory.memberId::eq),
DynamicQuery.generateEq(itemId, inventory.item.id::eq))
.fetchOne()
return Optional.ofNullable(jpaQueryFactory
.selectFrom(inventory)
.where(
DynamicQuery.generateEq(memberId, inventory.memberId::eq),
DynamicQuery.generateEq(itemId, inventory.item.id::eq))
.fetchOne()
);
}

public Optional<Inventory> findDefault(Long memberId, ItemType type) {
return Optional.ofNullable(
jpaQueryFactory.selectFrom(inventory)
.where(
DynamicQuery.generateEq(memberId, inventory.memberId::eq),
DynamicQuery.generateEq(type, inventory.item.type::eq),
inventory.isDefault.isTrue())
.fetchOne()
return Optional.ofNullable(jpaQueryFactory
.selectFrom(inventory)
.where(
DynamicQuery.generateEq(memberId, inventory.memberId::eq),
DynamicQuery.generateEq(type, inventory.item.type::eq),
inventory.isDefault.isTrue())
.fetchOne()
);
}

Expand Down
49 changes: 38 additions & 11 deletions src/main/java/com/moabam/api/domain/payment/Payment.java
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
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 java.time.LocalDateTime;
Expand Down Expand Up @@ -53,18 +52,17 @@ public class Payment {
@JoinColumn(name = "product_id", updatable = false, nullable = false)
private Product product;

@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "coupon_id")
private Coupon coupon;

@Column(name = "coupon_wallet_id")
private Long couponWalletId;

@Embedded
private Order order;

@Column(name = "amount", nullable = false)
private int amount;
@Column(name = "total_amount", nullable = false)
private int totalAmount;

@Column(name = "discount_amount", nullable = false)
private int discountAmount;

@Column(name = "payment_key")
private String paymentKey;
Expand All @@ -84,11 +82,14 @@ public class Payment {
private LocalDateTime approvedAt;

@Builder
public Payment(Long memberId, Product product, Order order, int amount, PaymentStatus status) {
public Payment(Long memberId, Product product, Long couponWalletId, Order order, int totalAmount,
int discountAmount, PaymentStatus status) {
this.memberId = requireNonNull(memberId);
this.product = requireNonNull(product);
this.couponWalletId = couponWalletId;
this.order = requireNonNull(order);
this.amount = validateAmount(amount);
this.totalAmount = validateAmount(totalAmount);
this.discountAmount = validateAmount(discountAmount);
this.status = requireNonNullElse(status, PaymentStatus.READY);
}

Expand All @@ -100,20 +101,46 @@ private int validateAmount(int amount) {
return amount;
}

public void validateInfo(Long memberId, int amount) {
validateByMember(memberId);
validateByTotalAmount(amount);
}

public void validateByMember(Long memberId) {
if (!this.memberId.equals(memberId)) {
throw new BadRequestException(INVALID_MEMBER_PAYMENT);
}
}

private void validateByTotalAmount(int amount) {
if (this.totalAmount != amount) {
throw new BadRequestException(INVALID_PAYMENT_INFO);
}
}

public boolean isCouponApplied() {
return !isNull(this.couponWalletId);
}

public void applyCoupon(Coupon coupon, Long couponWalletId) {
this.coupon = coupon;
this.couponWalletId = couponWalletId;
this.amount = max(MIN_AMOUNT, this.amount - coupon.getPoint());
this.discountAmount = coupon.getPoint();
this.totalAmount = Math.max(MIN_AMOUNT, this.totalAmount - coupon.getPoint());
}

public void request(String orderId) {
this.order.updateId(orderId);
this.requestedAt = LocalDateTime.now();
}

public void confirm(String paymentKey, LocalDateTime approvedAt) {
this.paymentKey = paymentKey;
this.approvedAt = approvedAt;
this.status = PaymentStatus.DONE;
}

public void fail(String paymentKey) {
this.paymentKey = paymentKey;
this.status = PaymentStatus.ABORTED;
}
}
Loading

0 comments on commit 99e9afe

Please sign in to comment.