diff --git a/src/main/java/poomasi/domain/order/_aftersales/controller/AfterSalesController.java b/src/main/java/poomasi/domain/order/_aftersales/controller/AfterSalesController.java new file mode 100644 index 00000000..4d4d8c2e --- /dev/null +++ b/src/main/java/poomasi/domain/order/_aftersales/controller/AfterSalesController.java @@ -0,0 +1,80 @@ +package poomasi.domain.order._aftersales.controller; + + +import com.siot.IamportRestClient.exception.IamportResponseException; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.security.access.annotation.Secured; +import org.springframework.web.bind.annotation.*; +import poomasi.domain.order._aftersales.dto.cancel.request.FarmCancelRequest; +import poomasi.domain.order._aftersales.dto.cancel.request.ProductCancelRequest; +import poomasi.domain.order._aftersales.dto.refund.request.ProductRefundRequest; +import poomasi.domain.order._aftersales.dto.refund.request.ProductRefundRequestApprovalRequest; +import poomasi.domain.order._aftersales.dto.refund.request.ProductRefundRequestDeniedRequest; +import poomasi.domain.order._aftersales.service.FarmAfterSalesService; +import poomasi.domain.order._aftersales.service.ProductAfterSalesService; + +import java.io.IOException; + +@RestController +@RequestMapping("/api/aftersales") +@RequiredArgsConstructor +public class AfterSalesController { + + private final ProductAfterSalesService productAfterSalesService; + private final FarmAfterSalesService farmAfterSalesService; + + //-------------------------product cancel---------------------// + @Secured({"ROLE_CUSTOMER", "ROLE_FARMER"}) + @PostMapping("/product/cancel") + public ResponseEntity productCancel(@RequestBody ProductCancelRequest productCancelRequest) throws IOException, IamportResponseException { + return ResponseEntity.ok( + productAfterSalesService.cancel(productCancelRequest) + ); + } + + //-------------------------product refund---------------------// + @Secured({"ROLE_CUSTOMER", "ROLE_FARMER"}) + @PostMapping("/refund-request") + public ResponseEntity requestRefund(@RequestBody ProductRefundRequest productRefundRequest) { + return ResponseEntity.ok( + productAfterSalesService. + createRefundRequest(productRefundRequest) + ); + } + + @Secured({"ROLE_FARMER"}) + @PostMapping("/approve-refund-request") + public ResponseEntity approveRefundRequest(@RequestBody ProductRefundRequestApprovalRequest productRefundRequestApprovalRequest) throws IOException, IamportResponseException { + return ResponseEntity.ok( + productAfterSalesService.processRefundApproval(productRefundRequestApprovalRequest) + ); + } + + + @Secured({"ROLE_FARMER"}) + @PostMapping("/deniedrefund-request") + public ResponseEntity deniedRefundRequest(@RequestBody ProductRefundRequestDeniedRequest productRefundRequestDeniedRequest) { + return ResponseEntity.ok( + productAfterSalesService.processRefundDenied(productRefundRequestDeniedRequest) + ); + } + + + //-------------------------farm cancel---------------------// + @Secured({"ROLE_CUSTOMER", "ROLE_FARMER"}) + @PostMapping("/farm/cancel") + public ResponseEntity farmCancel(@RequestBody FarmCancelRequest farmCancelRequest) throws IOException, IamportResponseException { + return ResponseEntity.ok( + farmAfterSalesService.farmCancel(farmCancelRequest) + ); + } + + + //------------웹훅 api 받아서 해야 함---------// + + + + + +} diff --git a/src/main/java/poomasi/domain/order/_aftersales/controller/CancelController.java b/src/main/java/poomasi/domain/order/_aftersales/controller/CancelController.java deleted file mode 100644 index 9691382a..00000000 --- a/src/main/java/poomasi/domain/order/_aftersales/controller/CancelController.java +++ /dev/null @@ -1,8 +0,0 @@ -package poomasi.domain.order._aftersales.controller; - - -import org.springframework.web.bind.annotation.RestController; - -@RestController -public class CancelController { -} diff --git a/src/main/java/poomasi/domain/order/_aftersales/controller/ExchangeController.java b/src/main/java/poomasi/domain/order/_aftersales/controller/ExchangeController.java deleted file mode 100644 index 7d22e7fd..00000000 --- a/src/main/java/poomasi/domain/order/_aftersales/controller/ExchangeController.java +++ /dev/null @@ -1,8 +0,0 @@ -package poomasi.domain.order._aftersales.controller; - - -import org.springframework.web.bind.annotation.RestController; - -@RestController -public class ExchangeController { -} diff --git a/src/main/java/poomasi/domain/order/_aftersales/controller/RefundController.java b/src/main/java/poomasi/domain/order/_aftersales/controller/RefundController.java deleted file mode 100644 index 8226cec4..00000000 --- a/src/main/java/poomasi/domain/order/_aftersales/controller/RefundController.java +++ /dev/null @@ -1,42 +0,0 @@ -package poomasi.domain.order._aftersales.controller; - - -import lombok.RequiredArgsConstructor; -import org.springframework.security.access.annotation.Secured; -import org.springframework.web.bind.annotation.*; -import poomasi.domain.order._aftersales.dto.FullRefundRequest; -import poomasi.domain.order._aftersales.dto.PartialRefundRequest; -import poomasi.domain.order._aftersales.service.RefundService; - -@RestController -@RequestMapping("/api/refund") -@RequiredArgsConstructor -public class RefundController { - - private final RefundService refundService; - - - @Secured({"ROLE_CUSTOMER", "ROLE_FARMER"}) - @GetMapping("/{refundId}") - public void getRefund(@PathVariable("refundId") Long refundId) { - - } - - @Secured({"ROLE_CUSTOMER", "ROLE_FARMER"}) - @PostMapping("/{orderProductDetailsId}") - public void processFullRefund (@PathVariable("orderProductDetailsId") Long orderProductDetailsId, - @RequestBody FullRefundRequest fullRefundRequest) { - //TODO : order product details 내부 메서드 보고 - //TODO : 환불 가능하지 받아 와야 함 - } - - - @Secured({"ROLE_CUSTOMER", "ROLE_FARMER"}) - @PostMapping("/api/refund/{orderProductDetailsId}") - public void processPartialRefund (@PathVariable("orderProductDetailsId") Long orderProductDetailsId, - @RequestBody PartialRefundRequest partialRefundRequest) { - - - - } -} diff --git a/src/main/java/poomasi/domain/order/_aftersales/dto/FullRefundRequest.java b/src/main/java/poomasi/domain/order/_aftersales/dto/FullRefundRequest.java deleted file mode 100644 index d27e4423..00000000 --- a/src/main/java/poomasi/domain/order/_aftersales/dto/FullRefundRequest.java +++ /dev/null @@ -1,6 +0,0 @@ -package poomasi.domain.order._aftersales.dto; - -public record FullRefundRequest( - String refundReason -) { -} diff --git a/src/main/java/poomasi/domain/order/_aftersales/dto/PartialRefundRequest.java b/src/main/java/poomasi/domain/order/_aftersales/dto/PartialRefundRequest.java deleted file mode 100644 index faf155e2..00000000 --- a/src/main/java/poomasi/domain/order/_aftersales/dto/PartialRefundRequest.java +++ /dev/null @@ -1,9 +0,0 @@ -package poomasi.domain.order._aftersales.dto; - -import java.math.BigDecimal; - -public record PartialRefundRequest( - BigDecimal refundAmount, // type check 필요 - String refundReason -) { -} diff --git a/src/main/java/poomasi/domain/order/_aftersales/dto/cancel/request/FarmCancelRequest.java b/src/main/java/poomasi/domain/order/_aftersales/dto/cancel/request/FarmCancelRequest.java new file mode 100644 index 00000000..2034f7dd --- /dev/null +++ b/src/main/java/poomasi/domain/order/_aftersales/dto/cancel/request/FarmCancelRequest.java @@ -0,0 +1,4 @@ +package poomasi.domain.order._aftersales.dto.cancel.request; + +public record FarmCancelRequest(Long reservationId) { +} diff --git a/src/main/java/poomasi/domain/order/_aftersales/dto/cancel/request/ProductCancelRequest.java b/src/main/java/poomasi/domain/order/_aftersales/dto/cancel/request/ProductCancelRequest.java new file mode 100644 index 00000000..20a9a326 --- /dev/null +++ b/src/main/java/poomasi/domain/order/_aftersales/dto/cancel/request/ProductCancelRequest.java @@ -0,0 +1,4 @@ +package poomasi.domain.order._aftersales.dto.cancel.request; + +public record ProductCancelRequest(Long orderedProductId, Integer cancelRequestQuantity, String cancelReason) { +} diff --git a/src/main/java/poomasi/domain/order/_aftersales/dto/cancel/response/FarmCancelResponse.java b/src/main/java/poomasi/domain/order/_aftersales/dto/cancel/response/FarmCancelResponse.java new file mode 100644 index 00000000..8158ee58 --- /dev/null +++ b/src/main/java/poomasi/domain/order/_aftersales/dto/cancel/response/FarmCancelResponse.java @@ -0,0 +1,4 @@ +package poomasi.domain.order._aftersales.dto.cancel.response; + +public record FarmCancelResponse(Long reservationId, String status) { +} diff --git a/src/main/java/poomasi/domain/order/_aftersales/dto/cancel/response/ProductCancelResponse.java b/src/main/java/poomasi/domain/order/_aftersales/dto/cancel/response/ProductCancelResponse.java new file mode 100644 index 00000000..6b11a4b8 --- /dev/null +++ b/src/main/java/poomasi/domain/order/_aftersales/dto/cancel/response/ProductCancelResponse.java @@ -0,0 +1,16 @@ +package poomasi.domain.order._aftersales.dto.cancel.response; + +import poomasi.domain.order._aftersales.entity._product.ProductAfterSalesStatus; +import poomasi.domain.order.entity._product.OrderedProductStatus; + +import java.math.BigDecimal; + +public record ProductCancelResponse( + Long orderedProductId, + OrderedProductStatus orderedProductStatus, + + Long productAfterSalesDetailId, + Integer cancelQuantity, + ProductAfterSalesStatus productAfterSalesStatus, + BigDecimal finalCancelAmount) { +} diff --git a/src/main/java/poomasi/domain/order/_aftersales/dto/exchange/request/ProductExchangeRequest.java b/src/main/java/poomasi/domain/order/_aftersales/dto/exchange/request/ProductExchangeRequest.java new file mode 100644 index 00000000..8b8c60ee --- /dev/null +++ b/src/main/java/poomasi/domain/order/_aftersales/dto/exchange/request/ProductExchangeRequest.java @@ -0,0 +1,4 @@ +package poomasi.domain.order._aftersales.dto.exchange.request; + +public record ProductExchangeRequest(Long orderedProductId, String exchangeReason) { +} diff --git a/src/main/java/poomasi/domain/order/_aftersales/dto/exchange/response/ProductExchangeResponse.java b/src/main/java/poomasi/domain/order/_aftersales/dto/exchange/response/ProductExchangeResponse.java new file mode 100644 index 00000000..4a8083a8 --- /dev/null +++ b/src/main/java/poomasi/domain/order/_aftersales/dto/exchange/response/ProductExchangeResponse.java @@ -0,0 +1,4 @@ +package poomasi.domain.order._aftersales.dto.exchange.response; + +public record ProductExchangeResponse(Long orderedProductId, String message) { +} diff --git a/src/main/java/poomasi/domain/order/_aftersales/dto/refund/request/ProductRefundRequest.java b/src/main/java/poomasi/domain/order/_aftersales/dto/refund/request/ProductRefundRequest.java new file mode 100644 index 00000000..940b0d4f --- /dev/null +++ b/src/main/java/poomasi/domain/order/_aftersales/dto/refund/request/ProductRefundRequest.java @@ -0,0 +1,13 @@ +package poomasi.domain.order._aftersales.dto.refund.request; + +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Positive; +import jakarta.validation.constraints.Size; + +public record ProductRefundRequest( + @NotNull Long orderedProductId, // 필수 필드 + @Positive Integer refundRequestQuantity, //필수 + @Size(max = 500) String refundReason, // 필수 필드 + @Size(max = 20) String request // nullable 필드 +) { +} diff --git a/src/main/java/poomasi/domain/order/_aftersales/dto/refund/request/ProductRefundRequestApprovalRequest.java b/src/main/java/poomasi/domain/order/_aftersales/dto/refund/request/ProductRefundRequestApprovalRequest.java new file mode 100644 index 00000000..2bef6976 --- /dev/null +++ b/src/main/java/poomasi/domain/order/_aftersales/dto/refund/request/ProductRefundRequestApprovalRequest.java @@ -0,0 +1,5 @@ +package poomasi.domain.order._aftersales.dto.refund.request; + +public record ProductRefundRequestApprovalRequest(Long productAfterSalesDetailId, + String invoiceNumber) { +} diff --git a/src/main/java/poomasi/domain/order/_aftersales/dto/refund/request/ProductRefundRequestDeniedRequest.java b/src/main/java/poomasi/domain/order/_aftersales/dto/refund/request/ProductRefundRequestDeniedRequest.java new file mode 100644 index 00000000..88f91f2f --- /dev/null +++ b/src/main/java/poomasi/domain/order/_aftersales/dto/refund/request/ProductRefundRequestDeniedRequest.java @@ -0,0 +1,5 @@ +package poomasi.domain.order._aftersales.dto.refund.request; + +public record ProductRefundRequestDeniedRequest(Long productAfterSalesDetailId, + String refundDeinedReason) { +} diff --git a/src/main/java/poomasi/domain/order/_aftersales/dto/refund/response/ProductRefundRequestApprovalResponse.java b/src/main/java/poomasi/domain/order/_aftersales/dto/refund/response/ProductRefundRequestApprovalResponse.java new file mode 100644 index 00000000..1e1bc6bd --- /dev/null +++ b/src/main/java/poomasi/domain/order/_aftersales/dto/refund/response/ProductRefundRequestApprovalResponse.java @@ -0,0 +1,12 @@ +package poomasi.domain.order._aftersales.dto.refund.response; + +import java.math.BigDecimal; + +public record ProductRefundRequestApprovalResponse( + Long orderedProductId, + Integer count, + BigDecimal refundAmount, + Long productAfterSalesDetailId, + String invoiceNumber +) { +} diff --git a/src/main/java/poomasi/domain/order/_aftersales/dto/refund/response/ProductRefundRequestDeniedResponse.java b/src/main/java/poomasi/domain/order/_aftersales/dto/refund/response/ProductRefundRequestDeniedResponse.java new file mode 100644 index 00000000..109d320f --- /dev/null +++ b/src/main/java/poomasi/domain/order/_aftersales/dto/refund/response/ProductRefundRequestDeniedResponse.java @@ -0,0 +1,8 @@ +package poomasi.domain.order._aftersales.dto.refund.response; + +import poomasi.domain.order._aftersales.entity._product.ProductAfterSalesStatus; + +public record ProductRefundRequestDeniedResponse(Long productAfterSalesDetailId, + ProductAfterSalesStatus productAfterSalesStatus, + String productRefundDeniedReason) { +} diff --git a/src/main/java/poomasi/domain/order/_aftersales/dto/refund/response/ProductRefundRequestResponse.java b/src/main/java/poomasi/domain/order/_aftersales/dto/refund/response/ProductRefundRequestResponse.java new file mode 100644 index 00000000..d5c3e26a --- /dev/null +++ b/src/main/java/poomasi/domain/order/_aftersales/dto/refund/response/ProductRefundRequestResponse.java @@ -0,0 +1,19 @@ +package poomasi.domain.order._aftersales.dto.refund.response; + +import poomasi.domain.order._aftersales.entity._product.ProductAfterSalesStatus; +import poomasi.domain.order.entity._product.OrderedProductStatus; + +import java.math.BigDecimal; + +public record ProductRefundRequestResponse( + Long orderedProductId, + OrderedProductStatus orderedProductStatus, + + Long productAfterSalesDetailId, + Integer refundQuantity, + ProductAfterSalesStatus productAfterSalesTypem, + BigDecimal finalRefundAmount +) { +} + + diff --git a/src/main/java/poomasi/domain/order/_aftersales/entity/_abstract/AbstractAfterSales.java b/src/main/java/poomasi/domain/order/_aftersales/entity/_abstract/AbstractAfterSales.java deleted file mode 100644 index cbbe0afc..00000000 --- a/src/main/java/poomasi/domain/order/_aftersales/entity/_abstract/AbstractAfterSales.java +++ /dev/null @@ -1,77 +0,0 @@ -package poomasi.domain.order._aftersales.entity._abstract; - -import jakarta.persistence.*; -import jdk.jfr.Timestamp; -import org.hibernate.annotations.CreationTimestamp; -import org.hibernate.annotations.UpdateTimestamp; - -import java.time.LocalDateTime; - -@MappedSuperclass -public abstract class AbstractAfterSales { - - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; - - @Column(name = "created_at") - @CreationTimestamp - private LocalDateTime createdAt = LocalDateTime.now(); - - @Column(name = "updated_at") - @UpdateTimestamp - private LocalDateTime updateAt = LocalDateTime.now(); - - @Column(name = "deleted_at") - @Timestamp - private LocalDateTime deletedAt; - -} - -/* -* package poomasi.domain.order._aftersales.entity; - -import jakarta.persistence.*; -import jdk.jfr.Description; -import org.hibernate.annotations.CreationTimestamp; -import org.hibernate.annotations.UpdateTimestamp; -import poomasi.domain.order._payment.entity.Payment; - -import java.math.BigDecimal; -import java.time.LocalDateTime; - -@Entity -@Table(name="refund_history") -public class Refund { - - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; - - @CreationTimestamp - @Column(name = "created_at") - private LocalDateTime createdAt = LocalDateTime.now(); - - @Column(name = "updated_at") - @UpdateTimestamp - private LocalDateTime updatedAt = LocalDateTime.now(); - - @Description("삭제 시간") - private LocalDateTime deletedAt; - - @ManyToOne(fetch = FetchType.LAZY) - private Payment payment; - - - private BigDecimal refundAmount; - - @OneToOne(fetch = FetchType.LAZY) - private OrderedProduct orderProductDetails; - -private String refundReason; - -} - - * -* */ - diff --git a/src/main/java/poomasi/domain/order/_aftersales/entity/_farm/FarmAfterSales.java b/src/main/java/poomasi/domain/order/_aftersales/entity/_farm/FarmAfterSales.java index 055a8ca3..c573c2ca 100644 --- a/src/main/java/poomasi/domain/order/_aftersales/entity/_farm/FarmAfterSales.java +++ b/src/main/java/poomasi/domain/order/_aftersales/entity/_farm/FarmAfterSales.java @@ -1,6 +1,6 @@ package poomasi.domain.order._aftersales.entity._farm; -import poomasi.domain.order._aftersales.entity._abstract.AbstractAfterSales; -public class FarmAfterSales extends AbstractAfterSales { + +public class FarmAfterSales { } diff --git a/src/main/java/poomasi/domain/order/_aftersales/entity/_product/ProductAfterSales.java b/src/main/java/poomasi/domain/order/_aftersales/entity/_product/ProductAfterSales.java deleted file mode 100644 index f2345caa..00000000 --- a/src/main/java/poomasi/domain/order/_aftersales/entity/_product/ProductAfterSales.java +++ /dev/null @@ -1,21 +0,0 @@ -package poomasi.domain.order._aftersales.entity._product; - -import jakarta.persistence.*; -import poomasi.domain.order._aftersales.entity._abstract.AbstractAfterSales; -import poomasi.domain.order.entity._product.ProductOrder; - -import java.util.List; - -@Entity -@Table(name="product_after_sales") -public class ProductAfterSales extends AbstractAfterSales { - - @OneToOne - @JoinColumn(name = "product_order_id") - private ProductOrder productOrder; - - @OneToMany - private List productAfterSalesDetail; - -} - diff --git a/src/main/java/poomasi/domain/order/_aftersales/entity/_product/ProductAfterSalesDetail.java b/src/main/java/poomasi/domain/order/_aftersales/entity/_product/ProductAfterSalesDetail.java index b37f4fbb..41a2fe2a 100644 --- a/src/main/java/poomasi/domain/order/_aftersales/entity/_product/ProductAfterSalesDetail.java +++ b/src/main/java/poomasi/domain/order/_aftersales/entity/_product/ProductAfterSalesDetail.java @@ -1,42 +1,101 @@ - package poomasi.domain.order._aftersales.entity._product; +package poomasi.domain.order._aftersales.entity._product; - import jakarta.persistence.*; - import jdk.jfr.Description; - import poomasi.domain.order.entity._product.OrderedProduct; +import jakarta.persistence.*; +import jdk.jfr.Description; +import jdk.jfr.Timestamp; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.hibernate.annotations.CreationTimestamp; +import org.hibernate.annotations.UpdateTimestamp; +import poomasi.domain.order.entity._product.OrderedProduct; - import java.math.BigDecimal; +import java.math.BigDecimal; +import java.time.LocalDateTime; - @Description("상품 판매 후 교환/환불/추소 history") - @Entity - @Table(name="product_after_sales_detail") - public class ProductAfterSalesDetail { +@Description("상품 판매 후 교환/환불/추소 history") +@Entity +@Getter +@Table(name="product_after_sales_detail") +@NoArgsConstructor +public class ProductAfterSalesDetail { - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; - @ManyToOne - private ProductAfterSales productAfterSales; + @Column(name = "created_at") + @CreationTimestamp + private LocalDateTime createdAt = LocalDateTime.now(); - @OneToOne - private OrderedProduct orderedProduct; + @Column(name = "updated_at") + @UpdateTimestamp + private LocalDateTime updateAt = LocalDateTime.now(); + @Column(name = "deleted_at") + @Timestamp + private LocalDateTime deletedAt; - @OneToOne(fetch = FetchType.LAZY, cascade = CascadeType.ALL) - @JoinColumn(name = "refund_exchange_detail_id", nullable = true) // 외래 키 설정 - private RefundExchangeDetail refundExchangeDetail; + @ManyToOne + private OrderedProduct orderedProduct; - @Description("ordered products의 환불/교환/취소 금액") - private BigDecimal amount; + @OneToOne(fetch = FetchType.LAZY, cascade = CascadeType.ALL) + @JoinColumn(name = "product_refund_detail_id", nullable = true) // 외래 키 설정 + private ProductRefundDetail productRefundDetail; + + //TODO : payment에 있는 것을 변경해야 함 + private String impUid; - @Description("환불/교환/취소 사유") - private String reason; + @Description("환불/교환/취소 금액") + private BigDecimal adjustAmount; + + @Description("취소/교환/환불 수량") + private Integer adjustmentQuantity; - @Description("환불 받을 계좌번호") - private String refundAccount; + @Description("환불/교환/취소 요청 사유") + private String reason; - @Enumerated(EnumType.STRING) - private ProductAfterSalesType productAfterSalesType; + @Enumerated(EnumType.STRING) + private ProductAfterSalesStatus productAfterSalesStatus; + @Builder + public ProductAfterSalesDetail(OrderedProduct orderedProduct, + BigDecimal adjustAmount, + String reason, + Integer adjustmentQuantity, + ProductAfterSalesStatus productAfterSalesStatus) { + this.orderedProduct = orderedProduct; + this.adjustAmount = adjustAmount; + this.reason = reason; + this.adjustmentQuantity = adjustmentQuantity; + this.productAfterSalesStatus = productAfterSalesStatus; + } + + public void setOrderedProduct(OrderedProduct orderedProduct) { + this.orderedProduct = orderedProduct; + } + + public void setProductAfterSalesStatus(ProductAfterSalesStatus productAfterSalesStatus){ + this.productAfterSalesStatus = productAfterSalesStatus; + } + + public String getProductRefundDeniedReason(){ + return this.productRefundDetail.getProductRefundDeniedReason(); + } + public void setProductRefundDeniedReason(String productRefundDeniedReason){ + this.productRefundDetail.setProductRefundDeniedReason(productRefundDeniedReason); } + + public void setProductRefundDetail(ProductRefundDetail productRefundDetail) { + this.productRefundDetail = productRefundDetail; + productRefundDetail.setProductAfterSalesDetail(this); + } + + public void changeRefundApproveStatus(String invoiceNumber){ + this.productAfterSalesStatus = ProductAfterSalesStatus.REFUND_APPROVED; + this.productRefundDetail.setInvoiceNumber(invoiceNumber); + } + +} + diff --git a/src/main/java/poomasi/domain/order/_aftersales/entity/_product/ProductAfterSalesStatus.java b/src/main/java/poomasi/domain/order/_aftersales/entity/_product/ProductAfterSalesStatus.java new file mode 100644 index 00000000..5a0ab449 --- /dev/null +++ b/src/main/java/poomasi/domain/order/_aftersales/entity/_product/ProductAfterSalesStatus.java @@ -0,0 +1,22 @@ +package poomasi.domain.order._aftersales.entity._product; + +public enum ProductAfterSalesStatus { + EXCHANGE, + CANCEL, + REFUND, + + + //환불 + REFUND_REQUESTED, // 환불 요청됨 + REFUND_APPROVED, // 환불 승인됨 + REFUND_SHIPMENT_STARTED, // 환불 배송 시작 (반품 물품의 배송 시작) + REFUND_IN_TRANSIT, // 환불 배송 중 (반품 물품이 배송 중) + REFUND_DELIVERED, // 환불 배송 완료 (반품 물품이 도착함) + REFUND_IN_PROGRESS, // 환불 처리 중 (반품 수거 중이거나 처리 대기 중) + REFUND_COMPLETED, // 환불 완료 + REFUND_DENIED // 환불 요청 거절됨 + + + ; + +} diff --git a/src/main/java/poomasi/domain/order/_aftersales/entity/_product/ProductAfterSalesType.java b/src/main/java/poomasi/domain/order/_aftersales/entity/_product/ProductAfterSalesType.java deleted file mode 100644 index cfe7f4a8..00000000 --- a/src/main/java/poomasi/domain/order/_aftersales/entity/_product/ProductAfterSalesType.java +++ /dev/null @@ -1,7 +0,0 @@ -package poomasi.domain.order._aftersales.entity._product; - -public enum ProductAfterSalesType { - EXCHANGE, - CANCEL, - REFUND -} diff --git a/src/main/java/poomasi/domain/order/_aftersales/entity/_product/ProductRefundDetail.java b/src/main/java/poomasi/domain/order/_aftersales/entity/_product/ProductRefundDetail.java new file mode 100644 index 00000000..954c3fe0 --- /dev/null +++ b/src/main/java/poomasi/domain/order/_aftersales/entity/_product/ProductRefundDetail.java @@ -0,0 +1,76 @@ +package poomasi.domain.order._aftersales.entity._product; + + +import jakarta.persistence.*; +import jdk.jfr.Description; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import poomasi.domain.product.entity.Product; + +@Entity +@Table(name= "product_refund_detail") +@Getter +@NoArgsConstructor +public class ProductRefundDetail { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @OneToOne(mappedBy = "productRefundDetail") // 주인이 아닌 쪽에 mappedBy 설정 + private ProductAfterSalesDetail productAfterSalesDetail; + + @Description("반품 회수지. 기본 값은 보낸 주소") + private String pickupLocationAddress; + + @Description("반품 회수지 상세 정보") + private String pickupLocationAddressDetail; + + @Description("반송지. 기본 값은 받은 주소") + private String returnAddress; + + @Description("반송지. 기본 값은 받은 주소") + private String returnAddressDetail; + + @Description("반품/교환 시 운송장 번호") + private String invoiceNumber; + + @Description("반품/교환 시 요청 사항") + private String request; + + @Description("환불 거절 사유") + private String productRefundDeniedReason; + + @Builder + public ProductRefundDetail(ProductAfterSalesDetail productAfterSalesDetail, + String pickupLocationAddress, + String pickupLocationAddressDetail, + String returnAddress, + String returnAddressDetail, + String invoiceNumber, + String request, + String productRefundDeniedReason){ + this.productAfterSalesDetail = productAfterSalesDetail; + this.pickupLocationAddress = pickupLocationAddress; + this.pickupLocationAddressDetail = pickupLocationAddressDetail; + this.returnAddress = returnAddress; + this.returnAddressDetail = returnAddressDetail; + this.invoiceNumber = invoiceNumber; + this.request = request; + this.productRefundDeniedReason = productRefundDeniedReason; + } + + public void setProductRefundDeniedReason(String productRefundDeniedReason) { + this.productRefundDeniedReason = productRefundDeniedReason; + } + + public void setProductAfterSalesDetail(ProductAfterSalesDetail productAfterSalesDetail) { + this.productAfterSalesDetail = productAfterSalesDetail; + } + + public void setInvoiceNumber(String invoiceNumber){ + this.invoiceNumber = invoiceNumber; + } + +} diff --git a/src/main/java/poomasi/domain/order/_aftersales/entity/_product/RefundExchangeDetail.java b/src/main/java/poomasi/domain/order/_aftersales/entity/_product/RefundExchangeDetail.java deleted file mode 100644 index 6e81e378..00000000 --- a/src/main/java/poomasi/domain/order/_aftersales/entity/_product/RefundExchangeDetail.java +++ /dev/null @@ -1,29 +0,0 @@ -package poomasi.domain.order._aftersales.entity._product; - - -import jakarta.persistence.*; -import jdk.jfr.Description; - -@Entity -@Table(name= "refund_exchange_detail") -public class RefundExchangeDetail { - - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; - - @OneToOne(mappedBy = "refundExchangeDetail") // 주인이 아닌 쪽에 mappedBy 설정 - private ProductAfterSalesDetail productAfterSalesDetail; - - @Description("반품 회수지. 기본 값은 보낸 주소") - private String pickupLocation; - - @Description("반송지. 기본 값은 받은 주소") - private String returnAddress; - - @Description("반품/교환 시 운송장 번호") - private String invoiceNumber; - - @Description("반품/교환 시 요청 사항") - private String request; -} diff --git a/src/main/java/poomasi/domain/order/_aftersales/repository/ProductAfterSalesDetailRepository.java b/src/main/java/poomasi/domain/order/_aftersales/repository/ProductAfterSalesDetailRepository.java new file mode 100644 index 00000000..e7b53e82 --- /dev/null +++ b/src/main/java/poomasi/domain/order/_aftersales/repository/ProductAfterSalesDetailRepository.java @@ -0,0 +1,11 @@ +package poomasi.domain.order._aftersales.repository; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; +import poomasi.domain.order._aftersales.entity._product.ProductAfterSalesDetail; + +@Repository +public interface ProductAfterSalesDetailRepository extends JpaRepository { + + +} diff --git a/src/main/java/poomasi/domain/order/_aftersales/repository/ProductAfterSalesRepository.java b/src/main/java/poomasi/domain/order/_aftersales/repository/ProductAfterSalesRepository.java deleted file mode 100644 index 9d274f26..00000000 --- a/src/main/java/poomasi/domain/order/_aftersales/repository/ProductAfterSalesRepository.java +++ /dev/null @@ -1,7 +0,0 @@ -package poomasi.domain.order._aftersales.repository; - -import org.springframework.data.jpa.repository.JpaRepository; -import poomasi.domain.order._aftersales.entity._product.ProductAfterSales; - -public interface ProductAfterSalesRepository extends JpaRepository { -} diff --git a/src/main/java/poomasi/domain/order/_aftersales/repository/ProductRefundDetailRepository.java b/src/main/java/poomasi/domain/order/_aftersales/repository/ProductRefundDetailRepository.java new file mode 100644 index 00000000..3bd40c3d --- /dev/null +++ b/src/main/java/poomasi/domain/order/_aftersales/repository/ProductRefundDetailRepository.java @@ -0,0 +1,9 @@ +package poomasi.domain.order._aftersales.repository; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; +import poomasi.domain.order._aftersales.entity._product.ProductRefundDetail; + +@Repository +public interface ProductRefundDetailRepository extends JpaRepository { +} diff --git a/src/main/java/poomasi/domain/order/_aftersales/service/CancelService.java b/src/main/java/poomasi/domain/order/_aftersales/service/CancelService.java new file mode 100644 index 00000000..5238467c --- /dev/null +++ b/src/main/java/poomasi/domain/order/_aftersales/service/CancelService.java @@ -0,0 +1,9 @@ +package poomasi.domain.order._aftersales.service; + +import com.siot.IamportRestClient.exception.IamportResponseException; + +import java.io.IOException; + +public interface CancelService { + T cancel(P parameter) throws IOException, IamportResponseException; +} diff --git a/src/main/java/poomasi/domain/order/_aftersales/service/FarmAfterSalesService.java b/src/main/java/poomasi/domain/order/_aftersales/service/FarmAfterSalesService.java new file mode 100644 index 00000000..df44b4bf --- /dev/null +++ b/src/main/java/poomasi/domain/order/_aftersales/service/FarmAfterSalesService.java @@ -0,0 +1,14 @@ +package poomasi.domain.order._aftersales.service; + + +import org.springframework.stereotype.Service; +import poomasi.domain.order._aftersales.dto.cancel.request.FarmCancelRequest; + +@Service +public class FarmAfterSalesService { + + public String farmCancel(FarmCancelRequest farmCancelRequest){ + return "success!"; + } + +} diff --git a/src/main/java/poomasi/domain/order/_aftersales/service/ProductAfterSalesService.java b/src/main/java/poomasi/domain/order/_aftersales/service/ProductAfterSalesService.java new file mode 100644 index 00000000..2e938c56 --- /dev/null +++ b/src/main/java/poomasi/domain/order/_aftersales/service/ProductAfterSalesService.java @@ -0,0 +1,371 @@ +package poomasi.domain.order._aftersales.service; + + +import com.siot.IamportRestClient.exception.IamportResponseException; +import jdk.jfr.Description; +import lombok.RequiredArgsConstructor; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import poomasi.domain.auth.security.userdetail.UserDetailsImpl; +import poomasi.domain.member.entity.Member; +import poomasi.domain.order._aftersales.dto.cancel.request.ProductCancelRequest; +import poomasi.domain.order._aftersales.dto.cancel.response.ProductCancelResponse; +import poomasi.domain.order._aftersales.dto.refund.request.ProductRefundRequest; +import poomasi.domain.order._aftersales.dto.refund.request.ProductRefundRequestApprovalRequest; +import poomasi.domain.order._aftersales.dto.refund.request.ProductRefundRequestDeniedRequest; +import poomasi.domain.order._aftersales.dto.refund.response.ProductRefundRequestApprovalResponse; +import poomasi.domain.order._aftersales.dto.refund.response.ProductRefundRequestDeniedResponse; +import poomasi.domain.order._aftersales.dto.refund.response.ProductRefundRequestResponse; +import poomasi.domain.order._aftersales.entity._product.ProductAfterSalesDetail; +import poomasi.domain.order._aftersales.entity._product.ProductAfterSalesStatus; +import poomasi.domain.order._aftersales.entity._product.ProductRefundDetail; +import poomasi.domain.order._aftersales.repository.ProductAfterSalesDetailRepository; +import poomasi.domain.order._aftersales.repository.ProductRefundDetailRepository; +import poomasi.domain.order._payment.util.PaymentUtil; +import poomasi.domain.order.entity._product.OrderedProduct; +import poomasi.domain.order.entity._product.ProductOrder; +import poomasi.domain.order.entity._product.OrderedProductStatus; +import poomasi.domain.order.entity._product.ProductOrderDetails; +import poomasi.domain.order.repository.OrderedProductRepository; +import poomasi.global.error.BusinessException; + +import java.io.IOException; +import java.math.BigDecimal; + +import static poomasi.domain.order._aftersales.entity._product.ProductAfterSalesStatus.CANCEL; +import static poomasi.domain.order.entity._product.OrderedProductStatus.*; +import static poomasi.global.error.BusinessError.*; + +@Service +@RequiredArgsConstructor +public class ProductAfterSalesService implements CancelService{ + + private final OrderedProductRepository orderedProductRepository; + private final PaymentUtil paymentUtil; + private final ProductAfterSalesDetailRepository productAfterSalesDetailRepository; + private final ProductRefundDetailRepository productRefundDetailRepository; + + //-------------------------cancel---------------------// + @Description("판매자 확인 전 취소하는 메서드. 판매자 확인 대기 전 경우만 취소 할 수 있음") + @Override + @Transactional + public ProductCancelResponse cancel(ProductCancelRequest productCancelRequest) throws IOException, IamportResponseException { + + Long orderedProductId = productCancelRequest.orderedProductId(); + String cancelReason = productCancelRequest.cancelReason(); + + //주인 검증 - 유저의 orderedProductId가 맞는지 검증 + OrderedProduct orderedProduct = validateProductCancelRequestByMemberId(orderedProductId); + + //수량 검증 + Integer cancelRequestQuantity = productCancelRequest.cancelRequestQuantity(); + Integer adjustableQuantity = orderedProduct.getAdjustableQuantity(); + if(cancelRequestQuantity > adjustableQuantity){ + throw new BusinessException(CANCEL_QUANTITY_EXCEEDED); + } + + //포트원 취소를 위한 주문 Id 찾기 + ProductOrder productOrder = orderedProduct.getProductOrder(); + String impUid = productOrder.getImpUid(); + + //판매자 확인 대기 전이 아니라면 주문 취소를 할 수 없다 + OrderedProductStatus orderedProductStatus = orderedProduct.getOrderedProductStatus(); + if(orderedProductStatus != PENDING_SELLER_APPROVAL){ + throw new BusinessException(SHIPPING_ALREADY_IN_PROGRESS); + } + + //최종 취소 될 금액 계산 -> 배송비는 처음 한 번 환불 + BigDecimal finalCancelAmount = calculateCancelAmount(orderedProduct, cancelRequestQuantity); + + //배송비 환불 플래그 설정 + + // checksum 검증 + BigDecimal checkSum = productOrder.getCheckSum(); + + //취소하려는 금액이 남은 환불 가능한 금액보다 크다면 + if(finalCancelAmount.compareTo(checkSum) > 0){ + throw new BusinessException(CHECKSUM_EXCESSIVE_REFUND_AMOUNT); + } + + //취소 요청 후, 주문 취소 상태로 변경 + paymentUtil.partialRefundByImpUid(impUid, checkSum, finalCancelAmount, cancelReason); + //orderedProduct.setOrderedProductStatus(CANCEL_PENDING); + + //checksum 뺴기 : 주문 취소가 정상적으로 완료가 되었다면 동기화 + productOrder.subtractChecksum(finalCancelAmount); + + //취소/환불/교환 가능 수량 변경 및 플래그 설정 + orderedProduct.subtractRefundableCount(cancelRequestQuantity); + orderedProduct.addCancelQuantity(cancelRequestQuantity); + //모두 취소 해버렸다면 + orderedProductStatus = orderedProduct.changeOrderedProductStatusToCancel(); + + //TODO : 취소 된 수량도 추가해야 하나? 오늘 회의에서 결정함 + //취소 된 상품 수량 증가 + orderedProduct.getProduct().addStock(cancelRequestQuantity); + + //취소 내역 저장 + ProductAfterSalesDetail productAfterSalesDetail = new ProductAfterSalesDetail() + .builder() + .orderedProduct(orderedProduct) + .adjustAmount(finalCancelAmount) + .reason(cancelReason) + .adjustmentQuantity(cancelRequestQuantity) + .productAfterSalesStatus(CANCEL) + .build(); + orderedProduct.addProductAfterSalesDetail(productAfterSalesDetail); + productAfterSalesDetailRepository.save(productAfterSalesDetail); + + //응답 반환 + return new ProductCancelResponse(orderedProductId, + orderedProductStatus, + productAfterSalesDetail.getId(), + cancelRequestQuantity, + productAfterSalesDetail.getProductAfterSalesStatus(), + finalCancelAmount + ); + + } + + @Description("요청이 구매자 소유인지 확인하는 메서드") + private OrderedProduct validateProductCancelRequestByMemberId(Long orderedProductId){ + Member member = getMember(); + Long memberId = getMember().getId(); + OrderedProduct orderedProduct = orderedProductRepository.findById(orderedProductId) + .orElseThrow(()-> new BusinessException(ORDERED_PRODUCT_NOT_FOUND)); + + if(orderedProduct.getProductOrder().getMember().getId() != memberId){ + new BusinessException(ORDERED_PRODUCT_NOT_FOUND); // TODO : 메서드 추출 이후, error enum 변경 + } + + return orderedProduct; + } + + + @Description("취소 요청에서 취소 금액 계산하는 메서드. 취소 전적이 한 번이라도 있으면 배송비 환불 x.") + private BigDecimal calculateCancelAmount(OrderedProduct orderedProduct, Integer cancelRequestQuantity){ + + boolean isCanceled = orderedProduct.isCanceled(); + + BigDecimal cancelAmount = orderedProduct.getPrice() + .multiply(new BigDecimal(cancelRequestQuantity) + ); + + if(!isCanceled){ + //배송비 붙여야 한다 + BigDecimal deliveryFee = orderedProduct.getDeliveryFee(); + cancelAmount = cancelAmount.add(deliveryFee); + } + return cancelAmount; + } + + + //-------------------------refund---------------------// + @Description("환불 요청하는 메서드") + @Transactional + public ProductRefundRequestResponse createRefundRequest(ProductRefundRequest productRefundRequest) { + Long orderedProductId = productRefundRequest.orderedProductId(); + String refundReason = productRefundRequest.refundReason(); + + // 주인 검증 - 유저의 orderedProductId가 맞는지 검증 + OrderedProduct orderedProduct = validateProductCancelRequestByMemberId(orderedProductId); + + // 수량 검증 - 조정 가능한 수량보다 환불 수량이 많으면 exception + Integer refundRequestQuantity = productRefundRequest.refundRequestQuantity(); + Integer adjustableQuantity = orderedProduct.getAdjustableQuantity(); + if(refundRequestQuantity > adjustableQuantity){ + throw new BusinessException(REFUND_QUANTITY_EXCEEDED); + } + + //포트원 취소를 위한 주문 Id 찾기 + ProductOrder productOrder = orderedProduct.getProductOrder(); + String impUid = productOrder.getImpUid(); + + //구매 확정 상태라면 환불을 할 수 없다 + OrderedProductStatus orderedProductStatus = orderedProduct.getOrderedProductStatus(); + if(orderedProductStatus == DELIVERED){ + throw new BusinessException(PURCHASE_ALREADY_CONFIRMED); + } + + // 배송 대기 전 상태라면 환불을 할 수 없다. + if(orderedProductStatus == PENDING_SELLER_APPROVAL){ + throw new BusinessException(REFUND_NOT_ALLOWED_BEFORE_SHIPPING); + } + + //최종 환불 금액 계산 + BigDecimal finalRefundAmount = calculateRefundAmount(orderedProduct, refundRequestQuantity); + + // checksum 검증 + BigDecimal checkSum = productOrder.getCheckSum(); + + //취소하려는 금액이 남은 환불 가능한 금액보다 크다면 + if(finalRefundAmount.compareTo(checkSum) > 0){ + throw new BusinessException(CHECKSUM_EXCESSIVE_REFUND_AMOUNT); + } + + //취소/환불/교환 가능 수량 변경 + orderedProduct.subtractRefundableCount(refundRequestQuantity); + + //환불 내역 저장 + ProductAfterSalesDetail productAfterSalesDetail = new ProductAfterSalesDetail() + .builder() + .orderedProduct(orderedProduct) + .adjustAmount(finalRefundAmount) + .reason(refundReason) + .adjustmentQuantity(refundRequestQuantity) + .productAfterSalesStatus(ProductAfterSalesStatus.REFUND_REQUESTED) + .build(); + + //환불 상세 저장 + ProductRefundDetail productRefundDetail = createProductRefundDetail(orderedProduct, productOrder, productRefundRequest); + productAfterSalesDetail.setProductRefundDetail(productRefundDetail); + orderedProduct.addProductAfterSalesDetail(productAfterSalesDetail); + + productAfterSalesDetailRepository.save(productAfterSalesDetail); + productRefundDetailRepository.save(productRefundDetail); + + //응답 반환 + return new ProductRefundRequestResponse( + orderedProductId, + orderedProductStatus, + productAfterSalesDetail.getId(), + refundRequestQuantity, + productAfterSalesDetail.getProductAfterSalesStatus(), + finalRefundAmount + ); + } + + @Description("반품 상세 요청사항 만드는 메서드") + private ProductRefundDetail createProductRefundDetail(OrderedProduct orderedProduct, + ProductOrder productOrder, + ProductRefundRequest productRefundRequest) { + + ProductOrderDetails productOrderDetails = productOrder.getProductOrderDetails(); + String pickupLocationAddress = productOrderDetails.getDestinationAddress(); + String pickUpLocationAddressDetail = productOrderDetails.getDestinationAddressDetail(); + + String returnAddress = orderedProduct.getStoreAddress(); + String returnAddressDetail = orderedProduct.getStoreAddressDetail(); + + String request = productRefundRequest.request(); //nullable field + + ProductRefundDetail productRefundDetail = new ProductRefundDetail() + .builder() + .pickupLocationAddress(pickupLocationAddress) + .pickupLocationAddressDetail(pickUpLocationAddressDetail) + .returnAddress(returnAddress) + .returnAddressDetail(returnAddressDetail) + .request(request) + .build(); + + return productRefundDetail; + } + + + @Description("환불 요청에서 환불 금액 계산하는 메서드") + private BigDecimal calculateRefundAmount(OrderedProduct orderedProduct, Integer refundRequestQuantity){ + + BigDecimal refundAmount = orderedProduct.getPrice() + .multiply(new BigDecimal(refundRequestQuantity)); + + return refundAmount; + } + + + @Description("판매자 환불 거절 메서드") + @Transactional + public ProductRefundRequestDeniedResponse processRefundDenied(ProductRefundRequestDeniedRequest productRefundRequestDeniedRequest){ + + Long productAfterSalesDetailId = productRefundRequestDeniedRequest.productAfterSalesDetailId(); + String refundDeinedReason = productRefundRequestDeniedRequest.refundDeinedReason(); + + //환불 요청이 존재하는지 그리고 자신의 환불 요청인지 검증하고 + ProductAfterSalesDetail productAfterSalesDetail = validateProductRefundRequestByFarmerId(productAfterSalesDetailId); + + //refund detail 만든 후 + ProductRefundDetail productRefundDetail = productAfterSalesDetail.getProductRefundDetail(); + productRefundDetail.setProductRefundDeniedReason(refundDeinedReason); + + //환불 거부 상태로 등록 후 + productAfterSalesDetail.setProductAfterSalesStatus(ProductAfterSalesStatus.REFUND_DENIED); + + productAfterSalesDetail.setProductRefundDetail(productRefundDetail); + //db에 저장한다 + productRefundDetailRepository.save(productRefundDetail); + + //전달한다 + return new ProductRefundRequestDeniedResponse( + productAfterSalesDetail.getId(), + productAfterSalesDetail.getProductAfterSalesStatus(), + productAfterSalesDetail.getProductRefundDeniedReason() + ); + } + + @Description("판매자 환불 확인 메서드") + public ProductRefundRequestApprovalResponse processRefundApproval(ProductRefundRequestApprovalRequest productRefundRequestApprovalRequest) throws IOException, IamportResponseException { + + Long productAfterSalesDetailId = productRefundRequestApprovalRequest.productAfterSalesDetailId(); + String invoiceNumber = productRefundRequestApprovalRequest.invoiceNumber(); + + //환불 요청이 존재하는지 그리고 자신의 환불 요청인지 검증하고 + ProductAfterSalesDetail productAfterSalesDetail = validateProductRefundRequestByFarmerId(productAfterSalesDetailId); + + //환불 결제 금액 찾고 + BigDecimal finalRefundAmount = productAfterSalesDetail.getAdjustAmount(); + + //결제를 찾는다 + OrderedProduct orderedProduct = productAfterSalesDetail.getOrderedProduct(); + ProductOrder productOrder = orderedProduct.getProductOrder(); + + //환불에 필요한 parameter + String refundReason = productAfterSalesDetail.getReason(); + String impUid = productOrder.getImpUid(); + BigDecimal checkSum = productOrder.getCheckSum(); + + //환불 처리 + paymentUtil.partialRefundByImpUid(impUid, checkSum, finalRefundAmount, refundReason); + + //성공하면 checksum 포트원 서버와 동기화 + productOrder.subtractChecksum(finalRefundAmount); + + //운송장 번호와 상태 등록 + productAfterSalesDetail.changeRefundApproveStatus(invoiceNumber); + + return new ProductRefundRequestApprovalResponse( + orderedProduct.getId(), + productAfterSalesDetail.getAdjustmentQuantity(), + finalRefundAmount, + productAfterSalesDetailId, + invoiceNumber + ); + + } + + + @Description("환불 요청이 존재하고, 판매자 소유인지 확인하는 메서드 ") + private ProductAfterSalesDetail validateProductRefundRequestByFarmerId(Long productAfterSalesDetailId){ + ProductAfterSalesDetail productAfterSalesDetail = productAfterSalesDetailRepository.findById(productAfterSalesDetailId) + .orElseThrow(()-> new BusinessException(REFUND_AFTER_SALES_NOT_FOUND) + ); + Long farmerId = getMember().getId(); + /* + if(farmerId != productAfterSalesDetail.getOrderedProduct().getStoreId().getMember()){ + throw new BusinessException(REFUND_AFTER_SALES_REQUEST_INVALID_OWNER); + } + */ + return productAfterSalesDetail; + } + + // ------------------------------// + @Description("security context에서 member 객체 가져오는 메서드") + private Member getMember() { + Authentication authentication = SecurityContextHolder + .getContext().getAuthentication(); + Object impl = authentication.getPrincipal(); + Member member = ((UserDetailsImpl) impl).getMember(); + return member; + } + +} diff --git a/src/main/java/poomasi/domain/order/_aftersales/service/RefundService.java b/src/main/java/poomasi/domain/order/_aftersales/service/RefundService.java deleted file mode 100644 index 4d47f74e..00000000 --- a/src/main/java/poomasi/domain/order/_aftersales/service/RefundService.java +++ /dev/null @@ -1,22 +0,0 @@ -package poomasi.domain.order._aftersales.service; - - -import com.siot.IamportRestClient.IamportClient; -import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Service; -import poomasi.domain.order._aftersales.repository.ProductAfterSalesRepository; - -@Service -@RequiredArgsConstructor -public class RefundService { - - private final ProductAfterSalesRepository productAfterSalesRepository; - private final IamportClient iamportClient; - - - /* public void refundFull(){ - iamportClient. - }*/ - - -} diff --git a/src/main/java/poomasi/domain/order/_payment/config/IamportConfig.java b/src/main/java/poomasi/domain/order/_payment/config/IamportConfig.java index 8f5dd755..758f0f92 100644 --- a/src/main/java/poomasi/domain/order/_payment/config/IamportConfig.java +++ b/src/main/java/poomasi/domain/order/_payment/config/IamportConfig.java @@ -9,7 +9,7 @@ @Configuration public class IamportConfig { - @Value("${IMP_API_KEY}") + @Value("${imp.api.key}") private String apiKey; @Value("${imp.api.secretKey}") @@ -19,5 +19,4 @@ public class IamportConfig { public IamportClient iamportClient() { return new IamportClient(apiKey, secretKey); } - } diff --git a/src/main/java/poomasi/domain/order/_payment/controller/PaymentController.java b/src/main/java/poomasi/domain/order/_payment/controller/PaymentController.java index 21324d48..58777551 100644 --- a/src/main/java/poomasi/domain/order/_payment/controller/PaymentController.java +++ b/src/main/java/poomasi/domain/order/_payment/controller/PaymentController.java @@ -8,7 +8,7 @@ import org.springframework.web.bind.annotation.*; import poomasi.domain.order._payment.dto.request.PaymentPreRegisterRequest; import poomasi.domain.order._payment.dto.request.PaymentWebHookRequest; -import poomasi.domain.order._payment.service.PaymentService; +import poomasi.domain.order._payment.service.ProductPaymentService; import java.io.IOException; @@ -17,37 +17,33 @@ @RequiredArgsConstructor public class PaymentController { - private final PaymentService paymentService; + private final ProductPaymentService productPaymentService; @Description("사전 결제 api") @Secured({"ROLE_CUSTOMER", "ROLE_FARMER"}) @PostMapping("/pre-payment") public ResponseEntity postPrepare(PaymentPreRegisterRequest paymentPreRegisterRequest) throws IamportResponseException, IOException { return ResponseEntity.ok( - paymentService.portonePrePaymentRegister(paymentPreRegisterRequest) + productPaymentService.portonePrePaymentRegister(paymentPreRegisterRequest) ); } - @Description("사후 결제(검증 api)") - @PostMapping("/validate") - public void validatePayment(@RequestBody PaymentWebHookRequest paymentWebHookRequest) throws IamportResponseException, IOException { - paymentService.handlePortOneProductWebhookEvent(paymentWebHookRequest); + @Description("결제 바로 직전 포트원에서 보내는 confirm 요청" + " 결제를 진행하려면 HTTP Status 200 응답, 그렇지 않으면 500 응답 보내기" ) + @PostMapping("/confirm/") + public ResponseEntity confirmProductStock(@RequestParam String merchantUid, @RequestParam String impUid) throws IamportResponseException, IOException { + productPaymentService.confirmBeforePayment(merchantUid, impUid); + return ResponseEntity.ok().build(); } @Description("포트원 웹훅 수신 api") @PostMapping("/portone-webhook") public void handleIamportWebhook(@RequestBody PaymentWebHookRequest paymentWebHookRequest) throws IamportResponseException, IOException { - paymentService.handlePortOneProductWebhookEvent(paymentWebHookRequest); + productPaymentService.handlePortOneProductWebhookEvent(paymentWebHookRequest); } - @Description("결제 바로 직전 포트원에서 보내는 confirm 요청" + " 결제를 진행하려면 HTTP Status 200 응답, 그렇지 않으면 500 응답 보내기" ) - @PostMapping("/confirm-stock/") - public ResponseEntity confirmProductStock(@RequestParam String merchantUid, @RequestParam String impUid) throws IamportResponseException, IOException { - paymentService.confirmProductStock(merchantUid, impUid); - return ResponseEntity.ok().build(); - } + diff --git a/src/main/java/poomasi/domain/order/_payment/entity/Payment.java b/src/main/java/poomasi/domain/order/_payment/entity/Payment.java index ddc3048d..0983c68f 100644 --- a/src/main/java/poomasi/domain/order/_payment/entity/Payment.java +++ b/src/main/java/poomasi/domain/order/_payment/entity/Payment.java @@ -3,6 +3,7 @@ import jakarta.persistence.*; import jdk.jfr.Description; import lombok.Getter; +import poomasi.domain.order.entity.PaymentStatus; import poomasi.domain.order.entity._farm.FarmOrder; import poomasi.domain.order.entity._product.ProductOrder; @@ -17,6 +18,13 @@ public class Payment { @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; + @Column(name = "imp_uid") + @Description("아임포트 결제 imp_uid") + private String impUid; + + @OneToOne(mappedBy = "payment") + private ProductOrder productOrder; + @Description("포트원 결제 금액") private BigDecimal totalPrice; @@ -26,6 +34,9 @@ public class Payment { @Description("사용 포인트") private BigDecimal usedPoint; + @Description("배송비") + private BigDecimal deliveryFee; + @Description("최종 가격") private BigDecimal finalPrice; @@ -33,10 +44,23 @@ public class Payment { @Enumerated(EnumType.STRING) private PaymentMethod paymentMethod; - @OneToOne(mappedBy = "payment") - private ProductOrder productOrder; + @Description("checksum") + private BigDecimal checkSum; + + @Enumerated(EnumType.STRING) + private PaymentStatus paymentStatus = PaymentStatus.PAYMENT_PENDING; + + public void setCheckSum(BigDecimal checksum) { + this.checkSum = checksum; + } + + public void subtractCheckSum(BigDecimal checksum) { + this.checkSum = this.checkSum.subtract(checksum); + } + + public void setPaymentStatus(PaymentStatus paymentStatus) { + this.paymentStatus = paymentStatus; + } -/* @OneToOne(mappedBy = "payment") - private FarmOrder farmOrder;*/ } diff --git a/src/main/java/poomasi/domain/order/_payment/entity/PaymentState.java b/src/main/java/poomasi/domain/order/_payment/entity/PaymentState.java deleted file mode 100644 index d160f719..00000000 --- a/src/main/java/poomasi/domain/order/_payment/entity/PaymentState.java +++ /dev/null @@ -1,35 +0,0 @@ -package poomasi.domain.order._payment.entity; - - -import jdk.jfr.Description; - -@Description("임시로 남겨둠 .. . .") -public enum PaymentState { - PENDING, // 결제 대기 중 - COMPLETED, // 결제 완료 - FAILED, // 결제 실패 - CANCELLED, // 결제 취소됨 - REFUNDED, // 환불 완료 - DECLINED; // 결제 거부됨 - - @Override - public String toString() { - // 사용자 친화적인 문자열로 반환할 수 있도록 오버라이딩 - switch (this) { - case PENDING: - return "Payment Pending"; - case COMPLETED: - return "Payment Completed"; - case FAILED: - return "Payment Failed"; - case CANCELLED: - return "Payment Cancelled"; - case REFUNDED: - return "Payment Refunded"; - case DECLINED: - return "Payment Declined"; - default: - return super.toString(); - } - } -} diff --git a/src/main/java/poomasi/domain/order/_payment/service/FarmPaymentService.java b/src/main/java/poomasi/domain/order/_payment/service/FarmPaymentService.java new file mode 100644 index 00000000..ff842ae9 --- /dev/null +++ b/src/main/java/poomasi/domain/order/_payment/service/FarmPaymentService.java @@ -0,0 +1,15 @@ +package poomasi.domain.order._payment.service; + + +import com.siot.IamportRestClient.exception.IamportResponseException; +import org.springframework.stereotype.Service; + +import java.io.IOException; + +@Service +public class FarmPaymentService { + + + + +} diff --git a/src/main/java/poomasi/domain/order/_payment/service/PaymentService.java b/src/main/java/poomasi/domain/order/_payment/service/PaymentService.java deleted file mode 100644 index a65ec9dc..00000000 --- a/src/main/java/poomasi/domain/order/_payment/service/PaymentService.java +++ /dev/null @@ -1,279 +0,0 @@ -package poomasi.domain.order._payment.service; - -import com.siot.IamportRestClient.IamportClient; -import com.siot.IamportRestClient.exception.IamportResponseException; -import com.siot.IamportRestClient.request.CancelData; -import com.siot.IamportRestClient.request.PrepareData; -import com.siot.IamportRestClient.response.IamportResponse; -import jdk.jfr.Description; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.security.core.Authentication; -import org.springframework.security.core.context.SecurityContextHolder; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Isolation; -import org.springframework.transaction.annotation.Transactional; -import poomasi.domain.auth.security.userdetail.UserDetailsImpl; -import poomasi.domain.member.entity.Member; -import poomasi.domain.order._payment.dto.request.PaymentPreRegisterRequest; -import poomasi.domain.order._payment.dto.request.PaymentWebHookRequest; -import poomasi.domain.order._payment.dto.response.PaymentPreRegisterResponse; -import poomasi.domain.order._payment.dto.response.PaymentResponse; -import poomasi.domain.order._payment.entity.Payment; -import poomasi.domain.order._payment.repository.PaymentRepository; -import poomasi.domain.order.entity._product.OrderedProduct; -import poomasi.domain.order.entity._product.ProductOrder; -import poomasi.domain.order.repository.ProductOrderRepository; -import poomasi.domain.product._cart.service.CartService; -import poomasi.domain.product.entity.Product; -import poomasi.global.error.BusinessException; -import poomasi.global.error.PaymentConfirmError; -import poomasi.global.error.PaymentConfirmException; - -import java.io.IOException; -import java.math.BigDecimal; -import java.util.List; -import java.util.concurrent.Executors; -import java.util.concurrent.ScheduledExecutorService; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicBoolean; - -import static poomasi.domain.order.entity.OrderStatus.AWAITING_SELLER_CONFIRMATION; -import static poomasi.domain.order.entity.OrderStatus.PENDING; -import static poomasi.global.error.BusinessError.*; - -@Service -@RequiredArgsConstructor -@Slf4j -public class PaymentService { - - @Autowired - private final PaymentRepository paymentRepository; - private final IamportClient iamportClient; - private final ProductOrderRepository productOrderRepository; - private final CartService cartService; - - private final ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1); - private final AtomicBoolean isWebhookReceived = new AtomicBoolean(false); // 웹훅 수신 여부 체크 - - @Description("사전 결제 등록. 프론트엔드에게 서버 merchant uid를 return 해야 함") - public PaymentPreRegisterResponse portonePrePaymentRegister(PaymentPreRegisterRequest paymentPreRegisterRequest) throws IOException, IamportResponseException { - - PrepareData prepareData = new PrepareData(paymentPreRegisterRequest.merchantUid(), - paymentPreRegisterRequest.amount() - ); - iamportClient.postPrepare(prepareData); - return PaymentPreRegisterResponse.from( - paymentPreRegisterRequest.merchantUid() - ); - } - - - @Transactional(isolation = Isolation.SERIALIZABLE) - @Description("포트원 결제 직전 바로 받는 confirm 요청. 40초 대기") - public void confirmProductStock(String impUid, String merchantUid) throws IOException, IamportResponseException { - - ProductOrder productOrder = productOrderRepository.findByMerchantUidAndImpUid(merchantUid, impUid) - .orElseThrow(() -> new BusinessException(PAYMENT_NOT_FOUND)); - - List orderedProductList = productOrder.getOrderedProducts(); - //수량 검증 - for(OrderedProduct orderedProduct : orderedProductList) { - Product product = orderedProduct.getProduct(); - Integer remainQuantity = product.getStock(); - Integer orderQuantity = orderedProduct.getCount(); - - //주문 재고가 남은 재고보다 많다면 500 + reason 보내야 함 - if(orderQuantity > remainQuantity){ - throw new PaymentConfirmException(PaymentConfirmError.PAYMENT_PROUCT_CONFIRM_EXCEPTION); - } - } - BigDecimal amountToBePaid = productOrder.getTotalAmount(); - //재고 검증 완료 -> 200 OK 보내야 함 + 웹훅 수신 여부에 따라 분기 - scheduler.schedule(() -> { - try { - if (!isWebhookReceived.get()) { //수신 못 받으면 - if(sendAndValidateAmount(impUid, amountToBePaid)){ //impUid를 가지고 포트원 서버에 요청을 한 후, db와 결제 금액 비교한다. - productOrder.setOrderStatus(AWAITING_SELLER_CONFIRMATION); - decreaseStock(productOrder); - }else{ - //실제 결제 된 금액과 결제 되어야 할 금액이 다르다면 -> 결제 취소 api를 호출해야 한다. - cancelPaymentByImpUid(impUid); - } - } - } catch (IOException | IamportResponseException e) { - e.printStackTrace(); - } - }, 40, TimeUnit.SECONDS); - - } - - @Description("단건 조회 후, 결제 되어야 할 금액과 결제 된 금액이 같은지 확인하는 메서드") - public boolean sendAndValidateAmount(String impUid, BigDecimal amountToBePaid) throws IOException, IamportResponseException{ - BigDecimal amount = getPaymentAmount(impUid); - if(amountToBePaid.compareTo(amount)==0){ - return true; - } - return false; - } - - - @Description("포트원에서 결제 금액 조회하는 메서드") - public BigDecimal getPaymentAmount(String impUid) throws IOException, IamportResponseException{ - IamportResponse iamportResponse = getSingleTransaction(impUid); - return iamportResponse.getResponse().getAmount(); - } - - @Description("웹훅 처리 service -> 결제 정상적으로 성공됨을 보장") - @Transactional(isolation = Isolation.SERIALIZABLE) - public void handlePortOneProductWebhookEvent(PaymentWebHookRequest paymentWebHookRequest) throws IOException, IamportResponseException { - - isWebhookReceived.set(true); //웹훅 수신 플래그 설정하기 - - String impUid = paymentWebHookRequest.impUid(); - ProductOrder productOrder = productOrderRepository.findByImpUid(impUid) - .orElseThrow(() -> new BusinessException(PAYMENT_NOT_FOUND)); - BigDecimal amountToBePaid = productOrder.getTotalAmount(); - - //결제 되어야 할 금액과 결제 된 금액이 같다면 - if(sendAndValidateAmount(impUid, amountToBePaid)){ - productOrder.setOrderStatus(AWAITING_SELLER_CONFIRMATION); - decreaseStock(productOrder); - }else{ - cancelPaymentByImpUid(impUid); //실제 결제 된 금액과 결제 되어야 할 금액이 다르다면 -> 결제 취소 api를 호출해야 한다. - //throw new BusinessException - } - - } - - @Description("재고 차감 메서드") - @Transactional(isolation = Isolation.SERIALIZABLE) - public void decreaseStock(ProductOrder productOrder){ - List orderedProductList = productOrder.getOrderedProducts(); - for (OrderedProduct orderedProduct : orderedProductList){ - Product product = orderedProduct.getProduct(); - Integer remainQuantity = product.getStock(); //남은 수량 - Integer subtractQuantity = orderedProduct.getCount();//빼야 할 수량 - if(subtractQuantity > remainQuantity){ - throw new BusinessException(STOCK_QUANTITY_EXCEEDED); - } - product.subtractStock(subtractQuantity); - } - } - - /* - com.siot.IamportRestClient.response.Payment payment = iamportResponse.getResponse(); - int code = iamportResponse.getCode(); - String message = iamportResponse.getMessage(); - String status = payment.getStatus(); - BigDecimal amount = payment.getAmount(); - */ - @Description("단건 결제 조회 API") - public IamportResponse getSingleTransaction(String impUid) throws IOException, IamportResponseException { - IamportResponse iamportResponse = iamportClient.paymentByImpUid(impUid); - return iamportResponse; - } - - @Description("서버에서 마지막 검증") - public boolean verifyPostPayment(String impUid) throws IOException, IamportResponseException { - ProductOrder productOrder = productOrderRepository.findByImpUid(impUid) - .orElseThrow(() -> new BusinessException(PAYMENT_NOT_FOUND)); - if(productOrder.getOrderStatus() == PENDING){ - throw new BusinessException(PAYMENT_BAD_REQUEST); - } - return true; - } - - /*@Transactional - @Description("프론트에서 받아온 결과를 validate하는 메서드") - public void portoneVerifyPostPayment(PaymentWebHookRequest paymentWebHookRequest) throws IOException, IamportResponseException { - String impUid = paymentWebHookRequest.imp_uid(); - String merchantUid = paymentWebHookRequest.merchant_uid(); - IamportResponse iamportResponse = iamportClient.paymentByImpUid(impUid); - BigDecimal amount = iamportResponse.getResponse() - .getAmount(); - - ProductOrder order = productOrderRepository.findByMerchantUid(merchantUid) - .orElseThrow(() -> new BusinessException(ORDER_NOT_FOUND)); - - if(order.getOrderStatus()!=PENDING){ //이미 처리한 주문이라면 - throw new BusinessException(ORDER_ALREADY_PROCESSED); - } - - if(validatePaymentConsistency(order.getTotalAmount(), amount)){ //결제 금액이 맞지 않다면 -> 주문 취소 api 호출 - cancelPayment(iamportResponse); - throw new BusinessException(PAYMENT_AMOUNT_MISMATCH); - } - order.setOrderStatus(AWAITING_SELLER_CONFIRMATION); // 상태 변경 - cartService.removeSelected(); //장바구니 삭제 - }*/ - - //private boolean compareCartAndPaymentAmount(Cart cart, BigDecimal ) - - - private boolean validatePaymentConsistency(BigDecimal prepaymentAmount, BigDecimal postPaymentAmount){ - if (prepaymentAmount.compareTo(postPaymentAmount) != 0) { - return false; - } - return true; - } - - - public void cancelPaymentByImpUid(String impUid) throws IOException, IamportResponseException { - CancelData cancelDate = new CancelData(impUid, false); - iamportClient.cancelPaymentByImpUid(cancelDate); - } - - /*@Transactional - @Description("결제 전액 취소/환불 api") - public void cancelPayment(Payment payment, IamportResponse response) throws IOException, IamportResponseException{ - //TODO : 결제내역 단건 조회 통해서 이미 완료가 된 결제인지 확인 - //true면 Uid, false면 merchantUid로 판단 - CancelData cancelData = new CancelData(response.getResponse().getMerchantUid(), false); - iamportClient.cancelPaymentByImpUid(cancelData); - - }*/ - - /* @Description("결제 환불 api") - public void processRefund() throws IOException, IamportResponseException{ - - } -*/ - - @Transactional - @Description("결제 부분 환불 api 호출") - public void partialRefund(BigDecimal checkSum, IamportResponse response, BigDecimal amount) throws IOException, IamportResponseException { - //BigDecimal checkSum = payment.getChecksum(); - CancelData cancelData = new CancelData(response.getResponse().getMerchantUid(), false, amount); - cancelData.setChecksum(checkSum); - iamportClient.cancelPaymentByImpUid(cancelData); - - } - - public PaymentResponse getPayment(Long paymentId) { - Payment payment = paymentRepository.findById(paymentId) - .orElseThrow(() -> new BusinessException(PAYMENT_NOT_FOUND)); - return PaymentResponse.fromEntity(payment); - } - - @Description("orderID로 결제 방법 찾는 메서드") - public PaymentResponse getPaymentByOrderId(Long orderId) { - Member member = getMember(); - Payment payment = paymentRepository.findById(orderId) - .orElseThrow(() -> new BusinessException(PAYMENT_NOT_FOUND)); - return PaymentResponse.fromEntity(payment); - } - - - private Member getMember() { - Authentication authentication = SecurityContextHolder - .getContext().getAuthentication(); - Object impl = authentication.getPrincipal(); - Member member = ((UserDetailsImpl) impl).getMember(); - return member; - } - -} - - diff --git a/src/main/java/poomasi/domain/order/_payment/service/ProductPaymentService.java b/src/main/java/poomasi/domain/order/_payment/service/ProductPaymentService.java new file mode 100644 index 00000000..45751886 --- /dev/null +++ b/src/main/java/poomasi/domain/order/_payment/service/ProductPaymentService.java @@ -0,0 +1,191 @@ +package poomasi.domain.order._payment.service; + +import com.siot.IamportRestClient.IamportClient; +import com.siot.IamportRestClient.exception.IamportResponseException; +import com.siot.IamportRestClient.request.CancelData; +import com.siot.IamportRestClient.response.IamportResponse; +import jdk.jfr.Description; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Isolation; +import org.springframework.transaction.annotation.Transactional; +import poomasi.domain.auth.security.userdetail.UserDetailsImpl; +import poomasi.domain.member.entity.Member; +import poomasi.domain.order._payment.config.IamportConfig; +import poomasi.domain.order._payment.dto.request.PaymentPreRegisterRequest; +import poomasi.domain.order._payment.dto.request.PaymentWebHookRequest; +import poomasi.domain.order._payment.dto.response.PaymentPreRegisterResponse; +import poomasi.domain.order._payment.dto.response.PaymentResponse; +import poomasi.domain.order._payment.entity.Payment; +import poomasi.domain.order._payment.repository.PaymentRepository; +import poomasi.domain.order._payment.util.PaymentUtil; +import poomasi.domain.order.entity._product.OrderedProduct; +import poomasi.domain.order.entity._product.ProductOrder; +import poomasi.domain.order.repository.ProductOrderRepository; +import poomasi.domain.product._cart.service.CartService; +import poomasi.domain.product.entity.Product; +import poomasi.global.error.BusinessException; +import poomasi.global.error.PaymentConfirmError; +import poomasi.global.error.PaymentConfirmException; + +import java.io.IOException; +import java.math.BigDecimal; +import java.util.List; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; + +import static poomasi.domain.order.entity.PaymentStatus.*; +import static poomasi.global.error.BusinessError.*; + +@Service +@RequiredArgsConstructor +@Slf4j +public class ProductPaymentService{ + + @Autowired + private final PaymentRepository paymentRepository; + private final ProductOrderRepository productOrderRepository; + private final PaymentUtil paymentUtil; + + private final ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1); + private final AtomicBoolean isWebhookReceived = new AtomicBoolean(false); // 웹훅 수신 여부 체크 + //private final ThreadLocal isWebhookReceived = ThreadLocal.withInitial(AtomicBoolean::new); -> thread local로 제어 + + + @Description("사전 결제 등록. 프론트엔드에게 서버 merchant uid를 return 해야 함") + public PaymentPreRegisterResponse portonePrePaymentRegister(PaymentPreRegisterRequest paymentPreRegisterRequest) throws IOException, IamportResponseException { + + String merchantUid = paymentPreRegisterRequest.merchantUid(); + BigDecimal amount = paymentPreRegisterRequest.amount(); + + paymentUtil.sendPrepareData(merchantUid, amount); + return PaymentPreRegisterResponse.from( + paymentPreRegisterRequest.merchantUid() + ); + } + + @Transactional(isolation = Isolation.SERIALIZABLE) + @Description("포트원 결제 직전 바로 받는 confirm 요청. 40초 대기") + public void confirmBeforePayment(String impUid, String merchantUid) throws IOException, IamportResponseException { + + ProductOrder productOrder = productOrderRepository.findByMerchantUid(merchantUid) + .orElseThrow(() -> new BusinessException(PAYMENT_NOT_FOUND)); + + List orderedProductList = productOrder.getOrderedProducts(); + //수량 검증 + for(OrderedProduct orderedProduct : orderedProductList) { + Product product = orderedProduct.getProduct(); + Integer remainQuantity = product.getStock(); + Integer orderQuantity = orderedProduct.getCount(); + + //주문 재고가 남은 재고보다 많다면 500 + cancelReason 보내야 함 + if(orderQuantity > remainQuantity){ + throw new PaymentConfirmException(PaymentConfirmError.PAYMENT_PROUCT_CONFIRM_EXCEPTION); + } + } + + //결제 되어야 할 금액 + BigDecimal amountToBePaid = productOrder.getTotalAmount(); + + /* + * 1. 200ok 보내기 + * 2. 타이머 세팅 후 + * 타이머 타임 아웃 되면(웹훅을 받지 못하면) 결제 단건 api 호출 + * 만약 웹훅을 받으면 받은 데이터에서 getImpUid후, 결제 단건 호출 및 타이머 초기화 + * */ + + //재고 검증 완료 -> 200 OK 보내야 함 + 웹훅 수신 여부에 따라 분기 + scheduler.schedule(() -> { + try { + if (!isWebhookReceived.get()) { // 웹훅 수신 못 받으면 다시 보내기 + if(paymentUtil.validatePaymentAmount(impUid, amountToBePaid)){ + productOrder.setPaymentStatus(PAYMENT_COMPLETE); + decreaseStock(productOrder); //재고 차감 + }else{ + paymentUtil.cancelPaymentByImpUid(impUid); //실제 결제 된 금액과 결제 되어야 할 금액이 다르다면 -> 결제 취소 api를 호출해야 한다. + productOrder.setPaymentStatus(PAYMENT_DECLINED); + throw new BusinessException(PAYMENT_AMOUNT_MISMATCH); + } + } + } catch (IOException | IamportResponseException e) { + log.error(e.getMessage(), e); + throw new BusinessException(PAYMENT_BAD_REQUEST); + } + }, 40, TimeUnit.SECONDS); + + } + + @Description("웹훅 처리 service -> 결제 정상적으로 성공됨을 보장") + public void handlePortOneProductWebhookEvent(PaymentWebHookRequest paymentWebHookRequest) throws IOException, IamportResponseException { + + isWebhookReceived.set(true); //웹훅 수신 플래그 설정하기 + + String impUid = paymentWebHookRequest.impUid(); + String merchantUid = paymentWebHookRequest.merchantUid(); + ProductOrder productOrder = productOrderRepository.findByMerchantUid(merchantUid) + .orElseThrow(() -> new BusinessException(PAYMENT_NOT_FOUND)); + BigDecimal amountToBePaid = productOrder.getTotalAmount(); + + //결제 되어야 할 금액과 결제 된 금액이 같다면 + if(paymentUtil.validatePaymentAmount(impUid, amountToBePaid)){ + try{ + decreaseStock(productOrder); + productOrder.setPaymentStatus(PAYMENT_COMPLETE); + }catch(BusinessException businessException){ + productOrder.setPaymentStatus(PAYMENT_INSUFFICIENT_QUANTITY); + throw new BusinessException(PAYMENT_BAD_REQUEST); + } + }else{ + paymentUtil.cancelPaymentByImpUid(impUid); //실제 결제 된 금액과 결제 되어야 할 금액이 다르다면 -> 결제 취소 api를 호출해야 한다. + productOrder.setPaymentStatus(PAYMENT_DECLINED); + throw new BusinessException(PAYMENT_AMOUNT_MISMATCH); + } + } + + @Description("재고 차감 메서드. 감소하다 exception이 일어나면 rollback하고 결제 취소 해야 함") + @Transactional(isolation = Isolation.SERIALIZABLE) + public void decreaseStock(ProductOrder productOrder){ + List orderedProductList = productOrder.getOrderedProducts(); + for (OrderedProduct orderedProduct : orderedProductList){ + Product product = orderedProduct.getProduct(); + Integer remainQuantity = product.getStock(); //남은 수량 + Integer subtractQuantity = orderedProduct.getCount();//빼야 할 수량 + if(subtractQuantity > remainQuantity){ + throw new BusinessException(STOCK_QUANTITY_EXCEEDED); + } + product.subtractStock(subtractQuantity); + } + } + + + public PaymentResponse getPayment(Long paymentId) { + Payment payment = paymentRepository.findById(paymentId) + .orElseThrow(() -> new BusinessException(PAYMENT_NOT_FOUND)); + return PaymentResponse.fromEntity(payment); + } + + @Description("orderID로 결제 방법 찾는 메서드") + public PaymentResponse getPaymentByOrderId(Long orderId) { + Member member = getMember(); + Payment payment = paymentRepository.findById(orderId) + .orElseThrow(() -> new BusinessException(PAYMENT_NOT_FOUND)); + return PaymentResponse.fromEntity(payment); + } + + private Member getMember() { + Authentication authentication = SecurityContextHolder + .getContext().getAuthentication(); + Object impl = authentication.getPrincipal(); + Member member = ((UserDetailsImpl) impl).getMember(); + return member; + } + +} + + diff --git a/src/main/java/poomasi/domain/order/_payment/util/PaymentUtil.java b/src/main/java/poomasi/domain/order/_payment/util/PaymentUtil.java new file mode 100644 index 00000000..ce1d9212 --- /dev/null +++ b/src/main/java/poomasi/domain/order/_payment/util/PaymentUtil.java @@ -0,0 +1,88 @@ +package poomasi.domain.order._payment.util; + + +import com.siot.IamportRestClient.IamportClient; +import com.siot.IamportRestClient.exception.IamportResponseException; +import com.siot.IamportRestClient.request.CancelData; +import com.siot.IamportRestClient.request.PrepareData; +import com.siot.IamportRestClient.response.IamportResponse; +import com.siot.IamportRestClient.response.Payment; +import jdk.jfr.Description; +import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Transactional; +import poomasi.global.error.BusinessError; +import poomasi.global.error.BusinessException; + +import java.io.IOException; +import java.math.BigDecimal; + +@Component +public class PaymentUtil { + + private final IamportClient iamportClient; + + @Autowired + public PaymentUtil(IamportClient iamportClient) { + this.iamportClient = iamportClient; + } + + @Description("포트원에서 결제 금액 조회하는 메서드") + public BigDecimal getPaymentAmount(String impUid) throws IOException, IamportResponseException { + IamportResponse iamportResponse = getSingleTransaction(impUid); + return iamportResponse.getResponse().getAmount(); + } + + @Description("단건 결제 조회 API") + public IamportResponse getSingleTransaction(String impUid) throws IOException, IamportResponseException { + IamportResponse iamportResponse = iamportClient.paymentByImpUid(impUid); + return iamportResponse; + } + + @Description("결제 취소 api") + public void cancelPaymentByImpUid(String impUid) throws IOException, IamportResponseException { + CancelData cancelDate = new CancelData(impUid, false); + iamportClient.cancelPaymentByImpUid(cancelDate); + } + + @Transactional + @Description("imp uid로 결제 부분 환불 api 호출") + public void partialRefundByImpUid(String impUid, BigDecimal checkSum, BigDecimal amount, String reason) throws IOException, IamportResponseException { + CancelData cancelData = new CancelData(impUid, true, amount); + cancelData.setChecksum(checkSum); + cancelData.setReason(reason); + iamportClient.cancelPaymentByImpUid(cancelData); + } + + @Transactional + @Description("merchant Uid로 결제 부분 환불 api 호출") + public void partialRefundByMerchantUid(String merchantUid, BigDecimal checkSum, BigDecimal amount, String reason) throws IOException, IamportResponseException { + CancelData cancelData = new CancelData(merchantUid, false, amount); + cancelData.setChecksum(checkSum); + cancelData.setReason(reason); + iamportClient.cancelPaymentByImpUid(cancelData); + } + + + @Description("사전 결제 데이터 전송") + public void sendPrepareData(String merchantUid, BigDecimal amount) throws IOException, IamportResponseException { + PrepareData prepareData = this.generatePrepareData(merchantUid, amount); + iamportClient.postPrepare(prepareData); + } + + @Description("단건 조회 후, 결제 되어야 할 금액과 결제 된 금액이 같은지 확인하는 메서드") + public boolean validatePaymentAmount(String impUid, BigDecimal amountToBePaid) throws IOException, IamportResponseException{ + IamportResponse iamportResponse = getSingleTransaction(impUid); //내가 보냄 + BigDecimal amount = iamportResponse.getResponse().getAmount(); + if(amountToBePaid.compareTo(amount)!=0){ + return false; + } + return true; + } + + @Description("사전 결제를 위한 Prepare Data를 만드는 메서드") + private PrepareData generatePrepareData(String merchantUid, BigDecimal amount) { + return new PrepareData(merchantUid, amount); + } +} diff --git a/src/main/java/poomasi/domain/order/controller/OrderController.java b/src/main/java/poomasi/domain/order/controller/OrderController.java index a6e17aa1..d36111e6 100644 --- a/src/main/java/poomasi/domain/order/controller/OrderController.java +++ b/src/main/java/poomasi/domain/order/controller/OrderController.java @@ -7,13 +7,13 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.http.ResponseEntity; import org.springframework.security.access.annotation.Secured; -import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.web.bind.annotation.*; -import poomasi.domain.auth.security.userdetail.UserDetailsImpl; +import poomasi.domain.farm.service.FarmService; import poomasi.domain.order._payment.dto.request.PaymentPreRegisterRequest; -import poomasi.domain.order._payment.service.PaymentService; -import poomasi.domain.order.dto.request.OrderRegisterRequest; -import poomasi.domain.order.service.OrderService; +import poomasi.domain.order._payment.service.ProductPaymentService; +import poomasi.domain.order.dto.request.ProductOrderRegisterRequest; +import poomasi.domain.order.service.FarmOrderService; +import poomasi.domain.order.service.ProductOrderService; import java.io.IOException; @@ -24,16 +24,17 @@ @RequiredArgsConstructor public class OrderController { - private final OrderService orderService; - private final PaymentService paymentService; + private final ProductOrderService productOrderService; + private final FarmOrderService farmOrderService; + private final ProductPaymentService productPaymentService; @Secured({"ROLE_CUSTOMER", "ROLE_FARMER"}) @PostMapping("/product/pre-order") @Description("product 사전 결제") - public ResponseEntity createProductPreOrder(@RequestBody OrderRegisterRequest orderRegisterRequest) throws IOException, IamportResponseException { - PaymentPreRegisterRequest paymentPreRegisterRequest = orderService.productPreOrderRegister(orderRegisterRequest); + public ResponseEntity createProductPreOrder(@RequestBody ProductOrderRegisterRequest productOrderRegisterRequest) throws IOException, IamportResponseException { + PaymentPreRegisterRequest paymentPreRegisterRequest = productOrderService.productPreOrderRegister(productOrderRegisterRequest); return ResponseEntity.ok( - paymentService.portonePrePaymentRegister(paymentPreRegisterRequest) + productPaymentService.portonePrePaymentRegister(paymentPreRegisterRequest) ); } @@ -41,9 +42,9 @@ public ResponseEntity createProductPreOrder(@RequestBody OrderRegisterRequest @PostMapping("/farm/pre-order") @Description("farm 사전 결제") public ResponseEntity createFarmPreOrder() throws IOException, IamportResponseException { - PaymentPreRegisterRequest paymentPreRegisterRequest = orderService.farmPreOrderRegister(); + PaymentPreRegisterRequest paymentPreRegisterRequest = productOrderService.farmPreOrderRegister(); return ResponseEntity.ok( - paymentService.portonePrePaymentRegister(paymentPreRegisterRequest) + productPaymentService.portonePrePaymentRegister(paymentPreRegisterRequest) ); } @@ -51,7 +52,7 @@ public ResponseEntity createFarmPreOrder() throws IOException, IamportRespons @GetMapping("/{orderId}") public ResponseEntity getAllOrdersByMember(@PathVariable Long orderId) { return ResponseEntity.ok( - orderService.findOrderByMemberId(orderId) + productOrderService.findOrderByMemberId(orderId) ); } @@ -59,7 +60,7 @@ public ResponseEntity getAllOrdersByMember(@PathVariable Long orderId) { @GetMapping("/") public ResponseEntity getOrdersByMember() { return ResponseEntity.ok( - orderService.findAllOrdersByMemberId() + productOrderService.findAllOrdersByMemberId() ); } @@ -67,7 +68,7 @@ public ResponseEntity getOrdersByMember() { @GetMapping("/{orderId}/details") public ResponseEntity getOrderDetailsByMember(@PathVariable Long orderId) { return ResponseEntity.ok( - orderService.findOrderDetailsByOrderId(orderId) + productOrderService.findOrderDetailsByOrderId(orderId) ); } @@ -76,7 +77,7 @@ public ResponseEntity getOrderDetailsByMember(@PathVariable Long orderId) { @GetMapping("/{orderId}/product/details") public ResponseEntity getOrderProductDetailsByOrderId(@PathVariable Long orderId) { return ResponseEntity.ok( - orderService.findAllOrderProductDetails(orderId) + productOrderService.findAllOrderProductDetails(orderId) ); } @@ -84,7 +85,7 @@ public ResponseEntity getOrderProductDetailsByOrderId(@PathVariable Long orde @GetMapping("/{orderId}/product/details/{detailsId}") public ResponseEntity getOrderProductDetailsByDetailsId(@PathVariable Long orderId, @PathVariable Long detailsId) { return ResponseEntity.ok( - orderService.findOrderProductDetailsById(orderId, detailsId) + productOrderService.findOrderProductDetailsById(orderId, detailsId) ); } diff --git a/src/main/java/poomasi/domain/order/dto/request/OrderRegisterRequest.java b/src/main/java/poomasi/domain/order/dto/request/OrderRegisterRequest.java deleted file mode 100644 index b29c8c96..00000000 --- a/src/main/java/poomasi/domain/order/dto/request/OrderRegisterRequest.java +++ /dev/null @@ -1,6 +0,0 @@ -package poomasi.domain.order.dto.request; - -public record OrderRegisterRequest(String address, - String addressDetails, - String deliveryRequest) { -} diff --git a/src/main/java/poomasi/domain/order/dto/request/ProductOrderRegisterRequest.java b/src/main/java/poomasi/domain/order/dto/request/ProductOrderRegisterRequest.java new file mode 100644 index 00000000..55b5d106 --- /dev/null +++ b/src/main/java/poomasi/domain/order/dto/request/ProductOrderRegisterRequest.java @@ -0,0 +1,6 @@ +package poomasi.domain.order.dto.request; + +public record ProductOrderRegisterRequest(String destinationAddress, + String destinationAddressDetail, + String deliveryRequest) { +} diff --git a/src/main/java/poomasi/domain/order/dto/response/OrderDetailsResponse.java b/src/main/java/poomasi/domain/order/dto/response/OrderDetailsResponse.java index e3002a7c..749f9dab 100644 --- a/src/main/java/poomasi/domain/order/dto/response/OrderDetailsResponse.java +++ b/src/main/java/poomasi/domain/order/dto/response/OrderDetailsResponse.java @@ -9,8 +9,8 @@ public record OrderDetailsResponse( ) { public static OrderDetailsResponse fromEntity(ProductOrderDetails productOrderDetails) { return new OrderDetailsResponse( - productOrderDetails.getAddress(), - productOrderDetails.getAddressDetail(), + productOrderDetails.getDestinationAddress(), + productOrderDetails.getDestinationAddressDetail(), productOrderDetails.getDeliveryRequest() ); } diff --git a/src/main/java/poomasi/domain/order/entity/OrderStatus.java b/src/main/java/poomasi/domain/order/entity/OrderStatus.java deleted file mode 100644 index 2e71f97e..00000000 --- a/src/main/java/poomasi/domain/order/entity/OrderStatus.java +++ /dev/null @@ -1,8 +0,0 @@ -package poomasi.domain.order.entity; - -public enum OrderStatus { - PENDING, // 결제 대기 중 - AWAITING_SELLER_CONFIRMATION, // 판매자 확인 대기 중 - SELLER_CONFIRMED // 판매자 확인 완료 - ; -} \ No newline at end of file diff --git a/src/main/java/poomasi/domain/order/entity/PaymentStatus.java b/src/main/java/poomasi/domain/order/entity/PaymentStatus.java new file mode 100644 index 00000000..b8b4dc21 --- /dev/null +++ b/src/main/java/poomasi/domain/order/entity/PaymentStatus.java @@ -0,0 +1,10 @@ +package poomasi.domain.order.entity; + +public enum PaymentStatus { + PAYMENT_PENDING, // 결제 대기 중 + PAYMENT_COMPLETE, // 결제 성공 + PAYMENT_DECLINED, // 결제 거부 + PAYMENT_INSUFFICIENT_QUANTITY + ; +} + diff --git a/src/main/java/poomasi/domain/order/entity/_abstract/AbstractOrder.java b/src/main/java/poomasi/domain/order/entity/_abstract/AbstractOrder.java index 4b784690..d7a6ae7d 100644 --- a/src/main/java/poomasi/domain/order/entity/_abstract/AbstractOrder.java +++ b/src/main/java/poomasi/domain/order/entity/_abstract/AbstractOrder.java @@ -4,19 +4,23 @@ import jdk.jfr.Description; import jdk.jfr.Timestamp; import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import lombok.experimental.SuperBuilder; import org.hibernate.annotations.CreationTimestamp; -import org.hibernate.annotations.SQLDelete; import org.hibernate.annotations.UpdateTimestamp; import poomasi.domain.member.entity.Member; import poomasi.domain.order._payment.entity.Payment; -import poomasi.domain.order.entity.OrderStatus; import java.math.BigDecimal; import java.time.LocalDateTime; -import java.util.Date; + @MappedSuperclass @Getter +@Setter +@SuperBuilder // 빌더 패턴을 사용하도록 설정 +@NoArgsConstructor public abstract class AbstractOrder { @Id @@ -31,14 +35,6 @@ public abstract class AbstractOrder { @OneToOne(cascade = CascadeType.ALL, fetch = FetchType.LAZY) private Payment payment; - @Column(name = "merchant_uid") - @Description("서버 내부 주문 id(아임포트 id)") - private String merchantUid = "p" + new Date().getTime(); - - @Column(name = "imp_uid") - @Description("아임포트 결제 imp_uid") - private String impUid; - @Column(name = "created_at") @CreationTimestamp private LocalDateTime createdAt = LocalDateTime.now(); @@ -55,34 +51,22 @@ public abstract class AbstractOrder { @Description("총 결제 금액") private BigDecimal totalAmount; - @Enumerated(EnumType.STRING) - private OrderStatus orderStatus = OrderStatus.PENDING; - - @Description("checksum") - private BigDecimal checksum; - - public void setCheckSum(BigDecimal checksum) { - this.checksum = checksum; + public void setCheckSum(BigDecimal checkSum) { + this.payment.setCheckSum(checkSum); } - public void reduceChecksum(BigDecimal amount) { - checksum = checksum.subtract(amount); - } - public void setTotalAmount(BigDecimal totalAmount) { - this.totalAmount = totalAmount; + public void subtractChecksum(BigDecimal checkSum) { + this.payment.subtractCheckSum(checkSum); } - public void setOrderStatus(OrderStatus orderStatus) { - this.orderStatus = orderStatus; + public BigDecimal getCheckSum(){ + return this.payment.getCheckSum(); } - public void setImpUid(String impUid) { - this.impUid = impUid; + public void setTotalAmount(BigDecimal totalAmount) { + this.totalAmount = totalAmount; } - public void setMerchantUid(String merchantUid) { - this.merchantUid = merchantUid; - } } diff --git a/src/main/java/poomasi/domain/order/entity/_farm/FarmOrder.java b/src/main/java/poomasi/domain/order/entity/_farm/FarmOrder.java index 1de8705c..46501720 100644 --- a/src/main/java/poomasi/domain/order/entity/_farm/FarmOrder.java +++ b/src/main/java/poomasi/domain/order/entity/_farm/FarmOrder.java @@ -1,10 +1,13 @@ package poomasi.domain.order.entity._farm; import jakarta.persistence.*; +import jdk.jfr.Description; import org.hibernate.annotations.Comment; import poomasi.domain.order._payment.entity.Payment; import poomasi.domain.order.entity._abstract.AbstractOrder; +import java.util.Date; + //@Entity //@Table(name = "farm_order") public class FarmOrder extends AbstractOrder { @@ -19,7 +22,7 @@ public class FarmOrder extends AbstractOrder { private String description; @Comment("도로명 주소") - private String address; + private String destinationAddress; @Comment("상세 주소") private String addressDetail; @@ -30,5 +33,11 @@ public class FarmOrder extends AbstractOrder { @Comment("경도") private Double longitude; */ + + @Column(name = "merchant_uid") + @Description("서버 내부 주문 id(아임포트 id)") + private String merchantUid = "f" + new Date().getTime(); + + } diff --git a/src/main/java/poomasi/domain/order/entity/_product/OrderedProduct.java b/src/main/java/poomasi/domain/order/entity/_product/OrderedProduct.java index d495a281..13c988c0 100644 --- a/src/main/java/poomasi/domain/order/entity/_product/OrderedProduct.java +++ b/src/main/java/poomasi/domain/order/entity/_product/OrderedProduct.java @@ -11,6 +11,7 @@ import java.io.Serializable; import java.math.BigDecimal; +import java.util.List; @Entity @Table(name = "ordered_products") @@ -23,25 +24,23 @@ public class OrderedProduct implements Serializable { @Column(name = "ordered_product_id") private Long id; - @OneToOne(fetch = FetchType.LAZY) + @OneToMany(fetch = FetchType.LAZY) @JoinColumn(nullable = true, name = "product_after_sales_detail_id") - private ProductAfterSalesDetail productAfterSalesDetail; + private List productAfterSalesDetails; @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "product_order_id") private ProductOrder productOrder; - //FIXME : store Id를 참조해야 한다. //나중에 store Id로 변경해야 한다 - private Long storeId; + //private Store store; + //private Long storeId; @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "product_id") private Product product; - - @Column(name = "product_description", nullable = true) private String productDescription; @@ -50,7 +49,8 @@ public class OrderedProduct implements Serializable { @Description("구매 당시 1개당 가격") private BigDecimal price; - + + @Description("구매 수량") @Column(name="count") private Integer count; @@ -58,7 +58,21 @@ public class OrderedProduct implements Serializable { @Column(name = "invoice_number", nullable = true) private String invoiceNumber; - private ShippingStatus shippingStatus = ShippingStatus.ORDERED; + private OrderedProductStatus orderedProductStatus = OrderedProductStatus.PENDING_SELLER_APPROVAL; + + @Description("TODO : product의 delivery fee를 참조해야 한다.") + private BigDecimal deliveryFee; + + @Description("환불 가능한 남은 수량") + @Column(name = "refundable_count") + private Integer adjustableQuantity; + + @Description("취소 된 수량") + @Column(name = "cacnel_quantity") + private Integer cancelQuantity; + + @Description("flag가 설정되어 있으면 배송비 환불하지 않아도 된다") + private boolean isCanceled = false; // 웹훅 받아서 조회해야 함. // findByInvoiceNumber 후 @@ -79,12 +93,45 @@ public void setInvoiceNumber(String invoiceNumber) { this.invoiceNumber = invoiceNumber; } - public void setShippingStatus(ShippingStatus shippingStatus) { - this.shippingStatus = shippingStatus; + public void setOrderedProductStatus(OrderedProductStatus orderedProductStatus) { + this.orderedProductStatus = orderedProductStatus; + } + + public void addProductAfterSalesDetail(ProductAfterSalesDetail productAfterSalesDetail) { + this.productAfterSalesDetails.add(productAfterSalesDetail); + productAfterSalesDetail.setOrderedProduct(this); } public Long getOrderId(){ return this.productOrder.getId(); } + + public void subtractRefundableCount(Integer refundableCount) { + this.adjustableQuantity -= refundableCount; + } + + public void addCancelQuantity(Integer cancelQuantity) { + this.isCanceled = true; + this.cancelQuantity += cancelQuantity; + } + + public OrderedProductStatus changeOrderedProductStatusToCancel() { + if (this.count == this.cancelQuantity) { + this.orderedProductStatus = OrderedProductStatus.CANCELLED; + } + return this.orderedProductStatus; + } + + public String getStoreAddress(){ + //return this.store.getStoreAddress() + return "TODO : store의 address를 참조해야 함"; + } + + public String getStoreAddressDetail(){ + //return this.store.getStoreAddressDetail() + return "TODO: store의 address detail을 참조해야 함"; + } + + } diff --git a/src/main/java/poomasi/domain/order/entity/_product/OrderedProductStatus.java b/src/main/java/poomasi/domain/order/entity/_product/OrderedProductStatus.java new file mode 100644 index 00000000..407a9a9f --- /dev/null +++ b/src/main/java/poomasi/domain/order/entity/_product/OrderedProductStatus.java @@ -0,0 +1,31 @@ +package poomasi.domain.order.entity._product; + +public enum OrderedProductStatus { + + PENDING_SELLER_APPROVAL, // 판매자 수락 전 (주문 완료 후 대기 상태) + SHIPMENT_STARTED, // 배송 시작 (판매자 수락을 하면 바뀌는 상태) + IN_TRANSIT, // 배송 중 + DELIVERED, // 배송 완료 + CANCELLED, // 주문 취소 완료 (취소가 최종적으로 완료된 상태) + + //교환 + EXCHANGE_PENDING, // 교환 요청 대기중 + EXCHANGE_APPROVED, // 교환 요청 승인됨 + EXCHANGE_IN_PROGRESS, // 교환 처리 중 (배송 중이거나 준비 중) + EXCHANGE_COMPLETED, // 교환 완료 + EXCHANGE_DENIED, // 교환 요청 거절됨 + + //환불 + REFUND_REQUESTED, // 환불 요청됨 + REFUND_APPROVED, // 환불 승인됨 + REFUND_SHIPMENT_STARTED, // 환불 배송 시작 (반품 물품의 배송 시작) + REFUND_IN_TRANSIT, // 환불 배송 중 (반품 물품이 배송 중) + REFUND_DELIVERED, // 환불 배송 완료 (반품 물품이 도착함) + REFUND_IN_PROGRESS, // 환불 처리 중 (반품 수거 중이거나 처리 대기 중) + REFUND_COMPLETED, // 환불 완료 + REFUND_DENIED, // 환불 요청 거절됨 + + //주문 취소 + CANCEL_PENDING, // 주문 취소 대기 중 (취소 요청을 받은 상태) + ; +} \ No newline at end of file diff --git a/src/main/java/poomasi/domain/order/entity/_product/ProductOrder.java b/src/main/java/poomasi/domain/order/entity/_product/ProductOrder.java index 5618b63c..90c237e2 100644 --- a/src/main/java/poomasi/domain/order/entity/_product/ProductOrder.java +++ b/src/main/java/poomasi/domain/order/entity/_product/ProductOrder.java @@ -3,21 +3,30 @@ import jakarta.persistence.*; import jdk.jfr.Description; +import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; +import lombok.experimental.SuperBuilder; import org.hibernate.annotations.SQLDelete; -import poomasi.domain.order._aftersales.entity._product.ProductAfterSales; +import poomasi.domain.member.entity.Member; +import poomasi.domain.order.entity.PaymentStatus; import poomasi.domain.order.entity._abstract.AbstractOrder; +import java.math.BigDecimal; +import java.util.Date; import java.util.List; @Entity @Table(name = "product_order") @Getter -@NoArgsConstructor -@SQLDelete(sql = "UPDATE product_order SET deleted_at=current_timestamp WHERE id = ?") +@SuperBuilder +@SQLDelete(sql = "UPDATE product_order SET deleted_at = current_timestamp WHERE id = ?") public class ProductOrder extends AbstractOrder { + @Column(name = "merchant_uid") + @Description("서버 내부 주문 id(아임포트 id)") + private String merchantUid = "p" + new Date().getTime(); + @Column(name = "ordered_products_id") @OneToMany(cascade = CascadeType.ALL, orphanRemoval = true) private List orderedProducts; @@ -27,18 +36,33 @@ public class ProductOrder extends AbstractOrder { @Description("상품 배송지, 요청 사항") private ProductOrderDetails productOrderDetails; + public ProductOrder(){ - @OneToOne(fetch = FetchType.LAZY, cascade = CascadeType.ALL) - @JoinColumn(name = "product_after_sales_id") - @Description("결제 완료 후 취소/교환/환불 관리 객체") - private ProductAfterSales productAfterSales; - - public ProductOrder(ProductOrderDetails productOrderDetails) { - this.productOrderDetails = productOrderDetails; } public void addOrderedProduct(OrderedProduct orderedProduct) { this.orderedProducts.add(orderedProduct); } + public void setMerchantUid(String merchantUid) { + this.merchantUid = merchantUid; + } + + public String getImpUid(){ + return this.getPayment().getImpUid(); + } + + public PaymentStatus getPaymentStatus(){ + return this.getPayment().getPaymentStatus(); + } + + public void setPaymentStatus(PaymentStatus paymentStatus){ + this.getPayment().setPaymentStatus(paymentStatus); + } + + public void setProductOrderDetails(ProductOrderDetails productOrderDetails){ + this.productOrderDetails = productOrderDetails; + productOrderDetails.setProductOrder(this); + } + } diff --git a/src/main/java/poomasi/domain/order/entity/_product/ProductOrderDetails.java b/src/main/java/poomasi/domain/order/entity/_product/ProductOrderDetails.java index 4d8fb903..0cf2e983 100644 --- a/src/main/java/poomasi/domain/order/entity/_product/ProductOrderDetails.java +++ b/src/main/java/poomasi/domain/order/entity/_product/ProductOrderDetails.java @@ -2,6 +2,7 @@ import jakarta.persistence.*; import jdk.jfr.Description; +import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; @@ -18,21 +19,29 @@ public class ProductOrderDetails { @OneToOne(mappedBy = "productOrderDetails", cascade = CascadeType.ALL) // 필드명으로 지정 private ProductOrder productOrder; - @Column(name = "address") - private String address; + @Column(name = "return_address") + @Description("도착 주소") + private String destinationAddress; - @Column(name = "address_detail") - private String addressDetail; + @Column(name = "destination_address_detail") + @Description("도착 상세 주소") + private String destinationAddressDetail; @Description("배송 요청 사항") @Column(name = "delivery_request", length = 255) private String deliveryRequest; - public ProductOrderDetails(String address, String addressDetail, String deliveryRequest) { - this.address = address; - this.addressDetail = addressDetail; + + @Builder + public ProductOrderDetails(ProductOrder productOrder, String destinationAddress, String destinationAddressDetail, String deliveryRequest) { + this.productOrder = productOrder; + this.destinationAddress = destinationAddress; + this.destinationAddressDetail = destinationAddressDetail; this.deliveryRequest = deliveryRequest; } + public void setProductOrder(ProductOrder productOrder) { + this.productOrder = productOrder; + } } diff --git a/src/main/java/poomasi/domain/order/entity/_product/ShippingStatus.java b/src/main/java/poomasi/domain/order/entity/_product/ShippingStatus.java deleted file mode 100644 index e2a0a0aa..00000000 --- a/src/main/java/poomasi/domain/order/entity/_product/ShippingStatus.java +++ /dev/null @@ -1,8 +0,0 @@ -package poomasi.domain.order.entity._product; - -public enum ShippingStatus { - ORDERED, // 주문 완료 (배송 전 시작 상태) -> 배송 보내기 전 단계. 판매자 확인 후 취소 가능 - SHIPMENT_STARTED, // 배송 시작 - IN_TRANSIT, // 배송 중 - DELIVERED // 배송 완료 -} diff --git a/src/main/java/poomasi/domain/order/repository/ProductOrderRepository.java b/src/main/java/poomasi/domain/order/repository/ProductOrderRepository.java index 9f0ba5a1..c439df8a 100644 --- a/src/main/java/poomasi/domain/order/repository/ProductOrderRepository.java +++ b/src/main/java/poomasi/domain/order/repository/ProductOrderRepository.java @@ -8,8 +8,7 @@ public interface ProductOrderRepository extends JpaRepository { List findByMemberId(Long memberId); - //List findById(Long id); Optional findByMerchantUid(String merchantUid); - Optional findByImpUid(String impUid); - Optional findByMerchantUidAndImpUid(String merchantUid, String impUid); + //Optional findByImpUid(String impUid); + //Optional findByMerchantUidAndImpUid(String merchantUid, String impUid); } diff --git a/src/main/java/poomasi/domain/order/service/FarmOrderService.java b/src/main/java/poomasi/domain/order/service/FarmOrderService.java new file mode 100644 index 00000000..825369cc --- /dev/null +++ b/src/main/java/poomasi/domain/order/service/FarmOrderService.java @@ -0,0 +1,8 @@ +package poomasi.domain.order.service; + + +import org.springframework.stereotype.Service; + +@Service +public class FarmOrderService implements OrderService { +} diff --git a/src/main/java/poomasi/domain/order/service/OrderService.java b/src/main/java/poomasi/domain/order/service/OrderService.java index 4d34b046..1f89f786 100644 --- a/src/main/java/poomasi/domain/order/service/OrderService.java +++ b/src/main/java/poomasi/domain/order/service/OrderService.java @@ -1,218 +1,5 @@ package poomasi.domain.order.service; -import jdk.jfr.Description; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.security.core.Authentication; -import org.springframework.security.core.context.SecurityContextHolder; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; -import poomasi.domain.auth.security.userdetail.UserDetailsImpl; -import poomasi.domain.member.entity.Member; -import poomasi.domain.order._payment.dto.request.PaymentPreRegisterRequest; -import poomasi.domain.order.dto.request.OrderRegisterRequest; -import poomasi.domain.order.dto.response.OrderDetailsResponse; -import poomasi.domain.order.dto.response.OrderProductDetailsResponse; -import poomasi.domain.order.dto.response.OrderResponse; -import poomasi.domain.order.entity._product.OrderedProduct; -import poomasi.domain.order.entity._product.ProductOrder; -import poomasi.domain.order.entity._product.ProductOrderDetails; -import poomasi.domain.order.entity.OrderStatus; -import poomasi.domain.order.repository.OrderedProductRepository; -import poomasi.domain.order.repository.ProductOrderRepository; -import poomasi.domain.product._cart.entity.Cart; -import poomasi.domain.product._cart.repository.CartRepository; -import poomasi.domain.product.entity.Product; -import poomasi.domain.product.repository.ProductRepository; -import poomasi.global.error.BusinessException; - -import java.math.BigDecimal; -import java.util.List; -import java.util.stream.Collectors; - -import static poomasi.global.error.BusinessError.*; - -@RequiredArgsConstructor -@Service -@Slf4j -public class OrderService { - - private final ProductOrderRepository productOrderRepository; - private final CartRepository cartRepository; - private final ProductRepository productRepository; - private final OrderedProductRepository orderedProductRepository; - - @Transactional - public PaymentPreRegisterRequest productPreOrderRegister(OrderRegisterRequest orderRegisterRequest){ - Member member = getMember(); - Long memberId = member.getId(); - List cartList = cartRepository.findByMemberIdAndSelected(memberId); - - String address = orderRegisterRequest.address(); - String addressDetails = orderRegisterRequest.addressDetails(); - String deliveryRequest = orderRegisterRequest.deliveryRequest(); - - ProductOrder productOrder = new ProductOrder( - new ProductOrderDetails(address, - addressDetails, - deliveryRequest) - ); - - //cart에 있는 총 가격 계산하기 - BigDecimal totalPrice = BigDecimal.ZERO; - - // cart 돌면서 productOrder details 추가 - for (Cart cart : cartList) { - Long productId = cart.getProductId(); - Product product = productRepository.findById(productId) - .orElseThrow(() -> new BusinessException(PRODUCT_NOT_FOUND)); - - Integer productStock = product.getStock(); - Integer quantityInCart = cart.getCount(); - - // 현재 남아있는 재고보다 더 많이 요청하면 - if(quantityInCart > productStock){ - throw new BusinessException(PRODUCT_STOCK_ZERO); - } - - String productDescription = product.getDescription(); - Integer count = cart.getCount(); - String productName = product.getName(); - BigDecimal price = BigDecimal.valueOf(product.getPrice()); - OrderedProduct orderedProduct = OrderedProduct - .builder() - .product(product) - .productOrder(productOrder) - .productDescription(productDescription) - .productName(productName) - .price(price) - .count(count) - .build(); - productOrder.addOrderedProduct(orderedProduct); - totalPrice = totalPrice.add(price); - } - productOrder.setTotalAmount(totalPrice); - productOrder.setCheckSum(totalPrice); - productOrderRepository.save(productOrder); - - String merchantUid = productOrder.getMerchantUid(); - return new PaymentPreRegisterRequest(merchantUid, totalPrice); - } - - @Transactional - //TODO : 만들어야 합니다 ~ - public PaymentPreRegisterRequest farmPreOrderRegister(){ - Member member = getMember(); - String merchantUid = ""; - BigDecimal totalPrice = BigDecimal.ZERO; - - return new PaymentPreRegisterRequest(merchantUid, totalPrice); - } - - - @Description("멤버 ID 기반으로 모든 order 다 들고 오는 메서드") - public List findAllOrdersByMemberId(){ - Member member = getMember(); - Long memberId = member.getId(); - List productOrderList = productOrderRepository.findByMemberId(memberId); - return productOrderList - .stream() - .map(OrderResponse::fromEntity) - .collect(Collectors.toList() - ); - } - - @Description("멤버 id 기반으로 특정 orderId 들고오는 메서드") - public OrderResponse findOrderByMemberId(Long orderId){ - Member member = getMember(); - ProductOrder productOrder = productOrderRepository.findById(orderId) - .orElseThrow(()-> new BusinessException(ORDER_NOT_FOUND)); - - validateOrderOwnership(productOrder, member); - return OrderResponse.fromEntity(productOrder); - } - - - @Description("orderId 기반으로 order details(주소, 상세주소, 배송 요청 사항 ..등) 들고오는 메서드") - public OrderDetailsResponse findOrderDetailsByOrderId(Long orderId){ - ProductOrder productOrder = productOrderRepository.findById(orderId) - .orElseThrow(()-> new BusinessException(ORDER_NOT_FOUND)); - ProductOrderDetails productOrderDetails = productOrder.getProductOrderDetails(); - - return OrderDetailsResponse.fromEntity(productOrderDetails); - } - - - - @Description("orderId에 해당하는 order product details 가져오는 메서드") - public List findAllOrderProductDetails(Long orderId){ - Member member = getMember(); - ProductOrder productOrder = productOrderRepository.findById(orderId) - .orElseThrow(()-> new BusinessException(ORDER_NOT_FOUND)); - validateOrderOwnership(productOrder, member); - return productOrder.getOrderedProducts() - .stream() - .map(OrderProductDetailsResponse::fromEntity) - .collect(Collectors.toList() - ); - } - - - @Description("orderId에 해당하는 order product Details의 단건 조회") - public OrderProductDetailsResponse findOrderProductDetailsById(Long orderId, Long orderProductDetailsId){ - Member member = getMember(); - OrderedProduct orderedProduct = orderedProductRepository.findById(orderProductDetailsId) - .orElseThrow(()-> new BusinessException(ORDER_PRODUCT_DETAILS_NOT_FOUND)); - ProductOrder productOrder = orderedProduct.getProductOrder(); - - // productOrder product details의 주인 productOrder 검사 그리고 , orderId의 주인 member 검사 - validateOrderProductDetailsByOrderId(productOrder, orderId); - validateOrderOwnership(productOrder, member); - - return OrderProductDetailsResponse.fromEntity(orderedProduct); - } - - - @Description("member의 order인지 검사하는 메서드") - private void validateOrderOwnership(ProductOrder productOrder, Member member) { - if (!productOrder.getMember().getId().equals(member.getId())) { - throw new BusinessException(ORDER_NOT_OWNED_EXCEPTION); - } - } +public interface OrderService { - @Description("orderId에 해당하는 productOrder Product Details인지 조회하는 메서드") - private void validateOrderProductDetailsByOrderId(ProductOrder productOrder, Long orderId) { - if(productOrder.getId()!=orderId){ - throw new BusinessException(ORDER_PRODUCT_DETAILS_NOT_OWNED_EXCEPTION); - } - } - - - /* - @Description("결제가 완료 된 후, 주문 상태 변경하는 메서드. 굳이 없어도 되긴 함.") - private void completePaymentAndUpdateStatus(Long orderId){ - ProductOrder order = productOrderRepository.findById(orderId) - .orElseThrow(()-> new BusinessException(ORDER_NOT_FOUND)); - order.setOrderStatus(AWAITING_SELLER_CONFIRMATION); - }*/ - - @Description("주문 상태를 변경하는 메서드") - private void changeOrderStatus(Long orderId, OrderStatus orderStatus){ - ProductOrder productOrder = productOrderRepository.findById(orderId) - .orElseThrow(()-> new BusinessException(ORDER_NOT_FOUND)); - productOrder.setOrderStatus(orderStatus); - } - - - @Description("security context에서 member 객체 가져오는 메서드") - private Member getMember() { - Authentication authentication = SecurityContextHolder - .getContext().getAuthentication(); - Object impl = authentication.getPrincipal(); - Member member = ((UserDetailsImpl) impl).getMember(); - return member; - } - } - - diff --git a/src/main/java/poomasi/domain/order/service/ProductOrderService.java b/src/main/java/poomasi/domain/order/service/ProductOrderService.java new file mode 100644 index 00000000..5edd4359 --- /dev/null +++ b/src/main/java/poomasi/domain/order/service/ProductOrderService.java @@ -0,0 +1,224 @@ +package poomasi.domain.order.service; + +import jdk.jfr.Description; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import poomasi.domain.auth.security.userdetail.UserDetailsImpl; +import poomasi.domain.member.entity.Member; +import poomasi.domain.order._payment.dto.request.PaymentPreRegisterRequest; +import poomasi.domain.order.dto.request.ProductOrderRegisterRequest; +import poomasi.domain.order.dto.response.OrderDetailsResponse; +import poomasi.domain.order.dto.response.OrderProductDetailsResponse; +import poomasi.domain.order.dto.response.OrderResponse; +import poomasi.domain.order.entity._product.OrderedProduct; +import poomasi.domain.order.entity._product.ProductOrder; +import poomasi.domain.order.entity._product.ProductOrderDetails; +import poomasi.domain.order.entity.PaymentStatus; +import poomasi.domain.order.repository.OrderedProductRepository; +import poomasi.domain.order.repository.ProductOrderRepository; +import poomasi.domain.product._cart.entity.Cart; +import poomasi.domain.product._cart.repository.CartRepository; +import poomasi.domain.product.entity.Product; +import poomasi.domain.product.repository.ProductRepository; +import poomasi.global.error.BusinessException; + +import java.math.BigDecimal; +import java.util.List; +import java.util.stream.Collectors; + +import static poomasi.global.error.BusinessError.*; + +@RequiredArgsConstructor +@Service +@Slf4j +public class ProductOrderService { + + private final ProductOrderRepository productOrderRepository; + private final CartRepository cartRepository; + private final ProductRepository productRepository; + private final OrderedProductRepository orderedProductRepository; + + @Transactional + public PaymentPreRegisterRequest productPreOrderRegister(ProductOrderRegisterRequest productOrderRegisterRequest){ + Member member = getMember(); + Long memberId = member.getId(); + List cartList = cartRepository.findByMemberIdAndSelected(memberId); + + String destinationAddress = productOrderRegisterRequest.destinationAddress(); + String destinationAddressDetail = productOrderRegisterRequest.destinationAddressDetail(); + String deliveryRequest = productOrderRegisterRequest.deliveryRequest(); + + ProductOrder productOrder = new ProductOrder() + .builder() + .member(member) + .build(); + + ProductOrderDetails productOrderDetails = new ProductOrderDetails() + .builder() + .destinationAddress(destinationAddress) + .destinationAddressDetail(destinationAddressDetail) + .deliveryRequest(deliveryRequest) + .build(); + + productOrder.setProductOrderDetails(productOrderDetails); + + //cart에 있는 총 가격 계산하기 + BigDecimal totalPrice = BigDecimal.ZERO; + + // cart 돌면서 productOrder details 추가 + for (Cart cart : cartList) { + Long productId = cart.getProductId(); + Product product = productRepository.findById(productId) + .orElseThrow(() -> new BusinessException(PRODUCT_NOT_FOUND)); + + Integer productStock = product.getStock(); + Integer quantityInCart = cart.getCount(); + + // 현재 남아있는 재고보다 더 많이 요청하면 + // pending 상태로 저장이 안 됨. + if(quantityInCart > productStock){ + throw new BusinessException(PRODUCT_STOCK_ZERO); + } + + String productDescription = product.getDescription(); + Integer count = cart.getCount(); + String productName = product.getName(); + BigDecimal price = BigDecimal.valueOf(product.getPrice()); + + //TODO : Store store = product.getStore(); + + OrderedProduct orderedProduct = OrderedProduct + .builder() + .product(product) + .productOrder(productOrder) + //.store(store) + .productDescription(productDescription) + .productName(productName) + .price(price) + .count(count) + .build(); + + productOrder.addOrderedProduct(orderedProduct); + totalPrice = totalPrice.add(price); + } + productOrder.setTotalAmount(totalPrice); + productOrder.setCheckSum(totalPrice); + productOrderRepository.save(productOrder); + + String merchantUid = productOrder.getMerchantUid(); + return new PaymentPreRegisterRequest(merchantUid, totalPrice); + } + + @Transactional + //TODO : 만들어야 합니다 ~ + public PaymentPreRegisterRequest farmPreOrderRegister(){ + Member member = getMember(); + String merchantUid = ""; + BigDecimal totalPrice = BigDecimal.ZERO; + + return new PaymentPreRegisterRequest(merchantUid, totalPrice); + } + + + @Description("멤버 ID 기반으로 모든 order 다 들고 오는 메서드") + public List findAllOrdersByMemberId(){ + Member member = getMember(); + Long memberId = member.getId(); + List productOrderList = productOrderRepository.findByMemberId(memberId); + return productOrderList + .stream() + .map(OrderResponse::fromEntity) + .collect(Collectors.toList() + ); + } + + @Description("멤버 id 기반으로 특정 orderId 들고오는 메서드") + public OrderResponse findOrderByMemberId(Long orderId){ + Member member = getMember(); + ProductOrder productOrder = productOrderRepository.findById(orderId) + .orElseThrow(()-> new BusinessException(ORDER_NOT_FOUND)); + + validateOrderOwnership(productOrder, member); + return OrderResponse.fromEntity(productOrder); + } + + + @Description("orderId 기반으로 order details(주소, 상세주소, 배송 요청 사항 ..등) 들고오는 메서드") + public OrderDetailsResponse findOrderDetailsByOrderId(Long orderId){ + ProductOrder productOrder = productOrderRepository.findById(orderId) + .orElseThrow(()-> new BusinessException(ORDER_NOT_FOUND)); + ProductOrderDetails productOrderDetails = productOrder.getProductOrderDetails(); + + return OrderDetailsResponse.fromEntity(productOrderDetails); + } + + + + @Description("orderId에 해당하는 order product details 가져오는 메서드") + public List findAllOrderProductDetails(Long orderId){ + Member member = getMember(); + ProductOrder productOrder = productOrderRepository.findById(orderId) + .orElseThrow(()-> new BusinessException(ORDER_NOT_FOUND)); + validateOrderOwnership(productOrder, member); + return productOrder.getOrderedProducts() + .stream() + .map(OrderProductDetailsResponse::fromEntity) + .collect(Collectors.toList() + ); + } + + + @Description("orderId에 해당하는 order product Details의 단건 조회") + public OrderProductDetailsResponse findOrderProductDetailsById(Long orderId, Long orderProductDetailsId){ + Member member = getMember(); + OrderedProduct orderedProduct = orderedProductRepository.findById(orderProductDetailsId) + .orElseThrow(()-> new BusinessException(ORDER_PRODUCT_DETAILS_NOT_FOUND)); + ProductOrder productOrder = orderedProduct.getProductOrder(); + + // productOrder product details의 주인 productOrder 검사 그리고 , orderId의 주인 member 검사 + validateOrderProductDetailsByOrderId(productOrder, orderId); + validateOrderOwnership(productOrder, member); + + return OrderProductDetailsResponse.fromEntity(orderedProduct); + } + + + @Description("member의 order인지 검사하는 메서드") + private void validateOrderOwnership(ProductOrder productOrder, Member member) { + if (!productOrder.getMember().getId().equals(member.getId())) { + throw new BusinessException(ORDER_NOT_OWNED_EXCEPTION); + } + } + + @Description("orderId에 해당하는 productOrder Product Details인지 조회하는 메서드") + private void validateOrderProductDetailsByOrderId(ProductOrder productOrder, Long orderId) { + if(productOrder.getId()!=orderId){ + throw new BusinessException(ORDER_PRODUCT_DETAILS_NOT_OWNED_EXCEPTION); + } + } + + + @Description("주문 상태를 변경하는 메서드") + private void changeOrderStatus(Long orderId, PaymentStatus paymentStatus){ + ProductOrder productOrder = productOrderRepository.findById(orderId) + .orElseThrow(()-> new BusinessException(ORDER_NOT_FOUND)); + productOrder.setPaymentStatus(paymentStatus); + } + + + @Description("security context에서 member 객체 가져오는 메서드") + private Member getMember() { + Authentication authentication = SecurityContextHolder + .getContext().getAuthentication(); + Object impl = authentication.getPrincipal(); + Member member = ((UserDetailsImpl) impl).getMember(); + return member; + } + +} + + diff --git a/src/main/java/poomasi/global/error/BusinessError.java b/src/main/java/poomasi/global/error/BusinessError.java index b177035e..6e5cfa81 100644 --- a/src/main/java/poomasi/global/error/BusinessError.java +++ b/src/main/java/poomasi/global/error/BusinessError.java @@ -84,17 +84,28 @@ public enum BusinessError { ORDER_NOT_OWNED_EXCEPTION(HttpStatus.UNAUTHORIZED, "허가되지 않은 주문입니다."), ORDER_PRODUCT_DETAILS_NOT_FOUND(HttpStatus.NOT_FOUND, "주문을 찾을 수 없습니다."), ORDER_PRODUCT_DETAILS_NOT_OWNED_EXCEPTION(HttpStatus.UNAUTHORIZED, "허가되지 않은 주문입니다."), + ORDERED_PRODUCT_NOT_FOUND(HttpStatus.NOT_FOUND, "찾을 수 없는 주문입니다."), + // PAYMENT PAYMENT_NOT_FOUND(HttpStatus.NOT_FOUND, "결제를 찾을 수 없습니다."), PAYMENT_AMOUNT_MISMATCH(HttpStatus.BAD_REQUEST, "사전 결제 금액과 사후 결제 금액이 일치하지 않습니다."), PAYMENT_BAD_REQUEST(HttpStatus.BAD_REQUEST, "잘못 된 결제 요청입니다."), + CHECKSUM_EXCESSIVE_REFUND_AMOUNT(HttpStatus.BAD_REQUEST, "환불 요청 금액이 환불 가능한 금액보다 더 많습니다"), //Store - STORE_NOT_FOUND(HttpStatus.NOT_FOUND, "등록된 상점이 없습니다.") - - - + STORE_NOT_FOUND(HttpStatus.NOT_FOUND, "등록된 상점이 없습니다."), + + // After sales + SHIPPING_ALREADY_IN_PROGRESS(HttpStatus.BAD_REQUEST, "배송 준비 중이거나 배송 중인 주문입니다."), + EXCHANGE_NOT_ALLOWED_IN_TRANSIT(HttpStatus.BAD_REQUEST, "배송 중인 상품에 대해선 교환 요청을 할 수 없습니다."), + SHIPPING_COST_GREATER_THAN_REFUND(HttpStatus.BAD_REQUEST, "배송비가 환불 금액보다 더 큽니다."), + CANCEL_QUANTITY_EXCEEDED(HttpStatus.BAD_REQUEST, "취소 가능한 수량을 초과한 요청입니다."), + REFUND_QUANTITY_EXCEEDED(HttpStatus.BAD_REQUEST, "환불 가능한 수량을 초과한 요청입니다."), + PURCHASE_ALREADY_CONFIRMED(HttpStatus.BAD_REQUEST, "이미 구매 확정이 된 상태입니다."), + REFUND_NOT_ALLOWED_BEFORE_SHIPPING(HttpStatus.BAD_REQUEST, "배송 대기 전 상태에서는 환불을 요청할 수 없습니다."), + REFUND_AFTER_SALES_NOT_FOUND(HttpStatus.NOT_FOUND , "찾을 수 없는 환불 요청입니다."), + REFUND_AFTER_SALES_REQUEST_INVALID_OWNER(HttpStatus.BAD_REQUEST, "판매자의 환불 요청이 아닙니다."), ; private final HttpStatus httpStatus; diff --git a/src/main/java/poomasi/global/error/ExceptionAdvice.java b/src/main/java/poomasi/global/error/ExceptionAdvice.java index 7dc73824..66dc848d 100644 --- a/src/main/java/poomasi/global/error/ExceptionAdvice.java +++ b/src/main/java/poomasi/global/error/ExceptionAdvice.java @@ -1,11 +1,15 @@ package poomasi.global.error; +import com.siot.IamportRestClient.exception.IamportResponseException; import lombok.extern.slf4j.Slf4j; import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; import org.springframework.web.ErrorResponse; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.RestControllerAdvice; +import java.io.IOException; + @Slf4j @RestControllerAdvice public class ExceptionAdvice { @@ -41,4 +45,24 @@ public ErrorResponse paymentConfirmExceptionHandler(PaymentConfirmException exce .build(); } + @ExceptionHandler(IamportResponseException.class) + public ErrorResponse IamportResponseExceptionHandler(IamportResponseException exception) { + + log.error("[{}] : {}", "IamportResponseException", exception.getMessage()); + return ErrorResponse + .builder(exception, HttpStatus.BAD_GATEWAY, exception.getMessage()) + .title("아임포트 서버 응답 장애 발생") + .build(); + } + + @ExceptionHandler(IOException.class) + public ErrorResponse IamportResponseExceptionHandler(IOException exception) { + log.error("[{}] : {}", "IOException", exception.getMessage()); + return ErrorResponse + .builder(exception, HttpStatus.BAD_GATEWAY, exception.getMessage()) + .title("통신 도중 IOException 발생") + .build(); + } + + }