Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: 무료 주문 생성하기 API 구현 #520

Merged
merged 16 commits into from
Jul 26, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ private Money(BigDecimal amount) {
this.amount = amount;
}

public static final Money ZERO = Money.from(BigDecimal.ZERO);

public static Money from(BigDecimal amount) {
validateAmountNotNull(amount);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import java.math.BigDecimal;
import lombok.AccessLevel;
import lombok.Builder;
import lombok.Getter;
Expand Down Expand Up @@ -46,7 +45,7 @@ public static Coupon createCoupon(String name, Money discountAmount) {
// 검증 로직

private static void validateDiscountAmountPositive(Money discountAmount) {
if (!discountAmount.isGreaterThan(Money.from(BigDecimal.ZERO))) {
if (!discountAmount.isGreaterThan(Money.ZERO)) {
throw new CustomException(COUPON_DISCOUNT_AMOUNT_NOT_POSITIVE);
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,15 +37,15 @@ public ResponseEntity<Page<OrderAdminResponse>> getOrders(
}

@Operation(
summary = "완료된 주문 결제정보 조회하기",
description = "주문 결제정보를 조회합니다. 토스페이먼츠 API의 결제 정보인 Payment 객체를 반환합니다. 완료된 주문에 대해서만 조회 가능합니다.")
summary = "완료된 유료 주문 결제정보 조회하기",
description = "주문 결제정보를 조회합니다. 토스페이먼츠 API의 결제 정보인 Payment 객체를 반환합니다. 완료된 유료 주문만 조회할 수 있습니다")
@GetMapping("/{orderId}")
public ResponseEntity<PaymentResponse> getCompletedOrderPayment(@PathVariable Long orderId) {
var response = orderService.getCompletedOrderPayment(orderId);
var response = orderService.getCompletedPaidOrderPayment(orderId);
return ResponseEntity.ok(response);
}

@Operation(summary = "주문 결제 취소하기", description = "주문 상태를 취소로 변경하고 결제를 취소합니다.")
@Operation(summary = "주문 결제 취소하기", description = "주문 상태를 취소로 변경하고 결제를 취소합니다. 회비납입상태를 대기로 변경하고, 준회원으로 강등합니다.")
@PostMapping("/{orderId}/cancel")
public ResponseEntity<Void> cancelOrder(
@PathVariable Long orderId, @Valid @RequestBody OrderCancelRequest request) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,17 @@ public ResponseEntity<Void> createPendingOrder(@Valid @RequestBody OrderCreateRe
return ResponseEntity.ok().build();
}

@Operation(summary = "주문 완료하기", description = "주문을 완료합니다. 요청된 결제는 승인됩니다.")
@Operation(summary = "주문 완료", description = "임시 주문을 완료합니다. 요청된 결제는 승인됩니다.")
@PostMapping("/complete")
public ResponseEntity<Void> completeOrder(@Valid @RequestBody OrderCompleteRequest request) {
orderService.completeOrder(request);
return ResponseEntity.ok().build();
}

@Operation(summary = "무료 주문 생성", description = "무료 주문을 생성합니다. 무료 주문은 완료된 상태로 생성됩니다.")
@PostMapping("/free")
public ResponseEntity<Void> createFreeOrder(@Valid @RequestBody OrderCreateRequest request) {
orderService.createFreeOrder(request);
return ResponseEntity.ok().build();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -107,11 +107,12 @@ public Page<OrderAdminResponse> searchOrders(OrderQueryOption queryOption, Pagea
}

@Transactional(readOnly = true)
public PaymentResponse getCompletedOrderPayment(Long orderId) {
public PaymentResponse getCompletedPaidOrderPayment(Long orderId) {
Order order = orderRepository
.findById(orderId)
.filter(Order::isCompleted)
.orElseThrow(() -> new CustomException(ORDER_COMPLETED_NOT_FOUND));
.filter(o -> !o.isFree())
.orElseThrow(() -> new CustomException(ORDER_COMPLETED_PAID_NOT_FOUND));

return paymentClient.getPayment(order.getPaymentKey());
}
Expand Down Expand Up @@ -141,4 +142,29 @@ private ZonedDateTime getCanceledAt(PaymentResponse response) {
private Optional<ZonedDateTime> findLatestCancelDate(List<PaymentResponse.CancelDto> cancels) {
return cancels.stream().map(PaymentResponse.CancelDto::canceledAt).max(ZonedDateTime::compareTo);
}

@Transactional
public void createFreeOrder(OrderCreateRequest request) {
Membership membership = membershipRepository
.findById(request.membershipId())
.orElseThrow(() -> new CustomException(MEMBERSHIP_NOT_FOUND));

Optional<IssuedCoupon> issuedCoupon =
Optional.ofNullable(request.issuedCouponId()).map(this::getIssuedCoupon);

MoneyInfo moneyInfo = MoneyInfo.of(
Money.from(request.totalAmount()),
Money.from(request.discountAmount()),
Money.from(request.finalPaymentAmount()));

Member currentMember = memberUtil.getCurrentMember();

orderValidator.validateFreeOrderCreate(membership, issuedCoupon, currentMember);

Order order = Order.createFree(request.orderNanoId(), membership, issuedCoupon.orElse(null), moneyInfo);

orderRepository.save(order);

log.info("[OrderService] 무료 주문 생성: orderId={}", order.getId());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -58,4 +58,8 @@ private static void validateFinalPaymentAmount(Money totalAmount, Money discount
throw new CustomException(ErrorCode.ORDER_FINAL_PAYMENT_AMOUNT_MISMATCH);
}
}

public boolean isFree() {
return finalPaymentAmount.equals(Money.ZERO);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,26 @@ public static Order createPending(
.build();
}

public static Order createFree(
String nanoId, Membership membership, @Nullable IssuedCoupon issuedCoupon, MoneyInfo moneyInfo) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

무료 주문이면 쿠폰이 반드시 필요한거아닌가요??

Copy link
Member Author

@uwoobeat uwoobeat Jul 26, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

회비가 0원인 경우에는 쿠폰 없이 무료 주문이 가능하다고 봤어요
물론 회비를 무조건 양수로 지정하면 항상 쿠폰이 필요해지겠지만
쿠폰을 꼭 써야 무료 주문이다 보다는 최종결제금액이 0원이면 무료 주문이다 가 더 의미적으로 적절하다고 봤습니다

validateFreeOrder(moneyInfo);
return Order.builder()
.status(OrderStatus.COMPLETED)
.nanoId(nanoId)
.memberId(membership.getMember().getId())
.membershipId(membership.getId())
.recruitmentRoundId(membership.getRecruitmentRound().getId())
.issuedCouponId(issuedCoupon != null ? issuedCoupon.getId() : null)
.moneyInfo(moneyInfo)
.build();
}

private static void validateFreeOrder(MoneyInfo moneyInfo) {
if (!moneyInfo.isFree()) {
throw new CustomException(ORDER_FREE_FINAL_PAYMENT_NOT_ZERO);
}
}

// 데이터 변경 로직

/**
Expand All @@ -119,6 +139,7 @@ public void complete(String paymentKey, ZonedDateTime approvedAt) {
* 상태 변경 및 취소 시각을 저장하며, 예외를 발생시키지 않도록 외부 취소 요청 전에 validateCancelable을 호출합니다.
*/
public void cancel(ZonedDateTime canceledAt) {
// TODO: 취소 이벤트 발행을 통해 멤버십 및 멤버 상태에 대한 변경 로직 추가
validateCancelable();
this.status = OrderStatus.CANCELED;
this.canceledAt = canceledAt;
Expand All @@ -128,11 +149,19 @@ public void validateCancelable() {
if (status != OrderStatus.COMPLETED) {
throw new CustomException(ORDER_CANCEL_NOT_COMPLETED);
}

if (isFree()) {
throw new CustomException(ORDER_CANCEL_FREE_ORDER);
}
}

// 데이터 조회 로직

public boolean isCompleted() {
return status == OrderStatus.COMPLETED;
}

public boolean isFree() {
return moneyInfo.isFree();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@
import com.gdschongik.gdsc.global.annotation.DomainService;
import com.gdschongik.gdsc.global.exception.CustomException;
import jakarta.annotation.Nullable;
import java.math.BigDecimal;
import java.util.Optional;

@DomainService
Expand Down Expand Up @@ -68,7 +67,7 @@ private void validateIssuedCouponOwnership(IssuedCoupon issuedCoupon, Member cur
}

private void validateDiscountAmountZero(Money discountAmount) {
if (!discountAmount.equals(Money.from(BigDecimal.ZERO))) {
if (!discountAmount.equals(Money.ZERO)) {
throw new CustomException(ORDER_DISCOUNT_AMOUNT_NOT_ZERO);
}
}
Expand Down Expand Up @@ -99,4 +98,35 @@ public void validateCompleteOrder(
throw new CustomException(ORDER_COMPLETE_AMOUNT_MISMATCH);
}
}

public void validateFreeOrderCreate(
Membership membership, Optional<IssuedCoupon> optionalIssuedCoupon, Member currentMember) {
// TODO: 공통 로직으로 추출

// 멤버십 관련 검증

if (!membership.getMember().getId().equals(currentMember.getId())) {
throw new CustomException(ORDER_MEMBERSHIP_MEMBER_MISMATCH);
}

if (membership.getRegularRequirement().isPaymentSatisfied()) {
throw new CustomException(ORDER_MEMBERSHIP_ALREADY_PAID);
}

// 리쿠르팅 관련 검증

RecruitmentRound recruitmentRound = membership.getRecruitmentRound();

if (!recruitmentRound.isOpen()) {
throw new CustomException(ORDER_RECRUITMENT_PERIOD_INVALID);
}

// 발급쿠폰 관련 검증

if (optionalIssuedCoupon.isPresent()) {
var issuedCoupon = optionalIssuedCoupon.get();
validateIssuedCouponOwnership(issuedCoupon, currentMember);
issuedCoupon.validateUsable();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -117,10 +117,12 @@ public enum ErrorCode {
ORDER_ALREADY_COMPLETED(HttpStatus.CONFLICT, "이미 완료된 주문입니다."),
ORDER_COMPLETE_AMOUNT_MISMATCH(HttpStatus.CONFLICT, "주문 최종결제금액이 주문완료요청의 결제금액과 일치하지 않습니다."),
ORDER_COMPLETE_MEMBER_MISMATCH(HttpStatus.CONFLICT, "주문자와 현재 로그인한 멤버가 일치하지 않습니다."),
ORDER_COMPLETED_NOT_FOUND(HttpStatus.NOT_FOUND, "존재하지 않는 주문이거나, 완료되지 않은 주문입니다."),
ORDER_COMPLETED_PAID_NOT_FOUND(HttpStatus.NOT_FOUND, "존재하지 않는 주문이거나, 완료되지 않은 유료 주문입니다."),
ORDER_CANCEL_NOT_COMPLETED(HttpStatus.CONFLICT, "완료되지 않은 주문은 취소할 수 없습니다."),
ORDER_CANCEL_FREE_ORDER(HttpStatus.CONFLICT, "무료 주문은 취소할 수 없습니다."),
ORDER_CANCEL_RESPONSE_NOT_FOUND(
HttpStatus.INTERNAL_SERVER_ERROR, "주문 결제가 취소되었지만, 응답에 취소 정보가 존재하지 않습니다. 관리자에게 문의 바랍니다."),
ORDER_FREE_FINAL_PAYMENT_NOT_ZERO(HttpStatus.CONFLICT, "무료 주문의 최종결제금액은 0원이어야 합니다."),

// Order - MoneyInfo
ORDER_FINAL_PAYMENT_AMOUNT_MISMATCH(HttpStatus.CONFLICT, "주문 최종결제금액은 주문총액에서 할인금액을 뺀 값이어야 합니다."),
Expand Down
Loading